null, 'key' => null, 'passphrase' => null, ]; protected array $options = []; protected array $requestHandlers = []; /** * @param array $certificate * @param string $hostname * @param array $options */ public function __construct( array $certificate, string $hostname = 'localhost', array $options = [] ) { $this->certificate = $certificate; $this->hostname = $hostname; $this->options = array_replace_recursive([ 'context' => [], 'client_certificate_support_workaround' => false, # "support" until https://bugs.php.net/bug.php?id=80770 'client_certificate_support_workaround_timeout' => 3, ], $options); } public function setCertificate(string $certificateFile, string $keyFile, string $passphrase = ''): static { $this->certificate = [ 'file' => $certificateFile, 'key' => $keyFile, 'passphrase' => $passphrase, ]; return $this; } public function onRequest(RequestHandlerInterface|callable $callable): static { $this->requestHandlers[] = $callable; return $this; } public function listen(int $port = 1965): void { $contextOptions = array_replace_recursive([ 'ssl' => [ 'local_cert' => $this->certificate['file'], 'local_pk' => $this->certificate['key'], 'passphrase' => $this->certificate['passphrase'], 'allow_self_signed' => true, 'verify_peer' => false, 'capture_peer_cert' => true, ], ], $this->options['context']); $context = stream_context_create(options: $contextOptions); $socket = stream_socket_server( address: "tls://{$this->hostname}:{$port}", context: $context ); $connections = []; while (true) { if ($this->options['client_certificate_support_workaround']) { stream_context_set_option($socket, 'ssl', 'verify_peer', true); } echo "Listening for connections.\n"; $connection = stream_socket_accept( socket: $socket, timeout: empty($connections) ? -1 : 0, peer_name: $peer ); if ($connection) { $connections[$peer] = $connection; } else if ($this->options['client_certificate_support_workaround']) { echo "Enabling client certificate \"support\" workaround.\n"; echo "Listening without verifying peer.\n"; stream_context_set_option($socket, 'ssl', 'verify_peer', false); $connection = stream_socket_accept( socket: $socket, timeout: $this->options['client_certificate_support_workaround_timeout'], peer_name: $peer ); if ($connection) { $connections[$peer] = $connection; } } if (count($connections) == 0) { continue; } $streams = stream_select( read: $connections, write: $write, except: $except, seconds: 5 ); if ($streams) { foreach ($connections as $peer => $connection) { if (feof($connection)) { fclose($connection); unset($connections[$peer]); continue; } $request = Request::fromResource($connection); $response = new Response(); foreach ($this->requestHandlers as $requestHandler) { $response = $requestHandler($response, $request); } $response->send($connection); fclose($connection); unset($connections[$peer]); } } } } }