]>
Commit | Line | Data |
---|---|---|
7460295f MG |
1 | // shellinaboxd.c -- A custom web server that makes command line applications |
2 | // available as AJAX web applications. | |
e0bb8a33 | 3 | // Copyright (C) 2008-2009 Markus Gutschke <markus@shellinabox.com> |
7460295f MG |
4 | // |
5 | // This program is free software; you can redistribute it and/or modify | |
6 | // it under the terms of the GNU General Public License version 2 as | |
7 | // published by the Free Software Foundation. | |
8 | // | |
9 | // This program is distributed in the hope that it will be useful, | |
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of | |
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
12 | // GNU General Public License for more details. | |
13 | // | |
14 | // You should have received a copy of the GNU General Public License along | |
15 | // with this program; if not, write to the Free Software Foundation, Inc., | |
16 | // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | |
17 | // | |
18 | // In addition to these license terms, the author grants the following | |
19 | // additional rights: | |
20 | // | |
21 | // If you modify this program, or any covered work, by linking or | |
22 | // combining it with the OpenSSL project's OpenSSL library (or a | |
23 | // modified version of that library), containing parts covered by the | |
24 | // terms of the OpenSSL or SSLeay licenses, the author | |
25 | // grants you additional permission to convey the resulting work. | |
26 | // Corresponding Source for a non-source form of such a combination | |
27 | // shall include the source code for the parts of OpenSSL used as well | |
28 | // as that of the covered work. | |
29 | // | |
30 | // You may at your option choose to remove this additional permission from | |
31 | // the work, or from any part of it. | |
32 | // | |
33 | // It is possible to build this program in a way that it loads OpenSSL | |
34 | // libraries at run-time. If doing so, the following notices are required | |
35 | // by the OpenSSL and SSLeay licenses: | |
36 | // | |
37 | // This product includes software developed by the OpenSSL Project | |
38 | // for use in the OpenSSL Toolkit. (http://www.openssl.org/) | |
39 | // | |
40 | // This product includes cryptographic software written by Eric Young | |
41 | // (eay@cryptsoft.com) | |
42 | // | |
43 | // | |
44 | // The most up-to-date version of this program is always available from | |
45 | // http://shellinabox.com | |
46 | ||
47 | #define _GNU_SOURCE | |
bdd01e84 | 48 | #include "config.h" |
7460295f MG |
49 | |
50 | #include <fcntl.h> | |
51 | #include <getopt.h> | |
52 | #include <limits.h> | |
53 | #include <locale.h> | |
54 | #include <poll.h> | |
9b850878 | 55 | #include <stdarg.h> |
7460295f MG |
56 | #include <stdio.h> |
57 | #include <stdlib.h> | |
58 | #include <string.h> | |
d1edcc0e | 59 | #include <sys/resource.h> |
7460295f MG |
60 | #include <sys/types.h> |
61 | #include <sys/stat.h> | |
62 | #include <unistd.h> | |
63 | ||
47e62a9c MG |
64 | #ifdef HAVE_SYS_PRCTL_H |
65 | #include <sys/prctl.h> | |
66 | #endif | |
67 | ||
7460295f MG |
68 | #include "libhttp/http.h" |
69 | #include "logging/logging.h" | |
70 | #include "shellinabox/externalfile.h" | |
71 | #include "shellinabox/launcher.h" | |
72 | #include "shellinabox/privileges.h" | |
73 | #include "shellinabox/service.h" | |
74 | #include "shellinabox/session.h" | |
ecbff9b9 | 75 | #include "shellinabox/usercss.h" |
7460295f MG |
76 | |
77 | #define PORTNUM 4200 | |
78 | #define MAX_RESPONSE 2048 | |
79 | ||
ecbff9b9 MG |
80 | static int port; |
81 | static int portMin; | |
82 | static int portMax; | |
83 | static int localhostOnly = 0; | |
84 | static int noBeep = 0; | |
85 | static int numericHosts = 0; | |
86 | static int enableSSL = 1; | |
87 | static int enableSSLMenu = 1; | |
88 | static int linkifyURLs = 1; | |
89 | static char *certificateDir; | |
90 | static int certificateFd = -1; | |
91 | static HashMap *externalFiles; | |
92 | static Server *cgiServer; | |
93 | static char *cgiSessionKey; | |
94 | static int cgiSessions; | |
95 | static char *cssStyleSheet; | |
96 | static struct UserCSS *userCSSList; | |
7460295f MG |
97 | |
98 | static char *jsonEscape(const char *buf, int len) { | |
99 | static const char *hexDigit = "0123456789ABCDEF"; | |
100 | ||
101 | // Determine the space that is needed to encode the buffer | |
102 | int count = 0; | |
103 | const char *ptr = buf; | |
104 | for (int i = 0; i < len; i++) { | |
105 | unsigned char ch = *(unsigned char *)ptr++; | |
106 | if (ch < ' ') { | |
107 | switch (ch) { | |
108 | case '\b': case '\f': case '\n': case '\r': case '\t': | |
109 | count += 2; | |
110 | break; | |
111 | default: | |
112 | count += 6; | |
113 | break; | |
114 | } | |
115 | } else if (ch == '"' || ch == '\\' || ch == '/') { | |
116 | count += 2; | |
117 | } else if (ch > '\x7F') { | |
118 | count += 6; | |
119 | } else { | |
120 | count++; | |
121 | } | |
122 | } | |
123 | ||
124 | // Encode the buffer using JSON string escaping | |
125 | char *result; | |
126 | check(result = malloc(count + 1)); | |
127 | char *dst = result; | |
128 | ptr = buf; | |
129 | for (int i = 0; i < len; i++) { | |
130 | unsigned char ch = *(unsigned char *)ptr++; | |
131 | if (ch < ' ') { | |
132 | *dst++ = '\\'; | |
133 | switch (ch) { | |
134 | case '\b': *dst++ = 'b'; break; | |
135 | case '\f': *dst++ = 'f'; break; | |
136 | case '\n': *dst++ = 'n'; break; | |
137 | case '\r': *dst++ = 'r'; break; | |
138 | case '\t': *dst++ = 't'; break; | |
139 | default: | |
140 | unicode: | |
141 | *dst++ = 'u'; | |
142 | *dst++ = '0'; | |
143 | *dst++ = '0'; | |
144 | *dst++ = hexDigit[ch >> 4]; | |
145 | *dst++ = hexDigit[ch & 0xF]; | |
146 | break; | |
147 | } | |
148 | } else if (ch == '"' || ch == '\\' || ch == '/') { | |
149 | *dst++ = '\\'; | |
150 | *dst++ = ch; | |
151 | } else if (ch > '\x7F') { | |
152 | *dst++ = '\\'; | |
153 | goto unicode; | |
154 | } else { | |
155 | *dst++ = ch; | |
156 | } | |
157 | } | |
158 | *dst++ = '\000'; | |
159 | return result; | |
160 | } | |
161 | ||
9b850878 MG |
162 | static int printfUnchecked(const char *format, ...) { |
163 | // Some Linux distributions enable -Wformat=2 by default. This is a | |
164 | // very unfortunate decision, as that option generates a lot of false | |
165 | // positives. We try to work around the problem by defining an unchecked | |
166 | // version of "printf()" | |
167 | va_list ap; | |
168 | va_start(ap, format); | |
169 | int rc = vprintf(format, ap); | |
170 | va_end(ap); | |
171 | return rc; | |
172 | } | |
173 | ||
7460295f | 174 | static int completePendingRequest(struct Session *session, |
5e56158a | 175 | const char *buf, int len, int maxLength) { |
7460295f MG |
176 | // If there is no pending HTTP request, save the data and return |
177 | // immediately. | |
178 | if (!session->http) { | |
179 | if (len) { | |
180 | if (session->buffered) { | |
181 | check(session->buffered = realloc(session->buffered, | |
182 | session->len + len)); | |
183 | memcpy(session->buffered + session->len, buf, len); | |
184 | session->len += len; | |
185 | } else { | |
186 | check(session->buffered = malloc(len)); | |
187 | memcpy(session->buffered, buf, len); | |
188 | session->len = len; | |
189 | } | |
190 | } | |
191 | } else { | |
192 | // If we have a pending HTTP request, we can reply to it, now. | |
193 | char *data; | |
194 | if (session->buffered) { | |
195 | check(session->buffered = realloc(session->buffered, | |
196 | session->len + len)); | |
197 | memcpy(session->buffered + session->len, buf, len); | |
198 | session->len += len; | |
199 | if (maxLength > 0 && session->len > maxLength) { | |
200 | data = jsonEscape(session->buffered, maxLength); | |
201 | session->len -= maxLength; | |
202 | memmove(session->buffered, session->buffered + maxLength, | |
203 | session->len); | |
204 | } else { | |
205 | data = jsonEscape(session->buffered, session->len); | |
206 | free(session->buffered); | |
207 | session->buffered = NULL; | |
208 | session->len = 0; | |
209 | } | |
210 | } else { | |
211 | if (maxLength > 0 && len > maxLength) { | |
212 | session->len = len - maxLength; | |
213 | check(session->buffered = malloc(session->len)); | |
214 | memcpy(session->buffered, buf + maxLength, session->len); | |
215 | data = jsonEscape(buf, maxLength); | |
216 | } else { | |
217 | data = jsonEscape(buf, len); | |
218 | } | |
219 | } | |
220 | ||
221 | char *json = stringPrintf(NULL, "{" | |
222 | "\"session\":\"%s\"," | |
223 | "\"data\":\"%s\"" | |
224 | "}", | |
225 | session->sessionKey, data); | |
226 | free(data); | |
227 | HttpConnection *http = session->http; | |
228 | char *response = stringPrintf(NULL, | |
229 | "HTTP/1.1 200 OK\r\n" | |
230 | "Content-Type: application/json; " | |
231 | "charset=utf-8\r\n" | |
7933e348 | 232 | "Content-Length: %ld\r\n" |
38d64ad8 | 233 | "Cache-Control: no-cache\r\n" |
7460295f MG |
234 | "\r\n" |
235 | "%s", | |
7933e348 | 236 | (long)strlen(json), |
7460295f MG |
237 | strcmp(httpGetMethod(http), |
238 | "HEAD") ? json : ""); | |
239 | free(json); | |
240 | session->http = NULL; | |
241 | httpTransfer(http, response, strlen(response)); | |
242 | } | |
243 | if (session->done && !session->buffered) { | |
244 | finishSession(session); | |
245 | return 0; | |
246 | } | |
247 | return 1; | |
248 | } | |
249 | ||
250 | static void sessionDone(void *arg) { | |
251 | debug("Child terminated"); | |
252 | struct Session *session = (struct Session *)arg; | |
253 | session->done = 1; | |
254 | addToGraveyard(session); | |
255 | completePendingRequest(session, "", 0, INT_MAX); | |
256 | } | |
257 | ||
258 | static int handleSession(struct ServerConnection *connection, void *arg, | |
259 | short *events, short revents) { | |
260 | struct Session *session = (struct Session *)arg; | |
261 | session->connection = connection; | |
262 | int len = MAX_RESPONSE - session->len; | |
263 | if (len <= 0) { | |
264 | len = 1; | |
265 | } | |
266 | char buf[len]; | |
267 | int bytes = 0; | |
268 | if (revents & POLLIN) { | |
269 | bytes = NOINTR(read(session->pty, buf, len)); | |
270 | if (bytes <= 0) { | |
271 | return 0; | |
272 | } | |
273 | } | |
274 | int timedOut = serverGetTimeout(connection) < 0; | |
275 | if (bytes || timedOut) { | |
276 | if (!session->http && timedOut) { | |
277 | debug("Timeout. Closing session."); | |
278 | return 0; | |
279 | } | |
280 | check(!session->done); | |
281 | check(completePendingRequest(session, buf, bytes, MAX_RESPONSE)); | |
30046882 MG |
282 | connection = serverGetConnection(session->server, |
283 | connection, | |
284 | session->pty); | |
285 | session->connection = connection; | |
7460295f MG |
286 | if (session->len >= MAX_RESPONSE) { |
287 | serverConnectionSetEvents(session->server, connection, 0); | |
288 | } | |
289 | serverSetTimeout(connection, AJAX_TIMEOUT); | |
290 | return 1; | |
291 | } else { | |
292 | return 0; | |
293 | } | |
294 | } | |
295 | ||
296 | static int invalidatePendingHttpSession(void *arg, const char *key, | |
297 | char **value) { | |
298 | struct Session *session = *(struct Session **)value; | |
299 | if (session->http && session->http == (HttpConnection *)arg) { | |
300 | debug("Clearing pending HTTP connection for session %s", key); | |
301 | session->http = NULL; | |
302 | serverDeleteConnection(session->server, session->pty); | |
303 | ||
304 | // Return zero in order to remove this HTTP from the "session" hashmap | |
305 | return 0; | |
306 | } | |
307 | ||
308 | // If the session is still in use, do not remove it from the "sessions" map | |
309 | return 1; | |
310 | } | |
311 | ||
312 | static int dataHandler(HttpConnection *http, struct Service *service, | |
313 | const char *buf, int len, URL *url) { | |
314 | if (!buf) { | |
315 | // Somebody unexpectedly closed our http connection (e.g. because of a | |
316 | // timeout). This is the last notification that we will get. | |
317 | deleteURL(url); | |
318 | iterateOverSessions(invalidatePendingHttpSession, http); | |
319 | return HTTP_DONE; | |
320 | } | |
321 | ||
322 | // Find an existing session, or create the record for a new one | |
323 | int isNew; | |
3240f75b | 324 | struct Session *session = findCGISession(&isNew, http, url, cgiSessionKey); |
7460295f | 325 | if (session == NULL) { |
47e62a9c | 326 | httpSendReply(http, 400, "Bad Request", NO_MSG); |
7460295f MG |
327 | return HTTP_DONE; |
328 | } | |
329 | ||
3240f75b | 330 | // Sanity check |
7460295f MG |
331 | if (!isNew && strcmp(session->peerName, httpGetPeerName(http))) { |
332 | error("Peername changed from %s to %s", | |
333 | session->peerName, httpGetPeerName(http)); | |
47e62a9c | 334 | httpSendReply(http, 400, "Bad Request", NO_MSG); |
7460295f MG |
335 | return HTTP_DONE; |
336 | } | |
337 | ||
338 | const HashMap *args = urlGetArgs(session->url); | |
339 | int oldWidth = session->width; | |
340 | int oldHeight = session->height; | |
341 | const char *width = getFromHashMap(args, "width"); | |
342 | const char *height = getFromHashMap(args, "height"); | |
343 | const char *keys = getFromHashMap(args, "keys"); | |
344 | ||
345 | // Adjust window dimensions if provided by client | |
346 | if (width && height) { | |
347 | session->width = atoi(width); | |
348 | session->height = atoi(height); | |
349 | } | |
350 | ||
3240f75b MG |
351 | // Create a new session, if the client did not provide an existing one |
352 | if (isNew) { | |
d5324103 MG |
353 | if (keys) { |
354 | bad_new_session: | |
3240f75b | 355 | abandonSession(session); |
47e62a9c | 356 | httpSendReply(http, 400, "Bad Request", NO_MSG); |
7460295f MG |
357 | return HTTP_DONE; |
358 | } | |
d5324103 MG |
359 | |
360 | if (cgiServer && cgiSessions++) { | |
361 | serverExitLoop(cgiServer, 1); | |
362 | goto bad_new_session; | |
363 | } | |
7460295f | 364 | session->http = http; |
3240f75b MG |
365 | if (launchChild(service->id, session) < 0) { |
366 | abandonSession(session); | |
47e62a9c | 367 | httpSendReply(http, 500, "Internal Error", NO_MSG); |
3240f75b MG |
368 | return HTTP_DONE; |
369 | } | |
370 | if (cgiServer) { | |
371 | terminateLauncher(); | |
372 | } | |
373 | session->connection = serverAddConnection(httpGetServer(http), | |
7460295f MG |
374 | session->pty, handleSession, |
375 | sessionDone, session); | |
3240f75b | 376 | serverSetTimeout(session->connection, AJAX_TIMEOUT); |
7460295f MG |
377 | } |
378 | ||
379 | // Reset window dimensions of the pseudo TTY, if changed since last time set. | |
380 | if (session->width > 0 && session->height > 0 && | |
381 | (session->width != oldWidth || session->height != oldHeight)) { | |
382 | debug("Window size changed to %dx%d", session->width, session->height); | |
383 | setWindowSize(session->pty, session->width, session->height); | |
384 | } | |
385 | ||
386 | // Process keypresses, if any. Then send a synchronous reply. | |
387 | if (keys) { | |
388 | char *keyCodes; | |
389 | check(keyCodes = malloc(strlen(keys)/2)); | |
390 | int len = 0; | |
391 | for (const unsigned char *ptr = (const unsigned char *)keys; ;) { | |
392 | unsigned c0 = *ptr++; | |
393 | if (c0 < '0' || (c0 > '9' && c0 < 'A') || | |
394 | (c0 > 'F' && c0 < 'a') || c0 > 'f') { | |
395 | break; | |
396 | } | |
397 | unsigned c1 = *ptr++; | |
398 | if (c1 < '0' || (c1 > '9' && c1 < 'A') || | |
399 | (c1 > 'F' && c1 < 'a') || c1 > 'f') { | |
400 | break; | |
401 | } | |
402 | keyCodes[len++] = 16*((c0 & 0xF) + 9*(c0 > '9')) + | |
403 | (c1 & 0xF) + 9*(c1 > '9'); | |
404 | } | |
405 | if (write(session->pty, keyCodes, len) < 0 && errno == EAGAIN) { | |
406 | completePendingRequest(session, "\007", 1, MAX_RESPONSE); | |
407 | } | |
408 | free(keyCodes); | |
409 | httpSendReply(http, 200, "OK", " "); | |
0fae962f | 410 | check(session->http != http); |
7460295f | 411 | return HTTP_DONE; |
3240f75b MG |
412 | } else { |
413 | // This request is polling for data. Finish any pending requests and | |
414 | // queue (or process) a new one. | |
415 | if (session->http && session->http != http && | |
416 | !completePendingRequest(session, "", 0, MAX_RESPONSE)) { | |
47e62a9c | 417 | httpSendReply(http, 400, "Bad Request", NO_MSG); |
3240f75b MG |
418 | return HTTP_DONE; |
419 | } | |
420 | session->http = http; | |
7460295f MG |
421 | } |
422 | ||
423 | session->connection = serverGetConnection(session->server, | |
424 | session->connection, | |
425 | session->pty); | |
0fae962f | 426 | if (session->buffered || isNew) { |
7460295f MG |
427 | if (completePendingRequest(session, "", 0, MAX_RESPONSE) && |
428 | session->connection) { | |
429 | // Reset the timeout, as we just received a new request. | |
430 | serverSetTimeout(session->connection, AJAX_TIMEOUT); | |
431 | if (session->len < MAX_RESPONSE) { | |
432 | // Re-enable input on the child's pty | |
433 | serverConnectionSetEvents(session->server, session->connection,POLLIN); | |
434 | } | |
435 | } | |
436 | return HTTP_DONE; | |
437 | } else if (session->connection) { | |
438 | // Re-enable input on the child's pty | |
439 | serverConnectionSetEvents(session->server, session->connection, POLLIN); | |
440 | serverSetTimeout(session->connection, AJAX_TIMEOUT); | |
441 | } | |
442 | ||
443 | return HTTP_SUSPEND; | |
444 | } | |
445 | ||
446 | static void serveStaticFile(HttpConnection *http, const char *contentType, | |
447 | const char *start, const char *end) { | |
8e5d3913 MG |
448 | char *body = (char *)start; |
449 | char *bodyEnd = (char *)end; | |
450 | ||
451 | // Unfortunately, there are still some browsers that are so buggy that they | |
452 | // need special conditional code. In anything that has a "text" MIME type, | |
453 | // we allow simple conditionals. Nested conditionals are not supported. | |
454 | if (!memcmp(contentType, "text/", 5)) { | |
455 | char *tag = NULL; | |
456 | int condTrue = -1; | |
457 | char *ifPtr = NULL; | |
458 | char *elsePtr = NULL; | |
459 | for (char *ptr = body; bodyEnd - ptr >= 6; ) { | |
460 | char *eol = ptr; | |
461 | eol = memchr(eol, '\n', bodyEnd - eol); | |
462 | if (eol == NULL) { | |
463 | eol = bodyEnd; | |
464 | } else { | |
465 | ++eol; | |
466 | } | |
467 | if (!memcmp(ptr, "[if ", 4)) { | |
468 | char *bracket = memchr(ptr + 4, ']', eol - ptr - 4); | |
469 | if (bracket != NULL && bracket > ptr + 4) { | |
470 | check(tag = malloc(bracket - ptr - 3)); | |
471 | memcpy(tag, ptr + 4, bracket - ptr - 4); | |
472 | tag[bracket - ptr - 4] = '\000'; | |
473 | condTrue = 0; | |
474 | const char *userAgent = getFromHashMap(httpGetHeaders(http), | |
475 | "user-agent"); | |
476 | if (userAgent) { | |
477 | // Allow multiple comma separated conditions | |
478 | for (char *tagPtr = tag; *tagPtr; ) { | |
479 | char *e = strchr(tagPtr, ','); | |
480 | if (!e) { | |
481 | e = strchr(tag, '\000'); | |
482 | } else { | |
483 | *e++ = '\000'; | |
484 | } | |
485 | condTrue = userCSSGetDefine(tagPtr) || | |
486 | strstr(userAgent, tagPtr) != NULL; | |
487 | if (*e) { | |
488 | e[-1] = ','; | |
489 | } | |
490 | if (condTrue) { | |
491 | break; | |
492 | } | |
493 | tagPtr = e; | |
494 | } | |
495 | } | |
496 | ||
497 | // If we find any conditionals, then we need to make a copy of | |
498 | // the text document. We do this lazily, as presumably the majority | |
499 | // of text documents won't have conditionals. | |
500 | if (body == start) { | |
501 | check(body = malloc(end - start)); | |
502 | memcpy(body, start, end - start); | |
503 | bodyEnd += body - start; | |
504 | ptr += body - start; | |
505 | eol += body - start; | |
506 | } | |
507 | ||
508 | // Remember the beginning of the "[if ...]" statement | |
509 | ifPtr = ptr; | |
510 | } | |
511 | } else if (ifPtr && !elsePtr && eol - ptr >= strlen(tag) + 7 && | |
512 | !memcmp(ptr, "[else ", 6) && | |
513 | !memcmp(ptr + 6, tag, strlen(tag)) && | |
514 | ptr[6 + strlen(tag)] == ']') { | |
515 | // Found an "[else ...]" statement. Remember where it started. | |
516 | elsePtr = ptr; | |
517 | } else if (ifPtr && eol - ptr >= strlen(tag) + 8 && | |
518 | !memcmp(ptr, "[endif ", 7) && | |
519 | !memcmp(ptr + 7, tag, strlen(tag)) && | |
520 | ptr[7 + strlen(tag)] == ']') { | |
521 | // Found the closing "[endif ...]" statement. Now we can remove those | |
522 | // parts of the conditionals that do not apply to this user agent. | |
523 | char *s, *e; | |
524 | if (condTrue) { | |
525 | s = strchr(ifPtr, '\n') + 1; | |
526 | e = elsePtr ? elsePtr : ptr; | |
527 | } else { | |
528 | if (elsePtr) { | |
529 | s = strchr(elsePtr, '\n') + 1; | |
530 | e = ptr; | |
531 | } else { | |
532 | s = ifPtr; | |
533 | e = ifPtr; | |
534 | } | |
535 | } | |
536 | memmove(ifPtr, s, e - s); | |
537 | memmove(ifPtr + (e - s), eol, bodyEnd - eol); | |
538 | bodyEnd -= (s - ifPtr) + (eol - e); | |
539 | eol = ifPtr + (e - s); | |
540 | ifPtr = NULL; | |
541 | elsePtr = NULL; | |
542 | free(tag); | |
543 | tag = NULL; | |
544 | } | |
545 | ptr = eol; | |
546 | } | |
547 | free(tag); | |
548 | } | |
549 | ||
7460295f MG |
550 | char *response = stringPrintf(NULL, |
551 | "HTTP/1.1 200 OK\r\n" | |
552 | "Content-Type: %s\r\n" | |
7933e348 | 553 | "Content-Length: %ld\r\n" |
8e5d3913 MG |
554 | "%s\r\n", |
555 | contentType, (long)(bodyEnd - body), | |
556 | body == start ? "" : | |
557 | "Cache-Control: no-cache\r\n"); | |
7460295f MG |
558 | int len = strlen(response); |
559 | if (strcmp(httpGetMethod(http), "HEAD")) { | |
8e5d3913 MG |
560 | check(response = realloc(response, len + (bodyEnd - body))); |
561 | memcpy(response + len, body, bodyEnd - body); | |
562 | len += bodyEnd - body; | |
563 | } | |
564 | ||
565 | // If we expanded conditionals, we had to create a temporary copy. Delete | |
566 | // it now. | |
567 | if (body != start) { | |
568 | free(body); | |
7460295f | 569 | } |
8e5d3913 | 570 | |
7460295f MG |
571 | httpTransfer(http, response, len); |
572 | } | |
573 | ||
572ac014 MG |
574 | static const char *addr(const char *a) { |
575 | // Work-around for a gcc bug that could occasionally generate invalid | |
576 | // assembly instructions when optimizing code too agressively. | |
577 | asm volatile(""); | |
578 | return a; | |
579 | } | |
580 | ||
7460295f MG |
581 | static int shellInABoxHttpHandler(HttpConnection *http, void *arg, |
582 | const char *buf, int len) { | |
583 | checkGraveyard(); | |
584 | URL *url = newURL(http, buf, len); | |
585 | const HashMap *headers = httpGetHeaders(http); | |
586 | const char *contentType = getFromHashMap(headers, "content-type"); | |
587 | ||
588 | // Normalize the path info | |
589 | const char *pathInfo = urlGetPathInfo(url); | |
590 | while (*pathInfo == '/') { | |
591 | pathInfo++; | |
592 | } | |
593 | const char *endPathInfo; | |
594 | for (endPathInfo = pathInfo; | |
595 | *endPathInfo && *endPathInfo != '/'; | |
596 | endPathInfo++) { | |
597 | } | |
598 | int pathInfoLength = endPathInfo - pathInfo; | |
11cd1451 | 599 | |
1e1c8024 MG |
600 | if (!pathInfoLength || |
601 | (pathInfoLength == 5 && !memcmp(pathInfo, "plain", 5)) || | |
602 | (pathInfoLength == 6 && !memcmp(pathInfo, "secure", 6))) { | |
603 | // The root page serves the AJAX application. | |
7460295f MG |
604 | if (contentType && |
605 | !strncasecmp(contentType, "application/x-www-form-urlencoded", 33)) { | |
606 | // XMLHttpRequest carrying data between the AJAX application and the | |
607 | // client session. | |
608 | return dataHandler(http, arg, buf, len, url); | |
609 | } | |
1e1c8024 MG |
610 | extern char rootPageStart[]; |
611 | extern char rootPageEnd[]; | |
612 | char *rootPage; | |
613 | check(rootPage = malloc(rootPageEnd - rootPageStart + 1)); | |
614 | memcpy(rootPage, rootPageStart, rootPageEnd - rootPageStart); | |
615 | rootPage[rootPageEnd - rootPageStart] = '\000'; | |
616 | char *html = stringPrintf(NULL, rootPage, | |
617 | enableSSL ? "true" : "false"); | |
d9c5b5b2 MG |
618 | serveStaticFile(http, "text/html", html, strrchr(html, '\000')); |
619 | free(html); | |
1e1c8024 | 620 | free(rootPage); |
7460295f MG |
621 | } else if (pathInfoLength == 8 && !memcmp(pathInfo, "beep.wav", 8)) { |
622 | // Serve the audio sample for the console bell. | |
623 | extern char beepStart[]; | |
624 | extern char beepEnd[]; | |
625 | serveStaticFile(http, "audio/x-wav", beepStart, beepEnd); | |
08db8657 MG |
626 | } else if (pathInfoLength == 11 && !memcmp(pathInfo, "enabled.gif", 11)) { |
627 | // Serve the checkmark icon used in the context menu | |
628 | extern char enabledStart[]; | |
629 | extern char enabledEnd[]; | |
630 | serveStaticFile(http, "image/gif", enabledStart, enabledEnd); | |
7460295f MG |
631 | } else if (pathInfoLength == 11 && !memcmp(pathInfo, "favicon.ico", 11)) { |
632 | // Serve the favicon | |
633 | extern char faviconStart[]; | |
634 | extern char faviconEnd[]; | |
635 | serveStaticFile(http, "image/x-icon", faviconStart, faviconEnd); | |
636 | } else if (pathInfoLength == 14 && !memcmp(pathInfo, "ShellInABox.js", 14)) { | |
637 | // Serve both vt100.js and shell_in_a_box.js in the same transaction. | |
638 | // Also, indicate to the client whether the server is SSL enabled. | |
639 | extern char vt100Start[]; | |
640 | extern char vt100End[]; | |
641 | extern char shellInABoxStart[]; | |
642 | extern char shellInABoxEnd[]; | |
ecbff9b9 | 643 | char *userCSSString = getUserCSSString(userCSSList); |
46f2036a MG |
644 | char *stateVars = stringPrintf(NULL, |
645 | "serverSupportsSSL = %s;\n" | |
b624088c | 646 | "disableSSLMenu = %s;\n" |
f0c6fd39 | 647 | "suppressAllAudio = %s;\n" |
ecbff9b9 MG |
648 | "linkifyURLs = %d;\n" |
649 | "userCSSList = %s;\n\n", | |
b624088c MG |
650 | enableSSL ? "true" : "false", |
651 | !enableSSLMenu ? "true" : "false", | |
f0c6fd39 | 652 | noBeep ? "true" : "false", |
ecbff9b9 MG |
653 | linkifyURLs, userCSSString); |
654 | free(userCSSString); | |
46f2036a MG |
655 | int stateVarsLength = strlen(stateVars); |
656 | int contentLength = stateVarsLength + | |
572ac014 MG |
657 | (addr(vt100End) - addr(vt100Start)) + |
658 | (addr(shellInABoxEnd) - addr(shellInABoxStart)); | |
7460295f MG |
659 | char *response = stringPrintf(NULL, |
660 | "HTTP/1.1 200 OK\r\n" | |
661 | "Content-Type: text/javascript; charset=utf-8\r\n" | |
662 | "Content-Length: %d\r\n" | |
663 | "\r\n", | |
664 | contentLength); | |
665 | int headerLength = strlen(response); | |
666 | if (strcmp(httpGetMethod(http), "HEAD")) { | |
667 | check(response = realloc(response, headerLength + contentLength)); | |
668 | memcpy(memcpy(memcpy( | |
46f2036a | 669 | response + headerLength, stateVars, stateVarsLength)+stateVarsLength, |
7460295f MG |
670 | vt100Start, vt100End - vt100Start) + (vt100End - vt100Start), |
671 | shellInABoxStart, shellInABoxEnd - shellInABoxStart); | |
672 | } else { | |
673 | contentLength = 0; | |
674 | } | |
46f2036a | 675 | free(stateVars); |
7460295f MG |
676 | httpTransfer(http, response, headerLength + contentLength); |
677 | } else if (pathInfoLength == 10 && !memcmp(pathInfo, "styles.css", 10)) { | |
678 | // Serve the style sheet. | |
1f771613 MG |
679 | serveStaticFile(http, "text/css; charset=utf-8", |
680 | cssStyleSheet, strrchr(cssStyleSheet, '\000')); | |
ecbff9b9 MG |
681 | } else if (pathInfoLength > 8 && !memcmp(pathInfo, "usercss-", 8)) { |
682 | // Server user style sheets (if any) | |
683 | struct UserCSS *css = userCSSList; | |
684 | for (int idx = atoi(pathInfo + 8); | |
685 | idx-- > 0 && css; css = css->next ) { | |
686 | } | |
687 | if (css) { | |
688 | serveStaticFile(http, "text/css; charset=utf-8", | |
689 | css->style, css->style + css->styleLen); | |
690 | } else { | |
691 | httpSendReply(http, 404, "File not found", NO_MSG); | |
692 | } | |
7460295f | 693 | } else { |
47e62a9c | 694 | httpSendReply(http, 404, "File not found", NO_MSG); |
7460295f MG |
695 | } |
696 | ||
697 | deleteURL(url); | |
698 | return HTTP_DONE; | |
699 | } | |
700 | ||
701 | static int strtoint(const char *s, int minVal, int maxVal) { | |
702 | char *ptr; | |
703 | if (!*s) { | |
704 | fatal("Missing numeric value."); | |
705 | } | |
706 | long l = strtol(s, &ptr, 10); | |
707 | if (*ptr || l < minVal || l > maxVal) { | |
708 | fatal("Range error on numeric value \"%s\".", s); | |
709 | } | |
710 | return l; | |
711 | } | |
712 | ||
713 | static void usage(void) { | |
714 | // Drop privileges so that we can tell which uid/gid we would normally | |
715 | // run at. | |
716 | dropPrivileges(); | |
717 | uid_t r_uid, e_uid, s_uid; | |
718 | uid_t r_gid, e_gid, s_gid; | |
719 | check(!getresuid(&r_uid, &e_uid, &s_uid)); | |
720 | check(!getresgid(&r_gid, &e_gid, &s_gid)); | |
721 | const char *user = getUserName(r_uid); | |
722 | const char *group = getGroupName(r_gid); | |
723 | ||
724 | message("Usage: shellinaboxd [OPTIONS]...\n" | |
725 | "Starts an HTTP server that serves terminal emulators to AJAX " | |
726 | "enabled browsers.\n" | |
727 | "\n" | |
728 | "List of command line options:\n" | |
729 | " -b, --background[=PIDFILE] run in background\n" | |
730 | "%s" | |
1f771613 | 731 | " --css=FILE attach contents to CSS style sheet\n" |
d1edcc0e | 732 | " --cgi[=PORTMIN-PORTMAX] run as CGI\n" |
7460295f MG |
733 | " -d, --debug enable debug mode\n" |
734 | " -f, --static-file=URL:FILE serve static file from URL path\n" | |
735 | " -g, --group=GID switch to this group (default: %s)\n" | |
736 | " -h, --help print this message\n" | |
f0c6fd39 | 737 | " --linkify=[none|normal|agressive] default is \"normal\"\n" |
53b27f78 | 738 | " --localhost-only only listen on 127.0.0.1\n" |
46f2036a | 739 | " --no-beep suppress all audio output\n" |
7460295f MG |
740 | " -n, --numeric do not resolve hostnames\n" |
741 | " -p, --port=PORT select a port (default: %d)\n" | |
742 | " -s, --service=SERVICE define one or more services\n" | |
743 | "%s" | |
744 | " -q, --quiet turn off all messages\n" | |
745 | " -u, --user=UID switch to this user (default: %s)\n" | |
ecbff9b9 | 746 | " --user-css=STYLES defines user-selectable CSS options\n" |
7460295f MG |
747 | " -v, --verbose enable logging messages\n" |
748 | " --version prints version information\n" | |
749 | "\n" | |
750 | "Debug, quiet, and verbose are mutually exclusive.\n" | |
751 | "\n" | |
752 | "One or more --service arguments define services that should " | |
ecbff9b9 | 753 | "be made available\n" |
7460295f MG |
754 | "through the web interface:\n" |
755 | " SERVICE := <url-path> ':' APP\n" | |
78016c46 MG |
756 | " APP := 'LOGIN' | 'SSH' [ : <host> ] | " |
757 | "USER ':' CWD ':' <cmdline>\n" | |
7460295f MG |
758 | " USER := %s<username> ':' <groupname>\n" |
759 | " CWD := 'HOME' | <dir>\n" | |
760 | "\n" | |
761 | "<cmdline> supports variable expansion:\n" | |
762 | " ${columns} - number of columns\n" | |
763 | " ${gid} - gid id\n" | |
764 | " ${group} - group name\n" | |
765 | " ${home} - home directory\n" | |
766 | " ${lines} - number of rows\n" | |
767 | " ${peer} - name of remote peer\n" | |
768 | " ${uid} - user id\n" | |
ecbff9b9 MG |
769 | " ${user} - user name\n" |
770 | "\n" | |
771 | "One or more --user-css arguments define optional user-selectable " | |
772 | "CSS options.\n" | |
773 | "These options show up in the right-click context menu:\n" | |
774 | " STYLES := GROUP { ';' GROUP }*\n" | |
775 | " GROUP := OPTION { ',' OPTION }*\n" | |
776 | " OPTION := <label> ':' [ '-' | '+' ] <css-file>\n" | |
777 | "\n" | |
778 | "OPTIONs that make up a GROUP are mutually exclusive. But " | |
779 | "individual GROUPs are\n" | |
780 | "independent of each other.\n", | |
7460295f MG |
781 | !serverSupportsSSL() ? "" : |
782 | " -c, --cert=CERTDIR set certificate dir " | |
dc6575f2 | 783 | "(default: $PWD)\n" |
52404edf | 784 | " --cert-fd=FD set certificate file from fd\n", |
7460295f MG |
785 | group, PORTNUM, |
786 | !serverSupportsSSL() ? "" : | |
b624088c | 787 | " -t, --disable-ssl disable transparent SSL support\n" |
52404edf | 788 | " --disable-ssl-menu disallow changing transport mode\n", |
7460295f MG |
789 | user, supportsPAM() ? "'AUTH' | " : ""); |
790 | free((char *)user); | |
791 | free((char *)group); | |
792 | } | |
793 | ||
794 | static void destroyExternalFileHashEntry(void *arg, char *key, char *value) { | |
795 | free(key); | |
796 | free(value); | |
797 | } | |
798 | ||
799 | static void parseArgs(int argc, char * const argv[]) { | |
800 | int hasSSL = serverSupportsSSL(); | |
801 | if (!hasSSL) { | |
802 | enableSSL = 0; | |
803 | } | |
804 | int demonize = 0; | |
d1edcc0e | 805 | int cgi = 0; |
7460295f MG |
806 | const char *pidfile = NULL; |
807 | int verbosity = MSG_DEFAULT; | |
808 | externalFiles = newHashMap(destroyExternalFileHashEntry, NULL); | |
d1edcc0e | 809 | HashMap *serviceTable = newHashMap(destroyServiceHashEntry, NULL); |
1f771613 MG |
810 | extern char stylesStart[]; |
811 | extern char stylesEnd[]; | |
812 | check(cssStyleSheet = malloc(stylesEnd - stylesStart)); | |
813 | memcpy(cssStyleSheet, stylesStart, stylesEnd - stylesStart); | |
814 | cssStyleSheet[stylesEnd - stylesStart] = '\000'; | |
815 | ||
7460295f | 816 | for (;;) { |
d1edcc0e | 817 | static const char optstring[] = "+hb::c:df:g:np:s:tqu:v"; |
7460295f | 818 | static struct option options[] = { |
b624088c MG |
819 | { "help", 0, 0, 'h' }, |
820 | { "background", 2, 0, 'b' }, | |
821 | { "cert", 1, 0, 'c' }, | |
822 | { "cert-fd", 1, 0, 0 }, | |
1f771613 | 823 | { "css", 1, 0, 0 }, |
b624088c MG |
824 | { "cgi", 2, 0, 0 }, |
825 | { "debug", 0, 0, 'd' }, | |
826 | { "static-file", 1, 0, 'f' }, | |
827 | { "group", 1, 0, 'g' }, | |
f0c6fd39 | 828 | { "linkify", 1, 0, 0 }, |
53b27f78 | 829 | { "localhost-only", 0, 0, 0 }, |
b624088c MG |
830 | { "no-beep", 0, 0, 0 }, |
831 | { "numeric", 0, 0, 'n' }, | |
832 | { "port", 1, 0, 'p' }, | |
833 | { "service", 1, 0, 's' }, | |
834 | { "disable-ssl", 0, 0, 't' }, | |
835 | { "disable-ssl-menu", 0, 0, 0 }, | |
836 | { "quiet", 0, 0, 'q' }, | |
837 | { "user", 1, 0, 'u' }, | |
ecbff9b9 | 838 | { "user-css", 1, 0, 0 }, |
b624088c MG |
839 | { "verbose", 0, 0, 'v' }, |
840 | { "version", 0, 0, 0 }, | |
841 | { 0, 0, 0, 0 } }; | |
7460295f MG |
842 | int idx = -1; |
843 | int c = getopt_long(argc, argv, optstring, options, &idx); | |
844 | if (c > 0) { | |
845 | for (int i = 0; options[i].name; i++) { | |
846 | if (options[i].val == c) { | |
847 | idx = i; | |
848 | break; | |
849 | } | |
850 | } | |
851 | } else if (c < 0) { | |
852 | break; | |
853 | } | |
854 | if (idx-- <= 0) { | |
855 | // Help (or invalid argument) | |
856 | usage(); | |
ecbff9b9 | 857 | if (idx < -1) { |
56d6406f MG |
858 | fatal("Failed to parse command line"); |
859 | } | |
860 | exit(0); | |
7460295f MG |
861 | } else if (!idx--) { |
862 | // Background | |
d1edcc0e MG |
863 | if (cgi) { |
864 | fatal("CGI and background operations are mutually exclusive"); | |
865 | } | |
7460295f MG |
866 | demonize = 1; |
867 | if (optarg && pidfile) { | |
868 | fatal("Only one pidfile can be given"); | |
869 | } | |
1f771613 | 870 | if (optarg && *optarg) { |
7460295f MG |
871 | pidfile = strdup(optarg); |
872 | } | |
873 | } else if (!idx--) { | |
874 | // Certificate | |
875 | if (!hasSSL) { | |
876 | warn("Ignoring certificate directory, as SSL support is unavailable"); | |
877 | } | |
50eebacb | 878 | if (certificateFd >= 0) { |
dc6575f2 MG |
879 | fatal("Cannot set both a certificate directory and file handle"); |
880 | } | |
7460295f MG |
881 | if (certificateDir) { |
882 | fatal("Only one certificate directory can be selected"); | |
883 | } | |
1f771613 MG |
884 | struct stat st; |
885 | if (!optarg || !*optarg || stat(optarg, &st) || !S_ISDIR(st.st_mode)) { | |
886 | fatal("\"--cert\" expects a directory name"); | |
887 | } | |
7460295f | 888 | check(certificateDir = strdup(optarg)); |
dc6575f2 MG |
889 | } else if (!idx--) { |
890 | // Certificate file descriptor | |
891 | if (!hasSSL) { | |
892 | warn("Ignoring certificate directory, as SSL support is unavailable"); | |
893 | } | |
894 | if (certificateDir) { | |
895 | fatal("Cannot set both a certificate directory and file handle"); | |
896 | } | |
897 | if (certificateFd >= 0) { | |
898 | fatal("Only one certificate file handle can be provided"); | |
899 | } | |
1f771613 MG |
900 | if (!optarg || *optarg < '0' || *optarg > '9') { |
901 | fatal("\"--cert-fd\" expects a valid file handle"); | |
902 | } | |
dc6575f2 MG |
903 | int tmpFd = strtoint(optarg, 3, INT_MAX); |
904 | certificateFd = dup(tmpFd); | |
905 | if (certificateFd < 0) { | |
906 | fatal("Invalid certificate file handle"); | |
907 | } | |
908 | check(!NOINTR(close(tmpFd))); | |
1f771613 MG |
909 | } else if (!idx--) { |
910 | // CSS | |
911 | struct stat st; | |
912 | if (!optarg || !*optarg || stat(optarg, &st) || !S_ISREG(st.st_mode)) { | |
913 | fatal("\"--css\" expects a file name"); | |
914 | } | |
915 | FILE *css = fopen(optarg, "r"); | |
916 | if (!css) { | |
917 | fatal("Cannot read style sheet \"%s\"", optarg); | |
918 | } else { | |
919 | check(cssStyleSheet= realloc(cssStyleSheet, strlen(cssStyleSheet) + | |
920 | st.st_size + 2)); | |
921 | char *newData = strrchr(cssStyleSheet, '\000'); | |
922 | *newData++ = '\n'; | |
923 | if (fread(newData, 1, st.st_size, css) != st.st_size) { | |
924 | fatal("Failed to read style sheet \"%s\"", optarg); | |
925 | } | |
926 | newData[st.st_size]= '\000'; | |
927 | fclose(css); | |
928 | } | |
d1edcc0e MG |
929 | } else if (!idx--) { |
930 | // CGI | |
931 | if (demonize) { | |
932 | fatal("CGI and background operations are mutually exclusive"); | |
933 | } | |
934 | if (port) { | |
935 | fatal("Cannot specify a port for CGI operation"); | |
936 | } | |
937 | cgi = 1; | |
1f771613 | 938 | if (optarg && *optarg) { |
d1edcc0e MG |
939 | char *ptr = strchr(optarg, '-'); |
940 | if (!ptr) { | |
941 | fatal("Syntax error in port range specification"); | |
942 | } | |
943 | *ptr = '\000'; | |
944 | portMin = strtoint(optarg, 1, 65535); | |
945 | *ptr = '-'; | |
946 | portMax = strtoint(ptr + 1, portMin, 65535); | |
947 | } | |
7460295f MG |
948 | } else if (!idx--) { |
949 | // Debug | |
950 | if (!logIsDefault() && !logIsDebug()) { | |
951 | fatal("--debug is mutually exclusive with --quiet and --verbose."); | |
952 | } | |
953 | verbosity = MSG_DEBUG; | |
954 | logSetLogLevel(verbosity); | |
955 | } else if (!idx--) { | |
956 | // Static file | |
957 | char *ptr, *path, *file; | |
958 | if ((ptr = strchr(optarg, ':')) == NULL) { | |
959 | fatal("Syntax error in static-file definition \"%s\".", optarg); | |
960 | } | |
961 | check(path = malloc(ptr - optarg + 1)); | |
962 | memcpy(path, optarg, ptr - optarg); | |
963 | path[ptr - optarg] = '\000'; | |
964 | file = strdup(ptr + 1); | |
965 | if (getRefFromHashMap(externalFiles, path)) { | |
966 | fatal("Duplicate static-file definition for \"%s\".", path); | |
967 | } | |
968 | addToHashMap(externalFiles, path, file); | |
969 | } else if (!idx--) { | |
970 | // Group | |
971 | if (runAsGroup >= 0) { | |
972 | fatal("Duplicate --group option."); | |
973 | } | |
1f771613 MG |
974 | if (!optarg || !*optarg) { |
975 | fatal("\"--group\" expects a group name."); | |
976 | } | |
7460295f | 977 | runAsGroup = parseGroup(optarg, NULL); |
f0c6fd39 MG |
978 | } else if (!idx--) { |
979 | // Linkify | |
980 | if (!strcmp(optarg, "none")) { | |
981 | linkifyURLs = 0; | |
982 | } else if (!strcmp(optarg, "normal")) { | |
983 | linkifyURLs = 1; | |
984 | } else if (!strcmp(optarg, "aggressive")) { | |
985 | linkifyURLs = 2; | |
986 | } else { | |
987 | fatal("Invalid argument for --linkify. Must be " | |
988 | "\"none\", \"normal\", or \"aggressive\"."); | |
989 | } | |
53b27f78 MG |
990 | } else if (!idx--) { |
991 | // Localhost Only | |
992 | localhostOnly = 1; | |
46f2036a MG |
993 | } else if (!idx--) { |
994 | // No Beep | |
995 | noBeep = 1; | |
7460295f MG |
996 | } else if (!idx--) { |
997 | // Numeric | |
998 | numericHosts = 1; | |
999 | } else if (!idx--) { | |
1000 | // Port | |
d1edcc0e MG |
1001 | if (port) { |
1002 | fatal("Duplicate --port option"); | |
1003 | } | |
1004 | if (cgi) { | |
1005 | fatal("Cannot specifiy a port for CGI operation"); | |
1006 | } | |
1f771613 MG |
1007 | if (!optarg || *optarg < '0' || *optarg > '9') { |
1008 | fatal("\"--port\" expects a port number."); | |
1009 | } | |
7460295f MG |
1010 | port = strtoint(optarg, 1, 65535); |
1011 | } else if (!idx--) { | |
1012 | // Service | |
1013 | struct Service *service; | |
1014 | service = newService(optarg); | |
d1edcc0e | 1015 | if (getRefFromHashMap(serviceTable, service->path)) { |
7460295f MG |
1016 | fatal("Duplicate service description for \"%s\".", service->path); |
1017 | } | |
d1edcc0e | 1018 | addToHashMap(serviceTable, service->path, (char *)service); |
7460295f MG |
1019 | } else if (!idx--) { |
1020 | // Disable SSL | |
1021 | if (!hasSSL) { | |
1022 | warn("Ignoring disable-ssl option, as SSL support is unavailable"); | |
1023 | } | |
1024 | enableSSL = 0; | |
b624088c MG |
1025 | } else if (!idx--) { |
1026 | // Disable SSL Menu | |
1027 | if (!hasSSL) { | |
1028 | warn("Ignoring disable-ssl-menu option, as SSL support is " | |
1029 | "unavailable"); | |
1030 | } | |
1031 | enableSSLMenu = 0; | |
7460295f MG |
1032 | } else if (!idx--) { |
1033 | // Quiet | |
1034 | if (!logIsDefault() && !logIsQuiet()) { | |
1035 | fatal("--quiet is mutually exclusive with --debug and --verbose."); | |
1036 | } | |
1037 | verbosity = MSG_QUIET; | |
1038 | logSetLogLevel(verbosity); | |
1039 | } else if (!idx--) { | |
1040 | // User | |
1041 | if (runAsUser >= 0) { | |
1042 | fatal("Duplicate --user option."); | |
1043 | } | |
1f771613 MG |
1044 | if (!optarg || !*optarg) { |
1045 | fatal("\"--user\" expects a user name."); | |
1046 | } | |
7460295f | 1047 | runAsUser = parseUser(optarg, NULL); |
ecbff9b9 MG |
1048 | } else if (!idx--) { |
1049 | // User CSS | |
1050 | if (!optarg || !*optarg) { | |
1051 | fatal("\"--user-css\" expects a list of styles sheets and labels"); | |
1052 | } | |
1053 | parseUserCSS(&userCSSList, optarg); | |
7460295f MG |
1054 | } else if (!idx--) { |
1055 | // Verbose | |
1056 | if (!logIsDefault() && (!logIsInfo() || logIsDebug())) { | |
1057 | fatal("--verbose is mutually exclusive with --debug and --quiet"); | |
1058 | } | |
1059 | verbosity = MSG_INFO; | |
1060 | logSetLogLevel(verbosity); | |
1061 | } else if (!idx--) { | |
1062 | // Version | |
fe338aa8 | 1063 | message("ShellInABox version " VERSION " (revision " VCS_REVISION ")"); |
7460295f MG |
1064 | exit(0); |
1065 | } | |
1066 | } | |
1067 | if (optind != argc) { | |
1068 | usage(); | |
56d6406f | 1069 | fatal("Failed to parse command line"); |
7460295f MG |
1070 | } |
1071 | char *buf = NULL; | |
1072 | check(argc >= 1); | |
1073 | for (int i = 0; i < argc; i++) { | |
1074 | buf = stringPrintf(buf, " %s", argv[i]); | |
1075 | } | |
1076 | info("Command line:%s", buf); | |
1077 | free(buf); | |
1078 | ||
d1edcc0e MG |
1079 | // If the user did not specify a port, use the default one |
1080 | if (!cgi && !port) { | |
1081 | port = PORTNUM; | |
1082 | } | |
1083 | ||
7460295f | 1084 | // If the user did not register any services, provide the default service |
d1edcc0e | 1085 | if (!getHashmapSize(serviceTable)) { |
78016c46 MG |
1086 | addToHashMap(serviceTable, "/", (char *)newService(geteuid() ? ":SSH" : |
1087 | ":LOGIN")); | |
d1edcc0e MG |
1088 | } |
1089 | enumerateServices(serviceTable); | |
1090 | deleteHashMap(serviceTable); | |
1091 | ||
1092 | // Do not allow non-root URLs for CGI operation | |
1093 | if (cgi) { | |
1094 | for (int i = 0; i < numServices; i++) { | |
1095 | if (strcmp(services[i]->path, "/")) { | |
1096 | fatal("Non-root service URLs are incompatible with CGI operation"); | |
1097 | } | |
1098 | } | |
3240f75b | 1099 | check(cgiSessionKey = newSessionKey()); |
7460295f | 1100 | } |
7460295f MG |
1101 | |
1102 | if (demonize) { | |
1103 | pid_t pid; | |
1104 | check((pid = fork()) >= 0); | |
1105 | if (pid) { | |
1106 | _exit(0); | |
1107 | } | |
1108 | setsid(); | |
1109 | if (pidfile) { | |
47e62a9c MG |
1110 | #ifndef O_LARGEFILE |
1111 | #define O_LARGEFILE 0 | |
1112 | #endif | |
7460295f MG |
1113 | int fd = NOINTR(open(pidfile, |
1114 | O_WRONLY|O_TRUNC|O_LARGEFILE|O_CREAT, | |
1115 | 0644)); | |
1116 | if (fd >= 0) { | |
1117 | char buf[40]; | |
1118 | NOINTR(write(fd, buf, snprintf(buf, 40, "%d", (int)getpid()))); | |
dc6575f2 | 1119 | check(!NOINTR(close(fd))); |
7460295f MG |
1120 | } |
1121 | } | |
1122 | } | |
1123 | free((char *)pidfile); | |
1124 | } | |
1125 | ||
d1edcc0e MG |
1126 | static void removeLimits() { |
1127 | static int res[] = { RLIMIT_CPU, RLIMIT_DATA, RLIMIT_FSIZE, RLIMIT_NPROC }; | |
1128 | for (int i = 0; i < sizeof(res)/sizeof(int); i++) { | |
1129 | struct rlimit rl; | |
1130 | getrlimit(res[i], &rl); | |
1131 | if (rl.rlim_max < RLIM_INFINITY) { | |
1132 | rl.rlim_max = RLIM_INFINITY; | |
1133 | setrlimit(res[i], &rl); | |
1134 | getrlimit(res[i], &rl); | |
1135 | } | |
1136 | if (rl.rlim_cur < rl.rlim_max) { | |
1137 | rl.rlim_cur = rl.rlim_max; | |
1138 | setrlimit(res[i], &rl); | |
1139 | } | |
1140 | } | |
1141 | } | |
1142 | ||
b624088c MG |
1143 | static void setUpSSL(Server *server) { |
1144 | serverEnableSSL(server, enableSSL); | |
1145 | ||
1146 | // Enable SSL support (if available) | |
1147 | if (enableSSL) { | |
1148 | check(serverSupportsSSL()); | |
1149 | if (certificateFd >= 0) { | |
1150 | serverSetCertificateFd(server, certificateFd); | |
1151 | } else if (certificateDir) { | |
1152 | char *tmp; | |
1153 | if (strchr(certificateDir, '%')) { | |
1154 | fatal("Invalid certificate directory name \"%s\".", certificateDir); | |
1155 | } | |
1156 | check(tmp = stringPrintf(NULL, "%s/certificate%%s.pem", certificateDir)); | |
1157 | serverSetCertificate(server, tmp, 1); | |
1158 | free(tmp); | |
1159 | } else { | |
1160 | serverSetCertificate(server, "certificate%s.pem", 1); | |
1161 | } | |
1162 | } | |
1163 | } | |
1164 | ||
7460295f | 1165 | int main(int argc, char * const argv[]) { |
47e62a9c | 1166 | #ifdef HAVE_SYS_PRCTL_H |
7460295f MG |
1167 | // Disable core files |
1168 | prctl(PR_SET_DUMPABLE, 0, 0, 0, 0); | |
47e62a9c MG |
1169 | #endif |
1170 | struct rlimit rl = { 0 }; | |
1171 | setrlimit(RLIMIT_CORE, &rl); | |
d1edcc0e | 1172 | removeLimits(); |
7460295f MG |
1173 | |
1174 | // Parse command line arguments | |
1175 | parseArgs(argc, argv); | |
1176 | ||
1177 | // Fork the launcher process, allowing us to drop privileges in the main | |
1178 | // process. | |
d1edcc0e | 1179 | int launcherFd = forkLauncher(); |
7460295f MG |
1180 | |
1181 | // Make sure that our timestamps will print in the standard format | |
1182 | setlocale(LC_TIME, "POSIX"); | |
1183 | ||
1184 | // Create a new web server | |
1185 | Server *server; | |
d1edcc0e | 1186 | if (port) { |
53b27f78 | 1187 | check(server = newServer(localhostOnly, port)); |
11cd1451 | 1188 | dropPrivileges(); |
b624088c | 1189 | setUpSSL(server); |
d1edcc0e MG |
1190 | } else { |
1191 | // For CGI operation we fork the new server, so that it runs in the | |
1192 | // background. | |
1193 | pid_t pid; | |
1194 | int fds[2]; | |
11cd1451 | 1195 | dropPrivileges(); |
d1edcc0e MG |
1196 | check(!pipe(fds)); |
1197 | check((pid = fork()) >= 0); | |
1198 | if (pid) { | |
1199 | // Wait for child to output initial HTML page | |
1200 | char wait; | |
1201 | check(!NOINTR(close(fds[1]))); | |
1202 | check(!NOINTR(read(fds[0], &wait, 1))); | |
1203 | check(!NOINTR(close(fds[0]))); | |
1204 | _exit(0); | |
1205 | } | |
1206 | check(!NOINTR(close(fds[0]))); | |
53b27f78 MG |
1207 | check(server = newCGIServer(localhostOnly, portMin, portMax, |
1208 | AJAX_TIMEOUT)); | |
d1edcc0e | 1209 | cgiServer = server; |
b624088c | 1210 | setUpSSL(server); |
d1edcc0e MG |
1211 | |
1212 | // Output a <frameset> that includes our root page | |
1213 | check(port = serverGetListeningPort(server)); | |
1214 | extern char cgiRootStart[]; | |
1215 | extern char cgiRootEnd[]; | |
1216 | char *cgiRoot; | |
1217 | check(cgiRoot = malloc(cgiRootEnd - cgiRootStart + 1)); | |
1218 | memcpy(cgiRoot, cgiRootStart, cgiRootEnd - cgiRootStart); | |
ea9929c4 MG |
1219 | cgiRoot[cgiRootEnd - cgiRootStart] = '\000'; |
1220 | printf("X-ShellInABox-Port: %d\r\n" | |
1221 | "X-ShellInABox-Pid: %d\r\n" | |
1222 | "Content-type: text/html; charset=utf-8\r\n\r\n", | |
1223 | port, pid); | |
9b850878 | 1224 | printfUnchecked(cgiRoot, port, cgiSessionKey); |
d1edcc0e MG |
1225 | fflush(stdout); |
1226 | free(cgiRoot); | |
1227 | check(!NOINTR(close(fds[1]))); | |
b624088c | 1228 | closeAllFds((int []){ launcherFd, serverGetFd(server) }, 2); |
d1edcc0e MG |
1229 | logSetLogLevel(MSG_QUIET); |
1230 | } | |
7460295f MG |
1231 | |
1232 | // Set log file format | |
1233 | serverSetNumericHosts(server, numericHosts); | |
1234 | ||
1235 | // Disable /quit handler | |
1236 | serverRegisterHttpHandler(server, "/quit", NULL, NULL); | |
1237 | ||
1238 | // Register HTTP handler(s) | |
1239 | for (int i = 0; i < numServices; i++) { | |
1240 | serverRegisterHttpHandler(server, services[i]->path, | |
1241 | shellInABoxHttpHandler, services[i]); | |
1242 | } | |
1243 | ||
1244 | // Register handlers for external files | |
1245 | iterateOverHashMap(externalFiles, registerExternalFiles, server); | |
1246 | ||
1247 | // Start the server | |
1248 | serverLoop(server); | |
1249 | ||
1250 | // Clean up | |
1251 | deleteServer(server); | |
1252 | finishAllSessions(); | |
1253 | deleteHashMap(externalFiles); | |
1254 | for (int i = 0; i < numServices; i++) { | |
1255 | deleteService(services[i]); | |
1256 | } | |
1257 | free(services); | |
1258 | free(certificateDir); | |
3240f75b | 1259 | free(cgiSessionKey); |
7460295f | 1260 | info("Done"); |
d1edcc0e | 1261 | _exit(0); |
7460295f | 1262 | } |