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