X-Git-Url: http://git.squeep.com/?a=blobdiff_plain;f=lib%2Fpleroma%2Fweb%2Fsalmon%2Fsalmon.ex;h=0a69aa1ec9091530d8c99b4859afb898f4f74544;hb=305d2194136d4560e02c110d528164034d3727b2;hp=4f6dfed65cd121484ec091f44ce4afb0394b263c;hpb=fecbd099cda8d71e616ea266255384e3e4cc946a;p=akkoma diff --git a/lib/pleroma/web/salmon/salmon.ex b/lib/pleroma/web/salmon/salmon.ex index 4f6dfed65..0a69aa1ec 100644 --- a/lib/pleroma/web/salmon/salmon.ex +++ b/lib/pleroma/web/salmon/salmon.ex @@ -1,10 +1,17 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.Salmon do @httpoison Application.get_env(:pleroma, :httpoison) use Bitwise + + alias Pleroma.Instances + alias Pleroma.User alias Pleroma.Web.XML alias Pleroma.Web.OStatus.ActivityRepresenter - alias Pleroma.User + require Logger def decode(salmon) do @@ -29,7 +36,8 @@ defmodule Pleroma.Web.Salmon do with [data, _, _, _, _] <- decode(salmon), doc <- XML.parse_document(data), uri when not is_nil(uri) <- XML.string_from_xpath("/entry/author[1]/uri", doc), - {:ok, %{info: %{"magic_key" => magic_key}}} <- Pleroma.Web.OStatus.find_or_make_user(uri) do + {:ok, public_key} <- User.get_public_key_for_ap_id(uri), + magic_key <- encode_key(public_key) do {:ok, magic_key} end end @@ -37,9 +45,10 @@ defmodule Pleroma.Web.Salmon do def decode_and_validate(magickey, salmon) do [data, type, encoding, alg, sig] = decode(salmon) - signed_text = [data, type, encoding, alg] - |> Enum.map(&Base.url_encode64/1) - |> Enum.join(".") + signed_text = + [data, type, encoding, alg] + |> Enum.map(&Base.url_encode64/1) + |> Enum.join(".") key = decode_key(magickey) @@ -53,37 +62,56 @@ defmodule Pleroma.Web.Salmon do end def decode_key("RSA." <> magickey) do - make_integer = fn(bin) -> + make_integer = fn bin -> list = :erlang.binary_to_list(bin) - Enum.reduce(list, 0, fn (el, acc) -> (acc <<< 8) ||| el end) + Enum.reduce(list, 0, fn el, acc -> acc <<< 8 ||| el end) end - [modulus, exponent] = magickey - |> String.split(".") - |> Enum.map(fn (n) -> Base.url_decode64!(n, padding: false) end) - |> Enum.map(make_integer) + [modulus, exponent] = + magickey + |> String.split(".") + |> Enum.map(fn n -> Base.url_decode64!(n, padding: false) end) + |> Enum.map(make_integer) {: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 + 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 + # Native generation of RSA keys is only available since OTP 20+ and in default build conditions + # We try at compile time to generate natively an RSA key otherwise we fallback on the old way. + try do + _ = :public_key.generate_key({:rsa, 2048, 65537}) + + def generate_rsa_pem do + key = :public_key.generate_key({:rsa, 2048, 65537}) + entry = :public_key.pem_entry_encode(:RSAPrivateKey, key) + pem = :public_key.pem_encode([entry]) |> String.trim_trailing() {:ok, pem} - else - :error end + rescue + _ -> + 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 end def keys_from_pem(pem) do @@ -99,17 +127,20 @@ defmodule Pleroma.Web.Salmon do encoding = "base64url" alg = "RSA-SHA256" - signed_text = [doc, type, encoding, alg] - |> Enum.map(&Base.url_encode64/1) - |> Enum.join(".") + signed_text = + [doc, type, encoding, alg] + |> Enum.map(&Base.url_encode64/1) + |> Enum.join(".") - signature = signed_text - |> :public_key.sign(:sha256, private_key) - |> to_string - |> Base.url_encode64 + signature = + signed_text + |> :public_key.sign(:sha256, private_key) + |> to_string + |> Base.url_encode64() - doc_base64 = doc - |> Base.url_encode64 + doc_base64 = + doc + |> Base.url_encode64() # Don't need proper xml building, these strings are safe to leave unescaped salmon = """ @@ -125,39 +156,85 @@ defmodule Pleroma.Web.Salmon do {:ok, salmon} end - def remote_users(%{data: %{"to" => to}}) do + def remote_users(%{data: %{"to" => to} = data}) do + to = to ++ (data["cc"] || []) + to - |> Enum.map(fn(id) -> User.get_cached_by_ap_id(id) end) - |> Enum.filter(fn(user) -> user && !user.local end) + |> Enum.map(fn id -> User.get_cached_by_ap_id(id) end) + |> Enum.filter(fn user -> user && !user.local end) end - defp send_to_user(%{info: %{"salmon" => salmon}}, feed, poster) do - with {:ok, %{status_code: code}} <- poster.(salmon, feed, [{"Content-Type", "application/magic-envelope+xml"}], timeout: 10000, recv_timeout: 20000) do - Logger.debug(fn -> "Pushed to #{salmon}, code #{code}" end) + @doc "Pushes an activity to remote account." + def send_to_user(%{recipient: %{info: %{salmon: salmon}}} = params), + do: send_to_user(Map.put(params, :recipient, salmon)) + + def send_to_user(%{recipient: url, feed: feed, poster: poster} = params) when is_binary(url) do + with {:ok, %{status: code}} when code in 200..299 <- + poster.( + url, + feed, + [{"Content-Type", "application/magic-envelope+xml"}] + ) do + if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since], + do: Instances.set_reachable(url) + + Logger.debug(fn -> "Pushed to #{url}, code #{code}" end) + :ok else - e -> Logger.debug(fn -> "Pushing salmon to #{salmon} failed, #{inspect(e)}" end) + e -> + unless params[:unreachable_since], do: Instances.set_reachable(url) + Logger.debug(fn -> "Pushing Salmon to #{url} failed, #{inspect(e)}" end) + :error end end - defp send_to_user(_,_,_), do: nil - - def publish(user, activity, poster \\ &@httpoison.post/4) - def publish(%{info: %{"keys" => keys}} = user, activity, poster) do + def send_to_user(_), do: :noop + + @supported_activities [ + "Create", + "Follow", + "Like", + "Announce", + "Undo", + "Delete" + ] + + @doc """ + Publishes an activity to remote accounts + """ + @spec publish(User.t(), Pleroma.Activity.t(), Pleroma.HTTP.t()) :: none + def publish(user, activity, poster \\ &@httpoison.post/3) + + def publish(%{info: %{keys: keys}} = user, %{data: %{"type" => type}} = activity, poster) + when type in @supported_activities do feed = ActivityRepresenter.to_simple_form(activity, user, true) - |> ActivityRepresenter.wrap_with_entry - |> :xmerl.export_simple(:xmerl_xml) - |> to_string if feed do + feed = + ActivityRepresenter.wrap_with_entry(feed) + |> :xmerl.export_simple(:xmerl_xml) + |> to_string + {:ok, private, _} = keys_from_pem(keys) {:ok, feed} = encode(private, feed) - remote_users(activity) - |> Enum.each(fn(remote_user) -> - Task.start(fn -> - Logger.debug(fn -> "sending salmon to #{remote_user.ap_id}" end) - send_to_user(remote_user, feed, poster) - end) + remote_users = remote_users(activity) + + salmon_urls = Enum.map(remote_users, & &1.info.salmon) + reachable_urls_metadata = Instances.filter_reachable(salmon_urls) + reachable_urls = Map.keys(reachable_urls_metadata) + + remote_users + |> Enum.filter(&(&1.info.salmon in reachable_urls)) + |> Enum.each(fn remote_user -> + Logger.debug(fn -> "Sending Salmon to #{remote_user.ap_id}" end) + + Pleroma.Web.Federator.publish_single_salmon(%{ + recipient: remote_user, + feed: feed, + poster: poster, + unreachable_since: reachable_urls_metadata[remote_user.info.salmon] + }) end) end end