10 #include <sys/types.h>
16 /* If you increase this too far, you may run into file descriptor limits */
17 #define MAX_CHILDREN 256
19 typedef struct Child
{
29 typedef struct State
{
38 Child children
[MAX_CHILDREN
];
41 static bool spawn(const char *command
, char *const *args
, Child
*child
, int socket
)
43 int infds
[2], outfds
[2];
44 if (pipe(infds
) == -1 || pipe(outfds
) == -1) {
49 const pid_t pid
= fork();
66 snprintf(tlsenv
, 81, "TLS_CLIENT_HASH=%s", child
->owner
);
68 execvp(command
, args
);
76 child
->out
= outfds
[0];
77 setlinebuf(fdopen(infds
[1], "w"));
78 setlinebuf(fdopen(outfds
[0], "r"));
84 static bool write_all(int fd
, const char* buf
, int n
)
87 int w
= write(fd
, buf
, n
);
88 if (w
< 0) return false;
95 static void set_child_last_active(Child
*child
)
97 struct timespec clock_mono
;
98 clock_gettime(CLOCK_MONOTONIC
, &clock_mono
);
99 child
->last_active
= clock_mono
.tv_sec
;
102 /* Write anything written timelily on `in` to `out`, converting \n to \r\n.
103 * If `gemtext` is true, also space-stuff lines beginning with "```".
104 * Return -1 on read error, 0 on HUP, else 1. */
105 static int stream_text(int in
, int out
, bool gemtext
, int read_timeout
, int pause_timeout
) {
107 struct pollfd pfd
= { in
, POLLIN
| POLLHUP
, 0 };
110 poll(&pfd
, 1, read_timeout
);
111 while (pfd
.revents
& POLLIN
) {
112 const int r
= read(in
, buf
, 256 - 1);
113 if (r
< 0) return false;
118 if (gemtext
&& backticks
>= 0) {
121 if (backticks
== 3) {
122 write(out
, " ```", 4);
127 } else while (--backticks
>= 0) write(out
, "`", 1);
131 write(out
, "\r\n", 2);
133 } else write(out
, b
, 1);
137 poll(&pfd
, 1, pause_timeout
);
139 return (!(pfd
.revents
& POLLHUP
));
142 void respond(void *object
, const Request_Info
*request_info
, int socket
)
144 State
*state
= (State
*)object
;
146 #define put(s) write_all(socket, s, strlen(s))
148 if (!request_info
->tls_client_hash
) {
149 put("60 Client certificate required\r\n");
153 Child
*child
= NULL
, *slot
= NULL
;
154 bool spawned
= false;
156 /* Find child with this cert hash, or spawn new.
157 * For simplicity, we use a static array of children rather than
158 * allocating dynamically. This wastes a few KB of memory; you may want to
159 * rewrite this if memory is tight. We also don't bother to keep the list
160 * sorted, but just strcmp for each child. Terribly wasteful. */
161 for (int i
= 0; i
< state
->num_children
; ++i
) {
162 Child
*const c
= &state
->children
[i
];
164 if (0 == strncmp(c
->owner
,
165 request_info
->tls_client_hash
, 64)) {
170 if (slot
== NULL
|| (slot
->exists
171 && slot
->last_active
> c
->last_active
)) {
174 } else if (slot
== NULL
|| slot
->exists
) slot
= c
;
178 if (slot
== NULL
|| (slot
->exists
&& state
->num_children
< state
->max_children
)) {
179 slot
= &state
->children
[state
->num_children
++];
184 // TODO: would be nice to queue a regretful message for the owner
185 // of the child we're killing...
189 child
->exists
= false;
192 memset(child
, 0, sizeof(Child
));
194 if (request_info
->tls_client_hash
== NULL
) child
->owner
[0] = 0;
195 else strncpy(child
->owner
, request_info
->tls_client_hash
, 64);
197 if (!spawn(state
->command
, state
->args
, child
, socket
)) {
198 put("40 Spawn failure.\r\n");
202 set_child_last_active(child
);
203 child
->exists
= true;
208 const char *q
= request_info
->query_string_decoded
;
214 } else if (0 == strncmp(q
, "help", strlen(q
))) {
215 put("20 text/gemini\r\n");
216 put("An input line not beginning with '!' will be passed to the process.\r\n");
217 put("A newline will be appended unless the line ends with a trailing backslash.\r\n");
219 put("# gemrepl meta commands\r\n");
220 put("=> ?!help !help: This help\r\n");
221 put("=> ?!restart !restart: kill process and start again\r\n");
222 put("=> ?!nolink !nolink: suppress input link\r\n");
223 put("=> ?!showlink !showlink: show input link\r\n");
224 put("=> ?!? !?: Prompt for input\r\n");
225 put("=> ?!! !!: Literal '!'\r\n");
227 } else if (0 == strncmp(q
, "restart", strlen(q
))) {
228 kill(child
->pid
, SIGKILL
);
230 } else if (0 == strncmp(q
, "nolink", strlen(q
))) {
231 // TODO: might be better to have this be a permanent option
232 // attached to the cert rather than the child.
233 child
->nolink
= true;
234 put("20 text/gemini\r\n");
235 put("Input links disabled.\r\n");
236 put("=> ?!showlink re-enable\r\n");
238 } else if (0 == strncmp(q
, "showlink", strlen(q
))) {
239 child
->nolink
= false;
240 put("20 text/gemini\r\n");
241 put("Input links enabled.\r\n");
242 put("=> ?!? Input command\r\n");
244 } else if (*q
!= '!') {
245 put("40 Unknown gemrepl meta-command (use '!!' for a literal '!')\r\n");
250 put("20 text/gemini\r\n");
252 put("[gemrepl child spawned. Input \"!help\" for meta-commands]\r\n");
255 if (!child
->nolink
) put("=> ?!? Input command\r\n");
257 if (!spawned
) kill(child
->pid
, SIGCONT
);
259 int qlen
= strlen(q
);
260 if (!spawned
|| qlen
> 0) {
262 if (q
[qlen
-1] == '\\') {
266 signal(SIGPIPE
, SIG_IGN
);
267 bool succ
= (write(child
->in
, q
, qlen
) == qlen
268 && (!newline
|| write(child
->in
, "\n", 1) == 1));
269 signal(SIGPIPE
, SIG_DFL
);
271 put("[gemrepl: error when writing to child]\r\n");
276 const int succ
= stream_text(child
->out
, socket
, true, state
->read_timeout
, state
->pause_timeout
);
278 if (succ
< 0) put("[gemrepl: error when reading from child]\r\n");
279 else if (succ
== 0) {
280 // got HUP; sleep briefly to give child a chance to exit
284 set_child_last_active(child
);
286 if (waitpid(child
->pid
, NULL
, WNOHANG
) == child
->pid
) {
287 put("[gemrepl: child process terminated]");
290 child
->exists
= false;
292 kill(child
->pid
, SIGSTOP
);
296 /* How long in ms to wait for child to output something */
297 #define DEF_READ_TIMEOUT 5000
299 /* How long in ms child can pause between writes before we consider it to have
300 * finished writing */
301 #define DEF_PAUSE_TIMEOUT 500
305 printf("Usage: gemrepl [OPTION]... -s PATH COMMAND [ARG]...\n");
306 printf(" -h --help This help\n");
307 printf(" -s PATH --socket=PATH Path for socket file, which will be created\n");
308 printf(" -m NUM --max-children=NUM Max concurrent children to spawn (%d)\n", MAX_CHILDREN
);
309 printf(" -t MS --read-timeout=MS Time to wait for child to start writing (%d)\n", DEF_READ_TIMEOUT
);
310 printf(" -T MS --pause-timeout=MS Silence period after which child is paused (%d)\n", DEF_PAUSE_TIMEOUT
);
314 int main(int argc
, char **argv
)
321 State
*state
= malloc(sizeof(State
));
323 fprintf(stderr
, "Failed to allocate memory for state.");
327 state
->max_children
= MAX_CHILDREN
;
328 state
->read_timeout
= 5000;
329 state
->pause_timeout
= 500;
331 const struct option longoptions
[] =
332 { { "help", 0, NULL
, 'h' }
333 , { "socket", 1, NULL
, 's' }
334 , { "max-children", 1, NULL
, 'm' }
335 , { "read-timeout", 1, NULL
, 't' }
336 , { "pause-timeout", 1, NULL
, 'T' }
340 const char *socketname
= NULL
;
341 while (-1 != (o
= getopt_long(argc
, argv
, "+hs:m:t:T:", longoptions
, NULL
))) {
351 state
->max_children
= atoi(optarg
);
352 if (state
->max_children
<= 0 || state
->max_children
> MAX_CHILDREN
) {
353 printf("Bad value for max children.\n");
354 printf("You may need to increase MAX_CHILDREN in the source.\n");
359 state
->read_timeout
= atoi(optarg
);
360 if (state
->read_timeout
< 0) {
361 printf("Bad value for read timeout.\n");
366 state
->pause_timeout
= atoi(optarg
);
367 if (state
->pause_timeout
< 0) {
368 printf("Bad value for pause timeout.\n");
375 if (argv
[optind
] == NULL
|| socketname
== NULL
) {
380 state
->command
= argv
[optind
];
381 state
->args
= &argv
[optind
];
383 runSCGI(socketname
, respond
, state
);