c5d55118af
compatibility with systems that implement updwtmp() but not updwtmpx(). Also, changed Makefile rules in an attempt to improve compatibility with BSD make utilities. git-svn-id: https://shellinabox.googlecode.com/svn/trunk@208 0da03de8-d603-11dd-86c2-0f8696b7b6f9
1614 lines
50 KiB
C
1614 lines
50 KiB
C
// launcher.c -- Launch services from a privileged process
|
|
// Copyright (C) 2008-2010 Markus Gutschke <markus@shellinabox.com>
|
|
//
|
|
// This program is free software; you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License version 2 as
|
|
// published by the Free Software Foundation.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License along
|
|
// with this program; if not, write to the Free Software Foundation, Inc.,
|
|
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
//
|
|
// In addition to these license terms, the author grants the following
|
|
// additional rights:
|
|
//
|
|
// If you modify this program, or any covered work, by linking or
|
|
// combining it with the OpenSSL project's OpenSSL library (or a
|
|
// modified version of that library), containing parts covered by the
|
|
// terms of the OpenSSL or SSLeay licenses, the author
|
|
// grants you additional permission to convey the resulting work.
|
|
// Corresponding Source for a non-source form of such a combination
|
|
// shall include the source code for the parts of OpenSSL used as well
|
|
// as that of the covered work.
|
|
//
|
|
// You may at your option choose to remove this additional permission from
|
|
// the work, or from any part of it.
|
|
//
|
|
// It is possible to build this program in a way that it loads OpenSSL
|
|
// libraries at run-time. If doing so, the following notices are required
|
|
// by the OpenSSL and SSLeay licenses:
|
|
//
|
|
// This product includes software developed by the OpenSSL Project
|
|
// for use in the OpenSSL Toolkit. (http://www.openssl.org/)
|
|
//
|
|
// This product includes cryptographic software written by Eric Young
|
|
// (eay@cryptsoft.com)
|
|
//
|
|
//
|
|
// The most up-to-date version of this program is always available from
|
|
// http://shellinabox.com
|
|
|
|
#define _GNU_SOURCE
|
|
#include "config.h"
|
|
|
|
#define pthread_once x_pthread_once
|
|
#define execle x_execle
|
|
|
|
#include <dirent.h>
|
|
#include <dlfcn.h>
|
|
#include <fcntl.h>
|
|
#include <grp.h>
|
|
#include <limits.h>
|
|
#include <pwd.h>
|
|
#include <signal.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/ioctl.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/time.h>
|
|
#include <sys/types.h>
|
|
#include <sys/wait.h>
|
|
#include <sys/utsname.h>
|
|
#include <termios.h>
|
|
#include <unistd.h>
|
|
|
|
#ifdef HAVE_LIBUTIL_H
|
|
#include <libutil.h>
|
|
#endif
|
|
|
|
#ifdef HAVE_PTY_H
|
|
#include <pty.h>
|
|
#endif
|
|
|
|
#ifdef HAVE_SYS_UIO_H
|
|
#include <sys/uio.h>
|
|
#endif
|
|
|
|
#ifdef HAVE_UTIL_H
|
|
#include <util.h>
|
|
#endif
|
|
|
|
#ifdef HAVE_UTMP_H
|
|
#include <utmp.h>
|
|
#endif
|
|
|
|
#ifdef HAVE_UTMPX_H
|
|
#include <utmpx.h>
|
|
#endif
|
|
|
|
#if defined(HAVE_SECURITY_PAM_APPL_H)
|
|
#include <security/pam_appl.h>
|
|
|
|
#if defined(HAVE_SECURITY_PAM_MISC_H)
|
|
#include <security/pam_misc.h>
|
|
#endif
|
|
#else
|
|
struct pam_message;
|
|
struct pam_response;
|
|
struct pam_conv;
|
|
typedef struct pam_handle pam_handle_t;
|
|
#endif
|
|
|
|
#ifdef HAVE_STRLCAT
|
|
#define strncat(a,b,c) ({ char *_a = (a); strlcat(_a, (b), (c)+1); _a; })
|
|
#endif
|
|
|
|
#include "shellinabox/launcher.h"
|
|
#include "shellinabox/privileges.h"
|
|
#include "shellinabox/service.h"
|
|
#include "libhttp/hashmap.h"
|
|
#include "logging/logging.h"
|
|
|
|
#undef pthread_once
|
|
#undef execle
|
|
int execle(const char *, const char *, ...);
|
|
|
|
#if defined(HAVE_PTHREAD_H) && defined(__linux__)
|
|
#include <pthread.h>
|
|
extern int pthread_once(pthread_once_t *, void (*)(void))__attribute__((weak));
|
|
#endif
|
|
|
|
// If PAM support is available, take advantage of it. Otherwise, silently fall
|
|
// back on legacy operations for session management.
|
|
#if defined(HAVE_SECURITY_PAM_APPL_H) && defined(HAVE_DLOPEN)
|
|
static int (*x_pam_acct_mgmt)(pam_handle_t *, int);
|
|
static int (*x_pam_authenticate)(pam_handle_t *, int);
|
|
#if defined(HAVE_SECURITY_PAM_CLIENT_H)
|
|
static int (**x_pam_binary_handler_fn)(void *, pamc_bp_t *);
|
|
#endif
|
|
static int (*x_pam_close_session)(pam_handle_t *, int);
|
|
static int (*x_pam_end)(pam_handle_t *, int);
|
|
static int (*x_pam_get_item)(const pam_handle_t *, int, const void **);
|
|
static int (*x_pam_open_session)(pam_handle_t *, int);
|
|
static int (*x_pam_set_item)(pam_handle_t *, int, const void *);
|
|
static int (*x_pam_start)(const char *, const char *, const struct pam_conv *,
|
|
pam_handle_t **);
|
|
static int (*x_misc_conv)(int, const struct pam_message **,
|
|
struct pam_response **, void *);
|
|
|
|
#define pam_acct_mgmt x_pam_acct_mgmt
|
|
#define pam_authenticate x_pam_authenticate
|
|
#define pam_binary_handler_fn x_pam_binary_handler_fn
|
|
#define pam_close_session x_pam_close_session
|
|
#define pam_end x_pam_end
|
|
#define pam_get_item x_pam_get_item
|
|
#define pam_open_session x_pam_open_session
|
|
#define pam_set_item x_pam_set_item
|
|
#define pam_start x_pam_start
|
|
#define misc_conv x_misc_conv
|
|
#endif
|
|
|
|
static int launcher = -1;
|
|
static uid_t restricted;
|
|
|
|
|
|
// If the PAM misc library cannot be found, we have to provide our own basic
|
|
// conversation function. As we know that this code is only ever called from
|
|
// ShellInABox, it can be kept significantly simpler than the more generic
|
|
// code that the PAM library implements.
|
|
|
|
static int read_string(int echo, const char *prompt, char **retstr) {
|
|
*retstr = NULL;
|
|
struct termios term_before, term_tmp;
|
|
if (tcgetattr(0, &term_before) != 0) {
|
|
return -1;
|
|
}
|
|
memcpy(&term_tmp, &term_before, sizeof(term_tmp));
|
|
if (!echo) {
|
|
term_tmp.c_lflag &= ~ECHO;
|
|
}
|
|
int nc;
|
|
for (;;) {
|
|
tcsetattr(0, TCSAFLUSH, &term_tmp);
|
|
fprintf(stderr, "%s", prompt);
|
|
char *line;
|
|
const int lineLength = 512;
|
|
check(line = calloc(1, lineLength));
|
|
nc = read(0, line, lineLength - 1);
|
|
tcsetattr(0, TCSADRAIN, &term_before);
|
|
if (!echo) {
|
|
fprintf(stderr, "\n");
|
|
}
|
|
if (nc > 0) {
|
|
if (line[nc-1] == '\n') {
|
|
nc--;
|
|
} else if (echo) {
|
|
fprintf(stderr, "\n");
|
|
}
|
|
line[nc] = '\000';
|
|
check(*retstr = line);
|
|
break;
|
|
} else {
|
|
memset(line, 0, lineLength);
|
|
free(line);
|
|
if (echo) {
|
|
fprintf(stderr, "\n");
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
tcsetattr(0, TCSADRAIN, &term_before);
|
|
return nc;
|
|
}
|
|
|
|
#if defined(HAVE_SECURITY_PAM_APPL_H) && defined(HAVE_DLOPEN)
|
|
#if defined(HAVE_SECURITY_PAM_CLIENT_H)
|
|
static pamc_bp_t *p(pamc_bp_t *p) {
|
|
// GCC is too smart for its own good, and triggers a warning in
|
|
// PAM_BP_RENEW, unless we pass the first argument through a function.
|
|
return p;
|
|
}
|
|
#endif
|
|
|
|
static int my_misc_conv(int num_msg, const struct pam_message **msgm,
|
|
struct pam_response **response, void *appdata_ptr) {
|
|
if (num_msg <= 0) {
|
|
return PAM_CONV_ERR;
|
|
}
|
|
struct pam_response *reply;
|
|
check(reply = (struct pam_response *)calloc(num_msg,
|
|
sizeof(struct pam_response)));
|
|
for (int count = 0; count < num_msg; count++) {
|
|
char *string = NULL;
|
|
switch(msgm[count]->msg_style) {
|
|
case PAM_PROMPT_ECHO_OFF:
|
|
if (read_string(0, msgm[count]->msg, &string) < 0) {
|
|
goto failed_conversation;
|
|
}
|
|
break;
|
|
case PAM_PROMPT_ECHO_ON:
|
|
if (read_string(1, msgm[count]->msg, &string) < 0) {
|
|
goto failed_conversation;
|
|
}
|
|
break;
|
|
case PAM_ERROR_MSG:
|
|
if (fprintf(stderr, "%s\n", msgm[count]->msg) < 0) {
|
|
goto failed_conversation;
|
|
}
|
|
break;
|
|
case PAM_TEXT_INFO:
|
|
if (fprintf(stdout, "%s\n", msgm[count]->msg) < 0) {
|
|
goto failed_conversation;
|
|
}
|
|
break;
|
|
#if defined(HAVE_SECURITY_PAM_CLIENT_H)
|
|
case PAM_BINARY_PROMPT: {
|
|
pamc_bp_t binary_prompt = NULL;
|
|
if (!msgm[count]->msg || !*pam_binary_handler_fn) {
|
|
goto failed_conversation;
|
|
}
|
|
PAM_BP_RENEW(p(&binary_prompt), PAM_BP_RCONTROL(msgm[count]->msg),
|
|
PAM_BP_LENGTH(msgm[count]->msg));
|
|
PAM_BP_FILL(binary_prompt, 0, PAM_BP_LENGTH(msgm[count]->msg),
|
|
PAM_BP_RDATA(msgm[count]->msg));
|
|
if ((*pam_binary_handler_fn)(appdata_ptr, &binary_prompt) !=
|
|
PAM_SUCCESS || !binary_prompt) {
|
|
goto failed_conversation;
|
|
}
|
|
string = (char *)binary_prompt;
|
|
break; }
|
|
#endif
|
|
default:
|
|
goto failed_conversation;
|
|
}
|
|
if (string) {
|
|
reply[count].resp_retcode = 0;
|
|
reply[count].resp = string;
|
|
}
|
|
}
|
|
failed_conversation:
|
|
*response = reply;
|
|
return PAM_SUCCESS;
|
|
}
|
|
|
|
static void *loadSymbol(const char *lib, const char *fn) {
|
|
void *dl = RTLD_DEFAULT;
|
|
void *rc = dlsym(dl, fn);
|
|
if (!rc) {
|
|
#ifdef RTLD_NOLOAD
|
|
dl = dlopen(lib, RTLD_LAZY|RTLD_GLOBAL|RTLD_NOLOAD);
|
|
#else
|
|
dl = NULL;
|
|
#endif
|
|
if (dl == NULL) {
|
|
dl = dlopen(lib, RTLD_LAZY|RTLD_GLOBAL);
|
|
}
|
|
if (dl != NULL) {
|
|
rc = dlsym(dl, fn);
|
|
}
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
static void loadPAM(void) {
|
|
check(!pam_start);
|
|
check(!misc_conv);
|
|
struct {
|
|
union {
|
|
void *avoid_gcc_warning_about_type_punning;
|
|
void **var;
|
|
};
|
|
const char *lib;
|
|
const char *fn;
|
|
} symbols[] = {
|
|
{ { &pam_acct_mgmt }, "libpam.so", "pam_acct_mgmt" },
|
|
{ { &pam_authenticate }, "libpam.so", "pam_authenticate" },
|
|
#if defined(HAVE_SECURITY_PAM_CLIENT_H)
|
|
{ { &pam_binary_handler_fn }, "libpam_misc.so", "pam_binary_handler_fn" },
|
|
#endif
|
|
{ { &pam_close_session }, "libpam.so", "pam_close_session" },
|
|
{ { &pam_end }, "libpam.so", "pam_end" },
|
|
{ { &pam_get_item }, "libpam.so", "pam_get_item" },
|
|
{ { &pam_open_session }, "libpam.so", "pam_open_session" },
|
|
{ { &pam_set_item }, "libpam.so", "pam_set_item" },
|
|
{ { &pam_start }, "libpam.so", "pam_start" },
|
|
{ { &misc_conv }, "libpam_misc.so", "misc_conv" }
|
|
};
|
|
for (unsigned i = 0; i < sizeof(symbols)/sizeof(symbols[0]); i++) {
|
|
if (!(*symbols[i].var = loadSymbol(symbols[i].lib, symbols[i].fn))) {
|
|
#if defined(HAVE_SECURITY_PAM_CLIENT_H)
|
|
if (!strcmp(symbols[i].fn, "pam_binary_handler_fn")) {
|
|
// Binary conversation support is optional
|
|
continue;
|
|
} else
|
|
#endif
|
|
if (!strcmp(symbols[i].fn, "misc_conv")) {
|
|
// PAM misc is optional
|
|
*symbols[i].var = (void *)my_misc_conv;
|
|
continue;
|
|
}
|
|
debug("Failed to load PAM support. Could not find \"%s\"",
|
|
symbols[i].fn);
|
|
for (unsigned j = 0; j < sizeof(symbols)/sizeof(symbols[0]); j++) {
|
|
*symbols[j].var = NULL;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
debug("Loaded PAM suppport");
|
|
}
|
|
#endif
|
|
|
|
int supportsPAM(void) {
|
|
#if defined(HAVE_SECURITY_PAM_APPL_H) && !defined(HAVE_DLOPEN)
|
|
return 1;
|
|
#else
|
|
#if defined(HAVE_SECURITY_PAM_APPL_H)
|
|
|
|
// We want to call loadPAM() exactly once. For single-threaded applications,
|
|
// this is straight-forward. For threaded applications, we need to call
|
|
// pthread_once(), instead. We perform run-time checks for whether we are
|
|
// single- or multi-threaded, so that the same code can be used.
|
|
// This currently only works on Linux.
|
|
#if defined(HAVE_PTHREAD_H) && defined(__linux__) && defined(__i386__)
|
|
if (!!&pthread_once) {
|
|
static pthread_once_t once = PTHREAD_ONCE_INIT;
|
|
pthread_once(&once, loadPAM);
|
|
} else
|
|
#endif
|
|
{
|
|
static int initialized;
|
|
if (!initialized) {
|
|
initialized = 1;
|
|
loadPAM();
|
|
}
|
|
}
|
|
return misc_conv && pam_start;
|
|
#else
|
|
return 0;
|
|
#endif
|
|
#endif
|
|
}
|
|
|
|
#ifndef HAVE_GETPWUID_R
|
|
// This is a not-thread-safe replacement for getpwuid_r()
|
|
#define getpwuid_r x_getpwuid_r
|
|
static int getpwuid_r(uid_t uid, struct passwd *pwd, char *buf, size_t buflen,
|
|
struct passwd **result) {
|
|
if (result) {
|
|
*result = NULL;
|
|
}
|
|
if (!pwd) {
|
|
return -1;
|
|
}
|
|
errno = 0;
|
|
struct passwd *p = getpwuid(uid);
|
|
if (!p) {
|
|
return errno ? -1 : 0;
|
|
}
|
|
*pwd = *p;
|
|
if (result) {
|
|
*result = pwd;
|
|
}
|
|
return 0;
|
|
}
|
|
#endif
|
|
|
|
int launchChild(int service, struct Session *session, const char *url) {
|
|
if (launcher < 0) {
|
|
errno = EINVAL;
|
|
return -1;
|
|
}
|
|
|
|
char *u;
|
|
check(u = strdup(url));
|
|
for (int i; u[i = strcspn(u, "\\\"'`${};() \r\n\t\v\f")]; ) {
|
|
static const char hex[] = "0123456789ABCDEF";
|
|
check(u = realloc(u, strlen(u) + 4));
|
|
memmove(u + i + 3, u + i + 1, strlen(u + i));
|
|
u[i + 2] = hex[ u[i] & 0xF];
|
|
u[i + 1] = hex[(u[i] >> 4) & 0xF];
|
|
u[i] = '%';
|
|
}
|
|
|
|
struct LaunchRequest *request;
|
|
ssize_t len = sizeof(struct LaunchRequest) + strlen(u) + 1;
|
|
check(request = calloc(len, 1));
|
|
request->service = service;
|
|
request->width = session->width;
|
|
request->height = session->height;
|
|
strncat(request->peerName, httpGetPeerName(session->http),
|
|
sizeof(request->peerName) - 1);
|
|
request->urlLength = strlen(u);
|
|
memcpy(&request->url, u, request->urlLength);
|
|
free(u);
|
|
if (NOINTR(write(launcher, request, len)) != len) {
|
|
free(request);
|
|
return -1;
|
|
}
|
|
free(request);
|
|
pid_t pid;
|
|
char cmsg_buf[CMSG_SPACE(sizeof(int))];
|
|
struct iovec iov = { 0 };
|
|
struct msghdr msg = { 0 };
|
|
iov.iov_base = &pid;
|
|
iov.iov_len = sizeof(pid);
|
|
msg.msg_iov = &iov;
|
|
msg.msg_iovlen = 1;
|
|
msg.msg_control = &cmsg_buf;
|
|
msg.msg_controllen = sizeof(cmsg_buf);
|
|
int bytes = NOINTR(recvmsg(launcher, &msg, 0));
|
|
if (bytes < 0) {
|
|
return -1;
|
|
}
|
|
check(bytes == sizeof(pid));
|
|
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
|
|
check(cmsg);
|
|
check(cmsg->cmsg_level == SOL_SOCKET);
|
|
check(cmsg->cmsg_type == SCM_RIGHTS);
|
|
memcpy(&session->pty, CMSG_DATA(cmsg), sizeof(int));
|
|
return pid;
|
|
}
|
|
|
|
struct Utmp {
|
|
const char pid[32];
|
|
int pty;
|
|
int useLogin;
|
|
#ifdef HAVE_UTMPX_H
|
|
struct utmpx utmpx;
|
|
#endif
|
|
};
|
|
|
|
static HashMap *childProcesses;
|
|
|
|
void initUtmp(struct Utmp *utmp, int useLogin, const char *ptyPath,
|
|
const char *peerName) {
|
|
memset(utmp, 0, sizeof(struct Utmp));
|
|
utmp->pty = -1;
|
|
utmp->useLogin = useLogin;
|
|
#ifdef HAVE_UTMPX_H
|
|
utmp->utmpx.ut_type = useLogin ? LOGIN_PROCESS : USER_PROCESS;
|
|
dcheck(!strncmp(ptyPath, "/dev/pts", 8));
|
|
strncat(&utmp->utmpx.ut_line[0], ptyPath + 5, sizeof(utmp->utmpx.ut_line));
|
|
strncat(&utmp->utmpx.ut_id[0], ptyPath + 8, sizeof(utmp->utmpx.ut_id));
|
|
strncat(&utmp->utmpx.ut_user[0], "SHELLINABOX", sizeof(utmp->utmpx.ut_user));
|
|
strncat(&utmp->utmpx.ut_host[0], peerName, sizeof(utmp->utmpx.ut_host));
|
|
struct timeval tv;
|
|
check(!gettimeofday(&tv, NULL));
|
|
utmp->utmpx.ut_tv.tv_sec = tv.tv_sec;
|
|
utmp->utmpx.ut_tv.tv_usec = tv.tv_usec;
|
|
#endif
|
|
}
|
|
|
|
struct Utmp *newUtmp(int useLogin, const char *ptyPath,
|
|
const char *peerName) {
|
|
struct Utmp *utmp;
|
|
check(utmp = malloc(sizeof(struct Utmp)));
|
|
initUtmp(utmp, useLogin, ptyPath, peerName);
|
|
return utmp;
|
|
}
|
|
|
|
#if defined(HAVE_UPDWTMP) && !defined(HAVE_UPDWTMPX)
|
|
#define min(a,b) ({ typeof(a) _a=(a); typeof(b) _b=(b); _a < _b ? _a : _b; })
|
|
#define updwtmpx x_updwtmpx
|
|
|
|
static void updwtmpx(const char *wtmpx_file, const struct utmpx *utx) {
|
|
struct utmp ut = { 0 };
|
|
ut.ut_type = utx->ut_type;
|
|
ut.ut_pid = utx->ut_pid;
|
|
ut.ut_tv.tv_sec = utx->ut_tv.tv_sec;
|
|
ut.ut_tv.tv_usec = utx->ut_tv.tv_usec;
|
|
memcpy(&ut.ut_line, &utx->ut_line,
|
|
min(sizeof(ut.ut_line), sizeof(utx->ut_line)));
|
|
memcpy(&ut.ut_id, &utx->ut_id,
|
|
min(sizeof(ut.ut_id), sizeof(utx->ut_id)));
|
|
memcpy(&ut.ut_user, &utx->ut_user,
|
|
min(sizeof(ut.ut_user), sizeof(utx->ut_user)));
|
|
memcpy(&ut.ut_host, &utx->ut_host,
|
|
min(sizeof(ut.ut_host), sizeof(utx->ut_host)));
|
|
updwtmp(wtmpx_file, &ut);
|
|
}
|
|
#endif
|
|
|
|
void destroyUtmp(struct Utmp *utmp) {
|
|
if (utmp) {
|
|
if (utmp->pty >= 0) {
|
|
#ifdef HAVE_UTMPX_H
|
|
utmp->utmpx.ut_type = DEAD_PROCESS;
|
|
memset(&utmp->utmpx.ut_user, 0, sizeof(utmp->utmpx.ut_user));
|
|
memset(&utmp->utmpx.ut_host, 0, sizeof(utmp->utmpx.ut_host));
|
|
struct timeval tv;
|
|
check(!gettimeofday(&tv, NULL));
|
|
utmp->utmpx.ut_tv.tv_sec = tv.tv_sec;
|
|
utmp->utmpx.ut_tv.tv_usec = tv.tv_usec;
|
|
|
|
// Temporarily regain privileges to update the utmp database
|
|
uid_t r_uid, e_uid, s_uid;
|
|
uid_t r_gid, e_gid, s_gid;
|
|
check(!getresuid(&r_uid, &e_uid, &s_uid));
|
|
check(!getresgid(&r_gid, &e_gid, &s_gid));
|
|
setresuid(0, 0, 0);
|
|
setresgid(0, 0, 0);
|
|
|
|
setutxent();
|
|
pututxline(&utmp->utmpx);
|
|
endutxent();
|
|
|
|
#if defined(HAVE_UPDWTMP) || defined(HAVE_UPDWTMPX)
|
|
if (!utmp->useLogin) {
|
|
updwtmpx("/var/log/wtmp", &utmp->utmpx);
|
|
}
|
|
#endif
|
|
|
|
// Switch back to the lower privileges
|
|
check(!setresgid(r_gid, e_gid, s_gid));
|
|
check(!setresuid(r_uid, e_uid, s_uid));
|
|
#endif
|
|
|
|
NOINTR(close(utmp->pty));
|
|
}
|
|
}
|
|
}
|
|
|
|
void deleteUtmp(struct Utmp *utmp) {
|
|
destroyUtmp(utmp);
|
|
free(utmp);
|
|
}
|
|
|
|
static void destroyUtmpHashEntry(void *arg, char *key, char *value) {
|
|
(void)arg;
|
|
(void)key;
|
|
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);
|
|
DIR *dir = opendir("/proc/self/fd");
|
|
if (dir == 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 {
|
|
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(dir)) {
|
|
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));
|
|
}
|
|
}
|
|
|
|
#if !defined(HAVE_OPENPTY) && !defined(HAVE_PTSNAME_R)
|
|
static int ptsname_r(int fd, char *buf, size_t buflen) {
|
|
// It is unfortunate that ptsname_r is not universally available.
|
|
// For the time being, this is not a big problem, as ShellInABox is
|
|
// single-threaded (and so is the launcher process). But if this
|
|
// code gets re-used in a multi-threaded application, that could
|
|
// lead to problems.
|
|
if (buf == NULL) {
|
|
errno = EINVAL;
|
|
return -1;
|
|
}
|
|
char *p = ptsname(fd);
|
|
if (p == NULL) {
|
|
return -1;
|
|
}
|
|
if (buflen < strlen(p) + 1) {
|
|
errno = ERANGE;
|
|
return -1;
|
|
}
|
|
strcpy(buf, p);
|
|
return 0;
|
|
}
|
|
#endif
|
|
|
|
static int forkPty(int *pty, int useLogin, struct Utmp **utmp,
|
|
const char *peerName) {
|
|
int slave;
|
|
char ptyPath[PATH_MAX];
|
|
#ifdef HAVE_OPENPTY
|
|
if (openpty(pty, &slave, ptyPath, NULL, NULL) < 0) {
|
|
*pty = -1;
|
|
*utmp = NULL;
|
|
return -1;
|
|
}
|
|
#else
|
|
if ((*pty = posix_openpt(O_RDWR|O_NOCTTY)) < 0 ||
|
|
grantpt(*pty) < 0 ||
|
|
unlockpt(*pty) < 0 ||
|
|
ptsname_r(*pty, ptyPath, sizeof(ptyPath)) < 0 ||
|
|
(slave = NOINTR(open(ptyPath, O_RDWR|O_NOCTTY))) < 0) {
|
|
if (*pty >= 0) {
|
|
NOINTR(close(*pty));
|
|
}
|
|
|
|
// Try old-style pty handling
|
|
char fname[40] = "/dev/ptyXX";
|
|
for (const char *ptr1 = "pqrstuvwxyzabcde"; *ptr1; ptr1++) {
|
|
fname[8] = *ptr1;
|
|
for (const char *ptr2 = "0123456789abcdef"; *ptr2; ptr2++) {
|
|
fname[9] = *ptr2;
|
|
if ((*pty = NOINTR(open(fname, O_RDWR, 0))) < 0) {
|
|
if (errno == ENOENT) {
|
|
continue;
|
|
}
|
|
}
|
|
grantpt(*pty);
|
|
unlockpt(*pty);
|
|
if (ptsname_r(*pty, ptyPath, sizeof(ptyPath)) < 0) {
|
|
strcpy(ptyPath, fname);
|
|
ptyPath[5] = 't';
|
|
}
|
|
if ((slave = NOINTR(open(ptyPath, O_RDWR|O_NOCTTY))) >= 0) {
|
|
debug("Opened old-style pty: %s", ptyPath);
|
|
goto success;
|
|
}
|
|
NOINTR(close(*pty));
|
|
}
|
|
}
|
|
*pty = -1;
|
|
*utmp = NULL;
|
|
return -1;
|
|
}
|
|
success:
|
|
#endif
|
|
|
|
// Fill in utmp entry
|
|
*utmp = newUtmp(useLogin, ptyPath, peerName);
|
|
|
|
// Now, fork off the child process
|
|
pid_t pid;
|
|
if ((pid = fork()) < 0) {
|
|
NOINTR(close(slave));
|
|
NOINTR(close(*pty));
|
|
*pty = -1;
|
|
deleteUtmp(*utmp);
|
|
*utmp = NULL;
|
|
return -1;
|
|
} else if (pid == 0) {
|
|
pid = getpid();
|
|
snprintf((char *)&(*utmp)->pid[0], sizeof((*utmp)->pid), "%d", pid);
|
|
#ifdef HAVE_UTMPX_H
|
|
(*utmp)->utmpx.ut_pid = pid;
|
|
#endif
|
|
(*utmp)->pty = slave;
|
|
|
|
closeAllFds((int []){ slave }, 1);
|
|
|
|
#ifdef HAVE_LOGIN_TTY
|
|
login_tty(slave);
|
|
#else
|
|
// Become the session/process-group leader
|
|
setsid();
|
|
setpgid(0, 0);
|
|
|
|
// Redirect standard I/O to the pty
|
|
dup2(slave, 0);
|
|
dup2(slave, 1);
|
|
dup2(slave, 2);
|
|
if (slave > 2) {
|
|
NOINTR(close(slave));
|
|
}
|
|
#endif
|
|
*pty = 0;
|
|
|
|
// Force the pty to be our control terminal
|
|
NOINTR(close(NOINTR(open(ptyPath, O_RDWR))));
|
|
|
|
return 0;
|
|
} else {
|
|
snprintf((char *)&(*utmp)->pid[0], sizeof((*utmp)->pid), "%d", pid);
|
|
#ifdef HAVE_UTMPX_H
|
|
(*utmp)->utmpx.ut_pid = pid;
|
|
#endif
|
|
(*utmp)->pty = *pty;
|
|
fcntl(*pty, F_SETFL, O_NONBLOCK|O_RDWR);
|
|
NOINTR(close(slave));
|
|
return pid;
|
|
}
|
|
}
|
|
|
|
static const struct passwd *getPWEnt(uid_t uid) {
|
|
struct passwd pwbuf, *pw;
|
|
char *buf;
|
|
#ifdef _SC_GETPW_R_SIZE_MAX
|
|
int len = sysconf(_SC_GETPW_R_SIZE_MAX);
|
|
if (len <= 0) {
|
|
len = 4096;
|
|
}
|
|
#else
|
|
int len = 4096;
|
|
#endif
|
|
check(buf = malloc(len));
|
|
check(!getpwuid_r(uid, &pwbuf, buf, len, &pw) && pw);
|
|
if (!pw->pw_name ) pw->pw_name = (char *)"";
|
|
if (!pw->pw_passwd) pw->pw_passwd = (char *)"";
|
|
if (!pw->pw_gecos ) pw->pw_gecos = (char *)"";
|
|
if (!pw->pw_dir ) pw->pw_dir = (char *)"";
|
|
if (!pw->pw_shell ) pw->pw_shell = (char *)"";
|
|
struct passwd *passwd;
|
|
check(passwd = calloc(sizeof(struct passwd) +
|
|
strlen(pw->pw_name) +
|
|
strlen(pw->pw_passwd) +
|
|
strlen(pw->pw_gecos) +
|
|
strlen(pw->pw_dir) +
|
|
strlen(pw->pw_shell) + 5, 1));
|
|
passwd->pw_uid = pw->pw_uid;
|
|
passwd->pw_gid = pw->pw_gid;
|
|
strncat(passwd->pw_shell = strrchr(
|
|
strncat(passwd->pw_dir = strrchr(
|
|
strncat(passwd->pw_gecos = strrchr(
|
|
strncat(passwd->pw_passwd = strrchr(
|
|
strncat(passwd->pw_name = (char *)(passwd + 1),
|
|
pw->pw_name, strlen(pw->pw_name)), '\000') + 1,
|
|
pw->pw_passwd, strlen(pw->pw_passwd)), '\000') + 1,
|
|
pw->pw_gecos, strlen(pw->pw_gecos)), '\000') + 1,
|
|
pw->pw_dir, strlen(pw->pw_dir)), '\000') + 1,
|
|
pw->pw_shell, strlen(pw->pw_shell));
|
|
free(buf);
|
|
return passwd;
|
|
}
|
|
|
|
static void sigAlrmHandler(int sig, siginfo_t *info, void *unused) {
|
|
(void)sig;
|
|
(void)info;
|
|
(void)unused;
|
|
puts("\nLogin timed out after 60 seconds.");
|
|
_exit(1);
|
|
}
|
|
|
|
static pam_handle_t *internalLogin(struct Service *service, struct Utmp *utmp,
|
|
char ***environment) {
|
|
// Time out after 60 seconds
|
|
struct sigaction sa;
|
|
memset(&sa, 0, sizeof(sa));
|
|
sa.sa_flags = SA_SIGINFO;
|
|
sa.sa_sigaction = sigAlrmHandler;
|
|
check(!sigaction(SIGALRM, &sa, NULL));
|
|
alarm(60);
|
|
|
|
// Change the prompt to include the host name
|
|
const char *hostname = NULL;
|
|
if (service->authUser == 2 /* SSH */) {
|
|
// If connecting to a remote host, include that hostname
|
|
hostname = strrchr(service->cmdline, '@');
|
|
if (!hostname || !strcmp(++hostname, "localhost")) {
|
|
hostname = NULL;
|
|
}
|
|
}
|
|
struct utsname uts;
|
|
memset(&uts, 0, sizeof(uts));
|
|
if (!hostname) {
|
|
// Find our local hostname
|
|
check(!uname(&uts));
|
|
hostname = uts.nodename;
|
|
}
|
|
const char *fqdn;
|
|
check(fqdn = strdup(hostname));
|
|
check(hostname = strdup(hostname));
|
|
char *dot = strchr(hostname, '.');
|
|
if (dot) {
|
|
*dot = '\000';
|
|
}
|
|
|
|
const struct passwd *pw;
|
|
pam_handle_t *pam = NULL;
|
|
if (service->authUser == 2 /* SSH */) {
|
|
// Just ask for the user name. SSH will negotiate the password
|
|
char *user = NULL;
|
|
char *prompt;
|
|
check(prompt = stringPrintf(NULL, "%s login: ", hostname));
|
|
for (;;) {
|
|
if (read_string(1, prompt, &user) <= 0) {
|
|
free(user);
|
|
free(prompt);
|
|
_exit(1);
|
|
}
|
|
if (*user) {
|
|
for (char *u = user; *u; u++) {
|
|
char ch = *u;
|
|
if (!((ch >= '0' && ch <= '9') ||
|
|
(ch >= 'A' && ch <= 'Z') ||
|
|
(ch >= 'a' && ch <= 'z') ||
|
|
ch == '-' || ch == '_' || ch == '.')) {
|
|
goto invalid_user_name;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
invalid_user_name:
|
|
free(user);
|
|
user = NULL;
|
|
}
|
|
free(prompt);
|
|
char *cmdline = stringPrintf(NULL, service->cmdline, user);
|
|
free(user);
|
|
|
|
// Replace '@localhost' with the actual host name. This results in a nicer
|
|
// prompt when SSH asks for the password.
|
|
char *ptr = strrchr(cmdline, '@');
|
|
if (!strcmp(ptr + 1, "localhost")) {
|
|
int offset = ptr + 1 - cmdline;
|
|
check(cmdline = realloc(cmdline,
|
|
strlen(cmdline) + strlen(fqdn) -
|
|
strlen("localhost") + 1));
|
|
ptr = cmdline + offset;
|
|
*ptr = '\000';
|
|
strncat(ptr, fqdn, strlen(fqdn));
|
|
}
|
|
|
|
free((void *)service->cmdline);
|
|
service->cmdline = cmdline;
|
|
|
|
// Run SSH as an unprivileged user
|
|
if ((service->uid = restricted) == 0) {
|
|
if (runAsUser >= 0) {
|
|
service->uid = runAsUser;
|
|
} else {
|
|
service->uid = getUserId("nobody");
|
|
}
|
|
if (runAsGroup >= 0) {
|
|
service->gid = runAsGroup;
|
|
} else {
|
|
service->gid = getGroupId("nogroup");
|
|
}
|
|
}
|
|
pw = getPWEnt(service->uid);
|
|
if (restricted) {
|
|
service->gid = pw->pw_gid;
|
|
}
|
|
service->user = getUserName(service->uid);
|
|
service->group = getGroupName(service->gid);
|
|
} else {
|
|
// Use PAM to negotiate user authentication and authorization
|
|
#if defined(HAVE_SECURITY_PAM_APPL_H)
|
|
struct pam_conv conv = { .conv = misc_conv };
|
|
if (service->authUser) {
|
|
check(supportsPAM());
|
|
check(pam_start("shellinabox", NULL, &conv, &pam) == PAM_SUCCESS);
|
|
|
|
const char *origPrompt;
|
|
check(pam_get_item(pam, PAM_USER_PROMPT, (void *)&origPrompt) ==
|
|
PAM_SUCCESS);
|
|
char *prompt;
|
|
check(prompt = stringPrintf(NULL, "%s %s", hostname,
|
|
origPrompt ? origPrompt : "login: "));
|
|
check(pam_set_item(pam, PAM_USER_PROMPT, prompt) == PAM_SUCCESS);
|
|
|
|
// Up to three attempts to enter the user id and password
|
|
for (int i = 0;;) {
|
|
check(pam_set_item(pam, PAM_USER, NULL) == PAM_SUCCESS);
|
|
int rc;
|
|
if ((rc = pam_authenticate(pam, PAM_SILENT)) ==
|
|
PAM_SUCCESS &&
|
|
(geteuid() ||
|
|
(rc = pam_acct_mgmt(pam, PAM_SILENT)) ==
|
|
PAM_SUCCESS)) {
|
|
break;
|
|
}
|
|
if (++i == 3) {
|
|
// Quit if login failed.
|
|
puts("\nMaximum number of tries exceeded (3)");
|
|
pam_end(pam, rc);
|
|
_exit(1);
|
|
} else {
|
|
puts("\nLogin incorrect");
|
|
}
|
|
}
|
|
check(pam_set_item(pam, PAM_USER_PROMPT, "login: ") == PAM_SUCCESS);
|
|
free(prompt);
|
|
|
|
// Retrieve user id, and group id.
|
|
const char *name;
|
|
check(pam_get_item(pam, PAM_USER, (void *)&name) == PAM_SUCCESS);
|
|
pw = getPWEnt(getUserId(name));
|
|
check(service->uid < 0);
|
|
check(service->gid < 0);
|
|
check(!service->user);
|
|
check(!service->group);
|
|
service->uid = pw->pw_uid;
|
|
service->gid = pw->pw_gid;
|
|
check(service->user = strdup(pw->pw_name));
|
|
service->group = getGroupName(pw->pw_gid);
|
|
} else {
|
|
check(service->uid >= 0);
|
|
check(service->gid >= 0);
|
|
check(service->user);
|
|
check(service->group);
|
|
if (supportsPAM()) {
|
|
check(pam_start("shellinabox", service->user, &conv, &pam) ==
|
|
PAM_SUCCESS);
|
|
int rc;
|
|
|
|
// PAM account management requires root access. Just skip it, if we
|
|
// are running with lower privileges.
|
|
if (!geteuid() &&
|
|
(rc = pam_acct_mgmt(pam, PAM_SILENT)) !=
|
|
PAM_SUCCESS) {
|
|
pam_end(pam, rc);
|
|
_exit(1);
|
|
}
|
|
}
|
|
pw = getPWEnt(service->uid);
|
|
}
|
|
#else
|
|
check(!supportsPAM());
|
|
pw = getPWEnt(service->uid);
|
|
#endif
|
|
}
|
|
free((void *)fqdn);
|
|
free((void *)hostname);
|
|
|
|
if (service->useDefaultShell) {
|
|
check(!service->cmdline);
|
|
service->cmdline = strdup(*pw->pw_shell ?
|
|
pw->pw_shell : "/bin/sh");
|
|
}
|
|
|
|
if (restricted &&
|
|
(service->uid != (int)restricted || service->gid != (int)pw->pw_gid)) {
|
|
puts("\nAccess denied!");
|
|
#if defined(HAVE_SECURITY_PAM_APPL_H)
|
|
if (service->authUser != 2 /* SSH */) {
|
|
pam_end(pam, PAM_SUCCESS);
|
|
}
|
|
#endif
|
|
_exit(1);
|
|
}
|
|
|
|
if (service->authUser != 2 /* SSH */) {
|
|
#if defined(HAVE_SECURITY_PAM_APPL_H)
|
|
if (pam) {
|
|
#ifdef HAVE_UTMPX_H
|
|
check(pam_set_item(pam, PAM_TTY, (const void **)utmp->utmpx.ut_line) ==
|
|
PAM_SUCCESS);
|
|
#endif
|
|
}
|
|
#else
|
|
check(!pam);
|
|
#endif
|
|
}
|
|
|
|
// Retrieve supplementary group ids.
|
|
int ngroups;
|
|
#if defined(__linux__)
|
|
// On Linux, we can query the number of supplementary groups. On all other
|
|
// platforms, we play it safe and just assume a fixed upper bound.
|
|
ngroups = 0;
|
|
getgrouplist(service->user, pw->pw_gid, NULL, &ngroups);
|
|
#else
|
|
ngroups = 128;
|
|
#endif
|
|
check(ngroups >= 0);
|
|
if (ngroups > 0) {
|
|
// Set supplementary group ids
|
|
gid_t *groups;
|
|
check(groups = malloc((ngroups + 1) * sizeof(gid_t)));
|
|
check(getgrouplist(service->user, pw->pw_gid, groups, &ngroups) >= 0);
|
|
|
|
// Make sure that any group that was requested on the command line is
|
|
// included, if it is not one of the normal groups for this user.
|
|
for (int i = 0; ; i++) {
|
|
if (i == ngroups) {
|
|
groups[ngroups++] = service->gid;
|
|
break;
|
|
} else if ((int)groups[i] == service->gid) {
|
|
break;
|
|
}
|
|
}
|
|
setgroups(ngroups, groups);
|
|
free(groups);
|
|
}
|
|
|
|
// Add standard environment variables
|
|
int numEnvVars = 0;
|
|
for (char **e = *environment; *e; numEnvVars++, e++) {
|
|
}
|
|
check(*environment = realloc(*environment,
|
|
(numEnvVars + 6)*sizeof(char *)));
|
|
(*environment)[numEnvVars++] = stringPrintf(NULL, "HOME=%s", pw->pw_dir);
|
|
(*environment)[numEnvVars++] = stringPrintf(NULL, "SHELL=%s", pw->pw_shell);
|
|
check(
|
|
(*environment)[numEnvVars++] = strdup(
|
|
"PATH=/usr/local/bin:/usr/bin:/bin:/usr/games"));
|
|
(*environment)[numEnvVars++] = stringPrintf(NULL, "LOGNAME=%s",
|
|
service->user);
|
|
(*environment)[numEnvVars++] = stringPrintf(NULL, "USER=%s", service->user);
|
|
(*environment)[numEnvVars++] = NULL;
|
|
free((void *)pw);
|
|
|
|
// Update utmp/wtmp entries
|
|
#ifdef HAVE_UTMPX_H
|
|
if (service->authUser != 2 /* SSH */) {
|
|
memset(&utmp->utmpx.ut_user, 0, sizeof(utmp->utmpx.ut_user));
|
|
strncat(&utmp->utmpx.ut_user[0], service->user,
|
|
sizeof(utmp->utmpx.ut_user));
|
|
setutxent();
|
|
pututxline(&utmp->utmpx);
|
|
endutxent();
|
|
|
|
#if defined(HAVE_UPDWTMP) || defined(HAVE_UPDWTMPX)
|
|
updwtmpx("/var/log/wtmp", &utmp->utmpx);
|
|
#endif
|
|
}
|
|
#endif
|
|
|
|
alarm(0);
|
|
return pam;
|
|
}
|
|
|
|
static void destroyVariableHashEntry(void *arg, char *key, char *value) {
|
|
(void)arg;
|
|
free(key);
|
|
free(value);
|
|
}
|
|
|
|
static void execService(int width, int height, struct Service *service,
|
|
const char *peerName, char **environment,
|
|
const char *url) {
|
|
(void)width;
|
|
(void)height;
|
|
|
|
// Create a hash table with all the variables that we can expand. This
|
|
// includes all environment variables being passed to the child.
|
|
HashMap *vars;
|
|
check(vars = newHashMap(destroyVariableHashEntry, NULL));
|
|
for (char **e = environment; *e; e++) {
|
|
char *ptr = strchr(*e, '=');
|
|
char *key, *value;
|
|
if (!ptr) {
|
|
check(key = strdup(*e));
|
|
check(value = strdup(""));
|
|
} else {
|
|
check(key = malloc(ptr - *e + 1));
|
|
memcpy(key, *e, ptr - *e);
|
|
key[ptr - *e] = '\000';
|
|
check(value = strdup(ptr + 1));
|
|
}
|
|
// All of our variables are lower-case
|
|
for (ptr = key; *ptr; ptr++) {
|
|
if (*ptr >= 'A' && *ptr <= 'Z') {
|
|
*ptr += 'a' - 'A';
|
|
}
|
|
}
|
|
addToHashMap(vars, key, value);
|
|
}
|
|
char *key, *value;
|
|
check(key = strdup("gid"));
|
|
addToHashMap(vars, key, stringPrintf(NULL, "%d", service->gid));
|
|
check(key = strdup("group"));
|
|
check(value = strdup(service->group));
|
|
addToHashMap(vars, key, value);
|
|
check(key = strdup("peer"));
|
|
check(value = strdup(peerName));
|
|
addToHashMap(vars, key, value);
|
|
check(key = strdup("uid"));
|
|
addToHashMap(vars, key, stringPrintf(NULL, "%d", service->uid));
|
|
check(key = strdup("url"));
|
|
addToHashMap(vars, key, strdup(url));
|
|
|
|
enum { ENV, ARGS } state = ENV;
|
|
enum { NONE, SINGLE, DOUBLE
|
|
} quote = NONE;
|
|
char *cmdline;
|
|
check(cmdline = strdup(service->cmdline));
|
|
int argc = 0;
|
|
char **argv;
|
|
check(argv = malloc(sizeof(char *)));
|
|
key = NULL;
|
|
value = NULL;
|
|
for (char *ptr = cmdline; ; ptr++) {
|
|
if (!key && *ptr && *ptr != ' ') {
|
|
key = ptr;
|
|
}
|
|
switch (*ptr) {
|
|
case '\'':
|
|
if (quote == SINGLE || quote == NONE) {
|
|
memmove(ptr, ptr + 1, strlen(ptr));
|
|
ptr--;
|
|
quote = quote == SINGLE ? NONE : SINGLE;
|
|
} else {
|
|
dcheck(quote == DOUBLE);
|
|
}
|
|
break;
|
|
case '\"':
|
|
if (quote == DOUBLE || quote == NONE) {
|
|
memmove(ptr, ptr + 1, strlen(ptr));
|
|
ptr--;
|
|
quote = quote == DOUBLE ? NONE : DOUBLE;
|
|
} else {
|
|
dcheck(quote == SINGLE);
|
|
}
|
|
break;
|
|
case '$':
|
|
if ((quote == NONE || quote == DOUBLE) && ptr[1] == '{') {
|
|
// Always treat environment variables as if they were quoted. There
|
|
// is no good reason for us to try to look for spaces within
|
|
// expanded environment variables. This just leads to subtle bugs.
|
|
char *end = ptr + 2;
|
|
while (*end && *end != '}') {
|
|
end++;
|
|
}
|
|
char ch = *end;
|
|
*end = '\000';
|
|
const char *repl = getFromHashMap(vars, ptr + 2);
|
|
int replLen = repl ? strlen(repl) : 0;
|
|
*end = ch;
|
|
if (ch) {
|
|
end++;
|
|
}
|
|
int incr = replLen - (end - ptr);
|
|
if (incr > 0) {
|
|
char *oldCmdline = cmdline;
|
|
check(cmdline = realloc(cmdline,
|
|
(end - cmdline) + strlen(end) +
|
|
incr + 1));
|
|
ptr += cmdline - oldCmdline;
|
|
end += cmdline - oldCmdline;
|
|
if (key) {
|
|
key += cmdline - oldCmdline;
|
|
}
|
|
if (value) {
|
|
value += cmdline - oldCmdline;
|
|
}
|
|
}
|
|
memmove(ptr + replLen, end, strlen(end) + 1);
|
|
if (repl) {
|
|
memcpy(ptr, repl, replLen);
|
|
}
|
|
ptr += replLen - 1;
|
|
}
|
|
break;
|
|
case '\\':
|
|
if (!ptr[1]) {
|
|
*ptr-- = '\000';
|
|
} else {
|
|
memmove(ptr, ptr + 1, strlen(ptr));
|
|
}
|
|
break;
|
|
case '=':
|
|
// This is the seperator between keys and values of any environment
|
|
// variable that we are asked to set.
|
|
if (state == ENV && quote == NONE && !value) {
|
|
*ptr = '\000';
|
|
value = ptr + 1;
|
|
}
|
|
break;
|
|
case ' ':
|
|
// If this space character is not quoted, this is the start of a new
|
|
// command line argument.
|
|
if (quote != NONE) {
|
|
break;
|
|
}
|
|
// Fall thru
|
|
case '\000':;
|
|
char ch = *ptr;
|
|
if (key) {
|
|
*ptr = '\000';
|
|
if (state == ENV && value) {
|
|
// Override an existing environment variable.
|
|
int numEnvVars = 0;
|
|
int len = strlen(key);
|
|
for (char **e = environment; *e; e++, numEnvVars++) {
|
|
if (!strncmp(*e, key, len) && (*e)[len] == '=') {
|
|
int s_size = len + strlen(value) + 1;
|
|
check(*e = realloc(*e, s_size + 1));
|
|
(*e)[len + 1] = '\000';
|
|
strncat(*e, value, s_size);
|
|
numEnvVars = -1;
|
|
break;
|
|
}
|
|
}
|
|
// Add a new environment variable
|
|
if (numEnvVars >= 0) {
|
|
check(environment = realloc(environment,
|
|
(numEnvVars + 2)*sizeof(char *)));
|
|
value[-1] = '=';
|
|
environment[numEnvVars++] = strdup(key);
|
|
environment[numEnvVars] = NULL;
|
|
}
|
|
} else {
|
|
// Add entry to argv.
|
|
state = ARGS;
|
|
argv[argc++] = strdup(key);
|
|
check(argv = realloc(argv, (argc + 1)*sizeof(char *)));
|
|
}
|
|
}
|
|
key = NULL;
|
|
value = NULL;
|
|
if (!ch) {
|
|
goto done;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
done:
|
|
free(cmdline);
|
|
argv[argc] = NULL;
|
|
deleteHashMap(vars);
|
|
check(argc);
|
|
|
|
extern char **environ;
|
|
environ = environment;
|
|
char *cmd = strdup(argv[0]);
|
|
char *slash = strrchr(argv[0], '/');
|
|
if (slash) {
|
|
memmove(argv[0], slash + 1, strlen(slash));
|
|
}
|
|
if (service->useDefaultShell) {
|
|
int len = strlen(argv[0]);
|
|
check(argv[0] = realloc(argv[0], len + 2));
|
|
memmove(argv[0] + 1, argv[0], len);
|
|
argv[0][0] = '-';
|
|
argv[0][len + 1] = '\000';
|
|
}
|
|
execvp(cmd, argv);
|
|
}
|
|
|
|
void setWindowSize(int pty, int width, int height) {
|
|
if (width > 0 && height > 0) {
|
|
#ifdef TIOCSSIZE
|
|
{
|
|
struct ttysize win;
|
|
ioctl(pty, TIOCGSIZE, &win);
|
|
win.ts_lines = height;
|
|
win.ts_cols = width;
|
|
ioctl(pty, TIOCSSIZE, &win);
|
|
}
|
|
#endif
|
|
#ifdef TIOCGWINSZ
|
|
{
|
|
struct winsize win;
|
|
ioctl(pty, TIOCGWINSZ, &win);
|
|
win.ws_row = height;
|
|
win.ws_col = width;
|
|
ioctl(pty, TIOCSWINSZ, &win);
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
static void childProcess(struct Service *service, int width, int height,
|
|
struct Utmp *utmp, const char *peerName,
|
|
const char *url) {
|
|
// Set initial window size
|
|
setWindowSize(0, width, height);
|
|
|
|
// Set up environment variables
|
|
static const char *legalEnv[] = { "TZ", "HZ", NULL };
|
|
char **environment;
|
|
check(environment = malloc(2*sizeof(char *)));
|
|
int numEnvVars = 1;
|
|
check(environment[0] = strdup("TERM=xterm"));
|
|
if (width > 0 && height > 0) {
|
|
numEnvVars += 2;
|
|
check(environment = realloc(environment,
|
|
(numEnvVars + 1)*sizeof(char *)));
|
|
environment[numEnvVars-2] = stringPrintf(NULL, "COLUMNS=%d", width);
|
|
environment[numEnvVars-1] = stringPrintf(NULL, "LINES=%d", height);
|
|
}
|
|
for (int i = 0; legalEnv[i]; i++) {
|
|
char *value = getenv(legalEnv[i]);
|
|
if (value) {
|
|
numEnvVars++;
|
|
check(environment = realloc(environment,
|
|
(numEnvVars + 1)*sizeof(char *)));
|
|
environment[numEnvVars-1] = stringPrintf(NULL, "%s=%s",
|
|
legalEnv[i], value);
|
|
}
|
|
}
|
|
environment[numEnvVars] = NULL;
|
|
|
|
// Set initial terminal settings
|
|
struct termios tt = { 0 };
|
|
tcgetattr(0, &tt);
|
|
cfsetispeed(&tt, 38400);
|
|
cfsetospeed(&tt, 38400);
|
|
tt.c_iflag = TTYDEF_IFLAG & ~ISTRIP;
|
|
tt.c_oflag = TTYDEF_OFLAG;
|
|
tt.c_lflag = TTYDEF_LFLAG;
|
|
tt.c_cflag = (TTYDEF_CFLAG & ~(CS7|PARENB|HUPCL)) | CS8;
|
|
tt.c_cc[VERASE] = '\x7F';
|
|
tcsetattr(0, TCSAFLUSH, &tt);
|
|
|
|
// Assert root privileges in order to update utmp entry.
|
|
setresuid(0, 0, 0);
|
|
setresgid(0, 0, 0);
|
|
#ifdef HAVE_UTMPX_H
|
|
setutxent();
|
|
struct utmpx utmpx = utmp->utmpx;
|
|
if (service->useLogin || service->authUser) {
|
|
utmpx.ut_type = LOGIN_PROCESS;
|
|
memset(utmpx.ut_host, 0, sizeof(utmpx.ut_host));
|
|
}
|
|
pututxline(&utmpx);
|
|
endutxent();
|
|
|
|
#if defined(HAVE_UPDWTMP) || defined(HAVE_UPDWTMPX)
|
|
if (!utmp->useLogin) {
|
|
memset(&utmpx.ut_user, 0, sizeof(utmpx.ut_user));
|
|
strncat(&utmpx.ut_user[0], "LOGIN", sizeof(utmpx.ut_user));
|
|
updwtmpx("/var/log/wtmp", &utmpx);
|
|
}
|
|
#endif
|
|
#endif
|
|
|
|
// Create session. We might have to fork another process as PAM wants us
|
|
// to close the session when the child terminates. And we must retain
|
|
// permissions, as session closure could require root permissions.
|
|
// None of this really applies if we are running as an unprivileged user.
|
|
// In that case, we do not bother about session management.
|
|
if (!service->useLogin) {
|
|
pam_handle_t *pam = internalLogin(service, utmp, &environment);
|
|
#if defined(HAVE_SECURITY_PAM_APPL_H)
|
|
if (pam && !geteuid()) {
|
|
if (pam_open_session(pam, PAM_SILENT) != PAM_SUCCESS) {
|
|
fprintf(stderr, "Access denied.\n");
|
|
_exit(1);
|
|
}
|
|
pid_t pid = fork();
|
|
switch (pid) {
|
|
case -1:
|
|
_exit(1);
|
|
case 0:
|
|
break;
|
|
default:;
|
|
// Finish all pending PAM operations.
|
|
int status, rc;
|
|
check(NOINTR(waitpid(pid, &status, 0)) == pid);
|
|
check((rc = pam_close_session(pam, PAM_SILENT)) ==
|
|
PAM_SUCCESS);
|
|
check(pam_end(pam, rc) == PAM_SUCCESS);
|
|
_exit(WIFEXITED(status) ? WEXITSTATUS(status) : -WTERMSIG(status));
|
|
}
|
|
}
|
|
#else
|
|
check(!pam);
|
|
#endif
|
|
}
|
|
|
|
// Change user and group ids
|
|
check(!setresgid(service->gid, service->gid, service->gid));
|
|
check(!setresuid(service->uid, service->uid, service->uid));
|
|
|
|
// Change working directory
|
|
if (service->useHomeDir) {
|
|
check(!service->useLogin);
|
|
const struct passwd *pw = getPWEnt(getuid());
|
|
check(!service->cwd);
|
|
check(service->cwd = strdup(pw->pw_dir));
|
|
free((void *)pw);
|
|
}
|
|
check(service->cwd);
|
|
if (!*service->cwd || *service->cwd != '/' || chdir(service->cwd)) {
|
|
check(service->cwd = realloc((char *)service->cwd, 2));
|
|
*(char *)service->cwd = '\000';
|
|
strncat((char *)service->cwd, "/", 1);
|
|
puts("No directory, logging in with HOME=/");
|
|
check(!chdir("/"));
|
|
for (int i = 0; environment[i]; i++) {
|
|
if (!strncmp(environment[i], "HOME=", 5)) {
|
|
free(environment[i]);
|
|
check(environment[i] = strdup("HOME=/"));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Finally, launch the child process.
|
|
if (service->useLogin == 1) {
|
|
execle("/bin/login", "login", "-p", "-h", peerName,
|
|
(void *)0, environment);
|
|
execle("/usr/bin/login", "login", "-p", "-h", peerName,
|
|
(void *)0, environment);
|
|
} else {
|
|
execService(width, height, service, peerName, environment, url);
|
|
}
|
|
_exit(1);
|
|
}
|
|
|
|
static void sigChildHandler(int sig, siginfo_t *info, void *unused) {
|
|
(void)sig;
|
|
(void)info;
|
|
(void)unused;
|
|
}
|
|
|
|
static void launcherDaemon(int fd) {
|
|
struct sigaction sa;
|
|
memset(&sa, 0, sizeof(sa));
|
|
sa.sa_flags = SA_NOCLDSTOP | SA_SIGINFO;
|
|
sa.sa_sigaction = sigChildHandler;
|
|
check(!sigaction(SIGCHLD, &sa, NULL));
|
|
|
|
struct LaunchRequest request;
|
|
for (;;) {
|
|
errno = 0;
|
|
int len = read(fd, &request, sizeof(request));
|
|
if (len != sizeof(request) && errno != EINTR) {
|
|
if (len) {
|
|
debug("Failed to read launch request");
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Check whether our read operation got interrupted, because a child
|
|
// has died.
|
|
int status;
|
|
pid_t pid;
|
|
while (NOINTR(pid = waitpid(-1, &status, WNOHANG)) > 0) {
|
|
if (WIFEXITED(pid) || WIFSIGNALED(pid)) {
|
|
char key[32];
|
|
snprintf(&key[0], sizeof(key), "%d", pid);
|
|
deleteFromHashMap(childProcesses, key);
|
|
}
|
|
}
|
|
if (len != sizeof(request)) {
|
|
continue;
|
|
}
|
|
|
|
char *url;
|
|
check(url = calloc(request.urlLength + 1, 1));
|
|
readURL:
|
|
len = read(fd, url, request.urlLength + 1);
|
|
if (len != request.urlLength + 1 && errno != EINTR) {
|
|
debug("Failed to read URL");
|
|
free(url);
|
|
break;
|
|
}
|
|
while (NOINTR(pid = waitpid(-1, &status, WNOHANG)) > 0) {
|
|
if (WIFEXITED(pid) || WIFSIGNALED(pid)) {
|
|
char key[32];
|
|
snprintf(&key[0], sizeof(key), "%d", pid);
|
|
deleteFromHashMap(childProcesses, key);
|
|
}
|
|
}
|
|
if (len != request.urlLength + 1) {
|
|
goto readURL;
|
|
}
|
|
|
|
check(request.service >= 0);
|
|
check(request.service < numServices);
|
|
|
|
// Sanitize the host name, so that we do not pass any unexpected characters
|
|
// to our child process.
|
|
request.peerName[sizeof(request.peerName)-1] = '\000';
|
|
for (char *s = request.peerName; *s; s++) {
|
|
if (!((*s >= '0' && *s <= '9') ||
|
|
(*s >= 'A' && *s <= 'Z') ||
|
|
(*s >= 'a' && *s <= 'z') ||
|
|
*s == '.' || *s == '-')) {
|
|
*s = '-';
|
|
}
|
|
}
|
|
|
|
// Fork and exec the child process.
|
|
int pty;
|
|
struct Utmp *utmp;
|
|
if ((pid = forkPty(&pty,
|
|
services[request.service]->useLogin,
|
|
&utmp, request.peerName)) == 0) {
|
|
childProcess(services[request.service], request.width, request.height,
|
|
utmp, request.peerName, url);
|
|
free(url);
|
|
_exit(1);
|
|
} else {
|
|
// Remember the utmp entry so that we can clean up when the child
|
|
// terminates.
|
|
free(url);
|
|
if (pid > 0) {
|
|
if (!childProcesses) {
|
|
childProcesses = newHashMap(destroyUtmpHashEntry, NULL);
|
|
}
|
|
addToHashMap(childProcesses, utmp->pid, (char *)utmp);
|
|
} else {
|
|
int fds[2];
|
|
if (!pipe(fds)) {
|
|
NOINTR(write(fds[1], "forkpty() failed\r\n", 18));
|
|
NOINTR(close(fds[1]));
|
|
pty = fds[0];
|
|
pid = 0;
|
|
}
|
|
}
|
|
|
|
// Send file handle and process id back to parent
|
|
char cmsg_buf[CMSG_SPACE(sizeof(int))] = { 0 };
|
|
struct iovec iov = { 0 };
|
|
struct msghdr msg = { 0 };
|
|
iov.iov_base = &pid;
|
|
iov.iov_len = sizeof(pid);
|
|
msg.msg_iov = &iov;
|
|
msg.msg_iovlen = 1;
|
|
msg.msg_control = &cmsg_buf;
|
|
msg.msg_controllen = sizeof(cmsg_buf);
|
|
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
|
|
check(cmsg);
|
|
cmsg->cmsg_level = SOL_SOCKET;
|
|
cmsg->cmsg_type = SCM_RIGHTS;
|
|
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
|
|
memcpy(CMSG_DATA(cmsg), &pty, sizeof(int));
|
|
if (NOINTR(sendmsg(fd, &msg, 0)) != sizeof(pid)) {
|
|
break;
|
|
}
|
|
NOINTR(close(pty));
|
|
}
|
|
}
|
|
deleteHashMap(childProcesses);
|
|
_exit(0);
|
|
}
|
|
|
|
int forkLauncher(void) {
|
|
int pair[2];
|
|
check(!socketpair(AF_UNIX, SOCK_STREAM, 0, pair));
|
|
|
|
switch (fork()) {
|
|
case 0:;
|
|
// If our real-uid is not "root", then we should not allow anybody to
|
|
// login unauthenticated users as anyone other than their own.
|
|
uid_t tmp;
|
|
check(!getresuid(&restricted, &tmp, &tmp));
|
|
|
|
// Temporarily drop most permissions. We still retain the ability to
|
|
// switch back to root, which is necessary for launching "login".
|
|
lowerPrivileges();
|
|
closeAllFds((int []){ pair[1] }, 1);
|
|
launcherDaemon(pair[1]);
|
|
fatal("exit() failed!");
|
|
case -1:
|
|
fatal("fork() failed!");
|
|
default:
|
|
NOINTR(close(pair[1]));
|
|
launcher = pair[0];
|
|
return launcher;
|
|
}
|
|
}
|
|
|
|
void terminateLauncher(void) {
|
|
if (launcher >= 0) {
|
|
NOINTR(close(launcher));
|
|
launcher = -1;
|
|
}
|
|
}
|