fork repo
Some checks failed
backend / cross (aarch64) (push) Failing after 17s
backend / cross (armhf) (push) Failing after 31s
backend / cross (mips) (push) Failing after 31s
backend / cross (mips64) (push) Failing after 31s
backend / cross (mips64el) (push) Failing after 31s
frontend / build (push) Failing after 32s
backend / cross (arm) (push) Failing after 2m19s
backend / cross (i686) (push) Failing after 1s
backend / cross (mipsel) (push) Failing after 31s
backend / cross (s390x) (push) Failing after 31s
backend / cross (win32) (push) Failing after 31s
backend / cross (x86_64) (push) Failing after 32s
docker / build (push) Failing after 6m14s
Some checks failed
backend / cross (aarch64) (push) Failing after 17s
backend / cross (armhf) (push) Failing after 31s
backend / cross (mips) (push) Failing after 31s
backend / cross (mips64) (push) Failing after 31s
backend / cross (mips64el) (push) Failing after 31s
frontend / build (push) Failing after 32s
backend / cross (arm) (push) Failing after 2m19s
backend / cross (i686) (push) Failing after 1s
backend / cross (mipsel) (push) Failing after 31s
backend / cross (s390x) (push) Failing after 31s
backend / cross (win32) (push) Failing after 31s
backend / cross (x86_64) (push) Failing after 32s
docker / build (push) Failing after 6m14s
This commit is contained in:
16460
src/html.h
generated
Normal file
16460
src/html.h
generated
Normal file
File diff suppressed because it is too large
Load Diff
240
src/http.c
Normal file
240
src/http.c
Normal file
@@ -0,0 +1,240 @@
|
||||
#include <libwebsockets.h>
|
||||
#include <string.h>
|
||||
#include <zlib.h>
|
||||
|
||||
#include "html.h"
|
||||
#include "server.h"
|
||||
#include "utils.h"
|
||||
|
||||
enum { AUTH_OK, AUTH_FAIL, AUTH_ERROR };
|
||||
|
||||
static char *html_cache = NULL;
|
||||
static size_t html_cache_len = 0;
|
||||
|
||||
static int send_unauthorized(struct lws *wsi, unsigned int code, enum lws_token_indexes header) {
|
||||
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, code, &p, end) ||
|
||||
lws_add_http_header_by_token(wsi, header, (unsigned char *)"Basic realm=\"ttyd\"", 18, &p, end) ||
|
||||
lws_add_http_header_content_length(wsi, 0, &p, end) || lws_finalize_http_header(wsi, &p, end) ||
|
||||
lws_write(wsi, buffer + LWS_PRE, p - (buffer + LWS_PRE), LWS_WRITE_HTTP_HEADERS) < 0)
|
||||
return AUTH_FAIL;
|
||||
|
||||
return lws_http_transaction_completed(wsi) ? AUTH_FAIL : AUTH_ERROR;
|
||||
}
|
||||
|
||||
static int check_auth(struct lws *wsi, struct pss_http *pss) {
|
||||
if (server->auth_header != NULL) {
|
||||
if (lws_hdr_custom_length(wsi, server->auth_header, strlen(server->auth_header)) > 0) return AUTH_OK;
|
||||
return send_unauthorized(wsi, HTTP_STATUS_PROXY_AUTH_REQUIRED, WSI_TOKEN_HTTP_PROXY_AUTHENTICATE);
|
||||
}
|
||||
|
||||
if(server->credential != NULL) {
|
||||
char buf[256];
|
||||
int len = lws_hdr_copy(wsi, buf, sizeof(buf), WSI_TOKEN_HTTP_AUTHORIZATION);
|
||||
if (len >= 7 && strstr(buf, "Basic ")) {
|
||||
if (!strcmp(buf + 6, server->credential)) return AUTH_OK;
|
||||
}
|
||||
return send_unauthorized(wsi, HTTP_STATUS_UNAUTHORIZED, WSI_TOKEN_HTTP_WWW_AUTHENTICATE);
|
||||
}
|
||||
|
||||
return AUTH_OK;
|
||||
}
|
||||
|
||||
static bool accept_gzip(struct lws *wsi) {
|
||||
char buf[256];
|
||||
int len = lws_hdr_copy(wsi, buf, sizeof(buf), WSI_TOKEN_HTTP_ACCEPT_ENCODING);
|
||||
return len > 0 && strstr(buf, "gzip") != NULL;
|
||||
}
|
||||
|
||||
static bool uncompress_html(char **output, size_t *output_len) {
|
||||
if (html_cache == NULL || html_cache_len == 0) {
|
||||
z_stream stream;
|
||||
memset(&stream, 0, sizeof(stream));
|
||||
if (inflateInit2(&stream, 16 + 15) != Z_OK) return false;
|
||||
|
||||
html_cache_len = index_html_size;
|
||||
html_cache = xmalloc(html_cache_len);
|
||||
|
||||
stream.avail_in = index_html_len;
|
||||
stream.avail_out = html_cache_len;
|
||||
stream.next_in = (void *)index_html;
|
||||
stream.next_out = (void *)html_cache;
|
||||
|
||||
int ret = inflate(&stream, Z_SYNC_FLUSH);
|
||||
inflateEnd(&stream);
|
||||
if (ret != Z_STREAM_END) {
|
||||
free(html_cache);
|
||||
html_cache = NULL;
|
||||
html_cache_len = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
*output = html_cache;
|
||||
*output_len = html_cache_len;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void pss_buffer_free(struct pss_http *pss) {
|
||||
if (pss->buffer != (char *)index_html && pss->buffer != html_cache) free(pss->buffer);
|
||||
}
|
||||
|
||||
static void access_log(struct lws *wsi, const char *path) {
|
||||
char rip[50];
|
||||
|
||||
lws_get_peer_simple(lws_get_network_wsi(wsi), rip, sizeof(rip));
|
||||
lwsl_notice("HTTP %s - %s\n", path, rip);
|
||||
}
|
||||
|
||||
int callback_http(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len) {
|
||||
struct pss_http *pss = (struct pss_http *)user;
|
||||
unsigned char buffer[4096 + LWS_PRE], *p, *end;
|
||||
char buf[256];
|
||||
bool done = false;
|
||||
|
||||
switch (reason) {
|
||||
case LWS_CALLBACK_HTTP:
|
||||
access_log(wsi, (const char *)in);
|
||||
snprintf(pss->path, sizeof(pss->path), "%s", (const char *)in);
|
||||
switch (check_auth(wsi, pss)) {
|
||||
case AUTH_OK:
|
||||
break;
|
||||
case AUTH_FAIL:
|
||||
return 0;
|
||||
case AUTH_ERROR:
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
|
||||
p = buffer + LWS_PRE;
|
||||
end = p + sizeof(buffer) - LWS_PRE;
|
||||
|
||||
if (strcmp(pss->path, endpoints.token) == 0) {
|
||||
const char *credential = server->credential != NULL ? server->credential : "";
|
||||
size_t n = sprintf(buf, "{\"token\": \"%s\"}", credential);
|
||||
if (lws_add_http_header_status(wsi, HTTP_STATUS_OK, &p, end) ||
|
||||
lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE,
|
||||
(unsigned char *)"application/json;charset=utf-8", 30, &p, end) ||
|
||||
lws_add_http_header_content_length(wsi, (unsigned long)n, &p, end) ||
|
||||
lws_finalize_http_header(wsi, &p, end) ||
|
||||
lws_write(wsi, buffer + LWS_PRE, p - (buffer + LWS_PRE), LWS_WRITE_HTTP_HEADERS) < 0)
|
||||
return 1;
|
||||
|
||||
pss->buffer = pss->ptr = strdup(buf);
|
||||
pss->len = n;
|
||||
lws_callback_on_writable(wsi);
|
||||
break;
|
||||
}
|
||||
|
||||
// redirects `/base-path` to `/base-path/`
|
||||
if (strcmp(pss->path, endpoints.parent) == 0) {
|
||||
if (lws_add_http_header_status(wsi, HTTP_STATUS_FOUND, &p, end) ||
|
||||
lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_LOCATION, (unsigned char *)endpoints.index,
|
||||
(int)strlen(endpoints.index), &p, end) ||
|
||||
lws_add_http_header_content_length(wsi, 0, &p, end) || lws_finalize_http_header(wsi, &p, end) ||
|
||||
lws_write(wsi, buffer + LWS_PRE, p - (buffer + LWS_PRE), LWS_WRITE_HTTP_HEADERS) < 0)
|
||||
return 1;
|
||||
goto try_to_reuse;
|
||||
}
|
||||
|
||||
if (strcmp(pss->path, endpoints.index) != 0) {
|
||||
lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL);
|
||||
goto try_to_reuse;
|
||||
}
|
||||
|
||||
const char *content_type = "text/html";
|
||||
if (server->index != NULL) {
|
||||
int n = lws_serve_http_file(wsi, server->index, content_type, NULL, 0);
|
||||
if (n < 0 || (n > 0 && lws_http_transaction_completed(wsi))) return 1;
|
||||
} else {
|
||||
char *output = (char *)index_html;
|
||||
size_t output_len = index_html_len;
|
||||
if (lws_add_http_header_status(wsi, HTTP_STATUS_OK, &p, end) ||
|
||||
lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, (const unsigned char *)content_type, 9, &p,
|
||||
end))
|
||||
return 1;
|
||||
#ifdef LWS_WITH_HTTP_STREAM_COMPRESSION
|
||||
if (!uncompress_html(&output, &output_len)) return 1;
|
||||
#else
|
||||
if (accept_gzip(wsi)) {
|
||||
if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_ENCODING, (unsigned char *)"gzip", 4, &p, end))
|
||||
return 1;
|
||||
} else {
|
||||
if (!uncompress_html(&output, &output_len)) return 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (lws_add_http_header_content_length(wsi, (unsigned long)output_len, &p, end) ||
|
||||
lws_finalize_http_header(wsi, &p, end) ||
|
||||
lws_write(wsi, buffer + LWS_PRE, p - (buffer + LWS_PRE), LWS_WRITE_HTTP_HEADERS) < 0)
|
||||
return 1;
|
||||
|
||||
pss->buffer = pss->ptr = output;
|
||||
pss->len = output_len;
|
||||
lws_callback_on_writable(wsi);
|
||||
}
|
||||
break;
|
||||
|
||||
case LWS_CALLBACK_HTTP_WRITEABLE:
|
||||
if (!pss->buffer || pss->len == 0) {
|
||||
goto try_to_reuse;
|
||||
}
|
||||
|
||||
do {
|
||||
int n = sizeof(buffer) - LWS_PRE;
|
||||
int m = lws_get_peer_write_allowance(wsi);
|
||||
if (m == 0) {
|
||||
lws_callback_on_writable(wsi);
|
||||
return 0;
|
||||
} else if (m != -1 && m < n) {
|
||||
n = m;
|
||||
}
|
||||
if (pss->ptr + n > pss->buffer + pss->len) {
|
||||
n = (int)(pss->len - (pss->ptr - pss->buffer));
|
||||
done = true;
|
||||
}
|
||||
memcpy(buffer + LWS_PRE, pss->ptr, n);
|
||||
pss->ptr += n;
|
||||
if (lws_write_http(wsi, buffer + LWS_PRE, (size_t)n) < n) {
|
||||
pss_buffer_free(pss);
|
||||
return -1;
|
||||
}
|
||||
} while (!lws_send_pipe_choked(wsi) && !done);
|
||||
|
||||
if (!done && pss->ptr < pss->buffer + pss->len) {
|
||||
lws_callback_on_writable(wsi);
|
||||
break;
|
||||
}
|
||||
|
||||
pss_buffer_free(pss);
|
||||
goto try_to_reuse;
|
||||
|
||||
case LWS_CALLBACK_HTTP_FILE_COMPLETION:
|
||||
goto try_to_reuse;
|
||||
#if (defined(LWS_OPENSSL_SUPPORT) || defined(LWS_WITH_TLS)) && !defined(LWS_WITH_MBEDTLS)
|
||||
case LWS_CALLBACK_OPENSSL_PERFORM_CLIENT_CERT_VERIFICATION:
|
||||
if (!len || (SSL_get_verify_result((SSL *)in) != X509_V_OK)) {
|
||||
int err = X509_STORE_CTX_get_error((X509_STORE_CTX *)user);
|
||||
int depth = X509_STORE_CTX_get_error_depth((X509_STORE_CTX *)user);
|
||||
const char *msg = X509_verify_cert_error_string(err);
|
||||
lwsl_err("client certificate verification error: %s (%d), depth: %d\n", msg, err, depth);
|
||||
return 1;
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
/* if we're on HTTP1.1 or 2.0, will keep the idle connection alive */
|
||||
try_to_reuse:
|
||||
if (lws_http_transaction_completed(wsi)) return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
395
src/protocol.c
Normal file
395
src/protocol.c
Normal file
@@ -0,0 +1,395 @@
|
||||
#include <errno.h>
|
||||
#include <json.h>
|
||||
#include <libwebsockets.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "pty.h"
|
||||
#include "server.h"
|
||||
#include "utils.h"
|
||||
|
||||
// initial message list
|
||||
static char initial_cmds[] = {SET_WINDOW_TITLE, SET_PREFERENCES};
|
||||
|
||||
static int send_initial_message(struct lws *wsi, int index) {
|
||||
unsigned char message[LWS_PRE + 1 + 4096];
|
||||
unsigned char *p = &message[LWS_PRE];
|
||||
char buffer[128];
|
||||
int n = 0;
|
||||
|
||||
char cmd = initial_cmds[index];
|
||||
switch (cmd) {
|
||||
case SET_WINDOW_TITLE:
|
||||
gethostname(buffer, sizeof(buffer) - 1);
|
||||
n = sprintf((char *)p, "%c%s (%s)", cmd, server->command, buffer);
|
||||
break;
|
||||
case SET_PREFERENCES:
|
||||
n = sprintf((char *)p, "%c%s", cmd, server->prefs_json);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return lws_write(wsi, p, (size_t)n, LWS_WRITE_BINARY);
|
||||
}
|
||||
|
||||
static json_object *parse_window_size(const char *buf, size_t len, uint16_t *cols, uint16_t *rows) {
|
||||
json_tokener *tok = json_tokener_new();
|
||||
json_object *obj = json_tokener_parse_ex(tok, buf, len);
|
||||
struct json_object *o = NULL;
|
||||
|
||||
if (json_object_object_get_ex(obj, "columns", &o)) *cols = (uint16_t)json_object_get_int(o);
|
||||
if (json_object_object_get_ex(obj, "rows", &o)) *rows = (uint16_t)json_object_get_int(o);
|
||||
|
||||
json_tokener_free(tok);
|
||||
return obj;
|
||||
}
|
||||
|
||||
static bool check_host_origin(struct lws *wsi) {
|
||||
char buf[256];
|
||||
memset(buf, 0, sizeof(buf));
|
||||
int len = lws_hdr_copy(wsi, buf, (int)sizeof(buf), WSI_TOKEN_ORIGIN);
|
||||
if (len <= 0) return false;
|
||||
|
||||
const char *prot, *address, *path;
|
||||
int port;
|
||||
if (lws_parse_uri(buf, &prot, &address, &port, &path)) return false;
|
||||
if (port == 80 || port == 443) {
|
||||
sprintf(buf, "%s", address);
|
||||
} else {
|
||||
sprintf(buf, "%s:%d", address, port);
|
||||
}
|
||||
|
||||
char host_buf[256];
|
||||
memset(host_buf, 0, sizeof(host_buf));
|
||||
len = lws_hdr_copy(wsi, host_buf, (int)sizeof(host_buf), WSI_TOKEN_HOST);
|
||||
|
||||
return len > 0 && strcasecmp(buf, host_buf) == 0;
|
||||
}
|
||||
|
||||
static pty_ctx_t *pty_ctx_init(struct pss_tty *pss) {
|
||||
pty_ctx_t *ctx = xmalloc(sizeof(pty_ctx_t));
|
||||
ctx->pss = pss;
|
||||
ctx->ws_closed = false;
|
||||
return ctx;
|
||||
}
|
||||
|
||||
static void pty_ctx_free(pty_ctx_t *ctx) { free(ctx); }
|
||||
|
||||
static void process_read_cb(pty_process *process, pty_buf_t *buf, bool eof) {
|
||||
pty_ctx_t *ctx = (pty_ctx_t *)process->ctx;
|
||||
if (ctx->ws_closed) {
|
||||
pty_buf_free(buf);
|
||||
return;
|
||||
}
|
||||
|
||||
if (eof && !process_running(process))
|
||||
ctx->pss->lws_close_status = process->exit_code == 0 ? 1000 : 1006;
|
||||
else
|
||||
ctx->pss->pty_buf = buf;
|
||||
lws_callback_on_writable(ctx->pss->wsi);
|
||||
}
|
||||
|
||||
static void process_exit_cb(pty_process *process) {
|
||||
pty_ctx_t *ctx = (pty_ctx_t *)process->ctx;
|
||||
if (ctx->ws_closed) {
|
||||
lwsl_notice("process killed with signal %d, pid: %d\n", process->exit_signal, process->pid);
|
||||
goto done;
|
||||
}
|
||||
|
||||
lwsl_notice("process exited with code %d, pid: %d\n", process->exit_code, process->pid);
|
||||
ctx->pss->process = NULL;
|
||||
ctx->pss->lws_close_status = process->exit_code == 0 ? 1000 : 1006;
|
||||
lws_callback_on_writable(ctx->pss->wsi);
|
||||
|
||||
done:
|
||||
pty_ctx_free(ctx);
|
||||
}
|
||||
|
||||
static char **build_args(struct pss_tty *pss) {
|
||||
int i, n = 0;
|
||||
char **argv = xmalloc((server->argc + pss->argc + 1) * sizeof(char *));
|
||||
|
||||
for (i = 0; i < server->argc; i++) {
|
||||
argv[n++] = server->argv[i];
|
||||
}
|
||||
|
||||
for (i = 0; i < pss->argc; i++) {
|
||||
argv[n++] = pss->args[i];
|
||||
}
|
||||
|
||||
argv[n] = NULL;
|
||||
|
||||
return argv;
|
||||
}
|
||||
|
||||
static char **build_env(struct pss_tty *pss) {
|
||||
int i = 0, n = 2;
|
||||
char **envp = xmalloc(n * sizeof(char *));
|
||||
|
||||
// TERM
|
||||
envp[i] = xmalloc(36);
|
||||
snprintf(envp[i], 36, "TERM=%s", server->terminal_type);
|
||||
i++;
|
||||
|
||||
// TTYD_USER
|
||||
if (strlen(pss->user) > 0) {
|
||||
envp = xrealloc(envp, (++n) * sizeof(char *));
|
||||
envp[i] = xmalloc(40);
|
||||
snprintf(envp[i], 40, "TTYD_USER=%s", pss->user);
|
||||
i++;
|
||||
}
|
||||
|
||||
envp[i] = NULL;
|
||||
|
||||
return envp;
|
||||
}
|
||||
|
||||
static bool spawn_process(struct pss_tty *pss, uint16_t columns, uint16_t rows) {
|
||||
pty_process *process = process_init((void *)pty_ctx_init(pss), server->loop, build_args(pss), build_env(pss));
|
||||
if (server->cwd != NULL) process->cwd = strdup(server->cwd);
|
||||
if (columns > 0) process->columns = columns;
|
||||
if (rows > 0) process->rows = rows;
|
||||
if (pty_spawn(process, process_read_cb, process_exit_cb) != 0) {
|
||||
lwsl_err("pty_spawn: %d (%s)\n", errno, strerror(errno));
|
||||
process_free(process);
|
||||
return false;
|
||||
}
|
||||
lwsl_notice("started process, pid: %d\n", process->pid);
|
||||
pss->process = process;
|
||||
lws_callback_on_writable(pss->wsi);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void wsi_output(struct lws *wsi, pty_buf_t *buf) {
|
||||
if (buf == NULL) return;
|
||||
char *message = xmalloc(LWS_PRE + 1 + buf->len);
|
||||
char *ptr = message + LWS_PRE;
|
||||
|
||||
*ptr = OUTPUT;
|
||||
memcpy(ptr + 1, buf->base, buf->len);
|
||||
size_t n = buf->len + 1;
|
||||
|
||||
if (lws_write(wsi, (unsigned char *)ptr, n, LWS_WRITE_BINARY) < n) {
|
||||
lwsl_err("write OUTPUT to WS\n");
|
||||
}
|
||||
|
||||
free(message);
|
||||
}
|
||||
|
||||
static bool check_auth(struct lws *wsi, struct pss_tty *pss) {
|
||||
if (server->auth_header != NULL) {
|
||||
return lws_hdr_custom_copy(wsi, pss->user, sizeof(pss->user), server->auth_header, strlen(server->auth_header)) > 0;
|
||||
}
|
||||
|
||||
if (server->credential != NULL) {
|
||||
char buf[256];
|
||||
size_t n = lws_hdr_copy(wsi, buf, sizeof(buf), WSI_TOKEN_HTTP_AUTHORIZATION);
|
||||
return n >= 7 && strstr(buf, "Basic ") && !strcmp(buf + 6, server->credential);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int callback_tty(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len) {
|
||||
struct pss_tty *pss = (struct pss_tty *)user;
|
||||
char buf[256];
|
||||
size_t n = 0;
|
||||
|
||||
switch (reason) {
|
||||
case LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION:
|
||||
if (server->once && server->client_count > 0) {
|
||||
lwsl_warn("refuse to serve WS client due to the --once option.\n");
|
||||
return 1;
|
||||
}
|
||||
if (server->max_clients > 0 && server->client_count == server->max_clients) {
|
||||
lwsl_warn("refuse to serve WS client due to the --max-clients option.\n");
|
||||
return 1;
|
||||
}
|
||||
if (!check_auth(wsi, pss)) return 1;
|
||||
|
||||
n = lws_hdr_copy(wsi, pss->path, sizeof(pss->path), WSI_TOKEN_GET_URI);
|
||||
#if defined(LWS_ROLE_H2)
|
||||
if (n <= 0) n = lws_hdr_copy(wsi, pss->path, sizeof(pss->path), WSI_TOKEN_HTTP_COLON_PATH);
|
||||
#endif
|
||||
if (strncmp(pss->path, endpoints.ws, n) != 0) {
|
||||
lwsl_warn("refuse to serve WS client for illegal ws path: %s\n", pss->path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (server->check_origin && !check_host_origin(wsi)) {
|
||||
lwsl_warn(
|
||||
"refuse to serve WS client from different origin due to the "
|
||||
"--check-origin option.\n");
|
||||
return 1;
|
||||
}
|
||||
break;
|
||||
|
||||
case LWS_CALLBACK_ESTABLISHED:
|
||||
pss->initialized = false;
|
||||
pss->authenticated = false;
|
||||
pss->wsi = wsi;
|
||||
pss->lws_close_status = LWS_CLOSE_STATUS_NOSTATUS;
|
||||
|
||||
if (server->url_arg) {
|
||||
while (lws_hdr_copy_fragment(wsi, buf, sizeof(buf), WSI_TOKEN_HTTP_URI_ARGS, n++) > 0) {
|
||||
if (strncmp(buf, "arg=", 4) == 0) {
|
||||
pss->args = xrealloc(pss->args, (pss->argc + 1) * sizeof(char *));
|
||||
pss->args[pss->argc] = strdup(&buf[4]);
|
||||
pss->argc++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
server->client_count++;
|
||||
|
||||
lws_get_peer_simple(lws_get_network_wsi(wsi), pss->address, sizeof(pss->address));
|
||||
lwsl_notice("WS %s - %s, clients: %d\n", pss->path, pss->address, server->client_count);
|
||||
break;
|
||||
|
||||
case LWS_CALLBACK_SERVER_WRITEABLE:
|
||||
if (!pss->initialized) {
|
||||
if (pss->initial_cmd_index == sizeof(initial_cmds)) {
|
||||
pss->initialized = true;
|
||||
pty_resume(pss->process);
|
||||
break;
|
||||
}
|
||||
if (send_initial_message(wsi, pss->initial_cmd_index) < 0) {
|
||||
lwsl_err("failed to send initial message, index: %d\n", pss->initial_cmd_index);
|
||||
lws_close_reason(wsi, LWS_CLOSE_STATUS_UNEXPECTED_CONDITION, NULL, 0);
|
||||
return -1;
|
||||
}
|
||||
pss->initial_cmd_index++;
|
||||
lws_callback_on_writable(wsi);
|
||||
break;
|
||||
}
|
||||
|
||||
if (pss->lws_close_status > LWS_CLOSE_STATUS_NOSTATUS) {
|
||||
lws_close_reason(wsi, pss->lws_close_status, NULL, 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (pss->pty_buf != NULL) {
|
||||
wsi_output(wsi, pss->pty_buf);
|
||||
pty_buf_free(pss->pty_buf);
|
||||
pss->pty_buf = NULL;
|
||||
pty_resume(pss->process);
|
||||
}
|
||||
break;
|
||||
|
||||
case LWS_CALLBACK_RECEIVE:
|
||||
if (pss->buffer == NULL) {
|
||||
pss->buffer = xmalloc(len);
|
||||
pss->len = len;
|
||||
memcpy(pss->buffer, in, len);
|
||||
} else {
|
||||
pss->buffer = xrealloc(pss->buffer, pss->len + len);
|
||||
memcpy(pss->buffer + pss->len, in, len);
|
||||
pss->len += len;
|
||||
}
|
||||
|
||||
const char command = pss->buffer[0];
|
||||
|
||||
// check auth
|
||||
if (server->credential != NULL && !pss->authenticated && command != JSON_DATA) {
|
||||
lwsl_warn("WS client not authenticated\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// check if there are more fragmented messages
|
||||
if (lws_remaining_packet_payload(wsi) > 0 || !lws_is_final_fragment(wsi)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
switch (command) {
|
||||
case INPUT:
|
||||
if (!server->writable) break;
|
||||
int err = pty_write(pss->process, pty_buf_init(pss->buffer + 1, pss->len - 1));
|
||||
if (err) {
|
||||
lwsl_err("uv_write: %s (%s)\n", uv_err_name(err), uv_strerror(err));
|
||||
return -1;
|
||||
}
|
||||
break;
|
||||
case RESIZE_TERMINAL:
|
||||
if (pss->process == NULL) break;
|
||||
json_object_put(
|
||||
parse_window_size(pss->buffer + 1, pss->len - 1, &pss->process->columns, &pss->process->rows));
|
||||
pty_resize(pss->process);
|
||||
break;
|
||||
case PAUSE:
|
||||
pty_pause(pss->process);
|
||||
break;
|
||||
case RESUME:
|
||||
pty_resume(pss->process);
|
||||
break;
|
||||
case JSON_DATA:
|
||||
if (pss->process != NULL) break;
|
||||
uint16_t columns = 0;
|
||||
uint16_t rows = 0;
|
||||
json_object *obj = parse_window_size(pss->buffer, pss->len, &columns, &rows);
|
||||
if (server->credential != NULL) {
|
||||
struct json_object *o = NULL;
|
||||
if (json_object_object_get_ex(obj, "AuthToken", &o)) {
|
||||
const char *token = json_object_get_string(o);
|
||||
if (token != NULL && !strcmp(token, server->credential))
|
||||
pss->authenticated = true;
|
||||
else
|
||||
lwsl_warn("WS authentication failed with token: %s\n", token);
|
||||
}
|
||||
if (!pss->authenticated) {
|
||||
json_object_put(obj);
|
||||
lws_close_reason(wsi, LWS_CLOSE_STATUS_POLICY_VIOLATION, NULL, 0);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
json_object_put(obj);
|
||||
if (!spawn_process(pss, columns, rows)) return 1;
|
||||
break;
|
||||
default:
|
||||
lwsl_warn("ignored unknown message type: %c\n", command);
|
||||
break;
|
||||
}
|
||||
|
||||
if (pss->buffer != NULL) {
|
||||
free(pss->buffer);
|
||||
pss->buffer = NULL;
|
||||
}
|
||||
break;
|
||||
|
||||
case LWS_CALLBACK_CLOSED:
|
||||
if (pss->wsi == NULL) break;
|
||||
|
||||
server->client_count--;
|
||||
lwsl_notice("WS closed from %s, clients: %d\n", pss->address, server->client_count);
|
||||
if (pss->buffer != NULL) free(pss->buffer);
|
||||
if (pss->pty_buf != NULL) pty_buf_free(pss->pty_buf);
|
||||
for (int i = 0; i < pss->argc; i++) {
|
||||
free(pss->args[i]);
|
||||
}
|
||||
|
||||
if (pss->process != NULL) {
|
||||
((pty_ctx_t *)pss->process->ctx)->ws_closed = true;
|
||||
if (process_running(pss->process)) {
|
||||
pty_pause(pss->process);
|
||||
lwsl_notice("killing process, pid: %d\n", pss->process->pid);
|
||||
pty_kill(pss->process, server->sig_code);
|
||||
}
|
||||
}
|
||||
|
||||
if ((server->once || server->exit_no_conn) && server->client_count == 0) {
|
||||
lwsl_notice("exiting due to the --once/--exit-no-conn option.\n");
|
||||
force_exit = true;
|
||||
lws_cancel_service(context);
|
||||
exit(0);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
485
src/pty.c
Normal file
485
src/pty.c
Normal file
@@ -0,0 +1,485 @@
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
#if defined(__OpenBSD__) || defined(__APPLE__)
|
||||
#include <util.h>
|
||||
#elif defined(__FreeBSD__)
|
||||
#include <libutil.h>
|
||||
#else
|
||||
#include <pty.h>
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include <crt_externs.h>
|
||||
#define environ (*_NSGetEnviron())
|
||||
#else
|
||||
extern char **environ;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#include "pty.h"
|
||||
#include "utils.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
HRESULT (WINAPI *pCreatePseudoConsole)(COORD, HANDLE, HANDLE, DWORD, HPCON *);
|
||||
HRESULT (WINAPI *pResizePseudoConsole)(HPCON, COORD);
|
||||
void (WINAPI *pClosePseudoConsole)(HPCON);
|
||||
#endif
|
||||
|
||||
static void alloc_cb(uv_handle_t *unused, size_t suggested_size, uv_buf_t *buf) {
|
||||
buf->base = xmalloc(suggested_size);
|
||||
buf->len = suggested_size;
|
||||
}
|
||||
|
||||
static void close_cb(uv_handle_t *handle) { free(handle); }
|
||||
|
||||
static void async_free_cb(uv_handle_t *handle) {
|
||||
free((uv_async_t *) handle -> data);
|
||||
}
|
||||
|
||||
pty_buf_t *pty_buf_init(char *base, size_t len) {
|
||||
pty_buf_t *buf = xmalloc(sizeof(pty_buf_t));
|
||||
buf->base = xmalloc(len);
|
||||
memcpy(buf->base, base, len);
|
||||
buf->len = len;
|
||||
return buf;
|
||||
}
|
||||
|
||||
void pty_buf_free(pty_buf_t *buf) {
|
||||
if (buf == NULL) return;
|
||||
if (buf->base != NULL) free(buf->base);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void read_cb(uv_stream_t *stream, ssize_t n, const uv_buf_t *buf) {
|
||||
uv_read_stop(stream);
|
||||
pty_process *process = (pty_process *) stream->data;
|
||||
if (n <= 0) {
|
||||
if (n == UV_ENOBUFS || n == 0) return;
|
||||
process->read_cb(process, NULL, true);
|
||||
goto done;
|
||||
}
|
||||
process->read_cb(process, pty_buf_init(buf->base, (size_t) n), false);
|
||||
|
||||
done:
|
||||
free(buf->base);
|
||||
}
|
||||
|
||||
static void write_cb(uv_write_t *req, int unused) {
|
||||
pty_buf_t *buf = (pty_buf_t *) req->data;
|
||||
pty_buf_free(buf);
|
||||
free(req);
|
||||
}
|
||||
|
||||
pty_process *process_init(void *ctx, uv_loop_t *loop, char *argv[], char *envp[]) {
|
||||
pty_process *process = xmalloc(sizeof(pty_process));
|
||||
memset(process, 0, sizeof(pty_process));
|
||||
process->ctx = ctx;
|
||||
process->loop = loop;
|
||||
process->argv = argv;
|
||||
process->envp = envp;
|
||||
process->columns = 80;
|
||||
process->rows = 24;
|
||||
process->exit_code = -1;
|
||||
return process;
|
||||
}
|
||||
|
||||
bool process_running(pty_process *process) {
|
||||
return process != NULL && process->pid > 0 && uv_kill(process->pid, 0) == 0;
|
||||
}
|
||||
|
||||
void process_free(pty_process *process) {
|
||||
if (process == NULL) return;
|
||||
#ifdef _WIN32
|
||||
if (process->si.lpAttributeList != NULL) {
|
||||
DeleteProcThreadAttributeList(process->si.lpAttributeList);
|
||||
free(process->si.lpAttributeList);
|
||||
}
|
||||
if (process->pty != NULL) pClosePseudoConsole(process->pty);
|
||||
if (process->handle != NULL) CloseHandle(process->handle);
|
||||
#else
|
||||
close(process->pty);
|
||||
uv_thread_join(&process->tid);
|
||||
#endif
|
||||
if (process->in != NULL) uv_close((uv_handle_t *) process->in, close_cb);
|
||||
if (process->out != NULL) uv_close((uv_handle_t *) process->out, close_cb);
|
||||
if (process->argv != NULL) free(process->argv);
|
||||
if (process->cwd != NULL) free(process->cwd);
|
||||
char **p = process->envp;
|
||||
for (; *p; p++) free(*p);
|
||||
free(process->envp);
|
||||
}
|
||||
|
||||
void pty_pause(pty_process *process) {
|
||||
if (process == NULL) return;
|
||||
if (process->paused) return;
|
||||
uv_read_stop((uv_stream_t *) process->out);
|
||||
}
|
||||
|
||||
void pty_resume(pty_process *process) {
|
||||
if (process == NULL) return;
|
||||
if (!process->paused) return;
|
||||
process->out->data = process;
|
||||
uv_read_start((uv_stream_t *) process->out, alloc_cb, read_cb);
|
||||
}
|
||||
|
||||
int pty_write(pty_process *process, pty_buf_t *buf) {
|
||||
if (process == NULL) {
|
||||
pty_buf_free(buf);
|
||||
return UV_ESRCH;
|
||||
}
|
||||
uv_buf_t b = uv_buf_init(buf->base, buf->len);
|
||||
uv_write_t *req = xmalloc(sizeof(uv_write_t));
|
||||
req->data = buf;
|
||||
return uv_write(req, (uv_stream_t *) process->in, &b, 1, write_cb);
|
||||
}
|
||||
|
||||
bool pty_resize(pty_process *process) {
|
||||
if (process == NULL) return false;
|
||||
if (process->columns <= 0 || process->rows <= 0) return false;
|
||||
#ifdef _WIN32
|
||||
COORD size = {(int16_t) process->columns, (int16_t) process->rows};
|
||||
return pResizePseudoConsole(process->pty, size) == S_OK;
|
||||
#else
|
||||
struct winsize size = {process->rows, process->columns, 0, 0};
|
||||
return ioctl(process->pty, TIOCSWINSZ, &size) == 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool pty_kill(pty_process *process, int sig) {
|
||||
if (process == NULL) return false;
|
||||
#ifdef _WIN32
|
||||
return TerminateProcess(process->handle, 1) != 0;
|
||||
#else
|
||||
return uv_kill(-process->pid, sig) == 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
bool conpty_init() {
|
||||
uv_lib_t kernel;
|
||||
if (uv_dlopen("kernel32.dll", &kernel)) {
|
||||
uv_dlclose(&kernel);
|
||||
return false;
|
||||
}
|
||||
static struct {
|
||||
char *name;
|
||||
FARPROC *ptr;
|
||||
} conpty_entry[] = {{"CreatePseudoConsole", (FARPROC *) &pCreatePseudoConsole},
|
||||
{"ResizePseudoConsole", (FARPROC *) &pResizePseudoConsole},
|
||||
{"ClosePseudoConsole", (FARPROC *) &pClosePseudoConsole},
|
||||
{NULL, NULL}};
|
||||
for (int i = 0; conpty_entry[i].name != NULL && conpty_entry[i].ptr != NULL; i++) {
|
||||
if (uv_dlsym(&kernel, conpty_entry[i].name, (void **) conpty_entry[i].ptr)) {
|
||||
uv_dlclose(&kernel);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static WCHAR *to_utf16(char *str) {
|
||||
int len = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0);
|
||||
if (len <= 0) return NULL;
|
||||
WCHAR *wstr = xmalloc((len + 1) * sizeof(WCHAR));
|
||||
if (len != MultiByteToWideChar(CP_UTF8, 0, str, -1, wstr, len)) {
|
||||
free(wstr);
|
||||
return NULL;
|
||||
}
|
||||
wstr[len] = L'\0';
|
||||
return wstr;
|
||||
}
|
||||
|
||||
// convert argv to cmdline for CreateProcessW
|
||||
static WCHAR *join_args(char **argv) {
|
||||
char args[256] = {0};
|
||||
char **ptr = argv;
|
||||
for (; *ptr; ptr++) {
|
||||
char *quoted = (char *) quote_arg(*ptr);
|
||||
size_t arg_len = strlen(args) + 1;
|
||||
size_t quoted_len = strlen(quoted);
|
||||
if (arg_len == 1) memset(args, 0, 2);
|
||||
if (arg_len != 1) strcat(args, " ");
|
||||
strncat(args, quoted, quoted_len);
|
||||
if (quoted != *ptr) free(quoted);
|
||||
}
|
||||
if (args[255] != '\0') args[255] = '\0'; // truncate
|
||||
return to_utf16(args);
|
||||
}
|
||||
|
||||
static bool conpty_setup(HPCON *hnd, COORD size, STARTUPINFOEXW *si_ex, char **in_name, char **out_name) {
|
||||
static int count = 0;
|
||||
char buf[256];
|
||||
HPCON pty = INVALID_HANDLE_VALUE;
|
||||
SECURITY_ATTRIBUTES sa = {0};
|
||||
HANDLE in_pipe = INVALID_HANDLE_VALUE;
|
||||
HANDLE out_pipe = INVALID_HANDLE_VALUE;
|
||||
const DWORD open_mode = PIPE_ACCESS_INBOUND | PIPE_ACCESS_OUTBOUND | FILE_FLAG_FIRST_PIPE_INSTANCE;
|
||||
const DWORD pipe_mode = PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT;
|
||||
DWORD pid = GetCurrentProcessId();
|
||||
bool ret = false;
|
||||
|
||||
sa.nLength = sizeof(sa);
|
||||
snprintf(buf, sizeof(buf), "\\\\.\\pipe\\ttyd-term-in-%d-%d", pid, count);
|
||||
*in_name = strdup(buf);
|
||||
snprintf(buf, sizeof(buf), "\\\\.\\pipe\\ttyd-term-out-%d-%d", pid, count);
|
||||
*out_name = strdup(buf);
|
||||
in_pipe = CreateNamedPipeA(*in_name, open_mode, pipe_mode, 1, 0, 0, 30000, &sa);
|
||||
out_pipe = CreateNamedPipeA(*out_name, open_mode, pipe_mode, 1, 0, 0, 30000, &sa);
|
||||
if (in_pipe == INVALID_HANDLE_VALUE || out_pipe == INVALID_HANDLE_VALUE) {
|
||||
print_error("CreateNamedPipeA");
|
||||
goto failed;
|
||||
}
|
||||
|
||||
HRESULT hr = pCreatePseudoConsole(size, in_pipe, out_pipe, 0, &pty);
|
||||
if (FAILED(hr)) {
|
||||
print_error("CreatePseudoConsole");
|
||||
goto failed;
|
||||
}
|
||||
|
||||
si_ex->StartupInfo.cb = sizeof(STARTUPINFOEXW);
|
||||
si_ex->StartupInfo.dwFlags |= STARTF_USESTDHANDLES;
|
||||
si_ex->StartupInfo.hStdError = NULL;
|
||||
si_ex->StartupInfo.hStdInput = NULL;
|
||||
si_ex->StartupInfo.hStdOutput = NULL;
|
||||
size_t bytes_required;
|
||||
InitializeProcThreadAttributeList(NULL, 1, 0, &bytes_required);
|
||||
si_ex->lpAttributeList = (PPROC_THREAD_ATTRIBUTE_LIST) xmalloc(bytes_required);
|
||||
if (!InitializeProcThreadAttributeList(si_ex->lpAttributeList, 1, 0, &bytes_required)) {
|
||||
print_error("InitializeProcThreadAttributeList");
|
||||
goto failed;
|
||||
}
|
||||
if (!UpdateProcThreadAttribute(si_ex->lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, pty, sizeof(HPCON),
|
||||
NULL, NULL)) {
|
||||
print_error("UpdateProcThreadAttribute");
|
||||
goto failed;
|
||||
}
|
||||
count++;
|
||||
*hnd = pty;
|
||||
ret = true;
|
||||
goto done;
|
||||
|
||||
failed:
|
||||
ret = false;
|
||||
free(*in_name);
|
||||
*in_name = NULL;
|
||||
free(*out_name);
|
||||
*out_name = NULL;
|
||||
done:
|
||||
if (in_pipe != INVALID_HANDLE_VALUE) CloseHandle(in_pipe);
|
||||
if (out_pipe != INVALID_HANDLE_VALUE) CloseHandle(out_pipe);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void connect_cb(uv_connect_t *req, int status) { free(req); }
|
||||
|
||||
static void CALLBACK conpty_exit(void *context, BOOLEAN unused) {
|
||||
pty_process *process = (pty_process *) context;
|
||||
uv_async_send(&process->async);
|
||||
}
|
||||
|
||||
static void async_cb(uv_async_t *async) {
|
||||
pty_process *process = (pty_process *) async->data;
|
||||
UnregisterWait(process->wait);
|
||||
|
||||
DWORD exit_code;
|
||||
GetExitCodeProcess(process->handle, &exit_code);
|
||||
process->exit_code = (int) exit_code;
|
||||
process->exit_signal = 1;
|
||||
process->exit_cb(process);
|
||||
|
||||
uv_close((uv_handle_t *) async, async_free_cb);
|
||||
process_free(process);
|
||||
}
|
||||
|
||||
int pty_spawn(pty_process *process, pty_read_cb read_cb, pty_exit_cb exit_cb) {
|
||||
char *in_name = NULL;
|
||||
char *out_name = NULL;
|
||||
DWORD flags = EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT;
|
||||
COORD size = {(int16_t) process->columns, (int16_t) process->rows};
|
||||
|
||||
if (!conpty_setup(&process->pty, size, &process->si, &in_name, &out_name)) return 1;
|
||||
|
||||
SetConsoleCtrlHandler(NULL, FALSE);
|
||||
|
||||
int status = 1;
|
||||
process->in = xmalloc(sizeof(uv_pipe_t));
|
||||
process->out = xmalloc(sizeof(uv_pipe_t));
|
||||
uv_pipe_init(process->loop, process->in, 0);
|
||||
uv_pipe_init(process->loop, process->out, 0);
|
||||
|
||||
uv_connect_t *in_req = xmalloc(sizeof(uv_connect_t));
|
||||
uv_connect_t *out_req = xmalloc(sizeof(uv_connect_t));
|
||||
uv_pipe_connect(in_req, process->in, in_name, connect_cb);
|
||||
uv_pipe_connect(out_req, process->out, out_name, connect_cb);
|
||||
|
||||
PROCESS_INFORMATION pi = {0};
|
||||
WCHAR *cmdline, *cwd;
|
||||
cmdline = join_args(process->argv);
|
||||
if (cmdline == NULL) goto cleanup;
|
||||
if (process->envp != NULL) {
|
||||
char **p = process->envp;
|
||||
for (; *p; p++) {
|
||||
WCHAR *env = to_utf16(*p);
|
||||
if (env == NULL) goto cleanup;
|
||||
_wputenv(env);
|
||||
free(env);
|
||||
}
|
||||
}
|
||||
if (process->cwd != NULL) {
|
||||
cwd = to_utf16(process->cwd);
|
||||
if (cwd == NULL) goto cleanup;
|
||||
}
|
||||
|
||||
if (!CreateProcessW(NULL, cmdline, NULL, NULL, FALSE, flags, NULL, cwd, &process->si.StartupInfo, &pi)) {
|
||||
print_error("CreateProcessW");
|
||||
DWORD exitCode = 0;
|
||||
if (GetExitCodeProcess(pi.hProcess, &exitCode)) printf("== exit code: %d\n", exitCode);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
process->pid = pi.dwProcessId;
|
||||
process->handle = pi.hProcess;
|
||||
process->paused = true;
|
||||
process->read_cb = read_cb;
|
||||
process->exit_cb = exit_cb;
|
||||
process->async.data = process;
|
||||
uv_async_init(process->loop, &process->async, async_cb);
|
||||
|
||||
if (!RegisterWaitForSingleObject(&process->wait, pi.hProcess, conpty_exit, process, INFINITE, WT_EXECUTEONLYONCE)) {
|
||||
print_error("RegisterWaitForSingleObject");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
status = 0;
|
||||
|
||||
cleanup:
|
||||
if (in_name != NULL) free(in_name);
|
||||
if (out_name != NULL) free(out_name);
|
||||
if (cmdline != NULL) free(cmdline);
|
||||
if (cwd != NULL) free(cwd);
|
||||
return status;
|
||||
}
|
||||
#else
|
||||
static bool fd_set_cloexec(const int fd) {
|
||||
int flags = fcntl(fd, F_GETFD);
|
||||
if (flags < 0) return false;
|
||||
return (flags & FD_CLOEXEC) == 0 || fcntl(fd, F_SETFD, flags | FD_CLOEXEC) != -1;
|
||||
}
|
||||
|
||||
static bool fd_duplicate(int fd, uv_pipe_t *pipe) {
|
||||
int fd_dup = dup(fd);
|
||||
if (fd_dup < 0) return false;
|
||||
|
||||
if (!fd_set_cloexec(fd_dup)) return false;
|
||||
|
||||
int status = uv_pipe_open(pipe, fd_dup);
|
||||
if (status) close(fd_dup);
|
||||
return status == 0;
|
||||
}
|
||||
|
||||
static void wait_cb(void *arg) {
|
||||
pty_process *process = (pty_process *) arg;
|
||||
|
||||
pid_t pid;
|
||||
int stat;
|
||||
do
|
||||
pid = waitpid(process->pid, &stat, 0);
|
||||
while (pid != process->pid && errno == EINTR);
|
||||
|
||||
if (WIFEXITED(stat)) {
|
||||
process->exit_code = WEXITSTATUS(stat);
|
||||
}
|
||||
if (WIFSIGNALED(stat)) {
|
||||
int sig = WTERMSIG(stat);
|
||||
process->exit_code = 128 + sig;
|
||||
process->exit_signal = sig;
|
||||
}
|
||||
|
||||
uv_async_send(&process->async);
|
||||
}
|
||||
|
||||
static void async_cb(uv_async_t *async) {
|
||||
pty_process *process = (pty_process *) async->data;
|
||||
process->exit_cb(process);
|
||||
|
||||
uv_close((uv_handle_t *) async, async_free_cb);
|
||||
process_free(process);
|
||||
}
|
||||
|
||||
int pty_spawn(pty_process *process, pty_read_cb read_cb, pty_exit_cb exit_cb) {
|
||||
int status = 0;
|
||||
|
||||
uv_disable_stdio_inheritance();
|
||||
|
||||
int master, pid;
|
||||
struct winsize size = {process->rows, process->columns, 0, 0};
|
||||
pid = forkpty(&master, NULL, NULL, &size);
|
||||
if (pid < 0) {
|
||||
status = -errno;
|
||||
return status;
|
||||
} else if (pid == 0) {
|
||||
setsid();
|
||||
if (process->cwd != NULL) chdir(process->cwd);
|
||||
if (process->envp != NULL) {
|
||||
char **p = process->envp;
|
||||
for (; *p; p++) putenv(*p);
|
||||
}
|
||||
int ret = execvp(process->argv[0], process->argv);
|
||||
if (ret < 0) {
|
||||
perror("execvp failed\n");
|
||||
_exit(-errno);
|
||||
}
|
||||
}
|
||||
|
||||
int flags = fcntl(master, F_GETFL);
|
||||
if (flags == -1) {
|
||||
status = -errno;
|
||||
goto error;
|
||||
}
|
||||
if (fcntl(master, F_SETFL, flags | O_NONBLOCK) == -1) {
|
||||
status = -errno;
|
||||
goto error;
|
||||
}
|
||||
if (!fd_set_cloexec(master)) {
|
||||
status = -errno;
|
||||
goto error;
|
||||
}
|
||||
|
||||
process->in = xmalloc(sizeof(uv_pipe_t));
|
||||
process->out = xmalloc(sizeof(uv_pipe_t));
|
||||
uv_pipe_init(process->loop, process->in, 0);
|
||||
uv_pipe_init(process->loop, process->out, 0);
|
||||
|
||||
if (!fd_duplicate(master, process->in) || !fd_duplicate(master, process->out)) {
|
||||
status = -errno;
|
||||
goto error;
|
||||
}
|
||||
|
||||
process->pty = master;
|
||||
process->pid = pid;
|
||||
process->paused = true;
|
||||
process->read_cb = read_cb;
|
||||
process->exit_cb = exit_cb;
|
||||
process->async.data = process;
|
||||
uv_async_init(process->loop, &process->async, async_cb);
|
||||
uv_thread_create(&process->tid, wait_cb, process);
|
||||
|
||||
return 0;
|
||||
|
||||
error:
|
||||
close(master);
|
||||
uv_kill(pid, SIGKILL);
|
||||
waitpid(pid, NULL, 0);
|
||||
return status;
|
||||
}
|
||||
#endif
|
||||
68
src/pty.h
Normal file
68
src/pty.h
Normal file
@@ -0,0 +1,68 @@
|
||||
#ifndef TTYD_PTY_H
|
||||
#define TTYD_PTY_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <uv.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#ifndef HPCON
|
||||
#define HPCON VOID *
|
||||
#endif
|
||||
#ifndef PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE
|
||||
#define PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE 0x00020016
|
||||
#endif
|
||||
|
||||
bool conpty_init();
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
char *base;
|
||||
size_t len;
|
||||
} pty_buf_t;
|
||||
|
||||
struct pty_process_;
|
||||
typedef struct pty_process_ pty_process;
|
||||
typedef void (*pty_read_cb)(pty_process *, pty_buf_t *, bool);
|
||||
typedef void (*pty_exit_cb)(pty_process *);
|
||||
|
||||
struct pty_process_ {
|
||||
int pid, exit_code, exit_signal;
|
||||
uint16_t columns, rows;
|
||||
#ifdef _WIN32
|
||||
STARTUPINFOEXW si;
|
||||
HPCON pty;
|
||||
HANDLE handle;
|
||||
HANDLE wait;
|
||||
#else
|
||||
pid_t pty;
|
||||
uv_thread_t tid;
|
||||
#endif
|
||||
char **argv;
|
||||
char **envp;
|
||||
char *cwd;
|
||||
|
||||
uv_loop_t *loop;
|
||||
uv_async_t async;
|
||||
uv_pipe_t *in;
|
||||
uv_pipe_t *out;
|
||||
bool paused;
|
||||
|
||||
pty_read_cb read_cb;
|
||||
pty_exit_cb exit_cb;
|
||||
void *ctx;
|
||||
};
|
||||
|
||||
pty_buf_t *pty_buf_init(char *base, size_t len);
|
||||
void pty_buf_free(pty_buf_t *buf);
|
||||
pty_process *process_init(void *ctx, uv_loop_t *loop, char *argv[], char *envp[]);
|
||||
bool process_running(pty_process *process);
|
||||
void process_free(pty_process *process);
|
||||
int pty_spawn(pty_process *process, pty_read_cb read_cb, pty_exit_cb exit_cb);
|
||||
void pty_pause(pty_process *process);
|
||||
void pty_resume(pty_process *process);
|
||||
int pty_write(pty_process *process, pty_buf_t *buf);
|
||||
bool pty_resize(pty_process *process);
|
||||
bool pty_kill(pty_process *process, int sig);
|
||||
|
||||
#endif // TTYD_PTY_H
|
||||
634
src/server.c
Normal file
634
src/server.c
Normal file
@@ -0,0 +1,634 @@
|
||||
#include "server.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <getopt.h>
|
||||
#include <json.h>
|
||||
#include <libwebsockets.h>
|
||||
#include <signal.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
#ifndef TTYD_VERSION
|
||||
#define TTYD_VERSION "unknown"
|
||||
#endif
|
||||
|
||||
volatile bool force_exit = false;
|
||||
struct lws_context *context;
|
||||
struct server *server;
|
||||
struct endpoints endpoints = {"/ws", "/", "/token", ""};
|
||||
|
||||
extern int callback_http(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len);
|
||||
extern int callback_tty(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len);
|
||||
|
||||
// websocket protocols
|
||||
static const struct lws_protocols protocols[] = {{"http-only", callback_http, sizeof(struct pss_http), 0},
|
||||
{"tty", callback_tty, sizeof(struct pss_tty), 0},
|
||||
{NULL, NULL, 0, 0}};
|
||||
|
||||
#ifndef LWS_WITHOUT_EXTENSIONS
|
||||
// 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}};
|
||||
#endif
|
||||
|
||||
#if LWS_LIBRARY_VERSION_NUMBER >= 4000000
|
||||
static const uint32_t backoff_ms[] = {1000, 2000, 3000, 4000, 5000};
|
||||
static lws_retry_bo_t retry = {
|
||||
.retry_ms_table = backoff_ms,
|
||||
.retry_ms_table_count = LWS_ARRAY_SIZE(backoff_ms),
|
||||
.conceal_count = LWS_ARRAY_SIZE(backoff_ms),
|
||||
.secs_since_valid_ping = 5,
|
||||
.secs_since_valid_hangup = 10,
|
||||
.jitter_percent = 0,
|
||||
};
|
||||
#endif
|
||||
|
||||
// command line options
|
||||
static const struct option options[] = {{"port", required_argument, NULL, 'p'},
|
||||
{"interface", required_argument, NULL, 'i'},
|
||||
{"socket-owner", required_argument, NULL, 'U'},
|
||||
{"credential", required_argument, NULL, 'c'},
|
||||
{"auth-header", required_argument, NULL, 'H'},
|
||||
{"uid", required_argument, NULL, 'u'},
|
||||
{"gid", required_argument, NULL, 'g'},
|
||||
{"signal", required_argument, NULL, 's'},
|
||||
{"cwd", required_argument, NULL, 'w'},
|
||||
{"index", required_argument, NULL, 'I'},
|
||||
{"base-path", required_argument, NULL, 'b'},
|
||||
#if LWS_LIBRARY_VERSION_NUMBER >= 4000000
|
||||
{"ping-interval", required_argument, NULL, 'P'},
|
||||
#endif
|
||||
{"srv-buf-size", required_argument, NULL, 'f'},
|
||||
{"ipv6", no_argument, NULL, '6'},
|
||||
{"ssl", no_argument, NULL, 'S'},
|
||||
{"ssl-cert", required_argument, NULL, 'C'},
|
||||
{"ssl-key", required_argument, NULL, 'K'},
|
||||
{"ssl-ca", required_argument, NULL, 'A'},
|
||||
{"url-arg", no_argument, NULL, 'a'},
|
||||
{"writable", no_argument, NULL, 'W'},
|
||||
{"terminal-type", required_argument, NULL, 'T'},
|
||||
{"client-option", required_argument, NULL, 't'},
|
||||
{"check-origin", no_argument, NULL, 'O'},
|
||||
{"max-clients", required_argument, NULL, 'm'},
|
||||
{"once", no_argument, NULL, 'o'},
|
||||
{"exit-no-conn", no_argument, NULL, 'q'},
|
||||
{"browser", no_argument, NULL, 'B'},
|
||||
{"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:U:c:H:u:g:s:w:I:b:P:f:6aSC:K:A:Wt:T:Om:oqBd:vh";
|
||||
|
||||
static void print_help() {
|
||||
// clang-format off
|
||||
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"
|
||||
" -p, --port Port to listen (default: 7681, use `0` for random port)\n"
|
||||
" -i, --interface Network interface to bind (eg: eth0), or UNIX domain socket path (eg: /var/run/ttyd.sock)\n"
|
||||
" -U, --socket-owner User owner of the UNIX domain socket file, when enabled (eg: user:group)\n"
|
||||
" -c, --credential Credential for basic authentication (format: username:password)\n"
|
||||
" -H, --auth-header HTTP Header name for auth proxy, this will configure ttyd to let a HTTP reverse proxy handle authentication\n"
|
||||
" -u, --uid User id to run with\n"
|
||||
" -g, --gid Group id to run with\n"
|
||||
" -s, --signal Signal to send to the command when exit it (default: 1, SIGHUP)\n"
|
||||
" -w, --cwd Working directory to be set for the child program\n"
|
||||
" -a, --url-arg Allow client to send command line arguments in URL (eg: http://localhost:7681?arg=foo&arg=bar)\n"
|
||||
" -W, --writable Allow clients to write to the TTY (readonly by default)\n"
|
||||
" -t, --client-option Send option to client (format: key=value), repeat to add more options\n"
|
||||
" -T, --terminal-type Terminal type to report, default: xterm-256color\n"
|
||||
" -O, --check-origin Do not allow websocket connection from different origin\n"
|
||||
" -m, --max-clients Maximum clients to support (default: 0, no limit)\n"
|
||||
" -o, --once Accept only one client and exit on disconnection\n"
|
||||
" -q, --exit-no-conn Exit on all clients disconnection\n"
|
||||
" -B, --browser Open terminal with the default system browser\n"
|
||||
" -I, --index Custom index.html path\n"
|
||||
" -b, --base-path Expected base path for requests coming from a reverse proxy (eg: /mounted/here, max length: 128)\n"
|
||||
#if LWS_LIBRARY_VERSION_NUMBER >= 4000000
|
||||
" -P, --ping-interval Websocket ping interval(sec) (default: 5)\n"
|
||||
#endif
|
||||
" -f, --srv-buf-size Maximum chunk of file (in bytes) that can be sent at once, a larger value may improve throughput (default: 4096)\n"
|
||||
#ifdef LWS_WITH_IPV6
|
||||
" -6, --ipv6 Enable IPv6 support\n"
|
||||
#endif
|
||||
#if defined(LWS_OPENSSL_SUPPORT) || defined(LWS_WITH_TLS)
|
||||
" -S, --ssl Enable SSL\n"
|
||||
" -C, --ssl-cert SSL certificate file path\n"
|
||||
" -K, --ssl-key SSL key file path\n"
|
||||
" -A, --ssl-ca SSL CA file path for client certificate verification\n"
|
||||
#endif
|
||||
" -d, --debug Set log level (default: 7)\n"
|
||||
" -v, --version Print the version and exit\n"
|
||||
" -h, --help Print this text and exit\n\n"
|
||||
"Visit https://github.com/tsl0922/ttyd to get more information and report bugs.\n",
|
||||
TTYD_VERSION
|
||||
);
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
static void print_config() {
|
||||
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(" close signal: %s (%d)\n", server->sig_name, server->sig_code);
|
||||
lwsl_notice(" terminal type: %s\n", server->terminal_type);
|
||||
if (endpoints.parent[0]) {
|
||||
lwsl_notice("endpoints:\n");
|
||||
lwsl_notice(" base-path: %s\n", endpoints.parent);
|
||||
lwsl_notice(" index : %s\n", endpoints.index);
|
||||
lwsl_notice(" token : %s\n", endpoints.token);
|
||||
lwsl_notice(" websocket: %s\n", endpoints.ws);
|
||||
}
|
||||
if (server->auth_header != NULL) lwsl_notice(" auth header: %s\n", server->auth_header);
|
||||
if (server->check_origin) lwsl_notice(" check origin: true\n");
|
||||
if (server->url_arg) lwsl_notice(" allow url arg: true\n");
|
||||
if (server->max_clients > 0) lwsl_notice(" max clients: %d\n", server->max_clients);
|
||||
if (server->once) lwsl_notice(" once: true\n");
|
||||
if (server->exit_no_conn) lwsl_notice(" exit_no_conn: true\n");
|
||||
if (server->index != NULL) lwsl_notice(" custom index.html: %s\n", server->index);
|
||||
if (server->cwd != NULL) lwsl_notice(" working directory: %s\n", server->cwd);
|
||||
if (!server->writable) lwsl_warn("The --writable option is not set, will start in readonly mode\n");
|
||||
}
|
||||
|
||||
static struct server *server_new(int argc, char **argv, int start) {
|
||||
struct server *ts;
|
||||
size_t cmd_len = 0;
|
||||
|
||||
ts = xmalloc(sizeof(struct server));
|
||||
|
||||
memset(ts, 0, sizeof(struct server));
|
||||
ts->client_count = 0;
|
||||
ts->sig_code = SIGHUP;
|
||||
sprintf(ts->terminal_type, "%s", "xterm-256color");
|
||||
get_sig_name(ts->sig_code, ts->sig_name, sizeof(ts->sig_name));
|
||||
if (start == argc) return ts;
|
||||
|
||||
int cmd_argc = argc - start;
|
||||
char **cmd_argv = &argv[start];
|
||||
ts->argv = xmalloc(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->argc = cmd_argc;
|
||||
|
||||
ts->command = xmalloc(cmd_len + 1);
|
||||
char *ptr = ts->command;
|
||||
for (int i = 0; i < cmd_argc; i++) {
|
||||
size_t len = strlen(ts->argv[i]);
|
||||
ptr = memcpy(ptr, ts->argv[i], len + 1) + len;
|
||||
if (i != cmd_argc - 1) {
|
||||
*ptr++ = ' ';
|
||||
}
|
||||
}
|
||||
*ptr = '\0'; // null terminator
|
||||
|
||||
ts->loop = xmalloc(sizeof *ts->loop);
|
||||
uv_loop_init(ts->loop);
|
||||
|
||||
return ts;
|
||||
}
|
||||
|
||||
static void server_free(struct server *ts) {
|
||||
if (ts == NULL) return;
|
||||
if (ts->credential != NULL) free(ts->credential);
|
||||
if (ts->auth_header != NULL) free(ts->auth_header);
|
||||
if (ts->index != NULL) free(ts->index);
|
||||
if (ts->cwd != NULL) free(ts->cwd);
|
||||
free(ts->command);
|
||||
free(ts->prefs_json);
|
||||
|
||||
char **p = ts->argv;
|
||||
for (; *p; p++) free(*p);
|
||||
free(ts->argv);
|
||||
|
||||
if (strlen(ts->socket_path) > 0) {
|
||||
struct stat st;
|
||||
if (!stat(ts->socket_path, &st)) {
|
||||
unlink(ts->socket_path);
|
||||
}
|
||||
}
|
||||
|
||||
uv_loop_close(ts->loop);
|
||||
|
||||
free(ts->loop);
|
||||
free(ts);
|
||||
}
|
||||
|
||||
static void signal_cb(uv_signal_t *watcher, int signum) {
|
||||
char sig_name[20];
|
||||
|
||||
switch (watcher->signum) {
|
||||
case SIGINT:
|
||||
case SIGTERM:
|
||||
get_sig_name(watcher->signum, sig_name, sizeof(sig_name));
|
||||
lwsl_notice("received signal: %s (%d), exiting...\n", sig_name, watcher->signum);
|
||||
break;
|
||||
default:
|
||||
signal(SIGABRT, SIG_DFL);
|
||||
abort();
|
||||
}
|
||||
|
||||
if (force_exit) exit(EXIT_FAILURE);
|
||||
force_exit = true;
|
||||
|
||||
lws_cancel_service(context);
|
||||
uv_stop(server->loop);
|
||||
|
||||
lwsl_notice("send ^C to force exit.\n");
|
||||
}
|
||||
|
||||
static int parse_int(char *name, char *str) {
|
||||
char *endptr;
|
||||
errno = 0;
|
||||
long val = strtol(str, &endptr, 0);
|
||||
if (errno != 0 || endptr == str) {
|
||||
fprintf(stderr, "ttyd: invalid value for %s: %s\n", name, str);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
return (int)val;
|
||||
}
|
||||
|
||||
static int calc_command_start(int argc, char **argv) {
|
||||
// make a copy of argc and argv
|
||||
int argc_copy = argc;
|
||||
char **argv_copy = xmalloc(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++) {
|
||||
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) {
|
||||
print_help();
|
||||
return 0;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
if (!conpty_init()) {
|
||||
fprintf(stderr, "ERROR: ConPTY init failed! Make sure you are on Windows 10 1809 or later.");
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
int start = calc_command_start(argc, argv);
|
||||
server = 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.gid = -1;
|
||||
info.uid = -1;
|
||||
info.max_http_header_pool = 16;
|
||||
info.options = LWS_SERVER_OPTION_LIBUV | LWS_SERVER_OPTION_VALIDATE_UTF8 | LWS_SERVER_OPTION_DISABLE_IPV6;
|
||||
#ifndef LWS_WITHOUT_EXTENSIONS
|
||||
info.extensions = extensions;
|
||||
#endif
|
||||
info.max_http_header_data = 65535;
|
||||
|
||||
int debug_level = LLL_ERR | LLL_WARN | LLL_NOTICE;
|
||||
char iface[128] = "";
|
||||
char socket_owner[128] = "";
|
||||
bool browser = false;
|
||||
bool ssl = false;
|
||||
char cert_path[1024] = "";
|
||||
char key_path[1024] = "";
|
||||
char ca_path[1024] = "";
|
||||
|
||||
struct json_object *client_prefs = json_object_new_object();
|
||||
|
||||
#ifdef _WIN32
|
||||
json_object_object_add(client_prefs, "isWindows", json_object_new_boolean(true));
|
||||
#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 'v':
|
||||
printf("ttyd version %s\n", TTYD_VERSION);
|
||||
return 0;
|
||||
case 'd':
|
||||
debug_level = parse_int("debug", optarg);
|
||||
break;
|
||||
case 'a':
|
||||
server->url_arg = true;
|
||||
break;
|
||||
case 'W':
|
||||
server->writable = true;
|
||||
break;
|
||||
case 'O':
|
||||
server->check_origin = true;
|
||||
break;
|
||||
case 'm':
|
||||
server->max_clients = parse_int("max-clients", optarg);
|
||||
break;
|
||||
case 'o':
|
||||
server->once = true;
|
||||
break;
|
||||
case 'q':
|
||||
server->exit_no_conn = true;
|
||||
break;
|
||||
case 'B':
|
||||
browser = true;
|
||||
break;
|
||||
case 'p':
|
||||
info.port = parse_int("port", optarg);
|
||||
if (info.port < 0) {
|
||||
fprintf(stderr, "ttyd: invalid port: %s\n", optarg);
|
||||
return -1;
|
||||
}
|
||||
break;
|
||||
case 'i':
|
||||
strncpy(iface, optarg, sizeof(iface) - 1);
|
||||
iface[sizeof(iface) - 1] = '\0';
|
||||
break;
|
||||
case 'U':
|
||||
strncpy(socket_owner, optarg, sizeof(socket_owner) - 1);
|
||||
socket_owner[sizeof(socket_owner) - 1] = '\0';
|
||||
break;
|
||||
case 'c':
|
||||
if (strchr(optarg, ':') == NULL) {
|
||||
fprintf(stderr, "ttyd: invalid credential, format: username:password\n");
|
||||
return -1;
|
||||
}
|
||||
char b64_text[256];
|
||||
lws_b64_encode_string(optarg, strlen(optarg), b64_text, sizeof(b64_text));
|
||||
server->credential = strdup(b64_text);
|
||||
break;
|
||||
case 'H':
|
||||
server->auth_header = strdup(optarg);
|
||||
break;
|
||||
case 'u':
|
||||
info.uid = parse_int("uid", optarg);
|
||||
break;
|
||||
case 'g':
|
||||
info.gid = parse_int("gid", optarg);
|
||||
break;
|
||||
case 's': {
|
||||
int sig = get_sig(optarg);
|
||||
if (sig > 0) {
|
||||
server->sig_code = sig;
|
||||
get_sig_name(sig, server->sig_name, sizeof(server->sig_name));
|
||||
} else {
|
||||
fprintf(stderr, "ttyd: invalid signal: %s\n", optarg);
|
||||
return -1;
|
||||
}
|
||||
} break;
|
||||
case 'w':
|
||||
server->cwd = strdup(optarg);
|
||||
break;
|
||||
case 'I':
|
||||
if (!strncmp(optarg, "~/", 2)) {
|
||||
const char *home = getenv("HOME");
|
||||
server->index = malloc(strlen(home) + strlen(optarg) - 1);
|
||||
sprintf(server->index, "%s%s", home, optarg + 1);
|
||||
} else {
|
||||
server->index = strdup(optarg);
|
||||
}
|
||||
struct stat st;
|
||||
if (stat(server->index, &st) == -1) {
|
||||
fprintf(stderr, "Can not stat index.html: %s, error: %s\n", server->index, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
fprintf(stderr, "Invalid index.html path: %s, is it a dir?\n", server->index);
|
||||
return -1;
|
||||
}
|
||||
break;
|
||||
case 'b': {
|
||||
char path[128];
|
||||
strncpy(path, optarg, 128);
|
||||
size_t len = strlen(path);
|
||||
while (len && path[len - 1] == '/') path[--len] = 0; // trim trailing /
|
||||
if (!len) break;
|
||||
#define sc(f) \
|
||||
strncpy(path + len, endpoints.f, 128 - len); \
|
||||
endpoints.f = strdup(path);
|
||||
sc(ws) sc(index) sc(token) sc(parent)
|
||||
#undef sc
|
||||
} break;
|
||||
#if LWS_LIBRARY_VERSION_NUMBER >= 4000000
|
||||
case 'P': {
|
||||
int interval = parse_int("ping-interval", optarg);
|
||||
if (interval < 0) {
|
||||
fprintf(stderr, "ttyd: invalid ping interval: %s\n", optarg);
|
||||
return -1;
|
||||
}
|
||||
retry.secs_since_valid_ping = interval;
|
||||
retry.secs_since_valid_hangup = interval + 7;
|
||||
} break;
|
||||
#endif
|
||||
case 'f': {
|
||||
int serv_buf_size = parse_int("srv-buf-size", optarg);
|
||||
if (serv_buf_size < 0) {
|
||||
fprintf(stderr, "ttyd: invalid srv-buf-size: %s\n", optarg);
|
||||
return -1;
|
||||
}
|
||||
info.pt_serv_buf_size = serv_buf_size;
|
||||
} break;
|
||||
case '6':
|
||||
info.options &= ~(LWS_SERVER_OPTION_DISABLE_IPV6);
|
||||
break;
|
||||
#if defined(LWS_OPENSSL_SUPPORT) || defined(LWS_WITH_TLS)
|
||||
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 'T':
|
||||
strncpy(server->terminal_type, optarg, sizeof(server->terminal_type) - 1);
|
||||
server->terminal_type[sizeof(server->terminal_type) - 1] = '\0';
|
||||
break;
|
||||
case '?':
|
||||
break;
|
||||
case 't':
|
||||
optind--;
|
||||
for (; optind < start && *argv[optind] != '-'; optind++) {
|
||||
char *option = 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, "=");
|
||||
if (value == NULL) {
|
||||
fprintf(stderr, "ttyd: invalid client option: %s, format: key=value\n", optarg);
|
||||
return -1;
|
||||
}
|
||||
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);
|
||||
|
||||
char server_hdr[128] = "";
|
||||
sprintf(server_hdr, "ttyd/%s (libwebsockets/%s)", TTYD_VERSION, LWS_LIBRARY_VERSION);
|
||||
info.server_string = server_hdr;
|
||||
|
||||
#if LWS_LIBRARY_VERSION_NUMBER < 4000000
|
||||
info.ws_ping_pong_interval = 5;
|
||||
#else
|
||||
info.retry_and_idle_policy = &retry;
|
||||
#endif
|
||||
|
||||
if (strlen(iface) > 0) {
|
||||
info.iface = iface;
|
||||
if (endswith(info.iface, ".sock") || endswith(info.iface, ".socket")) {
|
||||
#if defined(LWS_USE_UNIX_SOCK) || defined(LWS_WITH_UNIX_SOCK)
|
||||
info.options |= LWS_SERVER_OPTION_UNIX_SOCK;
|
||||
info.port = 0; // warmcat/libwebsockets#1985
|
||||
strncpy(server->socket_path, info.iface, sizeof(server->socket_path) - 1);
|
||||
if (strlen(socket_owner) > 0) {
|
||||
info.unix_socket_perms = socket_owner;
|
||||
}
|
||||
#else
|
||||
fprintf(stderr, "libwebsockets is not compiled with UNIX domain socket support");
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(LWS_OPENSSL_SUPPORT) || defined(LWS_WITH_TLS)
|
||||
if (ssl) {
|
||||
info.ssl_cert_filepath = cert_path;
|
||||
info.ssl_private_key_filepath = key_path;
|
||||
#ifndef LWS_WITH_MBEDTLS
|
||||
info.ssl_options_set = SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1;
|
||||
#endif
|
||||
if (strlen(ca_path) > 0) {
|
||||
info.ssl_ca_filepath = ca_path;
|
||||
info.options |= LWS_SERVER_OPTION_REQUIRE_VALID_OPENSSL_CLIENT_CERT;
|
||||
}
|
||||
info.options |= LWS_SERVER_OPTION_ALLOW_NON_SSL_ON_SSL_PORT | LWS_SERVER_OPTION_REDIRECT_HTTP_TO_HTTPS;
|
||||
}
|
||||
#endif
|
||||
|
||||
lwsl_notice("ttyd %s (libwebsockets %s)\n", TTYD_VERSION, LWS_LIBRARY_VERSION);
|
||||
print_config();
|
||||
|
||||
// lws custom header requires lower case name, and terminating :
|
||||
if (server->auth_header != NULL) {
|
||||
size_t auth_header_len = strlen(server->auth_header);
|
||||
server->auth_header = xrealloc(server->auth_header, auth_header_len + 2);
|
||||
strcat(server->auth_header + auth_header_len, ":");
|
||||
lowercase(server->auth_header);
|
||||
}
|
||||
|
||||
void *foreign_loops[1];
|
||||
foreign_loops[0] = server->loop;
|
||||
info.foreign_loops = foreign_loops;
|
||||
info.options |= LWS_SERVER_OPTION_EXPLICIT_VHOSTS;
|
||||
|
||||
context = lws_create_context(&info);
|
||||
if (context == NULL) {
|
||||
lwsl_err("libwebsockets context creation failed\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
struct lws_vhost *vhost = lws_create_vhost(context, &info);
|
||||
if (vhost == NULL) {
|
||||
lwsl_err("libwebsockets vhost creation failed\n");
|
||||
return 1;
|
||||
}
|
||||
int port = lws_get_vhost_listen_port(vhost);
|
||||
lwsl_notice(" Listening on port: %d\n", port);
|
||||
|
||||
if (browser) {
|
||||
char url[30];
|
||||
sprintf(url, "%s://localhost:%d", ssl ? "https" : "http", port);
|
||||
open_uri(url);
|
||||
}
|
||||
|
||||
#define sig_count 2
|
||||
int sig_nums[] = {SIGINT, SIGTERM};
|
||||
uv_signal_t signals[sig_count];
|
||||
for (int i = 0; i < sig_count; i++) {
|
||||
uv_signal_init(server->loop, &signals[i]);
|
||||
uv_signal_start(&signals[i], signal_cb, sig_nums[i]);
|
||||
}
|
||||
|
||||
lws_service(context, 0);
|
||||
|
||||
for (int i = 0; i < sig_count; i++) {
|
||||
uv_signal_stop(&signals[i]);
|
||||
}
|
||||
#undef sig_count
|
||||
|
||||
lws_context_destroy(context);
|
||||
|
||||
// cleanup
|
||||
server_free(server);
|
||||
|
||||
return 0;
|
||||
}
|
||||
86
src/server.h
Normal file
86
src/server.h
Normal file
@@ -0,0 +1,86 @@
|
||||
#include <libwebsockets.h>
|
||||
#include <stdbool.h>
|
||||
#include <uv.h>
|
||||
|
||||
#include "pty.h"
|
||||
|
||||
// client message
|
||||
#define INPUT '0'
|
||||
#define RESIZE_TERMINAL '1'
|
||||
#define PAUSE '2'
|
||||
#define RESUME '3'
|
||||
#define JSON_DATA '{'
|
||||
|
||||
// server message
|
||||
#define OUTPUT '0'
|
||||
#define SET_WINDOW_TITLE '1'
|
||||
#define SET_PREFERENCES '2'
|
||||
|
||||
// url paths
|
||||
struct endpoints {
|
||||
char *ws;
|
||||
char *index;
|
||||
char *token;
|
||||
char *parent;
|
||||
};
|
||||
|
||||
extern volatile bool force_exit;
|
||||
extern struct lws_context *context;
|
||||
extern struct server *server;
|
||||
extern struct endpoints endpoints;
|
||||
|
||||
struct pss_http {
|
||||
char path[128];
|
||||
char *buffer;
|
||||
char *ptr;
|
||||
size_t len;
|
||||
};
|
||||
|
||||
struct pss_tty {
|
||||
bool initialized;
|
||||
int initial_cmd_index;
|
||||
bool authenticated;
|
||||
char user[30];
|
||||
char address[50];
|
||||
char path[128];
|
||||
char **args;
|
||||
int argc;
|
||||
|
||||
struct lws *wsi;
|
||||
char *buffer;
|
||||
size_t len;
|
||||
|
||||
pty_process *process;
|
||||
pty_buf_t *pty_buf;
|
||||
|
||||
int lws_close_status;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
struct pss_tty *pss;
|
||||
bool ws_closed;
|
||||
} pty_ctx_t;
|
||||
|
||||
struct server {
|
||||
int client_count; // client count
|
||||
char *prefs_json; // client preferences
|
||||
char *credential; // encoded basic auth credential
|
||||
char *auth_header; // header name used for auth proxy
|
||||
char *index; // custom index.html
|
||||
char *command; // full command line
|
||||
char **argv; // command with arguments
|
||||
int argc; // command + arguments count
|
||||
char *cwd; // working directory
|
||||
int sig_code; // close signal
|
||||
char sig_name[20]; // human readable signal string
|
||||
bool url_arg; // allow client to send cli arguments in URL
|
||||
bool writable; // whether clients to write to the TTY
|
||||
bool check_origin; // whether allow websocket connection from different origin
|
||||
int max_clients; // maximum clients to support
|
||||
bool once; // whether accept only one client and exit on disconnection
|
||||
bool exit_no_conn; // whether exit on all clients disconnection
|
||||
char socket_path[255]; // UNIX domain socket path
|
||||
char terminal_type[30]; // terminal type to report
|
||||
|
||||
uv_loop_t *loop; // the libuv event loop
|
||||
};
|
||||
163
src/utils.c
Normal file
163
src/utils.c
Normal file
@@ -0,0 +1,163 @@
|
||||
#include <ctype.h>
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#if defined(__linux__) && !defined(__ANDROID__)
|
||||
const char *sys_signame[NSIG] = {
|
||||
"zero", "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "UNUSED", "FPE", "KILL", "USR1",
|
||||
"SEGV", "USR2", "PIPE", "ALRM", "TERM", "STKFLT", "CHLD", "CONT", "STOP", "TSTP", "TTIN",
|
||||
"TTOU", "URG", "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "IO", "PWR", "SYS", NULL};
|
||||
#endif
|
||||
|
||||
#if defined(_WIN32) || defined(__CYGWIN__)
|
||||
#include <windows.h>
|
||||
#undef NSIG
|
||||
#define NSIG 33
|
||||
const char *sys_signame[NSIG] = {
|
||||
"zero", "HUP", "INT", "QUIT", "ILL", "TRAP", "IOT", "EMT", "FPE", "KILL", "BUS",
|
||||
"SEGV", "SYS", "PIPE", "ALRM", "TERM", "URG", "STOP", "TSTP", "CONT", "CHLD", "TTIN",
|
||||
"TTOU", "IO", "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "PWR", "USR1", "USR2", NULL};
|
||||
#endif
|
||||
|
||||
void *xmalloc(size_t size) {
|
||||
if (size == 0) return NULL;
|
||||
void *p = malloc(size);
|
||||
if (!p) abort();
|
||||
return p;
|
||||
}
|
||||
|
||||
void *xrealloc(void *p, size_t size) {
|
||||
if ((size == 0) && (p == NULL)) return NULL;
|
||||
p = realloc(p, size);
|
||||
if (!p) abort();
|
||||
return p;
|
||||
}
|
||||
|
||||
char *uppercase(char *s) {
|
||||
while(*s) {
|
||||
*s = (char)toupper((int)*s);
|
||||
s++;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
char *lowercase(char *s) {
|
||||
while(*s) {
|
||||
*s = (char)tolower((int)*s);
|
||||
s++;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
bool endswith(const char *str, const char *suffix) {
|
||||
size_t str_len = strlen(str);
|
||||
size_t suffix_len = strlen(suffix);
|
||||
return str_len > suffix_len && !strcmp(str + (str_len - suffix_len), suffix);
|
||||
}
|
||||
|
||||
int get_sig_name(int sig, char *buf, size_t len) {
|
||||
int n = snprintf(buf, len, "SIG%s", sig < NSIG ? sys_signame[sig] : "unknown");
|
||||
uppercase(buf);
|
||||
return n;
|
||||
}
|
||||
|
||||
int get_sig(const char *sig_name) {
|
||||
for (int sig = 1; sig < NSIG; sig++) {
|
||||
const char *name = sys_signame[sig];
|
||||
if (name != NULL && (strcasecmp(name, sig_name) == 0 || strcasecmp(name, sig_name + 3) == 0))
|
||||
return sig;
|
||||
}
|
||||
return atoi(sig_name);
|
||||
}
|
||||
|
||||
int open_uri(char *uri) {
|
||||
#ifdef __APPLE__
|
||||
char command[256];
|
||||
sprintf(command, "open %s > /dev/null 2>&1", uri);
|
||||
return system(command);
|
||||
#elif defined(_WIN32) || defined(__CYGWIN__)
|
||||
return ShellExecute(0, 0, uri, 0, 0, SW_SHOW) > (HINSTANCE)32 ? 0 : 1;
|
||||
#else
|
||||
// check if X server is running
|
||||
if (system("xset -q > /dev/null 2>&1")) return 1;
|
||||
char command[256];
|
||||
sprintf(command, "xdg-open %s > /dev/null 2>&1", uri);
|
||||
return system(command);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
char *strsep(char **sp, char *sep) {
|
||||
char *p, *s;
|
||||
if (sp == NULL || *sp == NULL || **sp == '\0') return (NULL);
|
||||
s = *sp;
|
||||
p = s + strcspn(s, sep);
|
||||
if (*p != '\0') *p++ = '\0';
|
||||
*sp = p;
|
||||
return s;
|
||||
}
|
||||
|
||||
const char *quote_arg(const char *arg) {
|
||||
int len = 0, n = 0;
|
||||
int force_quotes = 0;
|
||||
char *q, *d;
|
||||
const char *p = arg;
|
||||
if (!*p) force_quotes = 1;
|
||||
while (*p) {
|
||||
if (isspace(*p) || *p == '*' || *p == '?' || *p == '{' || *p == '\'')
|
||||
force_quotes = 1;
|
||||
else if (*p == '"')
|
||||
n++;
|
||||
else if (*p == '\\') {
|
||||
int count = 0;
|
||||
while (*p == '\\') {
|
||||
count++;
|
||||
p++;
|
||||
len++;
|
||||
}
|
||||
if (*p == '"' || !*p) n += count * 2 + 1;
|
||||
continue;
|
||||
}
|
||||
len++;
|
||||
p++;
|
||||
}
|
||||
if (!force_quotes && n == 0) return arg;
|
||||
|
||||
d = q = xmalloc(len + n + 3);
|
||||
*d++ = '"';
|
||||
while (*arg) {
|
||||
if (*arg == '"')
|
||||
*d++ = '\\';
|
||||
else if (*arg == '\\') {
|
||||
int count = 0;
|
||||
while (*arg == '\\') {
|
||||
count++;
|
||||
*d++ = *arg++;
|
||||
}
|
||||
if (*arg == '"' || !*arg) {
|
||||
while (count-- > 0) *d++ = '\\';
|
||||
if (!*arg) break;
|
||||
*d++ = '\\';
|
||||
}
|
||||
}
|
||||
*d++ = *arg++;
|
||||
}
|
||||
*d++ = '"';
|
||||
*d++ = '\0';
|
||||
return q;
|
||||
}
|
||||
|
||||
void print_error(char *func) {
|
||||
LPVOID buffer;
|
||||
DWORD dw = GetLastError();
|
||||
FormatMessage(
|
||||
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
NULL, dw, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&buffer, 0, NULL);
|
||||
wprintf(L"== %s failed with error %d: %s", func, dw, buffer);
|
||||
LocalFree(buffer);
|
||||
}
|
||||
#endif
|
||||
39
src/utils.h
Normal file
39
src/utils.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#ifndef TTYD_UTIL_H
|
||||
#define TTYD_UTIL_H
|
||||
|
||||
#define container_of(ptr, type, member) \
|
||||
({ \
|
||||
const typeof(((type *)0)->member) *__mptr = (ptr); \
|
||||
(type *)((char *)__mptr - offsetof(type, member)); \
|
||||
})
|
||||
|
||||
// malloc with NULL check
|
||||
void *xmalloc(size_t size);
|
||||
|
||||
// realloc with NULL check
|
||||
void *xrealloc(void *p, size_t size);
|
||||
|
||||
// Convert a string to upper case
|
||||
char *uppercase(char *s);
|
||||
|
||||
// Convert a string to lower case
|
||||
char *lowercase(char *s);
|
||||
|
||||
// Check whether str ends with suffix
|
||||
bool endswith(const char *str, const char *suffix);
|
||||
|
||||
// Get human readable signal string
|
||||
int get_sig_name(int sig, char *buf, size_t len);
|
||||
|
||||
// Get signal code from string like SIGHUP
|
||||
int get_sig(const char *sig_name);
|
||||
|
||||
// Open uri with the default application of system
|
||||
int open_uri(char *uri);
|
||||
|
||||
#ifdef _WIN32
|
||||
char *strsep(char **sp, char *sep);
|
||||
const char *quote_arg(const char *arg);
|
||||
void print_error(char *func);
|
||||
#endif
|
||||
#endif // TTYD_UTIL_H
|
||||
Reference in New Issue
Block a user