7.23.2007

Reverse Proxy in PHP5, Rev2

It's gotten a bit more complex; The proxy handler didn't pass all the client headers to the proxy server. This caused problems with having the wrong client type, no Etag caching, cookie passing, etc. Here's the current rev, which solves a lot of these issues.

The cookie handling was broken because I wasn't using cookies on my back-end app. My SSO implementation was caching the cookies to the back-end server in the session.

So, here you go!


<?php

class ProxyHandler
{
private
$url;
private
$proxy_url;
private
$proxy_host;
private
$proxy_proto;
private
$translated_url;
private
$curl_handler;
private
$cache_control=false;
private
$pragma=false;
private
$client_headers=array();

function
__construct($url, $proxy_url)
{
// Strip the trailing '/' from the URLs so they are the same.
$this->url = preg_replace(',/$,','',$url);
$this->proxy_url = preg_replace(',/$,','',$proxy_url);

// Parse all the parameters for the URL
if (isset($_SERVER['PATH_INFO']))
{
$proxy_url .= $_SERVER['PATH_INFO'];
}
else
{
// Add the '/' at the end
$proxy_url .= '/';
}

if (
$_SERVER['QUERY_STRING'] !== '')
{
$proxy_url .= "?{$_SERVER['QUERY_STRING']}";
}

$this->translated_url = $proxy_url;

$this->curl_handler = curl_init($this->translated_url);

// Set various options
$this->setCurlOption(CURLOPT_RETURNTRANSFER, true);
$this->setCurlOption(CURLOPT_BINARYTRANSFER, true); // For images, etc.
$this->setCurlOption(CURLOPT_USERAGENT,$_SERVER['HTTP_USER_AGENT']);
$this->setCurlOption(CURLOPT_WRITEFUNCTION, array($this,'readResponse'));
$this->setCurlOption(CURLOPT_HEADERFUNCTION, array($this,'readHeaders'));

// Process post data.
if (count($_POST))
{
// Empty the post data
$post=array();

// Set the post data
$this->setCurlOption(CURLOPT_POST, true);

// Encode and form the post data
foreach($_POST as $key=>$value)
{
$post[] = urlencode($key)."=".urlencode($value);
}

$this->setCurlOption(CURLOPT_POSTFIELDS, implode('&',$post));

unset(
$post);
}
elseif (
$_SERVER['REQUEST_METHOD'] !== 'GET') // Default request method is 'get'
{
// Set the request method
$this->setCurlOption(CURLOPT_CUSTOMREQUEST, $_SERVER['REQUEST_METHOD']);
}

// Handle the client headers.
$this->handleClientHeaders();

}

public function
setClientHeader($header)
{
$this->client_headers[] = $header;
}

// Executes the proxy.
public function execute()
{
$this->setCurlOption(CURLOPT_HTTPHEADER, $this->client_headers);
curl_exec($this->curl_handler);
}

// Get the information about the request.
// Should not be called before exec.
public function getCurlInfo()
{
return
curl_getinfo($this->curl_handler);
}

// Sets a curl option.
public function setCurlOption($option, $value)
{
curl_setopt($this->curl_handler, $option, $value);
}

protected function
readHeaders(&$cu, $string)
{
$length = strlen($string);
if (
preg_match(',^Location:,', $string))
{
$string = str_replace($this->proxy_url, $this->url, $string);
}
elseif(
preg_match(',^Cache-Control:,', $string))
{
$this->cache_control = true;
}
elseif(
preg_match(',^Pragma:,', $string))
{
$this->pragma = true;
}
if (
header !== "\r\n")
{
header(rtrim($string));

}
return
$length;
}

protected function
handleClientHeaders()
{
$headers = apache_request_headers();

foreach (
$headers as $header => $value) {
switch(
$header)
{
case
'Host':
break;
default:
$this->setClientHeader(sprintf('%s: %s', $header, $value));
break;
}
}
}

protected function
readResponse(&$cu, $string)
{
static
$headersParsed = false;

// Clear the Cache-Control and Pragma headers
// if they aren't passed from the proxy application.
if ($headersParsed === false)
{
if (!
$this->cache_control)
{
header('Cache-Control: ');
}
if (!
$this->pragma)
{
header('Pragma: ');
}
$headersParsed = true;
}
$length = strlen($string);
echo
$string;
return
$length;
}
}

?>






Update: Added a google code project for php5rp at Google Code and here's the Subversion Link for downloading.

7.17.2007

Writing A Reverse Proxy in PHP5

So I have been working on a little class to run a reverse proxy from PHP using cURL. I have extended this class for my own purposes (single-sign-on) to handle some special request parameters, but here it is. It has some warts, but it's a good starting point. I would appreciate any pointers anyone has to offer.


<?php

class ProxyHandler
{
private $url;
private $translated_url;
private $curl_handler;

function __construct($url, $proxy_url)
{
$this->url = $url;
$this->proxy_url = $proxy_url;

// Parse all the parameters for the URL
if (isset($_SERVER['PATH_INFO']))
{
$proxy_url .= $_SERVER['PATH_INFO'];
}
else
{
$proxy_url .= '/';
}

if ($_SERVER['QUERY_STRING'] !== '')
{
$proxy_url .= "?{$_SERVER['QUERY_STRING']}";
}

$this->translated_url = $proxy_url;

$this->curl_handler = curl_init($proxy_url);

// Set various options
$this->setCurlOption(CURLOPT_RETURNTRANSFER, true);
$this->setCurlOption(CURLOPT_BINARYTRANSFER, true); // For images, etc.
$this->setCurlOption(CURLOPT_USERAGENT,$_SERVER['HTTP_USER_AGENT']);
$this->setCurlOption(CURLOPT_WRITEFUNCTION, array($this,'readResponse'));
$this->setCurlOption(CURLOPT_HEADERFUNCTION, array($this,'readHeaders'));

// Process post data.
if (count($_POST))
{
// Empty the post data
$post=array();

// Set the post data
$this->setCurlOption(CURLOPT_POST, true);

// Encode and form the post data
foreach($_POST as $key=>$value)
{
$post[] = urlencode($key)."=".urlencode($value);
}

$this->setCurlOption(CURLOPT_POSTFIELDS, implode('&',$post));

unset($post);
}
elseif ($_SERVER['REQUEST_METHOD'] !== 'GET') // Default request method is 'get'
{
// Set the request method
$this->setCurlOption(CURLOPT_CUSTOMREQUEST, $_SERVER['REQUEST_METHOD']);
}

}

// Executes the proxy.
public function execute()
{
curl_exec($this->curl_handler);
}

// Get the information about the request.
// Should not be called before exec.
public function getCurlInfo()
{
return curl_getinfo($this->curl_handler);
}

// Sets a curl option.
public function setCurlOption($option, $value)
{
curl_setopt($this->curl_handler, $option, $value);
}

protected function readHeaders(&$cu, $string)
{
$length = strlen($string);
if (preg_match(',^Location:,', $string))
{
$string = str_replace($this->proxy_url, $this->url, $string);
}
header($string);
return $length;
}

protected function readResponse(&$cu, $string)
{
$length = strlen($string);
echo $string;
return $length;
}
}
?>


And here's an example .htaccess file:


RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f [NC]
RewriteCond %{REQUEST_FILENAME} !-d [NC]
RewriteCond %{REQUEST_URI} !^/index.php
RewriteRule ^(.+)$ index.php/$1 [QSA]


And an example usage:


$proxy = new ProxyHandler('http://publicsite.example.com','http://privatesite.example.com');
$proxy->execute();



Cheers.