/* * ldisc.c: PuTTY line discipline. Sits between the input coming * from keypresses in the window, and the output channel leading to * the back end. Implements echo and/or local line editing, * depending on what's currently configured. */ #include #include #include #include "putty.h" #include "terminal.h" struct Ldisc_tag { Terminal *term; Backend *backend; Seat *seat; /* * When the backend is not reporting true from sendok(), terminal * input that comes here is stored in this bufchain instead. When * the backend later decides it wants session input, we empty the * queue in ldisc_check_sendok_callback(), passing its contents on * to the backend. Before then, we also provide data from this * queue to term_get_userpass_input() via ldisc_get_input_token(), * to be interpreted as user responses to username and password * prompts during authentication. * * Unfortunately, the data stored in this queue is not all of the * same type: our output to the backend consists of both raw bytes * sent to backend_send(), and also session specials such as * SS_EOL and SS_EC. So we have to encode our queued data in a way * that can represent both. * * The encoding is private to this source file, so we can change * it if necessary and only have to worry about the encode and * decode functions here. Currently, it is: * * - Bytes other than 0xFF are stored literally. * - The byte 0xFF itself is stored as 0xFF 0xFF. * - A session special (code, arg) is stored as 0xFF, followed by * a big-endian 4-byte integer containing code, followed by * another big-endian 4-byte integer containing arg. * * (This representation relies on session special codes being at * most 0xFEFFFFFF when represented in 32 bits, so that the first * byte of the 'code' integer can't be confused with the 0xFF * followup byte indicating a literal 0xFF, But since session * special codes are defined by an enum counting up from zero, and * there are only a couple of dozen of them, that shouldn't be a * problem! Even so, just in case, an assertion checks that at * encode time.) */ bufchain input_queue; IdempotentCallback input_queue_callback; prompts_t *prompts; /* * Values cached out of conf. */ bool telnet_keyboard, telnet_newline; int protocol, localecho, localedit; char *buf; size_t buflen, bufsiz; bool quotenext; }; #define ECHOING (ldisc->localecho == FORCE_ON || \ (ldisc->localecho == AUTO && \ (backend_ldisc_option_state(ldisc->backend, LD_ECHO)))) #define EDITING (ldisc->localedit == FORCE_ON || \ (ldisc->localedit == AUTO && \ (backend_ldisc_option_state(ldisc->backend, LD_EDIT)))) static void c_write(Ldisc *ldisc, const void *buf, int len) { seat_stdout(ldisc->seat, buf, len); } static int plen(Ldisc *ldisc, unsigned char c) { if ((c >= 32 && c <= 126) || (c >= 160 && !in_utf(ldisc->term))) return 1; else if (c < 128) return 2; /* ^x for some x */ else if (in_utf(ldisc->term) && c >= 0xC0) return 1; /* UTF-8 introducer character * (FIXME: combining / wide chars) */ else if (in_utf(ldisc->term) && c >= 0x80 && c < 0xC0) return 0; /* UTF-8 followup character */ else return 4; /* hex representation */ } static void pwrite(Ldisc *ldisc, unsigned char c) { if ((c >= 32 && c <= 126) || (!in_utf(ldisc->term) && c >= 0xA0) || (in_utf(ldisc->term) && c >= 0x80)) { c_write(ldisc, &c, 1); } else if (c < 128) { char cc[2]; cc[1] = (c == 127 ? '?' : c + 0x40); cc[0] = '^'; c_write(ldisc, cc, 2); } else { char cc[5]; sprintf(cc, "<%02X>", c); c_write(ldisc, cc, 4); } } static bool char_start(Ldisc *ldisc, unsigned char c) { if (in_utf(ldisc->term)) return (c < 0x80 || c >= 0xC0); else return true; } static void bsb(Ldisc *ldisc, int n) { while (n--) c_write(ldisc, "\010 \010", 3); } static void ldisc_input_queue_callback(void *ctx); #define CTRL(x) (x^'@') #define KCTRL(x) ((x^'@') | 0x100) Ldisc *ldisc_create(Conf *conf, Terminal *term, Backend *backend, Seat *seat) { Ldisc *ldisc = snew(Ldisc); ldisc->buf = NULL; ldisc->buflen = 0; ldisc->bufsiz = 0; ldisc->quotenext = false; ldisc->backend = backend; ldisc->term = term; ldisc->seat = seat; bufchain_init(&ldisc->input_queue); ldisc->prompts = NULL; ldisc->input_queue_callback.fn = ldisc_input_queue_callback; ldisc->input_queue_callback.ctx = ldisc; ldisc->input_queue_callback.queued = false; bufchain_set_callback(&ldisc->input_queue, &ldisc->input_queue_callback); ldisc_configure(ldisc, conf); /* Link ourselves into the backend and the terminal */ if (term) term->ldisc = ldisc; if (backend) backend_provide_ldisc(backend, ldisc); return ldisc; } void ldisc_configure(Ldisc *ldisc, Conf *conf) { ldisc->telnet_keyboard = conf_get_bool(conf, CONF_telnet_keyboard); ldisc->telnet_newline = conf_get_bool(conf, CONF_telnet_newline); ldisc->protocol = conf_get_int(conf, CONF_protocol); ldisc->localecho = conf_get_int(conf, CONF_localecho); ldisc->localedit = conf_get_int(conf, CONF_localedit); } void ldisc_free(Ldisc *ldisc) { bufchain_clear(&ldisc->input_queue); if (ldisc->term) ldisc->term->ldisc = NULL; if (ldisc->backend) backend_provide_ldisc(ldisc->backend, NULL); if (ldisc->buf) sfree(ldisc->buf); if (ldisc->prompts && ldisc->prompts->ldisc_ptr_to_us == &ldisc->prompts) ldisc->prompts->ldisc_ptr_to_us = NULL; delete_callbacks_for_context(ldisc); sfree(ldisc); } void ldisc_echoedit_update(Ldisc *ldisc) { seat_echoedit_update(ldisc->seat, ECHOING, EDITING); } void ldisc_enable_prompt_callback(Ldisc *ldisc, prompts_t *prompts) { /* * Called by the terminal to indicate that there's a prompts_t * currently in flight, or to indicate that one has just finished * (by passing NULL). When ldisc->prompts is not null, we notify * the terminal whenever new data arrives in our input queue, so * that it can continue the interactive prompting process. */ ldisc->prompts = prompts; if (prompts) ldisc->prompts->ldisc_ptr_to_us = &ldisc->prompts; } static void ldisc_input_queue_callback(void *ctx) { /* * Toplevel callback that is triggered whenever the input queue * lengthens. If we're currently processing an interactive prompt, * we call back the Terminal to tell it to do some more stuff with * that prompt based on the new input. */ Ldisc *ldisc = (Ldisc *)ctx; if (ldisc->term && ldisc->prompts) { /* * The integer return value from this call is discarded, * because we have no channel to pass it on to the backend * that originally wanted it. But that's OK, because if the * return value is >= 0 (that is, the prompts are either * completely filled in, or aborted by the user), then the * terminal will notify the callback in the prompts_t, and * when that calls term_get_userpass_input again, it will * return the same answer again. */ term_get_userpass_input(ldisc->term, ldisc->prompts); } } static void ldisc_to_backend_raw( Ldisc *ldisc, const void *vbuf, size_t len) { if (backend_sendok(ldisc->backend)) { backend_send(ldisc->backend, vbuf, len); } else { const char *buf = (const char *)vbuf; while (len > 0) { /* * Encode raw data in input_queue, by storing large chunks * as long as they don't include 0xFF, and pausing every * time they do to escape it. */ const char *ff = memchr(buf, '\xFF', len); size_t this_len = ff ? ff - buf : len; if (this_len > 0) { bufchain_add(&ldisc->input_queue, buf, len); } else { bufchain_add(&ldisc->input_queue, "\xFF\xFF", 2); this_len = 1; } buf += this_len; len -= this_len; } } } static void ldisc_to_backend_special( Ldisc *ldisc, SessionSpecialCode code, int arg) { if (backend_sendok(ldisc->backend)) { backend_special(ldisc->backend, code, arg); } else { /* * Encode a session special in input_queue. */ unsigned char data[9]; data[0] = 0xFF; PUT_32BIT_MSB_FIRST(data+1, code); PUT_32BIT_MSB_FIRST(data+5, arg); assert(data[1] != 0xFF && "SessionSpecialCode encoding collides with FF FF escape"); bufchain_add(&ldisc->input_queue, data, 9); } } bool ldisc_has_input_buffered(Ldisc *ldisc) { return bufchain_size(&ldisc->input_queue) > 0; } LdiscInputToken ldisc_get_input_token(Ldisc *ldisc) { assert(bufchain_size(&ldisc->input_queue) > 0 && "You're not supposed to call this unless there is buffered input!"); LdiscInputToken tok; char c; bufchain_fetch_consume(&ldisc->input_queue, &c, 1); if (c != '\xFF') { /* A literal non-FF byte */ tok.is_special = false; tok.chr = c; return tok; } else { char data[8]; /* See if the byte after the FF is also FF, indicating a literal FF */ bufchain_fetch_consume(&ldisc->input_queue, data, 1); if (data[0] == '\xFF') { tok.is_special = false; tok.chr = '\xFF'; return tok; } /* If not, get the rest of an 8-byte chunk and decode a special */ bufchain_fetch_consume(&ldisc->input_queue, data+1, 7); tok.is_special = true; tok.code = GET_32BIT_MSB_FIRST(data); tok.arg = toint(GET_32BIT_MSB_FIRST(data+4)); return tok; } } static void ldisc_check_sendok_callback(void *ctx) { Ldisc *ldisc = (Ldisc *)ctx; if (!(ldisc->backend && backend_sendok(ldisc->backend))) return; /* * Flush the ldisc input queue into the backend, which is now * willing to receive the data. */ while (bufchain_size(&ldisc->input_queue) > 0) { /* * Process either a chunk of non-special data, or an FF * escape, depending on whether the first thing we see is an * FF byte. */ ptrlen data = bufchain_prefix(&ldisc->input_queue); const char *ff = memchr(data.ptr, '\xFF', data.len); if (ff != data.ptr) { /* Send a maximal block of data not containing any * difficult bytes. */ if (ff) data.len = ff - (const char *)data.ptr; backend_send(ldisc->backend, data.ptr, data.len); bufchain_consume(&ldisc->input_queue, data.len); } else { /* Decode either a special or an escaped FF byte. The * easiest way to do this is to reuse the decoding code * already in ldisc_get_input_token. */ LdiscInputToken tok = ldisc_get_input_token(ldisc); if (tok.is_special) backend_special(ldisc->backend, tok.code, tok.arg); else backend_send(ldisc->backend, &tok.chr, 1); } } } void ldisc_check_sendok(Ldisc *ldisc) { queue_toplevel_callback(ldisc_check_sendok_callback, ldisc); } void ldisc_send(Ldisc *ldisc, const void *vbuf, int len, bool interactive) { const char *buf = (const char *)vbuf; int keyflag = 0; assert(ldisc->term); if (interactive) { /* * Interrupt a paste from the clipboard, if one was in * progress when the user pressed a key. This is easier than * buffering the current piece of data and saving it until the * terminal has finished pasting, and has the potential side * benefit of permitting a user to cancel an accidental huge * paste. */ term_nopaste(ldisc->term); } /* * Less than zero means null terminated special string. */ if (len < 0) { len = strlen(buf); keyflag = KCTRL('@'); } /* * Either perform local editing, or just send characters. */ if (EDITING) { while (len--) { int c; c = (unsigned char)(*buf++) + keyflag; if (!interactive && c == '\r') c += KCTRL('@'); switch (ldisc->quotenext ? ' ' : c) { /* * ^h/^?: delete, and output BSBs, to return to * last character boundary (in UTF-8 mode this may * be more than one byte) * ^w: delete, and output BSBs, to return to last * space/nonspace boundary * ^u: delete, and output BSBs, to return to BOL * ^c: Do a ^u then send a telnet IP * ^z: Do a ^u then send a telnet SUSP * ^\: Do a ^u then send a telnet ABORT * ^r: echo "^R\n" and redraw line * ^v: quote next char * ^d: if at BOL, end of file and close connection, * else send line and reset to BOL * ^m: send line-plus-\r\n and reset to BOL */ case KCTRL('H'): case KCTRL('?'): /* backspace/delete */ if (ldisc->buflen > 0) { do { if (ECHOING) bsb(ldisc, plen(ldisc, ldisc->buf[ldisc->buflen - 1])); ldisc->buflen--; } while (!char_start(ldisc, ldisc->buf[ldisc->buflen])); } break; case CTRL('W'): /* delete word */ while (ldisc->buflen > 0) { if (ECHOING) bsb(ldisc, plen(ldisc, ldisc->buf[ldisc->buflen - 1])); ldisc->buflen--; if (ldisc->buflen > 0 && isspace((unsigned char)ldisc->buf[ldisc->buflen-1]) && !isspace((unsigned char)ldisc->buf[ldisc->buflen])) break; } break; case CTRL('U'): /* delete line */ case CTRL('C'): /* Send IP */ case CTRL('\\'): /* Quit */ case CTRL('Z'): /* Suspend */ while (ldisc->buflen > 0) { if (ECHOING) bsb(ldisc, plen(ldisc, ldisc->buf[ldisc->buflen - 1])); ldisc->buflen--; } ldisc_to_backend_special(ldisc, SS_EL, 0); /* * We don't send IP, SUSP or ABORT if the user has * configured telnet specials off! This breaks * talkers otherwise. */ if (!ldisc->telnet_keyboard) goto default_case; if (c == CTRL('C')) ldisc_to_backend_special(ldisc, SS_IP, 0); if (c == CTRL('Z')) ldisc_to_backend_special(ldisc, SS_SUSP, 0); if (c == CTRL('\\')) ldisc_to_backend_special(ldisc, SS_ABORT, 0); break; case CTRL('R'): /* redraw line */ if (ECHOING) { int i; c_write(ldisc, "^R\r\n", 4); for (i = 0; i < ldisc->buflen; i++) pwrite(ldisc, ldisc->buf[i]); } break; case CTRL('V'): /* quote next char */ ldisc->quotenext = true; break; case CTRL('D'): /* logout or send */ if (ldisc->buflen == 0) { ldisc_to_backend_special(ldisc, SS_EOF, 0); } else { ldisc_to_backend_raw(ldisc, ldisc->buf, ldisc->buflen); ldisc->buflen = 0; } break; /* * This particularly hideous bit of code from RDB * allows ordinary ^M^J to do the same thing as * magic-^M when in Raw protocol. The line `case * KCTRL('M'):' is _inside_ the if block. Thus: * * - receiving regular ^M goes straight to the * default clause and inserts as a literal ^M. * - receiving regular ^J _not_ directly after a * literal ^M (or not in Raw protocol) fails the * if condition, leaps to the bottom of the if, * and falls through into the default clause * again. * - receiving regular ^J just after a literal ^M * in Raw protocol passes the if condition, * deletes the literal ^M, and falls through * into the magic-^M code * - receiving a magic-^M empties the line buffer, * signals end-of-line in one of the various * entertaining ways, and _doesn't_ fall out of * the bottom of the if and through to the * default clause because of the break. */ case CTRL('J'): if (ldisc->protocol == PROT_RAW && ldisc->buflen > 0 && ldisc->buf[ldisc->buflen - 1] == '\r') { if (ECHOING) bsb(ldisc, plen(ldisc, ldisc->buf[ldisc->buflen - 1])); ldisc->buflen--; /* FALLTHROUGH */ case KCTRL('M'): /* send with newline */ if (ldisc->buflen > 0) ldisc_to_backend_raw(ldisc, ldisc->buf, ldisc->buflen); if (ldisc->protocol == PROT_RAW) ldisc_to_backend_raw(ldisc, "\r\n", 2); else if (ldisc->protocol == PROT_TELNET && ldisc->telnet_newline) ldisc_to_backend_special(ldisc, SS_EOL, 0); else ldisc_to_backend_raw(ldisc, "\r", 1); if (ECHOING) c_write(ldisc, "\r\n", 2); ldisc->buflen = 0; break; } /* FALLTHROUGH */ default: /* get to this label from ^V handler */ default_case: sgrowarray(ldisc->buf, ldisc->bufsiz, ldisc->buflen); ldisc->buf[ldisc->buflen++] = c; if (ECHOING) pwrite(ldisc, (unsigned char) c); ldisc->quotenext = false; break; } } } else { if (ldisc->buflen != 0) { ldisc_to_backend_raw(ldisc, ldisc->buf, ldisc->buflen); while (ldisc->buflen > 0) { bsb(ldisc, plen(ldisc, ldisc->buf[ldisc->buflen - 1])); ldisc->buflen--; } } if (len > 0) { if (ECHOING) c_write(ldisc, buf, len); if (keyflag && ldisc->protocol == PROT_TELNET && len == 1) { switch (buf[0]) { case CTRL('M'): if (ldisc->protocol == PROT_TELNET && ldisc->telnet_newline) ldisc_to_backend_special(ldisc, SS_EOL, 0); else ldisc_to_backend_raw(ldisc, "\r", 1); break; case CTRL('?'): case CTRL('H'): if (ldisc->telnet_keyboard) { ldisc_to_backend_special(ldisc, SS_EC, 0); break; } case CTRL('C'): if (ldisc->telnet_keyboard) { ldisc_to_backend_special(ldisc, SS_IP, 0); break; } case CTRL('Z'): if (ldisc->telnet_keyboard) { ldisc_to_backend_special(ldisc, SS_SUSP, 0); break; } default: ldisc_to_backend_raw(ldisc, buf, len); break; } } else ldisc_to_backend_raw(ldisc, buf, len); } } }