Files
app/lib/services/host_mapping_proxy_service.dart

315 lines
9.0 KiB
Dart

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<String, String> _hostMap = const {};
HostMappingProxyEndpoint? get endpoint => _hostMap.isEmpty ? null : _endpoint;
Future<HostMappingProxyEndpoint?> configure(
Map<String, String> hostMap,
) async {
final cleaned = <String, String>{};
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<void> 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<void> _serveClient(Socket client) async {
Socket? remote;
StreamController<List<int>>? 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<List<int>>().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<List<int>>(sync: true);
late StreamSubscription<Uint8List> 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<int> 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<Socket> _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<String> 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<int> _rewriteHttpHeader(List<int> 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<String> 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<List<int>> 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,
});
}