From 57bd59e4071adf847f94229479e5ffa0951721fd Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Wed, 26 Apr 2017 14:25:44 +0200 Subject: [PATCH 1/2] Salmon creation. --- lib/pleroma/web/salmon/salmon.ex | 56 +++++++++++++++++++++++++++++++- test/fixtures/private_key.pem | 27 +++++++++++++++ test/web/salmon/salmon_test.exs | 33 +++++++++++++++++++ 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/private_key.pem diff --git a/lib/pleroma/web/salmon/salmon.ex b/lib/pleroma/web/salmon/salmon.ex index 3881f2758..24b5eb0d9 100644 --- a/lib/pleroma/web/salmon/salmon.ex +++ b/lib/pleroma/web/salmon/salmon.ex @@ -57,7 +57,7 @@ def decode_and_validate(magickey, salmon) do end end - defp decode_key("RSA." <> magickey) do + def decode_key("RSA." <> magickey) do make_integer = fn(bin) -> list = :erlang.binary_to_list(bin) Enum.reduce(list, 0, fn (el, acc) -> (acc <<< 8) ||| el end) @@ -70,4 +70,58 @@ defp decode_key("RSA." <> magickey) do {:RSAPublicKey, modulus, exponent} end + + def encode_key({:RSAPublicKey, modulus, exponent}) do + modulus_enc = :binary.encode_unsigned(modulus) |> Base.url_encode64 + exponent_enc = :binary.encode_unsigned(exponent) |> Base.url_encode64 + + "RSA.#{modulus_enc}.#{exponent_enc}" + end + + def generate_rsa_pem do + port = Port.open({:spawn, "openssl genrsa"}, [:binary]) + {:ok, pem} = receive do + {^port, {:data, pem}} -> {:ok, pem} + end + Port.close(port) + if Regex.match?(~r/RSA PRIVATE KEY/, pem) do + {:ok, pem} + else + :error + end + end + + def keys_from_pem(pem) do + [private_key_code] = :public_key.pem_decode(pem) + private_key = :public_key.pem_entry_decode(private_key_code) + {:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} = private_key + public_key = {:RSAPublicKey, modulus, exponent} + {:ok, private_key, public_key} + end + + def encode(private_key, doc) do + type = "application/atom+xml" + encoding = "base64url" + alg = "RSA-SHA256" + + signed_text = [doc, type, encoding, alg] + |> Enum.map(&Base.url_encode64/1) + |> Enum.join(".") + + signature = :public_key.sign(signed_text, :sha256, private_key) |> to_string |> Base.url_encode64 + doc_base64= doc |> Base.url_encode64 + + # Don't need proper xml building, these strings are safe to leave unescaped + salmon = """ + + + #{doc_base64} + #{encoding} + #{alg} + #{signature} + + """ + + {:ok, salmon} + end end diff --git a/test/fixtures/private_key.pem b/test/fixtures/private_key.pem new file mode 100644 index 000000000..7a4b14654 --- /dev/null +++ b/test/fixtures/private_key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAqnWeDtrqWasCKNXiuSq1tSCLI5H7BSvIROy5YfuGsXHrIlCq +LdIm9QlIUUmIi9QyzgiGEDsPCCkA1UguCVgF/UrJ1+FvHcHsTELkkBu/yCl9mrgt +WzTckhb6KjOhqtxi/TKgRaJ2Rlwz2bvH5sbCP9qffthitdxfh14KC5V0gqDt1xCy +WgZo79vbYMcVkcQoh5uLtG64ksYFBMfgnLaSj7xg5i2qCDiIY7bqBujo5HllDqeo +w3LXmsztt1cT8heXEjW0SYJvAHJK00OsG1kp4cqhfKzxLCHNGQJVHQxLOXy97I7o +HOeuhbxPhjpGSBMgw7YFm3ODXviqf557eqFcaQIDAQABAoIBAC6f+VnK22sncXHF +/zvyyL0AZ86U8XpanW7s6VA5wn/qzwwV0Fa0Mt+3aEaDvIuywSrF/hWWcegjfwzX +r2/y2cCMomUgTopvLrk1WttoG68eWjLlydI2xVZYXpkIgmH/4juri1dAtuVL9wrJ +aEZhe2SH4jSJ74Ya/y5BtLGycaoA9FHyIzHPTx52Ix2jWKWtKimW8J+aERi2uHdN +7yTnLT2APhs5fnvNnn0tg85CI3Ny2GNiqmAail14yVfRz8Sf6qDIepH5Jfz9oll4 +I+GYUOLs6eTgkHXBn8LGhtHTE/9UJmb42OyWrW8X+nc/Mjz5xh0u/g1Gdp36oUMz +OotfneECgYEA3cGfQxmxjEqSbXt9jbxiCukU7PmkDDQqBu97URC4N8qEcMF1wW7X +AddU7Kq/UJU+oqjD/7UQHoS2ZThPtto6SpVdXQzsnrnPWQcrv5b1DV/TpXfwGoZ3 +svUIAcx4vGzhhmHDJCBsdY6n8xWBYtSqfLFXgN5UkdafLGy3EkCEtmUCgYEAxMgl +7eU2QkWkzgJxOj6xjG2yqM3jxOvvoiRnD0rIQaBS70P/1N94ZkMXzOwddddZ5OW+ +55h/a8TmFKP/+NW4PHRYra/dazGI4IBlw6Yeq6uq/4jbuSqtBbaNn/Dz5kdHBTqM +PtbBvc9Fztd2zb3InyyLbb4c+WjMqi0AooN027UCgYB4Tax7GJtLwsEBiDcrB4Ig +7SYfEae/vyT1skIyTmHCUqnbCfk6QUl/hDRcWJ2FuBHM6MW8GZxvEgxpiU0lo+pv +v+xwqKxNx/wHDm7bd6fl45DMee7WVRDnEyuO3kC56E/JOYxGMxjkBcpzg703wqvj +Dcqs7PDwVYDw9uGykzHsSQKBgEQnNcvA+RvW1w9qlSChGgkS7S+9r0dCl8pGZVNM +iTMBfffUS0TE6QQx9IpKtKFdpoq6b3XywR7oIO/BJSRfkOGPQi9Vm5BGpatrjNNI +M5Mtb5n1InRtLWOvKDnez/pPcW+EKZKR+qPsp7bNtR3ovxUx7lBh6dMP0uKVl4Sx +lsWJAoGBAIeek9eG+S3m2jaJRHasfKo5mJ2JrrmnjQXUOGUP8/CgO8sW1VmG2WAk +Av7+BRI2mP2f+3SswG/AoRGmRXXw65ly63ws8ixrhK0MG3MgqDkWc69SbTaaMJ+u +BQFYMsB1vZdUV3CaRqySkjY68QWGcJ4Z5JKHuTXzKv/GeFmw0V9R +-----END RSA PRIVATE KEY----- diff --git a/test/web/salmon/salmon_test.exs b/test/web/salmon/salmon_test.exs index 4ebb32081..6fbabd19f 100644 --- a/test/web/salmon/salmon_test.exs +++ b/test/web/salmon/salmon_test.exs @@ -16,4 +16,37 @@ test "errors on wrong magic key" do {:ok, salmon} = File.read("test/fixtures/salmon.xml") assert Salmon.decode_and_validate(@wrong_magickey, salmon) == :error end + + test "generates an RSA private key pem" do + {:ok, key} = Salmon.generate_rsa_pem + assert is_binary(key) + assert Regex.match?(~r/RSA/, key) + end + + test "it encodes a magic key from a public key" do + key = Salmon.decode_key(@magickey) + magic_key = Salmon.encode_key(key) + + assert @magickey == magic_key + end + + test "returns a public and private key from a pem" do + pem = File.read!("test/fixtures/private_key.pem") + {:ok, private, public} = Salmon.keys_from_pem(pem) + + assert elem(private, 0) == :RSAPrivateKey + assert elem(public, 0) == :RSAPublicKey + end + + test "encodes an xml payload with a private key" do + doc = File.read!("test/fixtures/incoming_note_activity.xml") + pem = File.read!("test/fixtures/private_key.pem") + {:ok, private, public} = Salmon.keys_from_pem(pem) + + # Let's try a roundtrip. + {:ok, salmon} = Salmon.encode(private, doc) + {:ok, decoded_doc} = Salmon.decode_and_validate(Salmon.encode_key(public), salmon) + + assert doc == decoded_doc + end end From c5fa682c317717c64168bf2d77b28d805ffff450 Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Wed, 26 Apr 2017 18:33:10 +0200 Subject: [PATCH 2/2] Refactor, add beginnings of websub client subscriptions. --- lib/pleroma/web/activity_pub/activity_pub.ex | 2 +- lib/pleroma/web/federator/federator.ex | 32 +++++++++++++++++++ lib/pleroma/web/websub/websub.ex | 26 ++++++++++++--- .../web/websub/websub_client_subscription.ex | 13 ++++++++ ...4155_create_websub_client_subscription.exs | 15 +++++++++ test/web/websub/websub_test.exs | 12 ++++++- 6 files changed, 93 insertions(+), 7 deletions(-) create mode 100644 lib/pleroma/web/federator/federator.ex create mode 100644 lib/pleroma/web/websub/websub_client_subscription.ex create mode 100644 priv/repo/migrations/20170426154155_create_websub_client_subscription.exs diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 7264123d8..82f9fcc1c 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -36,7 +36,7 @@ def create(to, actor, context, object, additional \\ %{}, published \\ nil) do {:ok, activity} = add_conversation_id(activity) if actor.local do - Pleroma.Web.Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity) + Pleroma.Web.Federator.enqueue(:publish, activity) end {:ok, activity} diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex new file mode 100644 index 000000000..f489ed837 --- /dev/null +++ b/lib/pleroma/web/federator/federator.ex @@ -0,0 +1,32 @@ +defmodule Pleroma.Web.Federator do + alias Pleroma.User + require Logger + + @websub_verifier Application.get_env(:pleroma, :websub_verifier) + + def handle(:publish, activity) do + Logger.debug("Running publish for #{activity.data["id"]}") + with actor when not is_nil(actor) <- User.get_cached_by_ap_id(activity.data["actor"]) do + Pleroma.Web.Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity) + end + end + + def handle(:verify_websub, websub) do + Logger.debug("Running websub verification for #{websub.id} (#{websub.topic}, #{websub.callback})") + @websub_verifier.verify(websub) + end + + def handle(type, payload) do + Logger.debug("Unknown task: #{type}") + {:error, "Don't know what do do with this"} + end + + def enqueue(type, payload) do + # for now, just run immediately in a new process. + if Mix.env == :test do + handle(type, payload) + else + spawn(fn -> handle(type, payload) end) + end + end +end diff --git a/lib/pleroma/web/websub/websub.ex b/lib/pleroma/web/websub/websub.ex index cc66b52dd..03b0aec8f 100644 --- a/lib/pleroma/web/websub/websub.ex +++ b/lib/pleroma/web/websub/websub.ex @@ -1,13 +1,11 @@ defmodule Pleroma.Web.Websub do alias Pleroma.Repo - alias Pleroma.Web.Websub.WebsubServerSubscription + alias Pleroma.Web.Websub.{WebsubServerSubscription, WebsubClientSubscription} alias Pleroma.Web.OStatus.FeedRepresenter alias Pleroma.Web.OStatus import Ecto.Query - @websub_verifier Application.get_env(:pleroma, :websub_verifier) - def verify(subscription, getter \\ &HTTPoison.get/3 ) do challenge = Base.encode16(:crypto.strong_rand_bytes(8)) lease_seconds = NaiveDateTime.diff(subscription.valid_until, subscription.updated_at) |> to_string @@ -71,8 +69,7 @@ def incoming_subscription_request(user, %{"hub.mode" => "subscribe"} = params) d change = Ecto.Changeset.change(websub, %{valid_until: NaiveDateTime.add(websub.updated_at, lease_time)}) websub = Repo.update!(change) - # Just spawn that for now, maybe pool later. - spawn(fn -> @websub_verifier.verify(websub) end) + Pleroma.Web.Federator.enqueue(:verify_websub, websub) {:ok, websub} else {:error, reason} -> @@ -99,4 +96,23 @@ defp valid_topic(%{"hub.topic" => topic}, user) do {:error, "Wrong topic requested, expected #{OStatus.feed_path(user)}, got #{topic}"} end end + + def subscribe(user, topic) do + # Race condition, use transactions + {:ok, subscription} = with subscription when not is_nil(subscription) <- Repo.get_by(WebsubClientSubscription, topic: topic) do + subscribers = [user.ap_id, subscription.subcribers] |> Enum.uniq + change = Ecto.Changeset.change(subscription, %{subscribers: subscribers}) + Repo.update(change) + else _e -> + subscription = %WebsubClientSubscription{ + topic: topic, + subscribers: [user.ap_id], + state: "requested", + secret: :crypto.strong_rand_bytes(8) |> Base.url_encode64 + } + Repo.insert(subscription) + end + + {:ok, subscription} + end end diff --git a/lib/pleroma/web/websub/websub_client_subscription.ex b/lib/pleroma/web/websub/websub_client_subscription.ex new file mode 100644 index 000000000..341e27c51 --- /dev/null +++ b/lib/pleroma/web/websub/websub_client_subscription.ex @@ -0,0 +1,13 @@ +defmodule Pleroma.Web.Websub.WebsubClientSubscription do + use Ecto.Schema + + schema "websub_client_subscriptions" do + field :topic, :string + field :secret, :string + field :valid_until, :naive_datetime + field :state, :string + field :subscribers, {:array, :string}, default: [] + + timestamps() + end +end diff --git a/priv/repo/migrations/20170426154155_create_websub_client_subscription.exs b/priv/repo/migrations/20170426154155_create_websub_client_subscription.exs new file mode 100644 index 000000000..f42782840 --- /dev/null +++ b/priv/repo/migrations/20170426154155_create_websub_client_subscription.exs @@ -0,0 +1,15 @@ +defmodule Pleroma.Repo.Migrations.CreateWebsubClientSubscription do + use Ecto.Migration + + def change do + create table(:websub_client_subscriptions) do + add :topic, :string + add :secret, :string + add :valid_until, :naive_datetime + add :state, :string + add :subscribers, :map + + timestamps() + end + end +end diff --git a/test/web/websub/websub_test.exs b/test/web/websub/websub_test.exs index 334ba03fc..7b77e696b 100644 --- a/test/web/websub/websub_test.exs +++ b/test/web/websub/websub_test.exs @@ -58,7 +58,6 @@ test "an incoming subscription request" do "hub.lease_seconds" => "100" } - {:ok, subscription } = Websub.incoming_subscription_request(user, data) assert subscription.topic == Pleroma.Web.OStatus.feed_path(user) assert subscription.state == "requested" @@ -87,4 +86,15 @@ test "an incoming subscription request for an existing subscription" do assert length(Repo.all(WebsubServerSubscription)) == 1 assert subscription.id == sub.id end + + test "initiate a subscription for a given user and topic" do + user = insert(:user) + topic = "http://example.org/some-topic.atom" + + {:ok, websub} = Websub.subscribe(user, topic) + assert websub.subscribers == [user.ap_id] + assert websub.topic == topic + assert is_binary(websub.secret) + assert websub.state == "accepted" + end end