diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/pleroma/application.ex | 1 | ||||
-rw-r--r-- | lib/pleroma/captcha/captcha.ex | 66 | ||||
-rw-r--r-- | lib/pleroma/captcha/captcha_service.ex | 28 | ||||
-rw-r--r-- | lib/pleroma/captcha/kocaptcha.ex | 67 | ||||
-rw-r--r-- | lib/pleroma/web/router.ex | 1 | ||||
-rw-r--r-- | lib/pleroma/web/twitter_api/controllers/util_controller.ex | 4 | ||||
-rw-r--r-- | lib/pleroma/web/twitter_api/twitter_api.ex | 65 |
7 files changed, 208 insertions, 24 deletions
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 8705395a4..e15991957 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -24,6 +24,7 @@ defmodule Pleroma.Application do # Start the Ecto repository supervisor(Pleroma.Repo, []), worker(Pleroma.Emoji, []), + worker(Pleroma.Captcha, []), worker( Cachex, [ diff --git a/lib/pleroma/captcha/captcha.ex b/lib/pleroma/captcha/captcha.ex new file mode 100644 index 000000000..5630f6b57 --- /dev/null +++ b/lib/pleroma/captcha/captcha.ex @@ -0,0 +1,66 @@ +defmodule Pleroma.Captcha do + use GenServer + + @ets_options [:ordered_set, :private, :named_table, {:read_concurrency, true}] + + @doc false + def start_link() do + GenServer.start_link(__MODULE__, [], name: __MODULE__) + end + + @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 + + @doc """ + Ask the configured captcha service for a new captcha + """ + def new() do + GenServer.call(__MODULE__, :new) + end + + @doc """ + Ask the configured captcha service to validate the captcha + """ + def validate(token, captcha) do + GenServer.call(__MODULE__, {:validate, token, captcha}) + end + + @doc false + def handle_call(:new, _from, state) do + enabled = Pleroma.Config.get([__MODULE__, :enabled]) + + if !enabled do + {:reply, %{type: :none}, state} + else + {:reply, method().new(), state} + end + end + + @doc false + def handle_call({:validate, token, captcha}, _from, state) do + {:reply, method().validate(token, captcha), state} + end + + @doc false + def handle_info(:cleanup, state) do + :ok = method().cleanup() + + seconds_retained = Pleroma.Config.get!([__MODULE__, :seconds_retained]) + # Schedule the next clenup + Process.send_after(self(), :cleanup, 1000 * seconds_retained) + + {:noreply, state} + end + + defp method, do: Pleroma.Config.get!([__MODULE__, :method]) +end diff --git a/lib/pleroma/captcha/captcha_service.ex b/lib/pleroma/captcha/captcha_service.ex new file mode 100644 index 000000000..8d0b76f86 --- /dev/null +++ b/lib/pleroma/captcha/captcha_service.ex @@ -0,0 +1,28 @@ +defmodule Pleroma.Captcha.Service do + @doc """ + Request new captcha from a captcha service. + + Returns: + + Service-specific data for using the newly created captcha + """ + @callback new() :: map + + @doc """ + Validated the provided captcha solution. + + Arguments: + * `token` the captcha is associated with + * `captcha` solution of the captcha to validate + + 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 +end diff --git a/lib/pleroma/captcha/kocaptcha.ex b/lib/pleroma/captcha/kocaptcha.ex new file mode 100644 index 000000000..51900d123 --- /dev/null +++ b/lib/pleroma/captcha/kocaptcha.ex @@ -0,0 +1,67 @@ +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]) + + case Tesla.get(endpoint <> "/new") do + {:error, _} -> + %{error: "Kocaptcha service unavailable"} + + {: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 + 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 + end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index daff3362c..60342cfb4 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -99,6 +99,7 @@ defmodule Pleroma.Web.Router do get("/password_reset/:token", UtilController, :show_password_reset) post("/password_reset", UtilController, :password_reset) get("/emoji", UtilController, :emoji) + get("/captcha", UtilController, :captcha) end scope "/api/pleroma/admin", Pleroma.Web.AdminAPI do diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 2f2b69623..38653f0b8 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -284,4 +284,8 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do json(conn, %{error: msg}) end end + + def captcha(conn, _params) do + json(conn, Pleroma.Captcha.new()) + end end diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex index 1e764f24a..90b8345c5 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -132,38 +132,55 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do bio: User.parse_bio(params["bio"]), email: params["email"], password: params["password"], - password_confirmation: params["confirm"] + password_confirmation: params["confirm"], + captcha_solution: params["captcha_solution"], + captcha_token: params["captcha_token"] } - registrations_open = Pleroma.Config.get([:instance, :registrations_open]) - - # no need to query DB if registration is open - token = - unless registrations_open || is_nil(tokenString) do - Repo.get_by(UserInviteToken, %{token: tokenString}) + 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 + else + Pleroma.Captcha.validate(params[:captcha_token], params[:captcha_solution]) end - cond do - registrations_open || (!is_nil(token) && !token.used) -> - changeset = User.register_changeset(%User{info: %{}}, params) - - with {:ok, user} <- Repo.insert(changeset) do - !registrations_open && UserInviteToken.mark_as_used(token.token) - {:ok, user} - else - {:error, changeset} -> - errors = - Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end) - |> Jason.encode!() + # Captcha invalid + if not captcha_ok do + # I have no idea how this error handling works + {:error, %{error: Jason.encode!(%{captcha: ["Invalid CAPTCHA"]})}} + else + registrations_open = Pleroma.Config.get([:instance, :registrations_open]) - {:error, %{error: errors}} + # no need to query DB if registration is open + token = + unless registrations_open || is_nil(tokenString) do + Repo.get_by(UserInviteToken, %{token: tokenString}) end - !registrations_open && is_nil(token) -> - {:error, "Invalid token"} + cond do + registrations_open || (!is_nil(token) && !token.used) -> + changeset = User.register_changeset(%User{info: %{}}, params) - !registrations_open && token.used -> - {:error, "Expired token"} + with {:ok, user} <- Repo.insert(changeset) do + !registrations_open && UserInviteToken.mark_as_used(token.token) + {:ok, user} + else + {:error, changeset} -> + errors = + Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end) + |> Jason.encode!() + + {:error, %{error: errors}} + end + + !registrations_open && is_nil(token) -> + {:error, "Invalid token"} + + !registrations_open && token.used -> + {:error, "Expired token"} + end end end |