udphole

Basic UDP wormhole proxy
git clone git://git.finwo.net/app/udphole
Log | Files | Refs | README | LICENSE

commit 51a13a397904c238e6687f5247013047d89c6fc4
parent dbb36807a4481ac1e804b81b88e737e032e9d590
Author: Robin Bron <robin.bron@yourhosting.nl>
Date:   Fri,  6 Mar 2026 21:39:39 +0100

Fix handling incomplete input

Diffstat:
MMakefile | 2+-
Msrc/common/resp.c | 361++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Msrc/common/resp.h | 7+++++--
Msrc/interface/api/server.c | 41++++++++++++++++++++++++++++++++++++-----
4 files changed, 289 insertions(+), 122 deletions(-)

diff --git a/Makefile b/Makefile @@ -7,7 +7,7 @@ SRC:= # UNAME_SYSTEM=$(call lc,$(shell uname -s)) BIN?=udphole -VERSION?=1.3.8 +VERSION?=1.3.9 CC:=gcc CPP:=g++ diff --git a/src/common/resp.c b/src/common/resp.c @@ -13,155 +13,288 @@ static void resp_free_internal(resp_object *o); -static int resp_read_byte_from_buf(const char **buf, size_t *len) { - if (*len < 1) return -1; - unsigned char c = (unsigned char)(*buf)[0]; - *buf += 1; - *len -= 1; - return (int)c; -} +int resp_read_buf(const char *buf, size_t len, resp_object **out_obj) { + log_trace("resp_read_buf: START buf=%p len=%zu out_obj=%p", (void*)buf, len, (void*)out_obj); + if (!out_obj) return -1; -static int resp_read_line_from_buf(const char **buf, size_t *len, char *out, size_t out_size) { - size_t i = 0; - int prev = -1; - while (i + 1 < out_size) { - if (*len < 1) return -1; - int b = (int)(unsigned char)(*buf)[0]; - *buf += 1; - *len -= 1; - if (prev == '\r' && b == '\n') { - out[i - 1] = '\0'; - return 0; + const char *start = buf; + const char *p = buf; + size_t remaining = len; + + // We need at least 1 byte + if (len <= 0) return -1; + + // Ensure we have memory to place data in + resp_object *output = *out_obj; + if (!output) { + *out_obj = output = calloc(1, sizeof(resp_object)); + if (!output) return -1; + } + + // Skip empty lines (only \r\n) + if (p[0] == '\r' || p[0] == '\n') { + while (remaining > 0 && (p[0] == '\r' || p[0] == '\n')) { + p++; + remaining--; } - prev = b; - out[i++] = (char)b; + if (remaining == 0) return 0; // only whitespace, need more data } - return -1; -} -resp_object *resp_read_buf(const char *buf, size_t len) { - const char *p = buf; - size_t remaining = len; + // Consume first character for object type detection + int type_c = p[0]; + remaining--; + p++; + + log_trace("resp_read_buf: type_c='%c' (0x%02x)", type_c >= 32 ? type_c : '.', type_c); + + // And act accordingly + switch((char)type_c) { - int type_c = resp_read_byte_from_buf(&p, &remaining); - if (type_c < 0) return NULL; - if (type_c == -2) return NULL; - resp_object *o = calloc(1, sizeof(resp_object)); - if (!o) return NULL; - char line[LINE_BUF]; - switch ((char)type_c) { case '+': - o->type = RESPT_SIMPLE; - if (resp_read_line_from_buf(&p, &remaining, line, sizeof(line)) != 0) { - free(o); - return NULL; + log_trace("resp_read_buf: case '+' SIMPLE"); + output->type = output->type ? output->type : RESPT_SIMPLE; + if (output->type != RESPT_SIMPLE) { + return -2; // Mismatching types + } + // Read until \r\n, don't include \r\n in string + { + size_t i = 0; + char line[LINE_BUF]; + int found_crlf = 0; + while (i + 1 < LINE_BUF && remaining > 0) { + if (remaining >= 2 && p[0] == '\r' && p[1] == '\n') { + p += 2; + remaining -= 2; + found_crlf = 1; + break; + } + line[i++] = p[0]; + p++; + remaining--; + } + if (!found_crlf) { + log_trace("resp_read_buf: SIMPLE incomplete, returning -1"); + return -1; // Incomplete, need more data + } + line[i] = '\0'; + if (output->u.s) free(output->u.s); + output->u.s = strdup(line); + log_trace("resp_read_buf: SIMPLE value='%s'", line); } - o->u.s = strdup(line); break; + case '-': - o->type = RESPT_ERROR; - if (resp_read_line_from_buf(&p, &remaining, line, sizeof(line)) != 0) { - free(o); - return NULL; + log_trace("resp_read_buf: case '-' ERROR"); + output->type = output->type ? output->type : RESPT_ERROR; + if (output->type != RESPT_ERROR) { + return -2; // Mismatching types + } + // Read until \r\n, don't include \r\n in string + { + size_t i = 0; + char line[LINE_BUF]; + int found_crlf = 0; + while (i + 1 < LINE_BUF && remaining > 0) { + if (remaining >= 2 && p[0] == '\r' && p[1] == '\n') { + p += 2; + remaining -= 2; + found_crlf = 1; + break; + } + line[i++] = p[0]; + p++; + remaining--; + } + if (!found_crlf) { + log_trace("resp_read_buf: ERROR incomplete, returning -1"); + return -1; // Incomplete, need more data + } + line[i] = '\0'; + if (output->u.s) free(output->u.s); + output->u.s = strdup(line); + log_trace("resp_read_buf: ERROR value='%s'", line); } - o->u.s = strdup(line); break; + case ':': + log_trace("resp_read_buf: case ':' INT"); + output->type = output->type ? output->type : RESPT_INT; + if (output->type != RESPT_INT) { + return -2; // Mismatching types + } + // Read until \r\n, don't include \r\n in string + // value = strtoll(line); { - if (resp_read_line_from_buf(&p, &remaining, line, sizeof(line)) != 0) { - free(o); - return NULL; + size_t i = 0; + char line[LINE_BUF]; + int found_crlf = 0; + while (i + 1 < LINE_BUF && remaining > 0) { + if (remaining >= 2 && p[0] == '\r' && p[1] == '\n') { + p += 2; + remaining -= 2; + found_crlf = 1; + break; + } + line[i++] = p[0]; + p++; + remaining--; } - o->type = RESPT_INT; - o->u.i = (long long)strtoll(line, NULL, 10); - break; + if (!found_crlf) { + log_trace("resp_read_buf: INT incomplete, returning -1"); + return -1; // Incomplete, need more data + } + line[i] = '\0'; + output->u.i = strtoll(line, NULL, 10); + log_trace("resp_read_buf: INT value=%lld", output->u.i); } + break; + case '$': + log_trace("resp_read_buf: case '$' BULK"); + output->type = output->type ? output->type : RESPT_BULK; + if (output->type != RESPT_BULK) { + return -2; // Mismatching types + } + // Read until \r\n, don't include \r\n in string + // data_length = strtoll(line); { - if (resp_read_line_from_buf(&p, &remaining, line, sizeof(line)) != 0) { - free(o); - return NULL; + size_t i = 0; + char line[LINE_BUF]; + int found_crlf = 0; + while (i + 1 < LINE_BUF && remaining > 0) { + if (remaining >= 2 && p[0] == '\r' && p[1] == '\n') { + p += 2; + remaining -= 2; + found_crlf = 1; + break; + } + line[i++] = p[0]; + p++; + remaining--; } - long blen = strtol(line, NULL, 10); - if (blen < 0 || blen > (long)MAX_BULK_LEN) { - free(o); - return NULL; + if (!found_crlf) { + log_trace("resp_read_buf: BULK length incomplete, returning -1"); + return -1; // Incomplete, need more data } - o->type = RESPT_BULK; - if (blen == 0) { - o->u.s = strdup(""); - if (resp_read_line_from_buf(&p, &remaining, line, sizeof(line)) != 0) { - free(o->u.s); - free(o); - return NULL; + line[i] = '\0'; + long data_length = strtol(line, NULL, 10); + log_trace("resp_read_buf: BULK data_length=%ld", data_length); + + if (data_length < 0) { + output->u.s = NULL; + } else if (data_length == 0) { + // Null bulk string or empty string - need \r\n + + + if (remaining >= 2 && p[0] == '\r' && p[1] == '\n') { + p += 2; + remaining -= 2; + } else { + log_trace("resp_read_buf: BULK zero incomplete, returning -1"); + return -1; // Incomplete, need more data } + if (output->u.s) free(output->u.s); + output->u.s = strdup(""); } else { - if ((size_t)blen > remaining) { - free(o); - return NULL; + // Read data_length bytes + if ((size_t)data_length > remaining) { + log_trace("resp_read_buf: BULK not enough data, returning -1"); + return -1; // not enough data } - o->u.s = malloc((size_t)blen + 1); - if (!o->u.s) { - free(o); - return NULL; - } - memcpy(o->u.s, p, (size_t)blen); - p += blen; - remaining -= (size_t)blen; - o->u.s[blen] = '\0'; - if (remaining < 2) { - free(o->u.s); - free(o); - return NULL; + if (output->u.s) free(output->u.s); + output->u.s = malloc((size_t)data_length + 1); + if (!output->u.s) return -1; + memcpy(output->u.s, p, (size_t)data_length); + output->u.s[data_length] = '\0'; + p += data_length; + remaining -= data_length; + // Skip \r\n + if (remaining >= 2 && p[0] == '\r' && p[1] == '\n') { + p += 2; + remaining -= 2; + } else { + free(output->u.s); + output->u.s = NULL; + log_trace("resp_read_buf: BULK data incomplete, returning -1"); + return -1; // Incomplete, need more data } - if (p[0] != '\r' || p[1] != '\n') { - free(o->u.s); - free(o); - return NULL; - } - p += 2; - remaining -= 2; } - break; } + break; + case '*': + log_trace("resp_read_buf: case '*' ARRAY"); + output->type = output->type ? output->type : RESPT_ARRAY; + if (output->type != RESPT_ARRAY) { + return -2; // Mismatching types + } + // Read until \r\n, don't include \r\n in string + // items = strtoll(line); { - if (resp_read_line_from_buf(&p, &remaining, line, sizeof(line)) != 0) { - free(o); - return NULL; + size_t i = 0; + char line[LINE_BUF]; + int found_crlf = 0; + while (i + 1 < LINE_BUF && remaining > 0) { + if (remaining >= 2 && p[0] == '\r' && p[1] == '\n') { + p += 2; + remaining -= 2; + found_crlf = 1; + break; + } + line[i++] = p[0]; + p++; + remaining--; } - long n = strtol(line, NULL, 10); - if (n < 0 || n > 65536) { - free(o); - return NULL; + if (!found_crlf) { + log_trace("resp_read_buf: ARRAY count incomplete, returning -1"); + return -1; // Incomplete, need more data } - o->type = RESPT_ARRAY; - o->u.arr.n = (size_t)n; - o->u.arr.elem = n ? calloc((size_t)n, sizeof(resp_object)) : NULL; - if (n && !o->u.arr.elem) { - free(o); - return NULL; + line[i] = '\0'; + long items = strtol(line, NULL, 10); + log_trace("resp_read_buf: ARRAY items=%ld", items); + + if (items < 0 || items > 65536) { + log_trace("resp_read_buf: ARRAY items invalid, returning -1"); + return -1; } - for (size_t i = 0; i < (size_t)n; i++) { - resp_object *sub = resp_read_buf(p, remaining); - if (!sub) { - for (size_t j = 0; j < i; j++) resp_free_internal(&o->u.arr.elem[j]); - free(o->u.arr.elem); - free(o); - return NULL; + + // Initialize array if needed + if (!output->u.arr.elem) { + output->u.arr.n = 0; + output->u.arr.elem = NULL; + } + + for (size_t j = 0; j < (size_t)items; j++) { + if (remaining == 0) { + log_trace("resp_read_buf: ARRAY elem[%zu] no remaining, returning -1", j); + return -1; } - p += remaining - (sub ? remaining : 0); - remaining = 0; - o->u.arr.elem[i] = *sub; - free(sub); + resp_object *element = NULL; + log_trace("resp_read_buf: ARRAY calling recursive for elem[%zu]", j); + int element_consumed = resp_read_buf(p, remaining, &element); + log_trace("resp_read_buf: ARRAY elem[%zu] consumed=%d element=%p", j, element_consumed, (void*)element); + if (element_consumed <= 0) { + log_trace("resp_read_buf: ARRAY elem[%zu] failed, returning -1", j); + return -1; + } + if (resp_array_append_obj(output, element) != 0) { + resp_free(element); + log_trace("resp_read_buf: ARRAY append failed, returning -1"); + return -1; + } + p += element_consumed; + remaining -= element_consumed; } - break; } + break; + default: - free(o); - return NULL; + log_trace("resp_read_buf: default case, returning -1"); + return -1; } - return o; + + log_trace("resp_read_buf: returning %d", (int)(p - start)); + return (int)(p - start); } static int resp_read_byte(int fd) { diff --git a/src/common/resp.h b/src/common/resp.h @@ -40,8 +40,11 @@ void resp_map_set(resp_object *map, const char *key, resp_object *value); resp_object *resp_read(int fd); /* Returns new object: caller owns the result, must call resp_free() */ -resp_object *resp_read_buf(const char *buf, size_t len); -/* Returns new object: caller owns the result, must call resp_free() */ +int resp_read_buf(const char *buf, size_t len, resp_object **out_obj); +/* Returns 0=no data yet, <0=incomplete (need more data), >0=bytes consumed from buffer */ +/* If out_obj is NULL, returns -1 */ +/* If *out_obj is NULL, creates new object */ +/* If *out_obj exists, appends to it (for arrays) */ int resp_encode_array(int argc, const resp_object *const *argv, char **out_buf, size_t *out_len); /* Returns allocated string in out_buf: caller must free() the string */ diff --git a/src/interface/api/server.c b/src/interface/api/server.c @@ -603,31 +603,60 @@ int api_client_pt(int64_t timestamp, struct pt_task *task) { return SCHED_RUNNING; } - char buf[1]; - ssize_t n = recv(state->fd, buf, 1, MSG_PEEK); - if (n <= 0) { + // Read data into receive buffer + if (state->rlen >= READ_BUF_SIZE) { + // Buffer full, protocol error goto cleanup; } - resp_object *cmd = resp_read(state->fd); - if (!cmd) { + ssize_t n = recv(state->fd, state->rbuf + state->rlen, READ_BUF_SIZE - state->rlen, 0); + if (n < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { return SCHED_RUNNING; } goto cleanup; } + if (n == 0) { + goto cleanup; + } + state->rlen += (size_t)n; + log_trace("api: received %zd bytes, buffer len=%zu", n, state->rlen); + + // Try to parse RESP object from buffer + resp_object *cmd = NULL; + int consumed = resp_read_buf(state->rbuf, state->rlen, &cmd); + log_trace("api: resp_read_buf returned %d, cmd=%p", consumed, (void*)cmd); + if (consumed > 0) { + // Successfully parsed - consume bytes from buffer + memmove(state->rbuf, state->rbuf + consumed, state->rlen - consumed); + state->rlen -= consumed; + } else if (consumed < 0) { + // Incomplete - need more data, will retry on next call with same buffer + return SCHED_RUNNING; + } else { + // No data - shouldn't happen, but continue + return SCHED_RUNNING; + } + + if (!cmd) { + return SCHED_RUNNING; + } if (cmd->type != RESPT_ARRAY || cmd->u.arr.n == 0) { + log_trace("api: not an array or empty (type=%d, n=%zu), sending protocol error", cmd->type, cmd->u.arr.n); resp_free(cmd); api_write_err(state, "Protocol error"); client_flush(state); return SCHED_RUNNING; } + log_trace("api: array has %zu elements", cmd->u.arr.n); + char *args[MAX_ARGS]; int nargs = 0; for (size_t i = 0; i < cmd->u.arr.n && nargs < MAX_ARGS; i++) { resp_object *elem = &cmd->u.arr.elem[i]; + log_trace("api: elem[%zu] type=%d, s=%s", i, elem->type, elem->u.s ? elem->u.s : "(null)"); if (elem->type == RESPT_BULK && elem->u.s) { args[nargs++] = elem->u.s; elem->u.s = NULL; @@ -636,9 +665,11 @@ int api_client_pt(int64_t timestamp, struct pt_task *task) { } } + log_trace("api: dispatching command with %d args", nargs); if (nargs > 0) { dispatch_command(state, args, nargs); } + log_trace("api: command dispatched"); for (int j = 0; j < nargs; j++) { free(args[j]);