| 3 |
liveuser |
1 |
<?php
|
|
|
2 |
|
|
|
3 |
namespace React\Socket;
|
|
|
4 |
|
|
|
5 |
use React\EventLoop\LoopInterface;
|
|
|
6 |
use React\Promise\Deferred;
|
|
|
7 |
use RuntimeException;
|
|
|
8 |
use UnexpectedValueException;
|
|
|
9 |
|
|
|
10 |
/**
|
|
|
11 |
* This class is considered internal and its API should not be relied upon
|
|
|
12 |
* outside of Socket.
|
|
|
13 |
*
|
|
|
14 |
* @internal
|
|
|
15 |
*/
|
|
|
16 |
class StreamEncryption
|
|
|
17 |
{
|
|
|
18 |
private $loop;
|
|
|
19 |
private $method;
|
|
|
20 |
private $server;
|
|
|
21 |
|
|
|
22 |
public function __construct(LoopInterface $loop, $server = true)
|
|
|
23 |
{
|
|
|
24 |
$this->loop = $loop;
|
|
|
25 |
$this->server = $server;
|
|
|
26 |
|
|
|
27 |
// support TLSv1.0+ by default and exclude legacy SSLv2/SSLv3.
|
|
|
28 |
// As of PHP 7.2+ the main crypto method constant includes all TLS versions.
|
|
|
29 |
// As of PHP 5.6+ the crypto method is a bitmask, so we explicitly include all TLS versions.
|
|
|
30 |
// For legacy PHP < 5.6 the crypto method is a single value only and this constant includes all TLS versions.
|
|
|
31 |
// @link https://3v4l.org/9PSST
|
|
|
32 |
if ($server) {
|
|
|
33 |
$this->method = \STREAM_CRYPTO_METHOD_TLS_SERVER;
|
|
|
34 |
|
|
|
35 |
if (\PHP_VERSION_ID < 70200 && \PHP_VERSION_ID >= 50600) {
|
|
|
36 |
$this->method |= \STREAM_CRYPTO_METHOD_TLSv1_0_SERVER | \STREAM_CRYPTO_METHOD_TLSv1_1_SERVER | \STREAM_CRYPTO_METHOD_TLSv1_2_SERVER; // @codeCoverageIgnore
|
|
|
37 |
}
|
|
|
38 |
} else {
|
|
|
39 |
$this->method = \STREAM_CRYPTO_METHOD_TLS_CLIENT;
|
|
|
40 |
|
|
|
41 |
if (\PHP_VERSION_ID < 70200 && \PHP_VERSION_ID >= 50600) {
|
|
|
42 |
$this->method |= \STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT | \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT | \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; // @codeCoverageIgnore
|
|
|
43 |
}
|
|
|
44 |
}
|
|
|
45 |
}
|
|
|
46 |
|
|
|
47 |
public function enable(Connection $stream)
|
|
|
48 |
{
|
|
|
49 |
return $this->toggle($stream, true);
|
|
|
50 |
}
|
|
|
51 |
|
|
|
52 |
public function toggle(Connection $stream, $toggle)
|
|
|
53 |
{
|
|
|
54 |
// pause actual stream instance to continue operation on raw stream socket
|
|
|
55 |
$stream->pause();
|
|
|
56 |
|
|
|
57 |
// TODO: add write() event to make sure we're not sending any excessive data
|
|
|
58 |
|
|
|
59 |
// cancelling this leaves this stream in an inconsistent state…
|
|
|
60 |
$deferred = new Deferred(function () {
|
|
|
61 |
throw new \RuntimeException();
|
|
|
62 |
});
|
|
|
63 |
|
|
|
64 |
// get actual stream socket from stream instance
|
|
|
65 |
$socket = $stream->stream;
|
|
|
66 |
|
|
|
67 |
// get crypto method from context options or use global setting from constructor
|
|
|
68 |
$method = $this->method;
|
|
|
69 |
$context = \stream_context_get_options($socket);
|
|
|
70 |
if (isset($context['ssl']['crypto_method'])) {
|
|
|
71 |
$method = $context['ssl']['crypto_method'];
|
|
|
72 |
}
|
|
|
73 |
|
|
|
74 |
$that = $this;
|
|
|
75 |
$toggleCrypto = function () use ($socket, $deferred, $toggle, $method, $that) {
|
|
|
76 |
$that->toggleCrypto($socket, $deferred, $toggle, $method);
|
|
|
77 |
};
|
|
|
78 |
|
|
|
79 |
$this->loop->addReadStream($socket, $toggleCrypto);
|
|
|
80 |
|
|
|
81 |
if (!$this->server) {
|
|
|
82 |
$toggleCrypto();
|
|
|
83 |
}
|
|
|
84 |
|
|
|
85 |
$loop = $this->loop;
|
|
|
86 |
|
|
|
87 |
return $deferred->promise()->then(function () use ($stream, $socket, $loop, $toggle) {
|
|
|
88 |
$loop->removeReadStream($socket);
|
|
|
89 |
|
|
|
90 |
$stream->encryptionEnabled = $toggle;
|
|
|
91 |
$stream->resume();
|
|
|
92 |
|
|
|
93 |
return $stream;
|
|
|
94 |
}, function($error) use ($stream, $socket, $loop) {
|
|
|
95 |
$loop->removeReadStream($socket);
|
|
|
96 |
$stream->resume();
|
|
|
97 |
throw $error;
|
|
|
98 |
});
|
|
|
99 |
}
|
|
|
100 |
|
|
|
101 |
public function toggleCrypto($socket, Deferred $deferred, $toggle, $method)
|
|
|
102 |
{
|
|
|
103 |
$error = null;
|
|
|
104 |
\set_error_handler(function ($_, $errstr) use (&$error) {
|
|
|
105 |
$error = \str_replace(array("\r", "\n"), ' ', $errstr);
|
|
|
106 |
|
|
|
107 |
// remove useless function name from error message
|
|
|
108 |
if (($pos = \strpos($error, "): ")) !== false) {
|
|
|
109 |
$error = \substr($error, $pos + 3);
|
|
|
110 |
}
|
|
|
111 |
});
|
|
|
112 |
|
|
|
113 |
$result = \stream_socket_enable_crypto($socket, $toggle, $method);
|
|
|
114 |
|
|
|
115 |
\restore_error_handler();
|
|
|
116 |
|
|
|
117 |
if (true === $result) {
|
|
|
118 |
$deferred->resolve();
|
|
|
119 |
} else if (false === $result) {
|
|
|
120 |
// overwrite callback arguments for PHP7+ only, so they do not show
|
|
|
121 |
// up in the Exception trace and do not cause a possible cyclic reference.
|
|
|
122 |
$d = $deferred;
|
|
|
123 |
$deferred = null;
|
|
|
124 |
|
|
|
125 |
if (\feof($socket) || $error === null) {
|
|
|
126 |
// EOF or failed without error => connection closed during handshake
|
|
|
127 |
$d->reject(new \UnexpectedValueException(
|
|
|
128 |
'Connection lost during TLS handshake',
|
|
|
129 |
\defined('SOCKET_ECONNRESET') ? \SOCKET_ECONNRESET : 0
|
|
|
130 |
));
|
|
|
131 |
} else {
|
|
|
132 |
// handshake failed with error message
|
|
|
133 |
$d->reject(new \UnexpectedValueException(
|
|
|
134 |
'Unable to complete TLS handshake: ' . $error
|
|
|
135 |
));
|
|
|
136 |
}
|
|
|
137 |
} else {
|
|
|
138 |
// need more data, will retry
|
|
|
139 |
}
|
|
|
140 |
}
|
|
|
141 |
}
|