2023-06-20 21:26:40 +00:00
|
|
|
flake: { config, lib, pkgs, ... }:
|
2023-06-05 21:52:51 +00:00
|
|
|
|
|
|
|
let
|
2023-06-20 21:06:24 +00:00
|
|
|
inherit (lib) filterAttrs foldl imap1 mapAttrsToList mkEnableOption mkIf mkMerge mkOption mkPackageOption types;
|
2023-06-06 01:07:31 +00:00
|
|
|
intakeCfg = config.services.intake;
|
2023-06-05 21:52:51 +00:00
|
|
|
in {
|
|
|
|
options = {
|
2023-06-06 01:07:31 +00:00
|
|
|
services.intake = {
|
|
|
|
listen.addr = mkOption {
|
|
|
|
type = types.str;
|
|
|
|
default = "0.0.0.0";
|
2023-06-19 18:34:52 +00:00
|
|
|
description = "The listen address for the entry point to intake services. This endpoint will redirect to a "
|
|
|
|
"local port based on the request's HTTP Basic Auth credentials.";
|
2023-06-06 01:07:31 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
listen.port = mkOption {
|
|
|
|
type = types.port;
|
2023-06-08 05:47:02 +00:00
|
|
|
default = 80;
|
2023-06-19 18:34:52 +00:00
|
|
|
description = "The listen port for the entry point to intake services. This endpoint will redirect to a local "
|
|
|
|
"port based on the request's HTTP Basic Auth credentials.";
|
2023-06-06 20:47:57 +00:00
|
|
|
};
|
|
|
|
|
2023-06-20 21:06:24 +00:00
|
|
|
package = mkPackageOption pkgs "intake" {};
|
|
|
|
|
2023-06-06 20:47:57 +00:00
|
|
|
internalPortStart = mkOption {
|
|
|
|
type = types.port;
|
|
|
|
default = 24130;
|
2023-06-19 18:34:52 +00:00
|
|
|
description = "The first port to use for internal service endpoints. A number of ports will be continguously "
|
|
|
|
"allocated equal to the number of users with enabled intake services.";
|
2023-06-06 01:07:31 +00:00
|
|
|
};
|
|
|
|
|
2023-06-21 03:59:58 +00:00
|
|
|
extraPackages = mkOption {
|
|
|
|
type = types.listOf types.package;
|
|
|
|
default = [];
|
|
|
|
description = "Extra packages available to all enabled users and their intake services.";
|
|
|
|
};
|
|
|
|
|
2023-06-06 01:07:31 +00:00
|
|
|
users = mkOption {
|
|
|
|
description = "User intake service definitions.";
|
|
|
|
default = {};
|
|
|
|
type = types.attrsOf (types.submodule {
|
|
|
|
options = {
|
|
|
|
enable = mkEnableOption "intake, a personal feed aggregator.";
|
|
|
|
|
2023-06-21 03:59:58 +00:00
|
|
|
extraPackages = mkOption {
|
2023-06-06 01:07:31 +00:00
|
|
|
type = types.listOf types.package;
|
|
|
|
default = [];
|
2023-06-21 03:59:58 +00:00
|
|
|
description = "Extra packages available to this user and their intake service.";
|
2023-06-06 01:07:31 +00:00
|
|
|
};
|
|
|
|
};
|
|
|
|
});
|
|
|
|
};
|
|
|
|
};
|
2023-06-05 21:52:51 +00:00
|
|
|
};
|
|
|
|
|
2023-06-06 01:07:31 +00:00
|
|
|
config =
|
|
|
|
let
|
|
|
|
# Define the intake package and a python environment to run it from
|
2023-06-20 21:06:24 +00:00
|
|
|
intake = intakeCfg.package;
|
2023-06-06 01:07:31 +00:00
|
|
|
pythonEnv = pkgs.python38.withPackages (pypkgs: [ intake ]);
|
2023-06-06 20:47:57 +00:00
|
|
|
|
|
|
|
# Assign each user an internal port for their personal intake instance
|
|
|
|
enabledUsers = filterAttrs (userName: userCfg: userCfg.enable) intakeCfg.users;
|
|
|
|
enabledUserNames = mapAttrsToList (userName: userCfg: userName) enabledUsers;
|
|
|
|
userPortList = imap1 (i: userName: { ${userName} = i + intakeCfg.internalPortStart; }) enabledUserNames;
|
|
|
|
userPort = foldl (acc: val: acc // val) {} userPortList;
|
2023-06-19 18:34:52 +00:00
|
|
|
|
|
|
|
# To avoid polluting PATH with httpd programs, define an htpasswd wrapper
|
|
|
|
htpasswdWrapper = pkgs.writeShellScriptBin "htpasswd" ''
|
|
|
|
${pkgs.apacheHttpd}/bin/htpasswd $@
|
|
|
|
'';
|
|
|
|
|
|
|
|
# File locations
|
|
|
|
intakeDir = "/etc/intake";
|
|
|
|
intakePwd = "${intakeDir}/htpasswd";
|
2023-06-06 01:07:31 +00:00
|
|
|
in {
|
2023-06-22 01:32:44 +00:00
|
|
|
# Apply the overlay so intake is included in pkgs.
|
2023-06-20 21:26:40 +00:00
|
|
|
nixpkgs.overlays = [ flake.overlays.default ];
|
|
|
|
|
2023-06-21 03:59:58 +00:00
|
|
|
# Define a user group for access to the htpasswd file. nginx needs to be able to read it.
|
2023-06-19 18:34:52 +00:00
|
|
|
users.groups.intake.members = mkIf (enabledUsers != {}) (enabledUserNames ++ [ "nginx" ]);
|
|
|
|
|
|
|
|
# Define an activation script that ensures that the htpasswd file exists.
|
|
|
|
system.activationScripts.etc-intake = ''
|
|
|
|
if [ ! -e ${intakeDir} ]; then
|
|
|
|
${pkgs.coreutils}/bin/mkdir -p ${intakeDir};
|
|
|
|
fi
|
|
|
|
${pkgs.coreutils}/bin/chown root:root ${intakeDir}
|
|
|
|
${pkgs.coreutils}/bin/chmod 755 ${intakeDir}
|
|
|
|
if [ ! -e ${intakePwd} ]; then
|
|
|
|
${pkgs.coreutils}/bin/touch ${intakePwd}
|
|
|
|
fi
|
|
|
|
${pkgs.coreutils}/bin/chown root:intake ${intakePwd}
|
|
|
|
${pkgs.coreutils}/bin/chmod 660 ${intakePwd}
|
|
|
|
'';
|
|
|
|
|
2023-06-21 03:59:58 +00:00
|
|
|
# Give every intake user the htpasswd wrapper, the shared packages, and the user-specific packages.
|
2023-06-19 18:34:52 +00:00
|
|
|
users.users =
|
|
|
|
let
|
2023-06-21 03:59:58 +00:00
|
|
|
addPackagesToUser = userName: {
|
|
|
|
${userName}.packages =
|
|
|
|
[ htpasswdWrapper intake ]
|
|
|
|
++ intakeCfg.extraPackages
|
|
|
|
++ intakeCfg.users.${userName}.extraPackages;
|
|
|
|
};
|
|
|
|
in mkMerge (map addPackagesToUser enabledUserNames);
|
2023-06-19 18:34:52 +00:00
|
|
|
|
2023-06-22 01:32:44 +00:00
|
|
|
# Enable cron
|
|
|
|
services.cron.enable = true;
|
|
|
|
|
2023-06-06 20:47:57 +00:00
|
|
|
# Define a user service for each configured user
|
2023-06-06 01:07:31 +00:00
|
|
|
systemd.services =
|
|
|
|
let
|
|
|
|
runScript = userName: pkgs.writeShellScript "intake-run.sh" ''
|
2023-06-23 04:47:16 +00:00
|
|
|
# Add the setuid wrapper directory so `crontab` is accessible
|
|
|
|
export PATH="${config.security.wrapperDir}:$PATH"
|
2023-06-06 20:47:57 +00:00
|
|
|
${pythonEnv}/bin/intake run -d /home/${userName}/.local/share/intake --port ${toString userPort.${userName}}
|
2023-06-06 01:07:31 +00:00
|
|
|
'';
|
|
|
|
# systemd service definition for a single user, given `services.intake.users.userName` = `userCfg`
|
|
|
|
userServiceConfig = userName: userCfg: {
|
|
|
|
"intake@${userName}" = {
|
|
|
|
description = "Intake service for user ${userName}";
|
|
|
|
script = "${runScript userName}";
|
2023-06-21 03:59:58 +00:00
|
|
|
path = intakeCfg.extraPackages ++ userCfg.extraPackages;
|
2023-06-06 01:07:31 +00:00
|
|
|
serviceConfig = {
|
|
|
|
User = userName;
|
|
|
|
Type = "simple";
|
|
|
|
};
|
|
|
|
wantedBy = [ "multi-user.target" ];
|
|
|
|
after = [ "network.target" ];
|
|
|
|
enable = userCfg.enable;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
in mkMerge (mapAttrsToList userServiceConfig intakeCfg.users);
|
2023-06-06 20:47:57 +00:00
|
|
|
|
|
|
|
# Define an nginx reverse proxy to request auth
|
|
|
|
services.nginx = mkIf (enabledUsers != {}) {
|
|
|
|
enable = true;
|
|
|
|
virtualHosts."intake" = mkIf (enabledUsers != {}) {
|
|
|
|
listen = [ intakeCfg.listen ];
|
|
|
|
locations."/" = {
|
|
|
|
proxyPass = "http://127.0.0.1:$target_port";
|
2023-06-19 18:34:52 +00:00
|
|
|
basicAuthFile = intakePwd;
|
2023-06-06 20:47:57 +00:00
|
|
|
};
|
|
|
|
extraConfig = foldl (acc: val: acc + val) "" (mapAttrsToList (userName: port: ''
|
|
|
|
if ($remote_user = "${userName}") {
|
|
|
|
set $target_port ${toString port};
|
|
|
|
}
|
|
|
|
'') userPort);
|
|
|
|
};
|
|
|
|
};
|
2023-06-05 21:52:51 +00:00
|
|
|
};
|
2023-06-06 01:07:31 +00:00
|
|
|
}
|