1 // shellinaboxd.c -- A custom web server that makes command line applications
2 // available as AJAX web applications.
3 // Copyright (C) 2008-2009 Markus Gutschke <markus@shellinabox.com>
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.
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.
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.
18 // In addition to these license terms, the author grants the following
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.
30 // You may at your option choose to remove this additional permission from
31 // the work, or from any part of it.
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:
37 // This product includes software developed by the OpenSSL Project
38 // for use in the OpenSSL Toolkit. (http://www.openssl.org/)
40 // This product includes cryptographic software written by Eric Young
41 // (eay@cryptsoft.com)
44 // The most up-to-date version of this program is always available from
45 // http://shellinabox.com
59 #include <sys/resource.h>
60 #include <sys/types.h>
64 #ifdef HAVE_SYS_PRCTL_H
65 #include <sys/prctl.h>
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"
75 #include "shellinabox/usercss.h"
78 #define MAX_RESPONSE 2048
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;
98 static char *jsonEscape(const char *buf, int len) {
99 static const char *hexDigit = "0123456789ABCDEF";
101 // Determine the space that is needed to encode the buffer
103 const char *ptr = buf;
104 for (int i = 0; i < len; i++) {
105 unsigned char ch = *(unsigned char *)ptr++;
108 case '\b': case '\f': case '\n': case '\r': case '\t':
115 } else if (ch == '"' || ch == '\\' || ch == '/') {
117 } else if (ch > '\x7F') {
124 // Encode the buffer using JSON string escaping
126 check(result = malloc(count + 1));
129 for (int i = 0; i < len; i++) {
130 unsigned char ch = *(unsigned char *)ptr++;
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;
144 *dst++ = hexDigit[ch >> 4];
145 *dst++ = hexDigit[ch & 0xF];
148 } else if (ch == '"' || ch == '\\' || ch == '/') {
151 } else if (ch > '\x7F') {
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()"
168 va_start(ap, format);
169 int rc = vprintf(format, ap);
174 static int completePendingRequest(struct Session *session,
175 const char *buf, int len, int maxLength) {
176 // If there is no pending HTTP request, save the data and return
178 if (!session->http) {
180 if (session->buffered) {
181 check(session->buffered = realloc(session->buffered,
182 session->len + len));
183 memcpy(session->buffered + session->len, buf, len);
186 check(session->buffered = malloc(len));
187 memcpy(session->buffered, buf, len);
192 // If we have a pending HTTP request, we can reply to it, now.
194 if (session->buffered) {
195 check(session->buffered = realloc(session->buffered,
196 session->len + len));
197 memcpy(session->buffered + session->len, buf, 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,
205 data = jsonEscape(session->buffered, session->len);
206 free(session->buffered);
207 session->buffered = NULL;
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);
217 data = jsonEscape(buf, len);
221 char *json = stringPrintf(NULL, "{"
222 "\"session\":\"%s\","
225 session->sessionKey, data);
227 HttpConnection *http = session->http;
228 char *response = stringPrintf(NULL,
229 "HTTP/1.1 200 OK\r\n"
230 "Content-Type: application/json; "
232 "Content-Length: %ld\r\n"
233 "Cache-Control: no-cache\r\n"
237 strcmp(httpGetMethod(http),
238 "HEAD") ? json : "");
240 session->http = NULL;
241 httpTransfer(http, response, strlen(response));
243 if (session->done && !session->buffered) {
244 finishSession(session);
250 static void sessionDone(void *arg) {
251 debug("Child terminated");
252 struct Session *session = (struct Session *)arg;
254 addToGraveyard(session);
255 completePendingRequest(session, "", 0, INT_MAX);
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;
268 if (revents & POLLIN) {
269 bytes = NOINTR(read(session->pty, buf, len));
274 int timedOut = serverGetTimeout(connection) < 0;
275 if (bytes || timedOut) {
276 if (!session->http && timedOut) {
277 debug("Timeout. Closing session.");
280 check(!session->done);
281 check(completePendingRequest(session, buf, bytes, MAX_RESPONSE));
282 connection = serverGetConnection(session->server,
285 session->connection = connection;
286 if (session->len >= MAX_RESPONSE) {
287 serverConnectionSetEvents(session->server, connection, 0);
289 serverSetTimeout(connection, AJAX_TIMEOUT);
296 static int invalidatePendingHttpSession(void *arg, const char *key,
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);
304 // Return zero in order to remove this HTTP from the "session" hashmap
308 // If the session is still in use, do not remove it from the "sessions" map
312 static int dataHandler(HttpConnection *http, struct Service *service,
313 const char *buf, int len, URL *url) {
315 // Somebody unexpectedly closed our http connection (e.g. because of a
316 // timeout). This is the last notification that we will get.
318 iterateOverSessions(invalidatePendingHttpSession, http);
322 // Find an existing session, or create the record for a new one
324 struct Session *session = findCGISession(&isNew, http, url, cgiSessionKey);
325 if (session == NULL) {
326 httpSendReply(http, 400, "Bad Request", NO_MSG);
331 if (!isNew && strcmp(session->peerName, httpGetPeerName(http))) {
332 error("Peername changed from %s to %s",
333 session->peerName, httpGetPeerName(http));
334 httpSendReply(http, 400, "Bad Request", NO_MSG);
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");
345 // Adjust window dimensions if provided by client
346 if (width && height) {
347 session->width = atoi(width);
348 session->height = atoi(height);
351 // Create a new session, if the client did not provide an existing one
355 abandonSession(session);
356 httpSendReply(http, 400, "Bad Request", NO_MSG);
360 if (cgiServer && cgiSessions++) {
361 serverExitLoop(cgiServer, 1);
362 goto bad_new_session;
364 session->http = http;
365 if (launchChild(service->id, session) < 0) {
366 abandonSession(session);
367 httpSendReply(http, 500, "Internal Error", NO_MSG);
373 session->connection = serverAddConnection(httpGetServer(http),
374 session->pty, handleSession,
375 sessionDone, session);
376 serverSetTimeout(session->connection, AJAX_TIMEOUT);
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);
386 // Process keypresses, if any. Then send a synchronous reply.
389 check(keyCodes = malloc(strlen(keys)/2));
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') {
397 unsigned c1 = *ptr++;
398 if (c1 < '0' || (c1 > '9' && c1 < 'A') ||
399 (c1 > 'F' && c1 < 'a') || c1 > 'f') {
402 keyCodes[len++] = 16*((c0 & 0xF) + 9*(c0 > '9')) +
403 (c1 & 0xF) + 9*(c1 > '9');
405 if (write(session->pty, keyCodes, len) < 0 && errno == EAGAIN) {
406 completePendingRequest(session, "\007", 1, MAX_RESPONSE);
409 httpSendReply(http, 200, "OK", " ");
410 check(session->http != http);
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)) {
417 httpSendReply(http, 400, "Bad Request", NO_MSG);
420 session->http = http;
423 session->connection = serverGetConnection(session->server,
426 if (session->buffered || isNew) {
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);
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);
446 static void serveStaticFile(HttpConnection *http, const char *contentType,
447 const char *start, const char *end) {
448 char *response = stringPrintf(NULL,
449 "HTTP/1.1 200 OK\r\n"
450 "Content-Type: %s\r\n"
451 "Content-Length: %ld\r\n"
453 contentType, (long)(end - start));
454 int len = strlen(response);
455 if (strcmp(httpGetMethod(http), "HEAD")) {
456 check(response = realloc(response, len + (end - start)));
457 memcpy(response + len, start, end - start);
460 httpTransfer(http, response, len);
463 static const char *addr(const char *a) {
464 // Work-around for a gcc bug that could occasionally generate invalid
465 // assembly instructions when optimizing code too agressively.
470 static int shellInABoxHttpHandler(HttpConnection *http, void *arg,
471 const char *buf, int len) {
473 URL *url = newURL(http, buf, len);
474 const HashMap *headers = httpGetHeaders(http);
475 const char *contentType = getFromHashMap(headers, "content-type");
477 // Normalize the path info
478 const char *pathInfo = urlGetPathInfo(url);
479 while (*pathInfo == '/') {
482 const char *endPathInfo;
483 for (endPathInfo = pathInfo;
484 *endPathInfo && *endPathInfo != '/';
487 int pathInfoLength = endPathInfo - pathInfo;
489 if (!pathInfoLength ||
490 (pathInfoLength == 5 && !memcmp(pathInfo, "plain", 5)) ||
491 (pathInfoLength == 6 && !memcmp(pathInfo, "secure", 6))) {
492 // The root page serves the AJAX application.
494 !strncasecmp(contentType, "application/x-www-form-urlencoded", 33)) {
495 // XMLHttpRequest carrying data between the AJAX application and the
497 return dataHandler(http, arg, buf, len, url);
499 extern char rootPageStart[];
500 extern char rootPageEnd[];
502 check(rootPage = malloc(rootPageEnd - rootPageStart + 1));
503 memcpy(rootPage, rootPageStart, rootPageEnd - rootPageStart);
504 rootPage[rootPageEnd - rootPageStart] = '\000';
505 char *html = stringPrintf(NULL, rootPage,
506 enableSSL ? "true" : "false");
507 serveStaticFile(http, "text/html", html, strrchr(html, '\000'));
510 } else if (pathInfoLength == 8 && !memcmp(pathInfo, "beep.wav", 8)) {
511 // Serve the audio sample for the console bell.
512 extern char beepStart[];
513 extern char beepEnd[];
514 serveStaticFile(http, "audio/x-wav", beepStart, beepEnd);
515 } else if (pathInfoLength == 11 && !memcmp(pathInfo, "enabled.gif", 11)) {
516 // Serve the checkmark icon used in the context menu
517 extern char enabledStart[];
518 extern char enabledEnd[];
519 serveStaticFile(http, "image/gif", enabledStart, enabledEnd);
520 } else if (pathInfoLength == 11 && !memcmp(pathInfo, "favicon.ico", 11)) {
522 extern char faviconStart[];
523 extern char faviconEnd[];
524 serveStaticFile(http, "image/x-icon", faviconStart, faviconEnd);
525 } else if (pathInfoLength == 14 && !memcmp(pathInfo, "ShellInABox.js", 14)) {
526 // Serve both vt100.js and shell_in_a_box.js in the same transaction.
527 // Also, indicate to the client whether the server is SSL enabled.
528 extern char vt100Start[];
529 extern char vt100End[];
530 extern char shellInABoxStart[];
531 extern char shellInABoxEnd[];
532 char *userCSSString = getUserCSSString(userCSSList);
533 char *stateVars = stringPrintf(NULL,
534 "serverSupportsSSL = %s;\n"
535 "disableSSLMenu = %s;\n"
536 "suppressAllAudio = %s;\n"
537 "linkifyURLs = %d;\n"
538 "userCSSList = %s;\n\n",
539 enableSSL ? "true" : "false",
540 !enableSSLMenu ? "true" : "false",
541 noBeep ? "true" : "false",
542 linkifyURLs, userCSSString);
544 int stateVarsLength = strlen(stateVars);
545 int contentLength = stateVarsLength +
546 (addr(vt100End) - addr(vt100Start)) +
547 (addr(shellInABoxEnd) - addr(shellInABoxStart));
548 char *response = stringPrintf(NULL,
549 "HTTP/1.1 200 OK\r\n"
550 "Content-Type: text/javascript; charset=utf-8\r\n"
551 "Content-Length: %d\r\n"
554 int headerLength = strlen(response);
555 if (strcmp(httpGetMethod(http), "HEAD")) {
556 check(response = realloc(response, headerLength + contentLength));
557 memcpy(memcpy(memcpy(
558 response + headerLength, stateVars, stateVarsLength)+stateVarsLength,
559 vt100Start, vt100End - vt100Start) + (vt100End - vt100Start),
560 shellInABoxStart, shellInABoxEnd - shellInABoxStart);
565 httpTransfer(http, response, headerLength + contentLength);
566 } else if (pathInfoLength == 10 && !memcmp(pathInfo, "styles.css", 10)) {
567 // Serve the style sheet.
568 serveStaticFile(http, "text/css; charset=utf-8",
569 cssStyleSheet, strrchr(cssStyleSheet, '\000'));
570 } else if (pathInfoLength > 8 && !memcmp(pathInfo, "usercss-", 8)) {
571 // Server user style sheets (if any)
572 struct UserCSS *css = userCSSList;
573 for (int idx = atoi(pathInfo + 8);
574 idx-- > 0 && css; css = css->next ) {
577 serveStaticFile(http, "text/css; charset=utf-8",
578 css->style, css->style + css->styleLen);
580 httpSendReply(http, 404, "File not found", NO_MSG);
583 httpSendReply(http, 404, "File not found", NO_MSG);
590 static int strtoint(const char *s, int minVal, int maxVal) {
593 fatal("Missing numeric value.");
595 long l = strtol(s, &ptr, 10);
596 if (*ptr || l < minVal || l > maxVal) {
597 fatal("Range error on numeric value \"%s\".", s);
602 static void usage(void) {
603 // Drop privileges so that we can tell which uid/gid we would normally
606 uid_t r_uid, e_uid, s_uid;
607 uid_t r_gid, e_gid, s_gid;
608 check(!getresuid(&r_uid, &e_uid, &s_uid));
609 check(!getresgid(&r_gid, &e_gid, &s_gid));
610 const char *user = getUserName(r_uid);
611 const char *group = getGroupName(r_gid);
613 message("Usage: shellinaboxd [OPTIONS]...\n"
614 "Starts an HTTP server that serves terminal emulators to AJAX "
615 "enabled browsers.\n"
617 "List of command line options:\n"
618 " -b, --background[=PIDFILE] run in background\n"
620 " --css=FILE attach contents to CSS style sheet\n"
621 " --cgi[=PORTMIN-PORTMAX] run as CGI\n"
622 " -d, --debug enable debug mode\n"
623 " -f, --static-file=URL:FILE serve static file from URL path\n"
624 " -g, --group=GID switch to this group (default: %s)\n"
625 " -h, --help print this message\n"
626 " --linkify=[none|normal|agressive] default is \"normal\"\n"
627 " --localhost-only only listen on 127.0.0.1\n"
628 " --no-beep suppress all audio output\n"
629 " -n, --numeric do not resolve hostnames\n"
630 " -p, --port=PORT select a port (default: %d)\n"
631 " -s, --service=SERVICE define one or more services\n"
633 " -q, --quiet turn off all messages\n"
634 " -u, --user=UID switch to this user (default: %s)\n"
635 " --user-css=STYLES defines user-selectable CSS options\n"
636 " -v, --verbose enable logging messages\n"
637 " --version prints version information\n"
639 "Debug, quiet, and verbose are mutually exclusive.\n"
641 "One or more --service arguments define services that should "
642 "be made available\n"
643 "through the web interface:\n"
644 " SERVICE := <url-path> ':' APP\n"
645 " APP := 'LOGIN' | 'SSH' [ : <host> ] | "
646 "USER ':' CWD ':' <cmdline>\n"
647 " USER := %s<username> ':' <groupname>\n"
648 " CWD := 'HOME' | <dir>\n"
650 "<cmdline> supports variable expansion:\n"
651 " ${columns} - number of columns\n"
653 " ${group} - group name\n"
654 " ${home} - home directory\n"
655 " ${lines} - number of rows\n"
656 " ${peer} - name of remote peer\n"
657 " ${uid} - user id\n"
658 " ${user} - user name\n"
660 "One or more --user-css arguments define optional user-selectable "
662 "These options show up in the right-click context menu:\n"
663 " STYLES := GROUP { ';' GROUP }*\n"
664 " GROUP := OPTION { ',' OPTION }*\n"
665 " OPTION := <label> ':' [ '-' | '+' ] <css-file>\n"
667 "OPTIONs that make up a GROUP are mutually exclusive. But "
668 "individual GROUPs are\n"
669 "independent of each other.\n",
670 !serverSupportsSSL() ? "" :
671 " -c, --cert=CERTDIR set certificate dir "
673 " --cert-fd=FD set certificate file from fd\n",
675 !serverSupportsSSL() ? "" :
676 " -t, --disable-ssl disable transparent SSL support\n"
677 " --disable-ssl-menu disallow changing transport mode\n",
678 user, supportsPAM() ? "'AUTH' | " : "");
683 static void destroyExternalFileHashEntry(void *arg, char *key, char *value) {
688 static void parseArgs(int argc, char * const argv[]) {
689 int hasSSL = serverSupportsSSL();
695 const char *pidfile = NULL;
696 int verbosity = MSG_DEFAULT;
697 externalFiles = newHashMap(destroyExternalFileHashEntry, NULL);
698 HashMap *serviceTable = newHashMap(destroyServiceHashEntry, NULL);
699 extern char stylesStart[];
700 extern char stylesEnd[];
701 check(cssStyleSheet = malloc(stylesEnd - stylesStart));
702 memcpy(cssStyleSheet, stylesStart, stylesEnd - stylesStart);
703 cssStyleSheet[stylesEnd - stylesStart] = '\000';
706 static const char optstring[] = "+hb::c:df:g:np:s:tqu:v";
707 static struct option options[] = {
708 { "help", 0, 0, 'h' },
709 { "background", 2, 0, 'b' },
710 { "cert", 1, 0, 'c' },
711 { "cert-fd", 1, 0, 0 },
714 { "debug", 0, 0, 'd' },
715 { "static-file", 1, 0, 'f' },
716 { "group", 1, 0, 'g' },
717 { "linkify", 1, 0, 0 },
718 { "localhost-only", 0, 0, 0 },
719 { "no-beep", 0, 0, 0 },
720 { "numeric", 0, 0, 'n' },
721 { "port", 1, 0, 'p' },
722 { "service", 1, 0, 's' },
723 { "disable-ssl", 0, 0, 't' },
724 { "disable-ssl-menu", 0, 0, 0 },
725 { "quiet", 0, 0, 'q' },
726 { "user", 1, 0, 'u' },
727 { "user-css", 1, 0, 0 },
728 { "verbose", 0, 0, 'v' },
729 { "version", 0, 0, 0 },
732 int c = getopt_long(argc, argv, optstring, options, &idx);
734 for (int i = 0; options[i].name; i++) {
735 if (options[i].val == c) {
744 // Help (or invalid argument)
747 fatal("Failed to parse command line");
753 fatal("CGI and background operations are mutually exclusive");
756 if (optarg && pidfile) {
757 fatal("Only one pidfile can be given");
759 if (optarg && *optarg) {
760 pidfile = strdup(optarg);
765 warn("Ignoring certificate directory, as SSL support is unavailable");
767 if (certificateFd >= 0) {
768 fatal("Cannot set both a certificate directory and file handle");
770 if (certificateDir) {
771 fatal("Only one certificate directory can be selected");
774 if (!optarg || !*optarg || stat(optarg, &st) || !S_ISDIR(st.st_mode)) {
775 fatal("\"--cert\" expects a directory name");
777 check(certificateDir = strdup(optarg));
779 // Certificate file descriptor
781 warn("Ignoring certificate directory, as SSL support is unavailable");
783 if (certificateDir) {
784 fatal("Cannot set both a certificate directory and file handle");
786 if (certificateFd >= 0) {
787 fatal("Only one certificate file handle can be provided");
789 if (!optarg || *optarg < '0' || *optarg > '9') {
790 fatal("\"--cert-fd\" expects a valid file handle");
792 int tmpFd = strtoint(optarg, 3, INT_MAX);
793 certificateFd = dup(tmpFd);
794 if (certificateFd < 0) {
795 fatal("Invalid certificate file handle");
797 check(!NOINTR(close(tmpFd)));
801 if (!optarg || !*optarg || stat(optarg, &st) || !S_ISREG(st.st_mode)) {
802 fatal("\"--css\" expects a file name");
804 FILE *css = fopen(optarg, "r");
806 fatal("Cannot read style sheet \"%s\"", optarg);
808 check(cssStyleSheet= realloc(cssStyleSheet, strlen(cssStyleSheet) +
810 char *newData = strrchr(cssStyleSheet, '\000');
812 if (fread(newData, 1, st.st_size, css) != st.st_size) {
813 fatal("Failed to read style sheet \"%s\"", optarg);
815 newData[st.st_size]= '\000';
821 fatal("CGI and background operations are mutually exclusive");
824 fatal("Cannot specify a port for CGI operation");
827 if (optarg && *optarg) {
828 char *ptr = strchr(optarg, '-');
830 fatal("Syntax error in port range specification");
833 portMin = strtoint(optarg, 1, 65535);
835 portMax = strtoint(ptr + 1, portMin, 65535);
839 if (!logIsDefault() && !logIsDebug()) {
840 fatal("--debug is mutually exclusive with --quiet and --verbose.");
842 verbosity = MSG_DEBUG;
843 logSetLogLevel(verbosity);
846 char *ptr, *path, *file;
847 if ((ptr = strchr(optarg, ':')) == NULL) {
848 fatal("Syntax error in static-file definition \"%s\".", optarg);
850 check(path = malloc(ptr - optarg + 1));
851 memcpy(path, optarg, ptr - optarg);
852 path[ptr - optarg] = '\000';
853 file = strdup(ptr + 1);
854 if (getRefFromHashMap(externalFiles, path)) {
855 fatal("Duplicate static-file definition for \"%s\".", path);
857 addToHashMap(externalFiles, path, file);
860 if (runAsGroup >= 0) {
861 fatal("Duplicate --group option.");
863 if (!optarg || !*optarg) {
864 fatal("\"--group\" expects a group name.");
866 runAsGroup = parseGroup(optarg, NULL);
869 if (!strcmp(optarg, "none")) {
871 } else if (!strcmp(optarg, "normal")) {
873 } else if (!strcmp(optarg, "aggressive")) {
876 fatal("Invalid argument for --linkify. Must be "
877 "\"none\", \"normal\", or \"aggressive\".");
891 fatal("Duplicate --port option");
894 fatal("Cannot specifiy a port for CGI operation");
896 if (!optarg || *optarg < '0' || *optarg > '9') {
897 fatal("\"--port\" expects a port number.");
899 port = strtoint(optarg, 1, 65535);
902 struct Service *service;
903 service = newService(optarg);
904 if (getRefFromHashMap(serviceTable, service->path)) {
905 fatal("Duplicate service description for \"%s\".", service->path);
907 addToHashMap(serviceTable, service->path, (char *)service);
911 warn("Ignoring disable-ssl option, as SSL support is unavailable");
917 warn("Ignoring disable-ssl-menu option, as SSL support is "
923 if (!logIsDefault() && !logIsQuiet()) {
924 fatal("--quiet is mutually exclusive with --debug and --verbose.");
926 verbosity = MSG_QUIET;
927 logSetLogLevel(verbosity);
930 if (runAsUser >= 0) {
931 fatal("Duplicate --user option.");
933 if (!optarg || !*optarg) {
934 fatal("\"--user\" expects a user name.");
936 runAsUser = parseUser(optarg, NULL);
939 if (!optarg || !*optarg) {
940 fatal("\"--user-css\" expects a list of styles sheets and labels");
942 parseUserCSS(&userCSSList, optarg);
945 if (!logIsDefault() && (!logIsInfo() || logIsDebug())) {
946 fatal("--verbose is mutually exclusive with --debug and --quiet");
948 verbosity = MSG_INFO;
949 logSetLogLevel(verbosity);
952 message("ShellInABox version " VERSION " (revision " VCS_REVISION ")");
956 if (optind != argc) {
958 fatal("Failed to parse command line");
962 for (int i = 0; i < argc; i++) {
963 buf = stringPrintf(buf, " %s", argv[i]);
965 info("Command line:%s", buf);
968 // If the user did not specify a port, use the default one
973 // If the user did not register any services, provide the default service
974 if (!getHashmapSize(serviceTable)) {
975 addToHashMap(serviceTable, "/", (char *)newService(geteuid() ? ":SSH" :
978 enumerateServices(serviceTable);
979 deleteHashMap(serviceTable);
981 // Do not allow non-root URLs for CGI operation
983 for (int i = 0; i < numServices; i++) {
984 if (strcmp(services[i]->path, "/")) {
985 fatal("Non-root service URLs are incompatible with CGI operation");
988 check(cgiSessionKey = newSessionKey());
993 check((pid = fork()) >= 0);
1000 #define O_LARGEFILE 0
1002 int fd = NOINTR(open(pidfile,
1003 O_WRONLY|O_TRUNC|O_LARGEFILE|O_CREAT,
1007 NOINTR(write(fd, buf, snprintf(buf, 40, "%d", (int)getpid())));
1008 check(!NOINTR(close(fd)));
1012 free((char *)pidfile);
1015 static void removeLimits() {
1016 static int res[] = { RLIMIT_CPU, RLIMIT_DATA, RLIMIT_FSIZE, RLIMIT_NPROC };
1017 for (int i = 0; i < sizeof(res)/sizeof(int); i++) {
1019 getrlimit(res[i], &rl);
1020 if (rl.rlim_max < RLIM_INFINITY) {
1021 rl.rlim_max = RLIM_INFINITY;
1022 setrlimit(res[i], &rl);
1023 getrlimit(res[i], &rl);
1025 if (rl.rlim_cur < rl.rlim_max) {
1026 rl.rlim_cur = rl.rlim_max;
1027 setrlimit(res[i], &rl);
1032 static void setUpSSL(Server *server) {
1033 serverEnableSSL(server, enableSSL);
1035 // Enable SSL support (if available)
1037 check(serverSupportsSSL());
1038 if (certificateFd >= 0) {
1039 serverSetCertificateFd(server, certificateFd);
1040 } else if (certificateDir) {
1042 if (strchr(certificateDir, '%')) {
1043 fatal("Invalid certificate directory name \"%s\".", certificateDir);
1045 check(tmp = stringPrintf(NULL, "%s/certificate%%s.pem", certificateDir));
1046 serverSetCertificate(server, tmp, 1);
1049 serverSetCertificate(server, "certificate%s.pem", 1);
1054 int main(int argc, char * const argv[]) {
1055 #ifdef HAVE_SYS_PRCTL_H
1056 // Disable core files
1057 prctl(PR_SET_DUMPABLE, 0, 0, 0, 0);
1059 struct rlimit rl = { 0 };
1060 setrlimit(RLIMIT_CORE, &rl);
1063 // Parse command line arguments
1064 parseArgs(argc, argv);
1066 // Fork the launcher process, allowing us to drop privileges in the main
1068 int launcherFd = forkLauncher();
1070 // Make sure that our timestamps will print in the standard format
1071 setlocale(LC_TIME, "POSIX");
1073 // Create a new web server
1076 check(server = newServer(localhostOnly, port));
1080 // For CGI operation we fork the new server, so that it runs in the
1086 check((pid = fork()) >= 0);
1088 // Wait for child to output initial HTML page
1090 check(!NOINTR(close(fds[1])));
1091 check(!NOINTR(read(fds[0], &wait, 1)));
1092 check(!NOINTR(close(fds[0])));
1095 check(!NOINTR(close(fds[0])));
1096 check(server = newCGIServer(localhostOnly, portMin, portMax,
1101 // Output a <frameset> that includes our root page
1102 check(port = serverGetListeningPort(server));
1103 extern char cgiRootStart[];
1104 extern char cgiRootEnd[];
1106 check(cgiRoot = malloc(cgiRootEnd - cgiRootStart + 1));
1107 memcpy(cgiRoot, cgiRootStart, cgiRootEnd - cgiRootStart);
1108 cgiRoot[cgiRootEnd - cgiRootStart] = '\000';
1109 printf("X-ShellInABox-Port: %d\r\n"
1110 "X-ShellInABox-Pid: %d\r\n"
1111 "Content-type: text/html; charset=utf-8\r\n\r\n",
1113 printfUnchecked(cgiRoot, port, cgiSessionKey);
1116 check(!NOINTR(close(fds[1])));
1117 closeAllFds((int []){ launcherFd, serverGetFd(server) }, 2);
1118 logSetLogLevel(MSG_QUIET);
1121 // Set log file format
1122 serverSetNumericHosts(server, numericHosts);
1124 // Disable /quit handler
1125 serverRegisterHttpHandler(server, "/quit", NULL, NULL);
1127 // Register HTTP handler(s)
1128 for (int i = 0; i < numServices; i++) {
1129 serverRegisterHttpHandler(server, services[i]->path,
1130 shellInABoxHttpHandler, services[i]);
1133 // Register handlers for external files
1134 iterateOverHashMap(externalFiles, registerExternalFiles, server);
1140 deleteServer(server);
1141 finishAllSessions();
1142 deleteHashMap(externalFiles);
1143 for (int i = 0; i < numServices; i++) {
1144 deleteService(services[i]);
1147 free(certificateDir);
1148 free(cgiSessionKey);