forked from urvishpatelce/lxd-app
471 lines
14 KiB
PHP
471 lines
14 KiB
PHP
<?php
|
|
|
|
namespace Zounar\PHPProxy;
|
|
|
|
use CURLFile;
|
|
use Exception;
|
|
use RuntimeException;
|
|
|
|
/**
|
|
* @author Robin Zounar <https://github.com/zounar>
|
|
* @license http://unlicense.org
|
|
* @package Zounar\PHPProxy
|
|
*
|
|
* Credits to:
|
|
* https://github.com/cowboy/php-simple-proxy/
|
|
* https://gist.github.com/iovar
|
|
*
|
|
* Usage:
|
|
* To call this script two headers must be sent
|
|
* HTTP_PROXY_AUTH Access key for the proxy (should be changed)
|
|
* HTTP_PROXY_TARGET_URL URL to be called by this script
|
|
*
|
|
* Debug:
|
|
* To debug, send HTTP_PROXY_DEBUG header with any non-zero value
|
|
*
|
|
* Compatibility:
|
|
* PHP >=5.6
|
|
* libcurl
|
|
* gzip
|
|
* PHP safe_mode disabled
|
|
*/
|
|
class Proxy
|
|
{
|
|
/**
|
|
* Your private auth key. It is recommended to change it.
|
|
* If you installed the package via composer, call `Proxy::$AUTH_KEY = '<your-new-key>';` before running the proxy.
|
|
* If you copied this file, change the value here in place.
|
|
* @var string
|
|
*/
|
|
public static $AUTH_KEY = 'Bj5pnZEX6DkcG6Nz6AjDUT1bvcGRVhRaXDuKDX9CjsEs2';
|
|
|
|
/**
|
|
* Set this to false to disable authorization. Useful for debugging, not recommended in production.
|
|
* @var bool
|
|
*/
|
|
public static $ENABLE_AUTH = true;
|
|
|
|
/**
|
|
* If true, PHP safe mode compatibility will not be checked
|
|
* (you may not need it if no POST files are sent over proxy)
|
|
* @var bool
|
|
*/
|
|
public static $IGNORE_SAFE_MODE = false;
|
|
|
|
/**
|
|
* Enable debug mode (you can do it by sending Proxy-Debug header as well).
|
|
* This value overrides any value specified in Proxy-Debug header.
|
|
* @var bool
|
|
*/
|
|
public static $DEBUG = false;
|
|
|
|
/**
|
|
* When set to false the fetched header is not included in the result
|
|
* @var bool
|
|
*/
|
|
public static $CURLOPT_HEADER = true;
|
|
|
|
/**
|
|
* When set to false the fetched result is echoed immediately instead of waiting for the fetch to complete first
|
|
* @var bool
|
|
*/
|
|
public static $CURLOPT_RETURNTRANSFER = true;
|
|
|
|
/**
|
|
* Target URL is set via Proxy-Target-URL header. For debugging purposes you might set it directly here.
|
|
* This value overrides any value specified in Proxy-Target-URL header.
|
|
* @var string
|
|
*/
|
|
public static $TARGET_URL = '';
|
|
|
|
/**
|
|
* Name of remote debug header
|
|
* @var string
|
|
*/
|
|
public static $HEADER_HTTP_PROXY_DEBUG = 'HTTP_PROXY_DEBUG';
|
|
|
|
/**
|
|
* Name of the proxy auth key header
|
|
* @var string
|
|
*/
|
|
public static $HEADER_HTTP_PROXY_AUTH = 'HTTP_PROXY_AUTH';
|
|
|
|
/**
|
|
* Name of the target url header
|
|
* @var string
|
|
*/
|
|
public static $HEADER_HTTP_PROXY_TARGET_URL = 'HTTP_PROXY_TARGET_URL';
|
|
|
|
/**
|
|
* Line break for debug purposes
|
|
* @var string
|
|
*/
|
|
protected static $HR = PHP_EOL . PHP_EOL . '----------------------------------------------' . PHP_EOL . PHP_EOL;
|
|
|
|
/**
|
|
* @return string[]
|
|
*/
|
|
protected static function getSkippedHeaders()
|
|
{
|
|
return [
|
|
static::$HEADER_HTTP_PROXY_TARGET_URL,
|
|
static::$HEADER_HTTP_PROXY_AUTH,
|
|
static::$HEADER_HTTP_PROXY_DEBUG,
|
|
'HTTP_HOST',
|
|
'HTTP_ACCEPT_ENCODING'
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Return variable or default value if not set
|
|
* @param mixed $variable
|
|
* @param mixed|null $default
|
|
* @return mixed
|
|
* @noinspection PhpParameterByRefIsNotUsedAsReferenceInspection
|
|
*/
|
|
protected static function ri(&$variable, $default = null)
|
|
{
|
|
if (isset($variable)) {
|
|
return $variable;
|
|
} else {
|
|
return $default;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string $message
|
|
*/
|
|
protected static function exitWithError($message)
|
|
{
|
|
http_response_code(500);
|
|
echo 'PROXY ERROR: ' . $message;
|
|
exit(500);
|
|
}
|
|
|
|
/**
|
|
* @return bool
|
|
*/
|
|
public static function isInstalledWithComposer()
|
|
{
|
|
$autoloaderPath = join(DIRECTORY_SEPARATOR, [dirname(dirname(__DIR__)), 'autoload.php']);
|
|
return is_readable($autoloaderPath);
|
|
}
|
|
|
|
/**
|
|
* @return void
|
|
*/
|
|
public static function registerErrorHandlers()
|
|
{
|
|
set_error_handler(function ($code, $message, $file, $line) {
|
|
Proxy::exitWithError("($code) $message in $file at line $line");
|
|
}, E_ALL);
|
|
|
|
set_exception_handler(function (Exception $ex) {
|
|
Proxy::exitWithError("{$ex->getMessage()} in {$ex->getFile()} at line {$ex->getLine()}");
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @return void
|
|
*/
|
|
public static function checkCompatibility()
|
|
{
|
|
if (!static::$IGNORE_SAFE_MODE && function_exists('ini_get') && ini_get('safe_mode')) {
|
|
throw new RuntimeException('Safe mode is enabled, this may cause problems with uploading files');
|
|
}
|
|
|
|
if (!function_exists('curl_init')) {
|
|
throw new RuntimeException('libcurl is not installed on this server');
|
|
}
|
|
|
|
if (!function_exists('gzdecode')) {
|
|
throw new RuntimeException('gzip is not installed on this server');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return bool
|
|
*/
|
|
protected static function hasCURLFileSupport()
|
|
{
|
|
return class_exists('CURLFile');
|
|
}
|
|
|
|
/**
|
|
* @param string $headerString
|
|
* @return string[]
|
|
*/
|
|
protected static function splitResponseHeaders($headerString)
|
|
{
|
|
$results = [];
|
|
$headerLines = preg_split('/[\r\n]+/', $headerString);
|
|
foreach ($headerLines as $headerLine) {
|
|
if (empty($headerLine)) {
|
|
continue;
|
|
}
|
|
|
|
// Header contains HTTP version specification and path
|
|
if (strpos($headerLine, 'HTTP/') === 0) {
|
|
// Reset the output array as there may by multiple response headers
|
|
$results = [];
|
|
continue;
|
|
}
|
|
|
|
$results[] = "$headerLine";
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Returns true if response code matches 2xx or 3xx
|
|
* @param int $responseCode
|
|
* @return bool
|
|
*/
|
|
public static function isResponseCodeOk($responseCode)
|
|
{
|
|
return preg_match('/^[23]\d\d$/', $responseCode) === 1;
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
protected static function getTargetUrl()
|
|
{
|
|
if (!empty(static::$TARGET_URL)) {
|
|
$targetURL = static::$TARGET_URL;
|
|
} else {
|
|
$targetURL = static::ri($_SERVER[static::$HEADER_HTTP_PROXY_TARGET_URL]);
|
|
}
|
|
|
|
if (empty($targetURL)) {
|
|
throw new RuntimeException(static::$HEADER_HTTP_PROXY_TARGET_URL . ' header is empty');
|
|
}
|
|
|
|
if (filter_var($targetURL, FILTER_VALIDATE_URL) === false) {
|
|
throw new RuntimeException(static::$HEADER_HTTP_PROXY_TARGET_URL . ' "' . $targetURL . '" is invalid');
|
|
}
|
|
|
|
return $targetURL;
|
|
}
|
|
|
|
/**
|
|
* @return bool
|
|
*/
|
|
protected static function isDebug()
|
|
{
|
|
return static::$DEBUG || !empty($_SERVER[static::$HEADER_HTTP_PROXY_DEBUG]);
|
|
}
|
|
|
|
/**
|
|
* @return bool
|
|
*/
|
|
protected static function isAuthenticated()
|
|
{
|
|
return !static::$ENABLE_AUTH || static::ri($_SERVER[static::$HEADER_HTTP_PROXY_AUTH]) === static::$AUTH_KEY;
|
|
}
|
|
|
|
/**
|
|
* @param string[] $skippedHeaders
|
|
* @return string[]
|
|
*/
|
|
protected static function getIncomingRequestHeaders($skippedHeaders = [])
|
|
{
|
|
$results = [];
|
|
foreach ($_SERVER as $key => $value) {
|
|
if (in_array($key, $skippedHeaders)) {
|
|
continue;
|
|
}
|
|
|
|
$loweredKey = strtolower($key);
|
|
if (strpos($loweredKey, 'http_') === 0) {
|
|
// Remove prefix
|
|
$key = substr($loweredKey, strlen('http_'));
|
|
// Replace underscores with dashes
|
|
$key = str_replace('_', '-', $key);
|
|
// Capital each word
|
|
$key = ucwords($key, '-');
|
|
|
|
$results[] = "$key: $value";
|
|
}
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* @param string $targetURL
|
|
* @return false|resource
|
|
*/
|
|
protected static function createRequest($targetURL)
|
|
{
|
|
$request = curl_init($targetURL);
|
|
|
|
// Set input data
|
|
$requestMethod = strtoupper(static::ri($_SERVER['REQUEST_METHOD']));
|
|
if ($requestMethod === "PUT" || $requestMethod === "PATCH") {
|
|
curl_setopt($request, CURLOPT_POSTFIELDS, file_get_contents('php://input'));
|
|
} elseif ($requestMethod === "POST") {
|
|
$data = array();
|
|
|
|
if (!empty($_FILES)) {
|
|
if (!static::hasCURLFileSupport()) {
|
|
curl_setopt($request, CURLOPT_SAFE_UPLOAD, false);
|
|
}
|
|
|
|
foreach ($_FILES as $fileName => $file) {
|
|
$filePath = realpath($file['tmp_name']);
|
|
|
|
if (static::hasCURLFileSupport()) {
|
|
$data[$fileName] = new CURLFile($filePath, $file['type'], $file['name']);
|
|
} else {
|
|
$data[$fileName] = '@' . $filePath;
|
|
}
|
|
}
|
|
}
|
|
|
|
curl_setopt($request, CURLOPT_POSTFIELDS, $data + $_POST);
|
|
}
|
|
|
|
$headers = static::getIncomingRequestHeaders(static::getSkippedHeaders());
|
|
|
|
curl_setopt_array($request, [
|
|
CURLOPT_FOLLOWLOCATION => true,
|
|
CURLOPT_HEADER => static::$CURLOPT_HEADER,
|
|
CURLOPT_RETURNTRANSFER => static::$CURLOPT_RETURNTRANSFER,
|
|
CURLINFO_HEADER_OUT => true,
|
|
CURLOPT_HTTPHEADER => $headers
|
|
]);
|
|
|
|
return $request;
|
|
}
|
|
|
|
/**
|
|
* @return int HTTP response code (200, 404, 500, etc.)
|
|
*/
|
|
public static function run()
|
|
{
|
|
if (!static::isAuthenticated()) {
|
|
throw new RuntimeException(static::$HEADER_HTTP_PROXY_AUTH . ' header is invalid');
|
|
}
|
|
|
|
$debug = static::isDebug();
|
|
$targetURL = static::getTargetUrl();
|
|
|
|
$request = static::createRequest($targetURL);
|
|
|
|
// Get response
|
|
$response = curl_exec($request);
|
|
|
|
$headerSize = curl_getinfo($request, CURLINFO_HEADER_SIZE);
|
|
$responseHeader = substr($response, 0, $headerSize);
|
|
$responseBody = substr($response, $headerSize);
|
|
$responseInfo = curl_getinfo($request);
|
|
$responseCode = static::ri($responseInfo['http_code'], 500);
|
|
$redirectCount = static::ri($responseInfo['redirect_count'], 0);
|
|
$requestHeaders = preg_split('/[\r\n]+/', static::ri($responseInfo['request_header'], ''));
|
|
if ($responseCode === 0) {
|
|
$responseCode = 404;
|
|
}
|
|
|
|
$finalRequestURL = curl_getinfo($request, CURLINFO_EFFECTIVE_URL);
|
|
if ($redirectCount > 0 && !empty($finalRequestURL)) {
|
|
$finalRequestURLParts = parse_url($finalRequestURL);
|
|
$effectiveURL = static::ri($finalRequestURLParts['scheme'], 'http') . '://' .
|
|
static::ri($finalRequestURLParts['host']) . static::ri($finalRequestURLParts['path'], '');
|
|
}
|
|
|
|
curl_close($request);
|
|
|
|
//----------------------------------
|
|
|
|
// Split header text into an array.
|
|
$responseHeaders = static::splitResponseHeaders($responseHeader);
|
|
// Pass headers to output
|
|
foreach ($responseHeaders as $header) {
|
|
$headerParts = preg_split('/:\s+/', $header, 2);
|
|
if (count($headerParts) !== 2) {
|
|
throw new RuntimeException("Can not parse header \"$header\"");
|
|
}
|
|
|
|
$headerName = $headerParts[0];
|
|
$loweredHeaderName = strtolower($headerName);
|
|
|
|
$headerValue = $headerParts[1];
|
|
$loweredHeaderValue = strtolower($headerValue);
|
|
|
|
// Pass following headers to response
|
|
if (in_array($loweredHeaderName,
|
|
['content-type', 'content-language', 'content-security', 'server'])) {
|
|
header("$headerName: $headerValue");
|
|
} elseif (strpos($loweredHeaderName, 'x-') === 0) {
|
|
header("$headerName: $headerValue");
|
|
} // Replace cookie domain and path
|
|
elseif ($loweredHeaderName === 'set-cookie') {
|
|
$newValue = preg_replace('/((?>domain)\s*=\s*)[^;\s]+/', '\1.' . $_SERVER['HTTP_HOST'], $headerValue);
|
|
$newValue = preg_replace('/\s*;?\s*path\s*=\s*[^;\s]+/', '', $newValue);
|
|
header("$headerName: $newValue", false);
|
|
} // Decode response body if gzip encoding is used
|
|
elseif ($loweredHeaderName === 'content-encoding' && $loweredHeaderValue === 'gzip') {
|
|
$responseBody = gzdecode($responseBody);
|
|
}
|
|
}
|
|
|
|
http_response_code($responseCode);
|
|
|
|
//----------------------------------
|
|
|
|
if ($debug) {
|
|
echo 'Headers sent to proxy' . PHP_EOL . PHP_EOL;
|
|
echo implode(PHP_EOL, static::getIncomingRequestHeaders());
|
|
echo static::$HR;
|
|
|
|
if (!empty($_GET)) {
|
|
echo '$_GET sent to proxy' . PHP_EOL . PHP_EOL;
|
|
print_r($_GET);
|
|
echo static::$HR;
|
|
}
|
|
|
|
if (!empty($_POST)) {
|
|
echo '$_POST sent to proxy' . PHP_EOL . PHP_EOL;
|
|
print_r($_POST);
|
|
echo static::$HR;
|
|
}
|
|
|
|
echo 'Headers sent to target' . PHP_EOL . PHP_EOL;
|
|
echo implode(PHP_EOL, $requestHeaders);
|
|
echo static::$HR;
|
|
|
|
if (isset($effectiveURL) && $effectiveURL !== $targetURL) {
|
|
echo "Request was redirected from \"$targetURL\" to \"$effectiveURL\"";
|
|
echo static::$HR;
|
|
}
|
|
|
|
echo 'Headers received from target' . PHP_EOL . PHP_EOL;
|
|
echo $responseHeader;
|
|
echo static::$HR;
|
|
|
|
echo 'Headers sent from proxy to client' . PHP_EOL . PHP_EOL;
|
|
echo implode(PHP_EOL, headers_list());
|
|
echo static::$HR;
|
|
|
|
echo 'Body sent from proxy to client' . PHP_EOL . PHP_EOL;
|
|
}
|
|
|
|
echo $responseBody;
|
|
return $responseCode;
|
|
}
|
|
}
|
|
|
|
if (!Proxy::isInstalledWithComposer()) {
|
|
Proxy::checkCompatibility();
|
|
Proxy::registerErrorHandlers();
|
|
$responseCode = Proxy::run();
|
|
|
|
if (Proxy::isResponseCodeOk($responseCode)) {
|
|
exit(0);
|
|
} else {
|
|
exit($responseCode);
|
|
}
|
|
}
|