From 514c899275a32e6ef63305f9424c50344d41b12e Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 11 Feb 2020 10:12:57 +0300 Subject: [PATCH 01/88] adding gun adapter --- CHANGELOG.md | 1 + config/config.exs | 67 +- config/description.exs | 2 +- config/test.exs | 2 + docs/API/admin_api.md | 2 + docs/configuration/cheatsheet.md | 36 +- lib/mix/tasks/pleroma/benchmark.ex | 39 + lib/mix/tasks/pleroma/emoji.ex | 9 +- lib/pleroma/application.ex | 90 +- lib/pleroma/config/config_db.ex | 11 - lib/pleroma/config/transfer_task.ex | 43 +- lib/pleroma/gun/api.ex | 26 + lib/pleroma/gun/api/mock.ex | 151 +++ lib/pleroma/gun/conn.ex | 29 + lib/pleroma/gun/gun.ex | 45 + lib/pleroma/http/adapter.ex | 64 ++ lib/pleroma/http/adapter/gun.ex | 123 +++ lib/pleroma/http/adapter/hackney.ex | 41 + lib/pleroma/http/connection.ex | 113 ++- lib/pleroma/http/http.ex | 156 ++- lib/pleroma/http/request.ex | 23 + lib/pleroma/http/request_builder.ex | 107 +- lib/pleroma/object/fetcher.ex | 6 +- lib/pleroma/otp_version.ex | 63 ++ lib/pleroma/pool/connections.ex | 415 ++++++++ lib/pleroma/pool/pool.ex | 22 + lib/pleroma/pool/request.ex | 72 ++ lib/pleroma/pool/supervisor.ex | 36 + lib/pleroma/reverse_proxy/client.ex | 26 +- lib/pleroma/reverse_proxy/client/hackney.ex | 24 + lib/pleroma/reverse_proxy/client/tesla.ex | 87 ++ lib/pleroma/reverse_proxy/reverse_proxy.ex | 20 +- .../mrf/media_proxy_warming_policy.ex | 14 +- lib/pleroma/web/rel_me.ex | 18 +- lib/pleroma/web/rich_media/parser.ex | 18 +- lib/pleroma/web/web_finger/web_finger.ex | 2 +- mix.exs | 4 + mix.lock | 2 + test/activity/ir/topics_test.exs | 2 +- test/config/config_db_test.exs | 8 - test/fixtures/warnings/otp_version/21.1 | 1 + test/fixtures/warnings/otp_version/22.1 | 1 + test/fixtures/warnings/otp_version/22.4 | 1 + test/fixtures/warnings/otp_version/23.0 | 1 + test/fixtures/warnings/otp_version/error | 1 + test/fixtures/warnings/otp_version/undefined | 1 + test/gun/gun_test.exs | 33 + test/http/adapter/gun_test.exs | 266 +++++ test/http/adapter/hackney_test.exs | 54 + test/http/adapter_test.exs | 65 ++ test/http/connection_test.exs | 142 +++ test/http/request_builder_test.exs | 30 +- test/http_test.exs | 35 +- test/notification_test.exs | 7 + test/otp_version_test.exs | 58 ++ test/pool/connections_test.exs | 959 ++++++++++++++++++ test/reverse_proxy/client/tesla_test.exs | 93 ++ .../reverse_proxy_test.exs | 121 ++- test/support/http_request_mock.ex | 94 +- test/user_invite_token_test.exs | 4 - .../admin_api/admin_api_controller_test.exs | 9 +- test/web/common_api/common_api_utils_test.exs | 7 + test/web/push/impl_test.exs | 2 +- 63 files changed, 3615 insertions(+), 389 deletions(-) create mode 100644 lib/pleroma/gun/api.ex create mode 100644 lib/pleroma/gun/api/mock.ex create mode 100644 lib/pleroma/gun/conn.ex create mode 100644 lib/pleroma/gun/gun.ex create mode 100644 lib/pleroma/http/adapter.ex create mode 100644 lib/pleroma/http/adapter/gun.ex create mode 100644 lib/pleroma/http/adapter/hackney.ex create mode 100644 lib/pleroma/http/request.ex create mode 100644 lib/pleroma/otp_version.ex create mode 100644 lib/pleroma/pool/connections.ex create mode 100644 lib/pleroma/pool/pool.ex create mode 100644 lib/pleroma/pool/request.ex create mode 100644 lib/pleroma/pool/supervisor.ex create mode 100644 lib/pleroma/reverse_proxy/client/hackney.ex create mode 100644 lib/pleroma/reverse_proxy/client/tesla.ex create mode 100644 test/fixtures/warnings/otp_version/21.1 create mode 100644 test/fixtures/warnings/otp_version/22.1 create mode 100644 test/fixtures/warnings/otp_version/22.4 create mode 100644 test/fixtures/warnings/otp_version/23.0 create mode 100644 test/fixtures/warnings/otp_version/error create mode 100644 test/fixtures/warnings/otp_version/undefined create mode 100644 test/gun/gun_test.exs create mode 100644 test/http/adapter/gun_test.exs create mode 100644 test/http/adapter/hackney_test.exs create mode 100644 test/http/adapter_test.exs create mode 100644 test/http/connection_test.exs create mode 100644 test/otp_version_test.exs create mode 100644 test/pool/connections_test.exs create mode 100644 test/reverse_proxy/client/tesla_test.exs rename test/{ => reverse_proxy}/reverse_proxy_test.exs (79%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e838983b..48080503a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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.
API Changes diff --git a/config/config.exs b/config/config.exs index ccc0c4e52..27091393b 100644 --- a/config/config.exs +++ b/config/config.exs @@ -58,20 +58,6 @@ 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 :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, 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" diff --git a/config/description.exs b/config/description.exs index efea7c137..d5322fa33 100644 --- a/config/description.exs +++ b/config/description.exs @@ -2728,7 +2728,7 @@ key: :adapter, type: :module, description: "Tesla adapter", - suggestions: [Tesla.Adapter.Hackney] + suggestions: [Tesla.Adapter.Hackney, Tesla.Adapter.Gun] } ] }, diff --git a/config/test.exs b/config/test.exs index 078c46205..83783cf8f 100644 --- a/config/test.exs +++ b/config/test.exs @@ -94,6 +94,8 @@ 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 diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md index fb6dfcb08..cd8123c5d 100644 --- a/docs/API/admin_api.md +++ b/docs/API/admin_api.md @@ -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` diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 2bd935983..1c67eca35 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -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 diff --git a/lib/mix/tasks/pleroma/benchmark.ex b/lib/mix/tasks/pleroma/benchmark.ex index 84dccf7f3..01e079136 100644 --- a/lib/mix/tasks/pleroma/benchmark.ex +++ b/lib/mix/tasks/pleroma/benchmark.ex @@ -74,4 +74,43 @@ def run(["render_timeline", nickname | _] = args) 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 diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex index 24d999707..b4e8d3a0b 100644 --- a/lib/mix/tasks/pleroma/emoji.ex +++ b/lib/mix/tasks/pleroma/emoji.ex @@ -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 @@ def run(["ls-packs" | args]) 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 @@ def run(["get-packs" | args]) 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}]: ")) diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 27758cf94..df6d3a98d 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -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 @@ def named_version, do: @name <> " " <> @version 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 @@ def user_agent 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 @@ def start(_type, _args) 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 @@ def start(_type, _args) 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 @@ def start(_type, _args) 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 @@ defp setup_instrumenters 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 @@ defp idempotency_expiration, 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 @@ defp build_cachex(type, opts), 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 @@ defp chat_child(_env, true) 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 @@ defp task_children(_) 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 diff --git a/lib/pleroma/config/config_db.ex b/lib/pleroma/config/config_db.ex index 119251bee..bdacefa97 100644 --- a/lib/pleroma/config/config_db.ex +++ b/lib/pleroma/config/config_db.ex @@ -278,8 +278,6 @@ defp do_convert({:proxy_url, {type, host, port}}) 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 @@ defp do_transform(%{"tuple" => [":proxy_url", %{"tuple" => [type, host, port]}]} {: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 diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex index 6c5ba1f95..251074aaa 100644 --- a/lib/pleroma/config/transfer_task.ex +++ b/lib/pleroma/config/transfer_task.ex @@ -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 @@ def load_and_update_env(deleted \\ [], restart_pleroma? \\ true) 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 @@ defp merge_and_update(setting) 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 index 000000000..a0c3c5415 --- /dev/null +++ b/lib/pleroma/gun/api.ex @@ -0,0 +1,26 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# 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 index 000000000..0134b016e --- /dev/null +++ b/lib/pleroma/gun/api/mock.ex @@ -0,0 +1,151 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# 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 index 000000000..2474829d6 --- /dev/null +++ b/lib/pleroma/gun/conn.ex @@ -0,0 +1,29 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# 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 index 000000000..4a1bbc95f --- /dev/null +++ b/lib/pleroma/gun/gun.ex @@ -0,0 +1,45 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# 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 index 000000000..6166a3eb4 --- /dev/null +++ b/lib/pleroma/http/adapter.ex @@ -0,0 +1,64 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# 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 index 000000000..f25afeda7 --- /dev/null +++ b/lib/pleroma/http/adapter/gun.ex @@ -0,0 +1,123 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# 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 index 000000000..00db30083 --- /dev/null +++ b/lib/pleroma/http/adapter/hackney.ex @@ -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 diff --git a/lib/pleroma/http/connection.ex b/lib/pleroma/http/connection.ex index 7e2c6f5e8..85918341a 100644 --- a/lib/pleroma/http/connection.ex +++ b/lib/pleroma/http/connection.ex @@ -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] + + require Logger + + alias Pleroma.Config + alias Pleroma.HTTP.Adapter @doc """ - Configure a client connection - - # Returns - - Tesla.Env.client + 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 - # 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) + defp pool_timeout(opts) do + timeout = + Config.get([:pools, opts[:pool], :timeout]) || Config.get([:pools, :default, :timeout]) - @hackney_options - |> Keyword.merge(adapter_options) - |> Keyword.merge(options) - |> Keyword.merge(proxy: proxy_url) + Keyword.merge(opts, timeout: timeout) + end + + @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 diff --git a/lib/pleroma/http/http.ex b/lib/pleroma/http/http.ex index dec24458a..ad47dc936 100644 --- a/lib/pleroma/http/http.ex +++ b/lib/pleroma/http/http.ex @@ -4,21 +4,47 @@ defmodule Pleroma.HTTP do @moduledoc """ - + Wrapper for `Tesla.request/2`. """ alias Pleroma.HTTP.Connection + alias Pleroma.HTTP.Request alias Pleroma.HTTP.RequestBuilder, as: Builder + alias Tesla.Client + alias Tesla.Env + + require Logger @type t :: __MODULE__ @doc """ - Builds and perform http request. + Performs GET request. + + See `Pleroma.HTTP.request/5` + """ + @spec get(Request.url() | nil, Request.headers(), keyword()) :: + nil | {:ok, Env.t()} | {:error, any()} + def get(url, headers \\ [], options \\ []) + def get(nil, _, _), do: nil + def get(url, headers, options), do: request(:get, url, "", headers, options) + + @doc """ + Performs POST request. + + See `Pleroma.HTTP.request/5` + """ + @spec post(Request.url(), String.t(), Request.headers(), keyword()) :: + {:ok, Env.t()} | {:error, any()} + def post(url, body, headers \\ [], options \\ []), + do: request(:post, url, body, headers, options) + + @doc """ + Builds and performs http request. # Arguments: `method` - :get, :post, :put, :delete - `url` - `body` + `url` - full url + `body` - request body `headers` - a keyworld list of headers, e.g. `[{"content-type", "text/plain"}]` `options` - custom, per-request middleware or adapter options @@ -26,23 +52,78 @@ 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) + :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} - params = Keyword.get(options, :params, []) + :exit, e -> + {:error, e} + end + end - %{} - |> 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)).() + @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} @@ -52,35 +133,16 @@ def request(method, url, body \\ "", headers \\ [], options \\ []) do 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 - end + 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 - def process_request_options(options) do - Keyword.merge(Pleroma.HTTP.Connection.hackney_options([]), options) - 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 index 000000000..891d88d53 --- /dev/null +++ b/lib/pleroma/http/request.ex @@ -0,0 +1,23 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HTTP.Request do + @moduledoc """ + Request struct. + """ + defstruct method: :get, url: "", query: [], headers: [], body: "", opts: [] + + @type method :: :head | :get | :delete | :trace | :options | :post | :put | :patch + @type url :: String.t() + @type headers :: [{String.t(), String.t()}] + + @type t :: %__MODULE__{ + method: method(), + url: url(), + query: keyword(), + headers: headers(), + body: String.t(), + opts: keyword() + } +end diff --git a/lib/pleroma/http/request_builder.ex b/lib/pleroma/http/request_builder.ex index e23457999..491acd0f9 100644 --- a/lib/pleroma/http/request_builder.ex +++ b/lib/pleroma/http/request_builder.ex @@ -7,77 +7,54 @@ defmodule Pleroma.HTTP.RequestBuilder do Helper functions for building Tesla requests """ + alias Pleroma.HTTP.Request + alias Tesla.Multipart + @doc """ - Specify the request method when building a request - - ## Parameters - - - request (Map) - Collected request options - - m (atom) - Request method - - ## Returns - - Map + 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 - - ## Parameters - - - request (Map) - Collected request options - - u (String) - Request URL - - ## Returns - - Map """ - @spec url(map(), String.t()) :: map() - def url(request, u) do - Map.put_new(request, :url, u) - end + @spec method(Request.t(), Request.method()) :: Request.t() + def method(request, m), do: %{request | method: m} + + @doc """ + Specify the request method when building a request + """ + @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 @@ def add_optional_params(request, definitions, [{key, value} | tail]) 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 diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index 037c42339..5e9bf1574 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -137,7 +137,7 @@ defp make_signature(id, date) do date: date }) - [{:Signature, signature}] + [{"signature", signature}] end defp sign_fetch(headers, id, date) do @@ -150,7 +150,7 @@ defp sign_fetch(headers, id, date) 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 @@ def fetch_and_contain_remote_object_from_id(id) when is_binary(id) 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 index 000000000..0be189304 --- /dev/null +++ b/lib/pleroma/otp_version.ex @@ -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 index 000000000..1ed16d1c1 --- /dev/null +++ b/lib/pleroma/pool/connections.ex @@ -0,0 +1,415 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# 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 index 000000000..a7ae64ce4 --- /dev/null +++ b/lib/pleroma/pool/pool.ex @@ -0,0 +1,22 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# 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 index 000000000..2c3574561 --- /dev/null +++ b/lib/pleroma/pool/request.ex @@ -0,0 +1,72 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# 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 index 000000000..32be2264d --- /dev/null +++ b/lib/pleroma/pool/supervisor.ex @@ -0,0 +1,36 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# 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 diff --git a/lib/pleroma/reverse_proxy/client.ex b/lib/pleroma/reverse_proxy/client.ex index 776c4794c..63261b94c 100644 --- a/lib/pleroma/reverse_proxy/client.ex +++ b/lib/pleroma/reverse_proxy/client.ex @@ -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 @@ def stream_body(ref), do: client().stream_body(ref) 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 index 000000000..e41560ab0 --- /dev/null +++ b/lib/pleroma/reverse_proxy/client/hackney.ex @@ -0,0 +1,24 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# 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 index 000000000..55a11b4a8 --- /dev/null +++ b/lib/pleroma/reverse_proxy/client/tesla.ex @@ -0,0 +1,87 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# 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 diff --git a/lib/pleroma/reverse_proxy/reverse_proxy.ex b/lib/pleroma/reverse_proxy/reverse_proxy.ex index 2ed719315..9f5710c92 100644 --- a/lib/pleroma/reverse_proxy/reverse_proxy.ex +++ b/lib/pleroma/reverse_proxy/reverse_proxy.ex @@ -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 @@ def call(conn = %{method: method}, url, opts) when method in @methods 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 @@ def call(conn, _, _) 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 @@ defp chunk_reply(conn, client, opts, sent_so_far, duration) 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 <- diff --git a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex index df774b0f7..ade87daf2 100644 --- a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex @@ -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 diff --git a/lib/pleroma/web/rel_me.ex b/lib/pleroma/web/rel_me.ex index 16b1a53d2..0ae926375 100644 --- a/lib/pleroma/web/rel_me.ex +++ b/lib/pleroma/web/rel_me.ex @@ -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 @@ def parse(url) when is_binary(url) 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 diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex index c06b0a0f2..9deb03845 100644 --- a/lib/pleroma/web/rich_media/parser.ex +++ b/lib/pleroma/web/rich_media/parser.ex @@ -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 @@ defp get_ttl_from_image(data, url) 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 diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex index b4cc80179..91e9e2271 100644 --- a/lib/pleroma/web/web_finger/web_finger.ex +++ b/lib/pleroma/web/web_finger/web_finger.ex @@ -205,7 +205,7 @@ def finger(account) 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 b28c65694..7c6de5423 100644 --- a/mix.exs +++ b/mix.exs @@ -120,6 +120,10 @@ defp deps 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"}, diff --git a/mix.lock b/mix.lock index 9c811a974..158a87e47 100644 --- 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"}, diff --git a/test/activity/ir/topics_test.exs b/test/activity/ir/topics_test.exs index e75f83586..8729e5746 100644 --- a/test/activity/ir/topics_test.exs +++ b/test/activity/ir/topics_test.exs @@ -83,7 +83,7 @@ test "converts tags to hash tags", %{activity: %{object: %{data: data} = object} 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]) diff --git a/test/config/config_db_test.exs b/test/config/config_db_test.exs index 812709fd8..394040a59 100644 --- a/test/config/config_db_test.exs +++ b/test/config/config_db_test.exs @@ -478,14 +478,6 @@ test "simple keyword" 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 index 000000000..90cd64c4f --- /dev/null +++ b/test/fixtures/warnings/otp_version/21.1 @@ -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 index 000000000..d9b314368 --- /dev/null +++ b/test/fixtures/warnings/otp_version/22.1 @@ -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 index 000000000..1da8ccd28 --- /dev/null +++ b/test/fixtures/warnings/otp_version/22.4 @@ -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 index 000000000..4266d8634 --- /dev/null +++ b/test/fixtures/warnings/otp_version/23.0 @@ -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 index 000000000..8fdd954df --- /dev/null +++ b/test/fixtures/warnings/otp_version/error @@ -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 index 000000000..66dc9051d --- /dev/null +++ b/test/fixtures/warnings/otp_version/undefined @@ -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 index 000000000..7f185617c --- /dev/null +++ b/test/gun/gun_test.exs @@ -0,0 +1,33 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# 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 index 000000000..37489e1a4 --- /dev/null +++ b/test/http/adapter/gun_test.exs @@ -0,0 +1,266 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# 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 index 000000000..35cb58125 --- /dev/null +++ b/test/http/adapter/hackney_test.exs @@ -0,0 +1,54 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# 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 index 000000000..37e47dabe --- /dev/null +++ b/test/http/adapter_test.exs @@ -0,0 +1,65 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# 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 index 000000000..c1ff0cc21 --- /dev/null +++ b/test/http/connection_test.exs @@ -0,0 +1,142 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# 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 diff --git a/test/http/request_builder_test.exs b/test/http/request_builder_test.exs index 80ef25d7b..27ca651be 100644 --- a/test/http/request_builder_test.exs +++ b/test/http/request_builder_test.exs @@ -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 @@ test "don't add if keyword is empty" do test "add query parameter" do assert RequestBuilder.add_optional_params( - %{}, + %Request{}, %{query: :query, body: :body, another: :val}, [ {:query, "param1=val1¶m2=val2"}, {:body, "some body"} ] - ) == %{query: "param1=val1¶m2=val2", body: "some body"} + ) == %Request{query: "param1=val1¶m2=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 @@ test "add file parameter" 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 @@ test "add key to body" do %Tesla.Multipart.Part{ body: "\"someval\"", dispositions: [name: "somekey"], - headers: ["Content-Type": "application/json"] + headers: [{"content-type", "application/json"}] } ] } diff --git a/test/http_test.exs b/test/http_test.exs index 5f9522cf0..d80b96496 100644 --- a/test/http_test.exs +++ b/test/http_test.exs @@ -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 @@ test "returns successfully result" 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 @@ test "returns successfully result for json content-type" 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 diff --git a/test/notification_test.exs b/test/notification_test.exs index 04bf5b41a..1de3c6e3b 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -649,6 +649,13 @@ test "notifications are deleted if a remote user is deleted" 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 index 000000000..f26b90f61 --- /dev/null +++ b/test/otp_version_test.exs @@ -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 index 000000000..6f0e041ae --- /dev/null +++ b/test/pool/connections_test.exs @@ -0,0 +1,959 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# 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 index 000000000..75a70988c --- /dev/null +++ b/test/reverse_proxy/client/tesla_test.exs @@ -0,0 +1,93 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# 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 diff --git a/test/reverse_proxy_test.exs b/test/reverse_proxy/reverse_proxy_test.exs similarity index 79% rename from test/reverse_proxy_test.exs rename to test/reverse_proxy/reverse_proxy_test.exs index 0672f57db..1ab3cc4bb 100644 --- a/test/reverse_proxy_test.exs +++ b/test/reverse_proxy/reverse_proxy_test.exs @@ -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 @@ defp user_agent_mock(user_agent, invokes) 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 @@ test "closed connection", %{conn: conn} 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 @@ test "length returns error if content-length more than option", %{conn: conn} 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 @@ defp headers_mock(_) 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 @@ defp disposition_headers_mock(headers) 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 @@ test "with content-disposition header", %{conn: conn} 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 diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index ba3341327..5727871ea 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -107,7 +107,7 @@ def get( "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 @@ def get( "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 @@ def get( "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 @@ def get( "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 @@ def get( "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 @@ def get( "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 @@ def get( "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 @@ def get( "https://mst3k.interlinked.me/users/luciferMysticus", _, _, - Accept: "application/activity+json" + [{"accept", "application/activity+json"}] ) do {:ok, %Tesla.Env{ @@ -248,7 +248,7 @@ def get( "https://hubzilla.example.org/channel/kaniini", _, _, - Accept: "application/activity+json" + [{"accept", "application/activity+json"}] ) do {:ok, %Tesla.Env{ @@ -257,7 +257,7 @@ def get( }} 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 @@ def get("https://niu.moe/users/rye", _, _, Accept: "application/activity+json") }} 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 @@ def get("http://mastodon.example.org/users/admin/statuses/100787282858396771", _ }} 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 @@ def get("https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3" }} 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 @@ def get("https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39", _, }} 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 @@ def get("https://wedistribute.org/wp-json/pterotype/v1/actor/-blog", _, _, _) 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 @@ def get("http://mastodon.example.org/users/admin", _, _, Accept: "application/ac }} 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 @@ def get("http://mastodon.example.org/users/relay", _, _, Accept: "application/ac }} 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 @@ def get( "http://mastodon.example.org/@admin/99541947525187367", _, _, - Accept: "application/activity+json" + _ ) do {:ok, %Tesla.Env{ @@ -582,7 +586,7 @@ def get("https://shitposter.club/notice/7369654", _, _, _) 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 @@ def get( "https://mstdn.io/users/mayuutann/statuses/99568293732299394", _, _, - Accept: "application/activity+json" + [{"accept", "application/activity+json"}] ) do {:ok, %Tesla.Env{ @@ -614,7 +618,7 @@ def get("https://pleroma.soykaf.com/users/lain/feed.atom", _, _, _) 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 @@ def get( "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 @@ def get( "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 @@ def get( "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 @@ def get( "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 @@ def get( "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 @@ def get( "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 @@ def get( "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 @@ def get( "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 @@ def get( "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 @@ def get( "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 @@ def get( "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 @@ def get( "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 @@ def get( "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 @@ def get("https://apfed.club/channel/indio", _, _, _) 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 @@ def get( "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 @@ def get( "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 @@ def get( }} 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 @@ def get("https://info.pleroma.site/activity.json", _, _, _) 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 @@ def get("https://info.pleroma.site/activity2.json", _, _, _) 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, diff --git a/test/user_invite_token_test.exs b/test/user_invite_token_test.exs index 111e40361..671560e41 100644 --- a/test/user_invite_token_test.exs +++ b/test/user_invite_token_test.exs @@ -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 @@ test "expires today returns true", %{invite: invite} 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 @@ test "not overdue date and less uses returns true", %{invite: invite} 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 @@ test "not overdue date with more uses returns false", %{invite: invite} 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 diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index 5fbdf96f6..02ffbfa0b 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -2439,7 +2439,8 @@ test "saving full setting if value is not keyword", %{conn: conn} do "value" => "Tesla.Adapter.Httpc", "db" => [":adapter"] } - ] + ], + "need_reboot" => true } end @@ -2526,7 +2527,6 @@ test "common config example", %{conn: conn} 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 @@ test "common config example", %{conn: conn} 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 @@ test "common config example", %{conn: conn} do ":seconds_valid", ":path", ":key1", - ":partial_chain", ":regex1", ":regex2", ":regex3", @@ -2583,7 +2581,8 @@ test "common config example", %{conn: conn} do "value" => "Tesla.Adapter.Httpc", "db" => [":adapter"] } - ] + ], + "need_reboot" => true } end diff --git a/test/web/common_api/common_api_utils_test.exs b/test/web/common_api/common_api_utils_test.exs index 848300ef3..759501a67 100644 --- a/test/web/common_api/common_api_utils_test.exs +++ b/test/web/common_api/common_api_utils_test.exs @@ -474,6 +474,13 @@ test "returns recipients when object not found" 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" ] diff --git a/test/web/push/impl_test.exs b/test/web/push/impl_test.exs index acae7a734..737976f1f 100644 --- a/test/web/push/impl_test.exs +++ b/test/web/push/impl_test.exs @@ -126,7 +126,7 @@ test "renders title and body for follow activity" 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" From 2a219f5e86bea076b1bc93f1a9205c764d43a380 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 18 Feb 2020 09:12:46 -0600 Subject: [PATCH 02/88] Improve changelog message --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48080503a..e4bce5c02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,7 +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. +- New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires minimum OTP version of 22.2 otherwise Pleroma won’t start. For hackney OTP update is not required.
API Changes From 7d73e7a09a72354acf526652e307149afbf5b1a3 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 18 Feb 2020 09:18:09 -0600 Subject: [PATCH 03/88] Spelling --- lib/pleroma/http/adapter/gun.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/http/adapter/gun.ex b/lib/pleroma/http/adapter/gun.ex index f25afeda7..ec6475e96 100644 --- a/lib/pleroma/http/adapter/gun.ex +++ b/lib/pleroma/http/adapter/gun.ex @@ -90,7 +90,7 @@ defp try_to_get_conn(uri, opts) 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." + "Gun connections pool checkin was not successful. Trying to open conn for next request." ) :ok = Connections.open_conn(uri, :gun_connections, opts) From 138a3c1fe48bbace79c0121d4571db3c2a827860 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 18 Feb 2020 09:30:18 -0600 Subject: [PATCH 04/88] Spelling was wrong in test as well --- test/http/adapter/gun_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/http/adapter/gun_test.exs b/test/http/adapter/gun_test.exs index 37489e1a4..1d7977c83 100644 --- a/test/http/adapter/gun_test.exs +++ b/test/http/adapter/gun_test.exs @@ -101,7 +101,7 @@ test "get conn on next request" do assert opts[:conn] == nil assert opts[:close_conn] == nil end) =~ - "Gun connections pool checkin was not succesfull. Trying to open conn for next request." + "Gun connections pool checkin was not successful. Trying to open conn for next request." opts = Gun.options(uri) From c9db0507f8d49aee9988b0b63477672f5df9c0b2 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Wed, 19 Feb 2020 12:19:03 +0300 Subject: [PATCH 05/88] removing retry option and changing some logger messages levels --- lib/pleroma/http/adapter/gun.ex | 28 +++++++++++++++++++++------- lib/pleroma/pool/connections.ex | 17 ++++++++--------- test/http/adapter/gun_test.exs | 2 +- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/lib/pleroma/http/adapter/gun.ex b/lib/pleroma/http/adapter/gun.ex index ec6475e96..f1018dd8d 100644 --- a/lib/pleroma/http/adapter/gun.ex +++ b/lib/pleroma/http/adapter/gun.ex @@ -15,7 +15,7 @@ defmodule Pleroma.HTTP.Adapter.Gun do connect_timeout: 20_000, domain_lookup_timeout: 5_000, tls_handshake_timeout: 5_000, - retry_timeout: 100, + retry: 0, await_up_timeout: 5_000 ] @@ -89,7 +89,7 @@ defp try_to_get_conn(uri, opts) do try do case Connections.checkin(uri, :gun_connections) do nil -> - Logger.info( + Logger.debug( "Gun connections pool checkin was not successful. Trying to open conn for next request." ) @@ -97,7 +97,9 @@ defp try_to_get_conn(uri, opts) do opts conn when is_pid(conn) -> - Logger.debug("received conn #{inspect(conn)} #{Connections.compose_uri(uri)}") + Logger.debug( + "received conn #{inspect(conn)} #{uri.scheme}://#{Connections.compose_uri(uri)}" + ) opts |> Keyword.put(:conn, conn) @@ -105,18 +107,30 @@ defp try_to_get_conn(uri, opts) do end rescue error -> - Logger.warn("Gun connections pool checkin caused error #{inspect(error)}") + Logger.warn( + "Gun connections pool checkin caused error #{uri.scheme}://#{ + Connections.compose_uri(uri) + } #{inspect(error)}" + ) + opts catch :exit, {:timeout, _} -> - Logger.info( - "Gun connections pool checkin with timeout error #{Connections.compose_uri(uri)}" + Logger.warn( + "Gun connections pool checkin with timeout error #{uri.scheme}://#{ + Connections.compose_uri(uri) + }" ) opts :exit, error -> - Logger.warn("Gun pool checkin exited with error #{inspect(error)}") + Logger.warn( + "Gun pool checkin exited with error #{uri.scheme}://#{Connections.compose_uri(uri)} #{ + inspect(error) + }" + ) + opts end end diff --git a/lib/pleroma/pool/connections.ex b/lib/pleroma/pool/connections.ex index 1ed16d1c1..c7136e0e0 100644 --- a/lib/pleroma/pool/connections.ex +++ b/lib/pleroma/pool/connections.ex @@ -52,8 +52,7 @@ def open_conn(%URI{} = uri, name, opts) do opts = opts |> Enum.into(%{}) - |> Map.put_new(:receive, false) - |> Map.put_new(:retry, pool_opts[:retry] || 5) + |> Map.put_new(:retry, pool_opts[:retry] || 0) |> Map.put_new(:retry_timeout, pool_opts[:retry_timeout] || 100) |> Map.put_new(:await_up_timeout, pool_opts[:await_up_timeout] || 5_000) @@ -108,11 +107,11 @@ def handle_cast({:checkout, conn_pid, pid}, state) do 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)}") + Logger.debug("checkout for closed conn #{inspect(conn_pid)}") state nil -> - Logger.info("checkout for alive conn #{inspect(conn_pid)}, but is not in state") + Logger.debug("checkout for alive conn #{inspect(conn_pid)}, but is not in state") state end @@ -172,15 +171,15 @@ def handle_info({:gun_up, conn_pid, _protocol}, state) do }) else :error_gun_info -> - Logger.warn(":gun.info caused error") + Logger.debug(":gun.info caused error") state false -> - Logger.warn(":gun_up message for closed conn #{inspect(conn_pid)}") + Logger.debug(":gun_up message for closed conn #{inspect(conn_pid)}") state nil -> - Logger.warn( + Logger.debug( ":gun_up message for alive conn #{inspect(conn_pid)}, but deleted from state" ) @@ -216,11 +215,11 @@ def handle_info({:gun_down, conn_pid, _protocol, _reason, _killed}, state) do 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)}") + Logger.debug(":gun_down message for closed conn #{inspect(conn_pid)}") state nil -> - Logger.warn( + Logger.debug( ":gun_down message for alive conn #{inspect(conn_pid)}, but deleted from state" ) diff --git a/test/http/adapter/gun_test.exs b/test/http/adapter/gun_test.exs index 1d7977c83..ef1b4a882 100644 --- a/test/http/adapter/gun_test.exs +++ b/test/http/adapter/gun_test.exs @@ -91,7 +91,7 @@ test "don't receive conn if receive_conn is false" do test "get conn on next request" do level = Application.get_env(:logger, :level) - Logger.configure(level: :info) + Logger.configure(level: :debug) on_exit(fn -> Logger.configure(level: level) end) uri = URI.parse("http://some-domain2.com") From 3849bbb60d9085bced717fef1f09216d570af287 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Fri, 21 Feb 2020 10:15:56 +0300 Subject: [PATCH 06/88] temp using tesla from fork --- mix.exs | 6 +++++- mix.lock | 46 +++++++++++++++++++++++----------------------- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/mix.exs b/mix.exs index 273307bbe..18e33b214 100644 --- a/mix.exs +++ b/mix.exs @@ -119,7 +119,11 @@ defp deps do {:calendar, "~> 0.17.4"}, {:cachex, "~> 3.0.2"}, {:poison, "~> 3.0", override: true}, - {:tesla, "~> 1.3", override: true}, + # {:tesla, "~> 1.3", override: true}, + {:tesla, + github: "alex-strizhakov/tesla", + ref: "922cc3db13b421763edbea76246b8ea61c38c6fa", + override: true}, {:castore, "~> 0.1"}, {:cowlib, "~> 2.8", override: true}, {:gun, diff --git a/mix.lock b/mix.lock index 12ce1afac..10b2fe30d 100644 --- a/mix.lock +++ b/mix.lock @@ -21,42 +21,42 @@ "crontab": {:hex, :crontab, "1.1.8", "2ce0e74777dfcadb28a1debbea707e58b879e6aa0ffbf9c9bb540887bce43617", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, "crypt": {:git, "https://github.com/msantos/crypt", "1f2b58927ab57e72910191a7ebaeff984382a1d3", [ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"]}, "custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"}, - "db_connection": {:hex, :db_connection, "2.2.1", "caee17725495f5129cb7faebde001dc4406796f12a62b8949f4ac69315080566", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"}, + "db_connection": {:hex, :db_connection, "2.2.1", "caee17725495f5129cb7faebde001dc4406796f12a62b8949f4ac69315080566", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "2b02ece62d9f983fcd40954e443b7d9e6589664380e5546b2b9b523cd0fb59e1"}, "decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, - "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm"}, - "ecto": {:hex, :ecto, "3.3.3", "0830bf3aebcbf3d8c1a1811cd581773b6866886c012f52c0f027031fa96a0b53", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, + "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"}, + "ecto": {:hex, :ecto, "3.3.3", "0830bf3aebcbf3d8c1a1811cd581773b6866886c012f52c0f027031fa96a0b53", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "12e368e3c2a2938d7776defaabdae40e82900fc4d8d66120ec1e01dfd8b93c3a"}, "ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"}, - "ecto_sql": {:hex, :ecto_sql, "3.3.4", "aa18af12eb875fbcda2f75e608b3bd534ebf020fc4f6448e4672fcdcbb081244", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4 or ~> 3.3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, - "esshd": {:hex, :esshd, "0.1.1", "d4dd4c46698093a40a56afecce8a46e246eb35463c457c246dacba2e056f31b5", [:mix], [], "hexpm"}, + "ecto_sql": {:hex, :ecto_sql, "3.3.4", "aa18af12eb875fbcda2f75e608b3bd534ebf020fc4f6448e4672fcdcbb081244", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4 or ~> 3.3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5eccbdbf92e3c6f213007a82d5dbba4cd9bb659d1a21331f89f408e4c0efd7a8"}, + "esshd": {:hex, :esshd, "0.1.1", "d4dd4c46698093a40a56afecce8a46e246eb35463c457c246dacba2e056f31b5", [:mix], [], "hexpm", "d73e341e3009d390aa36387dc8862860bf9f874c94d9fd92ade2926376f49981"}, "eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm", "b14f1dc204321429479c569cfbe8fb287541184ed040956c8862cb7a677b8406"}, "ex2ms": {:hex, :ex2ms, "1.5.0", "19e27f9212be9a96093fed8cdfbef0a2b56c21237196d26760f11dfcfae58e97", [:mix], [], "hexpm"}, "ex_aws": {:hex, :ex_aws, "2.1.1", "1e4de2106cfbf4e837de41be41cd15813eabc722315e388f0d6bb3732cec47cd", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "1.6.3 or 1.6.5 or 1.7.1 or 1.8.6 or ~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8", [hex: :jsx, repo: "hexpm", optional: true]}, {:poison, ">= 1.2.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "06b6fde12b33bb6d65d5d3493e903ba5a56d57a72350c15285a4298338089e10"}, "ex_aws_s3": {:hex, :ex_aws_s3, "2.0.2", "c0258bbdfea55de4f98f0b2f0ca61fe402cc696f573815134beb1866e778f47b", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "0569f5b211b1a3b12b705fe2a9d0e237eb1360b9d76298028df2346cad13097a"}, "ex_const": {:hex, :ex_const, "0.2.4", "d06e540c9d834865b012a17407761455efa71d0ce91e5831e86881b9c9d82448", [:mix], [], "hexpm", "96fd346610cc992b8f896ed26a98be82ac4efb065a0578f334a32d60a3ba9767"}, - "ex_doc": {:hex, :ex_doc, "0.21.3", "857ec876b35a587c5d9148a2512e952e24c24345552259464b98bfbb883c7b42", [:mix], [{:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, + "ex_doc": {:hex, :ex_doc, "0.21.3", "857ec876b35a587c5d9148a2512e952e24c24345552259464b98bfbb883c7b42", [:mix], [{:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "0db1ee8d1547ab4877c5b5dffc6604ef9454e189928d5ba8967d4a58a801f161"}, "ex_machina": {:hex, :ex_machina, "2.3.0", "92a5ad0a8b10ea6314b876a99c8c9e3f25f4dde71a2a835845b136b9adaf199a", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "b84f6af156264530b312a8ab98ac6088f6b77ae5fe2058305c81434aa01fbaf9"}, - "ex_syslogger": {:hex, :ex_syslogger, "1.5.0", "bc936ee3fd13d9e592cb4c3a1e8a55fccd33b05e3aa7b185f211f3ed263ff8f0", [:mix], [{:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:syslog, "~> 1.0.5", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm"}, - "excoveralls": {:hex, :excoveralls, "0.12.2", "a513defac45c59e310ac42fcf2b8ae96f1f85746410f30b1ff2b710a4b6cd44b", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, + "ex_syslogger": {:hex, :ex_syslogger, "1.5.0", "bc936ee3fd13d9e592cb4c3a1e8a55fccd33b05e3aa7b185f211f3ed263ff8f0", [:mix], [{:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:syslog, "~> 1.0.5", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "f3b4b184dcdd5f356b7c26c6cd72ab0918ba9dfb4061ccfaf519e562942af87b"}, + "excoveralls": {:hex, :excoveralls, "0.12.2", "a513defac45c59e310ac42fcf2b8ae96f1f85746410f30b1ff2b710a4b6cd44b", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "151c476331d49b45601ffc45f43cb3a8beb396b02a34e3777fea0ad34ae57d89"}, "fast_html": {:hex, :fast_html, "1.0.3", "2cc0d4b68496266a1530e0c852cafeaede0bd10cfdee26fda50dc696c203162f", [:make, :mix], [], "hexpm", "ab3d782b639d3c4655fbaec0f9d032c91f8cab8dd791ac7469c2381bc7c32f85"}, "fast_sanitize": {:hex, :fast_sanitize, "0.1.7", "2a7cd8734c88a2de6de55022104f8a3b87f1fdbe8bbf131d9049764b53d50d0d", [:mix], [{:fast_html, "~> 1.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f39fe8ea08fbac17487c30bf09b7d9f3e12472e51fb07a88ffeb8fd17da8ab67"}, "flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "31fc8090fde1acd267c07c36ea7365b8604055f897d3a53dd967658c691bd827"}, - "floki": {:hex, :floki, "0.25.0", "b1c9ddf5f32a3a90b43b76f3386ca054325dc2478af020e87b5111c19f2284ac", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm"}, + "floki": {:hex, :floki, "0.25.0", "b1c9ddf5f32a3a90b43b76f3386ca054325dc2478af020e87b5111c19f2284ac", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "631f4e627c46d5ecd347df5a2accdaf0621c77c3693c5b75a8ad58e84c61f242"}, "gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"}, "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"}, + "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"}, "http_signatures": {:git, "https://git.pleroma.social/pleroma/http_signatures.git", "293d77bb6f4a67ac8bde1428735c3b42f22cbb30", [ref: "293d77bb6f4a67ac8bde1428735c3b42f22cbb30"]}, - "httpoison": {:hex, :httpoison, "1.6.2", "ace7c8d3a361cebccbed19c283c349b3d26991eff73a1eaaa8abae2e3c8089b6", [:mix], [{:hackney, "~> 1.15 and >= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, + "httpoison": {:hex, :httpoison, "1.6.2", "ace7c8d3a361cebccbed19c283c349b3d26991eff73a1eaaa8abae2e3c8089b6", [:mix], [{:hackney, "~> 1.15 and >= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "aa2c74bd271af34239a3948779612f87df2422c2fdcfdbcec28d9c105f0773fe"}, "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"}, "inet_cidr": {:hex, :inet_cidr, "1.0.4", "a05744ab7c221ca8e395c926c3919a821eb512e8f36547c062f62c4ca0cf3d6e", [:mix], [], "hexpm", "64a2d30189704ae41ca7dbdd587f5291db5d1dda1414e0774c29ffc81088c1bc"}, "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fdf843bca858203ae1de16da2ee206f53416bbda5dc8c9e78f43243de4bc3afe"}, - "joken": {:hex, :joken, "2.2.0", "2daa1b12be05184aff7b5ace1d43ca1f81345962285fff3f88db74927c954d3a", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm"}, - "jose": {:hex, :jose, "1.10.1", "16d8e460dae7203c6d1efa3f277e25b5af8b659febfc2f2eb4bacf87f128b80a", [:mix, :rebar3], [], "hexpm"}, + "joken": {:hex, :joken, "2.2.0", "2daa1b12be05184aff7b5ace1d43ca1f81345962285fff3f88db74927c954d3a", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "b4f92e30388206f869dd25d1af628a1d99d7586e5cf0672f64d4df84c4d2f5e9"}, + "jose": {:hex, :jose, "1.10.1", "16d8e460dae7203c6d1efa3f277e25b5af8b659febfc2f2eb4bacf87f128b80a", [:mix, :rebar3], [], "hexpm", "3c7ddc8a9394b92891db7c2771da94bf819834a1a4c92e30857b7d582e2f8257"}, "libring": {:hex, :libring, "1.4.0", "41246ba2f3fbc76b3971f6bce83119dfec1eee17e977a48d8a9cfaaf58c2a8d6", [:mix], [], "hexpm"}, "makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "a10c6eb62cca416019663129699769f0c2ccf39428b3bb3c0cb38c718a0c186d"}, "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"}, @@ -69,38 +69,38 @@ "mogrify": {:hex, :mogrify, "0.6.1", "de1b527514f2d95a7bbe9642eb556061afb337e220cf97adbf3a4e6438ed70af", [:mix], [], "hexpm", "3bc928d817974fa10cc11e6c89b9a9361e37e96dbbf3d868c41094ec05745dcd"}, "mox": {:hex, :mox, "0.5.1", "f86bb36026aac1e6f924a4b6d024b05e9adbed5c63e8daa069bd66fb3292165b", [:mix], [], "hexpm", "052346cf322311c49a0f22789f3698eea030eec09b8c47367f0686ef2634ae14"}, "myhtmlex": {:git, "https://git.pleroma.social/pleroma/myhtmlex.git", "ad0097e2f61d4953bfef20fb6abddf23b87111e6", [ref: "ad0097e2f61d4953bfef20fb6abddf23b87111e6", submodules: true]}, - "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm"}, + "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"}, "nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]}, "oban": {:hex, :oban, "0.12.1", "695e9490c6e0edfca616d80639528e448bd29b3bff7b7dd10a56c79b00a5d7fb", [:mix], [{:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c1d58d69b8b5a86e7167abbb8cc92764a66f25f12f6172052595067fc6a30a17"}, "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.4", "8dd29ed783f2e12195d7e0a4640effc0a7c37e6537da491f1db01839eee6d053", [:mix], [], "hexpm", "595d09db74cb093b1903381c9de423276a931a2480a46a1a5dc7f932a2a6375b"}, - "phoenix": {:hex, :phoenix, "1.4.13", "67271ad69b51f3719354604f4a3f968f83aa61c19199343656c9caee057ff3b8", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, - "phoenix_ecto": {:hex, :phoenix_ecto, "4.1.0", "a044d0756d0464c5a541b4a0bf4bcaf89bffcaf92468862408290682c73ae50d", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, - "phoenix_html": {:hex, :phoenix_html, "2.14.0", "d8c6bc28acc8e65f8ea0080ee05aa13d912c8758699283b8d3427b655aabe284", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, + "phoenix": {:hex, :phoenix, "1.4.13", "67271ad69b51f3719354604f4a3f968f83aa61c19199343656c9caee057ff3b8", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ab765a0feddb81fc62e2116c827b5f068df85159c162bee760745276ad7ddc1b"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.1.0", "a044d0756d0464c5a541b4a0bf4bcaf89bffcaf92468862408290682c73ae50d", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "c5e666a341ff104d0399d8f0e4ff094559b2fde13a5985d4cb5023b2c2ac558b"}, + "phoenix_html": {:hex, :phoenix_html, "2.14.0", "d8c6bc28acc8e65f8ea0080ee05aa13d912c8758699283b8d3427b655aabe284", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "b0bb30eda478a06dbfbe96728061a93833db3861a49ccb516f839ecb08493fbb"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm", "1f13f9f0f3e769a667a6b6828d29dec37497a082d195cc52dbef401a9b69bf38"}, "phoenix_swoosh": {:hex, :phoenix_swoosh, "0.2.0", "a7e0b32077cd6d2323ae15198839b05d9caddfa20663fd85787479e81f89520e", [:mix], [{:phoenix, "~> 1.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.2", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:swoosh, "~> 0.1", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "ebf1bfa7b3c1c850c04929afe02e2e0d7ab135e0706332c865de03e761676b1f"}, "plug": {:hex, :plug, "1.9.0", "8d7c4e26962283ff9f8f3347bd73838e2413fbc38b7bb5467d5924f68f3a5a4a", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "9902eda2c52ada2a096434682e99a2493f5d06a94d6ac6bcfff9805f952350f1"}, - "plug_cowboy": {:hex, :plug_cowboy, "2.1.2", "8b0addb5908c5238fac38e442e81b6fcd32788eaa03246b4d55d147c47c5805e", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, - "plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.1.2", "8b0addb5908c5238fac38e442e81b6fcd32788eaa03246b4d55d147c47c5805e", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "7d722581ce865a237e14da6d946f92704101740a256bd13ec91e63c0b122fc70"}, + "plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"}, "plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "79fd4fcf34d110605c26560cbae8f23c603ec4158c08298bd4360fdea90bb5cf"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, "postgrex": {:hex, :postgrex, "0.15.3", "5806baa8a19a68c4d07c7a624ccdb9b57e89cbc573f1b98099e3741214746ae4", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "4737ce62a31747b4c63c12b20c62307e51bb4fcd730ca0c32c280991e0606c90"}, - "prometheus": {:hex, :prometheus, "4.5.0", "8f4a2246fe0beb50af0f77c5e0a5bb78fe575c34a9655d7f8bc743aad1c6bf76", [:mix, :rebar3], [], "hexpm"}, + "prometheus": {:hex, :prometheus, "4.5.0", "8f4a2246fe0beb50af0f77c5e0a5bb78fe575c34a9655d7f8bc743aad1c6bf76", [:mix, :rebar3], [], "hexpm", "679b5215480fff612b8351f45c839d995a07ce403e42ff02f1c6b20960d41a4e"}, "prometheus_ecto": {:hex, :prometheus_ecto, "1.4.3", "3dd4da1812b8e0dbee81ea58bb3b62ed7588f2eae0c9e97e434c46807ff82311", [:mix], [{:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm", "8d66289f77f913b37eda81fd287340c17e61a447549deb28efc254532b2bed82"}, "prometheus_ex": {:hex, :prometheus_ex, "3.0.5", "fa58cfd983487fc5ead331e9a3e0aa622c67232b3ec71710ced122c4c453a02f", [:mix], [{:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm", "9fd13404a48437e044b288b41f76e64acd9735fb8b0e3809f494811dfa66d0fb"}, "prometheus_phoenix": {:hex, :prometheus_phoenix, "1.3.0", "c4b527e0b3a9ef1af26bdcfbfad3998f37795b9185d475ca610fe4388fdd3bb5", [:mix], [{:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.3 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm", "c4d1404ac4e9d3d963da601db2a7d8ea31194f0017057fabf0cfb9bf5a6c8c75"}, "prometheus_plugs": {:hex, :prometheus_plugs, "1.1.5", "25933d48f8af3a5941dd7b621c889749894d8a1082a6ff7c67cc99dec26377c5", [:mix], [{:accept, "~> 0.1", [hex: :accept, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}, {:prometheus_process_collector, "~> 1.1", [hex: :prometheus_process_collector, repo: "hexpm", optional: true]}], "hexpm", "0273a6483ccb936d79ca19b0ab629aef0dba958697c94782bb728b920dfc6a79"}, "quack": {:hex, :quack, "0.1.1", "cca7b4da1a233757fdb44b3334fce80c94785b3ad5a602053b7a002b5a8967bf", [:mix], [{:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: false]}, {:tesla, "~> 1.2.0", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "d736bfa7444112eb840027bb887832a0e403a4a3437f48028c3b29a2dbbd2543"}, "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"}, - "recon": {:hex, :recon, "2.5.0", "2f7fcbec2c35034bade2f9717f77059dc54eb4e929a3049ca7ba6775c0bd66cd", [:mix, :rebar3], [], "hexpm"}, + "recon": {:hex, :recon, "2.5.0", "2f7fcbec2c35034bade2f9717f77059dc54eb4e929a3049ca7ba6775c0bd66cd", [:mix, :rebar3], [], "hexpm", "72f3840fedd94f06315c523f6cecf5b4827233bed7ae3fe135b2a0ebeab5e196"}, "remote_ip": {:git, "https://git.pleroma.social/pleroma/remote_ip.git", "825dc00aaba5a1b7c4202a532b696b595dd3bcb3", [ref: "825dc00aaba5a1b7c4202a532b696b595dd3bcb3"]}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"}, "sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm", "2e1ec458f892ffa81f9f8386e3f35a1af6db7a7a37748a64478f13163a1f3573"}, "swoosh": {:hex, :swoosh, "0.23.5", "bfd9404bbf5069b1be2ffd317923ce57e58b332e25dbca2a35dedd7820dfee5a", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "e3928e1d2889a308aaf3e42755809ac21cffd77cb58eef01cbfdab4ce2fd1e21"}, - "syslog": {:hex, :syslog, "1.0.6", "995970c9aa7feb380ac493302138e308d6e04fd57da95b439a6df5bb3bf75076", [:rebar3], [], "hexpm"}, + "syslog": {:hex, :syslog, "1.0.6", "995970c9aa7feb380ac493302138e308d6e04fd57da95b439a6df5bb3bf75076", [:rebar3], [], "hexpm", "769ddfabd0d2a16f3f9c17eb7509951e0ca4f68363fb26f2ee51a8ec4a49881a"}, "telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"}, - "tesla": {:hex, :tesla, "1.3.2", "deb92c5c9ce35e747a395ba413ca78593a4f75bf0e1545630ee2e3d34264021e", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.3", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"}, + "tesla": {:git, "https://github.com/alex-strizhakov/tesla.git", "922cc3db13b421763edbea76246b8ea61c38c6fa", [ref: "922cc3db13b421763edbea76246b8ea61c38c6fa"]}, "timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "f354efb2400dd7a80fd9eb6c8419068c4f632da4ac47f3d8822d6e33f08bc852"}, "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"}, "tzdata": {:hex, :tzdata, "0.5.22", "f2ba9105117ee0360eae2eca389783ef7db36d533899b2e84559404dbc77ebb8", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "cd66c8a1e6a9e121d1f538b01bef459334bb4029a1ffb4eeeb5e4eae0337e7b6"}, From a03c420b84d9901be70520d8c027ccb53449990d Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Fri, 21 Feb 2020 12:32:42 +0300 Subject: [PATCH 07/88] by default don't use gun retries remove conn depends on retry setting from config --- config/config.exs | 2 +- lib/pleroma/pool/connections.ex | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/config/config.exs b/config/config.exs index 853a53fc9..7f3a4d1b6 100644 --- a/config/config.exs +++ b/config/config.exs @@ -599,7 +599,7 @@ config :pleroma, :connections_pool, receive_connection_timeout: 250, max_connections: 250, - retry: 5, + retry: 0, retry_timeout: 100, await_up_timeout: 5_000 diff --git a/lib/pleroma/pool/connections.ex b/lib/pleroma/pool/connections.ex index c7136e0e0..d20927580 100644 --- a/lib/pleroma/pool/connections.ex +++ b/lib/pleroma/pool/connections.ex @@ -5,6 +5,8 @@ defmodule Pleroma.Pool.Connections do use GenServer + alias Pleroma.Config + require Logger @type domain :: String.t() @@ -33,7 +35,7 @@ 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) + timeout = Config.get([:connections_pool, :receive_connection_timeout], 250) GenServer.call( name, @@ -47,7 +49,7 @@ 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], []) + pool_opts = Config.get([:connections_pool], []) opts = opts @@ -193,12 +195,13 @@ def handle_info({:gun_up, conn_pid, _protocol}, state) do @impl true def handle_info({:gun_down, conn_pid, _protocol, _reason, _killed}, state) do + retries = Config.get([:connections_pool, :retry], 0) # 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)}") + if conn.retries == retries do + Logger.debug("closing conn if retries is eq #{inspect(conn_pid)}") :ok = API.close(conn.conn) put_in( From ad8f26c0a4a0a579e93547e78313d3e4ecef6ed5 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Fri, 21 Feb 2020 12:53:40 +0300 Subject: [PATCH 08/88] more info in Connections.checkin timout errors --- lib/pleroma/http/adapter/gun.ex | 13 +++++++++---- test/http_test.exs | 4 ++-- test/pool/connections_test.exs | 8 ++++++-- test/reverse_proxy/client/tesla_test.exs | 4 ++-- test/reverse_proxy/reverse_proxy_test.exs | 8 ++++---- 5 files changed, 23 insertions(+), 14 deletions(-) diff --git a/lib/pleroma/http/adapter/gun.ex b/lib/pleroma/http/adapter/gun.ex index f1018dd8d..fc40b324a 100644 --- a/lib/pleroma/http/adapter/gun.ex +++ b/lib/pleroma/http/adapter/gun.ex @@ -115,11 +115,16 @@ defp try_to_get_conn(uri, opts) do opts catch - :exit, {:timeout, _} -> + :exit, {:timeout, {_, operation, [_, {method, _}, _]}} -> + messages_len = + :gun_connections + |> Process.whereis() + |> Process.info(:message_queue_len) + Logger.warn( - "Gun connections pool checkin with timeout error #{uri.scheme}://#{ - Connections.compose_uri(uri) - }" + "Gun connections pool checkin with timeout error for #{operation} #{method} #{ + uri.scheme + }://#{Connections.compose_uri(uri)}. Messages length: #{messages_len}" ) opts diff --git a/test/http_test.exs b/test/http_test.exs index d80b96496..83c27f6e1 100644 --- a/test/http_test.exs +++ b/test/http_test.exs @@ -61,8 +61,8 @@ test "returns successfully result" do describe "connection pools" do @describetag :integration - clear_config([Pleroma.Gun.API]) do - Pleroma.Config.put([Pleroma.Gun.API], Pleroma.Gun) + clear_config(Pleroma.Gun.API) do + Pleroma.Config.put(Pleroma.Gun.API, Pleroma.Gun) end test "gun" do diff --git a/test/pool/connections_test.exs b/test/pool/connections_test.exs index 6f0e041ae..d0d711c55 100644 --- a/test/pool/connections_test.exs +++ b/test/pool/connections_test.exs @@ -15,6 +15,10 @@ defmodule Pleroma.Pool.ConnectionsTest do :ok end + clear_config([:connections_pool, :retry]) do + Pleroma.Config.put([:connections_pool, :retry], 5) + end + setup do name = :test_connections adapter = Application.get_env(:tesla, :adapter) @@ -429,8 +433,8 @@ test "remove frequently used and idle", %{name: name} do describe "integration test" do @describetag :integration - clear_config([API]) do - Pleroma.Config.put([API], Pleroma.Gun) + clear_config(API) do + Pleroma.Config.put(API, Pleroma.Gun) end test "opens connection and reuse it on next request", %{name: name} do diff --git a/test/reverse_proxy/client/tesla_test.exs b/test/reverse_proxy/client/tesla_test.exs index 75a70988c..231271b0d 100644 --- a/test/reverse_proxy/client/tesla_test.exs +++ b/test/reverse_proxy/client/tesla_test.exs @@ -8,8 +8,8 @@ defmodule Pleroma.ReverseProxy.Client.TeslaTest do alias Pleroma.ReverseProxy.Client @moduletag :integration - clear_config_all([Pleroma.Gun.API]) do - Pleroma.Config.put([Pleroma.Gun.API], Pleroma.Gun) + clear_config_all(Pleroma.Gun.API) do + Pleroma.Config.put(Pleroma.Gun.API, Pleroma.Gun) end setup do diff --git a/test/reverse_proxy/reverse_proxy_test.exs b/test/reverse_proxy/reverse_proxy_test.exs index 1ab3cc4bb..f61fc02c5 100644 --- a/test/reverse_proxy/reverse_proxy_test.exs +++ b/test/reverse_proxy/reverse_proxy_test.exs @@ -345,12 +345,12 @@ test "with content-disposition header", %{conn: conn} do 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) + 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) + clear_config(Pleroma.Gun.API) do + Pleroma.Config.put(Pleroma.Gun.API, Pleroma.Gun) end setup do From 6806df80ddb1e52aef2b89b923d9a3e2844b5aeb Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Fri, 21 Feb 2020 14:28:16 +0300 Subject: [PATCH 09/88] don't log info ssl messages --- lib/pleroma/http/adapter/gun.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/http/adapter/gun.ex b/lib/pleroma/http/adapter/gun.ex index fc40b324a..0a6872ad6 100644 --- a/lib/pleroma/http/adapter/gun.ex +++ b/lib/pleroma/http/adapter/gun.ex @@ -58,7 +58,8 @@ defp add_scheme_opts(opts, %URI{scheme: "https", host: host, port: port}) do depth: 20, reuse_sessions: false, verify_fun: - {&:ssl_verify_hostname.verify_fun/3, [check_hostname: Adapter.domain_or_fallback(host)]} + {&:ssl_verify_hostname.verify_fun/3, [check_hostname: Adapter.domain_or_fallback(host)]}, + log_level: :warning ] ] From f604f9e47061b9d47c1bb62cc7aaf44fabdf69b3 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Fri, 21 Feb 2020 14:33:55 +0300 Subject: [PATCH 10/88] hackney pool timeout --- lib/pleroma/http/connection.ex | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/http/connection.ex b/lib/pleroma/http/connection.ex index 85918341a..e2d7afbbd 100644 --- a/lib/pleroma/http/connection.ex +++ b/lib/pleroma/http/connection.ex @@ -33,8 +33,14 @@ def options(%URI{} = uri, opts \\ []) do end defp pool_timeout(opts) do - timeout = - Config.get([:pools, opts[:pool], :timeout]) || Config.get([:pools, :default, :timeout]) + {config_key, default} = + if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Gun do + {:pools, Config.get([:pools, :default, :timeout])} + else + {:hackney_pools, 10_000} + end + + timeout = Config.get([config_key, opts[:pool], :timeout], default) Keyword.merge(opts, timeout: timeout) end From d44f9e3b6cfd5a0dae07f6194bfd05360afd6560 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Fri, 21 Feb 2020 16:56:55 +0300 Subject: [PATCH 11/88] fix for timeout clause --- lib/pleroma/http/adapter/gun.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/http/adapter/gun.ex b/lib/pleroma/http/adapter/gun.ex index 0a6872ad6..7b7e38d8c 100644 --- a/lib/pleroma/http/adapter/gun.ex +++ b/lib/pleroma/http/adapter/gun.ex @@ -117,7 +117,7 @@ defp try_to_get_conn(uri, opts) do opts catch :exit, {:timeout, {_, operation, [_, {method, _}, _]}} -> - messages_len = + {:message_queue_len, messages_len} = :gun_connections |> Process.whereis() |> Process.info(:message_queue_len) From 8efae966b1e87fe448a13d04eae0898c4a102c29 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Mon, 24 Feb 2020 19:56:27 +0300 Subject: [PATCH 12/88] open conn in separate task --- lib/mix/tasks/pleroma/benchmark.ex | 2 +- lib/pleroma/gun/api.ex | 7 +- lib/pleroma/gun/api/mock.ex | 5 +- lib/pleroma/gun/conn.ex | 146 +++++++++++++++ lib/pleroma/gun/gun.ex | 5 +- lib/pleroma/http/adapter/gun.ex | 21 +-- lib/pleroma/pool/connections.ex | 287 ++++++++++------------------- restarter/lib/pleroma.ex | 4 +- test/gun/gun_test.exs | 6 + test/http/adapter/gun_test.exs | 17 +- test/http/connection_test.exs | 2 +- test/pool/connections_test.exs | 186 ++++++++++--------- 12 files changed, 384 insertions(+), 304 deletions(-) diff --git a/lib/mix/tasks/pleroma/benchmark.ex b/lib/mix/tasks/pleroma/benchmark.ex index 01e079136..7a7430289 100644 --- a/lib/mix/tasks/pleroma/benchmark.ex +++ b/lib/mix/tasks/pleroma/benchmark.ex @@ -79,7 +79,7 @@ def run(["adapters"]) do start_pleroma() :ok = - Pleroma.Pool.Connections.open_conn( + Pleroma.Gun.Conn.open( "https://httpbin.org/stream-bytes/1500", :gun_connections ) diff --git a/lib/pleroma/gun/api.ex b/lib/pleroma/gun/api.ex index a0c3c5415..f79c9f443 100644 --- a/lib/pleroma/gun/api.ex +++ b/lib/pleroma/gun/api.ex @@ -6,9 +6,10 @@ 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 await_up(pid, pos_integer()) :: {:ok, atom()} | {:error, atom()} @callback connect(pid(), map()) :: reference() @callback await(pid(), reference()) :: {:response, :fin, 200, []} + @callback set_owner(pid(), pid()) :: :ok def open(host, port, opts), do: api().open(host, port, opts) @@ -16,11 +17,13 @@ def info(pid), do: api().info(pid) def close(pid), do: api().close(pid) - def await_up(pid), do: api().await_up(pid) + def await_up(pid, timeout \\ 5_000), do: api().await_up(pid, timeout) def connect(pid, opts), do: api().connect(pid, opts) def await(pid, ref), do: api().await(pid, ref) + def set_owner(pid, owner), do: api().set_owner(pid, owner) + 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 index 0134b016e..6d24b0e69 100644 --- a/lib/pleroma/gun/api/mock.ex +++ b/lib/pleroma/gun/api/mock.ex @@ -118,7 +118,10 @@ def open('localhost', 9050, _) do end @impl API - def await_up(_pid), do: {:ok, :http} + def await_up(_pid, _timeout), do: {:ok, :http} + + @impl API + def set_owner(_pid, _owner), do: :ok @impl API def connect(pid, %{host: _, port: 80}) do diff --git a/lib/pleroma/gun/conn.ex b/lib/pleroma/gun/conn.ex index 2474829d6..ddb9f30b0 100644 --- a/lib/pleroma/gun/conn.ex +++ b/lib/pleroma/gun/conn.ex @@ -6,6 +6,11 @@ defmodule Pleroma.Gun.Conn do @moduledoc """ Struct for gun connection data """ + alias Pleroma.Gun.API + alias Pleroma.Pool.Connections + + require Logger + @type gun_state :: :up | :down @type conn_state :: :active | :idle @@ -26,4 +31,145 @@ defmodule Pleroma.Gun.Conn do last_reference: 0, crf: 1, retries: 0 + + @spec open(String.t() | URI.t(), atom(), keyword()) :: :ok | nil + def open(url, name, opts \\ []) + def open(url, name, opts) when is_binary(url), do: open(URI.parse(url), name, opts) + + def open(%URI{} = uri, name, opts) do + pool_opts = Pleroma.Config.get([:connections_pool], []) + + opts = + opts + |> Enum.into(%{}) + |> Map.put_new(:retry, pool_opts[:retry] || 0) + |> Map.put_new(:retry_timeout, pool_opts[:retry_timeout] || 100) + |> Map.put_new(:await_up_timeout, pool_opts[:await_up_timeout] || 5_000) + + key = "#{uri.scheme}:#{uri.host}:#{uri.port}" + + Logger.debug("opening new connection #{Connections.compose_uri_log(uri)}") + + conn_pid = + if Connections.count(name) < opts[:max_connection] do + do_open(uri, opts) + else + try_do_open(name, uri, opts) + end + + if is_pid(conn_pid) do + conn = %Pleroma.Gun.Conn{ + conn: conn_pid, + gun_state: :up, + conn_state: :active, + last_reference: :os.system_time(:second) + } + + :ok = API.set_owner(conn_pid, Process.whereis(name)) + Connections.add_conn(name, key, conn) + end + end + + defp do_open(uri, %{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, opts[:await_up_timeout]), + stream <- API.connect(conn, connect_opts), + {:response, :fin, 200, _} <- API.await(conn, stream) do + conn + else + error -> + Logger.warn( + "Received error on opening connection with http proxy #{ + Connections.compose_uri_log(uri) + } #{inspect(error)}" + ) + + nil + end + end + + defp do_open(uri, %{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, opts[:await_up_timeout]) do + conn + else + error -> + Logger.warn( + "Received error on opening connection with socks proxy #{ + Connections.compose_uri_log(uri) + } #{inspect(error)}" + ) + + nil + end + end + + defp do_open(%URI{host: host, port: port} = uri, opts) do + {_type, host} = Pleroma.HTTP.Adapter.domain_or_ip(host) + + with {:ok, conn} <- API.open(host, port, opts), + {:ok, _} <- API.await_up(conn, opts[:await_up_timeout]) do + conn + else + error -> + Logger.warn( + "Received error on opening connection #{Connections.compose_uri_log(uri)} #{ + inspect(error) + }" + ) + + nil + 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 + + defp try_do_open(name, uri, opts) do + Logger.debug("try to open conn #{Connections.compose_uri_log(uri)}") + + with [{close_key, least_used} | _conns] <- + Connections.get_unused_conns(name), + :ok <- Pleroma.Gun.API.close(least_used.conn) do + Connections.remove_conn(name, close_key) + + do_open(uri, opts) + else + [] -> nil + end + end end diff --git a/lib/pleroma/gun/gun.ex b/lib/pleroma/gun/gun.ex index 4a1bbc95f..da82983b1 100644 --- a/lib/pleroma/gun/gun.ex +++ b/lib/pleroma/gun/gun.ex @@ -32,7 +32,7 @@ def open(host, port, opts \\ %{}), do: :gun.open(host, port, Map.take(opts, @gun defdelegate close(pid), to: :gun @impl API - defdelegate await_up(pid), to: :gun + defdelegate await_up(pid, timeout \\ 5_000), to: :gun @impl API defdelegate connect(pid, opts), to: :gun @@ -42,4 +42,7 @@ def open(host, port, opts \\ %{}), do: :gun.open(host, port, Map.take(opts, @gun @spec flush(pid() | reference()) :: :ok defdelegate flush(pid), to: :gun + + @impl API + defdelegate set_owner(pid, owner), to: :gun end diff --git a/lib/pleroma/http/adapter/gun.ex b/lib/pleroma/http/adapter/gun.ex index 7b7e38d8c..908d71898 100644 --- a/lib/pleroma/http/adapter/gun.ex +++ b/lib/pleroma/http/adapter/gun.ex @@ -12,7 +12,7 @@ defmodule Pleroma.HTTP.Adapter.Gun do alias Pleroma.Pool.Connections @defaults [ - connect_timeout: 20_000, + connect_timeout: 5_000, domain_lookup_timeout: 5_000, tls_handshake_timeout: 5_000, retry: 0, @@ -94,13 +94,11 @@ defp try_to_get_conn(uri, opts) do "Gun connections pool checkin was not successful. Trying to open conn for next request." ) - :ok = Connections.open_conn(uri, :gun_connections, opts) + Task.start(fn -> Pleroma.Gun.Conn.open(uri, :gun_connections, opts) end) opts conn when is_pid(conn) -> - Logger.debug( - "received conn #{inspect(conn)} #{uri.scheme}://#{Connections.compose_uri(uri)}" - ) + Logger.debug("received conn #{inspect(conn)} #{Connections.compose_uri_log(uri)}") opts |> Keyword.put(:conn, conn) @@ -109,13 +107,14 @@ defp try_to_get_conn(uri, opts) do rescue error -> Logger.warn( - "Gun connections pool checkin caused error #{uri.scheme}://#{ - Connections.compose_uri(uri) - } #{inspect(error)}" + "Gun connections pool checkin caused error #{Connections.compose_uri_log(uri)} #{ + inspect(error) + }" ) opts catch + # TODO: here must be no timeouts :exit, {:timeout, {_, operation, [_, {method, _}, _]}} -> {:message_queue_len, messages_len} = :gun_connections @@ -124,15 +123,15 @@ defp try_to_get_conn(uri, opts) do Logger.warn( "Gun connections pool checkin with timeout error for #{operation} #{method} #{ - uri.scheme - }://#{Connections.compose_uri(uri)}. Messages length: #{messages_len}" + Connections.compose_uri_log(uri) + }. Messages length: #{messages_len}" ) opts :exit, error -> Logger.warn( - "Gun pool checkin exited with error #{uri.scheme}://#{Connections.compose_uri(uri)} #{ + "Gun pool checkin exited with error #{Connections.compose_uri_log(uri)} #{ inspect(error) }" ) diff --git a/lib/pleroma/pool/connections.ex b/lib/pleroma/pool/connections.ex index d20927580..a444f822f 100644 --- a/lib/pleroma/pool/connections.ex +++ b/lib/pleroma/pool/connections.ex @@ -20,7 +20,6 @@ defmodule Pleroma.Pool.Connections do defstruct conns: %{}, opts: [] alias Pleroma.Gun.API - alias Pleroma.Gun.Conn @spec start_link({atom(), keyword()}) :: {:ok, pid()} def start_link({name, opts}) do @@ -44,23 +43,6 @@ def checkin(%URI{} = uri, name) do ) 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 = Config.get([:connections_pool], []) - - opts = - opts - |> Enum.into(%{}) - |> Map.put_new(:retry, pool_opts[:retry] || 0) - |> 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) @@ -72,23 +54,37 @@ def get_state(name) do GenServer.call(name, :state) end + @spec count(atom()) :: pos_integer() + def count(name) do + GenServer.call(name, :count) + end + + @spec get_unused_conns(atom()) :: [{domain(), conn()}] + def get_unused_conns(name) do + GenServer.call(name, :unused_conns) + end + @spec checkout(pid(), pid(), atom()) :: :ok def checkout(conn, pid, name) do GenServer.cast(name, {:checkout, conn, pid}) end + @spec add_conn(atom(), String.t(), Pleroma.Gun.Conn.t()) :: :ok + def add_conn(name, key, conn) do + GenServer.cast(name, {:add_conn, key, conn}) + end + + @spec remove_conn(atom(), String.t()) :: :ok + def remove_conn(name, key) do + GenServer.cast(name, {:remove_conn, key}) + 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] + def handle_cast({:add_conn, key, conn}, state) do + state = put_in(state.conns[key], conn) - 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 + Process.monitor(conn.conn) + {:noreply, state} end @impl true @@ -120,14 +116,20 @@ def handle_cast({:checkout, conn_pid, pid}, state) do {:noreply, state} end + @impl true + def handle_cast({:remove_conn, key}, state) do + state = put_in(state.conns, Map.delete(state.conns, key)) + {:noreply, state} + end + @impl true def handle_call({:checkin, uri}, from, state) do - Logger.debug("checkin #{compose_uri(uri)}") - key = compose_key(uri) + key = "#{uri.scheme}:#{uri.host}:#{uri.port}" + Logger.debug("checkin #{key}") case state.conns[key] do %{conn: conn, gun_state: gun_state} = current_conn when gun_state == :up -> - Logger.debug("reusing conn #{compose_uri(uri)}") + Logger.debug("reusing conn #{key}") with time <- :os.system_time(:second), last_reference <- time - current_conn.last_reference, @@ -154,12 +156,31 @@ def handle_call({:checkin, uri}, from, state) do @impl true def handle_call(:state, _from, state), do: {:reply, state, state} + @impl true + def handle_call(:count, _from, state) do + {:reply, Enum.count(state.conns), state} + end + + @impl true + def handle_call(:unused_conns, _from, state) do + unused_conns = + state.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) + + {:reply, unused_conns, state} + end + @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), + with conn_key when is_binary(conn_key) <- compose_key_gun_info(conn_pid), {key, conn} <- find_conn(state.conns, conn_pid, conn_key), + {true, key} <- {Process.alive?(conn_pid), key}, time <- :os.system_time(:second), last_reference <- time - conn.last_reference, current_crf <- crf(last_reference, 100, conn.crf) do @@ -176,14 +197,16 @@ def handle_info({:gun_up, conn_pid, _protocol}, state) do Logger.debug(":gun.info caused error") state - false -> + {false, key} -> Logger.debug(":gun_up message for closed conn #{inspect(conn_pid)}") - state + + put_in( + state.conns, + Map.delete(state.conns, key) + ) nil -> - Logger.debug( - ":gun_up message for alive conn #{inspect(conn_pid)}, but deleted from state" - ) + Logger.debug(":gun_up message for conn which is not found in state") :ok = API.close(conn_pid) @@ -198,8 +221,8 @@ def handle_info({:gun_down, conn_pid, _protocol, _reason, _killed}, state) do retries = Config.get([:connections_pool, :retry], 0) # 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 + with {key, conn} <- find_conn(state.conns, conn_pid), + {true, key} <- {Process.alive?(conn_pid), key} do if conn.retries == retries do Logger.debug("closing conn if retries is eq #{inspect(conn_pid)}") :ok = API.close(conn.conn) @@ -216,15 +239,17 @@ def handle_info({:gun_down, conn_pid, _protocol, _reason, _killed}, state) do }) end else - false -> + {false, key} -> # gun can send gun_down for closed conn, maybe connection is not closed yet Logger.debug(":gun_down message for closed conn #{inspect(conn_pid)}") - state + + put_in( + state.conns, + Map.delete(state.conns, key) + ) nil -> - Logger.debug( - ":gun_down message for alive conn #{inspect(conn_pid)}, but deleted from state" - ) + Logger.debug(":gun_down message for conn which is not found in state") :ok = API.close(conn_pid) @@ -234,7 +259,29 @@ def handle_info({:gun_down, conn_pid, _protocol, _reason, _killed}, state) do {:noreply, state} end - defp compose_key(%URI{scheme: scheme, host: host, port: port}), do: "#{scheme}:#{host}:#{port}" + @impl true + def handle_info({:DOWN, _ref, :process, conn_pid, reason}, state) do + Logger.debug("received DOWM message for #{inspect(conn_pid)} reason -> #{inspect(reason)}") + + state = + with {key, conn} <- find_conn(state.conns, conn_pid) do + Enum.each(conn.used_by, fn {pid, _ref} -> + Process.exit(pid, reason) + end) + + put_in( + state.conns, + Map.delete(state.conns, key) + ) + else + nil -> + Logger.debug(":DOWN message for conn which is not found in state") + + state + end + + {:noreply, state} + end defp compose_key_gun_info(pid) do try do @@ -265,153 +312,11 @@ defp find_conn(conns, conn_pid, conn_key) do 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}" + def compose_uri_log(%URI{scheme: scheme, host: host, path: path}) do + "#{scheme}://#{host}#{path}" + end end diff --git a/restarter/lib/pleroma.ex b/restarter/lib/pleroma.ex index d7817909d..4ade890f9 100644 --- a/restarter/lib/pleroma.ex +++ b/restarter/lib/pleroma.ex @@ -44,7 +44,7 @@ def handle_cast(:need_reboot, state) do end def handle_cast({:restart, :test, _}, state) do - Logger.warn("pleroma restarted") + Logger.warn("pleroma manually restarted") {:noreply, Map.put(state, :need_reboot?, false)} end @@ -57,7 +57,7 @@ def handle_cast({:restart, _, delay}, state) do def handle_cast({:after_boot, _}, %{after_boot: true} = state), do: {:noreply, state} def handle_cast({:after_boot, :test}, state) do - Logger.warn("pleroma restarted") + Logger.warn("pleroma restarted after boot") {:noreply, Map.put(state, :after_boot, true)} end diff --git a/test/gun/gun_test.exs b/test/gun/gun_test.exs index 7f185617c..9f3e0f938 100644 --- a/test/gun/gun_test.exs +++ b/test/gun/gun_test.exs @@ -19,6 +19,12 @@ test "opens connection and receive response" do assert json = receive_response(conn, ref) assert %{"args" => %{"a" => "b", "c" => "d"}} = Jason.decode!(json) + + {:ok, pid} = Task.start(fn -> Process.sleep(50) end) + + :ok = :gun.set_owner(conn, pid) + + assert :gun.info(conn).owner == pid end defp receive_response(conn, ref, acc \\ "") do diff --git a/test/http/adapter/gun_test.exs b/test/http/adapter/gun_test.exs index ef1b4a882..a8dcbae04 100644 --- a/test/http/adapter/gun_test.exs +++ b/test/http/adapter/gun_test.exs @@ -7,6 +7,7 @@ defmodule Pleroma.HTTP.Adapter.GunTest do use Pleroma.Tests.Helpers import ExUnit.CaptureLog alias Pleroma.Config + alias Pleroma.Gun.Conn alias Pleroma.HTTP.Adapter.Gun alias Pleroma.Pool.Connections @@ -72,7 +73,7 @@ test "https url with non standart port" do test "receive conn by default" do uri = URI.parse("http://another-domain.com") - :ok = Connections.open_conn(uri, :gun_connections) + :ok = Conn.open(uri, :gun_connections) received_opts = Gun.options(uri) assert received_opts[:close_conn] == false @@ -81,7 +82,7 @@ test "receive conn by default" do 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) + :ok = Conn.open(uri, :gun_connections) opts = [receive_conn: false] received_opts = Gun.options(opts, uri) @@ -118,7 +119,7 @@ test "merges with defaul http adapter config" do test "default ssl adapter opts with connection" do uri = URI.parse("https://some-domain.com") - :ok = Connections.open_conn(uri, :gun_connections) + :ok = Conn.open(uri, :gun_connections) opts = Gun.options(uri) @@ -167,7 +168,7 @@ test "passed opts have more weight than defaults" do 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) + :ok = Conn.open(uri, :gun_connections) opts = Gun.options(uri) :ok = Gun.after_request(opts) conn = opts[:conn] @@ -185,7 +186,7 @@ test "body_as not chunks" do test "body_as chunks" do uri = URI.parse("http://some-domain.com") - :ok = Connections.open_conn(uri, :gun_connections) + :ok = Conn.open(uri, :gun_connections) opts = Gun.options([body_as: :chunks], uri) :ok = Gun.after_request(opts) conn = opts[:conn] @@ -205,7 +206,7 @@ test "body_as chunks" do test "with no connection" do uri = URI.parse("http://uniq-domain.com") - :ok = Connections.open_conn(uri, :gun_connections) + :ok = Conn.open(uri, :gun_connections) opts = Gun.options([body_as: :chunks], uri) conn = opts[:conn] @@ -227,7 +228,7 @@ test "with no connection" do test "with ipv4" do uri = URI.parse("http://127.0.0.1") - :ok = Connections.open_conn(uri, :gun_connections) + :ok = Conn.open(uri, :gun_connections) opts = Gun.options(uri) send(:gun_connections, {:gun_up, opts[:conn], :http}) :ok = Gun.after_request(opts) @@ -246,7 +247,7 @@ test "with ipv4" do test "with ipv6" do uri = URI.parse("http://[2a03:2880:f10c:83:face:b00c:0:25de]") - :ok = Connections.open_conn(uri, :gun_connections) + :ok = Conn.open(uri, :gun_connections) opts = Gun.options(uri) send(:gun_connections, {:gun_up, opts[:conn], :http}) :ok = Gun.after_request(opts) diff --git a/test/http/connection_test.exs b/test/http/connection_test.exs index c1ff0cc21..53ccbc9cd 100644 --- a/test/http/connection_test.exs +++ b/test/http/connection_test.exs @@ -124,7 +124,7 @@ test "default ssl adapter opts with connection" do uri = URI.parse("https://some-domain.com") pid = Process.whereis(:federation) - :ok = Pleroma.Pool.Connections.open_conn(uri, :gun_connections, genserver_pid: pid) + :ok = Pleroma.Gun.Conn.open(uri, :gun_connections, genserver_pid: pid) opts = Connection.options(uri) diff --git a/test/pool/connections_test.exs b/test/pool/connections_test.exs index d0d711c55..f766e3b5f 100644 --- a/test/pool/connections_test.exs +++ b/test/pool/connections_test.exs @@ -45,7 +45,7 @@ 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) + :ok = Conn.open(url, name) conn = Connections.checkin(url, name) assert is_pid(conn) @@ -110,7 +110,7 @@ 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) + :ok = Conn.open(url, name) conn = Connections.checkin(url, name) assert is_pid(conn) @@ -139,7 +139,7 @@ test "reuse for ipv4", %{name: name} do refute Connections.checkin(url, name) - :ok = Connections.open_conn(url, name) + :ok = Conn.open(url, name) conn = Connections.checkin(url, name) assert is_pid(conn) @@ -182,7 +182,7 @@ test "reuse for ipv6", %{name: name} do refute Connections.checkin(url, name) - :ok = Connections.open_conn(url, name) + :ok = Conn.open(url, name) conn = Connections.checkin(url, name) assert is_pid(conn) @@ -209,7 +209,7 @@ test "reuse for ipv6", %{name: name} do test "up and down ipv4", %{name: name} do self = self() url = "http://127.0.0.1" - :ok = Connections.open_conn(url, name) + :ok = Conn.open(url, name) conn = Connections.checkin(url, name) send(name, {:gun_down, conn, nil, nil, nil}) send(name, {:gun_up, conn, nil}) @@ -229,7 +229,7 @@ test "up and down ipv4", %{name: name} do 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) + :ok = Conn.open(url, name) conn = Connections.checkin(url, name) send(name, {:gun_down, conn, nil, nil, nil}) send(name, {:gun_up, conn, nil}) @@ -253,13 +253,13 @@ test "reuses connection based on protocol", %{name: name} do https_key = "https:some-domain.com:443" refute Connections.checkin(http_url, name) - :ok = Connections.open_conn(http_url, name) + :ok = Conn.open(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) + :ok = Conn.open(https_url, name) https_conn = Connections.checkin(https_url, name) refute conn == https_conn @@ -288,17 +288,17 @@ 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 Conn.open(url, name) refute Connections.checkin(url, name) end) =~ - "Received error on opening connection http://gun-not-up.com: {:error, :timeout}" + "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) + :ok = Conn.open(url, name) conn = Connections.checkin(url, name) assert is_pid(conn) @@ -347,7 +347,7 @@ test "process gun_down message and then gun_up", %{name: name} do test "async processes get same conn for same domain", %{name: name} do url = "http://some-domain.com" - :ok = Connections.open_conn(url, name) + :ok = Conn.open(url, name) tasks = for _ <- 1..5 do @@ -381,8 +381,8 @@ 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) + :ok = Conn.open(https_url, name) + :ok = Conn.open(http_url, name) conn1 = Connections.checkin(https_url, name) @@ -413,7 +413,7 @@ test "remove frequently used and idle", %{name: name} do :ok = Connections.checkout(conn1, self, name) another_url = "http://another-domain.com" - :ok = Connections.open_conn(another_url, name) + :ok = Conn.open(another_url, name) conn = Connections.checkin(another_url, name) %Connections{ @@ -437,9 +437,19 @@ test "remove frequently used and idle", %{name: name} do Pleroma.Config.put(API, Pleroma.Gun) end + test "opens connection and change owner", %{name: name} do + url = "https://httpbin.org" + :ok = Conn.open(url, name) + conn = Connections.checkin(url, name) + + pid = Process.whereis(name) + + assert :gun.info(conn).owner == pid + end + test "opens connection and reuse it on next request", %{name: name} do url = "http://httpbin.org" - :ok = Connections.open_conn(url, name) + :ok = Conn.open(url, name) Process.sleep(250) conn = Connections.checkin(url, name) @@ -462,7 +472,7 @@ test "opens connection and reuse it on next request", %{name: name} do test "opens ssl connection and reuse it on next request", %{name: name} do url = "https://httpbin.org" - :ok = Connections.open_conn(url, name) + :ok = Conn.open(url, name) Process.sleep(1_000) conn = Connections.checkin(url, name) @@ -488,8 +498,8 @@ test "remove frequently used and idle", %{name: name} do https1 = "https://www.google.com" https2 = "https://httpbin.org" - :ok = Connections.open_conn(https1, name) - :ok = Connections.open_conn(https2, name) + :ok = Conn.open(https1, name) + :ok = Conn.open(https2, name) Process.sleep(1_500) conn = Connections.checkin(https1, name) @@ -513,7 +523,7 @@ test "remove frequently used and idle", %{name: name} do :ok = Connections.checkout(conn, self, name) http = "http://httpbin.org" Process.sleep(1_000) - :ok = Connections.open_conn(http, name) + :ok = Conn.open(http, name) conn = Connections.checkin(http, name) %Connections{ @@ -535,8 +545,8 @@ test "remove earlier used and idle", %{name: name} do https1 = "https://www.google.com" https2 = "https://httpbin.org" - :ok = Connections.open_conn(https1, name) - :ok = Connections.open_conn(https2, name) + :ok = Conn.open(https1, name) + :ok = Conn.open(https2, name) Process.sleep(1_500) Connections.checkin(https1, name) @@ -563,7 +573,7 @@ test "remove earlier used and idle", %{name: name} do :ok = Connections.checkout(conn, self, name) http = "http://httpbin.org" - :ok = Connections.open_conn(http, name) + :ok = Conn.open(http, name) Process.sleep(1_000) conn = Connections.checkin(http, name) @@ -587,8 +597,8 @@ test "doesn't open new conn on pool overflow", %{name: name} do https1 = "https://www.google.com" https2 = "https://httpbin.org" - :ok = Connections.open_conn(https1, name) - :ok = Connections.open_conn(https2, name) + :ok = Conn.open(https1, name) + :ok = Conn.open(https2, name) Process.sleep(1_000) Connections.checkin(https1, name) conn1 = Connections.checkin(https1, name) @@ -639,8 +649,8 @@ test "get idle connection with the smallest crf", %{ https1 = "https://www.google.com" https2 = "https://httpbin.org" - :ok = Connections.open_conn(https1, name) - :ok = Connections.open_conn(https2, name) + :ok = Conn.open(https1, name) + :ok = Conn.open(https2, name) Process.sleep(1_500) Connections.checkin(https1, name) Connections.checkin(https2, name) @@ -694,7 +704,7 @@ test "get idle connection with the smallest crf", %{ } = Connections.get_state(name) http = "http://httpbin.org" - :ok = Connections.open_conn(http, name) + :ok = Conn.open(http, name) Process.sleep(1_000) conn = Connections.checkin(http, name) @@ -725,7 +735,7 @@ test "get idle connection with the smallest crf", %{ 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}) + :ok = Conn.open(url, name, proxy: {{127, 0, 0, 1}, 8123}) conn = Connections.checkin(url, name) @@ -745,7 +755,7 @@ test "as ip", %{name: name} do test "as host", %{name: name} do url = "http://proxy-tuple-atom.com" - :ok = Connections.open_conn(url, name, proxy: {'localhost', 9050}) + :ok = Conn.open(url, name, proxy: {'localhost', 9050}) conn = Connections.checkin(url, name) %Connections{ @@ -765,7 +775,7 @@ test "as host", %{name: name} do 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}) + :ok = Conn.open(url, name, proxy: {{127, 0, 0, 1}, 8123}) conn = Connections.checkin(url, name) %Connections{ @@ -784,7 +794,7 @@ test "as ip and ssl", %{name: name} do test "as host and ssl", %{name: name} do url = "https://proxy-tuple-atom.com" - :ok = Connections.open_conn(url, name, proxy: {'localhost', 9050}) + :ok = Conn.open(url, name, proxy: {'localhost', 9050}) conn = Connections.checkin(url, name) %Connections{ @@ -804,7 +814,7 @@ test "as host and ssl", %{name: name} do test "with socks type", %{name: name} do url = "http://proxy-socks.com" - :ok = Connections.open_conn(url, name, proxy: {:socks5, 'localhost', 1234}) + :ok = Conn.open(url, name, proxy: {:socks5, 'localhost', 1234}) conn = Connections.checkin(url, name) @@ -825,7 +835,7 @@ test "with socks type", %{name: name} do test "with socks4 type and ssl", %{name: name} do url = "https://proxy-socks.com" - :ok = Connections.open_conn(url, name, proxy: {:socks4, 'localhost', 1234}) + :ok = Conn.open(url, name, proxy: {:socks4, 'localhost', 1234}) conn = Connections.checkin(url, name) @@ -892,71 +902,75 @@ test "recently used will have higher crf", %{crf: crf} do 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() - } - } + test "crf is equalent, sorting by reference", %{name: name} do + Connections.add_conn(name, "1", %Conn{ + conn_state: :idle, + last_reference: now() - 1 + }) - assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(conns) + Connections.add_conn(name, "2", %Conn{ + conn_state: :idle, + last_reference: now() + }) + + assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(name) 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 - } - } + test "reference is equalent, sorting by crf", %{name: name} do + Connections.add_conn(name, "1", %Conn{ + conn_state: :idle, + crf: 1.999 + }) - assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(conns) + Connections.add_conn(name, "2", %Conn{ + conn_state: :idle, + crf: 2 + }) + + assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(name) 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() - } - } + test "higher crf and lower reference", %{name: name} do + Connections.add_conn(name, "1", %Conn{ + conn_state: :idle, + crf: 3, + last_reference: now() - 1 + }) - assert [{"2", _unused_conn} | _others] = Connections.get_unused_conns(conns) + Connections.add_conn(name, "2", %Conn{ + conn_state: :idle, + crf: 2, + last_reference: now() + }) + + assert [{"2", _unused_conn} | _others] = Connections.get_unused_conns(name) 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() - } - } + test "lower crf and lower reference", %{name: name} do + Connections.add_conn(name, "1", %Conn{ + conn_state: :idle, + crf: 1.99, + last_reference: now() - 1 + }) - assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(conns) + Connections.add_conn(name, "2", %Conn{ + conn_state: :idle, + crf: 2, + last_reference: now() + }) + + assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(name) end end + test "count/1", %{name: name} do + assert Connections.count(name) == 0 + Connections.add_conn(name, "1", %Conn{conn: self()}) + assert Connections.count(name) == 1 + Connections.remove_conn(name, "1") + assert Connections.count(name) == 0 + end + defp now do :os.system_time(:second) end From 6b012ddd69aec0f85c22ad91dbb76e05f2edaf58 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 25 Feb 2020 19:01:29 +0300 Subject: [PATCH 13/88] some docs --- docs/configuration/cheatsheet.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index d99537a50..d5a978c5a 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -394,6 +394,8 @@ For each pool, the options are: Advanced settings for connections pool. Pool with opened connections. These connections can be reused in worker pools. +For big instances it's recommended to increase `max_connections` up to 500-1000. It will increase memory usage, but federation would work faster. + * `: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. From 2622cf1190fe8e6ec9145a8cd2538a56889aa7e2 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Mon, 2 Mar 2020 09:22:34 +0300 Subject: [PATCH 14/88] returning repo parameters --- config/config.exs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/config/config.exs b/config/config.exs index 159aa6398..82012dc10 100644 --- a/config/config.exs +++ b/config/config.exs @@ -49,8 +49,7 @@ config :pleroma, Pleroma.Repo, types: Pleroma.PostgresTypes, telemetry_event: [Pleroma.Repo.Instrumenter], - migration_lock: nil, - parameters: [gin_fuzzy_search_limit: "500"] + migration_lock: nil config :pleroma, Pleroma.Captcha, enabled: true, @@ -603,6 +602,8 @@ config :pleroma, configurable_from_database: false +config :pleroma, Pleroma.Repo, parameters: [gin_fuzzy_search_limit: "500"] + config :pleroma, :connections_pool, receive_connection_timeout: 250, max_connections: 250, From 137c600cae9869e706d10b06dea04c9249e043da Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Mon, 2 Mar 2020 10:01:07 +0300 Subject: [PATCH 15/88] stop connections manually --- test/pool/connections_test.exs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/test/pool/connections_test.exs b/test/pool/connections_test.exs index f766e3b5f..0e7a118ab 100644 --- a/test/pool/connections_test.exs +++ b/test/pool/connections_test.exs @@ -23,11 +23,18 @@ defmodule Pleroma.Pool.ConnectionsTest 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} = + {:ok, pid} = Connections.start_link({name, [max_connections: 2, receive_connection_timeout: 1_500]}) + on_exit(fn -> + Application.put_env(:tesla, :adapter, adapter) + + if Process.alive?(pid) do + GenServer.stop(name) + end + end) + {:ok, name: name} end From 85d571fc238c14bedbc0d9a0af2c7c0d76d62c4a Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 2 Mar 2020 12:52:41 -0600 Subject: [PATCH 16/88] Move Tesla repo to our GitLab --- mix.exs | 2 +- mix.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index 017228b4c..5c1d89208 100644 --- a/mix.exs +++ b/mix.exs @@ -121,7 +121,7 @@ defp deps do {:poison, "~> 3.0", override: true}, # {:tesla, "~> 1.3", override: true}, {:tesla, - github: "alex-strizhakov/tesla", + git: "https://git.pleroma.social/pleroma/elixir-libraries/tesla.git", ref: "922cc3db13b421763edbea76246b8ea61c38c6fa", override: true}, {:castore, "~> 0.1"}, diff --git a/mix.lock b/mix.lock index fecc959e0..8b5c61895 100644 --- a/mix.lock +++ b/mix.lock @@ -102,7 +102,7 @@ "swoosh": {:hex, :swoosh, "0.23.5", "bfd9404bbf5069b1be2ffd317923ce57e58b332e25dbca2a35dedd7820dfee5a", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "e3928e1d2889a308aaf3e42755809ac21cffd77cb58eef01cbfdab4ce2fd1e21"}, "syslog": {:hex, :syslog, "1.0.6", "995970c9aa7feb380ac493302138e308d6e04fd57da95b439a6df5bb3bf75076", [:rebar3], [], "hexpm", "769ddfabd0d2a16f3f9c17eb7509951e0ca4f68363fb26f2ee51a8ec4a49881a"}, "telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"}, - "tesla": {:git, "https://github.com/alex-strizhakov/tesla.git", "922cc3db13b421763edbea76246b8ea61c38c6fa", [ref: "922cc3db13b421763edbea76246b8ea61c38c6fa"]}, + "tesla": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/tesla.git", "922cc3db13b421763edbea76246b8ea61c38c6fa", [ref: "922cc3db13b421763edbea76246b8ea61c38c6fa"]}, "timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "f354efb2400dd7a80fd9eb6c8419068c4f632da4ac47f3d8822d6e33f08bc852"}, "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"}, "tzdata": {:hex, :tzdata, "0.5.22", "f2ba9105117ee0360eae2eca389783ef7db36d533899b2e84559404dbc77ebb8", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "cd66c8a1e6a9e121d1f538b01bef459334bb4029a1ffb4eeeb5e4eae0337e7b6"}, From f987d83885eef7cd8d114feefe8870a8c5e841c6 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 2 Mar 2020 13:00:05 -0600 Subject: [PATCH 17/88] Clarify in docs how to control connections_pool for Gun. It could easily be confused with the Hackney settings. --- docs/configuration/cheatsheet.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 507f15b87..abb5a3c5f 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -395,7 +395,8 @@ For each pool, the options are: Advanced settings for connections pool. Pool with opened connections. These connections can be reused in worker pools. -For big instances it's recommended to increase `max_connections` up to 500-1000. It will increase memory usage, but federation would work faster. +For big instances it's recommended to increase `config :pleroma, :connections_pool, max_connections: 500` up to 500-1000. +It will increase memory usage, but federation would work faster. * `:receive_connection_timeout` - timeout to receive connection from pool. Default: 250ms. * `:max_connections` - maximum number of connections in the pool. Default: 250 connections. From 3ecdead31ae65f395104a5fd7fafc847a7b97eca Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 3 Mar 2020 10:33:40 +0300 Subject: [PATCH 18/88] debug logs on pleroma restart --- restarter/lib/pleroma.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/restarter/lib/pleroma.ex b/restarter/lib/pleroma.ex index 4ade890f9..e48bc4d1d 100644 --- a/restarter/lib/pleroma.ex +++ b/restarter/lib/pleroma.ex @@ -44,7 +44,7 @@ def handle_cast(:need_reboot, state) do end def handle_cast({:restart, :test, _}, state) do - Logger.warn("pleroma manually restarted") + Logger.debug("pleroma manually restarted") {:noreply, Map.put(state, :need_reboot?, false)} end @@ -57,7 +57,7 @@ def handle_cast({:restart, _, delay}, state) do def handle_cast({:after_boot, _}, %{after_boot: true} = state), do: {:noreply, state} def handle_cast({:after_boot, :test}, state) do - Logger.warn("pleroma restarted after boot") + Logger.debug("pleroma restarted after boot") {:noreply, Map.put(state, :after_boot, true)} end From 4c8569d403f47957f7a5d698c595959007c8a95a Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 3 Mar 2020 12:19:29 +0300 Subject: [PATCH 19/88] otp_version refactor --- lib/pleroma/application.ex | 35 +++++----- lib/pleroma/otp_version.ex | 68 +++++++++----------- test/fixtures/warnings/otp_version/error | 1 - test/fixtures/warnings/otp_version/undefined | 1 - test/otp_version_test.exs | 42 ++++-------- 5 files changed, 60 insertions(+), 87 deletions(-) delete mode 100644 test/fixtures/warnings/otp_version/error delete mode 100644 test/fixtures/warnings/otp_version/undefined diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 00e33d7ac..9b228d6b9 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -66,16 +66,23 @@ def start(_type, _args) 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. - " + if adapter() == Tesla.Adapter.Gun do + case Pleroma.OTPVersion.check() 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 end # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html @@ -202,11 +209,7 @@ defp http_pools_children(:test) do [hackney_pool, Pleroma.Pool.Supervisor] end - defp http_pools_children(_) do - :tesla - |> Application.get_env(:adapter) - |> http_pools() - end + defp http_pools_children(_), do: http_pools(adapter()) defp http_pools(Tesla.Adapter.Hackney) do pools = [:federation, :media] @@ -227,4 +230,6 @@ defp http_pools(Tesla.Adapter.Hackney) do defp http_pools(Tesla.Adapter.Gun), do: [Pleroma.Pool.Supervisor] defp http_pools(_), do: [] + + defp adapter, do: Application.get_env(:tesla, :adapter) end diff --git a/lib/pleroma/otp_version.ex b/lib/pleroma/otp_version.ex index 0be189304..54ceaff47 100644 --- a/lib/pleroma/otp_version.ex +++ b/lib/pleroma/otp_version.ex @@ -1,63 +1,53 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.OTPVersion do - @type check_status() :: :undefined | {:error, String.t()} | :ok + @type check_status() :: :ok | :undefined | {:error, String.t()} - require Logger - - @spec check_version() :: check_status() - def check_version do + @spec check() :: check_status() + def check 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) + |> get_version_from_files() + |> do_check() end - @spec get_and_check_version(module(), [Path.t()]) :: check_status() - def get_and_check_version(Tesla.Adapter.Gun, paths) do + @spec check([Path.t()]) :: check_status() + def check(paths) do paths - |> check_files() - |> check_version() + |> get_version_from_files() + |> do_check() end - def get_and_check_version(_, _), do: :ok + defp get_version_from_files([]), do: nil - defp check_files([]), do: nil - - defp check_files([path | paths]) do + defp get_version_from_files([path | paths]) do if File.exists?(path) do File.read!(path) else - check_files(paths) + get_version_from_files(paths) end end - defp check_version(nil), do: :undefined + defp do_check(nil), do: :undefined - defp check_version(version) do - try do - version = String.replace(version, ~r/\r|\n|\s/, "") + defp do_check(version) do + version = String.replace(version, ~r/\r|\n|\s/, "") - formatted = - version - |> String.split(".") - |> Enum.map(&String.to_integer/1) - |> Enum.take(2) + [major, minor] = + 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 + if (major == 22 and minor >= 2) or major > 22 do + :ok + else + {:error, version} end end end diff --git a/test/fixtures/warnings/otp_version/error b/test/fixtures/warnings/otp_version/error deleted file mode 100644 index 8fdd954df..000000000 --- a/test/fixtures/warnings/otp_version/error +++ /dev/null @@ -1 +0,0 @@ -22 \ No newline at end of file diff --git a/test/fixtures/warnings/otp_version/undefined b/test/fixtures/warnings/otp_version/undefined deleted file mode 100644 index 66dc9051d..000000000 --- a/test/fixtures/warnings/otp_version/undefined +++ /dev/null @@ -1 +0,0 @@ -undefined \ No newline at end of file diff --git a/test/otp_version_test.exs b/test/otp_version_test.exs index f26b90f61..af278cc72 100644 --- a/test/otp_version_test.exs +++ b/test/otp_version_test.exs @@ -1,58 +1,38 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.OTPVersionTest do use ExUnit.Case, async: true alias Pleroma.OTPVersion - describe "get_and_check_version/2" do + describe "check/1" do test "22.4" do - assert OTPVersion.get_and_check_version(Tesla.Adapter.Gun, [ - "test/fixtures/warnings/otp_version/22.4" - ]) == :ok + assert OTPVersion.check(["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"} + assert OTPVersion.check(["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"} + assert OTPVersion.check(["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 + assert OTPVersion.check(["test/fixtures/warnings/otp_version/23.0"]) == :ok end test "with non existance file" do - assert OTPVersion.get_and_check_version(Tesla.Adapter.Gun, [ + assert OTPVersion.check([ "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 + assert OTPVersion.check([]) == :undefined end end end From 097ad10d02598fb6b77f305c10341a13fb57ceee Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 3 Mar 2020 09:29:51 +0000 Subject: [PATCH 20/88] Apply suggestion to lib/pleroma/pool/connections.ex --- lib/pleroma/pool/connections.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/pool/connections.ex b/lib/pleroma/pool/connections.ex index a444f822f..c5098cd86 100644 --- a/lib/pleroma/pool/connections.ex +++ b/lib/pleroma/pool/connections.ex @@ -128,7 +128,7 @@ def handle_call({:checkin, uri}, from, state) do Logger.debug("checkin #{key}") case state.conns[key] do - %{conn: conn, gun_state: gun_state} = current_conn when gun_state == :up -> + %{conn: conn, gun_state: :up} = current_conn -> Logger.debug("reusing conn #{key}") with time <- :os.system_time(:second), From 2c8d80dc0ad594cfe25ebadd9e7a187c95914b34 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 3 Mar 2020 09:29:57 +0000 Subject: [PATCH 21/88] Apply suggestion to lib/pleroma/pool/connections.ex --- lib/pleroma/pool/connections.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/pool/connections.ex b/lib/pleroma/pool/connections.ex index c5098cd86..c4c5fd66c 100644 --- a/lib/pleroma/pool/connections.ex +++ b/lib/pleroma/pool/connections.ex @@ -145,7 +145,7 @@ def handle_call({:checkin, uri}, from, state) do {:reply, conn, state} end - %{gun_state: gun_state} when gun_state == :down -> + %{gun_state: :down} -> {:reply, nil, state} nil -> From a3ad028973154dafad910d4d73d7d4d4822627c1 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 3 Mar 2020 09:34:36 +0000 Subject: [PATCH 22/88] Apply suggestion to lib/pleroma/http/adapter.ex --- lib/pleroma/http/adapter.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/http/adapter.ex b/lib/pleroma/http/adapter.ex index 6166a3eb4..32046b1d3 100644 --- a/lib/pleroma/http/adapter.ex +++ b/lib/pleroma/http/adapter.ex @@ -57,7 +57,7 @@ def domain_or_ip(host) do {:error, :einval} -> {:domain, :idna.encode(charlist)} - {:ok, ip} when is_tuple(ip) and tuple_size(ip) in [4, 8] -> + {:ok, ip} -> {:ip, ip} end end From df3c59d9280b94cf99571cbbd1b10c334db8e44d Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 3 Mar 2020 09:45:18 +0000 Subject: [PATCH 23/88] Apply suggestion to docs/configuration/cheatsheet.md --- docs/configuration/cheatsheet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index f735b19b8..65f37e846 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -416,7 +416,7 @@ It will increase memory usage, but federation would work faster. Advanced settings for workers pools. -There's four pools used: +There are 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. From d30ff35d94ff7d8bc07f0221323a75b07641ee8d Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 3 Mar 2020 09:46:53 +0000 Subject: [PATCH 24/88] Apply suggestion to lib/pleroma/http/request_builder.ex --- lib/pleroma/http/request_builder.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/http/request_builder.ex b/lib/pleroma/http/request_builder.ex index 491acd0f9..046741d99 100644 --- a/lib/pleroma/http/request_builder.ex +++ b/lib/pleroma/http/request_builder.ex @@ -35,7 +35,7 @@ def url(request, u), do: %{request | url: u} def headers(request, headers) do headers_list = if Pleroma.Config.get([:http, :send_user_agent]) do - headers ++ [{"user-agent", Pleroma.Application.user_agent()}] + [{"user-agent", Pleroma.Application.user_agent()} | headers] else headers end From 614e3934f9190ff199df087de34146ad5f34c660 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 3 Mar 2020 09:50:42 +0000 Subject: [PATCH 25/88] Apply suggestion to lib/pleroma/http/http.ex --- lib/pleroma/http/http.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/http/http.ex b/lib/pleroma/http/http.ex index ad47dc936..5fb468689 100644 --- a/lib/pleroma/http/http.ex +++ b/lib/pleroma/http/http.ex @@ -64,8 +64,8 @@ def request(method, url, body, headers, options) when is_binary(url) do 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 + if tesla_adapter() == Tesla.Adapter.Gun && pid do + Process.alive?(pid) else false end From a21a66972f8733de766bc538fe81f2e0ccb57925 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 3 Mar 2020 09:52:01 +0000 Subject: [PATCH 26/88] Apply suggestion to lib/pleroma/http/http.ex --- lib/pleroma/http/http.ex | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/pleroma/http/http.ex b/lib/pleroma/http/http.ex index 5fb468689..0235f89ea 100644 --- a/lib/pleroma/http/http.ex +++ b/lib/pleroma/http/http.ex @@ -76,12 +76,7 @@ def request(method, url, body, headers, options) when is_binary(url) do |> Map.put(:env, Pleroma.Config.get([:env])) |> Map.put(:pool_alive?, pool_alive?) - response = - request( - client, - request, - request_opts - ) + response = request(client, request, request_opts) Connection.after_request(adapter_opts) From 7eb65929924af50146d89192c2cf557e3bdbf07f Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 3 Mar 2020 09:53:31 +0000 Subject: [PATCH 27/88] Apply suggestion to lib/pleroma/pool/connections.ex --- lib/pleroma/pool/connections.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/pool/connections.ex b/lib/pleroma/pool/connections.ex index c4c5fd66c..84617815f 100644 --- a/lib/pleroma/pool/connections.ex +++ b/lib/pleroma/pool/connections.ex @@ -180,10 +180,10 @@ def handle_info({:gun_up, conn_pid, _protocol}, state) do state = with conn_key when is_binary(conn_key) <- compose_key_gun_info(conn_pid), {key, conn} <- find_conn(state.conns, conn_pid, conn_key), - {true, key} <- {Process.alive?(conn_pid), key}, - time <- :os.system_time(:second), - last_reference <- time - conn.last_reference, - current_crf <- crf(last_reference, 100, conn.crf) do + {true, key} <- {Process.alive?(conn_pid), key} do + time = :os.system_time(:second) + last_reference = time - conn.last_reference + current_crf = crf(last_reference, 100, conn.crf) put_in(state.conns[key], %{ conn | gun_state: :up, From 151dc4e387cfbb91b7cd85461ce0deb1e5f5fe30 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 3 Mar 2020 09:53:37 +0000 Subject: [PATCH 28/88] Apply suggestion to lib/pleroma/reverse_proxy/client/tesla.ex --- lib/pleroma/reverse_proxy/client/tesla.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/reverse_proxy/client/tesla.ex b/lib/pleroma/reverse_proxy/client/tesla.ex index 55a11b4a8..498a905e1 100644 --- a/lib/pleroma/reverse_proxy/client/tesla.ex +++ b/lib/pleroma/reverse_proxy/client/tesla.ex @@ -16,7 +16,7 @@ defmodule Pleroma.ReverseProxy.Client.Tesla do @impl true def request(method, url, headers, body, opts \\ []) do - _adapter = check_adapter() + check_adapter() with opts <- Keyword.merge(opts, body_as: :chunks, mode: :passive), {:ok, response} <- From 28ed4b41d03c6a137d198b8c67fb081c7ebfbbc6 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 3 Mar 2020 13:05:28 +0300 Subject: [PATCH 29/88] naming for checkin from pool timeout --- config/config.exs | 2 +- docs/configuration/cheatsheet.md | 2 +- lib/pleroma/pool/connections.ex | 3 ++- test/pool/connections_test.exs | 3 +-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config/config.exs b/config/config.exs index 7c94a0f26..661dfad20 100644 --- a/config/config.exs +++ b/config/config.exs @@ -607,7 +607,7 @@ prepare: :unnamed config :pleroma, :connections_pool, - receive_connection_timeout: 250, + checkin_timeout: 250, max_connections: 250, retry: 0, retry_timeout: 100, diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 65f37e846..ef3cc40e6 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -404,7 +404,7 @@ Advanced settings for connections pool. Pool with opened connections. These conn For big instances it's recommended to increase `config :pleroma, :connections_pool, max_connections: 500` up to 500-1000. It will increase memory usage, but federation would work faster. -* `:receive_connection_timeout` - timeout to receive connection from pool. Default: 250ms. +* `:checkin_timeout` - timeout to checkin 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. diff --git a/lib/pleroma/pool/connections.ex b/lib/pleroma/pool/connections.ex index 84617815f..05fa8f7ad 100644 --- a/lib/pleroma/pool/connections.ex +++ b/lib/pleroma/pool/connections.ex @@ -34,7 +34,7 @@ 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 = Config.get([:connections_pool, :receive_connection_timeout], 250) + timeout = Config.get([:connections_pool, :checkin_timeout], 250) GenServer.call( name, @@ -184,6 +184,7 @@ def handle_info({:gun_up, conn_pid, _protocol}, state) do time = :os.system_time(:second) last_reference = time - conn.last_reference current_crf = crf(last_reference, 100, conn.crf) + put_in(state.conns[key], %{ conn | gun_state: :up, diff --git a/test/pool/connections_test.exs b/test/pool/connections_test.exs index 0e7a118ab..a084f31b9 100644 --- a/test/pool/connections_test.exs +++ b/test/pool/connections_test.exs @@ -24,8 +24,7 @@ defmodule Pleroma.Pool.ConnectionsTest do adapter = Application.get_env(:tesla, :adapter) Application.put_env(:tesla, :adapter, Tesla.Adapter.Gun) - {:ok, pid} = - Connections.start_link({name, [max_connections: 2, receive_connection_timeout: 1_500]}) + {:ok, pid} = Connections.start_link({name, [max_connections: 2, checkin_timeout: 1_500]}) on_exit(fn -> Application.put_env(:tesla, :adapter, adapter) From 24d1ac125c6ae719b3d119f2ec0079dcd74eadc2 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 3 Mar 2020 13:24:19 +0300 Subject: [PATCH 30/88] hiding raise error logic to otp_version module --- lib/pleroma/application.ex | 23 ++++------------------- lib/pleroma/otp_version.ex | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 9b228d6b9..d0b9c3c41 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -42,6 +42,10 @@ def start(_type, _args) do setup_instrumenters() load_custom_modules() + if adapter() == Tesla.Adapter.Gun do + Pleroma.OTPVersion.check!() + end + # Define workers and child supervisors to be supervised children = [ @@ -66,25 +70,6 @@ def start(_type, _args) do Pleroma.Gopher.Server ] - if adapter() == Tesla.Adapter.Gun do - case Pleroma.OTPVersion.check() 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 - 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] diff --git a/lib/pleroma/otp_version.ex b/lib/pleroma/otp_version.ex index 54ceaff47..9ced2d27d 100644 --- a/lib/pleroma/otp_version.ex +++ b/lib/pleroma/otp_version.ex @@ -5,6 +5,26 @@ defmodule Pleroma.OTPVersion do @type check_status() :: :ok | :undefined | {:error, String.t()} + @spec check!() :: :ok | no_return() + def check! do + case check() 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 + end + @spec check() :: check_status() def check do # OTP Version https://erlang.org/doc/system_principles/versions.html#otp-version From d0e4d3ca3b9d8b8ed00d58e9e1c2a05ab561326c Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 3 Mar 2020 14:56:49 +0300 Subject: [PATCH 31/88] removing unnecessary with comment in tesla client impovement --- lib/pleroma/pool/connections.ex | 40 +++++++++++------------ lib/pleroma/reverse_proxy/client/tesla.ex | 8 +++-- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/lib/pleroma/pool/connections.ex b/lib/pleroma/pool/connections.ex index 05fa8f7ad..bde3ffd13 100644 --- a/lib/pleroma/pool/connections.ex +++ b/lib/pleroma/pool/connections.ex @@ -36,17 +36,16 @@ def checkin(url, name) when is_binary(url), do: checkin(URI.parse(url), name) def checkin(%URI{} = uri, name) do timeout = Config.get([:connections_pool, :checkin_timeout], 250) - GenServer.call( - name, - {:checkin, uri}, - timeout - ) + GenServer.call(name, {:checkin, uri}, timeout) end @spec alive?(atom()) :: boolean() def alive?(name) do - pid = Process.whereis(name) - if pid, do: Process.alive?(pid), else: false + if pid = Process.whereis(name) do + Process.alive?(pid) + else + false + end end @spec get_state(atom()) :: t() @@ -131,19 +130,20 @@ def handle_call({:checkin, uri}, from, state) do %{conn: conn, gun_state: :up} = current_conn -> Logger.debug("reusing conn #{key}") - 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 + 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] + }) + + {:reply, conn, state} %{gun_state: :down} -> {:reply, nil, state} diff --git a/lib/pleroma/reverse_proxy/client/tesla.ex b/lib/pleroma/reverse_proxy/client/tesla.ex index 498a905e1..80a0c8972 100644 --- a/lib/pleroma/reverse_proxy/client/tesla.ex +++ b/lib/pleroma/reverse_proxy/client/tesla.ex @@ -18,8 +18,9 @@ defmodule Pleroma.ReverseProxy.Client.Tesla do def request(method, url, headers, body, opts \\ []) do check_adapter() - with opts <- Keyword.merge(opts, body_as: :chunks, mode: :passive), - {:ok, response} <- + opts = Keyword.merge(opts, body_as: :chunks) + + with {:ok, response} <- Pleroma.HTTP.request( method, url, @@ -40,7 +41,8 @@ def request(method, url, headers, body, opts \\ []) do @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 connection was reused, but in tesla were redirects, + # tesla returns new opened connection, which must be closed manually if opts[:old_conn], do: Tesla.Adapter.Gun.close(pid) # if there were redirects we need to checkout old conn conn = opts[:old_conn] || opts[:conn] From 05429730e46b8605544637feebd4c409a4e9ed18 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 3 Mar 2020 15:11:48 +0300 Subject: [PATCH 32/88] unnecessary with --- lib/pleroma/http/http.ex | 51 ++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/lib/pleroma/http/http.ex b/lib/pleroma/http/http.ex index 0235f89ea..f7b0095d7 100644 --- a/lib/pleroma/http/http.ex +++ b/lib/pleroma/http/http.ex @@ -55,33 +55,36 @@ def post(url, body, headers \\ [], options \\ []), @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 && pid do - Process.alive?(pid) - else - false - end + 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) - request_opts = - adapter_opts - |> Enum.into(%{}) - |> Map.put(:env, Pleroma.Config.get([:env])) - |> Map.put(:pool_alive?, pool_alive?) + adapter = Application.get_env(:tesla, :adapter) + client = Tesla.client([Tesla.Middleware.FollowRedirects], adapter) - response = request(client, request, request_opts) + pid = Process.whereis(adapter_opts[:pool]) - Connection.after_request(adapter_opts) + pool_alive? = + if adapter == Tesla.Adapter.Gun && pid do + Process.alive?(pid) + else + false + end - response - 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 @spec request(Client.t(), keyword(), map()) :: {:ok, Env.t()} | {:error, any()} @@ -138,6 +141,4 @@ defp build_request(method, headers, options, url, body, params) do |> Builder.add_param(:query, :query, params) |> Builder.convert_to_keyword() end - - defp tesla_adapter, do: Application.get_env(:tesla, :adapter) end From ee8071f0d5a8a53f6a9ae635d6ea57ce8576e21b Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 3 Mar 2020 15:12:09 +0300 Subject: [PATCH 33/88] removing unused method --- lib/pleroma/http/request_builder.ex | 20 -------------------- test/http/request_builder_test.exs | 17 ----------------- 2 files changed, 37 deletions(-) diff --git a/lib/pleroma/http/request_builder.ex b/lib/pleroma/http/request_builder.ex index 046741d99..5b92ce764 100644 --- a/lib/pleroma/http/request_builder.ex +++ b/lib/pleroma/http/request_builder.ex @@ -49,26 +49,6 @@ def headers(request, headers) do @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 - - """ - @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 - case definitions do - %{^key => location} -> - request - |> add_param(location, key, value) - |> add_optional_params(definitions, tail) - - _ -> - add_optional_params(request, definitions, tail) - end - end - @doc """ Add optional parameters to the request """ diff --git a/test/http/request_builder_test.exs b/test/http/request_builder_test.exs index f87ca11d3..f6eeac6c0 100644 --- a/test/http/request_builder_test.exs +++ b/test/http/request_builder_test.exs @@ -36,23 +36,6 @@ test "send custom user agent" do end end - describe "add_optional_params/3" do - test "don't add if keyword is empty" do - assert RequestBuilder.add_optional_params(%{}, %{}, []) == %{} - end - - test "add query parameter" do - assert RequestBuilder.add_optional_params( - %Request{}, - %{query: :query, body: :body, another: :val}, - [ - {:query, "param1=val1¶m2=val2"}, - {:body, "some body"} - ] - ) == %Request{query: "param1=val1¶m2=val2", body: "some body"} - end - end - describe "add_param/4" do test "add file parameter" do %Request{ From e605e79df9761cef3d9f93c489dd4618c6b70eda Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 3 Mar 2020 15:44:13 +0300 Subject: [PATCH 34/88] simplification of formatting host method case for format_proxy method --- lib/pleroma/gun/conn.ex | 6 ++--- lib/pleroma/http/adapter.ex | 29 +++--------------------- lib/pleroma/http/adapter/gun.ex | 20 +++++++++++++---- test/http/adapter/gun_test.exs | 21 ++++++++++++++++- test/http/adapter_test.exs | 40 +-------------------------------- 5 files changed, 43 insertions(+), 73 deletions(-) diff --git a/lib/pleroma/gun/conn.ex b/lib/pleroma/gun/conn.ex index ddb9f30b0..a33d75558 100644 --- a/lib/pleroma/gun/conn.ex +++ b/lib/pleroma/gun/conn.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Gun.Conn do @@ -131,7 +131,7 @@ defp do_open(uri, %{proxy: {proxy_type, proxy_host, proxy_port}} = opts) do end defp do_open(%URI{host: host, port: port} = uri, opts) do - {_type, host} = Pleroma.HTTP.Adapter.domain_or_ip(host) + host = Pleroma.HTTP.Connection.parse_host(host) with {:ok, conn} <- API.open(host, port, opts), {:ok, _} <- API.await_up(conn, opts[:await_up_timeout]) do @@ -149,7 +149,7 @@ defp do_open(%URI{host: host, port: port} = uri, opts) do end defp destination_opts(%URI{host: host, port: port}) do - {_type, host} = Pleroma.HTTP.Adapter.domain_or_ip(host) + host = Pleroma.HTTP.Connection.parse_host(host) %{host: host, port: port} end diff --git a/lib/pleroma/http/adapter.ex b/lib/pleroma/http/adapter.ex index 32046b1d3..a3b84d8f3 100644 --- a/lib/pleroma/http/adapter.ex +++ b/lib/pleroma/http/adapter.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.HTTP.Adapter do @@ -8,7 +8,6 @@ defmodule Pleroma.HTTP.Adapter do @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 @@ -29,9 +28,8 @@ def after_request(_opts), do: :ok 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 + case Connection.parse_proxy(proxy_url) do + {:ok, host, port} -> {host, port} {:ok, type, host, port} -> {type, host, port} _ -> nil end @@ -40,25 +38,4 @@ def format_proxy(proxy_url) do @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} -> - {:ip, ip} - end - end end diff --git a/lib/pleroma/http/adapter/gun.ex b/lib/pleroma/http/adapter/gun.ex index 908d71898..5e88786bd 100644 --- a/lib/pleroma/http/adapter/gun.ex +++ b/lib/pleroma/http/adapter/gun.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.HTTP.Adapter.Gun do @@ -42,7 +42,7 @@ def after_request(opts) do end defp add_original(opts, %URI{host: host, port: port}) do - formatted_host = Adapter.domain_or_fallback(host) + formatted_host = format_host(host) Keyword.put(opts, :original, "#{formatted_host}:#{port}") end @@ -57,8 +57,7 @@ defp add_scheme_opts(opts, %URI{scheme: "https", host: host, port: port}) do cacertfile: CAStore.file_path(), depth: 20, reuse_sessions: false, - verify_fun: - {&:ssl_verify_hostname.verify_fun/3, [check_hostname: Adapter.domain_or_fallback(host)]}, + verify_fun: {&:ssl_verify_hostname.verify_fun/3, [check_hostname: format_host(host)]}, log_level: :warning ] ] @@ -139,4 +138,17 @@ defp try_to_get_conn(uri, opts) do opts end end + + @spec format_host(String.t()) :: charlist() + def format_host(host) do + host_charlist = to_charlist(host) + + case :inet.parse_address(host_charlist) do + {:error, :einval} -> + :idna.encode(host_charlist) + + {:ok, _ip} -> + host_charlist + end + end end diff --git a/test/http/adapter/gun_test.exs b/test/http/adapter/gun_test.exs index a8dcbae04..a05471ac6 100644 --- a/test/http/adapter/gun_test.exs +++ b/test/http/adapter/gun_test.exs @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.HTTP.Adapter.GunTest do @@ -264,4 +264,23 @@ test "with ipv6" do } = Connections.get_state(:gun_connections) end end + + describe "format_host/1" do + test "with domain" do + assert Gun.format_host("example.com") == 'example.com' + end + + test "with idna domain" do + assert Gun.format_host("ですexample.com") == 'xn--example-183fne.com' + end + + test "with ipv4" do + assert Gun.format_host("127.0.0.1") == '127.0.0.1' + end + + test "with ipv6" do + assert Gun.format_host("2a03:2880:f10c:83:face:b00c:0:25de") == + '2a03:2880:f10c:83:face:b00c:0:25de' + end + end end diff --git a/test/http/adapter_test.exs b/test/http/adapter_test.exs index 37e47dabe..4c805837c 100644 --- a/test/http/adapter_test.exs +++ b/test/http/adapter_test.exs @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.HTTP.AdapterTest do @@ -7,44 +7,6 @@ defmodule Pleroma.HTTP.AdapterTest do 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 From 7d68924e4f7233590457aa7e32a21f082dd0584f Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 3 Mar 2020 16:08:21 +0300 Subject: [PATCH 35/88] naming --- lib/pleroma/gun/conn.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/gun/conn.ex b/lib/pleroma/gun/conn.ex index a33d75558..a8b8c92c1 100644 --- a/lib/pleroma/gun/conn.ex +++ b/lib/pleroma/gun/conn.ex @@ -54,7 +54,7 @@ def open(%URI{} = uri, name, opts) do if Connections.count(name) < opts[:max_connection] do do_open(uri, opts) else - try_do_open(name, uri, opts) + close_least_used_and_do_open(name, uri, opts) end if is_pid(conn_pid) do @@ -159,7 +159,7 @@ defp add_http2_opts(opts, "https", tls_opts) do defp add_http2_opts(opts, _, _), do: opts - defp try_do_open(name, uri, opts) do + defp close_least_used_and_do_open(name, uri, opts) do Logger.debug("try to open conn #{Connections.compose_uri_log(uri)}") with [{close_key, least_used} | _conns] <- From 8fc00b7cbff86885ec99d01821c403a766202659 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 3 Mar 2020 16:27:46 +0300 Subject: [PATCH 36/88] return error if connection failed to open --- lib/pleroma/gun/conn.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/gun/conn.ex b/lib/pleroma/gun/conn.ex index a8b8c92c1..9ae419092 100644 --- a/lib/pleroma/gun/conn.ex +++ b/lib/pleroma/gun/conn.ex @@ -90,7 +90,7 @@ defp do_open(uri, %{proxy: {proxy_host, proxy_port}} = opts) do } #{inspect(error)}" ) - nil + error end end @@ -126,7 +126,7 @@ defp do_open(uri, %{proxy: {proxy_type, proxy_host, proxy_port}} = opts) do } #{inspect(error)}" ) - nil + error end end @@ -144,7 +144,7 @@ defp do_open(%URI{host: host, port: port} = uri, opts) do }" ) - nil + error end end @@ -169,7 +169,7 @@ defp close_least_used_and_do_open(name, uri, opts) do do_open(uri, opts) else - [] -> nil + [] -> {:error, :pool_overflowed} end end end From 7c0ed9302cb13ab44c1bf18017538315dcd0ce2e Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 3 Mar 2020 16:46:20 +0300 Subject: [PATCH 37/88] unnecessary mock --- test/notification_test.exs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/test/notification_test.exs b/test/notification_test.exs index 1c60f6866..56a581810 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -649,13 +649,6 @@ test "notifications are deleted if a remote user is deleted" 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() From 6ebf389d6e6ca5f3e56f9b017531f5f7e301ed3c Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 3 Mar 2020 16:51:49 +0300 Subject: [PATCH 38/88] poolboy timeout fix --- lib/pleroma/http/http.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/http/http.ex b/lib/pleroma/http/http.ex index f7b0095d7..4b774472e 100644 --- a/lib/pleroma/http/http.ex +++ b/lib/pleroma/http/http.ex @@ -102,8 +102,8 @@ def request(%Client{} = client, request, %{pool: pool, timeout: timeout}) do try do :poolboy.transaction( pool, - &Pleroma.Pool.Request.execute(&1, client, request, timeout + 500), - timeout + 1_000 + &Pleroma.Pool.Request.execute(&1, client, request, timeout), + timeout ) rescue e -> From aaa879ce75a62e69a458226e65bef31b0f2ed08c Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 3 Mar 2020 17:27:22 +0300 Subject: [PATCH 39/88] proxy parsing errors --- lib/pleroma/http/connection.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/http/connection.ex b/lib/pleroma/http/connection.ex index e2d7afbbd..bdd062929 100644 --- a/lib/pleroma/http/connection.ex +++ b/lib/pleroma/http/connection.ex @@ -71,15 +71,15 @@ def parse_proxy(proxy) when is_binary(proxy) do else {_, _} -> Logger.warn("parsing port in proxy fail #{inspect(proxy)}") - {:error, :error_parsing_port_in_proxy} + {:error, :invalid_proxy_port} :error -> Logger.warn("parsing port in proxy fail #{inspect(proxy)}") - {:error, :error_parsing_port_in_proxy} + {:error, :invalid_proxy_port} _ -> Logger.warn("parsing proxy fail #{inspect(proxy)}") - {:error, :error_parsing_proxy} + {:error, :invalid_proxy} end end @@ -89,7 +89,7 @@ def parse_proxy(proxy) when is_tuple(proxy) do else _ -> Logger.warn("parsing proxy fail #{inspect(proxy)}") - {:error, :error_parsing_proxy} + {:error, :invalid_proxy} end end From 24bf5c4e89e6f97ed3d53157cead48c04015a51b Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 3 Mar 2020 17:27:56 +0300 Subject: [PATCH 40/88] remove try block from pool request --- lib/pleroma/http/http.ex | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/lib/pleroma/http/http.ex b/lib/pleroma/http/http.ex index 4b774472e..cc0c39400 100644 --- a/lib/pleroma/http/http.ex +++ b/lib/pleroma/http/http.ex @@ -99,23 +99,11 @@ def request(%Client{} = client, request, %{pool_alive?: false}) do end def request(%Client{} = client, request, %{pool: pool, timeout: timeout}) do - try do - :poolboy.transaction( - pool, - &Pleroma.Pool.Request.execute(&1, client, request, timeout), - timeout - ) - 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 + :poolboy.transaction( + pool, + &Pleroma.Pool.Request.execute(&1, client, request, timeout), + timeout + ) end @spec request_try(Client.t(), keyword()) :: {:ok, Env.t()} | {:error, any()} From 3723d723652b747b00fc26054101c15e39a5af18 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 3 Mar 2020 17:32:59 +0300 Subject: [PATCH 41/88] proxy parse tests fix --- test/http/connection_test.exs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/http/connection_test.exs b/test/http/connection_test.exs index 53ccbc9cd..37de11e7a 100644 --- a/test/http/connection_test.exs +++ b/test/http/connection_test.exs @@ -51,31 +51,31 @@ test "as tuple with string host" do 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} + assert Connection.parse_proxy("127.0.0.1") == {:error, :invalid_proxy} end) =~ "parsing proxy fail \"127.0.0.1\"" end test "host without port" do capture_log(fn -> - assert Connection.parse_proxy("localhost") == {:error, :error_parsing_proxy} + assert Connection.parse_proxy("localhost") == {:error, :invalid_proxy} end) =~ "parsing proxy fail \"localhost\"" end test "host with bad port" do capture_log(fn -> - assert Connection.parse_proxy("localhost:port") == {:error, :error_parsing_port_in_proxy} + assert Connection.parse_proxy("localhost:port") == {:error, :invalid_proxy_port} end) =~ "parsing port in proxy fail \"localhost:port\"" end test "ip with bad port" do capture_log(fn -> - assert Connection.parse_proxy("127.0.0.1:15.9") == {:error, :error_parsing_port_in_proxy} + assert Connection.parse_proxy("127.0.0.1:15.9") == {:error, :invalid_proxy_port} end) =~ "parsing port in proxy fail \"127.0.0.1:15.9\"" end test "as tuple without port" do capture_log(fn -> - assert Connection.parse_proxy({:socks5, :localhost}) == {:error, :error_parsing_proxy} + assert Connection.parse_proxy({:socks5, :localhost}) == {:error, :invalid_proxy} end) =~ "parsing proxy fail {:socks5, :localhost}" end From 1ad34bfdbaee7d98167dc7dc7be8b65fd5e6c5f1 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 3 Mar 2020 17:44:04 +0300 Subject: [PATCH 42/88] no try block in checkout connection --- lib/pleroma/http/adapter/gun.ex | 53 ++++++--------------------------- 1 file changed, 9 insertions(+), 44 deletions(-) diff --git a/lib/pleroma/http/adapter/gun.ex b/lib/pleroma/http/adapter/gun.ex index 5e88786bd..30c5c3c16 100644 --- a/lib/pleroma/http/adapter/gun.ex +++ b/lib/pleroma/http/adapter/gun.ex @@ -86,56 +86,21 @@ defp maybe_get_conn(adapter_opts, uri, connection_opts) do end defp try_to_get_conn(uri, opts) do - try do - case Connections.checkin(uri, :gun_connections) do - nil -> - Logger.debug( - "Gun connections pool checkin was not successful. Trying to open conn for next request." - ) - - Task.start(fn -> Pleroma.Gun.Conn.open(uri, :gun_connections, opts) end) - opts - - conn when is_pid(conn) -> - Logger.debug("received conn #{inspect(conn)} #{Connections.compose_uri_log(uri)}") - - opts - |> Keyword.put(:conn, conn) - |> Keyword.put(:close_conn, false) - end - rescue - error -> - Logger.warn( - "Gun connections pool checkin caused error #{Connections.compose_uri_log(uri)} #{ - inspect(error) - }" - ) - - opts - catch - # TODO: here must be no timeouts - :exit, {:timeout, {_, operation, [_, {method, _}, _]}} -> - {:message_queue_len, messages_len} = - :gun_connections - |> Process.whereis() - |> Process.info(:message_queue_len) - - Logger.warn( - "Gun connections pool checkin with timeout error for #{operation} #{method} #{ - Connections.compose_uri_log(uri) - }. Messages length: #{messages_len}" + case Connections.checkin(uri, :gun_connections) do + nil -> + Logger.debug( + "Gun connections pool checkin was not successful. Trying to open conn for next request." ) + Task.start(fn -> Pleroma.Gun.Conn.open(uri, :gun_connections, opts) end) opts - :exit, error -> - Logger.warn( - "Gun pool checkin exited with error #{Connections.compose_uri_log(uri)} #{ - inspect(error) - }" - ) + conn when is_pid(conn) -> + Logger.debug("received conn #{inspect(conn)} #{Connections.compose_uri_log(uri)}") opts + |> Keyword.put(:conn, conn) + |> Keyword.put(:close_conn, false) end end From 8854770fc4e9079131a0897d5fb6c0ccccf98bc6 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 3 Mar 2020 18:01:35 +0300 Subject: [PATCH 43/88] retry and retry_timeout settings default change --- config/config.exs | 4 ++-- docs/configuration/cheatsheet.md | 4 ++-- lib/pleroma/gun/conn.ex | 4 ++-- lib/pleroma/http/adapter/gun.ex | 3 ++- lib/pleroma/pool/connections.ex | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/config/config.exs b/config/config.exs index 661dfad20..f0dab24b5 100644 --- a/config/config.exs +++ b/config/config.exs @@ -609,8 +609,8 @@ config :pleroma, :connections_pool, checkin_timeout: 250, max_connections: 250, - retry: 0, - retry_timeout: 100, + retry: 1, + retry_timeout: 1000, await_up_timeout: 5_000 config :pleroma, :pools, diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index ef3cc40e6..a39a7436d 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -406,8 +406,8 @@ It will increase memory usage, but federation would work faster. * `:checkin_timeout` - timeout to checkin 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. +* `:retry` - number of retries, while `gun` will try to reconnect if connections goes down. Default: 1. +* `:retry_timeout` - timeout while `gun` will try to reconnect. Default: 1000ms. * `:await_up_timeout` - timeout while `gun` will wait until connection is up. Default: 5000ms. ### :pools diff --git a/lib/pleroma/gun/conn.ex b/lib/pleroma/gun/conn.ex index 9ae419092..d73bec360 100644 --- a/lib/pleroma/gun/conn.ex +++ b/lib/pleroma/gun/conn.ex @@ -42,8 +42,8 @@ def open(%URI{} = uri, name, opts) do opts = opts |> Enum.into(%{}) - |> Map.put_new(:retry, pool_opts[:retry] || 0) - |> Map.put_new(:retry_timeout, pool_opts[:retry_timeout] || 100) + |> Map.put_new(:retry, pool_opts[:retry] || 1) + |> Map.put_new(:retry_timeout, pool_opts[:retry_timeout] || 1000) |> Map.put_new(:await_up_timeout, pool_opts[:await_up_timeout] || 5_000) key = "#{uri.scheme}:#{uri.host}:#{uri.port}" diff --git a/lib/pleroma/http/adapter/gun.ex b/lib/pleroma/http/adapter/gun.ex index 30c5c3c16..ecf9c5b62 100644 --- a/lib/pleroma/http/adapter/gun.ex +++ b/lib/pleroma/http/adapter/gun.ex @@ -15,7 +15,8 @@ defmodule Pleroma.HTTP.Adapter.Gun do connect_timeout: 5_000, domain_lookup_timeout: 5_000, tls_handshake_timeout: 5_000, - retry: 0, + retry: 1, + retry_timeout: 1000, await_up_timeout: 5_000 ] diff --git a/lib/pleroma/pool/connections.ex b/lib/pleroma/pool/connections.ex index bde3ffd13..0f7a1bfd8 100644 --- a/lib/pleroma/pool/connections.ex +++ b/lib/pleroma/pool/connections.ex @@ -219,7 +219,7 @@ def handle_info({:gun_up, conn_pid, _protocol}, state) do @impl true def handle_info({:gun_down, conn_pid, _protocol, _reason, _killed}, state) do - retries = Config.get([:connections_pool, :retry], 0) + retries = Config.get([:connections_pool, :retry], 1) # we can't get info on this pid, because pid is dead state = with {key, conn} <- find_conn(state.conns, conn_pid), From f98ee730f01de528797e38f27964b69a465662c4 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 3 Mar 2020 18:53:44 +0300 Subject: [PATCH 44/88] adapter renaming to adapter_helper --- .../http/{adapter.ex => adapter_helper.ex} | 2 +- .../http/{adapter => adapter_helper}/gun.ex | 8 +++--- .../{adapter => adapter_helper}/hackney.ex | 6 ++-- lib/pleroma/http/connection.ex | 8 +++--- .../{adapter => adapter_helper}/gun_test.exs | 4 +-- .../hackney_test.exs | 4 +-- test/http/adapter_helper_test.exs | 28 +++++++++++++++++++ test/http/adapter_test.exs | 27 ------------------ 8 files changed, 44 insertions(+), 43 deletions(-) rename lib/pleroma/http/{adapter.ex => adapter_helper.ex} (96%) rename lib/pleroma/http/{adapter => adapter_helper}/gun.ex (94%) rename lib/pleroma/http/{adapter => adapter_helper}/hackney.ex (87%) rename test/http/{adapter => adapter_helper}/gun_test.exs (99%) rename test/http/{adapter => adapter_helper}/hackney_test.exs (93%) create mode 100644 test/http/adapter_helper_test.exs delete mode 100644 test/http/adapter_test.exs diff --git a/lib/pleroma/http/adapter.ex b/lib/pleroma/http/adapter_helper.ex similarity index 96% rename from lib/pleroma/http/adapter.ex rename to lib/pleroma/http/adapter_helper.ex index a3b84d8f3..2c13666ec 100644 --- a/lib/pleroma/http/adapter.ex +++ b/lib/pleroma/http/adapter_helper.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.HTTP.Adapter do +defmodule Pleroma.HTTP.AdapterHelper do alias Pleroma.HTTP.Connection @type proxy :: diff --git a/lib/pleroma/http/adapter/gun.ex b/lib/pleroma/http/adapter_helper/gun.ex similarity index 94% rename from lib/pleroma/http/adapter/gun.ex rename to lib/pleroma/http/adapter_helper/gun.ex index ecf9c5b62..b3298ec7f 100644 --- a/lib/pleroma/http/adapter/gun.ex +++ b/lib/pleroma/http/adapter_helper/gun.ex @@ -2,10 +2,10 @@ # Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.HTTP.Adapter.Gun do - @behaviour Pleroma.HTTP.Adapter +defmodule Pleroma.HTTP.AdapterHelper.Gun do + @behaviour Pleroma.HTTP.AdapterHelper - alias Pleroma.HTTP.Adapter + alias Pleroma.HTTP.AdapterHelper require Logger @@ -28,7 +28,7 @@ def options(connection_opts \\ [], %URI{} = uri) do |> Keyword.merge(Pleroma.Config.get([:http, :adapter], [])) |> add_original(uri) |> add_scheme_opts(uri) - |> Adapter.maybe_add_proxy(Adapter.format_proxy(proxy)) + |> AdapterHelper.maybe_add_proxy(AdapterHelper.format_proxy(proxy)) |> maybe_get_conn(uri, connection_opts) end diff --git a/lib/pleroma/http/adapter/hackney.ex b/lib/pleroma/http/adapter_helper/hackney.ex similarity index 87% rename from lib/pleroma/http/adapter/hackney.ex rename to lib/pleroma/http/adapter_helper/hackney.ex index 00db30083..a0e161eaa 100644 --- a/lib/pleroma/http/adapter/hackney.ex +++ b/lib/pleroma/http/adapter_helper/hackney.ex @@ -1,5 +1,5 @@ -defmodule Pleroma.HTTP.Adapter.Hackney do - @behaviour Pleroma.HTTP.Adapter +defmodule Pleroma.HTTP.AdapterHelper.Hackney do + @behaviour Pleroma.HTTP.AdapterHelper @defaults [ connect_timeout: 10_000, @@ -17,7 +17,7 @@ def options(connection_opts \\ [], %URI{} = uri) do |> Keyword.merge(Pleroma.Config.get([:http, :adapter], [])) |> Keyword.merge(connection_opts) |> add_scheme_opts(uri) - |> Pleroma.HTTP.Adapter.maybe_add_proxy(proxy) + |> Pleroma.HTTP.AdapterHelper.maybe_add_proxy(proxy) end defp add_scheme_opts(opts, %URI{scheme: "http"}), do: opts diff --git a/lib/pleroma/http/connection.ex b/lib/pleroma/http/connection.ex index bdd062929..dc2761182 100644 --- a/lib/pleroma/http/connection.ex +++ b/lib/pleroma/http/connection.ex @@ -18,7 +18,7 @@ defmodule Pleroma.HTTP.Connection do require Logger alias Pleroma.Config - alias Pleroma.HTTP.Adapter + alias Pleroma.HTTP.AdapterHelper @doc """ Merge default connection & adapter options with received ones. @@ -50,9 +50,9 @@ 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 + Tesla.Adapter.Gun -> AdapterHelper.Gun + Tesla.Adapter.Hackney -> AdapterHelper.Hackney + _ -> AdapterHelper end end diff --git a/test/http/adapter/gun_test.exs b/test/http/adapter_helper/gun_test.exs similarity index 99% rename from test/http/adapter/gun_test.exs rename to test/http/adapter_helper/gun_test.exs index a05471ac6..bc7e3f0e0 100644 --- a/test/http/adapter/gun_test.exs +++ b/test/http/adapter_helper/gun_test.exs @@ -2,13 +2,13 @@ # Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.HTTP.Adapter.GunTest do +defmodule Pleroma.HTTP.AdapterHelper.GunTest do use ExUnit.Case, async: true use Pleroma.Tests.Helpers import ExUnit.CaptureLog alias Pleroma.Config alias Pleroma.Gun.Conn - alias Pleroma.HTTP.Adapter.Gun + alias Pleroma.HTTP.AdapterHelper.Gun alias Pleroma.Pool.Connections setup_all do diff --git a/test/http/adapter/hackney_test.exs b/test/http/adapter_helper/hackney_test.exs similarity index 93% rename from test/http/adapter/hackney_test.exs rename to test/http/adapter_helper/hackney_test.exs index 35cb58125..82f5a7883 100644 --- a/test/http/adapter/hackney_test.exs +++ b/test/http/adapter_helper/hackney_test.exs @@ -2,12 +2,12 @@ # Copyright © 2017-2019 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.HTTP.Adapter.HackneyTest do +defmodule Pleroma.HTTP.AdapterHelper.HackneyTest do use ExUnit.Case use Pleroma.Tests.Helpers alias Pleroma.Config - alias Pleroma.HTTP.Adapter.Hackney + alias Pleroma.HTTP.AdapterHelper.Hackney setup_all do uri = URI.parse("http://domain.com") diff --git a/test/http/adapter_helper_test.exs b/test/http/adapter_helper_test.exs new file mode 100644 index 000000000..24d501ad5 --- /dev/null +++ b/test/http/adapter_helper_test.exs @@ -0,0 +1,28 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HTTP.AdapterHelperTest do + use ExUnit.Case, async: true + + alias Pleroma.HTTP.AdapterHelper + + describe "format_proxy/1" do + test "with nil" do + assert AdapterHelper.format_proxy(nil) == nil + end + + test "with string" do + assert AdapterHelper.format_proxy("127.0.0.1:8123") == {{127, 0, 0, 1}, 8123} + end + + test "localhost with port" do + assert AdapterHelper.format_proxy("localhost:8123") == {'localhost', 8123} + end + + test "tuple" do + assert AdapterHelper.format_proxy({:socks4, :localhost, 9050}) == + {:socks4, 'localhost', 9050} + end + end +end diff --git a/test/http/adapter_test.exs b/test/http/adapter_test.exs deleted file mode 100644 index 4c805837c..000000000 --- a/test/http/adapter_test.exs +++ /dev/null @@ -1,27 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.HTTP.AdapterTest do - use ExUnit.Case, async: true - - alias Pleroma.HTTP.Adapter - - 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 From 23f407bf093723344e63eba6a63f5cd58aa7313e Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 3 Mar 2020 18:57:16 +0300 Subject: [PATCH 45/88] don't test gun itself --- test/gun/gun_test.exs | 39 --------------------------------------- 1 file changed, 39 deletions(-) delete mode 100644 test/gun/gun_test.exs diff --git a/test/gun/gun_test.exs b/test/gun/gun_test.exs deleted file mode 100644 index 9f3e0f938..000000000 --- a/test/gun/gun_test.exs +++ /dev/null @@ -1,39 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# 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) - - {:ok, pid} = Task.start(fn -> Process.sleep(50) end) - - :ok = :gun.set_owner(conn, pid) - - assert :gun.info(conn).owner == pid - 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 From 884d9710b209cc9981c7de61d4e95fd26cd83820 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 3 Mar 2020 19:24:14 +0300 Subject: [PATCH 46/88] refactoring for gun api modules --- config/test.exs | 2 +- lib/pleroma/gun/api.ex | 46 ++++++++++----- lib/pleroma/gun/conn.ex | 22 +++---- lib/pleroma/gun/gun.ex | 49 +++++---------- lib/pleroma/pool/connections.ex | 10 ++-- test/http/adapter_helper/gun_test.exs | 2 +- test/http/connection_test.exs | 2 +- test/http_test.exs | 4 +- test/pool/connections_test.exs | 7 +-- test/reverse_proxy/client/tesla_test.exs | 4 +- test/reverse_proxy/reverse_proxy_test.exs | 4 +- .../api/mock.ex => test/support/gun_mock.ex | 59 ++++++++++--------- 12 files changed, 104 insertions(+), 107 deletions(-) rename lib/pleroma/gun/api/mock.ex => test/support/gun_mock.ex (79%) diff --git a/config/test.exs b/config/test.exs index 7cc669c19..bce9dd4aa 100644 --- a/config/test.exs +++ b/config/test.exs @@ -90,7 +90,7 @@ config :pleroma, :modules, runtime_dir: "test/fixtures/modules" -config :pleroma, Pleroma.Gun.API, Pleroma.Gun.API.Mock +config :pleroma, Pleroma.Gun, Pleroma.GunMock config :pleroma, Pleroma.Emails.NewUsersDigestEmail, enabled: true diff --git a/lib/pleroma/gun/api.ex b/lib/pleroma/gun/api.ex index f79c9f443..76aac5874 100644 --- a/lib/pleroma/gun/api.ex +++ b/lib/pleroma/gun/api.ex @@ -3,27 +3,43 @@ # 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, pos_integer()) :: {:ok, atom()} | {:error, atom()} - @callback connect(pid(), map()) :: reference() - @callback await(pid(), reference()) :: {:response, :fin, 200, []} - @callback set_owner(pid(), pid()) :: :ok + @behaviour Pleroma.Gun - def open(host, port, opts), do: api().open(host, port, opts) + alias Pleroma.Gun - def info(pid), do: api().info(pid) + @gun_keys [ + :connect_timeout, + :http_opts, + :http2_opts, + :protocols, + :retry, + :retry_timeout, + :trace, + :transport, + :tls_opts, + :tcp_opts, + :socks_opts, + :ws_opts + ] - def close(pid), do: api().close(pid) + @impl Gun + def open(host, port, opts \\ %{}), do: :gun.open(host, port, Map.take(opts, @gun_keys)) - def await_up(pid, timeout \\ 5_000), do: api().await_up(pid, timeout) + @impl Gun + defdelegate info(pid), to: :gun - def connect(pid, opts), do: api().connect(pid, opts) + @impl Gun + defdelegate close(pid), to: :gun - def await(pid, ref), do: api().await(pid, ref) + @impl Gun + defdelegate await_up(pid, timeout \\ 5_000), to: :gun - def set_owner(pid, owner), do: api().set_owner(pid, owner) + @impl Gun + defdelegate connect(pid, opts), to: :gun - defp api, do: Pleroma.Config.get([Pleroma.Gun.API], Pleroma.Gun) + @impl Gun + defdelegate await(pid, ref), to: :gun + + @impl Gun + defdelegate set_owner(pid, owner), to: :gun end diff --git a/lib/pleroma/gun/conn.ex b/lib/pleroma/gun/conn.ex index d73bec360..319718690 100644 --- a/lib/pleroma/gun/conn.ex +++ b/lib/pleroma/gun/conn.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Gun.Conn do @moduledoc """ Struct for gun connection data """ - alias Pleroma.Gun.API + alias Pleroma.Gun alias Pleroma.Pool.Connections require Logger @@ -65,7 +65,7 @@ def open(%URI{} = uri, name, opts) do last_reference: :os.system_time(:second) } - :ok = API.set_owner(conn_pid, Process.whereis(name)) + :ok = Gun.set_owner(conn_pid, Process.whereis(name)) Connections.add_conn(name, key, conn) end end @@ -77,10 +77,10 @@ defp do_open(uri, %{proxy: {proxy_host, proxy_port}} = opts) do |> 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, opts[:await_up_timeout]), - stream <- API.connect(conn, connect_opts), - {:response, :fin, 200, _} <- API.await(conn, stream) do + {:ok, conn} <- Gun.open(proxy_host, proxy_port, open_opts), + {:ok, _} <- Gun.await_up(conn, opts[:await_up_timeout]), + stream <- Gun.connect(conn, connect_opts), + {:response, :fin, 200, _} <- Gun.await(conn, stream) do conn else error -> @@ -115,8 +115,8 @@ defp do_open(uri, %{proxy: {proxy_type, proxy_host, proxy_port}} = opts) do |> 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, opts[:await_up_timeout]) do + with {:ok, conn} <- Gun.open(proxy_host, proxy_port, opts), + {:ok, _} <- Gun.await_up(conn, opts[:await_up_timeout]) do conn else error -> @@ -133,8 +133,8 @@ defp do_open(uri, %{proxy: {proxy_type, proxy_host, proxy_port}} = opts) do defp do_open(%URI{host: host, port: port} = uri, opts) do host = Pleroma.HTTP.Connection.parse_host(host) - with {:ok, conn} <- API.open(host, port, opts), - {:ok, _} <- API.await_up(conn, opts[:await_up_timeout]) do + with {:ok, conn} <- Gun.open(host, port, opts), + {:ok, _} <- Gun.await_up(conn, opts[:await_up_timeout]) do conn else error -> @@ -164,7 +164,7 @@ defp close_least_used_and_do_open(name, uri, opts) do with [{close_key, least_used} | _conns] <- Connections.get_unused_conns(name), - :ok <- Pleroma.Gun.API.close(least_used.conn) do + :ok <- Gun.close(least_used.conn) do Connections.remove_conn(name, close_key) do_open(uri, opts) diff --git a/lib/pleroma/gun/gun.ex b/lib/pleroma/gun/gun.ex index da82983b1..35390bb11 100644 --- a/lib/pleroma/gun/gun.ex +++ b/lib/pleroma/gun/gun.ex @@ -3,46 +3,27 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Gun do - @behaviour Pleroma.Gun.API + @callback open(charlist(), pos_integer(), map()) :: {:ok, pid()} + @callback info(pid()) :: map() + @callback close(pid()) :: :ok + @callback await_up(pid, pos_integer()) :: {:ok, atom()} | {:error, atom()} + @callback connect(pid(), map()) :: reference() + @callback await(pid(), reference()) :: {:response, :fin, 200, []} + @callback set_owner(pid(), pid()) :: :ok - alias Pleroma.Gun.API + def open(host, port, opts), do: api().open(host, port, opts) - @gun_keys [ - :connect_timeout, - :http_opts, - :http2_opts, - :protocols, - :retry, - :retry_timeout, - :trace, - :transport, - :tls_opts, - :tcp_opts, - :socks_opts, - :ws_opts - ] + def info(pid), do: api().info(pid) - @impl API - def open(host, port, opts \\ %{}), do: :gun.open(host, port, Map.take(opts, @gun_keys)) + def close(pid), do: api().close(pid) - @impl API - defdelegate info(pid), to: :gun + def await_up(pid, timeout \\ 5_000), do: api().await_up(pid, timeout) - @impl API - defdelegate close(pid), to: :gun + def connect(pid, opts), do: api().connect(pid, opts) - @impl API - defdelegate await_up(pid, timeout \\ 5_000), to: :gun + def await(pid, ref), do: api().await(pid, ref) - @impl API - defdelegate connect(pid, opts), to: :gun + def set_owner(pid, owner), do: api().set_owner(pid, owner) - @impl API - defdelegate await(pid, ref), to: :gun - - @spec flush(pid() | reference()) :: :ok - defdelegate flush(pid), to: :gun - - @impl API - defdelegate set_owner(pid, owner), to: :gun + defp api, do: Pleroma.Config.get([Pleroma.Gun], Pleroma.Gun.API) end diff --git a/lib/pleroma/pool/connections.ex b/lib/pleroma/pool/connections.ex index 0f7a1bfd8..92179fbfc 100644 --- a/lib/pleroma/pool/connections.ex +++ b/lib/pleroma/pool/connections.ex @@ -19,7 +19,7 @@ defmodule Pleroma.Pool.Connections do defstruct conns: %{}, opts: [] - alias Pleroma.Gun.API + alias Pleroma.Gun @spec start_link({atom(), keyword()}) :: {:ok, pid()} def start_link({name, opts}) do @@ -209,7 +209,7 @@ def handle_info({:gun_up, conn_pid, _protocol}, state) do nil -> Logger.debug(":gun_up message for conn which is not found in state") - :ok = API.close(conn_pid) + :ok = Gun.close(conn_pid) state end @@ -226,7 +226,7 @@ def handle_info({:gun_down, conn_pid, _protocol, _reason, _killed}, state) do {true, key} <- {Process.alive?(conn_pid), key} do if conn.retries == retries do Logger.debug("closing conn if retries is eq #{inspect(conn_pid)}") - :ok = API.close(conn.conn) + :ok = Gun.close(conn.conn) put_in( state.conns, @@ -252,7 +252,7 @@ def handle_info({:gun_down, conn_pid, _protocol, _reason, _killed}, state) do nil -> Logger.debug(":gun_down message for conn which is not found in state") - :ok = API.close(conn_pid) + :ok = Gun.close(conn_pid) state end @@ -287,7 +287,7 @@ def handle_info({:DOWN, _ref, :process, conn_pid, reason}, state) do 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) + %{origin_host: origin_host, origin_scheme: scheme, origin_port: port} = Gun.info(pid) host = case :inet.ntoa(origin_host) do diff --git a/test/http/adapter_helper/gun_test.exs b/test/http/adapter_helper/gun_test.exs index bc7e3f0e0..66ca416d9 100644 --- a/test/http/adapter_helper/gun_test.exs +++ b/test/http/adapter_helper/gun_test.exs @@ -12,7 +12,7 @@ defmodule Pleroma.HTTP.AdapterHelper.GunTest do alias Pleroma.Pool.Connections setup_all do - {:ok, _} = Registry.start_link(keys: :unique, name: Pleroma.Gun.API.Mock) + {:ok, _} = Registry.start_link(keys: :unique, name: Pleroma.GunMock) :ok end diff --git a/test/http/connection_test.exs b/test/http/connection_test.exs index 37de11e7a..3f32898cb 100644 --- a/test/http/connection_test.exs +++ b/test/http/connection_test.exs @@ -10,7 +10,7 @@ defmodule Pleroma.HTTP.ConnectionTest do alias Pleroma.HTTP.Connection setup_all do - {:ok, _} = Registry.start_link(keys: :unique, name: Pleroma.Gun.API.Mock) + {:ok, _} = Registry.start_link(keys: :unique, name: Pleroma.GunMock) :ok end diff --git a/test/http_test.exs b/test/http_test.exs index 83c27f6e1..d45d34f32 100644 --- a/test/http_test.exs +++ b/test/http_test.exs @@ -61,8 +61,8 @@ test "returns successfully result" do describe "connection pools" do @describetag :integration - clear_config(Pleroma.Gun.API) do - Pleroma.Config.put(Pleroma.Gun.API, Pleroma.Gun) + clear_config(Pleroma.Gun) do + Pleroma.Config.put(Pleroma.Gun, Pleroma.Gun.API) end test "gun" do diff --git a/test/pool/connections_test.exs b/test/pool/connections_test.exs index a084f31b9..31dd5f6fa 100644 --- a/test/pool/connections_test.exs +++ b/test/pool/connections_test.exs @@ -6,12 +6,11 @@ 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, _} = Registry.start_link(keys: :unique, name: Pleroma.GunMock) :ok end @@ -439,8 +438,8 @@ test "remove frequently used and idle", %{name: name} do describe "integration test" do @describetag :integration - clear_config(API) do - Pleroma.Config.put(API, Pleroma.Gun) + clear_config(Pleroma.Gun) do + Pleroma.Config.put(Pleroma.Gun, Pleroma.Gun.API) end test "opens connection and change owner", %{name: name} do diff --git a/test/reverse_proxy/client/tesla_test.exs b/test/reverse_proxy/client/tesla_test.exs index 231271b0d..78bd31530 100644 --- a/test/reverse_proxy/client/tesla_test.exs +++ b/test/reverse_proxy/client/tesla_test.exs @@ -8,8 +8,8 @@ defmodule Pleroma.ReverseProxy.Client.TeslaTest do alias Pleroma.ReverseProxy.Client @moduletag :integration - clear_config_all(Pleroma.Gun.API) do - Pleroma.Config.put(Pleroma.Gun.API, Pleroma.Gun) + clear_config_all(Pleroma.Gun) do + Pleroma.Config.put(Pleroma.Gun, Pleroma.Gun.API) end setup do diff --git a/test/reverse_proxy/reverse_proxy_test.exs b/test/reverse_proxy/reverse_proxy_test.exs index f61fc02c5..8e72698ee 100644 --- a/test/reverse_proxy/reverse_proxy_test.exs +++ b/test/reverse_proxy/reverse_proxy_test.exs @@ -349,8 +349,8 @@ test "with content-disposition header", %{conn: conn} 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) + clear_config(Pleroma.Gun) do + Pleroma.Config.put(Pleroma.Gun, Pleroma.Gun.API) end setup do diff --git a/lib/pleroma/gun/api/mock.ex b/test/support/gun_mock.ex similarity index 79% rename from lib/pleroma/gun/api/mock.ex rename to test/support/gun_mock.ex index 6d24b0e69..e13afd08c 100644 --- a/lib/pleroma/gun/api/mock.ex +++ b/test/support/gun_mock.ex @@ -2,16 +2,17 @@ # Copyright © 2017-2019 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Gun.API.Mock do - @behaviour Pleroma.Gun.API +defmodule Pleroma.GunMock do + @behaviour Pleroma.Gun - alias Pleroma.Gun.API + alias Pleroma.Gun + alias Pleroma.GunMock - @impl API + @impl Gun 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, %{ + Registry.register(GunMock, conn_pid, %{ origin_scheme: "https", origin_host: 'some-domain.com', origin_port: 443 @@ -20,7 +21,7 @@ def open('some-domain.com', 443, _) do {:ok, conn_pid} end - @impl API + @impl Gun 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 @@ -28,7 +29,7 @@ def open(ip, port, _) scheme = if port == 443, do: "https", else: "http" - Registry.register(API.Mock, conn_pid, %{ + Registry.register(GunMock, conn_pid, %{ origin_scheme: scheme, origin_host: ip, origin_port: port @@ -37,7 +38,7 @@ def open(ip, port, _) {:ok, conn_pid} end - @impl API + @impl Gun def open('localhost', 1234, %{ protocols: [:socks], proxy: {:socks5, 'localhost', 1234}, @@ -45,7 +46,7 @@ def open('localhost', 1234, %{ }) do {:ok, conn_pid} = Task.start_link(fn -> Process.sleep(1_000) end) - Registry.register(API.Mock, conn_pid, %{ + Registry.register(GunMock, conn_pid, %{ origin_scheme: "http", origin_host: 'proxy-socks.com', origin_port: 80 @@ -54,7 +55,7 @@ def open('localhost', 1234, %{ {:ok, conn_pid} end - @impl API + @impl Gun def open('localhost', 1234, %{ protocols: [:socks], proxy: {:socks4, 'localhost', 1234}, @@ -69,7 +70,7 @@ def open('localhost', 1234, %{ }) do {:ok, conn_pid} = Task.start_link(fn -> Process.sleep(1_000) end) - Registry.register(API.Mock, conn_pid, %{ + Registry.register(GunMock, conn_pid, %{ origin_scheme: "https", origin_host: 'proxy-socks.com', origin_port: 443 @@ -78,14 +79,14 @@ def open('localhost', 1234, %{ {:ok, conn_pid} end - @impl API + @impl Gun def open('gun-not-up.com', 80, _opts), do: {:error, :timeout} - @impl API + @impl Gun 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, %{ + Registry.register(GunMock, conn_pid, %{ origin_scheme: "https", origin_host: 'example.com', origin_port: 443 @@ -94,11 +95,11 @@ def open('example.com', port, _) when port in [443, 115] do {:ok, conn_pid} end - @impl API + @impl Gun def open(domain, 80, _) do {:ok, conn_pid} = Task.start_link(fn -> Process.sleep(1_000) end) - Registry.register(API.Mock, conn_pid, %{ + Registry.register(GunMock, conn_pid, %{ origin_scheme: "http", origin_host: domain, origin_port: 80 @@ -107,48 +108,48 @@ def open(domain, 80, _) do {:ok, conn_pid} end - @impl API + @impl Gun def open({127, 0, 0, 1}, 8123, _) do Task.start_link(fn -> Process.sleep(1_000) end) end - @impl API + @impl Gun def open('localhost', 9050, _) do Task.start_link(fn -> Process.sleep(1_000) end) end - @impl API + @impl Gun def await_up(_pid, _timeout), do: {:ok, :http} - @impl API + @impl Gun def set_owner(_pid, _owner), do: :ok - @impl API + @impl Gun def connect(pid, %{host: _, port: 80}) do ref = make_ref() - Registry.register(API.Mock, ref, pid) + Registry.register(GunMock, ref, pid) ref end - @impl API + @impl Gun def connect(pid, %{host: _, port: 443, protocols: [:http2], transport: :tls}) do ref = make_ref() - Registry.register(API.Mock, ref, pid) + Registry.register(GunMock, ref, pid) ref end - @impl API + @impl Gun def await(pid, ref) do - [{_, ^pid}] = Registry.lookup(API.Mock, ref) + [{_, ^pid}] = Registry.lookup(GunMock, ref) {:response, :fin, 200, []} end - @impl API + @impl Gun def info(pid) do - [{_, info}] = Registry.lookup(API.Mock, pid) + [{_, info}] = Registry.lookup(GunMock, pid) info end - @impl API + @impl Gun def close(_pid), do: :ok end From d9c5ae7c09c7cbf3f4f66e01b7ed69a3d6388916 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 3 Mar 2020 17:16:24 -0600 Subject: [PATCH 47/88] Update Copyrights for gun related files --- lib/pleroma/gun/api.ex | 2 +- lib/pleroma/gun/gun.ex | 2 +- lib/pleroma/http/request.ex | 2 +- lib/pleroma/pool/connections.ex | 2 +- lib/pleroma/pool/pool.ex | 2 +- lib/pleroma/pool/request.ex | 2 +- lib/pleroma/pool/supervisor.ex | 2 +- lib/pleroma/reverse_proxy/client/hackney.ex | 2 +- lib/pleroma/reverse_proxy/client/tesla.ex | 2 +- test/http/adapter_helper/hackney_test.exs | 2 +- test/http/connection_test.exs | 2 +- test/pool/connections_test.exs | 2 +- test/reverse_proxy/client/tesla_test.exs | 2 +- test/support/gun_mock.ex | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/pleroma/gun/api.ex b/lib/pleroma/gun/api.ex index 76aac5874..f51cd7db8 100644 --- a/lib/pleroma/gun/api.ex +++ b/lib/pleroma/gun/api.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Gun.API do diff --git a/lib/pleroma/gun/gun.ex b/lib/pleroma/gun/gun.ex index 35390bb11..81855e89e 100644 --- a/lib/pleroma/gun/gun.ex +++ b/lib/pleroma/gun/gun.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Gun do diff --git a/lib/pleroma/http/request.ex b/lib/pleroma/http/request.ex index 891d88d53..761bd6ccf 100644 --- a/lib/pleroma/http/request.ex +++ b/lib/pleroma/http/request.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.HTTP.Request do diff --git a/lib/pleroma/pool/connections.ex b/lib/pleroma/pool/connections.ex index 92179fbfc..f1fab2a24 100644 --- a/lib/pleroma/pool/connections.ex +++ b/lib/pleroma/pool/connections.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Pool.Connections do diff --git a/lib/pleroma/pool/pool.ex b/lib/pleroma/pool/pool.ex index a7ae64ce4..21a6fbbc5 100644 --- a/lib/pleroma/pool/pool.ex +++ b/lib/pleroma/pool/pool.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Pool do diff --git a/lib/pleroma/pool/request.ex b/lib/pleroma/pool/request.ex index 2c3574561..cce309599 100644 --- a/lib/pleroma/pool/request.ex +++ b/lib/pleroma/pool/request.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Pool.Request do diff --git a/lib/pleroma/pool/supervisor.ex b/lib/pleroma/pool/supervisor.ex index 32be2264d..f436849ac 100644 --- a/lib/pleroma/pool/supervisor.ex +++ b/lib/pleroma/pool/supervisor.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Pool.Supervisor do diff --git a/lib/pleroma/reverse_proxy/client/hackney.ex b/lib/pleroma/reverse_proxy/client/hackney.ex index e41560ab0..e84118a90 100644 --- a/lib/pleroma/reverse_proxy/client/hackney.ex +++ b/lib/pleroma/reverse_proxy/client/hackney.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.ReverseProxy.Client.Hackney do diff --git a/lib/pleroma/reverse_proxy/client/tesla.ex b/lib/pleroma/reverse_proxy/client/tesla.ex index 80a0c8972..dbc6b66a3 100644 --- a/lib/pleroma/reverse_proxy/client/tesla.ex +++ b/lib/pleroma/reverse_proxy/client/tesla.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.ReverseProxy.Client.Tesla do diff --git a/test/http/adapter_helper/hackney_test.exs b/test/http/adapter_helper/hackney_test.exs index 82f5a7883..3306616ef 100644 --- a/test/http/adapter_helper/hackney_test.exs +++ b/test/http/adapter_helper/hackney_test.exs @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.HTTP.AdapterHelper.HackneyTest do diff --git a/test/http/connection_test.exs b/test/http/connection_test.exs index 3f32898cb..5c1ecda0b 100644 --- a/test/http/connection_test.exs +++ b/test/http/connection_test.exs @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.HTTP.ConnectionTest do diff --git a/test/pool/connections_test.exs b/test/pool/connections_test.exs index 31dd5f6fa..963fae665 100644 --- a/test/pool/connections_test.exs +++ b/test/pool/connections_test.exs @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Pool.ConnectionsTest do diff --git a/test/reverse_proxy/client/tesla_test.exs b/test/reverse_proxy/client/tesla_test.exs index 78bd31530..c8b0d5842 100644 --- a/test/reverse_proxy/client/tesla_test.exs +++ b/test/reverse_proxy/client/tesla_test.exs @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.ReverseProxy.Client.TeslaTest do diff --git a/test/support/gun_mock.ex b/test/support/gun_mock.ex index e13afd08c..9d664e366 100644 --- a/test/support/gun_mock.ex +++ b/test/support/gun_mock.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.GunMock do From 8d9dee1ba951e81aaa08b4db64b431a7456dae56 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Wed, 4 Mar 2020 08:56:36 +0300 Subject: [PATCH 48/88] retry_timeout description change --- docs/configuration/cheatsheet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index a39a7436d..85cc6170a 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -407,7 +407,7 @@ It will increase memory usage, but federation would work faster. * `:checkin_timeout` - timeout to checkin 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: 1. -* `:retry_timeout` - timeout while `gun` will try to reconnect. Default: 1000ms. +* `:retry_timeout` - time between retries when gun will try to reconnect in milliseconds. Default: 1000ms. * `:await_up_timeout` - timeout while `gun` will wait until connection is up. Default: 5000ms. ### :pools From 6b2fb9160cd945cdd4b1265c793d1f85d559fccb Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Wed, 4 Mar 2020 09:23:42 +0300 Subject: [PATCH 49/88] otp version --- lib/pleroma/application.ex | 20 ++++++++++++- lib/pleroma/otp_version.ex | 61 +++++--------------------------------- test/otp_version_test.exs | 18 ++++++----- 3 files changed, 38 insertions(+), 61 deletions(-) diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index d0b9c3c41..c8a0617a5 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -43,7 +43,25 @@ def start(_type, _args) do load_custom_modules() if adapter() == Tesla.Adapter.Gun do - Pleroma.OTPVersion.check!() + if version = Pleroma.OTPVersion.version() do + [major, minor] = + version + |> String.split(".") + |> Enum.map(&String.to_integer/1) + |> Enum.take(2) + + if (major == 22 and minor < 2) or major < 22 do + raise " + !!!OTP VERSION WARNING!!! + You are using gun adapter with OTP version #{version}, which doesn't support correct handling of unordered certificates chains. + " + end + else + raise " + !!!OTP VERSION WARNING!!! + To support correct handling of unordered certificates chains - OTP version must be > 22.2. + " + end end # Define workers and child supervisors to be supervised diff --git a/lib/pleroma/otp_version.ex b/lib/pleroma/otp_version.ex index 9ced2d27d..114d0054f 100644 --- a/lib/pleroma/otp_version.ex +++ b/lib/pleroma/otp_version.ex @@ -3,71 +3,26 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.OTPVersion do - @type check_status() :: :ok | :undefined | {:error, String.t()} - - @spec check!() :: :ok | no_return() - def check! do - case check() 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 - end - - @spec check() :: check_status() - def check do + @spec version() :: String.t() | nil + def version do # OTP Version https://erlang.org/doc/system_principles/versions.html#otp-version [ Path.join(:code.root_dir(), "OTP_VERSION"), Path.join([:code.root_dir(), "releases", :erlang.system_info(:otp_release), "OTP_VERSION"]) ] |> get_version_from_files() - |> do_check() end - @spec check([Path.t()]) :: check_status() - def check(paths) do - paths - |> get_version_from_files() - |> do_check() - end + @spec get_version_from_files([Path.t()]) :: String.t() | nil + def get_version_from_files([]), do: nil - defp get_version_from_files([]), do: nil - - defp get_version_from_files([path | paths]) do + def get_version_from_files([path | paths]) do if File.exists?(path) do - File.read!(path) + path + |> File.read!() + |> String.replace(~r/\r|\n|\s/, "") else get_version_from_files(paths) end end - - defp do_check(nil), do: :undefined - - defp do_check(version) do - version = String.replace(version, ~r/\r|\n|\s/, "") - - [major, minor] = - version - |> String.split(".") - |> Enum.map(&String.to_integer/1) - |> Enum.take(2) - - if (major == 22 and minor >= 2) or major > 22 do - :ok - else - {:error, version} - end - end end diff --git a/test/otp_version_test.exs b/test/otp_version_test.exs index af278cc72..7d2538ec8 100644 --- a/test/otp_version_test.exs +++ b/test/otp_version_test.exs @@ -9,30 +9,34 @@ defmodule Pleroma.OTPVersionTest do describe "check/1" do test "22.4" do - assert OTPVersion.check(["test/fixtures/warnings/otp_version/22.4"]) == :ok + assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/22.4"]) == + "22.4" end test "22.1" do - assert OTPVersion.check(["test/fixtures/warnings/otp_version/22.1"]) == {:error, "22.1"} + assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/22.1"]) == + "22.1" end test "21.1" do - assert OTPVersion.check(["test/fixtures/warnings/otp_version/21.1"]) == {:error, "21.1"} + assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/21.1"]) == + "21.1" end test "23.0" do - assert OTPVersion.check(["test/fixtures/warnings/otp_version/23.0"]) == :ok + assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/23.0"]) == + "23.0" end test "with non existance file" do - assert OTPVersion.check([ + assert OTPVersion.get_version_from_files([ "test/fixtures/warnings/otp_version/non-exising", "test/fixtures/warnings/otp_version/22.4" - ]) == :ok + ]) == "22.4" end test "empty paths" do - assert OTPVersion.check([]) == :undefined + assert OTPVersion.get_version_from_files([]) == nil end end end From 22d52f5691d985e7daaa955e97e0722f038f6fae Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Wed, 4 Mar 2020 09:41:23 +0300 Subject: [PATCH 50/88] same copyright date format --- lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex | 2 +- lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex | 2 +- priv/repo/migrations/20190408123347_create_conversations.exs | 2 +- test/web/activity_pub/mrf/anti_followbot_policy_test.exs | 2 +- test/web/activity_pub/mrf/anti_link_spam_policy_test.exs | 2 +- test/web/activity_pub/mrf/ensure_re_prepended_test.exs | 2 +- test/web/activity_pub/mrf/no_placeholder_text_policy_test.exs | 2 +- test/web/activity_pub/mrf/normalize_markup_test.exs | 2 +- test/web/activity_pub/mrf/object_age_policy_test.exs | 2 +- test/web/activity_pub/mrf/reject_non_public_test.exs | 2 +- test/web/activity_pub/mrf/simple_policy_test.exs | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex index b3547ecd4..0270b96ae 100644 --- a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do diff --git a/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex b/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex index f67f48ab6..fc3475048 100644 --- a/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy do diff --git a/priv/repo/migrations/20190408123347_create_conversations.exs b/priv/repo/migrations/20190408123347_create_conversations.exs index d75459e82..3eaa6136c 100644 --- a/priv/repo/migrations/20190408123347_create_conversations.exs +++ b/priv/repo/migrations/20190408123347_create_conversations.exs @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Repo.Migrations.CreateConversations do diff --git a/test/web/activity_pub/mrf/anti_followbot_policy_test.exs b/test/web/activity_pub/mrf/anti_followbot_policy_test.exs index 37a7bfcf7..fca0de7c6 100644 --- a/test/web/activity_pub/mrf/anti_followbot_policy_test.exs +++ b/test/web/activity_pub/mrf/anti_followbot_policy_test.exs @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicyTest do diff --git a/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs b/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs index b524fdd23..fc0be6f91 100644 --- a/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs +++ b/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicyTest do diff --git a/test/web/activity_pub/mrf/ensure_re_prepended_test.exs b/test/web/activity_pub/mrf/ensure_re_prepended_test.exs index dbc8b9e80..38ddec5bb 100644 --- a/test/web/activity_pub/mrf/ensure_re_prepended_test.exs +++ b/test/web/activity_pub/mrf/ensure_re_prepended_test.exs @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrependedTest do diff --git a/test/web/activity_pub/mrf/no_placeholder_text_policy_test.exs b/test/web/activity_pub/mrf/no_placeholder_text_policy_test.exs index 63ed71129..64ea61dd4 100644 --- a/test/web/activity_pub/mrf/no_placeholder_text_policy_test.exs +++ b/test/web/activity_pub/mrf/no_placeholder_text_policy_test.exs @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicyTest do diff --git a/test/web/activity_pub/mrf/normalize_markup_test.exs b/test/web/activity_pub/mrf/normalize_markup_test.exs index 0207be56b..9b39c45bd 100644 --- a/test/web/activity_pub/mrf/normalize_markup_test.exs +++ b/test/web/activity_pub/mrf/normalize_markup_test.exs @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkupTest do diff --git a/test/web/activity_pub/mrf/object_age_policy_test.exs b/test/web/activity_pub/mrf/object_age_policy_test.exs index 643609da4..e521fae44 100644 --- a/test/web/activity_pub/mrf/object_age_policy_test.exs +++ b/test/web/activity_pub/mrf/object_age_policy_test.exs @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicyTest do diff --git a/test/web/activity_pub/mrf/reject_non_public_test.exs b/test/web/activity_pub/mrf/reject_non_public_test.exs index fc1d190bb..5cc68bca8 100644 --- a/test/web/activity_pub/mrf/reject_non_public_test.exs +++ b/test/web/activity_pub/mrf/reject_non_public_test.exs @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublicTest do diff --git a/test/web/activity_pub/mrf/simple_policy_test.exs b/test/web/activity_pub/mrf/simple_policy_test.exs index df0f223f8..e825a1514 100644 --- a/test/web/activity_pub/mrf/simple_policy_test.exs +++ b/test/web/activity_pub/mrf/simple_policy_test.exs @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do From d6bebd4f9c8086dd87c75f3637a5d392a05f2daf Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Wed, 4 Mar 2020 18:13:24 +0300 Subject: [PATCH 51/88] moving some logic to tesla adapter - checking original inside gun adapter - flushing streams on max_body error --- lib/pleroma/http/adapter_helper/gun.ex | 17 ++--------------- lib/pleroma/pool/request.ex | 10 ++-------- mix.exs | 2 +- mix.lock | 3 +-- test/http/adapter_helper/gun_test.exs | 7 ------- test/http/connection_test.exs | 1 - 6 files changed, 6 insertions(+), 34 deletions(-) diff --git a/lib/pleroma/http/adapter_helper/gun.ex b/lib/pleroma/http/adapter_helper/gun.ex index b3298ec7f..5d5870d90 100644 --- a/lib/pleroma/http/adapter_helper/gun.ex +++ b/lib/pleroma/http/adapter_helper/gun.ex @@ -26,7 +26,6 @@ def options(connection_opts \\ [], %URI{} = uri) do @defaults |> Keyword.merge(Pleroma.Config.get([:http, :adapter], [])) - |> add_original(uri) |> add_scheme_opts(uri) |> AdapterHelper.maybe_add_proxy(AdapterHelper.format_proxy(proxy)) |> maybe_get_conn(uri, connection_opts) @@ -42,17 +41,12 @@ def after_request(opts) do :ok end - defp add_original(opts, %URI{host: host, port: port}) do - formatted_host = format_host(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 + defp add_scheme_opts(opts, %URI{scheme: "https", host: host}) do adapter_opts = [ certificates_verification: true, + transport: :tls, tls_opts: [ verify: :verify_peer, cacertfile: CAStore.file_path(), @@ -63,13 +57,6 @@ defp add_scheme_opts(opts, %URI{scheme: "https", host: host, port: port}) do ] ] - adapter_opts = - if port != 443 do - Keyword.put(adapter_opts, :transport, :tls) - else - adapter_opts - end - Keyword.merge(opts, adapter_opts) end diff --git a/lib/pleroma/pool/request.ex b/lib/pleroma/pool/request.ex index cce309599..0f271b3d0 100644 --- a/lib/pleroma/pool/request.ex +++ b/lib/pleroma/pool/request.ex @@ -28,12 +28,7 @@ def handle_call({:execute, client, request}, _from, state) do 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) - + def handle_info({:gun_data, _conn, _stream, _, _}, state) do {:noreply, state} end @@ -49,8 +44,7 @@ def handle_info({:gun_down, _conn, _protocol, _reason, _killed}, state) do end @impl true - def handle_info({:gun_error, _conn, stream, _error}, state) do - :ok = :gun.flush(stream) + def handle_info({:gun_error, _conn, _stream, _error}, state) do {:noreply, state} end diff --git a/mix.exs b/mix.exs index 5c1d89208..43e7e6f63 100644 --- a/mix.exs +++ b/mix.exs @@ -122,7 +122,7 @@ defp deps do # {:tesla, "~> 1.3", override: true}, {:tesla, git: "https://git.pleroma.social/pleroma/elixir-libraries/tesla.git", - ref: "922cc3db13b421763edbea76246b8ea61c38c6fa", + ref: "67436cf003d40370e944462649193706bb22ca35", override: true}, {:castore, "~> 0.1"}, {:cowlib, "~> 2.8", override: true}, diff --git a/mix.lock b/mix.lock index 255b4888b..b5daf50dc 100644 --- a/mix.lock +++ b/mix.lock @@ -102,7 +102,7 @@ "swoosh": {:hex, :swoosh, "0.23.5", "bfd9404bbf5069b1be2ffd317923ce57e58b332e25dbca2a35dedd7820dfee5a", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "e3928e1d2889a308aaf3e42755809ac21cffd77cb58eef01cbfdab4ce2fd1e21"}, "syslog": {:hex, :syslog, "1.0.6", "995970c9aa7feb380ac493302138e308d6e04fd57da95b439a6df5bb3bf75076", [:rebar3], [], "hexpm", "769ddfabd0d2a16f3f9c17eb7509951e0ca4f68363fb26f2ee51a8ec4a49881a"}, "telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"}, - "tesla": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/tesla.git", "922cc3db13b421763edbea76246b8ea61c38c6fa", [ref: "922cc3db13b421763edbea76246b8ea61c38c6fa"]}, + "tesla": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/tesla.git", "67436cf003d40370e944462649193706bb22ca35", [ref: "67436cf003d40370e944462649193706bb22ca35"]}, "timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "f354efb2400dd7a80fd9eb6c8419068c4f632da4ac47f3d8822d6e33f08bc852"}, "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"}, "tzdata": {:hex, :tzdata, "0.5.22", "f2ba9105117ee0360eae2eca389783ef7db36d533899b2e84559404dbc77ebb8", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "cd66c8a1e6a9e121d1f538b01bef459334bb4029a1ffb4eeeb5e4eae0337e7b6"}, @@ -112,4 +112,3 @@ "web_push_encryption": {:hex, :web_push_encryption, "0.2.3", "a0ceab85a805a30852f143d22d71c434046fbdbafbc7292e7887cec500826a80", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:poison, "~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "9315c8f37c108835cf3f8e9157d7a9b8f420a34f402d1b1620a31aed5b93ecdf"}, "websocket_client": {:git, "https://github.com/jeremyong/websocket_client.git", "9a6f65d05ebf2725d62fb19262b21f1805a59fbf", []}, } - diff --git a/test/http/adapter_helper/gun_test.exs b/test/http/adapter_helper/gun_test.exs index 66ca416d9..c1bf909a6 100644 --- a/test/http/adapter_helper/gun_test.exs +++ b/test/http/adapter_helper/gun_test.exs @@ -35,8 +35,6 @@ test "https url with default port" do {&: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 @@ -46,8 +44,6 @@ test "https ipv4 with default port" do 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 @@ -58,8 +54,6 @@ test "https ipv6 with default port" do 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 @@ -129,7 +123,6 @@ test "default ssl adapter opts with connection" do 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 diff --git a/test/http/connection_test.exs b/test/http/connection_test.exs index 5c1ecda0b..d4db3798c 100644 --- a/test/http/connection_test.exs +++ b/test/http/connection_test.exs @@ -134,7 +134,6 @@ test "default ssl adapter opts with connection" do 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 From fe47bcde8c20d7c968a7fb20637b4bccc6389691 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Wed, 4 Mar 2020 19:44:03 +0300 Subject: [PATCH 52/88] updating tesla ref --- mix.exs | 2 +- mix.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index 43e7e6f63..3b1bbbaf2 100644 --- a/mix.exs +++ b/mix.exs @@ -122,7 +122,7 @@ defp deps do # {:tesla, "~> 1.3", override: true}, {:tesla, git: "https://git.pleroma.social/pleroma/elixir-libraries/tesla.git", - ref: "67436cf003d40370e944462649193706bb22ca35", + ref: "61b7503cef33f00834f78ddfafe0d5d9dec2270b", override: true}, {:castore, "~> 0.1"}, {:cowlib, "~> 2.8", override: true}, diff --git a/mix.lock b/mix.lock index b5daf50dc..af53e5c0f 100644 --- a/mix.lock +++ b/mix.lock @@ -102,7 +102,7 @@ "swoosh": {:hex, :swoosh, "0.23.5", "bfd9404bbf5069b1be2ffd317923ce57e58b332e25dbca2a35dedd7820dfee5a", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "e3928e1d2889a308aaf3e42755809ac21cffd77cb58eef01cbfdab4ce2fd1e21"}, "syslog": {:hex, :syslog, "1.0.6", "995970c9aa7feb380ac493302138e308d6e04fd57da95b439a6df5bb3bf75076", [:rebar3], [], "hexpm", "769ddfabd0d2a16f3f9c17eb7509951e0ca4f68363fb26f2ee51a8ec4a49881a"}, "telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"}, - "tesla": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/tesla.git", "67436cf003d40370e944462649193706bb22ca35", [ref: "67436cf003d40370e944462649193706bb22ca35"]}, + "tesla": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/tesla.git", "61b7503cef33f00834f78ddfafe0d5d9dec2270b", [ref: "61b7503cef33f00834f78ddfafe0d5d9dec2270b"]}, "timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "f354efb2400dd7a80fd9eb6c8419068c4f632da4ac47f3d8822d6e33f08bc852"}, "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"}, "tzdata": {:hex, :tzdata, "0.5.22", "f2ba9105117ee0360eae2eca389783ef7db36d533899b2e84559404dbc77ebb8", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "cd66c8a1e6a9e121d1f538b01bef459334bb4029a1ffb4eeeb5e4eae0337e7b6"}, From b34bc669b91903a4567f6f527ebe16f9cd7e0ccf Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Wed, 4 Mar 2020 20:09:18 +0300 Subject: [PATCH 53/88] adding descriptions --- config/description.exs | 213 +++++++++++++++++++++++++++++++ docs/configuration/cheatsheet.md | 4 +- 2 files changed, 215 insertions(+), 2 deletions(-) diff --git a/config/description.exs b/config/description.exs index 307f8b5bc..531d73145 100644 --- a/config/description.exs +++ b/config/description.exs @@ -2966,5 +2966,218 @@ suggestions: [2] } ] + }, + %{ + group: :pleroma, + key: :connections_pool, + type: :group, + description: "Advanced settings for `gun` connections pool", + children: [ + %{ + key: :checkin_timeout, + type: :integer, + description: "Timeout to checkin connection from pool. Default: 250ms.", + suggestions: [250] + }, + %{ + key: :max_connections, + type: :integer, + description: "Maximum number of connections in the pool. Default: 250 connections.", + suggestions: [250] + }, + %{ + key: :retry, + type: :integer, + description: + "Number of retries, while `gun` will try to reconnect if connection goes down. Default: 1.", + suggestions: [1] + }, + %{ + key: :retry_timeout, + type: :integer, + description: + "Time between retries when `gun` will try to reconnect in milliseconds. Default: 1000ms.", + suggestions: [1000] + }, + %{ + key: :await_up_timeout, + type: :integer, + description: "Timeout while `gun` will wait until connection is up. Default: 5000ms.", + suggestions: [5000] + } + ] + }, + %{ + group: :pleroma, + key: :pools, + type: :group, + description: "Advanced settings for `gun` workers pools", + children: [ + %{ + key: :federation, + type: :keyword, + description: "Settings for federation pool.", + children: [ + %{ + key: :size, + type: :integer, + description: "Number workers in the pool.", + suggestions: [50] + }, + %{ + key: :max_overflow, + type: :integer, + description: "Number of additional workers if pool is under load.", + suggestions: [10] + }, + %{ + key: :timeout, + type: :integer, + description: "Timeout while `gun` will wait for response.", + suggestions: [150_000] + } + ] + }, + %{ + key: :media, + type: :keyword, + description: "Settings for media pool.", + children: [ + %{ + key: :size, + type: :integer, + description: "Number workers in the pool.", + suggestions: [50] + }, + %{ + key: :max_overflow, + type: :integer, + description: "Number of additional workers if pool is under load.", + suggestions: [10] + }, + %{ + key: :timeout, + type: :integer, + description: "Timeout while `gun` will wait for response.", + suggestions: [150_000] + } + ] + }, + %{ + key: :upload, + type: :keyword, + description: "Settings for upload pool.", + children: [ + %{ + key: :size, + type: :integer, + description: "Number workers in the pool.", + suggestions: [25] + }, + %{ + key: :max_overflow, + type: :integer, + description: "Number of additional workers if pool is under load.", + suggestions: [5] + }, + %{ + key: :timeout, + type: :integer, + description: "Timeout while `gun` will wait for response.", + suggestions: [300_000] + } + ] + }, + %{ + key: :default, + type: :keyword, + description: "Settings for default pool.", + children: [ + %{ + key: :size, + type: :integer, + description: "Number workers in the pool.", + suggestions: [10] + }, + %{ + key: :max_overflow, + type: :integer, + description: "Number of additional workers if pool is under load.", + suggestions: [2] + }, + %{ + key: :timeout, + type: :integer, + description: "Timeout while `gun` will wait for response.", + suggestions: [10_000] + } + ] + } + ] + }, + %{ + group: :pleroma, + key: :hackney_pools, + type: :group, + description: "Advanced settings for `hackney` connections pools", + children: [ + %{ + key: :federation, + type: :keyword, + description: "Settings for federation pool.", + children: [ + %{ + key: :max_connections, + type: :integer, + description: "Number workers in the pool.", + suggestions: [50] + }, + %{ + key: :timeout, + type: :integer, + description: "Timeout while `hackney` will wait for response.", + suggestions: [150_000] + } + ] + }, + %{ + key: :media, + type: :keyword, + description: "Settings for media pool.", + children: [ + %{ + key: :max_connections, + type: :integer, + description: "Number workers in the pool.", + suggestions: [50] + }, + %{ + key: :timeout, + type: :integer, + description: "Timeout while `hackney` will wait for response.", + suggestions: [150_000] + } + ] + }, + %{ + key: :upload, + type: :keyword, + description: "Settings for upload pool.", + children: [ + %{ + key: :max_connections, + type: :integer, + description: "Number workers in the pool.", + suggestions: [25] + }, + %{ + key: :timeout, + type: :integer, + description: "Timeout while `hackney` will wait for response.", + suggestions: [300_000] + } + ] + } + ] } ] diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 85cc6170a..833d243e8 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -406,8 +406,8 @@ It will increase memory usage, but federation would work faster. * `:checkin_timeout` - timeout to checkin 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: 1. -* `:retry_timeout` - time between retries when gun will try to reconnect in milliseconds. Default: 1000ms. +* `:retry` - number of retries, while `gun` will try to reconnect if connection goes down. Default: 1. +* `:retry_timeout` - time between retries when `gun` will try to reconnect in milliseconds. Default: 1000ms. * `:await_up_timeout` - timeout while `gun` will wait until connection is up. Default: 5000ms. ### :pools From eb324467d9c5c761a776ffc98347246c61ad02ae Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Thu, 5 Mar 2020 09:51:52 +0300 Subject: [PATCH 54/88] removing try block in getting gun info --- lib/pleroma/pool/connections.ex | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/lib/pleroma/pool/connections.ex b/lib/pleroma/pool/connections.ex index f1fab2a24..f96c08f21 100644 --- a/lib/pleroma/pool/connections.ex +++ b/lib/pleroma/pool/connections.ex @@ -285,20 +285,15 @@ def handle_info({:DOWN, _ref, :process, conn_pid, reason}, state) do end 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} = Gun.info(pid) + %{origin_host: origin_host, origin_scheme: scheme, origin_port: port} = Gun.info(pid) - host = - case :inet.ntoa(origin_host) do - {:error, :einval} -> origin_host - ip -> ip - end + host = + case :inet.ntoa(origin_host) do + {:error, :einval} -> origin_host + ip -> ip + end - "#{scheme}:#{host}:#{port}" - rescue - _ -> :error_gun_info - end + "#{scheme}:#{host}:#{port}" end defp find_conn(conns, conn_pid) do From f0753eed0fdddd30e127213c89a118dd2e087dc9 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Thu, 5 Mar 2020 17:31:06 +0300 Subject: [PATCH 55/88] removing try block in tesla request added mocks for tests which fail with Tesla.Mock.Error --- lib/pleroma/http/http.ex | 24 +++-------- lib/pleroma/pool/request.ex | 2 +- lib/pleroma/web/push/impl.ex | 2 +- lib/pleroma/web/web_finger/web_finger.ex | 3 +- test/fixtures/users_mock/localhost.json | 41 +++++++++++++++++++ test/notification_test.exs | 20 +++++++++ .../mrf/anti_link_spam_policy_test.exs | 9 ++++ test/web/activity_pub/relay_test.exs | 5 +++ .../notification_controller_test.exs | 13 ++++++ .../views/notification_view_test.exs | 13 ++++++ .../mastodon_api/views/status_view_test.exs | 17 ++++++++ test/web/streamer/streamer_test.exs | 12 ++++++ 12 files changed, 139 insertions(+), 22 deletions(-) create mode 100644 test/fixtures/users_mock/localhost.json diff --git a/lib/pleroma/http/http.ex b/lib/pleroma/http/http.ex index 7b7c79b64..466a94adc 100644 --- a/lib/pleroma/http/http.ex +++ b/lib/pleroma/http/http.ex @@ -88,15 +88,11 @@ def request(method, url, body, headers, options) when is_binary(url) do 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, %{env: :test}), do: request(client, request) - def request(%Client{} = client, request, %{body_as: :chunks}) do - request_try(client, request) - end + def request(%Client{} = client, request, %{body_as: :chunks}), do: request(client, request) - def request(%Client{} = client, request, %{pool_alive?: false}) do - request_try(client, request) - end + def request(%Client{} = client, request, %{pool_alive?: false}), do: request(client, request) def request(%Client{} = client, request, %{pool: pool, timeout: timeout}) do :poolboy.transaction( @@ -106,18 +102,8 @@ def request(%Client{} = client, request, %{pool: pool, timeout: timeout}) do ) end - @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 + @spec request(Client.t(), keyword()) :: {:ok, Env.t()} | {:error, any()} + def request(client, request), do: Tesla.request(client, request) defp build_request(method, headers, options, url, body, params) do Builder.new() diff --git a/lib/pleroma/pool/request.ex b/lib/pleroma/pool/request.ex index 0f271b3d0..db7c10c01 100644 --- a/lib/pleroma/pool/request.ex +++ b/lib/pleroma/pool/request.ex @@ -22,7 +22,7 @@ def execute(pid, client, request, timeout) do @impl true def handle_call({:execute, client, request}, _from, state) do - response = Pleroma.HTTP.request_try(client, request) + response = Pleroma.HTTP.request(client, request) {:reply, response, state} end diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex index afa510f08..233e55f21 100644 --- a/lib/pleroma/web/push/impl.ex +++ b/lib/pleroma/web/push/impl.ex @@ -32,7 +32,7 @@ def perform( type = Activity.mastodon_notification_type(notif.activity) gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key) avatar_url = User.avatar_url(actor) - object = Object.normalize(activity) + object = Object.normalize(activity) || activity user = User.get_cached_by_id(user_id) direct_conversation_id = Activity.direct_conversation_id(activity, user) diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex index db567a02e..7ffd0e51b 100644 --- a/lib/pleroma/web/web_finger/web_finger.ex +++ b/lib/pleroma/web/web_finger/web_finger.ex @@ -173,7 +173,8 @@ def find_lrdd_template(domain) do get_template_from_xml(body) else _ -> - with {:ok, %{body: body}} <- HTTP.get("https://#{domain}/.well-known/host-meta", []) do + with {:ok, %{body: body, status: status}} when status in 200..299 <- + HTTP.get("https://#{domain}/.well-known/host-meta", []) do get_template_from_xml(body) else e -> {:error, "Can't find LRDD template: #{inspect(e)}"} diff --git a/test/fixtures/users_mock/localhost.json b/test/fixtures/users_mock/localhost.json new file mode 100644 index 000000000..a49935db1 --- /dev/null +++ b/test/fixtures/users_mock/localhost.json @@ -0,0 +1,41 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "http://localhost:4001/schemas/litepub-0.1.jsonld", + { + "@language": "und" + } + ], + "attachment": [], + "endpoints": { + "oauthAuthorizationEndpoint": "http://localhost:4001/oauth/authorize", + "oauthRegistrationEndpoint": "http://localhost:4001/api/v1/apps", + "oauthTokenEndpoint": "http://localhost:4001/oauth/token", + "sharedInbox": "http://localhost:4001/inbox" + }, + "followers": "http://localhost:4001/users/{{nickname}}/followers", + "following": "http://localhost:4001/users/{{nickname}}/following", + "icon": { + "type": "Image", + "url": "http://localhost:4001/media/4e914f5b84e4a259a3f6c2d2edc9ab642f2ab05f3e3d9c52c81fc2d984b3d51e.jpg" + }, + "id": "http://localhost:4001/users/{{nickname}}", + "image": { + "type": "Image", + "url": "http://localhost:4001/media/f739efddefeee49c6e67e947c4811fdc911785c16ae43da4c3684051fbf8da6a.jpg?name=f739efddefeee49c6e67e947c4811fdc911785c16ae43da4c3684051fbf8da6a.jpg" + }, + "inbox": "http://localhost:4001/users/{{nickname}}/inbox", + "manuallyApprovesFollowers": false, + "name": "{{nickname}}", + "outbox": "http://localhost:4001/users/{{nickname}}/outbox", + "preferredUsername": "{{nickname}}", + "publicKey": { + "id": "http://localhost:4001/users/{{nickname}}#main-key", + "owner": "http://localhost:4001/users/{{nickname}}", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5DLtwGXNZElJyxFGfcVc\nXANhaMadj/iYYQwZjOJTV9QsbtiNBeIK54PJrYuU0/0YIdrvS1iqheX5IwXRhcwa\nhm3ZyLz7XeN9st7FBni4BmZMBtMpxAuYuu5p/jbWy13qAiYOhPreCx0wrWgm/lBD\n9mkgaxIxPooBE0S4ZWEJIDIV1Vft3AWcRUyWW1vIBK0uZzs6GYshbQZB952S0yo4\nFzI1hABGHncH8UvuFauh4EZ8tY7/X5I0pGRnDOcRN1dAht5w5yTA+6r5kebiFQjP\nIzN/eCO/a9Flrj9YGW7HDNtjSOH0A31PLRGlJtJO3yK57dnf5ppyCZGfL4emShQo\ncQIDAQAB\n-----END PUBLIC KEY-----\n\n" + }, + "summary": "your friendly neighborhood pleroma developer
I like cute things and distributed systems, and really hate delete and redrafts", + "tag": [], + "type": "Person", + "url": "http://localhost:4001/users/{{nickname}}" +} \ No newline at end of file diff --git a/test/notification_test.exs b/test/notification_test.exs index 56a581810..c71df4e07 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -649,12 +649,20 @@ test "notifications are deleted if a remote user is deleted" 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() assert Enum.empty?(Notification.for_user(local_user)) end + @tag capture_log: true test "move activity generates a notification" do %{ap_id: old_ap_id} = old_user = insert(:user) %{ap_id: new_ap_id} = new_user = insert(:user, also_known_as: [old_ap_id]) @@ -664,6 +672,18 @@ test "move activity generates a notification" do User.follow(follower, old_user) User.follow(other_follower, old_user) + old_user_url = old_user.ap_id + + body = + File.read!("test/fixtures/users_mock/localhost.json") + |> String.replace("{{nickname}}", old_user.nickname) + |> Jason.encode!() + + Tesla.Mock.mock(fn + %{method: :get, url: ^old_user_url} -> + %Tesla.Env{status: 200, body: body} + end) + Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user) ObanHelpers.perform_all() diff --git a/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs b/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs index fc0be6f91..1a13699be 100644 --- a/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs +++ b/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs @@ -110,6 +110,15 @@ test "it allows posts with links" do end describe "with unknown actors" do + setup do + Tesla.Mock.mock(fn + %{method: :get, url: "http://invalid.actor"} -> + %Tesla.Env{status: 500, body: ""} + end) + + :ok + end + test "it rejects posts without links" do message = @linkless_message diff --git a/test/web/activity_pub/relay_test.exs b/test/web/activity_pub/relay_test.exs index e3115dcd8..12bf90d90 100644 --- a/test/web/activity_pub/relay_test.exs +++ b/test/web/activity_pub/relay_test.exs @@ -89,6 +89,11 @@ test "returns error when object is unknown" do } ) + Tesla.Mock.mock(fn + %{method: :get, url: "http://mastodon.example.org/eee/99541947525187367"} -> + %Tesla.Env{status: 500, body: ""} + end) + assert capture_log(fn -> assert Relay.publish(activity) == {:error, nil} end) =~ "[error] error: nil" diff --git a/test/web/mastodon_api/controllers/notification_controller_test.exs b/test/web/mastodon_api/controllers/notification_controller_test.exs index d452ddbdd..0f0a060d2 100644 --- a/test/web/mastodon_api/controllers/notification_controller_test.exs +++ b/test/web/mastodon_api/controllers/notification_controller_test.exs @@ -407,11 +407,24 @@ test "see notifications after muting user with notifications and with_muted para assert length(json_response(conn, 200)) == 1 end + @tag capture_log: true test "see move notifications with `with_move` parameter" do old_user = insert(:user) new_user = insert(:user, also_known_as: [old_user.ap_id]) %{user: follower, conn: conn} = oauth_access(["read:notifications"]) + old_user_url = old_user.ap_id + + body = + File.read!("test/fixtures/users_mock/localhost.json") + |> String.replace("{{nickname}}", old_user.nickname) + |> Jason.encode!() + + Tesla.Mock.mock(fn + %{method: :get, url: ^old_user_url} -> + %Tesla.Env{status: 200, body: body} + end) + User.follow(follower, old_user) Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user) Pleroma.Tests.ObanHelpers.perform_all() diff --git a/test/web/mastodon_api/views/notification_view_test.exs b/test/web/mastodon_api/views/notification_view_test.exs index 4df9c3c03..57e4c8f1e 100644 --- a/test/web/mastodon_api/views/notification_view_test.exs +++ b/test/web/mastodon_api/views/notification_view_test.exs @@ -108,11 +108,24 @@ test "Follow notification" do NotificationView.render("index.json", %{notifications: [notification], for: followed}) end + @tag capture_log: true test "Move notification" do old_user = insert(:user) new_user = insert(:user, also_known_as: [old_user.ap_id]) follower = insert(:user) + old_user_url = old_user.ap_id + + body = + File.read!("test/fixtures/users_mock/localhost.json") + |> String.replace("{{nickname}}", old_user.nickname) + |> Jason.encode!() + + Tesla.Mock.mock(fn + %{method: :get, url: ^old_user_url} -> + %Tesla.Env{status: 200, body: body} + end) + User.follow(follower, old_user) Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user) Pleroma.Tests.ObanHelpers.perform_all() diff --git a/test/web/mastodon_api/views/status_view_test.exs b/test/web/mastodon_api/views/status_view_test.exs index 191895c6f..7df72decb 100644 --- a/test/web/mastodon_api/views/status_view_test.exs +++ b/test/web/mastodon_api/views/status_view_test.exs @@ -92,6 +92,23 @@ test "returns a temporary ap_id based user for activities missing db users" do Repo.delete(user) Cachex.clear(:user_cache) + finger_url = + "https://localhost/.well-known/webfinger?resource=acct:#{user.nickname}@localhost" + + Tesla.Mock.mock_global(fn + %{method: :get, url: "http://localhost/.well-known/host-meta"} -> + %Tesla.Env{status: 404, body: ""} + + %{method: :get, url: "https://localhost/.well-known/host-meta"} -> + %Tesla.Env{status: 404, body: ""} + + %{ + method: :get, + url: ^finger_url + } -> + %Tesla.Env{status: 404, body: ""} + end) + %{account: ms_user} = StatusView.render("show.json", activity: activity) assert ms_user.acct == "erroruser@example.com" diff --git a/test/web/streamer/streamer_test.exs b/test/web/streamer/streamer_test.exs index 339f99bbf..a04d70f21 100644 --- a/test/web/streamer/streamer_test.exs +++ b/test/web/streamer/streamer_test.exs @@ -122,6 +122,18 @@ test "it doesn't send notify to the 'user:notification' stream' when a domain is test "it sends follow activities to the 'user:notification' stream", %{ user: user } do + user_url = user.ap_id + + body = + File.read!("test/fixtures/users_mock/localhost.json") + |> String.replace("{{nickname}}", user.nickname) + |> Jason.encode!() + + Tesla.Mock.mock_global(fn + %{method: :get, url: ^user_url} -> + %Tesla.Env{status: 200, body: body} + end) + user2 = insert(:user) task = Task.async(fn -> assert_receive {:text, _}, @streamer_timeout end) From 058c9b01ac063f3cca22a653032663916a16a234 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Thu, 5 Mar 2020 18:28:04 +0300 Subject: [PATCH 56/88] returning, not needed --- lib/pleroma/web/push/impl.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex index 233e55f21..afa510f08 100644 --- a/lib/pleroma/web/push/impl.ex +++ b/lib/pleroma/web/push/impl.ex @@ -32,7 +32,7 @@ def perform( type = Activity.mastodon_notification_type(notif.activity) gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key) avatar_url = User.avatar_url(actor) - object = Object.normalize(activity) || activity + object = Object.normalize(activity) user = User.get_cached_by_id(user_id) direct_conversation_id = Activity.direct_conversation_id(activity, user) From 931111fd5518cb79449cf79ffe29cb774c55d5ff Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Thu, 5 Mar 2020 18:57:45 +0300 Subject: [PATCH 57/88] removing integration tests --- test/http_test.exs | 25 -- test/pool/connections_test.exs | 301 ---------------------- test/reverse_proxy/client/tesla_test.exs | 93 ------- test/reverse_proxy/reverse_proxy_test.exs | 41 --- 4 files changed, 460 deletions(-) delete mode 100644 test/reverse_proxy/client/tesla_test.exs diff --git a/test/http_test.exs b/test/http_test.exs index 4aa08afcb..fd254b590 100644 --- a/test/http_test.exs +++ b/test/http_test.exs @@ -58,29 +58,4 @@ test "returns successfully result" do } end end - - describe "connection pools" do - @describetag :integration - clear_config(Pleroma.Gun) do - Pleroma.Config.put(Pleroma.Gun, Pleroma.Gun.API) - 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 diff --git a/test/pool/connections_test.exs b/test/pool/connections_test.exs index 963fae665..753fd8b0b 100644 --- a/test/pool/connections_test.exs +++ b/test/pool/connections_test.exs @@ -435,307 +435,6 @@ test "remove frequently used and idle", %{name: name} do } = Connections.get_state(name) end - describe "integration test" do - @describetag :integration - - clear_config(Pleroma.Gun) do - Pleroma.Config.put(Pleroma.Gun, Pleroma.Gun.API) - end - - test "opens connection and change owner", %{name: name} do - url = "https://httpbin.org" - :ok = Conn.open(url, name) - conn = Connections.checkin(url, name) - - pid = Process.whereis(name) - - assert :gun.info(conn).owner == pid - end - - test "opens connection and reuse it on next request", %{name: name} do - url = "http://httpbin.org" - :ok = Conn.open(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 = Conn.open(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 = Conn.open(https1, name) - :ok = Conn.open(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 = Conn.open(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 = Conn.open(https1, name) - :ok = Conn.open(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 = Conn.open(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 = Conn.open(https1, name) - :ok = Conn.open(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 = Conn.open(https1, name) - :ok = Conn.open(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 = Conn.open(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" diff --git a/test/reverse_proxy/client/tesla_test.exs b/test/reverse_proxy/client/tesla_test.exs deleted file mode 100644 index c8b0d5842..000000000 --- a/test/reverse_proxy/client/tesla_test.exs +++ /dev/null @@ -1,93 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# 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) do - Pleroma.Config.put(Pleroma.Gun, Pleroma.Gun.API) - 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 diff --git a/test/reverse_proxy/reverse_proxy_test.exs b/test/reverse_proxy/reverse_proxy_test.exs index 18aae5a6b..c17ab0f89 100644 --- a/test/reverse_proxy/reverse_proxy_test.exs +++ b/test/reverse_proxy/reverse_proxy_test.exs @@ -341,45 +341,4 @@ test "with content-disposition header", %{conn: conn} 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) do - Pleroma.Config.put(Pleroma.Gun, Pleroma.Gun.API) - 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 From 56ff02f2ef56465b14c9670b930d154911cc7470 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Fri, 6 Mar 2020 20:23:58 +0300 Subject: [PATCH 58/88] removing GunMock to use Mox --- test/http/adapter_helper/gun_test.exs | 88 ++++++------ test/http/adapter_helper/hackney_test.exs | 8 +- test/http/connection_test.exs | 25 ++-- test/pool/connections_test.exs | 127 ++++++++++++++---- test/support/gun_mock.ex | 155 ---------------------- test/test_helper.exs | 3 + 6 files changed, 172 insertions(+), 234 deletions(-) delete mode 100644 test/support/gun_mock.ex diff --git a/test/http/adapter_helper/gun_test.exs b/test/http/adapter_helper/gun_test.exs index c1bf909a6..b1b34858a 100644 --- a/test/http/adapter_helper/gun_test.exs +++ b/test/http/adapter_helper/gun_test.exs @@ -5,17 +5,29 @@ defmodule Pleroma.HTTP.AdapterHelper.GunTest do use ExUnit.Case, async: true use Pleroma.Tests.Helpers + import ExUnit.CaptureLog + import Mox + alias Pleroma.Config alias Pleroma.Gun.Conn alias Pleroma.HTTP.AdapterHelper.Gun alias Pleroma.Pool.Connections - setup_all do - {:ok, _} = Registry.start_link(keys: :unique, name: Pleroma.GunMock) + setup :verify_on_exit! + + defp gun_mock(_) do + gun_mock() :ok end + defp gun_mock do + Pleroma.GunMock + |> expect(:open, fn _, _, _ -> Task.start_link(fn -> Process.sleep(1000) end) end) + |> expect(:await_up, fn _, _ -> {:ok, :http} end) + |> expect(:set_owner, fn _, _ -> :ok end) + end + describe "options/1" do clear_config([:http, :adapter]) do Config.put([:http, :adapter], a: 1, b: 2) @@ -24,23 +36,20 @@ defmodule Pleroma.HTTP.AdapterHelper.GunTest do test "https url with default port" do uri = URI.parse("https://example.com") - opts = Gun.options(uri) + opts = Gun.options([receive_conn: false], 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 + refute opts[:tls_opts] == [] - assert tls_opts[:verify_fun] == + assert opts[:tls_opts][:verify_fun] == {&:ssl_verify_hostname.verify_fun/3, [check_hostname: 'example.com']} - assert File.exists?(tls_opts[:cacertfile]) + assert File.exists?(opts[:tls_opts][:cacertfile]) end test "https ipv4 with default port" do uri = URI.parse("https://127.0.0.1") - opts = Gun.options(uri) + opts = Gun.options([receive_conn: false], uri) assert opts[:tls_opts][:verify_fun] == {&:ssl_verify_hostname.verify_fun/3, [check_hostname: '127.0.0.1']} @@ -49,7 +58,7 @@ test "https ipv4 with default port" do test "https ipv6 with default port" do uri = URI.parse("https://[2a03:2880:f10c:83:face:b00c:0:25de]") - opts = Gun.options(uri) + opts = Gun.options([receive_conn: false], uri) assert opts[:tls_opts][:verify_fun] == {&:ssl_verify_hostname.verify_fun/3, @@ -59,32 +68,14 @@ test "https ipv6 with default port" do test "https url with non standart port" do uri = URI.parse("https://example.com:115") - opts = Gun.options(uri) + opts = Gun.options([receive_conn: false], uri) assert opts[:certificates_verification] assert opts[:transport] == :tls end - test "receive conn by default" do - uri = URI.parse("http://another-domain.com") - :ok = Conn.open(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 = Conn.open(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 + gun_mock() level = Application.get_env(:logger, :level) Logger.configure(level: :debug) on_exit(fn -> Logger.configure(level: level) end) @@ -105,12 +96,13 @@ test "get conn on next request" do end test "merges with defaul http adapter config" do - defaults = Gun.options(URI.parse("https://example.com")) + defaults = Gun.options([receive_conn: false], URI.parse("https://example.com")) assert Keyword.has_key?(defaults, :a) assert Keyword.has_key?(defaults, :b) end test "default ssl adapter opts with connection" do + gun_mock() uri = URI.parse("https://some-domain.com") :ok = Conn.open(uri, :gun_connections) @@ -118,10 +110,7 @@ test "default ssl adapter opts with connection" do 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 + refute opts[:tls_opts] == [] assert opts[:close_conn] == false assert is_pid(opts[:conn]) @@ -158,7 +147,32 @@ test "passed opts have more weight than defaults" do end end + describe "options/1 with receive_conn parameter" do + setup :gun_mock + + test "receive conn by default" do + uri = URI.parse("http://another-domain.com") + :ok = Conn.open(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-domain.com") + :ok = Conn.open(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 + end + describe "after_request/1" do + setup :gun_mock + test "body_as not chunks" do uri = URI.parse("http://some-domain.com") :ok = Conn.open(uri, :gun_connections) @@ -223,7 +237,6 @@ test "with ipv4" do uri = URI.parse("http://127.0.0.1") :ok = Conn.open(uri, :gun_connections) opts = Gun.options(uri) - send(:gun_connections, {:gun_up, opts[:conn], :http}) :ok = Gun.after_request(opts) conn = opts[:conn] @@ -242,7 +255,6 @@ test "with ipv6" do uri = URI.parse("http://[2a03:2880:f10c:83:face:b00c:0:25de]") :ok = Conn.open(uri, :gun_connections) opts = Gun.options(uri) - send(:gun_connections, {:gun_up, opts[:conn], :http}) :ok = Gun.after_request(opts) conn = opts[:conn] diff --git a/test/http/adapter_helper/hackney_test.exs b/test/http/adapter_helper/hackney_test.exs index 3306616ef..5fda075f6 100644 --- a/test/http/adapter_helper/hackney_test.exs +++ b/test/http/adapter_helper/hackney_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.HTTP.AdapterHelper.HackneyTest do - use ExUnit.Case + use ExUnit.Case, async: true use Pleroma.Tests.Helpers alias Pleroma.Config @@ -20,11 +20,7 @@ defmodule Pleroma.HTTP.AdapterHelper.HackneyTest do 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) + opts = Hackney.options([proxy: "localhost:8123"], uri) assert opts[:a] == 1 assert opts[:b] == 2 diff --git a/test/http/connection_test.exs b/test/http/connection_test.exs index d4db3798c..a5ddfd435 100644 --- a/test/http/connection_test.exs +++ b/test/http/connection_test.exs @@ -3,16 +3,16 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.HTTP.ConnectionTest do - use ExUnit.Case + use ExUnit.Case, async: true use Pleroma.Tests.Helpers + import ExUnit.CaptureLog + import Mox + alias Pleroma.Config alias Pleroma.HTTP.Connection - setup_all do - {:ok, _} = Registry.start_link(keys: :unique, name: Pleroma.GunMock) - :ok - end + setup :verify_on_exit! describe "parse_host/1" do test "as atom to charlist" do @@ -123,16 +123,19 @@ test "default ssl adapter opts with connection" do uri = URI.parse("https://some-domain.com") - pid = Process.whereis(:federation) - :ok = Pleroma.Gun.Conn.open(uri, :gun_connections, genserver_pid: pid) + Pleroma.GunMock + |> expect(:open, fn 'some-domain.com', 443, _ -> + Task.start_link(fn -> Process.sleep(1000) end) + end) + |> expect(:await_up, fn _, _ -> {:ok, :http2} end) + |> expect(:set_owner, fn _, _ -> :ok end) + + :ok = Pleroma.Gun.Conn.open(uri, :gun_connections) 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 + refute opts[:tls_opts] == [] assert opts[:close_conn] == false assert is_pid(opts[:conn]) diff --git a/test/pool/connections_test.exs b/test/pool/connections_test.exs index 753fd8b0b..06f32b74e 100644 --- a/test/pool/connections_test.exs +++ b/test/pool/connections_test.exs @@ -3,39 +3,83 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Pool.ConnectionsTest do - use ExUnit.Case + use ExUnit.Case, async: true use Pleroma.Tests.Helpers + import ExUnit.CaptureLog + import Mox + alias Pleroma.Gun.Conn + alias Pleroma.GunMock alias Pleroma.Pool.Connections + setup :verify_on_exit! + setup_all do - {:ok, _} = Registry.start_link(keys: :unique, name: Pleroma.GunMock) - :ok - end - - clear_config([:connections_pool, :retry]) do - Pleroma.Config.put([:connections_pool, :retry], 5) - end - - setup do name = :test_connections - adapter = Application.get_env(:tesla, :adapter) - Application.put_env(:tesla, :adapter, Tesla.Adapter.Gun) - - {:ok, pid} = Connections.start_link({name, [max_connections: 2, checkin_timeout: 1_500]}) + {:ok, pid} = Connections.start_link({name, [checkin_timeout: 150]}) + {:ok, _} = Registry.start_link(keys: :unique, name: Pleroma.GunMock) on_exit(fn -> - Application.put_env(:tesla, :adapter, adapter) - - if Process.alive?(pid) do - GenServer.stop(name) - end + if Process.alive?(pid), do: GenServer.stop(name) end) {:ok, name: name} end + defp open_mock(num \\ 1) do + GunMock + |> expect(:open, num, &start_and_register(&1, &2, &3)) + |> expect(:await_up, num, fn _, _ -> {:ok, :http} end) + |> expect(:set_owner, num, fn _, _ -> :ok end) + end + + defp connect_mock(mock) do + mock + |> expect(:connect, &connect(&1, &2)) + |> expect(:await, &await(&1, &2)) + end + + defp info_mock(mock), do: expect(mock, :info, &info(&1)) + + defp start_and_register('gun-not-up.com', _, _), do: {:error, :timeout} + + defp start_and_register(host, port, _) do + {:ok, pid} = Task.start_link(fn -> Process.sleep(1000) end) + + scheme = + case port do + 443 -> "https" + _ -> "http" + end + + Registry.register(GunMock, pid, %{ + origin_scheme: scheme, + origin_host: host, + origin_port: port + }) + + {:ok, pid} + end + + defp info(pid) do + [{_, info}] = Registry.lookup(GunMock, pid) + info + end + + defp connect(pid, _) do + ref = make_ref() + Registry.register(GunMock, ref, pid) + ref + end + + defp await(pid, ref) do + [{_, ^pid}] = Registry.lookup(GunMock, ref) + {:response, :fin, 200, []} + end + + defp now, do: :os.system_time(:second) + describe "alive?/2" do test "is alive", %{name: name} do assert Connections.alive?(name) @@ -47,6 +91,7 @@ test "returns false if not started" do end test "opens connection and reuse it on next request", %{name: name} do + open_mock() url = "http://some-domain.com" key = "http:some-domain.com:80" refute Connections.checkin(url, name) @@ -112,6 +157,7 @@ test "opens connection and reuse it on next request", %{name: name} do end test "reuse connection for idna domains", %{name: name} do + open_mock() url = "http://ですsome-domain.com" refute Connections.checkin(url, name) @@ -140,6 +186,7 @@ test "reuse connection for idna domains", %{name: name} do end test "reuse for ipv4", %{name: name} do + open_mock() url = "http://127.0.0.1" refute Connections.checkin(url, name) @@ -183,6 +230,7 @@ test "reuse for ipv4", %{name: name} do end test "reuse for ipv6", %{name: name} do + open_mock() url = "http://[2a03:2880:f10c:83:face:b00c:0:25de]" refute Connections.checkin(url, name) @@ -212,6 +260,10 @@ test "reuse for ipv6", %{name: name} do end test "up and down ipv4", %{name: name} do + open_mock() + |> info_mock() + |> allow(self(), name) + self = self() url = "http://127.0.0.1" :ok = Conn.open(url, name) @@ -233,6 +285,11 @@ test "up and down ipv4", %{name: name} do test "up and down ipv6", %{name: name} do self = self() + + open_mock() + |> info_mock() + |> allow(self, name) + url = "http://[2a03:2880:f10c:83:face:b00c:0:25de]" :ok = Conn.open(url, name) conn = Connections.checkin(url, name) @@ -252,6 +309,7 @@ test "up and down ipv6", %{name: name} do end test "reuses connection based on protocol", %{name: name} do + open_mock(2) http_url = "http://some-domain.com" http_key = "http:some-domain.com:80" https_url = "https://some-domain.com" @@ -290,6 +348,7 @@ test "reuses connection based on protocol", %{name: name} do end test "connection can't get up", %{name: name} do + expect(GunMock, :open, &start_and_register(&1, &2, &3)) url = "http://gun-not-up.com" assert capture_log(fn -> @@ -301,6 +360,11 @@ test "connection can't get up", %{name: name} do test "process gun_down message and then gun_up", %{name: name} do self = self() + + open_mock() + |> info_mock() + |> allow(self, name) + url = "http://gun-down-and-up.com" key = "http:gun-down-and-up.com:80" :ok = Conn.open(url, name) @@ -351,6 +415,7 @@ test "process gun_down message and then gun_up", %{name: name} do end test "async processes get same conn for same domain", %{name: name} do + open_mock() url = "http://some-domain.com" :ok = Conn.open(url, name) @@ -383,6 +448,7 @@ test "async processes get same conn for same domain", %{name: name} do end test "remove frequently used and idle", %{name: name} do + open_mock(3) self = self() http_url = "http://some-domain.com" https_url = "https://some-domain.com" @@ -437,6 +503,9 @@ test "remove frequently used and idle", %{name: name} do describe "with proxy" do test "as ip", %{name: name} do + open_mock() + |> connect_mock() + url = "http://proxy-string.com" key = "http:proxy-string.com:80" :ok = Conn.open(url, name, proxy: {{127, 0, 0, 1}, 8123}) @@ -458,6 +527,9 @@ test "as ip", %{name: name} do end test "as host", %{name: name} do + open_mock() + |> connect_mock() + url = "http://proxy-tuple-atom.com" :ok = Conn.open(url, name, proxy: {'localhost', 9050}) conn = Connections.checkin(url, name) @@ -477,6 +549,9 @@ test "as host", %{name: name} do end test "as ip and ssl", %{name: name} do + open_mock() + |> connect_mock() + url = "https://proxy-string.com" :ok = Conn.open(url, name, proxy: {{127, 0, 0, 1}, 8123}) @@ -497,6 +572,9 @@ test "as ip and ssl", %{name: name} do end test "as host and ssl", %{name: name} do + open_mock() + |> connect_mock() + url = "https://proxy-tuple-atom.com" :ok = Conn.open(url, name, proxy: {'localhost', 9050}) conn = Connections.checkin(url, name) @@ -516,6 +594,8 @@ test "as host and ssl", %{name: name} do end test "with socks type", %{name: name} do + open_mock() + url = "http://proxy-socks.com" :ok = Conn.open(url, name, proxy: {:socks5, 'localhost', 1234}) @@ -537,6 +617,7 @@ test "with socks type", %{name: name} do end test "with socks4 type and ssl", %{name: name} do + open_mock() url = "https://proxy-socks.com" :ok = Conn.open(url, name, proxy: {:socks4, 'localhost', 1234}) @@ -667,15 +748,13 @@ test "lower crf and lower reference", %{name: name} do end end - test "count/1", %{name: name} do + test "count/1" do + name = :test_count + {:ok, _} = Connections.start_link({name, [checkin_timeout: 150]}) assert Connections.count(name) == 0 Connections.add_conn(name, "1", %Conn{conn: self()}) assert Connections.count(name) == 1 Connections.remove_conn(name, "1") assert Connections.count(name) == 0 end - - defp now do - :os.system_time(:second) - end end diff --git a/test/support/gun_mock.ex b/test/support/gun_mock.ex deleted file mode 100644 index 9d664e366..000000000 --- a/test/support/gun_mock.ex +++ /dev/null @@ -1,155 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.GunMock do - @behaviour Pleroma.Gun - - alias Pleroma.Gun - alias Pleroma.GunMock - - @impl Gun - def open('some-domain.com', 443, _) do - {:ok, conn_pid} = Task.start_link(fn -> Process.sleep(1_000) end) - - Registry.register(GunMock, conn_pid, %{ - origin_scheme: "https", - origin_host: 'some-domain.com', - origin_port: 443 - }) - - {:ok, conn_pid} - end - - @impl Gun - 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(GunMock, conn_pid, %{ - origin_scheme: scheme, - origin_host: ip, - origin_port: port - }) - - {:ok, conn_pid} - end - - @impl Gun - 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(GunMock, conn_pid, %{ - origin_scheme: "http", - origin_host: 'proxy-socks.com', - origin_port: 80 - }) - - {:ok, conn_pid} - end - - @impl Gun - 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(GunMock, conn_pid, %{ - origin_scheme: "https", - origin_host: 'proxy-socks.com', - origin_port: 443 - }) - - {:ok, conn_pid} - end - - @impl Gun - def open('gun-not-up.com', 80, _opts), do: {:error, :timeout} - - @impl Gun - 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(GunMock, conn_pid, %{ - origin_scheme: "https", - origin_host: 'example.com', - origin_port: 443 - }) - - {:ok, conn_pid} - end - - @impl Gun - def open(domain, 80, _) do - {:ok, conn_pid} = Task.start_link(fn -> Process.sleep(1_000) end) - - Registry.register(GunMock, conn_pid, %{ - origin_scheme: "http", - origin_host: domain, - origin_port: 80 - }) - - {:ok, conn_pid} - end - - @impl Gun - def open({127, 0, 0, 1}, 8123, _) do - Task.start_link(fn -> Process.sleep(1_000) end) - end - - @impl Gun - def open('localhost', 9050, _) do - Task.start_link(fn -> Process.sleep(1_000) end) - end - - @impl Gun - def await_up(_pid, _timeout), do: {:ok, :http} - - @impl Gun - def set_owner(_pid, _owner), do: :ok - - @impl Gun - def connect(pid, %{host: _, port: 80}) do - ref = make_ref() - Registry.register(GunMock, ref, pid) - ref - end - - @impl Gun - def connect(pid, %{host: _, port: 443, protocols: [:http2], transport: :tls}) do - ref = make_ref() - Registry.register(GunMock, ref, pid) - ref - end - - @impl Gun - def await(pid, ref) do - [{_, ^pid}] = Registry.lookup(GunMock, ref) - {:response, :fin, 200, []} - end - - @impl Gun - def info(pid) do - [{_, info}] = Registry.lookup(GunMock, pid) - info - end - - @impl Gun - def close(_pid), do: :ok -end diff --git a/test/test_helper.exs b/test/test_helper.exs index 6b91d2b46..ee880e226 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -6,7 +6,10 @@ ExUnit.start(exclude: [:federated | os_exclude]) Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual) + Mox.defmock(Pleroma.ReverseProxy.ClientMock, for: Pleroma.ReverseProxy.Client) +Mox.defmock(Pleroma.GunMock, for: Pleroma.Gun) + {:ok, _} = Application.ensure_all_started(:ex_machina) ExUnit.after_suite(fn _results -> From c93c3096d5ffb2df1493f2b8e3f0627d9a8c5910 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Fri, 6 Mar 2020 21:04:18 +0300 Subject: [PATCH 59/88] little refactor --- lib/pleroma/gun/gun.ex | 6 ++++-- lib/pleroma/http/adapter_helper/gun.ex | 18 ++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/pleroma/gun/gun.ex b/lib/pleroma/gun/gun.ex index 81855e89e..4043e4880 100644 --- a/lib/pleroma/gun/gun.ex +++ b/lib/pleroma/gun/gun.ex @@ -11,6 +11,10 @@ defmodule Pleroma.Gun do @callback await(pid(), reference()) :: {:response, :fin, 200, []} @callback set_owner(pid(), pid()) :: :ok + @api Pleroma.Config.get([Pleroma.Gun], Pleroma.Gun.API) + + defp api, do: @api + def open(host, port, opts), do: api().open(host, port, opts) def info(pid), do: api().info(pid) @@ -24,6 +28,4 @@ def connect(pid, opts), do: api().connect(pid, opts) def await(pid, ref), do: api().await(pid, ref) def set_owner(pid, owner), do: api().set_owner(pid, owner) - - defp api, do: Pleroma.Config.get([Pleroma.Gun], Pleroma.Gun.API) end diff --git a/lib/pleroma/http/adapter_helper/gun.ex b/lib/pleroma/http/adapter_helper/gun.ex index 5d5870d90..9b03f4653 100644 --- a/lib/pleroma/http/adapter_helper/gun.ex +++ b/lib/pleroma/http/adapter_helper/gun.ex @@ -5,10 +5,9 @@ defmodule Pleroma.HTTP.AdapterHelper.Gun do @behaviour Pleroma.HTTP.AdapterHelper - alias Pleroma.HTTP.AdapterHelper - require Logger + alias Pleroma.HTTP.AdapterHelper alias Pleroma.Pool.Connections @defaults [ @@ -22,20 +21,23 @@ defmodule Pleroma.HTTP.AdapterHelper.Gun do @spec options(keyword(), URI.t()) :: keyword() def options(connection_opts \\ [], %URI{} = uri) do - proxy = Pleroma.Config.get([:http, :proxy_url], nil) + formatted_proxy = + Pleroma.Config.get([:http, :proxy_url], nil) + |> AdapterHelper.format_proxy() + + config_opts = Pleroma.Config.get([:http, :adapter], []) @defaults - |> Keyword.merge(Pleroma.Config.get([:http, :adapter], [])) + |> Keyword.merge(config_opts) |> add_scheme_opts(uri) - |> AdapterHelper.maybe_add_proxy(AdapterHelper.format_proxy(proxy)) + |> AdapterHelper.maybe_add_proxy(formatted_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) + if opts[:conn] && opts[:body_as] != :chunks do + Connections.checkout(opts[:conn], self(), :gun_connections) end :ok From 78282dc9839dbd17c4649cd3936bb8f4c8283745 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Fri, 6 Mar 2020 21:24:19 +0300 Subject: [PATCH 60/88] little polishing --- lib/pleroma/http/adapter_helper/gun.ex | 4 ++-- lib/pleroma/http/adapter_helper/hackney.ex | 4 +++- lib/pleroma/http/connection.ex | 15 ++++++++------- lib/pleroma/pool/connections.ex | 3 +-- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/lib/pleroma/http/adapter_helper/gun.ex b/lib/pleroma/http/adapter_helper/gun.ex index 9b03f4653..862e851c0 100644 --- a/lib/pleroma/http/adapter_helper/gun.ex +++ b/lib/pleroma/http/adapter_helper/gun.ex @@ -5,11 +5,11 @@ defmodule Pleroma.HTTP.AdapterHelper.Gun do @behaviour Pleroma.HTTP.AdapterHelper - require Logger - alias Pleroma.HTTP.AdapterHelper alias Pleroma.Pool.Connections + require Logger + @defaults [ connect_timeout: 5_000, domain_lookup_timeout: 5_000, diff --git a/lib/pleroma/http/adapter_helper/hackney.ex b/lib/pleroma/http/adapter_helper/hackney.ex index a0e161eaa..d08afae0c 100644 --- a/lib/pleroma/http/adapter_helper/hackney.ex +++ b/lib/pleroma/http/adapter_helper/hackney.ex @@ -13,8 +13,10 @@ defmodule Pleroma.HTTP.AdapterHelper.Hackney do def options(connection_opts \\ [], %URI{} = uri) do proxy = Pleroma.Config.get([:http, :proxy_url], nil) + config_opts = Pleroma.Config.get([:http, :adapter], []) + @defaults - |> Keyword.merge(Pleroma.Config.get([:http, :adapter], [])) + |> Keyword.merge(config_opts) |> Keyword.merge(connection_opts) |> add_scheme_opts(uri) |> Pleroma.HTTP.AdapterHelper.maybe_add_proxy(proxy) diff --git a/lib/pleroma/http/connection.ex b/lib/pleroma/http/connection.ex index 97eec88c1..777e5d4c8 100644 --- a/lib/pleroma/http/connection.ex +++ b/lib/pleroma/http/connection.ex @@ -6,6 +6,14 @@ defmodule Pleroma.HTTP.Connection do @moduledoc """ Configure Tesla.Client with default and customized adapter options. """ + + alias Pleroma.Config + alias Pleroma.HTTP.AdapterHelper + + require Logger + + @defaults [pool: :federation] + @type ip_address :: ipv4_address() | ipv6_address() @type ipv4_address :: {0..255, 0..255, 0..255, 0..255} @type ipv6_address :: @@ -13,13 +21,6 @@ defmodule Pleroma.HTTP.Connection do @type proxy_type() :: :socks4 | :socks5 @type host() :: charlist() | ip_address() - @defaults [pool: :federation] - - require Logger - - alias Pleroma.Config - alias Pleroma.HTTP.AdapterHelper - @doc """ Merge default connection & adapter options with received ones. """ diff --git a/lib/pleroma/pool/connections.ex b/lib/pleroma/pool/connections.ex index f96c08f21..7529e9240 100644 --- a/lib/pleroma/pool/connections.ex +++ b/lib/pleroma/pool/connections.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Pool.Connections do use GenServer alias Pleroma.Config + alias Pleroma.Gun require Logger @@ -19,8 +20,6 @@ defmodule Pleroma.Pool.Connections do defstruct conns: %{}, opts: [] - alias Pleroma.Gun - @spec start_link({atom(), keyword()}) :: {:ok, pid()} def start_link({name, opts}) do GenServer.start_link(__MODULE__, opts, name: name) From 14678a7708fb43e60f2f3b610f15d5090616d85c Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Sat, 7 Mar 2020 10:12:34 +0300 Subject: [PATCH 61/88] using `stub` instead `expect` --- test/http/adapter_helper/gun_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/http/adapter_helper/gun_test.exs b/test/http/adapter_helper/gun_test.exs index b1b34858a..c65b89786 100644 --- a/test/http/adapter_helper/gun_test.exs +++ b/test/http/adapter_helper/gun_test.exs @@ -23,7 +23,7 @@ defp gun_mock(_) do defp gun_mock do Pleroma.GunMock - |> expect(:open, fn _, _, _ -> Task.start_link(fn -> Process.sleep(1000) end) end) + |> stub(:open, fn _, _, _ -> Task.start_link(fn -> Process.sleep(1000) end) end) |> expect(:await_up, fn _, _ -> {:ok, :http} end) |> expect(:set_owner, fn _, _ -> :ok end) end From 9f884a263904c8b243507d35b29da712a31fb444 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Sat, 7 Mar 2020 11:01:37 +0300 Subject: [PATCH 62/88] tests changes --- test/http/adapter_helper/gun_test.exs | 4 +- test/http/connection_test.exs | 28 -------------- test/http_test.exs | 2 +- test/reverse_proxy/reverse_proxy_test.exs | 45 ++++++++++++----------- 4 files changed, 27 insertions(+), 52 deletions(-) diff --git a/test/http/adapter_helper/gun_test.exs b/test/http/adapter_helper/gun_test.exs index c65b89786..66622b605 100644 --- a/test/http/adapter_helper/gun_test.exs +++ b/test/http/adapter_helper/gun_test.exs @@ -24,8 +24,8 @@ defp gun_mock(_) do defp gun_mock do Pleroma.GunMock |> stub(:open, fn _, _, _ -> Task.start_link(fn -> Process.sleep(1000) end) end) - |> expect(:await_up, fn _, _ -> {:ok, :http} end) - |> expect(:set_owner, fn _, _ -> :ok end) + |> stub(:await_up, fn _, _ -> {:ok, :http} end) + |> stub(:set_owner, fn _, _ -> :ok end) end describe "options/1" do diff --git a/test/http/connection_test.exs b/test/http/connection_test.exs index a5ddfd435..25a2bac1c 100644 --- a/test/http/connection_test.exs +++ b/test/http/connection_test.exs @@ -7,13 +7,10 @@ defmodule Pleroma.HTTP.ConnectionTest do use Pleroma.Tests.Helpers import ExUnit.CaptureLog - import Mox alias Pleroma.Config alias Pleroma.HTTP.Connection - setup :verify_on_exit! - describe "parse_host/1" do test "as atom to charlist" do assert Connection.parse_host(:localhost) == 'localhost' @@ -115,30 +112,5 @@ test "passed opts have more weight than defaults" do 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") - - Pleroma.GunMock - |> expect(:open, fn 'some-domain.com', 443, _ -> - Task.start_link(fn -> Process.sleep(1000) end) - end) - |> expect(:await_up, fn _, _ -> {:ok, :http2} end) - |> expect(:set_owner, fn _, _ -> :ok end) - - :ok = Pleroma.Gun.Conn.open(uri, :gun_connections) - - opts = Connection.options(uri) - - assert opts[:certificates_verification] - refute opts[:tls_opts] == [] - - assert opts[:close_conn] == false - assert is_pid(opts[:conn]) - end end end diff --git a/test/http_test.exs b/test/http_test.exs index fd254b590..618485b55 100644 --- a/test/http_test.exs +++ b/test/http_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.HTTPTest do - use ExUnit.Case + use ExUnit.Case, async: true use Pleroma.Tests.Helpers import Tesla.Mock alias Pleroma.HTTP diff --git a/test/reverse_proxy/reverse_proxy_test.exs b/test/reverse_proxy/reverse_proxy_test.exs index c17ab0f89..abdfddcb7 100644 --- a/test/reverse_proxy/reverse_proxy_test.exs +++ b/test/reverse_proxy/reverse_proxy_test.exs @@ -3,14 +3,17 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.ReverseProxyTest do - use Pleroma.Web.ConnCase + use Pleroma.Web.ConnCase, async: true + import ExUnit.CaptureLog import Mox + alias Pleroma.ReverseProxy alias Pleroma.ReverseProxy.ClientMock + alias Plug.Conn setup_all do - {:ok, _} = Registry.start_link(keys: :unique, name: Pleroma.ReverseProxy.ClientMock) + {:ok, _} = Registry.start_link(keys: :unique, name: ClientMock) :ok end @@ -21,7 +24,7 @@ defp user_agent_mock(user_agent, invokes) do ClientMock |> expect(:request, fn :get, url, _, _, _ -> - Registry.register(Pleroma.ReverseProxy.ClientMock, url, 0) + Registry.register(ClientMock, url, 0) {:ok, 200, [ @@ -30,13 +33,13 @@ defp user_agent_mock(user_agent, invokes) do ], %{url: url}} end) |> expect(:stream_body, invokes, fn %{url: url} = client -> - case Registry.lookup(Pleroma.ReverseProxy.ClientMock, url) do + case Registry.lookup(ClientMock, url) do [{_, 0}] -> - Registry.update_value(Pleroma.ReverseProxy.ClientMock, url, &(&1 + 1)) + Registry.update_value(ClientMock, url, &(&1 + 1)) {:ok, json, client} [{_, 1}] -> - Registry.unregister(Pleroma.ReverseProxy.ClientMock, url) + Registry.unregister(ClientMock, url) :done end end) @@ -81,7 +84,7 @@ test "closed connection", %{conn: conn} 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) + Registry.register(ClientMock, "/stream-bytes/" <> length, 0) {:ok, 200, [{"content-type", "application/octet-stream"}], %{url: "/stream-bytes/" <> length}} @@ -89,10 +92,10 @@ defp stream_mock(invokes, with_close? \\ false) do |> 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 + case Registry.lookup(ClientMock, "/stream-bytes/" <> length) do [{_, current}] when current < max -> Registry.update_value( - Pleroma.ReverseProxy.ClientMock, + ClientMock, "/stream-bytes/" <> length, &(&1 + 10) ) @@ -100,7 +103,7 @@ defp stream_mock(invokes, with_close? \\ false) do {:ok, "0123456789", client} [{_, ^max}] -> - Registry.unregister(Pleroma.ReverseProxy.ClientMock, "/stream-bytes/" <> length) + Registry.unregister(ClientMock, "/stream-bytes/" <> length) :done end end) @@ -214,24 +217,24 @@ test "streaming", %{conn: conn} do conn = ReverseProxy.call(conn, "/stream-bytes/200") assert conn.state == :chunked assert byte_size(conn.resp_body) == 200 - assert Plug.Conn.get_resp_header(conn, "content-type") == ["application/octet-stream"] + assert Conn.get_resp_header(conn, "content-type") == ["application/octet-stream"] end defp headers_mock(_) do ClientMock |> expect(:request, fn :get, "/headers", headers, _, _ -> - Registry.register(Pleroma.ReverseProxy.ClientMock, "/headers", 0) + Registry.register(ClientMock, "/headers", 0) {:ok, 200, [{"content-type", "application/json"}], %{url: "/headers", headers: headers}} end) |> expect(:stream_body, 2, fn %{url: url, headers: headers} = client -> - case Registry.lookup(Pleroma.ReverseProxy.ClientMock, url) do + case Registry.lookup(ClientMock, url) do [{_, 0}] -> - Registry.update_value(Pleroma.ReverseProxy.ClientMock, url, &(&1 + 1)) + Registry.update_value(ClientMock, url, &(&1 + 1)) headers = for {k, v} <- headers, into: %{}, do: {String.capitalize(k), v} {:ok, Jason.encode!(%{headers: headers}), client} [{_, 1}] -> - Registry.unregister(Pleroma.ReverseProxy.ClientMock, url) + Registry.unregister(ClientMock, url) :done end end) @@ -244,7 +247,7 @@ defp headers_mock(_) do test "header passes", %{conn: conn} do conn = - Plug.Conn.put_req_header( + Conn.put_req_header( conn, "accept", "text/html" @@ -257,7 +260,7 @@ test "header passes", %{conn: conn} do test "header is filtered", %{conn: conn} do conn = - Plug.Conn.put_req_header( + Conn.put_req_header( conn, "accept-language", "en-US" @@ -301,18 +304,18 @@ test "add cache-control", %{conn: conn} do defp disposition_headers_mock(headers) do ClientMock |> expect(:request, fn :get, "/disposition", _, _, _ -> - Registry.register(Pleroma.ReverseProxy.ClientMock, "/disposition", 0) + Registry.register(ClientMock, "/disposition", 0) {:ok, 200, headers, %{url: "/disposition"}} end) |> expect(:stream_body, 2, fn %{url: "/disposition"} = client -> - case Registry.lookup(Pleroma.ReverseProxy.ClientMock, "/disposition") do + case Registry.lookup(ClientMock, "/disposition") do [{_, 0}] -> - Registry.update_value(Pleroma.ReverseProxy.ClientMock, "/disposition", &(&1 + 1)) + Registry.update_value(ClientMock, "/disposition", &(&1 + 1)) {:ok, "", client} [{_, 1}] -> - Registry.unregister(Pleroma.ReverseProxy.ClientMock, "/disposition") + Registry.unregister(ClientMock, "/disposition") :done end end) From 5f42ecc4c74172b1b17c126106fda9da24065b11 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Sat, 7 Mar 2020 12:24:39 +0300 Subject: [PATCH 63/88] start gun upload pool, if proxy_remote is enabled --- lib/pleroma/pool/supervisor.ex | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/pool/supervisor.ex b/lib/pleroma/pool/supervisor.ex index f436849ac..8dc5b64b7 100644 --- a/lib/pleroma/pool/supervisor.ex +++ b/lib/pleroma/pool/supervisor.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Pool.Supervisor do use Supervisor + alias Pleroma.Config alias Pleroma.Pool def start_link(args) do @@ -17,8 +18,7 @@ def init(_) do %{ id: Pool.Connections, start: - {Pool.Connections, :start_link, - [{:gun_connections, Pleroma.Config.get([:connections_pool])}]} + {Pool.Connections, :start_link, [{:gun_connections, Config.get([:connections_pool])}]} } ] ++ pools() @@ -26,7 +26,16 @@ def init(_) do end defp pools do - for {pool_name, pool_opts} <- Pleroma.Config.get([:pools]) do + pools = Config.get(:pools) + + pools = + if Config.get([Pleroma.Upload, :proxy_remote]) == false do + Keyword.delete(pools, :upload) + else + pools + end + + for {pool_name, pool_opts} <- pools do pool_opts |> Keyword.put(:id, {Pool, pool_name}) |> Keyword.put(:name, pool_name) From 426f5ee48a09dbf321c013db08cc849c8929d86d Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 10 Mar 2020 15:31:44 +0300 Subject: [PATCH 64/88] tesla adapter can't be changed in adminFE --- lib/pleroma/config/transfer_task.ex | 58 +++++++++---------- .../admin_api/admin_api_controller_test.exs | 21 +------ 2 files changed, 31 insertions(+), 48 deletions(-) diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex index bf1b943d8..4a4c022f0 100644 --- a/lib/pleroma/config/transfer_task.ex +++ b/lib/pleroma/config/transfer_task.ex @@ -20,8 +20,7 @@ defmodule Pleroma.Config.TransferTask do {:pleroma, :markup}, {:pleroma, :streamer}, {:pleroma, :pools}, - {:pleroma, :connections_pool}, - {:tesla, :adapter} + {:pleroma, :connections_pool} ] @reboot_time_subkeys [ @@ -35,8 +34,6 @@ defmodule Pleroma.Config.TransferTask do {:pleroma, :gopher, [:enabled]} ] - @reject [nil, :prometheus] - def start_link(_) do load_and_update_env() if Pleroma.Config.get(:env) == :test, do: Ecto.Adapters.SQL.Sandbox.checkin(Repo) @@ -45,35 +42,30 @@ def start_link(_) do @spec load_and_update_env([ConfigDB.t()]) :: :ok | false def load_and_update_env(deleted \\ [], restart_pleroma? \\ true) do - with {:configurable, true} <- - {:configurable, Pleroma.Config.get(:configurable_from_database)}, - true <- Ecto.Adapters.SQL.table_exists?(Repo, "config"), - started_applications <- Application.started_applications() do + with {_, true} <- {:configurable, Pleroma.Config.get(:configurable_from_database)} do # We need to restart applications for loaded settings take effect - in_db = Repo.all(ConfigDB) with_deleted = in_db ++ deleted - reject_for_restart = if restart_pleroma?, do: @reject, else: [:pleroma | @reject] + # TODO: some problem with prometheus after restart! + reject = [nil, :prometheus] - applications = - with_deleted - |> Enum.map(&merge_and_update(&1)) - |> Enum.uniq() - # TODO: some problem with prometheus after restart! - |> Enum.reject(&(&1 in reject_for_restart)) - - # to be ensured that pleroma will be restarted last - applications = - if :pleroma in applications do - List.delete(applications, :pleroma) ++ [:pleroma] + reject_for_restart = + if restart_pleroma? do + reject else - Restarter.Pleroma.rebooted() - applications + [:pleroma | reject] end - Enum.each(applications, &restart(started_applications, &1, Pleroma.Config.get(:env))) + started_applications = Application.started_applications() + + with_deleted + |> Enum.map(&merge_and_update(&1)) + |> Enum.uniq() + |> Enum.reject(&(&1 in reject_for_restart)) + |> maybe_set_pleroma_last() + |> Enum.each(&restart(started_applications, &1, Pleroma.Config.get(:env))) :ok else @@ -81,6 +73,18 @@ def load_and_update_env(deleted \\ [], restart_pleroma? \\ true) do end end + defp maybe_set_pleroma_last(apps) do + # to be ensured that pleroma will be restarted last + if :pleroma in apps do + apps + |> List.delete(:pleroma) + |> List.insert_at(-1, :pleroma) + else + Restarter.Pleroma.rebooted() + apps + end + end + defp group_for_restart(:logger, key, _, merged_value) do # change logger configuration in runtime, without restart if Keyword.keyword?(merged_value) and @@ -93,14 +97,10 @@ defp group_for_restart(:logger, key, _, merged_value) do 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 + if pleroma_need_restart?(group, key, value), do: group end defp merge_and_update(setting) do diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index d6b839948..76240e5bc 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -2513,8 +2513,7 @@ test "saving full setting if value is not keyword", %{conn: conn} do "value" => "Tesla.Adapter.Httpc", "db" => [":adapter"] } - ], - "need_reboot" => true + ] } end @@ -2586,9 +2585,6 @@ test "update config setting & delete with fallback to default value", %{ end test "common config example", %{conn: conn} do - adapter = Application.get_env(:tesla, :adapter) - on_exit(fn -> Application.put_env(:tesla, :adapter, adapter) end) - conn = post(conn, "/api/pleroma/admin/config", %{ configs: [ @@ -2607,16 +2603,10 @@ test "common config example", %{conn: conn} do %{"tuple" => [":regex4", "~r/https:\/\/example.com/s"]}, %{"tuple" => [":name", "Pleroma"]} ] - }, - %{ - "group" => ":tesla", - "key" => ":adapter", - "value" => "Tesla.Adapter.Httpc" } ] }) - assert Application.get_env(:tesla, :adapter) == Tesla.Adapter.Httpc assert Config.get([Pleroma.Captcha.NotReal, :name]) == "Pleroma" assert json_response(conn, 200) == %{ @@ -2648,15 +2638,8 @@ test "common config example", %{conn: conn} do ":regex4", ":name" ] - }, - %{ - "group" => ":tesla", - "key" => ":adapter", - "value" => "Tesla.Adapter.Httpc", - "db" => [":adapter"] } - ], - "need_reboot" => true + ] } end From f39e1b9eff859c0795911212c59304f68fca92bc Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 10 Mar 2020 15:54:11 +0300 Subject: [PATCH 65/88] add verify tls_opts only when we open connection for other requests tesla will add tls_opts --- lib/pleroma/gun/conn.ex | 24 +++++++++++++++++ lib/pleroma/http/adapter_helper/gun.ex | 33 ++++------------------- lib/pleroma/http/connection.ex | 13 +++++++++ test/http/adapter_helper/gun_test.exs | 37 ++++---------------------- test/http/connection_test.exs | 19 +++++++++++++ 5 files changed, 66 insertions(+), 60 deletions(-) diff --git a/lib/pleroma/gun/conn.ex b/lib/pleroma/gun/conn.ex index 319718690..57a847c30 100644 --- a/lib/pleroma/gun/conn.ex +++ b/lib/pleroma/gun/conn.ex @@ -45,6 +45,7 @@ def open(%URI{} = uri, name, opts) do |> Map.put_new(:retry, pool_opts[:retry] || 1) |> Map.put_new(:retry_timeout, pool_opts[:retry_timeout] || 1000) |> Map.put_new(:await_up_timeout, pool_opts[:await_up_timeout] || 5_000) + |> maybe_add_tls_opts(uri) key = "#{uri.scheme}:#{uri.host}:#{uri.port}" @@ -70,6 +71,29 @@ def open(%URI{} = uri, name, opts) do end end + defp maybe_add_tls_opts(opts, %URI{scheme: "http"}), do: opts + + defp maybe_add_tls_opts(opts, %URI{scheme: "https", host: host}) do + tls_opts = [ + verify: :verify_peer, + cacertfile: CAStore.file_path(), + depth: 20, + reuse_sessions: false, + verify_fun: + {&:ssl_verify_hostname.verify_fun/3, + [check_hostname: Pleroma.HTTP.Connection.format_host(host)]} + ] + + tls_opts = + if Keyword.keyword?(opts[:tls_opts]) do + Keyword.merge(tls_opts, opts[:tls_opts]) + else + tls_opts + end + + Map.put(opts, :tls_opts, tls_opts) + end + defp do_open(uri, %{proxy: {proxy_host, proxy_port}} = opts) do connect_opts = uri diff --git a/lib/pleroma/http/adapter_helper/gun.ex b/lib/pleroma/http/adapter_helper/gun.ex index 862e851c0..55c2b192a 100644 --- a/lib/pleroma/http/adapter_helper/gun.ex +++ b/lib/pleroma/http/adapter_helper/gun.ex @@ -45,21 +45,11 @@ def after_request(opts) do defp add_scheme_opts(opts, %URI{scheme: "http"}), do: opts - defp add_scheme_opts(opts, %URI{scheme: "https", host: host}) do - adapter_opts = [ - certificates_verification: true, - transport: :tls, - tls_opts: [ - verify: :verify_peer, - cacertfile: CAStore.file_path(), - depth: 20, - reuse_sessions: false, - verify_fun: {&:ssl_verify_hostname.verify_fun/3, [check_hostname: format_host(host)]}, - log_level: :warning - ] - ] - - Keyword.merge(opts, adapter_opts) + defp add_scheme_opts(opts, %URI{scheme: "https"}) do + opts + |> Keyword.put(:certificates_verification, true) + |> Keyword.put(:transport, :tls) + |> Keyword.put(:tls_opts, log_level: :warning) end defp maybe_get_conn(adapter_opts, uri, connection_opts) do @@ -93,17 +83,4 @@ defp try_to_get_conn(uri, opts) do |> Keyword.put(:close_conn, false) end end - - @spec format_host(String.t()) :: charlist() - def format_host(host) do - host_charlist = to_charlist(host) - - case :inet.parse_address(host_charlist) do - {:error, :einval} -> - :idna.encode(host_charlist) - - {:ok, _ip} -> - host_charlist - end - end end diff --git a/lib/pleroma/http/connection.ex b/lib/pleroma/http/connection.ex index 777e5d4c8..0fc88f708 100644 --- a/lib/pleroma/http/connection.ex +++ b/lib/pleroma/http/connection.ex @@ -106,4 +106,17 @@ def parse_host(host) when is_binary(host) do {:ok, ip} -> ip end end + + @spec format_host(String.t()) :: charlist() + def format_host(host) do + host_charlist = to_charlist(host) + + case :inet.parse_address(host_charlist) do + {:error, :einval} -> + :idna.encode(host_charlist) + + {:ok, _ip} -> + host_charlist + end + end end diff --git a/test/http/adapter_helper/gun_test.exs b/test/http/adapter_helper/gun_test.exs index 66622b605..6af8be15d 100644 --- a/test/http/adapter_helper/gun_test.exs +++ b/test/http/adapter_helper/gun_test.exs @@ -38,31 +38,23 @@ test "https url with default port" do opts = Gun.options([receive_conn: false], uri) assert opts[:certificates_verification] - refute opts[:tls_opts] == [] - - assert opts[:tls_opts][:verify_fun] == - {&:ssl_verify_hostname.verify_fun/3, [check_hostname: 'example.com']} - - assert File.exists?(opts[:tls_opts][:cacertfile]) + assert opts[:tls_opts][:log_level] == :warning end test "https ipv4 with default port" do uri = URI.parse("https://127.0.0.1") opts = Gun.options([receive_conn: false], uri) - - assert opts[:tls_opts][:verify_fun] == - {&:ssl_verify_hostname.verify_fun/3, [check_hostname: '127.0.0.1']} + assert opts[:certificates_verification] + assert opts[:tls_opts][:log_level] == :warning end test "https ipv6 with default port" do uri = URI.parse("https://[2a03:2880:f10c:83:face:b00c:0:25de]") opts = Gun.options([receive_conn: false], uri) - - assert opts[:tls_opts][:verify_fun] == - {&:ssl_verify_hostname.verify_fun/3, - [check_hostname: '2a03:2880:f10c:83:face:b00c:0:25de']} + assert opts[:certificates_verification] + assert opts[:tls_opts][:log_level] == :warning end test "https url with non standart port" do @@ -269,23 +261,4 @@ test "with ipv6" do } = Connections.get_state(:gun_connections) end end - - describe "format_host/1" do - test "with domain" do - assert Gun.format_host("example.com") == 'example.com' - end - - test "with idna domain" do - assert Gun.format_host("ですexample.com") == 'xn--example-183fne.com' - end - - test "with ipv4" do - assert Gun.format_host("127.0.0.1") == '127.0.0.1' - end - - test "with ipv6" do - assert Gun.format_host("2a03:2880:f10c:83:face:b00c:0:25de") == - '2a03:2880:f10c:83:face:b00c:0:25de' - end - end end diff --git a/test/http/connection_test.exs b/test/http/connection_test.exs index 25a2bac1c..0f62eddd2 100644 --- a/test/http/connection_test.exs +++ b/test/http/connection_test.exs @@ -113,4 +113,23 @@ test "passed opts have more weight than defaults" do assert opts[:proxy] == {'example.com', 4321} end end + + describe "format_host/1" do + test "with domain" do + assert Connection.format_host("example.com") == 'example.com' + end + + test "with idna domain" do + assert Connection.format_host("ですexample.com") == 'xn--example-183fne.com' + end + + test "with ipv4" do + assert Connection.format_host("127.0.0.1") == '127.0.0.1' + end + + test "with ipv6" do + assert Connection.format_host("2a03:2880:f10c:83:face:b00c:0:25de") == + '2a03:2880:f10c:83:face:b00c:0:25de' + end + end end From 863ec33ba2a90708d199f18683ffe0c4658c710a Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Wed, 11 Mar 2020 12:21:44 +0100 Subject: [PATCH 66/88] Add support for funkwhale Audio activity reel2bits fixture not included as it lacks the Actor fixture for it. Closes: https://git.pleroma.social/pleroma/pleroma/issues/1624 Closes: https://git.pleroma.social/pleroma/pleroma/issues/764 --- .../web/activity_pub/transmogrifier.ex | 5 ++- .../web/mastodon_api/views/status_view.ex | 2 +- test/fixtures/tesla_mock/funkwhale_audio.json | 44 +++++++++++++++++++ .../tesla_mock/funkwhale_channel.json | 44 +++++++++++++++++++ test/support/http_request_mock.ex | 15 +++++++ .../mastodon_api/views/status_view_test.exs | 16 +++++++ test/web/oauth/oauth_controller_test.exs | 2 +- 7 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 test/fixtures/tesla_mock/funkwhale_audio.json create mode 100644 test/fixtures/tesla_mock/funkwhale_channel.json diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 9cd3de705..f52b065f6 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -229,7 +229,8 @@ def fix_url(%{"url" => url} = object) when is_map(url) do Map.put(object, "url", url["href"]) end - def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do + def fix_url(%{"type" => object_type, "url" => url} = object) + when object_type in ["Video", "Audio"] and is_list(url) do first_element = Enum.at(url, 0) link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end) @@ -398,7 +399,7 @@ def handle_incoming( %{"type" => "Create", "object" => %{"type" => objtype} = object} = data, options ) - when objtype in ["Article", "Event", "Note", "Video", "Page", "Question", "Answer"] do + when objtype in ["Article", "Event", "Note", "Video", "Page", "Question", "Answer", "Audio"] do actor = Containment.get_actor(data) data = diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index f7469cdff..a042075f5 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -421,7 +421,7 @@ def get_reply_to(%{data: %{"object" => _object}} = activity, _) do end def render_content(%{data: %{"type" => object_type}} = object) - when object_type in ["Video", "Event"] do + when object_type in ["Video", "Event", "Audio"] do with name when not is_nil(name) and name != "" <- object.data["name"] do "

#{name}

#{object.data["content"]}" else diff --git a/test/fixtures/tesla_mock/funkwhale_audio.json b/test/fixtures/tesla_mock/funkwhale_audio.json new file mode 100644 index 000000000..15736b1f8 --- /dev/null +++ b/test/fixtures/tesla_mock/funkwhale_audio.json @@ -0,0 +1,44 @@ +{ + "id": "https://channels.tests.funkwhale.audio/federation/music/uploads/42342395-0208-4fee-a38d-259a6dae0871", + "type": "Audio", + "name": "Compositions - Test Audio for Pleroma", + "attributedTo": "https://channels.tests.funkwhale.audio/federation/actors/compositions", + "published": "2020-03-11T10:01:52.714918+00:00", + "to": "https://www.w3.org/ns/activitystreams#Public", + "url": [ + { + "type": "Link", + "mimeType": "audio/ogg", + "href": "https://channels.tests.funkwhale.audio/api/v1/listen/3901e5d8-0445-49d5-9711-e096cf32e515/?upload=42342395-0208-4fee-a38d-259a6dae0871&download=false" + }, + { + "type": "Link", + "mimeType": "text/html", + "href": "https://channels.tests.funkwhale.audio/library/tracks/74" + } + ], + "content": "

This is a test Audio for Pleroma.

", + "mediaType": "text/html", + "tag": [ + { + "type": "Hashtag", + "name": "#funkwhale" + }, + { + "type": "Hashtag", + "name": "#test" + }, + { + "type": "Hashtag", + "name": "#tests" + } + ], + "summary": "#funkwhale #test #tests", + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers" + } + ] +} diff --git a/test/fixtures/tesla_mock/funkwhale_channel.json b/test/fixtures/tesla_mock/funkwhale_channel.json new file mode 100644 index 000000000..cf9ee8151 --- /dev/null +++ b/test/fixtures/tesla_mock/funkwhale_channel.json @@ -0,0 +1,44 @@ +{ + "id": "https://channels.tests.funkwhale.audio/federation/actors/compositions", + "outbox": "https://channels.tests.funkwhale.audio/federation/actors/compositions/outbox", + "inbox": "https://channels.tests.funkwhale.audio/federation/actors/compositions/inbox", + "preferredUsername": "compositions", + "type": "Person", + "name": "Compositions", + "followers": "https://channels.tests.funkwhale.audio/federation/actors/compositions/followers", + "following": "https://channels.tests.funkwhale.audio/federation/actors/compositions/following", + "manuallyApprovesFollowers": false, + "url": [ + { + "type": "Link", + "href": "https://channels.tests.funkwhale.audio/channels/compositions", + "mediaType": "text/html" + }, + { + "type": "Link", + "href": "https://channels.tests.funkwhale.audio/api/v1/channels/compositions/rss", + "mediaType": "application/rss+xml" + } + ], + "icon": { + "type": "Image", + "url": "https://channels.tests.funkwhale.audio/media/attachments/75/b4/f1/nosmile.jpeg", + "mediaType": "image/jpeg" + }, + "summary": "

I'm testing federation with the fediverse :)

", + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers" + } + ], + "publicKey": { + "owner": "https://channels.tests.funkwhale.audio/federation/actors/compositions", + "publicKeyPem": "-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAv25u57oZfVLV3KltS+HcsdSx9Op4MmzIes1J8Wu8s0KbdXf2zEwS\nsVqyHgs/XCbnzsR3FqyJTo46D2BVnvZcuU5srNcR2I2HMaqQ0oVdnATE4K6KdcgV\nN+98pMWo56B8LTgE1VpvqbsrXLi9jCTzjrkebVMOP+ZVu+64v1qdgddseblYMnBZ\nct0s7ONbHnqrWlTGf5wES1uIZTVdn5r4MduZG+Uenfi1opBS0lUUxfWdW9r0oF2b\nyneZUyaUCbEroeKbqsweXCWVgnMarUOsgqC42KM4cf95lySSwTSaUtZYIbTw7s9W\n2jveU/rVg8BYZu5JK5obgBoxtlUeUoSswwIDAQAB\n-----END RSA PUBLIC KEY-----\n", + "id": "https://channels.tests.funkwhale.audio/federation/actors/compositions#main-key" + }, + "endpoints": { + "sharedInbox": "https://channels.tests.funkwhale.audio/federation/shared/inbox" + } +} diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index d46887865..0079d8c44 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -1273,6 +1273,21 @@ def get("https://patch.cx/users/rin", _, _, _) do {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/rin.json")}} end + def get( + "https://channels.tests.funkwhale.audio/federation/music/uploads/42342395-0208-4fee-a38d-259a6dae0871", + _, + _, + _ + ) do + {:ok, + %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/funkwhale_audio.json")}} + end + + def get("https://channels.tests.funkwhale.audio/federation/actors/compositions", _, _, _) do + {:ok, + %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/funkwhale_channel.json")}} + end + def get("http://example.com/rel_me/error", _, _, _) do {:ok, %Tesla.Env{status: 404, body: ""}} end diff --git a/test/web/mastodon_api/views/status_view_test.exs b/test/web/mastodon_api/views/status_view_test.exs index 191895c6f..3e1812a1f 100644 --- a/test/web/mastodon_api/views/status_view_test.exs +++ b/test/web/mastodon_api/views/status_view_test.exs @@ -420,6 +420,22 @@ test "a peertube video" do assert length(represented[:media_attachments]) == 1 end + test "funkwhale audio" do + user = insert(:user) + + {:ok, object} = + Pleroma.Object.Fetcher.fetch_object_from_id( + "https://channels.tests.funkwhale.audio/federation/music/uploads/42342395-0208-4fee-a38d-259a6dae0871" + ) + + %Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"]) + + represented = StatusView.render("show.json", %{for: user, activity: activity}) + + assert represented[:id] == to_string(activity.id) + assert length(represented[:media_attachments]) == 1 + end + test "a Mobilizon event" do user = insert(:user) diff --git a/test/web/oauth/oauth_controller_test.exs b/test/web/oauth/oauth_controller_test.exs index cff469c28..5f86d999c 100644 --- a/test/web/oauth/oauth_controller_test.exs +++ b/test/web/oauth/oauth_controller_test.exs @@ -581,7 +581,7 @@ test "redirects with oauth authorization, " <> # In case scope param is missing, expecting _all_ app-supported scopes to be granted for user <- [non_admin, admin], {requested_scopes, expected_scopes} <- - %{scopes_subset => scopes_subset, nil => app_scopes} do + %{scopes_subset => scopes_subset, nil: app_scopes} do conn = post( build_conn(), From 1306b92997dc6e76e5d617d529dbc229d5aee200 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Thu, 12 Mar 2020 18:28:54 +0300 Subject: [PATCH 67/88] clean up --- lib/pleroma/application.ex | 18 +++-- lib/pleroma/config/transfer_task.ex | 42 +++++------ lib/pleroma/gun/conn.ex | 31 ++++----- lib/pleroma/http/adapter_helper.ex | 2 +- lib/pleroma/http/adapter_helper/gun.ex | 33 ++++----- lib/pleroma/http/connection.ex | 8 +-- lib/pleroma/http/http.ex | 5 +- lib/pleroma/pool/connections.ex | 96 +++++++++----------------- test/http/adapter_helper/gun_test.exs | 12 ++-- test/pool/connections_test.exs | 2 +- 10 files changed, 94 insertions(+), 155 deletions(-) diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index c8a0617a5..55b5be488 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -42,7 +42,9 @@ def start(_type, _args) do setup_instrumenters() load_custom_modules() - if adapter() == Tesla.Adapter.Gun do + adapter = Application.get_env(:tesla, :adapter) + + if adapter == Tesla.Adapter.Gun do if version = Pleroma.OTPVersion.version() do [major, minor] = version @@ -74,7 +76,7 @@ def start(_type, _args) do Pleroma.Plugs.RateLimiter.Supervisor ] ++ cachex_children() ++ - http_pools_children(Config.get(:env)) ++ + http_children(adapter, @env) ++ [ Pleroma.Stats, Pleroma.JobQueueMonitor, @@ -206,15 +208,13 @@ defp task_children(_) do end # start hackney and gun pools in tests - defp http_pools_children(:test) do + defp http_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: http_pools(adapter()) - - defp http_pools(Tesla.Adapter.Hackney) do + defp http_children(Tesla.Adapter.Hackney, _) do pools = [:federation, :media] pools = @@ -230,9 +230,7 @@ defp http_pools(Tesla.Adapter.Hackney) do end end - defp http_pools(Tesla.Adapter.Gun), do: [Pleroma.Pool.Supervisor] + defp http_children(Tesla.Adapter.Gun, _), do: [Pleroma.Pool.Supervisor] - defp http_pools(_), do: [] - - defp adapter, do: Application.get_env(:tesla, :adapter) + defp http_children(_, _), do: [] end diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex index 4a4c022f0..b6d80adb7 100644 --- a/lib/pleroma/config/transfer_task.ex +++ b/lib/pleroma/config/transfer_task.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Config.TransferTask do use Task + alias Pleroma.Config alias Pleroma.ConfigDB alias Pleroma.Repo @@ -36,36 +37,31 @@ defmodule Pleroma.Config.TransferTask do def start_link(_) do load_and_update_env() - if Pleroma.Config.get(:env) == :test, do: Ecto.Adapters.SQL.Sandbox.checkin(Repo) + if Config.get(:env) == :test, do: Ecto.Adapters.SQL.Sandbox.checkin(Repo) :ignore end - @spec load_and_update_env([ConfigDB.t()]) :: :ok | false - def load_and_update_env(deleted \\ [], restart_pleroma? \\ true) do - with {_, true} <- {:configurable, Pleroma.Config.get(:configurable_from_database)} do + @spec load_and_update_env([ConfigDB.t()], boolean()) :: :ok + def load_and_update_env(deleted_settings \\ [], restart_pleroma? \\ true) do + with {_, true} <- {:configurable, Config.get(:configurable_from_database)} do # We need to restart applications for loaded settings take effect - in_db = Repo.all(ConfigDB) - - with_deleted = in_db ++ deleted # TODO: some problem with prometheus after restart! - reject = [nil, :prometheus] - - reject_for_restart = + reject_restart = if restart_pleroma? do - reject + [nil, :prometheus] else - [:pleroma | reject] + [:pleroma, nil, :prometheus] end started_applications = Application.started_applications() - with_deleted - |> Enum.map(&merge_and_update(&1)) + (Repo.all(ConfigDB) ++ deleted_settings) + |> Enum.map(&merge_and_update/1) |> Enum.uniq() - |> Enum.reject(&(&1 in reject_for_restart)) + |> Enum.reject(&(&1 in reject_restart)) |> maybe_set_pleroma_last() - |> Enum.each(&restart(started_applications, &1, Pleroma.Config.get(:env))) + |> Enum.each(&restart(started_applications, &1, Config.get(:env))) :ok else @@ -108,18 +104,14 @@ defp merge_and_update(setting) do key = ConfigDB.from_string(setting.key) group = ConfigDB.from_string(setting.group) - default = Pleroma.Config.Holder.config(group, key) + default = Config.Holder.config(group, key) value = ConfigDB.from_binary(setting.value) merged_value = - if Ecto.get_meta(setting, :state) == :deleted do - default - else - if can_be_merged?(default, value) do - ConfigDB.merge_group(group, key, default, value) - else - value - end + cond do + Ecto.get_meta(setting, :state) == :deleted -> default + can_be_merged?(default, value) -> ConfigDB.merge_group(group, key, default, value) + true -> value end :ok = update_env(group, key, merged_value) diff --git a/lib/pleroma/gun/conn.ex b/lib/pleroma/gun/conn.ex index 57a847c30..20823a765 100644 --- a/lib/pleroma/gun/conn.ex +++ b/lib/pleroma/gun/conn.ex @@ -49,8 +49,6 @@ def open(%URI{} = uri, name, opts) do key = "#{uri.scheme}:#{uri.host}:#{uri.port}" - Logger.debug("opening new connection #{Connections.compose_uri_log(uri)}") - conn_pid = if Connections.count(name) < opts[:max_connection] do do_open(uri, opts) @@ -109,9 +107,9 @@ defp do_open(uri, %{proxy: {proxy_host, proxy_port}} = opts) do else error -> Logger.warn( - "Received error on opening connection with http proxy #{ - Connections.compose_uri_log(uri) - } #{inspect(error)}" + "Opening proxied connection to #{compose_uri_log(uri)} failed with error #{ + inspect(error) + }" ) error @@ -145,9 +143,9 @@ defp do_open(uri, %{proxy: {proxy_type, proxy_host, proxy_port}} = opts) do else error -> Logger.warn( - "Received error on opening connection with socks proxy #{ - Connections.compose_uri_log(uri) - } #{inspect(error)}" + "Opening socks proxied connection to #{compose_uri_log(uri)} failed with error #{ + inspect(error) + }" ) error @@ -163,9 +161,7 @@ defp do_open(%URI{host: host, port: port} = uri, opts) do else error -> Logger.warn( - "Received error on opening connection #{Connections.compose_uri_log(uri)} #{ - inspect(error) - }" + "Opening connection to #{compose_uri_log(uri)} failed with error #{inspect(error)}" ) error @@ -184,16 +180,17 @@ defp add_http2_opts(opts, "https", tls_opts) do defp add_http2_opts(opts, _, _), do: opts defp close_least_used_and_do_open(name, uri, opts) do - Logger.debug("try to open conn #{Connections.compose_uri_log(uri)}") - - with [{close_key, least_used} | _conns] <- - Connections.get_unused_conns(name), - :ok <- Gun.close(least_used.conn) do - Connections.remove_conn(name, close_key) + with [{key, conn} | _conns] <- Connections.get_unused_conns(name), + :ok <- Gun.close(conn.conn) do + Connections.remove_conn(name, key) do_open(uri, opts) else [] -> {:error, :pool_overflowed} end end + + def compose_uri_log(%URI{scheme: scheme, host: host, path: path}) do + "#{scheme}://#{host}#{path}" + end end diff --git a/lib/pleroma/http/adapter_helper.ex b/lib/pleroma/http/adapter_helper.ex index 2c13666ec..510722ff9 100644 --- a/lib/pleroma/http/adapter_helper.ex +++ b/lib/pleroma/http/adapter_helper.ex @@ -7,7 +7,7 @@ defmodule Pleroma.HTTP.AdapterHelper do @type proxy :: {Connection.host(), pos_integer()} - | {Connection.proxy_type(), pos_integer()} + | {Connection.proxy_type(), Connection.host(), pos_integer()} @callback options(keyword(), URI.t()) :: keyword() @callback after_request(keyword()) :: :ok diff --git a/lib/pleroma/http/adapter_helper/gun.ex b/lib/pleroma/http/adapter_helper/gun.ex index 55c2b192a..f14b95c19 100644 --- a/lib/pleroma/http/adapter_helper/gun.ex +++ b/lib/pleroma/http/adapter_helper/gun.ex @@ -20,8 +20,8 @@ defmodule Pleroma.HTTP.AdapterHelper.Gun do ] @spec options(keyword(), URI.t()) :: keyword() - def options(connection_opts \\ [], %URI{} = uri) do - formatted_proxy = + def options(incoming_opts \\ [], %URI{} = uri) do + proxy = Pleroma.Config.get([:http, :proxy_url], nil) |> AdapterHelper.format_proxy() @@ -30,8 +30,8 @@ def options(connection_opts \\ [], %URI{} = uri) do @defaults |> Keyword.merge(config_opts) |> add_scheme_opts(uri) - |> AdapterHelper.maybe_add_proxy(formatted_proxy) - |> maybe_get_conn(uri, connection_opts) + |> AdapterHelper.maybe_add_proxy(proxy) + |> maybe_get_conn(uri, incoming_opts) end @spec after_request(keyword()) :: :ok @@ -43,44 +43,35 @@ def after_request(opts) do :ok end - defp add_scheme_opts(opts, %URI{scheme: "http"}), do: opts + defp add_scheme_opts(opts, %{scheme: "http"}), do: opts - defp add_scheme_opts(opts, %URI{scheme: "https"}) do + defp add_scheme_opts(opts, %{scheme: "https"}) do opts |> Keyword.put(:certificates_verification, true) - |> Keyword.put(:transport, :tls) |> Keyword.put(:tls_opts, log_level: :warning) end - defp maybe_get_conn(adapter_opts, uri, connection_opts) do + defp maybe_get_conn(adapter_opts, uri, incoming_opts) do {receive_conn?, opts} = adapter_opts - |> Keyword.merge(connection_opts) + |> Keyword.merge(incoming_opts) |> Keyword.pop(:receive_conn, true) if Connections.alive?(:gun_connections) and receive_conn? do - try_to_get_conn(uri, opts) + checkin_conn(uri, opts) else opts end end - defp try_to_get_conn(uri, opts) do + defp checkin_conn(uri, opts) do case Connections.checkin(uri, :gun_connections) do nil -> - Logger.debug( - "Gun connections pool checkin was not successful. Trying to open conn for next request." - ) - - Task.start(fn -> Pleroma.Gun.Conn.open(uri, :gun_connections, opts) end) + Task.start(Pleroma.Gun.Conn, :open, [uri, :gun_connections, opts]) opts conn when is_pid(conn) -> - Logger.debug("received conn #{inspect(conn)} #{Connections.compose_uri_log(uri)}") - - opts - |> Keyword.put(:conn, conn) - |> Keyword.put(:close_conn, false) + Keyword.merge(opts, conn: conn, close_conn: false) end end end diff --git a/lib/pleroma/http/connection.ex b/lib/pleroma/http/connection.ex index 0fc88f708..76de3fcfe 100644 --- a/lib/pleroma/http/connection.ex +++ b/lib/pleroma/http/connection.ex @@ -71,15 +71,15 @@ def parse_proxy(proxy) when is_binary(proxy) do {:ok, parse_host(host), port} else {_, _} -> - Logger.warn("parsing port in proxy fail #{inspect(proxy)}") + Logger.warn("Parsing port failed #{inspect(proxy)}") {:error, :invalid_proxy_port} :error -> - Logger.warn("parsing port in proxy fail #{inspect(proxy)}") + Logger.warn("Parsing port failed #{inspect(proxy)}") {:error, :invalid_proxy_port} _ -> - Logger.warn("parsing proxy fail #{inspect(proxy)}") + Logger.warn("Parsing proxy failed #{inspect(proxy)}") {:error, :invalid_proxy} end end @@ -89,7 +89,7 @@ def parse_proxy(proxy) when is_tuple(proxy) do {:ok, type, parse_host(host), port} else _ -> - Logger.warn("parsing proxy fail #{inspect(proxy)}") + Logger.warn("Parsing proxy failed #{inspect(proxy)}") {:error, :invalid_proxy} end end diff --git a/lib/pleroma/http/http.ex b/lib/pleroma/http/http.ex index 466a94adc..583b56484 100644 --- a/lib/pleroma/http/http.ex +++ b/lib/pleroma/http/http.ex @@ -56,10 +56,9 @@ def post(url, body, headers \\ [], options \\ []), {:ok, Env.t()} | {:error, any()} def request(method, url, body, headers, options) when is_binary(url) do uri = URI.parse(url) - received_adapter_opts = Keyword.get(options, :adapter, []) - adapter_opts = Connection.options(uri, received_adapter_opts) + adapter_opts = Connection.options(uri, options[:adapter] || []) options = put_in(options[:adapter], adapter_opts) - params = Keyword.get(options, :params, []) + params = options[:params] || [] request = build_request(method, headers, options, url, body, params) adapter = Application.get_env(:tesla, :adapter) diff --git a/lib/pleroma/pool/connections.ex b/lib/pleroma/pool/connections.ex index 7529e9240..772833509 100644 --- a/lib/pleroma/pool/connections.ex +++ b/lib/pleroma/pool/connections.ex @@ -87,18 +87,11 @@ def handle_cast({:add_conn, key, conn}, state) do @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 + conn_state = if used_by == [], do: :idle, else: conn.conn_state put_in(state.conns[key], %{conn | conn_state: conn_state, used_by: used_by}) else @@ -123,26 +116,23 @@ def handle_cast({:remove_conn, key}, state) do @impl true def handle_call({:checkin, uri}, from, state) do key = "#{uri.scheme}:#{uri.host}:#{uri.port}" - Logger.debug("checkin #{key}") case state.conns[key] do - %{conn: conn, gun_state: :up} = current_conn -> - Logger.debug("reusing conn #{key}") - + %{conn: pid, gun_state: :up} = conn -> time = :os.system_time(:second) - last_reference = time - current_conn.last_reference - current_crf = crf(last_reference, 100, current_conn.crf) + last_reference = time - conn.last_reference + crf = crf(last_reference, 100, conn.crf) state = put_in(state.conns[key], %{ - current_conn + conn | last_reference: time, - crf: current_crf, + crf: crf, conn_state: :active, - used_by: [from | current_conn.used_by] + used_by: [from | conn.used_by] }) - {:reply, conn, state} + {:reply, pid, state} %{gun_state: :down} -> {:reply, nil, state} @@ -164,50 +154,48 @@ def handle_call(:count, _from, state) do def handle_call(:unused_conns, _from, state) do unused_conns = state.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) + |> Enum.filter(&filter_conns/1) + |> Enum.sort(&sort_conns/2) {:reply, unused_conns, state} end + defp filter_conns({_, %{conn_state: :idle, used_by: []}}), do: true + defp filter_conns(_), do: false + + defp sort_conns({_, c1}, {_, c2}) do + c1.crf <= c2.crf and c1.last_reference <= c2.last_reference + end + @impl true def handle_info({:gun_up, conn_pid, _protocol}, state) do - state = - with conn_key when is_binary(conn_key) <- compose_key_gun_info(conn_pid), - {key, conn} <- find_conn(state.conns, conn_pid, conn_key), - {true, key} <- {Process.alive?(conn_pid), key} do - time = :os.system_time(:second) - last_reference = time - conn.last_reference - current_crf = crf(last_reference, 100, conn.crf) + %{origin_host: host, origin_scheme: scheme, origin_port: port} = Gun.info(conn_pid) + host = + case :inet.ntoa(host) do + {:error, :einval} -> host + ip -> ip + end + + key = "#{scheme}:#{host}:#{port}" + + state = + with {_key, conn} <- find_conn(state.conns, conn_pid, key), + {true, key} <- {Process.alive?(conn_pid), key} 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.debug(":gun.info caused error") - state - {false, key} -> - Logger.debug(":gun_up message for closed conn #{inspect(conn_pid)}") - put_in( state.conns, Map.delete(state.conns, key) ) nil -> - Logger.debug(":gun_up message for conn which is not found in state") - :ok = Gun.close(conn_pid) state @@ -224,7 +212,6 @@ def handle_info({:gun_down, conn_pid, _protocol, _reason, _killed}, state) do with {key, conn} <- find_conn(state.conns, conn_pid), {true, key} <- {Process.alive?(conn_pid), key} do if conn.retries == retries do - Logger.debug("closing conn if retries is eq #{inspect(conn_pid)}") :ok = Gun.close(conn.conn) put_in( @@ -240,18 +227,13 @@ def handle_info({:gun_down, conn_pid, _protocol, _reason, _killed}, state) do end else {false, key} -> - # gun can send gun_down for closed conn, maybe connection is not closed yet - Logger.debug(":gun_down message for closed conn #{inspect(conn_pid)}") - put_in( state.conns, Map.delete(state.conns, key) ) nil -> - Logger.debug(":gun_down message for conn which is not found in state") - - :ok = Gun.close(conn_pid) + Logger.debug(":gun_down for conn which isn't found in state") state end @@ -275,7 +257,7 @@ def handle_info({:DOWN, _ref, :process, conn_pid, reason}, state) do ) else nil -> - Logger.debug(":DOWN message for conn which is not found in state") + Logger.debug(":DOWN for conn which isn't found in state") state end @@ -283,18 +265,6 @@ def handle_info({:DOWN, _ref, :process, conn_pid, reason}, state) do {:noreply, state} end - defp compose_key_gun_info(pid) do - %{origin_host: origin_host, origin_scheme: scheme, origin_port: port} = Gun.info(pid) - - host = - case :inet.ntoa(origin_host) do - {:error, :einval} -> origin_host - ip -> ip - end - - "#{scheme}:#{host}:#{port}" - end - defp find_conn(conns, conn_pid) do Enum.find(conns, fn {_key, conn} -> conn.conn == conn_pid @@ -310,8 +280,4 @@ defp find_conn(conns, conn_pid, conn_key) do def crf(current, steps, crf) do 1 + :math.pow(0.5, current / steps) * crf end - - def compose_uri_log(%URI{scheme: scheme, host: host, path: path}) do - "#{scheme}://#{host}#{path}" - end end diff --git a/test/http/adapter_helper/gun_test.exs b/test/http/adapter_helper/gun_test.exs index 6af8be15d..18025b986 100644 --- a/test/http/adapter_helper/gun_test.exs +++ b/test/http/adapter_helper/gun_test.exs @@ -6,7 +6,6 @@ defmodule Pleroma.HTTP.AdapterHelper.GunTest do use ExUnit.Case, async: true use Pleroma.Tests.Helpers - import ExUnit.CaptureLog import Mox alias Pleroma.Config @@ -63,7 +62,6 @@ test "https url with non standart port" do opts = Gun.options([receive_conn: false], uri) assert opts[:certificates_verification] - assert opts[:transport] == :tls end test "get conn on next request" do @@ -73,14 +71,12 @@ test "get conn on next request" do on_exit(fn -> Logger.configure(level: level) end) uri = URI.parse("http://some-domain2.com") - assert capture_log(fn -> - opts = Gun.options(uri) + opts = Gun.options(uri) - assert opts[:conn] == nil - assert opts[:close_conn] == nil - end) =~ - "Gun connections pool checkin was not successful. Trying to open conn for next request." + assert opts[:conn] == nil + assert opts[:close_conn] == nil + Process.sleep(50) opts = Gun.options(uri) assert is_pid(opts[:conn]) diff --git a/test/pool/connections_test.exs b/test/pool/connections_test.exs index 06f32b74e..aeda54875 100644 --- a/test/pool/connections_test.exs +++ b/test/pool/connections_test.exs @@ -355,7 +355,7 @@ test "connection can't get up", %{name: name} do refute Conn.open(url, name) refute Connections.checkin(url, name) end) =~ - "Received error on opening connection http://gun-not-up.com {:error, :timeout}" + "Opening connection to http://gun-not-up.com failed with error {:error, :timeout}" end test "process gun_down message and then gun_up", %{name: name} do From 98ed0d1c4bd2db354154cc4a1d1e6530eb68f499 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Fri, 13 Mar 2020 09:37:57 +0300 Subject: [PATCH 68/88] more clean up --- lib/pleroma/http/adapter_helper/gun.ex | 2 +- lib/pleroma/http/adapter_helper/hackney.ex | 2 +- lib/pleroma/http/connection.ex | 12 +++++++----- lib/pleroma/pool/request.ex | 1 - lib/pleroma/pool/supervisor.ex | 15 ++++++--------- lib/pleroma/reverse_proxy/client/tesla.ex | 9 +++++---- lib/pleroma/reverse_proxy/reverse_proxy.ex | 2 +- 7 files changed, 21 insertions(+), 22 deletions(-) diff --git a/lib/pleroma/http/adapter_helper/gun.ex b/lib/pleroma/http/adapter_helper/gun.ex index f14b95c19..ead7cdc6b 100644 --- a/lib/pleroma/http/adapter_helper/gun.ex +++ b/lib/pleroma/http/adapter_helper/gun.ex @@ -22,7 +22,7 @@ defmodule Pleroma.HTTP.AdapterHelper.Gun do @spec options(keyword(), URI.t()) :: keyword() def options(incoming_opts \\ [], %URI{} = uri) do proxy = - Pleroma.Config.get([:http, :proxy_url], nil) + Pleroma.Config.get([:http, :proxy_url]) |> AdapterHelper.format_proxy() config_opts = Pleroma.Config.get([:http, :adapter], []) diff --git a/lib/pleroma/http/adapter_helper/hackney.ex b/lib/pleroma/http/adapter_helper/hackney.ex index d08afae0c..dcb4cac71 100644 --- a/lib/pleroma/http/adapter_helper/hackney.ex +++ b/lib/pleroma/http/adapter_helper/hackney.ex @@ -11,7 +11,7 @@ defmodule Pleroma.HTTP.AdapterHelper.Hackney do @spec options(keyword(), URI.t()) :: keyword() def options(connection_opts \\ [], %URI{} = uri) do - proxy = Pleroma.Config.get([:http, :proxy_url], nil) + proxy = Pleroma.Config.get([:http, :proxy_url]) config_opts = Pleroma.Config.get([:http, :adapter], []) diff --git a/lib/pleroma/http/connection.ex b/lib/pleroma/http/connection.ex index 76de3fcfe..ebacf7902 100644 --- a/lib/pleroma/http/connection.ex +++ b/lib/pleroma/http/connection.ex @@ -30,12 +30,12 @@ def options(%URI{} = uri, opts \\ []) do @defaults |> pool_timeout() |> Keyword.merge(opts) - |> adapter().options(uri) + |> adapter_helper().options(uri) end defp pool_timeout(opts) do {config_key, default} = - if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Gun do + if adapter() == Tesla.Adapter.Gun do {:pools, Config.get([:pools, :default, :timeout])} else {:hackney_pools, 10_000} @@ -47,10 +47,12 @@ defp pool_timeout(opts) do end @spec after_request(keyword()) :: :ok - def after_request(opts), do: adapter().after_request(opts) + def after_request(opts), do: adapter_helper().after_request(opts) - defp adapter do - case Application.get_env(:tesla, :adapter) do + defp adapter, do: Application.get_env(:tesla, :adapter) + + defp adapter_helper do + case adapter() do Tesla.Adapter.Gun -> AdapterHelper.Gun Tesla.Adapter.Hackney -> AdapterHelper.Hackney _ -> AdapterHelper diff --git a/lib/pleroma/pool/request.ex b/lib/pleroma/pool/request.ex index db7c10c01..3fb930db7 100644 --- a/lib/pleroma/pool/request.ex +++ b/lib/pleroma/pool/request.ex @@ -39,7 +39,6 @@ def handle_info({:gun_up, _conn, _protocol}, state) do @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 diff --git a/lib/pleroma/pool/supervisor.ex b/lib/pleroma/pool/supervisor.ex index 8dc5b64b7..faf646cb2 100644 --- a/lib/pleroma/pool/supervisor.ex +++ b/lib/pleroma/pool/supervisor.ex @@ -13,16 +13,13 @@ def start_link(args) do end def init(_) do - children = - [ - %{ - id: Pool.Connections, - start: - {Pool.Connections, :start_link, [{:gun_connections, Config.get([:connections_pool])}]} - } - ] ++ pools() + conns_child = %{ + id: Pool.Connections, + start: + {Pool.Connections, :start_link, [{:gun_connections, Config.get([:connections_pool])}]} + } - Supervisor.init(children, strategy: :one_for_one) + Supervisor.init([conns_child | pools()], strategy: :one_for_one) end defp pools do diff --git a/lib/pleroma/reverse_proxy/client/tesla.ex b/lib/pleroma/reverse_proxy/client/tesla.ex index dbc6b66a3..e81ea8bde 100644 --- a/lib/pleroma/reverse_proxy/client/tesla.ex +++ b/lib/pleroma/reverse_proxy/client/tesla.ex @@ -3,11 +3,11 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.ReverseProxy.Client.Tesla do + @behaviour Pleroma.ReverseProxy.Client + @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()} @@ -18,7 +18,7 @@ defmodule Pleroma.ReverseProxy.Client.Tesla do def request(method, url, headers, body, opts \\ []) do check_adapter() - opts = Keyword.merge(opts, body_as: :chunks) + opts = Keyword.put(opts, :body_as, :chunks) with {:ok, response} <- Pleroma.HTTP.request( @@ -39,7 +39,8 @@ def request(method, url, headers, body, opts \\ []) do end @impl true - @spec stream_body(map()) :: {:ok, binary(), map()} | {:error, atom() | String.t()} | :done + @spec stream_body(map()) :: + {:ok, binary(), map()} | {:error, atom() | String.t()} | :done | no_return() def stream_body(%{pid: pid, opts: opts, fin: true}) do # if connection was reused, but in tesla were redirects, # tesla returns new opened connection, which must be closed manually diff --git a/lib/pleroma/reverse_proxy/reverse_proxy.ex b/lib/pleroma/reverse_proxy/reverse_proxy.ex index 8f1aa3200..35b973b56 100644 --- a/lib/pleroma/reverse_proxy/reverse_proxy.ex +++ b/lib/pleroma/reverse_proxy/reverse_proxy.ex @@ -59,7 +59,7 @@ defmodule Pleroma.ReverseProxy do * `req_headers`, `resp_headers` additional headers. - * `http`: options for [gun](https://github.com/ninenines/gun). + * `http`: options for [hackney](https://github.com/benoitc/hackney) or [gun](https://github.com/ninenines/gun). """ @default_options [pool: :media] From 35471205f862fa069c6d87aefc1d827c9fab6e08 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Mon, 16 Mar 2020 15:47:25 +0300 Subject: [PATCH 69/88] temp fix for `:gun.info` MatchError --- lib/pleroma/pool/connections.ex | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/lib/pleroma/pool/connections.ex b/lib/pleroma/pool/connections.ex index 772833509..16aa80548 100644 --- a/lib/pleroma/pool/connections.ex +++ b/lib/pleroma/pool/connections.ex @@ -169,19 +169,26 @@ defp sort_conns({_, c1}, {_, c2}) do @impl true def handle_info({:gun_up, conn_pid, _protocol}, state) do - %{origin_host: host, origin_scheme: scheme, origin_port: port} = Gun.info(conn_pid) + # TODO: temp fix for gun MatchError https://github.com/ninenines/gun/issues/222 + # TODO: REMOVE LATER + {key, conn} = + try do + %{origin_host: host, origin_scheme: scheme, origin_port: port} = Gun.info(conn_pid) - host = - case :inet.ntoa(host) do - {:error, :einval} -> host - ip -> ip + host = + case :inet.ntoa(host) do + {:error, :einval} -> host + ip -> ip + end + + key = "#{scheme}:#{host}:#{port}" + find_conn(state.conns, conn_pid, key) + rescue + MatcheError -> find_conn(state.conns, conn_pid) end - key = "#{scheme}:#{host}:#{port}" - state = - with {_key, conn} <- find_conn(state.conns, conn_pid, key), - {true, key} <- {Process.alive?(conn_pid), key} do + with {true, key} <- {Process.alive?(conn_pid), key} do put_in(state.conns[key], %{ conn | gun_state: :up, From bf474ca3c154544b54720ea23c06191e68f32522 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Mon, 16 Mar 2020 16:23:49 +0300 Subject: [PATCH 70/88] fix --- lib/pleroma/pool/connections.ex | 34 +++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/lib/pleroma/pool/connections.ex b/lib/pleroma/pool/connections.ex index 16aa80548..91102faf7 100644 --- a/lib/pleroma/pool/connections.ex +++ b/lib/pleroma/pool/connections.ex @@ -167,28 +167,30 @@ defp sort_conns({_, c1}, {_, c2}) do c1.crf <= c2.crf and c1.last_reference <= c2.last_reference end - @impl true - def handle_info({:gun_up, conn_pid, _protocol}, state) do + defp find_conn_from_gun_info(conns, pid) do # TODO: temp fix for gun MatchError https://github.com/ninenines/gun/issues/222 # TODO: REMOVE LATER - {key, conn} = - try do - %{origin_host: host, origin_scheme: scheme, origin_port: port} = Gun.info(conn_pid) + try do + %{origin_host: host, origin_scheme: scheme, origin_port: port} = Gun.info(pid) - host = - case :inet.ntoa(host) do - {:error, :einval} -> host - ip -> ip - end + host = + case :inet.ntoa(host) do + {:error, :einval} -> host + ip -> ip + end - key = "#{scheme}:#{host}:#{port}" - find_conn(state.conns, conn_pid, key) - rescue - MatcheError -> find_conn(state.conns, conn_pid) - end + key = "#{scheme}:#{host}:#{port}" + find_conn(conns, pid, key) + rescue + MatcheError -> find_conn(conns, pid) + end + end + @impl true + def handle_info({:gun_up, conn_pid, _protocol}, state) do state = - with {true, key} <- {Process.alive?(conn_pid), key} do + with {key, conn} <- find_conn_from_gun_info(state.conns, conn_pid), + {true, key} <- {Process.alive?(conn_pid), key} do put_in(state.conns[key], %{ conn | gun_state: :up, From b17d8d305f5e9bf25644fd9b3457a965e3a5c001 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 16 Mar 2020 15:39:34 -0500 Subject: [PATCH 71/88] Enable Gun adapter by default We need devs to dogfood this before we merge it into the 2.1 release --- config/config.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.exs b/config/config.exs index 3ec1868b2..154eda48a 100644 --- a/config/config.exs +++ b/config/config.exs @@ -170,7 +170,7 @@ "application/ld+json" => ["activity+json"] } -config :tesla, adapter: Tesla.Adapter.Hackney +config :tesla, adapter: Tesla.Adapter.Gun # Configures http settings, upstream proxy etc. config :pleroma, :http, proxy_url: nil, From 7f9b5284fa7dd1d9100de730a6fe0c93739d1b30 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Fri, 20 Mar 2020 20:58:47 +0300 Subject: [PATCH 72/88] updating clear_config --- test/http/adapter_helper/gun_test.exs | 4 +--- test/http/adapter_helper/hackney_test.exs | 5 +---- test/http/connection_test.exs | 2 +- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/test/http/adapter_helper/gun_test.exs b/test/http/adapter_helper/gun_test.exs index 18025b986..2e961826e 100644 --- a/test/http/adapter_helper/gun_test.exs +++ b/test/http/adapter_helper/gun_test.exs @@ -28,9 +28,7 @@ defp gun_mock do end describe "options/1" do - clear_config([:http, :adapter]) do - Config.put([:http, :adapter], a: 1, b: 2) - end + setup do: clear_config([:http, :adapter], a: 1, b: 2) test "https url with default port" do uri = URI.parse("https://example.com") diff --git a/test/http/adapter_helper/hackney_test.exs b/test/http/adapter_helper/hackney_test.exs index 5fda075f6..3f7e708e0 100644 --- a/test/http/adapter_helper/hackney_test.exs +++ b/test/http/adapter_helper/hackney_test.exs @@ -6,7 +6,6 @@ defmodule Pleroma.HTTP.AdapterHelper.HackneyTest do use ExUnit.Case, async: true use Pleroma.Tests.Helpers - alias Pleroma.Config alias Pleroma.HTTP.AdapterHelper.Hackney setup_all do @@ -15,9 +14,7 @@ defmodule Pleroma.HTTP.AdapterHelper.HackneyTest do end describe "options/2" do - clear_config([:http, :adapter]) do - Config.put([:http, :adapter], a: 1, b: 2) - end + setup do: clear_config([:http, :adapter], a: 1, b: 2) test "add proxy and opts from config", %{uri: uri} do opts = Hackney.options([proxy: "localhost:8123"], uri) diff --git a/test/http/connection_test.exs b/test/http/connection_test.exs index 0f62eddd2..5cc78ad5b 100644 --- a/test/http/connection_test.exs +++ b/test/http/connection_test.exs @@ -82,7 +82,7 @@ test "with nil" do end describe "options/3" do - clear_config([:http, :proxy_url]) + setup do: clear_config([:http, :proxy_url]) test "without proxy_url in config" do Config.delete([:http, :proxy_url]) From eb9744cadea7191b088ddaadfbd5fa4d4fd45090 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 14 Jan 2020 14:42:30 +0300 Subject: [PATCH 73/88] activities generation tasks --- benchmarks/load_testing/activities.ex | 515 ++++++++++++++ benchmarks/load_testing/fetcher.ex | 709 ++++++++++++------- benchmarks/load_testing/generator.ex | 410 ----------- benchmarks/load_testing/helper.ex | 10 +- benchmarks/load_testing/users.ex | 161 +++++ benchmarks/mix/tasks/pleroma/load_testing.ex | 136 +--- config/benchmark.exs | 2 +- lib/pleroma/application.ex | 2 +- 8 files changed, 1184 insertions(+), 761 deletions(-) create mode 100644 benchmarks/load_testing/activities.ex delete mode 100644 benchmarks/load_testing/generator.ex create mode 100644 benchmarks/load_testing/users.ex diff --git a/benchmarks/load_testing/activities.ex b/benchmarks/load_testing/activities.ex new file mode 100644 index 000000000..db0e5a66f --- /dev/null +++ b/benchmarks/load_testing/activities.ex @@ -0,0 +1,515 @@ +defmodule Pleroma.LoadTesting.Activities do + @moduledoc """ + Module for generating different activities. + """ + import Ecto.Query + import Pleroma.LoadTesting.Helper, only: [to_sec: 1] + + alias Ecto.UUID + alias Pleroma.Constants + alias Pleroma.LoadTesting.Users + alias Pleroma.Repo + alias Pleroma.Web.CommonAPI + + require Constants + + @defaults [ + iterations: 170, + friends_used: 20, + non_friends_used: 20 + ] + + @max_concurrency 30 + + @visibility ~w(public private direct unlisted) + @types ~w(simple emoji mentions hell_thread attachment tag like reblog simple_thread remote) + @groups ~w(user friends non_friends) + + @spec generate(User.t(), keyword()) :: :ok + def generate(user, opts \\ []) do + {:ok, _} = + Agent.start_link(fn -> %{} end, + name: :benchmark_state + ) + + opts = Keyword.merge(@defaults, opts) + + friends = + user + |> Users.get_users(limit: opts[:friends_used], local: :local, friends?: true) + |> Enum.shuffle() + + non_friends = + user + |> Users.get_users(limit: opts[:non_friends_used], local: :local, friends?: false) + |> Enum.shuffle() + + task_data = + for visibility <- @visibility, + type <- @types, + group <- @groups, + do: {visibility, type, group} + + IO.puts("Starting generating #{opts[:iterations]} iterations of activities...") + + friends_thread = Enum.take(friends, 5) + non_friends_thread = Enum.take(friends, 5) + + public_long_thread = fn -> + generate_long_thread("public", user, friends_thread, non_friends_thread, opts) + end + + private_long_thread = fn -> + generate_long_thread("private", user, friends_thread, non_friends_thread, opts) + end + + iterations = opts[:iterations] + + {time, _} = + :timer.tc(fn -> + Enum.each( + 1..iterations, + fn + i when i == iterations - 2 -> + spawn(public_long_thread) + spawn(private_long_thread) + generate_activities(user, friends, non_friends, Enum.shuffle(task_data), opts) + + _ -> + generate_activities(user, friends, non_friends, Enum.shuffle(task_data), opts) + end + ) + end) + + IO.puts("Generating iterations activities take #{to_sec(time)} sec.\n") + :ok + end + + defp generate_long_thread(visibility, user, friends, non_friends, _opts) do + group = + if visibility == "public", + do: "friends", + else: "user" + + tasks = get_reply_tasks(visibility, group) |> Stream.cycle() |> Enum.take(50) + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "Start of #{visibility} long thread", + "visibility" => visibility + }) + + Agent.update(:benchmark_state, fn state -> + key = + if visibility == "public", + do: :public_thread, + else: :private_thread + + Map.put(state, key, activity) + end) + + acc = {activity.id, ["@" <> user.nickname, "reply to long thread"]} + insert_replies_for_long_thread(tasks, visibility, user, friends, non_friends, acc) + IO.puts("Generating #{visibility} long thread ended\n") + end + + defp insert_replies_for_long_thread(tasks, visibility, user, friends, non_friends, acc) do + Enum.reduce(tasks, acc, fn + "friend", {id, data} -> + friend = Enum.random(friends) + insert_reply(friend, List.delete(data, "@" <> friend.nickname), id, visibility) + + "non_friend", {id, data} -> + non_friend = Enum.random(non_friends) + insert_reply(non_friend, List.delete(data, "@" <> non_friend.nickname), id, visibility) + + "user", {id, data} -> + insert_reply(user, List.delete(data, "@" <> user.nickname), id, visibility) + end) + end + + defp generate_activities(user, friends, non_friends, task_data, opts) do + Task.async_stream( + task_data, + fn {visibility, type, group} -> + insert_activity(type, visibility, group, user, friends, non_friends, opts) + end, + max_concurrency: @max_concurrency, + timeout: 30_000 + ) + |> Stream.run() + end + + defp insert_activity("simple", visibility, group, user, friends, non_friends, _opts) do + {:ok, _activity} = + group + |> get_actor(user, friends, non_friends) + |> CommonAPI.post(%{"status" => "Simple status", "visibility" => visibility}) + end + + defp insert_activity("emoji", visibility, group, user, friends, non_friends, _opts) do + {:ok, _activity} = + group + |> get_actor(user, friends, non_friends) + |> CommonAPI.post(%{ + "status" => "Simple status with emoji :firefox:", + "visibility" => visibility + }) + end + + defp insert_activity("mentions", visibility, group, user, friends, non_friends, _opts) do + user_mentions = + get_random_mentions(friends, Enum.random(0..3)) ++ + get_random_mentions(non_friends, Enum.random(0..3)) + + user_mentions = + if Enum.random([true, false]), + do: ["@" <> user.nickname | user_mentions], + else: user_mentions + + {:ok, _activity} = + group + |> get_actor(user, friends, non_friends) + |> CommonAPI.post(%{ + "status" => Enum.join(user_mentions, ", ") <> " simple status with mentions", + "visibility" => visibility + }) + end + + defp insert_activity("hell_thread", visibility, group, user, friends, non_friends, _opts) do + mentions = + with {:ok, nil} <- Cachex.get(:user_cache, "hell_thread_mentions") do + cached = + ([user | Enum.take(friends, 10)] ++ Enum.take(non_friends, 10)) + |> Enum.map(&"@#{&1.nickname}") + |> Enum.join(", ") + + Cachex.put(:user_cache, "hell_thread_mentions", cached) + cached + else + {:ok, cached} -> cached + end + + {:ok, _activity} = + group + |> get_actor(user, friends, non_friends) + |> CommonAPI.post(%{ + "status" => mentions <> " hell thread status", + "visibility" => visibility + }) + end + + defp insert_activity("attachment", visibility, group, user, friends, non_friends, _opts) do + actor = get_actor(group, user, friends, non_friends) + + obj_data = %{ + "actor" => actor.ap_id, + "name" => "4467-11.jpg", + "type" => "Document", + "url" => [ + %{ + "href" => + "#{Pleroma.Web.base_url()}/media/b1b873552422a07bf53af01f3c231c841db4dfc42c35efde681abaf0f2a4eab7.jpg", + "mediaType" => "image/jpeg", + "type" => "Link" + } + ] + } + + object = Repo.insert!(%Pleroma.Object{data: obj_data}) + + {:ok, _activity} = + CommonAPI.post(actor, %{ + "status" => "Post with attachment", + "visibility" => visibility, + "media_ids" => [object.id] + }) + end + + defp insert_activity("tag", visibility, group, user, friends, non_friends, _opts) do + {:ok, _activity} = + group + |> get_actor(user, friends, non_friends) + |> CommonAPI.post(%{"status" => "Status with #tag", "visibility" => visibility}) + end + + defp insert_activity("like", visibility, group, user, friends, non_friends, opts) do + actor = get_actor(group, user, friends, non_friends) + + with activity_id when not is_nil(activity_id) <- get_random_create_activity_id(), + {:ok, _activity, _object} <- CommonAPI.favorite(activity_id, actor) do + :ok + else + {:error, _} -> + insert_activity("like", visibility, group, user, friends, non_friends, opts) + + nil -> + Process.sleep(15) + insert_activity("like", visibility, group, user, friends, non_friends, opts) + end + end + + defp insert_activity("reblog", visibility, group, user, friends, non_friends, opts) do + actor = get_actor(group, user, friends, non_friends) + + with activity_id when not is_nil(activity_id) <- get_random_create_activity_id(), + {:ok, _activity, _object} <- CommonAPI.repeat(activity_id, actor) do + :ok + else + {:error, _} -> + insert_activity("reblog", visibility, group, user, friends, non_friends, opts) + + nil -> + Process.sleep(15) + insert_activity("reblog", visibility, group, user, friends, non_friends, opts) + end + end + + defp insert_activity("simple_thread", visibility, group, user, friends, non_friends, _opts) + when visibility in ["public", "unlisted", "private"] do + actor = get_actor(group, user, friends, non_friends) + tasks = get_reply_tasks(visibility, group) + + {:ok, activity} = + CommonAPI.post(user, %{"status" => "Simple status", "visibility" => "unlisted"}) + + acc = {activity.id, ["@" <> actor.nickname, "reply to status"]} + insert_replies(tasks, visibility, user, friends, non_friends, acc) + end + + defp insert_activity("simple_thread", "direct", group, user, friends, non_friends, _opts) do + actor = get_actor(group, user, friends, non_friends) + tasks = get_reply_tasks("direct", group) + + list = + case group do + "non_friends" -> + Enum.take(non_friends, 3) + + _ -> + Enum.take(friends, 3) + end + + data = Enum.map(list, &("@" <> &1.nickname)) + + {:ok, activity} = + CommonAPI.post(actor, %{ + "status" => Enum.join(data, ", ") <> "simple status", + "visibility" => "direct" + }) + + acc = {activity.id, ["@" <> user.nickname | data] ++ ["reply to status"]} + insert_direct_replies(tasks, user, list, acc) + end + + defp insert_activity("remote", _, "user", _, _, _, _), do: :ok + + defp insert_activity("remote", visibility, group, user, _friends, _non_friends, opts) do + remote_friends = + Users.get_users(user, limit: opts[:friends_used], local: :external, friends?: true) + + remote_non_friends = + Users.get_users(user, limit: opts[:non_friends_used], local: :external, friends?: false) + + actor = get_actor(group, user, remote_friends, remote_non_friends) + + {act_data, obj_data} = prepare_activity_data(actor, visibility, user) + {activity_data, object_data} = other_data(actor) + + activity_data + |> Map.merge(act_data) + |> Map.put("object", Map.merge(object_data, obj_data)) + |> Pleroma.Web.ActivityPub.ActivityPub.insert(false) + end + + defp get_actor("user", user, _friends, _non_friends), do: user + defp get_actor("friends", _user, friends, _non_friends), do: Enum.random(friends) + defp get_actor("non_friends", _user, _friends, non_friends), do: Enum.random(non_friends) + + defp other_data(actor) do + %{host: host} = URI.parse(actor.ap_id) + datetime = DateTime.utc_now() + context_id = "http://#{host}:4000/contexts/#{UUID.generate()}" + activity_id = "http://#{host}:4000/activities/#{UUID.generate()}" + object_id = "http://#{host}:4000/objects/#{UUID.generate()}" + + activity_data = %{ + "actor" => actor.ap_id, + "context" => context_id, + "id" => activity_id, + "published" => datetime, + "type" => "Create", + "directMessage" => false + } + + object_data = %{ + "actor" => actor.ap_id, + "attachment" => [], + "attributedTo" => actor.ap_id, + "bcc" => [], + "bto" => [], + "content" => "Remote post", + "context" => context_id, + "conversation" => context_id, + "emoji" => %{}, + "id" => object_id, + "published" => datetime, + "sensitive" => false, + "summary" => "", + "tag" => [], + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "type" => "Note" + } + + {activity_data, object_data} + end + + defp prepare_activity_data(actor, "public", _mention) do + obj_data = %{ + "cc" => [actor.follower_address], + "to" => [Constants.as_public()] + } + + act_data = %{ + "cc" => [actor.follower_address], + "to" => [Constants.as_public()] + } + + {act_data, obj_data} + end + + defp prepare_activity_data(actor, "private", _mention) do + obj_data = %{ + "cc" => [], + "to" => [actor.follower_address] + } + + act_data = %{ + "cc" => [], + "to" => [actor.follower_address] + } + + {act_data, obj_data} + end + + defp prepare_activity_data(actor, "unlisted", _mention) do + obj_data = %{ + "cc" => [Constants.as_public()], + "to" => [actor.follower_address] + } + + act_data = %{ + "cc" => [Constants.as_public()], + "to" => [actor.follower_address] + } + + {act_data, obj_data} + end + + defp prepare_activity_data(_actor, "direct", mention) do + %{host: mentioned_host} = URI.parse(mention.ap_id) + + obj_data = %{ + "cc" => [], + "content" => + "@#{ + mention.nickname + } direct message", + "tag" => [ + %{ + "href" => mention.ap_id, + "name" => "@#{mention.nickname}@#{mentioned_host}", + "type" => "Mention" + } + ], + "to" => [mention.ap_id] + } + + act_data = %{ + "cc" => [], + "directMessage" => true, + "to" => [mention.ap_id] + } + + {act_data, obj_data} + end + + defp get_reply_tasks("public", "user"), do: ~w(friend non_friend user) + defp get_reply_tasks("public", "friends"), do: ~w(non_friend user friend) + defp get_reply_tasks("public", "non_friends"), do: ~w(user friend non_friend) + + defp get_reply_tasks(visibility, "user") when visibility in ["unlisted", "private"], + do: ~w(friend user friend) + + defp get_reply_tasks(visibility, "friends") when visibility in ["unlisted", "private"], + do: ~w(user friend user) + + defp get_reply_tasks(visibility, "non_friends") when visibility in ["unlisted", "private"], + do: [] + + defp get_reply_tasks("direct", "user"), do: ~w(friend user friend) + defp get_reply_tasks("direct", "friends"), do: ~w(user friend user) + defp get_reply_tasks("direct", "non_friends"), do: ~w(user non_friend user) + + defp insert_replies(tasks, visibility, user, friends, non_friends, acc) do + Enum.reduce(tasks, acc, fn + "friend", {id, data} -> + friend = Enum.random(friends) + insert_reply(friend, data, id, visibility) + + "non_friend", {id, data} -> + non_friend = Enum.random(non_friends) + insert_reply(non_friend, data, id, visibility) + + "user", {id, data} -> + insert_reply(user, data, id, visibility) + end) + end + + defp insert_direct_replies(tasks, user, list, acc) do + Enum.reduce(tasks, acc, fn + group, {id, data} when group in ["friend", "non_friend"] -> + actor = Enum.random(list) + + {reply_id, _} = + insert_reply(actor, List.delete(data, "@" <> actor.nickname), id, "direct") + + {reply_id, data} + + "user", {id, data} -> + {reply_id, _} = insert_reply(user, List.delete(data, "@" <> user.nickname), id, "direct") + {reply_id, data} + end) + end + + defp insert_reply(actor, data, activity_id, visibility) do + {:ok, reply} = + CommonAPI.post(actor, %{ + "status" => Enum.join(data, ", "), + "visibility" => visibility, + "in_reply_to_status_id" => activity_id + }) + + {reply.id, ["@" <> actor.nickname | data]} + end + + defp get_random_mentions(_users, count) when count == 0, do: [] + + defp get_random_mentions(users, count) do + users + |> Enum.shuffle() + |> Enum.take(count) + |> Enum.map(&"@#{&1.nickname}") + end + + defp get_random_create_activity_id do + Repo.one( + from(a in Pleroma.Activity, + where: fragment("(?)->>'type' = ?", a.data, ^"Create"), + order_by: fragment("RANDOM()"), + limit: 1, + select: a.id + ) + ) + end +end diff --git a/benchmarks/load_testing/fetcher.ex b/benchmarks/load_testing/fetcher.ex index a45a71d4a..bd65ac84f 100644 --- a/benchmarks/load_testing/fetcher.ex +++ b/benchmarks/load_testing/fetcher.ex @@ -1,260 +1,489 @@ defmodule Pleroma.LoadTesting.Fetcher do - use Pleroma.LoadTesting.Helper + alias Pleroma.Activity + alias Pleroma.Pagination + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.MastodonAPI.MastodonAPI + alias Pleroma.Web.MastodonAPI.StatusView - def fetch_user(user) do - Benchee.run(%{ - "By id" => fn -> Repo.get_by(User, id: user.id) end, - "By ap_id" => fn -> Repo.get_by(User, ap_id: user.ap_id) end, - "By email" => fn -> Repo.get_by(User, email: user.email) end, - "By nickname" => fn -> Repo.get_by(User, nickname: user.nickname) end - }) + @spec run_benchmarks(User.t()) :: any() + def run_benchmarks(user) do + fetch_user(user) + fetch_timelines(user) + render_views(user) end - def query_timelines(user) do - home_timeline_params = %{ - "count" => 20, - "with_muted" => true, - "type" => ["Create", "Announce"], + defp formatters do + [ + Benchee.Formatters.Console + ] + end + + defp fetch_user(user) do + Benchee.run( + %{ + "By id" => fn -> Repo.get_by(User, id: user.id) end, + "By ap_id" => fn -> Repo.get_by(User, ap_id: user.ap_id) end, + "By email" => fn -> Repo.get_by(User, email: user.email) end, + "By nickname" => fn -> Repo.get_by(User, nickname: user.nickname) end + }, + formatters: formatters() + ) + end + + defp fetch_timelines(user) do + fetch_home_timeline(user) + fetch_direct_timeline(user) + fetch_public_timeline(user) + fetch_public_timeline(user, :local) + fetch_public_timeline(user, :tag) + fetch_notifications(user) + fetch_favourites(user) + fetch_long_thread(user) + end + + defp render_views(user) do + render_timelines(user) + render_long_thread(user) + end + + defp opts_for_home_timeline(user) do + %{ "blocking_user" => user, + "count" => "20", "muting_user" => user, + "type" => ["Create", "Announce"], + "user" => user, + "with_muted" => "true" + } + end + + defp fetch_home_timeline(user) do + opts = opts_for_home_timeline(user) + + recipients = [user.ap_id | User.following(user)] + + first_page_last = + ActivityPub.fetch_activities(recipients, opts) |> Enum.reverse() |> List.last() + + second_page_last = + ActivityPub.fetch_activities(recipients, Map.put(opts, "max_id", first_page_last.id)) + |> Enum.reverse() + |> List.last() + + third_page_last = + ActivityPub.fetch_activities(recipients, Map.put(opts, "max_id", second_page_last.id)) + |> Enum.reverse() + |> List.last() + + forth_page_last = + ActivityPub.fetch_activities(recipients, Map.put(opts, "max_id", third_page_last.id)) + |> Enum.reverse() + |> List.last() + + Benchee.run( + %{ + "home timeline" => fn opts -> ActivityPub.fetch_activities(recipients, opts) end + }, + inputs: %{ + "1 page" => opts, + "2 page" => Map.put(opts, "max_id", first_page_last.id), + "3 page" => Map.put(opts, "max_id", second_page_last.id), + "4 page" => Map.put(opts, "max_id", third_page_last.id), + "5 page" => Map.put(opts, "max_id", forth_page_last.id), + "1 page only media" => Map.put(opts, "only_media", "true"), + "2 page only media" => + Map.put(opts, "max_id", first_page_last.id) |> Map.put("only_media", "true"), + "3 page only media" => + Map.put(opts, "max_id", second_page_last.id) |> Map.put("only_media", "true"), + "4 page only media" => + Map.put(opts, "max_id", third_page_last.id) |> Map.put("only_media", "true"), + "5 page only media" => + Map.put(opts, "max_id", forth_page_last.id) |> Map.put("only_media", "true") + }, + formatters: formatters() + ) + end + + defp opts_for_direct_timeline(user) do + %{ + :visibility => "direct", + "blocking_user" => user, + "count" => "20", + "type" => "Create", + "user" => user, + "with_muted" => "true" + } + end + + defp fetch_direct_timeline(user) do + recipients = [user.ap_id] + + opts = opts_for_direct_timeline(user) + + first_page_last = + recipients + |> ActivityPub.fetch_activities_query(opts) + |> Pagination.fetch_paginated(opts) + |> List.last() + + opts2 = Map.put(opts, "max_id", first_page_last.id) + + second_page_last = + recipients + |> ActivityPub.fetch_activities_query(opts2) + |> Pagination.fetch_paginated(opts2) + |> List.last() + + opts3 = Map.put(opts, "max_id", second_page_last.id) + + third_page_last = + recipients + |> ActivityPub.fetch_activities_query(opts3) + |> Pagination.fetch_paginated(opts3) + |> List.last() + + opts4 = Map.put(opts, "max_id", third_page_last.id) + + forth_page_last = + recipients + |> ActivityPub.fetch_activities_query(opts4) + |> Pagination.fetch_paginated(opts4) + |> List.last() + + Benchee.run( + %{ + "direct timeline" => fn opts -> + ActivityPub.fetch_activities_query(recipients, opts) |> Pagination.fetch_paginated(opts) + end + }, + inputs: %{ + "1 page" => opts, + "2 page" => opts2, + "3 page" => opts3, + "4 page" => opts4, + "5 page" => Map.put(opts4, "max_id", forth_page_last.id) + }, + formatters: formatters() + ) + end + + defp opts_for_public_timeline(user) do + %{ + "type" => ["Create", "Announce"], + "local_only" => false, + "blocking_user" => user, + "muting_user" => user + } + end + + defp opts_for_public_timeline(user, :local) do + %{ + "type" => ["Create", "Announce"], + "local_only" => true, + "blocking_user" => user, + "muting_user" => user + } + end + + defp opts_for_public_timeline(user, :tag) do + %{ + "blocking_user" => user, + "count" => "20", + "local_only" => nil, + "muting_user" => user, + "tag" => ["tag"], + "tag_all" => [], + "tag_reject" => [], + "type" => "Create", + "user" => user, + "with_muted" => "true" + } + end + + defp fetch_public_timeline(user) do + opts = opts_for_public_timeline(user) + + fetch_public_timeline(opts, "public timeline") + end + + defp fetch_public_timeline(user, :local) do + opts = opts_for_public_timeline(user, :local) + + fetch_public_timeline(opts, "public timeline only local") + end + + defp fetch_public_timeline(user, :tag) do + opts = opts_for_public_timeline(user, :tag) + + fetch_public_timeline(opts, "hashtag timeline") + end + + defp fetch_public_timeline(user, :only_media) do + opts = opts_for_public_timeline(user) |> Map.put("only_media", "true") + + fetch_public_timeline(opts, "public timeline only media") + end + + defp fetch_public_timeline(opts, title) when is_binary(title) do + first_page_last = ActivityPub.fetch_public_activities(opts) |> List.last() + + second_page_last = + ActivityPub.fetch_public_activities(Map.put(opts, "max_id", first_page_last.id)) + |> List.last() + + third_page_last = + ActivityPub.fetch_public_activities(Map.put(opts, "max_id", second_page_last.id)) + |> List.last() + + forth_page_last = + ActivityPub.fetch_public_activities(Map.put(opts, "max_id", third_page_last.id)) + |> List.last() + + Benchee.run( + %{ + title => fn opts -> + ActivityPub.fetch_public_activities(opts) + end + }, + inputs: %{ + "1 page" => opts, + "2 page" => Map.put(opts, "max_id", first_page_last.id), + "3 page" => Map.put(opts, "max_id", second_page_last.id), + "4 page" => Map.put(opts, "max_id", third_page_last.id), + "5 page" => Map.put(opts, "max_id", forth_page_last.id) + }, + formatters: formatters() + ) + end + + defp opts_for_notifications do + %{"count" => "20", "with_muted" => "true"} + end + + defp fetch_notifications(user) do + opts = opts_for_notifications() + + first_page_last = MastodonAPI.get_notifications(user, opts) |> List.last() + + second_page_last = + MastodonAPI.get_notifications(user, Map.put(opts, "max_id", first_page_last.id)) + |> List.last() + + third_page_last = + MastodonAPI.get_notifications(user, Map.put(opts, "max_id", second_page_last.id)) + |> List.last() + + forth_page_last = + MastodonAPI.get_notifications(user, Map.put(opts, "max_id", third_page_last.id)) + |> List.last() + + Benchee.run( + %{ + "Notifications" => fn opts -> + MastodonAPI.get_notifications(user, opts) + end + }, + inputs: %{ + "1 page" => opts, + "2 page" => Map.put(opts, "max_id", first_page_last.id), + "3 page" => Map.put(opts, "max_id", second_page_last.id), + "4 page" => Map.put(opts, "max_id", third_page_last.id), + "5 page" => Map.put(opts, "max_id", forth_page_last.id) + }, + formatters: formatters() + ) + end + + defp fetch_favourites(user) do + first_page_last = ActivityPub.fetch_favourites(user) |> List.last() + + second_page_last = + ActivityPub.fetch_favourites(user, %{"max_id" => first_page_last.id}) |> List.last() + + third_page_last = + ActivityPub.fetch_favourites(user, %{"max_id" => second_page_last.id}) |> List.last() + + forth_page_last = + ActivityPub.fetch_favourites(user, %{"max_id" => third_page_last.id}) |> List.last() + + Benchee.run( + %{ + "Favourites" => fn opts -> + ActivityPub.fetch_favourites(user, opts) + end + }, + inputs: %{ + "1 page" => %{}, + "2 page" => %{"max_id" => first_page_last.id}, + "3 page" => %{"max_id" => second_page_last.id}, + "4 page" => %{"max_id" => third_page_last.id}, + "5 page" => %{"max_id" => forth_page_last.id} + }, + formatters: formatters() + ) + end + + defp opts_for_long_thread(user) do + %{ + "blocking_user" => user, "user" => user } + end - mastodon_public_timeline_params = %{ - "count" => 20, - "local_only" => true, - "only_media" => "false", - "type" => ["Create", "Announce"], - "with_muted" => "true", - "blocking_user" => user, - "muting_user" => user - } + defp fetch_long_thread(user) do + %{public_thread: public, private_thread: private} = + Agent.get(:benchmark_state, fn state -> state end) - mastodon_federated_timeline_params = %{ - "count" => 20, - "only_media" => "false", - "type" => ["Create", "Announce"], - "with_muted" => "true", - "blocking_user" => user, - "muting_user" => user - } + opts = opts_for_long_thread(user) - following = User.following(user) + private_input = {private.data["context"], Map.put(opts, "exclude_id", private.id)} - Benchee.run(%{ - "User home timeline" => fn -> - Pleroma.Web.ActivityPub.ActivityPub.fetch_activities( - following, - home_timeline_params - ) - end, - "User mastodon public timeline" => fn -> - Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities( - mastodon_public_timeline_params - ) - end, - "User mastodon federated public timeline" => fn -> - Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities( - mastodon_federated_timeline_params - ) - end - }) + public_input = {public.data["context"], Map.put(opts, "exclude_id", public.id)} - home_activities = - Pleroma.Web.ActivityPub.ActivityPub.fetch_activities( - following, - home_timeline_params + Benchee.run( + %{ + "fetch context" => fn {context, opts} -> + ActivityPub.fetch_activities_for_context(context, opts) + end + }, + inputs: %{ + "Private long thread" => private_input, + "Public long thread" => public_input + }, + formatters: formatters() + ) + end + + defp render_timelines(user) do + opts = opts_for_home_timeline(user) + + recipients = [user.ap_id | User.following(user)] + + home_activities = ActivityPub.fetch_activities(recipients, opts) |> Enum.reverse() + + recipients = [user.ap_id] + + opts = opts_for_direct_timeline(user) + + direct_activities = + recipients + |> ActivityPub.fetch_activities_query(opts) + |> Pagination.fetch_paginated(opts) + + opts = opts_for_public_timeline(user) + + public_activities = ActivityPub.fetch_public_activities(opts) + + opts = opts_for_public_timeline(user, :tag) + + tag_activities = ActivityPub.fetch_public_activities(opts) + + opts = opts_for_notifications() + + notifications = MastodonAPI.get_notifications(user, opts) + + favourites = ActivityPub.fetch_favourites(user) + + Benchee.run( + %{ + "Rendering home timeline" => fn -> + StatusView.render("index.json", %{ + activities: home_activities, + for: user, + as: :activity + }) + end, + "Rendering direct timeline" => fn -> + StatusView.render("index.json", %{ + activities: direct_activities, + for: user, + as: :activity + }) + end, + "Rendering public timeline" => fn -> + StatusView.render("index.json", %{ + activities: public_activities, + for: user, + as: :activity + }) + end, + "Rendering tag timeline" => fn -> + StatusView.render("index.json", %{ + activities: tag_activities, + for: user, + as: :activity + }) + end, + "Rendering notifications" => fn -> + Pleroma.Web.MastodonAPI.NotificationView.render("index.json", %{ + notifications: notifications, + for: user + }) + end, + "Rendering favourites timeline" => fn -> + StatusView.render("index.json", %{ + activities: favourites, + for: user, + as: :activity + }) + end + }, + formatters: formatters() + ) + end + + defp render_long_thread(user) do + %{public_thread: public, private_thread: private} = + Agent.get(:benchmark_state, fn state -> state end) + + opts = %{for: user} + public_activity = Activity.get_by_id_with_object(public.id) + private_activity = Activity.get_by_id_with_object(private.id) + + Benchee.run( + %{ + "render" => fn opts -> + StatusView.render("show.json", opts) + end + }, + inputs: %{ + "Public root" => Map.put(opts, :activity, public_activity), + "Private root" => Map.put(opts, :activity, private_activity) + }, + formatters: formatters() + ) + + fetch_opts = opts_for_long_thread(user) + + public_context = + ActivityPub.fetch_activities_for_context( + public.data["context"], + Map.put(fetch_opts, "exclude_id", public.id) ) - public_activities = - Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities(mastodon_public_timeline_params) - - public_federated_activities = - Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities( - mastodon_federated_timeline_params + private_context = + ActivityPub.fetch_activities_for_context( + private.data["context"], + Map.put(fetch_opts, "exclude_id", private.id) ) - Benchee.run(%{ - "Rendering home timeline" => fn -> - Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{ - activities: home_activities, - for: user, - as: :activity - }) - end, - "Rendering public timeline" => fn -> - Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{ - activities: public_activities, - for: user, - as: :activity - }) - end, - "Rendering public federated timeline" => fn -> - Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{ - activities: public_federated_activities, - for: user, - as: :activity - }) - end, - "Rendering favorites timeline" => fn -> - conn = Phoenix.ConnTest.build_conn(:get, "http://localhost:4001/api/v1/favourites", nil) - Pleroma.Web.MastodonAPI.StatusController.favourites( - %Plug.Conn{conn | - assigns: %{user: user}, - query_params: %{"limit" => "0"}, - body_params: %{}, - cookies: %{}, - params: %{}, - path_params: %{}, - private: %{ - Pleroma.Web.Router => {[], %{}}, - phoenix_router: Pleroma.Web.Router, - phoenix_action: :favourites, - phoenix_controller: Pleroma.Web.MastodonAPI.StatusController, - phoenix_endpoint: Pleroma.Web.Endpoint, - phoenix_format: "json", - phoenix_layout: {Pleroma.Web.LayoutView, "app.html"}, - phoenix_recycled: true, - - phoenix_view: Pleroma.Web.MastodonAPI.StatusView, - plug_session: %{"user_id" => user.id}, - plug_session_fetch: :done, - plug_session_info: :write, - plug_skip_csrf_protection: true - } - }, - %{}) - end, - }) - end - - def query_notifications(user) do - without_muted_params = %{"count" => "20", "with_muted" => "false"} - with_muted_params = %{"count" => "20", "with_muted" => "true"} - - Benchee.run(%{ - "Notifications without muted" => fn -> - Pleroma.Web.MastodonAPI.MastodonAPI.get_notifications(user, without_muted_params) - end, - "Notifications with muted" => fn -> - Pleroma.Web.MastodonAPI.MastodonAPI.get_notifications(user, with_muted_params) - end - }) - - without_muted_notifications = - Pleroma.Web.MastodonAPI.MastodonAPI.get_notifications(user, without_muted_params) - - with_muted_notifications = - Pleroma.Web.MastodonAPI.MastodonAPI.get_notifications(user, with_muted_params) - - Benchee.run(%{ - "Render notifications without muted" => fn -> - Pleroma.Web.MastodonAPI.NotificationView.render("index.json", %{ - notifications: without_muted_notifications, - for: user - }) - end, - "Render notifications with muted" => fn -> - Pleroma.Web.MastodonAPI.NotificationView.render("index.json", %{ - notifications: with_muted_notifications, - for: user - }) - end - }) - end - - def query_dms(user) do - params = %{ - "count" => "20", - "with_muted" => "true", - "type" => "Create", - "blocking_user" => user, - "user" => user, - visibility: "direct" - } - - Benchee.run(%{ - "Direct messages with muted" => fn -> - Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_query([user.ap_id], params) - |> Pleroma.Pagination.fetch_paginated(params) - end, - "Direct messages without muted" => fn -> - Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_query([user.ap_id], params) - |> Pleroma.Pagination.fetch_paginated(Map.put(params, "with_muted", false)) - end - }) - - dms_with_muted = - Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_query([user.ap_id], params) - |> Pleroma.Pagination.fetch_paginated(params) - - dms_without_muted = - Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_query([user.ap_id], params) - |> Pleroma.Pagination.fetch_paginated(Map.put(params, "with_muted", false)) - - Benchee.run(%{ - "Rendering dms with muted" => fn -> - Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{ - activities: dms_with_muted, - for: user, - as: :activity - }) - end, - "Rendering dms without muted" => fn -> - Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{ - activities: dms_without_muted, - for: user, - as: :activity - }) - end - }) - end - - def query_long_thread(user, activity) do - Benchee.run(%{ - "Fetch main post" => fn -> - Pleroma.Activity.get_by_id_with_object(activity.id) - end, - "Fetch context of main post" => fn -> - Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_for_context( - activity.data["context"], - %{ - "blocking_user" => user, - "user" => user, - "exclude_id" => activity.id - } - ) - end - }) - - activity = Pleroma.Activity.get_by_id_with_object(activity.id) - - context = - Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_for_context( - activity.data["context"], - %{ - "blocking_user" => user, - "user" => user, - "exclude_id" => activity.id + Benchee.run( + %{ + "render" => fn opts -> + StatusView.render("context.json", opts) + end + }, + inputs: %{ + "Public context" => %{user: user, activity: public_activity, activities: public_context}, + "Private context" => %{ + user: user, + activity: private_activity, + activities: private_context } - ) - - Benchee.run(%{ - "Render status" => fn -> - Pleroma.Web.MastodonAPI.StatusView.render("show.json", %{ - activity: activity, - for: user - }) - end, - "Render context" => fn -> - Pleroma.Web.MastodonAPI.StatusView.render( - "index.json", - for: user, - activities: context, - as: :activity - ) - |> Enum.reverse() - end - }) + }, + formatters: formatters() + ) end end diff --git a/benchmarks/load_testing/generator.ex b/benchmarks/load_testing/generator.ex deleted file mode 100644 index e4673757c..000000000 --- a/benchmarks/load_testing/generator.ex +++ /dev/null @@ -1,410 +0,0 @@ -defmodule Pleroma.LoadTesting.Generator do - use Pleroma.LoadTesting.Helper - alias Pleroma.Web.CommonAPI - - def generate_like_activities(user, posts) do - count_likes = Kernel.trunc(length(posts) / 4) - IO.puts("Starting generating #{count_likes} like activities...") - - {time, _} = - :timer.tc(fn -> - Task.async_stream( - Enum.take_random(posts, count_likes), - fn post -> {:ok, _, _} = CommonAPI.favorite(post.id, user) end, - max_concurrency: 10, - timeout: 30_000 - ) - |> Stream.run() - end) - - IO.puts("Inserting like activities take #{to_sec(time)} sec.\n") - end - - def generate_users(opts) do - IO.puts("Starting generating #{opts[:users_max]} users...") - {time, users} = :timer.tc(fn -> do_generate_users(opts) end) - - IO.puts("Inserting users took #{to_sec(time)} sec.\n") - users - end - - defp do_generate_users(opts) do - max = Keyword.get(opts, :users_max) - - Task.async_stream( - 1..max, - &generate_user_data(&1), - max_concurrency: 10, - timeout: 30_000 - ) - |> Enum.to_list() - end - - defp generate_user_data(i) do - remote = Enum.random([true, false]) - - user = %User{ - name: "Test テスト User #{i}", - email: "user#{i}@example.com", - nickname: "nick#{i}", - password_hash: - "$pbkdf2-sha512$160000$bU.OSFI7H/yqWb5DPEqyjw$uKp/2rmXw12QqnRRTqTtuk2DTwZfF8VR4MYW2xMeIlqPR/UX1nT1CEKVUx2CowFMZ5JON8aDvURrZpJjSgqXrg", - bio: "Tester Number #{i}", - local: remote - } - - user_urls = - if remote do - base_url = - Enum.random(["https://domain1.com", "https://domain2.com", "https://domain3.com"]) - - ap_id = "#{base_url}/users/#{user.nickname}" - - %{ - ap_id: ap_id, - follower_address: ap_id <> "/followers", - following_address: ap_id <> "/following" - } - else - %{ - ap_id: User.ap_id(user), - follower_address: User.ap_followers(user), - following_address: User.ap_following(user) - } - end - - user = Map.merge(user, user_urls) - - Repo.insert!(user) - end - - def generate_activities(user, users) do - do_generate_activities(user, users) - end - - defp do_generate_activities(user, users) do - IO.puts("Starting generating 20000 common activities...") - - {time, _} = - :timer.tc(fn -> - Task.async_stream( - 1..20_000, - fn _ -> - do_generate_activity([user | users]) - end, - max_concurrency: 10, - timeout: 30_000 - ) - |> Stream.run() - end) - - IO.puts("Inserting common activities take #{to_sec(time)} sec.\n") - - IO.puts("Starting generating 20000 activities with mentions...") - - {time, _} = - :timer.tc(fn -> - Task.async_stream( - 1..20_000, - fn _ -> - do_generate_activity_with_mention(user, users) - end, - max_concurrency: 10, - timeout: 30_000 - ) - |> Stream.run() - end) - - IO.puts("Inserting activities with menthions take #{to_sec(time)} sec.\n") - - IO.puts("Starting generating 10000 activities with threads...") - - {time, _} = - :timer.tc(fn -> - Task.async_stream( - 1..10_000, - fn _ -> - do_generate_threads([user | users]) - end, - max_concurrency: 10, - timeout: 30_000 - ) - |> Stream.run() - end) - - IO.puts("Inserting activities with threads take #{to_sec(time)} sec.\n") - end - - defp do_generate_activity(users) do - post = %{ - "status" => "Some status without mention with random user" - } - - CommonAPI.post(Enum.random(users), post) - end - - def generate_power_intervals(opts \\ []) do - count = Keyword.get(opts, :count, 20) - power = Keyword.get(opts, :power, 2) - IO.puts("Generating #{count} intervals for a power #{power} series...") - counts = Enum.map(1..count, fn n -> :math.pow(n, power) end) - sum = Enum.sum(counts) - - densities = - Enum.map(counts, fn c -> - c / sum - end) - - densities - |> Enum.reduce(0, fn density, acc -> - if acc == 0 do - [{0, density}] - else - [{_, lower} | _] = acc - [{lower, lower + density} | acc] - end - end) - |> Enum.reverse() - end - - def generate_tagged_activities(opts \\ []) do - tag_count = Keyword.get(opts, :tag_count, 20) - users = Keyword.get(opts, :users, Repo.all(User)) - activity_count = Keyword.get(opts, :count, 200_000) - - intervals = generate_power_intervals(count: tag_count) - - IO.puts( - "Generating #{activity_count} activities using #{tag_count} different tags of format `tag_n`, starting at tag_0" - ) - - Enum.each(1..activity_count, fn _ -> - random = :rand.uniform() - i = Enum.find_index(intervals, fn {lower, upper} -> lower <= random && upper > random end) - CommonAPI.post(Enum.random(users), %{"status" => "a post with the tag #tag_#{i}"}) - end) - end - - defp do_generate_activity_with_mention(user, users) do - mentions_cnt = Enum.random([2, 3, 4, 5]) - with_user = Enum.random([true, false]) - users = Enum.shuffle(users) - mentions_users = Enum.take(users, mentions_cnt) - mentions_users = if with_user, do: [user | mentions_users], else: mentions_users - - mentions_str = - Enum.map(mentions_users, fn user -> "@" <> user.nickname end) |> Enum.join(", ") - - post = %{ - "status" => mentions_str <> "some status with mentions random users" - } - - CommonAPI.post(Enum.random(users), post) - end - - defp do_generate_threads(users) do - thread_length = Enum.random([2, 3, 4, 5]) - actor = Enum.random(users) - - post = %{ - "status" => "Start of the thread" - } - - {:ok, activity} = CommonAPI.post(actor, post) - - Enum.each(1..thread_length, fn _ -> - user = Enum.random(users) - - post = %{ - "status" => "@#{actor.nickname} reply to thread", - "in_reply_to_status_id" => activity.id - } - - CommonAPI.post(user, post) - end) - end - - def generate_remote_activities(user, users) do - do_generate_remote_activities(user, users) - end - - defp do_generate_remote_activities(user, users) do - IO.puts("Starting generating 10000 remote activities...") - - {time, _} = - :timer.tc(fn -> - Task.async_stream( - 1..10_000, - fn i -> - do_generate_remote_activity(i, user, users) - end, - max_concurrency: 10, - timeout: 30_000 - ) - |> Stream.run() - end) - - IO.puts("Inserting remote activities take #{to_sec(time)} sec.\n") - end - - defp do_generate_remote_activity(i, user, users) do - actor = Enum.random(users) - %{host: host} = URI.parse(actor.ap_id) - date = Date.utc_today() - datetime = DateTime.utc_now() - - map = %{ - "actor" => actor.ap_id, - "cc" => [actor.follower_address, user.ap_id], - "context" => "tag:mastodon.example.org,#{date}:objectId=#{i}:objectType=Conversation", - "id" => actor.ap_id <> "/statuses/#{i}/activity", - "object" => %{ - "actor" => actor.ap_id, - "atomUri" => actor.ap_id <> "/statuses/#{i}", - "attachment" => [], - "attributedTo" => actor.ap_id, - "bcc" => [], - "bto" => [], - "cc" => [actor.follower_address, user.ap_id], - "content" => - "

- user.ap_id <> - "\" class=\"u-url mention\">@" <> user.nickname <> "

", - "context" => "tag:mastodon.example.org,#{date}:objectId=#{i}:objectType=Conversation", - "conversation" => - "tag:mastodon.example.org,#{date}:objectId=#{i}:objectType=Conversation", - "emoji" => %{}, - "id" => actor.ap_id <> "/statuses/#{i}", - "inReplyTo" => nil, - "inReplyToAtomUri" => nil, - "published" => datetime, - "sensitive" => true, - "summary" => "cw", - "tag" => [ - %{ - "href" => user.ap_id, - "name" => "@#{user.nickname}@#{host}", - "type" => "Mention" - } - ], - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "type" => "Note", - "url" => "http://#{host}/@#{actor.nickname}/#{i}" - }, - "published" => datetime, - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "type" => "Create" - } - - Pleroma.Web.ActivityPub.ActivityPub.insert(map, false) - end - - def generate_dms(user, users, opts) do - IO.puts("Starting generating #{opts[:dms_max]} DMs") - {time, _} = :timer.tc(fn -> do_generate_dms(user, users, opts) end) - IO.puts("Inserting dms take #{to_sec(time)} sec.\n") - end - - defp do_generate_dms(user, users, opts) do - Task.async_stream( - 1..opts[:dms_max], - fn _ -> - do_generate_dm(user, users) - end, - max_concurrency: 10, - timeout: 30_000 - ) - |> Stream.run() - end - - defp do_generate_dm(user, users) do - post = %{ - "status" => "@#{user.nickname} some direct message", - "visibility" => "direct" - } - - CommonAPI.post(Enum.random(users), post) - end - - def generate_long_thread(user, users, opts) do - IO.puts("Starting generating long thread with #{opts[:thread_length]} replies") - {time, activity} = :timer.tc(fn -> do_generate_long_thread(user, users, opts) end) - IO.puts("Inserting long thread replies take #{to_sec(time)} sec.\n") - {:ok, activity} - end - - defp do_generate_long_thread(user, users, opts) do - {:ok, %{id: id} = activity} = CommonAPI.post(user, %{"status" => "Start of long thread"}) - - Task.async_stream( - 1..opts[:thread_length], - fn _ -> do_generate_thread(users, id) end, - max_concurrency: 10, - timeout: 30_000 - ) - |> Stream.run() - - activity - end - - defp do_generate_thread(users, activity_id) do - CommonAPI.post(Enum.random(users), %{ - "status" => "reply to main post", - "in_reply_to_status_id" => activity_id - }) - end - - def generate_non_visible_message(user, users) do - IO.puts("Starting generating 1000 non visible posts") - - {time, _} = - :timer.tc(fn -> - do_generate_non_visible_posts(user, users) - end) - - IO.puts("Inserting non visible posts take #{to_sec(time)} sec.\n") - end - - defp do_generate_non_visible_posts(user, users) do - [not_friend | users] = users - - make_friends(user, users) - - Task.async_stream(1..1000, fn _ -> do_generate_non_visible_post(not_friend, users) end, - max_concurrency: 10, - timeout: 30_000 - ) - |> Stream.run() - end - - defp make_friends(_user, []), do: nil - - defp make_friends(user, [friend | users]) do - {:ok, _} = User.follow(user, friend) - {:ok, _} = User.follow(friend, user) - make_friends(user, users) - end - - defp do_generate_non_visible_post(not_friend, users) do - post = %{ - "status" => "some non visible post", - "visibility" => "private" - } - - {:ok, activity} = CommonAPI.post(not_friend, post) - - thread_length = Enum.random([2, 3, 4, 5]) - - Enum.each(1..thread_length, fn _ -> - user = Enum.random(users) - - post = %{ - "status" => "@#{not_friend.nickname} reply to non visible post", - "in_reply_to_status_id" => activity.id, - "visibility" => "private" - } - - CommonAPI.post(user, post) - end) - end -end diff --git a/benchmarks/load_testing/helper.ex b/benchmarks/load_testing/helper.ex index 47b25c65f..23bbb1cec 100644 --- a/benchmarks/load_testing/helper.ex +++ b/benchmarks/load_testing/helper.ex @@ -1,11 +1,3 @@ defmodule Pleroma.LoadTesting.Helper do - defmacro __using__(_) do - quote do - import Ecto.Query - alias Pleroma.Repo - alias Pleroma.User - - defp to_sec(microseconds), do: microseconds / 1_000_000 - end - end + def to_sec(microseconds), do: microseconds / 1_000_000 end diff --git a/benchmarks/load_testing/users.ex b/benchmarks/load_testing/users.ex new file mode 100644 index 000000000..951b30d91 --- /dev/null +++ b/benchmarks/load_testing/users.ex @@ -0,0 +1,161 @@ +defmodule Pleroma.LoadTesting.Users do + @moduledoc """ + Module for generating users with friends. + """ + import Ecto.Query + import Pleroma.LoadTesting.Helper, only: [to_sec: 1] + + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.User.Query + + @defaults [ + users: 20_000, + friends: 100 + ] + + @max_concurrency 30 + + @spec generate(keyword()) :: User.t() + def generate(opts \\ []) do + opts = Keyword.merge(@defaults, opts) + + IO.puts("Starting generating #{opts[:users]} users...") + + {time, _} = :timer.tc(fn -> generate_users(opts[:users]) end) + + IO.puts("Generating users take #{to_sec(time)} sec.\n") + + main_user = + Repo.one(from(u in User, where: u.local == true, order_by: fragment("RANDOM()"), limit: 1)) + + IO.puts("Starting making friends for #{opts[:friends]} users...") + {time, _} = :timer.tc(fn -> make_friends(main_user, opts[:friends]) end) + + IO.puts("Making friends take #{to_sec(time)} sec.\n") + + Repo.get(User, main_user.id) + end + + defp generate_users(max) do + Task.async_stream( + 1..max, + &generate_user(&1), + max_concurrency: @max_concurrency, + timeout: 30_000 + ) + |> Stream.run() + end + + defp generate_user(i) do + remote = Enum.random([true, false]) + + %User{ + name: "Test テスト User #{i}", + email: "user#{i}@example.com", + nickname: "nick#{i}", + password_hash: Comeonin.Pbkdf2.hashpwsalt("test"), + bio: "Tester Number #{i}", + local: !remote + } + |> user_urls() + |> Repo.insert!() + end + + defp user_urls(%{local: true} = user) do + urls = %{ + ap_id: User.ap_id(user), + follower_address: User.ap_followers(user), + following_address: User.ap_following(user) + } + + Map.merge(user, urls) + end + + defp user_urls(%{local: false} = user) do + base_domain = Enum.random(["domain1.com", "domain2.com", "domain3.com"]) + + ap_id = "https://#{base_domain}/users/#{user.nickname}" + + urls = %{ + ap_id: ap_id, + follower_address: ap_id <> "/followers", + following_address: ap_id <> "/following" + } + + Map.merge(user, urls) + end + + defp make_friends(main_user, max) when is_integer(max) do + number_of_users = + (max / 2) + |> Kernel.trunc() + + main_user + |> get_users(%{limit: number_of_users, local: :local}) + |> run_stream(main_user) + + main_user + |> get_users(%{limit: number_of_users, local: :external}) + |> run_stream(main_user) + end + + defp make_friends(%User{} = main_user, %User{} = user) do + {:ok, _} = User.follow(main_user, user) + {:ok, _} = User.follow(user, main_user) + end + + @spec get_users(User.t(), keyword()) :: [User.t()] + def get_users(user, opts) do + criteria = %{limit: opts[:limit]} + + criteria = + if opts[:local] do + Map.put(criteria, opts[:local], true) + else + criteria + end + + criteria = + if opts[:friends?] do + Map.put(criteria, :friends, user) + else + criteria + end + + query = + criteria + |> Query.build() + |> random_without_user(user) + + query = + if opts[:friends?] == false do + friends_ids = + %{friends: user} + |> Query.build() + |> Repo.all() + |> Enum.map(& &1.id) + + from(u in query, where: u.id not in ^friends_ids) + else + query + end + + Repo.all(query) + end + + defp random_without_user(query, user) do + from(u in query, + where: u.id != ^user.id, + order_by: fragment("RANDOM()") + ) + end + + defp run_stream(users, main_user) do + Task.async_stream(users, &make_friends(main_user, &1), + max_concurrency: @max_concurrency, + timeout: 30_000 + ) + |> Stream.run() + end +end diff --git a/benchmarks/mix/tasks/pleroma/load_testing.ex b/benchmarks/mix/tasks/pleroma/load_testing.ex index 0a751adac..262300990 100644 --- a/benchmarks/mix/tasks/pleroma/load_testing.ex +++ b/benchmarks/mix/tasks/pleroma/load_testing.ex @@ -1,114 +1,55 @@ defmodule Mix.Tasks.Pleroma.LoadTesting do use Mix.Task - use Pleroma.LoadTesting.Helper - import Mix.Pleroma - import Pleroma.LoadTesting.Generator - import Pleroma.LoadTesting.Fetcher + import Ecto.Query + + alias Ecto.Adapters.SQL + alias Pleroma.Repo + alias Pleroma.User @shortdoc "Factory for generation data" @moduledoc """ Generates data like: - local/remote users - - local/remote activities with notifications - - direct messages - - long thread - - non visible posts + - local/remote activities with differrent visibility: + - simple activiities + - with emoji + - with mentions + - hellthreads + - with attachments + - with tags + - likes + - reblogs + - simple threads + - long threads ## Generate data - MIX_ENV=benchmark mix pleroma.load_testing --users 20000 --dms 20000 --thread_length 2000 - MIX_ENV=benchmark mix pleroma.load_testing -u 20000 -d 20000 -t 2000 + MIX_ENV=benchmark mix pleroma.load_testing --users 20000 --friends 1000 --iterations 170 --friends_used 20 --non_friends_used 20 + MIX_ENV=benchmark mix pleroma.load_testing -u 20000 -f 1000 -i 170 -fu 20 -nfu 20 Options: - `--users NUMBER` - number of users to generate. Defaults to: 20000. Alias: `-u` - - `--dms NUMBER` - number of direct messages to generate. Defaults to: 20000. Alias `-d` - - `--thread_length` - number of messages in thread. Defaults to: 2000. ALias `-t` + - `--friends NUMBER` - number of friends for main user. Defaults to: 1000. Alias: `-f` + - `--iterations NUMBER` - number of iterations to generate activities. For each iteration in database is inserted about 120+ activities with different visibility, actors and types.Defaults to: 170. Alias: `-i` + - `--friends_used NUMBER` - number of main user friends used in activity generation. Defaults to: 20. Alias: `-fu` + - `--non_friends_used NUMBER` - number of non friends used in activity generation. Defaults to: 20. Alias: `-nfu` """ - @aliases [u: :users, d: :dms, t: :thread_length] + @aliases [u: :users, f: :friends, i: :iterations, fu: :friends_used, nfu: :non_friends_used] @switches [ users: :integer, - dms: :integer, - thread_length: :integer + friends: :integer, + iterations: :integer, + friends_used: :integer, + non_friends_used: :integer ] - @users_default 20_000 - @dms_default 1_000 - @thread_length_default 2_000 def run(args) do - start_pleroma() - Pleroma.Config.put([:instance, :skip_thread_containment], true) + Mix.Pleroma.start_pleroma() + clean_tables() {opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases) - users_max = Keyword.get(opts, :users, @users_default) - dms_max = Keyword.get(opts, :dms, @dms_default) - thread_length = Keyword.get(opts, :thread_length, @thread_length_default) - - clean_tables() - - opts = - Keyword.put(opts, :users_max, users_max) - |> Keyword.put(:dms_max, dms_max) - |> Keyword.put(:thread_length, thread_length) - - generate_users(opts) - - # main user for queries - IO.puts("Fetching local main user...") - - {time, user} = - :timer.tc(fn -> - Repo.one( - from(u in User, where: u.local == true, order_by: fragment("RANDOM()"), limit: 1) - ) - end) - - IO.puts("Fetching main user take #{to_sec(time)} sec.\n") - - IO.puts("Fetching local users...") - - {time, users} = - :timer.tc(fn -> - Repo.all( - from(u in User, - where: u.id != ^user.id, - where: u.local == true, - order_by: fragment("RANDOM()"), - limit: 10 - ) - ) - end) - - IO.puts("Fetching local users take #{to_sec(time)} sec.\n") - - IO.puts("Fetching remote users...") - - {time, remote_users} = - :timer.tc(fn -> - Repo.all( - from(u in User, - where: u.id != ^user.id, - where: u.local == false, - order_by: fragment("RANDOM()"), - limit: 10 - ) - ) - end) - - IO.puts("Fetching remote users take #{to_sec(time)} sec.\n") - - generate_activities(user, users) - - generate_remote_activities(user, remote_users) - - generate_like_activities( - user, Pleroma.Repo.all(Pleroma.Activity.Queries.by_type("Create")) - ) - - generate_dms(user, users, opts) - - {:ok, activity} = generate_long_thread(user, users, opts) - - generate_non_visible_message(user, users) + user = Pleroma.LoadTesting.Users.generate(opts) + Pleroma.LoadTesting.Activities.generate(user, opts) IO.puts("Users in DB: #{Repo.aggregate(from(u in User), :count, :id)}") @@ -120,19 +61,14 @@ def run(args) do "Notifications in DB: #{Repo.aggregate(from(n in Pleroma.Notification), :count, :id)}" ) - fetch_user(user) - query_timelines(user) - query_notifications(user) - query_dms(user) - query_long_thread(user, activity) - Pleroma.Config.put([:instance, :skip_thread_containment], false) - query_timelines(user) + Pleroma.LoadTesting.Fetcher.run_benchmarks(user) end defp clean_tables do IO.puts("Deleting old data...\n") - Ecto.Adapters.SQL.query!(Repo, "TRUNCATE users CASCADE;") - Ecto.Adapters.SQL.query!(Repo, "TRUNCATE activities CASCADE;") - Ecto.Adapters.SQL.query!(Repo, "TRUNCATE objects CASCADE;") + SQL.query!(Repo, "TRUNCATE users CASCADE;") + SQL.query!(Repo, "TRUNCATE activities CASCADE;") + SQL.query!(Repo, "TRUNCATE objects CASCADE;") + SQL.query!(Repo, "TRUNCATE oban_jobs CASCADE;") end end diff --git a/config/benchmark.exs b/config/benchmark.exs index ff59395cf..e867253eb 100644 --- a/config/benchmark.exs +++ b/config/benchmark.exs @@ -39,7 +39,7 @@ adapter: Ecto.Adapters.Postgres, username: "postgres", password: "postgres", - database: "pleroma_test", + database: "pleroma_benchmark", hostname: System.get_env("DB_HOST") || "localhost", pool_size: 10 diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 33f1705df..51850abb5 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -157,7 +157,7 @@ defp build_cachex(type, opts), defp chat_enabled?, do: Pleroma.Config.get([:chat, :enabled]) - defp streamer_child(:test), do: [] + defp streamer_child(env) when env in [:test, :benchmark], do: [] defp streamer_child(_) do [Pleroma.Web.Streamer.supervisor()] From 1f29ecdcd7ecdc4ad8d6bc8fc4c34efbc9b7fe1d Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 18 Feb 2020 12:19:10 +0300 Subject: [PATCH 74/88] sync with develop --- benchmarks/load_testing/activities.ex | 42 +++++++++++++ benchmarks/load_testing/helper.ex | 11 ++++ benchmarks/load_testing/users.ex | 61 +++++++++++-------- .../mix/tasks/pleroma/benchmarks/tags.ex | 24 +++----- benchmarks/mix/tasks/pleroma/load_testing.ex | 10 +-- lib/mix/pleroma.ex | 1 + 6 files changed, 99 insertions(+), 50 deletions(-) diff --git a/benchmarks/load_testing/activities.ex b/benchmarks/load_testing/activities.ex index db0e5a66f..121d5c500 100644 --- a/benchmarks/load_testing/activities.ex +++ b/benchmarks/load_testing/activities.ex @@ -85,6 +85,48 @@ def generate(user, opts \\ []) do :ok end + def generate_power_intervals(opts \\ []) do + count = Keyword.get(opts, :count, 20) + power = Keyword.get(opts, :power, 2) + IO.puts("Generating #{count} intervals for a power #{power} series...") + counts = Enum.map(1..count, fn n -> :math.pow(n, power) end) + sum = Enum.sum(counts) + + densities = + Enum.map(counts, fn c -> + c / sum + end) + + densities + |> Enum.reduce(0, fn density, acc -> + if acc == 0 do + [{0, density}] + else + [{_, lower} | _] = acc + [{lower, lower + density} | acc] + end + end) + |> Enum.reverse() + end + + def generate_tagged_activities(opts \\ []) do + tag_count = Keyword.get(opts, :tag_count, 20) + users = Keyword.get(opts, :users, Repo.all(Pleroma.User)) + activity_count = Keyword.get(opts, :count, 200_000) + + intervals = generate_power_intervals(count: tag_count) + + IO.puts( + "Generating #{activity_count} activities using #{tag_count} different tags of format `tag_n`, starting at tag_0" + ) + + Enum.each(1..activity_count, fn _ -> + random = :rand.uniform() + i = Enum.find_index(intervals, fn {lower, upper} -> lower <= random && upper > random end) + CommonAPI.post(Enum.random(users), %{"status" => "a post with the tag #tag_#{i}"}) + end) + end + defp generate_long_thread(visibility, user, friends, non_friends, _opts) do group = if visibility == "public", diff --git a/benchmarks/load_testing/helper.ex b/benchmarks/load_testing/helper.ex index 23bbb1cec..cab60acb4 100644 --- a/benchmarks/load_testing/helper.ex +++ b/benchmarks/load_testing/helper.ex @@ -1,3 +1,14 @@ defmodule Pleroma.LoadTesting.Helper do + alias Ecto.Adapters.SQL + alias Pleroma.Repo + def to_sec(microseconds), do: microseconds / 1_000_000 + + def clean_tables do + IO.puts("Deleting old data...\n") + SQL.query!(Repo, "TRUNCATE users CASCADE;") + SQL.query!(Repo, "TRUNCATE activities CASCADE;") + SQL.query!(Repo, "TRUNCATE objects CASCADE;") + SQL.query!(Repo, "TRUNCATE oban_jobs CASCADE;") + end end diff --git a/benchmarks/load_testing/users.ex b/benchmarks/load_testing/users.ex index 951b30d91..bc31dc08b 100644 --- a/benchmarks/load_testing/users.ex +++ b/benchmarks/load_testing/users.ex @@ -20,31 +20,31 @@ defmodule Pleroma.LoadTesting.Users do def generate(opts \\ []) do opts = Keyword.merge(@defaults, opts) - IO.puts("Starting generating #{opts[:users]} users...") - - {time, _} = :timer.tc(fn -> generate_users(opts[:users]) end) - - IO.puts("Generating users take #{to_sec(time)} sec.\n") + generate_users(opts[:users]) main_user = Repo.one(from(u in User, where: u.local == true, order_by: fragment("RANDOM()"), limit: 1)) - IO.puts("Starting making friends for #{opts[:friends]} users...") - {time, _} = :timer.tc(fn -> make_friends(main_user, opts[:friends]) end) - - IO.puts("Making friends take #{to_sec(time)} sec.\n") + make_friends(main_user, opts[:friends]) Repo.get(User, main_user.id) end - defp generate_users(max) do - Task.async_stream( - 1..max, - &generate_user(&1), - max_concurrency: @max_concurrency, - timeout: 30_000 - ) - |> Stream.run() + def generate_users(max) do + IO.puts("Starting generating #{opts[:users]} users...") + + {time, _} = + :timer.tc(fn -> + Task.async_stream( + 1..max, + &generate_user(&1), + max_concurrency: @max_concurrency, + timeout: 30_000 + ) + |> Stream.run() + end) + + IO.puts("Generating users take #{to_sec(time)} sec.\n") end defp generate_user(i) do @@ -86,18 +86,25 @@ defp user_urls(%{local: false} = user) do Map.merge(user, urls) end - defp make_friends(main_user, max) when is_integer(max) do - number_of_users = - (max / 2) - |> Kernel.trunc() + def make_friends(main_user, max) when is_integer(max) do + IO.puts("Starting making friends for #{opts[:friends]} users...") - main_user - |> get_users(%{limit: number_of_users, local: :local}) - |> run_stream(main_user) + {time, _} = + :timer.tc(fn -> + number_of_users = + (max / 2) + |> Kernel.trunc() - main_user - |> get_users(%{limit: number_of_users, local: :external}) - |> run_stream(main_user) + main_user + |> get_users(%{limit: number_of_users, local: :local}) + |> run_stream(main_user) + + main_user + |> get_users(%{limit: number_of_users, local: :external}) + |> run_stream(main_user) + end) + + IO.puts("Making friends take #{to_sec(time)} sec.\n") end defp make_friends(%User{} = main_user, %User{} = user) do diff --git a/benchmarks/mix/tasks/pleroma/benchmarks/tags.ex b/benchmarks/mix/tasks/pleroma/benchmarks/tags.ex index fd1506907..657403202 100644 --- a/benchmarks/mix/tasks/pleroma/benchmarks/tags.ex +++ b/benchmarks/mix/tasks/pleroma/benchmarks/tags.ex @@ -1,9 +1,12 @@ defmodule Mix.Tasks.Pleroma.Benchmarks.Tags do use Mix.Task - alias Pleroma.Repo - alias Pleroma.LoadTesting.Generator + + import Pleroma.LoadTesting.Helper, only: [clean_tables: 0] import Ecto.Query + alias Pleroma.Repo + alias Pleroma.Web.MastodonAPI.TimelineController + def run(_args) do Mix.Pleroma.start_pleroma() activities_count = Repo.aggregate(from(a in Pleroma.Activity), :count, :id) @@ -11,8 +14,8 @@ def run(_args) do if activities_count == 0 do IO.puts("Did not find any activities, cleaning and generating") clean_tables() - Generator.generate_users(users_max: 10) - Generator.generate_tagged_activities() + Pleroma.LoadTesting.Users.generate_users(10) + Pleroma.LoadTesting.Activities.generate_tagged_activities() else IO.puts("Found #{activities_count} activities, won't generate new ones") end @@ -34,7 +37,7 @@ def run(_args) do Benchee.run( %{ "Hashtag fetching, any" => fn tags -> - Pleroma.Web.MastodonAPI.TimelineController.hashtag_fetching( + TimelineController.hashtag_fetching( %{ "any" => tags }, @@ -44,7 +47,7 @@ def run(_args) do end, # Will always return zero results because no overlapping hashtags are generated. "Hashtag fetching, all" => fn tags -> - Pleroma.Web.MastodonAPI.TimelineController.hashtag_fetching( + TimelineController.hashtag_fetching( %{ "all" => tags }, @@ -64,7 +67,7 @@ def run(_args) do Benchee.run( %{ "Hashtag fetching" => fn tag -> - Pleroma.Web.MastodonAPI.TimelineController.hashtag_fetching( + TimelineController.hashtag_fetching( %{ "tag" => tag }, @@ -77,11 +80,4 @@ def run(_args) do time: 5 ) end - - defp clean_tables do - IO.puts("Deleting old data...\n") - Ecto.Adapters.SQL.query!(Repo, "TRUNCATE users CASCADE;") - Ecto.Adapters.SQL.query!(Repo, "TRUNCATE activities CASCADE;") - Ecto.Adapters.SQL.query!(Repo, "TRUNCATE objects CASCADE;") - end end diff --git a/benchmarks/mix/tasks/pleroma/load_testing.ex b/benchmarks/mix/tasks/pleroma/load_testing.ex index 262300990..72b225f09 100644 --- a/benchmarks/mix/tasks/pleroma/load_testing.ex +++ b/benchmarks/mix/tasks/pleroma/load_testing.ex @@ -1,8 +1,8 @@ defmodule Mix.Tasks.Pleroma.LoadTesting do use Mix.Task import Ecto.Query + import Pleroma.LoadTesting.Helper, only: [clean_tables: 0] - alias Ecto.Adapters.SQL alias Pleroma.Repo alias Pleroma.User @@ -63,12 +63,4 @@ def run(args) do Pleroma.LoadTesting.Fetcher.run_benchmarks(user) end - - defp clean_tables do - IO.puts("Deleting old data...\n") - SQL.query!(Repo, "TRUNCATE users CASCADE;") - SQL.query!(Repo, "TRUNCATE activities CASCADE;") - SQL.query!(Repo, "TRUNCATE objects CASCADE;") - SQL.query!(Repo, "TRUNCATE oban_jobs CASCADE;") - end end diff --git a/lib/mix/pleroma.ex b/lib/mix/pleroma.ex index 3ad6edbfb..4dfcc32e7 100644 --- a/lib/mix/pleroma.ex +++ b/lib/mix/pleroma.ex @@ -5,6 +5,7 @@ defmodule Mix.Pleroma do @doc "Common functions to be reused in mix tasks" def start_pleroma do + Mix.Task.run("app.start") Application.put_env(:phoenix, :serve_endpoints, false, persistent: true) if Pleroma.Config.get(:env) != :test do From 56503c385e8412a1189748bcf3fdfd4090be9f56 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 17 Mar 2020 13:47:13 +0300 Subject: [PATCH 75/88] fix --- benchmarks/load_testing/activities.ex | 4 ++-- benchmarks/load_testing/users.ex | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/benchmarks/load_testing/activities.ex b/benchmarks/load_testing/activities.ex index 121d5c500..24c6b5531 100644 --- a/benchmarks/load_testing/activities.ex +++ b/benchmarks/load_testing/activities.ex @@ -19,7 +19,7 @@ defmodule Pleroma.LoadTesting.Activities do non_friends_used: 20 ] - @max_concurrency 30 + @max_concurrency 10 @visibility ~w(public private direct unlisted) @types ~w(simple emoji mentions hell_thread attachment tag like reblog simple_thread remote) @@ -81,7 +81,7 @@ def generate(user, opts \\ []) do ) end) - IO.puts("Generating iterations activities take #{to_sec(time)} sec.\n") + IO.puts("Generating iterations of activities take #{to_sec(time)} sec.\n") :ok end diff --git a/benchmarks/load_testing/users.ex b/benchmarks/load_testing/users.ex index bc31dc08b..b73ac8651 100644 --- a/benchmarks/load_testing/users.ex +++ b/benchmarks/load_testing/users.ex @@ -14,7 +14,7 @@ defmodule Pleroma.LoadTesting.Users do friends: 100 ] - @max_concurrency 30 + @max_concurrency 10 @spec generate(keyword()) :: User.t() def generate(opts \\ []) do @@ -31,7 +31,7 @@ def generate(opts \\ []) do end def generate_users(max) do - IO.puts("Starting generating #{opts[:users]} users...") + IO.puts("Starting generating #{max} users...") {time, _} = :timer.tc(fn -> @@ -87,7 +87,7 @@ defp user_urls(%{local: false} = user) do end def make_friends(main_user, max) when is_integer(max) do - IO.puts("Starting making friends for #{opts[:friends]} users...") + IO.puts("Starting making friends for #{max} users...") {time, _} = :timer.tc(fn -> @@ -107,7 +107,7 @@ def make_friends(main_user, max) when is_integer(max) do IO.puts("Making friends take #{to_sec(time)} sec.\n") end - defp make_friends(%User{} = main_user, %User{} = user) do + def make_friends(%User{} = main_user, %User{} = user) do {:ok, _} = User.follow(main_user, user) {:ok, _} = User.follow(user, main_user) end From 96e279655763fedcb701e59c500023a70568c4c6 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Mon, 30 Mar 2020 11:59:14 +0300 Subject: [PATCH 76/88] use in timelines benchmark new user generator --- benchmarks/load_testing/activities.ex | 2 +- benchmarks/load_testing/users.ex | 9 ++++---- .../mix/tasks/pleroma/benchmarks/timelines.ex | 22 +++++++------------ 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/benchmarks/load_testing/activities.ex b/benchmarks/load_testing/activities.ex index 24c6b5531..23ee2b987 100644 --- a/benchmarks/load_testing/activities.ex +++ b/benchmarks/load_testing/activities.ex @@ -81,7 +81,7 @@ def generate(user, opts \\ []) do ) end) - IO.puts("Generating iterations of activities take #{to_sec(time)} sec.\n") + IO.puts("Generating iterations of activities took #{to_sec(time)} sec.\n") :ok end diff --git a/benchmarks/load_testing/users.ex b/benchmarks/load_testing/users.ex index b73ac8651..1a8c6e22f 100644 --- a/benchmarks/load_testing/users.ex +++ b/benchmarks/load_testing/users.ex @@ -33,7 +33,7 @@ def generate(opts \\ []) do def generate_users(max) do IO.puts("Starting generating #{max} users...") - {time, _} = + {time, users} = :timer.tc(fn -> Task.async_stream( 1..max, @@ -41,10 +41,11 @@ def generate_users(max) do max_concurrency: @max_concurrency, timeout: 30_000 ) - |> Stream.run() + |> Enum.to_list() end) - IO.puts("Generating users take #{to_sec(time)} sec.\n") + IO.puts("Generating users took #{to_sec(time)} sec.\n") + users end defp generate_user(i) do @@ -104,7 +105,7 @@ def make_friends(main_user, max) when is_integer(max) do |> run_stream(main_user) end) - IO.puts("Making friends take #{to_sec(time)} sec.\n") + IO.puts("Making friends took #{to_sec(time)} sec.\n") end def make_friends(%User{} = main_user, %User{} = user) do diff --git a/benchmarks/mix/tasks/pleroma/benchmarks/timelines.ex b/benchmarks/mix/tasks/pleroma/benchmarks/timelines.ex index dc6f3d3fc..9b7ac6111 100644 --- a/benchmarks/mix/tasks/pleroma/benchmarks/timelines.ex +++ b/benchmarks/mix/tasks/pleroma/benchmarks/timelines.ex @@ -1,9 +1,10 @@ defmodule Mix.Tasks.Pleroma.Benchmarks.Timelines do use Mix.Task - alias Pleroma.Repo - alias Pleroma.LoadTesting.Generator + + import Pleroma.LoadTesting.Helper, only: [clean_tables: 0] alias Pleroma.Web.CommonAPI + alias Plug.Conn def run(_args) do Mix.Pleroma.start_pleroma() @@ -11,7 +12,7 @@ def run(_args) do # Cleaning tables clean_tables() - [{:ok, user} | users] = Generator.generate_users(users_max: 1000) + [{:ok, user} | users] = Pleroma.LoadTesting.Users.generate_users(1000) # Let the user make 100 posts @@ -38,8 +39,8 @@ def run(_args) do "user timeline, no followers" => fn reading_user -> conn = Phoenix.ConnTest.build_conn() - |> Plug.Conn.assign(:user, reading_user) - |> Plug.Conn.assign(:skip_link_headers, true) + |> Conn.assign(:user, reading_user) + |> Conn.assign(:skip_link_headers, true) Pleroma.Web.MastodonAPI.AccountController.statuses(conn, %{"id" => user.id}) end @@ -56,8 +57,8 @@ def run(_args) do "user timeline, all following" => fn reading_user -> conn = Phoenix.ConnTest.build_conn() - |> Plug.Conn.assign(:user, reading_user) - |> Plug.Conn.assign(:skip_link_headers, true) + |> Conn.assign(:user, reading_user) + |> Conn.assign(:skip_link_headers, true) Pleroma.Web.MastodonAPI.AccountController.statuses(conn, %{"id" => user.id}) end @@ -66,11 +67,4 @@ def run(_args) do time: 60 ) end - - defp clean_tables do - IO.puts("Deleting old data...\n") - Ecto.Adapters.SQL.query!(Repo, "TRUNCATE users CASCADE;") - Ecto.Adapters.SQL.query!(Repo, "TRUNCATE activities CASCADE;") - Ecto.Adapters.SQL.query!(Repo, "TRUNCATE objects CASCADE;") - end end From 2afc7a9112fc11bc51abc2b65aea03d6d5045695 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Mon, 30 Mar 2020 12:16:45 +0300 Subject: [PATCH 77/88] changelog fix --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f393ea8eb..52e6c33f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - NodeInfo: `pleroma:api/v1/notifications:include_types_filter` to the `features` list. - Configuration: `:restrict_unauthenticated` setting, restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses. +- New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires minimum OTP version of 22.2 otherwise Pleroma won’t start. For hackney OTP update is not required.
API Changes - Mastodon API: Support for `include_types` in `/api/v1/notifications`. @@ -97,7 +98,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - User settings: Add _This account is a_ option. - A new users admin digest email - OAuth: admin scopes support (relevant setting: `[:auth, :enforce_oauth_admin_scope_usage]`). -- New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires minimum OTP version of 22.2 otherwise Pleroma won’t start. For hackney OTP update is not required. - Add an option `authorized_fetch_mode` to require HTTP signatures for AP fetches. - ActivityPub: support for `replies` collection (output for outgoing federation & fetching on incoming federation). - Mix task to refresh counter cache (`mix pleroma.refresh_counter_cache`) From 1fcdcb12a717fa3dbd54a5c3778bd216df6449ad Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Mon, 30 Mar 2020 12:47:12 +0300 Subject: [PATCH 78/88] updating gun with bug fix https://github.com/ninenines/gun/issues/222 --- lib/pleroma/pool/connections.ex | 31 +++++++++++-------------------- mix.exs | 2 +- mix.lock | 2 +- 3 files changed, 13 insertions(+), 22 deletions(-) diff --git a/lib/pleroma/pool/connections.ex b/lib/pleroma/pool/connections.ex index 91102faf7..4d4ba913c 100644 --- a/lib/pleroma/pool/connections.ex +++ b/lib/pleroma/pool/connections.ex @@ -167,29 +167,20 @@ defp sort_conns({_, c1}, {_, c2}) do c1.crf <= c2.crf and c1.last_reference <= c2.last_reference end - defp find_conn_from_gun_info(conns, pid) do - # TODO: temp fix for gun MatchError https://github.com/ninenines/gun/issues/222 - # TODO: REMOVE LATER - try do - %{origin_host: host, origin_scheme: scheme, origin_port: port} = Gun.info(pid) - - host = - case :inet.ntoa(host) do - {:error, :einval} -> host - ip -> ip - end - - key = "#{scheme}:#{host}:#{port}" - find_conn(conns, pid, key) - rescue - MatcheError -> find_conn(conns, pid) - end - end - @impl true def handle_info({:gun_up, conn_pid, _protocol}, state) do + %{origin_host: host, origin_scheme: scheme, origin_port: port} = Gun.info(conn_pid) + + host = + case :inet.ntoa(host) do + {:error, :einval} -> host + ip -> ip + end + + key = "#{scheme}:#{host}:#{port}" + state = - with {key, conn} <- find_conn_from_gun_info(state.conns, conn_pid), + with {key, conn} <- find_conn(state.conns, conn_pid, key), {true, key} <- {Process.alive?(conn_pid), key} do put_in(state.conns[key], %{ conn diff --git a/mix.exs b/mix.exs index 77d043d37..87c025d89 100644 --- a/mix.exs +++ b/mix.exs @@ -127,7 +127,7 @@ defp deps do {:castore, "~> 0.1"}, {:cowlib, "~> 2.8", override: true}, {:gun, - github: "ninenines/gun", ref: "bd6425ab87428cf4c95f4d23e0a48fd065fbd714", override: true}, + github: "ninenines/gun", ref: "e1a69b36b180a574c0ac314ced9613fdd52312cc", override: true}, {:jason, "~> 1.0"}, {:mogrify, "~> 0.6.1"}, {:ex_aws, "~> 2.1"}, diff --git a/mix.lock b/mix.lock index b791dccc4..6cca578d6 100644 --- a/mix.lock +++ b/mix.lock @@ -47,7 +47,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"]}, + "gun": {:git, "https://github.com/ninenines/gun.git", "e1a69b36b180a574c0ac314ced9613fdd52312cc", [ref: "e1a69b36b180a574c0ac314ced9613fdd52312cc"]}, "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"}, From b607ae1a1c0ef6557094ec0fb10ba2d19d621f7f Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Mon, 30 Mar 2020 13:50:00 +0300 Subject: [PATCH 79/88] removing grouped reports admin api endpoint --- lib/pleroma/web/activity_pub/utils.ex | 96 --------- .../web/admin_api/admin_api_controller.ex | 8 - .../web/admin_api/views/report_view.ex | 28 +-- lib/pleroma/web/router.ex | 1 - .../admin_api/admin_api_controller_test.exs | 203 ------------------ 5 files changed, 1 insertion(+), 335 deletions(-) diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index c65bbed67..2d685ecc0 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -795,102 +795,6 @@ def get_reports(params, page, page_size) do ActivityPub.fetch_activities([], params, :offset) end - def parse_report_group(activity) do - reports = get_reports_by_status_id(activity["id"]) - max_date = Enum.max_by(reports, &NaiveDateTime.from_iso8601!(&1.data["published"])) - actors = Enum.map(reports, & &1.user_actor) - [%{data: %{"object" => [account_id | _]}} | _] = reports - - account = - AccountView.render("show.json", %{ - user: User.get_by_ap_id(account_id) - }) - - status = get_status_data(activity) - - %{ - date: max_date.data["published"], - account: account, - status: status, - actors: Enum.uniq(actors), - reports: reports - } - end - - defp get_status_data(status) do - case status["deleted"] do - true -> - %{ - "id" => status["id"], - "deleted" => true - } - - _ -> - Activity.get_by_ap_id(status["id"]) - end - end - - def get_reports_by_status_id(ap_id) do - from(a in Activity, - where: fragment("(?)->>'type' = 'Flag'", a.data), - where: fragment("(?)->'object' @> ?", a.data, ^[%{id: ap_id}]), - or_where: fragment("(?)->'object' @> ?", a.data, ^[ap_id]) - ) - |> Activity.with_preloaded_user_actor() - |> Repo.all() - end - - @spec get_reports_grouped_by_status([String.t()]) :: %{ - required(:groups) => [ - %{ - required(:date) => String.t(), - required(:account) => %{}, - required(:status) => %{}, - required(:actors) => [%User{}], - required(:reports) => [%Activity{}] - } - ] - } - def get_reports_grouped_by_status(activity_ids) do - parsed_groups = - activity_ids - |> Enum.map(fn id -> - id - |> build_flag_object() - |> parse_report_group() - end) - - %{ - groups: parsed_groups - } - end - - @spec get_reported_activities() :: [ - %{ - required(:activity) => String.t(), - required(:date) => String.t() - } - ] - def get_reported_activities do - reported_activities_query = - from(a in Activity, - where: fragment("(?)->>'type' = 'Flag'", a.data), - select: %{ - activity: fragment("jsonb_array_elements((? #- '{object,0}')->'object')", a.data) - }, - group_by: fragment("activity") - ) - - from(a in subquery(reported_activities_query), - distinct: true, - select: %{ - id: fragment("COALESCE(?->>'id'::text, ? #>> '{}')", a.activity, a.activity) - } - ) - |> Repo.all() - |> Enum.map(& &1.id) - end - def update_report_state(%Activity{} = activity, state) when state in @strip_status_report_states do {:ok, stripped_activity} = strip_report_status_data(activity) diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 0368df1e9..ca5439920 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -715,14 +715,6 @@ def list_reports(conn, params) do |> render("index.json", %{reports: reports}) end - def list_grouped_reports(conn, _params) do - statuses = Utils.get_reported_activities() - - conn - |> put_view(ReportView) - |> render("index_grouped.json", Utils.get_reports_grouped_by_status(statuses)) - end - def report_show(conn, %{"id" => id}) do with %Activity{} = report <- Activity.get_by_id(id) do conn diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex index fc8733ce8..ca0bcebc7 100644 --- a/lib/pleroma/web/admin_api/views/report_view.ex +++ b/lib/pleroma/web/admin_api/views/report_view.ex @@ -4,7 +4,7 @@ defmodule Pleroma.Web.AdminAPI.ReportView do use Pleroma.Web, :view - alias Pleroma.Activity + alias Pleroma.HTML alias Pleroma.User alias Pleroma.Web.AdminAPI.Report @@ -44,32 +44,6 @@ def render("show.json", %{report: report, user: user, account: account, statuses } end - def render("index_grouped.json", %{groups: groups}) do - reports = - Enum.map(groups, fn group -> - status = - case group.status do - %Activity{} = activity -> StatusView.render("show.json", %{activity: activity}) - _ -> group.status - end - - %{ - date: group[:date], - account: group[:account], - status: Map.put_new(status, "deleted", false), - actors: Enum.map(group[:actors], &merge_account_views/1), - reports: - group[:reports] - |> Enum.map(&Report.extract_report_info(&1)) - |> Enum.map(&render(__MODULE__, "show.json", &1)) - } - end) - - %{ - reports: reports - } - end - def render("index_notes.json", %{notes: notes}) when is_list(notes) do Enum.map(notes, &render(__MODULE__, "show_note.json", &1)) end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index a22f744c1..5a0902739 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -186,7 +186,6 @@ defmodule Pleroma.Web.Router do patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email) get("/reports", AdminAPIController, :list_reports) - get("/grouped_reports", AdminAPIController, :list_grouped_reports) get("/reports/:id", AdminAPIController, :report_show) patch("/reports", AdminAPIController, :reports_update) post("/reports/:id/notes", AdminAPIController, :report_notes_create) diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index c9e228cc8..ea0c92502 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -21,7 +21,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do alias Pleroma.UserInviteToken alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.CommonAPI - alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MediaProxy setup_all do @@ -1586,208 +1585,6 @@ test "returns 403 when requested by anonymous" do end end - describe "GET /api/pleroma/admin/grouped_reports" do - setup do - [reporter, target_user] = insert_pair(:user) - - date1 = (DateTime.to_unix(DateTime.utc_now()) + 1000) |> DateTime.from_unix!() - date2 = (DateTime.to_unix(DateTime.utc_now()) + 2000) |> DateTime.from_unix!() - date3 = (DateTime.to_unix(DateTime.utc_now()) + 3000) |> DateTime.from_unix!() - - first_status = - insert(:note_activity, user: target_user, data_attrs: %{"published" => date1}) - - second_status = - insert(:note_activity, user: target_user, data_attrs: %{"published" => date2}) - - third_status = - insert(:note_activity, user: target_user, data_attrs: %{"published" => date3}) - - {:ok, first_report} = - CommonAPI.report(reporter, %{ - "account_id" => target_user.id, - "status_ids" => [first_status.id, second_status.id, third_status.id] - }) - - {:ok, second_report} = - CommonAPI.report(reporter, %{ - "account_id" => target_user.id, - "status_ids" => [first_status.id, second_status.id] - }) - - {:ok, third_report} = - CommonAPI.report(reporter, %{ - "account_id" => target_user.id, - "status_ids" => [first_status.id] - }) - - %{ - first_status: Activity.get_by_ap_id_with_object(first_status.data["id"]), - second_status: Activity.get_by_ap_id_with_object(second_status.data["id"]), - third_status: Activity.get_by_ap_id_with_object(third_status.data["id"]), - first_report: first_report, - first_status_reports: [first_report, second_report, third_report], - second_status_reports: [first_report, second_report], - third_status_reports: [first_report], - target_user: target_user, - reporter: reporter - } - end - - test "returns reports grouped by status", %{ - conn: conn, - first_status: first_status, - second_status: second_status, - third_status: third_status, - first_status_reports: first_status_reports, - second_status_reports: second_status_reports, - third_status_reports: third_status_reports, - target_user: target_user, - reporter: reporter - } do - response = - conn - |> get("/api/pleroma/admin/grouped_reports") - |> json_response(:ok) - - assert length(response["reports"]) == 3 - - first_group = Enum.find(response["reports"], &(&1["status"]["id"] == first_status.id)) - - second_group = Enum.find(response["reports"], &(&1["status"]["id"] == second_status.id)) - - third_group = Enum.find(response["reports"], &(&1["status"]["id"] == third_status.id)) - - assert length(first_group["reports"]) == 3 - assert length(second_group["reports"]) == 2 - assert length(third_group["reports"]) == 1 - - assert first_group["date"] == - Enum.max_by(first_status_reports, fn act -> - NaiveDateTime.from_iso8601!(act.data["published"]) - end).data["published"] - - assert first_group["status"] == - Map.put( - stringify_keys(StatusView.render("show.json", %{activity: first_status})), - "deleted", - false - ) - - assert(first_group["account"]["id"] == target_user.id) - - assert length(first_group["actors"]) == 1 - assert hd(first_group["actors"])["id"] == reporter.id - - assert Enum.map(first_group["reports"], & &1["id"]) -- - Enum.map(first_status_reports, & &1.id) == [] - - assert second_group["date"] == - Enum.max_by(second_status_reports, fn act -> - NaiveDateTime.from_iso8601!(act.data["published"]) - end).data["published"] - - assert second_group["status"] == - Map.put( - stringify_keys(StatusView.render("show.json", %{activity: second_status})), - "deleted", - false - ) - - assert second_group["account"]["id"] == target_user.id - - assert length(second_group["actors"]) == 1 - assert hd(second_group["actors"])["id"] == reporter.id - - assert Enum.map(second_group["reports"], & &1["id"]) -- - Enum.map(second_status_reports, & &1.id) == [] - - assert third_group["date"] == - Enum.max_by(third_status_reports, fn act -> - NaiveDateTime.from_iso8601!(act.data["published"]) - end).data["published"] - - assert third_group["status"] == - Map.put( - stringify_keys(StatusView.render("show.json", %{activity: third_status})), - "deleted", - false - ) - - assert third_group["account"]["id"] == target_user.id - - assert length(third_group["actors"]) == 1 - assert hd(third_group["actors"])["id"] == reporter.id - - assert Enum.map(third_group["reports"], & &1["id"]) -- - Enum.map(third_status_reports, & &1.id) == [] - end - - test "reopened report renders status data", %{ - conn: conn, - first_report: first_report, - first_status: first_status - } do - {:ok, _} = CommonAPI.update_report_state(first_report.id, "resolved") - - response = - conn - |> get("/api/pleroma/admin/grouped_reports") - |> json_response(:ok) - - first_group = Enum.find(response["reports"], &(&1["status"]["id"] == first_status.id)) - - assert first_group["status"] == - Map.put( - stringify_keys(StatusView.render("show.json", %{activity: first_status})), - "deleted", - false - ) - end - - test "reopened report does not render status data if status has been deleted", %{ - conn: conn, - first_report: first_report, - first_status: first_status, - target_user: target_user - } do - {:ok, _} = CommonAPI.update_report_state(first_report.id, "resolved") - {:ok, _} = CommonAPI.delete(first_status.id, target_user) - - refute Activity.get_by_ap_id(first_status.id) - - response = - conn - |> get("/api/pleroma/admin/grouped_reports") - |> json_response(:ok) - - assert Enum.find(response["reports"], &(&1["status"]["deleted"] == true))["status"][ - "deleted" - ] == true - - assert length(Enum.filter(response["reports"], &(&1["status"]["deleted"] == false))) == 2 - end - - test "account not empty if status was deleted", %{ - conn: conn, - first_report: first_report, - first_status: first_status, - target_user: target_user - } do - {:ok, _} = CommonAPI.update_report_state(first_report.id, "resolved") - {:ok, _} = CommonAPI.delete(first_status.id, target_user) - - refute Activity.get_by_ap_id(first_status.id) - - response = - conn - |> get("/api/pleroma/admin/grouped_reports") - |> json_response(:ok) - - assert Enum.find(response["reports"], &(&1["status"]["deleted"] == true))["account"] - end - end - describe "PUT /api/pleroma/admin/statuses/:id" do setup do activity = insert(:note_activity) From 219d3aaa2d1fd2474a88ec40d7e6938741e7fc4b Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 31 Mar 2020 13:05:16 -0500 Subject: [PATCH 80/88] Update AdminFE build in preparation for Pleroma 2.0.2 --- .../{app.c836e084.css => app.85534e14.css} | 0 ...f.650c8e81.css => chunk-0d8f.d85f5a29.css} | 2 +- ...a.3936457d.css => chunk-136a.f1130f8e.css} | 2 +- priv/static/adminfe/chunk-13e9.98eaadba.css | 1 + priv/static/adminfe/chunk-2b9c.feb61a2b.css | 1 + priv/static/adminfe/chunk-46cf.a43e9415.css | 1 - ...f.d45db7be.css => chunk-46ef.145de4f9.css} | 2 +- priv/static/adminfe/chunk-4e7d.7aace723.css | 1 - priv/static/adminfe/chunk-87b3.2affd602.css | 1 - priv/static/adminfe/chunk-87b3.3c6ede9c.css | 1 + ...f.cba3ae06.css => chunk-88c9.184084df.css} | 2 +- ...7.4d39576f.css => chunk-cf57.26596375.css} | 2 +- priv/static/adminfe/index.html | 2 +- priv/static/adminfe/static/js/app.d2c3c6b3.js | Bin 181998 -> 0 bytes .../adminfe/static/js/app.d2c3c6b3.js.map | Bin 403968 -> 0 bytes priv/static/adminfe/static/js/app.d898cc2b.js | Bin 0 -> 185128 bytes .../adminfe/static/js/app.d898cc2b.js.map | Bin 0 -> 410154 bytes ...d8f.a85e3222.js => chunk-0d8f.6d50ff86.js} | Bin 33538 -> 33538 bytes ...3222.js.map => chunk-0d8f.6d50ff86.js.map} | Bin 116201 -> 116201 bytes ...36a.142aa42a.js => chunk-136a.c4719e3e.js} | Bin 19553 -> 19553 bytes ...a42a.js.map => chunk-136a.c4719e3e.js.map} | Bin 69090 -> 69090 bytes .../adminfe/static/js/chunk-13e9.79da1569.js | Bin 0 -> 9528 bytes .../static/js/chunk-13e9.79da1569.js.map | Bin 0 -> 40125 bytes .../adminfe/static/js/chunk-2b9c.cf321c74.js | Bin 0 -> 28194 bytes .../static/js/chunk-2b9c.cf321c74.js.map | Bin 0 -> 95810 bytes .../adminfe/static/js/chunk-46cf.3bd3567a.js | Bin 9526 -> 0 bytes .../static/js/chunk-46cf.3bd3567a.js.map | Bin 40123 -> 0 bytes ...6ef.215af110.js => chunk-46ef.671cac7d.js} | Bin 7765 -> 7765 bytes ...f110.js.map => chunk-46ef.671cac7d.js.map} | Bin 26170 -> 26170 bytes .../adminfe/static/js/chunk-4e7d.a40ad735.js | Bin 23331 -> 0 bytes .../static/js/chunk-4e7d.a40ad735.js.map | Bin 80396 -> 0 bytes ...7b3.4704cadf.js => chunk-87b3.3c11ef09.js} | Bin 103161 -> 103449 bytes .../static/js/chunk-87b3.3c11ef09.js.map | Bin 0 -> 358904 bytes .../static/js/chunk-87b3.4704cadf.js.map | Bin 358274 -> 0 bytes .../adminfe/static/js/chunk-88c9.e3583744.js | Bin 0 -> 24234 bytes .../static/js/chunk-88c9.e3583744.js.map | Bin 0 -> 92387 bytes .../adminfe/static/js/chunk-cf57.3e45f57f.js | Bin 0 -> 29728 bytes .../static/js/chunk-cf57.3e45f57f.js.map | Bin 0 -> 89855 bytes .../adminfe/static/js/chunk-cf57.42b96339.js | Bin 29100 -> 0 bytes .../static/js/chunk-cf57.42b96339.js.map | Bin 88026 -> 0 bytes .../adminfe/static/js/chunk-e5cf.501d7902.js | Bin 24234 -> 0 bytes .../static/js/chunk-e5cf.501d7902.js.map | Bin 92386 -> 0 bytes .../adminfe/static/js/runtime.cb26bbd1.js | Bin 0 -> 3969 bytes ...a19e5d1.js.map => runtime.cb26bbd1.js.map} | Bin 16759 -> 16759 bytes .../adminfe/static/js/runtime.fa19e5d1.js | Bin 3969 -> 0 bytes 45 files changed, 9 insertions(+), 9 deletions(-) rename priv/static/adminfe/{app.c836e084.css => app.85534e14.css} (100%) rename priv/static/adminfe/{chunk-0d8f.650c8e81.css => chunk-0d8f.d85f5a29.css} (96%) rename priv/static/adminfe/{chunk-136a.3936457d.css => chunk-136a.f1130f8e.css} (98%) create mode 100644 priv/static/adminfe/chunk-13e9.98eaadba.css create mode 100644 priv/static/adminfe/chunk-2b9c.feb61a2b.css delete mode 100644 priv/static/adminfe/chunk-46cf.a43e9415.css rename priv/static/adminfe/{chunk-46ef.d45db7be.css => chunk-46ef.145de4f9.css} (92%) delete mode 100644 priv/static/adminfe/chunk-4e7d.7aace723.css delete mode 100644 priv/static/adminfe/chunk-87b3.2affd602.css create mode 100644 priv/static/adminfe/chunk-87b3.3c6ede9c.css rename priv/static/adminfe/{chunk-e5cf.cba3ae06.css => chunk-88c9.184084df.css} (92%) rename priv/static/adminfe/{chunk-cf57.4d39576f.css => chunk-cf57.26596375.css} (74%) delete mode 100644 priv/static/adminfe/static/js/app.d2c3c6b3.js delete mode 100644 priv/static/adminfe/static/js/app.d2c3c6b3.js.map create mode 100644 priv/static/adminfe/static/js/app.d898cc2b.js create mode 100644 priv/static/adminfe/static/js/app.d898cc2b.js.map rename priv/static/adminfe/static/js/{chunk-0d8f.a85e3222.js => chunk-0d8f.6d50ff86.js} (99%) rename priv/static/adminfe/static/js/{chunk-0d8f.a85e3222.js.map => chunk-0d8f.6d50ff86.js.map} (99%) rename priv/static/adminfe/static/js/{chunk-136a.142aa42a.js => chunk-136a.c4719e3e.js} (99%) rename priv/static/adminfe/static/js/{chunk-136a.142aa42a.js.map => chunk-136a.c4719e3e.js.map} (99%) create mode 100644 priv/static/adminfe/static/js/chunk-13e9.79da1569.js create mode 100644 priv/static/adminfe/static/js/chunk-13e9.79da1569.js.map create mode 100644 priv/static/adminfe/static/js/chunk-2b9c.cf321c74.js create mode 100644 priv/static/adminfe/static/js/chunk-2b9c.cf321c74.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-46cf.3bd3567a.js delete mode 100644 priv/static/adminfe/static/js/chunk-46cf.3bd3567a.js.map rename priv/static/adminfe/static/js/{chunk-46ef.215af110.js => chunk-46ef.671cac7d.js} (99%) rename priv/static/adminfe/static/js/{chunk-46ef.215af110.js.map => chunk-46ef.671cac7d.js.map} (98%) delete mode 100644 priv/static/adminfe/static/js/chunk-4e7d.a40ad735.js delete mode 100644 priv/static/adminfe/static/js/chunk-4e7d.a40ad735.js.map rename priv/static/adminfe/static/js/{chunk-87b3.4704cadf.js => chunk-87b3.3c11ef09.js} (60%) create mode 100644 priv/static/adminfe/static/js/chunk-87b3.3c11ef09.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-87b3.4704cadf.js.map create mode 100644 priv/static/adminfe/static/js/chunk-88c9.e3583744.js create mode 100644 priv/static/adminfe/static/js/chunk-88c9.e3583744.js.map create mode 100644 priv/static/adminfe/static/js/chunk-cf57.3e45f57f.js create mode 100644 priv/static/adminfe/static/js/chunk-cf57.3e45f57f.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-cf57.42b96339.js delete mode 100644 priv/static/adminfe/static/js/chunk-cf57.42b96339.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-e5cf.501d7902.js delete mode 100644 priv/static/adminfe/static/js/chunk-e5cf.501d7902.js.map create mode 100644 priv/static/adminfe/static/js/runtime.cb26bbd1.js rename priv/static/adminfe/static/js/{runtime.fa19e5d1.js.map => runtime.cb26bbd1.js.map} (93%) delete mode 100644 priv/static/adminfe/static/js/runtime.fa19e5d1.js diff --git a/priv/static/adminfe/app.c836e084.css b/priv/static/adminfe/app.85534e14.css similarity index 100% rename from priv/static/adminfe/app.c836e084.css rename to priv/static/adminfe/app.85534e14.css diff --git a/priv/static/adminfe/chunk-0d8f.650c8e81.css b/priv/static/adminfe/chunk-0d8f.d85f5a29.css similarity index 96% rename from priv/static/adminfe/chunk-0d8f.650c8e81.css rename to priv/static/adminfe/chunk-0d8f.d85f5a29.css index 0b2a3f669..931620872 100644 --- a/priv/static/adminfe/chunk-0d8f.650c8e81.css +++ b/priv/static/adminfe/chunk-0d8f.d85f5a29.css @@ -1 +1 @@ -.select-field[data-v-29abde8c]{width:350px}@media only screen and (max-width:480px){.select-field[data-v-29abde8c]{width:100%;margin-bottom:5px}}@media only screen and (max-width:801px) and (min-width:481px){.select-field[data-v-29abde8c]{width:50%}}.actions-button[data-v-3850612b]{text-align:left;width:350px;padding:10px}.actions-button-container[data-v-3850612b]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.el-dropdown[data-v-3850612b]{float:right}.el-icon-edit[data-v-3850612b]{margin-right:5px}.tag-container[data-v-3850612b]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.tag-text[data-v-3850612b]{padding-right:20px}.no-hover[data-v-3850612b]:hover{color:#606266;background-color:#fff;cursor:auto}.el-dialog__body{padding:20px}.create-account-form-item{margin-bottom:20px}.create-account-form-item-without-margin{margin-bottom:0}@media only screen and (max-width:480px){.create-user-dialog{width:85%}.create-account-form-item{margin-bottom:20px}.el-dialog__body{padding:20px}}.moderate-user-button{text-align:left;width:200px;padding:10px}.moderate-user-button-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.actions-button{text-align:left;width:350px;padding:10px}.actions-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:0 15px 10px}.active-tag{color:#409eff;font-weight:700}.active-tag .el-icon-check{color:#409eff;float:right;margin:7px 0 0 15px}.el-dropdown-link:hover{cursor:pointer;color:#409eff}.create-account>.el-icon-plus{margin-right:5px}.password-reset-token{margin:0 0 14px}.password-reset-token-dialog{width:50%}.reset-password-link{text-decoration:underline}.users-container h1{margin:22px 0 0 15px}.users-container .pagination{margin:25px 0;text-align:center}.users-container .search{width:350px;float:right}.users-container .filter-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:22px 15px 15px}.users-container .user-count{color:grey;font-size:28px}@media only screen and (max-width:480px){.password-reset-token-dialog{width:85%}.users-container h1{margin:7px 10px 15px}.users-container .actions-button{width:100%}.users-container .actions-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin:0 10px 7px}.users-container .el-icon-arrow-down{font-size:12px}.users-container .search{width:100%}.users-container .filter-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:82px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin:0 10px}.users-container .el-tag{width:30px;display:inline-block;margin-bottom:4px;font-weight:700}.users-container .el-tag.el-tag--danger,.users-container .el-tag.el-tag--success{padding-left:8px}} \ No newline at end of file +.select-field[data-v-29abde8c]{width:350px}@media only screen and (max-width:480px){.select-field[data-v-29abde8c]{width:100%;margin-bottom:5px}}@media only screen and (max-width:801px) and (min-width:481px){.select-field[data-v-29abde8c]{width:50%}}.actions-button[data-v-3850612b]{text-align:left;width:350px;padding:10px}.actions-button-container[data-v-3850612b]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.el-dropdown[data-v-3850612b]{float:right}.el-icon-edit[data-v-3850612b]{margin-right:5px}.tag-container[data-v-3850612b]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.tag-text[data-v-3850612b]{padding-right:20px}.no-hover[data-v-3850612b]:hover{color:#606266;background-color:#fff;cursor:auto}.el-dialog__body{padding:20px}.create-account-form-item{margin-bottom:20px}.create-account-form-item-without-margin{margin-bottom:0}@media only screen and (max-width:480px){.create-user-dialog{width:85%}.create-account-form-item{margin-bottom:20px}.el-dialog__body{padding:20px}}.moderate-user-button{text-align:left;width:200px;padding:10px}.moderate-user-button-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.actions-button{text-align:left;width:350px;padding:10px}.actions-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:0 15px 10px}.active-tag{color:#409eff;font-weight:700}.active-tag .el-icon-check{color:#409eff;float:right;margin:7px 0 0 15px}.el-dropdown-link:hover{cursor:pointer;color:#409eff}.create-account>.el-icon-plus{margin-right:5px}.password-reset-token{margin:0 0 14px}.password-reset-token-dialog{width:50%}.reset-password-link{text-decoration:underline}.users-container h1{margin:10px 0 0 15px}.users-container .pagination{margin:25px 0;text-align:center}.users-container .search{width:350px;float:right}.users-container .filter-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:22px 15px 15px}.users-container .user-count{color:grey;font-size:28px}@media only screen and (max-width:480px){.password-reset-token-dialog{width:85%}.users-container h1{margin:7px 10px 15px}.users-container .actions-button{width:100%}.users-container .actions-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin:0 10px 7px}.users-container .el-icon-arrow-down{font-size:12px}.users-container .search{width:100%}.users-container .filter-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:82px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin:0 10px}.users-container .el-tag{width:30px;display:inline-block;margin-bottom:4px;font-weight:700}.users-container .el-tag.el-tag--danger,.users-container .el-tag.el-tag--success{padding-left:8px}} \ No newline at end of file diff --git a/priv/static/adminfe/chunk-136a.3936457d.css b/priv/static/adminfe/chunk-136a.f1130f8e.css similarity index 98% rename from priv/static/adminfe/chunk-136a.3936457d.css rename to priv/static/adminfe/chunk-136a.f1130f8e.css index 2857a9d6e..f492b37d0 100644 --- a/priv/static/adminfe/chunk-136a.3936457d.css +++ b/priv/static/adminfe/chunk-136a.f1130f8e.css @@ -1 +1 @@ -.copy-popover{width:330px}.emoji-buttons{place-self:center;min-width:200px}.emoji-container-grid{display:grid;grid-template-columns:75px auto auto 200px;grid-column-gap:15px;margin-bottom:10px}.emoji-preview-img{max-width:100%;place-self:center}.emoji-info{place-self:center}.copy-pack-container{place-self:center stretch}.copy-pack-select{width:100%}.remote-emoji-container-grid{display:grid;grid-template-columns:75px auto auto 160px;grid-column-gap:15px;margin-bottom:10px}@media only screen and (max-width:480px){.emoji-container-flex{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;border:1px solid #dcdfe6;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1);border-radius:4px;padding:15px;margin:0 15px 15px 0}.emoji-info,.emoji-preview-img{margin-bottom:10px}.emoji-buttons{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;width:100%}.emoji-buttons button{padding:10px 5px;width:47%}}@media only screen and (max-width:801px) and (min-width:481px){.emoji-container-grid{grid-column-gap:10px}.emoji-buttons .el-button+.el-button{margin-left:5px}.remote-emoji-container-grid{grid-column-gap:10px}}.add-new-emoji{height:36px;font-size:14px;font-weight:700;color:#606266}.text{line-height:20px;margin-right:15px}.upload-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.upload-button{margin-left:10px}.upload-file-url{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}@media only screen and (max-width:480px){.new-emoji-uploader-form label.el-form-item__label{padding:0}}.download-archive{width:250px}.download-pack-button-container{width:265px}.download-pack-button-container .el-link,.download-pack-button-container .el-link span,.download-pack-button-container .el-link span .download-archive{width:inherit}.download-shared-pack{display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:10px}.download-shared-pack-button{margin-left:10px}.el-collapse-item__content{padding-bottom:0}.el-collapse-item__header{height:36px;font-size:14px;font-weight:700;color:#606266}.emoji-pack-card{margin-top:5px}.emoji-pack-metadata .el-form-item{margin-bottom:10px}.has-background .el-collapse-item__header{background:#f6f6f6}.no-background .el-collapse-item__header{background:#fff}.pack-button-container{margin:0 0 18px 120px}.save-pack-button-container{margin-bottom:8px;width:265px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}@media only screen and (max-width:480px){.delete-pack-button{width:45%}.download-pack-button-container{width:100%}.download-shared-pack{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.download-shared-pack-button{margin-left:0;margin-top:10px;padding:10px}.pack-button-container{width:100%;margin:0 0 22px}.remote-pack-metadata .el-form-item__content{line-height:24px;margin-top:4px}.save-pack-button{width:54%}.save-pack-button-container{margin-bottom:8px;width:100%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.save-pack-button-container button{padding:10px 5px}.save-pack-button-container .el-button+.el-button{margin-left:3px}}.emoji-packs-header-button-container{margin:0 0 22px 15px}.create-pack,.emoji-packs-header-button-container{display:-webkit-box;display:-ms-flexbox;display:flex}.create-pack{-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.create-pack-button{margin-left:10px}.emoji-packs-form{margin:0 30px}.emoji-packs-header{margin:22px 0 20px 15px}.import-pack-button{margin-left:10px}.line{width:100%;height:0;border:1px solid #eee;margin-bottom:22px}@media only screen and (min-width:1824px){.emoji-packs{max-width:1824px;margin:auto}}@media only screen and (max-width:480px){.create-pack{height:82px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.create-pack-button{margin-left:0}.divider{margin:15px 0}.el-message{min-width:80%}.el-message-box{width:80%}.emoji-packs-form{margin:0 7px}.emoji-packs-form label{padding-right:8px}.emoji-packs-form .el-form-item{margin-bottom:15px}.emoji-packs-header{margin:15px}.emoji-packs-header-button-container{height:82px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.emoji-packs-header-button-container .el-button+.el-button{margin:7px 0 0}.emoji-packs-header-button-container .el-button+.el-button,.reload-emoji-button{width:-webkit-fit-content;width:-moz-fit-content;width:fit-content}} \ No newline at end of file +.copy-popover{width:330px}.emoji-buttons{place-self:center;min-width:200px}.emoji-container-grid{display:grid;grid-template-columns:75px auto auto 200px;grid-column-gap:15px;margin-bottom:10px}.emoji-preview-img{max-width:100%;place-self:center}.emoji-info{place-self:center}.copy-pack-container{place-self:center stretch}.copy-pack-select{width:100%}.remote-emoji-container-grid{display:grid;grid-template-columns:75px auto auto 160px;grid-column-gap:15px;margin-bottom:10px}@media only screen and (max-width:480px){.emoji-container-flex{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;border:1px solid #dcdfe6;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1);border-radius:4px;padding:15px;margin:0 15px 15px 0}.emoji-info,.emoji-preview-img{margin-bottom:10px}.emoji-buttons{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;width:100%}.emoji-buttons button{padding:10px 5px;width:47%}}@media only screen and (max-width:801px) and (min-width:481px){.emoji-container-grid{grid-column-gap:10px}.emoji-buttons .el-button+.el-button{margin-left:5px}.remote-emoji-container-grid{grid-column-gap:10px}}.add-new-emoji{height:36px;font-size:14px;font-weight:700;color:#606266}.text{line-height:20px;margin-right:15px}.upload-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.upload-button{margin-left:10px}.upload-file-url{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}@media only screen and (max-width:480px){.new-emoji-uploader-form label.el-form-item__label{padding:0}}.download-archive{width:250px}.download-pack-button-container{width:265px}.download-pack-button-container .el-link,.download-pack-button-container .el-link span,.download-pack-button-container .el-link span .download-archive{width:inherit}.download-shared-pack{display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:10px}.download-shared-pack-button{margin-left:10px}.el-collapse-item__content{padding-bottom:0}.el-collapse-item__header{height:36px;font-size:14px;font-weight:700;color:#606266}.emoji-pack-card{margin-top:5px}.emoji-pack-metadata .el-form-item{margin-bottom:10px}.has-background .el-collapse-item__header{background:#f6f6f6}.no-background .el-collapse-item__header{background:#fff}.pack-button-container{margin:0 0 18px 120px}.save-pack-button-container{margin-bottom:8px;width:265px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}@media only screen and (max-width:480px){.delete-pack-button{width:45%}.download-pack-button-container{width:100%}.download-shared-pack{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.download-shared-pack-button{margin-left:0;margin-top:10px;padding:10px}.pack-button-container{width:100%;margin:0 0 22px}.remote-pack-metadata .el-form-item__content{line-height:24px;margin-top:4px}.save-pack-button{width:54%}.save-pack-button-container{margin-bottom:8px;width:100%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.save-pack-button-container button{padding:10px 5px}.save-pack-button-container .el-button+.el-button{margin-left:3px}}.emoji-packs-header-button-container{margin:0 0 22px 15px}.create-pack,.emoji-packs-header-button-container{display:-webkit-box;display:-ms-flexbox;display:flex}.create-pack{-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.create-pack-button{margin-left:10px}.emoji-packs-form{margin:0 30px}.emoji-packs-header{margin:10px 0 20px 15px}.import-pack-button{margin-left:10px}.line{width:100%;height:0;border:1px solid #eee;margin-bottom:22px}@media only screen and (min-width:1824px){.emoji-packs{max-width:1824px;margin:auto}}@media only screen and (max-width:480px){.create-pack{height:82px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.create-pack-button{margin-left:0}.divider{margin:15px 0}.el-message{min-width:80%}.el-message-box{width:80%}.emoji-packs-form{margin:0 7px}.emoji-packs-form label{padding-right:8px}.emoji-packs-form .el-form-item{margin-bottom:15px}.emoji-packs-header{margin:15px}.emoji-packs-header-button-container{height:82px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.emoji-packs-header-button-container .el-button+.el-button{margin:7px 0 0}.emoji-packs-header-button-container .el-button+.el-button,.reload-emoji-button{width:-webkit-fit-content;width:-moz-fit-content;width:fit-content}} \ No newline at end of file diff --git a/priv/static/adminfe/chunk-13e9.98eaadba.css b/priv/static/adminfe/chunk-13e9.98eaadba.css new file mode 100644 index 000000000..9f377eee2 --- /dev/null +++ b/priv/static/adminfe/chunk-13e9.98eaadba.css @@ -0,0 +1 @@ +.moderation-log-container[data-v-5d520014]{margin:0 15px}h1[data-v-5d520014]{margin:10px 0 20px}.el-timeline[data-v-5d520014]{margin:25px 45px 0 0;padding:0}.moderation-log-date-panel[data-v-5d520014]{width:350px}.moderation-log-nav-container[data-v-5d520014]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.moderation-log-search[data-v-5d520014]{width:350px}.moderation-log-user-select[data-v-5d520014]{margin:0 0 20px;width:350px}.search-container[data-v-5d520014]{text-align:right}.pagination[data-v-5d520014]{text-align:center}@media only screen and (max-width:480px){.moderation-log-date-panel[data-v-5d520014]{width:100%}.moderation-log-user-select[data-v-5d520014]{margin:0 0 10px;width:55%}.moderation-log-search[data-v-5d520014]{width:40%}}@media only screen and (max-width:801px) and (min-width:481px){.moderation-log-date-panel[data-v-5d520014]{width:55%}.moderation-log-user-select[data-v-5d520014]{margin:0 0 10px;width:55%}.moderation-log-search[data-v-5d520014]{width:40%}} \ No newline at end of file diff --git a/priv/static/adminfe/chunk-2b9c.feb61a2b.css b/priv/static/adminfe/chunk-2b9c.feb61a2b.css new file mode 100644 index 000000000..f54eca1f5 --- /dev/null +++ b/priv/static/adminfe/chunk-2b9c.feb61a2b.css @@ -0,0 +1 @@ +.status-card{margin-bottom:10px}.status-card .account{text-decoration:underline;line-height:26px;font-size:13px}.status-card .image{width:20%}.status-card .image img{width:100%}.status-card .show-more-button{margin-left:5px}.status-card .status-account{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.status-card .status-avatar-img{display:inline-block;width:15px;height:15px;margin-right:5px}.status-card .status-account-name{display:inline-block;margin:0;height:22px}.status-card .status-body{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.status-card .status-checkbox{margin-right:7px}.status-card .status-content{font-size:15px;line-height:26px}.status-card .status-deleted{font-style:italic;margin-top:3px}.status-card .status-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.status-card .status-without-content{font-style:italic}@media only screen and (max-width:480px){.el-message{min-width:80%}.el-message-box{width:80%}.status-card .el-card__header{padding:10px 17px}.status-card .el-tag{margin:3px 4px 3px 0}.status-card .status-account-container{margin-bottom:5px}.status-card .status-actions-button{margin:3px 0}.status-card .status-actions{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.status-card .status-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}}.moderate-user-button{text-align:left;width:200px;padding:10px}.moderate-user-button-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}@media (max-width:800px){.security-settings-modal .el-dialog{width:90%}}.security-settings-modal .el-alert .el-alert__description{word-break:break-word;font-size:1em}.security-settings-modal .form-text{display:block;margin-top:.25rem;color:#909399}.avatar-name-container[data-v-77412d30],header[data-v-77412d30]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}header[data-v-77412d30]{margin:22px 0;padding-left:15px}header h1[data-v-77412d30]{margin:0 0 0 10px}table[data-v-77412d30]{margin:10px 0 0 15px}table .name-col[data-v-77412d30]{width:150px}.el-table--border[data-v-77412d30]:after,.el-table--group[data-v-77412d30]:after,.el-table[data-v-77412d30]:before{background-color:transparent}.poll ul[data-v-77412d30]{list-style-type:none;padding:0;width:30%}.image[data-v-77412d30]{width:20%}.image img[data-v-77412d30]{width:100%}.no-statuses[data-v-77412d30]{margin-left:28px;color:#606266}.recent-statuses-container[data-v-77412d30]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:67%}.recent-statuses-header[data-v-77412d30]{margin-top:10px}.statuses[data-v-77412d30]{padding:0 20px 0 0}.show-private[data-v-77412d30]{width:200px;text-align:left;line-height:67px;margin-right:20px}.show-private-statuses[data-v-77412d30]{margin-left:28px;margin-bottom:20px}.recent-statuses[data-v-77412d30]{margin-left:28px}.user-page-header[data-v-77412d30]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;padding:0 20px}.user-page-header h1[data-v-77412d30]{display:inline}.user-profile-card[data-v-77412d30]{margin:0 20px;width:30%;height:-webkit-fit-content;height:-moz-fit-content;height:fit-content}.user-profile-container[data-v-77412d30]{display:-webkit-box;display:-ms-flexbox;display:flex}.user-profile-table[data-v-77412d30]{margin:0}.user-profile-tag[data-v-77412d30]{margin:0 4px 4px 0}@media only screen and (max-width:480px){.avatar-name-container[data-v-77412d30]{margin-bottom:10px}.recent-statuses[data-v-77412d30]{margin:20px 10px 15px}.recent-statuses-container[data-v-77412d30]{width:100%;margin:0 10px}.show-private-statuses[data-v-77412d30]{margin:0 10px 20px}.user-page-header[data-v-77412d30]{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;padding:0;margin:7px 0 15px 10px}.user-profile-card[data-v-77412d30]{margin:0 10px;width:95%}.user-profile-card td[data-v-77412d30]{width:80px}.user-profile-container[data-v-77412d30]{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}}@media only screen and (max-width:801px) and (min-width:481px){.avatar-name-container[data-v-77412d30]{margin-bottom:20px}.recent-statuses[data-v-77412d30]{margin:20px 10px 15px 0}.recent-statuses-container[data-v-77412d30]{width:97%;margin:0 20px}.show-private-statuses[data-v-77412d30]{margin:0 10px 20px 0}.user-page-header[data-v-77412d30]{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;padding:0;margin:7px 0 20px 20px}.user-profile-card[data-v-77412d30]{margin:0 20px;width:-webkit-fit-content;width:-moz-fit-content;width:fit-content}.user-profile-container[data-v-77412d30]{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}} \ No newline at end of file diff --git a/priv/static/adminfe/chunk-46cf.a43e9415.css b/priv/static/adminfe/chunk-46cf.a43e9415.css deleted file mode 100644 index aa7160528..000000000 --- a/priv/static/adminfe/chunk-46cf.a43e9415.css +++ /dev/null @@ -1 +0,0 @@ -.moderation-log-container[data-v-5798cff5]{margin:0 15px}h1[data-v-5798cff5]{margin:22px 0 20px}.el-timeline[data-v-5798cff5]{margin:25px 45px 0 0;padding:0}.moderation-log-date-panel[data-v-5798cff5]{width:350px}.moderation-log-nav-container[data-v-5798cff5]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.moderation-log-search[data-v-5798cff5]{width:350px}.moderation-log-user-select[data-v-5798cff5]{margin:0 0 20px;width:350px}.search-container[data-v-5798cff5]{text-align:right}.pagination[data-v-5798cff5]{text-align:center}@media only screen and (max-width:480px){.moderation-log-date-panel[data-v-5798cff5]{width:100%}.moderation-log-user-select[data-v-5798cff5]{margin:0 0 10px;width:55%}.moderation-log-search[data-v-5798cff5]{width:40%}}@media only screen and (max-width:801px) and (min-width:481px){.moderation-log-date-panel[data-v-5798cff5]{width:55%}.moderation-log-user-select[data-v-5798cff5]{margin:0 0 10px;width:55%}.moderation-log-search[data-v-5798cff5]{width:40%}} \ No newline at end of file diff --git a/priv/static/adminfe/chunk-46ef.d45db7be.css b/priv/static/adminfe/chunk-46ef.145de4f9.css similarity index 92% rename from priv/static/adminfe/chunk-46ef.d45db7be.css rename to priv/static/adminfe/chunk-46ef.145de4f9.css index d6cc7d182..deb5249ac 100644 --- a/priv/static/adminfe/chunk-46ef.d45db7be.css +++ b/priv/static/adminfe/chunk-46ef.145de4f9.css @@ -1 +1 @@ -.invites-container .actions-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:20px 15px 15px}.invites-container .create-invite-token{text-align:left;width:350px;padding:10px}.invites-container .create-new-token-dialog{width:40%}.invites-container .el-dialog__body{padding:5px 20px 0}.invites-container h1{margin:22px 0 0 15px}.invites-container .icon{margin-right:5px}.invites-container .invite-token-table{width:100%;margin:0 15px}.invites-container .invite-via-email{text-align:left;width:350px;padding:10px}.invites-container .invite-via-email-dialog{width:50%}.invites-container .info{color:#666;font-size:13px;line-height:22px;margin:0 0 10px}@media only screen and (max-width:480px){.invites-container .actions-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:82px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:15px 10px 7px}.invites-container .cell{padding:0}.invites-container .create-invite-token{width:100%}.invites-container .create-new-token-dialog{width:85%}.invites-container .el-date-editor{width:150px}.invites-container .el-dialog__body{padding:5px 15px 0}.invites-container h1{margin:7px 10px 15px}.invites-container .invite-token-table{width:100%;margin:0 5px;font-size:12px;font-weight:500}.invites-container .invite-via-email{width:100%;margin:10px 0 0}.invites-container .invite-via-email-dialog{width:85%}.invites-container .info{margin:0 0 10px 5px}.invites-container th .cell{padding:0}.create-invite-token,.invite-via-email{width:100%}} \ No newline at end of file +.invites-container .actions-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:20px 15px 15px}.invites-container .create-invite-token{text-align:left;width:350px;padding:10px}.invites-container .create-new-token-dialog{width:40%}.invites-container .el-dialog__body{padding:5px 20px 0}.invites-container h1{margin:10px 0 0 15px}.invites-container .icon{margin-right:5px}.invites-container .invite-token-table{width:100%;margin:0 15px}.invites-container .invite-via-email{text-align:left;width:350px;padding:10px}.invites-container .invite-via-email-dialog{width:50%}.invites-container .info{color:#666;font-size:13px;line-height:22px;margin:0 0 10px}@media only screen and (max-width:480px){.invites-container .actions-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:82px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:15px 10px 7px}.invites-container .cell{padding:0}.invites-container .create-invite-token{width:100%}.invites-container .create-new-token-dialog{width:85%}.invites-container .el-date-editor{width:150px}.invites-container .el-dialog__body{padding:5px 15px 0}.invites-container h1{margin:7px 10px 15px}.invites-container .invite-token-table{width:100%;margin:0 5px;font-size:12px;font-weight:500}.invites-container .invite-via-email{width:100%;margin:10px 0 0}.invites-container .invite-via-email-dialog{width:85%}.invites-container .info{margin:0 0 10px 5px}.invites-container th .cell{padding:0}.create-invite-token,.invite-via-email{width:100%}} \ No newline at end of file diff --git a/priv/static/adminfe/chunk-4e7d.7aace723.css b/priv/static/adminfe/chunk-4e7d.7aace723.css deleted file mode 100644 index 9a35b64a0..000000000 --- a/priv/static/adminfe/chunk-4e7d.7aace723.css +++ /dev/null @@ -1 +0,0 @@ -.status-card{margin-bottom:10px}.status-card .account{text-decoration:underline;line-height:26px;font-size:13px}.status-card .image{width:20%}.status-card .image img{width:100%}.status-card .show-more-button{margin-left:5px}.status-card .status-account{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.status-card .status-avatar-img{display:inline-block;width:15px;height:15px;margin-right:5px}.status-card .status-account-name{display:inline-block;margin:0;height:22px}.status-card .status-body{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.status-card .status-checkbox{margin-right:7px}.status-card .status-content{font-size:15px;line-height:26px}.status-card .status-deleted{font-style:italic;margin-top:3px}.status-card .status-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.status-card .status-without-content{font-style:italic}@media only screen and (max-width:480px){.el-message{min-width:80%}.el-message-box{width:80%}.status-card .el-card__header{padding:10px 17px}.status-card .el-tag{margin:3px 4px 3px 0}.status-card .status-account-container{margin-bottom:5px}.status-card .status-actions-button{margin:3px 0}.status-card .status-actions{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.status-card .status-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}}.moderate-user-button{text-align:left;width:200px;padding:10px}.moderate-user-button-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.avatar-name-container[data-v-68790c38],header[data-v-68790c38]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}header[data-v-68790c38]{margin:22px 0;padding-left:15px}header h1[data-v-68790c38]{margin:0 0 0 10px}table[data-v-68790c38]{margin:10px 0 0 15px}table .name-col[data-v-68790c38]{width:150px}.el-table--border[data-v-68790c38]:after,.el-table--group[data-v-68790c38]:after,.el-table[data-v-68790c38]:before{background-color:transparent}.poll ul[data-v-68790c38]{list-style-type:none;padding:0;width:30%}.image[data-v-68790c38]{width:20%}.image img[data-v-68790c38]{width:100%}.no-statuses[data-v-68790c38]{margin-left:28px;color:#606266}.recent-statuses-container[data-v-68790c38]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:67%}.recent-statuses-header[data-v-68790c38]{margin-top:10px}.statuses[data-v-68790c38]{padding:0 20px 0 0}.show-private[data-v-68790c38]{width:200px;text-align:left;line-height:67px;margin-right:20px}.show-private-statuses[data-v-68790c38]{margin-left:28px;margin-bottom:20px}.recent-statuses[data-v-68790c38]{margin-left:28px}.user-page-header[data-v-68790c38]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;padding:0 20px}.user-page-header h1[data-v-68790c38]{display:inline}.user-profile-card[data-v-68790c38]{margin:0 20px;width:30%;height:-webkit-fit-content;height:-moz-fit-content;height:fit-content}.user-profile-container[data-v-68790c38]{display:-webkit-box;display:-ms-flexbox;display:flex}.user-profile-table[data-v-68790c38]{margin:0}.user-profile-tag[data-v-68790c38]{margin:0 4px 4px 0}@media only screen and (max-width:480px){.avatar-name-container[data-v-68790c38]{margin-bottom:10px}.recent-statuses[data-v-68790c38]{margin:20px 10px 15px}.recent-statuses-container[data-v-68790c38]{width:100%;margin:0 10px}.show-private-statuses[data-v-68790c38]{margin:0 10px 20px}.user-page-header[data-v-68790c38]{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;padding:0;margin:7px 0 15px 10px}.user-profile-card[data-v-68790c38]{margin:0 10px;width:95%}.user-profile-card td[data-v-68790c38]{width:80px}.user-profile-container[data-v-68790c38]{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}}@media only screen and (max-width:801px) and (min-width:481px){.avatar-name-container[data-v-68790c38]{margin-bottom:20px}.recent-statuses[data-v-68790c38]{margin:20px 10px 15px 0}.recent-statuses-container[data-v-68790c38]{width:97%;margin:0 20px}.show-private-statuses[data-v-68790c38]{margin:0 10px 20px 0}.user-page-header[data-v-68790c38]{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;padding:0;margin:7px 0 20px 20px}.user-profile-card[data-v-68790c38]{margin:0 20px;width:-webkit-fit-content;width:-moz-fit-content;width:fit-content}.user-profile-container[data-v-68790c38]{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}} \ No newline at end of file diff --git a/priv/static/adminfe/chunk-87b3.2affd602.css b/priv/static/adminfe/chunk-87b3.2affd602.css deleted file mode 100644 index c4fa46d3e..000000000 --- a/priv/static/adminfe/chunk-87b3.2affd602.css +++ /dev/null @@ -1 +0,0 @@ -a{text-decoration:underline}.center-label label{text-align:center}.center-label label span{float:left}.code{background-color:rgba(173,190,214,.48);border-radius:3px;font-family:monospace;padding:0 3px}.delete-setting-button{margin-left:5px}.description>p{font-size:14px;color:#606266;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei;font-weight:700;line-height:20px;margin:0 0 14px}.description>p code{display:inline;padding:2px 3px;font-size:14px}.description-container{overflow-wrap:break-word;margin-bottom:0}.divider{margin:0 0 18px}.divider.thick-line{height:2px}.editable-keyword-container{width:100%}.el-form-item .rate-limit{margin-right:0}.el-input-group__prepend{padding-left:10px;padding-right:10px}.esshd-list{margin:0}.expl,.expl>p{color:#666;font-size:13px;line-height:22px;margin:5px 0 0;overflow-wrap:break-word;overflow:hidden;text-overflow:ellipsis}.expl>p code,.expl code{display:inline;line-height:22px;font-size:13px;padding:2px 3px}.follow-relay{width:350px;margin-right:7px}.form-container{margin-bottom:80px}.grouped-settings-header{margin:0 0 14px}.highlight{background-color:#e6e6e6}.icons-button-container{width:100%;margin-bottom:10px}.icons-button-desc{font-size:14px;color:#606266;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei;margin-left:5px}.icon-container{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:95%}.icon-values-container{display:-webkit-box;display:-ms-flexbox;display:flex;margin:0 10px 10px 0}.icon-key-input{width:30%;margin-right:8px}.icon-minus-button{width:36px;height:36px}.icon-value-input{width:70%;margin-left:8px}.icons-container,.input-container{display:-webkit-box;display:-ms-flexbox;display:flex}.input-container{-webkit-box-align:start;-ms-flex-align:start;align-items:start;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.input-container .el-form-item{margin-right:30px;width:100%}.input-container .el-select,.keyword-container{width:100%}label{overflow:hidden;text-overflow:ellipsis}.label-font{font-size:14px;color:#606266;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei;font-weight:700}.limit-button-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.limit-expl{margin-left:10px}.limit-input{width:47%;margin:0 0 5px 1%}.line{width:100%;height:0;border:1px solid #eee;margin-bottom:18px}.mascot{margin-bottom:15px}.mascot-container{width:100%}.mascot-input{margin-bottom:7px}.mascot-name-container{display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:7px}.mascot-name-input{margin-right:10px}.multiple-select-container{width:100%}.name-input{width:30%;margin-right:8px}.pattern-input{width:20%;margin-right:8px}.proxy-url-input{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-bottom:10px;width:100%}.proxy-url-host-input{width:35%;margin-right:8px}.proxy-url-value-input{width:35%;margin-left:8px;margin-right:10px}.prune-options{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.prune-options .el-radio{margin-top:11px}.rate-limit .el-form-item__content{width:100%;display:-webkit-box;display:-ms-flexbox;display:flex}.rate-limit-container,.rate-limit-content{width:100%}.rate-limit-label{float:right}.rate-limit-label-container{font-size:14px;color:#606266;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei;font-weight:700;height:36px;width:100%;margin-right:10px}.relays-container{margin:0 15px}.replacement-input{width:80%;margin-left:8px;margin-right:10px}.scale-input{width:47%;margin:0 1% 5px 0}.setting-input{display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:10px}.settings-container{max-width:1824px;margin:auto}.settings-container .el-tabs{margin-top:20px}.settings-delete-button{margin-left:5px}.settings-docs-button{width:163px;text-align:left;padding:10px}.settings-header{margin:0}.settings-header-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:22px 30px 15px 15px}.settings-reboot-button{width:145px;text-align:left;padding:10px;margin-right:5px}.single-input{margin-right:10px}.socks5-checkbox{font-size:14px;color:#606266;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei;font-weight:700;margin-left:10px}.socks5-checkbox-container{width:40%;height:36px;margin-right:5px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.ssl-tls-opts{margin:36px 0 0}.submit-button{float:right;margin:0 30px 22px 0}.submit-button-container{width:100%;position:fixed;bottom:0;right:0;z-index:10000}.switch-input{height:36px}.text{line-height:20px;margin-right:15px}.upload-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.value-input{width:70%;margin-left:8px;margin-right:10px}@media only screen and (min-width:1824px){.submit-button-container{max-width:1637px;margin-left:auto;margin-right:auto;right:auto}}@media only screen and (max-width:480px){.crontab,.crontab label{width:100%}.delete-setting-button{margin:4px 0 0 5px;height:28px}.delete-setting-button-container{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto}.description>p{line-height:18px;margin:0 5px 7px 0}.description>p code{display:inline;line-height:18px;padding:2px 3px;font-size:14px}.divider{margin:0 0 10px}.divider .thick-line{height:2px}.follow-relay{width:70%;margin-right:5px}.follow-relay input{width:100%}.follow-relay-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.input{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto}.input-container{width:100%}.input-container .el-form-item:first-child{margin:0;padding:0 15px 10px 0}.input-container .el-form-item.crontab-container:first-child{margin:0;padding:0}.input-container .el-form-item:first-child .mascot-form-item,.input-container .el-form-item:first-child .rate-limit{padding:0}.input-container .settings-delete-button{margin-top:4px;float:right}.input-row{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.limit-input{width:45%}.proxy-url-input{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;margin-bottom:0}.proxy-url-host-input{width:100%;margin-bottom:5px}.proxy-url-value-input{width:100%;margin-left:0}.prune-options{height:80px}.prune-options,.rate-limit .el-form-item__content{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.rate-limit-label{float:left}.scale-input{width:45%}.setting-label{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.settings-header{width:-webkit-fit-content;width:-moz-fit-content;width:fit-content;display:inline-block;margin:0}.settings-header-container{margin:15px}.nav-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:15px}.settings-menu{width:163px}.socks5-checkbox-container{width:100%}.submit-button{margin:0 15px 22px 0}.el-input__inner{padding:0 5px}.el-form-item__label:not(.no-top-margin){padding-left:3px;padding-right:10px;line-height:22px;margin-top:7px}.el-message{min-width:80%}.el-select__tags{overflow:hidden}.expl,.expl>p{line-height:16px}.icon-key-input{width:40%;margin-right:4px}.icon-minus-button{width:28px;height:28px;margin-top:4px}.icon-values-container{margin:0 7px 7px 0}.icon-value-input{width:60%;margin-left:4px}.icons-button-container{line-height:24px}.line{margin-bottom:10px}.mascot-container{margin-bottom:5px}.name-input{width:40%;margin-right:5px}p.expl{line-height:20px}.pattern-input{width:40%;margin-right:4px}.relays-container{margin:0 10px}.replacement-input{width:60%;margin-left:4px;margin-right:5px}.value-input{width:60%;margin-left:5px;margin-right:8px}}@media only screen and (max-width:801px) and (min-width:481px){.delete-setting-button{margin:4px 0 0 10px;height:28px}.delete-setting-button-container{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto}.description>p{line-height:18px;margin:0 15px 10px 0}.icon-minus-button{width:28px;height:28px;margin-top:4px}.input{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto}.input-container .el-form-item__label span{margin-left:10px}.input-row,.nav-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.nav-container{height:36px;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:15px 30px 15px 15px}.rate-limit-label-container{width:250px}.settings-delete-button{float:right}} \ No newline at end of file diff --git a/priv/static/adminfe/chunk-87b3.3c6ede9c.css b/priv/static/adminfe/chunk-87b3.3c6ede9c.css new file mode 100644 index 000000000..f0e6bf4ee --- /dev/null +++ b/priv/static/adminfe/chunk-87b3.3c6ede9c.css @@ -0,0 +1 @@ +a{text-decoration:underline}.center-label label{text-align:center}.center-label label span{float:left}.code{background-color:rgba(173,190,214,.48);border-radius:3px;font-family:monospace;padding:0 3px}.delete-setting-button{margin-left:5px}.description>p{font-size:14px;color:#606266;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei;font-weight:700;line-height:20px;margin:0 0 14px}.description>p code{display:inline;padding:2px 3px;font-size:14px}.description-container{overflow-wrap:break-word;margin-bottom:0}.divider{margin:0 0 18px}.divider.thick-line{height:2px}.editable-keyword-container{width:100%}.el-form-item .rate-limit{margin-right:0}.el-input-group__prepend{padding-left:10px;padding-right:10px}.esshd-list{margin:0}.expl,.expl>p{color:#666;font-size:13px;line-height:22px;margin:5px 0 0;overflow-wrap:break-word;overflow:hidden;text-overflow:ellipsis}.expl>p code,.expl code{display:inline;line-height:22px;font-size:13px;padding:2px 3px}.follow-relay{width:350px;margin-right:7px}.form-container{margin-bottom:80px}.grouped-settings-header{margin:0 0 14px}.highlight{background-color:#e6e6e6}.icons-button-container{width:100%;margin-bottom:10px}.icons-button-desc{font-size:14px;color:#606266;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei;margin-left:5px}.icon-container{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:95%}.icon-values-container{display:-webkit-box;display:-ms-flexbox;display:flex;margin:0 10px 10px 0}.icon-key-input{width:30%;margin-right:8px}.icon-minus-button{width:36px;height:36px}.icon-value-input{width:70%;margin-left:8px}.icons-container,.input-container{display:-webkit-box;display:-ms-flexbox;display:flex}.input-container{-webkit-box-align:start;-ms-flex-align:start;align-items:start;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.input-container .el-form-item{margin-right:30px;width:100%}.input-container .el-select,.keyword-container{width:100%}label{overflow:hidden;text-overflow:ellipsis}.label-font{font-size:14px;color:#606266;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei;font-weight:700}.limit-button-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.limit-expl{margin-left:10px}.limit-input{width:47%;margin:0 0 5px 1%}.line{width:100%;height:0;border:1px solid #eee;margin-bottom:18px}.mascot{margin-bottom:15px}.mascot-container{width:100%}.mascot-input{margin-bottom:7px}.mascot-name-container{display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:7px}.mascot-name-input{margin-right:10px}.multiple-select-container{width:100%}.name-input{width:30%;margin-right:8px}.pattern-input{width:20%;margin-right:8px}.proxy-url-input{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-bottom:10px;width:100%}.proxy-url-host-input{width:35%;margin-right:8px}.proxy-url-value-input{width:35%;margin-left:8px;margin-right:10px}.prune-options{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.prune-options .el-radio{margin-top:11px}.rate-limit .el-form-item__content{width:100%;display:-webkit-box;display:-ms-flexbox;display:flex}.rate-limit-container,.rate-limit-content{width:100%}.rate-limit-label{float:right}.rate-limit-label-container{font-size:14px;color:#606266;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei;font-weight:700;height:36px;width:100%;margin-right:10px}.relays-container{margin:0 15px}.replacement-input{width:80%;margin-left:8px;margin-right:10px}.scale-input{width:47%;margin:0 1% 5px 0}.setting-input{display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:10px}.settings-container{max-width:1824px;margin:auto}.settings-container .el-tabs{margin-top:20px}.settings-delete-button{margin-left:5px}.settings-docs-button{width:163px;text-align:left;padding:10px}.settings-header{margin:0}.header-sidebar-opened{max-width:1585px}.header-sidebar-closed{max-width:1728px}.settings-header-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:22px 30px 15px 15px}.settings-reboot-button{width:145px;text-align:left;padding:10px;margin-right:5px}.single-input{margin-right:10px}.socks5-checkbox{font-size:14px;color:#606266;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei;font-weight:700;margin-left:10px}.socks5-checkbox-container{width:40%;height:36px;margin-right:5px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.ssl-tls-opts{margin:36px 0 0}.submit-button{float:right;margin:0 30px 22px 0}.submit-button-container{width:100%;position:fixed;bottom:0;right:0;z-index:10000}.switch-input{height:36px}.text{line-height:20px;margin-right:15px}.upload-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.value-input{width:70%;margin-left:8px;margin-right:10px}@media only screen and (min-width:1824px){.sidebar-closed{max-width:1586px}.sidebar-opened{max-width:1442px}.submit-button-container{width:100%;max-width:inherit;margin-left:auto;margin-right:auto;right:auto}}@media only screen and (max-width:480px){.crontab,.crontab label{width:100%}.delete-setting-button{margin:4px 0 0 5px;height:28px}.delete-setting-button-container{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto}.description>p{line-height:18px;margin:0 5px 7px 0}.description>p code{display:inline;line-height:18px;padding:2px 3px;font-size:14px}.divider{margin:0 0 10px}.divider .thick-line{height:2px}.follow-relay{width:70%;margin-right:5px}.follow-relay input{width:100%}.follow-relay-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.input{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto}.input-container{width:100%}.input-container .el-form-item:first-child{margin:0;padding:0 15px 10px 0}.input-container .el-form-item.crontab-container:first-child{margin:0;padding:0}.input-container .el-form-item:first-child .mascot-form-item,.input-container .el-form-item:first-child .rate-limit{padding:0}.input-container .settings-delete-button{margin-top:4px;float:right}.input-row{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.limit-input{width:45%}.proxy-url-input{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;margin-bottom:0}.proxy-url-host-input{width:100%;margin-bottom:5px}.proxy-url-value-input{width:100%;margin-left:0}.prune-options{height:80px}.prune-options,.rate-limit .el-form-item__content{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.rate-limit-label{float:left}.scale-input{width:45%}.setting-label{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.settings-header{width:-webkit-fit-content;width:-moz-fit-content;width:fit-content;display:inline-block;margin:0}.settings-header-container{margin:10px 15px 15px}.nav-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:15px}.settings-menu{width:163px}.socks5-checkbox-container{width:100%}.submit-button{margin:0 15px 22px 0}.el-input__inner{padding:0 5px}.el-form-item__label:not(.no-top-margin){padding-left:3px;padding-right:10px;line-height:22px;margin-top:7px}.el-message{min-width:80%}.el-select__tags{overflow:hidden}.expl,.expl>p{line-height:16px}.icon-key-input{width:40%;margin-right:4px}.icon-minus-button{width:28px;height:28px;margin-top:4px}.icon-values-container{margin:0 7px 7px 0}.icon-value-input{width:60%;margin-left:4px}.icons-button-container{line-height:24px}.line{margin-bottom:10px}.mascot-container{margin-bottom:5px}.name-input{width:40%;margin-right:5px}p.expl{line-height:20px}.pattern-input{width:40%;margin-right:4px}.relays-container{margin:0 10px}.replacement-input{width:60%;margin-left:4px;margin-right:5px}.value-input{width:60%;margin-left:5px;margin-right:8px}}@media only screen and (max-width:818px) and (min-width:481px){.delete-setting-button{margin:4px 0 0 10px;height:28px}.delete-setting-button-container{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto}.description>p{line-height:18px;margin:0 15px 10px 0}.icon-minus-button{width:28px;height:28px;margin-top:4px}.input{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto}.input-container .el-form-item__label span{margin-left:10px}.input-row,.nav-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.nav-container{height:36px;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:15px 30px 15px 15px}.rate-limit-label-container{width:250px}.settings-delete-button{float:right}} \ No newline at end of file diff --git a/priv/static/adminfe/chunk-e5cf.cba3ae06.css b/priv/static/adminfe/chunk-88c9.184084df.css similarity index 92% rename from priv/static/adminfe/chunk-e5cf.cba3ae06.css rename to priv/static/adminfe/chunk-88c9.184084df.css index a74b42d14..f3299f33b 100644 --- a/priv/static/adminfe/chunk-e5cf.cba3ae06.css +++ b/priv/static/adminfe/chunk-88c9.184084df.css @@ -1 +1 @@ -a{text-decoration:underline}.note-header{-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline;height:40px}.note-actor{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.note-actor-name{margin:0;height:22px}.note-avatar-img{width:15px;height:15px;margin-right:5px}.note-body{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.note-card{margin-bottom:15px}.note-content{font-size:15px}.note-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}@media only screen and (max-width:480px){.el-card__header{padding:10px 17px}.note-header{height:80px}.note-actor-container{margin-bottom:5px}.note-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}}.status-card{margin-bottom:10px}.status-card .account{text-decoration:underline;line-height:26px;font-size:13px}.status-card .image{width:20%}.status-card .image img{width:100%}.status-card .show-more-button{margin-left:5px}.status-card .status-account{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.status-card .status-avatar-img{display:inline-block;width:15px;height:15px;margin-right:5px}.status-card .status-account-name{display:inline-block;margin:0;height:22px}.status-card .status-body{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.status-card .status-checkbox{margin-right:7px}.status-card .status-content{font-size:15px;line-height:26px}.status-card .status-deleted{font-style:italic;margin-top:3px}.status-card .status-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.status-card .status-without-content{font-style:italic}@media only screen and (max-width:480px){.el-message{min-width:80%}.el-message-box{width:80%}.status-card .el-card__header{padding:10px 17px}.status-card .el-tag{margin:3px 4px 3px 0}.status-card .status-account-container{margin-bottom:5px}.status-card .status-actions-button{margin:3px 0}.status-card .status-actions{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.status-card .status-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}}.account{text-decoration:underline}.avatar-img{vertical-align:bottom;width:15px;height:15px;margin-left:5px}.divider{margin:15px 0}.el-card__body{padding:17px}.el-card__header{background-color:#fafafa;padding:10px 20px}.el-collapse{border-bottom:none}.el-collapse-item__header{height:46px;font-size:14px}.el-collapse-item__content{padding-bottom:7px}.el-icon-arrow-right{margin-right:6px}.el-icon-close{padding:10px 5px 10px 10px;cursor:pointer}h4{margin:0;height:17px}.report .header-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline;height:40px}.id{color:grey;margin-top:6px}.line{width:100%;height:0;border:.5px solid #ebeef5;margin:15px 0}.new-note p{font-size:14px;font-weight:500;height:17px;margin:13px 0 7px}.note{-webkit-box-shadow:0 2px 5px 0 rgba(0,0,0,.1);box-shadow:0 2px 5px 0 rgba(0,0,0,.1);margin-bottom:10px}.no-notes{font-style:italic;color:grey}.report-row-key{font-weight:500;font-size:14px}.report-title{margin:0}.report-note-form{margin:15px 0 0}.report-post-note{margin:5px 0 0;text-align:right}.reports-pagination{margin:25px 0;text-align:center}.reports-timeline{margin:30px 45px 45px 19px;padding:0}.statuses{margin-top:15px}.submit-button{display:block;margin:7px 0 17px auto}.timestamp{margin:0;font-style:italic;color:grey}@media only screen and (max-width:480px){.report .header-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;height:auto}.report .id{margin:6px 0 0}.report .report-actions-button,.report .report-tag{margin:3px 0 6px}.report .title-container{margin-bottom:7px}.reports-timeline{margin:20px 10px}.reports-timeline .el-timeline-item__wrapper{padding-left:20px}}.select-field[data-v-ecc36f5a]{width:350px}@media only screen and (max-width:480px){.select-field[data-v-ecc36f5a]{width:100%;margin-bottom:5px}}@media only screen and (max-width:801px) and (min-width:481px){.select-field[data-v-ecc36f5a]{width:50%}}.reports-container .reports-filter-container[data-v-34fb34a2]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;margin:22px 15px;padding-bottom:0}.reports-container h1[data-v-34fb34a2]{margin:22px 0 0 15px}.reports-container .no-reports-message[data-v-34fb34a2]{color:grey;margin-left:19px}.reports-container .report-count[data-v-34fb34a2]{color:grey;font-size:28px}@media only screen and (max-width:480px){.reports-container h1[data-v-34fb34a2]{margin:7px 10px 15px}.reports-container .reports-filter-container[data-v-34fb34a2]{margin:0 10px}} \ No newline at end of file +a{text-decoration:underline}.note-header{-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline;height:40px}.note-actor{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.note-actor-name{margin:0;height:22px}.note-avatar-img{width:15px;height:15px;margin-right:5px}.note-body{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.note-card{margin-bottom:15px}.note-content{font-size:15px}.note-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}@media only screen and (max-width:480px){.el-card__header{padding:10px 17px}.note-header{height:80px}.note-actor-container{margin-bottom:5px}.note-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}}.status-card{margin-bottom:10px}.status-card .account{text-decoration:underline;line-height:26px;font-size:13px}.status-card .image{width:20%}.status-card .image img{width:100%}.status-card .show-more-button{margin-left:5px}.status-card .status-account{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.status-card .status-avatar-img{display:inline-block;width:15px;height:15px;margin-right:5px}.status-card .status-account-name{display:inline-block;margin:0;height:22px}.status-card .status-body{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.status-card .status-checkbox{margin-right:7px}.status-card .status-content{font-size:15px;line-height:26px}.status-card .status-deleted{font-style:italic;margin-top:3px}.status-card .status-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.status-card .status-without-content{font-style:italic}@media only screen and (max-width:480px){.el-message{min-width:80%}.el-message-box{width:80%}.status-card .el-card__header{padding:10px 17px}.status-card .el-tag{margin:3px 4px 3px 0}.status-card .status-account-container{margin-bottom:5px}.status-card .status-actions-button{margin:3px 0}.status-card .status-actions{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.status-card .status-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}}.account{text-decoration:underline}.avatar-img{vertical-align:bottom;width:15px;height:15px;margin-left:5px}.divider{margin:15px 0}.el-card__body{padding:17px}.el-card__header{background-color:#fafafa;padding:10px 20px}.el-collapse{border-bottom:none}.el-collapse-item__header{height:46px;font-size:14px}.el-collapse-item__content{padding-bottom:7px}.el-icon-arrow-right{margin-right:6px}.el-icon-close{padding:10px 5px 10px 10px;cursor:pointer}h4{margin:0;height:17px}.report .header-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline;height:40px}.id{color:grey;margin-top:6px}.line{width:100%;height:0;border:.5px solid #ebeef5;margin:15px 0}.new-note p{font-size:14px;font-weight:500;height:17px;margin:13px 0 7px}.note{-webkit-box-shadow:0 2px 5px 0 rgba(0,0,0,.1);box-shadow:0 2px 5px 0 rgba(0,0,0,.1);margin-bottom:10px}.no-notes{font-style:italic;color:grey}.report-row-key{font-weight:500;font-size:14px}.report-title{margin:0}.report-note-form{margin:15px 0 0}.report-post-note{margin:5px 0 0;text-align:right}.reports-pagination{margin:25px 0;text-align:center}.reports-timeline{margin:30px 45px 45px 19px;padding:0}.statuses{margin-top:15px}.submit-button{display:block;margin:7px 0 17px auto}.timestamp{margin:0;font-style:italic;color:grey}@media only screen and (max-width:480px){.report .header-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;height:auto}.report .id{margin:6px 0 0}.report .report-actions-button,.report .report-tag{margin:3px 0 6px}.report .title-container{margin-bottom:7px}.reports-timeline{margin:20px 10px}.reports-timeline .el-timeline-item__wrapper{padding-left:20px}}.select-field[data-v-ecc36f5a]{width:350px}@media only screen and (max-width:480px){.select-field[data-v-ecc36f5a]{width:100%;margin-bottom:5px}}@media only screen and (max-width:801px) and (min-width:481px){.select-field[data-v-ecc36f5a]{width:50%}}.reports-container .reports-filter-container[data-v-0a3cd0a0]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;margin:22px 15px;padding-bottom:0}.reports-container h1[data-v-0a3cd0a0]{margin:10px 0 0 15px}.reports-container .no-reports-message[data-v-0a3cd0a0]{color:grey;margin-left:19px}.reports-container .report-count[data-v-0a3cd0a0]{color:grey;font-size:28px}@media only screen and (max-width:480px){.reports-container h1[data-v-0a3cd0a0]{margin:7px 10px 15px}.reports-container .reports-filter-container[data-v-0a3cd0a0]{margin:0 10px}} \ No newline at end of file diff --git a/priv/static/adminfe/chunk-cf57.4d39576f.css b/priv/static/adminfe/chunk-cf57.26596375.css similarity index 74% rename from priv/static/adminfe/chunk-cf57.4d39576f.css rename to priv/static/adminfe/chunk-cf57.26596375.css index 1190aca24..9f72b88c1 100644 --- a/priv/static/adminfe/chunk-cf57.4d39576f.css +++ b/priv/static/adminfe/chunk-cf57.26596375.css @@ -1 +1 @@ -.actions-button[data-v-3850612b]{text-align:left;width:350px;padding:10px}.actions-button-container[data-v-3850612b]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.el-dropdown[data-v-3850612b]{float:right}.el-icon-edit[data-v-3850612b]{margin-right:5px}.tag-container[data-v-3850612b]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.tag-text[data-v-3850612b]{padding-right:20px}.no-hover[data-v-3850612b]:hover{color:#606266;background-color:#fff;cursor:auto}.status-card{margin-bottom:10px}.status-card .account{text-decoration:underline;line-height:26px;font-size:13px}.status-card .image{width:20%}.status-card .image img{width:100%}.status-card .show-more-button{margin-left:5px}.status-card .status-account{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.status-card .status-avatar-img{display:inline-block;width:15px;height:15px;margin-right:5px}.status-card .status-account-name{display:inline-block;margin:0;height:22px}.status-card .status-body{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.status-card .status-checkbox{margin-right:7px}.status-card .status-content{font-size:15px;line-height:26px}.status-card .status-deleted{font-style:italic;margin-top:3px}.status-card .status-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.status-card .status-without-content{font-style:italic}@media only screen and (max-width:480px){.el-message{min-width:80%}.el-message-box{width:80%}.status-card .el-card__header{padding:10px 17px}.status-card .el-tag{margin:3px 4px 3px 0}.status-card .status-account-container{margin-bottom:5px}.status-card .status-actions-button{margin:3px 0}.status-card .status-actions{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.status-card .status-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}}.statuses-container{padding:0 15px}.statuses-container .status-container{margin:0 0 10px}.checkbox-container{margin-bottom:15px}.filter-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:22px 0 15px}.select-instance{width:350px}.statuses-pagination{padding:15px 0;text-align:center}h1{margin:22px 0 0}@media only screen and (max-width:480px){.checkbox-container{margin-bottom:10px}.filter-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin:10px 0}.select-field{width:100%;margin-bottom:5px}.select-instance{width:100%}} \ No newline at end of file +.actions-button[data-v-3850612b]{text-align:left;width:350px;padding:10px}.actions-button-container[data-v-3850612b]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.el-dropdown[data-v-3850612b]{float:right}.el-icon-edit[data-v-3850612b]{margin-right:5px}.tag-container[data-v-3850612b]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.tag-text[data-v-3850612b]{padding-right:20px}.no-hover[data-v-3850612b]:hover{color:#606266;background-color:#fff;cursor:auto}.status-card{margin-bottom:10px}.status-card .account{text-decoration:underline;line-height:26px;font-size:13px}.status-card .image{width:20%}.status-card .image img{width:100%}.status-card .show-more-button{margin-left:5px}.status-card .status-account{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.status-card .status-avatar-img{display:inline-block;width:15px;height:15px;margin-right:5px}.status-card .status-account-name{display:inline-block;margin:0;height:22px}.status-card .status-body{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.status-card .status-checkbox{margin-right:7px}.status-card .status-content{font-size:15px;line-height:26px}.status-card .status-deleted{font-style:italic;margin-top:3px}.status-card .status-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.status-card .status-without-content{font-style:italic}@media only screen and (max-width:480px){.el-message{min-width:80%}.el-message-box{width:80%}.status-card .el-card__header{padding:10px 17px}.status-card .el-tag{margin:3px 4px 3px 0}.status-card .status-account-container{margin-bottom:5px}.status-card .status-actions-button{margin:3px 0}.status-card .status-actions{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.status-card .status-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}}.statuses-container{padding:0 15px}.statuses-container h1{margin:10px 0 15px}.statuses-container .status-container{margin:0 0 10px}.checkbox-container{margin-bottom:15px}.filter-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:22px 0 15px}.select-instance{width:350px}.statuses-pagination{padding:15px 0;text-align:center}@media only screen and (max-width:480px){.checkbox-container{margin-bottom:10px}.filter-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin:10px 0}.select-field{width:100%;margin-bottom:5px}.select-instance{width:100%}} \ No newline at end of file diff --git a/priv/static/adminfe/index.html b/priv/static/adminfe/index.html index 717b0f32d..3651c1cf0 100644 --- a/priv/static/adminfe/index.html +++ b/priv/static/adminfe/index.html @@ -1 +1 @@ -Admin FE
\ No newline at end of file +Admin FE
\ No newline at end of file diff --git a/priv/static/adminfe/static/js/app.d2c3c6b3.js b/priv/static/adminfe/static/js/app.d2c3c6b3.js deleted file mode 100644 index c527207ddf4fb46c151e1e7160107d1e89753585..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 181998 zcmeFa+ix4$zUTL^u!%+=>6oNgypwLV$CmAucgvTy)b8Ew?J;PoWU@_>GD%6c)dJ=v z!3;2i06~BOG6C|EJmeujo(4hwo}2^ZzsTqNTWeJnFOuxGTj!ja*}W~2RjbzJcfYMw zYo~+J{`jNMv z9`$B}@n|hB%nGCRv;6Ti&P`{NL2s77IVw%oX6rXkx|3XYbF`MP-HCt6uNNkpqta-t zyIz>;;)Cw}85hTPak^d@Y>wj7+@#c9I~(kG@`K)ZR6N>0$QS52{aHMoc3;Q&!pR^$ zy)%B-$(M8GTvV>q>`%VHQ?qz9>-^of)Axt3#>1R%xVg|U_wH~wnr`O%v)NH+W8?G` za8yg<$?J_uxm@0up1jWI2K$@oG`{=xs5|TDxb(2qXqU>(YOd03ma66EcB9@bMN#yu zQ7=_%wP7WSO6^7^=RT_+4vUp)t5m6SrBSLhL$hM7*(lZ8tzxNMubtGQMyXwI_M1_y z)U2Kqqh_nrpzCV6)T%}FQLWZXt!g7zZIny(X0KRh3dL%v+^7|!a;aUbaV4r&d9c)o ziVRfN-$t_-xi-~Oqs-k>qgbKKNZJz>NV!TusID|Zriw(9CKq@np`#4=60>vEY;|f2P)-OG2%|SnbZ9` zvu>0txf=80ZoAsFua!!v97VZuQGjN&dX+JO77weX-B$lity=2>0YIP)9Mnx1B>M1B zP-8;1sMIRg9hA$pe!E^NRqL&1{`$7H0?O5DRH{{?T-1(AQM27+3}y`kn-QH?Yv3G6 zG~0r5rQFU{+ND~g(Ob=0t_3tI#i&K=RxYaBY(ZZmYU(MFV(_W~9pGE6G)v7|GsmN> zu*NDF)_(MhjYg?lZJa>WrCObz6%Y?~m7Aq{)a*wUkk)ASn1V(v=c)n(V=z_g$GWLF z>_j#iH!F;07%MlSAx)>{W>r`7Mnki&GHsCwyac_{rB)efk(Hk# zD|J7rH89o^)d0b!a7Os||F@C#(r${?+(RaHO+bQ0AXtcs3{AU3MAZcqrA;nOmM z*^fTxsFo@X57!oO)z2PFuR(b^o~+lQd=?B4MHVe!)M9?1wgva|w_y|@P{J#XW|V`> zD#qqD0LC~CHID#&&43NM5CJtP8|+u03t$0g8lrCp1K5T2g!E&l`jm$_dy+KY4JZ=w5gmZR20-Kq{i4SPs1q!mDCGft7m+pV{)Yiv5{W(~ zhM0_47_rS%D(QL{7~nE&)5*MI1?eTR0QvJ6ywr*s9}dx4`P{~L-)=Z9_1)i{uiqS( zx~1;^{@P&u=B%_IA9Rn0v(3SIAz!}#YIJQHHy$4i(XSb$Ygbn5U3daZ3Jk&)@zj_(1uKetAK7K@diTRR{sk*Nw1JksSe~zJ1R!8Zji|BJ zl5m9F5Ik_9L=NIQ!U|}}GO@2cJ%WKiZx99ZK)ko>MO03u0w2&l;)&)^UmFcW_q2uF zDhNaWOM}7jwkA!3sa12Dr!A+xE%{upcadFs)_<6VWQ%sI=|RLuQ=~nT7!8VYYDS1f zU|4}0fLc4+Lgu$oX>=r(spguvP)B4YUp)h%x`G zuIGBpowdRd8ljzI^({D9$^k0?#%WfiBQSGJ$%$cVb%BhhW!)ej5H2ytjM9yS1igoG?T2J+sl|#U1#2~#2kiY7`{)PJ+>NDULoEJMd1EmWM5C3a`kls{dlLF5?-a|-R$mR3X#!lHbqKl^Uu zuqN;TBltBj%M`R3$QtD40b5-SSZR(+0V_)kBxM2n@iIBOae8`PGC6)97sp5OC@Yo2 zwbgR@5`U;S>`w7}O{O!&s`#f4ZxQ_f!`VR9bIwq8g}I zFJWR;=C{Vl9U3@V;#Rhm2I z5HiqPD5r{*t-d5e>lny-s!?Pf>Yaz`#Wo|?;B<6AtGTB|w%U!iYHzE=0MIJMc3LF@-IiIj2{(#v znMKx8tu%^mnZ*_=pbd3vnYvYLH;T4QJ%xPTDKm7n)GRmO+C<y$%MZq;Sfpf9S` z>RYjL1OEa4jRAoQb{9j5K zou^mG_awZv_2J1lzfROk!L3pki@nl{UtjU`7mJa_?*_Bt!SQf7?M>nZB{6?%wXX1% z_j2Uy^KJOWsb!4$nt}=lX6U8T9u^4_!R8qD7*!Y`i0vja3{JzdlH66CsEl=hbSM)N zfhp=eYgDo3NH?fO&1It#TtKy#qsGvr4TeZWI11A(k^n|vw!4rQI>!B#eXK}R4Rf^2 zHH;qnW7oNY#IH1lvSsl;d0H`smga;h%5*hVn+2Kz0|sG-sFh8E8Kp^|pl=$L9;?Ds zE;SWUK|q`BZTBr)RvohVaz(yLxk+2ZL%GbAmSmCz_VhVaK!_{#S{FHuLFWFti873w zlTx+S!$vKl++@a8$^v7#En_P}v)N>TcZEO!y#dBD5JW^pEiFP)O%>NCc(Ps0c;`i{0M@N@ki@TNMXPR^2d=JBR}*Pz1n5 zax^i51S-OdaLs+g9okBeaLY2{tpyUV5p0o%DpqVuMH>-96Q~3cMNRZmM=grOVMj%l z%?Hv5ikOS2-B%Q;xKXRE8GPgyHw-FTjvz#gdT4Mo|B*N#0EP^(92&MR%>t_9FC-w8 z!N1G`q7eG(eG?*TBBGL8L(pDijDczRHDMzeQj0{Q?Rvv_MTFvbMbPrh0$F*6f?~ij zaRMl-0vUrLOiwW%0uH+K@wA5)>5}mS4H-8x0YKo;EQ-b>$Gan)0DUn-A|Cvay$p94 zPcX5nc%x;q!x+f$q~(YP<=Y5Th9h{1MOCXqh{Z{PD!9j`XHq08Xh`f`P0UY)i48_W z(Q5-qERHu^@qo#|I^^C^{|$tLeFbAp0(OL2Kw4Baq!AGSX$q(EpNUw=dIzel$l%9M zcQP7lmFRk8LQ>*NB34*)6H>5`pq`tNWPi=een}r}brvc~*N~|!(q-u+8%(D_j?yVj zTx3BIbP0I+K){(4bi#lR1xP`haH%|Y(8k|XZd4tYeY^otebT>oT_(UNp3dU^tb6FL zT)V>e6Upd#2%-YFDo6oxWd8zKE+}V&>5CF}-@zH?fIM~_8*o~N{oQwWp8yb#6VT^z z2no@F->#WRu%@$V_ZKM~mR}GsUA{C9Qg?)7c5SYsV3$DiU`NoUaF_fGfRFsN zS3w>p&OtuAn!c?pdWN@C+csiqKEp}Jr~d?(qQ3XzkJlv;X8rhZE{bm#u3a9uX=KC! z7D|bjHM#CqZ3jSBF;YmGRJRC#G?fHHB_I;uB1PQ^p_H%(WgWm<&?Qm=xnYgqG1Qqo zjIBb1Q`Qa&mngdul~^0(m`auxCDx$EvIIz^Zlee4b+li$Q!ykXyH2^&s_qaTX;w+t zX?A$wWU>A1NCmac_9Va7#F|)`pKoSexyN@dq1{O2CKtpAF zaYUra^b-ZjoOehA2qKC>MQC2C1Grgh$P%sMrK1yx(M!7NveE|RAW-i>6m1I6VS~7D z{Eb>CcwaUA49VmwmaW1d$YES4Wq~EHk(q=Qn&d-e1_OF`2LveKWvyWI2H8^*BURWK z_|@AQ*6cSzP1523G9vPfZNaKoGMQPeOQ~|aKJGHr9Y+#G3mO>Z;EtYJ%8<$*W4wyl z-}){{Km&pib`L<3n37Xbw_BA@V8zC}lSkLZiU;G#VOBNk_q8*kO(MgGE!p6fF;VtQ z#4oWA{%a>uB95wf7h zYE4cVg(mK+I%qvB_oXWFSYa5~MTJ)Dzct$@thLtIcF6ef zkn|hkPQM}&k}3e=sT$L@Wm(3D=_>aIM+#-I0XU!&=W-~%fV0aZ%#BnYt}t+w^MNlW zcnJwjAcc(&R|8to^-2K>&ob1_S_cinGS`7bqlpV3hJ-Ci6V{@x4~_c|wH4Yd|L=gW zi4g~PA9k1fO>;c~7#tocT++simUE(Hkt#rN#uT*HBsEp+HlmYip|y~zLjZHFg>F~Y z9UmT31J6SSx{%t&Toge`1W{F#{8c^>^05q74ar9Av(@_WP&lA82_%;*h|@BYq2j}; zRq$Peefd@bJK+iZl(vKrVTJ0pJcufFD=dj%sjR++pjnfvVr4JL)h=FXy~Y#n!@*Ne z%Ope^D_mXGwB3ihPh0b;t8&VLfGR0GVy!|*-ZyaOFUDoL@{Bwg*<%yDxLwp(FcVui z2#4M(joVCB$#7rK0+v`7sSB8h_yA)!AzA<7(1fdm;{}KtwJqHJhzvJ}jknbCpL zjk$84%xa}}f+$CcqeH~BB3A*I7`ixFPFJN%dning=|f|fhHJ_pRdtovQV6S?me%a0 z`(#-H3VM^4Wyx@@kShIdl~pJj3*7-ho^7}KM3~7@o**ZP{_iMfEf)jUsWSu`LR!g~ z$aGFiiBqJOlnW(zrcub?!!30=9+CN;l72vuG(5sa#sPB_^2iH|CL17u(7yY=4S`sU zMBxv!Q3%B}v5mQGicswV67tsd<75tWt>nAO9}Q)|Atq`7WT_^?B%BPJJnB0z0Y}aU zU=baaPDMJ}_dc<@i1fMUqsT(L?xrO=WvwXvFe`iD!dc1r_5+7mN%zV#xp-D4I4_F>fcr&FpK?RIMyQQ10ERx9udSkrTGERXX42Mn((MhDasRvj->=# z$sp)~fg{9W4W>fZ_zy?B&}B;`Qo&lPQzF$=;>Ldl4l2X9m`v2?LBA|dTq&~(-ABP6 zbZO~FmMWHM43Vu$lsVbza&`zW!O9{qunnDAP@AFoUV*E`?vSwiu$eJR#jHSdIx1IU zU!g{^BXm<$R^1+bSp^bx=mXzroBSE68b^K-#Qv^n ziI(DqNM#!1KieD;M=V26uqcc2Dng7Xq7eaKQ7dx>#=4}x7{XTK0OR;;C{X|`bES;z zLgxwpRRZ0NB_bq9+>=-_jRpSc22lY0idx%Is|)IgItYXMaAe@7C}kXdf>GM8{pHnl z8KwK(Y5&!@JDCd_B^NI?Ps`;mRqRDiX|Y!_GDQSQp@1oRr51^cP&Z^$p}YohKA1wJ z*C6OCiV^B@0QDD=gz+R-iGu5j0{ z7BXs?7)qGTz8Sy@>mxa^Md=Nh4W{|7pr-PkiUg>L1z9UImhv-MovIg8VIu-rBAgt# zSvqaAmD>a9Sq9R$#U@HNM zk1I)o;}4c2xye-g)4qi^v+OS}Ah~aY;(jiVHlkCLjqu ziq})6y^=y`J^I zD}VFN$#}4zD}S|_-h5tuaVu%bk4}C#?sbR#@sxdievr6y*q!zIYa7};H@(&Q+s5BE zHr8*&nN5MogyPb4)}73zKM!X8wY0BJe*O2~=kF={1>u$=?wzm2>$k$*ynKmT)n0eT zwT@kzvSBZC@%+4?3arfNZcE~M;ojT7{;+iU&)KOs?T?R+2BX*6NN>1yu{r%G?VSh% zXRT?XBWl7oLw|FA{Iq1lK|IO?wIi(f1N+0fGV=0Nv%2VnhU$(1LFW; z!osKQu;VYb0>%o79F^9?bM({Jb}slF_mOXCf8sh8F2trHVF9DbY)I}Qbo`^zPBknS z_*z+nx>?*&yViyGScLurPqJlmYr&9ksixvT+&*ttYg5#t|^9hm@!AVfe_TgNN|t| zMzlVCCGsITFXSpNCJCmPC-HWbv#K4p+x0H)a#ePL`$)#gP=ExuV8GutF22FQP1Ohh zkehIBaGO}4@GsBWqJA0tW3Lzpn4G@{DTF75e~L)7k_G%Te+2~HM}i}P%18k4xpEWj zJYeY#^^PX|U-ys^2BRpjNN+VsdH0cYieH%^p0bt%@6_Yin&L>TjCssB%5Enx00ZWSqDdZ%VMcmy`L673@?ynmI zx-iZh^A5rrbysC=VFyap5~T|t$yIvTJaD;7yq8r?c+-U=!@d^+Gk)s}eD z&Jx&8vFFSewGkuZW?=1-*&*)Lw(Jeo5q)E26X?cYVj{>9m020*BD}DXlzU|9S_@>N z*fk2=g1wS_l00#;;l; z6PI4O3VI;A*A?dE8|klGmjOtm0M@m^4N>4el4S!*de|a-U`bHPOe|msQ)s3p&-j{D zn$!S(G#x;CEU3}mGLXa8A+e|@YUrnOA0a{D03qBosz3>gb+si!6eKngLsE!Yhjam# zrYD^YVQ2@e0+h zG%a?v?$HlK$Xzip(uKPgg+Q@u3mDzAT6;iJOcFp5f@+_rGp6##o4WU_!w1)7oV_}p zv&;B-?IxL~kcn-|*oBBxLnKEZuxih=ECPVu&}jz-b}SLgLFq6AQG`qlojV{!0J+KJ z(pR>zsyh{QAW4xhV2N4@!?o&kRR%&=wfYd>L_4CnLoJ@NW4>c)01g*m@Cr_n`_23NQH?b$Z>QjAz=%yHn8Z!J&%I5JQ_)Elbet|(hEaJ z3)WUBF|lAy+)Q&6MWdT|40}hzDr;lbOu6yQ+|Jsz`e>hkI?8MnV`(BbR5GVxTBTeh z=S;szX3{6p4ok%4bF}V;HeE3TSrJITF2S27eC|KOyithE(+;5kRJhU11~tlX>VpWk zqIMRauJ6clLvrba3P1|43#*cdCK8=yM-#A0Q__@A@N~;6sq`ICq7Q_ln&h`YOy>sZ zstND1W$YDM0BM>-1Q4|l%WtXDKr$OtBQ}giDTEh-EY8|&Q}#`+Pq9VB6)8y|XYn~Q z`jV8aN5p9{MhIN9tZZW#;KDv}t`h1|KC52erB61zqLm1j*;n9IRV>j+R?Ngy znIuzDZe7eIL8{g8*D{TiMXM{RWAu=|_8ludBP!aiws(Ljl3&~QtL6ZNhmC+ixEMzK z<_H^ZWwK#-L$DuSM0fe{)?It=cAasEaUzT{Kp{ zY+0NgQnXV!pr>YC22rghpGrY>daucbLnPq7 zC``o!X@Dk1B>0LTnvue!HlU0mws28P2i|~UmIwI=*U%FaQl+@M;b;w6XfOy79ca-y zHd9+sBL=Ywi$?q76<_{-`^MgtF`h4Ug3^HFkS3$9j%w--v%!hE8`rK5=SI&eMiR&s z+>WS>kU&&G1>zh^Y!H$zv{@6;BW-b#wZ$CYN#b9eNHrmUwIu^NWD=OYI@pa^&?j;& zd(e)oG?%KuU`6X79B9mU;alDfoWZE7LZwEC7=>mas}Sb|3ZNWph;Efg@jiJ&wN;22 zJ|w!$$b-mumLqzQM~EjV2%=I~>_dZ?bixq)rLT3go85C?r9}KyEW&2JdV+~aJrN|! zBlb7w2P;g20CBLW)YQ%p|KS<{8uqw|PRkT=BF+^hN7V(Hp|VdT71!!jZIxgLt^n{s zXD>*a;*`PYIB4#}I<<2aRQo_DSbYY!@Sz6uCwEb*NEnevrbRn{41oiNEI(GWs6gG! zd^@KCH7J$SAYdY6MWr`30*VZGh3SAM|KV1L&xQp`;jNrEW-i@A>`=vTsslhjYGbjf zGeZ<|!~!?SO@Tyrja;N9siHzMGfxhdFmskWOkXOY+3F%2bjS#Q+{p68BswedkX8GN zMsXkEe%s2|;jePZ{eR21 z(kRK-M(yLp>HWKRhu7psynUOMpl{z^yVzs`Str8OpEk*30ckgra>AT7r3j2nbGhi+ zD1+=dF~P{zM%iTIQF+C<=1`=pRuVoii|ji&cRGp-o>QDe#x~Y8;t@iff^?vZ$13739T;_?{KCG2#@eQ+2NYI!9q-FGLeHLNS9uOJ@PMsi>BNd}EP2&$;~W5J%LMF=S^P&WhvU_Z5R z#aHZ#(|15_ZUsh*kYK$jrB52)6deHI3vqaXTX#8GP}q}~*HG+CwvT)}T{W*nIb}H> zT-BL14AI;Hxdd2r}JEEzjI-Y1}2>3maxQMZ3zL+ueI`Yt%=s!TGGgruR}vEf`R5sDRLV+^27?GXl0 z-76jI1O|(?V@awr)&*6_z_dk>I%QgFi3jPwaU_yQyt-D&ofd#OP7u=ICI^yUET@8d z4*(!Ai&tZW80B)U2DEi<5hj3wUHo8DRE`5$5iBi4>iCx;c>E682U;GpP^_0nux@N< zpu|B4T#~h}TZ9t@C#E5#$)anG6Ohu9hUX_S|C5sapfgoE0R^5C35nZHkzy{@DH)7;Y2;W%?H5VCNrqGXB48PNe;0jcy(J(G$ z`FfmC_N^XeCCQuyX=bNtcec_FpaZX%uf~k7yqO3tmsMJ&*7~Xx6gCBOXa1B65Xjez zjP1n(Y{4}Nu0S;W+k_L8U6-k5RB7XgCpdFTfQFfw5#Ina3X5-qA&^+`hnPoYOyU(3 z4#xptS%Wm_5}G%DWJ#=9A_`WJ+Tc^1VqlOzQH3`+y%nHv8?zd$2;YJZb)-OuFA7eG z0LY9W*Yt(nA#g_;LZ66KkYRMH76l6}*?_|O5anDVPNr${4Od6@C-9-Ykq#$DLg3O` zY;ZRs;CYs#;nkBSA}~sfGA_^|cur{tUnqR9M1AbPdX*D3U{0Z3REM|*ZxMKaL8OB1 zLH18Eq|8q{{J`y17tS;xo+e?WK(M|FgAA>v*q_EEY9KhMDKea_%w4KB<0$`s0wc}p2#YNd8=yQ6<9>@ArzuGusg|;l>u3z)M-gq_t% z0jZU^(;{INlvmr1}`I39kzDhReY@B=&B z_?An2751wZEwaix!vQWKYc!%3gI5f!b;rflpFmPGS=^@DA)gF_4Z_s9BIRp2MN{??I%a>c14{m?IEWG8 zPfjbLnxC6`MyHSP6<%fqDzOBjd@-&PV(1K%1wsD=#p%LX7pV~3WoR`5B zyG)*lu1xZ2`VIIFVzl0QMk=6gwf6L`tyNqTG1SaL#WLnLzeRZhteKR;I0ry*v4U$1 z9b1f5GqYV%M0_Q*BU$%lVF4>e7bK=s(~8Z_A0&Y6L=+y26*v&OqE&8r6mcX6D5s68 z1DJrC>i`?^lx70rG$q_VVUjI+xS0APk64W9cv8#45>eQk`xe*`f!R`|e>{O>>^kiN ziy~T>Z-sLw_=|(#WQ~%8wh0ZR93gJMK!E0;;#jDMbyZBEnLc&{Ydu`I2{~^=iF1%A zToT~{9<4egAURcqI8knbD-$&y#X{l-SENatVB;83bUK%u^3Y8=Bl!Jx4ZDCKJr4jE zOGC1D*c0LvO&W(Q#3GAE7tP|;raV1ihMMJWxDJriw~F!{t1~TAmpV9Bc%+R3&=#xp`z*m-?eh7ySr`&HZKgS6F2o47Fy=RTfsR9aMTa89(S$Hga-y$aNt zLrJoiNaGJhFN&`4SEmPR8@c-+n4@d_t@)D!(jLrAraHk)8nmU&%TI5c)ggfqCO4J!9OW$`O(spD zZ}?s3FCm}h!lM>l6UF=twB-s#o-e zO=vr94IjX!c?OX(x5(C1N1UAJ>L`Es@ zBk7C}0KFrRId+9}`S7bX8veqN)05}{F~>UCWJkWr{noO|jgtx{HRq^*JVF2Em%AUX ziLi#<(d%P-+eCUAcDS~hwB+FaNj05e;A{Yp_?w}h*a}r$RpJ`hXdtGp01I$cteZHw z!U;sfQR2jR)kgbBxP^`)P>(|6eqZNK)>~{rK)EU7T`Ox}E$I#s5sefb)E9!ZOR`U9 zH#zWagdkOmTQU@tP_qmKKU^lFanj}hEybLf0me76chMMTF5m2g9x^77Fr)>4+70FP z!q=$5mS<9ZJWmK+UKkdNZ^nu04=GeZNwXrj$nr)O6?zn>9PK4t1En31wks23x-{s@O=vX}|c~T|bC} zye(TD2$OE~+J}cy=sISCl4l7=>^FK4y{W=c4;phIrR8rE-4Z@wIYP5YLlDBK3R~Dw zd%}&*t5+|YzNcD|hHGccxTRnQ{H2o?wU3q;nlOExGm90<@iBddg>hx)R{F-QqAKBD zi6ce)H3vr>El!Qos9@CrN5e{NE5W9H3X%|zX&(on&2cckZ&kDKSk7@697;q%b5k)) zMTf=cF^Lea>0o#9vcW+YbY!3iAomHmn;&|I)g@tmUijg+N^R-#pHsJg#A(>4kJxo(P6)~$JAF7~Zin}1Gs&=GO zRKY40;lA5g2m*9^gZDE59BsH|3i#QG1ghzjd^PE4fV1xg&n@n9zM-B(lfkN0a^ifW zz=S@YDSb)@oCZo~bfXd;y3v98fWf}^h3FpoR?$+P)Bp z+Ky?~826X1pPT$tGKRQP{W85}Dq}s{nS@rv#*tDGAGnf`MR42qZN77!f)X>T@UuJo zQuxq)w^m!=heIG#5{tal3ycunWHE?f>8x`FzA0o=D8x1@VgV~oi?$#wB74~V;^jqb zra7S-$L2x1vi#8ttC7q}yND^4-(~aIqg*4KM zDs;y_BdG(tZTFqp6_QtQJX*;47|LR*q=f(;1M+StJKLUlat+r7P8CGaa0;WV4lMl8 z^puj;vvvRuP!~K|5Jswed(spvRz|D)K_~b;e%}u|N{p_!m8JjM1X*A@!ip+6k_@6) z)PHALBv%Za;(ZTJ+9@@{@)X1;K%WEi210y$JcBOl_LUu3igX&^?Ej}I;S7(6g;dxWWS2gAZK@c0U=5u z9lR1xSBP2?g3X^+ec5Vo95ZB1B~2f)D@Qwc_<9gmy-fIsIaE0=2{qNwo32np7H6_) z>npZYl`yZn;Z|pBu713LfB5j>U#^Kv;&^+VJaD7A!k-W8mc-0U?ah( zwc{)+DOKDCg~bthNNX3XQymylJ!Ro|QJS@`bfFJrKK+L)esbjrszGZvW#-71N5<&q zGalXYJu5G%7@+;N!MHg$nM=BN-F-L|p-5~p=SIU#D(WF#$QN)2vVu6YZIUrnS)jlr zVR8GYY-=Zq-dm`a2uwr|*$;^=W$uxil8k^zHSzk02-g53emXD^64m}NJ!>NhWHyKm z>BsUP&%#)K6no4Lhh`8rq#Fyf(}w&6awZbFM9$}hhvPSQmM;G}ZozQCe&<3a?8EPC zr*E4?hPcrk5=TfC+v=gL4R|tjB_U$|m|}zW9sOx0JKh6{9~t{PcGJ%3v-`YpL(e1E zlnAqD6u6+R`>y9zB5K!=klG;OzT15n#?tvH`leo=$<}|lquR!&&CUi z;UN>`;8AWU_@pJ<0Uip00tV!`)?}uIe)vUuKwSS$Tn)C$GfMqSs& z-CcMGS$5zwr4r_v|L$*-zm2|qZ6AM|C12*g@*_#AvH7DE6|~`z!Wd;)VK%uX_7BGi zKR_ExdC3BTOlm43OsD)@wV+R&uyz4)hR@AWPH?;ZQ9#hz*nRt<7KO4V_qUn*&yEd^ z3f85K28}3gZQmJDBR^CBTU9)`c7aO;RlU^<V$rm=i!!i(Y)Q-3R<95)Tp2I zM`W|n+&qlc)3&WvQ#5>NO)A~eXB+caC-U|9j*_#58tjuleOs>etG;ax%u+2gke;81 z?tkP;v~;(Ak9vc#XaHmgwHX4@@LHn4pzm4{!x%#5CUEg$++uC1U>j^2=Io395aJc7 z(s|KA&<`V;Nr5g(bu;9V%v7^;7^Xz)X?B^eQ_>fSYA6k?)d_g4&cj}aSvA{dMyT0Q zx8Pz!E`ZVNXgtC*%=jVqkQ!(~mlDg$HWf^YkU;0!k*3jRh6 znwLhj^)8L5Cak_)5#%#=J0O77U)EB-bg+bVMyFqDg3x22C4_)1Q=XHm=I9D?5jP zwhG6_L;b8VMK1gJs>YSJHvMog(PWG?Y!;eX0#lRrVrGW|%_~6$m?|~#$j$*N zrA};JqE?4=z@~sLoztByOfyL6#kf`UDTH~t6yAmuj0rns5iMTx62LCYH?8W-RAeFF8oKF#FxCm~`}1p|X{rK;>qM-PG9B zs3}Y`VkiZYR#3lvwZGFkFin5D?^CEM5xLGfjS)D0kO%qo5mh&Hn6CGe{p!?or1B z>vAjWWVq}WUJ}uNZI)5PMqeR5ic@VvC+VymG6c+6b>bC*g|KXeqd^cmgyreY9z`RXzQy3+v>>M6|n!=Yl8w8R(H zak7ecD}zSdk}(pV}e?;M9EBp&X6<#6~IWwrb3 z&%a$BXh}+7|9V)R&6Tn49Xva}{Mj#7#>#utvk_;#R{hxNJ+G8Hc}qrEJ;E2#ZjD;z z5Oz%Y7aS;ZwhJ>Uc^WyO~4*4 zmH*>O>(Cp7r1A^Okx@NG{+Vp6`(Z7Rkakh&VBbO;kocvIR;qZyq$6=oO2W^`MzL)J zTWkkVeX6}bwo1;A#Dnebq@s!{T58<%{#-UWAgM_RVM(K$F$PI5GV98Y1!sw7yNW%x zUr}Q*Do?OvR-t6Mwr$Z7e5^Z?E7~%vT1@*srJeSr@D3Q%DluY{JVV*#3Stu1X;m@X znSrzb9WZ0ggwQ@jvl(>ooy-V!O&BnBUQ><3BdOyh0RSLWTMG;cFjH@3Log{`sn{2q zu=*7sR*7AUS=iVNq*p#*s<(Dn1?f*h%T=X61#6%{zyUoiq*(%igMj=Gbt01juxVd1 zOJZg^C&=0kllc+UG3YWqh=jlH_(t*p8k)JV%@df>lcqRl)iUh zaj>)RtC0rbLk27sGT<3~SCRm_;+qJ8`dDay3<@};KnU}Sj9VbA7W<%>pB=BRX@*}P zA0^&9=q{JS@rLoA4mQTiHh>dkXZg|wmF1=B2`?efkAIrH%NMxB5oG?7j$X4%y*_92 z`it+k-ki9L!@-fg8^Yf>x%=#UcVm2bM3KNKJn?My%MH7cyuQIdVQ;d~ko)7FojGjh z&-j~fo*YxFubY!@n9yPKhU+QbAB5iDzWEg+o_6tJcQ6c-Y{oyji!KfAFDw7;FZbfm z_Satz-GUZgL++okceGpoNZRpHJo4A08>gqPulF2KY#@{tuuFQ4n!jeRkkjMotJnRV z!`xReRa3nuZES ze_e%~*7|7wz+bx2yv~F{S}?|VOOScchc+otG|sa zj>bc#z+YF6#kKawGyCg07}{MQd$ewP`iu6yPrEpY!ys>3~h}Xj-qxdv;Z!#HA*7953 z(P%u&9SlbMxx?}P@i5N)UH(RVBmZ|8L$m&5e43ka2={)xnSc2B?oT`SUOsxf_wxSZ zpB~-K7iQ<-?D6OEi}Ok8P5gek8HQg=4g!Dt>bJNzv%72E_4C4{G>NC<;YqwXEyShx z-O+e5n{H0HKG@%M;_bYkZADAw7t+rp&(e)Qrxc(Uq+ZrKOD(G1Trk$4 z>s|pCnGkw&o4%?k>XC2gT>W*86^I~^fpJ9LELD0G--IdohJFqxEHiA4bU^btlt!Z=l!cE>rVye4aFvQs~YuYB|ib zoYC9u*W*wM+ z|HEm0l3y=O_5G;UIOY3zb5t6wP1g&9%{8vJ_gg=5m2ZFj%P)KR_4Pu}8VuG8$JXF( zqv0D&U*C8@)3Z_cFz)1UA06dc$-a7T6+2NU@%0sG56BRahx|m?Qb3h zsZ{7|H9M8R{KRSwH_uQIv$%6MW!vGa?xb@j<00Pfd^Ik0ORr<*;P33x?s%#<9enlG zjf-v;PI)r|bG|(qKt;@_^Hp?S*pE*JJ;1piPv6YO*bPD5cana>74@Hw(VkOuS$bQ) z?A9SR)$w8eyl{BTe6--s+1}&tzekGiKDc}D&h4ieE?GZAQ@M2k9xZG0tHmpb>(FFz z?wi0lz3V!-r5U4_7r_E*Y__XzmBHH9&g0!xQ!LEZ&o1h+yuLL2{FIg^JUcIVVBEd; z?7`N(3@kKITymJ$q}}ely_Y+;AASGR?eCYH1#E223d!WcHFPBF(c`-heqDZ4n94q8 znA0QYg>KIyY&L%VdKmA5nLAKiB9Bn*Yh{*8*ks+l*|jGtFu+|r*;Bt?cX z`_t2%U2_VabSK@z>Dn6x9>W%Q-mkr5_Ge(SaCSr{sWU-C>F!|q(`Y}&?2Y%=*GseU z?rcJt)S85dX0ll;c+GWBn(OQp^J0*WzxYxYXDGzUu#?~D9t}3Ai=K=RyBpp8!@+0+ zOFEvI#C-`x-AdkAgOWXrk@@H?v_-zqkGnLScJ8i?*3r_o`dV0hT93M%s*&BZH z5^yL;->%+KbHx9CK7C?}##j^O#`MR+uE6tc5k^8553XwdASH_jmyt!%|aWLH+OkWy~@>gS`lEU>;OjN%BCbs4<@$@Q8JWVn2^j|P0JfDm)bg$%# zYruwV(z~exi?9l>y}EUF`xw5R3_dtREq@1b6HjspXM4f0TY9y=w!VJeIfDnEC5q@E zM%Kmq(jHi(h=d2T_;9*4J|4|F<$}?p6jdRChy9}6&bYgeTD&|?;Yl~WrM7Nbp^@+Y@zdR!DvnoM$&`P3#culx7NC)F2uS$p3)1d?M*x?OcLcXMb}D; z$iWlU%%ccbqO{+gp&vz{<^?V{a(GOd&c?IuFlmZa6^JU2@8!pBT`eMm(U71cRZM8Y zmG;Ux`q~hBTUvMeM==>j{#J4aHK>|NK#zZ_QP{fpsYYiJn~rVr zgwlk?1Su(_1gs^u=H);JWl{Oh!r6Hykom!xrZObGtbK9;vIAY@o}{|($z{4vY?}1? zxw!p!O9ZH2WEPqwvN0H)z+EqmF;l&F1iPjN;iI+5dO;}cjK2A16pY~Vt+T`KyO-LV zG%FA{9S}!-*`0Mp=$qumc`zieL1891dANke->yRAZz&poyBdv>%+oK$s5lKNNLShK z4pEkUdg;)0%YJMo`kuFNextD0W_~@qqjJPMDp&XdAJJFhbyTl5bUk@Z6N71)Z~_4& zLgfliqINZ-U+GEQnX_NFuR{KIiu~=%kpEA>zn8dmS^i~5%}SH?QO1?KP7eIZabN$Q00qmeh6pp1&u|Pn;^w z;338Im1s~zQJC`XP+?H$6^_G9K37)A|M9NSgs=O9DTR-{J^{bfFH+_TyXU8AOzvu= zoQ*7^+t@}fs#pnLQN8eCZOr_psb@5Go{<*^1k(b(jyHQ|JQupoK}vXlypxk6$KI)O zLwAlf2N`9-B)dK1T56sVarD+>RK59%RM#69=)=Gc;RQF{Nb&qxnThbqYht4nJ-85g z^7D<*?Q41SeKNQs;pDr%LZ!Q8*z@P?Nh!a0p-^E!TS`Egc=2Wamm3}wOr8z8_f)U* zM=2;w;d3b`p_>fV%zM+2iYBVyaY;rTV74f9hF%`7$;Dsxq!W4xC@p7E+FZbB^Wza( z4zcAy>7m>8K-k$=<#fI$Voe-42o0P4E>1kEVD|oqWa0F<*W;;tVgK00ojdio8gyM0 zT`oXUYG{5YF+7bA$0zZlpm-8uT1r+)2W1)SoD9=bQ|V4+B~N~e5c=W-B&taSkEBot z$as2D#?$M9pVkPnq|#+FCtYeC->#h{mc|0HH0FsVx|sXh9c0tR%zuUB9?;DO)6YXU zakZ~ylstX#_6d~uR@}7qbtdoh!<b)c%~y>il}Wu7FhWuV2pI{7YhE9=PcB6D`%nuR~CJDF*%I-n|vU*lY*E zV7xd|9W2nrsI43S<0XR=9Tu(qQj*~UE78+YweZ8m79k2dSQy?SG+5;eZ%I)4a9wFi zCETT^VB>O0zFLzf{BnsNhnIg8FaLJ0{VTlu*G*s4v)w&>bv${kQlz8F_((-J)7z9L zoKOX0s^`vlJdC>|%2p1#$HSSGu((=*t>K{e#<#kEJhFO*q?t-`&KWe0t)?hy-FeEe z*=nJ`ez9sH73~aqyR-L0CVfOIUl|p4R7CZocNE7^cXQUWxmq<+X7bEWbw#QKE?DW$ zptm*T)X+}8pMs@lSI+W-X;C#BCvm=G{RB8c#;#gvelLgi!*Lh@HL~u&jC_AKJL+s~ zoSvSRPOGKydS;B|jy-~IHoAt&>@ zM}l#5Z=3hBbD*{T(QY07v;7`Bu5ndwsL_6V?IK_!i!i|JxAe&dj`j(gAxc%c(Ed>! z8LI#6CMW(AwRpjQbLDLLM!VPO)q9YHcnJ%hO5??{HmZ8#cWGhUEZ#gzib!GQgt+5hS8=~r!Lc0BqFP-pA*;i`wc_?tBK3SGCAKeqe)QATP)>OEbe}OCOB4dnjLGjZ1 znJfIGTwy|;mG|u^Xvx+(sf39XCqmPkQ_PB;S-e{AP?i`kRw|CTl$I&Zf|fyyx-vy4c)Z9Mbx$Z6 zMBY5-enx&=AXtj*3Yc}NPXV-EfOzl}O<3izDd)gd$V#BaN<1hC>+5!hzF_%7ERO&F-#}AkkvLq`C>wHq8RiD3m~i-7<=l%|s&pm7&(2Qq1O<-|eSh}EfWS)1zHEKrd>n0Rb=bHV;b7{?Mp3{~%mbFz$gv4lBNIWvaOr=CO zrlu;0gTjPgYx!#R{Q-nCefG zs{`x2clV%9=RMc?AlvydovT8LT@2;T&9uVIr*GE%u#$jQx{I~9Iio1{?EQK1?2v!^ z=ee^!>9GO-rsw&a`LS(A$kSPms2fwi1s8Wv@>T%Gby+pzgIxU0Hyi)@Ia{`0-1vH< zG>fM*+J5uRTD)o1;SWaoy^a*LitV}QB287?8H&pMZ1wctZ$@zy4z#khyM*43-;uCq3?cekB=u)S`U2d!3g=S3sZMt zH%6H3>jL>ijc~X79!VJFRTtq1Jpzx7v$glXAO61od;j;r@6+GA>o?9fUZ?WVP$9gH zHYd+#FZ}cQuAn))^*sNN|M7oE5B$IX_rKBK|L5P^_kYLVfA~-T$*%lQ`u^YjpZOQh zM=v@M^)Md2p7p;glj8g~;_upMGhfc%AoxG}{dWq1GZl$LPYAK#_xCqqhwUsB!Tg%Q zQW}j<*Vc=p^$l7kOZ>K4zLo!nfBQf4|9q$WEpI9*-+GAQRO-cp;aYd2QC`2n{r~>| z`yc<;fB1L*K}~oO%0e4Gq*nje|L~9h@jv@kY)NdEkVaHY0)H%4o)LAfdJP%p5aK_w; zsX0%KNe0nQj+d`gYrAwgS)}V@HeOd{5WbZ?gJ$#2qjqgn=FQw>>jpw{85cX+@tEw} zc%6{Bs^u}Z9Hl3d#2(d^4gufWM@M?LL!XJ=dpLNfBBJMJBAWqx=J#SGs7Wy6RGKqi zW#yS??Kcbvy{ha3*O>?HiG#trJIAjdj0TT)k-sSwWUgnQ5%0J?GGSB9*U-SUG(CPL zM!tzwL^lQw>$44L7(Zk`P>e@@65pJa7B8jIA)d=0j$g6)CI6L_m0Kx-*b3hvmKTXk z5Xc-UJK}-jQc^AqrjO8#zQL+sfJ&#IAZh2>A%qK%D{OgDp-Tod_3ORoMmbqx}mg-tH~!Tx?sq0Jf_B{O4g z#>)2*MfX-lBn!kVE%a077mpf#=brZN(@d4;^4_t*#@YEIE+q`$1?_q9TOtio=|biS zZZN&~j)K5ZcPQeO?GksUkRJNm_EWk^1c0js9Q0%#Nr9X!KW9Sbyzttm0{2YFj}aJ! z(|K|6`T}wB@tzT&9!Poyc>A9{AsA@fTI~$TBPnKF0U>M&`B)Ryb?6JzUccEOh1AI4 zv=kbA^9}vX-C4qpp0!T9!_)5jsc@$$V&}tv<|h@-B$LU$7(XU{j=9}9AsL*8`H2)% zpD%}j`C)d=aH7wgLy@Q~KQ3CLs}VJcR0~Q8(z(q{Eu2ENXSqCw$1tijvlfiKPOF#L zAMzS99uOCYp?v^SYWgMl7T`CHnQO>G*eBe z(PmH_jqe8Mzaz#qIiCbGSCjZ0KP4kB%;XXvt!QZwOE~drdY_xZg>>%#Drh!sW2O5Q1O7E!l?41s};=$b2p_PXmGYC_szYdjowk0>BbbWBh&UzrB;oa%Rg zA}r1eZ|2p&-UVu4!DgFKGCcpJ5gNpK7l1XJvzSgx`4EPH@l?C9=8dG zS_YtaMO&9Zu_v@op_3<8Muwt0lrdy8L=@ZSGR{+xoT_l;D4fNcm9QkKC1JgMFyR?5 z2v0Q-j@Nw<=^{3Gv&;qyq2+t=CXE6E?538uVfjgSgqFL&%p1lB?1EwCa4TEmu+p8` zx@(W7gvxql>M9O<+K=Pm-FP_b{{DNd{P#uKul)FhxQcSE^;gn?;FwAs~(={(whAk~*z&xttzIv5E88Zd=1V{=*nUvBV4g;ZZB;1Hy5WG~oI zH)@<0Fj#0-tld2RF3iZDo88L(+&nJMI>T?Xm&|rgvA8)@l>fZ&(uMi;CS)XW*7x1L zK#Hv}W`qBWrbvz?(1z;;8Rjg(d6Hwdh2DX?I$v)``@HL&bB>F$jyX&;RqImrN&GE>?5bx-xI5^<+;SJG6fXJ4n9`|g^g3rw%VfK;=gBf_ z_8`W7I)SovJP~(uL*9RYL^1>9OMEMwn^k%>-hW@xzVj0v+2JKLa?l95C{yjjEw;h` zA^K^(GymAc>aafQNy*&{q7{SDKyytNk*p~rs^{E|&X_{XP#%z7mN!D2MnYgX5U2W? zP5ZVz_jZEy?9)!_>lwspvdKI+7}*_h$xSxJ-OB!8X9moci8K-r7*FnXsj|$Dgm8?_ zi2vpr_gOMJ{N|gkI~2fq@^dct2+lK0w;=ZdbJDyF8J;13>UxrrrLfbxx=xj;q-U7`^H?Rqy$z%yO;xusH&K%FTcLJ47}w}->| zY0eq@7bBbJ+yoX8eKF(``AfOqVVrXHoOuVk7{i%|Mig@egj0~Gz)faN0SJadyjG0X zaS(3Kd=h>R((BIE4P<8EM^6G0C6i=VWI9$B-jS}8N*MN}q3+0WRseoDz)E=vQLe3{ ztqyGex=F9Z0dTHGRIbCTgbD3|zZOR~qPl~wp4yyz*S*#4Oq>YCK1^Ev;x2icNgQv} z$RHd6;D~O1;=a@FIC_ce2AfEQi`@`QBJcP#|?;)Td=bVF(FrecVg+NI5 zsU1?5aRgjI*!u@$F%=WeI26u*p7SNrUeeGwlTZjZ%c*I&ASExR<>+8OJStKn4M9Ac zzLi%zcrfB5Iz?VD7}-TfUHDKDLKoM-_qgQ)A)G;*_&7?4YKRc|eR~{F6b80Wrs;G9 z7s6LUA@~A$E1;obEAfgjSEupuQA(T1!AAL4$Fms;{XGN#E(JE&vaEMMplCSBjb@G23{0QwrgIZDn^JRWz!b(pqjQ1Fd>r-mUp1q&GsIeCinv@UR zjyGu+LYNPd5GJ((4=Kddx%<8_mSf6-i8p81%sC9sG8AD$reUzmg9@y_Pvy-*_`w8p zDhffymeX0k-gy1Xf~INR^B;_Q!((FLrp~kAagTUKbzdY?pW7`6b z6PRyB1S$1vF$^ABrHO)JF6J=6Cm;xjv3PSW#<*2s<=i4%W%FHEQC^5|1#rkV(SvZn zT>C2*O`V1288QrEO4+4*?!BuV`f9`WR3ZIh^;`B;D3|8eknO@P`r;mfVMqqUchxEr z(@B1QXFbnBH`_h#_*V&0x-7gi%6()ftZF5o1?OAW0OigcV&)N<9BM12h1yRgs?T3# zS9_r}MJAPe`{cOX3<+QQoZ};T3KS8N5Lh1Y48Yo{siY=5U#;Lb^Ns?Sue!g)a&wIq z@4`GP!|DVQpXYd)mI>9m2tU47@6F@mnLv=8zKycvCtt}KCJxRE97L$dRV3Xe|;H2Z|vs;-(JSf@55!zTJR(QfmajW@CER~j|m z8X%8`e9ebt&F1t|s@R^yo3!$ZjcBsZmX~m&)_&w9)0+}o597_}dC~^I4JfVYy_x1; zaCUW6_$}Uih->GR^c1j!_erl8?w}COq04V1SB;}hCw891g%HWcNpKAXY&4ACKM?n+I(UwdREO>DnmNvml<;>b^KHY%c-a9)K7n&9FnUJ5JAzJn+&_nlf?H65b;MdFr!~mh=hxu z50MPn*BUX)bkWT@qThZv=0>u}2O}Lb=k}(jG@o2`i?sLDEG_Qqg~esX0@uwH+7C|*vH%~s&_ z=XA;GJss_wm1SR8;4Sldt#KiNZyo+p&Vc1%a=>Yhoso7tK}rJY3hbVP&5#815?p>o zCj=3nBpv4Q?5t452$zI?lBd|{Nc#VM7FNsVno)T3!v_vAUqRdM238}|ILrv}qgMUF z+n0B~tW?Ig|DuDmGJF*^_5tBa@iBP%`mI4XS?$6sK7aI9v(dX?^wI1Wj(&%)SB$QM zUW=Opv+*yRh%La~0ZnsGPczt%2VMXk(xm6B~5D~y870JVdI zS1VWW(@&4t%e!&~+0Nn8056IH(BhLRr+HA^q%h=PsMX{+d5og9GZ&1%6Zg9(1ESzd z0A;tHiqG!|!GL%Fq{G|0R$OAu9 zCkOmXxE4jhT*bk5eCzp(&hwvQ0-!2AxbJ>H3ctU1zkdn8zjnXhhTlK9-#>@nzq#Lc z^gEF^4l|v>{#7t!s%GTb(p00LeP9V|xn)t{?8_D6Hg4`;z>>9B_j$pB#L)_-M9^g@ z%$O(T+!xN;{woT_LW7+%7vZvlRj5IDYAIiQ(Zfc{gfBCjX7r>Ep$_2+@q~w4ny&PzMh<@?Nk=JfJiXba zrXzI~?5ytm`mG;RFV(#@!o@7I)i@-57OQc5x^c1@w$TtOO0a2jbq>Q>i1*0ZO70%p`%{X(1#1G{*t}ti=k$G$UT-7qK>*{c}DcY5r{(>`?5!zu=6d@$7mJ zNeU#C)Bf$bm!fp9gg>m5z`uJb{QtR~nYw_1`BPFsL2^v0HGHY_tFQH_)C4OWe>9I3 z-d;&at!mnDk`Z>?yja!3&SsTs}tE;SmeVy{5wC zRr;Qabc8?u?A+TJYa-B;P_Bgd+=01Y3g$Yb(wS>!iG{E=GV?OFs2mQqz2aR<;cUxFp~=y< zg{b;@CtMcpnoRzegy*cxxd7ED+5d*>xfkE@BAKjZP8H+46>@x~6>^-hLXQ81Ss^Z# z@6(JO-hPWVNzIVMIrGrk{u3F8Lywk1_QftoSE7bD=p_c?(G{rCi=CVpAAc$NOcj3E zzri^{D_#)d^Cq_}9x(rg5xuf7`-Tx-I-*N2?Y*Hy!MuatUH#^f(6Q?!G#*{p3G!B3 z0yaP5EhPB3auUV~(NHomM>13VVRsh!b-smUKZEN1D^VS&BKNswkB8ewoFj~*bMZgxvEx)K0*bM7rcux5Bo5Qo-K#7ZsCKaJnk&j7yvUku=>g%4cATVVnY z7X38CLZJ3*a=Z?|pyNf^eTYRS?vNAnoQYsBck`Li@V>&H+s61r?^BHmk}md+-ny$3 zkMG-7K2?5S=CXPcFCW}ZZD{Y84fY=&XhVq``Qy)wwpT4bS@o9Y%%nqi zZum=_LW9CW>kD3DxCE!g$L>GaA$q&CP!swNVm!e%wBXZiQS9?NcI)3IN1p~!ulDTb z{EWiqYqA9w16gmcC6tF@^Dkg9tvvpoEojr!$2M^CvW)rn0ed$l;~P32N;)AvGXJh~d77;sxH1UwH7-+{(hZF!)lY%awTdKu+E6 ztYeS4#!fDguA^^W7tP^*URqrCvV<|aY3vo112!pY8~GA42ig;-3Y0}WhgVY$*co7N zrSvnDNHA|`X~rQ?@W)vwyfbFzMZqHs zf(0R>FpoAM`5=G}I}03YN6rfa+NMjAW;`=dqMe~qY%&-)xPkPILG!`gn_p$;=icwV zf02Q0Z`4^8y3vGsn`g@4bib@6I?%BAel%@HSIz zP74Y-^AtqT>&?Ot>c{Wt!5kVFdIr6Hy(e)^iLASTV_gv18?gJ1Q&4?$Dv=bEkCa#v zOcZMl$Pi%w%1~R_neDF<4eaR%N~DrvfmtUW&aLIcJVcmnl>y7=G8ij#7 zp6Y`1+Jk?p^xED$F5X;;i#Hiuy!jW0i+^t5zqc5{+PxA1yBP%RUW$N!ZrM%84k-%e zon%-bNpHHA(|;l-`Q;)~9$$%+#~GwN{%=UpIS{f8lh9I+qagDfJDpG8rKJt73g_zh zQ#NT{V6G=RU}s)X-M$hfw=*cYeJM(UZn?(z(j`iRT}2!nO_VOgG3G*c*R3dIBR=?o zL5wXk4*Jd7G4DW3koy$mI`=6%lq_$yQiWFmV1M{m0faL{I65sD^|m(x>pC2yG8;dc z3@D9a5914+#^%n%(N1$Wjt)5IAV0R@7xW$iztw^{tr)Po9){_11j1x|nv;FH$NLBF zO(tWqVgKr1WroWg9#3aEKd)}?C`=<~TdYeY|J^ z&?0!c`KcY?le`px4g9l?ds4+?!qwe0NTd)2$K!Qhb=YVRP4OBumE6fBxjUF@d!7zC zFYLe4IbInXW&YJ+g;6Xg`4v8i&H@WNNc4<5{$)Sx+E{#l?bgn{+fQHq^yKdCy?b|G z;zjR0c=UZXio-ElrG-8=&kzVt?>&6{>|S`{<^89RABJvLx>V#(DW*Hp*?ih?YUx$Z zNagEP=zPP$%kU7!|H5Ew?@@|^hy-T8W#Q$0Ei~d42+zAOu;16F&#fsYEwz+W&a2mh z77ckiLx9~}dUSKlViY~Ny|wp%1?}CwvoL>x6ah&g4t4k5?$*->Pxc->UVfQx21%j$ z)vsr>8LWVQ=AaP#%Kc+_EAXS3!U05u0?WWPO2$?72lZ{uN*pq;WX~<2$Wc+@6`9=N zT|~NZp1IWHJ!+UHA@~skVfc3RB8e689e)BK>wG!#JnNn8FZ_?fnF2@V4}a3U_g{rU zc^$#pRRdEim>HN~eq<0GxL{MKitWGJuPU`T zWYWisGD4M;N-S2H6#oX%nA9qH=ffJzadpx{-28H4I#IEBRAF#*w5y%cCYZ(YtV>=!TPb&z`ltDdE?l_1Qn~z%m1)6F zRJ*8FC$Xb1i@m#74|~%61j=2=mHeqMF%Mx`kJ@Shk8ow)WGQoQq);zF)EB;FxSmoc z??uOkKo*a^lI`(L7R)i9-hv4+^^{B<{VKkSl5mmr1v2$5=E#(Q`XzQ1+ow&ESo~Q@nWxV0xlPuFBeqpB`3cmK$hALNrX>`CJ9?q~sb*Nu z*#j_l$Y1=Syw*o?7Cd!3Sd%H}%#>TsUt}eWau>V2DPu5m$;@f`^rtD5FPYOa%hM+? z7xG({tC}-EyLvTm(3YgE77QCzp{6m|b&Ppuxr*1OYt!}B_OBD*3kAe(Csk_r9;|b+ z*pxD2#t0KYqw6q$L^ae4g!#toVbm_bZ7M_fIvc4XN-uqvH@pd`nH3(q_ z2uD$=!IlzD6(yyKt5*d{kc0&S3;t?U2f zy&@(3bpM6-^WEp1=Xo%2kSxniuh)`@XE@K9_Hg#zXYYNQ9Zy!#GUu_YD*mx!>1w?d zRyV&%7PE$y^S0fEHTV*vtYNaQhB|EP@LmVgPp12QA?*cAA?lZd1mYz$ki?Ez5OEdC zBtEH~Z71SNuyrD?_Q@pTYQHGRgkJY#0YBvVT*LTWj~u)R$n}24=`G!i9h#;?2pv0g z#rnQj`mz;EkCvA7>BjNi901&+mT70!-!mIS?;W99`CR1J&F2PJ zZa#Mqh%u$K&=w+vK-L-QF17;JeD+r_1GR_gEh3a>x|g155y`u3o+t_RGoa2;_3{?* z^;fhaKjUH4HBh!NVJI%c(jYqJzuqS9Z^w1GLOK!aY`)-n>u|kRmjx&&K^NBL8>6GQ zY_J=4i~2J(+E9Tv?4;HYj(krnXZ9oq+_t4QZ5*2LcD_go#^FZekHV<)h4Epp$HU%j zu=zqAW)a9iDpESr3bYU=(9y!8$L@`Y2;BR{Km>pL$ zv()2pS-L~m@-R$^a6^Kl!wq3cgd36`?-Mt6@emJ9C)l-o!~;_}q?Qs$(A@zE;Nv?X z0S0x?FCGCB$a?xza$hduwGS3RdOF^s@_x+oet8?(d19~0KR$f@gx|m7Y;c4;9X}I2 z!F^d7j7_|<4JvNf`on+C&aTm1C!L=Qxs1y+eKLfqa$-iitvhXpk##&7xO(LH-07!} zU4Ht^(NkxR9=m+x_^~6OI(+7@FJGKHd12JZjKy)Rzgcz7_eN%XU*E|yN9ImGU~M+g zL|qe|s3-KqkS^}RJRz?~v@dt!^!)j`(?|9fwloT#+>#>Ds-ATJ8^!`?c_-dWIZ4=02GV< zUeNE3L%%y+Z|xWQtqA&yK!4+dhQ6WkIO5$0B8-UW?->O!ne4C$C{EFbJIJ$L>4$=6 z``AVl5eaFTRWPGYSVl({p2*Nt@rEW}U06$!3z5Za1cs6KmMN!fRb=bgSA%qH#K}GC z!l?DfE0D7p)T^6;YfNq=77hr(^14iY$D+3KXM!kOPi#M!M9Tvw*=nV?K(C24g|`+D zHn}ks3G(GERx;bRwC&VjF)Jr6X4sbe8WCQXGUy98gBU!ykx@bJQ4fqA+Qi5lKIF@F z#k34KNzHAwPR1t}?7}vJv$nUJot44r$9J9W#~=4K&P8I0ZTR|jYuYiwj34BU#C}nJ z_CeN>SVZ5!uTo)9LwrOlalD9qXn9G@L1H_$pSxj^&F!bVz$TWKgz$j*d02-quA}8A z+MtPud&H3K5xhJ^+mqXvNZUIWy>Hr{Sko|V?^qi(t$@^+Jv9&nGh4V3&Rkv ziTR8UF>}os1w8s*Pg2GA-DejoxQ{8;UZkIK-|D;>Fl4l+I=yiXu$+2PrO>~v=o zOPD5#z~u!yGKZmNhaM(I7l$enrAGyJdLI_I1~GeLwC{;afaU>bD|vjXO^cy{d4ew@2PPT-S4cg4 zhG~?uX7a_OoU}9NkCRw-2AfiiY`i=B4mnlo??PS?mmGzWoI3 zMw|ch0^SIW-ASB_nZ+j_0Rd^^4wYUT5#0nBfjj1*A zr%v{9gx~KQ*hw6G2&6iUoWuwP_Y%KWw2(R8PX?AS*^<*)rwvY=n1oQp@@^#)2PLxz zyQfE@GwNIxf#HH9H4iWNqw8^0L0spa-zPA_ea`muK8D1y<~BTOrlENj!hrpV614-boKrF#1o9F;#I@#jZA^VTJYUcXW!R2L`6w@@SUE} z&Crc_fDgA9BTyr2wd+K3Es*q^x&%9leYvz`c8jARcdh zhv7XSq$jPmdqdpyDlzQ$rBey`laz)z-wgrg{Mpw#^yERIZ`Do&J#P{d;QRN`NPDE+ z#HiwR(}P^;R?Pam7n8#CDJ+pm0_O-DKeNNhwN~e;W0#|xH>VJgvz)TJ)Lok*G%%bb zK~DBopPXtT{f$4TQ|5hzG-SKI7N_Ch4bqh%e?}q53!I7sp7pik%crElSxQ~)0m{^J zx7{Y%SbAs5ozgI#I6MlC>a%rXNtke?9C7CJ?&`-lLvHHk0+Cf0&YcVanV0o2niT8R z0ozB|by8YOr19|+)LP2;E6%26Cm?>~?lUV))8MjsR+F@}-@n@JaaxEBuF|*a1GXaE z!+MADMNaYWJ!inD4T~e=*`Ct?AB{cwgd2;CI(M~yV6)k4EnmOP=$6pXTa9)CW!MLg z-7oj{h#|6Dfzs2rLa=S>vq|5oPwq))pG?r%CwHN#Od?r&K$i!2>OJG z@?@982R@u+tX)5fsF*qZjF+-G4TVn6of;p2W-b(%yEu1#Zam$ho77O~+?kWdlKQcm z2b%{4&&Ww#KfQga`{g0E`TAjn!F9co| zh&mZTKcs3z!j?A10}Hgh@z53Twdy$H?sALfft9+4lPy6^gVkmyj-lX{aNoQ#Sk6NB zG(#AWmc#>#&3^x~p-ODs4-*A#OlKl8AjoO-gXkTR{}i=Tb1)&WgAjLtlpa|A(Pu49 zh8<#ebB#vXSYU(3UlG(H{^u08r<>$j^W+dsr5nipf||p8y;q($;?3YZ7`%{C%)qhZ z-S>z`MJus4=0~^`;)eIXvV~zpa{x4>J@%c`7czaqMB^sWr%_^+2oLuu6)SxdE2xiE zON6hDqMA>s(!*Ov0?3oS=bi)Z7qw>kNY2!z-~JdFx6P$yj>Ea;Jw3h zB^`MD@#qpavdbp(96~l1FEl2AGY1HTD3-o7t%w#@6B}*Hn1M^Zvd~O5QXS5O)ETp> zMz7bn{uqt;YQ&KQkU)FUixWrnn4G1dq+?Bb=(thzBFRyS&o&*Q0|rmT|A5+{KW9NF z*vhD~@}%#er%+WpGIAmcBeGl45)|E$QUiVDspit&t#~xr$yzhsiKvnBCLP(|%FwE~ zppRdAD#7vhq}?IE#ent?OuZu^ZxOTYgPO+aLNv9XkV-C4e(A z_3*#RCCCeupvT}9SbY$0j{A?2`s-2stw}uT#C*7cPo!8p0+X3)c6Lr7;q*6;Mh^_B zh?}|O2?3RNR#A3|wh<}!b+nRseaE8a$N>rGI&p%z4o9;~4Edl^%ndq{5Qe6*vnPZ) z2?%xefDk!%!iw?eU=*p5jrG<6&(aSVyY>fot~Yxe8s0|{zHITeaXcH~4I095T0o!{ z<<9@0q1K3?mYCk&o^b0W;MUs{ZaUcFkyF<$v4=)o38ET0*#PLkG@ZS~3UY*efD(#* zbB%SM=)*|CZ*Va1Bh>yP=GOO+i>4M+%pUx(n}Aw(52!`eJ~~?4lOHZM|ExITVnkcf zWNz#Uqm2ZNHuiu~f@B{ZlpYR8WH)8##K8{}Pi&7;*7}}MTTei3eGjNb$nfZ3wiji^ zyl?_s0J!6ZfeU_662{UfTvqpl%W48HtABoQiG>N|^B_#1AjZ~N=;niov|bwp#`2!P zSWWsFv6ES9s4;SrS9R<(Ap5R$XfM?;)5IpQHEH*EXz$F69hgdDX?qQY4Tz_p(XkAM{ z>)IaBk`O3fNzCZO=aqf3_IX}eO7P0=Go?$1^yj{KQwjl*buPZ-T7U^M^d52Fdv{v^xP2MMOj<6t_oCz#G8z;x!r2&VfJM?&~;oS7#& ziwc_DNj8aD-y?8Jh*;lA53e8$rsnp9)?5Nwb9+E5s`kU9JRh*!l)bgfW*ST|z~RG; zO;h`jXwoM}fpla~ARS2n>ByfUkR-`>mM(i(ndjPvA7GN}&UFq3;?jvCVVCL9%{iQS z@V&d?5(}pfa{y$smx`pPXp>VOKh8rdbF>qkw;FhgIrI4R1W^K^oVl{uff#H=o$Yne zS)1|Op$IzdEry`-#9pv-XbG(ZXOvJdM1}A8fp08)M=G9ij~ZF$lLaoHJ$Ht~3y3&p*=zfE6dh|4U#=dE84>YaChEie?4A!x{%RcQDo`1uj9VOgEI)~c(x&NQJ7_+QpuIo2# zLe*%y)w#kizI1CFpg23i{rXLDN72f-uU~RBfs5^(TPrjF>AABLYt&jHAFqiA--K0T z=e)#(OWJvuq#5MQV~#KjN0HhzPCu!PQ^U`^VZaXE92(4UesM%y1@e%^jPU8+AmK}= zB_irr*j&gONSa5wYwN6MQ^YZQzXrUK=$7X5$e9bL$Ne7d)ZG~7VM-?^lbdiNx5{0f zKk?awr;ktC7E(3DHO|j}`pmhb9E_vhO!8L^urS;p1Zj{JauCEA3|KJ&IfArPcP6~l zXF7_<7Fo`oI&%E-UtKtM?lUpR@|{c&Kl1TZ4 zuc#&GCliuQd;Hl4vRay3s89J|*;7YZs0?JC>}3tcmdXB3$9{(sZZbDV=pPg${Cvmn zSWG>xQjR5YPTol_l>*N7H+=?wH&@69#7WGpx1PF>Ga({!d1%THPF0KY1`JvJhu4RP zs5gdXG7@E@u@Kua(Thvzd>pLm-5?cI-oK?LlZ4g>jLQ2E{3KX64WW~0Wx`*CSeBd6 zqq`)uXywq6^e}ikx2PC4yDFL1ly~Qi^T_X%ImbS&@oD1xpQ7p<33~7qa@s<~=wTPo_Ldf)D zek@}yMlvQDQB5G7-!OmiQ_g)mcr)hrz_E;6a5K+5Q_R#djAG}bgZ3$m&MhsSYptv% z+-M(!duBEQfjuy5*eCxa*lSfMx5u1xSB_|_d2|e3=B5iE2^^N}dF^IDIxnr8c`TTpHkO@Yppq3YIc>;0*bOE0i~oK z^dZ$+aeeJpTx> z9b(Hu+FA1GZPxVngx>F&Q73FNJFNPho$QH2cER!Wp3V57U7BGV-21WC@4Oc0cQ>#l z*5dcP3M>BOBg==9o013?%SZmdsO5twsE;fkgTCWxM=k8>AqbwCjfZ^_j?XV9hIpjC6^0_x>l?|Akk!F`Ods9)NXPTEtit%i>g*P19v?<}046#rgFCXusdFPo7nU(FZ=}OS9 zH-Z<9|8!QnAWz2`V!|fGZ2bmtT{sbKI4=xW4Tk{inKUZMIgwVoUz;qwIO}tk#hcR) z4*k%N2R2?!hI?Q}e*TIXB9%Hj8%m1xewgtt+XZ74o6xvw|9!->!Eh35(W5w7z8E3E6zp1w0mVADKj+bm5#nB|kJFqUy7| z`_*Ya;p>Gj=CA-MXCg^Ab&l=}LP)-pJmVHfV)5VKJ@U0L=wGPS??B-?K>}# zsVL3g(^*cXn@VrU3ss0fQXop zzSVoi-2I^K{kp@p4*Cu6F8vL%_K@3Qz}*BD@8;#eJ{?XvvdH8p{q&n=8%B{nm}zz&uh(s~0^82Ns0_k$0(Oq4ZzH zRH<{3&UOx7>|{>K-MO{YTxj%SbV-DL$XPDy3*5@I8=aL6ae}1$7|Dydzm*v}(qLlSa&=JJ?xCX&ZVerEu={t3jNqJ;WUkf|KM=qiPr)yh zBeHjzH!J4~+{##@+nuY^>sy&*BH77PVs$2qG0PU*uuE}Q;Fiy%yK^7RGm83%1JHKI z&INsmTbV>AvR-#X(F2peip$}CW#yPTywLXfOr+>uYsCuZFRLB&-EZ|if%PMtZgth|I(OggB1xcQN< zCO4AkpUV4;_;I&%!23wgf|t6`-JH}hh*;iUJ8SDna<4a+8yjuReob;OI>&sQfM+^7 zcWd!Vra@-6No}c%do`vC@Q9BmC)6r`_wnv4uxoT`#|D5Yr%9fe-8!C0TOslVqdR02 z>S1vj&c{Tfi~D^)4gv3ZeNNarf~KHV$YA4T#8OBp`D{mNyyJYmF$*@C#|`NL6hbmf82+oMuIilUOrtm+%;tU@UnGa*FpGR z#^w6@Q9qj|Yo}A`CyMj7Pj_Vua3YO9}#l92_& zlj!@{TKC!3eGs^Z`@9QP8$oHu;{j?r^X%l;eX_lQyi%wE*{WUN5l+0RfPa*q99F0fU0&u z?ER6~g>|NOjJmp+BzGL8F|vgSASb(5o4q3>u1|B)&#vrKdi}UKCJ7HsUgAl;izhI0 zlM4oqPP=(O0|@4u41gIo%Cl*|fAT|keTb(ujrdvz+?v3H-N_T42S>a&I%y#~^T;2$ z5KHj$vK_pK#cY3c+Z>!TP3M88c9e8m4~xf*Sg*qL$~rTwD@_tvRwj~dn9Gu#qHehj zT5S3!_XtPSG_&1!>#2Wbx|oby4leIwD162d`&?%!uvUzU2~*Aeveow)G7}L0*@u_3h^*ZcL>fGcSXSgWuwI}2aGew1rxJv=?I zjB9r6;a?0~E}p+~zutMMezfDwX=gj6q(}aAtKi|zt&C@m^-~?Thc1}8>&E%n2EHZn zkhO2o?XZJ7mF{n@%(QT^PE%Hs@K}Tqn4OzhbCQ|19ZSxAnPOGCuza8^sUcZgXSubK zHFMu_&JX0|n?%l{=un8rETbK-^MuOBd!W-dk8{$FkKP|WV2OI`&CKQ++4pq<8_D-| zq}|vNCB5b{17;+Bf=XRyIFUl9*xWM6K4nvJ>e-4cf2X<0cBY;J*BH|JQmexJoMr-~ ziZYlDLB7A*y*j_%>U20^P5$Oq&C{KO&!i7v(d5J)QpHM}nh8fG>VdtP@kd3ZFU1E? ze#Vk+MXPXL8uFp$6N5VcpGf!9@$q?oK}pBY2M3QL;bl>brlt?%o5h*a8*2;A9k1F`G5cKQ-cGQ+%)1JrwL)$O;6VcAFowPiPtKt0;}DP9-lEBmg!$#Tj;jy>0{|znfQ4*dVY53c_Dg!KKXn$di-?q zaV2_uD*3n=J-(8@bqT9w?#af-J6QLA{OpNr zo)e$CPqnT!m&ik{%0TFwexmu@b2&8_%Eb--zcYvaX6cCoE1B?RGRIe3 zZ0gX#sY8$b4OYLuS-Q#Y;uC){%{QQ%u=|tE>_QU|HIH$SbWBrr8q4%eLuwccsaGOh z+8qOUD4?;~YRI9xdI-lyX; zyRg#idWKs#B`ZD}D>@8|=-DSns)Y`r2HU969=MAoXXPAeWBMjD61FM&Q;9x@hdpdY zovAol*TcX57|~MZFEruYtD(L`=TeOgkh9yPy<=mD0F<^8vnqe2c-gi5Eq+X1=#Cox5_UTBH}VjSRb^>|^3lAVg&K;x0Y?+;i!*=2B~8 z4N*K_OJr0@&aTsh^@IU=tCCyZ8u)&8M|z??*J!~)P@}7?CJh`N0AHeCZn+?cGaQq7 zzO{ak)KL;5q6(&NHer+h4*~>JZToEXPc~WYq1pQJj_Whs0}Oz2^2YBJj>W(>^Vg;2 zxgqS4=qGQ4R39d^I7mOqLOMn2HT{AesR)!T(51BZD^Bjy;Ql%Qf=EHI_HX%`Ew$R8S|OMS(AIMzQ=WbPrS~O z#^k0aD6BPxChSs$v7{Cg99HkOxrL=WQDS4b0x6iAO%4T$>u3*`5GHmLuh%Cu(H&}H zX}AL41a$!5wPGR}h-c!rGM~A=_U!I(*8*+x_Uv-4h%_Vl2FzwRE&bUiRz8#7=hvTF zTpRxSv!7kt-2GdWa8}V6aHYe|&kO?&Xr5?1rA+!36Ri`L@QgANX-wVJ(X|pmXPVa* zn{7KsIr(5_L+5Lz@j7T;J>0!kPv=s(RK8Htf1PvN86e2kKlX9AE~l=owL757>R_;5 zKXBmc)vMX7MGmQ6Ie>zhJAk;GPWhS+4%L)uEgejD;}ahj=SuPLRJE2Z zwQR9gS}oSH)xu^uTdY=AXWR`>ayz`>yUAZXz>9ogQdRkUHeak9XHGU}W=r(#xa;+e zQ%qLAlr2_jC#!|5pSO#-lRPeFE5*WQK9|c@i+*+y{C6=_fd+En9q}C zX0lRqO&l*(vXwF=n8XU7m9v$a>(K!R*H3&5-XOluB0hz|g)kMjJ$F)=4+|SY<9L-n5mR0QJ$&fL0^?C z&z4e5dZ}1W<(c$ic@Y@pXS1b3IaSCOXR9;We3h#zSA0_`Zq5Mk^2vOmnw_1kWpkx! zisywwikfTH(juku!Uh#D=Vy62Tb|{bjuf-`vdZ#24{FjMeG95&^SSDcwNeROtE9@+ zY^{(#St(_ylo#28Fou>Yg&AHIOPe5Nxlr9dUM!wi-*`lLArU)?7P^1rSYe$=AO&Eq zR8kOsp(g6h(Y-P3xsB^U%z%9%QHq|PBYoHHQmn%}fP;M827Z8TzD~lCM1N4H3 zx(wq5UCOF94}!o%3Z?4Fe6EDcD%Cce zvs_4l5|vV3-6<7$Q7l$xcu|C7fw_g5Je;Yj_VTms8GxTFf>7XbZnjaSu9W@Ja*&&; zWM}iKTqu=Nsr(|9!Gsv+EZ^mYUG%J0F1d=UFdHhip3GQJ3X6PIR-gF`ov1fVly$s9 z6LkC}lU6CE%1~Ifc0_MVV6KJ=zSfwPB4}ULSZcb?i5fMP#TlE*0u9VkHGHo`D|Ba; z&!L(!?+fLV&_Jw7JL?u`D*F^wYPUZGb2hC^KhtL0()QDiPa{Bxe z`xOjS$B@gCdCqnT&h&TM=&lAa)P45e8h)Z@dyZ#1DT=2BCBMjn&o*upHG!Qz=C)$)R_Zc zbpZHk(}rNI?_U0T7>pkUxk8f0RCF9dBMl<1vOISJ5}h4+Vzc7Mdme62l=v4T%>ey zwrJJW@}hd?LsZYTs)N78^%X>nu0H>sx_x&vfW%nJ>g$d}vA2FrizH54WSK>+(}l&{ z4C`{PTGnEiN9ZcDUKWayIwfNHJ5Z?1vPu_W(gJB&?{w`7Fq726=LQ;P*&f4!qap>d zB;Myzv{pt4(+bT5O6Jp4v;kQuUlP}?RGEoV4k*IFm^nUxS@NQW)B(zg#WK-(DsXoQ z4W()x;FoiJ;O2ADfC`9(vPHGQLT{I_O7IR`LcF$R3IxzIh(#scB^^LcNMH`2bC~1a ziwaA5wZYnAH5%qXOsf6H8faJ(j#xKbFG`?n=!UMXWU?R3imSYo4ey0rhJ?OxP1Q~v zF=C?ep^li`y&oxewOZO!nlasfqHewt>)sP>0}Ju{iMsjq`29rNz_IxKMBV>#{C=YC z;Y?EZKX>ADXU9~?#iPsR5fySO-JU-^$HPUl=MyQjA=AOBi=V}MmTB9=4TW>_@ROw* z>pWbthXiG>>f!v{6QAYbnmt^d&MfQU$<3o}9&Xyh<>}1I!C*dcXIw`H2DcgMtVm`@ zUfRHn92Lf*6BTVd)4MbjVKd-?OwXgAa9A=n?8mN5_1N!s+W_6651V}WOn<_+Wakg% zJB06amg1fyLkR4gYK*zP#~-)-9WMJAQ5RQRZCn9&`;yHd3S)6ad+#11mgoAqoTGy& ze1OsY;4`g^?;v-VGWa6j1hC#9)1co6aTvUnyw^nVoTn=4e{N;2j!in#l19R1XM5V*!&o(jq(R`P2H>U;G!3H}}abkjWy?O0&|9YRzsg))> zKQTlGBf*GrS@*yhXh4TXK2>hkTLMHKU%1ds2N>;v&#Vnb3K~>0O{~yassCftJ!b9Dz@u$nhU- za^)-9N}0x7o_Xfdbp5d%CkEqv-B?mT4Mm0_b%+cmBTa?8k` zdf$)=fJ)b8irqJ!?GVbG9A(qF4+Or7sn~Ovo2(~n>bo7~=7aWJyCTA{bbYCQ%>zJJ+sX#_u5c-B|VKlLSwP0*lk{q_a3D#IJT4^YlpD*Akt_{UHLYSaV$AjxQdJ3S+j(f-Ucez#o{v_d<(a zc)FFj7A;kYouv^H8xrnL%`wc8ATfe#dQ^mPf_&kQh83gk zcX8S;3)KFdu+7Am!9g~=a0jz>EK>}k;7#|1cAQt)zXKvnmYkxvDqOl{B8GM}lOlz8 zis>joVZGgA`?rpi-mnHbnZj7rc~`XwfmU^cMs9gAEb4$SWqJLrl&;u_yONlqSSor< zlF!DViv-ZQf+(dCYXZ9KZf$0FIdd%f(n%JIld~iQ8*WP04@#Qk42GqH>&z31$dXhD zbdMzysZl%#@(mK}%E4#&{!)mM(VE=_PdD)}!!KiJS%Mmo>RmOt%nbSErja@4g9uG_ zp_S=Fev!KxKSgSnFy6hNio+7Q$~;Zi{d%bzge)5LM^xaotyorEH#UZw9)JHJoe%;= zO+T}o>0CA;b2Rh2v07BnHqlTT#Pac)Aa?0 zI{Yqtk)+e%65c^+H>NW#g=84Xq-t0`8L(5Ku20vmPiN#|wi!Hh9A|o}YlogS>8^1q z?tKtJ08X6{P#<%=$!y$0k6~zr!JA4*MGNy0dIxRT$^Q?cmz+Xo&X6=eC~YBm#^aAK21z0ngo_}`)1#z5=5>+Ba$XhLQ=5D2F|WO$Obsb=i#U4+ zMQ+h4a+inZ!;yGrL=hOkIXMNY*;yH^ej=x95%Be0)H!5fr_M#X8d!@Lk8vEy%n{b& zT{faP>VaH5@ep6SJZPdcNoS$)R>vW_eG-+x`DLv!;QgW;TAfly6{GAIH>7mO{153v z(DHb;(8b}h(HVLu9t!&L2_C6uh9*clwb!(gq=ETlNS4Hu5%e(#F?-qY_0Ubz6mSQX z=-BY0V*tL;F-*S}odIhC%R@&z7B3QTO(>LZ47zKY#Rf;l1d!xChOnYUjMfltWM*CU z3ewnBB_CF|wMJmr=<=0g$6=*1uPO!Db!8X1=~TUE(#NjNN2+>eME@rLT&C+0Fo^?) zl5^SHRh^i?Q+jX@rYC4Ky<23TlQqXryN(Z?H1Q`PjmD)+2d^l8orBWJ2-txZ_Xlj5 zS$ejVTb)3TSr+7&wg5SRezn^(*}ebVbNz&UBfP;CEcw`GG!5+~AGTZg3(03JQZI!J z9svRn8l>(}T9Zq+hHwU8bpy`i#y&}4PuG8$Y&h!8RPw1Ovyq+~lCorbtHUTQb`QlJ z?ZTzwJAc)^<$LZW1^9qWLc;t0NWZbi`LYn8D`1jaYsl)@-5sZ-z8#d6Vpt14)a7|y zf5hp@sv!&C#-xJgT01JlCae}?Ser!n!L%^NKQT_xN6qA!Ut-<5SXUcf1c)chi$EJN zb}x4Zr6ovf-5_qFmJBng*sRKFg?$o6kSKvu3S#?O+>F0k2QI_*GjFq9RmRcX%zVL& z<+;fk!@m%C3eUQfVIYsAl@q!DY4I2#%qclc6M5M%M*QMC3CaKadgR*@p+=n zSvCmShst!=>zO2_G6fY$#IBYs*$2 z>3ZS-)HKb8CNHveTPo&lle|#Dw+u&=a@9PtsyNc%>2MK@E>_zN1SbIeAgb~a%a>}D zVa6+XpH)hmn#g<`UxqSM$cFVM4nvjFN#?qOzW^KDxK#2{N!66~4fpZ~tKC?`8I3LL z0xmRcmR* z1zi<^RyD_jY8x8>6&SyEx^w3GR?KhW%akjs=~;VHs;TK}N#0X6yqU~_iG5(UbD3fx zQUON+6<~^MMFEfjGA&?axj4&vT#@JvP9ao)(*x6%!}kCzU>9Ed^mfGzZ!5iOCxw1oUyN!!=IVcv*ts1@Ip8OjQY2JN3_cDQAg%O+G%A$}BJ4 zRc)0T@O}Ug`658&d3jaNY@2v@fV-;D`Yj~vu zZ;ZRBhC>%!Cnj~#H5|Q9cCz4lIU|nmo<|*}JOk&i>!=)=G$8lhy-EHRHN(S;HD;2f z(|1!&Wmd`fXZx%6xSJ(Sk7mQ>MmGjCddw6epFv4wgGo8S%w}4yv2NJ;+3IQ@@3z@W zEddt|F~B7rE-O-PwzlVZ_wD+ln2s?qk8F0`!nLu-Bhf$)$py1-C;uf$&pmnM)aN^r zAV9&cCdacJG?Oo!1jh^bCnKWeD-}3q5oz50L4IMa#;=ieP~MHR`BH;5y(o7=_hZh) zEY*4Z9FZQ&vqwxg!=Vzytkh;jRKzLB?Mz|_zxEtz50{e8^iGUFMHRDrAw`*EY7 z`zDJpROd>po*^;FL0|sX_MZ0SJjzm0&9jo_@m+S;R5i<@lgoqO{8WplY{dB#eJK`e zNAPNvpIE7F7}`(@)>JRuF^mz*9yxYQHNwG>jhYcT!=7s5&X^Nsw3rS z4w|$Yv{E2QxxYqXJV*CeTTCd@6T-6l8{VpQ97PL6h#{OYnVOba*B)yK+6-M$M<7f5 zm~A#R(J+~P!-eT!GV^#jv!=~*Nu$(Rs?apPy-Wy`ippyfv{0gzr5 zh_wL9h~W+w;ERS-RLPt$KLVLuOO{8qRxnx~VIGBJmOjm*<4MM-%K2SB<3&@FbLuJ> zV(LH6xlDp4MbkuZ$|l^UT$CD8EmVL?0bNZ{K}!u#sfs2HDs;yY3w)00aKys3^?ICG zn0HSsR>p{hDh#6(cqRG}5m*DF6_zdyq=eTzs7FCr5Ev0b1PEBAN(1IB2JI)+a7Yjl z*RJ3o@BaE3w{~)@QqT)i>HbF0FvP|CU!tL2>aTxUApAv>6hR6gp&FsmIN}RP4~9^I z{jo1oVh?PRL@DBR1O9#4(gtk@kv*HlI2%4lLsLz^2gR@@3Amo#7(@EX12;l6n z7e$oViz2o&0G`vYUlm-aYt0h8Q|8fP zZFaS!)-X4?Xa`S+E)s(BV4|l&(Vp;2zk*G9v%qzs?5H|PgEbJyI3rVHP2NXT=#{e@ zg-5j1FLf6;)(Atk69AGz^3aP%zW5KdKBTjXj}c~u;G8cXM+D2xo|J6KGfAO^&7v@> zc;giIfgJL54jTb$XSLjhH?zJfYNS}*tU}eGIJiOt1So>72}6U{0QeA?%!+DEe-m5RR4~U` zi=ZPdqRp|+A3??!cdHW5L~I^d3z>!C+*o6{!f3cpv2YQ)X&LjAJu^ed?5bU3QxtbY z#DG86E>=pupT5j2YF@sue>+Kft@^}X0e25AntrL*SQ$b^?jJn{suKzEQ`;p(DZ%`6 zX6_?mO*hq(LND1Z;0Q?cMaeK$Mj>&tn=<4V%g6aC6l)DM{ zU=&!5BvUKsO&*DcpIl+~B1C{W^<<&|+do9pmx~xbTi#3EYFXmI6WgGkmjBJ{f9(nP5FqaFakQPgp=DFr>q8S z<)9y5Bg?wF>=Wx}^&-8jsnVLsaI+8;3oNx*53HM{a!^N94Ldp-GMziAGO%Diz#N8v zVf`v?TAzw-H)!fcCa@9vo@v&sHaB-vbRT#;fBO19@z_UkB@)08oAs%yWdd063gP;5 z^B2x>O)8NnTg-4ASby-DlN1<^1FN0B@>e_@jsrV8_$*;1Q5@KlWM%ZHA1Lz>c_&P1^_xqL{^~-v(ZlWjXt*R3QD+b(1*)fU4T$c@P@WVQ_$qmu z&RA}TdirUXX(B$aqnea**uWwc@G$a|=Zp9n)H)cV(%2wNp=@@>l81v;A)8*iviI?bXwt zoIi6qbxxRy=Pfeo zMxG(_lOpdIX|uSW$>X@>9&s5S7UGA6_+hdv5?|s)viwZ^MKP|X=-My%#wc@Mu^9EV z{V;Ra$tT@f^be1^yHt3|9EUfWZvVK|(L4#Q{L2&ez!1%Wy4}3yUkR7F3!lm|=HHy> zGCW?~ATigPo`lcrK`MHZCOhYXb($1bxK7s7xY3e&j8zXe%efwir$Mzgdvt=sd-Jl?slVRx=Hz0eZ%VER}Q%eo5|osrkwYxrJi<#7(T>1qn`e(yhPTQ zmSIRopHi!;nuk#xj*L;t-=;Un$>jhbOsrR^0%f>MYk!Nu(Tysol$c0 zK1h5_X;xG2PDe;>>WnUqOj|j#0Sn-Z6c-?ISOK8^a0yFq4OTZmny2j-3>q50<+Mvb z-R>?l+NpWDLvgm3GK}Ka*BS_!sJ_)nIXvUd33!WI6yLJji>=04(yn&b8c!VyuSj#% zNJUTG6j{P{JG@R;f2q(5p;Esuzsh<#T#;Oj_(mmGb06rAq@nIM)mjeKrds`!a^J&h zQr%8UXC$PE1Z=2ZhJ^kYJqH-om0E+EQcBuIp%iN?5alTWgjRnh)y%GBQ_pr+JAVnS zYCvQ@T!aC$6m($g{L$>SdP{iR4R~;-r`kI3;S`yNR~b#12b;5}8ghb$MC~^<=+a+J z<<-ATs z>+uTi(_W@OLkicFrFR6mE$8w%+^|9UdAkX(2Ca3To#>>LKQg5>%>5Lw1WujqAl2V! z!OPlB%?q$dZO+6kWq7-|nrie@@B`L~ffyun?PY)ztf<+ZvCn4~yX}p&jzO^tYLker zf4#G~+Us_@8~v0^Ye?vaaKrcE>S~M17aJY$rhPrN016O*j#K{Il#l~W_jtseL}AUq_SVv4PY3xt|3)0rUAF>wvJ=;m^&!7!jJW)Q;4q)wz3 zq4*}Vwb)qiZzzO7m?P+1XNg$OrcM|hDUKo4mzQZXle+Gwtj~I?Nm(Ldq16U!miV}n z`pX0HpbvPon%QU_2;y5d$Zu|>S67>2D$K11DiH6sp=XW3&3O|HNg1Vp2XKIPTe#$e zK*y4V-0M^v$#<%ob@S7ERc`aS+@iHmf5sY>bf^v>DgpojHbrJnw#T6?*WI>G*GTnR zE30C4J@VF$G>-@rkjsf1@O-PgKe!H(dW>?F114e3fz&FA=Q63)rd2C=S))3jF8Nf` z;+)0-(6RwU2qEaQfC@|3)(0`5gh(+UjG_tWfNO-OkiTDP+i+e(A-C!~5Y4w}A6&T# zrU@BW96hTSn;Xr)%%>Em251~U#+*lZfU#7($Jp%}0ff)?Qw?a3PuE~poW4oDtkErl z0iQa&^EJ7lHG0j|N>|Ma%^3^H)jfY(V8sUt{&{zKxn?R1W7lO4Akf?lG}OSyK<08 z5tV8Qq0UKZK}6G+N|8w_+H|<0DGHxBDgg(b7ukk19Ck*dbUWo?CGMT?8cX!m1TS|b zf`(6OU9w-k@n`O(=iCLYBMj3ve_6dZ7KqQFlV{xp?e&-lQ>B(AUV*IEzWpLPYV1g& zN9fUri%cWAXa4Lur~HaajS zTN_EPL7-Ji_z2%`aX1YNhRR4o2n5|b|GN*D zc#iCLB`dLVu+agabot_jtz?_<8mJMnW)xjkLBcv%WXSC=RBT>bY}&$0tMF)Q41C{; zz)_@ture}>CV(ftfD`Yq*fISH8wqfum3IB+C~i9Sq=^b5)DJkD3K|Nl60;#Okc{rD z-L?`c2awys@1TNIn3Ci<6=={}n*sL*)8LF$nX#7dw@RJwp~|EUdE7g$$+~JlhwqU! z<0`468TZt*rr8!zLnp%=CtwKDva3k#(&ST{F)z~~ZZ0z88~`k%0>7#APN5o^VS7p? zLwi(?7>tYil^jmHO#hJdmRTiip6JI~b7tLGm|NSN;v@NHaJ8F?Kj~lLpak`gAjQ?L z1dt{Yon!8CDb_6bq3x2T`isp@qu1*8vk1F1tQpX_hx%!PnUf;=j%T}rh3iBIE+c)x zDfn0yQX`R9q(1WF@6J%6^g?@*;VMJ|{kjxl0;G;>PjMO@?Xjse6O4 zP2+hgnYfw6X+xcGY+rk_C75i~DqkztHqBTj`k*U|*?YBF{65y8vFSwWv#lQ-*=*Um z1)oiBQg(+XOtP4PfAh4i!wXg#iuqd9j{h!t;=hgV(WdSD=z*Ub=4@`!%zp7OQ~t?V z5pW|+MJNOFh$lR%DV~~8ZbC6?8!u<|Y1K;9rQ2jZiPu&~@;%?Ph%7cw_m{7ltqI1? zN-!`<9%1+N{8OJ6ilGdhVkvEmm64{m%7J{4Ca-;M7qF8#6Cvz>q{a>Zj#^>)3(C1Y zUT7IfTyk!I#?gV#tb(w?(aG~03kmJ)C`ngT7-H6sxP+~G#%7fKT&e|%h%JgttIN?c zUK5=-wIShDCXpQ~?K|eRW36+Y>-*~*q9~n?dNcCf{<;$C=rupL!NPmi8(PjuYY~Fm zQ44IL-8 z5cp=mK7+ff0A=9eVDJQU5D?2x$+M6)w{@`%)SjVz#p^0|kYL3l`TtC(y zoe^mcW)nsjlr_NBf|k%lb3Jo~1F6hVCagogIfB{1E_pht`Psb{@>fZdx6n@#8Pb+o zKy8@OZ_@Z3xHDB7gQGS3g#~XcU;qc1*N#cCG4&jVC1sB@-x}@KQpyFL2MrQHtYu{+ zM|G2BKyvLGhAEz7gvI#H0bQ(ujFRLWgX;zuU$y_J)mcXklsMe<9rZ2kSl0C;l(u@q zh<2(vEIhs&rxEitM>`c40Not=z6^ixu`tJGG{r8|-oWaYZp_?BXHx08na`%pCYk1F ztu(g9Mi0&i1OJ##tP4H0mC??3LAsM=Z+L06WA0;^#ibpjK#)+8UrX%+Gfp{}V(aj% zEBfMeyo@CaJ~kv-+pN1?40N8Q&c|#IL!{wg&ncv+kOh8 zf|4yoK;>**W=o6

%88?sk~un(C#3wDekwFwO~&02?+~Z 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 gH-=At9y`TP63M@lIn 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 z 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%0AZLY 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

S-yc7*K1a$^Q&#uC!N!Q(vM|DOh}Cph-sba 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 At^fc4 diff --git a/priv/static/adminfe/static/js/app.d898cc2b.js b/priv/static/adminfe/static/js/app.d898cc2b.js new file mode 100644 index 0000000000000000000000000000000000000000..9d60db06b8d453dac55ccddd3d021d42175b15df GIT binary patch literal 185128 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

jD6s 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ℜM&joT@kCQKrZGf-gswXcJZaSu{nA0(9RRv$U_3 zTVr7aS~Z)tXJFVgFGEC@E2iHd?9ZgR%r-lMN6=qK*bN9{sv;QnTFBh|L?fIEhb0*% zXhI(IwjGrcjfjn9{%2i32b|hgY#VITEJJc9>#e`hlKY%DJNG&zJ36{?mlm2{dz$CA z=s8e+@!>b=RC4^WInqf02j6lES28jar2XK)IaRDkOPS91W~a@A$}j3)HN7u?F}pGH zO3s>X@1wcJU#3Jf8#B7Bg}XcaWt1wGtT-4snST8+jUzN9Ka%jfjj;aiTeG7u6?f`{ zpmuzM28}!501=#K>v@^#S_uhYeDLmE?(kADTV5{)sbUh-=<$tP)tXkcX|em2_yOaI z4hRtvdCSD)&3jwp@x#e(%kM@r{=QAqI)sbQT&h@D;6F;O9|`3|2?*aPgZui7W#m zoF7^ZBwqVT!I+I*5vlx}B1f4!`X`zC3aDe)$K4nZ7zY*f!t-3C)r`l1;5}dd~?lTK6;8rext1$4E@qRQ$bi6#@0 zbORIp*ghHZiLsQXvJpPHTi)jY6jQ!!E}5epk5wPV53HW^FRsEDcl2bR+b3~ew?=jnFOpMBJjn23 zjM`+p-ONYOOXThrKFTieII+v#xYwzsx*ycvY0q5%zqDCmnc0`1&ccLn5-UX++FwE25|JSTqy=bU(Fv$MY2jE$xd)K`V0{^RagbS zB%H>9l2!EJimsnxrDI4>DtVc2TjBlRw)#6&8V0ZZVk;LlnLILJXu^(FL@o2=+mmnA zT9{!UI)~_w`hKr{#b5NyED07-N2h!?zbQgifryugDEBZ-A$O+qezdBKCy(&6C3BXi zF1ld&F64u=a#cpD0lFB*x6-)ih9i7>euNK~euNL*tG4xRY-sL+edw=kcPv3vDU~2g z`m(Qj+fWC1Jq`3If%nAVjNdU+L+&}Q~w=Sau zS4B4SZ;XG=CCO{SaUgxuh<5Ba_UMQ0*d3d{h2Pk>L~)tFkhIv!-f=NNjcl=3SZb^hnM6ZRi5&!Tx?#Sn#fP{+CxK7F z?~trt3mAJfbauIljMfPP$;N8g(IRHhQ;8qS#?!=XSDNqGFd?{BIT;<2NuEDyOQ8;MpHo)(% z6NA@ifVq>R9mvt)>eO6-Znmls_+NvrX9p8_?xxF$3#cIA9L5A>{v*sHW`c`F2y_+{ z;)jygwi3SaTP1SvY#OSF8o*3jTo_=_4Pq{uY-N)d zR+{I>_cLsS^tAbT0XYhuwL4DK#Tyv#RNS>#Svx>_QM|lI`X7zg)?8nN(%H zV=uTnv>D}o9wUdB$CWJ#8J|w{ZTB*b0fXo2|}m4}?&O5C*= zgjGU@pq@8{%x6MU1?{D1#t*{#9Z`dQ_+b>R+Y&qan@rdsZ`Le6=$=K9giZEuPTdap zA&cVI0KesIUI|;qi^Lrc_^S+1 z#6JVypi5+ih;&SPRDy0T^H)G|2~rAoXZ)2#`pDn89H~2%XW(^EzX0;=4Iqz^t9>w-Q?=6>L#tc z(1xN+)!~qib1=)XqMMmz;b88EyRsexX4xt8nh75Pa}T5UHCnDXbUDt*`aTYBI9HA2 zx4a4F=VdTY1DGGe1n>EqgIVGDnxJm}HJG2VRA{7TPN-yW9eu?hi*D;&j?dw}7J3$| z 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 adeO6AmhXr=_a5B&@$OxG9j!A7rvC$uV~I8Z literal 0 HcmV?d00001 diff --git a/priv/static/adminfe/static/js/chunk-88c9.e3583744.js.map b/priv/static/adminfe/static/js/chunk-88c9.e3583744.js.map new file mode 100644 index 0000000000000000000000000000000000000000..20e503d0c633b4e7eda94948db6a3f78d548fc31 GIT binary patch literal 92387 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 literal 0 HcmV?d00001 diff --git a/priv/static/adminfe/static/js/chunk-cf57.3e45f57f.js b/priv/static/adminfe/static/js/chunk-cf57.3e45f57f.js new file mode 100644 index 0000000000000000000000000000000000000000..2b4fd918f45d545b6ff3ab7f795810df2c9a5ae2 GIT binary patch literal 29728 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

sk|UiM-x(gS+*xuxr`@1!XWV6<-bJY7n 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 P2lwv%f?|gBrt|*+`MzuY diff --git a/priv/static/adminfe/static/js/chunk-cf57.42b96339.js.map b/priv/static/adminfe/static/js/chunk-cf57.42b96339.js.map deleted file mode 100644 index 7471835b947c8cf23abd8484aa4f9b9e239cb3f7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 88026 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 R+tCj{ZQa3V&^nV~`ajqih{^x} diff --git a/priv/static/adminfe/static/js/chunk-e5cf.501d7902.js.map b/priv/static/adminfe/static/js/chunk-e5cf.501d7902.js.map deleted file mode 100644 index 60676bfe719200acd489a5411c786e4e973f27b5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 92386 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 Oyz2LpYg^Z@efj?>pmQSt diff --git a/priv/static/adminfe/static/js/runtime.cb26bbd1.js b/priv/static/adminfe/static/js/runtime.cb26bbd1.js new file mode 100644 index 0000000000000000000000000000000000000000..7180cc6e341ee1fa248750bafa195086b5fd0b4a GIT binary patch literal 3969 zcmai1TW{kw7JlDf;cx*%gc(NiO}>mUm=5M)2ko}l>AtMjC}`?%q#1>DDXM8|`@i?l zx+YDgKoFDS?K`*coO7cpEnRMO0QA(3M>^0rGdB25*pX$DnMCixJrnp7?OBdL(>=?Q zkQK>pzs=qlUV#4WvTZh*P??xrwrAB&?#JVTAFDD8`~+{H*RM&q^_SaGu1A9Yc=!U+ z@uM})xW^;#Oq4%9=%1`Pf_0BvilPVvvvfQTU}yJ05LpmO(2z6Vu&(ngJS*~0_ z1G=@~Dyv88ihzsbv3Lv=%g^T>;=nDJPF|dJ;sVo%Prp&^of<{zR$ZcYde3Zf6Ec}Z zTlvPd`=sCJ6($mg(r&|>B&l`+br9h8h+Zs?6fciOE+Npjw=ALz5)&9+;t5m^ZmA)| z<*NAPbiqeXw$d)YhfR24)sva>%I}0Q-e|cBECQs>MSjfT_oK0{-1<9R;y)#)e4|;h zxrtb_xH(-et-`<`x@z6EXPIOD*r4`!;Wf~GyBH2hi^RKyp zKN^ouL}r!#dK;!IMK%#@rqOIEhSP-_PUlM)Zk{W37;Ij}huLttlIy`Vn!#{7J*?$m zRjNfin6BcbU%gzR4kSo4JdDsg@ADFZPG-$q?s<5}8$uhmAP z`67k6>hG!bN76K0E1358#A*#s@HTjA1(TUvLJFw97Eh{{(--$&EkL5xJY3CF)$OlM zLDnk}B0Y%iduieANej zkYUKHJ@x0`IcM`jrj^e=4hHV^X zS-ORygjVR6na9$TL%+jocbeJ#g>DBIB({D?VfOBXcxInc+WhEH0fNg+*b-bqDyME& zgbD6*kEvV|O+#k5mgBMDxMSen+GvT);TI_$KqK@9!(7m~iH|X~y>Pj_`%>PK$!i=5 zk$_09=PgI7^+C)ReqM9rY8j3(@0T z)f@@4#cCGM`+FLWjMu4%7R&ygh9jk#O(Pli`x}m&!5ouCj9goDBvP+BM6$=-aO6@# zyjsU-$4p_i7q((sE*jGD3~lj*q>{k3G$bn+?Oes zF1tuZhQJ{8oQE-ueDIcFaxF1v$)^55k)vOYcm(!BxPK$mH^>08j(An`k7cS0eaRda zxq+gWYe@ZO@^lnpbyTx0dVVWhrwvT}!O7#^kS16NtEp)tE=VHL6y`p*S~lwxrg~cO z1oDJCL{|+qwFdWfu*T~QzfS=FOJpg-)7~mry80@_a1-DVQ%zG^r=W5TZ9(nV*a^&ewCPO(}5n>$OPlD|Kt5j{UL97mo(sN<_t8tI`!9~!qAmpf$kVV?TM;z>f);a2v*f&v^gu^# zY5q*wx81e3o*E+r|Ce8Bp95mDR3UOy<*VXDeZC^R+K` zQ{g!YL5cWMOjE}sXrveqGLi}j&N%7gb#|3f2Y`X-_?{KmWz_jaog?LnO2+DZR*xck zCbc)fMLRgK?t-4VKr3)^yypU!J)_8Y`wq?d@#EI%ItqqAI;D1Q)9AInPd<~>t9H>; zx>pGRlmvzGwIC@l?vVu@e~L~;phxXuSr*TO3MfVt^I>%EI;zgKLSwu&BQ~f6v;s_J iM{H231lNK}-m{1K<rkL|_{>xb{2CK#3^>TRNl?~Y>vsd$*>l3Ndj|bPxhttt!KxVvZ-gjY} zV5z*|oX5v}M404=?TdS3N;p#(7^CrGJR=gQwCF;78Rx$9sIs#vQTgBEW&{Lg6>aMqrE4+~`jVphu0HXE4MY8u- g#R;Q8w<2x)+v)lJ_hFc8g+1-2ai`d7K8xr5F9H^VjsO4v diff --git a/priv/static/adminfe/static/js/runtime.fa19e5d1.js b/priv/static/adminfe/static/js/runtime.fa19e5d1.js deleted file mode 100644 index b905e42e19aa329c594697f783983b87151c7707..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3969 zcmai1ZExc?68?U_!r=ji2s4ai$(CPMSZo*j;TG++xb1zpoKetxiL|SbE=4s>ZU6TU z^=faEEs$7z=gc#lXJ&3xt+^|V4kSMI?THRF^o$LDV|HQ*Co0qT;f@&iL_1QzXTBqO z7Lqc%-)-|Zik4D8xO|(>W|(K{KHri0p6{m9k{;?R4}1qsz|CkDZvEkQQs{}nKkh$E z?&!f9XWa8a(wtR4KIxyVIY{fCDQ9IF7-I2s8pwOQ%MASB<^(aF3J`T$_SfT*R(YXZ zfCId>($!W^SR(;theP=sFjAaO_aFx>IS=|mXHZuX4f5$XtUptOO2e$n*uCBno85$j zXVI3was4{(*LlE9=0MwRc#~!Iy#WjYSRUcCg%+dLp)9xz^zAK)Fp-%G6s=$ffCEcf zk>$AoK0BV_lB2D)D<0)0Jd^sW$2|Ch63QE`P=N&jad(h!Q~CSBSXXWRnJ)7~*|FGY zQf_V{(oJrT=W`F3^qU&gUB8z&GPV_N_uPnjp+TnQ(*$1UV^PUq&{-2Vz7m@SN5AiMl6ZzFnjKO4#~<&|2E~y8 z8Dxma#}DrPW&eNqzrMTdUtxomi;%D6Dni#Fp?DoHlLe?Tj5lbZ1zV_f8js^GY)B*v zu}}AMSl{A@gb3G4&d2dot>u-xojfhTXwH-p%WyuNBh+&K()!i{i0@gM1`%PX6xh8e<4*bZMB#G)x*Ttu=O}7NEG68ufIlsbdy^9$U1s{B+`ANbjQ` z{MG<{4~G@{Ve@|** z5t9*LCp|~1^`0dQ-)}i`wTy&{LetYsQvdzH5$5(lFsn09SS69Y-$tA_^C5 zaij-2n;%Lbp8BUeeN_7NZD?l^J@ z47*yVVB|q=AMub(xa6F(o+Ce3=nUy0(B2sB@X-v;>07PG)@4a}DVhDbeKSGv@+0?Y z3WUqSlYt??Nj>L*PXiykMG#yoa9Xr!2QYB-s~!)6z2NTOaPwftiV8(g0g z2T5+g@VRlR-%KGUA!J7_+rrE5l^f8&M;08t+#A$63nDEwZOj=+B$~^_`&Q3p17K{V z6;B{n++l`nu*)^5&x17_r|4rQ;g$lwigL-k@mHJHr98KY=1U|Et7?agh_c2p? znoayHGELC|FRs%`PN)8ljyH{)yy0Ee3D+`bpy8=we;fb?Px%$m9YKH|t4oKzM!Z#C zg4Xj!psK-e1X<#t{_~c=%?W=ijYAun!Np?)`c?H^qG?{b9G>#%xwuyWX0B3lmV2M+kf2M{c}>8+H&~=WCP$D Y|NYPRSy#nT#iF%bh=_i!=p#G)3G0m)X8-^I From f6dc33615bb6a27cd0a963bb8a610bb30bd6d619 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 31 Mar 2020 14:29:43 -0500 Subject: [PATCH 81/88] Add imagemagick to Docker image to fix broken mogrify plugin --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4f7f12716..b21f86fcd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,7 +33,7 @@ ARG DATA=/var/lib/pleroma RUN echo "http://nl.alpinelinux.org/alpine/latest-stable/community" >> /etc/apk/repositories &&\ apk update &&\ - apk add ncurses postgresql-client &&\ + apk add imagemagick ncurses postgresql-client &&\ adduser --system --shell /bin/false --home ${HOME} pleroma &&\ mkdir -p ${DATA}/uploads &&\ mkdir -p ${DATA}/static &&\ From 2553400a662de7170dd56ee0950a6c1bb1513e45 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Sun, 29 Mar 2020 22:01:49 +0200 Subject: [PATCH 82/88] Initial failing test statement against funkwhale channels --- .../mastodon_api/views/account_view_test.exs | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs index 0d1c3ecb3..8d00e3c21 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -5,13 +5,19 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do use Pleroma.DataCase - import Pleroma.Factory - alias Pleroma.User alias Pleroma.UserRelationship alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI.AccountView + import Pleroma.Factory + import Tesla.Mock + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + test "Represent a user account" do source_data = %{ "tag" => [ @@ -164,6 +170,17 @@ test "Represent a Service(bot) account" do assert expected == AccountView.render("show.json", %{user: user}) end + test "Represent a Funkwhale channel" do + {:ok, user} = + User.get_or_fetch_by_ap_id( + "https://channels.tests.funkwhale.audio/federation/actors/compositions" + ) + + assert represented = AccountView.render("show.json", %{user: user}) + assert represented.acct == "compositions@channels.tests.funkwhale.audio" + assert represented.url == "https://channels.tests.funkwhale.audio/channels/compositions" + end + test "Represent a deactivated user for an admin" do admin = insert(:user, is_admin: true) deactivated_user = insert(:user, deactivated: true) From b30fb1f3bbf8fb8e49cc5276225dc09771c79477 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Sun, 29 Mar 2020 22:30:50 +0200 Subject: [PATCH 83/88] User: Fix use of source_data in profile_url/1 --- lib/pleroma/user.ex | 5 +++-- test/web/mastodon_api/views/account_view_test.exs | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index d9aa54057..ca0bfca11 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -305,7 +305,8 @@ def banner_url(user, options \\ []) do end end - def profile_url(%User{source_data: %{"url" => url}}), do: url + def profile_url(%User{uri: url}) when url != nil, do: url + def profile_url(%User{source_data: %{"url" => url}}) when is_binary(url), do: url def profile_url(%User{ap_id: ap_id}), do: ap_id def profile_url(_), do: nil @@ -314,7 +315,7 @@ def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}" def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers" - @spec ap_following(User.t()) :: Sring.t() + @spec ap_following(User.t()) :: String.t() def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa def ap_following(%User{} = user), do: "#{ap_id(user)}/following" diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs index 8d00e3c21..ef3f3eff1 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -178,7 +178,9 @@ test "Represent a Funkwhale channel" do assert represented = AccountView.render("show.json", %{user: user}) assert represented.acct == "compositions@channels.tests.funkwhale.audio" - assert represented.url == "https://channels.tests.funkwhale.audio/channels/compositions" + # assert represented.url == "https://channels.tests.funkwhale.audio/channels/compositions" + assert represented.url == + "https://channels.tests.funkwhale.audio/federation/actors/compositions" end test "Represent a deactivated user for an admin" do From 185520d1b4d3fdf8ecde7814faec92bbb531ce59 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Mon, 30 Mar 2020 02:01:09 +0200 Subject: [PATCH 84/88] Provide known-good user.uri, remove User.profile_url/1 --- lib/pleroma/user.ex | 5 ----- lib/pleroma/web/activity_pub/activity_pub.ex | 13 +++++++++++++ lib/pleroma/web/mastodon_api/views/account_view.ex | 4 ++-- lib/pleroma/web/metadata/opengraph.ex | 2 +- .../static_fe/static_fe/_user_card.html.eex | 2 +- .../templates/static_fe/static_fe/profile.html.eex | 2 +- test/web/mastodon_api/views/account_view_test.exs | 4 +--- 7 files changed, 19 insertions(+), 13 deletions(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index ca0bfca11..ff828aa17 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -305,11 +305,6 @@ def banner_url(user, options \\ []) do end end - def profile_url(%User{uri: url}) when url != nil, do: url - def profile_url(%User{source_data: %{"url" => url}}) when is_binary(url), do: url - def profile_url(%User{ap_id: ap_id}), do: ap_id - def profile_url(_), do: nil - def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}" def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 9c0f5d771..53b6ad654 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1379,6 +1379,18 @@ def upload(file, opts \\ []) do end end + @spec get_actor_url(any()) :: binary() | nil + defp get_actor_url(url) when is_binary(url), do: url + defp get_actor_url(%{"href" => href}) when is_binary(href), do: href + + defp get_actor_url(url) when is_list(url) do + url + |> List.first() + |> get_actor_url() + end + + defp get_actor_url(_url), do: nil + defp object_to_user_data(data) do avatar = data["icon"]["url"] && @@ -1408,6 +1420,7 @@ defp object_to_user_data(data) do user_data = %{ ap_id: data["id"], + uri: get_actor_url(data["url"]), ap_enabled: true, source_data: data, banner: banner, diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 0efcabc01..c482bba64 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -43,7 +43,7 @@ def render("mention.json", %{user: user}) do id: to_string(user.id), acct: user.nickname, username: username_from_nickname(user.nickname), - url: User.profile_url(user) + url: user.uri || user.ap_id } end @@ -207,7 +207,7 @@ defp do_render("show.json", %{user: user} = opts) do following_count: following_count, statuses_count: user.note_count, note: user.bio || "", - url: User.profile_url(user), + url: user.uri || user.ap_id, avatar: image, avatar_static: image, header: header, diff --git a/lib/pleroma/web/metadata/opengraph.ex b/lib/pleroma/web/metadata/opengraph.ex index 21446ac77..68c871e71 100644 --- a/lib/pleroma/web/metadata/opengraph.ex +++ b/lib/pleroma/web/metadata/opengraph.ex @@ -68,7 +68,7 @@ def build_tags(%{user: user}) do property: "og:title", content: Utils.user_name_string(user) ], []}, - {:meta, [property: "og:url", content: User.profile_url(user)], []}, + {:meta, [property: "og:url", content: user.uri || user.ap_id], []}, {:meta, [property: "og:description", content: truncated_bio], []}, {:meta, [property: "og:type", content: "website"], []}, {:meta, [property: "og:image", content: Utils.attachment_url(User.avatar_url(user))], []}, diff --git a/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex index c7789f9ac..2a7582d45 100644 --- a/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex +++ b/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex @@ -1,5 +1,5 @@

- +
diff --git a/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex index 94063c92d..e7d2aecad 100644 --- a/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex +++ b/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex @@ -8,7 +8,7 @@ <%= raw Formatter.emojify(@user.name, emoji_for_user(@user)) %> | - <%= link "@#{@user.nickname}@#{Endpoint.host()}", to: User.profile_url(@user) %> + <%= link "@#{@user.nickname}@#{Endpoint.host()}", to: (@user.uri || @user.ap_id) %>

<%= raw @user.bio %>

diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs index ef3f3eff1..8d00e3c21 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -178,9 +178,7 @@ test "Represent a Funkwhale channel" do assert represented = AccountView.render("show.json", %{user: user}) assert represented.acct == "compositions@channels.tests.funkwhale.audio" - # assert represented.url == "https://channels.tests.funkwhale.audio/channels/compositions" - assert represented.url == - "https://channels.tests.funkwhale.audio/federation/actors/compositions" + assert represented.url == "https://channels.tests.funkwhale.audio/channels/compositions" end test "Represent a deactivated user for an admin" do From d3cd3b96bff4c8ba205d4699eb8cf9d1b6fd5a7d Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 31 Mar 2020 17:28:41 -0500 Subject: [PATCH 85/88] Remove problematic --cache-from argument --- .gitlab-ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1b7c03ebb..e4bd8d282 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -288,7 +288,7 @@ docker: - export CI_VCS_REF=$CI_COMMIT_SHORT_SHA allow_failure: true script: - - docker build --cache-from $IMAGE_TAG_SLUG --build-arg VCS_REF=$CI_VCS_REF --build-arg BUILD_DATE=$CI_JOB_TIMESTAMP -t $IMAGE_TAG -t $IMAGE_TAG_SLUG -t $IMAGE_TAG_LATEST . + - docker build --build-arg VCS_REF=$CI_VCS_REF --build-arg BUILD_DATE=$CI_JOB_TIMESTAMP -t $IMAGE_TAG -t $IMAGE_TAG_SLUG -t $IMAGE_TAG_LATEST . - docker push $IMAGE_TAG - docker push $IMAGE_TAG_SLUG - docker push $IMAGE_TAG_LATEST @@ -306,7 +306,7 @@ docker-stable: before_script: *before-docker allow_failure: true script: - - docker build --cache-from $IMAGE_TAG_SLUG --build-arg VCS_REF=$CI_VCS_REF --build-arg BUILD_DATE=$CI_JOB_TIMESTAMP -t $IMAGE_TAG -t $IMAGE_TAG_SLUG -t $IMAGE_TAG_LATEST_STABLE . + - docker build --build-arg VCS_REF=$CI_VCS_REF --build-arg BUILD_DATE=$CI_JOB_TIMESTAMP -t $IMAGE_TAG -t $IMAGE_TAG_SLUG -t $IMAGE_TAG_LATEST_STABLE . - docker push $IMAGE_TAG - docker push $IMAGE_TAG_SLUG - docker push $IMAGE_TAG_LATEST_STABLE @@ -324,7 +324,7 @@ docker-release: before_script: *before-docker allow_failure: true script: - - docker build --cache-from $IMAGE_TAG_SLUG --build-arg VCS_REF=$CI_VCS_REF --build-arg BUILD_DATE=$CI_JOB_TIMESTAMP -t $IMAGE_TAG -t $IMAGE_TAG_SLUG . + - docker build --build-arg VCS_REF=$CI_VCS_REF --build-arg BUILD_DATE=$CI_JOB_TIMESTAMP -t $IMAGE_TAG -t $IMAGE_TAG_SLUG . - docker push $IMAGE_TAG - docker push $IMAGE_TAG_SLUG tags: From c2715ed77269bea1eb70d0d5e4b00e7d86eed854 Mon Sep 17 00:00:00 2001 From: jp Date: Tue, 31 Mar 2020 21:31:23 -0400 Subject: [PATCH 86/88] add imagemagick and update inherited container to alpine:3.11 --- .gitlab-ci.yml | 7 ++++--- Dockerfile | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e4bd8d282..6785c05f9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -288,7 +288,7 @@ docker: - export CI_VCS_REF=$CI_COMMIT_SHORT_SHA allow_failure: true script: - - docker build --build-arg VCS_REF=$CI_VCS_REF --build-arg BUILD_DATE=$CI_JOB_TIMESTAMP -t $IMAGE_TAG -t $IMAGE_TAG_SLUG -t $IMAGE_TAG_LATEST . + - docker build --cache-from $IMAGE_TAG_SLUG --build-arg VCS_REF=$CI_VCS_REF --build-arg BUILD_DATE=$CI_JOB_TIMESTAMP -t $IMAGE_TAG -t $IMAGE_TAG_SLUG -t $IMAGE_TAG_LATEST . - docker push $IMAGE_TAG - docker push $IMAGE_TAG_SLUG - docker push $IMAGE_TAG_LATEST @@ -296,6 +296,7 @@ docker: - dind only: - develop@pleroma/pleroma + - /^ops/.*$/@jp/pleroma docker-stable: stage: docker @@ -306,7 +307,7 @@ docker-stable: before_script: *before-docker allow_failure: true script: - - docker build --build-arg VCS_REF=$CI_VCS_REF --build-arg BUILD_DATE=$CI_JOB_TIMESTAMP -t $IMAGE_TAG -t $IMAGE_TAG_SLUG -t $IMAGE_TAG_LATEST_STABLE . + - docker build --cache-from $IMAGE_TAG_SLUG --build-arg VCS_REF=$CI_VCS_REF --build-arg BUILD_DATE=$CI_JOB_TIMESTAMP -t $IMAGE_TAG -t $IMAGE_TAG_SLUG -t $IMAGE_TAG_LATEST_STABLE . - docker push $IMAGE_TAG - docker push $IMAGE_TAG_SLUG - docker push $IMAGE_TAG_LATEST_STABLE @@ -324,7 +325,7 @@ docker-release: before_script: *before-docker allow_failure: true script: - - docker build --build-arg VCS_REF=$CI_VCS_REF --build-arg BUILD_DATE=$CI_JOB_TIMESTAMP -t $IMAGE_TAG -t $IMAGE_TAG_SLUG . + - docker build --cache-from $IMAGE_TAG_SLUG --build-arg VCS_REF=$CI_VCS_REF --build-arg BUILD_DATE=$CI_JOB_TIMESTAMP -t $IMAGE_TAG -t $IMAGE_TAG_SLUG . - docker push $IMAGE_TAG - docker push $IMAGE_TAG_SLUG tags: diff --git a/Dockerfile b/Dockerfile index b21f86fcd..29931a5e3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ RUN apk add git gcc g++ musl-dev make &&\ mkdir release &&\ mix release --path release -FROM alpine:3.9 +FROM alpine:3.11 ARG BUILD_DATE ARG VCS_REF From bcaaba4660c7f2f31756bbd64ed93fcd8e0b1d85 Mon Sep 17 00:00:00 2001 From: jp Date: Tue, 31 Mar 2020 22:16:36 -0400 Subject: [PATCH 87/88] remove testing `only:` in docker build --- .gitlab-ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6785c05f9..1b7c03ebb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -296,7 +296,6 @@ docker: - dind only: - develop@pleroma/pleroma - - /^ops/.*$/@jp/pleroma docker-stable: stage: docker From 94ddbe4098e167f9537d168261a6cc76fa17508b Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Wed, 1 Apr 2020 09:55:05 +0300 Subject: [PATCH 88/88] restrict remote users from indexing --- lib/pleroma/web/metadata.ex | 7 +++++- lib/pleroma/web/metadata/restrict_indexing.ex | 25 +++++++++++++++++++ test/web/metadata/metadata_test.exs | 25 +++++++++++++++++++ test/web/metadata/restrict_indexing_test.exs | 21 ++++++++++++++++ 4 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 lib/pleroma/web/metadata/restrict_indexing.ex create mode 100644 test/web/metadata/metadata_test.exs create mode 100644 test/web/metadata/restrict_indexing_test.exs diff --git a/lib/pleroma/web/metadata.ex b/lib/pleroma/web/metadata.ex index c9aac27dc..a9f70c43e 100644 --- a/lib/pleroma/web/metadata.ex +++ b/lib/pleroma/web/metadata.ex @@ -6,7 +6,12 @@ defmodule Pleroma.Web.Metadata do alias Phoenix.HTML def build_tags(params) do - Enum.reduce(Pleroma.Config.get([__MODULE__, :providers], []), "", fn parser, acc -> + providers = [ + Pleroma.Web.Metadata.Providers.RestrictIndexing + | Pleroma.Config.get([__MODULE__, :providers], []) + ] + + Enum.reduce(providers, "", fn parser, acc -> rendered_html = params |> parser.build_tags() diff --git a/lib/pleroma/web/metadata/restrict_indexing.ex b/lib/pleroma/web/metadata/restrict_indexing.ex new file mode 100644 index 000000000..f15607896 --- /dev/null +++ b/lib/pleroma/web/metadata/restrict_indexing.ex @@ -0,0 +1,25 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.RestrictIndexing do + @behaviour Pleroma.Web.Metadata.Providers.Provider + + @moduledoc """ + Restricts indexing of remote users. + """ + + @impl true + def build_tags(%{user: %{local: false}}) do + [ + {:meta, + [ + name: "robots", + content: "noindex, noarchive" + ], []} + ] + end + + @impl true + def build_tags(%{user: %{local: true}}), do: [] +end diff --git a/test/web/metadata/metadata_test.exs b/test/web/metadata/metadata_test.exs new file mode 100644 index 000000000..3f8b29e58 --- /dev/null +++ b/test/web/metadata/metadata_test.exs @@ -0,0 +1,25 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MetadataTest do + use Pleroma.DataCase, async: true + + import Pleroma.Factory + + describe "restrict indexing remote users" do + test "for remote user" do + user = insert(:user, local: false) + + assert Pleroma.Web.Metadata.build_tags(%{user: user}) =~ + "" + end + + test "for local user" do + user = insert(:user) + + refute Pleroma.Web.Metadata.build_tags(%{user: user}) =~ + "" + end + end +end diff --git a/test/web/metadata/restrict_indexing_test.exs b/test/web/metadata/restrict_indexing_test.exs new file mode 100644 index 000000000..aad0bac42 --- /dev/null +++ b/test/web/metadata/restrict_indexing_test.exs @@ -0,0 +1,21 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.RestrictIndexingTest do + use ExUnit.Case, async: true + + describe "build_tags/1" do + test "for remote user" do + assert Pleroma.Web.Metadata.Providers.RestrictIndexing.build_tags(%{ + user: %Pleroma.User{local: false} + }) == [{:meta, [name: "robots", content: "noindex, noarchive"], []}] + end + + test "for local user" do + assert Pleroma.Web.Metadata.Providers.RestrictIndexing.build_tags(%{ + user: %Pleroma.User{local: true} + }) == [] + end + end +end

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 delta 78 zcmaFa&Hl2ReM2!LvvHcyv5$372xBxiF*#*3WwQ)XCZ_^1FtarH fSp*0%Wi~lAIWjhrkCq&h>HH>>-^miQ>HPT+&(0jO delta 73 zcmaDfo8{4LmIq|BRs$dfpAe}b)6clVZ)7Wvq`yED9T5l@?) zZN6vR+fPPk@9V(m4vW|mK@yw7=5}s98AZ+;xyS-f82dApHDWh6jrU@Z7}kF7#Aa+c zY;7BRt)13OX|qztttXVuUE_G3a!+u-!5Ssiuw(ZV`(!iMxJw2TAUCcS(I5%$4LfsN z|M#DKYX$%Ntu?X}`17v0b>E)fwVQjaIlKorumgbGgS&Qz{tWTY1UTN%--Vm9Ea#`O zuszqHBMjFSi?Yb`g0+2m<$({3XxVfo+z7#{6?YrJ;HCe4rN{1R5 z7ey8~#5l+e%e3sFI>$+~L?n0@MUd<2-E$_!PQ>PRq<#%zd*qfLGp#43&UboD za3@Y4O>4bh(i^EM$m6`UQtXq&#)jD3G_@1>YGssS;1h1G%fA{kR1yqLBQBx=&$P#c zRT{GoXzM8;l^^!~8L|;TjL##p1=H}4xSQx^uuFDW9PmJ+Iw5~L-e)&>` zGh<=F`@;iK{$^TTu`k@zaZ9^(4mOM6M{@C~e8FidxNoMAjPK*I~ zK$RkcvdhZLGdFW*N#Hlya2kh#5)Mn#aAC&K26ReH!CoaUnnfv0g5a`ZwaWkPLQ3WT zYOhxQ=B8+R|8^5Czktj|_X&4oT4)3+ESRIT~8bGJ8znBbmevb zATo11LC~|TZZ4BWABZ|tRyM-YLMUK%FaS!|tl1sZ?3~x{Y3D9p6R*o$mW_&t$0CQ; zgtcrJuE0(S#L5j}X;J<5s$@K1|G`FX*%P`>ur|RmQ012zUa}5=Wv!Iy6QQPGuv&Id zGIDnYfnTlKb_y*Um?51uVkhLx$9|KzrtcciZoaZW7(v{Kt%I>SGh=K)h)GcnDGb?X zh>Qgx)(tWqpq>)x4lR2Ql^c6J)L9K7mzlNUy5Z)g2Zv^5fI~|rH**WfzcM~+&@l{Q zOx0Dqu=pV8E=|ZH3)t6dOZ5wk2u!HOSvoP0bQImTe?8T5Cj-~WqL#z$(QjQE77QaA zZxCkCcQ)&V045U5z_AF*p=V(i-VzHJk&R?pzdkWcrhh5{HytiQ|DO#O(|C*f+MlN65rIvH@ zTax8}U6b-P$S7_182OAt{QUZbsa@i|_T}ZqEcQd*;{!0z<5x17PGiUyH#99hjG@g7 zM>-VT`85#ZlOPmuX;>5Nopu|3M(CmEXVtCugFIyr57RiouqYELHJ}uJ!K!?UM{qGt z{Ji@VCt1Y8V9BxYAxEQBo2VN!0|PpI`+tSy3{gOs#x4BUJU?&tdX4w*yHR9-)}L6V zwUzTFahQ`tfjC^4PjFx+92b($5W;j6&I2yJ&I9bc zyj%(d^bgs2U<-TT;7d5m4`zmq;Shm$6tX<;LK`bvP7_ME=_RpXK@8jG+P(vcamTRl zKmqKca0vYP4U$63KyEwF{sK#^d&o|dFdC4jf9cei&0d_$!zLI74FR9F)APMD-x$v{n#gg@*X~6~s>n-re zbizG(0FQmT1Z3$W&4u<4J7&i79O|}<4|8x1^j7BqR%>cII?f=bhAj9Dtf!XP9A!z7 zf)A#A(dCY8o3=8OdZ+?Rz1&f~RBNu%w^7%ubP6Z%pDPqj9C*{1Mcghe4SX2!Ys>D? zkLU4)y9U?*|4X2#){OG3XJMf>+JFp%4qlHQMMbIyhyQ8seGwW}*nbinqW%~jbwRN7 z9t?e9Betn#3Z8)={inwQq^jx z*@8q!0pZkUjctqrpEr}ZnZsfqLY_&UjkayzZ-6dAWT&mxG67=})-yYYp&o*!a!s{E zwt(lht^qw~W;akYv*BrUE>z%+BE_I|Ye&~!8_C91BVjy$kWd$^CgV>h03@Bku?9$_j!hqd>0=k3Zse4z3%Fh^&DK$Z0D(BXUG!4d8XB$9+a*byp1}KyF9L7LgN@)&Pc$o%%x} zXGGQj#`wd{6?t`#6S)FB^k--Oor3NuXKDbYKfikg0|5;4l(YtBKz_G1r<;2-Qmz3E zd$_}4x}&s;c?I}*c5i+GD;tn~b+!i3=i>M98|k~VyXc;u#S0SU@EqMKP{QO zF)`IJ0<>zh;_GcTArWOo1;CJly13@IlFUPr)12O`w59dTK-CqJjOvlOailm1s9W@XDdBmrOQhFGW zat)wOhl>*`q@nVG2Cz!w#Vz&TVb+w9el?uLqz(bn_zc5j?oFrUdX!ShT*F98tHzr1 zE`vGgRW(-uBQ)~*>WIiAb*2V@dml6E zS0jayCJkdOUZ3A=lO`353K(Pd&Ue%Ik~~(as{xF~_Hl>W(pbep1L*9l-H?1cRxMEj zc=p%%B`mMdcgE^$4J?`r8+IqVCBKgmp!-)?fR4KEjW4B+KsibSn7oHKBQg)(Y{^lp z0Zi0E8qhop;LS1ydNBV;y-oroQo|HbN=-15hdVo;sfCKGwdP==#ss@?{uvAWG0VT*( zm5K&1JRgouT68rP9u3H$GKvL}%b`*NAvVX$caIm69IDQ!0X#cTx0gg#XKMfh^I;Z# zC9>)u8bGHO-yUhWgvzNEfDb;#$1lsal#W!^Y5=pAo#vF5k;+;PpwHRG^qQoV&ozLx z_ii>LpGT_pG=M%2Zw1*MDW7WqOQJt$Q@kTp5*omK-QBvN<`(762)+FM-T7y_3<8t` zHH-i)g}(c}eN9CZDc@-THI9?`gfuF36%b?G2K{piYpmj?0d#GDGNq9bE7xiOowJp< zM^bqlHs!+hYB8nsmjGS2h7q8p(1XSC0-rgZR;)Z&0V#u*0KoCKwM9iQ0n(u{yvO9m zrx}u#yFvw^i{3_uv___)Il!>A#U-sY5+IE=OdU~5WthR?Vot+0QyHuQte)`OQjt~l zR6ve>>-*~qnt{3MTN*$&w_1Btxw&$)1~7x)u$w*UqFiOL1~7y979$VlDuXqE&Kcd@ zd?ab*91Z*dk8-1Mt``7m8&-9ZDcTcj##hS(e*3 zG~-367&L-$=q2xI_6pTWG_b^=EITJu0%EDcTtjepJYGzdk;`gl5MQ?GDMcuZu2zQ$ zse}u7pNlnld&y~`kpMAi46`z0XLR90Wu*qtp6m32oK`4%G=SUhS)$6VSe6UUyNlho zG71W!Lu)7jl_c!_FdME;NNA=?rvm29H0Om}j9cG!sB4uNX{z7_Hh|&sGxfTKY5*0m z9FtGJ^-|)H0O_b>V|2?E6Mbm~FpZUG>X^WEpY~HqSYQ$=<0V%6J_y5)!SPiCcQgDb zyLz!z|Go!O@N?{q+)3bbfBhW`{5k=BmB2V+ah!<85MF^a;B9mf^2R^m9oCldPxxX4 zeffYp9=xV@jq`r*`t11OQ~%4uN&os26f`eCj*jod{hB%OO1bvshwAk+eGVb1@L~1Z zX7K%sJPBuTcX%0AerbXM9>Ay8_*Mnp@7-Jb|I=#yqmd^?=J9j*1QcFmU0#*ZkeiTSTBt(#)2~d|u-~IjT=tcvi zd}MZJ=e^%#XDzU+>#nXYG|-JNOXtBf3FGn7`u*}!63?dHAX!@fxO5S8CVqFgUaQrr zwPf0@orl3iQX9p+VCttpXh(6s7LI$tW%Ya({Pwth8Z4FbXk&D417j8iNiB+deln=` zf=)aecLPN|ONvO}!OLG;UHc1Yoo?_nkM?a|`TpNyi6TbM2%~ruj8oX6>qmi?T#dWl z|2aOc9v|1LcYj*?R9+hUBe8$m#cPkkaoFxANjtbqr+znWPoh~r9JhO6lEM)_oYFo^ z@;KC8LF^{#Fbz)ItE=tRd{6k#^o+ueUMB4%y^4Z2agx4T%wPhL!-Pg}-Ce%O1sR*0_4ZN5sn@g(TA()?Va5>d;5p2&Gzoz=FzKGyWVcY+3mIt6;S;= z3#Y(9>lpvvkK&FW;bQCPb+g^t-{0GBAGZEDM9b^OYy6vBjXH63==X6w4GyQ_h%a`M z4Zrt1xHLb)u|K`iAA8-DNuExF;Eg{`f}L>+&?LJg$hP6U*EtKi)PY(Lg6I^NiTs(y zF#)zP7|DXTjJfk+e2`AVG08NU#%WBZ#C4iAk1_twgI%8l-1XA|{*3sC0zx8&h{eC- z_yYfK`YDkJ6D!WSAI+GmA^v(c>I74#)2nbu9WYMFf?;yNw!&qc4!9$e*dv9e!6fp# z!ePT0D-9A-FNs2iNESjva5;&mDP^5gXpTP@en=fPj&K$EN!pn9sS8~m4nn$whVP~4 zG)QIX9yCng0Mxxg?EBe^_;5n9t}%<3~5IYk-Z zk1xWc5Cn3eVTP=G8jb^E?N25V1xGT7XHieM;wQ;=5F*dMf(j&>woO}?6T~QC8VOpx zJ_(%!DM=g8NVRD&@{w}Jebw1yZNdu^<|48~wi%@~G7$-gKanV_WV=3uAnskXPcsgu z(JZ0J;c0~?31?c#MsRl;PLSYeU?O{=d(TgO`4jA;RRs5^T_QD0I54`i=@b&Z@fq5U zN0V8~3Wv#V+zBJX`6)@`DI=#6n=m7h4bCN8C~t~_nPjhGvOQ)!N{6a_4`H5UzX3#- z(;)2*f}Zrp0OA1)&`uhRk|t(9vM>oNA=Y8@_)H1_Wf(KGI}IQoS-q{+9r)v36m04& zdH|bt2Rmb^h(0pKwvDrJaq3H`ex3!KLRd5i_C8DpTVa%n3?q_>xnMq-_>i9D2+~2^ zBN4~(bmT|jHAi!j`Xe$G1(>;(SZw)fNX|5jxG~%E^+{Vkc;CnT<%`KHbT>=YM)bPZIfC*iAh^gKw(R*G}79d z6x${y!fq3TpR{kEGaUc?MBV)~70*yO`yGxZPCtViH;l@N9fIp48mSeiN}=$p&tdNm4IlvW*HDA zq(|gmNfwz+$fN7|0r^vKE{4xT4MFW(u?B3n5ky2jn`kwM^F%xH$z;`Xsr0V1s|;Eh($vHqh8IpVXH#yDuAJr(#Va?De5X0F&57Hb(&Wdz({a|@Ua3IW zrlCXnRcoEVHc%cxL*F(~ETCaz8-zp0lJCUQBep%n0G+9AAeVziVj8+4eAi0el^)%! z2SsYN);+f<6q;zTrs)Zdo~6+fT@Wms0jf2=GWhtx*@c_CgRI$J3CEu0$fKuP3nJ4a z7*`IqSqi>#n*Et+_GeDBs)^p2rufnCnBM)4(>oOT(IK?%pdS?35QDCq-eYkkn#b0g zfp{XYom|4>Y*%jp-uud(8>S>ic6(^ipC!B_iavBaNoK9gB=}T9(JAP2Mtk$T5b$o9(3RoEIks90t*Bhb zpuxI;OGGrJ#w>hg@vUdh#0qED3TG0|XEvON(qm|Q3~5TJ)`mkvZ732Bt$0Hj>O(u! zhvLkkb>>hM9a=?);+~;(Pb9$}*+`FsMr3JlNn5Q&R+mUbkF4lC(Nt?A>w%GYU}QZo z5?w}Cmy!5uOFiI*TQG&0GaYoi9BN-JVE3fHpbsme(W2^I6 zLTGFi7>ju0sG=E|uMDrTm3k~vkFC@bzry!7)f!(JjET^jSj344IY}!W(UY%icPsl; zyfL-jn2JZI)}x6Gr^Lo+BFZFInMAyiSZ^fajl_B*5%Cf$UhK>E z(^48$CX%iA$|6RlG3zP%r8e5qYta&aE!wpV!1UTlPv(cy$z0C*m9E4GU)g4{{>)l` zCcI~s_e|!F*>#o$_1e;~bIQ3m_uM-7vNB16na(HXhrzp&aJsVLbUiRK@|Ed_9|IAS zubk#oT!-dVy{D@j%^0@|m*gZYci`WA^L~)T(K$sXNkD(ijcpfoFn*%8rLaNe%l)w8 zg#vDZk`jxKOBdUXMx(rge;VZ${%P{R<+p`~tup%E3(9L7%|2~9T$jdEF0c^>iR{5nxe?j6m zTKw-eOJDA?hNJ>X|EBS(yoZ+E@=;?CEg9z~$dFlf8co%0i(9tQ0?kzQ_kc2icP!6) z?y`xN2fGdC*W&gjBtSc5MJa8pu}Vl@|iNG zOZ3}r4A4NN=^B?a>9^fTH*)R5lKHUl2%j8SZUXf=i%1r3Gy>M;1-D#opy|m*iP?LmL82xOwP3E|Q=t}jKbTvU)+)co~-*%s7=m}u|$OBf3-Z8ZMm z9voUe|2y3TA_gzTNg{SG2+?SC7^#bY<@zNB_;Zd}@+~=?fX&7scOmbZYw<2@M%EHT z5kri4=D5QM8ax#A5*dO;5r8p6v46xdC-(GN)5!+@G#jxPoSO*zx*;TA<9fI80u;7| zQ@D}EfP~pVMlz#*7V|q%{FWu#2Xwo!&)1+pF-A&UtN3pK=D2F9>O^3RjX?pt-Pn;- zu+0QC<~R^&yl=yXM+|ul+3*Y3h-es#2HrbvV=HB{u;pJsAhP*;^xwhwA#aoAo7_Sk z*}*6vPv^b!Cwc~A4VcZ22pQXIjR#@~u@Gqk2W&T9C^d+VB-!Ax&k;>AKNT13fnUoc zVIJv}R}R6a(I6JE(_Z5oaY&K&Rw(a*viO?(-kLXbgQ#xQ2Ygc%SGkmHn1 zY7WF6z$WXwWq?EwrKA?nwjr6OKnm_dG+3ibK*(6z<`K$VWZEMAo5+L2x9sDecRa3+ zpg_2-VfPYd9>dsu%t#oyq7*TJ^9Tj$nG7nk5YrP6kY)`v*tW*kcH`a#G$6npI6Xwz z^YlhipDJkrO5T9R^y9LLt`9aEYt3@pYF;DDjatn{eHT7&_FJpFlp8mR5MlunEW(x2 zJ=;KXiC$6$)l1?BcF=<3k0wk*foH5gIow1#>E9Q`kVj9WaVA;a?Y_fB zKofRsHjw9u^*p#$4w%|bW1pxUkoX)=i@WcD8ZqB({6XD6NcZ#GsP8KE-A0ePW1x!c zqz&}NINL-^qp@}yfddw23!TVLjOKp_+7X!1G=Bv1>;QR!*m>WGIjxZe$)(6#6w3`Z z4Cc%_O(m3OSTwq%0HgfX01ea`z}{8MUo*Wr>?)I@*mbtSd4MLRCRjQaHxR@xS&uYt zQl$Wg%Qdl5PqP5@4<%Eo>c}BwOfD$ns5mfQr!Er4qL2CaEfXjGpNa^39E)#Q9Jyw~ z7fp^2HiA@ZgH5N;SNpfvYiH8`fJsm_*g&$QxOt0fj1FNRNv|UeROm>F@=E7*scv{) zCtMm0t;Sfo=Pe5DX&Qa9(LB@XrN2>=0o@l5v6}|ZkvE&YKJvix!!3$|I?V}4mNJ-A zrtQzjO(g76!X{rS$8J+4JG{qu=(&75~(Z*2xCKg9rk{}6poNc6%2>KQX zcYNM>kYgk6GS2iBPHIvNd-UISv%6blp`1Bn}Rj(K1l zgCsy5?y+M6b&L;c;t%!Cyx6$;gV8jgF!QP0_nKgZG=@;*Td&pU92l(2+H<*iDT6%1 zgD6nirE9@}W)F5yO8j8t(NsF8;60;YO(GnF9u6F0AQMN5uPYOJ^U9IY#gB!s^Z~#P z5_yv2`%2R9Luq35o|w3iss^>F;tS0rb^xX_XB1D{B2MSN2}+Ki<@QF%yCHwnNa z#8;w?a`TnU0H|UYM;7?5nIX^mYNny`A=%g?x%M#g0Ms}W8V@A}JZXvZR8P(d6#Y?) z!w!=nF~ofz4ZD#vVm_nBRhcU8Wz$mQ(GF^7aw&>2PSiYH*nCOzJjZy%3oSD^iErGv zBP)4vOUITB;cKCS3`HXdMc9Y!59w1HotRQ+7KhXE&o0VXWDXWyrYM9YML)u`iP$Fq z&s@k>v(4sYL+U+viRND!DClp?H@Rd2bFDtn`d@BcEZKir|EK)+BbMZPUl{*8goY!- zk7(hbeN&+yq5IG3`|lvHBZEwHUkkr~mLV2<>mTRlDCe;F$Laou!v98;{5yE_M`Un9 z&O!T;*m2MtVn0IR->feeYZ5y_V^In6ua%cpZ?p{Jy8FkY+Hs5HtRJDfxUbXd&~-Dk zBk*D&{-P=WqKj^ka{Lx-O$n((I$kvCoo^d?u07nsuvj&)jfPbkG`cvk}QNR=%Nc+>=#(Su3B= zlafXQ5109U!F75iG7Y=6v!vD?%<$IZ>iytp_3_i5zxwdeQ+d>_`(5~%HeLqBwwDmE zl1@_(Z*6*~(|F_^FIDbTt2OzbyCtKBH-EzlwhdHtO9o!Gq%9dW+LD0{9<(I`Om0CY zJn3Wi!Tk0cHfK~StE-jOTafTtv_Zn=3FVR03fnYpXR7-(TpIk=ConSRYqv0`ts5?; z)i6W7e+z0s?u=&!sOanWckqst-BJYb&$RpC8Eg161D@4?4tvkcMvkAckwc)ok>kH* zBgfD5zW6iR$nmpmV|l0-%EJQmxuinGrV_ftvn6PUNmO#v2yQI{qkeI^3<=Kegd2D zRyMp1>#Jm?2gzOUe5|j2m0V1_-o8eSx5crH^%(L!&QqhYoZ^K|4I{@%-UT+oV6y^Z zrWZ`GVFICXR&Qtgg#R1b-w3v1!m8cwXlQayHv>ptStKpE{o{!l{Mf*G@~K2K36g5i z9r*|~WIx&s8IS#lQ!6*GJzA?huGVY+l?2_2n!qxxrr1tP8!uOwJYqpdqoz*EdV*ti zXhL#F58%t5DV3h5xLcy0%!IhdOhWO`j!FDc;L#oD)(onYV&v=CeCMAp-3H ztzuha8FJ!3y9d;Q!g0=?+d(Rs-|isupex13nD>CvuTdC9A&eA{d(_uYy@7v@nK$zB z(kmq}ByhG8p&gCGOAj*+vSw-3d*P2~*e>g>K7CyG>h*R0pLcZFT&WTUWBo3-268Z7hbUB{FGYSFjc^ zp-P`w5~`!%13GVXb+NT*d>TKy{*5AFrubKmF5R#E#fhu%-Gq`zI*p| zj4g*KGIvfo)b)w)5gFEnjL@Hg zXcWk#NViwC-hs)jJ52Z%ORWUOE9n4}a7sJjv5!(hIvd) zIIlz^Ej%@)Hby{t5d^5(D7Q)gs+HPRWe_+*e@FhWS6Aoq|CZ)2w&U+80sWEp#Wdb{ zG$uKt&;{S}!InlG8QliSOdB7`t>c1XDm>NPM6oGJ#om&*2}HvpFPqg)WufYT&Jj@hhZxv& z48mwOl5Cx29At+0LXruRn#rx500Dp;mm;!g%ny;ezoC~I>b&yv4ZTd?TxQHGX6O6N z+wP1%4lb;tv*|`l?-K^0YMwG%iik4`ifKxY(7Y`!{($BKkn^f zd||u~Fpj!dOn{ZkMk|Vf#Ab04IZAS(V5x$((~B&OofpN>FMIdv^|CE>mNovC*O6go z0hN$^liP0!vbV;`jPiGfZkSQ~oQRz`J|wqF6|suj7OEs^(lfS!^)nr) z*vSCTToW4FWpC{v$?Yv`EJL!@dcE%5XazttPdSU44&EgPsYic1#8qiv3LloAAsb+H;K( zsMHMm)-#Yy@KmSL3~1XazaVCd&DL_b-$uM~#p==2UDDfc&f#}(^KC4#tlE08{1oGn zTx(aXN`+M@wxf50Xl09meHy>!o0<|?aD{GUX$?tOn&WUvnkH8)D;*WUzjMdrP;Fyu zkMk$%XXBAFg!aw)P}T(%wZ?De=FGn=S-}i_b(F{`9$)d+36+Q_NymQK7)X}#TmJSPs33l#Tf8VVPwTPkW|TmHrF0Q@_z^IC=GuefI-YTBuF0 zyc4xE%6&*NrQE8D+$JK>59rJf$q0mG@9T`Uc|ucCOm?RSJ7mQj%VQ1%QkSa%vZ;Z} za1)O-^D8~Q4K$G7=DoPC)%!xLY54;SxPt!r?{!6$r6E&s`flgudo=CU_Bz9;EXiBv$_aSe|T7>0lJtA#Bxzs<}gjJ z5*%tHo+KK5oZ5pG>F)5tpJJhhZkUA>&6X(=xFrVf=<|XCS=<=_1#3fC8fPBaNQ!Vr zljg&<f>6;^G>F)g5*nhK?!Q%0E&ar{ZSUw64IO7f${VhdDK=*((N$op5Gf%0jNv13jLNcjF=B7+jg5}j%>?dz7 zih0E=lU=p7*sk~}7{m(5kGVMIKwlh4){_~F;s|k4e0EwLl$jqcHqac;+0`xet>Z~5 z0-RRIPNbeCM()ZOJ3ey#NDpro`O(IQTRM;ohMRFJTb|-ca)IO?hZzs;=X_oqPtS6L z!1t0q@)JF#j^cQtLx9LwAAMhyKq%^6XIOkyR1L)MvBM7vqm&npEiW7Xddq`gkTJ&I zv>{dH*aeXPGE52DE@%WRevpmia@T7P0xWCMO~eU5usPAGlYt-URg@XdNaN>w_94;5 z#YOeI9~4o?gbzZj7-|)|T}kYNB7GW-$9kop+OMKQonmRdZ08kR%5o4cra?cr3~+Hm zw=#NnQeVLWC{4fID36g!%gZY#%z`!fi)&`DpugbjdfD9@d9KAvUTiR0a>RLw-5~|y zUGH}>I3BLp<*I#sx^luxwFOhtLMk6zTE_DsJkip%P$gDeeQ8(iZPyob?~d64Pfv(8 z3BW=)-9E13<{#@8pLp2arRnl3z3I73Y@XA6z3X@uMLC*wsl&Y!x8ky`baZpRLkJ*H z^~RN? zVU)~c{hm)K+JAu;=UkVt^vm|{2UX1;CtEiTkYdCk=EVfeu~Ae~`lxb*3SN^SJ@P3g z#*1S(j@izgmtMQ`oy|VxDTbMRXdy(}Zk<}}qT2oHTFzzoO+!zI zpY0~IZ*_O)b^Bf8)ln;l^DwB~Uwg0!ZD)*iEZm3Zx|_yD2+QD~j|>&ajSDN48y8%s z$(u>s%;Q; zWZ9A}ZYsFlKyjb2E-lQ>(@EZNv{!&v5-#^V6hA=XUwIL8Lxfg}<)xj))_`2} z=Ag<0g7*qW;-8qsMP5C~97{@v$aZBCf|-;0VIoJHqcgvi$FI*->6CLd7WY)c!tmM~ zGd*?Z_v75&RM_Fa-}oh~H+p)!P^$8o>7})ma>n#GZw=BvQLJ?jBAPb?U8CSAKd*w|4}=;myzZzKeehp5b>je~R-j z4Cyk2PjardyMO`f#~s3H+3>5>?IpT3%UM`Lt42JiN?$ zyo6uhC&So3Um!;iRq(zYUg!Ll3Nsv-tzD0o%6xS7`j_J+UMZkA6pxpnHXz4K&dJf- zD2Ic4ohy$ICO6;xh}jtNn5pm)*XT`}EjU>Grc7pt*5O3FRl3fmB7s)QG(FI=-@2(i zy%NsEhP#Opb$N!LH)Vti76<5rY)DDXaKw$+F)CSd$x7LfDe(!~D__*@WMi00o^Vtc z27}=Q!gao`Mm~;S&#b~fp^gJ|*h#BCEpb@VG2M$fDlh6#4SVx^7+_BJh6~tC;I9Zb zH!C+QP@vi%ezLrDeu@=zBtJV;t7#=reaX7P6f5Z2SkD*`W zbrInkewR)QJZ}_$K|aAAi%mfXjaMf0)Oz|YgGg680^KzV9uCo2`Fx%_bo{t%-W|mA z(&e@L>kpsef6Jd>9-0Brm;4PN5G(`C+cIzACn&QQUx7M(U<=AAsn zL1QRXDE`gZ!JKfJjU4I%Gmj=P46zxTGyV?^pk#PdsqYHLugTZQb28<5$iMek$Rz8n zkKb4B*=|VEpKcv{i)Ig;gpWCyW-dElV2Ea|P$^5m&e8g+i*ramVr;=-`{+lNKY;ek zeKg^%1Vki#V?ychHQEw7j9=T#hcD19XY->SWTwG+8E>-pgJ~JhwJ*~w((*_1zd;vZ z*eok2_^)ubcEo-r?Kj|($0sBFAc6c>091`dbByE!$_pG3?Vz-Rii7F_ncMfdtk}YX zfoI;wPqc0)MF?KykctCuzVRYM*6{`v$@gyq?9e3QZj9sLrY&_{ybKp?;$tga+`(P_ z)Aq*k%Ajm{J4^oIO>X~o>cAPlA|YVTN(0)MB6mS{J@A8!mbYC#Gj+d+^qKjKdWi&`9yq^aeTq4oiAS5$N z3AfKW$}ZDI3ar3P6o z72dH&_fz=qSA41>S94k=7$O0YwV|`rReV221hNn@l~1N`-p2=A+G*jIb;{~DOY@fC znrHAk3j^J5E%1()jJCgGuk*QTLa|PDg4SS0n02?T0@d5fs?5Zh_DJ!Tul*Bk_B_$U*moNjM7^JKW5``xPHfuruTF z_*Z#K2pih+gBiG>g7O&h2Jq|k5nTQJWdAD`*GKuj=CX0PWO^ZlX`SM$p zioSr}?1N_X%{N}DgN^9>aL|$RurR>85wdRdT?Di0;}w>HFkD^2olZM}>wfaq3G}iDJT@Ukq|a>fg0J!jcHfA1&09^t*NaJ-J^ zF{=+Jm%lK_!D=41TAy5^!y5mQE>)*|$#-4@8Ser8gPq3c>|rsbM~@i5!!RhyL}7y3 z1&fbD>mux>gLUuz!&@=+t$1IY zDx#UnJl~Lb_+Bw6vh+SQZw2Vi_F_2}s3UB$rx=1w-w>+cqoRE{gilOS8MB&iz8}s$ z!4Q>>$K&657cE}m#2zYy4Q>CU7fk3C+=__t;0cVX`Q%%Ez>W>^1X9a?vnrgei)eFJ zpsg4ttPoh@MH&xE?%~6)P;)(CF&aJmKaq6dy%BieYHE ze^uUpx%Nm1H1aXFvo*P|EZj-SiZ_TB9U^5)<=pvE`SYP{Z7gZ%tK_7Mv+icgVtbI( zLNONLX|>L(*1yh_tnoi^5EjK1y}^k0$@SB|TuAdrbouZX70H6JxkOGw`@Ut&DeW>I z1vUF+-q(f?ory-bO0+_2p_p_tP9=(0u@cW!x=hQh`ITA*XZybFB#XN^{m?r-a{)Di z(BI=R=A7fPqdVp*jvgZ^b*B?P{OR7s5Uk4(!r&#Xi6R~) z8u-(`iy=5vj1VTZ=_?n3z6ZJ(q81sp_F&SC1AVPh5Q>mkn(Lz&54lgd#*aylJdKI4cr=x>yj3 zkaKBn+`DwG#Gf`^4AIdLJq+-`G2=kr7Zrp8q^CM;UcLn{i@={o7K9>1V=3K^{%{fK z)5(I6+?x%H5X*iZ*u}6VZ_Ms*@8r?=8@{&yEy1E3YO(hpre`iS`o5=-DnRIoIfZ;wJtSls5cCacItfhB768?Z-RoTp)EpXpq*T!6fn!Z& zoz+c5u8YQ>rWUj!P&2`wy;+V`mI;awWgmW+xydP@4_XU{=GnEI&KY8*o(Im9;Ja@0 z*_E0;ifz&6fEH4eL&weIjkBgpjXowT;uRq#j%qJt{`mYA*VXX&rT0q4*cnD zK`25HXf!DL!EnRjz@O$`3{gXFTy43jkU!lm2t|ly=ke7Cx8C4Sdkeza=^5q-8xjn$ z3N8R@8rd8U-AFQ@0vDQdKufSFhvw1leuzgAnGjhXEkOEe;-iD6i@={27v4okPnw@j z_uWy=pB`Tf(GeE5Hi2qI;!l$cLJ^{$Di`49%c(nWI-f3I4A&TYu>0=5%Pots0tCBg zIb<#$O`bal{OR+>5RI|>tyOn2hS(@AGWg=<4>y# zLJ>kASQp6Z^n)8l=F{s7fuXwcXmhad@ZnFh3qld1!*t_a-!(3Ox?K>85KR%CN$3{2 z{6Tj?=v=;Zr(^S}_gjEA>SJKK78$H!k>m$!3q$aZquWtTR!lo;{~Ar87haJ zjr3241Akgx5Q-4g5|oNMe~jE|j6XfU7@{70v{C=ya>z>2B1B8kH$V0*N;Fq z^G4?HB1G8_;_;S?knt`+#u~Goy!#E3&B1GMHFR9~(*6J{h zu*fX_Ub`B)d5a;|B6C0sDH2q#&stZYoAJrKUVu#KxK)whN$s9HX)wg{DFRiWvv)nW zj+1f>5(|rsID>6eY`#GS66bMc=n?zTaxU$rySvDy`G$TfT zT;~w7j97pqI>-L`;H_KpCOs=s5uzTh)tcoe=(bT+bOen-Gc+IF8isy}oF;5?@#j1xTvJ z#hc*Boug9UA`~GSB!9d*f9-I%C@=txendAGbM;c+bjYV>H7XVj4m5qX_TQ9K^kA1@pD{d7};!9 zgj{RXU9WDrDJ#9sf_@I91>D8kVa~+YBb+5*R#MZ$`|cuLsz9sjJlui`bZ6K#rMdXS zKD~Hm)nbT6D*|=&PyJV}DQ4N|FGAFIN0Ya%TW6W=iV$5Jc@^i9>g?JvL*wnEC$7W- zvog78fEuK>BM;8g3GSwE@$@z9+{4bXBcz^`<=IK6)3&dXQX zbWwm@53G;=*qpdH46%IXfdh5ZfuHUr)zmZGo4APqXla&x1c+zyc=k&F zXEqz)xStL_cra{YGaeopW8dLtexiGv(i0?{qKWgFay@XAQ5>BI+>vPEC@DU$y4}`( zZIGDZt^9Vo6!Q5vc*=SUH;z)-gVE=B9K0Y(U3l7V8dHo(?;@{&X{GwfxgdgCjT$}`r_bzt#HjiGl+OPKx z+gp1_uQ&Ol5$GXD67sP#DLpGTxut literal 0 HcmV?d00001 diff --git a/priv/static/adminfe/static/js/chunk-2b9c.cf321c74.js b/priv/static/adminfe/static/js/chunk-2b9c.cf321c74.js new file mode 100644 index 0000000000000000000000000000000000000000..f06da02689e1b032bdc69066f1b2b402f65f48cb GIT binary patch literal 28194 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 a`0#bl{1@F^|8#R>-GB4|WmRy_j{grC5!n_1 literal 0 HcmV?d00001 diff --git a/priv/static/adminfe/static/js/chunk-2b9c.cf321c74.js.map b/priv/static/adminfe/static/js/chunk-2b9c.cf321c74.js.map new file mode 100644 index 0000000000000000000000000000000000000000..1ec750dd1ae7f39f7a2465b71d64335349c8f690 GIT binary patch literal 95810 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 G@BcqRY}NJv literal 0 HcmV?d00001 diff --git a/priv/static/adminfe/static/js/chunk-46cf.3bd3567a.js b/priv/static/adminfe/static/js/chunk-46cf.3bd3567a.js deleted file mode 100644 index 0795a46b64e5251f4a1bd0ac4b50f77c0103c8e5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9526 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 diff --git a/priv/static/adminfe/static/js/chunk-46ef.215af110.js b/priv/static/adminfe/static/js/chunk-46ef.671cac7d.js similarity index 99% rename from priv/static/adminfe/static/js/chunk-46ef.215af110.js rename to priv/static/adminfe/static/js/chunk-46ef.671cac7d.js index db11c7488fc38c4593d070fae4e33c020e9caf9b..805cdea13fb17f2bb8e378d39781dd66816f6f97 100644 GIT binary patch delta 23 ecmca=bJb>pn;gHHxnXi*vU!SLRpn;gH9p=n~8p`n3ZRvyfWa1qd@@ OIWRY6H8ZnvV8{hTtrJrK diff --git a/priv/static/adminfe/static/js/chunk-4e7d.a40ad735.js b/priv/static/adminfe/static/js/chunk-4e7d.a40ad735.js deleted file mode 100644 index ef2379ed9bbec1a154999a4a3ed175a51e681c19..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23331 zcmd^HeRJD3lmGudh01%Tk94MFyGfJO8Bfn|Y2F+=cI+n2Wjr(qOSC9bB_a7Op1BWk zpYT4({T2%lph!7#nznQEbf&Gm02YhI?qadKVEZCYqwJ!0Ax_6ZIDA=T>3F>X_WAS2 zPo3U)QViN3KYHO{k`B8M#pB5P_K4y}&<|ft(a;K`*%Vqd*3hhR#hc|@@P8_lB~Zs7Acx;Z`I5bDWbKTBnzTA?bq?OyI_^1Dfe|oDT1Sq%!thHWXiA5>lof}sSqPQ3bG90wMX9E8_vM4g~6%>$-~T9`Y!AD6JZqPPg|&>%27#Lb_V_XLyKLx#U91$ZCHlm_mBE8EBN*5 zd9>tpI)1*scX}>D*^YZbr=9xF(#RMU>qL?-*P+y5E}+jli5S87_yVAlLL_IslkoFr z`IPNRt5Eu06_KCOQ?xr|nnppKdj3cJ8i)Y43lcy|7Kd92?7|w^#JoEW`l74bZ&l|g zp3e3RV6uX|OXjf~W~l^XpyTc$>2fm$1ybganIaF@n6@4_`9*vU!yaeMNpN*SlXg0vI-mU2nQ>N=w#b$STAU^VX0VUh39^r0 zpgT*~ZamuSu9DiWGC|!)nP3m2=7<3(uTz2b>w*4F6zAC9cnT}_Q6DDS7JGgkT)a`o z%d}g|hm`?+IaXAJ-1&$ani;b9f`^ndAoyJ9cno$2ay5`GRs`h`#wpwlh{pRCE z3ANI(EhSkISnq`soE@WXNaEnFd8Tg@Q#8*eV;pT_O;h5Wv$AtDm#lS(E8?aRwHd zMnxVS+CFrzFXWnca+(C_PrP!~r8YpR3#rV^q8c=tZ2+I7O{yIRc{IC*>KN+aY+lvG zKsjUpnQfUL-R@Z|oxem%&D}h^=nili|SGPqQ^2938i*3SR)d8ZrI!@Uuh>0`sODl?_Z@!j0H$n#e;;v0d%=tnc?FkI~ zr4LmYA0+{OEPg1i)}-=GXAVuRFu+l=P#G6VyI(oqR@T>Xc&p8&8GDIH`*QFzW`Uzd z$1@wS|NZa(p$&lg)9L-m|DnDc-$qdLED?7xNKvXf^@3;=r$lMVj|TUlffF^+C0Q@6 zuhE=>@nf&*vX1VS8IDkDMlyN&_|Nh`k;HP2Z$Ref&lU2SyS(4ztu=`Qc^fsY{5Eos zWMPmTjmIJngW@i;r36CtuJBp=B=Twu*D6lWvf8m^Hrkj-g@aX7i+O!#c^=D@Qu6LU zow$@DPlJSK{1Z7DD35PpRz^Y%AfLqX)teVuOu`WU;1^lTB=Ab>Om$~;wYkF6C`kL{ z)6e)8vzzGPs%$0mmt2m8+zz=9J^YvriP0>XOy#YYhGuL_(BDZpx?!7zH7|LV>}Y@PeqM8hN&zn zbf6jv?l@01vTiKJ$egSQJFq{-;{nzp$$#VM^=L`ma-S$_#@E(3qI;UMO-}3rl-LlP zzO6N!#)e$yFs|J<(#-Ds7jz=c4h>6`_vaqsWWe>m!1Bq*0@6v6_}-%@kAGSTA3X8Y z-<_v9E}i7y!uFj|SZiG^y*Hf*@2TuX;w+dXay{=sO0rxc*ya8BU}Ngsz?veRWr#D` z+dTFxm`=$RcXDB)0G+n7gA&sy;7%^Apicest#<}fp*y)SnWEF_huue}lS~A50kmzwi|CA-b8fZ(+T)kUhs@Q}f}2Y~Nc>#A z{Eas~)XcO18oj^q1AHEk7V6O!00rb06d`!3kT>ldGN~%(xcZD zhv-$50Fj{i{78II%_8B^7Laxie?gDyV9JVusMg^x+{uLn6Li`hO=Curc7q9ba$#ya z_FjA_JDDAEVf|BJ+6#im22=l(F}bjqxc4yHLlOnd0!^_N+6s)87GCdWWhcEl7nYK0 zoUU%;!p@jdtsNFd68c*-(P*@US^x=A{9(f4FN$tN{kP7qS&0>Z7vRDsH0n4^e&=b6 z?nFzU1LIK^H3`V$&GRiDqNT$E)DA4a+~sD&egiloaXddeo+Zx4sedg`Qw*@5vW zi<)#UJc!{`5nioxC7@4>JUZOs=DuE}1>j8be##G6{`wIkw*ZotcxM}d2_WxlVX*)N zGm}7B-|bg9+pE5YF(wP9y?D5L{D3j(y(j?#lHSWlghP<^Dvg!R{~tVVq2#GJj8)%fEjg8JGuGek8czO=XxhCfQ*_v z+G68+u8mp=7}Dq$oANg`dZw&9KVlOz9O}7C0P*M1<_ep{p=OQ+(1Er;e!;e9 zs1Gy?AXRxWjafj3T2(B7w&&&6vt=etZ;u72L#3ZeZZ?OC0VHG`4>m5|s^&ynMhl?1 zi}8bNZq{>K0By|6Y4R&KYYSol#MH_A3qD*D&D0Wr@FV=Gch@Uw^hiss1(4JRFA5gR zk(OEuAf5+rheteG^V|YR_co?e=J`nLo&^xklMj;DJ<>e408)v)(-q$DkyZ%{AbEYX z^p=g=sOXM}%fD^x?lUn2U>R^=46vh!cfYJ0vC@n*?<{~Y4zlzaW7OzMKuY^|y0^>Q zn(BSC0AlUl`H+u{RI}Csh&fBe3WA0(Oli_p4%-h`Ll%DpU~(N81MDc`;AH0t7cRn~ zIamV5K^N(~HSfvt5-Ys|Fb)eNI?j(HK10UFS119*q9>zGb|c5KdrI4~A`jT5Q2@s1 z!1NSmR8A6{TwU^En`;TS0Mef1{k3k^+EW4wvRl7Be9LEGq3xCh5Sy2me`3uoG@C7e zB=~V~{F7o)p(WS?NP@*uz#J^J1X}ibreaSitXSzS`eqC%X8Z*KGmBs_V-~ zhddf*;Z;rucb1Z47sSIHCyU-eJ!9{3`bf<#^*zG^ z=oH!6>+9LIuV4X5cdQ(W>zFN+BmmQ}ZDG{q`oS@u@iNgJEQI!9JA2M&uhf>r0wUI$SK0uSfa?MCR5GeGzEjv-m zO?=k=PV};N;1?Y;Bo!B}ojlL-wzn0eX(n4|C`D@_q&GqC@}IEmOWr>b>W~AZ1Y5Ef zBG$L=?e1+Kz1}%_v-j@g+1}9`D73uQNu=Oo@#!}0A%9e?o9WwSiixX0R4c+Uw|+_q z{VbV^^%&7x9pPC|0gt0~kD@9FcTzb?Ex6Jgm{GAEH`+syn%dY~pQ1tyV5*0!ftzQ# zh?WnK=}y-}-+}ct>a2|3PqF6?U zh$jUYRrqmnC{((BDiFd|wIXp6%d0g3#!>WgYm(;(|5Gsz7o*W;7)Glpndv1z6-+bN zpC?PI)0{@C?Lv57NKsp;u>mw33QiT3C~-QA^HJN|$VKZ4^&iDV|F{TJNo605i%UTY zC2BvzjQtwqDZ~TZ&=@u+QX)}+R1}U)xE-h?2}R;UVQv6{G z`I$mbTj96uEU3Bz4QxhPiNj+RIe%e`{$%fq-0jU}tqJY50nr$Z9dgQf(sl=DQSU6vM*%WdAMj6icelH}-Fp6fZ8V}fNa$FUXmLFR^He=^ z1s4me7{$QALfA!(Ab0d|x=|@&u1XOpsLusSrW7$VrHDC|BE~jx!~D8;DIyYTT)SnG zK}cDseb!0iraB@`vuffz5Em$!Bv6t;ZOYQbqWWi9Emo~i(KyRdcXtaywavBkV?&{o zMyy9r`VmVWAllYAzg~P(mXdYTu!+?I)p)jMYc}COpBPoeq>@ui9sCm12B(sE&gBG+ z#OYq1A7vxtOX+-$QSw#p@QWd3lAiaoDFg!sdiAg9|?xh)&UXexV z$I9aDG+dK-5-8GZMRSJfGlg=R#z~&ct&l5Ire(sks+-PAOA>5Bic^Gw(j1*LZYYS; zvhn{_kg*MpgJl$UUT$2Sb~YvJX2naB>KN2sqaCw+*LzFG%2Yo&6%62cJfZ`YSX|a% zX@2XfWYxjg3{`oo1w*6OHORnUMLa-ov>C6q3aXw@sTYcst9oHjf4NBcp2BPpFC^4l zL-?PiY|pMc4CPF5RZRuK;Q982%1V%tW?k0E3**|&uLd$+%uLmHy@0Ybg$8G(q@=oA zvjsBMit;M%u*pNVB4<6)hGI?3Q!gfm*WX8V3=$h~o4Ej&t-+~s@aD>qVqe#E(lUyw z_89fuIH6Thd@(|Gi08*Jk_cWUUHZ{QX{&53{NODwcbv*z3>UQ{ul#W=j2<2!jZ&3x zgFnD!_X2sa3sq37rtD&k158>q(Y#EvzBBgBNyXa|7m#+uwzs=G{I zQx}p#&8K1y_4$g`*57eWihE_T+lo4n?^Yl*dO~NH$q~|ZmUon^y1WdcsJz2KC~XN1 z^PlSz9RV$8omG!-yuQ_Mw7y)uz4`JIbt#LpQg1vtg%Wt*QCVw^)!Hb0+f|qIl+5)F z70SoG-_~mS=WS}!in$W(Z(p(|<7?JzhR%`XhDA@(BrfQl_BU^INyk?#+m2m$-81%1 zSF0^#mDuM$RQ{1`mA-;-7u8(zW9rIG>7}EjSjGHPy>@FT#u*CE@)L4wQa2hr3!KK{;snf@t0_uE zG7Upu3#L`oH2tUZG&9DeER{yGPF$K=>0muGIka1!KF?q;5jv~Ukjv6(tKITR@pk#5 zSf{WkB~i^MnG{tS_AvJBhU~hNVA5G5)1RhCsV@3ZG@L*35Et8`7|P6v78`6{#4R2I z&>Zr0zxWW0#|X7fk<9_V^3^_`i|*H6{yKax9}VE(@w}JNojEV}e`&(5BF;K$y~PO> z^|1_k#+o|cD?kd?U4>0&a7TCO4qmY1G(j%4jEAduDS{FWYhkWfd>mRmQoQDGAK+DX z@yS2oKhBq^XTkmWnbn_LN_b^oJTK(OuoPas~&bY zpf8K`h%F-DQBP;1pXYH?Zqq=i9dLp*B@Cr7bk(T1$mn zI@9j1-eFoC(Y*!wuf8Qg=WTi+C6%4X2}>yx7FY-M>oZ}E zF+^_=XZb%!?J z#eAQr2piziQfQREr=C9}&kfC-x$c!*rMcOX=%?uRnJ0Z~WQy-bDPld6Ei2D`d@Mrh zhVqJd*RA?5TCtW#9G#1-ZG%mz}+DWUo2_DkC&DYmc*e94Z zRQ;%!PXi}mcozxj&z{pp^u8z`?g{R4$sD^IoXnfyk{wmH|5=P$X!)EZHO}oKi?{L+ zsX~jz*P+x(d1c>!ub9Z4SsdHJFLDHY(?N3MPI3$D!}*U_1x*MsORLkzkn|%^s$vY&QN{QEzVT> z#^m@!1q6w!3rP^$K9^TjEDJG94cVnYq$4d0ie z+?1g5ewC$c)EZ0i-^RKE^?~&5ia#J_C5DBVRZh5wGM1gcnm<~})j>r_8XVBY)e`r4 z`p(!1<-MgiDJG{y2t}ocz#m}X-B=f5szyQI!bELyg{(!`d?y9>W4*S9&VAxf*?}Ov z`qu1kKzSeDZ=_dW&-nV>e*xEL&gzAIu*RscKA`QV=k9X3?cv;r%Qu zDVBO)hpQnzYEJ-%e3f6nBOMXf(h>Ul1><6}2sSqnm$`}CPo1a#w!HlBts`d*-n+KCVBx5B_b^|07TkQP+3jCet~DBr zN5goSrE5<|?RXMppmcXK8n;KMLna`;b+ldv#jClWMcFi6ONQks}8{y5a|qsS&Cikjl19c82g z1oy=xIf*i=KZ(;gdl9AS>1fj4O``s&^L!i+kwcKDS=`2Fe-yQoVFzDXH%am5n=~6u zsCo(;pudbV{*D#WD+2p-GVFzEuSY$G-j}>ge-a$PAlL-z#bk7p^i2bl@v}vN^7F-9 z-e5Ex3d-#y9YL(XWPxEAeye1+1O=}W^t$Y2L z+y$kQy*Jaie?)%`Tas3z4|XFd%CZUlh2GLHh&Cb?r2vgT%@g{E@`)+^=)9@AGmbix z4!PN%aV-$ ze!~<`p)Z4KOVOQ^@hAifb14K~w;IYi?{bH}WOPjcCb!a;Tqa zGtJJS#-lVN11f1?j4ZDasv0)5nQthCJ{DW@5WAHxA0TF-d+Ey1X3@7bsTBQRJR}jL z_^pEveIYOH#&Oo%yq_{5228Wpf&Itp>&1DuBEXnBy}y4U4hYcFwU!y7*dzMOk3P_*VYn?DvxrJU`3FK2$IaO zg}-clE16^A!m;JTv8eO0Rp*|R=-CoI8er7cdOZWJCm8lDdOb1qo;CHJ2(xE}*%Ly0 zme8Jvr)R~}7iI5TP45egzJ<{j68n~xzM$T>sPl}Ywl=U57>EQ0RssW|WngI;h|C98 z<^!Q+U}+hM&<0j$JTgZsX>}k(@RwoEz=~ub_zo<-rnsefC^Qc(%|lV4p(S7_=neZ- z^}zgP+8SD{hl2IcVm*$kJff_v@t3JF7IpHH;3e7h3%OPp3} zC6bE5q*h^4>0wjbz48pSww77t=lK{CED;$G_$kHmCX>D}vwb0t^HIdQrL12ai?`x0 zgJ9M-iLRv3msxGgE`&<_wbd@f0J96vJ((X~A@e!yR9m7S{AH7c`%}yPskA+{ZBM1| zm|i$9sNWWb?Nd%fxF=S)XVq~UPqjZWKTN%IQKxgOP8VH+BY&B4_|X+K`O8brM07~b zRC+p#X^b$%J|nfp&=~*blQ9n-^-oAM2?P4OH<|DptR-`7Oxj3oFlW?qm~A0AMulZ| zgs;EH*m24;o4!oUE*oRhbadWqC}}sh?%uh#-nw=7(wR}(LMWi!%qEyvT{cUyk&Lt{ zQ6@JVE5xhDh^~_0k77h+q;#C_oIe_3Qax;u{C7r}xkqG3I_DyX*N%^(Dfdi7WW!Cl zpG^mc95~s}CK8*X0uPgRJ01#n)NV!NB;ydv{bYi9Zk%OYC(XzdG2x%oNs+VBoAEfF zG$MAXX+L3nBpzU40Z)XeIFOX2`|&VMC@6y39LGrdNk+ZZB&I2TV>+22qVYlkEQlt= zS!@9{O%Sa$8XhGR@-_OkGtDxDm&CeS#Fl<8DeH3V>>I=hQvvP1S{m(=`SObx@p_IO zjYoZQaP;Rn$0qOtagDd5OaeM_d&<;C9J`jgWqn2tw?K2DnI=XeT>WT*%+RXC5BTh!`mfDR!rid;7p6%*h~OQIHYOe9Hn{%r#+O9x>v6NrR|jWZKQ6}~n^k$4q@vWLVz$=W5|6J9qdO2zssS$l&h>HAet zNi8qe7LqCM^k~Ra2*gMujU-e3c(ON)5b~oIzrm`;g!oRDSf`L%-N=~&Q<|~sf^MSR zNY6N;N~IBu6Ko~vbwquCGHmsyZPr&wegjHgC7muc1{w3;Zi1=h2va;2VmmoO3gHEr zlbtc-CgPkL%QoRijseD1$d#h-2q%o;Pq5Z??dF4ey}sJnY1Gf3)>j`iMh`BYHddcd z;`@4|-oU?g{JXV7DSQ0APoEDs^GW?-ef3@aQGN9}|Bj>-)jQ`JkLoD6`i3)K)_1A! z9u+2-)JQ|WpYk{T`*?L9MV_qocIuC*_!BAqct-KP)d4lsSbd6GyY%A))p{wlex6@z z53TQ^?tc9(ReC3tUKJ`mXD;l5j=lOGm{mWd;w>ruc1H2WYO=%7pVYfMR31N>+Fxc= z*kc+RM8l9PjI_e9Gb%h-{jkIQeF+Yrfkyq%D;a?-Bm;T)&72agFNrrl6Dm*ZnN$-2FNF(HGnCqoS7U(P+*t+qR|I#z zev(ILYo~GYh)O^=p~XrXDJpC0?kb3B4;!m{4`1I${rdXO>LX-+KnnicVqnp2t`7B~ zv^}(mVp8BCCq05>>d2;=_bKI6cqkQ)SOrf{9>rYv2{GsFGztxj`UGO)1!L5xATaypEtPiSKLBZSa5Cx$<_+m(#BiPe+TPian|cn zfzcP3lJ^Ujd(Y*5^+K{%WNZHf}-4+Jk22b797SS8A+ z0F`^ZdKk@dp%S?fmQtEU^ST<7F*diIN|ThC@~z!w15tV=_>~n8XzPP9CaBKmsiVM65_md6JCs76O4aFnQ;H*^ET<^sU4)Dx(}8B% z(3&msS-G0zfM(9pP3~K@cE|L=bA`F`ML_Cpqp%t4_R4N zO^My39XKKK*W#mLtu&BfNlkOM<|_f52N-&~(-r5hB$H}MDQ&nX-bgwdEz3T}S1v|K zsy$(!z#i=9*(!DV;PZIN1Fi*lsYIv)WYms@UgBHtp<*E&R8D2s_xSvhuEQA3DLE}g zfIGo@f!Z7!4F+T!9tMNQSH|G>)i7vqmoX70@GvU)v}EFsG4*K_iYP*CVAHI?M~ z=ZUxH(N@}knw&8X!h0b0LUR4^Q2OhR4(^c@pZID%TduxhEh1NRg=l>J;hD#^jfcuM zv^&zF!xd1ulnK+%bVxh^Slo!tcgd}!k1sq@v&W9qt$^#$k>1XNZ0GS(5tWn^IEnw}UnO@BpmkGr{UXSSBj7j2B5 z&xn>gjeT+_$+i&J6{KEwc%^S#GFO;dqCpNcF*CA`sc0hKH&#pz`#Qx(PI5`ucqD zC&RP5JPbXTKIoQ^z;nI3rsIpat2yDms`Z|KkRyo24@`v-JW17olMGY3(qXC_dCueF z2KR0+83y=76in|^fA$opdBYjkQ-bI817md(kLe>(qh}^S1Y~_d-5~u6R1>4;_s1JF zb!gDU75~uu7xI3j!(H?)G&m9lQD&@6qSS16^=6|n-Z|nS0_Gt^gqJTPNlCa*EJ%-5 z@9sA0nN)bd<-Lx;FVBLRwlQ{U)KA*n0433+ns|YK=o6AKpXI5ME8vy=V^hm9`#V)c zSk8xKBvcNeJoQggf=dZ1$G1H7QeYmL`dZxiD@b!096Xitsx8AEubKyJ83=!|vNQ;Wvu|1B;(E?6`S9Wx9Qd?r!w5&*EJeQ(8%^;M!Zy58=8&1z&qQ$-X%@fnB(elNy? zT%*2@)!GM*^M@NxPu>ZF`)J|xo!SgyHz+DAnm1noqMdsXB03o+mNyVhZPfew3}bG| z4FU6lOCs<=7J2?gCNu=;;x+h-Bw1A$>~n#Kk}r+2o4*~tj`pCZqz&Pc%mxoCJ@v%MbGQUQUz0b zn$99%O$f-6ZOlA;#0sG+qX_ztFt^@FjJ_+v2Xrj2JV1~MkdFwt7;9~;W~5(FSn-C` z5NRQ;7L9%pp7$VfOWCqhyzGGCr_ z)+-NjLdGXhmi;=G-h4r-A0fPCzTcH*2CtcDW}Yk0eU({rFv$_>LI$jmwDd_vz|KPa z$`g(|S@%RzVgnH!i|h9ot-CoLmDONg`T**zsn9MW8!COwI-NkE-0H=97T)@MVF_=S z*j>H5(>QxdItR*TuA6^=KgzG055+@$=DN8k1mi6sbVRd=2)KNZzIFTY9nl1sO5HAt zNWe(SQ|b2-$XZvohpq`FL{CvIzNa9lWLf0hOQ+2(Qxm@Q(uq}D-r}JNYf8GmG%`CH z_@O)+=z^#{6V?>^1gi3b6~P4ez?q&|&g0c+I5iMG#ga31%dl!vQh!Zfp`>p}!%iC_ zY+fdhlq0eh>r)~YE3mv}21zhz(ZI%_1t&5!OS^;zOLA)`zo@Jgr*GJt&fbd30;}^z zZfThWUJ_b8f5%Jm7a~#xcKQZB4wglup-k|5LU1oeKWk(Tz&_-|jeorM2yYov9hk%< zGfyMy9LnyQP&RuML|@^wu~!B3P**^b-FipHU9YQcC>JpNhyZX`)qTeLj8nJ7H0|~U zkp@xkAcY)U@~ zZV=Y8dhMwhqWrl)HFvl(4Mw5ti^F6_ofIs>vGjvk2J!uZ63V7&OaQ8S$#f1^;b8H_4K)x_H?Ez^CD{MRRbWky}w zf@TkN0V?@LIx#dh*qKNRq^Lc!F@qOz*jH>edcO##NekB0os$?lG9qG^lPD+MI$sfx&SEbuQ-7ZDLCNOshZDp$ZO@Ob_4EkTA2 zaKkJC61-&h{1+{&f@7`PX5`E#WLdWtAnI@Zt?8C?lBi+kOW5%uy z!2d`0Xg`x(q$J7Z_83qfs{M!K6LheyT;&zAn@>KE9#Yes zEJOwBG)*|)Q$0{ud5JvEz0pC5>*j7=^lCOa)2~Gi%z1q|a@OaQ<9XfL53*QuMJZyUttL6nRfdcnC>xtRu!@ivR0@|-eT&2#V z^@LpCle_PvbMW1!dQ7yyDn$kE?r1ZL2b8zC8%Gcmv~@HFJVs^&{w46ZITMc(`zBIL zaPp)t=Web`Q+dhtL$2$nQi@7?=vP%osljF`#FIm+Z;Z z^gAUHQtFt^33uO;aNn%uuq!yrbukgdeuyP9CNaA?AmB|M*ud<_$9H9u1EL6U#1uu? z!=F5kJVC&w9pL+vkq{mND$;L0$cYPOG7Q0pGPg>a`6jKR>;Ke8s4Iq zPGn^e4qB*agL|qT&EU~_31Pa8kW!@6n~Sxx(g@9schpR|&b{%DnqgkmPXEWKT4DGY zs2UIYJyo0j&r&tdnd`_H7Beuu0s8xy(|oApveSH$*lmpbUOsr=pOXF~EXMnO_#fK$ z1M?!&$#48Y$kJ^5p-gQ2+0uWJ;<28)zw&6G!txL5ci!Vq zqki{2`8^F&BLYH!#0x>mg!JHw5LcBjp9Io07R!^YO>!)Ibxdb4YsBH4eH^Je`<3C8 z1gY7rJp{#j_48j5b-~KSK2B3z%&ZWzD9Mk=Ld^QFO#6n+_n(7>d-c(;6s7)!K)tKq znh8q%|9n^&_y;q1eYE{DM{9w5xeqR zQWo32$>*-7G#bP~7b*eyKOvT4V3lEVDDWTPV1_K@Y)H3)0;F{Kg}x7QS^l*toKZG# zLy5F7l+~!ak@R#Rjs$o6SNeIutw4fGGS}-1GD3_`_;bi$uMYCjCx6E^aiU=yvCw6l z$2(UM*_S4B?9f@qY%!3<<(iI|0elP!9GqQ|bOAd4hF53y5$&i?BIXJ2;>W6qa7g#p z>(T^bd!+NXMCWsuM&PR79>5Tz!FCECSQVUwFZr_( zbU!=LCVYp7tSC8rLt8<8LbSaYx)kX2(JimtX*BLU#^?t4&5rA1KoP|CWr*=*;`$)- zT#v2wSOsA(#I7}QQA{m{^+Y-i+)j5*fQU6uMw%_=o{k(*@~-K8$VuGH?HlF6pOo~Y z-zMe(Ewdp5p55rkA7;F1#ye!%CMv{Z#MvV(ZxWlsG4jHc6rLNH1qC2)!tDbzv*o2L zUx?HXCNM@k*v!)>YC{|>qmR#u759m|emgPoNt&KJD#=pVvE?EtBfBxA zm$75vV@nIN=st*fB7Ej;HQTH>ShQ*wZk<2A= z-N4CCA7ZEt$>5N81QFb^N>AnTgCSCx54-3{QT*i>G&qc^A>H~U*EC1hmJ?pbQ%RLi zKtK!4s6(NCFEQs0sb9UV{hr?i7pJ1sn{;*;XBh=Q(_O?27tr5wKG58AIR)^$lX5;l zkUk#y6k$=Ycg2o?n$M!~NmObZP5ccMROLZ8n^5Ez@i!%C<p0jT>l5vp^LcmiG!czU4t7xHe1(MaG}<4W%W1u?8bB@`^xG^ zx^_-)`rkf~i{|Ox{;Cu>xb{iP0pVR;CkN*Cu>aR*ig=<&qXi2GG3fO`CZ!1l)j zdO_ge+P1;5CIwcJj;92Wad53qSMmeKryHwOs(Et#;9BM28h#Pd8-%?i^Plv(1?9gz zaO1}BH-2B;C_fD8QGf|mLwyUyH5Yh(X44!$z3!7od+S8kwA%X^gpRf(1I z3`og45TrH2*8JlQsAsI?y1e3W<0FZD2BtqqnNQR@y|AGe^EcmXXD*%-;y(Gx&1Nw! zD0vyuqT@zA4NMp|*jM4_Eh?u`TIs~F2YUQr8ybAmQ&auaRYh#;9ywM{}}6u3bZl8Ra2)*prBt?Ip~!P<)A6U z7}L8eq!avl21~Yih?iqfml;~H&q8IkPqNW6n#(*58DAAQe3I)ecQvhO(k{@KuZ9vL zICzq>Mqq&%T{T!SAJytnl6k*kzKO6p!P^|&5uUFq&%*l}e0S$)=;aUMb}}8<=DD5u z=yDNP;c(PGSJ#~t-L}EF*#vT~5;$Vi$J0JzzuH68%;P63H)d#I=D|6*=4qDGFkicg z>lWyZn#ooQis^GZBcF>?HW|L6a*AbIf6jIW~~-7I$?OX<)8Y~=@k=JVB2!-Z@7 z9+1#sS-3dsN-TyfE#HS2D^8^`;8$j8R0~|PObg5)#9K#4qsbqIyKV4__}k%bO&D+C zRlfbU5@vc*SwOos{uu4zx1#uZ=oYSrUR2GvRv;wXZ_|54+$|@#QWrK!qntvgZ1l(U z0t``_pQ$a5p>Rnw=uQH zQ+6^nBWU`t-;GuC{+pfRRIcM4IK%>o6tCnDNd1(b%z3#vDdxJ4VuxXP?nC#uj(lLs!ooD7M#d55QZdqbqL5&E;6f8g8|nIVqo#XI*Li&Tutu80^Gd~0O>urX z9C=!`kU0)V`#FWS?wUqK-VZoVa-R6VP5g8)q|g!}n6zGT9P0isMJVdG-w%cdLwO;i z@=-%ntA&D}=(2P?Z9B;-(Cy06WHi74qxz3pZB72?KOnev7#%__C>jw@*6_xJK4p8H z{@B9R_VF%9IYyK78P#!xmk$5w~RnnFcGG}k%)8=R#cn1CjnZ@w(lv@U^->nOm1Wd zSO{)?zeXiXV3f-4%P313XEoaOIjrRY2-E6C!{#%yXd|j~#x`x3t5o}7%5#Nar5>WD z!-o;(!t-Rk0D2Y@t7Tb*`6&8&!h^Tch*puSez!O`L+@l^^l46#%5WX?;*oqg>y^ z=oWLlyXk`jH%zz#oe=q`KKeE7A)q4o=<6m7yBLPON70_BC^g`t7{=-dN0z?QURxLw)Y9G_LzxrK4mruSH@{rD)` zu55famWI0QTXAn>H(rCNMMFbq4za3x$C- zdXc#@?MsDzlG2JRWLc$Y)OKY!8pb|Td7GS9sU(9=NbtI5 z3G-^+$U{mB4^T(sqLpup2WAq@wIerw}kjJsK@Qn@0WVV8}Eo z;#+qNQ6X?&+Z0DUlMD%p06`A-sqks3G_P6(u2nWw9~5u0ShHR%Wi8u`Hqf)rRaWZl z3aKUINGqq^5NIUA&~nOLsTl5JSEs=X#T-=zq}j?|PFtaTiH{)HQ&LXt#bJ6Iu7v)t5Y zKUn?QWg0_Z85Apr##yN-=QoT)aDekJ7O;GS2{bvZCAJn2Qm&B|?qg>7gP`AG&w)|l zL@bep$J}QapXktlSyCSX#~*TJhElq!9^IBb_r4>CM3X%P;y=ol%_KQ%E>y{IKHpAW zQ|HnQ82H~J8g_SmgEUO^E*YxcRc_zqWJYr-#Z#|kC|FMMq!VWbgep5z>+Z3iC$>6E z3y@e^>3lr?c#Y#eCLkfN3Dymk5^a?#@A`~L@}QPhDvTIuWP!n-+a5A!4~f}mQMPBp zgblt$$Yks8oqOx8TX(;evE>gmTE#978yb*d>O5#t!rK92LjrcUM;Q_&)iiFwbgs{B zk~r~gf)ie6Kdk4dpfR)?LjT9+eHu6mZu}KeNyfVNPYl#11B?|eVi%*R{BzAR!Ddek zR*UVXnP85G*q95VJTvV~aCVKjOdEx4SOjw@*V1)zN5UkYi_3vF{-n9Jx3h@TTm)n| z?e{t9;F=LGcG3|v%AvmpL(I7jhPK_BZt$r!n{@UAXh_VAcKQ&K!G<1JPcl-ckcfR6 z4te{Puxlx|WoeKMlPcs=#Sm?RNOpDHpJGOVJtt|lMxhk!PB~5;i^KXiYro3l|!vCqWdPL(aSKib;2 zX4eh~U;_ZBek|XtRSd8NTeosnbeZK`VP{t;x4-3XH|hOXlKW-m{aLo1tL#~p)9n(& z&Q%JGViBv()nC@6bCs8~=UmkQZ%44p@OEBGHnPapb?fEVw%;7O%NlR4^524;>7~Db zU}2y%ju=E^blsRb9ApRAYHJZ4D@9NKWG%ySa0~!UG7mkFg0$ z`|$1_j$T^f9>%spn1kI^RH6H1f`O;2(6JT592}&j3gflO&who0tq|rYM|ykD!BW?P zV@H^S9VXPm`e1}J5w60>wh-p1f<}+p&}~;i2Gay8%mJyC{P^v@uM&r;Z6U~MsWNPw z{py=Q%V|QGqbdv=9~yh^_yotms%$lr7%aWq5oYx_FHhK5ahI#hTXH5 zXv?D1b~J=Js!Q2}{x5!oxEjHc5dYGf(_WZk@hxVyV763kdh5ZXe6?2BRx4!MoDdJ? z*t_?WV;`F1SdbIEr_+V0>a@tYb} zqp7V{%rt8D{L%ZMnx|>S%+8^-_B@L2}g;=eXfR%WQ=($LP`8YfjTAC1zz`4hqperTnU93?qd`tbv3|4P@GEp2i^Ci` z+6ETio4oR^y3@8Og*j?r$=)uiTE29gd4)NnK@}&Fw4>d9GV%M(j%M1{!hF@S+fU!# z@>_E{7UXo*Z=7w7A9@wK&S``>s$;kIHvG=8>vSy4QI*->>ide(bsc(`qcprbdj;NE zj&&UkVa|Cq(t*szUb#s}}Ek_uVs(l4GaEVUBjU*_(zxd^mR9 zZIIK`79Qd3~|&RTwxA z66UA}i5{iCLl`&@66UBbMK61A{1%)pg*obT>nA6^E)AT|4RchN`tLJe4g;r4VU9Yr z=ZEXQJq(;v3v<*%Y~6h6cg=&eI#8wgwf^*lPaWl0V<}|XoDiVO`@#Afz_ki8aPkgw zwE6C6c;Bz!+75DtsG&sk$6J{zs$8X#IIBx9J zG0agR-W}}t6aR5mJygA0&tAbOEXtf?n=fSAoB-fNy-EN43||)7#Cel2M?GTymy28l z=MjUPREJBy-hSzic~jTNhB+$XwY7V`S5KX?ggNR>?nH0zc{HTXn}j*)P2!u8FZI-U zlQ2ic*LnN)iQj?~Uzmg01@(RBFP_5cEnCwMRt{l~O7-GwYu~ScLGbxRTi?6ce_PD6 z5DVEhCj_dF=*?uq*R9kIV!|Bl5PrF{o@21<5W*Y?j>vC6JFWW|yKrETlj-2%Mf}F^ zqcS(R2y;|Pe);+2nb!jL#hQUwAxACh?dF+JL>4>07-nfK?fKcsmfwbprG+^I@UV77m43@W|U)TDP(G;i(7B~8AaB2Rl+RQuieo@d|8FeTyGWTT&UJv zY~1%r%(qzq+-axUQeXW3RqRjRILB5C zGu8Sh(a%0Brq24q9Od1c@k?KMytMdVup=0}nDg{jIt@8e1i{Z4faNb@Pe_z{W z`&#ll!t6~&<~SSF#irT)Y~J3c90UGdBbWVCrJI>@oN{{{@^@C1?_~OV^46tu*<9!Q zBl14v+i^?WXWkZ1ci-_1eZToPyVY<=IK}}md!1qF-O6%`j<1-Jd)*8y@BX#^hz77& z+2uQts91`mTms*^%{T;C@hW^*^|F#tt3?Ob+^*mNE!mpb=wq83Fi`MXiHq`Os3gxu zH*WO~M`w-!nlR&LJA1?$eSUB)py@>H<>jWc#SMR5?6B^~`+SUXA1vo{D}n^4)fBlK zpK#lpy~3^J`a&GUnT45N*@Y6o&0UOVZ{Zg=mhzyL=>)q?FS({1n@DNjsr%1Q`CAhMkZ3S}6`sA_^)#9cak7mDnkDZi%)0Y+ z64Uj4Y0b71TgiM~%ZNPQ2jq=yH#ujMq}WS>P}s2g;xmIi9B2fQ^|Y z*qq>*3-)Meg}$petpgY5U87pir?PvSjSS>sh5Z(;l_M$e8y86dZ_`$UA@V6!kx%(z zd2YVS2g86s`5n0)_o{<AyTjCVcq$i!sHCsqzop=)N+Z9>Pdh{t%tQ=I-xF&O<*`QwoI3Y)(mb0^Qa%=>SIclZ9b>!pXn@;G#woq* zl??H2)5F(Kf5uRcM9`FH##C#x4lE{|viQRnx_!aZ$e?-3Wo0Hso%UD66mqws^ZgCx z_?ve@x*UjG<-Pg2Yts8Pa-?@!IBQ{R7I-zjNVwE7@e?9EG{taoJQ;Nm$V~l_&}-vKe3HbcP0TTg5Rg&0eSM}G^5YR@K8PRb`&)C3$1WY0 zo0q08)mym2zqJix*MuUZ!mdPqB_#Aa+#3D2L@UIT zQXThns5V(TPBVQbJ2<-Ob{4{)*&(y==fOWsaJSghUjO1V=HS|Jgahw7ASk1Jh8{9e ze~g`co|WVWhY&4wzyEz-1Svbd$TypL7ed$tv@0#D{h<$>EJ6;}jDj~oV5zv5gTmYP zK>+hYh#0I{?ZG0ux$ImmnLMa>2`b-cO94V$TWVHi6f}cJOCf}kV!)%MpoaDRwHpug zEbw+QASkP}>Y|OkCKVGOv$9h{td>TOyYIGqg+}?Nt=|amZDnlfZZv%XC0|^ZSc(Yv zD>X%Wy0=2!8kWhT3tj6PG~}E2S<2`{9z(bb~tY^Q0FX zEO!+!wf_B@|1bvqcG|1hmFkWzq=eLA5I`67tGX*SHYP~4&?`X)*X%k<@H~yVD%W2gMe zRuE7fO?gWhMKxpY+F(YZ6@D4QA4%jEHHcA(g=w0avyx#R(h+%!R-l^DQD~fen&Akq za8%SmnMBWjc@X_t^rBdP1yBkGUeeQ>veG}X5O5JBEq;(p;5s_GwtHP=>{c|2LNCR$ zz}o!6MZtb7w+!{gTcd@x9NR8oK+$eKhkjkaxGDn*>lA+|51f(amNhU<&8;8{j~CU5 zkiXtq#bQcfmL>jNR%rD9%k#o4D=f$iQ~Xcmh7I+9adue97RhR z4}mao>oTofc!RpsD-21}jeA_e_dMj)wZFHWR}k#SphpKLvI1dyX4>|YU$ww6n3g5b zmr-8H3IuSr%MQ%Rl0LeKQV8rzVZyW z05IKb&?gpg4z?p}^dKC?l!#xJ04!|z>1mAi)YE<;4QTZfv@I`KU6(-UDqgd|or5>r zqZYXZ1h84(x8Pz0LZvl04?nD0>M~?pY1RQM+b;SKK1gj7`B$zGX*TK)L_+3J+uvia zWr5J|*9z3!xIDx-5(TDv_)1?T{K9 z#4iLSxYw~Udma~bZ(Fj}I~Ic|L5k04^2E?w3xin~#086k8}21dpBi+9AvLy-T&(vr z?}RO{l8W!tKCkEo>&u=~8jNTiT&}lW zksp`6f3(kHlE;tWFR}5#TtI$AuenHeobH@I8iEhQ7M_gwXa<||g2c{f)Q_Vf&Si?C z>8UY#H%4An*L01nYqFF!?-0>w(8OeyXn^;Hns(>HXDSPj<-k@6<^(v8Aw6|asQ+v_ zIK;CZ27^l}U(ds&>$d4>oPeSG2nF%D;5LcVjv+L_OYT4+`7-P>yl*Tv^a}&|;NiD^UCBD89 zJlj(ei+hC4#Yt-SyRKGTXvb@}{I#Qo7uu7p+j#CsBtgiE*FXa<$#2P}x+cA%GH-88 zC-jiZ3)Vfxg4EC}W;BTNy zzsBCb?aIpYC*sUV1}H%;tDh@?lch-V3yB|F(fGew6DWgnb>rc6sMQbu_M2t)6drk! z{#O`~N3&EJNS)~f#-%c@kpB{$2tZ~)>>?U?}IU3EE z@u&I8tjuER-{)gm67f_V>X>GD%Buf%l;MG(A1YMJY~91F740d!(se9(*;z0t7A3%M z>4f`)sy6uq+l|%Ax@CNFgn||_Hq){~BKh#^%eNQc*_ZfPo0QuAKovhR7A zx43ezaoJq&`5nUy^WcuRL)oCLq{%~R=4~@+T6wMfH5Kd=P4+pr;9(v6pqnbDVW(T` zE`XBPo~nmnQh{7J>@ezZm$4 zlAGs`?z*S#U_-7b-q$)a#Z^=DMU^KUuJ|anPu1ynvzh0V_wp1umdb_^w{`4PlQuOd zAfxx&707{K5~IKJR*|cdw_dl&CWK<23UgY+LLi^P6RB$c>||J9uvjNtR8z{UyLx4+ zp|4n-P9i+ONB@??3OP3iRQaQT${*2{D?edWvMbAf!)X2J-sWNJUNrCBq7o1#FA(WV zH};~3nm#a-e|I6=-)7$l3STZNYjnv+8_2@``#w=*-zr+>X(G#2TKHYyZ6dnH;cH*; z0FPnTN&QnB-J~5?ae~>N(UqW$J4x0Ug>6B z52Y&lB|5!rk7293%&_T%&oGYHGwg)&a zIZ~CUoM!Frw4yAE7PdPp2{G>Q#(SDl$eukZmx3k;woF>ng z@I8J5oCD7DA)(T(;TUG)b$1B|N#=$j^3QK!>q&b^{1URg`21%x;vYItSV3Cz4Os7h^uz-wCLYLy@PFu1b~97`JYVx|Q< zC$@v!4qge)G}AlmmERa1?Dv;l?+Da#y+4CNs`?`+UH+7y)!La%vQ=j=>Z!OA0$t85 zufWm_+l^_wbVBDmg7kwgjP5X-Q5 zEy3TmBd)+(Lv~#*hAH8#t8L5tQS4ooSrVJz=+*M9a)v3BtE*AVR*xBR)USN|Et@o_ zXc&}5*u-P-^v?NAhgG6g=xWjNUX34ngDB}YBo2EO9de9OD*imqT#W%f?YIUJ++Wg) z90zcq7S(-#x!!fIch=KxVfVyKw%Z@kxsZ|L(RbV-B%J|k4t}P!WZ3FY+i`k*1!1~Y z+#IGyr?lJ7U9I$$9nkBjV|j*t(8ppSKkqomRi82bo*R*q%eINy9I7uHCvsE<*5c0A zmkxIP5ee<9#<*`eNnjN#88>;G5{qJ4-4xFrFg6EtNzQWJr}QX!PA4y6ZLYVrzUP2;`n zHRjoF=2RPEvpipCxg@_!!RGqfx#Pb@oy@WW3Ek6_qu92!*fh}%&&q(!A<%aX@k!Ovse$)u+YN6n#%WlLDUjbkKJa-6Txyyq!xB%C^i!FF`vtw{| zbQsQG`nY;t6(dOpUFXe$*NV4)oZEIn`p0w$3^nOWRr9-d2CxYCeXW(w*~W z+pC9wMyo9dd)|tAj5HUR&hKlc^BTP2)JCTB4e$xvIRR03W+L3!3hX3&Y+A@u!qI52 zEe(hTfoG4P{8@ss;(pZGtB9?`#L!#{(u${lCHh=A27*1a&SRK%-+-?)tkH!;icZrO z3+_c;*YgCM+X!olhR3#)7LJ z**M*o)Q!bB6q-X2nJuN+CxNydesT1fsWvvtzciwmi_7|wHeog0AYaj=mhScdA*>i; zy@bLrlKQ12bOjR2MdgP$HD-DVNoAqY&n9abOWQW1fH0QIOqml=#8%8pJvqhc2u=|E z3|*VkVK2_RL4b#mj2vO0_5ohQg%wvtQ6 zC}QSG)?l+F?{B7XDh>|mtjvp&$tj=wu~lM9;Yek0(9<0ioepKAmv^?r9;W$dgAMv& z4g^tTrO?EF{?w*zLU%#RWS#*DnAT`8o?=Ik1!N;a^sbkg`|F7sIq#BYxY?bzV8^v+ zJjRWUCphOQdo8hkPx~CrQBbNc?Suy{OLg+Pjas{={WCUrm}Xd|F?d$ZDBNAeI&go~ zF~@gs^sAeswZf6O2W6phoX@eXDOdDNqD*&YO%6iLgd6bbV?HRhA`=O}Hcy<;+;5~4 zzXzv5%O;JNH6cgW-T)ddKCeh*1qY@DJ|3u{cIZGa{l(TUdv4As&fNLjWsNIk+`6G` zc@)YlTfBj5zs2X+Xk$-2*&EU-BfTE#iQ9IQS2 zr1%QN&z+SZHU>g5Y9v9{#V+$LTCh z8skzN1KDNa?d(a>-- z#KjNK=-6~0XMp9oc3B3ux#*LJw!n2VVqr0lrsOmce+KL3oaU}($(?WFyk!nLm2;Uw g4Z%R(Vr`zazV`d-wRAMahSOK0QFd+n+O<#rAN|0hga7~l diff --git a/priv/static/adminfe/static/js/chunk-87b3.4704cadf.js b/priv/static/adminfe/static/js/chunk-87b3.3c11ef09.js similarity index 60% rename from priv/static/adminfe/static/js/chunk-87b3.4704cadf.js rename to priv/static/adminfe/static/js/chunk-87b3.3c11ef09.js index 9766fd7d28215a87e7382ea6b4fb406c1037af6c..3899ff190c68e24bba56309177c54dbba6c4da26 100644 GIT binary patch delta 390 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