Skip to content

Commit

Permalink
mcaptcha package and module with nixos tests
Browse files Browse the repository at this point in the history
This completes #17

Co-authored-by: Shahar "Dawn" Or <mightyiampresence@gmail.com>
Co-authored-by: Rohit <rohitsutradhar311@gmail.com>
Co-authored-by: Matúš Ferech <matus.ferech@gmail.com>
Co-authored-by: Alejandro Sanchez Medina <alejandrosanchzmedina@gmail.com>
  • Loading branch information
5 people committed Sep 26, 2023
1 parent c2e29a8 commit deb0eb8
Show file tree
Hide file tree
Showing 8 changed files with 628 additions and 0 deletions.
2 changes: 2 additions & 0 deletions all-packages.nix
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

libgnunetchat = callPackage ./pkgs/libgnunetchat {};
librecast = callPackage ./pkgs/librecast {inherit lcrq;};
mcaptcha = callPackage ./pkgs/mcaptcha {};
mcaptcha-cache = callPackage ./pkgs/mcaptcha-cache {};
pretalx = callPackage ./pkgs/pretalx {};
pretalx-frontend = callPackage ./pkgs/pretalx/frontend.nix {};
pretalx-full = callPackage ./pkgs/pretalx {
Expand Down
1 change: 1 addition & 0 deletions modules/all-modules.nix
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# Refer to <https://github.com/ngi-nix/ngipkgs/issues/40>.
#liberaforms = import ./liberaforms.nix;
flarum = import ./flarum.nix;
mcaptcha = import ./mcaptcha.nix;
pretalx = import ./pretalx.nix;
rosenpass = import ./rosenpass.nix;
unbootable = import ./unbootable.nix;
Expand Down
268 changes: 268 additions & 0 deletions modules/mcaptcha.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
{
config,
lib,
options,
pkgs,
...
}:
with builtins;
with lib; let
cfg = config.services.mcaptcha;

# mCaptcha has no support for defaults. Every option must be specified.
# The module-provided defaults below are based on
# https://github.com/mCaptcha/mCaptcha/blob/f337ee0643d88723776e1de4e5588dfdb6c0c574/config/default.toml
settings = {
debug = false;
source_code = "https://github.com/mCaptcha/mCaptcha";
commercial = false;
allow_demo = false;
allow_registration = true;

server = {
port = cfg.server.port;
domain = cfg.server.host;
ip = cfg.server.bindAddress;
proxy_has_tls = false;
};

database =
{
pool = 4;
database_type = "postgres";
}
// lib.optionalAttrs (!cfg.database.createLocally) {
username = cfg.database.user;
hostname = cfg.database.host;
port = cfg.database.port;
name = cfg.database.name;
};

captcha = {
gc = 30;
runners = 4;
queue_length = 2000;
enable_stats = true;

default_difficulty_strategy = {
avg_traffic_difficulty = 50000;
peak_sustainable_traffic_difficulty = 3000000;
broke_my_site_traffic_difficulty = 5000000;
duration = 30;
};
};

redis = {
pool = 4;
};
};

