From 128e77343ee36e7c6f8a740d6a2bb923586d947f Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 24 Dec 2023 22:56:50 +0100 Subject: [PATCH 1/4] nixos: services: add pyload --- modules/nixos/services/default.nix | 1 + modules/nixos/services/pyload/default.nix | 66 +++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 modules/nixos/services/pyload/default.nix diff --git a/modules/nixos/services/default.nix b/modules/nixos/services/default.nix index b27570d..cb06808 100644 --- a/modules/nixos/services/default.nix +++ b/modules/nixos/services/default.nix @@ -26,6 +26,7 @@ ./podgrab ./postgresql ./postgresql-backup + ./pyload ./quassel ./rss-bridge ./sabnzbd diff --git a/modules/nixos/services/pyload/default.nix b/modules/nixos/services/pyload/default.nix new file mode 100644 index 0000000..e6a978f --- /dev/null +++ b/modules/nixos/services/pyload/default.nix @@ -0,0 +1,66 @@ +{ config, lib, ... }: +let + cfg = config.my.services.pyload; +in +{ + imports = [ + ./nixos.nix + ]; + + options.my.services.pyload = with lib; { + enable = mkEnableOption "pyload download manager"; + + credentialsFile = mkOption { + type = types.path; + example = "/run/secrets/pyload-credentials.env"; + description = "pyload credentials"; + }; + + downloadDirectory = mkOption { + type = types.str; + default = "/data/downloads"; + example = "/var/lib/pyload/download"; + description = "Download directory"; + }; + + port = mkOption { + type = types.port; + default = 9093; + example = 8080; + description = "Internal port for webui"; + }; + }; + + config = lib.mkIf cfg.enable { + services.pyload = { + enable = true; + + # Listening on `localhost` leads to 502 with the reverse proxy... + listenAddress = "127.0.0.1"; + + inherit (cfg) + credentialsFile + downloadDirectory + port + ; + }; + + # User media group when downloading files + systemd. services. pyload = { + serviceConfig = { + Group = lib.mkForce "media"; + }; + }; + + # Set-up media group + users.groups.media = { }; + + my.services.nginx.virtualHosts = { + pyload = { + inherit (cfg) port; + }; + }; + + # FIXME: fail2ban + }; +} From c8afac3e8a611ffa09bd9ad62d0c97733a89a83a Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 24 Dec 2023 22:57:37 +0100 Subject: [PATCH 2/4] hosts: nixos: porthos: secrets: add pyload creds --- hosts/nixos/porthos/secrets/pyload/credentials.age | 7 +++++++ hosts/nixos/porthos/secrets/secrets.nix | 2 ++ 2 files changed, 9 insertions(+) create mode 100644 hosts/nixos/porthos/secrets/pyload/credentials.age diff --git a/hosts/nixos/porthos/secrets/pyload/credentials.age b/hosts/nixos/porthos/secrets/pyload/credentials.age new file mode 100644 index 0000000..089f962 --- /dev/null +++ b/hosts/nixos/porthos/secrets/pyload/credentials.age @@ -0,0 +1,7 @@ +age-encryption.org/v1 +-> ssh-ed25519 cKojmg nJbOfp0/wmFOZLzcWjoGB7wEB8e56aO1NntSmn5KomU +/Vio4Z/t7IPJrdzdwUPidVH3wrouSkwRzNHP0T4z3x0 +-> ssh-ed25519 jPowng QXg/xqs7/VfkYQg3X77w4i53q64bL9oYeTxqb9NVhiQ +sMHIXlmrIxtIr+s0X4lBqev/PPd3AKD5P7AP5K4NeJg +--- gzTn+6+aa4Ptic1lsvSt+r3IEBysHrvMMIyONogMDF0 +ˮUE_ Date: Sun, 24 Dec 2023 22:58:03 +0100 Subject: [PATCH 3/4] hosts: nixos: porthos: services: enable pyload --- hosts/nixos/porthos/services.nix | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hosts/nixos/porthos/services.nix b/hosts/nixos/porthos/services.nix index d73cdc1..2486752 100644 --- a/hosts/nixos/porthos/services.nix +++ b/hosts/nixos/porthos/services.nix @@ -134,6 +134,10 @@ in }; # Regular backups postgresql-backup.enable = true; + pyload = { + enable = true; + credentialsFile = secrets."pyload/credentials".path; + }; # RSS provider for websites that do not provide any feeds rss-bridge.enable = true; # Usenet client From b192a1d38d59750d30b4fec455e31910ce036daa Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 24 Dec 2023 22:58:18 +0100 Subject: [PATCH 4/4] 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 ]; + }; +}