1 { config, lib, pkgs, ... }:
5 cfg = config.services.ttyd;
13 # Command line arguments for the ttyd daemon
14 args = [ "--port" (toString cfg.port) ]
15 ++ optionals (cfg.socket != null) [ "--interface" cfg.socket ]
16 ++ optionals (cfg.interface != null) [ "--interface" cfg.interface ]
17 ++ [ "--signal" (toString cfg.signal) ]
18 ++ (lib.concatLists (lib.mapAttrsToList (_k: _v: [ "--client-option" "${_k}=${_v}" ]) cfg.clientOptions))
19 ++ [ "--terminal-type" cfg.terminalType ]
20 ++ optionals cfg.checkOrigin [ "--check-origin" ]
21 ++ optionals cfg.writeable [ "--writable" ] # the typo is correct
22 ++ [ "--max-clients" (toString cfg.maxClients) ]
23 ++ optionals (cfg.indexFile != null) [ "--index" cfg.indexFile ]
24 ++ optionals cfg.enableIPv6 [ "--ipv6" ]
25 ++ optionals cfg.enableSSL [ "--ssl"
26 "--ssl-cert" cfg.certFile
27 "--ssl-key" cfg.keyFile ]
28 ++ optionals ( cfg.enableSSL && cfg.caFile != null ) [ "--ssl-ca" cfg.caFile ]
29 ++ [ "--debug" (toString cfg.logLevel) ];
39 enable = lib.mkEnableOption ("ttyd daemon");
44 description = "Port to listen on (use 0 for random port)";
48 type = types.nullOr types.path;
50 example = "/var/run/ttyd.sock";
51 description = "UNIX domain socket path to bind.";
54 interface = mkOption {
55 type = types.nullOr types.str;
58 description = "Network interface to bind.";
62 type = types.nullOr types.str;
64 description = "Username for basic http authentication.";
67 passwordFile = mkOption {
68 type = types.nullOr types.path;
70 apply = value: if value == null then null else toString value;
72 File containing the password to use for basic http authentication.
73 For insecurely putting the password in the globally readable store use
74 `pkgs.writeText "ttydpw" "MyPassword"`.
81 description = "Signal to send to the command on session close.";
84 entrypoint = mkOption {
85 type = types.listOf types.str;
86 default = [ "${pkgs.shadow}/bin/login" ];
87 defaultText = lib.literalExpression ''
88 [ "''${pkgs.shadow}/bin/login" ]
90 example = lib.literalExpression ''
91 [ (lib.getExe pkgs.htop) ]
93 description = "Which command ttyd runs.";
94 apply = lib.escapeShellArgs;
99 # `login` needs to be run as root
101 description = "Which unix user ttyd should run as.";
104 writeable = mkOption {
105 type = types.nullOr types.bool;
106 default = null; # null causes an eval error, forcing the user to consider attack surface
108 description = "Allow clients to write to the TTY.";
111 clientOptions = mkOption {
112 type = types.attrsOf types.str;
114 example = lib.literalExpression ''
117 fontFamily = "Fira Code";
121 Attribute set of client options for xtermjs.
122 <https://xtermjs.org/docs/api/terminal/interfaces/iterminaloptions/>
126 terminalType = mkOption {
128 default = "xterm-256color";
129 description = "Terminal type to report.";
132 checkOrigin = mkOption {
135 description = "Whether to allow a websocket connection from a different origin.";
138 maxClients = mkOption {
141 description = "Maximum clients to support (0, no limit)";
144 indexFile = mkOption {
145 type = types.nullOr types.path;
147 description = "Custom index.html path";
150 enableIPv6 = mkOption {
153 description = "Whether or not to enable IPv6 support.";
156 enableSSL = mkOption {
159 description = "Whether or not to enable SSL (https) support.";
162 certFile = mkOption {
163 type = types.nullOr types.path;
165 description = "SSL certificate file path.";
169 type = types.nullOr types.path;
171 apply = value: if value == null then null else toString value;
174 For insecurely putting the keyFile in the globally readable store use
175 `pkgs.writeText "ttydKeyFile" "SSLKEY"`.
180 type = types.nullOr types.path;
182 description = "SSL CA file path for client certificate verification.";
185 logLevel = mkOption {
188 description = "Set log level.";
193 ###### implementation
195 config = lib.mkIf cfg.enable {
198 [ { assertion = cfg.enableSSL
199 -> cfg.certFile != null && cfg.keyFile != null;
200 message = "SSL is enabled for ttyd, but no certFile or keyFile has been specified."; }
201 { assertion = cfg.writeable != null;
202 message = "services.ttyd.writeable must be set"; }
203 { assertion = ! (cfg.interface != null && cfg.socket != null);
204 message = "Cannot set both interface and socket for ttyd."; }
205 { assertion = (cfg.username != null) == (cfg.passwordFile != null);
206 message = "Need to set both username and passwordFile for ttyd"; }
209 systemd.services.ttyd = {
210 description = "ttyd Web Server Daemon";
212 wantedBy = [ "multi-user.target" ];
216 LoadCredential = lib.optionalString (cfg.passwordFile != null) "TTYD_PASSWORD_FILE:${cfg.passwordFile}";
219 script = if cfg.passwordFile != null then ''
220 PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/TTYD_PASSWORD_FILE")
221 ${pkgs.ttyd}/bin/ttyd ${lib.escapeShellArgs args} \
222 --credential ${lib.escapeShellArg cfg.username}:"$PASSWORD" \
226 ${pkgs.ttyd}/bin/ttyd ${lib.escapeShellArgs args} \