diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/pleroma/helpers/mogrify_helper.ex | 25 | ||||
| -rw-r--r-- | lib/pleroma/web/mastodon_api/views/status_view.ex | 3 | ||||
| -rw-r--r-- | lib/pleroma/web/media_proxy/media_proxy.ex | 53 | ||||
| -rw-r--r-- | lib/pleroma/web/media_proxy/media_proxy_controller.ex | 76 | ||||
| -rw-r--r-- | lib/pleroma/web/router.ex | 2 | 
5 files changed, 141 insertions, 18 deletions
| diff --git a/lib/pleroma/helpers/mogrify_helper.ex b/lib/pleroma/helpers/mogrify_helper.ex new file mode 100644 index 000000000..67edb35c3 --- /dev/null +++ b/lib/pleroma/helpers/mogrify_helper.ex @@ -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.MogrifyHelper do +  @moduledoc """ +  Handles common Mogrify operations. +  """ + +  @spec store_as_temporary_file(String.t(), binary()) :: {:ok, String.t()} | {:error, atom()} +  @doc "Stores binary content fetched from specified URL as a temporary file." +  def store_as_temporary_file(url, body) do +    path = Mogrify.temporary_path_for(%{path: url}) +    with :ok <- File.write(path, body), do: {:ok, path} +  end + +  @spec store_as_temporary_file(String.t(), String.t()) :: Mogrify.Image.t() | any() +  @doc "Modifies file at specified path by resizing to specified limit dimensions." +  def in_place_resize_to_limit(path, resize_dimensions) do +    path +    |> Mogrify.open() +    |> Mogrify.resize_to_limit(resize_dimensions) +    |> Mogrify.save(in_place: true) +  end +end diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 24167f66f..2a206f743 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -419,6 +419,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 @@ -434,7 +435,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 b2b524524..f4791c758 100644 --- a/lib/pleroma/web/media_proxy/media_proxy.ex +++ b/lib/pleroma/web/media_proxy/media_proxy.ex @@ -20,6 +20,14 @@ defmodule Pleroma.Web.MediaProxy do      end    end +  def preview_url(url) do +    if disabled?() or whitelisted?(url) do +      url +    else +      encode_preview_url(url) +    end +  end +    defp disabled?, do: !Config.get([:media_proxy, :enabled], false)    defp local?(url), do: String.starts_with?(url, Pleroma.Web.base_url()) @@ -43,17 +51,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 @@ -71,10 +91,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 @@ -82,4 +102,29 @@ 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 filename_matches(%{"filename" => _} = _, path, url) do +    filename = filename(url) + +    if filename && not basename_matches?(path, filename) do +      {:wrong_filename, filename} +    else +      :ok +    end +  end + +  def filename_matches(_, _, _), 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 4657a4383..fe3f61c18 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -5,19 +5,21 @@  defmodule Pleroma.Web.MediaProxy.MediaProxyController do    use Pleroma.Web, :controller +  alias Pleroma.Config +  alias Pleroma.Helpers.MogrifyHelper    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), +    with config <- Config.get([:media_proxy], []), +         {_, true} <- {:enabled, Keyword.get(config, :enabled, false)},           {:ok, url} <- MediaProxy.decode_url(sig64, url64), -         :ok <- filename_matches(params, conn.request_path, url) do +         :ok <- MediaProxy.filename_matches(params, conn.request_path, url) do        ReverseProxy.call(conn, url, Keyword.get(config, :proxy_opts, @default_proxy_opts))      else -      false -> +      {:enabled, false} ->          send_resp(conn, 404, Plug.Conn.Status.reason_phrase(404))        {:error, :invalid_signature} -> @@ -28,20 +30,68 @@ 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} = params) do +    with {_, true} <- {:enabled, Config.get([:media_preview_proxy, :enabled], false)}, +         {:ok, url} <- MediaProxy.decode_url(sig64, url64), +         :ok <- MediaProxy.filename_matches(params, conn.request_path, 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), +         {_, true} <- {:acceptable_content_length, acceptable_body_length?(head_response)} 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}).") + +      {:acceptable_content_length, false} -> +        send_resp(conn, :unprocessable_entity, "Source file size exceeds limit.")      end    end -  def filename_matches(_, _, _), do: :ok +  defp handle_preview("image/" <> _, %{params: params} = conn, url) do +    with {:ok, %{status: status, body: body}} when status in 200..299 <- Tesla.get(url), +         {:ok, path} <- MogrifyHelper.store_as_temporary_file(url, body), +         resize_dimensions <- +           Map.get( +             params, +             "limit_dimensions", +             Config.get([:media_preview_proxy, :limit_dimensions]) +           ), +         %Mogrify.Image{} <- MogrifyHelper.in_place_resize_to_limit(path, resize_dimensions) do +      send_file(conn, 200, path) +    else +      {_, %{status: _}} -> +        send_resp(conn, :failed_dependency, "Can't fetch the image.") + +      _ -> +        send_resp(conn, :failed_dependency, "Can't handle image preview.") +    end +  end + +  defp handle_preview(content_type, conn, _url) do +    send_resp(conn, :unprocessable_entity, "Unsupported content type: #{content_type}.") +  end + +  defp acceptable_body_length?(head_response) do +    max_body_length = Config.get([:media_preview_proxy, :max_body_length], nil) +    content_length = Tesla.get_header(head_response, "content-length") +    content_length = with {int, _} <- Integer.parse(content_length), do: int -  defp does_not_match(path, filename) do -    basename = Path.basename(path) -    basename != filename and URI.decode(basename) != filename and URI.encode(basename) != filename +    content_length == :error or +      max_body_length in [nil, :infinity] or +      content_length <= max_body_length    end  end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 7a171f9fb..6fb47029a 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -663,6 +663,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 | 
