Skip to content

Commit 18af163

Browse files
authored
feat(tls): introduce TCPConnector for poolable TLS connections (#612)
1 parent 1bdb8ba commit 18af163

File tree

7 files changed

+192
-2
lines changed

7 files changed

+192
-2
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## 5.1.0
4+
5+
### features
6+
7+
* feat(tls): introduce `TLS\TCPConnector` for poolable TLS connections
8+
* feat(tls): `TLS\StreamInterface` now extends `TCP\StreamInterface`, enabling TLS streams to be used with `TCP\SocketPoolInterface`
9+
310
## 5.0.0
411

512
### breaking changes

docs/content/networking/tls.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ Use `Acceptor` to perform TLS handshakes on incoming streams:
3232

3333
@example('networking/tls-lazy-acceptor.php')
3434

35+
### TLS Connection Pooling
36+
37+
`TLS\TCPConnector` implements `TCP\ConnectorInterface`, which means it can be used with `TCP\SocketPool` to enable connection pooling for TLS connections. This avoids repeated TLS handshakes when making multiple requests to the same host:
38+
39+
@example('networking/tls-pool.php')
40+
3541
## Examples
3642

3743
### HTTPS Client with ALPN
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
require_once __DIR__ . '/../../../vendor/autoload.php';
6+
7+
use Psl\TCP;
8+
use Psl\TLS;
9+
10+
// Create a TLS-aware connector that implements TCP\ConnectorInterface
11+
$connector = new TLS\TCPConnector(
12+
new TCP\Connector(),
13+
new TLS\Connector(TLS\ClientConfig::default()->withPeerVerification(true)),
14+
);
15+
16+
// Use it with a standard TCP socket pool for connection reuse
17+
$pool = new TCP\SocketPool($connector);
18+
19+
// First request — establishes a new TLS connection
20+
$stream = $pool->checkout('example.com', 443);
21+
$stream->writeAll("GET / HTTP/1.1\r\nHost: example.com\r\nConnection: keep-alive\r\n\r\n");
22+
$response = $stream->read();
23+
$pool->checkin($stream);
24+
25+
// Second request — reuses the existing TLS connection (no new handshake)
26+
$stream = $pool->checkout('example.com', 443);
27+
$stream->writeAll("GET /about HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n");
28+
$response = $stream->read();
29+
$pool->clear($stream);
30+
31+
$pool->close();

src/Psl/Internal/Loader.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,6 +1155,7 @@ final class Loader
11551155
'Psl\\TLS\\LazyAcceptor' => 'Psl/TLS/LazyAcceptor.php',
11561156
'Psl\\TLS\\ClientHello' => 'Psl/TLS/ClientHello.php',
11571157
'Psl\\TLS\\Version' => 'Psl/TLS/Version.php',
1158+
'Psl\\TLS\\TCPConnector' => 'Psl/TLS/TCPConnector.php',
11581159
'Psl\\TLS\\StreamInterface' => 'Psl/TLS/StreamInterface.php',
11591160
'Psl\\TLS\\Internal\\Stream' => 'Psl/TLS/Internal/Stream.php',
11601161
'Psl\\TLS\\Internal\\ClientHelloParser' => 'Psl/TLS/Internal/ClientHelloParser.php',

src/Psl/TLS/StreamInterface.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44

55
namespace Psl\TLS;
66

7-
use Psl\Network;
7+
use Psl\TCP;
88

99
/**
1010
* A TLS-encrypted network stream with access to TLS connection state.
1111
*/
12-
interface StreamInterface extends Network\StreamInterface
12+
interface StreamInterface extends TCP\StreamInterface
1313
{
1414
/**
1515
* Returns the TLS connection state for this connection.

src/Psl/TLS/TCPConnector.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Psl\TLS;
6+
7+
use Psl\DateTime\Duration;
8+
use Psl\Network;
9+
use Psl\TCP;
10+
11+
/**
12+
* A TCP connector that upgrades connections to TLS after establishing them.
13+
*
14+
* Composes a {@see TCP\ConnectorInterface} for the underlying TCP connection
15+
* and a {@see Connector} for the TLS handshake, allowing each layer to be
16+
* configured independently.
17+
*
18+
* The resulting streams can be used with {@see TCP\SocketPoolInterface} to
19+
* enable connection pooling for TLS connections (e.g. DNS-over-TLS).
20+
*/
21+
final readonly class TCPConnector implements TCP\ConnectorInterface
22+
{
23+
public function __construct(
24+
private TCP\ConnectorInterface $tcpConnector,
25+
private Connector $tlsConnector,
26+
) {}
27+
28+
/**
29+
* Connect to the given host over TCP and perform a TLS handshake.
30+
*
31+
* The $host parameter is used both as the TCP connection target and as the
32+
* TLS Server Name Indication (SNI) value, unless overridden by the
33+
* {@see ClientConfig::$peerName} of the TLS connector.
34+
*
35+
* @param non-empty-string $host
36+
* @param int<0, 65535> $port
37+
*
38+
* @throws Network\Exception\RuntimeException If the TCP connection fails.
39+
* @throws Network\Exception\TimeoutException If the connection times out.
40+
* @throws Exception\HandshakeFailedException If the TLS handshake fails.
41+
*/
42+
public function connect(string $host, int $port, null|Duration $timeout = null): TCP\StreamInterface
43+
{
44+
$stream = $this->tcpConnector->connect($host, $port, $timeout);
45+
46+
return $this->tlsConnector->connect($stream, $host);
47+
}
48+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Psl\Tests\Unit\TLS;
6+
7+
use PHPUnit\Framework\TestCase;
8+
use Psl\Async;
9+
use Psl\TCP;
10+
use Psl\TLS;
11+
12+
final class TCPConnectorTest extends TestCase
13+
{
14+
private const string CERT_FILE = __DIR__ . '/../../fixture/certs/server.crt';
15+
private const string KEY_FILE = __DIR__ . '/../../fixture/certs/server.key';
16+
17+
public function testTCPConnectorImplementsConnectorInterface(): void
18+
{
19+
$connector = new TLS\TCPConnector(new TCP\Connector(), TLS\Connector::default());
20+
21+
static::assertInstanceOf(TCP\ConnectorInterface::class, $connector);
22+
}
23+
24+
public function testTCPConnectorConnectsAndUpgradesToTls(): void
25+
{
26+
$cert = TLS\Certificate::create(self::CERT_FILE, self::KEY_FILE);
27+
$server_config = TLS\ServerConfig::create($cert);
28+
$acceptor = new TLS\Acceptor($server_config);
29+
30+
$listener = TCP\listen('127.0.0.1', 0);
31+
$port = $listener->getLocalAddress()->port;
32+
33+
Async\concurrently([
34+
'server' => static function () use ($listener, $acceptor): void {
35+
$connection = $listener->accept();
36+
$tls = $acceptor->accept($connection);
37+
$data = $tls->read();
38+
static::assertSame('hello from tcp-connector', $data);
39+
$tls->writeAll('hello back');
40+
$tls->close();
41+
$listener->close();
42+
},
43+
'client' => static function () use ($port): void {
44+
$config = TLS\ClientConfig::default()->withPeerVerification(false)->withAllowSelfSigned(true);
45+
46+
$connector = new TLS\TCPConnector(new TCP\Connector(), new TLS\Connector($config));
47+
48+
$stream = $connector->connect('127.0.0.1', $port);
49+
50+
static::assertInstanceOf(TCP\StreamInterface::class, $stream);
51+
static::assertInstanceOf(TLS\StreamInterface::class, $stream);
52+
53+
$stream->writeAll('hello from tcp-connector');
54+
$response = $stream->readAll();
55+
static::assertSame('hello back', $response);
56+
$stream->close();
57+
},
58+
]);
59+
}
60+
61+
public function testTCPConnectorWorksWithSocketPool(): void
62+
{
63+
$cert = TLS\Certificate::create(self::CERT_FILE, self::KEY_FILE);
64+
$server_config = TLS\ServerConfig::create($cert);
65+
$acceptor = new TLS\Acceptor($server_config);
66+
67+
$listener = TCP\listen('127.0.0.1', 0);
68+
$port = $listener->getLocalAddress()->port;
69+
70+
Async\concurrently([
71+
'server' => static function () use ($listener, $acceptor): void {
72+
$connection = $listener->accept();
73+
$tls = $acceptor->accept($connection);
74+
$data = $tls->read();
75+
static::assertSame('pooled-request', $data);
76+
$tls->writeAll('pooled-response');
77+
$tls->close();
78+
$listener->close();
79+
},
80+
'client' => static function () use ($port): void {
81+
$config = TLS\ClientConfig::default()->withPeerVerification(false)->withAllowSelfSigned(true);
82+
83+
$pool = new TCP\SocketPool(new TLS\TCPConnector(new TCP\Connector(), new TLS\Connector($config)));
84+
85+
$stream = $pool->checkout('127.0.0.1', $port);
86+
static::assertInstanceOf(TLS\StreamInterface::class, $stream);
87+
88+
$stream->writeAll('pooled-request');
89+
$response = $stream->readAll();
90+
static::assertSame('pooled-response', $response);
91+
92+
$pool->clear($stream);
93+
$pool->close();
94+
},
95+
]);
96+
}
97+
}

0 commit comments

Comments
 (0)