1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.ReverseProxyTest do
6 use Pleroma.Web.ConnCase, async: true
7 import ExUnit.CaptureLog
9 alias Pleroma.ReverseProxy
10 alias Pleroma.ReverseProxy.ClientMock
13 {:ok, _} = Registry.start_link(keys: :unique, name: Pleroma.ReverseProxy.ClientMock)
17 setup :verify_on_exit!
19 defp user_agent_mock(user_agent, invokes) do
20 json = Jason.encode!(%{"user-agent": user_agent})
23 |> expect(:request, fn :get, url, _, _, _ ->
24 Registry.register(Pleroma.ReverseProxy.ClientMock, url, 0)
28 {"content-type", "application/json"},
29 {"content-length", byte_size(json) |> to_string()}
32 |> expect(:stream_body, invokes, fn %{url: url} ->
33 case Registry.lookup(Pleroma.ReverseProxy.ClientMock, url) do
35 Registry.update_value(Pleroma.ReverseProxy.ClientMock, url, &(&1 + 1))
39 Registry.unregister(Pleroma.ReverseProxy.ClientMock, url)
45 describe "reverse proxy" do
46 test "do not track successful request", %{conn: conn} do
47 user_agent_mock("hackney/1.15.1", 2)
50 conn = ReverseProxy.call(conn, url)
52 assert conn.status == 200
53 assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, nil}
57 describe "user-agent" do
58 test "don't keep", %{conn: conn} do
59 user_agent_mock("hackney/1.15.1", 2)
60 conn = ReverseProxy.call(conn, "/user-agent")
61 assert json_response(conn, 200) == %{"user-agent" => "hackney/1.15.1"}
64 test "keep", %{conn: conn} do
65 user_agent_mock(Pleroma.Application.user_agent(), 2)
66 conn = ReverseProxy.call(conn, "/user-agent-keep", keep_user_agent: true)
67 assert json_response(conn, 200) == %{"user-agent" => Pleroma.Application.user_agent()}
71 test "closed connection", %{conn: conn} do
73 |> expect(:request, fn :get, "/closed", _, _, _ -> {:ok, 200, [], %{}} end)
74 |> expect(:stream_body, fn _ -> {:error, :closed} end)
75 |> expect(:close, fn _ -> :ok end)
77 conn = ReverseProxy.call(conn, "/closed")
81 describe "max_body " do
82 test "length returns error if content-length more than option", %{conn: conn} do
83 user_agent_mock("hackney/1.15.1", 0)
85 assert capture_log(fn ->
86 ReverseProxy.call(conn, "/huge-file", max_body_length: 4)
88 "[error] Elixir.Pleroma.ReverseProxy: request to \"/huge-file\" failed: :body_too_large"
90 assert {:ok, true} == Cachex.get(:failed_proxy_url_cache, "/huge-file")
92 assert capture_log(fn ->
93 ReverseProxy.call(conn, "/huge-file", max_body_length: 4)
97 defp stream_mock(invokes, with_close? \\ false) do
99 |> expect(:request, fn :get, "/stream-bytes/" <> length, _, _, _ ->
100 Registry.register(Pleroma.ReverseProxy.ClientMock, "/stream-bytes/" <> length, 0)
102 {:ok, 200, [{"content-type", "application/octet-stream"}],
103 %{url: "/stream-bytes/" <> length}}
105 |> expect(:stream_body, invokes, fn %{url: "/stream-bytes/" <> length} ->
106 max = String.to_integer(length)
108 case Registry.lookup(Pleroma.ReverseProxy.ClientMock, "/stream-bytes/" <> length) do
109 [{_, current}] when current < max ->
110 Registry.update_value(
111 Pleroma.ReverseProxy.ClientMock,
112 "/stream-bytes/" <> length,
119 Registry.unregister(Pleroma.ReverseProxy.ClientMock, "/stream-bytes/" <> length)
125 expect(ClientMock, :close, fn _ -> :ok end)
129 test "max_body_length returns error if streaming body more than that option", %{conn: conn} do
132 assert capture_log(fn ->
133 ReverseProxy.call(conn, "/stream-bytes/50", max_body_length: 30)
135 "[warn] Elixir.Pleroma.ReverseProxy request to /stream-bytes/50 failed while reading/chunking: :body_too_large"
139 describe "HEAD requests" do
140 test "common", %{conn: conn} do
142 |> expect(:request, fn :head, "/head", _, _, _ ->
143 {:ok, 200, [{"content-type", "text/html; charset=utf-8"}]}
146 conn = ReverseProxy.call(Map.put(conn, :method, "HEAD"), "/head")
147 assert html_response(conn, 200) == ""
151 defp error_mock(status) when is_integer(status) do
153 |> expect(:request, fn :get, "/status/" <> _, _, _, _ ->
158 describe "returns error on" do
159 test "500", %{conn: conn} do
163 capture_log(fn -> ReverseProxy.call(conn, url) end) =~
164 "[error] Elixir.Pleroma.ReverseProxy: request to /status/500 failed with HTTP status 500"
166 assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true}
168 {:ok, ttl} = Cachex.ttl(:failed_proxy_url_cache, url)
172 test "400", %{conn: conn} do
176 capture_log(fn -> ReverseProxy.call(conn, url) end) =~
177 "[error] Elixir.Pleroma.ReverseProxy: request to /status/400 failed with HTTP status 400"
179 assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true}
180 assert Cachex.ttl(:failed_proxy_url_cache, url) == {:ok, nil}
183 test "403", %{conn: conn} do
188 ReverseProxy.call(conn, url, failed_request_ttl: :timer.seconds(120))
190 "[error] Elixir.Pleroma.ReverseProxy: request to /status/403 failed with HTTP status 403"
192 {:ok, ttl} = Cachex.ttl(:failed_proxy_url_cache, url)
196 test "204", %{conn: conn} do
198 expect(ClientMock, :request, fn :get, _url, _, _, _ -> {:ok, 204, [], %{}} end)
201 conn = ReverseProxy.call(conn, url)
202 assert conn.resp_body == "Request failed: No Content"
205 "[error] Elixir.Pleroma.ReverseProxy: request to \"/status/204\" failed with HTTP status 204"
207 assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true}
208 assert Cachex.ttl(:failed_proxy_url_cache, url) == {:ok, nil}
212 test "streaming", %{conn: conn} do
214 conn = ReverseProxy.call(conn, "/stream-bytes/200")
215 assert conn.state == :chunked
216 assert byte_size(conn.resp_body) == 200
217 assert Plug.Conn.get_resp_header(conn, "content-type") == ["application/octet-stream"]
220 defp headers_mock(_) do
222 |> expect(:request, fn :get, "/headers", headers, _, _ ->
223 Registry.register(Pleroma.ReverseProxy.ClientMock, "/headers", 0)
224 {:ok, 200, [{"content-type", "application/json"}], %{url: "/headers", headers: headers}}
226 |> expect(:stream_body, 2, fn %{url: url, headers: headers} ->
227 case Registry.lookup(Pleroma.ReverseProxy.ClientMock, url) do
229 Registry.update_value(Pleroma.ReverseProxy.ClientMock, url, &(&1 + 1))
230 headers = for {k, v} <- headers, into: %{}, do: {String.capitalize(k), v}
231 {:ok, Jason.encode!(%{headers: headers})}
234 Registry.unregister(Pleroma.ReverseProxy.ClientMock, url)
242 describe "keep request headers" do
243 setup [:headers_mock]
245 test "header passes", %{conn: conn} do
247 Plug.Conn.put_req_header(
252 |> ReverseProxy.call("/headers")
254 %{"headers" => headers} = json_response(conn, 200)
255 assert headers["Accept"] == "text/html"
258 test "header is filtered", %{conn: conn} do
260 Plug.Conn.put_req_header(
265 |> ReverseProxy.call("/headers")
267 %{"headers" => headers} = json_response(conn, 200)
268 refute headers["Accept-Language"]
272 test "returns 400 on non GET, HEAD requests", %{conn: conn} do
273 conn = ReverseProxy.call(Map.put(conn, :method, "POST"), "/ip")
274 assert conn.status == 400
277 describe "cache resp headers" do
278 test "returns headers", %{conn: conn} do
280 |> expect(:request, fn :get, "/cache/" <> ttl, _, _, _ ->
281 {:ok, 200, [{"cache-control", "public, max-age=" <> ttl}], %{}}
283 |> expect(:stream_body, fn _ -> :done end)
285 conn = ReverseProxy.call(conn, "/cache/10")
286 assert {"cache-control", "public, max-age=10"} in conn.resp_headers
289 test "add cache-control", %{conn: conn} do
291 |> expect(:request, fn :get, "/cache", _, _, _ ->
292 {:ok, 200, [{"ETag", "some ETag"}], %{}}
294 |> expect(:stream_body, fn _ -> :done end)
296 conn = ReverseProxy.call(conn, "/cache")
297 assert {"cache-control", "public"} in conn.resp_headers
301 defp disposition_headers_mock(headers) do
303 |> expect(:request, fn :get, "/disposition", _, _, _ ->
304 Registry.register(Pleroma.ReverseProxy.ClientMock, "/disposition", 0)
306 {:ok, 200, headers, %{url: "/disposition"}}
308 |> expect(:stream_body, 2, fn %{url: "/disposition"} ->
309 case Registry.lookup(Pleroma.ReverseProxy.ClientMock, "/disposition") do
311 Registry.update_value(Pleroma.ReverseProxy.ClientMock, "/disposition", &(&1 + 1))
315 Registry.unregister(Pleroma.ReverseProxy.ClientMock, "/disposition")
321 describe "response content disposition header" do
322 test "not atachment", %{conn: conn} do
323 disposition_headers_mock([
324 {"content-type", "image/gif"},
325 {"content-length", 0}
328 conn = ReverseProxy.call(conn, "/disposition")
330 assert {"content-type", "image/gif"} in conn.resp_headers
333 test "with content-disposition header", %{conn: conn} do
334 disposition_headers_mock([
335 {"content-disposition", "attachment; filename=\"filename.jpg\""},
336 {"content-length", 0}
339 conn = ReverseProxy.call(conn, "/disposition")
341 assert {"content-disposition", "attachment; filename=\"filename.jpg\""} in conn.resp_headers