Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tcp): add support for TLS/SSL in TCP #451

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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#L14)
- [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];
}
93 changes: 67 additions & 26 deletions examples/tcp/basic-http-server.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
Loading