Merge branch 'develop' into feature/activitypub
authorlain <lain@soykaf.club>
Mon, 12 Feb 2018 09:24:15 +0000 (10:24 +0100)
committerlain <lain@soykaf.club>
Mon, 12 Feb 2018 09:24:15 +0000 (10:24 +0100)
28 files changed:
config/config.exs
lib/pleroma/activity.ex
lib/pleroma/plugs/http_signature.ex [new file with mode: 0644]
lib/pleroma/user.ex
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/activity_pub/activity_pub_controller.ex [new file with mode: 0644]
lib/pleroma/web/activity_pub/views/object_view.ex [new file with mode: 0644]
lib/pleroma/web/activity_pub/views/user_view.ex [new file with mode: 0644]
lib/pleroma/web/federator/federator.ex
lib/pleroma/web/http_signatures/http_signatures.ex [new file with mode: 0644]
lib/pleroma/web/ostatus/ostatus.ex
lib/pleroma/web/ostatus/ostatus_controller.ex
lib/pleroma/web/router.ex
lib/pleroma/web/twitter_api/representers/object_representer.ex
lib/pleroma/web/web_finger/web_finger.ex
priv/repo/migrations/20171212163643_add_recipients_to_activities.exs [new file with mode: 0644]
priv/repo/migrations/20171212164525_fill_recipients_in_activities.exs [new file with mode: 0644]
test/fixtures/httpoison_mock/admin@mastdon.example.org.json [new file with mode: 0644]
test/support/httpoison_mock.ex
test/user_test.exs
test/web/activity_pub/activity_pub_controller_test.exs [new file with mode: 0644]
test/web/activity_pub/activity_pub_test.exs
test/web/activity_pub/views/object_view_test.exs [new file with mode: 0644]
test/web/activity_pub/views/user_view_test.exs [new file with mode: 0644]
test/web/http_sigs/http_sig_test.exs [new file with mode: 0644]
test/web/http_sigs/priv.key [new file with mode: 0644]
test/web/http_sigs/pub.key [new file with mode: 0644]
test/web/twitter_api/representers/object_representer_test.exs

index 01109b30f326455407dad32f6c2b23b1f50dac2d..e6c695215e50b06054d089034e5ed7381c9e90cb 100644 (file)
@@ -27,7 +27,8 @@ config :logger, :console,
   metadata: [:request_id]
 
 config :mime, :types, %{
-  "application/xrd+xml" => ["xrd+xml"]
+  "application/xrd+xml" => ["xrd+xml"],
+  "application/activity+json" => ["activity+json"]
 }
 
 config :pleroma, :websub, Pleroma.Web.Websub
index afd09982fcd45211249c0c4cac75f80fefaf65db..a8154859a7f81429eac2b293c2a32cd44f8a34f9 100644 (file)
@@ -7,6 +7,7 @@ defmodule Pleroma.Activity do
     field :data, :map
     field :local, :boolean, default: true
     field :actor, :string
+    field :recipients, {:array, :string}
     has_many :notifications, Notification, on_delete: :delete_all
 
     timestamps()
diff --git a/lib/pleroma/plugs/http_signature.ex b/lib/pleroma/plugs/http_signature.ex
new file mode 100644 (file)
index 0000000..17030cd
--- /dev/null
@@ -0,0 +1,19 @@
+defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
+  alias Pleroma.Web.HTTPSignatures
+  import Plug.Conn
+
+  def init(options) do
+    options
+  end
+
+  def call(conn, opts) do
+    if get_req_header(conn, "signature") do
+      conn = conn
+      |> put_req_header("(request-target)", String.downcase("#{conn.method} #{conn.request_path}"))
+
+      assign(conn, :valid_signature, HTTPSignatures.validate_conn(conn))
+    else
+      conn
+    end
+  end
+end
index 81cec82656ddfe90d3ddd875ae4224ddb158ab6c..ddf66cee95ee4840d9823851921f362ed881e252 100644 (file)
@@ -80,9 +80,15 @@ defmodule Pleroma.User do
     |> validate_length(:name, max: 100)
     |> put_change(:local, false)
     if changes.valid? do
-      followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
-      changes
-      |> put_change(:follower_address, followers)
+      case changes.changes[:info]["source_data"] do
+        %{"followers" => followers} ->
+          changes
+          |> put_change(:follower_address, followers)
+        _ ->
+          followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
+          changes
+          |> put_change(:follower_address, followers)
+      end
     else
       changes
     end
