import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/services.dart'; class TrustAPPHttpdns { static const MethodChannel _channel = MethodChannel('TrustAPP_httpdns'); /// Initialize the SDK. /// [apiUrl] is the unified endpoint URL, e.g. "https://httpdns.example.com:8445". /// [appId] and [secretKey] are required for authentication. static Future init({ required String appId, String? apiUrl, @Deprecated('Use apiUrl instead') String? primaryServiceHost, @Deprecated('Use apiUrl instead') String? backupServiceHost, @Deprecated('Use apiUrl instead') int servicePort = 443, String? secretKey, }) async { final String normalizedAppId = appId.trim(); if (normalizedAppId.isEmpty) { return false; } final Map args = { 'appId': normalizedAppId, if (apiUrl != null && apiUrl.trim().isNotEmpty) 'apiUrl': apiUrl.trim(), if (primaryServiceHost != null && primaryServiceHost.trim().isNotEmpty) 'primaryServiceHost': primaryServiceHost.trim(), if (backupServiceHost != null && backupServiceHost.trim().isNotEmpty) 'backupServiceHost': backupServiceHost.trim(), 'servicePort': servicePort, if (secretKey != null && secretKey.isNotEmpty) 'secretKey': secretKey, }; final bool? ok = await _channel.invokeMethod('initialize', args); return ok ?? false; } static Future build() async { final bool? ok = await _channel.invokeMethod('build'); return ok ?? false; } static Future setLogEnabled(bool enabled) async { await _channel.invokeMethod('setLogEnabled', {'enabled': enabled}); } static Future setPersistentCacheIPEnabled(bool enabled, {int? discardExpiredAfterSeconds}) async { await _channel.invokeMethod('setPersistentCacheIPEnabled', { 'enabled': enabled, if (discardExpiredAfterSeconds != null) 'discardExpiredAfterSeconds': discardExpiredAfterSeconds, }); } static Future setReuseExpiredIPEnabled(bool enabled) async { await _channel .invokeMethod('setReuseExpiredIPEnabled', {'enabled': enabled}); } static Future setHttpsRequestEnabled(bool enabled) async { await _channel .invokeMethod('setHttpsRequestEnabled', {'enabled': enabled}); } static Future>> resolveHostSyncNonBlocking( String hostname, { String ipType = 'auto', // auto/ipv4/ipv6/both }) async { final Map? res = await _channel.invokeMethod>('resolveHostSyncNonBlocking', { 'hostname': hostname, 'ipType': ipType, }); final Map> out = >{ 'ipv4': [], '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(); } if (v6 is List) { out['ipv6'] = v6.map((e) => e.toString()).toList(); } return out; } /// V1 resolve API: /// qtype supports A / AAAA, optional cip for route simulation. static Future> resolveHost( String hostname, { String qtype = 'A', String? cip, }) async { final Map? res = await _channel.invokeMethod>('resolveHostV1', { 'hostname': hostname, 'qtype': qtype, if (cip != null && cip.trim().isNotEmpty) 'cip': cip.trim(), }); final Map out = { 'ipv4': [], 'ipv6': [], '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; } static Future setPreResolveHosts(List hosts, {String ipType = 'auto'}) async { await _channel.invokeMethod('setPreResolveHosts', { 'hosts': hosts, 'ipType': ipType, }); } static Future setPreResolveAfterNetworkChanged(bool enabled) async { await _channel.invokeMethod('setPreResolveAfterNetworkChanged', { 'enabled': enabled, }); } static Future setIPRankingList(Map hostPortMap) async { await _channel .invokeMethod('setIPRankingList', {'hostPortMap': hostPortMap}); } static Future getSessionId() async { return _channel.invokeMethod('getSessionId'); } static Future cleanAllHostCache() async { await _channel.invokeMethod('cleanAllHostCache'); } static TrustAPPHttpdnsHttpAdapter createHttpAdapter({ TrustAPPHttpdnsAdapterOptions options = const TrustAPPHttpdnsAdapterOptions(), }) { return TrustAPPHttpdnsHttpAdapter._(options); } } class TrustAPPHttpdnsAdapterOptions { final String ipType; final int connectTimeoutMs; final int readTimeoutMs; final bool allowInsecureCertificatesForDebugOnly; const TrustAPPHttpdnsAdapterOptions({ this.ipType = 'auto', this.connectTimeoutMs = 3000, this.readTimeoutMs = 5000, this.allowInsecureCertificatesForDebugOnly = false, }); } class TrustAPPHttpdnsRequestResult { final int statusCode; final Map> headers; final Uint8List body; final String usedIp; const TrustAPPHttpdnsRequestResult({ required this.statusCode, required this.headers, required this.body, required this.usedIp, }); } class TrustAPPHttpdnsHttpAdapter { final TrustAPPHttpdnsAdapterOptions _options; TrustAPPHttpdnsHttpAdapter._(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 request( Uri uri, { String method = 'GET', Map? headers, List? 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> resolved = await TrustAPPHttpdns.resolveHostSyncNonBlocking( uri.host, ipType: _options.ipType, ); final List ips = [ ...?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 payload = await resp.fold>([], (List previous, List element) { previous.addAll(element); return previous; }); final Map> responseHeaders = >{}; resp.headers.forEach((String name, List values) { responseHeaders[name] = values; }); return TrustAPPHttpdnsRequestResult( 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'); } }