PageRenderTime 49ms CodeModel.GetById 16ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/Wrench/Connection.php

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