diff options
author | Alexander Strizhakov <alex.strizhakov@gmail.com> | 2020-04-29 10:31:41 +0300 |
---|---|---|
committer | Alexander Strizhakov <alex.strizhakov@gmail.com> | 2020-05-06 13:25:48 +0300 |
commit | 73b1b3ff5e2761c62738e85ea1a6f822c718be2f (patch) | |
tree | 818a78f43b96bce51ea55b77db1aba26c9895b75 /lib | |
parent | 5ca7b9f6908cf3ec3cfb68bbaffcf72e85361b41 (diff) | |
download | pleroma-73b1b3ff5e2761c62738e85ea1a6f822c718be2f.tar.gz |
fixing connection leaks
Diffstat (limited to 'lib')
-rw-r--r-- | lib/mix/tasks/pleroma/benchmark.ex | 4 | ||||
-rw-r--r-- | lib/pleroma/gun/conn.ex | 13 | ||||
-rw-r--r-- | lib/pleroma/http/adapter_helper.ex | 41 | ||||
-rw-r--r-- | lib/pleroma/http/adapter_helper/gun.ex | 82 | ||||
-rw-r--r-- | lib/pleroma/http/adapter_helper/hackney.ex | 43 | ||||
-rw-r--r-- | lib/pleroma/http/connection.ex | 103 | ||||
-rw-r--r-- | lib/pleroma/http/gun.ex | 65 | ||||
-rw-r--r-- | lib/pleroma/http/hackney.ex | 49 | ||||
-rw-r--r-- | lib/pleroma/http/http.ex | 41 | ||||
-rw-r--r-- | lib/pleroma/http/middleware/follow_redirects.ex | 109 | ||||
-rw-r--r-- | lib/pleroma/http/proxy.ex | 67 | ||||
-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.ex | 12 |
14 files changed, 328 insertions, 305 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 |