ssh.c (18019B)
1/* 2 * Licensed to the Apache Software Foundation (ASF) under one 3 * or more contributor license agreements. See the NOTICE file 4 * distributed with this work for additional information 5 * regarding copyright ownership. The ASF licenses this file 6 * to you under the Apache License, Version 2.0 (the 7 * "License"); you may not use this file except in compliance 8 * with the License. You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, 13 * software distributed under the License is distributed on an 14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 * KIND, either express or implied. See the License for the 16 * specific language governing permissions and limitations 17 * under the License. 18 */ 19 20#include "config.h" 21 22#include "argv.h" 23#include "common-ssh/sftp.h" 24#include "common-ssh/ssh.h" 25#include "settings.h" 26#include "sftp.h" 27#include "ssh.h" 28#include "terminal/terminal.h" 29#include "ttymode.h" 30 31#ifdef ENABLE_SSH_AGENT 32#include "ssh_agent.h" 33#endif 34 35#include <libssh2.h> 36#include <libssh2_sftp.h> 37#include <guacamole/client.h> 38#include <guacamole/mem.h> 39#include <guacamole/recording.h> 40#include <guacamole/socket.h> 41#include <guacamole/timestamp.h> 42#include <guacamole/wol.h> 43#include <openssl/err.h> 44#include <openssl/ssl.h> 45 46#ifdef LIBSSH2_USES_GCRYPT 47#include <gcrypt.h> 48#endif 49 50#include <errno.h> 51#include <netdb.h> 52#include <netinet/in.h> 53#include <poll.h> 54#include <pthread.h> 55#include <stdbool.h> 56#include <stddef.h> 57#include <stdio.h> 58#include <stdlib.h> 59#include <string.h> 60#include <sys/socket.h> 61#include <sys/time.h> 62 63/** 64 * Produces a new user object containing a username and password or private 65 * key, prompting the user as necessary to obtain that information. 66 * 67 * @param client 68 * The Guacamole client containing any existing user data, as well as the 69 * terminal to use when prompting the user. 70 * 71 * @return 72 * A new user object containing the user's username and other credentials, 73 * or NULL if fails to import key. 74 */ 75static guac_common_ssh_user* guac_ssh_get_user(guac_client* client) { 76 77 guac_ssh_client* ssh_client = (guac_ssh_client*) client->data; 78 guac_ssh_settings* settings = ssh_client->settings; 79 80 guac_common_ssh_user* user; 81 82 /* Get username */ 83 if (settings->username == NULL) 84 settings->username = guac_terminal_prompt(ssh_client->term, 85 "Login as: ", true); 86 87 /* Create user object from username */ 88 user = guac_common_ssh_create_user(settings->username); 89 90 /* If key specified, import */ 91 if (settings->key_base64 != NULL) { 92 93 guac_client_log(client, GUAC_LOG_DEBUG, 94 "Attempting private key import (WITHOUT passphrase)"); 95 96 /* Attempt to read key without passphrase */ 97 if (guac_common_ssh_user_import_key(user, 98 settings->key_base64, NULL)) { 99 100 /* Log failure of initial attempt */ 101 guac_client_log(client, GUAC_LOG_DEBUG, 102 "Initial import failed: %s", 103 guac_common_ssh_key_error()); 104 105 guac_client_log(client, GUAC_LOG_DEBUG, 106 "Re-attempting private key import (WITH passphrase)"); 107 108 /* Prompt for passphrase if missing */ 109 if (settings->key_passphrase == NULL) 110 settings->key_passphrase = 111 guac_terminal_prompt(ssh_client->term, 112 "Key passphrase: ", false); 113 114 /* Reattempt import with passphrase */ 115 if (guac_common_ssh_user_import_key(user, 116 settings->key_base64, 117 settings->key_passphrase)) { 118 119 /* If still failing, give up */ 120 guac_client_abort(client, 121 GUAC_PROTOCOL_STATUS_CLIENT_UNAUTHORIZED, 122 "Auth key import failed: %s", 123 guac_common_ssh_key_error()); 124 125 guac_common_ssh_destroy_user(user); 126 return NULL; 127 128 } 129 130 } /* end decrypt key with passphrase */ 131 132 /* Success */ 133 guac_client_log(client, GUAC_LOG_INFO, 134 "Auth key successfully imported."); 135 136 } /* end if key given */ 137 138 /* If available, get password from settings */ 139 else if (settings->password != NULL) { 140 guac_common_ssh_user_set_password(user, settings->password); 141 } 142 143 /* Clear screen of any prompts */ 144 guac_terminal_printf(ssh_client->term, "\x1B[H\x1B[J"); 145 146 return user; 147 148} 149 150/** 151 * A function used to generate a terminal prompt to gather additional 152 * credentials from the guac_client during a connection, and using 153 * the specified string to generate the prompt for the user. 154 * 155 * @param client 156 * The guac_client object associated with the current connection 157 * where additional credentials are required. 158 * 159 * @param cred_name 160 * The prompt text to display to the screen when prompting for the 161 * additional credentials. 162 * 163 * @return 164 * The string of credentials gathered from the user. 165 */ 166static char* guac_ssh_get_credential(guac_client *client, char* cred_name) { 167 168 guac_ssh_client* ssh_client = (guac_ssh_client*) client->data; 169 return guac_terminal_prompt(ssh_client->term, cred_name, false); 170 171} 172 173void* ssh_input_thread(void* data) { 174 175 guac_client* client = (guac_client*) data; 176 guac_ssh_client* ssh_client = (guac_ssh_client*) client->data; 177 178 char buffer[8192]; 179 int bytes_read; 180 181 /* Write all data read */ 182 while ((bytes_read = guac_terminal_read_stdin(ssh_client->term, buffer, sizeof(buffer))) > 0) { 183 pthread_mutex_lock(&(ssh_client->term_channel_lock)); 184 libssh2_channel_write(ssh_client->term_channel, buffer, bytes_read); 185 pthread_mutex_unlock(&(ssh_client->term_channel_lock)); 186 187 /* Make sure ssh_input_thread can be terminated anyway */ 188 if (client->state == GUAC_CLIENT_STOPPING) 189 break; 190 } 191 192 /* Stop the client so that ssh_client_thread can be terminated */ 193 guac_client_stop(client); 194 return NULL; 195 196} 197 198void* ssh_client_thread(void* data) { 199 200 guac_client* client = (guac_client*) data; 201 guac_ssh_client* ssh_client = (guac_ssh_client*) client->data; 202 guac_ssh_settings* settings = ssh_client->settings; 203 204 char buffer[8192]; 205 206 pthread_t input_thread; 207 208 /* If Wake-on-LAN is enabled, attempt to wake. */ 209 if (settings->wol_send_packet) { 210 guac_client_log(client, GUAC_LOG_DEBUG, "Sending Wake-on-LAN packet, " 211 "and pausing for %d seconds.", settings->wol_wait_time); 212 213 /* Send the Wake-on-LAN request. */ 214 if (guac_wol_wake(settings->wol_mac_addr, settings->wol_broadcast_addr, 215 settings->wol_udp_port)) 216 return NULL; 217 218 /* If wait time is specified, sleep for that amount of time. */ 219 if (settings->wol_wait_time > 0) 220 guac_timestamp_msleep(settings->wol_wait_time * 1000); 221 } 222 223 /* Init SSH base libraries */ 224 if (guac_common_ssh_init(client)) { 225 guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, 226 "SSH library initialization failed"); 227 return NULL; 228 } 229 230 char ssh_ttymodes[GUAC_SSH_TTYMODES_SIZE(1)]; 231 232 /* Set up screen recording, if requested */ 233 if (settings->recording_path != NULL) { 234 ssh_client->recording = guac_recording_create(client, 235 settings->recording_path, 236 settings->recording_name, 237 settings->create_recording_path, 238 !settings->recording_exclude_output, 239 !settings->recording_exclude_mouse, 240 0, /* Touch events not supported */ 241 settings->recording_include_keys); 242 } 243 244 /* Create terminal options with required parameters */ 245 guac_terminal_options* options = guac_terminal_options_create( 246 settings->width, settings->height, settings->resolution); 247 248 /* Set optional parameters */ 249 options->disable_copy = settings->disable_copy; 250 options->max_scrollback = settings->max_scrollback; 251 options->font_name = settings->font_name; 252 options->font_size = settings->font_size; 253 options->color_scheme = settings->color_scheme; 254 options->backspace = settings->backspace; 255 256 /* Create terminal */ 257 ssh_client->term = guac_terminal_create(client, options); 258 259 /* Free options struct now that it's been used */ 260 guac_mem_free(options); 261 262 /* Fail if terminal init failed */ 263 if (ssh_client->term == NULL) { 264 guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, 265 "Terminal initialization failed"); 266 return NULL; 267 } 268 269 /* Send current values of exposed arguments to owner only */ 270 guac_client_for_owner(client, guac_ssh_send_current_argv, ssh_client); 271 272 /* Set up typescript, if requested */ 273 if (settings->typescript_path != NULL) { 274 guac_terminal_create_typescript(ssh_client->term, 275 settings->typescript_path, 276 settings->typescript_name, 277 settings->create_typescript_path); 278 } 279 280 /* Get user and credentials */ 281 ssh_client->user = guac_ssh_get_user(client); 282 if (ssh_client->user == NULL) { 283 /* Already aborted within guac_ssh_get_user() */ 284 return NULL; 285 } 286 287 /* Ensure connection is kept alive during lengthy connects */ 288 guac_socket_require_keep_alive(client->socket); 289 290 /* Open SSH session */ 291 ssh_client->session = guac_common_ssh_create_session(client, 292 settings->hostname, settings->port, ssh_client->user, settings->server_alive_interval, 293 settings->host_key, guac_ssh_get_credential); 294 if (ssh_client->session == NULL) { 295 /* Already aborted within guac_common_ssh_create_session() */ 296 return NULL; 297 } 298 299 pthread_mutex_init(&ssh_client->term_channel_lock, NULL); 300 301 /* Open channel for terminal */ 302 ssh_client->term_channel = 303 libssh2_channel_open_session(ssh_client->session->session); 304 if (ssh_client->term_channel == NULL) { 305 guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR, 306 "Unable to open terminal channel."); 307 return NULL; 308 } 309 310 /* Set the client timezone */ 311 if (settings->timezone != NULL) { 312 if (libssh2_channel_setenv(ssh_client->term_channel, "TZ", 313 settings->timezone)) { 314 guac_client_log(client, GUAC_LOG_WARNING, 315 "Unable to set the timezone: SSH server " 316 "refused to set \"TZ\" variable."); 317 } 318 } 319 320 321#ifdef ENABLE_SSH_AGENT 322 /* Start SSH agent forwarding, if enabled */ 323 if (ssh_client->enable_agent) { 324 libssh2_session_callback_set(ssh_client->session, 325 LIBSSH2_CALLBACK_AUTH_AGENT, (void*) ssh_auth_agent_callback); 326 327 /* Request agent forwarding */ 328 if (libssh2_channel_request_auth_agent(ssh_client->term_channel)) 329 guac_client_log(client, GUAC_LOG_ERROR, "Agent forwarding request failed"); 330 else 331 guac_client_log(client, GUAC_LOG_INFO, "Agent forwarding enabled."); 332 } 333 334 ssh_client->auth_agent = NULL; 335#endif 336 337 /* Start SFTP session as well, if enabled */ 338 if (settings->enable_sftp) { 339 340 /* Create SSH session specific for SFTP */ 341 guac_client_log(client, GUAC_LOG_DEBUG, "Reconnecting for SFTP..."); 342 ssh_client->sftp_session = 343 guac_common_ssh_create_session(client, settings->hostname, 344 settings->port, ssh_client->user, settings->server_alive_interval, 345 settings->host_key, NULL); 346 if (ssh_client->sftp_session == NULL) { 347 /* Already aborted within guac_common_ssh_create_session() */ 348 return NULL; 349 } 350 351 /* Request SFTP */ 352 ssh_client->sftp_filesystem = guac_common_ssh_create_sftp_filesystem( 353 ssh_client->sftp_session, settings->sftp_root_directory, 354 NULL, settings->sftp_disable_download, 355 settings->sftp_disable_upload); 356 357 /* Expose filesystem to connection owner */ 358 guac_client_for_owner(client, 359 guac_common_ssh_expose_sftp_filesystem, 360 ssh_client->sftp_filesystem); 361 362 /* Init handlers for Guacamole-specific console codes */ 363 if (!settings->sftp_disable_upload) 364 guac_terminal_set_upload_path_handler(ssh_client->term, 365 guac_sftp_set_upload_path); 366 367 if (!settings->sftp_disable_download) 368 guac_terminal_set_file_download_handler(ssh_client->term, 369 guac_sftp_download_file); 370 371 guac_client_log(client, GUAC_LOG_DEBUG, "SFTP session initialized"); 372 373 } 374 375 /* Set up the ttymode array prior to requesting the PTY */ 376 int ttymodeBytes = guac_ssh_ttymodes_init(ssh_ttymodes, 377 GUAC_SSH_TTY_OP_VERASE, settings->backspace, GUAC_SSH_TTY_OP_END); 378 if (ttymodeBytes < 1) 379 guac_client_log(client, GUAC_LOG_WARNING, "Unable to set TTY modes." 380 " Backspace may not work as expected."); 381 382 /* Request PTY */ 383 int term_height = guac_terminal_get_rows(ssh_client->term); 384 int term_width = guac_terminal_get_columns(ssh_client->term); 385 if (libssh2_channel_request_pty_ex(ssh_client->term_channel, 386 settings->terminal_type, strlen(settings->terminal_type), 387 ssh_ttymodes, ttymodeBytes, term_width, term_height, 0, 0)) { 388 guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR, "Unable to allocate PTY."); 389 return NULL; 390 } 391 392 /* Forward specified locale */ 393 if (settings->locale != NULL) { 394 if (libssh2_channel_setenv(ssh_client->term_channel, "LANG", 395 settings->locale)) { 396 guac_client_log(client, GUAC_LOG_WARNING, 397 "Unable to forward locale: SSH server refused to set " 398 "\"LANG\" environment variable."); 399 } 400 } 401 402 /* If a command is specified, run that instead of a shell */ 403 if (settings->command != NULL) { 404 if (libssh2_channel_exec(ssh_client->term_channel, settings->command)) { 405 guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR, 406 "Unable to execute command."); 407 return NULL; 408 } 409 } 410 411 /* Otherwise, request a shell */ 412 else if (libssh2_channel_shell(ssh_client->term_channel)) { 413 guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR, 414 "Unable to associate shell with PTY."); 415 return NULL; 416 } 417 418 /* Logged in */ 419 guac_client_log(client, GUAC_LOG_INFO, "SSH connection successful."); 420 guac_terminal_start(ssh_client->term); 421 422 /* Start input thread */ 423 if (pthread_create(&(input_thread), NULL, ssh_input_thread, (void*) client)) { 424 guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, "Unable to start input thread"); 425 return NULL; 426 } 427 428 /* Set non-blocking */ 429 libssh2_session_set_blocking(ssh_client->session->session, 0); 430 431 /* While data available, write to terminal */ 432 int bytes_read = 0; 433 for (;;) { 434 435 /* Track total amount of data read */ 436 int total_read = 0; 437 438 /* Timeout for polling socket activity */ 439 int timeout; 440 441 pthread_mutex_lock(&(ssh_client->term_channel_lock)); 442 443 /* Stop reading at EOF */ 444 if (libssh2_channel_eof(ssh_client->term_channel)) { 445 pthread_mutex_unlock(&(ssh_client->term_channel_lock)); 446 break; 447 } 448 449 /* Client is stopping, break the loop */ 450 if (client->state == GUAC_CLIENT_STOPPING) { 451 pthread_mutex_unlock(&(ssh_client->term_channel_lock)); 452 break; 453 } 454 455 /* Send keepalive at configured interval */ 456 if (settings->server_alive_interval > 0) { 457 timeout = 0; 458 if (libssh2_keepalive_send(ssh_client->session->session, &timeout) > 0) { 459 pthread_mutex_unlock(&(ssh_client->term_channel_lock)); 460 break; 461 } 462 timeout *= 1000; 463 } 464 /* If keepalive is not configured, sleep for the default of 1 second */ 465 else 466 timeout = GUAC_SSH_DEFAULT_POLL_TIMEOUT; 467 468 /* Read terminal data */ 469 bytes_read = libssh2_channel_read(ssh_client->term_channel, 470 buffer, sizeof(buffer)); 471 472 pthread_mutex_unlock(&(ssh_client->term_channel_lock)); 473 474 /* Attempt to write data received. Exit on failure. */ 475 if (bytes_read > 0) { 476 int written = guac_terminal_write(ssh_client->term, buffer, bytes_read); 477 if (written < 0) 478 break; 479 480 total_read += bytes_read; 481 } 482 483 else if (bytes_read < 0 && bytes_read != LIBSSH2_ERROR_EAGAIN) 484 break; 485 486#ifdef ENABLE_SSH_AGENT 487 /* If agent open, handle any agent packets */ 488 if (ssh_client->auth_agent != NULL) { 489 bytes_read = ssh_auth_agent_read(ssh_client->auth_agent); 490 if (bytes_read > 0) 491 total_read += bytes_read; 492 else if (bytes_read < 0 && bytes_read != LIBSSH2_ERROR_EAGAIN) 493 ssh_client->auth_agent = NULL; 494 } 495#endif 496 497 /* Wait for more data if reads turn up empty */ 498 if (total_read == 0) { 499 500 /* Wait on the SSH session file descriptor only */ 501 struct pollfd fds[] = {{ 502 .fd = ssh_client->session->fd, 503 .events = POLLIN, 504 .revents = 0, 505 }}; 506 507 /* Wait up to computed timeout */ 508 if (poll(fds, 1, timeout) < 0) 509 break; 510 511 } 512 513 } 514 515 /* Kill client and Wait for input thread to die */ 516 guac_client_stop(client); 517 pthread_join(input_thread, NULL); 518 519 pthread_mutex_destroy(&ssh_client->term_channel_lock); 520 521 guac_client_log(client, GUAC_LOG_INFO, "SSH connection ended."); 522 return NULL; 523 524} 525