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}