Merge pull request #285314 from pbsds/ttyd-1706718068
nixos/ttyd: add `entrypoint` and `writable` option
This commit is contained in:
commit
bf7c95ce73
2 changed files with 78 additions and 34 deletions
|
@ -1,11 +1,17 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
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 ]
|
||||
|
@ -14,6 +20,7 @@ let
|
|||
++ (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" ]
|
||||
|
@ -30,40 +37,40 @@ in
|
|||
|
||||
options = {
|
||||
services.ttyd = {
|
||||
enable = mkEnableOption (lib.mdDoc "ttyd daemon");
|
||||
enable = lib.mkEnableOption ("ttyd daemon");
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
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 {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
example = "/var/run/ttyd.sock";
|
||||
description = lib.mdDoc "UNIX domain socket path to bind.";
|
||||
description = "UNIX domain socket path to bind.";
|
||||
};
|
||||
|
||||
interface = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "eth0";
|
||||
description = lib.mdDoc "Network interface to bind.";
|
||||
description = "Network interface to bind.";
|
||||
};
|
||||
|
||||
username = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = lib.mdDoc "Username for basic authentication.";
|
||||
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 = lib.mdDoc ''
|
||||
File containing the password to use for basic authentication.
|
||||
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"`.
|
||||
'';
|
||||
|
@ -72,19 +79,46 @@ in
|
|||
signal = mkOption {
|
||||
type = types.ints.u8;
|
||||
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 {
|
||||
type = types.attrsOf types.str;
|
||||
default = {};
|
||||
example = literalExpression ''
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
fontSize = "16";
|
||||
fontFamily = "Fira Code";
|
||||
}
|
||||
'';
|
||||
description = lib.mdDoc ''
|
||||
description = ''
|
||||
Attribute set of client options for xtermjs.
|
||||
<https://xtermjs.org/docs/api/terminal/interfaces/iterminaloptions/>
|
||||
'';
|
||||
|
@ -93,50 +127,50 @@ in
|
|||
terminalType = mkOption {
|
||||
type = types.str;
|
||||
default = "xterm-256color";
|
||||
description = lib.mdDoc "Terminal type to report.";
|
||||
description = "Terminal type to report.";
|
||||
};
|
||||
|
||||
checkOrigin = mkOption {
|
||||
type = types.bool;
|
||||
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 {
|
||||
type = types.int;
|
||||
default = 0;
|
||||
description = lib.mdDoc "Maximum clients to support (0, no limit)";
|
||||
description = "Maximum clients to support (0, no limit)";
|
||||
};
|
||||
|
||||
indexFile = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
description = lib.mdDoc "Custom index.html path";
|
||||
description = "Custom index.html path";
|
||||
};
|
||||
|
||||
enableIPv6 = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = lib.mdDoc "Whether or not to enable IPv6 support.";
|
||||
description = "Whether or not to enable IPv6 support.";
|
||||
};
|
||||
|
||||
enableSSL = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = lib.mdDoc "Whether or not to enable SSL (https) support.";
|
||||
description = "Whether or not to enable SSL (https) support.";
|
||||
};
|
||||
|
||||
certFile = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
description = lib.mdDoc "SSL certificate file path.";
|
||||
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 = lib.mdDoc ''
|
||||
description = ''
|
||||
SSL key file path.
|
||||
For insecurely putting the keyFile in the globally readable store use
|
||||
`pkgs.writeText "ttydKeyFile" "SSLKEY"`.
|
||||
|
@ -146,25 +180,27 @@ in
|
|||
caFile = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
description = lib.mdDoc "SSL CA file path for client certificate verification.";
|
||||
description = "SSL CA file path for client certificate verification.";
|
||||
};
|
||||
|
||||
logLevel = mkOption {
|
||||
type = types.int;
|
||||
default = 7;
|
||||
description = lib.mdDoc "Set log level.";
|
||||
description = "Set log level.";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
###### implementation
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
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);
|
||||
|
@ -177,21 +213,19 @@ in
|
|||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
serviceConfig = {
|
||||
# Runs login which needs to be run as root
|
||||
# login: Cannot possibly work without effective root
|
||||
User = "root";
|
||||
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 ${escapeShellArg cfg.username}:"$PASSWORD" \
|
||||
${pkgs.shadow}/bin/login
|
||||
--credential ${lib.escapeShellArg cfg.username}:"$PASSWORD" \
|
||||
${cfg.entrypoint}
|
||||
''
|
||||
else ''
|
||||
${pkgs.ttyd}/bin/ttyd ${lib.escapeShellArgs args} \
|
||||
${pkgs.shadow}/bin/login
|
||||
${cfg.entrypoint}
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
|
|
@ -2,18 +2,28 @@ import ../make-test-python.nix ({ lib, pkgs, ... }: {
|
|||
name = "ttyd";
|
||||
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 = {
|
||||
enable = true;
|
||||
username = "foo";
|
||||
passwordFile = pkgs.writeText "password" "bar";
|
||||
writeable = true;
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
machine.wait_for_unit("ttyd.service")
|
||||
machine.wait_for_open_port(7681)
|
||||
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"
|
||||
for machine in [readonly, writeable]:
|
||||
machine.wait_for_unit("ttyd.service")
|
||||
machine.wait_for_open_port(7681)
|
||||
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"
|
||||
'';
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue