From fcac2b20abc91b518e8873d8f53facf741c5ab7b Mon Sep 17 00:00:00 2001 From: Jansen Price Date: Thu, 3 Sep 2020 15:59:02 -0500 Subject: [PATCH] Improvements to request validation --- src/Orbit/Request.php | 5 ++ src/Orbit/Server.php | 122 +++++++++++++++++++++++++++--------------- 2 files changed, 83 insertions(+), 44 deletions(-) diff --git a/src/Orbit/Request.php b/src/Orbit/Request.php index 653f186..b71921d 100644 --- a/src/Orbit/Request.php +++ b/src/Orbit/Request.php @@ -22,6 +22,11 @@ class Request foreach ($data as $key => $value) { $this->{$key} = urldecode($value); } + + // If scheme is missing, infer as default scheme + if (!$this->scheme) { + $this->scheme = Server::SCHEME; + } } public function getUrlAppendPath($text) diff --git a/src/Orbit/Server.php b/src/Orbit/Server.php index d166b1d..fb5ff88 100644 --- a/src/Orbit/Server.php +++ b/src/Orbit/Server.php @@ -8,6 +8,8 @@ use ForceUTF8\Encoding; class Server { + const SCHEME = "gemini"; + public static $version = "0.3"; public $config; @@ -71,7 +73,6 @@ class Server $this->logger->debug("Root directory '$path'"); - var_dump($this->getListenAddress()); $server = stream_socket_server( $this->getListenAddress(), $errno, $errstr, @@ -83,12 +84,11 @@ class Server throw new \Exception("Error " . $errno . ": " . $errstr); } - $protocol = "gemini"; $name = stream_socket_get_name($server, false); - $this->logger->info("Listening on $protocol://$name ..."); + $this->logger->info(sprintf("Listening on %s://%s...", self::SCHEME, $name)); while (true) { - # This is to swallow up the `timeout` warning + # onWarning is added here to swallow up the `timeout` warning set_error_handler([$this, 'onWarning']); $client = stream_socket_accept($server, $this->timeout, $client_name); //stream_socket_enable_crypto($server, true, STREAM_CRYPTO_METHOD_TLSv1_2_SERVER); @@ -96,25 +96,24 @@ class Server if ($client) { $time = ['start' => microtime(true)]; + $meta = stream_get_meta_data($client); - $this->logger->debug("$client_name Accepted"); + $this->logger->debug("$client_name Accepted", $meta); $request_buffer = stream_get_line($client, 1026, "\r\n"); - print($this->hexView($request_buffer)); - print("Length: " . mb_strlen($request_buffer) . "\n"); + //print($this->hexView($request_buffer)); + //print("Length: " . mb_strlen($request_buffer) . "\n"); $this->logger->info("REQ: $request_buffer", ["client" => $client_name]); - if (trim($request_buffer)) { - $request = new Request($request_buffer); + $request = new Request($request_buffer); - // Respond to client - $response = $this->handleResponse($request, $path); - $size = $response->send($client); - $time['end'] = microtime(true); - $this->logger->debug( - "RSP: " . trim($response->getHeader()), - ['size' => $size, 'time' => $time['end'] - $time['start']] - ); - } + // Respond to client + $response = $this->handleResponse($request, $path); + $size = $response->send($client); + $time['end'] = microtime(true); + $this->logger->debug( + "RSP: " . trim($response->getHeader()), + ['size' => $size, 'time' => $time['end'] - $time['start']] + ); fclose($client); $this->logger->debug("$client_name Closed"); @@ -124,38 +123,23 @@ class Server public function handleResponse($request, $dir) { - $response = new Response(); + list($is_valid, $response) = $this->validateRequest($request); - // Valid URL must not be more than 1024 chars - if (mb_strlen($request->url) > 1024) { - $response->setStatus(Response::STATUS_BAD_REQUEST); - $response->setMeta("Bad request - too long"); - return $response; - } - - if ($request->host != "127.0.0.1" && $request->host != $this->config->hostname) { - $response->setStatus(Response::STATUS_PROXY_REQUEST_REFUSED); - $response->setMeta("Proxy error - invalid host"); - return $response; - } - - // Valid URL must use correct port - if ($request->port != "" && $request->port != $this->config->port) { - $response->setStatus(Response::STATUS_PROXY_REQUEST_REFUSED); - $response->setMeta("Proxy error - invalid port"); - return $response; - } - - // Valid URL must not contain non-UTF-8 bytes - $conv = Encoding::fixUTF8($request->url); - if ($conv != $request->url) { - $response->setStatus(Response::STATUS_BAD_REQUEST); - $response->setMeta("Bad request - non-UTF8"); + if ($is_valid === false) { return $response; } $resource_path = rtrim($dir, "/") . $request->path; + // Check if within the server root + // Realpath will translate any '..' in the path + $realpath = realpath($resource_path); + if ($realpath && strpos($realpath, $dir) !== 0) { + $response->setStatus(Response::STATUS_PERMANENT_FAILURE); + $response->setMeta("Invalid location"); + return $response; + } + if (is_dir($resource_path)) { // If missing the final slash, issue a redirect if ($resource_path[-1] != "/") { @@ -209,6 +193,56 @@ class Server return $response; } + public function validateRequest($request) + { + $response = new Response(); + + // Valid URL must contain a host + if (!$request->host) { + $response->setStatus(Response::STATUS_BAD_REQUEST); + $response->setMeta("Bad request - url is empty"); + return [false, $response]; + } + + // Valid URL must be the target scheme + if ($request->scheme != self::SCHEME) { + $response->setStatus(Response::STATUS_PROXY_REQUEST_REFUSED); + $response->setMeta("Proxy error - unsupported scheme"); + return [false, $response]; + } + + // Valid URL must use targeted hostname + if ($request->host != "127.0.0.1" && $request->host != "localhost" && $request->host != $this->config->hostname) { + $response->setStatus(Response::STATUS_PROXY_REQUEST_REFUSED); + $response->setMeta("Proxy error - invalid host"); + return [false, $response]; + } + + // Valid URL must use correct port + if ($request->port != "" && $request->port != $this->config->port) { + $response->setStatus(Response::STATUS_PROXY_REQUEST_REFUSED); + $response->setMeta("Proxy error - invalid port"); + return [false, $response]; + } + + // Valid URL must not be more than 1024 chars + if (mb_strlen($request->url) > 1024) { + $response->setStatus(Response::STATUS_BAD_REQUEST); + $response->setMeta("Bad request - too long"); + return [false, $response]; + } + + // Valid URL must not contain non-UTF-8 bytes + $conv = Encoding::fixUTF8($request->url); + if ($conv != $request->url) { + $response->setStatus(Response::STATUS_BAD_REQUEST); + $response->setMeta("Bad request - non-UTF8"); + return [false, $response]; + } + + return [true, $response]; + } + public function log($level, $message, $context = []) { $this->getLogger()->log($level, $message, $context);