diff options
| -rw-r--r-- | config/config.exs | 26 | ||||
| -rw-r--r-- | config/test.exs | 3 | ||||
| -rw-r--r-- | docs/config.md | 2 | ||||
| -rw-r--r-- | lib/pleroma/application.ex | 10 | ||||
| -rw-r--r-- | lib/pleroma/captcha/captcha.ex | 87 | ||||
| -rw-r--r-- | lib/pleroma/captcha/captcha_service.ex | 21 | ||||
| -rw-r--r-- | lib/pleroma/captcha/kocaptcha.ex | 58 | ||||
| -rw-r--r-- | lib/pleroma/user.ex | 1 | ||||
| -rw-r--r-- | lib/pleroma/web/federator/retry_queue.ex | 3 | ||||
| -rw-r--r-- | lib/pleroma/web/twitter_api/twitter_api.ex | 16 | ||||
| -rw-r--r-- | test/captcha_test.exs | 18 | ||||
| -rw-r--r-- | test/support/captcha_mock.ex | 5 | 
12 files changed, 143 insertions, 107 deletions
| diff --git a/config/config.exs b/config/config.exs index d30b33197..601d9e227 100644 --- a/config/config.exs +++ b/config/config.exs @@ -12,7 +12,7 @@ config :pleroma, Pleroma.Repo, types: Pleroma.PostgresTypes  config :pleroma, Pleroma.Captcha,    enabled: false, -  seconds_retained: 180, +  seconds_valid: 60,    method: Pleroma.Captcha.Kocaptcha  config :pleroma, Pleroma.Captcha.Kocaptcha, endpoint: "https://captcha.kotobank.ch" @@ -54,6 +54,17 @@ config :pleroma, :uri_schemes,      "xmpp"    ] +websocket_config = [ +  path: "/websocket", +  serializer: [ +    {Phoenix.Socket.V1.JSONSerializer, "~> 1.0.0"}, +    {Phoenix.Socket.V2.JSONSerializer, "~> 2.0.0"} +  ], +  timeout: 60_000, +  transport_log: false, +  compress: false +] +  # Configures the endpoint  config :pleroma, Pleroma.Web.Endpoint,    url: [host: "localhost"], @@ -62,6 +73,8 @@ config :pleroma, Pleroma.Web.Endpoint,        {:_,         [           {"/api/v1/streaming", Elixir.Pleroma.Web.MastodonAPI.WebsocketHandler, []}, +         {"/socket/websocket", Phoenix.Endpoint.CowboyWebSocket, +          {nil, {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, websocket_config}}},           {:_, Plug.Adapters.Cowboy.Handler, {Pleroma.Web.Endpoint, []}}         ]}      ] @@ -173,13 +186,7 @@ config :pleroma, :mrf_simple,    reject: [],    accept: [] -config :pleroma, :media_proxy, -  enabled: false, -  # base_url: "https://cache.pleroma.social", -  proxy_opts: [ -    # inline_content_types: [] | false | true, -    # http: [:insecure] -  ] +config :pleroma, :media_proxy, enabled: false  config :pleroma, :chat, enabled: true @@ -249,7 +256,8 @@ config :pleroma, Pleroma.User,      "auth",      "proxy",      "dev", -    "internal" +    "internal", +    "media"    ]  config :pleroma, Pleroma.Web.Federator, max_jobs: 50 diff --git a/config/test.exs b/config/test.exs index 51aace407..67ed4737f 100644 --- a/config/test.exs +++ b/config/test.exs @@ -9,7 +9,8 @@ config :pleroma, Pleroma.Web.Endpoint,  # Disable captha for tests  config :pleroma, Pleroma.Captcha, -  enabled: true, +  # It should not be enabled for automatic tests +  enabled: false,    # A fake captcha service for tests    method: Pleroma.Captcha.Mock diff --git a/docs/config.md b/docs/config.md index f4bcae3fd..1c3219efe 100644 --- a/docs/config.md +++ b/docs/config.md @@ -172,7 +172,7 @@ Web Push Notifications configuration. You can use the mix task `mix web_push.gen  ## Pleroma.Captcha  * `enabled`: Whether the captcha should be shown on registration  * `method`: The method/service to use for captcha -* `seconds_retained`: The time in seconds for which the captcha is valid (stored in the cache) +* `seconds_valid`: The time in seconds for which the captcha is valid  ### Pleroma.Captcha.Kocaptcha  Kocaptcha is a very simple captcha service with a single API endpoint, diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 4542ed623..cb3e6b69b 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -32,6 +32,16 @@ defmodule Pleroma.Application do          worker(            Cachex,            [ +            :used_captcha_cache, +            [ +              ttl_interval: :timer.seconds(Pleroma.Config.get!([Pleroma.Captcha, :seconds_valid])) +            ] +          ], +          id: :cachex_used_captcha_cache +        ), +        worker( +          Cachex, +          [              :user_cache,              [                default_ttl: 25000, diff --git a/lib/pleroma/captcha/captcha.ex b/lib/pleroma/captcha/captcha.ex index 133a9fd68..0207bcbea 100644 --- a/lib/pleroma/captcha/captcha.ex +++ b/lib/pleroma/captcha/captcha.ex @@ -3,9 +3,11 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Captcha do -  use GenServer +  alias Plug.Crypto.KeyGenerator +  alias Plug.Crypto.MessageEncryptor +  alias Calendar.DateTime -  @ets_options [:ordered_set, :private, :named_table, {:read_concurrency, true}] +  use GenServer    @doc false    def start_link() do @@ -14,14 +16,6 @@ defmodule Pleroma.Captcha do    @doc false    def init(_) do -    # Create a ETS table to store captchas -    ets_name = Module.concat(method(), Ets) -    ^ets_name = :ets.new(Module.concat(method(), Ets), @ets_options) - -    # Clean up old captchas every few minutes -    seconds_retained = Pleroma.Config.get!([__MODULE__, :seconds_retained]) -    Process.send_after(self(), :cleanup, 1000 * seconds_retained) -      {:ok, nil}    end @@ -35,8 +29,8 @@ defmodule Pleroma.Captcha do    @doc """    Ask the configured captcha service to validate the captcha    """ -  def validate(token, captcha) do -    GenServer.call(__MODULE__, {:validate, token, captcha}) +  def validate(token, captcha, answer_data) do +    GenServer.call(__MODULE__, {:validate, token, captcha, answer_data})    end    @doc false @@ -46,24 +40,71 @@ defmodule Pleroma.Captcha do      if !enabled do        {:reply, %{type: :none}, state}      else -      {:reply, method().new(), state} +      new_captcha = method().new() + +      secret_key_base = Pleroma.Config.get!([Pleroma.Web.Endpoint, :secret_key_base]) + +      # This make salt a little different for two keys +      token = new_captcha[:token] +      secret = KeyGenerator.generate(secret_key_base, token <> "_encrypt") +      sign_secret = KeyGenerator.generate(secret_key_base, token <> "_sign") +      # Basicallty copy what Phoenix.Token does here, add the time to +      # the actual data and make it a binary to then encrypt it +      encrypted_captcha_answer = +        %{ +          at: DateTime.now_utc(), +          answer_data: new_captcha[:answer_data] +        } +        |> :erlang.term_to_binary() +        |> MessageEncryptor.encrypt(secret, sign_secret) + +      { +        :reply, +        # Repalce the answer with the encrypted answer +        %{new_captcha | answer_data: encrypted_captcha_answer}, +        state +      }      end    end    @doc false -  def handle_call({:validate, token, captcha}, _from, state) do -    {:reply, method().validate(token, captcha), state} -  end +  def handle_call({:validate, token, captcha, answer_data}, _from, state) do +    secret_key_base = Pleroma.Config.get!([Pleroma.Web.Endpoint, :secret_key_base]) +    secret = KeyGenerator.generate(secret_key_base, token <> "_encrypt") +    sign_secret = KeyGenerator.generate(secret_key_base, token <> "_sign") -  @doc false -  def handle_info(:cleanup, state) do -    :ok = method().cleanup() +    # If the time found is less than (current_time - seconds_valid), then the time has already passed. +    # Later we check that the time found is more than the presumed invalidatation time, that means +    # that the data is still valid and the captcha can be checked +    seconds_valid = Pleroma.Config.get!([Pleroma.Captcha, :seconds_valid]) +    valid_if_after = DateTime.subtract!(DateTime.now_utc(), seconds_valid) + +    result = +      with {:ok, data} <- MessageEncryptor.decrypt(answer_data, secret, sign_secret), +           %{at: at, answer_data: answer_md5} <- :erlang.binary_to_term(data) do +        try do +          if DateTime.before?(at, valid_if_after), do: throw({:error, "CAPTCHA expired"}) + +          if not is_nil(Cachex.get!(:used_captcha_cache, token)), +            do: throw({:error, "CAPTCHA already used"}) + +          res = method().validate(token, captcha, answer_md5) +          # Throw if an error occurs +          if res != :ok, do: throw(res) + +          # Mark this captcha as used +          {:ok, _} = +            Cachex.put(:used_captcha_cache, token, true, ttl: :timer.seconds(seconds_valid)) -    seconds_retained = Pleroma.Config.get!([__MODULE__, :seconds_retained]) -    # Schedule the next clenup -    Process.send_after(self(), :cleanup, 1000 * seconds_retained) +          :ok +        catch +          :throw, e -> e +        end +      else +        _ -> {:error, "Invalid answer data"} +      end -    {:noreply, state} +    {:reply, result, state}    end    defp method, do: Pleroma.Config.get!([__MODULE__, :method]) diff --git a/lib/pleroma/captcha/captcha_service.ex b/lib/pleroma/captcha/captcha_service.ex index a820751a8..8d27c04f1 100644 --- a/lib/pleroma/captcha/captcha_service.ex +++ b/lib/pleroma/captcha/captcha_service.ex @@ -8,9 +8,14 @@ defmodule Pleroma.Captcha.Service do    Returns: -  Service-specific data for using the newly created captcha +  Type/Name of the service, the token to identify the captcha, +  the data of the answer and service-specific data to use the newly created captcha    """ -  @callback new() :: map +  @callback new() :: %{ +              type: atom(), +              token: String.t(), +              answer_data: any() +            }    @doc """    Validated the provided captcha solution. @@ -18,15 +23,15 @@ defmodule Pleroma.Captcha.Service do    Arguments:    * `token` the captcha is associated with    * `captcha` solution of the captcha to validate +  * `answer_data` is the data needed to validate the answer (presumably encrypted)    Returns:    `true` if captcha is valid, `false` if not    """ -  @callback validate(token :: String.t(), captcha :: String.t()) :: boolean - -  @doc """ -  This function is called periodically to clean up old captchas -  """ -  @callback cleanup() :: :ok +  @callback validate( +              token :: String.t(), +              captcha :: String.t(), +              answer_data :: any() +            ) :: :ok | {:error, String.t()}  end diff --git a/lib/pleroma/captcha/kocaptcha.ex b/lib/pleroma/captcha/kocaptcha.ex index 66f9ce754..34a611492 100644 --- a/lib/pleroma/captcha/kocaptcha.ex +++ b/lib/pleroma/captcha/kocaptcha.ex @@ -3,13 +3,9 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.Captcha.Kocaptcha do -  alias Calendar.DateTime -    alias Pleroma.Captcha.Service    @behaviour Service -  @ets __MODULE__.Ets -    @impl Service    def new() do      endpoint = Pleroma.Config.get!([__MODULE__, :endpoint]) @@ -21,51 +17,21 @@ defmodule Pleroma.Captcha.Kocaptcha do        {:ok, res} ->          json_resp = Poison.decode!(res.body) -        token = json_resp["token"] - -        true = -          :ets.insert( -            @ets, -            {token, json_resp["md5"], DateTime.now_utc() |> DateTime.Format.unix()} -          ) - -        %{type: :kocaptcha, token: token, url: endpoint <> json_resp["url"]} -    end -  end - -  @impl Service -  def validate(token, captcha) do -    with false <- is_nil(captcha), -         [{^token, saved_md5, _}] <- :ets.lookup(@ets, token), -         true <- :crypto.hash(:md5, captcha) |> Base.encode16() == String.upcase(saved_md5) do -      # Clear the saved value -      :ets.delete(@ets, token) - -      true -    else -      _ -> false +        %{ +          type: :kocaptcha, +          token: json_resp["token"], +          url: endpoint <> json_resp["url"], +          answer_data: json_resp["md5"] +        }      end    end    @impl Service -  def cleanup() do -    seconds_retained = Pleroma.Config.get!([Pleroma.Captcha, :seconds_retained]) -    # If the time in ETS is less than current_time - seconds_retained, then the time has -    # already passed -    delete_after = -      DateTime.subtract!(DateTime.now_utc(), seconds_retained) |> DateTime.Format.unix() - -    :ets.select_delete( -      @ets, -      [ -        { -          {:_, :_, :"$1"}, -          [{:<, :"$1", {:const, delete_after}}], -          [true] -        } -      ] -    ) - -    :ok +  def validate(_token, captcha, answer_data) do +    # Here the token is unsed, because the unencrypted captcha answer is just passed to method +    if not is_nil(captcha) and +         :crypto.hash(:md5, captcha) |> Base.encode16() == String.upcase(answer_data), +       do: :ok, +       else: {:error, "Invalid CAPTCHA"}    end  end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 892f4e483..1edded415 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -495,6 +495,7 @@ defmodule Pleroma.User do        Enum.map(reqs, fn req -> req.actor end)        |> Enum.uniq()        |> Enum.map(fn ap_id -> get_by_ap_id(ap_id) end) +      |> Enum.filter(fn u -> !is_nil(u) end)        |> Enum.filter(fn u -> !following?(u, user) end)      {:ok, users} diff --git a/lib/pleroma/web/federator/retry_queue.ex b/lib/pleroma/web/federator/retry_queue.ex index 230a2c939..e0ce251d2 100644 --- a/lib/pleroma/web/federator/retry_queue.ex +++ b/lib/pleroma/web/federator/retry_queue.ex @@ -87,9 +87,8 @@ defmodule Pleroma.Web.Federator.RetryQueue do        )      popped -    |> List.foldl(true, fn e, acc -> +    |> Enum.each(fn e ->        :ets.delete_object(table, e) -      acc      end)      popped diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex index 0aa4a8d23..ecf81d492 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -140,22 +140,28 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do        password: params["password"],        password_confirmation: params["confirm"],        captcha_solution: params["captcha_solution"], -      captcha_token: params["captcha_token"] +      captcha_token: params["captcha_token"], +      captcha_answer_data: params["captcha_answer_data"]      }      captcha_enabled = Pleroma.Config.get([Pleroma.Captcha, :enabled])      # true if captcha is disabled or enabled and valid, false otherwise      captcha_ok =        if !captcha_enabled do -        true +        :ok        else -        Pleroma.Captcha.validate(params[:captcha_token], params[:captcha_solution]) +        Pleroma.Captcha.validate( +          params[:captcha_token], +          params[:captcha_solution], +          params[:captcha_answer_data] +        )        end      # Captcha invalid -    if not captcha_ok do +    if captcha_ok != :ok do +      {:error, error} = captcha_ok        # I have no idea how this error handling works -      {:error, %{error: Jason.encode!(%{captcha: ["Invalid CAPTCHA"]})}} +      {:error, %{error: Jason.encode!(%{captcha: [error]})}}      else        registrations_open = Pleroma.Config.get([:instance, :registrations_open]) diff --git a/test/captcha_test.exs b/test/captcha_test.exs index 7f559ac72..7ca9a4607 100644 --- a/test/captcha_test.exs +++ b/test/captcha_test.exs @@ -29,16 +29,18 @@ defmodule Pleroma.CaptchaTest do      end      test "new and validate" do -      assert Kocaptcha.new() == %{ -               type: :kocaptcha, -               token: "afa1815e14e29355e6c8f6b143a39fa2", -               url: "https://captcha.kotobank.ch/captchas/afa1815e14e29355e6c8f6b143a39fa2.png" -             } +      new = Kocaptcha.new() +      assert new[:type] == :kocaptcha +      assert new[:token] == "afa1815e14e29355e6c8f6b143a39fa2" + +      assert new[:url] == +               "https://captcha.kotobank.ch/captchas/afa1815e14e29355e6c8f6b143a39fa2.png"        assert Kocaptcha.validate( -               "afa1815e14e29355e6c8f6b143a39fa2", -               "7oEy8c" -             ) +               new[:token], +               "7oEy8c", +               new[:answer_data] +             ) == :ok      end    end  end diff --git a/test/support/captcha_mock.ex b/test/support/captcha_mock.ex index 3ab02916f..9061f2b45 100644 --- a/test/support/captcha_mock.ex +++ b/test/support/captcha_mock.ex @@ -10,8 +10,5 @@ defmodule Pleroma.Captcha.Mock do    def new(), do: %{type: :mock}    @impl Service -  def validate(_token, _captcha), do: true - -  @impl Service -  def cleanup(), do: :ok +  def validate(_token, _captcha, _data), do: :ok  end | 
