- Tightened setuid operation to not allow any user or group changes.

- Added support for --cgi mode.


git-svn-id: https://shellinabox.googlecode.com/svn/trunk@22 0da03de8-d603-11dd-86c2-0f8696b7b6f9
This commit is contained in:
zodiac 2009-01-01 05:53:04 +00:00
parent 8f2d101f6d
commit 35aa1a7881
19 changed files with 468 additions and 84 deletions

View file

@ -62,6 +62,7 @@ shellinaboxd_SOURCES = shellinabox/shellinaboxd.c \
shellinabox/service.h \ shellinabox/service.h \
shellinabox/session.c \ shellinabox/session.c \
shellinabox/session.h \ shellinabox/session.h \
shellinabox/cgi_root.html \
shellinabox/root_page.html \ shellinabox/root_page.html \
shellinabox/vt100.js \ shellinabox/vt100.js \
shellinabox/shell_in_a_box.js \ shellinabox/shell_in_a_box.js \

View file

@ -73,7 +73,8 @@ am__dirstamp = $(am__leading_dot)dirstamp
am_shellinaboxd_OBJECTS = shellinaboxd.$(OBJEXT) \ am_shellinaboxd_OBJECTS = shellinaboxd.$(OBJEXT) \
externalfile.$(OBJEXT) launcher.$(OBJEXT) privileges.$(OBJEXT) \ externalfile.$(OBJEXT) launcher.$(OBJEXT) privileges.$(OBJEXT) \
service.$(OBJEXT) session.$(OBJEXT) \ service.$(OBJEXT) session.$(OBJEXT) \
shellinabox/root_page.$(OBJEXT) shellinabox/vt100.$(OBJEXT) \ shellinabox/cgi_root.$(OBJEXT) shellinabox/root_page.$(OBJEXT) \
shellinabox/vt100.$(OBJEXT) \
shellinabox/shell_in_a_box.$(OBJEXT) \ shellinabox/shell_in_a_box.$(OBJEXT) \
shellinabox/styles.$(OBJEXT) shellinabox/favicon.$(OBJEXT) \ shellinabox/styles.$(OBJEXT) shellinabox/favicon.$(OBJEXT) \
shellinabox/beep.$(OBJEXT) shellinabox/beep.$(OBJEXT)
@ -299,6 +300,7 @@ shellinaboxd_SOURCES = shellinabox/shellinaboxd.c \
shellinabox/service.h \ shellinabox/service.h \
shellinabox/session.c \ shellinabox/session.c \
shellinabox/session.h \ shellinabox/session.h \
shellinabox/cgi_root.html \
shellinabox/root_page.html \ shellinabox/root_page.html \
shellinabox/vt100.js \ shellinabox/vt100.js \
shellinabox/shell_in_a_box.js \ shellinabox/shell_in_a_box.js \
@ -394,6 +396,8 @@ shellinabox/$(am__dirstamp):
shellinabox/$(DEPDIR)/$(am__dirstamp): shellinabox/$(DEPDIR)/$(am__dirstamp):
@$(MKDIR_P) shellinabox/$(DEPDIR) @$(MKDIR_P) shellinabox/$(DEPDIR)
@: > shellinabox/$(DEPDIR)/$(am__dirstamp) @: > shellinabox/$(DEPDIR)/$(am__dirstamp)
shellinabox/cgi_root.$(OBJEXT): shellinabox/$(am__dirstamp) \
shellinabox/$(DEPDIR)/$(am__dirstamp)
shellinabox/root_page.$(OBJEXT): shellinabox/$(am__dirstamp) \ shellinabox/root_page.$(OBJEXT): shellinabox/$(am__dirstamp) \
shellinabox/$(DEPDIR)/$(am__dirstamp) shellinabox/$(DEPDIR)/$(am__dirstamp)
shellinabox/vt100.$(OBJEXT): shellinabox/$(am__dirstamp) \ shellinabox/vt100.$(OBJEXT): shellinabox/$(am__dirstamp) \
@ -413,6 +417,7 @@ shellinaboxd$(EXEEXT): $(shellinaboxd_OBJECTS) $(shellinaboxd_DEPENDENCIES)
mostlyclean-compile: mostlyclean-compile:
-rm -f *.$(OBJEXT) -rm -f *.$(OBJEXT)
-rm -f shellinabox/beep.$(OBJEXT) -rm -f shellinabox/beep.$(OBJEXT)
-rm -f shellinabox/cgi_root.$(OBJEXT)
-rm -f shellinabox/favicon.$(OBJEXT) -rm -f shellinabox/favicon.$(OBJEXT)
-rm -f shellinabox/root_page.$(OBJEXT) -rm -f shellinabox/root_page.$(OBJEXT)
-rm -f shellinabox/shell_in_a_box.$(OBJEXT) -rm -f shellinabox/shell_in_a_box.$(OBJEXT)

View file

@ -64,8 +64,11 @@ typedef struct ServerConnection ServerConnection;
typedef struct Server Server; typedef struct Server Server;
typedef struct URL URL; typedef struct URL URL;
Server *newCGIServer(int portMin, int portMax, int timeout);
Server *newServer(int port); Server *newServer(int port);
void deleteServer(Server *server); void deleteServer(Server *server);
int serverGetListeningPort(Server *server);
int serverGetFd(Server *server);
void serverRegisterHttpHandler(Server *server, const char *url, void serverRegisterHttpHandler(Server *server, const char *url,
int (*handler)(HttpConnection *, void *, int (*handler)(HttpConnection *, void *,
const char *, int), void *arg); const char *, int), void *arg);

View file

