Skip to content

Commit

Permalink
[http2_adapter] HTTP/2 proxy (cfug#1386)
Browse files Browse the repository at this point in the history
### 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: cfug#1259 cfug#905

### Pull Request Description

Added proxy tunnel to http2 adaptor

---------

Signed-off-by: Alexey Z <[email protected]>
Signed-off-by: Alex Li <[email protected]>
Co-authored-by: Alexey Zdorovykh <[email protected]>
Co-authored-by: Alex Li <[email protected]>
  • Loading branch information
3 people authored Mar 28, 2023
1 parent ae90d7d commit 7664127
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 33 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 11 additions & 11 deletions plugins/http2_adapter/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
44 changes: 38 additions & 6 deletions plugins/http2_adapter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> 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:[email protected]:8888'),
),
);
}
```
10 changes: 4 additions & 6 deletions plugins/http2_adapter/example/example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> 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);
}
5 changes: 5 additions & 0 deletions plugins/http2_adapter/lib/src/client_setting.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
102 changes: 92 additions & 10 deletions plugins/http2_adapter/lib/src/connection_manager_imp.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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')) {
Expand Down Expand Up @@ -119,6 +113,7 @@ class _ConnectionManager implements ConnectionManager {
transportState.latestIdleTimeStamp = DateTime.now();
}
};

//
transportState.delayClose(
_closed ? Duration(milliseconds: 50) : _idleTimeout,
Expand All @@ -130,6 +125,93 @@ class _ConnectionManager implements ConnectionManager {
return transportState;
}

Future<SecureSocket> _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<void>();

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;
Expand Down
13 changes: 13 additions & 0 deletions plugins/http2_adapter/test/http2_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
});
}

0 comments on commit 7664127

Please sign in to comment.