aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/pleroma/activity.ex1
-rw-r--r--lib/pleroma/plugs/http_signature.ex19
-rw-r--r--lib/pleroma/user.ex10
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub.ex19
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub_controller.ex28
-rw-r--r--lib/pleroma/web/activity_pub/views/object_view.ex26
-rw-r--r--lib/pleroma/web/activity_pub/views/user_view.ex51
-rw-r--r--lib/pleroma/web/http_signatures/http_signatures.ex48
-rw-r--r--lib/pleroma/web/ostatus/ostatus_controller.ex22
-rw-r--r--lib/pleroma/web/router.ex12
-rw-r--r--lib/pleroma/web/web_finger/web_finger.ex1
11 files changed, 227 insertions, 10 deletions
diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex
index afd09982f..a8154859a 100644
--- a/lib/pleroma/activity.ex
+++ b/lib/pleroma/activity.ex
@@ -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
index 000000000..17030cdbf
--- /dev/null
+++ b/lib/pleroma/plugs/http_signature.ex
@@ -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
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 81cec8265..e544d3772 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -376,4 +376,14 @@ defmodule Pleroma.User do
:ok
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
+ {:ok, public_key}
+ else
+ _ -> :error
+ end
+ end
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 421fd5cd7..7b85770b7 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -1,14 +1,19 @@
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
+ 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 +220,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
data = Upload.store(file)
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
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
index 000000000..0d3e8f44c
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
@@ -0,0 +1,28 @@
+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")
+ 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
index 000000000..403f8cb17
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/views/object_view.ex
@@ -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
index 000000000..b3b02c4fb
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/views/user_view.ex
@@ -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
diff --git a/lib/pleroma/web/http_signatures/http_signatures.ex b/lib/pleroma/web/http_signatures/http_signatures.ex
new file mode 100644
index 000000000..830ddf64d
--- /dev/null
+++ b/lib/pleroma/web/http_signatures/http_signatures.ex
@@ -0,0 +1,48 @@
+# https://tools.ietf.org/html/draft-cavage-http-signatures-08
+defmodule Pleroma.Web.HTTPSignatures do
+ alias Pleroma.User
+
+ 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
+ validate_conn(conn, public_key)
+ 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
+end
diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex
index d442d16fd..e5f99c66c 100644
--- a/lib/pleroma/web/ostatus/ostatus_controller.ex
+++ b/lib/pleroma/web/ostatus/ostatus_controller.ex
@@ -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
@@ -64,13 +66,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
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 6e9f40955..6455ff108 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -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
diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex
index 95e717b17..09957e133 100644
--- a/lib/pleroma/web/web_finger/web_finger.ex
+++ b/lib/pleroma/web/web_finger/web_finger.ex
@@ -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()}}
]
}