This commit is contained in:
Mirai Kumiko 2025-06-27 16:35:35 +02:00
parent 7a4207c58a
commit 2cfc459cd0
Signed by: miraikumiko
GPG key ID: 3F178B1B5E0CB278
5 changed files with 246 additions and 86 deletions

View file

@ -1,5 +1,48 @@
defmodule Nulla.HTTPSignature do defmodule Nulla.HTTPSignature do
import Plug.Conn 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 def verify(conn, public_key_pem) do
with [sig_header] <- get_req_header(conn, "signature"), with [sig_header] <- get_req_header(conn, "signature"),

View file

@ -134,4 +134,25 @@ defmodule Nulla.Models.Actor do
def get_actor(by) when is_map(by) or is_list(by) do def get_actor(by) when is_map(by) or is_list(by) do
Repo.get_by(__MODULE__, by) Repo.get_by(__MODULE__, by)
end 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 end

View file

@ -65,6 +65,23 @@ defmodule Nulla.Models.Relation do
Repo.get_by(__MODULE__, by) Repo.get_by(__MODULE__, by)
end 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 def count_following(local_actor_id) do
__MODULE__ __MODULE__
|> where([r], r.local_actor_id == ^local_actor_id and r.following == true) |> where([r], r.local_actor_id == ^local_actor_id and r.following == true)

View file

@ -4,11 +4,82 @@ defmodule NullaWeb.InboxController do
alias Nulla.Snowflake alias Nulla.Snowflake
alias Nulla.HTTPSignature alias Nulla.HTTPSignature
alias Nulla.Utils alias Nulla.Utils
alias Nulla.Models.User
alias Nulla.Models.Actor alias Nulla.Models.Actor
alias Nulla.Models.Relation alias Nulla.Models.Relation
alias Nulla.Models.Activity 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, %{ def inbox(conn, %{
"id" => follow_id, "id" => follow_id,
"type" => "Follow", "type" => "Follow",
@ -20,7 +91,7 @@ defmodule NullaWeb.InboxController do
with local_actor <- Actor.get_actor(ap_id: target_uri), with local_actor <- Actor.get_actor(ap_id: target_uri),
{:ok, remote_actor_json} <- Utils.fetch_remote_actor(actor_uri), {:ok, remote_actor_json} <- Utils.fetch_remote_actor(actor_uri),
:ok <- HTTPSignature.verify(conn, remote_actor_json["publicKey"]["publicKeyPem"]), :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} <- {:ok, follow_activity} <-
Activity.create_activity(%{ Activity.create_activity(%{
ap_id: follow_id, ap_id: follow_id,
@ -36,95 +107,11 @@ defmodule NullaWeb.InboxController do
actor: local_actor.ap_id, actor: local_actor.ap_id,
object: Jason.encode!(follow_activity) object: Jason.encode!(follow_activity)
}), }),
{:ok, _relation} <- get_or_create_relation(local_actor.id, remote_actor.id), {:ok, _relation} <- Relation.get_or_create_relation(local_actor.id, remote_actor.id) do
:ok <- deliver_accept(accept_activity, remote_actor_json["inbox"], local_actor) do activity = %Activity{accept_activity | object: Jason.decode!(accept_activity.object)}
send_resp(conn, 200, "") body = Jason.encode!(ActivityPub.activity(activity))
else headers = HTTPSignature.make_header(body, remote_actor_json["inbox"], local_actor)
error -> request = Finch.build(:post, remote_actor_json["inbox"], headers, body)
IO.inspect(error, label: "Follow error")
json(conn, %{"error" => "Failed to process Follow"})
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
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
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)
signature_string = """
(request-target): post #{uri.path}
host: #{uri.host}
date: #{date}
digest: #{digest}
"""
user = User.get_user(id: local_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="#{local_actor.publicKey["id"]}",
algorithm="rsa-sha256",
headers="(request-target) host date digest",
signature="#{signature}"
"""
|> String.replace("\n", "")
|> String.trim()
headers = [
{"Content-Type", "application/activity+json"},
{"Date", date},
{"Digest", digest},
{"Signature", signature_header}
]
request = Finch.build(:post, inbox_url, headers, body)
case Finch.request(request, Nulla.Finch) do case Finch.request(request, Nulla.Finch) do
{:ok, %Finch.Response{status: code}} when code in 200..299 -> {:ok, %Finch.Response{status: code}} when code in 200..299 ->
@ -139,5 +126,97 @@ defmodule NullaWeb.InboxController do
IO.inspect(reason, label: "Finch delivery failed") IO.inspect(reason, label: "Finch delivery failed")
{:error, reason} {:error, reason}
end end
send_resp(conn, 200, "")
else
error ->
IO.inspect(error, label: "Follow error")
json(conn, %{"error" => "Failed to process Follow"})
end
end
def inbox(conn, %{
"id" => _accept_id,
"type" => "Accept",
"actor" => _actor_uri,
"object" => _target_uri
}) do
send_resp(conn, 200, "")
end
def inbox(conn, %{
"id" => _reject_id,
"type" => "Reject",
"actor" => _actor_uri,
"object" => _target_uri
}) do
send_resp(conn, 200, "")
end
def inbox(conn, %{
"id" => _block_id,
"type" => "Block",
"actor" => _actor_uri,
"object" => _target_uri
}) do
send_resp(conn, 200, "")
end
def inbox(conn, %{
"id" => _join_id,
"type" => "Join",
"actor" => _actor_uri,
"object" => _target_uri
}) do
send_resp(conn, 200, "")
end
def inbox(conn, %{
"id" => _leave_id,
"type" => "Leave",
"actor" => _actor_uri,
"object" => _target_uri
}) do
send_resp(conn, 200, "")
end
def inbox(conn, %{
"id" => _like_id,
"type" => "Like",
"actor" => _actor_uri,
"object" => _target_uri
}) do
send_resp(conn, 200, "")
end
def inbox(conn, %{
"id" => _dislike_id,
"type" => "Dislike",
"actor" => _actor_uri,
"object" => _target_uri
}) do
send_resp(conn, 200, "")
end
def inbox(conn, %{
"id" => _announce_id,
"type" => "Announce",
"actor" => _actor_uri,
"object" => _target_uri
}) do
send_resp(conn, 200, "")
end
def inbox(conn, %{
"id" => _question_id,
"type" => "Question",
"actor" => _actor_uri,
"object" => _target_uri
}) do
send_resp(conn, 200, "")
end
def inbox(conn, _params) do
send_resp(conn, 400, "")
end end
end end