Add command line options and help

This commit is contained in:
Shuanglei Tao
2016-09-16 13:50:57 +08:00
parent 45294ce7a9
commit 0a981bae66
5 changed files with 402 additions and 81 deletions

View File

@@ -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

62
http.c
View File

@@ -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;

View 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) {

316
server.c
View File

@@ -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
main(int argc, char **argv) {
if (argc == 1) {
printf("Usage: %s command [options]", argv[0]);
exit(EXIT_SUCCESS);
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]);
}
server = tty_server_new(argc, argv);
lwsl_notice("start command: %s\n", server->command);
// 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 || 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 - 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;

View File

@@ -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);