Hackney 4 ignores `with_body` and always returns a fully read binary, so Tesla.Adapter.Hackney never reaches the clause that honours :max_body and the option became a no-op. Only the gun path still enforced it, through Pleroma.HTTP.Adapter.Gun. Pleroma.Web.RelMe relies on nothing else, so it was fetching remote pages unbounded. Add a middleware that applies the limit for every adapter: reject when content-length already announces too much, and reject a body that came back over the limit. Streaming bodies are left alone -- neither adapter limits those and the caller owns the lease, so failing here on the header would strand a connection RichMedia is still expected to release. On the hackney path this bounds what callers are handed rather than peak memory, since the adapter has read the body before any middleware runs.
138 lines
4.1 KiB
Elixir
138 lines
4.1 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.HTTP do
|
|
@moduledoc """
|
|
Wrapper for `Tesla.request/2`.
|
|
"""
|
|
|
|
alias Pleroma.HTTP.AdapterHelper
|
|
alias Pleroma.HTTP.Request
|
|
alias Pleroma.HTTP.RequestBuilder, as: Builder
|
|
alias Tesla.Client
|
|
alias Tesla.Env
|
|
|
|
require Logger
|
|
|
|
@type t :: __MODULE__
|
|
@type method() :: :get | :post | :put | :delete | :head
|
|
|
|
@doc """
|
|
Performs GET request.
|
|
|
|
See `Pleroma.HTTP.request/5`
|
|
"""
|
|
@spec get(Request.url() | nil, Request.headers(), keyword()) ::
|
|
nil | {:ok, Env.t()} | {:error, any()}
|
|
def get(url, headers \\ [], options \\ [])
|
|
def get(nil, _, _), do: nil
|
|
def get(url, headers, options), do: request(:get, url, "", headers, options)
|
|
|
|
@spec head(Request.url(), Request.headers(), keyword()) :: {:ok, Env.t()} | {:error, any()}
|
|
def head(url, headers \\ [], options \\ []), do: request(:head, url, "", headers, options)
|
|
|
|
@doc """
|
|
Performs POST request.
|
|
|
|
See `Pleroma.HTTP.request/5`
|
|
"""
|
|
@spec post(Request.url(), Tesla.Env.body(), Request.headers(), keyword()) ::
|
|
{:ok, Env.t()} | {:error, any()}
|
|
def post(url, body, headers \\ [], options \\ []),
|
|
do: request(:post, url, body, headers, options)
|
|
|
|
@doc """
|
|
Builds and performs http request.
|
|
|
|
# Arguments:
|
|
`method` - :get, :post, :put, :delete, :head
|
|
`url` - full url
|
|
`body` - request body
|
|
`headers` - a keyworld list of headers, e.g. `[{"content-type", "text/plain"}]`
|
|
`options` - custom, per-request middleware or adapter options
|
|
|
|
# Returns:
|
|
`{:ok, %Tesla.Env{}}` or `{:error, error}`
|
|
|
|
"""
|
|
@spec request(method(), Request.url(), Tesla.Env.body(), Request.headers(), keyword()) ::
|
|
{:ok, Env.t()} | {:error, any()}
|
|
def request(method, url, body, headers, options) when is_binary(url) do
|
|
uri = URI.parse(url)
|
|
adapter_opts = AdapterHelper.options(uri, options || [])
|
|
|
|
options = put_in(options[:adapter], adapter_opts)
|
|
params = options[:params] || []
|
|
request = build_request(method, headers, options, url, body, params)
|
|
|
|
adapter = Application.get_env(:tesla, :adapter)
|
|
|
|
extra_middleware = options[:tesla_middleware] || []
|
|
|
|
client = Tesla.client(adapter_middlewares(adapter, extra_middleware), adapter)
|
|
|
|
maybe_limit(
|
|
fn ->
|
|
request(client, request)
|
|
end,
|
|
adapter,
|
|
adapter_opts
|
|
)
|
|
end
|
|
|
|
@spec request(Client.t(), keyword()) :: {:ok, Env.t()} | {:error, any()}
|
|
def request(client, request), do: Tesla.request(client, request)
|
|
|
|
defp build_request(method, headers, options, url, body, params) do
|
|
Builder.new()
|
|
|> Builder.method(method)
|
|
|> Builder.headers(headers)
|
|
|> Builder.opts(options)
|
|
|> Builder.url(url)
|
|
|> Builder.add_param(:body, :body, body)
|
|
|> Builder.add_param(:query, :query, params)
|
|
|> Builder.convert_to_keyword()
|
|
end
|
|
|
|
@prefix Pleroma.Gun.ConnectionPool
|
|
defp maybe_limit(fun, Tesla.Adapter.Gun, opts) do
|
|
ConcurrentLimiter.limit(:"#{@prefix}.#{opts[:pool] || :default}", fun)
|
|
end
|
|
|
|
defp maybe_limit(fun, _, _) do
|
|
fun.()
|
|
end
|
|
|
|
defp adapter_middlewares(Tesla.Adapter.Gun, extra_middleware) do
|
|
default_middleware() ++
|
|
[Pleroma.Tesla.Middleware.ConnectionPool] ++
|
|
extra_middleware
|
|
end
|
|
|
|
defp adapter_middlewares(_, extra_middleware) do
|
|
# A lot of tests are written expecting unencoded URLs
|
|
# and the burden of fixing that is high. Also it makes
|
|
# them hard to read. Tests will opt-in when we want to validate
|
|
# the encoding is being done correctly.
|
|
cond do
|
|
Pleroma.Config.get(:env) == :test and Pleroma.Config.get(:test_url_encoding) ->
|
|
default_middleware()
|
|
|
|
Pleroma.Config.get(:env) == :test ->
|
|
# Emulate redirects in test env, which are handled by adapters in other environments
|
|
[Tesla.Middleware.FollowRedirects]
|
|
|
|
# Hackney and Finch
|
|
true ->
|
|
default_middleware() ++ extra_middleware
|
|
end
|
|
end
|
|
|
|
defp default_middleware,
|
|
do: [
|
|
Tesla.Middleware.FollowRedirects,
|
|
Pleroma.Tesla.Middleware.EncodeUrl,
|
|
Pleroma.Tesla.Middleware.MaxBody
|
|
]
|
|
end
|