/lib/Wrench/Protocol/Protocol.php

https://github.com/tinkajts/php-websocket · PHP · 791 lines · 414 code · 103 blank · 274 comment · 64 complexity · 8b38ad03b8ecb8a08764af703b6f9df6 MD5 · raw file

  1. <?php
  2. namespace Wrench\Protocol;
  3. use Wrench\Payload\Payload;
  4. use Wrench\Exception\BadRequestException;
  5. use \Exception;
  6. use \InvalidArgumentException;
  7. /**
  8. * Definitions and implementation helpers for the Wrenchs protocol
  9. *
  10. * Based on RFC 6455: http://tools.ietf.org/html/rfc6455
  11. */
  12. abstract class Protocol
  13. {
  14. /**#@+
  15. * Relevant schemes
  16. *
  17. * @var string
  18. */
  19. const SCHEME_WEBSOCKET = 'ws';
  20. const SCHEME_WEBSOCKET_SECURE = 'wss';
  21. const SCHEME_UNDERLYING = 'tcp';
  22. const SCHEME_UNDERLYING_SECURE = 'tls';
  23. /**#@-*/
  24. /**#@+
  25. * HTTP headers
  26. *
  27. * @var string
  28. */
  29. const HEADER_HOST = 'Host';
  30. const HEADER_KEY = 'Sec-WebSocket-Key';
  31. const HEADER_PROTOCOL = 'Sec-WebSocket-Protocol';
  32. const HEADER_VERSION = 'Sec-WebSocket-Version';
  33. const HEADER_ACCEPT = 'Sec-WebSocket-Accept';
  34. const HEADER_EXTENSIONS = 'Sec-WebSocket-Extensions';
  35. const HEADER_ORIGIN = 'Origin';
  36. const HEADER_CONNECTION = 'Connection';
  37. const HEADER_UPGRADE = 'Upgrade';
  38. /**#@-*/
  39. /**#@+
  40. * HTTP error statuses
  41. *
  42. * @var int
  43. */
  44. const HTTP_SWITCHING_PROTOCOLS = 101;
  45. const HTTP_BAD_REQUEST = 400;
  46. const HTTP_UNAUTHORIZED = 401;
  47. const HTTP_FORBIDDEN = 403;
  48. const HTTP_NOT_FOUND = 404;
  49. const HTTP_RATE_LIMITED = 420;
  50. const HTTP_SERVER_ERROR = 500;
  51. const HTTP_NOT_IMPLEMENTED = 501;
  52. /**#@-*/
  53. /**#@+
  54. * Close statuses
  55. *
  56. * @see http://tools.ietf.org/html/rfc6455#section-7.4
  57. * @var int
  58. */
  59. const CLOSE_NORMAL = 1000;
  60. const CLOSE_GOING_AWAY = 1001;
  61. const CLOSE_PROTOCOL_ERROR = 1002;
  62. const CLOSE_DATA_INVALID = 1003;
  63. const CLOSE_RESERVED = 1004;
  64. const CLOSE_RESERVED_NONE = 1005;
  65. const CLOSE_RESERVED_ABNORM = 1006;
  66. const CLOSE_DATA_INCONSISTENT = 1007;
  67. const CLOSE_POLICY_VIOLATION = 1008;
  68. const CLOSE_MESSAGE_TOO_BIG = 1009;
  69. const CLOSE_EXTENSION_NEEDED = 1010;
  70. const CLOSE_UNEXPECTED = 1011;
  71. const CLOSE_RESERVED_TLS = 1015;
  72. /**#@-*/
  73. /**#@+
  74. * Frame types
  75. *
  76. * %x0 denotes a continuation frame
  77. * %x1 denotes a text frame
  78. * %x2 denotes a binary frame
  79. * %x3-7 are reserved for further non-control frames
  80. * %x8 denotes a connection close
  81. * %x9 denotes a ping
  82. * %xA denotes a pong
  83. * %xB-F are reserved for further control frames
  84. *
  85. * @var int
  86. */
  87. const TYPE_CONTINUATION = 0;
  88. const TYPE_TEXT = 1;
  89. const TYPE_BINARY = 2;
  90. const TYPE_RESERVED_3 = 3;
  91. const TYPE_RESERVED_4 = 4;
  92. const TYPE_RESERVED_5 = 5;
  93. const TYPE_RESERVED_6 = 6;
  94. const TYPE_RESERVED_7 = 7;
  95. const TYPE_CLOSE = 8;
  96. const TYPE_PING = 9;
  97. const TYPE_PONG = 10;
  98. const TYPE_RESERVED_11 = 11;
  99. const TYPE_RESERVED_12 = 12;
  100. const TYPE_RESERVED_13 = 13;
  101. const TYPE_RESERVED_14 = 14;
  102. const TYPE_RESERVED_15 = 15;
  103. /**#@-*/
  104. /**
  105. * Magic GUID
  106. *
  107. * Used in the WebSocket accept header
  108. *
  109. * @var string
  110. */
  111. const MAGIC_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
  112. /**
  113. * The request MUST contain an |Upgrade| header field whose value
  114. * MUST include the "websocket" keyword.
  115. */
  116. const UPGRADE_VALUE = 'websocket';
  117. /**
  118. * The request MUST contain a |Connection| header field whose value
  119. * MUST include the "Upgrade" token.
  120. */
  121. const CONNECTION_VALUE = 'Upgrade';
  122. /**
  123. * Request line format
  124. *
  125. * @var string printf compatible, passed request path string
  126. */
  127. const REQUEST_LINE_FORMAT = 'GET %s HTTP/1.1';
  128. /**
  129. * Request line regex
  130. *
  131. * Used for parsing requested path
  132. *
  133. * @var string preg_* compatible
  134. */
  135. const REQUEST_LINE_REGEX = '/^GET (\S+) HTTP\/1.1$/';
  136. /**
  137. * Response line format
  138. *
  139. * @var string
  140. */
  141. const RESPONSE_LINE_FORMAT = 'HTTP/1.1 %d %s';
  142. /**
  143. * Header line format
  144. *
  145. * @var string printf compatible, passed header name and value
  146. */
  147. const HEADER_LINE_FORMAT = '%s: %s';
  148. /**
  149. * Valid schemes
  150. *
  151. * @var array<string>
  152. */
  153. protected static $schemes = array(
  154. self::SCHEME_WEBSOCKET,
  155. self::SCHEME_WEBSOCKET_SECURE,
  156. self::SCHEME_UNDERLYING,
  157. self::SCHEME_UNDERLYING_SECURE
  158. );
  159. /**
  160. * Close status codes
  161. *
  162. * @var array<int => string>
  163. */
  164. public static $closeReasons = array(
  165. self::CLOSE_NORMAL => 'normal close',
  166. self::CLOSE_GOING_AWAY => 'going away',
  167. self::CLOSE_PROTOCOL_ERROR => 'protocol error',
  168. self::CLOSE_DATA_INVALID => 'data invalid',
  169. self::CLOSE_DATA_INCONSISTENT => 'data inconsistent',
  170. self::CLOSE_POLICY_VIOLATION => 'policy violation',
  171. self::CLOSE_MESSAGE_TOO_BIG => 'message too big',
  172. self::CLOSE_EXTENSION_NEEDED => 'extension needed',
  173. self::CLOSE_UNEXPECTED => 'unexpected error',
  174. self::CLOSE_RESERVED => null, // Don't use these!
  175. self::CLOSE_RESERVED_NONE => null,
  176. self::CLOSE_RESERVED_ABNORM => null,
  177. self::CLOSE_RESERVED_TLS => null
  178. );
  179. /**
  180. * Frame types
  181. *
  182. * @todo flip values and keys?
  183. * @var array<string => int>
  184. */
  185. public static $frameTypes = array(
  186. 'continuation' => self::TYPE_CONTINUATION,
  187. 'text' => self::TYPE_TEXT,
  188. 'binary' => self::TYPE_BINARY,
  189. 'close' => self::TYPE_CLOSE,
  190. 'ping' => self::TYPE_PING,
  191. 'pong' => self::TYPE_PONG
  192. );
  193. /**
  194. * HTTP errors
  195. *
  196. * @var array<int => string>
  197. */
  198. public static $httpResponses = array(
  199. self::HTTP_SWITCHING_PROTOCOLS => 'Switching Protocols',
  200. self::HTTP_BAD_REQUEST => 'Bad Request',
  201. self::HTTP_UNAUTHORIZED => 'Unauthorized',
  202. self::HTTP_FORBIDDEN => 'Forbidden',
  203. self::HTTP_NOT_FOUND => 'Not Found',
  204. self::HTTP_NOT_IMPLEMENTED => 'Not Implemented',
  205. self::HTTP_RATE_LIMITED => 'Enhance Your Calm'
  206. );
  207. /**
  208. * Gets a version number
  209. *
  210. * @return
  211. */
  212. abstract public function getVersion();
  213. /**
  214. * Subclasses should implement this method and return a boolean to the given
  215. * version string, as to whether they would like to accept requests from
  216. * user agents that specify that version.
  217. *
  218. * @return boolean
  219. */
  220. abstract public function acceptsVersion($version);
  221. /**
  222. * Gets a payload instance, suitable for use in decoding/encoding protocol
  223. * frames
  224. *
  225. * @return Payload
  226. */
  227. abstract public function getPayload();
  228. /**
  229. * Generates a key suitable for use in the protocol
  230. *
  231. * This base implementation returns a 16-byte (128 bit) random key as a
  232. * binary string.
  233. *
  234. * @return string
  235. */
  236. public function generateKey()
  237. {
  238. if (extension_loaded('openssl')) {
  239. $key = openssl_random_pseudo_bytes(16);
  240. } else {
  241. // SHA1 is 128 bit (= 16 bytes)
  242. $key = sha1(spl_object_hash($this) . mt_rand(0, PHP_INT_MAX) . uniqid('', true), true);
  243. }
  244. return base64_encode($key);
  245. }
  246. /**
  247. * Gets request handshake string
  248. *
  249. * The leading line from the client follows the Request-Line format.
  250. * The leading line from the server follows the Status-Line format. The
  251. * Request-Line and Status-Line productions are defined in [RFC2616].
  252. *
  253. * An unordered set of header fields comes after the leading line in
  254. * both cases. The meaning of these header fields is specified in
  255. * Section 4 of this document. Additional header fields may also be
  256. * present, such as cookies [RFC6265]. The format and parsing of
  257. * headers is as defined in [RFC2616].
  258. *
  259. * @param string $uri WebSocket URI, e.g. ws://example.org:8000/chat
  260. * @param string $key 16 byte binary string key
  261. * @param string $origin Origin of the request
  262. * @return string
  263. */
  264. public function getRequestHandshake(
  265. $uri,
  266. $key,
  267. $origin,
  268. array $headers = array()
  269. ) {
  270. if (!$uri || !$key || !$origin) {
  271. throw new InvalidArgumentException('You must supply a URI, key and origin');
  272. }
  273. list($scheme, $host, $port, $path) = self::validateUri($uri);
  274. $handshake = array(
  275. sprintf(self::REQUEST_LINE_FORMAT, $path)
  276. );
  277. $headers = array_merge(
  278. $this->getDefaultRequestHeaders(
  279. $host . ':' . $port, $key, $origin
  280. ),
  281. $headers
  282. );
  283. foreach ($headers as $name => $value) {
  284. $handshake[] = sprintf(self::HEADER_LINE_FORMAT, $name, $value);
  285. }
  286. return implode($handshake, "\r\n") . "\r\n\r\n";
  287. }
  288. /**
  289. * Gets a handshake response body
  290. *
  291. * @param string $key
  292. * @param array $headers
  293. */
  294. public function getResponseHandshake($key, array $headers = array())
  295. {
  296. $headers = array_merge(
  297. $this->getSuccessResponseHeaders(
  298. $key
  299. ),
  300. $headers
  301. );
  302. return $this->getHttpResponse(self::HTTP_SWITCHING_PROTOCOLS, $headers);
  303. }
  304. /**
  305. * Gets a response to an error in the handshake
  306. *
  307. * @param int|Exception $e Exception or HTTP error
  308. * @param array $headers
  309. */
  310. public function getResponseError($e, array $headers = array())
  311. {
  312. $code = false;
  313. if ($e instanceof Exception) {
  314. $code = $e->getCode();
  315. } elseif (is_numeric($e)) {
  316. $code = (int)$e;
  317. }
  318. if (!$code || $code < 400 || $code > 599) {
  319. $code = self::HTTP_SERVER_ERROR;
  320. }
  321. return $this->getHttpResponse($code, $headers);
  322. }
  323. /**
  324. * Gets an HTTP response
  325. *
  326. * @param int $status
  327. * @param array $headers
  328. */
  329. protected function getHttpResponse($status, array $headers = array())
  330. {
  331. if (array_key_exists($status, self::$httpResponses)) {
  332. $response = self::$httpResponses[$status];
  333. } else {
  334. $response = self::$httpResponses[self::HTTP_NOT_IMPLEMENTED];
  335. }
  336. $handshake = array(
  337. sprintf(self::RESPONSE_LINE_FORMAT, $status, $response)
  338. );
  339. foreach ($headers as $name => $value) {
  340. $handshake[] = sprintf(self::HEADER_LINE_FORMAT, $name, $value);
  341. }
  342. return implode($handshake, "\r\n") . "\r\n\r\n";
  343. }
  344. /**
  345. * @todo better header handling
  346. * @todo throw exception
  347. * @param unknown_type $response
  348. * @param unknown_type $key
  349. * @return boolean
  350. */
  351. public function validateResponseHandshake($response, $key)
  352. {
  353. if (!$response) {
  354. return false;
  355. }
  356. $headers = $this->getHeaders($response);
  357. if (!isset($headers[self::HEADER_ACCEPT])) {
  358. throw new HandshakeException('No accept header receieved on handshake response');
  359. }
  360. $accept = $headers[self::HEADER_ACCEPT];
  361. if (!$accept) {
  362. throw new HandshakeException('Invalid accept header');
  363. }
  364. $expected = $this->getAcceptValue($key);
  365. preg_match('#Sec-WebSocket-Accept:\s(.*)$#mU', $response, $matches);
  366. $keyAccept = trim($matches[1]);
  367. return ($keyAccept === $this->getEncodedHash($key)) ? true : false;
  368. }
  369. /**
  370. * Gets an encoded hash for a key
  371. *
  372. * @param string $key
  373. * @return string
  374. */
  375. public function getEncodedHash($key)
  376. {
  377. return base64_encode(pack('H*', sha1($key . self::MAGIC_GUID)));
  378. }
  379. /**
  380. * Validates a request handshake
  381. *
  382. * @param string $request
  383. * @throws BadRequestException
  384. */
  385. public function validateRequestHandshake(
  386. $request
  387. ) {
  388. if (!$request) {
  389. return false;
  390. }
  391. list($request, $headers) = $this->getRequestHeaders($request);
  392. $path = $this->validateRequestLine($request);
  393. if (!isset($headers[self::HEADER_ORIGIN]) || !$headers[self::HEADER_ORIGIN]) {
  394. throw new BadRequestException('No origin header');
  395. }
  396. $origin = $headers[self::HEADER_ORIGIN];
  397. if (!isset($headers[self::HEADER_UPGRADE])
  398. || strtolower($headers[self::HEADER_UPGRADE]) != self::UPGRADE_VALUE
  399. ) {
  400. throw new BadRequestException('Invalid upgrade header');
  401. }
  402. if (!isset($headers[self::HEADER_CONNECTION])
  403. || strpos($headers[self::HEADER_CONNECTION], self::CONNECTION_VALUE) === false
  404. ) {
  405. throw new BadRequestException('Invalid connection header');
  406. }
  407. if (!isset($headers[self::HEADER_HOST])) {
  408. // @todo Validate host == listening socket? Or would that break
  409. // TCP proxies?
  410. throw new BadRequestException('No host header');
  411. }
  412. if (!isset($headers[self::HEADER_VERSION])) {
  413. throw new BadRequestException('No version header received on handshake request');
  414. }
  415. if (!$this->acceptsVersion($headers[self::HEADER_VERSION])) {
  416. throw new BadRequestException('Unsupported version: ' . $version);
  417. }
  418. if (!isset($headers[self::HEADER_KEY])) {
  419. throw new BadRequestException('No key header received');
  420. }
  421. $key = trim($headers[self::HEADER_KEY]);
  422. if (!$key) {
  423. throw new BadRequestException('Invalid key');
  424. }
  425. // Optional
  426. $protocol = isset($headers[self::HEADER_PROTOCOL]) ? $headers[self::HEADER_PROTOCOL] : null;
  427. $extensions = array();
  428. if (isset($headers[self::HEADER_EXTENSIONS]) && $headers[self::HEADER_EXTENSIONS]) {
  429. $extensions = $headers[self::HEADER_EXTENSIONS];
  430. if (is_scalar($extensions)) {
  431. $extensions = array($extensions);
  432. }
  433. }
  434. return array($path, $origin, $key, $extensions, $protocol);
  435. }
  436. /**
  437. * Gets a suitable WebSocket close frame
  438. *
  439. * @param Exception|int $e
  440. */
  441. public function getCloseFrame($e)
  442. {
  443. $code = false;
  444. if ($e instanceof Exception) {
  445. $code = $e->getCode();
  446. } elseif (is_numeric($e)) {
  447. $code = (int)$e;
  448. }
  449. if (!$code || !key_exists($code, self::$closeReasons)) {
  450. $code = self::CLOSE_UNEXPECTED;
  451. }
  452. $body = pack('n', $code) . self::$closeReasons[$code];
  453. $payload = $this->getPayload();
  454. return $payload->encode($body, self::TYPE_CLOSE);
  455. }
  456. /**
  457. * Validates a WebSocket URI
  458. *
  459. * @param string $uri
  460. * @return array(string $scheme, string $host, int $port, string $path)
  461. */
  462. public function validateUri($uri)
  463. {
  464. $uri = (string)$uri;
  465. if (!$uri) {
  466. throw new InvalidArgumentException('Invalid URI');
  467. }
  468. $scheme = parse_url($uri, PHP_URL_SCHEME);
  469. $this->validateScheme($scheme);
  470. $host = parse_url($uri, PHP_URL_HOST);
  471. if (!$host) {
  472. throw new InvalidArgumentException("Invalid host");
  473. }
  474. $port = parse_url($uri, PHP_URL_PORT);
  475. if (!$port) {
  476. $port = $this->getPort($scheme);
  477. }
  478. $path = parse_url($uri, PHP_URL_PATH);
  479. if (!$path) {
  480. throw new InvalidArgumentException('Invalid path');
  481. }
  482. return array($scheme, $host, $port, $path);
  483. }
  484. /**
  485. * Validates a socket URI
  486. *
  487. * @param string $uri
  488. * @throws InvalidArgumentException
  489. * @return array(string $scheme, string $host, string $port)
  490. */
  491. public function validateSocketUri($uri)
  492. {
  493. $uri = (string)$uri;
  494. if (!$uri) {
  495. throw new InvalidArgumentException('Invalid URI');
  496. }
  497. $scheme = parse_url($uri, PHP_URL_SCHEME);
  498. $scheme = $this->validateScheme($scheme);
  499. $host = parse_url($uri, PHP_URL_HOST);
  500. if (!$host) {
  501. throw new InvalidArgumentException("Invalid host");
  502. }
  503. $port = parse_url($uri, PHP_URL_PORT);
  504. if (!$port) {
  505. $port = $this->getPort($scheme);
  506. }
  507. return array($scheme, $host, $port);
  508. }
  509. /**
  510. * Validates an origin URI
  511. *
  512. * @param string $origin
  513. * @throws InvalidArgumentException
  514. * @return string
  515. */
  516. public function validateOriginUri($origin)
  517. {
  518. $origin = (string)$origin;
  519. if (!$origin) {
  520. throw new InvalidArgumentException('Invalid URI');
  521. }
  522. $scheme = parse_url($origin, PHP_URL_SCHEME);
  523. if (!$scheme) {
  524. throw new InvalidArgumentException('Invalid scheme');
  525. }
  526. $host = parse_url($origin, PHP_URL_HOST);
  527. if (!$host) {
  528. throw new InvalidArgumentException("Invalid host");
  529. }
  530. return $origin;
  531. }
  532. /**
  533. * Validates a request line
  534. *
  535. * @param string $line
  536. * @throws BadRequestException
  537. */
  538. protected function validateRequestLine($line)
  539. {
  540. $matches = array(0 => null, 1 => null);
  541. if (!preg_match(self::REQUEST_LINE_REGEX, $line, $matches) || !$matches[1]) {
  542. throw new BadRequestException('Invalid request line', 400);
  543. }
  544. return $matches[1];
  545. }
  546. /**
  547. * Gets the expected accept value for a handshake response
  548. *
  549. * Note that the protocol calls for the base64 encoded value to be hashed,
  550. * not the original 16 byte random key.
  551. *
  552. * @see http://tools.ietf.org/html/rfc6455#section-4.2.2
  553. * @param string $key
  554. */
  555. protected function getAcceptValue($encoded_key)
  556. {
  557. return base64_encode(sha1($encoded_key . self::MAGIC_GUID, true));
  558. }
  559. /**
  560. * Gets the headers from a full response
  561. *
  562. * @param string $response
  563. * @return array()
  564. * @throws InvalidArgumentException
  565. */
  566. protected function getHeaders($response, &$request_line = null)
  567. {
  568. $parts = explode("\r\n\r\n", $response, 2);
  569. if (count($parts) != 2) {
  570. $parts = array($parts, '');
  571. }
  572. list($headers, $body) = $parts;
  573. $return = array();
  574. foreach (explode("\r\n", $headers) as $header) {
  575. $parts = explode(': ', $header, 2);
  576. if (count($parts) == 2) {
  577. list($name, $value) = $parts;
  578. if (!isset($return[$name])) {
  579. $return[$name] = $value;
  580. } else {
  581. if (is_array($return[$name])) {
  582. $return[$name][] = $value;
  583. } else {
  584. $return[$name] = array($return[$name], $value);
  585. }
  586. }
  587. }
  588. }
  589. return $return;
  590. }
  591. /**
  592. * Gets request headers
  593. *
  594. * @param string $response
  595. * @return array<string, array<string>> The request line, and an array of
  596. * headers
  597. * @throws InvalidArgumentException
  598. */
  599. protected function getRequestHeaders($response)
  600. {
  601. $eol = strpos($response, "\r\n");
  602. if ($eol === false) {
  603. throw new InvalidArgumentException('Invalid request line');
  604. }
  605. $request = substr($response, 0, $eol);
  606. $headers = $this->getHeaders(substr($response, $eol + 2));
  607. return array($request, $headers);
  608. }
  609. /**
  610. * Validates a scheme
  611. *
  612. * @param string $scheme
  613. * @return string Underlying scheme
  614. * @throws InvalidArgumentException
  615. */
  616. protected function validateScheme($scheme)
  617. {
  618. if (!$scheme) {
  619. throw new InvalidArgumentException('No scheme specified');
  620. }
  621. if (!in_array($scheme, self::$schemes)) {
  622. throw new InvalidArgumentException(
  623. 'Unknown socket scheme: ' . $scheme
  624. );
  625. }
  626. if ($scheme == self::SCHEME_WEBSOCKET_SECURE) {
  627. return self::SCHEME_UNDERLYING_SECURE;
  628. }
  629. return self::SCHEME_UNDERLYING;
  630. }
  631. /**
  632. * Gets the default request headers
  633. *
  634. * @param string $host
  635. * @param string $key
  636. * @param string $origin
  637. * @param int $version
  638. * @return multitype:unknown string NULL
  639. */
  640. protected function getDefaultRequestHeaders($host, $key, $origin)
  641. {
  642. return array(
  643. self::HEADER_HOST => $host,
  644. self::HEADER_UPGRADE => self::UPGRADE_VALUE,
  645. self::HEADER_CONNECTION => self::CONNECTION_VALUE,
  646. self::HEADER_KEY => $key,
  647. self::HEADER_ORIGIN => $origin,
  648. self::HEADER_VERSION => $this->getVersion()
  649. );
  650. }
  651. /**
  652. * Gets the default response headers
  653. *
  654. * @param string $key
  655. */
  656. protected function getSuccessResponseHeaders($key)
  657. {
  658. return array(
  659. self::HEADER_UPGRADE => self::UPGRADE_VALUE,
  660. self::HEADER_CONNECTION => self::CONNECTION_VALUE,
  661. self::HEADER_ACCEPT => $this->getAcceptValue($key)
  662. );
  663. }
  664. /**
  665. * Gets the default port for a scheme
  666. *
  667. * By default, the WebSocket Protocol uses port 80 for regular WebSocket
  668. * connections and port 443 for WebSocket connections tunneled over
  669. * Transport Layer Security
  670. *
  671. * @param string $uri
  672. * @return int
  673. */
  674. protected function getPort($scheme)
  675. {
  676. if ($scheme == self::SCHEME_WEBSOCKET) {
  677. return 80;
  678. } elseif ($scheme == self::SCHEME_WEBSOCKET_SECURE) {
  679. return 443;
  680. } elseif ($scheme == self::SCHEME_UNDERLYING) {
  681. return 80;
  682. } elseif ($scheme == self::SCHEME_UNDERLYING_SECURE) {
  683. return 443;
  684. } else {
  685. throw new InvalidArgumentException('Unknown websocket scheme');
  686. }
  687. }
  688. }