Initial Commit
This commit is contained in:
45
dependencies/amphp/socket/psalm.xml
vendored
Normal file
45
dependencies/amphp/socket/psalm.xml
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
<?xml version="1.0"?>
|
||||
<psalm
|
||||
totallyTyped="false"
|
||||
errorLevel="2"
|
||||
phpVersion="7.1"
|
||||
resolveFromConfigFile="true"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="https://getpsalm.org/schema/config"
|
||||
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
|
||||
>
|
||||
<projectFiles>
|
||||
<directory name="examples"/>
|
||||
<directory name="src"/>
|
||||
<ignoreFiles>
|
||||
<directory name="vendor"/>
|
||||
</ignoreFiles>
|
||||
</projectFiles>
|
||||
|
||||
<issueHandlers>
|
||||
<StringIncrement>
|
||||
<errorLevel type="suppress">
|
||||
<directory name="examples"/>
|
||||
<directory name="src"/>
|
||||
</errorLevel>
|
||||
</StringIncrement>
|
||||
|
||||
<RedundantConditionGivenDocblockType>
|
||||
<errorLevel type="suppress">
|
||||
<directory name="src"/>
|
||||
</errorLevel>
|
||||
</RedundantConditionGivenDocblockType>
|
||||
|
||||
<DocblockTypeContradiction>
|
||||
<errorLevel type="suppress">
|
||||
<directory name="src"/>
|
||||
</errorLevel>
|
||||
</DocblockTypeContradiction>
|
||||
|
||||
<MissingClosureParamType>
|
||||
<errorLevel type="suppress">
|
||||
<directory name="src"/>
|
||||
</errorLevel>
|
||||
</MissingClosureParamType>
|
||||
</issueHandlers>
|
||||
</psalm>
|
136
dependencies/amphp/socket/src/BindContext.php
vendored
Normal file
136
dependencies/amphp/socket/src/BindContext.php
vendored
Normal 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;
|
||||
}
|
||||
}
|
38
dependencies/amphp/socket/src/Certificate.php
vendored
Normal file
38
dependencies/amphp/socket/src/Certificate.php
vendored
Normal 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;
|
||||
}
|
||||
}
|
388
dependencies/amphp/socket/src/ClientTlsContext.php
vendored
Normal file
388
dependencies/amphp/socket/src/ClientTlsContext.php
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
121
dependencies/amphp/socket/src/ConnectContext.php
vendored
Normal file
121
dependencies/amphp/socket/src/ConnectContext.php
vendored
Normal 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;
|
||||
}
|
||||
}
|
10
dependencies/amphp/socket/src/ConnectException.php
vendored
Normal file
10
dependencies/amphp/socket/src/ConnectException.php
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Socket;
|
||||
|
||||
/**
|
||||
* Thrown if connecting fails.
|
||||
*/
|
||||
class ConnectException extends SocketException
|
||||
{
|
||||
}
|
23
dependencies/amphp/socket/src/Connector.php
vendored
Normal file
23
dependencies/amphp/socket/src/Connector.php
vendored
Normal 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;
|
||||
}
|
213
dependencies/amphp/socket/src/DatagramSocket.php
vendored
Normal file
213
dependencies/amphp/socket/src/DatagramSocket.php
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
108
dependencies/amphp/socket/src/DnsConnector.php
vendored
Normal file
108
dependencies/amphp/socket/src/DnsConnector.php
vendored
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
37
dependencies/amphp/socket/src/EncryptableSocket.php
vendored
Normal file
37
dependencies/amphp/socket/src/EncryptableSocket.php
vendored
Normal 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;
|
||||
}
|
213
dependencies/amphp/socket/src/Internal/functions.php
vendored
Normal file
213
dependencies/amphp/socket/src/Internal/functions.php
vendored
Normal 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;
|
||||
}
|
14
dependencies/amphp/socket/src/PendingAcceptError.php
vendored
Normal file
14
dependencies/amphp/socket/src/PendingAcceptError.php
vendored
Normal 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);
|
||||
}
|
||||
}
|
14
dependencies/amphp/socket/src/PendingReceiveError.php
vendored
Normal file
14
dependencies/amphp/socket/src/PendingReceiveError.php
vendored
Normal 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);
|
||||
}
|
||||
}
|
190
dependencies/amphp/socket/src/ResourceSocket.php
vendored
Normal file
190
dependencies/amphp/socket/src/ResourceSocket.php
vendored
Normal 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
179
dependencies/amphp/socket/src/Server.php
vendored
Normal 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;
|
||||
}
|
||||
}
|
394
dependencies/amphp/socket/src/ServerTlsContext.php
vendored
Normal file
394
dependencies/amphp/socket/src/ServerTlsContext.php
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
39
dependencies/amphp/socket/src/Socket.php
vendored
Normal file
39
dependencies/amphp/socket/src/Socket.php
vendored
Normal 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;
|
||||
}
|
108
dependencies/amphp/socket/src/SocketAddress.php
vendored
Normal file
108
dependencies/amphp/socket/src/SocketAddress.php
vendored
Normal 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();
|
||||
}
|
||||
}
|
8
dependencies/amphp/socket/src/SocketException.php
vendored
Normal file
8
dependencies/amphp/socket/src/SocketException.php
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Socket;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\ByteStream\StreamException;
|
||||
class SocketException extends StreamException
|
||||
{
|
||||
}
|
50
dependencies/amphp/socket/src/SocketPool.php
vendored
Normal file
50
dependencies/amphp/socket/src/SocketPool.php
vendored
Normal 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;
|
||||
}
|
23
dependencies/amphp/socket/src/StaticConnector.php
vendored
Normal file
23
dependencies/amphp/socket/src/StaticConnector.php
vendored
Normal 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);
|
||||
}
|
||||
}
|
10
dependencies/amphp/socket/src/TlsException.php
vendored
Normal file
10
dependencies/amphp/socket/src/TlsException.php
vendored
Normal 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
|
||||
{
|
||||
}
|
106
dependencies/amphp/socket/src/TlsInfo.php
vendored
Normal file
106
dependencies/amphp/socket/src/TlsInfo.php
vendored
Normal 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;
|
||||
}
|
||||
}
|
203
dependencies/amphp/socket/src/UnlimitedSocketPool.php
vendored
Normal file
203
dependencies/amphp/socket/src/UnlimitedSocketPool.php
vendored
Normal 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]);
|
||||
}
|
||||
}
|
||||
}
|
97
dependencies/amphp/socket/src/functions.php
vendored
Normal file
97
dependencies/amphp/socket/src/functions.php
vendored
Normal 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;
|
||||
}
|
Reference in New Issue
Block a user