configFile = (pkgs.formats.toml {}).generate "mcaptcha.config.toml" (lib.recursiveUpdate settings cfg.extraSettings);
in {
options.services.mcaptcha.enable = mkEnableOption "Enable mCaptcha server.";
options.services.mcaptcha.package = mkPackageOption pkgs "mcaptcha" {};

options.services.mcaptcha.extraSettings = mkOption {
type = types.attrs;
description = ''
Extra settings. Best sources of documentation for settings seem to be
https://github.com/mCaptcha/mCaptcha/blob/master/config/default.toml
https://github.com/mCaptcha/mCaptcha/blob/master/docs/CONFIGURATION.md
'';
default = {};
};

options.services.mcaptcha.user = mkOption {
type = types.str;
description = "User account to run under.";
default = "mcaptcha";
};

options.services.mcaptcha.group = mkOption {
type = types.str;
description = "Group for the user mCaptcha runs under.";
default = "mcaptcha";
};

options.services.mcaptcha.database.createLocally = mkOption {
type = types.bool;
description = "Whether to create and use a local databse instance";
default = false;
};

options.services.mcaptcha.database.passwordFile = mkOption {
type = types.nullOr types.path;
description = ''
Path to a file containing a database password.
Ignored when `database.createLocally`.
'';
default = null;
example = "/run/secrets/mcaptcha/database";
};

options.services.mcaptcha.database.name = mkOption {
type = types.str;
description = "Applies both when `database.createLocally` is set and not.";
default = "mcaptcha";
};

options.services.mcaptcha.database.user = mkOption {
type = types.str;
description = "Ignored when `database.createLocally`.";
example = "mcaptcha";
};

options.services.mcaptcha.database.host = mkOption {
type = types.str;
description = "Ignored when `database.createLocally`.";
example = "localhost";
};

options.services.mcaptcha.database.port = mkOption {
type = types.int;
description = "Ignored when `database.createLocally`.";
example = 5432;
};

options.services.mcaptcha.server.cookieSecretFile = mkOption {
type = types.path;
description = "Path to a file containing a cookie secret.";
example = "/run/secrets/mcaptcha/cookie-secret";
};

options.services.mcaptcha.captcha.saltFile = mkOption {
type = types.path;
description = "Path to a file containing a salt.";
example = "/run/secrets/mcaptcha/salt";
};

options.services.mcaptcha.redis.createLocally = mkOption {
type = types.bool;
description = "Whether to create a Redis instance locally.";
default = false;
};

options.services.mcaptcha.redis.host = mkOption {
type = types.str;
description = "Ignored when `redis.createLocally`.";
example = "redis.example.com";
};

options.services.mcaptcha.redis.port = mkOption {
type = types.int;
description = "Applies both when `redis.createLocally` is set and not.";
default = 6379;
};

options.services.mcaptcha.redis.user = mkOption {
type = types.str;
description = "Ignored when `redis.createLocally`.";
default = "default";
example = "mcaptcha";
};

options.services.mcaptcha.redis.passwordFile = mkOption {
type = types.path;
description = ''
Path to a file containing the Redis server password.
Ignored when `redis.createLocally`.";
'';
example = "/run/secrets/mcaptcha/redis-secret";
};

options.services.mcaptcha.server.port = mkOption {
type = types.int;
description = "Web server port.";
default = 7000;
};

options.services.mcaptcha.server.host = mkOption {
type = types.str;
description = "Web server host.";
default = "localhost";
example = "example.com";
};

options.services.mcaptcha.server.bindAddress = mkOption {
type = types.str;
description = "Web server IP addresses to bind to.";
default = "127.0.0.1";
example = "0.0.0.0";
};

config = mkIf cfg.enable {
systemd.services.mcaptcha.description = "mCaptcha: a CAPTCHA system that gives attackers a run for their money";

systemd.services.mcaptcha.script = let
serverCookieSecret = "export MCAPTCHA_SERVER_COOKIE_SECRET=$(< ${cfg.server.cookieSecretFile})";
captchaSalt = "export MCAPTCHA_CAPTCHA_SALT=$(< ${cfg.captcha.saltFile})";
databaseLocalUrl = ''export DATABASE_URL="postgres:///${cfg.database.name}?host=/run/postgresql"'';
databasePassword = "export MCAPTCHA_DATABASE_PASSWORD=$(< ${cfg.database.passwordFile})";
redisLocalUrl = ''export MCAPTCHA_REDIS_URL="redis://${cfg.redis.host}:${builtins.toString cfg.redis.port}"'';
redisRemoteUrl = ''
redis_user=$(${pkgs.urlencode}/bin/urlencode -e userinfo ${lib.escapeShellArg cfg.redis.user})
redis_pass=$(${pkgs.urlencode}/bin/urlencode -e userinfo < ${cfg.redis.passwordFile})
export MCAPTCHA_REDIS_URL="redis://$redis_user:$redis_pass@${cfg.redis.host}:${builtins.toString cfg.redis.port}"
'';
exec = "exec ${cfg.package}/bin/mcaptcha";
in
concatStringsSep "\n" [
serverCookieSecret
captchaSalt
(
if cfg.database.createLocally
then databaseLocalUrl
else databasePassword
)
(
if cfg.redis.createLocally
then redisLocalUrl
else redisRemoteUrl
)
exec
];

systemd.services.mcaptcha.environment.MCAPTCHA_CONFIG = builtins.toString configFile;
systemd.services.mcaptcha.after = ["syslog.target"] ++ lib.optionals cfg.database.createLocally ["postgresql.service"];
systemd.services.mcaptcha.bindsTo = lib.optionals cfg.database.createLocally ["postgresql.service"];
systemd.services.mcaptcha.wants = ["network-online.target"];
systemd.services.mcaptcha.wantedBy = ["multi-user.target"];
# Settings modeled after https://github.com/mCaptcha/mCaptcha/blob/f337ee0643d88723776e1de4e5588dfdb6c0c574/docs/DEPLOYMENT.md#6-systemd-service-configuration
systemd.services.mcaptcha.serviceConfig.User = cfg.user;
systemd.services.mcaptcha.serviceConfig.Type = "simple";
systemd.services.mcaptcha.serviceConfig.Restart = "on-failure";
systemd.services.mcaptcha.serviceConfig.RestartSec = 1;
systemd.services.mcaptcha.serviceConfig.SuccessExitStatus = "3 4";
systemd.services.mcaptcha.serviceConfig.RestartForceExitStatus = "3 4";
systemd.services.mcaptcha.serviceConfig.SystemCallArchitectures = "native";
systemd.services.mcaptcha.serviceConfig.MemoryDenyWriteExecute = true;
systemd.services.mcaptcha.serviceConfig.NoNewPrivileges = true;

users.users."${cfg.user}" = {
isSystemUser = true;
group = cfg.group;
};

users.groups."${cfg.group}" = {};

services.postgresql = lib.mkIf cfg.database.createLocally {
enable = true;
ensureDatabases = [cfg.database.name];
ensureUsers = [
{
name = cfg.user;
ensurePermissions = {"DATABASE ${cfg.database.name}" = "ALL PRIVILEGES";};
}
];
};

services.redis.servers.mcaptcha = lib.mkIf cfg.redis.createLocally {
enable = true;
port = cfg.redis.port;
extraParams = ["--loadmodule" "${pkgs.mcaptcha-cache}/lib/libcache.so"];
};
services.mcaptcha.redis.host = lib.mkIf cfg.redis.createLocally "127.0.0.1";
};
}
43 changes: 43 additions & 0 deletions pkgs/mcaptcha-cache/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
rustPlatform,
fetchFromGitHub,
lib,
}: let
src = fetchFromGitHub {
owner = "mCaptcha";
repo = "cache";
rev = "67d6c701baa804849abc53a78422a6da01358487";
# NOTE: Avoiding this typo fix (which caused a bug in libmcaptcha)
# https://github.com/mCaptcha/cache/commit/f30bc54e6374cf5fad07af8f3d38bbe5fbbb4b20
# until this is merged https://github.com/mCaptcha/libmcaptcha/pull/12
sha256 = "sha256-whRLgYkoBoVQiZwrmwBwqgHzPqqXC6g3na3YrH4/xVo=";
};
in
rustPlatform.buildRustPackage rec {
inherit src;
pname = "cache";
version = "unstable-2023-03-08";

cargoLock = {
lockFile = src + "/Cargo.lock";
outputHashes = {
"libmcaptcha-0.1.4" = "sha256-KwFT0Px5ZQGa26fjkiaT8lKc8ASVdfL/67E0hnaHl7I=";
};
};

nativeBuildInputs = [rustPlatform.bindgenHook];

# We are unable to figure out the following error 🤷
#
# ```
# warning: `mcaptcha-cache` (lib test) generated 4 warnings
# Finished test [unoptimized + debuginfo] target(s) in 0.03s
# Running unittests src/lib.rs (target/debug/deps/cache-e9c2ad24991bfc21)
# thread panicked while processing panic. aborting.
# error: test failed, to rerun pass `--lib`
#
# Caused by:
# process didn't exit successfully: `<redacted>/mcaptcha-cache/target/debug/deps/cache-e9c2ad24991bfc21` (signal: 6, SIGABRT: process abort signal)
# ```
doCheck = false;
}
Loading

0 comments on commit deb0eb8

Please sign in to comment.