PageRenderTime 28ms CodeModel.GetById 14ms RepoModel.GetById 1ms app.codeStats 0ms

/www/framework/Wrench/Connection.php

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