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