Browse Source

Merge branch 'captcha' into 'develop'

Captcha

See merge request pleroma/pleroma!550
tags/v0.9.9
lambda 5 years ago
parent
commit
52ac7dce5c
12 changed files with 287 additions and 25 deletions
  1. +7
    -0
      config/config.exs
  2. +13
    -1
      config/config.md
  3. +6
    -0
      config/test.exs
  4. +1
    -0
      lib/pleroma/application.ex
  5. +66
    -0
      lib/pleroma/captcha/captcha.ex
  6. +28
    -0
      lib/pleroma/captcha/captcha_service.ex
  7. +67
    -0
      lib/pleroma/captcha/kocaptcha.ex
  8. +1
    -0
      lib/pleroma/web/router.ex
  9. +4
    -0
      lib/pleroma/web/twitter_api/controllers/util_controller.ex
  10. +41
    -24
      lib/pleroma/web/twitter_api/twitter_api.ex
  11. +40
    -0
      test/captcha_test.exs
  12. +13
    -0
      test/support/captcha_mock.ex

+ 7
- 0
config/config.exs View File

@@ -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,


+ 13
- 1
config/config.md View File

@@ -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

+ 6
- 0
config/test.exs View File

@@ -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



+ 1
- 0
lib/pleroma/application.ex View File

@@ -24,6 +24,7 @@ defmodule Pleroma.Application do
# Start the Ecto repository
supervisor(Pleroma.Repo, []),
worker(Pleroma.Emoji, []),
worker(Pleroma.Captcha, []),
worker(
Cachex,
[


+ 66
- 0
lib/pleroma/captcha/captcha.ex View 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
- 0
lib/pleroma/captcha/captcha_service.ex View 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
- 0
lib/pleroma/captcha/kocaptcha.ex View 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

+ 1
- 0
lib/pleroma/web/router.ex View File

@@ -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


+ 4
- 0
lib/pleroma/web/twitter_api/controllers/util_controller.ex View File

@@ -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

+ 41
- 24
lib/pleroma/web/twitter_api/twitter_api.ex View File

@@ -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



+ 40
- 0
test/captcha_test.exs View 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
- 0
test/support/captcha_mock.ex View 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…
Cancel
Save