{ config, lib, pkgs, ... }: with lib; let cfg = config.services.freshrss; poolName = "freshrss"; extension-env = pkgs.buildEnv { name = "freshrss-extensions"; paths = cfg.extensions; }; env-vars = { DATA_PATH = cfg.dataDir; THIRDPARTY_EXTENSIONS_PATH = "${extension-env}/share/freshrss/"; }; in { meta.maintainers = with maintainers; [ etu stunkymonkey mattchrist ]; options.services.freshrss = { enable = mkEnableOption "FreshRSS RSS aggregator and reader with php-fpm backend."; package = mkPackageOption pkgs "freshrss" { }; extensions = mkOption { type = types.listOf types.package; default = [ ]; defaultText = literalExpression "[]"; example = literalExpression '' with freshrss-extensions; [ youtube ] ++ [ (freshrss-extensions.buildFreshRssExtension { FreshRssExtUniqueId = "ReadingTime"; pname = "reading-time"; version = "1.5"; src = pkgs.fetchFromGitLab { domain = "framagit.org"; owner = "Lapineige"; repo = "FreshRSS_Extension-ReadingTime"; rev = "fb6e9e944ef6c5299fa56ffddbe04c41e5a34ebf"; hash = "sha256-C5cRfaphx4Qz2xg2z+v5qRji8WVSIpvzMbethTdSqsk="; }; }) ] ''; description = "Additional extensions to be used."; }; defaultUser = mkOption { type = types.str; default = "admin"; description = "Default username for FreshRSS."; example = "eva"; }; passwordFile = mkOption { type = types.nullOr types.path; default = null; description = "Password for the defaultUser for FreshRSS."; example = "/run/secrets/freshrss"; }; baseUrl = mkOption { type = types.str; description = "Default URL for FreshRSS."; example = "https://freshrss.example.com"; }; language = mkOption { type = types.str; default = "en"; description = "Default language for FreshRSS."; example = "de"; }; database = { type = mkOption { type = types.enum [ "sqlite" "pgsql" "mysql" ]; default = "sqlite"; description = "Database type."; example = "pgsql"; }; host = mkOption { type = types.nullOr types.str; default = "localhost"; description = "Database host for FreshRSS."; }; port = mkOption { type = types.nullOr types.port; default = null; description = "Database port for FreshRSS."; example = 3306; }; user = mkOption { type = types.nullOr types.str; default = "freshrss"; description = "Database user for FreshRSS."; }; passFile = mkOption { type = types.nullOr types.path; default = null; description = "Database password file for FreshRSS."; example = "/run/secrets/freshrss"; }; name = mkOption { type = types.nullOr types.str; default = "freshrss"; description = "Database name for FreshRSS."; }; tableprefix = mkOption { type = types.nullOr types.str; default = null; description = "Database table prefix for FreshRSS."; example = "freshrss"; }; }; dataDir = mkOption { type = types.str; default = "/var/lib/freshrss"; description = "Default data folder for FreshRSS."; example = "/mnt/freshrss"; }; virtualHost = mkOption { type = types.nullOr types.str; default = "freshrss"; description = '' Name of the nginx virtualhost to use and setup. If null, do not setup any virtualhost. ''; }; pool = mkOption { type = types.str; default = poolName; description = '' Name of the php-fpm pool to use and setup. If not specified, a pool will be created with default values. ''; }; user = mkOption { type = types.str; default = "freshrss"; description = "User under which FreshRSS runs."; }; authType = mkOption { type = types.enum [ "form" "http_auth" "none" ]; default = "form"; description = "Authentication type for FreshRSS."; }; }; config = let defaultServiceConfig = { ReadWritePaths = "${cfg.dataDir}"; CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; DeviceAllow = ""; LockPersonality = true; NoNewPrivileges = true; PrivateDevices = true; PrivateTmp = true; PrivateUsers = true; ProcSubset = "pid"; ProtectClock = true; ProtectControlGroups = true; ProtectHome = true; ProtectHostname = true; ProtectKernelLogs = true; ProtectKernelModules = true; ProtectKernelTunables = true; ProtectProc = "invisible"; ProtectSystem = "strict"; RemoveIPC = true; RestrictNamespaces = true; RestrictRealtime = true; RestrictSUIDSGID = true; SystemCallArchitectures = "native"; SystemCallFilter = [ "@system-service" "~@resources" "~@privileged" ]; UMask = "0007"; Type = "oneshot"; User = cfg.user; Group = config.users.users.${cfg.user}.group; StateDirectory = "freshrss"; WorkingDirectory = cfg.package; }; in mkIf cfg.enable { assertions = mkIf (cfg.authType == "form") [ { assertion = cfg.passwordFile != null; message = '' `passwordFile` must be supplied when using "form" authentication! ''; } ]; # Set up a Nginx virtual host. services.nginx = mkIf (cfg.virtualHost != null) { enable = true; virtualHosts.${cfg.virtualHost} = { root = "${cfg.package}/p"; # php files handling # this regex is mandatory because of the API locations."~ ^.+?\.php(/.*)?$".extraConfig = '' fastcgi_pass unix:${config.services.phpfpm.pools.${cfg.pool}.socket}; fastcgi_split_path_info ^(.+\.php)(/.*)$; # By default, the variable PATH_INFO is not set under PHP-FPM # But FreshRSS API greader.php need it. If you have a “Bad Request” error, double check this var! # NOTE: the separate $path_info variable is required. For more details, see: # https://trac.nginx.org/nginx/ticket/321 set $path_info $fastcgi_path_info; fastcgi_param PATH_INFO $path_info; include ${pkgs.nginx}/conf/fastcgi_params; include ${pkgs.nginx}/conf/fastcgi.conf; ''; locations."/" = { tryFiles = "$uri $uri/ index.php"; index = "index.php index.html index.htm"; }; }; }; # Set up phpfpm pool services.phpfpm.pools = mkIf (cfg.pool == poolName) { ${poolName} = { user = "freshrss"; settings = { "listen.owner" = "nginx"; "listen.group" = "nginx"; "listen.mode" = "0600"; "pm" = "dynamic"; "pm.max_children" = 32; "pm.max_requests" = 500; "pm.start_servers" = 2; "pm.min_spare_servers" = 2; "pm.max_spare_servers" = 5; "catch_workers_output" = true; }; phpEnv = env-vars; }; }; users.users."${cfg.user}" = { description = "FreshRSS service user"; isSystemUser = true; group = "${cfg.user}"; home = cfg.dataDir; }; users.groups."${cfg.user}" = { }; systemd.tmpfiles.settings."10-freshrss".${cfg.dataDir}.d = { inherit (cfg) user; group = config.users.users.${cfg.user}.group; }; systemd.services.freshrss-config = let settingsFlags = concatStringsSep " \\\n " (mapAttrsToList (k: v: "${k} ${toString v}") { "--default_user" = ''"${cfg.defaultUser}"''; "--auth_type" = ''"${cfg.authType}"''; "--base_url" = ''"${cfg.baseUrl}"''; "--language" = ''"${cfg.language}"''; "--db-type" = ''"${cfg.database.type}"''; # The following attributes are optional depending on the type of # database. Those that evaluate to null on the left hand side # will be omitted. ${if cfg.database.name != null then "--db-base" else null} = ''"${cfg.database.name}"''; ${if cfg.database.passFile != null then "--db-password" else null} = ''"$(cat ${cfg.database.passFile})"''; ${if cfg.database.user != null then "--db-user" else null} = ''"${cfg.database.user}"''; ${if cfg.database.tableprefix != null then "--db-prefix" else null} = ''"${cfg.database.tableprefix}"''; # hostname:port e.g. "localhost:5432" ${if cfg.database.host != null && cfg.database.port != null then "--db-host" else null} = ''"${cfg.database.host}:${toString cfg.database.port}"''; # socket path e.g. "/run/postgresql" ${if cfg.database.host != null && cfg.database.port == null then "--db-host" else null} = ''"${cfg.database.host}"''; }); in { description = "Set up the state directory for FreshRSS before use"; wantedBy = [ "multi-user.target" ]; serviceConfig = defaultServiceConfig // { RemainAfterExit = true; }; restartIfChanged = true; environment = env-vars; script = let userScriptArgs = ''--user ${cfg.defaultUser} ${optionalString (cfg.authType == "form") ''--password "$(cat ${cfg.passwordFile})"''}''; updateUserScript = optionalString (cfg.authType == "form" || cfg.authType == "none") '' ./cli/update-user.php ${userScriptArgs} ''; createUserScript = optionalString (cfg.authType == "form" || cfg.authType == "none") '' ./cli/create-user.php ${userScriptArgs} ''; in '' # do installation or reconfigure if test -f ${cfg.dataDir}/config.php; then # reconfigure with settings ./cli/reconfigure.php ${settingsFlags} ${updateUserScript} else # check correct folders in data folder ./cli/prepare.php # install with settings ./cli/do-install.php ${settingsFlags} ${createUserScript} fi ''; }; systemd.services.freshrss-updater = { description = "FreshRSS feed updater"; after = [ "freshrss-config.service" ]; startAt = "*:0/5"; environment = env-vars; serviceConfig = defaultServiceConfig // { ExecStart = "${cfg.package}/app/actualize_script.php"; }; }; }; }