parent
962eb8d4ac
commit
514c899275
@ -0,0 +1,26 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Gun.API do
|
||||
@callback open(charlist(), pos_integer(), map()) :: {:ok, pid()}
|
||||
@callback info(pid()) :: map()
|
||||
@callback close(pid()) :: :ok
|
||||
@callback await_up(pid) :: {:ok, atom()} | {:error, atom()}
|
||||
@callback connect(pid(), map()) :: reference()
|
||||
@callback await(pid(), reference()) :: {:response, :fin, 200, []}
|
||||
|
||||
def open(host, port, opts), do: api().open(host, port, opts)
|
||||
|
||||
def info(pid), do: api().info(pid)
|
||||
|
||||
def close(pid), do: api().close(pid)
|
||||
|
||||
def await_up(pid), do: api().await_up(pid)
|
||||
|
||||
def connect(pid, opts), do: api().connect(pid, opts)
|
||||
|
||||
def await(pid, ref), do: api().await(pid, ref)
|
||||
|
||||
defp api, do: Pleroma.Config.get([Pleroma.Gun.API], Pleroma.Gun)
|
||||
end
|
@ -0,0 +1,151 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Gun.API.Mock do
|
||||
@behaviour Pleroma.Gun.API
|
||||
|
||||
alias Pleroma.Gun.API
|
||||
|
||||
@impl API
|
||||
def open('some-domain.com', 443, _) do
|
||||
{:ok, conn_pid} = Task.start_link(fn -> Process.sleep(1_000) end)
|
||||
|
||||
Registry.register(API.Mock, conn_pid, %{
|
||||
origin_scheme: "https",
|
||||
origin_host: 'some-domain.com',
|
||||
origin_port: 443
|
||||
})
|
||||
|
||||
{:ok, conn_pid}
|
||||
end
|
||||
|
||||
@impl API
|
||||
def open(ip, port, _)
|
||||
when ip in [{10_755, 10_368, 61_708, 131, 64_206, 45_068, 0, 9_694}, {127, 0, 0, 1}] and
|
||||
port in [80, 443] do
|
||||
{:ok, conn_pid} = Task.start_link(fn -> Process.sleep(1_000) end)
|
||||
|
||||
scheme = if port == 443, do: "https", else: "http"
|
||||
|
||||
Registry.register(API.Mock, conn_pid, %{
|
||||
origin_scheme: scheme,
|
||||
origin_host: ip,
|
||||
origin_port: port
|
||||
})
|
||||
|
||||
{:ok, conn_pid}
|
||||
end
|
||||
|
||||
@impl API
|
||||
def open('localhost', 1234, %{
|
||||
protocols: [:socks],
|
||||
proxy: {:socks5, 'localhost', 1234},
|
||||
socks_opts: %{host: 'proxy-socks.com', port: 80, version: 5}
|
||||
}) do
|
||||
{:ok, conn_pid} = Task.start_link(fn -> Process.sleep(1_000) end)
|
||||
|
||||
Registry.register(API.Mock, conn_pid, %{
|
||||
origin_scheme: "http",
|
||||
origin_host: 'proxy-socks.com',
|
||||
origin_port: 80
|
||||
})
|
||||
|
||||
{:ok, conn_pid}
|
||||
end
|
||||
|
||||
@impl API
|
||||
def open('localhost', 1234, %{
|
||||
protocols: [:socks],
|
||||
proxy: {:socks4, 'localhost', 1234},
|
||||
socks_opts: %{
|
||||
host: 'proxy-socks.com',
|
||||
port: 443,
|
||||
protocols: [:http2],
|
||||
tls_opts: [],
|
||||
transport: :tls,
|
||||
version: 4
|
||||
}
|
||||
}) do
|
||||
{:ok, conn_pid} = Task.start_link(fn -> Process.sleep(1_000) end)
|
||||
|
||||
Registry.register(API.Mock, conn_pid, %{
|
||||
origin_scheme: "https",
|
||||
origin_host: 'proxy-socks.com',
|
||||
origin_port: 443
|
||||
})
|
||||
|
||||
{:ok, conn_pid}
|
||||
end
|
||||
|
||||
@impl API
|
||||
def open('gun-not-up.com', 80, _opts), do: {:error, :timeout}
|
||||
|
||||
@impl API
|
||||
def open('example.com', port, _) when port in [443, 115] do
|
||||
{:ok, conn_pid} = Task.start_link(fn -> Process.sleep(1_000) end)
|
||||
|
||||
Registry.register(API.Mock, conn_pid, %{
|
||||
origin_scheme: "https",
|
||||
origin_host: 'example.com',
|
||||
origin_port: 443
|
||||
})
|
||||
|
||||
{:ok, conn_pid}
|
||||
end
|
||||
|
||||
@impl API
|
||||
def open(domain, 80, _) do
|
||||
{:ok, conn_pid} = Task.start_link(fn -> Process.sleep(1_000) end)
|
||||
|
||||
Registry.register(API.Mock, conn_pid, %{
|
||||
origin_scheme: "http",
|
||||
origin_host: domain,
|
||||
origin_port: 80
|
||||
})
|
||||
|
||||
{:ok, conn_pid}
|
||||
end
|
||||
|
||||
@impl API
|
||||
def open({127, 0, 0, 1}, 8123, _) do
|
||||
Task.start_link(fn -> Process.sleep(1_000) end)
|
||||
end
|
||||
|
||||
@impl API
|
||||
def open('localhost', 9050, _) do
|
||||
Task.start_link(fn -> Process.sleep(1_000) end)
|
||||
end
|
||||
|
||||
@impl API
|
||||
def await_up(_pid), do: {:ok, :http}
|
||||
|
||||
@impl API
|
||||
def connect(pid, %{host: _, port: 80}) do
|
||||
ref = make_ref()
|
||||
Registry.register(API.Mock, ref, pid)
|
||||
ref
|
||||
end
|
||||
|
||||
@impl API
|
||||
def connect(pid, %{host: _, port: 443, protocols: [:http2], transport: :tls}) do
|
||||
ref = make_ref()
|
||||
Registry.register(API.Mock, ref, pid)
|
||||
ref
|
||||
end
|
||||
|
||||
@impl API
|
||||
def await(pid, ref) do
|
||||
[{_, ^pid}] = Registry.lookup(API.Mock, ref)
|
||||
{:response, :fin, 200, []}
|
||||
end
|
||||
|
||||
@impl API
|
||||
def info(pid) do
|
||||
[{_, info}] = Registry.lookup(API.Mock, pid)
|
||||
info
|
||||
end
|
||||
|
||||
@impl API
|
||||
def close(_pid), do: :ok
|
||||
end
|
@ -0,0 +1,29 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Gun.Conn do
|
||||
@moduledoc """
|
||||
Struct for gun connection data
|
||||
"""
|
||||
@type gun_state :: :up | :down
|
||||
@type conn_state :: :active | :idle
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
conn: pid(),
|
||||
gun_state: gun_state(),
|
||||
conn_state: conn_state(),
|
||||
used_by: [pid()],
|
||||
last_reference: pos_integer(),
|
||||
crf: float(),
|
||||
retries: pos_integer()
|
||||
}
|
||||
|
||||
defstruct conn: nil,
|
||||
gun_state: :open,
|
||||
conn_state: :init,
|
||||
used_by: [],
|
||||
last_reference: 0,
|
||||
crf: 1,
|
||||
retries: 0
|
||||
end
|
@ -0,0 +1,45 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Gun do
|
||||
@behaviour Pleroma.Gun.API
|
||||
|
||||
alias Pleroma.Gun.API
|
||||
|
||||
@gun_keys [
|
||||
:connect_timeout,
|
||||
:http_opts,
|
||||
:http2_opts,
|
||||
:protocols,
|
||||
:retry,
|
||||
:retry_timeout,
|
||||
:trace,
|
||||
:transport,
|
||||
:tls_opts,
|
||||
:tcp_opts,
|
||||
:socks_opts,
|
||||
:ws_opts
|
||||
]
|
||||
|
||||
@impl API
|
||||
def open(host, port, opts \\ %{}), do: :gun.open(host, port, Map.take(opts, @gun_keys))
|
||||
|
||||
@impl API
|
||||
defdelegate info(pid), to: :gun
|
||||
|
||||
@impl API
|
||||
defdelegate close(pid), to: :gun
|
||||
|
||||
@impl API
|
||||
defdelegate await_up(pid), to: :gun
|
||||
|
||||
@impl API
|
||||
defdelegate connect(pid, opts), to: :gun
|
||||
|
||||
@impl API
|
||||
defdelegate await(pid, ref), to: :gun
|
||||
|
||||
@spec flush(pid() | reference()) :: :ok
|
||||
defdelegate flush(pid), to: :gun
|
||||
end
|
@ -0,0 +1,64 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.HTTP.Adapter do
|
||||
alias Pleroma.HTTP.Connection
|
||||
|
||||
@type proxy ::
|
||||
{Connection.host(), pos_integer()}
|
||||
| {Connection.proxy_type(), pos_integer()}
|
||||
@type host_type :: :domain | :ip
|
||||
|
||||
@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
|
||||
with {:ok, host, port} <- Connection.parse_proxy(proxy_url) do
|
||||
{host, port}
|
||||
else
|
||||
{: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)
|
||||
|
||||
@spec domain_or_fallback(String.t()) :: charlist()
|
||||
def domain_or_fallback(host) do
|
||||
case domain_or_ip(host) do
|
||||
{:domain, domain} -> domain
|
||||
{:ip, _ip} -> to_charlist(host)
|
||||
end
|
||||
end
|
||||
|
||||
@spec domain_or_ip(String.t()) :: {host_type(), Connection.host()}
|
||||
def domain_or_ip(host) do
|
||||
charlist = to_charlist(host)
|
||||
|
||||
case :inet.parse_address(charlist) do
|
||||
{:error, :einval} ->
|
||||
{:domain, :idna.encode(charlist)}
|
||||
|
||||
{:ok, ip} when is_tuple(ip) and tuple_size(ip) in [4, 8] ->
|
||||
{:ip, ip}
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,123 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.HTTP.Adapter.Gun do
|
||||
@behaviour Pleroma.HTTP.Adapter
|
||||
|
||||
alias Pleroma.HTTP.Adapter
|
||||
|
||||
require Logger
|
||||
|
||||
alias Pleroma.Pool.Connections
|
||||
|
||||
@defaults [
|
||||
connect_timeout: 20_000,
|
||||
domain_lookup_timeout: 5_000,
|
||||
tls_handshake_timeout: 5_000,
|
||||
retry_timeout: 100,
|
||||
await_up_timeout: 5_000
|
||||
]
|
||||
|
||||
@spec options(keyword(), URI.t()) :: keyword()
|
||||
def options(connection_opts \\ [], %URI{} = uri) do
|
||||
proxy = Pleroma.Config.get([:http, :proxy_url], nil)
|
||||
|
||||
@defaults
|
||||
|> Keyword.merge(Pleroma.Config.get([:http, :adapter], []))
|
||||
|> add_original(uri)
|
||||
|> add_scheme_opts(uri)
|
||||
|> Adapter.maybe_add_proxy(Adapter.format_proxy(proxy))
|
||||
|> maybe_get_conn(uri, connection_opts)
|
||||
end
|
||||
|
||||
@spec after_request(keyword()) :: :ok
|
||||
def after_request(opts) do
|
||||
with conn when not is_nil(conn) <- opts[:conn],
|
||||
body_as when body_as != :chunks <- opts[:body_as] do
|
||||
Connections.checkout(conn, self(), :gun_connections)
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp add_original(opts, %URI{host: host, port: port}) do
|
||||
formatted_host = Adapter.domain_or_fallback(host)
|
||||
|
||||
Keyword.put(opts, :original, "#{formatted_host}:#{port}")
|
||||
end
|
||||
|
||||
defp add_scheme_opts(opts, %URI{scheme: "http"}), do: opts
|
||||
|
||||
defp add_scheme_opts(opts, %URI{scheme: "https", host: host, port: port}) do
|
||||
adapter_opts = [
|
||||
certificates_verification: true,
|
||||
tls_opts: [
|
||||
verify: :verify_peer,
|
||||
cacertfile: CAStore.file_path(),
|
||||
depth: 20,
|
||||
reuse_sessions: false,
|
||||
verify_fun:
|
||||
{&:ssl_verify_hostname.verify_fun/3, [check_hostname: Adapter.domain_or_fallback(host)]}
|
||||
]
|
||||
]
|
||||
|
||||
adapter_opts =
|
||||
if port != 443 do
|
||||
Keyword.put(adapter_opts, :transport, :tls)
|
||||
else
|
||||
adapter_opts
|
||||
end
|
||||
|
||||
Keyword.merge(opts, adapter_opts)
|
||||
end
|
||||
|
||||
defp maybe_get_conn(adapter_opts, uri, connection_opts) do
|
||||
{receive_conn?, opts} =
|
||||
adapter_opts
|
||||
|> Keyword.merge(connection_opts)
|
||||
|> Keyword.pop(:receive_conn, true)
|
||||
|
||||
if Connections.alive?(:gun_connections) and receive_conn? do
|
||||
try_to_get_conn(uri, opts)
|
||||
else
|
||||
opts
|
||||
end
|
||||
end
|
||||
|
||||
defp try_to_get_conn(uri, opts) do
|
||||
try do
|
||||
case Connections.checkin(uri, :gun_connections) do
|
||||
nil ->
|
||||
Logger.info(
|
||||
"Gun connections pool checkin was not succesfull. Trying to open conn for next request."
|
||||
)
|
||||
|
||||
:ok = Connections.open_conn(uri, :gun_connections, opts)
|
||||
opts
|
||||
|
||||
conn when is_pid(conn) ->
|
||||
Logger.debug("received conn #{inspect(conn)} #{Connections.compose_uri(uri)}")
|
||||
|
||||
opts
|
||||
|> Keyword.put(:conn, conn)
|
||||
|> Keyword.put(:close_conn, false)
|
||||
end
|
||||
rescue
|
||||
error ->
|
||||
Logger.warn("Gun connections pool checkin caused error #{inspect(error)}")
|
||||
opts
|
||||
catch
|
||||
:exit, {:timeout, _} ->
|
||||
Logger.info(
|
||||
"Gun connections pool checkin with timeout error #{Connections.compose_uri(uri)}"
|
||||
)
|
||||
|
||||
opts
|
||||
|
||||
:exit, error ->
|
||||
Logger.warn("Gun pool checkin exited with error #{inspect(error)}")
|
||||
opts
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,41 @@
|
||||
defmodule Pleroma.HTTP.Adapter.Hackney do
|
||||
@behaviour Pleroma.HTTP.Adapter
|
||||
|
||||
@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], nil)
|
||||
|
||||
@defaults
|
||||
|> Keyword.merge(Pleroma.Config.get([:http, :adapter], []))
|
||||
|> Keyword.merge(connection_opts)
|
||||
|> add_scheme_opts(uri)
|
||||
|> Pleroma.HTTP.Adapter.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
|
@ -0,0 +1,23 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 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
|
@ -0,0 +1,63 @@
|
||||
defmodule Pleroma.OTPVersion do
|
||||
@type check_status() :: :undefined | {:error, String.t()} | :ok
|
||||
|
||||
require Logger
|
||||
|
||||
@spec check_version() :: check_status()
|
||||
def check_version do
|
||||
# OTP Version https://erlang.org/doc/system_principles/versions.html#otp-version
|
||||
paths = [
|
||||
Path.join(:code.root_dir(), "OTP_VERSION"),
|
||||
Path.join([:code.root_dir(), "releases", :erlang.system_info(:otp_release), "OTP_VERSION"])
|
||||
]
|
||||
|
||||
:tesla
|
||||
|> Application.get_env(:adapter)
|
||||
|> get_and_check_version(paths)
|
||||
end
|
||||
|
||||
@spec get_and_check_version(module(), [Path.t()]) :: check_status()
|
||||
def get_and_check_version(Tesla.Adapter.Gun, paths) do
|
||||
paths
|
||||
|> check_files()
|
||||
|> check_version()
|
||||
end
|
||||
|
||||
def get_and_check_version(_, _), do: :ok
|
||||
|
||||
defp check_files([]), do: nil
|
||||
|
||||
defp check_files([path | paths]) do
|
||||
if File.exists?(path) do
|
||||
File.read!(path)
|
||||
else
|
||||
check_files(paths)
|
||||
end
|
||||
end
|
||||
|
||||
defp check_version(nil), do: :undefined
|
||||
|
||||
defp check_version(version) do
|
||||
try do
|
||||
version = String.replace(version, ~r/\r|\n|\s/, "")
|
||||
|
||||
formatted =
|
||||
version
|
||||
|> String.split(".")
|
||||
|> Enum.map(&String.to_integer/1)
|
||||
|> Enum.take(2)
|
||||
|
||||
with [major, minor] when length(formatted) == 2 <- formatted,
|
||||
true <- (major == 22 and minor >= 2) or major > 22 do
|
||||
:ok
|
||||
else
|
||||
false -> {:error, version}
|
||||
_ -> :undefined
|
||||
end
|
||||
rescue
|
||||
_ -> :undefined
|
||||
catch
|
||||
_ -> :undefined
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,415 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Pool.Connections do
|
||||
use GenServer
|
||||
|
||||
require Logger
|
||||
|
||||
@type domain :: String.t()
|
||||
@type conn :: Pleroma.Gun.Conn.t()
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
conns: %{domain() => conn()},
|
||||
opts: keyword()
|
||||
}
|
||||
|
||||
defstruct conns: %{}, opts: []
|
||||
|
||||
alias Pleroma.Gun.API
|
||||
alias Pleroma.Gun.Conn
|
||||
|
||||
@spec start_link({atom(), keyword()}) :: {:ok, pid()}
|
||||
def start_link({name, opts}) do
|
||||
GenServer.start_link(__MODULE__, opts, name: name)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(opts), do: {:ok, %__MODULE__{conns: %{}, opts: opts}}
|
||||
|
||||
@spec checkin(String.t() | URI.t(), atom()) :: pid() | nil
|
||||
def checkin(url, name)
|
||||
def checkin(url, name) when is_binary(url), do: checkin(URI.parse(url), name)
|
||||
|
||||
def checkin(%URI{} = uri, name) do
|
||||
timeout = Pleroma.Config.get([:connections_pool, :receive_connection_timeout], 250)
|
||||
|
||||
GenServer.call(
|
||||
name,
|
||||
{:checkin, uri},
|
||||
timeout
|
||||
)
|
||||
end
|
||||
|
||||
@spec open_conn(String.t() | URI.t(), atom(), keyword()) :: :ok
|
||||
def open_conn(url, name, opts \\ [])
|
||||
def open_conn(url, name, opts) when is_binary(url), do: open_conn(URI.parse(url), name, opts)
|
||||
|
||||
def open_conn(%URI{} = uri, name, opts) do
|
||||
pool_opts = Pleroma.Config.get([:connections_pool], [])
|
||||
|
||||
opts =
|
||||
opts
|
||||
|> Enum.into(%{})
|
||||
|> Map.put_new(:receive, false)
|
||||
|> Map.put_new(:retry, pool_opts[:retry] || 5)
|
||||
|> Map.put_new(:retry_timeout, pool_opts[:retry_timeout] || 100)
|
||||
|> Map.put_new(:await_up_timeout, pool_opts[:await_up_timeout] || 5_000)
|
||||
|
||||
GenServer.cast(name, {:open_conn, %{opts: opts, uri: uri}})
|
||||
end
|
||||
|
||||
@spec alive?(atom()) :: boolean()
|
||||
def alive?(name) do
|
||||
pid = Process.whereis(name)
|
||||
if pid, do: Process.alive?(pid), else: false
|
||||
end
|
||||
|
||||
@spec get_state(atom()) :: t()
|
||||
def get_state(name) do
|
||||
GenServer.call(name, :state)
|
||||
end
|
||||
|
||||
@spec checkout(pid(), pid(), atom()) :: :ok
|
||||
def checkout(conn, pid, name) do
|
||||
GenServer.cast(name, {:checkout, conn, pid})
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast({:open_conn, %{opts: opts, uri: uri}}, state) do
|
||||
Logger.debug("opening new #{compose_uri(uri)}")
|
||||
max_connections = state.opts[:max_connections]
|
||||
|
||||
key = compose_key(uri)
|
||||
|
||||
if Enum.count(state.conns) < max_connections do
|
||||
open_conn(key, uri, state, opts)
|
||||
else
|
||||
try_to_open_conn(key, uri, state, opts)
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast({:checkout, conn_pid, pid}, state) do
|
||||
Logger.debug("checkout #{inspect(conn_pid)}")
|
||||
|
||||
state =
|
||||
with true <- Process.alive?(conn_pid),
|
||||
{key, conn} <- find_conn(state.conns, conn_pid),
|
||||
used_by <- List.keydelete(conn.used_by, pid, 0) do
|
||||
conn_state =
|
||||
if used_by == [] do
|
||||
:idle
|
||||
else
|
||||
conn.conn_state
|
||||
end
|
||||
|
||||
put_in(state.conns[key], %{conn | conn_state: conn_state, used_by: used_by})
|
||||
else
|
||||
false ->
|
||||
Logger.warn("checkout for closed conn #{inspect(conn_pid)}")
|
||||
state
|
||||
|
||||
nil ->
|
||||
Logger.info("checkout for alive conn #{inspect(conn_pid)}, but is not in state")
|
||||
state
|
||||
end
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:checkin, uri}, from, state) do
|
||||
Logger.debug("checkin #{compose_uri(uri)}")
|
||||
key = compose_key(uri)
|
||||
|
||||
case state.conns[key] do
|
||||
%{conn: conn, gun_state: gun_state} = current_conn when gun_state == :up ->
|
||||
Logger.debug("reusing conn #{compose_uri(uri)}")
|
||||
|
||||
with time <- :os.system_time(:second),
|
||||
last_reference <- time - current_conn.last_reference,
|
||||
current_crf <- crf(last_reference, 100, current_conn.crf),
|
||||
state <-
|
||||
put_in(state.conns[key], %{
|
||||
current_conn
|
||||
| last_reference: time,
|
||||
crf: current_crf,
|
||||
conn_state: :active,
|
||||
used_by: [from | current_conn.used_by]
|
||||
}) do
|
||||
{:reply, conn, state}
|
||||
end
|
||||
|
||||
%{gun_state: gun_state} when gun_state == :down ->
|
||||
{:reply, nil, state}
|
||||
|
||||
nil ->
|
||||
{:reply, nil, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:state, _from, state), do: {:reply, state, state}
|
||||
|
||||
@impl true
|
||||
def handle_info({:gun_up, conn_pid, _protocol}, state) do
|
||||
state =
|
||||
with true <- Process.alive?(conn_pid),
|
||||
conn_key when is_binary(conn_key) <- compose_key_gun_info(conn_pid),
|
||||
{key, conn} <- find_conn(state.conns, conn_pid, conn_key),
|
||||
time <- :os.system_time(:second),
|
||||
last_reference <- time - conn.last_reference,
|
||||
current_crf <- crf(last_reference, 100, conn.crf) do
|
||||
put_in(state.conns[key], %{
|
||||
conn
|
||||
| gun_state: :up,
|
||||
last_reference: time,
|
||||
crf: current_crf,
|
||||
conn_state: :active,
|
||||
retries: 0
|
||||
})
|
||||
else
|
||||
:error_gun_info ->
|
||||
Logger.warn(":gun.info caused error")
|
||||
state
|
||||
|
||||
false ->
|
||||
Logger.warn(":gun_up message for closed conn #{inspect(conn_pid)}")
|
||||
state
|
||||
|
||||
nil ->
|
||||
Logger.warn(
|
||||
":gun_up message for alive conn #{inspect(conn_pid)}, but deleted from state"
|
||||
)
|
||||
|
||||
:ok = API.close(conn_pid)
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:gun_down, conn_pid, _protocol, _reason, _killed}, state) do
|
||||
# we can't get info on this pid, because pid is dead
|
||||
state =
|
||||
with true <- Process.alive?(conn_pid),
|
||||
{key, conn} <- find_conn(state.conns, conn_pid) do
|
||||
if conn.retries == 5 do
|
||||
Logger.debug("closing conn if retries is eq 5 #{inspect(conn_pid)}")
|
||||
:ok = API.close(conn.conn)
|
||||
|
||||
put_in(
|
||||
state.conns,
|
||||
Map.delete(state.conns, key)
|
||||
)
|
||||
else
|
||||
put_in(state.conns[key], %{
|
||||
conn
|
||||
| gun_state: :down,
|
||||
retries: conn.retries + 1
|
||||
})
|
||||
end
|
||||
else
|
||||
false ->
|
||||
# gun can send gun_down for closed conn, maybe connection is not closed yet
|
||||
Logger.warn(":gun_down message for closed conn #{inspect(conn_pid)}")
|
||||
state
|
||||
|
||||
nil ->
|
||||
Logger.warn(
|
||||
":gun_down message for alive conn #{inspect(conn_pid)}, but deleted from state"
|
||||
)
|
||||
|
||||
:ok = API.close(conn_pid)
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
defp compose_key(%URI{scheme: scheme, host: host, port: port}), do: "#{scheme}:#{host}:#{port}"
|
||||
|
||||
defp compose_key_gun_info(pid) do
|
||||
try do
|
||||
# sometimes :gun.info can raise MatchError, which lead to pool terminate
|
||||
%{origin_host: origin_host, origin_scheme: scheme, origin_port: port} = API.info(pid)
|
||||
|
||||
host =
|
||||
case :inet.ntoa(origin_host) do
|
||||
{:error, :einval} -> origin_host
|
||||
ip -> ip
|
||||
end
|
||||
|
||||
"#{scheme}:#{host}:#{port}"
|
||||
rescue
|
||||
_ -> :error_gun_info
|
||||
end
|
||||
end
|
||||
|
||||
defp find_conn(conns, conn_pid) do
|
||||
Enum.find(conns, fn {_key, conn} ->
|
||||
conn.conn == conn_pid
|
||||
end)
|
||||
end
|
||||
|
||||
defp find_conn(conns, conn_pid, conn_key) do
|
||||
Enum.find(conns, fn {key, conn} ->
|
||||
key == conn_key and conn.conn == conn_pid
|
||||
end)
|
||||
end
|
||||
|
||||
defp open_conn(key, uri, state, %{proxy: {proxy_host, proxy_port}} = opts) do
|
||||
connect_opts =
|
||||
uri
|
||||
|> destination_opts()
|
||||
|> add_http2_opts(uri.scheme, Map.get(opts, :tls_opts, []))
|
||||
|
||||
with open_opts <- Map.delete(opts, :tls_opts),
|
||||
{:ok, conn} <- API.open(proxy_host, proxy_port, open_opts),
|
||||
{:ok, _} <- API.await_up(conn),
|
||||
stream <- API.connect(conn, connect_opts),
|
||||
{:response, :fin, 200, _} <- API.await(conn, stream),
|
||||
state <-
|
||||
put_in(state.conns[key], %Conn{
|
||||
conn: conn,
|
||||
gun_state: :up,
|
||||
conn_state: :active,
|
||||
last_reference: :os.system_time(:second)
|
||||
}) do
|
||||
{:noreply, state}
|
||||
else
|
||||
error ->
|
||||
Logger.warn(
|
||||
"Received error on opening connection with http proxy #{uri.scheme}://#{
|
||||
compose_uri(uri)
|
||||
}: #{inspect(error)}"
|
||||
)
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
end
|
||||
|
||||
defp open_conn(key, uri, state, %{proxy: {proxy_type, proxy_host, proxy_port}} = opts) do
|
||||
version =
|
||||
proxy_type
|
||||
|> to_string()
|
||||
|> String.last()
|
||||
|> case do
|
||||
"4" -> 4
|
||||
_ -> 5
|
||||
end
|
||||
|
||||
socks_opts =
|
||||
uri
|
||||
|> destination_opts()
|
||||
|> add_http2_opts(uri.scheme, Map.get(opts, :tls_opts, []))
|
||||
|> Map.put(:version, version)
|
||||
|
||||
opts =
|
||||
opts
|
||||
|> Map.put(:protocols, [:socks])
|
||||
|> Map.put(:socks_opts, socks_opts)
|
||||
|
||||
with {:ok, conn} <- API.open(proxy_host, proxy_port, opts),
|
||||
{:ok, _} <- API.await_up(conn),
|
||||
state <-
|
||||
put_in(state.conns[key], %Conn{
|
||||
conn: conn,
|
||||
gun_state: :up,
|
||||
conn_state: :active,
|
||||
last_reference: :os.system_time(:second)
|
||||
}) do
|
||||
{:noreply, state}
|
||||
else
|
||||
error ->
|
||||
Logger.warn(
|
||||
"Received error on opening connection with socks proxy #{uri.scheme}://#{
|
||||
compose_uri(uri)
|
||||
}: #{inspect(error)}"
|
||||
)
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
end
|
||||
|
||||
defp open_conn(key, %URI{host: host, port: port} = uri, state, opts) do
|
||||
Logger.debug("opening conn #{compose_uri(uri)}")
|
||||
{_type, host} = Pleroma.HTTP.Adapter.domain_or_ip(host)
|
||||
|
||||
with {:ok, conn} <- API.open(host, port, opts),
|
||||
{:ok, _} <- API.await_up(conn),
|
||||
state <-
|
||||
put_in(state.conns[key], %Conn{
|
||||
conn: conn,
|
||||
gun_state: :up,
|
||||
conn_state: :active,
|
||||
last_reference: :os.system_time(:second)
|
||||
}) do
|
||||
Logger.debug("new conn opened #{compose_uri(uri)}")
|
||||
Logger.debug("replying to the call #{compose_uri(uri)}")
|
||||
{:noreply, state}
|
||||
else
|
||||
error ->
|
||||
Logger.warn(
|
||||
"Received error on opening connection #{uri.scheme}://#{compose_uri(uri)}: #{
|
||||
inspect(error)
|
||||
}"
|
||||
)
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
end
|
||||
|
||||
defp destination_opts(%URI{host: host, port: port}) do
|
||||
{_type, host} = Pleroma.HTTP.Adapter.domain_or_ip(host)
|
||||
%{host: host, port: port}
|
||||
end
|
||||
|
||||
defp add_http2_opts(opts, "https", tls_opts) do
|
||||
Map.merge(opts, %{protocols: [:http2], transport: :tls, tls_opts: tls_opts})
|
||||
end
|
||||
|
||||
defp add_http2_opts(opts, _, _), do: opts
|
||||
|
||||
@spec get_unused_conns(map()) :: [{domain(), conn()}]
|
||||
def get_unused_conns(conns) do
|
||||
conns
|
||||
|> Enum.filter(fn {_k, v} ->
|
||||
v.conn_state == :idle and v.used_by == []
|
||||
end)
|
||||
|> Enum.sort(fn {_x_k, x}, {_y_k, y} ->
|
||||
x.crf <= y.crf and x.last_reference <= y.last_reference
|
||||
end)
|
||||
end
|
||||
|
||||
defp try_to_open_conn(key, uri, state, opts) do
|
||||
Logger.debug("try to open conn #{compose_uri(uri)}")
|
||||
|
||||
with [{close_key, least_used} | _conns] <- get_unused_conns(state.conns),
|
||||
:ok <- API.close(least_used.conn),
|
||||
state <-
|
||||
put_in(
|
||||
state.conns,
|
||||
Map.delete(state.conns, close_key)
|
||||
) do
|
||||
Logger.debug(
|
||||
"least used conn found and closed #{inspect(least_used.conn)} #{compose_uri(uri)}"
|
||||
)
|
||||
|
||||
open_conn(key, uri, state, opts)
|
||||
else
|
||||
[] -> {:noreply, state}
|
||||
end
|
||||
end
|
||||
|
||||
def crf(current, steps, crf) do
|
||||
1 + :math.pow(0.5, current / steps) * crf
|
||||
end
|
||||
|
||||
def compose_uri(%URI{} = uri), do: "#{uri.host}#{uri.path}"
|
||||
end
|
@ -0,0 +1,22 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Pool do
|
||||
def child_spec(opts) do
|
||||
poolboy_opts =
|
||||
opts
|
||||
|> Keyword.put(:worker_module, Pleroma.Pool.Request)
|
||||
|> Keyword.put(:name, {:local, opts[:name]})
|
||||
|> Keyword.put(:size, opts[:size])
|
||||
|> Keyword.put(:max_overflow, opts[:max_overflow])
|
||||
|
||||
%{
|
||||
id: opts[:id] || {__MODULE__, make_ref()},
|
||||
start: {:poolboy, :start_link, [poolboy_opts, [name: opts[:name]]]},
|
||||
restart: :permanent,
|
||||
shutdown: 5000,
|
||||
type: :worker
|
||||
}
|
||||
end
|
||||
end
|
@ -0,0 +1,72 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Pool.Request do
|
||||
use GenServer
|
||||
|
||||
require Logger
|
||||
|
||||
def start_link(args) do
|
||||
GenServer.start_link(__MODULE__, args)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(_), do: {:ok, []}
|
||||
|
||||
@spec execute(pid() | atom(), Tesla.Client.t(), keyword(), pos_integer()) ::
|
||||
{:ok, Tesla.Env.t()} | {:error, any()}
|
||||
def execute(pid, client, request, timeout) do
|
||||
GenServer.call(pid, {:execute, client, request}, timeout)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:execute, client, request}, _from, state) do
|
||||
response = Pleroma.HTTP.request_try(client, request)
|
||||
|
||||
{:reply, response, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:gun_data, _conn, stream, _, _}, state) do
|
||||
# in some cases if we reuse conn and got {:error, :body_too_large}
|
||||
# gun continues to send messages to this process,
|
||||
# so we flush messages for this request
|
||||
:ok = :gun.flush(stream)
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:gun_up, _conn, _protocol}, state) do
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:gun_down, _conn, _protocol, _reason, _killed}, state) do
|
||||
# don't flush messages here, because gun can reconnect
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:gun_error, _conn, stream, _error}, state) do
|
||||
:ok = :gun.flush(stream)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:gun_push, _conn, _stream, _new_stream, _method, _uri, _headers}, state) do
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:gun_response, _conn, _stream, _, _status, _headers}, state) do
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(msg, state) do
|
||||
Logger.warn("Received unexpected message #{inspect(__MODULE__)} #{inspect(msg)}")
|
||||
{:noreply, state}
|
||||
end
|
||||
end
|
@ -0,0 +1,36 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Pool.Supervisor do
|
||||
use Supervisor
|
||||
|
||||
alias Pleroma.Pool
|
||||
|
||||
def start_link(args) do
|
||||
Supervisor.start_link(__MODULE__, args, name: __MODULE__)
|
||||
end
|
||||
|
||||
def init(_) do
|
||||
children =
|
||||
[
|
||||
%{
|
||||
id: Pool.Connections,
|
||||
start:
|
||||
{Pool.Connections, :start_link,
|
||||
[{:gun_connections, Pleroma.Config.get([:connections_pool])}]}
|
||||
}
|
||||
] ++ pools()
|
||||
|
||||
Supervisor.init(children, strategy: :one_for_one)
|
||||
end
|
||||
|
||||
defp pools do
|
||||
for {pool_name, pool_opts} <- Pleroma.Config.get([:pools]) do
|
||||
pool_opts
|
||||
|> Keyword.put(:id, {Pool, pool_name})
|
||||
|> Keyword.put(:name, pool_name)
|
||||
|> Pool.child_spec()
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,24 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.ReverseProxy.Client.Hackney do
|
||||
@behaviour Pleroma.ReverseProxy.Client
|
||||
|
||||
@impl true
|
||||
def request(method, url, headers, body, opts \\ []) do
|
||||
:hackney.request(method, url, headers, body, opts)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def stream_body(ref) do
|
||||
case :hackney.stream_body(ref) do
|
||||
:done -> :done
|
||||
{:ok, data} -> {:ok, data, ref}
|
||||
{:error, error} -> {:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def close(ref), do: :hackney.close(ref)
|
||||
end
|
@ -0,0 +1,87 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.ReverseProxy.Client.Tesla do
|
||||
@type headers() :: [{String.t(), String.t()}]
|
||||
@type status() :: pos_integer()
|
||||
|
||||
@behaviour Pleroma.ReverseProxy.Client
|
||||
|
||||
@spec request(atom(), String.t(), headers(), String.t(), keyword()) ::
|
||||
{:ok, status(), headers}
|
||||
| {:ok, status(), headers, map()}
|
||||
| {:error, atom() | String.t()}
|
||||
| no_return()
|
||||
|
||||
@impl true
|
||||
def request(method, url, headers, body, opts \\ []) do
|
||||
_adapter = check_adapter()
|
||||
|
||||
with opts <- Keyword.merge(opts, body_as: :chunks, mode: :passive),
|
||||
{:ok, response} <-
|
||||
Pleroma.HTTP.request(
|
||||
method,
|
||||
url,
|
||||
body,
|
||||
headers,
|
||||
Keyword.put(opts, :adapter, opts)
|
||||
) do
|
||||
if is_map(response.body) and method != :head do
|
||||
{:ok, response.status, response.headers, response.body}
|
||||
else
|
||||
{:ok, response.status, response.headers}
|
||||
end
|
||||
else
|
||||
{:error, error} -> {:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
@spec stream_body(map()) :: {:ok, binary(), map()} | {:error, atom() | String.t()} | :done
|
||||
def stream_body(%{pid: pid, opts: opts, fin: true}) do
|
||||
# if connection was sended and there were redirects, we need to close new conn - pid 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)
|
||||
|
||||
:done
|
||||
end
|
||||
|
||||
def stream_body(client) do
|
||||
case read_chunk!(client) do
|
||||
{:fin, body} ->
|
||||
{:ok, body, Map.put(client, :fin, true)}
|
||||
|
||||
{:nofin, part} ->
|
||||
{:ok, part, client}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
defp read_chunk!(%{pid: pid, stream: stream, opts: opts}) do
|
||||
adapter = check_adapter()
|
||||
adapter.read_chunk(pid, stream, opts)
|
||||
end
|
||||
|
||||
@impl true
|
||||
@spec close(map) :: :ok | no_return()
|
||||
def close(%{pid: pid}) do
|
||||
adapter = check_adapter()
|
||||
adapter.close(pid)
|
||||
end
|
||||
|
||||
defp check_adapter do
|
||||
adapter = Application.get_env(:tesla, :adapter)
|
||||
|
||||
unless adapter == Tesla.Adapter.Gun do
|
||||
raise "#{adapter} doesn't support reading body in chunks"
|
||||
end
|
||||
|
||||
adapter
|
||||
end
|
||||
end
|
@ -0,0 +1 @@
|
||||
21.1
|
@ -0,0 +1 @@
|
||||
22.1
|
@ -0,0 +1 @@
|
||||
22.4
|
@ -0,0 +1 @@
|
||||
23.0
|
@ -0,0 +1 @@
|
||||
22
|
@ -0,0 +1 @@
|
||||
undefined
|
@ -0,0 +1,33 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.GunTest do
|
||||
use ExUnit.Case
|
||||
alias Pleroma.Gun
|
||||
|
||||
@moduletag :integration
|
||||
|
||||
test "opens connection and receive response" do
|
||||
{:ok, conn} = Gun.open('httpbin.org', 443)
|
||||
assert is_pid(conn)
|
||||
{:ok, _protocol} = Gun.await_up(conn)
|
||||
ref = :gun.get(conn, '/get?a=b&c=d')
|
||||
assert is_reference(ref)
|
||||
|
||||
assert {:response, :nofin, 200, _} = Gun.await(conn, ref)
|
||||
assert json = receive_response(conn, ref)
|
||||
|
||||
assert %{"args" => %{"a" => "b", "c" => "d"}} = Jason.decode!(json)
|
||||
end
|
||||
|
||||
defp receive_response(conn, ref, acc \\ "") do
|
||||
case Gun.await(conn, ref) do
|
||||
{:data, :nofin, body} ->
|
||||
receive_response(conn, ref, acc <> body)
|
||||
|
||||
{:data, :fin, body} ->
|
||||
acc <> body
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,266 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.HTTP.Adapter.GunTest do
|
||||
use ExUnit.Case, async: true
|
||||
use Pleroma.Tests.Helpers
|
||||
import ExUnit.CaptureLog
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.HTTP.Adapter.Gun
|
||||
alias Pleroma.Pool.Connections
|
||||
|
||||
setup_all do
|
||||
{:ok, _} = Registry.start_link(keys: :unique, name: Pleroma.Gun.API.Mock)
|
||||
:ok
|
||||
end
|
||||
|
||||
describe "options/1" do
|
||||
clear_config([:http, :adapter]) do
|
||||
Config.put([:http, :adapter], a: 1, b: 2)
|
||||
end
|
||||
|
||||
test "https url with default port" do
|
||||
uri = URI.parse("https://example.com")
|
||||
|
||||
opts = Gun.options(uri)
|
||||
assert opts[:certificates_verification]
|
||||
tls_opts = opts[:tls_opts]
|
||||
assert tls_opts[:verify] == :verify_peer
|
||||
assert tls_opts[:depth] == 20
|
||||
assert tls_opts[:reuse_sessions] == false
|
||||
|
||||
assert tls_opts[:verify_fun] ==
|
||||
{&:ssl_verify_hostname.verify_fun/3, [check_hostname: 'example.com']}
|
||||
|
||||
assert File.exists?(tls_opts[:cacertfile])
|
||||
|
||||
assert opts[:original] == "example.com:443"
|
||||
end
|
||||
|
||||
test "https ipv4 with default port" do
|
||||
uri = URI.parse("https://127.0.0.1")
|
||||
|
||||
opts = Gun.options(uri)
|
||||
|
||||
assert opts[:tls_opts][:verify_fun] ==
|
||||
{&:ssl_verify_hostname.verify_fun/3, [check_hostname: '127.0.0.1']}
|
||||
|
||||
assert opts[:original] == "127.0.0.1:443"
|
||||
end
|
||||
|
||||
test "https ipv6 with default port" do
|
||||
uri = URI.parse("https://[2a03:2880:f10c:83:face:b00c:0:25de]")
|
||||
|
||||
opts = Gun.options(uri)
|
||||
|
||||
assert opts[:tls_opts][:verify_fun] ==
|
||||
{&:ssl_verify_hostname.verify_fun/3,
|
||||
[check_hostname: '2a03:2880:f10c:83:face:b00c:0:25de']}
|
||||
|
||||
assert opts[:original] == "2a03:2880:f10c:83:face:b00c:0:25de:443"
|
||||
end
|
||||
|
||||
test "https url with non standart port" do
|
||||
uri = URI.parse("https://example.com:115")
|
||||
|
||||
opts = Gun.options(uri)
|
||||
|
||||
assert opts[:certificates_verification]
|
||||
assert opts[:transport] == :tls
|
||||
end
|
||||
|
||||
test "receive conn by default" do
|
||||
uri = URI.parse("http://another-domain.com")
|
||||
:ok = Connections.open_conn(uri, :gun_connections)
|
||||
|
||||
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-domain2.com")
|
||||
:ok = Connections.open_conn(uri, :gun_connections)
|
||||
|
||||
opts = [receive_conn: false]
|
||||
received_opts = Gun.options(opts, uri)
|
||||
assert received_opts[:close_conn] == nil
|
||||
assert received_opts[:conn] == nil
|
||||
end
|
||||
|
||||
test "get conn on next request" do
|
||||
level = Application.get_env(:logger, :level)
|
||||
Logger.configure(level: :info)
|
||||
on_exit(fn -> Logger.configure(level: level) end)
|
||||
uri = URI.parse("http://some-domain2.com")
|
||||
|
||||
assert capture_log(fn ->
|
||||
opts = Gun.options(uri)
|
||||
|
||||
assert opts[:conn] == nil
|
||||
assert opts[:close_conn] == nil
|
||||
end) =~
|
||||
"Gun connections pool checkin was not succesfull. Trying to open conn for next request."
|
||||
|
||||
opts = Gun.options(uri)
|
||||
|
||||
assert is_pid(opts[:conn])
|
||||
assert opts[:close_conn] == false
|
||||
end
|
||||
|
||||
test "merges with defaul http adapter config" do
|
||||
defaults = Gun.options(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")
|
||||
|
||||
:ok = Connections.open_conn(uri, :gun_connections)
|
||||
|
||||
opts = Gun.options(uri)
|
||||
|
||||
assert opts[:certificates_verification]
|
||||
tls_opts = opts[:tls_opts]
|
||||
assert tls_opts[:verify] == :verify_peer
|
||||
assert tls_opts[:depth] == 20
|
||||
assert tls_opts[:reuse_sessions] == false
|
||||
|
||||
assert opts[:original] == "some-domain.com:443"
|
||||
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 "after_request/1" do
|
||||
test "body_as not chunks" do
|
||||
uri = URI.parse("http://some-domain.com")
|
||||
:ok = Connections.open_conn(uri, :gun_connections)
|
||||
opts = Gun.options(uri)
|
||||
:ok = Gun.after_request(opts)
|
||||
conn = opts[:conn]
|
||||
|
||||
assert %Connections{
|
||||
conns: %{
|
||||
"http:some-domain.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.com")
|
||||
:ok = Connections.open_conn(uri, :gun_connections)
|
||||
opts = Gun.options([body_as: :chunks], uri)
|
||||
:ok = Gun.after_request(opts)
|
||||
conn = opts[:conn]
|
||||
self = self()
|
||||
|
||||
assert %Connections{
|
||||
conns: %{
|
||||
"http:some-domain.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")
|
||||
|
||||
:ok = Connections.open_conn(uri, :gun_connections)
|
||||
|
||||
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")
|
||||
:ok = Connections.open_conn(uri, :gun_connections)
|
||||
opts = Gun.options(uri)
|
||||
send(:gun_connections, {:gun_up, opts[:conn], :http})
|
||||
: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]")
|
||||
:ok = Connections.open_conn(uri, :gun_connections)
|
||||
opts = Gun.options(uri)
|
||||
send(:gun_connections, {:gun_up, opts[:conn], :http})
|
||||
: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
|
@ -0,0 +1,54 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.HTTP.Adapter.HackneyTest do
|
||||
use ExUnit.Case
|
||||
use Pleroma.Tests.Helpers
|
||||
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.HTTP.Adapter.Hackney
|
||||
|
||||
setup_all do
|
||||
uri = URI.parse("http://domain.com")
|
||||
{:ok, uri: uri}
|
||||
end
|
||||
|
||||
describe "options/2" do
|
||||
clear_config([:http, :adapter]) do
|
||||
Config.put([:http, :adapter], a: 1, b: 2)
|
||||
end
|
||||
|
||||
test "add proxy and opts from config", %{uri: uri} do
|
||||
proxy = Config.get([:http, :proxy_url])
|
||||
Config.put([:http, :proxy_url], "localhost:8123")
|
||||
on_exit(fn -> Config.put([:http, :proxy_url], proxy) end)
|
||||
|
||||
opts = Hackney.options(uri)
|
||||
|
||||
assert opts[:a] == 1
|
||||
assert opts[:b] == 2
|
||||
assert opts[:proxy] == "localhost:8123"
|
||||
end
|
||||
|
||||
test "respect connection opts and no proxy", %{uri: uri} do
|
||||
opts = Hackney.options([a: 2, b: 1], uri)
|
||||
|
||||
assert opts[:a] == 2
|
||||
assert opts[:b] == 1
|
||||
refute Keyword.has_key?(opts, :proxy)
|
||||
end
|
||||
|
||||
test "add opts for https" do
|
||||
uri = URI.parse("https://domain.com")
|
||||
|
||||
opts = Hackney.options(uri)
|
||||
|
||||
assert opts[:ssl_options] == [
|
||||
partial_chain: &:hackney_connect.partial_chain/1,
|
||||
versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"],
|
||||
server_name_indication: 'domain.com'
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,65 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.HTTP.AdapterTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Pleroma.HTTP.Adapter
|
||||
|
||||
describe "domain_or_ip/1" do
|
||||
test "with domain" do
|
||||
assert Adapter.domain_or_ip("example.com") == {:domain, 'example.com'}
|
||||
end
|
||||
|
||||
test "with idna domain" do
|
||||
assert Adapter.domain_or_ip("ですexample.com") == {:domain, 'xn--example-183fne.com'}
|
||||
end
|
||||
|
||||
test "with ipv4" do
|
||||
assert Adapter.domain_or_ip("127.0.0.1") == {:ip, {127, 0, 0, 1}}
|
||||
end
|
||||
|
||||
test "with ipv6" do
|
||||
assert Adapter.domain_or_ip("2a03:2880:f10c:83:face:b00c:0:25de") ==
|
||||
{:ip, {10_755, 10_368, 61_708, 131, 64_206, 45_068, 0, 9_694}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "domain_or_fallback/1" do
|
||||
test "with domain" do
|
||||
assert Adapter.domain_or_fallback("example.com") == 'example.com'
|
||||
end
|
||||
|
||||
test "with idna domain" do
|
||||
assert Adapter.domain_or_fallback("ですexample.com") == 'xn--example-183fne.com'
|
||||
end
|
||||
|
||||
test "with ipv4" do
|
||||
assert Adapter.domain_or_fallback("127.0.0.1") == '127.0.0.1'
|
||||
end
|
||||
|
||||
test "with ipv6" do
|
||||
assert Adapter.domain_or_fallback("2a03:2880:f10c:83:face:b00c:0:25de") ==
|
||||
'2a03:2880:f10c:83:face:b00c:0:25de'
|
||||
end
|
||||
end
|
||||
|
||||
describe "format_proxy/1" do
|
||||
test "with nil" do
|
||||
assert Adapter.format_proxy(nil) == nil
|
||||
end
|
||||
|
||||
test "with string" do
|
||||
assert Adapter.format_proxy("127.0.0.1:8123") == {{127, 0, 0, 1}, 8123}
|
||||
end
|
||||
|
||||
test "localhost with port" do
|
||||
assert Adapter.format_proxy("localhost:8123") == {'localhost', 8123}
|
||||
end
|
||||
|
||||
test "tuple" do
|
||||
assert Adapter.format_proxy({:socks4, :localhost, 9050}) == {:socks4, 'localhost', 9050}
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,142 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.HTTP.ConnectionTest do
|
||||
use ExUnit.Case
|
||||
use Pleroma.Tests.Helpers
|
||||
import ExUnit.CaptureLog
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.HTTP.Connection
|
||||
|
||||
setup_all do
|
||||
{:ok, _} = Registry.start_link(keys: :unique, name: Pleroma.Gun.API.Mock)
|
||||
:ok
|
||||
end
|
||||
|
||||
describe "parse_host/1" do
|
||||
test "as atom to charlist" do
|
||||
assert Connection.parse_host(:localhost) == 'localhost'
|
||||
end
|
||||
|
||||
test "as string to charlist" do
|
||||
assert Connection.parse_host("localhost.com") == 'localhost.com'
|
||||
end
|
||||
|
||||
test "as string ip to tuple" do
|
||||
assert Connection.parse_host("127.0.0.1") == {127, 0, 0, 1}
|
||||
end
|
||||
end
|
||||
|
||||
describe "parse_proxy/1" do
|
||||
test "ip with port" do
|
||||
assert Connection.parse_proxy("127.0.0.1:8123") == {:ok, {127, 0, 0, 1}, 8123}
|
||||
end
|
||||
|
||||
test "host with port" do
|
||||
assert Connection.parse_proxy("localhost:8123") == {:ok, 'localhost', 8123}
|
||||
end
|
||||
|
||||
test "as tuple" do
|
||||
assert Connection.parse_proxy({:socks4, :localhost, 9050}) ==
|
||||
{:ok, :socks4, 'localhost', 9050}
|
||||
end
|
||||
|
||||
test "as tuple with string host" do
|
||||
assert Connection.parse_proxy({:socks5, "localhost", 9050}) ==
|
||||
{:ok, :socks5, 'localhost', 9050}
|
||||
end
|
||||
end
|
||||
|
||||
describe "parse_proxy/1 errors" do
|
||||
test "ip without port" do
|
||||
capture_log(fn ->
|
||||
assert Connection.parse_proxy("127.0.0.1") == {:error, :error_parsing_proxy}
|
||||
end) =~ "parsing proxy fail \"127.0.0.1\""
|
||||
end
|
||||
|
||||
test "host without port" do
|
||||
capture_log(fn ->
|
||||
assert Connection.parse_proxy("localhost") == {:error, :error_parsing_proxy}
|
||||
end) =~ "parsing proxy fail \"localhost\""
|
||||
end
|
||||
|
||||
test "host with bad port" do
|
||||
capture_log(fn ->
|
||||
assert Connection.parse_proxy("localhost:port") == {:error, :error_parsing_port_in_proxy}
|
||||
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, :error_parsing_port_in_proxy}
|
||||
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, :error_parsing_proxy}
|
||||
end) =~ "parsing proxy fail {:socks5, :localhost}"
|
||||
end
|
||||
|
||||
test "with nil" do
|
||||
assert Connection.parse_proxy(nil) == nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "options/3" 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}
|
||||
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
|
||||
|
||||
test "default ssl adapter opts with connection" do
|
||||
adapter = Application.get_env(:tesla, :adapter)
|
||||
Application.put_env(:tesla, :adapter, Tesla.Adapter.Gun)
|
||||
on_exit(fn -> Application.put_env(:tesla, :adapter, adapter) end)
|
||||
|
||||
uri = URI.parse("https://some-domain.com")
|
||||
|
||||
pid = Process.whereis(:federation)
|
||||
:ok = Pleroma.Pool.Connections.open_conn(uri, :gun_connections, genserver_pid: pid)
|
||||
|
||||
opts = Connection.options(uri)
|
||||
|
||||
assert opts[:certificates_verification]
|
||||
tls_opts = opts[:tls_opts]
|
||||
assert tls_opts[:verify] == :verify_peer
|
||||
assert tls_opts[:depth] == 20
|
||||
assert tls_opts[:reuse_sessions] == false
|
||||
|
||||
assert opts[:original] == "some-domain.com:443"
|
||||
assert opts[:close_conn] == false
|
||||
assert is_pid(opts[:conn])
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,58 @@
|
||||
defmodule Pleroma.OTPVersionTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Pleroma.OTPVersion
|
||||
|
||||
describe "get_and_check_version/2" do
|
||||
test "22.4" do
|
||||
assert OTPVersion.get_and_check_version(Tesla.Adapter.Gun, [
|
||||
"test/fixtures/warnings/otp_version/22.4"
|
||||
]) == :ok
|
||||
end
|
||||
|
||||
test "22.1" do
|
||||
assert OTPVersion.get_and_check_version(Tesla.Adapter.Gun, [
|
||||
"test/fixtures/warnings/otp_version/22.1"
|
||||
]) == {:error, "22.1"}
|
||||
end
|
||||
|
||||
test "21.1" do
|
||||
assert OTPVersion.get_and_check_version(Tesla.Adapter.Gun, [
|
||||
"test/fixtures/warnings/otp_version/21.1"
|
||||
]) == {:error, "21.1"}
|
||||
end
|
||||
|
||||
test "23.0" do
|
||||
assert OTPVersion.get_and_check_version(Tesla.Adapter.Gun, [
|
||||
"test/fixtures/warnings/otp_version/23.0"
|
||||
]) == :ok
|
||||
end
|
||||
|
||||
test "undefined" do
|
||||
assert OTPVersion.get_and_check_version(Tesla.Adapter.Gun, [
|
||||
"test/fixtures/warnings/otp_version/undefined"
|
||||
]) == :undefined
|
||||
end
|
||||
|
||||
test "not parsable" do
|
||||
assert OTPVersion.get_and_check_version(Tesla.Adapter.Gun, [
|
||||
"test/fixtures/warnings/otp_version/error"
|
||||
]) == :undefined
|
||||
end
|
||||
|
||||
test "with non existance file" do
|
||||
assert OTPVersion.get_and_check_version(Tesla.Adapter.Gun, [
|
||||
"test/fixtures/warnings/otp_version/non-exising",
|
||||
"test/fixtures/warnings/otp_version/22.4"
|
||||
]) == :ok
|
||||
end
|
||||
|
||||
test "empty paths" do
|
||||
assert OTPVersion.get_and_check_version(Tesla.Adapter.Gun, []) == :undefined
|
||||
end
|
||||
|
||||
test "another adapter" do
|
||||
assert OTPVersion.get_and_check_version(Tesla.Adapter.Hackney, []) == :ok
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,959 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Pool.ConnectionsTest do
|
||||
use ExUnit.Case
|
||||
use Pleroma.Tests.Helpers
|
||||
import ExUnit.CaptureLog
|
||||
alias Pleroma.Gun.API
|
||||
alias Pleroma.Gun.Conn
|
||||
alias Pleroma.Pool.Connections
|
||||
|
||||
setup_all do
|
||||
{:ok, _} = Registry.start_link(keys: :unique, name: API.Mock)
|
||||
:ok
|
||||
end
|
||||
|
||||
setup do
|
||||
name = :test_connections
|
||||
adapter = Application.get_env(:tesla, :adapter)
|
||||
Application.put_env(:tesla, :adapter, Tesla.Adapter.Gun)
|
||||
on_exit(fn -> Application.put_env(:tesla, :adapter, adapter) end)
|
||||
|
||||
{:ok, _pid} =
|
||||
Connections.start_link({name, [max_connections: 2, receive_connection_timeout: 1_500]})
|
||||
|
||||
{:ok, name: name}
|
||||
end
|
||||
|
||||
describe "alive?/2" do
|
||||
test "is alive", %{name: name} do
|
||||
assert Connections.alive?(name)
|
||||
end
|
||||
|
||||
test "returns false if not started" do
|
||||
refute Connections.alive?(:some_random_name)
|
||||
end
|
||||
end
|
||||
|
||||
test "opens connection and reuse it on next request", %{name: name} do
|
||||
url = "http://some-domain.com"
|
||||
key = "http:some-domain.com:80"
|
||||
refute Connections.checkin(url, name)
|
||||
:ok = Connections.open_conn(url, name)
|
||||
|
||||
conn = Connections.checkin(url, name)
|
||||
assert is_pid(conn)
|
||||
assert Process.alive?(conn)
|
||||
|
||||
self = self()
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
^key => %Conn{
|
||||
conn: ^conn,
|
||||
gun_state: :up,
|
||||
used_by: [{^self, _}],
|
||||
conn_state: :active
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
|
||||
reused_conn = Connections.checkin(url, name)
|
||||
|
||||
assert conn == reused_conn
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
^key => %Conn{
|
||||
conn: ^conn,
|
||||
gun_state: :up,
|
||||
used_by: [{^self, _}, {^self, _}],
|
||||
conn_state: :active
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
|
||||
:ok = Connections.checkout(conn, self, name)
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
^key => %Conn{
|
||||
conn: ^conn,
|
||||
gun_state: :up,
|
||||
used_by: [{^self, _}],
|
||||
conn_state: :active
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
|
||||
:ok = Connections.checkout(conn, self, name)
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
^key => %Conn{
|
||||
conn: ^conn,
|
||||
gun_state: :up,
|
||||
used_by: [],
|
||||
conn_state: :idle
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
end
|
||||
|
||||
test "reuse connection for idna domains", %{name: name} do
|
||||
url = "http://ですsome-domain.com"
|
||||
refute Connections.checkin(url, name)
|
||||
|
||||
:ok = Connections.open_conn(url, name)
|
||||
|
||||
conn = Connections.checkin(url, name)
|
||||
assert is_pid(conn)
|
||||
assert Process.alive?(conn)
|
||||
|
||||
self = self()
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
"http:ですsome-domain.com:80" => %Conn{
|
||||
conn: ^conn,
|
||||
gun_state: :up,
|
||||
used_by: [{^self, _}],
|
||||
conn_state: :active
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
|
||||
reused_conn = Connections.checkin(url, name)
|
||||
|
||||
assert conn == reused_conn
|
||||
end
|
||||
|
||||
test "reuse for ipv4", %{name: name} do
|
||||
url = "http://127.0.0.1"
|
||||
|
||||
refute Connections.checkin(url, name)
|
||||
|
||||
:ok = Connections.open_conn(url, name)
|
||||
|
||||
conn = Connections.checkin(url, name)
|
||||
assert is_pid(conn)
|
||||
assert Process.alive?(conn)
|
||||
|
||||
self = self()
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
"http:127.0.0.1:80" => %Conn{
|
||||
conn: ^conn,
|
||||
gun_state: :up,
|
||||
used_by: [{^self, _}],
|
||||
conn_state: :active
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
|
||||
reused_conn = Connections.checkin(url, name)
|
||||
|
||||
assert conn == reused_conn
|
||||
|
||||
:ok = Connections.checkout(conn, self, name)
|
||||
:ok = Connections.checkout(reused_conn, self, name)
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
"http:127.0.0.1:80" => %Conn{
|
||||
conn: ^conn,
|
||||
gun_state: :up,
|
||||
used_by: [],
|
||||
conn_state: :idle
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
end
|
||||
|
||||
test "reuse for ipv6", %{name: name} do
|
||||
url = "http://[2a03:2880:f10c:83:face:b00c:0:25de]"
|
||||
|
||||
refute Connections.checkin(url, name)
|
||||
|
||||
:ok = Connections.open_conn(url, name)
|
||||
|
||||
conn = Connections.checkin(url, name)
|
||||
assert is_pid(conn)
|
||||
assert Process.alive?(conn)
|
||||
|
||||
self = self()
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
"http:2a03:2880:f10c:83:face:b00c:0:25de:80" => %Conn{
|
||||
conn: ^conn,
|
||||
gun_state: :up,
|
||||
used_by: [{^self, _}],
|
||||
conn_state: :active
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
|
||||
reused_conn = Connections.checkin(url, name)
|
||||
|
||||
assert conn == reused_conn
|
||||
end
|
||||
|
||||
test "up and down ipv4", %{name: name} do
|
||||
self = self()
|
||||
url = "http://127.0.0.1"
|
||||
:ok = Connections.open_conn(url, name)
|
||||
conn = Connections.checkin(url, name)
|
||||
send(name, {:gun_down, conn, nil, nil, nil})
|
||||
send(name, {:gun_up, conn, nil})
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
"http:127.0.0.1:80" => %Conn{
|
||||
conn: ^conn,
|
||||
gun_state: :up,
|
||||
used_by: [{^self, _}],
|
||||
conn_state: :active
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
end
|
||||
|
||||
test "up and down ipv6", %{name: name} do
|
||||
self = self()
|
||||
url = "http://[2a03:2880:f10c:83:face:b00c:0:25de]"
|
||||
:ok = Connections.open_conn(url, name)
|
||||
conn = Connections.checkin(url, name)
|
||||
send(name, {:gun_down, conn, nil, nil, nil})
|
||||
send(name, {:gun_up, conn, nil})
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
"http:2a03:2880:f10c:83:face:b00c:0:25de:80" => %Conn{
|
||||
conn: ^conn,
|
||||
gun_state: :up,
|
||||
used_by: [{^self, _}],
|
||||
conn_state: :active
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
end
|
||||
|
||||
test "reuses connection based on protocol", %{name: name} do
|
||||
http_url = "http://some-domain.com"
|
||||
http_key = "http:some-domain.com:80"
|
||||
https_url = "https://some-domain.com"
|
||||
https_key = "https:some-domain.com:443"
|
||||
|
||||
refute Connections.checkin(http_url, name)
|
||||
:ok = Connections.open_conn(http_url, name)
|
||||
conn = Connections.checkin(http_url, name)
|
||||
assert is_pid(conn)
|
||||
assert Process.alive?(conn)
|
||||
|
||||
refute Connections.checkin(https_url, name)
|
||||
:ok = Connections.open_conn(https_url, name)
|
||||
https_conn = Connections.checkin(https_url, name)
|
||||
|
||||
refute conn == https_conn
|
||||
|
||||
reused_https = Connections.checkin(https_url, name)
|
||||
|
||||
refute conn == reused_https
|
||||
|
||||
assert reused_https == https_conn
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
^http_key => %Conn{
|
||||
conn: ^conn,
|
||||
gun_state: :up
|
||||
},
|
||||
^https_key => %Conn{
|
||||
conn: ^https_conn,
|
||||
gun_state: :up
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
end
|
||||
|
||||
test "connection can't get up", %{name: name} do
|
||||
url = "http://gun-not-up.com"
|
||||
|
||||
assert capture_log(fn ->
|
||||
:ok = Connections.open_conn(url, name)
|
||||
refute Connections.checkin(url, name)
|
||||
end) =~
|
||||
"Received error on opening connection http://gun-not-up.com: {:error, :timeout}"
|
||||
end
|
||||
|
||||
test "process gun_down message and then gun_up", %{name: name} do
|
||||
self = self()
|
||||
url = "http://gun-down-and-up.com"
|
||||
key = "http:gun-down-and-up.com:80"
|
||||
:ok = Connections.open_conn(url, name)
|
||||
conn = Connections.checkin(url, name)
|
||||
|
||||
assert is_pid(conn)
|
||||
assert Process.alive?(conn)
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
^key => %Conn{
|
||||
conn: ^conn,
|
||||
gun_state: :up,
|
||||
used_by: [{^self, _}]
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
|
||||
send(name, {:gun_down, conn, :http, nil, nil})
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
^key => %Conn{
|
||||
conn: ^conn,
|
||||
gun_state: :down,
|
||||
used_by: [{^self, _}]
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
|
||||
send(name, {:gun_up, conn, :http})
|
||||
|
||||
conn2 = Connections.checkin(url, name)
|
||||
assert conn == conn2
|
||||
|
||||
assert is_pid(conn2)
|
||||
assert Process.alive?(conn2)
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
^key => %Conn{
|
||||
conn: _,
|
||||
gun_state: :up,
|
||||
used_by: [{^self, _}, {^self, _}]
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
end
|
||||
|
||||
test "async processes get same conn for same domain", %{name: name} do
|
||||
url = "http://some-domain.com"
|
||||
:ok = Connections.open_conn(url, name)
|
||||
|
||||
tasks =
|
||||
for _ <- 1..5 do
|
||||
Task.async(fn ->
|
||||
Connections.checkin(url, name)
|
||||
end)
|
||||
end
|
||||
|
||||
tasks_with_results = Task.yield_many(tasks)
|
||||
|
||||
results =
|
||||
Enum.map(tasks_with_results, fn {task, res} ->
|
||||
res || Task.shutdown(task, :brutal_kill)
|
||||
end)
|
||||
|
||||
conns = for {:ok, value} <- results, do: value
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
"http:some-domain.com:80" => %Conn{
|
||||
conn: conn,
|
||||
gun_state: :up
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
|
||||
assert Enum.all?(conns, fn res -> res == conn end)
|
||||
end
|
||||
|
||||
test "remove frequently used and idle", %{name: name} do
|
||||
self = self()
|
||||
http_url = "http://some-domain.com"
|
||||
https_url = "https://some-domain.com"
|
||||
:ok = Connections.open_conn(https_url, name)
|
||||
:ok = Connections.open_conn(http_url, name)
|
||||
|
||||
conn1 = Connections.checkin(https_url, name)
|
||||
|
||||
[conn2 | _conns] =
|
||||
for _ <- 1..4 do
|
||||
Connections.checkin(http_url, name)
|
||||
end
|
||||
|
||||
http_key = "http:some-domain.com:80"
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
^http_key => %Conn{
|
||||
conn: ^conn2,
|
||||
gun_state: :up,
|
||||
conn_state: :active,
|
||||
used_by: [{^self, _}, {^self, _}, {^self, _}, {^self, _}]
|
||||
},
|
||||
"https:some-domain.com:443" => %Conn{
|
||||
conn: ^conn1,
|
||||
gun_state: :up,
|
||||
conn_state: :active,
|
||||
used_by: [{^self, _}]
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
|
||||
:ok = Connections.checkout(conn1, self, name)
|
||||
|
||||
another_url = "http://another-domain.com"
|
||||
:ok = Connections.open_conn(another_url, name)
|
||||
conn = Connections.checkin(another_url, name)
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
"http:another-domain.com:80" => %Conn{
|
||||
conn: ^conn,
|
||||
gun_state: :up
|
||||
},
|
||||
^http_key => %Conn{
|
||||
conn: _,
|
||||
gun_state: :up
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
end
|
||||
|
||||
describe "integration test" do
|
||||
@describetag :integration
|
||||
|
||||
clear_config([API]) do
|
||||
Pleroma.Config.put([API], Pleroma.Gun)
|
||||
end
|
||||
|
||||
test "opens connection and reuse it on next request", %{name: name} do
|
||||
url = "http://httpbin.org"
|
||||
:ok = Connections.open_conn(url, name)
|
||||
Process.sleep(250)
|
||||
conn = Connections.checkin(url, name)
|
||||
|
||||
assert is_pid(conn)
|
||||
assert Process.alive?(conn)
|
||||
|
||||
reused_conn = Connections.checkin(url, name)
|
||||
|
||||
assert conn == reused_conn
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
"http:httpbin.org:80" => %Conn{
|
||||
conn: ^conn,
|
||||
gun_state: :up
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
end
|
||||
|
||||
test "opens ssl connection and reuse it on next request", %{name: name} do
|
||||
url = "https://httpbin.org"
|
||||
:ok = Connections.open_conn(url, name)
|
||||
Process.sleep(1_000)
|
||||
conn = Connections.checkin(url, name)
|
||||
|
||||
assert is_pid(conn)
|
||||
assert Process.alive?(conn)
|
||||
|
||||
reused_conn = Connections.checkin(url, name)
|
||||
|
||||
assert conn == reused_conn
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
"https:httpbin.org:443" => %Conn{
|
||||
conn: ^conn,
|
||||
gun_state: :up
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
end
|
||||
|
||||
test "remove frequently used and idle", %{name: name} do
|
||||
self = self()
|
||||
https1 = "https://www.google.com"
|
||||
https2 = "https://httpbin.org"
|
||||
|
||||
:ok = Connections.open_conn(https1, name)
|
||||
:ok = Connections.open_conn(https2, name)
|
||||
Process.sleep(1_500)
|
||||
conn = Connections.checkin(https1, name)
|
||||
|
||||
for _ <- 1..4 do
|
||||
Connections.checkin(https2, name)
|
||||
end
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
"https:httpbin.org:443" => %Conn{
|
||||
conn: _,
|
||||
gun_state: :up
|
||||
},
|
||||
"https:www.google.com:443" => %Conn{
|
||||
conn: _,
|
||||
gun_state: :up
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
|
||||
:ok = Connections.checkout(conn, self, name)
|
||||
http = "http://httpbin.org"
|
||||
Process.sleep(1_000)
|
||||
:ok = Connections.open_conn(http, name)
|
||||
conn = Connections.checkin(http, name)
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
"http:httpbin.org:80" => %Conn{
|
||||
conn: ^conn,
|
||||
gun_state: :up
|
||||
},
|
||||
"https:httpbin.org:443" => %Conn{
|
||||
conn: _,
|
||||
gun_state: :up
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
end
|
||||
|
||||
test "remove earlier used and idle", %{name: name} do
|
||||
self = self()
|
||||
|
||||
https1 = "https://www.google.com"
|
||||
https2 = "https://httpbin.org"
|
||||
:ok = Connections.open_conn(https1, name)
|
||||
:ok = Connections.open_conn(https2, name)
|
||||
Process.sleep(1_500)
|
||||
|
||||
Connections.checkin(https1, name)
|
||||
conn = Connections.checkin(https1, name)
|
||||
|
||||
Process.sleep(1_000)
|
||||
Connections.checkin(https2, name)
|
||||
Connections.checkin(https2, name)
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
"https:httpbin.org:443" => %Conn{
|
||||
conn: _,
|
||||
gun_state: :up
|
||||
},
|
||||
"https:www.google.com:443" => %Conn{
|
||||
conn: ^conn,
|
||||
gun_state: :up
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
|
||||
:ok = Connections.checkout(conn, self, name)
|
||||
:ok = Connections.checkout(conn, self, name)
|
||||
|
||||
http = "http://httpbin.org"
|
||||
:ok = Connections.open_conn(http, name)
|
||||
Process.sleep(1_000)
|
||||
|
||||
conn = Connections.checkin(http, name)
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
"http:httpbin.org:80" => %Conn{
|
||||
conn: ^conn,
|
||||
gun_state: :up
|
||||
},
|
||||
"https:httpbin.org:443" => %Conn{
|
||||
conn: _,
|
||||
gun_state: :up
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
end
|
||||
|
||||
test "doesn't open new conn on pool overflow", %{name: name} do
|
||||
self = self()
|
||||
|
||||
https1 = "https://www.google.com"
|
||||
https2 = "https://httpbin.org"
|
||||
:ok = Connections.open_conn(https1, name)
|
||||
:ok = Connections.open_conn(https2, name)
|
||||
Process.sleep(1_000)
|
||||
Connections.checkin(https1, name)
|
||||
conn1 = Connections.checkin(https1, name)
|
||||
conn2 = Connections.checkin(https2, name)
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
"https:httpbin.org:443" => %Conn{
|
||||
conn: ^conn2,
|
||||
gun_state: :up,
|
||||
conn_state: :active,
|
||||
used_by: [{^self, _}]
|
||||
},
|
||||
"https:www.google.com:443" => %Conn{
|
||||
conn: ^conn1,
|
||||
gun_state: :up,
|
||||
conn_state: :active,
|
||||
used_by: [{^self, _}, {^self, _}]
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
|
||||
refute Connections.checkin("http://httpbin.org", name)
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
"https:httpbin.org:443" => %Conn{
|
||||
conn: ^conn2,
|
||||
gun_state: :up,
|
||||
conn_state: :active,
|
||||
used_by: [{^self, _}]
|
||||
},
|
||||
"https:www.google.com:443" => %Conn{
|
||||
conn: ^conn1,
|
||||
gun_state: :up,
|
||||
conn_state: :active,
|
||||
used_by: [{^self, _}, {^self, _}]
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
end
|
||||
|
||||
test "get idle connection with the smallest crf", %{
|
||||
name: name
|
||||
} do
|
||||
self = self()
|
||||
|
||||
https1 = "https://www.google.com"
|
||||
https2 = "https://httpbin.org"
|
||||
|
||||
:ok = Connections.open_conn(https1, name)
|
||||
:ok = Connections.open_conn(https2, name)
|
||||
Process.sleep(1_500)
|
||||
Connections.checkin(https1, name)
|
||||
Connections.checkin(https2, name)
|
||||
Connections.checkin(https1, name)
|
||||
conn1 = Connections.checkin(https1, name)
|
||||
conn2 = Connections.checkin(https2, name)
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
"https:httpbin.org:443" => %Conn{
|
||||
conn: ^conn2,
|
||||
gun_state: :up,
|
||||
conn_state: :active,
|
||||
used_by: [{^self, _}, {^self, _}],
|
||||
crf: crf2
|
||||
},
|
||||
"https:www.google.com:443" => %Conn{
|
||||
conn: ^conn1,
|
||||
gun_state: :up,
|
||||
conn_state: :active,
|
||||
used_by: [{^self, _}, {^self, _}, {^self, _}],
|
||||
crf: crf1
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
|
||||
assert crf1 > crf2
|
||||
|
||||
:ok = Connections.checkout(conn1, self, name)
|
||||
:ok = Connections.checkout(conn1, self, name)
|
||||
:ok = Connections.checkout(conn1, self, name)
|
||||
|
||||
:ok = Connections.checkout(conn2, self, name)
|
||||
:ok = Connections.checkout(conn2, self, name)
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
"https:httpbin.org:443" => %Conn{
|
||||
conn: ^conn2,
|
||||
gun_state: :up,
|
||||
conn_state: :idle,
|
||||
used_by: []
|
||||
},
|
||||
"https:www.google.com:443" => %Conn{
|
||||
conn: ^conn1,
|
||||
gun_state: :up,
|
||||
conn_state: :idle,
|
||||
used_by: []
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
|
||||
http = "http://httpbin.org"
|
||||
:ok = Connections.open_conn(http, name)
|
||||
Process.sleep(1_000)
|
||||
conn = Connections.checkin(http, name)
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
"https:www.google.com:443" => %Conn{
|
||||
conn: ^conn1,
|
||||
gun_state: :up,
|
||||
conn_state: :idle,
|
||||
used_by: [],
|
||||
crf: crf1
|
||||
},
|
||||
"http:httpbin.org:80" => %Conn{
|
||||
conn: ^conn,
|
||||
gun_state: :up,
|
||||
conn_state: :active,
|
||||
used_by: [{^self, _}],
|
||||
crf: crf
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
|
||||
assert crf1 > crf
|
||||
end
|
||||
end
|
||||
|
||||
describe "with proxy" do
|
||||
test "as ip", %{name: name} do
|
||||
url = "http://proxy-string.com"
|
||||
key = "http:proxy-string.com:80"
|
||||
:ok = Connections.open_conn(url, name, proxy: {{127, 0, 0, 1}, 8123})
|
||||
|
||||
conn = Connections.checkin(url, name)
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
^key => %Conn{
|
||||
conn: ^conn,
|
||||
gun_state: :up
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
|
||||
reused_conn = Connections.checkin(url, name)
|
||||
|
||||
assert reused_conn == conn
|
||||
end
|
||||
|
||||
test "as host", %{name: name} do
|
||||
url = "http://proxy-tuple-atom.com"
|
||||
:ok = Connections.open_conn(url, name, proxy: {'localhost', 9050})
|
||||
conn = Connections.checkin(url, name)
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
"http:proxy-tuple-atom.com:80" => %Conn{
|
||||
conn: ^conn,
|
||||
gun_state: :up
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
|
||||
reused_conn = Connections.checkin(url, name)
|
||||
|
||||
assert reused_conn == conn
|
||||
end
|
||||
|
||||
test "as ip and ssl", %{name: name} do
|
||||
url = "https://proxy-string.com"
|
||||
|
||||
:ok = Connections.open_conn(url, name, proxy: {{127, 0, 0, 1}, 8123})
|
||||
conn = Connections.checkin(url, name)
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
"https:proxy-string.com:443" => %Conn{
|
||||
conn: ^conn,
|
||||
gun_state: :up
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
|
||||
reused_conn = Connections.checkin(url, name)
|
||||
|
||||
assert reused_conn == conn
|
||||
end
|
||||
|
||||
test "as host and ssl", %{name: name} do
|
||||
url = "https://proxy-tuple-atom.com"
|
||||
:ok = Connections.open_conn(url, name, proxy: {'localhost', 9050})
|
||||
conn = Connections.checkin(url, name)
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
"https:proxy-tuple-atom.com:443" => %Conn{
|
||||
conn: ^conn,
|
||||
gun_state: :up
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
|
||||
reused_conn = Connections.checkin(url, name)
|
||||
|
||||
assert reused_conn == conn
|
||||
end
|
||||
|
||||
test "with socks type", %{name: name} do
|
||||
url = "http://proxy-socks.com"
|
||||
|
||||
:ok = Connections.open_conn(url, name, proxy: {:socks5, 'localhost', 1234})
|
||||
|
||||
conn = Connections.checkin(url, name)
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
"http:proxy-socks.com:80" => %Conn{
|
||||
conn: ^conn,
|
||||
gun_state: :up
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
|
||||
reused_conn = Connections.checkin(url, name)
|
||||
|
||||
assert reused_conn == conn
|
||||
end
|
||||
|
||||
test "with socks4 type and ssl", %{name: name} do
|
||||
url = "https://proxy-socks.com"
|
||||
|
||||
:ok = Connections.open_conn(url, name, proxy: {:socks4, 'localhost', 1234})
|
||||
|
||||
conn = Connections.checkin(url, name)
|
||||
|
||||
%Connections{
|
||||
conns: %{
|
||||
"https:proxy-socks.com:443" => %Conn{
|
||||
conn: ^conn,
|
||||
gun_state: :up
|
||||
}
|
||||
}
|
||||
} = Connections.get_state(name)
|
||||
|
||||
reused_conn = Connections.checkin(url, name)
|
||||
|
||||
assert reused_conn == conn
|
||||
end
|
||||
end
|
||||
|
||||
describe "crf/3" do
|
||||
setup do
|
||||
crf = Connections.crf(1, 10, 1)
|
||||
{:ok, crf: crf}
|
||||
end
|
||||
|
||||
test "more used will have crf higher", %{crf: crf} do
|
||||
# used 3 times
|
||||
crf1 = Connections.crf(1, 10, crf)
|
||||
crf1 = Connections.crf(1, 10, crf1)
|
||||
|
||||
# used 2 times
|
||||
crf2 = Connections.crf(1, 10, crf)
|
||||
|
||||
assert crf1 > crf2
|
||||
end
|
||||
|
||||
test "recently used will have crf higher on equal references", %{crf: crf} do
|
||||
# used 3 sec ago
|
||||
crf1 = Connections.crf(3, 10, crf)
|
||||
|
||||
# used 4 sec ago
|
||||
crf2 = Connections.crf(4, 10, crf)
|
||||
|
||||
assert crf1 > crf2
|
||||
end
|
||||
|
||||
test "equal crf on equal reference and time", %{crf: crf} do
|
||||
# used 2 times
|
||||
crf1 = Connections.crf(1, 10, crf)
|
||||
|
||||
# used 2 times
|
||||
crf2 = Connections.crf(1, 10, crf)
|
||||
|
||||
assert crf1 == crf2
|
||||
end
|
||||
|
||||
test "recently used will have higher crf", %{crf: crf} do
|
||||
crf1 = Connections.crf(2, 10, crf)
|
||||
crf1 = Connections.crf(1, 10, crf1)
|
||||
|
||||
crf2 = Connections.crf(3, 10, crf)
|
||||
crf2 = Connections.crf(4, 10, crf2)
|
||||
assert crf1 > crf2
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_unused_conns/1" do
|
||||
test "crf is equalent, sorting by reference" do
|
||||
conns = %{
|
||||
"1" => %Conn{
|
||||
conn_state: :idle,
|
||||
last_reference: now() - 1
|
||||
},
|
||||
"2" => %Conn{
|
||||
conn_state: :idle,
|
||||
last_reference: now()
|
||||
}
|
||||
}
|
||||
|
||||
assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(conns)
|
||||
end
|
||||
|
||||
test "reference is equalent, sorting by crf" do
|
||||
conns = %{
|
||||
"1" => %Conn{
|
||||
conn_state: :idle,
|
||||
crf: 1.999
|
||||
},
|
||||
"2" => %Conn{
|
||||
conn_state: :idle,
|
||||
crf: 2
|
||||
}
|
||||
}
|
||||
|
||||
assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(conns)
|
||||
end
|
||||
|
||||
test "higher crf and lower reference" do
|
||||
conns = %{
|
||||
"1" => %Conn{
|
||||
conn_state: :idle,
|
||||
crf: 3,
|
||||
last_reference: now() - 1
|
||||
},
|
||||
"2" => %Conn{
|
||||
conn_state: :idle,
|
||||
crf: 2,
|
||||
last_reference: now()
|
||||
}
|
||||
}
|
||||
|
||||
assert [{"2", _unused_conn} | _others] = Connections.get_unused_conns(conns)
|
||||
end
|
||||
|
||||
test "lower crf and lower reference" do
|
||||
conns = %{
|
||||
"1" => %Conn{
|
||||
conn_state: :idle,
|
||||
crf: 1.99,
|
||||
last_reference: now() - 1
|
||||
},
|
||||
"2" => %Conn{
|
||||
conn_state: :idle,
|
||||
crf: 2,
|
||||
last_reference: now()
|
||||
}
|
||||
}
|
||||
|
||||
assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(conns)
|
||||
end
|
||||
end
|
||||
|
||||
defp now do
|
||||
:os.system_time(:second)
|
||||
end
|
||||
end
|
@ -0,0 +1,93 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.ReverseProxy.Client.TeslaTest do
|
||||
use ExUnit.Case
|
||||
use Pleroma.Tests.Helpers
|
||||
alias Pleroma.ReverseProxy.Client
|
||||
@moduletag :integration
|
||||
|
||||
clear_config_all([Pleroma.Gun.API]) do
|
||||
Pleroma.Config.put([Pleroma.Gun.API], Pleroma.Gun)
|
||||
end
|
||||
|
||||
setup do
|
||||
Application.put_env(:tesla, :adapter, Tesla.Adapter.Gun)
|
||||
|
||||
on_exit(fn ->
|
||||
Application.put_env(:tesla, :adapter, Tesla.Mock)
|
||||
end)
|
||||
end
|
||||
|
||||
test "get response body stream" do
|
||||
{:ok, status, headers, ref} =
|
||||
Client.Tesla.request(
|
||||
:get,
|
||||
"http://httpbin.org/stream-bytes/10",
|
||||
[{"accept", "application/octet-stream"}],
|
||||
"",
|
||||
[]
|
||||
)
|
||||
|
||||
assert status == 200
|
||||
assert headers != []
|
||||
|
||||
{:ok, response, ref} = Client.Tesla.stream_body(ref)
|
||||
check_ref(ref)
|
||||
assert is_binary(response)
|
||||
assert byte_size(response) == 10
|
||||
|
||||
assert :done == Client.Tesla.stream_body(ref)
|
||||
assert :ok = Client.Tesla.close(ref)
|
||||
end
|
||||
|
||||
test "head response" do
|
||||
{:ok, status, headers} = Client.Tesla.request(:head, "https://httpbin.org/get", [], "")
|
||||
|
||||
assert status == 200
|
||||
assert headers != []
|
||||
end
|
||||
|
||||
test "get error response" do
|
||||
{:ok, status, headers, _body} =
|
||||
Client.Tesla.request(
|
||||
:get,
|
||||
"https://httpbin.org/status/500",
|
||||
[],
|
||||
""
|
||||
)
|
||||
|
||||
assert status == 500
|
||||
assert headers != []
|
||||
end
|
||||
|
||||
describe "client error" do
|
||||
setup do
|
||||
adapter = Application.get_env(:tesla, :adapter)
|
||||
Application.put_env(:tesla, :adapter, Tesla.Adapter.Hackney)
|
||||
|
||||
on_exit(fn -> Application.put_env(:tesla, :adapter, adapter) end)
|
||||
:ok
|
||||
end
|
||||
|
||||
test "adapter doesn't support reading body in chunks" do
|
||||
assert_raise RuntimeError,
|
||||
"Elixir.Tesla.Adapter.Hackney doesn't support reading body in chunks",
|
||||
fn ->
|
||||
Client.Tesla.request(
|
||||
:get,
|
||||
"http://httpbin.org/stream-bytes/10",
|
||||
[{"accept", "application/octet-stream"}],
|
||||
""
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp check_ref(%{pid: pid, stream: stream} = ref) do
|
||||
assert is_pid(pid)
|
||||
assert is_reference(stream)
|
||||
assert ref[:fin]
|
||||
end
|
||||
end
|
Loading…
Reference in new issue