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 = types.nullOr types.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.path;
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 user = mkOption {
121 type = types.str;
122 default = "freshrss";
123 description = lib.mdDoc "User under which Freshrss runs.";
124 };
125 };
126
127 config =
128 let
129 defaultServiceConfig = {
130 ReadWritePaths = "${cfg.dataDir}";
131 CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
132 DeviceAllow = "";
133 LockPersonality = true;
134 NoNewPrivileges = true;
135 PrivateDevices = true;
136 PrivateTmp = true;
137 PrivateUsers = true;
138 ProcSubset = "pid";
139 ProtectClock = true;
140 ProtectControlGroups = true;
141 ProtectHome = true;
142 ProtectHostname = true;
143 ProtectKernelLogs = true;
144 ProtectKernelModules = true;
145 ProtectKernelTunables = true;
146 ProtectProc = "invisible";
147 ProtectSystem = "strict";
148 RemoveIPC = true;
149 RestrictNamespaces = true;
150 RestrictRealtime = true;
151 RestrictSUIDSGID = true;
152 SystemCallArchitectures = "native";
153 SystemCallFilter = [ "@system-service" "~@resources" "~@privileged" ];
154 UMask = "0007";
155 Type = "oneshot";
156 User = cfg.user;
157 Group = config.users.users.${cfg.user}.group;
158 StateDirectory = "freshrss";
159 WorkingDirectory = cfg.package;
160 };
161 in
162 mkIf cfg.enable {
163 # Set up a Nginx virtual host.
164 services.nginx = mkIf (cfg.virtualHost != null) {
165 enable = true;
166 virtualHosts.${cfg.virtualHost} = {
167 root = "${cfg.package}/p";
168
169 # php files handling
170 # this regex is mandatory because of the API
171 locations."~ ^.+?\.php(/.*)?$".extraConfig = ''
172 fastcgi_pass unix:${config.services.phpfpm.pools.${cfg.pool}.socket};
173 fastcgi_split_path_info ^(.+\.php)(/.*)$;
174 # By default, the variable PATH_INFO is not set under PHP-FPM
175 # But FreshRSS API greader.php need it. If you have a “Bad Request” error, double check this var!
176 # NOTE: the separate $path_info variable is required. For more details, see:
177 # https://trac.nginx.org/nginx/ticket/321
178 set $path_info $fastcgi_path_info;
179 fastcgi_param PATH_INFO $path_info;
180 include ${pkgs.nginx}/conf/fastcgi_params;
181 include ${pkgs.nginx}/conf/fastcgi.conf;
182 '';
183
184 locations."/" = {
185 tryFiles = "$uri $uri/ index.php";
186 index = "index.php index.html index.htm";
187 };
188 };
189 };
190
191 # Set up phpfpm pool
192 services.phpfpm.pools = mkIf (cfg.pool == poolName) {
193 ${poolName} = {
194 user = "freshrss";
195 settings = {
196 "listen.owner" = "nginx";
197 "listen.group" = "nginx";
198 "listen.mode" = "0600";
199 "pm" = "dynamic";
200 "pm.max_children" = 32;
201 "pm.max_requests" = 500;
202 "pm.start_servers" = 2;
203 "pm.min_spare_servers" = 2;
204 "pm.max_spare_servers" = 5;
205 "catch_workers_output" = true;
206 };
207 phpEnv = {
208 FRESHRSS_DATA_PATH = "${cfg.dataDir}";
209 };
210 };
211 };
212
213 users.users."${cfg.user}" = {
214 description = "FreshRSS service user";
215 isSystemUser = true;
216 group = "${cfg.user}";
217 home = cfg.dataDir;
218 };
219 users.groups."${cfg.user}" = { };
220
221 systemd.tmpfiles.rules = [
222 "d '${cfg.dataDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
223 ];
224
225 systemd.services.freshrss-config =
226 let
227 settingsFlags = concatStringsSep " \\\n "
228 (mapAttrsToList (k: v: "${k} ${toString v}") {
229 "--default_user" = ''"${cfg.defaultUser}"'';
230 "--auth_type" = ''"form"'';
231 "--base_url" = ''"${cfg.baseUrl}"'';
232 "--language" = ''"${cfg.language}"'';
233 "--db-type" = ''"${cfg.database.type}"'';
234 # The following attributes are optional depending on the type of
235 # database. Those that evaluate to null on the left hand side
236 # will be omitted.
237 ${if cfg.database.name != null then "--db-base" else null} = ''"${cfg.database.name}"'';
238 ${if cfg.database.passFile != null then "--db-password" else null} = ''"$(cat ${cfg.database.passFile})"'';
239 ${if cfg.database.user != null then "--db-user" else null} = ''"${cfg.database.user}"'';
240 ${if cfg.database.tableprefix != null then "--db-prefix" else null} = ''"${cfg.database.tableprefix}"'';
241 ${if cfg.database.host != null && cfg.database.port != null then "--db-host" else null} = ''"${cfg.database.host}:${toString cfg.database.port}"'';
242 });
243 in
244 {
245 description = "Set up the state directory for FreshRSS before use";
246 wantedBy = [ "multi-user.target" ];
247 serviceConfig = defaultServiceConfig //{
248 Type = "oneshot";
249 User = "freshrss";
250 Group = "freshrss";
251 StateDirectory = "freshrss";
252 WorkingDirectory = cfg.package;
253 };
254 environment = {
255 FRESHRSS_DATA_PATH = cfg.dataDir;
256 };
257
258 script = ''
259 # do installation or reconfigure
260 if test -f ${cfg.dataDir}/config.php; then
261 # reconfigure with settings
262 ./cli/reconfigure.php ${settingsFlags}
263 ./cli/update-user.php --user ${cfg.defaultUser} --password "$(cat ${cfg.passwordFile})"
264 else
265 # check correct folders in data folder
266 ./cli/prepare.php
267 # install with settings
268 ./cli/do-install.php ${settingsFlags}
269 ./cli/create-user.php --user ${cfg.defaultUser} --password "$(cat ${cfg.passwordFile})"
270 fi
271 '';
272 };
273
274 systemd.services.freshrss-updater = {
275 description = "FreshRSS feed updater";
276 after = [ "freshrss-config.service" ];
277 wantedBy = [ "multi-user.target" ];
278 startAt = "*:0/5";
279 environment = {
280 FRESHRSS_DATA_PATH = cfg.dataDir;
281 };
282 serviceConfig = defaultServiceConfig //{
283 ExecStart = "${cfg.package}/app/actualize_script.php";
284 };
285 };
286 };
287}