Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GUACAMOLE-1973: Add support for XTerm bracketed-paste mode #533

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/terminal/terminal-handlers.c
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,7 @@ static bool* __guac_terminal_get_flag(guac_terminal* term, int num, char private
switch (num) {
case 1: return &(term->application_cursor_keys); /* DECCKM */
case 25: return &(term->cursor_visible); /* DECTECM */
case 2004: return &(term->bracketed_paste_mode); /* XTerm bracketed-paste */
}
}

Expand Down
140 changes: 138 additions & 2 deletions src/terminal/terminal.c
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ void guac_terminal_reset(guac_terminal* term) {
term->application_cursor_keys = false;
term->automatic_carriage_return = false;
term->insert_mode = false;
term->bracketed_paste_mode = false;

/* Reset tabs */
term->tab_interval = 8;
Expand Down Expand Up @@ -1574,6 +1575,141 @@ int guac_terminal_send_string(guac_terminal* term, const char* data) {

}

#define IS_UTF8_START_1_BYTE(c) ((c & 0x80) == 0x00)
#define IS_UTF8_START_2_BYTE(c) ((c & 0xe0) == 0xc0)
#define IS_UTF8_START_3_BYTE(c) ((c & 0xf0) == 0xe0)
#define IS_UTF8_START_4_BYTE(c) ((c & 0xf8) == 0xf0)
#define IS_UTF8_CONTINUATION(c) ((c & 0xc0) == 0x80)

int guac_terminal_send_clipboard(guac_terminal *term) {

/* Allocate a temporary buffer for filtering the clipboard contents.
* As we're removing characters, we know it will be at most the size
* of the original plus the two bracketed paste markers. */
char *filtered = guac_mem_alloc(term->clipboard->length +
strlen(GUAC_TERMINAL_BRACKETED_PASTE_START) +
strlen(GUAC_TERMINAL_BRACKETED_PASTE_STOP));
uint8_t *src_ptr = (uint8_t *)term->clipboard->buffer;
uint8_t *src_end = (uint8_t *)(term->clipboard->buffer + term->clipboard->length);
uint8_t *dst_ptr = (uint8_t *)filtered;

/* Keep track of exactly how much data we've sieved */
int filtered_len = 0;

/* Send the paste start sequence */
if (term->bracketed_paste_mode) {
size_t seq_len = strlen(GUAC_TERMINAL_BRACKETED_PASTE_START);
memcpy(dst_ptr, GUAC_TERMINAL_BRACKETED_PASTE_START, seq_len);
dst_ptr += seq_len;
filtered_len += seq_len;
}

while (src_ptr < src_end) {

/* Allow UTF-8 codepoints.
* A valid UTF-8 sequence is between one and four bytes in length, and
* we can confirm the validity by testing the start bits of each byte.
*
* A Unicode codepoint is only valid for the smallest UTF-8 sequence that
* it fits into; larger UTF-8 sequences can only contain larger codepoints.
* Therefore, some bits in the sequence are required to be used as part of
* the codepoint number.
*
* If the sequence is valid, copy it in full. */

/* UTF-8 1-byte codepoint (U+0000 to U+007F)
* Start bits: 0xxxxxxx */
if (IS_UTF8_START_1_BYTE(src_ptr[0])) {

/* Exclude Unicode CO (U+0000 to U+001F) control characters, except
* for tab (U+0009), line feed (U+000A) and carriage return (U+000D). */
if (!((src_ptr[0] >= 0x00) && (src_ptr[0] < 0x20)) ||
(src_ptr[0] == 0x09) || (src_ptr[0] == 0x0a) || (src_ptr[0] == 0x0d)) {
dst_ptr[0] = src_ptr[0];
dst_ptr++;
filtered_len++;
src_ptr++;
continue;
}
}

/* UTF-8 2-byte codepoint (U+0080 to U+07FF)
* Start bits: 110xxxxx 10xxxxxx
* Required: xxxYYYYx xxxxxxxx */
else if (IS_UTF8_START_2_BYTE(src_ptr[0])) {
if ((src_ptr + 1 < src_end) &&
IS_UTF8_CONTINUATION(src_ptr[1]) &&
((src_ptr[0] & 0x1e) != 0x00)) {

/* Exclude Unicode C1 (U+0080 to U+009F) control characters: 11000010 100xxxxx */
if ((src_ptr[0] != 0xc2) || ((src_ptr[1] & 0xe0) != 0x80)) {
dst_ptr[0] = src_ptr[0];
dst_ptr[1] = src_ptr[1];
dst_ptr += 2;
filtered_len += 2;
src_ptr += 2;
continue;
}
}
}

/* UTF-8 3-byte codepoint (U+0800 to U+FFFF)
* Start bits: 1110xxxx 10xxxxxx 10xxxxxx
* Required: xxxxYYYY xxYxxxxx xxxxxxxx */
else if (IS_UTF8_START_3_BYTE(src_ptr[0])) {
if ((src_ptr + 2 < src_end) &&
IS_UTF8_CONTINUATION(src_ptr[1]) &&
IS_UTF8_CONTINUATION(src_ptr[2]) &&
(((src_ptr[0] & 0x0f) != 0x00) ||
((src_ptr[1] & 0x20) != 0x00))) {
dst_ptr[0] = src_ptr[0];
dst_ptr[1] = src_ptr[1];
dst_ptr[2] = src_ptr[2];
dst_ptr += 3;
filtered_len += 3;
src_ptr += 3;
continue;
}
}

/* UTF-8 4-byte codepoint (U+010000 to U+10FFFF)
* Start bits: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
* Required: xxxxxYYY xxYYxxxx xxxxxxxx xxxxxxxx */
else if (IS_UTF8_START_4_BYTE(src_ptr[0])) {
if ((src_ptr + 3 < src_end) &&
IS_UTF8_CONTINUATION(src_ptr[1]) &&
IS_UTF8_CONTINUATION(src_ptr[2]) &&
IS_UTF8_CONTINUATION(src_ptr[3]) &&
(((src_ptr[0] & 0x07) != 0x00) ||
((src_ptr[1] & 0x30) != 0x00))) {
dst_ptr[0] = src_ptr[0];
dst_ptr[1] = src_ptr[1];
dst_ptr[2] = src_ptr[2];
dst_ptr[3] = src_ptr[3];
dst_ptr += 4;
filtered_len += 4;
src_ptr += 4;
continue;
}
}

/* If the sequence is invalid, skip to the next byte. */
src_ptr++;
}

/* Send the paste stop sequence */
if (term->bracketed_paste_mode) {
size_t seq_len = strlen(GUAC_TERMINAL_BRACKETED_PASTE_STOP);
memcpy(dst_ptr, GUAC_TERMINAL_BRACKETED_PASTE_STOP, seq_len);
dst_ptr += seq_len;
filtered_len += seq_len;
}

int result = guac_terminal_send_data(term, filtered, filtered_len);
guac_mem_free(filtered);
return result;
}

static int __guac_terminal_send_key(guac_terminal* term, int keysym, int pressed) {

/* Ignore user input if terminal is not started */
Expand Down Expand Up @@ -1605,7 +1741,7 @@ static int __guac_terminal_send_key(guac_terminal* term, int keysym, int pressed

/* Ctrl+Shift+V or Cmd+v (mac style) shortcuts for paste */
if ((keysym == 'V' && term->mod_ctrl) || (keysym == 'v' && term->mod_meta))
return guac_terminal_send_data(term, term->clipboard->buffer, term->clipboard->length);
return guac_terminal_send_clipboard(term);

/*
* Ctrl+Shift+C and Cmd+c shortcuts for copying are not handled, as
Expand Down Expand Up @@ -1937,7 +2073,7 @@ static int __guac_terminal_send_mouse(guac_terminal* term, guac_user* user,

/* Paste contents of clipboard on right or middle mouse button up */
if ((released_mask & GUAC_CLIENT_MOUSE_RIGHT) || (released_mask & GUAC_CLIENT_MOUSE_MIDDLE))
return guac_terminal_send_data(term, term->clipboard->buffer, term->clipboard->length);
return guac_terminal_send_clipboard(term);

/* If left mouse button was just released, stop selection */
if (released_mask & GUAC_CLIENT_MOUSE_LEFT)
Expand Down
5 changes: 5 additions & 0 deletions src/terminal/terminal/terminal-priv.h
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,11 @@ struct guac_terminal {
*/
bool automatic_carriage_return;

/**
* Whether the current application supports bracketed paste mode.
*/
bool bracketed_paste_mode;

/**
* Whether insert mode is enabled (DECIM).
*/
Expand Down
29 changes: 29 additions & 0 deletions src/terminal/terminal/terminal.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,19 @@
*/
#define GUAC_TERMINAL_PIPE_AUTOFLUSH 2

/*
* Sequence to send to the client at the start of pasted clipboard data
* when in XTerm bracketed paste mode.
*/
#define GUAC_TERMINAL_BRACKETED_PASTE_START "\x1B[200~"

/*
* Sequence to send to the client at the end of pasted clipboard data
* when in XTerm bracketed paste mode.
*/
#define GUAC_TERMINAL_BRACKETED_PASTE_STOP "\x1B[201~"


/**
* Represents a terminal emulator which uses a given Guacamole client to
* render itself.
Expand Down Expand Up @@ -542,6 +555,22 @@ int guac_terminal_send_data(guac_terminal* term, const char* data, int length);
*/
int guac_terminal_send_string(guac_terminal* term, const char* data);

/**
* Sends the terminal clipboard contents after sanitisation. If terminal input
* is currently coming from a stream due to a prior call to
* guac_terminal_send_stream(), any input which would normally result from
* invoking this function is dropped.
*
* @param term
* The terminal which should receive the given data on STDIN.
*
* @return
* The number of bytes written to STDIN, or a negative value if an error
* occurs preventing the data from being written. This should always be
* the size of the data given unless data is intentionally dropped.
*/
int guac_terminal_send_clipboard(guac_terminal* term);

/**
* Writes the given buffer to the given terminal's STDOUT. All requested bytes
* will be written unless an error occurs. This function may block until space
Expand Down