9ce092fd8fa01c3cb548deffe21cb726ed6c0d77
[anni] / test / pleroma / web / media_proxy / media_proxy_controller_test.exs
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
6   use Pleroma.Web.ConnCase
7
8   import Mock
9   import Mox
10
11   alias Pleroma.ReverseProxy.ClientMock
12   alias Pleroma.Web.MediaProxy
13   alias Plug.Conn
14
15   describe "Media Proxy" do
16     setup do
17       clear_config([:media_proxy, :enabled], true)
18       clear_config([Pleroma.Web.Endpoint, :secret_key_base], "00000000000")
19
20       [url: MediaProxy.encode_url("https://google.fn/test.png")]
21     end
22
23     test "it returns 404 when disabled", %{conn: conn} do
24       clear_config([:media_proxy, :enabled], false)
25
26       assert %Conn{
27                status: 404,
28                resp_body: "Not Found"
29              } = get(conn, "/proxy/hhgfh/eeeee")
30
31       assert %Conn{
32                status: 404,
33                resp_body: "Not Found"
34              } = get(conn, "/proxy/hhgfh/eeee/fff")
35     end
36
37     test "it returns 403 for invalid signature", %{conn: conn, url: url} do
38       clear_config([Pleroma.Web.Endpoint, :secret_key_base], "000")
39       %{path: path} = URI.parse(url)
40
41       assert %Conn{
42                status: 403,
43                resp_body: "Forbidden"
44              } = get(conn, path)
45
46       assert %Conn{
47                status: 403,
48                resp_body: "Forbidden"
49              } = get(conn, "/proxy/hhgfh/eeee")
50
51       assert %Conn{
52                status: 403,
53                resp_body: "Forbidden"
54              } = get(conn, "/proxy/hhgfh/eeee/fff")
55     end
56
57     test "redirects to valid url when filename is invalidated", %{conn: conn, url: url} do
58       invalid_url = String.replace(url, "test.png", "test-file.png")
59       response = get(conn, invalid_url)
60       assert response.status == 302
61       assert redirected_to(response) == url
62     end
63
64     test "it performs ReverseProxy.call with valid signature", %{conn: conn, url: url} do
65       with_mock Pleroma.ReverseProxy,
66         call: fn _conn, _url, _opts -> %Conn{status: :success} end do
67         assert %Conn{status: :success} = get(conn, url)
68       end
69     end
70
71     test "it returns 404 when url is in banned_urls cache", %{conn: conn, url: url} do
72       MediaProxy.put_in_banned_urls("https://google.fn/test.png")
73
74       with_mock Pleroma.ReverseProxy,
75         call: fn _conn, _url, _opts -> %Conn{status: :success} end do
76         assert %Conn{status: 404, resp_body: "Not Found"} = get(conn, url)
77       end
78     end
79
80     test "it applies sandbox CSP to MediaProxy requests", %{conn: conn} do
81       media_url = "https://lain.com/image.png"
82       media_proxy_url = MediaProxy.encode_url(media_url)
83
84       ClientMock
85       |> expect(:request, fn :get, ^media_url, _, _, _ ->
86         {:ok, 200, [{"content-type", "image/png"}]}
87       end)
88
89       %Conn{resp_headers: headers} = get(conn, media_proxy_url)
90
91       assert {"content-security-policy", "sandbox;"} in headers
92     end
93   end
94
95   describe "Media Preview Proxy" do
96     def assert_dependencies_installed do
97       missing_dependencies = Pleroma.Helpers.MediaHelper.missing_dependencies()
98
99       assert missing_dependencies == [],
100              "Error: missing dependencies (please refer to `docs/installation`): #{inspect(missing_dependencies)}"
101     end
102
103     setup do
104       clear_config([:media_proxy, :enabled], true)
105       clear_config([:media_preview_proxy, :enabled], true)
106       clear_config([Pleroma.Web.Endpoint, :secret_key_base], "00000000000")
107
108       original_url = "https://google.fn/test.png"
109
110       [
111         url: MediaProxy.encode_preview_url(original_url),
112         media_proxy_url: MediaProxy.encode_url(original_url)
113       ]
114     end
115
116     test "returns 404 when media proxy is disabled", %{conn: conn} do
117       clear_config([:media_proxy, :enabled], false)
118
119       assert %Conn{
120                status: 404,
121                resp_body: "Not Found"
122              } = get(conn, "/proxy/preview/hhgfh/eeeee")
123
124       assert %Conn{
125                status: 404,
126                resp_body: "Not Found"
127              } = get(conn, "/proxy/preview/hhgfh/fff")
128     end
129
130     test "returns 404 when disabled", %{conn: conn} do
131       clear_config([:media_preview_proxy, :enabled], false)
132
133       assert %Conn{
134                status: 404,
135                resp_body: "Not Found"
136              } = get(conn, "/proxy/preview/hhgfh/eeeee")
137
138       assert %Conn{
139                status: 404,
140                resp_body: "Not Found"
141              } = get(conn, "/proxy/preview/hhgfh/fff")
142     end
143
144     test "it returns 403 for invalid signature", %{conn: conn, url: url} do
145       clear_config([Pleroma.Web.Endpoint, :secret_key_base], "000")
146       %{path: path} = URI.parse(url)
147
148       assert %Conn{
149                status: 403,
150                resp_body: "Forbidden"
151              } = get(conn, path)
152
153       assert %Conn{
154                status: 403,
155                resp_body: "Forbidden"
156              } = get(conn, "/proxy/preview/hhgfh/eeee")
157
158       assert %Conn{
159                status: 403,
160                resp_body: "Forbidden"
161              } = get(conn, "/proxy/preview/hhgfh/eeee/fff")
162     end
163
164     test "redirects to valid url when filename is invalidated", %{conn: conn, url: url} do
165       invalid_url = String.replace(url, "test.png", "test-file.png")
166       response = get(conn, invalid_url)
167       assert response.status == 302
168       assert redirected_to(response) == url
169     end
170
171     test "responds with 424 Failed Dependency if HEAD request to media proxy fails", %{
172       conn: conn,
173       url: url,
174       media_proxy_url: media_proxy_url
175     } do
176       Tesla.Mock.mock(fn
177         %{method: "HEAD", url: ^media_proxy_url} ->
178           %Tesla.Env{status: 500, body: ""}
179       end)
180
181       response = get(conn, url)
182       assert response.status == 424
183       assert response.resp_body == "Can't fetch HTTP headers (HTTP 500)."
184     end
185
186     test "redirects to media proxy URI on unsupported content type", %{
187       conn: conn,
188       url: url,
189       media_proxy_url: media_proxy_url
190     } do
191       Tesla.Mock.mock(fn
192         %{method: "HEAD", url: ^media_proxy_url} ->
193           %Tesla.Env{status: 200, body: "", headers: [{"content-type", "application/pdf"}]}
194       end)
195
196       response = get(conn, url)
197       assert response.status == 302
198       assert redirected_to(response) == media_proxy_url
199     end
200
201     test "with `static=true` and GIF image preview requested, responds with JPEG image", %{
202       conn: conn,
203       url: url,
204       media_proxy_url: media_proxy_url
205     } do
206       assert_dependencies_installed()
207
208       # Setting a high :min_content_length to ensure this scenario is not affected by its logic
209       clear_config([:media_preview_proxy, :min_content_length], 1_000_000_000)
210
211       Tesla.Mock.mock(fn
212         %{method: "HEAD", url: ^media_proxy_url} ->
213           %Tesla.Env{
214             status: 200,
215             body: "",
216             headers: [{"content-type", "image/gif"}, {"content-length", "1001718"}]
217           }
218
219         %{method: :get, url: ^media_proxy_url} ->
220           %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.gif")}
221       end)
222
223       response = get(conn, url <> "?static=true")
224
225       assert response.status == 200
226       assert Conn.get_resp_header(response, "content-type") == ["image/jpeg"]
227       assert response.resp_body != ""
228     end
229
230     test "with GIF image preview requested and no `static` param, redirects to media proxy URI",
231          %{
232            conn: conn,
233            url: url,
234            media_proxy_url: media_proxy_url
235          } do
236       Tesla.Mock.mock(fn
237         %{method: "HEAD", url: ^media_proxy_url} ->
238           %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/gif"}]}
239       end)
240
241       response = get(conn, url)
242
243       assert response.status == 302
244       assert redirected_to(response) == media_proxy_url
245     end
246
247     test "with `static` param and non-GIF image preview requested, " <>
248            "redirects to media preview proxy URI without `static` param",
249          %{
250            conn: conn,
251            url: url,
252            media_proxy_url: media_proxy_url
253          } do
254       Tesla.Mock.mock(fn
255         %{method: "HEAD", url: ^media_proxy_url} ->
256           %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/jpeg"}]}
257       end)
258
259       response = get(conn, url <> "?static=true")
260
261       assert response.status == 302
262       assert redirected_to(response) == url
263     end
264
265     test "with :min_content_length setting not matched by Content-Length header, " <>
266            "redirects to media proxy URI",
267          %{
268            conn: conn,
269            url: url,
270            media_proxy_url: media_proxy_url
271          } do
272       clear_config([:media_preview_proxy, :min_content_length], 100_000)
273
274       Tesla.Mock.mock(fn
275         %{method: "HEAD", url: ^media_proxy_url} ->
276           %Tesla.Env{
277             status: 200,
278             body: "",
279             headers: [{"content-type", "image/gif"}, {"content-length", "5000"}]
280           }
281       end)
282
283       response = get(conn, url)
284
285       assert response.status == 302
286       assert redirected_to(response) == media_proxy_url
287     end
288
289     test "thumbnails PNG images into PNG", %{
290       conn: conn,
291       url: url,
292       media_proxy_url: media_proxy_url
293     } do
294       assert_dependencies_installed()
295
296       Tesla.Mock.mock(fn
297         %{method: "HEAD", url: ^media_proxy_url} ->
298           %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/png"}]}
299
300         %{method: :get, url: ^media_proxy_url} ->
301           %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.png")}
302       end)
303
304       response = get(conn, url)
305
306       assert response.status == 200
307       assert Conn.get_resp_header(response, "content-type") == ["image/png"]
308       assert response.resp_body != ""
309     end
310
311     test "thumbnails JPEG images into JPEG", %{
312       conn: conn,
313       url: url,
314       media_proxy_url: media_proxy_url
315     } do
316       assert_dependencies_installed()
317
318       Tesla.Mock.mock(fn
319         %{method: "HEAD", url: ^media_proxy_url} ->
320           %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/jpeg"}]}
321
322         %{method: :get, url: ^media_proxy_url} ->
323           %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")}
324       end)
325
326       response = get(conn, url)
327
328       assert response.status == 200
329       assert Conn.get_resp_header(response, "content-type") == ["image/jpeg"]
330       assert response.resp_body != ""
331     end
332
333     test "redirects to media proxy URI in case of thumbnailing error", %{
334       conn: conn,
335       url: url,
336       media_proxy_url: media_proxy_url
337     } do
338       Tesla.Mock.mock(fn
339         %{method: "HEAD", url: ^media_proxy_url} ->
340           %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/jpeg"}]}
341
342         %{method: :get, url: ^media_proxy_url} ->
343           %Tesla.Env{status: 200, body: "<html><body>error</body></html>"}
344       end)
345
346       response = get(conn, url)
347
348       assert response.status == 302
349       assert redirected_to(response) == media_proxy_url
350     end
351   end
352 end