<?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);
        }
    }
}
