logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma git clone https://hacktivis.me/git/pleroma.git

reverse_proxy_test.exs (9985B)


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