aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorlambda <pleromagit@rogerbraun.net>2018-12-17 19:45:14 +0000
committerlambda <pleromagit@rogerbraun.net>2018-12-17 19:45:14 +0000
commit52ac7dce5c460d27d946d26070eb123e89af2914 (patch)
tree0f1ac473ae10290bd13220552e2cce8db851f5d4
parentc6d5f189d9847b0c23d8150f41c05d87402680eb (diff)
parentde981ac5a23673797951096b64a7f5ca49630467 (diff)
downloadpleroma-52ac7dce5c460d27d946d26070eb123e89af2914.tar.gz
Merge branch 'captcha' into 'develop'
Captcha See merge request pleroma/pleroma!550
-rw-r--r--config/config.exs7
-rw-r--r--config/config.md14
-rw-r--r--config/test.exs6
-rw-r--r--lib/pleroma/application.ex1
-rw-r--r--lib/pleroma/captcha/captcha.ex66
-rw-r--r--lib/pleroma/captcha/captcha_service.ex28
-rw-r--r--lib/pleroma/captcha/kocaptcha.ex67
-rw-r--r--lib/pleroma/web/router.ex1
-rw-r--r--lib/pleroma/web/twitter_api/controllers/util_controller.ex4
-rw-r--r--lib/pleroma/web/twitter_api/twitter_api.ex65
-rw-r--r--test/captcha_test.exs40
-rw-r--r--test/support/captcha_mock.ex13
12 files changed, 287 insertions, 25 deletions
diff --git a/config/config.exs b/config/config.exs
index df6ea09ae..036f1ac0b 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -10,6 +10,13 @@ config :pleroma, ecto_repos: [Pleroma.Repo]
config :pleroma, Pleroma.Repo, types: Pleroma.PostgresTypes
+config :pleroma, Pleroma.Captcha,
+ enabled: false,
+ seconds_retained: 180,
+ method: Pleroma.Captcha.Kocaptcha
+
+config :pleroma, Pleroma.Captcha.Kocaptcha, endpoint: "https://captcha.kotobank.ch"
+
# Upload configuration
config :pleroma, Pleroma.Upload,
uploader: Pleroma.Uploaders.Local,
diff --git a/config/config.md b/config/config.md
index 8282eab14..edabd6e0f 100644
--- a/config/config.md
+++ b/config/config.md
@@ -7,7 +7,7 @@ If you run Pleroma with ``MIX_ENV=prod`` the file is ``prod.secret.exs``, otherw
* `uploader`: Select which `Pleroma.Uploaders` to use
* `filters`: List of `Pleroma.Upload.Filter` to use.
* `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host.
-* `proxy_remote`: If you're using a remote uploader, Pleroma will proxy media requests instead of redirecting to it.
+* `proxy_remote`: If you\'re using a remote uploader, Pleroma will proxy media requests instead of redirecting to it.
* `proxy_opts`: Proxy options, see `Pleroma.ReverseProxy` documentation.
Note: `strip_exif` has been replaced by `Pleroma.Upload.Filter.Mogrify`.
@@ -163,3 +163,15 @@ Web Push Notifications configuration. You can use the mix task `mix web_push.gen
* ``subject``: a mailto link for the administrative contact. It’s best if this email is not a personal email address, but rather a group email so that if a person leaves an organization, is unavailable for an extended period, or otherwise can’t respond, someone else on the list can.
* ``public_key``: VAPID public key
* ``private_key``: VAPID private key
+
+## 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)
+
+### Pleroma.Captcha.Kocaptcha
+Kocaptcha is a very simple captcha service with a single API endpoint,
+the source code is here: https://github.com/koto-bank/kocaptcha. The default endpoint
+`https://captcha.kotobank.ch` is hosted by the developer.
+
+* `endpoint`: the kocaptcha endpoint to use \ No newline at end of file
diff --git a/config/test.exs b/config/test.exs
index 8f4a2dc17..51aace407 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -7,6 +7,12 @@ config :pleroma, Pleroma.Web.Endpoint,
url: [port: 4001],
server: true
+# Disable captha for tests
+config :pleroma, Pleroma.Captcha,
+ enabled: true,
+ # A fake captcha service for tests
+ method: Pleroma.Captcha.Mock
+
# Print only warnings and errors during test
config :logger, level: :warn
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
diff --git a/test/captcha_test.exs b/test/captcha_test.exs
new file mode 100644
index 000000000..54ffbd92f
--- /dev/null
+++ b/test/captcha_test.exs
@@ -0,0 +1,40 @@
+defmodule Pleroma.CaptchaTest do
+ use ExUnit.Case
+
+ import Tesla.Mock
+
+ alias Pleroma.Captcha.Kocaptcha
+
+ @ets_options [:ordered_set, :private, :named_table, {:read_concurrency, true}]
+
+ describe "Kocaptcha" do
+ setup do
+ ets_name = Kocaptcha.Ets
+ ^ets_name = :ets.new(ets_name, @ets_options)
+
+ mock(fn
+ %{method: :get, url: "https://captcha.kotobank.ch/new"} ->
+ json(%{
+ md5: "63615261b77f5354fb8c4e4986477555",
+ token: "afa1815e14e29355e6c8f6b143a39fa2",
+ url: "/captchas/afa1815e14e29355e6c8f6b143a39fa2.png"
+ })
+ end)
+
+ :ok
+ end
+
+ test "new and validate" do
+ assert Kocaptcha.new() == %{
+ type: :kocaptcha,
+ token: "afa1815e14e29355e6c8f6b143a39fa2",
+ url: "https://captcha.kotobank.ch/captchas/afa1815e14e29355e6c8f6b143a39fa2.png"
+ }
+
+ assert Kocaptcha.validate(
+ "afa1815e14e29355e6c8f6b143a39fa2",
+ "7oEy8c"
+ )
+ end
+ end
+end
diff --git a/test/support/captcha_mock.ex b/test/support/captcha_mock.ex
new file mode 100644
index 000000000..898aa17b8
--- /dev/null
+++ b/test/support/captcha_mock.ex
@@ -0,0 +1,13 @@
+defmodule Pleroma.Captcha.Mock do
+ alias Pleroma.Captcha.Service
+ @behaviour Service
+
+ @impl Service
+ def new(), do: %{type: :mock}
+
+ @impl Service
+ def validate(_token, _captcha), do: true
+
+ @impl Service
+ def cleanup(), do: :ok
+end