http-parser.c

Small C library to parse HTTP requests
Log | Files | Refs | README | LICENSE

commit 1b39ef1371720255248db85f0e47ac2415fe069c
parent af8fdd7447710492e741ae2cdede201e69345c4a
Author: finwo <finwo@pm.me>
Date:   Sun, 10 Nov 2019 16:14:51 +0100

Basic testing added

Diffstat:
M.gitignore | 1+
AMakefile | 7+++++++
Asrc/http-parser-statusses.h | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/http-parser.c | 255+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Msrc/http-parser.h | 41++++++++++++++++++++++++++++++++---------
Atest.c | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 409 insertions(+), 98 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -1,2 +1,3 @@ /lib *.o +http-parser-test diff --git a/Makefile b/Makefile @@ -0,0 +1,7 @@ + +.PHONY: test +test: http-parser-test + ./$< + +http-parser-test: test.c src/http-parser.c + $(CC) -o $@ $^ diff --git a/src/http-parser-statusses.h b/src/http-parser-statusses.h @@ -0,0 +1,105 @@ +#ifndef _HTTP_PARSER_H_ +#define _HTTP_PARSER_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +struct { + int status; + char *message; +} http_parser_statusses[] = { + { 100, "Continue" }, + { 101, "Switching Protocols" }, + { 102, "Processing" }, + { 103, "Early Hints" }, + { 200, "OK" }, + { 201, "Created" }, + { 202, "Accepted" }, + { 203, "Non-Authoritive Information" }, + { 204, "No Content" }, + { 205, "Reset Content" }, + { 206, "Partial Content" }, + { 207, "Multi-Status" }, + { 208, "Already Reported" }, + { 218, "This is fine" }, + { 226, "IM Used" }, + { 300, "Multiple Choices" }, + { 301, "Moved Permanently" }, + { 302, "Found" }, + { 303, "See Other" }, + { 304, "Not Modified" }, + { 305, "Use Proxy" }, + { 306, "Switch Proxy" }, + { 307, "Temporary Redirect" }, + { 308, "Permanent Redirect" }, + { 400, "Bad Request" }, + { 401, "Unauthorized" }, + { 402, "Payment Required" }, + { 403, "Forbidden" }, + { 404, "Not Found" }, + { 405, "Method Not Allowed" }, + { 406, "Not Acceptable" }, + { 407, "Proxy Authentication Required" }, + { 408, "Request Timeout" }, + { 409, "Conflict" }, + { 410, "Gone" }, + { 411, "Length Required" }, + { 412, "Precondition Failed" }, + { 413, "Payload Too Large" }, + { 414, "URI Too Long" }, + { 415, "Unsupported Media Type" }, + { 416, "Range Not Satisfiable" }, + { 417, "Expectation Failed" }, + { 418, "I'm a teapot" }, + { 419, "Page Expired" }, + { 420, "Enhance Your Calm" }, + { 421, "Misdirected Request" }, + { 422, "Unprocessable Entity" }, + { 423, "Locked" }, + { 424, "Failed Dependency" }, + { 425, "Too Early" }, + { 426, "Upgrade Required" }, + { 428, "Precondition Required" }, + { 429, "Too Many Requests" }, + { 431, "Request Header Fields Too Large" }, + { 440, "Login Time-out" }, + { 444, "No Response" }, + { 449, "Retry With" }, + { 450, "Blocked by Windows Parental Controls" }, + { 451, "Unavailable For Legal Reasons" }, + { 494, "Request header too large" }, + { 495, "SSL Certificate Error" }, + { 496, "SSL Certificate Required" }, + { 497, "HTTP Request Send to HTTPS Port" }, + { 498, "Invalid Token" }, + { 499, "Token Required" }, + { 500, "Internal Server Error" }, + { 501, "Not Implemented" }, + { 502, "Bad Gateway" }, + { 503, "Service Unavailable" }, + { 504, "Gateway Timeout" }, + { 505, "HTTP Version Not Supported" }, + { 506, "Variant Also Negotiates" }, + { 507, "Insufficient Storage" }, + { 509, "Bandwidth Limit Exceeded" }, + { 510, "Not Extended" }, + { 511, "Network Authentication Required" }, + { 520, "Web Server Returned an Unknown Error" }, + { 521, "Web Server is Down" }, + { 522, "Connection Timed Out" }, + { 523, "Origin is Unreachable" }, + { 524, "A Timeout Occurred" }, + { 525, "SSL Handshake Failed" }, + { 526, "Invalid SSL Certificate" }, + { 527, "Railgun Error" }, + { 530, "Site is frozen" }, + { 598, "Network read timeout error" }, + { 0,0 }, +}; + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // _HTTP_PARSER_H_ diff --git a/src/http-parser.c b/src/http-parser.c @@ -8,11 +8,15 @@ extern "C" { #include <strings.h> #include "http-parser.h" +#include "http-parser-statusses.h" #ifndef NULL #define NULL ((void*)0) #endif +/** + * Frees everything in a header that was malloc'd by http-parser + */ void http_parser_header_free(struct http_parser_header *header) { if (header->next) http_parser_header_free(header->next); if (header->key) free(header->key); @@ -20,8 +24,12 @@ void http_parser_header_free(struct http_parser_header *header) { free(header); } -char *http_parser_header_get(struct http_parser_request *request, char *key) { - struct http_parser_header *header = request->headers; +/** + * Searches for the given key in the list of headers + * Returns the header's value or NULL if not found + */ +char *http_parser_header_get(struct http_parser_message *subject, char *key) { + struct http_parser_header *header = subject->headers; while(header) { if (!strcasecmp(key, header->key)) { return header->value; @@ -31,27 +39,156 @@ char *http_parser_header_get(struct http_parser_request *request, char *key) { return NULL; } -void http_parser_request_free(struct http_parser_request *request) { - if (request->method) free(request->method); - if (request->path) free(request->path); - if (request->version) free(request->version); - if (request->body) free(request->body); - if (request->headers) http_parser_header_free(request->headers); - free(request); +/** + * Write a header into the subject's list of headers + */ +void http_parser_header_set(struct http_parser_message *subject, char *key, char *value) { + struct http_parser_header *header = malloc(sizeof(struct http_parser_header)); + header->key = key; + header->value = value; + header->next = subject->headers; + subject->headers = header; } -struct http_parser_request * http_parser_request_init() { - struct http_parser_request *request = calloc(1, sizeof(struct http_parser_request)); - return request; +/** + * Frees everything in a http_message that was malloc'd by http-parser + */ +void http_parser_message_free(struct http_parser_message *subject) { + if (subject->method) free(subject->method); + if (subject->path) free(subject->path); + if (subject->version) free(subject->version); + if (subject->body) free(subject->body); + if (subject->headers) http_parser_header_free(subject->headers); + free(subject); } -void http_parser_request_data(struct http_parser_request *request, char *data, int size) { +/** + * Frees everything in a http pair that was malloc'd by http-parser + */ +void http_parser_pair_free(struct http_parser_pair *pair) { + if (pair->request) http_parser_message_free(pair->request); + if (pair->response) http_parser_message_free(pair->response); + free(pair); +} + +/** + * Initializes a http_message as request + */ +struct http_parser_message * http_parser_request_init() { + struct http_parser_message *message = calloc(1, sizeof(struct http_parser_message)); + return message; +} + +/** + * Initializes a http_message as reponse + */ +struct http_parser_message * http_parser_response_init() { + struct http_parser_message *message = http_parser_request_init(); + message->status = 200; + return message; +} + +/** + * Initialize a http_pair with userdata + */ +struct http_parser_pair * http_parser_pair_init(void *udata) { + struct http_parser_pair *pair = calloc(1, sizeof(struct http_parser_pair)); + pair->request = http_parser_request_init(); + pair->response = http_parser_response_init(); + pair->udata = udata; + return pair; +} + +/** + * Removes the first string from a http_message's body + */ +static void http_parser_message_remove_body_string(struct http_parser_message *message) { + int length = strlen(message->body); + int size = message->bodysize - 2 - length; + char *buf = calloc(1,size+1); + memcpy(buf, message->body + length + 2, size); + free(message->body); + message->body = buf; + message->bodysize = size; +} + +/** + * Reads a header from a message's body and removes that lines from the body + * + * Caution: does not support multi-line headers yet + */ +static int http_parser_message_read_header(struct http_parser_message *message) { struct http_parser_header *header; + char *index; + + // Require more data if no line break found + index = strstr(message->body, "\r\n"); + if (!index) return 1; + *(index) = '\0'; + + // Detect end of headers + if (!strlen(message->body)) { + http_parser_message_remove_body_string(message); + return 0; + } + + // Prepare new header + header = calloc(1,sizeof(header)); + header->key = calloc(1,strlen(message->body)); + header->value = calloc(1,strlen(message->body)); + + // Detect colon + index = strstr(message->body, ":"); + if (!index) { + printf("NO COLON: %s\n", message->body); + http_parser_header_free(header); + http_parser_message_remove_body_string(message); + return 1; + } + + // Copy key & value + *(index) = '\0'; + strcpy(header->key, message->body); + strcpy(header->value, index + 1); + + // Assign to the header list + header->next = message->headers; + message->headers = header; + + // Remove the header line + // Twice, because we split the string + http_parser_message_remove_body_string(message); + http_parser_message_remove_body_string(message); + return 2; +} + +/** + * Pass data into the pair's request + * + * Triggers onRequest if set + */ +void http_parser_pair_request_data(struct http_parser_pair *pair, char *data, int size) { struct http_parser_event *ev; + http_parser_request_data(pair->request, data, size); + if (pair->request->_state == HTTP_PARSER_STATE_RESPONSE) { + if (pair->onRequest) { + ev = calloc(1,sizeof(struct http_parser_event)); + ev->request = pair->request; + ev->response = pair->response; + ev->udata = pair->udata; + pair->onRequest(ev); + free(ev); + pair->onRequest = NULL; + } + } +} + +/** + * Insert data into a http_message, acting as if it's a request + */ +void http_parser_request_data(struct http_parser_message *request, char *data, int size) { char *index; - int newsize; char *colon; - char *buf; char *aContentLength; int iContentLength; @@ -64,9 +201,8 @@ void http_parser_request_data(struct http_parser_request *request, char *data, i // Make string functions not segfault *(request->body + request->bodysize) = '\0'; - int running = 1; - while(running) { - switch(request->state) { + while(1) { + switch(request->_state) { case HTTP_PARSER_STATE_PANIC: return; case HTTP_PARSER_STATE_METHOD: @@ -81,17 +217,12 @@ void http_parser_request_data(struct http_parser_request *request, char *data, i request->path = calloc(1, 8192); request->version = calloc(1, 4); if (sscanf(request->body, "%6s %8191s HTTP/%3s", request->method, request->path, request->version) != 3) { - request->state = HTTP_PARSER_STATE_PANIC; + request->_state = HTTP_PARSER_STATE_PANIC; return; } - // Remove the method line - newsize = request->bodysize - 2 - (index - request->body); - buf = calloc(1,newsize+1); - memcpy(buf, index + 2, newsize); - free(request->body); - request->body = buf; - request->bodysize = newsize; + // Remove method line + http_parser_message_remove_body_string(request); // Detect query // No need to malloc, already done by sscanf @@ -102,63 +233,17 @@ void http_parser_request_data(struct http_parser_request *request, char *data, i } // Signal we're now reading headers - request->state = HTTP_PARSER_STATE_HEADER; + request->_state = HTTP_PARSER_STATE_HEADER; break; case HTTP_PARSER_STATE_HEADER: - - // Wait for more data if not line break found - index = strstr(request->body, "\r\n"); - if (!index) return; - *(index) = '\0'; - - // Detect end of headers - newsize = strlen(request->body); - if (!newsize) { - - // Remove the blank line - newsize = request->bodysize - 2; - buf = calloc(1,newsize+1); - memcpy(buf, index + 2, newsize); - free(request->body); - request->body = buf; - request->bodysize = newsize; - - // No content-length = respond + if (!http_parser_message_read_header(request)) { if (http_parser_header_get(request, "content-length")) { - request->state = HTTP_PARSER_STATE_BODY; + request->_state = HTTP_PARSER_STATE_BODY; } else { - request->state = HTTP_PARSER_STATE_RESPONSE; + request->_state = HTTP_PARSER_STATE_RESPONSE; } - - break; } - - // Prepare new header - header = calloc(1,sizeof(header)); - header->key = calloc(1,strlen(request->body)); - header->value = calloc(1,strlen(request->body)); - - // Copy key & value - colon = strstr(request->body, ":"); - if (colon) { - *(colon) = '\0'; - strcpy(header->key, request->body); - strcpy(header->value, colon + 1); - } - - // Assign to the header list - header->next = request->headers; - request->headers = header; - - // Remove the header line - newsize = request->bodysize - 2 - (index - request->body); - buf = calloc(1,newsize+1); - memcpy(buf, index + 2, newsize); - free(request->body); - request->body = buf; - request->bodysize = newsize; - break; case HTTP_PARSER_STATE_BODY: @@ -169,30 +254,22 @@ void http_parser_request_data(struct http_parser_request *request, char *data, i // Not enough data = skip if (request->bodysize < iContentLength) { - running = 0; - break; + return; } // Change size to indicated size request->bodysize = iContentLength; - request->state = HTTP_PARSER_STATE_RESPONSE; + request->_state = HTTP_PARSER_STATE_RESPONSE; break; case HTTP_PARSER_STATE_RESPONSE: - - if (request->onRequest) { - ev = calloc(1,sizeof(struct http_parser_event)); - ev->request = request; - request->onRequest(ev); - request->onRequest = NULL; - } - - running = 0; - break; + return; } } } +// TODO: http_parser_response_data + #ifdef __cplusplus } // extern "C" #endif diff --git a/src/http-parser.h b/src/http-parser.h @@ -18,26 +18,49 @@ struct http_parser_header { }; struct http_parser_event { - struct http_parser_request *request; + struct http_parser_message *request; + struct http_parser_message *response; + void *udata; }; -struct http_parser_request { - int bodysize; - int state; +struct http_parser_message { + int status; char *method; char *path; char *query; char *version; struct http_parser_header *headers; char *body; - void (*onRequest)(struct http_parser_event*); + int bodysize; + int _state; +}; + +struct http_parser_pair { + struct http_parser_message *request; + struct http_parser_message *response; void *udata; + void (*onRequest)(struct http_parser_event*); + void (*onResponse)(struct http_parser_event*); }; -char *http_parser_header_get(struct http_parser_request *request, char *key); -void http_parser_request_free(struct http_parser_request *request); -struct http_parser_request * http_parser_request_init(); -void http_parser_request_data(struct http_parser_request *request, char *data, int size); +// Header management +char *http_parser_header_get(struct http_parser_message *subject, char *key); +void http_parser_header_set(struct http_parser_message *subject, char *key, char *value); +char *http_parser_header_del(struct http_parser_message *subject, char *key); + +struct http_parser_pair * http_parser_pair_init(void *udata); +struct http_parser_message * http_parser_request_init(); +struct http_parser_message * http_parser_response_init(); + +void http_parser_request_data(struct http_parser_message *request, char *data, int size); + +void http_parser_response_data(struct http_parser_message *request, char *data, int size); + +void http_parser_pair_request_data(struct http_parser_pair *pair, char *data, int size); +void http_parser_pair_response_data(struct http_parser_pair *pair, char *data, int size); + +void http_parser_pair_free(struct http_parser_pair *pair); +void http_parser_message_free(struct http_parser_message *subject); #ifdef __cplusplus } // extern "C" diff --git a/test.c b/test.c @@ -0,0 +1,98 @@ +#include <stdio.h> +#include <string.h> + +#include "src/http-parser.h" + +#ifndef NULL +#define NULL ((void*)0) +#endif + +#define T_RED "\e[31m" +#define T_LIME "\e[32m" +#define T_MAGENTA "\e[35m" +#define T_NORMAL "\e[00m" +#define T_BOLD "\e[01m" + +#define ASSERT(M,c) (printf(((err|=!(c),(c)) ? (T_LIME " PASS " T_NORMAL " %s\n") : (T_RED " FAIL " T_NORMAL " %s\n")),M)) + + +/* static void onRequest(struct http_parser_event *ev) { */ +/* // The request has been received */ +/* // Answer the request directly or pass it to a route handler of sorts */ + +/* // Fetching the request */ +/* // Has been wrapped in http_parser_event to support more features in the future */ +/* struct http_parser_request *request = ev->request; */ + +/* // Basic http request data */ +/* printf("Method: %s\n", request->method); */ + +/* // Reading headers are case-insensitive due to non-compliant clients/servers */ +/* printf("Host: %s\n", http_parser_header_get(request, "host")); */ + +/* // Once you're done with the request, you'll have to free it */ +/* http_parser_request_free(request); */ +/* } */ + +/* // Initialize a request */ +/* struct http_parser_request *request = http_parser_init(); */ + +/* // Assign userdata into the request */ +/* // Use it to track whatever you need */ +/* request->udata = (void*)...; */ + +/* // Attach a function */ + +// Stored http messages +char *getMessage = + "GET /foobar HTTP/1.1\r\n" + "Host: localhost\r\n" + "\r\n" +; +char *postMessage = + "POST /foobar HTTP/1.1\r\n" + "Host: localhost\r\n" + "Content-Length: 13\r\n" + "\r\n" + "Hello World\r\n" +; +char *responseMessage = + "HTTP/1.0 200 OK\r\n" + "Content-Length: 11\r\n" + "\r\n" + "Hello World\r\n" +; + + + +/* // Passing network data into it */ +/* http_parser_request_data(request, message, strlen(message)); */ + +int main() { + struct http_parser_message *request = http_parser_request_init(); + struct http_parser_message *response = http_parser_response_init(); + int err = 0; + + printf("# Pre-loaded request\n"); + ASSERT("request->method is null", request->method == NULL); + ASSERT("request->body is null", request->body == NULL); + + http_parser_request_data(request, getMessage, strlen(getMessage)); + + printf("# Loaded GET request\n"); + ASSERT("request->version is 1.1", strcmp(request->version, "1.1") == 0); + ASSERT("request->method is GET", strcmp(request->method, "GET") == 0); + ASSERT("request->path is /foobar", strcmp(request->path, "/foobar") == 0); + + http_parser_message_free(request); + request = http_parser_request_init(); + http_parser_request_data(request, postMessage, strlen(postMessage)); + + printf("# Loaded POST request\n"); + ASSERT("request->version is 1.1", strcmp(request->version, "1.1") == 0); + ASSERT("request->method is POST", strcmp(request->method, "POST") == 0); + ASSERT("request->path is /foobar", strcmp(request->path, "/foobar") == 0); + ASSERT("request->body is \"Helo World\\r\\n\"", strcmp(request->body, "Hello World\r\n") == 0); + + return err; +}