SecureServer.php 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. <?php
  2. namespace React\Socket;
  3. use Evenement\EventEmitter;
  4. use React\EventLoop\Loop;
  5. use React\EventLoop\LoopInterface;
  6. use BadMethodCallException;
  7. use UnexpectedValueException;
  8. /**
  9. * The `SecureServer` class implements the `ServerInterface` and is responsible
  10. * for providing a secure TLS (formerly known as SSL) server.
  11. *
  12. * It does so by wrapping a `TcpServer` instance which waits for plaintext
  13. * TCP/IP connections and then performs a TLS handshake for each connection.
  14. *
  15. * ```php
  16. * $server = new React\Socket\TcpServer(8000);
  17. * $server = new React\Socket\SecureServer($server, null, array(
  18. * // tls context options here…
  19. * ));
  20. * ```
  21. *
  22. * Whenever a client completes the TLS handshake, it will emit a `connection` event
  23. * with a connection instance implementing [`ConnectionInterface`](#connectioninterface):
  24. *
  25. * ```php
  26. * $server->on('connection', function (React\Socket\ConnectionInterface $connection) {
  27. * echo 'Secure connection from' . $connection->getRemoteAddress() . PHP_EOL;
  28. *
  29. * $connection->write('hello there!' . PHP_EOL);
  30. * …
  31. * });
  32. * ```
  33. *
  34. * Whenever a client fails to perform a successful TLS handshake, it will emit an
  35. * `error` event and then close the underlying TCP/IP connection:
  36. *
  37. * ```php
  38. * $server->on('error', function (Exception $e) {
  39. * echo 'Error' . $e->getMessage() . PHP_EOL;
  40. * });
  41. * ```
  42. *
  43. * See also the `ServerInterface` for more details.
  44. *
  45. * Note that the `SecureServer` class is a concrete implementation for TLS sockets.
  46. * If you want to typehint in your higher-level protocol implementation, you SHOULD
  47. * use the generic `ServerInterface` instead.
  48. *
  49. * @see ServerInterface
  50. * @see ConnectionInterface
  51. */
  52. final class SecureServer extends EventEmitter implements ServerInterface
  53. {
  54. private $tcp;
  55. private $encryption;
  56. private $context;
  57. /**
  58. * Creates a secure TLS server and starts waiting for incoming connections
  59. *
  60. * It does so by wrapping a `TcpServer` instance which waits for plaintext
  61. * TCP/IP connections and then performs a TLS handshake for each connection.
  62. * It thus requires valid [TLS context options],
  63. * which in its most basic form may look something like this if you're using a
  64. * PEM encoded certificate file:
  65. *
  66. * ```php
  67. * $server = new React\Socket\TcpServer(8000);
  68. * $server = new React\Socket\SecureServer($server, null, array(
  69. * 'local_cert' => 'server.pem'
  70. * ));
  71. * ```
  72. *
  73. * Note that the certificate file will not be loaded on instantiation but when an
  74. * incoming connection initializes its TLS context.
  75. * This implies that any invalid certificate file paths or contents will only cause
  76. * an `error` event at a later time.
  77. *
  78. * If your private key is encrypted with a passphrase, you have to specify it
  79. * like this:
  80. *
  81. * ```php
  82. * $server = new React\Socket\TcpServer(8000);
  83. * $server = new React\Socket\SecureServer($server, null, array(
  84. * 'local_cert' => 'server.pem',
  85. * 'passphrase' => 'secret'
  86. * ));
  87. * ```
  88. *
  89. * Note that available [TLS context options],
  90. * their defaults and effects of changing these may vary depending on your system
  91. * and/or PHP version.
  92. * Passing unknown context options has no effect.
  93. *
  94. * This class takes an optional `LoopInterface|null $loop` parameter that can be used to
  95. * pass the event loop instance to use for this object. You can use a `null` value
  96. * here in order to use the [default loop](https://github.com/reactphp/event-loop#loop).
  97. * This value SHOULD NOT be given unless you're sure you want to explicitly use a
  98. * given event loop instance.
  99. *
  100. * Advanced usage: Despite allowing any `ServerInterface` as first parameter,
  101. * you SHOULD pass a `TcpServer` instance as first parameter, unless you
  102. * know what you're doing.
  103. * Internally, the `SecureServer` has to set the required TLS context options on
  104. * the underlying stream resources.
  105. * These resources are not exposed through any of the interfaces defined in this
  106. * package, but only through the internal `Connection` class.
  107. * The `TcpServer` class is guaranteed to emit connections that implement
  108. * the `ConnectionInterface` and uses the internal `Connection` class in order to
  109. * expose these underlying resources.
  110. * If you use a custom `ServerInterface` and its `connection` event does not
  111. * meet this requirement, the `SecureServer` will emit an `error` event and
  112. * then close the underlying connection.
  113. *
  114. * @param ServerInterface|TcpServer $tcp
  115. * @param ?LoopInterface $loop
  116. * @param array $context
  117. * @throws BadMethodCallException for legacy HHVM < 3.8 due to lack of support
  118. * @see TcpServer
  119. * @link https://www.php.net/manual/en/context.ssl.php for TLS context options
  120. */
  121. public function __construct(ServerInterface $tcp, LoopInterface $loop = null, array $context = array())
  122. {
  123. if (!\function_exists('stream_socket_enable_crypto')) {
  124. throw new \BadMethodCallException('Encryption not supported on your platform (HHVM < 3.8?)'); // @codeCoverageIgnore
  125. }
  126. // default to empty passphrase to suppress blocking passphrase prompt
  127. $context += array(
  128. 'passphrase' => ''
  129. );
  130. $this->tcp = $tcp;
  131. $this->encryption = new StreamEncryption($loop ?: Loop::get());
  132. $this->context = $context;
  133. $that = $this;
  134. $this->tcp->on('connection', function ($connection) use ($that) {
  135. $that->handleConnection($connection);
  136. });
  137. $this->tcp->on('error', function ($error) use ($that) {
  138. $that->emit('error', array($error));
  139. });
  140. }
  141. public function getAddress()
  142. {
  143. $address = $this->tcp->getAddress();
  144. if ($address === null) {
  145. return null;
  146. }
  147. return \str_replace('tcp://' , 'tls://', $address);
  148. }
  149. public function pause()
  150. {
  151. $this->tcp->pause();
  152. }
  153. public function resume()
  154. {
  155. $this->tcp->resume();
  156. }
  157. public function close()
  158. {
  159. return $this->tcp->close();
  160. }
  161. /** @internal */
  162. public function handleConnection(ConnectionInterface $connection)
  163. {
  164. if (!$connection instanceof Connection) {
  165. $this->emit('error', array(new \UnexpectedValueException('Base server does not use internal Connection class exposing stream resource')));
  166. $connection->close();
  167. return;
  168. }
  169. foreach ($this->context as $name => $value) {
  170. \stream_context_set_option($connection->stream, 'ssl', $name, $value);
  171. }
  172. // get remote address before starting TLS handshake in case connection closes during handshake
  173. $remote = $connection->getRemoteAddress();
  174. $that = $this;
  175. $this->encryption->enable($connection)->then(
  176. function ($conn) use ($that) {
  177. $that->emit('connection', array($conn));
  178. },
  179. function ($error) use ($that, $connection, $remote) {
  180. $error = new \RuntimeException(
  181. 'Connection from ' . $remote . ' failed during TLS handshake: ' . $error->getMessage(),
  182. $error->getCode()
  183. );
  184. $that->emit('error', array($error));
  185. $connection->close();
  186. }
  187. );
  188. }
  189. }