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