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