Merge branch 'develop' into gun
authorMark Felder <feld@FreeBSD.org>
Tue, 3 Mar 2020 23:15:49 +0000 (17:15 -0600)
committerMark Felder <feld@FreeBSD.org>
Tue, 3 Mar 2020 23:15:49 +0000 (17:15 -0600)
1  2 
lib/mix/tasks/pleroma/benchmark.ex
lib/pleroma/http/connection.ex
lib/pleroma/http/http.ex
lib/pleroma/http/request_builder.ex
lib/pleroma/reverse_proxy/client.ex
lib/pleroma/reverse_proxy/reverse_proxy.ex
lib/pleroma/web/web_finger/web_finger.ex
test/http_test.exs
test/reverse_proxy/reverse_proxy_test.exs
test/user_invite_token_test.exs

index 7a743028933422e6708f5b4b7a1db5b91c49aa3c,a4885b70cd74205d0c27cd91f3496db307199308..dd2b9c8f278b26d28506b14a413d341a704c6417
@@@ -1,5 -1,5 +1,5 @@@
  # Pleroma: A lightweight social networking server
- # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
  # SPDX-License-Identifier: AGPL-3.0-only
  
  defmodule Mix.Tasks.Pleroma.Benchmark do
        inputs: inputs
      )
    end
 +
 +  def run(["adapters"]) do
 +    start_pleroma()
 +
 +    :ok =
 +      Pleroma.Gun.Conn.open(
 +        "https://httpbin.org/stream-bytes/1500",
 +        :gun_connections
 +      )
 +
 +    Process.sleep(1_500)
 +
 +    Benchee.run(
 +      %{
 +        "Without conn and without pool" => fn ->
 +          {:ok, %Tesla.Env{}} =
 +            Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [],
 +              adapter: [pool: :no_pool, receive_conn: false]
 +            )
 +        end,
 +        "Without conn and with pool" => fn ->
 +          {:ok, %Tesla.Env{}} =
 +            Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [],
 +              adapter: [receive_conn: false]
 +            )
 +        end,
 +        "With reused conn and without pool" => fn ->
 +          {:ok, %Tesla.Env{}} =
 +            Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [],
 +              adapter: [pool: :no_pool]
 +            )
 +        end,
 +        "With reused conn and with pool" => fn ->
 +          {:ok, %Tesla.Env{}} = Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500")
 +        end
 +      },
 +      parallel: 10
 +    )
 +  end
  end
index dc2761182a86716f55a5d28874b30440f5a9af56,80e6c30d624b49abcb7bd19d89ba896d15a7c770..97eec88c1824dd995fe3717f2266a3ff7ab537de
  # Pleroma: A lightweight social networking server
- # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
  # SPDX-License-Identifier: AGPL-3.0-only
  
  defmodule Pleroma.HTTP.Connection do
    @moduledoc """
 -  Connection for http-requests.
 +  Configure Tesla.Client with default and customized adapter options.
    """
 +  @type ip_address :: ipv4_address() | ipv6_address()
 +  @type ipv4_address :: {0..255, 0..255, 0..255, 0..255}
 +  @type ipv6_address ::
 +          {0..65_535, 0..65_535, 0..65_535, 0..65_535, 0..65_535, 0..65_535, 0..65_535, 0..65_535}
 +  @type proxy_type() :: :socks4 | :socks5
 +  @type host() :: charlist() | ip_address()
  
 -  @hackney_options [
 -    connect_timeout: 10_000,
 -    recv_timeout: 20_000,
 -    follow_redirect: true,
 -    force_redirect: true,
 -    pool: :federation
 -  ]
 -  @adapter Application.get_env(:tesla, :adapter)
 +  @defaults [pool: :federation]
  
 -  @doc """
 -  Configure a client connection
 +  require Logger
  
 -  # Returns
 +  alias Pleroma.Config
 +  alias Pleroma.HTTP.AdapterHelper
  
 -  Tesla.Env.client
 +  @doc """
 +  Merge default connection & adapter options with received ones.
    """
 -  @spec new(Keyword.t()) :: Tesla.Env.client()
 -  def new(opts \\ []) do
 -    Tesla.client([], {@adapter, hackney_options(opts)})
 +
 +  @spec options(URI.t(), keyword()) :: keyword()
 +  def options(%URI{} = uri, opts \\ []) do
 +    @defaults
 +    |> pool_timeout()
 +    |> Keyword.merge(opts)
 +    |> adapter().options(uri)
 +  end
 +
 +  defp pool_timeout(opts) do
 +    {config_key, default} =
 +      if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Gun do
 +        {:pools, Config.get([:pools, :default, :timeout])}
 +      else
 +        {:hackney_pools, 10_000}
 +      end
 +
 +    timeout = Config.get([config_key, opts[:pool], :timeout], default)
 +
 +    Keyword.merge(opts, timeout: timeout)
 +  end
 +
 +  @spec after_request(keyword()) :: :ok
 +  def after_request(opts), do: adapter().after_request(opts)
 +
 +  defp adapter do
 +    case Application.get_env(:tesla, :adapter) do
 +      Tesla.Adapter.Gun -> AdapterHelper.Gun
 +      Tesla.Adapter.Hackney -> AdapterHelper.Hackney
 +      _ -> AdapterHelper
 +    end
    end
  
 -  # fetch Hackney options
 -  #
 -  def hackney_options(opts) do
 -    options = Keyword.get(opts, :adapter, [])
 -    adapter_options = Pleroma.Config.get([:http, :adapter], [])
 -    proxy_url = Pleroma.Config.get([:http, :proxy_url], nil)
 -
 -    @hackney_options
 -    |> Keyword.merge(adapter_options)
 -    |> Keyword.merge(options)
 -    |> Keyword.merge(proxy: proxy_url)
 +  @spec parse_proxy(String.t() | tuple() | nil) ::
 +          {:ok, host(), pos_integer()}
 +          | {:ok, proxy_type(), host(), pos_integer()}
 +          | {:error, atom()}
 +          | nil
 +
 +  def parse_proxy(nil), do: nil
 +
 +  def parse_proxy(proxy) when is_binary(proxy) do
 +    with [host, port] <- String.split(proxy, ":"),
 +         {port, ""} <- Integer.parse(port) do
 +      {:ok, parse_host(host), port}
 +    else
 +      {_, _} ->
 +        Logger.warn("parsing port in proxy fail #{inspect(proxy)}")
 +        {:error, :invalid_proxy_port}
 +
 +      :error ->
 +        Logger.warn("parsing port in proxy fail #{inspect(proxy)}")
 +        {:error, :invalid_proxy_port}
 +
 +      _ ->
 +        Logger.warn("parsing proxy fail #{inspect(proxy)}")
 +        {:error, :invalid_proxy}
 +    end
 +  end
 +
 +  def parse_proxy(proxy) when is_tuple(proxy) do
 +    with {type, host, port} <- proxy do
 +      {:ok, type, parse_host(host), port}
 +    else
 +      _ ->
 +        Logger.warn("parsing proxy fail #{inspect(proxy)}")
 +        {:error, :invalid_proxy}
 +    end
 +  end
 +
 +  @spec parse_host(String.t() | atom() | charlist()) :: charlist() | ip_address()
 +  def parse_host(host) when is_list(host), do: host
 +  def parse_host(host) when is_atom(host), do: to_charlist(host)
 +
 +  def parse_host(host) when is_binary(host) do
 +    host = to_charlist(host)
 +
 +    case :inet.parse_address(host) do
 +      {:error, :einval} -> host
 +      {:ok, ip} -> ip
 +    end
    end
  end
diff --combined lib/pleroma/http/http.ex
index cc0c394007354555f76e63419f7e98fb246ff430,ee5b5e127a09dec311971635c73f4b01d7b585c9..7b7c79b649532ef78413d5d51b40a34e172e80dd
@@@ -1,50 -1,24 +1,50 @@@
  # Pleroma: A lightweight social networking server
- # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # Copyright © 2017-2020 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.Connection
 +  alias Pleroma.HTTP.Request
    alias Pleroma.HTTP.RequestBuilder, as: Builder
 +  alias Tesla.Client
 +  alias Tesla.Env
 +
 +  require Logger
  
    @type t :: __MODULE__
  
    @doc """
 -  Builds and perform http request.
 +  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)
 +
 +  @doc """
 +  Performs POST request.
 +
 +  See `Pleroma.HTTP.request/5`
 +  """
 +  @spec post(Request.url(), String.t(), 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
 -  `url`
 -  `body`
 +  `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
  
    `{:ok, %Tesla.Env{}}` or `{:error, error}`
  
    """
 -  def request(method, url, body \\ "", headers \\ [], options \\ []) do
 +  @spec request(atom(), Request.url(), String.t(), Request.headers(), keyword()) ::
 +          {:ok, Env.t()} | {:error, any()}
 +  def request(method, url, body, headers, options) when is_binary(url) do
 +    uri = URI.parse(url)
 +    received_adapter_opts = Keyword.get(options, :adapter, [])
 +    adapter_opts = Connection.options(uri, received_adapter_opts)
 +    options = put_in(options[:adapter], adapter_opts)
 +    params = Keyword.get(options, :params, [])
 +    request = build_request(method, headers, options, url, body, params)
 +
 +    adapter = Application.get_env(:tesla, :adapter)
 +    client = Tesla.client([Tesla.Middleware.FollowRedirects], adapter)
 +
 +    pid = Process.whereis(adapter_opts[:pool])
 +
 +    pool_alive? =
 +      if adapter == Tesla.Adapter.Gun && pid do
 +        Process.alive?(pid)
 +      else
 +        false
 +      end
 +
 +    request_opts =
 +      adapter_opts
 +      |> Enum.into(%{})
 +      |> Map.put(:env, Pleroma.Config.get([:env]))
 +      |> Map.put(:pool_alive?, pool_alive?)
 +
 +    response = request(client, request, request_opts)
 +
 +    Connection.after_request(adapter_opts)
 +
 +    response
 +  end
 +
 +  @spec request(Client.t(), keyword(), map()) :: {:ok, Env.t()} | {:error, any()}
 +  def request(%Client{} = client, request, %{env: :test}), do: request_try(client, request)
 +
 +  def request(%Client{} = client, request, %{body_as: :chunks}) do
 +    request_try(client, request)
 +  end
 +
 +  def request(%Client{} = client, request, %{pool_alive?: false}) do
 +    request_try(client, request)
 +  end
 +
 +  def request(%Client{} = client, request, %{pool: pool, timeout: timeout}) do
 +    :poolboy.transaction(
 +      pool,
 +      &Pleroma.Pool.Request.execute(&1, client, request, timeout),
 +      timeout
 +    )
 +  end
 +
 +  @spec request_try(Client.t(), keyword()) :: {:ok, Env.t()} | {:error, any()}
 +  def request_try(client, request) do
      try do
 -      options =
 -        process_request_options(options)
 -        |> process_sni_options(url)
 -
 -      params = Keyword.get(options, :params, [])
 -
 -      %{}
 -      |> Builder.method(method)
 -      |> Builder.headers(headers)
 -      |> Builder.opts(options)
 -      |> Builder.url(url)
 -      |> Builder.add_param(:body, :body, body)
 -      |> Builder.add_param(:query, :query, params)
 -      |> Enum.into([])
 -      |> (&Tesla.request(Connection.new(options), &1)).()
 +      Tesla.request(client, request)
      rescue
        e ->
          {:error, e}
      end
    end
  
 -  defp process_sni_options(options, nil), do: options
 -
 -  defp process_sni_options(options, url) do
 -    uri = URI.parse(url)
 -    host = uri.host |> to_charlist()
 -
 -    case uri.scheme do
 -      "https" -> options ++ [ssl: [server_name_indication: host]]
 -      _ -> options
 -    end
 +  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
 -
 -  def process_request_options(options) do
 -    Keyword.merge(Pleroma.HTTP.Connection.hackney_options([]), options)
 -  end
 -
 -  @doc """
 -  Performs GET request.
 -
 -  See `Pleroma.HTTP.request/5`
 -  """
 -  def get(url, headers \\ [], options \\ []),
 -    do: request(:get, url, "", headers, options)
 -
 -  @doc """
 -  Performs POST request.
 -
 -  See `Pleroma.HTTP.request/5`
 -  """
 -  def post(url, body, headers \\ [], options \\ []),
 -    do: request(:post, url, body, headers, options)
  end
index 5b92ce7648680e1979328ec2a26149cb9089a3fb,77ef4bfd8a00469dfc938920a57c92c212e7e1db..2fc876d924017f9d55a8d5d9cf3dd108e22fff02
@@@ -1,5 -1,5 +1,5 @@@
  # Pleroma: A lightweight social networking server
- # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
  # SPDX-License-Identifier: AGPL-3.0-only
  
  defmodule Pleroma.HTTP.RequestBuilder do
    Helper functions for building Tesla requests
    """
  
 -  @doc """
 -  Specify the request method when building a request
 -
 -  ## Parameters
 -
 -  - request (Map) - Collected request options
 -  - m (atom) - Request method
 -
 -  ## Returns
 +  alias Pleroma.HTTP.Request
 +  alias Tesla.Multipart
  
 -  Map
 +  @doc """
 +  Creates new request
    """
 -  @spec method(map(), atom) :: map()
 -  def method(request, m) do
 -    Map.put_new(request, :method, m)
 -  end
 +  @spec new(Request.t()) :: Request.t()
 +  def new(%Request{} = request \\ %Request{}), do: request
  
    @doc """
    Specify the request method when building a request
 +  """
 +  @spec method(Request.t(), Request.method()) :: Request.t()
 +  def method(request, m), do: %{request | method: m}
  
 -  ## Parameters
 -
 -  - request (Map) - Collected request options
 -  - u (String) - Request URL
 -
 -  ## Returns
 -
 -  Map
 +  @doc """
 +  Specify the request method when building a request
    """
 -  @spec url(map(), String.t()) :: map()
 -  def url(request, u) do
 -    Map.put_new(request, :url, u)
 -  end
 +  @spec url(Request.t(), Request.url()) :: Request.t()
 +  def url(request, u), do: %{request | url: u}
  
    @doc """
    Add headers to the request
    """
 -  @spec headers(map(), list(tuple)) :: map()
 -  def headers(request, header_list) do
 -    header_list =
 +  @spec headers(Request.t(), Request.headers()) :: Request.t()
 +  def headers(request, headers) do
 +    headers_list =
        if Pleroma.Config.get([:http, :send_user_agent]) do
 -        header_list ++ [{"User-Agent", Pleroma.Application.user_agent()}]
 +        [{"user-agent", Pleroma.Application.user_agent()} | headers]
        else
 -        header_list
 +        headers
        end
  
 -    Map.put_new(request, :headers, header_list)
 +    %{request | headers: headers_list}
    end
  
    @doc """
    Add custom, per-request middleware or adapter options to the request
    """
 -  @spec opts(map(), Keyword.t()) :: map()
 -  def opts(request, options) do
 -    Map.put_new(request, :opts, options)
 -  end
 -
 -  @doc """
 -  Add optional parameters to the request
 -
 -  ## Parameters
 -
 -  - request (Map) - Collected request options
 -  - definitions (Map) - Map of parameter name to parameter location.
 -  - options (KeywordList) - The provided optional parameters
 -
 -  ## Returns
 -
 -  Map
 -  """
 -  @spec add_optional_params(map(), %{optional(atom) => atom}, keyword()) :: map()
 -  def add_optional_params(request, _, []), do: request
 -
 -  def add_optional_params(request, definitions, [{key, value} | tail]) do
 -    case definitions do
 -      %{^key => location} ->
 -        request
 -        |> add_param(location, key, value)
 -        |> add_optional_params(definitions, tail)
 -
 -      _ ->
 -        add_optional_params(request, definitions, tail)
 -    end
 -  end
 +  @spec opts(Request.t(), keyword()) :: Request.t()
 +  def opts(request, options), do: %{request | opts: options}
  
    @doc """
    Add optional parameters to the request
 -
 -  ## Parameters
 -
 -  - request (Map) - Collected request options
 -  - location (atom) - Where to put the parameter
 -  - key (atom) - The name of the parameter
 -  - value (any) - The value of the parameter
 -
 -  ## Returns
 -
 -  Map
    """
 -  @spec add_param(map(), atom, atom, any()) :: map()
 -  def add_param(request, :query, :query, values), do: Map.put(request, :query, values)
 +  @spec add_param(Request.t(), atom(), atom(), any()) :: Request.t()
 +  def add_param(request, :query, :query, values), do: %{request | query: values}
  
 -  def add_param(request, :body, :body, value), do: Map.put(request, :body, value)
 +  def add_param(request, :body, :body, value), do: %{request | body: value}
  
    def add_param(request, :body, key, value) do
      request
 -    |> Map.put_new_lazy(:body, &Tesla.Multipart.new/0)
 +    |> Map.put(:body, Multipart.new())
      |> Map.update!(
        :body,
 -      &Tesla.Multipart.add_field(
 +      &Multipart.add_field(
          &1,
          key,
          Jason.encode!(value),
 -        headers: [{:"Content-Type", "application/json"}]
 +        headers: [{"content-type", "application/json"}]
        )
      )
    end
  
    def add_param(request, :file, name, path) do
      request
 -    |> Map.put_new_lazy(:body, &Tesla.Multipart.new/0)
 -    |> Map.update!(:body, &Tesla.Multipart.add_file(&1, path, name: name))
 +    |> Map.put(:body, Multipart.new())
 +    |> Map.update!(:body, &Multipart.add_file(&1, path, name: name))
    end
  
    def add_param(request, :form, name, value) do
 -    request
 -    |> Map.update(:body, %{name => value}, &Map.put(&1, name, value))
 +    Map.update(request, :body, %{name => value}, &Map.put(&1, name, value))
    end
  
    def add_param(request, location, key, value) do
      Map.update(request, location, [{key, value}], &(&1 ++ [{key, value}]))
    end
 +
 +  def convert_to_keyword(request) do
 +    request
 +    |> Map.from_struct()
 +    |> Enum.into([])
 +  end
  end
index 63261b94cdea052350b9ae46edf00428733d4c70,26d14fabd7544ae03c5ff44fe87d5a74bb465de0..0d13ff1747cfdfd4bd16f98cda36111ccd048555
@@@ -1,25 -1,21 +1,25 @@@
  # Pleroma: A lightweight social networking server
- # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
  # SPDX-License-Identifier: AGPL-3.0-only
  
  defmodule Pleroma.ReverseProxy.Client do
 -  @callback request(atom(), String.t(), [tuple()], String.t(), list()) ::
 -              {:ok, pos_integer(), [tuple()], reference() | map()}
 -              | {:ok, pos_integer(), [tuple()]}
 +  @type status :: pos_integer()
 +  @type header_name :: String.t()
 +  @type header_value :: String.t()
 +  @type headers :: [{header_name(), header_value()}]
 +
 +  @callback request(atom(), String.t(), headers(), String.t(), list()) ::
 +              {:ok, status(), headers(), reference() | map()}
 +              | {:ok, status(), headers()}
                | {:ok, reference()}
                | {:error, term()}
  
 -  @callback stream_body(reference() | pid() | map()) ::
 -              {:ok, binary()} | :done | {:error, String.t()}
 +  @callback stream_body(map()) :: {:ok, binary(), map()} | :done | {:error, atom() | String.t()}
  
    @callback close(reference() | pid() | map()) :: :ok
  
 -  def request(method, url, headers, "", opts \\ []) do
 -    client().request(method, url, headers, "", opts)
 +  def request(method, url, headers, body \\ "", opts \\ []) do
 +    client().request(method, url, headers, body, opts)
    end
  
    def stream_body(ref), do: client().stream_body(ref)
    def close(ref), do: client().close(ref)
  
    defp client do
 -    Pleroma.Config.get([Pleroma.ReverseProxy.Client], :hackney)
 +    :tesla
 +    |> Application.get_env(:adapter)
 +    |> client()
    end
 +
 +  defp client(Tesla.Adapter.Hackney), do: Pleroma.ReverseProxy.Client.Hackney
 +  defp client(Tesla.Adapter.Gun), do: Pleroma.ReverseProxy.Client.Tesla
 +  defp client(_), do: Pleroma.Config.get!(Pleroma.ReverseProxy.Client)
  end
index 9f5710c92ec60d5ff8e34ff1b381a941ad1d252a,a281a00dc4f725e613f2447b78fa66276dce62c4..8f1aa320075311f9fcf8286b12b8289cd8daf6c7
@@@ -1,8 -1,10 +1,8 @@@
  # Pleroma: A lightweight social networking server
- # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
  # SPDX-License-Identifier: AGPL-3.0-only
  
  defmodule Pleroma.ReverseProxy do
 -  alias Pleroma.HTTP
 -
    @keep_req_headers ~w(accept user-agent accept-encoding cache-control if-modified-since) ++
                        ~w(if-unmodified-since if-none-match if-range range)
    @resp_cache_headers ~w(etag date last-modified cache-control)
  
    * `req_headers`, `resp_headers` additional headers.
  
 -  * `http`: options for [hackney](https://github.com/benoitc/hackney).
 +  * `http`: options for [gun](https://github.com/ninenines/gun).
  
    """
 -  @default_hackney_options [pool: :media]
 +  @default_options [pool: :media]
  
    @inline_content_types [
      "image/gif",
    def call(_conn, _url, _opts \\ [])
  
    def call(conn = %{method: method}, url, opts) when method in @methods do
 -    hackney_opts =
 -      Pleroma.HTTP.Connection.hackney_options([])
 -      |> Keyword.merge(@default_hackney_options)
 -      |> Keyword.merge(Keyword.get(opts, :http, []))
 -      |> HTTP.process_request_options()
 +    client_opts = Keyword.merge(@default_options, Keyword.get(opts, :http, []))
  
      req_headers = build_req_headers(conn.req_headers, opts)
  
        end
  
      with {:ok, nil} <- Cachex.get(:failed_proxy_url_cache, url),
 -         {:ok, code, headers, client} <- request(method, url, req_headers, hackney_opts),
 +         {:ok, code, headers, client} <- request(method, url, req_headers, client_opts),
           :ok <-
             header_length_constraint(
               headers,
      |> halt()
    end
  
 -  defp request(method, url, headers, hackney_opts) do
 +  defp request(method, url, headers, opts) do
      Logger.debug("#{__MODULE__} #{method} #{url} #{inspect(headers)}")
      method = method |> String.downcase() |> String.to_existing_atom()
  
 -    case client().request(method, url, headers, "", hackney_opts) do
 +    case client().request(method, url, headers, "", opts) do
        {:ok, code, headers, client} when code in @valid_resp_codes ->
          {:ok, code, downcase_headers(headers), client}
  
               duration,
               Keyword.get(opts, :max_read_duration, @max_read_duration)
             ),
 -         {:ok, data} <- client().stream_body(client),
 +         {:ok, data, client} <- client().stream_body(client),
           {:ok, duration} <- increase_read_duration(duration),
           sent_so_far = sent_so_far + byte_size(data),
           :ok <-
index 91e9e2271fcf90e6441e28937322288927078329,43a81c75d3a0606e89a0562b5f5482b92c89ed3b..db567a02e0fbebf5832be41f03e8b371fd1522b9
@@@ -1,5 -1,5 +1,5 @@@
  # Pleroma: A lightweight social networking server
- # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
  # SPDX-License-Identifier: AGPL-3.0-only
  
  defmodule Pleroma.Web.WebFinger do
      with response <-
             HTTP.get(
               address,
 -             Accept: "application/xrd+xml,application/jrd+json"
 +             [{"accept", "application/xrd+xml,application/jrd+json"}]
             ),
           {:ok, %{status: status, body: body}} when status in 200..299 <- response do
        doc = XML.parse_document(body)
diff --combined test/http_test.exs
index d45d34f32461cf0bb9242bf429e4d46bf8cc91a5,3edb0de3655b369db4bc6e2997b262d5f6821103..4aa08afcbae319a91fae530342b63d9837e92c52
@@@ -1,12 -1,10 +1,12 @@@
  # Pleroma: A lightweight social networking server
- # Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
  # SPDX-License-Identifier: AGPL-3.0-only
  
  defmodule Pleroma.HTTPTest do
 -  use Pleroma.DataCase
 +  use ExUnit.Case
 +  use Pleroma.Tests.Helpers
    import Tesla.Mock
 +  alias Pleroma.HTTP
  
    setup do
      mock(fn
@@@ -29,7 -27,7 +29,7 @@@
  
    describe "get/1" do
      test "returns successfully result" do
 -      assert Pleroma.HTTP.get("http://example.com/hello") == {
 +      assert HTTP.get("http://example.com/hello") == {
                 :ok,
                 %Tesla.Env{status: 200, body: "hello"}
               }
@@@ -38,7 -36,7 +38,7 @@@
  
    describe "get/2 (with headers)" do
      test "returns successfully result for json content-type" do
 -      assert Pleroma.HTTP.get("http://example.com/hello", [{"content-type", "application/json"}]) ==
 +      assert HTTP.get("http://example.com/hello", [{"content-type", "application/json"}]) ==
                 {
                   :ok,
                   %Tesla.Env{
  
    describe "post/2" do
      test "returns successfully result" do
 -      assert Pleroma.HTTP.post("http://example.com/world", "") == {
 +      assert HTTP.post("http://example.com/world", "") == {
                 :ok,
                 %Tesla.Env{status: 200, body: "world"}
               }
      end
    end
 +
 +  describe "connection pools" do
 +    @describetag :integration
 +    clear_config(Pleroma.Gun) do
 +      Pleroma.Config.put(Pleroma.Gun, Pleroma.Gun.API)
 +    end
 +
 +    test "gun" do
 +      adapter = Application.get_env(:tesla, :adapter)
 +      Application.put_env(:tesla, :adapter, Tesla.Adapter.Gun)
 +
 +      on_exit(fn ->
 +        Application.put_env(:tesla, :adapter, adapter)
 +      end)
 +
 +      options = [adapter: [pool: :federation]]
 +
 +      assert {:ok, resp} = HTTP.get("https://httpbin.org/user-agent", [], options)
 +
 +      assert resp.status == 200
 +
 +      state = Pleroma.Pool.Connections.get_state(:gun_connections)
 +      assert state.conns["https:httpbin.org:443"]
 +    end
 +  end
  end
index 8e72698ee89c6a0f8d5eaed68416e076500a4fff,18d70862c1bb5452f4d8befd3c66d798a9ff3f2a..18aae5a6bceb1e48be5022d962b802a645cc282c
@@@ -1,9 -1,9 +1,9 @@@
  # Pleroma: A lightweight social networking server
- # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
  # SPDX-License-Identifier: AGPL-3.0-only
  
  defmodule Pleroma.ReverseProxyTest do
 -  use Pleroma.Web.ConnCase, async: true
 +  use Pleroma.Web.ConnCase
    import ExUnit.CaptureLog
    import Mox
    alias Pleroma.ReverseProxy
           {"content-length", byte_size(json) |> to_string()}
         ], %{url: url}}
      end)
 -    |> expect(:stream_body, invokes, fn %{url: url} ->
 +    |> expect(:stream_body, invokes, fn %{url: url} = client ->
        case Registry.lookup(Pleroma.ReverseProxy.ClientMock, url) do
          [{_, 0}] ->
            Registry.update_value(Pleroma.ReverseProxy.ClientMock, url, &(&1 + 1))
 -          {:ok, json}
 +          {:ok, json, client}
  
          [{_, 1}] ->
            Registry.unregister(Pleroma.ReverseProxy.ClientMock, url)
      assert conn.halted
    end
  
 -  describe "max_body " do
 +  defp stream_mock(invokes, with_close? \\ false) do
 +    ClientMock
 +    |> expect(:request, fn :get, "/stream-bytes/" <> length, _, _, _ ->
 +      Registry.register(Pleroma.ReverseProxy.ClientMock, "/stream-bytes/" <> length, 0)
 +
 +      {:ok, 200, [{"content-type", "application/octet-stream"}],
 +       %{url: "/stream-bytes/" <> length}}
 +    end)
 +    |> expect(:stream_body, invokes, fn %{url: "/stream-bytes/" <> length} = client ->
 +      max = String.to_integer(length)
 +
 +      case Registry.lookup(Pleroma.ReverseProxy.ClientMock, "/stream-bytes/" <> length) do
 +        [{_, current}] when current < max ->
 +          Registry.update_value(
 +            Pleroma.ReverseProxy.ClientMock,
 +            "/stream-bytes/" <> length,
 +            &(&1 + 10)
 +          )
 +
 +          {:ok, "0123456789", client}
 +
 +        [{_, ^max}] ->
 +          Registry.unregister(Pleroma.ReverseProxy.ClientMock, "/stream-bytes/" <> length)
 +          :done
 +      end
 +    end)
 +
 +    if with_close? do
 +      expect(ClientMock, :close, fn _ -> :ok end)
 +    end
 +  end
 +
 +  describe "max_body" do
      test "length returns error if content-length more than option", %{conn: conn} do
        user_agent_mock("hackney/1.15.1", 0)
  
               end) == ""
      end
  
 -    defp stream_mock(invokes, with_close? \\ false) do
 -      ClientMock
 -      |> expect(:request, fn :get, "/stream-bytes/" <> length, _, _, _ ->
 -        Registry.register(Pleroma.ReverseProxy.ClientMock, "/stream-bytes/" <> length, 0)
 -
 -        {:ok, 200, [{"content-type", "application/octet-stream"}],
 -         %{url: "/stream-bytes/" <> length}}
 -      end)
 -      |> expect(:stream_body, invokes, fn %{url: "/stream-bytes/" <> length} ->
 -        max = String.to_integer(length)
 -
 -        case Registry.lookup(Pleroma.ReverseProxy.ClientMock, "/stream-bytes/" <> length) do
 -          [{_, current}] when current < max ->
 -            Registry.update_value(
 -              Pleroma.ReverseProxy.ClientMock,
 -              "/stream-bytes/" <> length,
 -              &(&1 + 10)
 -            )
 -
 -            {:ok, "0123456789"}
 -
 -          [{_, ^max}] ->
 -            Registry.unregister(Pleroma.ReverseProxy.ClientMock, "/stream-bytes/" <> length)
 -            :done
 -        end
 -      end)
 -
 -      if with_close? do
 -        expect(ClientMock, :close, fn _ -> :ok end)
 -      end
 -    end
 -
      test "max_body_length returns error if streaming body more than that option", %{conn: conn} do
        stream_mock(3, true)
  
        Registry.register(Pleroma.ReverseProxy.ClientMock, "/headers", 0)
        {:ok, 200, [{"content-type", "application/json"}], %{url: "/headers", headers: headers}}
      end)
 -    |> expect(:stream_body, 2, fn %{url: url, headers: headers} ->
 +    |> expect(:stream_body, 2, fn %{url: url, headers: headers} = client ->
        case Registry.lookup(Pleroma.ReverseProxy.ClientMock, url) do
          [{_, 0}] ->
            Registry.update_value(Pleroma.ReverseProxy.ClientMock, url, &(&1 + 1))
            headers = for {k, v} <- headers, into: %{}, do: {String.capitalize(k), v}
 -          {:ok, Jason.encode!(%{headers: headers})}
 +          {:ok, Jason.encode!(%{headers: headers}), client}
  
          [{_, 1}] ->
            Registry.unregister(Pleroma.ReverseProxy.ClientMock, url)
  
        {:ok, 200, headers, %{url: "/disposition"}}
      end)
 -    |> expect(:stream_body, 2, fn %{url: "/disposition"} ->
 +    |> expect(:stream_body, 2, fn %{url: "/disposition"} = client ->
        case Registry.lookup(Pleroma.ReverseProxy.ClientMock, "/disposition") do
          [{_, 0}] ->
            Registry.update_value(Pleroma.ReverseProxy.ClientMock, "/disposition", &(&1 + 1))
 -          {:ok, ""}
 +          {:ok, "", client}
  
          [{_, 1}] ->
            Registry.unregister(Pleroma.ReverseProxy.ClientMock, "/disposition")
        assert {"content-disposition", "attachment; filename=\"filename.jpg\""} in conn.resp_headers
      end
    end
 +
 +  describe "tesla client using gun integration" do
 +    @describetag :integration
 +
 +    clear_config(Pleroma.ReverseProxy.Client) do
 +      Pleroma.Config.put(Pleroma.ReverseProxy.Client, Pleroma.ReverseProxy.Client.Tesla)
 +    end
 +
 +    clear_config(Pleroma.Gun) do
 +      Pleroma.Config.put(Pleroma.Gun, Pleroma.Gun.API)
 +    end
 +
 +    setup do
 +      adapter = Application.get_env(:tesla, :adapter)
 +      Application.put_env(:tesla, :adapter, Tesla.Adapter.Gun)
 +
 +      on_exit(fn ->
 +        Application.put_env(:tesla, :adapter, adapter)
 +      end)
 +    end
 +
 +    test "common", %{conn: conn} do
 +      conn = ReverseProxy.call(conn, "http://httpbin.org/stream-bytes/10")
 +      assert byte_size(conn.resp_body) == 10
 +      assert conn.state == :chunked
 +      assert conn.status == 200
 +    end
 +
 +    test "ssl", %{conn: conn} do
 +      conn = ReverseProxy.call(conn, "https://httpbin.org/stream-bytes/10")
 +      assert byte_size(conn.resp_body) == 10
 +      assert conn.state == :chunked
 +      assert conn.status == 200
 +    end
 +
 +    test "follow redirects", %{conn: conn} do
 +      conn = ReverseProxy.call(conn, "https://httpbin.org/redirect/5")
 +      assert conn.state == :chunked
 +      assert conn.status == 200
 +    end
 +  end
  end
index 671560e410dfeb88b2e8553359a2a4537f6592ee,4f70ef337fe61661502dfa768d2d0fb086e7a4ca..63f18f13c24e7f43bea74de5cf8d1b35151317cd
@@@ -1,9 -1,10 +1,9 @@@
  # Pleroma: A lightweight social networking server
- # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
  # SPDX-License-Identifier: AGPL-3.0-only
  
  defmodule Pleroma.UserInviteTokenTest do
    use ExUnit.Case, async: true
 -  use Pleroma.DataCase
    alias Pleroma.UserInviteToken
  
    describe "valid_invite?/1 one time invites" do
@@@ -63,6 -64,7 +63,6 @@@
  
      test "expires yesterday returns false", %{invite: invite} do
        invite = %{invite | expires_at: Date.add(Date.utc_today(), -1)}
 -      invite = Repo.insert!(invite)
        refute UserInviteToken.valid_invite?(invite)
      end
    end
@@@ -80,6 -82,7 +80,6 @@@
  
      test "overdue date and less uses returns false", %{invite: invite} do
        invite = %{invite | expires_at: Date.add(Date.utc_today(), -1)}
 -      invite = Repo.insert!(invite)
        refute UserInviteToken.valid_invite?(invite)
      end
  
@@@ -90,6 -93,7 +90,6 @@@
  
      test "overdue date with more uses returns false", %{invite: invite} do
        invite = %{invite | expires_at: Date.add(Date.utc_today(), -1), uses: 5}
 -      invite = Repo.insert!(invite)
        refute UserInviteToken.valid_invite?(invite)
      end
    end