aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/pleroma/helpers/media_helper.ex41
-rw-r--r--lib/pleroma/reverse_proxy/reverse_proxy.ex4
-rw-r--r--lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/views/status_view.ex3
-rw-r--r--lib/pleroma/web/media_proxy/media_proxy.ex74
-rw-r--r--lib/pleroma/web/media_proxy/media_proxy_controller.ex119
-rw-r--r--lib/pleroma/web/metadata/utils.ex2
-rw-r--r--lib/pleroma/web/router.ex2
8 files changed, 219 insertions, 28 deletions
diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex
new file mode 100644
index 000000000..89dd4204b
--- /dev/null
+++ b/lib/pleroma/helpers/media_helper.ex
@@ -0,0 +1,41 @@
+# 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(uri_or_path, %{max_width: max_width, max_height: max_height} = options) do
+ quality = options[:quality] || 1
+
+ cmd = ~s"""
+ ffmpeg -i #{uri_or_path} -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" \
+ -loglevel quiet -f image2 -vcodec mjpeg -frames:v 1 -q:v #{quality} pipe:1
+ """
+
+ pid = Port.open({:spawn, cmd}, [:use_stdio, :in, :stream, :exit_status, :binary])
+ loop_recv(pid)
+ end
+
+ defp loop_recv(pid) do
+ loop_recv(pid, <<>>)
+ end
+
+ defp loop_recv(pid, acc) do
+ receive do
+ {^pid, {:data, data}} ->
+ loop_recv(pid, acc <> data)
+
+ {^pid, {:exit_status, 0}} ->
+ {:ok, acc}
+
+ {^pid, {:exit_status, status}} ->
+ {:error, status}
+ end
+ end
+end
diff --git a/lib/pleroma/reverse_proxy/reverse_proxy.ex b/lib/pleroma/reverse_proxy/reverse_proxy.ex
index 0de4e2309..35637e934 100644
--- a/lib/pleroma/reverse_proxy/reverse_proxy.ex
+++ b/lib/pleroma/reverse_proxy/reverse_proxy.ex
@@ -17,6 +17,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.
@@ -391,6 +393,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
diff --git a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex
index dfab105a3..5d8bb72aa 100644
--- a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex
@@ -27,7 +27,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do
end
url
- |> MediaProxy.url()
+ |> MediaProxy.preview_url()
|> HTTP.get([], adapter: opts)
end
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index 01b8bb6bb..1408a3add 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -417,6 +417,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
@@ -432,7 +433,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"],
diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex
index e18dd8224..6695d49ce 100644
--- a/lib/pleroma/web/media_proxy/media_proxy.ex
+++ b/lib/pleroma/web/media_proxy/media_proxy.ex
@@ -40,7 +40,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)
@@ -56,11 +56,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_domains =
@@ -85,17 +99,29 @@ defmodule Pleroma.Web.MediaProxy do
defp maybe_get_domain_from_url(domain), do: domain
- 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
@@ -113,10 +139,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
[
Config.get([:media_proxy, :base_url], Web.base_url()),
- "proxy",
+ path,
sig_base64,
url_base64,
filename
@@ -124,4 +150,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
diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex
index 9a64b0ef3..1c51aa5e3 100644
--- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex
+++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex
@@ -5,20 +5,22 @@
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),
+ def remote(conn, %{"sig" => sig64, "url" => url64}) do
+ with {_, true} <- {:enabled, MediaProxy.enabled?()},
{:ok, url} <- MediaProxy.decode_url(sig64, url64),
{_, 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 <- MediaProxy.verify_request_path_and_url(conn, url) do
+ ReverseProxy.call(conn, url, media_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 +31,103 @@ 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
+
+ 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
+ {_, %{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 handle_preview("image/" <> _ = _content_type, conn, url) do
+ handle_image_or_video_preview(conn, url)
+ end
+
+ defp handle_preview("video/" <> _ = _content_type, conn, url) do
+ handle_image_or_video_preview(conn, url)
+ end
+
+ defp handle_preview(content_type, conn, _url) do
+ send_resp(conn, :unprocessable_entity, "Unsupported content type: #{content_type}.")
+ end
+
+ defp handle_image_or_video_preview(%{params: params} = conn, url) do
+ quality = Config.get!([:media_preview_proxy, :quality])
- if filename && does_not_match(path, filename) do
- {:wrong_filename, filename}
+ with {thumbnail_max_width, thumbnail_max_height} <- thumbnail_max_dimensions(params),
+ media_proxy_url <- MediaProxy.url(url),
+ {:ok, thumbnail_binary} <-
+ MediaHelper.ffmpeg_resize(
+ media_proxy_url,
+ %{max_width: thumbnail_max_width, max_height: thumbnail_max_height, quality: quality}
+ ) do
+ conn
+ |> put_resp_header("content-type", "image/jpeg")
+ |> put_resp_header("content-disposition", "inline; filename=\"preview.jpg\"")
+ |> send_resp(200, thumbnail_binary)
else
- :ok
+ _ ->
+ send_resp(conn, :failed_dependency, "Can't handle preview.")
end
end
- def filename_matches(_, _, _), do: :ok
+ 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 preview_head_request_timeout do
+ Keyword.get(media_preview_proxy_opts(), :head_request_max_read_duration) ||
+ Keyword.get(media_proxy_opts(), :max_read_duration) ||
+ ReverseProxy.max_read_duration_default()
+ end
+
+ defp media_proxy_opts do
+ Config.get([:media_proxy, :proxy_opts], [])
+ 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 media_preview_proxy_opts do
+ Config.get([:media_preview_proxy, :proxy_opts], [])
end
end
diff --git a/lib/pleroma/web/metadata/utils.ex b/lib/pleroma/web/metadata/utils.ex
index 2f0dfb474..8a206e019 100644
--- a/lib/pleroma/web/metadata/utils.ex
+++ b/lib/pleroma/web/metadata/utils.ex
@@ -38,7 +38,7 @@ defmodule Pleroma.Web.Metadata.Utils do
def scrub_html(content), do: content
def attachment_url(url) do
- MediaProxy.url(url)
+ MediaProxy.preview_url(url)
end
def user_name_string(user) do
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index c6433cc53..edb635ecc 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -670,6 +670,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