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 Mar 30, 2024
1 parent d42f3fa commit 3690275
Show file tree
Hide file tree
Showing 18 changed files with 1,111 additions and 33 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.1"
},
"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 @@ -52,6 +52,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\Type](./component/type.md)
- [Psl\Unix](./component/unix.md)
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)
- [ConnectOptions](./../../src/Psl/TCP/TLS/ConnectOptions.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)


2 changes: 1 addition & 1 deletion docs/component/tcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

#### `Functions`

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

#### `Classes`

Expand Down
1 change: 1 addition & 0 deletions docs/documenter.php
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ function get_all_components(): array
'Psl\\Str\\Byte',
'Psl\\Str\\Grapheme',
'Psl\\TCP',
'Psl\\TCP\\TLS',
'Psl\\Trait',
'Psl\\Type',
'Psl\\Unix',
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\ConnectOptions::create();
if ($parsed_url['scheme'] === 'https') {
$options = $options->withTLSConnectOptions(
TCP\TLS\ConnectOptions::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];
}
24 changes: 24 additions & 0 deletions foo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

use Psl\File;
use Psl\Async;
use Psl\IO;

require 'vendor/autoload.php';

Async\main(function (): void {
$file = File\open_read_only(__FILE__);
$bytes = 0;
while (!($x = $file->reachedEndOfDataSource())) {
var_dump($x);
$byte = $file->readFixedSize(1);
$bytes += strlen($byte);

IO\write_line('read %d bytes.', $bytes);
var_dump($file->reachedEndOfDataSource());
}

$file->close();
});
7 changes: 7 additions & 0 deletions src/Psl/Internal/Loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,7 @@ final class Loader
'Psl\\Range\\LowerBoundRangeInterface' => 'Psl/Range/LowerBoundRangeInterface.php',
'Psl\\Range\\UpperBoundRangeInterface' => 'Psl/Range/UpperBoundRangeInterface.php',
'Psl\\Default\\DefaultInterface' => 'Psl/Default/DefaultInterface.php',
'Psl\\TCP\\TLS\\Exception\\ExceptionInterface' => 'Psl/TCP/TLS/Exception/ExceptionInterface.php',
];

public const TRAITS = [
Expand Down Expand Up @@ -815,6 +816,12 @@ final class Loader
'Psl\\Range\\ToRange' => 'Psl/Range/ToRange.php',
'Psl\\Range\\BetweenRange' => 'Psl/Range/BetweenRange.php',
'Psl\\Range\\FullRange' => 'Psl/Range/FullRange.php',
'Psl\\TCP\\TLS\\Exception\\NegotiationException' => 'Psl/TCP/TLS/Exception/NegotiationException.php',
'Psl\\TCP\\TLS\\Certificate' => 'Psl/TCP/TLS/Certificate.php',
'Psl\\TCP\\TLS\\ConnectOptions' => 'Psl/TCP/TLS/ConnectOptions.php',
'Psl\\TCP\\TLS\\HashingAlgorithm' => 'Psl/TCP/TLS/HashingAlgorithm.php',
'Psl\\TCP\\TLS\\SecurityLevel' => 'Psl/TCP/TLS/SecurityLevel.php',
'Psl\\TCP\\TLS\\Version' => 'Psl/TCP/TLS/Version.php',
];

public const ENUMS = [
Expand Down
17 changes: 15 additions & 2 deletions src/Psl/Network/Internal/socket_connect.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@

use Psl\Internal;
use Psl\Network\Exception;
use Psl\Str;
use Revolt\EventLoop;

use function fclose;
use function is_resource;
use function stream_context_create;
use function stream_socket_client;
use function stream_socket_get_name;

use const STREAM_CLIENT_ASYNC_CONNECT;
use const STREAM_CLIENT_CONNECT;
Expand Down Expand Up @@ -61,8 +63,19 @@ function socket_connect(string $uri, array $context = [], ?float $timeout = null
});

