diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/pleroma/activity.ex | 1 | ||||
-rw-r--r-- | lib/pleroma/plugs/http_signature.ex | 19 | ||||
-rw-r--r-- | lib/pleroma/user.ex | 10 | ||||
-rw-r--r-- | lib/pleroma/web/activity_pub/activity_pub.ex | 19 | ||||
-rw-r--r-- | lib/pleroma/web/activity_pub/activity_pub_controller.ex | 28 | ||||
-rw-r--r-- | lib/pleroma/web/activity_pub/views/object_view.ex | 26 | ||||
-rw-r--r-- | lib/pleroma/web/activity_pub/views/user_view.ex | 51 | ||||
-rw-r--r-- | lib/pleroma/web/http_signatures/http_signatures.ex | 48 | ||||
-rw-r--r-- | lib/pleroma/web/ostatus/ostatus_controller.ex | 22 | ||||
-rw-r--r-- | lib/pleroma/web/router.ex | 12 | ||||
-rw-r--r-- | lib/pleroma/web/web_finger/web_finger.ex | 1 |
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()}} ] } |