From 61a51df1b29695f6464193b18dd4f24809ef07df Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 30 Aug 2018 10:01:41 -0700 Subject: [PATCH 1/7] GUACAMOLE-622: Require guac_terminal_start() to be invoked before the terminal will render frames or accept user input. --- src/terminal/terminal.c | 29 ++++++++++++++++++++++++++--- src/terminal/terminal/terminal.h | 29 ++++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/src/terminal/terminal.c b/src/terminal/terminal.c index 760e128f..0427ae45 100644 --- a/src/terminal/terminal.c +++ b/src/terminal/terminal.c @@ -596,6 +596,7 @@ guac_terminal* guac_terminal_create(guac_client* client, available_width = 0; guac_terminal* term = malloc(sizeof(guac_terminal)); + term->started = false; term->client = client; term->upload_path_handler = NULL; term->file_download_handler = NULL; @@ -723,6 +724,11 @@ guac_terminal* guac_terminal_create(guac_client* client, } +void guac_terminal_start(guac_terminal* term) { + term->started = true; + guac_terminal_notify(term); +} + void guac_terminal_stop(guac_terminal* term) { /* Close input pipe and set fds to invalid */ @@ -851,11 +857,13 @@ wait_complete: int guac_terminal_render_frame(guac_terminal* terminal) { + guac_client* client = terminal->client; + int wait_result; /* Wait for data to be available */ wait_result = guac_terminal_wait(terminal, 1000); - if (wait_result) { + if (wait_result || !terminal->started) { guac_timestamp frame_start = guac_timestamp_current(); @@ -867,13 +875,14 @@ int guac_terminal_render_frame(guac_terminal* terminal) { - frame_end; /* Wait again if frame remaining */ - if (frame_remaining > 0) + if (frame_remaining > 0 || !terminal->started) wait_result = guac_terminal_wait(terminal, GUAC_TERMINAL_FRAME_TIMEOUT); else break; - } while (wait_result > 0); + } while (client->state == GUAC_CLIENT_RUNNING + && (wait_result > 0 || !terminal->started)); /* Flush terminal */ guac_terminal_lock(terminal); @@ -1672,6 +1681,13 @@ int guac_terminal_send_string(guac_terminal* term, const char* data) { static int __guac_terminal_send_key(guac_terminal* term, int keysym, int pressed) { + /* Ignore user input if terminal is not started */ + if (!term->started) { + guac_client_log(term->client, GUAC_LOG_DEBUG, "Ignoring user input " + "while terminal has not yet started."); + return 0; + } + /* Hide mouse cursor if not already hidden */ if (term->current_cursor != GUAC_TERMINAL_CURSOR_BLANK) { term->current_cursor = GUAC_TERMINAL_CURSOR_BLANK; @@ -1845,6 +1861,13 @@ int guac_terminal_send_key(guac_terminal* term, int keysym, int pressed) { static int __guac_terminal_send_mouse(guac_terminal* term, guac_user* user, int x, int y, int mask) { + /* Ignore user input if terminal is not started */ + if (!term->started) { + guac_client_log(term->client, GUAC_LOG_DEBUG, "Ignoring user input " + "while terminal has not yet started."); + return 0; + } + /* Determine which buttons were just released and pressed */ int released_mask = term->mouse_mask & ~mask; int pressed_mask = ~term->mouse_mask & mask; diff --git a/src/terminal/terminal/terminal.h b/src/terminal/terminal/terminal.h index bddf8e55..9a0145f7 100644 --- a/src/terminal/terminal/terminal.h +++ b/src/terminal/terminal/terminal.h @@ -171,6 +171,16 @@ struct guac_terminal { */ guac_client* client; + /** + * Whether user input should be handled and this terminal should render + * frames. Initially, this will be false, user input will be ignored, and + * rendering of frames will be withheld until guac_terminal_start() has + * been invoked. The data within frames will still be rendered, and text + * data received will still be handled, however actual frame boundaries + * will not be sent. + */ + bool started; + /** * The terminal render thread. */ @@ -526,7 +536,13 @@ struct guac_terminal { /** * Creates a new guac_terminal, having the given width and height, and - * rendering to the given client. + * rendering to the given client. As failover mechanisms and the Guacamole + * client implementation typically use the receipt of a "sync" message to + * denote successful connection, rendering of frames (sending of "sync") will + * be withheld until guac_terminal_start() is called, and user input will be + * ignored. The guac_terminal_start() function should be invoked only after + * either the underlying connection has truly succeeded, or until visible + * terminal output or user input is required. * * @param client * The client to which the terminal will be rendered. @@ -604,6 +620,17 @@ int guac_terminal_render_frame(guac_terminal* terminal); */ int guac_terminal_read_stdin(guac_terminal* terminal, char* c, int size); +/** + * Notifies the terminal that rendering should begin and that user input should + * now be accepted. This function must be invoked following terminal creation + * for the end of frames to be signalled with "sync" messages. Until this + * function is invoked, "sync" messages will be withheld. + * + * @param term + * The terminal to start. + */ +void guac_terminal_start(guac_terminal* term); + /** * Manually stop the terminal to forcibly unblock any pending reads/writes, * e.g. forcing guac_terminal_read_stdin() to return and cease all terminal I/O. From 0b39b0fc5fa9846ff350cc443b8f432efb4678ee Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 30 Aug 2018 10:06:20 -0700 Subject: [PATCH 2/7] GUACAMOLE-622: Implicitly invoke guac_terminal_start() if prompting is required. --- src/terminal/terminal.c | 3 +++ src/terminal/terminal/terminal.h | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/terminal/terminal.c b/src/terminal/terminal.c index 0427ae45..95e945d8 100644 --- a/src/terminal/terminal.c +++ b/src/terminal/terminal.c @@ -943,6 +943,9 @@ char* guac_terminal_prompt(guac_terminal* terminal, const char* title, int pos; char in_byte; + /* Prompting implicitly requires user input */ + guac_terminal_start(terminal); + /* Print title */ guac_terminal_printf(terminal, "%s", title); diff --git a/src/terminal/terminal/terminal.h b/src/terminal/terminal/terminal.h index 9a0145f7..08094baa 100644 --- a/src/terminal/terminal/terminal.h +++ b/src/terminal/terminal/terminal.h @@ -652,7 +652,9 @@ void guac_terminal_notify(guac_terminal* terminal); /** * Reads a single line from this terminal's STDIN, storing the result in a * newly-allocated string. Input is retrieved in the same manner as - * guac_terminal_read_stdin() and the same restrictions apply. + * guac_terminal_read_stdin() and the same restrictions apply. As reading input + * naturally requires user interaction, this function will implicitly invoke + * guac_terminal_start(). * * @param terminal * The terminal to which the provided title should be output, and from From 4606607309761cca9f69dbd8e15be2376950dac3 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 30 Aug 2018 10:08:30 -0700 Subject: [PATCH 3/7] GUACAMOLE-622: Start terminal for SSH only after SSH connection succeeds. --- src/protocols/ssh/ssh.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/protocols/ssh/ssh.c b/src/protocols/ssh/ssh.c index 59554fb7..0760f29c 100644 --- a/src/protocols/ssh/ssh.c +++ b/src/protocols/ssh/ssh.c @@ -335,6 +335,7 @@ void* ssh_client_thread(void* data) { /* Logged in */ guac_client_log(client, GUAC_LOG_INFO, "SSH connection successful."); + guac_terminal_start(ssh_client->term); /* Start input thread */ if (pthread_create(&(input_thread), NULL, ssh_input_thread, (void*) client)) { From 286cbf32a7a178cd6483a52983a33d486edc9129 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sat, 1 Sep 2018 21:26:37 -0700 Subject: [PATCH 4/7] GUACAMOLE-622: Ensure connection to guacd is kept alive even if the SSH daemon is taking its time responding. Lengthy connect times due to DNS verification, PAM, etc. are not uncommon. --- src/protocols/ssh/ssh.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/protocols/ssh/ssh.c b/src/protocols/ssh/ssh.c index 0760f29c..cddb2fed 100644 --- a/src/protocols/ssh/ssh.c +++ b/src/protocols/ssh/ssh.c @@ -233,6 +233,9 @@ void* ssh_client_thread(void* data) { return NULL; } + /* Ensure connection is kept alive during lengthy connects */ + guac_socket_require_keep_alive(client->socket); + /* Open SSH session */ ssh_client->session = guac_common_ssh_create_session(client, settings->hostname, settings->port, ssh_client->user, settings->server_alive_interval, From 1178b475dad6b8c567041dda8e3fabd34a584f75 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sat, 1 Sep 2018 21:41:13 -0700 Subject: [PATCH 5/7] GUACAMOLE-622: Do not allow STDIN to be redirected if the terminal is not yet started. --- src/terminal/terminal-stdin-stream.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/terminal/terminal-stdin-stream.c b/src/terminal/terminal-stdin-stream.c index 1f662d67..02f5b943 100644 --- a/src/terminal/terminal-stdin-stream.c +++ b/src/terminal/terminal-stdin-stream.c @@ -103,6 +103,22 @@ static int guac_terminal_input_stream_end_handler(guac_user* user, static int __guac_terminal_send_stream(guac_terminal* term, guac_user* user, guac_stream* stream) { + /* Deny redirecting STDIN if terminal is not started */ + if (!term->started) { + + guac_user_log(user, GUAC_LOG_DEBUG, "Attempt to direct the contents " + "of an inbound stream to STDIN denied. The terminal is not " + "yet ready for input."); + + guac_protocol_send_ack(user->socket, stream, + "Terminal not yet started.", + GUAC_PROTOCOL_STATUS_RESOURCE_CONFLICT); + + guac_socket_flush(user->socket); + return 1; + + } + /* If a stream is already being used for STDIN, deny creation of * further streams */ if (term->input_stream != NULL) { From 442b1d5cc2a460a9f6d4797e9b1428c181aee2c4 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 2 Sep 2018 14:32:05 -0700 Subject: [PATCH 6/7] GUACAMOLE-622: Start terminal for telnet only after login status is known (if login success/failure detection enabled). --- src/protocols/telnet/settings.c | 87 ++++++++++++++++++++++----- src/protocols/telnet/settings.h | 24 ++++++++ src/protocols/telnet/telnet.c | 102 +++++++++++++++++++++++++------- 3 files changed, 178 insertions(+), 35 deletions(-) diff --git a/src/protocols/telnet/settings.c b/src/protocols/telnet/settings.c index bef5cfae..890d5fec 100644 --- a/src/protocols/telnet/settings.c +++ b/src/protocols/telnet/settings.c @@ -53,6 +53,8 @@ const char* GUAC_TELNET_CLIENT_ARGS[] = { "backspace", "terminal-type", "scrollback", + "login-success-regex", + "login-failure-regex", NULL }; @@ -196,12 +198,31 @@ enum TELNET_ARGS_IDX { */ IDX_SCROLLBACK, + /** + * The regular expression to use when searching for whether login was + * successful. This parameter is optional. If given, the + * "login-failure-regex" parameter must also be specified, and the first + * frame of the Guacamole connection will be withheld until login + * success/failure has been determined. + */ + IDX_LOGIN_SUCCESS_REGEX, + + /** + * The regular expression to use when searching for whether login was + * unsuccessful. This parameter is optional. If given, the + * "login-success-regex" parameter must also be specified, and the first + * frame of the Guacamole connection will be withheld until login + * success/failure has been determined. + */ + IDX_LOGIN_FAILURE_REGEX, + TELNET_ARGS_COUNT }; /** - * Compiles the given regular expression, returning NULL if compilation fails. - * The returned regex_t must be freed with regfree() AND free(). + * Compiles the given regular expression, returning NULL if compilation fails + * or of the given regular expression is NULL. The returned regex_t must be + * freed with regfree() AND free(), or with guac_telnet_regex_free(). * * @param user * The user who provided the setting associated with the given regex @@ -211,10 +232,15 @@ enum TELNET_ARGS_IDX { * The regular expression pattern to compile. * * @return - * The compiled regular expression, or NULL if compilation fails. + * The compiled regular expression, or NULL if compilation fails or NULL + * was originally provided for the pattern. */ static regex_t* guac_telnet_compile_regex(guac_user* user, char* pattern) { + /* Nothing to compile if no pattern provided */ + if (pattern == NULL) + return NULL; + int compile_result; regex_t* regex = malloc(sizeof(regex_t)); @@ -233,6 +259,14 @@ static regex_t* guac_telnet_compile_regex(guac_user* user, char* pattern) { return regex; } +void guac_telnet_regex_free(regex_t** regex) { + if (*regex != NULL) { + regfree(*regex); + free(*regex); + *regex = NULL; + } +} + guac_telnet_settings* guac_telnet_parse_args(guac_user* user, int argc, const char** argv) { @@ -256,7 +290,7 @@ guac_telnet_settings* guac_telnet_parse_args(guac_user* user, guac_user_parse_args_string(user, GUAC_TELNET_CLIENT_ARGS, argv, IDX_USERNAME, NULL); - /* Read username regex only if password is specified */ + /* Read username regex only if username is specified */ if (settings->username != NULL) { settings->username_regex = guac_telnet_compile_regex(user, guac_user_parse_args_string(user, GUAC_TELNET_CLIENT_ARGS, argv, @@ -275,6 +309,35 @@ guac_telnet_settings* guac_telnet_parse_args(guac_user* user, IDX_PASSWORD_REGEX, GUAC_TELNET_DEFAULT_PASSWORD_REGEX)); } + /* Read optional login success detection regex */ + settings->login_success_regex = guac_telnet_compile_regex(user, + guac_user_parse_args_string(user, GUAC_TELNET_CLIENT_ARGS, argv, + IDX_LOGIN_SUCCESS_REGEX, NULL)); + + /* Read optional login failure detection regex */ + settings->login_failure_regex = guac_telnet_compile_regex(user, + guac_user_parse_args_string(user, GUAC_TELNET_CLIENT_ARGS, argv, + IDX_LOGIN_FAILURE_REGEX, NULL)); + + /* Both login success and login failure regexes must be provided if either + * is present at all */ + if (settings->login_success_regex != NULL + && settings->login_failure_regex == NULL) { + guac_telnet_regex_free(&settings->login_success_regex); + guac_user_log(user, GUAC_LOG_WARNING, "Ignoring provided value for " + "\"%s\" as \"%s\" must also be provided.", + GUAC_TELNET_CLIENT_ARGS[IDX_LOGIN_SUCCESS_REGEX], + GUAC_TELNET_CLIENT_ARGS[IDX_LOGIN_FAILURE_REGEX]); + } + else if (settings->login_failure_regex != NULL + && settings->login_success_regex == NULL) { + guac_telnet_regex_free(&settings->login_failure_regex); + guac_user_log(user, GUAC_LOG_WARNING, "Ignoring provided value for " + "\"%s\" as \"%s\" must also be provided.", + GUAC_TELNET_CLIENT_ARGS[IDX_LOGIN_FAILURE_REGEX], + GUAC_TELNET_CLIENT_ARGS[IDX_LOGIN_SUCCESS_REGEX]); + } + /* Read-only mode */ settings->read_only = guac_user_parse_args_boolean(user, GUAC_TELNET_CLIENT_ARGS, argv, @@ -380,17 +443,11 @@ void guac_telnet_settings_free(guac_telnet_settings* settings) { free(settings->username); free(settings->password); - /* Free username regex (if allocated) */ - if (settings->username_regex != NULL) { - regfree(settings->username_regex); - free(settings->username_regex); - } - - /* Free password regex (if allocated) */ - if (settings->password_regex != NULL) { - regfree(settings->password_regex); - free(settings->password_regex); - } + /* Free various regexes */ + guac_telnet_regex_free(&settings->username_regex); + guac_telnet_regex_free(&settings->password_regex); + guac_telnet_regex_free(&settings->login_success_regex); + guac_telnet_regex_free(&settings->login_failure_regex); /* Free display preferences */ free(settings->font_name); diff --git a/src/protocols/telnet/settings.h b/src/protocols/telnet/settings.h index 83ec4313..86302b77 100644 --- a/src/protocols/telnet/settings.h +++ b/src/protocols/telnet/settings.h @@ -117,6 +117,20 @@ typedef struct guac_telnet_settings { */ regex_t* password_regex; + /** + * The regular expression to use when searching for whether login was + * successful. If no such regex is specified, or if no login failure regex + * was specified, this will be NULL. + */ + regex_t* login_success_regex; + + /** + * The regular expression to use when searching for whether login failed. + * If no such regex is specified, or if no login success regex was + * specified, this will be NULL. + */ + regex_t* login_failure_regex; + /** * Whether this connection is read-only, and user input should be dropped. */ @@ -253,6 +267,16 @@ typedef struct guac_telnet_settings { guac_telnet_settings* guac_telnet_parse_args(guac_user* user, int argc, const char** argv); +/** + * Frees the regex pointed to by the given pointer, assigning the value NULL to + * that pointer once the regex is freed. If the pointer already contains NULL, + * this function has no effect. + * + * @param regex + * The address of the pointer to the regex that should be freed. + */ +void guac_telnet_regex_free(regex_t** regex); + /** * Frees the given guac_telnet_settings object, having been previously * allocated via guac_telnet_parse_args(). diff --git a/src/protocols/telnet/telnet.c b/src/protocols/telnet/telnet.c index c9a636c2..36e4cac4 100644 --- a/src/protocols/telnet/telnet.c +++ b/src/protocols/telnet/telnet.c @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -84,10 +85,32 @@ static int __guac_telnet_write_all(int fd, const char* buffer, int size) { /** * Searches for a line matching the stored password regex, appending the given * buffer to the internal pattern matching buffer. The internal pattern match - * buffer is cleared whenever a newline is read. Returns TRUE if a match is found and the - * value is sent. + * buffer is cleared whenever a newline is read. Returns true if a match is + * found and the value is sent. An enter keypress is automatically sent after + * the value is sent. + * + * @param client + * The guac_client associated with the telnet session. + * + * @param regex + * The regex to search for within the output of the telnet session + * associated with the given client. + * + * @param value + * The string value to send once a match is found, or NULL if no value + * should be sent. + * + * @param buffer + * The buffer of received data to search through. + * + * @param size + * The size of the given buffer, in bytes. + * + * @return + * true if a match is found, false otherwise. */ -static bool __guac_telnet_regex_search(guac_client* client, regex_t* regex, char* value, const char* buffer, int size) { +static bool __guac_telnet_regex_search(guac_client* client, regex_t* regex, + char* value, const char* buffer, int size) { static char line_buffer[1024] = {0}; static int length = 0; @@ -123,16 +146,17 @@ static bool __guac_telnet_regex_search(guac_client* client, regex_t* regex, char if (regexec(regex, line_buffer, 0, NULL, 0) == 0) { /* Send value */ - guac_terminal_send_string(telnet_client->term, value); - guac_terminal_send_key(telnet_client->term, 0xFF0D, 1); - guac_terminal_send_key(telnet_client->term, 0xFF0D, 0); + if (value != NULL) { + guac_terminal_send_string(telnet_client->term, value); + guac_terminal_send_string(telnet_client->term, "\x0D"); + } /* Stop searching for prompt */ - return TRUE; + return true; } - return FALSE; + return false; } /** @@ -158,9 +182,7 @@ static void __guac_telnet_event_handler(telnet_t* telnet, telnet_event_t* event, settings->username_regex, settings->username, event->data.buffer, event->data.size)) { guac_client_log(client, GUAC_LOG_DEBUG, "Username sent"); - regfree(settings->username_regex); - free(settings->username_regex); - settings->username_regex = NULL; + guac_telnet_regex_free(&settings->username_regex); } } @@ -172,18 +194,52 @@ static void __guac_telnet_event_handler(telnet_t* telnet, telnet_event_t* event, guac_client_log(client, GUAC_LOG_DEBUG, "Password sent"); - /* Do not continue searching for username once password is sent */ - if (settings->username_regex != NULL) { - regfree(settings->username_regex); - free(settings->username_regex); - settings->username_regex = NULL; - } + /* Do not continue searching for username/password once password is sent */ + guac_telnet_regex_free(&settings->username_regex); + guac_telnet_regex_free(&settings->password_regex); - regfree(settings->password_regex); - free(settings->password_regex); - settings->password_regex = NULL; } } + + /* Continue search for login success */ + if (settings->login_success_regex != NULL) { + if (__guac_telnet_regex_search(client, + settings->login_success_regex, NULL, + event->data.buffer, event->data.size)) { + + /* Allow terminal to render now that login has been deemed successful */ + guac_client_log(client, GUAC_LOG_DEBUG, "Login successful"); + guac_terminal_start(telnet_client->term); + + /* Stop all searches */ + guac_telnet_regex_free(&settings->username_regex); + guac_telnet_regex_free(&settings->password_regex); + guac_telnet_regex_free(&settings->login_success_regex); + guac_telnet_regex_free(&settings->login_failure_regex); + + } + } + + /* Continue search for login failure */ + if (settings->login_failure_regex != NULL) { + if (__guac_telnet_regex_search(client, + settings->login_failure_regex, NULL, + event->data.buffer, event->data.size)) { + + /* Advise that login has failed and connection should be closed */ + guac_client_abort(client, + GUAC_PROTOCOL_STATUS_CLIENT_UNAUTHORIZED, + "Login failed"); + + /* Stop all searches */ + guac_telnet_regex_free(&settings->username_regex); + guac_telnet_regex_free(&settings->password_regex); + guac_telnet_regex_free(&settings->login_success_regex); + guac_telnet_regex_free(&settings->login_failure_regex); + + } + } + break; /* Data destined for remote end */ @@ -508,6 +564,12 @@ void* guac_telnet_client_thread(void* data) { /* Logged in */ guac_client_log(client, GUAC_LOG_INFO, "Telnet connection successful."); + /* Allow terminal to render if login success/failure detection is not + * enabled */ + if (settings->login_success_regex == NULL + && settings->login_failure_regex == NULL) + guac_terminal_start(telnet_client->term); + /* Start input thread */ if (pthread_create(&(input_thread), NULL, __guac_telnet_input_thread, (void*) client)) { guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, "Unable to start input thread"); From 462d494ed865a0fd47ef6cb22d23089d12f2b1ac Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 2 Sep 2018 22:59:18 -0700 Subject: [PATCH 7/7] GUACAMOLE-622: Match each line against all regexes. --- src/protocols/telnet/telnet.c | 252 +++++++++++++++++++--------------- 1 file changed, 143 insertions(+), 109 deletions(-) diff --git a/src/protocols/telnet/telnet.c b/src/protocols/telnet/telnet.c index 36e4cac4..17abd681 100644 --- a/src/protocols/telnet/telnet.c +++ b/src/protocols/telnet/telnet.c @@ -83,65 +83,31 @@ static int __guac_telnet_write_all(int fd, const char* buffer, int size) { } /** - * Searches for a line matching the stored password regex, appending the given - * buffer to the internal pattern matching buffer. The internal pattern match - * buffer is cleared whenever a newline is read. Returns true if a match is - * found and the value is sent. An enter keypress is automatically sent after - * the value is sent. + * Matches the given line against the given regex, returning true and sending + * the given value if a match is found. An enter keypress is automatically + * sent after the value is sent. * * @param client * The guac_client associated with the telnet session. * * @param regex - * The regex to search for within the output of the telnet session - * associated with the given client. + * The regex to search for within the given line buffer. * * @param value - * The string value to send once a match is found, or NULL if no value - * should be sent. + * The string value to send through STDIN of the telnet session if a + * match is found, or NULL if no value should be sent. * - * @param buffer - * The buffer of received data to search through. - * - * @param size - * The size of the given buffer, in bytes. + * @param line_buffer + * The line of character data to test. * * @return * true if a match is found, false otherwise. */ -static bool __guac_telnet_regex_search(guac_client* client, regex_t* regex, - char* value, const char* buffer, int size) { - - static char line_buffer[1024] = {0}; - static int length = 0; +static bool guac_telnet_regex_exec(guac_client* client, regex_t* regex, + const char* value, const char* line_buffer) { guac_telnet_client* telnet_client = (guac_telnet_client*) client->data; - int i; - const char* current; - - /* Ensure line buffer contains only the most recent line */ - current = buffer; - for (i = 0; i < size; i++) { - - /* Reset line buffer and shift input buffer for each newline */ - if (*(current++) == '\n') { - length = 0; - buffer += i; - size -= i; - i = 0; - } - } - - /* Truncate if necessary */ - if (size + length + 1 > sizeof(line_buffer)) - size = sizeof(line_buffer) - length - 1; - - /* Append to line */ - memcpy(&(line_buffer[length]), buffer, size); - length += size; - line_buffer[length] = '\0'; - /* Send value upon match */ if (regexec(regex, line_buffer, 0, NULL, 0) == 0) { @@ -157,6 +123,138 @@ static bool __guac_telnet_regex_search(guac_client* client, regex_t* regex, } return false; + +} + +/** + * Matches the given line against the various stored regexes, automatically + * sending the configured username, password, or reporting login + * success/failure depending on context. If no search is in progress, either + * because no regexes have been defined or because all applicable searches have + * completed, this function has no effect. + * + * @param client + * The guac_client associated with the telnet session. + * + * @param line_buffer + * The line of character data to test. + */ +static void guac_telnet_search_line(guac_client* client, const char* line_buffer) { + + guac_telnet_client* telnet_client = (guac_telnet_client*) client->data; + guac_telnet_settings* settings = telnet_client->settings; + + /* Continue search for username prompt */ + if (settings->username_regex != NULL) { + if (guac_telnet_regex_exec(client, settings->username_regex, + settings->username, line_buffer)) { + guac_client_log(client, GUAC_LOG_DEBUG, "Username sent"); + guac_telnet_regex_free(&settings->username_regex); + } + } + + /* Continue search for password prompt */ + if (settings->password_regex != NULL) { + if (guac_telnet_regex_exec(client, settings->password_regex, + settings->password, line_buffer)) { + + guac_client_log(client, GUAC_LOG_DEBUG, "Password sent"); + + /* Do not continue searching for username/password once password is sent */ + guac_telnet_regex_free(&settings->username_regex); + guac_telnet_regex_free(&settings->password_regex); + + } + } + + /* Continue search for login success */ + if (settings->login_success_regex != NULL) { + if (guac_telnet_regex_exec(client, settings->login_success_regex, + NULL, line_buffer)) { + + /* Allow terminal to render now that login has been deemed successful */ + guac_client_log(client, GUAC_LOG_DEBUG, "Login successful"); + guac_terminal_start(telnet_client->term); + + /* Stop all searches */ + guac_telnet_regex_free(&settings->username_regex); + guac_telnet_regex_free(&settings->password_regex); + guac_telnet_regex_free(&settings->login_success_regex); + guac_telnet_regex_free(&settings->login_failure_regex); + + } + } + + /* Continue search for login failure */ + if (settings->login_failure_regex != NULL) { + if (guac_telnet_regex_exec(client, settings->login_failure_regex, + NULL, line_buffer)) { + + /* Advise that login has failed and connection should be closed */ + guac_client_abort(client, + GUAC_PROTOCOL_STATUS_CLIENT_UNAUTHORIZED, + "Login failed"); + + /* Stop all searches */ + guac_telnet_regex_free(&settings->username_regex); + guac_telnet_regex_free(&settings->password_regex); + guac_telnet_regex_free(&settings->login_success_regex); + guac_telnet_regex_free(&settings->login_failure_regex); + + } + } + +} + +/** + * Searches for a line matching the various stored regexes, automatically + * sending the configured username, password, or reporting login + * success/failure depending on context. If no search is in progress, either + * because no regexes have been defined or because all applicable searches + * have completed, this function has no effect. + * + * @param client + * The guac_client associated with the telnet session. + * + * @param buffer + * The buffer of received data to search through. + * + * @param size + * The size of the given buffer, in bytes. + */ +static void guac_telnet_search(guac_client* client, const char* buffer, int size) { + + static char line_buffer[1024] = {0}; + static int length = 0; + + /* Append all characters in buffer to current line */ + const char* current = buffer; + for (int i = 0; i < size; i++) { + + char c = *(current++); + + /* Attempt pattern match and clear buffer upon reading newline */ + if (c == '\n') { + if (length > 0) { + line_buffer[length] = '\0'; + guac_telnet_search_line(client, line_buffer); + length = 0; + } + } + + /* Append all non-newline characters to line buffer as long as space + * remains */ + else if (length < sizeof(line_buffer) - 1) + line_buffer[length++] = c; + + } + + /* Attempt pattern match if an unfinished line remains (may be a prompt) */ + if (length > 0) { + line_buffer[length] = '\0'; + guac_telnet_search_line(client, line_buffer); + } + } /** @@ -175,71 +273,7 @@ static void __guac_telnet_event_handler(telnet_t* telnet, telnet_event_t* event, /* Terminal output received */ case TELNET_EV_DATA: guac_terminal_write(telnet_client->term, event->data.buffer, event->data.size); - - /* Continue search for username prompt */ - if (settings->username_regex != NULL) { - if (__guac_telnet_regex_search(client, - settings->username_regex, settings->username, - event->data.buffer, event->data.size)) { - guac_client_log(client, GUAC_LOG_DEBUG, "Username sent"); - guac_telnet_regex_free(&settings->username_regex); - } - } - - /* Continue search for password prompt */ - if (settings->password_regex != NULL) { - if (__guac_telnet_regex_search(client, - settings->password_regex, settings->password, - event->data.buffer, event->data.size)) { - - guac_client_log(client, GUAC_LOG_DEBUG, "Password sent"); - - /* Do not continue searching for username/password once password is sent */ - guac_telnet_regex_free(&settings->username_regex); - guac_telnet_regex_free(&settings->password_regex); - - } - } - - /* Continue search for login success */ - if (settings->login_success_regex != NULL) { - if (__guac_telnet_regex_search(client, - settings->login_success_regex, NULL, - event->data.buffer, event->data.size)) { - - /* Allow terminal to render now that login has been deemed successful */ - guac_client_log(client, GUAC_LOG_DEBUG, "Login successful"); - guac_terminal_start(telnet_client->term); - - /* Stop all searches */ - guac_telnet_regex_free(&settings->username_regex); - guac_telnet_regex_free(&settings->password_regex); - guac_telnet_regex_free(&settings->login_success_regex); - guac_telnet_regex_free(&settings->login_failure_regex); - - } - } - - /* Continue search for login failure */ - if (settings->login_failure_regex != NULL) { - if (__guac_telnet_regex_search(client, - settings->login_failure_regex, NULL, - event->data.buffer, event->data.size)) { - - /* Advise that login has failed and connection should be closed */ - guac_client_abort(client, - GUAC_PROTOCOL_STATUS_CLIENT_UNAUTHORIZED, - "Login failed"); - - /* Stop all searches */ - guac_telnet_regex_free(&settings->username_regex); - guac_telnet_regex_free(&settings->password_regex); - guac_telnet_regex_free(&settings->login_success_regex); - guac_telnet_regex_free(&settings->login_failure_regex); - - } - } - + guac_telnet_search(client, event->data.buffer, event->data.size); break; /* Data destined for remote end */