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 -# 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 -# 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() + adapter = Application.get_env(:tesla, :adapter) + + [pool: :federation] |> Keyword.merge(opts) - |> adapter_helper().options(uri) + |> adapter_options(uri, adapter) 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 + @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) - 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 - - 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 - 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 + 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 +# 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 similarity index 95% rename from lib/pleroma/http/request_builder.ex rename to 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 # 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 similarity index 100% rename from lib/pleroma/http/request.ex rename to 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 -# 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 -# 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} + assert Connection.format_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 - 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} - end - end - - describe "format_host/1" do - test "with domain" do - assert Connection.format_host("example.com") == 'example.com' + assert Connection.options(%URI{}, pool: :media) == [env: :test, pool: :media] end - test "with idna domain" do - assert Connection.format_host("ですexample.com") == 'xn--example-183fne.com' + 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) + + 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 +# 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 similarity index 92% rename from test/http/adapter_helper/hackney_test.exs rename to 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 # 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 +# 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 similarity index 77% rename from test/http/request_builder_test.exs rename to 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 # 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