diff --git a/lib/nulla/httpsignature.ex b/lib/nulla/http_signature.ex similarity index 61% rename from lib/nulla/httpsignature.ex rename to lib/nulla/http_signature.ex index dad2d7f..ccfe0cc 100644 --- a/lib/nulla/httpsignature.ex +++ b/lib/nulla/http_signature.ex @@ -1,5 +1,48 @@ defmodule Nulla.HTTPSignature do import Plug.Conn + alias Nulla.Models.User + + def make_header(body, inbox_url, actor) do + digest = "SHA-256=" <> (:crypto.hash(:sha256, body) |> Base.encode64()) + date = DateTime.utc_now() |> Calendar.strftime("%a, %d %b %Y %H:%M:%S GMT") + uri = URI.parse(inbox_url) + + signature_string = """ + (request-target): post #{uri.path} + host: #{uri.host} + date: #{date} + digest: #{digest} + """ + + user = User.get_user(id: actor.id) + + private_key = + case :public_key.pem_decode(user.privateKeyPem) do + [entry] -> :public_key.pem_entry_decode(entry) + _ -> raise "Invalid PEM format" + end + + signature = + :public_key.sign(signature_string, :sha256, private_key) + |> Base.encode64() + + signature_header = + """ + keyId="#{actor.publicKey["id"]}", + algorithm="rsa-sha256", + headers="(request-target) host date digest", + signature="#{signature}" + """ + |> String.replace("\n", "") + |> String.trim() + + [ + {"Content-Type", "application/activity+json"}, + {"Date", date}, + {"Digest", digest}, + {"Signature", signature_header} + ] + end def verify(conn, public_key_pem) do with [sig_header] <- get_req_header(conn, "signature"), diff --git a/lib/nulla/keygen.ex b/lib/nulla/key_gen.ex similarity index 100% rename from lib/nulla/keygen.ex rename to lib/nulla/key_gen.ex diff --git a/lib/nulla/models/actor.ex b/lib/nulla/models/actor.ex index a8b8072..5c78142 100644 --- a/lib/nulla/models/actor.ex +++ b/lib/nulla/models/actor.ex @@ -134,4 +134,25 @@ defmodule Nulla.Models.Actor do def get_actor(by) when is_map(by) or is_list(by) do Repo.get_by(__MODULE__, by) end + + def get_or_create_actor(actor_json) do + ap_id = actor_json["id"] + + case __MODULE__.get_actor(ap_id: ap_id) do + nil -> + params = + actor_json + |> Map.put("ap_id", ap_id) + |> Map.delete("id") + |> Map.put("domain", URI.parse(ap_id).host) + + case __MODULE__.create_actor(params) do + {:ok, actor} -> {:ok, actor} + {:error, changeset} -> {:error, {:actor_creation_failed, changeset}} + end + + actor -> + {:ok, actor} + end + end end diff --git a/lib/nulla/models/relation.ex b/lib/nulla/models/relation.ex index 609ff0d..506b283 100644 --- a/lib/nulla/models/relation.ex +++ b/lib/nulla/models/relation.ex @@ -65,6 +65,23 @@ defmodule Nulla.Models.Relation do Repo.get_by(__MODULE__, by) end + def get_or_create_relation(local_actor_id, remote_actor_id) do + case __MODULE__.get_relation(local_actor_id: local_actor_id, remote_actor_id: remote_actor_id) do + nil -> + case __MODULE__.create_relation(%{ + followed_by: true, + local_actor_id: local_actor_id, + remote_actor_id: remote_actor_id + }) do + {:ok, relation} -> {:ok, relation} + {:error, changeset} -> {:error, {:actor_creation_failed, changeset}} + end + + relation -> + {:ok, relation} + end + end + def count_following(local_actor_id) do __MODULE__ |> where([r], r.local_actor_id == ^local_actor_id and r.following == true) diff --git a/lib/nulla_web/controllers/inbox_controller.ex b/lib/nulla_web/controllers/inbox_controller.ex index a787c55..168503e 100644 --- a/lib/nulla_web/controllers/inbox_controller.ex +++ b/lib/nulla_web/controllers/inbox_controller.ex @@ -4,11 +4,82 @@ defmodule NullaWeb.InboxController do alias Nulla.Snowflake alias Nulla.HTTPSignature alias Nulla.Utils - alias Nulla.Models.User alias Nulla.Models.Actor alias Nulla.Models.Relation alias Nulla.Models.Activity + def inbox(conn, %{ + "id" => _create_id, + "type" => "Create", + "actor" => _actor_uri, + "object" => _target_uri + }) do + send_resp(conn, 200, "") + end + + def inbox(conn, %{ + "id" => _read_id, + "type" => "Read", + "actor" => _actor_uri, + "object" => _target_uri + }) do + send_resp(conn, 200, "") + end + + def inbox(conn, %{ + "id" => _update_id, + "type" => "Update", + "actor" => _actor_uri, + "object" => _target_uri + }) do + send_resp(conn, 200, "") + end + + def inbox(conn, %{ + "id" => _delete_id, + "type" => "Delete", + "actor" => _actor_uri, + "object" => _target_uri + }) do + send_resp(conn, 200, "") + end + + def inbox(conn, %{ + "id" => _add_id, + "type" => "Add", + "actor" => _actor_uri, + "object" => _target_uri + }) do + send_resp(conn, 200, "") + end + + def inbox(conn, %{ + "id" => _view_id, + "type" => "View", + "actor" => _actor_uri, + "object" => _target_uri + }) do + send_resp(conn, 200, "") + end + + def inbox(conn, %{ + "id" => _move_id, + "type" => "Move", + "actor" => _actor_uri, + "object" => _target_uri + }) do + send_resp(conn, 200, "") + end + + def inbox(conn, %{ + "id" => _undo_id, + "type" => "Undo", + "actor" => _actor_uri, + "object" => _target_uri + }) do + send_resp(conn, 200, "") + end + def inbox(conn, %{ "id" => follow_id, "type" => "Follow", @@ -20,7 +91,7 @@ defmodule NullaWeb.InboxController do with local_actor <- Actor.get_actor(ap_id: target_uri), {:ok, remote_actor_json} <- Utils.fetch_remote_actor(actor_uri), :ok <- HTTPSignature.verify(conn, remote_actor_json["publicKey"]["publicKeyPem"]), - {:ok, remote_actor} <- get_or_create_actor(remote_actor_json), + {:ok, remote_actor} <- Actor.get_or_create_actor(remote_actor_json), {:ok, follow_activity} <- Activity.create_activity(%{ ap_id: follow_id, @@ -36,8 +107,26 @@ defmodule NullaWeb.InboxController do actor: local_actor.ap_id, object: Jason.encode!(follow_activity) }), - {:ok, _relation} <- get_or_create_relation(local_actor.id, remote_actor.id), - :ok <- deliver_accept(accept_activity, remote_actor_json["inbox"], local_actor) do + {:ok, _relation} <- Relation.get_or_create_relation(local_actor.id, remote_actor.id) do + activity = %Activity{accept_activity | object: Jason.decode!(accept_activity.object)} + body = Jason.encode!(ActivityPub.activity(activity)) + headers = HTTPSignature.make_header(body, remote_actor_json["inbox"], local_actor) + request = Finch.build(:post, remote_actor_json["inbox"], headers, body) + + case Finch.request(request, Nulla.Finch) do + {:ok, %Finch.Response{status: code}} when code in 200..299 -> + IO.puts("Accept delivered successfully") + :ok + + {:ok, %Finch.Response{status: code, body: resp}} -> + IO.inspect({:error, code, resp}, label: "Failed to deliver Accept") + {:error, {:http_error, code}} + + {:error, reason} -> + IO.inspect(reason, label: "Finch delivery failed") + {:error, reason} + end + send_resp(conn, 200, "") else error -> @@ -46,98 +135,88 @@ defmodule NullaWeb.InboxController do end end - defp get_or_create_actor(remote_actor_json) do - ap_id = remote_actor_json["id"] - - case Actor.get_actor(ap_id: ap_id) do - nil -> - params = - remote_actor_json - |> Map.put("ap_id", ap_id) - |> Map.delete("id") - |> Map.put("domain", URI.parse(ap_id).host) - - case Actor.create_actor(params) do - {:ok, actor} -> {:ok, actor} - {:error, changeset} -> {:error, {:actor_creation_failed, changeset}} - end - - actor -> - {:ok, actor} - end + def inbox(conn, %{ + "id" => _accept_id, + "type" => "Accept", + "actor" => _actor_uri, + "object" => _target_uri + }) do + send_resp(conn, 200, "") end - defp get_or_create_relation(local_actor_id, remote_actor_id) do - case Relation.get_relation(local_actor_id: local_actor_id, remote_actor_id: remote_actor_id) do - nil -> - Relation.create_relation(%{ - followed_by: true, - local_actor_id: local_actor_id, - remote_actor_id: remote_actor_id - }) - - relation -> - {:ok, relation} - end + def inbox(conn, %{ + "id" => _reject_id, + "type" => "Reject", + "actor" => _actor_uri, + "object" => _target_uri + }) do + send_resp(conn, 200, "") end - defp deliver_accept(accept_activity, inbox_url, local_actor) do - accept_activity = %Activity{accept_activity | object: Jason.decode!(accept_activity.object)} - body = Jason.encode!(ActivityPub.activity(accept_activity)) - digest = "SHA-256=" <> (:crypto.hash(:sha256, body) |> Base.encode64()) - date = DateTime.utc_now() |> Calendar.strftime("%a, %d %b %Y %H:%M:%S GMT") - uri = URI.parse(inbox_url) + def inbox(conn, %{ + "id" => _block_id, + "type" => "Block", + "actor" => _actor_uri, + "object" => _target_uri + }) do + send_resp(conn, 200, "") + end - signature_string = """ - (request-target): post #{uri.path} - host: #{uri.host} - date: #{date} - digest: #{digest} - """ + def inbox(conn, %{ + "id" => _join_id, + "type" => "Join", + "actor" => _actor_uri, + "object" => _target_uri + }) do + send_resp(conn, 200, "") + end - user = User.get_user(id: local_actor.id) + def inbox(conn, %{ + "id" => _leave_id, + "type" => "Leave", + "actor" => _actor_uri, + "object" => _target_uri + }) do + send_resp(conn, 200, "") + end - private_key = - case :public_key.pem_decode(user.privateKeyPem) do - [entry] -> :public_key.pem_entry_decode(entry) - _ -> raise "Invalid PEM format" - end + def inbox(conn, %{ + "id" => _like_id, + "type" => "Like", + "actor" => _actor_uri, + "object" => _target_uri + }) do + send_resp(conn, 200, "") + end - signature = - :public_key.sign(signature_string, :sha256, private_key) - |> Base.encode64() + def inbox(conn, %{ + "id" => _dislike_id, + "type" => "Dislike", + "actor" => _actor_uri, + "object" => _target_uri + }) do + send_resp(conn, 200, "") + end - signature_header = - """ - keyId="#{local_actor.publicKey["id"]}", - algorithm="rsa-sha256", - headers="(request-target) host date digest", - signature="#{signature}" - """ - |> String.replace("\n", "") - |> String.trim() + def inbox(conn, %{ + "id" => _announce_id, + "type" => "Announce", + "actor" => _actor_uri, + "object" => _target_uri + }) do + send_resp(conn, 200, "") + end - headers = [ - {"Content-Type", "application/activity+json"}, - {"Date", date}, - {"Digest", digest}, - {"Signature", signature_header} - ] + def inbox(conn, %{ + "id" => _question_id, + "type" => "Question", + "actor" => _actor_uri, + "object" => _target_uri + }) do + send_resp(conn, 200, "") + end - request = Finch.build(:post, inbox_url, headers, body) - - case Finch.request(request, Nulla.Finch) do - {:ok, %Finch.Response{status: code}} when code in 200..299 -> - IO.puts("Accept delivered successfully") - :ok - - {:ok, %Finch.Response{status: code, body: resp}} -> - IO.inspect({:error, code, resp}, label: "Failed to deliver Accept") - {:error, {:http_error, code}} - - {:error, reason} -> - IO.inspect(reason, label: "Finch delivery failed") - {:error, reason} - end + def inbox(conn, _params) do + send_resp(conn, 400, "") end end