diff --git a/README.md b/README.md index de20ab5..f5d1928 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,11 @@ PHPWEBSOCKET ============ -So here is a quick hack to implement websockets in php. -As of Feb/10 the only browsers that support websockets are [Google Chrome](http://www.google.com/chrome) and [Safari nightlies](http://nightly.webkit.org/) +So here is a quick hack to implement hybi-10 websockets in php! +Check out the [hybi-10 standard here](http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10#section-4.2) +Check [This Wikipedia Page](http://en.wikipedia.org/wiki/WebSocket#Browser_support) for suppored list of browsers. -Changelog ---------- -* 2010.02.18 - Chatbot demo extending websocket class -* 2010.02.18 - Websocket class for better reuse -* 2010.02.15 - Added Users list to keep track of handshakes -* 2010.02.15 - Minor cosmetic changes - Client side ----------- @@ -25,30 +19,19 @@ Client side } catch(ex){ log(ex); } -View source code of [client.html](http://github.com/GeorgeNava/phpwebsocket/blob/master/client.html) +View source code of [client.html](http://github.com/esromneb/phpwebsocket/blob/master/client.html) Server side ----------- - log("Handshaking..."); - list($resource,$host,$origin) = getheaders($buffer); - $upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" . - "Upgrade: WebSocket\r\n" . - "Connection: Upgrade\r\n" . - "WebSocket-Origin: " . $origin . "\r\n" . - "WebSocket-Location: ws://" . $host . $resource . "\r\n" . - "\r\n"; - $handshake = true; - socket_write($socket,$upgrade.chr(0),strlen($upgrade.chr(0))); - -View source code of [server.php](http://github.com/GeorgeNava/phpwebsocket/blob/master/server.php) + View source code of [chatbot.demo.php](http://github.com/esromneb/phpwebsocket/blob/master/chatbot.demo.php) Steps to run the test: ---------------------- - -* Save both files, client.html and server.php, in a folder in your local server running Apache and PHP. -* From the command line, run the server.php program to listen for socket connections. +* [See these instructions](http://net.tutsplus.com/tutorials/javascript-ajax/start-using-html5-websockets-today/) for info on how to run this under windows ( xampp: Apache + PHP ) +* Save both files, client.html and chatbot.demo.php, in a folder in your local server running Apache and PHP. +* From the command line, run the "php -q chatbot.demo.php" program to listen for socket connections. * Open Google Chrome and point to the client.html page * Done, your browser now has a full-duplex channel with the server. * Start sending commands to the server to get some responses. @@ -56,10 +39,11 @@ Steps to run the test: WebSockets for the masses! ========================== -Author +Author ------ -George Nava +Ben Morse +George Nava + -[http://georgenava.appspot.com](http://georgenava.appspot.com) -[http://mylittlehacks.appspot.com](http://mylittlehacks.appspot.com) -[http://twitter.com/georgenava](http://twitter.com/georgenava) +[http://portforwardpodcast.com/](http://portforwardpodcast.com/) +[http://twitter.com/PortFwdPodcast](http://twitter.com/PortFwdPodcast) diff --git a/chatbot.demo.php b/chatbot.demo.php index fa5ff8a..cd8973a 100644 --- a/chatbot.demo.php +++ b/chatbot.demo.php @@ -21,5 +21,4 @@ function process($user,$msg){ } } -$ws = new ChatBot("localhost",12345); -$ws->listen(); +$master = new ChatBot("localhost",12345); diff --git a/server.php b/server.php deleted file mode 100644 index 9fa3ef3..0000000 --- a/server.php +++ /dev/null @@ -1,150 +0,0 @@ -#!/php -q -php -q server.php */ -error_reporting(E_ALL); -set_time_limit(0); -ob_implicit_flush(); - -$master = WebSocket("localhost",12345); -$sockets = array($master); -$users = array(); -$debug = false; - -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){ console("socket_accept() failed"); 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{ process($user,$buffer); } - } - } - } -} - -//--------------------------------------------------------------- -function process($user,$msg){ - $action = unwrap($msg); - say("< ".$action); - switch($action){ - case "hello" : send($user->socket,"hello human"); break; - case "hi" : send($user->socket,"zup human"); break; - case "name" : send($user->socket,"my name is Multivac, silly I know"); break; - case "age" : send($user->socket,"I am older than time itself"); break; - case "date" : send($user->socket,"today is ".date("Y.m.d")); break; - case "time" : send($user->socket,"server time is ".date("H:i:s")); break; - case "thanks": send($user->socket,"you're welcome"); break; - case "bye" : send($user->socket,"bye"); break; - default : send($user->socket,$action." not understood"); break; - } -} - -function send($client,$msg){ - say("> ".$msg); - $msg = wrap($msg); - 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"); - echo "Server Started : ".date('Y-m-d H:i:s')."\n"; - echo "Master socket : ".$master."\n"; - echo "Listening on : ".$address." port ".$port."\n\n"; - 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); - console($socket." CONNECTED!"); -} - -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){ - console("\nRequesting handshake..."); - console($buffer); - /* - GET {resource} HTTP/1.1 - Upgrade: WebSocket - Connection: Upgrade - Host: {host} - Origin: {origin} - \r\n - */ - list($resource,$host,$origin) = getheaders($buffer); - //$resource = "/phpwebsocketchat/server.php"; - //$host = "localhost:12345"; - //$origin = "http://localhost"; - console("Handshaking..."); - $upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" . - "Upgrade: WebSocket\r\n" . - "Connection: Upgrade\r\n" . - "WebSocket-Origin: " . $origin . "\r\n" . - "WebSocket-Location: ws://" . $host . $resource . "\r\n" . - "\r\n"; - socket_write($user->socket,$upgrade.chr(0),strlen($upgrade.chr(0))); - $user->handshake=true; - console($upgrade); - console("Done handshaking..."); - return true; -} - -function getheaders($req){ - $req = substr($req,4); /* RegEx kill babies */ - $res = substr($req,0,strpos($req," HTTP")); - $req = substr($req,strpos($req,"Host:")+6); - $host = substr($req,0,strpos($req,"\r\n")); - $req = substr($req,strpos($req,"Origin:")+8); - $ori = substr($req,0,strpos($req,"\r\n")); - return array($res,$host,$ori); -} - -function getuserbysocket($socket){ - global $users; - $found=null; - foreach($users as $user){ - if($user->socket==$socket){ $found=$user; break; } - } - return $found; -} - -function say($msg=""){ echo $msg."\n"; } -function wrap($msg=""){ return chr(0).$msg.chr(255); } -function unwrap($msg=""){ return substr($msg,1,strlen($msg)-2); } -function console($msg=""){ global $debug; if($debug){ echo $msg."\n"; } } - -class User{ - var $id; - var $socket; - var $handshake; -} - -?> \ No newline at end of file diff --git a/websocket.class.php b/websocket.class.php index 2874f7e..6e98260 100644 --- a/websocket.class.php +++ b/websocket.class.php @@ -1,14 +1,12 @@ listen(); +// Usage: $master=new WebSocket("localhost",12345); class WebSocket{ var $master; var $sockets = array(); var $users = array(); - var $debug = false; + var $debug = true; function __construct($address,$port){ error_reporting(E_ALL); @@ -20,14 +18,14 @@ function __construct($address,$port){ socket_bind($this->master, $address, $port) or die("socket_bind() failed"); socket_listen($this->master,20) or die("socket_listen() failed"); $this->sockets[] = $this->master; - $this->say("---------------\n"); $this->say("Server Started : ".date('Y-m-d H:i:s')); $this->say("Listening on : ".$address." port ".$port); - $this->say("Master socket : ".$this->master); - $this->say("---------------\n"); - } - - function listen(){ + $this->say("Master socket : ".$this->master."\n"); + if( $this->debug ) + { + $this->say("Debugging on\n"); + } + while(true){ $changed = $this->sockets; socket_select($changed,$write=NULL,$except=NULL,NULL); @@ -56,21 +54,75 @@ function process($user,$msg){ $this->send($user->socket,$msg); } - function send($client,$msg){ - $this->say("> ".$msg); - $msg = $this->wrap($msg); - socket_write($client,$msg,strlen($msg)); - } - function broadcast($msg,$except=null){ - $this->say(">>".$msg); - foreach($this->users as $user){ - if($user->socket==$except){ continue; } /* exclude from broadcast */ - $msg = $this->wrap($msg); - socket_write($user->socket,$msg,strlen($msg)); + + +// unit test for message lengths of 124, 65535, 2^64 +// FIXME hook this up + function test_send($client) + { + $base = "Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + for( $i =0; $i < 10; $i++ ) + { + $this->send($client,$base); + $this->send($client,"".strlen($base)); + $base = $base."a"; } } +// FIXME throw error if message length is longer than 0x7FFFFFFFFFFFFFFF chracters + function send($client,$data){ + $this->say("> ".$data); + + + $header = " "; + $header[0] = chr(0x81); + $header_length = 1; + + //Payload length: 7 bits, 7+16 bits, or 7+64 bits + $dataLength = strlen($data); + + //The length of the payload data, in bytes: if 0-125, that is the payload length. + if($dataLength <= 125) + { + $header[1] = chr($dataLength); + $header_length = 2; + } + elseif ($dataLength <= 65535) + { + // If 126, the following 2 bytes interpreted as a 16 + // bit unsigned integer are the payload length. + + $header[1] = chr(126); + $header[2] = chr($dataLength >> 8); + $header[3] = chr($dataLength & 0xFF); + $header_length = 4; + } + else + { + // If 127, the following 8 bytes interpreted as a 64-bit unsigned integer (the + // most significant bit MUST be 0) are the payload length. + $header[1] = chr(127); + $header[2] = chr(($dataLength & 0xFF00000000000000) >> 56); + $header[3] = chr(($dataLength & 0xFF000000000000) >> 48); + $header[4] = chr(($dataLength & 0xFF0000000000) >> 40); + $header[5] = chr(($dataLength & 0xFF00000000) >> 32); + $header[6] = chr(($dataLength & 0xFF000000) >> 24); + $header[7] = chr(($dataLength & 0xFF0000) >> 16); + $header[8] = chr(($dataLength & 0xFF00 ) >> 8); + $header[9] = chr( $dataLength & 0xFF ); + $header_length = 10; + } + + $result = socket_write($client, $header . $data, strlen($data) + $header_length); + //$result = socket_write($client, chr(0x81) . chr(strlen($data)) . $data, strlen($data) + 2); + if ( !$result ) { + $this->disconnect($client); + $client = false; + } + $this->say("len(".strlen($data).")"); + } + function connect($socket){ $user = new User(); $user->id = uniqid(); @@ -78,6 +130,7 @@ function connect($socket){ array_push($this->users,$user); array_push($this->sockets,$socket); $this->log($socket." CONNECTED!"); + $this->log(date("d/n/Y ")."at ".date("H:i:s T")); } function disconnect($socket){ @@ -92,31 +145,44 @@ function disconnect($socket){ $this->log($socket." DISCONNECTED!"); if($index>=0){ array_splice($this->sockets,$index,1); } } - + function dohandshake($user,$buffer){ $this->log("\nRequesting handshake..."); $this->log($buffer); - list($resource,$host,$origin) = $this->getheaders($buffer); + list($resource,$host,$origin,$key1,$key2,$l8b,$key0) = $this->getheaders($buffer); $this->log("Handshaking..."); - $upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" . + //$port = explode(":",$host); + //$port = $port[1]; + //$this->log($origin."\r\n".$host); + $upgrade = "HTTP/1.1 101 WebSocket Protocol Handshake\r\n" . "Upgrade: WebSocket\r\n" . "Connection: Upgrade\r\n" . - "WebSocket-Origin: " . $origin . "\r\n" . - "WebSocket-Location: ws://" . $host . $resource . "\r\n" . - "\r\n"; - socket_write($user->socket,$upgrade.chr(0),strlen($upgrade.chr(0))); + "Sec-WebSocket-Origin: " . $origin . "\r\n" . + "Sec-WebSocket-Accept: " . $this->calcKeyHybi10($key0) . "\r\n" . "\r\n" ; + + socket_write($user->socket,$upgrade,strlen($upgrade)); $user->handshake=true; $this->log($upgrade); $this->log("Done handshaking..."); return true; } + function calcKeyHybi10($key){ + $CRAZY = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + $sha = sha1($key.$CRAZY,true); + return base64_encode($sha); + } + function getheaders($req){ $r=$h=$o=null; - if(preg_match("/GET (.*) HTTP/" ,$req,$match)){ $r=$match[1]; } - if(preg_match("/Host: (.*)\r\n/" ,$req,$match)){ $h=$match[1]; } - if(preg_match("/Origin: (.*)\r\n/",$req,$match)){ $o=$match[1]; } - return array($r,$h,$o); + if(preg_match("/GET (.*) HTTP/" ,$req,$match)){ $r=$match[1]; } + if(preg_match("/Host: (.*)\r\n/" ,$req,$match)){ $h=$match[1]; } + if(preg_match("/Origin: (.*)\r\n/" ,$req,$match)){ $o=$match[1]; } + if(preg_match("/Sec-WebSocket-Key1: (.*)\r\n/",$req,$match)){ $this->log("Sec Key1: ".$sk1=$match[1]); } + if(preg_match("/Sec-WebSocket-Key2: (.*)\r\n/",$req,$match)){ $this->log("Sec Key2: ".$sk2=$match[1]); } + if(preg_match("/Sec-WebSocket-Key: (.*)\r\n/" ,$req,$match)){ $this->log("new Sec Key2: ".$sk0=$match[1]); } + if($match=substr($req,-8)) { $this->log("Last 8 bytes: ".$l8b=$match); } + return array($r,$h,$o,$sk1,$sk2,$l8b,$sk0); } function getuserbysocket($socket){ @@ -130,9 +196,62 @@ function getuserbysocket($socket){ function say($msg=""){ echo $msg."\n"; } function log($msg=""){ if($this->debug){ echo $msg."\n"; } } function wrap($msg=""){ return chr(0).$msg.chr(255); } - function unwrap($msg=""){ return substr($msg,1,strlen($msg)-2); } -} + // copied from http://lemmingzshadow.net/386/php-websocket-serverclient-nach-draft-hybi-10/ + function unwrap($data="") + { + $bytes = $data; + $dataLength = ''; + $mask = ''; + $coded_data = ''; + $decodedData = ''; + $secondByte = sprintf('%08b', ord($bytes[1])); + $masked = ($secondByte[0] == '1') ? true : false; + $dataLength = ($masked === true) ? ord($bytes[1]) & 127 : ord($bytes[1]); + if($masked === true) + { + if($dataLength === 126) + { + $mask = substr($bytes, 4, 4); + $coded_data = substr($bytes, 8); + } + elseif($dataLength === 127) + { + $mask = substr($bytes, 10, 4); + $coded_data = substr($bytes, 14); + } + else + { + $mask = substr($bytes, 2, 4); + $coded_data = substr($bytes, 6); + } + for($i = 0; $i < strlen($coded_data); $i++) + { + $decodedData .= $coded_data[$i] ^ $mask[$i % 4]; + } + } + else + { + if($dataLength === 126) + { + $decodedData = substr($bytes, 4); + } + elseif($dataLength === 127) + { + $decodedData = substr($bytes, 10); + } + else + { + $decodedData = substr($bytes, 2); + } + } + + return $decodedData; + } + + + +} //class WebSocket class User{ var $id; @@ -140,4 +259,4 @@ class User{ var $handshake; } -?> \ No newline at end of file +?> diff --git a/websocket.demo.php b/websocket.demo.php index 2302ee3..4506461 100644 --- a/websocket.demo.php +++ b/websocket.demo.php @@ -4,5 +4,4 @@ // Basic WebSocket demo echoes msg back to client include "websocket.class.php"; -$ws = new WebSocket("localhost",12345); -$ws->listen(); +$master = new WebSocket("localhost",12345);