--single-session
[gemrepl.git] / main.c
blob23aa816e28525002a049fae7e1e4d77d09f38a6a
1 /* Copyright 2021, Martin Bays <mbays@sdf.org>
2 * SPDX-License-Identifier: GPL-3.0-or-later */
3 #include <fcntl.h>
4 #include <getopt.h>
5 #include <poll.h>
6 #include <pthread.h>
7 #include <signal.h>
8 #include <stdbool.h>
9 #include <stdio.h>
10 #include <stdint.h>
11 #include <stdlib.h>
12 #include <string.h>
13 #include <time.h>
14 #include <sys/types.h>
15 #include <sys/wait.h>
16 #include <unistd.h>
18 #include "gemscgi.h"
20 /* If you increase this too far, you may run into file descriptor limits */
21 #define MAX_CHILDREN 256
23 #define SESSION_ID_LEN 8
25 typedef struct Child {
26 bool exists;
27 bool newborn;
28 pthread_mutex_t *mutex; // initialised if child->exists, maybe also if not
29 char sess_id[SESSION_ID_LEN];
30 char owner[64];
31 uint64_t last_active;
32 pid_t pid;
33 int in;
34 int out;
35 int flag;
36 bool reading;
38 bool nolink;
39 bool plain;
40 } Child;
42 typedef enum output_format
43 { gemtext
44 , pre
45 , unwrapped
46 , raw
47 } output_format;
49 typedef struct State {
50 const char *command;
51 char *const *args;
52 output_format format;
53 bool convert_newlines;
55 int max_children;
56 int read_timeout;
57 int pause_timeout;
58 bool nolink;
59 bool single_session;
61 int num_children;
62 Child children[MAX_CHILDREN];
63 } State;
65 static bool spawn(const char *command, char *const *args, const char *query,
66 Child *child, int socket)
68 int infds[2], outfds[2], flagfds[2];
69 if (pipe(infds) == -1 || pipe(outfds) == -1 || pipe(flagfds) == -1) {
70 perror("pipe");
71 return false;
74 const pid_t pid = fork();
75 if (pid == -1) {
76 perror("fork");
77 return false;
80 if (pid == 0) {
81 // child
82 close(socket);
83 close(infds[1]);
84 close(outfds[0]);
85 close(flagfds[0]);
86 dup2(infds[0], 0);
87 dup2(outfds[1], 1);
88 dup2(outfds[1], 2);
89 dup2(flagfds[1], 3);
90 setbuffer(stdin, NULL, 0);
91 setbuffer(stdout, NULL, 0);
92 setbuffer(fdopen(3, "w"), NULL, 0);
93 setsid();
95 char tlsenv[64+16+1];
96 snprintf(tlsenv, 64+16+1, "TLS_CLIENT_HASH=%s", child->owner);
97 putenv(tlsenv);
99 if (query != NULL) {
100 char qenv[1024+16+1];
101 snprintf(qenv, 1024+16+1, "SPAWN_PARAMETER=%s", query);
102 putenv(qenv);
105 execvp(command, args);
106 exit(1);
107 } else {
108 // parent
109 close(infds[0]);
110 close(outfds[1]);
111 close(flagfds[1]);
112 child->pid = pid;
113 child->in = infds[1];
114 child->out = outfds[0];
115 child->flag = flagfds[0];
116 fcntl(child->in, F_SETFD, FD_CLOEXEC);
117 fcntl(child->out, F_SETFD, FD_CLOEXEC);
118 fcntl(child->flag, F_SETFD, FD_CLOEXEC);
119 setbuffer(fdopen(infds[1], "w"), NULL, 0);
120 setbuffer(fdopen(outfds[0], "r"), NULL, 0);
121 setbuffer(fdopen(flagfds[0], "r"), NULL, 0);
124 return true;
127 static bool write_all(int fd, const char* buf, int n)
129 while (n > 0) {
130 int w = write(fd, buf, n);
131 if (w < 0) return false;
132 buf += w;
133 n -= w;
135 return true;
138 static void set_child_last_active(Child *child)
140 struct timespec clock_mono;
141 clock_gettime(CLOCK_MONOTONIC, &clock_mono);
142 child->last_active = clock_mono.tv_sec;
145 /* Write anything written timelily on `in` to `out`,
146 * optionally converting \n to \r\n and space-stuffing gemini-magic lines.
147 * Streaming will cease if there is nothing to read on `in` for `read_timeout`
148 * ms, or after `pause_timeout` ms if something has been read, or after '<' is
149 * read from `flag` without a subsequent '>'.
150 * Return -1 on read error, 0 on HUP, else 1. */
151 static int stream_text(int in, int flag, int out,
152 bool convert_newlines,
153 bool escape_pre,
154 bool escape_all,
155 bool *child_reading,
156 int read_timeout, int pause_timeout) {
157 char buf[256];
158 struct pollfd pfd[2] = { { in, POLLIN | POLLHUP, 0 }, {flag, POLLIN, 0 } };
159 int backticks = 0;
160 char escape = 0;
161 bool read_something = false;
163 /* Note we set no total maximum time or output size limit; we leave it to
164 * the user to e.g. set a ulimit to handle runaway processes. */
165 while (true) {
166 poll(pfd, 2, *child_reading ? 20 :
167 read_something ? read_timeout : pause_timeout);
169 if (pfd[0].revents & POLLIN) {
170 read_something = true;
171 const int r = read(in, buf, 256 - 1);
172 if (r < 0) return false;
173 buf[r] = 0;
175 const char *b = buf;
176 while (*b) {
177 if ((escape_pre || escape_all) && backticks >= 0) {
178 if (*b == '`') {
179 escape = 0;
180 ++backticks;
181 if (backticks == 3) {
182 write(out, " ```", 4);
183 backticks = -1;
185 ++b;
186 continue;
187 } else while (--backticks >= 0) write(out, "`", 1);
190 if (escape_all && escape > 0) {
191 if (escape == '\n') {
192 if (*b == '#' || *b == '>') {
193 write(out, " ", 1);
194 } else if (*b == '=' || *b == '*') {
195 escape = *b;
196 ++b;
197 continue;
199 } else {
200 if ((escape == '=' && *b == '>')
201 || (escape == '*' && *b == ' ')) {
202 write(out, " ", 1);
204 write(out, &escape, 1);
206 escape = 0;
209 if (convert_newlines && *b == '\n') {
210 write(out, "\r\n", 2);
211 backticks = 0;
212 escape = '\n';
213 } else write(out, b, 1);
214 ++b;
216 } else if (pfd[1].revents & POLLIN) {
217 const int r = read(flag, buf, 256);
218 for (int i = 0; i < r; ++i ) {
219 if (buf[i] == '<') *child_reading = true;
220 if (buf[i] == '>') *child_reading = false;
222 } else break;
224 while (--backticks >= 0) write(out, "`", 1);
225 if (escape > 0 && escape != '\n') write(out, &escape, 1);
226 return (!(pfd[0].revents & POLLHUP));
229 #define put(s) write_all(socket, s, strlen(s))
230 #define putn(s,n) write_all(socket, s, n)
232 static Child *get_session(State *state, const Request_Info *request_info, int socket)
235 if (request_info->tls_client_hash == NULL) {
236 put("60 Client certificate required\r\n");
237 return NULL;
240 if (request_info->path_info == NULL || strlen(request_info->path_info) <= 1) {
241 if (state->single_session) {
242 for (int i = 0; i < state->num_children; ++i) {
243 Child *const c = &state->children[i];
244 if (c->mutex != NULL && pthread_mutex_trylock(c->mutex) != 0) continue;
245 bool found = (c->exists &&
246 0 == strncmp(c->owner, request_info->tls_client_hash, 64));
247 if (c->mutex != NULL) pthread_mutex_unlock(c->mutex);
248 if (found) {
249 put("30 ");
250 put(request_info->script_path);
251 put("/");
252 putn(c->sess_id, SESSION_ID_LEN);
253 put("\r\n");
254 return NULL;
259 Child *slot = NULL;
260 uint64_t last_active = UINT64_MAX;
261 for (int i = 0; i < state->num_children; ++i) {
262 Child *const c = &state->children[i];
263 if (c->mutex != NULL && pthread_mutex_trylock(c->mutex) != 0) continue;
264 if (c->exists) {
265 if (last_active > c->last_active) {
266 slot = c;
267 last_active = c->last_active;
269 } else if (slot == NULL || last_active < UINT64_MAX) slot = c;
270 if (c->mutex != NULL) pthread_mutex_unlock(c->mutex);
273 if (slot == NULL || (last_active < UINT64_MAX && state->num_children < state->max_children)) {
274 slot = &state->children[state->num_children++];
277 Child *const child = slot;
278 if (child->mutex != NULL) pthread_mutex_lock(child->mutex);
280 if (child->exists) {
281 // TODO: would be nice to queue a regretful message for the owner
282 // of the child we're killing...
283 close(child->in);
284 close(child->out);
285 close(child->flag);
286 kill(child->pid, 9);
287 child->exists = false;
290 memset(child, 0, sizeof(Child));
292 strncpy(child->owner, request_info->tls_client_hash, 64);
293 for (int i = 0; i < SESSION_ID_LEN; ++i) {
294 child->sess_id[i] = 'A' + random()%26 + (random()%2 ? ('a'-'A') : 0);
297 if (!spawn(state->command, state->args, request_info->query_string_decoded, child, socket)) {
298 put("40 Spawn failure.\r\n");
299 if (child->mutex != NULL) pthread_mutex_unlock(child->mutex);
300 return NULL;
303 if (child->mutex == NULL) {
304 child->mutex = malloc(sizeof(pthread_mutex_t));
305 if (child->mutex == NULL) {
306 put("40 Spawn failure (malloc).\r\n");
307 return NULL;
310 if (pthread_mutex_init(child->mutex, NULL) != 0) {
311 put("40 Spawn failure (mutex_init).\r\n");
312 free(child->mutex);
313 child->mutex = NULL;
314 return NULL;
317 // Note: we never destroy the mutex, because we never know that it
318 // would be safe to do so.
320 pthread_mutex_lock(child->mutex);
323 child->exists = true;
324 child->newborn = true;
325 set_child_last_active(child);
327 child->nolink = state->nolink;
329 put("30 ");
330 put(request_info->script_path);
331 put("/");
332 putn(child->sess_id, SESSION_ID_LEN);
333 put("\r\n");
334 pthread_mutex_unlock(child->mutex);
335 return NULL;
338 if (0 == strncmp(request_info->path_info, "/list", strlen(request_info->path_info))) {
339 put("20 text/gemini\r\n");
340 bool found = false;
341 for (int i = 0; i < state->num_children; ++i) {
342 Child *const c = &state->children[i];
343 if (c->mutex != NULL && pthread_mutex_trylock(c->mutex) != 0) continue;
344 if (c->exists &&
345 0 == strncmp(c->owner, request_info->tls_client_hash, 64)) {
346 if (!found) {
347 found = true;
348 put("20 text/gemini\r\n");
350 put("=> ");
351 put(request_info->script_path);
352 put("/");
353 putn(c->sess_id, SESSION_ID_LEN);
354 put(" Resume session\r\n");
356 if (c->mutex != NULL) pthread_mutex_unlock(c->mutex);
358 if (!found) put("No sessions found.\r\n");
359 return NULL;
362 if (strlen(request_info->path_info) != 1+SESSION_ID_LEN) {
363 put("51 Bad session id.\r\n");
364 return NULL;
367 // drop initial '/'
368 const char *sess_id = request_info->path_info + 1;
370 /* Find child with this sess_id.
371 * For simplicity, in particular for the mutex handling, we use a static
372 * array of children rather than allocating dynamically, and don't sort.
373 * This could be optimised. */
374 Child *child = NULL;
375 for (int i = 0; child == NULL && i < state->num_children; ++i) {
376 Child *const c = &state->children[i];
377 if (c->mutex != NULL && pthread_mutex_trylock(c->mutex) != 0) continue;
378 if (c->exists &&
379 0 == strncmp(c->sess_id, sess_id, SESSION_ID_LEN)) {
380 child = c;
382 if (c->mutex != NULL) pthread_mutex_unlock(c->mutex);
385 if (child == NULL) {
386 put("20 text/gemini\r\nSession not found.\r\n=> ");
387 put(request_info->script_path);
388 put(" Start new session\r\n");
389 return NULL;
392 pthread_mutex_lock(child->mutex);
393 const char* owner = child->owner;
394 pthread_mutex_unlock(child->mutex);
396 if (0 != strncmp(owner, request_info->tls_client_hash, 64)) {
397 put("61 Wrong certificate for session.\r\n");
398 return NULL;
401 return child;
404 static void do_command(const State* state, Child *child, const char* q, int socket) {
405 if (*q == '!') {
406 ++q;
407 if (*q == '?') {
408 put("10\r\n");
409 return;
410 } else if (0 == strncmp(q, "help", strlen(q))) {
411 put("20 text/gemini\r\n");
412 put("An input line not beginning with '!' will be passed to the process.\r\n");
413 put("\r\n");
414 put("# gemrepl meta commands\r\n");
415 put("=> ?!help !help: This help\r\n");
416 put("=> ?!kill !kill: kill process\r\n");
417 if (state->format != raw) {
418 put("=> ?!nolink !nolink: suppress input link\r\n");
419 put("=> ?!showlink !showlink: show input link\r\n");
420 put("=> ?!plain !plain: use text/plain for responses\r\n");
421 put("=> ?!gemtext !gemtext: use text/gemini for responses (default)\r\n");
423 put("=> ?!C !C: pass ^C (SIGINT) to process\r\n");
424 put("=> ?!? !?: Prompt for input\r\n");
425 put("=> ?!! !!: Literal '!'\r\n");
426 return;
427 } else if (0 == strncmp(q, "kill", strlen(q))) {
428 kill(-child->pid, SIGKILL);
429 q += strlen(q);
430 } else if (0 == strncmp(q, "C", strlen(q))) {
431 kill(-child->pid, SIGINT);
432 q += strlen(q);
433 } else if (0 == strncmp(q, "nolink", strlen(q))) {
434 // TODO: might be better to have this be a permanent option
435 // attached to the cert rather than the child.
436 child->nolink = true;
437 put("20 text/gemini\r\n");
438 put("Input links disabled.\r\n");
439 put("=> ?!showlink Re-enable input links\r\n");
440 return;
441 } else if (0 == strncmp(q, "showlink", strlen(q))) {
442 child->nolink = false;
443 put("20 text/gemini\r\n");
444 put("Input links enabled.\r\n");
445 put("=> ?!? Input command\r\n");
446 return;
447 } else if (0 == strncmp(q, "plain", strlen(q))) {
448 child->plain = true;
449 put("20 text/gemini\r\n");
450 put("Plaintext mode enabled.\r\n");
451 put("=> ?!gemtext Re-enable gemtext\r\n");
452 return;
453 } else if (0 == strncmp(q, "gemtext", strlen(q))) {
454 child->plain = false;
455 put("20 text/gemini\r\n");
456 put("Gemtext mode enabled.\r\n");
457 put("=> ?!? Input command\r\n");
458 return;
459 } else if (*q != '!') {
460 put("40 Unknown gemrepl meta-command (use '!!' for a literal '!')\r\n");
461 return;
463 } else if (strchr(q, '\n') != NULL) {
464 put("40 Input may not include embedded newlines\r\n");
465 return;
468 if (state->format != raw) {
469 if (child->plain) put("20 text/plain\r\n");
470 else put("20 text/gemini\r\n");
472 if (child->newborn) {
473 put("[gemrepl: child spawned. Input \"!help\" for meta-commands]\r\n");
476 if (!(child->nolink || child->plain)) put("=> ?!? Input command\r\n");
479 if (!child->newborn) kill(-child->pid, SIGCONT);
481 int qlen = strlen(q);
482 if (!child->newborn) {
483 bool succ = (write(child->in, q, qlen) == qlen
484 && write(child->in, "\n", 1) == 1);
485 if (!succ) {
486 put("[gemrepl: error when writing to child]\r\n");
489 child->reading = false;
492 if (state->format == pre && !child->plain) put("```\r\n");
493 const int succ = stream_text(child->out, child->flag, socket,
494 state->convert_newlines,
495 state->format == pre && !child->plain,
496 state->format == unwrapped && !child->plain,
497 &child->reading,
498 state->read_timeout,
499 state->pause_timeout);
500 if (state->format == pre && !child->plain) put("\r\n```\r\n");
502 if (succ < 0) put("[gemrepl: error when reading from child]\r\n");
503 else if (succ == 0) {
504 // got HUP; sleep briefly to give child a chance to exit
505 usleep(100);
508 set_child_last_active(child);
509 child->newborn = false;
511 if (waitpid(child->pid, NULL, WNOHANG) == child->pid) {
512 if (state->format != raw) put("[gemrepl: child process terminated]");
513 close(child->in);
514 close(child->out);
515 close(child->flag);
516 child->exists = false;
517 } else {
518 kill(-child->pid, SIGSTOP);
522 // Wrap arguments of do_command into a single struct, for use with
523 // pthread_create.
524 typedef struct Do_Command_Arg {
525 const State* state;
526 Child *child;
527 const char* q;
528 int socket;
529 } Do_Command_Arg;
531 static void *do_command_thread(void *object)
533 Do_Command_Arg *arg = (Do_Command_Arg *)object;
535 pthread_mutex_lock(arg->child->mutex);
536 do_command(arg->state, arg->child, arg->q, arg->socket);
537 pthread_mutex_unlock(arg->child->mutex);
539 close(arg->socket);
540 free(arg);
541 return NULL;
544 void respond(void *object, const Request_Info *request_info, int socket)
546 State *state = (State *)object;
548 Child *child = get_session(state, request_info, socket);
550 if (child == NULL) {
551 close(socket);
552 return;
555 Do_Command_Arg *arg = malloc(sizeof(Do_Command_Arg));
556 if (arg == NULL) {
557 close(socket);
558 return;
560 arg->state = state;
561 arg->child = child;
562 arg->q = request_info->query_string_decoded;
563 arg->socket = socket;
565 pthread_t tid;
566 pthread_create(&tid, NULL, do_command_thread, arg);
569 /* How long in ms to wait for child to output something */
570 #define DEF_READ_TIMEOUT 3000
572 /* How long in ms child can pause between writes before we consider it to have
573 * finished writing */
574 #define DEF_PAUSE_TIMEOUT 300
576 static void usage()
578 printf("Usage: gemrepl [OPTION]... -s PATH COMMAND [ARG]...\n");
579 printf(" -h --help This help\n");
580 printf(" -s PATH --socket=PATH Path for socket file, which will be created\n");
581 printf(" -m NUM --max-children=NUM Max concurrent children to spawn (%d)\n", MAX_CHILDREN);
582 printf(" -t MS --read-timeout=MS Time to wait for child to start writing (%d)\n", DEF_READ_TIMEOUT);
583 printf(" -T MS --pause-timeout=MS Silence period after which child is paused (%d)\n", DEF_PAUSE_TIMEOUT);
584 printf(" -S --synchronous Disable timeouts. Use fd 3 instead (see docs).\n");
585 printf(" -L --no-link Don't write input links.\n");
586 printf(" -1 --single-session Allow only one session per user.\n");
587 printf(" -n --lf-crlf Convert \\n to \\r\\n (default unless --format=raw)\n");
588 printf(" -N --no-lf-crlf Preserve newlines\n");
589 printf(" -f FMT --format=FMT Format of output of command. Possible formats:\n");
590 printf(" gemtext: text/gemini (default)\n");
591 printf(" pre: preformatted text\n");
592 printf(" unwrapped: plain text without hard wrapping\n");
593 printf(" raw: gemini protocol output, including response headers\n");
597 /* state as global variable, so we can clean up on termination */
598 State state = {};
600 static void cleanup(int sig) {
601 for (int i = 0; i < state.num_children; ++i) {
602 Child *const c = &state.children[i];
603 if (c->exists) {
604 close(c->in);
605 close(c->out);
606 close(c->flag);
607 kill(-c->pid, SIGKILL);
608 waitpid(c->pid, NULL, 0);
611 exit(1);
614 int main(int argc, char **argv)
616 if (argc < 2) {
617 usage();
618 exit(1);
621 state.max_children = MAX_CHILDREN;
622 state.read_timeout = DEF_READ_TIMEOUT;
623 state.pause_timeout = DEF_PAUSE_TIMEOUT;
624 state.format = gemtext;
626 int convert_newlines = -1;
628 const struct option longoptions[] =
629 { { "help", 0, NULL, 'h' }
630 , { "socket", 1, NULL, 's' }
631 , { "format", 1, NULL, 'f' }
632 , { "no-link", 1, NULL, 'L' }
633 , { "max-children", 1, NULL, 'm' }
634 , { "read-timeout", 1, NULL, 't' }
635 , { "pause-timeout", 1, NULL, 'T' }
636 , { "synchronous", 0, NULL, 'S' }
637 , { "single-session", 0, NULL, '1' }
638 , { "lf-crlf", 0, NULL, 'n' }
639 , { "no-lf-crlf", 0, NULL, 'N' }
640 , { 0,0,0,0 }
642 int o;
643 const char *socketname = NULL;
644 while (-1 != (o = getopt_long(argc, argv, "+1hs:f:Lm:t:T:SnN", longoptions, NULL))) {
645 switch (o) {
646 case 'h':
647 case '?':
648 usage();
649 exit((o=='?'));
650 case 's':
651 socketname = optarg;
652 break;
653 case '1':
654 state.single_session = true;
655 break;
656 case 'f':
657 if (0 == strcmp(optarg, "gemtext")) state.format=gemtext;
658 else if (0 == strcmp(optarg, "pre")) state.format=pre;
659 else if (0 == strcmp(optarg, "unwrapped")) state.format=unwrapped;
660 else if (0 == strcmp(optarg, "raw")) state.format=raw;
661 else {
662 printf("Unknown format.\n");
663 exit(1);
665 break;
666 case 'L':
667 state.nolink = true;
668 break;
669 case 'm':
670 state.max_children = atoi(optarg);
671 if (state.max_children <= 0 || state.max_children > MAX_CHILDREN) {
672 printf("Bad value for max children.\n");
673 printf("You may need to increase MAX_CHILDREN in the source.\n");
674 exit(1);
676 break;
677 case 't':
678 state.read_timeout = atoi(optarg);
679 break;
680 case 'T':
681 state.pause_timeout = atoi(optarg);
682 break;
683 case 'S':
684 state.read_timeout = -1;
685 state.pause_timeout = -1;
686 break;
687 case 'n':
688 convert_newlines = 1;
689 break;
690 case 'N':
691 convert_newlines = 0;
692 break;
696 if (argv[optind] == NULL || socketname == NULL) {
697 usage();
698 exit(1);
701 state.command = argv[optind];
702 state.args = &argv[optind];
703 state.convert_newlines = convert_newlines < 0 ? state.format != raw : convert_newlines;
705 srandom(time(NULL));
707 struct sigaction act = {};
708 act.sa_handler = cleanup;
709 sigaction(SIGTERM, &act, NULL);
710 sigaction(SIGINT, &act, NULL);
711 act.sa_handler = SIG_IGN;
712 sigaction(SIGPIPE, &act, NULL);
714 runSCGI(socketname, respond, &state);