From 8a06f79a48d9d991ae51a8789ba6bb5ae9b0998f Mon Sep 17 00:00:00 2001 From: wanghongenpin Date: Fri, 9 May 2025 18:43:34 +0800 Subject: [PATCH] request edit support http2 (#388)(#51) --- ios/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 1 + lib/network/channel/channel_dispatcher.dart | 14 +++--- lib/network/channel/network.dart | 5 ++- lib/network/handle/http_proxy_handle.dart | 13 ++++-- lib/network/http/h2/h2_codec.dart | 45 +++++++++++++------ lib/network/http/http.dart | 7 ++- lib/network/http/http_client.dart | 2 +- lib/ui/desktop/request/request_editor.dart | 22 +++++++-- lib/ui/mobile/request/request_editor.dart | 11 ++++- lib/ui/mobile/request/request_sequence.dart | 14 +++++- 11 files changed, 100 insertions(+), 36 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 4ef7d8b..766c348 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 55; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 8e3ca5d..15cada4 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -59,6 +59,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/lib/network/channel/channel_dispatcher.dart b/lib/network/channel/channel_dispatcher.dart index a29d190..018c824 100644 --- a/lib/network/channel/channel_dispatcher.dart +++ b/lib/network/channel/channel_dispatcher.dart @@ -101,17 +101,21 @@ class ChannelDispatcher extends ChannelHandler { return; } - if (!decodeResult.isDone) { - return; - } - if (decodeResult.forward != null) { + buffer.clearRead(); + if (remoteChannel != null) { await remoteChannel.writeBytes(decodeResult.forward!); } else { logger.w("[$channel] forward remoteChannel is null"); } - buffer.clearRead(); + + if (decodeResult.data == null) { + return; + } + } + + if (!decodeResult.isDone) { return; } diff --git a/lib/network/channel/network.dart b/lib/network/channel/network.dart index 232c90c..88c081e 100644 --- a/lib/network/channel/network.dart +++ b/lib/network/channel/network.dart @@ -177,13 +177,14 @@ class Server extends Network { if (selectedProtocol != null) certificate.setAlpnProtocols([selectedProtocol], true); //处理客户端ssl握手 - var secureSocket = await SecureSocket.secureServer(channel.socket, certificate, bufferedData: data); + var secureSocket = await SecureSocket.secureServer(channel.socket, certificate, bufferedData: data, + supportedProtocols: selectedProtocol != null ? [selectedProtocol] : null); channel.serverSecureSocket(secureSocket, channelContext); } catch (error, trace) { logger.e('[${channel.id}] $hostAndPort ssl error', error: error, stackTrace: trace); try { channelContext.processInfo ??= - await ProcessInfoUtils.getProcessByPort(channel.remoteSocketAddress, hostAndPort?.domain ?? 'unknown'); + await ProcessInfoUtils.getProcessByPort(channel.remoteSocketAddress, hostAndPort?.domain ?? 'unknown'); } catch (ignore) { /*ignore*/ } diff --git a/lib/network/handle/http_proxy_handle.dart b/lib/network/handle/http_proxy_handle.dart index 4c8b6f3..64b0f68 100644 --- a/lib/network/handle/http_proxy_handle.dart +++ b/lib/network/handle/http_proxy_handle.dart @@ -87,7 +87,7 @@ class HttpProxyChannelHandler extends ChannelHandler { //实现抓包代理转发 if (httpRequest.method != HttpMethod.connect) { - log.d("[${channel.id}] ${httpRequest.method.name} ${httpRequest.requestUrl}"); + log.d("[${channel.id}] ${httpRequest.protocolVersion} ${httpRequest.method.name} ${httpRequest.requestUrl}"); if (HostFilter.filter(httpRequest.hostAndPort?.host)) { await remoteChannel.write(httpRequest); return; @@ -171,7 +171,8 @@ class HttpProxyChannelHandler extends ChannelHandler { } else { if (clientChannel.isSsl) { await HttpClients.connectRequest(hostAndPort, proxyChannel, proxyInfo: proxyInfo); - await proxyChannel.secureSocket(channelContext, host: hostAndPort.host); + await proxyChannel.secureSocket(channelContext, + host: hostAndPort.host, supportedProtocols: httpRequest.protocolVersion == "HTTP/2" ? ["h2"] : null); } } @@ -191,7 +192,11 @@ class HttpProxyChannelHandler extends ChannelHandler { final proxyChannel = await connectRemote(channelContext, clientChannel, remoteAddress); if (clientChannel.isSsl) { - await proxyChannel.secureSocket(channelContext, host: hostAndPort.host); + await proxyChannel.secureSocket(channelContext, + host: hostAndPort.host, + supportedProtocols: channelContext.clientChannel?.selectedProtocol == null + ? null + : [channelContext.clientChannel!.selectedProtocol!]); } //https代理新建连接请求 @@ -222,7 +227,7 @@ class HttpResponseProxyHandler extends ChannelHandler { @override void channelRead(ChannelContext channelContext, Channel channel, HttpResponse msg) async { - var request = channelContext.currentRequest; + var request = msg.request ?? channelContext.currentRequest; request?.response = msg; //域名是否过滤 diff --git a/lib/network/http/h2/h2_codec.dart b/lib/network/http/h2/h2_codec.dart index 5722bca..c70cc22 100644 --- a/lib/network/http/h2/h2_codec.dart +++ b/lib/network/http/h2/h2_codec.dart @@ -32,7 +32,7 @@ import 'hpack/hpack.dart'; abstract class Http2Codec implements Codec { static const maxFrameSize = 16384; - static final List connectionPrefacePRI = "PRI * HTTP/2.0".codeUnits; + static final List connectionPrefacePRI = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".codeUnits; HPackDecoder decoder = HPackDecoder(); @@ -44,42 +44,52 @@ abstract class Http2Codec implements Codec { @override DecoderResult decode(ChannelContext channelContext, ByteBuf byteBuf, {bool resolveBody = true}) { + DecoderResult result = DecoderResult(); + //Connection Preface PRI * HTTP/2.0 if (byteBuf.get(byteBuf.readerIndex) == 0x50 && byteBuf.get(byteBuf.readerIndex + 1) == 0x52 && byteBuf.get(byteBuf.readerIndex + 2) == 0x49 && isConnectionPrefacePRI(byteBuf)) { - DecoderResult result = DecoderResult(); - result.forward = byteBuf.readAvailableBytes(); - return result; + + result.forward = byteBuf.readBytes(connectionPrefacePRI.length); + // logger.d( + // "Connection Preface ${connectionPrefacePRI.length} ${String.fromCharCodes(result.forward!)} ${byteBuf.readableBytes()}"); + if (byteBuf.readableBytes() <= 0) { + return result; + } } while (byteBuf.isReadable()) { - DecoderResult result = DecoderResult(isDone: false); FrameHeader? frameHeader = FrameReader.readFrameHeader(byteBuf); - // logger.d("${frameHeader?.streamIdentifier} frame ${frameHeader?.length} ${byteBuf.readableBytes()}"); - + // logger.d( + // "frameHeader streamId: ${frameHeader?.streamIdentifier} frame ${frameHeader?.type.name} ${frameHeader?.length} ${byteBuf.readableBytes()}"); if (frameHeader == null) { + result.isDone = false; return result; } List? framePayload = FrameReader._readFramePayload(byteBuf, frameHeader.length); if (framePayload == null) { + result.isDone = false; byteBuf.readerIndex -= FrameReader.headerLength; return result; } - result = parseHttp2Packet(channelContext, frameHeader, framePayload); - if (result.isDone) { - return result; + var parseResult = parseHttp2Packet(channelContext, frameHeader, framePayload); + if (result.forward != null) { + parseResult.forward = List.of(result.forward!)..addAll(parseResult.forward ?? []); } + + return parseResult; } - return DecoderResult(isDone: false); + result.isDone = false; + return result; } DecoderResult parseHttp2Packet(ChannelContext channelContext, FrameHeader frameHeader, List framePayload) { - var result = DecoderResult(); + var result = DecoderResult(isDone: false); // logger.d( // "${this is Http2RequestDecoder ? 'request' : 'response'} streamId: ${frameHeader.streamIdentifier} ${frameHeader.type} endHeaders: ${frameHeader.hasEndHeadersFlag} " // "endStream: ${frameHeader.hasEndStreamFlag} ${frameHeader.length}"); @@ -116,6 +126,12 @@ abstract class Http2Codec implements Codec { SettingHandler.handleSettingsFrame(channelContext, frameHeader, ByteBuf(framePayload)); result.forward = List.from(frameHeader.encode())..addAll(framePayload); return result; + case FrameType.windowUpdate: + //处理WINDOW_UPDATE帧 + var windowSizeIncrement = readInt32(framePayload, 0) & 0x7fffffff; + logger.d("h2 windowUpdate streamId: ${frameHeader.streamIdentifier} windowSizeIncrement: $windowSizeIncrement"); + result.forward = List.from(frameHeader.encode())..addAll(framePayload); + return result; case FrameType.goaway: var lastStreamId = readInt32(framePayload, 0); var errorCode = readInt32(framePayload, 4); @@ -200,8 +216,9 @@ abstract class Http2Codec implements Codec { void _writeFrame(BytesBuilder bytesBuilder, FrameType type, int flag, int streamId, List payload) { FrameHeader frameHeader = FrameHeader(payload.length, type, flag, streamId); - // logger.d( - // "${this is Http2RequestDecoder ? 'request' : 'response'} _writeFrame streamId: ${frameHeader.streamIdentifier} ${frameHeader.type} flags:${frameHeader.flags} endHeaders: ${frameHeader.hasEndHeadersFlag} endStream: ${frameHeader.hasEndStreamFlag} ${payload.length}"); + logger.d( + "${this is Http2RequestDecoder ? 'request' : 'response'} _writeFrame streamId: ${frameHeader.streamIdentifier} ${frameHeader.type} flags:${frameHeader.flags} endHeaders: ${frameHeader.hasEndHeadersFlag} endStream: ${frameHeader.hasEndStreamFlag} ${payload.length}"); + bytesBuilder.add(frameHeader.encode()); bytesBuilder.add(payload); } diff --git a/lib/network/http/http.dart b/lib/network/http/http.dart index 8c951a5..4cc6be6 100644 --- a/lib/network/http/http.dart +++ b/lib/network/http/http.dart @@ -246,6 +246,7 @@ class HttpRequest extends HttpMessage { '_class': 'HttpRequest', 'uri': requestUrl, 'method': method.name, + 'protocolVersion': protocolVersion, 'packageSize': packageSize, 'headers': headers.toJson(), 'body': body == null ? null : String.fromCharCodes(body!), @@ -254,7 +255,9 @@ class HttpRequest extends HttpMessage { } factory HttpRequest.fromJson(Map json) { - var request = HttpRequest(HttpMethod.valueOf(json['method']), json['uri']); + var request = HttpRequest(HttpMethod.valueOf(json['method']), json['uri'], + protocolVersion: json['protocolVersion'] ?? "HTTP/1.1"); + request.headers.addAll(HttpHeaders.fromJson(json['headers'])); request.body = json['body']?.toString().codeUnits; if (json['requestTime'] != null) { @@ -320,7 +323,7 @@ class HttpResponse extends HttpMessage { @override String toString() { - return 'HttpResponse{status: ${status.code}, headers: $headers, contentLength: $contentLength, bodyLength: ${body?.length}}'; + return 'HttpResponse{status: ${status.code}, protocolVersion: $protocolVersion headers: $headers, contentLength: $contentLength, bodyLength: ${body?.length}}'; } } diff --git a/lib/network/http/http_client.dart b/lib/network/http/http_client.dart index 03d7aa1..8b28c9d 100644 --- a/lib/network/http/http_client.dart +++ b/lib/network/http/http_client.dart @@ -195,7 +195,7 @@ class Http2ClientHandler { onError: (error, trace) => handler.exceptionCaught(channelContext, channel, error, trace: trace), onDone: () => handler.channelInactive(channelContext, channel)); - channel.writeBytes("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".codeUnits); + channel.writeBytes(Http2Codec.connectionPrefacePRI); } onData(ChannelContext channelContext, Channel channel, Uint8List data) { diff --git a/lib/ui/desktop/request/request_editor.dart b/lib/ui/desktop/request/request_editor.dart index 688d3ad..31d2af8 100644 --- a/lib/ui/desktop/request/request_editor.dart +++ b/lib/ui/desktop/request/request_editor.dart @@ -143,7 +143,22 @@ class RequestEditorState extends State { title: Row(children: [ const Text("Response", style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), const Spacer(), - Text(response?.status.toString() ?? '', style: const TextStyle(fontSize: 14)) + Text.rich(TextSpan(children: [ + TextSpan( + text: response?.protocolVersion, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + decorationColor: Colors.green, + color: Colors.green)), + WidgetSpan(child: SizedBox(width: 12)), + TextSpan( + text: response?.status.code.toString() ?? '', + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14, + color: response?.status.isSuccessful() == true ? Colors.green : Colors.red)) + ])) ]), message: response, readOnly: true)) @@ -160,8 +175,8 @@ class RequestEditorState extends State { var headers = requestKey.currentState?.getHeaders(); var requestBody = requestKey.currentState?.getBody(); String url = currentState.requestUrl.text; - - HttpRequest request = HttpRequest(HttpMethod.valueOf(currentState.requestMethod), Uri.parse(url).toString()); + HttpRequest request = HttpRequest(HttpMethod.valueOf(currentState.requestMethod), Uri.parse(url).toString(), + protocolVersion: this.request?.protocolVersion ?? "HTTP/1.1"); request.headers.addAll(headers); request.body = requestBody == null ? null : utf8.encode(requestBody); @@ -300,6 +315,7 @@ class _HttpState extends State<_HttpWidget> { height: MediaQuery.of(context).size.height - 120, child: DefaultTabController( length: tabs.length, + initialIndex: tabs.length >= 3 ? 1 : 0, child: Scaffold( primary: false, appBar: PreferredSize( diff --git a/lib/ui/mobile/request/request_editor.dart b/lib/ui/mobile/request/request_editor.dart index 6cf8a0a..2715d90 100644 --- a/lib/ui/mobile/request/request_editor.dart +++ b/lib/ui/mobile/request/request_editor.dart @@ -161,9 +161,14 @@ class RequestEditorState extends State with SingleTickerPro return _HttpWidget( key: responseKey, title: Row(children: [ + Text(response?.protocolVersion ?? '', + style: const TextStyle(fontWeight: FontWeight.w500, color: Colors.blue)), + const SizedBox(width: 10), Text("${localizations.statusCode}: ", style: const TextStyle(fontWeight: FontWeight.w500)), const SizedBox(width: 10), - Text(response?.status.toString() ?? "", style: const TextStyle(color: Colors.blue)) + Text(response?.status.toString() ?? "", + style: + TextStyle(color: response?.status.isSuccessful() == true ? Colors.blue : Colors.red)) ]), readOnly: true, message: response); @@ -179,7 +184,9 @@ class RequestEditorState extends State with SingleTickerPro var requestBody = requestKey.currentState?.getBody(); String url = currentState.requestUrl.text; - HttpRequest request = HttpRequest(HttpMethod.valueOf(currentState.requestMethod), Uri.parse(url).toString()); + HttpRequest request = HttpRequest(HttpMethod.valueOf(currentState.requestMethod), Uri.parse(url).toString(), + protocolVersion: this.request?.protocolVersion ?? "HTTP/1.1"); + request.headers.addAll(headers); request.body = requestBody == null ? null : utf8.encode(requestBody); diff --git a/lib/ui/mobile/request/request_sequence.dart b/lib/ui/mobile/request/request_sequence.dart index 598fd4c..6409c30 100644 --- a/lib/ui/mobile/request/request_sequence.dart +++ b/lib/ui/mobile/request/request_sequence.dart @@ -18,7 +18,12 @@ class RequestSequence extends StatefulWidget { final Function(List)? onRemove; const RequestSequence( - {super.key, required this.container, required this.proxyServer, this.displayDomain = true, this.onRemove, this.sortDesc}); + {super.key, + required this.container, + required this.proxyServer, + this.displayDomain = true, + this.onRemove, + this.sortDesc}); @override State createState() { @@ -154,9 +159,14 @@ class RequestSequenceState extends State with AutomaticKeepAliv Divider(thickness: 0.2, height: 0, color: Theme.of(context).dividerColor), itemCount: view.length, itemBuilder: (context, index) { + final requestId = view.elementAt(index).requestId; + + // 确保每个 requestId 对应唯一的 GlobalKey + final key = indexes.putIfAbsent(requestId, () => GlobalKey()); + return RequestRow( index: sortDesc ? view.length - index : index, - key: indexes[view.elementAt(index).requestId] ??= GlobalKey(), + key: key, request: view.elementAt(index), proxyServer: widget.proxyServer, displayDomain: widget.displayDomain,