Add support for Transfer-Encoding

This commit is contained in:
2025-07-28 16:32:31 +02:00
parent 5d12070a29
commit a63e68f969
9 changed files with 959 additions and 625 deletions
+1
View File
@@ -10,3 +10,4 @@ coverage_report
*.o
*.a
*.so
*.pem
+6
View File
@@ -21,6 +21,12 @@ else
LFLAGS += -lssl -lcrypto
endif
HTTPS = 1
ifeq ($(HTTPS),1)
CFLAGS += -DHTTPS_ENABLED
LFLAGS += -lssl -lcrypto
endif
# Installation directories
PREFIX ?= /usr/local
LIBDIR = $(PREFIX)/lib
+346 -210
View File
@@ -365,6 +365,8 @@ void print_bytes(HTTP_String prefix, HTTP_String src)
#include <stdint.h>
#include <string.h>
#include <assert.h>
#include <stdlib.h>
#include <limits.h>
#ifndef HTTP_AMALGAMATION
#include "parse.h"
@@ -372,43 +374,43 @@ void print_bytes(HTTP_String prefix, HTTP_String src)
#endif
// 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 = <URI-reference, see [URI], Section 4.1>
// absolute-URI = <absolute-URI, see [URI], Section 4.3>
// relative-part = <relative-part, see [URI], Section 4.2>
// authority = <authority, see [URI], Section 3.2>
// uri-host = <host, see [URI], Section 3.2.2>
// port = <port, see [URI], Section 3.2.3>
// path-abempty = <path-abempty, see [URI], Section 3.3>
// segment = <segment, see [URI], Section 3.3>
// query = <query, see [URI], Section 3.4>
//
// 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 / "+" / "-" / "." )
// 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 = <URI-reference, see [URI], Section 4.1>
// absolute-URI = <absolute-URI, see [URI], Section 4.3>
// relative-part = <relative-part, see [URI], Section 4.2>
// authority = <authority, see [URI], Section 3.2>
// uri-host = <host, see [URI], Section 3.2.2>
// port = <port, see [URI], Section 3.2.3>
// path-abempty = <path-abempty, see [URI], Section 3.3>
// segment = <segment, see [URI], Section 3.3>
// query = <query, see [URI], Section 3.4>
//
// 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;
@@ -472,6 +474,10 @@ 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)
{
@@ -481,8 +487,7 @@ consume_absolute_path(Scanner *s)
for (;;) {
while (s->cur < s->len && is_pchar(s->src[s->cur]))
s->cur++;
CONSUME_OPTIONAL_SEQUENCE(s, is_pchar);
if (s->cur == s->len || s->src[s->cur] != '/')
break;
@@ -519,8 +524,7 @@ static int parse_path(Scanner *s, HTTP_String *path, int abempty)
s->cur++;
for (;;) {
while (s->cur < s->len && is_pchar(s->src[s->cur]))
s->cur++;
CONSUME_OPTIONAL_SEQUENCE(s, is_pchar);
if (s->cur == s->len || s->src[s->cur] != '/')
break;
@@ -534,8 +538,7 @@ static int parse_path(Scanner *s, HTTP_String *path, int abempty)
s->cur++;
for (;;) {
while (s->cur < s->len && is_pchar(s->src[s->cur]))
s->cur++;
CONSUME_OPTIONAL_SEQUENCE(s, is_pchar)
if (s->cur == s->len || s->src[s->cur] != '/')
break;
@@ -815,8 +818,9 @@ static int parse_authority(Scanner *s, HTTP_Authority *authority)
HTTP_String userinfo;
{
int start = s->cur;
while (s->cur < s->len && is_userinfo(s->src[s->cur]))
s->cur++;
CONSUME_OPTIONAL_SEQUENCE(s, is_userinfo);
if (s->cur < s->len && s->src[s->cur] == '@') {
userinfo = (HTTP_String) {
s->src + start,
@@ -1014,6 +1018,21 @@ static int parse_request_target(Scanner *s, HTTP_URL *url)
return -1;
}
bool consume_str(Scanner *scan, HTTP_String token)
{
HTTP_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';
@@ -1022,14 +1041,7 @@ static int is_header_body(char c)
static int parse_headers(Scanner *s, HTTP_Header *headers, int max_headers)
{
int num_headers = 0;
for (;;) {
if (s->len - s->cur > 1
&& s->src[s->cur+0] == '\r'
&& s->src[s->cur+1] == '\n') {
s->cur += 2;
break;
}
while (!consume_str(s, HTTP_STR("\r\n"))) {
// RFC 9112:
// field-line = field-name ":" OWS field-value OWS
@@ -1056,26 +1068,69 @@ static int parse_headers(Scanner *s, HTTP_Header *headers, int max_headers)
s->cur++;
start = s->cur;
while (s->cur < s->len && is_header_body(s->src[s->cur]))
s->cur++;
CONSUME_OPTIONAL_SEQUENCE(s, is_header_body);
HTTP_String body = { s->src + start, s->cur - start };
body = http_trim(body);
if (s->len - s->cur < 2
|| s->src[s->cur+0] != '\r'
|| s->src[s->cur+1] != '\n')
return -1; // ERROR
s->cur += 2;
if (num_headers < max_headers)
headers[num_headers++] = (HTTP_Header) { name, body };
if (!consume_str(s, HTTP_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_content_length(const char *src, int len, unsigned long long *out)
parse_transfer_encoding(HTTP_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, HTTP_STR("chunked"))) opt = TRANSFER_ENCODING_OPTION_CHUNKED;
else if (consume_str(&s, HTTP_STR("compress"))) opt = TRANSFER_ENCODING_OPTION_COMPRESS;
else if (consume_str(&s, HTTP_STR("deflate"))) opt = TRANSFER_ENCODING_OPTION_DEFLATE;
else if (consume_str(&s, HTTP_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'))
@@ -1084,7 +1139,7 @@ parse_content_length(const char *src, int len, unsigned long long *out)
if (cur == len || !is_digit(src[cur]))
return -1;
unsigned long long buf = 0;
uint64_t buf = 0;
do {
int d = src[cur++] - '0';
if (buf > (UINT64_MAX - d) / 10)
@@ -1096,6 +1151,202 @@ parse_content_length(const char *src, int len, unsigned long long *out)
return 0;
}
static int parse_body(Scanner *s,
HTTP_Header *headers, int num_headers,
HTTP_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 = http_find_header(headers, num_headers, HTTP_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 (http_find_header(headers, num_headers, HTTP_STR("Content-Length")) != -1)
return -1;
HTTP_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, HTTP_COUNT(opts));
if (num != 1 || opts[0] != TRANSFER_ENCODING_OPTION_CHUNKED)
return -1;
HTTP_String chunks_maybe[128];
HTTP_String *chunks = chunks_maybe;
int num_chunks = 0;
int max_chunks = HTTP_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;
HTTP_String *new_chunks = malloc(max_chunks * sizeof(HTTP_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++] = (HTTP_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 = (HTTP_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 = http_find_header(headers, num_headers, HTTP_STR("Content-Length"));
if (header_index != -1) {
// Have Content-Length
HTTP_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 = (HTTP_String) { s->src + s->cur, len };
s->cur += len;
return 1;
}
// No Content-Length or Transfer-Encoding
if (body_expected) return -1;
*body = (HTTP_String) { NULL, 0 };
return 1;
}
static int contains_head(char *src, int len)
{
int cur = 0;
@@ -1115,80 +1366,17 @@ static int parse_request(Scanner *s, HTTP_Request *req)
if (!contains_head(s->src + s->cur, s->len - s->cur))
return 0;
if (s->len - s->cur >= 3
&& s->src[s->cur+0] == 'G'
&& s->src[s->cur+1] == 'E'
&& s->src[s->cur+2] == 'T') {
s->cur += 3;
req->method = HTTP_METHOD_GET;
} else if (s->len - s->cur >= 4
&& s->src[s->cur+0] == 'P'
&& s->src[s->cur+1] == 'O'
&& s->src[s->cur+2] == 'S'
&& s->src[s->cur+3] == 'T') {
s->cur += 4;
req->method = HTTP_METHOD_POST;
} else if (s->len - s->cur >= 3
&& s->src[s->cur+0] == 'P'
&& s->src[s->cur+1] == 'U'
&& s->src[s->cur+2] == 'T') {
s->cur += 3;
req->method = HTTP_METHOD_PUT;
} else if (s->len - s->cur >= 4
&& s->src[s->cur+0] == 'H'
&& s->src[s->cur+1] == 'E'
&& s->src[s->cur+2] == 'A'
&& s->src[s->cur+3] == 'D') {
s->cur += 4;
req->method = HTTP_METHOD_HEAD;
} else if (s->len - s->cur >= 6
&& s->src[s->cur+0] == 'D'
&& s->src[s->cur+1] == 'E'
&& s->src[s->cur+2] == 'L'
&& s->src[s->cur+3] == 'E'
&& s->src[s->cur+4] == 'T'
&& s->src[s->cur+5] == 'E') {
s->cur += 6;
req->method = HTTP_METHOD_DELETE;
} else if (s->len - s->cur >= 7
&& s->src[s->cur+0] == 'C'
&& s->src[s->cur+1] == 'O'
&& s->src[s->cur+2] == 'N'
&& s->src[s->cur+3] == 'N'
&& s->src[s->cur+4] == 'E'
&& s->src[s->cur+5] == 'C'
&& s->src[s->cur+6] == 'T') {
s->cur += 7;
req->method = HTTP_METHOD_CONNECT;
} else if (s->len - s->cur >= 7
&& s->src[s->cur+0] == 'O'
&& s->src[s->cur+1] == 'P'
&& s->src[s->cur+2] == 'T'
&& s->src[s->cur+3] == 'I'
&& s->src[s->cur+4] == 'O'
&& s->src[s->cur+5] == 'N'
&& s->src[s->cur+6] == 'S') {
s->cur += 7;
req->method = HTTP_METHOD_OPTIONS;
} else if (s->len - s->cur >= 5
&& s->src[s->cur+0] == 'T'
&& s->src[s->cur+1] == 'R'
&& s->src[s->cur+2] == 'A'
&& s->src[s->cur+3] == 'C'
&& s->src[s->cur+4] == 'E') {
s->cur += 5;
req->method = HTTP_METHOD_TRACE;
} else if (s->len - s->cur >= 5
&& s->src[s->cur+0] == 'P'
&& s->src[s->cur+1] == 'A'
&& s->src[s->cur+2] == 'T'
&& s->src[s->cur+3] == 'C'
&& s->src[s->cur+4] == 'H') {
s->cur += 5;
req->method = HTTP_METHOD_PATCH;
} else {
return -1;
}
if (0) {}
else if (consume_str(s, HTTP_STR("GET "))) req->method = HTTP_METHOD_GET;
else if (consume_str(s, HTTP_STR("POST "))) req->method = HTTP_METHOD_POST;
else if (consume_str(s, HTTP_STR("PUT "))) req->method = HTTP_METHOD_PUT;
else if (consume_str(s, HTTP_STR("HEAD "))) req->method = HTTP_METHOD_HEAD;
else if (consume_str(s, HTTP_STR("DELETE "))) req->method = HTTP_METHOD_DELETE;
else if (consume_str(s, HTTP_STR("CONNECT "))) req->method = HTTP_METHOD_CONNECT;
else if (consume_str(s, HTTP_STR("OPTIONS "))) req->method = HTTP_METHOD_OPTIONS;
else if (consume_str(s, HTTP_STR("TRACE "))) req->method = HTTP_METHOD_TRACE;
else if (consume_str(s, HTTP_STR("PATCH "))) req->method = HTTP_METHOD_PATCH;
else return -1;
if (s->cur == s->len || s->src[s->cur] != ' ')
return -1;
@@ -1209,33 +1397,12 @@ static int parse_request(Scanner *s, HTTP_Request *req)
s->cur = s2.cur;
}
{
if (s->len - s->cur < 7
|| s->src[s->cur+0] != ' '
|| s->src[s->cur+1] != 'H'
|| s->src[s->cur+2] != 'T'
|| s->src[s->cur+3] != 'T'
|| s->src[s->cur+4] != 'P'
|| s->src[s->cur+5] != '/'
|| s->src[s->cur+6] != '1')
return -1; // ERROR
s->cur += 7;
if (s->cur == s->len || s->src[s->cur] != '.')
if (consume_str(s, HTTP_STR(" HTTP/1.1\r\n"))) {
req->minor = 1;
} else if (consume_str(s, HTTP_STR(" HTTP/1.0\r\n")) || consume_str(s, HTTP_STR(" HTTP/1\r\n"))) {
req->minor = 0;
else {
s->cur++;
if (s->cur == s->len || !is_digit(s->src[s->cur]))
return -1; // ERROR;
req->minor = s->src[s->cur] - '0';
s->cur++;
}
if (s->len - s->cur < 2
|| s->src[s->cur+0] != '\r'
|| s->src[s->cur+1] != '\n')
return -1; // ERROR
s->cur += 2;
} else {
return -1;
}
int num_headers = parse_headers(s, req->headers, HTTP_MAX_HEADERS);
@@ -1243,8 +1410,11 @@ static int parse_request(Scanner *s, HTTP_Request *req)
return num_headers;
req->num_headers = num_headers;
// TODO
return 1;
bool body_expected = true;
if (req->method == HTTP_METHOD_GET || req->method == HTTP_METHOD_DELETE) // TODO: maybe other methods?
body_expected = false;
return parse_body(s, req->headers, req->num_headers, &req->body, body_expected);
}
int http_find_header(HTTP_Header *headers, int num_headers, HTTP_String name)
@@ -1260,24 +1430,12 @@ static int parse_response(Scanner *s, HTTP_Response *res)
if (!contains_head(s->src + s->cur, s->len - s->cur))
return 0;
if (s->len - s->cur < 6
|| s->src[s->cur+0] != 'H'
|| s->src[s->cur+1] != 'T'
|| s->src[s->cur+2] != 'T'
|| s->src[s->cur+3] != 'P'
|| s->src[s->cur+4] != '/'
|| s->src[s->cur+5] != '1')
return -1; // ERROR
s->cur += 6;
if (s->cur == s->len || s->src[s->cur] != '.')
if (consume_str(s, HTTP_STR("HTTP/1.1 "))) {
res->minor = 1;
} else if (consume_str(s, HTTP_STR("HTTP/1.0 ")) || consume_str(s, HTTP_STR("HTTP/1 "))) {
res->minor = 0;
else {
s->cur++;
if (s->cur == s->len || !is_digit(s->src[s->cur]))
return -1; // ERROR
res->minor = s->src[s->cur] - '0';
s->cur++;
} else {
return -1;
}
if (s->len - s->cur < 5
@@ -1311,34 +1469,9 @@ static int parse_response(Scanner *s, HTTP_Response *res)
return num_headers;
res->num_headers = num_headers;
int content_length_index = http_find_header(
res->headers, res->num_headers,
HTTP_STR("Content-Length"));
if (content_length_index == -1) {
res->body.ptr = NULL;
res->body.len = 0;
return 1;
}
bool body_expected = true; // TODO
// TODO: transfer-encoding
HTTP_String content_length_str = res->headers[content_length_index].value;
unsigned long long content_length;
if (parse_content_length(content_length_str.ptr, content_length_str.len, &content_length) < 0) {
HTTP_ASSERT(0); // TODO
}
if (content_length > 1<<20) {
HTTP_ASSERT(0); // TODO
}
if (content_length > (unsigned long long) (s->len - s->cur))
return 0;
res->body.ptr = s->src + s->cur;
res->body.len = content_length;
return 1;
return parse_body(s, res->headers, res->num_headers, &res->body, body_expected);
}
int http_parse_ipv4(char *src, int len, HTTP_IPv4 *ipv4)
@@ -2418,7 +2551,7 @@ static EVP_PKEY *generate_rsa_key_pair(int key_bits)
return NULL;
}
EVP_PKEY *pkey;
EVP_PKEY *pkey = NULL;
if (EVP_PKEY_keygen(ctx, &pkey) <= 0) {
EVP_PKEY_CTX_free(ctx);
return NULL;
@@ -4420,6 +4553,9 @@ void http_request_builder_submit(HTTP_RequestBuilder handle)
void http_response_free(HTTP_Response *res)
{
if (res == NULL)
return;
HTTP_Client *client = res->context;
ClientConnection *conn = NULL;
+51
View File
@@ -0,0 +1,51 @@
#include <stddef.h>
#include <chttp.h>
int main(void)
{
http_global_init();
HTTP_Client *client = http_client_init();
if (client == NULL)
return -1;
HTTP_Response *responses[2];
HTTP_String urls[] = {
HTTP_STR("http://coz.is"),
HTTP_STR("http://coz.is"),
};
bool trace = false;
HTTP_RequestBuilder builder;
if (http_client_get_builder(client, &builder) < 0) return -1;
http_request_builder_trace(builder, trace);
http_request_builder_user_data(builder, &responses[0]);
http_request_builder_line(builder, HTTP_METHOD_GET, urls[0]);
http_request_builder_submit(builder);
if (http_client_get_builder(client, &builder) < 0) return -1;
http_request_builder_trace(builder, trace);
http_request_builder_user_data(builder, &responses[1]);
http_request_builder_line(builder, HTTP_METHOD_GET, urls[1]);
http_request_builder_submit(builder);
for (int i = 0; i < 2; i++) {
void **dst;
HTTP_Response *output;
if (http_client_wait(client, &output, (void*) &dst) < 0)
return -1;
*dst = output;
}
HTTP_Response *responseA = responses[0];
HTTP_Response *responseB = responses[1];
// ... process responses ...
http_response_free(responseA);
http_response_free(responseB);
http_client_free(client);
http_global_free();
return 0;
}
+2
View File
@@ -158,6 +158,8 @@ int main(int argc, char **argv)
// TODO
return -1;
}
if (res == NULL)
continue;
HTTP_String body = res->body;
+3 -1
View File
@@ -1,10 +1,12 @@
Allow resolving requests from different threads
Find a way to make sure chttp.h and chttp.c are always up to date
parse Transfer-Encoding
Find a way to compile OpenSSL on windows
handle 3xx client redirections
add discussion on string management
add discussion on error management
check that virtual hosts over HTTPS work
add examples for the router
add timers
add debug and release builds
make it easy to switch on and off HTTPS when building
+1 -1
View File
@@ -33,7 +33,7 @@ static EVP_PKEY *generate_rsa_key_pair(int key_bits)
return NULL;
}
EVP_PKEY *pkey;
EVP_PKEY *pkey = NULL;
if (EVP_PKEY_keygen(ctx, &pkey) <= 0) {
EVP_PKEY_CTX_free(ctx);
return NULL;
+3
View File
@@ -368,6 +368,9 @@ void http_request_builder_submit(HTTP_RequestBuilder handle)
void http_response_free(HTTP_Response *res)
{
if (res == NULL)
return;
HTTP_Client *client = res->context;
ClientConnection *conn = NULL;
+342 -209
View File
@@ -2,6 +2,8 @@
#include <stdint.h>
#include <string.h>
#include <assert.h>
#include <stdlib.h>
#include <limits.h>
#ifndef HTTP_AMALGAMATION
#include "parse.h"
@@ -9,43 +11,43 @@
#endif
// 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 = <URI-reference, see [URI], Section 4.1>
// absolute-URI = <absolute-URI, see [URI], Section 4.3>
// relative-part = <relative-part, see [URI], Section 4.2>
// authority = <authority, see [URI], Section 3.2>
// uri-host = <host, see [URI], Section 3.2.2>
// port = <port, see [URI], Section 3.2.3>
// path-abempty = <path-abempty, see [URI], Section 3.3>
// segment = <segment, see [URI], Section 3.3>
// query = <query, see [URI], Section 3.4>
//
// 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 / "+" / "-" / "." )
// 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 = <URI-reference, see [URI], Section 4.1>
// absolute-URI = <absolute-URI, see [URI], Section 4.3>
// relative-part = <relative-part, see [URI], Section 4.2>
// authority = <authority, see [URI], Section 3.2>
// uri-host = <host, see [URI], Section 3.2.2>
// port = <port, see [URI], Section 3.2.3>
// path-abempty = <path-abempty, see [URI], Section 3.3>
// segment = <segment, see [URI], Section 3.3>
// query = <query, see [URI], Section 3.4>
//
// 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;
@@ -109,6 +111,10 @@ 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)
{
@@ -118,8 +124,7 @@ consume_absolute_path(Scanner *s)
for (;;) {
while (s->cur < s->len && is_pchar(s->src[s->cur]))
s->cur++;
CONSUME_OPTIONAL_SEQUENCE(s, is_pchar);
if (s->cur == s->len || s->src[s->cur] != '/')
break;
@@ -156,8 +161,7 @@ static int parse_path(Scanner *s, HTTP_String *path, int abempty)
s->cur++;
for (;;) {
while (s->cur < s->len && is_pchar(s->src[s->cur]))
s->cur++;
CONSUME_OPTIONAL_SEQUENCE(s, is_pchar);
if (s->cur == s->len || s->src[s->cur] != '/')
break;
@@ -171,8 +175,7 @@ static int parse_path(Scanner *s, HTTP_String *path, int abempty)
s->cur++;
for (;;) {
while (s->cur < s->len && is_pchar(s->src[s->cur]))
s->cur++;
CONSUME_OPTIONAL_SEQUENCE(s, is_pchar)
if (s->cur == s->len || s->src[s->cur] != '/')
break;
@@ -452,8 +455,9 @@ static int parse_authority(Scanner *s, HTTP_Authority *authority)
HTTP_String userinfo;
{
int start = s->cur;
while (s->cur < s->len && is_userinfo(s->src[s->cur]))
s->cur++;
CONSUME_OPTIONAL_SEQUENCE(s, is_userinfo);
if (s->cur < s->len && s->src[s->cur] == '@') {
userinfo = (HTTP_String) {
s->src + start,
@@ -651,6 +655,21 @@ static int parse_request_target(Scanner *s, HTTP_URL *url)
return -1;
}
bool consume_str(Scanner *scan, HTTP_String token)
{
HTTP_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';
@@ -659,14 +678,7 @@ static int is_header_body(char c)
static int parse_headers(Scanner *s, HTTP_Header *headers, int max_headers)
{
int num_headers = 0;
for (;;) {
if (s->len - s->cur > 1
&& s->src[s->cur+0] == '\r'
&& s->src[s->cur+1] == '\n') {
s->cur += 2;
break;
}
while (!consume_str(s, HTTP_STR("\r\n"))) {
// RFC 9112:
// field-line = field-name ":" OWS field-value OWS
@@ -693,26 +705,69 @@ static int parse_headers(Scanner *s, HTTP_Header *headers, int max_headers)
s->cur++;
start = s->cur;
while (s->cur < s->len && is_header_body(s->src[s->cur]))
s->cur++;
CONSUME_OPTIONAL_SEQUENCE(s, is_header_body);
HTTP_String body = { s->src + start, s->cur - start };
body = http_trim(body);
if (s->len - s->cur < 2
|| s->src[s->cur+0] != '\r'
|| s->src[s->cur+1] != '\n')
return -1; // ERROR
s->cur += 2;
if (num_headers < max_headers)
headers[num_headers++] = (HTTP_Header) { name, body };
if (!consume_str(s, HTTP_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_content_length(const char *src, int len, unsigned long long *out)
parse_transfer_encoding(HTTP_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, HTTP_STR("chunked"))) opt = TRANSFER_ENCODING_OPTION_CHUNKED;
else if (consume_str(&s, HTTP_STR("compress"))) opt = TRANSFER_ENCODING_OPTION_COMPRESS;
else if (consume_str(&s, HTTP_STR("deflate"))) opt = TRANSFER_ENCODING_OPTION_DEFLATE;
else if (consume_str(&s, HTTP_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'))
@@ -721,7 +776,7 @@ parse_content_length(const char *src, int len, unsigned long long *out)
if (cur == len || !is_digit(src[cur]))
return -1;
unsigned long long buf = 0;
uint64_t buf = 0;
do {
int d = src[cur++] - '0';
if (buf > (UINT64_MAX - d) / 10)
@@ -733,6 +788,202 @@ parse_content_length(const char *src, int len, unsigned long long *out)
return 0;
}
static int parse_body(Scanner *s,
HTTP_Header *headers, int num_headers,
HTTP_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 = http_find_header(headers, num_headers, HTTP_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 (http_find_header(headers, num_headers, HTTP_STR("Content-Length")) != -1)
return -1;
HTTP_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, HTTP_COUNT(opts));
if (num != 1 || opts[0] != TRANSFER_ENCODING_OPTION_CHUNKED)
return -1;
HTTP_String chunks_maybe[128];
HTTP_String *chunks = chunks_maybe;
int num_chunks = 0;
int max_chunks = HTTP_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;
HTTP_String *new_chunks = malloc(max_chunks * sizeof(HTTP_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++] = (HTTP_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 = (HTTP_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 = http_find_header(headers, num_headers, HTTP_STR("Content-Length"));
if (header_index != -1) {
// Have Content-Length
HTTP_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 = (HTTP_String) { s->src + s->cur, len };
s->cur += len;
return 1;
}
// No Content-Length or Transfer-Encoding
if (body_expected) return -1;
*body = (HTTP_String) { NULL, 0 };
return 1;
}
static int contains_head(char *src, int len)
{
int cur = 0;
@@ -752,80 +1003,17 @@ static int parse_request(Scanner *s, HTTP_Request *req)
if (!contains_head(s->src + s->cur, s->len - s->cur))
return 0;
if (s->len - s->cur >= 3
&& s->src[s->cur+0] == 'G'
&& s->src[s->cur+1] == 'E'
&& s->src[s->cur+2] == 'T') {
s->cur += 3;
req->method = HTTP_METHOD_GET;
} else if (s->len - s->cur >= 4
&& s->src[s->cur+0] == 'P'
&& s->src[s->cur+1] == 'O'
&& s->src[s->cur+2] == 'S'
&& s->src[s->cur+3] == 'T') {
s->cur += 4;
req->method = HTTP_METHOD_POST;
} else if (s->len - s->cur >= 3
&& s->src[s->cur+0] == 'P'
&& s->src[s->cur+1] == 'U'
&& s->src[s->cur+2] == 'T') {
s->cur += 3;
req->method = HTTP_METHOD_PUT;
} else if (s->len - s->cur >= 4
&& s->src[s->cur+0] == 'H'
&& s->src[s->cur+1] == 'E'
&& s->src[s->cur+2] == 'A'
&& s->src[s->cur+3] == 'D') {
s->cur += 4;
req->method = HTTP_METHOD_HEAD;
} else if (s->len - s->cur >= 6
&& s->src[s->cur+0] == 'D'
&& s->src[s->cur+1] == 'E'
&& s->src[s->cur+2] == 'L'
&& s->src[s->cur+3] == 'E'
&& s->src[s->cur+4] == 'T'
&& s->src[s->cur+5] == 'E') {
s->cur += 6;
req->method = HTTP_METHOD_DELETE;
} else if (s->len - s->cur >= 7
&& s->src[s->cur+0] == 'C'
&& s->src[s->cur+1] == 'O'
&& s->src[s->cur+2] == 'N'
&& s->src[s->cur+3] == 'N'
&& s->src[s->cur+4] == 'E'
&& s->src[s->cur+5] == 'C'
&& s->src[s->cur+6] == 'T') {
s->cur += 7;
req->method = HTTP_METHOD_CONNECT;
} else if (s->len - s->cur >= 7
&& s->src[s->cur+0] == 'O'
&& s->src[s->cur+1] == 'P'
&& s->src[s->cur+2] == 'T'
&& s->src[s->cur+3] == 'I'
&& s->src[s->cur+4] == 'O'
&& s->src[s->cur+5] == 'N'
&& s->src[s->cur+6] == 'S') {
s->cur += 7;
req->method = HTTP_METHOD_OPTIONS;
} else if (s->len - s->cur >= 5
&& s->src[s->cur+0] == 'T'
&& s->src[s->cur+1] == 'R'
&& s->src[s->cur+2] == 'A'
&& s->src[s->cur+3] == 'C'
&& s->src[s->cur+4] == 'E') {
s->cur += 5;
req->method = HTTP_METHOD_TRACE;
} else if (s->len - s->cur >= 5
&& s->src[s->cur+0] == 'P'
&& s->src[s->cur+1] == 'A'
&& s->src[s->cur+2] == 'T'
&& s->src[s->cur+3] == 'C'
&& s->src[s->cur+4] == 'H') {
s->cur += 5;
req->method = HTTP_METHOD_PATCH;
} else {
return -1;
}
if (0) {}
else if (consume_str(s, HTTP_STR("GET "))) req->method = HTTP_METHOD_GET;
else if (consume_str(s, HTTP_STR("POST "))) req->method = HTTP_METHOD_POST;
else if (consume_str(s, HTTP_STR("PUT "))) req->method = HTTP_METHOD_PUT;
else if (consume_str(s, HTTP_STR("HEAD "))) req->method = HTTP_METHOD_HEAD;
else if (consume_str(s, HTTP_STR("DELETE "))) req->method = HTTP_METHOD_DELETE;
else if (consume_str(s, HTTP_STR("CONNECT "))) req->method = HTTP_METHOD_CONNECT;
else if (consume_str(s, HTTP_STR("OPTIONS "))) req->method = HTTP_METHOD_OPTIONS;
else if (consume_str(s, HTTP_STR("TRACE "))) req->method = HTTP_METHOD_TRACE;
else if (consume_str(s, HTTP_STR("PATCH "))) req->method = HTTP_METHOD_PATCH;
else return -1;
if (s->cur == s->len || s->src[s->cur] != ' ')
return -1;
@@ -846,33 +1034,12 @@ static int parse_request(Scanner *s, HTTP_Request *req)
s->cur = s2.cur;
}
{
if (s->len - s->cur < 7
|| s->src[s->cur+0] != ' '
|| s->src[s->cur+1] != 'H'
|| s->src[s->cur+2] != 'T'
|| s->src[s->cur+3] != 'T'
|| s->src[s->cur+4] != 'P'
|| s->src[s->cur+5] != '/'
|| s->src[s->cur+6] != '1')
return -1; // ERROR
s->cur += 7;
if (s->cur == s->len || s->src[s->cur] != '.')
if (consume_str(s, HTTP_STR(" HTTP/1.1\r\n"))) {
req->minor = 1;
} else if (consume_str(s, HTTP_STR(" HTTP/1.0\r\n")) || consume_str(s, HTTP_STR(" HTTP/1\r\n"))) {
req->minor = 0;
else {
s->cur++;
if (s->cur == s->len || !is_digit(s->src[s->cur]))
return -1; // ERROR;
req->minor = s->src[s->cur] - '0';
s->cur++;
}
if (s->len - s->cur < 2
|| s->src[s->cur+0] != '\r'
|| s->src[s->cur+1] != '\n')
return -1; // ERROR
s->cur += 2;
} else {
return -1;
}
int num_headers = parse_headers(s, req->headers, HTTP_MAX_HEADERS);
@@ -880,8 +1047,11 @@ static int parse_request(Scanner *s, HTTP_Request *req)
return num_headers;
req->num_headers = num_headers;
// TODO
return 1;
bool body_expected = true;
if (req->method == HTTP_METHOD_GET || req->method == HTTP_METHOD_DELETE) // TODO: maybe other methods?
body_expected = false;
return parse_body(s, req->headers, req->num_headers, &req->body, body_expected);
}
int http_find_header(HTTP_Header *headers, int num_headers, HTTP_String name)
@@ -897,24 +1067,12 @@ static int parse_response(Scanner *s, HTTP_Response *res)
if (!contains_head(s->src + s->cur, s->len - s->cur))
return 0;
if (s->len - s->cur < 6
|| s->src[s->cur+0] != 'H'
|| s->src[s->cur+1] != 'T'
|| s->src[s->cur+2] != 'T'
|| s->src[s->cur+3] != 'P'
|| s->src[s->cur+4] != '/'
|| s->src[s->cur+5] != '1')
return -1; // ERROR
s->cur += 6;
if (s->cur == s->len || s->src[s->cur] != '.')
if (consume_str(s, HTTP_STR("HTTP/1.1 "))) {
res->minor = 1;
} else if (consume_str(s, HTTP_STR("HTTP/1.0 ")) || consume_str(s, HTTP_STR("HTTP/1 "))) {
res->minor = 0;
else {
s->cur++;
if (s->cur == s->len || !is_digit(s->src[s->cur]))
return -1; // ERROR
res->minor = s->src[s->cur] - '0';
s->cur++;
} else {
return -1;
}
if (s->len - s->cur < 5
@@ -948,34 +1106,9 @@ static int parse_response(Scanner *s, HTTP_Response *res)
return num_headers;
res->num_headers = num_headers;
int content_length_index = http_find_header(
res->headers, res->num_headers,
HTTP_STR("Content-Length"));
if (content_length_index == -1) {
res->body.ptr = NULL;
res->body.len = 0;
return 1;
}
bool body_expected = true; // TODO
// TODO: transfer-encoding
HTTP_String content_length_str = res->headers[content_length_index].value;
unsigned long long content_length;
if (parse_content_length(content_length_str.ptr, content_length_str.len, &content_length) < 0) {
HTTP_ASSERT(0); // TODO
}
if (content_length > 1<<20) {
HTTP_ASSERT(0); // TODO
}
if (content_length > (unsigned long long) (s->len - s->cur))
return 0;
res->body.ptr = s->src + s->cur;
res->body.len = content_length;
return 1;
return parse_body(s, res->headers, res->num_headers, &res->body, body_expected);
}
int http_parse_ipv4(char *src, int len, HTTP_IPv4 *ipv4)