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 26, 2024
1 parent b66e921 commit 956a462
Show file tree
Hide file tree
Showing 31 changed files with 1,247 additions and 61 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
45 changes: 31 additions & 14 deletions examples/tcp/basic-http-client.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,45 @@

declare(strict_types=1);

namespace Psl\Example\TCP;

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://psalm.dev/');

$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['scheme'] === 'https' ? 443 : 80;
$path = $parsed_url['path'] ?? '/';

return $response;
}
$options = TCP\ConnectOptions::create();
if ($port === 443) {
$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];
}
8 changes: 8 additions & 0 deletions src/Psl/File/ReadHandle.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ public function __construct(string $file)
parent::__construct($this->readHandle);
}

/**
* {@inheritDoc}
*/
public function reachedEndOfDataSource(): bool
{
return $this->readHandle->reachedEndOfDataSource();
}

/**
* {@inheritDoc}
*/
Expand Down
8 changes: 8 additions & 0 deletions src/Psl/File/ReadWriteHandle.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ public function __construct(string $file, WriteMode $write_mode = WriteMode::Ope
parent::__construct($this->readWriteHandle);
}

/**
* {@inheritDoc}
*/
public function reachedEndOfDataSource(): bool
{
return $this->readWriteHandle->reachedEndOfDataSource();
}

/**
* {@inheritDoc}
*/
Expand Down
8 changes: 8 additions & 0 deletions src/Psl/IO/CloseReadStreamHandle.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ public function __construct(mixed $stream)
$this->handle = new Internal\ResourceHandle($stream, read: true, write: false, seek: false, close: true);
}

/**
* {@inheritDoc}
*/
public function reachedEndOfDataSource(): bool
{
return $this->handle->reachedEndOfDataSource();
}

/**
* {@inheritDoc}
*/
Expand Down
8 changes: 8 additions & 0 deletions src/Psl/IO/CloseReadWriteStreamHandle.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ public function __construct(mixed $stream)
$this->handle = new Internal\ResourceHandle($stream, read: true, write: true, seek: false, close: true);
}

/**
* {@inheritDoc}
*/
public function reachedEndOfDataSource(): bool
{
return $this->handle->reachedEndOfDataSource();
}

/**
* {@inheritDoc}
*/
Expand Down
8 changes: 8 additions & 0 deletions src/Psl/IO/CloseSeekReadStreamHandle.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ public function __construct(mixed $stream)
$this->handle = new Internal\ResourceHandle($stream, read: true, write: false, seek: true, close: true);
}

/**
* {@inheritDoc}
*/
public function reachedEndOfDataSource(): bool
{
return $this->handle->reachedEndOfDataSource();
}

/**
* {@inheritDoc}
*/
Expand Down
8 changes: 8 additions & 0 deletions src/Psl/IO/CloseSeekReadWriteStreamHandle.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ public function __construct(mixed $stream)
$this->handle = new Internal\ResourceHandle($stream, read: true, write: true, seek: true, close: true);
}

/**
* {@inheritDoc}
*/
public function reachedEndOfDataSource(): bool
{
return $this->handle->reachedEndOfDataSource();
}

/**
* {@inheritDoc}
*/
Expand Down
84 changes: 59 additions & 25 deletions src/Psl/IO/Internal/ResourceHandle.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,19 @@

use function error_get_last;
use function fclose;
use function feof;
use function fread;
use function fseek;
use function ftell;
use function fwrite;
use function is_resource;
use function max;
use function str_contains;
use function stream_get_contents;
use function stream_get_meta_data;
use function stream_set_blocking;
use function stream_set_read_buffer;
use function stream_set_write_buffer;
use function substr;

/**
Expand Down Expand Up @@ -58,6 +62,8 @@ class ResourceHandle implements IO\CloseSeekReadWriteStreamHandleInterface
*/
private string $readWatcher = 'invalid';

private bool $reachedEof = false;

/**
* @var null|Async\Sequence<array{string, null|float}, int<0, max>>
*/
Expand All @@ -70,30 +76,37 @@ class ResourceHandle implements IO\CloseSeekReadWriteStreamHandleInterface
*/
private string $writeWatcher = 'invalid';

private bool $useSingleRead = false;

