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