aboutsummaryrefslogtreecommitdiff
path: root/lib/pleroma/http
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pleroma/http')
-rw-r--r--lib/pleroma/http/adapter_helper.ex41
-rw-r--r--lib/pleroma/http/adapter_helper/gun.ex77
-rw-r--r--lib/pleroma/http/adapter_helper/hackney.ex43
-rw-r--r--lib/pleroma/http/connection.ex135
-rw-r--r--lib/pleroma/http/http.ex135
-rw-r--r--lib/pleroma/http/request.ex23
-rw-r--r--lib/pleroma/http/request_builder.ex121
7 files changed, 411 insertions, 164 deletions
diff --git a/lib/pleroma/http/adapter_helper.ex b/lib/pleroma/http/adapter_helper.ex
new file mode 100644
index 000000000..510722ff9
--- /dev/null
+++ b/lib/pleroma/http/adapter_helper.ex
@@ -0,0 +1,41 @@
+# 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
new file mode 100644
index 000000000..ead7cdc6b
--- /dev/null
+++ b/lib/pleroma/http/adapter_helper/gun.ex
@@ -0,0 +1,77 @@
+# 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
+ opts
+ |> Keyword.put(:certificates_verification, true)
+ |> Keyword.put(:tls_opts, log_level: :warning)
+ 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 ->
+ Task.start(Pleroma.Gun.Conn, :open, [uri, :gun_connections, opts])
+ 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
new file mode 100644
index 000000000..dcb4cac71
--- /dev/null
+++ b/lib/pleroma/http/adapter_helper/hackney.ex
@@ -0,0 +1,43 @@
+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 80e6c30d6..ebacf7902 100644
--- a/lib/pleroma/http/connection.ex
+++ b/lib/pleroma/http/connection.ex
@@ -4,40 +4,121 @@
defmodule Pleroma.HTTP.Connection do
@moduledoc """
- Connection for http-requests.
+ Configure Tesla.Client with default and customized adapter options.
"""
- @hackney_options [
- connect_timeout: 10_000,
- recv_timeout: 20_000,
- follow_redirect: true,
- force_redirect: true,
- pool: :federation
- ]
- @adapter Application.get_env(:tesla, :adapter)
+ alias Pleroma.Config
+ alias Pleroma.HTTP.AdapterHelper
- @doc """
- Configure a client connection
+ require Logger
+
+ @defaults [pool: :federation]
- # Returns
+ @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()
- Tesla.Env.client
+ @doc """
+ Merge default connection & adapter options with received ones.
"""
- @spec new(Keyword.t()) :: Tesla.Env.client()
- def new(opts \\ []) do
- Tesla.client([], {@adapter, hackney_options(opts)})
+
+ @spec options(URI.t(), keyword()) :: keyword()
+ def options(%URI{} = uri, opts \\ []) do
+ @defaults
+ |> pool_timeout()
+ |> Keyword.merge(opts)
+ |> adapter_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
+
+ 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
- # fetch Hackney options
- #
- def hackney_options(opts) do
- options = Keyword.get(opts, :adapter, [])
- adapter_options = Pleroma.Config.get([:http, :adapter], [])
- proxy_url = Pleroma.Config.get([:http, :proxy_url], nil)
-
- @hackney_options
- |> Keyword.merge(adapter_options)
- |> Keyword.merge(options)
- |> Keyword.merge(proxy: proxy_url)
+ @spec parse_host(String.t() | atom() | charlist()) :: charlist() | ip_address()
+ def parse_host(host) when is_list(host), do: host
+ def parse_host(host) when is_atom(host), do: to_charlist(host)
+
+ def parse_host(host) when is_binary(host) do
+ host = to_charlist(host)
+
+ case :inet.parse_address(host) do
+ {:error, :einval} -> host
+ {:ok, ip} -> ip
+ end
+ end
+
+ @spec format_host(String.t()) :: charlist()
+ def format_host(host) do
+ host_charlist = to_charlist(host)
+
+ case :inet.parse_address(host_charlist) do
+ {:error, :einval} ->
+ :idna.encode(host_charlist)
+
+ {:ok, _ip} ->
+ host_charlist
+ end
end
end
diff --git a/lib/pleroma/http/http.ex b/lib/pleroma/http/http.ex
index ee5b5e127..583b56484 100644
--- a/lib/pleroma/http/http.ex
+++ b/lib/pleroma/http/http.ex
@@ -4,21 +4,47 @@
defmodule Pleroma.HTTP do
@moduledoc """
-
+ Wrapper for `Tesla.request/2`.
"""
alias Pleroma.HTTP.Connection
+ alias Pleroma.HTTP.Request
alias Pleroma.HTTP.RequestBuilder, as: Builder
+ alias Tesla.Client
+ alias Tesla.Env
+
+ require Logger
@type t :: __MODULE__
@doc """
- Builds and perform http request.
+ Performs GET request.
+
+ See `Pleroma.HTTP.request/5`
+ """
+ @spec get(Request.url() | nil, Request.headers(), keyword()) ::
+ nil | {:ok, Env.t()} | {:error, any()}
+ def get(url, headers \\ [], options \\ [])
+ def get(nil, _, _), do: nil
+ def get(url, headers, options), do: request(:get, url, "", headers, options)
+
+ @doc """
+ Performs POST request.
+
+ See `Pleroma.HTTP.request/5`
+ """
+ @spec post(Request.url(), String.t(), Request.headers(), keyword()) ::
+ {:ok, Env.t()} | {:error, any()}
+ def post(url, body, headers \\ [], options \\ []),
+ do: request(:post, url, body, headers, options)
+
+ @doc """
+ Builds and performs http request.
# Arguments:
`method` - :get, :post, :put, :delete
- `url`
- `body`
+ `url` - full url
+ `body` - request body
`headers` - a keyworld list of headers, e.g. `[{"content-type", "text/plain"}]`
`options` - custom, per-request middleware or adapter options
@@ -26,61 +52,66 @@ defmodule Pleroma.HTTP do
`{:ok, %Tesla.Env{}}` or `{:error, error}`
"""
- def request(method, url, body \\ "", headers \\ [], options \\ []) do
- try do
- options =
- process_request_options(options)
- |> process_sni_options(url)
-
- params = Keyword.get(options, :params, [])
-
- %{}
- |> Builder.method(method)
- |> Builder.headers(headers)
- |> Builder.opts(options)
- |> Builder.url(url)
- |> Builder.add_param(:body, :body, body)
- |> Builder.add_param(:query, :query, params)
- |> Enum.into([])
- |> (&Tesla.request(Connection.new(options), &1)).()
- rescue
- e ->
- {:error, e}
- catch
- :exit, e ->
- {:error, e}
- end
- end
+ @spec request(atom(), Request.url(), String.t(), Request.headers(), keyword()) ::
+ {:ok, Env.t()} | {:error, any()}
+ def request(method, url, body, headers, options) when is_binary(url) do
+ uri = URI.parse(url)
+ adapter_opts = Connection.options(uri, options[:adapter] || [])
+ options = put_in(options[:adapter], adapter_opts)
+ params = options[:params] || []
+ request = build_request(method, headers, options, url, body, params)
- defp process_sni_options(options, nil), do: options
+ adapter = Application.get_env(:tesla, :adapter)
+ client = Tesla.client([Tesla.Middleware.FollowRedirects], adapter)
- defp process_sni_options(options, url) do
- uri = URI.parse(url)
- host = uri.host |> to_charlist()
+ pid = Process.whereis(adapter_opts[:pool])
- case uri.scheme do
- "https" -> options ++ [ssl: [server_name_indication: host]]
- _ -> options
- end
- end
+ 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)
- def process_request_options(options) do
- Keyword.merge(Pleroma.HTTP.Connection.hackney_options([]), options)
+ response
end
- @doc """
- Performs GET request.
+ @spec request(Client.t(), keyword(), map()) :: {:ok, Env.t()} | {:error, any()}
+ def request(%Client{} = client, request, %{env: :test}), do: request(client, request)
- See `Pleroma.HTTP.request/5`
- """
- def get(url, headers \\ [], options \\ []),
- do: request(:get, url, "", headers, options)
+ def request(%Client{} = client, request, %{body_as: :chunks}), do: request(client, request)
- @doc """
- Performs POST request.
+ def request(%Client{} = client, request, %{pool_alive?: false}), do: request(client, request)
- See `Pleroma.HTTP.request/5`
- """
- def post(url, body, headers \\ [], options \\ []),
- do: request(:post, url, body, headers, options)
+ def request(%Client{} = client, request, %{pool: pool, timeout: timeout}) do
+ :poolboy.transaction(
+ pool,
+ &Pleroma.Pool.Request.execute(&1, client, request, timeout),
+ timeout
+ )
+ end
+
+ @spec request(Client.t(), keyword()) :: {:ok, Env.t()} | {:error, any()}
+ def request(client, request), do: Tesla.request(client, request)
+
+ defp build_request(method, headers, options, url, body, params) do
+ Builder.new()
+ |> Builder.method(method)
+ |> Builder.headers(headers)
+ |> Builder.opts(options)
+ |> Builder.url(url)
+ |> Builder.add_param(:body, :body, body)
+ |> Builder.add_param(:query, :query, params)
+ |> Builder.convert_to_keyword()
+ end
end
diff --git a/lib/pleroma/http/request.ex b/lib/pleroma/http/request.ex
new file mode 100644
index 000000000..761bd6ccf
--- /dev/null
+++ b/lib/pleroma/http/request.ex
@@ -0,0 +1,23 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.HTTP.Request do
+ @moduledoc """
+ Request struct.
+ """
+ defstruct method: :get, url: "", query: [], headers: [], body: "", opts: []
+
+ @type method :: :head | :get | :delete | :trace | :options | :post | :put | :patch
+ @type url :: String.t()
+ @type headers :: [{String.t(), String.t()}]
+
+ @type t :: %__MODULE__{
+ method: method(),
+ url: url(),
+ query: keyword(),
+ headers: headers(),
+ body: String.t(),
+ opts: keyword()
+ }
+end
diff --git a/lib/pleroma/http/request_builder.ex b/lib/pleroma/http/request_builder.ex
index 77ef4bfd8..2fc876d92 100644
--- a/lib/pleroma/http/request_builder.ex
+++ b/lib/pleroma/http/request_builder.ex
@@ -7,136 +7,87 @@ defmodule Pleroma.HTTP.RequestBuilder do
Helper functions for building Tesla requests
"""
- @doc """
- Specify the request method when building a request
-
- ## Parameters
-
- - request (Map) - Collected request options
- - m (atom) - Request method
-
- ## Returns
+ alias Pleroma.HTTP.Request
+ alias Tesla.Multipart
- Map
+ @doc """
+ Creates new request
"""
- @spec method(map(), atom) :: map()
- def method(request, m) do
- Map.put_new(request, :method, m)
- end
+ @spec new(Request.t()) :: Request.t()
+ def new(%Request{} = request \\ %Request{}), do: request
@doc """
Specify the request method when building a request
+ """
+ @spec method(Request.t(), Request.method()) :: Request.t()
+ def method(request, m), do: %{request | method: m}
- ## Parameters
-
- - request (Map) - Collected request options
- - u (String) - Request URL
-
- ## Returns
-
- Map
+ @doc """
+ Specify the request method when building a request
"""
- @spec url(map(), String.t()) :: map()
- def url(request, u) do
- Map.put_new(request, :url, u)
- end
+ @spec url(Request.t(), Request.url()) :: Request.t()
+ def url(request, u), do: %{request | url: u}
@doc """
Add headers to the request
"""
- @spec headers(map(), list(tuple)) :: map()
- def headers(request, header_list) do
- header_list =
+ @spec headers(Request.t(), Request.headers()) :: Request.t()
+ def headers(request, headers) do
+ headers_list =
if Pleroma.Config.get([:http, :send_user_agent]) do
- header_list ++ [{"User-Agent", Pleroma.Application.user_agent()}]
+ [{"user-agent", Pleroma.Application.user_agent()} | headers]
else
- header_list
+ headers
end
- Map.put_new(request, :headers, header_list)
+ %{request | headers: headers_list}
end
@doc """
Add custom, per-request middleware or adapter options to the request
"""
- @spec opts(map(), Keyword.t()) :: map()
- def opts(request, options) do
- Map.put_new(request, :opts, options)
- end
-
- @doc """
- Add optional parameters to the request
-
- ## Parameters
-
- - request (Map) - Collected request options
- - definitions (Map) - Map of parameter name to parameter location.
- - options (KeywordList) - The provided optional parameters
-
- ## Returns
-
- Map
- """
- @spec add_optional_params(map(), %{optional(atom) => atom}, keyword()) :: map()
- def add_optional_params(request, _, []), do: request
-
- def add_optional_params(request, definitions, [{key, value} | tail]) do
- case definitions do
- %{^key => location} ->
- request
- |> add_param(location, key, value)
- |> add_optional_params(definitions, tail)
-
- _ ->
- add_optional_params(request, definitions, tail)
- end
- end
+ @spec opts(Request.t(), keyword()) :: Request.t()
+ def opts(request, options), do: %{request | opts: options}
@doc """
Add optional parameters to the request
-
- ## Parameters
-
- - request (Map) - Collected request options
- - location (atom) - Where to put the parameter
- - key (atom) - The name of the parameter
- - value (any) - The value of the parameter
-
- ## Returns
-
- Map
"""
- @spec add_param(map(), atom, atom, any()) :: map()
- def add_param(request, :query, :query, values), do: Map.put(request, :query, values)
+ @spec add_param(Request.t(), atom(), atom(), any()) :: Request.t()
+ def add_param(request, :query, :query, values), do: %{request | query: values}
- def add_param(request, :body, :body, value), do: Map.put(request, :body, value)
+ def add_param(request, :body, :body, value), do: %{request | body: value}
def add_param(request, :body, key, value) do
request
- |> Map.put_new_lazy(:body, &Tesla.Multipart.new/0)
+ |> Map.put(:body, Multipart.new())
|> Map.update!(
:body,
- &Tesla.Multipart.add_field(
+ &Multipart.add_field(
&1,
key,
Jason.encode!(value),
- headers: [{:"Content-Type", "application/json"}]
+ headers: [{"content-type", "application/json"}]
)
)
end
def add_param(request, :file, name, path) do
request
- |> Map.put_new_lazy(:body, &Tesla.Multipart.new/0)
- |> Map.update!(:body, &Tesla.Multipart.add_file(&1, path, name: name))
+ |> Map.put(:body, Multipart.new())
+ |> Map.update!(:body, &Multipart.add_file(&1, path, name: name))
end
def add_param(request, :form, name, value) do
- request
- |> Map.update(:body, %{name => value}, &Map.put(&1, name, value))
+ Map.update(request, :body, %{name => value}, &Map.put(&1, name, value))
end
def add_param(request, location, key, value) do
Map.update(request, location, [{key, value}], &(&1 ++ [{key, value}]))
end
+
+ def convert_to_keyword(request) do
+ request
+ |> Map.from_struct()
+ |> Enum.into([])
+ end
end