Merge remote-tracking branch 'remotes/origin/develop' into 2168-media-preview-proxy
authorIvan Tashkinov <ivantashkinov@gmail.com>
Thu, 2 Jul 2020 13:36:54 +0000 (16:36 +0300)
committerIvan Tashkinov <ivantashkinov@gmail.com>
Thu, 2 Jul 2020 13:36:54 +0000 (16:36 +0300)
# Conflicts:
# config/config.exs
# lib/pleroma/web/media_proxy/media_proxy.ex
# lib/pleroma/web/media_proxy/media_proxy_controller.ex

13 files changed:
.gitlab-ci.yml
config/config.exs
lib/pleroma/exec.ex [new file with mode: 0644]
lib/pleroma/helpers/media_helper.ex [new file with mode: 0644]
lib/pleroma/reverse_proxy/reverse_proxy.ex
lib/pleroma/web/mastodon_api/views/status_view.ex
lib/pleroma/web/media_proxy/media_proxy.ex
lib/pleroma/web/media_proxy/media_proxy_controller.ex
lib/pleroma/web/router.ex
mix.exs
mix.lock
test/exec_test.exs [new file with mode: 0644]
test/web/media_proxy/media_proxy_test.exs

index b4bd59b43a32ddad040b3df83d746902e2c6060c..5c12647a00f2b6afce42f9a5f0456f7fd962637a 100644 (file)
@@ -6,6 +6,8 @@ variables: &global_variables
   POSTGRES_PASSWORD: postgres
   DB_HOST: postgres
   MIX_ENV: test
+  SHELL: /bin/sh
+  USER: root
 
 cache: &global_cache_policy
   key: ${CI_COMMIT_REF_SLUG}
