// 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 <sys/socket.h>
#include <sys/un.h>
#include <time.h>
#include <unistd.h>

#ifdef HAVE_SYS_PRCTL_H
#include <sys/prctl.h>
#endif

#include "libhttp/http.h"
#include "libhttp/server.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            peerCheckEnabled  = 1;
static int            enableSSL         = 1;
static int            enableSSLMenu     = 1;
static int            forceSSL          = 1; // TODO enable http fallback with commandline option
int                   enableUtmpLogging = 1;
static char           *messagesOrigin   = NULL;
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("[server] Session %s done.", session->sessionKey);
  if (session->cleanup) {
    terminateChild(session);
  }
  session->done           = 1;
  addToGraveyard(session);
  completePendingRequest(session, "", 0, INT_MAX);
}

static void delaySession(void) {
  struct timespec ts;
  ts.tv_sec              = 0;
  ts.tv_nsec             = 200 * 1000; // Delay for 0.2 ms
  nanosleep(&ts, NULL);
}

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("[server] Timeout. Closing session %s!", session->sessionKey);
      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);
    session->ptyFirstRead       = 0;
    return 1;
  } else {
    if (revents & POLLHUP) {
      if (session->useLogin && session->ptyFirstRead) {
        // Workaround for random "Session closed" issues related to /bin/login
        // closing and reopening our pty during initialization. This happens only
        // on some systems like Fedora for example.
        // Here we allow that our pty is closed by ignoring POLLHUP on first read.
        // Delay is also needed so that login process has some time to reopen pty.
        // Note that the issue may occur anyway but with workaround we reduce the
        // chances.
        debug("[server] POLLHUP received on login PTY first read!");
        session->ptyFirstRead   = 0;
        delaySession();
        return 1;
      }
      debug("[server] POLLHUP received on PTY! Closing session %s!",
            session->sessionKey);
    }
    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("[server] 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.
    iterateOverSessions(invalidatePendingHttpSession, http);
    return HTTP_DONE;
  }

  // Find an existing session, or create the record for a new one
  const HashMap *args     = urlGetArgs(url);
  const char *sessionKey  = getFromHashMap(args, "session");

  int sessionIsNew;
  struct Session *session = findSession(sessionKey, cgiSessionKey, &sessionIsNew, http);
  if (session == NULL) {
    httpSendReply(http, 400, "Bad Request", NO_MSG);
    return HTTP_DONE;
  }

  // Sanity check
  if (!sessionIsNew && peerCheckEnabled && strcmp(session->peerName, httpGetPeerName(http))) {
    error("[server] Peername changed from %s to %s",
          session->peerName, httpGetPeerName(http));
    httpSendReply(http, 400, "Bad Request", NO_MSG);
    return HTTP_DONE;
  }

  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 (sessionIsNew) {
    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;
    session->useLogin     = service->useLogin;
    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("[server] 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 || sessionIsNew) {
    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) {
          if (tag != NULL) {
            free(tag);
          }
          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.
      int status          = dataHandler(http, arg, buf, len, url);
      deleteURL(url);
      return status;
    }
    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"
                                         "serverMessagesOrigin = %s%s%s;\n\n",
                                         enableSSL      ? "true" : "false",
                                         !enableSSLMenu ? "true" : "false",
                                         noBeep         ? "true" : "false",
                                         linkifyURLs,
                                         userCSSString,
                                         messagesOrigin ? "'" : "",
                                         messagesOrigin ? messagesOrigin : "false",
                                         messagesOrigin ? "'" : "");
    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("[config] Missing numeric value!");
  }
  long l = strtol(s, &ptr, 10);
  if (*ptr || l < minVal || l > maxVal) {
    fatal("[config] 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);

  printf("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|aggressive] 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"
          "  -m, --messages-origin=ORIGIN allow iframe message passing from origin\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"
          "      --disable-utmp-logging  disable logging to utmp and wtmp\n"
          "  -q, --quiet                 turn off all messages\n"
          "      --unixdomain-only=PATH:USER:GROUP:CHMOD listen on unix socket\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"
          "      --disable-peer-check    disable peer check on a session\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"
          "  ${realip}  - value of HTTP header field 'X-Real-IP'\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"
          "\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;
    forceSSL               = 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:nm:p: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' },
      { "messages-origin",      1, 0, 'm' },
      { "pidfile",              1, 0,  0  },
      { "port",                 1, 0, 'p' },
      { "service",              1, 0, 's' },
      { "disable-ssl",          0, 0, 't' },
      { "disable-ssl-menu",     0, 0,  0  },
      { "disable-utmp-logging", 0, 0,  0  },
      { "quiet",                0, 0, 'q' },
      { "unixdomain-only",      1, 0,  0, },
      { "user",                 1, 0, 'u' },
      { "user-css",             1, 0,  0  },
      { "verbose",              0, 0, 'v' },
      { "version",              0, 0,  0  },
      { "disable-peer-check",   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)
      if (idx < -1) {
        fatal("[server] Failed to parse command line!");
      } else {
        usage();
      }
      exit(0);
    } else if (!idx--) {
      // Background
      if (cgi) {
        fatal("[config] CGI and background operations are mutually exclusive!");
      }
      demonize            = 1;
      if (optarg && pidfile) {
        fatal("[config] Only one pidfile can be given!");
      }
      if (optarg && *optarg) {
        check(pidfile     = strdup(optarg));
      }
    } else if (!idx--) {
      // Certificate
      if (!hasSSL) {
        warn("[config] Ignoring certificate directory, as SSL support is unavailable.");
      }
      if (certificateFd >= 0) {
        fatal("[config] Cannot set both a certificate directory and file handle!");
      }
      if (certificateDir) {
        fatal("[config] Only one certificate directory can be selected!");
      }
      struct stat st;
      if (!optarg || !*optarg || stat(optarg, &st) || !S_ISDIR(st.st_mode)) {
        fatal("[config] Option --cert expects a directory name!");
      }
      check(certificateDir = strdup(optarg));
    } else if (!idx--) {
      // Certificate file descriptor
      if (!hasSSL) {
        warn("[config] Ignoring certificate directory, as SSL support is unavailable.");
      }
      if (certificateDir) {
        fatal("[config] Cannot set both a certificate directory and file handle!");
      }
      if (certificateFd >= 0) {
        fatal("[config] Only one certificate file handle can be provided!");
      }
      if (!optarg || *optarg < '0' || *optarg > '9') {
        fatal("[config] Option --cert-fd expects a valid file handle.");
      }
      int tmpFd            = strtoint(optarg, 3, INT_MAX);
      certificateFd        = dup(tmpFd);
      if (certificateFd < 0) {
        fatal("[config] 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("[config] Option --css expects a file name!");
      }
      FILE *css            = fopen(optarg, "r");
      if (!css) {
        fatal("[config] 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("[config] Failed to read style sheet \"%s\"!", optarg);
        }
        newData[st.st_size]= '\000';
        fclose(css);
      }
    } else if (!idx--) {
      // CGI
      if (demonize) {
        fatal("[config] CGI and background operations are mutually exclusive!");
      }
      if (pidfile) {
        fatal("[config] CGI operation and --pidfile are mutually exclusive!");
      }
      if (port) {
        fatal("[config] Cannot specify a port for CGI operation!");
      }
      cgi                  = 1;
      if (optarg && *optarg) {
        char *ptr          = strchr(optarg, '-');
        if (!ptr) {
          fatal("[config] 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("[config] Option --debug is mutually exclusive with --quiet and --verbose!");
      }
      verbosity            = MSG_DEBUG;
      logSetLogLevel(verbosity);
    } else if (!idx--) {
      // Static file
      if (!optarg || !*optarg) {
	    fatal("[config] Option --static-file expects an argument!");
      }
      char *ptr, *path, *file;
      if ((ptr             = strchr(optarg, ':')) == NULL) {
        fatal("[config] 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("[config] Duplicate static-file definition for \"%s\"!", path);
      }
      addToHashMap(externalFiles, path, file);
    } else if (!idx--) {
      // Group
      if (runAsGroup >= 0) {
        fatal("[config] Duplicate --group option.");
      }
      if (!optarg || !*optarg) {
        fatal("[config] Option --group expects a group name.");
      }
      runAsGroup           = parseGroupArg(optarg, NULL);
    } else if (!idx--) {
      // Linkify
      if (!optarg || !*optarg) {
        fatal("[config] Option --linkify expects an argument.");
      }
      if (!strcmp(optarg, "none")) {
        linkifyURLs        = 0;
      } else if (!strcmp(optarg, "normal")) {
        linkifyURLs        = 1;
      } else if (!strcmp(optarg, "aggressive")) {
        linkifyURLs        = 2;
      } else {
        fatal("[config] 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--) {
      // Messages origin
      if (messagesOrigin) {
        fatal("[config] Duplicated --messages-origin option.");
      }
      if (!optarg || !*optarg) {
        fatal("[config] Option --messages-origin expects an argument.");
      }
      check(messagesOrigin = strdup(optarg));
    } else if (!idx--) {
      // Pidfile
      if (cgi) {
        fatal("[config] CGI operation and --pidfile are mutually exclusive");
      }
      if (!optarg || !*optarg) {
        fatal("[config] Must specify a filename for --pidfile option");
      }
      if (pidfile) {
        fatal("[config] Only one pidfile can be given");
      }
      check(pidfile        = strdup(optarg));
    } else if (!idx--) {
      // Port
      if (port) {
        fatal("[config] Duplicate --port option!");
      }
      if (cgi) {
        fatal("[config] Cannot specifiy a port for CGI operation");
      }
      if (!optarg || *optarg < '0' || *optarg > '9') {
        fatal("[config] Option --port expects a port number.");
      }
      port = strtoint(optarg, 1, 65535);
    } else if (!idx--) {
      // Service
      if (!optarg || !*optarg) {
        fatal("[config] Option \"--service\" expects an argument.");
      }
      struct Service *service;
      service              = newService(optarg);
      if (getRefFromHashMap(serviceTable, service->path)) {
        fatal("[config] Duplicate service description for \"%s\".", service->path);
      }
      addToHashMap(serviceTable, service->path, (char *)service);
    } else if (!idx--) {
      // Disable SSL
      if (!hasSSL) {
        warn("[config] Ignoring disable-ssl option, as SSL support is unavailable.");
      }
      enableSSL            = 0;
      forceSSL             = 0;
    } else if (!idx--) {
      // Disable SSL Menu
      if (!hasSSL) {
        warn("[config] Ignoring disable-ssl-menu option, as SSL support is unavailable.");
      }
      enableSSLMenu        = 0;
    } else if (!idx--) {
      // Disable UTMP logging
      enableUtmpLogging    = 0;
    } else if (!idx--) {
      // Quiet
      if (!logIsDefault() && !logIsQuiet()) {
         fatal("[config] Option --quiet is mutually exclusive with --debug and --verbose!");
      }
      verbosity            = MSG_QUIET;
      logSetLogLevel(verbosity);
    } else if (!idx--) {
      // Unix domain only
      if (!optarg || !*optarg) {
        fatal("[config] Option --unixdomain-only expects an argument!");
      }
      char *ptr, *s, *tmp;

      // Unix domain path
      s                    = optarg;
      ptr                  = strchr(s, ':');
      if (ptr == NULL || ptr == s || ptr - s >= UNIX_PATH_MAX) {
        fatal("[config] Syntax error in unixdomain-only path definition \"%s\".",
		      optarg);
      }
      check(unixDomainPath = strndup(s, ptr - s));

      // Unix domain user
      s                    = ptr + 1;
      ptr                  = strchr(s, ':');
      if (ptr == NULL || ptr == s) {
        fatal("[config] Syntax error in unixdomain-only user definition \"%s\".",
              optarg);
      }
      check(tmp            = strndup(s, ptr - s));
      unixDomainUser       = parseUserArg(tmp, NULL);
      free(tmp);

      // Unix domain group
      s                    = ptr + 1;
      ptr                  = strchr(s, ':');
      if (ptr == NULL || ptr == s) {
        fatal("[config] Syntax error in unixdomain-only group definition \"%s\".",
		      optarg);
      }
      check(tmp            = strndup(s, ptr - s));
      unixDomainGroup      = parseGroupArg(tmp, NULL);
      free(tmp);

      // Unix domain chmod
      s                    = ptr + 1;
      if (strlen(ptr) == 1) {
        fatal("[config] Syntax error in unixdomain-only chmod definition \"%s\".",
              optarg);
      }
      unixDomainChmod      = strtol(s, NULL, 8);

    } else if (!idx--) {
      // User
      if (runAsUser >= 0) {
        fatal("[config] Duplicate --user option.");
      }
      if (!optarg || !*optarg) {
        fatal("[config] Option --user expects a user name.");
      }
      runAsUser            = parseUserArg(optarg, NULL);
    } else if (!idx--) {
      // User CSS
      if (!optarg || !*optarg) {
        fatal("[config] Option --user-css expects a list of styles sheets "
              "and labels!");
      }
      parseUserCSS(&userCSSList, optarg);
    } else if (!idx--) {
      // Verbose
      if (!logIsDefault() && (!logIsInfo() || logIsDebug())) {
         fatal("[config] Option --verbose is mutually exclusive with --debug and --quiet!");
      }
      verbosity            = MSG_INFO;
      logSetLogLevel(verbosity);
    } else if (!idx--) {
      // Version
      printf("ShellInABox version " VERSION VCS_REVISION "\n");
      exit(0);
    } else if (!idx--) {
      // disable-peer-check
      peerCheckEnabled = 0;
    }
  }
  if (optind != argc) {
    fatal("[config] Failed to parse command line!");
  }
  char *buf                = NULL;
  check(argc >= 1);

  info("[server] Version " VERSION VCS_REVISION);
  for (int i = 0; i < argc; i++) {
    buf                    = stringPrintf(buf, "%s ", argv[i]);
  }
  info("[server] 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("[config] 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) {

  serverSetupSSL(server, enableSSL, forceSSL);

  // Enable SSL support (if available)
  if (enableSSL) {
    check(serverSupportsSSL());
    if (certificateFd >= 0) {
      serverSetCertificateFd(server, certificateFd);
    } else if (certificateDir) {
      char *tmp;
      if (strchr(certificateDir, '%')) {
        fatal("[ssl] 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"
           "X-ShellInABox-Session: %s\r\n"
           "Content-type: text/html; charset=utf-8\r\n\r\n",
           port, getpid(), cgiSessionKey);
    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);
  free(messagesOrigin);
  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("[server] Done");
  _exit(0);
}
