Skip to content

Instantly share code, notes, and snippets.

@clayfreeman
Last active May 12, 2023 16:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save clayfreeman/a29d7b533c47adbf51e5b947238b931e to your computer and use it in GitHub Desktop.
Save clayfreeman/a29d7b533c47adbf51e5b947238b931e to your computer and use it in GitHub Desktop.
Stream socket client in PHP using a HTTP CONNECT proxy
<?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();
}
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