try {
/** @var resource */
return $suspension->suspend();
/** @var resource $socket */
$socket = $suspension->suspend();

if (stream_socket_get_name($socket, true) === false) {
fclose($socket);

throw new Exception\RuntimeException(Str\format(
'Connection to %s refused%s',
$uri,
));
}

return $socket;
} finally {
EventLoop::cancel($write_watcher);
EventLoop::cancel($timeout_watcher);
Expand Down
77 changes: 61 additions & 16 deletions src/Psl/TCP/ConnectOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,18 @@
final class ConnectOptions implements DefaultInterface
{
/**
* Initializes a new instance of {@see ConnectOptions} with the specified settings.
* Constructor is private to enforce immutability. Use static creation methods instead.
*
* @param bool $noDelay Determines whether the TCP_NODELAY option is enabled, controlling
* the use of the Nagle algorithm. When true, TCP_NODELAY is enabled,
* and the Nagle algorithm is disabled.
* @param bool $noDelay Indicates whether to disable Nagle's algorithm. When true, packets are sent immediately.
* @param null|array{non-empty-string, null|int} $bindTo Specifies the IP address and optionally the port to bind to.
* Format: [`IP`, `Port`]. `Port` is optional and can be null.
*
* @pure
*/
public function __construct(
public readonly bool $noDelay,
private function __construct(
public readonly bool $noDelay,
public readonly ?array $bindTo,
public readonly ?TLS\ConnectOptions $TLSConnectOptions,
) {
}

Expand All @@ -41,18 +43,12 @@ public function __construct(
*/
public static function create(bool $noDelay = false): ConnectOptions
{
return new self($noDelay);
return new self($noDelay, null, null);
}

/**
* Creates and returns a default instance of {@see ConnectOptions}.
*
* The default instance has the TCP_NODELAY option disabled, allowing the Nagle algorithm
* to be used. This method is a convenience wrapper around the `create` method, adhering to
* the {@see DefaultInterface} contract.
*
* @return static A default ConnectOptions instance with noDelay set to false.
*
* @pure
*/
public static function default(): static
Expand All @@ -61,14 +57,63 @@ public static function default(): static
}

/**
* Returns a new instance of {@see ConnectOptions} with the noDelay setting modified.
* Returns a new instance with noDelay enabled.
*
* @param bool $enabled Specifies the desired state of the TCP_NODELAY option.
* @return ConnectOptions A new instance with noDelay set to true.
*
* @mutation-free
*/
public function withNoDelay(bool $enabled = true): ConnectOptions
{
return new self($enabled);
return new self($enabled, $this->bindTo, $this->TLSConnectOptions);
}

/**
* Returns a new instance with the specified IP and optionally port to bind to.
*
* @param non-empty-string $ip The IP address to bind the connection to.
* @param int|null $port The port number to bind the connection to, or null to not specify.
*
* @return ConnectOptions A new instance with the updated bindTo option.
*
* @mutation-free
*/
public function withBindTo(string $ip, ?int $port = null): ConnectOptions
{
return new self($this->noDelay, [$ip, $port], $this->TLSConnectOptions);
}

/**
* Returns a new instance without any bindTo configuration.
*
* @return ConnectOptions A new instance with bindTo set to null.
*
* @mutation-free
*/
public function withoutBindTo(): ConnectOptions
{
return new self($this->noDelay, null, $this->TLSConnectOptions);
}

/**
* Returns a new instance with the specified TLS connect options.
*
* @param TLS\ConnectOptions $tls_connect_options The TLS connect options.
*
* @mutation-free
*/
public function withTLSConnectOptions(TLS\ConnectOptions $tls_connect_options): ConnectOptions
{
return new self($this->noDelay, $this->bindTo, $tls_connect_options);
}

/**
* Returns a new instance without the Tls connect options.
*
* @mutation-free
*/
public function withoutTlsConnectOptions(): ConnectOptions
{
return new self($this->noDelay, $this->bindTo, null);
}
}
47 changes: 47 additions & 0 deletions src/Psl/TCP/TLS/Certificate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace Psl\TCP\TLS;

/**
* Represents a TLS certificate for secure TCP connections.
*
* This class encapsulates the necessary information for a TLS certificate, including
* the certificate file itself, an optional private key file, and an optional passphrase
* for the key.
*
* @immutable
*/
final class Certificate
{
/**
* Constructor for the Certificate class.
*
* @param non-empty-string $certificateFile The path to the certificate file.
* @param non-empty-string|null $keyFile The path to the private key file associated with the certificate, if any.
* @param non-empty-string|null $passphrase The passphrase for the private key file, if the file is encrypted.
*
* @pure
*/
public function __construct(
public readonly string $certificateFile,
public readonly ?string $keyFile = null,
public readonly ?string $passphrase = null,
) {
}

/**
* Creates a new Certificate instance.
*
* @param non-empty-string $certificate_file The path to the certificate file.
* @param non-empty-string|null $key_file The path to the private key file associated with the certificate.
* @param non-empty-string|null $passphrase The passphrase for the private key file, if the file is encrypted.
*
* @pure
*/
public static function create(string $certificate_file, ?string $key_file = null, ?string $passphrase = null): Certificate
{
return new self($certificate_file, $key_file, $passphrase);
}
}
Loading

0 comments on commit 3690275

Please sign in to comment.