Merge branch 'main' into flutter-3.19.6

# Conflicts:
#	lib/ui/component/qrcode/qr_scan_view.dart
#	pubspec.yaml
This commit is contained in:
wanghongenpin
2025-04-19 12:47:05 +08:00
96 changed files with 2381 additions and 1007 deletions

3
.gitignore vendored
View File

@@ -46,4 +46,5 @@ app.*.map.json
/android/app/release
l10n_errors.txt
pubspec.lock
pubspec.lock
/dist/

View File

@@ -2,6 +2,9 @@
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
formatter:
page_width: 120
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`

1
android/.gitignore vendored
View File

@@ -11,3 +11,4 @@ GeneratedPluginRegistrant.java
key.properties
**/*.keystore
**/*.jks
/app/.cxx/

66
assets/js/fetch.js Normal file
View File

@@ -0,0 +1,66 @@
function fetch(url, options) {
options = options || {};
return new Promise((resolve, reject) => {
const request = new XMLHttpRequest();
const keys = [];
const all = [];
const headers = {};
const response = () => ({
ok: (request.status / 100 | 0) === 2, // 200-299
statusText: request.statusText,
status: request.status,
url: request.responseURL,
body: request.response.body,
text: () => Promise.resolve(request.responseText),
json: () => {
// TODO: review this handle because it may discard \n from json attributes
try {
// console.log('RESPONSE TEXT IN FETCH: ' + request.responseText);
return Promise.resolve(JSON.parse(request.responseText));
} catch (e) {
// console.log('ERROR on fetch parsing JSON: ' + e.message);
return Promise.resolve(request.responseText);
}
},
blob: () => Promise.resolve(request.response.body),
clone: response,
headers: {
keys: () => keys,
entries: () => all,
get: n => headers[n.toLowerCase()],
has: n => n.toLowerCase() in headers
}
});
request.open(options.method || 'get', url, true);
request.onload = () => {
request.getAllResponseHeaders().replace(/^(.*?):[^\S\n]*([\s\S]*?)$/gm, (m, key, value) => {
keys.push(key = key.toLowerCase());
all.push([key, value]);
headers[key] = headers[key] ? `${headers[key]},${value}` : value;
});
resolve(response());
};
request.onerror = reject;
request.withCredentials = options.credentials == 'include';
if (options.headers) {
if (options.headers.constructor.name == 'Object') {
for (const i in options.headers) {
request.setRequestHeader(i, options.headers[i]);
}
} else { // if it is some Headers pollyfill, the way to iterate is through for of
for (const header of options.headers) {
request.setRequestHeader(header[0], header[1]);
}
}
}
request.send(options.body || null);
});
}

18
distribute_options.yaml Normal file
View File

@@ -0,0 +1,18 @@
output: dist/
releases:
- name: dev
jobs:
- name: macos-dmg
package:
platform: macos
target: dmg
build_args:
profile: true
- name: windows-exe
package:
platform: windows
target: exe
build_args:
profile: true

View File

@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objectVersion = 55;
objects = {
/* Begin PBXBuildFile section */
@@ -743,6 +743,7 @@
DEVELOPMENT_TEAM = DM3F8VR243;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -926,6 +927,7 @@
DEVELOPMENT_TEAM = DM3F8VR243;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -951,6 +953,7 @@
DEVELOPMENT_TEAM = DM3F8VR243;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",

View File

@@ -315,5 +315,15 @@
"time": "DateTime",
"nowTimestamp": "Now timestamp",
"hosts": "Hosts",
"toAddress": "To Address"
"toAddress": "To Address",
"appUpdateCheckVersion": "Check for Updates",
"appUpdateNotAvailableMsg": "Already Using The Latest Version",
"appUpdateDialogTitle": "Update Available",
"appUpdateUpdateMsg": "A new version of ProxyPin is available. Would you like to update now?",
"appUpdateCurrentVersionLbl": "Current Version",
"appUpdateNewVersionLbl": "New Version",
"appUpdateUpdateNowBtnTxt": "Update Now",
"appUpdateLaterBtnTxt": "Later",
"appUpdateIgnoreBtnTxt": "Ignore"
}

View File

@@ -314,5 +314,15 @@
"time": "时间",
"nowTimestamp": "当前时间戳(秒)",
"hosts": "Hosts 映射",
"toAddress": "映射地址"
"toAddress": "映射地址",
"appUpdateCheckVersion": "检查更新",
"appUpdateNotAvailableMsg": "已是最新版本",
"appUpdateDialogTitle": "有可用更新",
"appUpdateUpdateMsg": "ProxyPin 的新版本现已推出。您想现在更新吗?",
"appUpdateCurrentVersionLbl": "当前版本",
"appUpdateNewVersionLbl": "新版本",
"appUpdateUpdateNowBtnTxt": "现在更新",
"appUpdateLaterBtnTxt": "以后再说",
"appUpdateIgnoreBtnTxt": "忽略"
}

View File

@@ -29,21 +29,31 @@ import 'package:proxypin/ui/mobile/mobile.dart';
import 'package:proxypin/utils/navigator.dart';
import 'package:proxypin/utils/platform.dart';
import 'package:window_manager/window_manager.dart';
import 'package:windows_single_instance/windows_single_instance.dart';
import 'network/util/logger.dart';
///主入口
///@author wanghongen
void main(List<String> args) async {
WidgetsFlutterBinding.ensureInitialized();
var instance = AppConfiguration.instance;
//多窗口
if (args.firstOrNull == 'multi_window') {
final windowId = int.parse(args[1]);
final argument = args[2].isEmpty ? const {} : jsonDecode(args[2]) as Map<String, dynamic>;
runApp(FluentApp(multiWindow(windowId, argument), (await instance)));
runApp(FluentApp(multiWindow(windowId, argument), (await AppConfiguration.instance)));
return;
}
if (Platform.isWindows) {
await WindowsSingleInstance.ensureSingleInstance([], "ProxyPin", onSecondWindow: (args) {
logger.d('WindowsSingleInstance onSecondWindow $args');
windowManager.show();
});
}
var instance = AppConfiguration.instance;
var configuration = Configuration.instance;
//移动端
if (Platforms.isMobile()) {

View File

@@ -17,7 +17,7 @@
import 'dart:convert';
import 'dart:io';
import 'package:proxypin/network/host_port.dart';
import 'package:proxypin/network/channel/host_port.dart';
import 'package:proxypin/network/util/file_read.dart';
import 'package:proxypin/network/components/host_filter.dart';
import 'package:proxypin/network/util/logger.dart';

View File

@@ -0,0 +1,41 @@
import 'package:proxypin/network/channel/channel.dart';
import 'package:proxypin/network/channel/channel_context.dart';
import 'package:proxypin/network/http/http.dart';
import 'package:proxypin/network/http/websocket.dart';
///请求和响应事件监听
abstract class EventListener {
void onRequest(Channel channel, HttpRequest request);
void onResponse(ChannelContext channelContext, HttpResponse response);
void onMessage(Channel channel, HttpMessage message, WebSocketFrame frame) {}
}
class CombinedEventListener extends EventListener {
final List<EventListener> listeners;
CombinedEventListener(this.listeners);
@override
void onRequest(Channel channel, HttpRequest request) {
for (var element in listeners) {
element.onRequest(channel, request);
}
}
@override
void onResponse(ChannelContext channelContext, HttpResponse response) {
for (var element in listeners) {
element.onResponse(channelContext, response);
}
}
@override
void onMessage(Channel channel, HttpMessage message, WebSocketFrame frame) {
for (var element in listeners) {
element.onMessage(channel, message, frame);
}
}
}

View File

@@ -18,22 +18,20 @@ import 'dart:async';
import 'dart:io';
import 'package:proxypin/network/bin/configuration.dart';
import 'package:proxypin/network/channel.dart';
import 'package:proxypin/network/components/hosts.dart';
import 'package:proxypin/network/components/interceptor.dart';
import 'package:proxypin/network/components/request_block.dart';
import 'package:proxypin/network/components/request_rewrite.dart';
import 'package:proxypin/network/components/script.dart';
import 'package:proxypin/network/http/http.dart';
import 'package:proxypin/network/http/websocket.dart';
import 'package:proxypin/network/handle/http_proxy_handle.dart';
import 'package:proxypin/network/util/crts.dart';
import 'package:proxypin/utils/platform.dart';
import '../handler.dart';
import '../http/codec.dart';
import '../network.dart';
import '../channel/network.dart';
import '../util/logger.dart';
import '../util/system_proxy.dart';
import 'listener.dart';
Future<void> main() async {
var configuration = await Configuration.instance;
@@ -71,7 +69,9 @@ class ProxyServer {
return;
}
SystemProxy.setSslProxyEnable(enableSsl, port);
if (configuration.enableSystemProxy) {
SystemProxy.setSslProxyEnable(enableSsl, port);
}
}
/// 启动代理服务
@@ -88,7 +88,7 @@ class ProxyServer {
interceptors.sort((a, b) => a.priority.compareTo(b.priority));
server.initChannel((channel) {
channel.pipeline.handle(
channel.dispatcher.handle(
HttpRequestCodec(),
HttpResponseCodec(),
HttpProxyChannelHandler(listener: CombinedEventListener(listeners), interceptors: interceptors),
@@ -156,30 +156,3 @@ class ProxyServer {
listeners.add(listener);
}
}
class CombinedEventListener extends EventListener {
final List<EventListener> listeners;
CombinedEventListener(this.listeners);
@override
void onRequest(Channel channel, HttpRequest request) {
for (var element in listeners) {
element.onRequest(channel, request);
}
}
@override
void onResponse(ChannelContext channelContext, HttpResponse response) {
for (var element in listeners) {
element.onResponse(channelContext, response);
}
}
@override
void onMessage(Channel channel, HttpMessage message, WebSocketFrame frame) {
for (var element in listeners) {
element.onMessage(channel, message, frame);
}
}
}

View File

@@ -1,431 +0,0 @@
/*
* Copyright 2023 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:async';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:proxypin/network/host_port.dart';
import 'package:proxypin/network/http/codec.dart';
import 'package:proxypin/network/http/h2/setting.dart';
import 'package:proxypin/network/http/http.dart';
import 'package:proxypin/network/http_client.dart';
import 'package:proxypin/network/util/attribute_keys.dart';
import 'package:proxypin/network/util/byte_buf.dart';
import 'package:proxypin/network/util/logger.dart';
import 'package:proxypin/network/util/process_info.dart';
import 'package:proxypin/network/util/socket_address.dart';
import 'package:proxypin/utils/lang.dart';
import 'handler.dart';
///处理I/O事件或截获I/O操作
///[T] 读取的数据类型
///@author wanghongen
abstract class ChannelHandler<T> {
var log = logger;
///连接建立
void channelActive(ChannelContext context, Channel channel) {}
///读取数据事件
void channelRead(ChannelContext channelContext, Channel channel, T msg) {}
///连接断开
void channelInactive(ChannelContext channelContext, Channel channel) {
// log.i("close $channel");
}
void exceptionCaught(ChannelContext channelContext, Channel channel, dynamic error, {StackTrace? trace}) {
HostAndPort? host = channelContext.host;
log.e("[${channel.id}] error $host $channel", error: error, stackTrace: trace);
channel.close();
}
}
///与网络套接字或组件的连接能够进行读、写、连接和绑定等I/O操作。
class Channel {
final int _id;
final ChannelPipeline pipeline = ChannelPipeline();
Socket _socket;
//是否打开
bool isOpen = true;
//此通道连接到的远程地址
final InetSocketAddress remoteSocketAddress;
//是否写入中
bool isWriting = false;
Object? error; //异常
Channel(this._socket)
: _id = DateTime.now().millisecondsSinceEpoch + Random().nextInt(999999),
remoteSocketAddress = InetSocketAddress(_socket.remoteAddress, _socket.remotePort);
///返回此channel的全局唯一标识符。
String get id => _id.toRadixString(36);
Socket get socket => _socket;
Future<SecureSocket> secureSocket(ChannelContext channelContext,
{String? host, List<String>? supportedProtocols}) async {
SecureSocket secureSocket = await SecureSocket.secure(socket,
host: host, supportedProtocols: supportedProtocols, onBadCertificate: (certificate) => true);
_socket = secureSocket;
_socket.done.then((value) => isOpen = false);
pipeline.listen(this, channelContext);
return secureSocket;
}
serverSecureSocket(SecureSocket secureSocket, ChannelContext channelContext) {
_socket = secureSocket;
_socket.done.then((value) => isOpen = false);
pipeline.listen(this, channelContext);
}
String? get selectedProtocol => isSsl ? (_socket as SecureSocket).selectedProtocol : null;
///是否是ssl链接
bool get isSsl => _socket is SecureSocket;
Future<void> write(Object obj) async {
var data = pipeline.encoder.encode(obj);
await writeBytes(data);
}
Future<void> writeBytes(List<int> bytes) async {
if (isClosed) {
logger.w("[$id] channel is closed");
return;
}
//只能有一个写入
int retry = 0;
while (isWriting && retry++ < 30) {
await Future.delayed(const Duration(milliseconds: 100));
}
isWriting = true;
try {
if (!isClosed) {
_socket.add(bytes);
}
await _socket.flush();
} catch (e, t) {
if (e is StateError && e.message == "StreamSink is closed") {
isOpen = false;
} else {
logger.e("[$id] write error", error: e, stackTrace: t);
}
} finally {
isWriting = false;
}
}
///写入并关闭此channel
Future<void> writeAndClose(Object obj) async {
await write(obj);
close();
}
///关闭此channel
void close() async {
if (isClosed) {
return;
}
//写入中,延迟关闭
int retry = 0;
while (isWriting && retry++ < 10) {
await Future.delayed(const Duration(milliseconds: 150));
}
isOpen = false;
_socket.destroy();
}
///返回此channel是否打开
bool get isClosed => !isOpen;
@override
String toString() {
return 'Channel($id $remoteSocketAddress';
}
}
///
class ChannelContext {
final Map<String, Object> _attributes = {};
//和本地客户端的连接
Channel? clientChannel;
//和远程服务端的连接
Channel? serverChannel;
EventListener? listener;
//http2 stream
final Map<int, Pair<HttpRequest, ValueWrap<HttpResponse>>> _streams = {};
ChannelContext();
//创建服务端连接
Future<Channel> connectServerChannel(HostAndPort hostAndPort, ChannelHandler channelHandler) async {
serverChannel = await HttpClients.startConnect(hostAndPort, channelHandler, this);
putAttribute(clientChannel!.id, serverChannel);
putAttribute(serverChannel!.id, clientChannel);
return serverChannel!;
}
T? getAttribute<T>(String key) {
if (!_attributes.containsKey(key)) {
return null;
}
return _attributes[key] as T;
}
void putAttribute(String key, Object? value) {
if (value == null) {
_attributes.remove(key);
return;
}
_attributes[key] = value;
}
HostAndPort? get host => getAttribute(AttributeKeys.host);
set host(HostAndPort? host) => putAttribute(AttributeKeys.host, host);
HttpRequest? get currentRequest => getAttribute(AttributeKeys.request);
set currentRequest(HttpRequest? request) => putAttribute(AttributeKeys.request, request);
set processInfo(ProcessInfo? processInfo) => putAttribute(AttributeKeys.processInfo, processInfo);
ProcessInfo? get processInfo => getAttribute(AttributeKeys.processInfo);
StreamSetting? setting;
HttpRequest? putStreamRequest(int streamId, HttpRequest request) {
var old = _streams[streamId]?.key;
_streams[streamId] = Pair(request, ValueWrap());
return old;
}
void putStreamResponse(int streamId, HttpResponse response) {
var stream = _streams[streamId]!;
stream.key.response = response;
response.request = stream.key;
stream.value.set(response);
}
HttpRequest? getStreamRequest(int streamId) {
return _streams[streamId]?.key;
}
HttpResponse? getStreamResponse(int streamId) {
return _streams[streamId]?.value.get();
}
void removeStream(int streamId) {
_streams.remove(streamId);
}
}
class ChannelPipeline extends ChannelHandler<Uint8List> {
late Decoder decoder;
late Encoder encoder;
late ChannelHandler handler;
final ByteBuf buffer = ByteBuf();
handle(Decoder decoder, Encoder encoder, ChannelHandler handler) {
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();
channel.socket.listen((data) => channel.pipeline.channelRead(channelContext, channel, data),
onError: (error, trace) => channel.pipeline.exceptionCaught(channelContext, channel, error, trace: trace),
onDone: () => channel.pipeline.channelInactive(channelContext, channel));
}
@override
void channelActive(ChannelContext context, Channel channel) {
handler.channelActive(context, channel);
}
///远程转发请求
remoteForward(ChannelContext channelContext, HostAndPort remote) async {
var clientChannel = channelContext.clientChannel!;
Channel? remoteChannel =
channelContext.serverChannel ?? await channelContext.connectServerChannel(remote, RelayHandler(clientChannel));
ProxyInfo? proxyInfo = channelContext.getAttribute(AttributeKeys.proxyInfo);
if (clientChannel.isSsl && !remoteChannel.isSsl) {
//代理认证
if (proxyInfo?.isAuthenticated == true) {
await HttpClients.connectRequest(remote, remoteChannel, proxyInfo: proxyInfo);
}
await remoteChannel.secureSocket(channelContext, host: channelContext.getAttribute(AttributeKeys.domain));
}
relay(channelContext, clientChannel, remoteChannel);
}
/// 转发请求
void relay(ChannelContext channelContext, Channel clientChannel, Channel remoteChannel) {
var rawCodec = RawCodec();
clientChannel.pipeline.channelHandle(rawCodec, RelayHandler(remoteChannel));
remoteChannel.pipeline.channelHandle(rawCodec, RelayHandler(clientChannel));
var body = buffer.bytes;
buffer.clear();
handler.channelRead(channelContext, clientChannel, body);
}
@override
void channelRead(ChannelContext channelContext, Channel channel, Uint8List msg) async {
try {
//手机扫码连接转发远程
HostAndPort? remote = channelContext.getAttribute(AttributeKeys.remote);
buffer.add(msg);
if (remote != null) {
await remoteForward(channelContext, remote);
return;
}
Channel? remoteChannel = channelContext.getAttribute(channel.id);
//大body 不解析直接转发
if (buffer.length > Codec.maxBodyLength && handler is! RelayHandler && remoteChannel != null) {
logger.w("[$channel] forward large body");
relay(channelContext, channel, remoteChannel);
return;
}
var decodeResult = decoder.decode(channelContext, buffer);
if (!decodeResult.isDone) {
return;
}
if (decodeResult.forward != null) {
if (remoteChannel != null) {
await remoteChannel.writeBytes(decodeResult.forward!);
} else {
logger.w("[$channel] forward remoteChannel is null");
}
buffer.clearRead();
return;
}
var length = buffer.length;
buffer.clearRead();
var data = decodeResult.data;
if (data is HttpMessage) {
data.packageSize = length;
data.remoteHost = channel.remoteSocketAddress.host;
data.remotePort = channel.remoteSocketAddress.port;
}
if (data is HttpRequest) {
channelContext.currentRequest = data;
data.hostAndPort = channelContext.host ?? getHostAndPort(data, ssl: channel.isSsl);
if (data.headers.host != null && data.headers.host?.contains(":") == false) {
data.hostAndPort?.host = data.headers.host!;
}
if (data.method != HttpMethod.connect) {
try {
data.processInfo ??=
await ProcessInfoUtils.getProcessByPort(channel.remoteSocketAddress, data.remoteDomain()!);
} catch (ignore) {
/*ignore*/
}
}
}
if (data is HttpResponse) {
data.requestId = channelContext.currentRequest?.requestId ?? data.requestId;
data.request ??= channelContext.currentRequest;
}
//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.currentRequest?.hostAndPort = channelContext.host;
logger.d("webSocket ${data.request?.hostAndPort}");
remoteChannel.write(data);
channelContext.listener?.onResponse(channelContext, data);
var rawCodec = RawCodec();
channel.pipeline.channelHandle(rawCodec, WebSocketChannelHandler(remoteChannel, data));
remoteChannel.pipeline.channelHandle(rawCodec, WebSocketChannelHandler(channel, data.request!));
return;
}
handler.channelRead(channelContext, channel, data!);
} catch (error, trace) {
buffer.clear();
exceptionCaught(channelContext, channel, error, trace: trace);
}
}
@override
exceptionCaught(ChannelContext channelContext, Channel channel, dynamic error, {StackTrace? trace}) {
handler.exceptionCaught(channelContext, channel, error, trace: trace);
}
@override
channelInactive(ChannelContext channelContext, Channel channel) {
handler.channelInactive(channelContext, channel);
}
}
class RawCodec extends Codec<Uint8List, List<int>> {
@override
DecoderResult<Uint8List> decode(ChannelContext channelContext, ByteBuf byteBuf, {bool resolveBody = true}) {
var decoderResult = DecoderResult<Uint8List>()..data = byteBuf.readAvailableBytes();
return decoderResult;
}
@override
List<int> encode(dynamic data) {
return data as List<int>;
}
}
abstract interface class ChannelInitializer {
void initChannel(Channel channel);
}

View File

@@ -0,0 +1,163 @@
/*
* Copyright 2023 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:async';
import 'dart:io';
import 'dart:math';
import 'package:proxypin/network/channel/channel_context.dart';
import 'package:proxypin/network/channel/host_port.dart';
import 'package:proxypin/network/util/logger.dart';
import 'package:proxypin/network/util/socket_address.dart';
import 'channel_dispatcher.dart';
///处理I/O事件或截获I/O操作
///[T] 读取的数据类型
///@author wanghongen
abstract class ChannelHandler<T> {
var log = logger;
///连接建立
void channelActive(ChannelContext context, Channel channel) {}
///读取数据事件
void channelRead(ChannelContext channelContext, Channel channel, T msg) {}
///连接断开
void channelInactive(ChannelContext channelContext, Channel channel) {
// log.i("close $channel");
}
void exceptionCaught(ChannelContext channelContext, Channel channel, dynamic error, {StackTrace? trace}) {
HostAndPort? host = channelContext.host;
log.e("[${channel.id}] error $host $channel", error: error, stackTrace: trace);
channel.close();
}
}
///与网络套接字或组件的连接能够进行读、写、连接和绑定等I/O操作。
class Channel {
final int _id;
final ChannelDispatcher dispatcher = ChannelDispatcher();
Socket _socket;
//是否打开
bool isOpen = true;
//此通道连接到的远程地址
final InetSocketAddress remoteSocketAddress;
//是否写入中
bool isWriting = false;
Object? error; //异常
Channel(this._socket)
: _id = DateTime.now().millisecondsSinceEpoch + Random().nextInt(999999),
remoteSocketAddress = InetSocketAddress(_socket.remoteAddress, _socket.remotePort);
///返回此channel的全局唯一标识符。
String get id => _id.toRadixString(36);
Socket get socket => _socket;
Future<SecureSocket> secureSocket(ChannelContext channelContext,
{String? host, List<String>? supportedProtocols}) async {
SecureSocket secureSocket = await SecureSocket.secure(socket,
host: host, supportedProtocols: supportedProtocols, onBadCertificate: (certificate) => true);
_socket = secureSocket;
_socket.done.then((value) => isOpen = false);
dispatcher.listen(this, channelContext);
return secureSocket;
}
serverSecureSocket(SecureSocket secureSocket, ChannelContext channelContext) {
_socket = secureSocket;
_socket.done.then((value) => isOpen = false);
dispatcher.listen(this, channelContext);
}
String? get selectedProtocol => isSsl ? (_socket as SecureSocket).selectedProtocol : null;
///是否是ssl链接
bool get isSsl => _socket is SecureSocket;
Future<void> write(Object obj) async {
var data = dispatcher.encoder.encode(obj);
await writeBytes(data);
}
Future<void> writeBytes(List<int> bytes) async {
if (isClosed) {
logger.w("[$id] channel is closed");
return;
}
//只能有一个写入
int retry = 0;
while (isWriting && retry++ < 30) {
await Future.delayed(const Duration(milliseconds: 100));
}
isWriting = true;
try {
if (!isClosed) {
_socket.add(bytes);
}
await _socket.flush();
} catch (e, t) {
if (e is StateError && e.message == "StreamSink is closed") {
isOpen = false;
} else {
logger.e("[$id] write error", error: e, stackTrace: t);
}
} finally {
isWriting = false;
}
}
///写入并关闭此channel
Future<void> writeAndClose(Object obj) async {
await write(obj);
close();
}
///关闭此channel
void close() async {
if (isClosed) {
return;
}
//写入中,延迟关闭
int retry = 0;
while (isWriting && retry++ < 10) {
await Future.delayed(const Duration(milliseconds: 150));
}
isOpen = false;
_socket.destroy();
}
///返回此channel是否打开
bool get isClosed => !isOpen;
@override
String toString() {
return 'Channel($id $remoteSocketAddress';
}
}

View File

@@ -0,0 +1,91 @@
import 'package:proxypin/network/channel/channel.dart';
import 'package:proxypin/network/channel/host_port.dart';
import 'package:proxypin/network/http/h2/setting.dart';
import 'package:proxypin/network/http/http.dart';
import 'package:proxypin/network/http/http_client.dart';
import 'package:proxypin/network/util/attribute_keys.dart';
import 'package:proxypin/network/util/process_info.dart';
import 'package:proxypin/utils/lang.dart';
import '../bin/listener.dart';
///
class ChannelContext {
final Map<String, Object> _attributes = {};
//和本地客户端的连接
Channel? clientChannel;
//和远程服务端的连接
Channel? serverChannel;
EventListener? listener;
//http2 stream
final Map<int, Pair<HttpRequest, ValueWrap<HttpResponse>>> _streams = {};
ChannelContext();
//创建服务端连接
Future<Channel> connectServerChannel(HostAndPort hostAndPort, ChannelHandler channelHandler) async {
serverChannel = await HttpClients.startConnect(hostAndPort, channelHandler, this);
putAttribute(clientChannel!.id, serverChannel);
putAttribute(serverChannel!.id, clientChannel);
return serverChannel!;
}
T? getAttribute<T>(String key) {
if (!_attributes.containsKey(key)) {
return null;
}
return _attributes[key] as T;
}
void putAttribute(String key, Object? value) {
if (value == null) {
_attributes.remove(key);
return;
}
_attributes[key] = value;
}
HostAndPort? get host => getAttribute(AttributeKeys.host);
set host(HostAndPort? host) => putAttribute(AttributeKeys.host, host);
HttpRequest? get currentRequest => getAttribute(AttributeKeys.request);
set currentRequest(HttpRequest? request) => putAttribute(AttributeKeys.request, request);
set processInfo(ProcessInfo? processInfo) => putAttribute(AttributeKeys.processInfo, processInfo);
ProcessInfo? get processInfo => getAttribute(AttributeKeys.processInfo);
StreamSetting? setting;
HttpRequest? putStreamRequest(int streamId, HttpRequest request) {
var old = _streams[streamId]?.key;
_streams[streamId] = Pair(request, ValueWrap());
return old;
}
void putStreamResponse(int streamId, HttpResponse response) {
var stream = _streams[streamId]!;
stream.key.response = response;
response.request = stream.key;
stream.value.set(response);
}
HttpRequest? getStreamRequest(int streamId) {
return _streams[streamId]?.key;
}
HttpResponse? getStreamResponse(int streamId) {
return _streams[streamId]?.value.get();
}
void removeStream(int streamId) {
_streams.remove(streamId);
}
}

View File

@@ -0,0 +1,214 @@
import 'dart:typed_data';
import 'package:proxypin/network/channel/channel.dart';
import 'package:proxypin/network/channel/channel_context.dart';
import 'package:proxypin/network/handle/relay_handle.dart';
import 'package:proxypin/network/channel/host_port.dart';
import 'package:proxypin/network/handle/websocket_handle.dart';
import 'package:proxypin/network/http/codec.dart';
import 'package:proxypin/network/http/http.dart';
import 'package:proxypin/network/http/http_client.dart';
import 'package:proxypin/network/util/attribute_keys.dart';
import 'package:proxypin/network/util/byte_buf.dart';
import 'package:proxypin/network/util/logger.dart';
import 'package:proxypin/network/util/process_info.dart';
class ChannelDispatcher extends ChannelHandler<Uint8List> {
late Decoder decoder;
late Encoder encoder;
late ChannelHandler handler;
final ByteBuf buffer = ByteBuf();
handle(Decoder decoder, Encoder encoder, ChannelHandler handler) {
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();
channel.socket.listen((data) => channel.dispatcher.channelRead(channelContext, channel, data),
onError: (error, trace) => channel.dispatcher.exceptionCaught(channelContext, channel, error, trace: trace),
onDone: () => channel.dispatcher.channelInactive(channelContext, channel));
}
@override
void channelActive(ChannelContext context, Channel channel) {
handler.channelActive(context, channel);
}
///远程转发请求
remoteForward(ChannelContext channelContext, HostAndPort remote) async {
var clientChannel = channelContext.clientChannel!;
Channel? remoteChannel =
channelContext.serverChannel ?? await channelContext.connectServerChannel(remote, RelayHandler(clientChannel));
ProxyInfo? proxyInfo = channelContext.getAttribute(AttributeKeys.proxyInfo);
if (clientChannel.isSsl && !remoteChannel.isSsl) {
//代理认证
if (proxyInfo?.isAuthenticated == true) {
await HttpClients.connectRequest(remote, remoteChannel, proxyInfo: proxyInfo);
}
await remoteChannel.secureSocket(channelContext, host: channelContext.getAttribute(AttributeKeys.domain));
}
relay(channelContext, clientChannel, remoteChannel);
}
/// 转发请求
void relay(ChannelContext channelContext, Channel clientChannel, Channel remoteChannel) {
var rawCodec = RawCodec();
clientChannel.dispatcher.channelHandle(rawCodec, RelayHandler(remoteChannel));
remoteChannel.dispatcher.channelHandle(rawCodec, RelayHandler(clientChannel));
var body = buffer.bytes;
buffer.clear();
handler.channelRead(channelContext, clientChannel, body);
}
@override
void channelRead(ChannelContext channelContext, Channel channel, Uint8List msg) async {
try {
//手机扫码连接转发远程
HostAndPort? remote = channelContext.getAttribute(AttributeKeys.remote);
buffer.add(msg);
if (remote != null) {
await remoteForward(channelContext, remote);
return;
}
Channel? remoteChannel = channelContext.getAttribute(channel.id);
//大body 不解析直接转发
if (buffer.length > Codec.maxBodyLength && handler is! RelayHandler && remoteChannel != null) {
logger.w("[$channel] forward large body");
relay(channelContext, channel, remoteChannel);
return;
}
var decodeResult = decoder.decode(channelContext, buffer);
//If the body does not support parsing, forward directly
if (decodeResult.supportedParse == false) {
notSupportedForward(channelContext, channel, decodeResult);
return;
}
if (!decodeResult.isDone) {
return;
}
if (decodeResult.forward != null) {
if (remoteChannel != null) {
await remoteChannel.writeBytes(decodeResult.forward!);
} else {
logger.w("[$channel] forward remoteChannel is null");
}
buffer.clearRead();
return;
}
var length = buffer.length;
buffer.clearRead();
var data = decodeResult.data;
if (data is HttpMessage) {
data.packageSize = length;
data.remoteHost = channel.remoteSocketAddress.host;
data.remotePort = channel.remoteSocketAddress.port;
}
if (data is HttpRequest) {
channelContext.currentRequest = data;
data.hostAndPort = channelContext.host ?? getHostAndPort(data, ssl: channel.isSsl);
if (data.headers.host != null && data.headers.host?.contains(":") == false) {
data.hostAndPort?.host = data.headers.host!;
}
data.processInfo ??= await ProcessInfoUtils.getProcessByPort(channel.remoteSocketAddress, data.remoteDomain()!);
}
if (data is HttpResponse) {
data.requestId = channelContext.currentRequest?.requestId ?? data.requestId;
data.request ??= channelContext.currentRequest;
}
//websocket协议
if (data is HttpResponse && data.isWebSocket && remoteChannel != null) {
onWebSocketHandle(channelContext, channel, data);
return;
}
handler.channelRead(channelContext, channel, data!);
} catch (error, trace) {
buffer.clear();
exceptionCaught(channelContext, channel, error, trace: trace);
}
}
/// websocket 处理
onWebSocketHandle(ChannelContext channelContext, Channel channel, HttpResponse data) {
Channel remoteChannel = channelContext.getAttribute(channel.id);
data.request?.response = data;
channelContext.host =
channelContext.host?.copyWith(scheme: channel.isSsl ? HostAndPort.wssScheme : HostAndPort.wsScheme);
channelContext.currentRequest?.hostAndPort = channelContext.host;
logger.d("webSocket ${data.request?.hostAndPort}");
remoteChannel.write(data);
channelContext.listener?.onResponse(channelContext, data);
var rawCodec = RawCodec();
channel.dispatcher.channelHandle(rawCodec, WebSocketChannelHandler(remoteChannel, data));
remoteChannel.dispatcher.channelHandle(rawCodec, WebSocketChannelHandler(channel, data.request!));
}
notSupportedForward(ChannelContext channelContext, Channel channel, DecoderResult decodeResult) {
Channel? remoteChannel = channelContext.getAttribute(channel.id);
buffer.add(decodeResult.forward ?? []);
relay(channelContext, channel, remoteChannel!);
if (decodeResult.data is HttpResponse) {
var response = decodeResult.data as HttpResponse;
logger.w("[$channel] not supported parse ${response.headers.contentType}");
response.request ??= channelContext.currentRequest;
channelContext.currentRequest?.response = response;
channelContext.listener?.onResponse(channelContext, response);
}
}
@override
exceptionCaught(ChannelContext channelContext, Channel channel, dynamic error, {StackTrace? trace}) {
handler.exceptionCaught(channelContext, channel, error, trace: trace);
}
@override
channelInactive(ChannelContext channelContext, Channel channel) {
handler.channelInactive(channelContext, channel);
}
}
class RawCodec extends Codec<Uint8List, List<int>> {
@override
DecoderResult<Uint8List> decode(ChannelContext channelContext, ByteBuf byteBuf, {bool resolveBody = true}) {
var decoderResult = DecoderResult<Uint8List>()..data = byteBuf.readAvailableBytes();
return decoderResult;
}
@override
List<int> encode(dynamic data) {
return data as List<int>;
}
}
abstract interface class ChannelInitializer {
void initChannel(Channel channel);
}

View File

@@ -38,8 +38,9 @@ class HostAndPort {
String scheme;
String host;
final int port;
bool ipv6 = false;
HostAndPort(this.scheme, this.host, this.port);
HostAndPort(this.scheme, this.host, this.port, {this.ipv6 = false});
factory HostAndPort.host(String host, int port, {String? scheme}) {
return HostAndPort(scheme ?? (port == 443 ? httpsScheme : httpScheme), host, port);
@@ -74,15 +75,20 @@ class HostAndPort {
return HostAndPort(scheme, domain, scheme == httpScheme ? 80 : 443);
}
}
//ip格式 host:port
List<String> hostAndPort = domain.split(":");
if (hostAndPort.length == 2) {
bool isSsl = ssl ?? hostAndPort[1] == "443";
var indexOf = domain.lastIndexOf(':');
String host = domain.substring(0, indexOf == -1 ? domain.length : indexOf);
String? port = indexOf == -1 ? null : domain.substring(indexOf + 1, domain.length);
bool ipv6 = host.startsWith('[') && host.endsWith(']');
if (port != null) {
bool isSsl = port == "443" || ssl == true;
scheme ??= isSsl ? httpsScheme : httpScheme;
return HostAndPort(scheme, hostAndPort[0], int.parse(hostAndPort[1]));
return HostAndPort(scheme, host, int.parse(port), ipv6: ipv6);
}
scheme ??= (ssl == true ? httpsScheme : httpScheme);
return HostAndPort(scheme, hostAndPort[0], scheme == httpScheme ? 80 : 443);
return HostAndPort(scheme, host, scheme == httpScheme ? 80 : 443, ipv6: ipv6);
}
String get domain {

View File

@@ -19,9 +19,11 @@ import 'dart:io';
import 'dart:typed_data';
import 'package:proxypin/network/bin/configuration.dart';
import 'package:proxypin/network/channel.dart';
import 'package:proxypin/network/channel/channel.dart';
import 'package:proxypin/network/channel/channel_context.dart';
import 'package:proxypin/network/channel/channel_dispatcher.dart';
import 'package:proxypin/network/components/host_filter.dart';
import 'package:proxypin/network/handler.dart';
import 'package:proxypin/network/handle/relay_handle.dart';
import 'package:proxypin/network/socks/socks5.dart';
import 'package:proxypin/network/util/attribute_keys.dart';
import 'package:proxypin/network/util/crts.dart';
@@ -29,6 +31,7 @@ import 'package:proxypin/network/util/logger.dart';
import 'package:proxypin/network/util/process_info.dart';
import 'package:proxypin/network/util/tls.dart';
import '../bin/listener.dart';
import 'host_port.dart';
abstract class Network {
@@ -41,12 +44,12 @@ abstract class Network {
Channel listen(Channel channel, ChannelContext channelContext) {
_channelInitializer.call(channel);
channel.pipeline.channelActive(channelContext, channel);
channel.dispatcher.channelActive(channelContext, channel);
channel.socket.listen((data) => onEvent(data, channelContext, channel),
onError: (error, StackTrace trace) =>
channel.pipeline.exceptionCaught(channelContext, channel, error, trace: trace),
onDone: () => channel.pipeline.channelInactive(channelContext, channel));
channel.dispatcher.exceptionCaught(channelContext, channel, error, trace: trace),
onDone: () => channel.dispatcher.channelInactive(channelContext, channel));
return channel;
}
@@ -55,8 +58,8 @@ abstract class Network {
///
void relay(Channel clientChannel, Channel remoteChannel) {
var rawCodec = RawCodec();
clientChannel.pipeline.channelHandle(rawCodec, RelayHandler(remoteChannel));
remoteChannel.pipeline.channelHandle(rawCodec, RelayHandler(clientChannel));
clientChannel.dispatcher.channelHandle(rawCodec, RelayHandler(remoteChannel));
remoteChannel.dispatcher.channelHandle(rawCodec, RelayHandler(clientChannel));
}
}
@@ -114,7 +117,7 @@ class Server extends Network {
var remoteChannel = channelContext.serverChannel ??
await channelContext.connectServerChannel(hostAndPort!, RelayHandler(channel));
relay(channel, remoteChannel);
channel.pipeline.channelRead(channelContext, channel, data);
channel.dispatcher.channelRead(channelContext, channel, data);
return;
}
@@ -125,20 +128,22 @@ class Server extends Network {
}
//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));
if (configuration.enableSocks5 && Socks5.isSocks5(data) && channel.dispatcher.handler is! SocksServerHandler) {
channel.dispatcher.channelHandle(RawCodec(),
SocksServerHandler(channel.dispatcher.decoder, channel.dispatcher.encoder, channel.dispatcher.handler));
}
channel.pipeline.channelRead(channelContext, channel, data);
channel.dispatcher.channelRead(channelContext, channel, data);
}
/// ssl握手
void ssl(ChannelContext channelContext, Channel channel, Uint8List data) async {
var hostAndPort = channelContext.host;
try {
String? serviceName = TLS.getDomain(data) ?? hostAndPort?.host;
if (hostAndPort == null) {
var domain = TLS.getDomain(data);
var domain = serviceName;
var port = 443;
if (domain == null) {
var process = await ProcessInfoUtils.getProcessByPort(
@@ -157,17 +162,17 @@ class Server extends Network {
if (HostFilter.filter(hostAndPort.host) || !configuration.enableSsl) {
remoteChannel = remoteChannel ?? await channelContext.connectServerChannel(hostAndPort, RelayHandler(channel));
relay(channel, remoteChannel);
channel.pipeline.channelRead(channelContext, channel, data);
channel.dispatcher.channelRead(channelContext, channel, data);
return;
}
if (remoteChannel != null && !remoteChannel.isSsl) {
var supportProtocols = configuration.enabledHttp2 ? TLS.supportProtocols(data) : null;
await remoteChannel.secureSocket(channelContext, host: hostAndPort.host, supportedProtocols: supportProtocols);
await remoteChannel.secureSocket(channelContext, host: serviceName, supportedProtocols: supportProtocols);
}
//ssl自签证书
var certificate = await CertificateManager.getCertificateContext(hostAndPort.host);
var certificate = await CertificateManager.getCertificateContext(serviceName!);
var selectedProtocol = remoteChannel?.selectedProtocol;
if (selectedProtocol != null) certificate.setAlpnProtocols([selectedProtocol], true);
@@ -184,7 +189,7 @@ class Server extends Network {
}
channelContext.host ??= hostAndPort;
channel.pipeline.exceptionCaught(channelContext, channel, error, trace: trace);
channel.dispatcher.exceptionCaught(channelContext, channel, error, trace: trace);
}
}
}
@@ -195,7 +200,7 @@ class Client extends Network {
String host = hostAndPort.host;
//ipv6
if (host.startsWith("[") && host.endsWith(']')) {
host = host.substring(host.lastIndexOf(":") + 1, host.length - 1);
host = host.substring(1, host.length - 1);
}
return Socket.connect(host, hostAndPort.port, timeout: timeout).then((socket) {
@@ -220,6 +225,6 @@ class Client extends Network {
@override
Future<void> onEvent(Uint8List data, ChannelContext channelContext, Channel channel) async {
channel.pipeline.channelRead(channelContext, channel, data);
channel.dispatcher.channelRead(channelContext, channel, data);
}
}

View File

@@ -15,7 +15,7 @@
*/
import 'package:proxypin/network/components/manager/hosts_manager.dart';
import 'package:proxypin/network/host_port.dart';
import 'package:proxypin/network/channel/host_port.dart';
import 'package:proxypin/network/util/logger.dart';
import 'interceptor.dart';

View File

@@ -1,4 +1,4 @@
import 'package:proxypin/network/host_port.dart';
import 'package:proxypin/network/channel/host_port.dart';
import 'package:proxypin/network/http/http.dart';
/// A Interceptor that can intercept and modify the request and response.

View File

@@ -17,12 +17,17 @@
import 'dart:io';
import 'package:flutter_js/flutter_js.dart';
import 'package:path_provider/path_provider.dart';
import 'package:proxypin/network/util/logger.dart';
/// FileBridge for file operation
/// @Author: Hongen Wang
class FileBridge {
static const String code = '''
function getApplicationSupportDirectory() {
return sendMessage('getApplicationSupportDirectory', JSON.stringify(''));
}
function File(path) {
return {
path: path,
@@ -92,6 +97,10 @@ class FileBridge {
logger.e('registerFile error: ${result.stringResult}');
}
flutterJs.onMessage('getApplicationSupportDirectory', (args) {
return getApplicationSupportDirectory().then((dir) => dir.path);
});
flutterJs.onMessage('file.readAsString', (path) {
return File(path).readAsString();
});

View File

@@ -0,0 +1,473 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter_js/javascript_runtime.dart';
import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';
import 'package:proxypin/network/bin/server.dart';
import 'package:proxypin/network/util/file_read.dart';
import 'package:proxypin/network/util/logger.dart';
import 'package:proxypin/utils/platform.dart';
/*
* Based on bits and pieces from different OSS sources
*
* 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
*
* http://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.
*/
// ignore: non_constant_identifier_names
var _XHR_DEBUG = false;
setXhrDebug(bool value) => _XHR_DEBUG = value;
const HTTP_GET = "get";
const HTTP_POST = "post";
const HTTP_PATCH = "patch";
const HTTP_DELETE = "delete";
const HTTP_PUT = "put";
const HTTP_HEAD = "head";
enum HttpMethod { put, get, post, delete, patch, head }
String _debugSendNativeCallback() {
if (_XHR_DEBUG) {
return """console.log("XMLHttpRequest._send_native_callback");
console.log("arguments");
console.log(arguments);
console.log(responseInfo);
console.log(responseText);
console.log(error);""";
} else
return "";
}
final String xhrJsCode = """
function XMLHttpRequest() {
this._send_native = XMLHttpRequestExtension_send_native;
this._httpMethod = null;
this._url = null;
this._requestHeaders = [];
this._responseHeaders = [];
this.response = null;
this.responseText = null;
this.responseXML = null;
this.onreadystatechange = null;
this.onloadstart = null;
this.onprogress = null;
this.onabort = null;
this.onerror = null;
this.onload = null;
this.onloadend = null;
this.ontimeout = null;
this.readyState = 0;
this.status = 0;
this.statusText = "";
this.withCredentials = null;
};
// readystate enum
XMLHttpRequest.UNSENT = 0;
XMLHttpRequest.OPENED = 1;
XMLHttpRequest.HEADERS = 2;
XMLHttpRequest.LOADING = 3;
XMLHttpRequest.DONE = 4;
XMLHttpRequest.prototype.constructor = XMLHttpRequest;
XMLHttpRequest.prototype.open = function(httpMethod, url) {
this._httpMethod = httpMethod;
this._url = url;
this.readyState = XMLHttpRequest.OPENED;
if (typeof this.onreadystatechange === "function") {
//console.log("Calling onreadystatechange(OPENED)...");
this.onreadystatechange();
}
};
XMLHttpRequest.prototype.send = function(data) {
this.readyState = XMLHttpRequest.LOADING;
if (typeof this.onreadystatechange === "function") {
//console.log("Calling onreadystatechange(LOADING)...");
this.onreadystatechange();
}
if (typeof this.onloadstart === "function") {
//console.log("Calling onloadstart()...");
this.onloadstart();
}
var that = this;
this._send_native(this._httpMethod, this._url, this._requestHeaders, data || null, function(responseInfo, responseText, error) {
that._send_native_callback(responseInfo, responseText, error);
}, this);
};
XMLHttpRequest.prototype.abort = function() {
this.readyState = XMLHttpRequest.UNSENT;
// Note: this.onreadystatechange() is not supposed to be called according to the XHR specs
}
// responseInfo: {statusCode, statusText, responseHeaders}
XMLHttpRequest.prototype._send_native_callback = function(responseInfo, responseText, error) {
${_debugSendNativeCallback()}
if (this.readyState === XMLHttpRequest.UNSENT) {
console.log("XHR native callback ignored because the request has been aborted");
if (typeof this.onabort === "function") {
//console.log("Calling onabort()...");
this.onabort();
}
return;
}
if (this.readyState != XMLHttpRequest.LOADING) {
// Request was not expected
console.log("XHR native callback ignored because the current state is not LOADING");
return;
}
// Response info
// TODO: responseXML?
this.responseURL = this._url;
this.status = responseInfo.statusCode;
this.statusText = responseInfo.statusText;
this.responseBody = responseInfo.body;
this._responseHeaders = responseInfo.responseHeaders || [];
this.readyState = XMLHttpRequest.DONE;
// Response
this.response = null;
this.responseText = null;
this.responseXML = null;
if (error) {
this.responseText = error;
} else {
this.responseText = responseText;
this.response = {
body: responseInfo.body,
}
// console.log('RESPONSE TEXT: ' + responseText);
}
this.readyState = XMLHttpRequest.DONE;
if (typeof this.onreadystatechange === "function") {
//console.log("Calling onreadystatechange(DONE)...");
this.onreadystatechange();
}
if (error === "timeout") {
// Timeout
console.warn("Got XHR timeout");
if (typeof this.ontimeout === "function") {
//console.log("Calling ontimeout()...");
this.ontimeout();
}
} else if (error) {
// Error
console.warn("Got XHR error:", error);
if (typeof this.onerror === "function") {
//console.log("Calling onerror()...");
this.onerror();
}
} else {
// Success
//console.log("XHR success: response =", this.response);
if (typeof this.onload === "function") {
//console.log("Calling onload()...");
this.onload();
}
}
if (typeof this.onloadend === "function") {
//console.log("Calling onloadend()...");
this.onloadend();
}
};
XMLHttpRequest.prototype.setRequestHeader = function(header, value) {
this._requestHeaders.push([header, value]);
};
XMLHttpRequest.prototype.getAllResponseHeaders = function() {
var ret = "";
for (var i = 0; i < this._responseHeaders.length; i++) {
var keyValue = this._responseHeaders[i];
ret += keyValue[0] + ": " + keyValue[1] + "\\r\\n";
}
return ret;
};
XMLHttpRequest.prototype.getResponseHeader = function(name) {
var ret = "";
for (var i = 0; i < this._responseHeaders.length; i++) {
var keyValue = this._responseHeaders[i];
if (keyValue[0] !== name) continue;
if (ret === "") ret += ", ";
ret += keyValue[1];
}
return ret;
};
// XMLHttpRequest.prototype.overrideMimeType = function() {
// // TODO
// };
this.XMLHttpRequest = XMLHttpRequest;""";
RegExp regexpHeader = RegExp("^([\\w-])+:(?!\\s*\$).+\$");
class XhrPendingCall {
int? idRequest;
String? method;
String? url;
Map<String, String> headers;
String? body;
XhrPendingCall({
required this.idRequest,
required this.method,
required this.url,
required this.headers,
required this.body,
});
}
const XHR_PENDING_CALLS_KEY = "xhrPendingCalls";
http.Client? httpClient;
xhrSetHttpClient(http.Client client) {
httpClient = client;
}
extension JavascriptRuntimeXhrExtension on JavascriptRuntime {
List<dynamic>? getPendingXhrCalls() {
return dartContext[XHR_PENDING_CALLS_KEY];
}
bool hasPendingXhrCalls() => getPendingXhrCalls()!.isNotEmpty;
void clearXhrPendingCalls() {
dartContext[XHR_PENDING_CALLS_KEY] = [];
}
Future<void> enableFetch2({bool enabledProxy = false}) async {
enableXhr2(enabledProxy: enabledProxy);
final fetchPolyfill = await FileRead.readAsString('assets/js/fetch.js');
final evalFetchResult = evaluate(fetchPolyfill);
logger.d('Eval Fetch Result: $evalFetchResult');
}
Future<http.Client> createClient(enabledProxy) async {
if (!enabledProxy) {
return http.Client();
}
// ProxyServer.current.isRunning
var httpClient = HttpClient();
print(ProxyServer.current?.isRunning);
String proxy;
if (Platforms.isDesktop()) {
Map? proxyResult = await DesktopMultiWindow.invokeMethod(0, 'getProxyInfo');
if (proxyResult == null) {
return http.Client();
}
proxy = "${proxyResult['host']}:${proxyResult['port']}";
} else {
if (ProxyServer.current?.isRunning == true) {
proxy = "127.0.0.1:${ProxyServer.current!.port}";
} else {
return http.Client();
}
}
httpClient.findProxy = (uri) {
return "PROXY $proxy";
};
httpClient.badCertificateCallback = (X509Certificate cert, String host, int port) => true;
// 创建一个 IOClient 实例,将 HttpClient 传入
return IOClient(httpClient);
}
void enableXhr2({bool enabledProxy = false}) async {
httpClient = httpClient ?? await createClient(enabledProxy);
dartContext[XHR_PENDING_CALLS_KEY] = [];
Timer.periodic(Duration(milliseconds: 40), (timer) {
// exits if there is no pending call to remote
if (!hasPendingXhrCalls()) return;
// collect the pending calls into a local variable making copies
List<dynamic> pendingCalls = List<dynamic>.from(getPendingXhrCalls()!);
// clear the global pending calls list
clearXhrPendingCalls();
// for each pending call, calls the remote http service
pendingCalls.forEach((element) async {
XhrPendingCall pendingCall = element as XhrPendingCall;
HttpMethod eMethod = HttpMethod.values
.firstWhere((e) => e.toString().toLowerCase() == ("HttpMethod.${pendingCall.method}".toLowerCase()));
late http.Response response;
switch (eMethod) {
case HttpMethod.head:
response = await httpClient!.head(
Uri.parse(pendingCall.url!),
headers: pendingCall.headers,
);
break;
case HttpMethod.get:
response = await httpClient!.get(
Uri.parse(pendingCall.url!),
headers: pendingCall.headers,
);
break;
case HttpMethod.post:
response = await httpClient!.post(
Uri.parse(pendingCall.url!),
body: (pendingCall.body is String) ? pendingCall.body : jsonEncode(pendingCall.body),
headers: pendingCall.headers,
);
break;
case HttpMethod.put:
response = await httpClient!.put(
Uri.parse(pendingCall.url!),
body: (pendingCall.body is String) ? pendingCall.body : jsonEncode(pendingCall.body),
headers: pendingCall.headers,
);
break;
case HttpMethod.patch:
response = await httpClient!.patch(
Uri.parse(pendingCall.url!),
body: (pendingCall.body is String) ? pendingCall.body : jsonEncode(pendingCall.body),
headers: pendingCall.headers,
);
break;
case HttpMethod.delete:
response = await httpClient!.delete(
Uri.parse(pendingCall.url!),
headers: pendingCall.headers,
);
break;
}
// assuming request was successfully executed
String? responseText;
List<int>? body;
try {
responseText = utf8.decode(response.bodyBytes);
responseText = jsonEncode(json.decode(responseText));
} on Exception {
// responseText = response.body;
body = response.bodyBytes;
}
// logger.d('RESPONSE TEXT: $responseText');
final xhrResult = XmlHttpRequestResponse(
responseText: responseText,
responseInfo: XhtmlHttpResponseInfo(statusCode: 200, statusText: "OK", body: body),
);
final responseInfo = jsonEncode(xhrResult.responseInfo);
//final responseText = xhrResult.responseText; //.replaceAll("\\n", "\\\n");
final error = xhrResult.error;
// send back to the javascript environment the
// response for the http pending callback
this.evaluate(
"globalThis.xhrRequests[${pendingCall.idRequest}].callback($responseInfo, `$responseText`, $error);",
);
});
});
this.evaluate("""
var xhrRequests = {};
var idRequest = -1;
function XMLHttpRequestExtension_send_native() {
idRequest += 1;
var cb = arguments[4];
var context = arguments[5];
xhrRequests[idRequest] = {
callback: function(responseInfo, responseText, error) {
cb(responseInfo, responseText, error);
}
};
var args = [];
args[0] = arguments[0];
args[1] = arguments[1];
args[2] = arguments[2];
args[3] = arguments[3];
args[4] = idRequest;
sendMessage('SendNative', JSON.stringify(args));
}
""");
final evalXhrResult = this.evaluate(xhrJsCode);
if (_XHR_DEBUG) print('RESULT evalXhrResult: $evalXhrResult');
this.onMessage('SendNative', (arguments) {
try {
String? method = arguments[0];
String? url = arguments[1];
dynamic headersList = arguments[2];
String? body = arguments[3];
int? idRequest = arguments[4];
Map<String, String> headers = {};
headersList.forEach((header) {
// final headerMatch = regexpHeader.allMatches(value).first;
// String? headerName = headerMatch.group(0);
// String? headerValue = headerMatch.group(1);
// if (headerName != null) {
// headers[headerName] = headerValue ?? '';
// }
String headerKey = header[0];
headers[headerKey] = header[1];
});
(dartContext[XHR_PENDING_CALLS_KEY] as List<dynamic>).add(
XhrPendingCall(
idRequest: idRequest,
method: method,
url: url,
headers: headers,
body: body,
),
);
} on Error catch (e) {
if (_XHR_DEBUG) print('ERROR calling sendNative on Dart: >>>> $e');
} on Exception catch (e) {
if (_XHR_DEBUG) print('Exception calling sendNative on Dart: >>>> $e');
}
});
}
}
class XhtmlHttpResponseInfo {
final int? statusCode;
final String? statusText;
final List<int>? body;
final List<List<String>> responseHeaders = [];
XhtmlHttpResponseInfo({
this.body,
this.statusCode,
this.statusText,
});
void addResponseHeaders(String name, String value) {
responseHeaders.add([name, value]);
}
Map<String, Object?> toJson() {
return {
"statusCode": statusCode,
"statusText": statusText,
"body": body,
"responseHeaders": jsonEncode(responseHeaders)
};
}
}
class XmlHttpRequestResponse {
final String? responseText;
final String? error; // should be timeout in case of timeout
final XhtmlHttpResponseInfo? responseInfo;
XmlHttpRequestResponse({this.responseText, this.responseInfo, this.error});
Map<String, Object?> toJson() {
return {'responseText': responseText, 'responseInfo': responseInfo!.toJson(), 'error': error};
}
}

View File

@@ -115,7 +115,7 @@ class RequestBlockItem {
//匹配url
bool match(String url, BlockType blockType) {
urlReg ??= RegExp('^${this.url.replaceAll("*", ".*")}');
urlReg ??= RegExp(this.url.replaceAll("*", ".*"));
return enabled && type == blockType && urlReg!.hasMatch(url);
}

View File

@@ -21,7 +21,9 @@ import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter_js/flutter_js.dart';
import 'package:proxypin/network/components/js/file.dart';
import 'package:proxypin/network/components/js/md5.dart';
import 'package:proxypin/network/components/js/xhr.dart';
import 'package:proxypin/network/http/http.dart';
import 'package:proxypin/network/http/http.dart' as http;
import 'package:proxypin/network/http/http_headers.dart';
import 'package:proxypin/network/util/lang.dart';
import 'package:proxypin/network/util/logger.dart';
@@ -69,7 +71,7 @@ async function onResponse(context, request, response) {
final Map<ScriptItem, String> _scriptMap = {};
static JavascriptRuntime flutterJs = getJavascriptRuntime();
static JavascriptRuntime flutterJs = getJavascriptRuntime(xhr: false);
static String? deviceId;
@@ -89,6 +91,9 @@ async function onResponse(context, request, response) {
deviceId = await DeviceUtils.deviceId();
Md5Bridge.registerMd5(flutterJs);
FileBridge.registerFile(flutterJs);
flutterJs.enableFetch2();
logger.d('init script manager $deviceId');
}
return _instance!;
@@ -252,9 +257,7 @@ async function onResponse(context, request, response) {
}
request.attributes['scriptContext'] = result['scriptContext'];
scriptSession = result['scriptContext']['session'] ?? {};
var httpRequest = convertHttpRequest(request, result);
return httpRequest;
request = convertHttpRequest(request, result);
}
}
return request;
@@ -283,7 +286,7 @@ async function onResponse(context, request, response) {
return null;
}
scriptSession = result['scriptContext']['session'] ?? {};
return convertHttpResponse(response, result);
response = convertHttpResponse(response, result);
}
}
return response;
@@ -349,7 +352,7 @@ async function onResponse(context, request, response) {
//http request
HttpRequest convertHttpRequest(HttpRequest request, Map<dynamic, dynamic> map) {
request.headers.clear();
request.method = HttpMethod.values.firstWhere((element) => element.name == map['method']);
request.method = http.HttpMethod.values.firstWhere((element) => element.name == map['method']);
String query = UriUtils.mapToQuery(map['queries']);
var requestUri = request.requestUri!.replace(path: map['path'], query: query);
@@ -364,7 +367,7 @@ async function onResponse(context, request, response) {
request.headers.addValues(key, value.map((e) => e.toString()).toList());
return;
}
request.headers.add(key, value);
request.headers.set(key, value);
});
//判断是否是二进制
@@ -391,7 +394,7 @@ async function onResponse(context, request, response) {
return;
}
response.headers.add(key, value);
response.headers.set(key, value);
});
response.headers.remove(HttpHeaders.CONTENT_ENCODING);

View File

@@ -1,48 +1,20 @@
/*
* Copyright 2023 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:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:proxypin/network/bin/listener.dart';
import 'package:proxypin/network/channel/channel.dart';
import 'package:proxypin/network/channel/channel_context.dart';
import 'package:proxypin/network/components/host_filter.dart';
import 'package:proxypin/network/components/interceptor.dart';
import 'package:proxypin/network/components/request_rewrite.dart';
import 'package:proxypin/network/host_port.dart';
import 'package:proxypin/network/channel/host_port.dart';
import 'package:proxypin/network/http/http.dart';
import 'package:proxypin/network/http/http_client.dart';
import 'package:proxypin/network/http/http_headers.dart';
import 'package:proxypin/network/http/websocket.dart';
import 'package:proxypin/network/proxy_helper.dart';
import 'package:proxypin/network/util/proxy_helper.dart';
import 'package:proxypin/network/util/attribute_keys.dart';
import 'package:proxypin/network/util/logger.dart';
import 'package:proxypin/network/util/uri.dart';
import 'package:proxypin/utils/ip.dart';
import 'channel.dart';
import 'components/interceptor.dart';
import 'http_client.dart';
///
abstract class EventListener {
void onRequest(Channel channel, HttpRequest request);
void onResponse(ChannelContext channelContext, HttpResponse response);
void onMessage(Channel channel, HttpMessage message, WebSocketFrame frame) {}
}
/// http请求处理器
class HttpProxyChannelHandler extends ChannelHandler<HttpRequest> {
EventListener? listener;
@@ -282,49 +254,3 @@ class HttpResponseProxyHandler extends ChannelHandler<HttpResponse> {
clientChannel.close();
}
}
class RelayHandler extends ChannelHandler<Object> {
final Channel remoteChannel;
RelayHandler(this.remoteChannel);
@override
void channelRead(ChannelContext channelContext, Channel channel, Object msg) async {
//
remoteChannel.write(msg);
}
@override
void channelInactive(ChannelContext channelContext, Channel channel) {
remoteChannel.close();
}
}
/// websocket处理器
class WebSocketChannelHandler extends ChannelHandler<Uint8List> {
final WebSocketDecoder decoder = WebSocketDecoder();
final Channel proxyChannel;
final HttpMessage message;
WebSocketChannelHandler(this.proxyChannel, this.message);
@override
void channelRead(ChannelContext channelContext, Channel channel, Uint8List msg) {
proxyChannel.write(msg);
WebSocketFrame? frame;
try {
frame = decoder.decode(msg);
} catch (e) {
log.e("websocket decode error", error: e);
}
if (frame == null) {
return;
}
frame.isFromClient = message is HttpRequest;
message.messages.add(frame);
channelContext.listener?.onMessage(channel, message, frame);
logger.d("socket channelRead ${frame.payloadLength} ${frame.fin} ${frame.payloadDataAsString}");
}
}

View File

@@ -0,0 +1,19 @@
import 'package:proxypin/network/channel/channel.dart';
import 'package:proxypin/network/channel/channel_context.dart';
class RelayHandler extends ChannelHandler<Object> {
final Channel remoteChannel;
RelayHandler(this.remoteChannel);
@override
void channelRead(ChannelContext channelContext, Channel channel, Object msg) async {
//发送给客户端
remoteChannel.write(msg);
}
@override
void channelInactive(ChannelContext channelContext, Channel channel) {
remoteChannel.close();
}
}

View File

@@ -0,0 +1,36 @@
import 'dart:typed_data';
import 'package:proxypin/network/channel/channel.dart';
import 'package:proxypin/network/channel/channel_context.dart';
import 'package:proxypin/network/http/http.dart';
import 'package:proxypin/network/http/websocket.dart';
import 'package:proxypin/network/util/logger.dart';
/// websocket处理器
class WebSocketChannelHandler extends ChannelHandler<Uint8List> {
final WebSocketDecoder decoder = WebSocketDecoder();
final Channel proxyChannel;
final HttpMessage message;
WebSocketChannelHandler(this.proxyChannel, this.message);
@override
void channelRead(ChannelContext channelContext, Channel channel, Uint8List msg) {
proxyChannel.write(msg);
WebSocketFrame? frame;
try {
frame = decoder.decode(msg);
} catch (e) {
log.e("websocket decode error", error: e);
}
if (frame == null) {
return;
}
frame.isFromClient = message is HttpRequest;
message.messages.add(frame);
channelContext.listener?.onMessage(channel, message, frame);
logger.d("socket channelRead ${frame.payloadLength} ${frame.fin} ${frame.payloadDataAsString}");
}
}

View File

@@ -25,9 +25,11 @@ import 'codec.dart';
class Result {
final bool isDone;
final bool supportedParse;
Uint8List? body;
Result(this.isDone, {this.body});
Result(this.isDone, {this.body, this.supportedParse = true});
}
class BodyReader {
@@ -56,6 +58,9 @@ class BodyReader {
//chunked编码
if (message.headers.isChunked) {
_readChunked(data);
} else if (message.headers.contentType == 'video/x-flv') {
//Directly forward without processing for now
return Result(false, supportedParse: false, body: data);
} else {
_readFixedLengthContent(data);
}

View File

@@ -17,8 +17,8 @@
import 'dart:math';
import 'dart:typed_data';
import 'package:proxypin/network/channel.dart';
import 'package:proxypin/network/host_port.dart';
import 'package:proxypin/network/channel/channel_context.dart';
import 'package:proxypin/network/channel/host_port.dart';
import 'package:proxypin/network/http/body_reader.dart';
import 'package:proxypin/network/http/constants.dart';
import 'package:proxypin/network/http/h2/codec.dart';
@@ -51,11 +51,12 @@ enum State {
class DecoderResult<T> {
bool isDone = true;
T? data;
bool supportedParse;
//转发消息
List<int>? forward;
DecoderResult({this.isDone = true});
DecoderResult({this.isDone = true, this.supportedParse = true});
}
/// 解码
@@ -122,6 +123,13 @@ abstract class HttpCodec<T extends HttpMessage> implements Codec<T, T> {
_state = State.done;
result.data!.body = bodyResult?.body;
}
//If the body does not support parsing, forward directly
if (bodyResult != null && !bodyResult.supportedParse) {
result.supportedParse = false;
result.forward = bodyResult.body;
return result;
}
}
if (_state == State.done) {

View File

@@ -17,7 +17,7 @@
import 'dart:math';
import 'dart:typed_data';
import 'package:proxypin/network/channel.dart';
import 'package:proxypin/network/channel/channel_context.dart';
import 'package:proxypin/network/http/codec.dart';
import 'package:proxypin/network/http/h2/hpack.dart';
import 'package:proxypin/network/http/h2/setting.dart';

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
import 'package:proxypin/network/channel.dart';
import 'package:proxypin/network/channel/channel_context.dart';
import 'package:proxypin/network/http/h2/frame.dart';
import 'package:proxypin/network/util/byte_buf.dart';

View File

@@ -17,7 +17,7 @@
import 'dart:convert';
import 'dart:math';
import 'package:proxypin/network/host_port.dart';
import 'package:proxypin/network/channel/host_port.dart';
import 'package:proxypin/network/http/content_type.dart';
import 'package:proxypin/network/http/websocket.dart';
import 'package:proxypin/network/util/logger.dart';
@@ -342,7 +342,11 @@ class HttpStatus {
/// 504 Gateway Timeout
static final HttpStatus gatewayTimeout = newStatus(504, "Gateway Timeout");
static HttpStatus newStatus(int statusCode, String reasonPhrase) {
static HttpStatus newStatus(int statusCode, String? reasonPhrase) {
if (reasonPhrase == null) {
return HttpStatus.valueOf(statusCode);
}
return HttpStatus(statusCode, reasonPhrase);
}

View File

@@ -17,22 +17,23 @@
import 'dart:async';
import 'dart:convert';
import 'package:proxypin/network/host_port.dart';
import 'package:proxypin/network/channel/channel_context.dart';
import 'package:proxypin/network/channel/host_port.dart';
import 'package:proxypin/network/http/http.dart';
import 'package:proxypin/network/http/http_headers.dart';
import 'package:proxypin/network/network.dart';
import 'package:proxypin/network/channel/network.dart';
import 'package:proxypin/network/util/system_proxy.dart';
import 'package:proxy_manager/proxy_manager.dart';
import 'channel.dart';
import 'http/codec.dart';
import '../channel/channel.dart';
import 'codec.dart';
class HttpClients {
///
static Future<Channel> startConnect(
HostAndPort hostAndPort, ChannelHandler handler, ChannelContext channelContext) async {
var client = Client()
..initChannel((channel) => channel.pipeline.channelHandle(HttpClientCodec(), handler));
..initChannel((channel) => channel.dispatcher.channelHandle(HttpClientCodec(), handler));
return client.connect(hostAndPort, channelContext);
}
@@ -41,7 +42,7 @@ class HttpClients {
static Future<Channel> proxyConnect(HostAndPort hostAndPort, ChannelHandler handler, ChannelContext channelContext,
{ProxyInfo? proxyInfo}) async {
var client = Client()
..initChannel((channel) => channel.pipeline.channelHandle(HttpClientCodec(), handler));
..initChannel((channel) => channel.dispatcher.channelHandle(HttpClientCodec(), handler));
if (proxyInfo == null) {
var proxyTypes = hostAndPort.isSsl() ? ProxyTypes.https : ProxyTypes.http;
@@ -64,10 +65,10 @@ class HttpClients {
///
static Future<Channel> connectRequest(HostAndPort hostAndPort, Channel channel, {ProxyInfo? proxyInfo}) async {
ChannelHandler handler = channel.pipeline.handler;
ChannelHandler handler = channel.dispatcher.handler;
// connect请求
var httpResponseHandler = HttpResponseHandler();
channel.pipeline.handler = httpResponseHandler;
channel.dispatcher.handler = httpResponseHandler;
HttpRequest proxyRequest = HttpRequest(HttpMethod.connect, '${hostAndPort.host}:${hostAndPort.port}');
proxyRequest.headers.set(HttpHeaders.HOST, '${hostAndPort.host}:${hostAndPort.port}');
@@ -81,7 +82,7 @@ class HttpClients {
await channel.write(proxyRequest);
var response = await httpResponseHandler.getResponse(const Duration(seconds: 5));
channel.pipeline.handler = handler;
channel.dispatcher.handler = handler;
if (!response.status.isSuccessful()) {
throw Exception("$hostAndPort Proxy failed to establish tunnel "
@@ -94,7 +95,7 @@ class HttpClients {
///
static Future<Channel> connect(Uri uri, ChannelHandler handler, ChannelContext channelContext) async {
Client client = Client()
..initChannel((channel) => channel.pipeline.handle(HttpResponseCodec(), HttpRequestCodec(), handler));
..initChannel((channel) => channel.dispatcher.handle(HttpResponseCodec(), HttpRequestCodec(), handler));
if (uri.scheme == "https" || uri.scheme == "wss") {
return client.secureConnect(HostAndPort.of(uri.toString()), channelContext);
}
@@ -114,7 +115,7 @@ class HttpClients {
var httpResponseHandler = HttpResponseHandler();
var client = Client()
..initChannel((channel) => channel.pipeline.handle(HttpResponseCodec(), HttpRequestCodec(), httpResponseHandler));
..initChannel((channel) => channel.dispatcher.handle(HttpResponseCodec(), HttpRequestCodec(), httpResponseHandler));
ChannelContext channelContext = ChannelContext();
Channel channel = await client.connect(hostAndPort, channelContext);
@@ -125,7 +126,7 @@ class HttpClients {
///
static Future<HttpResponse> proxyRequest(HttpRequest request,
{ProxyInfo? proxyInfo, Duration timeout = const Duration(seconds: 15)}) async {
{ProxyInfo? proxyInfo, Duration timeout = const Duration(seconds: 30)}) async {
if (request.headers.host == null || request.headers.host?.trim().isEmpty == true) {
try {
var uri = Uri.parse(request.requestUrl);

View File

@@ -16,12 +16,13 @@
import 'dart:typed_data';
import 'package:proxypin/network/channel.dart';
import 'package:proxypin/network/channel/channel.dart';
import 'package:proxypin/network/channel/channel_context.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';
import '../channel/host_port.dart';
/// @author wanghongen
class Socks5 {
@@ -62,7 +63,7 @@ class SocksServerHandler extends ChannelHandler<Uint8List> {
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'));
channel.dispatcher.exceptionCaught(channelContext, channel, Exception('Unsupported SOCKS version: $version'));
return;
}
@@ -78,7 +79,7 @@ class SocksServerHandler extends ChannelHandler<Uint8List> {
if (cmd != Socks5.cmdConnect) {
var out = encodeCommandResponse(Socks5.repCommandNotSupported);
await channel.writeBytes(out);
channel.pipeline.exceptionCaught(channelContext, channel, Exception('Unsupported SOCKS cmd: $cmd'));
channel.dispatcher.exceptionCaught(channelContext, channel, Exception('Unsupported SOCKS cmd: $cmd'));
return;
}
@@ -89,7 +90,7 @@ class SocksServerHandler extends ChannelHandler<Uint8List> {
if (dstAddrType != Socks5.atypIpv4) {
var out = encodeCommandResponse(Socks5.repAddressTypeNotSupported);
await channel.writeBytes(out);
channel.pipeline.exceptionCaught(channelContext, channel, Exception('Unsupported SOCKS atyp: $dstAddrType'));
channel.dispatcher.exceptionCaught(channelContext, channel, Exception('Unsupported SOCKS atyp: $dstAddrType'));
return;
}
@@ -103,7 +104,7 @@ class SocksServerHandler extends ChannelHandler<Uint8List> {
final out = encodeCommandResponse(Socks5.repSuccess, bndAddrType: Socks5.repSocks5ServerAtypIpv4);
await channel.writeBytes(out);
channel.pipeline.handle(originalDecoder, originalEncoder, originalHandler);
channel.dispatcher.handle(originalDecoder, originalEncoder, originalHandler);
socksState = SocksState.connected;
return;
}

View File

@@ -19,6 +19,7 @@ import 'dart:typed_data';
import 'package:proxypin/native/installed_apps.dart';
import 'package:proxypin/native/process_info.dart';
import 'package:proxypin/network/util/logger.dart';
import 'package:proxypin/network/util/socket_address.dart';
import 'package:win32audio/win32audio.dart';
@@ -37,27 +38,32 @@ class ProcessInfoUtils {
static final processInfoCache = ExpiringCache<String, ProcessInfo>(const Duration(minutes: 5));
static Future<ProcessInfo?> getProcessByPort(InetSocketAddress socketAddress, String cacheKeyPre) async {
if (Platform.isAndroid) {
var app = await ProcessInfoPlugin.getProcessByPort(socketAddress.host, socketAddress.port);
if (app != null) {
return app;
}
if (socketAddress.host == '127.0.0.1') {
return ProcessInfo('com.network.proxy', "ProxyPin", '', os: Platform.operatingSystem);
try {
if (Platform.isAndroid) {
var app = await ProcessInfoPlugin.getProcessByPort(socketAddress.host, socketAddress.port);
if (app != null) {
return app;
}
if (socketAddress.host == '127.0.0.1') {
return ProcessInfo('com.network.proxy', "ProxyPin", '', os: Platform.operatingSystem);
}
return null;
}
var pid = await _getPid(socketAddress);
if (pid == null) return null;
String cacheKey = "$cacheKeyPre:$pid";
var processInfo = processInfoCache.get(cacheKey);
if (processInfo != null) return processInfo;
processInfo = await getProcess(pid);
processInfoCache.set(cacheKey, processInfo!);
return processInfo;
} catch (e) {
logger.e("getProcessByPort error: $e");
return null;
}
var pid = await _getPid(socketAddress);
if (pid == null) return null;
String cacheKey = "$cacheKeyPre:$pid";
var processInfo = processInfoCache.get(cacheKey);
if (processInfo != null) return processInfo;
processInfo = await getProcess(pid);
processInfoCache.set(cacheKey, processInfo!);
return processInfo;
}
// 获取进程 ID

View File

@@ -17,18 +17,19 @@
import 'dart:convert';
import 'dart:io';
import 'package:proxypin/network/channel.dart';
import 'package:proxypin/network/bin/listener.dart';
import 'package:proxypin/network/channel/channel.dart';
import 'package:proxypin/network/channel/channel_context.dart';
import 'package:proxypin/network/components/manager/request_rewrite_manager.dart';
import 'package:proxypin/network/components/manager/script_manager.dart';
import 'package:proxypin/network/handler.dart';
import 'package:proxypin/network/host_port.dart';
import 'package:proxypin/network/channel/host_port.dart';
import 'package:proxypin/network/http/codec.dart';
import 'package:proxypin/network/http/http.dart';
import 'package:proxypin/network/http/http_headers.dart';
import 'package:proxypin/network/util/crts.dart';
import 'package:proxypin/network/util/localizations.dart';
import 'components/host_filter.dart';
import '../components/host_filter.dart';
class ProxyHelper {
//

View File

@@ -16,7 +16,7 @@
import 'dart:io';
import 'package:proxypin/network/host_port.dart';
import 'package:proxypin/network/channel/host_port.dart';
import 'package:proxypin/network/util/logger.dart';
import 'package:proxypin/utils/ip.dart';
import 'package:proxypin/utils/lang.dart';

View File

@@ -163,9 +163,9 @@ class HistoryStorage {
var json = jsonDecode(readAsBytes);
var log = json['log'];
String name = formatDate(DateTime.now(), [mm, '-', d, ' ', HH, ':', nn, ':', ss]);
List? pages = log['pages'] as List;
if (pages.isNotEmpty) {
name = pages.first['title'];
List? pages = log['pages'] as List?;
if (pages?.isNotEmpty == true) {
name = pages?.first['title'];
}
//解析请求

View File

@@ -0,0 +1,107 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:http/http.dart' as http;
import 'package:proxypin/network/util/logger.dart';
import 'package:proxypin/ui/app_update/remote_version_entity.dart';
import 'package:proxypin/ui/component/app_dialog.dart';
import 'package:proxypin/ui/configuration.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'constants.dart';
import 'new_version_dialog.dart';
class AppUpdateRepository {
static final HttpClient httpClient = HttpClient();
static Future<void> checkUpdate(BuildContext context, {bool canIgnore = true, bool showToast = false}) async {
try {
var lastVersion = await getLatestVersion();
if (lastVersion == null) {
logger.w("[AppUpdate] failed to fetch latest version info");
return;
}
if (!context.mounted) return;
var availableUpdates = compareVersions(AppConfiguration.version, lastVersion.version);
if (availableUpdates) {
if (canIgnore) {
var ignoreVersion = await SharedPreferencesAsync().getString(Constants.ignoreReleaseVersionKey);
if (ignoreVersion == lastVersion.version) {
logger.d("ignored release [${lastVersion.version}]");
return;
}
}
logger.d("new version available: $lastVersion");
if (!context.mounted) return;
NewVersionDialog(
AppConfiguration.version,
lastVersion,
canIgnore: true,
).show(context);
return;
}
logger.i("already using latest version[${AppConfiguration.version}], last: [${lastVersion.version}]");
if (showToast) {
AppLocalizations localizations = AppLocalizations.of(context)!;
CustomToast.success(localizations.appUpdateNotAvailableMsg).show(context);
}
} catch (e) {
logger.e("Error checking for updates: $e");
if (showToast) {
CustomToast.error(e.toString()).show(context);
}
}
}
/// Fetches the latest version information from the GitHub releases API.
static Future<RemoteVersionEntity?> getLatestVersion({bool includePreReleases = false}) async {
final response = await http.get(Uri.parse(Constants.githubReleasesApiUrl));
if (response.statusCode != 200 || response.body.isEmpty) {
logger.w("[AppUpdate] failed to fetch latest version info");
return null;
}
var body = jsonDecode(response.body) as List;
final releases = body.map((e) => GithubReleaseParser.parse(e as Map<String, dynamic>));
late RemoteVersionEntity latest;
if (includePreReleases) {
latest = releases.first;
} else {
latest = releases.firstWhere((e) => e.preRelease == false);
}
logger.d("[AppUpdate] latest version: $latest");
return latest;
}
static bool compareVersions(String currentVersion, String latestVersion) {
String normalizeVersion(String version) {
return version.startsWith('v') ? version.substring(1) : version;
}
List<int> parseVersion(String version) {
return normalizeVersion(version).split('.').map(int.parse).toList();
}
List<int> current = parseVersion(currentVersion);
List<int> latest = parseVersion(latestVersion);
for (int i = 0; i < current.length; i++) {
if (i >= latest.length || current[i] > latest[i]) {
return false; // 当前版本高于最新版本
} else if (current[i] < latest[i]) {
return true; // 需要更新
}
}
return latest.length > current.length; // 最新版本有更多的子版本号
}
}

View File

@@ -0,0 +1,11 @@
abstract class Constants {
static const githubUrl = "https://github.com/wanghongenpin/proxypin";
static const githubReleasesApiUrl =
"https://api.github.com/repos/wanghongenpin/proxypin/releases";
static const githubLatestReleaseUrl =
"https://github.com/wanghongenpin/proxypin/releases/latest";
static const String ignoreReleaseVersionKey = "ignored_release_version";
}
const kAnimationDuration = Duration(milliseconds: 250);

View File

@@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:proxypin/network/util/logger.dart';
import 'package:proxypin/ui/app_update/remote_version_entity.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher.dart';
import 'constants.dart';
class NewVersionDialog extends StatelessWidget {
NewVersionDialog(
this.currentVersion,
this.newVersion, {
this.canIgnore = true,
}) : super(key: _dialogKey);
final String currentVersion;
final RemoteVersionEntity newVersion;
final bool canIgnore;
static final _dialogKey = GlobalKey(debugLabel: 'new version dialog');
Future<void> show(BuildContext context) async {
if (_dialogKey.currentContext == null) {
return showDialog(
context: context,
useRootNavigator: true,
builder: (context) => this,
);
} else {
logger.d("new version dialog is already open");
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
AppLocalizations localizations = AppLocalizations.of(context)!;
return AlertDialog(
title: Text(localizations.appUpdateDialogTitle),
// scrollable: true,
content: Container(
constraints: BoxConstraints(maxHeight: 230, maxWidth: 500),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(localizations.appUpdateUpdateMsg),
const SizedBox(height: 5),
Text.rich(
TextSpan(
children: [
TextSpan(text: "${localizations.appUpdateCurrentVersionLbl}: ", style: theme.textTheme.bodySmall),
TextSpan(text: currentVersion, style: theme.textTheme.labelMedium),
],
),
),
Text.rich(
TextSpan(
children: [
TextSpan(text: "${localizations.appUpdateNewVersionLbl}: ", style: theme.textTheme.bodySmall),
TextSpan(text: newVersion.version, style: theme.textTheme.labelMedium),
],
),
),
Text(newVersion.content ?? '', style: theme.textTheme.labelMedium),
],
))),
actions: [
if (canIgnore)
TextButton(
onPressed: () async {
SharedPreferencesAsync().setString(Constants.ignoreReleaseVersionKey, newVersion.version);
logger.i("ignored release [${newVersion.version}]");
if (context.mounted) Navigator.pop(context);
},
child: Text(localizations.appUpdateIgnoreBtnTxt),
),
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(localizations.appUpdateLaterBtnTxt),
),
TextButton(
onPressed: () async {
await launchUrl(Uri.parse(newVersion.url), mode: LaunchMode.externalApplication);
},
child: Text(localizations.appUpdateUpdateNowBtnTxt),
),
],
);
}
}

View File

@@ -0,0 +1,49 @@
import 'package:proxypin/utils/lang.dart';
class RemoteVersionEntity {
final String version;
final String buildNumber;
final String releaseTag;
final bool preRelease;
final String url;
final String? content;
final DateTime publishedAt;
RemoteVersionEntity({
required this.version,
required this.buildNumber,
required this.releaseTag,
required this.preRelease,
required this.url,
this.content,
required this.publishedAt,
});
@override
String toString() {
return 'RemoteVersionEntity(version: $version, buildNumber: $buildNumber, releaseTag: $releaseTag, preRelease: $preRelease, url: $url, publishedAt: $publishedAt)';
}
}
abstract class GithubReleaseParser {
static RemoteVersionEntity parse(Map<String, dynamic> json) {
final fullTag = json['tag_name'] as String;
final fullVersion = fullTag.removePrefix("v").split("-").first.split("+");
var version = fullVersion.first;
var buildNumber = fullVersion.elementAtOrElse(1, (index) => "");
final preRelease = json["prerelease"] as bool;
final publishedAt = DateTime.parse(json["published_at"] as String);
var body = json['body']?.toString().split("English: ");
return RemoteVersionEntity(
version: version,
buildNumber: buildNumber,
releaseTag: fullTag,
preRelease: preRelease,
url: json["html_url"] as String,
content: body?.last,
publishedAt: publishedAt);
}
}

View File

@@ -0,0 +1,124 @@
import 'package:flutter/material.dart';
import 'package:toastification/toastification.dart';
class AppAlertDialog extends StatelessWidget {
const AppAlertDialog({
super.key,
this.title,
required this.message,
});
final String? title;
final String message;
factory AppAlertDialog.fromErr(({String type, String? message}) err) => AppAlertDialog(
title: err.message == null ? null : err.type,
message: err.message ?? err.type,
);
Future<void> show(BuildContext context) async {
await showDialog(
context: context,
useRootNavigator: true,
builder: (context) => this,
);
}
@override
Widget build(BuildContext context) {
final localizations = MaterialLocalizations.of(context);
return AlertDialog(
title: title != null ? Text(title!) : null,
content: SingleChildScrollView(
child: SizedBox(
width: 468,
child: Text(message),
),
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(localizations.okButtonLabel),
),
],
);
}
}
enum AlertType {
info,
error,
success;
ToastificationType get _toastificationType => switch (this) {
success => ToastificationType.success,
error => ToastificationType.error,
info => ToastificationType.info,
};
}
class CustomToast extends StatelessWidget {
const CustomToast(
this.message, {
super.key,
this.type = AlertType.info,
this.icon,
this.duration = const Duration(seconds: 3),
});
const CustomToast.error(
this.message, {
super.key,
this.duration = const Duration(seconds: 5),
}) : type = AlertType.error,
icon = Icons.error;
const CustomToast.success(
this.message, {
super.key,
this.duration = const Duration(seconds: 3),
}) : type = AlertType.success,
icon = Icons.check_circle;
final String message;
final AlertType type;
final IconData? icon;
final Duration duration;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(4)),
color: Theme.of(context).colorScheme.surface,
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(child: Text(message)),
],
),
);
}
void show(BuildContext context) {
toastification.show(
context: context,
title: Text(message),
icon: icon == null ? null : Icon(icon),
type: type._toastificationType,
alignment: Alignment.bottomLeft,
autoCloseDuration: duration,
style: ToastificationStyle.flat,
pauseOnHover: true,
showProgressBar: false,
dragToClose: true,
closeOnClick: true,
closeButton: ToastCloseButton(showType: CloseButtonShowType.onHover),
);
}
}

View File

@@ -18,6 +18,7 @@ import 'dart:convert';
import 'dart:io';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:path_provider/path_provider.dart';
@@ -89,7 +90,7 @@ Widget multiWindow(int windowId, Map<dynamic, dynamic> argument) {
}
if (argument['name'] == 'JavaScript') {
return const JavaScript();
return JavaScript(windowId: windowId);
}
if (argument['name'] == 'RegExpPage') {
@@ -215,6 +216,20 @@ void registerMethodHandler() {
return 'done';
}
if (call.method == 'pickFiles') {
var extensions = call.arguments['allowedExtensions'];
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: extensions == null ? FileType.any : FileType.custom,
allowedExtensions: extensions == null ? null : List.from(extensions),
initialDirectory: "/Downloads");
if (result == null || result.files.isEmpty) return null;
return result.files.single.path;
}
if (call.method == 'saveFile') {
return await FilePicker.platform.saveFile(fileName: call.arguments['fileName']);
}
if (call.method == 'getApplicationSupportDirectory') {
return getApplicationSupportDirectory().then((it) => it.path);
}

View File

@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -11,9 +12,12 @@ import 'package:flutter_toastr/flutter_toastr.dart';
import 'package:highlight/languages/javascript.dart';
import 'package:proxypin/network/components/js/file.dart';
import 'package:proxypin/network/components/js/md5.dart';
import 'package:proxypin/network/components/js/xhr.dart';
class JavaScript extends StatefulWidget {
const JavaScript({super.key});
final int? windowId;
const JavaScript({super.key, this.windowId});
@override
State<StatefulWidget> createState() {
@@ -40,13 +44,14 @@ class _JavaScriptState extends State<JavaScript> {
void initState() {
super.initState();
if (resetEnvironment || flutterJs == null) {
flutterJs = getJavascriptRuntime();
flutterJs = getJavascriptRuntime(xhr: false);
}
// register channel callback
final channelCallbacks = JavascriptRuntime.channelFunctionsRegistered[flutterJs!.getEngineInstanceId()];
channelCallbacks!["ConsoleLog"] = consoleLog;
Md5Bridge.registerMd5(flutterJs!);
FileBridge.registerFile(flutterJs!);
flutterJs?.enableFetch2(enabledProxy: true);
code = CodeController(language: javascript, text: 'console.log("Hello, World!")');
}
@@ -86,10 +91,20 @@ class _JavaScriptState extends State<JavaScript> {
//选择文件
ElevatedButton.icon(
onPressed: () async {
FilePickerResult? result =
await FilePicker.platform.pickFiles(type: FileType.custom, allowedExtensions: ['js']);
if (result != null) {
File file = File(result.files.single.path!);
String? path;
if (Platform.isMacOS) {
path = await DesktopMultiWindow.invokeMethod(0, "pickFiles", {
"allowedExtensions": ['js']
});
WindowController.fromWindowId(widget.windowId!).show();
} else {
FilePickerResult? result =
await FilePicker.platform.pickFiles(type: FileType.custom, allowedExtensions: ['js']);
path = result?.files.single.path;
}
if (path != null) {
File file = File(path);
String content = await file.readAsString();
code.text = content;
setState(() {});
@@ -145,7 +160,8 @@ class _JavaScriptState extends State<JavaScript> {
))))),
const SizedBox(height: 10),
Row(children: [
Text("${localizations.output}:", style: TextStyle(fontSize: 16, color: primaryColor, fontWeight: FontWeight.w500)),
Text("${localizations.output}:",
style: TextStyle(fontSize: 16, color: primaryColor, fontWeight: FontWeight.w500)),
const SizedBox(width: 15),
//copy
IconButton(

View File

@@ -24,9 +24,10 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_qr_reader/flutter_qr_reader.dart';
import 'package:flutter_qr_reader_plus/flutter_qr_reader.dart';
import 'package:flutter_toastr/flutter_toastr.dart';
import 'package:image_pickers/image_pickers.dart';
import 'package:proxypin/ui/component/app_dialog.dart';
import 'package:proxypin/ui/component/qrcode/qr_scan_view.dart';
import 'package:proxypin/ui/component/text_field.dart';
import 'package:proxypin/utils/platform.dart';
@@ -152,7 +153,7 @@ class _QrDecodeState extends State<_QrDecode> with AutomaticKeepAliveClientMixin
String? path = await selectImage();
if (path == null) return;
var result = await FlutterQrReader.imgScan(path);
if (result.isEmpty) {
if (result == null) {
if (context.mounted) FlutterToastr.show(localizations.decodeFail, context, duration: 2);
return;
}
@@ -349,17 +350,22 @@ class _QrEncodeState extends State<_QrEncode> with AutomaticKeepAliveClientMixin
return;
}
if (Platforms.isDesktop()) {
String? path = (await FilePicker.platform.saveFile(fileName: "qrcode.png"));
if (path == null) return;
String? path;
if (Platform.isMacOS) {
path = await DesktopMultiWindow.invokeMethod(0, "saveFile", {"fileName": "qrcode.png"});
WindowController.fromWindowId(widget.windowId!).show();
} else {
path = (await FilePicker.platform.saveFile(fileName: "qrcode.png", initialDirectory: "~/Downloads"));
}
var imageBytes = await toImageBytes();
if (imageBytes == null) return;
if (path == null) return;
await File(path).writeAsBytes(imageBytes);
if (mounted) {
FlutterToastr.show(localizations.saveSuccess, context, duration: 2);
}
var imageBytes = await toImageBytes();
if (imageBytes == null) return;
await File(path).writeAsBytes(imageBytes);
if (mounted) {
CustomToast.success(localizations.saveSuccess).show(context);
}
}

View File

@@ -14,7 +14,6 @@
* limitations under the License.
*/
import 'dart:convert';
import 'dart:io';
@@ -25,7 +24,7 @@ import 'package:path_provider/path_provider.dart';
/// @author wanghongen
/// 2024/1/1
class ThemeModel {
class ColorMapping {
static final Map<String, Color> colors = {
"Blue": Colors.blue,
"Pink": Colors.pink,
@@ -39,9 +38,19 @@ class ThemeModel {
"Grey": Colors.grey,
};
static Color getColor(String colorName) {
return colors[colorName] ?? Colors.blue;
}
static String getColorName(Color color) {
return colors.entries.firstWhere((entry) => entry.value == color).key;
}
}
class ThemeModel {
ThemeMode mode;
bool useMaterial3;
String color = "Blue";
String color = "Pink";
ThemeModel({this.mode = ThemeMode.system, this.useMaterial3 = true});
@@ -50,17 +59,19 @@ class ThemeModel {
useMaterial3: useMaterial3 ?? this.useMaterial3,
);
Color get themeColor => colors[color] ?? Colors.blue;
Color get themeColor => ColorMapping.colors[color] ?? Colors.blue;
}
class AppConfiguration {
static const String version = "1.1.8";
ValueNotifier<bool> globalChange = ValueNotifier(false);
ThemeModel _theme = ThemeModel();
Locale? _language;
//是否显示更新内容公告
bool upgradeNoticeV17 = true;
bool upgradeNoticeV18 = true;
/// 是否启用画中画
ValueNotifier<bool> pipEnabled = ValueNotifier(Platform.isAndroid);
@@ -93,9 +104,14 @@ class AppConfiguration {
static Future<AppConfiguration> get instance async {
if (_instance == null) {
AppConfiguration configuration = AppConfiguration._();
await configuration.initConfig();
_instance = configuration;
try {
AppConfiguration configuration = AppConfiguration._();
await configuration.initConfig();
_instance = configuration;
} catch (e) {
logger.e("load config error: $e");
_instance = AppConfiguration._();
}
}
return _instance!;
}
@@ -124,7 +140,7 @@ class AppConfiguration {
Color get themeColor => _theme.themeColor;
set setThemeColor(String colorName) {
var color = ThemeModel.colors[colorName];
var color = ColorMapping.colors[colorName];
if (color == null || color == themeColor) return;
_theme.color = colorName;
@@ -145,7 +161,7 @@ class AppConfiguration {
Future<File> get _path async {
if (Platforms.isDesktop()) {
var userHome = Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'];
return File('$userHome/.proxypin/ui_config.json');
return File('$userHome${Platform.pathSeparator}.proxypin${Platform.pathSeparator}ui_config.json');
}
final directory = await getApplicationSupportDirectory();
@@ -177,7 +193,7 @@ class AppConfiguration {
_theme = ThemeModel(mode: mode, useMaterial3: config['useMaterial3'] ?? true);
_theme.color = config['themeColor'] ?? "Blue";
upgradeNoticeV17 = config['upgradeNoticeV17'] ?? true;
upgradeNoticeV18 = config['upgradeNoticeV18'] ?? true;
_language = config['language'] == null ? null : Locale.fromSubtags(languageCode: config['language']);
pipEnabled.value = config['pipEnabled'] ?? true;
pipIcon.value = config['pipIcon'] ?? false;
@@ -222,16 +238,13 @@ class AppConfiguration {
'mode': _theme.mode.name,
'themeColor': _theme.color,
'useMaterial3': _theme.useMaterial3,
'upgradeNoticeV17': upgradeNoticeV17,
'upgradeNoticeV18': upgradeNoticeV18,
"language": _language?.languageCode,
"headerExpanded": headerExpanded,
if (memoryCleanupThreshold != null) 'memoryCleanupThreshold': memoryCleanupThreshold,
if (Platforms.isMobile()) 'pipEnabled': pipEnabled.value,
if (Platforms.isMobile()) 'pipIcon': pipIcon.value ? true : null,
if (Platforms.isMobile()) 'bottomNavigation': bottomNavigation,
if (Platforms.isDesktop())
"windowSize": windowSize == null ? null : {"width": windowSize?.width, "height": windowSize?.height},
if (Platforms.isDesktop())

View File

@@ -17,9 +17,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:proxypin/network/bin/configuration.dart';
import 'package:proxypin/network/bin/listener.dart';
import 'package:proxypin/network/bin/server.dart';
import 'package:proxypin/network/channel.dart';
import 'package:proxypin/network/handler.dart';
import 'package:proxypin/network/channel/channel.dart';
import 'package:proxypin/network/channel/channel_context.dart';
import 'package:proxypin/network/http/http.dart';
import 'package:proxypin/network/http/websocket.dart';
import 'package:proxypin/ui/component/memory_cleanup.dart';
@@ -34,6 +35,7 @@ import 'package:proxypin/ui/desktop/request/list.dart';
import 'package:proxypin/ui/desktop/toolbar/toolbar.dart';
import 'package:proxypin/utils/listenable_list.dart';
import '../app_update/app_update_repository.dart';
import '../component/split_view.dart';
/// @author wanghongen
@@ -88,10 +90,12 @@ class _DesktopHomePagePageState extends State<DesktopHomePage> implements EventL
proxyServer.addListener(this);
panel = NetworkTabController(tabStyle: const TextStyle(fontSize: 16), proxyServer: proxyServer);
if (widget.appConfiguration.upgradeNoticeV17) {
if (widget.appConfiguration.upgradeNoticeV18) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showUpgradeNotice();
});
} else {
AppUpdateRepository.checkUpdate(context);
}
}
@@ -142,39 +146,39 @@ class _DesktopHomePagePageState extends State<DesktopHomePage> implements EventL
actions: [
TextButton(
onPressed: () {
widget.appConfiguration.upgradeNoticeV17 = false;
widget.appConfiguration.upgradeNoticeV18 = false;
widget.appConfiguration.flushConfig();
Navigator.pop(context);
},
child: Text(localizations.cancel))
],
title: Text(isCN ? '更新内容V1.1.7' : "Update content V1.1.7", style: const TextStyle(fontSize: 18)),
title: Text(isCN ? '更新内容V${AppConfiguration.version}' : "Update content V${AppConfiguration.version}",
style: const TextStyle(fontSize: 18)),
content: Container(
constraints: const BoxConstraints(maxWidth: 600),
child: SelectableText(
isCN
? '提示默认不会开启HTTPS抓包请安装证书后再开启HTTPS抓包。\n'
'点击HTTPS抓包(加锁图标),选择安装根证书,按照提示操作即可。\n\n'
'1. 新增socks5代理支持, 可在设置中关闭\n'
'2. 请求列表增加按时间排序\n'
'3. 响应新增图片保存\n'
'4. 请求重写新增json格式化\n'
'5. 修复安卓首次在画中画开启VPN闪退\n'
'6. 修复Illegal IPv6 address问题\n'
'7. 修复Windows历史导入安卓har历史文件崩溃问题\n'
'8. 修复复制python请求头不全问题;\n'
'9. 修复二维码保存的背景颜色问题;\n'
'1. 新增app检查更新\n'
'2. 关键词高亮支持持久化\n'
'3. 修复请求域名和tls域名不一致问题\n'
'4. 修复IPV6建立链接失败问题\n'
'5. Windows单例窗口内置VCLibs\n'
'6. 脚本支持获取应用目录, 脚本修复字节响应请求异常问题, 脚本支持执行多个\n'
'7. 工具箱js fetch支持代理\n'
'8. 修复部分curl导入失败问题;\n'
: 'TipsBy default, HTTPS packet capture will not be enabled. Please install the certificate before enabling HTTPS packet capture。\n'
'Click HTTPS Capture packets(Lock icon)Choose to install the root certificate and follow the prompts to proceed。\n\n'
'1. Added support for socks5 proxy, which can be turned off in settings\n'
'2. Add request list sorted by time\n'
'3. Response to saving newly added images\n'
'4. Request rewriting to add json format\n'
'5. Fixed the issue when opening VPN in Picture-in-Picture mode on Android for the first time\n'
'6. Fix Illegal IPv6 address issue\n'
'7. Fix Windows history import Android har history file crash issue\n'
'8. Fix the problem of incomplete copy of python request header\n'
'9. Fixed the background color issue when saving QR code\n'
'1. Added app check update\n'
'2. Keyword highlighting supports persistence\n'
'3. Fixed TLS SNI inconsistency\n'
'4. Fixed the issue of IPV6 link establishment failure\n'
'5. Windows singleton window with built-in VCLibs\n'
'6. Fixed Illegal IPv6 address issue\n'
'7. The script supports obtaining application directories, fixes byte response request exception issues, and supports executing multiple instances\n'
'8. Toolbox js fetch supports proxy\n'
'9. Fixed some curl import failure issues\n'
'',
style: const TextStyle(fontSize: 14))));
});

View File

@@ -25,10 +25,11 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_toastr/flutter_toastr.dart';
import 'package:proxypin/network/host_port.dart';
import 'package:proxypin/network/channel/host_port.dart';
import 'package:proxypin/network/http/http.dart';
import 'package:proxypin/network/http_client.dart';
import 'package:proxypin/network/http/http_client.dart';
import 'package:proxypin/storage/favorites.dart';
import 'package:proxypin/ui/component/app_dialog.dart';
import 'package:proxypin/ui/component/utils.dart';
import 'package:proxypin/ui/component/widgets.dart';
import 'package:proxypin/ui/content/panel.dart';
@@ -86,7 +87,7 @@ class _FavoritesState extends State<Favorites> {
panel: widget.panel,
onRemove: (Favorite favorite) {
FavoriteStorage.removeFavorite(favorite);
FlutterToastr.show(localizations.deleteFavoriteSuccess, context);
CustomToast.success(localizations.deleteFavoriteSuccess).show(context);
setState(() {});
},
);
@@ -208,7 +209,7 @@ class _FavoriteItemState extends State<_FavoriteItem> {
HttpClients.proxyRequest(httpRequest, proxyInfo: proxyInfo);
if (mounted) {
FlutterToastr.show(localizations.reSendRequest, context);
CustomToast.success(localizations.reSendRequest).show(context);
}
}

View File

@@ -23,9 +23,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_toastr/flutter_toastr.dart';
import 'package:proxypin/network/bin/server.dart';
import 'package:proxypin/network/host_port.dart';
import 'package:proxypin/network/channel/host_port.dart';
import 'package:proxypin/network/http/http.dart';
import 'package:proxypin/network/http_client.dart';
import 'package:proxypin/network/http/http_client.dart';
import 'package:proxypin/network/util/logger.dart';
import 'package:proxypin/storage/histories.dart';
import 'package:proxypin/ui/component/history_cache_time.dart';

View File

@@ -155,7 +155,7 @@ class _PreferenceState extends State<Preference> {
///主题颜色
Widget themeColor(BuildContext context) {
return Wrap(
children: ThemeModel.colors.entries.map((pair) {
children: ColorMapping.colors.entries.map((pair) {
var dividerColor = Theme.of(context).focusColor;
var background = appConfiguration.themeColor == pair.value ? dividerColor : Colors.transparent;

View File

@@ -24,17 +24,18 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_toastr/flutter_toastr.dart';
import 'package:proxypin/network/bin/configuration.dart';
import 'package:proxypin/network/bin/server.dart';
import 'package:proxypin/network/channel.dart';
import 'package:proxypin/network/channel/channel.dart';
import 'package:proxypin/network/channel/channel_context.dart';
import 'package:proxypin/network/components/host_filter.dart';
import 'package:proxypin/network/host_port.dart';
import 'package:proxypin/network/channel/host_port.dart';
import 'package:proxypin/network/http/http.dart';
import 'package:proxypin/network/http_client.dart';
import 'package:proxypin/network/http/http_client.dart';
import 'package:proxypin/ui/component/transition.dart';
import 'package:proxypin/ui/component/utils.dart';
import 'package:proxypin/ui/content/panel.dart';
import 'package:proxypin/ui/desktop/request/model/search_model.dart';
import 'package:proxypin/ui/desktop/request/request.dart';
import 'package:proxypin/ui/desktop/widgets/highlight.dart';
import 'package:proxypin/utils/keyword_highlight.dart';
import 'package:proxypin/utils/listenable_list.dart';
/// 左侧域名
@@ -102,12 +103,12 @@ class DomainWidgetState extends State<DomainList> with AutomaticKeepAliveClientM
highlightHandler();
});
};
DesktopKeywordHighlight.keywordsController.addListener(highlightListener);
KeywordHighlights.addListener(highlightListener);
}
@override
dispose() {
DesktopKeywordHighlight.keywordsController.removeListener(highlightListener);
KeywordHighlights.removeListener(highlightListener);
super.dispose();
}
@@ -412,17 +413,18 @@ class _DomainRequestsState extends State<DomainRequests> {
if (!changing) {
changing = true;
Future.delayed(const Duration(milliseconds: 500), () {
setState(() {
changing = false;
});
transitionState.currentState?.show();
if (mounted) {
setState(() {
changing = false;
});
transitionState.currentState?.show();
}
});
}
}
@override
Widget build(BuildContext context) {
return Column(children: [
_hostWidget(widget.domain),
Offstage(offstage: !selected, child: Column(children: widget.body.toList()))

View File

@@ -20,10 +20,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_toastr/flutter_toastr.dart';
import 'package:proxypin/network/bin/server.dart';
import 'package:proxypin/network/channel.dart';
import 'package:proxypin/network/host_port.dart';
import 'package:proxypin/network/channel/channel.dart';
import 'package:proxypin/network/channel/channel_context.dart';
import 'package:proxypin/network/channel/host_port.dart';
import 'package:proxypin/network/http/http.dart';
import 'package:proxypin/network/http_client.dart';
import 'package:proxypin/network/http/http_client.dart';
import 'package:proxypin/ui/component/widgets.dart';
import 'package:proxypin/ui/content/panel.dart';
import 'package:proxypin/ui/desktop/request/model/search_model.dart';

View File

@@ -25,10 +25,11 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_toastr/flutter_toastr.dart';
import 'package:proxypin/network/bin/server.dart';
import 'package:proxypin/network/components/manager/script_manager.dart';
import 'package:proxypin/network/host_port.dart';
import 'package:proxypin/network/channel/host_port.dart';
import 'package:proxypin/network/http/http.dart';
import 'package:proxypin/network/http_client.dart';
import 'package:proxypin/network/http/http_client.dart';
import 'package:proxypin/storage/favorites.dart';
import 'package:proxypin/ui/component/app_dialog.dart';
import 'package:proxypin/ui/component/utils.dart';
import 'package:proxypin/ui/component/widgets.dart';
import 'package:proxypin/ui/content/panel.dart';
@@ -36,6 +37,7 @@ import 'package:proxypin/ui/desktop/request/repeat.dart';
import 'package:proxypin/ui/desktop/toolbar/setting/script.dart';
import 'package:proxypin/ui/desktop/widgets/highlight.dart';
import 'package:proxypin/utils/curl.dart';
import 'package:proxypin/utils/keyword_highlight.dart';
import 'package:proxypin/utils/lang.dart';
import 'package:proxypin/utils/python.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -131,7 +133,7 @@ class _RequestWidgetState extends State<RequestWidget> {
return highlightColor;
}
return DesktopKeywordHighlight.getHighlightColor(path);
return KeywordHighlights.getHighlightColor(path);
}
void changeState() {
@@ -295,7 +297,7 @@ class _RequestWidgetState extends State<RequestWidget> {
var proxyInfo = widget.proxyServer.isRunning ? ProxyInfo.of("127.0.0.1", widget.proxyServer.port) : null;
HttpClients.proxyRequest(request, proxyInfo: proxyInfo);
FlutterToastr.show(localizations.reSendRequest, rootNavigator: true, context);
CustomToast.success(localizations.reSendRequest).show(context);
}
PopupMenuItem popupItem(String text, {VoidCallback? onTap}) {

View File

@@ -22,13 +22,14 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_toastr/flutter_toastr.dart';
import 'package:proxypin/network/host_port.dart';
import 'package:proxypin/network/channel/host_port.dart';
import 'package:proxypin/network/http/http.dart';
import 'package:proxypin/network/http/http_headers.dart';
import 'package:proxypin/network/http_client.dart';
import 'package:proxypin/network/http/http_client.dart';
import 'package:proxypin/network/util/logger.dart';
import 'package:proxypin/ui/component/split_view.dart';
import 'package:proxypin/ui/component/state_component.dart';
import 'package:proxypin/ui/configuration.dart';
import 'package:proxypin/ui/content/body.dart';
import 'package:proxypin/utils/curl.dart';
import 'package:proxypin/utils/lang.dart';
@@ -204,7 +205,7 @@ class RequestEditorState extends State<RequestEditor> {
onPressed: () {
try {
setState(() {
request = parseCurl(text!);
request = Curl.parse(text!);
requestKey.currentState?.change(request!);
requestLineKey.currentState?.change(request?.requestUrl, request?.method.name);
});
@@ -276,7 +277,7 @@ class _HttpState extends State<_HttpWidget> {
message = widget.message;
body = TextEditingController(text: widget.message?.bodyAsString);
if (widget.message?.headers == null && !widget.readOnly) {
initHeader["User-Agent"] = ["ProxyPin/1.1.7"];
initHeader["User-Agent"] = ["ProxyPin/${AppConfiguration.version}"];
initHeader["Accept"] = ["*/*"];
return;
}

View File

@@ -23,7 +23,7 @@ import 'package:proxypin/network/http/http.dart';
import 'package:proxypin/ui/component/utils.dart';
import 'package:proxypin/ui/desktop/request/model/search_model.dart';
import 'package:proxypin/ui/desktop/request/request.dart';
import 'package:proxypin/ui/desktop/widgets/highlight.dart';
import 'package:proxypin/utils/keyword_highlight.dart';
import 'package:proxypin/utils/listenable_list.dart';
///请求序列 列表
@@ -70,7 +70,7 @@ class RequestSequenceState extends State<RequestSequence> with AutomaticKeepAliv
highlightHandler();
});
};
DesktopKeywordHighlight.keywordsController.addListener(highlightListener);
KeywordHighlights.addListener(highlightListener);
}
changeState() {
@@ -90,7 +90,7 @@ class RequestSequenceState extends State<RequestSequence> with AutomaticKeepAliv
@override
void dispose() {
DesktopKeywordHighlight.keywordsController.removeListener(highlightListener);
KeywordHighlights.removeListener(highlightListener);
super.dispose();
}

View File

@@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:proxypin/ui/app_update/app_update_repository.dart';
import 'package:proxypin/ui/configuration.dart';
import 'package:url_launcher/url_launcher.dart';
class DesktopAbout extends StatefulWidget {
const DesktopAbout({super.key});
@override
State<StatefulWidget> createState() {
return _AppUpdateStateChecking();
}
}
class _AppUpdateStateChecking extends State<DesktopAbout> {
bool checkUpdating = false;
AppLocalizations get localizations => AppLocalizations.of(context)!;
@override
Widget build(BuildContext context) {
bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');
String gitHub = "https://github.com/wanghongenpin/proxypin";
return AlertDialog(
titlePadding: const EdgeInsets.only(left: 20, top: 10, right: 15),
title: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
const Expanded(child: SizedBox()),
Text(localizations.about, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500)),
const Expanded(child: SizedBox()),
Align(alignment: Alignment.topRight, child: CloseButton())
]),
content: SizedBox(
width: 360,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text("ProxyPin", style: TextStyle(fontSize: 20)),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.only(left: 10, right: 10),
child:
Text(isCN ? "全平台开源免费抓包软件" : "Full platform open source free capture HTTP(S) traffic software")),
const SizedBox(height: 10),
Text("v${AppConfiguration.version}"),
const SizedBox(height: 10),
ListTile(
title: Text('GitHub'),
trailing: const Icon(Icons.open_in_new, size: 22),
onTap: () => launchUrl(Uri.parse(gitHub))),
ListTile(
title: Text(localizations.feedback),
trailing: const Icon(Icons.open_in_new, size: 22),
onTap: () => launchUrl(Uri.parse("$gitHub/issues"))),
ListTile(
title: Text(localizations.appUpdateCheckVersion),
trailing: checkUpdating
? const SizedBox(width: 22, height: 22, child: CircularProgressIndicator())
: const Icon(Icons.sync, size: 22),
onTap: () async {
if (checkUpdating) {
return;
}
setState(() {
checkUpdating = true;
});
await AppUpdateRepository.checkUpdate(context, canIgnore: false, showToast: true);
setState(() {
checkUpdating = false;
});
}),
ListTile(
title: Text(isCN ? "下载地址" : "Download"),
trailing: const Icon(Icons.open_in_new, size: 22),
onTap: () => launchUrl(
Uri.parse(isCN ? "https://gitee.com/wanghongenpin/proxypin/releases" : "$gitHub/releases")))
],
)),
);
}
}

View File

@@ -19,7 +19,7 @@ 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/host_port.dart';
import 'package:proxypin/network/channel/host_port.dart';
import 'package:proxypin/ui/component/widgets.dart';
/// @author wanghongen

View File

@@ -161,8 +161,9 @@ class _DomainFilterState extends State<DomainFilter> {
//导入
import() async {
final FilePickerResult? result =
await FilePicker.platform.pickFiles(allowedExtensions: ['config'], type: FileType.custom);
await FilePicker.platform.pickFiles(allowedExtensions: ['config'], type: FileType.custom, initialDirectory: "/Downloads");
var file = result?.files.single;
if (file == null) {
return;

View File

@@ -340,8 +340,8 @@ class _HostsDialogState extends State<HostsDialog> {
//导入
import() async {
final FilePickerResult? result =
await FilePicker.platform.pickFiles(allowedExtensions: ['json'], type: FileType.custom);
final FilePickerResult? result = await FilePicker.platform
.pickFiles(allowedExtensions: ['json'], type: FileType.custom, initialDirectory: "/Downloads");
var file = result?.files.single;
if (file == null) {
return;

View File

@@ -153,16 +153,24 @@ class RequestRewriteState extends State<RequestRewriteWidget> {
//导入js
import() async {
FilePickerResult? result =
await FilePicker.platform.pickFiles(type: FileType.custom, allowedExtensions: ['config', 'json']);
if (result == null || result.files.isEmpty) {
String? path;
if (Platform.isMacOS) {
path = await DesktopMultiWindow.invokeMethod(0, "pickFiles", {
"allowedExtensions": ['config', 'json']
});
WindowController.fromWindowId(widget.windowId).show();
} else {
FilePickerResult? result =
await FilePicker.platform.pickFiles(type: FileType.custom, allowedExtensions: ['config', 'json']);
path = result?.files.single.path;
}
if (path == null) {
return;
}
var file = result.files.single;
try {
List json = jsonDecode(await File(file.path!).readAsString());
List json = jsonDecode(await File(path).readAsString());
for (var item in json) {
var rule = RequestRewriteRule.formJson(item);
var items = (item['items'] as List).map((e) => RewriteItem.fromJson(e)).toList();
@@ -176,7 +184,7 @@ class RequestRewriteState extends State<RequestRewriteWidget> {
}
setState(() {});
} catch (e, t) {
logger.e('导入失败 $file', error: e, stackTrace: t);
logger.e('导入失败 $path', error: e, stackTrace: t);
if (mounted) {
FlutterToastr.show("${localizations.importFailed} $e", context);
}
@@ -362,7 +370,15 @@ class _RequestRuleListState extends State<RequestRuleList> {
if (indexes.isEmpty) return;
String fileName = 'proxypin-rewrites.config';
var path = await FilePicker.platform.saveFile(fileName: fileName);
String? path;
if (Platform.isMacOS) {
path = await DesktopMultiWindow.invokeMethod(0, "saveFile", {"fileName": fileName});
WindowController.fromWindowId(widget.windowId).show();
} else {
path = await FilePicker.platform.saveFile(fileName: fileName);
}
if (path == null) {
return;
}
@@ -644,7 +660,7 @@ class _RewriteRuleEditState extends State<RewriteRuleEdit> {
return DesktopRewriteUpdate(key: rewriteUpdateKey, items: items, ruleType: ruleType, request: widget.request);
}
return DesktopRewriteReplace(key: rewriteReplaceKey, items: items, ruleType: ruleType);
return DesktopRewriteReplace(key: rewriteReplaceKey, items: items, ruleType: ruleType, windowId: widget.windowId);
}
Widget textField(String label, TextEditingController controller, String hint,

View File

@@ -14,6 +14,9 @@
* limitations under the License.
*/
import 'dart:io';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -27,10 +30,11 @@ import 'package:proxypin/utils/lang.dart';
/// @author wanghongen
/// 2023/10/8
class DesktopRewriteReplace extends StatefulWidget {
final int? windowId;
final RuleType ruleType;
final List<RewriteItem>? items;
const DesktopRewriteReplace({super.key, this.items, required this.ruleType});
const DesktopRewriteReplace({super.key, this.items, required this.ruleType, this.windowId});
@override
State<DesktopRewriteReplace> createState() => RewriteReplaceState();
@@ -238,11 +242,15 @@ class RewriteReplaceState extends State<DesktopRewriteReplace> {
const SizedBox(width: 10),
FilledButton(
onPressed: () async {
FilePickerResult? result = await FilePicker.platform.pickFiles();
if (result == null || result.files.isEmpty) {
return;
String? path;
if (Platform.isMacOS) {
path = await DesktopMultiWindow.invokeMethod(0, "pickFiles");
if (widget.windowId != null) WindowController.fromWindowId(widget.windowId!).show();
} else {
FilePickerResult? result = await FilePicker.platform.pickFiles();
path = result?.files.single.path;
}
var path = result.files.first.path;
if (path == null) {
return;
}

View File

@@ -156,13 +156,23 @@ class _ScriptWidgetState extends State<ScriptWidget> {
//导入js
import() async {
FilePickerResult? result = await FilePicker.platform.pickFiles(type: FileType.custom, allowedExtensions: ['json']);
if (result == null || result.files.isEmpty) {
String? path;
if (Platform.isMacOS) {
path = await DesktopMultiWindow.invokeMethod(0, "pickFiles", {
"allowedExtensions": ['json']
});
WindowController.fromWindowId(widget.windowId).show();
} else {
FilePickerResult? result =
await FilePicker.platform.pickFiles(type: FileType.custom, allowedExtensions: ['json']);
path = result?.files.single.path;
}
if (path == null) {
return;
}
var file = result.files.single.path;
try {
var json = jsonDecode(await File(file!).readAsString());
var json = jsonDecode(await File(path).readAsString());
var scriptManager = (await ScriptManager.instance);
if (json is List<dynamic>) {
for (var item in json) {
@@ -180,7 +190,7 @@ class _ScriptWidgetState extends State<ScriptWidget> {
}
setState(() {});
} catch (e, t) {
logger.e('导入失败 $file', error: e, stackTrace: t);
logger.e('导入失败 $path', error: e, stackTrace: t);
if (mounted) {
FlutterToastr.show("${localizations.importFailed} $e", context);
}
@@ -626,7 +636,13 @@ class _ScriptListState extends State<ScriptList> {
if (indexes.isEmpty) return;
//文件名称
String fileName = 'proxypin-scripts.json';
String? path = await FilePicker.platform.saveFile(fileName: fileName);
String? path;
if (Platform.isMacOS) {
path = await DesktopMultiWindow.invokeMethod(0, "saveFile", {"fileName": fileName});
WindowController.fromWindowId(widget.windowId).show();
} else {
path = await FilePicker.platform.saveFile(fileName: fileName);
}
if (path == null) {
return;
}

View File

@@ -24,10 +24,10 @@ 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/about.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';
import 'package:url_launcher/url_launcher.dart';
import 'filter.dart';
@@ -56,7 +56,6 @@ class _SettingState extends State<Setting> {
@override
Widget build(BuildContext context) {
return MenuAnchor(
builder: (context, controller, child) {
return IconButton(
@@ -94,49 +93,7 @@ class _SettingState extends State<Setting> {
}
showAbout() {
bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');
String gitHub = "https://github.com/wanghongenpin/proxypin";
showDialog(
context: context,
builder: (context) {
return AlertDialog(
titlePadding: const EdgeInsets.only(left: 20, top: 10, right: 15),
title: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
const Expanded(child: SizedBox()),
Text(localizations.about, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500)),
const Expanded(child: SizedBox()),
Align(alignment: Alignment.topRight, child: CloseButton())
]),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text("ProxyPin", style: TextStyle(fontSize: 20)),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.only(left: 10, right: 10),
child:
Text(isCN ? "全平台开源免费抓包软件" : "Full platform open source free capture HTTP(S) traffic software")),
const SizedBox(height: 10),
const Text("V1.1.7"),
const SizedBox(height: 10),
ListTile(
title: Text('GitHub', textAlign: TextAlign.center, style: TextStyle(color: Colors.blue)),
onTap: () => launchUrl(Uri.parse(gitHub))),
ListTile(
title:
Text(localizations.feedback, textAlign: TextAlign.center, style: TextStyle(color: Colors.blue)),
onTap: () => launchUrl(Uri.parse("$gitHub/issues"))),
ListTile(
title: Text(isCN ? "下载地址" : "Download",
textAlign: TextAlign.center, style: TextStyle(color: Colors.blue)),
onTap: () => launchUrl(
Uri.parse(isCN ? "https://gitee.com/wanghongenpin/proxypin/releases" : "$gitHub/releases")))
],
),
);
});
showDialog(context: context, builder: (context) => DesktopAbout());
}
///设置外部代理地址

View File

@@ -1,22 +1,11 @@
import 'package:flutter/material.dart';
import 'package:proxypin/ui/component/state_component.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:proxypin/utils/keyword_highlight.dart';
///配置关键词高亮
///@Author: WangHongEn
class DesktopKeywordHighlight extends StatefulWidget {
static Map<Color, String> keywords = {};
static ValueNotifier keywordsController = ValueNotifier<Map>(keywords);
static Color? getHighlightColor(String key) {
for (var entry in keywords.entries) {
if (key.contains(entry.value)) {
return entry.key;
}
}
return null;
}
const DesktopKeywordHighlight({super.key});
@override
@@ -35,7 +24,7 @@ class _KeywordHighlightState extends State<DesktopKeywordHighlight> {
Colors.grey: localizations.gray,
};
var map = Map.of(DesktopKeywordHighlight.keywords);
Map<Color, String> map = Map.of(KeywordHighlights.keywords);
return AlertDialog(
title: ListTile(
@@ -52,7 +41,7 @@ class _KeywordHighlightState extends State<DesktopKeywordHighlight> {
TextButton(
child: Text(localizations.done),
onPressed: () {
DesktopKeywordHighlight.keywords = map;
KeywordHighlights.saveKeywords(map);
Navigator.of(context).pop();
},
),
@@ -97,10 +86,4 @@ class _KeywordHighlightState extends State<DesktopKeywordHighlight> {
border: const OutlineInputBorder(),
);
}
@override
void dispose() {
DesktopKeywordHighlight.keywordsController.value = Map.from(DesktopKeywordHighlight.keywords);
super.dispose();
}
}

View File

@@ -25,12 +25,13 @@ import 'package:proxypin/native/app_lifecycle.dart';
import 'package:proxypin/native/pip.dart';
import 'package:proxypin/native/vpn.dart';
import 'package:proxypin/network/bin/configuration.dart';
import 'package:proxypin/network/bin/listener.dart';
import 'package:proxypin/network/bin/server.dart';
import 'package:proxypin/network/channel.dart';
import 'package:proxypin/network/handler.dart';
import 'package:proxypin/network/channel/channel.dart';
import 'package:proxypin/network/channel/channel_context.dart';
import 'package:proxypin/network/http/http.dart';
import 'package:proxypin/network/http/websocket.dart';
import 'package:proxypin/network/http_client.dart';
import 'package:proxypin/network/http/http_client.dart';
import 'package:proxypin/ui/component/memory_cleanup.dart';
import 'package:proxypin/ui/component/toolbox/toolbox.dart';
import 'package:proxypin/ui/configuration.dart';
@@ -48,6 +49,8 @@ import 'package:proxypin/utils/lang.dart';
import 'package:proxypin/utils/listenable_list.dart';
import 'package:proxypin/utils/navigator.dart';
import '../app_update/app_update_repository.dart';
///移动端首页
///@author wanghongen
class MobileHomePage extends StatefulWidget {
@@ -114,10 +117,12 @@ class MobileHomeState extends State<MobileHomePage> implements EventListener, Li
proxyServer.addListener(this);
proxyServer.start();
if (widget.appConfiguration.upgradeNoticeV17) {
if (widget.appConfiguration.upgradeNoticeV18) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showUpgradeNotice();
});
} else if (Platform.isAndroid) {
AppUpdateRepository.checkUpdate(context);
}
}
@@ -271,18 +276,19 @@ class MobileHomeState extends State<MobileHomePage> implements EventListener, Li
'9. 修复二维码保存的背景颜色问题;\n'
: 'TipsBy default, HTTPS packet capture will not be enabled. Please install the certificate before enabling HTTPS packet capture。\n\n'
'Click HTTPS Capture packets(Lock icon)Choose to install the root certificate and follow the prompts to proceed。\n\n'
'1. Added support for socks5 proxy, which can be turned off in settings\n'
'2. Add request list sorted by time\n'
'3. Response to saving newly added images\n'
'4. Request rewriting to add json format\n'
'5. Fixed the issue when opening VPN in Picture-in-Picture mode on Android for the first time\n'
'6. Fix Illegal IPv6 address issue\n'
'7. Fix Windows history import Android har history file crash issue\n'
'8. Fix the problem of incomplete copy of python request header\n'
'9. Fixed the background color issue when saving QR code\n'
'1. Added app check update\n'
'2. Keyword highlighting supports persistence\n'
'3. Fixed TLS SNI inconsistency\n'
'4. Fixed the issue of IPV6 link establishment failure\n'
'5. Windows singleton window with built-in VCLibs\n'
'6. Fixed Illegal IPv6 address issue\n'
'7. The script supports obtaining application directories, fixes byte response request exception issues, and supports executing multiple instances\n'
'8. Toolbox js fetch supports proxy\n'
'9. Fixed some curl import failure issues\n'
'';
showAlertDialog(isCN ? '更新内容V1.1.7' : "Update content V1.1.7", content, () {
widget.appConfiguration.upgradeNoticeV17 = false;
showAlertDialog(isCN ? '更新内容V${AppConfiguration.version}' : "Update content V${AppConfiguration.version}", content,
() {
widget.appConfiguration.upgradeNoticeV18 = false;
widget.appConfiguration.flushConfig();
});
}

View File

@@ -24,9 +24,9 @@ import 'package:flutter_toastr/flutter_toastr.dart';
import 'package:proxypin/network/bin/configuration.dart';
import 'package:proxypin/network/bin/server.dart';
import 'package:proxypin/network/components/host_filter.dart';
import 'package:proxypin/network/host_port.dart';
import 'package:proxypin/network/channel/host_port.dart';
import 'package:proxypin/network/http/http.dart';
import 'package:proxypin/network/http_client.dart';
import 'package:proxypin/network/http/http_client.dart';
import 'package:proxypin/ui/component/widgets.dart';
import 'package:proxypin/ui/mobile/request/request_sequence.dart';
import 'package:proxypin/utils/listenable_list.dart';

View File

@@ -26,9 +26,9 @@ import 'package:proxypin/network/bin/server.dart';
import 'package:proxypin/network/components/manager/request_rewrite_manager.dart';
import 'package:proxypin/network/components/manager/rewrite_rule.dart';
import 'package:proxypin/network/components/manager/script_manager.dart';
import 'package:proxypin/network/host_port.dart';
import 'package:proxypin/network/channel/host_port.dart';
import 'package:proxypin/network/http/http.dart';
import 'package:proxypin/network/http_client.dart';
import 'package:proxypin/network/http/http_client.dart';
import 'package:proxypin/storage/favorites.dart';
import 'package:proxypin/ui/component/utils.dart';
import 'package:proxypin/ui/component/widgets.dart';

View File

@@ -25,9 +25,9 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_toastr/flutter_toastr.dart';
import 'package:proxypin/network/bin/configuration.dart';
import 'package:proxypin/network/bin/server.dart';
import 'package:proxypin/network/host_port.dart';
import 'package:proxypin/network/channel/host_port.dart';
import 'package:proxypin/network/http/http.dart';
import 'package:proxypin/network/http_client.dart';
import 'package:proxypin/network/http/http_client.dart';
import 'package:proxypin/network/util/logger.dart';
import 'package:proxypin/storage/histories.dart';
import 'package:proxypin/ui/component/history_cache_time.dart';

View File

@@ -19,7 +19,8 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:proxypin/network/bin/server.dart';
import 'package:proxypin/network/channel.dart';
import 'package:proxypin/network/channel/channel.dart';
import 'package:proxypin/network/channel/channel_context.dart';
import 'package:proxypin/network/http/http.dart';
import 'package:proxypin/ui/desktop/request/model/search_model.dart';
import 'package:proxypin/ui/mobile/request/domians.dart';

View File

@@ -24,9 +24,9 @@ import 'package:proxypin/network/bin/server.dart';
import 'package:proxypin/network/components/manager/request_rewrite_manager.dart';
import 'package:proxypin/network/components/manager/rewrite_rule.dart';
import 'package:proxypin/network/components/manager/script_manager.dart';
import 'package:proxypin/network/host_port.dart';
import 'package:proxypin/network/channel/host_port.dart';
import 'package:proxypin/network/http/http.dart';
import 'package:proxypin/network/http_client.dart';
import 'package:proxypin/network/http/http_client.dart';
import 'package:proxypin/network/util/cache.dart';
import 'package:proxypin/storage/favorites.dart';
import 'package:proxypin/ui/component/utils.dart';
@@ -36,8 +36,8 @@ import 'package:proxypin/ui/mobile/request/repeat.dart';
import 'package:proxypin/ui/mobile/request/request_editor.dart';
import 'package:proxypin/ui/mobile/setting/request_rewrite.dart';
import 'package:proxypin/ui/mobile/setting/script.dart';
import 'package:proxypin/ui/mobile/widgets/highlight.dart';
import 'package:proxypin/utils/curl.dart';
import 'package:proxypin/utils/keyword_highlight.dart';
import 'package:proxypin/utils/lang.dart';
import 'package:proxypin/utils/navigator.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -92,7 +92,7 @@ class RequestRowState extends State<RequestRow> {
return highlightColor;
}
return KeywordHighlight.getHighlightColor(url);
return KeywordHighlights.getHighlightColor(url);
}
BuildContext getContext() => mounted ? super.context : NavigatorHelper().context;

View File

@@ -21,10 +21,11 @@ import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_toastr/flutter_toastr.dart';
import 'package:proxypin/network/bin/server.dart';
import 'package:proxypin/network/host_port.dart';
import 'package:proxypin/network/channel/host_port.dart';
import 'package:proxypin/network/http/http.dart';
import 'package:proxypin/network/http/http_headers.dart';
import 'package:proxypin/network/http_client.dart';
import 'package:proxypin/network/http/http_client.dart';
import 'package:proxypin/ui/configuration.dart';
import 'package:proxypin/ui/content/body.dart';
import 'package:proxypin/utils/curl.dart';
import 'package:proxypin/utils/lang.dart';
@@ -102,7 +103,7 @@ class RequestEditorState extends State<MobileRequestEditor> with SingleTickerPro
onPressed: () {
try {
setState(() {
request = parseCurl(text!);
request = Curl.parse(text!);
requestKey.currentState?.change(request!);
requestLineKey.currentState?.change(request?.requestUrl, request?.method.name);
});
@@ -193,12 +194,13 @@ class RequestEditorState extends State<MobileRequestEditor> with SingleTickerPro
responseKey.currentState?.change(response);
responseChange.value = 1;
tabController.animateTo(1);
// FlutterToastr.show(localizations.requestSuccess, context);
}).catchError((e) {
responseChange.value = -1;
FlutterToastr.show('${localizations.fail}$e', context);
});
tabController.animateTo(1);
}
}
@@ -252,7 +254,7 @@ class _HttpState extends State<_HttpWidget> with AutomaticKeepAliveClientMixin {
message = widget.message;
body = widget.message?.bodyAsString;
if (widget.message?.headers == null && !widget.readOnly) {
initHeader["User-Agent"] = ["ProxyPin/1.1.7"];
initHeader["User-Agent"] = ["ProxyPin/${AppConfiguration.version}"];
initHeader["Accept"] = ["*/*"];
return;
}

View File

@@ -5,7 +5,7 @@ import 'package:proxypin/network/bin/server.dart';
import 'package:proxypin/network/http/http.dart';
import 'package:proxypin/ui/desktop/request/model/search_model.dart';
import 'package:proxypin/ui/mobile/request/request.dart';
import 'package:proxypin/ui/mobile/widgets/highlight.dart';
import 'package:proxypin/utils/keyword_highlight.dart';
import 'package:proxypin/utils/listenable_list.dart';
///请求序列 列表
@@ -53,12 +53,12 @@ class RequestSequenceState extends State<RequestSequence> with AutomaticKeepAliv
setState(() {});
});
};
KeywordHighlight.keywordsController.addListener(highlightListener);
KeywordHighlights.addListener(highlightListener);
}
@override
dispose() {
KeywordHighlight.keywordsController.removeListener(highlightListener);
KeywordHighlights.removeListener(highlightListener);
super.dispose();
}

View File

@@ -55,7 +55,7 @@ class MobileSearchState extends State<MobileSearch> {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 20),
padding: const EdgeInsets.only(left: 0),
child: TextFormField(
controller: _keywordController,
textAlignVertical: TextAlignVertical.center,

View File

@@ -150,7 +150,7 @@ class _PreferenceState extends State<Preference> {
Widget themeColor(BuildContext context) {
return Wrap(
children: ThemeModel.colors.entries.map((pair) {
children: ColorMapping.colors.entries.map((pair) {
var dividerColor = Theme.of(context).focusColor;
var background = appConfiguration.themeColor == pair.value ? dividerColor : Colors.transparent;

View File

@@ -4,7 +4,7 @@ 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/host_port.dart';
import 'package:proxypin/network/channel/host_port.dart';
import 'package:proxypin/ui/component/widgets.dart';
class ExternalProxyDialog extends StatefulWidget {

View File

@@ -15,13 +15,25 @@
*/
import 'package:flutter/material.dart';
import 'package:proxypin/ui/configuration.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../app_update/app_update_repository.dart';
/// 关于
class About extends StatelessWidget {
class About extends StatefulWidget {
const About({super.key});
@override
State<StatefulWidget> createState() {
return _AboutState();
}
}
class _AboutState extends State<About> {
bool checkUpdating = false;
@override
Widget build(BuildContext context) {
AppLocalizations localizations = AppLocalizations.of(context)!;
@@ -39,26 +51,41 @@ class About extends StatelessWidget {
padding: const EdgeInsets.only(left: 10, right: 10),
child: Text(isCN ? "全平台开源免费抓包软件" : "Full platform open source free capture HTTP(S) traffic software")),
const SizedBox(height: 10),
const Text("V1.1.7"),
Text("v${AppConfiguration.version}"),
ListTile(
title: const Text("GitHub"),
trailing: const Icon(Icons.arrow_right),
trailing: const Icon(Icons.open_in_new, size: 22),
onTap: () {
launchUrl(Uri.parse(gitHub), mode: LaunchMode.externalApplication);
}),
ListTile(
title: Text(localizations.feedback),
trailing: const Icon(Icons.arrow_right),
trailing: const Icon(Icons.open_in_new, size: 22),
onTap: () {
launchUrl(Uri.parse("$gitHub/issues"), mode: LaunchMode.externalApplication);
}),
ListTile(
title: Text(localizations.appUpdateCheckVersion),
trailing: checkUpdating
? const SizedBox(width: 22, height: 22, child: CircularProgressIndicator())
: const Icon(Icons.sync, size: 22),
onTap: () async {
if (checkUpdating) {
return;
}
setState(() {
checkUpdating = true;
});
await AppUpdateRepository.checkUpdate(context, canIgnore: false, showToast: true);
setState(() {
checkUpdating = false;
});
}),
ListTile(
title: Text(isCN ? "下载地址" : "Download"),
trailing: const Icon(Icons.arrow_right),
trailing: const Icon(Icons.open_in_new, size: 22),
onTap: () {
launchUrl(
Uri.parse(
isCN ? "https://gitee.com/wanghongenpin/proxypin/releases" : "$gitHub/releases"),
launchUrl(Uri.parse(isCN ? "https://gitee.com/wanghongenpin/proxypin/releases" : "$gitHub/releases"),
mode: LaunchMode.externalApplication);
})
],

View File

@@ -13,28 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:proxypin/ui/component/state_component.dart';
import 'package:proxypin/ui/component/widgets.dart';
import 'package:proxypin/utils/keyword_highlight.dart';
class KeywordHighlight extends StatefulWidget {
static Map<Color, String> keywords = {};
static bool enabled = true;
static ValueNotifier keywordsController = ValueNotifier<Map>(keywords);
static Color? getHighlightColor(String? key) {
if (key == null || !enabled) {
return null;
}
for (var entry in keywords.entries) {
if (key.contains(entry.value)) {
return entry.key;
}
}
return null;
}
const KeywordHighlight({super.key});
@override
@@ -59,7 +45,8 @@ class _KeywordHighlightState extends State<KeywordHighlight> {
title: Text(localizations.keyword + localizations.highlight,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
actions: [
SwitchWidget(scale: 0.7, value: KeywordHighlight.enabled, onChanged: (val) => KeywordHighlight.enabled = val),
SwitchWidget(
scale: 0.7, value: KeywordHighlights.enabled, onChanged: (val) => KeywordHighlights.enabled = val),
const SizedBox(width: 10)
],
),
@@ -75,12 +62,12 @@ class _KeywordHighlightState extends State<KeywordHighlight> {
child: TextFormField(
minLines: 2,
maxLines: 2,
initialValue: KeywordHighlight.keywords[e.key],
initialValue: KeywordHighlights.keywords[e.key],
onChanged: (value) {
if (value.isEmpty) {
KeywordHighlight.keywords.remove(e.key);
KeywordHighlights.keywords.remove(e.key);
} else {
KeywordHighlight.keywords[e.key] = value;
KeywordHighlights.keywords[e.key] = value;
}
},
decoration: decoration(localizations.keyword),
@@ -102,10 +89,10 @@ class _KeywordHighlightState extends State<KeywordHighlight> {
@override
void dispose() {
if (KeywordHighlight.enabled) {
KeywordHighlight.keywordsController.value = Map.from(KeywordHighlight.keywords);
if (KeywordHighlights.enabled) {
KeywordHighlights.saveKeywords(Map.from(KeywordHighlights.keywords));
} else {
KeywordHighlight.keywordsController.value = {};
KeywordHighlights.saveKeywords(KeywordHighlights.keywords);
}
super.dispose();
}

View File

@@ -26,7 +26,7 @@ import 'package:proxypin/network/bin/server.dart';
import 'package:proxypin/network/components/host_filter.dart';
import 'package:proxypin/network/components/manager/request_rewrite_manager.dart';
import 'package:proxypin/network/components/manager/script_manager.dart';
import 'package:proxypin/network/http_client.dart';
import 'package:proxypin/network/http/http_client.dart';
import 'package:proxypin/network/util/logger.dart';
import 'package:proxypin/ui/component/qrcode/qr_scan_view.dart';
import 'package:proxypin/ui/component/utils.dart';

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 'package:proxypin/network/http/http.dart';
import 'package:proxypin/network/http/http_headers.dart';
import 'package:proxypin/utils/lang.dart';
@@ -34,115 +35,114 @@ String curlRequest(HttpRequest request) {
"${headers.join('\\\n')} \\\n $body --compressed";
}
const String _h = "-H";
const String _header = "--header";
const String _x = "-X";
const String _request = "--request";
const String _data = "--data";
const String _dataRaw = "--data-raw";
const String _d = "-d";
///解析curl
HttpRequest parseCurl(String curl) {
var lines = curl.trim().split('\n');
HttpMethod method = HttpMethod.get;
HttpHeaders headers = HttpHeaders();
String requestUrl = '';
String body = '';
bool parseBody = false;
for (var it in lines) {
it = it.trim();
if (it.endsWith("\\")) {
it = it.substring(0, it.length - 1);
}
//header
if (it.startsWith(_h) || it.startsWith(_header)) {
int index = it.startsWith(_h) ? _h.length : _header.length;
var line = it.substring(index).trim();
line = line.startsWith("'") ? line.substring(1) : line;
var endIdx = endIndex(line);
it = line.substring(endIdx + 1);
line = line.substring(0, endIdx == -1 ? line.length : endIdx);
var pair = _split(line, ":");
if (pair != null) {
headers.add(pair.key, pair.value);
}
if (endIdx == -1) {
continue;
}
it = it.trim();
}
if (it.startsWith(_data) || it.startsWith(_d)) {
//body
String value;
if (it.startsWith(_dataRaw)) {
value = it.substring(_dataRaw.length).trim();
} else if (it.startsWith(_data)) {
value = it.substring(_data.length).trim();
} else {
value = it.substring(_d.length).trim();
}
value = value.startsWith('\$') || value.startsWith("'") ? value.substring(1) : value;
int index = endIndex(value);
if (index > 0) {
body += value.substring(0, index);
} else {
parseBody = true;
body += value;
}
} else if (parseBody) {
int index = endIndex(it);
if (index >= 0) {
parseBody = false;
body += "\n${it.substring(0, index)}";
} else {
body += "\n$it";
}
} else if (it.startsWith(_x) || it.startsWith(_request)) {
//method
int index = it.startsWith(_x) ? _x.length : _request.length;
var value = it.substring(index).trim();
method = HttpMethod.valueOf(Strings.trimWrap(value, "'"));
} else if (it.trim().startsWith("'http") || it.startsWith('curl') && it.contains("'http")) {
var index = it.indexOf("'");
var value = it.substring(index + 1).trim();
if (value.endsWith("'")) {
value = value.substring(0, value.length - 1);
}
try {
requestUrl = Uri.decodeFull(value);
} catch (e) {
requestUrl = value;
}
}
}
if (body.isNotEmpty && method == HttpMethod.get) {
method = HttpMethod.post;
}
HttpRequest request = HttpRequest(method, requestUrl);
request.headers.addAll(headers);
request.body = body.codeUnits;
return request;
main() {
print(Curl.parse(
"curl -X POST 'https://example.com/api' -H 'Content-Type: application/json' -d '{\"key\":\"value\"}'"));
}
Pair<String, String>? _split(String line, String code) {
try {
var index = line.codeUnits.indexOf(code.codeUnits.first);
var key = line.substring(0, index).trim();
var value = line.substring(index + 1).trim();
return Pair(key, value);
} catch (e) {
return null;
class Curl {
static const String _h = "-H";
static const String _header = "--header";
static const String _x = "-X";
static const String _request = "--request";
static const String _data = "--data";
static const String _dataRaw = "--data-raw";
static const String _d = "-d";
static HttpRequest parse(String curlCommand) {
HttpMethod method = HttpMethod.get;
HttpHeaders headers = HttpHeaders();
String? url;
String? data;
// 去除 "curl" 关键字并去除首尾空格
String trimmedCommand = curlCommand.replaceFirst('curl', '').trim();
List<String> parts = [];
String currentPart = '';
bool inQuotes = false;
bool inBody = false;
// 处理可能包含引号的参数
for (int i = 0; i < trimmedCommand.length; i++) {
String char = trimmedCommand[i];
if (char == '"' || char == "'") {
if (inBody) {
currentPart += char;
continue;
}
// 如果当前字符是引号,切换 inQuotes 状态
inQuotes = !inQuotes;
} else if (char == ' ' && !inQuotes) {
if (inBody && currentPart.length > 2) {
// 如果当前部分是数据,去掉前后的引号
currentPart = currentPart.substring(1, currentPart.length - 1);
}
if (currentPart == '-d' || currentPart == '--data' || currentPart == '--data-raw') {
inBody = true;
} else {
inBody = false;
}
parts.add(currentPart);
currentPart = '';
} else {
currentPart += char;
}
}
if (currentPart.isNotEmpty) {
if (inBody && currentPart.length > 2) {
// 如果当前部分是数据,去掉前后的引号
currentPart = currentPart.substring(1, currentPart.length - 1);
}
parts.add(currentPart);
}
String protocolVersion = "HTTP/1.1";
// 遍历参数列表进行解析
for (int i = 0; i < parts.length; i++) {
String part = parts[i];
if (part == _x || part == _request) {
// 解析请求方法
if (i + 1 < parts.length) {
method = HttpMethod.valueOf(parts[++i]);
}
} else if (part == _h || part == _header) {
// 解析请求头
if (i + 1 < parts.length) {
String headerStr = parts[++i];
List<String> headerParts = headerStr.splitFirst(':'.codeUnits.first);
if (headerParts.length == 2) {
headers.add(headerParts[0], headerParts[1]);
}
}
} else if (part == _d || part == _dataRaw || part == _data) {
// 解析请求数据
if (i + 1 < parts.length) {
data = parts[++i];
}
} else if (!part.startsWith('-') && part.startsWith("http")) {
// 解析请求 URL
url = part;
} else if ("--http2" == part) {
// protocolVersion = "HTTP2";
}
}
if (data?.isNotEmpty == true && method == HttpMethod.get) {
method = HttpMethod.post;
}
HttpRequest request = HttpRequest(method, url ?? '', protocolVersion: protocolVersion);
request.headers.addAll(headers);
request.body = data?.codeUnits;
return request;
}
}

View File

@@ -16,11 +16,12 @@
import 'dart:convert';
import 'dart:io';
import 'package:proxypin/network/host_port.dart';
import 'package:proxypin/network/channel/host_port.dart';
import 'package:proxypin/network/http/content_type.dart';
import 'package:proxypin/network/http/http.dart';
import 'package:proxypin/network/http/http_headers.dart';
import 'package:proxypin/network/util/process_info.dart';
import 'package:proxypin/ui/configuration.dart';
class Har {
static int maxBodyLength = 1024 * 1024 * 4;
@@ -81,7 +82,7 @@ class Har {
title = title.contains("ProxyPin") ? title : "[ProxyPin]$title";
har["log"] = {
"version": "1.2",
"creator": {"name": "ProxyPin", "version": "1.1.7"},
"creator": {"name": "ProxyPin", "version": AppConfiguration.version},
"pages": [
{
"title": title,
@@ -136,7 +137,7 @@ class Har {
List headers = request['headers'];
var httpRequest = HttpRequest(HttpMethod.valueOf(method), request['url'], protocolVersion: request['httpVersion']);
if (har.containsKey("_id")) httpRequest.requestId = har['_id']; // 页面标识
if (har.containsKey("_id")) httpRequest.requestId = har['_id'].toString(); // 页面标识
httpRequest.processInfo = har['_app'] == null ? null : ProcessInfo.fromJson(har['_app']);
httpRequest.body = request['postData']?['text']?.toString().codeUnits;
for (var element in headers) {

View File

@@ -0,0 +1,70 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:proxypin/ui/configuration.dart';
import 'package:shared_preferences/shared_preferences.dart';
class KeywordHighlights {
static bool _enabled = true;
static bool initialized = false;
static const String storeKey = "highlightKeywords";
static final ValueNotifier _keywordsController = ValueNotifier<Map<Color, String>>({});
static Map<Color, String> get keywords => _keywordsController.value;
static bool get enabled => _enabled;
static set enabled(bool value) {
_enabled = value;
SharedPreferences.getInstance().then((prefs) {
prefs.setBool('highlightEnabled', value);
});
}
static Color? getHighlightColor(String? key) {
if (key == null || !_enabled) {
return null;
}
for (var entry in _keywordsController.value.entries) {
if (key.contains(entry.value)) {
return entry.key;
}
}
return null;
}
static addListener(VoidCallback listener) {
if (!initialized) {
initialized = true;
SharedPreferences.getInstance().then((prefs) {
var enabledVal = prefs.getBool('highlightEnabled');
if (enabledVal != null) {
enabled = enabledVal;
}
var val = prefs.getString(storeKey);
if (val == null) {
return;
}
var map = jsonDecode(val);
map.forEach((key, value) {
var color = ColorMapping.getColor(key);
_keywordsController.value[color] = value;
});
});
}
_keywordsController.addListener(listener);
}
static Future<void> saveKeywords(Map<Color, String> keywords) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
var map = keywords.map((key, value) => MapEntry(ColorMapping.getColorName(key), value));
prefs.setString(storeKey, jsonEncode(map));
_keywordsController.value = keywords;
}
static removeListener(VoidCallback listener) {
_keywordsController.removeListener(listener);
}
}

View File

@@ -13,6 +13,15 @@ extension ListFirstWhere<T> on Iterable<T> {
return null;
}
}
T elementAtOrElse(int index, T Function(int index) defaultValue) {
if (index < 0) return defaultValue(index);
var count = 0;
for (final element in this) {
if (index == count++) return element;
}
return defaultValue(index);
}
}
extension DateTimeFormat on DateTime {
@@ -102,6 +111,14 @@ class Strings {
/// 这样会导致,换行时上一行可能会留很大的空白区域
/// 把每个字符插入一个0宽的字符 \u{200B}
extension StringEnhance on String {
String removePrefix(String prefix) {
if (startsWith(prefix)) {
return substring(prefix.length, length);
} else {
return this;
}
}
String fixAutoLines() {
return Characters(this).join('\u{200B}');
}

View File

@@ -6,7 +6,7 @@ cd ../build/linux/x64/release
rm -rf package
mkdir -p package/DEBIAN
echo "Package: ProxyPin" >> package/DEBIAN/control
echo "Version: 1.1.7" >> package/DEBIAN/control
echo "Version: 1.1.8" >> package/DEBIAN/control
echo "Priority: optional" >> package/DEBIAN/control
echo "Architecture: amd64" >> package/DEBIAN/control
echo "Depends: ca-certificates" >> package/DEBIAN/control

View File

@@ -7,6 +7,7 @@ import Foundation
import desktop_multi_window
import device_info_plus
import file_picker
import flutter_desktop_context_menu
import flutter_js
import path_provider_foundation
@@ -20,6 +21,7 @@ import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterMultiWindowPlugin.register(with: registry.registrar(forPlugin: "FlutterMultiWindowPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FlutterDesktopContextMenuPlugin.register(with: registry.registrar(forPlugin: "FlutterDesktopContextMenuPlugin"))
FlutterJsPlugin.register(with: registry.registrar(forPlugin: "FlutterJsPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))

View File

@@ -59,6 +59,7 @@
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">

View File

@@ -21,7 +21,10 @@ class AppDelegate: FlutterAppDelegate {
return true
}
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
override func applicationWillTerminate(_ notification: Notification) {
AppLifecycleChannel.appDetached()
NSLog("applicationWillTerminate")

View File

@@ -2,6 +2,7 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>

View File

@@ -2,6 +2,7 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>

View File

@@ -6,8 +6,6 @@
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
@@ -15,6 +13,6 @@
<key>com.apple.security.scripting-targets</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,10 @@
title: ProxyPin
contents:
- x: 448
y: 344
type: link
path: "/Applications"
- x: 192
y: 344
type: file
path: ProxyPin.app

View File

@@ -2,7 +2,7 @@ name: proxypin
description: ProxyPin
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.1.7+17
version: 1.1.8+18
environment:
sdk: '>=3.0.2 <4.0.0'
@@ -14,10 +14,11 @@ dependencies:
sdk: flutter
intl: any
cupertino_icons: ^1.0.8
pointycastle: ^3.9.1
pointycastle: ^4.0.0
logger: ^2.5.0
date_format: ^2.0.9
window_manager: ^0.4.3
windows_single_instance: ^1.0.1
desktop_multi_window:
git:
url: https://gitee.com/wanghongenpin/flutter-plugins.git
@@ -38,12 +39,14 @@ dependencies:
shared_preferences: ^2.2.3
image_pickers: ^2.0.5+2
url_launcher: ^6.3.1
toastification: ^3.0.2
qr_flutter: ^4.1.0
flutter_qr_reader: ^1.0.5
brotli: ^0.6.0
win32audio: ^1.3.1
vclibs: ^0.1.3
dev_dependencies:
flutter_test:
@@ -58,3 +61,4 @@ flutter:
- assets/certs/ca.crt
- assets/certs/ca_key.pem
- assets/icon.png
- assets/js/

View File

@@ -13,8 +13,10 @@
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h>
#include <vclibs/vclibs_plugin_c_api.h>
#include <win32audio/win32audio_plugin_c_api.h>
#include <window_manager/window_manager_plugin.h>
#include <windows_single_instance/windows_single_instance_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
DesktopMultiWindowPluginRegisterWithRegistrar(
@@ -31,8 +33,12 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
VclibsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("VclibsPluginCApi"));
Win32audioPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("Win32audioPluginCApi"));
WindowManagerPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
WindowsSingleInstancePluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WindowsSingleInstancePlugin"));
}

View File

@@ -10,8 +10,10 @@ list(APPEND FLUTTER_PLUGIN_LIST
screen_retriever_windows
share_plus
url_launcher_windows
vclibs
win32audio
window_manager
windows_single_instance
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@@ -0,0 +1,39 @@
[Setup]
AppId={{APP_ID}}
AppVersion={{APP_VERSION}}
AppName={{DISPLAY_NAME}}
AppPublisher={{PUBLISHER}}
AppPublisherURL={{PUBLISHER_URL}}
AppSupportURL={{PUBLISHER_URL}}
AppUpdatesURL={{PUBLISHER_URL}}
DefaultDirName={{INSTALL_DIR_NAME}}
DisableProgramGroupPage=yes
OutputDir=.
OutputBaseFilename={{OUTPUT_BASE_FILENAME}}
Compression=lzma
SolidCompression=yes
SetupIconFile={{SETUP_ICON_FILE}}
WizardStyle=modern
CloseApplications=force
[Languages]
{% for locale in LOCALES %}
{% if locale == 'en' %}Name: "english"; MessagesFile: "compiler:Default.isl"{% endif %}
{% if locale == 'zh' %}Name: "chinesesimplified"; MessagesFile: "compiler:Languages\\ChineseSimplified.isl"{% endif %}
{% if locale == 'ja' %}Name: "japanese"; MessagesFile: "compiler:Languages\\Japanese.isl"{% endif %}
{% endfor %}
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: {% if CREATE_DESKTOP_ICON != true %}unchecked{% else %}checkablealone{% endif %}
Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
[Files]
Source: "{{SOURCE_DIR}}\\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons]
Name: "{autoprograms}\\{{DISPLAY_NAME}}"; Filename: "{app}\\{{EXECUTABLE_NAME}}"
Name: "{autodesktop}\\{{DISPLAY_NAME}}"; Filename: "{app}\\{{EXECUTABLE_NAME}}"; Tasks: desktopicon
[Run]
Filename: "{app}\\{{EXECUTABLE_NAME}}"; Description: "{cm:LaunchProgram,{{DISPLAY_NAME}}}"; Flags: nowait postinstall skipifsilent

View File

@@ -0,0 +1,11 @@
app_id: 502cbca5-a7f1-4f8f-894d-9820bac2e36f
publisher: ProxyPin
publisher_url: https://github.com/wanghongenpin/proxypin
display_name: ProxyPin
create_desktop_icon: true
install_dir_name: "{autopf64}\\ProxyPin"
setup_icon_file: windows\runner\resources\app_icon.ico
locales:
- en
- zh
script_template: inno_setup.sas