view usr/src/cmd/cmd-inet/usr.bin/rsh.c @ 4:1a15d5aaf794

synchronized with onnv_86 (6202) in onnv-gate
author Koji Uno <koji.uno@sun.com>
date Mon, 31 Aug 2009 14:38:03 +0900
parents c9caec207d52
children
line wrap: on
line source

/*
 * Copyright 2003 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

#pragma ident	"%Z%%M%	%I%	%E% SMI"

/*
 * Copyright (c) 1983 Regents of the University of California.
 * All rights reserved.  The Berkeley software License Agreement
 * specifies the terms and conditions for redistribution.
 *
 */

#define	_FILE_OFFSET_BITS 64

#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
/* just for FIONBIO ... */
#include <sys/filio.h>
#include <sys/stat.h>
#include <sys/select.h>

#include <netinet/in.h>

#include <assert.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <signal.h>
#include <pwd.h>
#include <netdb.h>
#include <locale.h>
#include <priv_utils.h>

#include <k5-int.h>
#include <profile/prof_int.h>
#include <com_err.h>
#include <kcmd.h>
#include <krb5.h>

/* signal disposition - signal handler or SIG_IGN, SIG_ERR, etc. */
typedef void (*sigdisp_t)(int);

extern errcode_t	profile_get_options_boolean(profile_t, char **,
    profile_options_boolean *);
extern errcode_t	profile_get_options_string(profile_t, char **,
    profile_option_strings *);

#define	RSH_BUFSIZ (1024 * 50)

static char des_inbuf[2 * RSH_BUFSIZ];	/* needs to be > largest read size */
static char des_outbuf[2 * RSH_BUFSIZ];	/* needs to be > largest write size */
static krb5_data desinbuf, desoutbuf;
static krb5_encrypt_block eblock;	/* eblock for encrypt/decrypt */
static krb5_context bsd_context;
static krb5_auth_context auth_context;
static krb5_creds *cred;
static krb5_keyblock *session_key;

static int encrypt_flag;	/* Flag set, when encryption is used */
static boolean_t krb5auth_flag;	/* Flag set, when KERBEROS is enabled */
static int fflag;	/* Flag set, if creds to be fwd'ed via -f */
static int Fflag;	/* Flag set, if fwd'able creds to be fwd'ed via -F */

/* Flag set, if -PN / -PO is specified */
static boolean_t rcmdoption_done;

/* Flags set, if corres. cmd line options are turned on */
static boolean_t encrypt_done, fwd_done, fwdable_done;

static profile_options_boolean option[] = {
	{ "encrypt", &encrypt_flag, 0 },
	{ "forward", &fflag, 0 },
	{ "forwardable", &Fflag, 0 },
	{ NULL, NULL, 0 }
};

static char *rcmdproto;
static profile_option_strings rcmdversion[] = {
	{ "rcmd_protocol", &rcmdproto, 0 },
	{ NULL, NULL, 0 }
};

static char *realmdef[] = { "realms", NULL, "rsh", NULL };
static char *appdef[] = { "appdefaults", "rsh", NULL };

static void sendsig(int);
static sigdisp_t sigdisp(int);
static boolean_t init_service(boolean_t);
static int desrshread(int, char *, int);
static int desrshwrite(int, char *, int);

static int		options;
static int		rfd2;
static int		portnumber;

static const char	rlogin_path[] = "/usr/bin/rlogin";
static const char	dash_x[] = "-x ";	/* Note the blank after -x */

static boolean_t readiv, writeiv;

#define	set2mask(setp)	((setp)->__sigbits[0])
#define	mask2set(mask, setp) \
	((mask) == -1 ? sigfillset(setp) : (set2mask(setp) = (mask)))

#ifdef DEBUG
#define	DEBUGOPTSTRING	"D:"
#else
#define	DEBUGOPTSTRING	""
#endif	/* DEBUG */

static void
sigsetmask(int mask)
{
	sigset_t	nset;

	(void) sigprocmask(0, NULL, &nset);
	mask2set(mask, &nset);
	(void) sigprocmask(SIG_SETMASK, &nset, NULL);
}

static int
sigblock(int mask)
{
	sigset_t oset;
	sigset_t nset;

	(void) sigprocmask(0, NULL, &nset);
	mask2set(mask, &nset);
	(void) sigprocmask(SIG_BLOCK, &nset, &oset);
	return (set2mask(&oset));
}

/*
 * Get signal disposition (or signal handler) for a given signal
 */
static sigdisp_t
sigdisp(int sig)
{
	struct sigaction act;

	act.sa_handler = NULL;
	act.sa_flags = 0;
	(void) sigemptyset(&act.sa_mask);
	(void) sigaction(sig, NULL, &act);
	return (act.sa_handler);
}

static pid_t child_pid = -1;

/*
 * If you do a command like "rsh host output | wc"
 * and wc terminates, then the parent will receive SIGPIPE
 * and the child needs to be terminated.
 */
/* ARGSUSED */
static void
sigpipehandler(int signal)
{
	if (child_pid != -1)
		(void) kill(child_pid, SIGKILL);
	exit(EXIT_SUCCESS);
}

#define	mask(s)	(1 << ((s) - 1))

static void
usage(void) {
	(void) fprintf(stderr, "%s\n%s\n",
	    gettext("usage: rsh [ -PN / -PO ] [ -l login ] [ -n ] "
		"[ -k realm ] [ -a ] [ -x ] [ -f / -F ] host command"),
	    gettext("       rsh [ -PN / -PO ] [ -l login ] [ -k realm ] "
		"[ -a ] [ -x ] [ -f / -F ] host"));
	exit(EXIT_FAILURE);
}

static void
die(const char *message)
{
	(void) fputs(message, stderr);
	usage();
}

static void
usage_forward(void)
{
	die(gettext("rsh: Only one of -f and -F allowed.\n"));
}

/*
 * rsh - remote shell
 */
/* VARARGS */
int
main(int argc, char **argv)
{
	int c, rem;
	char *cmd, *cp, **ap, buf[RSH_BUFSIZ], **argv0, *args, *args_no_x;
	char *host = NULL, *user = NULL;
	int cc;
	boolean_t asrsh = B_FALSE;
	struct passwd *pwd;
	boolean_t readfrom_rem;
	boolean_t readfrom_rfd2;
	int one = 1;
	int omask;
	boolean_t nflag = B_FALSE;
	char *krb_realm = NULL;
	krb5_flags authopts;
	krb5_error_code status;
	enum kcmd_proto kcmd_proto = KCMD_NEW_PROTOCOL;
	uid_t uid = getuid();

	c = (argc + 1) * sizeof (char *);
	if ((argv0 = malloc(c)) == NULL) {
		perror("malloc");
		return (EXIT_FAILURE);
	}
	(void) memcpy(argv0, argv, c);

	(void) setlocale(LC_ALL, "");

	(void) textdomain(TEXT_DOMAIN);

	/*
	 * Determine command name used to invoke to rlogin(1). Users can
	 * create links named by a host pointing to the binary and type
	 * "hostname" to log into that host afterwards.
	 */
	cmd = strrchr(argv[0], '/');
	cmd = (cmd != NULL) ? (cmd + 1) : argv[0];

	/*
	 *	Add "remsh" as an alias for "rsh" (System III, V networking
	 *	add-ons often used this name for the remote shell since rsh
	 *	was already taken for the restricted shell).  Note that this
	 *	usurps the ability to use "remsh" as the name of a host (by
	 *	symlinking it to rsh), so we go one step farther:  if the
	 *	file "/usr/bin/remsh" does not exist, we behave as if "remsh"
	 *	is a host name.  If it does exist, we accept "remsh" as an
	 *	"rsh" alias.
	 */
	if (strcmp(cmd, "remsh") == 0) {
		struct stat sb;

		if (stat("/usr/bin/remsh", &sb) < 0)
			host = cmd;
	} else if (strcmp(cmd, "rsh") != 0) {
		host = cmd;
	}

	/* Handle legacy synopsis "rsh hostname options [command]". */
	if (host == NULL) {
		if (argc < 2)
			usage();
		if (*argv[1] != '-') {
			host = argv[1];
			argc--;
			argv[1] = argv[0];
			argv++;
			asrsh = B_TRUE;
		}
	}

	while ((c = getopt(argc, argv,
	    DEBUGOPTSTRING "8AFLP:ade:fk:l:nwx")) != -1) {
		switch (c) {
#ifdef DEBUG
		case 'D':
			portnumber = htons(atoi(optarg));
			krb5auth_flag = B_TRUE;
			break;
#endif /* DEBUG */
		case 'F':
			if (fflag)
				usage_forward();
			Fflag = 1;
			krb5auth_flag = B_TRUE;
			fwdable_done = B_TRUE;
			break;
		case 'f':
			if (Fflag)
				usage_forward();
			fflag = 1;
			krb5auth_flag = B_TRUE;
			fwd_done = B_TRUE;
			break;
		case 'P':
			if (strcmp(optarg, "N") == 0)
				kcmd_proto = KCMD_NEW_PROTOCOL;
			else if (strcmp(optarg, "O") == 0)
				kcmd_proto = KCMD_OLD_PROTOCOL;
			else
				die(gettext("rsh: Only -PN or -PO "
				    "allowed.\n"));
			if (rcmdoption_done)
				die(gettext("rsh: Only one of -PN and -PO "
				    "allowed.\n"));
			rcmdoption_done = B_TRUE;
			krb5auth_flag = B_TRUE;
			break;
		case 'a':
			krb5auth_flag = B_TRUE;
			break;
		case 'd':
			options |= SO_DEBUG;
			break;
		case 'k':
			krb_realm = optarg;
			krb5auth_flag = B_TRUE;
			break;
		case 'l':
			user = optarg;
			break;
		case 'n':
			if (!nflag) {
				if (close(STDIN_FILENO) < 0) {
					perror("close");
					return (EXIT_FAILURE);
				}
				/*
				 * "STDION_FILENO" defined to 0 by POSIX
				 * and hence the lowest file descriptor.
				 * So the open(2) below is guaranteed to
				 * reopen it because we closed it above.
				 */
				if (open("/dev/null", O_RDONLY) < 0) {
					perror("open");
					return (EXIT_FAILURE);
				}
				nflag = B_TRUE;
			}
			break;
		case 'x':
			encrypt_flag = 1;
			krb5auth_flag = B_TRUE;
			encrypt_done = B_TRUE;
			break;
		/*
		 * Ignore the -L, -w, -e and -8 flags to allow aliases with
		 * rlogin to work. Actually rlogin(1) doesn't understand
		 * -w either but because "rsh -w hostname command" used
		 * to work we still accept it.
		 */
		case '8':
		case 'L':
		case 'e':
		case 'w':
		/*
		 * On the lines of the -L, -w, -e and -8 options above, we
		 * ignore the -A option too, in order to allow aliases with
		 * rlogin to work.
		 *
		 * Mind you !, the -a option to trigger Kerberos authentication
		 * in rsh, has a totally different usage in rlogin, its the
		 * -A option (in rlogin) which needs to be used to talk
		 * Kerberos.
		 */
		case 'A':
			break;
		default:
			usage();
		}
	}

	argc -= optind;
	argv += optind;

	if (host == NULL) {
		if (argc == 0)
			usage();
		argc--;
		host = *argv++;
		asrsh = B_TRUE;
	}

	if (argc == 0) {
		(void) setreuid(uid, uid);
		if (nflag)
			usage();
		if (asrsh)
			*argv0 = "rlogin";
		(void) execv(rlogin_path, argv0);
		perror(rlogin_path);

		(void) fprintf(stderr, gettext("No local rlogin "
				"program found.\n"));
		return (EXIT_FAILURE);
	}

	if (__init_suid_priv(0, PRIV_NET_PRIVADDR, NULL) == -1) {
		(void) fprintf(stderr,
		    gettext("Insufficient privileges, "
			"rsh must be set-uid root\n"));
		return (EXIT_FAILURE);
	}

	pwd = getpwuid(uid);
	if (pwd == NULL) {
		(void) fprintf(stderr, gettext("who are you?\n"));
		return (EXIT_FAILURE);
	}
	if (user == NULL)
		user = pwd->pw_name;

	if (krb5auth_flag) {
		status = krb5_init_context(&bsd_context);
		if (status) {
			com_err("rsh", status, "while initializing krb5");
			return (EXIT_FAILURE);
		}

		/*
		 * Get our local realm to look up local realm options.
		 */
		status = krb5_get_default_realm(bsd_context, &realmdef[1]);
		if (status) {
			com_err("rsh", status,
				gettext("while getting default realm"));
			return (EXIT_FAILURE);
		}
		/*
		 * Check the realms section in krb5.conf for encryption,
		 * forward & forwardable info
		 */
		profile_get_options_boolean(bsd_context->profile, realmdef,
						option);
		/*
		 * Check the appdefaults section
		 */
		profile_get_options_boolean(bsd_context->profile, appdef,
						option);
		profile_get_options_string(bsd_context->profile, appdef,
						rcmdversion);
		/*
		 * Set the *_flag variables, if the corresponding *_done are
		 * set to 1, because we dont want the config file values
		 * overriding the command line options.
		 */
		if (encrypt_done)
			encrypt_flag = 1;
		if (fwd_done) {
			fflag = 1;
			Fflag = 0;
		} else if (fwdable_done) {
			Fflag = 1;
			fflag = 0;
		}
		if (!rcmdoption_done && (rcmdproto != NULL)) {
			if (strncmp(rcmdproto, "rcmdv2", 6) == 0) {
				kcmd_proto = KCMD_NEW_PROTOCOL;
			} else if (strncmp(rcmdproto, "rcmdv1", 6) == 0) {
				kcmd_proto = KCMD_OLD_PROTOCOL;
			} else {
				(void) fprintf(stderr, gettext("Unrecognized "
					"KCMD protocol (%s)"), rcmdproto);
				return (EXIT_FAILURE);
			}
		}


		if (encrypt_flag && (!krb5_privacy_allowed())) {
			(void) fprintf(stderr, gettext("rsh: Encryption not "
				"supported.\n"));
			return (EXIT_FAILURE);
		}
	}

	/*
	 * Connect with the service (shell/kshell) on the daemon side
	 */
	if (portnumber == 0) {
		while (!init_service(krb5auth_flag)) {
			/*
			 * Connecting to the 'kshell' service failed,
			 * fallback to normal rsh; Reset all KRB5 flags
			 * and connect to 'shell' service on the server
			 */
			krb5auth_flag = B_FALSE;
			encrypt_flag = fflag = Fflag = 0;
		}
	}

	cc = encrypt_flag ? strlen(dash_x) : 0;
	for (ap = argv; *ap != NULL; ap++)
		cc += strlen(*ap) + 1;
	cp = args = malloc(cc);
	if (cp == NULL)
		perror("malloc");
	if (encrypt_flag) {
		int length;

		length = strlcpy(args, dash_x, cc);
		cp += length;
		cc -= length;
	}
	args_no_x = args;

	for (ap = argv; *ap != NULL; ap++) {
		int length;

		length = strlcpy(cp, *ap, cc);
		assert(length < cc);
		cp += length;
		cc -= length;
		if (ap[1] != NULL) {
			*cp++ = ' ';
			cc--;
		}
	}

	if (krb5auth_flag) {
		authopts = AP_OPTS_MUTUAL_REQUIRED;
		/*
		 * Piggy-back forwarding flags on top of authopts;
		 * they will be reset in kcmd
		 */
		if (fflag || Fflag)
			authopts |= OPTS_FORWARD_CREDS;
		if (Fflag)
			authopts |= OPTS_FORWARDABLE_CREDS;

		status = kcmd(&rem, &host, portnumber,
				pwd->pw_name, user,
				args, &rfd2, "host", krb_realm,
				bsd_context, &auth_context, &cred,
				NULL,	/* No need for sequence number */
				NULL,	/* No need for server seq # */
				authopts,
				1,	/* Always set anyport */
				&kcmd_proto);
		if (status != 0) {
			/*
			 * If new protocol requested, we dont fallback to
			 * less secure ones.
			 */
			if (kcmd_proto == KCMD_NEW_PROTOCOL) {
				(void) fprintf(stderr, gettext("rsh: kcmdv2 "
					"to host %s failed - %s\n"
					"Fallback to normal rsh denied."),
					host, error_message(status));
				return (EXIT_FAILURE);
			}
			/* check NO_TKT_FILE or equivalent... */
			if (status != -1) {
				(void) fprintf(stderr,
				gettext("rsh: kcmd to host %s failed - %s\n"
				"trying normal rsh...\n\n"),
				host, error_message(status));
			} else {
				(void) fprintf(stderr,
					gettext("trying normal rsh...\n"));
			}
			/*
			 * kcmd() failed, so we now fallback to normal rsh,
			 * after resetting the KRB5 flags and the 'args' array
			 */
			krb5auth_flag = B_FALSE;
			encrypt_flag = fflag = Fflag = 0;
			args = args_no_x;
			(void) init_service(B_FALSE);
		} else {
			/*
			 * Set up buffers for desread and deswrite.
			 */
			desinbuf.data = des_inbuf;
			desoutbuf.data = des_outbuf;
			desinbuf.length = sizeof (des_inbuf);
			desoutbuf.length = sizeof (des_outbuf);

			session_key = &cred->keyblock;

			if (kcmd_proto == KCMD_NEW_PROTOCOL) {
				status = krb5_auth_con_getlocalsubkey(
				    bsd_context,
				    auth_context,
				    &session_key);
				if (status) {
					com_err("rsh", status,
					    "determining subkey for session");
					return (EXIT_FAILURE);
				}
				if (session_key == NULL) {
					com_err("rsh", 0, "no subkey "
					    "negotiated for connection");
					return (EXIT_FAILURE);
				}
			}

			eblock.crypto_entry = session_key->enctype;
			eblock.key = (krb5_keyblock *)session_key;

			init_encrypt(encrypt_flag, bsd_context, kcmd_proto,
			    &desinbuf, &desoutbuf, CLIENT, &eblock);
			if (encrypt_flag) {
				char *s = gettext("This rsh session is using "
				    "encryption for all data transmissions.");
				(void) write(STDERR_FILENO, s, strlen(s));
				(void) write(STDERR_FILENO, "\r\n", 2);
			}
		}
	}

	/*
	 * Don't merge this with the "if" statement above because
	 * "krb5auth_flag" might be set to false inside it.
	 */
	if (!krb5auth_flag) {
		rem = rcmd_af(&host, portnumber, pwd->pw_name, user, args,
		    &rfd2, AF_INET6);
		if (rem < 0)
			return (EXIT_FAILURE);
	}
	__priv_relinquish();

	if (rfd2 < 0) {
		(void) fprintf(stderr, gettext("rsh: can't establish "
				"stderr\n"));
		return (EXIT_FAILURE);
	}
	if (options & SO_DEBUG) {
		if (setsockopt(rem, SOL_SOCKET, SO_DEBUG, (char *)&one,
		    sizeof (one)) < 0)
			perror("rsh: setsockopt (stdin)");
		if (setsockopt(rfd2, SOL_SOCKET, SO_DEBUG, (char *)&one,
		    sizeof (one)) < 0)
			perror("rsh: setsockopt (stderr)");
	}
	omask = sigblock(mask(SIGINT)|mask(SIGQUIT)|mask(SIGTERM));

	if (sigdisp(SIGINT) != SIG_IGN)
		(void) sigset(SIGINT, sendsig);
	if (sigdisp(SIGQUIT) != SIG_IGN)
		(void) sigset(SIGQUIT, sendsig);
	if (sigdisp(SIGTERM) != SIG_IGN)
		(void) sigset(SIGTERM, sendsig);

	if (nflag) {
		(void) shutdown(rem, SHUT_WR);
	} else {
		child_pid = fork();
		if (child_pid < 0) {
			perror("rsh: fork");
			return (EXIT_FAILURE);
		}

		if (!encrypt_flag) {
			(void) ioctl(rfd2, FIONBIO, &one);
			(void) ioctl(rem, FIONBIO, &one);
		}

		if (child_pid == 0) {
			/* Child */
			fd_set remset;
			char *bp;
			int  wc;
			(void) close(rfd2);
		reread:
			errno = 0;
			cc = read(0, buf, sizeof (buf));
			if (cc <= 0)
				goto done;
			bp = buf;
		rewrite:
			FD_ZERO(&remset);
			FD_SET(rem, &remset);
			if (select(rem + 1, NULL, &remset, NULL, NULL) < 0) {
				if (errno != EINTR) {
					perror("rsh: select");
					return (EXIT_FAILURE);
				}
				goto rewrite;
			}
			if (!FD_ISSET(rem, &remset))
				goto rewrite;
			writeiv = B_FALSE;
			wc = desrshwrite(rem, bp, cc);
			if (wc < 0) {
				if (errno == EWOULDBLOCK)
					goto rewrite;
				goto done;
			}
			cc -= wc; bp += wc;
			if (cc == 0)
				goto reread;
			goto rewrite;
		done:
			(void) shutdown(rem, SHUT_WR);
			return (EXIT_SUCCESS);
		}
	}

#define	MAX(a, b)	(((a) > (b)) ? (a) : (b))

	sigsetmask(omask);
	readfrom_rem = B_TRUE;
	readfrom_rfd2 = B_TRUE;
	(void) sigset(SIGPIPE, sigpipehandler);
	do {
		fd_set readyset;

		FD_ZERO(&readyset);
		if (readfrom_rem)
			FD_SET(rem, &readyset);
		if (readfrom_rfd2)
			FD_SET(rfd2, &readyset);
		if (select(MAX(rem, rfd2) + 1, &readyset, NULL, NULL,
		    NULL) < 0) {
			if (errno != EINTR) {
				perror("rsh: select");
				return (EXIT_FAILURE);
			}
			continue;
		}
		if (FD_ISSET(rfd2, &readyset)) {
			errno = 0;
			readiv = B_TRUE;
			cc = desrshread(rfd2, buf, sizeof (buf));
			if (cc <= 0) {
				if (errno != EWOULDBLOCK)
					readfrom_rfd2 = B_FALSE;
			} else {
				(void) write(STDERR_FILENO, buf, cc);
			}
		}
		if (FD_ISSET(rem, &readyset)) {
			errno = 0;
			readiv = B_FALSE;
			cc = desrshread(rem, buf, sizeof (buf));
			if (cc <= 0) {
				if (errno != EWOULDBLOCK)
					readfrom_rem = B_FALSE;
			} else
				(void) write(STDOUT_FILENO, buf, cc);
		}
	} while (readfrom_rem || readfrom_rfd2);

	if (!nflag)
		(void) kill(child_pid, SIGKILL);
	return (EXIT_SUCCESS);
}

static void
sendsig(int signum)
{
	char	buffer;

	writeiv = B_TRUE;
	buffer = (char)signum;
	(void) desrshwrite(rfd2, &buffer, 1);
}

static boolean_t
init_service(boolean_t krb5flag)
{
	struct servent *sp;

	if (krb5flag) {
		sp = getservbyname("kshell", "tcp");
		if (sp == NULL) {
			(void) fprintf(stderr,
				gettext("rsh: kshell/tcp: unknown service.\n"
				"trying normal shell/tcp service\n"));
			return (B_FALSE);
		}
	} else {
		sp = getservbyname("shell", "tcp");
		if (sp == NULL) {
			portnumber = htons(IPPORT_CMDSERVER);
			return (B_TRUE);
		}
	}

	portnumber = sp->s_port;
	return (B_TRUE);
}

static int
desrshread(int fd, char *buf, int len)
{
	return (desread(fd, buf, len, readiv ? 1 : 0));
}

static int
desrshwrite(int fd, char *buf, int len)
{
	return (deswrite(fd, buf, len, writeiv ? 1 : 0));
}