aboutsummaryrefslogtreecommitdiff
path: root/lib/pleroma/web/media_proxy/controller.ex
blob: bb257c2622f87515cb2df5d9e222d9b5f4c84ab4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
defmodule Pleroma.Web.MediaProxy.MediaProxyController do
  use Pleroma.Web, :controller
  require Logger

  @httpoison Application.get_env(:pleroma, :httpoison)

  @max_body_length 25 * 1_048_576

  @cache_control %{
    default: "public, max-age=1209600",
    error: "public, must-revalidate, max-age=160"
  }

  # Content-types that will not be returned as content-disposition attachments
  # Override with :media_proxy, :safe_content_types in the configuration
  @safe_content_types [
    "image/gif",
    "image/jpeg",
    "image/jpg",
    "image/png",
    "image/svg+xml",
    "audio/mpeg",
    "audio/mp3",
    "video/webm",
    "video/mp4"
  ]

  def remote(conn, params = %{"sig" => sig, "url" => url}) do
    config = Application.get_env(:pleroma, :media_proxy, [])

    with true <- Keyword.get(config, :enabled, false),
         {:ok, url} <- Pleroma.Web.MediaProxy.decode_url(sig, url),
         filename <- Path.basename(URI.parse(url).path),
         true <-
           if(Map.get(params, "filename"),
             do: filename == Path.basename(conn.request_path),
             else: true
           ),
         {:ok, content_type, body} <- proxy_request(url),
         safe_content_type <-
           Enum.member?(
             Keyword.get(config, :safe_content_types, @safe_content_types),
             content_type
           ) do
      conn
      |> put_resp_content_type(content_type)
      |> set_cache_header(:default)
      |> put_resp_header(
        "content-security-policy",
        "default-src 'none'; style-src 'unsafe-inline'; media-src data:; img-src 'self' data:"
      )
      |> put_resp_header("x-xss-protection", "1; mode=block")
      |> put_resp_header("x-content-type-options", "nosniff")
      |> put_attachement_header(safe_content_type, filename)
      |> send_resp(200, body)
    else
      false ->
        send_error(conn, 404)

      {:error, :invalid_signature} ->
        send_error(conn, 403)

      {:error, {:http, _, url}} ->
        redirect_or_error(conn, url, Keyword.get(config, :redirect_on_failure, true))
    end
  end

  defp proxy_request(link) do
    headers = [
      {"user-agent",
       "Pleroma/MediaProxy; #{Pleroma.Web.base_url()} <#{
         Application.get_env(:pleroma, :instance)[:email]
       }>"}
    ]

    options =
      @httpoison.process_request_options([:insecure, {:follow_redirect, true}]) ++
        [{:pool, :default}]

    with {:ok, 200, headers, client} <- :hackney.request(:get, link, headers, "", options),
         headers = Enum.into(headers, Map.new()),
         {:ok, body} <- proxy_request_body(client),
         content_type <- proxy_request_content_type(headers, body) do
      {:ok, content_type, body}
    else
      {:ok, status, _, _} ->
        Logger.warn("MediaProxy: request failed, status #{status}, link: #{link}")
        {:error, {:http, :bad_status, link}}

      {:error, error} ->
        Logger.warn("MediaProxy: request failed, error #{inspect(error)}, link: #{link}")
        {:error, {:http, error, link}}
    end
  end

  defp set_cache_header(conn, key) do
    Plug.Conn.put_resp_header(conn, "cache-control", @cache_control[key])
  end

  defp redirect_or_error(conn, url, true), do: redirect(conn, external: url)
  defp redirect_or_error(conn, url, _), do: send_error(conn, 502, "Media proxy error: " <> url)

  defp send_error(conn, code, body \\ "") do
    conn
    |> set_cache_header(:error)
    |> send_resp(code, body)
  end

  defp proxy_request_body(client), do: proxy_request_body(client, <<>>)

  defp proxy_request_body(client, body) when byte_size(body) < @max_body_length do
    case :hackney.stream_body(client) do
      {:ok, data} -> proxy_request_body(client, <<body::binary, data::binary>>)
      :done -> {:ok, body}
      {:error, reason} -> {:error, reason}
    end
  end

  defp proxy_request_body(client, _) do
    :hackney.close(client)
    {:error, :body_too_large}
  end

  # TODO: the body is passed here as well because some hosts do not provide a content-type.
  # At some point we may want to use magic numbers to discover the content-type and reply a proper one.
  defp proxy_request_content_type(headers, _body) do
    headers["Content-Type"] || headers["content-type"] || "application/octet-stream"
  end

  defp put_attachement_header(conn, true, _), do: conn

  defp put_attachement_header(conn, false, filename) do
    put_resp_header(conn, "content-disposition", "attachment; filename='#{filename}'")
  end
end