1{ config, lib, pkgs, ... }: 2 3with lib; 4 5let 6 7 cfg = config.services.ttyd; 8 9 # Command line arguments for the ttyd daemon 10 args = [ "--port" (toString cfg.port) ] 11 ++ optionals (cfg.socket != null) [ "--interface" cfg.socket ] 12 ++ optionals (cfg.interface != null) [ "--interface" cfg.interface ] 13 ++ [ "--signal" (toString cfg.signal) ] 14 ++ (concatLists (mapAttrsToList (_k: _v: [ "--client-option" "${_k}=${_v}" ]) cfg.clientOptions)) 15 ++ [ "--terminal-type" cfg.terminalType ] 16 ++ optionals cfg.checkOrigin [ "--check-origin" ] 17 ++ [ "--max-clients" (toString cfg.maxClients) ] 18 ++ optionals (cfg.indexFile != null) [ "--index" cfg.indexFile ] 19 ++ optionals cfg.enableIPv6 [ "--ipv6" ] 20 ++ optionals cfg.enableSSL [ "--ssl-cert" cfg.certFile 21 "--ssl-key" cfg.keyFile 22 "--ssl-ca" cfg.caFile ] 23 ++ [ "--debug" (toString cfg.logLevel) ]; 24 25in 26 27{ 28 29 ###### interface 30 31 options = { 32 services.ttyd = { 33 enable = mkEnableOption (lib.mdDoc "ttyd daemon"); 34 35 port = mkOption { 36 type = types.port; 37 default = 7681; 38 description = lib.mdDoc "Port to listen on (use 0 for random port)"; 39 }; 40 41 socket = mkOption { 42 type = types.nullOr types.path; 43 default = null; 44 example = "/var/run/ttyd.sock"; 45 description = lib.mdDoc "UNIX domain socket path to bind."; 46 }; 47 48 interface = mkOption { 49 type = types.nullOr types.str; 50 default = null; 51 example = "eth0"; 52 description = lib.mdDoc "Network interface to bind."; 53 }; 54 55 username = mkOption { 56 type = types.nullOr types.str; 57 default = null; 58 description = lib.mdDoc "Username for basic authentication."; 59 }; 60 61 passwordFile = mkOption { 62 type = types.nullOr types.path; 63 default = null; 64 apply = value: if value == null then null else toString value; 65 description = lib.mdDoc '' 66 File containing the password to use for basic authentication. 67 For insecurely putting the password in the globally readable store use 68 `pkgs.writeText "ttydpw" "MyPassword"`. 69 ''; 70 }; 71 72 signal = mkOption { 73 type = types.ints.u8; 74 default = 1; 75 description = lib.mdDoc "Signal to send to the command on session close."; 76 }; 77 78 clientOptions = mkOption { 79 type = types.attrsOf types.str; 80 default = {}; 81 example = literalExpression '' 82 { 83 fontSize = "16"; 84 fontFamily = "Fira Code"; 85 } 86 ''; 87 description = lib.mdDoc '' 88 Attribute set of client options for xtermjs. 89 <https://xtermjs.org/docs/api/terminal/interfaces/iterminaloptions/> 90 ''; 91 }; 92 93 terminalType = mkOption { 94 type = types.str; 95 default = "xterm-256color"; 96 description = lib.mdDoc "Terminal type to report."; 97 }; 98 99 checkOrigin = mkOption { 100 type = types.bool; 101 default = false; 102 description = lib.mdDoc "Whether to allow a websocket connection from a different origin."; 103 }; 104 105 maxClients = mkOption { 106 type = types.int; 107 default = 0; 108 description = lib.mdDoc "Maximum clients to support (0, no limit)"; 109 }; 110 111 indexFile = mkOption { 112 type = types.nullOr types.path; 113 default = null; 114 description = lib.mdDoc "Custom index.html path"; 115 }; 116 117 enableIPv6 = mkOption { 118 type = types.bool; 119 default = false; 120 description = lib.mdDoc "Whether or not to enable IPv6 support."; 121 }; 122 123 enableSSL = mkOption { 124 type = types.bool; 125 default = false; 126 description = lib.mdDoc "Whether or not to enable SSL (https) support."; 127 }; 128 129 certFile = mkOption { 130 type = types.nullOr types.path; 131 default = null; 132 description = lib.mdDoc "SSL certificate file path."; 133 }; 134 135 keyFile = mkOption { 136 type = types.nullOr types.path; 137 default = null; 138 apply = value: if value == null then null else toString value; 139 description = lib.mdDoc '' 140 SSL key file path. 141 For insecurely putting the keyFile in the globally readable store use 142 `pkgs.writeText "ttydKeyFile" "SSLKEY"`. 143 ''; 144 }; 145 146 caFile = mkOption { 147 type = types.nullOr types.path; 148 default = null; 149 description = lib.mdDoc "SSL CA file path for client certificate verification."; 150 }; 151 152 logLevel = mkOption { 153 type = types.int; 154 default = 7; 155 description = lib.mdDoc "Set log level."; 156 }; 157 }; 158 }; 159 160 ###### implementation 161 162 config = mkIf cfg.enable { 163 164 assertions = 165 [ { assertion = cfg.enableSSL 166 -> cfg.certFile != null && cfg.keyFile != null && cfg.caFile != null; 167 message = "SSL is enabled for ttyd, but no certFile, keyFile or caFile has been specified."; } 168 { assertion = ! (cfg.interface != null && cfg.socket != null); 169 message = "Cannot set both interface and socket for ttyd."; } 170 { assertion = (cfg.username != null) == (cfg.passwordFile != null); 171 message = "Need to set both username and passwordFile for ttyd"; } 172 ]; 173 174 systemd.services.ttyd = { 175 description = "ttyd Web Server Daemon"; 176 177 wantedBy = [ "multi-user.target" ]; 178 179 serviceConfig = { 180 # Runs login which needs to be run as root 181 # login: Cannot possibly work without effective root 182 User = "root"; 183 }; 184 185 script = if cfg.passwordFile != null then '' 186 PASSWORD=$(cat ${escapeShellArg cfg.passwordFile}) 187 ${pkgs.ttyd}/bin/ttyd ${lib.escapeShellArgs args} \ 188 --credential ${escapeShellArg cfg.username}:"$PASSWORD" \ 189 ${pkgs.shadow}/bin/login 190 '' 191 else '' 192 ${pkgs.ttyd}/bin/ttyd ${lib.escapeShellArgs args} \ 193 ${pkgs.shadow}/bin/login 194 ''; 195 }; 196 }; 197}