Jump to content

WebSocket protocol?


DWilliams

Recommended Posts

I've been tinkering with websockets lately and attempting to create a server in PHP.

 

I read this summary on Wikipedia to gain an understanding of the protocol. It seems rather easy to implement, but I'm having some issues. I have a test page with JavaScript attempting to open a websocket and a socket server script in PHP that does nothing other than listen for and accept connections. Now, this seems to work on the surface. A socket is opened and immediately closes, which is what I would expect since the server is not coded to follow websocket protocol. What is confusing me, however, is that no handshake request is ever sent by the JS client. I have wireshark set up and capturing all packets going back and forth, and the initial handshake packet described by Wikipedia is never sent.

 

Maybe my sleep deprived brain is missing something obvious here, but shouldn't the client always send the first handshake packet as soon as the socket is opened, regardless of whether the server in question is just a dumb script that doesn't know what to do with it?

Link to comment
Share on other sites

Without seeing the code behind it, can't really say much about why you see what you do.  If you php script doesn't hold the socket open, probably it's being closed before the handshake can be sent.

 

Such as if your doing like

$client = socket_accept($server);
socket_close($client); //or just ending script

the socket will open then close immediately, without giving a chance for anything to be sent.

 

Link to comment
Share on other sites

The server code is just some generic PHP socket server script I picked up for testing purposes:

 

<?php
// Set time limit to indefinite execution
set_time_limit (0);

function server_log($msg)
{
echo '[' . date('G:i:s') . "] $msg\n";	
}

// Set the ip and port we will listen on
$address = 'localhost';
$port = 10000;
$max_clients = 10;

// Array that will hold client information
$client = Array();

// Create a TCP Stream socket
$sock = socket_create(AF_INET, SOCK_STREAM, 0);
// Bind the socket to an address/port
socket_bind($sock, $address, $port) or die('Could not bind to address');
server_log("Server started on {$address}:{$port}");
// Start listening for connections
socket_listen($sock);

// Loop continuously
while (true) 
{
// Setup clients listen socket for reading
$read[0] = $sock;
for ($i = 0; $i < $max_clients; $i++) {
	if (isset($client[$i]['sock']))
		$read[$i + 1] = $client[$i]['sock'];
}
// Set up a blocking call to socket_select()
if (socket_select($read, $write = NULL, $except = NULL, $tv_sec = 5) < 1)
	continue;
/* if a new connection is being made add it to the client array */
if (in_array($sock, $read)) {
	for ($i = 0; $i < $max_clients; $i++) {
		if (empty($client[$i]['sock'])) {
			$client[$i]['sock'] = socket_accept($sock);
			echo "New client connected $i\r\n";
			break;
		}
		elseif ($i == $max_clients - 1)
			echo "Too many clients...\r\n";
	}
} // end if in_array
// If a client is trying to write - handle it now
for ($i = 0; $i < $max_clients; $i++) { // for each client
	if (isset($client[$i]['sock'])) {
		if (in_array($client[$i]['sock'], $read)) {
			$input = socket_read($client[$i]['sock'], 1024);
			if ($input == null) {
				echo "Client disconnecting $i\r\n";
				// Zero length string meaning disconnected
				unset($client[$i]);
			} else {
				echo "New input received $i\r\n";
				// send it to the other clients
				for ($j = 0; $j < $max_clients; $j++) {
					if (isset($client[$j]['sock']) && $j != $i) {
						echo "Writing '$input' to client $j\r\n";
						socket_write($client[$j]['sock'], $input, strlen($input));
					}
				}
				if ($input == 'exit') {
					// requested disconnect
					socket_close($client[$i]['sock']);
				}
			}
		} else {
			echo "Client disconnected $i\r\n";
			// Close the socket
			socket_close($client[$i]['sock']);
			unset($client[$i]);
		}
	}
}
} // end while
// Close the master sockets
socket_close($sock);

 

That seems like it should work for purposes of getting the JS websocket to send its handshake

Link to comment
Share on other sites

Do you watch the output of the PHP server script while testing?  Can you see it output a message when the connection is accepted and when data is received?

 

I see your binding to localhost, note that Wireshark will not capture packets going across localhost without some hacks.  It'd be easier to probably rely on output from the server script, or setup the server on a different host so your connecting via LAN/Internet and wireshark can see the packets.

Link to comment
Share on other sites

Yeah, the script outputs that it's being connected to:

 

New client connected 0
Client disconnected 0

 

And I'm capturing loopback with wireshark. I can see all the packets for downloading the page itself, just nothing about websockets or the HTTP upgrade packet. I've looked through every HTTP packet it produces and run searches for any of the terms that should be in the handshake packet

Link to comment
Share on other sites

Hmm I tried setting up the websocket example from this page and it doesn't work either. Similar result. No connection is made although the server script still shows something hitting it. No handshake packets are sent on this example either.

 

