summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexander Strizhakov <alex.strizhakov@gmail.com>2020-04-29 10:31:41 +0300
committerAlexander Strizhakov <alex.strizhakov@gmail.com>2020-05-06 13:25:48 +0300
commit73b1b3ff5e2761c62738e85ea1a6f822c718be2f (patch)
tree818a78f43b96bce51ea55b77db1aba26c9895b75
parent5ca7b9f6908cf3ec3cfb68bbaffcf72e85361b41 (diff)
fixing connection leaks
-rw-r--r--lib/mix/tasks/pleroma/benchmark.ex4
-rw-r--r--lib/pleroma/gun/conn.ex13
-rw-r--r--lib/pleroma/http/adapter_helper.ex41
-rw-r--r--lib/pleroma/http/adapter_helper/gun.ex82
-rw-r--r--lib/pleroma/http/adapter_helper/hackney.ex43
-rw-r--r--lib/pleroma/http/connection.ex103
-rw-r--r--lib/pleroma/http/gun.ex65
-rw-r--r--lib/pleroma/http/hackney.ex49
-rw-r--r--lib/pleroma/http/http.ex41
-rw-r--r--lib/pleroma/http/middleware/follow_redirects.ex109
-rw-r--r--lib/pleroma/http/proxy.ex67
-rw-r--r--lib/pleroma/http/request/builder.ex (renamed from lib/pleroma/http/request_builder.ex)4
-rw-r--r--lib/pleroma/http/request/request.ex (renamed from lib/pleroma/http/request.ex)0
-rw-r--r--lib/pleroma/reverse_proxy/client/tesla.ex12
-rw-r--r--mix.exs2
-rw-r--r--mix.lock2
-rw-r--r--test/http/adapter_helper/gun_test.exs233
-rw-r--r--test/http/adapter_helper_test.exs28
-rw-r--r--test/http/connection_test.exs128
-rw-r--r--test/http/gun_test.exs84
-rw-r--r--test/http/hackney_test.exs (renamed from test/http/adapter_helper/hackney_test.exs)4
-rw-r--r--test/http/middleware/follow_redirects_test.exs284
-rw-r--r--test/http/proxy_test.exs90
-rw-r--r--test/http/request/builder_test.exs (renamed from test/http/request_builder_test.exs)18
24 files changed, 822 insertions, 684 deletions
diff --git a/lib/mix/tasks/pleroma/benchmark.ex b/lib/mix/tasks/pleroma/benchmark.ex
index 6ab7fe8ef..3d37dfa24 100644
--- a/lib/mix/tasks/pleroma/benchmark.ex
+++ b/lib/mix/tasks/pleroma/benchmark.ex
@@ -92,13 +92,13 @@ defmodule Mix.Tasks.Pleroma.Benchmark do
"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]
+ adapter: [pool: :no_pool, reuse_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]
+ adapter: [reuse_conn: false]
)
end,
"With reused conn and without pool" => fn ->
diff --git a/lib/pleroma/gun/conn.ex b/lib/pleroma/gun/conn.ex
index 4c2f5b1bf..52b80cc85 100644
--- a/lib/pleroma/gun/conn.ex
+++ b/lib/pleroma/gun/conn.ex
@@ -72,14 +72,17 @@ defmodule Pleroma.Gun.Conn do
defp maybe_add_tls_opts(opts, %URI{scheme: "http"}), do: opts
defp maybe_add_tls_opts(opts, %URI{scheme: "https", host: host}) do
+ charlist_host =
+ host
+ |> to_charlist()
+ |> :idna.encode()
+
tls_opts = [
verify: :verify_peer,
cacertfile: CAStore.file_path(),
depth: 20,
reuse_sessions: false,
- verify_fun:
- {&:ssl_verify_hostname.verify_fun/3,
- [check_hostname: Pleroma.HTTP.Connection.format_host(host)]}
+ verify_fun: {&:ssl_verify_hostname.verify_fun/3, [check_hostname: charlist_host]}
]
tls_opts =
@@ -153,7 +156,7 @@ defmodule Pleroma.Gun.Conn do
end
defp do_open(%URI{host: host, port: port} = uri, opts) do
- host = Pleroma.HTTP.Connection.parse_host(host)
+ host = Pleroma.HTTP.Connection.format_host(host)
with {:ok, conn} <- Gun.open(host, port, opts),
{:ok, _} <- Gun.await_up(conn, opts[:await_up_timeout]) do
@@ -169,7 +172,7 @@ defmodule Pleroma.Gun.Conn do
end
defp destination_opts(%URI{host: host, port: port}) do
- host = Pleroma.HTTP.Connection.parse_host(host)
+ host = Pleroma.HTTP.Connection.format_host(host)
%{host: host, port: port}
end
diff --git a/lib/pleroma/http/adapter_helper.ex b/lib/pleroma/http/adapter_helper.ex
deleted file mode 100644
index 510722ff9..000000000
--- a/lib/pleroma/http/adapter_helper.ex
+++ /dev/null
@@ -1,41 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.HTTP.AdapterHelper do
- alias Pleroma.HTTP.Connection
-
- @type proxy ::
- {Connection.host(), pos_integer()}
- | {Connection.proxy_type(), Connection.host(), pos_integer()}
-
- @callback options(keyword(), URI.t()) :: keyword()
- @callback after_request(keyword()) :: :ok
-
- @spec options(keyword(), URI.t()) :: keyword()
- def options(opts, _uri) do
- proxy = Pleroma.Config.get([:http, :proxy_url], nil)
- maybe_add_proxy(opts, format_proxy(proxy))
- end
-
- @spec maybe_get_conn(URI.t(), keyword()) :: keyword()
- def maybe_get_conn(_uri, opts), do: opts
-
- @spec after_request(keyword()) :: :ok
- def after_request(_opts), do: :ok
-
- @spec format_proxy(String.t() | tuple() | nil) :: proxy() | nil
- def format_proxy(nil), do: nil
-
- def format_proxy(proxy_url) do
- case Connection.parse_proxy(proxy_url) do
- {:ok, host, port} -> {host, port}
- {:ok, type, host, port} -> {type, host, port}
- _ -> nil
- end
- end
-
- @spec maybe_add_proxy(keyword(), proxy() | nil) :: keyword()
- def maybe_add_proxy(opts, nil), do: opts
- def maybe_add_proxy(opts, proxy), do: Keyword.put_new(opts, :proxy, proxy)
-end
diff --git a/lib/pleroma/http/adapter_helper/gun.ex b/lib/pleroma/http/adapter_helper/gun.ex
deleted file mode 100644
index bdf2bcc06..000000000
--- a/lib/pleroma/http/adapter_helper/gun.ex
+++ /dev/null
@@ -1,82 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.HTTP.AdapterHelper.Gun do
- @behaviour Pleroma.HTTP.AdapterHelper
-
- alias Pleroma.HTTP.AdapterHelper
- alias Pleroma.Pool.Connections
-
- require Logger
-
- @defaults [
- connect_timeout: 5_000,
- domain_lookup_timeout: 5_000,
- tls_handshake_timeout: 5_000,
- retry: 1,
- retry_timeout: 1000,
- await_up_timeout: 5_000
- ]
-
- @spec options(keyword(), URI.t()) :: keyword()
- def options(incoming_opts \\ [], %URI{} = uri) do
- proxy =
- Pleroma.Config.get([:http, :proxy_url])
- |> AdapterHelper.format_proxy()
-
- config_opts = Pleroma.Config.get([:http, :adapter], [])
-
- @defaults
- |> Keyword.merge(config_opts)
- |> add_scheme_opts(uri)
- |> AdapterHelper.maybe_add_proxy(proxy)
- |> maybe_get_conn(uri, incoming_opts)
- end
-
- @spec after_request(keyword()) :: :ok
- def after_request(opts) do
- if opts[:conn] && opts[:body_as] != :chunks do
- Connections.checkout(opts[:conn], self(), :gun_connections)
- end
-
- :ok
- end
-
- defp add_scheme_opts(opts, %{scheme: "http"}), do: opts
-
- defp add_scheme_opts(opts, %{scheme: "https"}) do
- tls_opts = [
- log_level: :warning,
- session_lifetime: 6000,
- session_cache_client_max: 250
- ]
-
- opts
- |> Keyword.put(:certificates_verification, true)
- |> Keyword.put(:tls_opts, tls_opts)
- end
-
- defp maybe_get_conn(adapter_opts, uri, incoming_opts) do
- {receive_conn?, opts} =
- adapter_opts
- |> Keyword.merge(incoming_opts)
- |> Keyword.pop(:receive_conn, true)
-
- if Connections.alive?(:gun_connections) and receive_conn? do
- checkin_conn(uri, opts)
- else
- opts
- end
- end
-
- defp checkin_conn(uri, opts) do
- case Connections.checkin(uri, :gun_connections) do
- nil ->
- opts
-
- conn when is_pid(conn) ->
- Keyword.merge(opts, conn: conn, close_conn: false)
- end
- end
-end
diff --git a/lib/pleroma/http/adapter_helper/hackney.ex b/lib/pleroma/http/adapter_helper/hackney.ex
deleted file mode 100644
index dcb4cac71..000000000
--- a/lib/pleroma/http/adapter_helper/hackney.ex
+++ /dev/null
@@ -1,43 +0,0 @@
-defmodule Pleroma.HTTP.AdapterHelper.Hackney do
- @behaviour Pleroma.HTTP.AdapterHelper
-
- @defaults [
- connect_timeout: 10_000,
- recv_timeout: 20_000,
- follow_redirect: true,
- force_redirect: true,
- pool: :federation
- ]
-
- @spec options(keyword(), URI.t()) :: keyword()
- def options(connection_opts \\ [], %URI{} = uri) do
- proxy = Pleroma.Config.get([:http, :proxy_url])
-
- config_opts = Pleroma.Config.get([:http, :adapter], [])
-
- @defaults
- |> Keyword.merge(config_opts)
- |> Keyword.merge(connection_opts)
- |> add_scheme_opts(uri)
- |> Pleroma.HTTP.AdapterHelper.maybe_add_proxy(proxy)
- end
-
- defp add_scheme_opts(opts, %URI{scheme: "http"}), do: opts
-
- defp add_scheme_opts(opts, %URI{scheme: "https", host: host}) do
- ssl_opts = [
- ssl_options: [
- # Workaround for remote server certificate chain issues
- partial_chain: &:hackney_connect.partial_chain/1,
-
- # We don't support TLS v1.3 yet
- versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"],
- server_name_indication: to_charlist(host)
- ]
- ]
-
- Keyword.merge(opts, ssl_opts)
- end
-
- def after_request(_), do: :ok
-end
diff --git a/lib/pleroma/http/connection.ex b/lib/pleroma/http/connection.ex
index ebacf7902..f5c1432fa 100644
--- a/lib/pleroma/http/connection.ex
+++ b/lib/pleroma/http/connection.ex
@@ -7,100 +7,31 @@ defmodule Pleroma.HTTP.Connection do
Configure Tesla.Client with default and customized adapter options.
"""
- alias Pleroma.Config
- alias Pleroma.HTTP.AdapterHelper
-
require Logger
- @defaults [pool: :federation]
-
- @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()
+ @type host() :: charlist() | ipv4_address() | ipv6_address()
@doc """
- Merge default connection & adapter options with received ones.
+ Merge default connection & adapter options with received options.
"""
@spec options(URI.t(), keyword()) :: keyword()
def options(%URI{} = uri, opts \\ []) do
- @defaults
- |> pool_timeout()
- |> Keyword.merge(opts)
- |> adapter_helper().options(uri)
- end
-
- defp pool_timeout(opts) do
- {config_key, default} =
- if 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_helper().after_request(opts)
-
- defp adapter, do: Application.get_env(:tesla, :adapter)
-
- defp adapter_helper do
- case adapter() do
- Tesla.Adapter.Gun -> AdapterHelper.Gun
- Tesla.Adapter.Hackney -> AdapterHelper.Hackney
- _ -> AdapterHelper
- end
- end
-
- @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 failed #{inspect(proxy)}")
- {:error, :invalid_proxy_port}
-
- :error ->
- Logger.warn("Parsing port failed #{inspect(proxy)}")
- {:error, :invalid_proxy_port}
-
- _ ->
- Logger.warn("Parsing proxy failed #{inspect(proxy)}")
- {:error, :invalid_proxy}
- end
- end
+ adapter = Application.get_env(:tesla, :adapter)
- 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 failed #{inspect(proxy)}")
- {:error, :invalid_proxy}
- end
+ [pool: :federation]
+ |> Keyword.merge(opts)
+ |> adapter_options(uri, adapter)
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)
+ @spec format_host(String.t() | atom() | charlist()) :: host()
+ def format_host(host) when is_list(host), do: host
+ def format_host(host) when is_atom(host), do: to_charlist(host)
- def parse_host(host) when is_binary(host) do
+ def format_host(host) when is_binary(host) do
host = to_charlist(host)
case :inet.parse_address(host) do
@@ -109,16 +40,10 @@ defmodule Pleroma.HTTP.Connection do
end
end
- @spec format_host(String.t()) :: charlist()
- def format_host(host) do
- host_charlist = to_charlist(host)
+ defp adapter_options(opts, uri, Tesla.Adapter.Gun), do: Pleroma.HTTP.Gun.options(opts, uri)
- case :inet.parse_address(host_charlist) do
- {:error, :einval} ->
- :idna.encode(host_charlist)
+ defp adapter_options(opts, uri, Tesla.Adapter.Hackney),
+ do: Pleroma.HTTP.Hackney.options(opts, uri)
- {:ok, _ip} ->
- host_charlist
- end
- end
+ defp adapter_options(opts, _, _), do: Keyword.put(opts, :env, Pleroma.Config.get(:env))
end
diff --git a/lib/pleroma/http/gun.ex b/lib/pleroma/http/gun.ex
new file mode 100644
index 000000000..eeaa22c51
--- /dev/null
+++ b/lib/pleroma/http/gun.ex
@@ -0,0 +1,65 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.HTTP.Gun do
+ alias Pleroma.Config
+
+ @spec options(keyword(), URI.t()) :: keyword()
+ def options(opts \\ [], %URI{} = uri) do
+ merge_with_defaults()
+ |> add_scheme_opts(uri)
+ |> maybe_add_proxy()
+ |> Keyword.merge(opts)
+ |> add_pool_timeout()
+ |> add_reuse_conn_flag()
+ |> add_pool_alive_flag()
+ end
+
+ defp merge_with_defaults do
+ config = Config.get([:http, :adapter], [])
+
+ defaults = [
+ connect_timeout: 5_000,
+ domain_lookup_timeout: 5_000,
+ tls_handshake_timeout: 5_000,
+ retry: 1,
+ retry_timeout: 1000,
+ await_up_timeout: 5_000
+ ]
+
+ Keyword.merge(defaults, config)
+ end
+
+ defp add_scheme_opts(opts, %{scheme: "http"}), do: opts
+
+ defp add_scheme_opts(opts, %{scheme: "https"}) do
+ tls_opts = [
+ log_level: :warning,
+ session_lifetime: 6000,
+ session_cache_client_max: 250
+ ]
+
+ Keyword.merge(opts, certificates_verification: true, tls_opts: tls_opts)
+ end
+
+ defp maybe_add_proxy(opts), do: Pleroma.HTTP.Proxy.maybe_add_proxy(opts)
+
+ defp add_pool_timeout(opts) do
+ default_timeout = Config.get([:pools, :default, :timeout])
+ timeout = Config.get([:pools, opts[:pool], :timeout], default_timeout)
+ Keyword.put(opts, :timeout, timeout)
+ end
+
+ defp add_reuse_conn_flag(opts) do
+ Keyword.update(opts, :reuse_conn, true, fn flag? ->
+ Pleroma.Pool.Connections.alive?(:gun_connections) and flag?
+ end)
+ end
+
+ defp add_pool_alive_flag(opts) do
+ pid = Process.whereis(opts[:pool])
+ pool_alive? = !is_nil(pid) && Process.alive?(pid)
+ Keyword.put(opts, :pool_alive?, pool_alive?)
+ end
+end
diff --git a/lib/pleroma/http/hackney.ex b/lib/pleroma/http/hackney.ex
new file mode 100644
index 000000000..782e8b5ae
--- /dev/null
+++ b/lib/pleroma/http/hackney.ex
@@ -0,0 +1,49 @@
+defmodule Pleroma.HTTP.Hackney do
+ @spec options(keyword(), URI.t()) :: keyword()
+ def options(opts \\ [], %URI{} = uri) do
+ merge_with_defaults()
+ |> add_scheme_opts(uri)
+ |> maybe_add_proxy()
+ |> merge_with_incoming_opts(opts)
+ |> add_pool_timeout()
+ end
+
+ defp merge_with_defaults do
+ config = Pleroma.Config.get([:http, :adapter], [])
+
+ defaults = [
+ connect_timeout: 10_000,
+ recv_timeout: 20_000,
+ follow_redirect: true,
+ force_redirect: true
+ ]
+
+ Keyword.merge(defaults, config)
+ end
+
+ defp add_scheme_opts(opts, %URI{scheme: "http"}), do: opts
+
+ defp add_scheme_opts(opts, %URI{scheme: "https", host: host}) do
+ ssl_opts = [
+ ssl_options: [
+ # Workaround for remote server certificate chain issues
+ partial_chain: &:hackney_connect.partial_chain/1,
+
+ # We don't support TLS v1.3 yet
+ versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"],
+ server_name_indication: to_charlist(host)
+ ]
+ ]
+
+ Keyword.merge(opts, ssl_opts)
+ end
+
+ defp maybe_add_proxy(opts), do: Pleroma.HTTP.Proxy.maybe_add_proxy(opts)
+
+ defp merge_with_incoming_opts(opts, incoming), do: Keyword.merge(opts, incoming)
+
+ defp add_pool_timeout(opts) do
+ timeout = Pleroma.Config.get([:hackney_pools, opts[:pool], :timeout], 10_000)
+ Keyword.put(opts, :timeout, timeout)
+ end
+end
diff --git a/lib/pleroma/http/http.ex b/lib/pleroma/http/http.ex
index 583b56484..2a182b81e 100644
--- a/lib/pleroma/http/http.ex
+++ b/lib/pleroma/http/http.ex
@@ -7,9 +7,8 @@ defmodule Pleroma.HTTP do
Wrapper for `Tesla.request/2`.
"""
- alias Pleroma.HTTP.Connection
alias Pleroma.HTTP.Request
- alias Pleroma.HTTP.RequestBuilder, as: Builder
+ alias Pleroma.HTTP.Request.Builder
alias Tesla.Client
alias Tesla.Env
@@ -56,44 +55,25 @@ defmodule Pleroma.HTTP do
{:ok, Env.t()} | {:error, any()}
def request(method, url, body, headers, options) when is_binary(url) do
uri = URI.parse(url)
- adapter_opts = Connection.options(uri, options[:adapter] || [])
+ adapter_opts = Pleroma.HTTP.Connection.options(uri, options[:adapter] || [])
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)
- client = Tesla.client([Tesla.Middleware.FollowRedirects], adapter)
- pid = Process.whereis(adapter_opts[:pool])
+ client = Tesla.client([Pleroma.HTTP.Middleware.FollowRedirects], adapter)
+ request = build_request(method, headers, options, url, body)
- 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
+ request(client, request, Enum.into(adapter_opts, %{}))
end
@spec request(Client.t(), keyword(), map()) :: {:ok, Env.t()} | {:error, any()}
- def request(%Client{} = client, request, %{env: :test}), do: request(client, request)
+ def request(client, request, %{env: :test}), do: request(client, request)
- def request(%Client{} = client, request, %{body_as: :chunks}), do: request(client, request)
+ def request(client, request, %{body_as: :chunks}), do: request(client, request)
- def request(%Client{} = client, request, %{pool_alive?: false}), do: request(client, request)
+ def request(client, request, %{pool_alive?: false}), do: request(client, request)
- def request(%Client{} = client, request, %{pool: pool, timeout: timeout}) do
+ def request(client, request, %{pool: pool, timeout: timeout}) do
:poolboy.transaction(
pool,
&Pleroma.Pool.Request.execute(&1, client, request, timeout),
@@ -104,14 +84,13 @@ defmodule Pleroma.HTTP do
@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
+ defp build_request(method, headers, options, url, body) 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
end
diff --git a/lib/pleroma/http/middleware/follow_redirects.ex b/lib/pleroma/http/middleware/follow_redirects.ex
new file mode 100644
index 000000000..bb22504c7
--- /dev/null
+++ b/lib/pleroma/http/middleware/follow_redirects.ex
@@ -0,0 +1,109 @@
+defmodule Pleroma.HTTP.Middleware.FollowRedirects do
+ @moduledoc """
+ Follow 3xx redirects
+ ## Example
+ ```
+ defmodule MyClient do
+ use Tesla
+ plug Tesla.Middleware.FollowRedirects, max_redirects: 3 # defaults to 5
+ end
+ ```
+ ## Options
+ - `:max_redirects` - limit number of redirects (default: `5`)
+ """
+
+ @behaviour Tesla.Middleware
+
+ @max_redirects 5
+ @redirect_statuses [301, 302, 303, 307, 308]
+
+ @impl Tesla.Middleware
+ def call(env, next, opts \\ []) do
+ max = Keyword.get(opts, :max_redirects, @max_redirects)
+
+ redirect(env, next, max)
+ end
+
+ defp redirect(env, next, left) do
+ opts = env.opts[:adapter]
+
+ adapter_opts =
+ if opts[:reuse_conn] do
+ checkin_conn(env.url, opts)
+ else
+ opts
+ end
+
+ env = %{env | opts: Keyword.put(env.opts, :adapter, adapter_opts)}
+
+ case Tesla.run(env, next) do
+ {:ok, %{status: status} = res} when status in @redirect_statuses and left > 0 ->
+ checkout_conn(adapter_opts)
+
+ case Tesla.get_header(res, "location") do
+ nil ->
+ {:ok, res}
+
+ location ->
+ location = parse_location(location, res)
+
+ env
+ |> new_request(res.status, location)
+ |> redirect(next, left - 1)
+ end
+
+ {:ok, %{status: status}} when status in @redirect_statuses ->
+ checkout_conn(adapter_opts)
+ {:error, {__MODULE__, :too_many_redirects}}
+
+ other ->
+ unless adapter_opts[:body_as] == :chunks do
+ checkout_conn(adapter_opts)
+ end
+
+ other
+ end
+ end
+
+ defp checkin_conn(url, opts) do
+ uri = URI.parse(url)
+
+ case Pleroma.Pool.Connections.checkin(uri, :gun_connections, opts) do
+ nil ->
+ opts
+
+ conn when is_pid(conn) ->
+ Keyword.merge(opts, conn: conn, close_conn: false)
+ end
+ end
+
+ defp checkout_conn(opts) do
+ if is_pid(opts[:conn]) do
+ Pleroma.Pool.Connections.checkout(opts[:conn], self(), :gun_connections)
+ end
+ end
+
+ # The 303 (See Other) redirect was added in HTTP/1.1 to indicate that the originally
+ # requested resource is not available, however a related resource (or another redirect)
+ # available via GET is available at the specified location.
+ # https://tools.ietf.org/html/rfc7231#section-6.4.4
+ defp new_request(env, 303, location), do: %{env | url: location, method: :get, query: []}
+
+ # The 307 (Temporary Redirect) status code indicates that the target
+ # resource resides temporarily under a different URI and the user agent
+ # MUST NOT change the request method (...)
+ # https://tools.ietf.org/html/rfc7231#section-6.4.7
+ defp new_request(env, 307, location), do: %{env | url: location}
+
+ defp new_request(env, _, location), do: %{env | url: location, query: []}
+
+ defp parse_location("https://" <> _rest = location, _env), do: location
+ defp parse_location("http://" <> _rest = location, _env), do: location
+
+ defp parse_location(location, env) do
+ env.url
+ |> URI.parse()
+ |> URI.merge(location)
+ |> URI.to_string()
+ end
+end
diff --git a/lib/pleroma/http/proxy.ex b/lib/pleroma/http/proxy.ex
new file mode 100644
index 000000000..aa9964120
--- /dev/null
+++ b/lib/pleroma/http/proxy.ex
@@ -0,0 +1,67 @@
+defmodule Pleroma.HTTP.Proxy do
+ require Logger
+
+ alias Pleroma.HTTP.Connection
+
+ @type proxy_type() :: :socks4 | :socks5
+
+ @spec parse_proxy(String.t() | tuple() | nil) ::
+ {:ok, Connection.host(), pos_integer()}
+ | {:ok, proxy_type(), Connection.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, Connection.format_host(host), port}
+ else
+ {_, _} ->
+ Logger.warn("Parsing port failed #{inspect(proxy)}")
+ {:error, :invalid_proxy_port}
+
+ :error ->
+ Logger.warn("Parsing port failed #{inspect(proxy)}")
+ {:error, :invalid_proxy_port}
+
+ _ ->
+ Logger.warn("Parsing proxy failed #{inspect(proxy)}")
+ {:error, :invalid_proxy}
+ end
+ end
+
+ def parse_proxy(proxy) when is_tuple(proxy) do
+ with {type, host, port} <- proxy do
+ {:ok, type, Connection.format_host(host), port}
+ else
+ _ ->
+ Logger.warn("Parsing proxy failed #{inspect(proxy)}")
+ {:error, :invalid_proxy}
+ end
+ end
+
+ defp format_proxy(nil), do: nil
+
+ defp format_proxy(proxy_url) do
+ case parse_proxy(proxy_url) do
+ {:ok, host, port} -> {host, port}
+ {:ok, type, host, port} -> {type, host, port}
+ _ -> nil
+ end
+ end
+
+ @spec maybe_add_proxy(keyword()) :: keyword()
+ def maybe_add_proxy(opts) do
+ proxy =
+ Pleroma.Config.get([:http, :proxy_url])
+ |> format_proxy()
+
+ if proxy do
+ Keyword.put_new(opts, :proxy, proxy)
+ else
+ opts
+ end
+ end
+end
diff --git a/lib/pleroma/http/request_builder.ex b/lib/pleroma/http/request/builder.ex
index 2fc876d92..50bc4e3ad 100644
--- a/lib/pleroma/http/request_builder.ex
+++ b/lib/pleroma/http/request/builder.ex
@@ -2,7 +2,7 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.HTTP.RequestBuilder do
+defmodule Pleroma.HTTP.Request.Builder do
@moduledoc """
Helper functions for building Tesla requests
"""
@@ -53,8 +53,6 @@ defmodule Pleroma.HTTP.RequestBuilder do
Add optional parameters to the request
"""
@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: %{request | body: value}
def add_param(request, :body, key, value) do
diff --git a/lib/pleroma/http/request.ex b/lib/pleroma/http/request/request.ex
index 761bd6ccf..761bd6ccf 100644
--- a/lib/pleroma/http/request.ex
+++ b/lib/pleroma/http/request/request.ex
diff --git a/lib/pleroma/reverse_proxy/client/tesla.ex b/lib/pleroma/reverse_proxy/client/tesla.ex
index e81ea8bde..999b1a4cb 100644
--- a/lib/pleroma/reverse_proxy/client/tesla.ex
+++ b/lib/pleroma/reverse_proxy/client/tesla.ex
@@ -26,7 +26,7 @@ defmodule Pleroma.ReverseProxy.Client.Tesla do
url,
body,
headers,
- Keyword.put(opts, :adapter, opts)
+ adapter: opts
) do
if is_map(response.body) and method != :head do
{:ok, response.status, response.headers, response.body}
@@ -41,14 +41,8 @@ defmodule Pleroma.ReverseProxy.Client.Tesla do
@impl true
@spec stream_body(map()) ::
{:ok, binary(), map()} | {:error, atom() | String.t()} | :done | no_return()
- def stream_body(%{pid: pid, opts: opts, fin: true}) do
- # if connection was reused, but in tesla were redirects,
- # tesla returns new opened connection, which must be closed manually
- if opts[:old_conn], do: Tesla.Adapter.Gun.close(pid)
- # if there were redirects we need to checkout old conn
- conn = opts[:old_conn] || opts[:conn]
-
- if conn, do: :ok = Pleroma.Pool.Connections.checkout(conn, self(), :gun_connections)
+ def stream_body(%{pid: pid, fin: true}) do
+ :ok = Pleroma.Pool.Connections.checkout(pid, self(), :gun_connections)
:done
end
diff --git a/mix.exs b/mix.exs
index 45cd37006..712c5ec31 100644
--- a/mix.exs
+++ b/mix.exs
@@ -131,7 +131,7 @@ defmodule Pleroma.Mixfile do
# {:tesla, "~> 1.3", override: true},
{:tesla,
git: "https://git.pleroma.social/pleroma/elixir-libraries/tesla.git",
- ref: "61b7503cef33f00834f78ddfafe0d5d9dec2270b",
+ ref: "e555341e7a6a60fc06712652feabcd90f51767f4",
override: true},
{:castore, "~> 0.1"},
{:cowlib, "~> 2.8", override: true},
diff --git a/mix.lock b/mix.lock
index a39e8061a..79dc233d9 100644
--- a/mix.lock
+++ b/mix.lock
@@ -104,7 +104,7 @@
"swoosh": {:hex, :swoosh, "0.23.5", "bfd9404bbf5069b1be2ffd317923ce57e58b332e25dbca2a35dedd7820dfee5a", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "e3928e1d2889a308aaf3e42755809ac21cffd77cb58eef01cbfdab4ce2fd1e21"},
"syslog": {:hex, :syslog, "1.0.6", "995970c9aa7feb380ac493302138e308d6e04fd57da95b439a6df5bb3bf75076", [:rebar3], [], "hexpm", "769ddfabd0d2a16f3f9c17eb7509951e0ca4f68363fb26f2ee51a8ec4a49881a"},
"telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"},
- "tesla": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/tesla.git", "61b7503cef33f00834f78ddfafe0d5d9dec2270b", [ref: "61b7503cef33f00834f78ddfafe0d5d9dec2270b"]},
+ "tesla": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/tesla.git", "e555341e7a6a60fc06712652feabcd90f51767f4", [ref: "e555341e7a6a60fc06712652feabcd90f51767f4"]},
"timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "f354efb2400dd7a80fd9eb6c8419068c4f632da4ac47f3d8822d6e33f08bc852"},
"trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"},
"tzdata": {:hex, :tzdata, "0.5.22", "f2ba9105117ee0360eae2eca389783ef7db36d533899b2e84559404dbc77ebb8", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "cd66c8a1e6a9e121d1f538b01bef459334bb4029a1ffb4eeeb5e4eae0337e7b6"},
diff --git a/test/http/adapter_helper/gun_test.exs b/test/http/adapter_helper/gun_test.exs
deleted file mode 100644
index 95a8cf814..000000000
--- a/test/http/adapter_helper/gun_test.exs
+++ /dev/null
@@ -1,233 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.HTTP.AdapterHelper.GunTest do
- use ExUnit.Case
- use Pleroma.Tests.Helpers
-
- import Mox
-
- alias Pleroma.Config
- alias Pleroma.HTTP.AdapterHelper.Gun
- alias Pleroma.Pool.Connections
-
- setup :verify_on_exit!
- setup :set_mox_global
-
- defp gun_mock do
- Pleroma.GunMock
- |> stub(:open, fn _, _, _ ->
- Task.start_link(fn -> Process.sleep(1000) end)
- end)
- |> stub(:await_up, fn _, _ -> {:ok, :http} end)
- |> stub(:set_owner, fn _, _ -> :ok end)
-
- :ok
- end
-
- describe "options/1" do
- setup do: clear_config([:http, :adapter], a: 1, b: 2)
-
- test "https url with default port" do
- uri = URI.parse("https://example.com")
-
- opts = Gun.options([receive_conn: false], uri)
- assert opts[:certificates_verification]
- assert opts[:tls_opts][:log_level] == :warning
- end
-
- test "https ipv4 with default port" do
- uri = URI.parse("https://127.0.0.1")
-
- opts = Gun.options([receive_conn: false], uri)
- assert opts[:certificates_verification]
- assert opts[:tls_opts][:log_level] == :warning
- end
-
- test "https ipv6 with default port" do
- uri = URI.parse("https://[2a03:2880:f10c:83:face:b00c:0:25de]")
-
- opts = Gun.options([receive_conn: false], uri)
- assert opts[:certificates_verification]
- assert opts[:tls_opts][:log_level] == :warning
- end
-
- test "https url with non standart port" do
- uri = URI.parse("https://example.com:115")
-
- opts = Gun.options([receive_conn: false], uri)
-
- assert opts[:certificates_verification]
- end
-
- test "merges with defaul http adapter config" do
- defaults = Gun.options([receive_conn: false], URI.parse("https://example.com"))
- assert Keyword.has_key?(defaults, :a)
- assert Keyword.has_key?(defaults, :b)
- end
-
- test "default ssl adapter opts with connection" do
- gun_mock()
- uri = URI.parse("https://some-domain.com")
-
- opts = Gun.options(uri)
-
- assert opts[:certificates_verification]
- refute opts[:tls_opts] == []
-
- assert opts[:close_conn] == false
- assert is_pid(opts[:conn])
- end
-
- test "parses string proxy host & port" do
- proxy = Config.get([:http, :proxy_url])
- Config.put([:http, :proxy_url], "localhost:8123")
- on_exit(fn -> Config.put([:http, :proxy_url], proxy) end)
-
- uri = URI.parse("https://some-domain.com")
- opts = Gun.options([receive_conn: false], uri)
- assert opts[:proxy] == {'localhost', 8123}
- end
-
- test "parses tuple proxy scheme host and port" do
- proxy = Config.get([:http, :proxy_url])
- Config.put([:http, :proxy_url], {:socks, 'localhost', 1234})
- on_exit(fn -> Config.put([:http, :proxy_url], proxy) end)
-
- uri = URI.parse("https://some-domain.com")
- opts = Gun.options([receive_conn: false], uri)
- assert opts[:proxy] == {:socks, 'localhost', 1234}
- end
-
- test "passed opts have more weight than defaults" do
- proxy = Config.get([:http, :proxy_url])
- Config.put([:http, :proxy_url], {:socks5, 'localhost', 1234})
- on_exit(fn -> Config.put([:http, :proxy_url], proxy) end)
- uri = URI.parse("https://some-domain.com")
- opts = Gun.options([receive_conn: false, proxy: {'example.com', 4321}], uri)
-
- assert opts[:proxy] == {'example.com', 4321}
- end
- end
-
- describe "options/1 with receive_conn parameter" do
- setup do: gun_mock()
-
- test "receive conn by default" do
- uri = URI.parse("http://another-domain.com")
-
- received_opts = Gun.options(uri)
- assert received_opts[:close_conn] == false
- assert is_pid(received_opts[:conn])
- end
-
- test "don't receive conn if receive_conn is false" do
- uri = URI.parse("http://another-domain.com")
-
- opts = [receive_conn: false]
- received_opts = Gun.options(opts, uri)
- assert received_opts[:close_conn] == nil
- assert received_opts[:conn] == nil
- end
- end
-
- describe "after_request/1" do
- setup do: gun_mock()
-
- test "body_as not chunks" do
- uri = URI.parse("http://some-domain-5.com")
- opts = Gun.options(uri)
- :ok = Gun.after_request(opts)
- conn = opts[:conn]
-
- assert match?(
- %Connections{
- conns: %{
- "http:some-domain-5.com:80" => %Pleroma.Gun.Conn{
- conn: ^conn,
- conn_state: :idle,
- used_by: []
- }
- }
- },
- Connections.get_state(:gun_connections)
- )
- end
-
- test "body_as chunks" do
- uri = URI.parse("http://some-domain-6.com")
- opts = Gun.options([body_as: :chunks], uri)
- :ok = Gun.after_request(opts)
- conn = opts[:conn]
- self = self()
-
- assert match?(
- %Connections{
- conns: %{
- "http:some-domain-6.com:80" => %Pleroma.Gun.Conn{
- conn: ^conn,
- conn_state: :active,
- used_by: [{^self, _}]
- }
- }
- },
- Connections.get_state(:gun_connections)
- )
- end
-
- test "with no connection" do
- uri = URI.parse("http://uniq-domain.com")
- opts = Gun.options([body_as: :chunks], uri)
- conn = opts[:conn]
- opts = Keyword.delete(opts, :conn)
- self = self()
-
- :ok = Gun.after_request(opts)
-
- assert %Connections{
- conns: %{
- "http:uniq-domain.com:80" => %Pleroma.Gun.Conn{
- conn: ^conn,
- conn_state: :active,
- used_by: [{^self, _}]
- }
- }
- } = Connections.get_state(:gun_connections)
- end
-
- test "with ipv4" do
- uri = URI.parse("http://127.0.0.1")
- opts = Gun.options(uri)
- :ok = Gun.after_request(opts)
- conn = opts[:conn]
-
- assert %Connections{
- conns: %{
- "http:127.0.0.1:80" => %Pleroma.Gun.Conn{
- conn: ^conn,
- conn_state: :idle,
- used_by: []
- }
- }
- } = Connections.get_state(:gun_connections)
- end
-
- test "with ipv6" do
- uri = URI.parse("http://[2a03:2880:f10c:83:face:b00c:0:25de]")
- opts = Gun.options(uri)
- :ok = Gun.after_request(opts)
- conn = opts[:conn]
-
- assert %Connections{
- conns: %{
- "http:2a03:2880:f10c:83:face:b00c:0:25de:80" => %Pleroma.Gun.Conn{
- conn: ^conn,
- conn_state: :idle,
- used_by: []
- }
- }
- } = Connections.get_state(:gun_connections)
- end
- end
-end
diff --git a/test/http/adapter_helper_test.exs b/test/http/adapter_helper_test.exs
deleted file mode 100644
index 24d501ad5..000000000
--- a/test/http/adapter_helper_test.exs
+++ /dev/null
@@ -1,28 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.HTTP.AdapterHelperTest do
- use ExUnit.Case, async: true
-
- alias Pleroma.HTTP.AdapterHelper
-
- describe "format_proxy/1" do
- test "with nil" do
- assert AdapterHelper.format_proxy(nil) == nil
- end
-
- test "with string" do
- assert AdapterHelper.format_proxy("127.0.0.1:8123") == {{127, 0, 0, 1}, 8123}
- end
-
- test "localhost with port" do
- assert AdapterHelper.format_proxy("localhost:8123") == {'localhost', 8123}
- end
-
- test "tuple" do
- assert AdapterHelper.format_proxy({:socks4, :localhost, 9050}) ==
- {:socks4, 'localhost', 9050}
- end
- end
-end
diff --git a/test/http/connection_test.exs b/test/http/connection_test.exs
index 5cc78ad5b..19b39a6b7 100644
--- a/test/http/connection_test.exs
+++ b/test/http/connection_test.exs
@@ -3,133 +3,51 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTTP.ConnectionTest do
- use ExUnit.Case, async: true
- use Pleroma.Tests.Helpers
+ use ExUnit.Case
- import ExUnit.CaptureLog
-
- alias Pleroma.Config
alias Pleroma.HTTP.Connection
- describe "parse_host/1" do
+ describe "format_host/1" do
test "as atom to charlist" do
- assert Connection.parse_host(:localhost) == 'localhost'
+ assert Connection.format_host(:localhost) == 'localhost'
end
test "as string to charlist" do
- assert Connection.parse_host("localhost.com") == 'localhost.com'
+ assert Connection.format_host("localhost.com") == 'localhost.com'
end
test "as string ip to tuple" do
- assert Connection.parse_host("127.0.0.1") == {127, 0, 0, 1}
- end
- end
-
- describe "parse_proxy/1" do
- test "ip with port" do
- assert Connection.parse_proxy("127.0.0.1:8123") == {:ok, {127, 0, 0, 1}, 8123}
- end
-
- test "host with port" do
- assert Connection.parse_proxy("localhost:8123") == {:ok, 'localhost', 8123}
- end
-
- test "as tuple" do
- assert Connection.parse_proxy({:socks4, :localhost, 9050}) ==
- {:ok, :socks4, 'localhost', 9050}
- end
-
- test "as tuple with string host" do
- assert Connection.parse_proxy({:socks5, "localhost", 9050}) ==
- {:ok, :socks5, 'localhost', 9050}
- end
- end
-
- describe "parse_proxy/1 errors" do
- test "ip without port" do
- capture_log(fn ->
- assert Connection.parse_proxy("127.0.0.1") == {:error, :invalid_proxy}
- end) =~ "parsing proxy fail \"127.0.0.1\""
- end
-
- test "host without port" do
- capture_log(fn ->
- assert Connection.parse_proxy("localhost") == {:error, :invalid_proxy}
- end) =~ "parsing proxy fail \"localhost\""
- end
-
- test "host with bad port" do
- capture_log(fn ->
- assert Connection.parse_proxy("localhost:port") == {:error, :invalid_proxy_port}
- end) =~ "parsing port in proxy fail \"localhost:port\""
- end
-
- test "ip with bad port" do
- capture_log(fn ->
- assert Connection.parse_proxy("127.0.0.1:15.9") == {:error, :invalid_proxy_port}
- end) =~ "parsing port in proxy fail \"127.0.0.1:15.9\""
- end
-
- test "as tuple without port" do
- capture_log(fn ->
- assert Connection.parse_proxy({:socks5, :localhost}) == {:error, :invalid_proxy}
- end) =~ "parsing proxy fail {:socks5, :localhost}"
- end
-
- test "with nil" do
- assert Connection.parse_proxy(nil) == nil
+ assert Connection.format_host("127.0.0.1") == {127, 0, 0, 1}
end
end
- describe "options/3" do
- setup do: clear_config([:http, :proxy_url])
-
- test "without proxy_url in config" do
- Config.delete([:http, :proxy_url])
-
- opts = Connection.options(%URI{})
- refute Keyword.has_key?(opts, :proxy)
- end
-
- test "parses string proxy host & port" do
- Config.put([:http, :proxy_url], "localhost:8123")
-
- opts = Connection.options(%URI{})
- assert opts[:proxy] == {'localhost', 8123}
- end
-
- test "parses tuple proxy scheme host and port" do
- Config.put([:http, :proxy_url], {:socks, 'localhost', 1234})
-
- opts = Connection.options(%URI{})
- assert opts[:proxy] == {:socks, 'localhost', 1234}
+ describe "options/2" do
+ test "defaults" do
+ assert Connection.options(%URI{}) == [env: :test, pool: :federation]
end
test "passed opts have more weight than defaults" do
- Config.put([:http, :proxy_url], {:socks5, 'localhost', 1234})
-
- opts = Connection.options(%URI{}, proxy: {'example.com', 4321})
-
- assert opts[:proxy] == {'example.com', 4321}
+ assert Connection.options(%URI{}, pool: :media) == [env: :test, pool: :media]
end
- end
- describe "format_host/1" do
- test "with domain" do
- assert Connection.format_host("example.com") == 'example.com'
- end
+ test "adding defaults for hackney adapter" do
+ initial = Application.get_env(:tesla, :adapter)
+ Application.put_env(:tesla, :adapter, Tesla.Adapter.Hackney)
+ on_exit(fn -> Application.put_env(:tesla, :adapter, initial) end)
- test "with idna domain" do
- assert Connection.format_host("ですexample.com") == 'xn--example-183fne.com'
+ refute %URI{scheme: "https", host: "example.com"}
+ |> Connection.options()
+ |> Keyword.delete(:pool) == []
end
- test "with ipv4" do
- assert Connection.format_host("127.0.0.1") == '127.0.0.1'
- end
+ test "adding defaults for gun adapter" do
+ initial = Application.get_env(:tesla, :adapter)
+ Application.put_env(:tesla, :adapter, Tesla.Adapter.Gun)
+ on_exit(fn -> Application.put_env(:tesla, :adapter, initial) end)
- test "with ipv6" do
- assert Connection.format_host("2a03:2880:f10c:83:face:b00c:0:25de") ==
- '2a03:2880:f10c:83:face:b00c:0:25de'
+ refute %URI{scheme: "https", host: "example.com"}
+ |> Connection.options()
+ |> Keyword.delete(:pool) == []
end
end
end
diff --git a/test/http/gun_test.exs b/test/http/gun_test.exs
new file mode 100644
index 000000000..c64cf7125
--- /dev/null
+++ b/test/http/gun_test.exs
@@ -0,0 +1,84 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.HTTP.GunTest do
+ use ExUnit.Case
+ use Pleroma.Tests.Helpers
+
+ alias Pleroma.HTTP.Gun
+
+ describe "options/1" do
+ test "https url with default port" do
+ uri = URI.parse("https://example.com")
+
+ opts = Gun.options([reuse_conn: false], uri)
+ assert opts[:certificates_verification]
+ assert opts[:tls_opts][:log_level] == :warning
+ assert opts[:reuse_conn] == false
+ end
+
+ test "https ipv4 with default port" do
+ uri = URI.parse("https://127.0.0.1")
+
+ opts = Gun.options([reuse_conn: false], uri)
+ assert opts[:certificates_verification]
+ end
+
+ test "https ipv6 with default port" do
+ uri = URI.parse("https://[2a03:2880:f10c:83:face:b00c:0:25de]")
+
+ opts = Gun.options([reuse_conn: false], uri)
+ assert opts[:certificates_verification]
+ end
+
+ test "https url with non standart port" do
+ uri = URI.parse("https://example.com:115")
+
+ opts = Gun.options([reuse_conn: false], uri)
+
+ assert opts[:certificates_verification]
+ end
+
+ test "merges with defaul http adapter config" do
+ clear_config([:http, :adapter], a: 1, b: 2)
+ defaults = Gun.options([reuse_conn: false], URI.parse("https://example.com"))
+ assert Keyword.has_key?(defaults, :a)
+ assert Keyword.has_key?(defaults, :b)
+ end
+
+ test "default ssl adapter opts with connection" do
+ uri = URI.parse("https://some-domain.com")
+
+ opts = Gun.options(uri)
+
+ assert opts[:certificates_verification]
+ assert opts[:reuse_conn]
+ refute opts[:tls_opts] == []
+ end
+
+ test "parses string proxy host & port" do
+ clear_config([:http, :proxy_url], "localhost:8123")
+
+ uri = URI.parse("https://some-domain.com")
+ opts = Gun.options([reuse_conn: false], uri)
+ assert opts[:proxy] == {'localhost', 8123}
+ end
+
+ test "parses tuple proxy scheme host and port" do
+ clear_config([:http, :proxy_url], {:socks, 'localhost', 1234})
+
+ uri = URI.parse("https://some-domain.com")
+ opts = Gun.options([reuse_conn: false], uri)
+ assert opts[:proxy] == {:socks, 'localhost', 1234}
+ end
+
+ test "passed opts have more weight than defaults" do
+ clear_config([:http, :proxy_url], {:socks5, 'localhost', 1234})
+
+ uri = URI.parse("https://some-domain.com")
+ opts = Gun.options([reuse_conn: false, proxy: {'example.com', 4321}], uri)
+ assert opts[:proxy] == {'example.com', 4321}
+ end
+ end
+end
diff --git a/test/http/adapter_helper/hackney_test.exs b/test/http/hackney_test.exs
index 3f7e708e0..492eb511e 100644
--- a/test/http/adapter_helper/hackney_test.exs
+++ b/test/http/hackney_test.exs
@@ -2,11 +2,11 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.HTTP.AdapterHelper.HackneyTest do
+defmodule Pleroma.HTTP.HackneyTest do
use ExUnit.Case, async: true
use Pleroma.Tests.Helpers
- alias Pleroma.HTTP.AdapterHelper.Hackney
+ alias Pleroma.HTTP.Hackney
setup_all do
uri = URI.parse("http://domain.com")
diff --git a/test/http/middleware/follow_redirects_test.exs b/test/http/middleware/follow_redirects_test.exs
new file mode 100644
index 000000000..533aff2c1
--- /dev/null
+++ b/test/http/middleware/follow_redirects_test.exs
@@ -0,0 +1,284 @@
+defmodule Pleroma.HTTP.Middleware.FollowRedirectsTest do
+ use ExUnit.Case
+
+ import Mox
+
+ alias Pleroma.Gun.Conn
+ alias Pleroma.HTTP.Middleware.FollowRedirects
+ alias Pleroma.Pool.Connections
+
+ setup :verify_on_exit!
+ setup :set_mox_global
+
+ defp gun_mock do
+ Pleroma.GunMock
+ |> stub(:open, fn _, _, _ ->
+ Task.start_link(fn -> Process.sleep(1000) end)
+ end)
+ |> stub(:await_up, fn _, _ -> {:ok, :http} end)
+ |> stub(:set_owner, fn _, _ -> :ok end)
+
+ :ok
+ end
+
+ setup do
+ gun_mock()
+
+ env = %Tesla.Env{
+ body: "",
+ headers: [
+ {"user-agent", "Pleroma"}
+ ],
+ method: :get,
+ opts: [
+ adapter: [
+ pool: :media,
+ reuse_conn: true
+ ]
+ ]
+ }
+
+ {:ok, env: env}
+ end
+
+ defmodule NoRedirect do
+ def call(env, _opts) do
+ opts = env.opts[:adapter]
+ assert opts[:reuse_conn]
+ assert opts[:conn]
+ assert opts[:close_conn] == false
+
+ {:ok, %{env | status: 200, body: opts[:conn]}}
+ end
+ end
+
+ describe "checkin/checkout conn without redirects" do
+ setup do
+ next = [{NoRedirect, :call, [[]]}]
+ {:ok, next: next}
+ end
+
+ test "common", %{env: env, next: next} do
+ env = %{env | url: "https://common.com/media/common.jpg"}
+ assert {:ok, %{body: conn}} = FollowRedirects.call(env, next)
+
+ assert match?(
+ %Connections{
+ conns: %{
+ "https:common.com:443" => %Conn{
+ awaited_by: [],
+ conn: ^conn,
+ conn_state: :idle,
+ gun_state: :up,
+ retries: 0,
+ used_by: []
+ }
+ }
+ },
+ Connections.get_state(:gun_connections)
+ )
+ end
+
+ test "reverse proxy call", %{env: env, next: next} do
+ env =
+ put_in(env.opts[:adapter][:body_as], :chunks)
+ |> Map.put(:url, "https://chunks.com/media/chunks.jpg")
+
+ assert {:ok, %{body: conn}} = FollowRedirects.call(env, next)
+
+ self = self()
+
+ assert match?(
+ %Connections{
+ conns: %{
+ "https:chunks.com:443" => %Conn{
+ awaited_by: [],
+ conn: ^conn,
+ conn_state: :active,
+ gun_state: :up,
+ retries: 0,
+ used_by: [{^self, _}]
+ }
+ }
+ },
+ Connections.get_state(:gun_connections)
+ )
+ end
+ end
+
+ defmodule OneRedirect do
+ def call(%{url: "https://first-redirect.com"} = env, _opts) do
+ opts = env.opts[:adapter]
+ assert opts[:reuse_conn]
+ assert opts[:conn]
+ assert opts[:close_conn] == false
+
+ {:ok, %{env | status: 302, body: opts[:conn], headers: [{"location", opts[:final_url]}]}}
+ end
+
+ def call(env, _opts) do
+ opts = env.opts[:adapter]
+ assert opts[:reuse_conn]
+ assert opts[:conn]
+ assert opts[:close_conn] == false
+
+ {:ok, %{env | status: 200, body: opts[:conn]}}
+ end
+ end
+
+ describe "checkin/checkout with 1 redirect" do
+ setup do
+ next = [{OneRedirect, :call, [[]]}]
+
+ {:ok, next: next}
+ end
+
+ test "common with redirect", %{env: env, next: next} do
+ adapter_opts = Keyword.put(env.opts[:adapter], :final_url, "https://another-final-url.com")
+
+ env =
+ put_in(env.opts[:adapter], adapter_opts)
+ |> Map.put(:url, "https://first-redirect.com")
+
+ assert {:ok, %{body: conn}} = FollowRedirects.call(env, next)
+
+ assert match?(
+ %Connections{
+ conns: %{
+ "https:first-redirect.com:443" => %Conn{
+ awaited_by: [],
+ conn: _,
+ conn_state: :idle,
+ gun_state: :up,
+ retries: 0,
+ used_by: []
+ },
+ "https:another-final-url.com:443" => %Conn{
+ awaited_by: [],
+ conn: ^conn,
+ conn_state: :idle,
+ gun_state: :up,
+ retries: 0,
+ used_by: []
+ }
+ }
+ },
+ Connections.get_state(:gun_connections)
+ )
+ end
+
+ test "reverse proxy with redirect", %{env: env, next: next} do
+ adapter_opts =
+ Keyword.merge(env.opts[:adapter], body_as: :chunks, final_url: "https://final-url.com")
+
+ env =
+ put_in(env.opts[:adapter], adapter_opts)
+ |> Map.put(:url, "https://first-redirect.com")
+
+ assert {:ok, %{body: conn}} = FollowRedirects.call(env, next)
+
+ self = self()
+
+ assert match?(
+ %Connections{
+ conns: %{
+ "https:first-redirect.com:443" => %Conn{
+ awaited_by: [],
+ conn: _,
+ conn_state: :idle,
+ gun_state: :up,
+ retries: 0,
+ used_by: []
+ },
+ "https:final-url.com:443" => %Conn{
+ awaited_by: [],
+ conn: ^conn,
+ conn_state: :active,
+ gun_state: :up,
+ retries: 0,
+ used_by: [{^self, _}]
+ }
+ }
+ },
+ Connections.get_state(:gun_connections)
+ )
+ end
+ end
+
+ defmodule TwoRedirect do
+ def call(%{url: "https://1-redirect.com"} = env, _opts) do
+ opts = env.opts[:adapter]
+ assert opts[:reuse_conn]
+ assert opts[:conn]
+ assert opts[:close_conn] == false
+
+ {:ok,
+ %{env | status: 302, body: opts[:conn], headers: [{"location", "https://2-redirect.com"}]}}
+ end
+
+ def call(%{url: "https://2-redirect.com"} = env, _opts) do
+ opts = env.opts[:adapter]
+ assert opts[:reuse_conn]
+ assert opts[:conn]
+ assert opts[:close_conn] == false
+
+ {:ok, %{env | status: 302, body: opts[:conn], headers: [{"location", opts[:final_url]}]}}
+ end
+
+ def call(env, _opts) do
+ opts = env.opts[:adapter]
+ assert opts[:reuse_conn]
+ assert opts[:conn]
+ assert opts[:close_conn] == false
+
+ {:ok, %{env | status: 200, body: opts[:conn]}}
+ end
+ end
+
+ describe "checkin/checkout conn with max redirects" do
+ setup do
+ next = [{TwoRedirect, :call, [[]]}]
+ {:ok, next: next}
+ end
+
+ test "common with max redirects", %{env: env, next: next} do
+ adapter_opts =
+ Keyword.merge(env.opts[:adapter],
+ final_url: "https://some-final-url.com"
+ )
+
+ env =
+ put_in(env.opts[:adapter], adapter_opts)
+ |> Map.put(:url, "https://1-redirect.com")
+
+ assert match?(
+ {:error, {FollowRedirects, :too_many_redirects}},
+ FollowRedirects.call(env, next, max_redirects: 1)
+ )
+
+ assert match?(
+ %Connections{
+ conns: %{
+ "https:1-redirect.com:443" => %Conn{
+ awaited_by: [],
+ conn: _,
+ conn_state: :idle,
+ gun_state: :up,
+ retries: 0,
+ used_by: []
+ },
+ "https:2-redirect.com:443" => %Conn{
+ awaited_by: [],
+ conn: _,
+ conn_state: :idle,
+ gun_state: :up,
+ retries: 0,
+ used_by: []
+ }
+ }
+ },
+ Connections.get_state(:gun_connections)
+ )
+ end
+ end
+end
diff --git a/test/http/proxy_test.exs b/test/http/proxy_test.exs
new file mode 100644
index 000000000..ba5a9ca85
--- /dev/null
+++ b/test/http/proxy_test.exs
@@ -0,0 +1,90 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.HTTP.ProxyTest do
+ use ExUnit.Case, async: true
+ use Pleroma.Tests.Helpers
+
+ import ExUnit.CaptureLog
+
+ alias Pleroma.HTTP.Proxy
+
+ describe "parse_proxy/1" do
+ test "ip with port" do
+ assert Proxy.parse_proxy("127.0.0.1:8123") == {:ok, {127, 0, 0, 1}, 8123}
+ end
+
+ test "host with port" do
+ assert Proxy.parse_proxy("localhost:8123") == {:ok, 'localhost', 8123}
+ end
+
+ test "as tuple" do
+ assert Proxy.parse_proxy({:socks4, :localhost, 9050}) ==
+ {:ok, :socks4, 'localhost', 9050}
+ end
+
+ test "as tuple with string host" do
+ assert Proxy.parse_proxy({:socks5, "localhost", 9050}) ==
+ {:ok, :socks5, 'localhost', 9050}
+ end
+ end
+
+ describe "parse_proxy/1 errors" do
+ test "ip without port" do
+ capture_log(fn ->
+ assert Proxy.parse_proxy("127.0.0.1") == {:error, :invalid_proxy}
+ end) =~ "parsing proxy fail \"127.0.0.1\""
+ end
+
+ test "host without port" do
+ capture_log(fn ->
+ assert Proxy.parse_proxy("localhost") == {:error, :invalid_proxy}
+ end) =~ "parsing proxy fail \"localhost\""
+ end
+
+ test "host with bad port" do
+ capture_log(fn ->
+ assert Proxy.parse_proxy("localhost:port") == {:error, :invalid_proxy_port}
+ end) =~ "parsing port in proxy fail \"localhost:port\""
+ end
+
+ test "ip with bad port" do
+ capture_log(fn ->
+ assert Proxy.parse_proxy("127.0.0.1:15.9") == {:error, :invalid_proxy_port}
+ end) =~ "parsing port in proxy fail \"127.0.0.1:15.9\""
+ end
+
+ test "as tuple without port" do
+ capture_log(fn ->
+ assert Proxy.parse_proxy({:socks5, :localhost}) == {:error, :invalid_proxy}
+ end) =~ "parsing proxy fail {:socks5, :localhost}"
+ end
+
+ test "with nil" do
+ assert Proxy.parse_proxy(nil) == nil
+ end
+ end
+
+ describe "maybe_add_proxy/1" do
+ test "proxy as ip with port" do
+ clear_config([:http, :proxy_url], "127.0.0.1:8123")
+
+ assert Proxy.maybe_add_proxy([]) == [proxy: {{127, 0, 0, 1}, 8123}]
+ end
+
+ test "proxy as localhost with port" do
+ clear_config([:http, :proxy_url], "localhost:8123")
+ assert Proxy.maybe_add_proxy([]) == [proxy: {'localhost', 8123}]
+ end
+
+ test "proxy as tuple" do
+ clear_config([:http, :proxy_url], {:socks4, :localhost, 9050})
+ assert Proxy.maybe_add_proxy([]) == [proxy: {:socks4, 'localhost', 9050}]
+ end
+
+ test "without proxy" do
+ assert Proxy.maybe_add_proxy([]) == []
+ end
+ end
+end
diff --git a/test/http/request_builder_test.exs b/test/http/request/builder_test.exs
index f11528c3f..eda87fad1 100644
--- a/test/http/request_builder_test.exs
+++ b/test/http/request/builder_test.exs
@@ -2,26 +2,26 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.HTTP.RequestBuilderTest do
+defmodule Pleroma.HTTP.Request.BuilderTest do
use ExUnit.Case, async: true
use Pleroma.Tests.Helpers
alias Pleroma.Config
alias Pleroma.HTTP.Request
- alias Pleroma.HTTP.RequestBuilder
+ alias Pleroma.HTTP.Request.Builder
describe "headers/2" do
setup do: clear_config([:http, :send_user_agent])
setup do: clear_config([:http, :user_agent])
test "don't send pleroma user agent" do
- assert RequestBuilder.headers(%Request{}, []) == %Request{headers: []}
+ assert Builder.headers(%Request{}, []) == %Request{headers: []}
end
test "send pleroma user agent" do
Config.put([:http, :send_user_agent], true)
Config.put([:http, :user_agent], :default)
- assert RequestBuilder.headers(%Request{}, []) == %Request{
+ assert Builder.headers(%Request{}, []) == %Request{
headers: [{"user-agent", Pleroma.Application.user_agent()}]
}
end
@@ -30,7 +30,7 @@ defmodule Pleroma.HTTP.RequestBuilderTest do
Config.put([:http, :send_user_agent], true)
Config.put([:http, :user_agent], "totally-not-pleroma")
- assert RequestBuilder.headers(%Request{}, []) == %Request{
+ assert Builder.headers(%Request{}, []) == %Request{
headers: [{"user-agent", "totally-not-pleroma"}]
}
end
@@ -55,7 +55,7 @@ defmodule Pleroma.HTTP.RequestBuilderTest do
}
]
}
- } = RequestBuilder.add_param(%Request{}, :file, "filename.png", "some-path/filename.png")
+ } = Builder.add_param(%Request{}, :file, "filename.png", "some-path/filename.png")
end
test "add key to body" do
@@ -71,17 +71,17 @@ defmodule Pleroma.HTTP.RequestBuilderTest do
}
]
}
- } = RequestBuilder.add_param(%{}, :body, "somekey", "someval")
+ } = Builder.add_param(%{}, :body, "somekey", "someval")
end
test "add form parameter" do
- assert RequestBuilder.add_param(%{}, :form, "somename", "someval") == %{
+ assert Builder.add_param(%{}, :form, "somename", "someval") == %{
body: %{"somename" => "someval"}
}
end
test "add for location" do
- assert RequestBuilder.add_param(%{}, :some_location, "somekey", "someval") == %{
+ assert Builder.add_param(%{}, :some_location, "somekey", "someval") == %{
some_location: [{"somekey", "someval"}]
}
end