balls/lib/balls_pds/router.ex

421 lines
12 KiB
Elixir
Raw Normal View History

2024-12-02 11:03:06 +00:00
defmodule BallsPDS.Router do
require Logger
use Plug.Router
alias BallsPDS.Ecto.Schema.Object
alias BallsPDS.Ecto.Schema.ObjectReadAgent
alias BallsPDS.Util.ACL
alias BallsPDS.Plug.WACAuthenticationPlug
alias BallsPDS.Plug.ObjectPlug
alias BallsPDS.Plug.ObjectAuthorizationPlug
alias BallsPDS.Repo
alias BallsPDS.Ecto.Schema.CollectionObject
import BallsPDS.Plug.ObjectPlug, only: [is_acl_get?: 1]
plug(:match)
plug(:dispatch)
plug(Plug.Parsers,
parsers: [:multipart],
pass: ["*/*"],
length: 100_000_000
)
plug(WACAuthenticationPlug)
plug(ObjectPlug)
plug(ObjectAuthorizationPlug)
match _, via: :get do
if is_acl_get?(conn) do
handle_acl_get(conn)
else
handle_authorized_get(conn)
end
end
match _, via: :post do
handle_authorized_post(conn)
end
match _ do
send_resp(conn, 405, "Method not allowed")
end
def handle_acl_get(conn) do
object_path = String.replace_suffix(conn.request_path, ".acl.jsonld", "")
case Object.get_by_path(object_path) do
nil ->
send_resp(conn, 404, "Not found")
object ->
authorized_readers = ObjectReadAgent.get_read_agents(object)
document = ACL.render_acl_document(object_path, authorized_readers)
conn
|> put_resp_content_type("application/ld+json")
|> send_resp(200, Jason.encode!(document))
end
end
def handle_authorized_get(conn) do
object =
%{
:content_type => content_type,
:storage_key => storage_key,
:activitypub_type => type
} = conn.assigns[:object]
case File.read("./priv/data/#{storage_key}") do
{:ok, file_contents} ->
is_collection? = type in ["Collection", "OrderedCollection"]
file_contents =
if is_collection? do
# Inject back in the collection items.
collection = Jason.decode!(file_contents)
# TODO: not sure how to handle every weird way a collection might be represented not just list of ids.
ids =
CollectionObject.get_ids(object)
|> Enum.map(fn {_, ap_id} ->
ap_id
end)
items_name =
if type == "OrderedCollection" do
"orderedItems"
else
"items"
end
Map.put(collection, items_name, ids) |> Map.put("totalItems", length(ids))
Jason.encode(collection)
else
file_contents
end
conn
|> put_resp_content_type(content_type)
|> send_resp(200, file_contents)
{:error, _error} ->
send_resp(conn, 500, "Unknown error")
end
end
defp extract_collection_info(%{"type" => "OrderedCollection", "orderedItems" => items})
when is_list(items) do
ids =
Enum.reduce(items, [], fn item, acc ->
case dereference_id(item) do
nil -> acc
id -> [id | acc]
end
end)
|> Enum.reverse()
{"OrderedCollection", ids}
end
defp extract_collection_info(%{"type" => "Collection", "items" => items}) when is_list(items) do
ids =
Enum.reduce(items, [], fn item, acc ->
case dereference_id(item) do
nil -> acc
id -> [id | acc]
end
end)
|> Enum.reverse()
{"Collection", ids}
end
defp extract_collection_info(_), do: {nil, nil}
defp dereference_id(object) when is_binary(object), do: object
defp dereference_id(%{"id" => id}) when is_binary(id), do: id
defp dereference_id(_), do: nil
def handle_authorized_post(%Plug.Conn{:halted => false, :method => "POST"} = conn) do
conn
|> maybe_set_public()
|> maybe_set_content_type()
|> validate_read_acls()
|> maybe_write_object()
|> maybe_save_db()
|> maybe_set_read_acls()
|> maybe_save_collection_items()
|> send_resp(200, "OK")
|> halt()
end
defp maybe_set_public(%Plug.Conn{:halted => false} = conn) do
with {:object, object} when not is_nil(object) <- {:object, conn.assigns[:object]},
object_params <- conn.assigns[:object_params] || %{},
{:public, %{"public" => public?}} when public? in ["true", "false"] <-
{:public, conn.body_params},
public? <- public? == "true" do
Map.put(object_params, :public, public?)
Plug.Conn.assign(conn, :object_params, object_params)
else
{:object, nil} -> conn
{:public, _} -> send_resp(conn, 500, "Set public error") |> halt()
end
end
defp maybe_set_public(%Plug.Conn{} = conn), do: conn
defp maybe_set_content_type(%Plug.Conn{:halted => false} = conn) do
with {:object, object} when not is_nil(object) <- {:object, conn.assigns[:object]},
object_params <- conn.assigns[:object_params] || %{} do
object_params =
case conn.body_params do
%{"file" => %{"content_type" => content_type}} ->
Map.put(object_params, :content_type, content_type)
%{"content_type" => content_type} when is_binary(content_type) ->
Map.put(object_params, :content_type, content_type)
_ ->
object_params
end
Plug.Conn.assign(conn, :object_params, object_params)
else
{:object, nil} -> conn
end
end
defp maybe_set_content_type(%Plug.Conn{} = conn), do: conn
defp validate_read_acls(%Plug.Conn{:halted => false} = conn) do
read =
case Map.get(conn.body_params, "read") do
val when is_binary(val) -> [val]
val when is_list(val) -> val
val -> val
end
case read do
nil ->
conn
read when is_list(read) ->
all_valid? =
Enum.reduce_while(read, true, fn
acl, _ ->
if ACL.is_valid_acl?(acl) do
{:cont, true}
else
{:halt, false}
end
end)
if all_valid? do
conn
else
send_resp(conn, 500, "Validate read error: invalid acl") |> halt()
end
_ ->
send_resp(conn, 500, "Validate read error") |> halt()
end
end
defp validate_read_acls(%Plug.Conn{} = conn), do: conn
# This can't be called until object is already inserted. Assume already validated.
defp maybe_set_read_acls(%Plug.Conn{:halted => false} = conn) do
read =
case Map.get(conn.body_params, "read") do
val when is_binary(val) -> [val]
val when is_list(val) -> val
end
with {:object, %Object{} = object} <- {:object, conn.assigns[:object]},
{:reset_read, _} <- {:reset_read, ObjectReadAgent.revoke_all_read(object)},
{:update_read, :ok} <- {:update_read, ObjectReadAgent.authorize_read(object, read)} do
conn
else
{:object, nil} ->
conn
{:update_read, error} ->
Logger.error("Failed to authorized read: #{inspect(error)}")
send_resp(conn, 500, "Set read error") |> halt()
end
end
defp maybe_set_read_acls(%Plug.Conn{} = conn), do: conn
defp maybe_write_object(
%Plug.Conn{
:halted => false,
:request_path => request_path,
:body_params => %{
"file" => %{"path" => uploaded_file_path, "content_type" => content_type}
}
} = conn
) do
with {:read, {:ok, body}} <- {:read, File.read(uploaded_file_path)},
{:parse,
{:ok,
%{:body => body, :activitypub_type => activitypub_type, :ids => ap_collection_ids}}} <-
{:parse, maybe_parse_ap_object(body, content_type)},
storage_key <- request_path |> Base.encode16(case: :lower),
filename <- "./priv/data/#{storage_key}",
{:write, :ok} <- {:write, File.write(filename, body)},
object_params <- conn.assigns[:object_params],
object_params <- Map.put(object_params, :storage_key, storage_key),
conn <- Plug.Conn.assign(conn, :object_params, object_params) do
conn =
if activitypub_type == nil do
conn
else
object_params = Map.put(object_params, :activitypub_type, activitypub_type)
Plug.Conn.assign(conn, :object_params, object_params)
end
conn =
if is_list(ap_collection_ids) and length(ap_collection_ids) > 0 do
conn |> assign(:ids, ap_collection_ids)
else
conn
end
conn
else
{:read, error} ->
Logger.error("Failed to read uploaded file: #{inspect(error)}")
send_resp(conn, 500, "Error") |> halt()
{:write, error} ->
Logger.error("Failed to write uploaded file to location: #{inspect(error)}")
send_resp(conn, 500, "Error") |> halt()
end
end
defp maybe_write_object(%Plug.Conn{} = conn), do: conn
defp maybe_parse_ap_object(body, content_type)
when is_binary(body) and is_binary(content_type) do
if String.starts_with?(content_type, "application/json") do
with {:parse,
{:ok, %{"@context" => _, "type" => activitypub_type, "id" => activitypub_id} = parsed}}
when is_binary(activitypub_id) and is_binary(activitypub_type) <-
{:parse, Jason.decode(body)},
{:collection, {_, ids}} <- {:collection, extract_collection_info(parsed)} do
parsed =
if activitypub_type in ["Collection", "OrderedCollection"] do
parsed |> Map.drop(["items", "orderedItems"])
else
parsed
end
{:ok,
%{
body: Jason.encode(parsed),
activitypub_type: activitypub_type,
ids: ids
}}
else
{:parse, {:ok, _parsed}} ->
{:ok,
%{
body: body,
activitypub_type: nil,
ids: nil
}}
{:parse, {:error, error}} ->
# Should I really save broken JSON?
{:error, error}
end
else
{:ok,
%{
body: body,
activitypub_type: nil,
ids: nil
}}
end
end
defp maybe_save_db(%Plug.Conn{:halted => false} = conn) do
with {:object, %Object{} = object} <- {:object, conn.assigns[:object]},
{:params, %{} = object_params} when map_size(object_params) > 0 <-
{:params, conn.assigns[:object_params]},
{:changeset, {:ok, changeset}} <- {:changeset, Object.changeset(object, object_params)},
{:upsert, {:ok, object}} <-
{:upsert, Repo.insert(changeset, on_conflict: :replace_all)} do
conn |> assign(:object, object) |> assign(:object_params, %{})
else
{:object, nil} ->
Logger.error("This should not happen, object was nil in save to db.")
send_resp(conn, 500, "Error") |> halt()
{:params, _} ->
conn
{:changeset, {:error, error}} ->
Logger.error("Changeset on save to db failed for some reason: #{inspect(error)}")
send_resp(conn, 500, "Error") |> halt()
{:upsert, {:error, error}} ->
Logger.error("Upsert failed for some reason: #{inspect(error)}")
send_resp(conn, 500, "Error") |> halt()
end
end
defp maybe_save_db(%Plug.Conn{} = conn), do: conn
defp maybe_save_collection_items(
%Plug.Conn{:halted => false, :assigns => %{:ids => ids, :object => object}} = conn
) do
CollectionObject.delete_all(object)
CollectionObject.insert_all(object, wrap(ids))
conn
end
defp maybe_save_collection_items(
%Plug.Conn{
:halted => false,
:assigns => %{:object => object},
:body_params => %{"ids" => ids}
} = conn
) do
CollectionObject.delete_all(object)
CollectionObject.insert_all(object, wrap(ids))
conn
end
defp maybe_save_collection_items(
%Plug.Conn{
:halted => false,
:assigns => %{:object => object},
:body_params => %{"append_ids" => ids}
} = conn
) do
CollectionObject.insert_all(object, wrap(ids))
conn
end
defp maybe_save_collection_items(
%Plug.Conn{
:halted => false,
:assigns => %{:object => object},
:body_params => %{"delete_ids" => ids}
} = conn
) do
CollectionObject.delete_all(object, wrap(ids))
conn
end
defp maybe_save_collection_items(%Plug.Conn{} = conn), do: conn
defp wrap(id) when is_binary(id), do: [id]
defp wrap(ids) when is_list(ids), do: ids
end