logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma

salmon.ex (7360B)


      1 # Pleroma: A lightweight social networking server
      2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
      3 # SPDX-License-Identifier: AGPL-3.0-only
      4 
      5 defmodule Pleroma.Web.Salmon do
      6   @behaviour Pleroma.Web.Federator.Publisher
      7 
      8   use Bitwise
      9 
     10   alias Pleroma.Activity
     11   alias Pleroma.HTTP
     12   alias Pleroma.Instances
     13   alias Pleroma.Keys
     14   alias Pleroma.User
     15   alias Pleroma.Web.ActivityPub.Visibility
     16   alias Pleroma.Web.Federator.Publisher
     17   alias Pleroma.Web.OStatus
     18   alias Pleroma.Web.OStatus.ActivityRepresenter
     19   alias Pleroma.Web.XML
     20 
     21   require Logger
     22 
     23   def decode(salmon) do
     24     doc = XML.parse_document(salmon)
     25 
     26     {:xmlObj, :string, data} = :xmerl_xpath.string('string(//me:data[1])', doc)
     27     {:xmlObj, :string, sig} = :xmerl_xpath.string('string(//me:sig[1])', doc)
     28     {:xmlObj, :string, alg} = :xmerl_xpath.string('string(//me:alg[1])', doc)
     29     {:xmlObj, :string, encoding} = :xmerl_xpath.string('string(//me:encoding[1])', doc)
     30     {:xmlObj, :string, type} = :xmerl_xpath.string('string(//me:data[1]/@type)', doc)
     31 
     32     {:ok, data} = Base.url_decode64(to_string(data), ignore: :whitespace)
     33     {:ok, sig} = Base.url_decode64(to_string(sig), ignore: :whitespace)
     34     alg = to_string(alg)
     35     encoding = to_string(encoding)
     36     type = to_string(type)
     37 
     38     [data, type, encoding, alg, sig]
     39   end
     40 
     41   def fetch_magic_key(salmon) do
     42     with [data, _, _, _, _] <- decode(salmon),
     43          doc <- XML.parse_document(data),
     44          uri when not is_nil(uri) <- XML.string_from_xpath("/entry/author[1]/uri", doc),
     45          {:ok, public_key} <- User.get_public_key_for_ap_id(uri),
     46          magic_key <- encode_key(public_key) do
     47       {:ok, magic_key}
     48     end
     49   end
     50 
     51   def decode_and_validate(magickey, salmon) do
     52     [data, type, encoding, alg, sig] = decode(salmon)
     53 
     54     signed_text =
     55       [data, type, encoding, alg]
     56       |> Enum.map(&Base.url_encode64/1)
     57       |> Enum.join(".")
     58 
     59     key = decode_key(magickey)
     60 
     61     verify = :public_key.verify(signed_text, :sha256, sig, key)
     62 
     63     if verify do
     64       {:ok, data}
     65     else
     66       :error
     67     end
     68   end
     69 
     70   def decode_key("RSA." <> magickey) do
     71     make_integer = fn bin ->
     72       list = :erlang.binary_to_list(bin)
     73       Enum.reduce(list, 0, fn el, acc -> acc <<< 8 ||| el end)
     74     end
     75 
     76     [modulus, exponent] =
     77       magickey
     78       |> String.split(".")
     79       |> Enum.map(fn n -> Base.url_decode64!(n, padding: false) end)
     80       |> Enum.map(make_integer)
     81 
     82     {:RSAPublicKey, modulus, exponent}
     83   end
     84 
     85   def encode_key({:RSAPublicKey, modulus, exponent}) do
     86     modulus_enc = :binary.encode_unsigned(modulus) |> Base.url_encode64()
     87     exponent_enc = :binary.encode_unsigned(exponent) |> Base.url_encode64()
     88 
     89     "RSA.#{modulus_enc}.#{exponent_enc}"
     90   end
     91 
     92   def encode(private_key, doc) do
     93     type = "application/atom+xml"
     94     encoding = "base64url"
     95     alg = "RSA-SHA256"
     96 
     97     signed_text =
     98       [doc, type, encoding, alg]
     99       |> Enum.map(&Base.url_encode64/1)
    100       |> Enum.join(".")
    101 
    102     signature =
    103       signed_text
    104       |> :public_key.sign(:sha256, private_key)
    105       |> to_string
    106       |> Base.url_encode64()
    107 
    108     doc_base64 =
    109       doc
    110       |> Base.url_encode64()
    111 
    112     # Don't need proper xml building, these strings are safe to leave unescaped
    113     salmon = """
    114     <?xml version="1.0" encoding="UTF-8"?>
    115     <me:env xmlns:me="http://salmon-protocol.org/ns/magic-env">
    116       <me:data type="application/atom+xml">#{doc_base64}</me:data>
    117       <me:encoding>#{encoding}</me:encoding>
    118       <me:alg>#{alg}</me:alg>
    119       <me:sig>#{signature}</me:sig>
    120     </me:env>
    121     """
    122 
    123     {:ok, salmon}
    124   end
    125 
    126   def remote_users(%User{id: user_id}, %{data: %{"to" => to} = data}) do
    127     cc = Map.get(data, "cc", [])
    128 
    129     bcc =
    130       data
    131       |> Map.get("bcc", [])
    132       |> Enum.reduce([], fn ap_id, bcc ->
    133         case Pleroma.List.get_by_ap_id(ap_id) do
    134           %Pleroma.List{user_id: ^user_id} = list ->
    135             {:ok, following} = Pleroma.List.get_following(list)
    136             bcc ++ Enum.map(following, & &1.ap_id)
    137 
    138           _ ->
    139             bcc
    140         end
    141       end)
    142 
    143     [to, cc, bcc]
    144     |> Enum.concat()
    145     |> Enum.map(&User.get_cached_by_ap_id/1)
    146     |> Enum.filter(fn user -> user && !user.local end)
    147   end
    148 
    149   @doc "Pushes an activity to remote account."
    150   def publish_one(%{recipient: %{info: %{salmon: salmon}}} = params),
    151     do: publish_one(Map.put(params, :recipient, salmon))
    152 
    153   def publish_one(%{recipient: url, feed: feed} = params) when is_binary(url) do
    154     with {:ok, %{status: code}} when code in 200..299 <-
    155            HTTP.post(
    156              url,
    157              feed,
    158              [{"Content-Type", "application/magic-envelope+xml"}]
    159            ) do
    160       if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
    161         do: Instances.set_reachable(url)
    162 
    163       Logger.debug(fn -> "Pushed to #{url}, code #{code}" end)
    164       {:ok, code}
    165     else
    166       e ->
    167         unless params[:unreachable_since], do: Instances.set_reachable(url)
    168         Logger.debug(fn -> "Pushing Salmon to #{url} failed, #{inspect(e)}" end)
    169         {:error, "Unreachable instance"}
    170     end
    171   end
    172 
    173   def publish_one(%{recipient_id: recipient_id} = params) do
    174     recipient = User.get_cached_by_id(recipient_id)
    175 
    176     params
    177     |> Map.delete(:recipient_id)
    178     |> Map.put(:recipient, recipient)
    179     |> publish_one()
    180   end
    181 
    182   def publish_one(_), do: :noop
    183 
    184   @supported_activities [
    185     "Create",
    186     "Follow",
    187     "Like",
    188     "Announce",
    189     "Undo",
    190     "Delete"
    191   ]
    192 
    193   def is_representable?(%Activity{data: %{"type" => type}} = activity)
    194       when type in @supported_activities,
    195       do: Visibility.is_public?(activity)
    196 
    197   def is_representable?(_), do: false
    198 
    199   @doc """
    200   Publishes an activity to remote accounts
    201   """
    202   @spec publish(User.t(), Pleroma.Activity.t()) :: none
    203   def publish(user, activity)
    204 
    205   def publish(%{keys: keys} = user, %{data: %{"type" => type}} = activity)
    206       when type in @supported_activities do
    207     feed = ActivityRepresenter.to_simple_form(activity, user, true)
    208 
    209     if feed do
    210       feed =
    211         ActivityRepresenter.wrap_with_entry(feed)
    212         |> :xmerl.export_simple(:xmerl_xml)
    213         |> to_string
    214 
    215       {:ok, private, _} = Keys.keys_from_pem(keys)
    216       {:ok, feed} = encode(private, feed)
    217 
    218       remote_users = remote_users(user, activity)
    219 
    220       salmon_urls = Enum.map(remote_users, & &1.info.salmon)
    221       reachable_urls_metadata = Instances.filter_reachable(salmon_urls)
    222       reachable_urls = Map.keys(reachable_urls_metadata)
    223 
    224       remote_users
    225       |> Enum.filter(&(&1.info.salmon in reachable_urls))
    226       |> Enum.each(fn remote_user ->
    227         Logger.debug(fn -> "Sending Salmon to #{remote_user.ap_id}" end)
    228 
    229         Publisher.enqueue_one(__MODULE__, %{
    230           recipient_id: remote_user.id,
    231           feed: feed,
    232           unreachable_since: reachable_urls_metadata[remote_user.info.salmon]
    233         })
    234       end)
    235     end
    236   end
    237 
    238   def publish(%{id: id}, _), do: Logger.debug(fn -> "Keys missing for user #{id}" end)
    239 
    240   def gather_webfinger_links(%User{} = user) do
    241     {:ok, _private, public} = Keys.keys_from_pem(user.keys)
    242     magic_key = encode_key(public)
    243 
    244     [
    245       %{"rel" => "salmon", "href" => OStatus.salmon_path(user)},
    246       %{
    247         "rel" => "magic-public-key",
    248         "href" => "data:application/magic-public-key,#{magic_key}"
    249       }
    250     ]
    251   end
    252 
    253   def gather_nodeinfo_protocol_names, do: []
    254 end