Skip to content

Instantly share code, notes, and snippets.

@samunders-core
Last active November 18, 2018 23:27
Show Gist options
  • Save samunders-core/e067462496bb61d29e2ca05f1f379369 to your computer and use it in GitHub Desktop.
Save samunders-core/e067462496bb61d29e2ca05f1f379369 to your computer and use it in GitHub Desktop.
URL-rewriting PHP proxy, works with Apache's userdir module
<?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