* @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 = '';` 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); } }