Skip to content

Commit

Permalink
[http2_adapter] 🐛 Support HTTP/1.x for proxy responses (#2054)
Browse files Browse the repository at this point in the history
Fixes #2053.

According to RFC 2616/9110, protocols with the same major version will
be allowed in responses.

### New Pull Request Checklist

- [x] I have read the
[Documentation](https://pub.dev/documentation/dio/latest/)
- [x] I have searched for a similar pull request in the
[project](https://github.com/cfug/dio/pulls) and found none
- [x] I have updated this branch with the latest `main` branch to avoid
conflicts (via merge from master or rebase)
- [x] I have added the required tests to prove the fix/feature I'm
adding
- [x] I have updated the documentation (if necessary)
- [x] I have run the tests without failures
- [x] I have updated the `CHANGELOG.md` in the corresponding package

---------

Signed-off-by: Alex Li <[email protected]>
  • Loading branch information
AlexV525 authored Jan 16, 2024
1 parent 51d0bbb commit adc6842
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 12 deletions.
1 change: 1 addition & 0 deletions plugins/http2_adapter/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- Fix cancellation for streamed responses and downloads.
- Fix progress for streamed responses and downloads.
- Bump minimum Dart SDK to 3.0.0 as required by the `http2` package.
- Allows `HTTP/1.0` when connecting to proxies.

## 2.4.0

Expand Down
18 changes: 15 additions & 3 deletions plugins/http2_adapter/lib/src/connection_manager.dart
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
part of 'http2_adapter.dart';

/// ConnectionManager is used to manager the connections that should be reusable.
/// The main responsibility of ConnectionManager is to implement a connection reuse
/// strategy for http2.
/// Manages the connections that should be reusable.
/// It implements a connection reuse strategy for HTTP/2.
abstract class ConnectionManager {
factory ConnectionManager({
Duration idleTimeout = const Duration(seconds: 15),
void Function(Uri uri, ClientSetting)? onClientCreate,
ProxyConnectedPredicate proxyConnectedPredicate =
defaultProxyConnectedPredicate,
}) =>
_ConnectionManager(
idleTimeout: idleTimeout,
onClientCreate: onClientCreate,
proxyConnectedPredicate: proxyConnectedPredicate,
);

/// Get the connection(may reuse) for each request.
Expand All @@ -20,3 +22,13 @@ abstract class ConnectionManager {

void close({bool force = false});
}

/// {@template dio_http2_adapter.ProxyConnectedPredicate}
/// Checks whether the proxy has been connected through the given [status].
/// {@endtemplate}
typedef ProxyConnectedPredicate = bool Function(String protocol, String status);

/// Accepts HTTP/1.x connections for proxies.
bool defaultProxyConnectedPredicate(String protocol, String status) {
return status.startsWith(RegExp(r'HTTP/1+\.\d 200'));
}
32 changes: 23 additions & 9 deletions plugins/http2_adapter/lib/src/connection_manager_imp.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class _ConnectionManager implements ConnectionManager {
_ConnectionManager({
Duration? idleTimeout,
this.onClientCreate,
this.proxyConnectedPredicate = defaultProxyConnectedPredicate,
}) : _idleTimeout = idleTimeout ?? const Duration(seconds: 1);

/// Callback when socket created.
Expand All @@ -13,6 +14,9 @@ class _ConnectionManager implements ConnectionManager {
/// for unverifiable certificates.
final void Function(Uri uri, ClientSetting)? onClientCreate;

/// {@macro dio_http2_adapter.ProxyConnectedPredicate}
final ProxyConnectedPredicate proxyConnectedPredicate;

/// Sets the idle timeout(milliseconds) of non-active persistent
/// connections. For the sake of socket reuse feature with http/2,
/// the value should not be less than 1 second.
Expand Down Expand Up @@ -173,7 +177,9 @@ class _ConnectionManager implements ConnectionManager {
// 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');
// TODO(EVERYONE): Figure out why we can only use an HTTP/1.x proxy here.
const proxyProtocol = 'HTTP/1.1';
proxySocket.write('CONNECT ${target.host}:${target.port} $proxyProtocol');
proxySocket.write(crlf);
proxySocket.write('Host: ${target.host}:${target.port}');

Expand Down Expand Up @@ -203,16 +209,24 @@ class _ConnectionManager implements ConnectionManager {
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'),
);
if (!completerProxyInitialization.isCompleted) {
if (proxyConnectedPredicate(proxyProtocol, statusLine)) {
completerProxyInitialization.complete();
} else {
completerProxyInitialization.completeError(
SocketException(
'Proxy cannot be initialized with status = [$statusLine], '
'host = ${target.host}, port = ${target.port}',
),
);
}
}
},
onError: (e, s) {
if (!completerProxyInitialization.isCompleted) {
completerProxyInitialization.completeError(e, s);
}
},
onError: completerProxyInitialization.completeError,
);

await completerProxyInitialization.future;
Expand Down
18 changes: 18 additions & 0 deletions plugins/http2_adapter/test/http2_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,22 @@ void main() {
expect(content, contains('Numkey: 2'));
expect(content, contains('Booleankey: false'));
});

group(ProxyConnectedPredicate, () {
group('defaultProxyConnectedPredicate', () {
test(
'accepts HTTP/1.x for HTTP/1.1 proxy',
() {
expect(
defaultProxyConnectedPredicate('HTTP/1.1', 'HTTP/1.1 200'),
true,
);
expect(
defaultProxyConnectedPredicate('HTTP/1.1', 'HTTP/1.0 200'),
true,
);
},
);
});
});
}

0 comments on commit adc6842

Please sign in to comment.