Repository: codeigniter4/CodeIgniter4 (PHP)

Issue:
Bug: [CURLRequest] body contains "HTTP/1.0 200 Connection established"
### PHP Version

8.1

### CodeIgniter4 Version

4.4.4

### CodeIgniter4 Installation Method

Composer (using `codeigniter4/appstarter`)

### Which operating systems have you tested for this bug?

Windows

### Which server did you use?

apache

### Database

_No response_

### What happened?

$response = $this->curlClient->request('POST', $url);
echo $response->getBody();

Body return this 

HTTP/1.0 200 Connection established
HTTP/1.1 200 OK
referrer-policy: no-referrer
x-frame-options: SAMEORIGIN
strict-transport-security: max-age=16070400; includeSubDomains
cache-control: no-store
x-content-type-options: nosniff
set-cookie: KC_RESTART=; Version=1; Expires=Thu, 01-Jan-1970 00:00:10 GMT; Max-Age=0; Path=/a/; Secure; HttpOnlyHttpOnly;SameSite=none; Secure
pragma: no-cache
x-xss-protection: 1; mode=block
content-type: application/json
content-length: 2684
x-envoy-upstream-service-time: 38
date: Wed, 07 Feb 2024 07:06:40 GMT
server: istio-envoy

{"data":1,"expires_in":1800}

### Steps to Reproduce

$response = $this->curlClient->request('POST', $url);
echo $response->getBody();

### Expected Output

echo $response->getBody(); == {"data":1,"expires_in":1800}

I understand that getBody returns all the information returned by the server. But I guess it should be that $response->headers() returns the headers and $response->getBody() returns the data.

### Anything else?

executing curl from the command line returns only data correctly. Postman also returns the data correctly


Discussion:
Thank you for reporting!
But we cannot reproduce the issue.
Can you show the exact steps to reproduce the issue?

I changed 
      
  if (strpos($output, 'HTTP/1.1 100 Continue') === 0) {
            $output = substr($output, strpos($output, $breakString) + 4);
        }

        if (strpos($output, 'HTTP/1.1 200 Connection established') === 0) {
            $output = substr($output, strpos($output, $breakString) + 4);
        }

to 

  if (strpos($output, 'HTTP/1.0 100 Continue') === 0) {
            $output = substr($output, strpos($output, $breakString) + 4);
        }

        if (strpos($output, 'HTTP/1.0 200 Connection established') === 0) {
            $output = substr($output, strpos($output, $breakString) + 4);
        }


and it works properly

see what header the server returns

HTTP/1.0 200 Connection established
HTTP/1.1 200 OK
As part of the explanation. The protocol header can take several values https://everything.curl.dev/http/versions
HTTP/1.0 100 Continue
HTTP/1.0 200 Connection established
HTTP/1.1 100 Continue
HTTP/1.1 200 Connection established
HTTP/2 100 Continue
HTTP/2 200 Connection established
HTTP/3 100 Continue
HTTP/3 200 Connection established

and in the code we have it rigid

 if (strpos($output, 'HTTP/1.1 100 Continue') === 0) {
            $output = substr($output, strpos($output, $breakString) + 4);
        }


		This code reuest to www.google.pl return HTTP/1.0 200 Connection established HTTP/2 200
		
	   $client = \Config\Services::curlrequest();
       $options = [
        'http_errors' => false,
        'debug'       => true, 
        'proxy'       => 'ip_proxy',
        
        ];

       $adr ='https://www.google.pl';
       $response = $client->request('GET', $adr, $options);

            foreach ($response->headers() as $name => $value) {
                echo $name . ': ' . $response->getHeaderLine($name) . "\n";
            }

            echo $response->getBody();
Cannot reproduce. Why do you still use `HTTP/1.0`?

```console
$ docker pull ubuntu/squid
$ docker run -d --name squid-container -e TZ=UTC -p 3128:3128 ubuntu/squid
```

