Fork of Pleroma with site-specific changes and feature branches https://git.pleroma.social/pleroma/pleroma
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

345 lines
11KB

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