11 #include <sys/types.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
{
25 char sess_id
[SESSION_ID_LEN
];
38 typedef enum output_format
45 typedef struct State
{
49 bool convert_newlines
;
56 Child children
[MAX_CHILDREN
];
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) {
68 const pid_t pid
= fork();
84 setbuffer(stdin
, NULL
, 0);
85 setbuffer(stdout
, NULL
, 0);
86 setbuffer(fdopen(3, "w"), NULL
, 0);
90 snprintf(tlsenv
, 81, "TLS_CLIENT_HASH=%s", child
->owner
);
95 snprintf(qenv
, 1035, "INIT_QUERY=%s", query
);
99 execvp(command
, args
);
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);
121 static bool write_all(int fd
, const char* buf
, int n
)
124 int w
= write(fd
, buf
, n
);
125 if (w
< 0) return false;
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
,
150 int read_timeout
, int pause_timeout
) {
152 struct pollfd pfd
[2] = { { in
, POLLIN
| POLLHUP
, 0 }, {flag
, POLLIN
, 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. */
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;
171 if ((escape_pre
|| escape_all
) && backticks
>= 0) {
175 if (backticks
== 3) {
176 write(out
, " ```", 4);
181 } else while (--backticks
>= 0) write(out
, "`", 1);
184 if (escape_all
&& escape
> 0) {
185 if (escape
== '\n') {
186 if (*b
== '#' || *b
== '>') {
188 } else if (*b
== '=' || *b
== '*') {
194 if ((escape
== '=' && *b
== '>')
195 || (escape
== '*' && *b
== ' ')) {
198 write(out
, &escape
, 1);
203 if (convert_newlines
&& *b
== '\n') {
204 write(out
, "\r\n", 2);
207 } else write(out
, b
, 1);
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;
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");
235 const char *q
= request_info
->query_string_decoded
;
237 if (request_info
->path_info
== NULL
|| strlen(request_info
->path_info
) <= 1) {
239 for (int i
= 0; i
< state
->num_children
; ++i
) {
240 Child
*const c
= &state
->children
[i
];
242 if (slot
== NULL
|| (slot
->exists
243 && slot
->last_active
> c
->last_active
)) {
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
;
255 // TODO: would be nice to queue a regretful message for the owner
256 // of the child we're killing...
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");
276 child
->exists
= true;
277 child
->newborn
= true;
278 set_child_last_active(child
);
281 put(request_info
->script_path
);
283 putn(child
->sess_id
, SESSION_ID_LEN
);
286 } else if (0 == strncmp(request_info
->path_info
, "/list", strlen(request_info
->path_info
))) {
287 put("20 text/gemini\r\n");
289 for (int i
= 0; i
< state
->num_children
; ++i
) {
290 Child
*const c
= &state
->children
[i
];
292 0 == strncmp(c
->owner
, request_info
->tls_client_hash
, 64)) {
295 put("20 text/gemini\r\n");
298 put(request_info
->script_path
);
300 putn(c
->sess_id
, SESSION_ID_LEN
);
301 put(" Resume session\r\n");
304 if (!found
) put("No sessions found.\r\n");
308 if (strlen(request_info
->path_info
) != 1+SESSION_ID_LEN
) {
309 put("51 Bad session id.\r\n");
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. */
320 for (int i
= 0; i
< state
->num_children
; ++i
) {
321 Child
*const c
= &state
->children
[i
];
323 0 == strncmp(c
->sess_id
, sess_id
, SESSION_ID_LEN
)) {
330 put("20 text/gemini\r\nSession not found.\r\n=> ");
331 put(request_info
->script_path
);
332 put(" Start new session\r\n");
336 if (0 != strncmp(child
->owner
, request_info
->tls_client_hash
, 64)) {
337 put("61 Wrong certificate for session.\r\n");
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");
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");
364 } else if (0 == strncmp(q
, "kill", strlen(q
))) {
365 kill(-child
->pid
, SIGKILL
);
367 } else if (0 == strncmp(q
, "C", strlen(q
))) {
368 kill(-child
->pid
, SIGINT
);
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");
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");
384 } else if (0 == strncmp(q
, "plain", strlen(q
))) {
386 put("20 text/gemini\r\n");
387 put("Plaintext mode enabled.\r\n");
388 put("=> ?!gemtext Re-enable gemtext\r\n");
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");
396 } else if (*q
!= '!') {
397 put("40 Unknown gemrepl meta-command (use '!!' for a literal '!')\r\n");
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
) {
418 if (qlen
> 0 && q
[qlen
-1] == '\\') {
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
);
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
,
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
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]");
455 child
->exists
= false;
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
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
)
493 State
*state
= malloc(sizeof(State
));
495 fprintf(stderr
, "Failed to allocate memory for state.");
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' }
518 const char *socketname
= NULL
;
519 while (-1 != (o
= getopt_long(argc
, argv
, "+hs:f:m:t:T:nN", longoptions
, NULL
))) {
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
;
534 printf("Unknown format.\n");
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");
547 state
->read_timeout
= atoi(optarg
);
548 if (state
->read_timeout
< 0) {
549 printf("Bad value for read timeout.\n");
554 state
->pause_timeout
= atoi(optarg
);
555 if (state
->pause_timeout
< 0) {
556 printf("Bad value for pause timeout.\n");
561 convert_newlines
= 1;
564 convert_newlines
= 0;
569 if (argv
[optind
] == NULL
|| socketname
== NULL
) {
574 state
->command
= argv
[optind
];
575 state
->args
= &argv
[optind
];
576 state
->convert_newlines
= convert_newlines
< 0 ? state
->format
!= raw
: convert_newlines
;
580 runSCGI(socketname
, respond
, state
);