diff options
Diffstat (limited to 'ldisc.c')
-rw-r--r-- | ldisc.c | 277 |
1 files changed, 256 insertions, 21 deletions
@@ -11,7 +11,62 @@ #include "putty.h" #include "terminal.h" -#include "ldisc.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 && \ @@ -72,6 +127,8 @@ static void bsb(Ldisc *ldisc, int n) c_write(ldisc, "\010 \010", 3); } +static void ldisc_input_queue_callback(void *ctx); + #define CTRL(x) (x^'@') #define KCTRL(x) ((x^'@') | 0x100) @@ -88,6 +145,14 @@ Ldisc *ldisc_create(Conf *conf, Terminal *term, Backend *backend, Seat *seat) 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 */ @@ -110,12 +175,16 @@ void ldisc_configure(Ldisc *ldisc, Conf *conf) 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); } @@ -124,6 +193,173 @@ 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; @@ -206,7 +442,7 @@ void ldisc_send(Ldisc *ldisc, const void *vbuf, int len, bool interactive) bsb(ldisc, plen(ldisc, ldisc->buf[ldisc->buflen - 1])); ldisc->buflen--; } - backend_special(ldisc->backend, SS_EL, 0); + 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 @@ -215,11 +451,11 @@ void ldisc_send(Ldisc *ldisc, const void *vbuf, int len, bool interactive) if (!ldisc->telnet_keyboard) goto default_case; if (c == CTRL('C')) - backend_special(ldisc->backend, SS_IP, 0); + ldisc_to_backend_special(ldisc, SS_IP, 0); if (c == CTRL('Z')) - backend_special(ldisc->backend, SS_SUSP, 0); + ldisc_to_backend_special(ldisc, SS_SUSP, 0); if (c == CTRL('\\')) - backend_special(ldisc->backend, SS_ABORT, 0); + ldisc_to_backend_special(ldisc, SS_ABORT, 0); break; case CTRL('R'): /* redraw line */ if (ECHOING) { @@ -234,9 +470,9 @@ void ldisc_send(Ldisc *ldisc, const void *vbuf, int len, bool interactive) break; case CTRL('D'): /* logout or send */ if (ldisc->buflen == 0) { - backend_special(ldisc->backend, SS_EOF, 0); + ldisc_to_backend_special(ldisc, SS_EOF, 0); } else { - backend_send(ldisc->backend, ldisc->buf, ldisc->buflen); + ldisc_to_backend_raw(ldisc, ldisc->buf, ldisc->buflen); ldisc->buflen = 0; } break; @@ -272,14 +508,13 @@ void ldisc_send(Ldisc *ldisc, const void *vbuf, int len, bool interactive) /* FALLTHROUGH */ case KCTRL('M'): /* send with newline */ if (ldisc->buflen > 0) - backend_send(ldisc->backend, - ldisc->buf, ldisc->buflen); + ldisc_to_backend_raw(ldisc, ldisc->buf, ldisc->buflen); if (ldisc->protocol == PROT_RAW) - backend_send(ldisc->backend, "\r\n", 2); + ldisc_to_backend_raw(ldisc, "\r\n", 2); else if (ldisc->protocol == PROT_TELNET && ldisc->telnet_newline) - backend_special(ldisc->backend, SS_EOL, 0); + ldisc_to_backend_special(ldisc, SS_EOL, 0); else - backend_send(ldisc->backend, "\r", 1); + ldisc_to_backend_raw(ldisc, "\r", 1); if (ECHOING) c_write(ldisc, "\r\n", 2); ldisc->buflen = 0; @@ -287,7 +522,7 @@ void ldisc_send(Ldisc *ldisc, const void *vbuf, int len, bool interactive) } /* FALLTHROUGH */ default: /* get to this label from ^V handler */ - default_case: + default_case: sgrowarray(ldisc->buf, ldisc->bufsiz, ldisc->buflen); ldisc->buf[ldisc->buflen++] = c; if (ECHOING) @@ -298,7 +533,7 @@ void ldisc_send(Ldisc *ldisc, const void *vbuf, int len, bool interactive) } } else { if (ldisc->buflen != 0) { - backend_send(ldisc->backend, ldisc->buf, ldisc->buflen); + ldisc_to_backend_raw(ldisc, ldisc->buf, ldisc->buflen); while (ldisc->buflen > 0) { bsb(ldisc, plen(ldisc, ldisc->buf[ldisc->buflen - 1])); ldisc->buflen--; @@ -311,33 +546,33 @@ void ldisc_send(Ldisc *ldisc, const void *vbuf, int len, bool interactive) switch (buf[0]) { case CTRL('M'): if (ldisc->protocol == PROT_TELNET && ldisc->telnet_newline) - backend_special(ldisc->backend, SS_EOL, 0); + ldisc_to_backend_special(ldisc, SS_EOL, 0); else - backend_send(ldisc->backend, "\r", 1); + ldisc_to_backend_raw(ldisc, "\r", 1); break; case CTRL('?'): case CTRL('H'): if (ldisc->telnet_keyboard) { - backend_special(ldisc->backend, SS_EC, 0); + ldisc_to_backend_special(ldisc, SS_EC, 0); break; } case CTRL('C'): if (ldisc->telnet_keyboard) { - backend_special(ldisc->backend, SS_IP, 0); + ldisc_to_backend_special(ldisc, SS_IP, 0); break; } case CTRL('Z'): if (ldisc->telnet_keyboard) { - backend_special(ldisc->backend, SS_SUSP, 0); + ldisc_to_backend_special(ldisc, SS_SUSP, 0); break; } default: - backend_send(ldisc->backend, buf, len); + ldisc_to_backend_raw(ldisc, buf, len); break; } } else - backend_send(ldisc->backend, buf, len); + ldisc_to_backend_raw(ldisc, buf, len); } } } |