From: Shuanglei Tao Date: Mon, 12 Sep 2016 00:30:45 +0000 (+0800) Subject: Initial commit X-Git-Url: http://git.prime8.dev/?a=commitdiff_plain;h=2d53cfe03f92e1ad37bee4c6d2ff67651889f77c;p=ttyd.git Initial commit --- 2d53cfe03f92e1ad37bee4c6d2ff67651889f77c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..62beb87 --- /dev/null +++ b/.gitignore @@ -0,0 +1,52 @@ +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su + +# Cmake files +CMakeCache.txt +CMakeFiles +CMakeScripts +Makefile +cmake_install.cmake +install_manifest.txt +CTestTestfile.cmake +build + +# Clion files +.idea/ + +# Project files +html.h \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..d06092a --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,43 @@ +cmake_minimum_required(VERSION 2.8) +project(ttyd) + +set(SOURCE_FILES server.c http.c protocol.c) + +find_package(OpenSSL REQUIRED) +find_package(Libwebsockets QUIET) + +# libwebsockets 1.x doesn't support cmake +if (NOT Libwebsockets_DIR) + pkg_check_modules(Libwebsockets REQUIRED libwebsockets) + find_path(LIBWEBSOCKETS_INCLUDE_DIR libwebsockets.h + HINTS ${LIBWEBSOCKETS_INCLUDEDIR} ${LIBWEBSOCKETS_INCLUDE_DIRS}) + find_library(LIBWEBSOCKETS_LIBRARIES NAMES websockets libwebsockets + HINTS ${LIBWEBSOCKETS_LIBDIR} ${LIBWEBSOCKETS_LIBRARY_DIRS}) + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(LIBWEBSOCKETS DEFAULT_MSG LIBWEBSOCKETS_LIBRARIES LIBWEBSOCKETS_INCLUDE_DIR) + mark_as_advanced(LIBWEBSOCKETS_INCLUDE_DIR LIBWEBSOCKETS_LIBRARIES) +endif() + +find_package(PkgConfig) +pkg_check_modules(PC_JSON-C REQUIRED json-c) +find_path(JSON-C_INCLUDE_DIR json.h + HINTS ${PC_JSON-C_INCLUDEDIR} ${PC_JSON-C_INCLUDE_DIRS} PATH_SUFFIXES json-c json) +find_library(JSON-C_LIBRARY NAMES json-c libjson-c + HINTS ${PC_JSON-C_LIBDIR} ${PC_JSON-C_LIBRARY_DIRS}) +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(JSON-C DEFAULT_MSG JSON-C_LIBRARY JSON-C_INCLUDE_DIR) +mark_as_advanced(JSON-C_INCLUDE_DIR JSON-C_LIBRARY) + +find_program(CMAKE_XXD NAMES xxd) +add_custom_command(OUTPUT html.h + COMMAND ${CMAKE_XXD} -i index.html html.h + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + COMMENT "Generating html.h from index.html") +list(APPEND SOURCE_FILES html.h) + +set(INCLUDE_DIRS ${OPENSSL_INCLUDE_DIR} ${LIBWEBSOCKETS_INCLUDE_DIR} ${JSON-C_INCLUDE_DIR}) +set(LINK_LIBS util pthread ${OPENSSL_LIBRARIES} ${LIBWEBSOCKETS_LIBRARIES} ${JSON-C_LIBRARY}) + +add_executable(${PROJECT_NAME} ${SOURCE_FILES}) +target_include_directories(${PROJECT_NAME} PUBLIC ${INCLUDE_DIRS}) +target_link_libraries(${PROJECT_NAME} ${LINK_LIBS}) \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d81dc10 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Shuanglei Tao + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0ef7f2b --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# ttyd - terminal emulator for the web + +ttyd is a simple command line tool for sharing terminal over the web, inspired by [GoTTY](https://github.com/yudai/gotty). + +![screenshot](screenshot.gif) + +> **WARNING:** ttyd is still under heavily development, so features may be incomplete or expected to have bugs. + +# Requirements + +- [CMake](https://cmake.org) +- [OpenSSL](https://www.openssl.org) +- [JSON-C](https://github.com/json-c/json-c) +- [Libwebsockets](https://libwebsockets.org) + +# Installation + +### For Mac OS X users + +```bash +brew install cmake openssl json-c libwebsockets +git clone https://github.com/tsl0922/ttyd.git +cd ttyd && mkdir build && cd build +cmake -DOPENSSL_ROOT_DIR=/usr/local/opt/openssl .. +make +``` + +### For Linux users + +Ubuntu as example: + +```bash +sudo apt-get install cmake libwebsockets-dev libjson-c-dev libssl-dev +git clone https://github.com/tsl0922/ttyd.git +cd ttyd && mkdir build && cd build +cmake .. +make +``` + +The `ttyd` executable file will be in the `build` directory. + +# Usage + +``` +Usage: ttyd command [options] +``` + +ttyd will start a web server at port `7681`. When you open , the `command` will be started with `options` as arguments and now you can see the running command on the web! :tada: + +# Credits + +- [GoTTY](https://github.com/yudai/gotty): ttyd is a port of GoTTY to `C` language. +- [hterm](https://chromium.googlesource.com/apps/libapps/+/HEAD/hterm): ttyd uses hterm to run a terminal emulator on the web. \ No newline at end of file diff --git a/http.c b/http.c new file mode 100644 index 0000000..435329a --- /dev/null +++ b/http.c @@ -0,0 +1,75 @@ +#include "server.h" +#include "html.h" + +int +callback_http(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len) { + unsigned char buffer[4096 + LWS_PRE]; + unsigned char *p; + unsigned char *end; + char buf[256]; + int n; + + switch (reason) { + case LWS_CALLBACK_HTTP: + lwsl_notice("lws_http_serve: %s\n", in); + + { + char name[100], rip[50]; + lws_get_peer_addresses(wsi, lws_get_socket_fd(wsi), name, + sizeof(name), rip, sizeof(rip)); + sprintf(buf, "%s (%s)", name, rip); + lwsl_notice("HTTP connect from %s\n", buf); + } + + if (len < 1) { + lws_return_http_status(wsi, HTTP_STATUS_BAD_REQUEST, NULL); + goto try_to_reuse; + } + + if (lws_hdr_total_length(wsi, WSI_TOKEN_POST_URI)) + return 0; + + if (strcmp((const char *) in, "/")) { + lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL); + goto try_to_reuse; + } + + p = buffer + LWS_PRE; + end = p + sizeof(buffer) - LWS_PRE; + + if (lws_add_http_header_status(wsi, 200, &p, end)) + return 1; + if (lws_add_http_header_by_token(wsi, + WSI_TOKEN_HTTP_CONTENT_TYPE, + (unsigned char *) "text/html", + 9, &p, end)) + return 1; + if (lws_add_http_header_content_length(wsi, index_html_len, &p, end)) + return 1; + if (lws_finalize_http_header(wsi, &p, end)) + return 1; + n = lws_write(wsi, buffer + LWS_PRE, p - (buffer + LWS_PRE), LWS_WRITE_HTTP_HEADERS); + if (n < 0) { + return 1; + } + + n = lws_write_http(wsi, index_html, index_html_len); + if (n < 0) + return 1; + goto try_to_reuse; + case LWS_CALLBACK_HTTP_WRITEABLE: + lwsl_info("LWS_CALLBACK_HTTP_WRITEABLE\n"); + break; + default: + break; + } + + return 0; + + /* if we're on HTTP1.1 or 2.0, will keep the idle connection alive */ + try_to_reuse: + if (lws_http_transaction_completed(wsi)) + return -1; + + return 0; +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..37d54d6 --- /dev/null +++ b/index.html @@ -0,0 +1,674 @@ + + + + + Web Terminal + + + + + + +
+ + \ No newline at end of file diff --git a/protocol.c b/protocol.c new file mode 100644 index 0000000..f1f9b56 --- /dev/null +++ b/protocol.c @@ -0,0 +1,287 @@ +#include "server.h" + +// client message +#define INPUT '0' +#define PING '1' +#define RESIZE_TERMINAL '2' + +// 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 + +char * +base64_encode(const unsigned char *buffer, size_t length) { + BIO *bio, *b64; + BUF_MEM *bptr; + + b64 = BIO_new(BIO_f_base64()); + bio = BIO_new(BIO_s_mem()); + b64 = BIO_push(b64, bio); + + BIO_write(b64, buffer, (int) length); + BIO_flush(b64); + BIO_get_mem_ptr(b64, &bptr); + + char *data = (char *)malloc(bptr->length); + memcpy(data, bptr->data, bptr->length-1); + data[bptr->length-1] = 0; + + BIO_free_all(b64); + + return data; +} + +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, 10); + 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!"); + return NULL; + } + columns = json_object_get_int(o); + if (!json_object_object_get_ex(obj, "rows", &o)) { + lwsl_err("rows field not exists!"); + return NULL; + } + rows = json_object_get_int(o); + + struct winsize *size = malloc(sizeof(struct winsize)); + memset(size, 0, sizeof(struct winsize)); + size->ws_col = (unsigned short) columns; + size->ws_row = (unsigned short) rows; + + return size; +} + +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: /* */ + 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 *) malloc(sizeof(struct pty_data)); + frame->len = bytes; + if (bytes > 0) { + frame->data = 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; +} + +void +tty_client_destroy(struct tty_client *client) { + if (client->exit) + return; + + // stop event loop + client->exit = true; + + // kill process and free resource + if (kill(client->pid, SIGHUP) != 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); + + // remove from clients list + pthread_mutex_lock(&server->lock); + LIST_REMOVE(client, list); + server->client_count--; + pthread_mutex_unlock(&server->lock); +} + +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; + char *data; + struct winsize *size; + + switch (reason) { + case LWS_CALLBACK_ESTABLISHED: + client->exit = false; + client->initialized = false; + client->wsi = wsi; + 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); + 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); + + 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); + free(frame->data); + free(frame); + + if(lws_partial_buffered(wsi)){ + lws_callback_on_writable(wsi); + break; + } + } + pthread_mutex_unlock(&client->lock); + break; + + case LWS_CALLBACK_RECEIVE: + data = (char *) in; + char command = data[0]; + switch (command) { + case INPUT: + if (write(client->pty, data + 1, len - 1) < 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(data + 1); + if (size != NULL) { + if (ioctl(client->pty, TIOCSWINSZ, size) == -1) { + lwsl_err("ioctl TIOCSWINSZ: %d (%s)\n", errno, strerror(errno)); + } + free(size); + } + break; + default: + lwsl_notice("unknown message type: %c\n", command); + break; + } + 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); + break; + + default: + break; + } + + return 0; +} \ No newline at end of file diff --git a/screenshot.gif b/screenshot.gif new file mode 100644 index 0000000..503c515 Binary files /dev/null and b/screenshot.gif differ diff --git a/server.c b/server.c new file mode 100644 index 0000000..d8abf33 --- /dev/null +++ b/server.c @@ -0,0 +1,134 @@ +#include "server.h" + +volatile bool force_exit = false; +struct lws_context *context; +struct tty_server *server; + +static const struct lws_protocols protocols[] = { + { + "http-only", + callback_http, + 0, + 0, + }, + { + "tty", + callback_tty, + sizeof(struct tty_client), + 128, + }, + {NULL, NULL, 0, 0} +}; + +static const struct lws_extension extensions[] = { + { + "permessage-deflate", + lws_extension_callback_pm_deflate, + "permessage-deflate" + }, + { + "deflate-frame", + lws_extension_callback_pm_deflate, + "deflate_frame" + }, + {NULL, NULL, NULL} +}; + +struct tty_server* +tty_server_new(int argc, char **argv) { + struct tty_server *ts; + size_t cmd_len = 0; + + ts = malloc(sizeof(struct tty_server)); + LIST_INIT(&ts->clients); + ts->client_count = 0; + ts->argv = malloc(sizeof(char *) * argc); + for (int i = 1; i < argc; i++) { + size_t len = strlen(argv[i]); + ts->argv[i-1] = malloc(len); + strcpy(ts->argv[i-1], argv[i]); + + cmd_len += len; + if (i != argc -1) { + cmd_len++; // for space + } + } + ts->argv[argc-1] = NULL; + + ts->command = malloc(cmd_len); + char *ptr = ts->command; + for (int i = 0; i < argc - 1; i++) { + ptr = stpcpy(ptr, ts->argv[i]); + if (i != argc -2) { + sprintf(ptr++, "%c", ' '); + } + } + + return ts; +} + +void +sig_handler(int sig) { + force_exit = true; + lws_cancel_service(context); +} + +int +main(int argc, char **argv) { + if (argc == 1) { + printf("Usage: %s command [options]", argv[0]); + exit(EXIT_SUCCESS); + } + server = tty_server_new(argc, argv); + lwsl_notice("start command: %s\n", server->command); + + struct lws_context_creation_info info; + memset(&info, 0, sizeof(info)); + info.port = 7681; + info.iface = NULL; + info.protocols = protocols; + info.ssl_cert_filepath = NULL; + info.ssl_private_key_filepath = NULL; + info.gid = -1; + info.uid = -1; + info.max_http_header_pool = 16; + info.options = LWS_SERVER_OPTION_VALIDATE_UTF8; + info.extensions = extensions; + info.timeout_secs = 5; + + signal(SIGINT, sig_handler); + + context = lws_create_context(&info); + if (context == NULL) { + lwsl_err("libwebsockets init failed\n"); + return -1; + } + + // libwebsockets main loop + while (!force_exit) { + pthread_mutex_lock(&server->lock); + if (!LIST_EMPTY(&server->clients)) { + struct tty_client *client; + LIST_FOREACH(client, &server->clients, list) { + if (!STAILQ_EMPTY(&client->queue)) { + lws_callback_on_writable(client->wsi); + } + } + } + pthread_mutex_unlock(&server->lock); + lws_service(context, 10); + } + + lws_context_destroy(context); + + // cleanup + int i = 0; + do { + free(server->argv[i++]); + } while (server->argv[i] != NULL); + free(server->argv); + free(server->command); + free(server); + + return 0; +} diff --git a/server.h b/server.h new file mode 100644 index 0000000..079e10b --- /dev/null +++ b/server.h @@ -0,0 +1,70 @@ +#include "lws_config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#ifdef __APPLE__ +#include +#else +#include +#endif + +#include + +extern volatile bool force_exit; +extern struct lws_context *context; +extern struct tty_server *server; + +struct pty_data { + char *data; + int len; + STAILQ_ENTRY(pty_data) list; +}; + +struct tty_client { + bool exit; + bool initialized; + char hostname[100]; + char address[50]; + + struct lws *wsi; + int pid; + int pty; + pthread_t thread; + + STAILQ_HEAD(pty, pty_data) queue; + pthread_mutex_t lock; + + LIST_ENTRY(tty_client) list; +}; + +struct tty_server { + LIST_HEAD(client, tty_client) clients; + int client_count; + char *command; // full command line + char **argv; // command with arguments + pthread_mutex_t lock; +}; + +extern void +tty_client_destroy(struct tty_client *client); + +extern int +callback_http(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len); + +extern int +callback_tty(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len); +