Hoa central
Connection.php
Go to the documentation of this file.
1 <?php
2 
37 namespace Hoa\Websocket;
38 
39 use Hoa\Core;
40 use Hoa\Socket;
41 
50 abstract class Connection
52  implements Core\Event\Listenable
53 {
60 
66  const OPCODE_TEXT_FRAME = 0x1;
67 
73  const OPCODE_BINARY_FRAME = 0x2;
74 
81 
87  const OPCODE_PING = 0x9;
88 
94  const OPCODE_PONG = 0xa;
95 
101  const CLOSE_NORMAL = 1000;
102 
108  const CLOSE_GOING_AWAY = 1001;
109 
115  const CLOSE_PROTOCOL_ERROR = 1002;
116 
122  const CLOSE_DATA_ERROR = 1003;
123 
129  const CLOSE_STATUS_ERROR = 1005;
130 
136  const CLOSE_ABNORMAL = 1006;
137 
143  const CLOSE_MESSAGE_ERROR = 1007;
144 
150  const CLOSE_POLICY_ERROR = 1008;
151 
157  const CLOSE_MESSAGE_TOO_BIG = 1009;
158 
165 
171  const CLOSE_SERVER_ERROR = 1011;
172 
178  const CLOSE_TLS = 1015;
179 
180 
186  protected $_on = null;
187 
188 
189 
199  public function __construct(Socket\Connection $connection)
200  {
201  parent::__construct($connection);
202  $this->getConnection()->setNodeName('\Hoa\Websocket\Node');
203  $this->_on = new Core\Event\Listener($this, [
204  'open',
205  'message',
206  'binary-message',
207  'ping',
208  'close',
209  'error'
210  ]);
211 
212  return;
213  }
214 
223  public function on($listenerId, $callable)
224  {
225  $this->_on->attach($listenerId, $callable);
226 
227  return $this;
228  }
229 
236  protected function _run(Socket\Node $node)
237  {
238  try {
239  if (FAILED === $node->getHandshake()) {
240  $this->doHandshake();
241  $this->_on->fire(
242  'open',
243  new Core\Event\Bucket()
244  );
245 
246  return;
247  }
248 
249  try {
250  $frame = $node->getProtocolImplementation()->readFrame();
251  } catch (Exception\CloseError $e) {
252  $this->close($e->getErrorCode(), $e->getMessage());
253 
254  return;
255  }
256 
257  if (false === $frame) {
258  return;
259  }
260 
261  if ($this instanceof Server &&
262  isset($frame['mask']) &&
263  0x0 === $frame['mask']) {
264  $this->close(
265  self::CLOSE_MESSAGE_ERROR,
266  'All messages from the client must be masked.'
267  );
268 
269  return;
270  }
271 
272  $fromText = false;
273  $fromBinary = false;
274 
275  switch ($frame['opcode']) {
276  case self::OPCODE_BINARY_FRAME:
277  $fromBinary = true;
278 
279  case self::OPCODE_TEXT_FRAME:
280  if (0x1 === $frame['fin']) {
281  if (0 < $node->getNumberOfFragments()) {
282  $this->close(self::CLOSE_PROTOCOL_ERROR);
283 
284  break;
285  }
286 
287  if (true === $fromBinary) {
288  $fromBinary = false;
289  $this->_on->fire(
290  'binary-message',
291  new Core\Event\Bucket([
292  'message' => $frame['message']
293  ])
294  );
295 
296  break;
297  }
298 
299  if (false === (bool) preg_match('//u', $frame['message'])) {
300  $this->close(self::CLOSE_MESSAGE_ERROR);
301 
302  break;
303  }
304 
305  $this->_on->fire(
306  'message',
307  new Core\Event\Bucket([
308  'message' => $frame['message']
309  ])
310  );
311 
312  break;
313  } else {
314  $node->setComplete(false);
315  }
316 
317  $fromText = true;
318 
319  case self::OPCODE_CONTINUATION_FRAME:
320  if (false === $fromText) {
321  if (0 === $node->getNumberOfFragments()) {
322  $this->close(self::CLOSE_PROTOCOL_ERROR);
323 
324  break;
325  }
326  } else {
327  $fromText = false;
328 
329  if (true === $fromBinary) {
330  $node->setBinary(true);
331  $fromBinary = false;
332  }
333  }
334 
335  $node->appendMessageFragment($frame['message']);
336 
337  if (0x1 === $frame['fin']) {
338  $message = $node->getFragmentedMessage();
339  $isBinary = $node->isBinary();
340  $node->clearFragmentation();
341 
342  if (true === $isBinary) {
343  $this->_on->fire(
344  'binary-message',
345  new Core\Event\Bucket([
346  'message' => $message
347  ])
348  );
349 
350  break;
351  }
352 
353  if (false === (bool) preg_match('//u', $message)) {
354  $this->close(self::CLOSE_MESSAGE_ERROR);
355 
356  break;
357  }
358 
359  $this->_on->fire(
360  'message',
361  new Core\Event\Bucket([
362  'message' => $message
363  ])
364  );
365  } else {
366  $node->setComplete(false);
367  }
368 
369  break;
370 
371  case self::OPCODE_PING:
372  $message = &$frame['message'];
373 
374  if (0x0 === $frame['fin'] ||
375  0x7d < $frame['length']) {
376  $this->close(self::CLOSE_PROTOCOL_ERROR);
377 
378  break;
379  }
380 
381  $node
382  ->getProtocolImplementation()
383  ->writeFrame(
384  $message,
385  self::OPCODE_PONG,
386  true
387  );
388 
389  $this->_on->fire(
390  'ping',
391  new Core\Event\Bucket([
392  'message' => $message
393  ])
394  );
395 
396  break;
397 
398  case self::OPCODE_PONG:
399  if (0 === $frame['fin']) {
400  $this->close(self::CLOSE_PROTOCOL_ERROR);
401 
402  break;
403  }
404 
405  break;
406 
407  case self::OPCODE_CONNECTION_CLOSE:
408  $length = &$frame['length'];
409 
410  if (1 === $length ||
411  0x7d < $length) {
412  $this->close(self::CLOSE_PROTOCOL_ERROR);
413 
414  break;
415  }
416 
417  $code = self::CLOSE_NORMAL;
418  $reason = null;
419 
420  if (0 < $length) {
421  $message = &$frame['message'];
422  $_code = unpack('nc', substr($message, 0, 2));
423  $code = &$_code['c'];
424 
425  if (1000 > $code ||
426  (1004 <= $code && $code <= 1006) ||
427  (1012 <= $code && $code <= 1016) ||
428  5000 <= $code) {
429  $this->close(self::CLOSE_PROTOCOL_ERROR);
430 
431  break;
432  }
433 
434  if (2 < $length) {
435  $reason = substr($message, 2);
436 
437  if (false === (bool) preg_match('//u', $reason)) {
438  $this->close(self::CLOSE_MESSAGE_ERROR);
439 
440  break;
441  }
442  }
443  }
444 
445  $this->close(self::CLOSE_NORMAL);
446  $this->_on->fire(
447  'close',
448  new Core\Event\Bucket([
449  'code' => $code,
450  'reason' => $reason
451  ])
452  );
453 
454  break;
455 
456  default:
457  $this->close(self::CLOSE_PROTOCOL_ERROR);
458  }
459  } catch (Core\Exception\Idle $e) {
460  try {
461  $this->close(self::CLOSE_SERVER_ERROR);
462  $exception = $e;
463  } catch (Core\Exception\Idle $ee) {
464  $this->getConnection()->disconnect();
465  $exception = new Core\Exception\Group(
466  'An exception has been thrown. We have tried to close ' .
467  'the connection but another exception has been thrown.',
468  42
469  );
470  $exception[] = $e;
471  $exception[] = $ee;
472  }
473 
474  $this->_on->fire(
475  'error',
476  new Core\Event\Bucket([
477  'exception' => $exception
478  ])
479  );
480  }
481 
482  return;
483  }
484 
491  abstract protected function doHandshake();
492 
500  protected function _send($message, Socket\Node $node)
501  {
502  $mustMask = $this instanceof Client;
503 
504  return function ($opcode, $end) use (&$message, $node, $mustMask) {
505  return
506  $node
507  ->getProtocolImplementation()
508  ->send($message, $opcode, $end, $mustMask);
509  };
510  }
511 
522  public function send(
523  $message,
524  Socket\Node $node = null,
525  $opcode = self::OPCODE_TEXT_FRAME,
526  $end = true
527  ) {
528  $send = parent::send($message, $node);
529 
530  if (null === $send) {
531  return null;
532  }
533 
534  return $send($opcode, $end);
535  }
536 
546  public function close($code = self::CLOSE_NORMAL, $reason = null)
547  {
548  $connection = $this->getConnection();
549  $protocol = $connection->getCurrentNode()->getProtocolImplementation();
550 
551  if (null !== $protocol) {
552  $protocol->close($code, $reason);
553  }
554 
555  return $connection->disconnect();
556  }
557 }
on($listenerId, $callable)
Definition: Connection.php:223
_run(Socket\Node $node)
Definition: Connection.php:236
send($message, Socket\Node $node=null, $opcode=self::OPCODE_TEXT_FRAME, $end=true)
Definition: Connection.php:522
_send($message, Socket\Node $node)
Definition: Connection.php:500
close($code=self::CLOSE_NORMAL, $reason=null)
Definition: Connection.php:546
__construct(Socket\Connection $connection)
Definition: Connection.php:199