]>
Commit | Line | Data |
---|---|---|
7460295f MG |
1 | // shellinaboxd.c -- A custom web server that makes command line applications |
2 | // available as AJAX web applications. | |
e0bb8a33 | 3 | // Copyright (C) 2008-2009 Markus Gutschke <markus@shellinabox.com> |
7460295f MG |
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 | |
bdd01e84 | 48 | #include "config.h" |
7460295f MG |
49 | |
50 | #include <fcntl.h> | |
51 | #include <getopt.h> | |
52 | #include <limits.h> | |
53 | #include <locale.h> | |
54 | #include <poll.h> | |
9b850878 | 55 | #include <stdarg.h> |
7460295f MG |
56 | #include <stdio.h> |
57 | #include <stdlib.h> | |
58 | #include <string.h> | |
d1edcc0e | 59 | #include <sys/resource.h> |
7460295f MG |
60 | #include <sys/types.h> |
61 | #include <sys/stat.h> | |
62 | #include <unistd.h> | |
63 | ||
47e62a9c MG |
64 | #ifdef HAVE_SYS_PRCTL_H |
65 | #include <sys/prctl.h> | |
66 | #endif | |
67 | ||
7460295f MG |
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" | |
ecbff9b9 | 75 | #include "shellinabox/usercss.h" |
7460295f MG |
76 | |
77 | #define PORTNUM 4200 | |
78 | #define MAX_RESPONSE 2048 | |
79 | ||
ecbff9b9 MG |
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; | |
7460295f MG |
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 | ||
9b850878 MG |
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 | ||
7460295f | 174 | static int completePendingRequest(struct Session *session, |
5e56158a | 175 | const char *buf, int len, int maxLength) { |
7460295f MG |
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" | |
7933e348 | 232 | "Content-Length: %ld\r\n" |
38d64ad8 | 233 | "Cache-Control: no-cache\r\n" |
7460295f MG |
234 | "\r\n" |
235 | "%s", | |
7933e348 | 236 | (long)strlen(json), |
7460295f MG |
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)); | |
30046882 MG |
282 | connection = serverGetConnection(session->server, |
283 | connection, | |
284 | session->pty); | |
285 | session->connection = connection; | |
7460295f MG |
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; | |
3240f75b | 324 | struct Session *session = findCGISession(&isNew, http, url, cgiSessionKey); |
7460295f | 325 | if (session == NULL) { |
47e62a9c | 326 | httpSendReply(http, 400, "Bad Request", NO_MSG); |
7460295f MG |
327 | return HTTP_DONE; |
328 | } | |
329 | ||
3240f75b | 330 | // Sanity check |
7460295f MG |
331 | if (!isNew && strcmp(session->peerName, httpGetPeerName(http))) { |
332 | error("Peername changed from %s to %s", | |
333 | session->peerName, httpGetPeerName(http)); | |
47e62a9c | 334 | httpSendReply(http, 400, "Bad Request", NO_MSG); |
7460295f MG |
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"); | |
1ef7f27f | 344 | const char *rootURL = getFromHashMap(args, "rooturl"); |
7460295f MG |
345 | |
346 | // Adjust window dimensions if provided by client | |
347 | if (width && height) { | |
348 | session->width = atoi(width); | |
349 | session->height = atoi(height); | |
350 | } | |
351 | ||
3240f75b MG |
352 | // Create a new session, if the client did not provide an existing one |
353 | if (isNew) { | |
d5324103 MG |
354 | if (keys) { |
355 | bad_new_session: | |
3240f75b | 356 | abandonSession(session); |
47e62a9c | 357 | httpSendReply(http, 400, "Bad Request", NO_MSG); |
7460295f MG |
358 | return HTTP_DONE; |
359 | } | |
d5324103 MG |
360 | |
361 | if (cgiServer && cgiSessions++) { | |
362 | serverExitLoop(cgiServer, 1); | |
363 | goto bad_new_session; | |
364 | } | |
7460295f | 365 | session->http = http; |
1ef7f27f MG |
366 | if (launchChild(service->id, session, |
367 | rootURL && *rootURL ? rootURL : urlGetURL(url)) < 0) { | |
3240f75b | 368 | abandonSession(session); |
47e62a9c | 369 | httpSendReply(http, 500, "Internal Error", NO_MSG); |
3240f75b MG |
370 | return HTTP_DONE; |
371 | } | |
372 | if (cgiServer) { | |
373 | terminateLauncher(); | |
374 | } | |
375 | session->connection = serverAddConnection(httpGetServer(http), | |
7460295f MG |
376 | session->pty, handleSession, |
377 | sessionDone, session); | |
3240f75b | 378 | serverSetTimeout(session->connection, AJAX_TIMEOUT); |
7460295f MG |
379 | } |
380 | ||
381 | // Reset window dimensions of the pseudo TTY, if changed since last time set. | |
382 | if (session->width > 0 && session->height > 0 && | |
383 | (session->width != oldWidth || session->height != oldHeight)) { | |
384 | debug("Window size changed to %dx%d", session->width, session->height); | |
385 | setWindowSize(session->pty, session->width, session->height); | |
386 | } | |
387 | ||
388 | // Process keypresses, if any. Then send a synchronous reply. | |
389 | if (keys) { | |
390 | char *keyCodes; | |
391 | check(keyCodes = malloc(strlen(keys)/2)); | |
392 | int len = 0; | |
393 | for (const unsigned char *ptr = (const unsigned char *)keys; ;) { | |
394 | unsigned c0 = *ptr++; | |
395 | if (c0 < '0' || (c0 > '9' && c0 < 'A') || | |
396 | (c0 > 'F' && c0 < 'a') || c0 > 'f') { | |
397 | break; | |
398 | } | |
399 | unsigned c1 = *ptr++; | |
400 | if (c1 < '0' || (c1 > '9' && c1 < 'A') || | |
401 | (c1 > 'F' && c1 < 'a') || c1 > 'f') { | |
402 | break; | |
403 | } | |
404 | keyCodes[len++] = 16*((c0 & 0xF) + 9*(c0 > '9')) + | |
405 | (c1 & 0xF) + 9*(c1 > '9'); | |
406 | } | |
407 | if (write(session->pty, keyCodes, len) < 0 && errno == EAGAIN) { | |
408 | completePendingRequest(session, "\007", 1, MAX_RESPONSE); | |
409 | } | |
410 | free(keyCodes); | |
411 | httpSendReply(http, 200, "OK", " "); | |
0fae962f | 412 | check(session->http != http); |
7460295f | 413 | return HTTP_DONE; |
3240f75b MG |
414 | } else { |
415 | // This request is polling for data. Finish any pending requests and | |
416 | // queue (or process) a new one. | |
417 | if (session->http && session->http != http && | |
418 | !completePendingRequest(session, "", 0, MAX_RESPONSE)) { | |
47e62a9c | 419 | httpSendReply(http, 400, "Bad Request", NO_MSG); |
3240f75b MG |
420 | return HTTP_DONE; |
421 | } | |
422 | session->http = http; | |
7460295f MG |
423 | } |
424 | ||
425 | session->connection = serverGetConnection(session->server, | |
426 | session->connection, | |
427 | session->pty); | |
0fae962f | 428 | if (session->buffered || isNew) { |
7460295f MG |
429 | if (completePendingRequest(session, "", 0, MAX_RESPONSE) && |
430 | session->connection) { | |
431 | // Reset the timeout, as we just received a new request. | |
432 | serverSetTimeout(session->connection, AJAX_TIMEOUT); | |
433 | if (session->len < MAX_RESPONSE) { | |
434 | // Re-enable input on the child's pty | |
435 | serverConnectionSetEvents(session->server, session->connection,POLLIN); | |
436 | } | |
437 | } | |
438 | return HTTP_DONE; | |
439 | } else if (session->connection) { | |
440 | // Re-enable input on the child's pty | |
441 | serverConnectionSetEvents(session->server, session->connection, POLLIN); | |
442 | serverSetTimeout(session->connection, AJAX_TIMEOUT); | |
443 | } | |
444 | ||
445 | return HTTP_SUSPEND; | |
446 | } | |
447 | ||
448 | static void serveStaticFile(HttpConnection *http, const char *contentType, | |
449 | const char *start, const char *end) { | |
8e5d3913 MG |
450 | char *body = (char *)start; |
451 | char *bodyEnd = (char *)end; | |
452 | ||
453 | // Unfortunately, there are still some browsers that are so buggy that they | |
454 | // need special conditional code. In anything that has a "text" MIME type, | |
455 | // we allow simple conditionals. Nested conditionals are not supported. | |
456 | if (!memcmp(contentType, "text/", 5)) { | |
457 | char *tag = NULL; | |
458 | int condTrue = -1; | |
459 | char *ifPtr = NULL; | |
460 | char *elsePtr = NULL; | |
461 | for (char *ptr = body; bodyEnd - ptr >= 6; ) { | |
462 | char *eol = ptr; | |
463 | eol = memchr(eol, '\n', bodyEnd - eol); | |
464 | if (eol == NULL) { | |
465 | eol = bodyEnd; | |
466 | } else { | |
467 | ++eol; | |
468 | } | |
469 | if (!memcmp(ptr, "[if ", 4)) { | |
470 | char *bracket = memchr(ptr + 4, ']', eol - ptr - 4); | |
471 | if (bracket != NULL && bracket > ptr + 4) { | |
472 | check(tag = malloc(bracket - ptr - 3)); | |
473 | memcpy(tag, ptr + 4, bracket - ptr - 4); | |
474 | tag[bracket - ptr - 4] = '\000'; | |
475 | condTrue = 0; | |
476 | const char *userAgent = getFromHashMap(httpGetHeaders(http), | |
477 | "user-agent"); | |
478 | if (userAgent) { | |
479 | // Allow multiple comma separated conditions | |
480 | for (char *tagPtr = tag; *tagPtr; ) { | |
481 | char *e = strchr(tagPtr, ','); | |
482 | if (!e) { | |
483 | e = strchr(tag, '\000'); | |
484 | } else { | |
485 | *e++ = '\000'; | |
486 | } | |
487 | condTrue = userCSSGetDefine(tagPtr) || | |
488 | strstr(userAgent, tagPtr) != NULL; | |
489 | if (*e) { | |
490 | e[-1] = ','; | |
491 | } | |
492 | if (condTrue) { | |
493 | break; | |
494 | } | |
495 | tagPtr = e; | |
496 | } | |
497 | } | |
498 | ||
499 | // If we find any conditionals, then we need to make a copy of | |
500 | // the text document. We do this lazily, as presumably the majority | |
501 | // of text documents won't have conditionals. | |
502 | if (body == start) { | |
503 | check(body = malloc(end - start)); | |
504 | memcpy(body, start, end - start); | |
505 | bodyEnd += body - start; | |
506 | ptr += body - start; | |
507 | eol += body - start; | |
508 | } | |
509 | ||
510 | // Remember the beginning of the "[if ...]" statement | |
511 | ifPtr = ptr; | |
512 | } | |
513 | } else if (ifPtr && !elsePtr && eol - ptr >= strlen(tag) + 7 && | |
514 | !memcmp(ptr, "[else ", 6) && | |
515 | !memcmp(ptr + 6, tag, strlen(tag)) && | |
516 | ptr[6 + strlen(tag)] == ']') { | |
517 | // Found an "[else ...]" statement. Remember where it started. | |
518 | elsePtr = ptr; | |
519 | } else if (ifPtr && eol - ptr >= strlen(tag) + 8 && | |
520 | !memcmp(ptr, "[endif ", 7) && | |
521 | !memcmp(ptr + 7, tag, strlen(tag)) && | |
522 | ptr[7 + strlen(tag)] == ']') { | |
523 | // Found the closing "[endif ...]" statement. Now we can remove those | |
524 | // parts of the conditionals that do not apply to this user agent. | |
525 | char *s, *e; | |
526 | if (condTrue) { | |
527 | s = strchr(ifPtr, '\n') + 1; | |
528 | e = elsePtr ? elsePtr : ptr; | |
529 | } else { | |
530 | if (elsePtr) { | |
531 | s = strchr(elsePtr, '\n') + 1; | |
532 | e = ptr; | |
533 | } else { | |
534 | s = ifPtr; | |
535 | e = ifPtr; | |
536 | } | |
537 | } | |
538 | memmove(ifPtr, s, e - s); | |
539 | memmove(ifPtr + (e - s), eol, bodyEnd - eol); | |
540 | bodyEnd -= (s - ifPtr) + (eol - e); | |
541 | eol = ifPtr + (e - s); | |
542 | ifPtr = NULL; | |
543 | elsePtr = NULL; | |
544 | free(tag); | |
545 | tag = NULL; | |
546 | } | |
547 | ptr = eol; | |
548 | } | |
549 | free(tag); | |
550 | } | |
551 | ||
7460295f MG |
552 | char *response = stringPrintf(NULL, |
553 | "HTTP/1.1 200 OK\r\n" | |
554 | "Content-Type: %s\r\n" | |
7933e348 | 555 | "Content-Length: %ld\r\n" |
8e5d3913 MG |
556 | "%s\r\n", |
557 | contentType, (long)(bodyEnd - body), | |
558 | body == start ? "" : | |
559 | "Cache-Control: no-cache\r\n"); | |
7460295f MG |
560 | int len = strlen(response); |
561 | if (strcmp(httpGetMethod(http), "HEAD")) { | |
8e5d3913 MG |
562 | check(response = realloc(response, len + (bodyEnd - body))); |
563 | memcpy(response + len, body, bodyEnd - body); | |
564 | len += bodyEnd - body; | |
565 | } | |
566 | ||
567 | // If we expanded conditionals, we had to create a temporary copy. Delete | |
568 | // it now. | |
569 | if (body != start) { | |
570 | free(body); | |
7460295f | 571 | } |
8e5d3913 | 572 | |
7460295f MG |
573 | httpTransfer(http, response, len); |
574 | } | |
575 | ||
572ac014 MG |
576 | static const char *addr(const char *a) { |
577 | // Work-around for a gcc bug that could occasionally generate invalid | |
578 | // assembly instructions when optimizing code too agressively. | |
579 | asm volatile(""); | |
580 | return a; | |
581 | } | |
582 | ||
7460295f MG |
583 | static int shellInABoxHttpHandler(HttpConnection *http, void *arg, |
584 | const char *buf, int len) { | |
585 | checkGraveyard(); | |
586 | URL *url = newURL(http, buf, len); | |
587 | const HashMap *headers = httpGetHeaders(http); | |
588 | const char *contentType = getFromHashMap(headers, "content-type"); | |
589 | ||
590 | // Normalize the path info | |
591 | const char *pathInfo = urlGetPathInfo(url); | |
592 | while (*pathInfo == '/') { | |
593 | pathInfo++; | |
594 | } | |
595 | const char *endPathInfo; | |
596 | for (endPathInfo = pathInfo; | |
597 | *endPathInfo && *endPathInfo != '/'; | |
598 | endPathInfo++) { | |
599 | } | |
600 | int pathInfoLength = endPathInfo - pathInfo; | |
11cd1451 | 601 | |
1e1c8024 MG |
602 | if (!pathInfoLength || |
603 | (pathInfoLength == 5 && !memcmp(pathInfo, "plain", 5)) || | |
604 | (pathInfoLength == 6 && !memcmp(pathInfo, "secure", 6))) { | |
605 | // The root page serves the AJAX application. | |
7460295f MG |
606 | if (contentType && |
607 | !strncasecmp(contentType, "application/x-www-form-urlencoded", 33)) { | |
608 | // XMLHttpRequest carrying data between the AJAX application and the | |
609 | // client session. | |
610 | return dataHandler(http, arg, buf, len, url); | |
611 | } | |
1e1c8024 MG |
612 | extern char rootPageStart[]; |
613 | extern char rootPageEnd[]; | |
614 | char *rootPage; | |
615 | check(rootPage = malloc(rootPageEnd - rootPageStart + 1)); | |
616 | memcpy(rootPage, rootPageStart, rootPageEnd - rootPageStart); | |
617 | rootPage[rootPageEnd - rootPageStart] = '\000'; | |
618 | char *html = stringPrintf(NULL, rootPage, | |
619 | enableSSL ? "true" : "false"); | |
d9c5b5b2 MG |
620 | serveStaticFile(http, "text/html", html, strrchr(html, '\000')); |
621 | free(html); | |
1e1c8024 | 622 | free(rootPage); |
7460295f MG |
623 | } else if (pathInfoLength == 8 && !memcmp(pathInfo, "beep.wav", 8)) { |
624 | // Serve the audio sample for the console bell. | |
625 | extern char beepStart[]; | |
626 | extern char beepEnd[]; | |
627 | serveStaticFile(http, "audio/x-wav", beepStart, beepEnd); | |
08db8657 MG |
628 | } else if (pathInfoLength == 11 && !memcmp(pathInfo, "enabled.gif", 11)) { |
629 | // Serve the checkmark icon used in the context menu | |
630 | extern char enabledStart[]; | |
631 | extern char enabledEnd[]; | |
632 | serveStaticFile(http, "image/gif", enabledStart, enabledEnd); | |
7460295f MG |
633 | } else if (pathInfoLength == 11 && !memcmp(pathInfo, "favicon.ico", 11)) { |
634 | // Serve the favicon | |
635 | extern char faviconStart[]; | |
636 | extern char faviconEnd[]; | |
637 | serveStaticFile(http, "image/x-icon", faviconStart, faviconEnd); | |
638 | } else if (pathInfoLength == 14 && !memcmp(pathInfo, "ShellInABox.js", 14)) { | |
639 | // Serve both vt100.js and shell_in_a_box.js in the same transaction. | |
640 | // Also, indicate to the client whether the server is SSL enabled. | |
641 | extern char vt100Start[]; | |
642 | extern char vt100End[]; | |
643 | extern char shellInABoxStart[]; | |
644 | extern char shellInABoxEnd[]; | |
ecbff9b9 | 645 | char *userCSSString = getUserCSSString(userCSSList); |
46f2036a MG |
646 | char *stateVars = stringPrintf(NULL, |
647 | "serverSupportsSSL = %s;\n" | |
b624088c | 648 | "disableSSLMenu = %s;\n" |
f0c6fd39 | 649 | "suppressAllAudio = %s;\n" |
ecbff9b9 MG |
650 | "linkifyURLs = %d;\n" |
651 | "userCSSList = %s;\n\n", | |
b624088c MG |
652 | enableSSL ? "true" : "false", |
653 | !enableSSLMenu ? "true" : "false", | |
f0c6fd39 | 654 | noBeep ? "true" : "false", |
ecbff9b9 MG |
655 | linkifyURLs, userCSSString); |
656 | free(userCSSString); | |
46f2036a MG |
657 | int stateVarsLength = strlen(stateVars); |
658 | int contentLength = stateVarsLength + | |
572ac014 MG |
659 | (addr(vt100End) - addr(vt100Start)) + |
660 | (addr(shellInABoxEnd) - addr(shellInABoxStart)); | |
7460295f MG |
661 | char *response = stringPrintf(NULL, |
662 | "HTTP/1.1 200 OK\r\n" | |
663 | "Content-Type: text/javascript; charset=utf-8\r\n" | |
664 | "Content-Length: %d\r\n" | |
665 | "\r\n", | |
666 | contentLength); | |
667 | int headerLength = strlen(response); | |
668 | if (strcmp(httpGetMethod(http), "HEAD")) { | |
669 | check(response = realloc(response, headerLength + contentLength)); | |
670 | memcpy(memcpy(memcpy( | |
46f2036a | 671 | response + headerLength, stateVars, stateVarsLength)+stateVarsLength, |
7460295f MG |
672 | vt100Start, vt100End - vt100Start) + (vt100End - vt100Start), |
673 | shellInABoxStart, shellInABoxEnd - shellInABoxStart); | |
674 | } else { | |
675 | contentLength = 0; | |
676 | } | |
46f2036a | 677 | free(stateVars); |
7460295f MG |
678 | httpTransfer(http, response, headerLength + contentLength); |
679 | } else if (pathInfoLength == 10 && !memcmp(pathInfo, "styles.css", 10)) { | |
680 | // Serve the style sheet. | |
1f771613 MG |
681 | serveStaticFile(http, "text/css; charset=utf-8", |
682 | cssStyleSheet, strrchr(cssStyleSheet, '\000')); | |
ecbff9b9 MG |
683 | } else if (pathInfoLength > 8 && !memcmp(pathInfo, "usercss-", 8)) { |
684 | // Server user style sheets (if any) | |
685 | struct UserCSS *css = userCSSList; | |
686 | for (int idx = atoi(pathInfo + 8); | |
687 | idx-- > 0 && css; css = css->next ) { | |
688 | } | |
689 | if (css) { | |
690 | serveStaticFile(http, "text/css; charset=utf-8", | |
691 | css->style, css->style + css->styleLen); | |
692 | } else { | |
693 | httpSendReply(http, 404, "File not found", NO_MSG); | |
694 | } | |
7460295f | 695 | } else { |
47e62a9c | 696 | httpSendReply(http, 404, "File not found", NO_MSG); |
7460295f MG |
697 | } |
698 | ||
699 | deleteURL(url); | |
700 | return HTTP_DONE; | |
701 | } | |
702 | ||
703 | static int strtoint(const char *s, int minVal, int maxVal) { | |
704 | char *ptr; | |
705 | if (!*s) { | |
706 | fatal("Missing numeric value."); | |
707 | } | |
708 | long l = strtol(s, &ptr, 10); | |
709 | if (*ptr || l < minVal || l > maxVal) { | |
710 | fatal("Range error on numeric value \"%s\".", s); | |
711 | } | |
712 | return l; | |
713 | } | |
714 | ||
715 | static void usage(void) { | |
716 | // Drop privileges so that we can tell which uid/gid we would normally | |
717 | // run at. | |
718 | dropPrivileges(); | |
719 | uid_t r_uid, e_uid, s_uid; | |
720 | uid_t r_gid, e_gid, s_gid; | |
721 | check(!getresuid(&r_uid, &e_uid, &s_uid)); | |
722 | check(!getresgid(&r_gid, &e_gid, &s_gid)); | |
723 | const char *user = getUserName(r_uid); | |
724 | const char *group = getGroupName(r_gid); | |
725 | ||
726 | message("Usage: shellinaboxd [OPTIONS]...\n" | |
727 | "Starts an HTTP server that serves terminal emulators to AJAX " | |
728 | "enabled browsers.\n" | |
729 | "\n" | |
730 | "List of command line options:\n" | |
731 | " -b, --background[=PIDFILE] run in background\n" | |
732 | "%s" | |
1f771613 | 733 | " --css=FILE attach contents to CSS style sheet\n" |
d1edcc0e | 734 | " --cgi[=PORTMIN-PORTMAX] run as CGI\n" |
7460295f MG |
735 | " -d, --debug enable debug mode\n" |
736 | " -f, --static-file=URL:FILE serve static file from URL path\n" | |
737 | " -g, --group=GID switch to this group (default: %s)\n" | |
738 | " -h, --help print this message\n" | |
f0c6fd39 | 739 | " --linkify=[none|normal|agressive] default is \"normal\"\n" |
53b27f78 | 740 | " --localhost-only only listen on 127.0.0.1\n" |
46f2036a | 741 | " --no-beep suppress all audio output\n" |
7460295f MG |
742 | " -n, --numeric do not resolve hostnames\n" |
743 | " -p, --port=PORT select a port (default: %d)\n" | |
744 | " -s, --service=SERVICE define one or more services\n" | |
745 | "%s" | |
746 | " -q, --quiet turn off all messages\n" | |
747 | " -u, --user=UID switch to this user (default: %s)\n" | |
ecbff9b9 | 748 | " --user-css=STYLES defines user-selectable CSS options\n" |
7460295f MG |
749 | " -v, --verbose enable logging messages\n" |
750 | " --version prints version information\n" | |
751 | "\n" | |
752 | "Debug, quiet, and verbose are mutually exclusive.\n" | |
753 | "\n" | |
754 | "One or more --service arguments define services that should " | |
ecbff9b9 | 755 | "be made available\n" |
7460295f MG |
756 | "through the web interface:\n" |
757 | " SERVICE := <url-path> ':' APP\n" | |
78016c46 MG |
758 | " APP := 'LOGIN' | 'SSH' [ : <host> ] | " |
759 | "USER ':' CWD ':' <cmdline>\n" | |
7460295f MG |
760 | " USER := %s<username> ':' <groupname>\n" |
761 | " CWD := 'HOME' | <dir>\n" | |
762 | "\n" | |
763 | "<cmdline> supports variable expansion:\n" | |
764 | " ${columns} - number of columns\n" | |
765 | " ${gid} - gid id\n" | |
766 | " ${group} - group name\n" | |
767 | " ${home} - home directory\n" | |
768 | " ${lines} - number of rows\n" | |
769 | " ${peer} - name of remote peer\n" | |
770 | " ${uid} - user id\n" | |
ecbff9b9 MG |
771 | " ${user} - user name\n" |
772 | "\n" | |
773 | "One or more --user-css arguments define optional user-selectable " | |
774 | "CSS options.\n" | |
775 | "These options show up in the right-click context menu:\n" | |
776 | " STYLES := GROUP { ';' GROUP }*\n" | |
777 | " GROUP := OPTION { ',' OPTION }*\n" | |
778 | " OPTION := <label> ':' [ '-' | '+' ] <css-file>\n" | |
779 | "\n" | |
780 | "OPTIONs that make up a GROUP are mutually exclusive. But " | |
781 | "individual GROUPs are\n" | |
782 | "independent of each other.\n", | |
7460295f MG |
783 | !serverSupportsSSL() ? "" : |
784 | " -c, --cert=CERTDIR set certificate dir " | |
dc6575f2 | 785 | "(default: $PWD)\n" |
52404edf | 786 | " --cert-fd=FD set certificate file from fd\n", |
7460295f MG |
787 | group, PORTNUM, |
788 | !serverSupportsSSL() ? "" : | |
b624088c | 789 | " -t, --disable-ssl disable transparent SSL support\n" |
52404edf | 790 | " --disable-ssl-menu disallow changing transport mode\n", |
7460295f MG |
791 | user, supportsPAM() ? "'AUTH' | " : ""); |
792 | free((char *)user); | |
793 | free((char *)group); | |
794 | } | |
795 | ||
796 | static void destroyExternalFileHashEntry(void *arg, char *key, char *value) { | |
797 | free(key); | |
798 | free(value); | |
799 | } | |
800 | ||
801 | static void parseArgs(int argc, char * const argv[]) { | |
802 | int hasSSL = serverSupportsSSL(); | |
803 | if (!hasSSL) { | |
804 | enableSSL = 0; | |
805 | } | |
806 | int demonize = 0; | |
d1edcc0e | 807 | int cgi = 0; |
7460295f MG |
808 | const char *pidfile = NULL; |
809 | int verbosity = MSG_DEFAULT; | |
810 | externalFiles = newHashMap(destroyExternalFileHashEntry, NULL); | |
d1edcc0e | 811 | HashMap *serviceTable = newHashMap(destroyServiceHashEntry, NULL); |
1f771613 MG |
812 | extern char stylesStart[]; |
813 | extern char stylesEnd[]; | |
1ef7f27f | 814 | check(cssStyleSheet = malloc(stylesEnd - stylesStart + 1)); |
1f771613 MG |
815 | memcpy(cssStyleSheet, stylesStart, stylesEnd - stylesStart); |
816 | cssStyleSheet[stylesEnd - stylesStart] = '\000'; | |
817 | ||
7460295f | 818 | for (;;) { |
d1edcc0e | 819 | static const char optstring[] = "+hb::c:df:g:np:s:tqu:v"; |
7460295f | 820 | static struct option options[] = { |
b624088c MG |
821 | { "help", 0, 0, 'h' }, |
822 | { "background", 2, 0, 'b' }, | |
823 | { "cert", 1, 0, 'c' }, | |
824 | { "cert-fd", 1, 0, 0 }, | |
1f771613 | 825 | { "css", 1, 0, 0 }, |
b624088c MG |
826 | { "cgi", 2, 0, 0 }, |
827 | { "debug", 0, 0, 'd' }, | |
828 | { "static-file", 1, 0, 'f' }, | |
829 | { "group", 1, 0, 'g' }, | |
f0c6fd39 | 830 | { "linkify", 1, 0, 0 }, |
53b27f78 | 831 | { "localhost-only", 0, 0, 0 }, |
b624088c MG |
832 | { "no-beep", 0, 0, 0 }, |
833 | { "numeric", 0, 0, 'n' }, | |
834 | { "port", 1, 0, 'p' }, | |
835 | { "service", 1, 0, 's' }, | |
836 | { "disable-ssl", 0, 0, 't' }, | |
837 | { "disable-ssl-menu", 0, 0, 0 }, | |
838 | { "quiet", 0, 0, 'q' }, | |
839 | { "user", 1, 0, 'u' }, | |
ecbff9b9 | 840 | { "user-css", 1, 0, 0 }, |
b624088c MG |
841 | { "verbose", 0, 0, 'v' }, |
842 | { "version", 0, 0, 0 }, | |
843 | { 0, 0, 0, 0 } }; | |
7460295f MG |
844 | int idx = -1; |
845 | int c = getopt_long(argc, argv, optstring, options, &idx); | |
846 | if (c > 0) { | |
847 | for (int i = 0; options[i].name; i++) { | |
848 | if (options[i].val == c) { | |
849 | idx = i; | |
850 | break; | |
851 | } | |
852 | } | |
853 | } else if (c < 0) { | |
854 | break; | |
855 | } | |
856 | if (idx-- <= 0) { | |
857 | // Help (or invalid argument) | |
858 | usage(); | |
ecbff9b9 | 859 | if (idx < -1) { |
56d6406f MG |
860 | fatal("Failed to parse command line"); |
861 | } | |
862 | exit(0); | |
7460295f MG |
863 | } else if (!idx--) { |
864 | // Background | |
d1edcc0e MG |
865 | if (cgi) { |
866 | fatal("CGI and background operations are mutually exclusive"); | |
867 | } | |
7460295f MG |
868 | demonize = 1; |
869 | if (optarg && pidfile) { | |
870 | fatal("Only one pidfile can be given"); | |
871 | } | |
1f771613 | 872 | if (optarg && *optarg) { |
7460295f MG |
873 | pidfile = strdup(optarg); |
874 | } | |
875 | } else if (!idx--) { | |
876 | // Certificate | |
877 | if (!hasSSL) { | |
878 | warn("Ignoring certificate directory, as SSL support is unavailable"); | |
879 | } | |
50eebacb | 880 | if (certificateFd >= 0) { |
dc6575f2 MG |
881 | fatal("Cannot set both a certificate directory and file handle"); |
882 | } | |
7460295f MG |
883 | if (certificateDir) { |
884 | fatal("Only one certificate directory can be selected"); | |
885 | } | |
1f771613 MG |
886 | struct stat st; |
887 | if (!optarg || !*optarg || stat(optarg, &st) || !S_ISDIR(st.st_mode)) { | |
888 | fatal("\"--cert\" expects a directory name"); | |
889 | } | |
7460295f | 890 | check(certificateDir = strdup(optarg)); |
dc6575f2 MG |
891 | } else if (!idx--) { |
892 | // Certificate file descriptor | |
893 | if (!hasSSL) { | |
894 | warn("Ignoring certificate directory, as SSL support is unavailable"); | |
895 | } | |
896 | if (certificateDir) { | |
897 | fatal("Cannot set both a certificate directory and file handle"); | |
898 | } | |
899 | if (certificateFd >= 0) { | |
900 | fatal("Only one certificate file handle can be provided"); | |
901 | } | |
1f771613 MG |
902 | if (!optarg || *optarg < '0' || *optarg > '9') { |
903 | fatal("\"--cert-fd\" expects a valid file handle"); | |
904 | } | |
dc6575f2 MG |
905 | int tmpFd = strtoint(optarg, 3, INT_MAX); |
906 | certificateFd = dup(tmpFd); | |
907 | if (certificateFd < 0) { | |
908 | fatal("Invalid certificate file handle"); | |
909 | } | |
910 | check(!NOINTR(close(tmpFd))); | |
1f771613 MG |
911 | } else if (!idx--) { |
912 | // CSS | |
913 | struct stat st; | |
914 | if (!optarg || !*optarg || stat(optarg, &st) || !S_ISREG(st.st_mode)) { | |
915 | fatal("\"--css\" expects a file name"); | |
916 | } | |
917 | FILE *css = fopen(optarg, "r"); | |
918 | if (!css) { | |
919 | fatal("Cannot read style sheet \"%s\"", optarg); | |
920 | } else { | |
921 | check(cssStyleSheet= realloc(cssStyleSheet, strlen(cssStyleSheet) + | |
922 | st.st_size + 2)); | |
923 | char *newData = strrchr(cssStyleSheet, '\000'); | |
924 | *newData++ = '\n'; | |
925 | if (fread(newData, 1, st.st_size, css) != st.st_size) { | |
926 | fatal("Failed to read style sheet \"%s\"", optarg); | |
927 | } | |
928 | newData[st.st_size]= '\000'; | |
929 | fclose(css); | |
930 | } | |
d1edcc0e MG |
931 | } else if (!idx--) { |
932 | // CGI | |
933 | if (demonize) { | |
934 | fatal("CGI and background operations are mutually exclusive"); | |
935 | } | |
936 | if (port) { | |
937 | fatal("Cannot specify a port for CGI operation"); | |
938 | } | |
939 | cgi = 1; | |
1f771613 | 940 | if (optarg && *optarg) { |
d1edcc0e MG |
941 | char *ptr = strchr(optarg, '-'); |
942 | if (!ptr) { | |
943 | fatal("Syntax error in port range specification"); | |
944 | } | |
945 | *ptr = '\000'; | |
946 | portMin = strtoint(optarg, 1, 65535); | |
947 | *ptr = '-'; | |
948 | portMax = strtoint(ptr + 1, portMin, 65535); | |
949 | } | |
7460295f MG |
950 | } else if (!idx--) { |
951 | // Debug | |
952 | if (!logIsDefault() && !logIsDebug()) { | |
953 | fatal("--debug is mutually exclusive with --quiet and --verbose."); | |
954 | } | |
955 | verbosity = MSG_DEBUG; | |
956 | logSetLogLevel(verbosity); | |
957 | } else if (!idx--) { | |
958 | // Static file | |
959 | char *ptr, *path, *file; | |
960 | if ((ptr = strchr(optarg, ':')) == NULL) { | |
961 | fatal("Syntax error in static-file definition \"%s\".", optarg); | |
962 | } | |
963 | check(path = malloc(ptr - optarg + 1)); | |
964 | memcpy(path, optarg, ptr - optarg); | |
965 | path[ptr - optarg] = '\000'; | |
966 | file = strdup(ptr + 1); | |
967 | if (getRefFromHashMap(externalFiles, path)) { | |
968 | fatal("Duplicate static-file definition for \"%s\".", path); | |
969 | } | |
970 | addToHashMap(externalFiles, path, file); | |
971 | } else if (!idx--) { | |
972 | // Group | |
973 | if (runAsGroup >= 0) { | |
974 | fatal("Duplicate --group option."); | |
975 | } | |
1f771613 MG |
976 | if (!optarg || !*optarg) { |
977 | fatal("\"--group\" expects a group name."); | |
978 | } | |
7460295f | 979 | runAsGroup = parseGroup(optarg, NULL); |
f0c6fd39 MG |
980 | } else if (!idx--) { |
981 | // Linkify | |
982 | if (!strcmp(optarg, "none")) { | |
983 | linkifyURLs = 0; | |
984 | } else if (!strcmp(optarg, "normal")) { | |
985 | linkifyURLs = 1; | |
986 | } else if (!strcmp(optarg, "aggressive")) { | |
987 | linkifyURLs = 2; | |
988 | } else { | |
989 | fatal("Invalid argument for --linkify. Must be " | |
990 | "\"none\", \"normal\", or \"aggressive\"."); | |
991 | } | |
53b27f78 MG |
992 | } else if (!idx--) { |
993 | // Localhost Only | |
994 | localhostOnly = 1; | |
46f2036a MG |
995 | } else if (!idx--) { |
996 | // No Beep | |
997 | noBeep = 1; | |
7460295f MG |
998 | } else if (!idx--) { |
999 | // Numeric | |
1000 | numericHosts = 1; | |
1001 | } else if (!idx--) { | |
1002 | // Port | |
d1edcc0e MG |
1003 | if (port) { |
1004 | fatal("Duplicate --port option"); | |
1005 | } | |
1006 | if (cgi) { | |
1007 | fatal("Cannot specifiy a port for CGI operation"); | |
1008 | } | |
1f771613 MG |
1009 | if (!optarg || *optarg < '0' || *optarg > '9') { |
1010 | fatal("\"--port\" expects a port number."); | |
1011 | } | |
7460295f MG |
1012 | port = strtoint(optarg, 1, 65535); |
1013 | } else if (!idx--) { | |
1014 | // Service | |
1015 | struct Service *service; | |
1016 | service = newService(optarg); | |
d1edcc0e | 1017 | if (getRefFromHashMap(serviceTable, service->path)) { |
7460295f MG |
1018 | fatal("Duplicate service description for \"%s\".", service->path); |
1019 | } | |
d1edcc0e | 1020 | addToHashMap(serviceTable, service->path, (char *)service); |
7460295f MG |
1021 | } else if (!idx--) { |
1022 | // Disable SSL | |
1023 | if (!hasSSL) { | |
1024 | warn("Ignoring disable-ssl option, as SSL support is unavailable"); | |
1025 | } | |
1026 | enableSSL = 0; | |
b624088c MG |
1027 | } else if (!idx--) { |
1028 | // Disable SSL Menu | |
1029 | if (!hasSSL) { | |
1030 | warn("Ignoring disable-ssl-menu option, as SSL support is " | |
1031 | "unavailable"); | |
1032 | } | |
1033 | enableSSLMenu = 0; | |
7460295f MG |
1034 | } else if (!idx--) { |
1035 | // Quiet | |
1036 | if (!logIsDefault() && !logIsQuiet()) { | |
1037 | fatal("--quiet is mutually exclusive with --debug and --verbose."); | |
1038 | } | |
1039 | verbosity = MSG_QUIET; | |
1040 | logSetLogLevel(verbosity); | |
1041 | } else if (!idx--) { | |
1042 | // User | |
1043 | if (runAsUser >= 0) { | |
1044 | fatal("Duplicate --user option."); | |
1045 | } | |
1f771613 MG |
1046 | if (!optarg || !*optarg) { |
1047 | fatal("\"--user\" expects a user name."); | |
1048 | } | |
7460295f | 1049 | runAsUser = parseUser(optarg, NULL); |
ecbff9b9 MG |
1050 | } else if (!idx--) { |
1051 | // User CSS | |
1052 | if (!optarg || !*optarg) { | |
1053 | fatal("\"--user-css\" expects a list of styles sheets and labels"); | |
1054 | } | |
1055 | parseUserCSS(&userCSSList, optarg); | |
7460295f MG |
1056 | } else if (!idx--) { |
1057 | // Verbose | |
1058 | if (!logIsDefault() && (!logIsInfo() || logIsDebug())) { | |
1059 | fatal("--verbose is mutually exclusive with --debug and --quiet"); | |
1060 | } | |
1061 | verbosity = MSG_INFO; | |
1062 | logSetLogLevel(verbosity); | |
1063 | } else if (!idx--) { | |
1064 | // Version | |
fe338aa8 | 1065 | message("ShellInABox version " VERSION " (revision " VCS_REVISION ")"); |
7460295f MG |
1066 | exit(0); |
1067 | } | |
1068 | } | |
1069 | if (optind != argc) { | |
1070 | usage(); | |
56d6406f | 1071 | fatal("Failed to parse command line"); |
7460295f MG |
1072 | } |
1073 | char *buf = NULL; | |
1074 | check(argc >= 1); | |
1075 | for (int i = 0; i < argc; i++) { | |
1076 | buf = stringPrintf(buf, " %s", argv[i]); | |
1077 | } | |
1078 | info("Command line:%s", buf); | |
1079 | free(buf); | |
1080 | ||
d1edcc0e MG |
1081 | // If the user did not specify a port, use the default one |
1082 | if (!cgi && !port) { | |
1083 | port = PORTNUM; | |
1084 | } | |
1085 | ||
7460295f | 1086 | // If the user did not register any services, provide the default service |
d1edcc0e | 1087 | if (!getHashmapSize(serviceTable)) { |
78016c46 MG |
1088 | addToHashMap(serviceTable, "/", (char *)newService(geteuid() ? ":SSH" : |
1089 | ":LOGIN")); | |
d1edcc0e MG |
1090 | } |
1091 | enumerateServices(serviceTable); | |
1092 | deleteHashMap(serviceTable); | |
1093 | ||
1094 | // Do not allow non-root URLs for CGI operation | |
1095 | if (cgi) { | |
1096 | for (int i = 0; i < numServices; i++) { | |
1097 | if (strcmp(services[i]->path, "/")) { | |
1098 | fatal("Non-root service URLs are incompatible with CGI operation"); | |
1099 | } | |
1100 | } | |
3240f75b | 1101 | check(cgiSessionKey = newSessionKey()); |
7460295f | 1102 | } |
7460295f MG |
1103 | |
1104 | if (demonize) { | |
1105 | pid_t pid; | |
1106 | check((pid = fork()) >= 0); | |
1107 | if (pid) { | |
1108 | _exit(0); | |
1109 | } | |
1110 | setsid(); | |
1111 | if (pidfile) { | |
47e62a9c MG |
1112 | #ifndef O_LARGEFILE |
1113 | #define O_LARGEFILE 0 | |
1114 | #endif | |
7460295f MG |
1115 | int fd = NOINTR(open(pidfile, |
1116 | O_WRONLY|O_TRUNC|O_LARGEFILE|O_CREAT, | |
1117 | 0644)); | |
1118 | if (fd >= 0) { | |
1119 | char buf[40]; | |
1120 | NOINTR(write(fd, buf, snprintf(buf, 40, "%d", (int)getpid()))); | |
dc6575f2 | 1121 | check(!NOINTR(close(fd))); |
7460295f MG |
1122 | } |
1123 | } | |
1124 | } | |
1125 | free((char *)pidfile); | |
1126 | } | |
1127 | ||
d1edcc0e MG |
1128 | static void removeLimits() { |
1129 | static int res[] = { RLIMIT_CPU, RLIMIT_DATA, RLIMIT_FSIZE, RLIMIT_NPROC }; | |
1130 | for (int i = 0; i < sizeof(res)/sizeof(int); i++) { | |
1131 | struct rlimit rl; | |
1132 | getrlimit(res[i], &rl); | |
1133 | if (rl.rlim_max < RLIM_INFINITY) { | |
1134 | rl.rlim_max = RLIM_INFINITY; | |
1135 | setrlimit(res[i], &rl); | |
1136 | getrlimit(res[i], &rl); | |
1137 | } | |
1138 | if (rl.rlim_cur < rl.rlim_max) { | |
1139 | rl.rlim_cur = rl.rlim_max; | |
1140 | setrlimit(res[i], &rl); | |
1141 | } | |
1142 | } | |
1143 | } | |
1144 | ||
b624088c MG |
1145 | static void setUpSSL(Server *server) { |
1146 | serverEnableSSL(server, enableSSL); | |
1147 | ||
1148 | // Enable SSL support (if available) | |
1149 | if (enableSSL) { | |
1150 | check(serverSupportsSSL()); | |
1151 | if (certificateFd >= 0) { | |
1152 | serverSetCertificateFd(server, certificateFd); | |
1153 | } else if (certificateDir) { | |
1154 | char *tmp; | |
1155 | if (strchr(certificateDir, '%')) { | |
1156 | fatal("Invalid certificate directory name \"%s\".", certificateDir); | |
1157 | } | |
1158 | check(tmp = stringPrintf(NULL, "%s/certificate%%s.pem", certificateDir)); | |
1159 | serverSetCertificate(server, tmp, 1); | |
1160 | free(tmp); | |
1161 | } else { | |
1162 | serverSetCertificate(server, "certificate%s.pem", 1); | |
1163 | } | |
1164 | } | |
1165 | } | |
1166 | ||
7460295f | 1167 | int main(int argc, char * const argv[]) { |
47e62a9c | 1168 | #ifdef HAVE_SYS_PRCTL_H |
7460295f MG |
1169 | // Disable core files |
1170 | prctl(PR_SET_DUMPABLE, 0, 0, 0, 0); | |
47e62a9c MG |
1171 | #endif |
1172 | struct rlimit rl = { 0 }; | |
1173 | setrlimit(RLIMIT_CORE, &rl); | |
d1edcc0e | 1174 | removeLimits(); |
7460295f MG |
1175 | |
1176 | // Parse command line arguments | |
1177 | parseArgs(argc, argv); | |
1178 | ||
1179 | // Fork the launcher process, allowing us to drop privileges in the main | |
1180 | // process. | |
d1edcc0e | 1181 | int launcherFd = forkLauncher(); |
7460295f MG |
1182 | |
1183 | // Make sure that our timestamps will print in the standard format | |
1184 | setlocale(LC_TIME, "POSIX"); | |
1185 | ||
1186 | // Create a new web server | |
1187 | Server *server; | |
d1edcc0e | 1188 | if (port) { |
53b27f78 | 1189 | check(server = newServer(localhostOnly, port)); |
11cd1451 | 1190 | dropPrivileges(); |
b624088c | 1191 | setUpSSL(server); |
d1edcc0e MG |
1192 | } else { |
1193 | // For CGI operation we fork the new server, so that it runs in the | |
1194 | // background. | |
1195 | pid_t pid; | |
1196 | int fds[2]; | |
11cd1451 | 1197 | dropPrivileges(); |
d1edcc0e MG |
1198 | check(!pipe(fds)); |
1199 | check((pid = fork()) >= 0); | |
1200 | if (pid) { | |
1201 | // Wait for child to output initial HTML page | |
1202 | char wait; | |
1203 | check(!NOINTR(close(fds[1]))); | |
1204 | check(!NOINTR(read(fds[0], &wait, 1))); | |
1205 | check(!NOINTR(close(fds[0]))); | |
1206 | _exit(0); | |
1207 | } | |
1208 | check(!NOINTR(close(fds[0]))); | |
53b27f78 MG |
1209 | check(server = newCGIServer(localhostOnly, portMin, portMax, |
1210 | AJAX_TIMEOUT)); | |
d1edcc0e | 1211 | cgiServer = server; |
b624088c | 1212 | setUpSSL(server); |
d1edcc0e MG |
1213 | |
1214 | // Output a <frameset> that includes our root page | |
1215 | check(port = serverGetListeningPort(server)); | |
1216 | extern char cgiRootStart[]; | |
1217 | extern char cgiRootEnd[]; | |
1218 | char *cgiRoot; | |
1219 | check(cgiRoot = malloc(cgiRootEnd - cgiRootStart + 1)); | |
1220 | memcpy(cgiRoot, cgiRootStart, cgiRootEnd - cgiRootStart); | |
ea9929c4 MG |
1221 | cgiRoot[cgiRootEnd - cgiRootStart] = '\000'; |
1222 | printf("X-ShellInABox-Port: %d\r\n" | |
1223 | "X-ShellInABox-Pid: %d\r\n" | |
1224 | "Content-type: text/html; charset=utf-8\r\n\r\n", | |
fd239d93 | 1225 | port, getpid()); |
9b850878 | 1226 | printfUnchecked(cgiRoot, port, cgiSessionKey); |
d1edcc0e MG |
1227 | fflush(stdout); |
1228 | free(cgiRoot); | |
1229 | check(!NOINTR(close(fds[1]))); | |
b624088c | 1230 | closeAllFds((int []){ launcherFd, serverGetFd(server) }, 2); |
d1edcc0e MG |
1231 | logSetLogLevel(MSG_QUIET); |
1232 | } | |
7460295f MG |
1233 | |
1234 | // Set log file format | |
1235 | serverSetNumericHosts(server, numericHosts); | |
1236 | ||
1237 | // Disable /quit handler | |
1238 | serverRegisterHttpHandler(server, "/quit", NULL, NULL); | |
1239 | ||
1240 | // Register HTTP handler(s) | |
1241 | for (int i = 0; i < numServices; i++) { | |
1242 | serverRegisterHttpHandler(server, services[i]->path, | |
1243 | shellInABoxHttpHandler, services[i]); | |
1244 | } | |
1245 | ||
1246 | // Register handlers for external files | |
1247 | iterateOverHashMap(externalFiles, registerExternalFiles, server); | |
1248 | ||
1249 | // Start the server | |
1250 | serverLoop(server); | |
1251 | ||
1252 | // Clean up | |
1253 | deleteServer(server); | |
1254 | finishAllSessions(); | |
1255 | deleteHashMap(externalFiles); | |
1256 | for (int i = 0; i < numServices; i++) { | |
1257 | deleteService(services[i]); | |
1258 | } | |
1259 | free(services); | |
1260 | free(certificateDir); | |
3240f75b | 1261 | free(cgiSessionKey); |
7460295f | 1262 | info("Done"); |
d1edcc0e | 1263 | _exit(0); |
7460295f | 1264 | } |