logo

pleroma

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

reverse_proxy_test.exs (10214B)


  1. # Pleroma: A lightweight social networking server
  2. # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
  3. # SPDX-License-Identifier: AGPL-3.0-only
  4. defmodule Pleroma.ReverseProxyTest do
  5. use Pleroma.Web.ConnCase, async: true
  6. import ExUnit.CaptureLog
  7. import Mox
  8. alias Pleroma.ReverseProxy
  9. alias Pleroma.ReverseProxy.ClientMock
  10. alias Plug.Conn
  11. setup_all do
  12. {:ok, _} = Registry.start_link(keys: :unique, name: ClientMock)
  13. :ok
  14. end
  15. setup :verify_on_exit!
  16. defp user_agent_mock(user_agent, invokes) do
  17. json = Jason.encode!(%{"user-agent": user_agent})
  18. ClientMock
  19. |> expect(:request, fn :get, url, _, _, _ ->
  20. Registry.register(ClientMock, url, 0)
  21. {:ok, 200,
  22. [
  23. {"content-type", "application/json"},
  24. {"content-length", byte_size(json) |> to_string()}
  25. ], %{url: url}}
  26. end)
  27. |> expect(:stream_body, invokes, fn %{url: url} = client ->
  28. case Registry.lookup(ClientMock, url) do
  29. [{_, 0}] ->
  30. Registry.update_value(ClientMock, url, &(&1 + 1))
  31. {:ok, json, client}
  32. [{_, 1}] ->
  33. Registry.unregister(ClientMock, url)
  34. :done
  35. end
  36. end)
  37. end
  38. describe "reverse proxy" do
  39. test "do not track successful request", %{conn: conn} do
  40. user_agent_mock("hackney/1.15.1", 2)
  41. url = "/success"
  42. conn = ReverseProxy.call(conn, url)
  43. assert conn.status == 200
  44. assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, nil}
  45. end
  46. end
  47. describe "user-agent" do
  48. test "don't keep", %{conn: conn} do
  49. user_agent_mock("hackney/1.15.1", 2)
  50. conn = ReverseProxy.call(conn, "/user-agent")
  51. assert json_response(conn, 200) == %{"user-agent" => "hackney/1.15.1"}
  52. end
  53. test "keep", %{conn: conn} do
  54. user_agent_mock(Pleroma.Application.user_agent(), 2)
  55. conn = ReverseProxy.call(conn, "/user-agent-keep", keep_user_agent: true)
  56. assert json_response(conn, 200) == %{"user-agent" => Pleroma.Application.user_agent()}
  57. end
  58. end
  59. test "closed connection", %{conn: conn} do
  60. ClientMock
  61. |> expect(:request, fn :get, "/closed", _, _, _ -> {:ok, 200, [], %{}} end)
  62. |> expect(:stream_body, fn _ -> {:error, :closed} end)
  63. |> expect(:close, fn _ -> :ok end)
  64. conn = ReverseProxy.call(conn, "/closed")
  65. assert conn.halted
  66. end
  67. defp stream_mock(invokes, with_close? \\ false) do
  68. ClientMock
  69. |> expect(:request, fn :get, "/stream-bytes/" <> length, _, _, _ ->
  70. Registry.register(ClientMock, "/stream-bytes/" <> length, 0)
  71. {:ok, 200, [{"content-type", "application/octet-stream"}],
  72. %{url: "/stream-bytes/" <> length}}
  73. end)
  74. |> expect(:stream_body, invokes, fn %{url: "/stream-bytes/" <> length} = client ->
  75. max = String.to_integer(length)
  76. case Registry.lookup(ClientMock, "/stream-bytes/" <> length) do
  77. [{_, current}] when current < max ->
  78. Registry.update_value(
  79. ClientMock,
  80. "/stream-bytes/" <> length,
  81. &(&1 + 10)
  82. )
  83. {:ok, "0123456789", client}
  84. [{_, ^max}] ->
  85. Registry.unregister(ClientMock, "/stream-bytes/" <> length)
  86. :done
  87. end
  88. end)
  89. if with_close? do
  90. expect(ClientMock, :close, fn _ -> :ok end)
  91. end
  92. end
  93. describe "max_body" do
  94. test "length returns error if content-length more than option", %{conn: conn} do
  95. user_agent_mock("hackney/1.15.1", 0)
  96. assert capture_log(fn ->
  97. ReverseProxy.call(conn, "/huge-file", max_body_length: 4)
  98. end) =~
  99. "[error] Elixir.Pleroma.ReverseProxy: request to \"/huge-file\" failed: :body_too_large"
  100. assert {:ok, true} == Cachex.get(:failed_proxy_url_cache, "/huge-file")
  101. assert capture_log(fn ->
  102. ReverseProxy.call(conn, "/huge-file", max_body_length: 4)
  103. end) == ""
  104. end
  105. test "max_body_length returns error if streaming body more than that option", %{conn: conn} do
  106. stream_mock(3, true)
  107. assert capture_log(fn ->
  108. ReverseProxy.call(conn, "/stream-bytes/50", max_body_length: 30)
  109. end) =~
  110. "[warn] Elixir.Pleroma.ReverseProxy request to /stream-bytes/50 failed while reading/chunking: :body_too_large"
  111. end
  112. end
  113. describe "HEAD requests" do
  114. test "common", %{conn: conn} do
  115. ClientMock
  116. |> expect(:request, fn :head, "/head", _, _, _ ->
  117. {:ok, 200, [{"content-type", "text/html; charset=utf-8"}]}
  118. end)
  119. conn = ReverseProxy.call(Map.put(conn, :method, "HEAD"), "/head")
  120. assert html_response(conn, 200) == ""
  121. end
  122. end
  123. defp error_mock(status) when is_integer(status) do
  124. ClientMock
  125. |> expect(:request, fn :get, "/status/" <> _, _, _, _ ->
  126. {:error, status}
  127. end)
  128. end
  129. describe "returns error on" do
  130. test "500", %{conn: conn} do
  131. error_mock(500)
  132. url = "/status/500"
  133. capture_log(fn -> ReverseProxy.call(conn, url) end) =~
  134. "[error] Elixir.Pleroma.ReverseProxy: request to /status/500 failed with HTTP status 500"
  135. assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true}
  136. {:ok, ttl} = Cachex.ttl(:failed_proxy_url_cache, url)
  137. assert ttl <= 60_000
  138. end
  139. test "400", %{conn: conn} do
  140. error_mock(400)
  141. url = "/status/400"
  142. capture_log(fn -> ReverseProxy.call(conn, url) end) =~
  143. "[error] Elixir.Pleroma.ReverseProxy: request to /status/400 failed with HTTP status 400"
  144. assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true}
  145. assert Cachex.ttl(:failed_proxy_url_cache, url) == {:ok, nil}
  146. end
  147. test "403", %{conn: conn} do
  148. error_mock(403)
  149. url = "/status/403"
  150. capture_log(fn ->
  151. ReverseProxy.call(conn, url, failed_request_ttl: :timer.seconds(120))
  152. end) =~
  153. "[error] Elixir.Pleroma.ReverseProxy: request to /status/403 failed with HTTP status 403"
  154. {:ok, ttl} = Cachex.ttl(:failed_proxy_url_cache, url)
  155. assert ttl > 100_000
  156. end
  157. test "204", %{conn: conn} do
  158. url = "/status/204"
  159. expect(ClientMock, :request, fn :get, _url, _, _, _ -> {:ok, 204, [], %{}} end)
  160. capture_log(fn ->
  161. conn = ReverseProxy.call(conn, url)
  162. assert conn.resp_body == "Request failed: No Content"
  163. assert conn.halted
  164. end) =~
  165. "[error] Elixir.Pleroma.ReverseProxy: request to \"/status/204\" failed with HTTP status 204"
  166. assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true}
  167. assert Cachex.ttl(:failed_proxy_url_cache, url) == {:ok, nil}
  168. end
  169. end
  170. test "streaming", %{conn: conn} do
  171. stream_mock(21)
  172. conn = ReverseProxy.call(conn, "/stream-bytes/200")
  173. assert conn.state == :chunked
  174. assert byte_size(conn.resp_body) == 200
  175. assert Conn.get_resp_header(conn, "content-type") == ["application/octet-stream"]
  176. end
  177. defp headers_mock(_) do
  178. ClientMock
  179. |> expect(:request, fn :get, "/headers", headers, _, _ ->
  180. Registry.register(ClientMock, "/headers", 0)
  181. {:ok, 200, [{"content-type", "application/json"}], %{url: "/headers", headers: headers}}
  182. end)
  183. |> expect(:stream_body, 2, fn %{url: url, headers: headers} = client ->
  184. case Registry.lookup(ClientMock, url) do
  185. [{_, 0}] ->
  186. Registry.update_value(ClientMock, url, &(&1 + 1))
  187. headers = for {k, v} <- headers, into: %{}, do: {String.capitalize(k), v}
  188. {:ok, Jason.encode!(%{headers: headers}), client}
  189. [{_, 1}] ->
  190. Registry.unregister(ClientMock, url)
  191. :done
  192. end
  193. end)
  194. :ok
  195. end
  196. describe "keep request headers" do
  197. setup [:headers_mock]
  198. test "header passes", %{conn: conn} do
  199. conn =
  200. Conn.put_req_header(
  201. conn,
  202. "accept",
  203. "text/html"
  204. )
  205. |> ReverseProxy.call("/headers")
  206. %{"headers" => headers} = json_response(conn, 200)
  207. assert headers["Accept"] == "text/html"
  208. end
  209. test "header is filtered", %{conn: conn} do
  210. conn =
  211. Conn.put_req_header(
  212. conn,
  213. "accept-language",
  214. "en-US"
  215. )
  216. |> ReverseProxy.call("/headers")
  217. %{"headers" => headers} = json_response(conn, 200)
  218. refute headers["Accept-Language"]
  219. end
  220. end
  221. test "returns 400 on non GET, HEAD requests", %{conn: conn} do
  222. conn = ReverseProxy.call(Map.put(conn, :method, "POST"), "/ip")
  223. assert conn.status == 400
  224. end
  225. describe "cache resp headers" do
  226. test "add cache-control", %{conn: conn} do
  227. ClientMock
  228. |> expect(:request, fn :get, "/cache", _, _, _ ->
  229. {:ok, 200, [{"ETag", "some ETag"}], %{}}
  230. end)
  231. |> expect(:stream_body, fn _ -> :done end)
  232. conn = ReverseProxy.call(conn, "/cache")
  233. assert {"cache-control", "public, max-age=1209600"} in conn.resp_headers
  234. end
  235. end
  236. defp disposition_headers_mock(headers) do
  237. ClientMock
  238. |> expect(:request, fn :get, "/disposition", _, _, _ ->
  239. Registry.register(ClientMock, "/disposition", 0)
  240. {:ok, 200, headers, %{url: "/disposition"}}
  241. end)
  242. |> expect(:stream_body, 2, fn %{url: "/disposition"} = client ->
  243. case Registry.lookup(ClientMock, "/disposition") do
  244. [{_, 0}] ->
  245. Registry.update_value(ClientMock, "/disposition", &(&1 + 1))
  246. {:ok, "", client}
  247. [{_, 1}] ->
  248. Registry.unregister(ClientMock, "/disposition")
  249. :done
  250. end
  251. end)
  252. end
  253. describe "response content disposition header" do
  254. test "not atachment", %{conn: conn} do
  255. disposition_headers_mock([
  256. {"content-type", "image/gif"},
  257. {"content-length", "0"}
  258. ])
  259. conn = ReverseProxy.call(conn, "/disposition")
  260. assert {"content-type", "image/gif"} in conn.resp_headers
  261. end
  262. test "with content-disposition header", %{conn: conn} do
  263. disposition_headers_mock([
  264. {"content-disposition", "attachment; filename=\"filename.jpg\""},
  265. {"content-length", "0"}
  266. ])
  267. conn = ReverseProxy.call(conn, "/disposition")
  268. assert {"content-disposition", "attachment; filename=\"filename.jpg\""} in conn.resp_headers
  269. end
  270. end
  271. end