telnet.c (21948B)
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 "telnet.h" 24#include "terminal/terminal.h" 25 26#include <guacamole/client.h> 27#include <guacamole/mem.h> 28#include <guacamole/protocol.h> 29#include <guacamole/recording.h> 30#include <guacamole/timestamp.h> 31#include <guacamole/wol.h> 32#include <libtelnet.h> 33 34#include <errno.h> 35#include <netdb.h> 36#include <netinet/in.h> 37#include <poll.h> 38#include <pthread.h> 39#include <stdbool.h> 40#include <stdlib.h> 41#include <string.h> 42#include <sys/socket.h> 43#include <sys/time.h> 44#include <unistd.h> 45 46/** 47 * Support levels for various telnet options, required for connection 48 * negotiation by telnet_init(), part of libtelnet. 49 */ 50static const telnet_telopt_t __telnet_options[] = { 51 { TELNET_TELOPT_ECHO, TELNET_WONT, TELNET_DO }, 52 { TELNET_TELOPT_TTYPE, TELNET_WILL, TELNET_DONT }, 53 { TELNET_TELOPT_COMPRESS2, TELNET_WONT, TELNET_DO }, 54 { TELNET_TELOPT_MSSP, TELNET_WONT, TELNET_DO }, 55 { TELNET_TELOPT_NAWS, TELNET_WILL, TELNET_DONT }, 56 { TELNET_TELOPT_NEW_ENVIRON, TELNET_WILL, TELNET_DONT }, 57 { -1, 0, 0 } 58}; 59 60/** 61 * Write the entire buffer given to the specified file descriptor, retrying 62 * the write automatically if necessary. This function will return a value 63 * not equal to the buffer's size iff an error occurs which prevents all 64 * future writes. 65 * 66 * @param fd The file descriptor to write to. 67 * @param buffer The buffer to write. 68 * @param size The number of bytes from the buffer to write. 69 */ 70static int __guac_telnet_write_all(int fd, const char* buffer, int size) { 71 72 int remaining = size; 73 while (remaining > 0) { 74 75 /* Attempt to write data */ 76 int ret_val = write(fd, buffer, remaining); 77 if (ret_val <= 0) 78 return -1; 79 80 /* If successful, contine with what data remains (if any) */ 81 remaining -= ret_val; 82 buffer += ret_val; 83 84 } 85 86 return size; 87 88} 89 90/** 91 * Matches the given line against the given regex, returning true and sending 92 * the given value if a match is found. An enter keypress is automatically 93 * sent after the value is sent. 94 * 95 * @param client 96 * The guac_client associated with the telnet session. 97 * 98 * @param regex 99 * The regex to search for within the given line buffer. 100 * 101 * @param value 102 * The string value to send through STDIN of the telnet session if a 103 * match is found, or NULL if no value should be sent. 104 * 105 * @param line_buffer 106 * The line of character data to test. 107 * 108 * @return 109 * true if a match is found, false otherwise. 110 */ 111static bool guac_telnet_regex_exec(guac_client* client, regex_t* regex, 112 const char* value, const char* line_buffer) { 113 114 guac_telnet_client* telnet_client = (guac_telnet_client*) client->data; 115 116 /* Send value upon match */ 117 if (regexec(regex, line_buffer, 0, NULL, 0) == 0) { 118 119 /* Send value */ 120 if (value != NULL) { 121 guac_terminal_send_string(telnet_client->term, value); 122 guac_terminal_send_string(telnet_client->term, "\x0D"); 123 } 124 125 /* Stop searching for prompt */ 126 return true; 127 128 } 129 130 return false; 131 132} 133 134/** 135 * Matches the given line against the various stored regexes, automatically 136 * sending the configured username, password, or reporting login 137 * success/failure depending on context. If no search is in progress, either 138 * because no regexes have been defined or because all applicable searches have 139 * completed, this function has no effect. 140 * 141 * @param client 142 * The guac_client associated with the telnet session. 143 * 144 * @param line_buffer 145 * The line of character data to test. 146 */ 147static void guac_telnet_search_line(guac_client* client, const char* line_buffer) { 148 149 guac_telnet_client* telnet_client = (guac_telnet_client*) client->data; 150 guac_telnet_settings* settings = telnet_client->settings; 151 152 /* Continue search for username prompt */ 153 if (settings->username_regex != NULL) { 154 if (guac_telnet_regex_exec(client, settings->username_regex, 155 settings->username, line_buffer)) { 156 guac_client_log(client, GUAC_LOG_DEBUG, "Username sent"); 157 guac_telnet_regex_free(&settings->username_regex); 158 } 159 } 160 161 /* Continue search for password prompt */ 162 if (settings->password_regex != NULL) { 163 if (guac_telnet_regex_exec(client, settings->password_regex, 164 settings->password, line_buffer)) { 165 166 guac_client_log(client, GUAC_LOG_DEBUG, "Password sent"); 167 168 /* Do not continue searching for username/password once password is sent */ 169 guac_telnet_regex_free(&settings->username_regex); 170 guac_telnet_regex_free(&settings->password_regex); 171 172 } 173 } 174 175 /* Continue search for login success */ 176 if (settings->login_success_regex != NULL) { 177 if (guac_telnet_regex_exec(client, settings->login_success_regex, 178 NULL, line_buffer)) { 179 180 /* Allow terminal to render now that login has been deemed successful */ 181 guac_client_log(client, GUAC_LOG_DEBUG, "Login successful"); 182 guac_terminal_start(telnet_client->term); 183 184 /* Stop all searches */ 185 guac_telnet_regex_free(&settings->username_regex); 186 guac_telnet_regex_free(&settings->password_regex); 187 guac_telnet_regex_free(&settings->login_success_regex); 188 guac_telnet_regex_free(&settings->login_failure_regex); 189 190 } 191 } 192 193 /* Continue search for login failure */ 194 if (settings->login_failure_regex != NULL) { 195 if (guac_telnet_regex_exec(client, settings->login_failure_regex, 196 NULL, line_buffer)) { 197 198 /* Advise that login has failed and connection should be closed */ 199 guac_client_abort(client, 200 GUAC_PROTOCOL_STATUS_CLIENT_UNAUTHORIZED, 201 "Login failed"); 202 203 /* Stop all searches */ 204 guac_telnet_regex_free(&settings->username_regex); 205 guac_telnet_regex_free(&settings->password_regex); 206 guac_telnet_regex_free(&settings->login_success_regex); 207 guac_telnet_regex_free(&settings->login_failure_regex); 208 209 } 210 } 211 212} 213 214/** 215 * Searches for a line matching the various stored regexes, automatically 216 * sending the configured username, password, or reporting login 217 * success/failure depending on context. If no search is in progress, either 218 * because no regexes have been defined or because all applicable searches 219 * have completed, this function has no effect. 220 * 221 * @param client 222 * The guac_client associated with the telnet session. 223 * 224 * @param buffer 225 * The buffer of received data to search through. 226 * 227 * @param size 228 * The size of the given buffer, in bytes. 229 */ 230static void guac_telnet_search(guac_client* client, const char* buffer, int size) { 231 232 static char line_buffer[1024] = {0}; 233 static int length = 0; 234 235 /* Append all characters in buffer to current line */ 236 const char* current = buffer; 237 for (int i = 0; i < size; i++) { 238 239 char c = *(current++); 240 241 /* Attempt pattern match and clear buffer upon reading newline */ 242 if (c == '\n') { 243 if (length > 0) { 244 line_buffer[length] = '\0'; 245 guac_telnet_search_line(client, line_buffer); 246 length = 0; 247 } 248 } 249 250 /* Append all non-newline characters to line buffer as long as space 251 * remains */ 252 else if (length < sizeof(line_buffer) - 1) 253 line_buffer[length++] = c; 254 255 } 256 257 /* Attempt pattern match if an unfinished line remains (may be a prompt) */ 258 if (length > 0) { 259 line_buffer[length] = '\0'; 260 guac_telnet_search_line(client, line_buffer); 261 } 262 263} 264 265/** 266 * Event handler, as defined by libtelnet. This function is passed to 267 * telnet_init() and will be called for every event fired by libtelnet, 268 * including feature enable/disable and receipt/transmission of data. 269 */ 270static void __guac_telnet_event_handler(telnet_t* telnet, telnet_event_t* event, void* data) { 271 272 guac_client* client = (guac_client*) data; 273 guac_telnet_client* telnet_client = (guac_telnet_client*) client->data; 274 guac_telnet_settings* settings = telnet_client->settings; 275 276 switch (event->type) { 277 278 /* Terminal output received */ 279 case TELNET_EV_DATA: 280 guac_terminal_write(telnet_client->term, event->data.buffer, event->data.size); 281 guac_telnet_search(client, event->data.buffer, event->data.size); 282 break; 283 284 /* Data destined for remote end */ 285 case TELNET_EV_SEND: 286 if (__guac_telnet_write_all(telnet_client->socket_fd, event->data.buffer, event->data.size) 287 != event->data.size) 288 guac_client_stop(client); 289 break; 290 291 /* Remote feature enabled */ 292 case TELNET_EV_WILL: 293 if (event->neg.telopt == TELNET_TELOPT_ECHO) 294 telnet_client->echo_enabled = 0; /* Disable local echo, as remote will echo */ 295 break; 296 297 /* Remote feature disabled */ 298 case TELNET_EV_WONT: 299 if (event->neg.telopt == TELNET_TELOPT_ECHO) 300 telnet_client->echo_enabled = 1; /* Enable local echo, as remote won't echo */ 301 break; 302 303 /* Local feature enable */ 304 case TELNET_EV_DO: 305 if (event->neg.telopt == TELNET_TELOPT_NAWS) { 306 telnet_client->naws_enabled = 1; 307 guac_telnet_send_naws(telnet, 308 guac_terminal_get_columns(telnet_client->term), 309 guac_terminal_get_rows(telnet_client->term)); 310 } 311 break; 312 313 /* Terminal type request */ 314 case TELNET_EV_TTYPE: 315 if (event->ttype.cmd == TELNET_TTYPE_SEND) 316 telnet_ttype_is(telnet_client->telnet, settings->terminal_type); 317 break; 318 319 /* Environment request */ 320 case TELNET_EV_ENVIRON: 321 322 /* Only send USER if entire environment was requested */ 323 if (event->environ.size == 0) 324 guac_telnet_send_user(telnet, settings->username); 325 326 break; 327 328 /* Connection warnings */ 329 case TELNET_EV_WARNING: 330 guac_client_log(client, GUAC_LOG_WARNING, "%s", event->error.msg); 331 break; 332 333 /* Connection errors */ 334 case TELNET_EV_ERROR: 335 guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR, 336 "Telnet connection closing with error: %s", event->error.msg); 337 break; 338 339 /* Ignore other events */ 340 default: 341 break; 342 343 } 344 345} 346 347/** 348 * Input thread, started by the main telnet client thread. This thread 349 * continuously reads from the terminal's STDIN and transfers all read 350 * data to the telnet connection. 351 * 352 * @param data The current guac_client instance. 353 * @return Always NULL. 354 */ 355static void* __guac_telnet_input_thread(void* data) { 356 357 guac_client* client = (guac_client*) data; 358 guac_telnet_client* telnet_client = (guac_telnet_client*) client->data; 359 360 char buffer[8192]; 361 int bytes_read; 362 363 /* Write all data read */ 364 while ((bytes_read = guac_terminal_read_stdin(telnet_client->term, buffer, sizeof(buffer))) > 0) { 365 telnet_send(telnet_client->telnet, buffer, bytes_read); 366 if (telnet_client->echo_enabled) 367 guac_terminal_write(telnet_client->term, buffer, bytes_read); 368 } 369 370 return NULL; 371 372} 373 374/** 375 * Connects to the telnet server specified within the data associated 376 * with the given guac_client, which will have been populated by 377 * guac_client_init. 378 * 379 * @return The connected telnet instance, if successful, or NULL if the 380 * connection fails for any reason. 381 */ 382static telnet_t* __guac_telnet_create_session(guac_client* client) { 383 384 int retval; 385 386 int fd; 387 struct addrinfo* addresses; 388 struct addrinfo* current_address; 389 390 char connected_address[1024]; 391 char connected_port[64]; 392 393 guac_telnet_client* telnet_client = (guac_telnet_client*) client->data; 394 guac_telnet_settings* settings = telnet_client->settings; 395 396 struct addrinfo hints = { 397 .ai_family = AF_UNSPEC, 398 .ai_socktype = SOCK_STREAM, 399 .ai_protocol = IPPROTO_TCP 400 }; 401 402 /* Get socket */ 403 fd = socket(AF_INET, SOCK_STREAM, 0); 404 405 /* Get addresses connection */ 406 if ((retval = getaddrinfo(settings->hostname, settings->port, 407 &hints, &addresses))) { 408 guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, "Error parsing given address or port: %s", 409 gai_strerror(retval)); 410 return NULL; 411 412 } 413 414 /* Attempt connection to each address until success */ 415 current_address = addresses; 416 while (current_address != NULL) { 417 418 int retval; 419 420 /* Resolve hostname */ 421 if ((retval = getnameinfo(current_address->ai_addr, 422 current_address->ai_addrlen, 423 connected_address, sizeof(connected_address), 424 connected_port, sizeof(connected_port), 425 NI_NUMERICHOST | NI_NUMERICSERV))) 426 guac_client_log(client, GUAC_LOG_DEBUG, "Unable to resolve host: %s", gai_strerror(retval)); 427 428 /* Connect */ 429 if (connect(fd, current_address->ai_addr, 430 current_address->ai_addrlen) == 0) { 431 432 guac_client_log(client, GUAC_LOG_DEBUG, "Successfully connected to " 433 "host %s, port %s", connected_address, connected_port); 434 435 /* Done if successful connect */ 436 break; 437 438 } 439 440 /* Otherwise log information regarding bind failure */ 441 else 442 guac_client_log(client, GUAC_LOG_DEBUG, "Unable to connect to " 443 "host %s, port %s: %s", 444 connected_address, connected_port, strerror(errno)); 445 446 current_address = current_address->ai_next; 447 448 } 449 450 /* If unable to connect to anything, fail */ 451 if (current_address == NULL) { 452 guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_NOT_FOUND, 453 "Unable to connect to any addresses."); 454 return NULL; 455 } 456 457 /* Free addrinfo */ 458 freeaddrinfo(addresses); 459 460 /* Open telnet session */ 461 telnet_t* telnet = telnet_init(__telnet_options, __guac_telnet_event_handler, 0, client); 462 if (telnet == NULL) { 463 guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, "Telnet client allocation failed."); 464 return NULL; 465 } 466 467 /* Save file descriptor */ 468 telnet_client->socket_fd = fd; 469 470 return telnet; 471 472} 473 474/** 475 * Sends a 16-bit value over the given telnet connection with the byte order 476 * required by the telnet protocol. 477 * 478 * @param telnet The telnet connection to use. 479 * @param value The value to send. 480 */ 481static void __guac_telnet_send_uint16(telnet_t* telnet, uint16_t value) { 482 483 unsigned char buffer[2]; 484 buffer[0] = (value >> 8) & 0xFF; 485 buffer[1] = value & 0xFF; 486 487 telnet_send(telnet, (char*) buffer, 2); 488 489} 490 491/** 492 * Sends an 8-bit value over the given telnet connection. 493 * 494 * @param telnet The telnet connection to use. 495 * @param value The value to send. 496 */ 497static void __guac_telnet_send_uint8(telnet_t* telnet, uint8_t value) { 498 telnet_send(telnet, (char*) (&value), 1); 499} 500 501void guac_telnet_send_naws(telnet_t* telnet, uint16_t width, uint16_t height) { 502 telnet_begin_sb(telnet, TELNET_TELOPT_NAWS); 503 __guac_telnet_send_uint16(telnet, width); 504 __guac_telnet_send_uint16(telnet, height); 505 telnet_finish_sb(telnet); 506} 507 508void guac_telnet_send_user(telnet_t* telnet, const char* username) { 509 510 /* IAC SB NEW-ENVIRON IS */ 511 telnet_begin_sb(telnet, TELNET_TELOPT_NEW_ENVIRON); 512 __guac_telnet_send_uint8(telnet, TELNET_ENVIRON_IS); 513 514 /* Only send username if defined */ 515 if (username != NULL) { 516 517 /* VAR "USER" */ 518 __guac_telnet_send_uint8(telnet, TELNET_ENVIRON_VAR); 519 telnet_send(telnet, "USER", 4); 520 521 /* VALUE username */ 522 __guac_telnet_send_uint8(telnet, TELNET_ENVIRON_VALUE); 523 telnet_send(telnet, username, strlen(username)); 524 525 } 526 527 /* IAC SE */ 528 telnet_finish_sb(telnet); 529 530} 531 532/** 533 * Waits for data on the given file descriptor for up to one second. The 534 * return value is identical to that of select(): 0 on timeout, < 0 on 535 * error, and > 0 on success. 536 * 537 * @param socket_fd The file descriptor to wait for. 538 * @return A value greater than zero on success, zero on timeout, and 539 * less than zero on error. 540 */ 541static int __guac_telnet_wait(int socket_fd) { 542 543 /* Build array of file descriptors */ 544 struct pollfd fds[] = {{ 545 .fd = socket_fd, 546 .events = POLLIN, 547 .revents = 0, 548 }}; 549 550 /* Wait for one second */ 551 return poll(fds, 1, 1000); 552 553} 554 555void* guac_telnet_client_thread(void* data) { 556 557 guac_client* client = (guac_client*) data; 558 guac_telnet_client* telnet_client = (guac_telnet_client*) client->data; 559 guac_telnet_settings* settings = telnet_client->settings; 560 561 pthread_t input_thread; 562 char buffer[8192]; 563 int wait_result; 564 565 /* If Wake-on-LAN is enabled, attempt to wake. */ 566 if (settings->wol_send_packet) { 567 guac_client_log(client, GUAC_LOG_DEBUG, "Sending Wake-on-LAN packet, " 568 "and pausing for %d seconds.", settings->wol_wait_time); 569 570 /* Send the Wake-on-LAN request. */ 571 if (guac_wol_wake(settings->wol_mac_addr, settings->wol_broadcast_addr, 572 settings->wol_udp_port)) 573 return NULL; 574 575 /* If wait time is specified, sleep for that amount of time. */ 576 if (settings->wol_wait_time > 0) 577 guac_timestamp_msleep(settings->wol_wait_time * 1000); 578 } 579 580 /* Set up screen recording, if requested */ 581 if (settings->recording_path != NULL) { 582 telnet_client->recording = guac_recording_create(client, 583 settings->recording_path, 584 settings->recording_name, 585 settings->create_recording_path, 586 !settings->recording_exclude_output, 587 !settings->recording_exclude_mouse, 588 0, /* Touch events not supported */ 589 settings->recording_include_keys); 590 } 591 592 /* Create terminal options with required parameters */ 593 guac_terminal_options* options = guac_terminal_options_create( 594 settings->width, settings->height, settings->resolution); 595 596 /* Set optional parameters */ 597 options->disable_copy = settings->disable_copy; 598 options->max_scrollback = settings->max_scrollback; 599 options->font_name = settings->font_name; 600 options->font_size = settings->font_size; 601 options->color_scheme = settings->color_scheme; 602 options->backspace = settings->backspace; 603 604 /* Create terminal */ 605 telnet_client->term = guac_terminal_create(client, options); 606 607 /* Free options struct now that it's been used */ 608 guac_mem_free(options); 609 610 /* Fail if terminal init failed */ 611 if (telnet_client->term == NULL) { 612 guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, 613 "Terminal initialization failed"); 614 return NULL; 615 } 616 617 /* Send current values of exposed arguments to owner only */ 618 guac_client_for_owner(client, guac_telnet_send_current_argv, 619 telnet_client); 620 621 /* Set up typescript, if requested */ 622 if (settings->typescript_path != NULL) { 623 guac_terminal_create_typescript(telnet_client->term, 624 settings->typescript_path, 625 settings->typescript_name, 626 settings->create_typescript_path); 627 } 628 629 /* Open telnet session */ 630 telnet_client->telnet = __guac_telnet_create_session(client); 631 if (telnet_client->telnet == NULL) { 632 /* Already aborted within __guac_telnet_create_session() */ 633 return NULL; 634 } 635 636 /* Logged in */ 637 guac_client_log(client, GUAC_LOG_INFO, "Telnet connection successful."); 638 639 /* Allow terminal to render if login success/failure detection is not 640 * enabled */ 641 if (settings->login_success_regex == NULL 642 && settings->login_failure_regex == NULL) 643 guac_terminal_start(telnet_client->term); 644 645 /* Start input thread */ 646 if (pthread_create(&(input_thread), NULL, __guac_telnet_input_thread, (void*) client)) { 647 guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, "Unable to start input thread"); 648 return NULL; 649 } 650 651 /* While data available, write to terminal */ 652 while ((wait_result = __guac_telnet_wait(telnet_client->socket_fd)) >= 0) { 653 654 /* Resume waiting of no data available */ 655 if (wait_result == 0) 656 continue; 657 658 int bytes_read = read(telnet_client->socket_fd, buffer, sizeof(buffer)); 659 if (bytes_read <= 0) 660 break; 661 662 telnet_recv(telnet_client->telnet, buffer, bytes_read); 663 664 } 665 666 /* Kill client and Wait for input thread to die */ 667 guac_client_stop(client); 668 pthread_join(input_thread, NULL); 669 670 guac_client_log(client, GUAC_LOG_INFO, "Telnet connection ended."); 671 return NULL; 672 673} 674