adding gun adapter
authorAlexander Strizhakov <alex.strizhakov@gmail.com>
Tue, 11 Feb 2020 07:12:57 +0000 (10:12 +0300)
committerAlexander Strizhakov <alex.strizhakov@gmail.com>
Tue, 18 Feb 2020 05:19:01 +0000 (08:19 +0300)
63 files changed:
CHANGELOG.md
config/config.exs
config/description.exs
config/test.exs
docs/API/admin_api.md
docs/configuration/cheatsheet.md
lib/mix/tasks/pleroma/benchmark.ex
lib/mix/tasks/pleroma/emoji.ex
lib/pleroma/application.ex
lib/pleroma/config/config_db.ex
lib/pleroma/config/transfer_task.ex
lib/pleroma/gun/api.ex [new file with mode: 0644]
lib/pleroma/gun/api/mock.ex [new file with mode: 0644]
lib/pleroma/gun/conn.ex [new file with mode: 0644]
lib/pleroma/gun/gun.ex [new file with mode: 0644]
lib/pleroma/http/adapter.ex [new file with mode: 0644]
lib/pleroma/http/adapter/gun.ex [new file with mode: 0644]
lib/pleroma/http/adapter/hackney.ex [new file with mode: 0644]
lib/pleroma/http/connection.ex
lib/pleroma/http/http.ex
lib/pleroma/http/request.ex [new file with mode: 0644]
lib/pleroma/http/request_builder.ex
lib/pleroma/object/fetcher.ex
lib/pleroma/otp_version.ex [new file with mode: 0644]
lib/pleroma/pool/connections.ex [new file with mode: 0644]
lib/pleroma/pool/pool.ex [new file with mode: 0644]
lib/pleroma/pool/request.ex [new file with mode: 0644]
lib/pleroma/pool/supervisor.ex [new file with mode: 0644]
lib/pleroma/reverse_proxy/client.ex
lib/pleroma/reverse_proxy/client/hackney.ex [new file with mode: 0644]
lib/pleroma/reverse_proxy/client/tesla.ex [new file with mode: 0644]
lib/pleroma/reverse_proxy/reverse_proxy.ex
lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex
lib/pleroma/web/rel_me.ex
lib/pleroma/web/rich_media/parser.ex
lib/pleroma/web/web_finger/web_finger.ex
mix.exs
mix.lock
test/activity/ir/topics_test.exs
test/config/config_db_test.exs
test/fixtures/warnings/otp_version/21.1 [new file with mode: 0644]
test/fixtures/warnings/otp_version/22.1 [new file with mode: 0644]
test/fixtures/warnings/otp_version/22.4 [new file with mode: 0644]
test/fixtures/warnings/otp_version/23.0 [new file with mode: 0644]
test/fixtures/warnings/otp_version/error [new file with mode: 0644]
test/fixtures/warnings/otp_version/undefined [new file with mode: 0644]
test/gun/gun_test.exs [new file with mode: 0644]
test/http/adapter/gun_test.exs [new file with mode: 0644]
test/http/adapter/hackney_test.exs [new file with mode: 0644]
test/http/adapter_test.exs [new file with mode: 0644]
test/http/connection_test.exs [new file with mode: 0644]
test/http/request_builder_test.exs
test/http_test.exs
test/notification_test.exs
test/otp_version_test.exs [new file with mode: 0644]
test/pool/connections_test.exs [new file with mode: 0644]
test/reverse_proxy/client/tesla_test.exs [new file with mode: 0644]
test/reverse_proxy/reverse_proxy_test.exs [moved from test/reverse_proxy_test.exs with 79% similarity]
test/support/http_request_mock.ex
test/user_invite_token_test.exs
test/web/admin_api/admin_api_controller_test.exs
test/web/common_api/common_api_utils_test.exs
test/web/push/impl_test.exs

index 3e838983b4b4393e26f0ae8ae27a90e685c7acbb..48080503a2fada65d32d1cc0718f438043bf06a9 100644 (file)
@@ -73,6 +73,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Support for custom Elixir modules (such as MRF policies)
 - User settings: Add _This account is a_ option.
 - OAuth: admin scopes support (relevant setting: `[:auth, :enforce_oauth_admin_scope_usage]`).
+- New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires OTP version older that 22.2, otherwise pleroma won’t start. For hackney OTP update is not required.
 <details>
   <summary>API Changes</summary>
 
index ccc0c4e525b99b79a08a2c64e8fd6761ac8ff59f..27091393b642696a73df4ad177db18221f9e636e 100644 (file)
@@ -58,20 +58,6 @@ config :pleroma, Pleroma.Captcha,
 
 config :pleroma, Pleroma.Captcha.Kocaptcha, endpoint: "https://captcha.kotobank.ch"
 
-config :pleroma, :hackney_pools,
-  federation: [
-    max_connections: 50,
-    timeout: 150_000
-  ],
-  media: [
-    max_connections: 50,
-    timeout: 150_000
-  ],
-  upload: [
-    max_connections: 25,
-    timeout: 300_000
-  ]
-
 # Upload configuration
 config :pleroma, Pleroma.Upload,
   uploader: Pleroma.Uploaders.Local,
@@ -185,20 +171,12 @@ config :mime, :types, %{
 }
 
 config :tesla, adapter: Tesla.Adapter.Hackney
-
 # Configures http settings, upstream proxy etc.
 config :pleroma, :http,
   proxy_url: nil,
   send_user_agent: true,
   user_agent: :default,
-  adapter: [
-    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"]
-    ]
-  ]
+  adapter: []
 
 config :pleroma, :instance,
   name: "Pleroma",
@@ -612,6 +590,49 @@ config :pleroma, :modules, runtime_dir: "instance/modules"
 
 config :pleroma, configurable_from_database: false
 
+config :pleroma, :connections_pool,
+  receive_connection_timeout: 250,
+  max_connections: 250,
+  retry: 5,
+  retry_timeout: 100,
+  await_up_timeout: 5_000
+
+config :pleroma, :pools,
+  federation: [
+    size: 50,
+    max_overflow: 10,
+    timeout: 150_000
+  ],
+  media: [
+    size: 50,
+    max_overflow: 10,
+    timeout: 150_000
+  ],
+  upload: [
+    size: 25,
+    max_overflow: 5,
+    timeout: 300_000
+  ],
+  default: [
+    size: 10,
+    max_overflow: 2,
+    timeout: 10_000
+  ]
+
+config :pleroma, :hackney_pools,
+  federation: [
+    max_connections: 50,
+    timeout: 150_000
+  ],
+  media: [
+    max_connections: 50,
+    timeout: 150_000
+  ],
+  upload: [
+    max_connections: 25,
+    timeout: 300_000
+  ]
+
 # Import environment specific config. This must remain at the bottom
 # of this file so it overrides the configuration defined above.
 import_config "#{Mix.env()}.exs"
index efea7c137728dd2cbf2a1d7674ce97792ded7a4b..d5322fa3378b3e54827a59ad968cbbec6ffc1afb 100644 (file)
@@ -2728,7 +2728,7 @@ config :pleroma, :config_description, [
         key: :adapter,
         type: :module,
         description: "Tesla adapter",
-        suggestions: [Tesla.Adapter.Hackney]
+        suggestions: [Tesla.Adapter.Hackney, Tesla.Adapter.Gun]
       }
     ]
   },
index 078c462051e67ac1861e17c4d71d76945f3f9379..83783cf8fd7e1dfe25a39a43a308106c47fc50b4 100644 (file)
@@ -94,6 +94,8 @@ config :pleroma, Pleroma.ReverseProxy.Client, Pleroma.ReverseProxy.ClientMock
 
 config :pleroma, :modules, runtime_dir: "test/fixtures/modules"
 
+config :pleroma, Pleroma.Gun.API, Pleroma.Gun.API.Mock
+
 if File.exists?("./config/test.secret.exs") do
   import_config "test.secret.exs"
 else
index fb6dfcb087d48383bcd8c90b80a0c8257a653bd8..cd8123c5dd85851616a9a2a40b60e8bd725557f1 100644 (file)
@@ -731,6 +731,8 @@ Some modifications are necessary to save the config settings correctly:
 Most of the settings will be applied in `runtime`, this means that you don't need to restart the instance. But some settings are applied in `compile time` and require a reboot of the instance, such as:
 - all settings inside these keys:
   - `:hackney_pools`
+  - `:connections_pool`
+  - `:pools`
   - `:chat`
 - partially settings inside these keys:
   - `:seconds_valid` in `Pleroma.Captcha`
index 2bd9359832254b662c78224e81b1b78bdc393ba0..1c67eca356b941b272e172ef8addd8cc82f0e747 100644 (file)
@@ -368,8 +368,7 @@ Available caches:
 * `proxy_url`: an upstream proxy to fetch posts and/or media with, (default: `nil`)
 * `send_user_agent`: should we include a user agent with HTTP requests? (default: `true`)
 * `user_agent`: what user agent should we use? (default: `:default`), must be string or `:default`
-* `adapter`: array of hackney options
-
+* `adapter`: array of adapter options
 
 ### :hackney_pools
 
@@ -388,6 +387,39 @@ For each pool, the options are:
 * `timeout` - retention duration for connections
 
 
+### :connections_pool
+
+*For `gun` adapter*
+
+Advanced settings for connections pool. Pool with opened connections. These connections can be reused in worker pools.
+
+* `:receive_connection_timeout` - timeout to receive connection from pool. Default: 250ms.
+* `:max_connections` - maximum number of connections in the pool. Default: 250 connections.
+* `:retry` - number of retries, while `gun` will try to reconnect if connections goes down. Default: 5.
+* `:retry_timeout` - timeout while `gun` will try to reconnect. Default: 100ms.
+* `:await_up_timeout` - timeout while `gun` will wait until connection is up. Default: 5000ms.
+
+### :pools
+
+*For `gun` adapter*
+
+Advanced settings for workers pools.
+
+There's four pools used:
+
+* `:federation` for the federation jobs.
+  You may want this pool max_connections to be at least equal to the number of federator jobs + retry queue jobs.
+* `:media` for rich media, media proxy
+* `:upload` for uploaded media (if using a remote uploader and `proxy_remote: true`)
+* `:default` for other requests
+
+For each pool, the options are:
+
+* `:size` - how much workers the pool can hold
+* `:timeout` - timeout while `gun` will wait for response
+* `:max_overflow` - additional workers if pool is under load
+
+
 ## Captcha
 
 ### Pleroma.Captcha
