Initial Commit

This commit is contained in:
David Stone
2024-11-30 18:24:12 -07:00
commit e8f7955c1c
5432 changed files with 1397750 additions and 0 deletions

View File

@ -0,0 +1,136 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Socket;
use function WP_Ultimo\Dependencies\Amp\Socket\Internal\normalizeBindToOption;
final class BindContext
{
/** @var string|null */
private $bindTo;
/** @var int */
private $backlog = 128;
/** @var bool */
private $reusePort = \false;
/** @var bool */
private $broadcast = \false;
/** @var bool */
private $tcpNoDelay = \false;
/** @var int */
private $chunkSize = 8192;
/** @var ServerTlsContext|null */
private $tlsContext;
public function withoutBindTo() : self
{
return $this->withBindTo(null);
}
public function withBindTo(?string $bindTo) : self
{
$bindTo = normalizeBindToOption($bindTo);
$clone = clone $this;
$clone->bindTo = $bindTo;
return $clone;
}
public function getBindTo() : ?string
{
return $this->bindTo;
}
public function getBacklog() : int
{
return $this->backlog;
}
public function withBacklog(int $backlog) : self
{
$clone = clone $this;
$clone->backlog = $backlog;
return $clone;
}
public function hasReusePort() : bool
{
return $this->reusePort;
}
public function withReusePort() : self
{
$clone = clone $this;
$clone->reusePort = \true;
return $clone;
}
public function withoutReusePort() : self
{
$clone = clone $this;
$clone->reusePort = \false;
return $clone;
}
public function hasBroadcast() : bool
{
return $this->broadcast;
}
public function withBroadcast() : self
{
$clone = clone $this;
$clone->broadcast = \true;
return $clone;
}
public function withoutBroadcast() : self
{
$clone = clone $this;
$clone->broadcast = \false;
return $clone;
}
public function hasTcpNoDelay() : bool
{
return $this->tcpNoDelay;
}
public function withTcpNoDelay() : self
{
$clone = clone $this;
$clone->tcpNoDelay = \true;
return $clone;
}
public function withoutTcpNoDelay() : self
{
$clone = clone $this;
$clone->tcpNoDelay = \false;
return $clone;
}
public function getTlsContext() : ?ServerTlsContext
{
return $this->tlsContext;
}
public function withoutTlsContext() : self
{
return $this->withTlsContext(null);
}
public function withTlsContext(?ServerTlsContext $tlsContext) : self
{
$clone = clone $this;
$clone->tlsContext = $tlsContext;
return $clone;
}
public function getChunkSize() : int
{
return $this->chunkSize;
}
public function withChunkSize(int $chunkSize) : self
{
$clone = clone $this;
$clone->chunkSize = $chunkSize;
return $clone;
}
public function toStreamContextArray() : array
{
$array = ['socket' => [
'bindto' => $this->bindTo,
'backlog' => $this->backlog,
'ipv6_v6only' => \true,
// SO_REUSEADDR has SO_REUSEPORT semantics on Windows
'so_reuseaddr' => $this->reusePort && \stripos(\PHP_OS, 'WIN') === 0,
'so_reuseport' => $this->reusePort,
'so_broadcast' => $this->broadcast,
'tcp_nodelay' => $this->tcpNoDelay,
]];
if ($this->tlsContext) {
$array = \array_merge($array, $this->tlsContext->toStreamContextArray());
}
return $array;
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Socket;
/**
* @see ServerTlsContext::withDefaultCertificate()
* @see ServerTlsContext::withCertificates()
*/
final class Certificate
{
/** @var string */
private $certFile;
/** @var string */
private $keyFile;
/**
* @param string $certFile Certificate file with the certificate + intermediaries.
* @param string|null $keyFile Key file with the corresponding private key or `null` if the key is in $certFile.
*/
public function __construct(string $certFile, string $keyFile = null)
{
$this->certFile = $certFile;
$this->keyFile = $keyFile ?? $certFile;
}
/**
* @return string
*/
public function getCertFile() : string
{
return $this->certFile;
}
/**
* @return string
*/
public function getKeyFile() : string
{
return $this->keyFile;
}
}

View File

@ -0,0 +1,388 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Socket;
final class ClientTlsContext
{
public const TLSv1_0 = \STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT;
public const TLSv1_1 = \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
public const TLSv1_2 = \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
public const TLSv1_3 = \PHP_VERSION_ID >= 70400 ? \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT : 0;
private const TLS_VERSIONS = \PHP_VERSION_ID >= 70400 ? ['TLSv1.0' => self::TLSv1_0, 'TLSv1.1' => self::TLSv1_1, 'TLSv1.2' => self::TLSv1_2, 'TLSv1.3' => self::TLSv1_3] : ['TLSv1.0' => self::TLSv1_0, 'TLSv1.1' => self::TLSv1_1, 'TLSv1.2' => self::TLSv1_2];
/** @var int */
private $minVersion = self::TLSv1_0;
/** @var string|null */
private $peerName;
/** @var bool */
private $verifyPeer = \true;
/** @var int */
private $verifyDepth = 10;
/** @var string|null */
private $ciphers;
/** @var string|null */
private $caFile;
/** @var string|null */
private $caPath;
/** @var bool */
private $capturePeer = \false;
/** @var bool */
private $sniEnabled = \true;
/** @var int */
private $securityLevel = 2;
/** @var Certificate|null */
private $certificate;
/** @var string[] */
private $alpnProtocols = [];
public function __construct(string $peerName)
{
$this->peerName = $peerName;
}
/**
* Minimum TLS version to negotiate.
*
* Defaults to TLS 1.0.
*
* @param int $version One of the `ClientTlsContext::TLSv*` constants.
*
* @return self Cloned, modified instance.
* @throws \Error If an invalid minimum version is given.
*/
public function withMinimumVersion(int $version) : self
{
if (!\in_array($version, self::TLS_VERSIONS, \true)) {
throw new \Error(\sprintf('Invalid minimum version, only %s allowed', \implode(', ', \array_keys(self::TLS_VERSIONS))));
}
$clone = clone $this;
$clone->minVersion = $version;
return $clone;
}
/**
* Returns the minimum TLS version to negotiate.
*
* @return int
*/
public function getMinimumVersion() : int
{
return $this->minVersion;
}
/**
* Expected name of the peer.
*
* @param string $peerName
*
* @return self Cloned, modified instance.
*/
public function withPeerName(string $peerName) : self
{
$clone = clone $this;
$clone->peerName = $peerName;
return $clone;
}
/**
* @return null|string Expected name of the peer or `null` if such an expectation doesn't exist.
*/
public function getPeerName() : ?string
{
return $this->peerName;
}
/**
* Enable peer verification.
*
* @return self Cloned, modified instance.
*/
public function withPeerVerification() : self
{
$clone = clone $this;
$clone->verifyPeer = \true;
return $clone;
}
/**
* Disable peer verification, this is the default for servers.
*
* Warning: You usually shouldn't disable this setting for clients, because it allows active MitM attackers to
* intercept the communication and change it without anyone noticing.
*
* @return self Cloned, modified instance.
*/
public function withoutPeerVerification() : self
{
$clone = clone $this;
$clone->verifyPeer = \false;
return $clone;
}
/**
* @return bool Whether peer verification is enabled.
*/
public function hasPeerVerification() : bool
{
return $this->verifyPeer;
}
/**
* Maximum chain length the peer might present including the certificates in the local trust store.
*
* @param int $verifyDepth Maximum length of the certificate chain.
*
* @return self Cloned, modified instance.
*/
public function withVerificationDepth(int $verifyDepth) : self
{
if ($verifyDepth < 0) {
throw new \Error("Invalid verification depth ({$verifyDepth}), must be greater than or equal to 0");
}
$clone = clone $this;
$clone->verifyDepth = $verifyDepth;
return $clone;
}
/**
* @return int Maximum length of the certificate chain.
*/
public function getVerificationDepth() : int
{
return $this->verifyDepth;
}
/**
* List of ciphers to negotiate, the server's order is always preferred.
*
* @param string|null $ciphers List of ciphers in OpenSSL's format (colon separated).
*
* @return self Cloned, modified instance.
*/
public function withCiphers(string $ciphers = null) : self
{
$clone = clone $this;
$clone->ciphers = $ciphers;
return $clone;
}
/**
* @return string List of ciphers in OpenSSL's format (colon separated).
*/
public function getCiphers() : string
{
return $this->ciphers ?? \OPENSSL_DEFAULT_STREAM_CIPHERS;
}
/**
* CAFile to check for trusted certificates.
*
* @param string|null $cafile Path to the file or `null` to unset.
*
* @return self Cloned, modified instance.
*/
public function withCaFile(string $cafile = null) : self
{
$clone = clone $this;
$clone->caFile = $cafile;
return $clone;
}
/**
* @return null|string Path to the file if one is set, otherwise `null`.
*/
public function getCaFile() : ?string
{
return $this->caFile;
}
/**
* CAPath to check for trusted certificates.
*
* @param string|null $capath Path to the file or `null` to unset.
*
* @return self Cloned, modified instance.
*/
public function withCaPath(string $capath = null) : self
{
$clone = clone $this;
$clone->caPath = $capath;
return $clone;
}
/**
* @return null|string Path to the file if one is set, otherwise `null`.
*/
public function getCaPath() : ?string
{
return $this->caPath;
}
/**
* Capture the certificates sent by the peer.
*
* Note: This is the chain as sent by the peer, NOT the verified chain.
*
* @return self Cloned, modified instance.
*/
public function withPeerCapturing() : self
{
$clone = clone $this;
$clone->capturePeer = \true;
return $clone;
}
/**
* Don't capture the certificates sent by the peer.
*
* @return self Cloned, modified instance.
*/
public function withoutPeerCapturing() : self
{
$clone = clone $this;
$clone->capturePeer = \false;
return $clone;
}
/**
* @return bool Whether to capture the certificates sent by the peer.
*/
public function hasPeerCapturing() : bool
{
return $this->capturePeer;
}
/**
* Enable SNI.
*
* @return self Cloned, modified instance.
*/
public function withSni() : self
{
$clone = clone $this;
$clone->sniEnabled = \true;
return $clone;
}
/**
* Disable SNI.
*
* @return self Cloned, modified instance.
*/
public function withoutSni() : self
{
$clone = clone $this;
$clone->sniEnabled = \false;
return $clone;
}
/**
* @return bool Whether SNI is enabled or not.
*/
public function hasSni() : bool
{
return $this->sniEnabled;
}
/**
* Security level to use.
*
* Requires OpenSSL 1.1.0 or higher.
*
* @param int $level Must be between 0 and 5.
*
* @return self Cloned, modified instance.
*/
public function withSecurityLevel(int $level) : self
{
// See https://www.openssl.org/docs/manmaster/man3/SSL_CTX_set_security_level.html
// Level 2 is not recommended, because of SHA-1 by that document,
// but SHA-1 should be phased out now on general internet use.
// We therefore default to level 2.
if ($level < 0 || $level > 5) {
throw new \Error("Invalid security level ({$level}), must be between 0 and 5.");
}
if (!hasTlsSecurityLevelSupport()) {
throw new \Error("Can't set a security level, as PHP is compiled with OpenSSL < 1.1.0.");
}
$clone = clone $this;
$clone->securityLevel = $level;
return $clone;
}
/**
* @return int Security level between 0 and 5. Always 0 for OpenSSL < 1.1.0.
*/
public function getSecurityLevel() : int
{
// 0 is equivalent to previous versions of OpenSSL and just does nothing
if (!hasTlsSecurityLevelSupport()) {
return 0;
}
return $this->securityLevel;
}
/**
* Client certificate to use, if key is no present it assumes it is present in the same file as the certificate.
*
* @param Certificate $certificate Certificate and private key info
*
* @return self Cloned, modified instance.
*/
public function withCertificate(Certificate $certificate = null) : self
{
$clone = clone $this;
$clone->certificate = $certificate;
return $clone;
}
public function getCertificate() : ?Certificate
{
return $this->certificate;
}
/**
* @param string[] $protocols
*
* @return self Cloned, modified instance.
*/
public function withApplicationLayerProtocols(array $protocols) : self
{
if (!hasTlsAlpnSupport()) {
throw new \Error("Can't set an application layer protocol list, as PHP is compiled with OpenSSL < 1.0.2.");
}
foreach ($protocols as $protocol) {
if (!\is_string($protocol)) {
throw new \TypeError("Protocol names must be strings");
}
}
$clone = clone $this;
$clone->alpnProtocols = $protocols;
return $clone;
}
/**
* @return string[]
*/
public function getApplicationLayerProtocols() : array
{
return $this->alpnProtocols;
}
/**
* Converts this TLS context into PHP's equivalent stream context array.
*
* @return array Stream context array compatible with PHP's streams.
*/
public function toStreamContextArray() : array
{
$options = ['crypto_method' => $this->toStreamCryptoMethod(), 'peer_name' => $this->peerName, 'verify_peer' => $this->verifyPeer, 'verify_peer_name' => $this->verifyPeer, 'verify_depth' => $this->verifyDepth, 'ciphers' => $this->ciphers ?? \OPENSSL_DEFAULT_STREAM_CIPHERS, 'capture_peer_cert' => $this->capturePeer, 'capture_peer_cert_chain' => $this->capturePeer, 'SNI_enabled' => $this->sniEnabled];
if ($this->certificate !== null) {
$options['local_cert'] = $this->certificate->getCertFile();
if ($this->certificate->getCertFile() !== $this->certificate->getKeyFile()) {
$options['local_pk'] = $this->certificate->getKeyFile();
}
}
if ($this->caFile !== null) {
$options['cafile'] = $this->caFile;
}
if ($this->caPath !== null) {
$options['capath'] = $this->caPath;
}
if (hasTlsSecurityLevelSupport()) {
$options['security_level'] = $this->securityLevel;
}
if (!empty($this->alpnProtocols)) {
$options['alpn_protocols'] = \implode(',', $this->alpnProtocols);
}
return ['ssl' => $options];
}
/**
* @return int Crypto method compatible with PHP's streams.
*/
public function toStreamCryptoMethod() : int
{
switch ($this->minVersion) {
case self::TLSv1_0:
return self::TLSv1_0 | self::TLSv1_1 | self::TLSv1_2 | self::TLSv1_3;
case self::TLSv1_1:
return self::TLSv1_1 | self::TLSv1_2 | self::TLSv1_3;
case self::TLSv1_2:
return self::TLSv1_2 | self::TLSv1_3;
case self::TLSv1_3:
return self::TLSv1_3;
default:
throw new \RuntimeException('Unknown minimum TLS version: ' . $this->minVersion);
}
}
}

View File

@ -0,0 +1,121 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Socket;
use WP_Ultimo\Dependencies\Amp\Dns\Record;
use function WP_Ultimo\Dependencies\Amp\Socket\Internal\normalizeBindToOption;
final class ConnectContext
{
/** @var string|null */
private $bindTo;
/** @var int */
private $connectTimeout = 10000;
/** @var int */
private $maxAttempts = 2;
/** @var null|int */
private $typeRestriction;
/** @var bool */
private $tcpNoDelay = \false;
/** @var ClientTlsContext|null */
private $tlsContext;
public function withoutBindTo() : self
{
return $this->withBindTo(null);
}
public function withBindTo(?string $bindTo) : self
{
$bindTo = normalizeBindToOption($bindTo);
$clone = clone $this;
$clone->bindTo = $bindTo;
return $clone;
}
public function getBindTo() : ?string
{
return $this->bindTo;
}
public function withConnectTimeout(int $timeout) : self
{
if ($timeout <= 0) {
throw new \Error("Invalid connect timeout ({$timeout}), must be greater than 0");
}
$clone = clone $this;
$clone->connectTimeout = $timeout;
return $clone;
}
public function getConnectTimeout() : int
{
return $this->connectTimeout;
}
public function withMaxAttempts(int $maxAttempts) : self
{
if ($maxAttempts <= 0) {
throw new \Error("Invalid max attempts ({$maxAttempts}), must be greater than 0");
}
$clone = clone $this;
$clone->maxAttempts = $maxAttempts;
return $clone;
}
public function getMaxAttempts() : int
{
return $this->maxAttempts;
}
public function withoutDnsTypeRestriction() : self
{
return $this->withDnsTypeRestriction(null);
}
public function withDnsTypeRestriction(?int $type) : self
{
if ($type !== null && $type !== Record::AAAA && $type !== Record::A) {
throw new \Error('Invalid resolver type restriction');
}
$clone = clone $this;
$clone->typeRestriction = $type;
return $clone;
}
public function getDnsTypeRestriction() : ?int
{
return $this->typeRestriction;
}
public function hasTcpNoDelay() : bool
{
return $this->tcpNoDelay;
}
public function withTcpNoDelay() : self
{
$clone = clone $this;
$clone->tcpNoDelay = \true;
return $clone;
}
public function withoutTcpNoDelay() : self
{
$clone = clone $this;
$clone->tcpNoDelay = \false;
return $clone;
}
public function withoutTlsContext() : self
{
return $this->withTlsContext(null);
}
public function withTlsContext(?ClientTlsContext $tlsContext) : self
{
$clone = clone $this;
$clone->tlsContext = $tlsContext;
return $clone;
}
public function getTlsContext() : ?ClientTlsContext
{
return $this->tlsContext;
}
public function toStreamContextArray() : array
{
$options = ['tcp_nodelay' => $this->tcpNoDelay];
if ($this->bindTo !== null) {
$options['bindto'] = $this->bindTo;
}
$array = ['socket' => $options];
if ($this->tlsContext) {
$array = \array_merge($array, $this->tlsContext->toStreamContextArray());
}
return $array;
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Socket;
/**
* Thrown if connecting fails.
*/
class ConnectException extends SocketException
{
}

View File

@ -0,0 +1,23 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Socket;
use WP_Ultimo\Dependencies\Amp\CancellationToken;
use WP_Ultimo\Dependencies\Amp\CancelledException;
use WP_Ultimo\Dependencies\Amp\Promise;
interface Connector
{
/**
* Asynchronously establish a socket connection to the specified URI.
*
* @param string $uri URI in scheme://host:port format. TCP is assumed if no scheme is present.
* @param ConnectContext $context Socket connect context to use when connecting.
* @param CancellationToken|null $token
*
* @return Promise<EncryptableSocket>
*
* @throws ConnectException
* @throws CancelledException
*/
public function connect(string $uri, ?ConnectContext $context = null, ?CancellationToken $token = null) : Promise;
}

View File

@ -0,0 +1,213 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Socket;
use WP_Ultimo\Dependencies\Amp\Deferred;
use WP_Ultimo\Dependencies\Amp\Failure;
use WP_Ultimo\Dependencies\Amp\Loop;
use WP_Ultimo\Dependencies\Amp\Promise;
use WP_Ultimo\Dependencies\Amp\Success;
final class DatagramSocket
{
public const DEFAULT_CHUNK_SIZE = 8192;
/**
* Create a new Datagram (UDP server) on the specified server address.
*
* @param string $uri URI in scheme://host:port format. UDP is assumed if no scheme is present.
* @param BindContext|null $context Context options for listening.
*
* @return DatagramSocket
*
* @throws SocketException If binding to the specified URI failed.
* @throws \Error If an invalid scheme is given.
*/
public static function bind(string $uri, ?BindContext $context = null) : self
{
$context = $context ?? new BindContext();
$scheme = \strstr($uri, '://', \true);
if ($scheme === \false) {
$uri = 'udp://' . $uri;
} elseif ($scheme !== 'udp') {
throw new \Error('Only udp scheme allowed for datagram creation');
}
$streamContext = \stream_context_create($context->toStreamContextArray());
// Error reporting suppressed since stream_socket_server() emits an E_WARNING on failure (checked below).
$server = @\stream_socket_server($uri, $errno, $errstr, \STREAM_SERVER_BIND, $streamContext);
if (!$server || $errno) {
throw new SocketException(\sprintf('Could not create datagram %s: [Error: #%d] %s', $uri, $errno, $errstr), $errno);
}
return new self($server, $context->getChunkSize());
}
/** @var resource|null UDP socket resource. */
private $socket;
/** @var string Watcher ID. */
private $watcher;
/** @var SocketAddress */
private $address;
/** @var Deferred|null */
private $reader;
/** @var int */
private $chunkSize;
/**
* @param resource $socket A bound udp socket resource
* @param int $chunkSize Maximum chunk size for the
*
* @throws \Error If a stream resource is not given for $socket.
*/
public function __construct($socket, int $chunkSize = self::DEFAULT_CHUNK_SIZE)
{
if (!\is_resource($socket) || \get_resource_type($socket) !== 'stream') {
throw new \Error('Invalid resource given to constructor!');
}
$this->socket = $socket;
$this->address = SocketAddress::fromLocalResource($socket);
$this->chunkSize =& $chunkSize;
\stream_set_blocking($this->socket, \false);
$reader =& $this->reader;
$this->watcher = Loop::onReadable($this->socket, static function ($watcher, $socket) use(&$reader, &$chunkSize) {
$deferred = $reader;
$reader = null;
\assert($deferred !== null);
$data = @\stream_socket_recvfrom($socket, $chunkSize, 0, $address);
/** @psalm-suppress TypeDoesNotContainType */
if ($data === \false) {
Loop::cancel($watcher);
$deferred->resolve();
return;
}
$deferred->resolve([SocketAddress::fromSocketName($address), $data]);
/** @psalm-suppress RedundantCondition Resolution of the deferred above might read immediately again */
if (!$reader) {
Loop::disable($watcher);
}
});
Loop::disable($this->watcher);
}
/**
* Automatically cancels the loop watcher.
*/
public function __destruct()
{
if (!$this->socket) {
return;
}
$this->free();
}
/**
* @return Promise<array{0: SocketAddress, 1: string}|null> Resolves with null if the socket is closed.
*
* @throws PendingReceiveError If a receive request is already pending.
*/
public function receive() : Promise
{
if ($this->reader) {
throw new PendingReceiveError();
}
if (!$this->socket) {
return new Success();
// Resolve with null when endpoint is closed.
}
$this->reader = new Deferred();
Loop::enable($this->watcher);
return $this->reader->promise();
}
/**
* @param SocketAddress $address
* @param string $data
*
* @return Promise<int> Resolves with the number of bytes written to the socket.
*
* @throws SocketException If the UDP socket closes before the data can be sent.
*/
public function send(SocketAddress $address, string $data) : Promise
{
if (!$this->socket) {
return new Failure(new SocketException('The endpoint is not writable'));
}
try {
try {
\set_error_handler(static function (int $errno, string $errstr) {
throw new SocketException(\sprintf('Could not send packet on endpoint: %s', $errstr));
});
$result = \stream_socket_sendto($this->socket, $data, 0, $address->toString());
/** @psalm-suppress TypeDoesNotContainType */
if ($result < 0 || $result === \false) {
throw new SocketException('Could not send packet on endpoint: Unknown error');
}
} finally {
\restore_error_handler();
}
} catch (SocketException $e) {
return new Failure($e);
}
return new Success($result);
}
/**
* Raw stream socket resource.
*
* @return resource|null
*/
public final function getResource()
{
return $this->socket;
}
/**
* References the receive watcher.
*
* @see Loop::reference()
*/
public final function reference() : void
{
Loop::reference($this->watcher);
}
/**
* Unreferences the receive watcher.
*
* @see Loop::unreference()
*/
public final function unreference() : void
{
Loop::unreference($this->watcher);
}
/**
* Closes the datagram socket and stops receiving data. Any pending read is resolved with null.
*/
public function close() : void
{
if ($this->socket) {
/** @psalm-suppress InvalidPropertyAssignmentValue */
\fclose($this->socket);
}
$this->free();
}
/**
* @return bool
*/
public function isClosed() : bool
{
return $this->socket === null;
}
/**
* @return SocketAddress
*/
public function getAddress() : SocketAddress
{
return $this->address;
}
/**
* @param int $chunkSize The new maximum packet size to receive.
*/
public function setChunkSize(int $chunkSize) : void
{
$this->chunkSize = $chunkSize;
}
private function free() : void
{
Loop::cancel($this->watcher);
$this->socket = null;
if ($this->reader) {
$this->reader->resolve();
$this->reader = null;
}
}
}

View File

@ -0,0 +1,108 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Socket;
use WP_Ultimo\Dependencies\Amp\CancellationToken;
use WP_Ultimo\Dependencies\Amp\Deferred;
use WP_Ultimo\Dependencies\Amp\Dns;
use WP_Ultimo\Dependencies\Amp\Loop;
use WP_Ultimo\Dependencies\Amp\NullCancellationToken;
use WP_Ultimo\Dependencies\Amp\Promise;
use WP_Ultimo\Dependencies\Amp\TimeoutException;
use function WP_Ultimo\Dependencies\Amp\call;
final class DnsConnector implements Connector
{
private $resolver;
public function __construct(?Dns\Resolver $resolver = null)
{
$this->resolver = $resolver;
}
public function connect(string $uri, ?ConnectContext $context = null, ?CancellationToken $token = null) : Promise
{
$resolver = $this->resolver;
return call(static function () use($uri, $context, $token, $resolver) {
$context = $context ?? new ConnectContext();
$token = $token ?? new NullCancellationToken();
$attempt = 0;
$uris = [];
$failures = [];
[$scheme, $host, $port] = Internal\parseUri($uri);
if ($host[0] === '[') {
$host = \substr($host, 1, -1);
}
if ($port === 0 || @\inet_pton($host)) {
// Host is already an IP address or file path.
$uris = [$uri];
} else {
// Host is not an IP address, so resolve the domain name.
$records = (yield ($resolver ?? Dns\resolver())->resolve($host, $context->getDnsTypeRestriction()));
// Usually the faster response should be preferred, but we don't have a reliable way of determining IPv6
// support, so we always prefer IPv4 here.
\usort($records, static function (Dns\Record $a, Dns\Record $b) {
return $a->getType() - $b->getType();
});
foreach ($records as $record) {
/** @var Dns\Record $record */
if ($record->getType() === Dns\Record::AAAA) {
$uris[] = \sprintf('%s://[%s]:%d', $scheme, $record->getValue(), $port);
} else {
$uris[] = \sprintf('%s://%s:%d', $scheme, $record->getValue(), $port);
}
}
}
$flags = \STREAM_CLIENT_CONNECT | \STREAM_CLIENT_ASYNC_CONNECT;
$timeout = $context->getConnectTimeout();
foreach ($uris as $builtUri) {
try {
$streamContext = \stream_context_create($context->withoutTlsContext()->toStreamContextArray());
/** @psalm-suppress NullArgument */
if (!($socket = @\stream_socket_client($builtUri, $errno, $errstr, null, $flags, $streamContext))) {
throw new ConnectException(\sprintf('Connection to %s failed: [Error #%d] %s%s', $uri, $errno, $errstr, $failures ? '; previous attempts: ' . \implode($failures) : ''), $errno);
}
\stream_set_blocking($socket, \false);
$deferred = new Deferred();
$watcher = Loop::onWritable($socket, static function () use($deferred) {
$deferred->resolve();
});
$id = $token->subscribe([$deferred, 'fail']);
try {
(yield Promise\timeout($deferred->promise(), $timeout));
} catch (TimeoutException $e) {
throw new ConnectException(\sprintf('Connecting to %s failed: timeout exceeded (%d ms)%s', $uri, $timeout, $failures ? '; previous attempts: ' . \implode($failures) : ''), 110);
// See ETIMEDOUT in http://www.virtsync.com/c-error-codes-include-errno
} finally {
Loop::cancel($watcher);
$token->unsubscribe($id);
}
// The following hack looks like the only way to detect connection refused errors with PHP's stream sockets.
/** @psalm-suppress TypeDoesNotContainType */
if (\stream_socket_get_name($socket, \true) === \false) {
\fclose($socket);
throw new ConnectException(\sprintf('Connection to %s refused%s', $uri, $failures ? '; previous attempts: ' . \implode($failures) : ''), 111);
// See ECONNREFUSED in http://www.virtsync.com/c-error-codes-include-errno
}
} catch (ConnectException $e) {
// Includes only error codes used in this file, as error codes on other OS families might be different.
// In fact, this might show a confusing error message on OS families that return 110 or 111 by itself.
$knownReasons = [110 => 'connection timeout', 111 => 'connection refused'];
$code = $e->getCode();
$reason = $knownReasons[$code] ?? 'Error #' . $code;
if (++$attempt === $context->getMaxAttempts()) {
break;
}
$failures[] = "{$uri} ({$reason})";
continue;
// Could not connect to host, try next host in the list.
}
return ResourceSocket::fromClientSocket($socket, $context->getTlsContext());
}
/**
* This is reached if either all URIs failed or the maximum number of attempts is reached.
*
* @noinspection PhpUndefinedVariableInspection
* @psalm-suppress PossiblyUndefinedVariable
*/
throw $e;
});
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Socket;
use WP_Ultimo\Dependencies\Amp\CancellationToken;
use WP_Ultimo\Dependencies\Amp\Promise;
interface EncryptableSocket extends Socket
{
public const TLS_STATE_DISABLED = 0;
public const TLS_STATE_SETUP_PENDING = 1;
public const TLS_STATE_ENABLED = 2;
public const TLS_STATE_SHUTDOWN_PENDING = 3;
/**
* @param CancellationToken|null $cancellationToken
*
* @return Promise<void> Resolved when TLS is successfully set up on the socket.
*
* @throws SocketException Promise fails and the socket is closed if setting up TLS fails.
*/
public function setupTls(?CancellationToken $cancellationToken = null) : Promise;
/**
* @param CancellationToken|null $cancellationToken
*
* @return Promise<void> Resolved when TLS is successfully shutdown.
*
* @throws SocketException Promise fails and the socket is closed if shutting down TLS fails.
*/
public function shutdownTls(?CancellationToken $cancellationToken = null) : Promise;
/**
* @return int One of the TLS_STATE_* constants defined in this interface.
*/
public function getTlsState() : int;
/**
* @return TlsInfo|null The TLS (crypto) context info if TLS is enabled on the socket or null otherwise.
*/
public function getTlsInfo() : ?TlsInfo;
}

View File

@ -0,0 +1,213 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Socket\Internal;
use WP_Ultimo\Dependencies\Amp\CancellationToken;
use WP_Ultimo\Dependencies\Amp\Deferred;
use WP_Ultimo\Dependencies\Amp\Failure;
use WP_Ultimo\Dependencies\Amp\Loop;
use WP_Ultimo\Dependencies\Amp\NullCancellationToken;
use WP_Ultimo\Dependencies\Amp\Promise;
use WP_Ultimo\Dependencies\Amp\Socket\TlsException;
use WP_Ultimo\Dependencies\Amp\Success;
use WP_Ultimo\Dependencies\League\Uri;
use function WP_Ultimo\Dependencies\Amp\call;
/**
* Parse an URI into [scheme, host, port].
*
* @param string $uri
*
* @return array
*
* @throws \Error If an invalid URI has been passed.
*
* @internal
*/
function parseUri(string $uri) : array
{
if (\stripos($uri, 'unix://') === 0 || \stripos($uri, 'udg://') === 0) {
[$scheme, $path] = \explode('://', $uri, 2);
return [$scheme, \ltrim($path, '/'), 0];
}
if (\strpos($uri, '://') === \false) {
// Set a default scheme of tcp if none was given.
$uri = 'tcp://' . $uri;
}
try {
$uriParts = Uri\parse($uri);
} catch (\Exception $exception) {
throw new \Error("Invalid URI: {$uri}", 0, $exception);
}
$scheme = $uriParts['scheme'];
$host = $uriParts['host'] ?? '';
$port = $uriParts['port'] ?? 0;
if (!\in_array($scheme, ['tcp', 'udp', 'unix', 'udg'], \true)) {
throw new \Error("Invalid URI scheme ({$scheme}); tcp, udp, unix or udg scheme expected");
}
if ($host === '' || $port === 0) {
throw new \Error("Invalid URI: {$uri}; host and port components required");
}
if (\strpos($host, ':') !== \false) {
// IPv6 address
$host = \sprintf('[%s]', \trim($host, '[]'));
}
return [$scheme, $host, $port];
}
/**
* Enable encryption on an existing socket stream.
*
* @param resource $socket
* @param array $options
* @param CancellationToken $cancellationToken
*
* @return Promise<void>
*
* @internal
*/
function setupTls($socket, array $options, ?CancellationToken $cancellationToken) : Promise
{
$cancellationToken = $cancellationToken ?? new NullCancellationToken();
if (isset(\stream_get_meta_data($socket)['crypto'])) {
return new Failure(new TlsException("Can't setup TLS, because it has already been set up"));
}
\error_clear_last();
\stream_context_set_option($socket, $options);
try {
\set_error_handler(static function (int $errno, string $errstr) {
new TlsException('TLS negotiation failed: ' . $errstr);
});
$result = \stream_socket_enable_crypto($socket, $enable = \true);
if ($result === \false) {
throw new TlsException('TLS negotiation failed: Unknown error');
}
} catch (TlsException $e) {
return new Failure($e);
} finally {
\restore_error_handler();
}
// Yes, that function can return true / false / 0, don't use weak comparisons.
if ($result === \true) {
/** @psalm-suppress InvalidReturnStatement */
return new Success();
}
return call(static function () use($socket, $cancellationToken) {
$cancellationToken->throwIfRequested();
$deferred = new Deferred();
// Watcher is guaranteed to be created, because we throw above if cancellation has already been requested
$id = $cancellationToken->subscribe(static function ($e) use($deferred, &$watcher) {
Loop::cancel($watcher);
$deferred->fail($e);
});
$watcher = Loop::onReadable($socket, static function (string $watcher, $socket, Deferred $deferred) use($cancellationToken, $id) {
try {
try {
\set_error_handler(static function (int $errno, string $errstr) use($socket) {
if (\feof($socket)) {
$errstr = 'Connection reset by peer';
}
throw new TlsException('TLS negotiation failed: ' . $errstr);
});
$result = \stream_socket_enable_crypto($socket, \true);
if ($result === \false) {
$message = \feof($socket) ? 'Connection reset by peer' : 'Unknown error';
throw new TlsException('TLS negotiation failed: ' . $message);
}
} finally {
\restore_error_handler();
}
} catch (TlsException $e) {
Loop::cancel($watcher);
$cancellationToken->unsubscribe($id);
$deferred->fail($e);
return;
}
// If $result is 0, just wait for the next invocation
if ($result === \true) {
Loop::cancel($watcher);
$cancellationToken->unsubscribe($id);
$deferred->resolve();
}
}, $deferred);
return $deferred->promise();
});
}
/**
* Disable encryption on an existing socket stream.
*
* @param resource $socket
*
* @return Promise<void>
*
* @internal
* @psalm-suppress InvalidReturnType
*/
function shutdownTls($socket) : Promise
{
// note that disabling crypto *ALWAYS* returns false, immediately
// don't set _enabled to false, TLS can be setup only once
@\stream_socket_enable_crypto($socket, \false);
/** @psalm-suppress InvalidReturnStatement */
return new Success();
}
/**
* Normalizes "bindto" options to add a ":0" in case no port is present, otherwise PHP will silently ignore those.
*
* @param string|null $bindTo
*
* @return string|null
*
* @throws \Error If an invalid option has been passed.
*/
function normalizeBindToOption(string $bindTo = null)
{
if ($bindTo === null) {
return null;
}
if (\preg_match("/\\[(?P<ip>[0-9a-f:]+)\\](:(?P<port>\\d+))?\$/", $bindTo, $match)) {
$ip = $match['ip'];
$port = $match['port'] ?? 0;
if (@\inet_pton($ip) === \false) {
throw new \Error("Invalid IPv6 address: {$ip}");
}
if ($port < 0 || $port > 65535) {
throw new \Error("Invalid port: {$port}");
}
return "[{$ip}]:{$port}";
}
if (\preg_match("/(?P<ip>\\d+\\.\\d+\\.\\d+\\.\\d+)(:(?P<port>\\d+))?\$/", $bindTo, $match)) {
$ip = $match['ip'];
$port = $match['port'] ?? 0;
if (@\inet_pton($ip) === \false) {
throw new \Error("Invalid IPv4 address: {$ip}");
}
if ($port < 0 || $port > 65535) {
throw new \Error("Invalid port: {$port}");
}
return "{$ip}:{$port}";
}
throw new \Error("Invalid bindTo value: {$bindTo}");
}
/**
* Cleans up return values of stream_socket_get_name.
*
* @param string|false $address
*
* @return string|null
*/
function cleanupSocketName($address)
{
// https://3v4l.org/5C1lo
if ($address === \false || $address === "\x00") {
return null;
}
// Check if this is an IPv6 address which includes multiple colons but no square brackets
// @see https://github.com/reactphp/socket/blob/v0.8.10/src/TcpServer.php#L179-L184
// @license https://github.com/reactphp/socket/blob/v0.8.10/LICENSE
$pos = \strrpos($address, ':');
if ($pos !== \false && \strpos($address, ':') < $pos && $address[0] !== '[') {
$port = \substr($address, $pos + 1);
$address = '[' . \substr($address, 0, $pos) . ']:' . $port;
}
// -- End of imported code ----- //
return $address;
}

View File

@ -0,0 +1,14 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Socket;
/**
* Thrown in case a second read operation is attempted while another read operation is still pending.
*/
final class PendingAcceptError extends \Error
{
public function __construct(string $message = 'The previous accept operation must complete before accept can be called again', int $code = 0, \Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Socket;
/**
* Thrown in case a second read operation is attempted while another receive operation is still pending.
*/
final class PendingReceiveError extends \Error
{
public function __construct(string $message = 'The previous receive operation must complete before receive can be called again', int $code = 0, \Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}

View File

@ -0,0 +1,190 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Socket;
use WP_Ultimo\Dependencies\Amp\ByteStream\ClosedException;
use WP_Ultimo\Dependencies\Amp\ByteStream\ResourceInputStream;
use WP_Ultimo\Dependencies\Amp\ByteStream\ResourceOutputStream;
use WP_Ultimo\Dependencies\Amp\CancellationToken;
use WP_Ultimo\Dependencies\Amp\Failure;
use WP_Ultimo\Dependencies\Amp\Promise;
use function WP_Ultimo\Dependencies\Amp\call;
final class ResourceSocket implements EncryptableSocket
{
public const DEFAULT_CHUNK_SIZE = ResourceInputStream::DEFAULT_CHUNK_SIZE;
/**
* @param resource $resource Stream resource.
* @param int $chunkSize Read and write chunk size.
*
* @return self
*/
public static function fromServerSocket($resource, int $chunkSize = self::DEFAULT_CHUNK_SIZE) : self
{
return new self($resource, $chunkSize);
}
/**
* @param resource $resource Stream resource.
* @param int $chunkSize Read and write chunk size.
* @param ClientTlsContext|null $tlsContext
*
* @return self
*/
public static function fromClientSocket($resource, ?ClientTlsContext $tlsContext = null, int $chunkSize = self::DEFAULT_CHUNK_SIZE) : self
{
return new self($resource, $chunkSize, $tlsContext);
}
/** @var ClientTlsContext|null */
private $tlsContext;
/** @var int */
private $tlsState;
/** @var ResourceInputStream */
private $reader;
/** @var ResourceOutputStream */
private $writer;
/** @var SocketAddress */
private $localAddress;
/** @var SocketAddress */
private $remoteAddress;
/** @var TlsInfo|null */
private $tlsInfo;
/**
* @param resource $resource Stream resource.
* @param int $chunkSize Read and write chunk size.
* @param ClientTlsContext|null $tlsContext
*/
private function __construct($resource, int $chunkSize = self::DEFAULT_CHUNK_SIZE, ?ClientTlsContext $tlsContext = null)
{
$this->tlsContext = $tlsContext;
$this->reader = new ResourceInputStream($resource, $chunkSize);
$this->writer = new ResourceOutputStream($resource, $chunkSize);
$this->remoteAddress = SocketAddress::fromPeerResource($resource);
$this->localAddress = SocketAddress::fromLocalResource($resource);
$this->tlsState = self::TLS_STATE_DISABLED;
}
/** @inheritDoc */
public function setupTls(?CancellationToken $cancellationToken = null) : Promise
{
$resource = $this->getResource();
if ($resource === null) {
return new Failure(new ClosedException("Can't setup TLS, because the socket has already been closed"));
}
$this->tlsState = self::TLS_STATE_SETUP_PENDING;
if ($this->tlsContext) {
$promise = Internal\setupTls($resource, $this->tlsContext->toStreamContextArray(), $cancellationToken);
} else {
$context = @\stream_context_get_options($resource);
if (empty($context['ssl'])) {
return new Failure(new TlsException("Can't enable TLS without configuration. " . "If you used Amp\\Socket\\listen(), be sure to pass a ServerTlsContext within the BindContext " . "in the second argument, otherwise set the 'ssl' context option to the PHP stream resource."));
}
$promise = Internal\setupTls($resource, $context, $cancellationToken);
}
return call(function () use($promise) {
try {
(yield $promise);
$this->tlsState = self::TLS_STATE_ENABLED;
} catch (\Throwable $exception) {
$this->close();
throw $exception;
}
});
}
/** @inheritDoc */
public function shutdownTls(?CancellationToken $cancellationToken = null) : Promise
{
if (($resource = $this->reader->getResource()) === null) {
return new Failure(new ClosedException("Can't shutdown TLS, because the socket has already been closed"));
}
$this->tlsState = self::TLS_STATE_SHUTDOWN_PENDING;
return call(function () use($resource) {
try {
(yield Internal\shutdownTls($resource));
} finally {
$this->tlsState = self::TLS_STATE_DISABLED;
}
});
}
/** @inheritDoc */
public function read() : Promise
{
return $this->reader->read();
}
/** @inheritDoc */
public function write(string $data) : Promise
{
return $this->writer->write($data);
}
/** @inheritDoc */
public function end(string $data = '') : Promise
{
$promise = $this->writer->end($data);
$promise->onResolve(function () {
$this->close();
});
return $promise;
}
/** @inheritDoc */
public function close() : void
{
$this->reader->close();
$this->writer->close();
}
/** @inheritDoc */
public function reference() : void
{
$this->reader->reference();
}
/** @inheritDoc */
public function unreference() : void
{
$this->reader->unreference();
}
/** @inheritDoc */
public function getLocalAddress() : SocketAddress
{
return $this->localAddress;
}
/**
* @inheritDoc
*
* @return resource|null
*/
public function getResource()
{
return $this->reader->getResource();
}
/** @inheritDoc */
public function getRemoteAddress() : SocketAddress
{
return $this->remoteAddress;
}
/** @inheritDoc */
public function getTlsState() : int
{
return $this->tlsState;
}
/** @inheritDoc */
public function getTlsInfo() : ?TlsInfo
{
if (null !== $this->tlsInfo) {
return $this->tlsInfo;
}
$resource = $this->getResource();
if ($resource === null || !\is_resource($resource)) {
return null;
}
return $this->tlsInfo = TlsInfo::fromStreamResource($resource);
}
/** @inheritDoc */
public function isClosed() : bool
{
return $this->getResource() === null;
}
/**
* @param int $chunkSize New chunk size for reading and writing.
*/
public function setChunkSize(int $chunkSize) : void
{
$this->reader->setChunkSize($chunkSize);
$this->writer->setChunkSize($chunkSize);
}
}

179
dependencies/amphp/socket/src/Server.php vendored Normal file
View File

@ -0,0 +1,179 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Socket;
use WP_Ultimo\Dependencies\Amp\Deferred;
use WP_Ultimo\Dependencies\Amp\Loop;
use WP_Ultimo\Dependencies\Amp\Promise;
use WP_Ultimo\Dependencies\Amp\Success;
final class Server
{
/** @var resource|null Stream socket server resource. */
private $socket;
/** @var string Watcher ID. */
private $watcher;
/** @var SocketAddress Stream socket name */
private $address;
/** @var int */
private $chunkSize;
/** @var Deferred|null */
private $acceptor;
/**
* Listen for client connections on the specified server address.
*
* If you want to accept TLS connections, you have to use `yield $socket->setupTls()` after accepting new clients.
*
* @param string $uri URI in scheme://host:port format. TCP is assumed if no scheme is present.
* @param BindContext|null $context Context options for listening.
*
* @return Server
*
* @throws SocketException If binding to the specified URI failed.
* @throws \Error If an invalid scheme is given.
*/
public static function listen(string $uri, ?BindContext $context = null) : self
{
$context = $context ?? new BindContext();
$scheme = \strstr($uri, '://', \true);
if ($scheme === \false) {
$uri = 'tcp://' . $uri;
} elseif (!\in_array($scheme, ['tcp', 'unix'])) {
throw new \Error('Only tcp and unix schemes allowed for server creation');
}
$streamContext = \stream_context_create($context->toStreamContextArray());
// Error reporting suppressed since stream_socket_server() emits an E_WARNING on failure (checked below).
$server = @\stream_socket_server($uri, $errno, $errstr, \STREAM_SERVER_BIND | \STREAM_SERVER_LISTEN, $streamContext);
if (!$server || $errno) {
throw new SocketException(\sprintf('Could not create server %s: [Error: #%d] %s', $uri, $errno, $errstr), $errno);
}
return new self($server, $context->getChunkSize());
}
/**
* @param resource $socket A bound socket server resource
* @param int $chunkSize Chunk size for the input and output stream.
*
* @throws \Error If a stream resource is not given for $socket.
*/
public function __construct($socket, int $chunkSize = ResourceSocket::DEFAULT_CHUNK_SIZE)
{
if (!\is_resource($socket) || \get_resource_type($socket) !== 'stream') {
throw new \Error('Invalid resource given to constructor!');
}
$this->socket = $socket;
$this->chunkSize = $chunkSize;
$this->address = SocketAddress::fromLocalResource($socket);
\stream_set_blocking($this->socket, \false);
$acceptor =& $this->acceptor;
$this->watcher = Loop::onReadable($this->socket, static function ($watcher, $socket) use(&$acceptor, $chunkSize) {
// Error reporting suppressed since stream_socket_accept() emits E_WARNING on client accept failure.
if (!($client = @\stream_socket_accept($socket, 0))) {
// Timeout of 0 to be non-blocking.
return;
// Accepting client failed.
}
$deferred = $acceptor;
$acceptor = null;
\assert($deferred !== null);
$deferred->resolve(ResourceSocket::fromServerSocket($client, $chunkSize));
/** @psalm-suppress RedundantCondition Resolution of the deferred above might accept immediately again */
if (!$acceptor) {
Loop::disable($watcher);
}
});
Loop::disable($this->watcher);
}
/**
* Automatically cancels the loop watcher.
*/
public function __destruct()
{
if (!$this->socket) {
return;
}
$this->free();
}
private function free() : void
{
Loop::cancel($this->watcher);
$this->socket = null;
if ($this->acceptor) {
$this->acceptor->resolve();
$this->acceptor = null;
}
}
/**
* @return Promise<ResourceSocket|null>
*
* @throws PendingAcceptError If another accept request is pending.
*/
public function accept() : Promise
{
if ($this->acceptor) {
throw new PendingAcceptError();
}
if (!$this->socket) {
return new Success();
// Resolve with null when server is closed.
}
// Error reporting suppressed since stream_socket_accept() emits E_WARNING on client accept failure.
if ($client = @\stream_socket_accept($this->socket, 0)) {
// Timeout of 0 to be non-blocking.
return new Success(ResourceSocket::fromServerSocket($client, $this->chunkSize));
}
$this->acceptor = new Deferred();
Loop::enable($this->watcher);
return $this->acceptor->promise();
}
/**
* Closes the server and stops accepting connections. Any socket clients accepted will not be closed.
*/
public function close() : void
{
if ($this->socket) {
/** @psalm-suppress InvalidPropertyAssignmentValue */
\fclose($this->socket);
}
$this->free();
}
/**
* @return bool
*/
public function isClosed() : bool
{
return $this->socket === null;
}
/**
* References the accept watcher.
*
* @see Loop::reference()
*/
public final function reference() : void
{
Loop::reference($this->watcher);
}
/**
* Unreferences the accept watcher.
*
* @see Loop::unreference()
*/
public final function unreference() : void
{
Loop::unreference($this->watcher);
}
/**
* @return SocketAddress
*/
public function getAddress() : SocketAddress
{
return $this->address;
}
/**
* Raw stream socket resource.
*
* @return resource|null
*/
public final function getResource()
{
return $this->socket;
}
}

View File

@ -0,0 +1,394 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Socket;
final class ServerTlsContext
{
public const TLSv1_0 = \STREAM_CRYPTO_METHOD_TLSv1_0_SERVER;
public const TLSv1_1 = \STREAM_CRYPTO_METHOD_TLSv1_1_SERVER;
public const TLSv1_2 = \STREAM_CRYPTO_METHOD_TLSv1_2_SERVER;
public const TLSv1_3 = \PHP_VERSION_ID >= 70400 ? \STREAM_CRYPTO_METHOD_TLSv1_3_SERVER : 0;
private const TLS_VERSIONS = \PHP_VERSION_ID >= 70400 ? ['TLSv1.0' => self::TLSv1_0, 'TLSv1.1' => self::TLSv1_1, 'TLSv1.2' => self::TLSv1_2, 'TLSv1.3' => self::TLSv1_3] : ['TLSv1.0' => self::TLSv1_0, 'TLSv1.1' => self::TLSv1_1, 'TLSv1.2' => self::TLSv1_2];
/** @var int */
private $minVersion = self::TLSv1_0;
/** @var null|string */
private $peerName;
/** @var bool */
private $verifyPeer = \false;
/** @var int */
private $verifyDepth = 10;
/** @var null|string */
private $ciphers;
/** @var null|string */
private $caFile;
/** @var null|string */
private $caPath;
/** @var bool */
private $capturePeer = \false;
/** @var null|Certificate */
private $defaultCertificate;
/** @var Certificate[] */
private $certificates = [];
/** @var int */
private $securityLevel = 2;
/** @var string[] */
private $alpnProtocols = [];
/**
* Minimum TLS version to negotiate.
*
* Defaults to TLS 1.0.
*
* @param int $version One of the `ServerTlsContext::TLSv*` constants.
*
* @return self Cloned, modified instance.
* @throws \Error If an invalid minimum version is given.
*/
public function withMinimumVersion(int $version) : self
{
if (!\in_array($version, self::TLS_VERSIONS, \true)) {
throw new \Error(\sprintf('Invalid minimum version, only %s allowed', \implode(', ', \array_keys(self::TLS_VERSIONS))));
}
$clone = clone $this;
$clone->minVersion = $version;
return $clone;
}
/**
* Returns the minimum TLS version to negotiate.
*
* @return int
*/
public function getMinimumVersion() : int
{
return $this->minVersion;
}
/**
* Expected name of the peer.
*
* @param string|null $peerName
*
* @return self Cloned, modified instance.
*/
public function withPeerName(string $peerName = null) : self
{
$clone = clone $this;
$clone->peerName = $peerName;
return $clone;
}
/**
* @return null|string Expected name of the peer or `null` if such an expectation doesn't exist.
*/
public function getPeerName() : ?string
{
return $this->peerName;
}
/**
* Enable peer verification.
*
* @return self Cloned, modified instance.
*/
public function withPeerVerification() : self
{
$clone = clone $this;
$clone->verifyPeer = \true;
return $clone;
}
/**
* Disable peer verification, this is the default for servers.
*
* @return self Cloned, modified instance.
*/
public function withoutPeerVerification() : self
{
$clone = clone $this;
$clone->verifyPeer = \false;
return $clone;
}
/**
* @return bool Whether peer verification is enabled.
*/
public function hasPeerVerification() : bool
{
return $this->verifyPeer;
}
/**
* Maximum chain length the peer might present including the certificates in the local trust store.
*
* @param int $verifyDepth Maximum length of the certificate chain.
*
* @return self Cloned, modified instance.
*/
public function withVerificationDepth(int $verifyDepth) : self
{
if ($verifyDepth < 0) {
throw new \Error("Invalid verification depth ({$verifyDepth}), must be greater than or equal to 0");
}
$clone = clone $this;
$clone->verifyDepth = $verifyDepth;
return $clone;
}
/**
* @return int Maximum length of the certificate chain.
*/
public function getVerificationDepth() : int
{
return $this->verifyDepth;
}
/**
* List of ciphers to negotiate, the server's order is always preferred.
*
* @param string|null $ciphers List of ciphers in OpenSSL's format (colon separated).
*
* @return self Cloned, modified instance.
*/
public function withCiphers(string $ciphers = null) : self
{
$clone = clone $this;
$clone->ciphers = $ciphers;
return $clone;
}
/**
* @return string List of ciphers in OpenSSL's format (colon separated).
*/
public function getCiphers() : string
{
return $this->ciphers ?? \OPENSSL_DEFAULT_STREAM_CIPHERS;
}
/**
* CAFile to check for trusted certificates.
*
* @param string|null $cafile Path to the file or `null` to unset.
*
* @return self Cloned, modified instance.
*/
public function withCaFile(string $cafile = null) : self
{
$clone = clone $this;
$clone->caFile = $cafile;
return $clone;
}
/**
* @return null|string Path to the file if one is set, otherwise `null`.
*/
public function getCaFile() : ?string
{
return $this->caFile;
}
/**
* CAPath to check for trusted certificates.
*
* @param string|null $capath Path to the file or `null` to unset.
*
* @return self Cloned, modified instance.
*/
public function withCaPath(string $capath = null) : self
{
$clone = clone $this;
$clone->caPath = $capath;
return $clone;
}
/**
* @return null|string Path to the file if one is set, otherwise `null`.
*/
public function getCaPath() : ?string
{
return $this->caPath;
}
/**
* Capture the certificates sent by the peer.
*
* Note: This is the chain as sent by the peer, NOT the verified chain.
*
* @return self Cloned, modified instance.
*/
public function withPeerCapturing() : self
{
$clone = clone $this;
$clone->capturePeer = \true;
return $clone;
}
/**
* Don't capture the certificates sent by the peer.
*
* @return self Cloned, modified instance.
*/
public function withoutPeerCapturing() : self
{
$clone = clone $this;
$clone->capturePeer = \false;
return $clone;
}
/**
* @return bool Whether to capture the certificates sent by the peer.
*/
public function hasPeerCapturing() : bool
{
return $this->capturePeer;
}
/**
* Default certificate to use in case no SNI certificate matches.
*
* @param Certificate|null $defaultCertificate
*
* @return self Cloned, modified instance.
*/
public function withDefaultCertificate(Certificate $defaultCertificate = null) : self
{
$clone = clone $this;
$clone->defaultCertificate = $defaultCertificate;
return $clone;
}
/**
* @return Certificate|null Default certificate to use in case no SNI certificate matches, or `null` if unset.
*/
public function getDefaultCertificate() : ?Certificate
{
return $this->defaultCertificate;
}
/**
* Certificates to use for the given host names.
*
* @param array $certificates Must be a associative array mapping hostnames to certificate instances.
*
* @return self Cloned, modified instance.
*/
public function withCertificates(array $certificates) : self
{
foreach ($certificates as $key => $certificate) {
if (!\is_string($key)) {
throw new \TypeError('Expected an array mapping domain names to Certificate instances');
}
if (!$certificate instanceof Certificate) {
throw new \TypeError('Expected an array of Certificate instances');
}
if (\PHP_VERSION_ID < 70200 && $certificate->getCertFile() !== $certificate->getKeyFile()) {
throw new \Error('Different files for cert and key are not supported on this version of PHP. ' . 'Please upgrade to PHP 7.2 or later.');
}
}
$clone = clone $this;
$clone->certificates = $certificates;
return $clone;
}
/**
* @return array Associative array mapping hostnames to certificate instances.
*/
public function getCertificates() : array
{
return $this->certificates;
}
/**
* Security level to use.
*
* Requires OpenSSL 1.1.0 or higher.
*
* @param int $level Must be between 0 and 5.
*
* @return self Cloned, modified instance.
*/
public function withSecurityLevel(int $level) : self
{
// See https://www.openssl.org/docs/manmaster/man3/SSL_CTX_set_security_level.html
// Level 2 is not recommended, because of SHA-1 by that document,
// but SHA-1 should be phased out now on general internet use.
// We therefore default to level 2.
if ($level < 0 || $level > 5) {
throw new \Error("Invalid security level ({$level}), must be between 0 and 5.");
}
if (!hasTlsSecurityLevelSupport()) {
throw new \Error("Can't set a security level, as PHP is compiled with OpenSSL < 1.1.0.");
}
$clone = clone $this;
$clone->securityLevel = $level;
return $clone;
}
/**
* @return int Security level between 0 and 5. Always 0 for OpenSSL < 1.1.0.
*/
public function getSecurityLevel() : int
{
// 0 is equivalent to previous versions of OpenSSL and just does nothing
if (!hasTlsSecurityLevelSupport()) {
return 0;
}
return $this->securityLevel;
}
/**
* @param string[] $protocols
*
* @return self Cloned, modified instance.
*/
public function withApplicationLayerProtocols(array $protocols) : self
{
if (!hasTlsAlpnSupport()) {
throw new \Error("Can't set an application layer protocol list, as PHP is compiled with OpenSSL < 1.0.2.");
}
foreach ($protocols as $protocol) {
if (!\is_string($protocol)) {
throw new \TypeError("Protocol names must be strings");
}
}
$clone = clone $this;
$clone->alpnProtocols = $protocols;
return $clone;
}
/**
* @return string[]
*/
public function getApplicationLayerProtocols() : array
{
return $this->alpnProtocols;
}
/**
* Converts this TLS context into PHP's equivalent stream context array.
*
* @return array Stream context array compatible with PHP's streams.
*/
public function toStreamContextArray() : array
{
$options = ['crypto_method' => $this->toStreamCryptoMethod(), 'peer_name' => $this->peerName, 'verify_peer' => $this->verifyPeer, 'verify_peer_name' => $this->verifyPeer, 'verify_depth' => $this->verifyDepth, 'ciphers' => $this->ciphers ?? \OPENSSL_DEFAULT_STREAM_CIPHERS, 'honor_cipher_order' => \true, 'single_dh_use' => \true, 'no_ticket' => \true, 'capture_peer_cert' => $this->capturePeer, 'capture_peer_chain' => $this->capturePeer];
if (!empty($this->alpnProtocols)) {
$options['alpn_protocols'] = \implode(',', $this->alpnProtocols);
}
if ($this->defaultCertificate !== null) {
$options['local_cert'] = $this->defaultCertificate->getCertFile();
if ($this->defaultCertificate->getCertFile() !== $this->defaultCertificate->getKeyFile()) {
$options['local_pk'] = $this->defaultCertificate->getKeyFile();
}
}
if ($this->certificates) {
$options['SNI_server_certs'] = \array_map(static function (Certificate $certificate) {
if ($certificate->getCertFile() === $certificate->getKeyFile()) {
return $certificate->getCertFile();
}
return ['local_cert' => $certificate->getCertFile(), 'local_pk' => $certificate->getKeyFile()];
}, $this->certificates);
}
if ($this->caFile !== null) {
$options['cafile'] = $this->caFile;
}
if ($this->caPath !== null) {
$options['capath'] = $this->caPath;
}
if (\OPENSSL_VERSION_NUMBER >= 0x10100000) {
$options['security_level'] = $this->securityLevel;
}
return ['ssl' => $options];
}
/**
* @return int Crypto method compatible with PHP's streams.
*/
public function toStreamCryptoMethod() : int
{
switch ($this->minVersion) {
case self::TLSv1_0:
return self::TLSv1_0 | self::TLSv1_1 | self::TLSv1_2 | self::TLSv1_3;
case self::TLSv1_1:
return self::TLSv1_1 | self::TLSv1_2 | self::TLSv1_3;
case self::TLSv1_2:
return self::TLSv1_2 | self::TLSv1_3;
case self::TLSv1_3:
return self::TLSv1_3;
default:
throw new \RuntimeException('Unknown minimum TLS version: ' . $this->minVersion);
}
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Socket;
use WP_Ultimo\Dependencies\Amp\ByteStream\InputStream;
use WP_Ultimo\Dependencies\Amp\ByteStream\OutputStream;
interface Socket extends InputStream, OutputStream
{
/**
* References the read watcher, so the loop keeps running in case there's an active read.
*
* @see Loop::reference()
*/
public function reference() : void;
/**
* Unreferences the read watcher, so the loop doesn't keep running even if there are active reads.
*
* @see Loop::unreference()
*/
public function unreference() : void;
/**
* Force closes the socket, failing any pending reads or writes.
*/
public function close() : void;
/**
* Returns whether the socket has been closed.
*
* @return bool {@code true} if closed, otherwise {@code false}
*/
public function isClosed() : bool;
/**
* @return SocketAddress
*/
public function getLocalAddress() : SocketAddress;
/**
* @return SocketAddress
*/
public function getRemoteAddress() : SocketAddress;
}

View File

@ -0,0 +1,108 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Socket;
final class SocketAddress
{
/** @var string */
private $host;
/** @var int|null */
private $port;
/**
* @param resource $resource
*
* @return self
*/
public static function fromPeerResource($resource) : self
{
$name = @\stream_socket_get_name($resource, \true);
/** @psalm-suppress TypeDoesNotContainType */
if ($name === \false || $name === "\x00") {
return self::fromLocalResource($resource);
}
return self::fromSocketName($name);
}
/**
* @param resource $resource
*
* @return self
*/
public static function fromLocalResource($resource) : self
{
$wantPeer = \false;
do {
$name = @\stream_socket_get_name($resource, $wantPeer);
/** @psalm-suppress RedundantCondition */
if ($name !== \false && $name !== "\x00") {
return self::fromSocketName($name);
}
} while ($wantPeer = !$wantPeer);
return new self('');
}
/**
* @param string $name
*
* @return self
*/
public static function fromSocketName(string $name) : self
{
if ($portStartPos = \strrpos($name, ':')) {
$host = \substr($name, 0, $portStartPos);
$port = (int) \substr($name, $portStartPos + 1);
return new self($host, $port);
}
return new self($name);
}
/**
* @param string $host
* @param int|null $port
*/
public function __construct(string $host, ?int $port = null)
{
if ($port !== null && ($port < 1 || $port > 65535)) {
throw new \Error('Port number must be null or an integer between 1 and 65535');
}
if (\strrpos($host, ':')) {
$host = \trim($host, '[]');
}
$this->host = $host;
$this->port = $port;
}
/**
* @return string
*/
public function getHost() : string
{
return $this->host;
}
/**
* @return int
*/
public function getPort() : ?int
{
return $this->port;
}
/**
* @return string host:port formatted string.
*/
public function toString() : string
{
$host = $this->host;
if (\strrpos($host, ':')) {
$host = '[' . $host . ']';
}
if ($this->port === null) {
return $host;
}
return $host . ':' . $this->port;
}
/**
* @see toString
*
* @return string
*/
public function __toString() : string
{
return $this->toString();
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Socket;
use WP_Ultimo\Dependencies\Amp\ByteStream\StreamException;
class SocketException extends StreamException
{
}

View File

@ -0,0 +1,50 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Socket;
use WP_Ultimo\Dependencies\Amp\CancellationToken;
use WP_Ultimo\Dependencies\Amp\CancelledException;
use WP_Ultimo\Dependencies\Amp\Promise;
/**
* Allows pooling of connections for stateless protocols.
*/
interface SocketPool
{
/**
* Checkout a socket from the specified URI authority.
*
* The resulting socket resource should be checked back in via `SocketPool::checkin()` once the calling code is
* finished with the stream (even if the socket has been closed). Failure to checkin sockets will result in memory
* leaks and socket queue blockage. Instead of checking the socket in again, it can also be cleared to prevent
* re-use.
*
* @param string $uri URI in scheme://host:port format. TCP is assumed if no scheme is present. An
* optional fragment component can be used to differentiate different socket groups connected to the same URI.
* Connections to the same host with a different ConnectContext must use separate socket groups internally to
* prevent TLS negotiation with the wrong peer name or other TLS settings.
* @param ConnectContext $context Socket connect context to use when connecting.
* @param CancellationToken|null $token Optional cancellation token to cancel the checkout request.
*
* @return Promise<EncryptableSocket> Resolves to an EncryptableSocket instance once a connection is available.
*
* @throws SocketException
* @throws CancelledException
*/
public function checkout(string $uri, ConnectContext $context = null, CancellationToken $token = null) : Promise;
/**
* Return a previously checked-out socket to the pool so it can be reused.
*
* @param EncryptableSocket $socket Socket instance.
*
* @throws \Error If the provided resource is unknown to the pool.
*/
public function checkin(EncryptableSocket $socket) : void;
/**
* Remove the specified socket from the pool.
*
* @param EncryptableSocket $socket Socket instance.
*
* @throws \Error If the provided resource is unknown to the pool.
*/
public function clear(EncryptableSocket $socket) : void;
}

View File

@ -0,0 +1,23 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Socket;
use WP_Ultimo\Dependencies\Amp\CancellationToken;
use WP_Ultimo\Dependencies\Amp\Promise;
/**
* Connector that connects to a statically defined URI instead of the URI passed to the connect() call.
*/
final class StaticConnector implements Connector
{
private $uri;
private $connector;
public function __construct(string $uri, Connector $connector)
{
$this->uri = $uri;
$this->connector = $connector;
}
public function connect(string $uri, ?ConnectContext $context = null, ?CancellationToken $token = null) : Promise
{
return $this->connector->connect($this->uri, $context, $token);
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Socket;
/**
* Thrown if TLS can't be properly negotiated or is not supported on the given socket.
*/
class TlsException extends SocketException
{
}

View File

@ -0,0 +1,106 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Socket;
use WP_Ultimo\Dependencies\Kelunik\Certificate\Certificate;
/**
* Exposes a connection's negotiated TLS parameters.
*/
final class TlsInfo
{
/** @var string */
private $version;
/** @var string */
private $cipherName;
/** @var int */
private $cipherBits;
/** @var string */
private $cipherVersion;
/** @var string|null */
private $alpnProtocol;
/** @var array|null */
private $certificates;
/** @var Certificate[]|null */
private $parsedCertificates;
/**
* Constructs a new instance from a stream socket resource.
*
* @param resource $resource Stream socket resource.
*
* @return self|null Returns null if TLS is not enabled on the stream socket.
*/
public static function fromStreamResource($resource) : ?self
{
if (!\is_resource($resource) || \get_resource_type($resource) !== 'stream') {
throw new \Error("Expected a valid stream resource");
}
$metadata = \stream_get_meta_data($resource)['crypto'] ?? [];
$tlsContext = \stream_context_get_options($resource)['ssl'] ?? [];
return empty($metadata) ? null : self::fromMetaData($metadata, $tlsContext);
}
/**
* Constructs a new instance from PHP's internal info.
*
* Always pass the info as obtained from PHP as this method might extract additional fields in the future.
*
* @param array $cryptoInfo Crypto info obtained via `stream_get_meta_data($socket->getResource())["crypto"]`.
* @param array $tlsContext Context obtained via `stream_context_get_options($socket->getResource())["ssl"])`.
*
* @return self
*/
public static function fromMetaData(array $cryptoInfo, array $tlsContext) : self
{
if (isset($tlsContext["peer_certificate"])) {
$certificates = \array_merge([$tlsContext["peer_certificate"]], $tlsContext["peer_certificate_chain"] ?? []);
} else {
$certificates = $tlsContext["peer_certificate_chain"] ?? [];
}
return new self($cryptoInfo["protocol"], $cryptoInfo["cipher_name"], $cryptoInfo["cipher_bits"], $cryptoInfo["cipher_version"], $cryptoInfo["alpn_protocol"] ?? null, empty($certificates) ? null : $certificates);
}
private function __construct(string $version, string $cipherName, int $cipherBits, string $cipherVersion, ?string $alpnProtocol, ?array $certificates)
{
$this->version = $version;
$this->cipherName = $cipherName;
$this->cipherBits = $cipherBits;
$this->cipherVersion = $cipherVersion;
$this->alpnProtocol = $alpnProtocol;
$this->certificates = $certificates;
}
public function getVersion() : string
{
return $this->version;
}
public function getCipherName() : string
{
return $this->cipherName;
}
public function getCipherBits() : int
{
return $this->cipherBits;
}
public function getCipherVersion() : string
{
return $this->cipherVersion;
}
public function getApplicationLayerProtocol() : ?string
{
return $this->alpnProtocol;
}
/**
* @return Certificate[]
*
* @throws SocketException If peer certificates were not captured.
*/
public function getPeerCertificates() : array
{
if ($this->certificates === null) {
throw new SocketException("Peer certificates not captured; use ClientTlsContext::withPeerCapturing() to capture peer certificates");
}
if ($this->parsedCertificates === null) {
$this->parsedCertificates = \array_map(static function ($resource) {
return new Certificate($resource);
}, $this->certificates);
}
return $this->parsedCertificates;
}
}

View File

@ -0,0 +1,203 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Socket;
use WP_Ultimo\Dependencies\Amp\CancellationToken;
use WP_Ultimo\Dependencies\Amp\CancelledException;
use WP_Ultimo\Dependencies\Amp\Failure;
use WP_Ultimo\Dependencies\Amp\Loop;
use WP_Ultimo\Dependencies\Amp\Promise;
use WP_Ultimo\Dependencies\Amp\Struct;
use WP_Ultimo\Dependencies\Amp\Success;
use WP_Ultimo\Dependencies\League\Uri;
use function WP_Ultimo\Dependencies\Amp\call;
/**
* SocketPool implementation that doesn't impose any limits on concurrent open connections.
*/
final class UnlimitedSocketPool implements SocketPool
{
private const ALLOWED_SCHEMES = ['tcp' => null, 'unix' => null];
/** @var object[][] */
private $sockets = [];
/** @var string[] */
private $objectIdCacheKeyMap = [];
/** @var int[] */
private $pendingCount = [];
/** @var int */
private $idleTimeout;
/** @var Connector */
private $connector;
public function __construct(int $idleTimeout = 10000, ?Connector $connector = null)
{
$this->idleTimeout = $idleTimeout;
$this->connector = $connector ?? connector();
}
/** @inheritdoc */
public function checkout(string $uri, ConnectContext $context = null, CancellationToken $token = null) : Promise
{
// A request might already be cancelled before we reach the checkout, so do not even attempt to checkout in that
// case. The weird logic is required to throw the token's exception instead of creating a new one.
if ($token && $token->isRequested()) {
try {
$token->throwIfRequested();
} catch (CancelledException $e) {
return new Failure($e);
}
}
[$uri, $fragment] = $this->normalizeUri($uri);
$cacheKey = $uri;
if ($context && ($tlsContext = $context->getTlsContext())) {
$cacheKey .= ' + ' . \serialize($tlsContext->toStreamContextArray());
}
if ($fragment !== null) {
$cacheKey .= ' # ' . $fragment;
}
if (empty($this->sockets[$cacheKey])) {
return $this->checkoutNewSocket($uri, $cacheKey, $context, $token);
}
foreach ($this->sockets[$cacheKey] as $socketId => $socket) {
if (!$socket->isAvailable) {
continue;
}
if ($socket->object instanceof ResourceSocket) {
$resource = $socket->object->getResource();
if (!$resource || !\is_resource($resource) || \feof($resource)) {
$this->clearFromId(\spl_object_hash($socket->object));
continue;
}
} elseif ($socket->object->isClosed()) {
$this->clearFromId(\spl_object_hash($socket->object));
continue;
}
$socket->isAvailable = \false;
if ($socket->idleWatcher !== null) {
Loop::disable($socket->idleWatcher);
}
return new Success($socket->object);
}
return $this->checkoutNewSocket($uri, $cacheKey, $context, $token);
}
/** @inheritdoc */
public function clear(EncryptableSocket $socket) : void
{
$this->clearFromId(\spl_object_hash($socket));
}
/** @inheritdoc */
public function checkin(EncryptableSocket $socket) : void
{
$objectId = \spl_object_hash($socket);
if (!isset($this->objectIdCacheKeyMap[$objectId])) {
throw new \Error(\sprintf('Unknown socket: %d', $objectId));
}
$cacheKey = $this->objectIdCacheKeyMap[$objectId];
if ($socket instanceof ResourceSocket) {
$resource = $socket->getResource();
if (!$resource || !\is_resource($resource) || \feof($resource)) {
$this->clearFromId(\spl_object_hash($socket));
return;
}
} elseif ($socket->isClosed()) {
$this->clearFromId(\spl_object_hash($socket));
return;
}
$socket = $this->sockets[$cacheKey][$objectId];
$socket->isAvailable = \true;
if (isset($socket->idleWatcher)) {
Loop::enable($socket->idleWatcher);
} else {
$socket->idleWatcher = Loop::delay($this->idleTimeout, function () use($socket) {
$this->clearFromId(\spl_object_hash($socket->object));
});
Loop::unreference($socket->idleWatcher);
}
}
/**
* @param string $uri
*
* @return array
*
* @throws SocketException
*/
private function normalizeUri(string $uri) : array
{
if (\stripos($uri, 'unix://') === 0) {
return \explode('#', $uri) + [null, null];
}
try {
$parts = Uri\parse($uri);
} catch (\Exception $exception) {
throw new SocketException('Could not parse URI', 0, $exception);
}
if ($parts['scheme'] === null) {
throw new SocketException('Invalid URI for socket pool; no scheme given');
}
$port = $parts['port'] ?? 0;
if ($port === 0 || $parts['host'] === null) {
throw new SocketException('Invalid URI for socket pool; missing host or port');
}
$scheme = \strtolower($parts['scheme']);
$host = \strtolower($parts['host']);
if (!\array_key_exists($scheme, self::ALLOWED_SCHEMES)) {
throw new SocketException(\sprintf("Invalid URI for socket pool; '%s' scheme not allowed - scheme must be one of %s", $scheme, \implode(', ', \array_keys(self::ALLOWED_SCHEMES))));
}
if ($parts['query'] !== null) {
throw new SocketException('Invalid URI for socket pool; query component not allowed');
}
if ($parts['path'] !== '') {
throw new SocketException('Invalid URI for socket pool; path component must be empty');
}
if ($parts['user'] !== null) {
throw new SocketException('Invalid URI for socket pool; user component not allowed');
}
return [$scheme . '://' . $host . ':' . $port, $parts['fragment']];
}
private function checkoutNewSocket(string $uri, string $cacheKey, ConnectContext $connectContext = null, CancellationToken $token = null) : Promise
{
return call(function () use($uri, $cacheKey, $connectContext, $token) {
$this->pendingCount[$uri] = ($this->pendingCount[$uri] ?? 0) + 1;
try {
/** @var EncryptableSocket $socket */
$socket = (yield $this->connector->connect($uri, $connectContext, $token));
} finally {
if (--$this->pendingCount[$uri] === 0) {
unset($this->pendingCount[$uri]);
}
}
/** @psalm-suppress MissingConstructor */
$socketEntry = new class
{
use Struct;
/** @var string */
public $uri;
/** @var EncryptableSocket */
public $object;
/** @var bool */
public $isAvailable;
/** @var string|null */
public $idleWatcher;
};
$socketEntry->uri = $uri;
$socketEntry->isAvailable = \false;
$socketEntry->object = $socket;
$objectId = \spl_object_hash($socket);
$this->sockets[$cacheKey][$objectId] = $socketEntry;
$this->objectIdCacheKeyMap[$objectId] = $cacheKey;
return $socket;
});
}
private function clearFromId(string $objectId) : void
{
if (!isset($this->objectIdCacheKeyMap[$objectId])) {
throw new \Error(\sprintf('Unknown socket: %d', $objectId));
}
$cacheKey = $this->objectIdCacheKeyMap[$objectId];
$socket = $this->sockets[$cacheKey][$objectId];
if ($socket->idleWatcher) {
Loop::cancel($socket->idleWatcher);
}
unset($this->sockets[$cacheKey][$objectId], $this->objectIdCacheKeyMap[$objectId]);
if (empty($this->sockets[$cacheKey])) {
unset($this->sockets[$cacheKey]);
}
}
}

View File

@ -0,0 +1,97 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Socket;
use WP_Ultimo\Dependencies\Amp\CancellationToken;
use WP_Ultimo\Dependencies\Amp\CancelledException;
use WP_Ultimo\Dependencies\Amp\Loop;
use WP_Ultimo\Dependencies\Amp\Promise;
const LOOP_CONNECTOR_IDENTIFIER = Connector::class;
/**
* Listen for client connections on the specified server address.
*
* If you want to accept TLS connections, you have to use `yield $socket->setupTls()` after accepting new clients.
*
* @param string $uri URI in scheme://host:port format. TCP is assumed if no scheme is present.
* @param BindContext|null $context Context options for listening.
*
* @return Server
*
* @throws SocketException If binding to the specified URI failed.
* @throws \Error If an invalid scheme is given.
* @see Server::listen()
*
* @deprecated Use Server::listen() instead.
*/
function listen(string $uri, ?BindContext $context = null) : Server
{
return Server::listen($uri, $context);
}
/**
* Set or access the global socket Connector instance.
*
* @param Connector|null $connector
*
* @return Connector
*/
function connector(Connector $connector = null) : Connector
{
if ($connector === null) {
if ($connector = Loop::getState(LOOP_CONNECTOR_IDENTIFIER)) {
return $connector;
}
$connector = new DnsConnector();
}
Loop::setState(LOOP_CONNECTOR_IDENTIFIER, $connector);
return $connector;
}
/**
* Asynchronously establish a socket connection to the specified URI.
*
* @param string $uri URI in scheme://host:port format. TCP is assumed if no scheme is present.
* @param ConnectContext $context Socket connect context to use when connecting.
* @param CancellationToken|null $token
*
* @return Promise<EncryptableSocket>
*
* @throws ConnectException
* @throws CancelledException
*/
function connect(string $uri, ConnectContext $context = null, CancellationToken $token = null) : Promise
{
return connector()->connect($uri, $context, $token);
}
/**
* Returns a pair of connected stream socket resources.
*
* @return ResourceSocket[] Pair of socket resources.
*
* @throws SocketException If creating the sockets fails.
*/
function createPair() : array
{
try {
\set_error_handler(static function (int $errno, string $errstr) {
throw new SocketException(\sprintf('Failed to create socket pair. Errno: %d; %s', $errno, $errstr));
});
$sockets = \stream_socket_pair(\stripos(\PHP_OS, 'win') === 0 ? \STREAM_PF_INET : \STREAM_PF_UNIX, \STREAM_SOCK_STREAM, \STREAM_IPPROTO_IP);
if ($sockets === \false) {
throw new SocketException('Failed to create socket pair.');
}
} finally {
\restore_error_handler();
}
return [ResourceSocket::fromClientSocket($sockets[0]), ResourceSocket::fromClientSocket($sockets[1])];
}
/**
* @see https://wiki.openssl.org/index.php/Manual:OPENSSL_VERSION_NUMBER(3)
* @return bool
*/
function hasTlsAlpnSupport() : bool
{
return \defined('OPENSSL_VERSION_NUMBER') && \OPENSSL_VERSION_NUMBER >= 0x10002000;
}
function hasTlsSecurityLevelSupport() : bool
{
return \defined('OPENSSL_VERSION_NUMBER') && \OPENSSL_VERSION_NUMBER >= 0x10100000;
}