config->root_dir); $resource_path = rtrim($real_root_dir, "/") . $request->path; // Check if within the server root // getAbsolutePath will translate any '..' in the path $realpath = self::getAbsolutePath($resource_path); if ($realpath && strpos($realpath, $real_root_dir) !== 0) { $response->setStatus(Response::STATUS_PERMANENT_FAILURE); $response->setMeta("Invalid location"); return [false, $response]; } if (is_dir($resource_path)) { // If missing the final slash, issue a redirect if ($resource_path[-1] != "/") { $response->setStatus(Response::STATUS_REDIRECT_PERMANENT); $response->setMeta($request->getUrlAppendPath('/')); return [false, $response]; } // Check if index file exists if (file_exists($resource_path . DIRECTORY_SEPARATOR . $this->config->index_file)) { $resource_path = $resource_path . DIRECTORY_SEPARATOR . $this->config->index_file; } else { if (!$this->config->enable_directory_index) { $response->setStatus(Response::STATUS_NOT_FOUND); $response->setMeta('Path not available'); return [false, $response]; } else { $response->setStatus(Response::STATUS_SUCCESS); $response->setMeta('text/gemini'); $response->setBody($this->makeDirectoryIndex($resource_path, $real_root_dir)); return [true, $response]; } } } // File exists and is world readable if (file_exists($resource_path) && self::isWorldReadable($resource_path)) { $response->setStatus(Response::STATUS_SUCCESS); $pathinfo = pathinfo($resource_path); if (isset($pathinfo['extension'])) { $meta = $this->getCustomMimeFromFileExtension($pathinfo['extension']); if (!$meta) { $meta = mime_content_type($resource_path); } } else { // Use finfo_file to detect type $meta = finfo_file(finfo_open(FILEINFO_MIME), $resource_path); } $response->setMeta($meta); $response->setStaticFile($resource_path); } else { $response->setStatus(Response::STATUS_NOT_FOUND); $response->setMeta('Not found!'); } return [true, $response]; } /** * Get mime type from file extension for custom types * * @param string $extension * @return string */ public function getCustomMimeFromFileExtension($extension): string { switch ($extension) { case 'gmi': case 'gemini': return 'text/gemini'; break; case 'md': case 'markdown': return 'text/gemini'; break; case 'ans': case 'ansi': return 'text/x-ansi'; break; default: return ''; } } /** * Make a directory index suitable as response content * * @param string $path Current path * @param string $root Root path on disk of the server * @return string */ public function makeDirectoryIndex($path, $root): string { $files = glob($path . "*"); $body = "# Directory listing " . str_replace($root, '', $path) . "/\n\n"; if ($path != $root . "/") { // If not already at root, provide option to go up one parent $body .= "=> " . str_replace($root, '', dirname($path)) . "/ ..\n"; } foreach ($files as $file) { $relative_path = str_replace($path, '', $file); $is_dir = false; if (is_dir($file)) { $is_dir = true; $size = ''; } else { $size = filesize($file); } $body .= sprintf( "=> %s%s %s%s%s\n", urlencode($relative_path), ($is_dir ? '/' : ''), $relative_path, ($is_dir ? '/' : ''), ($size ? " ($size)" : '') ); } return $body; } /** * Report whether a given file is world readable or not * * @param string $file The file to check * @return bool */ public static function isWorldReadable(string $file): bool { return (bool)(fileperms($file) & self::WORLD_READABLE); } /** * Get an absolute path for a filename * * Translates .. and . to the real locations. The reason I am not using * realpath() to do it is it resolves symlinks * * @param string $path * @return string */ public static function getAbsolutePath($path): string { $path = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $path); $parts = array_filter(explode(DIRECTORY_SEPARATOR, $path), 'strlen'); $absolutes = []; foreach ($parts as $part) { if ('.' == $part) { continue; } if ('..' == $part) { array_pop($absolutes); } else { $absolutes[] = $part; } } return "/" . implode(DIRECTORY_SEPARATOR, $absolutes); } }