diff --git a/.gitignore b/.gitignore index 14378ef..f323d58 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ loop2 loop2.exe 3p/lib/* 3p/src/* -3p/include/* \ No newline at end of file +3p/include/* +build \ No newline at end of file diff --git a/README.md b/README.md index 39321bd..62ea7b2 100644 --- a/README.md +++ b/README.md @@ -45,3 +45,6 @@ int main(void) // If you want to use this code, you probably want to // add some checks! ``` + +## Contributing +The build result is a header and a .c obtained as an amalgamation of all the source files. Any header included in the source files must be guarded by a `#ifndef MICROTCP_AMALGAMATION`. \ No newline at end of file diff --git a/test/loop2.c b/examples/echo_tcp.c similarity index 96% rename from test/loop2.c rename to examples/echo_tcp.c index 914f820..18f809a 100644 --- a/test/loop2.c +++ b/examples/echo_tcp.c @@ -1,64 +1,64 @@ -#include -#include - -int main(void) -{ - microtcp_errcode_t errcode; - - microtcp_t *mtcp = microtcp_create("10.0.0.5", "10.0.0.4", NULL, NULL); - if (mtcp == NULL) { - fprintf(stderr, "Error: Failed to instanciate microtcp stack\n"); - return -1; - } - - uint16_t port = 80; - microtcp_socket_t *server = microtcp_open(mtcp, port, &errcode); - if (errcode) { - fprintf(stderr, "Error: %s\n", microtcp_strerror(errcode)); - microtcp_destroy(mtcp); - return -1; - } - assert(server); - - fprintf(stderr, "Listening on port %d\n", port); - - while (1) { - fprintf(stderr, "About to accept\n"); - microtcp_socket_t *client = microtcp_accept(server, false, &errcode); - if (errcode) { - fprintf(stderr, "Error: %s\n", microtcp_strerror(errcode)); - break; - } - - fprintf(stderr, "Accepted a connection\n"); - - char buffer[1024]; - size_t num = microtcp_recv(client, buffer, sizeof(buffer), false, &errcode); - if (errcode) { - fprintf(stderr, "Error: %s\n", microtcp_strerror(errcode)); - goto handled; - } - fprintf(stderr, "(%d bytes received)\n", (int) num); - - size_t sent1 = microtcp_send(client, "echo: ", 6, false, &errcode); - if (errcode) { - fprintf(stderr, "Error: %s\n", microtcp_strerror(errcode)); - goto handled; - } - fprintf(stderr, "(%d bytes sent 1)\n", (int) sent1); - - size_t sent2 = microtcp_send(client, buffer, num, false, &errcode); - if (errcode) { - fprintf(stderr, "Error: %s\n", microtcp_strerror(errcode)); - goto handled; - } - fprintf(stderr, "(%d bytes sent 2)\n", (int) sent2); - -handled: - microtcp_close(client); - } - - microtcp_close(server); - microtcp_destroy(mtcp); - return 0; -} +#include +#include + +int main(void) +{ + microtcp_errcode_t errcode; + + microtcp_t *mtcp = microtcp_create("10.0.0.5", "10.0.0.4", NULL, NULL); + if (mtcp == NULL) { + fprintf(stderr, "Error: Failed to instanciate microtcp stack\n"); + return -1; + } + + uint16_t port = 80; + microtcp_socket_t *server = microtcp_open(mtcp, port, &errcode); + if (errcode) { + fprintf(stderr, "Error: %s\n", microtcp_strerror(errcode)); + microtcp_destroy(mtcp); + return -1; + } + assert(server); + + fprintf(stderr, "Listening on port %d\n", port); + + while (1) { + fprintf(stderr, "About to accept\n"); + microtcp_socket_t *client = microtcp_accept(server, false, &errcode); + if (errcode) { + fprintf(stderr, "Error: %s\n", microtcp_strerror(errcode)); + break; + } + + fprintf(stderr, "Accepted a connection\n"); + + char buffer[1024]; + size_t num = microtcp_recv(client, buffer, sizeof(buffer), false, &errcode); + if (errcode) { + fprintf(stderr, "Error: %s\n", microtcp_strerror(errcode)); + goto handled; + } + fprintf(stderr, "(%d bytes received)\n", (int) num); + + size_t sent1 = microtcp_send(client, "echo: ", 6, false, &errcode); + if (errcode) { + fprintf(stderr, "Error: %s\n", microtcp_strerror(errcode)); + goto handled; + } + fprintf(stderr, "(%d bytes sent 1)\n", (int) sent1); + + size_t sent2 = microtcp_send(client, buffer, num, false, &errcode); + if (errcode) { + fprintf(stderr, "Error: %s\n", microtcp_strerror(errcode)); + goto handled; + } + fprintf(stderr, "(%d bytes sent 2)\n", (int) sent2); + +handled: + microtcp_close(client); + } + + microtcp_close(server); + microtcp_destroy(mtcp); + return 0; +} diff --git a/examples/http.c b/examples/http.c deleted file mode 100644 index f5c79d8..0000000 --- a/examples/http.c +++ /dev/null @@ -1,59 +0,0 @@ -#include - -typedef struct microhttp_server_t http_server_t; -http_server_t *microhttp_server_create(microtcp_t *mtcp, uint16_t port); -void microhttp_server_destroy(http_server_t *server); -void microhttp_server_serve(microhttp_server_t *server, void *data, void (*callback)(void*, microhttp_request_t*)); - -struct microhttp_server_t { - microtcp_t *tcp; - microtcp_listener_t *listener; -}; - -microhttp_server_t *microhttp_server_create(microtcp_t *mtcp, uint16_t port) -{ - microhttp_server_t *server = malloc(sizeof(microhttp_server_t)); - if (!server) - return NULL; - - microtcp_listener_t *listener = microtcp_listener_create(mtcp, port); - if (!listener) { - free(server); - return NULL; - } - - server->mtcp = mtcp; - server->listener = listener; - return server; -} - -void microhttp_server_serve(microhttp_server_t *server, void *data, void (*callback)(void*, microhttp_request_t*)) -{ - char buffer[65536]; - - while (1) { - - microtcp_socket_t *socket = microtcp_listener_accept(server->listener); - if (!socket) - continue; - - int num = microtcp_socket_recv(socket, buffer, sizeof(buffer)); - if (num >= 0) { - hp_error_t error; - hp_request_t request; - if (!hp_parse(buffer, num, &request, &error)) { - .. - } else { - .. - } - } - - microtcp_socket_destroy(socket); - } -} - -void microhttp_server_destroy(microhttp_server_t *server) -{ - microtcp_listener_destroy(server->listener); - free(server); -} diff --git a/examples/main.c b/examples/main.c deleted file mode 100644 index bf9b95d..0000000 --- a/examples/main.c +++ /dev/null @@ -1,49 +0,0 @@ -#include "net.h" - -static size_t send_callback() -{ -} - -int tun_fd; - -static size_t recv_callback(void *context, void *dst, size_t len) -{ - return read(tun_fd, dst, len); -} - -int main(void) -{ - net_t net; - net_init(&net, ip, mac, NULL, send_callback, recv_callback); - net_spawn_thread(&net); - - uint16_t port = 8080; - - net_listener_t *listener = net_listener_create(&net, port); - if (listener == NULL) { - fprintf(stderr, "Failed to start listening\n"); - net_free(&net); - return -1; - } - - while (1) { - - net_socket_t *client = net_listener_accept(listener); - - if (client == NULL) - continue; - - char message[1024]; - size_t messlen; - - messlen = net_socket_recv(client, message, sizeof(message)); - net_socket_send(client, "echo: ", sizeof("echo: ")); - net_socket_send(client, message, messlen); - - net_socket_destroy(client); - } - - net_listener_destroy(listener); - net_free(&net); - return 0; -} \ No newline at end of file diff --git a/examples/microhttp/http_parser.c b/examples/microhttp/http_parser.c new file mode 100644 index 0000000..1ffcf73 --- /dev/null +++ b/examples/microhttp/http_parser.c @@ -0,0 +1,883 @@ +#include +#include +#include +#include +#include "http_parser.h" + +typedef struct { + size_t offset; + size_t length; +} slice_t; + +typedef struct { + const unsigned char *src; + size_t len, cur; +} scanner_t; + +#define EMPTY_SLICE ((slice_t) {0, 0}) +#define EMPTY_STRING ((hp_string_t) {NULL, 0}) + +static bool is_lower_alpha(unsigned char c) +{ + return c >= 'a' && c <= 'z'; +} + +static bool is_upper_alpha(unsigned char c) +{ + return c >= 'A' && c <= 'Z'; +} + +static bool is_alpha(unsigned char c) +{ + return is_upper_alpha(c) + || is_lower_alpha(c); +} + +static bool is_digit(unsigned char c) +{ + return c >= '0' && c <= '9'; +} + +static bool is_hex_digit(unsigned char c) +{ + return is_digit(c) + || (c >= 'a' && c <= 'f') + || (c >= 'A' && c <= 'F'); +} + +static bool is_unreserved(unsigned char c) +{ + return is_alpha(c) || is_digit(c) + || c == '-' || c == '.' + || c == '_' || c == '~'; +} + +static bool is_subdelim(unsigned char c) +{ + return c == '!' || c == '$' + || c == '&' || c == '\'' + || c == '(' || c == ')' + || c == '*' || c == '+' + || c == ',' || c == ';' + || c == '='; +} + +static bool is_pchar(unsigned char c) +{ + return is_unreserved(c) + || is_subdelim(c) + || c == ':' || c == '@'; +} + +static void report(hp_error_t *err, const char *fmt, ...) +{ + if (err != NULL && !err->occurred) { + va_list args; + va_start(args, fmt); + vsnprintf(err->msg, sizeof(err->msg), fmt, args); + va_end(args); + + err->occurred = true; + } +} + +static slice_t slice_up(scanner_t *scanner, + bool (*is_head)(unsigned char), + bool (*is_body)(unsigned char)) +{ + size_t offset = scanner->cur; + if (scanner->cur < scanner->len && is_head(scanner->src[scanner->cur])) + do + scanner->cur++; + while (scanner->cur < scanner->len && is_body(scanner->src[scanner->cur])); + return (slice_t) {offset, scanner->cur-offset}; +} + +static bool follows_char(scanner_t scanner, unsigned char c) +{ + return scanner.cur < scanner.len && scanner.src[scanner.cur] == c; +} + +static bool follows_pchar(scanner_t scanner) +{ + return scanner.cur < scanner.len && is_pchar(scanner.src[scanner.cur]); +} + +static bool follows_pair(scanner_t scanner, char pair[2]) +{ + return scanner.cur+1 < scanner.len + && scanner.src[scanner.cur+0] == (unsigned char) pair[0] + && scanner.src[scanner.cur+1] == (unsigned char) pair[1]; +} + +static bool follows_digit(scanner_t scanner) +{ + return scanner.cur < scanner.len + && is_digit(scanner.src[scanner.cur]); +} + +static bool follows_hex_digit(scanner_t scanner) +{ + return scanner.cur < scanner.len + && is_hex_digit(scanner.src[scanner.cur]); +} + +static bool consume_char(scanner_t *scanner, unsigned char c) +{ + if (follows_char(*scanner, c)) { + scanner->cur++; + return true; + } else + return false; +} + +static void unconsume_char(scanner_t *scanner) +{ + assert(scanner->cur > 0); + scanner->cur--; +} + +static bool consume_pchar(scanner_t *scanner) +{ + if (follows_pchar(*scanner)) { + scanner->cur++; + return true; + } else + return false; +} + +static bool consume_pair(scanner_t *scanner, char pair[static 2]) +{ + if (follows_pair(*scanner, pair)) { + scanner->cur += 2; + return true; + } else + return false; +} + +static bool consume_digit(scanner_t *scanner, unsigned char *digit) +{ + if (follows_digit(*scanner)) { + *digit = scanner->src[scanner->cur]; + scanner->cur++; + return true; + } else + return false; +} + +static bool consume_hex_digit(scanner_t *scanner, unsigned char *digit) +{ + if (follows_hex_digit(*scanner)) { + *digit = scanner->src[scanner->cur]; + scanner->cur++; + return true; + } else + return false; +} + +static void consume_spaces(scanner_t *scanner) +{ + while (consume_char(scanner, ' ')); +} + +static bool consume_u64_base_10(scanner_t *scanner, uint64_t max, uint64_t *out) +{ + if (!follows_digit(*scanner)) + return false; + + uint64_t num = 0; + unsigned char digit; + while (consume_digit(scanner, &digit)) { + int u = digit - '0'; + if (num > (max - u) / 10) { + unconsume_char(scanner); + break; + } + num = num * 10 + u; + } + + *out = num; + return true; +} + +static int hex_digit_to_int(char c) +{ + assert(is_hex_digit(c)); + + if (is_lower_alpha(c)) + return c - 'a' + 10; + + if (is_upper_alpha(c)) + return c - 'A' + 10; + + assert(is_digit(c)); + return c - '0'; +} + +static bool consume_u64_base_16(scanner_t *scanner, uint64_t max, uint64_t *out) +{ + if (!follows_hex_digit(*scanner)) + return false; + + uint64_t num = 0; + unsigned char digit; + while (consume_hex_digit(scanner, &digit)) { + int u = hex_digit_to_int(digit); + if (num > (max - u) / 16) { + unconsume_char(scanner); + break; + } + num = num * 16 + u; + } + + *out = num; + return true; +} + +static bool consume_u8_base_10(scanner_t *scanner, uint8_t *out) +{ + uint64_t buffer; + bool ok = consume_u64_base_10(scanner, UINT8_MAX, &buffer); + assert(!ok || buffer <= UINT8_MAX); + *out = buffer; + return ok; +} + +static bool consume_u16_base_16(scanner_t *scanner, uint16_t *out) +{ + uint64_t buffer; + bool ok = consume_u64_base_16(scanner, UINT16_MAX, &buffer); + assert(!ok || buffer <= UINT16_MAX); + *out = buffer; + return ok; +} + +static bool consume_u16_base_10(scanner_t *scanner, uint16_t *out) +{ + uint64_t buffer; + bool ok = consume_u64_base_10(scanner, UINT16_MAX, &buffer); + assert(!ok || buffer <= UINT16_MAX); + *out = buffer; + return ok; +} + +// [ : ] // [ [ : ] @ ] { | | "[" "]" } [ : ] [ ] [ ? ] [ # ] + +static bool is_schema_first(unsigned char c) +{ + return is_alpha(c); +} + +static bool is_schema(unsigned char c) +{ + return is_alpha(c) + || is_digit(c) + || c == '+' + || c == '-' + || c == '.'; +} + +static slice_t parse_schema(scanner_t *scanner) +{ + size_t start = scanner->cur; + + slice_t schema = slice_up(scanner, is_schema_first, is_schema); + if (schema.length > 0) + if (!consume_char(scanner, ':')) { + scanner->cur = start; + return EMPTY_SLICE; + } + return schema; +} + +static bool is_username(unsigned char c) +{ + return is_unreserved(c) || is_subdelim(c); +} + +static bool is_username_first(unsigned char c) +{ + return is_username(c); +} + +static bool is_password(unsigned char c) +{ + return is_username(c); +} + +static bool is_password_first(unsigned char c) +{ + return is_password(c); +} + +static hp_string_t string_from_slice(const unsigned char *src, slice_t slice) +{ + assert(src != NULL); + + if (slice.length == 0) + return EMPTY_STRING; + else + return (hp_string_t) {(char*) src + slice.offset, slice.length}; +} + +static void parse_userinfo(scanner_t *scanner, slice_t *username, slice_t *password) +{ + size_t start = scanner->cur; + + *password = EMPTY_SLICE; + + *username = slice_up(scanner, is_username_first, is_username); + if (username->length > 0) { + + if (consume_char(scanner, ':')) + *password = slice_up(scanner, is_password_first, is_password); + + if (!consume_char(scanner, '@')) { + *username = EMPTY_SLICE; + *password = EMPTY_SLICE; + scanner->cur = start; // Rollback changes + } + } +} + +static bool parse_ipv4(scanner_t *scanner, uint32_t *out, hp_error_t *err) +{ + uint8_t byte; + uint32_t ipv4 = 0; + + for (int u = 0; u < 3; u++) { + + if (!consume_u8_base_10(scanner, &byte)) { + if (u == 0) + report(err, "Missing IPv4"); + else + report(err, "Missing IPv4 byte"); + return false; + } + ipv4 = (ipv4 << 8) + byte; + + if (!consume_char(scanner, '.')) + return false; + } + + if (!consume_u8_base_10(scanner, &byte)) { + report(err, "Missing IPv4 byte"); + return false; + } + ipv4 = (ipv4 << 8) + byte; + + *out = ipv4; + return true; +} + +static bool parse_ipv6(scanner_t *scanner, uint16_t ipv6[static 8], hp_error_t *err) +{ + uint16_t tail[8]; + size_t head_count = 0; + size_t tail_count = 0; + + if (!consume_pair(scanner, "::")) { + + do { + uint16_t word; + if (!consume_u16_base_16(scanner, &word)) { + if (scanner->cur == scanner->len) { + if (head_count == 0) + report(err, "Missing IPv6"); + else + report(err, "Missing IPv6 hex value"); + } else + report(err, "Invalid IPv6"); + return false; + } + + ipv6[head_count++] = word; + + if (head_count == 8) + break; + + if (!consume_char(scanner, ':')) { + report(err, "Missing ':' after IPv6 hex value"); + return false; + } + + } while (!consume_char(scanner, ':')); + } + + if (head_count + tail_count < 8) { + while (follows_hex_digit(*scanner)) { + + // We know the current character is a + // hex digit, therefore [parse_ipv6_word] + // won't fail. + uint16_t word; + (void) consume_u16_base_16(scanner, &word); + + tail[tail_count++] = word; + + if (head_count + tail_count == 8) + break; + + if (!consume_char(scanner, ':')) + break; + } + } + + assert(head_count + tail_count <= 8); + + for (size_t p = 0; p < 8 - head_count - tail_count; p++) + ipv6[head_count + p] = 0; + + for (size_t p = 0; p < tail_count; p++) + ipv6[8 - tail_count + p] = tail[p]; + + return true; +} + +static bool is_hostname(unsigned char c) +{ + return is_unreserved(c) || is_subdelim(c); +} + +static bool is_hostname_first(unsigned char c) +{ + return is_hostname(c); +} + +static bool parse_host(scanner_t *scanner, hp_host_t *host, hp_error_t *err) +{ + if (consume_char(scanner, '[')) { + if (!parse_ipv6(scanner, host->ipv6, err)) + return false; + if (!consume_char(scanner, ']')) { + report(err, "Missing ']' after IPv6"); + return false; + } + host->mode = HP_HOSTMODE_IPV6; + } else { + + uint32_t ipv4; + bool is_ipv4; + + if (follows_digit(*scanner)) { + size_t start = scanner->cur; + is_ipv4 = parse_ipv4(scanner, &ipv4, NULL); + if (!is_ipv4) + scanner->cur = start; + } else + is_ipv4 = false; + + if (is_ipv4) { + host->ipv4 = ipv4; + host->mode = HP_HOSTMODE_IPV4; + } else { + + slice_t hostname = slice_up(scanner, is_hostname_first, is_hostname); + if (hostname.length == 0) { + report(err, "Missing host"); + return false; + } + + host->mode = HP_HOSTMODE_NAME; + host->name = string_from_slice(scanner->src, hostname); + } + } + + host->no_port = !consume_u16_base_10(scanner, &host->port); + return true; +} + +static bool parse_path(scanner_t *scanner, slice_t *out, hp_error_t *err) +{ + out->offset = scanner->cur; + + if (!consume_char(scanner, '/')) + if (!follows_pchar(*scanner)) { + report(err, "Missing path"); + return false; + } + + while (consume_pchar(scanner)) { + while (consume_pchar(scanner)); + if (!consume_char(scanner, '/')) + break; + } + + out->length = scanner->cur - out->offset; + return true; +} + +static bool is_query(unsigned char c) +{ + return is_pchar(c) || c == '/' || c == '?'; +} + +static bool is_fragment(unsigned char c) +{ + return is_pchar(c) || c == '/'; +} + +static bool parse_url(scanner_t *scanner, hp_url_t *url, hp_error_t *err) +{ + url->schema = string_from_slice(scanner->src, parse_schema(scanner)); + + if (consume_pair(scanner, "//")) { + + slice_t username, password; + parse_userinfo(scanner, &username, &password); + url->username = string_from_slice(scanner->src, username); + url->password = string_from_slice(scanner->src, password); + + if (!parse_host(scanner, &url->host, err)) + return false; + + if (follows_char(*scanner, '/')) { + /* absolute path */ + // The parsing of the path can't fail + // because we already know there's at + // leat a '/' for it. + slice_t path; + (void) parse_path(scanner, &path, err); + url->path = string_from_slice(scanner->src, path); + } else + url->path = EMPTY_STRING; + + } else { + + url->host.mode = HP_HOSTMODE_NAME; + url->host.name = EMPTY_STRING; + url->host.no_port = true; + url->host.port = 0; + + url->username = EMPTY_STRING; + url->password = EMPTY_STRING; + + // TODO: Since there was no authority, + // the path is non optional. + + if (follows_char(*scanner, '?')) { + report(err, "Missing path before query"); + return false; + } + if (follows_char(*scanner, '#')) { + report(err, "Missing path before fragment"); + return false; + } + + slice_t path; + if (!parse_path(scanner, &path, err)) + return false; + url->path = string_from_slice(scanner->src, path); + } + + url->query = consume_char(scanner, '?') + ? string_from_slice(scanner->src, slice_up(scanner, is_query, is_query)) + : EMPTY_STRING; + + url->fragment = consume_char(scanner, '#') + ? string_from_slice(scanner->src, slice_up(scanner, is_fragment, is_fragment)) + : EMPTY_STRING; + return true; +} + +static bool is_header_name_body(unsigned char c) +{ + return is_alpha(c) || is_digit(c) || c == '-'; +} + +static bool is_header_name_head(unsigned char c) +{ + return is_header_name_body(c); +} + +static bool is_header_body_body(unsigned char c) +{ + return c != '\r'; +} + +static bool is_header_body_head(unsigned char c) +{ + return is_header_body_body(c); +} + +static bool parse_header(scanner_t *scanner, hp_header_t *header, hp_error_t *err) +{ + slice_t name, body; + + name = slice_up(scanner, is_header_name_head, is_header_name_body); + if (name.length == 0) { + report(err, "Missing header name"); + return false; + } + + if (!consume_char(scanner, ':')) { + report(err, "Missing ':' after header name"); + return false; + } + + body = slice_up(scanner, is_header_body_head, is_header_body_body); + + if (!consume_pair(scanner, "\r\n")) { + report(err, "Missing CRLF after header"); + return false; + } + + header->name = string_from_slice(scanner->src, name); + header->body = string_from_slice(scanner->src, body); + return true; +} + +static bool parse_version(scanner_t *scanner, int *major, int *minor, hp_error_t *err) +{ + unsigned char char_major = '0'; + unsigned char char_minor = '0'; + + if (!consume_char(scanner, 'H') || + !consume_char(scanner, 'T') || + !consume_char(scanner, 'T') || + !consume_char(scanner, 'P') || + !consume_char(scanner, '/') || + !consume_digit(scanner, &char_major)) { + report(err, "Invalid version token"); + return false; + } + if (consume_char(scanner, '.')) + if (!consume_digit(scanner, &char_minor)) { + report(err, "Invalid version token"); + return false; + } + *major = char_major - '0'; + *minor = char_minor - '0'; + return true; +} + +static bool get_method_id(hp_string_t str, hp_method_t *method) +{ + // CONNECT OPTIONS TRACE PATCH + switch (str.len) { + case 3: + if (str.str[0] == 'G' && + str.str[1] == 'E' && + str.str[2] == 'T') { + *method = HP_METHOD_GET; + return true; + } + if (str.str[0] == 'P' && + str.str[1] == 'U' && + str.str[2] == 'T') { + *method = HP_METHOD_PUT; + return true; + } + break; + + case 4: + if (str.str[0] == 'P' && + str.str[1] == 'O' && + str.str[2] == 'S' && + str.str[3] == 'T') { + *method = HP_METHOD_POST; + return true; + } + if (str.str[0] == 'H' && + str.str[1] == 'E' && + str.str[2] == 'A' && + str.str[3] == 'D') { + *method = HP_METHOD_HEAD; + return true; + } + break; + + case 5: + if (str.str[0] == 'T' && + str.str[1] == 'R' && + str.str[2] == 'A' && + str.str[3] == 'C' && + str.str[4] == 'E') { + *method = HP_METHOD_TRACE; + return true; + } + if (str.str[0] == 'P' && + str.str[1] == 'A' && + str.str[2] == 'T' && + str.str[3] == 'C' && + str.str[4] == 'H') { + *method = HP_METHOD_PATCH; + return true; + } + break; + + case 6: + if (str.str[0] == 'D' && + str.str[1] == 'E' && + str.str[2] == 'L' && + str.str[3] == 'E' && + str.str[4] == 'T' && + str.str[5] == 'E') { + *method = HP_METHOD_DELETE; + return true; + } + break; + } + return false; +} + +static bool is_method_body(unsigned char c) +{ + return is_upper_alpha(c); +} + +static bool is_method_head(unsigned char c) +{ + return is_method_body(c); +} + +static bool parse_method(scanner_t *scanner, hp_method_t *method, hp_error_t *err) +{ + slice_t method_slice = slice_up(scanner, is_method_head, is_method_body); + if (method_slice.length == 0) { + report(err, "Missing method"); + return false; + } + hp_string_t method_string = string_from_slice(scanner->src, method_slice); + if (!get_method_id(method_string, method)) { + report(err, "Invalid method %.*s", (int) method_string.len, method_string.str); + return false; + } + return true; +} + +static bool parse_status_line(scanner_t *scanner, + hp_method_t *method, + hp_url_t *url, + int *major, int *minor, + hp_error_t *err) +{ + if (!parse_method(scanner, method, err)) + return false; + + if (!consume_char(scanner, ' ')) { + report(err, "Missing space after method"); + return false; + } + + if (!parse_url(scanner, url, err)) + return false; + + if (!consume_char(scanner, ' ')) { + report(err, "Missing space after URL"); + return false; + } + + if (!parse_version(scanner, major, minor, err)) + return false; + + if (!consume_pair(scanner, "\r\n")) { + report(err, "Missing CRLF after version token"); + return false; + } + + return true; +} + +static void append_header(hp_request_t *req, + hp_header_t header) +{ + if (req->num_headers < HP_MAX_HEADERS) + req->headers[req->num_headers++] = header; +} + +bool hp_parse(const char *src, size_t len, + hp_request_t *out, hp_error_t *err) +{ + scanner_t scanner = {(unsigned char*) src, len, 0}; + + if (!parse_status_line(&scanner, &out->method, + &out->url, &out->major, + &out->minor, err)) + return false; + + out->num_headers = 0; + while (!consume_pair(&scanner, "\r\n")) { + + hp_header_t header; + if (!parse_header(&scanner, &header, err)) + return false; + + append_header(out, header); + } + + return true; +} + +bool hp_parse_url(const char *src, size_t len, + hp_url_t *url, hp_error_t *err) +{ + scanner_t scanner = {(unsigned char*) src, len, 0}; + return parse_url(&scanner, url, err); +} + +static char to_lower(char c) +{ + if (is_upper_alpha(c)) + return c - 'A' + 'a'; + else + return c; +} + +static bool case_insensitive_string_compare(hp_string_t s1, hp_string_t s2) +{ + if (s1.len != s2.len) + return false; + + for (size_t i = 0; i < s1.len; i++) + if (to_lower(s1.str[i]) != to_lower(s2.str[i])) + return false; + return true; +} + +hp_header_t *hp_get_header(hp_request_t req, const char *name) +{ + hp_string_t name2 = {name, strlen(name)}; + for (size_t i = 0; i < req.num_headers; i++) { + hp_header_t *header = req.headers + i; + if (case_insensitive_string_compare(name2, header->name)) + return header; + } + return NULL; +} + +static bool parse_content_length(const char *src, size_t len, size_t *out, hp_error_t *error) +{ + scanner_t scanner = {(unsigned char*) src, len, 0}; + consume_spaces(&scanner); + + if (!follows_digit(scanner)) { + report(error, "Non-digit character in Content-Length header"); + return false; + } + + *out = 0; + unsigned char digit; + while (consume_digit(&scanner, &digit)) { + int k = digit - '0'; + if (*out > (SIZE_MAX - k) / 10) { + report(error, "Unsigned integer is too big"); + return false; + } + *out = *out * 10 + k; + } + + if (scanner.cur < scanner.len) { + report(error, "Invalid character '%c'", scanner.src[scanner.cur]); + return false; + } + return true; +} + +bool hp_get_content_length(hp_request_t req, size_t *out, hp_error_t *error) +{ + hp_header_t *header = hp_get_header(req, "Content-Length"); + if (header == NULL) + return 0; + return parse_content_length(header->body.str, header->body.len, out, error); +} \ No newline at end of file diff --git a/examples/microhttp/http_parser.h b/examples/microhttp/http_parser.h new file mode 100644 index 0000000..b67c239 --- /dev/null +++ b/examples/microhttp/http_parser.h @@ -0,0 +1,70 @@ +#include +#include +#include + +#define HP_MAX_HEADERS 8 + +typedef struct { + const char *str; + size_t len; +} hp_string_t; + +typedef struct { + bool occurred; + char msg[256]; +} hp_error_t; + +typedef enum { + HP_METHOD_GET, + HP_METHOD_PUT, + HP_METHOD_POST, + HP_METHOD_HEAD, + HP_METHOD_PATCH, + HP_METHOD_TRACE, + HP_METHOD_DELETE, +} hp_method_t; + +typedef enum { + HP_HOSTMODE_NAME, + HP_HOSTMODE_IPV4, + HP_HOSTMODE_IPV6, +} hp_hostmode_t; + +typedef struct { + hp_hostmode_t mode; + union { + uint32_t ipv4; + uint16_t ipv6[8]; + hp_string_t name; + }; + bool no_port; + uint16_t port; +} hp_host_t; + +typedef struct { + hp_host_t host; + hp_string_t path; + hp_string_t query; + hp_string_t schema; + hp_string_t fragment; + hp_string_t username; + hp_string_t password; +} hp_url_t; + +typedef struct { + hp_string_t name; + hp_string_t body; +} hp_header_t; + +typedef struct { + int major, minor; + hp_url_t url; + hp_method_t method; + hp_header_t headers[HP_MAX_HEADERS]; + size_t num_headers; +} hp_request_t; + +bool hp_parse(const char *src, size_t len, hp_request_t *out, hp_error_t *err); +bool hp_parse_url(const char *src, size_t len, hp_url_t *url, hp_error_t *err); +hp_header_t *hp_get_header(hp_request_t req, const char *name); +bool hp_get_content_length(hp_request_t req, size_t *out, hp_error_t *error); \ No newline at end of file diff --git a/examples/microhttp/main.c b/examples/microhttp/main.c new file mode 100644 index 0000000..02302fe --- /dev/null +++ b/examples/microhttp/main.c @@ -0,0 +1,81 @@ +#include +#include +#include +#include +#include +#include "xhttp.h" + +static xh_handle handle; + +static void callback(xh_request *req, xh_response *res, void *userp) +{ + (void) req; + (void) userp; + + res->status = 200; + res->body.str = "Hello, world!"; + xh_header_add(res, "Content-Type", "text/plain"); +} +/* +static void handle_sigterm(int signum) +{ + (void) signum; + xh_quit(handle); +} +*/ +int main(void) +{ +/* + signal(SIGTERM, handle_sigterm); + signal(SIGINT, handle_sigterm); + +#ifndef _WIN32 + signal(SIGQUIT, handle_sigterm); +#endif +*/ + char ip[] = "10.0.0.4"; + char mac[] = "00:01:00:01:00:00"; + + struct device *dev = tuntap_init(); + if (!dev) { + fprintf(stderr, "Error: Couldn't initialize the TAP library\n"); + return -1; + } + + // This must be set AFTER tuntap_init because + // it sets the callback function to the default + // callback which writes to stderr. + //tuntap_log_set_cb(NULL); + + int netmask = 24; // TODO: Make this configurable + + if (tuntap_start(dev, TUNTAP_MODE_ETHERNET, TUNTAP_ID_ANY)) { + fprintf(stderr, "Error: Couldn't set up the TAP device\n"); + tuntap_release(dev); + return -1; + } + + tuntap_set_ip(dev, ip, netmask); + tuntap_set_hwaddr(dev, mac); + + if (tuntap_up(dev)) { + fprintf(stderr, "Error: Couldn't activate the TAP device\n"); + tuntap_release(dev); + } + + microhttp_config_t config = { + .userp = dev, + .ip = ip, + .mac = mac, + .recv_frame = (int(*)(void*, void*, size_t)) tuntap_read, + .send_frame = (int(*)(void*, const void*, size_t)) tuntap_write, + }; + + const char *error = xhttp(80, callback, NULL, &handle, config); + tuntap_release(dev); + if(error != NULL) { + fprintf(stderr, "Error: %s\n", error); + return 1; + } + return 0; +} \ No newline at end of file diff --git a/examples/microhttp/xhttp.c b/examples/microhttp/xhttp.c new file mode 100644 index 0000000..1d87623 --- /dev/null +++ b/examples/microhttp/xhttp.c @@ -0,0 +1,1634 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "xhttp.h" + + +/* __ _________________ * + * __ __ / // /_ __/_ __/ _ \ * + * \ \ // _ / / / / / / ___/ * + * /_\_\/_//_/ /_/ /_/ /_/ * + * * + * +--------------------------------------------------------------------------------------------+ * + * | | * + * | OVERVIEW | * + * | | * + * | The logic starts inside the [xhttp] function, where the server waits in a loop for events | * + * | provided by epoll (the event loop). | * + * | | * + * | Each client connection is represented by a [conn_t] structure, which is basically composed | * + * | by a buffer of input data, a buffer of output data, the parsing state of the input buffer | * + * | plus some more fields required to hold the state of the parsing and to manage the | * + * | connection. These structures are preallocated at start-up time and determine the capacity | * + * | of the server. | * + * | | * + * | Whenever a client requests to connect, the server decides if it can handle it or not. If | * + * | it can, it gives it a [conn_t] structure and registers it into the event loop. | * + * | | * + * | When the event loop signals that a connection sent some data, the data is copied from the | * + * | kernel into the user-space buffer of the [conn_t] structure. If the head of the request | * + * | wasn't received or was received partially, the character sequence "\r\n\r\n" (a blank line)| * + * | is searched for inside the downloaded data. The "\r\n\r\n" token signifies the end of the | * + * | request's head and the start of it's body. If the head wasn't received the server goes | * + * | back to waiting for new events. If the token is found, the head can be parsed and the size | * + * | of the body determined. If the whole body of the request was received with the head, the | * + * | request can already be handled. If the body wasn't received, the servers goes back to | * + * | waiting for events until the rest of the body is received. When the body is fully received,| * + * | the user-provided callback can be called to generate a response. | * + * | One thing to note is that multiple requests could be read from a single [recv], making it | * + * | necessary to perform these operations on the input buffer in a loop. | * + * | | * + * | If at any point of this process the request is determined to be invalid or an internal | * + * | error occurres, a 4xx or 5xx response is sent. | * + * | | * + * | While handling data input events, the response is never sent directly to the kernel buffer,| * + * | because the call to [send] could block the server. Instead, the response is written to the | * + * | [conn_t]'s output buffer. This buffer is only flushed to the kernel when a write-ready | * + * | event is triggered for that connection. | * + * | | * + * +--------------------------------------------------------------------------------------------+ * + * */ + +typedef enum { + XH_REQ, + XH_RES +} struct_type_t; + +typedef struct { + struct_type_t type; + xh_response public; + xh_table headers; + int capacity; + bool failed; +} xh_response2; + +typedef struct { + struct_type_t type; + xh_request public; +} xh_request2; + +typedef struct { + char *data; + uint32_t size; + uint32_t used; +} buffer_t; + +typedef struct conn_t conn_t; +struct conn_t { + + // This is used to hold a free-list + // of [conn_t] structures. + conn_t *next; + + // I/O buffers required for async. + // reads and writes. + buffer_t in, out; + + // Connection's socked file + // descriptor. + microtcp_socket_t *sock; + + // Number of resources served to + // this client. This is used to + // determine which connections to + // keep alive. + int served; + + // This flags can be set after a + // response is written to the output + // buffer. If set, then all reads + // from the client stop and when the + // output buffer is flushed the + // connection is closed. + bool close_when_uploaded; + + // The way writes to the output buffer occur is + // through the [append_string_to_output_buffer] + // function. Since the output buffer may beed to + // be resized, the [append_string_to_output_buffer] + // operation may fail. Since checking every time + // for the return value makes the code very verbose, + // instead of returning an error value, this flag + // is set. If this flag is set then + // [append_string_to_output_buffer] operations + // have no effect and when [upload] is called it + // returns the error that the first + // [append_string_to_output_buffer] that failed + // would have returned. + bool failed_to_append; + + bool head_received; + uint32_t body_offset; + uint32_t body_length; + xh_request2 request; +}; + +typedef struct { + bool exiting; + microtcp_socket_t *sock; + microtcp_mux_t *mux; + int connum; + conn_t *freelist; + xh_callback callback; + void *userp; + conn_t pool[MICROHTTP_MAX_CLIENTS]; +} context_t; + +static const char *statis_code_to_status_text(int code) +{ + switch(code) + { + case 100: return "Continue"; + case 101: return "Switching Protocols"; + case 102: return "Processing"; + + case 200: return "OK"; + case 201: return "Created"; + case 202: return "Accepted"; + case 203: return "Non-Authoritative Information"; + case 204: return "No Content"; + case 205: return "Reset Content"; + case 206: return "Partial Content"; + case 207: return "Multi-Status"; + case 208: return "Already Reported"; + + case 300: return "Multiple Choices"; + case 301: return "Moved Permanently"; + case 302: return "Found"; + case 303: return "See Other"; + case 304: return "Not Modified"; + case 305: return "Use Proxy"; + case 306: return "Switch Proxy"; + case 307: return "Temporary Redirect"; + case 308: return "Permanent Redirect"; + + case 400: return "Bad Request"; + case 401: return "Unauthorized"; + case 402: return "Payment Required"; + case 403: return "Forbidden"; + case 404: return "Not Found"; + case 405: return "Method Not Allowed"; + case 406: return "Not Acceptable"; + case 407: return "Proxy Authentication Required"; + case 408: return "Request Timeout"; + case 409: return "Conflict"; + case 410: return "Gone"; + case 411: return "Length Required"; + case 412: return "Precondition Failed"; + case 413: return "Request Entity Too Large"; + case 414: return "Request-URI Too Long"; + case 415: return "Unsupported Media Type"; + case 416: return "Requested Range Not Satisfiable"; + case 417: return "Expectation Failed"; + case 418: return "I'm a teapot"; + case 420: return "Enhance your calm"; + case 422: return "Unprocessable Entity"; + case 426: return "Upgrade Required"; + case 429: return "Too many requests"; + case 431: return "Request Header Fields Too Large"; + case 449: return "Retry With"; + case 451: return "Unavailable For Legal Reasons"; + + case 500: return "Internal Server Error"; + case 501: return "Not Implemented"; + case 502: return "Bad Gateway"; + case 503: return "Service Unavailable"; + case 504: return "Gateway Timeout"; + case 505: return "HTTP Version Not Supported"; + case 509: return "Bandwidth Limit Exceeded"; + } + return "???"; +} + +/* Symbol: find_header + * + * Finds the header from a header array. + * + * Arguments: + * + * - headers: The header set array. + * + * - name: Zero-terminated string that contains the + * header's name. The comparison with each + * header's name is made using [xh_header_cmp], + * so it's not case-sensitive. + * + * Returns: + * The index in the array of the matched header, or + * -1 is no header was found. + */ +static int find_header(xh_table headers, const char *name) +{ + for(int i = 0; i < headers.count; i += 1) + if(xh_header_cmp(name, headers.list[i].key.str)) + return i; + return -1; +} + +/* Symbol: xh_header_add + * + * Add or replace a header into a response object. + * + * Arguments: + * + * - res: The response object. + * + * - name: Zero-terminated string that contains + * the header's name. The comparison with + * each header's name is made using [xh_header_cmp], + * so it's not case-sensitive. + * + * - valfmt: A printf-like format string that evaluates + * to the header's value. + * + * Returns: + * Nothing. The header may or may not be added + * (or replaced) to the request. + */ +void xh_header_add(xh_response *res, const char *name, const char *valfmt, ...) +{ + xh_response2 *res2 = (xh_response2*) ((char*) res - offsetof(xh_response2, public)); + + assert(&res2->public == res); + + if(res2->failed) + return; + + int i = find_header(res2->headers, name); + + unsigned int name_len, value_len; + + name_len = name == NULL ? 0 : strlen(name); + + char value[512]; + { + va_list args; + va_start(args, valfmt); + int n = vsnprintf(value, sizeof(value), valfmt, args); + va_end(args); + + if(n < 0) + { + // Bad format. + res2->failed = 1; + return; + } + + if((unsigned int) n >= sizeof(value)) + { + // Static buffer is too small. + res2->failed = 1; + return; + } + + value_len = n; + } + + // Duplicate name and value. + char *name2, *value2; + { + void *mem = malloc(name_len + value_len + 2); + + if(mem == NULL) + { + // ERROR! + res2->failed = 1; + return; + } + + name2 = (char*) mem; + value2 = (char*) mem + name_len + 1; + + strcpy(name2, name); + strcpy(value2, value); + } + + if(i < 0) + { + if(res2->headers.count == res2->capacity) + { + int new_capacity = res2->capacity == 0 + ? 8 : res2->capacity * 2; + + void *tmp = realloc(res2->headers.list, + new_capacity * sizeof(xh_pair)); + + if(tmp == NULL) + { + // ERROR! + res2->failed = 1; + free(name2); + return; + } + + res2->headers.list = tmp; + res2->capacity = new_capacity; + } + + res2->headers.list[res2->headers.count] = (xh_pair) { + { name2, name_len }, { value2, value_len }, + }; + res2->headers.count += 1; + res2->public.headers = res2->headers; + } + else + { + free(res2->headers.list[i].key.str); + res2->headers.list[i] = (xh_pair) { + { name2, name_len }, { value2, value_len }, + }; + } +} + +/* Symbol: xh_header_rem + * + * Remove a header from a response object. + * + * Arguments: + * + * - res: The response object that contains the + * header to be removed. + * + * - name: Zero-terminated string that contains + * the header's name. The comparison with + * each header's name is made using [xh_header_cmp], + * so it's not case-sensitive. + * + * Returns: + * Nothing. + */ +void xh_header_rem(xh_response *res, const char *name) +{ + xh_response2 *res2 = (xh_response2*) ((char*) res - offsetof(xh_response2, public)); + + assert(&res2->public == res); + + if(res2->failed) + return; + + int i = find_header(res2->headers, name); + + if(i < 0) + return; + + free(res2->headers.list[i].key.str); + + assert(i >= 0); + + for(; i < res2->headers.count-1; i += 1) + res2->headers.list[i] = res2->headers.list[i+1]; + + res2->headers.count -= 1; + res2->public.headers = res2->headers; +} + +static xh_table get_headers_from_req_or_res(void *req_or_res) +{ + _Static_assert(offsetof(xh_response2, public) == offsetof(xh_request2, public), + "The public portion of xh_response2 and xh_request2 must be aligned the same way"); + + struct_type_t type = ((xh_request2*) ((char*) req_or_res + - offsetof(xh_request2, public)))->type; + + assert(type == XH_RES || type == XH_REQ); + + xh_table headers = (type == XH_REQ)? + ((xh_request *) req_or_res)->headers: + ((xh_response*) req_or_res)->headers; + return headers; +} + +/* Symbol: xh_header_get + * + * Find the contents of a header given it's + * name from a response or request object. + * + * Arguments: + * + * - req_or_res: The request or response object + * that contains the header. This + * argument must originally be of + * type [xh_request*] or [xh_response*]. + * + * - name: Zero-terminated string that contains + * the header's name. The comparison with + * each header's name is made using [xh_header_cmp], + * so it's not case-sensitive. + * + * Returns: + * A zero-terminated string containing the value of + * the header or NULL if the header isn't contained + * in the request/response. + * + * Notes: + * - The returned value is invalidated if + * the header is removed using [xh_hrem]. + */ +const char *xh_header_get(void *req_or_res, const char *name) +{ + xh_table headers = get_headers_from_req_or_res(req_or_res); + + int i = find_header(headers, name); + + if(i < 0) + return NULL; + + return headers.list[i].val.str; +} + +/* Symbol: xh_header_cmp + * + * This function compares header names. + * The comparison isn't case-sensitive. + * + * Arguments: + * + * - a: Zero-terminated string that contains + * the first header's name. + * + * - b: Zero-terminated string that contains + * the second header's name. + * + * Returns: + * 1 if the header names match, 0 otherwise. + */ +bool xh_header_cmp(const char *a, const char *b) +{ + if(a == NULL || b == NULL) + return a == b; + + while(*a != '\0' && *b != '\0' && tolower(*a) == tolower(*b)) + a += 1, b += 1; + + return tolower(*a) == tolower(*b); +} + +static void res_init(xh_response2 *res) +{ + memset(res, 0, sizeof(xh_response2)); + res->type = XH_RES; + res->public.body.len = -1; +} + +static void res_deinit(xh_response2 *res) +{ + if(res->headers.list != NULL) + { + assert(res->headers.count > 0); + for(int i = 0; i < res->headers.count; i += 1) + free(res->headers.list[i].key.str); + free(res->headers.list); + } +} + +static void res_reinit(xh_response2 *res) +{ + res_deinit(res); + res_init(res); +} + +static void req_init(xh_request2 *req) +{ + req->type = XH_REQ; +} + +static void req_deinit(xh_request *req) +{ + free(req->headers.list); + req->headers.list = NULL; + req->headers.count = 0; +} + +static void accept_connection(context_t *ctx) +{ + microtcp_errcode_t errcode; + microtcp_socket_t *accepted_sock = microtcp_accept(ctx->sock, true, &errcode); + + if(accepted_sock == NULL) + return; // Failed to accept. + + if(ctx->freelist == NULL) + { + // Connection limit reached. + microtcp_close(accepted_sock); + return; + } + + conn_t *conn = ctx->freelist; + ctx->freelist = conn->next; + + assert(((intptr_t) conn & + (intptr_t) 1) == 0); + + memset(conn, 0, sizeof(conn_t)); + conn->sock = accepted_sock; + req_init(&conn->request); + + if(microtcp_mux_register(ctx->mux, accepted_sock, MICROTCP_MUX_RECV|MICROTCP_MUX_SEND, conn)) + { + microtcp_close(accepted_sock); + + conn->sock = NULL; + conn->next = ctx->freelist; + ctx->freelist = conn; + return; + } + + ctx->connum += 1; +} + +static void close_connection(context_t *ctx, conn_t *conn) +{ + microtcp_close(conn->sock); + + if(conn->in.data != NULL) + { + free(conn->in.data); + conn->in.data = NULL; + } + + if(conn->out.data != NULL) + { + free(conn->out.data); + conn->out.data = NULL; + } + + if(conn->request.public.headers.list != NULL) + free(conn->request.public.headers.list); + + conn->sock = NULL; + + conn->next = ctx->freelist; + ctx->freelist = conn; + + ctx->connum -= 1; +} + +#if DEBUG +static void close_connection_(context_t *ctx, conn_t *conn, const char *file, int line) +{ + fprintf(stderr, "Closing connection at %s:%d.\n", file, line); + close_connection(ctx, conn); +} +#define close_connection(ctx, conn) close_connection_(ctx, conn, __FILE__, __LINE__) +#endif + +static bool is_uppercase_alpha(char c) +{ + return c >= 'A' && c <= 'Z'; +} + +static bool is_digit(char c) +{ + return c >= '0' && c <= '9'; +} + +static bool is_space(char c) +{ + return c == ' '; +} + +static void skip(char *str, uint32_t len, uint32_t *i, bool not, bool (*test)(char)) +{ + if(not) + while(*i < len && !test(str[*i])) + *i += 1; + else + while(*i < len && test(str[*i])) + *i += 1; +} + +static void skip_until(char *str, uint32_t len, uint32_t *i, char c) +{ + while(*i < len && str[*i] != c) + *i += 1; +} + +struct parse_err_t { + bool internal; + char *msg; + unsigned int len; +}; + +static struct parse_err_t parse(char *str, uint32_t len, xh_request *req) +{ + #define OK \ + ((struct parse_err_t) { .internal = 0, .msg = NULL}) + + #define FAILURE(msg_) \ + ((struct parse_err_t) { .internal = 0, .msg = msg_, .len = sizeof(msg_)-1 }) + + #define INTERNAL_FAILURE(msg_) \ + ((struct parse_err_t) { .internal = 1, .msg = msg_, .len = sizeof(msg_)-1 }) + + if(len == 0) + return FAILURE("Empty request"); + + uint32_t i = 0; + + uint32_t method_offset = i; + + skip(str, len, &i, 0, is_uppercase_alpha); + + uint32_t method_length = i - method_offset; + + if(method_length == 0) + return FAILURE("Missing method"); + + if(i == len) + return FAILURE("Missing URL and HTTP version"); + + if(!is_space(str[i])) + return FAILURE("Bad character after method. Methods can only have uppercase alphabetic characters"); + + skip(str, len, &i, 0, is_space); + + if(i == len) + return FAILURE("Missing URL and HTTP version"); + + uint32_t URL_offset = i; + while(i < len && str[i] != ' ' && str[i] != '?') + i += 1; + uint32_t URL_length = i - URL_offset; + + uint32_t params_offset; + if(i < len && str[i] == '?') + { + params_offset = i+1; + while(i < len && str[i] != ' ') + i += 1; + } + else params_offset = i; + uint32_t params_length = i - params_offset; + + if(i == len) + return FAILURE("Missing HTTP version"); + + assert(is_space(str[i])); + + skip(str, len, &i, 0, is_space); + + if(i == len) + return FAILURE("Missing HTTP version"); + + uint32_t version_offset = i; + + skip_until(str, len, &i, '\r'); + + uint32_t version_length = i - version_offset; + + if(version_length == 0) + return FAILURE("Missing HTTP version"); + + if(i == len) + return FAILURE("Missing CRLF after HTTP version"); + + assert(str[i] == '\r'); + + i += 1; // Skip the \r. + + if(i == len) + return FAILURE("Missing LF after CR"); + + if(str[i] != '\n') + return FAILURE("Missing LF after CR"); + + i += 1; // Skip the \n. + + int capacity = 0; + xh_table headers = { + .list = NULL, .count = 0 }; + + while(1) + { + if(i == len) + { + free(headers.list); + return FAILURE("Missing blank line"); + } + + if(i+1 < len && str[i] == '\r' && str[i+1] == '\n') + { + // Blank line. + i += 2; + break; + } + + uint32_t hname_offset = i; + + skip_until(str, len, &i, ':'); + + uint32_t hname_length = i - hname_offset; + + if(i == len) + { + free(headers.list); + return FAILURE("Malformed header"); + } + + if(hname_length == 0) + { + free(headers.list); + return FAILURE("Empty header name"); + } + + assert(str[i] == ':'); + + // Make the header name zero-terminated + // by overwriting the ':' with a '\0'. + str[i] = '\0'; + + i += 1; // Skip the ':'. + + uint32_t hvalue_offset = i; + + do + { + skip_until(str, len, &i, '\r'); + + if(i == len) + { + free(headers.list); + return FAILURE("Malformed header"); + } + + assert(str[i] == '\r'); + + i += 1; // Skip the \r. + + if(i == len) + { + free(headers.list); + return FAILURE("Malformed header"); + } + } + while(str[i] != '\n'); + assert(str[i] == '\n'); + i += 1; // Skip the '\n'. + + uint32_t hvalue_length = (i - 2) - hvalue_offset; + + if(headers.count == capacity) + { + int new_capacity = capacity == 0 ? 8 : capacity * 2; + + void *temp = realloc(headers.list, + new_capacity * sizeof(xh_pair)); + + if(temp == NULL) + { + free(headers.list); + return INTERNAL_FAILURE("No memory"); + } + + capacity = new_capacity; + headers.list = temp; + } + + headers.list[headers.count++] = (xh_pair) { + { str + hname_offset, hname_length }, + { str + hvalue_offset, hvalue_length }, + }; + + str[ hname_offset + hname_length] = '\0'; + str[hvalue_offset + hvalue_length] = '\0'; + } + + req->headers = headers; + + req->method = xh_string_new(str + method_offset, method_length); + req->URL = xh_string_new(str + URL_offset, URL_length); + req->params = xh_string_new(str + params_offset, params_length); + + str[ method_offset + method_length] = '\0'; + str[ URL_offset + URL_length] = '\0'; + str[ params_offset + params_length] = '\0'; + str[version_offset + version_length] = '\0'; + + // Validate the header. + { + bool unknown_method = 0; + + #define PAIR(p, q) (uint64_t) (((uint64_t) p << 32) | (uint64_t) q) + switch(PAIR(req->method.str[0], method_length)) + { + case PAIR('G', 3): req->method_id = XH_GET; unknown_method = !!strcmp(req->method.str, "GET"); break; + case PAIR('H', 4): req->method_id = XH_HEAD; unknown_method = !!strcmp(req->method.str, "HEAD"); break; + case PAIR('P', 4): req->method_id = XH_POST; unknown_method = !!strcmp(req->method.str, "POST"); break; + case PAIR('P', 3): req->method_id = XH_PUT; unknown_method = !!strcmp(req->method.str, "PUT"); break; + case PAIR('D', 6): req->method_id = XH_DELETE; unknown_method = !!strcmp(req->method.str, "DELETE"); break; + case PAIR('C', 7): req->method_id = XH_CONNECT; unknown_method = !!strcmp(req->method.str, "CONNECT"); break; + case PAIR('O', 7): req->method_id = XH_OPTIONS; unknown_method = !!strcmp(req->method.str, "OPTIONS"); break; + case PAIR('T', 5): req->method_id = XH_TRACE; unknown_method = !!strcmp(req->method.str, "TRACE"); break; + case PAIR('P', 5): req->method_id = XH_PATCH; unknown_method = !!strcmp(req->method.str, "PATCH"); break; + default: unknown_method = 1; break; + } + #undef PAIR + + if(unknown_method) + { + free(headers.list); + return FAILURE("Unknown method"); + } + } + + // Validate the HTTP version + { + bool bad_version = 0; + switch(version_length) + { + case sizeof("HTTP/M.N")-1: + + if(!strcmp(str + version_offset, "HTTP/0.9")) + { + req->version_major = 0; + req->version_minor = 9; + break; + } + + if(!strcmp(str + version_offset, "HTTP/1.0")) + { + req->version_major = 1; + req->version_minor = 0; + break; + } + + if(!strcmp(str + version_offset, "HTTP/1.1")) + { + req->version_major = 1; + req->version_minor = 1; + break; + } + + if(!strcmp(str + version_offset, "HTTP/2.0")) + { + req->version_major = 2; + req->version_minor = 0; + break; + } + + if(!strcmp(str + version_offset, "HTTP/3.0")) + { + req->version_major = 3; + req->version_minor = 0; + break; + } + + bad_version = 1; + break; + + case sizeof("HTTP/M")-1: + + if(!strcmp(str + version_offset, "HTTP/1")) + { + req->version_major = 1; + req->version_minor = 0; + break; + } + + if(!strcmp(str + version_offset, "HTTP/2")) + { + req->version_major = 2; + req->version_minor = 0; + break; + } + + if(!strcmp(str + version_offset, "HTTP/3")) + { + req->version_major = 3; + req->version_minor = 0; + break; + } + + bad_version = 1; + break; + + default: + bad_version = 1; + break; + } + + if(bad_version) + { + free(headers.list); + return FAILURE("Bad HTTP version"); + } + } + + return OK; + + #undef OK + #undef FAILURE + #undef INTERNAL_FAILURE +} + +static bool upload(conn_t *conn) +{ + if(conn->failed_to_append) + return 0; + + + if(conn->out.used > 0) + { + /* Flush the output buffer. */ + uint32_t sent, total; + + sent = 0; + total = conn->out.used; + + if(total == 0) + return 1; + + while(sent < total) + { + microtcp_errcode_t errcode; + size_t n = microtcp_send(conn->sock, conn->out.data + sent, total - sent, true, &errcode); + + if(n == 0) + { + if (errcode == MICROTCP_ERRCODE_WOULDBLOCK) + break; + + // ERROR! + return 0; + } + + sent += n; + } + + memmove(conn->out.data, conn->out.data + sent, total - sent); + conn->out.used -= sent; + } + return 1; +} + +static uint32_t find(const char *str, uint32_t len, const char *seq) +{ + if(seq == NULL || seq[0] == '\0') + return UINT32_MAX; + + if(str == NULL || len == 0) + return UINT32_MAX; + + uint32_t i = 0, seqlen = strlen(seq); + while(1) + { + while(i < len && str[i] != seq[0]) + i += 1; + + if(i == len) + return UINT32_MAX; + + assert(str[i] == seq[0]); + + if(i > len - seqlen) + return UINT32_MAX; + + if(!strncmp(seq, str + i, seqlen)) + return i; + + i += 1; + } +} + +static void append_string_to_output_buffer(conn_t *conn, xh_string data) +{ + if(conn->failed_to_append) + return; + + if(conn->out.size - conn->out.used < (uint32_t) data.len) + { + uint32_t new_size = 2 * conn->out.size; + + if(new_size < conn->out.used + (uint32_t) data.len) + new_size = conn->out.used + data.len; + + void *temp = realloc(conn->out.data, new_size); + + if(temp == NULL) + { + conn->failed_to_append = 1; + return; + } + + conn->out.data = temp; + conn->out.size = new_size; + } + + memcpy(conn->out.data + conn->out.used, data.str, data.len); + conn->out.used += data.len; + return; +} + +static bool client_wants_to_keep_alive(xh_request *req) +{ + bool keep_alive; + + const char *h_connection = xh_header_get(req, "Connection"); + + if(h_connection == NULL) + // No [Connection] header. No keep-alive. + keep_alive = 0; + else + { + // TODO: Make string comparisons case and whitespace insensitive. + if(!strcmp(h_connection, " Keep-Alive")) + keep_alive = 1; + else if(!strcmp(h_connection, " Close")) + keep_alive = 0; + else + keep_alive = 0; + } + + return keep_alive; +} + +static bool server_wants_to_keep_alive(context_t *ctx, conn_t *conn) +{ + bool keep_alive; + + if(conn->served >= 20) + keep_alive = 0; + + if(ctx->connum > 0.6 * MICROHTTP_MAX_CLIENTS) + keep_alive = 0; + + return keep_alive; +} + +static void append_response_status_line_to_output_buffer(conn_t *conn, int status) +{ + char buffer[256]; + + const char *status_text = statis_code_to_status_text(status); + assert(status_text != NULL); + + int n = snprintf(buffer, sizeof(buffer), "HTTP/1.1 %d %s\r\n", + status, status_text); + assert(n >= 0); + + if((unsigned int) n > sizeof(buffer)-1) + n = sizeof(buffer)-1; + + append_string_to_output_buffer(conn, xh_string_new(buffer, n)); +} + +static void append_response_head_to_output_buffer(xh_response *res, conn_t *conn) +{ + append_response_status_line_to_output_buffer(conn, res->status); + for(int i = 0; i < res->headers.count; i += 1) + { + xh_pair header = res->headers.list[i]; + append_string_to_output_buffer(conn, header.key); + append_string_to_output_buffer(conn, xh_string_from_literal(": ")); + append_string_to_output_buffer(conn, header.val); + append_string_to_output_buffer(conn, xh_string_from_literal("\r\n")); + } + append_string_to_output_buffer(conn, xh_string_from_literal("\r\n")); +} + +static void generate_response_by_calling_the_callback(context_t *ctx, conn_t *conn) +{ + xh_request *req = &conn->request.public; + + // If it's a HEAD request, tell the callback that + // it's a GET request but then throw awaiy the body. + bool head_only = 0; + if(req->method_id == XH_HEAD) + { + head_only = 1; + req->method_id = XH_GET; + req->method = xh_string_from_literal("GET"); + } + + xh_response2 res2; + xh_response *res = &res2.public; + { + res_init(&res2); + + ctx->callback(req, res, ctx->userp); + + req_deinit(req); + + if(res2.failed) + { + /* Callback failed to build the response. + * Overwrite with a new error response. + */ + res_reinit(&res2); + res->status = 500; + } + } + + if(res->body.str == NULL) + res->body.str = ""; + + if(res->body.len < 0) + res->body.len = strlen(res->body.str); + + int content_length = res->body.len; + + assert(content_length >= 0); + + bool callback_wants_to_keep_alive = !res->close; + bool keep_alive = client_wants_to_keep_alive(req) + && server_wants_to_keep_alive(ctx, conn) + && callback_wants_to_keep_alive; + + xh_header_add(res, "Content-Length", "%d", content_length); + xh_header_add(res, "Connection", keep_alive ? "Keep-Alive" : "Close"); + append_response_head_to_output_buffer(res, conn); + + /* Now write the body to the output or, if the * + * request was originally HEAD, throw the body * + * away. */ + + if(!head_only) + append_string_to_output_buffer(conn, res->body); + + conn->served += 1; + + if(!keep_alive) + conn->close_when_uploaded = 1; + + res_deinit(&res2); +} + +static uint32_t determine_content_length(xh_request *req) +{ + int i; + for(i = 0; i < req->headers.count; i += 1) + if(!strcmp(req->headers.list[i].key.str, + "Content-Length")) // TODO: Make it case-insensitive. + break; + + if(i == req->headers.count) + // No Content-Length header. + // Assume a length of 0. + return 0; + + const char *s = req->headers.list[i].val.str; + unsigned int k = 0; + + while(is_space(s[k])) + k += 1; + + if(s[k] == '\0') + // Header Content-Length is empty. + // Assume a length of 0. + return 0; + + if(!is_digit(s[k])) + // The first non-space character + // isn't a digit. That's bad. + return UINT32_MAX; + + uint32_t result = s[k] - '0'; + + k += 1; + + while(is_digit(s[k])) + { + result = result * 10 + s[k] - '0'; + k += 1; + } + + while(is_space(s[k])) + k += 1; + + if(s[k] != '\0') + // The header contains something other + // than whitespace and digits. Bad. + return UINT32_MAX; + + return result; +} + +static void when_data_is_ready_to_be_read(context_t *ctx, conn_t *conn) +{ + // Download the data in the input buffer. + uint32_t downloaded; + { + buffer_t *b = &conn->in; + uint32_t before = b->used; + while(1) + { + if(b->size - b->used < 128) + { + uint32_t new_size = (b->size == 0) ? 512 : (2 * b->size); + + // NOTE: We allocate one extra byte because this + // way we're sure that any sub-string of the + // buffer can be safely made zero-terminated + // by writing a zero after it temporarily. + void *temp = realloc(b->data, new_size + 1); + + if(temp == NULL) + { + // ERROR! + close_connection(ctx, conn); + return; + } + + // TODO: Change the pointers in conn->request + // if the head was already parsed. + + b->data = temp; + b->size = new_size; + } + + assert(b->size > b->used); + + microtcp_errcode_t errcode; + size_t n = microtcp_recv(conn->sock, b->data + b->used, b->size - b->used, true, &errcode); + + if(n == 0) + { + // Peer disconnected or an error occurred + + if(errcode == MICROTCP_ERRCODE_WOULDBLOCK) + break; // Done downloading. + if (errcode != MICROTCP_ERRCODE_NONE) + { + // An error occurred. + close_connection(ctx, conn); + return; + } + + // Peer disconnected. + close_connection(ctx, conn); + return; + } + + b->used += n; + } + downloaded = b->used - before; + } + + int served_during_this_while_loop = 0; + + while(1) + { + if(!conn->head_received) + { + // Search for an \r\n\r\n. + uint32_t i; + { + uint32_t start = 0; + if(served_during_this_while_loop == 0 && conn->in.used > downloaded + 3) + start = conn->in.used - downloaded - 3; + + i = find(conn->in.data + start, conn->in.used - start, "\r\n\r\n"); + + if(i == UINT32_MAX) + // No \r\n\r\n found. The head of the request wasn't fully received yet. + return; + + // i is relative to start. + i += start; + } + + struct parse_err_t err = parse(conn->in.data, i+4, &conn->request.public); + + uint32_t len = 0; // Anything other than UINT32_MAX goes. + if(err.msg == NULL) + len = determine_content_length(&conn->request.public); // Returns UINT32_MAX on failure. + + if(err.msg != NULL || len == UINT32_MAX) + { + char buffer[512]; + if(len == UINT32_MAX) + { + static const char msg[] = "Couldn't determine the content length"; + (void) snprintf(buffer, sizeof(buffer), + "HTTP/1.1 400 Bad Request\r\n" + "Content-Type: text/plain;charset=utf-8\r\n" + "Content-Length: %d\r\n" + "Connection: Close\r\n" + "\r\n%s", (int) sizeof(msg)-1, msg); + } + else if(err.internal) + { + (void) snprintf(buffer, sizeof(buffer), + "HTTP/1.1 500 Internal Server Error\r\n" + "Content-Type: text/plain;charset=utf-8\r\n" + "Content-Length: %d\r\n" + "Connection: Close\r\n" + "\r\n%s", err.len, err.msg); + } + else + { + // 400 Bad Request. + (void) snprintf(buffer, sizeof(buffer), + "HTTP/1.1 400 Bad Request\r\n" + "Content-Type: text/plain;charset=utf-8\r\n" + "Content-Length: %d\r\n" + "Connection: Close\r\n" + "\r\n%s", err.len, err.msg); + } + + // NOTE: If the static buffer [buffer] is too small + // to hold the response then the response will + // be sent truncated. But that's not a problem + // since we'll close the connection after this + // response either way. + + append_string_to_output_buffer(conn, xh_string_new(buffer, -1)); + conn->close_when_uploaded = 1; + return; + } + + conn->head_received = 1; + conn->body_offset = i + 4; + conn->body_length = len; + } + + if(conn->head_received && conn->body_offset + conn->body_length <= conn->in.used) + { + /* The rest of the body arrived. */ + + // Make the body temporarily zero-terminated: get the byte + // that comes after the body, then overwrite it with a '\0'. + // When you don't need it to be zero-terminated anymore, + // put the saved byte back in. + + char first_byte_after_body_in_input_buffer + = conn->in.data[conn->body_offset + conn->body_length]; + + conn->in.data[conn->body_offset + conn->body_length] = '\0'; + + xh_request *req = &conn->request.public; + req->body = xh_string_new(conn->in.data + conn->body_offset, conn->body_length); + + generate_response_by_calling_the_callback(ctx, conn); + + // Restore the byte after the body. + conn->in.data[conn->body_offset + conn->body_length] + = first_byte_after_body_in_input_buffer; + + // Remove the request from the input buffer by + // copying back its remaining contents. + uint32_t consumed = conn->body_offset + conn->body_length; + memmove(conn->in.data, conn->in.data + consumed, conn->in.used - consumed); + conn->in.used -= consumed; + conn->head_received = 0; + + served_during_this_while_loop += 1; + + if(conn->close_when_uploaded) + break; + } + } +} + +void xh_quit(xh_handle handle) +{ + context_t *ctx = handle; + ctx->exiting = 1; +} + +static const char *init(context_t *context, unsigned short port, + microhttp_config_t config) +{ + microtcp_callbacks_t callbacks = { + .data = config.userp, + .free = NULL, + .recv = config.recv_frame, + .send = config.send_frame, + }; + microtcp_t *mtcp = microtcp_create_using_callbacks(config.ip, config.mac, callbacks); + if (!mtcp) + return "Failed to initialize TCP"; + + microtcp_errcode_t errcode; + + context->sock = microtcp_open(mtcp, port, &errcode); + if (!context->sock) + return microtcp_strerror(errcode); + + context->mux = microtcp_mux_create(mtcp); + if (!context->mux) + { + microtcp_close(context->sock); + return microtcp_strerror(errcode); + } + + if (!microtcp_mux_register(context->mux, context->sock, MICROTCP_MUX_ACCEPT, NULL)) + { + microtcp_close(context->sock); + microtcp_mux_destroy(context->mux); + return "Failed to register the listener into the io multiplexer"; + } + + for(unsigned int i = 0; i < MICROHTTP_MAX_CLIENTS; i += 1) { + context->pool[i].sock = NULL; + context->pool[i].next = context->pool + i + 1; + } + + context->pool[MICROHTTP_MAX_CLIENTS-1].next = NULL; + context->freelist = context->pool; + + context->connum = 0; + context->exiting = 0; + return NULL; +} + +const char *xhttp(unsigned short port, xh_callback callback, + void *userp, xh_handle *handle, + microhttp_config_t config) +{ + context_t context; + + const char *error = init(&context, port, config); + + if(error != NULL) + return error; + + context.callback = callback; + context.userp = userp; + + if(handle) + *handle = &context; + + while(!context.exiting) + { + microtcp_muxevent_t ev; + if (!microtcp_mux_wait(context.mux, &ev)) + continue; + + if(ev.userp == NULL) + { + // New connection. + accept_connection(&context); + continue; + } + + conn_t *conn = ev.userp; +/* + if(ev.events & EPOLLRDHUP) + { + // Disconnection. + close_connection(&context, conn); + continue; + } + + if(ev.events & (EPOLLERR | EPOLLHUP)) + { + // Connection closed or an error occurred. + // We continue as nothing happened so that + // the error is reported on the [recv] or + // [send] call site. + ev.events = EPOLLIN | EPOLLOUT; + } +*/ + int old_connum = context.connum; + + if((ev.events & MICROTCP_MUX_RECV) + && conn->close_when_uploaded == 0) + { + // Note that this may close the connection. If any logic + // were to come after this function, it couldn't refer + // to the connection structure. + when_data_is_ready_to_be_read(&context, conn); + } + + if(old_connum == context.connum) + { + // The connection wasn't closed. Try to + // upload the data in the output buffer. + + if(!upload(conn)) + + close_connection(&context, conn); + + else + if(conn->out.used == 0 && conn->close_when_uploaded) + close_connection(&context, conn); + } + } + + for(unsigned int i = 0; i < MICROHTTP_MAX_CLIENTS; i += 1) + if(context.pool[i].sock != NULL) + close_connection(&context, context.pool + i); + + (void) microtcp_close(context.sock); + (void) microtcp_mux_destroy(context.mux); + return NULL; +} + +int xh_urlcmp(const char *URL, const char *fmt, ...) +{ + va_list va; + va_start(va, fmt); + int res = xh_vurlcmp(URL, fmt, va); + va_end(va); + return res; +} + +/* Returns: + * 0 - Match + * 1 - No match + * -1 - Error + */ +int xh_vurlcmp(const char *URL, const char *fmt, va_list va) +{ +#define MATCH 0 +#define ERROR -1 +#define NOMATCH 1 + + long i = 0; // Cursor over [fmt] + long j = 0; // Cursor over [URL] + while(1) { + + while(fmt[i] != '\0' && fmt[i] != ':') { + + if(URL[j] != fmt[i]) + return NOMATCH; + + i += 1; + j += 1; + } + + if(fmt[i] == '\0' || URL[j] == '\0') + break; + + assert(URL[j] != '\0'); + assert(fmt[i] == ':'); + + i += 1; // Skip ':' + + if(fmt[i] == 'd') { + + if(!isdigit(URL[j])) + return NOMATCH; + + long long buff = 0; + + do { + + long d = (URL[j] - '0'); + + if(buff > (LLONG_MAX - d) / 10) + return ERROR; /* Overflow */ + + buff = buff * 10 + d; + + j += 1; + + } while(isdigit(URL[j])); + + long long *dst = va_arg(va, long long*); + if(dst != NULL) + *dst = buff; + + } else if(fmt[i] == 's') { + + long off = j; + while(URL[j] != '\0' && URL[j] != '/' && URL[j] != fmt[i+1]) + j += 1; + long len = j - off; + + long dst_len = va_arg(va, long); + char *dst_ptr = va_arg(va, char*); + + if(dst_ptr != NULL && dst_len > 0) { + long copy; + if(dst_len >= len+1) + copy = len; + else + copy = dst_len-1; + memcpy(dst_ptr, URL + off, copy); + dst_ptr[copy] = '\0'; + } + + } else + /* Format ended unexpectedly or + got an invalid format specifier. */ + return ERROR; + + i += 1; // Skip the 'd' or 's' + } + + /* If the program gets here it means that either + * [fmt] or [URL] ended. If that's the case, if + * the other didn't end, then there's no match. + */ + if(fmt[i] != '\0' || URL[j] != '\0') + return NOMATCH; + + return MATCH; + +#undef MATCH +#undef ERROR +#undef NOMATCH +} \ No newline at end of file diff --git a/examples/microhttp/xhttp.h b/examples/microhttp/xhttp.h new file mode 100644 index 0000000..9c6ba00 --- /dev/null +++ b/examples/microhttp/xhttp.h @@ -0,0 +1,81 @@ +#ifndef XHTTP_H +#define XHTTP_H + +#define MICROHTTP_MAX_CLIENTS 64 + +typedef struct { + void *userp; + const char *ip; + const char *mac; + int (*send_frame)(void *userp, const void *src, size_t len); + int (*recv_frame)(void *userp, void *dst, size_t len); +} microhttp_config_t; + +typedef void *xh_handle; + +typedef struct { + char *str; int len; +} xh_string; + +typedef struct { + xh_string key, val; +} xh_pair; + +typedef struct { + xh_pair *list; + int count; +} xh_table; + +typedef enum { + XH_GET = 1, + XH_HEAD = 2, + XH_POST = 4, + XH_PUT = 8, + XH_DELETE = 16, + XH_CONNECT = 32, + XH_OPTIONS = 64, + XH_TRACE = 128, + XH_PATCH = 256, +} xh_method; + +typedef struct { + xh_method method_id; + xh_string method; + xh_string params; + xh_string URL; + unsigned int version_minor; + unsigned int version_major; + xh_table headers; + xh_string body; +} xh_request; + +typedef struct { + int status; + xh_table headers; + xh_string body; + _Bool close; +} xh_response; + +typedef void (*xh_callback)(xh_request*, xh_response*, void*); + +const char *xhttp(unsigned short port, xh_callback callback, + void *userp, xh_handle *handle, + microhttp_config_t config); +void xh_quit(xh_handle handle); + +void xh_header_add(xh_response *res, const char *name, const char *valfmt, ...); +void xh_header_rem(xh_response *res, const char *name); +const char *xh_header_get(void *req_or_res, const char *name); +_Bool xh_header_cmp(const char *a, const char *b); + +int xh_urlcmp(const char *URL, const char *fmt, ...); +int xh_vurlcmp(const char *URL, const char *fmt, va_list va); + + +#define xh_string_new(s, l) \ + ((xh_string) { (s), ((int) (l)) < 0 ? (int) strlen(s) : (int) (l) }) + +#define xh_string_from_literal(s) \ + ((xh_string) { (s), sizeof(s)-1 }) + +#endif // #ifndef XHTTP_H \ No newline at end of file diff --git a/examples/queue.c b/examples/queue.c deleted file mode 100644 index 2bcfd32..0000000 --- a/examples/queue.c +++ /dev/null @@ -1,302 +0,0 @@ -#include "queue.h" - -typedef struct queue_entry_t queue_entry_t; -typedef struct queue_event_t queue_event_t; - -struct queue_entry_t { - queue_entry_t *prev; - queue_entry_t *next; - void *data; - int events; - microtcp_queue_t *queue; - microtcp_socket_t *socket; - queue_event_t *event_entry; -}; - -struct queue_event_t { - - queue_event_t *prev; - queue_event_t *next; - - int events; - queue_entry_t *socket_entry; -}; - -struct microtcp_queue_t { - - microtcp_t *mtcp; - - pthread_cond_t something_happened; - - queue_event_t *queue_head; - queue_event_t *queue_tail; - - queue_entry_t *entry_free_list; - queue_entry_t entry_pool[MICROTCP_QUEUE_ENTRIES_MAX]; - - queue_event_t *event_free_list; - queue_event_t event_pool[MICROTCP_QUEUE_ENTRIES_MAX]; -}; - -static event_entry_t* -pop_event_entry_from_queue(microtcp_queue_t *queue) -{ - event_entry_t *event; - - if (queue->queue_tail) { - - // An event is present in the queue. Pop it. - - event = queue->queue_tail; - - if (event->prev) - event->prev->next = NULL; - else - queue->queue_head = NULL; - queue->queue_tail = event->prev; - - } else - event = NULL; - - return event; -} - -microtcp_event_t microtcp_queue_next(microtcp_queue_t *queue, bool no_block) -{ - microtcp_t *mtcp; - - event_entry_t *event = pop_event_entry_from_queue(queue); - - while (!event && !no_block) { - pthread_cond_wait(&queue->something_happened, &mtcp->lock); - event = pop_event_entry_from_queue(queue); - } - - microtcp_event_t result; - - if (event) { - queue_entry_t *socket_entry = event->socket_entry; - - result.type = event->events; - result.data = socket_entry->data; - result.socket = socket_entry->socket; - - } else { - result.type = 0; - result.data = NULL; - result.socket = NULL; - } - - return result; -} - -static void signal_socket_events_to_queues(microtcp_socket_t *socket, int events) -{ - queue_entry_t *entry = socket->queue_entry; - while (entry) { - - int interesting_events = events & entry->events; - - if (interesting_events) { - // Some events that are of interest to - // this queue are being signaled. - // Push them onto the queue's event queue - - microtcp_queue_t *queue = entry->queue; - - if (entry->event_entry) { - // At least one event was already signaled, so - // an event structure was already created. - // We just need to add to it the newly signaled - // events. - event_entry_t *event = entry->event_entry; - event->events |= interesting_events; - } else { - - // No events related to this socket are in the - // queue, so we need to create a new event structure. - // - // Unused event structure are organized in a free - // list, so we just need to pop one from there. - // - // We know for sure that a free event structure is - // available at this point because there is one for - // each socket registered into the queue. - assert(queue->event_free_list); - - event_entry_t *event = queue->event_free_list; - queue->event_free_list = event->next; - - // Set-up the event structure - event->prev = NULL; - event->next = NULL; - event->events = interesting_events; - event->socket_entry = entry; - - // Now push the event structure into the queue's queue - event->next = queue->queue_head; - if (queue->queue_head) - queue->queue_head->prev = event; - else - queue->queue_tail = event; - queue->queue_head = event; - } - - pthread_cond_signal(&queue->something_happened); - } - - entry = entry->next; - } -} - -microtcp_queue_t *microtcp_queue_create(microtcp_t *mtcp) -{ - microtcp_queue_t *queue = malloc(sizeof(microtcp_queue_t)); - if (!queue) - return NULL; - - queue->mtcp = mtcp; - queue->queue_head = NULL; - queue->queue_tail = NULL; - queue->entry_free_list = queue->entry_pool; - queue->event_free_list = queue->event_pool; - - for (size_t i = 0; i < MICROTCP_QUEUE_ENTRIES_MAX-1; i++) { - mtcp->entry_pool[i].mtcp = NULL; - mtcp->entry_pool[i].prev = NULL; - mtcp->entry_pool[i].next = mtcp->entry_pool + i+1; - - mtcp->event_pool[i].mtcp = NULL; - mtcp->event_pool[i].prev = NULL; - mtcp->event_pool[i].next = mtcp->event_pool + i+1; - } - mtcp->entry_pool[MICROTCP_QUEUE_ENTRIES_MAX-1].mtcp = NULL; - mtcp->entry_pool[MICROTCP_QUEUE_ENTRIES_MAX-1].prev = NULL; - mtcp->entry_pool[MICROTCP_QUEUE_ENTRIES_MAX-1].next = NULL; - - mtcp->event_pool[MICROTCP_QUEUE_ENTRIES_MAX-1].prev = NULL; - mtcp->event_pool[MICROTCP_QUEUE_ENTRIES_MAX-1].next = NULL; - - if (pthread_cond_init(&queue->something_happened, NULL)) { - free(queue); - return NULL; - } - - return queue; -} - -void microtcp_queue_destroy(microtcp_queue_t *queue) -{ - for (size_t i = 0; i < MICROTCP_QUEUE_ENTRIES_MAX; i++) { - if (queue->entries[i].mtcp == NULL) - continue; - microtcp_queue_unregister(queue, queue->entries[i].socket, MICROTCP_EVENT_ALL); - } - free(queue); -} - -bool microtcp_queue_register(microtcp_queue_t *queue, microtcp_socket_t *socket, void *data, int events) -{ - if (!events) - return true; // No events registered - - if (!queue->free_list) - return false; // Registration limit reached - - queue_entry_t *entry = queue->free_list; - queue->free_list = entry->next; - - entry->prev = NULL; - entry->next = NULL; - entry->data = data; - entry->queue = queue; - entry->events = events; - entry->socket = socket; - - entry->next = socket->queue_entry; - if (socket->queue_entry) - socket->queue_entry->prev = entry; - socket->queue_entry = entry; - - return true; -} - -static queue_entry_t *find_entry(microtcp_queue_t *queue, microtcp_socket_t *socket) -{ - queue_entry_t *entry = socket->queue_entry; - while (entry) { - if (entry->queue == queue) - return entry; - entry = entry->next; - } - return NULL; -} - -static void -unregister_socket_from_all_queues(microtcp_socket_t *socket) -{ - while (socket->queue_entry) - microtcp_queue_unregister(socket->queue_entry->queue, socket, MICROTCP_EVENT_ALL); -} - -bool microtcp_queue_unregister(microtcp_queue_t *queue, microtcp_socket_t *socket, int unregister_events) -{ - if (!queue || !socket || queue->mtcp != socket->mtcp) - return false; - - queue_entry_t *entry = find_entry(queue, socket); - if (!entry) - return false; // Socket wasn't registered in this queue - - int remaining_events = entry->events & ~unregister_events; - - if (!remaining_events) { - - // All events were unregistered, so the - // socket must be removed from the queue. - - // Unlink any events related to this socket - // from the queue - queue_event_t *event = entry->event_entry; - if (event) { - - // Remove the event structure from the queue - if (event->prev) - event->prev->next = event->next; - else - queue->queue_head = event->next; - - if (event->next) - event->next->prev = event->prev; - else - queue->queue_tail = event->prev; - - // Push the now unlinked event structure onto - // the free list - event->prev = NULL; - event->next = queue->event_free_list; - queue->event_free_list = event; - } - - // Unlink the entry from the socket's list - if (entry->prev) - entry->prev->next = entry->next; - else - socket->queue_entry = entry->next; - - if (entry->next) - entry->next->prev = entry->prev; - - // Put the entry structure back into the - // free list of the queue - entry->mtcp = NULL; // It's important to set this to NULL because - // this way the queue knows it's not used anymore. - entry->prev = NULL; - entry->next = queue->free_list; - queue->free_list = entry; - - } else - entry->events = remaining_events; - - return true; -} \ No newline at end of file diff --git a/examples/queue.h b/examples/queue.h deleted file mode 100644 index bf2c36c..0000000 --- a/examples/queue.h +++ /dev/null @@ -1,25 +0,0 @@ - -enum { - MICROTCP_EVENT_NONE = 0, - MICROTCP_EVENT_SEND = 1, - MICROTCP_EVENT_RECV = 2, - MICROTCP_EVENT_ACCEPT = 4, - - MICROTCP_EVENT_ALL = MICROTCP_EVENT_RECV - | MICROTCP_EVENT_SEND - | MICROTCP_EVENT_ACCEPT, -}; - -typedef struct microtcp_queue_t microtcp_queue_t; - -typedef struct { - int type; - void *data; - microtcp_socket_t *socket; -} microtcp_event_t; - -microtcp_queue_t *microtcp_queue_create(microtcp_t *mtcp); -void microtcp_queue_destroy(microtcp_queue_t *queue); -microtcp_event_t microtcp_queue_next(microtcp_queue_t *queue, bool no_block); -bool microtcp_queue_register(microtcp_queue_t *queue, microtcp_socket_t *socket, void *data, int events); -bool microtcp_queue_unregister(microtcp_queue_t *queue, microtcp_socket_t *socket, int unregister_events); diff --git a/examples/queue2.c b/examples/queue2.c deleted file mode 100644 index cb5e4a7..0000000 --- a/examples/queue2.c +++ /dev/null @@ -1,54 +0,0 @@ - -typedef struct { - size_t size; - size_t used; - uint8_t *data; -} buffer_t; - -typedef struct client_t client_t; -struct client_t { - client_t *prev; - client_t *next; - microtcp_socket_t *socket; - buffer_t input; - buffer_t output; -}; - -int main(void) -{ - microtcp_t *mtcp = microtcp_create(); - - microtcp_socket_t *server = microtcp_open(mtcp, 80); - - microtcp_queue_t *queue = microtcp_queue_create(mtcp); - microtcp_queue_register(queue, server, NULL, MICROTCP_EVENT_ACCEPT); - while (1) { - microtcp_event_t event = microtcp_queue_next(queue, no_block); - switch (event.type) { - - case MICROTCP_EVENT_NONE: - break; - - case MICROTCP_EVENT_READ: - break; - - case MICROTCP_EVENT_RECV: - { - microtcp_socket_t *client_socket = event.socket; - microtcp_recv(client_socket, ); - } - break; - - case MICROTCP_EVENT_ACCEPT: - { - microtcp_socket_t *client_socket = microtcp_accept(event.socket, true); - microtcp_queue_register(queue, client_socket, NULL, MICROTCP_EVENT_SEND | MICROTCP_EVENT_RECV); - } - break; - } - } - microtcp_queue_destroy(queue); - - microtcp_destroy(mtcp); - return 0; -} \ No newline at end of file diff --git a/examples/test.c b/examples/test.c deleted file mode 100644 index ca8d570..0000000 --- a/examples/test.c +++ /dev/null @@ -1,26 +0,0 @@ -#include "mutcp.h" - -int main(void) -{ - mutcp_t state; - mutcp_create(&state); - - mutcp_listener_socket_t *listener = - mutcp_listener_socket_create(&state, 8080); - - while (1) { - mutcp_socket_t *socket = - mutcp_listener_socket_accept(listener); - - char buffer[1024]; - size_t num = mutcp_socket_read(socket, buffer, sizeof(buffer)); - mutcp_socket_write(socket, "echo: ", 6); - mutcp_socket_write(socket, buffer, num); - - mutcp_socket_destroy(socket); - } - - mutcp_listener_socket_destroy(listener); - mutcp_destroy(&state); - return 0; -} diff --git a/include/microtcp.h b/include/microtcp.h index 9a9522d..9846ae8 100644 --- a/include/microtcp.h +++ b/include/microtcp.h @@ -9,6 +9,7 @@ typedef struct microtcp_socket_t microtcp_socket_t; #define MICROTCP_MAX_BUFFERS 8 #define MICROTCP_MAX_SOCKETS 32 +#define MICROTCP_MAX_MUX_ENTRIES 32 typedef enum { @@ -28,6 +29,9 @@ typedef enum { // Returned by microtcp_recv and microtcp_send MICROTCP_ERRCODE_NOTCONNECTION, + // Returned by microtcp_accept, microtcp_recv and microtcp_send + MICROTCP_ERRCODE_WOULDBLOCK, + } microtcp_errcode_t; typedef struct { @@ -51,4 +55,26 @@ void microtcp_close(microtcp_socket_t *socket); size_t microtcp_send(microtcp_socket_t *socket, const void *src, size_t len, bool no_block, microtcp_errcode_t *errcode); size_t microtcp_recv(microtcp_socket_t *socket, void *dst, size_t len, bool no_block, microtcp_errcode_t *errcode); void microtcp_step(microtcp_t *mtcp); -void microtcp_process_packet(microtcp_t *mtcp, const void *packet, size_t len); \ No newline at end of file +void microtcp_process_packet(microtcp_t *mtcp, const void *packet, size_t len); + +#ifdef MICROTCP_USING_MUX +typedef enum { + MICROTCP_MUX_ACCEPT = 1, + MICROTCP_MUX_RECV = 2, + MICROTCP_MUX_SEND = 4, +} microtcp_muxeventid_t; + +typedef struct { + void *userp; + int events; + microtcp_socket_t *socket; +} microtcp_muxevent_t; + +typedef struct microtcp_mux_t microtcp_mux_t; +microtcp_mux_t *microtcp_mux_create(microtcp_t *mtcp); +void microtcp_mux_destroy(microtcp_mux_t *mux); +bool microtcp_mux_register(microtcp_mux_t *mux, microtcp_socket_t *sock, int events, void *userp); +bool microtcp_mux_unregister(microtcp_mux_t *mux, microtcp_socket_t *sock, int events); +bool microtcp_mux_poll(microtcp_mux_t *mux, microtcp_muxevent_t *ev); +bool microtcp_mux_wait(microtcp_mux_t *mux, microtcp_muxevent_t *ev); +#endif \ No newline at end of file diff --git a/makefile b/makefile index 1984870..e755e92 100644 --- a/makefile +++ b/makefile @@ -45,10 +45,10 @@ endif # One of: drmemory, valgrind MEMDBG=valgrind -LIBDIR = 3p/lib/ -INCDIR = 3p/include/ +LIBDIR = 3p/lib +INCDIR = 3p/include -CFLAGS = $(CFLAGS_PLATFORM) -I$(INCDIR) -Iinclude/ -Wall -Wextra -DARP_DEBUG -DMICROTCP_DEBUG -DIP_DEBUG -DICMP_DEBUG -DTCP_DEBUG -DMICROTCP_BACKGROUND_THREAD -DMICROTCP_USING_TAP +CFLAGS = $(CFLAGS_PLATFORM) -I$(INCDIR) -Iinclude/ -Wall -Wextra -DARP_DEBUG -DMICROTCP_DEBUG -DIP_DEBUG -DICMP_DEBUG -DTCP_DEBUG -DMICROTCP_BACKGROUND_THREAD -DMICROTCP_USING_TAP -DMICROTCP_USING_MUX LFLAGS = -ltuntap $(LFLAGS_PLATFORM) -L$(LIBDIR) ifeq ($(MEMDBG),drmemory) @@ -57,12 +57,9 @@ else CFLAGS += -g endif -INCDIR=3p/include -LIBDIR=3p/lib - .PHONY: all clean -all: loop2 +all: build/microtcp.h build/microtcp.c build/echo_tcp build/echo_http 3p/lib/libtuntap.a: 3p/libtuntap/build/lib/libtuntap.a cp 3p/libtuntap/build/lib/libtuntap.a $(LIBDIR) @@ -90,10 +87,55 @@ all: loop2 3p/src/tinycthread.c: 3p/tinycthread/source/tinycthread.c cp 3p/tinycthread/source/tinycthread.c 3p/src -loop2: $(LIBDIR)/libtuntap.a 3p/include/tuntap.h 3p/include/tuntap-export.h 3p/src/tinycthread.c 3p/include/tinycthread.h - gcc src/endian.c src/arp.c src/ip.c src/icmp.c src/tcp.c src/microtcp.c test/loop2.c 3p/src/tinycthread.c -o loop2 $(CFLAGS) $(LFLAGS) +build/echo_tcp: examples/echo_tcp.c $(LIBDIR)/libtuntap.a 3p/include/tuntap.h 3p/include/tuntap-export.h build/microtcp.c build/microtcp.h + mkdir -p $(@D) + gcc build/microtcp.c examples/echo_tcp.c -o $@ $(CFLAGS) $(LFLAGS) + +build/microtcp.h: include/microtcp.h + mkdir -p $(@D) + [ ! -e $@ ] || rm $@ + echo "#define MICROTCP_AMALGAMATION" >> $@ + cat include/microtcp.h >> $@ + +build/microtcp.c: 3p/include/tinycthread.h 3p/src/tinycthread.c $(wildcard src/*.c src/*.h) + mkdir -p $(@D) + [ ! -e $@ ] || rm $@ + printf "#include \"microtcp.h\"\n" > $@ + printf "#ifdef MICROTCP_BACKGROUND_THREAD" >> $@ + printf "\n#line 1 \"3p/include/tinycthread.h\"\n" >> $@ + cat 3p/include/tinycthread.h >> $@ + printf "\n#line 1 \"3p/src/tinycthread.c\"\n" >> $@ + cat 3p/src/tinycthread.c >> $@ + printf "\n#endif /* MICROTCP_BACKGROUND_THREAD */" >> $@ + printf "\n#line 1 \"src/defs.h\"\n" >> $@ + cat src/defs.h >> $@ + printf "\n#line 1 \"src/endian.h\"\n" >> $@ + cat src/endian.h >> $@ + printf "\n#line 1 \"src/arp.h\"\n" >> $@ + cat src/arp.h >> $@ + printf "\n#line 1 \"src/icmp.h\"\n" >> $@ + cat src/icmp.h >> $@ + printf "\n#line 1 \"src/ip.h\"\n" >> $@ + cat src/ip.h >> $@ + printf "\n#line 1 \"src/tcp.h\"\n" >> $@ + cat src/tcp.h >> $@ + printf "\n#line 1 \"src/endian.c\"\n" >> $@ + cat src/endian.c >> $@ + printf "\n#line 1 \"src/arp.c\"\n" >> $@ + cat src/arp.c >> $@ + printf "\n#line 1 \"src/icmp.c\"\n" >> $@ + cat src/icmp.c >> $@ + printf "\n#line 1 \"src/ip.c\"\n" >> $@ + cat src/ip.c >> $@ + printf "\n#line 1 \"src/tcp.c\"\n" >> $@ + cat src/tcp.c >> $@ + printf "\n#line 1 \"src/microtcp.c\"\n" >> $@ + cat src/microtcp.c >> $@ + +build/echo_http: $(LIBDIR)/libtuntap.a 3p/include/tuntap.h 3p/include/tuntap-export.h build/microtcp.h build/microtcp.c + gcc examples/microhttp/main.c examples/microhttp/xhttp.c build/microtcp.c -o $@ $(CFLAGS) $(LFLAGS) clean: - rm -f loop2 loop2.exe + rm -fr build rm -fr 3p/libtuntap/build rm -f 3p/lib/* 3p/include/* \ No newline at end of file diff --git a/src/arp.c b/src/arp.c index 788c084..fae8ac5 100644 --- a/src/arp.c +++ b/src/arp.c @@ -115,8 +115,11 @@ struct ethhdr { */ #include + +#ifndef MICROTCP_AMALGAMATION #include "endian.h" #include "arp.h" +#endif #ifdef ARP_DEBUG #include diff --git a/src/arp.h b/src/arp.h index 33ce781..70f4670 100644 --- a/src/arp.h +++ b/src/arp.h @@ -1,7 +1,10 @@ #include #include #include + +#ifndef MICROTCP_AMALGAMATION #include "defs.h" +#endif #define ARP_MAX_PENDING_REQUESTS 32 #define ARP_TRANSLATION_TABLE_SIZE 128 diff --git a/src/endian.c b/src/endian.c index 04ecd07..0e167b8 100644 --- a/src/endian.c +++ b/src/endian.c @@ -1,4 +1,6 @@ +#ifndef MICROTCP_AMALGAMATION #include "endian.h" +#endif bool cpu_is_little_endian(void) { diff --git a/src/icmp.c b/src/icmp.c index bd84919..ebe7794 100644 --- a/src/icmp.c +++ b/src/icmp.c @@ -1,6 +1,9 @@ #include + +#ifndef MICROTCP_AMALGAMATION #include "endian.h" #include "icmp.h" +#endif #ifdef ICMP_DEBUG #include diff --git a/src/icmp.h b/src/icmp.h index 731ed97..f598666 100644 --- a/src/icmp.h +++ b/src/icmp.h @@ -1,5 +1,8 @@ #include + +#ifndef MICROTCP_AMALGAMATION #include "defs.h" +#endif typedef struct { void *output_ptr; diff --git a/src/ip.c b/src/ip.c index cc40997..6b63f08 100644 --- a/src/ip.c +++ b/src/ip.c @@ -1,6 +1,9 @@ #include + +#ifndef MICROTCP_AMALGAMATION #include "endian.h" #include "ip.h" +#endif #ifdef IP_DEBUG #include diff --git a/src/ip.h b/src/ip.h index a831ea1..2d0430e 100644 --- a/src/ip.h +++ b/src/ip.h @@ -1,8 +1,11 @@ #include #include #include + +#ifndef MICROTCP_AMALGAMATION #include "defs.h" #include "icmp.h" +#endif #define IP_PLUGGED_PROTOCOLS_MAX 4 diff --git a/src/microtcp.c b/src/microtcp.c index 2fc148a..c2358da 100644 --- a/src/microtcp.c +++ b/src/microtcp.c @@ -4,20 +4,22 @@ #include // strerror() #include #include -#include "ip.h" -#include "arp.h" -#include "tcp.h" -#include "endian.h" -#include + +#ifndef MICROTCP_AMALGAMATION +# include "ip.h" +# include "arp.h" +# include "tcp.h" +# include "endian.h" +# include "microtcp.h" +# ifdef MICROTCP_BACKGROUND_THREAD +# include "tinycthread.h" +# endif +#endif #ifdef MICROTCP_USING_TAP #include #endif -#ifdef MICROTCP_BACKGROUND_THREAD -#include "tinycthread.h" -#endif - #ifdef MICROTCP_DEBUG #include #define MICROTCP_DEBUG_LOG(fmt, ...) do { fprintf(stderr, "MICROTCP :: " fmt "\n", ## __VA_ARGS__); } while (0); @@ -33,6 +35,35 @@ #define UNLOCK_WHEN_THREADED(mtcp) do { (void) (mtcp); } while (0); #endif +#ifdef MICROTCP_USING_MUX +typedef struct mux_entry_t mux_entry_t; +struct mux_entry_t { + mux_entry_t **mux_prev; + mux_entry_t *mux_next; + mux_entry_t **sock_prev; + mux_entry_t *sock_next; + microtcp_mux_t *mux; // This is set on initialization + // of the parent microtcp_mux_t + // and never changed. + microtcp_socket_t *sock; + void *userp; + int triggered_events; + int events_of_interest; +}; + +struct microtcp_mux_t { + microtcp_t *mtcp; +#ifdef MICROTCP_BACKGROUND_THREAD + cnd_t queue_not_empty; +#endif + mux_entry_t *free_list; + mux_entry_t *idle_list; + mux_entry_t *ready_queue_head; + mux_entry_t *ready_queue_tail; + mux_entry_t entries[MICROTCP_MAX_MUX_ENTRIES]; +}; +#endif + typedef struct buffer_t buffer_t; struct buffer_t { microtcp_t *mtcp; @@ -56,6 +87,7 @@ struct microtcp_socket_t { tcp_listener_t *listener; tcp_connection_t *connection; }; + #ifdef MICROTCP_BACKGROUND_THREAD union { cnd_t something_to_accept; @@ -65,6 +97,10 @@ struct microtcp_socket_t { }; }; #endif + +#ifdef MICROTCP_USING_MUX + mux_entry_t *mux_list; +#endif }; struct microtcp_t { @@ -105,6 +141,7 @@ const char *microtcp_strerror(microtcp_errcode_t errcode) case MICROTCP_ERRCODE_BADCONDVAR: return "Condition variable error"; case MICROTCP_ERRCODE_NOTLISTENER: return "Invalid operation on a non-listener socket"; case MICROTCP_ERRCODE_CANTBLOCK: return "Can't execute a blocking call for this function"; + case MICROTCP_ERRCODE_WOULDBLOCK: return "Can't executa e non-blocking call for this function"; case MICROTCP_ERRCODE_NOTCONNECTION: return "Invalid operation on a non-connection socket"; } return "???"; @@ -367,7 +404,7 @@ void microtcp_step(microtcp_t *mtcp) // The call to [recv] (which is assumed to be blocking) // needs to be out of the critical section to give other - // threads the ability to progress in the mean time. + // threads the ability to progress in the mean time. int size = mtcp->callbacks.recv(mtcp->callbacks.data, packet, sizeof(packet)); if (size < 0) return; @@ -671,9 +708,9 @@ bool microtcp_callbacks_create_for_tap(const char *ip, const char *mac, *callbacks = (microtcp_callbacks_t) { .data = dev, - .free = tuntap_release, - .recv = tuntap_read, - .send = tuntap_write, + .free = (void(*)(void*)) tuntap_release, + .recv = (int(*)(void*, void*, size_t)) tuntap_read, + .send = (int(*)(void*, const void*, size_t)) tuntap_write, }; return true; @@ -758,17 +795,26 @@ push_unlinked_socket_into_free_list(microtcp_t *mtcp, microtcp_socket_t *socket) mtcp->free_socket_list = socket; } +#ifdef MICROTCP_USING_MUX +static void +signal_events_to_muxes_associated_to_socket(microtcp_socket_t *socket, int events); +#endif + static void ready_to_accept(void *data) { -#ifdef MICROTCP_BACKGROUND_THREAD microtcp_socket_t *socket = data; + (void) socket; + +#ifdef MICROTCP_BACKGROUND_THREAD cnd_signal(&socket->something_to_accept); -#else - (void) data; +#endif + +#ifdef MICROTCP_USING_MUX + signal_events_to_muxes_associated_to_socket(socket, MICROTCP_MUX_ACCEPT); #endif } -microtcp_socket_t *microtcp_open(microtcp_t *mtcp, uint16_t port, +microtcp_socket_t *microtcp_open(microtcp_t *mtcp, uint16_t port, microtcp_errcode_t *errcode) { microtcp_errcode_t errcode2 = MICROTCP_ERRCODE_NONE; @@ -795,6 +841,10 @@ microtcp_socket_t *microtcp_open(microtcp_t *mtcp, uint16_t port, socket->type = SOCKET_LISTENER; socket->listener = listener; +#ifdef MICROTCP_USING_MUX + socket->mux_list = NULL; +#endif + #ifdef MICROTCP_BACKGROUND_THREAD if (cnd_init(&socket->something_to_accept) != thrd_success) { errcode2 = MICROTCP_ERRCODE_BADCONDVAR; @@ -819,6 +869,8 @@ void microtcp_close(microtcp_socket_t *socket) microtcp_t *mtcp = socket->mtcp; + #warning "sockets should unregister from all multiplexers" + LOCK_WHEN_THREADED(mtcp); { switch (socket->type) { @@ -843,21 +895,29 @@ void microtcp_close(microtcp_socket_t *socket) static void ready_to_recv(void *data) { -#ifdef MICROTCP_BACKGROUND_THREAD microtcp_socket_t *socket = data; + (void) socket; + +#ifdef MICROTCP_BACKGROUND_THREAD cnd_signal(&socket->something_to_recv); -#else - (void) data; +#endif + +#ifdef MICROTCP_USING_MUX + signal_events_to_muxes_associated_to_socket(socket, MICROTCP_MUX_RECV); #endif } static void ready_to_send(void *data) { -#ifdef MICROTCP_BACKGROUND_THREAD microtcp_socket_t *socket = data; + (void) socket; + +#ifdef MICROTCP_BACKGROUND_THREAD cnd_signal(&socket->something_to_send); -#else - (void) data; +#endif + +#ifdef MICROTCP_USING_MUX + signal_events_to_muxes_associated_to_socket(socket, MICROTCP_MUX_SEND); #endif } @@ -886,14 +946,20 @@ microtcp_socket_t *microtcp_accept(microtcp_socket_t *socket, #ifdef MICROTCP_BACKGROUND_THREAD while (!connection && !no_block) { - if (cnd_wait(&socket->something_to_accept, &mtcp->lock) != thrd_success) - abort(); + if (cnd_wait(&socket->something_to_accept, &mtcp->lock) != thrd_success) { + errcode2 = MICROTCP_ERRCODE_BADCONDVAR; + push_unlinked_socket_into_free_list(mtcp, socket2); + goto unlock_and_exit; + } connection = tcp_listener_accept(socket->listener, socket2, ready_to_recv, ready_to_send); } #else - if (!connection && !no_block) { + if (!connection) { + if (no_block) + errcode2 = MICROTCP_ERRCODE_WOULDBLOCK; + else + errcode2 = MICROTCP_ERRCODE_CANTBLOCK; push_unlinked_socket_into_free_list(mtcp, socket2); - errcode2 = MICROTCP_ERRCODE_CANTBLOCK; goto unlock_and_exit; } #endif @@ -904,6 +970,10 @@ microtcp_socket_t *microtcp_accept(microtcp_socket_t *socket, socket2->type = SOCKET_CONNECTION; socket2->connection = connection; +#ifdef MICROTCP_USING_MUX + socket2->mux_list = NULL; +#endif + #ifdef MICROTCP_BACKGROUND_THREAD if (cnd_init(&socket2->something_to_recv) != thrd_success) { errcode2 = MICROTCP_ERRCODE_BADCONDVAR; @@ -953,13 +1023,18 @@ size_t microtcp_recv(microtcp_socket_t *socket, #ifdef MICROTCP_BACKGROUND_THREAD while (num == 0 && !no_block) { - if (cnd_wait(&socket->something_to_recv, &mtcp->lock) != thrd_success) - abort(); + if (cnd_wait(&socket->something_to_recv, &mtcp->lock) != thrd_success) { + errcode2 = MICROTCP_ERRCODE_BADCONDVAR; + goto unlock_and_exit; + } num = tcp_connection_recv(socket->connection, dst, len); } #else - if (num == 0 && !no_block) { - errcode2 = MICROTCP_ERRCODE_CANTBLOCK; + if (num == 0) { + if (no_block) + errcode2 = MICROTCP_ERRCODE_WOULDBLOCK; + else + errcode2 = MICROTCP_ERRCODE_CANTBLOCK; goto unlock_and_exit; } #endif @@ -994,18 +1069,348 @@ size_t microtcp_send(microtcp_socket_t *socket, #ifdef MICROTCP_BACKGROUND_THREAD while (num == 0 && !no_block) { - if (cnd_wait(&socket->something_to_send, &mtcp->lock) != thrd_success) - abort(); + if (cnd_wait(&socket->something_to_send, &mtcp->lock) != thrd_success) { + errcode2 = MICROTCP_ERRCODE_BADCONDVAR; + goto unlock_and_exit; + } num = tcp_connection_send(socket->connection, src, len); } #else - if (num == 0 && !no_block) - errcode2 = MICROTCP_ERRCODE_CANTBLOCK; + if (num == 0) { + if (no_block) + errcode2 = MICROTCP_ERRCODE_WOULDBLOCK; + else + errcode2 = MICROTCP_ERRCODE_CANTBLOCK; + goto unlock_and_exit; + } #endif } +unlock_and_exit: UNLOCK_WHEN_THREADED(mtcp); if (errcode) *errcode = errcode2; return num; } + +#ifdef MICROTCP_USING_MUX +microtcp_mux_t *microtcp_mux_create(microtcp_t *mtcp) +{ + microtcp_mux_t *mux = malloc(sizeof(microtcp_mux_t)); + if (!mux) + return NULL; + + mux->mtcp = mtcp; + + // Build the free list + static_assert(MICROTCP_MAX_MUX_ENTRIES > 1); + const int max = MICROTCP_MAX_MUX_ENTRIES; + for (int i = 1; i < max-1; i++) { + mux->entries[i].mux = mux; // This will be never changed + mux->entries[i].mux_prev = &mux->entries[i-1].mux_next; + mux->entries[i].mux_next = &mux->entries[i+1]; + } + mux->entries[0].mux = mux; // Never changed + mux->entries[0].mux_prev = &mux->free_list; + mux->entries[0].mux_next = &mux->entries[1]; + mux->entries[max-1].mux = mux; // Never changed + mux->entries[max-1].mux_prev = &mux->entries[max-2].mux_next; + mux->entries[max-1].mux_next = NULL; + + mux->idle_list = NULL; + mux->free_list = mux->entries; + mux->ready_queue_head = NULL; + mux->ready_queue_tail = NULL; + +#ifdef MICROTCP_BACKGROUND_THREAD + if (cnd_init(&mux->queue_not_empty) != thrd_success) { + free(mux); + return NULL; + } +#endif + + return mux; +} + +void microtcp_mux_destroy(microtcp_mux_t *mux) +{ + // Unregister all idle sockets + // Idle entries don't have pending events + // to deliver so by unregistering them the + // entry is unlinked. + while (mux->idle_list) + microtcp_mux_unregister(mux, mux->idle_list->sock, ~0); + + // Consume all previously reported events + // to make sure that when unregistering + // the entries are actually removed + while (microtcp_mux_poll(mux, NULL)); + + // Unreagister all sockets that have events + while (mux->ready_queue_head) { + mux_entry_t *entry = mux->ready_queue_head; + microtcp_mux_unregister(mux, entry->sock, ~0); + + // Since all events were consumed beforehand + // we're sure the entry was removed. + assert(entry != mux->ready_queue_head); + } + +#ifdef MICROTCP_BACKGROUND_THREAD + cnd_destroy(&mux->queue_not_empty); +#endif + + free(mux); +} + +static mux_entry_t* +find_socket_and_mux_entry(microtcp_mux_t *mux, microtcp_socket_t *sock) +{ + mux_entry_t *entry = sock->mux_list; + while (entry) { + if (entry->mux == mux) + break; + entry = entry->sock_next; + } + return entry; +} + +static void +move_mux_entry_to_free_list(mux_entry_t *entry) +{ + microtcp_mux_t *mux = entry->mux; + + // If the entry is in a list, unlink it + if (entry->mux_prev) + *entry->mux_prev = entry->mux_next; + if (entry->sock_prev) + *entry->sock_prev = entry->sock_next; + + // Put the structure into the free list + entry->mux_prev = &mux->free_list; + entry->mux_next = mux->free_list; + if (mux->free_list) + mux->free_list->mux_prev = &entry->mux_next; + mux->free_list = entry; +} + +static void +move_mux_entry_to_idle_list(mux_entry_t *entry) +{ + // To be moved to the idle list the entry + // must be associated to a socket so it + // must be in a socket mux list, therefore + // it must be true that + assert(entry->sock_prev); // not null iff the entry is in a mux list + + // Make sure the entry is unlinked relative + // to the lists in the mux + if (entry->mux_prev) + *entry->mux_prev = entry->mux_next; + + // Now actually insert it into the idle list + microtcp_mux_t *mux = entry->mux; + entry->mux_prev = &mux->idle_list; + entry->mux_next = mux->idle_list; + if (mux->idle_list) + mux->idle_list->mux_prev = &entry->mux_next; + mux->idle_list = entry; +} + +bool microtcp_mux_unregister(microtcp_mux_t *mux, microtcp_socket_t *sock, int events) +{ + LOCK_WHEN_THREADED(mux->mtcp); + + // There's no need to check that mux + // and socket have the same mtcp because + // if it's different it will result that + // the socket isn't registered into the + // mux. + + mux_entry_t *entry = find_socket_and_mux_entry(mux, sock); + if (!entry) { + // This socket wasn't registered into the mux + UNLOCK_WHEN_THREADED(mux->mtcp); + return false; + } + + // Unset the events of interest + entry->events_of_interest &= ~events; + + if (entry->triggered_events) { + // NOTE: Since we modified "events_of_interest" + // but not "triggered_events", any previously + // triggered events that were now unregistered + // will still be delivered to the user. + // + // Though when events are delivered, if all + // events registered were all unregistered, + // the socket is removed from the mux. + } else + // No events were previously reported so we can + // move the entry to the free list. + move_mux_entry_to_free_list(entry); + + UNLOCK_WHEN_THREADED(mux->mtcp); + return true; +} + +bool microtcp_mux_register(microtcp_mux_t *mux, microtcp_socket_t *sock, int events, void *userp) +{ + LOCK_WHEN_THREADED(mux->mtcp); + + if (mux->mtcp != sock->mtcp) { + UNLOCK_WHEN_THREADED(mux->mtcp); + return false; // mux and socket are associated to different microtcp stacks + } + + if (events == 0) { + UNLOCK_WHEN_THREADED(mux->mtcp); + return true; // Nothing to be done + } + + mux_entry_t *entry = find_socket_and_mux_entry(mux, sock); + if (!entry) { + // This is the first time that the socket is registered. + // Create an entry for it + if (mux->free_list == NULL) { + // The entry limit was reached. + // It's impossible to register the socket at this time + UNLOCK_WHEN_THREADED(mux->mtcp); + return false; + } + + // Pop from the free list + entry = mux->free_list; + *entry->mux_prev = entry->mux_next; + + // Push it into the idle list of the mux + entry->mux_prev = &mux->idle_list; + entry->mux_next = mux->idle_list; + if (mux->idle_list) + mux->idle_list->mux_prev = &entry->mux_next; + + // Push it into the socket mux list + entry->sock_prev = &sock->mux_list; + entry->sock_next = sock->mux_list; + if (sock->mux_list) + sock->mux_list->sock_prev = &entry->sock_next; + + // Initialize the entry + entry->sock = sock; + entry->userp = userp; + entry->triggered_events = 0; + entry->events_of_interest = 0; + // entry->mux = mux; This isn't necessary because the mux field + // is initialized once with the mux and never + // changed. + } + + entry->events_of_interest |= events; + + UNLOCK_WHEN_THREADED(mux->mtcp); + return true; +} + +bool microtcp_mux_poll(microtcp_mux_t *mux, microtcp_muxevent_t *ev) +{ + LOCK_WHEN_THREADED(mux->mtcp); + + // Get the tail of the queue (without popping it) + mux_entry_t *entry = mux->ready_queue_tail; + + if (!entry) { + UNLOCK_WHEN_THREADED(mux->mtcp); + return false; // No events occurred + } + + // If this socket was in the ready queue + // it must have triggered events + assert(entry->triggered_events); + + if (ev) { + ev->userp = entry->userp; + ev->events = entry->triggered_events; + ev->socket = entry->sock; + } + + // Unmark events as triggered + entry->triggered_events = 0; + + if (entry->events_of_interest == 0) + // All events were unregistered. + // We can remove the socket from the mux. + move_mux_entry_to_free_list(entry); + else + // The socket wasn't unregistered or + // wasn't unregistered completely so + // we put the entry into the idle list + move_mux_entry_to_idle_list(entry); + + UNLOCK_WHEN_THREADED(mux->mtcp); + return true; +} + +bool microtcp_mux_wait(microtcp_mux_t *mux, microtcp_muxevent_t *ev) +{ +#ifdef MICROTCP_BACKGROUND_THREAD + LOCK_WHEN_THREADED(mux->mtcp); + while (!microtcp_mux_poll(mux, ev)) + if (cnd_wait(&mux->queue_not_empty, &mux->mtcp->lock) != thrd_success) + abort(); + UNLOCK_WHEN_THREADED(mux->mtcp); + return true; +#else + return microtcp_mux_poll(mux, ev); +#endif +} + +static void +signal_events_to_muxes_associated_to_socket(microtcp_socket_t *socket, int events) +{ + // (This function is called by the socket and not the mux) + + assert(events); // If no events need to be signaled then + // this function has no reason to be called. + + mux_entry_t *entry = socket->mux_list; + while (entry) { + + microtcp_mux_t *mux = entry->mux; + + bool first_event_of_socket = (entry->triggered_events == 0); + entry->triggered_events |= events & entry->events_of_interest; + + if (first_event_of_socket) { + + bool queue_was_empty = (mux->ready_queue_head == NULL); + + // No entries were already triggered so + // it's necessary to move the entry from + // the mux's idle list to ready queue + + // Unlink it from the idle queue + *entry->mux_prev = entry->mux_next; + if (entry->mux_next) + entry->mux_next->mux_prev = entry->mux_prev; + + // Add it to the queue + entry->mux_prev = &mux->ready_queue_head; + entry->mux_next = mux->ready_queue_head; + if (mux->ready_queue_head) + mux->ready_queue_head->mux_prev = &entry->mux_next; + else + mux->ready_queue_tail = entry; + mux->ready_queue_head = entry; + +#ifdef MICROTCP_BACKGROUND_THREAD + if (queue_was_empty) + cnd_signal(&mux->queue_not_empty); +#else + (void) queue_was_empty; +#endif + } + entry = entry->sock_next; + } +} +#endif \ No newline at end of file diff --git a/src/tcp.c b/src/tcp.c index be3c45a..d31107a 100644 --- a/src/tcp.c +++ b/src/tcp.c @@ -1,14 +1,17 @@ #include #include #include -#include "endian.h" -#include "tcp.h" + +#ifndef MICROTCP_AMALGAMATION +# include "endian.h" +# include "tcp.h" +#endif #ifdef TCP_DEBUG -#include -#define TCP_DEBUG_LOG(fmt, ...) fprintf(stderr, "TCP :: " fmt "\n", ## __VA_ARGS__) +# include +# define TCP_DEBUG_LOG(fmt, ...) fprintf(stderr, "TCP :: " fmt "\n", ## __VA_ARGS__) #else -#define TCP_DEBUG_LOG(...) +# define TCP_DEBUG_LOG(...) #endif #define SEGMENT_OFFSET(seg) (cpu_is_little_endian() ? (seg)->offset2 : (seg)->offset1) @@ -532,6 +535,8 @@ tcp_listener_create(tcp_state_t *state, uint16_t port, void *callback_data, void tcp_listener_destroy(tcp_listener_t *listener) { + #warning "The previously accepted connections should't be closed with their listener.. I think" + // TODO: Close all connections tcp_state_t *state = listener->state; diff --git a/src/tcp.h b/src/tcp.h index 3907e88..86dd649 100644 --- a/src/tcp.h +++ b/src/tcp.h @@ -1,6 +1,9 @@ #include #include + +#ifndef MICROTCP_AMALGAMATION #include "defs.h" +#endif #define TCP_MAX_LISTENERS 32 #define TCP_MAX_SOCKETS 1024 @@ -43,7 +46,6 @@ struct tcp_listener_t { tcp_connection_t *non_established_list; tcp_connection_t *non_accepted_queue_head; tcp_connection_t *non_accepted_queue_tail; - void (*callback_ready_to_accept)(void*); void *callback_data; }; diff --git a/test/loop.c b/test/loop.c deleted file mode 100644 index 631e494..0000000 --- a/test/loop.c +++ /dev/null @@ -1,58 +0,0 @@ -/* -#include -#include - -int main(void) -{ - microtcp_errcode_t errcode; - - microtcp_t *mtcp = microtcp_create(); - if (mtcp == NULL) - return -1; - - uint16_t port = 80; - microtcp_socket_t *server = microtcp_open(mtcp, port, &errcode); - if (errcode) { - fprintf(stderr, "Error: %s\n", microtcp_strerror(errcode)); - microtcp_destroy(mtcp); - return -1; - } - assert(server); - - fprintf(stderr, "Listening on port %d\n", port); - - while (1) { - fprintf(stderr, "About to accept\n"); - microtcp_socket_t *client = microtcp_accept(server, false, &errcode); - if (errcode && errcode != MICROTCP_ERRCODE_NOTHINGTOACCEPT) { - fprintf(stderr, "Error: %s\n", microtcp_strerror(errcode)); - break; - } - - fprintf(stderr, "Accepted a connection\n"); - - char buffer[1024]; - size_t num = microtcp_recv(client, buffer, sizeof(buffer), &errcode); - if (errcode) { - fprintf(stderr, "Error: %s\n", microtcp_strerror(errcode)); - goto handled; - } - microtcp_send(client, "echo: ", 6, &errcode); - if (errcode) { - fprintf(stderr, "Error: %s\n", microtcp_strerror(errcode)); - goto handled; - } - microtcp_send(client, buffer, num, &errcode); - if (errcode) { - fprintf(stderr, "Error: %s\n", microtcp_strerror(errcode)); - goto handled; - } -handled: - microtcp_close(client); - } - - microtcp_close(server); - microtcp_destroy(mtcp); - return 0; -} -*/ \ No newline at end of file