Proxying with PHP in Ubuntu 14.04 (Apache 2.4, PHP 5.4+)

I’ve just had to evade a Russian block of one of my employer’s sites, let’s call it CasinoX. Presumably they had blocked both www.casinox.com and www.casinox.com’s IP address (which is a Cloud Flare IP btw).

Simply pointing ru.casinox.com to the real IP address of www.casinox.com’s server was a not a viable solution though as that would expose the real IP publicly which is a no-go in the online casino business as it is basically an invitation to be DDoS’ed.

With that in mind the mission looks like this:
1.) Point ru.casinox.com to a server (with an IP that is not blocked) that will forward all traffic to the real server and return the response.
2.) Load images, css, javascript etc (static content) straight from the ru.casinox.com site, forward the rest.
3.) Add a request header with the original IP of the requester.
4.) Use the custom header to log original IP instead of the proxy’s IP in the application logic on the real server.
5.) Replace all instances of www.casinox.com with ru.casinox.com in the body of the reply from the real server that the proxy in turn serves up to the browser (otherwise images etc would be broken as everything from www gets blocked).

For #2 we need to edit the .htaccess file on the proxy:

Options +FollowSymlinks
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !index.php
RewriteRule ^(.+)\.php(.*)$ index.php/$2 [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php/$1 [L]

So we’re first not redirecting any request to index.php as it’s there we will put our custom proxy logic, without the need for that custom logic we could’ve used a much simpler way of proxying, for instance with Apache or Nginx.

Secondly we’re redirecting all request for PHP files to index.php, reason for that is that we need to forward everything that will return dynamic content and that is the whole point of PHP scripts.

The rest of the lines are standard, redirect everything that doesn’t look like a file to index.php.

To go along with that htaccess file we need the following virtual hosts file:

<VirtualHost *:443>
        ServerName ru.casinox.com
        DocumentRoot /var/www/ru/
        SSLEngine on
        SSLCertificateFile    /etc/ssl/certs/geotrust.crt
        SSLCertificateKeyFile /etc/ssl/private/geotrust.key
        SSLCACertificateFile /etc/ssl/certs/geotrust_inter.crt

        php_value enable_post_data_reading Off

  <Directory /var/www/ru/>
     Options Indexes FollowSymLinks
     AllowOverride All
     Order allow,deny
     allow from all
  </Directory>
</VirtualHost>

<VirtualHost *:80>
        ServerName ru.casinox.com
        DocumentRoot /var/www/ru
        Redirect / https://ru.casinox.com/
</VirtualHost>

There is one line here that is extremely important and that is php_value enable_post_data_reading Off, properly proxying with PHP would be impossible without it as we would be unable to get at the original HTTP body without it. If it is turned on (default), the body will have been erased. More on this later.

Furthermore we’re directing all non-encrypted traffic to the encrypted version.

Let’s take a look at our index.php:

require_once '/opt/lib/php-proxy/proxy.php';
$proxy = new Proxy(array('timeout' => 10));
$server = "www.casinox.loc";
$body = $proxy->forward($server, '', array("The-Original-Ip" => $_SERVER['REMOTE_ADDR']));
echo str_replace($server, 'ru.casinox.loc', $body);

Here you can see the original IP being passed along in a new header and a simple str_replace of all instances of www with ru in the modified body we will return to the browser.

The first require statement brings me to the proxy script that is doing all the magic, I have no clue who the original author is but it’s working fine for me with a few modifications:

class Proxy {
  protected $ch;
  protected $config = array();
  function __construct($config = array()){
    if (!count($config)) die("Please provide a valid configuration");
    if(empty($config['http_port']))
      $config['http_port'] = 80;
    if(empty($config['https_port']))
      $config['https_port'] = 443;
    if(empty($config['timeout']))
      $config['timeout'] = 5;
    $this->config = $config;
    $this->ch = curl_init();
    curl_setopt($this->ch, CURLOPT_FOLLOWLOCATION, false);
    curl_setopt($this->ch, CURLOPT_MAXREDIRS, 10);
    curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($this->ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($this->ch, CURLOPT_SSL_VERIFYHOST, false);
    curl_setopt($this->ch, CURLOPT_HEADER, true);
    curl_setopt($this->ch, CURLOPT_TIMEOUT, $this->config["timeout"]);
  }
  
  public function forward($server, $url = '', $extra_forward_headers = array()){
    $url = empty($url) ? $_SERVER['REQUEST_URI'] : $url;
    $this->config['server'] = $server;
    if (isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] == "on")
      $url = "https://" . $server . ":" . $this->config["https_port"] . "/" . ltrim($url, "/");
    else 
      $url = "http://" . $server . ":" . $this->config["http_port"] . "/" . ltrim($url, "/");
    curl_setopt($this->ch, CURLOPT_URL, $url);
    $headers = $this->get_request_headers();
    $this->set_request_headers(array_merge($headers, $extra_forward_headers));
    
    if ($_SERVER["REQUEST_METHOD"] == "POST"){
	  //This wouldn't work for file uploads without php_value enable_post_data_reading Off
	  $body = file_get_contents('php://input');
	  $this->set_post($body);
    }elseif ($_SERVER["REQUEST_METHOD"] == "HEAD") 
      curl_setopt($this->ch, CURLOPT_NOBODY, true);
    
    $data = curl_exec($this->ch);
    $info = curl_getinfo($this->ch);
    $body = $info["size_download"] ? substr($data, $info["header_size"], $info["size_download"]) : "";
    $headers = substr($data, 0, $info["header_size"]);
    $this->set_response_headers($headers);
    curl_close($this->ch);
    echo $body;
  }
  
  function pickMachine($uid, $url = ''){
    $this->forward($this->config['machines'][$uid % count($this->config['machines'])], $url);
  }
  
  protected function get_content_type( $headers ){
    foreach( $headers as $name => $value ){
      if( 'content-type' == strtolower($name) ){
        $parts = explode(';', $value);
        return strtolower($parts[0]);
      }
    }
    return null;
  }
  
  protected function get_request_headers(){
    if (function_exists('getallheaders')) return getallheaders();
    $headers = '';
    foreach ($_SERVER as $name => $value){
      if(substr($name, 0, 5) == 'HTTP_')            
        $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;            
    }
    return $headers;
  }

  protected function set_request_headers($request){
    $strip = array("Content-Length", "Host");
    $headers = array();
    foreach ($request as $key => $value){
      if ($key && !in_array($key, $strip))
        $headers[] = "$key: $value";
    }
    curl_setopt($this->ch, CURLOPT_HTTPHEADER, $headers);
  }
  
  protected function set_response_headers($response){
    $strip = array("Transfer-Encoding");
    $headers = explode("\n", $response);
    foreach ($headers as &$header){
      if (!$header) continue;
      $pos = strpos($header, ":");
      $key = substr($header, 0, $pos);
      if (strtolower($key) == "location"){
        $base_url = $_SERVER["HTTP_HOST"];
        $base_url .= rtrim(str_replace(basename($_SERVER["SCRIPT_NAME"]), "", $_SERVER["SCRIPT_NAME"]), "/");
        $header = str_replace(":" . $this->config["http_port"], "", $header);
        $header = str_replace(":" . $this->config["https_port"], "", $header);
        $header = str_replace($this->config["server"], $base_url, $header);
      }
      
      if (!in_array($key, $strip))
        header($header, FALSE);
    }
  }
  
  protected function set_post($post){
    curl_setopt($this->ch, CURLOPT_POST, 1);
    curl_setopt($this->ch, CURLOPT_POSTFIELDS, $post);
  }
}

Note my comment in the middle there, file_get_contents(‘php://input’) wouldn’t work for file uploads if it wasn’t for the fact that we’ve set enable_post_data_reading to Off.

Finally we use our header like this to get the real IP of the requester:

function ip_in_range( $ip, $range ) {
  if ( strpos( $range, '/' ) == false ) 
    $range .= '/32';
  list( $range, $netmask ) = explode( '/', $range, 2 );
  $range_decimal = ip2long( $range );
  $ip_decimal = ip2long( $ip );
  $wildcard_decimal = pow( 2, ( 32 - $netmask ) ) - 1;
  $netmask_decimal = ~ $wildcard_decimal;
  return ( ( $ip_decimal & $netmask_decimal ) == ( $range_decimal & $netmask_decimal ) );
}

function remIp(){
  if(isset($_SERVER["HTTP_THE_ORIGINAL_IP"]))
    return $_SERVER["HTTP_THE_ORIGINAL_IP"];
  
  if(!isset($_SERVER["HTTP_CF_CONNECTING_IP"]))
    return $_SERVER['REMOTE_ADDR'];
  
  $cf_ip_ranges = array('204.93.240.0/24','204.93.177.0/24','199.27.128.0/21','173.245.48.0/20','103.21.244.0/22','103.22.200.0/22','103.31.4.0/22','141.101.64.0/18','108.162.192.0/18','190.93.240.0/20','188.114.96.0/20','197.234.240.0/22','198.41.128.0/17','162.158.0.0/15');
  
  foreach($cf_ip_ranges as $range) {
    if (ip_in_range($_SERVER['REMOTE_ADDR'], $range))
      return $_SERVER['HTTP_CF_CONNECTING_IP'];
  }

  return $_SERVER['REMOTE_ADDR'];
}

In real life the custom header is not The-Original-Ip of course, it’s something which is not possible to guess, otherwise IP spoofing would be very easy for people who can access the www version directly.

And that’s that, an example of how to code up our own custom proxy with PHP!

Related Posts

Tags: , ,