#include "server.h" // client message #define INPUT '0' #define PING '1' #define RESIZE_TERMINAL '2' #define JSON_DATA '{' // server message #define OUTPUT '0' #define PONG '1' #define SET_WINDOW_TITLE '2' #define SET_PREFERENCES '3' #define SET_RECONNECT '4' #define BUF_SIZE 1024 int send_initial_message(struct lws *wsi) { unsigned char message[LWS_PRE + 256]; unsigned char *p = &message[LWS_PRE]; int n; char hostname[128]; gethostname(hostname, sizeof(hostname) - 1); // window title n = sprintf((char *) p, "%c%s (%s)", SET_WINDOW_TITLE, server->command, hostname); if (lws_write(wsi, p, (size_t) n, LWS_WRITE_TEXT) < n) { return -1; } // reconnect time n = sprintf((char *) p, "%c%d", SET_RECONNECT, server->reconnect); if (lws_write(wsi, p, (size_t) n, LWS_WRITE_TEXT) < n) { return -1; } // client preferences n = sprintf((char *) p, "%c%s", SET_PREFERENCES, server->prefs_json); if (lws_write(wsi, p, (size_t) n, LWS_WRITE_TEXT) < n) { return -1; } return 0; } struct winsize * parse_window_size(const char *json) { int columns, rows; json_object *obj = json_tokener_parse(json); struct json_object *o = NULL; if (!json_object_object_get_ex(obj, "columns", &o)) { lwsl_err("columns field not exists!\n"); return NULL; } columns = json_object_get_int(o); if (!json_object_object_get_ex(obj, "rows", &o)) { lwsl_err("rows field not exists!\n"); return NULL; } rows = json_object_get_int(o); json_object_put(obj); struct winsize *size = t_malloc(sizeof(struct winsize)); memset(size, 0, sizeof(struct winsize)); size->ws_col = (unsigned short) columns; size->ws_row = (unsigned short) rows; return size; } bool check_host_origin(struct lws *wsi) { int origin_length = lws_hdr_total_length(wsi, WSI_TOKEN_ORIGIN); char buf[origin_length + 1]; memset(buf, 0, sizeof(buf)); int len = lws_hdr_copy(wsi, buf, sizeof(buf), WSI_TOKEN_ORIGIN); if (len > 0) { const char *prot, *address, *path; int port; if (lws_parse_uri(buf, &prot, &address, &port, &path)) return false; sprintf(buf, "%s:%d", address, port); int host_length = lws_hdr_total_length(wsi, WSI_TOKEN_HOST); if (host_length != strlen(buf)) return false; char host_buf[host_length + 1]; memset(host_buf, 0, sizeof(host_buf)); len = lws_hdr_copy(wsi, host_buf, sizeof(host_buf), WSI_TOKEN_HOST); return len > 0 && strcasecmp(buf, host_buf) == 0; } return false; } void tty_client_destroy(struct tty_client *client) { if (client->exit || client->pid <= 0) return; // stop event loop client->exit = true; // kill process and free resource lwsl_notice("sending %s to process %d\n", server->sig_name, client->pid); if (kill(client->pid, server->sig_code) != 0) { lwsl_err("kill: pid, errno: %d (%s)\n", client->pid, errno, strerror(errno)); } int status; while (waitpid(client->pid, &status, 0) == -1 && errno == EINTR) ; lwsl_notice("process exited with code %d, pid: %d\n", status, client->pid); close(client->pty); // free the buffer if (client->buffer != NULL) t_free(client->buffer); // remove from clients list pthread_mutex_lock(&server->lock); LIST_REMOVE(client, list); server->client_count--; pthread_mutex_unlock(&server->lock); } void * thread_run_command(void *args) { struct tty_client *client; int pty; int bytes; char buf[BUF_SIZE]; fd_set des_set; client = (struct tty_client *) args; pid_t pid = forkpty(&pty, NULL, NULL, NULL); switch (pid) { case -1: /* error */ lwsl_err("forkpty\n"); break; case 0: /* child */ if (setenv("TERM", "xterm-256color", true) < 0) { perror("setenv"); exit(1); } if (execvp(server->argv[0], server->argv) < 0) { perror("execvp"); exit(1); } break; default: /* parent */ lwsl_notice("started process, pid: %d\n", pid); client->pid = pid; client->pty = pty; while (!client->exit) { FD_ZERO (&des_set); FD_SET (pty, &des_set); if (select(pty + 1, &des_set, NULL, NULL, NULL) < 0) { break; } if (FD_ISSET (pty, &des_set)) { memset(buf, 0, BUF_SIZE); bytes = (int) read(pty, buf, BUF_SIZE); struct pty_data *frame = (struct pty_data *) t_malloc(sizeof(struct pty_data)); frame->len = bytes; if (bytes > 0) { frame->data = t_malloc((size_t) bytes); memcpy(frame->data, buf, bytes); } pthread_mutex_lock(&client->lock); STAILQ_INSERT_TAIL(&client->queue, frame, list); pthread_mutex_unlock(&client->lock); } } tty_client_destroy(client); break; } return 0; } int callback_tty(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len) { struct tty_client *client = (struct tty_client *) user; struct winsize *size; switch (reason) { case LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION: if (server->once && server->client_count > 0) { lwsl_notice("refuse to serve new client due to the --once option.\n"); return -1; } if (server->check_origin && !check_host_origin(wsi)) { lwsl_notice("refuse to serve new client from different origin due to the --check-origin option.\n"); return -1; } break; case LWS_CALLBACK_ESTABLISHED: client->exit = false; client->initialized = false; client->authenticated = false; client->wsi = wsi; client->buffer = NULL; lws_get_peer_addresses(wsi, lws_get_socket_fd(wsi), client->hostname, sizeof(client->hostname), client->address, sizeof(client->address)); STAILQ_INIT(&client->queue); if (pthread_create(&client->thread, NULL, thread_run_command, client) != 0) { lwsl_err("pthread_create\n"); return -1; } pthread_mutex_lock(&server->lock); LIST_INSERT_HEAD(&server->clients, client, list); server->client_count++; pthread_mutex_unlock(&server->lock); lwsl_notice("client connected from %s (%s), total: %d\n", client->hostname, client->address, server->client_count); break; case LWS_CALLBACK_SERVER_WRITEABLE: if (!client->initialized) { if (send_initial_message(wsi) < 0) return -1; client->initialized = true; break; } pthread_mutex_lock(&client->lock); while (!STAILQ_EMPTY(&client->queue)) { struct pty_data *frame = STAILQ_FIRST(&client->queue); // read error or client exited, close connection if (frame->len <= 0) { STAILQ_REMOVE_HEAD(&client->queue, list); t_free(frame); return -1; } char *b64_text = base64_encode((const unsigned char *) frame->data, (size_t) frame->len); size_t msg_len = LWS_PRE + strlen(b64_text) + 1; unsigned char message[msg_len]; unsigned char *p = &message[LWS_PRE]; size_t n = sprintf((char *) p, "%c%s", OUTPUT, b64_text); t_free(b64_text); if (lws_write(wsi, p, n, LWS_WRITE_TEXT) < n) { lwsl_err("lws_write\n"); break; } STAILQ_REMOVE_HEAD(&client->queue, list); t_free(frame->data); t_free(frame); if (lws_partial_buffered(wsi)) { lws_callback_on_writable(wsi); break; } } pthread_mutex_unlock(&client->lock); break; case LWS_CALLBACK_RECEIVE: if (client->buffer == NULL) { client->buffer = t_malloc(len + 1); client->len = len; memcpy(client->buffer, in, len); } else { client->buffer = t_realloc(client->buffer, client->len + len + 1); memcpy(client->buffer + client->len, in, len); client->len += len; } client->buffer[client->len] = '\0'; const char command = client->buffer[0]; // check auth if (server->credential != NULL && !client->authenticated && command != JSON_DATA) { lwsl_notice("websocket authentication failed\n"); return -1; } // check if there are more fragmented messages if (lws_remaining_packet_payload(wsi) > 0 || !lws_is_final_fragment(wsi)) { return 0; } switch (command) { case INPUT: if (server->readonly) return 0; if (write(client->pty, client->buffer + 1, client->len - 1) < client->len - 1) { lwsl_err("write INPUT to pty\n"); return -1; } break; case PING: { unsigned char c = PONG; if (lws_write(wsi, &c, 1, LWS_WRITE_TEXT) != 1) { lwsl_err("send PONG\n"); return -1; } } break; case RESIZE_TERMINAL: size = parse_window_size(client->buffer + 1); if (size != NULL) { if (ioctl(client->pty, TIOCSWINSZ, size) == -1) { lwsl_err("ioctl TIOCSWINSZ: %d (%s)\n", errno, strerror(errno)); } t_free(size); } break; case JSON_DATA: if (server->credential == NULL) break; { json_object *obj = json_tokener_parse(client->buffer); struct json_object *o = NULL; if (json_object_object_get_ex(obj, "AuthToken", &o)) { const char *token = json_object_get_string(o); if (strcmp(token, server->credential)) { lwsl_notice("websocket authentication failed with token: %s\n", token); return -1; } } client->authenticated = true; } break; default: lwsl_notice("unknown message type: %c\n", command); break; } if (client->buffer != NULL) { t_free(client->buffer); client->buffer = NULL; } break; case LWS_CALLBACK_CLOSED: tty_client_destroy(client); lwsl_notice("client disconnected from %s (%s), total: %d\n", client->hostname, client->address, server->client_count); if (server->once && server->client_count == 0) { lwsl_notice("exiting due to the --once option.\n"); force_exit = true; lws_cancel_service(context); exit(0); } break; default: break; } return 0; }