Repository: cfug/dio

Issue:
Compatibility issues HTTP_PROXY returns `HTTP/1.0 200 Connection established`
### Package

http2_adapter

### Version

1.2.4

### Operating-System

Android, iOS, Web, MacOS, Linux, Windows

### Output of `flutter doctor -v`

_No response_

### Dart Version

_No response_

### Steps to Reproduce

Use Surge as System Proxy  (HTTP)


### Expected Result

Request as expected.

### Actual Result

error: Proxy cannot be initialized


Good Example:

```
CONNECT www.baidu.com:443 HTTP/1.1
Host: www.baidu.com:443
User-Agent: curl/8.1.1
Proxy-Connection: Keep-Alive

HTTP/1.0 200 Connection established
```

https://www.rfc-editor.org/rfc/rfc9110#section-9.3.6

RFC metions a 200 response is good . but code is 

```
if (statusLine.startsWith('HTTP/1.1 200')) {
          completerProxyInitialization.complete();
        } else {
          completerProxyInitialization.completeError(
            SocketException('Proxy cannot be initialized'),
          );
        }
```
 
Also `Proxy-Connection: Keep-Alive` is good for proxy


Discussion: Could you set a debug breakpoint at L207 and inspect the result of `statusLine`?
Sorry, I'm debugging this issue by just sending same content through PacketSender, which can be accquire from https://packetsender.com/. And getting response from my Proxy Server is like below 


<img width="766" alt="image" src="https://github.com/cfug/dio/assets/2778352/7bbeff2a-96b5-4a55-ba09-be292591f452">


I'm following code from this source

![image](https://github.com/cfug/dio/assets/2778352/f327452f-044e-43b4-bfa0-4955ce14c651)

~~P.S. tripple \r\n is required to get response from Surge (Proxy Server ) , one is for line break , two are for ending HEADERS frame~~
curl works with my proxy , and I'm getting this verbose logs :

domain below is replaced by 'foo.com'
```
* Establish HTTP proxy tunnel to foo.com:443
> CONNECT foo.com:443 HTTP/1.1
> Host: foo.com:443
> User-Agent: curl/8.1.1
> Proxy-Connection: Keep-Alive
>
< HTTP/1.0 200 Connection established
<
* CONNECT phase completed
* CONNECT tunnel established, response 200
* ALPN: offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
*  CAfile: /Users/XXX/XXX/ssl/cacert.pem
*  CApath: none
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=*.foo.com
*  start date: Sep 26 00:00:00 2023 GMT
*  expire date: Dec 25 23:59:59 2023 GMT
*  subjectAltName: host "foo.com" matched cert's "*.foo.com"
*  issuer: C=AT; O=ZeroSSL; CN=ZeroSSL RSA Domain Secure Site CA
*  SSL certificate verify ok.
* using HTTP/2
* h2 [:method: GET]
* h2 [:scheme: https]
* h2 [:authority: foo.com]
* h2 [:path: /badge/aaad37f232c0/123.png]
* h2 [user-agent: curl/8.1.1]
* h2 [accept: */*]
* Using Stream ID: 1 (easy handle 0x15a80bc00)
> GET /badge/aaad37f232c0/2a9dfc9c3bc8c50099877013d0c524a1.png HTTP/2
> Host: foo.com
> User-Agent: curl/8.1.1
> Accept: */*
>
< HTTP/2 200
< last-modified: Wed, 01 Nov 2023 08:39:52 GMT
< etag: "2a9dfc9c3bc8c50099877013d0c524a1"
< content-type: image/png
< date: Mon, 04 Dec 2023 07:31:32 GMT
< server: tencent-cos
< vary: Origin, Access-Control-Request-Headers, Access-Control-Request-Method
< x-cos-hash-crc64ecma: 12697382968711187629316
< x-cos-meta-md5: 111
< x-cos-request-id: 111=
< content-length: 80438
< accept-ranges: bytes
< x-nws-log-uuid: 111
< x-cache-lookup: Cache Hit
< access-control-allow-methods: OPTIONS,HEAD,GET
<
```
Charles returns

```
$ nc -4  10.251.41.82 8888
CONNECT www.baidu.com:443 HTTP/1.1
Host:www.baidu.com:443
Proxy-Connection: Keep-Alive

HTTP/1.1 200 Connection established

```

~~Note that only double CRLF is required .~~
Heres Surge returns 

```
$ nc -4  10.251.41.82 6152
CONNECT www.baidu.com:443 HTTP/1.1
Host:www.baidu.com:443
Proxy-Connection: Keep-Alive

HTTP/1.0 200 Connection established
```
> Could you set a debug breakpoint at L207 and inspect the result of `statusLine`?


> > Could you set a debug breakpoint at L207 and inspect the result of `statusLine`?

<img width="1474" alt="image" src="https://github.com/cfug/dio/assets/2778352/ba749328-2ec1-46a6-93e4-eb1f8d537be0">

Fix patch:
diff --git a/plugins/http2_adapter/CHANGELOG.md b/plugins/http2_adapter/CHANGELOG.md
index a65d22591..585f347b1 100644
--- a/plugins/http2_adapter/CHANGELOG.md
+++ b/plugins/http2_adapter/CHANGELOG.md
@@ -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
 
diff --git a/plugins/http2_adapter/lib/src/connection_manager.dart b/plugins/http2_adapter/lib/src/connection_manager.dart
index 8dc8ae5f4..b97bfe823 100644
--- a/plugins/http2_adapter/lib/src/connection_manager.dart
+++ b/plugins/http2_adapter/lib/src/connection_manager.dart
@@ -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.
@@ -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'));
+}
diff --git a/plugins/http2_adapter/lib/src/connection_manager_imp.dart b/plugins/http2_adapter/lib/src/connection_manager_imp.dart
index 036039dee..28430b8fb 100644
--- a/plugins/http2_adapter/lib/src/connection_manager_imp.dart
+++ b/plugins/http2_adapter/lib/src/connection_manager_imp.dart
@@ -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.
@@ -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.
@@ -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}');
 
@@ -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;


Test patch:
diff --git a/plugins/http2_adapter/test/http2_test.dart b/plugins/http2_adapter/test/http2_test.dart
index 41bee3e56..319b40cd4 100644
--- a/plugins/http2_adapter/test/http2_test.dart
+++ b/plugins/http2_adapter/test/http2_test.dart
@@ -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,
+          );
+        },
+      );
+    });
+  });
 }