ActivityPub: Basic note federation with Mastodon.
This commit is contained in:
parent
ce31f3a922
commit
8cf97ee8e1
|
@ -383,10 +383,33 @@ def delete (%User{} = user) do
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_or_fetch_by_ap_id(ap_id) do
|
||||||
|
if user = get_by_ap_id(ap_id) do
|
||||||
|
user
|
||||||
|
else
|
||||||
|
with {:ok, user} <- ActivityPub.make_user_from_ap_id(ap_id) do
|
||||||
|
user
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# AP style
|
||||||
|
def public_key_from_info(%{"source_data" => %{"publicKey" => %{"publicKeyPem" => public_key_pem}}}) do
|
||||||
|
key = :public_key.pem_decode(public_key_pem)
|
||||||
|
|> hd()
|
||||||
|
|> :public_key.pem_entry_decode()
|
||||||
|
|
||||||
|
{:ok, key}
|
||||||
|
end
|
||||||
|
|
||||||
|
# OStatus Magic Key
|
||||||
|
def public_key_from_info(%{"magic_key" => magic_key}) do
|
||||||
|
{:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
|
||||||
|
end
|
||||||
|
|
||||||
def get_public_key_for_ap_id(ap_id) do
|
def get_public_key_for_ap_id(ap_id) do
|
||||||
with %User{} = user <- get_cached_by_ap_id(ap_id),
|
with %User{} = user <- get_or_fetch_by_ap_id(ap_id),
|
||||||
%{info: %{"magic_key" => magic_key}} <- user,
|
{:ok, public_key} <- public_key_from_info(user.info) do
|
||||||
public_key <- Pleroma.Web.Salmon.decode_key(magic_key) do
|
|
||||||
{:ok, public_key}
|
{:ok, public_key}
|
||||||
else
|
else
|
||||||
_ -> :error
|
_ -> :error
|
||||||
|
|
|
@ -223,18 +223,6 @@ def upload(file) do
|
||||||
Repo.insert(%Object{data: data})
|
Repo.insert(%Object{data: data})
|
||||||
end
|
end
|
||||||
|
|
||||||
def prepare_incoming(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do
|
|
||||||
with {:ok, user} <- OStatus.find_or_make_user(data["actor"]) do
|
|
||||||
data
|
|
||||||
else
|
|
||||||
_e -> :error
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def prepare_incoming(_) do
|
|
||||||
:error
|
|
||||||
end
|
|
||||||
|
|
||||||
def make_user_from_ap_id(ap_id) do
|
def make_user_from_ap_id(ap_id) do
|
||||||
with {:ok, %{status_code: 200, body: body}} <- @httpoison.get(ap_id, ["Accept": "application/activity+json"]),
|
with {:ok, %{status_code: 200, body: body}} <- @httpoison.get(ap_id, ["Accept": "application/activity+json"]),
|
||||||
{:ok, data} <- Poison.decode(body)
|
{:ok, data} <- Poison.decode(body)
|
||||||
|
@ -252,4 +240,36 @@ def make_user_from_ap_id(ap_id) do
|
||||||
User.insert_or_update_user(user_data)
|
User.insert_or_update_user(user_data)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# TODO: Extract to own module, align as close to Mastodon format as possible.
|
||||||
|
def sanitize_outgoing_activity_data(data) do
|
||||||
|
data
|
||||||
|
|> Map.put("@context", "https://www.w3.org/ns/activitystreams")
|
||||||
|
end
|
||||||
|
|
||||||
|
def prepare_incoming(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do
|
||||||
|
with {:ok, user} <- OStatus.find_or_make_user(data["actor"]) do
|
||||||
|
{:ok, data}
|
||||||
|
else
|
||||||
|
_e -> :error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def prepare_incoming(_) do
|
||||||
|
:error
|
||||||
|
end
|
||||||
|
|
||||||
|
def publish(actor, activity) do
|
||||||
|
remote_users = Pleroma.Web.Salmon.remote_users(activity)
|
||||||
|
data = sanitize_outgoing_activity_data(activity.data)
|
||||||
|
Enum.each remote_users, fn(user) ->
|
||||||
|
if user.info["ap_enabled"] do
|
||||||
|
inbox = user.info["source_data"]["inbox"]
|
||||||
|
Logger.info("Federating #{activity.data["id"]} to #{inbox}")
|
||||||
|
host = URI.parse(inbox).host
|
||||||
|
signature = Pleroma.Web.HTTPSignatures.sign(actor, %{host: host})
|
||||||
|
@httpoison.post(inbox, Poison.encode!(data), [{"Content-Type", "application/activity+json"}, {"signature", signature}])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -23,6 +23,8 @@ def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
|
||||||
with {:ok, data} <- ActivityPub.prepare_incoming(params),
|
with {:ok, data} <- ActivityPub.prepare_incoming(params),
|
||||||
{:ok, activity} <- ActivityPub.insert(data, false) do
|
{:ok, activity} <- ActivityPub.insert(data, false) do
|
||||||
json(conn, "ok")
|
json(conn, "ok")
|
||||||
|
else
|
||||||
|
e -> IO.inspect(e)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -47,6 +47,9 @@ def handle(:publish, activity) do
|
||||||
|
|
||||||
Logger.debug(fn -> "Sending #{activity.data["id"]} out via websub" end)
|
Logger.debug(fn -> "Sending #{activity.data["id"]} out via websub" end)
|
||||||
Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity)
|
Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity)
|
||||||
|
|
||||||
|
Logger.debug(fn -> "Sending #{activity.data["id"]} out via AP" end)
|
||||||
|
Pleroma.Web.ActivityPub.ActivityPub.publish(actor, activity)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# https://tools.ietf.org/html/draft-cavage-http-signatures-08
|
# https://tools.ietf.org/html/draft-cavage-http-signatures-08
|
||||||
defmodule Pleroma.Web.HTTPSignatures do
|
defmodule Pleroma.Web.HTTPSignatures do
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
|
|
||||||
def split_signature(sig) do
|
def split_signature(sig) do
|
||||||
default = %{"headers" => "date"}
|
default = %{"headers" => "date"}
|
||||||
|
@ -28,7 +29,16 @@ def validate_conn(conn) do
|
||||||
# For now, fetch the key for the actor.
|
# For now, fetch the key for the actor.
|
||||||
with actor_id <- conn.params["actor"],
|
with actor_id <- conn.params["actor"],
|
||||||
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
|
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
|
||||||
|
if validate_conn(conn, public_key) do
|
||||||
|
true
|
||||||
|
else
|
||||||
|
# Fetch user anew and try one more time
|
||||||
|
with actor_id <- conn.params["actor"],
|
||||||
|
{:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id),
|
||||||
|
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
|
||||||
validate_conn(conn, public_key)
|
validate_conn(conn, public_key)
|
||||||
|
end
|
||||||
|
end
|
||||||
else
|
else
|
||||||
_ -> false
|
_ -> false
|
||||||
end
|
end
|
||||||
|
@ -45,4 +55,22 @@ def build_signing_string(headers, used_headers) do
|
||||||
|> Enum.map(fn (header) -> "#{header}: #{headers[header]}" end)
|
|> Enum.map(fn (header) -> "#{header}: #{headers[header]}" end)
|
||||||
|> Enum.join("\n")
|
|> Enum.join("\n")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def sign(user, headers) do
|
||||||
|
with {:ok, %{info: %{"keys" => keys}}} <- Pleroma.Web.WebFinger.ensure_keys_present(user),
|
||||||
|
{:ok, private_key, _} = Pleroma.Web.Salmon.keys_from_pem(keys) do
|
||||||
|
sigstring = build_signing_string(headers, Map.keys(headers))
|
||||||
|
signature = :public_key.sign(sigstring, :sha256, private_key)
|
||||||
|
|> Base.encode64()
|
||||||
|
|
||||||
|
[
|
||||||
|
keyId: user.ap_id <> "#main-key",
|
||||||
|
algorithm: "rsa-sha256",
|
||||||
|
headers: Map.keys(headers) |> Enum.join(" "),
|
||||||
|
signature: signature
|
||||||
|
]
|
||||||
|
|> Enum.map(fn({k, v}) -> "#{k}=\"#{v}\"" end)
|
||||||
|
|> Enum.join(",")
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -370,4 +370,8 @@ test ".delete deactivates a user, all follow relationships and all create activi
|
||||||
|
|
||||||
refute Repo.get(Activity, activity.id)
|
refute Repo.get(Activity, activity.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "get_public_key_for_ap_id fetches a user that's not in the db" do
|
||||||
|
assert {:ok, _key} = User.get_public_key_for_ap_id("http://mastodon.example.org/users/admin")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
defmodule Pleroma.Web.HTTPSignaturesTest do
|
defmodule Pleroma.Web.HTTPSignaturesTest do
|
||||||
use Pleroma.DataCase
|
use Pleroma.DataCase
|
||||||
alias Pleroma.Web.HTTPSignatures
|
alias Pleroma.Web.HTTPSignatures
|
||||||
|
import Pleroma.Factory
|
||||||
|
|
||||||
@private_key (hd(:public_key.pem_decode(File.read!("test/web/http_sigs/priv.key")))
|
@private_key (hd(:public_key.pem_decode(File.read!("test/web/http_sigs/priv.key")))
|
||||||
|> :public_key.pem_entry_decode())
|
|> :public_key.pem_entry_decode())
|
||||||
|
@ -86,4 +87,29 @@ test "it validates a conn" do
|
||||||
|
|
||||||
assert HTTPSignatures.validate_conn(conn, public_key)
|
assert HTTPSignatures.validate_conn(conn, public_key)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it validates a conn and fetches the key" do
|
||||||
|
conn = %{
|
||||||
|
params: %{"actor" => "http://mastodon.example.org/users/admin"},
|
||||||
|
req_headers: [
|
||||||
|
{"host", "localtesting.pleroma.lol"},
|
||||||
|
{"x-forwarded-for", "127.0.0.1"},
|
||||||
|
{"connection", "close"},
|
||||||
|
{"content-length", "2307"},
|
||||||
|
{"user-agent", "http.rb/2.2.2 (Mastodon/2.1.0.rc3; +http://mastodon.example.org/)"},
|
||||||
|
{"date", "Sun, 11 Feb 2018 17:12:01 GMT"},
|
||||||
|
{"digest", "SHA-256=UXsAnMtR9c7mi1FOf6HRMtPgGI1yi2e9nqB/j4rZ99I="},
|
||||||
|
{"content-type", "application/activity+json"},
|
||||||
|
{"signature", "keyId=\"http://mastodon.example.org/users/admin#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) user-agent host date digest content-type\",signature=\"qXKqpQXUpC3d9bZi2ioEeAqP8nRMD021CzH1h6/w+LRk4Hj31ARJHDwQM+QwHltwaLDUepshMfz2WHSXAoLmzWtvv7xRwY+mRqe+NGk1GhxVZ/LSrO/Vp7rYfDpfdVtkn36LU7/Bzwxvvaa4ZWYltbFsRBL0oUrqsfmJFswNCQIG01BB52BAhGSCORHKtQyzo1IZHdxl8y80pzp/+FOK2SmHkqWkP9QbaU1qTZzckL01+7M5btMW48xs9zurEqC2sM5gdWMQSZyL6isTV5tmkTZrY8gUFPBJQZgihK44v3qgfWojYaOwM8ATpiv7NG8wKN/IX7clDLRMA8xqKRCOKw==\""},
|
||||||
|
{"(request-target)", "post /users/demiurge/inbox"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
assert HTTPSignatures.validate_conn(conn)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it generates a signature" do
|
||||||
|
user = insert(:user)
|
||||||
|
assert HTTPSignatures.sign(user, %{host: "mastodon.example.org"}) =~ "keyId=\""
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue