/* * 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" 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(), 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. * * 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; /* * Values cached out of conf. */ bool telnet_keyboard, telnet_newline; int protocol, localecho, localedit; 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 || \ (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 ldisc_input_queue_callback(void *ctx); static const TermLineEditorCallbackReceiverVtable ldisc_lineedit_receiver_vt; #define CTRL(x) (x^'@') Ldisc *ldisc_create(Conf *conf, Terminal *term, Backend *backend, Seat *seat) { Ldisc *ldisc = snew(Ldisc); memset(ldisc, 0, sizeof(Ldisc)); ldisc->backend = backend; ldisc->term = term; ldisc->seat = seat; bufchain_init(&ldisc->input_queue); ldisc->input_queue_callback.fn = ldisc_input_queue_callback; ldisc->input_queue_callback.ctx = ldisc; 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 */ 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); 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); delete_callbacks_for_context(ldisc); sfree(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_provide_userpass_le(Ldisc *ldisc, TermLineEditor *le) { /* * 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->userpass_le = le; } static inline bool is_dedicated_byte(char c, InputType type) { switch (type) { case DEDICATED: return true; case NORMAL: return false; case NONINTERACTIVE: /* * 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. */ return c == '\r'; default: unreachable("those values should be exhaustive"); } } static void ldisc_input_queue_consume(Ldisc *ldisc, size_t size) { 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_lineedit_to_terminal( TermLineEditorCallbackReceiver *rcv, ptrlen data) { Ldisc *ldisc = container_of(rcv, Ldisc, le_rcv); if (ECHOING) seat_stdout(ldisc->seat, data.ptr, data.len); } static void ldisc_lineedit_to_backend( TermLineEditorCallbackReceiver *rcv, ptrlen data) { Ldisc *ldisc = container_of(rcv, Ldisc, le_rcv); backend_send(ldisc->backend, data.ptr, data.len); } static void ldisc_lineedit_special( TermLineEditorCallbackReceiver *rcv, SessionSpecialCode code, int arg) { Ldisc *ldisc = container_of(rcv, Ldisc, le_rcv); backend_special(ldisc->backend, code, arg); } static void ldisc_lineedit_newline(TermLineEditorCallbackReceiver *rcv) { 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_idempotent_callback(&ldisc->input_queue_callback); } void ldisc_send(Ldisc *ldisc, const void *vbuf, int len, bool interactive) { 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); } InputType type; if (len < 0) { /* * Less than zero means null terminated special string. */ len = strlen(vbuf); type = DEDICATED; } else { type = interactive ? NORMAL : NONINTERACTIVE; } /* * 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; } } ldisc_input_queue_consume(ldisc, buf - start); } else { backend_send(ldisc->backend, buf, len); ldisc_input_queue_consume(ldisc, len); } } } }