logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma

reverse_proxy_test.exs (10922B)


      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.ReverseProxyTest do
      6   use Pleroma.Web.ConnCase, async: true
      7   import ExUnit.CaptureLog
      8   import Mox
      9   alias Pleroma.ReverseProxy
     10   alias Pleroma.ReverseProxy.ClientMock
     11 
     12   setup_all do
     13     {:ok, _} = Registry.start_link(keys: :unique, name: Pleroma.ReverseProxy.ClientMock)
     14     :ok
     15   end
     16 
     17   setup :verify_on_exit!
     18 
     19   defp user_agent_mock(user_agent, invokes) do
     20     json = Jason.encode!(%{"user-agent": user_agent})
     21 
     22     ClientMock
     23     |> expect(:request, fn :get, url, _, _, _ ->
     24       Registry.register(Pleroma.ReverseProxy.ClientMock, url, 0)
     25 
     26       {:ok, 200,
     27        [
     28          {"content-type", "application/json"},
     29          {"content-length", byte_size(json) |> to_string()}
     30        ], %{url: url}}
     31     end)
     32     |> expect(:stream_body, invokes, fn %{url: url} ->
     33       case Registry.lookup(Pleroma.ReverseProxy.ClientMock, url) do
     34         [{_, 0}] ->
     35           Registry.update_value(Pleroma.ReverseProxy.ClientMock, url, &(&1 + 1))
     36           {:ok, json}
     37 
     38         [{_, 1}] ->
     39           Registry.unregister(Pleroma.ReverseProxy.ClientMock, url)
     40           :done
     41       end
     42     end)
     43   end
     44 
     45   describe "reverse proxy" do
     46     test "do not track successful request", %{conn: conn} do
     47       user_agent_mock("hackney/1.15.1", 2)
     48       url = "/success"
     49 
     50       conn = ReverseProxy.call(conn, url)
     51 
     52       assert conn.status == 200
     53       assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, nil}
     54     end
     55   end
     56 
     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"}
     62     end
     63 
     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()}
     68     end
     69   end
     70 
     71   test "closed connection", %{conn: conn} do
     72     ClientMock
     73     |> expect(:request, fn :get, "/closed", _, _, _ -> {:ok, 200, [], %{}} end)
     74     |> expect(:stream_body, fn _ -> {:error, :closed} end)
     75     |> expect(:close, fn _ -> :ok end)
     76 
     77     conn = ReverseProxy.call(conn, "/closed")
     78     assert conn.halted
     79   end
     80 
     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)
     84 
     85       assert capture_log(fn ->
     86                ReverseProxy.call(conn, "/huge-file", max_body_length: 4)
     87              end) =~
     88                "[error] Elixir.Pleroma.ReverseProxy: request to \"/huge-file\" failed: :body_too_large"
     89 
     90       assert {:ok, true} == Cachex.get(:failed_proxy_url_cache, "/huge-file")
     91 
     92       assert capture_log(fn ->
     93                ReverseProxy.call(conn, "/huge-file", max_body_length: 4)
     94              end) == ""
     95     end
     96 
     97     defp stream_mock(invokes, with_close? \\ false) do
     98       ClientMock
     99       |> expect(:request, fn :get, "/stream-bytes/" <> length, _, _, _ ->
    100         Registry.register(Pleroma.ReverseProxy.ClientMock, "/stream-bytes/" <> length, 0)
    101 
    102         {:ok, 200, [{"content-type", "application/octet-stream"}],
    103          %{url: "/stream-bytes/" <> length}}
    104       end)
    105       |> expect(:stream_body, invokes, fn %{url: "/stream-bytes/" <> length} ->
    106         max = String.to_integer(length)
    107 
    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,
    113               &(&1 + 10)
    114             )
    115 
    116             {:ok, "0123456789"}
    117 
    118           [{_, ^max}] ->
    119             Registry.unregister(Pleroma.ReverseProxy.ClientMock, "/stream-bytes/" <> length)
    120             :done
    121         end
    122       end)
    123 
    124       if with_close? do
    125         expect(ClientMock, :close, fn _ -> :ok end)
    126       end
    127     end
    128 
    129     test "max_body_length returns error if streaming body more than that option", %{conn: conn} do
    130       stream_mock(3, true)
    131 
    132       assert capture_log(fn ->
    133                ReverseProxy.call(conn, "/stream-bytes/50", max_body_length: 30)
    134              end) =~
    135                "[warn] Elixir.Pleroma.ReverseProxy request to /stream-bytes/50 failed while reading/chunking: :body_too_large"
    136     end
    137   end
    138 
    139   describe "HEAD requests" do
    140     test "common", %{conn: conn} do
    141       ClientMock
    142       |> expect(:request, fn :head, "/head", _, _, _ ->
    143         {:ok, 200, [{"content-type", "text/html; charset=utf-8"}]}
    144       end)
    145 
    146       conn = ReverseProxy.call(Map.put(conn, :method, "HEAD"), "/head")
    147       assert html_response(conn, 200) == ""
    148     end
    149   end
    150 
    151   defp error_mock(status) when is_integer(status) do
    152     ClientMock
    153     |> expect(:request, fn :get, "/status/" <> _, _, _, _ ->
    154       {:error, status}
    155     end)
    156   end
    157 
    158   describe "returns error on" do
    159     test "500", %{conn: conn} do
    160       error_mock(500)
    161       url = "/status/500"
    162 
    163       capture_log(fn -> ReverseProxy.call(conn, url) end) =~
    164         "[error] Elixir.Pleroma.ReverseProxy: request to /status/500 failed with HTTP status 500"
    165 
    166       assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true}
    167 
    168       {:ok, ttl} = Cachex.ttl(:failed_proxy_url_cache, url)
    169       assert ttl <= 60_000
    170     end
    171 
    172     test "400", %{conn: conn} do
    173       error_mock(400)
    174       url = "/status/400"
    175 
    176       capture_log(fn -> ReverseProxy.call(conn, url) end) =~
    177         "[error] Elixir.Pleroma.ReverseProxy: request to /status/400 failed with HTTP status 400"
    178 
    179       assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true}
    180       assert Cachex.ttl(:failed_proxy_url_cache, url) == {:ok, nil}
    181     end
    182 
    183     test "403", %{conn: conn} do
    184       error_mock(403)
    185       url = "/status/403"
    186 
    187       capture_log(fn ->
    188         ReverseProxy.call(conn, url, failed_request_ttl: :timer.seconds(120))
    189       end) =~
    190         "[error] Elixir.Pleroma.ReverseProxy: request to /status/403 failed with HTTP status 403"
    191 
    192       {:ok, ttl} = Cachex.ttl(:failed_proxy_url_cache, url)
    193       assert ttl > 100_000
    194     end
    195 
    196     test "204", %{conn: conn} do
    197       url = "/status/204"
    198       expect(ClientMock, :request, fn :get, _url, _, _, _ -> {:ok, 204, [], %{}} end)
    199 
    200       capture_log(fn ->
    201         conn = ReverseProxy.call(conn, url)
    202         assert conn.resp_body == "Request failed: No Content"
    203         assert conn.halted
    204       end) =~
    205         "[error] Elixir.Pleroma.ReverseProxy: request to \"/status/204\" failed with HTTP status 204"
    206 
    207       assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true}
    208       assert Cachex.ttl(:failed_proxy_url_cache, url) == {:ok, nil}
    209     end
    210   end
    211 
    212   test "streaming", %{conn: conn} do
    213     stream_mock(21)
    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"]
    218   end
    219 
    220   defp headers_mock(_) do
    221     ClientMock
    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}}
    225     end)
    226     |> expect(:stream_body, 2, fn %{url: url, headers: headers} ->
    227       case Registry.lookup(Pleroma.ReverseProxy.ClientMock, url) do
    228         [{_, 0}] ->
    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})}
    232 
    233         [{_, 1}] ->
    234           Registry.unregister(Pleroma.ReverseProxy.ClientMock, url)
    235           :done
    236       end
    237     end)
    238 
    239     :ok
    240   end
    241 
    242   describe "keep request headers" do
    243     setup [:headers_mock]
    244 
    245     test "header passes", %{conn: conn} do
    246       conn =
    247         Plug.Conn.put_req_header(
    248           conn,
    249           "accept",
    250           "text/html"
    251         )
    252         |> ReverseProxy.call("/headers")
    253 
    254       %{"headers" => headers} = json_response(conn, 200)
    255       assert headers["Accept"] == "text/html"
    256     end
    257 
    258     test "header is filtered", %{conn: conn} do
    259       conn =
    260         Plug.Conn.put_req_header(
    261           conn,
    262           "accept-language",
    263           "en-US"
    264         )
    265         |> ReverseProxy.call("/headers")
    266 
    267       %{"headers" => headers} = json_response(conn, 200)
    268       refute headers["Accept-Language"]
    269     end
    270   end
    271 
    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
    275   end
    276 
    277   describe "cache resp headers" do
    278     test "returns headers", %{conn: conn} do
    279       ClientMock
    280       |> expect(:request, fn :get, "/cache/" <> ttl, _, _, _ ->
    281         {:ok, 200, [{"cache-control", "public, max-age=" <> ttl}], %{}}
    282       end)
    283       |> expect(:stream_body, fn _ -> :done end)
    284 
    285       conn = ReverseProxy.call(conn, "/cache/10")
    286       assert {"cache-control", "public, max-age=10"} in conn.resp_headers
    287     end
    288 
    289     test "add cache-control", %{conn: conn} do
    290       ClientMock
    291       |> expect(:request, fn :get, "/cache", _, _, _ ->
    292         {:ok, 200, [{"ETag", "some ETag"}], %{}}
    293       end)
    294       |> expect(:stream_body, fn _ -> :done end)
    295 
    296       conn = ReverseProxy.call(conn, "/cache")
    297       assert {"cache-control", "public"} in conn.resp_headers
    298     end
    299   end
    300 
    301   defp disposition_headers_mock(headers) do
    302     ClientMock
    303     |> expect(:request, fn :get, "/disposition", _, _, _ ->
    304       Registry.register(Pleroma.ReverseProxy.ClientMock, "/disposition", 0)
    305 
    306       {:ok, 200, headers, %{url: "/disposition"}}
    307     end)
    308     |> expect(:stream_body, 2, fn %{url: "/disposition"} ->
    309       case Registry.lookup(Pleroma.ReverseProxy.ClientMock, "/disposition") do
    310         [{_, 0}] ->
    311           Registry.update_value(Pleroma.ReverseProxy.ClientMock, "/disposition", &(&1 + 1))
    312           {:ok, ""}
    313 
    314         [{_, 1}] ->
    315           Registry.unregister(Pleroma.ReverseProxy.ClientMock, "/disposition")
    316           :done
    317       end
    318     end)
    319   end
    320 
    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}
    326       ])
    327 
    328       conn = ReverseProxy.call(conn, "/disposition")
    329 
    330       assert {"content-type", "image/gif"} in conn.resp_headers
    331     end
    332 
    333     test "with content-disposition header", %{conn: conn} do
    334       disposition_headers_mock([
    335         {"content-disposition", "attachment; filename=\"filename.jpg\""},
    336         {"content-length", 0}
    337       ])
    338 
    339       conn = ReverseProxy.call(conn, "/disposition")
    340 
    341       assert {"content-disposition", "attachment; filename=\"filename.jpg\""} in conn.resp_headers
    342     end
    343   end
    344 end