/**
* @param resource $stream
*/
public function __construct(mixed $stream, bool $read, bool $write, bool $seek, private bool $close)
public function __construct(mixed $stream, bool $read, bool $write, bool $seek, private readonly bool $close)
{
/** @psalm-suppress RedundantConditionGivenDocblockType - The stream is always a resource, but we want to make sure it is a stream resource. */
$this->stream = Type\resource('stream')->assert($stream);

/** @psalm-suppress UnusedFunctionCall */
stream_set_read_buffer($stream, 0);
stream_set_blocking($stream, false);

$meta = stream_get_meta_data($stream);
if ($read) {
$this->useSingleRead = ($meta["stream_type"] === "udp_socket" || $meta["stream_type"] === "STDIO");
}

$blocks = $meta['blocked'] || ($meta['wrapper_type'] ?? '') === 'plainfile';
if ($seek) {
Psl\invariant($meta['seekable'], 'Handle is not seekable.');
}

if ($read) {
stream_set_read_buffer($stream, 0);

Psl\invariant(str_contains($meta['mode'], 'r') || str_contains($meta['mode'], '+'), 'Handle is not readable.');

$this->readWatcher = EventLoop::onReadable($this->stream, function () {
$this->readSuspension?->resume(null);
$this->readSuspension?->resume();
});

$this->readSequence = new Async\Sequence(
/**
* @param array{null|int<1, max>, null|float} $input
Expand All @@ -102,6 +115,10 @@ function (array $input) use ($blocks): string {
[$max_bytes, $timeout] = $input;
$chunk = $this->tryRead($max_bytes);
if ('' !== $chunk || $blocks) {
if ($chunk === '' && feof($this->stream)) {
$this->reachedEof = true;
}

return $chunk;
}

Expand All @@ -120,8 +137,12 @@ function (array $input) use ($blocks): string {

try {
$suspension->suspend();
$chunk = $this->tryRead($max_bytes);
if ($chunk === '' && feof($this->stream)) {
$this->reachedEof = true;
}

return $this->tryRead($max_bytes);
return $chunk;
} finally {
$this->readSuspension = null;
EventLoop::disable($this->readWatcher);
Expand All @@ -136,6 +157,8 @@ function (array $input) use ($blocks): string {
}

if ($write) {
stream_set_write_buffer($stream, 0);

$writable = str_contains($meta['mode'], 'x')
|| str_contains($meta['mode'], 'w')
|| str_contains($meta['mode'], 'c')
Expand All @@ -144,9 +167,10 @@ function (array $input) use ($blocks): string {

Psl\invariant($writable, 'Handle is not writeable.');

$this->writeWatcher = EventLoop::onReadable($this->stream, function () {
$this->writeSuspension?->resume(null);
$this->writeWatcher = EventLoop::onWritable($this->stream, function () {
$this->writeSuspension?->resume();
});

$this->writeSequence = new Async\Sequence(
/**
* @param array{string, null|float} $input
Expand Down Expand Up @@ -254,6 +278,11 @@ public function tell(): int
return max($result, 0);
}

public function reachedEndOfDataSource(): bool
{
return $this->reachedEof;
}

/**
* {@inheritDoc}
*/
Expand All @@ -279,7 +308,13 @@ public function tryRead(?int $max_bytes = null): string
$max_bytes = self::MAXIMUM_READ_BUFFER_SIZE;
}

$result = fread($this->stream, $max_bytes);

if ($this->useSingleRead) {
$result = fread($this->stream, $max_bytes);
} else {
$result = stream_get_contents($this->stream, $max_bytes);
}

if ($result === false) {
/** @var array{message: string} $error */
$error = error_get_last();
Expand Down Expand Up @@ -311,31 +346,30 @@ public function close(): void
{
EventLoop::cancel($this->readWatcher);
EventLoop::cancel($this->writeWatcher);
if (null !== $this->stream) {

// don't close the stream if `$this->close` is false, or if it's already closed.
if ($this->close && is_resource($this->stream)) {
$exception = new Exception\AlreadyClosedException('Handle has already been closed.');

$this->readSequence?->cancel($exception);
$this->readSuspension?->throw($exception);

$this->writeSequence?->cancel($exception);

$this->readSuspension?->throw($exception);
$this->writeSuspension?->throw($exception);

// don't close the stream if `$this->close` is false, or if it's already closed.
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();
$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.');
}
} else {
// Stream could be set to a non-null closed-resource,
// if manually closed using `fclose($handle->getStream)`.
$this->stream = null;
throw new Exception\RuntimeException($error['message'] ?? 'unknown error.');
}
} else {
// Stream could be set to a non-null closed-resource,
// if manually closed using `fclose($handle->getStream)`.
$this->stream = null;
}
}
}
Loading

0 comments on commit 956a462

Please sign in to comment.