1 <?php
2
3 /*
4 Based on PHP WebSocket Server 0.2
5 - http://code.google.com/p/php-websocket-server/
6 - http://code.google.com/p/php-websocket-server/wiki/Scripting
7
8 WebSocket Protocol 07
9 - http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-07
10 - Supported by Firefox 6 (30/08/2011)
11
12 Whilst a big effort is made to follow the protocol documentation, the current script version may unknowingly differ.
13 Please report any bugs you may find, all feedback and questions are welcome!
14 */
15
16
17 class PHPWebSocket
18 {
19 // maximum amount of clients that can be connected at one time
20 const WS_MAX_CLIENTS = 100;
21
22 // maximum amount of clients that can be connected at one time on the same IP v4 address
23 const WS_MAX_CLIENTS_PER_IP = 15;
24
25 // amount of seconds a client has to send data to the server, before a ping request is sent to the client,
26 // if the client has not completed the opening handshake, the ping request is skipped and the client connection is closed
27 const WS_TIMEOUT_RECV = 10;
28
29 // amount of seconds a client has to reply to a ping request, before the client connection is closed
30 const WS_TIMEOUT_PONG = 5;
31
32 // the maximum length, in bytes, of a frame's payload data (a message consists of 1 or more frames), this is also internally limited to 2,147,479,538
33 const WS_MAX_FRAME_PAYLOAD_RECV = 100000;
34
35 // the maximum length, in bytes, of a message's payload data, this is also internally limited to 2,147,483,647
36 const WS_MAX_MESSAGE_PAYLOAD_RECV = 500000;
37
38
39
40
41 // internal
42 const WS_FIN = 128;
43 const WS_MASK = 128;
44
45 const WS_OPCODE_CONTINUATION = 0;
46 const WS_OPCODE_TEXT = 1;
47 const WS_OPCODE_BINARY = 2;
48 const WS_OPCODE_CLOSE = 8;
49 const WS_OPCODE_PING = 9;
50 const WS_OPCODE_PONG = 10;
51
52 const WS_PAYLOAD_LENGTH_16 = 126;
53 const WS_PAYLOAD_LENGTH_63 = 127;
54
55 const WS_READY_STATE_CONNECTING = 0;
56 const WS_READY_STATE_OPEN = 1;
57 const WS_READY_STATE_CLOSING = 2;
58 const WS_READY_STATE_CLOSED = 3;
59
60 const WS_STATUS_NORMAL_CLOSE = 1000;
61 const WS_STATUS_GONE_AWAY = 1001;
62 const WS_STATUS_PROTOCOL_ERROR = 1002;
63 const WS_STATUS_UNSUPPORTED_MESSAGE_TYPE = 1003;
64 const WS_STATUS_MESSAGE_TOO_BIG = 1004;
65
66 const WS_STATUS_TIMEOUT = 3000;
67
68 // global vars
69 public $wsClients = array();
70 public $wsRead = array();
71 public $wsClientCount = 0;
72 public $wsClientIPCount = array();
73 public $wsOnEvents = array();
74
75 /*
76 $this->wsClients[ integer ClientID ] = array(
77 0 => resource Socket, // client socket
78 1 => string MessageBuffer, // a blank string when there's no incoming frames
79 2 => integer ReadyState, // between 0 and 3
80 3 => integer LastRecvTime, // set to time() when the client is added
81 4 => int/false PingSentTime, // false when the server is not waiting for a pong
82 5 => int/false CloseStatus, // close status that wsOnClose() will be called with
83 6 => integer IPv4, // client's IP stored as a signed long, retrieved from ip2long()
84 7 => int/false FramePayloadDataLength, // length of a frame's payload data, reset to false when all frame data has been read (cannot reset to 0, to allow reading of mask key)
85 8 => integer FrameBytesRead, // amount of bytes read for a frame, reset to 0 when all frame data has been read
86 9 => string FrameBuffer, // joined onto end as a frame's data comes in, reset to blank string when all frame data has been read
87 10 => integer MessageOpcode, // stored by the first frame for fragmented messages, default value is 0
88 11 => integer MessageBufferLength // the payload data length of MessageBuffer
89 )
90
91 $wsRead[ integer ClientID ] = resource Socket // this one-dimensional array is used for socket_select()
92 // $wsRead[ 0 ] is the socket listening for incoming client connections
93
94 $wsClientCount = integer ClientCount // amount of clients currently connected
95
96 $wsClientIPCount[ integer IP ] = integer ClientCount // amount of clients connected per IP v4 address
97 */
98
99 // server state functions
100 function wsStartServer($host, $port) {
101 if (isset($this->wsRead[0])) return false;
102
103 if (!$this->wsRead[0] = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) {
104 return false;
105 }
106 if (!socket_set_option($this->wsRead[0], SOL_SOCKET, SO_REUSEADDR, 1)) {
107 socket_close($this->wsRead[0]);
108 return false;
109 }
110 if (!socket_bind($this->wsRead[0], $host, $port)) {
111 socket_close($this->wsRead[0]);
112 return false;
113 }
114 if (!socket_listen($this->wsRead[0], 10)) {
115 socket_close($this->wsRead[0]);
116 return false;
117 }
118
119 $write = array();
120 $except = array();
121
122 $nextPingCheck = time() + 1;
123 while (isset($this->wsRead[0])) {
124 $changed = $this->wsRead;
125 $result = socket_select($changed, $write, $except, 1);
126
127 if ($result === false) {
128 socket_close($this->wsRead[0]);
129 return false;
130 }
131 elseif ($result > 0) {
132 foreach ($changed as $clientID => $socket) {
133 if ($clientID != 0) {
134 // client socket changed
135 $buffer = '';
136 $bytes = @socket_recv($socket, $buffer, 4096, 0);
137
138 if ($bytes === false) {
139 // error on recv, remove client socket (will check to send close frame)
140 $this->wsSendClientClose($clientID, self::WS_STATUS_PROTOCOL_ERROR);
141 }
142 elseif ($bytes > 0) {
143 // process handshake or frame(s)
144 if (!$this->wsProcessClient($clientID, $buffer, $bytes)) {
145 $this->wsSendClientClose($clientID, self::WS_STATUS_PROTOCOL_ERROR);
146 }
147 }
148 else {
149 // 0 bytes received from client, meaning the client closed the TCP connection
150 $this->wsRemoveClient($clientID);
151 }
152 }
153 else {
154 // listen socket changed
155 $client = socket_accept($this->wsRead[0]);
156 if ($client !== false) {
157 // fetch client IP as integer
158 $clientIP = '';
159 $result = socket_getpeername($client, $clientIP);
160 $clientIP = ip2long($clientIP);
161
162 if ($result !== false && $this->wsClientCount < self::WS_MAX_CLIENTS && (!isset($this->wsClientIPCount[$clientIP]) || $this->wsClientIPCount[$clientIP] < self::WS_MAX_CLIENTS_PER_IP)) {
163 $this->wsAddClient($client, $clientIP);
164 }
165 else {
166 socket_close($client);
167 }
168 }
169 }
170 }
171 }
172
173 if (time() >= $nextPingCheck) {
174 $this->wsCheckIdleClients();
175 $nextPingCheck = time() + 1;
176 }
177 }
178
179 return true; // returned when wsStopServer() is called
180 }
181 function wsStopServer() {
182 // check if server is not running
183 if (!isset($this->wsRead[0])) return false;
184
185 // close all client connections
186 foreach ($this->wsClients as $clientID => $client) {
187 // if the client's opening handshake is complete, tell the client the server is 'going away'
188 if ($client[2] != self::WS_READY_STATE_CONNECTING) {
189 $this->wsSendClientClose($clientID, self::WS_STATUS_GONE_AWAY);
190 }
191 socket_close($client[0]);
192 }
193
194 // close the socket which listens for incoming clients
195 socket_close($this->wsRead[0]);
196
197 // reset variables
198 $this->wsRead = array();
199 $this->wsClients = array();
200 $this->wsClientCount = 0;
201 $this->wsClientIPCount = array();
202
203 return true;
204 }
205
206 // client timeout functions
207 function wsCheckIdleClients() {
208 $time = time();
209 foreach ($this->wsClients as $clientID => $client) {
210 if ($client[2] != self::WS_READY_STATE_CLOSED) {
211 // client ready state is not closed
212 if ($client[4] !== false) {
213 // ping request has already been sent to client, pending a pong reply
214 if ($time >= $client[4] + self::WS_TIMEOUT_PONG) {
215 // client didn't respond to the server's ping request in self::WS_TIMEOUT_PONG seconds
216 $this->wsSendClientClose($clientID, self::WS_STATUS_TIMEOUT);
217 $this->wsRemoveClient($clientID);
218 }
219 }
220 elseif ($time >= $client[3] + self::WS_TIMEOUT_RECV) {
221 // last data was received >= self::WS_TIMEOUT_RECV seconds ago
222 if ($client[2] != self::WS_READY_STATE_CONNECTING) {
223 // client ready state is open or closing
224 $this->wsClients[$clientID][4] = time();
225 $this->wsSendClientMessage($clientID, self::WS_OPCODE_PING, '');
226 }
227 else {
228 // client ready state is connecting
229 $this->wsRemoveClient($clientID);
230 }
231 }
232 }
233 }
234 }
235
236 // client existence functions
237 function wsAddClient($socket, $clientIP) {
238 // increase amount of clients connected
239 $this->wsClientCount++;
240
241 // increase amount of clients connected on this client's IP
242 if (isset($this->wsClientIPCount[$clientIP])) {
243 $this->wsClientIPCount[$clientIP]++;
244 }
245 else {
246 $this->wsClientIPCount[$clientIP] = 1;
247 }
248
249 // fetch next client ID
250 $clientID = $this->wsGetNextClientID();
251
252 // store initial client data
253 $this->wsClients[$clientID] = array($socket, '', self::WS_READY_STATE_CONNECTING, time(), false, 0, $clientIP, false, 0, '', 0, 0);
254
255 // store socket - used for socket_select()
256 $this->wsRead[$clientID] = $socket;
257 }
258 function wsRemoveClient($clientID) {
259 // fetch close status (which could be false), and call wsOnClose
260 $closeStatus = $this->wsClients[$clientID][5];
261 if ( array_key_exists('close', $this->wsOnEvents) )
262 foreach ( $this->wsOnEvents['close'] as $func )
263 $func($clientID, $closeStatus);
264
265 // close socket
266 $socket = $this->wsClients[$clientID][0];
267 socket_close($socket);
268
269 // decrease amount of clients connected on this client's IP
270 $clientIP = $this->wsClients[$clientID][6];
271 if ($this->wsClientIPCount[$clientIP] > 1) {
272 $this->wsClientIPCount[$clientIP]--;
273 }
274 else {
275 unset($this->wsClientIPCount[$clientIP]);
276 }
277
278 // decrease amount of clients connected
279 $this->wsClientCount--;
280
281 // remove socket and client data from arrays
282 unset($this->wsRead[$clientID], $this->wsClients[$clientID]);
283 }
284
285 // client data functions
286 function wsGetNextClientID() {
287 $i = 1; // starts at 1 because 0 is the listen socket
288 while (isset($this->wsRead[$i])) $i++;
289 return $i;
290 }
291 function wsGetClientSocket($clientID) {
292 return $this->wsClients[$clientID][0];
293 }
294
295 // client read functions
296 function wsProcessClient($clientID, &$buffer, $bufferLength) {
297 if ($this->wsClients[$clientID][2] == self::WS_READY_STATE_OPEN) {
298 // handshake completed
299 $result = $this->wsBuildClientFrame($clientID, $buffer, $bufferLength);
300 }
301 elseif ($this->wsClients[$clientID][2] == self::WS_READY_STATE_CONNECTING) {
302 // handshake not completed
303 $result = $this->wsProcessClientHandshake($clientID, $buffer);
304 if ($result) {
305 $this->wsClients[$clientID][2] = self::WS_READY_STATE_OPEN;
306
307 if ( array_key_exists('open', $this->wsOnEvents) )
308 foreach ( $this->wsOnEvents['open'] as $func )
309 $func($clientID);
310 }
311 }
312 else {
313 // ready state is set to closed
314 $result = false;
315 }
316
317 return $result;
318 }
319 function wsBuildClientFrame($clientID, &$buffer, $bufferLength) {
320 // increase number of bytes read for the frame, and join buffer onto end of the frame buffer
321 $this->wsClients[$clientID][8] += $bufferLength;
322 $this->wsClients[$clientID][9] .= $buffer;
323
324 // check if the length of the frame's payload data has been fetched, if not then attempt to fetch it from the frame buffer
325 if ($this->wsClients[$clientID][7] !== false || $this->wsCheckSizeClientFrame($clientID) == true) {
326 // work out the header length of the frame
327 $headerLength = ($this->wsClients[$clientID][7] <= 125 ? 0 : ($this->wsClients[$clientID][7] <= 65535 ? 2 : 8)) + 6;
328
329 // check if all bytes have been received for the frame
330 $frameLength = $this->wsClients[$clientID][7] + $headerLength;
331 if ($this->wsClients[$clientID][8] >= $frameLength) {
332 // check if too many bytes have been read for the frame (they are part of the next frame)
333 $nextFrameBytesLength = $this->wsClients[$clientID][8] - $frameLength;
334 if ($nextFrameBytesLength > 0) {
335 $this->wsClients[$clientID][8] -= $nextFrameBytesLength;
336 $nextFrameBytes = substr($this->wsClients[$clientID][9], $frameLength);
337 $this->wsClients[$clientID][9] = substr($this->wsClients[$clientID][9], 0, $frameLength);
338 }
339
340 // process the frame
341 $result = $this->wsProcessClientFrame($clientID);
342
343 // check if the client wasn't removed, then reset frame data
344 if (isset($this->wsClients[$clientID])) {
345 $this->wsClients[$clientID][7] = false;
346 $this->wsClients[$clientID][8] = 0;
347 $this->wsClients[$clientID][9] = '';
348 }
349
350 // if there's no extra bytes for the next frame, or processing the frame failed, return the result of processing the frame
351 if ($nextFrameBytesLength <= 0 || !$result) return $result;
352
353 // build the next frame with the extra bytes
354 return $this->wsBuildClientFrame($clientID, $nextFrameBytes, $nextFrameBytesLength);
355 }
356 }
357
358 return true;
359 }
360 function wsCheckSizeClientFrame($clientID) {
361 // check if at least 2 bytes have been stored in the frame buffer
362 if ($this->wsClients[$clientID][8] > 1) {
363 // fetch payload length in byte 2, max will be 127
364 $payloadLength = ord(substr($this->wsClients[$clientID][9], 1, 1)) & 127;
365
366 if ($payloadLength <= 125) {
367 // actual payload length is <= 125
368 $this->wsClients[$clientID][7] = $payloadLength;
369 }
370 elseif ($payloadLength == 126) {
371 // actual payload length is <= 65,535
372 if (substr($this->wsClients[$clientID][9], 3, 1) !== false) {
373 // at least another 2 bytes are set
374 $payloadLengthExtended = substr($this->wsClients[$clientID][9], 2, 2);
375 $array = unpack('na', $payloadLengthExtended);
376 $this->wsClients[$clientID][7] = $array['a'];
377 }
378 }
379 else {
380 // actual payload length is > 65,535
381 if (substr($this->wsClients[$clientID][9], 9, 1) !== false) {
382 // at least another 8 bytes are set
383 $payloadLengthExtended = substr($this->wsClients[$clientID][9], 2, 8);
384
385 // check if the frame's payload data length exceeds 2,147,483,647 (31 bits)
386 // the maximum integer in PHP is "usually" this number. More info: http://php.net/manual/en/language.types.integer.php
387 $payloadLengthExtended32_1 = substr($payloadLengthExtended, 0, 4);
388 $array = unpack('Na', $payloadLengthExtended32_1);
389 if ($array['a'] != 0 || ord(substr($payloadLengthExtended, 4, 1)) & 128) {
390 $this->wsSendClientClose($clientID, self::WS_STATUS_MESSAGE_TOO_BIG);
391 return false;
392 }
393
394 // fetch length as 32 bit unsigned integer, not as 64 bit
395 $payloadLengthExtended32_2 = substr($payloadLengthExtended, 4, 4);
396 $array = unpack('Na', $payloadLengthExtended32_2);
397
398 // check if the payload data length exceeds 2,147,479,538 (2,147,483,647 - 14 - 4095)
399 // 14 for header size, 4095 for last recv() next frame bytes
400 if ($array['a'] > 2147479538) {
401 $this->wsSendClientClose($clientID, self::WS_STATUS_MESSAGE_TOO_BIG);
402 return false;
403 }
404
405 // store frame payload data length
406 $this->wsClients[$clientID][7] = $array['a'];
407 }
408 }
409
410 // check if the frame's payload data length has now been stored
411 if ($this->wsClients[$clientID][7] !== false) {
412
413 // check if the frame's payload data length exceeds self::WS_MAX_FRAME_PAYLOAD_RECV
414 if ($this->wsClients[$clientID][7] > self::WS_MAX_FRAME_PAYLOAD_RECV) {
415 $this->wsClients[$clientID][7] = false;
416 $this->wsSendClientClose($clientID, self::WS_STATUS_MESSAGE_TOO_BIG);
417 return false;
418 }
419
420 // check if the message's payload data length exceeds 2,147,483,647 or self::WS_MAX_MESSAGE_PAYLOAD_RECV
421 // doesn't apply for control frames, where the payload data is not internally stored
422 $controlFrame = (ord(substr($this->wsClients[$clientID][9], 0, 1)) & 8) == 8;
423 if (!$controlFrame) {
424 $newMessagePayloadLength = $this->wsClients[$clientID][11] + $this->wsClients[$clientID][7];
425 if ($newMessagePayloadLength > self::WS_MAX_MESSAGE_PAYLOAD_RECV || $newMessagePayloadLength > 2147483647) {
426 $this->wsSendClientClose($clientID, self::WS_STATUS_MESSAGE_TOO_BIG);
427 return false;
428 }
429 }
430
431 return true;
432 }
433 }
434
435 return false;
436 }
437 function wsProcessClientFrame($clientID) {
438 // store the time that data was last received from the client
439 $this->wsClients[$clientID][3] = time();
440
441 // fetch frame buffer
442 $buffer = &$this->wsClients[$clientID][9];
443
444 // check at least 6 bytes are set (first 2 bytes and 4 bytes for the mask key)
445 if (substr($buffer, 5, 1) === false) return false;
446
447 // fetch first 2 bytes of header
448 $octet0 = ord(substr($buffer, 0, 1));
449 $octet1 = ord(substr($buffer, 1, 1));
450
451 $fin = $octet0 & self::WS_FIN;
452 $opcode = $octet0 & 15;
453
454 $mask = $octet1 & self::WS_MASK;
455 if (!$mask) return false; // close socket, as no mask bit was sent from the client
456
457 // fetch byte position where the mask key starts
458 $seek = $this->wsClients[$clientID][7] <= 125 ? 2 : ($this->wsClients[$clientID][7] <= 65535 ? 4 : 10);
459
460 // read mask key
461 $maskKey = substr($buffer, $seek, 4);
462
463 $array = unpack('Na', $maskKey);
464 $maskKey = $array['a'];
465 $maskKey = array(
466 $maskKey >> 24,
467 ($maskKey >> 16) & 255,
468 ($maskKey >> 8) & 255,
469 $maskKey & 255
470 );
471 $seek += 4;
472
473 // decode payload data
474 if (substr($buffer, $seek, 1) !== false) {
475 $data = str_split(substr($buffer, $seek));
476 foreach ($data as $key => $byte) {
477 $data[$key] = chr(ord($byte) ^ ($maskKey[$key % 4]));
478 }
479 $data = implode('', $data);
480 }
481 else {
482 $data = '';
483 }
484
485 // check if this is not a continuation frame and if there is already data in the message buffer
486 if ($opcode != self::WS_OPCODE_CONTINUATION && $this->wsClients[$clientID][11] > 0) {
487 // clear the message buffer
488 $this->wsClients[$clientID][11] = 0;
489 $this->wsClients[$clientID][1] = '';
490 }
491
492 // check if the frame is marked as the final frame in the message
493 if ($fin == self::WS_FIN) {
494 // check if this is the first frame in the message
495 if ($opcode != self::WS_OPCODE_CONTINUATION) {
496 // process the message
497 return $this->wsProcessClientMessage($clientID, $opcode, $data, $this->wsClients[$clientID][7]);
498 }
499 else {
500 // increase message payload data length
501 $this->wsClients[$clientID][11] += $this->wsClients[$clientID][7];
502
503 // push frame payload data onto message buffer
504 $this->wsClients[$clientID][1] .= $data;
505
506 // process the message
507 $result = $this->wsProcessClientMessage($clientID, $this->wsClients[$clientID][10], $this->wsClients[$clientID][1], $this->wsClients[$clientID][11]);
508
509 // check if the client wasn't removed, then reset message buffer and message opcode
510 if (isset($this->wsClients[$clientID])) {
511 $this->wsClients[$clientID][1] = '';
512 $this->wsClients[$clientID][10] = 0;
513 $this->wsClients[$clientID][11] = 0;
514 }
515
516 return $result;
517 }
518 }
519 else {
520 // check if the frame is a control frame, control frames cannot be fragmented
521 if ($opcode & 8) return false;
522
523 // increase message payload data length
524 $this->wsClients[$clientID][11] += $this->wsClients[$clientID][7];
525
526 // push frame payload data onto message buffer
527 $this->wsClients[$clientID][1] .= $data;
528
529 // if this is the first frame in the message, store the opcode
530 if ($opcode != self::WS_OPCODE_CONTINUATION) {
531 $this->wsClients[$clientID][10] = $opcode;
532 }
533 }
534
535 return true;
536 }
537 function wsProcessClientMessage($clientID, $opcode, &$data, $dataLength) {
538 // check opcodes
539 if ($opcode == self::WS_OPCODE_PING) {
540 // received ping message
541 return $this->wsSendClientMessage($clientID, self::WS_OPCODE_PONG, $data);
542 }
543 elseif ($opcode == self::WS_OPCODE_PONG) {
544 // received pong message (it's valid if the server did not send a ping request for this pong message)
545 if ($this->wsClients[$clientID][4] !== false) {
546 $this->wsClients[$clientID][4] = false;
547 }
548 }
549 elseif ($opcode == self::WS_OPCODE_CLOSE) {
550 // received close message
551 if (substr($data, 1, 1) !== false) {
552 $array = unpack('na', substr($data, 0, 2));
553 $status = $array['a'];
554 }
555 else {
556 $status = false;
557 }
558
559 if ($this->wsClients[$clientID][2] == self::WS_READY_STATE_CLOSING) {
560 // the server already sent a close frame to the client, this is the client's close frame reply
561 // (no need to send another close frame to the client)
562 $this->wsClients[$clientID][2] = self::WS_READY_STATE_CLOSED;
563 }
564 else {
565 // the server has not already sent a close frame to the client, send one now
566 $this->wsSendClientClose($clientID, self::WS_STATUS_NORMAL_CLOSE);
567 }
568
569 $this->wsRemoveClient($clientID);
570 }
571 elseif ($opcode == self::WS_OPCODE_TEXT || $opcode == self::WS_OPCODE_BINARY) {
572 if ( array_key_exists('message', $this->wsOnEvents) )
573 foreach ( $this->wsOnEvents['message'] as $func )
574 $func($clientID, $data, $dataLength, $opcode == self::WS_OPCODE_BINARY);
575 }
576 else {
577 // unknown opcode
578 return false;
579 }
580
581 return true;
582 }
583 function wsProcessClientHandshake($clientID, &$buffer) {
584 // fetch headers and request line
585 $sep = strpos($buffer, "\r\n\r\n");
586 if (!$sep) return false;
587
588 $headers = explode("\r\n", substr($buffer, 0, $sep));
589 $headersCount = sizeof($headers); // includes request line
590 if ($headersCount < 1) return false;
591
592 // fetch request and check it has at least 3 parts (space tokens)
593 $request = &$headers[0];
594 $requestParts = explode(' ', $request);
595 $requestPartsSize = sizeof($requestParts);
596 if ($requestPartsSize < 3) return false;
597
598 // check request method is GET
599 if (strtoupper($requestParts[0]) != 'GET') return false;
600
601 // check request HTTP version is at least 1.1
602 $httpPart = &$requestParts[$requestPartsSize - 1];
603 $httpParts = explode('/', $httpPart);
604 if (!isset($httpParts[1]) || (float) $httpParts[1] < 1.1) return false;
605
606 // store headers into a keyed array: array[headerKey] = headerValue
607 $headersKeyed = array();
608 for ($i=1; $i<$headersCount; $i++) {
609 $parts = explode(':', $headers[$i]);
610 if (!isset($parts[1])) return false;
611
612 $headersKeyed[trim($parts[0])] = trim($parts[1]);
613 }
614
615 // check Host header was received
616 if (!isset($headersKeyed['Host'])) return false;
617
618 // check Sec-WebSocket-Key header was received and decoded value length is 16
619 if (!isset($headersKeyed['Sec-WebSocket-Key'])) return false;
620 $key = $headersKeyed['Sec-WebSocket-Key'];
621 if (strlen(base64_decode($key)) != 16) return false;
622
623 // check Sec-WebSocket-Version header was received and value is 7
624 if (!isset($headersKeyed['Sec-WebSocket-Version']) || (int) $headersKeyed['Sec-WebSocket-Version'] < 7) return false; // should really be != 7, but Firefox 7 beta users send 8
625
626 // work out hash to use in Sec-WebSocket-Accept reply header
627 $hash = base64_encode(sha1($key.'258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
628
629 // build headers
630 $headers = array(
631 'HTTP/1.1 101 Switching Protocols',
632 'Upgrade: websocket',
633 'Connection: Upgrade',
634 'Sec-WebSocket-Accept: '.$hash
635 );
636 $headers = implode("\r\n", $headers)."\r\n\r\n";
637
638 // send headers back to client
639 $socket = $this->wsClients[$clientID][0];
640
641 $left = strlen($headers);
642 do {
643 $sent = @socket_send($socket, $headers, $left, 0);
644 if ($sent === false) return false;
645
646 $left -= $sent;
647 if ($sent > 0) $headers = substr($headers, $sent);
648 }
649 while ($left > 0);
650
651 return true;
652 }
653
654 // client write functions
655 function wsSendClientMessage($clientID, $opcode, $message) {
656 // check if client ready state is already closing or closed
657 if ($this->wsClients[$clientID][2] == self::WS_READY_STATE_CLOSING || $this->wsClients[$clientID][2] == self::WS_READY_STATE_CLOSED) return true;
658
659 // fetch message length
660 $messageLength = strlen($message);
661
662 // set max payload length per frame
663 $bufferSize = 4096;
664
665 // work out amount of frames to send, based on $bufferSize
666 $frameCount = ceil($messageLength / $bufferSize);
667 if ($frameCount == 0) $frameCount = 1;
668
669 // set last frame variables
670 $maxFrame = $frameCount - 1;
671 $lastFrameBufferLength = ($messageLength % $bufferSize) != 0 ? ($messageLength % $bufferSize) : ($messageLength != 0 ? $bufferSize : 0);
672
673 // loop around all frames to send
674 for ($i=0; $i<$frameCount; $i++) {
675 // fetch fin, opcode and buffer length for frame
676 $fin = $i != $maxFrame ? 0 : self::WS_FIN;
677 $opcode = $i != 0 ? self::WS_OPCODE_CONTINUATION : $opcode;
678
679 $bufferLength = $i != $maxFrame ? $bufferSize : $lastFrameBufferLength;
680
681 // set payload length variables for frame
682 if ($bufferLength <= 125) {
683 $payloadLength = $bufferLength;
684 $payloadLengthExtended = '';
685 $payloadLengthExtendedLength = 0;
686 }
687 elseif ($bufferLength <= 65535) {
688 $payloadLength = self::WS_PAYLOAD_LENGTH_16;
689 $payloadLengthExtended = pack('n', $bufferLength);
690 $payloadLengthExtendedLength = 2;
691 }
692 else {
693 $payloadLength = self::WS_PAYLOAD_LENGTH_63;
694 $payloadLengthExtended = pack('xxxxN', $bufferLength); // pack 32 bit int, should really be 64 bit int
695 $payloadLengthExtendedLength = 8;
696 }
697
698 // set frame bytes
699 $buffer = pack('n', (($fin | $opcode) << 8) | $payloadLength) . $payloadLengthExtended . substr($message, $i*$bufferSize, $bufferLength);
700
701 // send frame
702 $socket = $this->wsClients[$clientID][0];
703
704 $left = 2 + $payloadLengthExtendedLength + $bufferLength;
705 do {
706 $sent = @socket_send($socket, $buffer, $left, 0);
707 if ($sent === false) return false;
708
709 $left -= $sent;
710 if ($sent > 0) $buffer = substr($buffer, $sent);
711 }
712 while ($left > 0);
713 }
714
715 return true;
716 }
717 function wsSendClientClose($clientID, $status=false) {
718 // check if client ready state is already closing or closed
719 if ($this->wsClients[$clientID][2] == self::WS_READY_STATE_CLOSING || $this->wsClients[$clientID][2] == self::WS_READY_STATE_CLOSED) return true;
720
721 // store close status
722 $this->wsClients[$clientID][5] = $status;
723
724 // send close frame to client
725 $status = $status !== false ? pack('n', $status) : '';
726 $this->wsSendClientMessage($clientID, self::WS_OPCODE_CLOSE, $status);
727
728 // set client ready state to closing
729 $this->wsClients[$clientID][2] = self::WS_READY_STATE_CLOSING;
730 }
731
732 // client non-internal functions
733 function wsClose($clientID) {
734 return $this->wsSendClientClose($clientID, self::WS_STATUS_NORMAL_CLOSE);
735 }
736 function wsSend($clientID, $message, $binary=false) {
737 return $this->wsSendClientMessage($clientID, $binary ? self::WS_OPCODE_BINARY : self::WS_OPCODE_TEXT, $message);
738 }
739
740 function log( $message )
741 {
742 echo date('Y-m-d H:i:s: ') . $message . "\n";
743 }
744
745 function bind( $type, $func )
746 {
747 if ( !isset($this->wsOnEvents[$type]) )
748 $this->wsOnEvents[$type] = array();
749 $this->wsOnEvents[$type][] = $func;
750 }
751
752 function unbind( $type='' )
753 {
754 if ( $type ) unset($this->wsOnEvents[$type]);
755 else $this->wsOnEvents = array();
756 }
757 }
758 ?>