mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-05-18 16:06:50 +08:00
Merge branch 'main' into flutter-3.19.6
# Conflicts: # lib/ui/component/qrcode/qr_scan_view.dart # pubspec.yaml
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -46,4 +46,5 @@ app.*.map.json
|
||||
/android/app/release
|
||||
|
||||
l10n_errors.txt
|
||||
pubspec.lock
|
||||
pubspec.lock
|
||||
/dist/
|
||||
@@ -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
1
android/.gitignore
vendored
@@ -11,3 +11,4 @@ GeneratedPluginRegistrant.java
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
/app/.cxx/
|
||||
66
assets/js/fetch.js
Normal file
66
assets/js/fetch.js
Normal 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
18
distribute_options.yaml
Normal 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
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -314,5 +314,15 @@
|
||||
"time": "时间",
|
||||
"nowTimestamp": "当前时间戳(秒)",
|
||||
"hosts": "Hosts 映射",
|
||||
"toAddress": "映射地址"
|
||||
"toAddress": "映射地址",
|
||||
|
||||
"appUpdateCheckVersion": "检查更新",
|
||||
"appUpdateNotAvailableMsg": "已是最新版本",
|
||||
"appUpdateDialogTitle": "有可用更新",
|
||||
"appUpdateUpdateMsg": "ProxyPin 的新版本现已推出。您想现在更新吗?",
|
||||
"appUpdateCurrentVersionLbl": "当前版本",
|
||||
"appUpdateNewVersionLbl": "新版本",
|
||||
"appUpdateUpdateNowBtnTxt": "现在更新",
|
||||
"appUpdateLaterBtnTxt": "以后再说",
|
||||
"appUpdateIgnoreBtnTxt": "忽略"
|
||||
}
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
41
lib/network/bin/listener.dart
Normal file
41
lib/network/bin/listener.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
163
lib/network/channel/channel.dart
Normal file
163
lib/network/channel/channel.dart
Normal 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';
|
||||
}
|
||||
}
|
||||
91
lib/network/channel/channel_context.dart
Normal file
91
lib/network/channel/channel_context.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
214
lib/network/channel/channel_dispatcher.dart
Normal file
214
lib/network/channel/channel_dispatcher.dart
Normal 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);
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
473
lib/network/components/js/xhr.dart
Normal file
473
lib/network/components/js/xhr.dart
Normal 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};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
19
lib/network/handle/relay_handle.dart
Normal file
19
lib/network/handle/relay_handle.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
36
lib/network/handle/websocket_handle.dart
Normal file
36
lib/network/handle/websocket_handle.dart
Normal 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}");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
//请求本服务
|
||||
@@ -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';
|
||||
|
||||
@@ -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'];
|
||||
}
|
||||
|
||||
//解析请求
|
||||
|
||||
107
lib/ui/app_update/app_update_repository.dart
Normal file
107
lib/ui/app_update/app_update_repository.dart
Normal 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; // 最新版本有更多的子版本号
|
||||
}
|
||||
}
|
||||
11
lib/ui/app_update/constants.dart
Normal file
11
lib/ui/app_update/constants.dart
Normal 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);
|
||||
94
lib/ui/app_update/new_version_dialog.dart
Normal file
94
lib/ui/app_update/new_version_dialog.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
49
lib/ui/app_update/remote_version_entity.dart
Normal file
49
lib/ui/app_update/remote_version_entity.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
124
lib/ui/component/app_dialog.dart
Normal file
124
lib/ui/component/app_dialog.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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'
|
||||
: 'Tips:By 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))));
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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()))
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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}) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
82
lib/ui/desktop/toolbar/setting/about.dart
Normal file
82
lib/ui/desktop/toolbar/setting/about.dart
Normal 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")))
|
||||
],
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
///设置外部代理地址
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
: 'Tips:By 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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
})
|
||||
],
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
70
lib/utils/keyword_highlight.dart
Normal file
70
lib/utils/keyword_highlight.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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}');
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
enableGPUValidationMode = "1"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
10
macos/packaging/dmg/make_config.yaml
Normal file
10
macos/packaging/dmg/make_config.yaml
Normal 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
|
||||
@@ -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/
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
39
windows/packaging/exe/inno_setup.sas
Normal file
39
windows/packaging/exe/inno_setup.sas
Normal 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
|
||||
11
windows/packaging/exe/make_config.yaml
Normal file
11
windows/packaging/exe/make_config.yaml
Normal 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
|
||||
Reference in New Issue
Block a user