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    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}
142