Improve AP routes rate limit
[akkoma] / lib / pleroma / web / ostatus / ostatus_controller.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.OStatus.OStatusController do
6 use Pleroma.Web, :controller
7
8 alias Fallback.RedirectController
9 alias Pleroma.Activity
10 alias Pleroma.Object
11 alias Pleroma.User
12 alias Pleroma.Web.ActivityPub.ActivityPub
13 alias Pleroma.Web.ActivityPub.ActivityPubController
14 alias Pleroma.Web.ActivityPub.ObjectView
15 alias Pleroma.Web.ActivityPub.Visibility
16 alias Pleroma.Web.Endpoint
17 alias Pleroma.Web.Federator
18 alias Pleroma.Web.Metadata.PlayerView
19 alias Pleroma.Web.OStatus
20 alias Pleroma.Web.OStatus.ActivityRepresenter
21 alias Pleroma.Web.OStatus.FeedRepresenter
22 alias Pleroma.Web.Router
23 alias Pleroma.Web.XML
24
25 plug(
26 Pleroma.Plugs.RateLimiter,
27 {:ap_routes, params: ["uuid"]} when action in [:object, :activity]
28 )
29
30 plug(Pleroma.Web.FederatingPlug when action in [:salmon_incoming])
31
32 plug(
33 Pleroma.Plugs.SetFormatPlug
34 when action in [:feed_redirect, :object, :activity, :notice]
35 )
36
37 action_fallback(:errors)
38
39 def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname}) do
40 with {_, %User{} = user} <-
41 {:fetch_user, User.get_cached_by_nickname_or_id(nickname)} do
42 RedirectController.redirector_with_meta(conn, %{user: user})
43 end
44 end
45
46 def feed_redirect(%{assigns: %{format: format}} = conn, _params)
47 when format in ["json", "activity+json"] do
48 ActivityPubController.call(conn, :user)
49 end
50
51 def feed_redirect(conn, %{"nickname" => nickname}) do
52 with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do
53 redirect(conn, external: OStatus.feed_path(user))
54 end
55 end
56
57 def feed(conn, %{"nickname" => nickname} = params) do
58 with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do
59 query_params =
60 Map.take(params, ["max_id"])
61 |> Map.merge(%{"whole_db" => true, "actor_id" => user.ap_id})
62
63 activities =
64 ActivityPub.fetch_public_activities(query_params)
65 |> Enum.reverse()
66
67 response =
68 user
69 |> FeedRepresenter.to_simple_form(activities, [user])
70 |> :xmerl.export_simple(:xmerl_xml)
71 |> to_string
72
73 conn
74 |> put_resp_content_type("application/atom+xml")
75 |> send_resp(200, response)
76 end
77 end
78
79 defp decode_or_retry(body) do
80 with {:ok, magic_key} <- Pleroma.Web.Salmon.fetch_magic_key(body),
81 {:ok, doc} <- Pleroma.Web.Salmon.decode_and_validate(magic_key, body) do
82 {:ok, doc}
83 else
84 _e ->
85 with [decoded | _] <- Pleroma.Web.Salmon.decode(body),
86 doc <- XML.parse_document(decoded),
87 uri when not is_nil(uri) <- XML.string_from_xpath("/entry/author[1]/uri", doc),
88 {:ok, _} <- Pleroma.Web.OStatus.make_user(uri, true),
89 {:ok, magic_key} <- Pleroma.Web.Salmon.fetch_magic_key(body),
90 {:ok, doc} <- Pleroma.Web.Salmon.decode_and_validate(magic_key, body) do
91 {:ok, doc}
92 end
93 end
94 end
95
96 def salmon_incoming(conn, _) do
97 {:ok, body, _conn} = read_body(conn)
98 {:ok, doc} = decode_or_retry(body)
99
100 Federator.incoming_doc(doc)
101
102 conn
103 |> send_resp(200, "")
104 end
105
106 def object(%{assigns: %{format: format}} = conn, %{"uuid" => _uuid})
107 when format in ["json", "activity+json"] do
108 ActivityPubController.call(conn, :object)
109 end
110
111 def object(%{assigns: %{format: format}} = conn, %{"uuid" => uuid}) do
112 with id <- o_status_url(conn, :object, uuid),
113 {_, %Activity{} = activity} <-
114 {:activity, Activity.get_create_by_object_ap_id_with_object(id)},
115 {_, true} <- {:public?, Visibility.is_public?(activity)},
116 %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
117 case format do
118 "html" -> redirect(conn, to: "/notice/#{activity.id}")
119 _ -> represent_activity(conn, nil, activity, user)
120 end
121 else
122 reason when reason in [{:public?, false}, {:activity, nil}] ->
123 {:error, :not_found}
124
125 e ->
126 e
127 end
128 end
129
130 def activity(%{assigns: %{format: format}} = conn, %{"uuid" => _uuid})
131 when format in ["json", "activity+json"] do
132 ActivityPubController.call(conn, :activity)
133 end
134
135 def activity(%{assigns: %{format: format}} = conn, %{"uuid" => uuid}) do
136 with id <- o_status_url(conn, :activity, uuid),
137 {_, %Activity{} = activity} <- {:activity, Activity.normalize(id)},
138 {_, true} <- {:public?, Visibility.is_public?(activity)},
139 %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
140 case format do
141 "html" -> redirect(conn, to: "/notice/#{activity.id}")
142 _ -> represent_activity(conn, format, activity, user)
143 end
144 else
145 reason when reason in [{:public?, false}, {:activity, nil}] ->
146 {:error, :not_found}
147
148 e ->
149 e
150 end
151 end
152
153 def notice(%{assigns: %{format: format}} = conn, %{"id" => id}) do
154 with {_, %Activity{} = activity} <- {:activity, Activity.get_by_id_with_object(id)},
155 {_, true} <- {:public?, Visibility.is_public?(activity)},
156 %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
157 cond do
158 format == "html" && activity.data["type"] == "Create" ->
159 %Object{} = object = Object.normalize(activity)
160
161 RedirectController.redirector_with_meta(
162 conn,
163 %{
164 activity_id: activity.id,
165 object: object,
166 url: Router.Helpers.o_status_url(Endpoint, :notice, activity.id),
167 user: user
168 }
169 )
170
171 format == "html" ->
172 RedirectController.redirector(conn, nil)
173
174 true ->
175 represent_activity(conn, format, activity, user)
176 end
177 else
178 reason when reason in [{:public?, false}, {:activity, nil}] ->
179 conn
180 |> put_status(404)
181 |> RedirectController.redirector(nil, 404)
182
183 e ->
184 e
185 end
186 end
187
188 # Returns an HTML embedded <audio> or <video> player suitable for embed iframes.
189 def notice_player(conn, %{"id" => id}) do
190 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id_with_object(id),
191 true <- Visibility.is_public?(activity),
192 %Object{} = object <- Object.normalize(activity),
193 %{data: %{"attachment" => [%{"url" => [url | _]} | _]}} <- object,
194 true <- String.starts_with?(url["mediaType"], ["audio", "video"]) do
195 conn
196 |> put_layout(:metadata_player)
197 |> put_resp_header("x-frame-options", "ALLOW")
198 |> put_resp_header(
199 "content-security-policy",
200 "default-src 'none';style-src 'self' 'unsafe-inline';img-src 'self' data: https:; media-src 'self' https:;"
201 )
202 |> put_view(PlayerView)
203 |> render("player.html", url)
204 else
205 _error ->
206 conn
207 |> put_status(404)
208 |> RedirectController.redirector(nil, 404)
209 end
210 end
211
212 defp represent_activity(
213 conn,
214 "activity+json",
215 %Activity{data: %{"type" => "Create"}} = activity,
216 _user
217 ) do
218 object = Object.normalize(activity)
219
220 conn
221 |> put_resp_header("content-type", "application/activity+json")
222 |> json(ObjectView.render("object.json", %{object: object}))
223 end
224
225 defp represent_activity(_conn, "activity+json", _, _) do
226 {:error, :not_found}
227 end
228
229 defp represent_activity(conn, _, activity, user) do
230 response =
231 activity
232 |> ActivityRepresenter.to_simple_form(user, true)
233 |> ActivityRepresenter.wrap_with_entry()
234 |> :xmerl.export_simple(:xmerl_xml)
235 |> to_string
236
237 conn
238 |> put_resp_content_type("application/atom+xml")
239 |> send_resp(200, response)
240 end
241
242 def errors(conn, {:error, :not_found}) do
243 render_error(conn, :not_found, "Not found")
244 end
245
246 def errors(conn, {:fetch_user, nil}), do: errors(conn, {:error, :not_found})
247
248 def errors(conn, _) do
249 render_error(conn, :internal_server_error, "Something went wrong")
250 end
251 end