import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import '../core/utils/app_logger.dart'; class HostMappingProxyService { HostMappingProxyService._(); static final HostMappingProxyService instance = HostMappingProxyService._(); ServerSocket? _server; HostMappingProxyEndpoint? _endpoint; Map _hostMap = const {}; HostMappingProxyEndpoint? get endpoint => _hostMap.isEmpty ? null : _endpoint; Future configure( Map hostMap, ) async { final cleaned = {}; for (final entry in hostMap.entries) { final host = _normalizeHost(entry.key); final ip = entry.value.trim(); if (host.isNotEmpty && ip.isNotEmpty) { cleaned[host] = ip; } } if (cleaned.isEmpty) { await stop(); return null; } _hostMap = cleaned; if (_server == null) { _server = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); _endpoint = HostMappingProxyEndpoint( host: InternetAddress.loopbackIPv4.address, port: _server!.port, ); _server!.listen( _handleClient, onError: (e) { AppLogger.d('自定义线路本地代理监听错误: $e'); }, ); AppLogger.d('自定义线路本地代理已启动: ${_endpoint!.proxyUri}'); } return _endpoint; } Future stop() async { _hostMap = const {}; final server = _server; _server = null; _endpoint = null; if (server != null) { await server.close(); AppLogger.d('自定义线路本地代理已关闭'); } } void _handleClient(Socket client) { unawaited(_serveClient(client)); } Future _serveClient(Socket client) async { Socket? remote; StreamController>? rest; try { client.setOption(SocketOption.tcpNoDelay, true); final initial = await _readInitialRequest(client); rest = initial.rest; final lines = latin1.decode(initial.header).split('\r\n'); if (lines.isEmpty || lines.first.trim().isEmpty) { throw const FormatException('empty proxy request'); } final first = lines.first.split(' '); if (first.length < 3) { throw FormatException('invalid proxy request: ${lines.first}'); } final method = first[0].toUpperCase(); if (method == 'CONNECT') { final target = _splitAuthority(first[1], 443); remote = await _connectMapped(target.host, target.port); client.add(utf8.encode('HTTP/1.1 200 Connection Established\r\n\r\n')); await client.flush(); } else { final target = _httpTarget(first[1], lines); remote = await _connectMapped(target.host, target.port); remote.add(_rewriteHttpHeader(initial.header, target.requestTarget)); } remote.setOption(SocketOption.tcpNoDelay, true); unawaited(rest.stream.pipe(remote).catchError((_) {})); unawaited(remote.cast>().pipe(client).catchError((_) {})); } catch (e) { AppLogger.d('自定义线路本地代理请求失败: $e'); try { client.add(utf8.encode('HTTP/1.1 502 Bad Gateway\r\n\r\n')); await client.flush(); } catch (_) {} await rest?.close(); remote?.destroy(); client.destroy(); } } Future<_ProxyInitialRequest> _readInitialRequest(Socket socket) { final completer = Completer<_ProxyInitialRequest>(); final builder = BytesBuilder(copy: false); final rest = StreamController>(sync: true); late StreamSubscription subscription; subscription = socket.listen( (chunk) { if (completer.isCompleted) { rest.add(chunk); return; } builder.add(chunk); final bytes = builder.toBytes(); final end = _headerEnd(bytes); if (end >= 0) { final headerEnd = end + 4; completer.complete( _ProxyInitialRequest( header: bytes.sublist(0, headerEnd), rest: rest, ), ); if (bytes.length > headerEnd) { rest.add(bytes.sublist(headerEnd)); } } else if (bytes.length > 64 * 1024) { completer.completeError( const FormatException('proxy request header too large'), ); unawaited(subscription.cancel()); unawaited(rest.close()); } }, onError: (Object error, StackTrace stackTrace) { if (!completer.isCompleted) { completer.completeError(error, stackTrace); } else { rest.addError(error, stackTrace); } }, onDone: () { if (!completer.isCompleted) { completer.completeError(const SocketException('proxy client closed')); } unawaited(rest.close()); }, cancelOnError: false, ); return completer.future; } int _headerEnd(List bytes) { for (var i = 0; i <= bytes.length - 4; i++) { if (bytes[i] == 13 && bytes[i + 1] == 10 && bytes[i + 2] == 13 && bytes[i + 3] == 10) { return i; } } return -1; } Future _connectMapped(String host, int port) { final normalized = _normalizeHost(host); final targetHost = _hostMap[normalized] ?? host; return Socket.connect( targetHost, port, timeout: const Duration(seconds: 12), ); } _ProxyTarget _httpTarget(String target, List lines) { final uri = Uri.tryParse(target); if (uri != null && uri.hasScheme && uri.host.isNotEmpty) { return _ProxyTarget( host: uri.host, port: uri.hasPort ? uri.port : (uri.scheme == 'https' ? 443 : 80), requestTarget: _originForm(uri), ); } final hostHeader = _headerValue(lines, 'Host'); final authority = _splitAuthority(hostHeader, 80); return _ProxyTarget( host: authority.host, port: authority.port, requestTarget: target, ); } List _rewriteHttpHeader(List header, String requestTarget) { final text = latin1.decode(header); final lines = text.split('\r\n'); final first = lines.first.split(' '); first[1] = requestTarget.isEmpty ? '/' : requestTarget; lines[0] = first.join(' '); final filtered = lines .where((line) => !line.toLowerCase().startsWith('proxy-connection:')) .join('\r\n'); return latin1.encode(filtered); } String _originForm(Uri uri) { final path = uri.path.isEmpty ? '/' : uri.path; return uri.hasQuery ? '$path?${uri.query}' : path; } String _headerValue(List lines, String name) { final prefix = '${name.toLowerCase()}:'; for (final line in lines.skip(1)) { if (line.toLowerCase().startsWith(prefix)) { return line.substring(line.indexOf(':') + 1).trim(); } } throw FormatException('missing $name header'); } _ProxyAuthority _splitAuthority(String value, int defaultPort) { final authority = value.trim(); if (authority.startsWith('[')) { final end = authority.indexOf(']'); if (end > 0) { final host = authority.substring(1, end); final port = authority.length > end + 2 ? int.tryParse(authority.substring(end + 2)) ?? defaultPort : defaultPort; return _ProxyAuthority(host, port); } } final colon = authority.lastIndexOf(':'); if (colon > 0 && colon < authority.length - 1) { final port = int.tryParse(authority.substring(colon + 1)); if (port != null) { return _ProxyAuthority(authority.substring(0, colon), port); } } return _ProxyAuthority(authority, defaultPort); } String _normalizeHost(String host) { final value = host.trim(); final uri = Uri.tryParse(value); final resolved = uri != null && uri.hasScheme && uri.host.isNotEmpty ? uri.host : value; return resolved.toLowerCase().replaceAll(RegExp(r'\.$'), ''); } } class HostMappingProxyEndpoint { final String host; final int port; const HostMappingProxyEndpoint({required this.host, required this.port}); String get proxyUri => 'http://$host:$port'; } class HostMappingHttpOverrides extends HttpOverrides { @override HttpClient createHttpClient(SecurityContext? context) { final client = super.createHttpClient(context); client.findProxy = (_) { final endpoint = HostMappingProxyService.instance.endpoint; if (endpoint == null) return 'DIRECT'; return 'PROXY ${endpoint.host}:${endpoint.port}; DIRECT'; }; return client; } } class _ProxyInitialRequest { final Uint8List header; final StreamController> rest; const _ProxyInitialRequest({required this.header, required this.rest}); } class _ProxyAuthority { final String host; final int port; const _ProxyAuthority(this.host, this.port); } class _ProxyTarget { final String host; final int port; final String requestTarget; const _ProxyTarget({ required this.host, required this.port, required this.requestTarget, }); }