@@ -376,4 +382,42 @@ 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_or_fetch_by_ap_id(ap_id),
+         {:ok, public_key} <- public_key_from_info(user.info) do
+      {:ok, public_key}
+    else
+      _ -> :error
+    end
+  end
+
+  def insert_or_update_user(data) do
+    cs = User.remote_user_creation(data)
+    Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
+  end
 end
index 421fd5cd72583b1f5d64dafbbcc72f93080af7b4..6e29768d14d8090286d9cccc24cdadab2de337e5 100644 (file)
@@ -1,14 +1,21 @@
 defmodule Pleroma.Web.ActivityPub.ActivityPub do
   alias Pleroma.{Activity, Repo, Object, Upload, User, Notification}
+  alias Pleroma.Web.OStatus
   import Ecto.Query
   import Pleroma.Web.ActivityPub.Utils
   require Logger
 
+  @httpoison Application.get_env(:pleroma, :httpoison)
+
+  def get_recipients(data) do
+    (data["to"] || []) ++ (data["cc"] || [])
+  end
+
   def insert(map, local \\ true) when is_map(map) do
     with nil <- Activity.get_by_ap_id(map["id"]),
          map <- lazy_put_activity_defaults(map),
          :ok <- insert_full_object(map) do
-      {:ok, activity} = Repo.insert(%Activity{data: map, local: local, actor: map["actor"]})
+      {:ok, activity} = Repo.insert(%Activity{data: map, local: local, actor: map["actor"], recipients: get_recipients(map)})
       Notification.create_notifications(activity)
       stream_out(activity)
       {:ok, activity}
@@ -215,4 +222,54 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     data = Upload.store(file)
     Repo.insert(%Object{data: data})
   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)
