ActivityPub: Basic note federation with Mastodon.
authorlain <lain@soykaf.club>
Sun, 11 Feb 2018 19:43:33 +0000 (20:43 +0100)
committerlain <lain@soykaf.club>
Sun, 11 Feb 2018 19:43:33 +0000 (20:43 +0100)
lib/pleroma/user.ex
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/activity_pub/activity_pub_controller.ex
lib/pleroma/web/federator/federator.ex
lib/pleroma/web/http_signatures/http_signatures.ex
test/user_test.exs
test/web/http_sigs/http_sig_test.exs

index 47aefaebabc178f3621ddf90995d2938855a246f..ddf66cee95ee4840d9823851921f362ed881e252 100644 (file)
@@ -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
index 4d0de71e467ea81a3d8143e47ba47eb24ac977d1..6e29768d14d8090286d9cccc24cdadab2de337e5 100644 (file)
@@ -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
index 0d3e8f44cf26bb2be74de2a1587e36d409f027f3..35723f75c064f12ef8f8720d19a22b0a644db3a3 100644 (file)
@@ -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
index c9f9dc7a1ec1eb677f2815f4e49e0766debc0b74..68e5544e7c2d5fdb68e46059e79fc02c7b739a66 100644 (file)
@@ -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
 
index 830ddf64df0f0f48592a4afb8f5b21043c2be334..cdc5e1f3f505c78637d1967da6c86613cbc3e2cc 100644 (file)
@@ -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
index 16d43e6195367bdb72ed8eed83ca148390335ff2..196363f1cf896f258af86a18632ac213ddf56b6b 100644 (file)
@@ -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
index bd9e10b656d09e2f47c8fb24a842f7e4b7aaba95..2061f45dee9faf87d533b8b4babea6bd32b19634 100644 (file)
@@ -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