# Pleroma: A lightweight social networking server # Copyright © 2017-2021 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.ReverseProxyTest do use Pleroma.Web.ConnCase import ExUnit.CaptureLog alias Pleroma.ReverseProxy alias Plug.Conn describe "reverse proxy" do test "do not track successful request", %{conn: conn} do url = "/success" Tesla.Mock.mock(fn %{url: ^url} -> %Tesla.Env{ status: 200, body: "" } end) conn = ReverseProxy.call(conn, url) assert response(conn, 200) assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, nil} end test "use Pleroma's user agent in the request; don't pass the client's", %{conn: conn} do clear_config([:http, :send_user_agent], true) # Mock will fail if the client's user agent isn't filtered wanted_headers = [{"user-agent", Pleroma.Application.user_agent()}] Tesla.Mock.mock(fn %{url: "/user-agent", headers: ^wanted_headers} -> %Tesla.Env{ status: 200, body: "" } end) conn = conn |> Plug.Conn.put_req_header("user-agent", "fake/1.0") |> ReverseProxy.call("/user-agent") assert response(conn, 200) end end describe "max_body" do test "length returns error if content-length more than option", %{conn: conn} do Tesla.Mock.mock(fn %{url: "/huge-file"} -> %Tesla.Env{ status: 200, headers: [{"content-length", "100"}], body: "This body is too large." } end) assert capture_log(fn -> ReverseProxy.call(conn, "/huge-file", max_body_length: 4) end) =~ "[error] Elixir.Pleroma.ReverseProxy: request to \"/huge-file\" failed: :body_too_large" assert {:ok, true} == Cachex.get(:failed_proxy_url_cache, "/huge-file") assert capture_log(fn -> ReverseProxy.call(conn, "/huge-file", max_body_length: 4) end) == "" end end describe "HEAD requests" do test "common", %{conn: conn} do Tesla.Mock.mock(fn %{method: :head, url: "/head"} -> %Tesla.Env{ status: 200, headers: [{"content-type", "text/html; charset=utf-8"}], body: "" } end) conn = ReverseProxy.call(Map.put(conn, :method, "HEAD"), "/head") assert html_response(conn, 200) == "" end end describe "returns error on" do test "500", %{conn: conn} do url = "/status/500" Tesla.Mock.mock(fn %{url: ^url} -> %Tesla.Env{ status: 500, body: "" } end) capture_log(fn -> ReverseProxy.call(conn, url) end) =~ "[error] Elixir.Pleroma.ReverseProxy: request to /status/500 failed with HTTP status 500" assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true} {:ok, ttl} = Cachex.ttl(:failed_proxy_url_cache, url) assert ttl <= 60_000 end test "400", %{conn: conn} do url = "/status/400" Tesla.Mock.mock(fn %{url: ^url} -> %Tesla.Env{ status: 400, body: "" } end) capture_log(fn -> ReverseProxy.call(conn, url) end) =~ "[error] Elixir.Pleroma.ReverseProxy: request to /status/400 failed with HTTP status 400" assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true} assert Cachex.ttl(:failed_proxy_url_cache, url) == {:ok, nil} end test "403", %{conn: conn} do url = "/status/403" Tesla.Mock.mock(fn %{url: ^url} -> %Tesla.Env{ status: 403, body: "" } end) capture_log(fn -> ReverseProxy.call(conn, url, failed_request_ttl: :timer.seconds(120)) end) =~ "[error] Elixir.Pleroma.ReverseProxy: request to /status/403 failed with HTTP status 403" {:ok, ttl} = Cachex.ttl(:failed_proxy_url_cache, url) assert ttl > 100_000 end end describe "keep request headers" do test "header passes", %{conn: conn} do Tesla.Mock.mock(fn %{url: "/headers"} -> %Tesla.Env{ status: 200, body: "" } end) conn = Conn.put_req_header( conn, "accept", "text/html" ) |> ReverseProxy.call("/headers") assert response(conn, 200) assert {"accept", "text/html"} in conn.req_headers end test "header is filtered", %{conn: conn} do # Mock will fail if the accept-language header isn't filtered wanted_headers = [{"accept-encoding", "*"}] Tesla.Mock.mock(fn %{url: "/headers", headers: ^wanted_headers} -> %Tesla.Env{ status: 200, body: "" } end) conn = conn |> Conn.put_req_header("accept-language", "en-US") |> Conn.put_req_header("accept-encoding", "*") |> ReverseProxy.call("/headers") assert response(conn, 200) end end test "returns 400 on non GET, HEAD requests", %{conn: conn} do Tesla.Mock.mock(fn %{url: "/ip"} -> %Tesla.Env{ status: 200, body: "" } end) conn = ReverseProxy.call(Map.put(conn, :method, "POST"), "/ip") assert response(conn, 400) end describe "cache resp headers not filtered" do test "add cache-control", %{conn: conn} do Tesla.Mock.mock(fn %{url: "/cache"} -> %Tesla.Env{ status: 200, headers: [ {"cache-control", "public, max-age=1209600"}, {"etag", "some ETag"}, {"expires", "Wed, 21 Oct 2015 07:28:00 GMT"} ], body: "" } end) conn = ReverseProxy.call(conn, "/cache") assert {"cache-control", "public, max-age=1209600"} in conn.resp_headers assert {"etag", "some ETag"} in conn.resp_headers assert {"expires", "Wed, 21 Oct 2015 07:28:00 GMT"} in conn.resp_headers end end describe "response content disposition header" do test "not attachment", %{conn: conn} do Tesla.Mock.mock(fn %{url: "/disposition"} -> %Tesla.Env{ status: 200, headers: [ {"content-type", "image/gif"}, {"content-length", "0"} ], body: "" } end) conn = ReverseProxy.call(conn, "/disposition") assert {"content-type", "image/gif"} in conn.resp_headers end test "with content-disposition header", %{conn: conn} do Tesla.Mock.mock(fn %{url: "/disposition"} -> %Tesla.Env{ status: 200, headers: [ {"content-disposition", "attachment; filename=\"filename.jpg\""}, {"content-length", "0"} ], body: "" } end) conn = ReverseProxy.call(conn, "/disposition") assert {"content-disposition", "attachment; filename=\"filename.jpg\""} in conn.resp_headers end end end