1 /* Copyright 2021, Martin Bays <mbays@sdf.org>
2 * SPDX-License-Identifier: GPL-3.0-or-later */
14 #include <sys/types.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
{
28 pthread_mutex_t
*mutex
; // initialised if child->exists, maybe also if not
29 char sess_id
[SESSION_ID_LEN
];
42 typedef enum output_format
49 typedef struct State
{
53 bool convert_newlines
;
62 Child children
[MAX_CHILDREN
];
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) {
74 const pid_t pid
= fork();
90 setbuffer(stdin
, NULL
, 0);
91 setbuffer(stdout
, NULL
, 0);
92 setbuffer(fdopen(3, "w"), NULL
, 0);
96 snprintf(tlsenv
, 64+16+1, "TLS_CLIENT_HASH=%s", child
->owner
);
100 char qenv
[1024+16+1];
101 snprintf(qenv
, 1024+16+1, "SPAWN_PARAMETER=%s", query
);
105 execvp(command
, args
);
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);
127 static bool write_all(int fd
, const char* buf
, int n
)
130 int w
= write(fd
, buf
, n
);
131 if (w
< 0) return false;
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
,
156 int read_timeout
, int pause_timeout
) {
158 struct pollfd pfd
[2] = { { in
, POLLIN
| POLLHUP
, 0 }, {flag
, POLLIN
, 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. */
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;
177 if ((escape_pre
|| escape_all
) && backticks
>= 0) {
181 if (backticks
== 3) {
182 write(out
, " ```", 4);
187 } else while (--backticks
>= 0) write(out
, "`", 1);
190 if (escape_all
&& escape
> 0) {
191 if (escape
== '\n') {
192 if (*b
== '#' || *b
== '>') {
194 } else if (*b
== '=' || *b
== '*') {
200 if ((escape
== '=' && *b
== '>')
201 || (escape
== '*' && *b
== ' ')) {
204 write(out
, &escape
, 1);
209 if (convert_newlines
&& *b
== '\n') {
210 write(out
, "\r\n", 2);
213 } else write(out
, b
, 1);
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;
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");
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
);
250 put(request_info
->script_path
);
252 putn(c
->sess_id
, SESSION_ID_LEN
);
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;
265 if (last_active
> c
->last_active
) {
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
);
281 // TODO: would be nice to queue a regretful message for the owner
282 // of the child we're killing...
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
);
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");
310 if (pthread_mutex_init(child
->mutex
, NULL
) != 0) {
311 put("40 Spawn failure (mutex_init).\r\n");
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
;
330 put(request_info
->script_path
);
332 putn(child
->sess_id
, SESSION_ID_LEN
);
334 pthread_mutex_unlock(child
->mutex
);
338 if (0 == strncmp(request_info
->path_info
, "/list", strlen(request_info
->path_info
))) {
339 put("20 text/gemini\r\n");
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;
345 0 == strncmp(c
->owner
, request_info
->tls_client_hash
, 64)) {
348 put("20 text/gemini\r\n");
351 put(request_info
->script_path
);
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");
362 if (strlen(request_info
->path_info
) != 1+SESSION_ID_LEN
) {
363 put("51 Bad session id.\r\n");
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. */
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;
379 0 == strncmp(c
->sess_id
, sess_id
, SESSION_ID_LEN
)) {
382 if (c
->mutex
!= NULL
) pthread_mutex_unlock(c
->mutex
);
386 put("20 text/gemini\r\nSession not found.\r\n=> ");
387 put(request_info
->script_path
);
388 put(" Start new session\r\n");
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");
404 static void do_command(const State
* state
, Child
*child
, const char* q
, int socket
) {
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");
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");
427 } else if (0 == strncmp(q
, "kill", strlen(q
))) {
428 kill(-child
->pid
, SIGKILL
);
430 } else if (0 == strncmp(q
, "C", strlen(q
))) {
431 kill(-child
->pid
, SIGINT
);
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");
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");
447 } else if (0 == strncmp(q
, "plain", strlen(q
))) {
449 put("20 text/gemini\r\n");
450 put("Plaintext mode enabled.\r\n");
451 put("=> ?!gemtext Re-enable gemtext\r\n");
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");
459 } else if (*q
!= '!') {
460 put("40 Unknown gemrepl meta-command (use '!!' for a literal '!')\r\n");
463 } else if (strchr(q
, '\n') != NULL
) {
464 put("40 Input may not include embedded newlines\r\n");
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);
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
,
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
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]");
516 child
->exists
= false;
518 kill(-child
->pid
, SIGSTOP
);
522 // Wrap arguments of do_command into a single struct, for use with
524 typedef struct 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
);
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
);
555 Do_Command_Arg
*arg
= malloc(sizeof(Do_Command_Arg
));
562 arg
->q
= request_info
->query_string_decoded
;
563 arg
->socket
= socket
;
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
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 */
600 static void cleanup(int sig
) {
601 for (int i
= 0; i
< state
.num_children
; ++i
) {
602 Child
*const c
= &state
.children
[i
];
607 kill(-c
->pid
, SIGKILL
);
608 waitpid(c
->pid
, NULL
, 0);
614 int main(int argc
, char **argv
)
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' }
643 const char *socketname
= NULL
;
644 while (-1 != (o
= getopt_long(argc
, argv
, "+1hs:f:Lm:t:T:SnN", longoptions
, NULL
))) {
654 state
.single_session
= true;
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
;
662 printf("Unknown format.\n");
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");
678 state
.read_timeout
= atoi(optarg
);
681 state
.pause_timeout
= atoi(optarg
);
684 state
.read_timeout
= -1;
685 state
.pause_timeout
= -1;
688 convert_newlines
= 1;
691 convert_newlines
= 0;
696 if (argv
[optind
] == NULL
|| socketname
== NULL
) {
701 state
.command
= argv
[optind
];
702 state
.args
= &argv
[optind
];
703 state
.convert_newlines
= convert_newlines
< 0 ? state
.format
!= raw
: convert_newlines
;
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
);