Server.php 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. <?php
  2. /**
  3. * Copyright (C) 2014-2020 Textalk/Abicart and contributors.
  4. *
  5. * This file is part of Websocket PHP and is free software under the ISC License.
  6. * License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
  7. */
  8. namespace WebSocket;
  9. class Server extends Base
  10. {
  11. // Default options
  12. protected static $default_options = [
  13. 'timeout' => null,
  14. 'fragment_size' => 4096,
  15. 'port' => 8000,
  16. 'logger' => null,
  17. ];
  18. protected $addr;
  19. protected $port;
  20. protected $listening;
  21. protected $request;
  22. protected $request_path;
  23. /**
  24. * @param array $options
  25. * Associative array containing:
  26. * - timeout: Set the socket timeout in seconds.
  27. * - fragment_size: Set framgemnt size. Default: 4096
  28. * - port: Chose port for listening. Default 8000.
  29. */
  30. public function __construct(array $options = array())
  31. {
  32. $this->options = array_merge(self::$default_options, $options);
  33. $this->port = $this->options['port'];
  34. $this->setLogger($this->options['logger']);
  35. do {
  36. $this->listening = @stream_socket_server("tcp://0.0.0.0:$this->port", $errno, $errstr);
  37. } while ($this->listening === false && $this->port++ < 10000);
  38. if (!$this->listening) {
  39. $error = "Could not open listening socket: {$errstr} ({$errno})";
  40. $this->logger->error($error);
  41. throw new ConnectionException($error, $errno);
  42. }
  43. $this->logger->info("Server listening to port {$this->port}");
  44. }
  45. public function __destruct()
  46. {
  47. if ($this->isConnected()) {
  48. fclose($this->socket);
  49. }
  50. $this->socket = null;
  51. }
  52. public function getPort()
  53. {
  54. return $this->port;
  55. }
  56. public function getPath()
  57. {
  58. return $this->request_path;
  59. }
  60. public function getRequest()
  61. {
  62. return $this->request;
  63. }
  64. public function getHeader($header)
  65. {
  66. foreach ($this->request as $row) {
  67. if (stripos($row, $header) !== false) {
  68. list($headername, $headervalue) = explode(":", $row);
  69. return trim($headervalue);
  70. }
  71. }
  72. return null;
  73. }
  74. public function accept()
  75. {
  76. $this->socket = null;
  77. return (bool)$this->listening;
  78. }
  79. protected function connect()
  80. {
  81. if (empty($this->options['timeout'])) {
  82. $this->socket = @stream_socket_accept($this->listening);
  83. if (!$this->socket) {
  84. $error = 'Server failed to connect.';
  85. $this->logger->error($error);
  86. throw new ConnectionException($error);
  87. }
  88. } else {
  89. $this->socket = @stream_socket_accept($this->listening, $this->options['timeout']);
  90. if (!$this->socket) {
  91. $error = 'Server failed to connect.';
  92. $this->logger->error($error);
  93. throw new ConnectionException($error);
  94. }
  95. stream_set_timeout($this->socket, $this->options['timeout']);
  96. }
  97. $this->performHandshake();
  98. $this->logger->info("Server connected to port {$this->port}");
  99. }
  100. protected function performHandshake()
  101. {
  102. $request = '';
  103. do {
  104. $buffer = stream_get_line($this->socket, 1024, "\r\n");
  105. $request .= $buffer . "\n";
  106. $metadata = stream_get_meta_data($this->socket);
  107. } while (!feof($this->socket) && $metadata['unread_bytes'] > 0);
  108. if (!preg_match('/GET (.*) HTTP\//mUi', $request, $matches)) {
  109. $error = "No GET in request: {$request}";
  110. $this->logger->error($error);
  111. throw new ConnectionException($error);
  112. }
  113. $get_uri = trim($matches[1]);
  114. $uri_parts = parse_url($get_uri);
  115. $this->request = explode("\n", $request);
  116. $this->request_path = $uri_parts['path'];
  117. /// @todo Get query and fragment as well.
  118. if (!preg_match('#Sec-WebSocket-Key:\s(.*)$#mUi', $request, $matches)) {
  119. $error = "Client had no Key in upgrade request: {$request}";
  120. $this->logger->error($error);
  121. throw new ConnectionException($error);
  122. }
  123. $key = trim($matches[1]);
  124. /// @todo Validate key length and base 64...
  125. $response_key = base64_encode(pack('H*', sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
  126. $header = "HTTP/1.1 101 Switching Protocols\r\n"
  127. . "Upgrade: websocket\r\n"
  128. . "Connection: Upgrade\r\n"
  129. . "Sec-WebSocket-Accept: $response_key\r\n"
  130. . "\r\n";
  131. $this->write($header);
  132. }
  133. }