/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ #include "config.h" #include "argv.h" #include "common/recording.h" #include "telnet.h" #include "terminal/terminal.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include /** * Support levels for various telnet options, required for connection * negotiation by telnet_init(), part of libtelnet. */ static const telnet_telopt_t __telnet_options[] = { { TELNET_TELOPT_ECHO, TELNET_WONT, TELNET_DO }, { TELNET_TELOPT_TTYPE, TELNET_WILL, TELNET_DONT }, { TELNET_TELOPT_COMPRESS2, TELNET_WONT, TELNET_DO }, { TELNET_TELOPT_MSSP, TELNET_WONT, TELNET_DO }, { TELNET_TELOPT_NAWS, TELNET_WILL, TELNET_DONT }, { TELNET_TELOPT_NEW_ENVIRON, TELNET_WILL, TELNET_DONT }, { -1, 0, 0 } }; /** * Write the entire buffer given to the specified file descriptor, retrying * the write automatically if necessary. This function will return a value * not equal to the buffer's size iff an error occurs which prevents all * future writes. * * @param fd The file descriptor to write to. * @param buffer The buffer to write. * @param size The number of bytes from the buffer to write. */ static int __guac_telnet_write_all(int fd, const char* buffer, int size) { int remaining = size; while (remaining > 0) { /* Attempt to write data */ int ret_val = write(fd, buffer, remaining); if (ret_val <= 0) return -1; /* If successful, contine with what data remains (if any) */ remaining -= ret_val; buffer += ret_val; } return size; } /** * 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_exec(guac_client* client, regex_t* regex, const char* value, const char* line_buffer) { guac_telnet_client* telnet_client = (guac_telnet_client*) client->data; /* 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_string(telnet_client->term, "\x0D"); } /* Stop searching for prompt */ return true; } 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); } } /** * Event handler, as defined by libtelnet. This function is passed to * telnet_init() and will be called for every event fired by libtelnet, * including feature enable/disable and receipt/transmission of data. */ static void __guac_telnet_event_handler(telnet_t* telnet, telnet_event_t* event, void* data) { guac_client* client = (guac_client*) data; guac_telnet_client* telnet_client = (guac_telnet_client*) client->data; guac_telnet_settings* settings = telnet_client->settings; switch (event->type) { /* Terminal output received */ case TELNET_EV_DATA: guac_terminal_write(telnet_client->term, event->data.buffer, event->data.size); guac_telnet_search(client, event->data.buffer, event->data.size); break; /* Data destined for remote end */ case TELNET_EV_SEND: if (__guac_telnet_write_all(telnet_client->socket_fd, event->data.buffer, event->data.size) != event->data.size) guac_client_stop(client); break; /* Remote feature enabled */ case TELNET_EV_WILL: if (event->neg.telopt == TELNET_TELOPT_ECHO) telnet_client->echo_enabled = 0; /* Disable local echo, as remote will echo */ break; /* Remote feature disabled */ case TELNET_EV_WONT: if (event->neg.telopt == TELNET_TELOPT_ECHO) telnet_client->echo_enabled = 1; /* Enable local echo, as remote won't echo */ break; /* Local feature enable */ case TELNET_EV_DO: if (event->neg.telopt == TELNET_TELOPT_NAWS) { telnet_client->naws_enabled = 1; guac_telnet_send_naws(telnet, telnet_client->term->term_width, telnet_client->term->term_height); } break; /* Terminal type request */ case TELNET_EV_TTYPE: if (event->ttype.cmd == TELNET_TTYPE_SEND) telnet_ttype_is(telnet_client->telnet, settings->terminal_type); break; /* Environment request */ case TELNET_EV_ENVIRON: /* Only send USER if entire environment was requested */ if (event->environ.size == 0) guac_telnet_send_user(telnet, settings->username); break; /* Connection warnings */ case TELNET_EV_WARNING: guac_client_log(client, GUAC_LOG_WARNING, "%s", event->error.msg); break; /* Connection errors */ case TELNET_EV_ERROR: guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR, "Telnet connection closing with error: %s", event->error.msg); break; /* Ignore other events */ default: break; } } /** * Input thread, started by the main telnet client thread. This thread * continuously reads from the terminal's STDIN and transfers all read * data to the telnet connection. * * @param data The current guac_client instance. * @return Always NULL. */ static void* __guac_telnet_input_thread(void* data) { guac_client* client = (guac_client*) data; guac_telnet_client* telnet_client = (guac_telnet_client*) client->data; char buffer[8192]; int bytes_read; /* Write all data read */ while ((bytes_read = guac_terminal_read_stdin(telnet_client->term, buffer, sizeof(buffer))) > 0) { telnet_send(telnet_client->telnet, buffer, bytes_read); if (telnet_client->echo_enabled) guac_terminal_write(telnet_client->term, buffer, bytes_read); } return NULL; } /** * Connects to the telnet server specified within the data associated * with the given guac_client, which will have been populated by * guac_client_init. * * @return The connected telnet instance, if successful, or NULL if the * connection fails for any reason. */ static telnet_t* __guac_telnet_create_session(guac_client* client) { int retval; int fd; struct addrinfo* addresses; struct addrinfo* current_address; char connected_address[1024]; char connected_port[64]; guac_telnet_client* telnet_client = (guac_telnet_client*) client->data; guac_telnet_settings* settings = telnet_client->settings; struct addrinfo hints = { .ai_family = AF_UNSPEC, .ai_socktype = SOCK_STREAM, .ai_protocol = IPPROTO_TCP }; /* Get socket */ fd = socket(AF_INET, SOCK_STREAM, 0); /* Get addresses connection */ if ((retval = getaddrinfo(settings->hostname, settings->port, &hints, &addresses))) { guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, "Error parsing given address or port: %s", gai_strerror(retval)); return NULL; } /* Attempt connection to each address until success */ current_address = addresses; while (current_address != NULL) { int retval; /* Resolve hostname */ if ((retval = getnameinfo(current_address->ai_addr, current_address->ai_addrlen, connected_address, sizeof(connected_address), connected_port, sizeof(connected_port), NI_NUMERICHOST | NI_NUMERICSERV))) guac_client_log(client, GUAC_LOG_DEBUG, "Unable to resolve host: %s", gai_strerror(retval)); /* Connect */ if (connect(fd, current_address->ai_addr, current_address->ai_addrlen) == 0) { guac_client_log(client, GUAC_LOG_DEBUG, "Successfully connected to " "host %s, port %s", connected_address, connected_port); /* Done if successful connect */ break; } /* Otherwise log information regarding bind failure */ else guac_client_log(client, GUAC_LOG_DEBUG, "Unable to connect to " "host %s, port %s: %s", connected_address, connected_port, strerror(errno)); current_address = current_address->ai_next; } /* If unable to connect to anything, fail */ if (current_address == NULL) { guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_NOT_FOUND, "Unable to connect to any addresses."); return NULL; } /* Free addrinfo */ freeaddrinfo(addresses); /* Open telnet session */ telnet_t* telnet = telnet_init(__telnet_options, __guac_telnet_event_handler, 0, client); if (telnet == NULL) { guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, "Telnet client allocation failed."); return NULL; } /* Save file descriptor */ telnet_client->socket_fd = fd; return telnet; } /** * Sends a 16-bit value over the given telnet connection with the byte order * required by the telnet protocol. * * @param telnet The telnet connection to use. * @param value The value to send. */ static void __guac_telnet_send_uint16(telnet_t* telnet, uint16_t value) { unsigned char buffer[2]; buffer[0] = (value >> 8) & 0xFF; buffer[1] = value & 0xFF; telnet_send(telnet, (char*) buffer, 2); } /** * Sends an 8-bit value over the given telnet connection. * * @param telnet The telnet connection to use. * @param value The value to send. */ static void __guac_telnet_send_uint8(telnet_t* telnet, uint8_t value) { telnet_send(telnet, (char*) (&value), 1); } void guac_telnet_send_naws(telnet_t* telnet, uint16_t width, uint16_t height) { telnet_begin_sb(telnet, TELNET_TELOPT_NAWS); __guac_telnet_send_uint16(telnet, width); __guac_telnet_send_uint16(telnet, height); telnet_finish_sb(telnet); } void guac_telnet_send_user(telnet_t* telnet, const char* username) { /* IAC SB NEW-ENVIRON IS */ telnet_begin_sb(telnet, TELNET_TELOPT_NEW_ENVIRON); __guac_telnet_send_uint8(telnet, TELNET_ENVIRON_IS); /* Only send username if defined */ if (username != NULL) { /* VAR "USER" */ __guac_telnet_send_uint8(telnet, TELNET_ENVIRON_VAR); telnet_send(telnet, "USER", 4); /* VALUE username */ __guac_telnet_send_uint8(telnet, TELNET_ENVIRON_VALUE); telnet_send(telnet, username, strlen(username)); } /* IAC SE */ telnet_finish_sb(telnet); } /** * Waits for data on the given file descriptor for up to one second. The * return value is identical to that of select(): 0 on timeout, < 0 on * error, and > 0 on success. * * @param socket_fd The file descriptor to wait for. * @return A value greater than zero on success, zero on timeout, and * less than zero on error. */ static int __guac_telnet_wait(int socket_fd) { /* Build array of file descriptors */ struct pollfd fds[] = {{ .fd = socket_fd, .events = POLLIN, .revents = 0, }}; /* Wait for one second */ return poll(fds, 1, 1000); } void* guac_telnet_client_thread(void* data) { guac_client* client = (guac_client*) data; guac_telnet_client* telnet_client = (guac_telnet_client*) client->data; guac_telnet_settings* settings = telnet_client->settings; pthread_t input_thread; char buffer[8192]; int wait_result; /* Set up screen recording, if requested */ if (settings->recording_path != NULL) { telnet_client->recording = guac_common_recording_create(client, settings->recording_path, settings->recording_name, settings->create_recording_path, !settings->recording_exclude_output, !settings->recording_exclude_mouse, settings->recording_include_keys); } /* Create terminal */ telnet_client->term = guac_terminal_create(client, telnet_client->clipboard, settings->disable_copy, settings->max_scrollback, settings->font_name, settings->font_size, settings->resolution, settings->width, settings->height, settings->color_scheme, settings->backspace); /* Fail if terminal init failed */ if (telnet_client->term == NULL) { guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, "Terminal initialization failed"); return NULL; } /* Send current values of exposed arguments to owner only */ guac_client_for_owner(client, guac_telnet_send_current_argv, telnet_client); /* Set up typescript, if requested */ if (settings->typescript_path != NULL) { guac_terminal_create_typescript(telnet_client->term, settings->typescript_path, settings->typescript_name, settings->create_typescript_path); } /* Open telnet session */ telnet_client->telnet = __guac_telnet_create_session(client); if (telnet_client->telnet == NULL) { /* Already aborted within __guac_telnet_create_session() */ return NULL; } /* 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"); return NULL; } /* While data available, write to terminal */ while ((wait_result = __guac_telnet_wait(telnet_client->socket_fd)) >= 0) { /* Resume waiting of no data available */ if (wait_result == 0) continue; int bytes_read = read(telnet_client->socket_fd, buffer, sizeof(buffer)); if (bytes_read <= 0) break; telnet_recv(telnet_client->telnet, buffer, bytes_read); } /* Kill client and Wait for input thread to die */ guac_client_stop(client); pthread_join(input_thread, NULL); guac_client_log(client, GUAC_LOG_INFO, "Telnet connection ended."); return NULL; }