Initial implementation of image preview proxy. Media proxy tests refactoring.
[akkoma] / lib / pleroma / web / media_proxy / media_proxy_controller.ex
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
5 defmodule Pleroma.Web.MediaProxy.MediaProxyController do
6 use Pleroma.Web, :controller
7
8 alias Pleroma.Config
9 alias Pleroma.Helpers.MogrifyHelper
10 alias Pleroma.ReverseProxy
11 alias Pleroma.Web.MediaProxy
12
13 @default_proxy_opts [max_body_length: 25 * 1_048_576, http: [follow_redirect: true]]
14
15 def remote(conn, %{"sig" => sig64, "url" => url64} = params) do
16 with config <- Config.get([:media_proxy], []),
17 {_, true} <- {:enabled, Keyword.get(config, :enabled, false)},
18 {:ok, url} <- MediaProxy.decode_url(sig64, url64),
19 :ok <- MediaProxy.filename_matches(params, conn.request_path, url) do
20 ReverseProxy.call(conn, url, Keyword.get(config, :proxy_opts, @default_proxy_opts))
21 else
22 {:enabled, false} ->
23 send_resp(conn, 404, Plug.Conn.Status.reason_phrase(404))
24
25 {:error, :invalid_signature} ->
26 send_resp(conn, 403, Plug.Conn.Status.reason_phrase(403))
27
28 {:wrong_filename, filename} ->
29 redirect(conn, external: MediaProxy.build_url(sig64, url64, filename))
30 end
31 end
32
33 def preview(conn, %{"sig" => sig64, "url" => url64} = params) do
34 with {_, true} <- {:enabled, Config.get([:media_preview_proxy, :enabled], false)},
35 {:ok, url} <- MediaProxy.decode_url(sig64, url64),
36 :ok <- MediaProxy.filename_matches(params, conn.request_path, url) do
37 handle_preview(conn, url)
38 else
39 {:enabled, false} ->
40 send_resp(conn, 404, Plug.Conn.Status.reason_phrase(404))
41
42 {:error, :invalid_signature} ->
43 send_resp(conn, 403, Plug.Conn.Status.reason_phrase(403))
44
45 {:wrong_filename, filename} ->
46 redirect(conn, external: MediaProxy.build_preview_url(sig64, url64, filename))
47 end
48 end
49
50 defp handle_preview(conn, url) do
51 with {:ok, %{status: status} = head_response} when status in 200..299 <- Tesla.head(url),
52 {_, true} <- {:acceptable_content_length, acceptable_body_length?(head_response)} do
53 content_type = Tesla.get_header(head_response, "content-type")
54 handle_preview(content_type, conn, url)
55 else
56 {_, %{status: status}} ->
57 send_resp(conn, :failed_dependency, "Can't fetch HTTP headers (HTTP #{status}).")
58
59 {:acceptable_content_length, false} ->
60 send_resp(conn, :unprocessable_entity, "Source file size exceeds limit.")
61 end
62 end
63
64 defp handle_preview("image/" <> _, %{params: params} = conn, url) do
65 with {:ok, %{status: status, body: body}} when status in 200..299 <- Tesla.get(url),
66 {:ok, path} <- MogrifyHelper.store_as_temporary_file(url, body),
67 resize_dimensions <-
68 Map.get(
69 params,
70 "limit_dimensions",
71 Config.get([:media_preview_proxy, :limit_dimensions])
72 ),
73 %Mogrify.Image{} <- MogrifyHelper.in_place_resize_to_limit(path, resize_dimensions) do
74 send_file(conn, 200, path)
75 else
76 {_, %{status: _}} ->
77 send_resp(conn, :failed_dependency, "Can't fetch the image.")
78
79 _ ->
80 send_resp(conn, :failed_dependency, "Can't handle image preview.")
81 end
82 end
83
84 defp handle_preview(content_type, conn, _url) do
85 send_resp(conn, :unprocessable_entity, "Unsupported content type: #{content_type}.")
86 end
87
88 defp acceptable_body_length?(head_response) do
89 max_body_length = Config.get([:media_preview_proxy, :max_body_length], nil)
90 content_length = Tesla.get_header(head_response, "content-length")
91 content_length = with {int, _} <- Integer.parse(content_length), do: int
92
93 content_length == :error or
94 max_body_length in [nil, :infinity] or
95 content_length <= max_body_length
96 end
97 end