nixpkgs/nixos/modules/services/web-apps/slskd.nix

334 lines
11 KiB
Nix

{ lib, pkgs, config, ... }:
let
settingsFormat = pkgs.formats.yaml {};
defaultUser = "slskd";
in {
options.services.slskd = with lib; with types; {
enable = mkEnableOption "enable slskd";
package = mkPackageOptionMD pkgs "slskd" { };
user = mkOption {
type = types.str;
default = defaultUser;
description = "User account under which slskd runs.";
};
group = mkOption {
type = types.str;
default = defaultUser;
description = "Group under which slskd runs.";
};
domain = mkOption {
type = types.nullOr types.str;
description = ''
If non-null, enables an nginx reverse proxy virtual host at this FQDN,
at the path configurated with `services.slskd.web.url_base`.
'';
example = "slskd.example.com";
};
nginx = mkOption {
type = types.submodule (import ../web-servers/nginx/vhost-options.nix { inherit config lib; });
default = {};
example = lib.literalExpression ''
{
enableACME = true;
forceHttps = true;
}
'';
description = ''
This option customizes the nginx virtual host set up for slskd.
'';
};
environmentFile = mkOption {
type = path;
description = ''
Path to the environment file sourced on startup.
It must at least contain the variables `SLSKD_SLSK_USERNAME` and `SLSKD_SLSK_PASSWORD`.
Web interface credentials should also be set here in `SLSKD_USERNAME` and `SLSKD_PASSWORD`.
Other, optional credentials like SOCKS5 with `SLSKD_SLSK_PROXY_USERNAME` and `SLSKD_SLSK_PROXY_PASSWORD`
should all reside here instead of in the world-readable nix store.
Variables are documented at https://github.com/slskd/slskd/blob/master/docs/config.md
'';
};
openFirewall = mkOption {
type = bool;
description = "Whether to open the firewall for the soulseek network listen port (not the web interface port).";
default = false;
};
settings = mkOption {
description = ''
Application configuration for slskd. See
[documentation](https://github.com/slskd/slskd/blob/master/docs/config.md).
'';
default = {};
type = submodule {
freeformType = settingsFormat.type;
options = {
remote_file_management = mkEnableOption "modification of share contents through the web ui";
flags = {
force_share_scan = mkOption {
type = bool;
description = "Force a rescan of shares on every startup.";
};
no_version_check = mkOption {
type = bool;
default = true;
visible = false;
description = "Don't perform a version check on startup.";
};
};
directories = {
incomplete = mkOption {
type = nullOr path;
description = "Directory where incomplete downloading files are stored.";
defaultText = "/var/lib/slskd/incomplete";
default = null;
};
downloads = mkOption {
type = nullOr path;
description = "Directory where downloaded files are stored.";
defaultText = "/var/lib/slskd/downloads";
default = null;
};
};
shares = {
directories = mkOption {
type = listOf str;
description = ''
Paths to shared directories. See
[documentation](https://github.com/slskd/slskd/blob/master/docs/config.md#directories)
for advanced usage.
'';
example = lib.literalExpression ''[ "/home/John/Music" "!/home/John/Music/Recordings" "[Music Drive]/mnt" ]'';
};
filters = mkOption {
type = listOf str;
example = lib.literalExpression ''[ "\.ini$" "Thumbs.db$" "\.DS_Store$" ]'';
description = "Regular expressions of files to exclude from sharing.";
};
};
rooms = mkOption {
type = listOf str;
description = "Chat rooms to join on startup.";
};
soulseek = {
description = mkOption {
type = str;
description = "The user description for the Soulseek network.";
defaultText = "A slskd user. https://github.com/slskd/slskd";
};
listen_port = mkOption {
type = port;
description = "The port on which to listen for incoming connections.";
default = 50300;
};
};
global = {
# TODO speed units
upload = {
slots = mkOption {
type = ints.unsigned;
description = "Limit of the number of concurrent upload slots.";
};
speed_limit = mkOption {
type = ints.unsigned;
description = "Total upload speed limit.";
};
};
download = {
slots = mkOption {
type = ints.unsigned;
description = "Limit of the number of concurrent download slots.";
};
speed_limit = mkOption {
type = ints.unsigned;
description = "Total upload download limit";
};
};
};
filters.search.request = mkOption {
type = listOf str;
example = lib.literalExpression ''[ "^.{1,2}$" ]'';
description = "Incoming search requests which match this filter are ignored.";
};
web = {
port = mkOption {
type = port;
default = 5030;
description = "The HTTP listen port.";
};
url_base = mkOption {
type = path;
default = "/";
description = "The base path in the url for web requests.";
};
# Users should use a reverse proxy instead for https
https.disabled = mkOption {
type = bool;
default = true;
description = "Disable the built-in HTTPS server";
};
};
retention = {
transfers = {
upload = {
succeeded = mkOption {
type = ints.unsigned;
description = "Lifespan of succeeded upload tasks.";
defaultText = "(indefinite)";
};
errored = mkOption {
type = ints.unsigned;
description = "Lifespan of errored upload tasks.";
defaultText = "(indefinite)";
};
cancelled = mkOption {
type = ints.unsigned;
description = "Lifespan of cancelled upload tasks.";
defaultText = "(indefinite)";
};
};
download = {
succeeded = mkOption {
type = ints.unsigned;
description = "Lifespan of succeeded download tasks.";
defaultText = "(indefinite)";
};
errored = mkOption {
type = ints.unsigned;
description = "Lifespan of errored download tasks.";
defaultText = "(indefinite)";
};
cancelled = mkOption {
type = ints.unsigned;
description = "Lifespan of cancelled download tasks.";
defaultText = "(indefinite)";
};
};
};
files = {
complete = mkOption {
type = ints.unsigned;
description = "Lifespan of completely downloaded files in minutes.";
example = 20160;
defaultText = "(indefinite)";
};
incomplete = mkOption {
type = ints.unsigned;
description = "Lifespan of incomplete downloading files in minutes.";
defaultText = "(indefinite)";
};
};
};
logger = {
# Disable by default, journald already retains as needed
disk = mkOption {
type = bool;
description = "Whether to log to the application directory.";
default = false;
visible = false;
};
};
};
};
};
};
config = let
cfg = config.services.slskd;
confWithoutNullValues = (lib.filterAttrsRecursive (key: value: (builtins.tryEval value).success && value != null) cfg.settings);
configurationYaml = settingsFormat.generate "slskd.yml" confWithoutNullValues;
in lib.mkIf cfg.enable {
# Force off, configuration file is in nix store and is immutable
services.slskd.settings.remote_configuration = lib.mkForce false;
users.users = lib.optionalAttrs (cfg.user == defaultUser) {
"${defaultUser}" = {
group = cfg.group;
isSystemUser = true;
};
};
users.groups = lib.optionalAttrs (cfg.group == defaultUser) {
"${defaultUser}" = {};
};
systemd.services.slskd = {
description = "A modern client-server application for the Soulseek file sharing network";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.group;
EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
StateDirectory = "slskd"; # Creates /var/lib/slskd and manages permissions
ExecStart = "${cfg.package}/bin/slskd --app-dir /var/lib/slskd --config ${configurationYaml}";
Restart = "on-failure";
ReadOnlyPaths = map (d: builtins.elemAt (builtins.split "[^/]*(/.+)" d) 1) cfg.settings.shares.directories;
ReadWritePaths =
(lib.optional (cfg.settings.directories.incomplete != null) cfg.settings.directories.incomplete) ++
(lib.optional (cfg.settings.directories.downloads != null) cfg.settings.directories.downloads);
LockPersonality = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateMounts = true;
PrivateTmp = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
RemoveIPC = true;
RestrictNamespaces = true;
RestrictSUIDSGID = true;
};
};
networking.firewall.allowedTCPPorts = lib.optional cfg.openFirewall cfg.settings.soulseek.listen_port;
services.nginx = lib.mkIf (cfg.domain != null) {
enable = lib.mkDefault true;
virtualHosts."${cfg.domain}" = lib.mkMerge [
cfg.nginx
{
locations."${cfg.settings.web.url_base}" = {
proxyPass = "http://127.0.0.1:${toString cfg.settings.web.port}";
proxyWebsockets = true;
};
}
];
};
};
meta = {
maintainers = with lib.maintainers; [ ppom melvyn2 ];
};
}