123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389 |
- /*
- * 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 <stdio.h>
- #include <ctype.h>
- #include <assert.h>
- #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);
- }
- }
- }
- }
|