A minimal reverse proxy using PHP & cURL

A reverse proxy acts as an intermediary between a client and one or more servers. Requests sent by the client are received by the proxy and passed on to one of the servers in the background. There are many scenarios in which such a setup might be useful. For example, reverse proxies can be used to transparently distribute the load from incoming requests to several servers.

There are many different ways in which a reverse proxy could be implemented. Specialized web servers like Nginx are obviously a good choice, but circumstances might put constraints on the choice of tools. Luckily, implementing a reverse proxy is possible in just about any programming language. After all, everything that is required is being able to receive HTTP requests and pass them on in a slightly modified form. Turns out that even PHP can do it! 😉

<?php

// Define getallheaders() in case that it doesn't already exist (e.g. Nginx, PHP-FPM, FastCGI)
// Taken from https://www.php.net/manual/en/function.getallheaders.php#84262
if (!function_exists('getallheaders')) { 
    function getallheaders() { 
       $headers = array (); 
       foreach ($_SERVER as $name => $value) { 
           if (substr($name, 0, 5) == 'HTTP_') { 
               $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; 
           } 
       } 
       return $headers; 
    } 
} 

function reformat($headers) {
    foreach ($headers as $name => $value) {
        yield "$name: $value";
    }
}

// Configuration parameters
$proxied_url = 'https://www.example.com';
$proxied_host = parse_url($proxied_url)['host'];

$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);

// HTTP messages consist of a request line such as 'GET https://example.com/asdf HTTP/1.1'…
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $_SERVER['REQUEST_METHOD']);
curl_setopt($ch, CURLOPT_URL, $proxied_url . $_SERVER['REQUEST_URI']);

// … a set of header fields…
$request_headers = getallheaders();
$request_headers['Host'] = $proxied_host;
$request_headers['X-Forwarded-Host'] = $_SERVER['SERVER_NAME'];
$request_headers = iterator_to_array(reformat($request_headers));
curl_setopt($ch, CURLOPT_HTTPHEADER, $request_headers);

// … and a message body.
$request_body = file_get_contents('php://input');
curl_setopt($ch, CURLOPT_POSTFIELDS, $request_body);

// Retrieve response headers in the same request as the body
// Taken from https://stackoverflow.com/a/41135574/3144403
$response_headers = [];
curl_setopt($ch, CURLOPT_HEADERFUNCTION,
    function($curl, $header) use (&amp;$response_headers) {
        $len = strlen($header);
        $header = explode(':', $header, 2);
        if (count($header) < 2) // ignore invalid headers
          return $len;

        $response_headers[strtolower(trim($header[0]))][] = trim($header[1]);

        return $len;
    }
);

$response_body = curl_exec($ch);
$response_code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

// Set the appropriate response status code &amp; headers
http_response_code($response_code);
foreach($response_headers as $name => $values)
    foreach($values as $value)
        header("$name: $value", false);

echo $response_body;