]> prime8.dev >> repos - ttyd.git/commitdiff
Add command line options and help
authorShuanglei Tao <tsl0922@gmail.com>
Fri, 16 Sep 2016 05:50:57 +0000 (13:50 +0800)
committerShuanglei Tao <tsl0922@gmail.com>
Fri, 16 Sep 2016 07:34:08 +0000 (15:34 +0800)
README.md
http.c
protocol.c
server.c
server.h

index 462ac25e802b06e07facf8655dfe644e07bcb358..648bdcf167d8137d3b07e01f86d03657e3472865 100644 (file)
--- 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] <command> [<arguments...>]
+
+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 <http://localhost:7681>, 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 <http://localhost:7681>, 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 435329aee428a373b4785819b3873faffbc6311f..7c167a625d55fe0e4b17ab2735ec347c091c6d8e 100644 (file)
--- 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;
 
index f1f9b56f34ee83cb0a0e24f5bee16b6464d63061..7bce529b547823a2335c4996d2377864fb4a18e9 100644 (file)
@@ -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) {
index d8abf331e158dc23b738f177d439ec43e7cc1458..153b7b656405b4befb92bdea0ddaab081c122ad4 100644 (file)
--- 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] <command> [<arguments...>]\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;
index 079e10b3ee9b812a01294c1a7bfec99e261ff5c2..4f9417342cf1c225662ef34b4d10673bc8b9cafe 100644 (file)
--- a/server.h
+++ b/server.h
@@ -1,20 +1,23 @@
 #include "lws_config.h"
 
+// warning: implicit declaration of function
+#define _GNU_SOURCE
+
 #include <stdio.h>
 #include <stdlib.h>
 #include <stdbool.h>
+#include <ctype.h>
 #include <signal.h>
 #include <string.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <pthread.h>
 #include <sys/ioctl.h>
 #include <sys/queue.h>
 #include <sys/stat.h>
 #include <sys/select.h>
 #include <sys/types.h>
 #include <sys/wait.h>
-#include <fcntl.h>
-#include <pthread.h>
-
-#include <libwebsockets.h>
 
 #ifdef __APPLE__
 #include <util.h>
@@ -22,6 +25,7 @@
 #include <pty.h>
 #endif
 
+#include <libwebsockets.h>
 #include <json.h>
 
 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);