mirror of
https://github.com/tsl0922/ttyd.git
synced 2025-12-23 04:14:18 +01:00
394 lines
13 KiB
C
394 lines
13 KiB
C
#include "server.h"
|
|
|
|
#define TTYD_VERSION "1.1.0"
|
|
|
|
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), 0},
|
|
{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"},
|
|
{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'},
|
|
{"ssl", no_argument, NULL, 'S'},
|
|
{"ssl-cert", required_argument, NULL, 'C'},
|
|
{"ssl-key", required_argument, NULL, 'K'},
|
|
{"ssl-ca", required_argument, NULL, 'A'},
|
|
{"readonly", no_argument, NULL, 'R'},
|
|
{"check-origin", no_argument, NULL, 'O'},
|
|
{"once", no_argument, NULL, 'o'},
|
|
{"debug", required_argument, NULL, 'd'},
|
|
{"version", no_argument, NULL, 'v'},
|
|
{"help", no_argument, NULL, 'h'},
|
|
{NULL, 0, 0, 0}
|
|
};
|
|
static const char *opt_string = "p:i:c:u:g:s:r:aSC:K:A:Rt:Ood:vh";
|
|
|
|
void print_help() {
|
|
fprintf(stderr, "ttyd is a tool for sharing terminal over the web\n\n"
|
|
"USAGE:\n"
|
|
" ttyd [options] <command> [<arguments...>]\n\n"
|
|
"VERSION:\n"
|
|
" %s\n\n"
|
|
"OPTIONS:\n"
|
|
" --port, -p Port to listen (default: 7681)\n"
|
|
" --interface, -i Network interface to bind\n"
|
|
" --credential, -c Credential for Basic Authentication (format: username:password)\n"
|
|
" --uid, -u User id to run with\n"
|
|
" --gid, -g Group id to run with\n"
|
|
" --signal, -s Signal to send to the command when exit it (default: SIGHUP)\n"
|
|
" --reconnect, -r Time to reconnect for the client in seconds (default: 10)\n"
|
|
" --readonly, -R Do not allow clients to write to the TTY\n"
|
|
" --client-option, -t Send option to client (format: key=value), repeat to add more options\n"
|
|
" --check-origin, -O Do not allow websocket connection from different origin\n"
|
|
" --once, -o Accept only one client and exit on disconnection\n"
|
|
" --ssl, -S Enable ssl\n"
|
|
" --ssl-cert, -C Ssl certificate file path\n"
|
|
" --ssl-key, -K Ssl key file path\n"
|
|
" --ssl-ca, -A Ssl ca file path\n"
|
|
" --debug, -d Set log level (0-9, default: 7)\n"
|
|
" --version, -v Print the version and exit\n"
|
|
" --help, -h Print this text and exit\n",
|
|
TTYD_VERSION
|
|
);
|
|
}
|
|
|
|
struct tty_server *
|
|
tty_server_new(int argc, char **argv, int start) {
|
|
struct tty_server *ts;
|
|
size_t cmd_len = 0;
|
|
|
|
ts = t_malloc(sizeof(struct tty_server));
|
|
|
|
memset(ts, 0, sizeof(struct tty_server));
|
|
LIST_INIT(&ts->clients);
|
|
ts->client_count = 0;
|
|
ts->reconnect = 10;
|
|
ts->sig_code = SIGHUP;
|
|
ts->sig_name = strdup("SIGHUP");
|
|
if (start == argc)
|
|
return ts;
|
|
|
|
int cmd_argc = argc - start;
|
|
char **cmd_argv = &argv[start];
|
|
ts->argv = t_malloc(sizeof(char *) * (cmd_argc + 1));
|
|
for (int i = 0; i < cmd_argc; i++) {
|
|
ts->argv[i] = strdup(cmd_argv[i]);
|
|
cmd_len += strlen(ts->argv[i]);
|
|
if (i != cmd_argc - 1) {
|
|
cmd_len++; // for space
|
|
}
|
|
}
|
|
ts->argv[cmd_argc] = NULL;
|
|
|
|
ts->command = t_malloc(cmd_len);
|
|
char *ptr = ts->command;
|
|
for (int i = 0; i < cmd_argc; i++) {
|
|
ptr = stpcpy(ptr, ts->argv[i]);
|
|
if (i != cmd_argc - 1) {
|
|
sprintf(ptr++, "%c", ' ');
|
|
}
|
|
}
|
|
|
|
return ts;
|
|
}
|
|
|
|
void
|
|
sig_handler(int sig) {
|
|
if (force_exit)
|
|
exit(EXIT_FAILURE);
|
|
|
|
char sig_name[20];
|
|
get_sig_name(sig, sig_name);
|
|
lwsl_notice("received signal: %s (%d), exiting...\n", sig_name, sig);
|
|
force_exit = true;
|
|
lws_cancel_service(context);
|
|
lwsl_notice("send ^C to force exit.\n");
|
|
}
|
|
|
|
int
|
|
calc_command_start(int argc, char **argv) {
|
|
// make a copy of argc and argv
|
|
int argc_copy = argc;
|
|
char **argv_copy = t_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 = argc;
|
|
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++) {
|
|
t_free(argv_copy[i]);
|
|
}
|
|
t_free(argv_copy);
|
|
|
|
// reset for next use
|
|
opterr = 1;
|
|
optind = 0;
|
|
|
|
return start;
|
|
}
|
|
|
|
int
|
|
main(int argc, char **argv) {
|
|
if (argc == 1) {
|
|
print_help();
|
|
return 0;
|
|
}
|
|
|
|
int start = calc_command_start(argc, argv);
|
|
server = tty_server_new(argc, argv, start);
|
|
|
|
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;
|
|
|
|
int debug_level = 7;
|
|
char iface[128] = "";
|
|
bool ssl = false;
|
|
char cert_path[1024] = "";
|
|
char key_path[1024] = "";
|
|
char ca_path[1024] = "";
|
|
|
|
struct json_object *client_prefs = json_object_new_object();
|
|
|
|
// 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 'v':
|
|
printf("ttyd version %s\n", TTYD_VERSION);
|
|
return 0;
|
|
case 'd':
|
|
debug_level = atoi(optarg);
|
|
break;
|
|
case 'R':
|
|
server->readonly = true;
|
|
break;
|
|
case 'O':
|
|
server->check_origin = true;
|
|
break;
|
|
case 'o':
|
|
server->once = true;
|
|
break;
|
|
case 'p':
|
|
info.port = atoi(optarg);
|
|
if (info.port < 0) {
|
|
fprintf(stderr, "ttyd: invalid port: %s\n", optarg);
|
|
return -1;
|
|
}
|
|
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);
|
|
if (server->reconnect <= 0) {
|
|
fprintf(stderr, "ttyd: invalid reconnect: %s\n", optarg);
|
|
return -1;
|
|
}
|
|
break;
|
|
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;
|
|
case '?':
|
|
break;
|
|
case 't':
|
|
optind--;
|
|
for(;optind < start && *argv[optind] != '-'; optind++){
|
|
char *option =strdup(optarg);
|
|
char *key = strsep(&option, "=");
|
|
if (key == NULL) {
|
|
fprintf(stderr, "ttyd: invalid client option: %s, format: key=value\n", optarg);
|
|
return -1;
|
|
}
|
|
char *value = strsep(&option, "=");
|
|
t_free(option);
|
|
struct json_object *obj = json_tokener_parse(value);
|
|
json_object_object_add(client_prefs, key, obj != NULL ? obj : json_object_new_string(value));
|
|
}
|
|
break;
|
|
default:
|
|
print_help();
|
|
return -1;
|
|
}
|
|
}
|
|
server->prefs_json = strdup(json_object_to_json_string(client_prefs));
|
|
json_object_put(client_prefs);
|
|
|
|
if (server->command == NULL || strlen(server->command) == 0) {
|
|
fprintf(stderr, "ttyd: missing start command\n");
|
|
return -1;
|
|
}
|
|
|
|
lws_set_log_level(debug_level, NULL);
|
|
|
|
#if LWS_LIBRARY_VERSION_MAJOR == 2
|
|
char server_hdr[128] = "";
|
|
sprintf(server_hdr, "ttyd/%s (libwebsockets/%s)", TTYD_VERSION, LWS_LIBRARY_VERSION);
|
|
info.server_string = server_hdr;
|
|
#endif
|
|
|
|
if (strlen(iface) > 0)
|
|
info.iface = iface;
|
|
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
|
|
}
|
|
|
|
signal(SIGINT, sig_handler); // ^C
|
|
signal(SIGTERM, sig_handler); // kill
|
|
|
|
context = lws_create_context(&info);
|
|
if (context == NULL) {
|
|
lwsl_err("libwebsockets init failed\n");
|
|
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);
|
|
if (server->check_origin)
|
|
lwsl_notice(" check origin: true\n");
|
|
if (server->readonly)
|
|
lwsl_notice(" readonly: true\n");
|
|
if (server->once)
|
|
lwsl_notice(" once: true\n");
|
|
|
|
// 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
|
|
if (server->credential != NULL)
|
|
t_free(server->credential);
|
|
t_free(server->command);
|
|
t_free(server->prefs_json);
|
|
int i = 0;
|
|
do {
|
|
t_free(server->argv[i++]);
|
|
} while (server->argv[i] != NULL);
|
|
t_free(server->argv);
|
|
t_free(server->sig_name);
|
|
t_free(server);
|
|
|
|
return 0;
|
|
}
|