pleroma/lib/pleroma/web/rich_media/helpers.ex
2026-07-17 20:35:17 +04:00

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