Last active
May 12, 2023 16:14
-
-
Save clayfreeman/a29d7b533c47adbf51e5b947238b931e to your computer and use it in GitHub Desktop.
Stream socket client in PHP using a HTTP CONNECT proxy
This file contains 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 | |
// Specify the hostname and port to which you wish to connect via HTTP proxy. | |
$target_hostname = 'www.google.com'; | |
$target_port = 443; | |
// Specify whether the proxied connection should use TLS. | |
$target_crypto = TRUE; | |
// Specify the address of the HTTP proxy to use. | |
// Use the same format as the $address parameter of \stream_socket_client(). | |
$proxy_client = 'tcp://127.0.0.1:80'; | |
// Specify the HTTP proxy credentials (optional). | |
$proxy_username = ''; | |
$proxy_password = ''; | |
//////////////////////////////////////////////////////////////////////////////// | |
// Create a stream context to facilitate crypto (should it be desired). | |
// We specify the peer name manually, otherwise TLS negotiation would fail. | |
$context = \stream_context_get_default($target_crypto ? [ | |
'ssl' => [ | |
'crypto_method' => \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT, | |
'peer_name' => $target_hostname, | |
], | |
] : []); | |
// Attempt to open a stream socket client to the HTTP proxy. | |
if (!$fh = \stream_socket_client($proxy_client, context: $context)) { | |
throw new \RuntimeException('Unable to connect to HTTP proxy'); | |
} | |
\stream_set_timeout($fh, 3); | |
/** | |
* Send a line to the HTTP proxy. | |
* | |
* @param string $line | |
* The line that should be sent to the HTTP proxy. | |
*/ | |
$write = function (string $line = '') use ($fh) { | |
\fwrite($fh, "{$line}\r\n"); | |
echo "-> {$line}" . \PHP_EOL; | |
}; | |
/** | |
* Read a line from the HTTP proxy. | |
* | |
* The resulting line will not contain a line ending sequence. | |
* | |
* @return string|false | |
* On success, a line read from the HTTP proxy is returned. | |
* On failure, FALSE is returned. | |
*/ | |
$read = function () use ($fh): string|false { | |
$line = \fgets($fh); | |
if ($line !== FALSE) { | |
$line = (string) \preg_replace('/\\r?\\n$/', '', $line); | |
} | |
echo "<- {$line}" . \PHP_EOL; | |
return $line; | |
}; | |
if ($proxy_username !== '' || $proxy_password !== '') { | |
$authorization = \base64_encode("{$proxy_username}:{$proxy_password}"); | |
} | |
// Request the HTTP proxy establish a TCP tunnel to the target host. | |
$write("CONNECT {$target_hostname}:{$target_port} HTTP/1.0"); | |
// Check if authorization needs to be sent. | |
if (isset($authorization)) { | |
$write("Proxy-Authorization: basic {$authorization}"); | |
} | |
// End the request with an empty line. | |
$write(); | |
$response_valid = FALSE; | |
$status_line_expr = '/^HTTP\\S* (?P<status_code>\\d{3})(?: (?P<status_text>.*))?/i'; | |
while (!\in_array($line = $read(), ['', FALSE], TRUE)) { | |
// Attempt to parse the first line of the response as a HTTP status message. | |
if (!$response_valid && \preg_match($status_line_expr, $line, $matches)) { | |
$status_code = \intval($matches['status_code'] ?? NULL); | |
$status_text = $matches['status_text'] ?? ''; | |
if ($status_code < 200 || $status_code >= 300) { | |
$message = 'Error establishing proxy connection' . ($status_text !== '' ? ": {$status_text}" : ''); | |
throw new \RuntimeException($message, $status_code); | |
} | |
$response_valid = TRUE; | |
} | |
elseif (!$response_valid) { | |
// If we've yet to see a HTTP status line, then the response is invalid. | |
throw new \RuntimeException('Invalid response from proxy server'); | |
} | |
} | |
try { | |
// Set a custom error handler to intercept any errors that occur when | |
// calling \stream_socket_enable_crypto(). | |
\set_error_handler(function (int $errno, string $errstr) { | |
throw new \RuntimeException('Unable to enable TLS on the underlying stream socket: ' . $errstr, $errno); | |
}); | |
// If crypto is desired for the proxied connection, attempt to enable it. | |
if ($target_crypto && !@\stream_socket_enable_crypto($fh, TRUE)) { | |
throw new \RuntimeException('Unable to enable TLS on the underlying stream socket'); | |
} | |
} | |
finally { | |
\restore_error_handler(); | |
} | |
// At this point, the proxied connection is fully established. | |
// Here, we demonstrate an example HTTPS GET request. | |
$write("GET / HTTP/1.0"); | |
$write(); | |
while (!\feof($fh)) { | |
$read(); | |
} |
This file contains 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
ProxyRequests On | |
ProxyVia Off | |
AllowCONNECT 1-65535 | |
<Proxy *> | |
Order deny,allow | |
Allow from all | |
AuthType Basic | |
AuthName "Authorization Required" | |
AuthUserFile .htpasswd | |
Require valid-user | |
</Proxy> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment