From 89d082521ff242ec9ff97407a8ffb7558f9732eb Mon Sep 17 00:00:00 2001 From: Simon Samtleben Date: Sat, 7 Apr 2012 12:06:48 +0200 Subject: [PATCH] FF now using websocket object. Some changes in php-client. Minor bugfixes/improvements. --- README.md | 18 ++- client/client.php | 251 -------------------------------- client/coffee/client.coffee | 8 +- client/coffee/status.coffee | 8 +- server/lib/WebSocket/Server.php | 14 +- server/lib/WebSocket/Socket.php | 55 ++++--- server/server.pem | 48 +++--- server/server.php | 6 +- 8 files changed, 81 insertions(+), 327 deletions(-) delete mode 100644 client/client.php diff --git a/README.md b/README.md index 271dc75..1029917 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,14 @@ PHP WebSocket ============= A websocket server implemented in php. -- Supports websocket draft hybi-10,13 (Currently tested with Chrome 16 and Firefox 9). +- Supports websocket draft hybi-10,13 (Currently tested with Chrome 18 and Firefox 11). - Supports origin-check. - Supports various security/performance settings. - Supports binary frames. (Currently receive only) -- Supports wss (Very Alpha! Chrome only!) +- Supports wss. (Needs valid certificate in Firefox.) - Application module, the server can be extended by custom behaviors. ## Bugs/Todos/Hints -- Optimize whole WSS/TLS stuff -- Optimize readBuffer() method. (Ideas welcome!) - Add support for fragmented frames. ## Server example @@ -23,9 +21,9 @@ This creates a server on localhost:8000 with one Application that listens on `ws // server settings: $server->setCheckOrigin(true); $server->setAllowedOrigin('foo.lh'); - $server->setMaxClients(20); - $server->setMaxConnectionsPerIp(5); - $server->setMaxRequestsPerMinute(50); + $server->setMaxClients(100); + $server->setMaxConnectionsPerIp(20); + $server->setMaxRequestsPerMinute(1000); $server->registerApplication('demo', \WebSocket\Application\DemoApplication::getInstance()); $server->run(); @@ -34,4 +32,8 @@ This creates a server on localhost:8000 with one Application that listens on `ws - [SplClassLoader](http://gist.github.com/221634) by the PHP Standards Working Group - [jQuery](http://jquery.com/) -- [CoffeeScript PHP] (https://github.com/alxlit/coffeescript-php) \ No newline at end of file +- [CoffeeScript PHP] (https://github.com/alxlit/coffeescript-php) + +## Demo + +- Check out http://jitt.li for a sample-project using this websocket server. \ No newline at end of file diff --git a/client/client.php b/client/client.php deleted file mode 100644 index 10e63c8..0000000 --- a/client/client.php +++ /dev/null @@ -1,251 +0,0 @@ - - * @version 2011-10-18 - */ - -class WebsocketClient -{ - private $_Socket = null; - - public function __construct($host, $port, $path = '/') - { - $this->_connect($host, $port, $path); - } - - public function __destruct() - { - $this->_disconnect(); - } - - public function sendData($data, $type = 'text', $masked = true) - { - fwrite($this->_Socket, $this->_hybi10EncodeData($data, $type, $masked)) or die('Error:' . $errno . ':' . $errstr); - $wsData = fread($this->_Socket, 2000); - $retData = $this->_hybi10DecodeData($wsData); - - return $retData; - } - - private function _connect($host, $port, $path) - { - $key = base64_encode($this->_generateRandomString(16, false, true)); - $header = "GET " . $path . " HTTP/1.1\r\n"; - $header.= "Host: ".$host.":".$port."\r\n"; - $header.= "Upgrade: websocket\r\n"; - $header.= "Connection: Upgrade\r\n"; - $header.= "Sec-WebSocket-Key: " . $key . "\r\n"; - $header.= "Sec-WebSocket-Origin: http://foobar.com\r\n"; - $header.= "Sec-WebSocket-Version: 8\r\n"; - - $this->_Socket = fsockopen($host, $port, $errno, $errstr, 2); - fwrite($this->_Socket, $header) or die('Error: ' . $errno . ':' . $errstr); - $response = fread($this->_Socket, 2000); - - preg_match('#Sec-WebSocket-Accept:\s(.*)$#mU', $response, $matches); - $keyAccept = trim($matches[1]); - $expectedResonse = base64_encode(pack('H*', sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'))); - - return ($keyAccept === $expectedResonse) ? true : false; - } - - private function _disconnect() - { - fclose($this->_Socket); - } - - private function _generateRandomString($length = 10, $addSpaces = true, $addNumbers = true) - { - $characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"ยง$%&/()=[]{}'; - $useChars = array(); - // select some random chars: - for($i = 0; $i < $length; $i++) - { - $useChars[] = $characters[mt_rand(0, strlen($characters)-1)]; - } - // add spaces and numbers: - if($addSpaces === true) - { - array_push($useChars, ' ', ' ', ' ', ' ', ' ', ' '); - } - if($addNumbers === true) - { - array_push($useChars, rand(0,9), rand(0,9), rand(0,9)); - } - shuffle($useChars); - $randomString = trim(implode('', $useChars)); - $randomString = substr($randomString, 0, $length); - return $randomString; - } - - private function _hybi10EncodeData($payload, $type = 'text', $masked = true) - { - $frameHead = array(); - $frame = ''; - $payloadLength = strlen($payload); - - switch($type) - { - 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; - - case 'text': - // first byte indicates FIN, Text-Frame (10000001): - $frameHead[0] = 129; - break; - - case 'close': - break; - } - - // set mask and payload length (using 1, 3 or 9 bytes) - if($payloadLength > 65535) - { - $payloadLengthBin = str_split(sprintf('%064b', $payloadLength), 8); - $frameHead[1] = ($masked === true) ? 255 : 127; - for($i = 0; $i < 8; $i++) - { - $frameHead[$i+2] = bindec($payloadLengthBin[$i]); - } - // most significant bit MUST be 0 (return false if to much data) - if($frameHead[2] > 127) - { - return false; - } - } - elseif($payloadLength > 125) - { - $payloadLengthBin = str_split(sprintf('%016b', $payloadLength), 8); - $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; - } - - private function _hybi10DecodeData($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; - - // @TODO: close connection if unmasked frame is received. - - 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: - // @TODO: Close connection on unknown opcode. - 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; - } -} - -$WebSocketClient = new WebsocketClient('127.0.0.1', 8000, '/echo'); -var_dump($WebSocketClient->sendData('test', 'ping')); -unset($WebSocketClient); \ No newline at end of file diff --git a/client/coffee/client.coffee b/client/coffee/client.coffee index 99acfee..41916e4 100644 --- a/client/coffee/client.coffee +++ b/client/coffee/client.coffee @@ -1,10 +1,10 @@ $(document).ready -> log = (msg) -> $('#log').append("#{msg}
") serverUrl = 'ws://127.0.0.1:8000/demo' - if $.browser.mozilla - socket = new MozWebSocket(serverUrl) - else - socket = new WebSocket(serverUrl) + if window.MozWebSocket + socket = new MozWebSocket serverUrl + else if window.WebSocket + socket = new WebSocket serverUrl socket.binaryType = 'blob' socket.onopen = (msg) -> diff --git a/client/coffee/status.coffee b/client/coffee/status.coffee index 17dfe1d..84c3c85 100644 --- a/client/coffee/status.coffee +++ b/client/coffee/status.coffee @@ -1,10 +1,10 @@ $(document).ready -> log = (msg) -> $('#log').prepend("#{msg}
") serverUrl = 'ws://localhost:8000/status' - if $.browser.mozilla - socket = new MozWebSocket(serverUrl) - else - socket = new WebSocket(serverUrl) + if window.MozWebSocket + socket = new MozWebSocket serverUrl + else if window.WebSocket + socket = new WebSocket serverUrl socket.onopen = (msg) -> $('#status').removeClass().addClass('online').html('connected') diff --git a/server/lib/WebSocket/Server.php b/server/lib/WebSocket/Server.php index 8685857..e2015c6 100644 --- a/server/lib/WebSocket/Server.php +++ b/server/lib/WebSocket/Server.php @@ -35,7 +35,7 @@ public function run() while(true) { $changed_sockets = $this->allsockets; - @stream_select($changed_sockets, $write = null, $except = null, 0, 5000); + @stream_select($changed_sockets, $write = null, $except = null, 0, 5000); foreach($changed_sockets as $socket) { if($socket == $this->master) @@ -74,15 +74,19 @@ public function run() } } else - { - $client = $this->clients[(int)$socket]; + { + $client = $this->clients[(int)$socket]; + if(!is_object($client)) + { + unset($this->clients[(int)$socket]); + continue; + } $data = $this->readBuffer($socket); $bytes = strlen($data); if($bytes === 0) { - $client->onDisconnect(); - //$this->removeClientOnError($client); + $client->onDisconnect(); continue; } elseif($data === false) diff --git a/server/lib/WebSocket/Socket.php b/server/lib/WebSocket/Socket.php index 46085d7..555b511 100644 --- a/server/lib/WebSocket/Socket.php +++ b/server/lib/WebSocket/Socket.php @@ -59,34 +59,33 @@ private function createSocket($host, $port) private function applySSLContext() { - // Certificate data: - $dn = array( - "countryName" => "DE", - "stateOrProvinceName" => "none", - "localityName" => "none", - "organizationName" => "none", - "organizationalUnitName" => "none", - "commonName" => "foo.lh", - "emailAddress" => "baz@foo.lh" - ); - - // Generate certificate - $privkey = openssl_pkey_new(); - $cert = openssl_csr_new($dn, $privkey); - $cert = openssl_csr_sign($cert, null, $privkey, 365); - - // Generate PEM file - $pem_passphrase = 'shinywss'; - $pem = array(); - openssl_x509_export($cert, $pem[0]); - openssl_pkey_export($privkey, $pem[1], $pem_passphrase); - $pem = implode($pem); - $pemfile = './server.pem'; - file_put_contents($pemfile, $pem); + $pem_file = './server.pem'; + $pem_passphrase = 'shinywss'; + // Generate PEM file + if(!file_exists($pem_file)) + { + $dn = array( + "countryName" => "DE", + "stateOrProvinceName" => "none", + "localityName" => "none", + "organizationName" => "none", + "organizationalUnitName" => "none", + "commonName" => "foo.lh", + "emailAddress" => "baz@foo.lh" + ); + $privkey = openssl_pkey_new(); + $cert = openssl_csr_new($dn, $privkey); + $cert = openssl_csr_sign($cert, null, $privkey, 365); + $pem = array(); + openssl_x509_export($cert, $pem[0]); + openssl_pkey_export($privkey, $pem[1], $pem_passphrase); + $pem = implode($pem); + file_put_contents($pem_file, $pem); + } // apply ssl context: - stream_context_set_option($this->context, 'ssl', 'local_cert', $pemfile); + stream_context_set_option($this->context, 'ssl', 'local_cert', $pem_file); stream_context_set_option($this->context, 'ssl', 'passphrase', $pem_passphrase); stream_context_set_option($this->context, 'ssl', 'allow_self_signed', true); stream_context_set_option($this->context, 'ssl', 'verify_peer', false); @@ -120,8 +119,8 @@ protected function readBuffer($resource) if($result === false || feof($resource)) { return false; - } - $buffer .= $result; + } + $buffer .= $result; $metadata = stream_get_meta_data($resource); $buffsize = ($metadata['unread_bytes'] > $buffsize) ? $buffsize : $metadata['unread_bytes']; } while($metadata['unread_bytes'] > 0); @@ -136,7 +135,7 @@ public function writeBuffer($resource, $string) $stringLength = strlen($string); for($written = 0; $written < $stringLength; $written += $fwrite) { - $fwrite = fwrite($resource, substr($string, $written)); + $fwrite = @fwrite($resource, substr($string, $written)); if($fwrite === false) { return false; diff --git a/server/server.pem b/server/server.pem index e6bad93..be0e97b 100644 --- a/server/server.pem +++ b/server/server.pem @@ -2,34 +2,34 @@ MIICsDCCAhmgAwIBAgIBADANBgkqhkiG9w0BAQQFADB1MQswCQYDVQQGEwJERTEN MAsGA1UECAwEbm9uZTENMAsGA1UEBwwEbm9uZTENMAsGA1UECgwEbm9uZTENMAsG A1UECwwEbm9uZTEPMA0GA1UEAwwGZm9vLmxoMRkwFwYJKoZIhvcNAQkBFgpiYXpA -Zm9vLmxoMB4XDTEyMDIwNDE1MjA1NloXDTEzMDIwMzE1MjA1NlowdTELMAkGA1UE +Zm9vLmxoMB4XDTEyMDQwNzA5MzEwNVoXDTEzMDQwNzA5MzEwNVowdTELMAkGA1UE BhMCREUxDTALBgNVBAgMBG5vbmUxDTALBgNVBAcMBG5vbmUxDTALBgNVBAoMBG5v bmUxDTALBgNVBAsMBG5vbmUxDzANBgNVBAMMBmZvby5saDEZMBcGCSqGSIb3DQEJ -ARYKYmF6QGZvby5saDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA24+O1B3p -aTjNNC4HHAGW5ebgcwp0IC6yOT4/HgN3+p9L1DG+mVVmhOaly2ezLF8JficZRzcQ -PvPfeQVdw8PaZQatV9lX7fqHbnTHKvwr/rO3PRfgcqUC5eGcFgXm2HXb2TUPM9mG -+zeBgePrAdZVhTSzFHMo0y9fILJyePnZDIkCAwEAAaNQME4wHQYDVR0OBBYEFE8Z -Bc0UIGxuczOj4mJpaIfsjWvpMB8GA1UdIwQYMBaAFE8ZBc0UIGxuczOj4mJpaIfs -jWvpMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAYg/ZAiyzeN0mvD9E -yE7gqke4MFz7pH4vwc2APngXTAOEBmYh/hklneNRAaJMdJ7fg4Bo00OEIUX8THh9 -1o4qxWVx40DbAYnaGHMTe5OhjH1jlQSIEmveSrrv08r4esvsjLogN4pMcYI4M8XV -OPGUHlFoxVdcYuSfwNfh0J6a4Hs= +ARYKYmF6QGZvby5saDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA7CDQtHvJ +RE+sy2f3zKyGrMpffcf7fvbaEwT3tEpFpOJOp5qaq6JRc961O6v72++tr8iRVlfO +MGYV5JavKBe0PLzR+tHa/+eigcvjujFsPZqTP+8zkmkOKQIsKjmtpQBKYGgqcDDR +Jv7xYASVBl3/6LuIaD1hjk+r6DH7uqmcA2cCAwEAAaNQME4wHQYDVR0OBBYEFNH0 +dPlR4RSosYd26yWaIvCeGN4kMB8GA1UdIwQYMBaAFNH0dPlR4RSosYd26yWaIvCe +GN4kMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAGeqrcS9tZ7kTYrEe +sT+iPtnqky8Sxu3znxkyjWKymDvELtc4gO17JaoFsQ+7PQBf22EzSgzuqlpk2gGa +pNaMwoS2o/q/Htm7KuGVP0yNEVCYAKNwutFynrn+pd87Pr/+Oq4ighvM8wEzLPl1 +OKSgTOSRdr80EdoSzgBKDD8gyuw= -----END CERTIFICATE----- -----BEGIN RSA PRIVATE KEY----- Proc-Type: 4,ENCRYPTED -DEK-Info: DES-EDE3-CBC,DC30D5B1F5F49E5A +DEK-Info: DES-EDE3-CBC,DE4E623117398BE5 -rj5iDsjUEZLbuQQs87N3b2xsFPmaGhBdM20UYDRd9df4exvwKffBYH3Tvvz9T3Dg -h/ba66Z6wRDQHPWfWPDmL0/hWirnVkkQzJoqTAm+jwQoL8NmKvIt3qBqCCFYUxzK -M3K8ex6HMNUcwP7xKRPTjvYsV0SKjoTBJ2XCB4AGoVzdMALi2JY9+SWbBVLoIGRS -dXR2dbAbX9XCjbtWzvU6pvjkJHuCi6UHNZPYNOeMNNz962/uVWYgYhlhdNzZYxJy -dpUHhD3CTIqcE3ZtyYhr1+/TUEOJA8y+arCDJhKgsvdvlDaVJEnhgdTvW5TAcWRq -FN/UdlwhtO4+i6eShN/MOFNXDC+dkHgZZ9MquP/m7WtbVBSCrIEsqCeIuS2CcwqI -UWMY4zlWi7LsDdqIGg0J7bgptF2z/WO3k6dr2/8NvWHNtI7pZUcrbQUSAt4yRp+R -Cjw5tPRWseHfE9V6Jf0Y8KWARsI1qCJKVDGEN/k8VnkYnooX7sN7rAxr/AtowS1I -w/2pwZex5MfbZ5kWZI8JcUeC+XN2WKgCj72YIRwiErVvGwKi4jZ3c86z6hkBXWUu -OBaEpU/asblZnhEj7SkMH0r/m9qKPDdbeXVtDsaGb7rukS4r/GohhfpmOxHfEbmB -qzYVA3jCpNG6aj1Ja8FDg4ACaofeqOiFGSj03CJNvlR1SdmQTguB/gmeom47lxbo -Q7WScIEZ1N3o9LmtmmtwqfFjp3WsW7pZUYSEBIMiu8ec9XYKqjEaBJhynCvObCxg -seS2N75gCCfLxOHAVDeTc9PGLyDyy4b3ai485asDFfrL+hBOexmaKg== +2FRyRzTeOSijgSCcvnBChbJJHMHkhAlB8Sxbo4fxeAIKyTGx9lyVZTtGN/XVWviK +XDh/7rL5qZz0zJcHshMg1pypNsJ+dSfF3KigpXxHm3Fpb5GnCDj3UKVFxOWPwu2K +ro9RyUdEdOwou2o1TnYnimgEmF7pb1CJFtR/sN1lZ+J/XUh8wfbn2Obce9GCtXGB +P+nVJECELVqX9KgquT49PKpIvf9gLq0Npns5P1lMenR4KGUlCGhFqgnuevPBaHaQ +yRDQfl/qnCehiA59KpCBPyNBPn2hsfpsN+AcnTkt6zd8wWjQA4SlyI1koSxG6FE7 +OjPYsAhpQRi5qVWDyp0jkZqnU42aCGe7CqXXsdOfZv1m0wWACAvxJzV5uWljk/aD +7+k/WcTHHsNF05FYD2eQOReQ75UnOvKF7EmDO0iSJ6NaH9kHg7bvCxPD2oQGMqU8 +ISeHhnI7URcyUBJZnEBCC2eJW6R2m/mI78P1KpOoCg4YbdB/ByGCyqDl134cv243 +Knja0TV5mmTkdXWnUdSAQX0RJpohfOGomYFeeN7TqoqZxQUfFGHNgQP3oNoEzYay +mMfvBk80TRuRD1UpEsg8f+4xw+F6L82rRnfLgNr7FKJYDSTpVsJ3L+xsmyP0hjNw +b10iH/iOd2RprzOZ4YS+LdLLhOBJ97BciBTm71j1h0yLQuGWVLdsrisiyb22luZp +jU5mF0+wLIxCVl+k/Ibk3UYbBlvGblJzVEOnR2BOf3P3C6Skvr2JvDP/74Si/ybI +idm+Igm+TnUQ2PIntjV0wkiJIEQY5XyDwt58TR99w2Y= -----END RSA PRIVATE KEY----- diff --git a/server/server.php b/server/server.php index fb83bd0..6c94283 100644 --- a/server/server.php +++ b/server/server.php @@ -16,11 +16,11 @@ $server = new \WebSocket\Server('127.0.0.1', 8000, false); // server settings: -$server->setMaxClients(20); +$server->setMaxClients(100); $server->setCheckOrigin(true); $server->setAllowedOrigin('foo.lh'); -$server->setMaxConnectionsPerIp(5); -$server->setMaxRequestsPerMinute(50); +$server->setMaxConnectionsPerIp(100); +$server->setMaxRequestsPerMinute(2000); // Hint: Status application should not be removed as it displays usefull server informations: $server->registerApplication('status', \WebSocket\Application\StatusApplication::getInstance());