Created
August 15, 2021 19:20
-
-
Save Zegnat/4f1b2e024777e5bfeb5ad18ee41b35eb to your computer and use it in GitHub Desktop.
Add gemini:// support for file_get_contents(). Will turn into proper repo with Composer support later.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php declare(strict_types=1); | |
namespace Zegnat\Gemini; | |
/** | |
* @see https://www.php.net/manual/en/class.streamwrapper.php | |
*/ | |
final class GeminiStreamWrapper { | |
/** @var resource $context if a stream context was provided it will be set here, even before __construct */ | |
public $context; | |
/** @var resource $connection if a stream was successfully opened, this holds the pointer to the TLS stream */ | |
private $connection; | |
private function __construct() | |
{ | |
} | |
/** | |
* First thing to implement! Create connection. | |
* | |
* The STREAM_REPORT_ERRORS option is ignored, if something goes wrong trigger_error is always called. | |
* The STREAM_USE_PATH option is also ignored, gemini:// paths will always be absolute to trigger this wrapper. | |
* Because STREAM_USE_PATH is ignored, $opened_path is never assigned. | |
*/ | |
public function stream_open(string $path, string $mode, int $options, string &$opened_path = null): bool { | |
// Made to match the HTTP wrapper (which for some reason allows c mode for reading) | |
if (($mode[0] !== 'r' && $mode[0] !== 'c') || \strchr($mode, '+') !== false) { | |
\trigger_error('fopen(' . $path . '): Failed to open stream: Gemini wrapper does not support writeable connections', \E_USER_WARNING); | |
return false; | |
} | |
$host = \parse_url($path, \PHP_URL_HOST); | |
$port = \parse_url($path, \PHP_URL_PORT) ?: 1965; | |
if (!is_string($host) || !is_int($port)) { | |
\trigger_error('fopen(' . $path . '): Failed to open stream: Provided path lacks a usable host/port', \E_USER_WARNING); | |
return false; | |
} | |
if (mb_strlen($path, '8bit') > 1024) { | |
\trigger_error('fopen(' . $path . '): Failed to open stream: Provided path cannot be bigger than 1024 bytes', \E_USER_WARNING); | |
return false; | |
} | |
// The TLS connection settings. | |
$context = \stream_context_create(['ssl' => [ | |
'allow_self_signed' => true, | |
]]); | |
// C: Opens connection | |
// C: Validates server certificate | |
$this->connection = \stream_socket_client('tls://' . $host . ':' . $port, context: $context); | |
if ($this->connection === false) { | |
\trigger_error('fopen(' . $path . '): Failed to open stream: Socket could not be opened', \E_USER_WARNING); | |
return false; | |
} | |
// C: Sends request (one CRLF terminated line) | |
$writeSuccess = \fwrite($this->connection, $path . "\r\n"); | |
if (false === $writeSuccess) { | |
$this->stream_close(); | |
\trigger_error('fopen(' . $path . '): Failed to open stream: Request could not be send to the server', \E_USER_WARNING); | |
return false; | |
} | |
// Receive response header (one CRLF terminated line) | |
$header = \fgets($this->connection, 2 + 1 + 1024 + 2); | |
// If a server sends a <STATUS> which is not a two-digit number or a <META> which exceeds 1024 bytes in length, the client SHOULD close the connection and disregard the response header, informing the user of an error. | |
if (false === $header || \preg_match("/^\d\d\x20.{0,1024}\r\n$/", $header) !== 1) { | |
$this->stream_close(); | |
\trigger_error('fopen(' . $path . '): Server returned an invalid response header', \E_USER_WARNING); | |
return false; | |
} | |
return true; | |
} | |
public function stream_read(int $count): string { | |
return \fread($this->connection, $count); | |
} | |
public function stream_eof(): bool { | |
return \feof($this->connection); | |
} | |
public function stream_close(): void { | |
\fclose($this->connection); | |
} | |
/** | |
* fstat is not supported for remote files, as per standard PHP. | |
* @see https://www.php.net/manual/en/function.fstat.php | |
*/ | |
public function stream_stat(): bool { | |
return false; | |
} | |
} | |
if (!stream_wrapper_register('gemini', GeminiStreamWrapper::class, \STREAM_IS_URL)) { | |
throw new \RuntimeException('Could not register the Gemini streamWrapper.'); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment