diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 98340036f7b6..a014c93afede 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1550,6 +1550,7 @@ ./services/web-servers/phpfpm/default.nix ./services/web-servers/pomerium.nix ./services/web-servers/rustus.nix + ./services/web-servers/send.nix ./services/web-servers/stargazer.nix ./services/web-servers/static-web-server.nix ./services/web-servers/tomcat.nix diff --git a/nixos/modules/services/web-servers/send.nix b/nixos/modules/services/web-servers/send.nix new file mode 100644 index 000000000000..696fbbdc7c80 --- /dev/null +++ b/nixos/modules/services/web-servers/send.nix @@ -0,0 +1,228 @@ +{ + config, + lib, + pkgs, + ... +}: +let + inherit (lib) mkOption types; + cfg = config.services.send; +in +{ + options = { + services.send = { + enable = lib.mkEnableOption "Send, a file sharing web sevice for ffsend."; + + package = lib.mkPackageOption pkgs "send" { }; + + environment = mkOption { + type = + with types; + attrsOf ( + nullOr (oneOf [ + bool + int + str + (listOf int) + ]) + ); + description = '' + All the available config options and their defaults can be found here: https://github.com/timvisee/send/blob/master/server/config.js, + some descriptions can found here: https://github.com/timvisee/send/blob/master/docs/docker.md#environment-variables + + Values under {option}`services.send.environment` will override the predefined values in the Send service. + - Time/duration should be in seconds + - Filesize values should be in bytes + ''; + example = { + DEFAULT_DOWNLOADS = 1; + DETECT_BASE_URL = true; + EXPIRE_TIMES_SECONDS = [ + 300 + 3600 + 86400 + 604800 + ]; + }; + }; + + dataDir = lib.mkOption { + type = types.path; + readOnly = true; + default = "/var/lib/send"; + description = '' + Directory for uploaded files. + Due to limitations in {option}`systemd.services.send.serviceConfig.DynamicUser`, this item is read only. + ''; + }; + + baseUrl = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Base URL for the Send service. + Leave it blank to automatically detect the base url. + ''; + }; + + host = lib.mkOption { + type = types.str; + default = "127.0.0.1"; + description = "The hostname or IP address for Send to bind to."; + }; + + port = lib.mkOption { + type = types.port; + default = 1443; + description = "Port the Send service listens on."; + }; + + openFirewall = lib.mkOption { + type = types.bool; + default = false; + description = "Whether to open firewall ports for send"; + }; + + redis = { + createLocally = lib.mkOption { + type = types.bool; + default = true; + description = "Whether to create a local redis automatically."; + }; + + name = lib.mkOption { + type = types.str; + default = "send"; + description = '' + Name of the redis server. + Only used if {option}`services.send.redis.createLocally` is set to true. + ''; + }; + + host = lib.mkOption { + type = types.str; + default = "localhost"; + description = "Redis server address."; + }; + + port = lib.mkOption { + type = types.port; + default = 6379; + description = "Port of the redis server."; + }; + + passwordFile = mkOption { + type = types.nullOr types.path; + default = null; + example = "/run/agenix/send-redis-password"; + description = '' + The path to the file containing the Redis password. + + If {option}`services.send.redis.createLocally` is set to true, + the content of this file will be used as the password for the locally created Redis instance. + + Leave it blank if no password is required. + ''; + }; + }; + }; + }; + + config = lib.mkIf cfg.enable { + + services.send.environment.DETECT_BASE_URL = cfg.baseUrl == null; + + assertions = [ + { + assertion = cfg.redis.createLocally -> cfg.redis.host == "localhost"; + message = "the redis host must be localhost if services.send.redis.createLocally is set to true"; + } + ]; + + networking.firewall.allowedTCPPorts = lib.optional cfg.openFirewall cfg.port; + + services.redis = lib.optionalAttrs cfg.redis.createLocally { + servers."${cfg.redis.name}" = { + enable = true; + bind = "localhost"; + port = cfg.redis.port; + }; + }; + + systemd.services.send = { + serviceConfig = { + Type = "simple"; + Restart = "always"; + StateDirectory = "send"; + WorkingDirectory = cfg.dataDir; + ReadWritePaths = cfg.dataDir; + LoadCredential = lib.optionalString ( + cfg.redis.passwordFile != null + ) "redis-password:${cfg.redis.passwordFile}"; + + # Hardening + RestrictAddressFamilies = [ + "AF_UNIX" + "AF_INET" + "AF_INET6" + ]; + AmbientCapabilities = lib.optionalString (cfg.port < 1024) "cap_net_bind_service"; + DynamicUser = true; + CapabilityBoundingSet = ""; + NoNewPrivileges = true; + RemoveIPC = true; + PrivateTmp = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "full"; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + UMask = "0077"; + }; + environment = + { + IP_ADDRESS = cfg.host; + PORT = toString cfg.port; + BASE_URL = if (cfg.baseUrl == null) then "http://${cfg.host}:${toString cfg.port}" else cfg.baseUrl; + FILE_DIR = cfg.dataDir + "/uploads"; + REDIS_HOST = cfg.redis.host; + REDIS_PORT = toString cfg.redis.port; + } + // (lib.mapAttrs ( + name: value: + if lib.isList value then + "[" + lib.concatStringsSep ", " (map (x: toString x) value) + "]" + else if lib.isBool value then + lib.boolToString value + else + toString value + ) cfg.environment); + after = + [ + "network.target" + ] + ++ lib.optionals cfg.redis.createLocally [ + "redis-${cfg.redis.name}.service" + ]; + description = "Send web service"; + wantedBy = [ "multi-user.target" ]; + script = '' + ${lib.optionalString (cfg.redis.passwordFile != null) '' + export REDIS_PASSWORD="$(cat $CREDENTIALS_DIRECTORY/redis-password)" + ''} + ${lib.getExe cfg.package} + ''; + }; + }; + + meta.maintainers = with lib.maintainers; [ moraxyc ]; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 77632ca4b33b..5409605f763a 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -904,6 +904,7 @@ in { seafile = handleTest ./seafile.nix {}; searx = runTest ./searx.nix; seatd = handleTest ./seatd.nix {}; + send = runTest ./send.nix; service-runner = handleTest ./service-runner.nix {}; sftpgo = runTest ./sftpgo.nix; sfxr-qt = handleTest ./sfxr-qt.nix {}; diff --git a/nixos/tests/send.nix b/nixos/tests/send.nix new file mode 100644 index 000000000000..b02f083fef9f --- /dev/null +++ b/nixos/tests/send.nix @@ -0,0 +1,34 @@ +{ lib, pkgs, ... }: +{ + name = "send"; + + meta = { + maintainers = with lib.maintainers; [ moraxyc ]; + }; + + nodes.machine = + { pkgs, ... }: + { + environment.systemPackages = with pkgs; [ + curl + ffsend + ]; + + services.send = { + enable = true; + }; + }; + + testScript = '' + machine.wait_for_unit("send.service") + + machine.wait_for_open_port(1443) + + machine.succeed("curl --fail --max-time 10 http://127.0.0.1:1443") + + machine.succeed("echo HelloWorld > /tmp/test") + url = machine.succeed("ffsend upload -q -h http://127.0.0.1:1443/ /tmp/test") + machine.succeed(f'ffsend download --output /tmp/download {url}') + machine.succeed("cat /tmp/download | grep HelloWorld") + ''; +}