Merge branch 'align-mastodon-conversations' into 'develop'
[akkoma] / lib / pleroma / reverse_proxy.ex
index 64c3c3a19407e4fbf6391af2a5a6a7b85d74ca6c..285d57309ea76a488ba14a51508d14daf76c3f10 100644 (file)
@@ -1,11 +1,19 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
 defmodule Pleroma.ReverseProxy do
-  @keep_req_headers ~w(accept user-agent accept-encoding cache-control if-modified-since if-none-match range)
+  alias Pleroma.HTTP
+
+  @keep_req_headers ~w(accept user-agent accept-encoding cache-control if-modified-since) ++
+                      ~w(if-unmodified-since if-none-match if-range range)
   @resp_cache_headers ~w(etag date last-modified cache-control)
   @keep_resp_headers @resp_cache_headers ++
-                       ~w(content-type content-disposition content-length accept-ranges vary)
+                       ~w(content-type content-disposition content-encoding content-range) ++
+                       ~w(accept-ranges vary)
   @default_cache_control_header "public, max-age=1209600"
   @valid_resp_codes [200, 206, 304]
-  @max_read_duration :timer.minutes(2)
+  @max_read_duration :timer.seconds(30)
   @max_body_length :infinity
   @methods ~w(GET HEAD)
 
