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,8 +107,26 @@ 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)}
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, "") send_resp(conn, 200, "")
else else
error -> error ->
@ -46,98 +135,88 @@ defmodule NullaWeb.InboxController do
end end
end end
defp get_or_create_actor(remote_actor_json) do def inbox(conn, %{
ap_id = remote_actor_json["id"] "id" => _accept_id,
"type" => "Accept",
case Actor.get_actor(ap_id: ap_id) do "actor" => _actor_uri,
nil -> "object" => _target_uri
params = }) do
remote_actor_json send_resp(conn, 200, "")
|> 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 end
defp get_or_create_relation(local_actor_id, remote_actor_id) do def inbox(conn, %{
case Relation.get_relation(local_actor_id: local_actor_id, remote_actor_id: remote_actor_id) do "id" => _reject_id,
nil -> "type" => "Reject",
Relation.create_relation(%{ "actor" => _actor_uri,
followed_by: true, "object" => _target_uri
local_actor_id: local_actor_id, }) do
remote_actor_id: remote_actor_id send_resp(conn, 200, "")
})
relation ->
{:ok, relation}
end
end end
defp deliver_accept(accept_activity, inbox_url, local_actor) do def inbox(conn, %{
accept_activity = %Activity{accept_activity | object: Jason.decode!(accept_activity.object)} "id" => _block_id,
body = Jason.encode!(ActivityPub.activity(accept_activity)) "type" => "Block",
digest = "SHA-256=" <> (:crypto.hash(:sha256, body) |> Base.encode64()) "actor" => _actor_uri,
date = DateTime.utc_now() |> Calendar.strftime("%a, %d %b %Y %H:%M:%S GMT") "object" => _target_uri
uri = URI.parse(inbox_url) }) do
send_resp(conn, 200, "")
end
signature_string = """ def inbox(conn, %{
(request-target): post #{uri.path} "id" => _join_id,
host: #{uri.host} "type" => "Join",
date: #{date} "actor" => _actor_uri,
digest: #{digest} "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 = def inbox(conn, %{
case :public_key.pem_decode(user.privateKeyPem) do "id" => _like_id,
[entry] -> :public_key.pem_entry_decode(entry) "type" => "Like",
_ -> raise "Invalid PEM format" "actor" => _actor_uri,
end "object" => _target_uri
}) do
send_resp(conn, 200, "")
end
signature = def inbox(conn, %{
:public_key.sign(signature_string, :sha256, private_key) "id" => _dislike_id,
|> Base.encode64() "type" => "Dislike",
"actor" => _actor_uri,
"object" => _target_uri
}) do
send_resp(conn, 200, "")
end
signature_header = def inbox(conn, %{
""" "id" => _announce_id,
keyId="#{local_actor.publicKey["id"]}", "type" => "Announce",
algorithm="rsa-sha256", "actor" => _actor_uri,
headers="(request-target) host date digest", "object" => _target_uri
signature="#{signature}" }) do
""" send_resp(conn, 200, "")
|> String.replace("\n", "") end
|> String.trim()
headers = [ def inbox(conn, %{
{"Content-Type", "application/activity+json"}, "id" => _question_id,
{"Date", date}, "type" => "Question",
{"Digest", digest}, "actor" => _actor_uri,
{"Signature", signature_header} "object" => _target_uri
] }) do
send_resp(conn, 200, "")
end
request = Finch.build(:post, inbox_url, headers, body) def inbox(conn, _params) do
send_resp(conn, 400, "")
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
end end
end end