summaryrefslogtreecommitdiff
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.ex50
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub.ex59
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub_controller.ex30
-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/federator/federator.ex3
-rw-r--r--lib/pleroma/web/http_signatures/http_signatures.ex76
-rw-r--r--lib/pleroma/web/ostatus/ostatus.ex7
-rw-r--r--lib/pleroma/web/ostatus/ostatus_controller.ex22
-rw-r--r--lib/pleroma/web/router.ex12
-rw-r--r--lib/pleroma/web/twitter_api/representers/object_representer.ex16
-rw-r--r--lib/pleroma/web/web_finger/web_finger.ex1
14 files changed, 352 insertions, 21 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..ddf66cee9 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -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
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 421fd5cd7..6e29768d1 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -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
index 000000000..35723f75c
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
@@ -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
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/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
new file mode 100644
index 000000000..cdc5e1f3f
--- /dev/null
+++ b/lib/pleroma/web/http_signatures/http_signatures.ex
@@ -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
diff --git a/lib/pleroma/web/ostatus/ostatus.ex b/lib/pleroma/web/ostatus/ostatus.ex
index c35ba42be..91c4474c5 100644
--- a/lib/pleroma/web/ostatus/ostatus.ex
+++ b/lib/pleroma/web/ostatus/ostatus.ex
@@ -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
diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex
index 4d48c5d2b..4388217d1 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
@@ -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
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/twitter_api/representers/object_representer.ex b/lib/pleroma/web/twitter_api/representers/object_representer.ex
index 69eaeb36c..e2d653ba8 100644
--- a/lib/pleroma/web/twitter_api/representers/object_representer.ex
+++ b/lib/pleroma/web/twitter_api/representers/object_representer.ex
@@ -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)
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()}}
]
}