@@ -53,10 +61,7 @@ defmodule Pleroma.ReverseProxy do
   * `http`: options for [hackney](https://github.com/benoitc/hackney).
 
   """
-  @hackney Application.get_env(:pleroma, :hackney, :hackney)
-  @httpoison Application.get_env(:pleroma, :httpoison, HTTPoison)
-
-  @default_hackney_options [{:follow_redirect, true}]
+  @default_hackney_options []
 
   @inline_content_types [
     "image/gif",
@@ -85,11 +90,13 @@ defmodule Pleroma.ReverseProxy do
           | {:redirect_on_failure, boolean()}
 
   @spec call(Plug.Conn.t(), url :: String.t(), [option()]) :: Plug.Conn.t()
-  def call(conn = %{method: method}, url, opts \\ []) when method in @methods do
+  def call(_conn, _url, _opts \\ [])
+
+  def call(conn = %{method: method}, url, opts) when method in @methods do
     hackney_opts =
       @default_hackney_options
       |> Keyword.merge(Keyword.get(opts, :http, []))
-      |> @httpoison.process_request_options()
+      |> HTTP.process_request_options()
 
     req_headers = build_req_headers(conn.req_headers, opts)
 
@@ -101,7 +108,7 @@ defmodule Pleroma.ReverseProxy do
       end
 
     with {:ok, code, headers, client} <- request(method, url, req_headers, hackney_opts),
-         :ok <- header_lenght_constraint(headers, Keyword.get(opts, :max_body_length)) do
+         :ok <- header_length_constraint(headers, Keyword.get(opts, :max_body_length)) do
       response(conn, client, url, code, headers, opts)
     else
       {:ok, code, headers} ->
@@ -139,7 +146,7 @@ defmodule Pleroma.ReverseProxy do
     Logger.debug("#{__MODULE__} #{method} #{url} #{inspect(headers)}")
     method = method |> String.downcase() |> String.to_existing_atom()
 
-    case @hackney.request(method, url, headers, "", hackney_opts) do
+    case :hackney.request(method, url, headers, "", hackney_opts) do
       {:ok, code, headers, client} when code in @valid_resp_codes ->
         {:ok, code, downcase_headers(headers), client}
 
@@ -189,7 +196,7 @@ defmodule Pleroma.ReverseProxy do
              duration,
              Keyword.get(opts, :max_read_duration, @max_read_duration)
            ),
-         {:ok, data} <- @hackney.stream_body(client),
+         {:ok, data} <- :hackney.stream_body(client),
          {:ok, duration} <- increase_read_duration(duration),
          sent_so_far = sent_so_far + byte_size(data),
          :ok <- body_size_constraint(sent_so_far, Keyword.get(opts, :max_body_size)),
@@ -225,6 +232,14 @@ defmodule Pleroma.ReverseProxy do
     end)
   end
 
+  defp get_content_type(headers) do
+    {_, content_type} =
+      List.keyfind(headers, "content-type", 0, {"content-type", "application/octet-stream"})
+
+    [content_type | _] = String.split(content_type, ";")
+    content_type
+  end
+
   defp put_resp_headers(conn, headers) do
     Enum.reduce(headers, conn, fn {k, v}, conn ->
       put_resp_header(conn, k, v)
@@ -232,50 +247,60 @@ defmodule Pleroma.ReverseProxy do
   end
 
   defp build_req_headers(headers, opts) do
-    headers =
-      headers
-      |> downcase_headers()
-      |> Enum.filter(fn {k, _} -> k in @keep_req_headers end)
-      |> (fn headers ->
-            headers = headers ++ Keyword.get(opts, :req_headers, [])
-
-            if Keyword.get(opts, :keep_user_agent, false) do
-              List.keystore(
-                headers,
-                "user-agent",
-                0,
-                {"user-agent", Pleroma.Application.user_agent()}
-              )
-            else
-              headers
-            end
-          end).()
+    headers
+    |> downcase_headers()
+    |> Enum.filter(fn {k, _} -> k in @keep_req_headers end)
+    |> (fn headers ->
+          headers = headers ++ Keyword.get(opts, :req_headers, [])
+
+          if Keyword.get(opts, :keep_user_agent, false) do
+            List.keystore(
+              headers,
+              "user-agent",
+              0,
+              {"user-agent", Pleroma.Application.user_agent()}
+            )
+          else
+            headers
+          end
+        end).()
   end
 
   defp build_resp_headers(headers, opts) do
-    headers =
-      headers
-      |> Enum.filter(fn {k, _} -> k in @keep_resp_headers end)
-      |> build_resp_cache_headers(opts)
-      |> build_resp_content_disposition_header(opts)
-      |> (fn headers -> headers ++ Keyword.get(opts, :resp_headers, []) end).()
+    headers
+    |> Enum.filter(fn {k, _} -> k in @keep_resp_headers end)
+    |> build_resp_cache_headers(opts)
+    |> build_resp_content_disposition_header(opts)
+    |> (fn headers -> headers ++ Keyword.get(opts, :resp_headers, []) end).()
   end
 
-  defp build_resp_cache_headers(headers, opts) do
+  defp build_resp_cache_headers(headers, _opts) do
     has_cache? = Enum.any?(headers, fn {k, _} -> k in @resp_cache_headers end)
-
-    if has_cache? do
-      headers
-    else
-      List.keystore(headers, "cache-control", 0, {"cache-control", @default_cache_control_header})
+    has_cache_control? = List.keymember?(headers, "cache-control", 0)
+
+    cond do
+      has_cache? && has_cache_control? ->
+        headers
+
+      has_cache? ->
+        # There's caching header present but no cache-control -- we need to explicitely override it
+        # to public as Plug defaults to "max-age=0, private, must-revalidate"
+        List.keystore(headers, "cache-control", 0, {"cache-control", "public"})
+
+      true ->
+        List.keystore(
+          headers,
+          "cache-control",
+          0,
+          {"cache-control", @default_cache_control_header}
+        )
     end
   end
 
   defp build_resp_content_disposition_header(headers, opts) do
     opt = Keyword.get(opts, :inline_content_types, @inline_content_types)
 
-    {_, content_type} =
-      List.keyfind(headers, "content-type", 0, {"content-type", "application/octect-stream"})
+    content_type = get_content_type(headers)
 
     attachment? =
       cond do
@@ -285,14 +310,32 @@ defmodule Pleroma.ReverseProxy do
       end
 
     if attachment? do
-      disposition = "attachment; filename=" <> Keyword.get(opts, :attachment_name, "attachment")
+      name =
+        try do
+          {{"content-disposition", content_disposition_string}, _} =
+            List.keytake(headers, "content-disposition", 0)
+
+          [name | _] =
+            Regex.run(
+              ~r/filename="((?:[^"\\]|\\.)*)"/u,
+              content_disposition_string || "",
+              capture: :all_but_first
+            )
+
+          name
+        rescue
+          MatchError -> Keyword.get(opts, :attachment_name, "attachment")
+        end
+
+      disposition = "attachment; filename=\"#{name}\""
+
       List.keystore(headers, "content-disposition", 0, {"content-disposition", disposition})
     else
       headers
     end
   end
 
-  defp header_lenght_constraint(headers, limit) when is_integer(limit) and limit > 0 do
+  defp header_length_constraint(headers, limit) when is_integer(limit) and limit > 0 do
     with {_, size} <- List.keyfind(headers, "content-length", 0),
          {size, _} <- Integer.parse(size),
          true <- size <= limit do
@@ -306,7 +349,7 @@ defmodule Pleroma.ReverseProxy do
     end
   end
 
-  defp header_lenght_constraint(_, _), do: :ok
+  defp header_length_constraint(_, _), do: :ok
 
   defp body_size_constraint(size, limit) when is_integer(limit) and limit > 0 and size >= limit do
     {:error, :body_too_large}
@@ -319,7 +362,6 @@ defmodule Pleroma.ReverseProxy do
     if duration > max do
       {:error, :read_duration_exceeded}
     else
-      Logger.debug("Duration #{inspect(duration)}")
       {:ok, {duration, :erlang.system_time(:millisecond)}}
     end
   end