From 265c22a92f9497850c6419ab0408b4889bb8da87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ha=C5=82as?= Date: Sat, 2 May 2026 09:19:14 +0200 Subject: [PATCH 1/2] M1I10: TlsProxy decorator --- src/Server/Socket/TlsProxy.php | 66 ++++++++++++++++++++ tests/Server/Socket/TlsProxyTest.php | 91 ++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 src/Server/Socket/TlsProxy.php create mode 100644 tests/Server/Socket/TlsProxyTest.php diff --git a/src/Server/Socket/TlsProxy.php b/src/Server/Socket/TlsProxy.php new file mode 100644 index 0000000..a99e840 --- /dev/null +++ b/src/Server/Socket/TlsProxy.php @@ -0,0 +1,66 @@ + $sslContextOptions + */ + public function __construct( + private readonly SocketProxyInterface $inner, + private readonly string $certFile, + private readonly string $keyFile, + private readonly array $sslContextOptions = [], + ) { + } + + public function createSocket(int $port, ProtocolType $protocol): Socket + { + return $this->inner->createSocket($port, $protocol); + } + + public function accept(Socket $socket): Connection + { + $connection = $this->inner->accept($socket); + + $reflection = new \ReflectionProperty(Connection::class, 'resource'); + /** @var \Socket $socketResource */ + $socketResource = $reflection->getValue($connection); + + $stream = @\socket_export_stream($socketResource); + + if ($stream === false) { + throw new SocketCreationException('Failed to export socket to stream'); + } + + \stream_set_timeout($stream, 30); + + \stream_context_set_option($stream, 'ssl', 'local_cert', $this->certFile); + \stream_context_set_option($stream, 'ssl', 'local_pk', $this->keyFile); + + foreach ($this->sslContextOptions as $option => $value) { + \stream_context_set_option($stream, 'ssl', $option, $value); + } + + $result = @\stream_socket_enable_crypto($stream, true, \STREAM_CRYPTO_METHOD_TLS_SERVER); + + if ($result === false) { + \fclose($stream); + throw new SocketCreationException('TLS handshake failed'); + } + + return $connection; + } + + public function isSupported(): bool + { + return \function_exists('stream_socket_enable_crypto') + && \function_exists('socket_export_stream'); + } +} diff --git a/tests/Server/Socket/TlsProxyTest.php b/tests/Server/Socket/TlsProxyTest.php new file mode 100644 index 0000000..078fcbe --- /dev/null +++ b/tests/Server/Socket/TlsProxyTest.php @@ -0,0 +1,91 @@ +createMock(SocketProxyInterface::class); + $proxy = new TlsProxy($inner, '/tmp/cert.pem', '/tmp/key.pem'); + + $this->assertInstanceOf(SocketProxyInterface::class, $proxy); + } + + public function testCreateSocketDelegatesToInner(): void + { + $expectedSocket = $this->createMock(Socket::class); + + $inner = $this->createMock(SocketProxyInterface::class); + $inner->expects($this->once()) + ->method('createSocket') + ->with(8080, ProtocolType::TCP) + ->willReturn($expectedSocket); + + $proxy = new TlsProxy($inner, '/tmp/cert.pem', '/tmp/key.pem'); + + $result = $proxy->createSocket(8080, ProtocolType::TCP); + + $this->assertSame($expectedSocket, $result); + } + + public function testAcceptThrowsOnTlsHandshakeFailure(): void + { + $server = @\socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + $this->assertNotFalse($server); + + \socket_bind($server, '127.0.0.1', 0); + \socket_listen($server); + + \socket_getsockname($server, $address, $port); + + /** @var string $address */ + /** @var int $port */ + $client = @\socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + $this->assertNotFalse($client); + \socket_connect($client, $address, $port); + + $clientSocket = @\socket_accept($server); + $this->assertNotFalse($clientSocket); + + $connection = new Connection($clientSocket); + + $inner = $this->createMock(SocketProxyInterface::class); + $inner->expects($this->once()) + ->method('accept') + ->willReturn($connection); + + $mockSocket = $this->createMock(Socket::class); + + $proxy = new TlsProxy($inner, '/tmp/nonexistent-cert.pem', '/tmp/nonexistent-key.pem'); + + $this->expectException(SocketCreationException::class); + $this->expectExceptionMessage('TLS handshake failed'); + + $proxy->accept($mockSocket); + + \socket_close($client); + \socket_close($server); + } + + public function testIsSupported(): void + { + $inner = $this->createMock(SocketProxyInterface::class); + $proxy = new TlsProxy($inner, '/tmp/cert.pem', '/tmp/key.pem'); + + $expected = \function_exists('stream_socket_enable_crypto') + && \function_exists('socket_export_stream'); + + $this->assertSame($expected, $proxy->isSupported()); + } +} From 7b3835bf6f2e5589721525e9cf0fb93cb9b5e314 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ha=C5=82as?= Date: Sat, 2 May 2026 09:26:14 +0200 Subject: [PATCH 2/2] M1I10: Address review - check context options return values, include error details --- src/Server/Socket/TlsProxy.php | 39 +++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/src/Server/Socket/TlsProxy.php b/src/Server/Socket/TlsProxy.php index a99e840..fd8e6e6 100644 --- a/src/Server/Socket/TlsProxy.php +++ b/src/Server/Socket/TlsProxy.php @@ -25,6 +25,9 @@ public function createSocket(int $port, ProtocolType $protocol): Socket return $this->inner->createSocket($port, $protocol); } + /** + * @throws SocketCreationException + */ public function accept(Socket $socket): Connection { $connection = $this->inner->accept($socket); @@ -36,23 +39,49 @@ public function accept(Socket $socket): Connection $stream = @\socket_export_stream($socketResource); if ($stream === false) { - throw new SocketCreationException('Failed to export socket to stream'); + $error = \error_get_last(); + + throw new SocketCreationException( + \is_array($error) ? $error['message'] : 'Failed to export socket to stream', + ); } \stream_set_timeout($stream, 30); - \stream_context_set_option($stream, 'ssl', 'local_cert', $this->certFile); - \stream_context_set_option($stream, 'ssl', 'local_pk', $this->keyFile); + if (!\stream_context_set_option($stream, 'ssl', 'local_cert', $this->certFile)) { + \fclose($stream); + + throw new SocketCreationException('Failed to set SSL context option: local_cert'); + } + + if (!\stream_context_set_option($stream, 'ssl', 'local_pk', $this->keyFile)) { + \fclose($stream); + + throw new SocketCreationException('Failed to set SSL context option: local_pk'); + } foreach ($this->sslContextOptions as $option => $value) { - \stream_context_set_option($stream, 'ssl', $option, $value); + if (!\stream_context_set_option($stream, 'ssl', $option, $value)) { + \fclose($stream); + + throw new SocketCreationException( + \sprintf('Failed to set SSL context option: %s', $option), + ); + } } $result = @\stream_socket_enable_crypto($stream, true, \STREAM_CRYPTO_METHOD_TLS_SERVER); if ($result === false) { + $error = \error_get_last(); \fclose($stream); - throw new SocketCreationException('TLS handshake failed'); + + throw new SocketCreationException( + \sprintf( + 'TLS handshake failed: %s', + \is_array($error) ? $error['message'] : 'unknown error', + ), + ); } return $connection;