diff --git a/CMakeLists.txt b/CMakeLists.txt index 7da0cbc3..91f040d4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,7 +53,7 @@ add_library(agent STATIC add_library(guiterminal STATIC terminal/terminal.c terminal/bidi.c - ldisc.c config.c dialog.c + ldisc.c terminal/lineedit.c config.c dialog.c $) add_library(noterminal STATIC @@ -115,7 +115,8 @@ add_executable(bidi_test target_link_libraries(bidi_test guiterminal utils ${platform_libraries}) add_executable(plink - ${platform}/plink.c) + ${platform}/plink.c + stubs/no-lineedit.c) # Note: if we ever port Plink to a platform where we can't implement a # serial backend, this be_list command will need to become platform- # dependent, so that it only sets the SERIAL option on platforms where diff --git a/defs.h b/defs.h index d7e2f338..faff0482 100644 --- a/defs.h +++ b/defs.h @@ -115,6 +115,11 @@ typedef struct LogContext LogContext; typedef struct LogPolicy LogPolicy; typedef struct LogPolicyVtable LogPolicyVtable; +typedef struct TermLineEditor TermLineEditor; +typedef struct TermLineEditorCallbackReceiver TermLineEditorCallbackReceiver; +typedef struct TermLineEditorCallbackReceiverVtable + TermLineEditorCallbackReceiverVtable; + typedef struct Seat Seat; typedef struct SeatVtable SeatVtable; typedef struct SeatDialogText SeatDialogText; diff --git a/ldisc.c b/ldisc.c index 27f41a6f..899f5c82 100644 --- a/ldisc.c +++ b/ldisc.c @@ -12,50 +12,36 @@ #include "putty.h" #include "terminal.h" +typedef enum InputType { NORMAL, DEDICATED, NONINTERACTIVE } InputType; + +struct input_chunk { + struct input_chunk *next; + InputType type; + size_t size; +}; + 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. + * When the backend is not reporting true from sendok(), we must + * buffer the input received by ldisc_send(). It's stored in the + * bufchain below, together with a linked list of input_chunk + * blocks storing the extra metadata about special keys and + * interactivity that ldisc_send() receives. * - * 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.) + * All input is added to this buffer initially, but we then + * process as much of it as possible immediately and hand it off + * to the backend or a TermLineEditor. Anything left stays in this + * buffer until ldisc_check_sendok() is next called, triggering a + * run of the callback that tries again to process the queue. */ bufchain input_queue; + struct input_chunk *inchunk_head, *inchunk_tail; IdempotentCallback input_queue_callback; - prompts_t *prompts; /* * Values cached out of conf. @@ -63,9 +49,13 @@ struct Ldisc_tag { bool telnet_keyboard, telnet_newline; int protocol, localecho, localedit; - char *buf; - size_t buflen, bufsiz; - bool quotenext; + TermLineEditor *le; + TermLineEditorCallbackReceiver le_rcv; + + /* We get one of these communicated to us by + * term_get_userpass_input while it's reading a prompt, so that we + * can push data straight into it */ + TermLineEditor *userpass_le; }; #define ECHOING (ldisc->localecho == FORCE_ON || \ @@ -75,71 +65,16 @@ struct Ldisc_tag { (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); +static const TermLineEditorCallbackReceiverVtable ldisc_lineedit_receiver_vt; + #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; + memset(ldisc, 0, sizeof(Ldisc)); ldisc->backend = backend; ldisc->term = term; @@ -147,12 +82,15 @@ Ldisc *ldisc_create(Conf *conf, Terminal *term, Backend *backend, 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); + if (ldisc->term) { + ldisc->le_rcv.vt = &ldisc_lineedit_receiver_vt; + ldisc->le = lineedit_new(ldisc->term, 0, &ldisc->le_rcv); + } + ldisc_configure(ldisc, conf); /* Link ourselves into the backend and the terminal */ @@ -171,19 +109,28 @@ void ldisc_configure(Ldisc *ldisc, Conf *conf) ldisc->protocol = conf_get_int(conf, CONF_protocol); ldisc->localecho = conf_get_int(conf, CONF_localecho); ldisc->localedit = conf_get_int(conf, CONF_localedit); + + unsigned flags = 0; + if (ldisc->protocol == PROT_RAW) + flags |= LE_CRLF_NEWLINE; + if (ldisc->telnet_keyboard) + flags |= LE_INTERRUPT | LE_SUSPEND | LE_ABORT; + lineedit_modify_flags(ldisc->le, ~0U, flags); } void ldisc_free(Ldisc *ldisc) { bufchain_clear(&ldisc->input_queue); + while (ldisc->inchunk_head) { + struct input_chunk *oldhead = ldisc->inchunk_head; + ldisc->inchunk_head = ldisc->inchunk_head->next; + sfree(oldhead); + } + lineedit_free(ldisc->le); 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); } @@ -191,180 +138,119 @@ void ldisc_free(Ldisc *ldisc) void ldisc_echoedit_update(Ldisc *ldisc) { seat_echoedit_update(ldisc->seat, ECHOING, EDITING); + + /* + * If we've just turned off local line editing mode, and our + * TermLineEditor had a partial buffer, then send the contents of + * the buffer. Rationale: (a) otherwise you lose data; (b) the + * user quite likely typed the buffer contents _anticipating_ that + * local editing would be turned off shortly, and the event was + * slow arriving. + */ + if (!EDITING) + lineedit_send_line(ldisc->le); } -void ldisc_enable_prompt_callback(Ldisc *ldisc, prompts_t *prompts) +void ldisc_provide_userpass_le(Ldisc *ldisc, TermLineEditor *le) { /* - * 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. + * Called by term_get_userpass_input to tell us when it has its + * own TermLineEditor processing a password prompt, so that we can + * inject our input into that instead of putting it into our own + * TermLineEditor or sending it straight to the backend. */ - ldisc->prompts = prompts; - if (prompts) - ldisc->prompts->ldisc_ptr_to_us = &ldisc->prompts; + ldisc->userpass_le = le; } -static void ldisc_input_queue_callback(void *ctx) +static inline bool is_dedicated_byte(char c, InputType type) { - /* - * 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) { + switch (type) { + case DEDICATED: + return true; + case NORMAL: + return false; + case NONINTERACTIVE: /* - * 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. + * Non-interactive input (e.g. from a paste) doesn't come with + * the ability to distinguish dedicated keypresses like Return + * from generic ones like Ctrl+M. So we just have to make up + * an answer to this question. In particular, we _must_ treat + * Ctrl+M as the Return key, because that's the only way a + * newline can be pasted at all. */ - term_get_userpass_input(ldisc->term, ldisc->prompts); + return c == '\r'; + default: + unreachable("those values should be exhaustive"); } } -static void ldisc_to_backend_raw( - Ldisc *ldisc, const void *vbuf, size_t len) +static void ldisc_input_queue_consume(Ldisc *ldisc, size_t size) { - 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; + bufchain_consume(&ldisc->input_queue, size); + while (size > 0) { + size_t thissize = (size < ldisc->inchunk_head->size ? + size : ldisc->inchunk_head->size); + ldisc->inchunk_head->size -= thissize; + size -= thissize; + + if (!ldisc->inchunk_head->size) { + struct input_chunk *oldhead = ldisc->inchunk_head; + ldisc->inchunk_head = ldisc->inchunk_head->next; + if (!ldisc->inchunk_head) + ldisc->inchunk_tail = NULL; + sfree(oldhead); } } } -static void ldisc_to_backend_special( - Ldisc *ldisc, SessionSpecialCode code, int arg) +static void ldisc_lineedit_to_terminal( + TermLineEditorCallbackReceiver *rcv, ptrlen data) { - 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); - } + Ldisc *ldisc = container_of(rcv, Ldisc, le_rcv); + if (ECHOING) + seat_stdout(ldisc->seat, data.ptr, data.len); } -bool ldisc_has_input_buffered(Ldisc *ldisc) +static void ldisc_lineedit_to_backend( + TermLineEditorCallbackReceiver *rcv, ptrlen data) { - return bufchain_size(&ldisc->input_queue) > 0; + Ldisc *ldisc = container_of(rcv, Ldisc, le_rcv); + backend_send(ldisc->backend, data.ptr, data.len); } -LdiscInputToken ldisc_get_input_token(Ldisc *ldisc) +static void ldisc_lineedit_special( + TermLineEditorCallbackReceiver *rcv, SessionSpecialCode code, int arg) { - 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; - } + Ldisc *ldisc = container_of(rcv, Ldisc, le_rcv); + backend_special(ldisc->backend, code, arg); } -static void ldisc_check_sendok_callback(void *ctx) +static void ldisc_lineedit_newline(TermLineEditorCallbackReceiver *rcv) { - 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); - } - } + Ldisc *ldisc = container_of(rcv, Ldisc, le_rcv); + if (ldisc->protocol == PROT_RAW) + backend_send(ldisc->backend, "\r\n", 2); + else if (ldisc->protocol == PROT_TELNET && ldisc->telnet_newline) + backend_special(ldisc->backend, SS_EOL, 0); + else + backend_send(ldisc->backend, "\r", 1); } +static const TermLineEditorCallbackReceiverVtable +ldisc_lineedit_receiver_vt = { + .to_terminal = ldisc_lineedit_to_terminal, + .to_backend = ldisc_lineedit_to_backend, + .special = ldisc_lineedit_special, + .newline = ldisc_lineedit_newline, +}; + void ldisc_check_sendok(Ldisc *ldisc) { - queue_toplevel_callback(ldisc_check_sendok_callback, ldisc); + queue_idempotent_callback(&ldisc->input_queue_callback); } 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) { @@ -379,202 +265,124 @@ void ldisc_send(Ldisc *ldisc, const void *vbuf, int len, bool interactive) term_nopaste(ldisc->term); } - /* - * Less than zero means null terminated special string. - */ + InputType type; 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--; - } - if (c == CTRL('U')) - break; /* ^U *just* erases a line */ - 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; - } - } + /* + * Less than zero means null terminated special string. + */ + len = strlen(vbuf); + type = DEDICATED; } 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; - } + type = interactive ? NORMAL : NONINTERACTIVE; + } - default: - ldisc_to_backend_raw(ldisc, buf, len); - break; + /* + * Append our data to input_queue, and ensure it's marked with the + * right type. + */ + bufchain_add(&ldisc->input_queue, vbuf, len); + if (!(ldisc->inchunk_tail && ldisc->inchunk_tail->type == type)) { + struct input_chunk *new_chunk = snew(struct input_chunk); + + new_chunk->type = type; + new_chunk->size = 0; + + new_chunk->next = NULL; + if (ldisc->inchunk_tail) + ldisc->inchunk_tail->next = new_chunk; + else + ldisc->inchunk_head = new_chunk; + ldisc->inchunk_tail = new_chunk; + } + ldisc->inchunk_tail->size += len; + + /* + * And process as much of the data immediately as we can. + */ + ldisc_input_queue_callback(ldisc); +} + +static void ldisc_input_queue_callback(void *ctx) +{ + Ldisc *ldisc = (Ldisc *)ctx; + + /* + * Toplevel callback that is triggered whenever the input queue + * lengthens. + */ + while (bufchain_size(&ldisc->input_queue)) { + ptrlen pl = bufchain_prefix(&ldisc->input_queue); + const char *start = pl.ptr, *buf = pl.ptr; + size_t len = (pl.len < ldisc->inchunk_head->size ? + pl.len : ldisc->inchunk_head->size); + InputType type = ldisc->inchunk_head->type; + + while (len > 0 && ldisc->userpass_le) { + char c = *buf++; + len--; + + bool dedicated = is_dedicated_byte(c, type); + lineedit_input(ldisc->userpass_le, c, dedicated); + } + + if (!backend_sendok(ldisc->backend)) { + ldisc_input_queue_consume(ldisc, buf - start); + break; + } + + /* + * Either perform local editing, or just send characters. + */ + if (EDITING) { + while (len > 0) { + char c = *buf++; + len--; + + bool dedicated = is_dedicated_byte(c, type); + lineedit_input(ldisc->le, c, dedicated); + } + ldisc_input_queue_consume(ldisc, buf - start); + } else { + if (ECHOING) + seat_stdout(ldisc->seat, buf, len); + if (type == DEDICATED && ldisc->protocol == PROT_TELNET) { + while (len > 0) { + char c = *buf++; + len--; + switch (c) { + case CTRL('M'): + if (ldisc->telnet_newline) + backend_special(ldisc->backend, SS_EOL, 0); + else + backend_send(ldisc->backend, "\r", 1); + break; + case CTRL('?'): + case CTRL('H'): + if (ldisc->telnet_keyboard) { + backend_special(ldisc->backend, SS_EC, 0); + break; + } + case CTRL('C'): + if (ldisc->telnet_keyboard) { + backend_special(ldisc->backend, SS_IP, 0); + break; + } + case CTRL('Z'): + if (ldisc->telnet_keyboard) { + backend_special(ldisc->backend, SS_SUSP, 0); + break; + } + + default: + backend_send(ldisc->backend, &c, 1); + break; + } } - } else - ldisc_to_backend_raw(ldisc, buf, len); + ldisc_input_queue_consume(ldisc, buf - start); + } else { + backend_send(ldisc->backend, buf, len); + ldisc_input_queue_consume(ldisc, len); + } } } } diff --git a/putty.h b/putty.h index 96c5ab34..a6fbeb54 100644 --- a/putty.h +++ b/putty.h @@ -2369,28 +2369,7 @@ void ldisc_configure(Ldisc *, Conf *); void ldisc_free(Ldisc *); void ldisc_send(Ldisc *, const void *buf, int len, bool interactive); void ldisc_echoedit_update(Ldisc *); -typedef struct LdiscInputToken { - /* - * Structure that encodes any single item of data that Ldisc can - * buffer: either a single character of raw data, or a session - * special. - */ - bool is_special; - union { - struct { - /* if is_special == false */ - char chr; - }; - struct { - /* if is_special == true */ - SessionSpecialCode code; - int arg; - }; - }; -} LdiscInputToken; -bool ldisc_has_input_buffered(Ldisc *); -LdiscInputToken ldisc_get_input_token(Ldisc *); /* asserts there is input */ -void ldisc_enable_prompt_callback(Ldisc *, prompts_t *); +void ldisc_provide_userpass_le(Ldisc *, TermLineEditor *); void ldisc_check_sendok(Ldisc *); /* diff --git a/stubs/no-lineedit.c b/stubs/no-lineedit.c new file mode 100644 index 00000000..219572d4 --- /dev/null +++ b/stubs/no-lineedit.c @@ -0,0 +1,18 @@ +/* + * Stubs of functions in lineedit.c, for use in programs that don't + * have any use for line editing (e.g. because they don't have a + * terminal either). + */ + +#include "putty.h" +#include "terminal.h" + +TermLineEditor *lineedit_new(Terminal *term, unsigned flags, + TermLineEditorCallbackReceiver *receiver) +{ + return NULL; +} +void lineedit_free(TermLineEditor *le) {} +void lineedit_input(TermLineEditor *le, char ch, bool dedicated) {} +void lineedit_modify_flags(TermLineEditor *le, unsigned clr, unsigned flip) {} +void lineedit_send_line(TermLineEditor *le) {} diff --git a/terminal/lineedit.c b/terminal/lineedit.c new file mode 100644 index 00000000..eaaa3a18 --- /dev/null +++ b/terminal/lineedit.c @@ -0,0 +1,520 @@ +/* + * Implementation of local line editing. Used during username and + * password input at login time, and also by ldisc during the main + * session (if the session's virtual terminal is in that mode). + * + * Because we're tied into a real GUI terminal (and not a completely + * standalone line-discipline module that deals purely with byte + * streams), we can support a slightly richer input interface than + * plain bytes. + * + * In particular, the 'dedicated' flag sent along with every byte is + * used to distinguish control codes input via Ctrl+letter from the + * same code input by a dedicated key like Return or Backspace. This + * allows us to interpret the Ctrl+letter key combination as inputting + * a literal control character to go into the line buffer, and the + * dedicated-key version as performing an editing function. + */ + +#include "putty.h" +#include "terminal.h" + +typedef struct BufChar BufChar; + +struct TermLineEditor { + Terminal *term; + BufChar *head, *tail; + unsigned flags; + bool quote_next_char; + TermLineEditorCallbackReceiver *receiver; +}; + +struct BufChar { + BufChar *prev, *next; + + /* The bytes of the character, to be sent on the wire */ + char wire[6]; + uint8_t nwire; + + /* Whether this character is considered complete */ + bool complete; + + /* Width of the character when it was displayed, in terminal cells */ + uint8_t width; + + /* Whether this character counts as whitespace, for ^W purposes */ + bool space; +}; + +TermLineEditor *lineedit_new(Terminal *term, unsigned flags, + TermLineEditorCallbackReceiver *receiver) +{ + TermLineEditor *le = snew(TermLineEditor); + le->term = term; + le->head = le->tail = NULL; + le->flags = flags; + le->quote_next_char = false; + le->receiver = receiver; + return le; +} + +static void bufchar_free(BufChar *bc) +{ + smemclr(bc, sizeof(*bc)); + sfree(bc); +} + +static void lineedit_free_buffer(TermLineEditor *le) +{ + while (le->head) { + BufChar *bc = le->head; + le->head = bc->next; + bufchar_free(bc); + } + le->tail = NULL; +} + +void lineedit_free(TermLineEditor *le) +{ + lineedit_free_buffer(le); + sfree(le); +} + +void lineedit_modify_flags(TermLineEditor *le, unsigned clr, unsigned flip) +{ + le->flags &= ~clr; + le->flags ^= flip; +} + +static void lineedit_term_write(TermLineEditor *le, ptrlen data) +{ + le->receiver->vt->to_terminal(le->receiver, data); +} + +static void lineedit_term_newline(TermLineEditor *le) +{ + lineedit_term_write(le, PTRLEN_LITERAL("\x0D\x0A")); +} + +static inline void lineedit_send_data(TermLineEditor *le, ptrlen data) +{ + le->receiver->vt->to_backend(le->receiver, data); +} + +static inline void lineedit_special(TermLineEditor *le, + SessionSpecialCode code, int arg) +{ + le->receiver->vt->special(le->receiver, code, arg); +} + +static inline void lineedit_send_newline(TermLineEditor *le) +{ + le->receiver->vt->newline(le->receiver); +} + +static void lineedit_delete_char(TermLineEditor *le) +{ + if (le->tail) { + BufChar *bc = le->tail; + le->tail = bc->prev; + if (!le->tail) + le->head = NULL; + else + le->tail->next = NULL; + + for (unsigned i = 0; i < bc->width; i++) + lineedit_term_write(le, PTRLEN_LITERAL("\x08 \x08")); + + bufchar_free(bc); + } +} + +static void lineedit_delete_word(TermLineEditor *le) +{ + /* + * Deleting a word stops at the _start_ of a word, i.e. at any + * boundary with a space on the left and a non-space on the right. + */ + if (!le->tail) + return; + + while (true) { + bool deleted_char_is_space = le->tail->space; + lineedit_delete_char(le); + if (!le->tail) + break; /* we've cleared the whole line */ + if (le->tail->space && !deleted_char_is_space) + break; /* we've just reached a word boundary */ + } +} + +static void lineedit_delete_line(TermLineEditor *le) +{ + while (le->tail) + lineedit_delete_char(le); + lineedit_special(le, SS_EL, 0); +} + +void lineedit_send_line(TermLineEditor *le) +{ + for (BufChar *bc = le->head; bc; bc = bc->next) + lineedit_send_data(le, make_ptrlen(bc->wire, bc->nwire)); + lineedit_free_buffer(le); + le->quote_next_char = false; +} + +static void lineedit_complete_line(TermLineEditor *le) +{ + lineedit_term_newline(le); + lineedit_send_line(le); + lineedit_send_newline(le); +} + +/* + * Send data to the terminal to display a BufChar. As a side effect, + * update bc->width to indicate how many character cells we think were + * taken up by what we just wrote. No other change to bc is made. + */ +static void lineedit_display_bufchar(TermLineEditor *le, BufChar *bc, + unsigned chr) +{ + char buf[6]; + buffer_sink bs[1]; + buffer_sink_init(bs, buf, lenof(buf)); + + /* Handle single-byte character set translation. */ + if (!in_utf(le->term) && DIRECT_CHAR(chr)) { + /* + * If we're not in UTF-8, i.e. we're in a single-byte + * character set, then first we must feed the input byte + * through term_translate, which will tell us whether it's a + * control character or not. (That varies with the charset: + * e.g. ISO 8859-1 and Win1252 disagree on a lot of + * 0x80-0x9F). + * + * In principle, we could pass NULL as our term_utf8_decode + * pointer, on the grounds that since the terminal isn't in + * UTF-8 mode term_translate shouldn't access it. But that + * seems needlessly reckless; we'll make up an empty one. + */ + term_utf8_decode dummy_utf8 = { .state = 0, .chr = 0, .size = 0 }; + chr = term_translate( + le->term, &dummy_utf8, (unsigned char)chr); + + /* + * After that, chr will be either a control-character value + * (00-1F, 7F, 80-9F), or a byte value ORed with one of the + * CSET_FOO character set indicators. The latter indicates + * that it's a printing character in this charset, in which + * case it takes up one character cell. + */ + if (chr & CSET_MASK) { + put_byte(bs, chr); + bc->width = 1; + goto got_char; + } + } + + /* + * If we got here without taking the 'goto' above, then we're now + * holding an actual Unicode character. + */ + assert(!IS_SURROGATE(chr)); /* and it should be an _actual_ one */ + + /* + * Deal with symbolic representations of control characters. + */ + + if (chr < 0x20 || chr == 0x7F) { + /* + * Represent C0 controls as '^C' or similar, and 7F as ^?. + */ + put_byte(bs, '^'); + put_byte(bs, chr ^ 0x40); + bc->width = 2; + goto got_char; + } + + if (chr >= 0x80 && chr < 0xA0) { + /* + * Represent C1 controls as <9B> or similar. + */ + put_fmt(bs, "<%02X>", chr); + bc->width = 4; + goto got_char; + } + + /* + * And if we get _here_, we're holding a printing (or at least not + * _control_, even if zero-width) Unicode character, which _must_ + * mean that the terminal is currently in UTF-8 mode (since if it + * were not then printing characters would have gone through the + * term_translate case above). So we can just write the UTF-8 for + * the character - but we must also pay attention to its width in + * character cells, which might be 0, 1 or 2. + */ + assert(in_utf(le->term)); + put_utf8_char(bs, chr); + bc->width = term_char_width(le->term, chr); + + got_char: + lineedit_term_write(le, make_ptrlen(buf, bs->out - buf)); +} + +/* Called when we've just added a byte to a UTF-8 character and want + * to see if it's complete */ +static void lineedit_check_utf8_complete(TermLineEditor *le, BufChar *bc) +{ + BinarySource src[1]; + BinarySource_BARE_INIT(src, bc->wire, bc->nwire); + DecodeUTF8Failure err; + unsigned chr = decode_utf8(src, &err); + if (err == DUTF8_E_OUT_OF_DATA) + return; /* not complete yet */ + + /* Any other error code is regarded as complete, and we just + * display the character as the U+FFFD that decode_utf8 will have + * returned anyway */ + bc->complete = true; + bc->space = (chr == ' '); + lineedit_display_bufchar(le, bc, chr); +} + +static void lineedit_input_printing_char(TermLineEditor *le, char ch); + +static void lineedit_redraw_line(TermLineEditor *le) +{ + /* FIXME: I'm not 100% sure this is the behaviour I really want in + * this situation, but it's at least very simple to implement */ + BufChar *prevhead = le->head; + le->head = le->tail = NULL; + while (prevhead) { + BufChar *bc = prevhead; + prevhead = prevhead->next; + + for (unsigned i = 0; i < bc->nwire; i++) + lineedit_input_printing_char(le, bc->wire[i]); + bufchar_free(bc); + } +} + +#define CTRL(c) ((char) (0x40 ^ (unsigned char)c)) + +void lineedit_input(TermLineEditor *le, char ch, bool dedicated) +{ + if (le->quote_next_char) { + /* + * If the previous keypress was ^V, 'quoting' the next + * character to be treated literally, then skip all the + * editing-control processing, and clear that flag. + */ + le->quote_next_char = false; + } else { + /* + * Input events that are only valid with the 'dedicated' flag. + * These are limited to the control codes that _have_ + * dedicated keys. + * + * Any case we actually handle here ends with a 'return' + * statement, so that if we fall out of the end of this switch + * at all, it's because the byte hasn't been handled here and + * will fall into the next switch dealing with ordinary input. + */ + if (dedicated) { + switch (ch) { + /* + * The Backspace key. + * + * Since our terminal can be configured to send either + * ^H or 7F (aka ^?) via the backspace key, we accept + * both. + * + * (We could query the Terminal's configuration here + * and accept only the one of those codes that the + * terminal is currently set to. But it's pointless, + * because whichever one the terminal isn't set to, + * the front end won't be sending it with + * dedicated=true anyway.) + */ + case CTRL('H'): + case 0x7F: + lineedit_delete_char(le); + return; + + /* + * The Return key. + */ + case CTRL('M'): + lineedit_complete_line(le); + return; + } + } + + /* + * Editing and special functions in response to ordinary keys + * or Ctrl+key combinations. Many editing functions have to be + * supported in this mode, like ^W and ^U, because there are + * no dedicated keys that generate the same control codes + * anyway. + * + * Again, we return if the key was handled. The final + * processing of ordinary data to go into the input buffer + * happens if we break from this switch. + */ + switch (ch) { + case CTRL('W'): + lineedit_delete_word(le); + return; + + case CTRL('U'): + lineedit_delete_line(le); + return; + + case CTRL('['): + if (!(le->flags & LE_ESC_ERASES)) + break; /* treat as normal input */ + lineedit_delete_line(le); + return; + + case CTRL('R'): + lineedit_term_write(le, PTRLEN_LITERAL("^R")); + lineedit_term_newline(le); + lineedit_redraw_line(le); + return; + + case CTRL('V'): + le->quote_next_char = true; + return; + + case CTRL('C'): + lineedit_delete_line(le); + if (!(le->flags & LE_INTERRUPT)) + break; /* treat as normal input */ + lineedit_special(le, SS_IP, 0); + return; + + case CTRL('Z'): + lineedit_delete_line(le); + if (!(le->flags & LE_SUSPEND)) + break; /* treat as normal input */ + lineedit_special(le, SS_SUSP, 0); + return; + + case CTRL('\\'): + lineedit_delete_line(le); + if (!(le->flags & LE_ABORT)) + break; /* treat as normal input */ + lineedit_special(le, SS_ABORT, 0); + return; + + case CTRL('D'): + if (le->flags & LE_EOF_ALWAYS) { + /* Our client wants to treat ^D / EOF as a special + * character in their own way. Just send an EOF + * special. */ + lineedit_special(le, SS_EOF, 0); + return; + } + + /* + * Otherwise, ^D has the same behaviour as in Unix tty + * line editing: if the edit buffer is non-empty then it's + * sent immediately without a newline, and if it is empty + * then an EOF is sent. + */ + if (le->head) { + lineedit_send_line(le); + return; + } + + lineedit_special(le, SS_EOF, 0); + return; + + case CTRL('J'): + if (le->flags & LE_CRLF_NEWLINE) { + /* + * If the previous character in the buffer is a + * literal Ctrl-M, and now the user sends Ctrl-J, then + * we amalgamate both into a newline event. + */ + if (le->tail && le->tail->nwire == 1 && + le->tail->wire[0] == CTRL('M')) { + lineedit_delete_char(le); /* erase ^J from buffer */ + lineedit_complete_line(le); + return; + } + } + } + } + + /* + * If we get to here, we've exhausted the options for treating our + * character as an editing or special function of any kind. Treat + * it as a printing character, or part of one. + */ + lineedit_input_printing_char(le, ch); +} + +static void lineedit_input_printing_char(TermLineEditor *le, char ch) +{ + /* + * Append ch to the line buffer, either as a new BufChar or by + * adding it to a previous one containing an as yet incomplete + * UTF-8 encoding. + */ + if (le->tail && !le->tail->complete) { + BufChar *bc = le->tail; + + /* + * If we're in UTF-8 mode, and ch is a UTF-8 continuation + * byte, then we can append it to bc, which we've just checked + * is missing at least one of those. + */ + if (in_utf(le->term) && (unsigned char)ch - 0x80U < 0x40) { + assert(bc->nwire < lenof(bc->wire)); + bc->wire[bc->nwire++] = ch; + lineedit_check_utf8_complete(le, bc); + return; + } + + /* + * Otherwise, the previous incomplete character can't be + * extended. Mark it as complete, and if possible, display it + * as a replacement character indicating that something weird + * happened. + */ + bc->complete = true; + if (in_utf(le->term)) + lineedit_display_bufchar(le, bc, 0xFFFD); + + /* + * But we still haven't processed the byte we're holding. Fall + * through to the next step, where we make a fresh BufChar for + * it. + */ + } + + /* + * Make a fresh BufChar. + */ + BufChar *bc = snew(BufChar); + bc->prev = le->tail; + le->tail = bc; + if (bc->prev) + bc->prev->next = bc; + else + le->head = bc; + bc->next = NULL; + bc->complete = false; + bc->space = false; + + bc->nwire = 1; + bc->wire[0] = ch; + if (in_utf(le->term)) { + lineedit_check_utf8_complete(le, bc); + } else { + bc->complete = true; /* always, in a single-byte charset */ + bc->space = (bc->wire[0] == ' '); + lineedit_display_bufchar(le, bc, CSET_ASCII | (unsigned char)ch); + } +} diff --git a/terminal/terminal.c b/terminal/terminal.c index b6e11c3f..a90390b1 100644 --- a/terminal/terminal.c +++ b/terminal/terminal.c @@ -62,6 +62,9 @@ static const char sco2ansicolour[] = { 0, 4, 2, 6, 1, 5, 3, 7 }; #define sel_nl_sz (sizeof(sel_nl)/sizeof(wchar_t)) static const wchar_t sel_nl[] = SEL_NL; +/* forward declaration */ +static void term_userpass_state_free(struct term_userpass_state *s); + /* * Fetch the character at a particular position in a line array, * for purposes of `wordtype'. The reason this isn't just a simple @@ -2128,6 +2131,8 @@ Terminal *term_init(Conf *myconf, struct unicode_data *ucsdata, TermWin *win) term->bidi_ctx = bidi_new_context(); + term->userpass_state = NULL; + palette_reset(term, false); return term; @@ -2190,6 +2195,10 @@ void term_free(Terminal *term) bidi_free_context(term->bidi_ctx); + /* In case a term_userpass_state is still around */ + if (term->userpass_state) + term_userpass_state_free(term->userpass_state); + sfree(term); } @@ -7823,15 +7832,19 @@ char *term_get_ttymode(Terminal *term, const char *mode) } struct term_userpass_state { + prompts_t *prompts; size_t curr_prompt; - bool done_prompt; /* printed out prompt yet? */ + enum TermUserpassPromptState { + TUS_INITIAL, /* haven't even printed the prompt yet */ + TUS_ACTIVE, /* prompt is currently receiving user input */ + TUS_ABORTED, /* user pressed ^C or ^D to cancel prompt */ + } prompt_state; + Terminal *term; + TermLineEditor *le; + TermLineEditorCallbackReceiver le_rcv; }; -/* Tiny wrapper to make it easier to write lots of little strings */ -static inline void term_write(Terminal *term, ptrlen data) -{ - term_data(term, data.ptr, data.len); -} +static void term_userpass_next_prompt(struct term_userpass_state *s); /* * Signal that a prompts_t is done. This involves sending a @@ -7844,11 +7857,123 @@ static inline SeatPromptResult signal_prompts_t(Terminal *term, prompts_t *p, assert(p->callback && "Asynchronous userpass input requires a callback"); queue_toplevel_callback(p->callback, p->callback_ctx); if (term->ldisc) - ldisc_enable_prompt_callback(term->ldisc, NULL); + ldisc_provide_userpass_le(term->ldisc, NULL); p->spr = spr; + if (p->data) { + term_userpass_state_free(p->data); + p->data = NULL; + } return spr; } +/* Tiny wrapper to make it easier to write lots of little strings */ +static inline void term_write(Terminal *term, ptrlen data) +{ + term_data(term, data.ptr, data.len); +} + +static void term_lineedit_to_terminal( + TermLineEditorCallbackReceiver *rcv, ptrlen data) +{ + struct term_userpass_state *s = container_of( + rcv, struct term_userpass_state, le_rcv); + prompt_t *pr = s->prompts->prompts[s->curr_prompt]; + if (pr->echo) + term_write(s->term, data); +} + +static void term_lineedit_to_backend( + TermLineEditorCallbackReceiver *rcv, ptrlen data) +{ + struct term_userpass_state *s = container_of( + rcv, struct term_userpass_state, le_rcv); + prompt_t *pr = s->prompts->prompts[s->curr_prompt]; + put_datapl(pr->result, data); +} + +static void term_lineedit_newline(TermLineEditorCallbackReceiver *rcv) +{ + struct term_userpass_state *s = container_of( + rcv, struct term_userpass_state, le_rcv); + + prompt_t *pr = s->prompts->prompts[s->curr_prompt]; + if (!pr->echo) { + /* If echo is disabled, we won't have printed the newline in + * term_lineedit_to_terminal, so print it now */ + term_write(s->term, PTRLEN_LITERAL("\x0D\x0A")); + } + + ldisc_provide_userpass_le(s->term->ldisc, NULL); + s->curr_prompt++; + s->prompt_state = TUS_INITIAL; + term_userpass_next_prompt(s); +} + +static void term_lineedit_special( + TermLineEditorCallbackReceiver *rcv, SessionSpecialCode code, int arg) +{ + struct term_userpass_state *s = container_of( + rcv, struct term_userpass_state, le_rcv); + switch (code) { + case SS_IP: + case SS_EOF: + ldisc_provide_userpass_le(s->term->ldisc, NULL); + s->prompt_state = TUS_ABORTED; + signal_prompts_t(s->term, s->prompts, SPR_USER_ABORT); + default: + break; + } +} + +static const TermLineEditorCallbackReceiverVtable +term_userpass_lineedit_receiver_vt = { + .to_terminal = term_lineedit_to_terminal, + .to_backend = term_lineedit_to_backend, + .special = term_lineedit_special, + .newline = term_lineedit_newline, +}; + +static struct term_userpass_state *term_userpass_state_new( + Terminal *term, prompts_t *prompts) +{ + struct term_userpass_state *s = snew(struct term_userpass_state); + s->prompts = prompts; + s->curr_prompt = 0; + s->prompt_state = TUS_INITIAL; + s->term = term; + s->le_rcv.vt = &term_userpass_lineedit_receiver_vt; + s->le = lineedit_new(term, LE_INTERRUPT | LE_EOF_ALWAYS | LE_ESC_ERASES, + &s->le_rcv); + assert(!term->userpass_state); + term->userpass_state = s; + return s; +} + +static void term_userpass_state_free(struct term_userpass_state *s) +{ + assert(s->term->userpass_state == s); + s->term->userpass_state = NULL; + lineedit_free(s->le); + sfree(s); +} + +static void term_userpass_next_prompt(struct term_userpass_state *s) +{ + if (s->prompt_state != TUS_INITIAL) + return; + if (s->curr_prompt < s->prompts->n_prompts) { + prompt_t *pr = s->prompts->prompts[s->curr_prompt]; + term_write(s->term, ptrlen_from_asciz(pr->prompt)); + s->prompt_state = TUS_ACTIVE; + ldisc_provide_userpass_le(s->term->ldisc, s->le); + } else { + /* This triggers the callback provided by the userpass client, + * which will call term_userpass_state to fetch the result + * we're storing here */ + signal_prompts_t(s->term, s->prompts, SPR_OK); + } +} + /* * Process some terminal data in the course of username/password * input. @@ -7873,10 +7998,8 @@ SeatPromptResult term_get_userpass_input(Terminal *term, prompts_t *p) /* * First call. Set some stuff up. */ - p->data = s = snew(struct term_userpass_state); + p->data = s = term_userpass_state_new(term, p); p->spr = SPR_INCOMPLETE; - s->curr_prompt = 0; - s->done_prompt = false; /* We only print the `name' caption if we have to... */ if (p->name_reqd && p->name) { ptrlen plname = ptrlen_from_asciz(p->name); @@ -7899,98 +8022,11 @@ SeatPromptResult term_get_userpass_input(Terminal *term, prompts_t *p) for (i = 0; i < (int)p->n_prompts; i++) prompt_set_result(p->prompts[i], ""); } + /* And print the first prompt. */ + term_userpass_next_prompt(s); } - while (s->curr_prompt < p->n_prompts) { - - prompt_t *pr = p->prompts[s->curr_prompt]; - bool finished_prompt = false; - - if (!s->done_prompt) { - term_write(term, ptrlen_from_asciz(pr->prompt)); - s->done_prompt = true; - } - - /* Breaking out here ensures that the prompt is printed even - * if we're now waiting for user data. */ - if (!ldisc_has_input_buffered(term->ldisc)) - break; - - /* FIXME: should we be using local-line-editing code instead? */ - while (!finished_prompt && ldisc_has_input_buffered(term->ldisc)) { - LdiscInputToken tok = ldisc_get_input_token(term->ldisc); - - char c; - if (tok.is_special) { - switch (tok.code) { - case SS_EOL: c = 13; break; - case SS_EC: c = 8; break; - case SS_IP: c = 3; break; - case SS_EOF: c = 3; break; - default: continue; - } - } else { - c = tok.chr; - } - - switch (c) { - case 10: - case 13: - term_write(term, PTRLEN_LITERAL("\r\n")); - /* go to next prompt, if any */ - s->curr_prompt++; - s->done_prompt = false; - finished_prompt = true; /* break out */ - break; - case 8: - case 127: - if (pr->result->len > 0) { - if (pr->echo) - term_write(term, PTRLEN_LITERAL("\b \b")); - strbuf_shrink_by(pr->result, 1); - } - break; - case 21: - case 27: - while (pr->result->len > 0) { - if (pr->echo) - term_write(term, PTRLEN_LITERAL("\b \b")); - strbuf_shrink_by(pr->result, 1); - } - break; - case 3: - case 4: - /* Immediate abort. */ - term_write(term, PTRLEN_LITERAL("\r\n")); - sfree(s); - p->data = NULL; - return signal_prompts_t(term, p, SPR_USER_ABORT); - default: - /* - * This simplistic check for printability is disabled - * when we're doing password input, because some people - * have control characters in their passwords. - */ - if (!pr->echo || (c >= ' ' && c <= '~') || - ((unsigned char) c >= 160)) { - put_byte(pr->result, c); - if (pr->echo) - term_write(term, make_ptrlen(&c, 1)); - } - break; - } - } - - } - - if (s->curr_prompt < p->n_prompts) { - ldisc_enable_prompt_callback(term->ldisc, p); - return SPR_INCOMPLETE; - } else { - sfree(s); - p->data = NULL; - return signal_prompts_t(term, p, SPR_OK); - } + return SPR_INCOMPLETE; } void term_notify_minimised(Terminal *term, bool minimised) diff --git a/terminal/terminal.h b/terminal/terminal.h index 66d57d8b..95c504ec 100644 --- a/terminal/terminal.h +++ b/terminal/terminal.h @@ -74,6 +74,8 @@ struct term_utf8_decode { int size; /* The size of the UTF character. */ }; +struct term_userpass_state; + struct terminal_tag { int compatibility_level; @@ -428,6 +430,12 @@ struct terminal_tag { WIN_RESIZE_NO, WIN_RESIZE_NEED_SEND, WIN_RESIZE_AWAIT_REPLY } win_resize_pending; int win_resize_pending_w, win_resize_pending_h; + + /* + * Indicates whether term_get_userpass_input is currently using + * the terminal to present a password prompt or similar. + */ + struct term_userpass_state *userpass_state; }; static inline bool in_utf(Terminal *term) @@ -561,4 +569,44 @@ static inline bool decpos_fn(pos *p, int cols) #define incpos(p) incpos_fn(&(p), GET_TERM_COLS) #define decpos(p) decpos_fn(&(p), GET_TERM_COLS) +struct TermLineEditorCallbackReceiverVtable { + void (*to_terminal)(TermLineEditorCallbackReceiver *rcv, ptrlen data); + void (*to_backend)(TermLineEditorCallbackReceiver *rcv, ptrlen data); + void (*special)(TermLineEditorCallbackReceiver *rcv, + SessionSpecialCode code, int arg); + void (*newline)(TermLineEditorCallbackReceiver *rcv); +}; +struct TermLineEditorCallbackReceiver { + const TermLineEditorCallbackReceiverVtable *vt; +}; +TermLineEditor *lineedit_new(Terminal *term, unsigned flags, + TermLineEditorCallbackReceiver *receiver); +void lineedit_free(TermLineEditor *le); +void lineedit_input(TermLineEditor *le, char ch, bool dedicated); +void lineedit_modify_flags(TermLineEditor *le, unsigned clr, unsigned flip); +void lineedit_send_line(TermLineEditor *le); + +/* + * Flags controlling the behaviour of TermLineEditor. + */ +#define LINEEDIT_FLAGS(X) \ + X(LE_INTERRUPT) /* pass SS_IP back to client on ^C */ \ + X(LE_SUSPEND) /* pass SS_SUSP back to client on ^Z */ \ + X(LE_ABORT) /* pass SS_ABORT back to client on ^\ */ \ + X(LE_EOF_ALWAYS) /* pass SS_EOF to client on *any* ^Z + * (X(not)just if the line buffer is empty) */ \ + X(LE_ESC_ERASES) /* make ESC erase the line, as well as ^U */ \ + X(LE_CRLF_NEWLINE) /* interpret manual ^M^J the same as Return */ \ + /* end of list */ +enum { + #define ALLOCATE_BIT_POSITION(flag) flag ## _bitpos, + LINEEDIT_FLAGS(ALLOCATE_BIT_POSITION) + #undef ALLOCATE_BIT_POSITION +}; +enum { + #define DEFINE_FLAG_BIT(flag) flag = 1 << flag ## _bitpos, + LINEEDIT_FLAGS(DEFINE_FLAG_BIT) + #undef DEFINE_FLAG_BIT +}; + #endif diff --git a/test/fuzzterm.c b/test/fuzzterm.c index 291ded6c..45ba91dd 100644 --- a/test/fuzzterm.c +++ b/test/fuzzterm.c @@ -119,10 +119,7 @@ static const TermWinVtable fuzz_termwin_vt = { void ldisc_send(Ldisc *ldisc, const void *buf, int len, bool interactive) {} void ldisc_echoedit_update(Ldisc *ldisc) {} -bool ldisc_has_input_buffered(Ldisc *ldisc) { return false; } -LdiscInputToken ldisc_get_input_token(Ldisc *ldisc) -{ unreachable("This fake ldisc never has any buffered input"); } -void ldisc_enable_prompt_callback(Ldisc *ldisc, prompts_t *p) +void ldisc_provide_userpass_le(Ldisc *ldisc, TermLineEditor *le) { unreachable("This fake ldisc should never be used for user/pass prompts"); } void modalfatalbox(const char *fmt, ...) { exit(0); } void nonfatal(const char *fmt, ...) { } diff --git a/test/test_lineedit.c b/test/test_lineedit.c index 7259a54e..488eca56 100644 --- a/test/test_lineedit.c +++ b/test/test_lineedit.c @@ -656,9 +656,10 @@ static void test_edit(Mock *mk, bool echo) conf_set_bool(mk->conf, CONF_telnet_keyboard, false); ldisc_configure(mk->ldisc, mk->conf); - /* Test UTF-8 characters of various lengths and ensure deleting one - * deletes the whole character from the buffer (by pressing Return and - * seeing what gets sent) but only sends one BSB */ + /* Test UTF-8 characters of various lengths and ensure deleting + * one deletes the whole character from the buffer (by pressing + * Return and seeing what gets sent) but sends a number of BSBs + * corresponding to the character's terminal width */ mk->term->utf = true; ldisc_send(mk->ldisc, "\xC2\xA0\xC2\xA1", 4, false); @@ -692,6 +693,29 @@ static void test_edit(Mock *mk, bool echo) EXPECT(mk, backend, PTRLEN_LITERAL("\xF0\x90\x80\x80\x0D\x0A")); reset(mk); + /* Double-width characters (Hangul, as it happens) */ + ldisc_send(mk->ldisc, "\xEA\xB0\x80\xEA\xB0\x81", 6, false); + EXPECT(mk, backend, PTRLEN_LITERAL("")); + EXPECT_TERMINAL(mk, PTRLEN_LITERAL("\xEA\xB0\x80\xEA\xB0\x81")); + ldisc_send(mk->ldisc, "\x08", -1, false); + EXPECT(mk, backend, PTRLEN_LITERAL("")); + EXPECT_TERMINAL(mk, PTRLEN_LITERAL("\xEA\xB0\x80\xEA\xB0\x81" + "\x08 \x08\x08 \x08")); + ldisc_send(mk->ldisc, "\x0D", -1, false); + EXPECT(mk, backend, PTRLEN_LITERAL("\xEA\xB0\x80\x0D\x0A")); + reset(mk); + + /* Zero-width characters */ + ldisc_send(mk->ldisc, "\xE2\x80\x8B\xE2\x80\x8B", 6, false); + EXPECT(mk, backend, PTRLEN_LITERAL("")); + EXPECT_TERMINAL(mk, PTRLEN_LITERAL("\xE2\x80\x8B\xE2\x80\x8B")); + ldisc_send(mk->ldisc, "\x08", -1, false); + EXPECT(mk, backend, PTRLEN_LITERAL("")); + EXPECT_TERMINAL(mk, PTRLEN_LITERAL("\xE2\x80\x8B\xE2\x80\x8B")); + ldisc_send(mk->ldisc, "\x0D", -1, false); + EXPECT(mk, backend, PTRLEN_LITERAL("\xE2\x80\x8B\x0D\x0A")); + reset(mk); + /* And reset back to non-UTF-8 mode and expect high-bit-set bytes * to be treated individually, as characters in a single-byte * charset. (In our case, given the test config, that will be