Skip to content

Commit

Permalink
feat(tcp): support TLS/SSL
Browse files Browse the repository at this point in the history
Signed-off-by: azjezz <azjezz@protonmail.com>
  • Loading branch information
azjezz committed May 29, 2024
1 parent fbbff2e commit 91e9400
Show file tree
Hide file tree
Showing 41 changed files with 2,296 additions and 174 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"ext-mbstring": "*",
"ext-sodium": "*",
"ext-intl": "*",
"ext-openssl": "*",
"revolt/event-loop": "^1.0.6"
},
"require-dev": {
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
- [Psl\Str\Byte](./component/str-byte.md)
- [Psl\Str\Grapheme](./component/str-grapheme.md)
- [Psl\TCP](./component/tcp.md)
- [Psl\TCP\TLS](./component/tcp-tls.md)
- [Psl\Trait](./component/trait.md)
- [Psl\Unix](./component/unix.md)
- [Psl\Vec](./component/vec.md)
Expand Down
2 changes: 1 addition & 1 deletion docs/component/network.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

#### `Classes`

- [Address](./../../src/Psl/Network/Address.php#L10)
- [Address](./../../src/Psl/Network/Address.php#L12)
- [SocketOptions](./../../src/Psl/Network/SocketOptions.php#L14)

#### `Enums`
Expand Down
21 changes: 21 additions & 0 deletions docs/component/tcp-tls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!--
This markdown file was generated using `docs/documenter.php`.
Any edits to it will likely be lost.
-->

[*index](./../README.md)

---

### `Psl\TCP\TLS` Component

#### `Classes`

- [Certificate](./../../src/Psl/TCP/TLS/Certificate.php#L16)
- [ClientOptions](./../../src/Psl/TCP/TLS/ClientOptions.php#L34)
- [HashingAlgorithm](./../../src/Psl/TCP/TLS/HashingAlgorithm.php#L15)
- [SecurityLevel](./../../src/Psl/TCP/TLS/SecurityLevel.php#L18)
- [Version](./../../src/Psl/TCP/TLS/Version.php#L10)


6 changes: 3 additions & 3 deletions docs/component/tcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@

#### `Functions`

- [connect](./../../src/Psl/TCP/connect.php#L19)
- [connect](./../../src/Psl/TCP/connect.php#L21)

#### `Classes`

- [ConnectOptions](./../../src/Psl/TCP/ConnectOptions.php#L14)
- [Server](./../../src/Psl/TCP/Server.php#L11)
- [ClientOptions](./../../src/Psl/TCP/ClientOptions.php#L14)
- [Server](./../../src/Psl/TCP/Server.php#L12)
- [ServerOptions](./../../src/Psl/TCP/ServerOptions.php#L15)


1 change: 1 addition & 0 deletions docs/documenter.php
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ function get_all_components(): array
'Psl\\Str\\Byte',
'Psl\\Str\\Grapheme',
'Psl\\TCP',
'Psl\\TCP\\TLS',
'Psl\\Trait',
'Psl\\Unix',
'Psl\\Locale',
Expand Down
43 changes: 31 additions & 12 deletions examples/tcp/basic-http-client.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,43 @@

use Psl\Async;
use Psl\IO;
use Psl\Str;
use Psl\TCP;

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

function fetch(string $host, string $path): string
Async\main(static function(): void {
[$headers, $content] = fetch('https://php-standard-library.github.io');

$output = IO\error_handle() ?? IO\output_handle();

$output->writeAll($headers);
$output->writeAll("\n");
$output->writeAll($content);
});

function fetch(string $url): array
{
$client = TCP\connect($host, 80);
$client->writeAll("GET {$path} HTTP/1.1\r\nHost: $host\r\nConnection: close\r\n\r\n");
$response = $client->readAll();
$client->close();
$parsed_url = parse_url($url);
$host = $parsed_url['host'];
$port = $parsed_url['port'] ?? ($parsed_url['scheme'] === 'https' ? 443 : 80);
$path = $parsed_url['path'] ?? '/';

return $response;
}
$options = TCP\ClientOptions::create();
if ($parsed_url['scheme'] === 'https') {
$options = $options->withTlsClientOptions(
TCP\TLS\ClientOptions::default()->withPeerName($host),
);
}

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

IO\write_error_line($response);
$response = $client->readAll();

return 0;
});
$position = Str\search($response, "\r\n\r\n");
$headers = Str\slice($response, 0, $position);
$content = Str\slice($response, $position + 4);

return [$headers, $content];
}
80 changes: 53 additions & 27 deletions examples/tcp/basic-http-server.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,72 @@
namespace Psl\Example\TCP;

use Psl\Async;
use Psl\File;
use Psl\Html;
use Psl\IO;
use Psl\Iter;
use Psl\Network;
use Psl\Str;
use Psl\TCP;

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

const RESPONSE_FORMAT = <<<HTML
<!DOCTYPE html>
<html lang='en'>
<head>
<title>PHP Standard Library - TCP server</title>
</head>
<body>
<h1>Hello, World!</h1>
<pre><code>%s</code></pre>
</body>
</html>
HTML;

$server = TCP\Server::create('localhost', 3030, TCP\ServerOptions::create(idle_connections: 1024));
/**
* Note: This example is purely for demonstration purposes, and should never be used in a production environment.
*/
$server = TCP\Server::create('localhost', 3030, TCP\ServerOptions::default()
->withTlsServerOptions(
TCP\TLS\ServerOptions::default()
->withMinimumVersion(TCP\TLS\Version::Tls12)
->withAllowSelfSignedCertificates()
->withPeerVerification(false)
->withSecurityLevel(TCP\TLS\SecurityLevel::Level2)
->withDefaultCertificate(TCP\TLS\Certificate::create(
certificate_file: __DIR__ . '/fixtures/localhost.crt',
key_file: __DIR__ . '/fixtures/localhost.key',
))
)
);

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

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

Iter\apply($server->incoming(), static function (Network\StreamSocketInterface $connection): void {
Async\run(static function() use($connection): void {
$request = $connection->read();

$connection->writeAll("HTTP/1.1 200 OK\nConnection: close\nContent-Type: text/html; charset=utf-8\n\n");
$connection->writeAll(Str\format(RESPONSE_FORMAT, Html\encode_special_characters($request)));
$connection->close();
})->catch(
static fn(IO\Exception\ExceptionInterface $e) => IO\write_error_line('Error: %s.', $e->getMessage())
)->ignore();
});
foreach ($server->incoming() as $connection) {
Async\Scheduler::defer(
static fn() => handle($connection)
);
}

IO\write_error_line('');
IO\write_error_line('Goodbye 👋');

function handle(Network\SocketInterface $connection): void
{
$peer = $connection->getPeerAddress();

IO\write_error_line('[SRV]: received a connection from peer "%s".', $peer);

try {
do {
$request = $connection->read();

$template = File\read(__DIR__ . '/templates/index.html');
$content = Str\format($template, Html\encode_special_characters($request));
$length = Str\Byte\length($content);

$connection->writeAll("HTTP/1.1 200 OK\nConnection: keep-alive\nContent-Type: text/html; charset=utf-8\nContent-Length: $length\n\n");
$connection->writeAll($content);
} while(!$connection->reachedEndOfDataSource());

IO\write_error_line('[SRV]: connection dropped by peer "%s".', $peer);
} catch (IO\Exception\ExceptionInterface $e) {
if (!$connection->reachedEndOfDataSource()) {
// If we reached end of data source ( EOF ) and gotten an error, that means that connect was most likely dropped
// by peer while we are performing a write operation, ignore it.
//
// otherwise, log the error:
IO\write_error_line('[SRV]: error "%s" at %s:%d"', $e->getMessage(), $e->getFile(), $e->getLine());
}
}
}
23 changes: 23 additions & 0 deletions examples/tcp/fixtures/localhost.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-----BEGIN CERTIFICATE-----
MIIDyTCCArGgAwIBAgITCuhaYvn3KHwsNke3HvooYbAq+TANBgkqhkiG9w0BAQsF
ADBvMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFu
Y2lzY28xEjAQBgNVBAoMCU15Q29tcGFueTETMBEGA1UECwwKTXlEaXZpc2lvbjES
MBAGA1UEAwwJbG9jYWxob3N0MB4XDTI0MDMzMDA4NTg1M1oXDTI1MDMzMDA4NTg1
M1owbzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJh
bmNpc2NvMRIwEAYDVQQKDAlNeUNvbXBhbnkxEzARBgNVBAsMCk15RGl2aXNpb24x
EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
ggEBAKKeShr27nJtWytAfo/6yr4JpJofE24tzKgeisfDS/YUkj/qqSNyLRO7J82c
dKQTcNajblzTv5RCSuM63uWwXvydMWNBe6YNES2m+CldgHOoztZI8K6r4DFoxuAI
8YqqNqexMP+FBvxfOfabqZjwnwnVRobwqW1jUSiYSbkgLhOMVrr+0FE+BXF0Xe/d
eXhy0sswhxZfQhZE09cS+D2N7XiP/iEnh50Ga6mtXpAKFs2OLvQwF1Mg+fGDufrz
TZhb4TrnmAPF4W3bSZqZWz2ERmb6u1Re8oUFGl6OgRkgYSaEaTMA65rLxeJwpebn
ZK4LjxxG9SXmt2PLxvZMbQs16fkCAwEAAaNeMFwwOwYDVR0RBDQwMoIJbG9jYWxo
b3N0gg0qLmV4YW1wbGUuY29thwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMB0GA1Ud
DgQWBBTQ3g3NGILNZckYdeFp/Ek6IGm1iDANBgkqhkiG9w0BAQsFAAOCAQEAkeLK
+pCBiC0P0y4vXbpeQ29JOkDda5jlzkXYgGqxPjIVVHy69zZWQEzmTQa7ItG0RIj+
/YJkF3eZGgeGs/dLn8oWDiO3WiAGkHFWuGHXihC6a4XH5/dWwCLyZq675HRv0F2J
I/glEq9MGJRvqhLqS8r/8HH03QP87UNMTLVmWuZZ6ugxtQIjqt8v1MzDqB/obpPV
3wkwzzZnqwsxDbr4jLKL/SaPZ9NJyTe5C8gxwPAUawBwmlErem2SVlfjoRSyx/zs
uEqQLW2kpIEEcpMY6g6gR/RJr18IPnBmf1R5DFg/S8/6sbPIRXgcY1AULQ3aiPSd
75hLwcPxLVtyGy6t7A==
-----END CERTIFICATE-----
28 changes: 28 additions & 0 deletions examples/tcp/fixtures/localhost.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCinkoa9u5ybVsr
QH6P+sq+CaSaHxNuLcyoHorHw0v2FJI/6qkjci0TuyfNnHSkE3DWo25c07+UQkrj
Ot7lsF78nTFjQXumDREtpvgpXYBzqM7WSPCuq+AxaMbgCPGKqjansTD/hQb8Xzn2
m6mY8J8J1UaG8KltY1EomEm5IC4TjFa6/tBRPgVxdF3v3Xl4ctLLMIcWX0IWRNPX
Evg9je14j/4hJ4edBmuprV6QChbNji70MBdTIPnxg7n6802YW+E655gDxeFt20ma
mVs9hEZm+rtUXvKFBRpejoEZIGEmhGkzAOuay8XicKXm52SuC48cRvUl5rdjy8b2
TG0LNen5AgMBAAECgf9PPlNeUHZhzGhg60zBXLTvZkOP1xTg2/Ce/EMklUau49dg
zjkdzMWql8kNqPAuBEs4TOu60HTLCoLzt/xmcUvYTcGDXKWkhTmZxYOopKeztM8W
HPUsKRVW/nfrNHB/4fJARVhbK7f7w2u7gJ9kp9zYLdXwa9YkOAGUhqFmVQge/b1o
9INIjW1RpcpFaKoFQIpqJzrWYczAXy9adGcHA04Di4WISyyKAib1Z1PxFhhC009J
9JhPdLEKprWJgVTfE/tlMDcOGY4niZYoKgPGCLsqvim6uB03P273lag/1CG+kZC3
oa3TLiSygCFZbdeG41zxBxvEk8mA39/snc21Nr0CgYEAze/ZBTyj8wM4ZGFXgEvl
jmBAHNFi1LD04/D7TcpDjY8YXiC2ULzw8mEOPVabR/Wov7cBCQNfNo35AI/MH6L8
ucu6gpI8ZAdZVtBN4T+wVEHdXsdsGLJq2wEgTsg580LYsXO7NN/gnmh0QL5d48cc
jjpOxmP+JIR0h9jbyARl0H0CgYEAyiaUQJZDc6gNlCTx13WSyPMBj71RTtpxagzL
ueC9H7qTxMfgHhSNIRxjB+Pu/VQ2WPG847KqrE7UmTl2dH8MWDn8HrEE5xM1A1I+
z8tzfMMuIJVmFwGSPuyHFGNl51Oct6xmP0jNdrve/lknH4R5O7ecwu2lxHhnZoST
k25olC0CgYAM4ihtj3GiTl1EymIzAIyH77WTF/Za4AcyC21tXG4FeSJJITrGqktY
noHJjJWCVvgLpmNGMRPP0en2Awj+IbA132z3pjZo+5y3NajpopZhbw1uVIOKt/6/
XL6srxIRCemMkHTxxd/DiT1cn4w4J8i9jSBIgRDxL+gqZ4K4bK4B8QKBgQCWto6f
XKhraSa+hZDdH2ZRdYN7hB1Dme8mruWQ7rJyHmufMZmxM4dI4V4f+tsqegeO5qP6
azF+B8PPfR0Im9Q7TvfedgH+ub4zfLUhvUCcCvSwDFKx4lUDntrS44yNHDRiaCFP
G1s8I7OMlDFr+Rtd33X7iqylP1NwBnX0XEOR/QKBgQDF/dMnvPO1sYxCczRNyf+I
HX4hdaxBbOOcuYwaRNBrWtAFf2QD5mbR+rr+s3Maka4EcBwbIdNhc8R4CahgIfRY
j5nvrT03RUA3PqgJ/fLhQ5A55A7Z3byLXvJ4/kN41yNSVgiyDHlRtrIa9+k0AtMi
JGklocjlCYwXv/YH2ltHzA==
-----END PRIVATE KEY-----
26 changes: 26 additions & 0 deletions examples/tcp/fixtures/openssl.cnf
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[ req ]
default_bits = 2048
distinguished_name = req_distinguished_name
req_extensions = req_ext
x509_extensions = v3_req
prompt = no

[ req_distinguished_name ]
C = US
ST = CA
L = San Francisco
O = MyCompany
OU = MyDivision
CN = localhost

[ req_ext ]
subjectAltName = @alt_names

[ v3_req ]
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = localhost
DNS.2 = *.example.com
IP.1 = 127.0.0.1
IP.2 = ::1
11 changes: 11 additions & 0 deletions examples/tcp/templates/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang='en'>
<head>
<title>PHP Standard Library - TCP server</title>
</head>
<body>
<h1>Hello, World!</h1>
<pre><code>%s</code></pre>
</body>
</html>

8 changes: 1 addition & 7 deletions src/Psl/IO/Internal/ResourceHandle.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
use Revolt\EventLoop\Suspension;

use function error_get_last;
use function fclose;
use function feof;
use function fread;
use function fseek;
Expand Down Expand Up @@ -360,13 +359,8 @@ public function close(): void
if ($this->close && is_resource($this->stream)) {
$stream = $this->stream;
$this->stream = null;
$result = @fclose($stream);
if ($result === false) {
/** @var array{message: string} $error */
$error = error_get_last();

throw new Exception\RuntimeException($error['message'] ?? 'unknown error.');
}
namespace\close_resource($stream);
} else {
// Stream could be set to a non-null closed-resource,
// if manually closed using `fclose($handle->getStream)`.
Expand Down
32 changes: 32 additions & 0 deletions src/Psl/IO/Internal/close_resource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace Psl\IO\Internal;

use Psl\IO\Exception;

use function error_get_last;
use function fclose;

/**
* ` * Closes a given resource stream.
*
* @param resource $stream
*
* @internal
*
* @codeCoverageIgnore
*
* @throws Exception\RuntimeException If closing the stream fails.
*/
function close_resource(mixed $stream): void
{
$result = @fclose($stream);
if ($result === false) {
/** @var array{message: string} $error */
$error = error_get_last();

throw new Exception\RuntimeException($error['message'] ?? 'unknown error.');
}
}
Loading

0 comments on commit 91e9400

Please sign in to comment.