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