[prev in list] [next in list] [prev in thread] [next in thread] 

List:       openssh-unix-dev
Subject:    cooked mode sessions
From:       Howard Chu <hyc () symas ! com>
Date:       2010-06-14 23:10:10
Message-ID: 4C16B6D2.1060701 () symas ! com
[Download RAW message or body]

Picking up on a couple really old threads (e.g. 
http://osdir.com/ml/ietf.secsh/2001-09/msg00003.html ) I've finally gotten 
around to this. The EXTPROC support on Linux is missing, but you can find 
kernel patches for that here

http://lkml.org/lkml/2010/6/11/403

I've also fixed up the netkit telnet / telnetd code to work with EXTPROC / 
LINEMODE on Linux, those patches are here

http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=585527

These ssh patches are still not even half-baked, just a proof of concept to 
get feedback and guidance on what the right approach actually is. To get an 
idea of where it's coming from you should read RFC1184 which gives the Telnet 
LINEMODE spec. Since the ssh protocol doesn't have the Do/Dont/Will/Wont 
option negotiation of Telnet things are of course a bit different here.

Instead of explicit negotiation, the server assumes the client supports 
linemode if it includes an EXTPROC bit in its tty modes. I've added a new 
"tty-changed" channel message as well, for the server to send tty mode changes 
back to the client. The client will assume the server supports linemode if it 
sends tty-changed messages.

If the server reports that the session tty is in cooked mode (ICANON|ECHO) 
then the client will use the readline library to process input. This opens the 
possibility of doing full local editing with command history on the client. 
(Though I haven't enabled history yet in this patch.)

So far this is only working as expected for dumb programs that don't try to 
manipulate the tty modes. I'm working on some patches to the readline library 
so that it will leave the tty in cooked mode if it detects that EXTPROC is set 
on the tty. So a remote bash shell will defer all input processing to the 
local client. Will also be able to support command completion, if the tty 
session has VEOL set to <TAB>.

Right now the tty mode handling on the client is a mess, it will need to be 
rationalized somehow to work cleanly with older raw-mode-only servers along 
with the linemode servers.

Feedback and help would be greatly welcomed.
-- 
   -- Howard Chu
   CTO, Symas Corp.           http://www.symas.com
   Director, Highland Sun     http://highlandsun.com/hyc/
   Chief Architect, OpenLDAP  http://www.openldap.org/project/

["dif1.txt" (text/plain)]

diff --git a/clientloop.c b/clientloop.c
index 76de372..2f2424a 100644
--- a/clientloop.c
+++ b/clientloop.c
@@ -85,6 +85,7 @@
 #include <termios.h>
 #include <pwd.h>
 #include <unistd.h>
+#include <readline/readline.h>
 
 #include "openbsd-compat/sys-queue.h"
 #include "xmalloc.h"
@@ -161,6 +162,8 @@ static int connection_out;	/* Connection to server (output). */
 static int need_rekeying;	/* Set to non-zero if rekeying is requested. */
 static int session_closed = 0;	/* In SSH2: login session closed. */
 
+static int got_prompt;
+
 static void client_init_dispatch(void);
 int	session_ident = -1;
 
@@ -1277,14 +1280,52 @@ client_filter_cleanup(int cid, void *ctx)
 	xfree(ctx);
 }
 
+static Buffer *rl_buf;
+static void readline_cb(char *line)
+{
+	if (!line) {
+		buffer_put_char(rl_buf, '\004');
+	} else {
+		buffer_append(rl_buf, line, strlen(line));
+		buffer_put_char(rl_buf, '\n');
+	}
+	got_prompt = 0;
+}
+
 int
 client_simple_escape_filter(Channel *c, char *buf, int len)
 {
+	struct termios *tio;
+	int ret, oldlen;
 	if (c->extended_usage != CHAN_EXTENDED_WRITE)
 		return 0;
 
-	return process_escapes(c, &c->input, &c->output, &c->extended,
+	oldlen = c->input.end - c->input.offset;
+	ret = process_escapes(c, &c->input, &c->output, &c->extended,
 	    buf, len);
+	if (ret < 0)
+		return ret;
+	tio = get_saved_tio();
+	/* If we're canonical, use readline */
+	if ((tio->c_lflag & (ECHO|ICANON)) == (ECHO|ICANON)) {
+		if (!got_prompt) {
+			char *ptr = c->output.buf + c->output.end;
+			c->output.buf[c->output.end] = 0;
+			while (ptr >= c->output.buf && *ptr != '\n')
+				ptr--;
+			rl_set_prompt(ptr+1);
+			rl_already_prompted = 1;
+			got_prompt = 1;
+		}
+		len = c->input.end - c->input.offset - oldlen;
+		c->input.end -= len;
+		memcpy(buf, c->input.buf + c->input.end, len);
+		while (len) {
+			rl_stuff_char(*buf++);
+			rl_callback_read_char();
+			len--;
+		}
+	}
 }
 
 static void
@@ -1863,6 +1904,17 @@ client_input_channel_req(int type, u_int32_t seq, void *ctxt)
 			    __func__, id);
 		}
 		packet_check_eom();
+	} else if (strcmp(rtype, "tty-change") == 0 ) {
+		ttyext tx = { fileno(stdin), 0, get_saved_tio() };
+		int nbytes;
+		tty_parse_modes(&tx, &nbytes);
+		/* server supports passthru, just use cooked mode now */
+		cooked_mode();
+		if (!rl_buf) {
+			rl_callback_handler_install(NULL, readline_cb);
+			rl_already_prompted = 1;
+			rl_buf = &c->input;
+		}
 	}
 	if (reply) {
 		packet_start(success ?
@@ -1872,6 +1924,7 @@ client_input_channel_req(int type, u_int32_t seq, void *ctxt)
 	}
 	xfree(rtype);
 }
+
 static void
 client_input_global_request(int type, u_int32_t seq, void *ctxt)
 {
diff --git a/packet.h b/packet.h
index 33523d7..9a94c95 100644
--- a/packet.h
+++ b/packet.h
@@ -87,8 +87,15 @@ int	 packet_remaining(void);
 void	 packet_send_ignore(int);
 void	 packet_add_padding(u_char);
 
+typedef struct ttyext {
+	int ttyfd;
+	int have_extproc;
+	struct termios *tio;
+} ttyext;
+
 void	 tty_make_modes(int, struct termios *);
-void	 tty_parse_modes(int, int *);
+void	 tty_parse_modes(ttyext *, int *);
+void	 tty_new_modes(void *old, char *buf, int len, int all);
 
 void	 packet_set_alive_timeouts(int);
 int	 packet_inc_alive_timeouts(void);
diff --git a/session.c b/session.c
index b49d08d..2fe48e7 100644
--- a/session.c
+++ b/session.c
@@ -43,6 +43,7 @@
 #include <sys/socket.h>
 #include <sys/un.h>
 #include <sys/wait.h>
+#include <sys/ioctl.h>
 
 #include <arpa/inet.h>
 
@@ -253,6 +254,40 @@ auth_input_request_forwarding(struct passwd * pw)
 	return 0;
 }
 
+static int
+pty_pkt_filter(Channel *c, char *buf, int len)
+{
+	if (buf[0] & TIOCPKT_IOCTL) {
+		Session *s = c->filter_ctx;
+		/* read termios struct and send diff */
+		packet_start(SSH2_MSG_CHANNEL_REQUEST);
+		packet_put_int(c->remote_id);
+		packet_put_cstring("tty-change");
+		packet_put_char(0);
+		tty_new_modes(s->termios, buf+1, len-1, !s->set_modes);
+		packet_send();
+		s->set_modes = 1;
+		len = 1;
+	}
+	if (buf[0] & TIOCPKT_FLUSHWRITE) {
+		packet_write_wait();
+	}
+	if (buf[0] & (TIOCPKT_NOSTOP|TIOCPKT_DOSTOP)) {
+		/* send xon-xoff msg */
+		packet_start(SSH2_MSG_CHANNEL_REQUEST);
+		packet_put_int(c->remote_id);
+		packet_put_cstring("xon-xoff");
+		packet_put_char(0);
+		packet_put_char((buf[0] & TIOCPKT_DOSTOP) != 0);
+		packet_send();
+	}
+	buf++;
+	len--;
+	if (len)
+		buffer_append(&c->input, buf, len);
+	return 0;
+}
+
 static void
 display_loginmsg(void)
 {
@@ -767,7 +802,11 @@ do_exec_pty(Session *s, const char *command)
 	s->ptymaster = ptymaster;
 	packet_set_interactive(1);
 	if (compat20) {
+		pty_pkt_mode(ptyfd);
+		if (s->have_extproc)
+			pty_line_mode(ptyfd, 1);
 		session_set_fds(s, ptyfd, fdout, -1, 1);
+		channel_register_filter(s->chanid, pty_pkt_filter, NULL, NULL, s);
 	} else {
 		server_loop(pid, ptyfd, fdout, -1);
 		/* server_loop _has_ closed ptyfd and fdout. */
@@ -2141,7 +2180,12 @@ session_pty_req(Session *s)
 	/* for SSH1 the tty modes length is not given */
 	if (!compat20)
 		n_bytes = packet_remaining();
-	tty_parse_modes(s->ttyfd, &n_bytes);
+	{
+		ttyext tx = { s->ttyfd, 0, NULL };
+		tty_parse_modes(&tx, &n_bytes);
+		s->have_extproc = tx.have_extproc;
+		s->termios = tx.tio;
+	}
 
 	if (!use_privsep)
 		pty_setowner(s->pw, s->tty);
diff --git a/session.h b/session.h
index cbb8e3a..6d72ab2 100644
--- a/session.h
+++ b/session.h
@@ -39,8 +39,11 @@ struct Session {
 	/* tty */
 	char	*term;
 	int	ptyfd, ttyfd, ptymaster;
+	int	have_extproc;
+	int set_modes;
 	u_int	row, col, xpixel, ypixel;
 	char	tty[TTYSZ];
+	void	*termios;
 
 	/* X11 */
 	u_int	display_number;
diff --git a/sshpty.c b/sshpty.c
index bbbc0fe..c5a081b 100644
--- a/sshpty.c
+++ b/sshpty.c
@@ -256,3 +256,18 @@ pty_setowner(struct passwd *pw, const char *tty)
 		}
 	}
 }
+
+void pty_line_mode(int fd, int on) {
+	struct termios tio;
+	tcgetattr(fd, &tio);
+	if (on) tio.c_lflag |= EXTPROC;
+	else tio.c_lflag &= ~EXTPROC;
+	tcsetattr(fd, TCSANOW, &tio);
+	debug("pty linemode %d\n", on);
+}
+
+void pty_pkt_mode(int fd) {
+	int on = 1;
+	ioctl(fd, TIOCPKT, (char *)&on);
+	debug("pty pktmode\n");
+}
diff --git a/sshpty.h b/sshpty.h
index cfa3224..0a2fb3e 100644
--- a/sshpty.h
+++ b/sshpty.h
@@ -20,6 +20,9 @@ struct termios *get_saved_tio(void);
 void	 leave_raw_mode(int);
 void	 enter_raw_mode(int);
 
+void pty_line_mode(int, int);
+void pty_pkt_mode(int);
+
 int	 pty_allocate(int *, int *, char *, size_t);
 void	 pty_release(const char *);
 void	 pty_make_controlling_tty(int *, const char *);
diff --git a/sshtty.c b/sshtty.c
index d214ce3..11580b9 100644
--- a/sshtty.c
+++ b/sshtty.c
@@ -45,7 +45,10 @@
 #include "sshpty.h"
 
 static struct termios _saved_tio;
+static struct termios _orig_tio;
 static int _in_raw_mode = 0;
+static int _saved_orig = 0;
+static int _cooked = 0;
 
 struct termios *
 get_saved_tio(void)
