From b2ea523523fa2057918a145723d5e2557af1827c Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 24 Dec 2023 22:58:18 +0100 Subject: [PATCH] WIP: WAITING FOR NIXPKS UPDATE --- modules/nixos/services/pyload/nixos.nix | 144 ++++++++++++++++++ .../declarative-config.patch | 18 +++ .../declarative-default-user.patch | 15 ++ .../default.nix | 4 + .../package.nix | 60 ++++++++ 5 files changed, 241 insertions(+) create mode 100644 modules/nixos/services/pyload/nixos.nix create mode 100644 overlays/pyload-declarative-user-management/declarative-config.patch create mode 100644 overlays/pyload-declarative-user-management/declarative-default-user.patch create mode 100644 overlays/pyload-declarative-user-management/default.nix create mode 100644 overlays/pyload-declarative-user-management/package.nix diff --git a/modules/nixos/services/pyload/nixos.nix b/modules/nixos/services/pyload/nixos.nix new file mode 100644 index 0000000..5ce7d7c --- /dev/null +++ b/modules/nixos/services/pyload/nixos.nix @@ -0,0 +1,144 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.services.pyload; + + stateDir = "/var/lib/pyload"; + userDir = "${stateDir}/config"; +in +{ + options = with lib; { + services.pyload = { + enable = mkEnableOption "pyload download manager"; + + package = mkPackageOption pkgs "pyload-ng" { }; + + listenAddress = mkOption { + type = types.str; + default = "localhost"; + example = "0.0.0.0"; + description = "Address to listen on for the web UI."; + }; + + port = mkOption { + type = types.port; + default = 8000; + example = 9876; + description = "Port to listen on for the web UI."; + }; + + downloadDirectory = mkOption { + type = types.path; + default = "${stateDir}/downloads"; + example = "/mnt/downloads"; + description = "Directory to store downloads"; + }; + + credentialsFile = mkOption { + type = with types; nullOr path; + default = null; + example = "/run/secrets/pyload-credentials.env"; + description = '' + File containing PYLOAD_DEFAULT_USERNAME and PYLOAD_DEFAULT_PASSWORD + in the format of an EnvironmentFile=, as described by systemd.exec(5). + + If not given, they default to the username/password combo of + pyload/pyload. + ''; + }; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.tmpfiles.rules = [ + "d ${cfg.downloadDirectory}" + ]; + + systemd.services.pyload = { + description = "pyload service"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + + # NOTE: unlike what the documentation says, it looks like `HOME` is not + # defined with this service definition... + # Since pyload tries to do the equivalent of `cd ~`, it needs to be able + # to resolve $HOME, which fails when `RootDirectory` is set. + # FIXME: check if `SetLoginEnvironment` fixes this issue in version 255 + environment = { + HOME = stateDir; + PYLOAD__WEBUI__HOST = cfg.listenAddress; + PYLOAD__WEBUI__PORT = builtins.toString cfg.port; + }; + + serviceConfig = { + # FIXME: use getExe + ExecStart = "${lib.getExe' cfg.package "pyload"} ${lib.escapeShellArgs [ + "--userdir" userDir + "--storagedir" cfg.downloadDirectory + ]}"; + + User = "pyload"; + Group = "pyload"; + DynamicUser = true; + + EnvironmentFile = lib.optional (cfg.credentialsFile != null) cfg.credentialsFile; + + StateDirectory = "pyload"; + WorkingDirectory = stateDir; + RuntimeDirectory = "pyload"; + RuntimeDirectoryMode = "0700"; + RootDirectory = "/run/pyload"; + BindReadOnlyPaths = [ + builtins.storeDir # Needed to run the python interpreter + ]; + BindPaths = [ + cfg.downloadDirectory + ]; + + # Hardening options + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateMounts = true; + PrivateTmp = true; + PrivateUsers = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + RemoveIPC = true; + RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX"; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ "@system-service" "~@resources" "~@privileged" ]; + UMask = "0002"; + CapabilityBoundingSet = [ + "~CAP_BLOCK_SUSPEND" + "~CAP_BPF" + "~CAP_CHOWN" + "~CAP_IPC_LOCK" + "~CAP_KILL" + "~CAP_LEASE" + "~CAP_LINUX_IMMUTABLE" + "~CAP_NET_ADMIN" + "~CAP_SYS_ADMIN" + "~CAP_SYS_BOOT" + "~CAP_SYS_CHROOT" + "~CAP_SYS_NICE" + "~CAP_SYS_PACCT" + "~CAP_SYS_PTRACE" + "~CAP_SYS_RESOURCE" + "~CAP_SYS_TTY_CONFIG" + ]; + }; + }; + }; +} diff --git a/overlays/pyload-declarative-user-management/declarative-config.patch b/overlays/pyload-declarative-user-management/declarative-config.patch new file mode 100644 index 0000000..42f89ee --- /dev/null +++ b/overlays/pyload-declarative-user-management/declarative-config.patch @@ -0,0 +1,18 @@ +diff --git a/src/pyload/core/__init__.py b/src/pyload/core/__init__.py +index 4324fc700..5d915a85e 100644 +--- a/src/pyload/core/__init__.py ++++ b/src/pyload/core/__init__.py +@@ -128,6 +128,13 @@ class Core: + else: + self._debug = max(0, int(debug)) + ++ # Allow setting any option declaratively, for the NixOS module ++ for env, value in os.environ.items(): ++ if not env.startswith("PYLOAD__"): ++ continue ++ section, opt = env.removeprefix("PYLOAD__").lower().split("__") ++ self.config.set(section, opt, value) ++ + # If no argument set, read storage dir from config file, + # otherwise save setting to config dir + if storagedir is None: diff --git a/overlays/pyload-declarative-user-management/declarative-default-user.patch b/overlays/pyload-declarative-user-management/declarative-default-user.patch new file mode 100644 index 0000000..3c3e6f4 --- /dev/null +++ b/overlays/pyload-declarative-user-management/declarative-default-user.patch @@ -0,0 +1,15 @@ +diff --git a/src/pyload/core/__init__.py b/src/pyload/core/__init__.py +index 4324fc700..f7fcd66ec 100644 +--- a/src/pyload/core/__init__.py ++++ b/src/pyload/core/__init__.py +@@ -46,8 +46,8 @@ class Exit(Exception): + # improve external scripts + class Core: + LOCALE_DOMAIN = APPID +- DEFAULT_USERNAME = APPID +- DEFAULT_PASSWORD = APPID ++ DEFAULT_USERNAME = os.getenv("PYLOAD_DEFAULT_USERNAME", APPID) ++ DEFAULT_PASSWORD = os.getenv("PYLOAD_DEFAULT_PASSWORD", APPID) + DEFAULT_DATADIR = os.path.join( + os.getenv("APPDATA") or USERHOMEDIR, "pyLoad" if os.name == "nt" else ".pyload" + ) diff --git a/overlays/pyload-declarative-user-management/default.nix b/overlays/pyload-declarative-user-management/default.nix new file mode 100644 index 0000000..475f126 --- /dev/null +++ b/overlays/pyload-declarative-user-management/default.nix @@ -0,0 +1,4 @@ +self: _super: +{ + pyload-ng = self.callPackage ./package.nix { }; +} diff --git a/overlays/pyload-declarative-user-management/package.nix b/overlays/pyload-declarative-user-management/package.nix new file mode 100644 index 0000000..4daa125 --- /dev/null +++ b/overlays/pyload-declarative-user-management/package.nix @@ -0,0 +1,60 @@ +{ lib, fetchPypi, python3 }: + +python3.pkgs.buildPythonApplication rec { + version = "0.5.0b3.dev75"; + pname = "pyload-ng"; + pyproject = true; + + src = fetchPypi { + inherit pname version; + hash = "sha256-1lPIKkZESonDaVCnac0iUu/gCqXVDBhNZrk5S0eC6F0="; + }; + + patches = [ + # Makes it possible to change the default username/password in the module + ./declarative-default-user.patch + ./declarative-config.patch + ]; + + postPatch = '' + # relax version bounds + sed -i 's/\([A-z0-9]*\)~=.*$/\1/' setup.cfg + # not sure what Flask-Session2 is but flask-session works just fine + sed -i '/Flask-Session2/d' setup.cfg + ''; + + propagatedBuildInputs = with python3.pkgs; [ + bitmath + certifi + cheroot + cryptography + filetype + flask + flask-babel + flask-caching + flask-compress + flask-session + flask-themes2 + js2py + pycurl + semver + setuptools + ]; + + passthru.optional-dependencies = { + plugins = with python3.pkgs; [ + beautifulsoup4 # for some plugins + colorlog # colorful console logging + pillow # for some CAPTCHA plugin + send2trash # send some files to trash instead of deleting them + slixmpp # XMPP plugin + ]; + }; + + meta = with lib; { + description = "Free and open-source download manager with support for 1-click-hosting sites"; + homepage = "https://github.com/pyload/pyload"; + license = licenses.agpl3Plus; + maintainers = with maintainers; [ ruby0b ]; + }; +}