1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8with lib;
9let
10 cfg = config.services.freshrss;
11 webserver = config.services.${cfg.webserver};
12
13 extension-env = pkgs.buildEnv {
14 name = "freshrss-extensions";
15 paths = cfg.extensions;
16 };
17 env-vars =
18 {
19 DATA_PATH = cfg.dataDir;
20 }
21 // lib.optionalAttrs (cfg.extensions != [ ]) {
22 THIRDPARTY_EXTENSIONS_PATH = "${extension-env}/share/freshrss/";
23 };
24in
25{
26 meta.maintainers = with maintainers; [
27 etu
28 stunkymonkey
29 mattchrist
30 ];
31
32 options.services.freshrss = {
33 enable = mkEnableOption "FreshRSS RSS aggregator and reader with php-fpm backend";
34
35 package = mkPackageOption pkgs "freshrss" { };
36
37 extensions = mkOption {
38 type = types.listOf types.package;
39 default = [ ];
40 defaultText = literalExpression "[]";
41 example = literalExpression ''
42 with freshrss-extensions; [
43 youtube
44 ] ++ [
45 (freshrss-extensions.buildFreshRssExtension {
46 FreshRssExtUniqueId = "ReadingTime";
47 pname = "reading-time";
48 version = "1.5";
49 src = pkgs.fetchFromGitLab {
50 domain = "framagit.org";
51 owner = "Lapineige";
52 repo = "FreshRSS_Extension-ReadingTime";
53 rev = "fb6e9e944ef6c5299fa56ffddbe04c41e5a34ebf";
54 hash = "sha256-C5cRfaphx4Qz2xg2z+v5qRji8WVSIpvzMbethTdSqsk=";
55 };
56 })
57 ]
58 '';
59 description = "Additional extensions to be used.";
60 };
61
62 defaultUser = mkOption {
63 type = types.str;
64 default = "admin";
65 description = "Default username for FreshRSS.";
66 example = "eva";
67 };
68
69 passwordFile = mkOption {
70 type = types.nullOr types.path;
71 default = null;
72 description = "Password for the defaultUser for FreshRSS.";
73 example = "/run/secrets/freshrss";
74 };
75
76 baseUrl = mkOption {
77 type = types.str;
78 description = "Default URL for FreshRSS.";
79 example = "https://freshrss.example.com";
80 };
81
82 language = mkOption {
83 type = types.str;
84 default = "en";
85 description = "Default language for FreshRSS.";
86 example = "de";
87 };
88
89 database = {
90 type = mkOption {
91 type = types.enum [
92 "sqlite"
93 "pgsql"
94 "mysql"
95 ];
96 default = "sqlite";
97 description = "Database type.";
98 example = "pgsql";
99 };
100
101 host = mkOption {
102 type = types.nullOr types.str;
103 default = "localhost";
104 description = "Database host for FreshRSS.";
105 };
106
107 port = mkOption {
108 type = types.nullOr types.port;
109 default = null;
110 description = "Database port for FreshRSS.";
111 example = 3306;
112 };
113
114 user = mkOption {
115 type = types.nullOr types.str;
116 default = "freshrss";
117 description = "Database user for FreshRSS.";
118 };
119
120 passFile = mkOption {
121 type = types.nullOr types.path;
122 default = null;
123 description = "Database password file for FreshRSS.";
124 example = "/run/secrets/freshrss";
125 };
126
127 name = mkOption {
128 type = types.nullOr types.str;
129 default = "freshrss";
130 description = "Database name for FreshRSS.";
131 };
132
133 tableprefix = mkOption {
134 type = types.nullOr types.str;
135 default = null;
136 description = "Database table prefix for FreshRSS.";
137 example = "freshrss";
138 };
139 };
140
141 dataDir = mkOption {
142 type = types.str;
143 default = "/var/lib/freshrss";
144 description = "Default data folder for FreshRSS.";
145 example = "/mnt/freshrss";
146 };
147
148 webserver = mkOption {
149 type = types.enum [
150 "nginx"
151 "caddy"
152 ];
153 default = "nginx";
154 description = ''
155 Whether to use nginx or caddy for virtual host management.
156
157 Further nginx configuration can be done by adapting `services.nginx.virtualHosts.<name>`.
158 See [](#opt-services.nginx.virtualHosts) for further information.
159
160 Further caddy configuration can be done by adapting `services.caddy.virtualHosts.<name>`.
161 See [](#opt-services.caddy.virtualHosts) for further information.
162 '';
163 };
164
165 virtualHost = mkOption {
166 type = types.str;
167 default = "freshrss";
168 description = ''
169 Name of the caddy/nginx virtualhost to use and setup.
170 '';
171 };
172
173 pool = mkOption {
174 type = types.nullOr types.str;
175 default = "freshrss";
176 description = ''
177 Name of the php-fpm pool to use and setup. If not specified, a pool will be created
178 with default values.
179 '';
180 };
181
182 user = mkOption {
183 type = types.str;
184 default = "freshrss";
185 description = "User under which FreshRSS runs.";
186 };
187
188 authType = mkOption {
189 type = types.enum [
190 "form"
191 "http_auth"
192 "none"
193 ];
194 default = "form";
195 description = "Authentication type for FreshRSS.";
196 };
197 };
198
199 config =
200 let
201 defaultServiceConfig = {
202 ReadWritePaths = "${cfg.dataDir}";
203 DeviceAllow = "";
204 LockPersonality = true;
205 NoNewPrivileges = true;
206 PrivateDevices = true;
207 PrivateTmp = true;
208 PrivateUsers = true;
209 ProcSubset = "pid";
210 ProtectClock = true;
211 ProtectControlGroups = true;
212 ProtectHome = true;
213 ProtectHostname = true;
214 ProtectKernelLogs = true;
215 ProtectKernelModules = true;
216 ProtectKernelTunables = true;
217 ProtectProc = "invisible";
218 ProtectSystem = "strict";
219 RemoveIPC = true;
220 RestrictNamespaces = true;
221 RestrictRealtime = true;
222 RestrictSUIDSGID = true;
223 SystemCallArchitectures = "native";
224 SystemCallFilter = [
225 "@system-service"
226 "~@resources"
227 "~@privileged"
228 ];
229 UMask = "0007";
230 Type = "oneshot";
231 User = cfg.user;
232 Group = config.users.users.${cfg.user}.group;
233 StateDirectory = "freshrss";
234 WorkingDirectory = cfg.package;
235 };
236 in
237 mkIf cfg.enable {
238 assertions = mkIf (cfg.authType == "form") [
239 {
240 assertion = cfg.passwordFile != null;
241 message = ''
242 `passwordFile` must be supplied when using "form" authentication!
243 '';
244 }
245 ];
246
247 # Set up a Caddy virtual host.
248 services.caddy = mkIf (cfg.webserver == "caddy") {
249 enable = true;
250 virtualHosts.${cfg.virtualHost}.extraConfig = ''
251 root * ${config.services.freshrss.package}/p
252 php_fastcgi unix/${config.services.phpfpm.pools.freshrss.socket} {
253 env FRESHRSS_DATA_PATH ${config.services.freshrss.dataDir}
254 }
255 file_server
256 '';
257 };
258
259 # Set up a Nginx virtual host.
260 services.nginx = mkIf (cfg.webserver == "nginx") {
261 enable = true;
262 virtualHosts.${cfg.virtualHost} = {
263 root = "${cfg.package}/p";
264
265 # php files handling
266 # this regex is mandatory because of the API
267 locations."~ ^.+?\\.php(/.*)?$".extraConfig = ''
268 fastcgi_pass unix:${config.services.phpfpm.pools.${cfg.pool}.socket};
269 fastcgi_split_path_info ^(.+\.php)(/.*)$;
270 # By default, the variable PATH_INFO is not set under PHP-FPM
271 # But FreshRSS API greader.php need it. If you have a “Bad Request” error, double check this var!
272 # NOTE: the separate $path_info variable is required. For more details, see:
273 # https://trac.nginx.org/nginx/ticket/321
274 set $path_info $fastcgi_path_info;
275 fastcgi_param PATH_INFO $path_info;
276 include ${pkgs.nginx}/conf/fastcgi_params;
277 include ${pkgs.nginx}/conf/fastcgi.conf;
278 '';
279
280 locations."/" = {
281 tryFiles = "$uri $uri/ index.php";
282 index = "index.php index.html index.htm";
283 };
284 };
285 };
286
287 # Set up phpfpm pool
288 services.phpfpm.pools = mkIf (cfg.pool != null) {
289 ${cfg.pool} = {
290 user = "freshrss";
291 settings = {
292 "listen.owner" = webserver.user;
293 "listen.group" = webserver.group;
294 "listen.mode" = "0600";
295 "pm" = "dynamic";
296 "pm.max_children" = 32;
297 "pm.max_requests" = 500;
298 "pm.start_servers" = 2;
299 "pm.min_spare_servers" = 2;
300 "pm.max_spare_servers" = 5;
301 "catch_workers_output" = true;
302 };
303 phpEnv = env-vars;
304 };
305 };
306
307 users.users."${cfg.user}" = {
308 description = "FreshRSS service user";
309 isSystemUser = true;
310 group = "${cfg.user}";
311 home = cfg.dataDir;
312 };
313 users.groups."${cfg.user}" = { };
314
315 systemd.tmpfiles.settings."10-freshrss".${cfg.dataDir}.d = {
316 inherit (cfg) user;
317 group = config.users.users.${cfg.user}.group;
318 };
319
320 systemd.services.freshrss-config =
321 let
322 settingsFlags = concatStringsSep " \\\n " (
323 mapAttrsToList (k: v: "${k} ${toString v}") {
324 "--default-user" = ''"${cfg.defaultUser}"'';
325 "--auth-type" = ''"${cfg.authType}"'';
326 "--base-url" = ''"${cfg.baseUrl}"'';
327 "--language" = ''"${cfg.language}"'';
328 "--db-type" = ''"${cfg.database.type}"'';
329 # The following attributes are optional depending on the type of
330 # database. Those that evaluate to null on the left hand side
331 # will be omitted.
332 ${if cfg.database.name != null then "--db-base" else null} = ''"${cfg.database.name}"'';
333 ${if cfg.database.passFile != null then "--db-password" else null} =
334 ''"$(cat ${cfg.database.passFile})"'';
335 ${if cfg.database.user != null then "--db-user" else null} = ''"${cfg.database.user}"'';
336 ${if cfg.database.tableprefix != null then "--db-prefix" else null} =
337 ''"${cfg.database.tableprefix}"'';
338 # hostname:port e.g. "localhost:5432"
339 ${if cfg.database.host != null && cfg.database.port != null then "--db-host" else null} =
340 ''"${cfg.database.host}:${toString cfg.database.port}"'';
341 # socket path e.g. "/run/postgresql"
342 ${if cfg.database.host != null && cfg.database.port == null then "--db-host" else null} =
343 ''"${cfg.database.host}"'';
344 }
345 );
346 in
347 {
348 description = "Set up the state directory for FreshRSS before use";
349 wantedBy = [ "multi-user.target" ];
350 serviceConfig = defaultServiceConfig // {
351 RemainAfterExit = true;
352 };
353 restartIfChanged = true;
354 environment = env-vars;
355
356 script =
357 let
358 userScriptArgs = ''--user ${cfg.defaultUser} ${
359 optionalString (cfg.authType == "form") ''--password "$(cat ${cfg.passwordFile})"''
360 }'';
361 updateUserScript = optionalString (cfg.authType == "form" || cfg.authType == "none") ''
362 ./cli/update-user.php ${userScriptArgs}
363 '';
364 createUserScript = optionalString (cfg.authType == "form" || cfg.authType == "none") ''
365 ./cli/create-user.php ${userScriptArgs}
366 '';
367 in
368 ''
369 # do installation or reconfigure
370 if test -f ${cfg.dataDir}/config.php; then
371 # reconfigure with settings
372 ./cli/reconfigure.php ${settingsFlags}
373 ${updateUserScript}
374 else
375 # check correct folders in data folder
376 ./cli/prepare.php
377 # install with settings
378 ./cli/do-install.php ${settingsFlags}
379 ${createUserScript}
380 fi
381 '';
382 };
383
384 systemd.services.freshrss-updater = {
385 description = "FreshRSS feed updater";
386 after = [ "freshrss-config.service" ];
387 startAt = "*:0/5";
388 environment = env-vars;
389 serviceConfig = defaultServiceConfig // {
390 ExecStart = "${cfg.package}/app/actualize_script.php";
391 };
392 };
393 };
394}