index 84dccf7f33282eded0b1c6712d5c05d1f63f201a..01e079136ae1d1eb690ee9bc6ed481fc7f58d8bc 100644 (file)
@@ -74,4 +74,43 @@ defmodule Mix.Tasks.Pleroma.Benchmark do
       inputs: inputs
     )
   end
+
+  def run(["adapters"]) do
+    start_pleroma()
+
+    :ok =
+      Pleroma.Pool.Connections.open_conn(
+        "https://httpbin.org/stream-bytes/1500",
+        :gun_connections
+      )
+
+    Process.sleep(1_500)
+
+    Benchee.run(
+      %{
+        "Without conn and without pool" => fn ->
+          {:ok, %Tesla.Env{}} =
+            Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [],
+              adapter: [pool: :no_pool, receive_conn: false]
+            )
+        end,
+        "Without conn and with pool" => fn ->
+          {:ok, %Tesla.Env{}} =
+            Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [],
+              adapter: [receive_conn: false]
+            )
+        end,
+        "With reused conn and without pool" => fn ->
+          {:ok, %Tesla.Env{}} =
+            Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [],
+              adapter: [pool: :no_pool]
+            )
+        end,
+        "With reused conn and with pool" => fn ->
+          {:ok, %Tesla.Env{}} = Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500")
+        end
+      },
+      parallel: 10
+    )
+  end
 end
index 24d999707381b4ed500949db38cf4fe5cabdb1f2..b4e8d3a0b75f02d89cdb6dff0e5f93e2515213ea 100644 (file)
@@ -4,13 +4,13 @@
 
 defmodule Mix.Tasks.Pleroma.Emoji do
   use Mix.Task
+  import Mix.Pleroma
 
   @shortdoc "Manages emoji packs"
   @moduledoc File.read!("docs/administration/CLI_tasks/emoji.md")
 
   def run(["ls-packs" | args]) do
-    Mix.Pleroma.start_pleroma()
-    Application.ensure_all_started(:hackney)
+    start_pleroma()
 
     {options, [], []} = parse_global_opts(args)
 
@@ -36,8 +36,7 @@ defmodule Mix.Tasks.Pleroma.Emoji do
   end
 
   def run(["get-packs" | args]) do
-    Mix.Pleroma.start_pleroma()
-    Application.ensure_all_started(:hackney)
+    start_pleroma()
 
     {options, pack_names, []} = parse_global_opts(args)
 
@@ -135,7 +134,7 @@ defmodule Mix.Tasks.Pleroma.Emoji do
   end
 
   def run(["gen-pack", src]) do
-    Application.ensure_all_started(:hackney)
+    start_pleroma()
 
     proposed_name = Path.basename(src) |> Path.rootname()
     name = String.trim(IO.gets("Pack name [#{proposed_name}]: "))
index 27758cf947d72ffd09fe12870a4c5953b9eaa4d5..df6d3a98d25743832a1fd4d1a5d04b19004e854e 100644 (file)
@@ -3,8 +3,12 @@
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Pleroma.Application do
-  import Cachex.Spec
   use Application
+
+  import Cachex.Spec
+
+  alias Pleroma.Config
+
   require Logger
 
   @name Mix.Project.config()[:name]
@@ -18,9 +22,9 @@ defmodule Pleroma.Application do
   def repository, do: @repository
 
   def user_agent do
-    case Pleroma.Config.get([:http, :user_agent], :default) do
+    case Config.get([:http, :user_agent], :default) do
       :default ->
-        info = "#{Pleroma.Web.base_url()} <#{Pleroma.Config.get([:instance, :email], "")}>"
+        info = "#{Pleroma.Web.base_url()} <#{Config.get([:instance, :email], "")}>"
         named_version() <> "; " <> info
 
       custom ->
@@ -32,7 +36,7 @@ defmodule Pleroma.Application do
   # for more information on OTP Applications
   def start(_type, _args) do
     Pleroma.HTML.compile_scrubbers()
-    Pleroma.Config.DeprecationWarnings.warn()
+    Config.DeprecationWarnings.warn()
     Pleroma.Plugs.HTTPSecurityPlug.warn_if_disabled()
     Pleroma.Repo.check_migrations_applied!()
     setup_instrumenters()
@@ -42,17 +46,17 @@ defmodule Pleroma.Application do
     children =
       [
         Pleroma.Repo,
-        Pleroma.Config.TransferTask,
+        Config.TransferTask,
         Pleroma.Emoji,
         Pleroma.Captcha,
         Pleroma.Plugs.RateLimiter.Supervisor
       ] ++
         cachex_children() ++
-        hackney_pool_children() ++
+        http_pools_children(Config.get(:env)) ++
         [
           Pleroma.Stats,
           Pleroma.JobQueueMonitor,
-          {Oban, Pleroma.Config.get(Oban)}
+          {Oban, Config.get(Oban)}
         ] ++
         task_children(@env) ++
         streamer_child(@env) ++
@@ -62,6 +66,18 @@ defmodule Pleroma.Application do
           Pleroma.Gopher.Server
         ]
 
+    case Pleroma.OTPVersion.check_version() do
+      :ok -> :ok
+      {:error, version} -> raise "
+        !!!OTP VERSION WARNING!!!
+        You are using gun adapter with OTP version #{version}, which doesn't support correct handling of unordered certificates chains.
+        "
+      :undefined -> raise "
+        !!!OTP VERSION WARNING!!!
+        To support correct handling of unordered certificates chains - OTP version must be > 22.2.
+        "
+    end
+
     # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
     # for other strategies and supported options
     opts = [strategy: :one_for_one, name: Pleroma.Supervisor]
@@ -69,7 +85,7 @@ defmodule Pleroma.Application do
   end
 
   def load_custom_modules do
-    dir = Pleroma.Config.get([:modules, :runtime_dir])
+    dir = Config.get([:modules, :runtime_dir])
 
     if dir && File.exists?(dir) do
       dir
@@ -110,20 +126,6 @@ defmodule Pleroma.Application do
     Pleroma.Web.Endpoint.Instrumenter.setup()
   end
 
-  def enabled_hackney_pools do
-    [:media] ++
-      if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do
-        [:federation]
-      else
-        []
-      end ++
-      if Pleroma.Config.get([Pleroma.Upload, :proxy_remote]) do
-        [:upload]
-      else
-        []
-      end
-  end
-
   defp cachex_children do
     [
       build_cachex("used_captcha", ttl_interval: seconds_valid_interval()),
@@ -145,7 +147,7 @@ defmodule Pleroma.Application do
     do: expiration(default: :timer.seconds(6 * 60 * 60), interval: :timer.seconds(60))
 
   defp seconds_valid_interval,
-    do: :timer.seconds(Pleroma.Config.get!([Pleroma.Captcha, :seconds_valid]))
+    do: :timer.seconds(Config.get!([Pleroma.Captcha, :seconds_valid]))
 
   defp build_cachex(type, opts),
     do: %{
@@ -154,7 +156,7 @@ defmodule Pleroma.Application do
       type: :worker
     }
 
-  defp chat_enabled?, do: Pleroma.Config.get([:chat, :enabled])
+  defp chat_enabled?, do: Config.get([:chat, :enabled])
 
   defp streamer_child(:test), do: []
 
@@ -168,13 +170,6 @@ defmodule Pleroma.Application do
 
   defp chat_child(_, _), do: []
 
-  defp hackney_pool_children do
-    for pool <- enabled_hackney_pools() do
-      options = Pleroma.Config.get([:hackney_pools, pool])
-      :hackney_pool.child_spec(pool, options)
-    end
-  end
-
   defp task_children(:test) do
     [
       %{
@@ -199,4 +194,37 @@ defmodule Pleroma.Application do
       }
     ]
   end
+
+  # start hackney and gun pools in tests
+  defp http_pools_children(:test) do
+    hackney_options = Config.get([:hackney_pools, :federation])
+    hackney_pool = :hackney_pool.child_spec(:federation, hackney_options)
+    [hackney_pool, Pleroma.Pool.Supervisor]
+  end
+
+  defp http_pools_children(_) do
+    :tesla
+    |> Application.get_env(:adapter)
+    |> http_pools()
+  end
+
+  defp http_pools(Tesla.Adapter.Hackney) do
+    pools = [:federation, :media]
+
+    pools =
+      if Config.get([Pleroma.Upload, :proxy_remote]) do
+        [:upload | pools]
+      else
+        pools
+      end
+
+    for pool <- pools do
+      options = Config.get([:hackney_pools, pool])
+      :hackney_pool.child_spec(pool, options)
+    end
+  end
+
+  defp http_pools(Tesla.Adapter.Gun), do: [Pleroma.Pool.Supervisor]
+
+  defp http_pools(_), do: []
 end
index 119251bee9b5f9eb83f100f8cddf17db09976621..bdacefa979d94365b79ebb3cae10f1dd515c2b62 100644 (file)
@@ -278,8 +278,6 @@ defmodule Pleroma.ConfigDB do
     }
   end
 
-  defp do_convert({:partial_chain, entity}), do: %{"tuple" => [":partial_chain", inspect(entity)]}
-
   defp do_convert(entity) when is_tuple(entity) do
     value =
       entity
@@ -323,15 +321,6 @@ defmodule Pleroma.ConfigDB do
     {:proxy_url, {do_transform_string(type), parse_host(host), port}}
   end
 
