From aff5b3aa1a95876fd426ab024d68ab2d87b0ebbd Mon Sep 17 00:00:00 2001 From: 3JlOy_PYCCKUI <3jl0y_pycckui@riseup.net> Date: Fri, 24 Nov 2023 23:41:55 +0300 Subject: [PATCH] nixos/flood: init --- .../manual/release-notes/rl-2405.section.md | 2 + nixos/modules/module-list.nix | 1 + nixos/modules/services/torrent/flood.nix | 287 ++++++++++++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/flood.nix | 44 +++ .../networking/p2p/flood/default.nix | 3 + 6 files changed, 338 insertions(+) create mode 100644 nixos/modules/services/torrent/flood.nix create mode 100644 nixos/tests/flood.nix diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md index bfd4bcee63d38..4295814241282 100644 --- a/nixos/doc/manual/release-notes/rl-2405.section.md +++ b/nixos/doc/manual/release-notes/rl-2405.section.md @@ -14,6 +14,8 @@ In addition to numerous new and upgraded packages, this release has the followin +- [Flood](https://flood.js.org), a beautiful web UI for various torrent clients. Available as [services.flood](#opt-services.flood.enable). + - [Guix](https://guix.gnu.org), a functional package manager inspired by Nix. Available as [services.guix](#opt-services.guix.enable). - [maubot](https://github.com/maubot/maubot), a plugin-based Matrix bot framework. Available as [services.maubot](#opt-services.maubot.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index fee7c35ed8f45..2d2ebe6ae43f2 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1220,6 +1220,7 @@ ./services/system/zram-generator.nix ./services/torrent/deluge.nix ./services/torrent/flexget.nix + ./services/torrent/flood.nix ./services/torrent/magnetico.nix ./services/torrent/opentracker.nix ./services/torrent/peerflix.nix diff --git a/nixos/modules/services/torrent/flood.nix b/nixos/modules/services/torrent/flood.nix new file mode 100644 index 0000000000000..befbceef0e2df --- /dev/null +++ b/nixos/modules/services/torrent/flood.nix @@ -0,0 +1,287 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.services.flood; +in +{ + options = { + services = { + flood = { + enable = lib.mkEnableOption (lib.mdDoc "Flood daemon"); + + package = lib.mkPackageOptionMD pkgs "flood" { }; + + baseUrl = lib.mkOption { + type = lib.types.str; + default = "/"; + description = lib.mdDoc '' + This URI will prefix all of Flood's HTTP requests. + ''; + }; + + address = lib.mkOption { + type = lib.types.str; + default = "127.0.0.1"; + description = lib.mdDoc '' + The host (address) that Flood should listen for web connections on. + ''; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 3000; + description = lib.mdDoc '' + The port that Flood should listen for web connections on. + ''; + }; + + openFirewall = lib.mkOption { + default = false; + type = lib.types.bool; + description = lib.mdDoc '' + Whether to open the firewall for the port in + {option}`services.flood.port`. + ''; + }; + + ssl = { + enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = lib.mdDoc '' + Enable SSL. + ''; + }; + + key = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = lib.mdDoc '' + Absolute path to private key for SSL. + ''; + }; + + cert = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = lib.mdDoc '' + Absolute path to fullchain cert for SSL. + ''; + }; + }; + + user = lib.mkOption { + type = lib.types.str; + default = "flood"; + description = lib.mdDoc '' + User account under which flood runs. + ''; + }; + + group = lib.mkOption { + type = lib.types.str; + default = "flood"; + description = lib.mdDoc '' + Group under which flood runs. + ''; + }; + + allowedPaths = lib.mkOption { + type = lib.types.listOf lib.types.path; + default = [ ]; + description = lib.mdDoc '' + List of allowed paths for file operations. + ''; + }; + + auth = { + deluge = { + host = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = lib.mdDoc '' + Host of Deluge RPC interface. + ''; + }; + port = lib.mkOption { + type = lib.types.nullOr lib.types.port; + default = null; + description = lib.mdDoc '' + Port of Deluge RPC interface. + ''; + }; + user = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = lib.mdDoc '' + Username of Deluge RPC interface. + ''; + }; + pass = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = lib.mdDoc '' + Password of Deluge RPC interface. + ''; + }; + }; + rtorrent = { + host = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = lib.mdDoc '' + Host of rTorrent's SCGI interface. + ''; + }; + port = lib.mkOption { + type = lib.types.nullOr lib.types.port; + default = null; + description = lib.mdDoc '' + Port of rTorrent's SCGI interface. + ''; + }; + socket = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = lib.mdDoc '' + Path to rTorrent's SCGI unix socket. + ''; + }; + }; + qbittorrent = { + url = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = lib.mdDoc '' + URL to qBittorrent Web API. + ''; + }; + user = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = lib.mdDoc '' + Username of qBittorrent Web API. + ''; + }; + pass = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = lib.mdDoc '' + Password of qBittorrent Web API. + ''; + }; + }; + transmission = { + url = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = lib.mdDoc '' + URL to Transmission RPC interface. + ''; + }; + user = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = lib.mdDoc '' + Username of Transmission RPC interface. + ''; + }; + pass = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = lib.mdDoc '' + Password of Transmission RPC interface. + ''; + }; + }; + }; + }; + }; + }; + + config = + let + addAuthOpt = prefix: opt: + lib.mapAttrsToList + (k: v: + "--${prefix}${toString k} ${toString v}") + (lib.filterAttrs (k: v: !isNull v) opt); + delugeAuth = addAuthOpt "de" cfg.auth.deluge; + rtorrentAuth = addAuthOpt "rt" cfg.auth.rtorrent; + qbittorrentAuth = addAuthOpt "qb" cfg.auth.qbittorrent; + transmissionAuth = addAuthOpt "tr" cfg.auth.transmission; + authOpts = lib.lists.findSingle (x: x != [ ]) [ ] [ null ] [ delugeAuth rtorrentAuth qbittorrentAuth transmissionAuth ]; + args = [ + "--rundir %S/flood" + "--baseuri ${cfg.baseUrl}" + "--host ${cfg.address}" + "--port ${toString cfg.port}" + ] + ++ lib.optionals cfg.ssl.enable [ "--ssl" ] + ++ lib.optionals (cfg.ssl.enable && (!isNull cfg.ssl.key)) [ "--sslkey ${cfg.ssl.key}" ] + ++ lib.optionals (cfg.ssl.enable && (!isNull cfg.ssl.cert)) [ "--sslcert ${cfg.ssl.cert}" ] + ++ lib.optionals (authOpts != [ ]) [ "--auth none" ] + ++ authOpts + ++ map (x: "--allowedpath ${toString x}") cfg.allowedPaths; + in + lib.mkIf cfg.enable { + assertions = [ + { + assertion = authOpts != [ null ]; + message = "Only one client authentication must be configured"; + } + ]; + systemd.services.flood = + { + after = [ "network.target" ]; + description = "Flood Daemon"; + wantedBy = [ "multi-user.target" ]; + path = [ pkgs.mediainfo ]; + serviceConfig = { + ExecStart = "${cfg.package}/bin/flood ${lib.concatStringsSep " " args}"; + Restart = "on-failure"; + UMask = "077"; + DynamicUser = true; + User = cfg.user; + Group = cfg.group; + StateDirectory = "flood"; + ReadWritePaths = cfg.allowedPaths ++ + lib.optionals (!isNull cfg.auth.rtorrent.socket) [ "-${cfg.auth.rtorrent.socket}" ]; + + AmbientCapabilities = [ "" ]; + CapabilityBoundingSet = [ "" ]; + DevicePolicy = "closed"; + ProtectSystem = "full"; + LockPersonality = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateTmp = true; + PrivateUsers = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProcSubset = "pid"; + ProtectProc = "invisible"; + RemoveIPC = true; + RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "~@cpu-emulation" + "~@debug" + "~@mount" + "~@obsolete" + "~@privileged" + "~@resources" + ]; + }; + }; + networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ cfg.port ]; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index e0572e3bed9cd..4d25eee105431 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -305,6 +305,7 @@ in { firewall-nftables = handleTest ./firewall.nix { nftables = true; }; fish = handleTest ./fish.nix {}; flannel = handleTestOn ["x86_64-linux"] ./flannel.nix {}; + flood = handleTest ./flood.nix {}; floorp = handleTest ./firefox.nix { firefoxPackage = pkgs.floorp; }; fluentd = handleTest ./fluentd.nix {}; fluidd = handleTest ./fluidd.nix {}; diff --git a/nixos/tests/flood.nix b/nixos/tests/flood.nix new file mode 100644 index 0000000000000..888c0d01c4f60 --- /dev/null +++ b/nixos/tests/flood.nix @@ -0,0 +1,44 @@ +import ./make-test-python.nix ({ pkgs, lib, ... }: +let + address = "127.0.0.127"; + port = 3030; + url = "${address}:${toString port}"; +in +{ + name = "flood"; + meta = with lib.maintainers; { + maintainers = [ _3JlOy-PYCCKUi ]; + }; + + nodes.machine = { pkgs, ... }: { + services.flood = { + enable = true; + inherit address port; + auth.rtorrent = { + host = "127.0.0.1"; + port = 9999; + }; + }; + environment.systemPackages = [ pkgs.jq ]; + }; + + testScript = '' + start_all() + machine.wait_for_unit("flood.service") + machine.wait_for_open_port(${toString port}, "${address}") + + machine.succeed("ss -tlpn 'src = ${address}' | grep LISTEN | grep node") + + machine.succeed("curl -sf '${url}' | grep Flood") + + # https://github.com/jesec/flood/blob/5a0d2bec844fe5f2163588b308158179d23b6d87/server/routes/api/auth.ts#L212 + machine.succeed("curl -sf -c /tmp/cookies 'http://${url}/api/auth/verify' | jq -e .username >&2") + + # https://github.com/jesec/flood/blob/5a0d2bec844fe5f2163588b308158179d23b6d87/server/routes/api/client.ts#L19 + status = machine.fail("curl -s --fail-with-body -b /tmp/cookies 'http://${url}/api/client/connection-test'") + + machine.succeed(f"echo '{status}' | jq .isConnected | grep -P '^false$'") + + machine.shutdown() + ''; +}) diff --git a/pkgs/applications/networking/p2p/flood/default.nix b/pkgs/applications/networking/p2p/flood/default.nix index 8ee94f17e50cd..4e74a03329513 100644 --- a/pkgs/applications/networking/p2p/flood/default.nix +++ b/pkgs/applications/networking/p2p/flood/default.nix @@ -1,6 +1,7 @@ { lib , buildNpmPackage , fetchFromGitHub +, nixosTests }: buildNpmPackage rec { @@ -16,6 +17,8 @@ buildNpmPackage rec { npmDepsHash = "sha256-XmDnvq+ni5TOf3UQFc4JvGI3LiGpjbrLAocRvrW8qgk="; + passthru.tests.flood = nixosTests.flood; + meta = with lib; { description = "Modern web UI for various torrent clients with a Node.js backend and React frontend"; homepage = "https://flood.js.org";