11 changed files with 1290 additions and 184 deletions
@ -0,0 +1,380 @@
|
||||
// HTTP headers/body codec: строгие границы сообщений и обработка hop-by-hop полей.
|
||||
#include "http_proxy.h" |
||||
#include "../../lib/debug_config.h" |
||||
#include "../../lib/strbuf.h" |
||||
#include "../../lib/platform_compat.h" |
||||
#include <string.h> |
||||
#include <stdio.h> |
||||
#include <limits.h> |
||||
|
||||
// Все протокольные ошибки имеют причину; значения credentials в лог не попадают.
|
||||
static int http_error(int status, const char* reason) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_PROXY, "HTTP codec: status=%d reason=%s", status, reason); |
||||
return -status; |
||||
} |
||||
|
||||
// ASCII comparison не зависит от locale.
|
||||
static int ascii_equal(const char* a, size_t len, const char* b) { |
||||
if (len != strlen(b)) return 0; |
||||
for (size_t i = 0; i < len; i++) { |
||||
unsigned char c = (unsigned char)a[i]; if (c >= 'A' && c <= 'Z') c += 'a' - 'A'; |
||||
unsigned char d = (unsigned char)b[i]; if (d >= 'A' && d <= 'Z') d += 'a' - 'A'; |
||||
if (c != d) return 0; |
||||
} |
||||
return 1; |
||||
} |
||||
|
||||
// RFC token, включая имена методов и полей.
|
||||
static int token_char(unsigned char c) { |
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || |
||||
(c && strchr("!#$%&'*+-.^_`|~", c)); |
||||
} |
||||
|
||||
// Проверяет token list и ищет элемент без учёта регистра.
|
||||
static int list_contains(const char* value, const char* name) { |
||||
int found = 0; |
||||
while (*value) { |
||||
while (*value == ' ' || *value == '\t' || *value == ',') value++; |
||||
if (!*value) break; |
||||
const char* start = value; |
||||
while (token_char((unsigned char)*value)) value++; |
||||
if (start == value) return -1; |
||||
if (ascii_equal(start, value - start, name)) found = 1; |
||||
while (*value == ' ' || *value == '\t') value++; |
||||
if (*value && *value != ',') return -1; |
||||
} |
||||
return found; |
||||
} |
||||
|
||||
// Upgrade содержит protocol-name[/protocol-version], с регистрозависимым сравнением.
|
||||
static int upgrade_contains(const char* value, const char* selected) { |
||||
int found = 0, count = 0; |
||||
while (*value) { |
||||
while (*value == ' ' || *value == '\t' || *value == ',') value++; |
||||
if (!*value) break; |
||||
const char* start = value; |
||||
while (token_char((unsigned char)*value)) value++; |
||||
if (value == start) return -1; |
||||
if (*value == '/') { |
||||
const char* version = ++value; |
||||
while (token_char((unsigned char)*value)) value++; |
||||
if (version == value) return -1; |
||||
} |
||||
if (selected && strlen(selected) == (size_t)(value - start) && !memcmp(start, selected, value - start)) found = 1; |
||||
count++; |
||||
while (*value == ' ' || *value == '\t') value++; |
||||
if (*value && *value != ',') return -1; |
||||
} |
||||
return count ? found : -1; |
||||
} |
||||
|
||||
// Без переполнения и принятия знака/суффикса.
|
||||
static int parse_number(const char* s, size_t len, unsigned base, uint64_t* result) { |
||||
uint64_t n = 0; |
||||
if (!len) return -1; |
||||
for (size_t i = 0; i < len; i++) { |
||||
unsigned char c = (unsigned char)s[i]; |
||||
unsigned d = c >= '0' && c <= '9' ? c - '0' : c >= 'a' && c <= 'f' ? c - 'a' + 10 : |
||||
c >= 'A' && c <= 'F' ? c - 'A' + 10 : base; |
||||
if (d >= base || n > (UINT64_MAX - d) / base) return -1; |
||||
n = n * base + d; |
||||
} |
||||
*result = n; return 0; |
||||
} |
||||
|
||||
// Накопление прекращается ровно после CRLFCRLF, независимо от размера recv.
|
||||
int http_proxy_headers_feed(struct http_proxy_message* m, const uint8_t* data, size_t len, size_t* used) { |
||||
*used = 0; |
||||
while (*used < len && !m->headers_done) { |
||||
if (m->header_len == HTTP_PROXY_HEADER_LIMIT) return http_error(431, "headers limit"); |
||||
unsigned char c = data[(*used)++]; |
||||
if (!c || c == 127 || (c < 32 && c != '\r' && c != '\n' && c != '\t')) return http_error(400, "header control byte"); |
||||
m->headers[m->header_len++] = (char)c; |
||||
size_t n = m->header_len; |
||||
if (c == '\n' && (n < 2 || m->headers[n-2] != '\r')) return http_error(400, "bare LF"); |
||||
if (n > 1 && m->headers[n-2] == '\r' && c != '\n') return http_error(400, "bare CR"); |
||||
if (!m->first_line_len) { |
||||
if (n > HTTP_PROXY_LINE_LIMIT + 2) return http_error(414, "start line limit"); |
||||
if (c == '\n') m->first_line_len = n; |
||||
} |
||||
if (n >= 4 && memcmp(m->headers + n - 4, "\r\n\r\n", 4) == 0) m->headers_done = 1; |
||||
} |
||||
m->headers[m->header_len] = 0; |
||||
return m->headers_done; |
||||
} |
||||
|
||||
// Список полей хранит ссылки на собственный проверенный буфер.
|
||||
static int parse_fields(struct http_proxy_message* m) { |
||||
char* p = m->headers + m->first_line_len; |
||||
m->headers[m->first_line_len - 2] = 0; |
||||
while (*p != '\r') { |
||||
if (m->field_count == HTTP_PROXY_FIELDS_LIMIT) return http_error(431, "field count limit"); |
||||
char* end = strstr(p, "\r\n"); |
||||
if (!end) return http_error(400, "field terminator"); |
||||
char* colon = strchr(p, ':'); |
||||
if (!colon || colon >= end || colon == p) return http_error(400, "field name"); |
||||
for (char* q = p; q < colon; q++) if (!token_char((unsigned char)*q)) return http_error(400, "field name token"); |
||||
*colon = 0; *end = 0; |
||||
char* value = colon + 1; while (*value == ' ' || *value == '\t') value++; |
||||
char* tail = end; while (tail > value && (tail[-1] == ' ' || tail[-1] == '\t')) *--tail = 0; |
||||
m->fields[m->field_count++] = (struct http_proxy_field){p, value}; |
||||
p = end + 2; |
||||
} |
||||
unsigned lengths = 0, transfers = 0, hosts = 0; |
||||
for (unsigned i = 0; i < m->field_count; i++) { |
||||
const char* name = m->fields[i].name; const char* value = m->fields[i].value; |
||||
if (ascii_equal(name, strlen(name), "Content-Length")) { |
||||
uint64_t n; |
||||
if (parse_number(value, strlen(value), 10, &n) < 0 || (lengths && n != m->length)) |
||||
return http_error(400, "Content-Length"); |
||||
m->length = n; m->has_length = 1; lengths++; |
||||
} else if (ascii_equal(name, strlen(name), "Transfer-Encoding")) { |
||||
if (++transfers != 1 || !ascii_equal(value, strlen(value), "chunked") || !m->minor) |
||||
return http_error(400, "unsupported Transfer-Encoding"); |
||||
m->chunked = 1; |
||||
} else if (ascii_equal(name, strlen(name), "Connection")) { |
||||
int close = list_contains(value, "close"), upgrade = list_contains(value, "upgrade"); |
||||
if (close < 0 || upgrade < 0) return http_error(400, "Connection list"); |
||||
m->close |= close; m->upgrade |= upgrade; |
||||
if (list_contains(value, "Content-Length") || list_contains(value, "Transfer-Encoding") || list_contains(value, "Host")) |
||||
return http_error(400, "Connection names framing field"); |
||||
} else if (ascii_equal(name, strlen(name), "Host")) { |
||||
if (++hosts != 1 || !*value || strpbrk(value, " \t/@?#,")) return http_error(400, "Host"); |
||||
} |
||||
} |
||||
if (m->chunked && m->has_length) return http_error(400, "Transfer-Encoding with Content-Length"); |
||||
m->body = m->chunked ? HTTP_BODY_CHUNK_SIZE : m->has_length && m->length ? HTTP_BODY_LENGTH : HTTP_BODY_DONE; |
||||
m->remaining = m->length; |
||||
return 0; |
||||
} |
||||
|
||||
// Определяет поля текущего соединения; framing реконструируется отдельно.
|
||||
static int remove_field(const struct http_proxy_message* m, const char* name) { |
||||
const char* fixed[] = {"Connection", "Proxy-Connection", "Proxy-Authorization", "Proxy-Authenticate", |
||||
"Keep-Alive", "TE", "Trailer", "Transfer-Encoding", "Content-Length", "Upgrade"}; |
||||
for (size_t i = 0; i < sizeof(fixed) / sizeof(fixed[0]); i++) if (ascii_equal(name, strlen(name), fixed[i])) return 1; |
||||
for (unsigned i = 0; i < m->field_count; i++) |
||||
if (ascii_equal(m->fields[i].name, strlen(m->fields[i].name), "Connection") && list_contains(m->fields[i].value, name) == 1) |
||||
return 1; |
||||
return 0; |
||||
} |
||||
|
||||
// Завершает сериализацию после проверенных полей; исходные байты учитываются независимо от результата.
|
||||
static int emit_headers(struct http_proxy_message* m, struct strbuf* out, int close, const char* upgrade, |
||||
http_proxy_emit emit, void* arg) { |
||||
for (unsigned i = 0; i < m->field_count; i++) { |
||||
const char* name = m->fields[i].name; |
||||
if (remove_field(m, name) || (m->status == 0 && ascii_equal(name, strlen(name), "Host"))) continue; |
||||
if (strbuf_addf(out, "%s: %s\r\n", name, m->fields[i].value) < 0) goto alloc_error; |
||||
} |
||||
if (m->has_length && strbuf_addf(out, "Content-Length: %llu\r\n", (unsigned long long)m->length) < 0) goto alloc_error; |
||||
if (m->chunked && !m->decode_chunks && strbuf_addf(out, "Transfer-Encoding: chunked\r\n") < 0) goto alloc_error; |
||||
if (upgrade && *upgrade) { |
||||
if (strbuf_addf(out, "Connection: Upgrade\r\nUpgrade: %s\r\n", upgrade) < 0) goto alloc_error; |
||||
} else if (close && strbuf_addf(out, "Connection: close\r\n") < 0) goto alloc_error; |
||||
if (strbuf_addf(out, "Via: 1.%u utun\r\n\r\n", m->minor) < 0) goto alloc_error; |
||||
if (emit(arg, (const uint8_t*)strbuf_str(out), out->len, m->header_len) < 0) goto alloc_error; |
||||
return 0; |
||||
alloc_error: |
||||
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "HTTP codec: header output failed bytes=%zu", out->len); |
||||
return -500; |
||||
} |
||||
|
||||
// Разбирает authority; IPv6 распознаётся, но общий CONNECT wire содержит только IPv4.
|
||||
static int parse_authority(const char* s, size_t len, int require_port, struct http_proxy_request* r) { |
||||
if (!len || memchr(s, '@', len) || memchr(s, '#', len)) return http_error(400, "authority"); |
||||
if (s[0] == '[') return http_error(502, "IPv6 destination unsupported"); |
||||
const char* colon = memchr(s, ':', len); |
||||
size_t host_len = colon ? (size_t)(colon - s) : len; |
||||
if (!host_len || host_len >= sizeof(r->host)) return http_error(400, "host length"); |
||||
for (size_t i = 0; i < host_len; i++) { |
||||
unsigned char c = (unsigned char)s[i]; |
||||
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '.' || c == '-')) |
||||
return http_error(400, "host syntax"); |
||||
} |
||||
r->port = 80; |
||||
if (colon) { |
||||
uint64_t port; |
||||
if (parse_number(colon + 1, len - host_len - 1, 10, &port) < 0 || !port || port > 65535) |
||||
return http_error(400, "port syntax/range"); |
||||
r->port = (uint16_t)port; |
||||
} else if (require_port) return http_error(400, "CONNECT port missing"); |
||||
memcpy(r->host, s, host_len); r->host[host_len] = 0; |
||||
snprintf(r->authority, sizeof(r->authority), "%s:%u", r->host, r->port); |
||||
return 0; |
||||
} |
||||
|
||||
// Строгая request line и origin-form без декодирования path/query.
|
||||
int http_proxy_request_parse(struct http_proxy_message* m, struct http_proxy_request* r, http_proxy_emit emit, void* arg) { |
||||
char* line = m->headers; |
||||
char* space = memchr(line, ' ', m->first_line_len - 2); |
||||
if (!space || space == line) return http_error(400, "request method"); |
||||
for (char* p = line; p < space; p++) if (!token_char((unsigned char)*p)) return http_error(400, "method token"); |
||||
char* target = space + 1; char* version = strchr(target, ' '); |
||||
if (!version || version == target) return http_error(400, "request target/version"); |
||||
if ((size_t)(m->headers + m->first_line_len - 2 - version) != 9 || memcmp(version, " HTTP/1.", 8) || |
||||
(version[8] != '0' && version[8] != '1')) return http_error(400, "HTTP version"); |
||||
m->minor = version[8] - '0'; *space = 0; *version = 0; |
||||
for (char* p = target; *p; p++) if ((unsigned char)*p <= 32 || *p == '#' || (unsigned char)*p >= 127) |
||||
return http_error(400, "request target byte"); |
||||
int ret = parse_fields(m); if (ret < 0) return ret; |
||||
unsigned hosts = 0; |
||||
for (unsigned i = 0; i < m->field_count; i++) if (ascii_equal(m->fields[i].name, strlen(m->fields[i].name), "Host")) hosts++; |
||||
if (m->minor && hosts != 1) return http_error(400, "HTTP/1.1 Host missing"); |
||||
r->connect = strcmp(line, "CONNECT") == 0; r->head = strcmp(line, "HEAD") == 0; |
||||
r->minor = m->minor; r->close = m->close || !m->minor; |
||||
if (r->connect) { |
||||
if (m->chunked || m->length) return http_error(400, "CONNECT content"); |
||||
return parse_authority(target, strlen(target), 1, r); |
||||
} |
||||
if (strlen(target) < 7 || !ascii_equal(target, 7, "http://")) return http_error(400, "absolute http URL required"); |
||||
char* authority = target + 7; char* path = authority + strcspn(authority, "/?"); |
||||
ret = parse_authority(authority, path - authority, 0, r); if (ret < 0) return ret; |
||||
if (m->upgrade) { |
||||
if (!m->minor) return http_error(400, "Upgrade requires HTTP/1.1"); |
||||
unsigned upgrades = 0; |
||||
for (unsigned i = 0; i < m->field_count; i++) if (ascii_equal(m->fields[i].name, strlen(m->fields[i].name), "Upgrade")) { |
||||
if (++upgrades > 1 || upgrade_contains(m->fields[i].value, NULL) < 0 || strlen(m->fields[i].value) >= sizeof(r->upgrade)) |
||||
return http_error(400, "Upgrade value"); |
||||
strcpy(r->upgrade, m->fields[i].value); |
||||
} |
||||
if (!upgrades) return http_error(400, "Upgrade missing"); |
||||
} |
||||
struct strbuf out = strbuf_new(); |
||||
const char* prefix = !*path || *path == '?' ? "/" : ""; |
||||
const char* request_path = strcmp(line, "OPTIONS") == 0 && !*path ? "*" : path; |
||||
if (*request_path == '*') prefix = ""; |
||||
if (strbuf_addf(&out, "%s %s%s HTTP/1.1\r\nHost: %s\r\n", line, prefix, request_path, r->authority) < 0) ret = -500; |
||||
else ret = emit_headers(m, &out, !m->upgrade, r->upgrade, emit, arg); |
||||
strbuf_free(&out); |
||||
if (ret == -500) DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "HTTP codec: request serialization failed"); |
||||
return ret; |
||||
} |
||||
|
||||
// Ответы HEAD/1xx/204/304 не имеют тела; Upgrade подтверждается только согласованным 101.
|
||||
int http_proxy_response_parse(struct http_proxy_message* m, const struct http_proxy_request* r, |
||||
int close_client, http_proxy_emit emit, void* arg) { |
||||
char* line = m->headers; |
||||
if (m->first_line_len < 15 || memcmp(line, "HTTP/1.", 7) || (line[7] != '0' && line[7] != '1') || line[8] != ' ') |
||||
return http_error(502, "response version/status line"); |
||||
uint64_t status; |
||||
if (parse_number(line + 9, 3, 10, &status) < 0 || status < 100 || status > 599 || line[12] != ' ') |
||||
return http_error(502, "response status"); |
||||
m->minor = line[7] - '0'; m->status = (unsigned)status; |
||||
int ret = parse_fields(m); if (ret < 0) return http_error(502, "response fields/framing"); |
||||
const char* upgrade = NULL; |
||||
if (m->status == 101) { |
||||
if (!r->upgrade[0] || !m->upgrade) return http_error(502, "unsolicited Upgrade"); |
||||
for (unsigned i = 0; i < m->field_count; i++) if (ascii_equal(m->fields[i].name, strlen(m->fields[i].name), "Upgrade")) { |
||||
if (upgrade || upgrade_contains(r->upgrade, m->fields[i].value) != 1) return http_error(502, "Upgrade mismatch"); |
||||
upgrade = m->fields[i].value; |
||||
} |
||||
if (!upgrade) return http_error(502, "101 Upgrade missing"); |
||||
} |
||||
if (r->head || m->status < 200 || m->status == 204 || m->status == 304) m->body = HTTP_BODY_DONE; |
||||
else if (!m->chunked && !m->has_length) { m->body = HTTP_BODY_EOF; close_client = 1; } |
||||
if ((m->status < 200 || m->status == 204) && (m->chunked || m->has_length)) return http_error(502, "body headers on no-content response"); |
||||
m->close = close_client; |
||||
m->decode_chunks = !r->minor && m->chunked; |
||||
if (!r->minor && m->status < 200) { |
||||
if (emit(arg, NULL, 0, m->header_len) < 0) return http_error(500, "interim credit output failed"); |
||||
return 0; |
||||
} |
||||
struct strbuf out = strbuf_new(); |
||||
if (strbuf_addf(&out, "HTTP/1.%u %s\r\n", r->minor, line + 9) < 0) ret = -500; |
||||
else ret = emit_headers(m, &out, close_client, upgrade, emit, arg); |
||||
strbuf_free(&out); |
||||
if (ret == -500) DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "HTTP codec: response serialization failed"); |
||||
return ret; |
||||
} |
||||
|
||||
// Chunk extensions: tokens/quoted-string с ограниченным размером строки.
|
||||
static int chunk_size(const char* line, size_t len, uint64_t* size) { |
||||
size_t n = 0; |
||||
while (n < len && ((line[n] >= '0' && line[n] <= '9') || (line[n] >= 'a' && line[n] <= 'f') || |
||||
(line[n] >= 'A' && line[n] <= 'F'))) n++; |
||||
if (parse_number(line, n, 16, size) < 0) return -1; |
||||
while (n < len) { |
||||
while (n < len && (line[n] == ' ' || line[n] == '\t')) n++; |
||||
if (n == len || line[n++] != ';') return -1; |
||||
while (n < len && (line[n] == ' ' || line[n] == '\t')) n++; |
||||
size_t start = n; while (n < len && token_char((unsigned char)line[n])) n++; |
||||
if (start == n) return -1; |
||||
while (n < len && (line[n] == ' ' || line[n] == '\t')) n++; |
||||
if (n < len && line[n] == '=') { |
||||
n++; while (n < len && (line[n] == ' ' || line[n] == '\t')) n++; |
||||
if (n < len && line[n] == '"') { |
||||
n++; |
||||
while (n < len && line[n] != '"') { |
||||
if (line[n] == '\\') { if (++n == len) return -1; } |
||||
if ((unsigned char)line[n] < 32 && line[n] != '\t') return -1; |
||||
n++; |
||||
} |
||||
if (n == len) return -1; |
||||
n++; |
||||
} else { |
||||
start = n; while (n < len && token_char((unsigned char)line[n])) n++; |
||||
if (start == n) return -1; |
||||
} |
||||
} |
||||
} |
||||
return 0; |
||||
} |
||||
|
||||
// Тело не буферизуется целиком; только chunk size/trailer line ожидают полного CRLF.
|
||||
int http_proxy_body_feed(struct http_proxy_message* m, const uint8_t* data, size_t len, size_t* used, |
||||
http_proxy_emit emit, void* arg) { |
||||
*used = 0; |
||||
while (*used < len && m->body != HTTP_BODY_DONE) { |
||||
if (m->body == HTTP_BODY_LENGTH || m->body == HTTP_BODY_CHUNK_DATA || m->body == HTTP_BODY_EOF) { |
||||
size_t n = len - *used; |
||||
if (m->body != HTTP_BODY_EOF && m->remaining < n) n = (size_t)m->remaining; |
||||
if (emit(arg, data + *used, n, n) < 0) return http_error(500, "body output failed"); |
||||
*used += n; |
||||
if (m->body != HTTP_BODY_EOF) { |
||||
m->remaining -= n; |
||||
if (!m->remaining) m->body = m->body == HTTP_BODY_LENGTH ? HTTP_BODY_DONE : HTTP_BODY_CHUNK_CRLF; |
||||
} |
||||
} else if (m->body == HTTP_BODY_CHUNK_CRLF) { |
||||
unsigned char c = data[(*used)++]; |
||||
if (c != (m->crlf_pos ? '\n' : '\r')) return http_error(400, "chunk data CRLF"); |
||||
if (++m->crlf_pos == 2) { |
||||
if (emit(arg, (const uint8_t*)"\r\n", m->decode_chunks ? 0 : 2, 2) < 0) |
||||
return http_error(500, "chunk CRLF output failed"); |
||||
m->crlf_pos = 0; m->body = HTTP_BODY_CHUNK_SIZE; |
||||
} |
||||
} else { |
||||
if (m->line_len == HTTP_PROXY_LINE_LIMIT) return http_error(431, "chunk/trailer line limit"); |
||||
unsigned char c = data[(*used)++]; |
||||
if (!c || c == 127 || (c < 32 && c != '\r' && c != '\n' && c != '\t')) return http_error(400, "chunk/trailer control byte"); |
||||
m->line[m->line_len++] = (char)c; |
||||
size_t n = m->line_len; |
||||
if (c == '\n' && (n < 2 || m->line[n-2] != '\r')) return http_error(400, "chunk/trailer bare LF"); |
||||
if (n > 1 && m->line[n-2] == '\r' && c != '\n') return http_error(400, "chunk/trailer bare CR"); |
||||
if (c != '\n') continue; |
||||
m->line[n] = 0; |
||||
int drop = m->decode_chunks; |
||||
if (m->body == HTTP_BODY_CHUNK_SIZE) { |
||||
if (chunk_size(m->line, n - 2, &m->remaining) < 0) return http_error(400, "chunk size/extensions"); |
||||
m->body = m->remaining ? HTTP_BODY_CHUNK_DATA : HTTP_BODY_TRAILERS; |
||||
} else { |
||||
m->trailer_len += n; |
||||
if (m->trailer_len > HTTP_PROXY_HEADER_LIMIT) return http_error(431, "trailers limit"); |
||||
if (n == 2) m->body = HTTP_BODY_DONE; |
||||
else { |
||||
char* colon = memchr(m->line, ':', n - 2); |
||||
if (!colon || colon == m->line) return http_error(400, "trailer name"); |
||||
for (char* p = m->line; p < colon; p++) if (!token_char((unsigned char)*p)) return http_error(400, "trailer token"); |
||||
*colon = 0; |
||||
if (ascii_equal(m->line, strlen(m->line), "Host")) return http_error(400, "Host trailer"); |
||||
if (ascii_equal(m->line, strlen(m->line), "Content-Length") || |
||||
ascii_equal(m->line, strlen(m->line), "Transfer-Encoding")) return http_error(400, "framing trailer"); |
||||
drop |= remove_field(m, m->line); *colon = ':'; |
||||
} |
||||
} |
||||
if (emit(arg, (const uint8_t*)m->line, drop ? 0 : n, n) < 0) return http_error(500, "chunk/trailer output failed"); |
||||
m->line_len = 0; |
||||
} |
||||
} |
||||
return m->body == HTTP_BODY_DONE; |
||||
} |
||||
@ -0,0 +1,66 @@
|
||||
/* Потоковый HTTP/1.x codec локального proxy. Не владеет сокетами/DNS/ETCP.
|
||||
* Заголовки проверяются целиком до пересылки; тело передаётся ограниченными порциями. |
||||
* emit получает исходное число байт для возврата ETCP credit после доставки результата. |
||||
* Возврат feed: 0=нужно продолжение, 1=сообщение завершено, <0=HTTP error status. */ |
||||
#ifndef HTTP_PROXY_H |
||||
#define HTTP_PROXY_H |
||||
|
||||
#include <stddef.h> |
||||
#include <stdint.h> |
||||
|
||||
#define HTTP_PROXY_LINE_LIMIT 8192 |
||||
#define HTTP_PROXY_HEADER_LIMIT 32768 |
||||
#define HTTP_PROXY_FIELDS_LIMIT 512 |
||||
|
||||
typedef int (*http_proxy_emit)(void* arg, const uint8_t* data, size_t len, size_t source_len); |
||||
|
||||
struct http_proxy_field { char* name; char* value; }; |
||||
enum http_proxy_body_state { HTTP_BODY_NONE, HTTP_BODY_LENGTH, HTTP_BODY_EOF, HTTP_BODY_CHUNK_SIZE, |
||||
HTTP_BODY_CHUNK_DATA, HTTP_BODY_CHUNK_CRLF, HTTP_BODY_TRAILERS, HTTP_BODY_DONE }; |
||||
|
||||
struct http_proxy_message { |
||||
char headers[HTTP_PROXY_HEADER_LIMIT + 1]; |
||||
size_t header_len; |
||||
size_t first_line_len; |
||||
struct http_proxy_field fields[HTTP_PROXY_FIELDS_LIMIT]; |
||||
unsigned field_count; |
||||
uint8_t headers_done; |
||||
uint8_t minor; |
||||
uint8_t close; |
||||
uint8_t chunked; |
||||
uint8_t has_length; |
||||
uint8_t upgrade; |
||||
uint8_t decode_chunks; |
||||
unsigned status; |
||||
uint64_t length; |
||||
enum http_proxy_body_state body; |
||||
uint64_t remaining; |
||||
char line[HTTP_PROXY_LINE_LIMIT + 1]; |
||||
size_t line_len; |
||||
size_t trailer_len; |
||||
unsigned crlf_pos; |
||||
}; |
||||
|
||||
struct http_proxy_request { |
||||
char host[256]; |
||||
char authority[264]; |
||||
char upgrade[HTTP_PROXY_LINE_LIMIT + 1]; |
||||
uint16_t port; |
||||
uint8_t connect; |
||||
uint8_t head; |
||||
uint8_t close; |
||||
uint8_t minor; |
||||
}; |
||||
|
||||
// Накапливает только заголовки; хвост входа остаётся у вызывающей стороны.
|
||||
int http_proxy_headers_feed(struct http_proxy_message* m, const uint8_t* data, size_t len, size_t* used); |
||||
// Проверяет запрос и отправляет реконструированные заголовки через emit (кроме CONNECT).
|
||||
int http_proxy_request_parse(struct http_proxy_message* m, struct http_proxy_request* r, http_proxy_emit emit, void* arg); |
||||
// Проверяет ответ, учитывает HEAD/Upgrade и формирует собственные connection headers.
|
||||
int http_proxy_response_parse(struct http_proxy_message* m, const struct http_proxy_request* r, |
||||
int close_client, http_proxy_emit emit, void* arg); |
||||
// Определяет точную границу тела, фильтрует trailers; поддерживает произвольные TCP-разбиения.
|
||||
int http_proxy_body_feed(struct http_proxy_message* m, const uint8_t* data, size_t len, size_t* used, |
||||
http_proxy_emit emit, void* arg); |
||||
|
||||
#endif |
||||
@ -0,0 +1,160 @@
|
||||
// Проверки HTTP codec на каждом возможном разбиении headers и побайтовом chunked body.
|
||||
#include <stdio.h> |
||||
#include <stdlib.h> |
||||
#include <string.h> |
||||
#include "proxy/http_proxy.h" |
||||
#include "debug_config.h" |
||||
#include "mem.h" |
||||
|
||||
#define CHECK(x) do { if (!(x)) { fprintf(stderr, "FAIL line %d: %s\n", __LINE__, #x); exit(1); } } while (0) |
||||
struct output { char bytes[100000]; size_t len, source; }; |
||||
|
||||
static int collect(void* arg, const uint8_t* data, size_t len, size_t source) { |
||||
struct output* out = arg; |
||||
CHECK(out->len + len < sizeof(out->bytes)); |
||||
if (len) memcpy(out->bytes + out->len, data, len); |
||||
out->len += len; out->source += source; out->bytes[out->len] = 0; |
||||
return 0; |
||||
} |
||||
|
||||
static int request(const char* text, struct http_proxy_message* m, struct http_proxy_request* r, struct output* out) { |
||||
memset(m, 0, sizeof(*m)); memset(r, 0, sizeof(*r)); memset(out, 0, sizeof(*out)); |
||||
size_t used; |
||||
int ret = http_proxy_headers_feed(m, (const uint8_t*)text, strlen(text), &used); |
||||
if (ret <= 0) return ret; |
||||
return http_proxy_request_parse(m, r, collect, out); |
||||
} |
||||
|
||||
static int response(const char* text, struct http_proxy_message* m, const struct http_proxy_request* r, struct output* out) { |
||||
memset(m, 0, sizeof(*m)); memset(out, 0, sizeof(*out)); |
||||
size_t used; |
||||
int ret = http_proxy_headers_feed(m, (const uint8_t*)text, strlen(text), &used); |
||||
if (ret <= 0) return ret; |
||||
return http_proxy_response_parse(m, r, r->close, collect, out); |
||||
} |
||||
|
||||
static void header_tests(void) { |
||||
const char text[] = "POST http://example.org:8080?x=1 HTTP/1.1\r\nHost: wrong.example\r\n" |
||||
"Proxy-Authorization: Basic secret\r\nConnection: X-Private, keep-alive\r\n" |
||||
"X-Private: secret\r\nAuthorization: Bearer origin\r\nContent-Length: 3\r\n\r\nabcNEXT"; |
||||
size_t header_len = strstr(text, "\r\n\r\n") + 4 - text; |
||||
for (size_t split = 0; split <= header_len; split++) { |
||||
struct http_proxy_message m = {0}; struct http_proxy_request r = {0}; struct output out = {0}; size_t used; |
||||
CHECK(http_proxy_headers_feed(&m, (const uint8_t*)text, split, &used) == (split == header_len)); |
||||
CHECK(used == split && out.source == 0); |
||||
CHECK(http_proxy_headers_feed(&m, (const uint8_t*)text + split, sizeof(text)-1-split, &used) == 1); |
||||
CHECK(used == header_len - split); |
||||
CHECK(http_proxy_request_parse(&m, &r, collect, &out) == 0); |
||||
CHECK(!strcmp(r.host, "example.org") && r.port == 8080); |
||||
CHECK(strstr(out.bytes, "POST /?x=1 HTTP/1.1\r\nHost: example.org:8080\r\n")); |
||||
CHECK(!strstr(out.bytes, "secret") && !strstr(out.bytes, "wrong.example")); |
||||
CHECK(strstr(out.bytes, "Authorization: Bearer origin\r\n")); |
||||
CHECK(strstr(out.bytes, "Connection: close\r\n") && out.source == header_len); |
||||
CHECK(http_proxy_body_feed(&m, (const uint8_t*)text + header_len, 7, &used, collect, &out) == 1); |
||||
CHECK(used == 3 && out.source == header_len + 3 && !strcmp(out.bytes + out.len - 3, "abc")); |
||||
} |
||||
struct http_proxy_message m; struct http_proxy_request r; struct output out; |
||||
CHECK(request("OPTIONS http://example.org HTTP/1.1\r\nHost: ignored\r\n\r\n", &m, &r, &out) == 0); |
||||
CHECK(strstr(out.bytes, "OPTIONS * HTTP/1.1\r\n")); |
||||
CHECK(request("CONNECT example.org:443 HTTP/1.1\r\nHost: ignored\r\n\r\n", &m, &r, &out) == 0); |
||||
CHECK(r.connect && r.port == 443 && out.len == 0); |
||||
const char* invalid[] = { |
||||
"CONNECT example.org:443junk HTTP/1.1\r\nHost: x\r\n\r\n", |
||||
"CONNECT example.org:443 GARBAGE\r\nHost: x\r\n\r\n", |
||||
"CONNECT example.org HTTP/1.1\r\nHost: x\r\n\r\n", |
||||
"CONNECT example.org:443 HTTP/1.1\r\nHost: x\r\nContent-Length: 1\r\n\r\n", |
||||
"GET http://x/ HTTP/1.1\r\n\r\n", |
||||
"GET http://x/ HTTP/1.1\r\nHost: x\r\nHost: x\r\n\r\n", |
||||
"GET http://x/ HTTP/1.1\r\nHost : x\r\n\r\n", |
||||
"GET http://x/ HTTP/1.1\nHost: x\n\n", |
||||
"GET http://x/ HTTP/1.1\r\nHost: x\r\nX: a\r\n folded\r\n\r\n", |
||||
"POST http://x/ HTTP/1.1\r\nHost: x\r\nContent-Length: 1\r\nContent-Length: 2\r\n\r\n", |
||||
"POST http://x/ HTTP/1.1\r\nHost: x\r\nContent-Length: 18446744073709551616\r\n\r\n", |
||||
"POST http://x/ HTTP/1.1\r\nHost: x\r\nContent-Length: +1\r\n\r\n", |
||||
"POST http://x/ HTTP/1.1\r\nHost: x\r\nContent-Length: 1\r\nTransfer-Encoding: chunked\r\n\r\n", |
||||
"POST http://x/ HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked, chunked\r\n\r\n", |
||||
"POST http://x/ HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: gzip\r\n\r\n", |
||||
"GET http://x/ HTTP/1.1\r\nHost: x\r\nConnection: Host\r\n\r\n", |
||||
"GET http://x/ HTTP/1.1\r\nHost: x\r\nConnection: Upgrade\r\nUpgrade: bad/version/extra\r\n\r\n", |
||||
"GET http://user@x/ HTTP/1.1\r\nHost: x\r\n\r\n", |
||||
"GET http://x:65536/ HTTP/1.1\r\nHost: x\r\n\r\n", |
||||
"GET http://x:0/ HTTP/1.1\r\nHost: x\r\n\r\n", |
||||
"GET http://x/#fragment HTTP/1.1\r\nHost: x\r\n\r\n" |
||||
}; |
||||
for (size_t i = 0; i < sizeof(invalid)/sizeof(invalid[0]); i++) { |
||||
CHECK(request(invalid[i], &m, &r, &out) == -400 && out.len == 0); |
||||
} |
||||
CHECK(request("GET http://[::1]/ HTTP/1.1\r\nHost: [::1]\r\n\r\n", &m, &r, &out) == -502); |
||||
char long_text[40000]; int n = snprintf(long_text, sizeof(long_text), "GET http://x/"); |
||||
memset(long_text+n, 'a', 3000); n += 3000; |
||||
snprintf(long_text+n, sizeof(long_text)-n, " HTTP/1.1\r\nHost: x\r\n\r\n"); |
||||
CHECK(request(long_text, &m, &r, &out) == 0); |
||||
n = snprintf(long_text, sizeof(long_text), "POST http://x/ HTTP/1.1\r\nHost: x\r\nX-Pad: "); |
||||
memset(long_text+n, 'a', 30000); n += 30000; |
||||
snprintf(long_text+n, sizeof(long_text)-n, "\r\nContent-Length: 2000\r\n\r\n"); |
||||
CHECK(request(long_text, &m, &r, &out) == 0); |
||||
n = snprintf(long_text, sizeof(long_text), "GET http://x/ HTTP/1.1\r\nHost: x\r\nX-Pad: "); |
||||
memset(long_text+n, 'a', 33000); long_text[n+33000] = 0; |
||||
CHECK(request(long_text, &m, &r, &out) == -431); |
||||
puts("[PASS] HTTP strict headers, absolute URL/Host, hop fields, arbitrary splits and limits"); |
||||
} |
||||
|
||||
static void body_tests(void) { |
||||
struct http_proxy_message m; struct http_proxy_request r; struct output out; |
||||
const char header[] = "POST http://x/ HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\nConnection: X-Hop\r\n\r\n"; |
||||
const char body[] = "3 ; foo=\"x\\\"y\"\r\nabc\r\n2\r\nde\r\n0\r\nX-End: yes\r\nX-Hop: secret\r\nProxy-Authorization: secret\r\n\r\n"; |
||||
CHECK(request(header, &m, &r, &out) == 0); |
||||
size_t head_out = out.len; |
||||
for (size_t i = 0; i < sizeof(body)-1; i++) { |
||||
size_t used; |
||||
CHECK(http_proxy_body_feed(&m, (const uint8_t*)body+i, 1, &used, collect, &out) == (i == sizeof(body)-2)); |
||||
CHECK(used == 1 && out.source <= strlen(header)+i+1); |
||||
} |
||||
CHECK(out.source == strlen(header)+strlen(body)); |
||||
CHECK(!strcmp(out.bytes+head_out, "3 ; foo=\"x\\\"y\"\r\nabc\r\n2\r\nde\r\n0\r\nX-End: yes\r\n\r\n")); |
||||
const char* bad[] = {"Z\r\n", "10000000000000000\r\n", "1\r\na!", "1\na", "0\r\nContent-Length: 1\r\n\r\n", |
||||
"0\r\nHost: x\r\n\r\n", "0\r\n folded\r\n\r\n", "1;foo=\"bad\r\n"}; |
||||
for (size_t i = 0; i < sizeof(bad)/sizeof(bad[0]); i++) { |
||||
size_t used; CHECK(request(header, &m, &r, &out) == 0); |
||||
CHECK(http_proxy_body_feed(&m, (const uint8_t*)bad[i], strlen(bad[i]), &used, collect, &out) == -400); |
||||
} |
||||
r = (struct http_proxy_request){.minor=0,.close=1}; |
||||
const char reply[] = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n"; |
||||
CHECK(response(reply, &m, &r, &out) == 0 && m.decode_chunks); |
||||
CHECK(!strstr(out.bytes, "Transfer-Encoding") && strstr(out.bytes, "HTTP/1.0 200 OK\r\n")); |
||||
head_out = out.len; |
||||
for (size_t i = 0; i < sizeof(body)-1; i++) { |
||||
size_t used; CHECK(http_proxy_body_feed(&m, (const uint8_t*)body+i, 1, &used, collect, &out) >= 0); |
||||
} |
||||
CHECK(!strcmp(out.bytes+head_out, "abcde") && out.source == strlen(reply)+strlen(body)); |
||||
puts("[PASS] chunked boundaries, extensions/trailers, source byte credit and HTTP/1.0 decoding"); |
||||
} |
||||
|
||||
static void response_tests(void) { |
||||
struct http_proxy_message m; struct output out; struct http_proxy_request r = {.minor=1}; |
||||
CHECK(response("HTTP/1.1 100 Continue\r\n\r\n", &m, &r, &out) == 0 && m.body == HTTP_BODY_DONE); |
||||
CHECK(response("HTTP/1.1 204 No Content\r\n\r\n", &m, &r, &out) == 0 && m.body == HTTP_BODY_DONE); |
||||
CHECK(response("HTTP/1.1 304 Not Modified\r\nContent-Length: 999\r\n\r\n", &m, &r, &out) == 0 && m.body == HTTP_BODY_DONE); |
||||
CHECK(response("HTTP/1.1 200 OK\r\n\r\n", &m, &r, &out) == 0 && m.body == HTTP_BODY_EOF && m.close); |
||||
CHECK(strstr(out.bytes, "Connection: close\r\n")); |
||||
r.head = 1; |
||||
CHECK(response("HTTP/1.1 200 OK\r\nContent-Length: 999\r\n\r\n", &m, &r, &out) == 0 && m.body == HTTP_BODY_DONE); |
||||
r.head = 0; |
||||
CHECK(response("HTTP/1.1 204 No Content\r\nContent-Length: 0\r\n\r\n", &m, &r, &out) == -502 && out.len == 0); |
||||
CHECK(response("HTTP/1.1 200 OK\r\nContent-Length: 2\r\nTransfer-Encoding: chunked\r\n\r\n", &m, &r, &out) == -502); |
||||
CHECK(response("HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\n", &m, &r, &out) == -502); |
||||
strcpy(r.upgrade, "websocket, example/1.2"); |
||||
CHECK(response("HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: example/1.2\r\n\r\n", &m, &r, &out) == 0); |
||||
CHECK(strstr(out.bytes, "Connection: Upgrade\r\nUpgrade: example/1.2\r\n")); |
||||
CHECK(response("HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: other\r\n\r\n", &m, &r, &out) == -502); |
||||
r.minor = 0; r.close = 1; r.upgrade[0] = 0; |
||||
CHECK(response("HTTP/1.1 100 Continue\r\n\r\n", &m, &r, &out) == 0 && out.len == 0 && out.source > 0); |
||||
puts("[PASS] HEAD/1xx/204/304/EOF response framing and negotiated Upgrade"); |
||||
} |
||||
|
||||
int main(void) { |
||||
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); |
||||
header_tests(); body_tests(); response_tests(); |
||||
CHECK(u_get_allocated_count() == 0); |
||||
return 0; |
||||
} |
||||
Loading…
Reference in new issue