I am developing a simple websocket server in PHP. I know there are quite a few existing implementations but I want to make my own so to learn the protocol better. I managed to do the handshaking fine and my clients connect to the server. I also managed to decode the data from the client but I have problems sending back messages. The client disconnects when it receives my response. Firefox says The connection to ws://localhost:12345/ was interrupted while the page was loading..
I used this answer as a guide.
Here is my code for wrapping the data:
private function wrap($msg = ""){
$length = strlen($msg);
$this->log("wrapping (" . $length . " bytes): " . $msg);
$bytesFormatted = chr(129);
if($length <= 125){
$bytesFormatted .= chr($length);
} else if($length >= 126 && $length <= 65535) {
$bytesFormatted .= chr(126);
$bytesFormatted .= chr(( $length >> 8 ) & 255);
$bytesFormatted .= chr(( $length ) & 255);
} else {
$bytesFormatted .= chr(127);
$bytesFormatted .= chr(( $length >> 56 ) & 255);
$bytesFormatted .= chr(( $length >> 48 ) & 255);
$bytesFormatted .= chr(( $length >> 40 ) & 255);
$bytesFormatted .= chr(( $length >> 32 ) & 255);
$bytesFormatted .= chr(( $length >> 24 ) & 255);
$bytesFormatted .= chr(( $length >> 16 ) & 255);
$bytesFormatted .= chr(( $length >> 8 ) & 255);
$bytesFormatted .= chr(( $length ) & 255);
}
$bytesFormatted .= $msg;
$this->log("wrapped (" . strlen($bytesFormatted) . " bytes): " . $bytesFormatted);
return $bytesFormatted;
}
UPDATE: I tried it with Chrome and I got the following error, printed in the console: A server must not mask any frames that it sends to the client.
I put some console printouts on the server. It is a basic echo server. I try with aaaa. So the actual wrapped message must be 6 bytes. Right?
Chrome prints the above error. Note also that after wrapping the message I simply write it to the socket:
$sent = socket_write($client, $bytesFormatted, strlen($bytesFormatted));
$this->say("! " . $sent);
It prints 6 meaning 6 bytes are actually written to the wire.
If I try with aaa, Chrome doesn't print the error but doesn't call my onmessage handler either. It hangs as if waiting for more data.
Any help highly appreciated. Thanks.
I had the same problem: for some messages sent from the server there was no response in the browser, for some the error "A server must not mask any frames ..." was displayed, though I did not add any mask.
The reason was in the handshake sent.
The handshake was:
"HTTP/1.1 101 Web Socket Protocol Handshake\r\n" .
...
"WebSocket-Location: ws://{$host}{$resource}\r\n\r\n" . chr(0)
That chr(0) was the reason, after I removed it everything works.
When I wrote my websocket classes, I had the same issue.
In my case, I used output buffering to determine that I was echo'ing something out before I sent the reply.
Might try that and see if it's the problem.
Related
What is the proper syntax for that function?
iptcembed('Cincinnati','https://www.stev.com/TMuploads/Hebe%20-%202022%202023%200107%201859%2010.jpg',['2#120'][0]);
I've manually inserted %20 for each space. I expected it to insert "Cincinnati" in a JPG file's IPTC metadata.
The definition of iptcembed() is:
iptcembed(string $iptc_data, string $filename, int $spool = 0): string|bool
The third argument ($spool) is just a flag - an integer: if the value is less than 2, then the function will return a string. Otherwise (f.e. if it is equal to or higher than 2) then the JPEG data will be "printed" to STDOUT. The documentation for that argument reads:
Spool flag. If the spool flag is less than 2 then the JPEG will be returned as a string. Otherwise the JPEG will be printed to STDOUT.
And the documentation on the function's return value mentions that you always have to expect the Boolean datatype when something goes wrong and a string can't be returned:
If spool is less than 2, the JPEG will be returned, or false on failure. Otherwise returns true on success or false on failure.
Making the iptc_data object is a bit tricky, so it's probably best to just use the sample code from the documentation of that function, particularly the iptc_make_tag() function given there:
<?php
// iptc_make_tag() function by Thies C. Arntzen
function iptc_make_tag($rec, $data, $value)
{
$length = strlen($value);
$retval = chr(0x1C) . chr($rec) . chr($data);
if($length < 0x8000)
{
$retval .= chr($length >> 8) . chr($length & 0xFF);
}
else
{
$retval .= chr(0x80) .
chr(0x04) .
chr(($length >> 24) & 0xFF) .
chr(($length >> 16) & 0xFF) .
chr(($length >> 8) & 0xFF) .
chr($length & 0xFF);
}
return $retval . $value;
}
// Path to jpeg file
$path = './phplogo.jpg';
// Set the IPTC tags
$iptc = array(
'2#120' => 'Test image',
'2#116' => 'Copyright 2008-2009, The PHP Group'
);
// Convert the IPTC tags into binary code
$data = '';
foreach($iptc as $tag => $string)
{
$tag = substr($tag, 2);
$data .= iptc_make_tag(2, $tag, $string);
}
// Embed the IPTC data
$content = iptcembed($data, $path);
// Write the new image data out to the file.
$fp = fopen($path, "wb");
fwrite($fp, $content);
fclose($fp);
?>
I'm trying to connect a PHP-based client to a websocket server.
Here's the code I have been using which has been widely published on different forums. But for some reason I just cannot get it to work.
Any help would be appreciated.
$host = 'host'; //where is the websocket server
$port = 443; //ssl
$local = "http://www.example.com/"; //url where this script run
$data = '{"id": 2,"command": "server_info"}'; //data to be send
$head = "GET / HTTP/1.1"."\r\n".
"Upgrade: WebSocket"."\r\n".
"Connection: Upgrade"."\r\n".
"Origin: $local"."\r\n".
"Host: $host"."\r\n".
"Content-Length: ".strlen($data)."\r\n"."\r\n";
////WebSocket handshake
$sock = fsockopen($host, $port, $errno, $errstr, 2);
fwrite($sock, $head ) or die('error:'.$errno.':'.$errstr);
$headers = fread($sock, 2000);
fwrite($sock, "\x00$data\xff" ) or die('error:'.$errno.':'.$errstr);
$wsdata = fread($sock, 2000); //receives the data included in the websocket package "\x00DATA\xff"
$retdata = trim($wsdata,"\x00\xff"); //extracts data
////WebSocket handshake
fclose($sock);
echo $retdata;
I would probably prefer to use an existing websocket client library (maybe https://github.com/gabrielbull/php-websocket-client or https://github.com/Devristo/phpws/tree/master/src/Devristo/Phpws/Client ?) rather than roll your own, but I got it to at least connect by using:
$head = "GET / HTTP/1.1"."\r\n".
"Host: $host"."\r\n".
"Upgrade: websocket"."\r\n".
"Connection: Upgrade"."\r\n".
"Sec-WebSocket-Key: asdasdaas76da7sd6asd6as7d"."\r\n".
"Sec-WebSocket-Version: 13"."\r\n".
"Content-Length: ".strlen($data)."\r\n"."\r\n";
My server is using TLS/SSL, so I also needed:
$sock = fsockopen('tls://'.$host, $port, $errno, $errstr, 2);
The full protocol spec is: https://tools.ietf.org/rfc/rfc6455.txt
UPDATE 2019: many servers requires the key to be more unique that the original example, resulting in failure to establish upgrade connection. The key generation is now changed accordingly.
Your header must contain:
Sec-WebSocket-Key: (some value)
Sec-WebSocket-Version: 13
to connect successfully.
When you have made the connection, you also need to use the hybi10 frame encoding.
See: https://tools.ietf.org/rfc/rfc6455.txt - Its a bit dry though.
I have made this working example:
<?php
$sp=websocket_open('127.0.0.1/ws_request.php?param=php_test',$errstr);
websocket_write($sp,"Websocket request message");
echo websocket_read($sp,true);
$sp=websocket_open('127.0.0.1:8080/ws_request.php?param=php_test',$errstr);
websocket_write($sp,"Websocket request message");
echo websocket_read($sp,true);
function websocket_open($url){
$key=base64_encode(openssl_random_pseudo_bytes(16));
$query=parse_url($url);
$header="GET / HTTP/1.1\r\n"
."pragma: no-cache\r\n"
."cache-control: no-cache\r\n"
."Upgrade: WebSocket\r\n"
."Connection: Upgrade\r\n"
."Sec-WebSocket-Key: $key\r\n"
."Sec-WebSocket-Version: 13\r\n"
."\r\n";
$sp=fsockopen($query['host'],$query['port'], $errno, $errstr,1);
if(!$sp) die("Unable to connect to server ".$url);
// Ask for connection upgrade to websocket
fwrite($sp,$header);
stream_set_timeout($sp,5);
$reaponse_header=fread($sp, 1024);
if(!strpos($reaponse_header," 101 ")
|| !strpos($reaponse_header,'Sec-WebSocket-Accept: ')){
die("Server did not accept to upgrade connection to websocket"
.$reaponse_header);
}
return $sp;
}
function websocket_write($sp, $data,$final=true){
// Assamble header: FINal 0x80 | Opcode 0x02
$header=chr(($final?0x80:0) | 0x02); // 0x02 binary
// Mask 0x80 | payload length (0-125)
if(strlen($data)<126) $header.=chr(0x80 | strlen($data));
elseif (strlen($data)<0xFFFF) $header.=chr(0x80 | 126) . pack("n",strlen($data));
elseif(PHP_INT_SIZE>4) // 64 bit
$header.=chr(0x80 | 127) . pack("Q",strlen($data));
else // 32 bit (pack Q dosen't work)
$header.=chr(0x80 | 127) . pack("N",0) . pack("N",strlen($data));
// Add mask
$mask=pack("N",rand(1,0x7FFFFFFF));
$header.=$mask;
// Mask application data.
for($i = 0; $i < strlen($data); $i++)
$data[$i]=chr(ord($data[$i]) ^ ord($mask[$i % 4]));
return fwrite($sp,$header.$data);
}
function websocket_read($sp,$wait_for_end=true,&$err=''){
$out_buffer="";
do{
// Read header
$header=fread($sp,2);
if(!$header) die("Reading header from websocket failed");
$opcode = ord($header[0]) & 0x0F;
$final = ord($header[0]) & 0x80;
$masked = ord($header[1]) & 0x80;
$payload_len = ord($header[1]) & 0x7F;
// Get payload length extensions
$ext_len = 0;
if($payload_len >= 0x7E){
$ext_len = 2;
if($payload_len == 0x7F) $ext_len = 8;
$ext=fread($sp,$ext_len);
if(!$ext) die("Reading header extension from websocket failed");
// Set extented paylod length
$payload_len= 0;
for($i=0;$i<$ext_len;$i++)
$payload_len += ord($header[$i]) << ($ext_len-$i-1)*8;
}
// Get Mask key
if($masked){
$mask=fread($sp,4);
if(!$mask) die("Reading header mask from websocket failed");
}
// Get payload
$frame_data='';
do{
$frame= fread($sp,$payload_len);
if(!$frame) die("Reading from websocket failed.");
$payload_len -= strlen($frame);
$frame_data.=$frame;
}while($payload_len>0);
// if opcode ping, reuse headers to send a pong and continue to read
if($opcode==9){
// Assamble header: FINal 0x80 | Opcode 0x02
$header[0]=chr(($final?0x80:0) | 0x0A); // 0x0A Pong
fwrite($sp,$header.$ext.$mask.$frame_data);
// Recieve and unmask data
}elseif($opcode<3){
$data="";
if($masked)
for ($i = 0; $i < $data_len; $i++)
$data.= $frame_data[$i] ^ $mask[$i % 4];
else
$data.= $frame_data;
$out_buffer.=$data;
}
// wait for Final
}while($wait_for_end && !$final);
return $out_buffer;
}
You can get the full version here: https://github.com/paragi/PHP-websocket-client
Connecting to a WSS stream with purely php:
Example with the public binance wss api.
<?php
$sock = stream_socket_client("tls://stream.binance.com:9443",$error,$errnum,30,STREAM_CLIENT_CONNECT,stream_context_create(null));
if (!$sock) {
echo "[$errnum] $error" . PHP_EOL;
} else {
echo "Connected - Do NOT get rekt!" . PHP_EOL;
fwrite($sock, "GET /stream?streams=btcusdt#kline_1m HTTP/1.1\r\nHost: stream.binance.com:9443\r\nAccept: */*\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: ".rand(0,999)."\r\n\r\n");
while (!feof($sock)) {
var_dump(explode(",",fgets($sock, 512)));
}
}
More details: WebSockets - send json data via php
https://github.com/ratchetphp/Pawl
I tried around 10 different solutions from various stackoverflow threads but nothing worked, after spending about 6 hours trying different solution this worked for me.
scenario:
Ratchet as server
I was able to connect via reactjs
I wasn't able to connect via php,
Expected Result: To send message from php-client to all connected react clients. the git repo i provided was a lucky break i was looking for.
I tried the solution presented here and it works fine for me.
here are the details:
step1: Install the library from here with non-root user as follows:
php8.0 composer require textalk/websocket
then use the code below for sending some message to a socket which is located on localhost and has port number 8080:
<?php
require('vendor/autoload.php');
use WebSocket\Client;
$client = new Client("ws://127.0.0.1:8080");
$client->send($argv[1]);
?>
Since I had php7.1 webserver and the socket was installed with php8.0, I put the above code in a PHP file (testphp.php) and call it using shell_exec('php8.0 testphp.php hello');
The 400 error is because you're missing Host and Origin in the header.
$key=base64_encode(openssl_random_pseudo_bytes(16));
$query=parse_url($url);
$local = "http://".$query['host'];
if (isset($_SERVER['REMOTE_ADDR'])) $local = "http://".$_SERVER['REMOTE_ADDR'];
$header="GET / HTTP/1.1\r\n"
."Host: ".$query['host']."\r\n"
."Origin: ".$local."\r\n"
."Pragma: no-cache\r\n"
."Cache-Control: no-cache\r\n"
."Upgrade: websocket\r\n"
."Connection: Upgrade\r\n"
."Sec-WebSocket-Key: $key\r\n"
."Sec-WebSocket-Version: 13\r\n"
."\r\n";
I have this code to send a packet in PHP, and I wanted to know how to do it in perl, I have tried sending via hex data, but it is not working. Here is the PHP code:
$sIPAddr = "37.221.175.211";
$iPort = 7777;
$sPacket = "";
$aIPAddr = explode('.', $sIPAddr);
$sPacket .= "SAMP";
$sPacket .= chr($aIPAddr[0]);
$sPacket .= chr($aIPAddr[1]);
$sPacket .= chr($aIPAddr[2]);
$sPacket .= chr($aIPAddr[3]);
$sPacket .= chr($iPort & 0xFF);
$sPacket .= chr($iPort >> 8 & 0xFF);
$sPacket .= 'c';
$rSocket = fsockopen('udp://'.$sIPAddr, $iPort, $iError, $sError, 2);
fwrite($rSocket, $sPacket);
fclose($rSocket);
How would I go about doing this in Perl? I want to use a raw socket as well to send it.
This is what I tried, but the server is not replying to it, which makes me think that the data is corrupted somewhere:
$packet = Net::RawIP->new({
ip => {
saddr => $saddr,
daddr => $dest,
},
udp => {
source => $rsport,
dest => $port,
data => "\x53\x41\x4d\x50\x25\xdd\xaf\xd3\x61\x1e\x63", # this is the data from the PHP file in HEX
},
});
$packet->send;
Don't know about Net::RawIP, but here's the Perl variant that sends the exact same packet as your PHP code, using IO::Socket::INET module. For docs for it, see https://metacpan.org/pod/IO::Socket::INET
use strict;
use warnings;
use IO::Socket;
my $sIPAddr = '37.221.175.211';
my $iPort = 7777;
my $sPacket = 'SAMP' . join( '', map chr,
split(/\./, $sIPAddr),
$iPort & 0xFF,
$iPort >> 8 & 0xFF,
) . 'c';
my $sock = IO::Socket::INET->new(
Proto => 'udp',
PeerPort => $iPort,
PeerAddr => $sIPAddr,
) or die "Could not create socket: $!\n";
$sock->send( $sPacket );
I set up a basic video chat app using the WebRTC APIs in Chrome along with a WebSocket script I wrote myself following the W3C specs and other questions here on SO.
Sometimes though, when one PC sends ICE candidate info to the other PC via the WebSocket connection, a bunch of garbled text is attached to the end of the JSON-stringified candidate info.
This problem only happens sometimes though, and it never happens with the SDP info sent via the createOffer and createAnswer methods.
Please see the following link for an example of what I'm talking about:
http://s1290.beta.photobucket.com/user/HartleySan83/media/NGdata_zps0a7203e7.png.html?sort=3&o=0
Because the JSON-stringified candidate info always ends with '}}', by adding an if condition to the WebSocket server script, I was able to circumvent this problem and get the video chat app to work. Unfortunately, this is a hack that I'd like to avoid. Plus, I'd like to know why this is happening in the first place.
It's worth noting that when I either alert or echo the candidate info to the console on the client side before it's sent to the WebSocket server script, none of the extra garbled text is present, so I'm not sure why it's present with the candidate info on the server side and only sometimes.
The following is a code snippet of the client-side code where the candidate info is sent to the server-side script:
function startPeerConnection() {
navigator.webkitGetUserMedia({ audio: true, video: true }, function (stream) {
document.getElementById('vid1').src = webkitURL.createObjectURL(stream);
pc = new webkitRTCPeerConnection(null);
pc.onicecandidate = function (evt) {
if (evt.candidate) {
socket.send(JSON.stringify({ candidate: evt.candidate }));
}
};
pc.onaddstream = function (evt) {
document.getElementById('vid2').src = webkitURL.createObjectURL(evt.stream);
};
pc.addStream(stream);
}, function () {});
}
And the following is the server-side code that unmasks the received WebSocket data:
$len = ord($buffer[1]) & 127;
if ($len === 126) {
$masks_start = 4;
} else if ($len === 127) {
$masks_start = 10;
} else {
$masks_start = 2;
}
$masks = substr($buffer, $masks_start, 4);
$data = substr($buffer, $masks_start + 4);
$len = strlen($data);
$text = '';
for ($i = 0; $i < $len; $i++) {
$text .= $data[$i] ^ $masks[$i % 4];
}
if (($end = strpos($text, '}}')) !== false) {
// This if condition eliminates the garbled text.
// Without it, a "Could not decode a text frame as UTF-8"
// error is output to the Chrome console.
$text = substr($text, 0, $end + 2);
$len = strlen($text);
}
if ($len <= 125) {
$header = pack('C*', 129, $len);
} else if (($len > 125) && ($len < 65536)) {
$header = pack('C*', 129, 126, ($len >> 8) & 255, $len & 255);
} else if ($len >= 65536) {
$header = pack('C*', 129, 127, ($len >> 56) & 255, ($len >> 48) & 255, ($len >> 40) & 255, ($len >> 32) & 255, ($len >> 24) & 255, ($len >> 16) & 255, ($len >> 8) & 255, $len & 255);
}
$server_response = $header . $text;
foreach ($users as $user) {
if ($user !== $users[$user_idx]) {
#socket_write($user['socket'], $server_response, strlen($server_response));
}
}
I've searched high and low on the Internet for anyone else with the same problem, but I can't find anyone or anything in the specs that talks about this, so I imagine it's some problem with my code.
Any guidance that anyone can offer as to the source of the problem would be much appreciated.
Thank you.
Well, I finally found the problem. My server-side WebSocket code was indeed wrong. The problem was that I was miscalculating the length. I, unfortunately, was relying on some page I found about WebSockets in PHP, and as it turns out, the page had a number of errors in its code, which I slowly started to realize more and more. Anyway, here's the proper way to calculate the length of messages sent from a client to the server:
$len = ord($buffer[1]) & 127; // This is the default payload length.
if ($len === 126) { // If 126, then need to use the payload length at the 3rd and 4th bytes.
$masks_start = 4;
$len = (ord($buffer[2]) << 8) + ord($buffer[3]);
} else if ($len === 127) { // If 127, then need to use the next 8 bytes to calculate the length.
$masks_start = 10;
$len = (ord($buffer[2]) << 56) + (ord($buffer[3]) << 48) + (ord($buffer[4]) << 40) + (ord($buffer[5]) << 32) + (ord($buffer[6]) << 24) + (ord($buffer[7]) << 16) + (ord($buffer[8]) << 8) + ord($buffer[9]);
} else { // Otherwise, the default payload length is correct.
$masks_start = 2;
}
After doing that, everything worked great. Well, I'm still haven't figured out how to properly close a WebSocket connection, but other than that, the WebRTC video is working great.
Is the garbled binary data being added by client1 before it sends it to the Websocket server? Or are you only seeing it on the client2 after it's been processed by the websocket server? I ask because I ran into a similar problem where my signaling server (SignalR in this case) had a bug that corrupted the SDP I was sending in-between PeerConnections.
So Chrome 14 has implemented hybi10 version of websockets. I have a in house program that our company uses via chrome that uses websockets which is broken with this change.
Has anyone been successful framing the data using a php server? I am able to get the new handshake to work but I can't seem to figure out the framing. There is a python example here https://github.com/kanaka/websockify/blob/master/websocket.py#L233 but I am having a difficult time converting this to php, anyone have a suggestion?
I should mention that the function in question on the python example is decode_hybi().
i just completed a class wich makes the PHP-Websocket-Server of Nico Kaiser (https://github.com/nicokaiser/php-websocket) capable of handling hybi-10 frames and handshake. You can download the new class here: http://lemmingzshadow.net/386/php-websocket-serverclient-nach-draft-hybi-10/ (Connection.php)
This code assumes no errors or malformed frames and is based on this answer - How to (de)construct data frames in WebSockets hybi 08+?.
This code is very basic and is far from a complete solution. It works for my purposes (which are pretty basic). Hopefully it is of use to others.
function handle_data($data){
$bytes = $data;
$data_length = "";
$mask = "";
$coded_data = "" ;
$decoded_data = "";
$data_length = $bytes[1] & 127;
if($data_length === 126){
$mask = substr($bytes, 4, 8);
$coded_data = substr($bytes, 8);
}else if($data_length === 127){
$mask = substr($bytes, 10, 14);
$coded_data = substr($bytes, 14);
}else{
$mask = substr($bytes, 2, 6);
$coded_data = substr($bytes, 6);
}
for($i=0;$i<strlen($coded_data);$i++){
$decoded_data .= $coded_data[$i] ^ $mask[$i%4];
}
$this->log("Server Received->".$decoded_data);
return true;
}
Here is the code to send data back. Again this is pretty basic, it assumes you are sending a single text frame. No continuation frames etc. No error checking either. Hopefully others find it useful.
public function send($data)
{
$frame = Array();
$encoded = "";
$frame[0] = 0x81;
$data_length = strlen($data);
if($data_length <= 125){
$frame[1] = $data_length;
}else{
$frame[1] = 126;
$frame[2] = $data_length >> 8;
$frame[3] = $data_length & 0xFF;
}
for($i=0;$i<sizeof($frame);$i++){
$encoded .= chr($frame[$i]);
}
$encoded .= $data;
write_to_socket($this->socket, $encoded);
return true;
}