at 25.11-pre 12 kB view raw
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}