0
0
Fork 0
mirror of https://github.com/NixOS/nixpkgs.git synced 2025-07-14 06:00:33 +03:00

Merge pull request #285314 from pbsds/ttyd-1706718068

nixos/ttyd: add `entrypoint` and `writable` option
This commit is contained in:
Peder Bergebakken Sundt 2024-02-13 19:41:11 +01:00 committed by GitHub
commit bf7c95ce73
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 78 additions and 34 deletions

View file

@ -1,11 +1,17 @@
{ config, lib, pkgs, ... }: { config, lib, pkgs, ... }:
with lib;
let let
cfg = config.services.ttyd; cfg = config.services.ttyd;
inherit (lib)
optionals
types
concatLists
mapAttrsToList
mkOption
;
# Command line arguments for the ttyd daemon # Command line arguments for the ttyd daemon
args = [ "--port" (toString cfg.port) ] args = [ "--port" (toString cfg.port) ]
++ optionals (cfg.socket != null) [ "--interface" cfg.socket ] ++ optionals (cfg.socket != null) [ "--interface" cfg.socket ]
@ -14,6 +20,7 @@ let
++ (concatLists (mapAttrsToList (_k: _v: [ "--client-option" "${_k}=${_v}" ]) cfg.clientOptions)) ++ (concatLists (mapAttrsToList (_k: _v: [ "--client-option" "${_k}=${_v}" ]) cfg.clientOptions))
++ [ "--terminal-type" cfg.terminalType ] ++ [ "--terminal-type" cfg.terminalType ]
++ optionals cfg.checkOrigin [ "--check-origin" ] ++ optionals cfg.checkOrigin [ "--check-origin" ]
++ optionals cfg.writeable [ "--writable" ] # the typo is correct
++ [ "--max-clients" (toString cfg.maxClients) ] ++ [ "--max-clients" (toString cfg.maxClients) ]
++ optionals (cfg.indexFile != null) [ "--index" cfg.indexFile ] ++ optionals (cfg.indexFile != null) [ "--index" cfg.indexFile ]
++ optionals cfg.enableIPv6 [ "--ipv6" ] ++ optionals cfg.enableIPv6 [ "--ipv6" ]
@ -30,40 +37,40 @@ in
options = { options = {
services.ttyd = { services.ttyd = {
enable = mkEnableOption (lib.mdDoc "ttyd daemon"); enable = lib.mkEnableOption ("ttyd daemon");
port = mkOption { port = mkOption {
type = types.port; type = types.port;
default = 7681; default = 7681;
description = lib.mdDoc "Port to listen on (use 0 for random port)"; description = "Port to listen on (use 0 for random port)";
}; };
socket = mkOption { socket = mkOption {
type = types.nullOr types.path; type = types.nullOr types.path;
default = null; default = null;
example = "/var/run/ttyd.sock"; example = "/var/run/ttyd.sock";
description = lib.mdDoc "UNIX domain socket path to bind."; description = "UNIX domain socket path to bind.";
}; };
interface = mkOption { interface = mkOption {
type = types.nullOr types.str; type = types.nullOr types.str;
default = null; default = null;
example = "eth0"; example = "eth0";
description = lib.mdDoc "Network interface to bind."; description = "Network interface to bind.";
}; };
username = mkOption { username = mkOption {
type = types.nullOr types.str; type = types.nullOr types.str;
default = null; default = null;
description = lib.mdDoc "Username for basic authentication."; description = "Username for basic http authentication.";
}; };
passwordFile = mkOption { passwordFile = mkOption {
type = types.nullOr types.path; type = types.nullOr types.path;
default = null; default = null;
apply = value: if value == null then null else toString value; apply = value: if value == null then null else toString value;
description = lib.mdDoc '' description = ''
File containing the password to use for basic authentication. File containing the password to use for basic http authentication.
For insecurely putting the password in the globally readable store use For insecurely putting the password in the globally readable store use
`pkgs.writeText "ttydpw" "MyPassword"`. `pkgs.writeText "ttydpw" "MyPassword"`.
''; '';
@ -72,19 +79,46 @@ in
signal = mkOption { signal = mkOption {
type = types.ints.u8; type = types.ints.u8;
default = 1; default = 1;
description = lib.mdDoc "Signal to send to the command on session close."; 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 { clientOptions = mkOption {
type = types.attrsOf types.str; type = types.attrsOf types.str;
default = {}; default = {};
example = literalExpression '' example = lib.literalExpression ''
{ {
fontSize = "16"; fontSize = "16";
fontFamily = "Fira Code"; fontFamily = "Fira Code";
} }
''; '';
description = lib.mdDoc '' description = ''
Attribute set of client options for xtermjs. Attribute set of client options for xtermjs.
<https://xtermjs.org/docs/api/terminal/interfaces/iterminaloptions/> <https://xtermjs.org/docs/api/terminal/interfaces/iterminaloptions/>
''; '';
@ -93,50 +127,50 @@ in
terminalType = mkOption { terminalType = mkOption {
type = types.str; type = types.str;
default = "xterm-256color"; default = "xterm-256color";
description = lib.mdDoc "Terminal type to report."; description = "Terminal type to report.";
}; };
checkOrigin = mkOption { checkOrigin = mkOption {
type = types.bool; type = types.bool;
default = false; default = false;
description = lib.mdDoc "Whether to allow a websocket connection from a different origin."; description = "Whether to allow a websocket connection from a different origin.";
}; };
maxClients = mkOption { maxClients = mkOption {
type = types.int; type = types.int;
default = 0; default = 0;
description = lib.mdDoc "Maximum clients to support (0, no limit)"; description = "Maximum clients to support (0, no limit)";
}; };
indexFile = mkOption { indexFile = mkOption {
type = types.nullOr types.path; type = types.nullOr types.path;
default = null; default = null;
description = lib.mdDoc "Custom index.html path"; description = "Custom index.html path";
}; };
enableIPv6 = mkOption { enableIPv6 = mkOption {
type = types.bool; type = types.bool;
default = false; default = false;
description = lib.mdDoc "Whether or not to enable IPv6 support."; description = "Whether or not to enable IPv6 support.";
}; };
enableSSL = mkOption { enableSSL = mkOption {
type = types.bool; type = types.bool;
default = false; default = false;
description = lib.mdDoc "Whether or not to enable SSL (https) support."; description = "Whether or not to enable SSL (https) support.";
}; };
certFile = mkOption { certFile = mkOption {
type = types.nullOr types.path; type = types.nullOr types.path;
default = null; default = null;
description = lib.mdDoc "SSL certificate file path."; description = "SSL certificate file path.";
}; };
keyFile = mkOption { keyFile = mkOption {
type = types.nullOr types.path; type = types.nullOr types.path;
default = null; default = null;
apply = value: if value == null then null else toString value; apply = value: if value == null then null else toString value;
description = lib.mdDoc '' description = ''
SSL key file path. SSL key file path.
For insecurely putting the keyFile in the globally readable store use For insecurely putting the keyFile in the globally readable store use
`pkgs.writeText "ttydKeyFile" "SSLKEY"`. `pkgs.writeText "ttydKeyFile" "SSLKEY"`.
@ -146,25 +180,27 @@ in
caFile = mkOption { caFile = mkOption {
type = types.nullOr types.path; type = types.nullOr types.path;
default = null; default = null;
description = lib.mdDoc "SSL CA file path for client certificate verification."; description = "SSL CA file path for client certificate verification.";
}; };
logLevel = mkOption { logLevel = mkOption {
type = types.int; type = types.int;
default = 7; default = 7;
description = lib.mdDoc "Set log level."; description = "Set log level.";
}; };
}; };
}; };
###### implementation ###### implementation
config = mkIf cfg.enable { config = lib.mkIf cfg.enable {
assertions = assertions =
[ { assertion = cfg.enableSSL [ { assertion = cfg.enableSSL
-> cfg.certFile != null && cfg.keyFile != null && cfg.caFile != null; -> cfg.certFile != null && cfg.keyFile != null && cfg.caFile != null;
message = "SSL is enabled for ttyd, but no certFile, keyFile or caFile has been specified."; } 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); { assertion = ! (cfg.interface != null && cfg.socket != null);
message = "Cannot set both interface and socket for ttyd."; } message = "Cannot set both interface and socket for ttyd."; }
{ assertion = (cfg.username != null) == (cfg.passwordFile != null); { assertion = (cfg.username != null) == (cfg.passwordFile != null);
@ -177,21 +213,19 @@ in
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
serviceConfig = { serviceConfig = {
# Runs login which needs to be run as root User = cfg.user;
# login: Cannot possibly work without effective root
User = "root";
LoadCredential = lib.optionalString (cfg.passwordFile != null) "TTYD_PASSWORD_FILE:${cfg.passwordFile}"; LoadCredential = lib.optionalString (cfg.passwordFile != null) "TTYD_PASSWORD_FILE:${cfg.passwordFile}";
}; };
script = if cfg.passwordFile != null then '' script = if cfg.passwordFile != null then ''
PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/TTYD_PASSWORD_FILE") PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/TTYD_PASSWORD_FILE")
${pkgs.ttyd}/bin/ttyd ${lib.escapeShellArgs args} \ ${pkgs.ttyd}/bin/ttyd ${lib.escapeShellArgs args} \
--credential ${escapeShellArg cfg.username}:"$PASSWORD" \ --credential ${lib.escapeShellArg cfg.username}:"$PASSWORD" \
${pkgs.shadow}/bin/login ${cfg.entrypoint}
'' ''
else '' else ''
${pkgs.ttyd}/bin/ttyd ${lib.escapeShellArgs args} \ ${pkgs.ttyd}/bin/ttyd ${lib.escapeShellArgs args} \
${pkgs.shadow}/bin/login ${cfg.entrypoint}
''; '';
}; };
}; };

View file

@ -2,18 +2,28 @@ import ../make-test-python.nix ({ lib, pkgs, ... }: {
name = "ttyd"; name = "ttyd";
meta.maintainers = with lib.maintainers; [ stunkymonkey ]; meta.maintainers = with lib.maintainers; [ stunkymonkey ];
nodes.machine = { pkgs, ... }: { nodes.readonly = { pkgs, ... }: {
services.ttyd = {
enable = true;
entrypoint = [ (lib.getExe pkgs.htop) ];
writeable = false;
};
};
nodes.writeable = { pkgs, ... }: {
services.ttyd = { services.ttyd = {
enable = true; enable = true;
username = "foo"; username = "foo";
passwordFile = pkgs.writeText "password" "bar"; passwordFile = pkgs.writeText "password" "bar";
writeable = true;
}; };
}; };
testScript = '' testScript = ''
machine.wait_for_unit("ttyd.service") for machine in [readonly, writeable]:
machine.wait_for_open_port(7681) machine.wait_for_unit("ttyd.service")
response = machine.succeed("curl -vvv -u foo:bar -s -H 'Host: ttyd' http://127.0.0.1:7681/") machine.wait_for_open_port(7681)
assert '<title>ttyd - Terminal</title>' in response, "Page didn't load successfully" response = machine.succeed("curl -vvv -u foo:bar -s -H 'Host: ttyd' http://127.0.0.1:7681/")
assert '<title>ttyd - Terminal</title>' in response, "Page didn't load successfully"
''; '';
}) })