+      do
+      user_data = %{
+        ap_id: data["id"],
+        info: %{
+          "ap_enabled" => true,
+          "source_data" => data
+        },
+        nickname: "#{data["preferredUsername"]}@#{URI.parse(ap_id).host}",
+        name: data["name"]
+      }
+
+      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
new file mode 100644 (file)
index 0000000..35723f7
--- /dev/null
@@ -0,0 +1,30 @@
+defmodule Pleroma.Web.ActivityPub.ActivityPubController do
+  use Pleroma.Web, :controller
+  alias Pleroma.{User, Repo, Object}
+  alias Pleroma.Web.ActivityPub.{ObjectView, UserView}
+  alias Pleroma.Web.ActivityPub.ActivityPub
+
+  def user(conn, %{"nickname" => nickname}) do
+    with %User{} = user <- User.get_cached_by_nickname(nickname),
+         {:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do
+      json(conn, UserView.render("user.json", %{user: user}))
+    end
+  end
+
+  def object(conn, %{"uuid" => uuid}) do
+    with ap_id <- o_status_url(conn, :object, uuid),
+         %Object{} = object <- Object.get_cached_by_ap_id(ap_id) do
+      json(conn, ObjectView.render("object.json", %{object: object}))
+    end
+  end
+
+  # TODO: Move signature failure halt into plug
+  def inbox(%{assigns: %{valid_signature: true}} = conn, params) 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/activity_pub/views/object_view.ex b/lib/pleroma/web/activity_pub/views/object_view.ex
new file mode 100644 (file)
index 0000000..403f8cb
--- /dev/null
@@ -0,0 +1,26 @@
+defmodule Pleroma.Web.ActivityPub.ObjectView do
+  use Pleroma.Web, :view
+
+  def render("object.json", %{object: object}) do
+    base = %{
+      "@context" => [
+        "https://www.w3.org/ns/activitystreams",
+        "https://w3id.org/security/v1",
+        %{
+          "manuallyApprovesFollowers" => "as:manuallyApprovesFollowers",
+          "sensitive" => "as:sensitive",
+          "Hashtag" => "as:Hashtag",
+          "ostatus" => "http://ostatus.org#",
+          "atomUri" => "ostatus:atomUri",
+          "inReplyToAtomUri" => "ostatus:inReplyToAtomUri",
+          "conversation" => "ostatus:conversation",
+          "toot" => "http://joinmastodon.org/ns#",
+          "Emoji" => "toot:Emoji"
+        }
+      ]
+    }
+
+    additional = Map.take(object.data, ["id", "to", "cc", "actor", "content", "summary", "type"])
+    Map.merge(base, additional)
+  end
+end
diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex
new file mode 100644 (file)
index 0000000..b3b02c4
--- /dev/null
@@ -0,0 +1,51 @@
+defmodule Pleroma.Web.ActivityPub.UserView do
+  use Pleroma.Web, :view
+  alias Pleroma.Web.Salmon
+  alias Pleroma.User
+
+  def render("user.json", %{user: user}) do
+    {:ok, _, public_key} = Salmon.keys_from_pem(user.info["keys"])
+    public_key = :public_key.pem_entry_encode(:RSAPublicKey, public_key)
+    public_key = :public_key.pem_encode([public_key])
+    %{
+      "@context" => [
+        "https://www.w3.org/ns/activitystreams",
+        "https://w3id.org/security/v1",
+        %{
+          "manuallyApprovesFollowers" => "as:manuallyApprovesFollowers",
+          "sensitive" => "as:sensitive",
+          "Hashtag" => "as:Hashtag",
+          "ostatus" => "http://ostatus.org#",
+          "atomUri" => "ostatus:atomUri",
+          "inReplyToAtomUri" => "ostatus:inReplyToAtomUri",
+          "conversation" => "ostatus:conversation",
+          "toot" => "http://joinmastodon.org/ns#",
+          "Emoji" => "toot:Emoji"
+        }
+      ],
+      "id" => user.ap_id,
+      "type" => "Person",
+      "following" => "#{user.ap_id}/following",
+      "followers" => "#{user.ap_id}/followers",
+      "inbox" => "#{user.ap_id}/inbox",
+      "outbox" => "#{user.ap_id}/outbox",
+      "preferredUsername" => user.nickname,
+      "name" => user.name,
+      "summary" => user.bio,
+      "url" => user.ap_id,
+      "manuallyApprovesFollowers" => false,
+      "publicKey" => %{
+        "id" => "#{user.ap_id}#main-key",
+        "owner" => user.ap_id,
+        "publicKeyPem" => public_key
+      },
+      "endpoints" => %{
+        "sharedInbox" => "#{Pleroma.Web.Endpoint.url}/inbox"
+      },
+      "icon" => %{
+        "type" => "Image",
+        "url" => User.avatar_url(user)
+      }
+    }
+  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
 
diff --git a/lib/pleroma/web/http_signatures/http_signatures.ex b/lib/pleroma/web/http_signatures/http_signatures.ex
new file mode 100644 (file)
index 0000000..cdc5e1f
--- /dev/null
@@ -0,0 +1,76 @@
+# 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"}
+
+    sig = sig
+    |> String.trim()
+    |> String.split(",")
+    |> Enum.reduce(default, fn(part, acc) ->
+      [key | rest] = String.split(part, "=")
+      value = Enum.join(rest, "=")
+      Map.put(acc, key, String.trim(value, "\""))
+    end)
+
+    Map.put(sig, "headers", String.split(sig["headers"], ~r/\s/))
+  end
+
+  def validate(headers, signature, public_key) do
+    sigstring = build_signing_string(headers, signature["headers"])
+    {:ok, sig} = Base.decode64(signature["signature"])
+    :public_key.verify(sigstring, :sha256, sig, public_key)
+  end
+
+  def validate_conn(conn) do
+    # TODO: How to get the right key and see if it is actually valid for that request.
+    # 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
+      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
+  end
+
+  def validate_conn(conn, public_key) do
+    headers = Enum.into(conn.req_headers, %{})
+    signature = split_signature(headers["signature"])
+    validate(headers, signature, public_key)
+  end
+
+  def build_signing_string(headers, used_headers) do
+    used_headers
+    |> 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 c35ba42bee6d5bc263d72da8769bbc1128eee5cd..91c4474c52d7618c0745efca6b8aff3028071727 100644 (file)
@@ -218,11 +218,6 @@ defmodule Pleroma.Web.OStatus do
     end
   end
 
-  def insert_or_update_user(data) do
-    cs = User.remote_user_creation(data)
-    Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
-  end
-
   def make_user(uri, update \\ false) do
     with {:ok, info} <- gather_user_info(uri) do
       data = %{
@@ -236,7 +231,7 @@ defmodule Pleroma.Web.OStatus do
       with false <- update,
            %User{} = user <- User.get_by_ap_id(data.ap_id) do
         {:ok, user}
-      else _e -> insert_or_update_user(data)
+      else _e -> User.insert_or_update_user(data)
       end
     end
   end
index 4d48c5d2b9c5550bf13079fccd46892f5d483b3b..4388217d1020978b460dc0cca0e32142d3ac8bd3 100644 (file)
@@ -6,13 +6,15 @@ defmodule Pleroma.Web.OStatus.OStatusController do
   alias Pleroma.Repo
   alias Pleroma.Web.{OStatus, Federator}
   alias Pleroma.Web.XML
+  alias Pleroma.Web.ActivityPub.ActivityPubController
   import Ecto.Query
 
-  def feed_redirect(conn, %{"nickname" => nickname}) do
+  def feed_redirect(conn, %{"nickname" => nickname} = params) do
     user = User.get_cached_by_nickname(nickname)
 
     case get_format(conn) do
       "html" -> Fallback.RedirectController.redirector(conn, nil)
+      "activity+json" -> ActivityPubController.user(conn, params)
       _ -> redirect conn, external: OStatus.feed_path(user)
     end
   end
@@ -70,13 +72,17 @@ defmodule Pleroma.Web.OStatus.OStatusController do
     |> send_resp(200, "")
   end
 
-  def object(conn, %{"uuid" => uuid}) do
-    with id <- o_status_url(conn, :object, uuid),
-         %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id),
-         %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
-      case get_format(conn) do
-        "html" -> redirect(conn, to: "/notice/#{activity.id}")
-        _ -> represent_activity(conn, activity, user)
+  def object(conn, %{"uuid" => uuid} = params) do
+    if get_format(conn) == "activity+json" do
+      ActivityPubController.object(conn, params)
+    else
+      with id <- o_status_url(conn, :object, uuid),
+           %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id),
+             %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
+        case get_format(conn) do
+          "html" -> redirect(conn, to: "/notice/#{activity.id}")
+          _ -> represent_activity(conn, activity, user)
+        end
       end
     end
   end
index 6e9f40955a929a0b93e7d356dfa3d516fbd0c439..6455ff108aaeb1b08ff6424d81d5e1832d3917d0 100644 (file)
@@ -219,7 +219,7 @@ defmodule Pleroma.Web.Router do
   end
 
   pipeline :ostatus do
-    plug :accepts, ["xml", "atom", "html"]
+    plug :accepts, ["xml", "atom", "html", "activity+json"]
   end
 
   scope "/", Pleroma.Web do
@@ -237,6 +237,16 @@ defmodule Pleroma.Web.Router do
     post "/push/subscriptions/:id", Websub.WebsubController, :websub_incoming
   end
 
+  pipeline :activitypub do
+    plug :accepts, ["activity+json"]
+    plug Pleroma.Web.Plugs.HTTPSignaturePlug
+  end
+
+  scope "/", Pleroma.Web.ActivityPub do
+    pipe_through :activitypub
+    post "/users/:nickname/inbox", ActivityPubController, :inbox
+  end
+
   scope "/.well-known", Pleroma.Web do
     pipe_through :well_known
 
index 69eaeb36c3d9c60aeb790e5547de70ef558df155..e2d653ba85ee285b041ccd50f882ac7a0bb6937b 100644 (file)
@@ -2,9 +2,8 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ObjectRepresenter do
   use Pleroma.Web.TwitterAPI.Representers.BaseRepresenter
   alias Pleroma.Object
 
-  def to_map(%Object{} = object, _opts) do
+  def to_map(%Object{data: %{"url" => [url | _]}} = object, _opts) do
     data = object.data
-    url = List.first(data["url"])
     %{
       url: url["href"] |> Pleroma.Web.MediaProxy.url(),
       mimetype: url["mediaType"],
@@ -13,6 +12,19 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ObjectRepresenter do
     }
   end
 
+  def to_map(%Object{data: %{"url" => url} = data}, _opts) when is_binary(url) do
+    %{
+      url: url |> Pleroma.Web.MediaProxy.url(),
+      mimetype: data["mediaType"],
+      id: data["uuid"],
+      oembed: false
+    }
+  end
+
+  def to_map(%Object{}, _opts) do
+    %{}
+  end
+
   # If we only get the naked data, wrap in an object
   def to_map(%{} = data, opts) do
     to_map(%Object{data: data}, opts)
index 95e717b17859086c3e2dddb7a7561718161e8424..09957e13316da154353433b1d8433dd40f37277a 100644 (file)
@@ -45,6 +45,7 @@ defmodule Pleroma.Web.WebFinger do
         {:Link, %{rel: "http://webfinger.net/rel/profile-page", type: "text/html", href: user.ap_id}},
         {:Link, %{rel: "salmon", href: OStatus.salmon_path(user)}},
         {:Link, %{rel: "magic-public-key", href: "data:application/magic-public-key,#{magic_key}"}},
+        {:Link, %{rel: "self", type: "application/activity+json", href: user.ap_id}},
         {:Link, %{rel: "http://ostatus.org/schema/1.0/subscribe", template: OStatus.remote_follow_path()}}
       ]
     }
diff --git a/priv/repo/migrations/20171212163643_add_recipients_to_activities.exs b/priv/repo/migrations/20171212163643_add_recipients_to_activities.exs
new file mode 100644 (file)
index 0000000..7bce781
--- /dev/null
@@ -0,0 +1,11 @@
+defmodule Pleroma.Repo.Migrations.AddRecipientsToActivities do
+  use Ecto.Migration
+
+  def change do
+    alter table(:activities) do
+      add :recipients, {:array, :string}
+    end
+
+    create index(:activities, [:recipients], using: :gin)
+  end
+end
diff --git a/priv/repo/migrations/20171212164525_fill_recipients_in_activities.exs b/priv/repo/migrations/20171212164525_fill_recipients_in_activities.exs
new file mode 100644 (file)
index 0000000..1fcc0da
--- /dev/null
@@ -0,0 +1,21 @@
+defmodule Pleroma.Repo.Migrations.FillRecipientsInActivities do
+  use Ecto.Migration
+  alias Pleroma.{Repo, Activity}
+
+  def up do
+    max = Repo.aggregate(Activity, :max, :id)
+    if max do
+      IO.puts("#{max} activities")
+      chunks = 0..(round(max / 10_000))
+
+      Enum.each(chunks, fn (i) ->
+        min = i * 10_000
+        max = min + 10_000
+        execute("""
+        update activities set recipients = array(select jsonb_array_elements_text(data->'to')) where id > #{min} and id <= #{max};
+        """)
+        |> IO.inspect
+      end)
+    end
+  end
+end
diff --git a/test/fixtures/httpoison_mock/admin@mastdon.example.org.json b/test/fixtures/httpoison_mock/admin@mastdon.example.org.json
new file mode 100644 (file)
index 0000000..12aacdb
--- /dev/null
@@ -0,0 +1 @@
+{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","sensitive":"as:sensitive","movedTo":"as:movedTo","Hashtag":"as:Hashtag","ostatus":"http://ostatus.org#","atomUri":"ostatus:atomUri","inReplyToAtomUri":"ostatus:inReplyToAtomUri","conversation":"ostatus:conversation","toot":"http://joinmastodon.org/ns#","Emoji":"toot:Emoji"}],"id":"http://mastodon.example.org/users/admin","type":"Person","following":"http://mastodon.example.org/users/admin/following","followers":"http://mastodon.example.org/users/admin/followers","inbox":"http://mastodon.example.org/users/admin/inbox","outbox":"http://mastodon.example.org/users/admin/outbox","preferredUsername":"admin","name":"admin","summary":"\u003cp\u003e\u003c/p\u003e","url":"http://mastodon.example.org/@admin","manuallyApprovesFollowers":false,"publicKey":{"id":"http://mastodon.example.org/users/admin#main-key","owner":"http://mastodon.example.org/users/admin","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtc4Tir+3ADhSNF6VKrtW\nOU32T01w7V0yshmQei38YyiVwVvFu8XOP6ACchkdxbJ+C9mZud8qWaRJKVbFTMUG\nNX4+6Q+FobyuKrwN7CEwhDALZtaN2IPbaPd6uG1B7QhWorrY+yFa8f2TBM3BxnUy\nI4T+bMIZIEYG7KtljCBoQXuTQmGtuffO0UwJksidg2ffCF5Q+K//JfQagJ3UzrR+\nZXbKMJdAw4bCVJYs4Z5EhHYBwQWiXCyMGTd7BGlmMkY6Av7ZqHKC/owp3/0EWDNz\nNqF09Wcpr3y3e8nA10X40MJqp/wR+1xtxp+YGbq/Cj5hZGBG7etFOmIpVBrDOhry\nBwIDAQAB\n-----END PUBLIC KEY-----\n"},"endpoints":{"sharedInbox":"http://mastodon.example.org/inbox"}}
index 21607ba95ed510f61bf3cc24e08c4c096756b16b..7ac4885e93d671ba1c271c838032d790609fb5ba 100644 (file)
@@ -366,6 +366,13 @@ defmodule HTTPoisonMock do
     }}
   end
 
+  def get("http://mastodon.example.org/users/admin", ["Accept": "application/activity+json"], _) do
+    {:ok, %Response{
+      status_code: 200,
+      body: File.read!("test/fixtures/httpoison_mock/admin@mastdon.example.org.json")
+    }}
+  end
+
   def get(url, body, headers) do
     {:error, "Not implemented the mock response for get #{inspect(url)}, #{inspect(body)}, #{inspect(headers)}"}
   end
