/* * 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 "common-ssh/sftp.h" #include "common-ssh/ssh.h" #include "common-ssh/ssh-constants.h" #include "settings.h" #include "sftp.h" #include "ssh.h" #include "terminal/terminal.h" #include "ttymode.h" #ifdef ENABLE_SSH_AGENT #include "ssh_agent.h" #endif #include <libssh2.h> #include <libssh2_sftp.h> #include <guacamole/client.h> #include <guacamole/wol.h> #include <openssl/err.h> #include <openssl/ssl.h> #ifdef LIBSSH2_USES_GCRYPT #include <gcrypt.h> #endif #include <errno.h> #include <netdb.h> #include <netinet/in.h> #include <poll.h> #include <pthread.h> #include <stdbool.h> #include <stddef.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <sys/time.h> /** * A data structure that maps parameter names of credentials that will be * prompted to the actual text that will be displayed on the screen. */ typedef struct guac_ssh_credential_prompt_map { /** * The parameter name of the credential that is being prompted for. */ char* name; /** * The text that will display to the user during the prompt. */ char* prompt; } guac_ssh_credential_prompt_map; /** * The map of credentials the user can be prompted for to the text displayed * on the screen. */ guac_ssh_credential_prompt_map ssh_credential_prompt_map[] = { { GUAC_SSH_PARAMETER_NAME_USERNAME, "Login as: " }, { GUAC_SSH_PARAMETER_NAME_PASSWORD, "Password: " }, { GUAC_SSH_PARAMETER_NAME_PASSPHRASE, "Key passphrase: " }, { NULL, NULL} }; /** * This function generates a prompt to the specified instance of guac_client * for the credential specified in the cred_name parameter, which should * be a valid SSH connection parameter. * * @param client * The guac_client object associated with the current connection * where additional credentials are required. * * @param cred_name * The name of the parameter to prompt for in the client. */ static void guac_ssh_get_credential(guac_client *client, char* cred_name) { guac_ssh_client* ssh_client = (guac_ssh_client*) client->data; /* Lock the terminal thread while prompting for the credential. */ pthread_mutex_lock(&(ssh_client->ssh_credential_lock)); /* Let the owner know what we require. */ guac_client_owner_send_required(client, (const char* []) {cred_name, NULL}); /* Wait for the response, and then unlock the thread. */ pthread_cond_wait(&(ssh_client->ssh_credential_cond), &(ssh_client->ssh_credential_lock)); pthread_mutex_unlock(&(ssh_client->ssh_credential_lock)); } /** * Produces a new user object containing a username and password or private * key, prompting the user as necessary to obtain that information. * * @param client * The Guacamole client containing any existing user data, as well as the * terminal to use when prompting the user. * * @return * A new user object containing the user's username and other credentials, * or NULL if fails to import key. */ static guac_common_ssh_user* guac_ssh_get_user(guac_client* client) { guac_ssh_client* ssh_client = (guac_ssh_client*) client->data; guac_ssh_settings* settings = ssh_client->settings; guac_common_ssh_user* user; /* Get username */ while (settings->username == NULL) { /* Client owner supports required instruction, so send prompt(s) that way. */ if (guac_client_owner_supports_required(client)) { guac_ssh_get_credential(client, GUAC_SSH_PARAMETER_NAME_USERNAME); } /* Fall back to terminal prompting. */ else { settings->username = guac_terminal_prompt(ssh_client->term, "Login as: ", true); } } /* Create user object from username */ user = guac_common_ssh_create_user(settings->username); /* If key specified, import */ if (settings->key_base64 != NULL) { guac_client_log(client, GUAC_LOG_DEBUG, "Attempting private key import (WITHOUT passphrase)"); /* Attempt to read key without passphrase */ if (guac_common_ssh_user_import_key(user, settings->key_base64, NULL)) { /* Log failure of initial attempt */ guac_client_log(client, GUAC_LOG_DEBUG, "Initial import failed: %s", guac_common_ssh_key_error()); guac_client_log(client, GUAC_LOG_DEBUG, "Re-attempting private key import (WITH passphrase)"); /* Prompt for passphrase if missing */ while (settings->key_passphrase == NULL) { /* Send prompt via required instruction, if supported */ if (guac_client_owner_supports_required(client)) guac_ssh_get_credential(client, GUAC_SSH_PARAMETER_NAME_PASSPHRASE); /* Fall back to terminal prompt */ else settings->key_passphrase = guac_terminal_prompt(ssh_client->term, "Key passphrase: ", true); } /* Reattempt import with passphrase */ if (guac_common_ssh_user_import_key(user, settings->key_base64, settings->key_passphrase)) { /* If still failing, give up */ guac_client_abort(client, GUAC_PROTOCOL_STATUS_CLIENT_UNAUTHORIZED, "Auth key import failed: %s", guac_common_ssh_key_error()); guac_common_ssh_destroy_user(user); return NULL; } } /* end decrypt key with passphrase */ /* Success */ guac_client_log(client, GUAC_LOG_INFO, "Auth key successfully imported."); } /* end if key given */ /* If available, get password from settings */ else if (settings->password != NULL) { guac_common_ssh_user_set_password(user, settings->password); } /* Clear screen of any prompts */ guac_terminal_printf(ssh_client->term, "\x1B[H\x1B[J"); return user; } void* ssh_input_thread(void* data) { guac_client* client = (guac_client*) data; guac_ssh_client* ssh_client = (guac_ssh_client*) client->data; char buffer[8192]; int bytes_read; /* Write all data read */ while ((bytes_read = guac_terminal_read_stdin(ssh_client->term, buffer, sizeof(buffer))) > 0) { pthread_mutex_lock(&(ssh_client->term_channel_lock)); libssh2_channel_write(ssh_client->term_channel, buffer, bytes_read); pthread_mutex_unlock(&(ssh_client->term_channel_lock)); /* Make sure ssh_input_thread can be terminated anyway */ if (client->state == GUAC_CLIENT_STOPPING) break; } /* Stop the client so that ssh_client_thread can be terminated */ guac_client_stop(client); return NULL; } void* ssh_client_thread(void* data) { guac_client* client = (guac_client*) data; guac_ssh_client* ssh_client = (guac_ssh_client*) client->data; guac_ssh_settings* settings = ssh_client->settings; char buffer[8192]; pthread_t input_thread; /* If Wake-on-LAN is enabled, attempt to wake. */ if (settings->wol_send_packet) { guac_client_log(client, GUAC_LOG_DEBUG, "Sending Wake-on-LAN packet, " "and pausing for %d seconds.", settings->wol_wait_time); /* Send the Wake-on-LAN request. */ if (guac_wol_wake(settings->wol_mac_addr, settings->wol_broadcast_addr)) return NULL; /* If wait time is specified, sleep for that amount of time. */ if (settings->wol_wait_time > 0) guac_timestamp_msleep(settings->wol_wait_time * 1000); } /* Init SSH base libraries */ if (guac_common_ssh_init(client)) { guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, "SSH library initialization failed"); return NULL; } char ssh_ttymodes[GUAC_SSH_TTYMODES_SIZE(1)]; /* Set up screen recording, if requested */ if (settings->recording_path != NULL) { ssh_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 */ ssh_client->term = guac_terminal_create(client, ssh_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 (ssh_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_ssh_send_current_argv, ssh_client); /* Set up typescript, if requested */ if (settings->typescript_path != NULL) { guac_terminal_create_typescript(ssh_client->term, settings->typescript_path, settings->typescript_name, settings->create_typescript_path); } /* Get user and credentials */ ssh_client->user = guac_ssh_get_user(client); if (ssh_client->user == NULL) { /* Already aborted within guac_ssh_get_user() */ return NULL; } /* Open SSH session */ ssh_client->session = guac_common_ssh_create_session(client, settings->hostname, settings->port, ssh_client->user, settings->server_alive_interval, settings->host_key, guac_ssh_get_credential); if (ssh_client->session == NULL) { /* Already aborted within guac_common_ssh_create_session() */ return NULL; } pthread_mutex_init(&ssh_client->term_channel_lock, NULL); pthread_mutex_init(&ssh_client->ssh_credential_lock, NULL); pthread_cond_init(&(ssh_client->ssh_credential_cond), NULL); /* Open channel for terminal */ ssh_client->term_channel = libssh2_channel_open_session(ssh_client->session->session); if (ssh_client->term_channel == NULL) { guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR, "Unable to open terminal channel."); return NULL; } /* Set the client timezone */ if (settings->timezone != NULL) { if (libssh2_channel_setenv(ssh_client->term_channel, "TZ", settings->timezone)) { guac_client_log(client, GUAC_LOG_WARNING, "Unable to set the timezone: SSH server " "refused to set \"TZ\" variable."); } } #ifdef ENABLE_SSH_AGENT /* Start SSH agent forwarding, if enabled */ if (ssh_client->enable_agent) { libssh2_session_callback_set(ssh_client->session, LIBSSH2_CALLBACK_AUTH_AGENT, (void*) ssh_auth_agent_callback); /* Request agent forwarding */ if (libssh2_channel_request_auth_agent(ssh_client->term_channel)) guac_client_log(client, GUAC_LOG_ERROR, "Agent forwarding request failed"); else guac_client_log(client, GUAC_LOG_INFO, "Agent forwarding enabled."); } ssh_client->auth_agent = NULL; #endif /* Start SFTP session as well, if enabled */ if (settings->enable_sftp) { /* Create SSH session specific for SFTP */ guac_client_log(client, GUAC_LOG_DEBUG, "Reconnecting for SFTP..."); ssh_client->sftp_session = guac_common_ssh_create_session(client, settings->hostname, settings->port, ssh_client->user, settings->server_alive_interval, settings->host_key, NULL); if (ssh_client->sftp_session == NULL) { /* Already aborted within guac_common_ssh_create_session() */ return NULL; } /* Request SFTP */ ssh_client->sftp_filesystem = guac_common_ssh_create_sftp_filesystem( ssh_client->sftp_session, settings->sftp_root_directory, NULL, settings->sftp_disable_download, settings->sftp_disable_upload); /* Expose filesystem to connection owner */ guac_client_for_owner(client, guac_common_ssh_expose_sftp_filesystem, ssh_client->sftp_filesystem); /* Init handlers for Guacamole-specific console codes */ if (!settings->sftp_disable_upload) ssh_client->term->upload_path_handler = guac_sftp_set_upload_path; if (!settings->sftp_disable_download) ssh_client->term->file_download_handler = guac_sftp_download_file; guac_client_log(client, GUAC_LOG_DEBUG, "SFTP session initialized"); } /* Set up the ttymode array prior to requesting the PTY */ int ttymodeBytes = guac_ssh_ttymodes_init(ssh_ttymodes, GUAC_SSH_TTY_OP_VERASE, settings->backspace, GUAC_SSH_TTY_OP_END); if (ttymodeBytes < 1) guac_client_log(client, GUAC_LOG_WARNING, "Unable to set TTY modes." " Backspace may not work as expected."); /* Request PTY */ if (libssh2_channel_request_pty_ex(ssh_client->term_channel, settings->terminal_type, strlen(settings->terminal_type), ssh_ttymodes, ttymodeBytes, ssh_client->term->term_width, ssh_client->term->term_height, 0, 0)) { guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR, "Unable to allocate PTY."); return NULL; } /* Forward specified locale */ if (settings->locale != NULL) { if (libssh2_channel_setenv(ssh_client->term_channel, "LANG", settings->locale)) { guac_client_log(client, GUAC_LOG_WARNING, "Unable to forward locale: SSH server refused to set " "\"LANG\" environment variable."); } } /* If a command is specified, run that instead of a shell */ if (settings->command != NULL) { if (libssh2_channel_exec(ssh_client->term_channel, settings->command)) { guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR, "Unable to execute command."); return NULL; } } /* Otherwise, request a shell */ else if (libssh2_channel_shell(ssh_client->term_channel)) { guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR, "Unable to associate shell with PTY."); return NULL; } /* 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)) { guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, "Unable to start input thread"); return NULL; } /* Set non-blocking */ libssh2_session_set_blocking(ssh_client->session->session, 0); /* While data available, write to terminal */ int bytes_read = 0; for (;;) { /* Track total amount of data read */ int total_read = 0; /* Timeout for polling socket activity */ int timeout; pthread_mutex_lock(&(ssh_client->term_channel_lock)); /* Stop reading at EOF */ if (libssh2_channel_eof(ssh_client->term_channel)) { pthread_mutex_unlock(&(ssh_client->term_channel_lock)); break; } /* Client is stopping, break the loop */ if (client->state == GUAC_CLIENT_STOPPING) { pthread_mutex_unlock(&(ssh_client->term_channel_lock)); break; } /* Send keepalive at configured interval */ if (settings->server_alive_interval > 0) { timeout = 0; if (libssh2_keepalive_send(ssh_client->session->session, &timeout) > 0) break; timeout *= 1000; } /* If keepalive is not configured, sleep for the default of 1 second */ else timeout = GUAC_SSH_DEFAULT_POLL_TIMEOUT; /* Read terminal data */ bytes_read = libssh2_channel_read(ssh_client->term_channel, buffer, sizeof(buffer)); pthread_mutex_unlock(&(ssh_client->term_channel_lock)); /* Attempt to write data received. Exit on failure. */ if (bytes_read > 0) { int written = guac_terminal_write(ssh_client->term, buffer, bytes_read); if (written < 0) break; total_read += bytes_read; } else if (bytes_read < 0 && bytes_read != LIBSSH2_ERROR_EAGAIN) break; #ifdef ENABLE_SSH_AGENT /* If agent open, handle any agent packets */ if (ssh_client->auth_agent != NULL) { bytes_read = ssh_auth_agent_read(ssh_client->auth_agent); if (bytes_read > 0) total_read += bytes_read; else if (bytes_read < 0 && bytes_read != LIBSSH2_ERROR_EAGAIN) ssh_client->auth_agent = NULL; } #endif /* Wait for more data if reads turn up empty */ if (total_read == 0) { /* Wait on the SSH session file descriptor only */ struct pollfd fds[] = {{ .fd = ssh_client->session->fd, .events = POLLIN, .revents = 0, }}; /* Wait up to computed timeout */ if (poll(fds, 1, timeout) < 0) break; } } /* Kill client and Wait for input thread to die */ guac_client_stop(client); pthread_join(input_thread, NULL); pthread_cond_destroy(&(ssh_client->ssh_credential_cond)); pthread_mutex_destroy(&ssh_client->term_channel_lock); guac_client_log(client, GUAC_LOG_INFO, "SSH connection ended."); return NULL; }