1{ config, lib, pkgs, ... }:
2
3with lib;
4let
5 cfg = config.services.freshrss;
6
7 poolName = "freshrss";
8in
9{
10 meta.maintainers = with maintainers; [ etu stunkymonkey ];
11
12 options.services.freshrss = {
13 enable = mkEnableOption (mdDoc "FreshRSS feed reader");
14
15 package = mkOption {
16 type = types.package;
17 default = pkgs.freshrss;
18 defaultText = lib.literalExpression "pkgs.freshrss";
19 description = mdDoc "Which FreshRSS package to use.";
20 };
21
22 defaultUser = mkOption {
23 type = types.str;
24 default = "admin";
25 description = mdDoc "Default username for FreshRSS.";
26 example = "eva";
27 };
28
29 passwordFile = mkOption {
30 type = types.path;
31 description = mdDoc "Password for the defaultUser for FreshRSS.";
32 example = "/run/secrets/freshrss";
33 };
34
35 baseUrl = mkOption {
36 type = types.str;
37 description = mdDoc "Default URL for FreshRSS.";
38 example = "https://freshrss.example.com";
39 };
40
41 language = mkOption {
42 type = types.str;
43 default = "en";
44 description = mdDoc "Default language for FreshRSS.";
45 example = "de";
46 };
47
48 database = {
49 type = mkOption {
50 type = types.enum [ "sqlite" "pgsql" "mysql" ];
51 default = "sqlite";
52 description = mdDoc "Database type.";
53 example = "pgsql";
54 };
55
56 host = mkOption {
57 type = types.nullOr types.str;
58 default = "localhost";
59 description = mdDoc "Database host for FreshRSS.";
60 };
61
62 port = mkOption {
63 type = with types; nullOr port;
64 default = null;
65 description = mdDoc "Database port for FreshRSS.";
66 example = 3306;
67 };
68
69 user = mkOption {
70 type = types.nullOr types.str;
71 default = "freshrss";
72 description = mdDoc "Database user for FreshRSS.";
73 };
74
75 passFile = mkOption {
76 type = types.nullOr types.str;
77 default = null;
78 description = mdDoc "Database password file for FreshRSS.";
79 example = "/run/secrets/freshrss";
80 };
81
82 name = mkOption {
83 type = types.nullOr types.str;
84 default = "freshrss";
85 description = mdDoc "Database name for FreshRSS.";
86 };
87
88 tableprefix = mkOption {
89 type = types.nullOr types.str;
90 default = null;
91 description = mdDoc "Database table prefix for FreshRSS.";
92 example = "freshrss";
93 };
94 };
95
96 dataDir = mkOption {
97 type = types.str;
98 default = "/var/lib/freshrss";
99 description = mdDoc "Default data folder for FreshRSS.";
100 example = "/mnt/freshrss";
101 };
102
103 virtualHost = mkOption {
104 type = types.nullOr types.str;
105 default = "freshrss";
106 description = mdDoc ''
107 Name of the nginx virtualhost to use and setup. If null, do not setup any virtualhost.
108 '';
109 };
110
111 pool = mkOption {
112 type = types.str;
113 default = poolName;
114 description = mdDoc ''
115 Name of the phpfpm pool to use and setup. If not specified, a pool will be created
116 with default values.
117 '';
118 };
119 };
120
121
122 config =
123 let
124 systemd-hardening = {
125 CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
126 DeviceAllow = "";
127 LockPersonality = true;
128 NoNewPrivileges = true;
129 PrivateDevices = true;
130 PrivateTmp = true;
131 PrivateUsers = true;
132 ProcSubset = "pid";
133 ProtectClock = true;
134 ProtectControlGroups = true;
135 ProtectHome = true;
136 ProtectHostname = true;
137 ProtectKernelLogs = true;
138 ProtectKernelModules = true;
139 ProtectKernelTunables = true;
140 ProtectProc = "invisible";
141 ProtectSystem = "strict";
142 RemoveIPC = true;
143 RestrictNamespaces = true;
144 RestrictRealtime = true;
145 RestrictSUIDSGID = true;
146 SystemCallArchitectures = "native";
147 SystemCallFilter = [ "@system-service" "~@resources" "~@privileged" ];
148 UMask = "0007";
149 };
150 in
151 mkIf cfg.enable {
152 # Set up a Nginx virtual host.
153 services.nginx = mkIf (cfg.virtualHost != null) {
154 enable = true;
155 virtualHosts.${cfg.virtualHost} = {
156 root = "${cfg.package}/p";
157
158 # php files handling
159 # this regex is mandatory because of the API
160 locations."~ ^.+?\.php(/.*)?$".extraConfig = ''
161 fastcgi_pass unix:${config.services.phpfpm.pools.${cfg.pool}.socket};
162 fastcgi_split_path_info ^(.+\.php)(/.*)$;
163 # By default, the variable PATH_INFO is not set under PHP-FPM
164 # But FreshRSS API greader.php need it. If you have a “Bad Request” error, double check this var!
165 # NOTE: the separate $path_info variable is required. For more details, see:
166 # https://trac.nginx.org/nginx/ticket/321
167 set $path_info $fastcgi_path_info;
168 fastcgi_param PATH_INFO $path_info;
169 include ${pkgs.nginx}/conf/fastcgi_params;
170 include ${pkgs.nginx}/conf/fastcgi.conf;
171 '';
172
173 locations."/" = {
174 tryFiles = "$uri $uri/ index.php";
175 index = "index.php index.html index.htm";
176 };
177 };
178 };
179
180 # Set up phpfpm pool
181 services.phpfpm.pools = mkIf (cfg.pool == poolName) {
182 ${poolName} = {
183 user = "freshrss";
184 settings = {
185 "listen.owner" = "nginx";
186 "listen.group" = "nginx";
187 "listen.mode" = "0600";
188 "pm" = "dynamic";
189 "pm.max_children" = 32;
190 "pm.max_requests" = 500;
191 "pm.start_servers" = 2;
192 "pm.min_spare_servers" = 2;
193 "pm.max_spare_servers" = 5;
194 "catch_workers_output" = true;
195 };
196 phpEnv = {
197 FRESHRSS_DATA_PATH = "${cfg.dataDir}";
198 };
199 };
200 };
201
202 users.users.freshrss = {
203 description = "FreshRSS service user";
204 isSystemUser = true;
205 group = "freshrss";
206 };
207 users.groups.freshrss = { };
208
209 systemd.services.freshrss-config =
210 let
211 settingsFlags = concatStringsSep " \\\n "
212 (mapAttrsToList (k: v: "${k} ${toString v}") {
213 "--default_user" = ''"${cfg.defaultUser}"'';
214 "--auth_type" = ''"form"'';
215 "--base_url" = ''"${cfg.baseUrl}"'';
216 "--language" = ''"${cfg.language}"'';
217 "--db-type" = ''"${cfg.database.type}"'';
218 # The following attributes are optional depending on the type of
219 # database. Those that evaluate to null on the left hand side
220 # will be omitted.
221 ${if cfg.database.name != null then "--db-base" else null} = ''"${cfg.database.name}"'';
222 ${if cfg.database.passFile != null then "--db-password" else null} = ''"$(cat ${cfg.database.passFile})"'';
223 ${if cfg.database.user != null then "--db-user" else null} = ''"${cfg.database.user}"'';
224 ${if cfg.database.tableprefix != null then "--db-prefix" else null} = ''"${cfg.database.tableprefix}"'';
225 ${if cfg.database.host != null && cfg.database.port != null then "--db-host" else null} = ''"${cfg.database.host}:${toString cfg.database.port}"'';
226 });
227 in
228 {
229 description = "Set up the state directory for FreshRSS before use";
230 wantedBy = [ "multi-user.target" ];
231 serviceConfig = {
232 Type = "oneshot";
233 User = "freshrss";
234 Group = "freshrss";
235 StateDirectory = "freshrss";
236 WorkingDirectory = cfg.package;
237 } // systemd-hardening;
238 environment = {
239 FRESHRSS_DATA_PATH = cfg.dataDir;
240 };
241
242 script = ''
243 # create files with correct permissions
244 mkdir -m 755 -p ${cfg.dataDir}
245
246 # do installation or reconfigure
247 if test -f ${cfg.dataDir}/config.php; then
248 # reconfigure with settings
249 ./cli/reconfigure.php ${settingsFlags}
250 ./cli/update-user.php --user ${cfg.defaultUser} --password "$(cat ${cfg.passwordFile})"
251 else
252 # Copy the user data template directory
253 cp -r ./data ${cfg.dataDir}
254
255 # check correct folders in data folder
256 ./cli/prepare.php
257 # install with settings
258 ./cli/do-install.php ${settingsFlags}
259 ./cli/create-user.php --user ${cfg.defaultUser} --password "$(cat ${cfg.passwordFile})"
260 fi
261 '';
262 };
263
264 systemd.services.freshrss-updater = {
265 description = "FreshRSS feed updater";
266 after = [ "freshrss-config.service" ];
267 wantedBy = [ "multi-user.target" ];
268 startAt = "*:0/5";
269 environment = {
270 FRESHRSS_DATA_PATH = cfg.dataDir;
271 };
272 serviceConfig = {
273 Type = "oneshot";
274 User = "freshrss";
275 Group = "freshrss";
276 StateDirectory = "freshrss";
277 WorkingDirectory = cfg.package;
278 ExecStart = "${cfg.package}/app/actualize_script.php";
279 } // systemd-hardening;
280 };
281 };
282}