@@ -58,7 +61,7 @@ leave_raw_mode(int quiet)
 {
 	if (!_in_raw_mode)
 		return;
-	if (tcsetattr(fileno(stdin), TCSADRAIN, &_saved_tio) == -1) {
+	if (tcsetattr(fileno(stdin), TCSADRAIN, &_orig_tio) == -1) {
 		if (!quiet)
 			perror("tcsetattr");
 	} else
@@ -70,12 +73,18 @@ enter_raw_mode(int quiet)
 {
 	struct termios tio;
 
+	if (_cooked) return;
+
 	if (tcgetattr(fileno(stdin), &tio) == -1) {
 		if (!quiet)
 			perror("tcgetattr");
 		return;
 	}
 	_saved_tio = tio;
+	if (!_saved_orig) {
+		_saved_orig = 1;
+		_orig_tio = _saved_tio;
+	}
 	tio.c_iflag |= IGNPAR;
 	tio.c_iflag &= ~(ISTRIP | INLCR | IGNCR | ICRNL | IXON | IXANY | IXOFF);
 #ifdef IUCLC
@@ -94,3 +103,9 @@ enter_raw_mode(int quiet)
 	} else
 		_in_raw_mode = 1;
 }
+
+void
+cooked_mode()
+{
+	_cooked = 1;
+}
diff --git a/ttymodes.c b/ttymodes.c
index 6f51b8a..0659793 100644
--- a/ttymodes.c
+++ b/ttymodes.c
@@ -57,6 +57,7 @@
 #include "ssh1.h"
 #include "compat.h"
 #include "buffer.h"
+#include "xmalloc.h"
 
 #define TTY_OP_END		0
 /*
@@ -346,14 +347,15 @@ end:
  * manner from a packet being read.
  */
 void
-tty_parse_modes(int fd, int *n_bytes_ptr)
+tty_parse_modes(ttyext *tx, int *n_bytes_ptr)
 {
-	struct termios tio;
+	struct termios *tio;
 	int opcode, baud;
 	int n_bytes = 0;
 	int failure = 0;
 	u_int (*get_arg)(void);
 	int arg_size;
+	int fd = tx->ttyfd;
 
 	if (compat20) {
 		*n_bytes_ptr = packet_get_int();
@@ -366,19 +368,30 @@ tty_parse_modes(int fd, int *n_bytes_ptr)
 		arg_size = 1;
 	}
 
+	if (!tx->tio) {
+		tio = xmalloc(sizeof(struct termios));
+		tx->tio = tio;
+
 	/*
 	 * Get old attributes for the terminal.  We will modify these
 	 * flags. I am hoping that if there are any machine-specific
 	 * modes, they will initially have reasonable values.
 	 */
-	if (tcgetattr(fd, &tio) == -1) {
-		logit("tcgetattr: %.100s", strerror(errno));
-		failure = -1;
+		if (tcgetattr(fd, tio) == -1) {
+			logit("tcgetattr: %.100s", strerror(errno));
+			failure = -1;
+		}
+	} else {
+		tio = tx->tio;
 	}
 
 	for (;;) {
 		n_bytes += 1;
 		opcode = packet_get_char();
+#ifdef EXTPROC
+		if (opcode == 63)
+			tx->have_extproc = 1;
+#endif
 		switch (opcode) {
 		case TTY_OP_END:
 			goto set;
@@ -389,7 +402,7 @@ tty_parse_modes(int fd, int *n_bytes_ptr)
 			n_bytes += 4;
 			baud = packet_get_int();
 			if (failure != -1 &&
-			    cfsetispeed(&tio, baud_to_speed(baud)) == -1)
+			    cfsetispeed(tio, baud_to_speed(baud)) == -1)
 				error("cfsetispeed failed for %d", baud);
 			break;
 
@@ -399,22 +412,22 @@ tty_parse_modes(int fd, int *n_bytes_ptr)
 			n_bytes += 4;
 			baud = packet_get_int();
 			if (failure != -1 &&
-			    cfsetospeed(&tio, baud_to_speed(baud)) == -1)
+			    cfsetospeed(tio, baud_to_speed(baud)) == -1)
 				error("cfsetospeed failed for %d", baud);
 			break;
 
 #define TTYCHAR(NAME, OP) \
 	case OP: \
 	  n_bytes += arg_size; \
-	  tio.c_cc[NAME] = special_char_decode(get_arg()); \
+	  tio->c_cc[NAME] = special_char_decode(get_arg()); \
 	  break;
 #define TTYMODE(NAME, FIELD, OP) \
 	case OP: \
 	  n_bytes += arg_size; \
 	  if (get_arg()) \
-	    tio.FIELD |= NAME; \
+	    tio->FIELD |= NAME; \
 	  else \
-	    tio.FIELD &= ~NAME;	\
+	    tio->FIELD &= ~NAME;	\
 	  break;
 
 #include "ttymodes.h"
@@ -485,6 +498,54 @@ set:
 		return;		/* Packet parsed ok but tcgetattr() failed */
 
 	/* Set the new modes for the terminal. */
-	if (tcsetattr(fd, TCSANOW, &tio) == -1)
+	if (tcsetattr(fd, TCSANOW, tio) == -1)
 		logit("Setting tty modes failed: %.100s", strerror(errno));
 }
+
+/*
+ * Encodes difference between old and new terminal modes
+ * in a portable manner, and appends the modes to a packet
+ * being constructed.
+ */
+void
+tty_new_modes(void *old, char *cnew, int len, int all)
+{
+	struct termios *told = old, tnew = *told;
+	Buffer buf;
+	void (*put_arg)(Buffer *, u_int);
+
+	buffer_init(&buf);
+	if (compat20) {
+		put_arg = buffer_put_int;
+	} else {
+		put_arg = (void (*)(Buffer *, u_int)) buffer_put_char;
+	}
+
+	memcpy(&tnew, cnew, len);
+
+	/* Store values of changed mode flags. */
+#define TTYCHAR(NAME, OP) \
+	if (all || (told->c_cc[NAME] != tnew.c_cc[NAME])) { \
+		buffer_put_char(&buf, OP); \
+		put_arg(&buf, special_char_encode(tnew.c_cc[NAME])); }
+
+#define TTYMODE(NAME, FIELD, OP) \
+	if (all || (NAME != EXTPROC && (told->FIELD & NAME) != (tnew.FIELD & NAME))) { \
+		buffer_put_char(&buf, OP); \
+		put_arg(&buf, ((tnew.FIELD & NAME) != 0)); }
+
+#include "ttymodes.h"
+
+#undef TTYCHAR
+#undef TTYMODE
+
+end:
+	/* Mark end of mode data. */
+	buffer_put_char(&buf, TTY_OP_END);
+	if (compat20)
+		packet_put_string(buffer_ptr(&buf), buffer_len(&buf));
+	else
+		packet_put_raw(buffer_ptr(&buf), buffer_len(&buf));
+	buffer_free(&buf);
+	memcpy(told, &tnew, sizeof(told));
+}
diff --git a/ttymodes.h b/ttymodes.h
index 4d848fe..aa62c6e 100644
--- a/ttymodes.h
+++ b/ttymodes.h
@@ -151,6 +151,9 @@ TTYMODE(ECHOKE,	c_lflag, 61)
 #if defined(PENDIN)
 TTYMODE(PENDIN,	c_lflag, 62)
 #endif /* PENDIN */
+#if defined(EXTPROC)
+TTYMODE(EXTPROC,	c_lflag, 63)
+#endif /* EXTPROC */
 
 TTYMODE(OPOST,	c_oflag, 70)
 #if defined(OLCUC)


_______________________________________________
openssh-unix-dev mailing list
openssh-unix-dev@mindrot.org
https://lists.mindrot.org/mailman/listinfo/openssh-unix-dev


[prev in list] [next in list] [prev in thread] [next in thread] 

Configure | About | News | Add a list | Sponsored by KoreLogic