f3bb90eaf4
building from tarball can be tested this way: rsync -avix . /tmp/shellinabox --exclude-from=.gitignore --exclude=.git --delete --delete-excluded (cd /tmp/shellinabox && autoreconf -i && ./configure && make && ./shellinaboxd --version)
1356 lines
48 KiB
C
1356 lines
48 KiB
C
// shellinaboxd.c -- A custom web server that makes command line applications
|
|
// available as AJAX web applications.
|
|
// Copyright (C) 2008-2010 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 <setjmp.h>
|
|
#include <signal.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"
|
|
|
|
#ifdef HAVE_UNUSED
|
|
#defined ATTR_UNUSED __attribute__((unused))
|
|
#defined UNUSED(x) do { } while (0)
|
|
#else
|
|
#define ATTR_UNUSED
|
|
#define UNUSED(x) do { (void)(x); } while (0)
|
|
#endif
|
|
|
|
// Embedded resources
|
|
#include "shellinabox/beep.h"
|
|
#include "shellinabox/cgi_root.h"
|
|
#include "shellinabox/enabled.h"
|
|
#include "shellinabox/favicon.h"
|
|
#include "shellinabox/keyboard.h"
|
|
#include "shellinabox/keyboard-layout.h"
|
|
#include "shellinabox/print-styles.h"
|
|
#include "shellinabox/root_page.h"
|
|
#include "shellinabox/shell_in_a_box.h"
|
|
#include "shellinabox/styles.h"
|
|
#include "shellinabox/vt100.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 const char *pidfile;
|
|
static sigjmp_buf jmpenv;
|
|
static volatile int exiting;
|
|
|
|
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) {
|
|
struct Session *session = (struct Session *)arg;
|
|
debug("Session %s done", session->sessionKey);
|
|
if (session->cleanup) {
|
|
terminateChild(session);
|
|
}
|
|
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[MAX_RESPONSE];
|
|
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.");
|
|
session->cleanup = 1;
|
|
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) {
|
|
*events = 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 ATTR_UNUSED, URL *url) {
|
|
UNUSED(len);
|
|
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");
|
|
const char *rootURL = getFromHashMap(args, "rooturl");
|
|
|
|
// 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 (keys) {
|
|
bad_new_session:
|
|
abandonSession(session);
|
|
httpSendReply(http, 400, "Bad Request", NO_MSG);
|
|
return HTTP_DONE;
|
|
}
|
|
|
|
if (cgiServer && cgiSessions++) {
|
|
serverExitLoop(cgiServer, 1);
|
|
goto bad_new_session;
|
|
}
|
|
session->http = http;
|
|
if (launchChild(service->id, session,
|
|
rootURL && *rootURL ? rootURL : urlGetURL(url)) < 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,
|
|
session->pty, POLLIN);
|
|
}
|
|
}
|
|
return HTTP_DONE;
|
|
} else if (session->connection) {
|
|
// Re-enable input on the child's pty
|
|
serverConnectionSetEvents(session->server, session->connection,
|
|
session->pty, POLLIN);
|
|
serverSetTimeout(session->connection, AJAX_TIMEOUT);
|
|
}
|
|
|
|
return HTTP_SUSPEND;
|
|
}
|
|
|
|
static void serveStaticFile(HttpConnection *http, const char *contentType,
|
|
const char *start, const char *end) {
|
|
char *body = (char *)start;
|
|
char *bodyEnd = (char *)end;
|
|
|
|
// Unfortunately, there are still some browsers that are so buggy that they
|
|
// need special conditional code. In anything that has a "text" MIME type,
|
|
// we allow simple conditionals. Nested conditionals are not supported.
|
|
if (!memcmp(contentType, "text/", 5)) {
|
|
char *tag = NULL;
|
|
int condTrue = -1;
|
|
char *ifPtr = NULL;
|
|
char *elsePtr = NULL;
|
|
for (char *ptr = body; bodyEnd - ptr >= 6; ) {
|
|
char *eol = ptr;
|
|
eol = memchr(eol, '\n', bodyEnd - eol);
|
|
if (eol == NULL) {
|
|
eol = bodyEnd;
|
|
} else {
|
|
++eol;
|
|
}
|
|
if (!memcmp(ptr, "[if ", 4)) {
|
|
char *bracket = memchr(ptr + 4, ']', eol - ptr - 4);
|
|
if (bracket != NULL && bracket > ptr + 4) {
|
|
check(tag = malloc(bracket - ptr - 3));
|
|
memcpy(tag, ptr + 4, bracket - ptr - 4);
|
|
tag[bracket - ptr - 4] = '\000';
|
|
condTrue = 0;
|
|
const char *userAgent = getFromHashMap(httpGetHeaders(http),
|
|
"user-agent");
|
|
if (!userAgent) {
|
|
userAgent = "";
|
|
}
|
|
|
|
// Allow multiple comma separated conditions. Conditions are either
|
|
// substrings found in the user agent, or they are "DEFINES_..."
|
|
// tags at the top of user CSS files.
|
|
for (char *tagPtr = tag; *tagPtr; ) {
|
|
char *e = strchr(tagPtr, ',');
|
|
if (!e) {
|
|
e = strchr(tag, '\000');
|
|
} else {
|
|
*e++ = '\000';
|
|
}
|
|
condTrue = userCSSGetDefine(tagPtr) ||
|
|
strstr(userAgent, tagPtr) != NULL;
|
|
if (*e) {
|
|
e[-1] = ',';
|
|
}
|
|
if (condTrue) {
|
|
break;
|
|
}
|
|
tagPtr = e;
|
|
}
|
|
|
|
// If we find any conditionals, then we need to make a copy of
|
|
// the text document. We do this lazily, as presumably the majority
|
|
// of text documents won't have conditionals.
|
|
if (body == start) {
|
|
check(body = malloc(end - start));
|
|
memcpy(body, start, end - start);
|
|
bodyEnd += body - start;
|
|
ptr += body - start;
|
|
eol += body - start;
|
|
}
|
|
|
|
// Remember the beginning of the "[if ...]" statement
|
|
ifPtr = ptr;
|
|
}
|
|
} else if (ifPtr && !elsePtr && eol - ptr >= (ssize_t)strlen(tag) + 7 &&
|
|
!memcmp(ptr, "[else ", 6) &&
|
|
!memcmp(ptr + 6, tag, strlen(tag)) &&
|
|
ptr[6 + strlen(tag)] == ']') {
|
|
// Found an "[else ...]" statement. Remember where it started.
|
|
elsePtr = ptr;
|
|
} else if (ifPtr && eol - ptr >= (ssize_t)strlen(tag) + 8 &&
|
|
!memcmp(ptr, "[endif ", 7) &&
|
|
!memcmp(ptr + 7, tag, strlen(tag)) &&
|
|
ptr[7 + strlen(tag)] == ']') {
|
|
// Found the closing "[endif ...]" statement. Now we can remove those
|
|
// parts of the conditionals that do not apply to this user agent.
|
|
char *s, *e;
|
|
if (condTrue) {
|
|
s = strchr(ifPtr, '\n') + 1;
|
|
e = elsePtr ? elsePtr : ptr;
|
|
} else {
|
|
if (elsePtr) {
|
|
s = strchr(elsePtr, '\n') + 1;
|
|
e = ptr;
|
|
} else {
|
|
s = ifPtr;
|
|
e = ifPtr;
|
|
}
|
|
}
|
|
memmove(ifPtr, s, e - s);
|
|
memmove(ifPtr + (e - s), eol, bodyEnd - eol);
|
|
bodyEnd -= (s - ifPtr) + (eol - e);
|
|
eol = ifPtr + (e - s);
|
|
ifPtr = NULL;
|
|
elsePtr = NULL;
|
|
free(tag);
|
|
tag = NULL;
|
|
}
|
|
ptr = eol;
|
|
}
|
|
free(tag);
|
|
}
|
|
|
|
char *response = stringPrintf(NULL,
|
|
"HTTP/1.1 200 OK\r\n"
|
|
"Content-Type: %s\r\n"
|
|
"Content-Length: %ld\r\n"
|
|
"%s\r\n",
|
|
contentType, (long)(bodyEnd - body),
|
|
body == start ? "" :
|
|
"Cache-Control: no-cache\r\n");
|
|
int len = strlen(response);
|
|
if (strcmp(httpGetMethod(http), "HEAD")) {
|
|
check(response = realloc(response, len + (bodyEnd - body)));
|
|
memcpy(response + len, body, bodyEnd - body);
|
|
len += bodyEnd - body;
|
|
}
|
|
|
|
// If we expanded conditionals, we had to create a temporary copy. Delete
|
|
// it now.
|
|
if (body != start) {
|
|
free(body);
|
|
}
|
|
|
|
httpTransfer(http, response, len);
|
|
}
|
|
|
|
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, present the final path element
|
|
const char *pathInfo = urlGetPathInfo(url);
|
|
int pathInfoLength = 0;
|
|
pathInfo = strrchr (pathInfo, '/');
|
|
if (pathInfo) {
|
|
++pathInfo;
|
|
} else {
|
|
pathInfo = ""; // Cheap way to get an empty string
|
|
}
|
|
pathInfoLength = strlen (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);
|
|
}
|
|
UNUSED(rootPageSize);
|
|
char *html = stringPrintf(NULL, rootPageStart,
|
|
enableSSL ? "true" : "false");
|
|
serveStaticFile(http, "text/html", html, strrchr(html, '\000'));
|
|
free(html);
|
|
} else if (pathInfoLength == 8 && !memcmp(pathInfo, "beep.wav", 8)) {
|
|
// Serve the audio sample for the console bell.
|
|
serveStaticFile(http, "audio/x-wav", beepStart, beepStart + beepSize - 1);
|
|
} else if (pathInfoLength == 11 && !memcmp(pathInfo, "enabled.gif", 11)) {
|
|
// Serve the checkmark icon used in the context menu
|
|
serveStaticFile(http, "image/gif", enabledStart,
|
|
enabledStart + enabledSize - 1);
|
|
} else if (pathInfoLength == 11 && !memcmp(pathInfo, "favicon.ico", 11)) {
|
|
// Serve the favicon
|
|
serveStaticFile(http, "image/x-icon", faviconStart,
|
|
faviconStart + faviconSize - 1);
|
|
} else if (pathInfoLength == 13 && !memcmp(pathInfo, "keyboard.html", 13)) {
|
|
// Serve the keyboard layout
|
|
serveStaticFile(http, "text/html", keyboardLayoutStart,
|
|
keyboardLayoutStart + keyboardLayoutSize - 1);
|
|
} else if (pathInfoLength == 12 && !memcmp(pathInfo, "keyboard.png", 12)) {
|
|
// Serve the keyboard icon
|
|
serveStaticFile(http, "image/png", keyboardStart,
|
|
keyboardStart + keyboardSize - 1);
|
|
} 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.
|
|
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 +
|
|
vt100Size - 1 +
|
|
shellInABoxSize - 1;
|
|
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, vt100Size - 1) + vt100Size - 1,
|
|
shellInABoxStart, shellInABoxSize - 1);
|
|
} 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 == 16 && !memcmp(pathInfo, "print-styles.css",16)){
|
|
// Serve the style sheet.
|
|
serveStaticFile(http, "text/css; charset=utf-8",
|
|
printStylesStart, printStylesStart + printStylesSize - 1);
|
|
} 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"
|
|
" --pidfile=PIDFILE publish pid of daemon process\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 := "
|
|
#ifdef HAVE_BIN_LOGIN
|
|
"'LOGIN' | "
|
|
#endif
|
|
"'SSH' [ : <host> ] | "
|
|
"USER ':' CWD ':' CMD\n"
|
|
" USER := %s<username> ':' <groupname>\n"
|
|
" CWD := 'HOME' | <dir>\n"
|
|
" CMD := 'SHELL' | <cmdline>\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"
|
|
" ${url} - the URL that serves the terminal session\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 ATTR_UNUSED, char *key,
|
|
char *value) {
|
|
UNUSED(arg);
|
|
free(key);
|
|
free(value);
|
|
}
|
|
|
|
static void sigHandler(int signo, siginfo_t *info, void *context) {
|
|
if (exiting++) {
|
|
_exit(1);
|
|
}
|
|
siglongjmp(jmpenv, 1);
|
|
}
|
|
|
|
static void parseArgs(int argc, char * const argv[]) {
|
|
int hasSSL = serverSupportsSSL();
|
|
if (!hasSSL) {
|
|
enableSSL = 0;
|
|
}
|
|
int demonize = 0;
|
|
int cgi = 0;
|
|
int verbosity = MSG_DEFAULT;
|
|
externalFiles = newHashMap(destroyExternalFileHashEntry, NULL);
|
|
HashMap *serviceTable = newHashMap(destroyServiceHashEntry, NULL);
|
|
UNUSED(stylesSize);
|
|
check(cssStyleSheet = strdup(stylesStart));
|
|
|
|
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' },
|
|
{ "pidfile", 1, 0, 0 },
|
|
{ "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) {
|
|
check(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, st.st_size, 1, css) != 1) {
|
|
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 (pidfile) {
|
|
fatal("CGI operation and --pidfile= 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';
|
|
check(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 = parseGroupArg(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--) {
|
|
// Pidfile
|
|
if (cgi) {
|
|
fatal("CGI operation and --pidfile= are mutually exclusive");
|
|
}
|
|
if (!optarg || !*optarg) {
|
|
fatal("Must specify a filename for --pidfile= option");
|
|
}
|
|
if (pidfile) {
|
|
fatal("Only one pidfile can be given");
|
|
}
|
|
check(pidfile = strdup(optarg));
|
|
} 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 = parseUserArg(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 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(
|
|
#ifdef HAVE_BIN_LOGIN
|
|
geteuid() ? ":SSH" : ":LOGIN"
|
|
#else
|
|
":SSH"
|
|
#endif
|
|
));
|
|
}
|
|
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)));
|
|
} else {
|
|
free((char *)pidfile);
|
|
pidfile = NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void removeLimits() {
|
|
static int res[] = { RLIMIT_CPU, RLIMIT_DATA, RLIMIT_FSIZE, RLIMIT_NPROC };
|
|
for (unsigned 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));
|
|
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, getpid());
|
|
UNUSED(cgiRootSize);
|
|
printfUnchecked(cgiRootStart, port, cgiSessionKey);
|
|
fflush(stdout);
|
|
check(!NOINTR(close(fds[1])));
|
|
closeAllFds((int []){ launcherFd, serverGetFd(server) }, 2);
|
|
logSetLogLevel(MSG_QUIET);
|
|
}
|
|
|
|
// Set log file format
|
|
serverSetNumericHosts(server, numericHosts ||
|
|
logIsQuiet() || logIsDefault());
|
|
|
|
// 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
|
|
if (!sigsetjmp(jmpenv, 1)) {
|
|
// Clean up upon orderly shut down. Do _not_ cleanup if we die
|
|
// unexpectedly, as we cannot guarantee if we are still in a valid
|
|
// static. This means, we should never catch SIGABRT.
|
|
static const int signals[] = { SIGHUP, SIGINT, SIGQUIT, SIGTERM };
|
|
struct sigaction sa;
|
|
memset(&sa, 0, sizeof(sa));
|
|
sa.sa_sigaction = sigHandler;
|
|
sa.sa_flags = SA_SIGINFO | SA_RESETHAND;
|
|
for (int i = 0; i < sizeof(signals)/sizeof(*signals); ++i) {
|
|
sigaction(signals[i], &sa, NULL);
|
|
}
|
|
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);
|
|
if (pidfile) {
|
|
// As a convenience, remove the pidfile, if it is still the version that
|
|
// we wrote. In general, pidfiles are not expected to be incredibly
|
|
// reliable, as there is no way to properly deal with multiple programs
|
|
// accessing the same pidfile. But we at least make a best effort to be
|
|
// good citizens.
|
|
char buf[40];
|
|
int fd = open(pidfile, O_RDONLY);
|
|
if (fd >= 0) {
|
|
ssize_t sz;
|
|
NOINTR(sz = read(fd, buf, sizeof(buf)-1));
|
|
NOINTR(close(fd));
|
|
if (sz > 0) {
|
|
buf[sz] = '\000';
|
|
if (atoi(buf) == getpid()) {
|
|
unlink(pidfile);
|
|
}
|
|
}
|
|
}
|
|
free((char *)pidfile);
|
|
}
|
|
info("Done");
|
|
_exit(0);
|
|
}
|