forked from mirrors/akkoma
Merge branch 'captcha' into 'develop'
Captcha See merge request pleroma/pleroma!550
This commit is contained in:
commit
52ac7dce5c
|
@ -10,6 +10,13 @@
|
|||
|
||||
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,
|
||||
|
|
|
@ -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
|
|
@ -7,6 +7,12 @@
|
|||
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
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ def start(_type, _args) do
|
|||
# Start the Ecto repository
|
||||
supervisor(Pleroma.Repo, []),
|
||||
worker(Pleroma.Emoji, []),
|
||||
worker(Pleroma.Captcha, []),
|
||||
worker(
|
||||
Cachex,
|
||||
[
|
||||
|
|
66
lib/pleroma/captcha/captcha.ex
Normal file
66
lib/pleroma/captcha/captcha.ex
Normal file
|
@ -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
|
28
lib/pleroma/captcha/captcha_service.ex
Normal file
28
lib/pleroma/captcha/captcha_service.ex
Normal file
|
@ -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
|
67
lib/pleroma/captcha/kocaptcha.ex
Normal file
67
lib/pleroma/captcha/kocaptcha.ex
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -284,4 +284,8 @@ def delete_account(%{assigns: %{user: user}} = conn, params) do
|
|||
json(conn, %{error: msg})
|
||||
end
|
||||
end
|
||||
|
||||
def captcha(conn, _params) do
|
||||
json(conn, Pleroma.Captcha.new())
|
||||
end
|
||||
end
|
||||
|
|
|
@ -132,38 +132,55 @@ def register_user(params) 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)
|
||||
# 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])
|
||||
|
||||
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}}
|
||||
# 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
|
||||
|
||||
|
|
40
test/captcha_test.exs
Normal file
40
test/captcha_test.exs
Normal file
|
@ -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
|
13
test/support/captcha_mock.ex
Normal file
13
test/support/captcha_mock.ex
Normal file
|
@ -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
|
Loading…
Reference in a new issue