Skip to content

Commit da09886

Browse files
committed
feat(tcp): support TLS/SSL
Signed-off-by: azjezz <[email protected]>
1 parent d42f3fa commit da09886

39 files changed

+2211
-155
lines changed

Diff for: composer.json

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"ext-mbstring": "*",
1717
"ext-sodium": "*",
1818
"ext-intl": "*",
19+
"ext-openssl": "*",
1920
"revolt/event-loop": "^1.0.1"
2021
},
2122
"require-dev": {

Diff for: docs/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
- [Psl\Str\Byte](./component/str-byte.md)
5353
- [Psl\Str\Grapheme](./component/str-grapheme.md)
5454
- [Psl\TCP](./component/tcp.md)
55+
- [Psl\TCP\TLS](./component/tcp-tls.md)
5556
- [Psl\Trait](./component/trait.md)
5657
- [Psl\Type](./component/type.md)
5758
- [Psl\Unix](./component/unix.md)

Diff for: docs/component/tcp-tls.md

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<!--
2+
This markdown file was generated using `docs/documenter.php`.
3+
4+
Any edits to it will likely be lost.
5+
-->
6+
7+
[*index](./../README.md)
8+
9+
---
10+
11+
### `Psl\TCP\TLS` Component
12+
13+
#### `Classes`
14+
15+
- [Certificate](./../../src/Psl/TCP/TLS/Certificate.php#L16)
16+
- [ConnectOptions](./../../src/Psl/TCP/TLS/ConnectOptions.php#L34)
17+
- [HashingAlgorithm](./../../src/Psl/TCP/TLS/HashingAlgorithm.php#L15)
18+
- [SecurityLevel](./../../src/Psl/TCP/TLS/SecurityLevel.php#L18)
19+
- [Version](./../../src/Psl/TCP/TLS/Version.php#L10)
20+
21+

Diff for: docs/component/tcp.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
#### `Functions`
1414

15-
- [connect](./../../src/Psl/TCP/connect.php#L18)
15+
- [connect](./../../src/Psl/TCP/connect.php#L37)
1616

1717
#### `Classes`
1818

Diff for: docs/documenter.php

+1
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ function get_all_components(): array
222222
'Psl\\Str\\Byte',
223223
'Psl\\Str\\Grapheme',
224224
'Psl\\TCP',
225+
'Psl\\TCP\\TLS',
225226
'Psl\\Trait',
226227
'Psl\\Type',
227228
'Psl\\Unix',

Diff for: examples/tcp/basic-http-client.php

+31-12
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,43 @@
66

77
use Psl\Async;
88
use Psl\IO;
9+
use Psl\Str;
910
use Psl\TCP;
1011

1112
require __DIR__ . '/../../vendor/autoload.php';
1213

13-
function fetch(string $host, string $path): string
14+
Async\main(static function(): void {
15+
[$headers, $content] = fetch('https://php-standard-library.github.io');
16+
17+
$output = IO\error_handle() ?? IO\output_handle();
18+
19+
$output->writeAll($headers);
20+
$output->writeAll("\n");
21+
$output->writeAll($content);
22+
});
23+
24+
function fetch(string $url): array
1425
{
15-
$client = TCP\connect($host, 80);
16-
$client->writeAll("GET {$path} HTTP/1.1\r\nHost: $host\r\nConnection: close\r\n\r\n");
17-
$response = $client->readAll();
18-
$client->close();
26+
$parsed_url = parse_url($url);
27+
$host = $parsed_url['host'];
28+
$port = $parsed_url['port'] ?? ($parsed_url['scheme'] === 'https' ? 443 : 80);
29+
$path = $parsed_url['path'] ?? '/';
1930

20-
return $response;
21-
}
31+
$options = TCP\ClientOptions::create();
32+
if ($parsed_url['scheme'] === 'https') {
33+
$options = $options->withTlsClientOptions(
34+
TCP\TLS\ClientOptions::default()->withPeerName($host),
35+
);
36+
}
2237

23-
Async\main(static function (): int {
24-
$response = fetch('example.com', '/');
38+
$client = TCP\connect($host, $port, $options);
39+
$client->writeAll("GET $path HTTP/1.1\r\nHost: $host\r\nConnection: close\r\n\r\n");
2540

26-
IO\write_error_line($response);
41+
$response = $client->readAll();
2742

28-
return 0;
29-
});
43+
$position = Str\search($response, "\r\n\r\n");
44+
$headers = Str\slice($response, 0, $position);
45+
$content = Str\slice($response, $position + 4);
46+
47+
return [$headers, $content];
48+
}

Diff for: examples/tcp/basic-http-server.php

+47-27
Original file line numberDiff line numberDiff line change
@@ -5,46 +5,66 @@
55
namespace Psl\Example\TCP;
66

77
use Psl\Async;
8+
use Psl\File;
89
use Psl\Html;
910
use Psl\IO;
10-
use Psl\Iter;
1111
use Psl\Network;
1212
use Psl\Str;
1313
use Psl\TCP;
1414

1515
require __DIR__ . '/../../vendor/autoload.php';
1616

17-
const RESPONSE_FORMAT = <<<HTML
18-
<!DOCTYPE html>
19-
<html lang='en'>
20-
<head>
21-
<title>PHP Standard Library - TCP server</title>
22-
</head>
23-
<body>
24-
<h1>Hello, World!</h1>
25-
<pre><code>%s</code></pre>
26-
</body>
27-
</html>
28-
HTML;
29-
30-
$server = TCP\Server::create('localhost', 3030, TCP\ServerOptions::create(idle_connections: 1024));
17+
$server = TCP\Server::create('localhost', 3030, TCP\ServerOptions::default()
18+
->withTlsServerOptions(
19+
TCP\TLS\ServerOptions::default()
20+
->withMinimumVersion(TCP\TLS\Version::Tls12)
21+
->withAllowSelfSignedCertificates()
22+
->withPeerVerification(false)
23+
->withSecurityLevel(TCP\TLS\SecurityLevel::Level2)
24+
->withDefaultCertificate(TCP\TLS\Certificate::create(
25+
certificate_file: __DIR__ . '/fixtures/localhost.crt',
26+
key_file: __DIR__ . '/fixtures/localhost.key',
27+
))
28+
)
29+
);
3130

3231
Async\Scheduler::onSignal(SIGINT, $server->close(...));
3332

34-
IO\write_error_line('Server is listening on http://localhost:3030');
33+
IO\write_error_line('Server is listening on https://localhost:3030');
3534
IO\write_error_line('Click Ctrl+C to stop the server.');
3635

37-
Iter\apply($server->incoming(), static function (Network\StreamSocketInterface $connection): void {
38-
Async\run(static function() use($connection): void {
39-
$request = $connection->read();
40-
41-
$connection->writeAll("HTTP/1.1 200 OK\nConnection: close\nContent-Type: text/html; charset=utf-8\n\n");
42-
$connection->writeAll(Str\format(RESPONSE_FORMAT, Html\encode_special_characters($request)));
43-
$connection->close();
44-
})->catch(
45-
static fn(IO\Exception\ExceptionInterface $e) => IO\write_error_line('Error: %s.', $e->getMessage())
46-
)->ignore();
47-
});
36+
foreach ($server->incoming() as $connection) {
37+
Async\Scheduler::defer(
38+
static fn() => handle($connection)
39+
);
40+
}
4841

4942
IO\write_error_line('');
5043
IO\write_error_line('Goodbye 👋');
44+
45+
function handle(Network\SocketInterface $connection): void
46+
{
47+
$peer = $connection->getPeerAddress();
48+
49+
IO\write_error_line('[SRV]: received a connection from peer "%s".', $peer);
50+
51+
try {
52+
do {
53+
$request = $connection->read();
54+
55+
$template = File\read(__DIR__ . '/templates/index.html');
56+
$content = Str\format($template, Html\encode_special_characters($request));
57+
$length = Str\Byte\length($content);
58+
59+
$connection->writeAll("HTTP/1.1 200 OK\nConnection: keep-alive\nContent-Type: text/html; charset=utf-8\nContent-Length: $length\n\n");
60+
$connection->writeAll($content);
61+
} while(!$connection->reachedEndOfDataSource());
62+
63+
IO\write_error_line('[SRV]: connection dropped by peer "%s".', $peer);
64+
} catch (IO\Exception\ExceptionInterface $e) {
65+
IO\write_error_line('[SRV]: error "%s" at %s:%d"', $e->getMessage(), $e->getFile(), $e->getLine());
66+
if ($connection->reachedEndOfDataSource()) {
67+
IO\write_error_line('[SRV]: most likely that the connection was dropped by peer "%s" while we are writing, ignore it.', $peer);
68+
}
69+
}
70+
}

Diff for: examples/tcp/fixtures/localhost.crt

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDyTCCArGgAwIBAgITCuhaYvn3KHwsNke3HvooYbAq+TANBgkqhkiG9w0BAQsF
3+
ADBvMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFu
4+
Y2lzY28xEjAQBgNVBAoMCU15Q29tcGFueTETMBEGA1UECwwKTXlEaXZpc2lvbjES
5+
MBAGA1UEAwwJbG9jYWxob3N0MB4XDTI0MDMzMDA4NTg1M1oXDTI1MDMzMDA4NTg1
6+
M1owbzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJh
7+
bmNpc2NvMRIwEAYDVQQKDAlNeUNvbXBhbnkxEzARBgNVBAsMCk15RGl2aXNpb24x
8+
EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
9+
ggEBAKKeShr27nJtWytAfo/6yr4JpJofE24tzKgeisfDS/YUkj/qqSNyLRO7J82c
10+
dKQTcNajblzTv5RCSuM63uWwXvydMWNBe6YNES2m+CldgHOoztZI8K6r4DFoxuAI
11+
8YqqNqexMP+FBvxfOfabqZjwnwnVRobwqW1jUSiYSbkgLhOMVrr+0FE+BXF0Xe/d
12+
eXhy0sswhxZfQhZE09cS+D2N7XiP/iEnh50Ga6mtXpAKFs2OLvQwF1Mg+fGDufrz
13+
TZhb4TrnmAPF4W3bSZqZWz2ERmb6u1Re8oUFGl6OgRkgYSaEaTMA65rLxeJwpebn
14+
ZK4LjxxG9SXmt2PLxvZMbQs16fkCAwEAAaNeMFwwOwYDVR0RBDQwMoIJbG9jYWxo
15+
b3N0gg0qLmV4YW1wbGUuY29thwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMB0GA1Ud
16+
DgQWBBTQ3g3NGILNZckYdeFp/Ek6IGm1iDANBgkqhkiG9w0BAQsFAAOCAQEAkeLK
17+
+pCBiC0P0y4vXbpeQ29JOkDda5jlzkXYgGqxPjIVVHy69zZWQEzmTQa7ItG0RIj+
18+
/YJkF3eZGgeGs/dLn8oWDiO3WiAGkHFWuGHXihC6a4XH5/dWwCLyZq675HRv0F2J
19+
I/glEq9MGJRvqhLqS8r/8HH03QP87UNMTLVmWuZZ6ugxtQIjqt8v1MzDqB/obpPV
20+
3wkwzzZnqwsxDbr4jLKL/SaPZ9NJyTe5C8gxwPAUawBwmlErem2SVlfjoRSyx/zs
21+
uEqQLW2kpIEEcpMY6g6gR/RJr18IPnBmf1R5DFg/S8/6sbPIRXgcY1AULQ3aiPSd
22+
75hLwcPxLVtyGy6t7A==
23+
-----END CERTIFICATE-----

Diff for: examples/tcp/fixtures/localhost.key

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCinkoa9u5ybVsr
3+
QH6P+sq+CaSaHxNuLcyoHorHw0v2FJI/6qkjci0TuyfNnHSkE3DWo25c07+UQkrj
4+
Ot7lsF78nTFjQXumDREtpvgpXYBzqM7WSPCuq+AxaMbgCPGKqjansTD/hQb8Xzn2
5+
m6mY8J8J1UaG8KltY1EomEm5IC4TjFa6/tBRPgVxdF3v3Xl4ctLLMIcWX0IWRNPX
6+
Evg9je14j/4hJ4edBmuprV6QChbNji70MBdTIPnxg7n6802YW+E655gDxeFt20ma
7+
mVs9hEZm+rtUXvKFBRpejoEZIGEmhGkzAOuay8XicKXm52SuC48cRvUl5rdjy8b2
8+
TG0LNen5AgMBAAECgf9PPlNeUHZhzGhg60zBXLTvZkOP1xTg2/Ce/EMklUau49dg
9+
zjkdzMWql8kNqPAuBEs4TOu60HTLCoLzt/xmcUvYTcGDXKWkhTmZxYOopKeztM8W
10+
HPUsKRVW/nfrNHB/4fJARVhbK7f7w2u7gJ9kp9zYLdXwa9YkOAGUhqFmVQge/b1o
11+
9INIjW1RpcpFaKoFQIpqJzrWYczAXy9adGcHA04Di4WISyyKAib1Z1PxFhhC009J
12+
9JhPdLEKprWJgVTfE/tlMDcOGY4niZYoKgPGCLsqvim6uB03P273lag/1CG+kZC3
13+
oa3TLiSygCFZbdeG41zxBxvEk8mA39/snc21Nr0CgYEAze/ZBTyj8wM4ZGFXgEvl
14+
jmBAHNFi1LD04/D7TcpDjY8YXiC2ULzw8mEOPVabR/Wov7cBCQNfNo35AI/MH6L8
15+
ucu6gpI8ZAdZVtBN4T+wVEHdXsdsGLJq2wEgTsg580LYsXO7NN/gnmh0QL5d48cc
16+
jjpOxmP+JIR0h9jbyARl0H0CgYEAyiaUQJZDc6gNlCTx13WSyPMBj71RTtpxagzL
17+
ueC9H7qTxMfgHhSNIRxjB+Pu/VQ2WPG847KqrE7UmTl2dH8MWDn8HrEE5xM1A1I+
18+
z8tzfMMuIJVmFwGSPuyHFGNl51Oct6xmP0jNdrve/lknH4R5O7ecwu2lxHhnZoST
19+
k25olC0CgYAM4ihtj3GiTl1EymIzAIyH77WTF/Za4AcyC21tXG4FeSJJITrGqktY
20+
noHJjJWCVvgLpmNGMRPP0en2Awj+IbA132z3pjZo+5y3NajpopZhbw1uVIOKt/6/
21+
XL6srxIRCemMkHTxxd/DiT1cn4w4J8i9jSBIgRDxL+gqZ4K4bK4B8QKBgQCWto6f
22+
XKhraSa+hZDdH2ZRdYN7hB1Dme8mruWQ7rJyHmufMZmxM4dI4V4f+tsqegeO5qP6
23+
azF+B8PPfR0Im9Q7TvfedgH+ub4zfLUhvUCcCvSwDFKx4lUDntrS44yNHDRiaCFP
24+
G1s8I7OMlDFr+Rtd33X7iqylP1NwBnX0XEOR/QKBgQDF/dMnvPO1sYxCczRNyf+I
25+
HX4hdaxBbOOcuYwaRNBrWtAFf2QD5mbR+rr+s3Maka4EcBwbIdNhc8R4CahgIfRY
26+
j5nvrT03RUA3PqgJ/fLhQ5A55A7Z3byLXvJ4/kN41yNSVgiyDHlRtrIa9+k0AtMi
27+
JGklocjlCYwXv/YH2ltHzA==
28+
-----END PRIVATE KEY-----

Diff for: examples/tcp/fixtures/openssl.cnf

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[ req ]
2+
default_bits = 2048
3+
distinguished_name = req_distinguished_name
4+
req_extensions = req_ext
5+
x509_extensions = v3_req
6+
prompt = no
7+
8+
[ req_distinguished_name ]
9+
C = US
10+
ST = CA
11+
L = San Francisco
12+
O = MyCompany
13+
OU = MyDivision
14+
CN = localhost
15+
16+
[ req_ext ]
17+
subjectAltName = @alt_names
18+
19+
[ v3_req ]
20+
subjectAltName = @alt_names
21+
22+
[ alt_names ]
23+
DNS.1 = localhost
24+
DNS.2 = *.example.com
25+
IP.1 = 127.0.0.1
26+
IP.2 = ::1

Diff for: examples/tcp/templates/index.html

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!DOCTYPE html>
2+
<html lang='en'>
3+
<head>
4+
<title>PHP Standard Library - TCP server</title>
5+
</head>
6+
<body>
7+
<h1>Hello, World!</h1>
8+
<pre><code>%s</code></pre>
9+
</body>
10+
</html>
11+

Diff for: foo.php

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Psl\File;
6+
use Psl\Async;
7+
use Psl\IO;
8+
9+
require 'vendor/autoload.php';
10+
11+
Async\main(function (): void {
12+
$file = File\open_read_only(__FILE__);
13+
$bytes = 0;
14+
while (!($x = $file->reachedEndOfDataSource())) {
15+
var_dump($x);
16+
$byte = $file->readFixedSize(1);
17+
$bytes += strlen($byte);
18+
19+
IO\write_line('read %d bytes.', $bytes);
20+
var_dump($file->reachedEndOfDataSource());
21+
}
22+
23+
$file->close();
24+
});

Diff for: src/Psl/IO/Internal/ResourceHandle.php

+1-7
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
use Revolt\EventLoop\Suspension;
1414

1515
use function error_get_last;
16-
use function fclose;
1716
use function feof;
1817
use function fread;
1918
use function fseek;
@@ -359,13 +358,8 @@ public function close(): void
359358
if ($this->close && is_resource($this->stream)) {
360359
$stream = $this->stream;
361360
$this->stream = null;
362-
$result = @fclose($stream);
363-
if ($result === false) {
364-
/** @var array{message: string} $error */
365-
$error = error_get_last();
366361

367-
throw new Exception\RuntimeException($error['message'] ?? 'unknown error.');
368-
}
362+
namespace\close_resource($stream);
369363
} else {
370364
// Stream could be set to a non-null closed-resource,
371365
// if manually closed using `fclose($handle->getStream)`.

Diff for: src/Psl/IO/Internal/close_resource.php

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Psl\IO\Internal;
6+
7+
use Psl\IO\Exception;
8+
9+
use function error_get_last;
10+
use function fclose;
11+
12+
/**
13+
* @param resource $stream
14+
*
15+
* @internal
16+
*
17+
* @codeCoverageIgnore
18+
*/
19+
function close_resource(mixed $stream): void
20+
{
21+
$result = @fclose($stream);
22+
if ($result === false) {
23+
/** @var array{message: string} $error */
24+
$error = error_get_last();
25+
26+
throw new Exception\RuntimeException($error['message'] ?? 'unknown error.');
27+
}
28+
}

0 commit comments

Comments
 (0)