index 0c87b778c7dcdc38b093052e05d000d5708adc47..7f1f606447bf0dcefcdfc829463a3b19a06fca9f 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
diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs
new file mode 100644 (file)
index 0000000..21ed28c
--- /dev/null
@@ -0,0 +1,39 @@
+defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
+  use Pleroma.Web.ConnCase
+  import Pleroma.Factory
+  alias Pleroma.Web.ActivityPub.{UserView, ObjectView}
+  alias Pleroma.{Repo, User}
+
+  describe "/users/:nickname" do
+    test "it returns a json representation of the user", %{conn: conn} do
+      user = insert(:user)
+
+      conn = conn
+      |> put_req_header("accept", "application/activity+json")
+      |> get("/users/#{user.nickname}")
+
+      user = Repo.get(User, user.id)
+
+      assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
+    end
+  end
+
+  describe "/object/:uuid" do
+    test "it returns a json representation of the object", %{conn: conn} do
+      note = insert(:note)
+      uuid = String.split(note.data["id"], "/") |> List.last
+
+      conn = conn
+      |> put_req_header("accept", "application/activity+json")
+      |> get("/objects/#{uuid}")
+
+      assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
+    end
+  end
+
+  describe "/users/:nickname/inbox" do
+    test "it inserts an incoming activity into the database" do
+      assert false
+    end
+  end
+end
index f423ea8beceae157603b94c5a2714f64831efd1c..01e5362ec8667678b2ddadfe74e515da5ea9b396 100644 (file)
@@ -7,6 +7,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
 
   import Pleroma.Factory
 
