Last active
November 18, 2018 23:27
-
-
Save samunders-core/e067462496bb61d29e2ca05f1f379369 to your computer and use it in GitHub Desktop.
URL-rewriting PHP proxy, works with Apache's userdir module
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
function debug_exit($msg) { | |
echo ">{$msg}<"; | |
exit(200); | |
} | |
function readFtpLine($line, $s) { | |
if ("\r" == substr($line, -1)) { | |
socket_read($s, strlen("\n"), PHP_NORMAL_READ); | |
} | |
return SENTINEL; | |
} | |
function startFtpTransfer($s, $host, $resource) { | |
forEachBlockRead($s, 1024, PHP_NORMAL_READ, 'readFtpLine'); | |
# read: 220 welcome message | |
# login: USER anonymous | |
# read: 331 pwd required | |
# pwd: PASS [email protected] | |
# read: 230 logged in | |
# passive: PASV | |
# read: 227 complied | |
# get/post file: | |
return FALSE; | |
} | |
function writeReconstructedRequest($s, $protocol, $host, $resource) { | |
if ("ftp://" === substr($protocol, 0, strlen("ftp://"))) { | |
return startFtpTransfer($s, $host, $resource); | |
} | |
$verb = $_SERVER['REQUEST_METHOD']; | |
$line = "{$verb} {$resource} {$_SERVER['SERVER_PROTOCOL']}\r\n"; | |
if (FALSE === socket_write($s, $line, strlen($line))) { | |
return FALSE; | |
} | |
$heads = " PHP_AUTH_USER PHP_AUTH_PW CONTENT_TYPE CONTENT_LENGTH"; | |
foreach ($_SERVER as $key=>$value) { | |
if ("HTTP_" === substr($key, 0, strlen("HTTP_")) && "HTTP_HOST" !== $key) { | |
$header = substr($key, strlen("HTTP_")); | |
if ("CONNECTION" === $header) { | |
$value = "close"; // since we'll close socket anyway + don't have to keep track of read amount | |
} else if ("REFERER" === $header) { | |
$pattern = preg_quote("{$_SERVER['HTTP_HOST']}{$_SERVER['SCRIPT_NAME']}/", "/"); | |
$value = preg_replace("/{$pattern}/", "", $value); | |
// } else if ("USER_AGENT" === $header) { | |
// continue; | |
} | |
} elseif (FALSE === strpos($heads, $key)) { | |
continue; | |
} elseif ("PHP_AUTH_" === substr($key, 0, strlen("PHP_AUTH_"))) { | |
$authorization = "Basic ".base64_encode("{$_SERVER['PHP_AUTH_USER']}:{$_SERVER['PHP_AUTH_PW']}"); | |
continue; | |
} | |
$header = str_replace("_", "-", $header); | |
$line = "{$header}: {$value}\r\n"; | |
if (FALSE === socket_write($s, $line, strlen($line))) { | |
return FALSE; | |
} | |
} | |
$lines = array("Host: {$host}\r\n", "\r\n"); | |
if (strlen($authorization) > 0) { | |
array_unshift($lines, "Authorization: {$authorization}\r\n"); | |
} | |
foreach ($lines as $line) { | |
if (FALSE === socket_write($s, $line, strlen($line))) { | |
return FALSE; | |
} | |
} // FIXME: no php://input when enctype="multipart/form-data" | |
$body = fopen("php://input", "r"); | |
if (FALSE !== $body) { | |
for ($bytes = fread($body, 8192); strlen($bytes) > 0; $bytes = fread($body, 8192)) { | |
if (FALSE === socket_write($s, $bytes, strlen($bytes))) { | |
return FALSE; | |
} | |
} | |
fclose($body); | |
} | |
fflush($s); | |
return TRUE; | |
} | |
define(SENTINEL, "aborts forEachBlockRead loop"); | |
function forEachBlockRead($s, $blockSize, $readKind, $functionPointer) { | |
$block = socket_read($s, $blockSize, $readKind); | |
for (; strlen($block) > 0; $block = socket_read($s, $blockSize, $readKind)) { | |
if (SENTINEL === $functionPointer($block, $s)) { | |
break; | |
} | |
} | |
} | |
$postponedContent = ""; | |
$printContent = 'printBlock'; | |
function printHeader($line, $s) { | |
if ("\r" == substr($line, -1)) { | |
socket_read($s, strlen("\n"), PHP_NORMAL_READ); | |
if (strlen($line) == 1) { | |
return SENTINEL; // signal last header | |
} | |
} | |
if ("Connection: " === substr($line, 0, strlen("Connection: ")) || "Upgrade: " === substr($line, 0, strlen("Upgrade: "))) { | |
return; // will be provided by out server | |
} | |
$line = substr($line, 0, strlen($line) - 1); | |
if ("Content-Length: " === substr($line, 0, strlen("Content-Length: "))) { | |
global $postponedContent; | |
$postponedContent = $line; | |
return; | |
} elseif ("Location: " === substr($line, 0, strlen("Location: "))) { | |
$doubleSlashPos = strpos($line, "://"); | |
if (FALSE === $doubleSlashPos) { | |
$doubleSlashPos = -strlen("://"); | |
} | |
$protocol = substr($line, 0, strlen("://") + $doubleSlashPos); | |
$line = "{$protocol}{$_SERVER['HTTP_HOST']}{$_SERVER['SCRIPT_NAME']}/".substr($line, strlen($protocol)); | |
} | |
header($line); | |
global $printContent; | |
if ("Content-Type: text/html" === substr($line, 0, strlen("Content-Type: text/html"))) { | |
$printContent = 'bufferBlock'; | |
} else if ("Content-Type: application/javascript" === substr($line, 0, strlen("Content-Type: application/javascript"))) { | |
$printContent = 'bufferBlock'; | |
} | |
} | |
function printBlock($bytes, $s) { | |
global $postponedContent; | |
if (isset($postponedContent)) { | |
header($postponedContent); | |
$postponedContent = NULL; | |
} | |
echo $bytes; | |
} | |
function bufferBlock($lines, $s) { | |
global $postponedContent; | |
if ("Content-Length: " === substr($postponedContent, 0, strlen("Content-Length: "))) { | |
$postponedContent = ""; | |
} | |
$postponedContent .= $lines; | |
} | |
function close_status($s, $status, $msg, $nextHeader = NULL) { | |
header("{$_SERVER['SERVER_PROTOCOL']} {$status} {$msg}"); | |
if (isset($nextHeader)) { | |
header($nextHeader); | |
} | |
header("Content-Type: text/plain"); | |
echo $msg; | |
socket_close($s); | |
echo "\n<pre>".print_r($_SERVER, TRUE)."</pre>"; | |
} | |
function connectAndRelayRequest($s, $address, $protocol, $host, $port, $url) { | |
if (FALSE === socket_connect($s, $address, $port)) { | |
return close_status($s, 503, "{$host}:{$port} is down"); | |
} elseif (!writeReconstructedRequest($s, $protocol, $host, $url)) { | |
return close_status($s, 502, "{$host}:{$port} rejected request"); | |
} | |
header_remove(); | |
forEachBlockRead($s, 1024, PHP_NORMAL_READ, 'printHeader'); | |
global $printContent; | |
forEachBlockRead($s, 1280, PHP_BINARY_READ, $printContent); | |
global $postponedContent; | |
if (isset($postponedContent)) { | |
$pattern = preg_quote($host); | |
header("X-Original-Content-Length: ".strlen($postponedContent)); | |
$postponedContent = preg_replace("/{$pattern}/", "{$_SERVER['HTTP_HOST']}{$_SERVER['SCRIPT_NAME']}/\\0", $postponedContent); | |
header("Content-Length: ".strlen($postponedContent)); | |
echo $postponedContent; | |
} | |
socket_close($s); | |
} | |
for ($s = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); FALSE !== $s; ) { | |
$protocol = preg_replace(".+", "s", $_SERVER['HTTPS']); | |
$protocol = "http{$protocol}://"; | |
$queryPos = strpos($_SERVER['REQUEST_URI'], '?'); | |
if (FALSE !== $queryPos) { | |
$query = substr($_SERVER['REQUEST_URI'], $queryPos); | |
} | |
$script = $_SERVER['SCRIPT_NAME']; | |
for ($url = substr($_SERVER['PHP_SELF'], strlen($script)); "" === $url || substr($url, 0, 1) === "?"; ) { | |
return close_status($s, 200, "Usage: {$protocol}{$_SERVER['HTTP_HOST']}{$script}/[{$protocol}]host[:port][/uri][?query]"); | |
} // else starts with / | |
$url = substr($url, 1); | |
if ("ftp://" === substr($url, 0, strlen("ftp://"))) { | |
$protocol = "ftp://"; | |
} | |
$protLen = strlen($protocol); | |
if (substr($url, 0, $protLen) === $protocol) { | |
$url = substr($url, $protLen); | |
} | |
if (1 != sscanf($url, "%[^:/?]", $host)) { | |
return close_status($s, 400, "{$script}/[{$protocol}]host[:port][/uri][?query] expected instead of {$_SERVER['REQUEST_URI']}"); | |
} | |
$url = substr($url, strlen($host)); | |
$port = $_SERVER['SERVER_PORT']; | |
if (1 == sscanf($url, ":%[0-9]", $port)) { | |
$portStr = ":{$port}"; | |
$url = substr($url, strlen($port) + 1); | |
} | |
if ("" === $url) { | |
return close_status($s, 308, "Trailing / required for relative URLs to work", "Location: {$script}/{$host}{$portStr}/"); | |
} // else already starts with / | |
$address = $host; | |
if (!filter_var($address, FILTER_VALIDATE_IP)) { | |
$address = "127.0.0.1"; | |
if ("localhost" !== $host) { | |
// TODO: timeouts via http://pear.php.net/package/Net_DNS | |
$address = gethostbyname($host); | |
} | |
} | |
// debug_exit("protocol: {$protocol}, address: {$address}, host: {$host}, port: {$port}, url: {$url}, query: {$query}"); | |
return connectAndRelayRequest($s, $address, $protocol, $host, $port, $url.$query); | |
} | |
header("{$_SERVER['SERVER_PROTOCOL']} 500 Failed to create socket"); | |
?> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment