- djm@cvs.openbsd.org 2004/06/13 15:03:02
     [channels.c channels.h clientloop.c clientloop.h includes.h readconf.c]
     [readconf.h scp.1 sftp.1 ssh.1 ssh.c ssh_config.5]
     implement session multiplexing in the client (the server has supported
     this since 2.0); ok markus@
diff --git a/clientloop.c b/clientloop.c
index 31e6041..6401588 100644
--- a/clientloop.c
+++ b/clientloop.c
@@ -59,7 +59,7 @@
  */
 
 #include "includes.h"
-RCSID("$OpenBSD: clientloop.c,v 1.122 2004/05/22 06:32:12 djm Exp $");
+RCSID("$OpenBSD: clientloop.c,v 1.123 2004/06/13 15:03:02 djm Exp $");
 
 #include "ssh.h"
 #include "ssh1.h"
@@ -81,6 +81,9 @@
 #include "atomicio.h"
 #include "sshpty.h"
 #include "misc.h"
+#include "monitor_fdpass.h"
+#include "match.h"
+#include "msg.h"
 
 /* import options */
 extern Options options;
@@ -91,6 +94,9 @@
 /* Flag indicating that no shell has been requested */
 extern int no_shell_flag;
 
+/* Control socket */
+extern int control_fd;
+
 /*
  * Name of the host we are connecting to.  This is the name given on the
  * command line, or the HostName specified for the user-supplied name in a
@@ -131,9 +137,19 @@
 static void client_init_dispatch(void);
 int	session_ident = -1;
 
+struct confirm_ctx {
+	int want_tty;
+	int want_subsys;
+	Buffer cmd;
+	char *term;
+	struct termios tio;
+};
+
 /*XXX*/
 extern Kex *xxx_kex;
 
+void ssh_process_session2_setup(int, int, int, Buffer *);
+
 /* Restores stdin to blocking mode. */
 
 static void
@@ -291,19 +307,13 @@
 	/** XXX race */
 	received_window_change_signal = 0;
 
-	if (ioctl(fileno(stdin), TIOCGWINSZ, &ws) < 0)
-		return;
-
 	debug2("client_check_window_change: changed");
 
 	if (compat20) {
-		channel_request_start(session_ident, "window-change", 0);
-		packet_put_int(ws.ws_col);
-		packet_put_int(ws.ws_row);
-		packet_put_int(ws.ws_xpixel);
-		packet_put_int(ws.ws_ypixel);
-		packet_send();
+		channel_send_window_changes();
 	} else {
+		if (ioctl(fileno(stdin), TIOCGWINSZ, &ws) < 0)
+			return;
 		packet_start(SSH_CMSG_WINDOW_SIZE);
 		packet_put_int(ws.ws_row);
 		packet_put_int(ws.ws_col);
@@ -335,7 +345,6 @@
  * Waits until the client can do something (some data becomes available on
  * one of the file descriptors).
  */
-
 static void
 client_wait_until_can_do_something(fd_set **readsetp, fd_set **writesetp,
     int *maxfdp, int *nallocp, int rekeying)
@@ -381,6 +390,9 @@
 	if (packet_have_data_to_write())
 		FD_SET(connection_out, *writesetp);
 
+	if (control_fd != -1)
+		FD_SET(control_fd, *readsetp);
+
 	/*
 	 * Wait for something to happen.  This will suspend the process until
 	 * some selected descriptor can be read, written, or has some other
@@ -500,6 +512,176 @@
 }
 
 static void
+client_subsystem_reply(int type, u_int32_t seq, void *ctxt)
+{
+	int id;
+	Channel *c;
+	
+	id = packet_get_int();
+	packet_check_eom();
+
+	if ((c = channel_lookup(id)) == NULL) {
+		error("%s: no channel for id %d", __func__, id);
+		return;
+	}
+
+	if (type == SSH2_MSG_CHANNEL_SUCCESS)
+		debug2("Request suceeded on channel %d", id);
+	else if (type == SSH2_MSG_CHANNEL_FAILURE) {
+		error("Request failed on channel %d", id);
+		channel_free(c);
+	}
+}
+
+static void
+client_extra_session2_setup(int id, void *arg)
+{
+	struct confirm_ctx *cctx = arg;
+	Channel *c;
+	
+	if (cctx == NULL)
+		fatal("%s: cctx == NULL", __func__);
+	if ((c = channel_lookup(id)) == NULL)
+		fatal("%s: no channel for id %d", __func__, id);
+
+	client_session2_setup(id, cctx->want_tty, cctx->want_subsys, 
+	    cctx->term, &cctx->tio, c->rfd, &cctx->cmd, 
+	    client_subsystem_reply);
+	
+	c->confirm_ctx = NULL;
+	buffer_free(&cctx->cmd);
+	free(cctx->term);
+	free(cctx);
+}
+
+static void
+client_process_control(fd_set * readset)
+{
+	Buffer m;
+	Channel *c;
+	int client_fd, new_fd[3], ver;
+	socklen_t addrlen;
+	struct sockaddr_storage addr;
+	struct confirm_ctx *cctx;
+	char *cmd;
+	u_int len;
+	uid_t euid;
+	gid_t egid;
+
+	/*
+	 * Accept connection on control socket
+	 */
+	if (control_fd == -1 || !FD_ISSET(control_fd, readset))
+		return;
+
+	memset(&addr, 0, sizeof(addr));
+	addrlen = sizeof(addr);
+	if ((client_fd = accept(control_fd,
+	    (struct sockaddr*)&addr, &addrlen)) == -1) {
+		error("%s accept: %s", __func__, strerror(errno));
+		return;
+	}
+
+	if (getpeereid(client_fd, &euid, &egid) < 0) {
+		error("%s getpeereid failed: %s", __func__, strerror(errno));
+		close(client_fd);
+		return;
+	}
+	if ((euid != 0) && (getuid() != euid)) {
+		error("control mode uid mismatch: peer euid %u != uid %u",
+		    (u_int) euid, (u_int) getuid());
+		close(client_fd);
+		return;
+	}
+	/* XXX: implement use of ssh-askpass to confirm additional channels */
+
+	unset_nonblock(client_fd);
+
+	buffer_init(&m);
+
+	buffer_put_int(&m, getpid());
+	if (ssh_msg_send(client_fd, /* version */0, &m) == -1) {
+		error("%s: client msg_send failed", __func__);
+		close(client_fd);
+		return;
+	}
+	buffer_clear(&m);
+
+	if (ssh_msg_recv(client_fd, &m) == -1) {
+		error("%s: client msg_recv failed", __func__);
+		close(client_fd);
+		return;
+	}
+
+	if ((ver = buffer_get_char(&m)) != 0) {
+		error("%s: wrong client version %d", __func__, ver);
+		buffer_free(&m);
+		close(client_fd);
+		return;
+	}
+
+	cctx = xmalloc(sizeof(*cctx));
+	memset(cctx, 0, sizeof(*cctx));
+
+	cctx->want_tty = buffer_get_int(&m);
+	cctx->want_subsys = buffer_get_int(&m);
+	cctx->term = buffer_get_string(&m, &len);
+
+	cmd = buffer_get_string(&m, &len);
+	buffer_init(&cctx->cmd);
+	buffer_append(&cctx->cmd, cmd, strlen(cmd));
+
+	debug2("%s: accepted tty %d, subsys %d, cmd %s", __func__,
+	    cctx->want_tty, cctx->want_subsys, cmd);
+
+	/* Gather fds from client */
+	new_fd[0] = mm_receive_fd(client_fd);
+	new_fd[1] = mm_receive_fd(client_fd);
+	new_fd[2] = mm_receive_fd(client_fd);
+
+	debug2("%s: got fds stdin %d, stdout %d, stderr %d", __func__,
+	    new_fd[0], new_fd[1], new_fd[2]);
+
+	/* Try to pick up ttymodes from client before it goes raw */
+	if (cctx->want_tty && tcgetattr(new_fd[0], &cctx->tio) == -1)
+		error("%s: tcgetattr: %s", __func__, strerror(errno));
+
+	buffer_clear(&m);
+	if (ssh_msg_send(client_fd, /* version */0, &m) == -1) {
+		error("%s: client msg_send failed", __func__);
+		close(client_fd);
+		close(new_fd[0]);
+		close(new_fd[1]);
+		close(new_fd[2]);
+		return;
+	}
+	buffer_free(&m);
+
+	/* enable nonblocking unless tty */
+	if (!isatty(new_fd[0]))
+		set_nonblock(new_fd[0]);
+	if (!isatty(new_fd[1]))
+		set_nonblock(new_fd[1]);
+	if (!isatty(new_fd[2]))
+		set_nonblock(new_fd[2]);
+
+	set_nonblock(client_fd);
+
+	c = channel_new("session", SSH_CHANNEL_OPENING, 
+	    new_fd[0], new_fd[1], new_fd[2],
+	    CHAN_SES_WINDOW_DEFAULT, CHAN_SES_PACKET_DEFAULT,
+	    CHAN_EXTENDED_WRITE, "client-session", /*nonblock*/0);
+
+	/* XXX */
+	c->ctl_fd = client_fd;
+
+	debug3("%s: channel_new: %d", __func__, c->self);
+
+	channel_send_open(c->self);
+	channel_register_confirm(c->self, client_extra_session2_setup, cctx);
+}
+
+static void
 process_cmdline(void)
 {
 	void (*handler)(int);
@@ -901,9 +1083,6 @@
 static void
 client_channel_closed(int id, void *arg)
 {
-	if (id != session_ident)
-		error("client_channel_closed: id %d != session_ident %d",
-		    id, session_ident);
 	channel_cancel_cleanup(id);
 	session_closed = 1;
 	leave_raw_mode();
@@ -937,6 +1116,8 @@
 	connection_in = packet_get_connection_in();
 	connection_out = packet_get_connection_out();
 	max_fd = MAX(connection_in, connection_out);
+	if (control_fd != -1)
+		max_fd = MAX(max_fd, control_fd);
 
 	if (!compat20) {
 		/* enable nonblocking unless tty */
@@ -1054,6 +1235,9 @@
 		/* Buffer input from the connection.  */
 		client_process_net_input(readset);
 
+		/* Accept control connections.  */
+		client_process_control(readset);
+
 		if (quit_pending)
 			break;
 
@@ -1385,7 +1569,7 @@
 client_input_channel_req(int type, u_int32_t seq, void *ctxt)
 {
 	Channel *c = NULL;
-	int id, reply, success = 0;
+	int exitval, id, reply, success = 0;
 	char *rtype;
 
 	id = packet_get_int();
@@ -1395,18 +1579,21 @@
 	debug("client_input_channel_req: channel %d rtype %s reply %d",
 	    id, rtype, reply);
 
-	if (session_ident == -1) {
-		error("client_input_channel_req: no channel %d", session_ident);
-	} else if (id != session_ident) {
-		error("client_input_channel_req: channel %d: wrong channel: %d",
-		    session_ident, id);
-	}
 	c = channel_lookup(id);
 	if (c == NULL) {
 		error("client_input_channel_req: channel %d: unknown channel", id);
 	} else if (strcmp(rtype, "exit-status") == 0) {
-		success = 1;
-		exit_status = packet_get_int();
+		exitval = packet_get_int();
+		if (id == session_ident) {
+			success = 1;
+			exit_status = exitval;
+		} else if (c->ctl_fd == -1) {
+			error("client_input_channel_req: unexpected channel %d",
+			    session_ident);
+		} else {
+			atomicio(vwrite, c->ctl_fd, &exitval, sizeof(exitval));
+			success = 1;
+		}
 		packet_check_eom();
 	}
 	if (reply) {
@@ -1437,6 +1624,98 @@
 	xfree(rtype);
 }
 
+void
+client_session2_setup(int id, int want_tty, int want_subsystem, 
+    const char *term, struct termios *tiop, int in_fd, Buffer *cmd, 
+    dispatch_fn *subsys_repl)
+{
+	int len;
+
+	debug2("%s: id %d", __func__, id);
+
+	if (want_tty) {
+		struct winsize ws;
+		struct termios tio;
+
+		/* Store window size in the packet. */
+		if (ioctl(in_fd, TIOCGWINSZ, &ws) < 0)
+			memset(&ws, 0, sizeof(ws));
+
+		channel_request_start(id, "pty-req", 0);
+		packet_put_cstring(term != NULL ? term : "");
+		packet_put_int(ws.ws_col);
+		packet_put_int(ws.ws_row);
+		packet_put_int(ws.ws_xpixel);
+		packet_put_int(ws.ws_ypixel);
+		tio = get_saved_tio();
+		tty_make_modes(-1, tiop != NULL ? tiop : &tio);
+		packet_send();
+		/* XXX wait for reply */
+	}
+
+	/* Transfer any environment variables from client to server */
+	if (options.num_send_env != 0) {
+		int i, j, matched;
+		extern char **environ;
+		char *name, *val;
+
+		debug("Sending environment.");
+		for (i = 0; environ && environ[i] != NULL; i++) {
+			/* Split */
+			name = xstrdup(environ[i]);
+			if ((val = strchr(name, '=')) == NULL) {
+				free(name);
+				continue;
+			}
+			*val++ = '\0';
+
+			matched = 0;
+			for (j = 0; j < options.num_send_env; j++) {
+				if (match_pattern(name, options.send_env[j])) {
+					matched = 1;
+					break;
+				}
+			}
+			if (!matched) {
+				debug3("Ignored env %s", name);
+				free(name);
+				continue;
+			}
+
+			debug("Sending env %s = %s", name, val);
+			channel_request_start(id, "env", 0);
+			packet_put_cstring(name);
+			packet_put_cstring(val);
+			packet_send();
+			free(name);
+		}
+	}
+
+	len = buffer_len(cmd);
+	if (len > 0) {
+		if (len > 900)
+			len = 900;
+		if (want_subsystem) {
+			debug("Sending subsystem: %.*s", len, (u_char*)buffer_ptr(cmd));
+			channel_request_start(id, "subsystem", subsys_repl != NULL);
+			if (subsys_repl != NULL) {
+				/* register callback for reply */
+				/* XXX we assume that client_loop has already been called */
+				dispatch_set(SSH2_MSG_CHANNEL_FAILURE, subsys_repl);
+				dispatch_set(SSH2_MSG_CHANNEL_SUCCESS, subsys_repl);
+			}
+		} else {
+			debug("Sending command: %.*s", len, (u_char*)buffer_ptr(cmd));
+			channel_request_start(id, "exec", 0);
+		}
+		packet_put_string(buffer_ptr(cmd), buffer_len(cmd));
+		packet_send();
+	} else {
+		channel_request_start(id, "shell", 0);
+		packet_send();
+	}
+}
+
 static void
 client_init_dispatch_20(void)
 {
@@ -1503,5 +1782,7 @@
 {
 	leave_raw_mode();
 	leave_non_blocking();
+	if (options.control_path != NULL && control_fd != -1)
+		unlink(options.control_path);
 	_exit(i);
 }