shellinabox/shellinabox/shellinaboxd.c
zodiac 7ab3b32465 Initial version of code that allows users to interactively select from
different style sheet options. This code is still incomplete and subject to
change (e.g. the command line syntax might still change). But it is good
enough to demonstrate the concept on simple style sheets (such as selecting
between normal and reverse video).


git-svn-id: https://shellinabox.googlecode.com/svn/trunk@165 0da03de8-d603-11dd-86c2-0f8696b7b6f9
2009-08-11 07:21:51 +00:00

1141 lines
40 KiB
C

// shellinaboxd.c -- A custom web server that makes command line applications
// available as AJAX web applications.
// Copyright (C) 2008-2009 Markus Gutschke <markus@shellinabox.com>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2 as
// published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
// In addition to these license terms, the author grants the following
// additional rights:
//
// If you modify this program, or any covered work, by linking or
// combining it with the OpenSSL project's OpenSSL library (or a
// modified version of that library), containing parts covered by the
// terms of the OpenSSL or SSLeay licenses, the author
// grants you additional permission to convey the resulting work.
// Corresponding Source for a non-source form of such a combination
// shall include the source code for the parts of OpenSSL used as well
// as that of the covered work.
//
// You may at your option choose to remove this additional permission from
// the work, or from any part of it.
//
// It is possible to build this program in a way that it loads OpenSSL
// libraries at run-time. If doing so, the following notices are required
// by the OpenSSL and SSLeay licenses:
//
// This product includes software developed by the OpenSSL Project
// for use in the OpenSSL Toolkit. (http://www.openssl.org/)
//
// This product includes cryptographic software written by Eric Young
// (eay@cryptsoft.com)
//
//
// The most up-to-date version of this program is always available from
// http://shellinabox.com
#define _GNU_SOURCE
#include "config.h"
#include <fcntl.h>
#include <getopt.h>
#include <limits.h>
#include <locale.h>
#include <poll.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/resource.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#ifdef HAVE_SYS_PRCTL_H
#include <sys/prctl.h>
#endif
#include "libhttp/http.h"
#include "logging/logging.h"
#include "shellinabox/externalfile.h"
#include "shellinabox/launcher.h"
#include "shellinabox/privileges.h"
#include "shellinabox/service.h"
#include "shellinabox/session.h"
#include "shellinabox/usercss.h"
#define PORTNUM 4200
#define MAX_RESPONSE 2048
static int port;
static int portMin;
static int portMax;
static int localhostOnly = 0;
static int noBeep = 0;
static int numericHosts = 0;
static int enableSSL = 1;
static int enableSSLMenu = 1;
static int linkifyURLs = 1;
static char *certificateDir;
static int certificateFd = -1;
static HashMap *externalFiles;
static Server *cgiServer;
static char *cgiSessionKey;
static int cgiSessions;
static char *cssStyleSheet;
static struct UserCSS *userCSSList;
static char *jsonEscape(const char *buf, int len) {
static const char *hexDigit = "0123456789ABCDEF";
// Determine the space that is needed to encode the buffer
int count = 0;
const char *ptr = buf;
for (int i = 0; i < len; i++) {
unsigned char ch = *(unsigned char *)ptr++;
if (ch < ' ') {
switch (ch) {
case '\b': case '\f': case '\n': case '\r': case '\t':
count += 2;
break;
default:
count += 6;
break;
}
} else if (ch == '"' || ch == '\\' || ch == '/') {
count += 2;
} else if (ch > '\x7F') {
count += 6;
} else {
count++;
}
}
// Encode the buffer using JSON string escaping
char *result;
check(result = malloc(count + 1));
char *dst = result;
ptr = buf;
for (int i = 0; i < len; i++) {
unsigned char ch = *(unsigned char *)ptr++;
if (ch < ' ') {
*dst++ = '\\';
switch (ch) {
case '\b': *dst++ = 'b'; break;
case '\f': *dst++ = 'f'; break;
case '\n': *dst++ = 'n'; break;
case '\r': *dst++ = 'r'; break;
case '\t': *dst++ = 't'; break;
default:
unicode:
*dst++ = 'u';
*dst++ = '0';
*dst++ = '0';
*dst++ = hexDigit[ch >> 4];
*dst++ = hexDigit[ch & 0xF];
break;
}
} else if (ch == '"' || ch == '\\' || ch == '/') {
*dst++ = '\\';
*dst++ = ch;
} else if (ch > '\x7F') {
*dst++ = '\\';
goto unicode;
} else {
*dst++ = ch;
}
}
*dst++ = '\000';
return result;
}
static int printfUnchecked(const char *format, ...) {
// Some Linux distributions enable -Wformat=2 by default. This is a
// very unfortunate decision, as that option generates a lot of false
// positives. We try to work around the problem by defining an unchecked
// version of "printf()"
va_list ap;
va_start(ap, format);
int rc = vprintf(format, ap);
va_end(ap);
return rc;
}
static int completePendingRequest(struct Session *session,
const char *buf, int len, int maxLength) {
// If there is no pending HTTP request, save the data and return
// immediately.
if (!session->http) {
if (len) {
if (session->buffered) {
check(session->buffered = realloc(session->buffered,
session->len + len));
memcpy(session->buffered + session->len, buf, len);
session->len += len;
} else {
check(session->buffered = malloc(len));
memcpy(session->buffered, buf, len);
session->len = len;
}
}
} else {
// If we have a pending HTTP request, we can reply to it, now.
char *data;
if (session->buffered) {
check(session->buffered = realloc(session->buffered,
session->len + len));
memcpy(session->buffered + session->len, buf, len);
session->len += len;
if (maxLength > 0 && session->len > maxLength) {
data = jsonEscape(session->buffered, maxLength);
session->len -= maxLength;
memmove(session->buffered, session->buffered + maxLength,
session->len);
} else {
data = jsonEscape(session->buffered, session->len);
free(session->buffered);
session->buffered = NULL;
session->len = 0;
}
} else {
if (maxLength > 0 && len > maxLength) {
session->len = len - maxLength;
check(session->buffered = malloc(session->len));
memcpy(session->buffered, buf + maxLength, session->len);
data = jsonEscape(buf, maxLength);
} else {
data = jsonEscape(buf, len);
}
}
char *json = stringPrintf(NULL, "{"
"\"session\":\"%s\","
"\"data\":\"%s\""
"}",
session->sessionKey, data);
free(data);
HttpConnection *http = session->http;
char *response = stringPrintf(NULL,
"HTTP/1.1 200 OK\r\n"
"Content-Type: application/json; "
"charset=utf-8\r\n"
"Content-Length: %ld\r\n"
"Cache-Control: no-cache\r\n"
"\r\n"
"%s",
(long)strlen(json),
strcmp(httpGetMethod(http),
"HEAD") ? json : "");
free(json);
session->http = NULL;
httpTransfer(http, response, strlen(response));
}
if (session->done && !session->buffered) {
finishSession(session);
return 0;
}
return 1;
}
static void sessionDone(void *arg) {
debug("Child terminated");
struct Session *session = (struct Session *)arg;
session->done = 1;
addToGraveyard(session);
completePendingRequest(session, "", 0, INT_MAX);
}
static int handleSession(struct ServerConnection *connection, void *arg,
short *events, short revents) {
struct Session *session = (struct Session *)arg;
session->connection = connection;
int len = MAX_RESPONSE - session->len;
if (len <= 0) {
len = 1;
}
char buf[len];
int bytes = 0;
if (revents & POLLIN) {
bytes = NOINTR(read(session->pty, buf, len));
if (bytes <= 0) {
return 0;
}
}
int timedOut = serverGetTimeout(connection) < 0;
if (bytes || timedOut) {
if (!session->http && timedOut) {
debug("Timeout. Closing session.");
return 0;
}
check(!session->done);
check(completePendingRequest(session, buf, bytes, MAX_RESPONSE));
connection = serverGetConnection(session->server,
connection,
session->pty);
session->connection = connection;
if (session->len >= MAX_RESPONSE) {
serverConnectionSetEvents(session->server, connection, 0);
}
serverSetTimeout(connection, AJAX_TIMEOUT);
return 1;
} else {
return 0;
}
}
static int invalidatePendingHttpSession(void *arg, const char *key,
char **value) {
struct Session *session = *(struct Session **)value;
if (session->http && session->http == (HttpConnection *)arg) {
debug("Clearing pending HTTP connection for session %s", key);
session->http = NULL;
serverDeleteConnection(session->server, session->pty);
// Return zero in order to remove this HTTP from the "session" hashmap
return 0;
}
// If the session is still in use, do not remove it from the "sessions" map
return 1;
}
static int dataHandler(HttpConnection *http, struct Service *service,
const char *buf, int len, URL *url) {
if (!buf) {
// Somebody unexpectedly closed our http connection (e.g. because of a
// timeout). This is the last notification that we will get.
deleteURL(url);
iterateOverSessions(invalidatePendingHttpSession, http);
return HTTP_DONE;
}
// Find an existing session, or create the record for a new one
int isNew;
struct Session *session = findCGISession(&isNew, http, url, cgiSessionKey);
if (session == NULL) {
httpSendReply(http, 400, "Bad Request", NO_MSG);
return HTTP_DONE;
}
// Sanity check
if (!isNew && strcmp(session->peerName, httpGetPeerName(http))) {
error("Peername changed from %s to %s",
session->peerName, httpGetPeerName(http));
httpSendReply(http, 400, "Bad Request", NO_MSG);
return HTTP_DONE;
}
const HashMap *args = urlGetArgs(session->url);
int oldWidth = session->width;
int oldHeight = session->height;
const char *width = getFromHashMap(args, "width");
const char *height = getFromHashMap(args, "height");
const char *keys = getFromHashMap(args, "keys");
// Adjust window dimensions if provided by client
if (width && height) {
session->width = atoi(width);
session->height = atoi(height);
}
// Create a new session, if the client did not provide an existing one
if (isNew) {
if (cgiServer && cgiSessions++) {
serverExitLoop(cgiServer, 1);
abandonSession(session);
httpSendReply(http, 400, "Bad Request", NO_MSG);
return HTTP_DONE;
}
session->http = http;
if (launchChild(service->id, session) < 0) {
abandonSession(session);
httpSendReply(http, 500, "Internal Error", NO_MSG);
return HTTP_DONE;
}
if (cgiServer) {
terminateLauncher();
}
session->connection = serverAddConnection(httpGetServer(http),
session->pty, handleSession,
sessionDone, session);
serverSetTimeout(session->connection, AJAX_TIMEOUT);
}
// Reset window dimensions of the pseudo TTY, if changed since last time set.
if (session->width > 0 && session->height > 0 &&
(session->width != oldWidth || session->height != oldHeight)) {
debug("Window size changed to %dx%d", session->width, session->height);
setWindowSize(session->pty, session->width, session->height);
}
// Process keypresses, if any. Then send a synchronous reply.
if (keys) {
char *keyCodes;
check(keyCodes = malloc(strlen(keys)/2));
int len = 0;
for (const unsigned char *ptr = (const unsigned char *)keys; ;) {
unsigned c0 = *ptr++;
if (c0 < '0' || (c0 > '9' && c0 < 'A') ||
(c0 > 'F' && c0 < 'a') || c0 > 'f') {
break;
}
unsigned c1 = *ptr++;
if (c1 < '0' || (c1 > '9' && c1 < 'A') ||
(c1 > 'F' && c1 < 'a') || c1 > 'f') {
break;
}
keyCodes[len++] = 16*((c0 & 0xF) + 9*(c0 > '9')) +
(c1 & 0xF) + 9*(c1 > '9');
}
if (write(session->pty, keyCodes, len) < 0 && errno == EAGAIN) {
completePendingRequest(session, "\007", 1, MAX_RESPONSE);
}
free(keyCodes);
httpSendReply(http, 200, "OK", " ");
check(session->http != http);
return HTTP_DONE;
} else {
// This request is polling for data. Finish any pending requests and
// queue (or process) a new one.
if (session->http && session->http != http &&
!completePendingRequest(session, "", 0, MAX_RESPONSE)) {
httpSendReply(http, 400, "Bad Request", NO_MSG);
return HTTP_DONE;
}
session->http = http;
}
session->connection = serverGetConnection(session->server,
session->connection,
session->pty);
if (session->buffered || isNew) {
if (completePendingRequest(session, "", 0, MAX_RESPONSE) &&
session->connection) {
// Reset the timeout, as we just received a new request.
serverSetTimeout(session->connection, AJAX_TIMEOUT);
if (session->len < MAX_RESPONSE) {
// Re-enable input on the child's pty
serverConnectionSetEvents(session->server, session->connection,POLLIN);
}
}
return HTTP_DONE;
} else if (session->connection) {
// Re-enable input on the child's pty
serverConnectionSetEvents(session->server, session->connection, POLLIN);
serverSetTimeout(session->connection, AJAX_TIMEOUT);
}
return HTTP_SUSPEND;
}
static void serveStaticFile(HttpConnection *http, const char *contentType,
const char *start, const char *end) {
char *response = stringPrintf(NULL,
"HTTP/1.1 200 OK\r\n"
"Content-Type: %s\r\n"
"Content-Length: %ld\r\n"
"\r\n",
contentType, (long)(end - start));
int len = strlen(response);
if (strcmp(httpGetMethod(http), "HEAD")) {
check(response = realloc(response, len + (end - start)));
memcpy(response + len, start, end - start);
len += end - start;
}
httpTransfer(http, response, len);
}
static const char *addr(const char *a) {
// Work-around for a gcc bug that could occasionally generate invalid
// assembly instructions when optimizing code too agressively.
asm volatile("");
return a;
}
static int shellInABoxHttpHandler(HttpConnection *http, void *arg,
const char *buf, int len) {
checkGraveyard();
URL *url = newURL(http, buf, len);
const HashMap *headers = httpGetHeaders(http);
const char *contentType = getFromHashMap(headers, "content-type");
// Normalize the path info
const char *pathInfo = urlGetPathInfo(url);
while (*pathInfo == '/') {
pathInfo++;
}
const char *endPathInfo;
for (endPathInfo = pathInfo;
*endPathInfo && *endPathInfo != '/';
endPathInfo++) {
}
int pathInfoLength = endPathInfo - pathInfo;
if (!pathInfoLength ||
(pathInfoLength == 5 && !memcmp(pathInfo, "plain", 5)) ||
(pathInfoLength == 6 && !memcmp(pathInfo, "secure", 6))) {
// The root page serves the AJAX application.
if (contentType &&
!strncasecmp(contentType, "application/x-www-form-urlencoded", 33)) {
// XMLHttpRequest carrying data between the AJAX application and the
// client session.
return dataHandler(http, arg, buf, len, url);
}
extern char rootPageStart[];
extern char rootPageEnd[];
char *rootPage;
check(rootPage = malloc(rootPageEnd - rootPageStart + 1));
memcpy(rootPage, rootPageStart, rootPageEnd - rootPageStart);
rootPage[rootPageEnd - rootPageStart] = '\000';
char *html = stringPrintf(NULL, rootPage,
enableSSL ? "true" : "false");
serveStaticFile(http, "text/html", html, strrchr(html, '\000'));
free(html);
free(rootPage);
} else if (pathInfoLength == 8 && !memcmp(pathInfo, "beep.wav", 8)) {
// Serve the audio sample for the console bell.
extern char beepStart[];
extern char beepEnd[];
serveStaticFile(http, "audio/x-wav", beepStart, beepEnd);
} else if (pathInfoLength == 11 && !memcmp(pathInfo, "favicon.ico", 11)) {
// Serve the favicon
extern char faviconStart[];
extern char faviconEnd[];
serveStaticFile(http, "image/x-icon", faviconStart, faviconEnd);
} else if (pathInfoLength == 14 && !memcmp(pathInfo, "ShellInABox.js", 14)) {
// Serve both vt100.js and shell_in_a_box.js in the same transaction.
// Also, indicate to the client whether the server is SSL enabled.
extern char vt100Start[];
extern char vt100End[];
extern char shellInABoxStart[];
extern char shellInABoxEnd[];
char *userCSSString = getUserCSSString(userCSSList);
char *stateVars = stringPrintf(NULL,
"serverSupportsSSL = %s;\n"
"disableSSLMenu = %s;\n"
"suppressAllAudio = %s;\n"
"linkifyURLs = %d;\n"
"userCSSList = %s;\n\n",
enableSSL ? "true" : "false",
!enableSSLMenu ? "true" : "false",
noBeep ? "true" : "false",
linkifyURLs, userCSSString);
free(userCSSString);
int stateVarsLength = strlen(stateVars);
int contentLength = stateVarsLength +
(addr(vt100End) - addr(vt100Start)) +
(addr(shellInABoxEnd) - addr(shellInABoxStart));
char *response = stringPrintf(NULL,
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/javascript; charset=utf-8\r\n"
"Content-Length: %d\r\n"
"\r\n",
contentLength);
int headerLength = strlen(response);
if (strcmp(httpGetMethod(http), "HEAD")) {
check(response = realloc(response, headerLength + contentLength));
memcpy(memcpy(memcpy(
response + headerLength, stateVars, stateVarsLength)+stateVarsLength,
vt100Start, vt100End - vt100Start) + (vt100End - vt100Start),
shellInABoxStart, shellInABoxEnd - shellInABoxStart);
} else {
contentLength = 0;
}
free(stateVars);
httpTransfer(http, response, headerLength + contentLength);
} else if (pathInfoLength == 10 && !memcmp(pathInfo, "styles.css", 10)) {
// Serve the style sheet.
serveStaticFile(http, "text/css; charset=utf-8",
cssStyleSheet, strrchr(cssStyleSheet, '\000'));
} else if (pathInfoLength > 8 && !memcmp(pathInfo, "usercss-", 8)) {
// Server user style sheets (if any)
struct UserCSS *css = userCSSList;
for (int idx = atoi(pathInfo + 8);
idx-- > 0 && css; css = css->next ) {
}
if (css) {
serveStaticFile(http, "text/css; charset=utf-8",
css->style, css->style + css->styleLen);
} else {
httpSendReply(http, 404, "File not found", NO_MSG);
}
} else {
httpSendReply(http, 404, "File not found", NO_MSG);
}
deleteURL(url);
return HTTP_DONE;
}
static int strtoint(const char *s, int minVal, int maxVal) {
char *ptr;
if (!*s) {
fatal("Missing numeric value.");
}
long l = strtol(s, &ptr, 10);
if (*ptr || l < minVal || l > maxVal) {
fatal("Range error on numeric value \"%s\".", s);
}
return l;
}
static void usage(void) {
// Drop privileges so that we can tell which uid/gid we would normally
// run at.
dropPrivileges();
uid_t r_uid, e_uid, s_uid;
uid_t r_gid, e_gid, s_gid;
check(!getresuid(&r_uid, &e_uid, &s_uid));
check(!getresgid(&r_gid, &e_gid, &s_gid));
const char *user = getUserName(r_uid);
const char *group = getGroupName(r_gid);
message("Usage: shellinaboxd [OPTIONS]...\n"
"Starts an HTTP server that serves terminal emulators to AJAX "
"enabled browsers.\n"
"\n"
"List of command line options:\n"
" -b, --background[=PIDFILE] run in background\n"
"%s"
" --css=FILE attach contents to CSS style sheet\n"
" --cgi[=PORTMIN-PORTMAX] run as CGI\n"
" -d, --debug enable debug mode\n"
" -f, --static-file=URL:FILE serve static file from URL path\n"
" -g, --group=GID switch to this group (default: %s)\n"
" -h, --help print this message\n"
" --linkify=[none|normal|agressive] default is \"normal\"\n"
" --localhost-only only listen on 127.0.0.1\n"
" --no-beep suppress all audio output\n"
" -n, --numeric do not resolve hostnames\n"
" -p, --port=PORT select a port (default: %d)\n"
" -s, --service=SERVICE define one or more services\n"
"%s"
" -q, --quiet turn off all messages\n"
" -u, --user=UID switch to this user (default: %s)\n"
" --user-css=STYLES defines user-selectable CSS options\n"
" -v, --verbose enable logging messages\n"
" --version prints version information\n"
"\n"
"Debug, quiet, and verbose are mutually exclusive.\n"
"\n"
"One or more --service arguments define services that should "
"be made available\n"
"through the web interface:\n"
" SERVICE := <url-path> ':' APP\n"
" APP := 'LOGIN' | 'SSH' [ : <host> ] | "
"USER ':' CWD ':' <cmdline>\n"
" USER := %s<username> ':' <groupname>\n"
" CWD := 'HOME' | <dir>\n"
"\n"
"<cmdline> supports variable expansion:\n"
" ${columns} - number of columns\n"
" ${gid} - gid id\n"
" ${group} - group name\n"
" ${home} - home directory\n"
" ${lines} - number of rows\n"
" ${peer} - name of remote peer\n"
" ${uid} - user id\n"
" ${user} - user name\n"
"\n"
"One or more --user-css arguments define optional user-selectable "
"CSS options.\n"
"These options show up in the right-click context menu:\n"
" STYLES := GROUP { ';' GROUP }*\n"
" GROUP := OPTION { ',' OPTION }*\n"
" OPTION := <label> ':' [ '-' | '+' ] <css-file>\n"
"\n"
"OPTIONs that make up a GROUP are mutually exclusive. But "
"individual GROUPs are\n"
"independent of each other.\n",
!serverSupportsSSL() ? "" :
" -c, --cert=CERTDIR set certificate dir "
"(default: $PWD)\n"
" --cert-fd=FD set certificate file from fd\n",
group, PORTNUM,
!serverSupportsSSL() ? "" :
" -t, --disable-ssl disable transparent SSL support\n"
" --disable-ssl-menu disallow changing transport mode\n",
user, supportsPAM() ? "'AUTH' | " : "");
free((char *)user);
free((char *)group);
}
static void destroyExternalFileHashEntry(void *arg, char *key, char *value) {
free(key);
free(value);
}
static void parseArgs(int argc, char * const argv[]) {
int hasSSL = serverSupportsSSL();
if (!hasSSL) {
enableSSL = 0;
}
int demonize = 0;
int cgi = 0;
const char *pidfile = NULL;
int verbosity = MSG_DEFAULT;
externalFiles = newHashMap(destroyExternalFileHashEntry, NULL);
HashMap *serviceTable = newHashMap(destroyServiceHashEntry, NULL);
extern char stylesStart[];
extern char stylesEnd[];
check(cssStyleSheet = malloc(stylesEnd - stylesStart));
memcpy(cssStyleSheet, stylesStart, stylesEnd - stylesStart);
cssStyleSheet[stylesEnd - stylesStart] = '\000';
for (;;) {
static const char optstring[] = "+hb::c:df:g:np:s:tqu:v";
static struct option options[] = {
{ "help", 0, 0, 'h' },
{ "background", 2, 0, 'b' },
{ "cert", 1, 0, 'c' },
{ "cert-fd", 1, 0, 0 },
{ "css", 1, 0, 0 },
{ "cgi", 2, 0, 0 },
{ "debug", 0, 0, 'd' },
{ "static-file", 1, 0, 'f' },
{ "group", 1, 0, 'g' },
{ "linkify", 1, 0, 0 },
{ "localhost-only", 0, 0, 0 },
{ "no-beep", 0, 0, 0 },
{ "numeric", 0, 0, 'n' },
{ "port", 1, 0, 'p' },
{ "service", 1, 0, 's' },
{ "disable-ssl", 0, 0, 't' },
{ "disable-ssl-menu", 0, 0, 0 },
{ "quiet", 0, 0, 'q' },
{ "user", 1, 0, 'u' },
{ "user-css", 1, 0, 0 },
{ "verbose", 0, 0, 'v' },
{ "version", 0, 0, 0 },
{ 0, 0, 0, 0 } };
int idx = -1;
int c = getopt_long(argc, argv, optstring, options, &idx);
if (c > 0) {
for (int i = 0; options[i].name; i++) {
if (options[i].val == c) {
idx = i;
break;
}
}
} else if (c < 0) {
break;
}
if (idx-- <= 0) {
// Help (or invalid argument)
usage();
if (idx < -1) {
fatal("Failed to parse command line");
}
exit(0);
} else if (!idx--) {
// Background
if (cgi) {
fatal("CGI and background operations are mutually exclusive");
}
demonize = 1;
if (optarg && pidfile) {
fatal("Only one pidfile can be given");
}
if (optarg && *optarg) {
pidfile = strdup(optarg);
}
} else if (!idx--) {
// Certificate
if (!hasSSL) {
warn("Ignoring certificate directory, as SSL support is unavailable");
}
if (certificateFd >= 0) {
fatal("Cannot set both a certificate directory and file handle");
}
if (certificateDir) {
fatal("Only one certificate directory can be selected");
}
struct stat st;
if (!optarg || !*optarg || stat(optarg, &st) || !S_ISDIR(st.st_mode)) {
fatal("\"--cert\" expects a directory name");
}
check(certificateDir = strdup(optarg));
} else if (!idx--) {
// Certificate file descriptor
if (!hasSSL) {
warn("Ignoring certificate directory, as SSL support is unavailable");
}
if (certificateDir) {
fatal("Cannot set both a certificate directory and file handle");
}
if (certificateFd >= 0) {
fatal("Only one certificate file handle can be provided");
}
if (!optarg || *optarg < '0' || *optarg > '9') {
fatal("\"--cert-fd\" expects a valid file handle");
}
int tmpFd = strtoint(optarg, 3, INT_MAX);
certificateFd = dup(tmpFd);
if (certificateFd < 0) {
fatal("Invalid certificate file handle");
}
check(!NOINTR(close(tmpFd)));
} else if (!idx--) {
// CSS
struct stat st;
if (!optarg || !*optarg || stat(optarg, &st) || !S_ISREG(st.st_mode)) {
fatal("\"--css\" expects a file name");
}
FILE *css = fopen(optarg, "r");
if (!css) {
fatal("Cannot read style sheet \"%s\"", optarg);
} else {
check(cssStyleSheet= realloc(cssStyleSheet, strlen(cssStyleSheet) +
st.st_size + 2));
char *newData = strrchr(cssStyleSheet, '\000');
*newData++ = '\n';
if (fread(newData, 1, st.st_size, css) != st.st_size) {
fatal("Failed to read style sheet \"%s\"", optarg);
}
newData[st.st_size]= '\000';
fclose(css);
}
} else if (!idx--) {
// CGI
if (demonize) {
fatal("CGI and background operations are mutually exclusive");
}
if (port) {
fatal("Cannot specify a port for CGI operation");
}
cgi = 1;
if (optarg && *optarg) {
char *ptr = strchr(optarg, '-');
if (!ptr) {
fatal("Syntax error in port range specification");
}
*ptr = '\000';
portMin = strtoint(optarg, 1, 65535);
*ptr = '-';
portMax = strtoint(ptr + 1, portMin, 65535);
}
} else if (!idx--) {
// Debug
if (!logIsDefault() && !logIsDebug()) {
fatal("--debug is mutually exclusive with --quiet and --verbose.");
}
verbosity = MSG_DEBUG;
logSetLogLevel(verbosity);
} else if (!idx--) {
// Static file
char *ptr, *path, *file;
if ((ptr = strchr(optarg, ':')) == NULL) {
fatal("Syntax error in static-file definition \"%s\".", optarg);
}
check(path = malloc(ptr - optarg + 1));
memcpy(path, optarg, ptr - optarg);
path[ptr - optarg] = '\000';
file = strdup(ptr + 1);
if (getRefFromHashMap(externalFiles, path)) {
fatal("Duplicate static-file definition for \"%s\".", path);
}
addToHashMap(externalFiles, path, file);
} else if (!idx--) {
// Group
if (runAsGroup >= 0) {
fatal("Duplicate --group option.");
}
if (!optarg || !*optarg) {
fatal("\"--group\" expects a group name.");
}
runAsGroup = parseGroup(optarg, NULL);
} else if (!idx--) {
// Linkify
if (!strcmp(optarg, "none")) {
linkifyURLs = 0;
} else if (!strcmp(optarg, "normal")) {
linkifyURLs = 1;
} else if (!strcmp(optarg, "aggressive")) {
linkifyURLs = 2;
} else {
fatal("Invalid argument for --linkify. Must be "
"\"none\", \"normal\", or \"aggressive\".");
}
} else if (!idx--) {
// Localhost Only
localhostOnly = 1;
} else if (!idx--) {
// No Beep
noBeep = 1;
} else if (!idx--) {
// Numeric
numericHosts = 1;
} else if (!idx--) {
// Port
if (port) {
fatal("Duplicate --port option");
}
if (cgi) {
fatal("Cannot specifiy a port for CGI operation");
}
if (!optarg || *optarg < '0' || *optarg > '9') {
fatal("\"--port\" expects a port number.");
}
port = strtoint(optarg, 1, 65535);
} else if (!idx--) {
// Service
struct Service *service;
service = newService(optarg);
if (getRefFromHashMap(serviceTable, service->path)) {
fatal("Duplicate service description for \"%s\".", service->path);
}
addToHashMap(serviceTable, service->path, (char *)service);
} else if (!idx--) {
// Disable SSL
if (!hasSSL) {
warn("Ignoring disable-ssl option, as SSL support is unavailable");
}
enableSSL = 0;
} else if (!idx--) {
// Disable SSL Menu
if (!hasSSL) {
warn("Ignoring disable-ssl-menu option, as SSL support is "
"unavailable");
}
enableSSLMenu = 0;
} else if (!idx--) {
// Quiet
if (!logIsDefault() && !logIsQuiet()) {
fatal("--quiet is mutually exclusive with --debug and --verbose.");
}
verbosity = MSG_QUIET;
logSetLogLevel(verbosity);
} else if (!idx--) {
// User
if (runAsUser >= 0) {
fatal("Duplicate --user option.");
}
if (!optarg || !*optarg) {
fatal("\"--user\" expects a user name.");
}
runAsUser = parseUser(optarg, NULL);
} else if (!idx--) {
// User CSS
if (!optarg || !*optarg) {
fatal("\"--user-css\" expects a list of styles sheets and labels");
}
parseUserCSS(&userCSSList, optarg);
} else if (!idx--) {
// Verbose
if (!logIsDefault() && (!logIsInfo() || logIsDebug())) {
fatal("--verbose is mutually exclusive with --debug and --quiet");
}
verbosity = MSG_INFO;
logSetLogLevel(verbosity);
} else if (!idx--) {
// Version
message("ShellInABox version " VERSION " (revision " VCS_REVISION ")");
exit(0);
}
}
if (optind != argc) {
usage();
fatal("Failed to parse command line");
}
char *buf = NULL;
check(argc >= 1);
for (int i = 0; i < argc; i++) {
buf = stringPrintf(buf, " %s", argv[i]);
}
info("Command line:%s", buf);
free(buf);
// If the user did not specify a port, use the default one
if (!cgi && !port) {
port = PORTNUM;
}
// If the user did not register any services, provide the default service
if (!getHashmapSize(serviceTable)) {
addToHashMap(serviceTable, "/", (char *)newService(geteuid() ? ":SSH" :
":LOGIN"));
}
enumerateServices(serviceTable);
deleteHashMap(serviceTable);
// Do not allow non-root URLs for CGI operation
if (cgi) {
for (int i = 0; i < numServices; i++) {
if (strcmp(services[i]->path, "/")) {
fatal("Non-root service URLs are incompatible with CGI operation");
}
}
check(cgiSessionKey = newSessionKey());
}
if (demonize) {
pid_t pid;
check((pid = fork()) >= 0);
if (pid) {
_exit(0);
}
setsid();
if (pidfile) {
#ifndef O_LARGEFILE
#define O_LARGEFILE 0
#endif
int fd = NOINTR(open(pidfile,
O_WRONLY|O_TRUNC|O_LARGEFILE|O_CREAT,
0644));
if (fd >= 0) {
char buf[40];
NOINTR(write(fd, buf, snprintf(buf, 40, "%d", (int)getpid())));
check(!NOINTR(close(fd)));
}
}
}
free((char *)pidfile);
}
static void removeLimits() {
static int res[] = { RLIMIT_CPU, RLIMIT_DATA, RLIMIT_FSIZE, RLIMIT_NPROC };
for (int i = 0; i < sizeof(res)/sizeof(int); i++) {
struct rlimit rl;
getrlimit(res[i], &rl);
if (rl.rlim_max < RLIM_INFINITY) {
rl.rlim_max = RLIM_INFINITY;
setrlimit(res[i], &rl);
getrlimit(res[i], &rl);
}
if (rl.rlim_cur < rl.rlim_max) {
rl.rlim_cur = rl.rlim_max;
setrlimit(res[i], &rl);
}
}
}
static void setUpSSL(Server *server) {
serverEnableSSL(server, enableSSL);
// Enable SSL support (if available)
if (enableSSL) {
check(serverSupportsSSL());
if (certificateFd >= 0) {
serverSetCertificateFd(server, certificateFd);
} else if (certificateDir) {
char *tmp;
if (strchr(certificateDir, '%')) {
fatal("Invalid certificate directory name \"%s\".", certificateDir);
}
check(tmp = stringPrintf(NULL, "%s/certificate%%s.pem", certificateDir));
serverSetCertificate(server, tmp, 1);
free(tmp);
} else {
serverSetCertificate(server, "certificate%s.pem", 1);
}
}
}
int main(int argc, char * const argv[]) {
#ifdef HAVE_SYS_PRCTL_H
// Disable core files
prctl(PR_SET_DUMPABLE, 0, 0, 0, 0);
#endif
struct rlimit rl = { 0 };
setrlimit(RLIMIT_CORE, &rl);
removeLimits();
// Parse command line arguments
parseArgs(argc, argv);
// Fork the launcher process, allowing us to drop privileges in the main
// process.
int launcherFd = forkLauncher();
// Make sure that our timestamps will print in the standard format
setlocale(LC_TIME, "POSIX");
// Create a new web server
Server *server;
if (port) {
check(server = newServer(localhostOnly, port));
dropPrivileges();
setUpSSL(server);
} else {
// For CGI operation we fork the new server, so that it runs in the
// background.
pid_t pid;
int fds[2];
dropPrivileges();
check(!pipe(fds));
check((pid = fork()) >= 0);
if (pid) {
// Wait for child to output initial HTML page
char wait;
check(!NOINTR(close(fds[1])));
check(!NOINTR(read(fds[0], &wait, 1)));
check(!NOINTR(close(fds[0])));
_exit(0);
}
check(!NOINTR(close(fds[0])));
check(server = newCGIServer(localhostOnly, portMin, portMax,
AJAX_TIMEOUT));
cgiServer = server;
setUpSSL(server);
// Output a <frameset> that includes our root page
check(port = serverGetListeningPort(server));
extern char cgiRootStart[];
extern char cgiRootEnd[];
char *cgiRoot;
check(cgiRoot = malloc(cgiRootEnd - cgiRootStart + 1));
memcpy(cgiRoot, cgiRootStart, cgiRootEnd - cgiRootStart);
cgiRoot[cgiRootEnd - cgiRootStart] = '\000';
printf("X-ShellInABox-Port: %d\r\n"
"X-ShellInABox-Pid: %d\r\n"
"Content-type: text/html; charset=utf-8\r\n\r\n",
port, pid);
printfUnchecked(cgiRoot, port, cgiSessionKey);
fflush(stdout);
free(cgiRoot);
check(!NOINTR(close(fds[1])));
closeAllFds((int []){ launcherFd, serverGetFd(server) }, 2);
logSetLogLevel(MSG_QUIET);
}
// Set log file format
serverSetNumericHosts(server, numericHosts);
// Disable /quit handler
serverRegisterHttpHandler(server, "/quit", NULL, NULL);
// Register HTTP handler(s)
for (int i = 0; i < numServices; i++) {
serverRegisterHttpHandler(server, services[i]->path,
shellInABoxHttpHandler, services[i]);
}
// Register handlers for external files
iterateOverHashMap(externalFiles, registerExternalFiles, server);
// Start the server
serverLoop(server);
// Clean up
deleteServer(server);
finishAllSessions();
deleteHashMap(externalFiles);
for (int i = 0; i < numServices; i++) {
deleteService(services[i]);
}
free(services);
free(certificateDir);
free(cgiSessionKey);
info("Done");
_exit(0);
}