// cHTTP, an HTTP client and server library! // // This file was generated automatically. Do not modify directly. // // Refer to the end of this file for the license #ifndef CHTTP_DONT_INCLUDE #include "chttp.h" #endif //////////////////////////////////////////////////////////////////////////////////////// // src/basic.c //////////////////////////////////////////////////////////////////////////////////////// bool chttp_streq(CHTTP_String s1, CHTTP_String s2) { if (s1.len != s2.len) return false; for (int i = 0; i < s1.len; i++) if (s1.ptr[i] != s2.ptr[i]) return false; return true; } static char to_lower(char c) { if (c >= 'A' && c <= 'Z') return c - 'A' + 'a'; return c; } bool chttp_streqcase(CHTTP_String s1, CHTTP_String s2) { if (s1.len != s2.len) return false; for (int i = 0; i < s1.len; i++) if (to_lower(s1.ptr[i]) != to_lower(s2.ptr[i])) return false; return true; } CHTTP_String chttp_trim(CHTTP_String s) { int i = 0; while (i < s.len && (s.ptr[i] == ' ' || s.ptr[i] == '\t')) i++; if (i == s.len) { s.ptr = NULL; s.len = 0; } else { s.ptr += i; s.len -= i; while (s.ptr[s.len-1] == ' ' || s.ptr[s.len-1] == '\t') s.len--; } return s; } static bool is_printable(char c) { return c >= ' ' && c <= '~'; } void print_bytes(CHTTP_String prefix, CHTTP_String src) { if (src.len == 0) return; FILE *stream = stdout; bool new_line = true; int cur = 0; for (;;) { int start = cur; while (cur < src.len && is_printable(src.ptr[cur])) cur++; if (new_line) { fwrite(prefix.ptr, 1, prefix.len, stream); new_line = false; } fwrite(src.ptr + start, 1, cur - start, stream); if (cur == src.len) break; if (src.ptr[cur] == '\n') { putc('\\', stream); putc('n', stream); putc('\n', stream); new_line = true; } else if (src.ptr[cur] == '\r') { putc('\\', stream); putc('r', stream); } else { putc('.', stream); } cur++; } putc('\n', stream); } char *chttp_strerror(int code) { switch (code) { case CHTTP_OK: return "No error"; case CHTTP_ERROR_UNSPECIFIED: return "Unspecified error"; case CHTTP_ERROR_OOM: return "Out of memory"; case CHTTP_ERROR_BADURL: return "Invalid URL"; case CHTTP_ERROR_REQLIMIT: return "Parallel request limit reached"; case CHTTP_ERROR_BADHANDLE: return "Invalid handle"; case CHTTP_ERROR_NOTLS: return "TLS support not built-in"; } return "???"; } //////////////////////////////////////////////////////////////////////////////////////// // src/parse.c //////////////////////////////////////////////////////////////////////////////////////// // From RFC 9112 // request-target = origin-form // / absolute-form // / authority-form // / asterisk-form // origin-form = absolute-path [ "?" query ] // absolute-form = absolute-URI // authority-form = uri-host ":" port // asterisk-form = "*" // // From RFC 9110 // URI-reference = // absolute-URI = // relative-part = // authority = // uri-host = // port = // path-abempty = // segment = // query = // // absolute-path = 1*( "/" segment ) // partial-URI = relative-part [ "?" query ] // // From RFC 3986: // segment = *pchar // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" // pct-encoded = "%" HEXDIG HEXDIG // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" // / "*" / "+" / "," / ";" / "=" // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" // query = *( pchar / "/" / "?" ) // absolute-URI = scheme ":" hier-part [ "?" query ] // hier-part = "//" authority path-abempty // / path-absolute // / path-rootless // / path-empty // scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) typedef struct { char *src; int len; int cur; } Scanner; static int is_digit(char c) { return c >= '0' && c <= '9'; } static int is_alpha(char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); } static int is_hex_digit(char c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); } // From RFC 3986: // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" // / "*" / "+" / "," / ";" / "=" static int is_sub_delim(char c) { return c == '!' || c == '$' || c == '&' || c == '\'' || c == '(' || c == ')' || c == '*' || c == '+' || c == ',' || c == ';' || c == '='; } // From RFC 3986: // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" static int is_unreserved(char c) { return is_alpha(c) || is_digit(c) || c == '-' || c == '.' || c == '_' || c == '~'; } // From RFC 3986: // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" static int is_pchar(char c) { return is_unreserved(c) || is_sub_delim(c) || c == ':' || c == '@'; } static int is_tchar(char c) { return is_digit(c) || is_alpha(c) || c == '!' || c == '#' || c == '$' || c == '%' || c == '&' || c == '\'' || c == '*' || c == '+' || c == '-' || c == '.' || c == '^' || c == '_' || c == '~'; } static int is_vchar(char c) { return c >= ' ' && c <= '~'; } #define CONSUME_OPTIONAL_SEQUENCE(scanner, func) \ while ((scanner)->cur < (scanner)->len && (func)((scanner)->src[(scanner)->cur])) \ (scanner)->cur++; static int consume_absolute_path(Scanner *s) { if (s->cur == s->len || s->src[s->cur] != '/') return -1; // ERROR s->cur++; for (;;) { CONSUME_OPTIONAL_SEQUENCE(s, is_pchar); if (s->cur == s->len || s->src[s->cur] != '/') break; s->cur++; } return 0; } // If abempty=1: // path-abempty = *( "/" segment ) // else: // path-absolute = "/" [ segment-nz *( "/" segment ) ] // path-rootless = segment-nz *( "/" segment ) // path-empty = 0 static int parse_path(Scanner *s, CHTTP_String *path, int abempty) { int start = s->cur; if (abempty) { // path-abempty while (s->cur < s->len && s->src[s->cur] == '/') { do s->cur++; while (s->cur < s->len && is_pchar(s->src[s->cur])); } } else if (s->cur < s->len && (s->src[s->cur] == '/')) { // path-absolute s->cur++; if (s->cur < s->len && is_pchar(s->src[s->cur])) { s->cur++; for (;;) { CONSUME_OPTIONAL_SEQUENCE(s, is_pchar); if (s->cur == s->len || s->src[s->cur] != '/') break; s->cur++; } } } else if (s->cur < s->len && is_pchar(s->src[s->cur])) { // path-rootless s->cur++; for (;;) { CONSUME_OPTIONAL_SEQUENCE(s, is_pchar) if (s->cur == s->len || s->src[s->cur] != '/') break; s->cur++; } } else { // path->empty // (do nothing) } *path = (CHTTP_String) { s->src + start, s->cur - start, }; if (path->len == 0) path->ptr = NULL; return 0; } // RFC 3986: // query = *( pchar / "/" / "?" ) static int is_query(char c) { return is_pchar(c) || c == '/' || c == '?'; } // RFC 3986: // fragment = *( pchar / "/" / "?" ) static int is_fragment(char c) { return is_pchar(c) || c == '/' || c == '?'; } static int little_endian(void) { uint16_t x = 1; return *((uint8_t*) &x); } static void invert_bytes(void *p, int len) { char *c = p; for (int i = 0; i < len/2; i++) { char tmp = c[i]; c[i] = c[len-i-1]; c[len-i-1] = tmp; } } static int parse_ipv4(Scanner *s, CHTTP_IPv4 *ipv4) { unsigned int out = 0; int i = 0; for (;;) { if (s->cur == s->len || !is_digit(s->src[s->cur])) return -1; int b = 0; do { int x = s->src[s->cur++] - '0'; if (b > (UINT8_MAX - x) / 10) return -1; b = b * 10 + x; } while (s->cur < s->len && is_digit(s->src[s->cur])); out <<= 8; out |= (unsigned char) b; i++; if (i == 4) break; if (s->cur == s->len || s->src[s->cur] != '.') return -1; s->cur++; } if (little_endian()) invert_bytes(&out, 4); ipv4->data = out; return 0; } static int hex_digit_to_int(char c) { if (c >= 'a' && c <= 'f') return c - 'a' + 10; if (c >= 'A' && c <= 'F') return c - 'A' + 10; if (c >= '0' && c <= '9') return c - '0'; return -1; } static int parse_ipv6_comp(Scanner *s) { unsigned short buf; if (s->cur == s->len || !is_hex_digit(s->src[s->cur])) return -1; buf = hex_digit_to_int(s->src[s->cur]); s->cur++; if (s->cur == s->len || !is_hex_digit(s->src[s->cur])) return buf; buf <<= 4; buf |= hex_digit_to_int(s->src[s->cur]); s->cur++; if (s->cur == s->len || !is_hex_digit(s->src[s->cur])) return buf; buf <<= 4; buf |= hex_digit_to_int(s->src[s->cur]); s->cur++; if (s->cur == s->len || !is_hex_digit(s->src[s->cur])) return buf; buf <<= 4; buf |= hex_digit_to_int(s->src[s->cur]); s->cur++; return (int) buf; } static int parse_ipv6(Scanner *s, CHTTP_IPv6 *ipv6) { unsigned short head[8]; unsigned short tail[8]; int head_len = 0; int tail_len = 0; if (s->len - s->cur > 1 && s->src[s->cur+0] == ':' && s->src[s->cur+1] == ':') s->cur += 2; else { for (;;) { int ret = parse_ipv6_comp(s); if (ret < 0) return ret; head[head_len++] = (unsigned short) ret; if (head_len == 8) break; if (s->cur == s->len || s->src[s->cur] != ':') return -1; s->cur++; if (s->cur < s->len && s->src[s->cur] == ':') { s->cur++; break; } } } if (head_len < 8) { while (s->cur < s->len && is_hex_digit(s->src[s->cur])) { int ret = parse_ipv6_comp(s); if (ret < 0) return ret; tail[tail_len++] = (unsigned short) ret; if (head_len + tail_len == 8) break; if (s->cur == s->len || s->src[s->cur] != ':') break; s->cur++; } } for (int i = 0; i < head_len; i++) ipv6->data[i] = head[i]; for (int i = 0; i < 8 - head_len - tail_len; i++) ipv6->data[head_len + i] = 0; for (int i = 0; i < tail_len; i++) ipv6->data[8 - tail_len + i] = tail[i]; if (little_endian()) for (int i = 0; i < 8; i++) invert_bytes(&ipv6->data[i], 2); return 0; } // From RFC 3986: // reg-name = *( unreserved / pct-encoded / sub-delims ) static int is_regname(char c) { return is_unreserved(c) || is_sub_delim(c); } static int parse_regname(Scanner *s, CHTTP_String *regname) { if (s->cur == s->len || !is_regname(s->src[s->cur])) return -1; int start = s->cur; do s->cur++; while (s->cur < s->len && is_regname(s->src[s->cur])); regname->ptr = s->src + start; regname->len = s->cur - start; return 0; } static int parse_host(Scanner *s, CHTTP_Host *host) { int ret; if (s->cur < s->len && s->src[s->cur] == '[') { s->cur++; int start = s->cur; CHTTP_IPv6 ipv6; ret = parse_ipv6(s, &ipv6); if (ret < 0) return ret; host->mode = CHTTP_HOST_MODE_IPV6; host->ipv6 = ipv6; host->text = (CHTTP_String) { s->src + start, s->cur - start }; if (s->cur == s->len || s->src[s->cur] != ']') return -1; s->cur++; } else { int start = s->cur; CHTTP_IPv4 ipv4; ret = parse_ipv4(s, &ipv4); if (ret >= 0) { host->mode = CHTTP_HOST_MODE_IPV4; host->ipv4 = ipv4; } else { s->cur = start; CHTTP_String regname; ret = parse_regname(s, ®name); if (ret < 0) return ret; host->mode = CHTTP_HOST_MODE_NAME; host->name = regname; } host->text = (CHTTP_String) { s->src + start, s->cur - start }; } return 0; } // scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) static int is_scheme_head(char c) { return is_alpha(c); } static int is_scheme_body(char c) { return is_alpha(c) || is_digit(c) || c == '+' || c == '-' || c == '.'; } // userinfo = *( unreserved / pct-encoded / sub-delims / ":" ) // Note: percent-encoded characters (%XX) are not currently validated static int is_userinfo(char c) { return is_unreserved(c) || is_sub_delim(c) || c == ':'; } // authority = [ userinfo "@" ] host [ ":" port ] static int parse_authority(Scanner *s, CHTTP_Authority *authority) { CHTTP_String userinfo; { int start = s->cur; CONSUME_OPTIONAL_SEQUENCE(s, is_userinfo); if (s->cur < s->len && s->src[s->cur] == '@') { userinfo = (CHTTP_String) { s->src + start, s->cur - start }; s->cur++; } else { // Rollback s->cur = start; userinfo = (CHTTP_String) {NULL, 0}; } } CHTTP_Host host; { int ret = parse_host(s, &host); if (ret < 0) return ret; } int port = 0; if (s->cur < s->len && s->src[s->cur] == ':') { s->cur++; if (s->cur < s->len && is_digit(s->src[s->cur])) { port = s->src[s->cur++] - '0'; while (s->cur < s->len && is_digit(s->src[s->cur])) { int x = s->src[s->cur++] - '0'; if (port > (UINT16_MAX - x) / 10) return -1; // ERROR: Port too big port = port * 10 + x; } } } authority->userinfo = userinfo; authority->host = host; authority->port = port; return 0; } static int parse_uri(Scanner *s, CHTTP_URL *url, int allow_fragment) { CHTTP_String scheme = {0}; { int start = s->cur; if (s->cur == s->len || !is_scheme_head(s->src[s->cur])) return -1; // ERROR: Missing scheme do s->cur++; while (s->cur < s->len && is_scheme_body(s->src[s->cur])); scheme = (CHTTP_String) { s->src + start, s->cur - start, }; if (s->cur == s->len || s->src[s->cur] != ':') return -1; // ERROR: Missing ':' after scheme s->cur++; } int abempty = 0; CHTTP_Authority authority = {0}; if (s->len - s->cur > 1 && s->src[s->cur+0] == '/' && s->src[s->cur+1] == '/') { s->cur += 2; int ret = parse_authority(s, &authority); if (ret < 0) return ret; abempty = 1; } CHTTP_String path; int ret = parse_path(s, &path, abempty); if (ret < 0) return ret; CHTTP_String query = {0}; if (s->cur < s->len && s->src[s->cur] == '?') { int start = s->cur; do s->cur++; while (s->cur < s->len && is_query(s->src[s->cur])); query = (CHTTP_String) { s->src + start, s->cur - start, }; } CHTTP_String fragment = {0}; if (allow_fragment && s->cur < s->len && s->src[s->cur] == '#') { int start = s->cur; do s->cur++; while (s->cur < s->len && is_fragment(s->src[s->cur])); fragment = (CHTTP_String) { s->src + start, s->cur - start, }; } url->scheme = scheme; url->authority = authority; url->path = path; url->query = query; url->fragment = fragment; return 1; } // authority-form = host ":" port // host = IP-literal / IPv4address / reg-name // IP-literal = "[" ( IPv6address / IPvFuture ) "]" // reg-name = *( unreserved / pct-encoded / sub-delims ) static int parse_authority_form(Scanner *s, CHTTP_Host *host, int *port) { int ret; ret = parse_host(s, host); if (ret < 0) return ret; // Default port value *port = 0; if (s->cur == s->len || s->src[s->cur] != ':') return 0; // No port s->cur++; if (s->cur == s->len || !is_digit(s->src[s->cur])) return 0; // No port int buf = 0; do { int x = s->src[s->cur++] - '0'; if (buf > (UINT16_MAX - x) / 10) return -1; // ERROR buf = buf * 10 + x; } while (s->cur < s->len && is_digit(s->src[s->cur])); *port = buf; return 0; } static int parse_origin_form(Scanner *s, CHTTP_String *path, CHTTP_String *query) { int ret, start; start = s->cur; ret = consume_absolute_path(s); if (ret < 0) return ret; *path = (CHTTP_String) { s->src + start, s->cur - start }; if (s->cur < s->len && s->src[s->cur] == '?') { start = s->cur; do s->cur++; while (s->cur < s->len && is_query(s->src[s->cur])); *query = (CHTTP_String) { s->src + start, s->cur - start }; } else *query = (CHTTP_String) { NULL, 0 }; return 0; } static int parse_asterisk_form(Scanner *s) { if (s->len - s->cur < 2 || s->src[s->cur+0] != '*' || s->src[s->cur+1] != ' ') return -1; s->cur++; return 0; } static int parse_request_target(Scanner *s, CHTTP_URL *url) { int ret; memset(url, 0, sizeof(CHTTP_URL)); // asterisk-form ret = parse_asterisk_form(s); if (ret >= 0) return ret; ret = parse_uri(s, url, 0); if (ret >= 0) return ret; ret = parse_authority_form(s, &url->authority.host, &url->authority.port); if (ret >= 0) return ret; ret = parse_origin_form(s, &url->path, &url->query); if (ret >= 0) return ret; return -1; } bool consume_str(Scanner *scan, CHTTP_String token) { assert(token.len > 0); if (token.len > scan->len - scan->cur) return false; for (int i = 0; i < token.len; i++) if (scan->src[scan->cur + i] != token.ptr[i]) return false; scan->cur += token.len; return true; } static int is_header_body(char c) { return is_vchar(c) || c == ' ' || c == '\t'; } static int parse_headers(Scanner *s, CHTTP_Header *headers, int max_headers) { int num_headers = 0; while (!consume_str(s, CHTTP_STR("\r\n"))) { // RFC 9112: // field-line = field-name ":" OWS field-value OWS // // RFC 9110: // field-value = *field-content // field-content = field-vchar // [ 1*( SP / HTAB / field-vchar ) field-vchar ] // field-vchar = VCHAR / obs-text // obs-text = %x80-FF int start; if (s->cur == s->len || !is_tchar(s->src[s->cur])) return -1; // ERROR start = s->cur; do s->cur++; while (s->cur < s->len && is_tchar(s->src[s->cur])); CHTTP_String name = { s->src + start, s->cur - start }; if (s->cur == s->len || s->src[s->cur] != ':') return -1; // ERROR s->cur++; start = s->cur; CONSUME_OPTIONAL_SEQUENCE(s, is_header_body); CHTTP_String body = { s->src + start, s->cur - start }; body = chttp_trim(body); if (num_headers < max_headers) headers[num_headers++] = (CHTTP_Header) { name, body }; if (!consume_str(s, CHTTP_STR("\r\n"))) { return -1; } } return num_headers; } typedef enum { TRANSFER_ENCODING_OPTION_CHUNKED, TRANSFER_ENCODING_OPTION_COMPRESS, TRANSFER_ENCODING_OPTION_DEFLATE, TRANSFER_ENCODING_OPTION_GZIP, } TransferEncodingOption; static bool is_space(char c) { return c == ' ' || c == '\t'; } static int parse_transfer_encoding(CHTTP_String src, TransferEncodingOption *dst, int max) { Scanner s = { src.ptr, src.len, 0 }; int num = 0; for (;;) { CONSUME_OPTIONAL_SEQUENCE(&s, is_space); TransferEncodingOption opt; if (0) {} else if (consume_str(&s, CHTTP_STR("chunked"))) opt = TRANSFER_ENCODING_OPTION_CHUNKED; else if (consume_str(&s, CHTTP_STR("compress"))) opt = TRANSFER_ENCODING_OPTION_COMPRESS; else if (consume_str(&s, CHTTP_STR("deflate"))) opt = TRANSFER_ENCODING_OPTION_DEFLATE; else if (consume_str(&s, CHTTP_STR("gzip"))) opt = TRANSFER_ENCODING_OPTION_GZIP; else return -1; // Invalid option if (num == max) return -1; // Too many options dst[num++] = opt; CONSUME_OPTIONAL_SEQUENCE(&s, is_space); if (s.cur == s.len) break; if (s.src[s.cur] != ',') return -1; // Missing comma separator } return num; } static int parse_content_length(const char *src, int len, uint64_t *out) { int cur = 0; while (cur < len && (src[cur] == ' ' || src[cur] == '\t')) cur++; if (cur == len || !is_digit(src[cur])) return -1; uint64_t buf = 0; do { int d = src[cur++] - '0'; if (buf > (UINT64_MAX - d) / 10) return -1; buf = buf * 10 + d; } while (cur < len && is_digit(src[cur])); *out = buf; return 0; } static int parse_body(Scanner *s, CHTTP_Header *headers, int num_headers, CHTTP_String *body, bool body_expected) { // RFC 9112 section 6: // The presence of a message body in a request is signaled by a Content-Length or // Transfer-Encoding header field. Request message framing is independent of method // semantics. int header_index = chttp_find_header(headers, num_headers, CHTTP_STR("Transfer-Encoding")); if (header_index != -1) { // RFC 9112 section 6.1: // A server MAY reject a request that contains both Content-Length and Transfer-Encoding // or process such a request in accordance with the Transfer-Encoding alone. Regardless, // the server MUST close the connection after responding to such a request to avoid the // potential attacks. if (chttp_find_header(headers, num_headers, CHTTP_STR("Content-Length")) != -1) return -1; CHTTP_String value = headers[header_index].value; // RFC 9112 section 6.1: // If any transfer coding other than chunked is applied to a request's content, the // sender MUST apply chunked as the final transfer coding to ensure that the message // is properly framed. If any transfer coding other than chunked is applied to a // response's content, the sender MUST either apply chunked as the final transfer // coding or terminate the message by closing the connection. TransferEncodingOption opts[8]; int num = parse_transfer_encoding(value, opts, CHTTP_COUNT(opts)); if (num != 1 || opts[0] != TRANSFER_ENCODING_OPTION_CHUNKED) return -1; CHTTP_String chunks_maybe[128]; CHTTP_String *chunks = chunks_maybe; int num_chunks = 0; int max_chunks = CHTTP_COUNT(chunks_maybe); #define FREE_CHUNK_LIST \ if (chunks != chunks_maybe) \ free(chunks); char *content_start = s->src + s->cur; for (;;) { // RFC 9112 section 7.1: // The chunked transfer coding wraps content in order to transfer it as a series of chunks, // each with its own size indicator, followed by an OPTIONAL trailer section containing // trailer fields. if (s->cur == s->len) { FREE_CHUNK_LIST return 0; // Incomplete request } if (!is_hex_digit(s->src[s->cur])) { FREE_CHUNK_LIST return -1; } int chunk_len = 0; do { char c = s->src[s->cur++]; int n = hex_digit_to_int(c); if (chunk_len > (INT_MAX - n) / 16) { FREE_CHUNK_LIST return -1; // overflow } chunk_len = chunk_len * 16 + n; } while (s->cur < s->len && is_hex_digit(s->src[s->cur])); if (s->cur == s->len) { FREE_CHUNK_LIST return 0; // Incomplete request } if (s->src[s->cur] != '\r') { FREE_CHUNK_LIST return -1; } s->cur++; if (s->cur == s->len) { FREE_CHUNK_LIST return 0; } if (s->src[s->cur] != '\n') { FREE_CHUNK_LIST return -1; } s->cur++; char *chunk_ptr = s->src + s->cur; if (chunk_len > s->len - s->cur) { FREE_CHUNK_LIST return 0; // Incomplete request } s->cur += chunk_len; if (s->cur == s->len) return 0; // Incomplete request if (s->src[s->cur] != '\r') { FREE_CHUNK_LIST return -1; } s->cur++; if (s->cur == s->len) { FREE_CHUNK_LIST return 0; // Incomplete request } if (s->src[s->cur] != '\n') { FREE_CHUNK_LIST return -1; } s->cur++; if (chunk_len == 0) break; if (num_chunks == max_chunks) { max_chunks *= 2; CHTTP_String *new_chunks = malloc(max_chunks * sizeof(CHTTP_String)); if (new_chunks == NULL) { if (chunks != chunks_maybe) free(chunks); return -1; } for (int i = 0; i < num_chunks; i++) new_chunks[i] = chunks[i]; if (chunks != chunks_maybe) free(chunks); chunks = new_chunks; } chunks[num_chunks++] = (CHTTP_String) { chunk_ptr, chunk_len }; } char *content_ptr = content_start; for (int i = 0; i < num_chunks; i++) { memmove(content_ptr, chunks[i].ptr, chunks[i].len); content_ptr += chunks[i].len; } *body = (CHTTP_String) { content_start, content_ptr - content_start }; if (chunks != chunks_maybe) free(chunks); return 1; } // RFC 9112 section 6.3: // If a valid Content-Length header field is present without Transfer-Encoding, // its decimal value defines the expected message body length in octets. header_index = chttp_find_header(headers, num_headers, CHTTP_STR("Content-Length")); if (header_index != -1) { // Have Content-Length CHTTP_String value = headers[header_index].value; uint64_t tmp; if (parse_content_length(value.ptr, value.len, &tmp) < 0) return -1; if (tmp > INT_MAX) return -1; int len = (int) tmp; if (len > s->len - s->cur) return 0; // Incomplete request *body = (CHTTP_String) { s->src + s->cur, len }; s->cur += len; return 1; } // No Content-Length or Transfer-Encoding if (body_expected) return -1; *body = (CHTTP_String) { NULL, 0 }; return 1; } static int contains_head(char *src, int len) { int cur = 0; while (len - cur > 3) { if (src[cur+0] == '\r' && src[cur+1] == '\n' && src[cur+2] == '\r' && src[cur+3] == '\n') return 1; cur++; } return 0; } static int parse_request(Scanner *s, CHTTP_Request *req) { if (!contains_head(s->src + s->cur, s->len - s->cur)) return 0; req->secure = false; if (0) {} else if (consume_str(s, CHTTP_STR("GET "))) req->method = CHTTP_METHOD_GET; else if (consume_str(s, CHTTP_STR("POST "))) req->method = CHTTP_METHOD_POST; else if (consume_str(s, CHTTP_STR("PUT "))) req->method = CHTTP_METHOD_PUT; else if (consume_str(s, CHTTP_STR("HEAD "))) req->method = CHTTP_METHOD_HEAD; else if (consume_str(s, CHTTP_STR("DELETE "))) req->method = CHTTP_METHOD_DELETE; else if (consume_str(s, CHTTP_STR("CONNECT "))) req->method = CHTTP_METHOD_CONNECT; else if (consume_str(s, CHTTP_STR("OPTIONS "))) req->method = CHTTP_METHOD_OPTIONS; else if (consume_str(s, CHTTP_STR("TRACE "))) req->method = CHTTP_METHOD_TRACE; else if (consume_str(s, CHTTP_STR("PATCH "))) req->method = CHTTP_METHOD_PATCH; else return -1; { Scanner s2 = *s; int peek = s->cur; while (peek < s->len && s->src[peek] != ' ') peek++; if (peek == s->len) return -1; s2.len = peek; int ret = parse_request_target(&s2, &req->url); if (ret < 0) return ret; s->cur = s2.cur; } if (consume_str(s, CHTTP_STR(" HTTP/1.1\r\n"))) { req->minor = 1; } else if (consume_str(s, CHTTP_STR(" HTTP/1.0\r\n")) || consume_str(s, CHTTP_STR(" HTTP/1\r\n"))) { req->minor = 0; } else { return -1; } int num_headers = parse_headers(s, req->headers, CHTTP_MAX_HEADERS); if (num_headers < 0) return num_headers; req->num_headers = num_headers; // Request methods that typically don't have a body bool body_expected = true; if (req->method == CHTTP_METHOD_GET || req->method == CHTTP_METHOD_HEAD || req->method == CHTTP_METHOD_DELETE || req->method == CHTTP_METHOD_OPTIONS || req->method == CHTTP_METHOD_TRACE) body_expected = false; return parse_body(s, req->headers, req->num_headers, &req->body, body_expected); } int chttp_find_header(CHTTP_Header *headers, int num_headers, CHTTP_String name) { for (int i = 0; i < num_headers; i++) if (chttp_streqcase(name, headers[i].name)) return i; return -1; } static int parse_response(Scanner *s, CHTTP_Response *res) { if (!contains_head(s->src + s->cur, s->len - s->cur)) return 0; if (consume_str(s, CHTTP_STR("HTTP/1.1 "))) { res->minor = 1; } else if (consume_str(s, CHTTP_STR("HTTP/1.0 ")) || consume_str(s, CHTTP_STR("HTTP/1 "))) { res->minor = 0; } else { return -1; } if (s->len - s->cur < 4 || !is_digit(s->src[s->cur+0]) || !is_digit(s->src[s->cur+1]) || !is_digit(s->src[s->cur+2]) || s->src[s->cur+3] != ' ') return -1; res->status = (s->src[s->cur+0] - '0') * 100 + (s->src[s->cur+1] - '0') * 10 + (s->src[s->cur+2] - '0') * 1; s->cur += 4; // Parse reason phrase: HTAB / SP / VCHAR / obs-text // Note: obs-text (obsolete text, octets 0x80-0xFF) is not validated while (s->cur < s->len && ( s->src[s->cur] == '\t' || s->src[s->cur] == ' ' || is_vchar(s->src[s->cur]))) s->cur++; if (s->len - s->cur < 2 || s->src[s->cur+0] != '\r' || s->src[s->cur+1] != '\n') return -1; s->cur += 2; int num_headers = parse_headers(s, res->headers, CHTTP_MAX_HEADERS); if (num_headers < 0) return num_headers; res->num_headers = num_headers; // Responses with certain status codes don't have a body: // - 1xx (Informational) // - 204 (No Content) // - 304 (Not Modified) // Note: HEAD responses also don't have a body, but we can't determine // that here without access to the request method bool body_expected = true; if ((res->status >= 100 && res->status < 200) || res->status == 204 || res->status == 304) body_expected = false; return parse_body(s, res->headers, res->num_headers, &res->body, body_expected); } int chttp_parse_ipv4(char *src, int len, CHTTP_IPv4 *ipv4) { Scanner s = {src, len, 0}; int ret = parse_ipv4(&s, ipv4); if (ret < 0) return ret; return s.cur; } int chttp_parse_ipv6(char *src, int len, CHTTP_IPv6 *ipv6) { Scanner s = {src, len, 0}; int ret = parse_ipv6(&s, ipv6); if (ret < 0) return ret; return s.cur; } int chttp_parse_url(char *src, int len, CHTTP_URL *url) { Scanner s = {src, len, 0}; int ret = parse_uri(&s, url, 1); if (ret == 1) return s.cur; return ret; } int chttp_parse_request(char *src, int len, CHTTP_Request *req) { Scanner s = {src, len, 0}; int ret = parse_request(&s, req); if (ret == 1) return s.cur; return ret; } int chttp_parse_response(char *src, int len, CHTTP_Response *res) { Scanner s = {src, len, 0}; int ret = parse_response(&s, res); if (ret == 1) return s.cur; return ret; } CHTTP_String chttp_get_cookie(CHTTP_Request *req, CHTTP_String name) { // Simple cookie parsing - does not handle quoted values or special characters // See RFC 6265 for full cookie specification for (int i = 0; i < req->num_headers; i++) { if (!chttp_streqcase(req->headers[i].name, CHTTP_STR("Cookie"))) continue; char *src = req->headers[i].value.ptr; int len = req->headers[i].value.len; int cur = 0; // Cookie: name1=value1; name2=value2; name3=value3 for (;;) { while (cur < len && src[cur] == ' ') cur++; int off = cur; while (cur < len && src[cur] != '=') cur++; CHTTP_String cookie_name = { src + off, cur - off }; if (cur == len) break; cur++; off = cur; while (cur < len && src[cur] != ';') cur++; CHTTP_String cookie_value = { src + off, cur - off }; if (chttp_streq(name, cookie_name)) return cookie_value; if (cur == len) break; cur++; } } return CHTTP_STR(""); } CHTTP_String chttp_get_param(CHTTP_String body, CHTTP_String str, char *mem, int cap) { // This is just a best-effort implementation char *src = body.ptr; int len = body.len; int cur = 0; if (cur < len && src[cur] == '?') cur++; while (cur < len) { CHTTP_String name; { int off = cur; while (cur < len && src[cur] != '=' && src[cur] != '&') cur++; name = (CHTTP_String) { src + off, cur - off }; } CHTTP_String body = CHTTP_STR(""); if (cur < len) { cur++; if (src[cur-1] == '=') { int off = cur; while (cur < len && src[cur] != '&') cur++; body = (CHTTP_String) { src + off, cur - off }; if (cur < len) cur++; } } if (chttp_streq(str, name)) { bool percent_encoded = false; for (int i = 0; i < body.len; i++) if (body.ptr[i] == '+' || body.ptr[i] == '%') { percent_encoded = true; break; } if (!percent_encoded) return body; if (body.len > cap) return (CHTTP_String) { NULL, 0 }; CHTTP_String decoded = { mem, 0 }; for (int i = 0; i < body.len; i++) { char c = body.ptr[i]; if (c == '+') c = ' '; else { if (body.ptr[i] == '%') { if (body.len - i < 3 || !is_hex_digit(body.ptr[i+1]) || !is_hex_digit(body.ptr[i+2])) return (CHTTP_String) { NULL, 0 }; int h = hex_digit_to_int(body.ptr[i+1]); int l = hex_digit_to_int(body.ptr[i+2]); c = (h << 4) | l; i += 2; } } decoded.ptr[decoded.len++] = c; } return decoded; } } return CHTTP_STR(""); } int chttp_get_param_i(CHTTP_String body, CHTTP_String str) { char buf[128]; CHTTP_String out = chttp_get_param(body, str, buf, (int) sizeof(buf)); if (out.len == 0 || !is_digit(out.ptr[0])) return -1; int cur = 0; int res = 0; do { int d = out.ptr[cur++] - '0'; if (res > (INT_MAX - d) / 10) return -1; res = res * 10 + d; } while (cur < out.len && is_digit(out.ptr[cur])); return res; } bool chttp_match_host(CHTTP_Request *req, CHTTP_String domain, int port) { int idx = chttp_find_header(req->headers, req->num_headers, CHTTP_STR("Host")); if (idx < 0) return false; char tmp[1<<8]; if (port > -1 && port != 80) { int ret = snprintf(tmp, sizeof(tmp), "%.*s:%d", domain.len, domain.ptr, port); assert(ret > 0); domain = (CHTTP_String) { tmp, ret }; } CHTTP_String host = req->headers[idx].value; return chttp_streqcase(host, domain); } // , :: GMT static int parse_date(Scanner *s, CHTTP_Date *out) { struct { CHTTP_String str; CHTTP_WeekDay val; } week_day_table[] = { { CHTTP_STR("Mon, "), CHTTP_WEEKDAY_MON }, { CHTTP_STR("Tue, "), CHTTP_WEEKDAY_TUE }, { CHTTP_STR("Wed, "), CHTTP_WEEKDAY_WED }, { CHTTP_STR("Thu, "), CHTTP_WEEKDAY_THU }, { CHTTP_STR("Fri, "), CHTTP_WEEKDAY_FRI }, { CHTTP_STR("Sat, "), CHTTP_WEEKDAY_SAT }, { CHTTP_STR("Sun, "), CHTTP_WEEKDAY_SUN }, }; bool found = false; for (int i = 0; i < CHTTP_COUNT(week_day_table); i++) if (consume_str(s, week_day_table[i].str)) { out->week_day = week_day_table[i].val; found = true; break; } if (!found) return -1; if (1 >= s->len - s->cur || !is_digit(s->src[s->cur+0]) || !is_digit(s->src[s->cur+1])) return -1; out->day = (s->src[s->cur+0] - '0') * 10 + (s->src[s->cur+1] - '0') * 1; s->cur += 2; struct { CHTTP_String str; CHTTP_Month val; } month_table[] = { { CHTTP_STR(" Jan "), CHTTP_MONTH_JAN }, { CHTTP_STR(" Feb "), CHTTP_MONTH_FEB }, { CHTTP_STR(" Mar "), CHTTP_MONTH_MAR }, { CHTTP_STR(" Apr "), CHTTP_MONTH_APR }, { CHTTP_STR(" May "), CHTTP_MONTH_MAY }, { CHTTP_STR(" Jun "), CHTTP_MONTH_JUN }, { CHTTP_STR(" Jul "), CHTTP_MONTH_JUL }, { CHTTP_STR(" Aug "), CHTTP_MONTH_AUG }, { CHTTP_STR(" Sep "), CHTTP_MONTH_SEP }, { CHTTP_STR(" Oct "), CHTTP_MONTH_OCT }, { CHTTP_STR(" Nov "), CHTTP_MONTH_NOV }, { CHTTP_STR(" Dec "), CHTTP_MONTH_DEC }, }; found = false; for (int i = 0; i < CHTTP_COUNT(month_table); i++) if (consume_str(s, month_table[i].str)) { out->month = month_table[i].val; found = true; break; } if (!found) return -1; if (3 >= s->len - s->cur || !is_digit(s->src[s->cur+0]) || !is_digit(s->src[s->cur+1]) || !is_digit(s->src[s->cur+2]) || !is_digit(s->src[s->cur+3])) return -1; out->year = (s->src[s->cur+0] - '0') * 1000 + (s->src[s->cur+1] - '0') * 100 + (s->src[s->cur+2] - '0') * 10 + (s->src[s->cur+3] - '0') * 1; s->cur += 4; if (s->cur == s->len || s->src[s->cur] != ' ') return -1; s->cur++; if (7 >= s->len - s->cur || !is_digit(s->src[s->cur+0]) || !is_digit(s->src[s->cur+1]) || s->src[s->cur+2] != ':' || !is_digit(s->src[s->cur+3]) || !is_digit(s->src[s->cur+4]) || s->src[s->cur+5] != ':' || !is_digit(s->src[s->cur+6]) || !is_digit(s->src[s->cur+7]) || s->src[s->cur+8] != ' ' || s->src[s->cur+9] != 'G' || s->src[s->cur+10] != 'M' || s->src[s->cur+11] != 'T') return -1; out->hour = (s->src[s->cur+0] - '0') * 10 + (s->src[s->cur+1] - '0') * 1; out->minute = (s->src[s->cur+3] - '0') * 10 + (s->src[s->cur+4] - '0') * 1; out->second = (s->src[s->cur+6] - '0') * 10 + (s->src[s->cur+7] - '0') * 1; s->cur += 12; return 0; } // cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E // ; US-ASCII characters excluding CTLs, // ; whitespace, DQUOTE, comma, semicolon, // ; and backslash static bool is_cookie_octet(char c) { return c == 0x21 || (c >= 0x23 && c <= 0x2B) || (c >= 0x2D && c <= 0x3A) || (c >= 0x3C && c <= 0x5B) || (c >= 0x5D && c <= 0x7E); } int chttp_parse_set_cookie(CHTTP_String str, CHTTP_SetCookie *out) { Scanner s = { str.ptr, str.len, 0 }; // cookie-name = token if (s.cur == s.len || !is_tchar(s.src[s.cur])) return -1; int off = s.cur; do s.cur++; while (s.cur < s.len && is_tchar(s.src[s.cur])); out->name = (CHTTP_String) { s.src + off, s.cur - off }; // cookie-pair = cookie-name "=" cookie-value if (s.cur == s.len || s.src[s.cur] != '=') return -1; s.cur++; // cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) if (s.cur < s.len && s.src[s.cur] == '"') { s.cur++; // Consume opening double quote int off = s.cur; while (s.cur < s.len && is_cookie_octet(s.src[s.cur])) s.cur++; if (s.cur == s.len || s.src[s.cur] != '"') return -1; // Missing closing double quote out->value = (CHTTP_String) { s.src + off, s.cur - off }; s.cur++; // Consume closing double quote } else { int off = s.cur; while (s.cur < s.len && is_cookie_octet(s.src[s.cur])) s.cur++; out->value = (CHTTP_String) { s.src + off, s.cur - off }; } // *( ";" SP cookie-av ) // // cookie-av = expires-av / max-age-av / domain-av / // path-av / secure-av / httponly-av / // extension-av out->secure = false; out->chttp_only = false; out->have_date = false; out->have_max_age = false; out->have_domain = false; out->have_path = false; while (consume_str(&s, CHTTP_STR("; "))) { if (consume_str(&s, CHTTP_STR("Expires="))) { // expires-av = "Expires=" sane-cookie-date if (parse_date(&s, &out->date) < 0) return -1; out->have_date = true; } else if (consume_str(&s, CHTTP_STR("Max-Age="))) { // max-age-av = "Max-Age=" non-zero-digit *DIGIT uint32_t value = 0; if (s.cur == s.len || !is_digit(s.src[s.cur])) return -1; do { int d = s.src[s.cur++] - '0'; if (value > (UINT32_MAX - d) / 10) return -1; value = value * 10 + d; } while (s.cur < s.len && is_digit(s.src[s.cur])); out->have_max_age = true; out->max_age = value; } else if (consume_str(&s, CHTTP_STR("Domain="))) { // domain-av = "Domain=" domain-value // domain-value = // ; defined in RFC 1034, Section 3.5 // // From RFC 1034: // ::=