Files
nix-stuff/modules/hath/module.nix
Shelvacu 59d5ef53a4 nix fmt
2025-07-11 11:08:00 -07:00

220 lines
6.5 KiB
Nix

# TODO: what is the right way to make sure dirs are created automatically? What's the right way to initialize client id and client key without interactive?
# see: https://ehwiki.org/wiki/Hentai@Home
{
lib,
config,
pkgs,
...
}:
let
inherit (lib) mkOption types;
cfg = config.vacu.hath;
flags =
[
"--cache-dir=${cfg.cacheDir}"
"--data-dir=${cfg.dataDir}"
"--download-dir=${cfg.downloadDir}"
"--log-dir=${cfg.logDir}"
]
++ lib.optional (!cfg.bandwidthMonitor) "--disable_bwm"
++ lib.optional (!cfg.logging) "--disable_logging"
++ lib.optional cfg.flushLogs "--flush-logs"
++ lib.optional (cfg.maxConnections != null) "--max-connections=${toString cfg.maxConnections}"
++ lib.optional (cfg.port != null) "--port=${toString cfg.port}"
++ lib.optional (!cfg.freeSpaceCheck) "--skip_free_space_check"
++ lib.optional (!cfg.ipOriginCheck) "--disable-ip-origin-check"
++ lib.optional (!cfg.floodControl) "--disable-flood-control";
fullCommand = lib.singleton (lib.getExe cfg.package) ++ flags;
dirs = [
cfg.cacheDir
cfg.dataDir
cfg.downloadDir
cfg.logDir
];
capabilities = [ ] ++ lib.optional cfg.allowPrivilegedPort "CAP_NET_BIND_SERVICE";
credentialsType = types.submodule (
{ ... }:
{
options.clientId = mkOption { type = types.ints.unsigned; };
options.clientKeyPath = mkOption { type = types.path; };
}
);
in
{
options.vacu.hath = {
enable = lib.mkEnableOption "hath";
package = mkOption {
type = types.package;
default = pkgs.hentai-at-home;
};
user = mkOption {
type = types.passwdEntry types.str;
default = "hath";
readOnly = true;
};
group = mkOption {
type = types.passwdEntry types.str;
default = "hath";
readOnly = true;
};
autoStart = mkOption {
type = types.bool;
default = false;
};
allowPrivilegedPort = mkOption {
type = types.bool;
default = if cfg.port == null then true else cfg.port < 1024;
};
credentials = mkOption {
type = types.nullOr credentialsType;
default = null;
description = "The credentials for this client. If null, credentials must be provided to the H@H client manually.";
};
bandwidthMonitor = mkOption {
type = types.bool;
default = true;
description = "Inverse of the `--disable_bwm` option";
};
logging = mkOption {
type = types.bool;
default = true;
description = "Inverse of the `--disable_logging` option";
};
flushLogs = mkOption {
type = types.bool;
default = false;
description = "Equivalent to the `--flush-logs` option";
};
maxConnections = mkOption {
type = types.nullOr types.int;
default = null;
description = "Equivalent to `--max_connections=<n>`";
};
port = mkOption {
type = types.nullOr types.port;
default = null;
description = "`--port=<n>`";
};
freeSpaceCheck = mkOption {
type = types.bool;
default = true;
description = "Inverse of `--skip_free_space_check`";
};
ipOriginCheck = mkOption {
type = types.bool;
default = true;
description = "Inverse of `--disable-ip-origin-check`";
};
floodControl = mkOption {
type = types.bool;
default = true;
description = "Inverse of `--disable-flood-control`";
};
baseDir = mkOption {
type = types.path;
default = "/var/lib/hath";
};
cacheDir = mkOption {
type = types.path;
default = "${cfg.baseDir}/cache";
};
dataDir = mkOption {
type = types.path;
default = "${cfg.baseDir}/data";
};
downloadDir = mkOption {
type = types.path;
default = "${cfg.baseDir}/download";
};
logDir = mkOption {
type = types.path;
default = "${cfg.baseDir}/log";
};
clientLoginPath = mkOption {
type = types.path;
default = "${cfg.dataDir}/client_login";
readOnly = true;
description = "File containing the credentials, in the format {client_id}`-`{client_key}";
};
};
config = lib.mkIf cfg.enable {
users.users.${cfg.user} = {
isSystemUser = true;
group = cfg.group;
};
users.groups.${cfg.group} = { };
systemd.services.hath = {
wantedBy = lib.mkIf cfg.autoStart [ "multi-user.target" ];
description = "Hentai@Home client";
preStart = ''
set -euo pipefail
all_dirs=(${lib.escapeShellArgs dirs})
for d in "''${all_dirs[@]}"; do
containing_dir="$(dirname -- "$d")"
mkdir -p -- "$containing_dir"
if ! [[ -d "$d" ]]; then
install --owner=${lib.escapeShellArg cfg.user} --group=${lib.escapeShellArg cfg.group} --mode=rwxr-x--- -d -- "$d"
fi
done
${lib.optionalString (cfg.credentials != null) ''
client_id="${toString cfg.credentials.clientId}"
client_key="$(cat ${lib.escapeShellArg cfg.credentials.clientKeyPath})"
printf '%s-%s' "$client_id" "$client_key" > ${lib.escapeShellArg cfg.clientLoginPath}
''}
'';
script = "exec ${lib.escapeShellArgs fullCommand}";
serviceConfig = {
Type = "exec";
User = cfg.user;
Group = cfg.group;
BindReadOnlyPaths = [
"/nix/store"
"-/etc/resolv.conf"
"-/etc/nsswitch.conf"
"-/etc/hosts"
"-/etc/localtime"
];
BindPaths = dirs;
CapabilityBoundingSet = capabilities;
AmbientCapabilities = capabilities;
DeviceAllow = "";
ProtectSystem = "strict";
LockPersonality = true;
# it's java, which has a JIT, so it needs write-execute memory
# MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateMounts = true;
PrivateTmp = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectHome = true;
ProtectHostname = true;
ProtectControlGroups = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~pkey_alloc:ENOSPC"
];
UMask = "0027"; # this makes the default permissions u::rwx,g::r-x,o::---
};
};
};
}