From 0a981bae6617aefd6e526a6e9ab9667840a28ec8 Mon Sep 17 00:00:00 2001 From: Shuanglei Tao Date: Fri, 16 Sep 2016 13:50:57 +0800 Subject: [PATCH] Add command line options and help --- README.md | 21 +++- http.c | 62 ++++++++++- protocol.c | 53 ++++----- server.c | 314 ++++++++++++++++++++++++++++++++++++++++++++++------- server.h | 31 ++++-- 5 files changed, 401 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index 462ac25..648bdcf 100644 --- a/README.md +++ b/README.md @@ -42,10 +42,27 @@ The `ttyd` executable file will be in the `build` directory. # Usage ``` -Usage: ttyd command [options] +ttyd is a tool for sharing terminal over the web + +USAGE: ttyd [options] [] + +OPTIONS: + --port, -p Port to listen (default: 7681) + --interface, -i Network interface to bind + --credential, -c Credential for Basic Authentication (format: username:password) + --uid, -u User id to run with + --gid, -g Group id to run with + --signal, -s Signal to send to the command when exit it (default: SIGHUP) + --reconnect, -r Time to reconnect for the client in seconds (default: 10) + --ssl, -S Enable ssl + --ssl-cert, -C Ssl certificate file path + --ssl-key, -K Ssl key file path + --ssl-ca, -A Ssl ca file path + --debug, -d Set log level (0-9, default: 7) + --help, -h Print this text and exit ``` -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: +ttyd starts web server at port `7681` by default. 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 diff --git a/http.c b/http.c index 435329a..7c167a6 100644 --- a/http.c +++ b/http.c @@ -1,11 +1,55 @@ #include "server.h" #include "html.h" +int +check_auth(struct lws *wsi) { + if (server->credential == NULL) + return 0; + + int hdr_length = lws_hdr_total_length(wsi, WSI_TOKEN_HTTP_AUTHORIZATION); + char buf[hdr_length + 1]; + int len = lws_hdr_copy(wsi, buf, sizeof(buf), WSI_TOKEN_HTTP_AUTHORIZATION); + if (len > 0) { + // extract base64 text from authorization header + char *ptr = &buf[0]; + char *token, *b64_text = NULL; + int i = 1; + while ((token = strsep(&ptr, " ")) != NULL) { + if (strlen(token) == 0) + continue; + if (i++ == 2) { + b64_text = strdup(token); + break; + } + } + if (b64_text != NULL && strcmp(b64_text, server->credential) == 0) + return 0; + } + + unsigned char buffer[1024 + LWS_PRE], *p, *end; + p = buffer + LWS_PRE; + end = p + sizeof(buffer) - LWS_PRE; + + if (lws_add_http_header_status(wsi, HTTP_STATUS_UNAUTHORIZED, &p, end)) + return 1; + if (lws_add_http_header_by_token(wsi, + WSI_TOKEN_HTTP_WWW_AUTHENTICATE, + (unsigned char *)"Basic realm=\"ttyd\"", + 18, &p, end)) + return 1; + if (lws_add_http_header_content_length(wsi, 0, &p, end)) + return 1; + if (lws_finalize_http_header(wsi, &p, end)) + return 1; + if (lws_write(wsi, buffer + LWS_PRE, p - (buffer + LWS_PRE), LWS_WRITE_HTTP_HEADERS) < 0) + return 1; + + return -1; +} + 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; + unsigned char buffer[4096 + LWS_PRE], *p, *end; char buf[256]; int n; @@ -26,6 +70,18 @@ callback_http(struct lws *wsi, enum lws_callback_reasons reason, void *user, voi goto try_to_reuse; } + // TODO: this doesn't work for websocket + switch (check_auth(wsi)) { + case 1: + return 1; + case -1: + goto try_to_reuse; + case 0: + default: + break; + } + + // if a legal POST URL, let it continue and accept data if (lws_hdr_total_length(wsi, WSI_TOKEN_POST_URI)) return 0; diff --git a/protocol.c b/protocol.c index f1f9b56..7bce529 100644 --- a/protocol.c +++ b/protocol.c @@ -51,7 +51,7 @@ send_initial_message(struct lws *wsi) { return -1; } // reconnect time - n = sprintf((char *) p, "%c%d", SET_RECONNECT, 10); + n = sprintf((char *) p, "%c%d", SET_RECONNECT, server->reconnect); if (lws_write(wsi, p, (size_t) n, LWS_WRITE_TEXT) < n) { return -1; } @@ -84,6 +84,32 @@ parse_window_size(const char *json) { return size; } +void +tty_client_destroy(struct tty_client *client) { + if (client->exit) + 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); + + // 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; @@ -143,31 +169,6 @@ thread_run_command(void *args) { 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) { diff --git a/server.c b/server.c index d8abf33..153b7b6 100644 --- a/server.c +++ b/server.c @@ -1,39 +1,81 @@ #include "server.h" +#ifdef __linux__ +/* + * sys_signame -- an ordered list of signals. + * lifted from /usr/include/linux/signal.h + * this particular order is only correct for linux. + * this is _not_ portable. + */ +const char *sys_signame[NSIG] = { + "zero", "HUP", "INT", "QUIT", "ILL", "TRAP", "IOT", "UNUSED", + "FPE", "KILL", "USR1", "SEGV", "USR2", "PIPE", "ALRM", "TERM", + "STKFLT","CHLD", "CONT", "STOP", "TSTP", "TTIN", "TTOU", "IO", + "XCPU", "XFSZ", "VTALRM","PROF", "WINCH", NULL +}; +#endif + volatile bool force_exit = false; struct lws_context *context; struct tty_server *server; +// websocket protocols static const struct lws_protocols protocols[] = { - { - "http-only", - callback_http, - 0, - 0, - }, - { - "tty", - callback_tty, - sizeof(struct tty_client), - 128, - }, + {"http-only", callback_http, 0, 0}, + {"tty", callback_tty, sizeof(struct tty_client), 128}, {NULL, NULL, 0, 0} }; +// websocket extensions static const struct lws_extension extensions[] = { - { - "permessage-deflate", - lws_extension_callback_pm_deflate, - "permessage-deflate" - }, - { - "deflate-frame", - lws_extension_callback_pm_deflate, - "deflate_frame" - }, + {"permessage-deflate", lws_extension_callback_pm_deflate, "permessage-deflate"}, + {"deflate-frame", lws_extension_callback_pm_deflate, "deflate_frame"}, {NULL, NULL, NULL} }; +// command line options +static const struct option options[] = { + {"port", required_argument, NULL, 'p'}, + {"interface", required_argument, NULL, 'i'}, + {"credential", required_argument, NULL, 'c'}, + {"uid", required_argument, NULL, 'u'}, + {"gid", required_argument, NULL, 'g'}, + {"signal", required_argument, NULL, 's'}, + {"reconnect", required_argument, NULL, 'r'}, +#ifdef LWS_OPENSSL_SUPPORT + {"ssl", no_argument, NULL, 'S'}, + {"ssl-cert", required_argument, NULL, 'C'}, + {"ssl-key", required_argument, NULL, 'K'}, + {"ssl-ca", required_argument, NULL, 'A'}, +#endif + {"debug", required_argument, NULL, 'd'}, + {"help", no_argument, NULL, 'h'}, + {NULL, 0, 0, 0} +}; +static const char *opt_string = "p:i:c:u:g:s:r:aSC:K:A:d:vh"; + +void print_help() { + fprintf(stderr, "ttyd is a tool for sharing terminal over the web\n\n" + "USAGE: ttyd [options] []\n\n" + "OPTIONS:\n" + "\t--port, -p Port to listen (default: 7681)\n" + "\t--interface, -i Network interface to bind\n" + "\t--credential, -c Credential for Basic Authentication (format: username:password)\n" + "\t--uid, -u User id to run with\n" + "\t--gid, -g Group id to run with\n" + "\t--signal, -s Signal to send to the command when exit it (default: SIGHUP)\n" + "\t--reconnect, -r Time to reconnect for the client in seconds (default: 10)\n" +#ifdef LWS_OPENSSL_SUPPORT + "\t--ssl, -S Enable ssl\n" + "\t--ssl-cert, -C Ssl certificate file path\n" + "\t--ssl-key, -K Ssl key file path\n" + "\t--ssl-ca, -A Ssl ca file path\n" +#endif + "\t--debug, -d Set log level (0-9, default: 7)\n" + "\t--help, -h Print this text and exit\n" + ); +} + struct tty_server* tty_server_new(int argc, char **argv) { struct tty_server *ts; @@ -42,24 +84,25 @@ tty_server_new(int argc, char **argv) { 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) { + ts->credential = NULL; + ts->reconnect = 10; + ts->sig_code = SIGHUP; + ts->sig_name = strdup("SIGHUP"); + ts->argv = malloc(sizeof(char *) * (argc + 1)); + for (int i = 0; i < argc; i++) { + ts->argv[i] = strdup(argv[i]); + cmd_len += strlen(ts->argv[i]); + if (i != argc - 1) { cmd_len++; // for space } } - ts->argv[argc-1] = NULL; + ts->argv[argc] = NULL; ts->command = malloc(cmd_len); char *ptr = ts->command; - for (int i = 0; i < argc - 1; i++) { + for (int i = 0; i < argc; i++) { ptr = stpcpy(ptr, ts->argv[i]); - if (i != argc -2) { + if (i != argc -1) { sprintf(ptr++, "%c", ' '); } } @@ -67,20 +110,96 @@ tty_server_new(int argc, char **argv) { return ts; } +char * +uppercase(char *str) { + int i = 0; + do { + str[i] = (char) toupper(str[i]); + } while (str[i++] != '\0'); + return str; +} + +// Get human readable signal string +int +get_sig_name(int sig, char *buf) { + int n = sprintf(buf, "SIG%s", sig < NSIG ? sys_signame[sig] : "unknown"); + uppercase(buf); + return n; +} + +// Get signal code from string like SIGHUP +int +get_sig(const char *sig_name) { + if (strcasestr(sig_name, "sig") != sig_name || strlen(sig_name) <= 3) { + return -1; + } + for (int sig = 1; sig < NSIG; sig++) { + const char *name = sys_signame[sig]; + if (strcasecmp(name, sig_name + 3) == 0) + return sig; + } + return -1; +} + void sig_handler(int sig) { + char sig_name[20]; + get_sig_name(sig, sig_name); + lwsl_notice("received signal: %s (%d)\n", sig_name, sig); force_exit = true; lws_cancel_service(context); } +int +calc_command_start(int argc, char **argv) { + // make a copy of argc and argv + int argc_copy = argc; + char **argv_copy = malloc(sizeof(char *) * argc); + for (int i = 0; i < argc; i++) { + argv_copy[i] = strdup(argv[i]); + } + + // do not print error message for invalid option + opterr = 0; + while(getopt_long(argc_copy, argv_copy, opt_string, options, NULL) != -1) + ;; + int start= -1; + if (optind < argc) { + char *command = argv_copy[optind]; + for (int i = 0; i < argc; i++) { + if (strcmp(argv[i], command) == 0) { + start = i; + break; + } + } + } + + // free argv copy + for (int i = 0; i < argc; i++) { + free(argv_copy[i]); + } + free(argv_copy); + + // reset for next use + opterr = 1; + optind = 0; + + return start; +} + int main(int argc, char **argv) { - if (argc == 1) { - printf("Usage: %s command [options]", argv[0]); - exit(EXIT_SUCCESS); + if (argc == 1 || strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0) { + print_help(); + exit(0); + } + // parse command line + int start = calc_command_start(argc, argv); + if (start < 0) { + fprintf(stderr, "ttyd: missing start command\n"); + exit(1); } - server = tty_server_new(argc, argv); - lwsl_notice("start command: %s\n", server->command); + server = tty_server_new(argc - start, &argv[start]); struct lws_context_creation_info info; memset(&info, 0, sizeof(info)); @@ -96,6 +215,113 @@ main(int argc, char **argv) { info.extensions = extensions; info.timeout_secs = 5; + int debug_level = 7; + char iface[128] = ""; +#ifdef LWS_OPENSSL_SUPPORT + bool ssl = false; + char cert_path[1024] = ""; + char key_path[1024] = ""; + char ca_path[1024] = ""; +#endif + + // parse command line options + int c; + while ((c = getopt_long(start, argv, opt_string, options, NULL)) != -1) { + switch (c) { + case 'h': + print_help(); + return 0; + case 'd': + debug_level = atoi(optarg); + break; + case 'p': + info.port = atoi(optarg); + break; + case 'i': + strncpy(iface, optarg, sizeof(iface)); + iface[sizeof(iface)-1] = '\0'; + break; + case 'c': + if (strchr(optarg, ':') == NULL) { + fprintf(stderr, "ttyd: invalid credential, format: username:password\n"); + return -1; + } + server->credential = base64_encode((const unsigned char *) optarg, strlen(optarg)); + break; + case 'u': + info.uid = atoi(optarg); + break; + case 'g': + info.gid = atoi(optarg); + break; + case 's': + { + int sig = get_sig(optarg); + if (sig > 0) { + server->sig_code = get_sig(optarg); + server->sig_name = uppercase(strdup(optarg)); + } else { + fprintf(stderr, "ttyd: invalid signal: %s\n", optarg); + return -1; + } + } + break; + case 'r': + server->reconnect = atoi(optarg); + break; +#ifdef LWS_OPENSSL_SUPPORT + case 'S': + ssl = true; + break; + case 'C': + strncpy(cert_path, optarg, sizeof(cert_path) - 1); + cert_path[sizeof(cert_path) - 1] = '\0'; + break; + case 'K': + strncpy(key_path, optarg, sizeof(key_path) - 1); + key_path[sizeof(key_path) - 1] = '\0'; + break; + case 'A': + strncpy(ca_path, optarg, sizeof(ca_path) - 1); + ca_path[sizeof(ca_path) - 1] = '\0'; + break; +#endif + case '?': + break; + default: + print_help(); + exit(1); + } + } + + lws_set_log_level(debug_level, NULL); + + if (strlen(iface) > 0) + info.iface = iface; +#ifdef LWS_OPENSSL_SUPPORT + if (ssl) { + info.ssl_cert_filepath = cert_path; + info.ssl_private_key_filepath = key_path; + info.ssl_ca_filepath = ca_path; + info.ssl_cipher_list = "ECDHE-ECDSA-AES256-GCM-SHA384:" + "ECDHE-RSA-AES256-GCM-SHA384:" + "DHE-RSA-AES256-GCM-SHA384:" + "ECDHE-RSA-AES256-SHA384:" + "HIGH:!aNULL:!eNULL:!EXPORT:" + "!DES:!MD5:!PSK:!RC4:!HMAC_SHA1:" + "!SHA1:!DHE-RSA-AES128-GCM-SHA256:" + "!DHE-RSA-AES128-SHA256:" + "!AES128-GCM-SHA256:" + "!AES128-SHA256:" + "!DHE-RSA-AES256-SHA256:" + "!AES256-GCM-SHA384:" + "!AES256-SHA256"; +#if LWS_LIBRARY_VERSION_MAJOR == 2 + info.options |= LWS_SERVER_OPTION_REDIRECT_HTTP_TO_HTTPS; +#endif + } +#endif + signal(SIGINT, sig_handler); context = lws_create_context(&info); @@ -104,6 +330,13 @@ main(int argc, char **argv) { return -1; } + lwsl_notice("TTY configuration:\n"); + if (server->credential != NULL) + lwsl_notice(" credential: %s\n", server->credential); + lwsl_notice(" start command: %s\n", server->command); + lwsl_notice(" reconnect timeout: %ds\n", server->reconnect); + lwsl_notice(" close signal: %s (%d)\n", server->sig_name, server->sig_code); + // libwebsockets main loop while (!force_exit) { pthread_mutex_lock(&server->lock); @@ -122,12 +355,15 @@ main(int argc, char **argv) { lws_context_destroy(context); // cleanup + if (server->credential != NULL) + free(server->credential); + free(server->command); int i = 0; do { free(server->argv[i++]); } while (server->argv[i] != NULL); free(server->argv); - free(server->command); + free(server->sig_name); free(server); return 0; diff --git a/server.h b/server.h index 079e10b..4f94173 100644 --- a/server.h +++ b/server.h @@ -1,20 +1,23 @@ #include "lws_config.h" +// warning: implicit declaration of function +#define _GNU_SOURCE + #include #include #include +#include #include #include +#include +#include +#include #include #include #include #include #include #include -#include -#include - -#include #ifdef __APPLE__ #include @@ -22,6 +25,7 @@ #include #endif +#include #include extern volatile bool force_exit; @@ -52,15 +56,22 @@ struct tty_client { }; struct tty_server { - LIST_HEAD(client, tty_client) clients; - int client_count; - char *command; // full command line - char **argv; // command with arguments + LIST_HEAD(client, tty_client) clients; // client list + int client_count; // client count + char *credential; // encoded basic auth credential + int reconnect; // reconnect timeout + char *command; // full command line + char **argv; // command with arguments + int sig_code; // close signal + char *sig_name; // human readable signal string pthread_mutex_t lock; }; -extern void -tty_client_destroy(struct tty_client *client); +extern char * +base64_encode(const unsigned char *buffer, size_t length); + +extern int +check_auth(struct lws *wsi); extern int callback_http(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len); -- 2.43.4