421 lines
12 KiB
Elixir
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
|