factor gemini file retreival logic out of gemini modlule

this also fixes a critical bug causing false 'bad request' responses
This commit is contained in:
Eric S. Londres 2022-08-20 10:51:19 -04:00
parent 1411eef0a4
commit 45bcbec0d8
Signed by: slondr
GPG Key ID: A2D25B4D5CB970E4
2 changed files with 48 additions and 17 deletions

38
lib/file_retrieval.ex Normal file
View File

@ -0,0 +1,38 @@
defmodule Egalaxyd.FileRetrieval do
@moduledoc "Logic for getting file contents from disk"
def retrieve_file_contents(path) do
case normalized_path(path) do
{:error, error} -> {:error, error}
{:ok, canonical_path} ->
case File.read(canonical_path) do
{:ok, file_contents} ->
{:ok, file_contents, MIME.from_path(canonical_path)}
_ -> {:error, :unknown_error}
end
end
end
@doc """
Given a filesystem path, converts to canonical response path form by rewriting directories.
If the file cannot be found or stat'd, returns {:error, error}; otherwise returns {:ok, canonical_path}
"""
defp normalized_path(path) do
# Make sure the path is an absolute path to a file in the current working directory
# TODO: Adjust file base path via config option
prefixed_path = if String.starts_with?(path, File.cwd!), do: path, else: (File.cwd! <> path)
case File.stat(prefixed_path) do
{:error, error} -> {:error, error}
{:ok, %File.Stat{type: :directory}} ->
# This is a directory so first try to rewrite with a trailing slash, then to index.gmi
# TODO: Directory listening on fallback?
suffix = (if String.last(prefixed_path) != "/", do: "/", else: "") <> "index.gmi"
normalized_path(prefixed_path <> suffix)
{:ok, _} ->
# This is an existing file
{:ok, prefixed_path}
end
end
end

View File

@ -41,7 +41,7 @@ defmodule Geminex.RequestParser do
trimmed = String.trim(uri)
if String.length(trimmed) > 1024 do
response "59 bad request\r\n"
response "59 bad request: Request too long\r\n"
end
case String.trim(uri) |> URI.new() do
@ -56,9 +56,11 @@ defmodule Geminex.RequestParser do
response("53 protocol refused\r\n") # this is a gemini server!
request_references_forbidden_secrets?(req.path) ->
# Settings disallow requesting secrets and the path seems to reference them
Logger.notice("Client requested forbidden secret")
response("59 bad request\r\n")
request_attempts_to_escape?(req.path) ->
# Settings disallow directory backtracking and the request path seems to attempt this
Logger.notice("Client requested directory backtrack")
response("59 bad request\r\n")
true ->
# todo: mime typing
@ -101,26 +103,17 @@ defmodule Geminex.RequestParser do
@doc "Check request to ensure directory traversal is not attempted, if the setting is enabled"
defp request_attempts_to_escape?(path) do
(not ConfigSentinel.getopt(:allow_updir)) && String.match?(path, ~r/../)
(not ConfigSentinel.getopt(:allow_updir)) && String.match?(path, ~r/\.\./)
end
defp retrieve_file_contents(path) do
# first, check if the file exists and/or is a directory
case File.stat(File.cwd! <> path) do
case Egalaxyd.FileRetrieval.retrieve_file_contents(path) do
{:error, :enoent} -> {:error, "51 not found\r\n"}
{:error, :enametoolong} -> {:error, "59 bad request\r\n"}
{:error, _} -> {:error, "50 internal server error\r\n"}
{:ok, %File.Stat{type: :directory}} ->
# retry with index.gmi
# TODO: handle non-unix file paths
# TODO: Directory listing on fallback?
retrieve_file_contents(path <> (if String.last(path) != "/", do: "/", else: "") <> "index.gmi")
# TODO: check file permissions
{:ok, _} ->
case File.read(File.cwd! <> path) do
{:ok, content} -> {:ok, content, MIME.from_path(File.cwd! <> path)}
_ -> {:error, "50 internal server error\r\n"}
end
{:error, :enametoolong} -> {:error, "59 bad request: Request too long\r\n"}
{:error, other_error} ->
Logger.error other_error
{:error, "50 internal server error\r\n"}
{:ok, file_contents, mime_type} -> {:ok, file_contents, mime_type}
end
end
end