about summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
authorPeder Bergebakken Sundt <pbsds@hotmail.com>2024-02-13 19:41:11 +0100
committerGitHub <noreply@github.com>2024-02-13 19:41:11 +0100
commitbf7c95ce73d652d045a441403c06b4fbceaba179 (patch)
tree61a86ed89c6da447c149f2fddc1434bc7a299cdf /nixos
parent9858b3962a5944deb26b3e3907f270fceecd2b19 (diff)
parenta8880f1647e6b8e83f1f9909bea17d4c0dbe8428 (diff)
Merge pull request #285314 from pbsds/ttyd-1706718068
nixos/ttyd: add `entrypoint` and `writable` option
Diffstat (limited to 'nixos')
-rw-r--r--nixos/modules/services/web-servers/ttyd.nix92
-rw-r--r--nixos/tests/web-servers/ttyd.nix20
2 files changed, 78 insertions, 34 deletions
diff --git a/nixos/modules/services/web-servers/ttyd.nix b/nixos/modules/services/web-servers/ttyd.nix
index e545869ca4320..14361df2bb663 100644
--- a/nixos/modules/services/web-servers/ttyd.nix
+++ b/nixos/modules/services/web-servers/ttyd.nix
@@ -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}
       '';
     };
   };
diff --git a/nixos/tests/web-servers/ttyd.nix b/nixos/tests/web-servers/ttyd.nix
index d161673684b31..b79a2032ec75a 100644
--- a/nixos/tests/web-servers/ttyd.nix
+++ b/nixos/tests/web-servers/ttyd.nix
@@ -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"
   '';
 })