-  defp do_transform(%{"tuple" => [":partial_chain", entity]}) do
-    {partial_chain, []} =
-      entity
-      |> String.replace(~r/[^\w|^{:,[|^,|^[|^\]^}|^\/|^\.|^"]^\s/, "")
-      |> Code.eval_string()
-
-    {:partial_chain, partial_chain}
-  end
-
   defp do_transform(%{"tuple" => entity}) do
     Enum.reduce(entity, {}, fn val, acc -> Tuple.append(acc, do_transform(val)) end)
   end
index 6c5ba1f95ca040107bcf026c904ed6467e530e3d..251074aaac6d4f49cdd402d0f78ac0b2c23efde4 100644 (file)
@@ -18,7 +18,10 @@ defmodule Pleroma.Config.TransferTask do
     {:pleroma, Oban},
     {:pleroma, :rate_limit},
     {:pleroma, :markup},
-    {:plerome, :streamer}
+    {:pleroma, :streamer},
+    {:pleroma, :pools},
+    {:pleroma, :connections_pool},
+    {:tesla, :adapter}
   ]
 
   @reboot_time_subkeys [
@@ -74,6 +77,28 @@ defmodule Pleroma.Config.TransferTask do
     end
   end
 
+  defp group_for_restart(:logger, key, _, merged_value) do
+    # change logger configuration in runtime, without restart
+    if Keyword.keyword?(merged_value) and
+         key not in [:compile_time_application, :backends, :compile_time_purge_matching] do
+      Logger.configure_backend(key, merged_value)
+    else
+      Logger.configure([{key, merged_value}])
+    end
+
+    nil
+  end
+
+  defp group_for_restart(:tesla, _, _, _), do: :pleroma
+
+  defp group_for_restart(group, _, _, _) when group != :pleroma, do: group
+
+  defp group_for_restart(group, key, value, _) do
+    if pleroma_need_restart?(group, key, value) do
+      group
+    end
+  end
+
   defp merge_and_update(setting) do
     try do
       key = ConfigDB.from_string(setting.key)
@@ -95,21 +120,7 @@ defmodule Pleroma.Config.TransferTask do
 
       :ok = update_env(group, key, merged_value)
 
-      if group != :logger do
-        if group != :pleroma or pleroma_need_restart?(group, key, value) do
-          group
-        end
-      else
-        # change logger configuration in runtime, without restart
-        if Keyword.keyword?(merged_value) and
-             key not in [:compile_time_application, :backends, :compile_time_purge_matching] do
-          Logger.configure_backend(key, merged_value)
-        else
-          Logger.configure([{key, merged_value}])
-        end
-
-        nil
-      end
+      group_for_restart(group, key, value, merged_value)
     rescue
       error ->
         error_msg =
diff --git a/lib/pleroma/gun/api.ex b/lib/pleroma/gun/api.ex
new file mode 100644 (file)
index 0000000..a0c3c54
--- /dev/null
@@ -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
diff --git a/lib/pleroma/gun/api/mock.ex b/lib/pleroma/gun/api/mock.ex
new file mode 100644 (file)
index 0000000..0134b01
--- /dev/null
@@ -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
diff --git a/lib/pleroma/gun/conn.ex b/lib/pleroma/gun/conn.ex
new file mode 100644 (file)
index 0000000..2474829
--- /dev/null
@@ -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
diff --git a/lib/pleroma/gun/gun.ex b/lib/pleroma/gun/gun.ex
new file mode 100644 (file)
index 0000000..4a1bbc9
--- /dev/null
@@ -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
diff --git a/lib/pleroma/http/adapter.ex b/lib/pleroma/http/adapter.ex
new file mode 100644 (file)
index 0000000..6166a3e
--- /dev/null
@@ -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
diff --git a/lib/pleroma/http/adapter/gun.ex b/lib/pleroma/http/adapter/gun.ex
new file mode 100644 (file)
index 0000000..f25afed
--- /dev/null
@@ -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
diff --git a/lib/pleroma/http/adapter/hackney.ex b/lib/pleroma/http/adapter/hackney.ex
new file mode 100644 (file)
index 0000000..00db300
--- /dev/null
@@ -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
index 7e2c6f5e8207e7b9b413d408b8e4f892f0fc83cb..85918341a31fc2722b3db5e4eef44a32d2cb815f 100644 (file)
@@ -4,40 +4,99 @@
 
 defmodule Pleroma.HTTP.Connection do
   @moduledoc """
-  Connection for http-requests.
+  Configure Tesla.Client with default and customized adapter options.
   """
+  @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()
 
-  @hackney_options [
-    connect_timeout: 10_000,
-    recv_timeout: 20_000,
-    follow_redirect: true,
-    force_redirect: true,
-    pool: :federation
-  ]
-  @adapter Application.get_env(:tesla, :adapter)
+  @defaults [pool: :federation]
 
-  @doc """
-  Configure a client connection
+  require Logger
 
-  # Returns
+  alias Pleroma.Config
+  alias Pleroma.HTTP.Adapter
 
-  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().options(uri)
+  end
+
+  defp pool_timeout(opts) do
+    timeout =
+      Config.get([:pools, opts[:pool], :timeout]) || Config.get([:pools, :default, :timeout])
+
+    Keyword.merge(opts, timeout: timeout)
   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 after_request(keyword()) :: :ok
+  def after_request(opts), do: adapter().after_request(opts)
+
+  defp adapter do
+    case Application.get_env(:tesla, :adapter) do
+      Tesla.Adapter.Gun -> Adapter.Gun
+      Tesla.Adapter.Hackney -> Adapter.Hackney
+      _ -> Adapter
+    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 in proxy fail #{inspect(proxy)}")
+        {:error, :error_parsing_port_in_proxy}
+
+      :error ->
+        Logger.warn("parsing port in proxy fail #{inspect(proxy)}")
+        {:error, :error_parsing_port_in_proxy}
+
+      _ ->
+        Logger.warn("parsing proxy fail #{inspect(proxy)}")
+        {:error, :error_parsing_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 fail #{inspect(proxy)}")
+        {:error, :error_parsing_proxy}
+    end
+  end
+
+  @spec parse_host(String.t() | atom() | charlist()) :: charlist() | ip_address()
+  def parse_host(host) when is_list(host), do: host
+  def parse_host(host) when is_atom(host), do: to_charlist(host)
+
+  def parse_host(host) when is_binary(host) do
+    host = to_charlist(host)
+
+    case :inet.parse_address(host) do
+      {:error, :einval} -> host
+      {:ok, ip} -> ip
+    end
   end
 end
index dec24458a82b0a8c04f094aa7553a4dd54cf9e2b..ad47dc936f56ca60ed22705fefa2e06d79df3916 100644 (file)
@@ -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,97 @@ defmodule Pleroma.HTTP do
   `{:ok, %Tesla.Env{}}` or `{:error, error}`
 
   """
-  def request(method, url, body \\ "", headers \\ [], options \\ []) do
+  @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
+    with uri <- URI.parse(url),
+         received_adapter_opts <- Keyword.get(options, :adapter, []),
+         adapter_opts <- Connection.options(uri, received_adapter_opts),
+         options <- put_in(options[:adapter], adapter_opts),
+         params <- Keyword.get(options, :params, []),
+         request <- build_request(method, headers, options, url, body, params),
+         client <- Tesla.client([Tesla.Middleware.FollowRedirects], tesla_adapter()),
+         pid <- Process.whereis(adapter_opts[:pool]) do
+      pool_alive? =
+        if tesla_adapter() == Tesla.Adapter.Gun do
+          if pid, do: Process.alive?(pid), else: false
+        else
+          false
+        end
+
+      request_opts =
+        adapter_opts
+        |> Enum.into(%{})
+        |> Map.put(:env, Pleroma.Config.get([:env]))
+        |> Map.put(:pool_alive?, pool_alive?)
+
+      response =
+        request(
+          client,
+          request,
+          request_opts
+        )
+
+      Connection.after_request(adapter_opts)
+
+      response
+    end
+  end
+
+  @spec request(Client.t(), keyword(), map()) :: {:ok, Env.t()} | {:error, any()}
+  def request(%Client{} = client, request, %{env: :test}), do: request_try(client, request)
+
+  def request(%Client{} = client, request, %{body_as: :chunks}) do
+    request_try(client, request)
+  end
+
+  def request(%Client{} = client, request, %{pool_alive?: false}) do
+    request_try(client, request)
+  end
+
+  def request(%Client{} = client, request, %{pool: pool, timeout: timeout}) 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)).()
+      :poolboy.transaction(
+        pool,
+        &Pleroma.Pool.Request.execute(&1, client, request, timeout + 500),
+        timeout + 1_000
+      )
     rescue
       e ->
         {:error, e}
     catch
+      :exit, {:timeout, _} ->
+        Logger.warn("Receive response from pool failed #{request[:url]}")
+        {:error, :recv_pool_timeout}
+
       :exit, e ->
         {:error, e}
     end
   end
 
-  defp process_sni_options(options, nil), do: options
-
-  defp process_sni_options(options, url) do
-    uri = URI.parse(url)
-    host = uri.host |> to_charlist()
-
-    case uri.scheme do
-      "https" -> options ++ [ssl: [server_name_indication: host]]
-      _ -> options
+  @spec request_try(Client.t(), keyword()) :: {:ok, Env.t()} | {:error, any()}
+  def request_try(client, request) do
+    try do
+      Tesla.request(client, request)
+    rescue
+      e ->
+        {:error, e}
+    catch
+      :exit, e ->
+        {:error, e}
     end
   end
 
-  def process_request_options(options) do
-    Keyword.merge(Pleroma.HTTP.Connection.hackney_options([]), options)
+  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
 
-  @doc """
-  Performs GET request.
-
-  See `Pleroma.HTTP.request/5`
-  """
-  def get(url, headers \\ [], options \\ []),
-    do: request(:get, url, "", headers, options)
-
-  @doc """
-  Performs POST request.
-
-  See `Pleroma.HTTP.request/5`
-  """
-  def post(url, body, headers \\ [], options \\ []),
-    do: request(:post, url, body, headers, options)
+  defp tesla_adapter, do: Application.get_env(:tesla, :adapter)
 end
diff --git a/lib/pleroma/http/request.ex b/lib/pleroma/http/request.ex
new file mode 100644 (file)
index 0000000..891d88d
--- /dev/null
@@ -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
index e2345799932b11cdb462714f67ea59af7166123f..491acd0f9fbcc67fb282efdf23ea1efa3ab1f50d 100644 (file)
@@ -7,77 +7,54 @@ 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
+  alias Pleroma.HTTP.Request
+  alias Tesla.Multipart
 
-  ## Returns
-
-  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()}]
+        headers ++ [{"user-agent", Pleroma.Application.user_agent()}]
       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
+  @spec opts(Request.t(), keyword()) :: Request.t()
+  def opts(request, options), do: %{request | opts: options}
 
+  # NOTE: isn't used anywhere
   @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()
+  @spec add_optional_params(Request.t(), %{optional(atom) => atom}, keyword()) :: map()
   def add_optional_params(request, _, []), do: request
 
   def add_optional_params(request, definitions, [{key, value} | tail]) do
@@ -94,49 +71,43 @@ defmodule Pleroma.HTTP.RequestBuilder do
 
   @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
index 037c423395184a223cccd987881d727de451cc27..5e9bf15748007fe787b144e0a471e529a376161f 100644 (file)
@@ -137,7 +137,7 @@ defmodule Pleroma.Object.Fetcher do
         date: date
       })
 
-    [{:Signature, signature}]
+    [{"signature", signature}]
   end
 
   defp sign_fetch(headers, id, date) do
@@ -150,7 +150,7 @@ defmodule Pleroma.Object.Fetcher do
 
   defp maybe_date_fetch(headers, date) do
     if Pleroma.Config.get([:activitypub, :sign_object_fetches]) do
-      headers ++ [{:Date, date}]
+      headers ++ [{"date", date}]
     else
       headers
     end
@@ -162,7 +162,7 @@ defmodule Pleroma.Object.Fetcher do
     date = Pleroma.Signature.signed_date()
 
     headers =
-      [{:Accept, "application/activity+json"}]
+      [{"accept", "application/activity+json"}]
       |> maybe_date_fetch(date)
       |> sign_fetch(id, date)
 
diff --git a/lib/pleroma/otp_version.ex b/lib/pleroma/otp_version.ex
new file mode 100644 (file)
index 0000000..0be1893
--- /dev/null
@@ -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
diff --git a/lib/pleroma/pool/connections.ex b/lib/pleroma/pool/connections.ex
new file mode 100644 (file)
index 0000000..1ed16d1
--- /dev/null
@@ -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
diff --git a/lib/pleroma/pool/pool.ex b/lib/pleroma/pool/pool.ex
new file mode 100644 (file)
index 0000000..a7ae64c
--- /dev/null
@@ -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
diff --git a/lib/pleroma/pool/request.ex b/lib/pleroma/pool/request.ex
new file mode 100644 (file)
index 0000000..2c35745
--- /dev/null
@@ -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
diff --git a/lib/pleroma/pool/supervisor.ex b/lib/pleroma/pool/supervisor.ex
new file mode 100644 (file)
index 0000000..32be226
--- /dev/null
@@ -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
index 776c4794c013811a26488bbbaa386371a6250ee4..63261b94cdea052350b9ae46edf00428733d4c70 100644 (file)
@@ -3,19 +3,23 @@
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Pleroma.ReverseProxy.Client do
-  @callback request(atom(), String.t(), [tuple()], String.t(), list()) ::
-              {:ok, pos_integer(), [tuple()], reference() | map()}
-              | {:ok, pos_integer(), [tuple()]}
+  @type status :: pos_integer()
+  @type header_name :: String.t()
+  @type header_value :: String.t()
+  @type headers :: [{header_name(), header_value()}]
+
+  @callback request(atom(), String.t(), headers(), String.t(), list()) ::
+              {:ok, status(), headers(), reference() | map()}
+              | {:ok, status(), headers()}
               | {:ok, reference()}
               | {:error, term()}
 
-  @callback stream_body(reference() | pid() | map()) ::
-              {:ok, binary()} | :done | {:error, String.t()}
+  @callback stream_body(map()) :: {:ok, binary(), map()} | :done | {:error, atom() | String.t()}
 
   @callback close(reference() | pid() | map()) :: :ok
 
-  def request(method, url, headers, "", opts \\ []) do
-    client().request(method, url, headers, "", opts)
+  def request(method, url, headers, body \\ "", opts \\ []) do
+    client().request(method, url, headers, body, opts)
   end
 
   def stream_body(ref), do: client().stream_body(ref)
@@ -23,6 +27,12 @@ defmodule Pleroma.ReverseProxy.Client do
   def close(ref), do: client().close(ref)
 
   defp client do
-    Pleroma.Config.get([Pleroma.ReverseProxy.Client], :hackney)
+    :tesla
+    |> Application.get_env(:adapter)
+    |> client()
   end
+
+  defp client(Tesla.Adapter.Hackney), do: Pleroma.ReverseProxy.Client.Hackney
+  defp client(Tesla.Adapter.Gun), do: Pleroma.ReverseProxy.Client.Tesla
+  defp client(_), do: Pleroma.Config.get!(Pleroma.ReverseProxy.Client)
 end
diff --git a/lib/pleroma/reverse_proxy/client/hackney.ex b/lib/pleroma/reverse_proxy/client/hackney.ex
new file mode 100644 (file)
index 0000000..e41560a
--- /dev/null
@@ -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
diff --git a/lib/pleroma/reverse_proxy/client/tesla.ex b/lib/pleroma/reverse_proxy/client/tesla.ex
new file mode 100644 (file)
index 0000000..55a11b4
--- /dev/null
@@ -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
index 2ed7193150729b906f1f50ee5fffc9c98e7130e5..9f5710c92ec60d5ff8e34ff1b381a941ad1d252a 100644 (file)
@@ -3,8 +3,6 @@
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Pleroma.ReverseProxy do
-  alias Pleroma.HTTP
-
   @keep_req_headers ~w(accept user-agent accept-encoding cache-control if-modified-since) ++
                       ~w(if-unmodified-since if-none-match if-range range)
   @resp_cache_headers ~w(etag date last-modified cache-control)
@@ -61,10 +59,10 @@ defmodule Pleroma.ReverseProxy do
 
   * `req_headers`, `resp_headers` additional headers.
 
