@@ -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 | |||
@@ -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 |
@@ -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 |
@@ -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 | |||
@@ -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 |
@@ -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 |
@@ -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 |