管理端全部功能跑通

This commit is contained in:
robin
2026-02-27 10:35:22 +08:00
parent 4d275c921d
commit 150799f41d
263 changed files with 22664 additions and 4053 deletions

View File

@@ -1,87 +1,89 @@
import 'dart:async';
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/services.dart';
class AliyunHttpdns {
static const MethodChannel _channel = MethodChannel('aliyun_httpdns');
/// 1) 初始化:使用 accountId/secretKey/aesSecretKey
/// New API only:
/// appId + primary/backup service host + optional sign secret.
static Future<bool> init({
required int accountId,
required String appId,
required String primaryServiceHost,
String? backupServiceHost,
int servicePort = 443,
String? secretKey,
String? aesSecretKey,
}) async {
final ok =
await _channel.invokeMethod<bool>('initialize', <String, dynamic>{
'accountId': accountId,
if (secretKey != null) 'secretKey': secretKey,
if (aesSecretKey != null) 'aesSecretKey': aesSecretKey,
});
final String normalizedAppId = appId.trim();
final String normalizedPrimary = primaryServiceHost.trim();
if (normalizedAppId.isEmpty || normalizedPrimary.isEmpty) {
return false;
}
final Map<String, dynamic> args = <String, dynamic>{
'appId': normalizedAppId,
'primaryServiceHost': normalizedPrimary,
if (backupServiceHost != null && backupServiceHost.trim().isNotEmpty)
'backupServiceHost': backupServiceHost.trim(),
if (servicePort > 0) 'servicePort': servicePort,
if (secretKey != null && secretKey.isNotEmpty) 'secretKey': secretKey,
};
final bool? ok = await _channel.invokeMethod<bool>('initialize', args);
return ok ?? false;
}
/// 构建底层 service只有在调用了 initialize / 一系列 setXxx 后,
/// 调用本方法才会真正创建底层实例并应用配置
static Future<bool> build() async {
final ok = await _channel.invokeMethod<bool>('build');
final bool? ok = await _channel.invokeMethod<bool>('build');
return ok ?? false;
}
/// 2) 设置日志开关
static Future<void> setLogEnabled(bool enabled) async {
await _channel.invokeMethod<void>('setLogEnabled', <String, dynamic>{
'enabled': enabled,
});
await _channel.invokeMethod<void>('setLogEnabled', <String, dynamic>{'enabled': enabled});
}
/// 3) 设置持久化缓存
static Future<void> setPersistentCacheIPEnabled(bool enabled,
{int? discardExpiredAfterSeconds}) async {
await _channel
.invokeMethod<void>('setPersistentCacheIPEnabled', <String, dynamic>{
await _channel.invokeMethod<void>('setPersistentCacheIPEnabled', <String, dynamic>{
'enabled': enabled,
if (discardExpiredAfterSeconds != null)
'discardExpiredAfterSeconds': discardExpiredAfterSeconds,
});
}
/// 4) 是否允许复用过期 IP
static Future<void> setReuseExpiredIPEnabled(bool enabled) async {
await _channel
.invokeMethod<void>('setReuseExpiredIPEnabled', <String, dynamic>{
'enabled': enabled,
});
.invokeMethod<void>('setReuseExpiredIPEnabled', <String, dynamic>{'enabled': enabled});
}
/// 设置是否使用 HTTPS 解析链路,避免明文流量被系统拦截
static Future<void> setHttpsRequestEnabled(bool enabled) async {
await _channel
.invokeMethod<void>('setHttpsRequestEnabled', <String, dynamic>{
'enabled': enabled,
});
.invokeMethod<void>('setHttpsRequestEnabled', <String, dynamic>{'enabled': enabled});
}
/// 5) 伪异步解析:返回 IPv4/IPv6 数组
/// 返回格式:{"ipv4": `List<String>`, "ipv6": `List<String>`}
static Future<Map<String, List<String>>> resolveHostSyncNonBlocking(
String hostname, {
String ipType = 'auto', // auto/ipv4/ipv6/both
Map<String, String>? sdnsParams,
String? cacheKey,
}) async {
final Map<dynamic, dynamic>? res = await _channel
.invokeMethod('resolveHostSyncNonBlocking', <String, dynamic>{
final Map<dynamic, dynamic>? res =
await _channel.invokeMethod<Map<dynamic, dynamic>>('resolveHostSyncNonBlocking',
<String, dynamic>{
'hostname': hostname,
'ipType': ipType,
if (sdnsParams != null) 'sdnsParams': sdnsParams,
if (cacheKey != null) 'cacheKey': cacheKey,
});
final Map<String, List<String>> out = {
final Map<String, List<String>> out = <String, List<String>>{
'ipv4': <String>[],
'ipv6': <String>[],
};
if (res == null) return out;
final v4 = res['ipv4'];
final v6 = res['ipv6'];
if (res == null) {
return out;
}
final dynamic v4 = res['ipv4'];
final dynamic v6 = res['ipv6'];
if (v4 is List) {
out['ipv4'] = v4.map((e) => e.toString()).toList();
}
@@ -91,51 +93,190 @@ class AliyunHttpdns {
return out;
}
// 解析域名,返回 A/AAAA 记录等(保留旧接口以兼容,未在本任务使用)
static Future<Map<String, dynamic>?> resolve(String hostname,
{Map<String, dynamic>? options}) async {
final res = await _channel.invokeMethod<Map<dynamic, dynamic>>('resolve', {
/// V1 resolve API:
/// qtype supports A / AAAA, optional cip for route simulation.
static Future<Map<String, dynamic>> resolveHost(
String hostname, {
String qtype = 'A',
String? cip,
}) async {
final Map<dynamic, dynamic>? res =
await _channel.invokeMethod<Map<dynamic, dynamic>>('resolveHostV1', <String, dynamic>{
'hostname': hostname,
if (options != null) 'options': options,
'qtype': qtype,
if (cip != null && cip.trim().isNotEmpty) 'cip': cip.trim(),
});
return res?.map((key, value) => MapEntry(key.toString(), value));
final Map<String, dynamic> out = <String, dynamic>{
'ipv4': <String>[],
'ipv6': <String>[],
'ttl': 0,
};
if (res == null) {
return out;
}
final dynamic v4 = res['ipv4'];
final dynamic v6 = res['ipv6'];
if (v4 is List) {
out['ipv4'] = v4.map((e) => e.toString()).toList();
}
if (v6 is List) {
out['ipv6'] = v6.map((e) => e.toString()).toList();
}
final dynamic ttl = res['ttl'];
if (ttl is int) {
out['ttl'] = ttl;
}
return out;
}
// 1) setPreResolveHosts: 传入 host 列表native 侧调用 SDK 预解析
static Future<void> setPreResolveHosts(List<String> hosts,
{String ipType = 'auto'}) async {
static Future<void> setPreResolveHosts(List<String> hosts, {String ipType = 'auto'}) async {
await _channel.invokeMethod<void>('setPreResolveHosts', <String, dynamic>{
'hosts': hosts,
'ipType': ipType,
});
}
// 2) setLogEnabled: 已有,同步保留(在此文件顶部已有 setLogEnabled 实现)
// 3) setPreResolveAfterNetworkChanged: 是否在网络切换时自动刷新解析
static Future<void> setPreResolveAfterNetworkChanged(bool enabled) async {
await _channel.invokeMethod<void>(
'setPreResolveAfterNetworkChanged', <String, dynamic>{
await _channel.invokeMethod<void>('setPreResolveAfterNetworkChanged', <String, dynamic>{
'enabled': enabled,
});
}
// 4) getSessionId: 获取会话 id
static Future<String?> getSessionId() async {
final sid = await _channel.invokeMethod<String>('getSessionId');
return sid;
static Future<void> setIPRankingList(Map<String, int> hostPortMap) async {
await _channel
.invokeMethod<void>('setIPRankingList', <String, dynamic>{'hostPortMap': hostPortMap});
}
static Future<String?> getSessionId() async {
return _channel.invokeMethod<String>('getSessionId');
}
// 5) cleanAllHostCache: 清除所有缓存
static Future<void> cleanAllHostCache() async {
await _channel.invokeMethod<void>('cleanAllHostCache');
}
/// 设置 IP 优选列表
/// [hostPortMap] 域名和端口的映射,例如:{'www.aliyun.com': 443}
static Future<void> setIPRankingList(Map<String, int> hostPortMap) async {
await _channel.invokeMethod<void>('setIPRankingList', <String, dynamic>{
'hostPortMap': hostPortMap,
});
static AliyunHttpdnsHttpAdapter createHttpAdapter({
AliyunHttpdnsAdapterOptions options = const AliyunHttpdnsAdapterOptions(),
}) {
return AliyunHttpdnsHttpAdapter._(options);
}
}
class AliyunHttpdnsAdapterOptions {
final String ipType;
final int connectTimeoutMs;
final int readTimeoutMs;
final bool allowInsecureCertificatesForDebugOnly;
const AliyunHttpdnsAdapterOptions({
this.ipType = 'auto',
this.connectTimeoutMs = 3000,
this.readTimeoutMs = 5000,
this.allowInsecureCertificatesForDebugOnly = false,
});
}
class AliyunHttpdnsRequestResult {
final int statusCode;
final Map<String, List<String>> headers;
final Uint8List body;
final String usedIp;
const AliyunHttpdnsRequestResult({
required this.statusCode,
required this.headers,
required this.body,
required this.usedIp,
});
}
class AliyunHttpdnsHttpAdapter {
final AliyunHttpdnsAdapterOptions _options;
AliyunHttpdnsHttpAdapter._(this._options);
/// Fixed behavior:
/// 1) resolve host by HTTPDNS
/// 2) connect to IP:443
/// 3) keep HTTP Host as original domain
/// 4) no fallback to domain connect
Future<AliyunHttpdnsRequestResult> request(
Uri uri, {
String method = 'GET',
Map<String, String>? headers,
List<int>? body,
}) async {
if (uri.host.isEmpty) {
throw const HttpException('HOST_ROUTE_REJECTED: host is empty');
}
if (uri.scheme.toLowerCase() != 'https') {
throw const HttpException('TLS_EMPTY_SNI_FAILED: only https is supported');
}
final Map<String, List<String>> resolved = await AliyunHttpdns.resolveHostSyncNonBlocking(
uri.host,
ipType: _options.ipType,
);
final List<String> ips = <String>[
...?resolved['ipv4'],
...?resolved['ipv6'],
];
if (ips.isEmpty) {
throw const HttpException('NO_IP_AVAILABLE: HTTPDNS returned empty ip list');
}
Object? lastError;
for (final String ip in ips) {
final HttpClient client = HttpClient();
client.connectionTimeout = Duration(milliseconds: _options.connectTimeoutMs);
if (_options.allowInsecureCertificatesForDebugOnly) {
client.badCertificateCallback = (_, __, ___) => true;
}
try {
final Uri target = uri.replace(
host: ip,
port: uri.hasPort ? uri.port : 443,
);
final HttpClientRequest req = await client
.openUrl(method, target)
.timeout(Duration(milliseconds: _options.connectTimeoutMs));
req.headers.host = uri.host;
headers?.forEach((String key, String value) {
if (key.toLowerCase() == 'host') {
return;
}
req.headers.set(key, value);
});
if (body != null && body.isNotEmpty) {
req.add(body);
}
final HttpClientResponse resp =
await req.close().timeout(Duration(milliseconds: _options.readTimeoutMs));
final List<int> payload =
await resp.fold<List<int>>(<int>[], (List<int> previous, List<int> element) {
previous.addAll(element);
return previous;
});
final Map<String, List<String>> responseHeaders = <String, List<String>>{};
resp.headers.forEach((String name, List<String> values) {
responseHeaders[name] = values;
});
return AliyunHttpdnsRequestResult(
statusCode: resp.statusCode,
headers: responseHeaders,
body: Uint8List.fromList(payload),
usedIp: ip,
);
} catch (e) {
lastError = e;
} finally {
client.close(force: true);
}
}
throw HttpException('TLS_EMPTY_SNI_FAILED: all ip connect attempts failed, error=$lastError');
}
}