index 9b550920cb144e469eda74394f67079e133cce37..c8b6c7fad0ffe581ccaed93cb770f99e9bc59904 100644 (file)
@@ -398,6 +398,8 @@ config :pleroma, :media_proxy,
   proxy_opts: [
     redirect_on_failure: false,
     max_body_length: 25 * 1_048_576,
+    # Note: max_read_duration defaults to Pleroma.ReverseProxy.max_read_duration_default/1
+    max_read_duration: 30_000,
     http: [
       follow_redirect: true,
       pool: :media
@@ -412,6 +414,16 @@ config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Http,
 
 config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Script, script_path: nil
 
+# Note: media preview proxy depends on media proxy to be enabled
+config :pleroma, :media_preview_proxy,
+  enabled: false,
+  thumbnail_max_width: 400,
+  thumbnail_max_height: 200,
+  proxy_opts: [
+    head_request_max_read_duration: 5_000,
+    max_read_duration: 10_000
+  ]
+
 config :pleroma, :chat, enabled: true
 
 config :phoenix, :format_encoders, json: Jason
@@ -705,6 +717,10 @@ config :tzdata, :http_client, Pleroma.HTTP.Tzdata
 
 config :ex_aws, http_client: Pleroma.HTTP.ExAws
 
+config :pleroma, :exexec,
+  root_mode: false,
+  options: %{}
+
 # Import environment specific config. This must remain at the bottom
 # of this file so it overrides the configuration defined above.
 import_config "#{Mix.env()}.exs"
diff --git a/lib/pleroma/exec.ex b/lib/pleroma/exec.ex
new file mode 100644 (file)
index 0000000..1b088d3
--- /dev/null
@@ -0,0 +1,38 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Exec do
+  @moduledoc "Pleroma wrapper around Exexec commands."
+
+  alias Pleroma.Config
+
+  def ensure_started(options_overrides \\ %{}) do
+    options =
+      if Config.get([:exexec, :root_mode]) || System.get_env("USER") == "root" do
+        # Note: running as `root` is discouraged (yet Gitlab CI does that by default)
+        %{root: true, user: "root", limit_users: ["root"]}
+      else
+        %{}
+      end
+
+    options =
+      options
+      |> Map.merge(Config.get([:exexec, :options], %{}))
+      |> Map.merge(options_overrides)
+
+    with {:error, {:already_started, pid}} <- Exexec.start(options) do
+      {:ok, pid}
+    end
+  end
+
+  def run(cmd, options \\ %{}) do
+    ensure_started()
+    Exexec.run(cmd, options)
+  end
+
+  def cmd(cmd, options \\ %{}) do
+    options = Map.merge(%{sync: true, stdout: true}, options)
+    run(cmd, options)
+  end
+end
diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex
new file mode 100644 (file)
index 0000000..ecd2345
--- /dev/null
@@ -0,0 +1,25 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Helpers.MediaHelper do
+  @moduledoc """
+  Handles common media-related operations.
+  """
+
+  def ffmpeg_resize_remote(uri, %{max_width: max_width, max_height: max_height}) do
+    cmd = ~s"""
+    curl -L "#{uri}" |
+    ffmpeg -i pipe:0 -f lavfi -i color=c=white \
+      -filter_complex "[0:v] scale='min(#{max_width},iw)':'min(#{max_height},ih)': \
+        force_original_aspect_ratio=decrease [scaled]; \
+        [1][scaled] scale2ref [bg][img]; [bg] setsar=1 [bg]; [bg][img] overlay=shortest=1" \
+      -f image2 -vcodec mjpeg -frames:v 1 pipe:1 | \
+    cat
+    """
+
+    with {:ok, [stdout: stdout_list]} <- Pleroma.Exec.cmd(cmd) do
+      {:ok, Enum.join(stdout_list)}
+    end
+  end
+end
index 4bbeb493cab5c9b0b41b8028d7ffe08796ed2b4a..aeaf9bd399dd055d82e82d794b8b2a898ae51b01 100644 (file)
@@ -16,6 +16,8 @@ defmodule Pleroma.ReverseProxy do
   @failed_request_ttl :timer.seconds(60)
   @methods ~w(GET HEAD)
 
+  def max_read_duration_default, do: @max_read_duration
+
   @moduledoc """
   A reverse proxy.
 
@@ -370,6 +372,8 @@ defmodule Pleroma.ReverseProxy do
 
   defp body_size_constraint(_, _), do: :ok
 
+  defp check_read_duration(nil = _duration, max), do: check_read_duration(@max_read_duration, max)
+
   defp check_read_duration(duration, max)
        when is_integer(duration) and is_integer(max) and max > 0 do
     if duration > max do
index 6ee17f4dd05e3ad484db6c018500d2a96dc836d8..00d45bcd410afff54449318984e7d78789ba9ce6 100644 (file)
@@ -412,6 +412,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
     [attachment_url | _] = attachment["url"]
     media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
     href = attachment_url["href"] |> MediaProxy.url()
+    href_preview = attachment_url["href"] |> MediaProxy.preview_url()
 
     type =
       cond do
@@ -427,7 +428,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
       id: to_string(attachment["id"] || hash_id),
       url: href,
       remote_url: href,
-      preview_url: href,
+      preview_url: href_preview,
       text_url: href,
       type: type,
       description: attachment["name"],
index 077fabe47bf6fbab92ff175dfe44269dcf4e9de8..1b6242cb4d0fa9fad178216094956d9f088145a9 100644 (file)
@@ -37,7 +37,7 @@ defmodule Pleroma.Web.MediaProxy do
   def url("/" <> _ = url), do: url
 
   def url(url) do
-    if disabled?() or not url_proxiable?(url) do
+    if not enabled?() or not url_proxiable?(url) do
       url
     else
       encode_url(url)
@@ -53,11 +53,25 @@ defmodule Pleroma.Web.MediaProxy do
     end
   end
 
-  defp disabled?, do: !Config.get([:media_proxy, :enabled], false)
+  # Note: routing all URLs to preview handler (even local and whitelisted).
+  #   Preview handler will call url/1 on decoded URLs, and applicable ones will detour media proxy.
+  def preview_url(url) do
+    if preview_enabled?() do
+      encode_preview_url(url)
+    else
+      url
+    end
+  end
 
-  defp local?(url), do: String.starts_with?(url, Pleroma.Web.base_url())
+  def enabled?, do: Config.get([:media_proxy, :enabled], false)
 
-  defp whitelisted?(url) do
+  # Note: media proxy must be enabled for media preview proxy in order to load all
+  #   non-local non-whitelisted URLs through it and be sure that body size constraint is preserved.
+  def preview_enabled?, do: enabled?() and Config.get([:media_preview_proxy, :enabled], false)
+
+  def local?(url), do: String.starts_with?(url, Pleroma.Web.base_url())
+
+  def whitelisted?(url) do
     %{host: domain} = URI.parse(url)
 
     mediaproxy_whitelist = Config.get([:media_proxy, :whitelist])
@@ -76,17 +90,29 @@ defmodule Pleroma.Web.MediaProxy do
     end)
   end
 
-  def encode_url(url) do
+  defp base64_sig64(url) do
     base64 = Base.url_encode64(url, @base64_opts)
 
     sig64 =
       base64
-      |> signed_url
+      |> signed_url()
       |> Base.url_encode64(@base64_opts)
 
+    {base64, sig64}
+  end
+
+  def encode_url(url) do
+    {base64, sig64} = base64_sig64(url)
+
     build_url(sig64, base64, filename(url))
   end
 
+  def encode_preview_url(url) do
+    {base64, sig64} = base64_sig64(url)
+
+    build_preview_url(sig64, base64, filename(url))
+  end
+
   def decode_url(sig, url) do
     with {:ok, sig} <- Base.url_decode64(sig, @base64_opts),
          signature when signature == sig <- signed_url(url) do
@@ -104,10 +130,10 @@ defmodule Pleroma.Web.MediaProxy do
     if path = URI.parse(url_or_path).path, do: Path.basename(path)
   end
 
-  def build_url(sig_base64, url_base64, filename \\ nil) do
+  defp proxy_url(path, sig_base64, url_base64, filename) do
     [
       Pleroma.Config.get([:media_proxy, :base_url], Web.base_url()),
-      "proxy",
+      path,
       sig_base64,
       url_base64,
       filename
@@ -115,4 +141,36 @@ defmodule Pleroma.Web.MediaProxy do
     |> Enum.filter(& &1)
     |> Path.join()
   end
+
+  def build_url(sig_base64, url_base64, filename \\ nil) do
+    proxy_url("proxy", sig_base64, url_base64, filename)
+  end
+
+  def build_preview_url(sig_base64, url_base64, filename \\ nil) do
+    proxy_url("proxy/preview", sig_base64, url_base64, filename)
+  end
+
+  def verify_request_path_and_url(
+        %Plug.Conn{params: %{"filename" => _}, request_path: request_path},
+        url
+      ) do
+    verify_request_path_and_url(request_path, url)
+  end
+
+  def verify_request_path_and_url(request_path, url) when is_binary(request_path) do
+    filename = filename(url)
+
+    if filename && not basename_matches?(request_path, filename) do
+      {:wrong_filename, filename}
+    else
+      :ok
+    end
+  end
+
+  def verify_request_path_and_url(_, _), do: :ok
+
+  defp basename_matches?(path, filename) do
+    basename = Path.basename(path)
+    basename == filename or URI.decode(basename) == filename or URI.encode(basename) == filename
+  end
 end
index 9a64b0ef35776f9b2a3c189481fed15c30ae698a..0f4575e2f20d0782ade5a3183074ab57d491efd5 100644 (file)
@@ -5,20 +5,23 @@
 defmodule Pleroma.Web.MediaProxy.MediaProxyController do
   use Pleroma.Web, :controller
 
+  alias Pleroma.Config
+  alias Pleroma.Helpers.MediaHelper
   alias Pleroma.ReverseProxy
   alias Pleroma.Web.MediaProxy
 
-  @default_proxy_opts [max_body_length: 25 * 1_048_576, http: [follow_redirect: true]]
-
-  def remote(conn, %{"sig" => sig64, "url" => url64} = params) do
-    with config <- Pleroma.Config.get([:media_proxy], []),
-         true <- Keyword.get(config, :enabled, false),
-         {:ok, url} <- MediaProxy.decode_url(sig64, url64),
+  def remote(conn, %{"sig" => sig64, "url" => url64}) do
+    with {_, true} <- {:enabled, MediaProxy.enabled?()},
          {_, false} <- {:in_banned_urls, MediaProxy.in_banned_urls(url)},
-         :ok <- filename_matches(params, conn.request_path, url) do
-      ReverseProxy.call(conn, url, Keyword.get(config, :proxy_opts, @default_proxy_opts))
+         {:ok, url} <- MediaProxy.decode_url(sig64, url64),
+         :ok <- MediaProxy.verify_request_path_and_url(conn, url) do
+      proxy_opts = Config.get([:media_proxy, :proxy_opts], [])
+      ReverseProxy.call(conn, url, proxy_opts)
     else
-      error when error in [false, {:in_banned_urls, true}] ->
+      {:enabled, false} ->
+        send_resp(conn, 404, Plug.Conn.Status.reason_phrase(404))
+
+      {:in_banned_urls, true} ->
         send_resp(conn, 404, Plug.Conn.Status.reason_phrase(404))
 
       {:error, :invalid_signature} ->
@@ -29,20 +32,89 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do
     end
   end
 
-  def filename_matches(%{"filename" => _} = _, path, url) do
-    filename = MediaProxy.filename(url)
+  def preview(conn, %{"sig" => sig64, "url" => url64}) do
+    with {_, true} <- {:enabled, MediaProxy.preview_enabled?()},
+         {:ok, url} <- MediaProxy.decode_url(sig64, url64),
+         :ok <- MediaProxy.verify_request_path_and_url(conn, url) do
+      handle_preview(conn, url)
+    else
+      {:enabled, false} ->
+        send_resp(conn, 404, Plug.Conn.Status.reason_phrase(404))
+
+      {:error, :invalid_signature} ->
+        send_resp(conn, 403, Plug.Conn.Status.reason_phrase(403))
+
+      {:wrong_filename, filename} ->
+        redirect(conn, external: MediaProxy.build_preview_url(sig64, url64, filename))
+    end
+  end
 
-    if filename && does_not_match(path, filename) do
-      {:wrong_filename, filename}
+  defp handle_preview(conn, url) do
+    with {:ok, %{status: status} = head_response} when status in 200..299 <-
+           Tesla.head(url, opts: [adapter: [timeout: preview_head_request_timeout()]]) do
+      content_type = Tesla.get_header(head_response, "content-type")
+      handle_preview(content_type, conn, url)
     else
-      :ok
+      {_, %{status: status}} ->
+        send_resp(conn, :failed_dependency, "Can't fetch HTTP headers (HTTP #{status}).")
+
+      {:error, :recv_response_timeout} ->
+        send_resp(conn, :failed_dependency, "HEAD request timeout.")
+
+      _ ->
+        send_resp(conn, :failed_dependency, "Can't fetch HTTP headers.")
+    end
+  end
+
+  defp thumbnail_max_dimensions(params) do
+    config = Config.get([:media_preview_proxy], [])
+
+    thumbnail_max_width =
+      if w = params["thumbnail_max_width"] do
+        String.to_integer(w)
+      else
+        Keyword.fetch!(config, :thumbnail_max_width)
+      end
+
+    thumbnail_max_height =
+      if h = params["thumbnail_max_height"] do
+        String.to_integer(h)
+      else
+        Keyword.fetch!(config, :thumbnail_max_height)
+      end
+
+    {thumbnail_max_width, thumbnail_max_height}
+  end
+
+  defp handle_preview("image/" <> _ = _content_type, %{params: params} = conn, url) do
+    with {thumbnail_max_width, thumbnail_max_height} <- thumbnail_max_dimensions(params),
+         media_proxy_url <- MediaProxy.url(url),
+         {:ok, thumbnail_binary} <-
+           MediaHelper.ffmpeg_resize_remote(
+             media_proxy_url,
+             %{max_width: thumbnail_max_width, max_height: thumbnail_max_height}
+           ) do
+      conn
+      |> put_resp_header("content-type", "image/jpeg")
+      |> send_resp(200, thumbnail_binary)
+    else
+      _ ->
+        send_resp(conn, :failed_dependency, "Can't handle image preview.")
     end
   end
 
-  def filename_matches(_, _, _), do: :ok
+  defp handle_preview(content_type, conn, _url) do
+    send_resp(conn, :unprocessable_entity, "Unsupported content type: #{content_type}.")
+  end
+
+  defp preview_head_request_timeout do
+    Config.get([:media_preview_proxy, :proxy_opts, :head_request_max_read_duration]) ||
+      preview_timeout()
+  end
 
-  defp does_not_match(path, filename) do
-    basename = Path.basename(path)
-    basename != filename and URI.decode(basename) != filename and URI.encode(basename) != filename
+  defp preview_timeout do
+    Config.get([:media_preview_proxy, :proxy_opts, :max_read_duration]) ||
+      Config.get([:media_proxy, :proxy_opts, :max_read_duration]) ||
+      ReverseProxy.max_read_duration_default()
   end
 end
index 9e457848e5a574abf0193f06ac99525e5162e660..94f77378b29f3f513857f507cb518d1be6ec3250 100644 (file)
@@ -677,6 +677,8 @@ defmodule Pleroma.Web.Router do
   end
 
   scope "/proxy/", Pleroma.Web.MediaProxy do
+    get("/preview/:sig/:url", MediaProxyController, :preview)
+    get("/preview/:sig/:url/:filename", MediaProxyController, :preview)
     get("/:sig/:url", MediaProxyController, :remote)
     get("/:sig/:url/:filename", MediaProxyController, :remote)
   end
diff --git a/mix.exs b/mix.exs
index e2ab53bdeee0f73d91f7676b8b085038714d6337..dc25741cf0c078b2a87c656b89f6c4515ae4cd81 100644 (file)
--- a/mix.exs
+++ b/mix.exs
@@ -200,6 +200,8 @@ defmodule Pleroma.Mixfile do
        ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"},
       {:mox, "~> 0.5", only: :test},
       {:restarter, path: "./restarter"},
+      # Note: `runtime: true` for :exexec makes CI fail due to `root` user (see Pleroma.Exec)
+      {:exexec, "~> 0.2", runtime: false},
       {:open_api_spex,
        git: "https://git.pleroma.social/pleroma/elixir-libraries/open_api_spex.git",
        ref: "f296ac0924ba3cf79c7a588c4c252889df4c2edd"}
index 4f2777fa721b0a9b98217d95a46533d3e40720cb..a1d0bf0d294e11182dec6ed900dcae5cc841b193 100644 (file)
--- a/mix.lock
+++ b/mix.lock
@@ -30,7 +30,9 @@
   "ecto": {:hex, :ecto, "3.4.4", "a2c881e80dc756d648197ae0d936216c0308370332c5e77a2325a10293eef845", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4bd3ad62abc3b21fb629f0f7a3dab23a192fca837d257dd08449fba7373561"},
   "ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"},
   "ecto_sql": {:hex, :ecto_sql, "3.3.4", "aa18af12eb875fbcda2f75e608b3bd534ebf020fc4f6448e4672fcdcbb081244", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4 or ~> 3.3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5eccbdbf92e3c6f213007a82d5dbba4cd9bb659d1a21331f89f408e4c0efd7a8"},
+  "eimp": {:hex, :eimp, "1.0.14", "fc297f0c7e2700457a95a60c7010a5f1dcb768a083b6d53f49cd94ab95a28f22", [:rebar3], [{:p1_utils, "1.0.18", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "501133f3112079b92d9e22da8b88bf4f0e13d4d67ae9c15c42c30bd25ceb83b6"},
   "elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"},
+  "erlexec": {:hex, :erlexec, "1.10.9", "3cbb3476f942bfb8b68b85721c21c1835061cf6dd35f5285c2362e85b100ddc7", [:rebar3], [], "hexpm", "271e5b5f2d91cdb9887efe74d89026c199bfc69f074cade0d08dab60993fa14e"},
   "esshd": {:hex, :esshd, "0.1.1", "d4dd4c46698093a40a56afecce8a46e246eb35463c457c246dacba2e056f31b5", [:mix], [], "hexpm", "d73e341e3009d390aa36387dc8862860bf9f874c94d9fd92ade2926376f49981"},
   "eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm", "b14f1dc204321429479c569cfbe8fb287541184ed040956c8862cb7a677b8406"},
   "ex2ms": {:hex, :ex2ms, "1.5.0", "19e27f9212be9a96093fed8cdfbef0a2b56c21237196d26760f11dfcfae58e97", [:mix], [], "hexpm"},
@@ -41,6 +43,7 @@
   "ex_machina": {:hex, :ex_machina, "2.3.0", "92a5ad0a8b10ea6314b876a99c8c9e3f25f4dde71a2a835845b136b9adaf199a", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "b84f6af156264530b312a8ab98ac6088f6b77ae5fe2058305c81434aa01fbaf9"},
   "ex_syslogger": {:hex, :ex_syslogger, "1.5.2", "72b6aa2d47a236e999171f2e1ec18698740f40af0bd02c8c650bf5f1fd1bac79", [:mix], [{:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:syslog, "~> 1.1.0", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "ab9fab4136dbc62651ec6f16fa4842f10cf02ab4433fa3d0976c01be99398399"},
   "excoveralls": {:hex, :excoveralls, "0.12.2", "a513defac45c59e310ac42fcf2b8ae96f1f85746410f30b1ff2b710a4b6cd44b", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "151c476331d49b45601ffc45f43cb3a8beb396b02a34e3777fea0ad34ae57d89"},
+  "exexec": {:hex, :exexec, "0.2.0", "a6ffc48cba3ac9420891b847e4dc7120692fb8c08c9e82220ebddc0bb8d96103", [:mix], [{:erlexec, "~> 1.10", [hex: :erlexec, repo: "hexpm", optional: false]}], "hexpm", "312cd1c9befba9e078e57f3541e4f4257eabda6eb9c348154fe899d6ac633299"},
   "fast_html": {:hex, :fast_html, "1.0.3", "2cc0d4b68496266a1530e0c852cafeaede0bd10cfdee26fda50dc696c203162f", [:make, :mix], [], "hexpm", "ab3d782b639d3c4655fbaec0f9d032c91f8cab8dd791ac7469c2381bc7c32f85"},
   "fast_sanitize": {:hex, :fast_sanitize, "0.1.7", "2a7cd8734c88a2de6de55022104f8a3b87f1fdbe8bbf131d9049764b53d50d0d", [:mix], [{:fast_html, "~> 1.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f39fe8ea08fbac17487c30bf09b7d9f3e12472e51fb07a88ffeb8fd17da8ab67"},
   "flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "31fc8090fde1acd267c07c36ea7365b8604055f897d3a53dd967658c691bd827"},
@@ -77,6 +80,7 @@
   "nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]},
   "oban": {:hex, :oban, "1.2.0", "7cca94d341be43d220571e28f69131c4afc21095b25257397f50973d3fc59b07", [:mix], [{:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ba5f8b3f7d76967b3e23cf8014f6a13e4ccb33431e4808f036709a7f822362ee"},
   "open_api_spex": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/open_api_spex.git", "f296ac0924ba3cf79c7a588c4c252889df4c2edd", [ref: "f296ac0924ba3cf79c7a588c4c252889df4c2edd"]},
+  "p1_utils": {:hex, :p1_utils, "1.0.18", "3fe224de5b2e190d730a3c5da9d6e8540c96484cf4b4692921d1e28f0c32b01c", [:rebar3], [], "hexpm", "1fc8773a71a15553b179c986b22fbeead19b28fe486c332d4929700ffeb71f88"},
   "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
   "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "1.2.1", "9cbe354b58121075bd20eb83076900a3832324b7dd171a6895fab57b6bb2752c", [:mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}], "hexpm", "d3b40a4a4630f0b442f19eca891fcfeeee4c40871936fed2f68e1c4faa30481f"},
   "phoenix": {:hex, :phoenix, "1.4.13", "67271ad69b51f3719354604f4a3f968f83aa61c19199343656c9caee057ff3b8", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ab765a0feddb81fc62e2116c827b5f068df85159c162bee760745276ad7ddc1b"},
diff --git a/test/exec_test.exs b/test/exec_test.exs
new file mode 100644 (file)
index 0000000..45d3f77
--- /dev/null
@@ -0,0 +1,13 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.ExecTest do
+  alias Pleroma.Exec
+
+  use Pleroma.DataCase
+
+  test "it starts" do
+    assert {:ok, _} = Exec.ensure_started()
+  end
+end
index 69d2a71a68d2e3a727a9b0e90c58dadb108c75be..ac5d8fd32a60f8f82e4be9c28487eb4d73962f17 100644 (file)
@@ -5,42 +5,44 @@
 defmodule Pleroma.Web.MediaProxyTest do
   use ExUnit.Case
   use Pleroma.Tests.Helpers
-  import Pleroma.Web.MediaProxy
-  alias Pleroma.Web.MediaProxy.MediaProxyController
 
-  setup do: clear_config([:media_proxy, :enabled])
-  setup do: clear_config(Pleroma.Upload)
+  alias Pleroma.Config
+  alias Pleroma.Web.Endpoint
+  alias Pleroma.Web.MediaProxy
+
+  defp decode_result(encoded) do
+    [_, "proxy", sig, base64 | _] = URI.parse(encoded).path |> String.split("/")
+    {:ok, decoded} = MediaProxy.decode_url(sig, base64)
+    decoded
+  end
 
   describe "when enabled" do
-    setup do
-      Pleroma.Config.put([:media_proxy, :enabled], true)
-      :ok
-    end
+    setup do: clear_config([:media_proxy, :enabled], true)
 
     test "ignores invalid url" do
-      assert url(nil) == nil
-      assert url("") == nil
+      assert MediaProxy.url(nil) == nil
+      assert MediaProxy.url("") == nil
     end
 
     test "ignores relative url" do
-      assert url("/local") == "/local"
-      assert url("/") == "/"
+      assert MediaProxy.url("/local") == "/local"
+      assert MediaProxy.url("/") == "/"
     end
 
     test "ignores local url" do
-      local_url = Pleroma.Web.Endpoint.url() <> "/hello"
-      local_root = Pleroma.Web.Endpoint.url()
-      assert url(local_url) == local_url
-      assert url(local_root) == local_root
+      local_url = Endpoint.url() <> "/hello"
+      local_root = Endpoint.url()
+      assert MediaProxy.url(local_url) == local_url
+      assert MediaProxy.url(local_root) == local_root
     end
 
     test "encodes and decodes URL" do
       url = "https://pleroma.soykaf.com/static/logo.png"
-      encoded = url(url)
+      encoded = MediaProxy.url(url)
 
       assert String.starts_with?(
                encoded,
-               Pleroma.Config.get([:media_proxy, :base_url], Pleroma.Web.base_url())
+               Config.get([:media_proxy, :base_url], Pleroma.Web.base_url())
              )
 
       assert String.ends_with?(encoded, "/logo.png")
@@ -50,74 +52,95 @@ defmodule Pleroma.Web.MediaProxyTest do
 
     test "encodes and decodes URL without a path" do
       url = "https://pleroma.soykaf.com"
-      encoded = url(url)
+      encoded = MediaProxy.url(url)
       assert decode_result(encoded) == url
     end
 
     test "encodes and decodes URL without an extension" do
       url = "https://pleroma.soykaf.com/path/"
-      encoded = url(url)
+      encoded = MediaProxy.url(url)
       assert String.ends_with?(encoded, "/path")
       assert decode_result(encoded) == url
     end
 
     test "encodes and decodes URL and ignores query params for the path" do
       url = "https://pleroma.soykaf.com/static/logo.png?93939393939&bunny=true"
-      encoded = url(url)
+      encoded = MediaProxy.url(url)
       assert String.ends_with?(encoded, "/logo.png")
       assert decode_result(encoded) == url
     end
 
     test "validates signature" do
-      secret_key_base = Pleroma.Config.get([Pleroma.Web.Endpoint, :secret_key_base])
-
-      on_exit(fn ->
-        Pleroma.Config.put([Pleroma.Web.Endpoint, :secret_key_base], secret_key_base)
-      end)
+      secret_key_base = Config.get([Endpoint, :secret_key_base])
+      clear_config([Endpoint, :secret_key_base], secret_key_base)
 
-      encoded = url("https://pleroma.social")
+      encoded = MediaProxy.url("https://pleroma.social")
 
-      Pleroma.Config.put(
-        [Pleroma.Web.Endpoint, :secret_key_base],
+      Config.put(
+        [Endpoint, :secret_key_base],
         "00000000000000000000000000000000000000000000000"
       )
 
       [_, "proxy", sig, base64 | _] = URI.parse(encoded).path |> String.split("/")
-      assert decode_url(sig, base64) == {:error, :invalid_signature}
+      assert MediaProxy.decode_url(sig, base64) == {:error, :invalid_signature}
     end
 
-    test "filename_matches preserves the encoded or decoded path" do
-      assert MediaProxyController.filename_matches(
-               %{"filename" => "/Hello world.jpg"},
-               "/Hello world.jpg",
-               "http://pleroma.social/Hello world.jpg"
-             ) == :ok
+    def test_verify_request_path_and_url(request_path, url, expected_result) do
+      assert MediaProxy.verify_request_path_and_url(request_path, url) == expected_result
+
+      assert MediaProxy.verify_request_path_and_url(
+               %Plug.Conn{
+                 params: %{"filename" => Path.basename(request_path)},
+                 request_path: request_path
+               },
+               url
+             ) == expected_result
+    end
 
-      assert MediaProxyController.filename_matches(
-               %{"filename" => "/Hello%20world.jpg"},
-               "/Hello%20world.jpg",
-               "http://pleroma.social/Hello%20world.jpg"
+    test "if first arg of `verify_request_path_and_url/2` is a Plug.Conn without \"filename\" " <>
+           "parameter, `verify_request_path_and_url/2` returns :ok " do
+      assert MediaProxy.verify_request_path_and_url(
+               %Plug.Conn{params: %{}, request_path: "/some/path"},
+               "https://instance.com/file.jpg"
              ) == :ok
 
-      assert MediaProxyController.filename_matches(
-               %{"filename" => "/my%2Flong%2Furl%2F2019%2F07%2FS.jpg"},
-               "/my%2Flong%2Furl%2F2019%2F07%2FS.jpg",
-               "http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg"
+      assert MediaProxy.verify_request_path_and_url(
+               %Plug.Conn{params: %{}, request_path: "/path/to/file.jpg"},
+               "https://instance.com/file.jpg"
              ) == :ok
+    end
+
+    test "`verify_request_path_and_url/2` preserves the encoded or decoded path" do
+      test_verify_request_path_and_url(
+        "/Hello world.jpg",
+        "http://pleroma.social/Hello world.jpg",
+        :ok
+      )
+
+      test_verify_request_path_and_url(
+        "/Hello%20world.jpg",
+        "http://pleroma.social/Hello%20world.jpg",
+        :ok
+      )
+
+      test_verify_request_path_and_url(
+        "/my%2Flong%2Furl%2F2019%2F07%2FS.jpg",
+        "http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg",
+        :ok
+      )
 
-      assert MediaProxyController.filename_matches(
-               %{"filename" => "/my%2Flong%2Furl%2F2019%2F07%2FS.jp"},
-               "/my%2Flong%2Furl%2F2019%2F07%2FS.jp",
-               "http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg"
-             ) == {:wrong_filename, "my%2Flong%2Furl%2F2019%2F07%2FS.jpg"}
+      test_verify_request_path_and_url(
+        "/my%2Flong%2Furl%2F2019%2F07%2FS",
+        "http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg",
+        {:wrong_filename, "my%2Flong%2Furl%2F2019%2F07%2FS.jpg"}
+      )
     end
 
     test "encoded url are tried to match for proxy as `conn.request_path` encodes the url" do
       # conn.request_path will return encoded url
       request_path = "/ANALYSE-DAI-_-LE-STABLECOIN-100-D%C3%89CENTRALIS%C3%89-BQ.jpg"
 
-      assert MediaProxyController.filename_matches(
-               true,
+      assert MediaProxy.verify_request_path_and_url(
                request_path,
                "https://mydomain.com/uploads/2019/07/ANALYSE-DAI-_-LE-STABLECOIN-100-DÉCENTRALISÉ-BQ.jpg"
              ) == :ok
@@ -127,9 +150,9 @@ defmodule Pleroma.Web.MediaProxyTest do
       clear_config([:media_proxy, :base_url], "https://cache.pleroma.social")
 
       url = "https://pleroma.soykaf.com/static/logo.png"
-      encoded = url(url)
+      encoded = MediaProxy.url(url)
 
-      assert String.starts_with?(encoded, Pleroma.Config.get([:media_proxy, :base_url]))
+      assert String.starts_with?(encoded, Config.get([:media_proxy, :base_url]))
     end
 
     # Some sites expect ASCII encoded characters in the URL to be preserved even if
@@ -140,7 +163,7 @@ defmodule Pleroma.Web.MediaProxyTest do
       url =
         "https://pleroma.com/%20/%21/%22/%23/%24/%25/%26/%27/%28/%29/%2A/%2B/%2C/%2D/%2E/%2F/%30/%31/%32/%33/%34/%35/%36/%37/%38/%39/%3A/%3B/%3C/%3D/%3E/%3F/%40/%41/%42/%43/%44/%45/%46/%47/%48/%49/%4A/%4B/%4C/%4D/%4E/%4F/%50/%51/%52/%53/%54/%55/%56/%57/%58/%59/%5A/%5B/%5C/%5D/%5E/%5F/%60/%61/%62/%63/%64/%65/%66/%67/%68/%69/%6A/%6B/%6C/%6D/%6E/%6F/%70/%71/%72/%73/%74/%75/%76/%77/%78/%79/%7A/%7B/%7C/%7D/%7E/%7F/%80/%81/%82/%83/%84/%85/%86/%87/%88/%89/%8A/%8B/%8C/%8D/%8E/%8F/%90/%91/%92/%93/%94/%95/%96/%97/%98/%99/%9A/%9B/%9C/%9D/%9E/%9F/%C2%A0/%A1/%A2/%A3/%A4/%A5/%A6/%A7/%A8/%A9/%AA/%AB/%AC/%C2%AD/%AE/%AF/%B0/%B1/%B2/%B3/%B4/%B5/%B6/%B7/%B8/%B9/%BA/%BB/%BC/%BD/%BE/%BF/%C0/%C1/%C2/%C3/%C4/%C5/%C6/%C7/%C8/%C9/%CA/%CB/%CC/%CD/%CE/%CF/%D0/%D1/%D2/%D3/%D4/%D5/%D6/%D7/%D8/%D9/%DA/%DB/%DC/%DD/%DE/%DF/%E0/%E1/%E2/%E3/%E4/%E5/%E6/%E7/%E8/%E9/%EA/%EB/%EC/%ED/%EE/%EF/%F0/%F1/%F2/%F3/%F4/%F5/%F6/%F7/%F8/%F9/%FA/%FB/%FC/%FD/%FE/%FF"
 
-      encoded = url(url)
+      encoded = MediaProxy.url(url)
       assert decode_result(encoded) == url
     end
 
@@ -151,56 +174,34 @@ defmodule Pleroma.Web.MediaProxyTest do
       url =
         "https://pleroma.com/ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890-._~:/?#[]@!$&'()*+,;=|^`{}"
 
-      encoded = url(url)
+      encoded = MediaProxy.url(url)
       assert decode_result(encoded) == url
     end
 
     test "preserve unicode characters" do
       url = "https://ko.wikipedia.org/wiki/위키백과:대문"
 
-      encoded = url(url)
+      encoded = MediaProxy.url(url)
       assert decode_result(encoded) == url
     end
   end
 
   describe "when disabled" do
-    setup do
-      enabled = Pleroma.Config.get([:media_proxy, :enabled])
-
-      if enabled do
-        Pleroma.Config.put([:media_proxy, :enabled], false)
-
-        on_exit(fn ->
-          Pleroma.Config.put([:media_proxy, :enabled], enabled)
-          :ok
-        end)
-      end
-
-      :ok
-    end
+    setup do: clear_config([:media_proxy, :enabled], false)
 
     test "does not encode remote urls" do
-      assert url("https://google.fr") == "https://google.fr"
+      assert MediaProxy.url("https://google.fr") == "https://google.fr"
     end
   end
 
-  defp decode_result(encoded) do
-    [_, "proxy", sig, base64 | _] = URI.parse(encoded).path |> String.split("/")
-    {:ok, decoded} = decode_url(sig, base64)
-    decoded
-  end
-
   describe "whitelist" do
-    setup do
-      Pleroma.Config.put([:media_proxy, :enabled], true)
-      :ok
-    end
+    setup do: clear_config([:media_proxy, :enabled], true)
 
     test "mediaproxy whitelist" do
-      Pleroma.Config.put([:media_proxy, :whitelist], ["google.com", "feld.me"])
+      clear_config([:media_proxy, :whitelist], ["google.com", "feld.me"])
       url = "https://feld.me/foo.png"
 
-      unencoded = url(url)
+      unencoded = MediaProxy.url(url)
       assert unencoded == url
     end
 
@@ -211,17 +212,17 @@ defmodule Pleroma.Web.MediaProxyTest do
       media_url = "https://mycdn.akamai.com"
 
       url = "#{media_url}/static/logo.png"
-      encoded = url(url)
+      encoded = MediaProxy.url(url)
 
       assert String.starts_with?(encoded, media_url)
     end
 
     test "ensure Pleroma.Upload base_url is always whitelisted" do
       media_url = "https://media.pleroma.social"
-      Pleroma.Config.put([Pleroma.Upload, :base_url], media_url)
+      clear_config([Pleroma.Upload, :base_url], media_url)
 
       url = "#{media_url}/static/logo.png"
-      encoded = url(url)
+      encoded = MediaProxy.url(url)
 
       assert String.starts_with?(encoded, media_url)
     end