I’m trying to create a Man In The Middle persistent connection between a user and ratchet server, however when I try to do this i get a UTF-8 encoding error from ratchet server and then i fail to get a response altogether. The code is only supposed to close the connection to the ratchet server if the ratchet server disconnects or if the user connects.
i know the code disconnects from ratchet after receiving a response
I also haven’t added threads for multiple connections but i thought i’d add that once i’ve got the logic working:
<?php
// WebSocket Proxy Server
error_reporting(E_ALL);
set_time_limit(0);
$is_token = true;
$token = '';
$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($server === false) {
die("Failed to create socket: " . socket_strerror(socket_last_error()) . "n");
}
if (!socket_bind($server, 'localhost', 8091)) {
die("Failed to bind socket: " . socket_strerror(socket_last_error()) . "n");
}
if (!socket_listen($server)) {
die("Failed to listen on socket: " . socket_strerror(socket_last_error()) . "n");
}
echo "WebSocket server started on localhost:8091n";
while (true) {
$client = socket_accept($server);
if ($client === false) {
echo "Failed to accept connection: " . socket_strerror(socket_last_error()) . "n";
continue;
}
echo "Client connectedn";
$query = performHandshake($client);
if ($query === false) {
echo "Failed to perform WebSocket handshaken";
socket_close($client);
continue;
}
while (true) {
$input = readWebSocketFrame($client);
if ($input === false) {
echo "Client disconnectedn";
socket_close($client);
break;
}
echo "Received (hex): " . bin2hex($input) . "n";
echo "Received (plain text): " . $input . "n";
$response = forwardMessageToRatchetServer($input, $query);
if ($response === false) {
echo "Failed to get response from Ratchet servern";
sendWebSocketMessage($client, "Connection succeeded");
socket_close($client);
break;
} else {
echo "Response from Ratchet server (hex): " . bin2hex($response) . "n";
echo "Response from Ratchet server (plain text): " . $response . "n";
sendWebSocketMessage($client, $response);
}
}
}
function performHandshake($client) {
$request = socket_read($client, 1024);
if ($request === false) {
return false;
}
// Extract the token and other query parameters from the GET request
$is_token = false;
$query = [];
if (preg_match('/GET /?([^s]*) HTTP/1.1/', $request, $matches)) {
parse_str($matches[1], $query);
if (isset($query['token'])) {
$is_token = true;
}
}
// Extract WebSocket key for handshake response
preg_match('#Sec-WebSocket-Key: (.*)rn#', $request, $matches);
$key = base64_encode(sha1(trim($matches[1]) . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
$headers = "HTTP/1.1 101 Switching Protocolsrn";
$headers .= "Upgrade: websocketrn";
$headers .= "Connection: Upgradern";
$headers .= "Sec-WebSocket-Accept: $keyrnrn";
socket_write($client, $headers, strlen($headers));
// Return the query parameters for forwarding
return $is_token ? $query : false;
}
function readWebSocketFrame($client) {
$header = socket_read($client, 2);
if (!$header) {
return false;
}
$opcode = ord($header[0]) & 0x0F;
$masked = ord($header[1]) & 0x80;
$payload_length = ord($header[1]) & 0x7F;
if ($payload_length === 126) {
$header .= socket_read($client, 2);
$payload_length = unpack('n', substr($header, -2))[1];
} elseif ($payload_length === 127) {
$header .= socket_read($client, 8);
$payload_length = unpack('J', substr($header, -8))[1];
}
if ($masked) {
$mask = socket_read($client, 4);
}
$data = '';
while (strlen($data) < $payload_length) {
$chunk = socket_read($client, $payload_length - strlen($data));
if ($chunk === false) {
return false;
}
$data .= $chunk;
}
if ($masked) {
for ($i = 0; $i < $payload_length; $i++) {
$data[$i] = chr(ord($data[$i]) ^ ord($mask[$i % 4]));
}
}
return $data;
}
function sendWebSocketMessage($client, $message) {
$frame = chr(0x81); // Text frame
$length = strlen($message);
if ($length <= 125) {
$frame .= chr($length);
} elseif ($length <= 65535) {
$frame .= chr(126) . pack('n', $length);
} else {
$frame .= chr(127) . pack('J', $length);
}
$frame .= $message;
socket_write($client, $frame, strlen($frame));
}
function encodeWebSocketMessage($payload) {
$frameHead = [];
$payloadLength = strlen($payload);
$frameHead[0] = 0x81; // FIN + Text frame (0x01)
if ($payloadLength <= 125) {
$frameHead[1] = $payloadLength;
} elseif ($payloadLength <= 65535) {
$frameHead[1] = 126;
$frameHead[2] = ($payloadLength >> 8) & 255;
$frameHead[3] = $payloadLength & 255;
} else {
$frameHead[1] = 127;
$frameHead[2] = ($payloadLength >> 56) & 255;
$frameHead[3] = ($payloadLength >> 48) & 255;
$frameHead[4] = ($payloadLength >> 40) & 255;
$frameHead[5] = ($payloadLength >> 32) & 255;
$frameHead[6] = ($payloadLength >> 24) & 255;
$frameHead[7] = ($payloadLength >> 16) & 255;
$frameHead[8] = ($payloadLength >> 8) & 255;
$frameHead[9] = $payloadLength & 255;
}
// Generate a masking key
$mask = [];
for ($i = 0; $i < 4; $i++) {
$mask[$i] = chr(rand(0, 255));
}
$frameHead[1] |= 0x80; // Set the mask bit
$frame = '';
foreach ($frameHead as $byte) {
$frame .= chr($byte);
}
$frame .= implode('', $mask);
// Apply the mask to the payload
for ($i = 0; $i < $payloadLength; $i++) {
$frame .= chr(ord($payload[$i]) ^ ord($mask[$i % 4]));
}
return $frame;
}
function forwardMessageToRatchetServer($msg, $query = []) {
$ratchetClient = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($ratchetClient === false) {
echo "Failed to create Ratchet socket: " . socket_strerror(socket_last_error()) . "n";
return false;
}
socket_set_option($ratchetClient, SOL_SOCKET, SO_RCVTIMEO, array("sec" => 10, "usec" => 0));
socket_set_option($ratchetClient, SOL_SOCKET, SO_SNDTIMEO, array("sec" => 10, "usec" => 0));
if (!socket_connect($ratchetClient, 'localhost', 1112)) {
echo "Failed to connect to Ratchet server: " . socket_strerror(socket_last_error()) . "n";
socket_close($ratchetClient);
return false;
}
echo "Connected to Ratchet server on localhost:1112n";
// Prepare the query string if any parameters exist
$queryString = !empty($query) ? '?' . http_build_query($query) : '';
// Perform WebSocket handshake with Ratchet server
$key = base64_encode(random_bytes(16));
$headers = "GET /$queryString HTTP/1.1rn";
$headers .= "Host: localhost:1112rn";
$headers .= "Upgrade: websocketrn";
$headers .= "Connection: Upgradern";
$headers .= "Sec-WebSocket-Key: $keyrn";
$headers .= "Sec-WebSocket-Version: 13rnrnrn";
socket_write($ratchetClient, $headers, strlen($headers));
// Read handshake response
$response = '';
while ($line = socket_read($ratchetClient, 1024)) {
$response .= $line;
if (strpos($response, "rnrn") !== false) {
break;
}
}
echo "Handshake response from Ratchet:n$responsen";
// Send the actual message to Ratchet server
$encodedMsg = encodeWebSocketMessage($msg);
socket_write($ratchetClient, $encodedMsg, strlen($encodedMsg));
// Read the response from the Ratchet server
$response = '';
while (true) {
$chunk = socket_read($ratchetClient, 1024);
if ($chunk === false) {
echo "Socket read error: " . socket_strerror(socket_last_error()) . "n";
socket_close($ratchetClient);
return false;
}
$response .= $chunk;
// Break if the received data completes the WebSocket frame
if (strlen($chunk) < 1024) {
break;
}
}
socket_close($ratchetClient);
return $response;
}
?>
So, the code works to an extent (I have to refresh the browser twice) for example:
Client connected
Received (hex): 03e9
Received (plain text): ?
Connected to Ratchet server on localhost:1112
Handshake response from Ratchet:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: IfEsZ57LennkKBj+iUloVvXpVJ4=
X-Powered-By: Ratchet/0.4.4
Response from Ratchet server (hex): 882b03ef5261746368657420646574656374656420616e20696e76616c6964205554462d38207061796c6f6164
Response from Ratchet server (plain text): ?+?Ratchet detected an invalid UTF-8 payload
And ratchet does connect:
New connection opened: 105
The issue comes when trying to keep the connection state flowing persistently.
Also, if this could work with soccket.io it’d be helpful.
I saw an answer that says sirn-se (texttalk replacement) does this but i don’t think i’m to far away from getting a break through. If all else fails, I’ll use it but for now i’d like to figure this out