+  describe "building a user from his ap id" do
+    test "it returns a user" do
+      user_id = "http://mastodon.example.org/users/admin"
+      {:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
+      assert user.ap_id == user_id
+      assert user.nickname == "admin@mastodon.example.org"
+      assert user.info["source_data"]
+      assert user.info["ap_enabled"]
+      assert user.follower_address == "http://mastodon.example.org/users/admin/followers"
+    end
+  end
+
   describe "insertion" do
     test "returns the activity if one with the same id is already in" do
       activity = insert(:note_activity)
@@ -53,6 +65,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
       {:ok, activity} = ActivityPub.create(["user1", "user1", "user2"], %User{ap_id: "1"}, "", %{})
       assert activity.data["to"] == ["user1", "user2"]
       assert activity.actor == "1"
+      assert activity.recipients == ["user1", "user2"]
     end
   end
 
diff --git a/test/web/activity_pub/views/object_view_test.exs b/test/web/activity_pub/views/object_view_test.exs
new file mode 100644 (file)
index 0000000..6a1311b
--- /dev/null
@@ -0,0 +1,17 @@
+defmodule Pleroma.Web.ActivityPub.ObjectViewTest do
+  use Pleroma.DataCase
+  import Pleroma.Factory
+
+  alias Pleroma.Web.ActivityPub.ObjectView
+
+  test "renders a note object" do
+    note = insert(:note)
+
+    result = ObjectView.render("object.json", %{object: note})
+
+    assert result["id"] == note.data["id"]
+    assert result["to"] == note.data["to"]
+    assert result["content"] == note.data["content"]
+    assert result["type"] == "Note"
+  end
+end
diff --git a/test/web/activity_pub/views/user_view_test.exs b/test/web/activity_pub/views/user_view_test.exs
new file mode 100644 (file)
index 0000000..0c64e62
--- /dev/null
@@ -0,0 +1,18 @@
+defmodule Pleroma.Web.ActivityPub.UserViewTest do
+  use Pleroma.DataCase
+  import Pleroma.Factory
+
+  alias Pleroma.Web.ActivityPub.UserView
+
+  test "Renders a user, including the public key" do
+    user = insert(:user)
+    {:ok, user} = Pleroma.Web.WebFinger.ensure_keys_present(user)
+
+    result = UserView.render("user.json", %{user: user})
+
+    assert result["id"] == user.ap_id
+    assert result["preferredUsername"] == user.nickname
+
+    assert String.contains?(result["publicKey"]["publicKeyPem"], "BEGIN RSA PUBLIC KEY")
+  end
+end
diff --git a/test/web/http_sigs/http_sig_test.exs b/test/web/http_sigs/http_sig_test.exs
new file mode 100644 (file)
index 0000000..2061f45
--- /dev/null
@@ -0,0 +1,115 @@
+# http signatures
+# Test data from https://tools.ietf.org/html/draft-cavage-http-signatures-08#appendix-C
+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())
+
+  @public_key (hd(:public_key.pem_decode(File.read!("test/web/http_sigs/pub.key")))
+    |> :public_key.pem_entry_decode())
+
+  @headers %{
+    "(request-target)" => "post /foo?param=value&pet=dog",
+    "host" => "example.com",
+    "date" => "Thu, 05 Jan 2014 21:31:40 GMT",
+    "content-type" => "application/json",
+    "digest" => "SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=",
+    "content-length" => "18"
+  }
+
+  @body "{\"hello\": \"world\"}"
+
+  @default_signature """
+  keyId="Test",algorithm="rsa-sha256",signature="jKyvPcxB4JbmYY4mByyBY7cZfNl4OW9HpFQlG7N4YcJPteKTu4MWCLyk+gIr0wDgqtLWf9NLpMAMimdfsH7FSWGfbMFSrsVTHNTk0rK3usrfFnti1dxsM4jl0kYJCKTGI/UWkqiaxwNiKqGcdlEDrTcUhhsFsOIo8VhddmZTZ8w="
+  """
+
+  @basic_signature """
+  keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date",signature="HUxc9BS3P/kPhSmJo+0pQ4IsCo007vkv6bUm4Qehrx+B1Eo4Mq5/6KylET72ZpMUS80XvjlOPjKzxfeTQj4DiKbAzwJAb4HX3qX6obQTa00/qPDXlMepD2JtTw33yNnm/0xV7fQuvILN/ys+378Ysi082+4xBQFwvhNvSoVsGv4="
+  """
+
+  @all_headers_signature """
+  keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date content-type digest content-length",signature="Ef7MlxLXoBovhil3AlyjtBwAL9g4TN3tibLj7uuNB3CROat/9KaeQ4hW2NiJ+pZ6HQEOx9vYZAyi+7cmIkmJszJCut5kQLAwuX+Ms/mUFvpKlSo9StS2bMXDBNjOh4Auj774GFj4gwjS+3NhFeoqyr/MuN6HsEnkvn6zdgfE2i0="
+  """
+
+  test "split up a signature" do
+    expected = %{
+      "keyId" => "Test",
+      "algorithm" => "rsa-sha256",
+      "signature" => "jKyvPcxB4JbmYY4mByyBY7cZfNl4OW9HpFQlG7N4YcJPteKTu4MWCLyk+gIr0wDgqtLWf9NLpMAMimdfsH7FSWGfbMFSrsVTHNTk0rK3usrfFnti1dxsM4jl0kYJCKTGI/UWkqiaxwNiKqGcdlEDrTcUhhsFsOIo8VhddmZTZ8w=",
+      "headers" => ["date"]
+    }
+
+    assert HTTPSignatures.split_signature(@default_signature) == expected
+  end
+
+  test "validates the default case" do
+    signature = HTTPSignatures.split_signature(@default_signature)
+    assert HTTPSignatures.validate(@headers, signature, @public_key)
+  end
+
+  test "validates the basic case" do
+    signature = HTTPSignatures.split_signature(@basic_signature)
+    assert HTTPSignatures.validate(@headers, signature, @public_key)
+  end
+
+  test "validates the all-headers case" do
+    signature = HTTPSignatures.split_signature(@all_headers_signature)
+    assert HTTPSignatures.validate(@headers, signature, @public_key)
+  end
+
+  test "it contructs a signing string" do
+    expected = "date: Thu, 05 Jan 2014 21:31:40 GMT\ncontent-length: 18"
+    assert expected == HTTPSignatures.build_signing_string(@headers, ["date", "content-length"])
+  end
+
+  test "it validates a conn" do
+    public_key_pem = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnGb42rPZIapY4Hfhxrgn\nxKVJczBkfDviCrrYaYjfGxawSw93dWTUlenCVTymJo8meBlFgIQ70ar4rUbzl6GX\nMYvRdku072d1WpglNHXkjKPkXQgngFDrh2sGKtNB/cEtJcAPRO8OiCgPFqRtMiNM\nc8VdPfPdZuHEIZsJ/aUM38EnqHi9YnVDQik2xxDe3wPghOhqjxUM6eLC9jrjI+7i\naIaEygUdyst9qVg8e2FGQlwAeS2Eh8ygCxn+bBlT5OyV59jSzbYfbhtF2qnWHtZy\nkL7KOOwhIfGs7O9SoR2ZVpTEQ4HthNzainIe/6iCR5HGrao/T8dygweXFYRv+k5A\nPQIDAQAB\n-----END PUBLIC KEY-----\n"
+    [public_key] = :public_key.pem_decode(public_key_pem)
+
+    public_key = public_key
+    |> :public_key.pem_entry_decode()
+
+    conn = %{
+      req_headers: [
+        {"host", "localtesting.pleroma.lol"},
+        {"connection", "close"},
+        {"content-length", "2316"},
+        {"user-agent", "http.rb/2.2.2 (Mastodon/2.1.0.rc3; +http://mastodon.example.org/)"},
+        {"date", "Sun, 10 Dec 2017 14:23:49 GMT"},
+        {"digest", "SHA-256=x/bHADMW8qRrq2NdPb5P9fl0lYpKXXpe5h5maCIL0nM="},
+        {"content-type", "application/activity+json"},
+        {"(request-target)", "post /users/demiurge/inbox"},
+        {"signature", "keyId=\"http://mastodon.example.org/users/admin#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) user-agent host date digest content-type\",signature=\"i0FQvr51sj9BoWAKydySUAO1RDxZmNY6g7M62IA7VesbRSdFZZj9/fZapLp6YSuvxUF0h80ZcBEq9GzUDY3Chi9lx6yjpUAS2eKb+Am/hY3aswhnAfYd6FmIdEHzsMrpdKIRqO+rpQ2tR05LwiGEHJPGS0p528NvyVxrxMT5H5yZS5RnxY5X2HmTKEgKYYcvujdv7JWvsfH88xeRS7Jlq5aDZkmXvqoR4wFyfgnwJMPLel8P/BUbn8BcXglH/cunR0LUP7sflTxEz+Rv5qg+9yB8zgBsB4C0233WpcJxjeD6Dkq0EcoJObBR56F8dcb7NQtUDu7x6xxzcgSd7dHm5w==\""}]
+    }
+
+    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
diff --git a/test/web/http_sigs/priv.key b/test/web/http_sigs/priv.key
new file mode 100644 (file)
index 0000000..425518a
--- /dev/null
@@ -0,0 +1,15 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICXgIBAAKBgQDCFENGw33yGihy92pDjZQhl0C36rPJj+CvfSC8+q28hxA161QF
+NUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6Z4UMR7EOcpfdUE9Hf3m/hs+F
+UR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJwoYi+1hqp1fIekaxsyQIDAQAB
+AoGBAJR8ZkCUvx5kzv+utdl7T5MnordT1TvoXXJGXK7ZZ+UuvMNUCdN2QPc4sBiA
+QWvLw1cSKt5DsKZ8UETpYPy8pPYnnDEz2dDYiaew9+xEpubyeW2oH4Zx71wqBtOK
+kqwrXa/pzdpiucRRjk6vE6YY7EBBs/g7uanVpGibOVAEsqH1AkEA7DkjVH28WDUg
+f1nqvfn2Kj6CT7nIcE3jGJsZZ7zlZmBmHFDONMLUrXR/Zm3pR5m0tCmBqa5RK95u
+412jt1dPIwJBANJT3v8pnkth48bQo/fKel6uEYyboRtA5/uHuHkZ6FQF7OUkGogc
+mSJluOdc5t6hI1VsLn0QZEjQZMEOWr+wKSMCQQCC4kXJEsHAve77oP6HtG/IiEn7
+kpyUXRNvFsDE0czpJJBvL/aRFUJxuRK91jhjC68sA7NsKMGg5OXb5I5Jj36xAkEA
+gIT7aFOYBFwGgQAQkWNKLvySgKbAZRTeLBacpHMuQdl1DfdntvAyqpAZ0lY0RKmW
+G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI
+7U1yQXnTAEFYM560yJlzUpOb1V4cScGd365tiSMvxLOvTA==
+-----END RSA PRIVATE KEY-----
diff --git a/test/web/http_sigs/pub.key b/test/web/http_sigs/pub.key
new file mode 100644 (file)
index 0000000..b3bbf6c
--- /dev/null
@@ -0,0 +1,6 @@
+-----BEGIN PUBLIC KEY-----
+MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCFENGw33yGihy92pDjZQhl0C3
+6rPJj+CvfSC8+q28hxA161QFNUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6
+Z4UMR7EOcpfdUE9Hf3m/hs+FUR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJw
+oYi+1hqp1fIekaxsyQIDAQAB
+-----END PUBLIC KEY-----
index 791b30237b3032cdc033a71e2739d37818ee6a1e..ac8184407ecfe4ba8577c7e5318e9d736785d429 100644 (file)
@@ -28,4 +28,24 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ObjectReprenterTest do
 
     assert expected_object == ObjectRepresenter.to_map(object)
   end
+
+  test "represents mastodon-style attachments" do
+    object = %Object{
+      id: nil,
+      data: %{
+        "mediaType" => "image/png",
+        "name" => "blabla", "type" => "Document",
+        "url" => "http://mastodon.example.org/system/media_attachments/files/000/000/001/original/8619f31c6edec470.png"
+      }
+    }
+
+    expected_object = %{
+      url: "http://mastodon.example.org/system/media_attachments/files/000/000/001/original/8619f31c6edec470.png",
+      mimetype: "image/png",
+      oembed: false,
+      id: nil
+    }
+
+    assert expected_object == ObjectRepresenter.to_map(object)
+  end
 end