addons/proxy/classes/ProxyRequest.php

512 lines
11 KiB
PHP

<?php
/**
* Contao Open Source CMS
* Copyright (C) 2005-2012 Leo Feyer
*
* Formerly known as TYPOlight Open Source CMS.
*
* Proxy Module
*
* PHP version 5
* @copyright Jörg Kleuver 2008, TYPOlight Version
* @author Jörg Kleuver <joerg@kleuver.de>
*
* @copyright Glen Langer 2012
* @author Glen Langer (BugBuster); for Contao 3
* @package Proxy
* @license LGPL
*/
/**
* Class ProxyRequest
*
* Provide methods to handle HTTP request over Proxy.
* Enhance Request class from Leo Feyer with proxy functionality.
* @author Jörg Kleuver
*
* @copyright Glen Langer 2012
* @author Glen Langer (BugBuster); for Contao 3
* @version 3.0.0
* @package Proxy
* @license LGPL
*/
class ProxyRequest
{
/**
* Data to be added to the request
* @var string
*/
protected $strData;
/**
* Request method (defaults to GET)
* @var string
*/
protected $strMethod;
/**
* Error string
* @var string
*/
protected $strError;
/**
* Response code
* @var integer
*/
protected $intCode;
/**
* Response string
* @var string
*/
protected $strResponse;
/**
* Request string
* @var string
*/
protected $strRequest;
/**
* Headers array (these headers will be sent)
* @var array
*/
protected $arrHeaders = array();
/**
* Response headers array (these headers are returned)
* @var array
*/
protected $arrResponseHeaders = array();
/**
* Proxy handle
* @var resource
*/
protected $resProxy;
/**
* The socket for server connection
* @var resource | null
*/
protected $socket = null;
/**
* Set default values
* @throws Exception
*/
public function __construct()
{
$this->strData = '';
$this->strMethod = 'GET';
// check proxy settings
if ($GLOBALS['TL_CONFIG']['useProxy'])
{
$this->resProxy = new Proxy($GLOBALS['TL_CONFIG']['proxy_url'], $GLOBALS['TL_CONFIG']['proxy_local']);
}
}
/**
* Set an object property
* @param string
* @param mixed
* @throws Exception
*/
public function __set($strKey, $varValue)
{
switch ($strKey)
{
case 'data':
$this->strData = $varValue;
break;
case 'method':
$this->strMethod = $varValue;
break;
case 'proxy':
if (is_resource($varValue))
{
$this->resProxy = $varValue;
}
break;
default:
throw new Exception(sprintf('Invalid argument "%s"', $strKey));
break;
}
}
/**
* Return an object property
* @param string
* @return mixed
* @throws Exception
*/
public function __get($strKey)
{
switch ($strKey)
{
case 'error':
return $this->strError;
break;
case 'code':
return $this->intCode;
break;
case 'request':
return $this->strRequest;
break;
case 'response':
return $this->strResponse;
break;
case 'headers':
return $this->arrResponseHeaders;
break;
case 'proxy':
return $this->resProxy;
break;
default:
throw new Exception(sprintf('Unknown or protected property "%s"', $strKey));
break;
}
}
/**
* Set additional request headers
* @param string
* @param mixed
*/
public function setHeader($strKey, $varValue)
{
$this->arrHeaders[$strKey] = $varValue;
}
/**
* Return true if there has been an error
* @return boolean
*/
public function hasError()
{
return strlen($this->strError) ? true : false;
}
/**
* Perform an HTTP request (handle GET, POST, PUT and any other HTTP request)
* @param string
* @param string
* @param string
*/
public function send($strUrl, $strData=false, $strMethod=false)
{
$default = array
(
);
if ($strData)
{
$this->strData = $strData;
$default['Content-Length'] = 'Content-Length: '. strlen($this->strData);
}
if ($strMethod)
{
$this->strMethod = strtoupper($strMethod);
}
$uri = parse_url($strUrl);
switch ($uri['scheme'])
{
case 'http':
$port = isset($uri['port']) ? $uri['port'] : 80;
$host = $uri['host'] . (($port != 80) ? ':' . $port : '');
$secure = false;
break;
case 'https':
$port = isset($uri['port']) ? $uri['port'] : 443;
$host = $uri['host'] . (($port != 443) ? ':' . $port : '');
$secure = true;
break;
default:
$this->strError = 'Invalid schema ' . $uri['scheme'];
return;
break;
}
// Add the user-agent header
if (! isset($this->arrHeaders['User-Agent']))
{
$this->arrHeaders['User-Agent'] = 'Contao (+http://contao.org/)';
}
// Connect to host through proxy or direct
if ($this->resProxy && ! $this->resProxy->isLocal($uri['host']))
{
$this->connect($this->resProxy->host, $this->resProxy->port, false);
if (! is_resource($this->socket))
{
// unable to connect to proxy server
return;
}
// Add Proxy-Authorization header
if ($this->resProxy->user && ! isset($this->arrHeaders['Proxy-Authorization']))
{
$this->arrHeaders['Proxy-Authorization'] = 'Basic '.base64_encode ($this->resProxy->user . ':' . $this->resProxy->pass);
}
// if we are proxying HTTPS, preform CONNECT handshake with the proxy
if ($uri['scheme'] == 'https') {
try
{
@$this->connectHandshake($host, $port);
}
catch (Exception $e)
{
// Close socket
@fclose($this->socket);
$this->strError = $e->getMessage();
}
}
}
else
{
$this->connect($host, $port, $secure);
}
if (! is_resource($this->socket))
{
// unable to connect to host
return;
}
// Build request headers
if ($this->resProxy && $uri['scheme'] != 'https')
{
$request = "{$this->strMethod} {$strUrl} HTTP/1.0\r\n";
} else
{
$path = isset($uri['path']) ? $uri['path'] : '/';
if (isset($uri['query']))
{
$path .= '?' . $uri['query'];
}
$request = "{$this->strMethod} {$path} HTTP/1.0\r\n";
$request .= "Host: {$host} \r\n";
}
// Add all headers to the request string
foreach ($this->arrHeaders as $header=>$value)
{
$default[$header] = $header . ': ' . $value;
}
$request .= implode("\r\n", $default);
// Add the request body
$request .= "\r\n\r\n";
if (strlen($this->strData))
{
$request .= $this->strData . "\r\n";
}
$this->strRequest = $request;
fwrite($this->socket, $request);
$response = '';
while (!feof($this->socket) && ($chunk = fread($this->socket, 1024)) != false)
{
$response .= $chunk;
}
@fclose($this->socket);
list($split, $this->strResponse) = explode("\r\n\r\n", $response, 2);
$split = preg_split("/\r\n|\n|\r/", $split);
$this->arrResponseHeaders = array();
list($protocol, $code, $text) = explode(' ', trim(array_shift($split)), 3);
while (($line = trim(array_shift($split))) != false)
{
list($header, $value) = explode(':', $line, 2);
if (isset($this->arrResponseHeaders[$header]) && $header == 'Set-Cookie')
{
$this->arrResponseHeaders[$header] .= ',' . trim($value);
}
else
{
$this->arrResponseHeaders[$header] = trim($value);
}
}
$responses = array
(
100 => 'Continue',
101 => 'Switching Protocols',
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
203 => 'Non-Authoritative Information',
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Found',
303 => 'See Other',
304 => 'Not Modified',
305 => 'Use Proxy',
307 => 'Temporary Redirect',
400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required',
408 => 'Request Time-out',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Request Entity Too Large',
414 => 'Request-URI Too Large',
415 => 'Unsupported Media Type',
416 => 'Requested range not satisfiable',
417 => 'Expectation Failed',
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Time-out',
505 => 'HTTP Version not supported'
);
if (!isset($responses[$code]))
{
$code = floor($code / 100) * 100;
}
$this->intCode = $code;
if (!in_array(intval($code), array(200, 304)))
{
$this->strError = strlen($text) ? $text : $responses[$code];
}
}
/**
* Connect to the remote server or proxy
* @param string
* @param int
* @param boolean
*/
private function connect($host, $port = 80, $secure = false)
{
if ($secure)
{
$this->socket = @fsockopen('ssl://'.$host, $port, $errno, $errstr, 20);
}
else
{
$this->socket = @fsockopen($host, $port, $errno, $errstr, 15);
}
if (! is_resource($this->socket))
{
$this->strError = trim($errno .' '. $errstr);
}
}
/**
* Preform HTTPS handshaking with proxy using CONNECT method
* @param string $host
* @param integer $port
* @throws Exception
*/
private function connectHandshake($host, $port = 443)
{
$request = "CONNECT $host:$port HTTP/1.0\r\n" . "Host: " . $this->resProxy->host . "\r\n";
// Add the user-agent header
if (isset($this->arrHeaders['User-Agent']))
{
$request .= "User-Agent: " . $this->arrHeaders['User-Agent'] . "\r\n";
}
// If the proxy-authorization header is set, send it to proxy but remove it from headers sent to target host
if (isset($this->arrHeaders['Proxy-Authorization']))
{
$request .= "Proxy-Authorization: " . $this->arrHeaders['Proxy-Authorization'] . "\r\n";
unset($this->arrHeaders['Proxy-Authorization']);
}
$request .= "\r\n";
// Send the request
if (! @fwrite($this->socket, $request))
{
throw new Exception("Error writing request to proxy server");
}
// Read response headers only
$response = '';
$gotStatus = false;
while ($line = @fgets($this->socket))
{
$gotStatus = $gotStatus || (strpos($line, 'HTTP') !== false);
if ($gotStatus)
{
$response .= $line;
if (!chop($line)) break;
}
}
// Check that the response from the proxy is 200
if (substr($response, 9, 3) != 200)
{
throw new Exception("Unable to connect to HTTPS proxy. Server response: " . $response);
}
// If all is good, switch socket to secure mode. We have to fall back
// through the different modes
$modes = array(
STREAM_CRYPTO_METHOD_TLS_CLIENT,
STREAM_CRYPTO_METHOD_SSLv3_CLIENT,
STREAM_CRYPTO_METHOD_SSLv23_CLIENT,
STREAM_CRYPTO_METHOD_SSLv2_CLIENT
);
$success = false;
foreach($modes as $mode)
{
$success = stream_socket_enable_crypto($this->socket, true, $mode);
if ($success) break;
}
if (! $success)
{
throw new Exception("Unable to connect to HTTPS server through proxy: could not negotiate secure connection.");
}
}
}