diff --git a/lib/network/bin/configuration.dart b/lib/network/bin/configuration.dart index 65ebefe..c0f9579 100644 --- a/lib/network/bin/configuration.dart +++ b/lib/network/bin/configuration.dart @@ -37,6 +37,9 @@ class Configuration { //代理忽略域名 String proxyPassDomains = SystemProxy.proxyPassDomains; + //enabled socks5 proxy + bool enableSocks5 = true; + //外部代理 ProxyInfo? externalProxy; @@ -84,6 +87,7 @@ class Configuration { enableSsl = config['enableSsl'] == true; startup = config['startup'] ?? Platforms.isDesktop(); enableSystemProxy = config['enableSystemProxy'] ?? (config['enableDesktop'] ?? true); + enableSocks5 = config['enableSocks5'] ?? true; proxyPassDomains = config['proxyPassDomains'] ?? SystemProxy.proxyPassDomains; historyCacheTime = config['historyCacheTime'] ?? 0; if (config['externalProxy'] != null) { @@ -136,6 +140,7 @@ class Configuration { 'enableSsl': enableSsl, 'startup': startup, 'enableSystemProxy': enableSystemProxy, + 'enableSocks5': enableSocks5, 'proxyPassDomains': proxyPassDomains, 'externalProxy': externalProxy?.toJson(), 'appWhitelist': appWhitelist, diff --git a/lib/network/channel.dart b/lib/network/channel.dart index 388b239..e1f8d0a 100644 --- a/lib/network/channel.dart +++ b/lib/network/channel.dart @@ -107,7 +107,7 @@ class Channel { bool get isSsl => _socket is SecureSocket; Future write(Object obj) async { - var data = pipeline._encoder.encode(obj); + var data = pipeline.encoder.encode(obj); await writeBytes(data); } @@ -251,18 +251,22 @@ class ChannelContext { } class ChannelPipeline extends ChannelHandler { - late Decoder _decoder; - late Encoder _encoder; + late Decoder decoder; + late Encoder encoder; late ChannelHandler handler; final ByteBuf buffer = ByteBuf(); handle(Decoder decoder, Encoder encoder, ChannelHandler handler) { - _encoder = encoder; - _decoder = decoder; + this.encoder = encoder; + this.decoder = decoder; this.handler = handler; } + channelHandle(Codec codec, ChannelHandler handler) { + handle(codec, codec, handler); + } + /// 监听 void listen(Channel channel, ChannelContext channelContext) { buffer.clear(); @@ -297,8 +301,8 @@ class ChannelPipeline extends ChannelHandler { /// 转发请求 void relay(ChannelContext channelContext, Channel clientChannel, Channel remoteChannel) { var rawCodec = RawCodec(); - clientChannel.pipeline.handle(rawCodec, rawCodec, RelayHandler(remoteChannel)); - remoteChannel.pipeline.handle(rawCodec, rawCodec, RelayHandler(clientChannel)); + clientChannel.pipeline.channelHandle(rawCodec, RelayHandler(remoteChannel)); + remoteChannel.pipeline.channelHandle(rawCodec, RelayHandler(clientChannel)); var body = buffer.bytes; buffer.clear(); @@ -326,7 +330,7 @@ class ChannelPipeline extends ChannelHandler { return; } - var decodeResult = _decoder.decode(channelContext, buffer); + var decodeResult = decoder.decode(channelContext, buffer); if (!decodeResult.isDone) { return; } @@ -376,7 +380,8 @@ class ChannelPipeline extends ChannelHandler { //websocket协议 if (data is HttpResponse && data.isWebSocket && remoteChannel != null) { data.request?.response = data; - channelContext.host = channelContext.host?.copyWith(scheme: channel.isSsl ? HostAndPort.wssScheme : HostAndPort.wsScheme); + channelContext.host = + channelContext.host?.copyWith(scheme: channel.isSsl ? HostAndPort.wssScheme : HostAndPort.wsScheme); channelContext.currentRequest?.hostAndPort = channelContext.host; logger.d("webSocket ${data.request?.hostAndPort}"); @@ -385,8 +390,8 @@ class ChannelPipeline extends ChannelHandler { channelContext.listener?.onResponse(channelContext, data); var rawCodec = RawCodec(); - channel.pipeline.handle(rawCodec, rawCodec, WebSocketChannelHandler(remoteChannel, data)); - remoteChannel.pipeline.handle(rawCodec, rawCodec, WebSocketChannelHandler(channel, data.request!)); + channel.pipeline.channelHandle(rawCodec, WebSocketChannelHandler(remoteChannel, data)); + remoteChannel.pipeline.channelHandle(rawCodec, WebSocketChannelHandler(channel, data.request!)); return; } @@ -408,10 +413,10 @@ class ChannelPipeline extends ChannelHandler { } } -class RawCodec extends Codec { +class RawCodec extends Codec> { @override - DecoderResult decode(ChannelContext channelContext, ByteBuf byteBuf, {bool resolveBody = true}) { - var decoderResult = DecoderResult()..data = byteBuf.readAvailableBytes(); + DecoderResult decode(ChannelContext channelContext, ByteBuf byteBuf, {bool resolveBody = true}) { + var decoderResult = DecoderResult()..data = byteBuf.readAvailableBytes(); return decoderResult; } diff --git a/lib/network/handler.dart b/lib/network/handler.dart index 44ed40e..b7d0267 100644 --- a/lib/network/handler.dart +++ b/lib/network/handler.dart @@ -207,6 +207,12 @@ class HttpProxyChannelHandler extends ChannelHandler { } HostAndPort remoteAddress = hostAndPort; + + final ProxyInfo? socksProxy = channelContext.getAttribute(AttributeKeys.socks5Proxy); + if (socksProxy != null) { + remoteAddress = hostAndPort.copyWith(host: socksProxy.host, port: socksProxy.port!); + } + for (var interceptor in interceptors) { remoteAddress = await interceptor.preConnect(remoteAddress); } diff --git a/lib/network/http/codec.dart b/lib/network/http/codec.dart index c489ae4..031c0e6 100644 --- a/lib/network/http/codec.dart +++ b/lib/network/http/codec.dart @@ -14,7 +14,6 @@ * limitations under the License. */ -import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; @@ -71,13 +70,13 @@ abstract interface class Encoder { } /// 编解码器 -abstract class Codec implements Decoder, Encoder { +abstract class Codec implements Decoder, Encoder { static const int defaultMaxInitialLineLength = 1024000; // 1M static const int maxBodyLength = 4096000; // 4M } /// http编解码 -abstract class HttpCodec implements Codec { +abstract class HttpCodec implements Codec { final HttpParse _httpParse = HttpParse(); Http2Codec? _h2Codec; State _state = State.readInitial; @@ -266,3 +265,33 @@ class HttpResponseCodec extends HttpCodec { buffer.addByte(HttpConstants.lf); } } + +class HttpServerCodec extends Codec { + HttpRequestCodec requestCodec = HttpRequestCodec(); + HttpResponseCodec responseCodec = HttpResponseCodec(); + + @override + DecoderResult decode(ChannelContext channelContext, ByteBuf byteBuf) { + return requestCodec.decode(channelContext, byteBuf); + } + + @override + List encode(HttpResponse data) { + return responseCodec.encode(data); + } +} + +class HttpClientCodec extends Codec { + HttpRequestCodec requestCodec = HttpRequestCodec(); + HttpResponseCodec responseCodec = HttpResponseCodec(); + + @override + DecoderResult decode(ChannelContext channelContext, ByteBuf byteBuf) { + return responseCodec.decode(channelContext, byteBuf); + } + + @override + List encode(HttpRequest data) { + return requestCodec.encode(data); + } +} diff --git a/lib/network/http/h2/codec.dart b/lib/network/http/h2/codec.dart index a8b6fb8..85cb912 100644 --- a/lib/network/http/h2/codec.dart +++ b/lib/network/http/h2/codec.dart @@ -27,7 +27,7 @@ import 'package:proxypin/network/util/byte_buf.dart'; import 'frame.dart'; /// http编解码 -abstract class Http2Codec implements Codec { +abstract class Http2Codec implements Codec { static const maxFrameSize = 16384; static final List connectionPrefacePRI = "PRI * HTTP/2.0".codeUnits; diff --git a/lib/network/http_client.dart b/lib/network/http_client.dart index b96743c..d03ab3f 100644 --- a/lib/network/http_client.dart +++ b/lib/network/http_client.dart @@ -32,7 +32,7 @@ class HttpClients { static Future startConnect( HostAndPort hostAndPort, ChannelHandler handler, ChannelContext channelContext) async { var client = Client() - ..initChannel((channel) => channel.pipeline.handle(HttpResponseCodec(), HttpRequestCodec(), handler)); + ..initChannel((channel) => channel.pipeline.channelHandle(HttpClientCodec(), handler)); return client.connect(hostAndPort, channelContext); } @@ -41,7 +41,7 @@ class HttpClients { static Future proxyConnect(HostAndPort hostAndPort, ChannelHandler handler, ChannelContext channelContext, {ProxyInfo? proxyInfo}) async { var client = Client() - ..initChannel((channel) => channel.pipeline.handle(HttpResponseCodec(), HttpRequestCodec(), handler)); + ..initChannel((channel) => channel.pipeline.channelHandle(HttpClientCodec(), handler)); if (proxyInfo == null) { var proxyTypes = hostAndPort.isSsl() ? ProxyTypes.https : ProxyTypes.http; diff --git a/lib/network/network.dart b/lib/network/network.dart index 95e33bf..18ee98b 100644 --- a/lib/network/network.dart +++ b/lib/network/network.dart @@ -22,6 +22,7 @@ import 'package:proxypin/network/bin/configuration.dart'; import 'package:proxypin/network/channel.dart'; import 'package:proxypin/network/components/host_filter.dart'; import 'package:proxypin/network/handler.dart'; +import 'package:proxypin/network/socksx/socks5.dart'; import 'package:proxypin/network/util/attribute_keys.dart'; import 'package:proxypin/network/util/crts.dart'; import 'package:proxypin/network/util/logger.dart'; @@ -54,8 +55,8 @@ abstract class Network { /// 转发请求 void relay(Channel clientChannel, Channel remoteChannel) { var rawCodec = RawCodec(); - clientChannel.pipeline.handle(rawCodec, rawCodec, RelayHandler(remoteChannel)); - remoteChannel.pipeline.handle(rawCodec, rawCodec, RelayHandler(clientChannel)); + clientChannel.pipeline.channelHandle(rawCodec, RelayHandler(remoteChannel)); + remoteChannel.pipeline.channelHandle(rawCodec, RelayHandler(clientChannel)); } } @@ -123,6 +124,12 @@ class Server extends Network { return; } + //socks5 + if (configuration.enableSocks5 && Socks5.isSocks5(data) && channel.pipeline.handler is! SocksServerHandler) { + channel.pipeline.channelHandle( + RawCodec(), SocksServerHandler(channel.pipeline.decoder, channel.pipeline.encoder, channel.pipeline.handler)); + } + channel.pipeline.channelRead(channelContext, channel, data); } diff --git a/lib/network/socksx/socks5.dart b/lib/network/socksx/socks5.dart new file mode 100644 index 0000000..fbb9444 --- /dev/null +++ b/lib/network/socksx/socks5.dart @@ -0,0 +1,135 @@ +/* + * Copyright 2024 Hongen Wang All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'dart:typed_data'; + +import 'package:proxypin/network/channel.dart'; +import 'package:proxypin/network/http/codec.dart'; +import 'package:proxypin/network/util/attribute_keys.dart'; +import 'package:proxypin/network/util/logger.dart'; + +import '../host_port.dart'; + +/// @author wanghongen +class Socks5 { + static const int version = 5; + static const int methodNoAuth = 0; + static const int methodNoAcceptable = 0xff; + + static const int cmdConnect = 1; + + static const int atypIpv4 = 1; + + static const int repSuccess = 0; + static const int repCommandNotSupported = 7; + static const int repAddressTypeNotSupported = 8; + + static const int repSocks5ServerAtypIpv4 = 0x01; + static const int repSocks5ServerAtypDomain = 0x03; + static const int repSocks5ServerAtypIpv6 = 0x04; + + static bool isSocks5(Uint8List data) { + return data.length > 2 && data[0] == version; + } +} + +///Detects the version of the current SOCKS connection and initializes the pipeline with Socks5InitialRequestDecoder. +class SocksServerHandler extends ChannelHandler { + late Decoder originalDecoder; + late Encoder originalEncoder; + final ChannelHandler originalHandler; + + SocksState socksState = SocksState.init; + + SocksServerHandler(this.originalDecoder, this.originalEncoder, this.originalHandler); + + @override + void channelRead(ChannelContext channelContext, Channel channel, Uint8List msg) async { + int idx = 0; + final int version = msg[idx++]; + if (version != Socks5.version) { + await channel.writeBytes(Uint8List.fromList([Socks5.version, Socks5.methodNoAcceptable])); + channel.pipeline.exceptionCaught(channelContext, channel, Exception('Unsupported SOCKS version: $version')); + return; + } + + if (socksState == SocksState.init) { + //no auth + await channel.writeBytes(Uint8List.fromList([Socks5.version, Socks5.methodNoAuth])); + socksState = SocksState.connect; + return; + } + + if (socksState == SocksState.connect) { + final int cmd = msg[idx++]; + if (cmd != Socks5.cmdConnect) { + var out = encodeCommandResponse(Socks5.repCommandNotSupported); + await channel.writeBytes(out); + channel.pipeline.exceptionCaught(channelContext, channel, Exception('Unsupported SOCKS cmd: $cmd')); + return; + } + + //skip RSV + idx++; + + final int dstAddrType = msg[idx++]; + if (dstAddrType != Socks5.atypIpv4) { + var out = encodeCommandResponse(Socks5.repAddressTypeNotSupported); + await channel.writeBytes(out); + channel.pipeline.exceptionCaught(channelContext, channel, Exception('Unsupported SOCKS atyp: $dstAddrType')); + return; + } + + final host = '${msg[idx++]}.${msg[idx++]}.${msg[idx++]}.${msg[idx++]}'; + final int port = msg[idx++] << 8 | msg[idx++]; + final proxyInfo = ProxyInfo.of(host, port); + + logger.d('Socks5 connect ${proxyInfo.host}:${proxyInfo.port}'); + channelContext.putAttribute(AttributeKeys.socks5Proxy, proxyInfo); + + final out = encodeCommandResponse(Socks5.repSuccess, bndAddrType: Socks5.repSocks5ServerAtypIpv4); + await channel.writeBytes(out); + + channel.pipeline.handle(originalDecoder, originalEncoder, originalHandler); + socksState = SocksState.connected; + return; + } + } + + Uint8List encodeCommandResponse(int status, {int bndAddrType = 0, String? bndAddr, int bndPort = 0}) { + var out = BytesBuilder(); + out.addByte(Socks5.version); + out.addByte(status); + out.addByte(0x00); //RSV + out.addByte(bndAddrType); + + if (bndAddr != null) { + out.add(Int8List.fromList(bndAddr.split('.').map((e) => int.parse(e)).toList())); + } else { + out.add(Int8List.fromList([0, 0, 0, 0])); + } + out.addByte(bndPort >> 8); + out.addByte(bndPort & 0xff); + return out.takeBytes(); + } +} + +enum SocksState { + init, + auth, + connect, + connected, +} diff --git a/lib/network/util/attribute_keys.dart b/lib/network/util/attribute_keys.dart index 2b473ce..0074909 100644 --- a/lib/network/util/attribute_keys.dart +++ b/lib/network/util/attribute_keys.dart @@ -7,5 +7,6 @@ interface class AttributeKeys { static const String request = "REQUEST"; static const String remote = "REMOTE"; static const String proxyInfo = "PROXY_INFO"; + static const String socks5Proxy = "SOCKS5_PROXY"; static const String processInfo = "PROCESS_INFO"; } diff --git a/lib/ui/desktop/toolbar/setting/setting.dart b/lib/ui/desktop/toolbar/setting/setting.dart index 03d1f85..77845ee 100644 --- a/lib/ui/desktop/toolbar/setting/setting.dart +++ b/lib/ui/desktop/toolbar/setting/setting.dart @@ -23,6 +23,7 @@ import 'package:proxypin/network/components/manager/hosts_manager.dart'; import 'package:proxypin/network/components/manager/request_block_manager.dart'; import 'package:proxypin/network/util/system_proxy.dart'; import 'package:proxypin/ui/component/multi_window.dart'; +import 'package:proxypin/ui/component/widgets.dart'; import 'package:proxypin/ui/desktop/toolbar/setting/external_proxy.dart'; import 'package:proxypin/ui/desktop/toolbar/setting/hosts.dart'; import 'package:proxypin/ui/desktop/toolbar/setting/request_block.dart'; @@ -180,6 +181,21 @@ class _ProxyMenuState extends State<_ProxyMenu> { const Divider(thickness: 0.3, height: 8), setSystemProxy(), const Divider(thickness: 0.3, height: 8), + Row(children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 15), + child: Text("SOCKS5", style: const TextStyle(fontSize: 14)))), + SwitchWidget( + value: configuration.enableSocks5, + scale: 0.75, + onChanged: (val) { + configuration.enableSocks5 = val; + changed = true; + }), + SizedBox(width: 10) + ]), + const Divider(thickness: 0.3, height: 8), const SizedBox(height: 3), Padding( padding: const EdgeInsets.only(left: 15), @@ -225,22 +241,23 @@ class _ProxyMenuState extends State<_ProxyMenu> { ///设置系统代理 Widget setSystemProxy() { return Row(children: [ - Padding( - padding: const EdgeInsets.only(left: 15), - child: Text(localizations.systemProxy, style: const TextStyle(fontSize: 14))), Expanded( - child: Transform.scale( - scale: 0.8, - child: Switch( - hoverColor: Colors.transparent, - value: configuration.enableSystemProxy, - onChanged: (val) { - widget.proxyServer.setSystemProxyEnable(val); - configuration.enableSystemProxy = val; - setState(() { - changed = true; - }); - }))) + child: Padding( + padding: const EdgeInsets.only(left: 15, right: 20), + child: Text(localizations.systemProxy, style: const TextStyle(fontSize: 14)))), + Transform.scale( + scale: 0.75, + child: Switch( + hoverColor: Colors.transparent, + value: configuration.enableSystemProxy, + onChanged: (val) { + widget.proxyServer.setSystemProxyEnable(val); + configuration.enableSystemProxy = val; + setState(() { + changed = true; + }); + })), + SizedBox(width: 10) ]); } } diff --git a/lib/ui/mobile/setting/preference.dart b/lib/ui/mobile/setting/preference.dart index 3193de2..bca207a 100644 --- a/lib/ui/mobile/setting/preference.dart +++ b/lib/ui/mobile/setting/preference.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:proxypin/network/bin/configuration.dart'; import 'package:proxypin/network/bin/server.dart'; import 'package:proxypin/network/util/logger.dart'; import 'package:proxypin/ui/component/widgets.dart'; @@ -25,6 +26,7 @@ class Preference extends StatefulWidget { class _PreferenceState extends State { late ProxyServer proxyServer; + late Configuration configuration; late AppConfiguration appConfiguration; final memoryCleanupController = TextEditingController(); @@ -34,6 +36,7 @@ class _PreferenceState extends State { void initState() { super.initState(); proxyServer = widget.proxyServer; + configuration = widget.proxyServer.configuration; appConfiguration = widget.appConfiguration; if (!memoryCleanupList.contains(appConfiguration.memoryCleanupThreshold)) { @@ -61,6 +64,15 @@ class _PreferenceState extends State { proxyServer: proxyServer, title: '${localizations.proxy}${isEn ? ' ' : ''}${localizations.port}', textStyle: const TextStyle(fontSize: 16)), + ListTile( + title: Text("SOCKS5"), + trailing: SwitchWidget( + value: configuration.enableSocks5, + scale: 0.8, + onChanged: (value) { + configuration.enableSocks5 = value; + proxyServer.configuration.flushConfig(); + })), ListTile( title: Text(localizations.externalProxy), trailing: const Icon(Icons.keyboard_arrow_right), diff --git a/lib/ui/mobile/setting/script.dart b/lib/ui/mobile/setting/script.dart index bad6b53..6773690 100644 --- a/lib/ui/mobile/setting/script.dart +++ b/lib/ui/mobile/setting/script.dart @@ -118,7 +118,7 @@ class _MobileScriptState extends State { //导入js import() async { - FilePickerResult? result = await FilePicker.platform.pickFiles(type: FileType.custom, allowedExtensions: ['json']); + FilePickerResult? result = await FilePicker.platform.pickFiles(type: FileType.any); if (result == null || result.files.isEmpty) { return; }