1{
2 config,
3 lib,
4 pkgs,
5 utils,
6 ...
7}:
8let
9 cfg = config.services.pyload;
10
11 stateDir = "/var/lib/pyload";
12in
13{
14 meta.maintainers = with lib.maintainers; [ ambroisie ];
15
16 options = with lib; {
17 services.pyload = {
18 enable = mkEnableOption "pyLoad download manager";
19
20 package = mkPackageOption pkgs "pyLoad" { default = [ "pyload-ng" ]; };
21
22 listenAddress = mkOption {
23 type = types.str;
24 default = "localhost";
25 example = "0.0.0.0";
26 description = "Address to listen on for the web UI.";
27 };
28
29 port = mkOption {
30 type = types.port;
31 default = 8000;
32 example = 9876;
33 description = "Port to listen on for the web UI.";
34 };
35
36 downloadDirectory = mkOption {
37 type = types.path;
38 default = "${stateDir}/downloads";
39 example = "/mnt/downloads";
40 description = "Directory to store downloads.";
41 };
42
43 user = mkOption {
44 type = types.str;
45 default = "pyload";
46 description = "User under which pyLoad runs, and which owns the download directory.";
47 };
48
49 group = mkOption {
50 type = types.str;
51 default = "pyload";
52 description = "Group under which pyLoad runs, and which owns the download directory.";
53 };
54
55 credentialsFile = mkOption {
56 type = with types; nullOr path;
57 default = null;
58 example = "/run/secrets/pyload-credentials.env";
59 description = ''
60 File containing {env}`PYLOAD_DEFAULT_USERNAME` and
61 {env}`PYLOAD_DEFAULT_PASSWORD` in the format of an `EnvironmentFile=`,
62 as described by {manpage}`systemd.exec(5)`.
63
64 If not given, they default to the username/password combo of
65 pyload/pyload.
66 '';
67 };
68 };
69 };
70
71 config = lib.mkIf cfg.enable {
72 systemd.tmpfiles.settings.pyload = {
73 ${cfg.downloadDirectory}.d = { inherit (cfg) user group; };
74 };
75
76 systemd.services.pyload = {
77 description = "pyLoad download manager";
78 wantedBy = [ "multi-user.target" ];
79 after = [ "network.target" ];
80
81 # NOTE: unlike what the documentation says, it looks like `HOME` is not
82 # defined with this service definition...
83 # Since pyload tries to do the equivalent of `cd ~`, it needs to be able
84 # to resolve $HOME, which fails when `RootDirectory` is set.
85 # FIXME: check if `SetLoginEnvironment` fixes this issue in version 255
86 environment = {
87 HOME = stateDir;
88 PYLOAD__WEBUI__HOST = cfg.listenAddress;
89 PYLOAD__WEBUI__PORT = builtins.toString cfg.port;
90 };
91
92 serviceConfig = {
93 ExecStart = utils.escapeSystemdExecArgs [
94 (lib.getExe cfg.package)
95 "--userdir"
96 "${stateDir}/config"
97 "--storagedir"
98 cfg.downloadDirectory
99 ];
100
101 User = cfg.user;
102 Group = cfg.group;
103
104 EnvironmentFile = lib.optional (cfg.credentialsFile != null) cfg.credentialsFile;
105
106 StateDirectory = "pyload";
107 WorkingDirectory = stateDir;
108 RuntimeDirectory = "pyload";
109 RuntimeDirectoryMode = "0700";
110 RootDirectory = "/run/pyload";
111 BindReadOnlyPaths = [
112 builtins.storeDir # Needed to run the python interpreter
113 ];
114 BindPaths = [
115 cfg.downloadDirectory
116 ];
117
118 # Hardening options
119 LockPersonality = true;
120 NoNewPrivileges = true;
121 PrivateDevices = true;
122 PrivateMounts = true;
123 PrivateTmp = true;
124 PrivateUsers = true;
125 ProcSubset = "pid";
126 ProtectClock = true;
127 ProtectControlGroups = true;
128 ProtectHome = true;
129 ProtectHostname = true;
130 ProtectKernelLogs = true;
131 ProtectKernelModules = true;
132 ProtectKernelTunables = true;
133 ProtectProc = "invisible";
134 ProtectSystem = "strict";
135 RemoveIPC = true;
136 RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX";
137 RestrictNamespaces = true;
138 RestrictRealtime = true;
139 RestrictSUIDSGID = true;
140 SystemCallArchitectures = "native";
141 SystemCallFilter = [
142 "@system-service"
143 "~@resources"
144 "~@privileged"
145 ];
146 UMask = "0002";
147 CapabilityBoundingSet = [
148 "~CAP_BLOCK_SUSPEND"
149 "~CAP_BPF"
150 "~CAP_CHOWN"
151 "~CAP_IPC_LOCK"
152 "~CAP_KILL"
153 "~CAP_LEASE"
154 "~CAP_LINUX_IMMUTABLE"
155 "~CAP_NET_ADMIN"
156 "~CAP_SYS_ADMIN"
157 "~CAP_SYS_BOOT"
158 "~CAP_SYS_CHROOT"
159 "~CAP_SYS_NICE"
160 "~CAP_SYS_PACCT"
161 "~CAP_SYS_PTRACE"
162 "~CAP_SYS_RESOURCE"
163 "~CAP_SYS_TTY_CONFIG"
164 ];
165 };
166 };
167
168 users.users.pyload = lib.mkIf (cfg.user == "pyload") {
169 isSystemUser = true;
170 group = cfg.group;
171 home = stateDir;
172 };
173
174 users.groups.pyload = lib.mkIf (cfg.group == "pyload") { };
175 };
176}