From e6e446a65a0bd5acc1c81430407289430d341f9b Mon Sep 17 00:00:00 2001 From: Tanner Mckenney Date: Tue, 9 Feb 2021 18:31:10 -0800 Subject: [PATCH] Working Gemini Support --- src/Request.php | 20 ++++++++ src/Response.php | 58 ++++++++++++++++++++++- src/Server.php | 121 +++++++++++++++++++++++++++++++++++++++++++++-- test/server.php | 18 +++++-- 4 files changed, 210 insertions(+), 7 deletions(-) diff --git a/src/Request.php b/src/Request.php index b2b023e..da01870 100644 --- a/src/Request.php +++ b/src/Request.php @@ -6,5 +6,25 @@ namespace TitanII; * Gemini Request */ class Request { + /** + * Maximum length in bytes of the URL request. + */ + const MAX_LENGTH = 1024; + + /** + * URL string. + * + * @var string + */ + private string $url; + public function __construct(string $url) + { + $this->url = substr($url, 0, self::MAX_LENGTH); + } + + public function __toString(): string + { + return $this->url; + } } diff --git a/src/Response.php b/src/Response.php index bc81b91..4e0d058 100644 --- a/src/Response.php +++ b/src/Response.php @@ -6,5 +6,61 @@ namespace TitanII; * Gemini Response */ class Response { - + /** + * Valid Response Codes + */ + const CODES = [10, 11, 20, 30, 31, 40, 41, 42, 43, 44, 50, 51, 52, 53, 59, 60, 61, 62]; + + /** + * Response Code + * + * @var int + */ + private int $code = 20; + + /** + * Meta Data + * + * @var string + */ + private string $meta = "application/octet-stream"; + + /** + * Body Content. + * + * @var string + */ + private string $content = ""; + + public function __construct() + { + } + + public function setCode(int $code): self + { + if (!in_array($code, self::CODES)) { + throw new \Exception("Not a valid Gemini response code!"); + } + + $this->code = $code; + + return $this; + } + + public function setMeta(string $meta): self + { + $this->meta = $meta; + return $this; + } + + public function setContent(string $content): self + { + $this->content = $content; + return $this; + } + + public function __toString(): string + { + return $this->code . ' ' . $this->meta . "\r\n" . $this->content; + } } diff --git a/src/Server.php b/src/Server.php index 1a96de9..17faaa8 100644 --- a/src/Server.php +++ b/src/Server.php @@ -11,10 +11,125 @@ use TitanII\Response; * @author Tanner Mckenney */ class Server { - public function start(callable $action): void + /** + * Context created by stream_context_create(); + * + * @var resource + */ + private $context = null; + + /** + * Socket created by stream_socket_server(); + * + * @var resource + */ + private $socket = null; + + /** + * Function that gets called to handle incoming requests. + * + * @var callable + */ + private $handler = null; + + /** + * Server active flag. + * + * @var bool + */ + private bool $active = false; + + /** + * Constructor + */ + public function __construct() { - $request = new Request(); + $this->context = stream_context_create(); + + stream_context_set_option($this->context, 'ssl', 'allow_self_signed', true); + stream_context_set_option($this->context, 'ssl', 'verify_peer', false); + } - $response = $action($request); + /** + * @param callable Handle incoming requests. + * + * Parameter 1 Callable is expected to handle the following: + * + * @param Request + * + * @return Response + */ + public function setHandler(callable $handler): self + { + $this->handler = $handler; + + return $this; + } + + public function setCert(string $file): self + { + stream_context_set_option($this->context, 'ssl', 'local_cert', $file); + + return $this; + } + + public function setCertPassphrase(string $passphrase): self + { + stream_context_set_option($this->context, 'ssl', 'passphrase', $passphrase); + + return $this; + } + + public function setKey(string $key): self + { + stream_context_set_option($this->context, 'ssl', 'local_pk', $key); + + return $this; + } + + protected function openSocket(string $ip, int $port): bool + { + $addr = "tcp://" . $ip . ':' . $port; + + $this->socket = stream_socket_server($addr, $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $this->context); + + if (empty($this->socket)) { + return false; + } + + stream_socket_enable_crypto($this->socket, false); + + return true; + } + + public function start(string $ip = '0', int $port = 1965): void + { + $this->openSocket($ip, $port); + + $this->active = true; + + while ($this->active) { + $incoming = stream_socket_accept($this->socket, -1, $peername); + + stream_set_blocking($incoming, true); + stream_socket_enable_crypto($incoming, true, STREAM_CRYPTO_METHOD_TLSv1_2_SERVER); + + $body = fread($incoming, 1024); + + stream_set_blocking($incoming, false); + + $request = new Request($body); + + $response = call_user_func($this->handler, $request); + + fwrite($incoming, $response); + + fclose($incoming); + } + } + + public function stop(): void + { + $this->active = false; } } diff --git a/test/server.php b/test/server.php index 4879b49..ebd8717 100644 --- a/test/server.php +++ b/test/server.php @@ -10,6 +10,18 @@ use TitanII\Response; $server = new Server(); -$server->start(function (Request $request): Response { - return new Response(); -}); \ No newline at end of file +$server->setCert(__DIR__ . DIRECTORY_SEPARATOR . 'certs' . DIRECTORY_SEPARATOR . 'cert.pem'); +$server->setKey(__DIR__ . DIRECTORY_SEPARATOR . 'certs' . DIRECTORY_SEPARATOR . 'key.rsa'); + +$server->setHandler(function (Request $request): Response { + $response = new Response(); + + $response->setMeta('text/plain'); + $response->setContent("Hello world!"); + + echo $request; + + return $response; +}); + +$server->start(); \ No newline at end of file