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