Maybe something else is the issue here? I'm not running through a proxy or anything. I've tried it both from the local machine as well as from another computer and the result is the same

 

EDIT: I've also tried in both Firefox 8 and Chrome 15

Link to comment
Share on other sites

After some more tinkering I think I have confirmed that it is indeed the server script that's causing the problem. I don't see anything wrong with it but with new server code I was able to capture the packet.

 

Now my problem is that the WebSocket protocol seems to be adapting so rapidly that trying to apply it seems like trying to hit a moving target.

 

This is the packet I captured (sent from Chrome):

GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: 192.168.1.29:12345
Sec-WebSocket-Origin: http://192.168.1.29
Sec-WebSocket-Key: 0fEMxyrlgsuDp8IA02nxCA==
Sec-WebSocket-Version: 8

 

That differs from all the examples I can find on the internet as well as the Wikipedia page, so I guess I have to hunt down the IETF protocol description for version 8 and implement it off that...

Link to comment
Share on other sites

Now my problem is that the WebSocket protocol seems to be adapting so rapidly that trying to apply it seems like trying to hit a moving target.

 

I was messing with Websockets toward the middle of the year and had developed some php scripts to manage them.  I went to pull them up to post as an example, found out they don't work anymore because the protocol changed.  I looked at updating it, didn't have the time though.

 

Not sure how much more it will be changing,  Hopefully it gets nailed down soon.  I believe the current spec is http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17.  The 'version 8' in the header maybe corrisponds to http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-08.  I'd probably look at the newest and implement it, probably will still work with what chrome has implemented.

Link to comment
Share on other sites

I've implemented it with hybi-17 but it doesn't work. I don't see any differences in the handshake between what I implemented and the IETF paper for version 8.

 

This is my handshake function:

 

