StreamEncryption.php 5.2 KB

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