From 766412785c2742ec8c5d35ee8a3b14960ba80639 Mon Sep 17 00:00:00 2001 From: Alexey Z <47769319+Overman775@users.noreply.github.com> Date: Tue, 28 Mar 2023 20:12:42 +0500 Subject: [PATCH] [http2_adapter] HTTP/2 proxy (#1386) ### New Pull Request Checklist - [X] I have read the [Documentation](https://pub.dartlang.org/packages/dio) - [X] I have searched for a similar pull request in the [project](https://github.com/flutterchina/dio/pulls) and found none - [X] I have updated this branch with the latest `develop` to avoid conflicts (via merge from master or rebase) - [x] I have added the required tests to prove the fix/feature I am adding - [X] I have updated the documentation (if necessary) - [X] I have run the tests and they pass This merge request fixes / refers to the following issues: #1259 #905 ### Pull Request Description Added proxy tunnel to http2 adaptor --------- Signed-off-by: Alexey Z <47769319+Overman775@users.noreply.github.com> Signed-off-by: Alex Li Co-authored-by: Alexey Zdorovykh Co-authored-by: Alex Li --- .github/workflows/tests.yml | 3 + plugins/http2_adapter/CHANGELOG.md | 22 ++-- plugins/http2_adapter/README.md | 44 ++++++-- plugins/http2_adapter/example/example.dart | 10 +- .../http2_adapter/lib/src/client_setting.dart | 5 + .../lib/src/connection_manager_imp.dart | 102 ++++++++++++++++-- plugins/http2_adapter/test/http2_test.dart | 13 +++ 7 files changed, 166 insertions(+), 33 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3e35a51a9..ac847903b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -116,6 +116,9 @@ jobs: chmod +x ./scripts/prepare_pinning_certs.sh ./scripts/prepare_pinning_certs.sh shell: bash + - name: Install proxy + if: matrix.directory == 'plugins/http2_adapter' + run: sudo apt-get install -y squid - run: dart pub get working-directory: ${{ matrix.directory }} - run: dart test --chain-stack-traces diff --git a/plugins/http2_adapter/CHANGELOG.md b/plugins/http2_adapter/CHANGELOG.md index ce034c3ee..d91cacdf5 100644 --- a/plugins/http2_adapter/CHANGELOG.md +++ b/plugins/http2_adapter/CHANGELOG.md @@ -2,31 +2,31 @@ ## Unreleased +- Support proxy for the adapter. - Improve code formats according to linter rules. ## 2.1.0 -* For the `dio`'s 5.0 release. -* Add `validateCertificate` for `ClientSetting`. +- For the `dio`'s 5.0 release. +- Add `validateCertificate` for `ClientSetting`. -## [2.0.0] +## 2.0.0 -* support dio 4.0.0 +- support dio 4.0.0 -## [2.0.0-beta2] +## 2.0.0-beta2 - support null-safety - support dio 4.x - -## [1.0.1] - 2020.8.7 +## 1.0.1 - 2020.8.7 - merge #760 -## [1.0.0] - 2019.9.18 +## 1.0.0 - 2019.9.18 -* Support redirect +- Support redirect -## [0.0.2] - 2019.9.17 +## 0.0.2 - 2019.9.17 -* A Dio HttpAdapter which support Http/2.0. +- A Dio HttpAdapter which support Http/2.0. diff --git a/plugins/http2_adapter/README.md b/plugins/http2_adapter/README.md index 3e8a51196..5a4745ac9 100644 --- a/plugins/http2_adapter/README.md +++ b/plugins/http2_adapter/README.md @@ -19,20 +19,52 @@ dependencies: import 'package:dio/dio.dart'; import 'package:dio_http2_adapter/dio_http2_adapter.dart'; +void main() async { + final dio = Dio() + ..options.baseUrl = 'https://pub.dev' + ..interceptors.add(LogInterceptor()) + ..httpClientAdapter = Http2Adapter( + ConnectionManager(idleTimeout: Duration(seconds: 10)), + ); + + Response response; + response = await dio.get('/?xx=6'); + for (final e in response.redirects) { + print('redirect: ${e.statusCode} ${e.location}'); + } + print(response.data); +} +``` + +### Ignoring a bad certificate + +```dart void main() async { final dio = Dio() ..options.baseUrl = 'https://pub.dev' ..interceptors.add(LogInterceptor()) ..httpClientAdapter = Http2Adapter( ConnectionManager( - idleTimeout: 10000, - // Ignore bad certificate + idleTimeout: Duration(seconds: 10), onClientCreate: (_, config) => config.onBadCertificate = (_) => true, ), ); - final response = await dio.get('/?xx=something'); - print(response.data?.length); - print(response.redirects.length); - print(response.data); +} +``` + +### Configuring the proxy + +```dart +void main() async { + final dio = Dio() + ..options.baseUrl = 'https://pub.dev' + ..interceptors.add(LogInterceptor()) + ..httpClientAdapter = Http2Adapter( + ConnectionManager( + idleTimeout: Duration(seconds: 10), + onClientCreate: (_, config) => + config.proxy = Uri.parse('http://login:password@192.168.0.1:8888'), + ), + ); } ``` diff --git a/plugins/http2_adapter/example/example.dart b/plugins/http2_adapter/example/example.dart index 2e9449db2..9c5592781 100644 --- a/plugins/http2_adapter/example/example.dart +++ b/plugins/http2_adapter/example/example.dart @@ -6,15 +6,13 @@ void main() async { ..options.baseUrl = 'https://pub.dev' ..interceptors.add(LogInterceptor()) ..httpClientAdapter = Http2Adapter( - ConnectionManager( - idleTimeout: Duration(seconds: 10), - ), + ConnectionManager(idleTimeout: Duration(seconds: 10)), ); Response response; - response = await dio.get('/?xx=6'); - print(response.data?.length); - print(response.redirects.length); + for (final e in response.redirects) { + print('redirect: ${e.statusCode} ${e.location}'); + } print(response.data); } diff --git a/plugins/http2_adapter/lib/src/client_setting.dart b/plugins/http2_adapter/lib/src/client_setting.dart index 2b4c162c6..b81d36d06 100644 --- a/plugins/http2_adapter/lib/src/client_setting.dart +++ b/plugins/http2_adapter/lib/src/client_setting.dart @@ -27,4 +27,9 @@ class ClientSetting { /// methods evaluate the root or intermediate certificate, while /// [validateCertificate] evaluates the leaf certificate. ValidateCertificate? validateCertificate; + + /// Create clients with the given [proxy] setting. + /// When it's set, all HTTP/2 traffic from [Dio] will go through the proxy tunnel. + /// This setting uses [Uri] to correctly pass the scheme, address, and port of the proxy. + Uri? proxy; } diff --git a/plugins/http2_adapter/lib/src/connection_manager_imp.dart b/plugins/http2_adapter/lib/src/connection_manager_imp.dart index 4314159aa..359679c61 100644 --- a/plugins/http2_adapter/lib/src/connection_manager_imp.dart +++ b/plugins/http2_adapter/lib/src/connection_manager_imp.dart @@ -71,17 +71,11 @@ class _ConnectionManager implements ConnectionManager { if (onClientCreate != null) { onClientCreate!(uri, clientConfig); } - late SecureSocket socket; + + late final SecureSocket socket; + try { - // Create socket - socket = await SecureSocket.connect( - uri.host, - uri.port, - timeout: options.connectTimeout, - context: clientConfig.context, - onBadCertificate: clientConfig.onBadCertificate, - supportedProtocols: ['h2'], - ); + socket = await _createSocket(uri, options, clientConfig); } on SocketException catch (e) { if (e.osError == null) { if (e.message.contains('timed out')) { @@ -119,6 +113,7 @@ class _ConnectionManager implements ConnectionManager { transportState.latestIdleTimeStamp = DateTime.now(); } }; + // transportState.delayClose( _closed ? Duration(milliseconds: 50) : _idleTimeout, @@ -130,6 +125,93 @@ class _ConnectionManager implements ConnectionManager { return transportState; } + Future _createSocket( + Uri target, + RequestOptions options, + ClientSetting clientConfig, + ) async { + if (clientConfig.proxy == null) { + return SecureSocket.connect( + target.host, + target.port, + timeout: options.connectTimeout, + context: clientConfig.context, + onBadCertificate: clientConfig.onBadCertificate, + supportedProtocols: ['h2'], + ); + } + + final proxySocket = await Socket.connect( + clientConfig.proxy!.host, + clientConfig.proxy!.port, + timeout: options.connectTimeout, + ); + + final String credentialsProxy = + base64Encode(utf8.encode(clientConfig.proxy!.userInfo)); + + // Create http tunnel proxy https://www.ietf.org/rfc/rfc2817.txt + + // Use CRLF as the end of the line https://www.ietf.org/rfc/rfc2616.txt + const crlf = '\r\n'; + + proxySocket.write('CONNECT ${target.host}:${target.port} HTTP/1.1'); + proxySocket.write(crlf); + proxySocket.write('Host: ${target.host}:${target.port}'); + + if (credentialsProxy.isNotEmpty) { + proxySocket.write(crlf); + proxySocket.write('Proxy-Authorization: Basic $credentialsProxy'); + } + + proxySocket.write(crlf); + proxySocket.write(crlf); + + final completerProxyInitialization = Completer(); + + Never _onProxyError(Object? error, StackTrace stackTrace) { + throw DioError( + requestOptions: options, + error: error, + type: DioErrorType.connectionError, + stackTrace: stackTrace, + ); + } + + completerProxyInitialization.future.onError(_onProxyError); + + final proxySubscription = proxySocket.listen( + (event) { + final response = ascii.decode(event); + final lines = response.split(crlf); + final statusLine = lines.first; + + if (statusLine.startsWith('HTTP/1.1 200')) { + completerProxyInitialization.complete(); + } else { + completerProxyInitialization.completeError( + SocketException('Proxy cannot be initialized'), + ); + } + }, + onError: completerProxyInitialization.completeError, + ); + + await completerProxyInitialization.future; + + final socket = await SecureSocket.secure( + proxySocket, + host: target.host, + context: clientConfig.context, + onBadCertificate: clientConfig.onBadCertificate, + supportedProtocols: ['h2'], + ); + + proxySubscription.cancel(); + + return socket; + } + @override void removeConnection(ClientTransportConnection transport) { _ClientTransportConnectionState? transportState; diff --git a/plugins/http2_adapter/test/http2_test.dart b/plugins/http2_adapter/test/http2_test.dart index fbc59face..bfa988db6 100644 --- a/plugins/http2_adapter/test/http2_test.dart +++ b/plugins/http2_adapter/test/http2_test.dart @@ -36,4 +36,17 @@ void main() { final res = await dio.post('post', data: 'TEST'); expect(res.data.toString(), contains('TEST')); }); + + test('request with payload via proxy', () async { + final dio = Dio() + ..options.baseUrl = 'https://httpbin.org/' + ..httpClientAdapter = Http2Adapter(ConnectionManager( + idleTimeout: Duration(milliseconds: 10), + onClientCreate: (uri, settings) => + settings.proxy = Uri.parse('http://localhost:3128'), + )); + + final res = await dio.post('post', data: 'TEST'); + expect(res.data.toString(), contains('TEST')); + }); }