Merge branch 'removing-test-errors' into 'develop'
[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 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(%{data: %{"to" => to} = data}) do
127 to = to ++ (data["cc"] || [])
128
129 to
130 |> Enum.map(fn id -> User.get_cached_by_ap_id(id) end)
131 |> Enum.filter(fn user -> user && !user.local end)
132 end
133
134 @doc "Pushes an activity to remote account."
135 def publish_one(%{recipient: %{info: %{salmon: salmon}}} = params),
136 do: publish_one(Map.put(params, :recipient, salmon))
137
138 def publish_one(%{recipient: url, feed: feed} = params) when is_binary(url) do
139 with {:ok, %{status: code}} when code in 200..299 <-
140 HTTP.post(
141 url,
142 feed,
143 [{"Content-Type", "application/magic-envelope+xml"}]
144 ) do
145 if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
146 do: Instances.set_reachable(url)
147
148 Logger.debug(fn -> "Pushed to #{url}, code #{code}" end)
149 {:ok, code}
150 else
151 e ->
152 unless params[:unreachable_since], do: Instances.set_reachable(url)
153 Logger.debug(fn -> "Pushing Salmon to #{url} failed, #{inspect(e)}" end)
154 {:error, "Unreachable instance"}
155 end
156 end
157
158 def publish_one(_), do: :noop
159
160 @supported_activities [
161 "Create",
162 "Follow",
163 "Like",
164 "Announce",
165 "Undo",
166 "Delete"
167 ]
168
169 def is_representable?(%Activity{data: %{"type" => type}} = activity)
170 when type in @supported_activities,
171 do: Visibility.is_public?(activity)
172
173 def is_representable?(_), do: false
174
175 @doc """
176 Publishes an activity to remote accounts
177 """
178 @spec publish(User.t(), Pleroma.Activity.t()) :: none
179 def publish(user, activity)
180
181 def publish(%{info: %{keys: keys}} = user, %{data: %{"type" => type}} = activity)
182 when type in @supported_activities do
183 feed = ActivityRepresenter.to_simple_form(activity, user, true)
184
185 if feed do
186 feed =
187 ActivityRepresenter.wrap_with_entry(feed)
188 |> :xmerl.export_simple(:xmerl_xml)
189 |> to_string
190
191 {:ok, private, _} = Keys.keys_from_pem(keys)
192 {:ok, feed} = encode(private, feed)
193
194 remote_users = remote_users(activity)
195
196 salmon_urls = Enum.map(remote_users, & &1.info.salmon)
197 reachable_urls_metadata = Instances.filter_reachable(salmon_urls)
198 reachable_urls = Map.keys(reachable_urls_metadata)
199
200 remote_users
201 |> Enum.filter(&(&1.info.salmon in reachable_urls))
202 |> Enum.each(fn remote_user ->
203 Logger.debug(fn -> "Sending Salmon to #{remote_user.ap_id}" end)
204
205 Publisher.enqueue_one(__MODULE__, %{
206 recipient: remote_user,
207 feed: feed,
208 unreachable_since: reachable_urls_metadata[remote_user.info.salmon]
209 })
210 end)
211 end
212 end
213
214 def publish(%{id: id}, _), do: Logger.debug(fn -> "Keys missing for user #{id}" end)
215
216 def gather_webfinger_links(%User{} = user) do
217 {:ok, _private, public} = Keys.keys_from_pem(user.info.keys)
218 magic_key = encode_key(public)
219
220 [
221 %{"rel" => "salmon", "href" => OStatus.salmon_path(user)},
222 %{
223 "rel" => "magic-public-key",
224 "href" => "data:application/magic-public-key,#{magic_key}"
225 }
226 ]
227 end
228
229 def gather_nodeinfo_protocol_names, do: []
230 end