1 // shellinaboxd.c -- A custom web server that makes command line applications
2 // available as AJAX web applications.
3 // Copyright (C) 2008-2010 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
61 #include <sys/resource.h>
62 #include <sys/types.h>
66 #ifdef HAVE_SYS_PRCTL_H
67 #include <sys/prctl.h>
70 #include "libhttp/http.h"
71 #include "logging/logging.h"
72 #include "shellinabox/externalfile.h"
73 #include "shellinabox/launcher.h"
74 #include "shellinabox/privileges.h"
75 #include "shellinabox/service.h"
76 #include "shellinabox/session.h"
77 #include "shellinabox/usercss.h"
80 #include "shellinabox/beep.h"
81 #include "shellinabox/cgi_root.h"
82 #include "shellinabox/enabled.h"
83 #include "shellinabox/favicon.h"
84 #include "shellinabox/keyboard.h"
85 #include "shellinabox/keyboard-layout.h"
86 #include "shellinabox/print-styles.h"
87 #include "shellinabox/root_page.h"
88 #include "shellinabox/shell_in_a_box.h"
89 #include "shellinabox/styles.h"
90 #include "shellinabox/vt100.h"
93 #defined ATTR_UNUSED __attribute__((unused))
94 #defined UNUSED(x) do { } while (0)
97 #define UNUSED(x) do { (void)(x); } while (0)
101 #define MAX_RESPONSE 2048
106 static int localhostOnly = 0;
107 static int noBeep = 0;
108 static int numericHosts = 0;
109 static int enableSSL = 1;
110 static int enableSSLMenu = 1;
111 static int linkifyURLs = 1;
112 static char *certificateDir;
113 static int certificateFd = -1;
114 static HashMap *externalFiles;
115 static Server *cgiServer;
116 static char *cgiSessionKey;
117 static int cgiSessions;
118 static char *cssStyleSheet;
119 static struct UserCSS *userCSSList;
120 static const char *pidfile;
121 static sigjmp_buf jmpenv;
122 static volatile int exiting;
124 static char *jsonEscape(const char *buf, int len) {
125 static const char *hexDigit = "0123456789ABCDEF";
127 // Determine the space that is needed to encode the buffer
129 const char *ptr = buf;
130 for (int i = 0; i < len; i++) {
131 unsigned char ch = *(unsigned char *)ptr++;
134 case '\b': case '\f': case '\n': case '\r': case '\t':
141 } else if (ch == '"' || ch == '\\' || ch == '/') {
143 } else if (ch > '\x7F') {
150 // Encode the buffer using JSON string escaping
152 check(result = malloc(count + 1));
155 for (int i = 0; i < len; i++) {
156 unsigned char ch = *(unsigned char *)ptr++;
160 case '\b': *dst++ = 'b'; break;
161 case '\f': *dst++ = 'f'; break;
162 case '\n': *dst++ = 'n'; break;
163 case '\r': *dst++ = 'r'; break;
164 case '\t': *dst++ = 't'; break;
170 *dst++ = hexDigit[ch >> 4];
171 *dst++ = hexDigit[ch & 0xF];
174 } else if (ch == '"' || ch == '\\' || ch == '/') {
177 } else if (ch > '\x7F') {
188 static int printfUnchecked(const char *format, ...) {
189 // Some Linux distributions enable -Wformat=2 by default. This is a
190 // very unfortunate decision, as that option generates a lot of false
191 // positives. We try to work around the problem by defining an unchecked
192 // version of "printf()"
194 va_start(ap, format);
195 int rc = vprintf(format, ap);
200 static int completePendingRequest(struct Session *session,
201 const char *buf, int len, int maxLength) {
202 // If there is no pending HTTP request, save the data and return
204 if (!session->http) {
206 if (session->buffered) {
207 check(session->buffered = realloc(session->buffered,
208 session->len + len));
209 memcpy(session->buffered + session->len, buf, len);
212 check(session->buffered = malloc(len));
213 memcpy(session->buffered, buf, len);
218 // If we have a pending HTTP request, we can reply to it, now.
220 if (session->buffered) {
221 check(session->buffered = realloc(session->buffered,
222 session->len + len));
223 memcpy(session->buffered + session->len, buf, len);
225 if (maxLength > 0 && session->len > maxLength) {
226 data = jsonEscape(session->buffered, maxLength);
227 session->len -= maxLength;
228 memmove(session->buffered, session->buffered + maxLength,
231 data = jsonEscape(session->buffered, session->len);
232 free(session->buffered);
233 session->buffered = NULL;
237 if (maxLength > 0 && len > maxLength) {
238 session->len = len - maxLength;
239 check(session->buffered = malloc(session->len));
240 memcpy(session->buffered, buf + maxLength, session->len);
241 data = jsonEscape(buf, maxLength);
243 data = jsonEscape(buf, len);
247 char *json = stringPrintf(NULL, "{"
248 "\"session\":\"%s\","
251 session->sessionKey, data);
253 HttpConnection *http = session->http;
254 char *response = stringPrintf(NULL,
255 "HTTP/1.1 200 OK\r\n"
256 "Content-Type: application/json; "
258 "Content-Length: %ld\r\n"
259 "Cache-Control: no-cache\r\n"
263 strcmp(httpGetMethod(http),
264 "HEAD") ? json : "");
266 session->http = NULL;
267 httpTransfer(http, response, strlen(response));
269 if (session->done && !session->buffered) {
270 finishSession(session);
276 static void sessionDone(void *arg) {
277 debug("Child terminated");
278 struct Session *session = (struct Session *)arg;
280 addToGraveyard(session);
281 completePendingRequest(session, "", 0, INT_MAX);
284 static int handleSession(struct ServerConnection *connection, void *arg,
285 short *events ATTR_UNUSED, short revents) {
287 struct Session *session = (struct Session *)arg;
288 session->connection = connection;
289 int len = MAX_RESPONSE - session->len;
293 char buf[MAX_RESPONSE];
295 if (revents & POLLIN) {
296 bytes = NOINTR(read(session->pty, buf, len));
301 int timedOut = serverGetTimeout(connection) < 0;
302 if (bytes || timedOut) {
303 if (!session->http && timedOut) {
304 debug("Timeout. Closing session.");
307 check(!session->done);
308 check(completePendingRequest(session, buf, bytes, MAX_RESPONSE));
309 connection = serverGetConnection(session->server,
312 session->connection = connection;
313 if (session->len >= MAX_RESPONSE) {
314 serverConnectionSetEvents(session->server, connection, 0);
316 serverSetTimeout(connection, AJAX_TIMEOUT);
323 static int invalidatePendingHttpSession(void *arg, const char *key,
325 struct Session *session = *(struct Session **)value;
326 if (session->http && session->http == (HttpConnection *)arg) {
327 debug("Clearing pending HTTP connection for session %s", key);
328 session->http = NULL;
329 serverDeleteConnection(session->server, session->pty);
331 // Return zero in order to remove this HTTP from the "session" hashmap
335 // If the session is still in use, do not remove it from the "sessions" map
339 static int dataHandler(HttpConnection *http, struct Service *service,
340 const char *buf, int len ATTR_UNUSED, URL *url) {
343 // Somebody unexpectedly closed our http connection (e.g. because of a
344 // timeout). This is the last notification that we will get.
346 iterateOverSessions(invalidatePendingHttpSession, http);
350 // Find an existing session, or create the record for a new one
352 struct Session *session = findCGISession(&isNew, http, url, cgiSessionKey);
353 if (session == NULL) {
354 httpSendReply(http, 400, "Bad Request", NO_MSG);
359 if (!isNew && strcmp(session->peerName, httpGetPeerName(http))) {
360 error("Peername changed from %s to %s",
361 session->peerName, httpGetPeerName(http));
362 httpSendReply(http, 400, "Bad Request", NO_MSG);
366 const HashMap *args = urlGetArgs(session->url);
367 int oldWidth = session->width;
368 int oldHeight = session->height;
369 const char *width = getFromHashMap(args, "width");
370 const char *height = getFromHashMap(args, "height");
371 const char *keys = getFromHashMap(args, "keys");
372 const char *rootURL = getFromHashMap(args, "rooturl");
374 // Adjust window dimensions if provided by client
375 if (width && height) {
376 session->width = atoi(width);
377 session->height = atoi(height);
380 // Create a new session, if the client did not provide an existing one
384 abandonSession(session);
385 httpSendReply(http, 400, "Bad Request", NO_MSG);
389 if (cgiServer && cgiSessions++) {
390 serverExitLoop(cgiServer, 1);
391 goto bad_new_session;
393 session->http = http;
394 if (launchChild(service->id, session,
395 rootURL && *rootURL ? rootURL : urlGetURL(url)) < 0) {
396 abandonSession(session);
397 httpSendReply(http, 500, "Internal Error", NO_MSG);
403 session->connection = serverAddConnection(httpGetServer(http),
404 session->pty, handleSession,
405 sessionDone, session);
406 serverSetTimeout(session->connection, AJAX_TIMEOUT);
409 // Reset window dimensions of the pseudo TTY, if changed since last time set.
410 if (session->width > 0 && session->height > 0 &&
411 (session->width != oldWidth || session->height != oldHeight)) {
412 debug("Window size changed to %dx%d", session->width, session->height);
413 setWindowSize(session->pty, session->width, session->height);
416 // Process keypresses, if any. Then send a synchronous reply.
419 check(keyCodes = malloc(strlen(keys)/2));
421 for (const unsigned char *ptr = (const unsigned char *)keys; ;) {
422 unsigned c0 = *ptr++;
423 if (c0 < '0' || (c0 > '9' && c0 < 'A') ||
424 (c0 > 'F' && c0 < 'a') || c0 > 'f') {
427 unsigned c1 = *ptr++;
428 if (c1 < '0' || (c1 > '9' && c1 < 'A') ||
429 (c1 > 'F' && c1 < 'a') || c1 > 'f') {
432 keyCodes[len++] = 16*((c0 & 0xF) + 9*(c0 > '9')) +
433 (c1 & 0xF) + 9*(c1 > '9');
435 if (write(session->pty, keyCodes, len) < 0 && errno == EAGAIN) {
436 completePendingRequest(session, "\007", 1, MAX_RESPONSE);
439 httpSendReply(http, 200, "OK", " ");
440 check(session->http != http);
443 // This request is polling for data. Finish any pending requests and
444 // queue (or process) a new one.
445 if (session->http && session->http != http &&
446 !completePendingRequest(session, "", 0, MAX_RESPONSE)) {
447 httpSendReply(http, 400, "Bad Request", NO_MSG);
450 session->http = http;
453 session->connection = serverGetConnection(session->server,
456 if (session->buffered || isNew) {
457 if (completePendingRequest(session, "", 0, MAX_RESPONSE) &&
458 session->connection) {
459 // Reset the timeout, as we just received a new request.
460 serverSetTimeout(session->connection, AJAX_TIMEOUT);
461 if (session->len < MAX_RESPONSE) {
462 // Re-enable input on the child's pty
463 serverConnectionSetEvents(session->server, session->connection,POLLIN);
467 } else if (session->connection) {
468 // Re-enable input on the child's pty
469 serverConnectionSetEvents(session->server, session->connection, POLLIN);
470 serverSetTimeout(session->connection, AJAX_TIMEOUT);
476 static void serveStaticFile(HttpConnection *http, const char *contentType,
477 const char *start, const char *end) {
478 char *body = (char *)start;
479 char *bodyEnd = (char *)end;
481 // Unfortunately, there are still some browsers that are so buggy that they
482 // need special conditional code. In anything that has a "text" MIME type,
483 // we allow simple conditionals. Nested conditionals are not supported.
484 if (!memcmp(contentType, "text/", 5)) {
488 char *elsePtr = NULL;
489 for (char *ptr = body; bodyEnd - ptr >= 6; ) {
491 eol = memchr(eol, '\n', bodyEnd - eol);
497 if (!memcmp(ptr, "[if ", 4)) {
498 char *bracket = memchr(ptr + 4, ']', eol - ptr - 4);
499 if (bracket != NULL && bracket > ptr + 4) {
500 check(tag = malloc(bracket - ptr - 3));
501 memcpy(tag, ptr + 4, bracket - ptr - 4);
502 tag[bracket - ptr - 4] = '\000';
504 const char *userAgent = getFromHashMap(httpGetHeaders(http),
510 // Allow multiple comma separated conditions. Conditions are either
511 // substrings found in the user agent, or they are "DEFINES_..."
512 // tags at the top of user CSS files.
513 for (char *tagPtr = tag; *tagPtr; ) {
514 char *e = strchr(tagPtr, ',');
516 e = strchr(tag, '\000');
520 condTrue = userCSSGetDefine(tagPtr) ||
521 strstr(userAgent, tagPtr) != NULL;
531 // If we find any conditionals, then we need to make a copy of
532 // the text document. We do this lazily, as presumably the majority
533 // of text documents won't have conditionals.
535 check(body = malloc(end - start));
536 memcpy(body, start, end - start);
537 bodyEnd += body - start;
542 // Remember the beginning of the "[if ...]" statement
545 } else if (ifPtr && !elsePtr && eol - ptr >= (ssize_t)strlen(tag) + 7 &&
546 !memcmp(ptr, "[else ", 6) &&
547 !memcmp(ptr + 6, tag, strlen(tag)) &&
548 ptr[6 + strlen(tag)] == ']') {
549 // Found an "[else ...]" statement. Remember where it started.
551 } else if (ifPtr && eol - ptr >= (ssize_t)strlen(tag) + 8 &&
552 !memcmp(ptr, "[endif ", 7) &&
553 !memcmp(ptr + 7, tag, strlen(tag)) &&
554 ptr[7 + strlen(tag)] == ']') {
555 // Found the closing "[endif ...]" statement. Now we can remove those
556 // parts of the conditionals that do not apply to this user agent.
559 s = strchr(ifPtr, '\n') + 1;
560 e = elsePtr ? elsePtr : ptr;
563 s = strchr(elsePtr, '\n') + 1;
570 memmove(ifPtr, s, e - s);
571 memmove(ifPtr + (e - s), eol, bodyEnd - eol);
572 bodyEnd -= (s - ifPtr) + (eol - e);
573 eol = ifPtr + (e - s);
584 char *response = stringPrintf(NULL,
585 "HTTP/1.1 200 OK\r\n"
586 "Content-Type: %s\r\n"
587 "Content-Length: %ld\r\n"
589 contentType, (long)(bodyEnd - body),
591 "Cache-Control: no-cache\r\n");
592 int len = strlen(response);
593 if (strcmp(httpGetMethod(http), "HEAD")) {
594 check(response = realloc(response, len + (bodyEnd - body)));
595 memcpy(response + len, body, bodyEnd - body);
596 len += bodyEnd - body;
599 // If we expanded conditionals, we had to create a temporary copy. Delete
605 httpTransfer(http, response, len);
608 static int shellInABoxHttpHandler(HttpConnection *http, void *arg,
609 const char *buf, int len) {
611 URL *url = newURL(http, buf, len);
612 const HashMap *headers = httpGetHeaders(http);
613 const char *contentType = getFromHashMap(headers, "content-type");
615 // Normalize the path info
616 const char *pathInfo = urlGetPathInfo(url);
617 while (*pathInfo == '/') {
620 const char *endPathInfo;
621 for (endPathInfo = pathInfo;
622 *endPathInfo && *endPathInfo != '/';
625 int pathInfoLength = endPathInfo - pathInfo;
627 if (!pathInfoLength ||
628 (pathInfoLength == 5 && !memcmp(pathInfo, "plain", 5)) ||
629 (pathInfoLength == 6 && !memcmp(pathInfo, "secure", 6))) {
630 // The root page serves the AJAX application.
632 !strncasecmp(contentType, "application/x-www-form-urlencoded", 33)) {
633 // XMLHttpRequest carrying data between the AJAX application and the
635 return dataHandler(http, arg, buf, len, url);
637 char *html = stringPrintf(NULL, rootPageStart,
638 enableSSL ? "true" : "false");
639 serveStaticFile(http, "text/html", html, strrchr(html, '\000'));
641 } else if (pathInfoLength == 8 && !memcmp(pathInfo, "beep.wav", 8)) {
642 // Serve the audio sample for the console bell.
643 serveStaticFile(http, "audio/x-wav", beepStart, beepStart + beepSize - 1);
644 } else if (pathInfoLength == 11 && !memcmp(pathInfo, "enabled.gif", 11)) {
645 // Serve the checkmark icon used in the context menu
646 serveStaticFile(http, "image/gif", enabledStart,
647 enabledStart + enabledSize - 1);
648 } else if (pathInfoLength == 11 && !memcmp(pathInfo, "favicon.ico", 11)) {
650 serveStaticFile(http, "image/x-icon", faviconStart,
651 faviconStart + faviconSize - 1);
652 } else if (pathInfoLength == 13 && !memcmp(pathInfo, "keyboard.html", 13)) {
653 // Serve the keyboard layout
654 serveStaticFile(http, "text/html", keyboardLayoutStart,
655 keyboardLayoutStart + keyboardLayoutSize - 1);
656 } else if (pathInfoLength == 12 && !memcmp(pathInfo, "keyboard.png", 12)) {
657 // Serve the keyboard icon
658 serveStaticFile(http, "image/png", keyboardStart,
659 keyboardStart + keyboardSize - 1);
660 } else if (pathInfoLength == 14 && !memcmp(pathInfo, "ShellInABox.js", 14)) {
661 // Serve both vt100.js and shell_in_a_box.js in the same transaction.
662 // Also, indicate to the client whether the server is SSL enabled.
663 char *userCSSString = getUserCSSString(userCSSList);
664 char *stateVars = stringPrintf(NULL,
665 "serverSupportsSSL = %s;\n"
666 "disableSSLMenu = %s;\n"
667 "suppressAllAudio = %s;\n"
668 "linkifyURLs = %d;\n"
669 "userCSSList = %s;\n\n",
670 enableSSL ? "true" : "false",
671 !enableSSLMenu ? "true" : "false",
672 noBeep ? "true" : "false",
673 linkifyURLs, userCSSString);
675 int stateVarsLength = strlen(stateVars);
676 int contentLength = stateVarsLength +
679 char *response = stringPrintf(NULL,
680 "HTTP/1.1 200 OK\r\n"
681 "Content-Type: text/javascript; charset=utf-8\r\n"
682 "Content-Length: %d\r\n"
685 int headerLength = strlen(response);
686 if (strcmp(httpGetMethod(http), "HEAD")) {
687 check(response = realloc(response, headerLength + contentLength));
688 memcpy(memcpy(memcpy(
689 response + headerLength, stateVars, stateVarsLength)+stateVarsLength,
690 vt100Start, vt100Size - 1) + vt100Size - 1,
691 shellInABoxStart, shellInABoxSize - 1);
696 httpTransfer(http, response, headerLength + contentLength);
697 } else if (pathInfoLength == 10 && !memcmp(pathInfo, "styles.css", 10)) {
698 // Serve the style sheet.
699 serveStaticFile(http, "text/css; charset=utf-8",
700 cssStyleSheet, strrchr(cssStyleSheet, '\000'));
701 } else if (pathInfoLength == 16 && !memcmp(pathInfo, "print-styles.css",16)){
702 // Serve the style sheet.
703 serveStaticFile(http, "text/css; charset=utf-8",
704 printStylesStart, printStylesStart + printStylesSize - 1);
705 } else if (pathInfoLength > 8 && !memcmp(pathInfo, "usercss-", 8)) {
706 // Server user style sheets (if any)
707 struct UserCSS *css = userCSSList;
708 for (int idx = atoi(pathInfo + 8);
709 idx-- > 0 && css; css = css->next ) {
712 serveStaticFile(http, "text/css; charset=utf-8",
713 css->style, css->style + css->styleLen);
715 httpSendReply(http, 404, "File not found", NO_MSG);
718 httpSendReply(http, 404, "File not found", NO_MSG);
725 static int strtoint(const char *s, int minVal, int maxVal) {
728 fatal("Missing numeric value.");
730 long l = strtol(s, &ptr, 10);
731 if (*ptr || l < minVal || l > maxVal) {
732 fatal("Range error on numeric value \"%s\".", s);
737 static void usage(void) {
738 // Drop privileges so that we can tell which uid/gid we would normally
741 uid_t r_uid, e_uid, s_uid;
742 uid_t r_gid, e_gid, s_gid;
743 check(!getresuid(&r_uid, &e_uid, &s_uid));
744 check(!getresgid(&r_gid, &e_gid, &s_gid));
745 const char *user = getUserName(r_uid);
746 const char *group = getGroupName(r_gid);
748 message("Usage: shellinaboxd [OPTIONS]...\n"
749 "Starts an HTTP server that serves terminal emulators to AJAX "
750 "enabled browsers.\n"
752 "List of command line options:\n"
753 " -b, --background[=PIDFILE] run in background\n"
755 " --css=FILE attach contents to CSS style sheet\n"
756 " --cgi[=PORTMIN-PORTMAX] run as CGI\n"
757 " -d, --debug enable debug mode\n"
758 " -f, --static-file=URL:FILE serve static file from URL path\n"
759 " -g, --group=GID switch to this group (default: %s)\n"
760 " -h, --help print this message\n"
761 " --linkify=[none|normal|agressive] default is \"normal\"\n"
762 " --localhost-only only listen on 127.0.0.1\n"
763 " --no-beep suppress all audio output\n"
764 " -n, --numeric do not resolve hostnames\n"
765 " --pidfile=PIDFILE publish pid of daemon process\n"
766 " -p, --port=PORT select a port (default: %d)\n"
767 " -s, --service=SERVICE define one or more services\n"
769 " -q, --quiet turn off all messages\n"
770 " -u, --user=UID switch to this user (default: %s)\n"
771 " --user-css=STYLES defines user-selectable CSS options\n"
772 " -v, --verbose enable logging messages\n"
773 " --version prints version information\n"
775 "Debug, quiet, and verbose are mutually exclusive.\n"
777 "One or more --service arguments define services that should "
778 "be made available\n"
779 "through the web interface:\n"
780 " SERVICE := <url-path> ':' APP\n"
782 #ifdef HAVE_BIN_LOGIN
785 "'SSH' [ : <host> ] | "
786 "USER ':' CWD ':' CMD\n"
787 " USER := %s<username> ':' <groupname>\n"
788 " CWD := 'HOME' | <dir>\n"
789 " CMD := 'SHELL' | <cmdline>\n"
791 "<cmdline> supports variable expansion:\n"
792 " ${columns} - number of columns\n"
794 " ${group} - group name\n"
795 " ${home} - home directory\n"
796 " ${lines} - number of rows\n"
797 " ${peer} - name of remote peer\n"
798 " ${uid} - user id\n"
799 " ${url} - the URL that serves the terminal session\n"
800 " ${user} - user name\n"
802 "One or more --user-css arguments define optional user-selectable "
804 "These options show up in the right-click context menu:\n"
805 " STYLES := GROUP { ';' GROUP }*\n"
806 " GROUP := OPTION { ',' OPTION }*\n"
807 " OPTION := <label> ':' [ '-' | '+' ] <css-file>\n"
809 "OPTIONs that make up a GROUP are mutually exclusive. But "
810 "individual GROUPs are\n"
811 "independent of each other.\n",
812 !serverSupportsSSL() ? "" :
813 " -c, --cert=CERTDIR set certificate dir "
815 " --cert-fd=FD set certificate file from fd\n",
817 !serverSupportsSSL() ? "" :
818 " -t, --disable-ssl disable transparent SSL support\n"
819 " --disable-ssl-menu disallow changing transport mode\n",
820 user, supportsPAM() ? "'AUTH' | " : "");
825 static void destroyExternalFileHashEntry(void *arg ATTR_UNUSED, char *key,
832 static void sigHandler(int signo, siginfo_t *info, void *context) {
836 siglongjmp(jmpenv, 1);
839 static void parseArgs(int argc, char * const argv[]) {
840 int hasSSL = serverSupportsSSL();
846 int verbosity = MSG_DEFAULT;
847 externalFiles = newHashMap(destroyExternalFileHashEntry, NULL);
848 HashMap *serviceTable = newHashMap(destroyServiceHashEntry, NULL);
849 check(cssStyleSheet = strdup(stylesStart));
852 static const char optstring[] = "+hb::c:df:g:np:s:tqu:v";
853 static struct option options[] = {
854 { "help", 0, 0, 'h' },
855 { "background", 2, 0, 'b' },
856 { "cert", 1, 0, 'c' },
857 { "cert-fd", 1, 0, 0 },
860 { "debug", 0, 0, 'd' },
861 { "static-file", 1, 0, 'f' },
862 { "group", 1, 0, 'g' },
863 { "linkify", 1, 0, 0 },
864 { "localhost-only", 0, 0, 0 },
865 { "no-beep", 0, 0, 0 },
866 { "numeric", 0, 0, 'n' },
867 { "pidfile", 1, 0, 0 },
868 { "port", 1, 0, 'p' },
869 { "service", 1, 0, 's' },
870 { "disable-ssl", 0, 0, 't' },
871 { "disable-ssl-menu", 0, 0, 0 },
872 { "quiet", 0, 0, 'q' },
873 { "user", 1, 0, 'u' },
874 { "user-css", 1, 0, 0 },
875 { "verbose", 0, 0, 'v' },
876 { "version", 0, 0, 0 },
879 int c = getopt_long(argc, argv, optstring, options, &idx);
881 for (int i = 0; options[i].name; i++) {
882 if (options[i].val == c) {
891 // Help (or invalid argument)
894 fatal("Failed to parse command line");
900 fatal("CGI and background operations are mutually exclusive");
903 if (optarg && pidfile) {
904 fatal("Only one pidfile can be given");
906 if (optarg && *optarg) {
907 check(pidfile = strdup(optarg));
912 warn("Ignoring certificate directory, as SSL support is unavailable");
914 if (certificateFd >= 0) {
915 fatal("Cannot set both a certificate directory and file handle");
917 if (certificateDir) {
918 fatal("Only one certificate directory can be selected");
921 if (!optarg || !*optarg || stat(optarg, &st) || !S_ISDIR(st.st_mode)) {
922 fatal("\"--cert\" expects a directory name");
924 check(certificateDir = strdup(optarg));
926 // Certificate file descriptor
928 warn("Ignoring certificate directory, as SSL support is unavailable");
930 if (certificateDir) {
931 fatal("Cannot set both a certificate directory and file handle");
933 if (certificateFd >= 0) {
934 fatal("Only one certificate file handle can be provided");
936 if (!optarg || *optarg < '0' || *optarg > '9') {
937 fatal("\"--cert-fd\" expects a valid file handle");
939 int tmpFd = strtoint(optarg, 3, INT_MAX);
940 certificateFd = dup(tmpFd);
941 if (certificateFd < 0) {
942 fatal("Invalid certificate file handle");
944 check(!NOINTR(close(tmpFd)));
948 if (!optarg || !*optarg || stat(optarg, &st) || !S_ISREG(st.st_mode)) {
949 fatal("\"--css\" expects a file name");
951 FILE *css = fopen(optarg, "r");
953 fatal("Cannot read style sheet \"%s\"", optarg);
955 check(cssStyleSheet= realloc(cssStyleSheet, strlen(cssStyleSheet) +
957 char *newData = strrchr(cssStyleSheet, '\000');
959 if (fread(newData, st.st_size, 1, css) != 1) {
960 fatal("Failed to read style sheet \"%s\"", optarg);
962 newData[st.st_size]= '\000';
968 fatal("CGI and background operations are mutually exclusive");
971 fatal("CGI operation and --pidfile= are mutually exclusive");
974 fatal("Cannot specify a port for CGI operation");
977 if (optarg && *optarg) {
978 char *ptr = strchr(optarg, '-');
980 fatal("Syntax error in port range specification");
983 portMin = strtoint(optarg, 1, 65535);
985 portMax = strtoint(ptr + 1, portMin, 65535);
989 if (!logIsDefault() && !logIsDebug()) {
990 fatal("--debug is mutually exclusive with --quiet and --verbose.");
992 verbosity = MSG_DEBUG;
993 logSetLogLevel(verbosity);
996 char *ptr, *path, *file;
997 if ((ptr = strchr(optarg, ':')) == NULL) {
998 fatal("Syntax error in static-file definition \"%s\".", optarg);
1000 check(path = malloc(ptr - optarg + 1));
1001 memcpy(path, optarg, ptr - optarg);
1002 path[ptr - optarg] = '\000';
1003 check(file = strdup(ptr + 1));
1004 if (getRefFromHashMap(externalFiles, path)) {
1005 fatal("Duplicate static-file definition for \"%s\".", path);
1007 addToHashMap(externalFiles, path, file);
1008 } else if (!idx--) {
1010 if (runAsGroup >= 0) {
1011 fatal("Duplicate --group option.");
1013 if (!optarg || !*optarg) {
1014 fatal("\"--group\" expects a group name.");
1016 runAsGroup = parseGroup(optarg, NULL);
1017 } else if (!idx--) {
1019 if (!strcmp(optarg, "none")) {
1021 } else if (!strcmp(optarg, "normal")) {
1023 } else if (!strcmp(optarg, "aggressive")) {
1026 fatal("Invalid argument for --linkify. Must be "
1027 "\"none\", \"normal\", or \"aggressive\".");
1029 } else if (!idx--) {
1032 } else if (!idx--) {
1035 } else if (!idx--) {
1038 } else if (!idx--) {
1041 fatal("CGI operation and --pidfile= are mutually exclusive");
1043 if (!optarg || !*optarg) {
1044 fatal("Must specify a filename for --pidfile= option");
1047 fatal("Only one pidfile can be given");
1049 check(pidfile = strdup(optarg));
1050 } else if (!idx--) {
1053 fatal("Duplicate --port option");
1056 fatal("Cannot specifiy a port for CGI operation");
1058 if (!optarg || *optarg < '0' || *optarg > '9') {
1059 fatal("\"--port\" expects a port number.");
1061 port = strtoint(optarg, 1, 65535);
1062 } else if (!idx--) {
1064 struct Service *service;
1065 service = newService(optarg);
1066 if (getRefFromHashMap(serviceTable, service->path)) {
1067 fatal("Duplicate service description for \"%s\".", service->path);
1069 addToHashMap(serviceTable, service->path, (char *)service);
1070 } else if (!idx--) {
1073 warn("Ignoring disable-ssl option, as SSL support is unavailable");
1076 } else if (!idx--) {
1079 warn("Ignoring disable-ssl-menu option, as SSL support is "
1083 } else if (!idx--) {
1085 if (!logIsDefault() && !logIsQuiet()) {
1086 fatal("--quiet is mutually exclusive with --debug and --verbose.");
1088 verbosity = MSG_QUIET;
1089 logSetLogLevel(verbosity);
1090 } else if (!idx--) {
1092 if (runAsUser >= 0) {
1093 fatal("Duplicate --user option.");
1095 if (!optarg || !*optarg) {
1096 fatal("\"--user\" expects a user name.");
1098 runAsUser = parseUser(optarg, NULL);
1099 } else if (!idx--) {
1101 if (!optarg || !*optarg) {
1102 fatal("\"--user-css\" expects a list of styles sheets and labels");
1104 parseUserCSS(&userCSSList, optarg);
1105 } else if (!idx--) {
1107 if (!logIsDefault() && (!logIsInfo() || logIsDebug())) {
1108 fatal("--verbose is mutually exclusive with --debug and --quiet");
1110 verbosity = MSG_INFO;
1111 logSetLogLevel(verbosity);
1112 } else if (!idx--) {
1114 message("ShellInABox version " VERSION " (revision " VCS_REVISION ")");
1118 if (optind != argc) {
1120 fatal("Failed to parse command line");
1124 for (int i = 0; i < argc; i++) {
1125 buf = stringPrintf(buf, " %s", argv[i]);
1127 info("Command line:%s", buf);
1130 // If the user did not specify a port, use the default one
1131 if (!cgi && !port) {
1135 // If the user did not register any services, provide the default service
1136 if (!getHashmapSize(serviceTable)) {
1137 addToHashMap(serviceTable, "/",
1139 #ifdef HAVE_BIN_LOGIN
1140 geteuid() ? ":SSH" : ":LOGIN"
1146 enumerateServices(serviceTable);
1147 deleteHashMap(serviceTable);
1149 // Do not allow non-root URLs for CGI operation
1151 for (int i = 0; i < numServices; i++) {
1152 if (strcmp(services[i]->path, "/")) {
1153 fatal("Non-root service URLs are incompatible with CGI operation");
1156 check(cgiSessionKey = newSessionKey());
1161 check((pid = fork()) >= 0);
1169 #define O_LARGEFILE 0
1171 int fd = NOINTR(open(pidfile,
1172 O_WRONLY|O_TRUNC|O_LARGEFILE|O_CREAT,
1176 NOINTR(write(fd, buf, snprintf(buf, 40, "%d", (int)getpid())));
1177 check(!NOINTR(close(fd)));
1179 free((char *)pidfile);
1185 static void removeLimits() {
1186 static int res[] = { RLIMIT_CPU, RLIMIT_DATA, RLIMIT_FSIZE, RLIMIT_NPROC };
1187 for (unsigned i = 0; i < sizeof(res)/sizeof(int); i++) {
1189 getrlimit(res[i], &rl);
1190 if (rl.rlim_max < RLIM_INFINITY) {
1191 rl.rlim_max = RLIM_INFINITY;
1192 setrlimit(res[i], &rl);
1193 getrlimit(res[i], &rl);
1195 if (rl.rlim_cur < rl.rlim_max) {
1196 rl.rlim_cur = rl.rlim_max;
1197 setrlimit(res[i], &rl);
1202 static void setUpSSL(Server *server) {
1203 serverEnableSSL(server, enableSSL);
1205 // Enable SSL support (if available)
1207 check(serverSupportsSSL());
1208 if (certificateFd >= 0) {
1209 serverSetCertificateFd(server, certificateFd);
1210 } else if (certificateDir) {
1212 if (strchr(certificateDir, '%')) {
1213 fatal("Invalid certificate directory name \"%s\".", certificateDir);
1215 check(tmp = stringPrintf(NULL, "%s/certificate%%s.pem", certificateDir));
1216 serverSetCertificate(server, tmp, 1);
1219 serverSetCertificate(server, "certificate%s.pem", 1);
1224 int main(int argc, char * const argv[]) {
1225 #ifdef HAVE_SYS_PRCTL_H
1226 // Disable core files
1227 prctl(PR_SET_DUMPABLE, 0, 0, 0, 0);
1229 struct rlimit rl = { 0 };
1230 setrlimit(RLIMIT_CORE, &rl);
1233 // Parse command line arguments
1234 parseArgs(argc, argv);
1236 // Fork the launcher process, allowing us to drop privileges in the main
1238 int launcherFd = forkLauncher();
1240 // Make sure that our timestamps will print in the standard format
1241 setlocale(LC_TIME, "POSIX");
1243 // Create a new web server
1246 check(server = newServer(localhostOnly, port));
1250 // For CGI operation we fork the new server, so that it runs in the
1256 check((pid = fork()) >= 0);
1258 // Wait for child to output initial HTML page
1260 check(!NOINTR(close(fds[1])));
1261 check(!NOINTR(read(fds[0], &wait, 1)));
1262 check(!NOINTR(close(fds[0])));
1265 check(!NOINTR(close(fds[0])));
1266 check(server = newCGIServer(localhostOnly, portMin, portMax,
1271 // Output a <frameset> that includes our root page
1272 check(port = serverGetListeningPort(server));
1273 printf("X-ShellInABox-Port: %d\r\n"
1274 "X-ShellInABox-Pid: %d\r\n"
1275 "Content-type: text/html; charset=utf-8\r\n\r\n",
1277 printfUnchecked(cgiRootStart, port, cgiSessionKey);
1279 check(!NOINTR(close(fds[1])));
1280 closeAllFds((int []){ launcherFd, serverGetFd(server) }, 2);
1281 logSetLogLevel(MSG_QUIET);
1284 // Set log file format
1285 serverSetNumericHosts(server, numericHosts ||
1286 logIsQuiet() || logIsDefault());
1288 // Disable /quit handler
1289 serverRegisterHttpHandler(server, "/quit", NULL, NULL);
1291 // Register HTTP handler(s)
1292 for (int i = 0; i < numServices; i++) {
1293 serverRegisterHttpHandler(server, services[i]->path,
1294 shellInABoxHttpHandler, services[i]);
1297 // Register handlers for external files
1298 iterateOverHashMap(externalFiles, registerExternalFiles, server);
1301 if (!sigsetjmp(jmpenv, 1)) {
1302 // Clean up upon orderly shut down. Do _not_ cleanup if we die
1303 // unexpectedly, as we cannot guarantee if we are still in a valid
1304 // static. This means, we should never catch SIGABRT.
1305 static const int signals[] = { SIGHUP, SIGINT, SIGQUIT, SIGTERM };
1306 struct sigaction sa;
1307 memset(&sa, 0, sizeof(sa));
1308 sa.sa_sigaction = sigHandler;
1309 sa.sa_flags = SA_SIGINFO | SA_RESETHAND;
1310 for (int i = 0; i < sizeof(signals)/sizeof(*signals); ++i) {
1311 sigaction(signals[i], &sa, NULL);
1317 deleteServer(server);
1318 finishAllSessions();
1319 deleteHashMap(externalFiles);
1320 for (int i = 0; i < numServices; i++) {
1321 deleteService(services[i]);
1324 free(certificateDir);
1325 free(cgiSessionKey);
1327 // As a convenience, remove the pidfile, if it is still the version that
1328 // we wrote. In general, pidfiles are not expected to be incredibly
1329 // reliable, as there is no way to properly deal with multiple programs
1330 // accessing the same pidfile. But we at least make a best effort to be
1333 int fd = open(pidfile, O_RDONLY);
1336 NOINTR(sz = read(fd, buf, sizeof(buf)-1));
1340 if (atoi(buf) == getpid()) {
1345 free((char *)pidfile);