2019-09-12 18:47:15 +01:00
|
|
|
{
|
|
|
|
config,
|
|
|
|
lib,
|
|
|
|
pkgs,
|
|
|
|
...
|
|
|
|
}:
|
|
|
|
|
|
|
|
let
|
|
|
|
|
|
|
|
cfg = config.services.ttyd;
|
|
|
|
|
2024-01-31 17:58:59 +01:00
|
|
|
inherit (lib)
|
|
|
|
optionals
|
|
|
|
types
|
|
|
|
mkOption
|
|
|
|
;
|
|
|
|
|
2019-09-12 18:47:15 +01:00
|
|
|
# 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)
|
2024-09-08 19:00:53 +02:00
|
|
|
]
|
|
|
|
++ (lib.concatLists (
|
|
|
|
lib.mapAttrsToList (_k: _v: [
|
|
|
|
"--client-option"
|
|
|
|
"${_k}=${_v}"
|
|
|
|
]) cfg.clientOptions
|
|
|
|
))
|
2019-09-12 18:47:15 +01:00
|
|
|
++ [
|
|
|
|
"--terminal-type"
|
|
|
|
cfg.terminalType
|
|
|
|
]
|
|
|
|
++ optionals cfg.checkOrigin [ "--check-origin" ]
|
2024-01-31 17:54:32 +01:00
|
|
|
++ optionals cfg.writeable [ "--writable" ] # the typo is correct
|
2019-09-12 18:47:15 +01:00
|
|
|
++ [
|
|
|
|
"--max-clients"
|
|
|
|
(toString cfg.maxClients)
|
|
|
|
]
|
|
|
|
++ optionals (cfg.indexFile != null) [
|
|
|
|
"--index"
|
|
|
|
cfg.indexFile
|
2024-12-10 20:27:17 +01:00
|
|
|
]
|
2019-09-12 18:47:15 +01:00
|
|
|
++ optionals cfg.enableIPv6 [ "--ipv6" ]
|
2024-09-08 18:55:22 +02:00
|
|
|
++ optionals cfg.enableSSL [
|
|
|
|
"--ssl"
|
|
|
|
"--ssl-cert"
|
|
|
|
cfg.certFile
|
|
|
|
"--ssl-key"
|
|
|
|
cfg.keyFile
|
|
|
|
]
|
|
|
|
++ optionals (cfg.enableSSL && cfg.caFile != null) [
|
|
|
|
"--ssl-ca"
|
|
|
|
cfg.caFile
|
|
|
|
]
|
2019-09-12 18:47:15 +01:00
|
|
|
++ [
|
|
|
|
"--debug"
|
|
|
|
(toString cfg.logLevel)
|
|
|
|
];
|
|
|
|
|
|
|
|
in
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
###### interface
|
|
|
|
|
|
|
|
options = {
|
|
|
|
services.ttyd = {
|
2024-01-31 17:58:59 +01:00
|
|
|
enable = lib.mkEnableOption ("ttyd daemon");
|
2019-09-12 18:47:15 +01:00
|
|
|
|
|
|
|
port = mkOption {
|
2021-06-18 17:28:17 +02:00
|
|
|
type = types.port;
|
2019-09-12 18:47:15 +01:00
|
|
|
default = 7681;
|
2024-01-31 17:58:59 +01:00
|
|
|
description = "Port to listen on (use 0 for random port)";
|
2019-09-12 18:47:15 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
socket = mkOption {
|
|
|
|
type = types.nullOr types.path;
|
|
|
|
default = null;
|
|
|
|
example = "/var/run/ttyd.sock";
|
2024-01-31 17:58:59 +01:00
|
|
|
description = "UNIX domain socket path to bind.";
|
2019-09-12 18:47:15 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
interface = mkOption {
|
|
|
|
type = types.nullOr types.str;
|
|
|
|
default = null;
|
|
|
|
example = "eth0";
|
2024-01-31 17:58:59 +01:00
|
|
|
description = "Network interface to bind.";
|
2019-09-12 18:47:15 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
username = mkOption {
|
|
|
|
type = types.nullOr types.str;
|
|
|
|
default = null;
|
2024-01-31 17:59:39 +01:00
|
|
|
description = "Username for basic http authentication.";
|
2019-09-12 18:47:15 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
passwordFile = mkOption {
|
|
|
|
type = types.nullOr types.path;
|
|
|
|
default = null;
|
|
|
|
apply = value: if value == null then null else toString value;
|
2024-01-31 17:58:59 +01:00
|
|
|
description = ''
|
2024-01-31 17:59:39 +01:00
|
|
|
File containing the password to use for basic http authentication.
|
2019-09-12 18:47:15 +01:00
|
|
|
For insecurely putting the password in the globally readable store use
|
|
|
|
`pkgs.writeText "ttydpw" "MyPassword"`.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
signal = mkOption {
|
|
|
|
type = types.ints.u8;
|
|
|
|
default = 1;
|
2024-01-31 17:58:59 +01:00
|
|
|
description = "Signal to send to the command on session close.";
|
2019-09-12 18:47:15 +01:00
|
|
|
};
|
|
|
|
|
2024-01-31 17:59:39 +01:00
|
|
|
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.";
|
|
|
|
};
|
|
|
|
|
2024-01-31 17:54:32 +01:00
|
|
|
writeable = mkOption {
|
|
|
|
type = types.nullOr types.bool;
|
|
|
|
default = null; # null causes an eval error, forcing the user to consider attack surface
|
|
|
|
example = true;
|
2024-01-31 17:58:59 +01:00
|
|
|
description = "Allow clients to write to the TTY.";
|
2024-01-31 17:54:32 +01:00
|
|
|
};
|
|
|
|
|
2019-09-12 18:47:15 +01:00
|
|
|
clientOptions = mkOption {
|
|
|
|
type = types.attrsOf types.str;
|
|
|
|
default = { };
|
2024-01-31 17:58:59 +01:00
|
|
|
example = lib.literalExpression ''
|
2023-06-30 18:14:37 +02:00
|
|
|
{
|
|
|
|
fontSize = "16";
|
|
|
|
fontFamily = "Fira Code";
|
|
|
|
}
|
|
|
|
'';
|
2024-01-31 17:58:59 +01:00
|
|
|
description = ''
|
2019-09-12 18:47:15 +01:00
|
|
|
Attribute set of client options for xtermjs.
|
|
|
|
<https://xtermjs.org/docs/api/terminal/interfaces/iterminaloptions/>
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
terminalType = mkOption {
|
|
|
|
type = types.str;
|
|
|
|
default = "xterm-256color";
|
2024-01-31 17:58:59 +01:00
|
|
|
description = "Terminal type to report.";
|
2019-09-12 18:47:15 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
checkOrigin = mkOption {
|
|
|
|
type = types.bool;
|
|
|
|
default = false;
|
2024-01-31 17:58:59 +01:00
|
|
|
description = "Whether to allow a websocket connection from a different origin.";
|
2019-09-12 18:47:15 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
maxClients = mkOption {
|
|
|
|
type = types.int;
|
|
|
|
default = 0;
|
2024-01-31 17:58:59 +01:00
|
|
|
description = "Maximum clients to support (0, no limit)";
|
2019-09-12 18:47:15 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
indexFile = mkOption {
|
|
|
|
type = types.nullOr types.path;
|
|
|
|
default = null;
|
2024-01-31 17:58:59 +01:00
|
|
|
description = "Custom index.html path";
|
2019-09-12 18:47:15 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
enableIPv6 = mkOption {
|
|
|
|
type = types.bool;
|
|
|
|
default = false;
|
2024-01-31 17:58:59 +01:00
|
|
|
description = "Whether or not to enable IPv6 support.";
|
2019-09-12 18:47:15 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
enableSSL = mkOption {
|
|
|
|
type = types.bool;
|
|
|
|
default = false;
|
2024-01-31 17:58:59 +01:00
|
|
|
description = "Whether or not to enable SSL (https) support.";
|
2019-09-12 18:47:15 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
certFile = mkOption {
|
|
|
|
type = types.nullOr types.path;
|
|
|
|
default = null;
|
2024-01-31 17:58:59 +01:00
|
|
|
description = "SSL certificate file path.";
|
2019-09-12 18:47:15 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
keyFile = mkOption {
|
|
|
|
type = types.nullOr types.path;
|
|
|
|
default = null;
|
|
|
|
apply = value: if value == null then null else toString value;
|
2024-01-31 17:58:59 +01:00
|
|
|
description = ''
|
2019-09-12 18:47:15 +01:00
|
|
|
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;
|
2024-01-31 17:58:59 +01:00
|
|
|
description = "SSL CA file path for client certificate verification.";
|
2019-09-12 18:47:15 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
logLevel = mkOption {
|
|
|
|
type = types.int;
|
|
|
|
default = 7;
|
2024-01-31 17:58:59 +01:00
|
|
|
description = "Set log level.";
|
2019-09-12 18:47:15 +01:00
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
###### implementation
|
|
|
|
|
2024-01-31 17:58:59 +01:00
|
|
|
config = lib.mkIf cfg.enable {
|
2019-09-12 18:47:15 +01:00
|
|
|
|
|
|
|
assertions = [
|
|
|
|
{
|
2024-09-08 18:55:22 +02:00
|
|
|
assertion = cfg.enableSSL -> cfg.certFile != null && cfg.keyFile != null;
|
|
|
|
message = "SSL is enabled for ttyd, but no certFile or keyFile has been specified.";
|
|
|
|
}
|
2024-01-31 17:54:32 +01:00
|
|
|
{
|
|
|
|
assertion = cfg.writeable != null;
|
|
|
|
message = "services.ttyd.writeable must be set";
|
|
|
|
}
|
2019-09-12 18:47:15 +01:00
|
|
|
{
|
|
|
|
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 = {
|
2024-01-31 17:59:39 +01:00
|
|
|
User = cfg.user;
|
2024-01-20 17:33:00 +01:00
|
|
|
LoadCredential = lib.optionalString (
|
|
|
|
cfg.passwordFile != null
|
|
|
|
) "TTYD_PASSWORD_FILE:${cfg.passwordFile}";
|
2019-09-12 18:47:15 +01:00
|
|
|
};
|
2024-12-10 20:27:17 +01:00
|
|
|
|
2019-09-12 18:47:15 +01:00
|
|
|
script =
|
|
|
|
if cfg.passwordFile != null then
|
|
|
|
''
|
2024-01-20 17:33:00 +01:00
|
|
|
PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/TTYD_PASSWORD_FILE")
|
2019-09-12 18:47:15 +01:00
|
|
|
${pkgs.ttyd}/bin/ttyd ${lib.escapeShellArgs args} \
|
2024-01-31 17:58:59 +01:00
|
|
|
--credential ${lib.escapeShellArg cfg.username}:"$PASSWORD" \
|
2024-01-31 17:59:39 +01:00
|
|
|
${cfg.entrypoint}
|
2019-09-12 18:47:15 +01:00
|
|
|
''
|
|
|
|
else
|
2024-12-10 20:27:17 +01:00
|
|
|
''
|
2019-09-12 18:47:15 +01:00
|
|
|
${pkgs.ttyd}/bin/ttyd ${lib.escapeShellArgs args} \
|
2024-01-31 17:59:39 +01:00
|
|
|
${cfg.entrypoint}
|
2019-09-12 18:47:15 +01:00
|
|
|
'';
|
|
|
|
};
|
|
|
|
};
|
|
|
|
}
|