function dohandshake($user, $buffer)
{
server_log(1, 'Requesting handshake...');

// Determine which version of the WebSocket protocol the client is using
if(preg_match("/Sec-WebSocket-Version: (.*)\r\n/ ", $buffer, $match))
	$version = $match[1];
else 
	return false;

if($version == 
{
	// Extract header variables
	if(preg_match("/GET (.*) HTTP/"   ,$buffer,$match)){ $r=$match[1]; }
	if(preg_match("/Host: (.*)\r\n/"  ,$buffer,$match)){ $h=$match[1]; }
	if(preg_match("/Sec-WebSocket-Origin: (.*)\r\n/",$buffer,$match)){ $o=$match[1]; }
	if(preg_match("/Sec-WebSocket-Key: (.*)\r\n/",$buffer,$match)){ $k = $match[1]; }

	// Generate our Socket-Accept key based on the IETF specifications
	$accept_key = $k . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
	$accept_key = sha1($accept_key, true);
	$accept_key = base64_encode($accept_key);

	$upgrade =	"HTTP/1.1 101 Switching Protocols\r\n" .
					"Upgrade: websocket\r\n" .
					"Connection: Upgrade\r\n" .
					"Sec-WebSocket-Accept: $accept_key";

	socket_write($user->socket, $upgrade, strlen($upgrade));
	$user->handshake = true;
	return true;
}
else 
{
	server_log("Client is trying to use an unsupported WebSocket protocol ({$version})");
	return false;
}
}

 

I can't tell for sure whether it's working but I suspect the handshake is not working.The JS client connects to it but when it tries to send data, I get a "Error: INVALID_STATE_ERR: DOM Exception 11" exception, and the readyState never changes from 0.

 

When I feed the sample keys into my function it produces the correct accept key...

Link to comment
Share on other sites

Can you post the entire script so others may use it/learn from your experience?

 

Sure, here's the server code: http://pastebin.com/a1Mzt3pC

Pretty much any simple websocket will work for it, here's the one I was using to test: http://code.google.com/p/phpwebsocket/source/browse/trunk/%20phpwebsocket/client.html

 

I don't take credit for most of it, I mainly pieced it together from several outdated and/or half-working sources. My main contribution to it is the handshaking code.

 

It has flaws obviously, the main being is that it isn't coded to properly respond to ping/pong/close packets but the framework is there and ready to be implemented. It's a good starting point for somebody wanting to implement a server, though.

 

Also, for people not used to working with sockets in PHP take note that by default PHP has no support for listening sockets. In Linux/mac you'll need to recompile with --enable-sockets and under Windows you have to enable some module.

Link to comment
Share on other sites

I spent a little time fixing up the code I had written for websockets to handle the updated protocol.  Here's the code, if anyone finds it useful.  It is based around PHP's Stream's api rather than the socket extension.  It's not a full and complete implementation, but should be enough to get things started for anyone really interested in websockets.

 

 

WebSocketConnection.class.php:


<?php

require __DIR__.DIRECTORY_SEPARATOR.'WebSocketMessage.class.php';
require __DIR__.DIRECTORY_SEPARATOR.'ILogger.interface.php';

class WebsocketConnection {
private $mReadBuffer;

protected $mProtoType;
protected $mProtoVersion;

protected $mState;
protected $mReceivedClose;
protected $mSentClose;
protected $mCloseStartedOn;
protected $mStream;

protected $mRequestUri;
protected $mHeaders;
protected $mLoggers;

const HANDSHAKE=1;
const ESTABLISHED=2;
const CLOSING=3;
const CLOSED=4;

const PROTO_HYBI='hybi';
const PROTO_HIXIE='hixie';

public function __get($nm){ throw new Exception('Unknown property '.$nm); }
public function __set($nm,$v){ throw new Exception('Unknown property '.$nm); }

public function __construct(){
	$this->mState = self::CLOSED;
	$this->mReadBuffer = null;
	$this->mStream = null;
	$this->mHeaders=array();
	$this->mRequestUri='';
	$this->mLoggers=array();
	$this->mReceivedClose=false;
	$this->mSentClose=false;
	$this->mCloseStartedOn=0;
}

public function addLogger(ILogger $logger){
	$this->mLoggers[]=$logger;
}

private function log($str){
	foreach ($this->mLoggers as $l){
		$l->log($str);
	}
}

public function GetStream(){
	return $this->mStream;
}

public function IsConnected(){
	return $this->mState == self::ESTABLISHED || $this->mState == self::CLOSING;
}

/**
 * Fill the read buffer from the stream
 *
 */
protected function fillReadBuffer(){
	$r = array($this->mStream);
	$w = $e = null;

	while (stream_select($r, $w, $e, 0, 0) > 0){
		$this->log('Reading data from stream');

		$data = fread($this->mStream, 1024);
		if (strlen($data)==0){
			//Reading 0 data means the stream has been closed
			$this->Close(true);
			return;
		}

		$this->mReadBuffer .= $data;
		//$this->log('Read data: '.$data);

		$r = array($this->mStream);
		$w = $e = null;
	}
}

protected function WriteStream($str){
	fwrite($this->mStream, $str);
}

/**
 * Locates the first instance of a set of strings
 * Returns the position in $str, optionally returns the length
 * of the string found in $oLen
 */
protected function indexOfFirst($str, $findArr, &$oLen=null){
	$posArr = array();
	foreach ($findArr as $f){
		$posArr[] = array(strpos($str, $f), strlen($f));
	}

	$finalPos = false;
	$oLen = null;
	foreach ($posArr as $p){
		if ($p[0] !== false){
			if ($finalPos === false || $finalPos > $p[0]){
				$finalPos = $p[0];
				$oLen = $p[1];
			}
		}
	}

	return $finalPos;
}

/**
 * Read up to (and including) the next \r\n from the input buffer
 * Returns false if a full line cannot be read.
 *
 */
protected function readLine(&$line){
	$pos = $this->indexOfFirst($this->mReadBuffer, array("\r\n", "\n", "\r"), $len);
	if ($pos !== false){
		$end = $pos+$len;
		$line = substr($this->mReadBuffer, 0, $end);
		$this->mReadBuffer = substr($this->mReadBuffer, $end);
		return true;
	}

	return false;
}

/**
 * Reads exactly the specified number of bytes
 */
protected function readExactly($bytes, &$output){
	if (strlen($this->mReadBuffer) > $bytes){
		$output = substr($this->mReadBuffer, 0, $bytes);
		$this->mReadBuffer = substr($this->mReadBuffer, $bytes);
		return true;
	}
	return false;
}

/**
 * Peeks at the buffer by reading the requested data but not removing it from the buffer.
 *
 */
protected function peekExactly($bytes, &$output){
	if (strlen($this->mReadBuffer) > $bytes){
		$output = substr($this->mReadBuffer, 0, $bytes);
		return true;
	}
	return false;
}

/**
 * Read the opening handshake from other stream resources
 *
 */
protected function readHeaders(){
	$endOfHeaders=false;
	do {
		$this->fillReadBuffer();
		$curHeader=array();
		while (!$endOfHeaders && $this->readLine($line)){
			$line = rtrim($line, "\r\n");
			if ($line==''){
				$endOfHeaders=true;
			}
			else {
				if (substr($line, 0, 3) == 'GET'){
					$resourceStart = strpos($line, ' ')+1;
					$resourceEnd = strpos($line, ' ', $resourceStart);
					$this->mRequestUri = substr($line, $resourceStart, $resourceEnd-$resourceStart);
				}
				else {
					if (!ctype_space($line[0])){
						if (!empty($curHeader)){
							$this->mHeaders[$curHeader[0]] = $curHeader[1];
						}

						$pos = strpos($line, ':');
						$curHeader[0] = rtrim(strtoupper(substr($line, 0, $pos)));
						$curHeader[1] = ltrim(substr($line, $pos+1));
					}
					else {
						$curHeader[1] .= ' '.ltrim($line);
					}
				}
			}
		}
	} while (!$endOfHeaders);

	if ($curHeader){
		$this->mHeaders[$curHeader[0]] = $curHeader[1];
	}
}

/**
 * Upgrades the connection, processing the opening handshake and enabling the websocket.
 *
 */
public function DoUpgrade($stream){
	if (!is_resource($stream)){
		throw new InvalidArgumentException('Stream must be specified and must be a resource');
	}

	$this->mStream = $stream;
	if (function_exists('stream_set_read_buffer')){
		stream_set_read_buffer($this->mStream, 0);
	}
	stream_set_write_buffer($this->mStream, 0);
	stream_set_timeout($this->mStream, 2, 0);
	return $this->ProcessHandshake();
}

protected function ProcessHandshake(){
	$this->readHeaders();

	$this->log('Headers are: '.print_r($this->mHeaders, true));

	$responseCode = 101;
	$acceptKey = null;

	if (isset($this->mHeaders['SEC-WEBSOCKET-KEY1']) && isset($this->mHeaders['SEC-WEBSOCKET-KEY2'])){
		$this->mProtoType = self::PROTO_HIXIE;
		$this->mProtoVersion = 76;
	}
	else if (isset($this->mHeaders['SEC-WEBSOCKET-KEY'])){
		$this->mProtoType = self::PROTO_HYBI;
		$this->mProtoVersion = 17;
	}
	else {
		$this->log('Unknown websocket protocol.');
		throw new Exception('Unknown websocket protocol');
	}

	if (isset($this->mHeaders['SEC-WEBSOCKET-VERSION'])){
		$this->mProtoVersion = intval($this->mHeaders['SEC-WEBSOCKET-VERSION']);
	}

	if ($this->mProtoType==self::PROTO_HIXIE){
		while (!$this->readExactly(8, $last8)){ 
			$this->fillReadBuffer();
		}


		$keys = array(null, null, $last8);
		try {
			foreach (array($this->mHeaders['SEC-WEBSOCKET-KEY1'], $this->mHeaders['SEC-WEBSOCKET-KEY2']) as $n=>$val){
				$digits = '';
				$spaces = 0;
				for ($i=0; $i<strlen($val); $i++){
					$c = $val[$i];
					if (ctype_digit($c)){
						$digits .= $c;
					}
					else if (ctype_space($c)){
						$spaces++;
					}
				}

				if ($spaces==0){
					throw new Exception('No spaces found in key.  Must be at least one.');
				}

				$keys[$n] = floatval(floatval($digits)/$spaces);
			}

			$finalKey = pack('N2a8', intval($keys[0]), intval($keys[1]), $keys[2]);
			$acceptKey = md5($finalKey, true);
		}
		catch (Exception $e){
			$this->log('Unable to generate accept key.');
			$responseCode=400;
			break;
		}
	}
	else {
		$key = trim($this->mHeaders['SEC-WEBSOCKET-KEY']);
		$key = $key.'258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
		$acceptKey = base64_encode(sha1($key, true));
	}

	if (!isset($this->mHeaders['UPGRADE']) || strcasecmp($this->mHeaders['UPGRADE'], 'websocket') != 0){
		$this->log('No or Bad upgrade header');
		$this->log('$this->mHeaders[uPGRADE] = '.$this->mHeaders['UPGRADE']);
		$this->log('strcasecmp = '.strcasecmp($this->mHeaders['UPGRADE'], 'websocket'));
		var_dump($this->mHeaders['UPGRADE']);
		$responseCode = 400;
	}

	/*if (isset($this->mHeaders['SEC-WEBSOCKET-PROTOCOL'])){
		$requestedProtos = explode(',', $this->mHeaders['SEC-WEBSOCKET-PROTOCOL']);
		$hasValidProto = false;
		foreach ($requestedProtos as $p){
			if ($this->mServer->IsValidProtocol($p)){
				$hasValidProto = true;
			}
		}

		if (!$hasValidProto){
			$responseCode = 406;
			break;
		}
	}*/

	if (!$acceptKey){
		$responseCode=500;
	}

	$origin = isset($this->mHeaders['ORIGIN'])?$this->mHeaders['ORIGIN']:'null';
	$resource = sprintf('ws://%s%s', $this->mHeaders['HOST'], $this->mRequestUri);

	$this->log('Response Code is '.$responseCode);
	switch ($responseCode){
		case 101:
			$response  = "HTTP/1.1 101 Upgraded\r\n";
			$response .= "Upgrade: WebSocket\r\n";
			$response .= "Connection: Upgrade\r\n";
			$response .= "Sec-WebSocket-Origin: {$origin}\r\n";
			$response .= "Sec-WebSocket-Location: {$resource}\r\n";
			if (isset($this->mHeaders['SEC-WEBSOCKET-PROTOCOL'])){
				$response .= "Sec-WebSocket-Protocol: {$this->mHeaders['SEC-WEBSOCKET-PROTOCOL']}\r\n";
			}

			if ($this->mProtoType==self::PROTO_HIXIE){
				$response .= "\r\n{$acceptKey}";
			}
			else {
				$response .= "Sec-WebSocket-Accept: {$acceptKey}\r\n";
				$response .= "\r\n";
			}
			break;
		case 400:
			$response  = "HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n";
			break;

		case 406:
			$response  = "HTTP/1.1 406 Not Acceptable\r\nConnection: close\r\n\r\n";
			break;

		case 500:
		default:
			$response  = "HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\n\r\n";
			break;
	}

	$this->WriteStream($response);


	if ($responseCode == 101){
		$this->mState = self::ESTABLISHED;
		return true;
	}
	else {
		fclose($this->mStream);
		$this->mStream = null;
		$this->mState = self::CLOSED;
		return false;
	}
}

public function Close($force=false){
	if (!$this->mCloseStartedOn){
		$this->mCloseStartedOn=time();
		$this->log('Starting close at: '.$this->mCloseStartedOn);
	}

	if ($force || ($this->mSentClose && $this->mReceivedClose) || time()-$this->mCloseStartedOn > 10){
		if ($this->mStream) fclose($this->mStream);

		$this->mState = self::CLOSED;
		$this->mStream = null;
		$this->log('Shutdown stream, socket is closed.');
	}
	else {
		$this->mState = self::CLOSING;
		if (!$this->mSentClose){
			$this->log('Sending close frame.');
			$m = new WebSocketMessage(WebSocketMessage::TYPE_CLOSE);
			$this->SendMessage($m);
		}
	}
}

public function GetMessage(){
	$this->fillReadBuffer();
	$m = $this->ReadMessage();

	if ($m && $m->Type == WebSocketMessage::TYPE_CLOSE){
		$this->mReceivedClose = true;
		$this->Close();
		$m = null;
	}

	return $m;
}

public function SendMessage(WebSocketMessage $msg){
	if ($this->mProtoType == self::PROTO_HYBI){
		$this->sendHybiMessage($msg);
	}
	else {
		$this->sendHixieMessage($msg);
	}

	if ($msg->Type == WebSocketMessage::TYPE_CLOSE){
		$this->mSentClose=true;
		$this->Close();
	}
}

protected function ReadMessage(){
	$buff = $this->mReadBuffer;
	$ret=null;

	if ($this->mProtoType == self::PROTO_HYBI){
		$ret=$this->readHybiMessage($buff);
	}
	else {
		$ret=$this->readHixieMessage($buff);
	}

	if ($ret){
		$this->mReadBuffer = $buff;
	}

	return $ret;
}

protected function readHixieMessage(&$buffer){
	$ret = null;

	if (strlen($buffer) > 0){
		$type = ord($buffer[0]);
		if (($type & 0x80) == 0x80 && isset($buffer[1])){
			$length = 0;
			$byteNum=1;

			do {
				$lenByte=ord($buffer[$byteNum++]);
				$length = ($length*128)+($lenByte&0x7F);
			} while (isset($buffer[$byteNum]) && ($lenByte&0x80)==0x80);


			if ($type == 0xFF && $length == 0){
				$ret = new WebSocketMessage(WebSocketMessage::TYPE_CLOSE, null);
				$buffer = substr($buffer, min(strlen($buffer),2));
			}
			else if ($length > 0){
				$length = min(strlen($buffer),$length);
				$buffer = substr($buffer, $length);
			}
		}
		else if (($type&0x80) == 0){
			$payload=array();
			$byteNum=1;
			while (isset($buffer[$byteNum]) && ord($buffer[$byteNum]) != 0xFF){
				$payload[] = $buffer[$byteNum++];
			}

			if ($type == 0){
				$ret = new WebSocketMessage(WebSocketMessage::TYPE_TEXT, implode('', $payload), count($payload));
				$buffer = substr($buffer, $byteNum+1);
			}
		}
	}

	return $ret;
}

protected function readHybiMessage(&$buffer){
	$ret = null;

	do {
		if (strlen($buffer) > 2){
			$bytes = substr($buffer, 0, 2);
			$buffer = substr($buffer, 2);

			$byte = ord($bytes[0]);
			$flags = ($byte&0xF0)>>4;
			$opcode = $byte&0x0F;

			$byte = ord($bytes[1]);
			$ismasked = ($byte&0x80)>>7;
			$payloadLen = $byte&0x7F;

			$this->log(sprintf("Flags: %d; Opcode: %d; Masked: %d; Length: %d", $flags, $opcode, $ismasked, $payloadLen));

			$validLen=false;
			switch ($payloadLen){
				case '126':
					if (strlen($buffer) > 2){
						$payloadLenBytes = substr($buffer, 0, 2);
						$buffer = substr($buffer, 2);

						$payloadLen = unpack('nlen', $data);
						$payloadLen = $payloadLen['len'];

						$validLen=true;
					}
					break;
				case '127':
					if (strlen($buffer) > {
						$payloadLenBytes = substr($buffer, 0, ;
						$buffer = substr($buffer, ;

						$data = unpack("Nhi/Nlo", $payloadLenBytes);

						$payloadLen = ($data['hi']<<32)|$data['lo'];
						$validLen=true;
					}
					break;
				default:
					/* $payloadLen is itself. */
			}

			if ($opcode == 0x8){
				//Disable mask on closing frames
				$ismasked=0;
			}

			if ($ismasked && strlen($buffer) > 4){
				$maskKey = substr($buffer, 0, 4);
				$buffer = substr($buffer, 4);

				$this->log('Mask key: '.$maskKey);
			}
			else if ($ismasked){
				break; //No mask given, but one should be given.
			}

			if (strlen($buffer) >= $payloadLen){
				$payload = substr($buffer, 0, $payloadLen);
				$buffer = substr($buffer, $payloadLen);


				//$this->log('Payload (pre-mask): '.$payload);
				if ($ismasked){
					$payload = $this->applyMask($payload, $payloadLen, $maskKey);
					//$this->log('Payload (post-mask): '.$payload);
				}

				$ret = new WebsocketMessage($opcode, $payload, $payloadLen);
				if ($flags&0x8 == 0){
					$obj = $this->readHybiMessage($buffer);
					if ($obj){
						$ret->Data .= $obj->Data;
						$ret->DataLength += $obj->DataLength;
						$buffer = $newBuffer;
					}
				}
			}
		}
	} while (false);

	return $ret;
}

protected function applyMask($payload, $payloadLen, $mask){
	$newPayload='';

	for ($i=0; $i<$payloadLen; $i++){
		$keyByte = unpack('cchar', $mask[$i%4]);
		$keyByte = $keyByte['char'];

		$newPayload .= chr(ord($payload[$i]) ^ $keyByte);
	}

	return $newPayload;
}

protected function sendHybiMessage($msg){
	$len = $msg->DataLength; 
	if ($len > 125){
		$len = 126;
	}

	$packet = pack('cc', 0x80 | $msg->Type, 0x00 | $len);

	if ($len == 126){
		$packet .= pack('n', $msg->DataLength);
	}

	$packet .= $msg->Data;

	$this->WriteStream($packet);
}

protected function sendHixieMessage($msg){
	if ($msg->Type == WebSocketMessage::TYPE_CLOSE){
		$packet = chr(0xff).chr(0x00);
	}
	else {
		$utf8 = iconv('ISO-8859-1', 'UTF-8', $msg->Data);
		$packet = chr(0).$utf8.chr(0xFF);
	}

	$this->WriteStream($packet);
}

}

 

ILogger.interface.php

<?php

interface ILogger {
public function Log($str);
}

 

WebSocketMessage.class.php

<?php
class WebSocketMessage {
public $Data;
public $DataLength;
public $Type;

const TYPE_TEXT = 0x1;
const TYPE_BINARY = 0x2;
const TYPE_CLOSE = 0x8;

public function __construct($Type, $Data='', $Len=null){
	$this->Data = $Data;
	$this->Type = $Type;
	$this->DataLength = $Len?:strlen($Data);
}
public function __get($nm){ throw new Exception('Unknown property '.$nm); }
public function __set($nm,$v){ throw new Exception('Unknown property '.$nm); }

}

 

And a sample server script:

<?php 

require 'WebSocketConnection.class.php';

class EchoLogger implements ILogger {
public function log($str){
	echo rtrim($str), "\r\n";
}
}


$server=stream_socket_server('tcp://0.0.0.0:8010');
echo "Listening for connections\r\n";
while ($client=stream_socket_accept($server)){
$ws = new WebsocketConnection();
$ws->addLogger(new EchoLogger());
echo "Upgrading connection\r\n";
if ($ws->DoUpgrade($client)){
	echo "Upgrade complete, entering loop\r\n";
	do {
		$r = array($ws->GetStream());
		$w = $e = null;
		if (stream_select($r, $w, $e, 10, 0) > 0){
			if ($m = $ws->GetMessage()){
				$m->Data = md5($m->Data);
				$m->DataLength = strlen($m->Data);

				$ws->SendMessage($m);
			}
		}
	} while($ws->IsConnected());
}
else {
	echo "Upgrade failed\r\n";
}
}

Link to comment
Share on other sites

Thank you also DWilliams , I was writing a PHP Websocket tutorial  for my colleagues and I just realized from your code that  when I decoded from hybi 10 I just ignored close , ping and pong types. When I will finish it I will open new thread since I have many concerns about server performance using Websockets. Again thank you all.

Link to comment
Share on other sites

  • 6 months later...

Thanks for the readHybiMessage!

 

In case anyone else reads and tries to use that code please be aware of the typo in WebSocketConnection.class.php

 

in the case '126' in the readHybiMessage it should be

 

$payloadLen = unpack('nlen', $payloadLenBytes);

 

instead of

 

$payloadLen = unpack('nlen', $data);

 

 

 

 

 

and yes, i registered only to thank and come with the correction  =)

Link to comment
Share on other sites

This is not mine, i just found it on my hd and thought it might help. not sure if it even works.

i came across it once, and thought i should save it.

 

<?php
error_reporting(E_ALL);
set_time_limit(0);
ob_implicit_flush();
date_default_timezone_set('America/Chicago');

$master  = WebSocket('192.168.1.29', 12345);
$sockets = array($master);
$users   = array();

while(true)
{
$changed = $sockets;
socket_select($changed,$write=NULL,$except=NULL,NULL);
foreach($changed as $socket)
{
	if($socket == $master)
	{
		$client = socket_accept($master);
		if($client < 0)
		{
			server_log('socket_accept() failed', 2); 
			continue; 
		}
		else
		{ 
			connect($client); 
		}
	}
	else
	{
		$bytes = socket_recv($socket,$buffer,2048,0);
		if($bytes==0)
		{
			disconnect($socket);
		}
		else
		{	
			$user = getuserbysocket($socket);

			if(!$user->handshake)
			{
				dohandshake($user,$buffer);
			}
  		      else
  	  		   {
  	  		   	// Decode the WebSocket data and process accordingly
  	  		   	$data = hybi10Decode($buffer);
  	  		   	
  	  		   	if($data['type'] == 'text')
  	  		   	{
					process($user, $data['payload']); 
				}
				elseif($data['type'] == 'ping')
				{

				}
				elseif($data['type'] == 'pong')
				{

				}
				elseif($data['type'] == 'close')
				{

				}
			}
		}
	}
}
}

//---------------------------------------------------------------
function server_log($msg, $sev = 1)
{
echo '[' . date('G:i:s') . "] $msg\n";	
}

function process($user, $msg)
{
server_log("Message received: ". $msg);
}

function send($client,$msg)
{
server_log("> ".$msg, 1);
$msg = hybi10Encode($msg, 'close');
socket_write($client,$msg,strlen($msg));
}

function WebSocket($address,$port)
{
$master=socket_create(AF_INET, SOCK_STREAM, SOL_TCP)     or die("socket_create() failed");
socket_set_option($master, SOL_SOCKET, SO_REUSEADDR, 1)  or die("socket_option() failed");
socket_bind($master, $address, $port)                    or die("socket_bind() failed");
socket_listen($master,20)                                or die("socket_listen() failed");
server_log('Server Started : ' . date('Y-m-d H:i:s'), 1);
server_log("Master socket  : $master", 1);
server_log("Listening on   : {$address}:{$port}", 1);
return $master;
}

function connect($socket)
{
global $sockets,$users;
$user = new User();
$user->id = uniqid();
$user->socket = $socket;
array_push($users,$user);
array_push($sockets,$socket);
server_log('New client connected', 1);
}

function disconnect($socket){
  global $sockets,$users;
  $found=null;
  $n=count($users);
  for($i=0;$i<$n;$i++){
    if($users[$i]->socket==$socket){ $found=$i; break; }
  }
  if(!is_null($found)){ array_splice($users,$found,1); }
  $index = array_search($socket,$sockets);
  socket_close($socket);
  console($socket." DISCONNECTED!");
  if($index>=0){ array_splice($sockets,$index,1); }
}

function dohandshake($user, $buffer)
{
server_log('Requesting handshake...', 1);

// Determine which version of the WebSocket protocol the client is using
if(preg_match("/Sec-WebSocket-Version: (.*)\r\n/ ", $buffer, $match))
	$version = $match[1];
else 
	return false;

if($version == 
{
	// Extract header variables
	if(preg_match("/GET (.*) HTTP/"   ,$buffer,$match)){ $r=$match[1]; }
	if(preg_match("/Host: (.*)\r\n/"  ,$buffer,$match)){ $h=$match[1]; }
	if(preg_match("/Sec-WebSocket-Origin: (.*)\r\n/",$buffer,$match)){ $o=$match[1]; }
	if(preg_match("/Sec-WebSocket-Key: (.*)\r\n/",$buffer,$match)){ $k = $match[1]; }

	// Generate our Socket-Accept key based on the IETF specifications
	$accept_key = $k . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
	$accept_key = sha1($accept_key, true);
	$accept_key = base64_encode($accept_key);

	$upgrade =	"HTTP/1.1 101 Switching Protocols\r\n" .
					"Upgrade: websocket\r\n" .
					"Connection: Upgrade\r\n" .
					"Sec-WebSocket-Accept: $accept_key\r\n\r\n";

	socket_write($user->socket, $upgrade, strlen($upgrade));
	$user->handshake = true;
	return true;
}
else 
{
	server_log("Client is trying to use an unsupported WebSocket protocol ({$version})", 1);
	return false;
}
}

function getuserbysocket($socket)
{
global $users;
$found = null;
foreach($users as $user)
{
	if($user->socket==$socket)
	{
		$found = $user; 
		break; 
	}
}
  return $found;
}

function hybi10Encode($payload, $type = 'text', $masked = true)
{
$frameHead = array();
$frame = '';
$payloadLength = strlen($payload);

switch($type)
{
	case 'text':
	// first byte indicates FIN, Text-Frame (10000001):
	$frameHead[0] = 129;
	break;

	case 'close':
	// first byte indicates FIN, Close Frame(10001000):
	$frameHead[0] = 136;
	break;

	case 'ping':
	// first byte indicates FIN, Ping frame (10001001):
	$frameHead[0] = 137;
	break;

	case 'pong':
	// first byte indicates FIN, Pong frame (10001010):
	$frameHead[0] = 138;
	break;
}

// set mask and payload length (using 1, 3 or 9 bytes)
if($payloadLength > 65535)
{
	$payloadLengthBin = str_split(sprintf('%064b', $payloadLength), ;
	$frameHead[1] = ($masked === true) ? 255 : 127;
	for($i = 0; $i < 8; $i++)
	{
		$frameHead[$i+2] = bindec($payloadLengthBin[$i]);
	}

	// most significant bit MUST be 0 (close connection if frame too big)
	if($frameHead[2] > 127)
	{
		$this->close(1004);
		return false;
	}
}
elseif($payloadLength > 125)
{
	$payloadLengthBin = str_split(sprintf('%016b', $payloadLength), ;
	$frameHead[1] = ($masked === true) ? 254 : 126;
	$frameHead[2] = bindec($payloadLengthBin[0]);
	$frameHead[3] = bindec($payloadLengthBin[1]);
}
else
{
	$frameHead[1] = ($masked === true) ? $payloadLength + 128 : $payloadLength;
}

// convert frame-head to string:
foreach(array_keys($frameHead) as $i)
{
	$frameHead[$i] = chr($frameHead[$i]);
}

if($masked === true)
{
	// generate a random mask:
	$mask = array();
	for($i = 0; $i < 4; $i++)
	{
		$mask[$i] = chr(rand(0, 255));
	}

	$frameHead = array_merge($frameHead, $mask);
}
$frame = implode('', $frameHead);

// append payload to frame:
$framePayload = array();
for($i = 0; $i < $payloadLength; $i++)
{
	$frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i];
}

return $frame;
}

function hybi10Decode($data)
{
$payloadLength = '';
$mask = '';
$unmaskedPayload = '';
$decodedData = array();

// estimate frame type:
$firstByteBinary = sprintf('%08b', ord($data[0]));
$secondByteBinary = sprintf('%08b', ord($data[1]));
$opcode = bindec(substr($firstByteBinary, 4, 4));
$isMasked = ($secondByteBinary[0] == '1') ? true : false;
$payloadLength = ord($data[1]) & 127;

// close connection if unmasked frame is received:
if($isMasked === false)
{
	$this->close(1002);
}

switch($opcode)
{
	// text frame:
	case 1:
	$decodedData['type'] = 'text';
	break;

	// connection close frame:
	case 8:
	$decodedData['type'] = 'close';
	break;

	// ping frame:
	case 9:
	$decodedData['type'] = 'ping';
	break;

	// pong frame:
	case 10:
	$decodedData['type'] = 'pong';
	break;

	default:
	// Close connection on unknown opcode:
	$this->close(1003);
	break;
}

if($payloadLength === 126)
{
	$mask = substr($data, 4, 4);
	$payloadOffset = 8;
}
elseif($payloadLength === 127)
{
	$mask = substr($data, 10, 4);
	$payloadOffset = 14;
}
else
{
	$mask = substr($data, 2, 4);
	$payloadOffset = 6;
}

$dataLength = strlen($data);

if($isMasked === true)
{
	for($i = $payloadOffset; $i < $dataLength; $i++)
	{
		$j = $i - $payloadOffset;
		$unmaskedPayload .= $data[$i] ^ $mask[$j % 4];
	}
	$decodedData['payload'] = $unmaskedPayload;
}
else
{
	$payloadOffset = $payloadOffset - 4;
	$decodedData['payload'] = substr($data, $payloadOffset);
}

return $decodedData;
}

class User{
  var $id;
  var $socket;
  var $handshake;
}

Link to comment
Share on other sites

This thread is more than a year old. Please don't revive it unless you have something important to add.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.