GUACAMOLE-622: Merge withold first terminal frame until connection is verified.

This commit is contained in:
Nick Couchman 2018-09-21 20:26:47 -04:00
commit 54fda21366
7 changed files with 343 additions and 91 deletions

View File

@ -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,
@ -335,6 +338,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)) {

View File

@ -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);

View File

@ -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().

View File

@ -31,6 +31,7 @@
#include <netinet/in.h>
#include <poll.h>
#include <pthread.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
@ -82,57 +83,178 @@ 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.
* 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 given line buffer.
*
* @param value
* 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 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) {
/* Send value */
if (value != NULL) {
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);
guac_terminal_send_string(telnet_client->term, "\x0D");
}
/* Stop searching for prompt */
return TRUE;
return true;
}
return FALSE;
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);
}
}
/**
@ -151,39 +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");
regfree(settings->username_regex);
free(settings->username_regex);
settings->username_regex = NULL;
}
}
/* 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 once password is sent */
if (settings->username_regex != NULL) {
regfree(settings->username_regex);
free(settings->username_regex);
settings->username_regex = NULL;
}
regfree(settings->password_regex);
free(settings->password_regex);
settings->password_regex = NULL;
}
}
guac_telnet_search(client, event->data.buffer, event->data.size);
break;
/* Data destined for remote end */
@ -508,6 +598,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");

View File

@ -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) {

View File

@ -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);
@ -934,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);
@ -1672,6 +1684,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 +1864,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;

View File

@ -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.
@ -625,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