Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(tcp): support TLS/SSL
Browse files Browse the repository at this point in the history
Signed-off-by: azjezz <[email protected]>
azjezz committed May 29, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent fbbff2e commit 13120ba
Showing 40 changed files with 2,124 additions and 178 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@
"ext-mbstring": "*",
"ext-sodium": "*",
"ext-intl": "*",
"ext-openssl": "*",
"revolt/event-loop": "^1.0.6"
},
"require-dev": {
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 1 addition & 1 deletion docs/component/network.md
Original file line number Diff line number Diff line change
@@ -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`
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
@@ -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
@@ -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',
43 changes: 31 additions & 12 deletions examples/tcp/basic-http-client.php
Original file line number Diff line number Diff line change
@@ -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];
}
93 changes: 67 additions & 26 deletions examples/tcp/basic-http-server.php
Original file line number Diff line number Diff line change
@@ -5,46 +5,87 @@
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.
*
* Generate a self-signed certificate using the following command:
*
* $ cd examples/tcp/fixtures
* $ openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout privatekey.pem -out certificate.pem -config openssl.cnf
*/
$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/certificate.pem',
key_file: __DIR__ . '/fixtures/privatekey.pem',
))
)
);

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();
while (true) {
try {
$connection = $server->nextConnection();

$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();
});
Async\Scheduler::defer(static fn() => handle($connection));
} catch (TCP\TLS\Exception\NegotiationException $e) {
IO\write_error_line('[SRV]: error "%s" at %s:%d"', $e->getMessage(), $e->getFile(), $e->getLine());

continue;
} catch (Network\Exception\AlreadyStoppedException $e) {
break;
}
}

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

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

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

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());
}
} finally {
$connection->close();
}
}
23 changes: 23 additions & 0 deletions examples/tcp/fixtures/certificate.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-----BEGIN CERTIFICATE-----
MIIDyjCCArKgAwIBAgIUKZH353aWdxUxmEabhyie1GbT1wQwDQYJKoZIhvcNAQEL
BQAwbzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJh
bmNpc2NvMRIwEAYDVQQKDAlNeUNvbXBhbnkxEzARBgNVBAsMCk15RGl2aXNpb24x
EjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yNDA1MjkxMjIzMjlaFw0yNTA1MjkxMjIz
MjlaMG8xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNU2FuIEZy
YW5jaXNjbzESMBAGA1UECgwJTXlDb21wYW55MRMwEQYDVQQLDApNeURpdmlzaW9u
MRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQCmBzlJGgJNiKvVSQskqh30o8LOApZcsCZf1NlijQ4XggtunMgtV4663EWw
nOXewWD3Hssi/ZAaK4Az+0GQ3iMSGJAGBtOzAPCr0ZIUN42bx6NMx+iqQ37XkLw8
A9uPXK7hwM9EG6uWMVtw5OR7TugFGtyFmTLdxU3uaxtkmRi76haTyBOFpJs4xyFj
7WinGlCJ0EjKibW12xcCYWbRoObeJIBvviJizfve0dK+lVsP4sYP7gd3xwHq4xUO
x2lWeUFmAPL9+jDNfrwd985OAAkWO71q8MySvAVPaGGiu6gN5ReyzNAUxaYErJfd
BYQEVW25q/E6ez0r6lGSmLwbGqc3AgMBAAGjXjBcMDsGA1UdEQQ0MDKCCWxvY2Fs
aG9zdIINKi5leGFtcGxlLmNvbYcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATAdBgNV
HQ4EFgQUV7Hcc0Urg/HSV0jwHA8A1r/+yCUwDQYJKoZIhvcNAQELBQADggEBAFVf
ExPw0NL1vLJq1KeXM7dxazd9Ge0K+8OXELbgtPJfxotHLTUGx25uaxc5lKa3v2Aa
Du1iQMExPQCBWoO2pb9OrfePbagyzbkM2yVR/NNI9cXlk6BMMhluMF5onKkTApH+
QzqaU/VWyBCOgLsuzM1kwXpsJyTJ+pZgXVmwuFMefVsdcMT3Gz6Fnmn04aslOU62
Kgnkfx4rDPH4kqC1Zj4RwenJ03gCC9o2jaV8cZsMmu4tC3/hXHdWCJFV4DOCJ6w0
TFMP83vYgSQoLIZIPo9ka5yELaRSSE27LPEckdVYk4q+X/O2/SSZPgFFFcJ5qXqb
Y9KqGkvmSp//SvEDyV0=
-----END CERTIFICATE-----
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
28 changes: 28 additions & 0 deletions examples/tcp/fixtures/privatekey.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCmBzlJGgJNiKvV
SQskqh30o8LOApZcsCZf1NlijQ4XggtunMgtV4663EWwnOXewWD3Hssi/ZAaK4Az
+0GQ3iMSGJAGBtOzAPCr0ZIUN42bx6NMx+iqQ37XkLw8A9uPXK7hwM9EG6uWMVtw
5OR7TugFGtyFmTLdxU3uaxtkmRi76haTyBOFpJs4xyFj7WinGlCJ0EjKibW12xcC
YWbRoObeJIBvviJizfve0dK+lVsP4sYP7gd3xwHq4xUOx2lWeUFmAPL9+jDNfrwd
985OAAkWO71q8MySvAVPaGGiu6gN5ReyzNAUxaYErJfdBYQEVW25q/E6ez0r6lGS
mLwbGqc3AgMBAAECggEADhWLywM9UcV3yjKVkukpfGDN/Drk9Xzt7HA6dq0/lkfu
X1ZGdu44Cer4sHhG2cQuzRfcJJ489LNe/0nfsIHfmL/jq9c1azh3siOnkDZ8OUxQ
sok82AC8yF2bUj4DiKBUp4r7KixsvGN4fdW0+i7h6Njz/xNVaNG9gC2u17RTEFGy
tCMegse4XYTKxziOpGjhxwuIEOlY5sX3TdqTiiiJCz+PkD9WT2fPqZH9+K9msE1v
ETlCP6IKmt39TrZp6HCMmfDTvcX8tNUdNll/7b2NEAfSH4FJcNRVUVc/Xjw7SXeg
0e51qoYTRMqaO8Q1xDFtk9881LiWVoSPb6rTe17rBQKBgQDhy14xsmzfKeyTYcjF
ucs1NHh1xJBUNyQGE2aYq58Pg9CeuSnv9N+WWZdR2xNpQ5wq1F6ZS96B6FU1DT+/
A2YreGUJoLQan26oI+r3TEOASHZcliqV9Qn9R0vA5GRLixG0LIcv3s6askIqHFLY
3BX2goGIlvCGJiWpvd2MPvWsPQKBgQC8PRUl9Onob8ar8ELlVCeRCJ0wgIKQew95
MWKrfLATLaJJMLlU3JPLnN3gnOZhJ2u5/ClArofzbofhfkMMnoZoxInCs/hn4EwT
W1aeLGCQX/Qb89B2HCDz2t4KPRraq+diEj4vR9H8VvUTdcIvXQ4i9UfM9G1+xVpJ
1oPUytXUgwKBgGKgBArNFsT7ePx/T8Ud/GbG/n7iVvCSDUgiHUQ+YoHSX8OUuX64
hRkVFQWKHZZzE7mZfaCUBSLVKrK7kMaMY4pFUky8Ry8ByMHkvnM6epmEDT8v0HYj
zDM3ex1MJYrhud/rOzlrpu7nQgNGz+EtcOJ16sKQu4q9CuJzrlvd/E05AoGAYeIl
gCJWC78sATajoprbJEjlbFY3DqhfSHcMxv3ElYRyUjra9Kzq0cNVgTo1dinIk+Lz
FKZtHYHJeNFuTj6UyCADPtLVBjcVeC9T4FZVNF4hEvP636AK5qNWON7DexhO7qlr
2qwvHledgywF+Rkbg8QmPQaRdY1sQN8imGGNRb8CgYBoc1MSutl/BFppVs8aNAki
s+P7O0jifYvceqc3NOfQ36WLMS+7tzC5rI0KhVG7MEnkyi4hbtbSzPu74rTINsQc
UmmG14Or7Od/BYnLYgVUFK1LA6Xub37PbNqHPLaQM+41oH8n5139L50Hd6xKZ+7n
RonLBPsLONMcmb4U1ST9oQ==
-----END PRIVATE KEY-----
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
@@ -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;
@@ -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)`.
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 13120ba

Please sign in to comment.