at 25.11-pre 12 kB view raw
1{ 2 lib, 3 pkgs, 4 config, 5 ... 6}: 7 8let 9 settingsFormat = pkgs.formats.yaml { }; 10 defaultUser = "slskd"; 11in 12{ 13 options.services.slskd = 14 with lib; 15 with types; 16 { 17 enable = mkEnableOption "slskd"; 18 19 package = mkPackageOption pkgs "slskd" { }; 20 21 user = mkOption { 22 type = types.str; 23 default = defaultUser; 24 description = "User account under which slskd runs."; 25 }; 26 27 group = mkOption { 28 type = types.str; 29 default = defaultUser; 30 description = "Group under which slskd runs."; 31 }; 32 33 domain = mkOption { 34 type = types.nullOr types.str; 35 description = '' 36 If non-null, enables an nginx reverse proxy virtual host at this FQDN, 37 at the path configurated with `services.slskd.web.url_base`. 38 ''; 39 example = "slskd.example.com"; 40 }; 41 42 nginx = mkOption { 43 type = types.submodule (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }); 44 default = { }; 45 example = lib.literalExpression '' 46 { 47 enableACME = true; 48 forceHttps = true; 49 } 50 ''; 51 description = '' 52 This option customizes the nginx virtual host set up for slskd. 53 ''; 54 }; 55 56 environmentFile = mkOption { 57 type = path; 58 description = '' 59 Path to the environment file sourced on startup. 60 It must at least contain the variables `SLSKD_SLSK_USERNAME` and `SLSKD_SLSK_PASSWORD`. 61 Web interface credentials should also be set here in `SLSKD_USERNAME` and `SLSKD_PASSWORD`. 62 Other, optional credentials like SOCKS5 with `SLSKD_SLSK_PROXY_USERNAME` and `SLSKD_SLSK_PROXY_PASSWORD` 63 should all reside here instead of in the world-readable nix store. 64 Variables are documented at <https://github.com/slskd/slskd/blob/master/docs/config.md> 65 ''; 66 }; 67 68 openFirewall = mkOption { 69 type = bool; 70 description = "Whether to open the firewall for the soulseek network listen port (not the web interface port)."; 71 default = false; 72 }; 73 74 settings = mkOption { 75 description = '' 76 Application configuration for slskd. See 77 [documentation](https://github.com/slskd/slskd/blob/master/docs/config.md). 78 ''; 79 default = { }; 80 type = submodule { 81 freeformType = settingsFormat.type; 82 options = { 83 remote_file_management = mkEnableOption "modification of share contents through the web ui"; 84 85 flags = { 86 force_share_scan = mkOption { 87 type = bool; 88 description = "Force a rescan of shares on every startup."; 89 }; 90 no_version_check = mkOption { 91 type = bool; 92 default = true; 93 visible = false; 94 description = "Don't perform a version check on startup."; 95 }; 96 }; 97 98 directories = { 99 incomplete = mkOption { 100 type = nullOr path; 101 description = "Directory where incomplete downloading files are stored."; 102 defaultText = "/var/lib/slskd/incomplete"; 103 default = null; 104 }; 105 downloads = mkOption { 106 type = nullOr path; 107 description = "Directory where downloaded files are stored."; 108 defaultText = "/var/lib/slskd/downloads"; 109 default = null; 110 }; 111 }; 112 113 shares = { 114 directories = mkOption { 115 type = listOf str; 116 description = '' 117 Paths to shared directories. See 118 [documentation](https://github.com/slskd/slskd/blob/master/docs/config.md#directories) 119 for advanced usage. 120 ''; 121 example = lib.literalExpression ''[ "/home/John/Music" "!/home/John/Music/Recordings" "[Music Drive]/mnt" ]''; 122 }; 123 filters = mkOption { 124 type = listOf str; 125 example = lib.literalExpression ''[ "\.ini$" "Thumbs.db$" "\.DS_Store$" ]''; 126 description = "Regular expressions of files to exclude from sharing."; 127 }; 128 }; 129 130 rooms = mkOption { 131 type = listOf str; 132 description = "Chat rooms to join on startup."; 133 }; 134 135 soulseek = { 136 description = mkOption { 137 type = str; 138 description = "The user description for the Soulseek network."; 139 defaultText = "A slskd user. https://github.com/slskd/slskd"; 140 }; 141 listen_port = mkOption { 142 type = port; 143 description = "The port on which to listen for incoming connections."; 144 default = 50300; 145 }; 146 }; 147 148 global = { 149 # TODO speed units 150 upload = { 151 slots = mkOption { 152 type = ints.unsigned; 153 description = "Limit of the number of concurrent upload slots."; 154 }; 155 speed_limit = mkOption { 156 type = ints.unsigned; 157 description = "Total upload speed limit."; 158 }; 159 }; 160 download = { 161 slots = mkOption { 162 type = ints.unsigned; 163 description = "Limit of the number of concurrent download slots."; 164 }; 165 speed_limit = mkOption { 166 type = ints.unsigned; 167 description = "Total upload download limit"; 168 }; 169 }; 170 }; 171 172 filters.search.request = mkOption { 173 type = listOf str; 174 example = lib.literalExpression ''[ "^.{1,2}$" ]''; 175 description = "Incoming search requests which match this filter are ignored."; 176 }; 177 178 web = { 179 port = mkOption { 180 type = port; 181 default = 5030; 182 description = "The HTTP listen port."; 183 }; 184 url_base = mkOption { 185 type = path; 186 default = "/"; 187 description = "The base path in the url for web requests."; 188 }; 189 # Users should use a reverse proxy instead for https 190 https.disabled = mkOption { 191 type = bool; 192 default = true; 193 description = "Disable the built-in HTTPS server"; 194 }; 195 }; 196 197 retention = { 198 transfers = { 199 upload = { 200 succeeded = mkOption { 201 type = ints.unsigned; 202 description = "Lifespan of succeeded upload tasks."; 203 defaultText = "(indefinite)"; 204 }; 205 errored = mkOption { 206 type = ints.unsigned; 207 description = "Lifespan of errored upload tasks."; 208 defaultText = "(indefinite)"; 209 }; 210 cancelled = mkOption { 211 type = ints.unsigned; 212 description = "Lifespan of cancelled upload tasks."; 213 defaultText = "(indefinite)"; 214 }; 215 }; 216 download = { 217 succeeded = mkOption { 218 type = ints.unsigned; 219 description = "Lifespan of succeeded download tasks."; 220 defaultText = "(indefinite)"; 221 }; 222 errored = mkOption { 223 type = ints.unsigned; 224 description = "Lifespan of errored download tasks."; 225 defaultText = "(indefinite)"; 226 }; 227 cancelled = mkOption { 228 type = ints.unsigned; 229 description = "Lifespan of cancelled download tasks."; 230 defaultText = "(indefinite)"; 231 }; 232 }; 233 }; 234 files = { 235 complete = mkOption { 236 type = ints.unsigned; 237 description = "Lifespan of completely downloaded files in minutes."; 238 example = 20160; 239 defaultText = "(indefinite)"; 240 }; 241 incomplete = mkOption { 242 type = ints.unsigned; 243 description = "Lifespan of incomplete downloading files in minutes."; 244 defaultText = "(indefinite)"; 245 }; 246 }; 247 }; 248 249 logger = { 250 # Disable by default, journald already retains as needed 251 disk = mkOption { 252 type = bool; 253 description = "Whether to log to the application directory."; 254 default = false; 255 visible = false; 256 }; 257 }; 258 }; 259 }; 260 }; 261 }; 262 263 config = 264 let 265 cfg = config.services.slskd; 266 267 confWithoutNullValues = ( 268 lib.filterAttrsRecursive ( 269 key: value: (builtins.tryEval value).success && value != null 270 ) cfg.settings 271 ); 272 273 configurationYaml = settingsFormat.generate "slskd.yml" confWithoutNullValues; 274 275 in 276 lib.mkIf cfg.enable { 277 278 # Force off, configuration file is in nix store and is immutable 279 services.slskd.settings.remote_configuration = lib.mkForce false; 280 281 users.users = lib.optionalAttrs (cfg.user == defaultUser) { 282 "${defaultUser}" = { 283 group = cfg.group; 284 isSystemUser = true; 285 }; 286 }; 287 288 users.groups = lib.optionalAttrs (cfg.group == defaultUser) { 289 "${defaultUser}" = { }; 290 }; 291 292 systemd.services.slskd = { 293 description = "A modern client-server application for the Soulseek file sharing network"; 294 after = [ "network.target" ]; 295 wantedBy = [ "multi-user.target" ]; 296 serviceConfig = { 297 Type = "simple"; 298 User = cfg.user; 299 Group = cfg.group; 300 Environment = [ "DOTNET_USE_POLLING_FILE_WATCHER=1" ]; 301 EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile; 302 StateDirectory = "slskd"; # Creates /var/lib/slskd and manages permissions 303 ExecStart = "${cfg.package}/bin/slskd --app-dir /var/lib/slskd --config ${configurationYaml}"; 304 Restart = "on-failure"; 305 ReadOnlyPaths = map ( 306 d: builtins.elemAt (builtins.split "[^/]*(/.+)" d) 1 307 ) cfg.settings.shares.directories; 308 ReadWritePaths = 309 (lib.optional (cfg.settings.directories.incomplete != null) cfg.settings.directories.incomplete) 310 ++ (lib.optional (cfg.settings.directories.downloads != null) cfg.settings.directories.downloads); 311 LockPersonality = true; 312 NoNewPrivileges = true; 313 PrivateDevices = true; 314 PrivateMounts = true; 315 PrivateTmp = true; 316 PrivateUsers = true; 317 ProtectClock = true; 318 ProtectControlGroups = true; 319 ProtectHome = true; 320 ProtectHostname = true; 321 ProtectKernelLogs = true; 322 ProtectKernelModules = true; 323 ProtectKernelTunables = true; 324 ProtectProc = "invisible"; 325 ProtectSystem = "strict"; 326 RemoveIPC = true; 327 RestrictNamespaces = true; 328 RestrictSUIDSGID = true; 329 }; 330 }; 331 332 networking.firewall.allowedTCPPorts = lib.optional cfg.openFirewall cfg.settings.soulseek.listen_port; 333 334 services.nginx = lib.mkIf (cfg.domain != null) { 335 enable = lib.mkDefault true; 336 virtualHosts."${cfg.domain}" = lib.mkMerge [ 337 cfg.nginx 338 { 339 locations."${cfg.settings.web.url_base}" = { 340 proxyPass = "http://127.0.0.1:${toString cfg.settings.web.port}"; 341 proxyWebsockets = true; 342 }; 343 } 344 ]; 345 }; 346 }; 347 348 meta = { 349 maintainers = with lib.maintainers; [ 350 ppom 351 melvyn2 352 ]; 353 }; 354}