balls/lib/balls_pds/router.ex

421 lines
12 KiB
Elixir

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