at master 7.2 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7 8let 9 10 cfg = config.services.ttyd; 11 12 inherit (lib) 13 optionals 14 types 15 mkOption 16 ; 17 18 # Command line arguments for the ttyd daemon 19 args = [ 20 "--port" 21 (toString cfg.port) 22 ] 23 ++ optionals (cfg.socket != null) [ 24 "--interface" 25 cfg.socket 26 ] 27 ++ optionals (cfg.interface != null) [ 28 "--interface" 29 cfg.interface 30 ] 31 ++ [ 32 "--signal" 33 (toString cfg.signal) 34 ] 35 ++ (lib.concatLists ( 36 lib.mapAttrsToList (_k: _v: [ 37 "--client-option" 38 "${_k}=${_v}" 39 ]) cfg.clientOptions 40 )) 41 ++ [ 42 "--terminal-type" 43 cfg.terminalType 44 ] 45 ++ optionals cfg.checkOrigin [ "--check-origin" ] 46 ++ optionals cfg.writeable [ "--writable" ] # the typo is correct 47 ++ [ 48 "--max-clients" 49 (toString cfg.maxClients) 50 ] 51 ++ optionals (cfg.indexFile != null) [ 52 "--index" 53 cfg.indexFile 54 ] 55 ++ optionals cfg.enableIPv6 [ "--ipv6" ] 56 ++ optionals cfg.enableSSL [ 57 "--ssl" 58 "--ssl-cert" 59 cfg.certFile 60 "--ssl-key" 61 cfg.keyFile 62 ] 63 ++ optionals (cfg.enableSSL && cfg.caFile != null) [ 64 "--ssl-ca" 65 cfg.caFile 66 ] 67 ++ [ 68 "--debug" 69 (toString cfg.logLevel) 70 ]; 71 72in 73 74{ 75 76 ###### interface 77 78 options = { 79 services.ttyd = { 80 enable = lib.mkEnableOption ("ttyd daemon"); 81 82 port = mkOption { 83 type = types.port; 84 default = 7681; 85 description = "Port to listen on (use 0 for random port)"; 86 }; 87 88 socket = mkOption { 89 type = types.nullOr types.path; 90 default = null; 91 example = "/var/run/ttyd.sock"; 92 description = "UNIX domain socket path to bind."; 93 }; 94 95 interface = mkOption { 96 type = types.nullOr types.str; 97 default = null; 98 example = "eth0"; 99 description = "Network interface to bind."; 100 }; 101 102 username = mkOption { 103 type = types.nullOr types.str; 104 default = null; 105 description = "Username for basic http authentication."; 106 }; 107 108 passwordFile = mkOption { 109 type = types.nullOr types.path; 110 default = null; 111 apply = value: if value == null then null else toString value; 112 description = '' 113 File containing the password to use for basic http authentication. 114 For insecurely putting the password in the globally readable store use 115 `pkgs.writeText "ttydpw" "MyPassword"`. 116 ''; 117 }; 118 119 signal = mkOption { 120 type = types.ints.u8; 121 default = 1; 122 description = "Signal to send to the command on session close."; 123 }; 124 125 entrypoint = mkOption { 126 type = types.listOf types.str; 127 default = [ "${pkgs.shadow}/bin/login" ]; 128 defaultText = lib.literalExpression '' 129 [ "''${pkgs.shadow}/bin/login" ] 130 ''; 131 example = lib.literalExpression '' 132 [ (lib.getExe pkgs.htop) ] 133 ''; 134 description = "Which command ttyd runs."; 135 apply = lib.escapeShellArgs; 136 }; 137 138 user = mkOption { 139 type = types.str; 140 # `login` needs to be run as root 141 default = "root"; 142 description = "Which unix user ttyd should run as."; 143 }; 144 145 writeable = mkOption { 146 type = types.nullOr types.bool; 147 default = null; # null causes an eval error, forcing the user to consider attack surface 148 example = true; 149 description = "Allow clients to write to the TTY."; 150 }; 151 152 clientOptions = mkOption { 153 type = types.attrsOf types.str; 154 default = { }; 155 example = lib.literalExpression '' 156 { 157 fontSize = "16"; 158 fontFamily = "Fira Code"; 159 } 160 ''; 161 description = '' 162 Attribute set of client options for xtermjs. 163 <https://xtermjs.org/docs/api/terminal/interfaces/iterminaloptions/> 164 ''; 165 }; 166 167 terminalType = mkOption { 168 type = types.str; 169 default = "xterm-256color"; 170 description = "Terminal type to report."; 171 }; 172 173 checkOrigin = mkOption { 174 type = types.bool; 175 default = false; 176 description = "Whether to allow a websocket connection from a different origin."; 177 }; 178 179 maxClients = mkOption { 180 type = types.int; 181 default = 0; 182 description = "Maximum clients to support (0, no limit)"; 183 }; 184 185 indexFile = mkOption { 186 type = types.nullOr types.path; 187 default = null; 188 description = "Custom index.html path"; 189 }; 190 191 enableIPv6 = mkOption { 192 type = types.bool; 193 default = false; 194 description = "Whether or not to enable IPv6 support."; 195 }; 196 197 enableSSL = mkOption { 198 type = types.bool; 199 default = false; 200 description = "Whether or not to enable SSL (https) support."; 201 }; 202 203 certFile = mkOption { 204 type = types.nullOr types.path; 205 default = null; 206 description = "SSL certificate file path."; 207 }; 208 209 keyFile = mkOption { 210 type = types.nullOr types.path; 211 default = null; 212 apply = value: if value == null then null else toString value; 213 description = '' 214 SSL key file path. 215 For insecurely putting the keyFile in the globally readable store use 216 `pkgs.writeText "ttydKeyFile" "SSLKEY"`. 217 ''; 218 }; 219 220 caFile = mkOption { 221 type = types.nullOr types.path; 222 default = null; 223 description = "SSL CA file path for client certificate verification."; 224 }; 225 226 logLevel = mkOption { 227 type = types.int; 228 default = 7; 229 description = "Set log level."; 230 }; 231 }; 232 }; 233 234 ###### implementation 235 236 config = lib.mkIf cfg.enable { 237 238 assertions = [ 239 { 240 assertion = cfg.enableSSL -> cfg.certFile != null && cfg.keyFile != null; 241 message = "SSL is enabled for ttyd, but no certFile or keyFile has been specified."; 242 } 243 { 244 assertion = cfg.writeable != null; 245 message = "services.ttyd.writeable must be set"; 246 } 247 { 248 assertion = !(cfg.interface != null && cfg.socket != null); 249 message = "Cannot set both interface and socket for ttyd."; 250 } 251 { 252 assertion = (cfg.username != null) == (cfg.passwordFile != null); 253 message = "Need to set both username and passwordFile for ttyd"; 254 } 255 ]; 256 257 systemd.services.ttyd = { 258 description = "ttyd Web Server Daemon"; 259 260 wantedBy = [ "multi-user.target" ]; 261 262 serviceConfig = { 263 User = cfg.user; 264 LoadCredential = lib.optionalString ( 265 cfg.passwordFile != null 266 ) "TTYD_PASSWORD_FILE:${cfg.passwordFile}"; 267 }; 268 269 script = 270 if cfg.passwordFile != null then 271 '' 272 PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/TTYD_PASSWORD_FILE") 273 ${pkgs.ttyd}/bin/ttyd ${lib.escapeShellArgs args} \ 274 --credential ${lib.escapeShellArg cfg.username}:"$PASSWORD" \ 275 ${cfg.entrypoint} 276 '' 277 else 278 '' 279 ${pkgs.ttyd}/bin/ttyd ${lib.escapeShellArgs args} \ 280 ${cfg.entrypoint} 281 ''; 282 }; 283 }; 284}