diff --git a/Makefile b/Makefile index 1e4a1a8..5fb01e4 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ SUFFIX_DEBUG = _debug # ------------------------------------------------------ # -TEST_CFILES = tinyhttp.c tests/picohttpparser.c tests/test.c tests/test_reuse.c tests/test_chunking.c +TEST_CFILES = tinyhttp.c tests/picohttpparser.c tests/test.c tests/test_reuse.c tests/test_chunking.c tests/test_parse_request.c TEST_HFILES = tinyhttp.h tests/picohttpparser.h TEST_FLAGS = -Wall -Wextra diff --git a/docs.md b/docs.md index 3e7cfef..23aeaf2 100644 --- a/docs.md +++ b/docs.md @@ -4,9 +4,9 @@ TinyHTTP is a C library for implementing web servers. It offers two interfaces: 1. stream interface 1. server interface -The stream interface is a lower level interface to TinyHTTP's HTTP state machine. It's completely stand-alone and performs no internal I/O, making it ideal for embedding in applications with custom constraints. +The stream interface is a lower level interface to TinyHTTP's HTTP state machine. It's completely stand-alone and performs no internal I/O, making it ideal for embedding in applications with custom constraints. The only dependency are freestanding libc headers. -The server interface is based on the stream interface and adds to it a platform-dependant I/O system. It uses the most performant I/O model the platform it's compiled for, but is single-threaded. The design goal of the server interface is ease of use and reasonable performance. +The server interface is based on the stream interface and adds to it a platform-dependant I/O system. It uses the most performant I/O model available on the platform it's compiled for, but is single-threaded. The design goal of the server interface is ease of use and reasonable performance. It does also depend on libc. ## Table of Contents diff --git a/examples/social_network.c b/examples/social_network.c new file mode 100644 index 0000000..83ee24e --- /dev/null +++ b/examples/social_network.c @@ -0,0 +1,271 @@ +#include +#include +#include "../tinyhttp.h" + +///////////////////////////////////////////////////////////////// +// CONFIGURATION +///////////////////////////////////////////////////////////////// + +// Network stuff +#define ADDR "127.0.0.1" +#define PORT 8080 +#define REUSE 1 +#define BACKLOG 32 + +// Social stuff +#define MAX_USERS 32 +#define MAX_POSTS 32 +#define MAX_USERNAME 128 +#define MAX_PASSWORD 256 +#define MAX_BIO 512 +#define MAX_CONTENT 512 + +///////////////////////////////////////////////////////////////// +// UTILITIES +///////////////////////////////////////////////////////////////// + +typedef struct { + char *ptr; + ptrdiff_t len; +} string; + +#define S(X) ((string) {(X), sizeof(X)-1}) + +static string trim(string s) +{ + int i = 0; + while (i < s.len && (s.ptr[i] == ' ' || s.ptr[i] == '\t')) + i++; + + if (i == s.len) { + s.ptr = NULL; + s.len = 0; + } else { + s.ptr += i; + s.len -= i; + while (s.ptr[s.len-1] == ' ' || s.ptr[s.len-1] == '\n') + s.len--; + } + + return s; +} + +static int copy(string src, string *dst, char *mem, int cap) +{ + if (src.len > cap) + return -1; + memcpy(mem, src.ptr, src.len); + dst->ptr = mem; + dst->len = src.len; + return 0; +} + +///////////////////////////////////////////////////////////////// +// BUSINESS LOGIC +///////////////////////////////////////////////////////////////// + +typedef struct { + string name; + string pass; + string bio; + char name_mem[MAX_USERNAME]; + char pass_mem[MAX_PASSWORD]; + char bio_mem[MAX_BIO]; +} User; + +typedef struct { + int id; + string author; + string content; + char author_mem[MAX_USERNAME]; + char content_mem[MAX_CONTENT]; +} Post; + +static User users[MAX_USERS]; +static int user_count = 0; + +static Post posts[MAX_POSTS]; +static int posts_head = 0; +static int posts_count = 0; + +static void init_users_and_posts(void) +{ + // TODO +} + +static int find_user(string name) +{ + for (int i = 0; i < user_count; i++) + if (streq(name, users[i].name)) + return i; + return -1; +} + +static int create_user(string name, string pass) +{ + name = trim(name); + pass = trim(pass); + + if (name.len == 0 || pass.len == 0) + return -1; + + if (find_user(name) != -1) + return -1; + + if (user_count == MAX_USERS) + return -1; + + User *user = &users[user_count]; + + if (copy(name, &user->name, user->name_mem, sizeof(user->name_mem)) < 0 && + copy(pass, &user->pass, user->pass_mem, sizeof(user->pass_mem)) < 0) + return -1; + + user_count++; + return 0; +} + +static int delete_user(string name) +{ + int i = find_user(name); + if (i == -1) + return -1; + + // TODO +} + +static int create_post(string author, string content) +{ + // TODO +} + +static int delete_post(int id) +{ + // TODO +} + +static int modify_post(int id, string new_content) +{ + // TODO +} + +///////////////////////////////////////////////////////////////// +// ENDPOINTS +///////////////////////////////////////////////////////////////// + +#define MAX_SESSIONS 32 + +typedef struct { + int id; + string name; + char name_mem[MAX_USERNAME]; +} Session; + +static Session sessions[MAX_SESSIONS]; +static int session_count = 0; + +static void init_sessions(void) +{ + for (int i = 0; i < MAX_SESSIONS; i++) + sessions[i].id = -1; +} + +static int find_session(int id) +{ + for (int i = 0; i < MAX_SESSIONS; i++) + if (sessions[i].id == id) + return i; + return -1; +} + +static int create_session(string name) +{ + // TODO +} + +static int delete_session(string name) +{ + // TODO +} + +// TODO + +///////////////////////////////////////////////////////////////// +// ENTRY POINT +///////////////////////////////////////////////////////////////// + +static sig_atomic_t should_exit = 0; + +static void +signal_handler(int sig) +{ + if (sig == SIGINT) + should_exit = 1; +} + +int main(void) +{ + signal(SIGINT, signal_handler); + + init_users_and_posts(); + init_sessions(); + + TinyHTTPServerConfig config = { + .reuse = REUSE, + .plain_addr = ADDR, + .plain_port = PORT, + .plain_backlog = BACKLOG, + }; + + TinyHTTPServer *server = tinyhttp_server_init(config); + if (server == NULL) + return -1; + + while (!should_exit) { + + int ret; + TinyHTTPRequest *req; + TinyHTTPResponse res; + + ret = tinyhttp_server_wait(server, &req, &res, 1000); + if (ret < 0) return -1; // Error + if (ret > 0) continue; // Timeout + + char path_mem[1<<10]; + TinyHTTPString path; + if (tinyhttp_normalizepath(req->path, &path, path_mem, sizeof(path_mem)) < 0) { + tinyhttp_response_status(res, 200); + tinyhttp_response_send(res); + continue; + } + + TinyHTTPString sessid = tinyhttp_getcookie(req, TINYHTTP_STRING("sessid")); + + if (tinyhttp_streq(path, TINYHTTP_STRING("/users"))) { + tinyhttp_response_status(res, 200); + tinyhttp_response_body(res, "Users", -1); + tinyhttp_response_send(res); + continue; + } + + if (tinyhttp_streq(path, TINYHTTP_STRING("/posts"))) { + tinyhttp_response_status(res, 200); + tinyhttp_response_body(res, "Posts", -1); + tinyhttp_response_send(res); + continue; + } + + tinyhttp_response_status(res, 404); + tinyhttp_response_body(res, "Not FoundNothing here!", -1); + tinyhttp_response_send(res); + } + + tinyhttp_server_free(server); + return 0; +} diff --git a/misc/statelist.py b/misc/statelist.py new file mode 100644 index 0000000..e21e6c4 --- /dev/null +++ b/misc/statelist.py @@ -0,0 +1,73 @@ +table = [ + (1<<6, "DIED"), + (1<<5, "RECV"), + (1<<4, "SEND"), + (1<<3, "RECV_STARTED"), + (1<<2, "SEND_STARTED"), + (1<<1, "READY"), + (1<<0, "CLOSE"), +] + +outstates = [ + "STATUS", + "HEADER", + "BODY", +] + +i = 0 +k = 0 +while i < 2**len(table): + + tags = [] + for entry in table: + if i & entry[0]: + tags.append(entry[1]) + + statestr = "|".join(tags) + + if ("DIED" in tags) and ("READY" in tags): + i += 1 + continue + + if ("READY" not in tags) and ("DIED" not in tags) and ("SEND" not in tags) and ("RECV" not in tags): + i += 1 + continue + + if ("READY" in tags) and ("CLOSE" in tags): + i += 1 + continue + + if ("CLOSE" in tags) and ("SEND" not in tags): + i += 1 + continue + + if (("CLOSE" in tags) or ("READY" in tags)) and (("RECV" in tags)): + i += 1 + continue + + if ("DIED" in tags) and (("RECV" in tags) or ("SEND" in tags)): + i += 1 + continue + + if ("SEND_STARTED" in tags) and ("SEND" not in tags) and ("DIED" not in tags): + i += 1 + continue + + if ("RECV_STARTED" in tags) and ("RECV" not in tags) and ("DIED" not in tags): + i += 1 + continue + + if "READY" in tags: + for outstate in outstates: + print(k, statestr + "|" + outstate) + k += 1 + else: + print(k, statestr) + k += 1 + + i += 1 + +""" +Constraints: + - STATUS/HEADER/BODY only when READY +""" \ No newline at end of file diff --git a/tests/test.c b/tests/test.c index 4e97bb0..14a3d96 100644 --- a/tests/test.c +++ b/tests/test.c @@ -144,6 +144,7 @@ int main(void) test_basic_exchange(); test_reuse(); test_chunking(); + test_parse_request(); printf("OK\n"); return 0; } @@ -227,6 +228,41 @@ stream_into_buffer(TinyHTTPStream *stream, char *dst, int cap) return copied; } +int match_request(TinyHTTPRequest *r1, TinyHTTPRequest *r2) +{ + if (r1->method != r2->method) + return 0; + + if (r1->minor != r2->minor) + return 0; + + if (!tinyhttp_streq(r1->path, r2->path)) + return 0; + + if (r1->num_headers != r2->num_headers) + return 0; + + for (int i = 0; i < r2->num_headers; i++) { + if (!tinyhttp_streq(r1->headers[i].name, r2->headers[i].name)) + return 0; + if (!tinyhttp_streq(r1->headers[i].value, r2->headers[i].value)) + return 0; + } + + if (r1->body_len != r2->body_len) + return 0; + + if (r2->body_len == 0) { + if (r1->body != NULL) + return 0; + } else { + if (memcmp(r1->body, r2->body, r2->body_len)) + return 0; + } + + return 1; +} + static void expect_request(TinyHTTPStream *stream, TinyHTTPRequest expreq) { @@ -235,26 +271,10 @@ expect_request(TinyHTTPStream *stream, TinyHTTPRequest expreq) TinyHTTPRequest *req = tinyhttp_stream_request(stream); TEST(req); - - TEST(req->method == expreq.method); - TEST(req->minor == expreq.minor); - TEST(tinyhttp_streq(req->path, expreq.path)); - TEST(req->num_headers == expreq.num_headers); - for (int i = 0; i < expreq.num_headers; i++) { - TEST(tinyhttp_streq(req->headers[i].name, expreq.headers[i].name)); - TEST(tinyhttp_streq(req->headers[i].value, expreq.headers[i].value)); - } - - TEST(req->body_len == expreq.body_len); - if (expreq.body_len == 0) { - TEST(req->body == NULL); - } else { - TEST(!memcmp(req->body, expreq.body, expreq.body_len)); - } + TEST(match_request(req, &expreq)); } -static int -parse_request(TinyHTTPString txt, TinyHTTPRequest *req, char *buf, int max) +int parse_request(TinyHTTPString txt, TinyHTTPRequest *req, char *buf, int max) { const char *method; size_t method_len; diff --git a/tests/test.h b/tests/test.h index 0a9c787..49b36d4 100644 --- a/tests/test.h +++ b/tests/test.h @@ -29,6 +29,10 @@ void send_request(TinyHTTPStream *stream, const char *str); // "res". void recv_response(TinyHTTPStream *stream, Response *res, char *dst, int cap); +int parse_request(TinyHTTPString txt, TinyHTTPRequest *req, char *buf, int max); + +int match_request(TinyHTTPRequest *r1, TinyHTTPRequest *r2); + int header_exists(Response *res, TinyHTTPString name); int header_exists_with_value(Response *res, TinyHTTPString name, TinyHTTPString value); @@ -39,4 +43,5 @@ int header_exists_with_value(Response *res, TinyHTTPString name, TinyHTTPString #define TEST_END void test_reuse(void); -void test_chunking(void); \ No newline at end of file +void test_chunking(void); +void test_parse_request(void); \ No newline at end of file diff --git a/tests/test_parse_request.c b/tests/test_parse_request.c new file mode 100644 index 0000000..8032578 --- /dev/null +++ b/tests/test_parse_request.c @@ -0,0 +1,40 @@ +#include +#include "test.h" + +static void test_helper(char *src, int len, int expret, TinyHTTPRequest *expreq) +{ + if (len < 0) len = strlen(src); + TinyHTTPRequest req; + int ret = tinyhttp_parserequest(src, len, -1ULL, &req); + TEST(ret == expret); + if (expret > 0) { + TEST(match_request(&req, expreq)); + } +} + +void test_parse_request(void) +{ + { + char src[] = + "GET / HTTP/1.1\r\n" + "Host: 127.0.0.1:8080\r\n" + "Connection: Keep-Alive\r\n" + "\r\n"; + TinyHTTPRequest req; + req.method = TINYHTTP_METHOD_GET; + req.minor = 1; + req.path = TINYHTTP_STRING("/"); + req.num_headers = 2; + req.headers[0].name = TINYHTTP_STRING("Host"); + req.headers[0].value = TINYHTTP_STRING("127.0.0.1:8080"); + req.headers[1].name = TINYHTTP_STRING("Connection"); + req.headers[1].value = TINYHTTP_STRING("Keep-Alive"); + req.body = NULL; + req.body_len = 0; + + for (int i = 0; i < strlen(src)-1; i++) + test_helper(src, i, 0, &req); + + test_helper(src, strlen(src), strlen(src), &req); + } +} diff --git a/tinyhttp.c b/tinyhttp.c index 698b858..380af8b 100644 --- a/tinyhttp.c +++ b/tinyhttp.c @@ -471,8 +471,7 @@ parse_content_length(const char *src, ptrdiff_t len, unsigned long long *out) return 0; } -static int -parse_request(char *src, ptrdiff_t len, +int tinyhttp_parserequest(char *src, ptrdiff_t len, unsigned long long body_limit, TinyHTTPRequest *req) { ptrdiff_t ret = parse_request_head(src, len, req); @@ -1174,7 +1173,7 @@ char *tinyhttp_stream_recv_buf(TinyHTTPStream *stream, ptrdiff_t *cap) // Parse again. We assume everything will go // well as it did the first time. - parse_request(src, len, stream->bodylimit, &stream->req); + tinyhttp_parserequest(src, len, stream->bodylimit, &stream->req); } // Forward the write region from the input buffer @@ -1217,7 +1216,7 @@ process_next_request(TinyHTTPStream *stream) ptrdiff_t len; char *src = byte_queue_read_buf(&stream->in, &len); // TODO: What if this returns NULL? - int ret = parse_request(src, len, stream->bodylimit, &stream->req); + int ret = tinyhttp_parserequest(src, len, stream->bodylimit, &stream->req); // Request is incomplete if (ret == 0) { diff --git a/tinyhttp.h b/tinyhttp.h index 8529510..23434a9 100644 --- a/tinyhttp.h +++ b/tinyhttp.h @@ -190,6 +190,9 @@ void tinyhttp_printstate_(int state, const char *file, const char *line); int tinyhttp_findheader(TinyHTTPRequest *req, TinyHTTPString name); +int tinyhttp_parserequest(char *src, ptrdiff_t len, + unsigned long long body_limit, TinyHTTPRequest *req); + // Initializes an HTTP stream // // TODO: Comment on memfunc