```console
$ curl -v --proxy http://localhost:3128 https://www.google.pl
* Host localhost:3128 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:3128...
* Connected to localhost (::1) port 3128
* CONNECT tunnel: HTTP/1.1 negotiated
* allocate connect buffer
* Establish HTTP proxy tunnel to www.google.pl:443
> CONNECT www.google.pl:443 HTTP/1.1
> Host: www.google.pl:443
> User-Agent: curl/8.5.0
> Proxy-Connection: Keep-Alive
> 
< HTTP/1.1 200 Connection established
< 
* CONNECT phase completed
* CONNECT tunnel established, response 200
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / x25519 / id-ecPublicKey
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=*.google.pl
*  start date: Jan  9 06:34:13 2024 GMT
*  expire date: Apr  2 06:34:12 2024 GMT
*  subjectAltName: host "www.google.pl" matched cert's "*.google.pl"
*  issuer: C=US; O=Google Trust Services LLC; CN=GTS CA 1C3
*  SSL certificate verify ok.
*   Certificate level 0: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using sha256WithRSAEncryption
*   Certificate level 1: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
*   Certificate level 2: Public key type RSA (4096/152 Bits/secBits), signed using sha384WithRSAEncryption
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://www.google.pl/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: www.google.pl]
* [HTTP/2] [1] [:path: /]
* [HTTP/2] [1] [user-agent: curl/8.5.0]
* [HTTP/2] [1] [accept: */*]
> GET / HTTP/2
> Host: www.google.pl
> User-Agent: curl/8.5.0
> Accept: */*
> 
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
< HTTP/2 200 
< date: Thu, 08 Feb 2024 02:33:26 GMT
< expires: -1
< cache-control: private, max-age=0
< content-type: text/html; charset=ISO-8859-2
< content-security-policy-report-only: object-src 'none';base-uri 'self';script-src 'nonce-iP9bOGIZesCEhajHCzCyYg' 'strict-dynamic' 'report-sample' 'unsafe-eval' 'unsafe-inline' https: http:;report-uri https://csp.withgoogle.com/csp/gws/other-hp
< p3p: CP="This is not a P3P policy! See g.co/p3phelp for more info."
< server: gws
< x-xss-protection: 0
< x-frame-options: SAMEORIGIN
< set-cookie: 1P_JAR=2024-02-08-02; expires=Sat, 09-Mar-2024 02:33:26 GMT; path=/; domain=.google.pl; Secure
< set-cookie: AEC=Ae3NU9PVS9ERTsF4MBaHEHkFIpmqIbkOuLRcgNpHYHUUBRKYAD0stHx2Sok; expires=Tue, 06-Aug-2024 02:33:26 GMT; path=/; domain=.google.pl; Secure; HttpOnly; SameSite=lax
< set-cookie: NID=511=UfuEJr8IAiuabesErKhUm8XK_shijk-0oa_lw1ouTTKbhvhOVvlTSmnR_D3d_VTJwbVZu-0CIDzCYlUliNZmDphIXDndV_DCmDKc-asSmjILCQ7DB4cHQEjAE9EO9TDl9Ts17qhOiHraSA1LHHF-Fz8_E0o9cMc0SokTdcALKR8; expires=Fri, 09-Aug-2024 02:33:26 GMT; path=/; domain=.google.pl; HttpOnly
< alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
< accept-ranges: none
< vary: Accept-Encoding
< 
<!doctype html>...
```
I admit that my case is isolated. I deliberately don't use HTTP/1.0. I connect to different external services through different proxies and one of them uses an old protocol. I have no influence on it. But Codeigniter shouldn't be limited to just one HTTP/1.1
This piece of code solves my problem

        if (preg_match('/HTTP\/\d\.\d 200 Connection established/', $output)) {
            $output = substr($output, strpos($output, $breakString) + 4);
        }
I'm not the only one who has this problem. The problem is that the proxy returns its own header and the curl library returns too much
https://stackoverflow.com/questions/16965530/what-to-do-with-extra-http-header-from-proxy
https://github.com/cfug/dio/issues/2053
A small update. I migrated from 4.4.4 to 4.5.1. 
My proxy sends headers: 
```
HTTP/1.0 200 Connection established

HTTP/1.1 200 OK
cache-control: no-store
pragma: no-cache
content-length: 2684
content-type: application/json
```
\vendor\codeigniter4\framework\system\HTTP\CURLRequest.php  
Line 392 in version 4.5.1 has been changed to : 
```
        if (str_starts_with($output, 'HTTP/1.1 200 Connection established')) {
            $output = substr($output, strpos($output, $breakString) + 4);
        }



```
But the problem was not solved
My solution also stopped working

