set FD_CLOEXEC
[gemrepl.git] / main.c
blob96773642d3c2c80caf04f0567eb5a3e01b9fe6d3
1 #include <getopt.h>
2 #include <poll.h>
3 #include <signal.h>
4 #include <stdbool.h>
5 #include <stdio.h>
6 #include <stdint.h>
7 #include <stdlib.h>
8 #include <string.h>
9 #include <time.h>
10 #include <sys/types.h>
11 #include <sys/wait.h>
12 #include <unistd.h>
14 #include "gemscgi.h"
16 /* If you increase this too far, you may run into file descriptor limits */
17 #define MAX_CHILDREN 256
19 typedef struct Child {
20 bool exists;
21 char owner[64];
22 uint64_t last_active;
23 pid_t pid;
24 int in;
25 int out;
27 bool nolink;
28 bool plain;
29 } Child;
31 typedef enum output_format
32 { gemtext
33 , pre
34 , unwrapped
35 , raw
36 } output_format;
38 typedef struct State {
39 const char *command;
40 char *const *args;
41 output_format format;
42 bool convert_newlines;
44 int max_children;
45 int read_timeout;
46 int pause_timeout;
48 int num_children;
49 Child children[MAX_CHILDREN];
50 } State;
52 static bool spawn(const char *command, char *const *args, Child *child, int socket)
54 int infds[2], outfds[2];
55 if (pipe(infds) == -1 || pipe(outfds) == -1) {
56 perror("pipe");
57 return false;
60 const pid_t pid = fork();
61 if (pid == -1) {
62 perror("fork");
63 return false;
66 if (pid == 0) {
67 // child
68 close(socket);
69 close(infds[1]);
70 close(outfds[0]);
71 dup2(infds[0], 0);
72 dup2(outfds[1], 1);
73 dup2(outfds[1], 2);
74 setbuffer(stdin, NULL, 0);
75 setbuffer(stdout, NULL, 0);
76 setsid();
77 char tlsenv[81];
78 snprintf(tlsenv, 81, "TLS_CLIENT_HASH=%s", child->owner);
79 putenv(tlsenv);
80 execvp(command, args);
81 exit(1);
82 } else {
83 // parent
84 close(infds[0]);
85 close(outfds[1]);
86 child->pid = pid;
87 child->in = infds[1];
88 child->out = outfds[0];
89 fnctl(child->in, F_SETFD, FD_CLOEXEC);
90 fnctl(child->out, F_SETFD, FD_CLOEXEC);
91 setbuffer(fdopen(infds[1], "w"), NULL, 0);
92 setbuffer(fdopen(outfds[0], "r"), NULL, 0);
95 return true;
98 static bool write_all(int fd, const char* buf, int n)
100 while (n > 0) {
101 int w = write(fd, buf, n);
102 if (w < 0) return false;
103 buf += w;
104 n -= w;
106 return true;
109 static void set_child_last_active(Child *child)
111 struct timespec clock_mono;
112 clock_gettime(CLOCK_MONOTONIC, &clock_mono);
113 child->last_active = clock_mono.tv_sec;
116 /* Write anything written timelily on `in` to `out`,
117 * optionally converting \n to \r\n and space-stuffing gemini-magic lines.
118 * Return -1 on read error, 0 on HUP, else 1. */
119 static int stream_text(int in, int out,
120 bool convert_newlines,
121 bool escape_pre,
122 bool escape_all,
123 int read_timeout, int pause_timeout) {
124 char buf[256];
125 struct pollfd pfd = { in, POLLIN | POLLHUP, 0 };
126 int backticks = 0;
127 char escape = 0;
129 // TODO: timeouts should really be based on cpu time of child process, not
130 // wall clock time. This is particularly important for raw output, where
131 // an unfortunately timed sleep could lead to invalid responses.
133 /* Note we set no total maximum time or output size limit; we leave it to
134 * the user to e.g. set a ulimit to handle runaway processes. */
135 poll(&pfd, 1, read_timeout);
136 while (pfd.revents & POLLIN) {
137 const int r = read(in, buf, 256 - 1);
138 if (r < 0) return false;
139 buf[r] = 0;
141 const char *b = buf;
142 while (*b) {
143 if ((escape_pre || escape_all) && backticks >= 0) {
144 if (*b == '`') {
145 escape = 0;
146 ++backticks;
147 if (backticks == 3) {
148 write(out, " ```", 4);
149 backticks = -1;
151 ++b;
152 continue;
153 } else while (--backticks >= 0) write(out, "`", 1);
156 if (escape_all && escape > 0) {
157 if (escape == '\n') {
158 if (*b == '#' || *b == '>') {
159 write(out, " ", 1);
160 } else if (*b == '=' || *b == '*') {
161 escape = *b;
162 ++b;
163 continue;
165 } else {
166 if ((escape == '=' && *b == '>')
167 || (escape == '*' && *b == ' ')) {
168 write(out, " ", 1);
170 write(out, &escape, 1);
172 escape = 0;
175 if (convert_newlines && *b == '\n') {
176 write(out, "\r\n", 2);
177 backticks = 0;
178 escape = '\n';
179 } else write(out, b, 1);
180 ++b;
183 poll(&pfd, 1, pause_timeout);
185 while (--backticks >= 0) write(out, "`", 1);
186 if (escape > 0 && escape != '\n') write(out, &escape, 1);
187 return (!(pfd.revents & POLLHUP));
190 void respond(void *object, const Request_Info *request_info, int socket)
192 State *state = (State *)object;
194 #define put(s) write_all(socket, s, strlen(s))
196 if (!request_info->tls_client_hash) {
197 put("60 Client certificate required\r\n");
198 return;
201 Child *child = NULL, *slot = NULL;
202 bool spawned = false;
204 /* Find child with this cert hash, or spawn new.
205 * For simplicity, we use a static array of children rather than
206 * allocating dynamically. This wastes a few KB of memory; you may want to
207 * rewrite this if memory is tight. We also don't bother to keep the list
208 * sorted, but just strcmp for each child. Terribly wasteful. */
209 for (int i = 0; i < state->num_children; ++i) {
210 Child *const c = &state->children[i];
211 if (c->exists) {
212 if (0 == strncmp(c->owner,
213 request_info->tls_client_hash, 64)) {
214 child = c;
215 break;
218 if (slot == NULL || (slot->exists
219 && slot->last_active > c->last_active)) {
220 slot = c;
222 } else if (slot == NULL || slot->exists) slot = c;
225 if (child == NULL) {
226 if (slot == NULL || (slot->exists && state->num_children < state->max_children)) {
227 slot = &state->children[state->num_children++];
229 child = slot;
231 if (child->exists) {
232 // TODO: would be nice to queue a regretful message for the owner
233 // of the child we're killing...
234 close(child->in);
235 close(child->out);
236 kill(child->pid, 9);
237 child->exists = false;
240 memset(child, 0, sizeof(Child));
242 if (request_info->tls_client_hash == NULL) child->owner[0] = 0;
243 else strncpy(child->owner, request_info->tls_client_hash, 64);
245 if (!spawn(state->command, state->args, child, socket)) {
246 put("40 Spawn failure.\r\n");
247 return;
250 set_child_last_active(child);
251 child->exists = true;
253 spawned = true;
256 const char *q = request_info->query_string_decoded;
257 if (*q == '!') {
258 ++q;
259 if (*q == '?') {
260 put("10\r\n");
261 return;
262 } else if (0 == strncmp(q, "help", strlen(q))) {
263 put("20 text/gemini\r\n");
264 put("An input line not beginning with '!' will be passed to the process.\r\n");
265 put("A newline will be appended unless the line ends with a trailing backslash.\r\n");
266 put("\r\n");
267 put("# gemrepl meta commands\r\n");
268 put("=> ?!help !help: This help\r\n");
269 put("=> ?!kill !kill: kill process (and start again)\r\n");
270 if (state->format != raw) {
271 put("=> ?!nolink !nolink: suppress input link\r\n");
272 put("=> ?!showlink !showlink: show input link\r\n");
273 put("=> ?!plain !plain: use text/plain for responses\r\n");
274 put("=> ?!gemtext !gemtext: use text/gemini for responses (default)\r\n");
276 put("=> ?!C !C: pass ^C (SIGINT) to process\r\n");
277 put("=> ?!? !?: Prompt for input\r\n");
278 put("=> ?!! !!: Literal '!'\r\n");
279 return;
280 } else if (0 == strncmp(q, "kill", strlen(q))) {
281 kill(-child->pid, SIGKILL);
282 q += strlen(q);
283 } else if (0 == strncmp(q, "C", strlen(q))) {
284 kill(-child->pid, SIGINT);
285 q += strlen(q);
286 } else if (0 == strncmp(q, "nolink", strlen(q))) {
287 // TODO: might be better to have this be a permanent option
288 // attached to the cert rather than the child.
289 child->nolink = true;
290 put("20 text/gemini\r\n");
291 put("Input links disabled.\r\n");
292 put("=> ?!showlink Re-enable input links\r\n");
293 return;
294 } else if (0 == strncmp(q, "showlink", strlen(q))) {
295 child->nolink = false;
296 put("20 text/gemini\r\n");
297 put("Input links enabled.\r\n");
298 put("=> ?!? Input command\r\n");
299 return;
300 } else if (0 == strncmp(q, "plain", strlen(q))) {
301 child->plain = true;
302 put("20 text/gemini\r\n");
303 put("Plaintext mode enabled.\r\n");
304 put("=> ?!gemtext Re-enable gemtext\r\n");
305 return;
306 } else if (0 == strncmp(q, "gemtext", strlen(q))) {
307 child->plain = false;
308 put("20 text/gemini\r\n");
309 put("Gemtext mode enabled.\r\n");
310 put("=> ?!? Input command\r\n");
311 return;
312 } else if (*q != '!') {
313 put("40 Unknown gemrepl meta-command (use '!!' for a literal '!')\r\n");
314 return;
318 if (state->format != raw) {
319 if (child->plain) put("20 text/plain\r\n");
320 else put("20 text/gemini\r\n");
322 if (spawned) {
323 put("[gemrepl child spawned. Input \"!help\" for meta-commands]\r\n");
326 if (!(child->nolink || child->plain)) put("=> ?!? Input command\r\n");
329 if (!spawned) kill(-child->pid, SIGCONT);
331 int qlen = strlen(q);
332 if (!spawned || qlen > 0) {
333 bool newline = true;
334 if (q[qlen-1] == '\\') {
335 --qlen;
336 newline = false;
338 signal(SIGPIPE, SIG_IGN);
339 bool succ = (write(child->in, q, qlen) == qlen
340 && (!newline || write(child->in, "\n", 1) == 1));
341 signal(SIGPIPE, SIG_DFL);
342 if (!succ) {
343 put("[gemrepl: error when writing to child]\r\n");
347 if (state->format == pre && !child->plain) put("```\r\n");
348 const int succ = stream_text(child->out, socket,
349 state->convert_newlines,
350 state->format == pre && !child->plain,
351 state->format == unwrapped && !child->plain,
352 state->read_timeout, state->pause_timeout);
353 if (state->format == pre && !child->plain) put("\r\n```\r\n");
355 if (succ < 0) put("[gemrepl: error when reading from child]\r\n");
356 else if (succ == 0) {
357 // got HUP; sleep briefly to give child a chance to exit
358 usleep(100);
361 set_child_last_active(child);
363 if (waitpid(child->pid, NULL, WNOHANG) == child->pid) {
364 put("[gemrepl: child process terminated]");
365 close(child->in);
366 close(child->out);
367 child->exists = false;
368 } else {
369 kill(-child->pid, SIGSTOP);
373 /* How long in ms to wait for child to output something */
374 #define DEF_READ_TIMEOUT 3000
376 /* How long in ms child can pause between writes before we consider it to have
377 * finished writing */
378 #define DEF_PAUSE_TIMEOUT 300
380 static void usage()
382 printf("Usage: gemrepl [OPTION]... -s PATH COMMAND [ARG]...\n");
383 printf(" -h --help This help\n");
384 printf(" -s PATH --socket=PATH Path for socket file, which will be created\n");
385 printf(" -m NUM --max-children=NUM Max concurrent children to spawn (%d)\n", MAX_CHILDREN);
386 printf(" -t MS --read-timeout=MS Time to wait for child to start writing (%d)\n", DEF_READ_TIMEOUT);
387 printf(" -T MS --pause-timeout=MS Silence period after which child is paused (%d)\n", DEF_PAUSE_TIMEOUT);
388 printf(" -n --lf-crlf Convert \\n to \\r\\n (default unless --format=raw)\n");
389 printf(" -N --no-lf-crlf Preserve newlines\n");
390 printf(" -f FMT --format=FMT Format of output of command. Possible formats:\n");
391 printf(" gemtext: text/gemini (default)\n");
392 printf(" pre: preformatted text\n");
393 printf(" unwrapped: plain text without hard wrapping\n");
394 printf(" raw: gemini protocol output, including response headers\n");
398 int main(int argc, char **argv)
400 if (argc < 2) {
401 usage();
402 exit(1);
405 State *state = malloc(sizeof(State));
406 if (state == NULL) {
407 fprintf(stderr, "Failed to allocate memory for state.");
408 exit(1);
411 state->max_children = MAX_CHILDREN;
412 state->read_timeout = DEF_READ_TIMEOUT;
413 state->pause_timeout = DEF_PAUSE_TIMEOUT;
414 state->format = gemtext;
416 int convert_newlines = -1;
418 const struct option longoptions[] =
419 { { "help", 0, NULL, 'h' }
420 , { "socket", 1, NULL, 's' }
421 , { "format", 1, NULL, 'f' }
422 , { "max-children", 1, NULL, 'm' }
423 , { "read-timeout", 1, NULL, 't' }
424 , { "pause-timeout", 1, NULL, 'T' }
425 , { "lf-crlf", 0, NULL, 'n' }
426 , { "no-lf-crlf", 0, NULL, 'N' }
427 , { 0,0,0,0 }
429 int o;
430 const char *socketname = NULL;
431 while (-1 != (o = getopt_long(argc, argv, "+hs:f:m:t:T:nN", longoptions, NULL))) {
432 switch (o) {
433 case 'h':
434 case '?':
435 usage();
436 exit((o=='?'));
437 case 's':
438 socketname = optarg;
439 break;
440 case 'f':
441 if (0 == strcmp(optarg, "gemtext")) state->format=gemtext;
442 else if (0 == strcmp(optarg, "pre")) state->format=pre;
443 else if (0 == strcmp(optarg, "unwrapped")) state->format=unwrapped;
444 else if (0 == strcmp(optarg, "raw")) state->format=raw;
445 else {
446 printf("Unknown format.\n");
447 exit(1);
449 break;
450 case 'm':
451 state->max_children = atoi(optarg);
452 if (state->max_children <= 0 || state->max_children > MAX_CHILDREN) {
453 printf("Bad value for max children.\n");
454 printf("You may need to increase MAX_CHILDREN in the source.\n");
455 exit(1);
457 break;
458 case 't':
459 state->read_timeout = atoi(optarg);
460 if (state->read_timeout < 0) {
461 printf("Bad value for read timeout.\n");
462 exit(1);
464 break;
465 case 'T':
466 state->pause_timeout = atoi(optarg);
467 if (state->pause_timeout < 0) {
468 printf("Bad value for pause timeout.\n");
469 exit(1);
471 break;
472 case 'n':
473 convert_newlines = 1;
474 break;
475 case 'N':
476 convert_newlines = 0;
477 break;
481 if (argv[optind] == NULL || socketname == NULL) {
482 usage();
483 exit(1);
486 state->command = argv[optind];
487 state->args = &argv[optind];
488 state->convert_newlines = convert_newlines < 0 ? state->format != raw : convert_newlines;
490 runSCGI(socketname, respond, state);