187 lines
5.2 KiB
Elixir
187 lines
5.2 KiB
Elixir
# Pleroma: A lightweight social networking server
|
|
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
|
# SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
defmodule Pleroma.Web.RichMedia.Helpers do
|
|
alias Pleroma.Config
|
|
alias Pleroma.Gun.ConnectionPool
|
|
alias Pleroma.ReverseProxy.Client.Tesla, as: TeslaClient
|
|
|
|
require Logger
|
|
|
|
@type get_errors :: {:error, :body_too_large | :content_type | :head | :get}
|
|
|
|
@spec rich_media_get(String.t()) :: {:ok, String.t()} | get_errors()
|
|
def rich_media_get(url) do
|
|
case Pleroma.HTTP.AdapterHelper.can_stream?() do
|
|
true -> stream(url)
|
|
false -> head_first(url)
|
|
end
|
|
|> handle_result(url)
|
|
end
|
|
|
|
defp stream(url) do
|
|
case Pleroma.HTTP.get(url, req_headers(), http_options()) do
|
|
{:ok, %Tesla.Env{status: 200, body: stream_body, headers: headers}} ->
|
|
result =
|
|
try do
|
|
with {_, :ok} <- {:content_type, check_content_type(headers)},
|
|
{_, :ok} <- {:content_length, check_content_length(headers)},
|
|
{:read_stream, {:ok, body}} <- {:read_stream, read_stream(stream_body)} do
|
|
{:ok, body}
|
|
end
|
|
rescue
|
|
exception ->
|
|
cleanup_stream(stream_body, :error)
|
|
reraise exception, __STACKTRACE__
|
|
end
|
|
|
|
cleanup_stream(stream_body, result)
|
|
result
|
|
|
|
{:ok, %Tesla.Env{body: stream_body}} = response ->
|
|
cleanup_stream(stream_body, :rejected)
|
|
{:get, response}
|
|
|
|
response ->
|
|
{:get, response}
|
|
end
|
|
end
|
|
|
|
defp head_first(url) do
|
|
with {_, {:ok, %Tesla.Env{status: 200, headers: headers}}} <-
|
|
{:head, Pleroma.HTTP.head(url, req_headers(), http_options())},
|
|
{_, :ok} <- {:content_type, check_content_type(headers)},
|
|
{_, :ok} <- {:content_length, check_content_length(headers)},
|
|
{_, {:ok, %Tesla.Env{status: 200, body: body}}} <-
|
|
{:get, Pleroma.HTTP.get(url, req_headers(), http_options())} do
|
|
{:ok, body}
|
|
end
|
|
end
|
|
|
|
defp handle_result(result, url) do
|
|
case result do
|
|
{:ok, body} ->
|
|
{:ok, body}
|
|
|
|
{:head, _} ->
|
|
Logger.debug("Rich media error for #{url}: HTTP HEAD failed")
|
|
{:error, :head}
|
|
|
|
{:content_type, {_, type}} ->
|
|
Logger.debug("Rich media error for #{url}: content-type is #{type}")
|
|
{:error, :content_type}
|
|
|
|
{:content_length, :error} ->
|
|
Logger.debug("Rich media error for #{url}: content-length exceeded")
|
|
{:error, :body_too_large}
|
|
|
|
{:read_stream, :error} ->
|
|
Logger.debug("Rich media error for #{url}: content-length exceeded")
|
|
{:error, :body_too_large}
|
|
|
|
{:get, _} ->
|
|
Logger.debug("Rich media error for #{url}: HTTP GET failed")
|
|
{:error, :get}
|
|
end
|
|
end
|
|
|
|
defp check_content_type(headers) do
|
|
case List.keyfind(headers, "content-type", 0) do
|
|
{_, content_type} ->
|
|
case Plug.Conn.Utils.media_type(content_type) do
|
|
{:ok, "text", "html", _} -> :ok
|
|
_ -> {:error, content_type}
|
|
end
|
|
|
|
_ ->
|
|
:ok
|
|
end
|
|
end
|
|
|
|
defp check_content_length(headers) do
|
|
max_body = Keyword.get(http_options(), :max_body)
|
|
|
|
case List.keyfind(headers, "content-length", 0) do
|
|
{_, maybe_content_length} ->
|
|
case Integer.parse(maybe_content_length) do
|
|
{content_length, ""} when content_length <= max_body -> :ok
|
|
{_, ""} -> :error
|
|
_ -> :ok
|
|
end
|
|
|
|
_ ->
|
|
:ok
|
|
end
|
|
end
|
|
|
|
defp read_stream(%{pid: pid, stream: stream, opts: opts}) do
|
|
read_chunks(pid, stream, opts, "", 0, Keyword.get(http_options(), :max_body))
|
|
end
|
|
|
|
defp read_stream(stream) do
|
|
max_body = Keyword.get(http_options(), :max_body)
|
|
|
|
try do
|
|
result =
|
|
Stream.transform(stream, 0, fn chunk, total_bytes ->
|
|
new_total = total_bytes + byte_size(chunk)
|
|
|
|
if new_total > max_body do
|
|
raise("Exceeds max body limit of #{max_body}")
|
|
else
|
|
{[chunk], new_total}
|
|
end
|
|
end)
|
|
|> Enum.into(<<>>)
|
|
|
|
{:ok, result}
|
|
rescue
|
|
_ -> :error
|
|
end
|
|
end
|
|
|
|
defp read_chunks(pid, stream, opts, acc, total_bytes, max_body) do
|
|
case Tesla.Adapter.Gun.read_chunk(pid, stream, opts) do
|
|
{fin, chunk} when fin in [:fin, :nofin] ->
|
|
total_bytes = total_bytes + byte_size(chunk)
|
|
|
|
cond do
|
|
total_bytes > max_body ->
|
|
:error
|
|
|
|
fin == :fin ->
|
|
{:ok, acc <> chunk}
|
|
|
|
true ->
|
|
read_chunks(pid, stream, opts, acc <> chunk, total_bytes, max_body)
|
|
end
|
|
|
|
{:error, _reason} ->
|
|
:error
|
|
end
|
|
end
|
|
|
|
defp cleanup_stream(%{pid: pid, stream: stream}, {:ok, _body}),
|
|
do: ConnectionPool.release_stream(pid, stream)
|
|
|
|
defp cleanup_stream(%{pid: _pid} = client, _result), do: TeslaClient.close(client)
|
|
defp cleanup_stream(_stream, _result), do: :ok
|
|
|
|
defp http_options do
|
|
[
|
|
pool: :rich_media,
|
|
max_body: Config.get([:rich_media, :max_body], 5_000_000),
|
|
stream: true
|
|
]
|
|
end
|
|
|
|
defp req_headers do
|
|
user_agent = Config.get([:rich_media, :user_agent], :default)
|
|
|
|
case user_agent do
|
|
:default -> [{"user-agent", Pleroma.Application.user_agent() <> "; Bot"}]
|
|
custom -> [{"user-agent", custom}]
|
|
end
|
|
end
|
|
end
|