512 lines
11 KiB
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.");
|
|
}
|
|
}
|
|
|
|
}
|
|
|