From: lain Date: Sun, 11 Feb 2018 19:43:33 +0000 (+0100) Subject: ActivityPub: Basic note federation with Mastodon. X-Git-Url: https://git.squeep.com/?a=commitdiff_plain;h=8cf97ee8e15a36cbbf0964d5be53c88d29798163;p=akkoma ActivityPub: Basic note federation with Mastodon. --- diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 47aefaeba..ddf66cee9 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -383,10 +383,33 @@ defmodule Pleroma.User do :ok 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 - with %User{} = user <- get_cached_by_ap_id(ap_id), - %{info: %{"magic_key" => magic_key}} <- user, - public_key <- Pleroma.Web.Salmon.decode_key(magic_key) do + with %User{} = user <- get_or_fetch_by_ap_id(ap_id), + {:ok, public_key} <- public_key_from_info(user.info) do {:ok, public_key} else _ -> :error diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 4d0de71e4..6e29768d1 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -223,18 +223,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do Repo.insert(%Object{data: data}) 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 with {:ok, %{status_code: 200, body: body}} <- @httpoison.get(ap_id, ["Accept": "application/activity+json"]), {:ok, data} <- Poison.decode(body) @@ -252,4 +240,36 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do User.insert_or_update_user(user_data) 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 diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 0d3e8f44c..35723f75c 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -23,6 +23,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do with {:ok, data} <- ActivityPub.prepare_incoming(params), {:ok, activity} <- ActivityPub.insert(data, false) do json(conn, "ok") + else + e -> IO.inspect(e) end end end diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex index c9f9dc7a1..68e5544e7 100644 --- a/lib/pleroma/web/federator/federator.ex +++ b/lib/pleroma/web/federator/federator.ex @@ -47,6 +47,9 @@ defmodule Pleroma.Web.Federator do Logger.debug(fn -> "Sending #{activity.data["id"]} out via websub" end) 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 diff --git a/lib/pleroma/web/http_signatures/http_signatures.ex b/lib/pleroma/web/http_signatures/http_signatures.ex index 830ddf64d..cdc5e1f3f 100644 --- a/lib/pleroma/web/http_signatures/http_signatures.ex +++ b/lib/pleroma/web/http_signatures/http_signatures.ex @@ -1,6 +1,7 @@ # https://tools.ietf.org/html/draft-cavage-http-signatures-08 defmodule Pleroma.Web.HTTPSignatures do alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub def split_signature(sig) do default = %{"headers" => "date"} @@ -28,7 +29,16 @@ defmodule Pleroma.Web.HTTPSignatures do # For now, fetch the key for the actor. with actor_id <- conn.params["actor"], {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do - validate_conn(conn, public_key) + 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) + end + end else _ -> false end @@ -45,4 +55,22 @@ defmodule Pleroma.Web.HTTPSignatures do |> Enum.map(fn (header) -> "#{header}: #{headers[header]}" end) |> Enum.join("\n") 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 diff --git a/test/user_test.exs b/test/user_test.exs index 16d43e619..196363f1c 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -370,4 +370,8 @@ defmodule Pleroma.UserTest do refute Repo.get(Activity, activity.id) 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 diff --git a/test/web/http_sigs/http_sig_test.exs b/test/web/http_sigs/http_sig_test.exs index bd9e10b65..2061f45de 100644 --- a/test/web/http_sigs/http_sig_test.exs +++ b/test/web/http_sigs/http_sig_test.exs @@ -3,6 +3,7 @@ defmodule Pleroma.Web.HTTPSignaturesTest do use Pleroma.DataCase alias Pleroma.Web.HTTPSignatures + import Pleroma.Factory @private_key (hd(:public_key.pem_decode(File.read!("test/web/http_sigs/priv.key"))) |> :public_key.pem_entry_decode()) @@ -86,4 +87,29 @@ defmodule Pleroma.Web.HTTPSignaturesTest do assert HTTPSignatures.validate_conn(conn, public_key) 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