-  * `http`: options for [hackney](https://github.com/benoitc/hackney).
+  * `http`: options for [gun](https://github.com/ninenines/gun).
 
   """
-  @default_hackney_options [pool: :media]
+  @default_options [pool: :media]
 
   @inline_content_types [
     "image/gif",
@@ -97,11 +95,7 @@ defmodule Pleroma.ReverseProxy do
   def call(_conn, _url, _opts \\ [])
 
   def call(conn = %{method: method}, url, opts) when method in @methods do
-    hackney_opts =
-      Pleroma.HTTP.Connection.hackney_options([])
-      |> Keyword.merge(@default_hackney_options)
-      |> Keyword.merge(Keyword.get(opts, :http, []))
-      |> HTTP.process_request_options()
+    client_opts = Keyword.merge(@default_options, Keyword.get(opts, :http, []))
 
     req_headers = build_req_headers(conn.req_headers, opts)
 
@@ -113,7 +107,7 @@ defmodule Pleroma.ReverseProxy do
       end
 
     with {:ok, nil} <- Cachex.get(:failed_proxy_url_cache, url),
-         {:ok, code, headers, client} <- request(method, url, req_headers, hackney_opts),
+         {:ok, code, headers, client} <- request(method, url, req_headers, client_opts),
          :ok <-
            header_length_constraint(
              headers,
@@ -159,11 +153,11 @@ defmodule Pleroma.ReverseProxy do
     |> halt()
   end
 
-  defp request(method, url, headers, hackney_opts) do
+  defp request(method, url, headers, opts) do
     Logger.debug("#{__MODULE__} #{method} #{url} #{inspect(headers)}")
     method = method |> String.downcase() |> String.to_existing_atom()
 
-    case client().request(method, url, headers, "", hackney_opts) do
+    case client().request(method, url, headers, "", opts) do
       {:ok, code, headers, client} when code in @valid_resp_codes ->
         {:ok, code, downcase_headers(headers), client}
 
@@ -213,7 +207,7 @@ defmodule Pleroma.ReverseProxy do
              duration,
              Keyword.get(opts, :max_read_duration, @max_read_duration)
            ),
-         {:ok, data} <- client().stream_body(client),
+         {:ok, data, client} <- client().stream_body(client),
          {:ok, duration} <- increase_read_duration(duration),
          sent_so_far = sent_so_far + byte_size(data),
          :ok <-
index df774b0f7a471c9cc00c957d5ff8050dc3181f2a..ade87daf2f25460ba0f0d9a1d53dd26ad668ba18 100644 (file)
@@ -12,17 +12,23 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do
 
   require Logger
 
-  @hackney_options [
-    pool: :media,
-    recv_timeout: 10_000
+  @options [
+    pool: :media
   ]
 
   def perform(:prefetch, url) do
     Logger.debug("Prefetching #{inspect(url)}")
 
+    opts =
+      if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do
+        Keyword.put(@options, :recv_timeout, 10_000)
+      else
+        @options
+      end
+
     url
     |> MediaProxy.url()
-    |> HTTP.get([], adapter: @hackney_options)
+    |> HTTP.get([], adapter: opts)
   end
 
   def perform(:preload, %{"object" => %{"attachment" => attachments}} = _message) do
index 16b1a53d2886df63a1fa3ce2354b85b53156aa6b..0ae92637518d7303a62d7da2ae3a0a4292ff4761 100644 (file)
@@ -3,11 +3,9 @@
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Pleroma.Web.RelMe do
-  @hackney_options [
+  @options [
     pool: :media,
-    recv_timeout: 2_000,
-    max_body: 2_000_000,
-    with_body: true
+    max_body: 2_000_000
   ]
 
   if Pleroma.Config.get(:env) == :test do
@@ -25,8 +23,18 @@ defmodule Pleroma.Web.RelMe do
   def parse(_), do: {:error, "No URL provided"}
 
   defp parse_url(url) do
+    opts =
+      if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do
+        Keyword.merge(@options,
+          recv_timeout: 2_000,
+          with_body: true
+        )
+      else
+        @options
+      end
+
     with {:ok, %Tesla.Env{body: html, status: status}} when status in 200..299 <-
-           Pleroma.HTTP.get(url, [], adapter: @hackney_options),
+           Pleroma.HTTP.get(url, [], adapter: opts),
          data <-
            Floki.attribute(html, "link[rel~=me]", "href") ++
              Floki.attribute(html, "a[rel~=me]", "href") do
index c06b0a0f2668338415d592fde838bff42e49e056..9deb038451e2e8a8cdd63af7e891653a426017cf 100644 (file)
@@ -3,11 +3,9 @@
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Pleroma.Web.RichMedia.Parser do
-  @hackney_options [
+  @options [
     pool: :media,
-    recv_timeout: 2_000,
-    max_body: 2_000_000,
-    with_body: true
+    max_body: 2_000_000
   ]
 
   defp parsers do
@@ -77,8 +75,18 @@ defmodule Pleroma.Web.RichMedia.Parser do
   end
 
   defp parse_url(url) do
+    opts =
+      if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do
+        Keyword.merge(@options,
+          recv_timeout: 2_000,
+          with_body: true
+        )
+      else
+        @options
+      end
+
     try do
-      {:ok, %Tesla.Env{body: html}} = Pleroma.HTTP.get(url, [], adapter: @hackney_options)
+      {:ok, %Tesla.Env{body: html}} = Pleroma.HTTP.get(url, [], adapter: opts)
 
       html
       |> parse_html
index b4cc801799e97a5fa3224875136bd0a645e98acb..91e9e2271fcf90e6441e28937322288927078329 100644 (file)
@@ -205,7 +205,7 @@ defmodule Pleroma.Web.WebFinger do
     with response <-
            HTTP.get(
              address,
-             Accept: "application/xrd+xml,application/jrd+json"
+             [{"accept", "application/xrd+xml,application/jrd+json"}]
            ),
          {:ok, %{status: status, body: body}} when status in 200..299 <- response do
       doc = XML.parse_document(body)
diff --git a/mix.exs b/mix.exs
index b28c65694cf378752b9368f5f5721ffdfaf48d21..7c6de5423e31e3969aef4cb00d907f28919b7f26 100644 (file)
--- a/mix.exs
+++ b/mix.exs
@@ -120,6 +120,10 @@ defmodule Pleroma.Mixfile do
       {:cachex, "~> 3.0.2"},
       {:poison, "~> 3.0", override: true},
       {:tesla, "~> 1.3", override: true},
+      {:castore, "~> 0.1"},
+      {:cowlib, "~> 2.8", override: true},
+      {:gun,
+       github: "ninenines/gun", ref: "bd6425ab87428cf4c95f4d23e0a48fd065fbd714", override: true},
       {:jason, "~> 1.0"},
       {:mogrify, "~> 0.6.1"},
       {:ex_aws, "~> 2.1"},
index 9c811a97491f2ddcab5cd9107908ac3e5a835b66..158a87e4787230c8f57dbf516b703db03e08c187 100644 (file)
--- a/mix.lock
+++ b/mix.lock
@@ -9,6 +9,7 @@
   "cachex": {:hex, :cachex, "3.0.3", "4e2d3e05814a5738f5ff3903151d5c25636d72a3527251b753f501ad9c657967", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "3aadb1e605747122f60aa7b0b121cca23c14868558157563b3f3e19ea929f7d0"},
   "calendar": {:hex, :calendar, "0.17.6", "ec291cb2e4ba499c2e8c0ef5f4ace974e2f9d02ae9e807e711a9b0c7850b9aee", [:mix], [{:tzdata, "~> 0.5.20 or ~> 0.1.201603 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "738d0e17a93c2ccfe4ddc707bdc8e672e9074c8569498483feb1c4530fb91b2b"},
   "captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "e0f16822d578866e186a0974d65ad58cddc1e2ab", [ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"]},
+  "castore": {:hex, :castore, "0.1.5", "591c763a637af2cc468a72f006878584bc6c306f8d111ef8ba1d4c10e0684010", [:mix], [], "hexpm", "6db356b2bc6cc22561e051ff545c20ad064af57647e436650aa24d7d06cd941a"},
   "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "805abd97539caf89ec6d4732c91e62ba9da0cda51ac462380bbd28ee697a8c42"},
   "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
   "comeonin": {:hex, :comeonin, "4.1.2", "3eb5620fd8e35508991664b4c2b04dd41e52f1620b36957be837c1d7784b7592", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, repo: "hexpm", optional: true]}], "hexpm", "d8700a0ca4dbb616c22c9b3f6dd539d88deaafec3efe66869d6370c9a559b3e9"},
@@ -45,6 +46,7 @@
   "gen_stage": {:hex, :gen_stage, "0.14.3", "d0c66f1c87faa301c1a85a809a3ee9097a4264b2edf7644bf5c123237ef732bf", [:mix], [], "hexpm"},
   "gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm"},
   "gettext": {:hex, :gettext, "0.17.4", "f13088e1ec10ce01665cf25f5ff779e7df3f2dc71b37084976cf89d1aa124d5c", [:mix], [], "hexpm", "3c75b5ea8288e2ee7ea503ff9e30dfe4d07ad3c054576a6e60040e79a801e14d"},
+  "gun": {:git, "https://github.com/ninenines/gun.git", "bd6425ab87428cf4c95f4d23e0a48fd065fbd714", [ref: "bd6425ab87428cf4c95f4d23e0a48fd065fbd714"]},
   "hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "e0100f8ef7d1124222c11ad362c857d3df7cb5f4204054f9f0f4a728666591fc"},
   "html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"},
   "html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"},
index e75f83586a5e585426d9ed7c4a5379f4e81074a9..8729e5746cdc7990bab1bc9f4f71210eeee26858 100644 (file)
@@ -83,7 +83,7 @@ defmodule Pleroma.Activity.Ir.TopicsTest do
       assert Enum.member?(topics, "hashtag:bar")
     end
 
-    test "only converts strinngs to hash tags", %{
+    test "only converts strings to hash tags", %{
       activity: %{object: %{data: data} = object} = activity
     } do
       tagged_data = Map.put(data, "tag", [2])
index 812709fd87b029c87c3d84f5a00b3029135ca3a4..394040a59912201c333bd4fa9084e890173d0a07 100644 (file)
@@ -478,14 +478,6 @@ defmodule Pleroma.ConfigDBTest do
       assert ConfigDB.from_binary(binary) == [key: "value"]
     end
 
-    test "keyword with partial_chain key" do
-      binary =
-        ConfigDB.transform([%{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]}])
-
-      assert binary == :erlang.term_to_binary(partial_chain: &:hackney_connect.partial_chain/1)
-      assert ConfigDB.from_binary(binary) == [partial_chain: &:hackney_connect.partial_chain/1]
-    end
-
     test "keyword" do
       binary =
         ConfigDB.transform([
diff --git a/test/fixtures/warnings/otp_version/21.1 b/test/fixtures/warnings/otp_version/21.1
new file mode 100644 (file)
index 0000000..90cd64c
--- /dev/null
@@ -0,0 +1 @@
+21.1
\ No newline at end of file
diff --git a/test/fixtures/warnings/otp_version/22.1 b/test/fixtures/warnings/otp_version/22.1
new file mode 100644 (file)
index 0000000..d9b3143
--- /dev/null
@@ -0,0 +1 @@
+22.1
\ No newline at end of file
diff --git a/test/fixtures/warnings/otp_version/22.4 b/test/fixtures/warnings/otp_version/22.4
new file mode 100644 (file)
index 0000000..1da8ccd
--- /dev/null
@@ -0,0 +1 @@
+22.4
\ No newline at end of file
diff --git a/test/fixtures/warnings/otp_version/23.0 b/test/fixtures/warnings/otp_version/23.0
new file mode 100644 (file)
index 0000000..4266d86
--- /dev/null
@@ -0,0 +1 @@
+23.0
\ No newline at end of file
diff --git a/test/fixtures/warnings/otp_version/error b/test/fixtures/warnings/otp_version/error
new file mode 100644 (file)
index 0000000..8fdd954
--- /dev/null
@@ -0,0 +1 @@
+22
\ No newline at end of file
diff --git a/test/fixtures/warnings/otp_version/undefined b/test/fixtures/warnings/otp_version/undefined
new file mode 100644 (file)
index 0000000..66dc905
--- /dev/null
@@ -0,0 +1 @@
+undefined
\ No newline at end of file
diff --git a/test/gun/gun_test.exs b/test/gun/gun_test.exs
new file mode 100644 (file)
index 0000000..7f18561
--- /dev/null
@@ -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
diff --git a/test/http/adapter/gun_test.exs b/test/http/adapter/gun_test.exs
new file mode 100644 (file)
index 0000000..37489e1
--- /dev/null
@@ -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
diff --git a/test/http/adapter/hackney_test.exs b/test/http/adapter/hackney_test.exs
new file mode 100644 (file)
index 0000000..35cb581
--- /dev/null
@@ -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
diff --git a/test/http/adapter_test.exs b/test/http/adapter_test.exs
new file mode 100644 (file)
index 0000000..37e47da
--- /dev/null
@@ -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
diff --git a/test/http/connection_test.exs b/test/http/connection_test.exs
new file mode 100644 (file)
index 0000000..c1ff0cc
--- /dev/null
@@ -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
index 80ef25d7b1051f6651e6750054adbe4e3a6bc284..27ca651bebb882bf8a9f9e4d222ad285aa390e74 100644 (file)
@@ -5,30 +5,32 @@
 defmodule Pleroma.HTTP.RequestBuilderTest do
   use ExUnit.Case, async: true
   use Pleroma.Tests.Helpers
+  alias Pleroma.Config
+  alias Pleroma.HTTP.Request
   alias Pleroma.HTTP.RequestBuilder
 
   describe "headers/2" do
     clear_config([:http, :send_user_agent])
 
     test "don't send pleroma user agent" do
-      assert RequestBuilder.headers(%{}, []) == %{headers: []}
+      assert RequestBuilder.headers(%Request{}, []) == %Request{headers: []}
     end
 
     test "send pleroma user agent" do
-      Pleroma.Config.put([:http, :send_user_agent], true)
-      Pleroma.Config.put([:http, :user_agent], :default)
+      Config.put([:http, :send_user_agent], true)
+      Config.put([:http, :user_agent], :default)
 
-      assert RequestBuilder.headers(%{}, []) == %{
-               headers: [{"User-Agent", Pleroma.Application.user_agent()}]
+      assert RequestBuilder.headers(%Request{}, []) == %Request{
+               headers: [{"user-agent", Pleroma.Application.user_agent()}]
              }
     end
 
     test "send custom user agent" do
-      Pleroma.Config.put([:http, :send_user_agent], true)
-      Pleroma.Config.put([:http, :user_agent], "totally-not-pleroma")
+      Config.put([:http, :send_user_agent], true)
+      Config.put([:http, :user_agent], "totally-not-pleroma")
 
-      assert RequestBuilder.headers(%{}, []) == %{
-               headers: [{"User-Agent", "totally-not-pleroma"}]
+      assert RequestBuilder.headers(%Request{}, []) == %Request{
+               headers: [{"user-agent", "totally-not-pleroma"}]
              }
     end
   end
@@ -40,19 +42,19 @@ defmodule Pleroma.HTTP.RequestBuilderTest do
 
     test "add query parameter" do
       assert RequestBuilder.add_optional_params(
-               %{},
+               %Request{},
                %{query: :query, body: :body, another: :val},
                [
                  {:query, "param1=val1&param2=val2"},
                  {:body, "some body"}
                ]
-             ) == %{query: "param1=val1&param2=val2", body: "some body"}
+             ) == %Request{query: "param1=val1&param2=val2", body: "some body"}
     end
   end
 
   describe "add_param/4" do
     test "add file parameter" do
-      %{
+      %Request{
         body: %Tesla.Multipart{
           boundary: _,
           content_type_params: [],
@@ -69,7 +71,7 @@ defmodule Pleroma.HTTP.RequestBuilderTest do
             }
           ]
         }
-      } = RequestBuilder.add_param(%{}, :file, "filename.png", "some-path/filename.png")
+      } = RequestBuilder.add_param(%Request{}, :file, "filename.png", "some-path/filename.png")
     end
 
     test "add key to body" do
@@ -81,7 +83,7 @@ defmodule Pleroma.HTTP.RequestBuilderTest do
             %Tesla.Multipart.Part{
               body: "\"someval\"",
               dispositions: [name: "somekey"],
-              headers: ["Content-Type": "application/json"]
+              headers: [{"content-type", "application/json"}]
             }
           ]
         }
index 5f9522cf06af962ea1f2a58fa4f0204b72275666..d80b96496d455914d14914e80ed0d51a2c971ac4 100644 (file)
@@ -3,8 +3,10 @@
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Pleroma.HTTPTest do
-  use Pleroma.DataCase
+  use ExUnit.Case
+  use Pleroma.Tests.Helpers
   import Tesla.Mock
+  alias Pleroma.HTTP
 
   setup do
     mock(fn
@@ -27,7 +29,7 @@ defmodule Pleroma.HTTPTest do
 
   describe "get/1" do
     test "returns successfully result" do
-      assert Pleroma.HTTP.get("http://example.com/hello") == {
+      assert HTTP.get("http://example.com/hello") == {
                :ok,
                %Tesla.Env{status: 200, body: "hello"}
              }
@@ -36,7 +38,7 @@ defmodule Pleroma.HTTPTest do
 
   describe "get/2 (with headers)" do
     test "returns successfully result for json content-type" do
-      assert Pleroma.HTTP.get("http://example.com/hello", [{"content-type", "application/json"}]) ==
+      assert HTTP.get("http://example.com/hello", [{"content-type", "application/json"}]) ==
                {
                  :ok,
                  %Tesla.Env{
@@ -50,10 +52,35 @@ defmodule Pleroma.HTTPTest do
 
   describe "post/2" do
     test "returns successfully result" do
-      assert Pleroma.HTTP.post("http://example.com/world", "") == {
+      assert HTTP.post("http://example.com/world", "") == {
                :ok,
                %Tesla.Env{status: 200, body: "world"}
              }
     end
   end
+
+  describe "connection pools" do
+    @describetag :integration
+    clear_config([Pleroma.Gun.API]) do
+      Pleroma.Config.put([Pleroma.Gun.API], Pleroma.Gun)
+    end
+
+    test "gun" 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)
+
+      options = [adapter: [pool: :federation]]
+
+      assert {:ok, resp} = HTTP.get("https://httpbin.org/user-agent", [], options)
+
+      assert resp.status == 200
+
+      state = Pleroma.Pool.Connections.get_state(:gun_connections)
+      assert state.conns["https:httpbin.org:443"]
+    end
+  end
 end
index 04bf5b41aaf62883934167a3cca48978c7b658c8..1de3c6e3bf2a8095982f4e13e34335608e44c905 100644 (file)
@@ -649,6 +649,13 @@ defmodule Pleroma.NotificationTest do
         "object" => remote_user.ap_id
       }
 
+      remote_user_url = remote_user.ap_id
+
+      Tesla.Mock.mock(fn
+        %{method: :get, url: ^remote_user_url} ->
+          %Tesla.Env{status: 404, body: ""}
+      end)
+
       {:ok, _delete_activity} = Transmogrifier.handle_incoming(delete_user_message)
       ObanHelpers.perform_all()
 
diff --git a/test/otp_version_test.exs b/test/otp_version_test.exs
new file mode 100644 (file)
index 0000000..f26b90f
--- /dev/null
@@ -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
diff --git a/test/pool/connections_test.exs b/test/pool/connections_test.exs
new file mode 100644 (file)
index 0000000..6f0e041
--- /dev/null
@@ -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
diff --git a/test/reverse_proxy/client/tesla_test.exs b/test/reverse_proxy/client/tesla_test.exs
new file mode 100644 (file)
index 0000000..75a7098
--- /dev/null
@@ -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
similarity index 79%
rename from test/reverse_proxy_test.exs
rename to test/reverse_proxy/reverse_proxy_test.exs
index 0672f57db5ba5ebe3effd6129454d3492ca4c408..1ab3cc4bb60436b3e27f0f491d80606eb8576c39 100644 (file)
@@ -3,7 +3,7 @@
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Pleroma.ReverseProxyTest do
-  use Pleroma.Web.ConnCase, async: true
+  use Pleroma.Web.ConnCase
   import ExUnit.CaptureLog
   import Mox
   alias Pleroma.ReverseProxy
@@ -29,11 +29,11 @@ defmodule Pleroma.ReverseProxyTest do
          {"content-length", byte_size(json) |> to_string()}
        ], %{url: url}}
     end)
-    |> expect(:stream_body, invokes, fn %{url: url} ->
+    |> expect(:stream_body, invokes, fn %{url: url} = client ->
       case Registry.lookup(Pleroma.ReverseProxy.ClientMock, url) do
         [{_, 0}] ->
           Registry.update_value(Pleroma.ReverseProxy.ClientMock, url, &(&1 + 1))
-          {:ok, json}
+          {:ok, json, client}
 
         [{_, 1}] ->
           Registry.unregister(Pleroma.ReverseProxy.ClientMock, url)
@@ -78,7 +78,39 @@ defmodule Pleroma.ReverseProxyTest do
     assert conn.halted
   end
 
-  describe "max_body " do
+  defp stream_mock(invokes, with_close? \\ false) do
+    ClientMock
+    |> expect(:request, fn :get, "/stream-bytes/" <> length, _, _, _ ->
+      Registry.register(Pleroma.ReverseProxy.ClientMock, "/stream-bytes/" <> length, 0)
+
+      {:ok, 200, [{"content-type", "application/octet-stream"}],
+       %{url: "/stream-bytes/" <> length}}
+    end)
+    |> expect(:stream_body, invokes, fn %{url: "/stream-bytes/" <> length} = client ->
+      max = String.to_integer(length)
+
+      case Registry.lookup(Pleroma.ReverseProxy.ClientMock, "/stream-bytes/" <> length) do
+        [{_, current}] when current < max ->
+          Registry.update_value(
+            Pleroma.ReverseProxy.ClientMock,
+            "/stream-bytes/" <> length,
+            &(&1 + 10)
+          )
+
+          {:ok, "0123456789", client}
+
+        [{_, ^max}] ->
+          Registry.unregister(Pleroma.ReverseProxy.ClientMock, "/stream-bytes/" <> length)
+          :done
+      end
+    end)
+
+    if with_close? do
+      expect(ClientMock, :close, fn _ -> :ok end)
+    end
+  end
+
+  describe "max_body" do
     test "length returns error if content-length more than option", %{conn: conn} do
       user_agent_mock("hackney/1.15.1", 0)
 
@@ -94,38 +126,6 @@ defmodule Pleroma.ReverseProxyTest do
              end) == ""
     end
 
-    defp stream_mock(invokes, with_close? \\ false) do
-      ClientMock
-      |> expect(:request, fn :get, "/stream-bytes/" <> length, _, _, _ ->
-        Registry.register(Pleroma.ReverseProxy.ClientMock, "/stream-bytes/" <> length, 0)
-
-        {:ok, 200, [{"content-type", "application/octet-stream"}],
-         %{url: "/stream-bytes/" <> length}}
-      end)
-      |> expect(:stream_body, invokes, fn %{url: "/stream-bytes/" <> length} ->
-        max = String.to_integer(length)
-
-        case Registry.lookup(Pleroma.ReverseProxy.ClientMock, "/stream-bytes/" <> length) do
-          [{_, current}] when current < max ->
-            Registry.update_value(
-              Pleroma.ReverseProxy.ClientMock,
-              "/stream-bytes/" <> length,
-              &(&1 + 10)
-            )
-
-            {:ok, "0123456789"}
-
-          [{_, ^max}] ->
-            Registry.unregister(Pleroma.ReverseProxy.ClientMock, "/stream-bytes/" <> length)
-            :done
-        end
-      end)
-
-      if with_close? do
-        expect(ClientMock, :close, fn _ -> :ok end)
-      end
-    end
-
     test "max_body_length returns error if streaming body more than that option", %{conn: conn} do
       stream_mock(3, true)
 
@@ -223,12 +223,12 @@ defmodule Pleroma.ReverseProxyTest do
       Registry.register(Pleroma.ReverseProxy.ClientMock, "/headers", 0)
       {:ok, 200, [{"content-type", "application/json"}], %{url: "/headers", headers: headers}}
     end)
-    |> expect(:stream_body, 2, fn %{url: url, headers: headers} ->
+    |> expect(:stream_body, 2, fn %{url: url, headers: headers} = client ->
       case Registry.lookup(Pleroma.ReverseProxy.ClientMock, url) do
         [{_, 0}] ->
           Registry.update_value(Pleroma.ReverseProxy.ClientMock, url, &(&1 + 1))
           headers = for {k, v} <- headers, into: %{}, do: {String.capitalize(k), v}
-          {:ok, Jason.encode!(%{headers: headers})}
+          {:ok, Jason.encode!(%{headers: headers}), client}
 
         [{_, 1}] ->
           Registry.unregister(Pleroma.ReverseProxy.ClientMock, url)
@@ -305,11 +305,11 @@ defmodule Pleroma.ReverseProxyTest do
 
       {:ok, 200, headers, %{url: "/disposition"}}
     end)
-    |> expect(:stream_body, 2, fn %{url: "/disposition"} ->
+    |> expect(:stream_body, 2, fn %{url: "/disposition"} = client ->
       case Registry.lookup(Pleroma.ReverseProxy.ClientMock, "/disposition") do
         [{_, 0}] ->
           Registry.update_value(Pleroma.ReverseProxy.ClientMock, "/disposition", &(&1 + 1))
-          {:ok, ""}
+          {:ok, "", client}
 
         [{_, 1}] ->
           Registry.unregister(Pleroma.ReverseProxy.ClientMock, "/disposition")
@@ -341,4 +341,45 @@ defmodule Pleroma.ReverseProxyTest do
       assert {"content-disposition", "attachment; filename=\"filename.jpg\""} in conn.resp_headers
     end
   end
+
+  describe "tesla client using gun integration" do
+    @describetag :integration
+
+    clear_config([Pleroma.ReverseProxy.Client]) do
+      Pleroma.Config.put([Pleroma.ReverseProxy.Client], Pleroma.ReverseProxy.Client.Tesla)
+    end
+
+    clear_config([Pleroma.Gun.API]) do
+      Pleroma.Config.put([Pleroma.Gun.API], Pleroma.Gun)
+    end
+
+    setup 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)
+    end
+
+    test "common", %{conn: conn} do
+      conn = ReverseProxy.call(conn, "http://httpbin.org/stream-bytes/10")
+      assert byte_size(conn.resp_body) == 10
+      assert conn.state == :chunked
+      assert conn.status == 200
+    end
+
+    test "ssl", %{conn: conn} do
+      conn = ReverseProxy.call(conn, "https://httpbin.org/stream-bytes/10")
+      assert byte_size(conn.resp_body) == 10
+      assert conn.state == :chunked
+      assert conn.status == 200
+    end
+
+    test "follow redirects", %{conn: conn} do
+      conn = ReverseProxy.call(conn, "https://httpbin.org/redirect/5")
+      assert conn.state == :chunked
+      assert conn.status == 200
+    end
+  end
 end
index ba33413271277862951af05391008094896e83e9..5727871ea1c77c6d9ed44630606133e7e2dbcdc1 100644 (file)
@@ -107,7 +107,7 @@ defmodule HttpRequestMock do
         "https://osada.macgirvin.com/.well-known/webfinger?resource=acct:mike@osada.macgirvin.com",
         _,
         _,
-        Accept: "application/xrd+xml,application/jrd+json"
+        [{"accept", "application/xrd+xml,application/jrd+json"}]
       ) do
     {:ok,
      %Tesla.Env{
@@ -120,7 +120,7 @@ defmodule HttpRequestMock do
         "https://social.heldscal.la/.well-known/webfinger?resource=https://social.heldscal.la/user/29191",
         _,
         _,
-        Accept: "application/xrd+xml,application/jrd+json"
+        [{"accept", "application/xrd+xml,application/jrd+json"}]
       ) do
     {:ok,
      %Tesla.Env{
@@ -141,7 +141,7 @@ defmodule HttpRequestMock do
         "https://pawoo.net/.well-known/webfinger?resource=acct:https://pawoo.net/users/pekorino",
         _,
         _,
-        Accept: "application/xrd+xml,application/jrd+json"
+        [{"accept", "application/xrd+xml,application/jrd+json"}]
       ) do
     {:ok,
      %Tesla.Env{
@@ -167,7 +167,7 @@ defmodule HttpRequestMock do
         "https://social.stopwatchingus-heidelberg.de/.well-known/webfinger?resource=acct:https://social.stopwatchingus-heidelberg.de/user/18330",
         _,
         _,
-        Accept: "application/xrd+xml,application/jrd+json"
+        [{"accept", "application/xrd+xml,application/jrd+json"}]
       ) do
     {:ok,
      %Tesla.Env{
@@ -188,7 +188,7 @@ defmodule HttpRequestMock do
         "https://mamot.fr/.well-known/webfinger?resource=acct:https://mamot.fr/users/Skruyb",
         _,
         _,
-        Accept: "application/xrd+xml,application/jrd+json"
+        [{"accept", "application/xrd+xml,application/jrd+json"}]
       ) do
     {:ok,
      %Tesla.Env{
@@ -201,7 +201,7 @@ defmodule HttpRequestMock do
         "https://social.heldscal.la/.well-known/webfinger?resource=nonexistant@social.heldscal.la",
         _,
         _,
-        Accept: "application/xrd+xml,application/jrd+json"
+        [{"accept", "application/xrd+xml,application/jrd+json"}]
       ) do
     {:ok,
      %Tesla.Env{
@@ -214,7 +214,7 @@ defmodule HttpRequestMock do
         "https://squeet.me/xrd/?uri=lain@squeet.me",
         _,
         _,
-        Accept: "application/xrd+xml,application/jrd+json"
+        [{"accept", "application/xrd+xml,application/jrd+json"}]
       ) do
     {:ok,
      %Tesla.Env{
@@ -227,7 +227,7 @@ defmodule HttpRequestMock do
         "https://mst3k.interlinked.me/users/luciferMysticus",
         _,
         _,
-        Accept: "application/activity+json"
+        [{"accept", "application/activity+json"}]
       ) do
     {:ok,
      %Tesla.Env{
@@ -248,7 +248,7 @@ defmodule HttpRequestMock do
         "https://hubzilla.example.org/channel/kaniini",
         _,
         _,
-        Accept: "application/activity+json"
+        [{"accept", "application/activity+json"}]
       ) do
     {:ok,
      %Tesla.Env{
@@ -257,7 +257,7 @@ defmodule HttpRequestMock do
      }}
   end
 
-  def get("https://niu.moe/users/rye", _, _, Accept: "application/activity+json") do
+  def get("https://niu.moe/users/rye", _, _, [{"accept", "application/activity+json"}]) do
     {:ok,
      %Tesla.Env{
        status: 200,
@@ -265,7 +265,7 @@ defmodule HttpRequestMock do
      }}
   end
 
-  def get("https://n1u.moe/users/rye", _, _, Accept: "application/activity+json") do
+  def get("https://n1u.moe/users/rye", _, _, [{"accept", "application/activity+json"}]) do
     {:ok,
      %Tesla.Env{
        status: 200,
@@ -284,7 +284,7 @@ defmodule HttpRequestMock do
      }}
   end
 
-  def get("https://puckipedia.com/", _, _, Accept: "application/activity+json") do
+  def get("https://puckipedia.com/", _, _, [{"accept", "application/activity+json"}]) do
     {:ok,
      %Tesla.Env{
        status: 200,
@@ -308,9 +308,9 @@ defmodule HttpRequestMock do
      }}
   end
 
-  def get("https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39", _, _,
-        Accept: "application/activity+json"
-      ) do
+  def get("https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39", _, _, [
+        {"accept", "application/activity+json"}
+      ]) do
     {:ok,
      %Tesla.Env{
        status: 200,
@@ -318,7 +318,7 @@ defmodule HttpRequestMock do
      }}
   end
 
-  def get("https://mobilizon.org/@tcit", _, _, Accept: "application/activity+json") do
+  def get("https://mobilizon.org/@tcit", _, _, [{"accept", "application/activity+json"}]) do
     {:ok,
      %Tesla.Env{
        status: 200,
@@ -358,7 +358,7 @@ defmodule HttpRequestMock do
      }}
   end
 
-  def get("http://mastodon.example.org/users/admin", _, _, Accept: "application/activity+json") do
+  def get("http://mastodon.example.org/users/admin", _, _, _) do
     {:ok,
      %Tesla.Env{
        status: 200,
@@ -366,7 +366,9 @@ defmodule HttpRequestMock do
      }}
   end
 
-  def get("http://mastodon.example.org/users/relay", _, _, Accept: "application/activity+json") do
+  def get("http://mastodon.example.org/users/relay", _, _, [
+        {"accept", "application/activity+json"}
+      ]) do
     {:ok,
      %Tesla.Env{
        status: 200,
@@ -374,7 +376,9 @@ defmodule HttpRequestMock do
      }}
   end
 
-  def get("http://mastodon.example.org/users/gargron", _, _, Accept: "application/activity+json") do
+  def get("http://mastodon.example.org/users/gargron", _, _, [
+        {"accept", "application/activity+json"}
+      ]) do
     {:error, :nxdomain}
   end
 
@@ -557,7 +561,7 @@ defmodule HttpRequestMock do
         "http://mastodon.example.org/@admin/99541947525187367",
         _,
         _,
-        Accept: "application/activity+json"
+        _
       ) do
     {:ok,
      %Tesla.Env{
@@ -582,7 +586,7 @@ defmodule HttpRequestMock do
      }}
   end
 
-  def get("https://mstdn.io/users/mayuutann", _, _, Accept: "application/activity+json") do
+  def get("https://mstdn.io/users/mayuutann", _, _, [{"accept", "application/activity+json"}]) do
     {:ok,
      %Tesla.Env{
        status: 200,
@@ -594,7 +598,7 @@ defmodule HttpRequestMock do
         "https://mstdn.io/users/mayuutann/statuses/99568293732299394",
         _,
         _,
-        Accept: "application/activity+json"
+        [{"accept", "application/activity+json"}]
       ) do
     {:ok,
      %Tesla.Env{
@@ -614,7 +618,7 @@ defmodule HttpRequestMock do
      }}
   end
 
-  def get(url, _, _, Accept: "application/xrd+xml,application/jrd+json")
+  def get(url, _, _, [{"accept", "application/xrd+xml,application/jrd+json"}])
       when url in [
              "https://pleroma.soykaf.com/.well-known/webfinger?resource=acct:https://pleroma.soykaf.com/users/lain",
              "https://pleroma.soykaf.com/.well-known/webfinger?resource=https://pleroma.soykaf.com/users/lain"
@@ -641,7 +645,7 @@ defmodule HttpRequestMock do
         "https://shitposter.club/.well-known/webfinger?resource=https://shitposter.club/user/1",
         _,
         _,
-        Accept: "application/xrd+xml,application/jrd+json"
+        [{"accept", "application/xrd+xml,application/jrd+json"}]
       ) do
     {:ok,
      %Tesla.Env{
@@ -685,7 +689,7 @@ defmodule HttpRequestMock do
         "https://shitposter.club/.well-known/webfinger?resource=https://shitposter.club/user/5381",
         _,
         _,
-        Accept: "application/xrd+xml,application/jrd+json"
+        [{"accept", "application/xrd+xml,application/jrd+json"}]
       ) do
     {:ok,
      %Tesla.Env{
@@ -738,7 +742,7 @@ defmodule HttpRequestMock do
         "https://social.sakamoto.gq/.well-known/webfinger?resource=https://social.sakamoto.gq/users/eal",
         _,
         _,
-        Accept: "application/xrd+xml,application/jrd+json"
+        [{"accept", "application/xrd+xml,application/jrd+json"}]
       ) do
     {:ok,
      %Tesla.Env{
@@ -751,7 +755,7 @@ defmodule HttpRequestMock do
         "https://social.sakamoto.gq/objects/0ccc1a2c-66b0-4305-b23a-7f7f2b040056",
         _,
         _,
-        Accept: "application/atom+xml"
+        [{"accept", "application/atom+xml"}]
       ) do
     {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/sakamoto.atom")}}
   end
@@ -768,7 +772,7 @@ defmodule HttpRequestMock do
         "https://mastodon.social/.well-known/webfinger?resource=https://mastodon.social/users/lambadalambda",
         _,
         _,
-        Accept: "application/xrd+xml,application/jrd+json"
+        [{"accept", "application/xrd+xml,application/jrd+json"}]
       ) do
     {:ok,
      %Tesla.Env{
@@ -790,7 +794,7 @@ defmodule HttpRequestMock do
         "http://gs.example.org/.well-known/webfinger?resource=http://gs.example.org:4040/index.php/user/1",
         _,
         _,
-        Accept: "application/xrd+xml,application/jrd+json"
+        [{"accept", "application/xrd+xml,application/jrd+json"}]
       ) do
     {:ok,
      %Tesla.Env{
@@ -804,7 +808,7 @@ defmodule HttpRequestMock do
         "http://gs.example.org:4040/index.php/user/1",
         _,
         _,
-        Accept: "application/activity+json"
+        [{"accept", "application/activity+json"}]
       ) do
     {:ok, %Tesla.Env{status: 406, body: ""}}
   end
@@ -840,7 +844,7 @@ defmodule HttpRequestMock do
         "https://squeet.me/xrd?uri=lain@squeet.me",
         _,
         _,
-        Accept: "application/xrd+xml,application/jrd+json"
+        [{"accept", "application/xrd+xml,application/jrd+json"}]
       ) do
     {:ok,
      %Tesla.Env{
@@ -853,7 +857,7 @@ defmodule HttpRequestMock do
         "https://social.heldscal.la/.well-known/webfinger?resource=shp@social.heldscal.la",
         _,
         _,
-        Accept: "application/xrd+xml,application/jrd+json"
+        [{"accept", "application/xrd+xml,application/jrd+json"}]
       ) do
     {:ok,
      %Tesla.Env{
@@ -866,7 +870,7 @@ defmodule HttpRequestMock do
         "https://social.heldscal.la/.well-known/webfinger?resource=invalid_content@social.heldscal.la",
         _,
         _,
-        Accept: "application/xrd+xml,application/jrd+json"
+        [{"accept", "application/xrd+xml,application/jrd+json"}]
       ) do
     {:ok, %Tesla.Env{status: 200, body: ""}}
   end
@@ -883,7 +887,7 @@ defmodule HttpRequestMock do
         "http://framatube.org/main/xrd?uri=framasoft@framatube.org",
         _,
         _,
-        Accept: "application/xrd+xml,application/jrd+json"
+        [{"accept", "application/xrd+xml,application/jrd+json"}]
       ) do
     {:ok,
      %Tesla.Env{
@@ -905,7 +909,7 @@ defmodule HttpRequestMock do
         "http://gnusocial.de/main/xrd?uri=winterdienst@gnusocial.de",
         _,
         _,
-        Accept: "application/xrd+xml,application/jrd+json"
+        [{"accept", "application/xrd+xml,application/jrd+json"}]
       ) do
     {:ok,
      %Tesla.Env{
@@ -942,7 +946,7 @@ defmodule HttpRequestMock do
         "https://gerzilla.de/xrd/?uri=kaniini@gerzilla.de",
         _,
         _,
-        Accept: "application/xrd+xml,application/jrd+json"
+        [{"accept", "application/xrd+xml,application/jrd+json"}]
       ) do
     {:ok,
      %Tesla.Env{
@@ -1005,7 +1009,7 @@ defmodule HttpRequestMock do
      %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/osada-user-indio.json")}}
   end
 
-  def get("https://social.heldscal.la/user/23211", _, _, Accept: "application/activity+json") do
+  def get("https://social.heldscal.la/user/23211", _, _, [{"accept", "application/activity+json"}]) do
     {:ok, Tesla.Mock.json(%{"id" => "https://social.heldscal.la/user/23211"}, status: 200)}
   end
 
@@ -1138,7 +1142,7 @@ defmodule HttpRequestMock do
         "https://zetsubou.xn--q9jyb4c/.well-known/webfinger?resource=lain@zetsubou.xn--q9jyb4c",
         _,
         _,
-        Accept: "application/xrd+xml,application/jrd+json"
+        [{"accept", "application/xrd+xml,application/jrd+json"}]
       ) do
     {:ok,
      %Tesla.Env{
@@ -1151,7 +1155,7 @@ defmodule HttpRequestMock do
         "https://zetsubou.xn--q9jyb4c/.well-known/webfinger?resource=https://zetsubou.xn--q9jyb4c/users/lain",
         _,
         _,
-        Accept: "application/xrd+xml,application/jrd+json"
+        [{"accept", "application/xrd+xml,application/jrd+json"}]
       ) do
     {:ok,
      %Tesla.Env{
@@ -1173,7 +1177,9 @@ defmodule HttpRequestMock do
      }}
   end
 
-  def get("https://info.pleroma.site/activity.json", _, _, Accept: "application/activity+json") do
+  def get("https://info.pleroma.site/activity.json", _, _, [
+        {"accept", "application/activity+json"}
+      ]) do
     {:ok,
      %Tesla.Env{
        status: 200,
@@ -1185,7 +1191,9 @@ defmodule HttpRequestMock do
     {:ok, %Tesla.Env{status: 404, body: ""}}
   end
 
-  def get("https://info.pleroma.site/activity2.json", _, _, Accept: "application/activity+json") do
+  def get("https://info.pleroma.site/activity2.json", _, _, [
+        {"accept", "application/activity+json"}
+      ]) do
     {:ok,
      %Tesla.Env{
        status: 200,
@@ -1197,7 +1205,9 @@ defmodule HttpRequestMock do
     {:ok, %Tesla.Env{status: 404, body: ""}}
   end
 
-  def get("https://info.pleroma.site/activity3.json", _, _, Accept: "application/activity+json") do
+  def get("https://info.pleroma.site/activity3.json", _, _, [
+        {"accept", "application/activity+json"}
+      ]) do
     {:ok,
      %Tesla.Env{
        status: 200,
index 111e40361c5581913d3ee77bb2cdaf03fb1c80bf..671560e410dfeb88b2e8553359a2a4537f6592ee 100644 (file)
@@ -4,7 +4,6 @@
 
 defmodule Pleroma.UserInviteTokenTest do
   use ExUnit.Case, async: true
-  use Pleroma.DataCase
   alias Pleroma.UserInviteToken
 
   describe "valid_invite?/1 one time invites" do
@@ -64,7 +63,6 @@ defmodule Pleroma.UserInviteTokenTest do
 
     test "expires yesterday returns false", %{invite: invite} do
       invite = %{invite | expires_at: Date.add(Date.utc_today(), -1)}
-      invite = Repo.insert!(invite)
       refute UserInviteToken.valid_invite?(invite)
     end
   end
@@ -82,7 +80,6 @@ defmodule Pleroma.UserInviteTokenTest do
 
     test "overdue date and less uses returns false", %{invite: invite} do
       invite = %{invite | expires_at: Date.add(Date.utc_today(), -1)}
-      invite = Repo.insert!(invite)
       refute UserInviteToken.valid_invite?(invite)
     end
 
@@ -93,7 +90,6 @@ defmodule Pleroma.UserInviteTokenTest do
 
     test "overdue date with more uses returns false", %{invite: invite} do
       invite = %{invite | expires_at: Date.add(Date.utc_today(), -1), uses: 5}
-      invite = Repo.insert!(invite)
       refute UserInviteToken.valid_invite?(invite)
     end
   end
index 5fbdf96f6072af4238472ea128fb98e88a6d5bc3..02ffbfa0b056c3653a893f8bc2799846bf76bbda 100644 (file)
@@ -2439,7 +2439,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "value" => "Tesla.Adapter.Httpc",
                    "db" => [":adapter"]
                  }
-               ]
+               ],
+               "need_reboot" => true
              }
     end
 
@@ -2526,7 +2527,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                 %{"tuple" => [":seconds_valid", 60]},
                 %{"tuple" => [":path", ""]},
                 %{"tuple" => [":key1", nil]},
-                %{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]},
                 %{"tuple" => [":regex1", "~r/https:\/\/example.com/"]},
                 %{"tuple" => [":regex2", "~r/https:\/\/example.com/u"]},
                 %{"tuple" => [":regex3", "~r/https:\/\/example.com/i"]},
@@ -2556,7 +2556,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                      %{"tuple" => [":seconds_valid", 60]},
                      %{"tuple" => [":path", ""]},
                      %{"tuple" => [":key1", nil]},
-                     %{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]},
                      %{"tuple" => [":regex1", "~r/https:\\/\\/example.com/"]},
                      %{"tuple" => [":regex2", "~r/https:\\/\\/example.com/u"]},
                      %{"tuple" => [":regex3", "~r/https:\\/\\/example.com/i"]},
@@ -2569,7 +2568,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                      ":seconds_valid",
                      ":path",
                      ":key1",
-                     ":partial_chain",
                      ":regex1",
                      ":regex2",
                      ":regex3",
@@ -2583,7 +2581,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "value" => "Tesla.Adapter.Httpc",
                    "db" => [":adapter"]
                  }
-               ]
+               ],
+               "need_reboot" => true
              }
     end
 
index 848300ef37a2052ea0636105ca501f028b0bbd28..759501a677c739ee59dec11868abeb7af583ec51 100644 (file)
@@ -474,6 +474,13 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
       activity = insert(:note_activity, user: user, note: object)
       Pleroma.Repo.delete(object)
 
+      obj_url = activity.data["object"]
+
+      Tesla.Mock.mock(fn
+        %{method: :get, url: ^obj_url} ->
+          %Tesla.Env{status: 404, body: ""}
+      end)
+
       assert Utils.maybe_notify_mentioned_recipients(["test-test"], activity) == [
                "test-test"
              ]
index acae7a734deca67cb8d147f0c58e8d26feebb349..737976f1f635679655525a04eb5d71ff677c5245 100644 (file)
@@ -126,7 +126,7 @@ defmodule Pleroma.Web.Push.ImplTest do
     user = insert(:user, nickname: "Bob")
     other_user = insert(:user)
     {:ok, _, _, activity} = CommonAPI.follow(user, other_user)
-    object = Object.normalize(activity)
+    object = Object.normalize(activity, false)
 
     assert Impl.format_body(%{activity: activity}, user, object) == "@Bob has followed you"