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}