@ -1,5 +1,8 @@
newCGIServer
newServer newServer
deleteServer deleteServer
serverGetListeningPort
serverGetFd
serverRegisterHttpHandler serverRegisterHttpHandler
serverRegisterStreamingHttpHandler serverRegisterStreamingHttpHandler
serverAddConnection serverAddConnection

View file

@ -49,6 +49,7 @@
#include <string.h> #include <string.h>
#include <sys/poll.h> #include <sys/poll.h>
#include <sys/socket.h> #include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h> #include <sys/types.h>
#include <unistd.h> #include <unistd.h>
@ -166,6 +167,58 @@ static int serverQuitHandler(struct HttpConnection *http, void *arg) {
return HTTP_DONE; return HTTP_DONE;
} }
struct Server *newCGIServer(int portMin, int portMax, int timeout) {
struct Server *server = newServer(0);
server->serverTimeout = timeout;
int true = 1;
server->serverFd = socket(PF_INET, SOCK_STREAM, 0);
check(server->serverFd >= 0);
check(!setsockopt(server->serverFd, SOL_SOCKET, SO_REUSEADDR,
&true, sizeof(true)));
struct sockaddr_in serverAddr = { 0 };
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = INADDR_ANY;
// Linux unlike BSD does not have support for picking a local port range.
// So, we have to randomly pick a port from our allowed port range, and then
// keep iterating until we find an unused port.
if (portMin || portMax) {
struct timeval tv;
check(!gettimeofday(&tv, NULL));
srand((int)(tv.tv_usec ^ tv.tv_sec));
check(portMin > 0);
check(portMax < 65536);
check(portMax >= portMin);
int portStart = rand() % (portMax - portMin + 1) + portMin;
for (int p = 0; p <= portMax-portMin; p++) {
int port = (p+portStart)%(portMax-portMin+1)+ portMin;
serverAddr.sin_port = htons(port);
if (!bind(server->serverFd, (struct sockaddr *)&serverAddr,
sizeof(serverAddr))) {
break;
}
serverAddr.sin_port = 0;
}
if (!serverAddr.sin_port) {
fatal("Failed to find any available port");
}
}
check(!listen(server->serverFd, SOMAXCONN));
socklen_t socklen = (socklen_t)sizeof(serverAddr);
check(!getsockname(server->serverFd, (struct sockaddr *)&serverAddr,
&socklen));
check(socklen == sizeof(serverAddr));
server->port = ntohs(serverAddr.sin_port);
info("Listening on port %d", server->port);
check(server->pollFds = malloc(sizeof(struct pollfd)));
server->pollFds->fd = server->serverFd;
server->pollFds->events = POLLIN;
return server;
}
struct Server *newServer(int port) { struct Server *newServer(int port) {
struct Server *server; struct Server *server;
check(server = malloc(sizeof(struct Server))); check(server = malloc(sizeof(struct Server)));
@ -177,6 +230,7 @@ void initServer(struct Server *server, int port) {
server->port = port; server->port = port;
server->looping = 0; server->looping = 0;
server->exitAll = 0; server->exitAll = 0;
server->serverTimeout = -1;
server->serverFd = -1; server->serverFd = -1;
server->numericHosts = 0; server->numericHosts = 0;
server->pollFds = NULL; server->pollFds = NULL;
@ -208,6 +262,14 @@ void deleteServer(struct Server *server) {
free(server); free(server);
} }
int serverGetListeningPort(struct Server *server) {
return server->port;
}
int serverGetFd(struct Server *server) {
return server->serverFd;
}
struct ServerConnection *serverAddConnection(struct Server *server, int fd, struct ServerConnection *serverAddConnection(struct Server *server, int fd,
int (*handleConnection)(struct ServerConnection *c, int (*handleConnection)(struct ServerConnection *c,
void *arg, short *events, void *arg, short *events,
@ -363,6 +425,14 @@ void serverLoop(struct Server *server) {
} }
} }
// serverTimeout is always a delta value, unlike connection timeouts
// which are absolute times.
if (server->serverTimeout >= 0) {
if (timeout < 0 || timeout > server->serverTimeout + currentTime) {
timeout = server->serverTimeout+currentTime;
}
}
if (timeout >= 0) { if (timeout >= 0) {
// Wait at least one second longer than needed, so that even if // Wait at least one second longer than needed, so that even if
// poll() decides to return a second early (due to possible rounding // poll() decides to return a second early (due to possible rounding
@ -406,6 +476,12 @@ void serverLoop(struct Server *server) {
INITIAL_TIMEOUT); INITIAL_TIMEOUT);
} }
} }
} else {
if (server->serverTimeout > 0 && !server->numConnections) {
// In CGI mode, exit the server, if we haven't had any active
// connections in a while.
break;
}
} }
for (int i = 1; for (int i = 1;
(isTimeout || eventCount > 0) && i <= server->numConnections; (isTimeout || eventCount > 0) && i <= server->numConnections;

View file

@ -68,6 +68,7 @@ struct Server {
int port; int port;
int looping; int looping;
int exitAll; int exitAll;
int serverTimeout;
int serverFd; int serverFd;
int numericHosts; int numericHosts;
struct pollfd *pollFds; struct pollfd *pollFds;
@ -77,10 +78,13 @@ struct Server {
struct SSLSupport ssl; struct SSLSupport ssl;
}; };
struct Server *newCGIServer(int portMin, int portMax, int timeout);
struct Server *newServer(int port); struct Server *newServer(int port);
void initServer(struct Server *server, int port); void initServer(struct Server *server, int port);
void destroyServer(struct Server *server); void destroyServer(struct Server *server);
void deleteServer(struct Server *server); void deleteServer(struct Server *server);
int serverGetListeningPort(struct Server *server);
int serverGetFd(struct Server *server);
void serverRegisterHttpHandler(struct Server *server, const char *url, void serverRegisterHttpHandler(struct Server *server, const char *url,
int (*handler)(struct HttpConnection *, void *, int (*handler)(struct HttpConnection *, void *,
const char *, int), void *arg); const char *, int), void *arg);

View file

@ -74,7 +74,8 @@ void info(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
void warn(const char *fmt, ...) __attribute__((format(printf, 1, 2))); void warn(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
void error(const char *fmt, ...) __attribute__((format(printf, 1, 2))); void error(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
void message(const char *fmt, ...) __attribute__((format(printf, 1, 2))); void message(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
void fatal(const char *fmt, ...) __attribute__((format(printf, 1, 2))); void fatal(const char *fmt, ...) __attribute__((format(printf, 1, 2),
noreturn));
int logIsDebug(void); int logIsDebug(void);
int logIsInfo(void); int logIsInfo(void);
int logIsWarn(void); int logIsWarn(void);

69
shellinabox/cgi_root.html Normal file
View file

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xml:lang="en" lang="en">
<head>
<!--
ShellInABox - Make command line applications available as AJAX web applications
Copyright (C) 2008 Markus Gutschke markus@shellinabox.com
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2 as
published by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
In addition to these license terms, the author grants the following
additional rights:
If you modify this program, or any covered work, by linking or
combining it with the OpenSSL project's OpenSSL library (or a
modified version of that library), containing parts covered by the
terms of the OpenSSL or SSLeay licenses, the author
grants you additional permission to convey the resulting work.
Corresponding Source for a non-source form of such a combination
shall include the source code for the parts of OpenSSL used as well
as that of the covered work.
You may at your option choose to remove this additional permission from
the work, or from any part of it.
It is possible to build this program in a way that it loads OpenSSL
libraries at run-time. If doing so, the following notices are required
by the OpenSSL and SSLeay licenses:
This product includes software developed by the OpenSSL Project
for use in the OpenSSL Toolkit. (http://www.openssl.org/)
This product includes cryptographic software written by Eric Young
(eay@cryptsoft.com)
The most up-to-date version of this program is always available from
http://shellinabox.com
-->
<title>Shell In A Box</title>
<script type="text/javascript"><!--
(function() {
var url = document.location.protocol + '//' +
document.location.hostname + ':%d/';
var head = document.getElementsByTagName('head')[0];
var link = document.createElement('link');
link.rel = 'shortcut icon';
link.type = 'image/x-icon';
link.href = url + 'favicon.ico';
head.appendChild(link);
document.write('<frameset cols="*">\n' +
'<frame src="' + url +
document.location.search.replace(/^\?/, '') + '#' +
encodeURIComponent(document.location.href) + '">\n' +
'</frameset>');
})();
--></script>
</head>
</html>

View file

@ -103,6 +103,7 @@ static int (*x_misc_conv)(int, const struct pam_message **,
extern DIR *fdopendir(int) __attribute__((weak)); extern DIR *fdopendir(int) __attribute__((weak));
static int launcher = -1; static int launcher = -1;
static uid_t restricted;
static void *loadSymbol(const char *lib, const char *fn) { static void *loadSymbol(const char *lib, const char *fn) {
@ -288,6 +289,60 @@ static void destroyUtmpHashEntry(void *arg, char *key, char *value) {
deleteUtmp((struct Utmp *)value); deleteUtmp((struct Utmp *)value);
} }
void closeAllFds(int *exceptFds, int num) {
// Close all file handles. If possible, scan through "/proc/self/fd" as
// that is faster than calling close() on all possible file handles.
int nullFd = open("/dev/null", O_RDWR);
int dirFd = !&fdopendir ? -1 : open("/proc/self/fd", O_RDONLY);
if (dirFd < 0) {
for (int i = sysconf(_SC_OPEN_MAX); --i > 0; ) {
if (i != nullFd) {
for (int j = 0; j < num; j++) {
if (i == exceptFds[j]) {
goto no_close_1;
}
}
// Closing handles 0..2 is never a good idea. Instead, redirect them
// to /dev/null
if (i <= 2) {
NOINTR(dup2(nullFd, i));
} else {
NOINTR(close(i));
}
}
no_close_1:;
}
} else {
DIR *dir;
check(dir = fdopendir(dirFd));
struct dirent de, *res;
while (!readdir_r(dir, &de, &res) && res) {
if (res->d_name[0] < '0')
continue;
int fd = atoi(res->d_name);
if (fd != nullFd && fd != dirFd) {
for (int j = 0; j < num; j++) {
if (fd == exceptFds[j]) {
goto no_close_2;
}
}
// Closing handles 0..2 is never a good idea. Instead, redirect them
// to /dev/null
if (fd <= 2) {
NOINTR(dup2(nullFd, fd));
} else {
NOINTR(close(fd));
}
}
no_close_2:;
}
check(!closedir(dir));
}
if (nullFd > 2) {
check(!close(nullFd));
}
}
static int forkPty(int *pty, int useLogin, struct Utmp **utmp, static int forkPty(int *pty, int useLogin, struct Utmp **utmp,
const char *peerName) { const char *peerName) {
int slave; int slave;
@ -323,27 +378,7 @@ static int forkPty(int *pty, int useLogin, struct Utmp **utmp,
(*utmp)->utmpx.ut_pid = pid; (*utmp)->utmpx.ut_pid = pid;
(*utmp)->pty = slave; (*utmp)->pty = slave;
// Close all file handles. If possible, scan through "/proc/self/fd" as closeAllFds((int []){ slave }, 1);
// that is faster than calling close() on all possible file handles.
int dirFd = !&fdopendir ? -1 : open("/proc/self/fd", O_RDONLY);
if (dirFd < 0) {
for (int i = sysconf(_SC_OPEN_MAX); --i > 0; ) {
if (i != slave) {
NOINTR(close(i));
}
}
} else {
DIR *dir;
check(dir = fdopendir(dirFd));
struct dirent de, *res;
while (!readdir_r(dir, &de, &res) && res) {
int fd = atoi(res->d_name);
if (fd != slave && fd != dirFd) {
NOINTR(close(fd));
}
}
check(!closedir(dir));
}
// Become the session/process-group leader // Become the session/process-group leader
setsid(); setsid();
@ -958,7 +993,7 @@ static void launcherDaemon(int fd) {
_exit(0); _exit(0);
} }
void forkLauncher(void) { int forkLauncher(void) {
int pair[2]; int pair[2];
check(!socketpair(AF_UNIX, SOCK_STREAM, 0, pair)); check(!socketpair(AF_UNIX, SOCK_STREAM, 0, pair));
@ -973,14 +1008,14 @@ void forkLauncher(void) {
// switch back to root, which is necessary for launching "login". // switch back to root, which is necessary for launching "login".
lowerPrivileges(); lowerPrivileges();
NOINTR(close(pair[0])); NOINTR(close(pair[0]));
closeAllFds((int []){ pair[1] }, 1);
launcherDaemon(pair[1]); launcherDaemon(pair[1]);
fatal("exit() failed!"); fatal("exit() failed!");
case -1: case -1:
fatal("fork() failed!"); fatal("fork() failed!");
break;
default: default:
NOINTR(close(pair[1])); NOINTR(close(pair[1]));
launcher = pair[0]; launcher = pair[0];
return; return launcher;
} }
} }

View file

@ -60,6 +60,7 @@ struct LaunchRequest {
int supportsPAM(void); int supportsPAM(void);
int launchChild(int service, struct Session *session); int launchChild(int service, struct Session *session);
void setWindowSize(int pty, int width, int height); void setWindowSize(int pty, int width, int height);
void forkLauncher(void); int forkLauncher(void);
void closeAllFds(int *exceptFd, int num);
#endif #endif

View file

@ -60,40 +60,48 @@
int runAsUser = -1; int runAsUser = -1;
int runAsGroup = -1; int runAsGroup = -1;
uid_t restricted;
void removeGroupPrivileges(void) { static void removeGroupPrivileges(int showError) {
gid_t rg, eg, sg;
check(!getresgid(&rg, &eg, &sg));
// Remove all supplementary groups. Allow this command to fail. That could // Remove all supplementary groups. Allow this command to fail. That could
// happen if we run as an unprivileged user. // happen if we run as an unprivileged user.
setgroups(0, (gid_t *)""); setgroups(0, (gid_t *)"");
if (runAsGroup >= 0) { if (runAsGroup >= 0) {
uid_t ru, eu, su;
getresuid(&ru, &eu, &su);
// Try to switch the user-provided group. // Try to switch the user-provided group.
if (setresgid(runAsGroup, runAsGroup, runAsGroup)) { if ((ru && runAsGroup != rg) ||
if (restricted) { setresgid(runAsGroup, runAsGroup, runAsGroup)) {
_exit(1); if (showError) {
} else {
fatal("Only privileged users can change their group memberships"); fatal("Only privileged users can change their group memberships");
} else {
_exit(1);
} }
} }
} else { } else {
gid_t r, e, s; if (rg) {
check(!getresgid(&r, &e, &s));
if (r) {
// If we were started as a set-gid binary, drop these permissions, now. // If we were started as a set-gid binary, drop these permissions, now.
check(!setresgid(r, r, r)); check(!setresgid(rg, rg, rg));
} else { } else {
// If we are running as root, switch to "nogroup" // If we are running as root, switch to "nogroup"
gid_t n = getGroupId("nogroup"); gid_t ng = getGroupId("nogroup");
check(!setresgid(n, n, n)); check(!setresgid(ng, ng, ng));
} }
} }
} }
void lowerPrivileges(void) { void lowerPrivileges(void) {
uid_t r, e, g;
check(!getresuid(&r, &e, &g));
// Permanently lower all group permissions. We do not actually need these, // Permanently lower all group permissions. We do not actually need these,
// as we still have "root" user privileges in our saved-uid. // as we still have "root" user privileges in our saved-uid.
removeGroupPrivileges(); removeGroupPrivileges(0);
// Temporarily lower user privileges. If we used to have "root" privileges, // Temporarily lower user privileges. If we used to have "root" privileges,
// we can later still regain them. // we can later still regain them.
@ -101,10 +109,11 @@ void lowerPrivileges(void) {
if (runAsUser >= 0) { if (runAsUser >= 0) {
// Try to switch to the user-provided user id. // Try to switch to the user-provided user id.
if (r && runAsUser != r) {
fatal("Only privileged users can change their user id");
}
check(!setresuid(runAsUser, runAsUser, -1)); check(!setresuid(runAsUser, runAsUser, -1));
} else { } else {
uid_t r, e, s;
check(!getresuid(&r, &e, &s));
if (r) { if (r) {
// If we were started as a set-uid binary, temporarily lower these // If we were started as a set-uid binary, temporarily lower these
// permissions. // permissions.
@ -118,17 +127,19 @@ void lowerPrivileges(void) {
} }
void dropPrivileges(void) { void dropPrivileges(void) {
uid_t r, e, s;
check(!getresuid(&r, &e, &s));
// Drop all group privileges. // Drop all group privileges.
removeGroupPrivileges(); removeGroupPrivileges(1);
if (runAsUser >= 0) { if (runAsUser >= 0) {
// Try to switch to the user-provided user id. // Try to switch to the user-provided user id.
if (setresuid(runAsUser, runAsUser, runAsUser)) { if ((r && runAsUser != r) ||
setresuid(runAsUser, runAsUser, runAsUser)) {
fatal("Only privileged users can change their user id."); fatal("Only privileged users can change their user id.");
} }
} else { } else {
uid_t r, e, s;
check(!getresuid(&r, &e, &s));
if (r) { if (r) {
// If we were started as a set-uid binary, permanently drop these // If we were started as a set-uid binary, permanently drop these
// permissions. // permissions.

View file

@ -50,9 +50,7 @@
extern int runAsUser; extern int runAsUser;
extern int runAsGroup; extern int runAsGroup;
extern uid_t restricted;
void removeGroupPrivileges(void);
void lowerPrivileges(void); void lowerPrivileges(void);
void dropPrivileges(void); void dropPrivileges(void);
const char *getUserName(uid_t uid); const char *getUserName(uid_t uid);

View file

@ -68,6 +68,11 @@
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon"> <link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
<script type="text/javascript" src="ShellInABox.js"></script> <script type="text/javascript" src="ShellInABox.js"></script>
</head> </head>
<body onload="new ShellInABox()" scroll="no"><noscript>JavaScript <!-- Load ShellInABox from a timer as Konqueror sometimes fails to
correctly deal with the enclosing frameset (if any), if we do not
do this
-->
<body onload="setTimeout('new ShellInABox()', 100)"
scroll="no"><noscript>JavaScript
must be enabled for ShellInABox</noscript></body> must be enabled for ShellInABox</noscript></body>
</html> </html>

View file

@ -218,3 +218,7 @@ struct Session *findSession(int *isNew, HttpConnection *http, URL *url) {
void iterateOverSessions(int (*fnc)(void *, const char *, char **), void *arg){ void iterateOverSessions(int (*fnc)(void *, const char *, char **), void *arg){
iterateOverHashMap(sessions, fnc, arg); iterateOverHashMap(sessions, fnc, arg);
} }
int numSessions(void) {
return getHashmapSize(sessions);
}

View file

@ -77,5 +77,6 @@ void finishSession(struct Session *session);
void finishAllSessions(void); void finishAllSessions(void);
struct Session *findSession(int *isNew, HttpConnection *http, URL *url); struct Session *findSession(int *isNew, HttpConnection *http, URL *url);
void iterateOverSessions(int (*fnc)(void *, const char *, char **), void *arg); void iterateOverSessions(int (*fnc)(void *, const char *, char **), void *arg);
int numSessions(void);
#endif /* SESSION_H__ */ #endif /* SESSION_H__ */

View file

@ -91,11 +91,16 @@ function extend(subClass, baseClass) {
function ShellInABox(url, container) { function ShellInABox(url, container) {
if (url == undefined) { if (url == undefined) {
this.url = document.location.href; this.url = document.location.href.replace(/[?#].*/, '');
} else { } else {
this.url = url; this.url = url;
} }
if (document.location.hash != '') {
this.nextUrl = decodeURIComponent(document.location.hash).
replace(/^#/, '');
} else {
this.nextUrl = this.url; this.nextUrl = this.url;
}
this.session = null; this.session = null;
this.pendingKeys = ''; this.pendingKeys = '';
this.keysInFlight = false; this.keysInFlight = false;
@ -125,6 +130,13 @@ ShellInABox.prototype.sessionClosed = function() {
ShellInABox.prototype.reconnect = function() { ShellInABox.prototype.reconnect = function() {
this.showReconnect(false); this.showReconnect(false);
if (!this.session) { if (!this.session) {
if (document.location.hash != '') {
// A shellinaboxd daemon launched from a CGI only allows a single
// session. In order to reconnect, we must reload the frame definition
// and obtain a new port number. As this is a different origin, we
// need to get enclosing page to help us.
parent.location = this.nextUrl;
} else {
if (this.url != this.nextUrl) { if (this.url != this.nextUrl) {
document.location.replace(this.nextUrl); document.location.replace(this.nextUrl);
} else { } else {
@ -134,6 +146,8 @@ ShellInABox.prototype.reconnect = function() {
this.sendRequest(); this.sendRequest();
} }
} }
}
return false;
}; };
ShellInABox.prototype.sendRequest = function(request) { ShellInABox.prototype.sendRequest = function(request) {
@ -176,6 +190,7 @@ ShellInABox.prototype.onReadyStateChange = function(request) {
} }
} else if (request.status == 0) { } else if (request.status == 0) {
// Time Out // Time Out
this.inspect(request);/***/
this.sendRequest(request); this.sendRequest(request);
} else { } else {
this.sessionClosed(); this.sessionClosed();
@ -262,9 +277,20 @@ ShellInABox.prototype.resized = function(w, h) {
}; };
ShellInABox.prototype.toggleSSL = function() { ShellInABox.prototype.toggleSSL = function() {
if (document.location.hash != '') {
if (this.nextUrl.match(/\?plain$/)) {
this.nextUrl = this.nextUrl.replace(/\?plain$/, '');
} else {
this.nextUrl = this.nextUrl.replace(/[?#].*/, '') + '?plain';
}
if (!this.session) {
parent.location = this.nextUrl;
}
} else {
this.nextUrl = this.nextUrl.match(/^https:/) this.nextUrl = this.nextUrl.match(/^https:/)
? this.nextUrl.replace(/^https:/, 'http:').replace(/\/*$/, '/plain') ? this.nextUrl.replace(/^https:/, 'http:').replace(/\/*$/, '/plain')
: this.nextUrl.replace(/^http/, 'https').replace(/\/*plain$/, ''); : this.nextUrl.replace(/^http/, 'https').replace(/\/*plain$/, '');
}
if (this.nextUrl.match(/^[:]*:\/\/[^/]*$/)) { if (this.nextUrl.match(/^[:]*:\/\/[^/]*$/)) {
this.nextUrl += '/'; this.nextUrl += '/';
} }
@ -290,8 +316,13 @@ ShellInABox.prototype.extendContextMenu = function(entries, actions) {
// If the server supports both SSL and plain text connections, // If the server supports both SSL and plain text connections,
// provide a menu entry to switch between the two. // provide a menu entry to switch between the two.
var newNode = document.createElement('li'); var newNode = document.createElement('li');
newNode.innerHTML = var isSecure;
(this.nextUrl.match(/^https:/) ? '&#10004; ' : '') + 'Secure'; if (document.location.href != '') {
isSecure = !this.nextUrl.match(/\?plain$/);
} else {
isSecure = this.nextUrl.match(/^https:/);
}
newNode.innerHTML = (isSecure ? '&#10004; ' : '') + 'Secure';
if (node.nextSibling) { if (node.nextSibling) {
entries.insertBefore(newNode, node.nextSibling); entries.insertBefore(newNode, node.nextSibling);
} else { } else {

View file

@ -55,6 +55,7 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <sys/prctl.h> #include <sys/prctl.h>
#include <sys/resource.h>
#include <sys/types.h> #include <sys/types.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <unistd.h> #include <unistd.h>
@ -70,11 +71,14 @@
#define PORTNUM 4200 #define PORTNUM 4200
#define MAX_RESPONSE 2048 #define MAX_RESPONSE 2048
static int port = PORTNUM; static int port;
static int portMin;
static int portMax;
static int numericHosts = 0; static int numericHosts = 0;
static int enableSSL = 1; static int enableSSL = 1;
static char *certificateDir; static char *certificateDir;
static HashMap *externalFiles; static HashMap *externalFiles;
static Server *cgiServer;
static char *jsonEscape(const char *buf, int len) { static char *jsonEscape(const char *buf, int len) {
@ -288,6 +292,10 @@ static int dataHandler(HttpConnection *http, struct Service *service,
int isNew; int isNew;
struct Session *session = findSession(&isNew, http, url); struct Session *session = findSession(&isNew, http, url);
if (session == NULL) { if (session == NULL) {
if (cgiServer && numSessions() == 0) {
// CGI servers only ever serve a single session
serverExitLoop(cgiServer, 1);
}
httpSendReply(http, 400, "Bad Request", NULL); httpSendReply(http, 400, "Bad Request", NULL);
return HTTP_DONE; return HTTP_DONE;
} }
@ -324,6 +332,11 @@ static int dataHandler(HttpConnection *http, struct Service *service,
// Create a new session, if the client did not provide an existing one // Create a new session, if the client did not provide an existing one
if (isNew) { if (isNew) {
if (cgiServer && numSessions() > 1) {
deleteSession(session);
httpSendReply(http, 400, "Bad Request", NULL);
return HTTP_DONE;
}
debug("Creating new child process"); debug("Creating new child process");
if (launchChild(service->id, session) < 0) { if (launchChild(service->id, session) < 0) {
deleteSession(session); deleteSession(session);
@ -540,6 +553,7 @@ static void usage(void) {
"List of command line options:\n" "List of command line options:\n"
" -b, --background[=PIDFILE] run in background\n" " -b, --background[=PIDFILE] run in background\n"
"%s" "%s"
" --cgi[=PORTMIN-PORTMAX] run as CGI\n"
" -d, --debug enable debug mode\n" " -d, --debug enable debug mode\n"
" -f, --static-file=URL:FILE serve static file from URL path\n" " -f, --static-file=URL:FILE serve static file from URL path\n"
" -g, --group=GID switch to this group (default: %s)\n" " -g, --group=GID switch to this group (default: %s)\n"
@ -594,16 +608,18 @@ static void parseArgs(int argc, char * const argv[]) {
enableSSL = 0; enableSSL = 0;
} }
int demonize = 0; int demonize = 0;
int cgi = 0;
const char *pidfile = NULL; const char *pidfile = NULL;
int verbosity = MSG_DEFAULT; int verbosity = MSG_DEFAULT;
externalFiles = newHashMap(destroyExternalFileHashEntry, NULL); externalFiles = newHashMap(destroyExternalFileHashEntry, NULL);
HashMap *services = newHashMap(destroyServiceHashEntry, NULL); HashMap *serviceTable = newHashMap(destroyServiceHashEntry, NULL);
for (;;) { for (;;) {
static const char optstring[] = "+hbc:df:g:np:s:tqu:v"; static const char optstring[] = "+hb::c:df:g:np:s:tqu:v";
static struct option options[] = { static struct option options[] = {
{ "help", 0, 0, 'h' }, { "help", 0, 0, 'h' },
{ "background", 2, 0, 'b' }, { "background", 2, 0, 'b' },
{ "cert", 1, 0, 'c' }, { "cert", 1, 0, 'c' },
{ "cgi", 2, 0, 0 },
{ "debug", 0, 0, 'd' }, { "debug", 0, 0, 'd' },
{ "static-file", 1, 0, 'f' }, { "static-file", 1, 0, 'f' },
{ "group", 1, 0, 'g' }, { "group", 1, 0, 'g' },
@ -634,6 +650,9 @@ static void parseArgs(int argc, char * const argv[]) {
exit(idx != -1); exit(idx != -1);
} else if (!idx--) { } else if (!idx--) {
// Background // Background
if (cgi) {
fatal("CGI and background operations are mutually exclusive");
}
demonize = 1; demonize = 1;
if (optarg && pidfile) { if (optarg && pidfile) {
fatal("Only one pidfile can be given"); fatal("Only one pidfile can be given");
@ -650,6 +669,25 @@ static void parseArgs(int argc, char * const argv[]) {
fatal("Only one certificate directory can be selected"); fatal("Only one certificate directory can be selected");
} }
check(certificateDir = strdup(optarg)); check(certificateDir = strdup(optarg));
} else if (!idx--) {
// CGI
if (demonize) {
fatal("CGI and background operations are mutually exclusive");
}
if (port) {
fatal("Cannot specify a port for CGI operation");
}
cgi = 1;
if (optarg) {
char *ptr = strchr(optarg, '-');
if (!ptr) {
fatal("Syntax error in port range specification");
}
*ptr = '\000';
portMin = strtoint(optarg, 1, 65535);
*ptr = '-';
portMax = strtoint(ptr + 1, portMin, 65535);
}
} else if (!idx--) { } else if (!idx--) {
// Debug // Debug
if (!logIsDefault() && !logIsDebug()) { if (!logIsDefault() && !logIsDebug()) {
@ -682,15 +720,21 @@ static void parseArgs(int argc, char * const argv[]) {
numericHosts = 1; numericHosts = 1;
} else if (!idx--) { } else if (!idx--) {
// Port // Port
if (port) {
fatal("Duplicate --port option");
}
if (cgi) {
fatal("Cannot specifiy a port for CGI operation");
}
port = strtoint(optarg, 1, 65535); port = strtoint(optarg, 1, 65535);
} else if (!idx--) { } else if (!idx--) {
// Service // Service
struct Service *service; struct Service *service;
service = newService(optarg); service = newService(optarg);
if (getRefFromHashMap(services, service->path)) { if (getRefFromHashMap(serviceTable, service->path)) {
fatal("Duplicate service description for \"%s\".", service->path); fatal("Duplicate service description for \"%s\".", service->path);
} }
addToHashMap(services, service->path, (char *)service); addToHashMap(serviceTable, service->path, (char *)service);
} else if (!idx--) { } else if (!idx--) {
// Disable SSL // Disable SSL
if (!hasSSL) { if (!hasSSL) {
@ -735,12 +779,26 @@ static void parseArgs(int argc, char * const argv[]) {
info("Command line:%s", buf); info("Command line:%s", buf);
free(buf); free(buf);
// If the user did not register any services, provide the default service // If the user did not specify a port, use the default one
if (!getHashmapSize(services)) { if (!cgi && !port) {
addToHashMap(services, "/", (char *)newService(":LOGIN")); port = PORTNUM;
}
// If the user did not register any services, provide the default service
if (!getHashmapSize(serviceTable)) {
addToHashMap(serviceTable, "/", (char *)newService(":LOGIN"));
}
enumerateServices(serviceTable);
deleteHashMap(serviceTable);
// Do not allow non-root URLs for CGI operation
if (cgi) {
for (int i = 0; i < numServices; i++) {
if (strcmp(services[i]->path, "/")) {
fatal("Non-root service URLs are incompatible with CGI operation");
}
}
} }
enumerateServices(services);
deleteHashMap(services);
if (demonize) { if (demonize) {
pid_t pid; pid_t pid;
@ -763,16 +821,34 @@ static void parseArgs(int argc, char * const argv[]) {
free((char *)pidfile); free((char *)pidfile);
} }
static void removeLimits() {
static int res[] = { RLIMIT_CPU, RLIMIT_DATA, RLIMIT_FSIZE, RLIMIT_NPROC };
for (int i = 0; i < sizeof(res)/sizeof(int); i++) {
struct rlimit rl;
getrlimit(res[i], &rl);
if (rl.rlim_max < RLIM_INFINITY) {
rl.rlim_max = RLIM_INFINITY;
setrlimit(res[i], &rl);
getrlimit(res[i], &rl);
}
if (rl.rlim_cur < rl.rlim_max) {
rl.rlim_cur = rl.rlim_max;
setrlimit(res[i], &rl);
}
}
}
int main(int argc, char * const argv[]) { int main(int argc, char * const argv[]) {
// Disable core files // Disable core files
prctl(PR_SET_DUMPABLE, 0, 0, 0, 0); prctl(PR_SET_DUMPABLE, 0, 0, 0, 0);
removeLimits();
// Parse command line arguments // Parse command line arguments
parseArgs(argc, argv); parseArgs(argc, argv);
// Fork the launcher process, allowing us to drop privileges in the main // Fork the launcher process, allowing us to drop privileges in the main
// process. // process.
forkLauncher(); int launcherFd = forkLauncher();
dropPrivileges(); dropPrivileges();
// Make sure that our timestamps will print in the standard format // Make sure that our timestamps will print in the standard format
@ -780,7 +856,42 @@ int main(int argc, char * const argv[]) {
// Create a new web server // Create a new web server
Server *server; Server *server;
if (port) {
check(server = newServer(port)); check(server = newServer(port));
} else {
// For CGI operation we fork the new server, so that it runs in the
// background.
pid_t pid;
int fds[2];
check(!pipe(fds));
check((pid = fork()) >= 0);
if (pid) {
// Wait for child to output initial HTML page
char wait;
check(!NOINTR(close(fds[1])));
check(!NOINTR(read(fds[0], &wait, 1)));
check(!NOINTR(close(fds[0])));
_exit(0);
}
check(!NOINTR(close(fds[0])));
check(server = newCGIServer(portMin, portMax, AJAX_TIMEOUT));
cgiServer = server;
// Output a <frameset> that includes our root page
check(port = serverGetListeningPort(server));
extern char cgiRootStart[];
extern char cgiRootEnd[];
char *cgiRoot;
check(cgiRoot = malloc(cgiRootEnd - cgiRootStart + 1));
memcpy(cgiRoot, cgiRootStart, cgiRootEnd - cgiRootStart);
puts("Content-type: text/html; charset=utf-8\r\n\r");
printf(cgiRoot, port);
fflush(stdout);
free(cgiRoot);
check(!NOINTR(close(fds[1])));
closeAllFds((int []){ launcherFd, serverGetFd(server) }, 2);
logSetLogLevel(MSG_QUIET);
}
serverEnableSSL(server, enableSSL); serverEnableSSL(server, enableSSL);
// Enable SSL support (if available) // Enable SSL support (if available)
@ -827,5 +938,5 @@ int main(int argc, char * const argv[]) {
free(services); free(services);
free(certificateDir); free(certificateDir);
info("Done"); info("Done");
exit(0); _exit(0);
} }

View file

@ -52,6 +52,7 @@ shellinaboxd \- publish command line shell through AJAX interface
.B shellinaboxd .B shellinaboxd
[\ \fB-b\fP\ | \fB--background\fP[\fB=\fP\fIpidfile\fP]\ ] [\ \fB-b\fP\ | \fB--background\fP[\fB=\fP\fIpidfile\fP]\ ]
[\ \fB-c\fP\ | \fB--cert=\fP\fIcertdir\fP\ ] [\ \fB-c\fP\ | \fB--cert=\fP\fIcertdir\fP\ ]
[\ \fB--cgi\fP[\fB=\fP\fIportrange\fP]\ ]
[\ \fB-d\fP\ | \fB--debug\fP\ ] [\ \fB-d\fP\ | \fB--debug\fP\ ]
[\ \fB-f\fP\ | \fB--static-file=\fP\fIurl\fP:\fIfile\fP\ ] [\ \fB-f\fP\ | \fB--static-file=\fP\fIurl\fP:\fIfile\fP\ ]
[\ \fB-g\fP\ | \fB--group=\fP\fIgid\fP\ ] [\ \fB-g\fP\ | \fB--group=\fP\fIgid\fP\ ]
@ -136,6 +137,32 @@ certificate. Due to this usability problem, and due to the perceived
security implications, the use of auto-generated self-signed security implications, the use of auto-generated self-signed
certificates is intended for testing or in intranet deployments, only. certificates is intended for testing or in intranet deployments, only.
.TP .TP
\fB--cgi\fP[\fB=\fP\fIportrange\fP]
Instead of running
.B shellinaboxd
as a permanent process, it can be demand-loaded as a CGI web server
extension. When doing so, it will spawn a server that lives for the
duration of the user's session. If an optional
.I portrange
of the form
.BR MINPORT-MAXPORT
has been provided, the server limits itself to these port numbers. They
should be configured to pass through the firewall.
The
.B --cgi
option is mutually exclusive with the
.B --background
and
.B --port
options.
In order to be useful as a CGI script, the
.B shellinaboxd
binary probably will have to be made
.BR setuid-root .
This is currently a discouraged configuration. Use with care.
.TP
\fB-d\fP\ |\ \fB--debug\fP \fB-d\fP\ |\ \fB--debug\fP
Enables debugging mode, resulting in lots of log messages on Enables debugging mode, resulting in lots of log messages on
.IR stderr . .IR stderr .
@ -201,13 +228,8 @@ the server drops most privileges at start up. Unless overridden by the
option, it switches to option, it switches to
.BR nogroup . .BR nogroup .
When already running as an unprivileged user, the group remains When already running as an unprivileged user, group changes are not
unchanged, unless a change was explicitly requested with the possible.
.B --group
option. On most UNIX systems, the latter is only possible, if the
binary has been made
.BR setuid-root .
This is a non-standard configuration.
If running with SSL/TLS support enabled, the certificates must be If running with SSL/TLS support enabled, the certificates must be
accessible to the unprivileged user and/or group that the daemon accessible to the unprivileged user and/or group that the daemon

View file

@ -225,7 +225,8 @@ VT100.prototype.initializeElements = function(container) {
} }
this.container.innerHTML = this.container.innerHTML =
'<div id="reconnect" style="visibility: hidden">' + '<div id="reconnect" style="visibility: hidden">' +
'<input type="button" value="Connect" />' + '<input type="button" value="Connect" ' +
'onsubmit="return false" />' +
'</div>' + '</div>' +
'<div id="menu"></div>' + '<div id="menu"></div>' +
'<div id="scrollable">' + '<div id="scrollable">' +
@ -322,8 +323,9 @@ VT100.prototype.initializeElements = function(container) {
this.addListener(this.reconnectBtn.firstChild, 'click', this.addListener(this.reconnectBtn.firstChild, 'click',
function(vt100) { function(vt100) {
return function() { return function() {
vt100.reconnect(); var rc = vt100.reconnect();
vt100.input.focus(); vt100.input.focus();
return rc;
} }
}(this)); }(this));
@ -398,6 +400,7 @@ VT100.prototype.getCurrentComputedStyle = function(elem, style) {
}; };
VT100.prototype.reconnect = function() { VT100.prototype.reconnect = function() {
return false;
}; };
VT100.prototype.showReconnect = function(state) { VT100.prototype.showReconnect = function(state) {