{ config, lib, pkgs, ... }: let cfg = config.services.ttyd; inherit (lib) optionals types concatLists mapAttrsToList mkOption ; # Command line arguments for the ttyd daemon args = [ "--port" (toString cfg.port) ] ++ optionals (cfg.socket != null) [ "--interface" cfg.socket ] ++ optionals (cfg.interface != null) [ "--interface" cfg.interface ] ++ [ "--signal" (toString cfg.signal) ] ++ (concatLists (mapAttrsToList (_k: _v: [ "--client-option" "${_k}=${_v}" ]) cfg.clientOptions)) ++ [ "--terminal-type" cfg.terminalType ] ++ optionals cfg.checkOrigin [ "--check-origin" ] ++ optionals cfg.writeable [ "--writable" ] # the typo is correct ++ [ "--max-clients" (toString cfg.maxClients) ] ++ optionals (cfg.indexFile != null) [ "--index" cfg.indexFile ] ++ optionals cfg.enableIPv6 [ "--ipv6" ] ++ optionals cfg.enableSSL [ "--ssl-cert" cfg.certFile "--ssl-key" cfg.keyFile "--ssl-ca" cfg.caFile ] ++ [ "--debug" (toString cfg.logLevel) ]; in { ###### interface options = { services.ttyd = { enable = lib.mkEnableOption ("ttyd daemon"); port = mkOption { type = types.port; default = 7681; description = "Port to listen on (use 0 for random port)"; }; socket = mkOption { type = types.nullOr types.path; default = null; example = "/var/run/ttyd.sock"; description = "UNIX domain socket path to bind."; }; interface = mkOption { type = types.nullOr types.str; default = null; example = "eth0"; description = "Network interface to bind."; }; username = mkOption { type = types.nullOr types.str; default = null; description = "Username for basic http authentication."; }; passwordFile = mkOption { type = types.nullOr types.path; default = null; apply = value: if value == null then null else toString value; description = '' File containing the password to use for basic http authentication. For insecurely putting the password in the globally readable store use `pkgs.writeText "ttydpw" "MyPassword"`. ''; }; signal = mkOption { type = types.ints.u8; default = 1; description = "Signal to send to the command on session close."; }; entrypoint = mkOption { type = types.listOf types.str; default = [ "${pkgs.shadow}/bin/login" ]; defaultText = lib.literalExpression '' [ "''${pkgs.shadow}/bin/login" ] ''; example = lib.literalExpression '' [ (lib.getExe pkgs.htop) ] ''; description = "Which command ttyd runs."; apply = lib.escapeShellArgs; }; user = mkOption { type = types.str; # `login` needs to be run as root default = "root"; description = "Which unix user ttyd should run as."; }; writeable = mkOption { type = types.nullOr types.bool; default = null; # null causes an eval error, forcing the user to consider attack surface example = true; description = "Allow clients to write to the TTY."; }; clientOptions = mkOption { type = types.attrsOf types.str; default = {}; example = lib.literalExpression '' { fontSize = "16"; fontFamily = "Fira Code"; } ''; description = '' Attribute set of client options for xtermjs. ''; }; terminalType = mkOption { type = types.str; default = "xterm-256color"; description = "Terminal type to report."; }; checkOrigin = mkOption { type = types.bool; default = false; description = "Whether to allow a websocket connection from a different origin."; }; maxClients = mkOption { type = types.int; default = 0; description = "Maximum clients to support (0, no limit)"; }; indexFile = mkOption { type = types.nullOr types.path; default = null; description = "Custom index.html path"; }; enableIPv6 = mkOption { type = types.bool; default = false; description = "Whether or not to enable IPv6 support."; }; enableSSL = mkOption { type = types.bool; default = false; description = "Whether or not to enable SSL (https) support."; }; certFile = mkOption { type = types.nullOr types.path; default = null; description = "SSL certificate file path."; }; keyFile = mkOption { type = types.nullOr types.path; default = null; apply = value: if value == null then null else toString value; description = '' SSL key file path. For insecurely putting the keyFile in the globally readable store use `pkgs.writeText "ttydKeyFile" "SSLKEY"`. ''; }; caFile = mkOption { type = types.nullOr types.path; default = null; description = "SSL CA file path for client certificate verification."; }; logLevel = mkOption { type = types.int; default = 7; description = "Set log level."; }; }; }; ###### implementation config = lib.mkIf cfg.enable { assertions = [ { assertion = cfg.enableSSL -> cfg.certFile != null && cfg.keyFile != null && cfg.caFile != null; message = "SSL is enabled for ttyd, but no certFile, keyFile or caFile has been specified."; } { assertion = cfg.writeable != null; message = "services.ttyd.writeable must be set"; } { assertion = ! (cfg.interface != null && cfg.socket != null); message = "Cannot set both interface and socket for ttyd."; } { assertion = (cfg.username != null) == (cfg.passwordFile != null); message = "Need to set both username and passwordFile for ttyd"; } ]; systemd.services.ttyd = { description = "ttyd Web Server Daemon"; wantedBy = [ "multi-user.target" ]; serviceConfig = { User = cfg.user; LoadCredential = lib.optionalString (cfg.passwordFile != null) "TTYD_PASSWORD_FILE:${cfg.passwordFile}"; }; script = if cfg.passwordFile != null then '' PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/TTYD_PASSWORD_FILE") ${pkgs.ttyd}/bin/ttyd ${lib.escapeShellArgs args} \ --credential ${lib.escapeShellArg cfg.username}:"$PASSWORD" \ ${cfg.entrypoint} '' else '' ${pkgs.ttyd}/bin/ttyd ${lib.escapeShellArgs args} \ ${cfg.entrypoint} ''; }; }; }