From 035c5e8a23b4c6c8f4688acfbd63905e3a94eebb Mon Sep 17 00:00:00 2001 From: wanghongenpin Date: Mon, 28 Apr 2025 11:44:05 +0800 Subject: [PATCH] fix websocket decode & websocket payload download (#316) --- lib/l10n/app_en.arb | 1 + lib/l10n/app_zh.arb | 1 + lib/network/handle/websocket_handle.dart | 7 ++- lib/network/http/websocket.dart | 76 +++++++++++++++++++----- lib/ui/component/app_dialog.dart | 2 +- lib/ui/component/utils.dart | 26 +++++--- lib/ui/content/panel.dart | 36 ++++++++--- 7 files changed, 113 insertions(+), 36 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b2f685f..59c1f18 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -18,6 +18,7 @@ "proxySetting": "Proxy Setting", "systemProxy": "Set as System Proxy", "serverNotStart": "Proxy server not started", + "download": "Download", "start": "Start", "stop": "Stop", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 8a1bc54..d2a8df2 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -18,6 +18,7 @@ "proxySetting": "代理设置", "systemProxy": "设置为系统代理", "serverNotStart": "未开启抓包", + "download": "下载", "start": "开始", "stop": "停止", diff --git a/lib/network/handle/websocket_handle.dart b/lib/network/handle/websocket_handle.dart index c1a6086..f23de7f 100644 --- a/lib/network/handle/websocket_handle.dart +++ b/lib/network/handle/websocket_handle.dart @@ -21,8 +21,8 @@ class WebSocketChannelHandler extends ChannelHandler { WebSocketFrame? frame; try { frame = decoder.decode(msg); - } catch (e) { - log.e("websocket decode error", error: e); + } catch (e, stackTrace) { + log.e("websocket decode error", error: e, stackTrace: stackTrace); } if (frame == null) { return; @@ -31,6 +31,7 @@ class WebSocketChannelHandler extends ChannelHandler { message.messages.add(frame); channelContext.listener?.onMessage(channel, message, frame); - logger.d("socket channelRead ${frame.payloadLength} ${frame.fin} ${frame.payloadDataAsString}"); + logger.d( + "[${channelContext.clientChannel?.id}] socket channelRead ${frame.payloadLength} ${frame.fin} ${frame.payloadDataAsString}"); } } diff --git a/lib/network/http/websocket.dart b/lib/network/http/websocket.dart index 995316e..38683fa 100644 --- a/lib/network/http/websocket.dart +++ b/lib/network/http/websocket.dart @@ -16,9 +16,10 @@ import 'dart:convert'; import 'dart:io'; -import 'dart:math'; import 'dart:typed_data'; +import 'package:proxypin/network/util/logger.dart'; + class WebSocketFrame { final bool fin; @@ -52,12 +53,14 @@ class WebSocketFrame { bool get isText => opcode == 0x01; + bool get isBinary => opcode == 0x02; + String get payloadDataAsString { if (opcode == 0x08) { - return '[连接关闭]'; + return '连接关闭'; } if (opcode == 0x02) { - return '[二进制数据]'; + return '二进制数据'; } try { return utf8.decode(payloadData); @@ -69,9 +72,22 @@ class WebSocketFrame { ///websocket 解码器 class WebSocketDecoder { - WebSocketFrame? decode(Uint8List byteBuf) { - var frame = _parseWebSocketFrame(byteBuf); - return frame; + ByteBuffer buffer = ByteBuffer(); + + WebSocketFrame? decode(Uint8List newData) { + buffer.putBytes(newData); + if (!canParseWebSocketFrame(buffer.bytes)) { + return null; + } + + try { + WebSocketFrame frame = _parseWebSocketFrame(buffer.bytes); + buffer.clear(); + return frame; + } catch (e, stackTrace) { + logger.e("WebSocket decode error", error: e, stackTrace: stackTrace); + return null; + } } bool canParseWebSocketFrame(Uint8List data) { @@ -88,20 +104,29 @@ class WebSocketDecoder { var mask = reader.getUint8(1) >> 7; int payloadStart = 2; - if (mask == 1) { - payloadStart += 4; - } + int payloadLength = reader.getUint8(1) & 0x7f; - var payloadLength = reader.getUint8(1) & 0x7f; if (payloadLength == 126) { + if (data.length < 4) return false; + payloadLength = reader.getUint16(2); payloadStart += 2; } else if (payloadLength == 127) { + if (data.length < 10) return false; + payloadLength = reader.getUint64(2); payloadStart += 8; } + if (mask == 1) { + if (data.length < payloadStart + 4) { + return false; + } + payloadStart += 4; + } + if (data.length < payloadStart + payloadLength) { return false; } + return true; } @@ -115,8 +140,7 @@ class WebSocketDecoder { var opcode = reader.getUint8(0) & 0x0f; var mask = reader.getUint8(1) >> 7; - - var payloadLength = reader.getUint8(1) & 0x7f; + int payloadLength = reader.getUint8(1) & 0x7f; int payloadStart = 2; @@ -134,9 +158,14 @@ class WebSocketDecoder { payloadStart += 4; } - var payloadData = data.sublist(payloadStart, min(payloadStart + payloadLength, data.length)); + int payloadDataLength = payloadLength; + if (payloadStart + payloadDataLength > data.length) { + payloadDataLength = data.length - payloadStart; + logger.w("Payload data length exceeds available data, truncating."); + } + + var payloadData = data.sublist(payloadStart, payloadStart + payloadDataLength); - //根据maskKey解密内容 if (mask == 1) { payloadData = unmaskPayload(payloadData, maskingKey); } @@ -145,6 +174,7 @@ class WebSocketDecoder { //inflate payloadData = decompress(payloadData); } + return WebSocketFrame( fin: fin == 1, opcode: opcode, @@ -163,6 +193,7 @@ class WebSocketDecoder { try { return Uint8List.fromList(_ensureDecoder().convert(msg)); } catch (e) { + logger.e("Decompression error", error: e); return msg; } } @@ -176,3 +207,20 @@ class WebSocketDecoder { return unmaskedData; } } + +class ByteBuffer { + Uint8List _bytes = Uint8List(0); + + Uint8List get bytes => _bytes; + + void putBytes(Uint8List newBytes) { + Uint8List tmp = Uint8List(_bytes.length + newBytes.length); + tmp.setAll(0, _bytes); + tmp.setAll(_bytes.length, newBytes); + _bytes = tmp; + } + + void clear() { + _bytes = Uint8List(0); + } +} diff --git a/lib/ui/component/app_dialog.dart b/lib/ui/component/app_dialog.dart index 02c8aee..e378750 100644 --- a/lib/ui/component/app_dialog.dart +++ b/lib/ui/component/app_dialog.dart @@ -79,7 +79,7 @@ class CustomToast extends StatelessWidget { const CustomToast.success( this.message, { super.key, - this.duration = const Duration(seconds: 3), + this.duration = const Duration(seconds: 2), }) : type = AlertType.success, icon = Icons.check_circle; diff --git a/lib/ui/component/utils.dart b/lib/ui/component/utils.dart index 1841451..7d3552a 100644 --- a/lib/ui/component/utils.dart +++ b/lib/ui/component/utils.dart @@ -50,23 +50,26 @@ Icon getIcon(HttpResponse? response) { //展示报文大小 String getPackagesSize(HttpRequest request, HttpResponse? response) { - var package = getPackage(request); - var responsePackage = getPackage(response); + var package = getPackage(request.packageSize); + var responsePackage = getPackage(response?.packageSize); if (responsePackage.isEmpty) { return package; } return "$package / $responsePackage "; } -String getPackage(HttpMessage? message) { - var size = message?.packageSize; +String getPackage(int? size) { if (size == null) { return ""; } - if (size > 1024 * 1024) { - return "${(size / 1024 / 1024).toStringAsFixed(2)}M"; + if (size < 1025) { + return "$size B"; } - return "${(size / 1024).toStringAsFixed(2)}K"; + + if (size > 1024 * 1024) { + return "${(size / 1024 / 1024).toStringAsFixed(2)} MB"; + } + return "${(size / 1024).toStringAsFixed(2)} KB"; } String copyRequest(HttpRequest request, HttpResponse? response) { @@ -100,7 +103,7 @@ RelativeRect menuPosition(BuildContext context) { return position; } -Widget contextMenu(BuildContext context, EditableTextState editableTextState) { +Widget contextMenu(BuildContext context, EditableTextState editableTextState, {ContextMenuButtonItem? customItem}) { List list = [ ContextMenuButtonItem( onPressed: () { @@ -128,8 +131,13 @@ Widget contextMenu(BuildContext context, EditableTextState editableTextState) { editableTextState.selectAll(SelectionChangedCause.tap); }, type: ContextMenuButtonType.selectAll, - ) + ), ]; + + if (customItem != null) { + list.add(customItem); + } + if (Platform.isIOS) { list.add(ContextMenuButtonItem( onPressed: () async { diff --git a/lib/ui/content/panel.dart b/lib/ui/content/panel.dart index c21cd9d..4b90867 100644 --- a/lib/ui/content/panel.dart +++ b/lib/ui/content/panel.dart @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -21,6 +23,7 @@ import 'package:proxypin/network/bin/server.dart'; import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/http/websocket.dart'; import 'package:proxypin/storage/favorites.dart'; +import 'package:proxypin/ui/component/app_dialog.dart'; import 'package:proxypin/ui/component/share.dart'; import 'package:proxypin/ui/component/state_component.dart'; import 'package:proxypin/ui/component/utils.dart'; @@ -218,13 +221,28 @@ class NetworkTabState extends State with SingleTickerProvi child: Text(message.time.format(), style: const TextStyle(fontSize: 12, color: Colors.grey))), Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: message.isFromClient ? Colors.green.withOpacity(0.26) : Colors.blue.withOpacity(0.3), - borderRadius: BorderRadius.circular(10), - ), - child: Text(message.payloadDataAsString), - ) + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: + message.isFromClient ? Colors.green.withOpacity(0.26) : Colors.blue.withOpacity(0.3), + borderRadius: BorderRadius.circular(10), + ), + child: SelectableText( + "${message.payloadDataAsString}${message.isBinary ? ' ${getPackage(message.payloadLength)}' : ''}", + contextMenuBuilder: (context, editableTextState) => + contextMenu(context, editableTextState, + customItem: ContextMenuButtonItem( + label: localizations.download, + onPressed: () async { + String? path = + (await FilePicker.platform.saveFile(bytes: message.payloadData)); + if (path != null && mounted) { + CustomToast.success(localizations.saveSuccess).show(this.context); + } + }, + type: ContextMenuButtonType.custom, + )), + )) ]), ), const SizedBox(width: 8), @@ -266,9 +284,9 @@ class NetworkTabState extends State with SingleTickerProvi const SizedBox(height: 20), rowWidget("Response Content-Type", response?.headers.contentType), const SizedBox(height: 20), - rowWidget("Request Package", getPackage(request)), + rowWidget("Request Package", getPackage(request.packageSize)), const SizedBox(height: 20), - rowWidget("Response Package", getPackage(response)), + rowWidget("Response Package", getPackage(response?.packageSize)), const SizedBox(height: 20), ]; if (request.processInfo != null) {