bda5b36edcea2341d29052567b0b4638d3d41660
[anni] / lib / pleroma / web / media_proxy / media_proxy_controller.ex
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
5 defmodule Pleroma.Web.MediaProxy.MediaProxyController do
6   use Pleroma.Web, :controller
7
8   alias Pleroma.Config
9   alias Pleroma.Helpers.MediaHelper
10   alias Pleroma.Helpers.UriHelper
11   alias Pleroma.ReverseProxy
12   alias Pleroma.Web.MediaProxy
13   alias Plug.Conn
14
15   plug(:sandbox)
16
17   def remote(conn, %{"sig" => sig64, "url" => url64}) do
18     with {_, true} <- {:enabled, MediaProxy.enabled?()},
19          {:ok, url} <- MediaProxy.decode_url(sig64, url64),
20          {_, false} <- {:in_banned_urls, MediaProxy.in_banned_urls(url)},
21          :ok <- MediaProxy.verify_request_path_and_url(conn, url) do
22       ReverseProxy.call(conn, url, media_proxy_opts())
23     else
24       {:enabled, false} ->
25         send_resp(conn, 404, Conn.Status.reason_phrase(404))
26
27       {:in_banned_urls, true} ->
28         send_resp(conn, 404, Conn.Status.reason_phrase(404))
29
30       {:error, :invalid_signature} ->
31         send_resp(conn, 403, Conn.Status.reason_phrase(403))
32
33       {:wrong_filename, filename} ->
34         redirect(conn, external: MediaProxy.build_url(sig64, url64, filename))
35     end
36   end
37
38   def preview(%Conn{} = conn, %{"sig" => sig64, "url" => url64}) do
39     with {_, true} <- {:enabled, MediaProxy.preview_enabled?()},
40          {:ok, url} <- MediaProxy.decode_url(sig64, url64),
41          :ok <- MediaProxy.verify_request_path_and_url(conn, url) do
42       handle_preview(conn, url)
43     else
44       {:enabled, false} ->
45         send_resp(conn, 404, Conn.Status.reason_phrase(404))
46
47       {:error, :invalid_signature} ->
48         send_resp(conn, 403, Conn.Status.reason_phrase(403))
49
50       {:wrong_filename, filename} ->
51         redirect(conn, external: MediaProxy.build_preview_url(sig64, url64, filename))
52     end
53   end
54
55   defp handle_preview(conn, url) do
56     media_proxy_url = MediaProxy.url(url)
57
58     with {:ok, %{status: status} = head_response} when status in 200..299 <-
59            Pleroma.HTTP.request("HEAD", media_proxy_url, [], [], pool: :media) do
60       content_type = Tesla.get_header(head_response, "content-type")
61       content_length = Tesla.get_header(head_response, "content-length")
62       content_length = content_length && String.to_integer(content_length)
63       static = conn.params["static"] in ["true", true]
64
65       cond do
66         static and content_type == "image/gif" ->
67           handle_jpeg_preview(conn, media_proxy_url)
68
69         static ->
70           drop_static_param_and_redirect(conn)
71
72         content_type == "image/gif" ->
73           redirect(conn, external: media_proxy_url)
74
75         min_content_length_for_preview() > 0 and content_length > 0 and
76             content_length < min_content_length_for_preview() ->
77           redirect(conn, external: media_proxy_url)
78
79         true ->
80           handle_preview(content_type, conn, media_proxy_url)
81       end
82     else
83       # If HEAD failed, redirecting to media proxy URI doesn't make much sense; returning an error
84       {_, %{status: status}} ->
85         send_resp(conn, :failed_dependency, "Can't fetch HTTP headers (HTTP #{status}).")
86
87       {:error, :recv_response_timeout} ->
88         send_resp(conn, :failed_dependency, "HEAD request timeout.")
89
90       _ ->
91         send_resp(conn, :failed_dependency, "Can't fetch HTTP headers.")
92     end
93   end
94
95   defp handle_preview("image/png" <> _ = _content_type, conn, media_proxy_url) do
96     handle_png_preview(conn, media_proxy_url)
97   end
98
99   defp handle_preview("image/" <> _ = _content_type, conn, media_proxy_url) do
100     handle_jpeg_preview(conn, media_proxy_url)
101   end
102
103   defp handle_preview("video/" <> _ = _content_type, conn, media_proxy_url) do
104     handle_video_preview(conn, media_proxy_url)
105   end
106
107   defp handle_preview(_unsupported_content_type, conn, media_proxy_url) do
108     fallback_on_preview_error(conn, media_proxy_url)
109   end
110
111   defp handle_png_preview(conn, media_proxy_url) do
112     quality = Config.get!([:media_preview_proxy, :image_quality])
113     {thumbnail_max_width, thumbnail_max_height} = thumbnail_max_dimensions()
114
115     with {:ok, thumbnail_binary} <-
116            MediaHelper.image_resize(
117              media_proxy_url,
118              %{
119                max_width: thumbnail_max_width,
120                max_height: thumbnail_max_height,
121                quality: quality,
122                format: "png"
123              }
124            ) do
125       conn
126       |> put_preview_response_headers(["image/png", "preview.png"])
127       |> send_resp(200, thumbnail_binary)
128     else
129       _ ->
130         fallback_on_preview_error(conn, media_proxy_url)
131     end
132   end
133
134   defp handle_jpeg_preview(conn, media_proxy_url) do
135     quality = Config.get!([:media_preview_proxy, :image_quality])
136     {thumbnail_max_width, thumbnail_max_height} = thumbnail_max_dimensions()
137
138     with {:ok, thumbnail_binary} <-
139            MediaHelper.image_resize(
140              media_proxy_url,
141              %{max_width: thumbnail_max_width, max_height: thumbnail_max_height, quality: quality}
142            ) do
143       conn
144       |> put_preview_response_headers()
145       |> send_resp(200, thumbnail_binary)
146     else
147       _ ->
148         fallback_on_preview_error(conn, media_proxy_url)
149     end
150   end
151
152   defp handle_video_preview(conn, media_proxy_url) do
153     with {:ok, thumbnail_binary} <-
154            MediaHelper.video_framegrab(media_proxy_url) do
155       conn
156       |> put_preview_response_headers()
157       |> send_resp(200, thumbnail_binary)
158     else
159       _ ->
160         fallback_on_preview_error(conn, media_proxy_url)
161     end
162   end
163
164   defp drop_static_param_and_redirect(conn) do
165     uri_without_static_param =
166       conn
167       |> current_url()
168       |> UriHelper.modify_uri_params(%{}, ["static"])
169
170     redirect(conn, external: uri_without_static_param)
171   end
172
173   defp fallback_on_preview_error(conn, media_proxy_url) do
174     redirect(conn, external: media_proxy_url)
175   end
176
177   defp put_preview_response_headers(
178          conn,
179          [content_type, filename] = _content_info \\ ["image/jpeg", "preview.jpg"]
180        ) do
181     conn
182     |> put_resp_header("content-type", content_type)
183     |> put_resp_header("content-disposition", "inline; filename=\"#{filename}\"")
184     |> put_resp_header("cache-control", ReverseProxy.default_cache_control_header())
185   end
186
187   defp thumbnail_max_dimensions do
188     config = media_preview_proxy_config()
189
190     thumbnail_max_width = Keyword.fetch!(config, :thumbnail_max_width)
191     thumbnail_max_height = Keyword.fetch!(config, :thumbnail_max_height)
192
193     {thumbnail_max_width, thumbnail_max_height}
194   end
195
196   defp min_content_length_for_preview do
197     Keyword.get(media_preview_proxy_config(), :min_content_length, 0)
198   end
199
200   defp media_preview_proxy_config do
201     Config.get!([:media_preview_proxy])
202   end
203
204   defp media_proxy_opts do
205     Config.get([:media_proxy, :proxy_opts], [])
206   end
207
208   defp sandbox(conn, _params) do
209     conn
210     |> merge_resp_headers([{"content-security-policy", "sandbox;"}])
211   end
212 end