friendship ended with social-app. php is my new best friend
1<?php
2
3namespace React\Socket;
4
5use React\EventLoop\LoopInterface;
6use React\Promise\Deferred;
7use RuntimeException;
8use 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 */
16class 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 /**
48 * @param Connection $stream
49 * @return \React\Promise\PromiseInterface<Connection>
50 */
51 public function enable(Connection $stream)
52 {
53 return $this->toggle($stream, true);
54 }
55
56 /**
57 * @param Connection $stream
58 * @param bool $toggle
59 * @return \React\Promise\PromiseInterface<Connection>
60 */
61 public function toggle(Connection $stream, $toggle)
62 {
63 // pause actual stream instance to continue operation on raw stream socket
64 $stream->pause();
65
66 // TODO: add write() event to make sure we're not sending any excessive data
67
68 // cancelling this leaves this stream in an inconsistent state…
69 $deferred = new Deferred(function () {
70 throw new \RuntimeException();
71 });
72
73 // get actual stream socket from stream instance
74 $socket = $stream->stream;
75
76 // get crypto method from context options or use global setting from constructor
77 $method = $this->method;
78 $context = \stream_context_get_options($socket);
79 if (isset($context['ssl']['crypto_method'])) {
80 $method = $context['ssl']['crypto_method'];
81 }
82
83 $that = $this;
84 $toggleCrypto = function () use ($socket, $deferred, $toggle, $method, $that) {
85 $that->toggleCrypto($socket, $deferred, $toggle, $method);
86 };
87
88 $this->loop->addReadStream($socket, $toggleCrypto);
89
90 if (!$this->server) {
91 $toggleCrypto();
92 }
93
94 $loop = $this->loop;
95
96 return $deferred->promise()->then(function () use ($stream, $socket, $loop, $toggle) {
97 $loop->removeReadStream($socket);
98
99 $stream->encryptionEnabled = $toggle;
100 $stream->resume();
101
102 return $stream;
103 }, function($error) use ($stream, $socket, $loop) {
104 $loop->removeReadStream($socket);
105 $stream->resume();
106 throw $error;
107 });
108 }
109
110 /**
111 * @internal
112 * @param resource $socket
113 * @param Deferred<null> $deferred
114 * @param bool $toggle
115 * @param int $method
116 * @return void
117 */
118 public function toggleCrypto($socket, Deferred $deferred, $toggle, $method)
119 {
120 $error = null;
121 \set_error_handler(function ($_, $errstr) use (&$error) {
122 $error = \str_replace(array("\r", "\n"), ' ', $errstr);
123
124 // remove useless function name from error message
125 if (($pos = \strpos($error, "): ")) !== false) {
126 $error = \substr($error, $pos + 3);
127 }
128 });
129
130 $result = \stream_socket_enable_crypto($socket, $toggle, $method);
131
132 \restore_error_handler();
133
134 if (true === $result) {
135 $deferred->resolve(null);
136 } else if (false === $result) {
137 // overwrite callback arguments for PHP7+ only, so they do not show
138 // up in the Exception trace and do not cause a possible cyclic reference.
139 $d = $deferred;
140 $deferred = null;
141
142 if (\feof($socket) || $error === null) {
143 // EOF or failed without error => connection closed during handshake
144 $d->reject(new \UnexpectedValueException(
145 'Connection lost during TLS handshake (ECONNRESET)',
146 \defined('SOCKET_ECONNRESET') ? \SOCKET_ECONNRESET : 104
147 ));
148 } else {
149 // handshake failed with error message
150 $d->reject(new \UnexpectedValueException(
151 $error
152 ));
153 }
154 } else {
155 // need more data, will retry
156 }
157 }
158}