Skip to content

Commit a739eb0

Browse files
committed
Add dynamic command handshake support
1 parent eccebc6 commit a739eb0

4 files changed

Lines changed: 101 additions & 10 deletions

File tree

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.12.0)
22

33
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
44

5-
project(ttyd VERSION 1.7.7 LANGUAGES C)
5+
project(ttyd VERSION 1.7.8 LANGUAGES C)
66

77
set(TTYD_VERSION "${PROJECT_VERSION}")
88

src/protocol.c

Lines changed: 88 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -110,21 +110,82 @@ static void process_exit_cb(pty_process *process) {
110110

111111
static char **build_args(struct pss_tty *pss) {
112112
int i, n = 0;
113-
char **argv = xmalloc((server->argc + pss->argc + 1) * sizeof(char *));
114-
115-
for (i = 0; i < server->argc; i++) {
116-
argv[n++] = server->argv[i];
113+
char **argv;
114+
bool use_dynamic = (pss->dynamic_argv != NULL && pss->dynamic_argc > 0);
115+
116+
// Use dynamic command if available, otherwise use server default
117+
if (use_dynamic) {
118+
argv = xmalloc((pss->dynamic_argc + 1) * sizeof(char *));
119+
for (i = 0; i < pss->dynamic_argc; i++) {
120+
argv[n++] = pss->dynamic_argv[i];
121+
}
122+
} else {
123+
argv = xmalloc((server->argc + pss->argc + 1) * sizeof(char *));
124+
for (i = 0; i < server->argc; i++) {
125+
argv[n++] = server->argv[i];
126+
}
117127
}
118128

119-
for (i = 0; i < pss->argc; i++) {
120-
argv[n++] = pss->args[i];
129+
if (!use_dynamic) {
130+
for (i = 0; i < pss->argc; i++) {
131+
argv[n++] = pss->args[i];
132+
}
121133
}
122134

123135
argv[n] = NULL;
124136

125137
return argv;
126138
}
127139

140+
static void free_dynamic_command(struct pss_tty *pss);
141+
142+
// Parse dynamic command string into argv array using /bin/sh -c
143+
// Returns true on success, false on failure (e.g., empty command)
144+
static bool parse_dynamic_command(struct pss_tty *pss, const char *cmd) {
145+
if (cmd == NULL || strlen(cmd) == 0) {
146+
return false;
147+
}
148+
149+
// Limit command length to prevent excessive memory allocation
150+
size_t cmd_len = strlen(cmd);
151+
if (cmd_len > 8192) {
152+
lwsl_warn("dynamic command too long (max 8192 chars)\n");
153+
return false;
154+
}
155+
156+
free_dynamic_command(pss);
157+
158+
#ifdef _WIN32
159+
const char *shell = "cmd.exe";
160+
const char *flag = "/C";
161+
#else
162+
const char *shell = "/bin/sh";
163+
const char *flag = "-c";
164+
#endif
165+
166+
// Use shell -c to execute the command
167+
pss->dynamic_argc = 3;
168+
pss->dynamic_argv = xmalloc(4 * sizeof(char *));
169+
pss->dynamic_argv[0] = strdup(shell);
170+
pss->dynamic_argv[1] = strdup(flag);
171+
pss->dynamic_argv[2] = strdup(cmd);
172+
pss->dynamic_argv[3] = NULL;
173+
174+
return true;
175+
}
176+
177+
// Free dynamic command argv
178+
static void free_dynamic_command(struct pss_tty *pss) {
179+
if (pss->dynamic_argv != NULL) {
180+
for (int i = 0; i < pss->dynamic_argc; i++) {
181+
free(pss->dynamic_argv[i]);
182+
}
183+
free(pss->dynamic_argv);
184+
pss->dynamic_argv = NULL;
185+
pss->dynamic_argc = 0;
186+
}
187+
}
188+
128189
static char **build_env(struct pss_tty *pss) {
129190
int i = 0, n = 2;
130191
char **envp = xmalloc(n * sizeof(char *));
@@ -231,6 +292,8 @@ int callback_tty(struct lws *wsi, enum lws_callback_reasons reason, void *user,
231292
case LWS_CALLBACK_ESTABLISHED:
232293
pss->initialized = false;
233294
pss->authenticated = false;
295+
pss->dynamic_argv = NULL;
296+
pss->dynamic_argc = 0;
234297
pss->wsi = wsi;
235298
pss->lws_close_status = LWS_CLOSE_STATUS_NOSTATUS;
236299

@@ -345,6 +408,19 @@ int callback_tty(struct lws *wsi, enum lws_callback_reasons reason, void *user,
345408
return -1;
346409
}
347410
}
411+
// Parse dynamic command if enabled and present
412+
if (server->allow_dynamic_cmd) {
413+
struct json_object *cmd_obj = NULL;
414+
if (json_object_object_get_ex(obj, "command", &cmd_obj)) {
415+
const char *cmd = json_object_get_string(cmd_obj);
416+
if (cmd != NULL && strlen(cmd) > 0) {
417+
if (!parse_dynamic_command(pss, cmd)) {
418+
lwsl_warn("failed to parse dynamic command\n");
419+
// Fall back to server default command
420+
}
421+
}
422+
}
423+
}
348424
json_object_put(obj);
349425
if (!spawn_process(pss, columns, rows)) return 1;
350426
break;
@@ -366,9 +442,13 @@ int callback_tty(struct lws *wsi, enum lws_callback_reasons reason, void *user,
366442
lwsl_notice("WS closed from %s, clients: %d\n", pss->address, server->client_count);
367443
if (pss->buffer != NULL) free(pss->buffer);
368444
if (pss->pty_buf != NULL) pty_buf_free(pss->pty_buf);
369-
for (int i = 0; i < pss->argc; i++) {
370-
free(pss->args[i]);
445+
if (pss->args != NULL) {
446+
for (int i = 0; i < pss->argc; i++) {
447+
free(pss->args[i]);
448+
}
449+
free(pss->args);
371450
}
451+
free_dynamic_command(pss);
372452

373453
if (pss->process != NULL) {
374454
((pty_ctx_t *)pss->process->ctx)->ws_closed = true;

src/server.c

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,12 @@ static const struct option options[] = {{"port", required_argument, NULL, 'p'},
8080
{"once", no_argument, NULL, 'o'},
8181
{"exit-no-conn", no_argument, NULL, 'q'},
8282
{"browser", no_argument, NULL, 'B'},
83+
{"allow-dynamic-command", no_argument, NULL, 'D'},
8384
{"debug", required_argument, NULL, 'd'},
8485
{"version", no_argument, NULL, 'v'},
8586
{"help", no_argument, NULL, 'h'},
8687
{NULL, 0, 0, 0}};
87-
static const char *opt_string = "p:i:U:c:H:u:g:s:w:I:b:P:f:6aSC:K:A:Wt:T:Om:oqBd:vh";
88+
static const char *opt_string = "p:i:U:c:H:u:g:s:w:I:b:P:f:6aSC:K:A:Wt:T:Om:oqBDd:vh";
8889

8990
static void print_help() {
9091
// clang-format off
@@ -112,6 +113,7 @@ static void print_help() {
112113
" -o, --once Accept only one client and exit on disconnection\n"
113114
" -q, --exit-no-conn Exit on all clients disconnection\n"
114115
" -B, --browser Open terminal with the default system browser\n"
116+
" -D, --allow-dynamic-command Allow client to specify command via WebSocket handshake\n"
115117
" -I, --index Custom index.html path\n"
116118
" -b, --base-path Expected base path for requests coming from a reverse proxy (eg: /mounted/here, max length: 128)\n"
117119
#if LWS_LIBRARY_VERSION_NUMBER >= 4000000
@@ -155,6 +157,7 @@ static void print_config() {
155157
if (server->max_clients > 0) lwsl_notice(" max clients: %d\n", server->max_clients);
156158
if (server->once) lwsl_notice(" once: true\n");
157159
if (server->exit_no_conn) lwsl_notice(" exit_no_conn: true\n");
160+
if (server->allow_dynamic_cmd) lwsl_notice(" allow dynamic command: true\n");
158161
if (server->index != NULL) lwsl_notice(" custom index.html: %s\n", server->index);
159162
if (server->cwd != NULL) lwsl_notice(" working directory: %s\n", server->cwd);
160163
if (!server->writable) lwsl_warn("The --writable option is not set, will start in readonly mode\n");
@@ -378,6 +381,9 @@ int main(int argc, char **argv) {
378381
case 'B':
379382
browser = true;
380383
break;
384+
case 'D':
385+
server->allow_dynamic_cmd = true;
386+
break;
381387
case 'p':
382388
info.port = parse_int("port", optarg);
383389
if (info.port < 0) {

src/server.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ struct pss_tty {
4646
char **args;
4747
int argc;
4848

49+
// dynamic command support
50+
char **dynamic_argv; // argv for dynamic command (NULL if using server default)
51+
int dynamic_argc; // argc for dynamic command
52+
4953
struct lws *wsi;
5054
char *buffer;
5155
size_t len;
@@ -81,6 +85,7 @@ struct server {
8185
bool exit_no_conn; // whether exit on all clients disconnection
8286
char socket_path[255]; // UNIX domain socket path
8387
char terminal_type[30]; // terminal type to report
88+
bool allow_dynamic_cmd; // allow client to send command via websocket
8489

8590
uv_loop_t *loop; // the libuv event loop
8691
};

0 commit comments

Comments
 (0)