/* * ssh.c - Simple example of SSH X11 client using libssh * * Copyright (C) 2022 Marco Fortina * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. * * In addition, as a special exception, the copyright holders give * permission to link the code of portions of this program with the * OpenSSL library under certain conditions as described in each * individual source file, and distribute linked combinations * including the two. * You must obey the GNU General Public License in all respects * for all of the code used other than OpenSSL. * If you modify * file(s) with this exception, you may extend this exception to your * version of the file(s), but you are not obligated to do so. * If you * do not wish to do so, delete this exception statement from your * version. * If you delete this exception statement from all source * files in the program, then also delete it here. * * * * ssh_X11_client * ============== * * AUTHOR URL * https://gitlab.com/marco.fortina/libssh-x11-client/ * * This is a simple example of SSH X11 client using libssh. * * Features: * * - support local display (e.g. :0) * - support remote display (e.g. localhost:10.0) * - using callbacks and event polling to significantly reduce CPU utilization * - use X11 forwarding with authentication spoofing (like openssh) * * Note: * * - part of this code was inspired by openssh's one. * * Dependencies: * * - gcc >= 7.5.0 * - libssh >= 0.8.0 * - libssh-dev >= 0.8.0 * * To Build: * gcc -o ssh_X11_client ssh_X11_client.c -lssh -g * * Donations: * * If you liked this work and wish to support the developer please donate to: * Bitcoin: 1N2rQimKbeUQA8N2LU5vGopYQJmZsBM2d6 * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* * Data Structures and Macros */ #define _PATH_UNIX_X "/tmp/.X11-unix/X%d" #define _XAUTH_CMD "/usr/bin/xauth list %s 2>/dev/null" typedef struct item { ssh_channel channel; int fd_in; int fd_out; int protected; struct item *next; } node_t; node_t *node = NULL; /* * Mutex */ pthread_mutex_t mutex; /* * Function declarations */ /* Linked nodes to manage channel/fd tuples */ static void insert_item(ssh_channel channel, int fd_in, int fd_out, int protected); static void delete_item(ssh_channel channel); static node_t * search_item(ssh_channel channel); /* X11 Display */ const char * ssh_gai_strerror(int gaierr); static int x11_get_proto(const char *display, char **_proto, char **_data); static void set_nodelay(int fd); static int connect_local_xsocket_path(const char *pathname); static int connect_local_xsocket(int display_number); static int x11_connect_display(void); /* Send data to channel */ static int copy_fd_to_channel_callback(int fd, int revents, void *userdata); /* Read data from channel */ static int copy_channel_to_fd_callback(ssh_session session, ssh_channel channel, void *data, uint32_t len, int is_stderr, void *userdata); /* EOF&Close channel */ static void channel_close_callback(ssh_session session, ssh_channel channel, void *userdata); /* X11 Request */ static ssh_channel x11_open_request_callback(ssh_session session, const char *shost, int sport, void *userdata); /* Main loop */ static int main_loop(ssh_channel channel); /* Internals */ int64_t _current_timestamp(void); /* Global variables */ const char *hostname = NULL; int enableX11 = 1; /* * Callbacks Data Structures */ /* SSH Channel Callbacks */ struct ssh_channel_callbacks_struct channel_cb = { .channel_data_function = copy_channel_to_fd_callback, .channel_eof_function = channel_close_callback, .channel_close_function = channel_close_callback, .userdata = NULL }; /* SSH Callbacks */ struct ssh_callbacks_struct cb = { .channel_open_request_x11_function = x11_open_request_callback, .userdata = NULL }; /* * SSH Event Context */ short events = POLLIN | POLLPRI | POLLERR | POLLHUP | POLLNVAL; ssh_event event; /* * Internal data structures */ struct termios _saved_tio; /* * Internal functions */ int64_t _current_timestamp(void) { struct timeval tv; int64_t milliseconds; gettimeofday(&tv, NULL); milliseconds = (int64_t)(tv.tv_sec) * 1000 + (tv.tv_usec / 1000); return milliseconds; } static void _logging_callback(int priority, const char *function, const char *buffer, void *userdata) { FILE *fp = NULL; char buf[100]; int64_t milliseconds; time_t now = time (0); (void)userdata; strftime(buf, 100, "%Y-%m-%d %H:%M:%S", localtime (&now)); fp = fopen("debug.log","a"); if(fp == NULL) { printf("Error!"); exit(-11); } milliseconds = _current_timestamp(); fprintf(fp, "[%s.%jd, %d] %s: %s\n", buf, milliseconds, priority, function, buffer); fclose(fp); } static int _enter_term_raw_mode(void) { struct termios tio; int ret = tcgetattr(fileno(stdin), &tio); if (ret != -1) { _saved_tio = tio; tio.c_iflag |= IGNPAR; tio.c_iflag &= ~(ISTRIP | INLCR | IGNCR | ICRNL | IXON | IXANY | IXOFF); #ifdef IUCLC tio.c_iflag &= ~IUCLC; #endif tio.c_lflag &= ~(ISIG | ICANON | ECHO | ECHOE | ECHOK | ECHONL); #ifdef IEXTEN tio.c_lflag &= ~IEXTEN; #endif tio.c_oflag &= ~OPOST; tio.c_cc[VMIN] = 1; tio.c_cc[VTIME] = 0; ret = tcsetattr(fileno(stdin), TCSADRAIN, &tio); } return ret; } static int _leave_term_raw_mode(void) { int ret = tcsetattr(fileno(stdin), TCSADRAIN, &_saved_tio); return ret; } /* * Functions */ static void insert_item(ssh_channel channel, int fd_in, int fd_out, int protected) { node_t *node_iterator = NULL, *new = NULL; pthread_mutex_lock(&mutex); if (node == NULL) { /* Calloc ensure that node is full of 0 */ node = (node_t *) calloc(1, sizeof(node_t)); node->channel = channel; node->fd_in = fd_in; node->fd_out = fd_out; node->protected = protected; node->next = NULL; } else { node_iterator = node; while (node_iterator->next != NULL) node_iterator = node_iterator->next; /* Create the new node */ new = (node_t *) malloc(sizeof(node_t)); new->channel = channel; new->fd_in = fd_in; new->fd_out = fd_out; new->protected = protected; new->next = NULL; node_iterator->next = new; } pthread_mutex_unlock(&mutex); } static void delete_item(ssh_channel channel) { node_t *current = NULL, *previous = NULL; pthread_mutex_lock(&mutex); for (current = node; current; previous = current, current = current->next) { if (current->channel != channel) continue; if (previous == NULL) node = current->next; else previous->next = current->next; free(current); pthread_mutex_unlock(&mutex); return; } pthread_mutex_unlock(&mutex); } static node_t * search_item(ssh_channel channel) { node_t *current = node; pthread_mutex_lock(&mutex); while (current != NULL) { if (current->channel == channel) { pthread_mutex_unlock(&mutex); return current; } else { current = current->next; } } pthread_mutex_unlock(&mutex); return NULL; } static void set_nodelay(int fd) { int opt; socklen_t optlen; optlen = sizeof(opt); if (getsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &opt, &optlen) == -1) { _ssh_log(SSH_LOG_FUNCTIONS, __func__, "getsockopt TCP_NODELAY: %.100s", strerror(errno)); return; } if (opt == 1) { _ssh_log(SSH_LOG_FUNCTIONS, __func__, "fd %d is TCP_NODELAY", fd); return; } opt = 1; _ssh_log(SSH_LOG_FUNCTIONS, __func__, "fd %d setting TCP_NODELAY", fd); if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt)) == -1) _ssh_log(SSH_LOG_FUNCTIONS, __func__, "setsockopt TCP_NODELAY: %.100s", strerror(errno)); } const char * ssh_gai_strerror(int gaierr) { if (gaierr == EAI_SYSTEM && errno != 0) return strerror(errno); return gai_strerror(gaierr); } static int x11_get_proto(const char *display, char **_proto, char **_cookie) { char cmd[1024], line[512], xdisplay[512]; static char proto[512], cookie[512]; FILE *f = NULL; int ret = 0; *_proto = proto; *_cookie = cookie; proto[0] = cookie[0] = '\0'; if (strncmp(display, "localhost:", 10) == 0) { if ((ret = snprintf(xdisplay, sizeof(xdisplay), "unix:%s", display + 10)) < 0 || (size_t)ret >= sizeof(xdisplay)) { _ssh_log(SSH_LOG_FUNCTIONS, __func__, "display name too long. display: %s", display); return -1; } display = xdisplay; } snprintf(cmd, sizeof(cmd), _XAUTH_CMD, display); _ssh_log(SSH_LOG_FUNCTIONS, __func__, "xauth cmd: %s", cmd); f = popen(cmd, "r"); if (f && fgets(line, sizeof(line), f) && sscanf(line, "%*s %511s %511s", proto, cookie) == 2) { ret = 0; } else { ret = 1; } if (f) pclose(f); _ssh_log(SSH_LOG_FUNCTIONS, __func__, "proto: %s - cookie: %s - ret: %d", proto, cookie, ret); return ret; } static int connect_local_xsocket_path(const char *pathname) { int sock; struct sockaddr_un addr; sock = socket(AF_UNIX, SOCK_STREAM, 0); if (sock == -1) { _ssh_log(SSH_LOG_FUNCTIONS, __func__, "socket: %.100s", strerror(errno)); return -1; } memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; addr.sun_path[0] = '\0'; /* pathname is guaranteed to be initialized and larger than addr.sun_path[108] */ memcpy(addr.sun_path + 1, pathname, sizeof(addr.sun_path) - 1); if (connect(sock, (struct sockaddr *)&addr, offsetof(struct sockaddr_un, sun_path) + 1 + strlen(pathname)) == 0) return sock; close(sock); _ssh_log(SSH_LOG_FUNCTIONS, __func__, "connect %.100s: %.100s", addr.sun_path, strerror(errno)); return -1; } static int connect_local_xsocket(int display_number) { char buf[1024] = {0}; snprintf(buf, sizeof(buf), _PATH_UNIX_X, display_number); return connect_local_xsocket_path(buf); } static int x11_connect_display() { int display_number; const char *display = NULL; char buf[1024], *cp = NULL; struct addrinfo hints, *ai = NULL, *aitop = NULL; char strport[NI_MAXSERV]; int gaierr = 0, sock = 0; /* Try to open a socket for the local X server. */ display = getenv("DISPLAY"); _ssh_log(SSH_LOG_FUNCTIONS, __func__, "display: %s", display); if (!display) { return -1; } /* Check if it is a unix domain socket. */ if (strncmp(display, "unix:", 5) == 0 || display[0] == ':') { /* Connect to the unix domain socket. */ if (sscanf(strrchr(display, ':') + 1, "%d", &display_number) != 1) { _ssh_log(SSH_LOG_FUNCTIONS, __func__, "Could not parse display number from DISPLAY: %.100s", display); return -1; } _ssh_log(SSH_LOG_FUNCTIONS, __func__, "display_number: %d", display_number); /* Create a socket. */ sock = connect_local_xsocket(display_number); _ssh_log(SSH_LOG_FUNCTIONS, __func__, "socket: %d", sock); if (sock < 0) return -1; /* OK, we now have a connection to the display. */ return sock; } /* Connect to an inet socket. */ strncpy(buf, display, sizeof(buf) - 1); cp = strchr(buf, ':'); if (!cp) { _ssh_log(SSH_LOG_FUNCTIONS, __func__, "Could not find ':' in DISPLAY: %.100s", display); return -1; } *cp = 0; if (sscanf(cp + 1, "%d", &display_number) != 1) { _ssh_log(SSH_LOG_FUNCTIONS, __func__, "Could not parse display number from DISPLAY: %.100s", display); return -1; } /* Look up the host address */ memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_INET; hints.ai_socktype = SOCK_STREAM; snprintf(strport, sizeof(strport), "%u", 6000 + display_number); if ((gaierr = getaddrinfo(buf, strport, &hints, &aitop)) != 0) { _ssh_log(SSH_LOG_FUNCTIONS, __func__, "%.100s: unknown host. (%s)", buf, ssh_gai_strerror(gaierr)); return -1; } for (ai = aitop; ai; ai = ai->ai_next) { /* Create a socket. */ sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); if (sock == -1) { _ssh_log(SSH_LOG_FUNCTIONS, __func__, "socket: %.100s", strerror(errno)); continue; } /* Connect it to the display. */ if (connect(sock, ai->ai_addr, ai->ai_addrlen) == -1) { _ssh_log(SSH_LOG_FUNCTIONS, __func__, "connect %.100s port %u: %.100s", buf, 6000 + display_number, strerror(errno)); close(sock); continue; } /* Success */ break; } freeaddrinfo(aitop); if (!ai) { _ssh_log(SSH_LOG_FUNCTIONS, __func__, "connect %.100s port %u: %.100s", buf, 6000 + display_number, strerror(errno)); return -1; } set_nodelay(sock); return sock; } static int copy_fd_to_channel_callback(int fd, int revents, void *userdata) { ssh_channel channel = (ssh_channel)userdata; char buf[2097152]; int sz = 0, ret = 0; node_t *temp_node = search_item(channel); _ssh_log(SSH_LOG_FUNCTIONS, __func__, "event: %d - fd: %d", revents, fd); if (!channel) { _ssh_log(SSH_LOG_FUNCTIONS, __func__, "channel does not exist."); if (temp_node->protected == 0) { close(fd); } return -1; } if (fcntl(fd, F_GETFD) == -1) { _ssh_log(SSH_LOG_FUNCTIONS, __func__, "fcntl error. fd: %d", fd); ssh_channel_close(channel); return -1; } if ((revents & POLLIN) || (revents & POLLPRI)) { sz = read(fd, buf, sizeof(buf)); _ssh_log(SSH_LOG_FUNCTIONS, __func__, "sz: %d", sz); if (sz > 0) { ret = ssh_channel_write(channel, buf, sz); _ssh_log(SSH_LOG_FUNCTIONS, __func__, "channel_write ret: %d", ret); } else if (sz < 0) { ssh_channel_close(channel); return -1; } else { /* sz = 0. Why the hell I'm here? */ _ssh_log(SSH_LOG_FUNCTIONS, __func__, "Why the hell am I here?: sz: %d", sz); if (temp_node->protected == 0) { close(fd); } return -1; } } if ((revents & POLLHUP) || (revents & POLLNVAL) || (revents & POLLERR)) { ssh_channel_close(channel); return -1; } return sz; } static int copy_channel_to_fd_callback(ssh_session session, ssh_channel channel, void *data, uint32_t len, int is_stderr, void *userdata) { node_t *temp_node = NULL; int fd, sz; (void)session; (void)is_stderr; (void)userdata; temp_node = search_item(channel); fd = temp_node->fd_out; _ssh_log(SSH_LOG_FUNCTIONS, __func__, "len: %d - fd: %d - is_stderr: %d", len, fd, is_stderr); sz = write(fd, data, len); return sz; } static void channel_close_callback(ssh_session session, ssh_channel channel, void *userdata) { node_t *temp_node = NULL; (void)session; (void)userdata; temp_node = search_item(channel); if (temp_node != NULL) { int fd = temp_node->fd_in; _ssh_log(SSH_LOG_FUNCTIONS, __func__, "fd: %d", fd); delete_item(channel); ssh_event_remove_fd(event, fd); if (temp_node->protected == 0) { close(fd); } } } static ssh_channel x11_open_request_callback(ssh_session session, const char *shost, int sport, void *userdata) { ssh_channel channel = NULL; int sock; (void)shost; (void)sport; (void)userdata; channel = ssh_channel_new(session); sock = x11_connect_display(); _ssh_log(SSH_LOG_FUNCTIONS, __func__, "sock: %d", sock); insert_item(channel, sock, sock, 0); ssh_event_add_fd(event, sock, events, copy_fd_to_channel_callback, channel); ssh_event_add_session(event, session); ssh_add_channel_callbacks(channel, &channel_cb); return channel; } /* * MAIN LOOP */ static int main_loop(ssh_channel channel) { ssh_session session = ssh_channel_get_session(channel); insert_item(channel, fileno(stdin), fileno(stdout), 1); ssh_callbacks_init(&channel_cb); ssh_set_channel_callbacks(channel, &channel_cb); event = ssh_event_new(); if (event == NULL) { printf("Couldn't get a event\n"); return -1; } if (ssh_event_add_fd(event, fileno(stdin), events, copy_fd_to_channel_callback, channel) != SSH_OK) { printf("Couldn't add an fd to the event\n"); return -1; } if(ssh_event_add_session(event, session) != SSH_OK) { printf("Couldn't add the session to the event\n"); return -1; } do { if (ssh_event_dopoll(event, 1000) == SSH_ERROR) { printf("Error : %s\n", ssh_get_error(session)); /* fall through */ } } while (!ssh_channel_is_closed(channel)); delete_item(channel); ssh_event_remove_fd(event, fileno(stdin)); ssh_event_remove_session(event, session); ssh_event_free(event); return 0; } /* * USAGE */ static void usage(void) { fprintf(stderr, "Usage : ssh-X11-client [options] [login@]hostname\n" "sample X11 client - libssh-%s\n" "Options :\n" " -l user : Specifies the user to log in as on the remote machine.\n" " -p port : Port to connect to on the remote host.\n" " -v : Verbose mode. Multiple -v options increase the verbosity. The maximum is 5.\n" " -C : Requests compression of all data.\n" " -x : Disables X11 forwarding.\n" "\n", ssh_version(0)); exit(0); } static int opts(int argc, char **argv) { int i; while ((i = getopt(argc,argv,"x")) != -1) { switch(i) { case 'x': enableX11 = 0; break; default: fprintf(stderr, "Unknown option %c\n", optopt); return -1; } } if (optind < argc) { hostname = argv[optind++]; } if (hostname == NULL) { return -1; } return 0; } /* * MAIN */ int main(int argc, char **argv) { char *password = NULL; ssh_session session = NULL; ssh_channel channel = NULL; int ret; const char *display = NULL; char *proto = NULL, *cookie = NULL; ssh_set_log_callback(_logging_callback); ret = ssh_init(); if (ret != SSH_OK) return ret; session = ssh_new(); if (session == NULL) exit(-1); if (ssh_options_getopt(session, &argc, argv) || opts(argc, argv)) { fprintf(stderr, "Error parsing command line: %s\n", ssh_get_error(session)); ssh_free(session); ssh_finalize(); usage(); } if (ssh_options_set(session, SSH_OPTIONS_HOST, hostname) < 0) { return -1; } ret = ssh_connect(session); if (ret != SSH_OK) { fprintf(stderr, "Connection failed : %s\n", ssh_get_error(session)); exit(-1); } password = getpass("Password: "); ret = ssh_userauth_password(session, NULL, password); if (ret != SSH_AUTH_SUCCESS) { fprintf(stderr, "Error authenticating with password: %s\n", ssh_get_error(session)); exit(-1); } channel = ssh_channel_new(session); if (channel == NULL) return SSH_ERROR; ret = ssh_channel_open_session(channel); if (ret != SSH_OK) return ret; ret = ssh_channel_request_pty(channel); if (ret != SSH_OK) return ret; ret = ssh_channel_change_pty_size(channel, 80, 24); if (ret != SSH_OK) return ret; if (enableX11 == 1) { display = getenv("DISPLAY"); _ssh_log(SSH_LOG_FUNCTIONS, __func__, "display: %s", display); if (display) { ssh_callbacks_init(&cb); ret = ssh_set_callbacks(session, &cb); if (ret != SSH_OK) return ret; if (x11_get_proto(display, &proto, &cookie) != 0) { _ssh_log(SSH_LOG_FUNCTIONS, __func__, "Using fake authentication data for X11 forwarding"); proto = NULL; cookie = NULL; } _ssh_log(SSH_LOG_FUNCTIONS, __func__, "proto: %s - cookie: %s", proto, cookie); /* See https://gitlab.com/libssh/libssh-mirror/-/blob/master/src/channels.c#L2062 for details. */ ret = ssh_channel_request_x11(channel, 0, proto, cookie, 0); if (ret != SSH_OK) return ret; } } ret = _enter_term_raw_mode(); if (ret != 0) exit(-1); ret = ssh_channel_request_shell(channel); if (ret != SSH_OK) return ret; ret = main_loop(channel); if (ret != SSH_OK) return ret; _leave_term_raw_mode(); ssh_channel_close(channel); ssh_channel_free(channel); ssh_disconnect(session); ssh_free(session); ssh_finalize(); }