PageRenderTime 43ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/core/lib/Wrench/Connection.php

https://gitlab.com/daveson/lacitypatterns
PHP | 538 lines | 257 code | 71 blank | 210 comment | 19 complexity | 39ff4a1b89cd68599a4f5d927572c5ed MD5 | raw file
  1. <?php
  2. namespace Wrench;
  3. use Wrench\Payload\PayloadHandler;
  4. use Wrench\Protocol\Protocol;
  5. use Wrench\Payload\Payload;
  6. use Wrench\Util\Configurable;
  7. use Wrench\Socket\ServerClientSocket;
  8. use Wrench\Server;
  9. use Wrench\Exception as WrenchException;
  10. use Wrench\Exception\CloseException;
  11. use Wrench\Exception\ConnectionException;
  12. use Wrench\Exception\HandshakeException;
  13. use Wrench\Exception\BadRequestException;
  14. use \Exception;
  15. use \RuntimeException;
  16. /**
  17. * Represents a client connection on the server side
  18. *
  19. * i.e. the `Server` manages a bunch of `Connection`s
  20. */
  21. class Connection extends Configurable
  22. {
  23. /**
  24. * The connection manager
  25. *
  26. * @var Wrench\ConnectionManager
  27. */
  28. protected $manager;
  29. /**
  30. * Socket object
  31. *
  32. * Wraps the client connection resource
  33. *
  34. * @var ServerClientSocket
  35. */
  36. protected $socket;
  37. /**
  38. * Whether the connection has successfully handshaken
  39. *
  40. * @var boolean
  41. */
  42. protected $handshaked = false;
  43. /**
  44. * The application this connection belongs to
  45. *
  46. * @var Application
  47. */
  48. protected $application = null;
  49. /**
  50. * The IP address of the client
  51. *
  52. * @var string
  53. */
  54. protected $ip;
  55. /**
  56. * The port of the client
  57. *
  58. * @var int
  59. */
  60. protected $port;
  61. /**
  62. * The array of headers included with the original request (like Cookie for example)
  63. * The headers specific to the web sockets handshaking have been stripped out
  64. *
  65. * @var array
  66. */
  67. protected $headers = null;
  68. /**
  69. * The array of query parameters included in the original request
  70. * The array is in the format 'key' => 'value'
  71. *
  72. * @var array
  73. */
  74. protected $queryParams = null;
  75. /**
  76. * Connection ID
  77. *
  78. * @var string|null
  79. */
  80. protected $id = null;
  81. /**
  82. * @var PayloadHandler
  83. */
  84. protected $payloadHandler;
  85. /**
  86. * Constructor
  87. *
  88. * @param Server $server
  89. * @param ServerClientSocket $socket
  90. * @param array $options
  91. * @throws InvalidArgumentException
  92. */
  93. public function __construct(
  94. ConnectionManager $manager,
  95. ServerClientSocket $socket,
  96. array $options = array()
  97. ) {
  98. $this->manager = $manager;
  99. $this->socket = $socket;
  100. parent::__construct($options);
  101. $this->configureClientInformation();
  102. $this->configurePayloadHandler();
  103. $this->log('Connected');
  104. }
  105. /**
  106. * Gets the connection manager of this connection
  107. *
  108. * @return \Wrench\ConnectionManager
  109. */
  110. public function getConnectionManager()
  111. {
  112. return $this->manager;
  113. }
  114. /**
  115. * @see Wrench\Util.Configurable::configure()
  116. */
  117. protected function configure(array $options)
  118. {
  119. $options = array_merge(array(
  120. 'connection_id_secret' => 'asu5gj656h64Da(0crt8pud%^WAYWW$u76dwb',
  121. 'connection_id_algo' => 'sha512',
  122. ), $options);
  123. parent::configure($options);
  124. }
  125. protected function configurePayloadHandler()
  126. {
  127. $this->payloadHandler = new PayloadHandler(
  128. array($this, 'handlePayload'),
  129. $this->options
  130. );
  131. }
  132. /**
  133. * @throws RuntimeException
  134. */
  135. protected function configureClientInformation()
  136. {
  137. $this->ip = $this->socket->getIp();
  138. $this->port = $this->socket->getPort();
  139. $this->configureClientId();
  140. }
  141. /**
  142. * Configures the client ID
  143. *
  144. * We hash the client ID to prevent leakage of information if another client
  145. * happens to get a hold of an ID. The secret *must* be lengthy, and must
  146. * be kept secret for this to work: otherwise it's trivial to search the space
  147. * of possible IP addresses/ports (well, if not trivial, at least very fast).
  148. */
  149. protected function configureClientId()
  150. {
  151. $message = sprintf(
  152. '%s:uri=%s&ip=%s&port=%s',
  153. $this->options['connection_id_secret'],
  154. rawurlencode($this->manager->getUri()),
  155. rawurlencode($this->ip),
  156. rawurlencode($this->port)
  157. );
  158. $algo = $this->options['connection_id_algo'];
  159. if (extension_loaded('gmp')) {
  160. $hash = hash($algo, $message);
  161. $hash = gmp_strval(gmp_init('0x' . $hash, 16), 62);
  162. } else {
  163. // @codeCoverageIgnoreStart
  164. $hash = hash($algo, $message);
  165. // @codeCoverageIgnoreEnd
  166. }
  167. $this->id = $hash;
  168. }
  169. /**
  170. * Data receiver
  171. *
  172. * Called by the connection manager when the connection has received data
  173. *
  174. * @param string $data
  175. */
  176. public function onData($data)
  177. {
  178. if (!$this->handshaked) {
  179. return $this->handshake($data);
  180. }
  181. return $this->handle($data);
  182. }
  183. /**
  184. * Performs a websocket handshake
  185. *
  186. * @param string $data
  187. * @throws BadRequestException
  188. * @throws HandshakeException
  189. * @throws WrenchException
  190. */
  191. public function handshake($data)
  192. {
  193. try {
  194. list($path, $origin, $key, $extensions, $protocol, $headers, $params)
  195. = $this->protocol->validateRequestHandshake($data);
  196. $this->headers = $headers;
  197. $this->queryParams = $params;
  198. $this->application = $this->manager->getApplicationForPath($path);
  199. if (!$this->application) {
  200. throw new BadRequestException('Invalid application');
  201. }
  202. $this->manager->getServer()->notify(
  203. Server::EVENT_HANDSHAKE_REQUEST,
  204. array($this, $path, $origin, $key, $extensions)
  205. );
  206. $response = $this->protocol->getResponseHandshake($key);
  207. if (!$this->socket->isConnected()) {
  208. throw new HandshakeException('Socket is not connected');
  209. }
  210. if ($this->socket->send($response) === false) {
  211. throw new HandshakeException('Could not send handshake response');
  212. }
  213. $this->handshaked = true;
  214. $this->log(sprintf(
  215. 'Handshake successful: %s:%d (%s) connected to %s',
  216. $this->getIp(),
  217. $this->getPort(),
  218. $this->getId(),
  219. $path
  220. ), 'info');
  221. $this->manager->getServer()->notify(
  222. Server::EVENT_HANDSHAKE_SUCCESSFUL,
  223. array($this)
  224. );
  225. if (method_exists($this->application, 'onConnect')) {
  226. $this->application->onConnect($this);
  227. }
  228. } catch (WrenchException $e) {
  229. $this->log('Handshake failed: ' . $e, 'err');
  230. $this->close($e);
  231. }
  232. }
  233. /**
  234. * Returns a string export of the given binary data
  235. *
  236. * @param string $data
  237. * @return string
  238. */
  239. protected function export($data)
  240. {
  241. $export = '';
  242. foreach (str_split($data) as $chr) {
  243. $export .= '\\x' . ord($chr);
  244. }
  245. }
  246. /**
  247. * Handle data received from the client
  248. *
  249. * The data passed in may belong to several different frames across one or
  250. * more protocols. It may not even contain a single complete frame. This method
  251. * manages slotting the data into separate payload objects.
  252. *
  253. * @todo An endpoint MUST be capable of handling control frames in the
  254. * middle of a fragmented message.
  255. * @param string $data
  256. * @return void
  257. */
  258. public function handle($data)
  259. {
  260. $this->payloadHandler->handle($data);
  261. }
  262. /**
  263. * Handle a complete payload received from the client
  264. *
  265. * Public because called from our PayloadHandler
  266. *
  267. * @param string $payload
  268. */
  269. public function handlePayload(Payload $payload)
  270. {
  271. $app = $this->getClientApplication();
  272. $this->log('Handling payload: ' . $payload->getPayload(), 'debug');
  273. switch ($type = $payload->getType()) {
  274. case Protocol::TYPE_TEXT:
  275. if (method_exists($app, 'onData')) {
  276. $app->onData($payload, $this);
  277. }
  278. return;
  279. case Protocol::TYPE_BINARY:
  280. if(method_exists($app, 'onBinaryData')) {
  281. $app->onBinaryData($payload, $this);
  282. } else {
  283. $this->close(1003);
  284. }
  285. break;
  286. case Protocol::TYPE_PING:
  287. $this->log('Ping received', 'notice');
  288. $this->send($payload->getPayload(), Protocol::TYPE_PONG);
  289. $this->log('Pong!', 'debug');
  290. break;
  291. /**
  292. * A Pong frame MAY be sent unsolicited. This serves as a
  293. * unidirectional heartbeat. A response to an unsolicited Pong
  294. * frame is not expected.
  295. */
  296. case Protocol::TYPE_PONG:
  297. $this->log('Received unsolicited pong', 'info');
  298. break;
  299. case Protocol::TYPE_CLOSE:
  300. $this->log('Close frame received', 'notice');
  301. $this->close();
  302. $this->log('Disconnected', 'info');
  303. break;
  304. default:
  305. throw new ConnectionException('Unhandled payload type');
  306. }
  307. }
  308. /**
  309. * Sends the payload to the connection
  310. *
  311. * @param string $payload
  312. * @param string $type
  313. * @throws HandshakeException
  314. * @throws ConnectionException
  315. * @return boolean
  316. */
  317. public function send($data, $type = Protocol::TYPE_TEXT)
  318. {
  319. if (!$this->handshaked) {
  320. throw new HandshakeException('Connection is not handshaked');
  321. }
  322. $payload = $this->protocol->getPayload();
  323. // Servers don't send masked payloads
  324. $payload->encode($data, $type, false);
  325. if (!$payload->sendToSocket($this->socket)) {
  326. $this->log('Could not send payload to client', 'warn');
  327. throw new ConnectionException('Could not send data to connection: ' . $this->socket->getLastError());
  328. }
  329. return true;
  330. }
  331. /**
  332. * Processes data on the socket
  333. *
  334. * @throws CloseException
  335. */
  336. public function process()
  337. {
  338. $data = $this->socket->receive();
  339. $bytes = strlen($data);
  340. if ($bytes === 0 || $data === false) {
  341. throw new CloseException('Error reading data from socket: ' . $this->socket->getLastError());
  342. }
  343. $this->onData($data);
  344. }
  345. /**
  346. * Closes the connection according to the WebSocket protocol
  347. *
  348. * If an endpoint receives a Close frame and that endpoint did not
  349. * previously send a Close frame, the endpoint MUST send a Close frame
  350. * in response. It SHOULD do so as soon as is practical. An endpoint
  351. * MAY delay sending a close frame until its current message is sent
  352. * (for instance, if the majority of a fragmented message is already
  353. * sent, an endpoint MAY send the remaining fragments before sending a
  354. * Close frame). However, there is no guarantee that the endpoint which
  355. * has already sent a Close frame will continue to process data.
  356. * After both sending and receiving a close message, an endpoint
  357. * considers the WebSocket connection closed, and MUST close the
  358. * underlying TCP connection. The server MUST close the underlying TCP
  359. * connection immediately; the client SHOULD wait for the server to
  360. * close the connection but MAY close the connection at any time after
  361. * sending and receiving a close message, e.g. if it has not received a
  362. * TCP close from the server in a reasonable time period.
  363. *
  364. * @param int|Exception $statusCode
  365. * @return boolean
  366. */
  367. public function close($code = Protocol::CLOSE_NORMAL)
  368. {
  369. try {
  370. if (!$this->handshaked) {
  371. $response = $this->protocol->getResponseError($code);
  372. $this->socket->send($response);
  373. } else {
  374. $response = $this->protocol->getCloseFrame($code);
  375. $this->socket->send($response);
  376. }
  377. } catch (Exception $e) {
  378. $this->log('Unable to send close message', 'warning');
  379. }
  380. if ($this->application && method_exists($this->application, 'onDisconnect')) {
  381. $this->application->onDisconnect($this);
  382. }
  383. $this->socket->disconnect();
  384. $this->manager->removeConnection($this);
  385. }
  386. /**
  387. * Logs a message
  388. *
  389. * @param string $message
  390. * @param string $priority
  391. */
  392. public function log($message, $priority = 'info')
  393. {
  394. $this->manager->log(sprintf(
  395. '%s: %s:%d (%s): %s',
  396. __CLASS__,
  397. $this->getIp(),
  398. $this->getPort(),
  399. $this->getId(),
  400. $message
  401. ), $priority);
  402. }
  403. /**
  404. * Gets the IP address of the connection
  405. *
  406. * @return string Usually dotted quad notation
  407. */
  408. public function getIp()
  409. {
  410. return $this->ip;
  411. }
  412. /**
  413. * Gets the port of the connection
  414. *
  415. * @return int
  416. */
  417. public function getPort()
  418. {
  419. return $this->port;
  420. }
  421. /**
  422. * Gets the non-web-sockets headers included with the original request
  423. *
  424. * @return array
  425. */
  426. public function getHeaders()
  427. {
  428. return $this->headers;
  429. }
  430. /**
  431. * Gets the query parameters included with the original request
  432. *
  433. * @return array
  434. */
  435. public function getQueryParams()
  436. {
  437. return $this->queryParams;
  438. }
  439. /**
  440. * Gets the connection ID
  441. *
  442. * @return string
  443. */
  444. public function getId()
  445. {
  446. return $this->id;
  447. }
  448. /**
  449. * Gets the socket object
  450. *
  451. * @return Socket\ServerClientSocket
  452. */
  453. public function getSocket()
  454. {
  455. return $this->socket;
  456. }
  457. /**
  458. * Gets the client application
  459. *
  460. * @return Application
  461. */
  462. public function getClientApplication()
  463. {
  464. return (isset($this->application)) ? $this->application : false;
  465. }
  466. }