Don't set it rigidly. 
str_starts_with($output, 'HTTP/1.1 200 Connection established')
The protocol will change and there will be a problem
Please check good a functions, present's problems on issue calling, to more info please check debug log.
Suggest this function:
```
<?php namespace App\Libraries;

use Config\Services;

class CurlLibrary {

    public function curl(string $method = "GET", string $url = null, array $data = array()){
         $curl = Services::curlrequest();
         return $curl->request($method, $url, $data);
    }
}
```
Solve problems presents on yours implements and use rest api is good to solve use try catch ... and please not use direct and check out calling to source to more sure if working your pedding call.
Some older proxies add the header 
HTTP/1.0 200 Connection Established or 
HTTP/1.0 200 Connection established. 
Newer ones use 
HTTP/1.1 200 Connection Established or 
HTTP/1.1 200 Connection established. 

I suggest a change.

```
        if (preg_match('/HTTP\/\d\.\d 200 Connection established/i', $output)) {
            $output = substr($output, strpos($output, $breakString) + 4);
        }
```



Patch:
diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php
index 9da231c17716..90157e4e05d4 100644
--- a/system/HTTP/CURLRequest.php
+++ b/system/HTTP/CURLRequest.php
@@ -389,7 +389,7 @@ public function send(string $method, string $url)
             $output = substr($output, strpos($output, $breakString) + 4);
         }
 
-        if (str_starts_with($output, 'HTTP/1.1 200 Connection established')) {
+        if (preg_match('/HTTP\/\d\.\d 200 Connection established/i', $output)) {
             $output = substr($output, strpos($output, $breakString) + 4);
         }
 
diff --git a/user_guide_src/source/changelogs/v4.5.6.rst b/user_guide_src/source/changelogs/v4.5.6.rst
index 75d4d92212b6..1dbaf55f2482 100644
--- a/user_guide_src/source/changelogs/v4.5.6.rst
+++ b/user_guide_src/source/changelogs/v4.5.6.rst
@@ -40,6 +40,7 @@ Bugs Fixed
 - **Routing:** Fixed a TypeError in `str_replace()` when `Routing::$translateURIDashes` is set to `true` and a route is defined using a closure.
 
 - **Validation:** Fixed a bug where complex language strings were not properly handled.
+- **CURLRequest:** Added support for handling proxy responses using HTTP versions other than 1.1.
 
 See the repo's
 `CHANGELOG.md <https://github.com/codeigniter4/CodeIgniter4/blob/develop/CHANGELOG.md>`_


Test patch:
diff --git a/tests/system/HTTP/CURLRequestTest.php b/tests/system/HTTP/CURLRequestTest.php
index f14198be2264..df44c427a665 100644
--- a/tests/system/HTTP/CURLRequestTest.php
+++ b/tests/system/HTTP/CURLRequestTest.php
@@ -831,6 +831,21 @@ public function testSendProxied(): void
         $this->assertSame('Hi there', $response->getBody());
     }
 
+    public function testSendProxiedWithHTTP10(): void
+    {
+        $request = $this->getRequest([
+            'base_uri' => 'http://www.foo.com/api/v1/',
+            'delay'    => 100,
+        ]);
+
+        $output = "HTTP/1.0 200 Connection established
+Proxy-Agent: Fortinet-Proxy/1.0\x0d\x0a\x0d\x0aHTTP/1.1 200 OK\x0d\x0a\x0d\x0aHi there";
+        $request->setOutput($output);
+
+        $response = $request->get('answer');
+        $this->assertSame('Hi there', $response->getBody());
+    }
+
     /**
      * See: https://github.com/codeigniter4/CodeIgniter4/issues/7394
      */