Search code examples
phpwebsocketstreamphp-stream-wrappers

PHP - stream crypto enabled server cuts off bytes


I made a WebSocket server with PHP and added the feature to add SSL/TLS to the server. Now I enabled crypto with stream_socket_enable_crypto and everything works. I read data and it's decrypted. Only HTTP requests are a bit weird:

Client sends:

GET / HTTP/1.1
Host: example.com:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0
Accept: */*
...

Server receives:

 HTTP/1.1
Host: example.com:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0
Accept: */*
...

My Questions are: What is the reason for the missing bytes? What can I do to get those bytes back?

I use PHP 8.2

Server:

Terminal:

openssl genrsa -out wss.key 2048 && openssl req -key wss.key -new -x509 -days 365 -out wss.crt

PHP:

error_reporting(E_ALL);
ini_set("display_errors", "1");

if(ob_get_level() == 0) ob_start();

$server = stream_socket_server("tcp://example.com:8080", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN);

stream_context_set_option($server, ["ssl" => [
    "local_cert" => __DIR__."/wss.crt",
    "local_pk" => __DIR__."/wss.key",
    "disable_compression" => true,
    "verify_peer" => false,
    "verify_peer_name" => false,
    "allow_self_signed" => true
]]);

$done = false;
while(!$done) {
    $sockets = ["server" => $server];

    if(stream_select($sockets, $w, $e, 0, 20) === false)
        show("ERROR");

    if(in_array($server, $sockets)) {
        $client = stream_socket_accept($server);
        $sockets[] = $client;
        stream_socket_enable_crypto($client, true, STREAM_CRYPTO_METHOD_TLS_SERVER);
    }

    foreach($sockets as $client) {
        if(!in_array($client, $sockets))
            continue;

        show(fread($client, 100));

        fclose($client);
        $done = true;
        show("EXIT");
    }
}

function show(...$data) {
    var_dump($data);
    @ob_flush();
    flush();
}

Client:

JS:

url = "ws://example.com:8080";
socket = new WebSocket(url);
setTimeout(() => {
    socket.close();
    socket = new WebSocket(url);
}, 1000);

Change example.com:8080 with your host and port

for detailed code look at PHPWS


Solution

  • I now know the issue. The TCP paylaod starts after 00 00 and in HTTP there begins the GET / and in TLS the TLS headers: 17 03 03 03 15 with the TLS DATA TYPE (Application Data 23), the TLS VERSION (1.2 03 03) and the PAYLOAD LENGTH (789 03 15)

    HTTP Packet

    TLS Packet

    In PHP after stream_socket_enable_crypto the first package is read as TLS and the first 5 bytes as the TLS headers and after failing, because its HTTP, it is missing those 5 bytes in the payload.

    Solution:

    Before enabling crypto, read the data without clearing it from the buffer (PEEK)

    if(in_array($server, $sockets)) {
        $client = stream_socket_accept($server);
        $sockets[] = $client;
    
        $content = stream_socket_recvfrom($client, 5, STREAM_PEEK);
    
        stream_socket_enable_crypto($client, true, STREAM_CRYPTO_METHOD_TLS_SERVER);
    }
    

    Now the data can be used if it's just a HTTP request and no TLS header information are missing when it's a TLS handshake.