]> andersk Git - test.git/blob - shellinabox/shellinaboxd.c
350e3136fc1486dc4b702f09cdb5e3b6fe8b2d0f
[test.git] / shellinabox / shellinaboxd.c
1 // shellinaboxd.c -- A custom web server that makes command line applications
2 //                   available as AJAX web applications.
3 // Copyright (C) 2008-2009 Markus Gutschke <markus@shellinabox.com>
4 //
5 // This program is free software; you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License version 2 as
7 // published by the Free Software Foundation.
8 //
9 // This program is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License along
15 // with this program; if not, write to the Free Software Foundation, Inc.,
16 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17 //
18 // In addition to these license terms, the author grants the following
19 // additional rights:
20 //
21 // If you modify this program, or any covered work, by linking or
22 // combining it with the OpenSSL project's OpenSSL library (or a
23 // modified version of that library), containing parts covered by the
24 // terms of the OpenSSL or SSLeay licenses, the author
25 // grants you additional permission to convey the resulting work.
26 // Corresponding Source for a non-source form of such a combination
27 // shall include the source code for the parts of OpenSSL used as well
28 // as that of the covered work.
29 //
30 // You may at your option choose to remove this additional permission from
31 // the work, or from any part of it.
32 //
33 // It is possible to build this program in a way that it loads OpenSSL
34 // libraries at run-time. If doing so, the following notices are required
35 // by the OpenSSL and SSLeay licenses:
36 //
37 // This product includes software developed by the OpenSSL Project
38 // for use in the OpenSSL Toolkit. (http://www.openssl.org/)
39 //
40 // This product includes cryptographic software written by Eric Young
41 // (eay@cryptsoft.com)
42 //
43 //
44 // The most up-to-date version of this program is always available from
45 // http://shellinabox.com
46
47 #define _GNU_SOURCE
48 #include "config.h"
49
50 #include <fcntl.h>
51 #include <getopt.h>
52 #include <limits.h>
53 #include <locale.h>
54 #include <poll.h>
55 #include <stdarg.h>
56 #include <stdio.h>
57 #include <stdlib.h>
58 #include <string.h>
59 #include <sys/resource.h>
60 #include <sys/types.h>
61 #include <sys/stat.h>
62 #include <unistd.h>
63
64 #ifdef HAVE_SYS_PRCTL_H
65 #include <sys/prctl.h>
66 #endif
67
68 #include "libhttp/http.h"
69 #include "logging/logging.h"
70 #include "shellinabox/externalfile.h"
71 #include "shellinabox/launcher.h"
72 #include "shellinabox/privileges.h"
73 #include "shellinabox/service.h"
74 #include "shellinabox/session.h"
75 #include "shellinabox/usercss.h"
76
77 #define PORTNUM           4200
78 #define MAX_RESPONSE      2048
79
80 static int            port;
81 static int            portMin;
82 static int            portMax;
83 static int            localhostOnly = 0;
84 static int            noBeep        = 0;
85 static int            numericHosts  = 0;
86 static int            enableSSL     = 1;
87 static int            enableSSLMenu = 1;
88 static int            linkifyURLs   = 1;
89 static char           *certificateDir;
90 static int            certificateFd = -1;
91 static HashMap        *externalFiles;
92 static Server         *cgiServer;
93 static char           *cgiSessionKey;
94 static int            cgiSessions;
95 static char           *cssStyleSheet;
96 static struct UserCSS *userCSSList;
97
98 static char *jsonEscape(const char *buf, int len) {
99   static const char *hexDigit = "0123456789ABCDEF";
100
101   // Determine the space that is needed to encode the buffer
102   int count                   = 0;
103   const char *ptr             = buf;
104   for (int i = 0; i < len; i++) {
105     unsigned char ch          = *(unsigned char *)ptr++;
106     if (ch < ' ') {
107       switch (ch) {
108       case '\b': case '\f': case '\n': case '\r': case '\t':
109         count                += 2;
110         break;
111       default:
112         count                += 6;
113         break;
114       }
115     } else if (ch == '"' || ch == '\\' || ch == '/') {
116       count                  += 2;
117     } else if (ch > '\x7F') {
118       count                  += 6;
119     } else {
120       count++;
121     }
122   }
123
124   // Encode the buffer using JSON string escaping
125   char *result;
126   check(result                = malloc(count + 1));
127   char *dst                   = result;
128   ptr                         = buf;
129   for (int i = 0; i < len; i++) {
130     unsigned char ch          = *(unsigned char *)ptr++;
131     if (ch < ' ') {
132       *dst++                  = '\\';
133       switch (ch) {
134       case '\b': *dst++       = 'b'; break;
135       case '\f': *dst++       = 'f'; break;
136       case '\n': *dst++       = 'n'; break;
137       case '\r': *dst++       = 'r'; break;
138       case '\t': *dst++       = 't'; break;
139       default:
140       unicode:
141         *dst++                = 'u';
142         *dst++                = '0';
143         *dst++                = '0';
144         *dst++                = hexDigit[ch >> 4];
145         *dst++                = hexDigit[ch & 0xF];
146         break;
147       }
148     } else if (ch == '"' || ch == '\\' || ch == '/') {
149       *dst++                  = '\\';
150       *dst++                  = ch;
151     } else if (ch > '\x7F') {
152       *dst++                  = '\\';
153       goto unicode;
154     } else {
155       *dst++                  = ch;
156     }
157   }
158   *dst++                      = '\000';
159   return result;
160 }
161
162 static int printfUnchecked(const char *format, ...) {
163   // Some Linux distributions enable -Wformat=2 by default. This is a
164   // very unfortunate decision, as that option generates a lot of false
165   // positives. We try to work around the problem by defining an unchecked
166   // version of "printf()"
167   va_list ap;
168   va_start(ap, format);
169   int rc = vprintf(format, ap);
170   va_end(ap);
171   return rc;
172 }
173
174 static int completePendingRequest(struct Session *session,
175                                   const char *buf, int len, int maxLength) {
176   // If there is no pending HTTP request, save the data and return
177   // immediately.
178   if (!session->http) {
179     if (len) {
180       if (session->buffered) {
181         check(session->buffered = realloc(session->buffered,
182                                           session->len + len));
183         memcpy(session->buffered + session->len, buf, len);
184         session->len           += len;
185       } else {
186         check(session->buffered = malloc(len));
187         memcpy(session->buffered, buf, len);
188         session->len            = len;
189       }
190     }
191   } else {
192     // If we have a pending HTTP request, we can reply to it, now.
193     char *data;
194     if (session->buffered) {
195       check(session->buffered   = realloc(session->buffered,
196                                           session->len + len));
197       memcpy(session->buffered + session->len, buf, len);
198       session->len             += len;
199       if (maxLength > 0 && session->len > maxLength) {
200         data                    = jsonEscape(session->buffered, maxLength);
201         session->len           -= maxLength;
202         memmove(session->buffered, session->buffered + maxLength,
203                 session->len);
204       } else {
205         data                    = jsonEscape(session->buffered, session->len);
206         free(session->buffered);
207         session->buffered       = NULL;
208         session->len            = 0;
209       }
210     } else {
211       if (maxLength > 0 && len > maxLength) {
212         session->len            = len - maxLength;
213         check(session->buffered = malloc(session->len));
214         memcpy(session->buffered, buf + maxLength, session->len);
215         data                    = jsonEscape(buf, maxLength);
216       } else {
217         data                    = jsonEscape(buf, len);
218       }
219     }
220     
221     char *json                  = stringPrintf(NULL, "{"
222                                                "\"session\":\"%s\","
223                                                "\"data\":\"%s\""
224                                                "}",
225                                                session->sessionKey, data);
226     free(data);
227     HttpConnection *http        = session->http;
228     char *response              = stringPrintf(NULL,
229                                              "HTTP/1.1 200 OK\r\n"
230                                              "Content-Type: application/json; "
231                                              "charset=utf-8\r\n"
232                                              "Content-Length: %ld\r\n"
233                                              "Cache-Control: no-cache\r\n"
234                                              "\r\n"
235                                              "%s",
236                                              (long)strlen(json),
237                                              strcmp(httpGetMethod(http),
238                                                     "HEAD") ? json : "");
239     free(json);
240     session->http               = NULL;
241     httpTransfer(http, response, strlen(response));
242   }
243   if (session->done && !session->buffered) {
244     finishSession(session);
245     return 0;
246   }
247   return 1;
248 }
249
250 static void sessionDone(void *arg) {
251   debug("Child terminated");
252   struct Session *session = (struct Session *)arg;
253   session->done           = 1;
254   addToGraveyard(session);
255   completePendingRequest(session, "", 0, INT_MAX);
256 }
257
258 static int handleSession(struct ServerConnection *connection, void *arg,
259                          short *events, short revents) {
260   struct Session *session       = (struct Session *)arg;
261   session->connection           = connection;
262   int len                       = MAX_RESPONSE - session->len;
263   if (len <= 0) {
264     len                         = 1;
265   }
266   char buf[len];
267   int bytes                     = 0;
268   if (revents & POLLIN) {
269     bytes                       = NOINTR(read(session->pty, buf, len));
270     if (bytes <= 0) {
271       return 0;
272     }
273   }
274   int timedOut                  = serverGetTimeout(connection) < 0;
275   if (bytes || timedOut) {
276     if (!session->http && timedOut) {
277       debug("Timeout. Closing session.");
278       return 0;
279     }
280     check(!session->done);
281     check(completePendingRequest(session, buf, bytes, MAX_RESPONSE));
282     connection                  = serverGetConnection(session->server,
283                                                       connection,
284                                                       session->pty);
285     session->connection         = connection;
286     if (session->len >= MAX_RESPONSE) {
287       serverConnectionSetEvents(session->server, connection, 0);
288     }
289     serverSetTimeout(connection, AJAX_TIMEOUT);
290     return 1;
291   } else {
292     return 0;
293   }
294 }
295
296 static int invalidatePendingHttpSession(void *arg, const char *key,
297                                         char **value) {
298   struct Session *session = *(struct Session **)value;
299   if (session->http && session->http == (HttpConnection *)arg) {
300     debug("Clearing pending HTTP connection for session %s", key);
301     session->http         = NULL;
302     serverDeleteConnection(session->server, session->pty);
303
304     // Return zero in order to remove this HTTP from the "session" hashmap
305     return 0;
306   }
307
308   // If the session is still in use, do not remove it from the "sessions" map
309   return 1;
310 }
311
312 static int dataHandler(HttpConnection *http, struct Service *service,
313                        const char *buf, int len, URL *url) {
314   if (!buf) {
315     // Somebody unexpectedly closed our http connection (e.g. because of a
316     // timeout). This is the last notification that we will get.
317     deleteURL(url);
318     iterateOverSessions(invalidatePendingHttpSession, http);
319     return HTTP_DONE;
320   }
321
322   // Find an existing session, or create the record for a new one
323   int isNew;
324   struct Session *session = findCGISession(&isNew, http, url, cgiSessionKey);
325   if (session == NULL) {
326     httpSendReply(http, 400, "Bad Request", NO_MSG);
327     return HTTP_DONE;
328   }
329
330   // Sanity check
331   if (!isNew && strcmp(session->peerName, httpGetPeerName(http))) {
332     error("Peername changed from %s to %s",
333           session->peerName, httpGetPeerName(http));
334     httpSendReply(http, 400, "Bad Request", NO_MSG);
335     return HTTP_DONE;
336   }
337
338   const HashMap *args     = urlGetArgs(session->url);
339   int oldWidth            = session->width;
340   int oldHeight           = session->height;
341   const char *width       = getFromHashMap(args, "width");
342   const char *height      = getFromHashMap(args, "height");
343   const char *keys        = getFromHashMap(args, "keys");
344
345   // Adjust window dimensions if provided by client
346   if (width && height) {
347     session->width        = atoi(width);
348     session->height       = atoi(height);
349   }
350
351   // Create a new session, if the client did not provide an existing one
352   if (isNew) {
353     if (keys) {
354     bad_new_session:
355       abandonSession(session);
356       httpSendReply(http, 400, "Bad Request", NO_MSG);
357       return HTTP_DONE;
358     }
359
360     if (cgiServer && cgiSessions++) {
361       serverExitLoop(cgiServer, 1);
362       goto bad_new_session;
363     }
364     session->http         = http;
365     if (launchChild(service->id, session) < 0) {
366       abandonSession(session);
367       httpSendReply(http, 500, "Internal Error", NO_MSG);
368       return HTTP_DONE;
369     }
370     if (cgiServer) {
371       terminateLauncher();
372     }
373     session->connection   = serverAddConnection(httpGetServer(http),
374                                                 session->pty, handleSession,
375                                                 sessionDone, session);
376     serverSetTimeout(session->connection, AJAX_TIMEOUT);
377   }
378
379   // Reset window dimensions of the pseudo TTY, if changed since last time set.
380   if (session->width > 0 && session->height > 0 &&
381       (session->width != oldWidth || session->height != oldHeight)) {
382     debug("Window size changed to %dx%d", session->width, session->height);
383     setWindowSize(session->pty, session->width, session->height);
384   }
385
386   // Process keypresses, if any. Then send a synchronous reply.
387   if (keys) {
388     char *keyCodes;
389     check(keyCodes        = malloc(strlen(keys)/2));
390     int len               = 0;
391     for (const unsigned char *ptr = (const unsigned char *)keys; ;) {
392       unsigned c0         = *ptr++;
393       if (c0 < '0' || (c0 > '9' && c0 < 'A') ||
394           (c0 > 'F' && c0 < 'a') || c0 > 'f') {
395         break;
396       }
397       unsigned c1         = *ptr++;
398       if (c1 < '0' || (c1 > '9' && c1 < 'A') ||
399           (c1 > 'F' && c1 < 'a') || c1 > 'f') {
400         break;
401       }
402       keyCodes[len++]     = 16*((c0 & 0xF) + 9*(c0 > '9')) +
403                                 (c1 & 0xF) + 9*(c1 > '9');
404     }
405     if (write(session->pty, keyCodes, len) < 0 && errno == EAGAIN) {
406       completePendingRequest(session, "\007", 1, MAX_RESPONSE);
407     }
408     free(keyCodes);
409     httpSendReply(http, 200, "OK", " ");
410     check(session->http != http);
411     return HTTP_DONE;
412   } else {
413     // This request is polling for data. Finish any pending requests and
414     // queue (or process) a new one.
415     if (session->http && session->http != http &&
416         !completePendingRequest(session, "", 0, MAX_RESPONSE)) {
417       httpSendReply(http, 400, "Bad Request", NO_MSG);
418       return HTTP_DONE;
419     }
420     session->http         = http;
421   }
422
423   session->connection     = serverGetConnection(session->server,
424                                                 session->connection,
425                                                 session->pty);
426   if (session->buffered || isNew) {
427     if (completePendingRequest(session, "", 0, MAX_RESPONSE) &&
428         session->connection) {
429       // Reset the timeout, as we just received a new request.
430       serverSetTimeout(session->connection, AJAX_TIMEOUT);
431       if (session->len < MAX_RESPONSE) {
432         // Re-enable input on the child's pty
433         serverConnectionSetEvents(session->server, session->connection,POLLIN);
434       }
435     }
436     return HTTP_DONE;
437   } else if (session->connection) {
438     // Re-enable input on the child's pty
439     serverConnectionSetEvents(session->server, session->connection, POLLIN);
440     serverSetTimeout(session->connection, AJAX_TIMEOUT);
441   }
442
443   return HTTP_SUSPEND;
444 }
445
446 static void serveStaticFile(HttpConnection *http, const char *contentType,
447                             const char *start, const char *end) {
448   char *body                     = (char *)start;
449   char *bodyEnd                  = (char *)end;
450
451   // Unfortunately, there are still some browsers that are so buggy that they
452   // need special conditional code. In anything that has a "text" MIME type,
453   // we allow simple conditionals. Nested conditionals are not supported.
454   if (!memcmp(contentType, "text/", 5)) {
455     char *tag                    = NULL;
456     int condTrue                 = -1;
457     char *ifPtr                  = NULL;
458     char *elsePtr                = NULL;
459     for (char *ptr = body; bodyEnd - ptr >= 6; ) {
460       char *eol                  = ptr;
461       eol                        = memchr(eol, '\n', bodyEnd - eol);
462       if (eol == NULL) {
463         eol                      = bodyEnd;
464       } else {
465         ++eol;
466       }
467       if (!memcmp(ptr, "[if ", 4)) {
468         char *bracket            = memchr(ptr + 4, ']', eol - ptr - 4);
469         if (bracket != NULL && bracket > ptr + 4) {
470           check(tag              = malloc(bracket - ptr - 3));
471           memcpy(tag, ptr + 4, bracket - ptr - 4);
472           tag[bracket - ptr - 4] = '\000';
473           condTrue               = 0;
474           const char *userAgent  = getFromHashMap(httpGetHeaders(http),
475                                                   "user-agent");
476           if (userAgent) {
477             // Allow multiple comma separated conditions
478             for (char *tagPtr = tag; *tagPtr; ) {
479               char *e            = strchr(tagPtr, ',');
480               if (!e) {
481                 e                = strchr(tag, '\000');
482               } else {
483                 *e++             = '\000';
484               }
485               condTrue           = userCSSGetDefine(tagPtr) ||
486                                    strstr(userAgent, tagPtr) != NULL;
487               if (*e) {
488                 e[-1]            = ',';
489               }
490               if (condTrue) {
491                 break;
492               }
493               tagPtr             = e;
494             }
495           }
496
497           // If we find any conditionals, then we need to make a copy of
498           // the text document. We do this lazily, as presumably the majority
499           // of text documents won't have conditionals.
500           if (body == start) {
501             check(body           = malloc(end - start));
502             memcpy(body, start, end - start);
503             bodyEnd             += body - start;
504             ptr                 += body - start;
505             eol                 += body - start;
506           }
507
508           // Remember the beginning of the "[if ...]" statement
509           ifPtr                  = ptr;
510         }
511       } else if (ifPtr && !elsePtr && eol - ptr >= strlen(tag) + 7 &&
512                  !memcmp(ptr, "[else ", 6) &&
513                  !memcmp(ptr + 6, tag, strlen(tag)) &&
514                  ptr[6 + strlen(tag)] == ']') {
515         // Found an "[else ...]" statement. Remember where it started.
516         elsePtr                  = ptr;
517       } else if (ifPtr && eol - ptr >= strlen(tag) + 8 &&
518                  !memcmp(ptr, "[endif ", 7) &&
519                  !memcmp(ptr + 7, tag, strlen(tag)) &&
520                  ptr[7 + strlen(tag)] == ']') {
521         // Found the closing "[endif ...]" statement. Now we can remove those
522         // parts of the conditionals that do not apply to this user agent.
523         char *s, *e;
524         if (condTrue) {
525           s                      = strchr(ifPtr, '\n') + 1;
526           e                      = elsePtr ? elsePtr : ptr;
527         } else {
528           if (elsePtr) {
529             s                    = strchr(elsePtr, '\n') + 1;
530             e                    = ptr;
531           } else {
532             s                    = ifPtr;
533             e                    = ifPtr;
534           }
535         }
536         memmove(ifPtr, s, e - s);
537         memmove(ifPtr + (e - s), eol, bodyEnd - eol);
538         bodyEnd                 -= (s - ifPtr) + (eol - e);
539         eol                      = ifPtr + (e - s);
540         ifPtr                    = NULL;
541         elsePtr                  = NULL;
542         free(tag);
543         tag                      = NULL;
544       }
545       ptr                        = eol;
546     }
547     free(tag);
548   }
549
550   char *response   = stringPrintf(NULL,
551                                   "HTTP/1.1 200 OK\r\n"
552                                   "Content-Type: %s\r\n"
553                                   "Content-Length: %ld\r\n"
554                                   "%s\r\n",
555                                   contentType, (long)(bodyEnd - body),
556                                   body == start ? "" :
557                                   "Cache-Control: no-cache\r\n");
558   int len          = strlen(response);
559   if (strcmp(httpGetMethod(http), "HEAD")) {
560     check(response = realloc(response, len + (bodyEnd - body)));
561     memcpy(response + len, body, bodyEnd - body);
562     len           += bodyEnd - body;
563   }
564
565   // If we expanded conditionals, we had to create a temporary copy. Delete
566   // it now.
567   if (body != start) {
568     free(body);
569   }
570
571   httpTransfer(http, response, len);
572 }
573
574 static const char *addr(const char *a) {
575   // Work-around for a gcc bug that could occasionally generate invalid
576   // assembly instructions when optimizing code too agressively.
577   asm volatile("");
578   return a;
579 }
580
581 static int shellInABoxHttpHandler(HttpConnection *http, void *arg,
582                                   const char *buf, int len) {
583   checkGraveyard();
584   URL *url                = newURL(http, buf, len);
585   const HashMap *headers  = httpGetHeaders(http);
586   const char *contentType = getFromHashMap(headers, "content-type");
587
588   // Normalize the path info
589   const char *pathInfo    = urlGetPathInfo(url);
590   while (*pathInfo == '/') {
591     pathInfo++;
592   }
593   const char *endPathInfo;
594   for (endPathInfo        = pathInfo;
595        *endPathInfo && *endPathInfo != '/';
596        endPathInfo++) {
597   }
598   int pathInfoLength      = endPathInfo - pathInfo;
599
600   if (!pathInfoLength ||
601       (pathInfoLength == 5 && !memcmp(pathInfo, "plain", 5)) ||
602       (pathInfoLength == 6 && !memcmp(pathInfo, "secure", 6))) {
603     // The root page serves the AJAX application.
604     if (contentType &&
605         !strncasecmp(contentType, "application/x-www-form-urlencoded", 33)) {
606       // XMLHttpRequest carrying data between the AJAX application and the
607       // client session.
608       return dataHandler(http, arg, buf, len, url);
609     }
610     extern char rootPageStart[];
611     extern char rootPageEnd[];
612     char *rootPage;
613     check(rootPage = malloc(rootPageEnd - rootPageStart + 1));
614     memcpy(rootPage, rootPageStart, rootPageEnd - rootPageStart);
615     rootPage[rootPageEnd - rootPageStart] = '\000';
616     char *html            = stringPrintf(NULL, rootPage,
617                                          enableSSL ? "true" : "false");
618     serveStaticFile(http, "text/html", html, strrchr(html, '\000'));
619     free(html);
620     free(rootPage);
621   } else if (pathInfoLength == 8 && !memcmp(pathInfo, "beep.wav", 8)) {
622     // Serve the audio sample for the console bell.
623     extern char beepStart[];
624     extern char beepEnd[];
625     serveStaticFile(http, "audio/x-wav", beepStart, beepEnd);
626   } else if (pathInfoLength == 11 && !memcmp(pathInfo, "enabled.gif", 11)) {
627     // Serve the checkmark icon used in the context menu
628     extern char enabledStart[];
629     extern char enabledEnd[];
630     serveStaticFile(http, "image/gif", enabledStart, enabledEnd);
631   } else if (pathInfoLength == 11 && !memcmp(pathInfo, "favicon.ico", 11)) {
632     // Serve the favicon
633     extern char faviconStart[];
634     extern char faviconEnd[];
635     serveStaticFile(http, "image/x-icon", faviconStart, faviconEnd);
636   } else if (pathInfoLength == 14 && !memcmp(pathInfo, "ShellInABox.js", 14)) {
637     // Serve both vt100.js and shell_in_a_box.js in the same transaction.
638     // Also, indicate to the client whether the server is SSL enabled.
639     extern char vt100Start[];
640     extern char vt100End[];
641     extern char shellInABoxStart[];
642     extern char shellInABoxEnd[];
643     char *userCSSString   = getUserCSSString(userCSSList);
644     char *stateVars       = stringPrintf(NULL,
645                                          "serverSupportsSSL = %s;\n"
646                                          "disableSSLMenu    = %s;\n"
647                                          "suppressAllAudio  = %s;\n"
648                                          "linkifyURLs       = %d;\n"
649                                          "userCSSList       = %s;\n\n",
650                                          enableSSL      ? "true" : "false",
651                                          !enableSSLMenu ? "true" : "false",
652                                          noBeep         ? "true" : "false",
653                                          linkifyURLs, userCSSString);
654     free(userCSSString);
655     int stateVarsLength   = strlen(stateVars);
656     int contentLength     = stateVarsLength +
657                             (addr(vt100End) - addr(vt100Start)) +
658                             (addr(shellInABoxEnd) - addr(shellInABoxStart));
659     char *response        = stringPrintf(NULL,
660                              "HTTP/1.1 200 OK\r\n"
661                              "Content-Type: text/javascript; charset=utf-8\r\n"
662                              "Content-Length: %d\r\n"
663                              "\r\n",
664                              contentLength);
665     int headerLength      = strlen(response);
666     if (strcmp(httpGetMethod(http), "HEAD")) {
667       check(response      = realloc(response, headerLength + contentLength));
668       memcpy(memcpy(memcpy(
669           response + headerLength, stateVars, stateVarsLength)+stateVarsLength,
670         vt100Start, vt100End - vt100Start) + (vt100End - vt100Start),
671       shellInABoxStart, shellInABoxEnd - shellInABoxStart);
672     } else {
673       contentLength       = 0;
674     }
675     free(stateVars);
676     httpTransfer(http, response, headerLength + contentLength);
677   } else if (pathInfoLength == 10 && !memcmp(pathInfo, "styles.css", 10)) {
678     // Serve the style sheet.
679     serveStaticFile(http, "text/css; charset=utf-8",
680                     cssStyleSheet, strrchr(cssStyleSheet, '\000'));
681   } else if (pathInfoLength > 8 && !memcmp(pathInfo, "usercss-", 8)) {
682     // Server user style sheets (if any)
683     struct UserCSS *css   = userCSSList;
684     for (int idx          = atoi(pathInfo + 8);
685          idx-- > 0 && css; css = css->next ) {
686     }
687     if (css) {
688       serveStaticFile(http, "text/css; charset=utf-8",
689                       css->style, css->style + css->styleLen);
690     } else {
691       httpSendReply(http, 404, "File not found", NO_MSG);
692     }
693   } else {
694     httpSendReply(http, 404, "File not found", NO_MSG);
695   }
696
697   deleteURL(url);
698   return HTTP_DONE;
699 }
700
701 static int strtoint(const char *s, int minVal, int maxVal) {
702   char *ptr;
703   if (!*s) {
704     fatal("Missing numeric value.");
705   }
706   long l = strtol(s, &ptr, 10);
707   if (*ptr || l < minVal || l > maxVal) {
708     fatal("Range error on numeric value \"%s\".", s);
709   }
710   return l;
711 }
712
713 static void usage(void) {
714   // Drop privileges so that we can tell which uid/gid we would normally
715   // run at.
716   dropPrivileges();
717   uid_t r_uid, e_uid, s_uid;
718   uid_t r_gid, e_gid, s_gid;
719   check(!getresuid(&r_uid, &e_uid, &s_uid));
720   check(!getresgid(&r_gid, &e_gid, &s_gid));
721   const char *user  = getUserName(r_uid);
722   const char *group = getGroupName(r_gid);
723
724   message("Usage: shellinaboxd [OPTIONS]...\n"
725           "Starts an HTTP server that serves terminal emulators to AJAX "
726           "enabled browsers.\n"
727           "\n"
728           "List of command line options:\n"
729           "  -b, --background[=PIDFILE]  run in background\n"
730           "%s"
731           "      --css=FILE              attach contents to CSS style sheet\n"
732           "      --cgi[=PORTMIN-PORTMAX] run as CGI\n"
733           "  -d, --debug                 enable debug mode\n"
734           "  -f, --static-file=URL:FILE  serve static file from URL path\n"
735           "  -g, --group=GID             switch to this group (default: %s)\n"
736           "  -h, --help                  print this message\n"
737           "      --linkify=[none|normal|agressive] default is \"normal\"\n"
738           "      --localhost-only        only listen on 127.0.0.1\n"
739           "      --no-beep               suppress all audio output\n"
740           "  -n, --numeric               do not resolve hostnames\n"
741           "  -p, --port=PORT             select a port (default: %d)\n"
742           "  -s, --service=SERVICE       define one or more services\n"
743           "%s"
744           "  -q, --quiet                 turn off all messages\n"
745           "  -u, --user=UID              switch to this user (default: %s)\n"
746           "      --user-css=STYLES       defines user-selectable CSS options\n"
747           "  -v, --verbose               enable logging messages\n"
748           "      --version               prints version information\n"
749           "\n"
750           "Debug, quiet, and verbose are mutually exclusive.\n"
751           "\n"
752           "One or more --service arguments define services that should "
753           "be made available\n"
754           "through the web interface:\n"
755           "  SERVICE := <url-path> ':' APP\n"
756           "  APP     := 'LOGIN' | 'SSH' [ : <host> ] | "
757                         "USER ':' CWD ':' <cmdline>\n"
758           "  USER    := %s<username> ':' <groupname>\n"
759           "  CWD     := 'HOME' | <dir>\n"
760           "\n"
761           "<cmdline> supports variable expansion:\n"
762           "  ${columns} - number of columns\n"
763           "  ${gid}     - gid id\n"
764           "  ${group}   - group name\n"
765           "  ${home}    - home directory\n"
766           "  ${lines}   - number of rows\n"
767           "  ${peer}    - name of remote peer\n"
768           "  ${uid}     - user id\n"
769           "  ${user}    - user name\n"
770           "\n"
771           "One or more --user-css arguments define optional user-selectable "
772           "CSS options.\n"
773           "These options show up in the right-click context menu:\n"
774           "  STYLES  := GROUP { ';' GROUP }*\n"
775           "  GROUP   := OPTION { ',' OPTION }*\n"
776           "  OPTION  := <label> ':' [ '-' | '+' ] <css-file>\n"
777           "\n"
778           "OPTIONs that make up a GROUP are mutually exclusive. But "
779           "individual GROUPs are\n"
780           "independent of each other.\n",
781           !serverSupportsSSL() ? "" :
782           "  -c, --cert=CERTDIR          set certificate dir "
783           "(default: $PWD)\n"
784           "      --cert-fd=FD            set certificate file from fd\n",
785           group, PORTNUM,
786           !serverSupportsSSL() ? "" :
787           "  -t, --disable-ssl           disable transparent SSL support\n"
788           "      --disable-ssl-menu      disallow changing transport mode\n",
789           user, supportsPAM() ? "'AUTH' | " : "");
790   free((char *)user);
791   free((char *)group);
792 }
793
794 static void destroyExternalFileHashEntry(void *arg, char *key, char *value) {
795   free(key);
796   free(value);
797 }
798
799 static void parseArgs(int argc, char * const argv[]) {
800   int hasSSL               = serverSupportsSSL();
801   if (!hasSSL) {
802     enableSSL              = 0;
803   }
804   int demonize             = 0;
805   int cgi                  = 0;
806   const char *pidfile      = NULL;
807   int verbosity            = MSG_DEFAULT;
808   externalFiles            = newHashMap(destroyExternalFileHashEntry, NULL);
809   HashMap *serviceTable    = newHashMap(destroyServiceHashEntry, NULL);
810   extern char stylesStart[];
811   extern char stylesEnd[];
812   check(cssStyleSheet      = malloc(stylesEnd - stylesStart));
813   memcpy(cssStyleSheet, stylesStart, stylesEnd - stylesStart);
814   cssStyleSheet[stylesEnd - stylesStart] = '\000';
815
816   for (;;) {
817     static const char optstring[] = "+hb::c:df:g:np:s:tqu:v";
818     static struct option options[] = {
819       { "help",             0, 0, 'h' },
820       { "background",       2, 0, 'b' },
821       { "cert",             1, 0, 'c' },
822       { "cert-fd",          1, 0,  0  },
823       { "css",              1, 0,  0  },
824       { "cgi",              2, 0,  0  },
825       { "debug",            0, 0, 'd' },
826       { "static-file",      1, 0, 'f' },
827       { "group",            1, 0, 'g' },
828       { "linkify",          1, 0,  0  },
829       { "localhost-only",   0, 0,  0  },
830       { "no-beep",          0, 0,  0  },
831       { "numeric",          0, 0, 'n' },
832       { "port",             1, 0, 'p' },
833       { "service",          1, 0, 's' },
834       { "disable-ssl",      0, 0, 't' },
835       { "disable-ssl-menu", 0, 0,  0  },
836       { "quiet",            0, 0, 'q' },
837       { "user",             1, 0, 'u' },
838       { "user-css",         1, 0,  0  },
839       { "verbose",          0, 0, 'v' },
840       { "version",          0, 0,  0  },
841       { 0,                  0, 0,  0  } };
842     int idx                = -1;
843     int c                  = getopt_long(argc, argv, optstring, options, &idx);
844     if (c > 0) {
845       for (int i = 0; options[i].name; i++) {
846         if (options[i].val == c) {
847           idx              = i;
848           break;
849         }
850       }
851     } else if (c < 0) {
852       break;
853     }
854     if (idx-- <= 0) {
855       // Help (or invalid argument)
856       usage();
857       if (idx < -1) {
858         fatal("Failed to parse command line");
859       }
860       exit(0);
861     } else if (!idx--) {
862       // Background
863       if (cgi) {
864         fatal("CGI and background operations are mutually exclusive");
865       }
866       demonize            = 1;
867       if (optarg && pidfile) {
868         fatal("Only one pidfile can be given");
869       }
870       if (optarg && *optarg) {
871         pidfile            = strdup(optarg);
872       }
873     } else if (!idx--) {
874       // Certificate
875       if (!hasSSL) {
876         warn("Ignoring certificate directory, as SSL support is unavailable");
877       }
878       if (certificateFd >= 0) {
879         fatal("Cannot set both a certificate directory and file handle");
880       }
881       if (certificateDir) {
882         fatal("Only one certificate directory can be selected");
883       }
884       struct stat st;
885       if (!optarg || !*optarg || stat(optarg, &st) || !S_ISDIR(st.st_mode)) {
886         fatal("\"--cert\" expects a directory name");
887       }
888       check(certificateDir = strdup(optarg));
889     } else if (!idx--) {
890       // Certificate file descriptor
891       if (!hasSSL) {
892         warn("Ignoring certificate directory, as SSL support is unavailable");
893       }
894       if (certificateDir) {
895         fatal("Cannot set both a certificate directory and file handle");
896       }
897       if (certificateFd >= 0) {
898         fatal("Only one certificate file handle can be provided");
899       }
900       if (!optarg || *optarg < '0' || *optarg > '9') {
901         fatal("\"--cert-fd\" expects a valid file handle");
902       }
903       int tmpFd            = strtoint(optarg, 3, INT_MAX);
904       certificateFd        = dup(tmpFd);
905       if (certificateFd < 0) {
906         fatal("Invalid certificate file handle");
907       }
908       check(!NOINTR(close(tmpFd)));
909     } else if (!idx--) {
910       // CSS
911       struct stat st;
912       if (!optarg || !*optarg || stat(optarg, &st) || !S_ISREG(st.st_mode)) {
913         fatal("\"--css\" expects a file name");
914       }
915       FILE *css            = fopen(optarg, "r");
916       if (!css) {
917         fatal("Cannot read style sheet \"%s\"", optarg);
918       } else {
919         check(cssStyleSheet= realloc(cssStyleSheet, strlen(cssStyleSheet) +
920                                      st.st_size + 2));
921         char *newData      = strrchr(cssStyleSheet, '\000');
922         *newData++         = '\n';
923         if (fread(newData, 1, st.st_size, css) != st.st_size) {
924           fatal("Failed to read style sheet \"%s\"", optarg);
925         }
926         newData[st.st_size]= '\000';
927         fclose(css);
928       }
929     } else if (!idx--) {
930       // CGI
931       if (demonize) {
932         fatal("CGI and background operations are mutually exclusive");
933       }
934       if (port) {
935         fatal("Cannot specify a port for CGI operation");
936       }
937       cgi                  = 1;
938       if (optarg && *optarg) {
939         char *ptr          = strchr(optarg, '-');
940         if (!ptr) {
941           fatal("Syntax error in port range specification");
942         }
943         *ptr               = '\000';
944         portMin            = strtoint(optarg, 1, 65535);
945         *ptr               = '-';
946         portMax            = strtoint(ptr + 1, portMin, 65535);
947       }
948     } else if (!idx--) {
949       // Debug
950       if (!logIsDefault() && !logIsDebug()) {
951         fatal("--debug is mutually exclusive with --quiet and --verbose.");
952       }
953       verbosity            = MSG_DEBUG;
954       logSetLogLevel(verbosity);
955     } else if (!idx--) {
956       // Static file
957       char *ptr, *path, *file;
958       if ((ptr             = strchr(optarg, ':')) == NULL) {
959         fatal("Syntax error in static-file definition \"%s\".", optarg);
960       }
961       check(path           = malloc(ptr - optarg + 1));
962       memcpy(path, optarg, ptr - optarg);
963       path[ptr - optarg]   = '\000';
964       file                 = strdup(ptr + 1);
965       if (getRefFromHashMap(externalFiles, path)) {
966         fatal("Duplicate static-file definition for \"%s\".", path);
967       }
968       addToHashMap(externalFiles, path, file);
969     } else if (!idx--) {
970       // Group
971       if (runAsGroup >= 0) {
972         fatal("Duplicate --group option.");
973       }
974       if (!optarg || !*optarg) {
975         fatal("\"--group\" expects a group name.");
976       }
977       runAsGroup           = parseGroup(optarg, NULL);
978     } else if (!idx--) {
979       // Linkify
980       if (!strcmp(optarg, "none")) {
981         linkifyURLs        = 0;
982       } else if (!strcmp(optarg, "normal")) {
983         linkifyURLs        = 1;
984       } else if (!strcmp(optarg, "aggressive")) {
985         linkifyURLs        = 2;
986       } else {
987         fatal("Invalid argument for --linkify. Must be "
988               "\"none\", \"normal\", or \"aggressive\".");
989       }
990     } else if (!idx--) {
991       // Localhost Only
992       localhostOnly        = 1;
993     } else if (!idx--) {
994       // No Beep
995       noBeep               = 1;
996     } else if (!idx--) {
997       // Numeric
998       numericHosts         = 1;
999     } else if (!idx--) {
1000       // Port
1001       if (port) {
1002         fatal("Duplicate --port option");
1003       }
1004       if (cgi) {
1005         fatal("Cannot specifiy a port for CGI operation");
1006       }
1007       if (!optarg || *optarg < '0' || *optarg > '9') {
1008         fatal("\"--port\" expects a port number.");
1009       }
1010       port = strtoint(optarg, 1, 65535);
1011     } else if (!idx--) {
1012       // Service
1013       struct Service *service;
1014       service              = newService(optarg);
1015       if (getRefFromHashMap(serviceTable, service->path)) {
1016         fatal("Duplicate service description for \"%s\".", service->path);
1017       }
1018       addToHashMap(serviceTable, service->path, (char *)service);
1019     } else if (!idx--) {
1020       // Disable SSL
1021       if (!hasSSL) {
1022         warn("Ignoring disable-ssl option, as SSL support is unavailable");
1023       }
1024       enableSSL            = 0;
1025     } else if (!idx--) {
1026       // Disable SSL Menu
1027       if (!hasSSL) {
1028         warn("Ignoring disable-ssl-menu option, as SSL support is "
1029              "unavailable");
1030       }
1031       enableSSLMenu        = 0;
1032     } else if (!idx--) {
1033       // Quiet
1034       if (!logIsDefault() && !logIsQuiet()) {
1035         fatal("--quiet is mutually exclusive with --debug and --verbose.");
1036       }
1037       verbosity            = MSG_QUIET;
1038       logSetLogLevel(verbosity);
1039     } else if (!idx--) {
1040       // User
1041       if (runAsUser >= 0) {
1042         fatal("Duplicate --user option.");
1043       }
1044       if (!optarg || !*optarg) {
1045         fatal("\"--user\" expects a user name.");
1046       }
1047       runAsUser            = parseUser(optarg, NULL);
1048     } else if (!idx--) {
1049       // User CSS
1050       if (!optarg || !*optarg) {
1051         fatal("\"--user-css\" expects a list of styles sheets and labels");
1052       }
1053       parseUserCSS(&userCSSList, optarg);
1054     } else if (!idx--) {
1055       // Verbose
1056       if (!logIsDefault() && (!logIsInfo() || logIsDebug())) {
1057         fatal("--verbose is mutually exclusive with --debug and --quiet");
1058       }
1059       verbosity            = MSG_INFO;
1060       logSetLogLevel(verbosity);
1061     } else if (!idx--) {
1062       // Version
1063       message("ShellInABox version " VERSION " (revision " VCS_REVISION ")");
1064       exit(0);
1065     }
1066   }
1067   if (optind != argc) {
1068     usage();
1069     fatal("Failed to parse command line");
1070   }
1071   char *buf                = NULL;
1072   check(argc >= 1);
1073   for (int i = 0; i < argc; i++) {
1074     buf                    = stringPrintf(buf, " %s", argv[i]);
1075   }
1076   info("Command line:%s", buf);
1077   free(buf);
1078
1079   // If the user did not specify a port, use the default one
1080   if (!cgi && !port) {
1081     port                   = PORTNUM;
1082   }
1083
1084   // If the user did not register any services, provide the default service
1085   if (!getHashmapSize(serviceTable)) {
1086     addToHashMap(serviceTable, "/", (char *)newService(geteuid() ? ":SSH" :
1087                                                                    ":LOGIN"));
1088   }
1089   enumerateServices(serviceTable);
1090   deleteHashMap(serviceTable);
1091
1092   // Do not allow non-root URLs for CGI operation
1093   if (cgi) {
1094     for (int i = 0; i < numServices; i++) {
1095       if (strcmp(services[i]->path, "/")) {
1096         fatal("Non-root service URLs are incompatible with CGI operation");
1097       }
1098     }
1099     check(cgiSessionKey    = newSessionKey());
1100   }
1101
1102   if (demonize) {
1103     pid_t pid;
1104     check((pid             = fork()) >= 0);
1105     if (pid) {
1106       _exit(0);
1107     }
1108     setsid();
1109     if (pidfile) {
1110 #ifndef O_LARGEFILE
1111 #define O_LARGEFILE 0
1112 #endif
1113       int fd               = NOINTR(open(pidfile,
1114                                          O_WRONLY|O_TRUNC|O_LARGEFILE|O_CREAT,
1115                                          0644));
1116       if (fd >= 0) {
1117         char buf[40];
1118         NOINTR(write(fd, buf, snprintf(buf, 40, "%d", (int)getpid())));
1119         check(!NOINTR(close(fd)));
1120       }
1121     }
1122   }
1123   free((char *)pidfile);
1124 }
1125
1126 static void removeLimits() {
1127   static int res[] = { RLIMIT_CPU, RLIMIT_DATA, RLIMIT_FSIZE, RLIMIT_NPROC };
1128   for (int i = 0; i < sizeof(res)/sizeof(int); i++) {
1129     struct rlimit rl;
1130     getrlimit(res[i], &rl);
1131     if (rl.rlim_max < RLIM_INFINITY) {
1132       rl.rlim_max  = RLIM_INFINITY;
1133       setrlimit(res[i], &rl);
1134       getrlimit(res[i], &rl);
1135     }
1136     if (rl.rlim_cur < rl.rlim_max) {
1137       rl.rlim_cur  = rl.rlim_max;
1138       setrlimit(res[i], &rl);
1139     }
1140   }
1141 }
1142
1143 static void setUpSSL(Server *server) {
1144   serverEnableSSL(server, enableSSL);
1145
1146   // Enable SSL support (if available)
1147   if (enableSSL) {
1148     check(serverSupportsSSL());
1149     if (certificateFd >= 0) {
1150       serverSetCertificateFd(server, certificateFd);
1151     } else if (certificateDir) {
1152       char *tmp;
1153       if (strchr(certificateDir, '%')) {
1154         fatal("Invalid certificate directory name \"%s\".", certificateDir);
1155       }
1156       check(tmp = stringPrintf(NULL, "%s/certificate%%s.pem", certificateDir));
1157       serverSetCertificate(server, tmp, 1);
1158       free(tmp);
1159     } else {
1160       serverSetCertificate(server, "certificate%s.pem", 1);
1161     }
1162   }
1163 }
1164
1165 int main(int argc, char * const argv[]) {
1166 #ifdef HAVE_SYS_PRCTL_H
1167   // Disable core files
1168   prctl(PR_SET_DUMPABLE, 0, 0, 0, 0);
1169 #endif
1170   struct rlimit rl = { 0 };
1171   setrlimit(RLIMIT_CORE, &rl);
1172   removeLimits();
1173
1174   // Parse command line arguments
1175   parseArgs(argc, argv);
1176
1177   // Fork the launcher process, allowing us to drop privileges in the main
1178   // process.
1179   int launcherFd  = forkLauncher();
1180
1181   // Make sure that our timestamps will print in the standard format
1182   setlocale(LC_TIME, "POSIX");
1183
1184   // Create a new web server
1185   Server *server;
1186   if (port) {
1187     check(server  = newServer(localhostOnly, port));
1188     dropPrivileges();
1189     setUpSSL(server);
1190   } else {
1191     // For CGI operation we fork the new server, so that it runs in the
1192     // background.
1193     pid_t pid;
1194     int   fds[2];
1195     dropPrivileges();
1196     check(!pipe(fds));
1197     check((pid    = fork()) >= 0);
1198     if (pid) {
1199       // Wait for child to output initial HTML page
1200       char wait;
1201       check(!NOINTR(close(fds[1])));
1202       check(!NOINTR(read(fds[0], &wait, 1)));
1203       check(!NOINTR(close(fds[0])));
1204       _exit(0);
1205     }
1206     check(!NOINTR(close(fds[0])));
1207     check(server  = newCGIServer(localhostOnly, portMin, portMax,
1208                                  AJAX_TIMEOUT));
1209     cgiServer     = server;
1210     setUpSSL(server);
1211
1212     // Output a <frameset> that includes our root page
1213     check(port    = serverGetListeningPort(server));
1214     extern char cgiRootStart[];
1215     extern char cgiRootEnd[];
1216     char *cgiRoot;
1217     check(cgiRoot = malloc(cgiRootEnd - cgiRootStart + 1));
1218     memcpy(cgiRoot, cgiRootStart, cgiRootEnd - cgiRootStart);
1219     cgiRoot[cgiRootEnd - cgiRootStart] = '\000';
1220     printf("X-ShellInABox-Port: %d\r\n"
1221            "X-ShellInABox-Pid: %d\r\n"
1222            "Content-type: text/html; charset=utf-8\r\n\r\n",
1223            port, getpid());
1224     printfUnchecked(cgiRoot, port, cgiSessionKey);
1225     fflush(stdout);
1226     free(cgiRoot);
1227     check(!NOINTR(close(fds[1])));
1228     closeAllFds((int []){ launcherFd, serverGetFd(server) }, 2);
1229     logSetLogLevel(MSG_QUIET);
1230   }
1231
1232   // Set log file format
1233   serverSetNumericHosts(server, numericHosts);
1234
1235   // Disable /quit handler
1236   serverRegisterHttpHandler(server, "/quit", NULL, NULL);
1237
1238   // Register HTTP handler(s)
1239   for (int i = 0; i < numServices; i++) {
1240     serverRegisterHttpHandler(server, services[i]->path,
1241                               shellInABoxHttpHandler, services[i]);
1242   }
1243
1244   // Register handlers for external files
1245   iterateOverHashMap(externalFiles, registerExternalFiles, server);
1246
1247   // Start the server
1248   serverLoop(server);
1249
1250   // Clean up
1251   deleteServer(server);
1252   finishAllSessions();
1253   deleteHashMap(externalFiles);
1254   for (int i = 0; i < numServices; i++) {
1255     deleteService(services[i]);
1256   }
1257   free(services);
1258   free(certificateDir);
1259   free(cgiSessionKey);
1260   info("Done");
1261   _exit(0);
1262 }
This page took 0.123713 seconds and 3 git commands to generate.