improve use of flag
[gemrepl.git] / main.c
blobdca35aac733573a4504c330862b12db89ee3e9c1
1 #include <fcntl.h>
2 #include <getopt.h>
3 #include <poll.h>
4 #include <signal.h>
5 #include <stdbool.h>
6 #include <stdio.h>
7 #include <stdint.h>
8 #include <stdlib.h>
9 #include <string.h>
10 #include <time.h>
11 #include <sys/types.h>
12 #include <sys/wait.h>
13 #include <unistd.h>
15 #include "gemscgi.h"
17 /* If you increase this too far, you may run into file descriptor limits */
18 #define MAX_CHILDREN 256
20 #define SESSION_ID_LEN 8
22 typedef struct Child {
23 bool exists;
24 bool newborn;
25 char sess_id[SESSION_ID_LEN];
26 char owner[64];
27 uint64_t last_active;
28 pid_t pid;
29 int in;
30 int out;
31 int flag;
32 bool reading;
34 bool nolink;
35 bool plain;
36 } Child;
38 typedef enum output_format
39 { gemtext
40 , pre
41 , unwrapped
42 , raw
43 } output_format;
45 typedef struct State {
46 const char *command;
47 char *const *args;
48 output_format format;
49 bool convert_newlines;
51 int max_children;
52 int read_timeout;
53 int pause_timeout;
55 int num_children;
56 Child children[MAX_CHILDREN];
57 } State;
59 static bool spawn(const char *command, char *const *args, const char *query,
60 Child *child, int socket)
62 int infds[2], outfds[2], flagfds[2];
63 if (pipe(infds) == -1 || pipe(outfds) == -1 || pipe(flagfds) == -1) {
64 perror("pipe");
65 return false;
68 const pid_t pid = fork();
69 if (pid == -1) {
70 perror("fork");
71 return false;
74 if (pid == 0) {
75 // child
76 close(socket);
77 close(infds[1]);
78 close(outfds[0]);
79 close(flagfds[0]);
80 dup2(infds[0], 0);
81 dup2(outfds[1], 1);
82 dup2(outfds[1], 2);
83 dup2(flagfds[1], 3);
84 setbuffer(stdin, NULL, 0);
85 setbuffer(stdout, NULL, 0);
86 setbuffer(fdopen(3, "w"), NULL, 0);
87 setsid();
89 char tlsenv[81];
90 snprintf(tlsenv, 81, "TLS_CLIENT_HASH=%s", child->owner);
91 putenv(tlsenv);
93 if (query != NULL) {
94 char qenv[1035];
95 snprintf(qenv, 1035, "INIT_QUERY=%s", query);
96 putenv(qenv);
99 execvp(command, args);
100 exit(1);
101 } else {
102 // parent
103 close(infds[0]);
104 close(outfds[1]);
105 close(flagfds[1]);
106 child->pid = pid;
107 child->in = infds[1];
108 child->out = outfds[0];
109 child->flag = flagfds[0];
110 fcntl(child->in, F_SETFD, FD_CLOEXEC);
111 fcntl(child->out, F_SETFD, FD_CLOEXEC);
112 fcntl(child->flag, F_SETFD, FD_CLOEXEC);
113 setbuffer(fdopen(infds[1], "w"), NULL, 0);
114 setbuffer(fdopen(outfds[0], "r"), NULL, 0);
115 setbuffer(fdopen(flagfds[0], "r"), NULL, 0);
118 return true;
121 static bool write_all(int fd, const char* buf, int n)
123 while (n > 0) {
124 int w = write(fd, buf, n);
125 if (w < 0) return false;
126 buf += w;
127 n -= w;
129 return true;
132 static void set_child_last_active(Child *child)
134 struct timespec clock_mono;
135 clock_gettime(CLOCK_MONOTONIC, &clock_mono);
136 child->last_active = clock_mono.tv_sec;
139 /* Write anything written timelily on `in` to `out`,
140 * optionally converting \n to \r\n and space-stuffing gemini-magic lines.
141 * Streaming will cease if there is nothing to read on `in` for `read_timeout`
142 * ms, or after `pause_timeout` ms if something has been read, or after '<' is
143 * read from `flag` without a subsequent '>'.
144 * Return -1 on read error, 0 on HUP, else 1. */
145 static int stream_text(int in, int flag, int out,
146 bool convert_newlines,
147 bool escape_pre,
148 bool escape_all,
149 bool *child_reading,
150 int read_timeout, int pause_timeout) {
151 char buf[256];
152 struct pollfd pfd[2] = { { in, POLLIN | POLLHUP, 0 }, {flag, POLLIN, 0 } };
153 int backticks = 0;
154 char escape = 0;
155 bool read_something = false;
157 /* Note we set no total maximum time or output size limit; we leave it to
158 * the user to e.g. set a ulimit to handle runaway processes. */
159 while (true) {
160 poll(pfd, 2, *child_reading ? 20 :
161 read_something ? read_timeout : pause_timeout);
163 if (pfd[0].revents & POLLIN) {
164 read_something = true;
165 const int r = read(in, buf, 256 - 1);
166 if (r < 0) return false;
167 buf[r] = 0;
169 const char *b = buf;
170 while (*b) {
171 if ((escape_pre || escape_all) && backticks >= 0) {
172 if (*b == '`') {
173 escape = 0;
174 ++backticks;
175 if (backticks == 3) {
176 write(out, " ```", 4);
177 backticks = -1;
179 ++b;
180 continue;
181 } else while (--backticks >= 0) write(out, "`", 1);
184 if (escape_all && escape > 0) {
185 if (escape == '\n') {
186 if (*b == '#' || *b == '>') {
187 write(out, " ", 1);
188 } else if (*b == '=' || *b == '*') {
189 escape = *b;
190 ++b;
191 continue;
193 } else {
194 if ((escape == '=' && *b == '>')
195 || (escape == '*' && *b == ' ')) {
196 write(out, " ", 1);
198 write(out, &escape, 1);
200 escape = 0;
203 if (convert_newlines && *b == '\n') {
204 write(out, "\r\n", 2);
205 backticks = 0;
206 escape = '\n';
207 } else write(out, b, 1);
208 ++b;
210 } else if (pfd[1].revents & POLLIN) {
211 const int r = read(flag, buf, 256);
212 for (int i = 0; i < r; ++i ) {
213 if (buf[i] == '<') *child_reading = true;
214 if (buf[i] == '>') *child_reading = false;
216 } else break;
218 while (--backticks >= 0) write(out, "`", 1);
219 if (escape > 0 && escape != '\n') write(out, &escape, 1);
220 return (!(pfd[0].revents & POLLHUP));
223 void respond(void *object, const Request_Info *request_info, int socket)
225 State *state = (State *)object;
227 #define put(s) write_all(socket, s, strlen(s))
228 #define putn(s,n) write_all(socket, s, n)
230 if (request_info->tls_client_hash == NULL) {
231 put("60 Client certificate required\r\n");
232 return;
235 const char *q = request_info->query_string_decoded;
237 if (request_info->path_info == NULL || strlen(request_info->path_info) <= 1) {
238 Child *slot = NULL;
239 for (int i = 0; i < state->num_children; ++i) {
240 Child *const c = &state->children[i];
241 if (c->exists) {
242 if (slot == NULL || (slot->exists
243 && slot->last_active > c->last_active)) {
244 slot = c;
246 } else if (slot == NULL || slot->exists) slot = c;
249 if (slot == NULL || (slot->exists && state->num_children < state->max_children)) {
250 slot = &state->children[state->num_children++];
252 Child *const child = slot;
254 if (child->exists) {
255 // TODO: would be nice to queue a regretful message for the owner
256 // of the child we're killing...
257 close(child->in);
258 close(child->out);
259 close(child->flag);
260 kill(child->pid, 9);
261 child->exists = false;
264 memset(child, 0, sizeof(Child));
266 strncpy(child->owner, request_info->tls_client_hash, 64);
267 for (int i = 0; i < SESSION_ID_LEN; ++i) {
268 child->sess_id[i] = 'A' + random()%26 + (random()%2 ? ('a'-'A') : 0);
271 if (!spawn(state->command, state->args, q, child, socket)) {
272 put("40 Spawn failure.\r\n");
273 return;
276 child->exists = true;
277 child->newborn = true;
278 set_child_last_active(child);
280 put("30 ");
281 put(request_info->script_path);
282 put("/");
283 putn(child->sess_id, SESSION_ID_LEN);
284 put("\r\n");
285 return;
286 } else if (0 == strncmp(request_info->path_info, "/list", strlen(request_info->path_info))) {
287 put("20 text/gemini\r\n");
288 bool found = false;
289 for (int i = 0; i < state->num_children; ++i) {
290 Child *const c = &state->children[i];
291 if (c->exists &&
292 0 == strncmp(c->owner, request_info->tls_client_hash, 64)) {
293 if (!found) {
294 found = true;
295 put("20 text/gemini\r\n");
297 put("=> ");
298 put(request_info->script_path);
299 put("/");
300 putn(c->sess_id, SESSION_ID_LEN);
301 put(" Resume session\r\n");
304 if (!found) put("No sessions found.\r\n");
305 return;
308 if (strlen(request_info->path_info) != 1+SESSION_ID_LEN) {
309 put("51 Bad session id.\r\n");
310 return;
313 // drop initial '/'
314 const char *sess_id = request_info->path_info + 1;
316 /* Find child with this sess_id.
317 * For simplicity, we use a static array of children rather than
318 * allocating dynamically, and don't sort. This could be optimised. */
319 Child *child = NULL;
320 for (int i = 0; i < state->num_children; ++i) {
321 Child *const c = &state->children[i];
322 if (c->exists &&
323 0 == strncmp(c->sess_id, sess_id, SESSION_ID_LEN)) {
324 child = c;
325 break;
329 if (child == NULL) {
330 put("20 text/gemini\r\nSession not found.\r\n=> ");
331 put(request_info->script_path);
332 put(" Start new session\r\n");
333 return;
336 if (0 != strncmp(child->owner, request_info->tls_client_hash, 64)) {
337 put("61 Wrong certificate for session.\r\n");
338 return;
341 if (*q == '!') {
342 ++q;
343 if (*q == '?') {
344 put("10\r\n");
345 return;
346 } else if (0 == strncmp(q, "help", strlen(q))) {
347 put("20 text/gemini\r\n");
348 put("An input line not beginning with '!' will be passed to the process.\r\n");
349 put("A newline will be appended unless the line ends with a trailing backslash.\r\n");
350 put("\r\n");
351 put("# gemrepl meta commands\r\n");
352 put("=> ?!help !help: This help\r\n");
353 put("=> ?!kill !kill: kill process\r\n");
354 if (state->format != raw) {
355 put("=> ?!nolink !nolink: suppress input link\r\n");
356 put("=> ?!showlink !showlink: show input link\r\n");
357 put("=> ?!plain !plain: use text/plain for responses\r\n");
358 put("=> ?!gemtext !gemtext: use text/gemini for responses (default)\r\n");
360 put("=> ?!C !C: pass ^C (SIGINT) to process\r\n");
361 put("=> ?!? !?: Prompt for input\r\n");
362 put("=> ?!! !!: Literal '!'\r\n");
363 return;
364 } else if (0 == strncmp(q, "kill", strlen(q))) {
365 kill(-child->pid, SIGKILL);
366 q += strlen(q);
367 } else if (0 == strncmp(q, "C", strlen(q))) {
368 kill(-child->pid, SIGINT);
369 q += strlen(q);
370 } else if (0 == strncmp(q, "nolink", strlen(q))) {
371 // TODO: might be better to have this be a permanent option
372 // attached to the cert rather than the child.
373 child->nolink = true;
374 put("20 text/gemini\r\n");
375 put("Input links disabled.\r\n");
376 put("=> ?!showlink Re-enable input links\r\n");
377 return;
378 } else if (0 == strncmp(q, "showlink", strlen(q))) {
379 child->nolink = false;
380 put("20 text/gemini\r\n");
381 put("Input links enabled.\r\n");
382 put("=> ?!? Input command\r\n");
383 return;
384 } else if (0 == strncmp(q, "plain", strlen(q))) {
385 child->plain = true;
386 put("20 text/gemini\r\n");
387 put("Plaintext mode enabled.\r\n");
388 put("=> ?!gemtext Re-enable gemtext\r\n");
389 return;
390 } else if (0 == strncmp(q, "gemtext", strlen(q))) {
391 child->plain = false;
392 put("20 text/gemini\r\n");
393 put("Gemtext mode enabled.\r\n");
394 put("=> ?!? Input command\r\n");
395 return;
396 } else if (*q != '!') {
397 put("40 Unknown gemrepl meta-command (use '!!' for a literal '!')\r\n");
398 return;
402 if (state->format != raw) {
403 if (child->plain) put("20 text/plain\r\n");
404 else put("20 text/gemini\r\n");
406 if (child->newborn) {
407 put("[gemrepl: child spawned. Input \"!help\" for meta-commands]\r\n");
410 if (!(child->nolink || child->plain)) put("=> ?!? Input command\r\n");
413 if (!child->newborn) kill(-child->pid, SIGCONT);
415 int qlen = strlen(q);
416 if (!child->newborn) {
417 bool newline = true;
418 if (qlen > 0 && q[qlen-1] == '\\') {
419 --qlen;
420 newline = false;
422 signal(SIGPIPE, SIG_IGN);
423 bool succ = (write(child->in, q, qlen) == qlen
424 && (!newline || write(child->in, "\n", 1) == 1));
425 signal(SIGPIPE, SIG_DFL);
426 if (!succ) {
427 put("[gemrepl: error when writing to child]\r\n");
431 if (state->format == pre && !child->plain) put("```\r\n");
432 const int succ = stream_text(child->out, child->flag, socket,
433 state->convert_newlines,
434 state->format == pre && !child->plain,
435 state->format == unwrapped && !child->plain,
436 &child->reading,
437 state->read_timeout,
438 state->pause_timeout);
439 if (state->format == pre && !child->plain) put("\r\n```\r\n");
441 if (succ < 0) put("[gemrepl: error when reading from child]\r\n");
442 else if (succ == 0) {
443 // got HUP; sleep briefly to give child a chance to exit
444 usleep(100);
447 set_child_last_active(child);
448 child->newborn = false;
450 if (waitpid(child->pid, NULL, WNOHANG) == child->pid) {
451 put("[gemrepl: child process terminated]");
452 close(child->in);
453 close(child->out);
454 close(child->flag);
455 child->exists = false;
456 } else {
457 kill(-child->pid, SIGSTOP);
461 /* How long in ms to wait for child to output something */
462 #define DEF_READ_TIMEOUT 3000
464 /* How long in ms child can pause between writes before we consider it to have
465 * finished writing */
466 #define DEF_PAUSE_TIMEOUT 300
468 static void usage()
470 printf("Usage: gemrepl [OPTION]... -s PATH COMMAND [ARG]...\n");
471 printf(" -h --help This help\n");
472 printf(" -s PATH --socket=PATH Path for socket file, which will be created\n");
473 printf(" -m NUM --max-children=NUM Max concurrent children to spawn (%d)\n", MAX_CHILDREN);
474 printf(" -t MS --read-timeout=MS Time to wait for child to start writing (%d)\n", DEF_READ_TIMEOUT);
475 printf(" -T MS --pause-timeout=MS Silence period after which child is paused (%d)\n", DEF_PAUSE_TIMEOUT);
476 printf(" -n --lf-crlf Convert \\n to \\r\\n (default unless --format=raw)\n");
477 printf(" -N --no-lf-crlf Preserve newlines\n");
478 printf(" -f FMT --format=FMT Format of output of command. Possible formats:\n");
479 printf(" gemtext: text/gemini (default)\n");
480 printf(" pre: preformatted text\n");
481 printf(" unwrapped: plain text without hard wrapping\n");
482 printf(" raw: gemini protocol output, including response headers\n");
486 int main(int argc, char **argv)
488 if (argc < 2) {
489 usage();
490 exit(1);
493 State *state = malloc(sizeof(State));
494 if (state == NULL) {
495 fprintf(stderr, "Failed to allocate memory for state.");
496 exit(1);
499 state->max_children = MAX_CHILDREN;
500 state->read_timeout = DEF_READ_TIMEOUT;
501 state->pause_timeout = DEF_PAUSE_TIMEOUT;
502 state->format = gemtext;
504 int convert_newlines = -1;
506 const struct option longoptions[] =
507 { { "help", 0, NULL, 'h' }
508 , { "socket", 1, NULL, 's' }
509 , { "format", 1, NULL, 'f' }
510 , { "max-children", 1, NULL, 'm' }
511 , { "read-timeout", 1, NULL, 't' }
512 , { "pause-timeout", 1, NULL, 'T' }
513 , { "lf-crlf", 0, NULL, 'n' }
514 , { "no-lf-crlf", 0, NULL, 'N' }
515 , { 0,0,0,0 }
517 int o;
518 const char *socketname = NULL;
519 while (-1 != (o = getopt_long(argc, argv, "+hs:f:m:t:T:nN", longoptions, NULL))) {
520 switch (o) {
521 case 'h':
522 case '?':
523 usage();
524 exit((o=='?'));
525 case 's':
526 socketname = optarg;
527 break;
528 case 'f':
529 if (0 == strcmp(optarg, "gemtext")) state->format=gemtext;
530 else if (0 == strcmp(optarg, "pre")) state->format=pre;
531 else if (0 == strcmp(optarg, "unwrapped")) state->format=unwrapped;
532 else if (0 == strcmp(optarg, "raw")) state->format=raw;
533 else {
534 printf("Unknown format.\n");
535 exit(1);
537 break;
538 case 'm':
539 state->max_children = atoi(optarg);
540 if (state->max_children <= 0 || state->max_children > MAX_CHILDREN) {
541 printf("Bad value for max children.\n");
542 printf("You may need to increase MAX_CHILDREN in the source.\n");
543 exit(1);
545 break;
546 case 't':
547 state->read_timeout = atoi(optarg);
548 if (state->read_timeout < 0) {
549 printf("Bad value for read timeout.\n");
550 exit(1);
552 break;
553 case 'T':
554 state->pause_timeout = atoi(optarg);
555 if (state->pause_timeout < 0) {
556 printf("Bad value for pause timeout.\n");
557 exit(1);
559 break;
560 case 'n':
561 convert_newlines = 1;
562 break;
563 case 'N':
564 convert_newlines = 0;
565 break;
569 if (argv[optind] == NULL || socketname == NULL) {
570 usage();
571 exit(1);
574 state->command = argv[optind];
575 state->args = &argv[optind];
576 state->convert_newlines = convert_newlines < 0 ? state->format != raw : convert_newlines;
578 srandom(time(NULL));
580 runSCGI(socketname, respond, state);