diff --git a/Makefile.am b/Makefile.am index faf310a..43d3198 100644 --- a/Makefile.am +++ b/Makefile.am @@ -62,6 +62,7 @@ shellinaboxd_SOURCES = shellinabox/shellinaboxd.c \ shellinabox/service.h \ shellinabox/session.c \ shellinabox/session.h \ + shellinabox/cgi_root.html \ shellinabox/root_page.html \ shellinabox/vt100.js \ shellinabox/shell_in_a_box.js \ diff --git a/Makefile.in b/Makefile.in index 0185f98..00d9db4 100644 --- a/Makefile.in +++ b/Makefile.in @@ -73,7 +73,8 @@ am__dirstamp = $(am__leading_dot)dirstamp am_shellinaboxd_OBJECTS = shellinaboxd.$(OBJEXT) \ externalfile.$(OBJEXT) launcher.$(OBJEXT) privileges.$(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/styles.$(OBJEXT) shellinabox/favicon.$(OBJEXT) \ shellinabox/beep.$(OBJEXT) @@ -299,6 +300,7 @@ shellinaboxd_SOURCES = shellinabox/shellinaboxd.c \ shellinabox/service.h \ shellinabox/session.c \ shellinabox/session.h \ + shellinabox/cgi_root.html \ shellinabox/root_page.html \ shellinabox/vt100.js \ shellinabox/shell_in_a_box.js \ @@ -394,6 +396,8 @@ shellinabox/$(am__dirstamp): shellinabox/$(DEPDIR)/$(am__dirstamp): @$(MKDIR_P) shellinabox/$(DEPDIR) @: > shellinabox/$(DEPDIR)/$(am__dirstamp) +shellinabox/cgi_root.$(OBJEXT): shellinabox/$(am__dirstamp) \ + shellinabox/$(DEPDIR)/$(am__dirstamp) shellinabox/root_page.$(OBJEXT): shellinabox/$(am__dirstamp) \ shellinabox/$(DEPDIR)/$(am__dirstamp) shellinabox/vt100.$(OBJEXT): shellinabox/$(am__dirstamp) \ @@ -413,6 +417,7 @@ shellinaboxd$(EXEEXT): $(shellinaboxd_OBJECTS) $(shellinaboxd_DEPENDENCIES) mostlyclean-compile: -rm -f *.$(OBJEXT) -rm -f shellinabox/beep.$(OBJEXT) + -rm -f shellinabox/cgi_root.$(OBJEXT) -rm -f shellinabox/favicon.$(OBJEXT) -rm -f shellinabox/root_page.$(OBJEXT) -rm -f shellinabox/shell_in_a_box.$(OBJEXT) diff --git a/libhttp/http.h b/libhttp/http.h index c1eea68..fb5ec65 100644 --- a/libhttp/http.h +++ b/libhttp/http.h @@ -64,8 +64,11 @@ typedef struct ServerConnection ServerConnection; typedef struct Server Server; typedef struct URL URL; +Server *newCGIServer(int portMin, int portMax, int timeout); Server *newServer(int port); void deleteServer(Server *server); +int serverGetListeningPort(Server *server); +int serverGetFd(Server *server); void serverRegisterHttpHandler(Server *server, const char *url, int (*handler)(HttpConnection *, void *, const char *, int), void *arg); diff --git a/libhttp/libhttp.sym b/libhttp/libhttp.sym index 2b0e257..cdc79b4 100644 --- a/libhttp/libhttp.sym +++ b/libhttp/libhttp.sym @@ -1,5 +1,8 @@ +newCGIServer newServer deleteServer +serverGetListeningPort +serverGetFd serverRegisterHttpHandler serverRegisterStreamingHttpHandler serverAddConnection diff --git a/libhttp/server.c b/libhttp/server.c index e94c93c..d8d881e 100644 --- a/libhttp/server.c +++ b/libhttp/server.c @@ -49,6 +49,7 @@ #include #include #include +#include #include #include @@ -166,6 +167,58 @@ static int serverQuitHandler(struct HttpConnection *http, void *arg) { 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 *server; check(server = malloc(sizeof(struct Server))); @@ -177,6 +230,7 @@ void initServer(struct Server *server, int port) { server->port = port; server->looping = 0; server->exitAll = 0; + server->serverTimeout = -1; server->serverFd = -1; server->numericHosts = 0; server->pollFds = NULL; @@ -208,6 +262,14 @@ void deleteServer(struct Server *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, int (*handleConnection)(struct ServerConnection *c, 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) { // Wait at least one second longer than needed, so that even if // poll() decides to return a second early (due to possible rounding @@ -406,6 +476,12 @@ void serverLoop(struct Server *server) { 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; (isTimeout || eventCount > 0) && i <= server->numConnections; diff --git a/libhttp/server.h b/libhttp/server.h index c5ad32f..dfbf6c5 100644 --- a/libhttp/server.h +++ b/libhttp/server.h @@ -68,6 +68,7 @@ struct Server { int port; int looping; int exitAll; + int serverTimeout; int serverFd; int numericHosts; struct pollfd *pollFds; @@ -77,10 +78,13 @@ struct Server { struct SSLSupport ssl; }; +struct Server *newCGIServer(int portMin, int portMax, int timeout); struct Server *newServer(int port); void initServer(struct Server *server, int port); void destroyServer(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, int (*handler)(struct HttpConnection *, void *, const char *, int), void *arg); diff --git a/logging/logging.h b/logging/logging.h index c652b92..916a628 100644 --- a/logging/logging.h +++ b/logging/logging.h @@ -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 error(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 logIsInfo(void); int logIsWarn(void); diff --git a/shellinabox/cgi_root.html b/shellinabox/cgi_root.html new file mode 100644 index 0000000..edac070 --- /dev/null +++ b/shellinabox/cgi_root.html @@ -0,0 +1,69 @@ + + + + + + Shell In A Box + + + diff --git a/shellinabox/launcher.c b/shellinabox/launcher.c index d0d2cf1..ca38d09 100644 --- a/shellinabox/launcher.c +++ b/shellinabox/launcher.c @@ -103,6 +103,7 @@ static int (*x_misc_conv)(int, const struct pam_message **, extern DIR *fdopendir(int) __attribute__((weak)); static int launcher = -1; +static uid_t restricted; 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); } +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, const char *peerName) { int slave; @@ -323,27 +378,7 @@ static int forkPty(int *pty, int useLogin, struct Utmp **utmp, (*utmp)->utmpx.ut_pid = pid; (*utmp)->pty = slave; - // Close all file handles. If possible, scan through "/proc/self/fd" as - // 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)); - } + closeAllFds((int []){ slave }, 1); // Become the session/process-group leader setsid(); @@ -958,7 +993,7 @@ static void launcherDaemon(int fd) { _exit(0); } -void forkLauncher(void) { +int forkLauncher(void) { int pair[2]; 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". lowerPrivileges(); NOINTR(close(pair[0])); + closeAllFds((int []){ pair[1] }, 1); launcherDaemon(pair[1]); fatal("exit() failed!"); case -1: fatal("fork() failed!"); - break; default: NOINTR(close(pair[1])); launcher = pair[0]; - return; + return launcher; } } diff --git a/shellinabox/launcher.h b/shellinabox/launcher.h index 7bf6497..9893715 100644 --- a/shellinabox/launcher.h +++ b/shellinabox/launcher.h @@ -57,9 +57,10 @@ struct LaunchRequest { char peerName[128]; }; -int supportsPAM(void); +int supportsPAM(void); int launchChild(int service, struct Session *session); void setWindowSize(int pty, int width, int height); -void forkLauncher(void); +int forkLauncher(void); +void closeAllFds(int *exceptFd, int num); #endif diff --git a/shellinabox/privileges.c b/shellinabox/privileges.c index f76d2f3..a298b3c 100644 --- a/shellinabox/privileges.c +++ b/shellinabox/privileges.c @@ -60,40 +60,48 @@ int runAsUser = -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 // happen if we run as an unprivileged user. setgroups(0, (gid_t *)""); + if (runAsGroup >= 0) { + uid_t ru, eu, su; + getresuid(&ru, &eu, &su); + // Try to switch the user-provided group. - if (setresgid(runAsGroup, runAsGroup, runAsGroup)) { - if (restricted) { - _exit(1); - } else { + if ((ru && runAsGroup != rg) || + setresgid(runAsGroup, runAsGroup, runAsGroup)) { + if (showError) { fatal("Only privileged users can change their group memberships"); + } else { + _exit(1); } } } else { - gid_t r, e, s; - check(!getresgid(&r, &e, &s)); - if (r) { + if (rg) { // If we were started as a set-gid binary, drop these permissions, now. - check(!setresgid(r, r, r)); + check(!setresgid(rg, rg, rg)); } else { // If we are running as root, switch to "nogroup" - gid_t n = getGroupId("nogroup"); - check(!setresgid(n, n, n)); + gid_t ng = getGroupId("nogroup"); + check(!setresgid(ng, ng, ng)); } } } void lowerPrivileges(void) { + uid_t r, e, g; + check(!getresuid(&r, &e, &g)); + // Permanently lower all group permissions. We do not actually need these, // 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, // we can later still regain them. @@ -101,10 +109,11 @@ void lowerPrivileges(void) { if (runAsUser >= 0) { // 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)); } else { - uid_t r, e, s; - check(!getresuid(&r, &e, &s)); if (r) { // If we were started as a set-uid binary, temporarily lower these // permissions. @@ -118,17 +127,19 @@ void lowerPrivileges(void) { } void dropPrivileges(void) { + uid_t r, e, s; + check(!getresuid(&r, &e, &s)); + // Drop all group privileges. - removeGroupPrivileges(); + removeGroupPrivileges(1); if (runAsUser >= 0) { // 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."); } } else { - uid_t r, e, s; - check(!getresuid(&r, &e, &s)); if (r) { // If we were started as a set-uid binary, permanently drop these // permissions. diff --git a/shellinabox/privileges.h b/shellinabox/privileges.h index 57e4899..1783082 100644 --- a/shellinabox/privileges.h +++ b/shellinabox/privileges.h @@ -50,9 +50,7 @@ extern int runAsUser; extern int runAsGroup; -extern uid_t restricted; -void removeGroupPrivileges(void); void lowerPrivileges(void); void dropPrivileges(void); const char *getUserName(uid_t uid); diff --git a/shellinabox/root_page.html b/shellinabox/root_page.html index bed389b..dd5781e 100644 --- a/shellinabox/root_page.html +++ b/shellinabox/root_page.html @@ -68,6 +68,11 @@ -