From 1014d2526cff28003d5955becc3c6ea3d8729ff8 Mon Sep 17 00:00:00 2001 From: Francesco Cozzuto Date: Mon, 25 Mar 2024 21:05:36 +0100 Subject: [PATCH] first commit --- makefile | 26 ++ misc/draw_normal_distrib.py | 24 ++ misc/nginx.conf | 11 + src/buffer.hpp | 286 +++++++++++++++ src/main.cpp | 25 ++ src/netutils.hpp | 40 ++ src/parse.cpp | 713 ++++++++++++++++++++++++++++++++++++ src/parse.hpp | 89 +++++ src/pool.hpp | 206 +++++++++++ src/print.cpp | 39 ++ src/print.hpp | 74 ++++ src/queue.hpp | 82 +++++ src/server.hpp | 704 +++++++++++++++++++++++++++++++++++ src/slice.hpp | 54 +++ src/socket.cpp | 19 + src/socket.hpp | 401 ++++++++++++++++++++ test/fuzz_parse_ipv4.cpp | 33 ++ test/fuzz_parse_ipv6.cpp | 34 ++ test/test_parse_ipv4.cpp | 16 + test/test_queue.cpp | 109 ++++++ test/test_utils.cpp | 10 + test/test_utils.hpp | 3 + 22 files changed, 2998 insertions(+) create mode 100644 makefile create mode 100644 misc/draw_normal_distrib.py create mode 100644 misc/nginx.conf create mode 100644 src/buffer.hpp create mode 100644 src/main.cpp create mode 100644 src/netutils.hpp create mode 100644 src/parse.cpp create mode 100644 src/parse.hpp create mode 100644 src/pool.hpp create mode 100644 src/print.cpp create mode 100644 src/print.hpp create mode 100644 src/queue.hpp create mode 100644 src/server.hpp create mode 100644 src/slice.hpp create mode 100644 src/socket.cpp create mode 100644 src/socket.hpp create mode 100644 test/fuzz_parse_ipv4.cpp create mode 100644 test/fuzz_parse_ipv6.cpp create mode 100644 test/test_parse_ipv4.cpp create mode 100644 test/test_queue.cpp create mode 100644 test/test_utils.cpp create mode 100644 test/test_utils.hpp diff --git a/makefile b/makefile new file mode 100644 index 0000000..a2b14ae --- /dev/null +++ b/makefile @@ -0,0 +1,26 @@ +ifeq ($(OS),Windows_NT) + # Windows + EXT = .exe + LFLAGS = -lws2_32 +else + # Linux + EXT = + LFLAGS = +endif + +all: http$(EXT) test_queue$(EXT) test_parse_ipv4$(EXT) # fuzz_parse_ipv4$(EXT) fuzz_parse_ipv6$(EXT) + +http$(EXT): src/main.cpp src/parse.cpp src/socket.cpp + g++ $^ -o $@ -Wall -Wextra -ggdb $(LFLAGS) + +test_queue$(EXT): + g++ test/test_queue.cpp test/test_utils.cpp -o $@ -Wall -Wextra -ggdb + +test_parse_ipv4$(EXT): + g++ test/test_parse_ipv4.cpp test/test_utils.cpp src/parse.cpp -o $@ -Wall -Wextra -ggdb + +fuzz_parse_ipv4$(EXT): + clang++ test/fuzz_parse_ipv4.cpp -o $@ -fsanitize=fuzzer + +fuzz_parse_ipv6$(EXT): + clang++ test/fuzz_parse_ipv6.cpp -o $@ -fsanitize=fuzzer diff --git a/misc/draw_normal_distrib.py b/misc/draw_normal_distrib.py new file mode 100644 index 0000000..f3b6975 --- /dev/null +++ b/misc/draw_normal_distrib.py @@ -0,0 +1,24 @@ +import matplotlib.pyplot as plt +import numpy as np +import scipy.stats as stats +import math + +http_mu = 4.2 +http_std = 28.99 + +nginx_mu = 160.88 +nginx_std = 234.39 + + +def draw_normal(mu, std): + + mu = 0 + sigma = std + x = np.linspace(mu - 3*sigma, mu + 3*sigma, 100) + plt.plot(x, stats.norm.pdf(x, mu, sigma)) + +draw_normal(http_mu, http_std) +draw_normal(nginx_mu, nginx_std) + +plt.savefig('foo.png') +#plt.show() diff --git a/misc/nginx.conf b/misc/nginx.conf new file mode 100644 index 0000000..f887917 --- /dev/null +++ b/misc/nginx.conf @@ -0,0 +1,11 @@ +worker_processes 1; + +http { + server { + listen 8000; + location / { + add_header Content-Type text/plain; + return 200 'Hello, World!'; + } + } +} \ No newline at end of file diff --git a/src/buffer.hpp b/src/buffer.hpp new file mode 100644 index 0000000..be8f7b6 --- /dev/null +++ b/src/buffer.hpp @@ -0,0 +1,286 @@ + +#include +#include +#include +#include +#include +#include "slice.hpp" +#include "socket.hpp" + +#define MAX_VALUE(X) std::numeric_limits::max() + +struct Buffer { + + char *data; // Buffer content (or NULL when size=0) + int size; // Number of bytes stored in the buffer + int used; // Allocated bytes + bool fail; // True if at least one read or write operation failed at one point. No read or write operations can be performed after this is set. + + int crlfcrlf; // Cache for the result of seek(\r\n\r\n) + + bool ensure_unused_space(int min) + { + assert(!fail); + + if (used + min > size) { + // Allocation of a new buffer is necessary + int new_size = size > 0 ? 2 * size : 256; + char *new_data = new (std::nothrow) char[new_size]; + if (new_data == nullptr) { + fail = true; + return false; + } + memcpy(new_data, data, used); + delete[] data; + data = new_data; + size = new_size; + } + + return true; + } + +public: + Buffer() + { + data = nullptr; + size = 0; + used = 0; + fail = false; + crlfcrlf = -1; + } + + Buffer(Buffer&) = delete; + Buffer& operator=(Buffer& other) = delete; + + Buffer(Buffer&& other) + { + data = other.data; + size = other.size; + used = other.used; + fail = other.fail; + other.data = nullptr; + other.size = 0; + other.used = 0; + other.fail = false; + } + + Buffer& operator=(Buffer&& other) + { + if (this != &other) { + delete[] data; + data = other.data; + size = other.size; + used = other.used; + fail = other.fail; + other.data = nullptr; + other.size = 0; + other.used = 0; + other.fail = false; + } + return *this; + } + + ~Buffer() + { + delete[] data; + } + + int length() const + { + return used; + } + + bool failed() const + { + return fail; + } + + void overwrite(int off, const char *src, int len) + { + if (fail) return; + + if (off < 0 || off + len > used) { + // Slice (off, len) isn't fully contained by + // the buffer. + fail = true; + return; + } + + memmove(data + off, src, len); + } + + void write(const char *src, int len=-1) + { + // Only perform the write if no allocations failed previously + if (fail) return; + + if (len < 0) len = strlen(src); + + // Check for overflows + if (used > MAX_VALUE(used) - len) { + fail = true; + return; + } + + if (!ensure_unused_space(len)) + return; + + memcpy(data + used, src, len); + used += len; + } + + int read(char *dst, int max) + { + if (fail) return 0; + + crlfcrlf = -1; // Invalidate cache + + int copy = std::min(used, max); + memcpy(dst, data, copy); + memmove(data, data + copy, used - copy); + used -= copy; + return copy; + } + + // Moves byte from the socket to the buffer and + // returns true iff the peer closed the connection + bool write(Socket& sock) + { + if (fail) return false; + + bool closed = false; + while (1) { + + // Make sure the buffer has at least a certain amount of free memory + // to avoid small copies + constexpr int min_read = 256; + if (!ensure_unused_space(min_read)) + break; + int res = sock.read(data + used, size - used); + if (res == Socket::WOULD_BLOCK) + break; + if (res == Socket::OTHER_ERROR) { + fail = true; + return false; + } + if (res == 0) { + closed = true; + break; + } + + assert(res > 0); + + // Check for overflow + if (used > MAX_VALUE(used) - res) { + fail = true; + return false; + } + + used += res; + } + + return closed; + } + + // Moves byte from the buffer to the socket + int read(Socket& sock) + { + if (fail) return 0; + + int copied = 0; + while (copied < used) { + + // Make sure the buffer has at least a certain amount + // of free memory to avoid small copies + constexpr int min_read = 256; + if (!ensure_unused_space(min_read)) + break; + + int res = sock.write(data + copied, used - copied); + if (res == Socket::WOULD_BLOCK) + break; + if (res == Socket::OTHER_ERROR || res == 0) { + fail = true; + return 0; + } + + assert(res > 0); + copied += res; + } + + crlfcrlf = -1; // Invalidate cache + + memmove(data + copied, data, used - copied); + used -= copied; + return copied; + } + + // Find the index of the first occurrence of "needle" + // in the buffer's contents. Return -1 if it wasn't + // found. + int seek(const char *needle) + { + int len = strlen(needle); + + bool is_crlfcrlf = false; + if (len == 4 && !strcmp(needle, "\r\n\r\n")) { + if (crlfcrlf > -1) return crlfcrlf; + is_crlfcrlf = true; + } + + // If the token is contained by the buffer, its index + // must be lower than: + int lim = used - len + 1; + + // If the needle is larger than the buffer, then the + // limit is negative + int i = 0; + + while (i < lim && memcmp(data+i, needle, len)) + i++; + + if (i > lim) + return -1; + else { + if (is_crlfcrlf) crlfcrlf = i; + return i; + } + } + + // Removed "num" bytes from the head of the buffer + void consume(int num) + { + assert(num <= used); + memmove(data, data + num, used - num); + used -= num; + + crlfcrlf = -1; // Invalidate cache + } + + bool contains(const char *needle) + { + return seek(needle) >= 0; + } + + // Make a slice of the buffer's contents up to + // a given token. If include_token is true, then + // the slice will include the token at the end. + Slice slice_until(const char *token, bool include_token=false) + { + int end = seek(token); + if (end < 0) + return Slice("", 0); + else { + if (include_token) end += strlen(token); + return Slice(data, end); + } + } + + Slice slice(int off, int end) + { + if (end < off || off < 0 || end >= used) + return Slice("", 0); + + return Slice(data + off, end - off); + } +}; diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..c854e53 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,25 @@ +#include "server.hpp" + +int main() +{ + SocketSubsystem ss; + + Server<16384> server; + + int port = 8080; + if (!server.listen(port)) { + std::clog << "Couldn't start tcp server\n"; + return -1; + } + + for (;;) { + Request req; + server.wait(req); + server.status(200); + server.header("Content-Type", "text/plain"); + server.write("Hello, world!"); + server.send(); + } + + return 0; +} diff --git a/src/netutils.hpp b/src/netutils.hpp new file mode 100644 index 0000000..c7bad72 --- /dev/null +++ b/src/netutils.hpp @@ -0,0 +1,40 @@ +#include + +struct IPv4 { + + uint32_t data; + + IPv4(uint32_t d=0) + { + data = d; + } + + IPv4(const char *str) + { + if (!parse(str)) + data = 0; + } + + bool parse(const char *str, int len=-1); +}; + +struct IPv6 { + + uint16_t data[8]; + + IPv6() + { + for (int i = 0; i < 8; i++) + data[i] = 0; + } + + IPv6(const char *str) + { + if (!parse(str)) { + for (int i = 0; i < 8; i++) + data[i] = 0; + } + } + + bool parse(const char *str, int len=-1); +}; diff --git a/src/parse.cpp b/src/parse.cpp new file mode 100644 index 0000000..f0bef8f --- /dev/null +++ b/src/parse.cpp @@ -0,0 +1,713 @@ +#include +#include +#include +#include "parse.hpp" + +#define MAX_VALUE(X) std::numeric_limits::max() + +struct Scanner { + + const char *str; + intptr_t len; + intptr_t off; + + Scanner(const char *s, int l) + { + str = s; + len = l; + off = 0; + } + + bool end() const + { + return off == len; + } + + char curr() const + { + assert(off < len); + return str[off]; + } + + void back() + { + assert(off > 0); + off--; + } + + char peek() + { + assert(off+1 < len); + return str[off+1]; + } + + void consume() + { + assert(off < len); + off++; + } + + bool consume(char c) + { + if (end() || curr() != c) + return false; + consume(); + return true; + } + + bool consume(const char *s) + { + intptr_t l = strlen(s); + if (off + l > len) + return false; + if (strncmp(str + off, s, l)) + return false; + off += l; + return true; + } + + typedef bool (*CharTestFunc)(char c); + + /* + * Consumes a substring that starts with a character + * c such that testfn_head(c) is true and following + * chars are such that testfn_body(c) is true. + * + * Returns true iff at least a character was consumed. + */ + bool consume(CharTestFunc testfn_head, CharTestFunc testfn_body) + { + if (end() || !testfn_head(curr())) + return false; + + do + consume(); + while (!end() && testfn_body(curr())); + return true; + } + + /* + * It's like the previous function but uses the same + * function for head of the substring and body. + */ + bool consume(CharTestFunc testfn) + { + return consume(testfn, testfn); + } +}; + +bool is_upper_alpha(char c) +{ + return c >= 'A' && c <= 'Z'; +} + +bool is_alpha(char c) +{ + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); +} + +bool is_digit(char c) +{ + return (c >= '0' && c <= '9'); +} + +/* + * Parse the scheme token of the following URL in "src" if present + */ +void parse_scheme(Scanner &src, Slice &dst) +{ + intptr_t start = src.off; + + auto schema_testfn_head = [](auto c) { return is_upper_alpha(c); }; + auto schema_testfn_body = [](auto c) { return is_upper_alpha(c) || is_digit(c) || c == '+' || c == '-' || c == '.'; }; + + if (!src.consume(schema_testfn_head, schema_testfn_body)) + dst.whipe(); + else { + // May have a schema if ':' follows + if (!src.consume(':')) { + // Not a schema. Empty schema substring and + // restore the scanner cursor to the start + dst.whipe(); + src.off = start; + } else { + dst.str = src.str + src.off; + dst.off = start; + dst.len = src.off - start; + } + } +} + +/* + * From RFC 3986, Appendix A: + * + * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + * / "*" / "+" / "," / ";" / "=" + */ +bool is_subdelim(char c) +{ + return c == '!' || c == '$' || c == '&' || c == '\'' || c == '(' + || c == ')' || c == '*' || c == '+' || c == ',' || c == ';' + || c == '='; +} + +/* + * From RFC 3986, Section 2.3: + * Characters that are allowed in a URI but do not have a reserved + * purpose are called unreserved. These include uppercase and lowercase + * letters, decimal digits, hyphen, period, underscore, and tilde. + * + * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + */ +bool is_unreserved(char c) +{ + return is_alpha(c) || is_digit(c) || c == '-' || c == '.' || c == '_' || c == '~'; +} + +/* + * From RFC 3986, Appendix A: + * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + */ +bool is_pchar(char c) +{ + return is_unreserved(c) || is_subdelim(c) || c == ':' || c == '@'; +} + +void parse_user_info(Scanner &src, Slice &dst) +{ + intptr_t start = src.off; + if (src.consume([](char c) { return is_unreserved(c) || is_subdelim(c) || c == ':'; })) { + dst.str = src.str; + dst.off = start; + dst.len = src.len - start; + if (!src.consume("@")) { + // The scanned string wasn't an userinfo string + // after all. Clear the substring and put the + // scanner's cursor back to the start. + dst.whipe(); + src.off = start; + } + } else + dst.whipe(); +} + +bool is_hex(char c) +{ + return (c >= '0' && c <= '9') + || (c >= 'a' && c <= 'f') + || (c >= 'A' && c <= 'F'); +} + +bool parse_u16_base16(Scanner &src, uint16_t &dst) +{ + if (src.end() || !is_hex(src.curr())) + return false; // Missing hex number + + dst = 0; + do { + + // Hex digit to int + int d = src.curr() - '0'; + assert(d >= 0 && d < 16); + + // Only consume hex digits up to 2^16-1 + if (dst > (UINT16_MAX - d) / 16) { + src.back(); + break; + } + dst = dst * 16 + d; + + src.consume(); + + } while (!src.end() && is_hex(src.curr())); + return true; +} + +bool parse_ipv6(Scanner &src, IPv6 &dst) +{ + /* + * An IPv6 address is a sequence of 8 byte pairs expressed in + * hex and separated by ':' tokens. + * + * Each hex number represents a number from 0 to 2^16-1 which + * means it has a maximum of 4 hex digits. + * + * At any point between two hex numbers the "::" token may be + * used in place of ":". If that's the case the IPv6 will have + * less than 8 groups. The remaining hex numbers are assumed to + * be 0 and inserted where "::" is. It's also possible to have + * "::" at the beginning or end of the address. + */ + + // Current number of numbers appended to + // the IPv6 data. By the end this must be 8. + int count = 0; + + while (count < 8 && !src.consume("::")) { + + // If this isn't the first number, consume the preceding ':'. + if (count > 0 && !src.consume(":")) + return false; + + if (!parse_u16_base16(src, dst.data[count])) + return false; + count++; + } + + if (count < 8) { + // The "::" was used. + + // Parse the remaining ones in a buffer, then + // calculate the implicit zero numbers necessary + // to get to 8. Then, append the zeros and tail + // to the final array. + uint16_t tail[8]; + int tail_count = 0; + + while (count + tail_count < 7) { + + // If this isn't the first number, consume the preceding ':'. + if (tail_count > 0 && !src.consume(":")) + return false; + + if (!parse_u16_base16(src, tail[tail_count])) + return false; + tail_count++; + } + + assert(count + tail_count < 8); + + int implicit_pairs = 8 - (count + tail_count); + + for (int i = 0; i < implicit_pairs; i++) + dst.data[count++] = 0; + + // Now append the tail + for (int i = 0; i < tail_count; i++) + dst.data[count++] = tail[i]; + } + + assert(count == 8); + return true; +} + +bool parse_ipv6(Scanner &src, Host &dst) +{ + intptr_t start = src.off; + if (parse_ipv6(src, dst.ipv6)) { + dst.type = Host::IPV6; + dst.text.str = src.str; + dst.text.off = start; + dst.text.len = src.off - start; + return true; + } + return false; +} + +bool parse_u8_base10(Scanner &src, uint8_t &dst) +{ + if (src.end() || !is_digit(src.curr())) + return false; + + dst = 0; + do { + + int d = src.curr() - '0'; + assert(d >= 0 && d < 10); + + if (dst > (UINT8_MAX - d) / 10) { + src.back(); + break; + } + + dst = dst * 10 + d; + + src.consume(); + + } while (!src.end() && is_digit(src.curr())); + + return true; +} + +bool parse_u16_base10(Scanner &src, uint16_t &dst) +{ + if (src.end() || !is_digit(src.curr())) + return false; + + dst = 0; + do { + + int d = src.curr() - '0'; + assert(d >= 0 && d < 10); + + if (dst > (UINT16_MAX - d) / 10) { + src.back(); + break; + } + + dst = dst * 10 + d; + + src.consume(); + + } while (!src.end() && is_digit(src.curr())); + + return true; +} + +bool parse_ipv4(Scanner &src, IPv4 &dst) +{ + uint32_t word = 0; + for (int i = 0; i < 4; i++) { + + if (i > 0 && !src.consume('.')) + return false; + + uint8_t byte; + if (!parse_u8_base10(src, byte)) + return false; + word = (word << 8) | byte; + } + + dst = word; + return true; +} + +bool parse_ipv4(Scanner &src, Host &dst) +{ + intptr_t start = src.off; + if (parse_ipv4(src, dst.ipv4)) { + dst.type = Host::IPV4; + dst.text.str = src.str; + dst.text.off = start; + dst.text.len = src.off - start; + return true; + } + return false; +} + +bool parse_host(Scanner &src, Host &dst) +{ + if (src.end()) + return false; + + if (src.curr() == '[') { + + // IPv6 or IPvFuture + if (!parse_ipv6(src, dst)) + return false; + + if (!src.consume(']')) + return false; + + } else { + + bool ipv4; + + if (is_digit(src.curr())) + // May be a registered name or IPv4 + ipv4 = parse_ipv4(src, dst); + else + ipv4 = false; + + if (!ipv4) { + + // It's a registered name. + // + // From RFC 3986, Appendix A: + // + // reg-name = *( unreserved / pct-encoded / sub-delims ) + // + // It's worth noting that the registered name may be empty. + + intptr_t start = src.off; + src.consume([](char c) { return is_unreserved(c) || is_subdelim(c); }); + dst.type = Host::NAME; + dst.text.str = src.str; + dst.text.off = start; + dst.text.len = src.off - start; + } + } + + return true; +} + +bool parse_authority(Scanner &src, Authority &dst) +{ + parse_user_info(src, dst.userinfo); + + if (!parse_host(src, dst.host)) + return false; + + if (src.consume(':')) { + + // There may be a port + if (src.end() || !is_digit(src.curr())) + dst.port = -1; // No port + else { + uint16_t buffer; + if (!parse_u16_base10(src, buffer)) + return false; + dst.port = buffer; + } + } else + dst.port = -1; // No port + + return true; +} + +/* + * From RFC 3986, Section 3.4: + * + * query = *( pchar / "/" / "?" ) + * + * and Section 3.5: + * + * fragment = *( pchar / "/" / "?" ) + */ +Slice parse_query_or_fragment(Scanner& src) +{ + Slice res; + res.str = src.str; + res.off = src.off; + src.consume([](char c) { return is_pchar(c) || c == '/' || c == '?'; }); + res.len = src.off - res.off; + return res; +} + +Slice parse_path_abempty(Scanner& src) +{ + Slice path; + path.str = src.str; + path.off = src.off; + while (src.consume('/')) + src.consume(is_pchar); + path.len = src.off - path.off; + return path; +} + +Slice parse_path(Scanner& src) +{ + Slice path; + path.str = src.str; + path.off = src.off; + src.consume([](char c) { return is_pchar(c) || c == '/'; }); + path.len = src.off - path.off; + return path; +} + +/* + * See RFC 3986 + */ +bool parse_url(Scanner &src, URL &dst) +{ + Slice full; + full.str = src.str; + full.off = src.off; + + parse_scheme(src, dst.scheme); + + /* + * From RFC 3986, Section 3.2: + * The authority component is preceded by a double slash ("//") and is + * terminated by the next slash ("/"), question mark ("?"), or number + * sign ("#") character, or by the end of the URI. + */ + if (src.consume("//")) { + if (!parse_authority(src, dst.authority)) + return false; + if (src.peek() == '/') + dst.path = parse_path_abempty(src); + } else + dst.path = parse_path(src); + + if (src.consume('?')) dst.query = parse_query_or_fragment(src); + if (src.consume('#')) dst.fragment = parse_query_or_fragment(src); + + full.len = src.off - full.off; + dst.full = full; + return true; +} + +bool parse_method(Scanner& src, Method& dst, ParseError& error) +{ + intptr_t start = src.off; + if (!src.consume(is_upper_alpha)) { + error.write("Missing method"); + return false; + } + + Slice text; + text.str = src.str; + text.off = start; + text.len = src.off - start; + + if (text == "GET") + dst = GET; + else if (text == "POST") + dst = POST; + else { + error.write("Method not supported"); + return false; + } + + return true; +} + +bool parse_request(Scanner &src, Request &dst, ParseError& error) +{ + if (!parse_method(src, dst.method, error)) + return false; + + // Skip one space + if (!src.consume(' ')) { + error.write("Missing space after method"); + return false; + } + + URL url; + if (!parse_url(src, url)) { + error.write("Invalid URL"); + return false; + } + dst.url = url; + + if (!src.consume(" HTTP/1\r\n") && + !src.consume(" HTTP/1.0\r\n") && + !src.consume(" HTTP/1.1\r\n")) { + error.write("Invalid HTTP version token\n"); + return false; + } + + // Parse headers + if (!src.consume("\r\n")) { + + do { + Slice name; + Slice value; + + name.str = src.str; + name.off = src.off; + src.consume([](char c) { return c != ':'; }); + name.len = src.off - name.off; + + if (!src.consume(':')) { + error.write("Missing ':' after header name"); + return false; + } + + value.str = src.str; + value.off = src.off; + src.consume([](char c) { return c != '\r'; }); + value.len = src.off - value.off; + + // Add the parsed header to the request structure. + // If the header limit was reached, report that + // the header was dropped. + if (dst.count < MAX_REQUEST_HEADERS) + dst.headers[dst.count++] = (Header) {name, value}; + else + dst.ignored_count++; + + if (!src.consume("\r\n")) { + error.write("Missing CRLF after header body"); + return false; + } + + } while (!src.consume("\r\n")); // Parse headers until you find and empty line. + } + + // Now the cursor points to after the final CRLF. Lets make sure + // that nothing comes after that. + if (src.off != src.len) { + error.write("Bad characters after empty line"); + return false; + } + + // All done. + return true; +} + +bool Request::parse(const char *str, int len) +{ + ParseError error; + Scanner scanner(str, len); + valid = parse_request(scanner, *this, error); + return valid; +} + +bool Request::parse(const char *str, int len, ParseError& error) +{ + Scanner scanner(str, len); + valid = parse_request(scanner, *this, error); + return valid; +} + +bool Request::parse(Slice src) +{ + return parse(src.str, src.len); +} + +bool Request::parse(Slice src, ParseError& error) +{ + return parse(src.str, src.len, error); +} + +bool IPv4::parse(const char *str, int len) +{ + if (len < 0) len = strlen(str); + Scanner scanner(str, len); + return parse_ipv4(scanner, *this); +} + +bool IPv6::parse(const char *str, int len) +{ + if (len < 0) len = strlen(str); + Scanner scanner(str, len); + return parse_ipv6(scanner, *this); +} + +bool is_space(char c) +{ + return c == ' ' || c == '\t' || c == '\r' || c == '\n'; +} + +int Request::content_length() const +{ + if (!valid) + return 0; + + int i; // Header index + for (i = 0; i < count; i++) + if (headers[i].name == "Content-Length") + break; + if (i == count) + return 0; // Content-Length not found, assume 0 + + Slice value = headers[i].value; + int j = 0; // Header value cursor + + // Consume optional spaces + while (j < value.len && is_space(value[j])) + j++; + + // After the spaces is expected a number + if (j == value.len || !is_digit(value[j])) + return 0; // No number, assume 0 length + + // Found a digit. Parse the entire number until + // no more digits are found + int length = 0; + do { + char c = value[j]; + assert(is_digit(c)); + int digit = c - '0'; + if (length > (MAX_VALUE(length) - digit) / 10) { + // Overflow. The reported length is too big. + return -1; + } + length = length * 10 + digit; + j++; + } while (j < value.len && is_digit(value[j])); + + return length; +} \ No newline at end of file diff --git a/src/parse.hpp b/src/parse.hpp new file mode 100644 index 0000000..3e50f41 --- /dev/null +++ b/src/parse.hpp @@ -0,0 +1,89 @@ +#include "slice.hpp" +#include "netutils.hpp" + +struct Host { + + enum Type { NAME, IPV4, IPV6 }; + + Type type; + Slice text; // any type + IPv4 ipv4; // when type=IPV4 + IPv6 ipv6; // when type=IPV6; + + Host() + { + type = IPV4; + } +}; + +struct Authority { + Slice userinfo; + Host host; + int port; // -1 means no port + + Authority() + { + port = -1; + } +}; + +struct URL { + Slice full; + Slice scheme; + Authority authority; + Slice path; + Slice query; + Slice fragment; +}; + +struct Header { + Slice name; + Slice value; +}; + +enum Method { + GET, POST, +}; + +#include +#include + +struct ParseError { + char text[256]; + void write(const char *fmt, ...) + { + va_list args; + va_start(args, fmt); + vsnprintf(text, sizeof(text), fmt, args); + va_end(args); + } +}; + +constexpr int MAX_REQUEST_HEADERS = 32; + +struct Request { + + bool valid; + + Method method; + URL url; + + Header headers[MAX_REQUEST_HEADERS]; + int count, ignored_count; + + Slice body; + + Request() + { + valid = false; + method = GET; + count = 0; + ignored_count = 0; + } + + bool parse(const char *str, int len); + bool parse(const char *str, int len, ParseError& error); + bool parse(Slice slice); + bool parse(Slice slice, ParseError& error); + int content_length() const; +}; diff --git a/src/pool.hpp b/src/pool.hpp new file mode 100644 index 0000000..e094896 --- /dev/null +++ b/src/pool.hpp @@ -0,0 +1,206 @@ +#include + +template +class Pool { + + // Structure representing the bytes that can + // hold a "T". This lets us speak about T's + // memory without calling the constructor. + struct Slot { + alignas(T) char pad[sizeof(T)]; + }; + + static constexpr int ceil(int U, int V) + { + return (U + V - 1) / V; + } + + static const int NUM_BITSETS = ceil(N, 64); + + int num_allocated; + + // Memory available for allocation + Slot slots[N]; + + // Packed array of booleans. Each bit describes + // if its associated slot was allocated or not + uint64_t used[NUM_BITSETS]; + + // Returns the index from the right of the + // first set bit or -1 otherwise. + static int find_first_set_bit(uint64_t bits) + { + // First check that at least one bit is set + if (bits == 0) return -1; + + uint64_t bits_no_rightmost = bits & (bits - 1); + uint64_t bits_only_rightmost = bits - bits_no_rightmost; + + int index = 0; + uint64_t temp; + + // The index of the rightmost bit is the log2 + temp = bits_only_rightmost >> 32; + if (temp) { + // Bit is in the upper 32 bits + index += 32; + bits_only_rightmost = temp; + } + + temp = bits_only_rightmost >> 16; + if (temp) { + index += 16; + bits_only_rightmost = temp; + } + + temp = bits_only_rightmost >> 8; + if (temp) { + index += 8; + bits_only_rightmost = temp; + } + + static const uint8_t table[] = { + 0, 0, 1, 0, 2, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, + 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + + index += table[bits_only_rightmost]; + + return index; + } + + void set_bit(int index, bool val) + { + assert(index >= 0 && index < N); + + uint64_t mask = 1ULL << (index & 63); + if (val) + used[index >> 6] |= mask; + else + used[index >> 6] &= ~mask; + } + + bool get_bit(int index) const + { + uint64_t set = used[index >> 6]; + uint64_t mask = 1ULL << (index & 63); + return (set & mask) == mask; + } + + int get_index(T* addr) const + { + return (Slot*) addr - slots; + } + + T* find_not_allocated() + { + if (num_allocated == N) + return nullptr; + + // Find the index of a bitset with a zero bit + int i = 0; + while (used[i] == ~0ULL) + i++; + + // Now find the zero bit + int j = find_first_set_bit(~used[i]); + assert(j > -1); + + T* addr = (T*) &slots[i * 64 + j]; + assert(!allocated(addr)); + + return addr; + } + + void mark_allocated(T* addr) + { + num_allocated++; + set_bit(get_index(addr), 1); + } + + void mark_not_allocated(T* addr) + { + num_allocated--; + set_bit(get_index(addr), 0); + } + +public: + Pool() + { + num_allocated = 0; + for (int i = 0; i < NUM_BITSETS; i++) + used[i] = 0; + } + + ~Pool() + { + // Free allocated objects + for (int i = 0; i < N; i++) { + T* addr = (T*) &slots[i]; + if (allocated(addr)) + deallocate(addr); + } + } + + Pool(Pool& other) = delete; + Pool(Pool&& other) = delete; + Pool& operator=(Pool& other) = delete; + Pool& operator=(Pool&& other) = delete; + + T* allocate() + { + T* addr = find_not_allocated(); + if (addr == nullptr) return nullptr; + + new (addr) T(); + + mark_allocated(addr); + return addr; + } + + bool owned(T* addr) const + { + return (intptr_t) addr >= (intptr_t) slots + && (intptr_t) addr < (intptr_t) (slots + N); + } + + bool allocated(T* addr) + { + return owned(addr) && get_bit(get_index(addr)); + } + + int currently_allocated_count() const + { + return num_allocated; + } + + bool have_free_space() const + { + return num_allocated < N; + } + + void deallocate(T* addr) + { + if (!owned(addr) || !allocated(addr)) + return; + + // Destroy the object + addr->~T(); + + mark_not_allocated(addr); + } +}; \ No newline at end of file diff --git a/src/print.cpp b/src/print.cpp new file mode 100644 index 0000000..6eb8909 --- /dev/null +++ b/src/print.cpp @@ -0,0 +1,39 @@ + +#include "print.hpp" + +void vprint(std::ostream& dst, const char* fmt, PrintArg* pargs, int num_args) +{ + char sep = '@'; + + int curr_arg = 0; + int len = strlen(fmt); + int cur = 0; + while (cur < len) { + + // Move the cursor until the next separator + // or end of format string + int plain_text_off = cur; + while (cur < len && fmt[cur] != sep) + cur++; + int plain_text_len = cur - plain_text_off; + + // Write the plain text string + dst.write(fmt + plain_text_off, plain_text_len); + + // If the plain text string ended with a + // separator, print an argument + if (cur < len) { + assert(fmt[cur] == sep); + + if (curr_arg == num_args) { + // The '%' doesn't refer to any arguments + dst << sep; + } else { + dst << pargs[curr_arg++]; + } + + // Consume the '%' + cur++; + } + } +} \ No newline at end of file diff --git a/src/print.hpp b/src/print.hpp new file mode 100644 index 0000000..2a6df33 --- /dev/null +++ b/src/print.hpp @@ -0,0 +1,74 @@ +#include +#include +#include +#include + +struct PrintArg { + + enum Type { + INT, + FLOAT, + STRING, + OTHER, + }; + + Type type; + union { + int64_t as_int; + double as_float; + const char* as_string; + void* other; + } data; + + #define IS_INT(T) typename std::enable_if< std::is_integral::value>::type + #define ISNT_INT(T) typename std::enable_if::value>::type + + // Only used when type=OTHER + void (*print_ptr)(void* data, std::ostream& dst); + + template + PrintArg(IS_INT(T) value) + { + type = INT; + data.as_int = value; + } + + PrintArg(double value) + { + type = FLOAT; + data.as_float = value; + } + + PrintArg(const char* value) + { + type = STRING; + data.as_string = value; + } + + template + PrintArg(ISNT_INT(T)&& value) + { + data.other = &value; + print_ptr = [](void* data, std::ostream& dst) { dst << (*(T*) data); }; + } + + friend std::ostream& operator<<(std::ostream& dst, PrintArg& arg) + { + switch (arg.type) { + case INT : dst << arg.data.as_int; break; + case FLOAT : dst << arg.data.as_float; break; + case STRING: dst << arg.data.as_string; break; + case OTHER : arg.print_ptr(arg.data.other, dst); break; + } + return dst; + } +}; + +void vprint(std::ostream& dst, const char* fmt, PrintArg* pargs, int num_args); + +template +void print(std::ostream& dst, const char *fmt, Ts&& ...args) +{ + PrintArg pargs[] = {args...}; + vprint(dst, fmt, pargs, sizeof...(args)); +} diff --git a/src/queue.hpp b/src/queue.hpp new file mode 100644 index 0000000..c43999d --- /dev/null +++ b/src/queue.hpp @@ -0,0 +1,82 @@ +#include + +template +class Queue { + int head; + int used; + T items[N]; + +public: + Queue() + { + head = 0; + used = 0; + } + + Queue(Queue& other) = delete; + Queue(Queue&& other) = delete; + Queue& operator=(Queue& other) = delete; + Queue& operator=(Queue&& other) = delete; + + bool empty() const + { + return used == 0; + } + + int size() const + { + return used; + } + + template + bool push(U&& item) + { + if (used == N) + return false; + + int tail = (head + used) % N; + + items[tail] = std::forward(item); + used++; + + return true; + } + + bool pop() + { + if (used == 0) + return false; + items[head] = T(); + head = (head + 1) % N; + used--; + return true; + } + + bool pop(T& item) + { + if (used == 0) + return false; + item = std::move(items[head]); + return pop(); + } + + // Removes the first occurrence of item from the queue. Returns true if an + // item was found or false if it wasn't. + bool remove(const T& item) + { + int i; + for (i = 0; i < used; i++) { + int j = (head + i) % N; + if (items[j] == item) + break; + } + if (i == used) + return false; + for (; i < used-1; i++) { + int j = (head + i) % N; + items[j] = std::move(items[(j+1) % N]); + } + items[(head + used - 1) % N] = T(); + return true; + } +}; diff --git a/src/server.hpp b/src/server.hpp new file mode 100644 index 0000000..80b9ef6 --- /dev/null +++ b/src/server.hpp @@ -0,0 +1,704 @@ +#include +#include +#include +#include "pool.hpp" +#include "queue.hpp" +#include "parse.hpp" +#include "socket.hpp" +#include "buffer.hpp" + +/* + * Structure that represents the connection with a client + */ +struct Client { + + Socket sock; + Buffer in; + Buffer out; + + // Number of requests from this client that were + // handled. + int num_served; + + // True iff the client's reference is in the + // server's candidate queue + bool queued; + + // Tells the server that the connection with this + // client should be terminated when the output + // buffer is fully flushed. + bool close_when_flushed; + + Client() + { + queued = false; + num_served = 0; + close_when_flushed = false; + } + + Client(Client&) = delete; + Client(Client&&) = delete; + Client& operator=(Client&) = delete; + Client& operator=(Client&&) = delete; +}; + +template +class Server { + +public: + + Server() + { + state = NOTARGET; + } + + Server(Server& other) = delete; + Server(Server&& other) = delete; + Server& operator=(Server& other) = delete; + Server& operator=(Server&& other) = delete; + + /* + * Start listening for incoming connections on + * the specified port and on the "addr" interface. + * + * The "addr" argument must be an ipv4 address in + * dotted decimal notation. If it's NULL, the server + * will listen on all available interfaces. + */ + bool listen(int port=8080, const char *addr=nullptr); + + /* + * Get an HTTP request to handle. If a request was + * already queued this call won't block, else it + * will. + */ + void wait(Request& req); + + /* + * This function can only be called between two + * "wait" calls and it sets the HTTP status code + * of the reply to the last request returned by + * wait. + * + * You can't call this function more than once + * per request and it must be done before calling + * "status", "header", "write" or "send". + */ + void status(int code); + + /* + * Similarly to "status", this function sets a + * response header for the last request returned + * by "wait". + * + * It may be called more than once. But before + * "write" and "send". + */ + void header(const char *name, const char *value); + + /* + * Similar to "header" but appends bytes to the + * response's body. It must be called after "send". + */ + void write(const char *str, int len=-1); + + /* + * Mark a request as handled. You can no longer + * modify the response after you call this function. + */ + void send(); + +private: + + // Since responses are built using a kind of + // immediate-mode API ("status", "header", "write" + // and "send"), the server needs to hold a state + // to discriminate between valid and invalid calls + // for the response creation. + enum State { + + NOTARGET, // No request is being handled. This is both the + // starting value and that set by "send". + + STATUS, // A request was returned though "wait" but no + // "status" call was done + + HEADERS, // "status" has been called. Now either "header" + // or "write" are allowed. + + CONTENT, // A call to "write" has been done, so only other + // calls to it are allowed. + + // After any of these states you can "send" and + // go to the "NOTARGET" state. + }; + + State state; + + // Listening socket. + Socket socket_; + + // Pool of client structures + Pool pool; + + // The eventloop must be able to hold one entry per + // client and one more for the listening socket. + // + // It may be necessary to hold some timers for each + // client. + EventLoop evloop; + + // This queue holds references to clients that are + // "response candidates". A candidate is a client + // for which a request head was received, but the + // body may or may not. + // + // When a client is sending to the server a request, + // the moment the server receives the "\r\n\r\n" token + // it considers the client a "candidate" for being + // responded to. + // + // An HTTP request has this general structure: + // + // GET /home HTTP/1.1 \r\n + // header1: value1 \r\n + // header2: value2 \r\n + // Content-Length: XXX \r\n + // \r\n + // ... Content ... + // + // So the \r\n\r\n determines the end of the request's + // head and start of the body. There is no way of knowing + // if the request body was also received without parsing + // the entire request and getting the value of the + // "Content-Length" header. To avoid parsing the request + // twice or having to cache the result, we just mark the + // client as "candidate" and push it to this queue. The + // "wait" function will pop elements of this queue looking + // for one that's actually ready and return that to the + // user. + Queue queue; + + // The following fields are state necessary when responding + // to a request. They only hold meaning when the state isn't + // NOTARGET. + + Client* target; // Current client that's being responded to + + int offset_content_length; // Offset (in bytes) of the "Content-Length" header's value + // in the output buffer of the target client. This is set + // during the first "write" call after a "wait". + + int offset_content; // Offset (in bytes) of the response body in the output buffer + // of the target client. It's set at the first "write" call after + // "wait". + + int keep_alive; // This is 1 if the user set the "Connection: Keep-Alive" header or + // 0 if it set "Connection: Close". Its initial value is -1, so reading + // -1 means the user didn't specify anything yet. + + int req_bytes; // Size (in bytes) of the request that's being served. This is necessary + // when the response is completed and the request bytes can be dropped. + + static const char* status_text(int code); + + // Choose if a given connection can be kept alive. + // This is a function of: + // 1. num_clients: The number of curretly connected clients + // 2. max_clients: The client limit + // 3. How many responses were previously served to this client + static bool should_keep_alive(int num_clients, int max_clients, int num_served); + + void remove_client(Client* client); + void accept_incoming_connections(); + void handle_single_event(Event event); + void handle_client_data_and_queue_if_candidate(Client* client); + void flush_buffered_bytes_to_client_and_close_if_done(Client* client); +}; + +template +bool Server::listen(int port, const char *addr) +{ + if (socket_.active()) + return false; // Already listening + + Socket socket; + if (!socket.start_server(port, addr)) + return false; + + // We want to know when calling "accept" on the socket + // will not block. From the point of view of "poll" + // (the underlying syscall of the event loop) an ACCEPT + // operation is a read operation. + if (!evloop.add(socket, Event::RECV, this)) { + std::clog << "Couldn't add socket to event loop object\n"; + return false; + } + + std::clog << "Listening on " << (addr ? addr : "0.0.0.0") << ":" << port << "\n"; + + // Commit the socket structure + socket_ = std::move(socket); + + return true; +} + +/* + * See the forward declaration. + */ +template +void Server::wait(Request& req) +{ + // Make sure any pending response is sent and + // the state is NOTARGET. + send(); + + assert(state == NOTARGET); + + /* + * Basically what this loop is doing is handling + * TCP level I/O until one or more clients become + * response candidates. When that's true it pops + * a client, parses the request into the user-provided + * structure and checks that the request body was + * fully received. If it wasn't it drops the client + * and gets or waits for a new candidate. If the body + * was received, it returns to the user. + * + * Clients that were considered candidates but + * couldn't be served yet may receive more bytes + * in the future. Whenever they receive bytes they + * will be considered candidates again until they're + * served. + */ + do { + + while (queue.empty()) { + Event event = evloop.wait(); + handle_single_event(event); + } + + Client* candidate; + queue.pop(candidate); + candidate->queued = false; + assert(pool.allocated(candidate)); + + // It's known that the input buffer contains + // a \r\n\r\n or the client wouldn't have been + // inserted in the queue. + Slice head = candidate->in.slice_until("\r\n\r\n", true); + + ParseError error; + if (!req.parse(head, error)) { + // Invalid request + // TODO: Send a message to the client before removing it + std::clog << "Parsing Error: " << error.text << "\n"; + remove_client(candidate); + continue; + } + + int head_len = head.len; + int body_len = req.content_length(); + if (body_len < 0) { + // Malformed Content-Length header + std::clog << "Malformed Content-Length header\n"; + remove_client(candidate); + continue; + } + + int total_len = head_len + body_len; + + // We know the head of the request was received, but + // if the body wasn't we can't respond yet. + if (candidate->in.length() >= total_len) { + // Request was fully received + req.body = candidate->in.slice(head_len, total_len); + target = candidate; + state = STATUS; + req_bytes = total_len; + keep_alive = -1; + break; + } + + // Still waiting for the request's body. + // Go back to waiting for a candidate. + } while (1); +} + +template +bool Server::should_keep_alive(int num_clients, int max_clients, int num_served) +{ + // If the server is about 70% full, don't keep connections alive + if (10 * num_clients > 7 * max_clients) + return false; + + // Only keep alive if less than 5 responses were served + if (num_served >= 5) + return false; + + return true; +} + +template +void Server::remove_client(Client* client) +{ + assert(pool.allocated(client)); + evloop.remove(client->sock); + if (client->queued) + queue.remove(client); + pool.deallocate(client); + assert(!pool.allocated(client)); +} + +template +void Server::accept_incoming_connections() +{ + // TODO: Since we're leaving some connections in the queue + // when the client limit is reached, we need to make + // sure to serve them when some client structs are freed. + + // Accept all incoming connections until the client pool is full + for (Socket sock; pool.have_free_space() && socket_.accept(sock); ) { + + Client* client = pool.allocate(); + assert(client); + + // At first only register for receive events since + // there's nothing to be sent. + if (!evloop.add(sock, Event::RECV, client)) { + pool.deallocate(client); + continue; + } + + // Commit socket + client->sock = std::move(sock); + + // The newly accepted client may already have some + // data to be read. Generate a RECV event manually. + handle_single_event(Event(Event::RECV, client)); + } +} + +template +void Server::handle_client_data_and_queue_if_candidate(Client* client) +{ + // Client sent data. Copy it into the buffer + bool closed = client->in.write(client->sock); + if (closed || client->in.failed()) { + remove_client(client); + return; + } + + // If the client isn't already ready to be served, + // it may be now. Check wether the head of the request + // was fully received. + + // The head of a request is terminated by a CRLF CRLF + // token. + if (client->in.contains("\r\n\r\n")) { + + // Head was received! Push the client into the queue if + // it wasn't already. + + if (!client->queued) { + queue.push(client); + client->queued = true; + } + + // It's possible to avoid to check wether the client is + // contained in the queue by caching the information. + // This would speed up the process but also add redundancy + // to the class and possible bugs. + } +} + +template +void Server::flush_buffered_bytes_to_client_and_close_if_done(Client* client) +{ + // Client is ready to receive data + client->out.read(client->sock); + if (client->out.failed()) { + remove_client(client); + return; + } + + if (client->out.length() == 0) { + + // Nothing more to send. + + if (client->close_when_flushed) { + remove_client(client); + return; + } + + // Tell the eventloop we're not interested + // in output events for this client + evloop.remove_events(client->sock, Event::SEND); + } +} + +template +void Server::handle_single_event(Event event) +{ + if (event.data == nullptr) + return; // Event isn't relative to a socket + + if (event.data == this) + accept_incoming_connections(); + else { + Client* client = (Client*) event.data; + assert(pool.allocated(client)); + switch (event.type) { + case Event::FAILURE: remove_client(client); break; + case Event::RECV: handle_client_data_and_queue_if_candidate(client); break; + case Event::SEND: flush_buffered_bytes_to_client_and_close_if_done(client); break; + } + } +} + +template +void Server::status(int code) +{ + if (state == NOTARGET) + return; + + assert(target); + + if (state != STATUS) + return; // "status" called twice + + char buf[256]; + int len = snprintf(buf, sizeof(buf), "HTTP/1.1 %d %s\r\n", code, status_text(code)); + assert(len > 0); + + // No need to check for errors. We'll do it + // when "reply" is called. + target->out.write(buf, len); + + state = HEADERS; +} + +template +void Server::header(const char *name, const char *value) +{ + if (state == NOTARGET) + return; + + if (state == STATUS) + // Header added before a status, so first + // add a 200 for correctness sake + status(200); + + if (state == CONTENT) + // Can't add a header after the start of + // the response's body. + return; + + assert(state == HEADERS); + + // Make sure that the caller isn't writing + // a header that must be added automatically + // by this class. + // TODO: Make the check case-insensitive. + if (!strcmp(name, "Content-Length")) + return; + + if (!strcmp(name, "Connection")) { + if (!strcmp(value, "Close")) + keep_alive = 0; + else + keep_alive = 1; + return; + } + + target->out.write(name); + target->out.write(": "); + target->out.write(value); + target->out.write("\r\n"); +} + +template +void Server::write(const char *str, int len) +{ + if (len < 0) len = strlen(str); + + if (state == NOTARGET) + return; + + assert(target); + + if (state == STATUS) + status(200); + + // If this is the first time we append to the + // body of the response, append special headers + if (state == HEADERS) { + + // This is the start of the response body, so + // add any special header and the empty line + // separator. + + if (keep_alive == -1) keep_alive = 1; + + // If the user wants to keep the connection alive + // (or didn't specify it) then check first if it's + // reasonable given the server's state. + int num_clients = pool.currently_allocated_count(); + int max_clients = N; + int num_served = target->num_served; + if (!should_keep_alive(num_clients, max_clients, num_served)) + keep_alive = false; + + switch (keep_alive) { + case 0: target->out.write("Connection: Close\r\n"); break; + case 1: target->out.write("Connection: Keep-Alive\r\n"); break; + } + + // Append the Content-Length header with an empty value. + // When the response content is known we'll fill the value in. + target->out.write("Content-Length: "); + offset_content_length = target->out.length(); + target->out.write(" \r\n"); // This is exactly 9 spaces before the \r\n + + // Write an empty line + target->out.write("\r\n"); + + offset_content = target->out.length(); + state = CONTENT; + } + + target->out.write(str, len); +} + +template +void Server::send() +{ + if (state == NOTARGET) + return; + + assert(target); + + // Make sure the previous response parts are written + write(""); + + if (target->out.failed()) { + + // Actually the response construction failed, so drop the client. + remove_client(target); + + } else { + + // Update the Content-Length header's vale now + // that we know the content's length. + int content_length = target->out.length() - offset_content; + + char buf[32]; + int len = snprintf(buf, sizeof(buf), "%d", content_length); + assert(len < 10); + + target->out.overwrite(offset_content_length, buf, len); + + // Tell the eventloop that we're interested in output + // events for this client. + evloop.add_events(target->sock, Event::SEND); + + // If the connection isn't marked as reusable, mark it + // to be closed when the output buffer is flushed and + // stop listening for input data. + // NOTE: "keep_alive" can't be -1 at this point because + // it was set to either 0 or 1 when writing the + // "Connection" header. + if (keep_alive == 0) { + target->close_when_flushed = true; + evloop.remove_events(target->sock, Event::RECV); + } + + // Now that the request was served, we can remove it + // from the input buffer. + target->in.consume(req_bytes); + + // If the connection is keep-alive, pipelining is allowed + // so check if an other request is pending and if it is, + // put the client back into the queue. + if (keep_alive && target->in.contains("\r\n\r\n")) { + // We know that the client isn't already in the queue + // because we just popped and served it. + queue.push(target); + target->queued = true; + } + + target->num_served++; + } + + state = NOTARGET; + target = nullptr; + keep_alive = -1; + req_bytes = -1; +} + +template +const char* Server::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 "???"; +} diff --git a/src/slice.hpp b/src/slice.hpp new file mode 100644 index 0000000..5417a71 --- /dev/null +++ b/src/slice.hpp @@ -0,0 +1,54 @@ +#ifndef SLICE_HPP +#define SLICE_HPP + +#include +#include +#include + +struct Slice { + + const char *str; + intptr_t off; + intptr_t len; + + Slice() + { + whipe(); + } + + Slice(const char *str2, int len2) + { + str = str2; + len = len2; + off = 0; + } + + void whipe() + { + str = nullptr; + off = 0; + len = 0; + } + + char operator[](int index) const + { + assert(index >= 0 && index < len); + return str[off + index]; + } + + bool operator==(const char *s) const + { + intptr_t l = strlen(s); + if (l != len) + return false; + return !strncmp(str+off, s, len); + } + + friend std::ostream& operator<<(std::ostream& os, Slice& sl) + { + os.write(sl.str + sl.off, sl.len); + return os; + } +}; + +#endif \ No newline at end of file diff --git a/src/socket.cpp b/src/socket.cpp new file mode 100644 index 0000000..2b2acb3 --- /dev/null +++ b/src/socket.cpp @@ -0,0 +1,19 @@ +#include +#include +#include "socket.hpp" + +std::ostream& operator<<(std::ostream& os, Event::Type const& type) +{ + switch (type) { + case Event::FAILURE: os << "FAILURE"; break; + case Event::RECV: os << "RECV"; break; + case Event::SEND: os << "SEND"; break; + } + os << " (" << (int) type << ")"; + return os; +} + +std::ostream& operator<<(std::ostream& os, Event const& event) +{ + return os << "Event { type=" << event.type << ", data=" << event.data << " }"; +} diff --git a/src/socket.hpp b/src/socket.hpp new file mode 100644 index 0000000..d07aa90 --- /dev/null +++ b/src/socket.hpp @@ -0,0 +1,401 @@ +#ifndef SOCKET_HPP +#define SOCKET_HPP + +#include + +#ifdef _WIN32 +#include +#include +#define POLL WSAPoll +#define EWOULDBLOCK_2 WSAEWOULDBLOCK +#define EAGAIN_2 WSAEWOULDBLOCK +#define EINVAL_2 WSAEINVAL +#define CLOSESOCKET closesocket +#else +#include +#include +#include +#include +#include +#include +#include +#define SOCKET int +#define INVALID_SOCKET -1 +#define POLL poll +#define CLOSESOCKET close +#define EWOULDBLOCK_2 EWOULDBLOCK +#define EAGAIN_2 EAGAIN +#define EINVAL_2 EINVAL +#endif + +struct SocketSubsystem { + SocketSubsystem() + { + #ifdef _WIN32 + WSADATA data; + int res = WSAStartup(MAKEWORD(2,2), &data); + if (res) + std::cout << "WSAStartup failed\n"; + #endif + } + ~SocketSubsystem() + { + #ifdef _WIN32 + WSACleanup(); + #endif + } +}; + +struct Socket { + +private: + + static int get_last_error() + { + #ifdef _WIN32 + return WSAGetLastError(); + #else + return errno; + #endif + } + + static bool set_blocking(SOCKET fd, bool value) + { + #ifdef _WIN32 + unsigned long not_value = !value; + return ioctlsocket(fd, FIONBIO, ¬_value) != SOCKET_ERROR; + #else + int flags = fcntl(fd, F_GETFL); + if (flags == -1) + return false; + if (value) + flags &= ~O_NONBLOCK; + else + flags |= O_NONBLOCK; + return fcntl(fd, F_SETFL, flags) != -1; + #endif + } + +public: + + SOCKET fd_; + + enum { + WOULD_BLOCK = -1, + OTHER_ERROR = -2, + }; + + Socket(SOCKET fd=INVALID_SOCKET) + { + fd_ = fd; + } + + Socket(Socket&) = delete; + Socket& operator=(Socket&) = delete; + + Socket(Socket&& other) + { + if (this != &other) { + fd_ = other.fd_; + other.fd_ = INVALID_SOCKET; + } + } + + Socket& operator=(Socket&& other) + { + if (this != &other) { + if (fd_ != INVALID_SOCKET) CLOSESOCKET(fd_); + fd_ = other.fd_; + other.fd_ = INVALID_SOCKET; + } + return *this; + } + + ~Socket() + { + if (fd_ != INVALID_SOCKET) + CLOSESOCKET(fd_); + } + + bool active() const + { + return fd_ != INVALID_SOCKET; + } + + bool accept(Socket& dst) + { + if (!active()) return false; + + int accepted = ::accept(fd_, nullptr, nullptr); + if (accepted < 0) + return false; + + if (!set_blocking(accepted, false)) { + CLOSESOCKET(accepted); + return false; + } + + dst = accepted; + return true; + } + + int read(char *dst, int max) + { + if (!active()) return -1; + + int res = recv(fd_, dst, max, 0); + if (res < 0) { + int code = get_last_error(); + if (code == EWOULDBLOCK_2 || code == EAGAIN_2) + return WOULD_BLOCK; + else + return OTHER_ERROR; + } + assert(res >= 0); + return res; + } + + int write(char *src, int num) + { + if (!active()) return -1; + int res = send(fd_, src, num, 0); + if (res < 0) { + int code = get_last_error(); + if (code == EWOULDBLOCK_2 || code == EAGAIN_2) + return WOULD_BLOCK; + else + return OTHER_ERROR; + } + assert(res >= 0); + return res; + } + + bool start_server(int port, const char *addr) + { + if (active()) return false; + + SOCKET fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd == INVALID_SOCKET) { + std::clog << "Couldn't create socket (did you initialize the socket system?)\n"; + return false; + } + + if (!set_blocking(fd, false)) { + CLOSESOCKET(fd); + std::clog << "Couldn't set socket as non-blocking\n"; + return false; + } + + struct in_addr addr_buf; + if (addr == nullptr) + addr_buf.s_addr = INADDR_ANY; + else { + int res = inet_pton(AF_INET, addr, &addr_buf); + if (res == 0 || res == -1) { + if (res == 0) { + // Invalid address string + std::cout << "Invalid address string\n"; + } else { + // Unknown error + std::cout << "Unknown error\n"; + } + CLOSESOCKET(fd); + return false; + } + assert(res == 1); + } + + // Probably should only use this in debug + int v = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char*) &v, sizeof(int)); + + struct sockaddr_in full_addr_buf; + full_addr_buf.sin_family = AF_INET; + full_addr_buf.sin_port = htons(port); + full_addr_buf.sin_addr = addr_buf; + if (bind(fd, (struct sockaddr*) &full_addr_buf, sizeof(full_addr_buf))) { + int code = get_last_error(); + std::cout << "Couldn't bind to the specified address (code " << code << ")\n"; + CLOSESOCKET(fd); + return false; + } + + int backlog = 32; + if (listen(fd, backlog)) { + std::cout << "Couldn't start listening on " << addr << ":" << port << "\n"; + CLOSESOCKET(fd); + return false; + } + + fd_ = fd; + return true; + } + +}; + +struct Event { + enum Type { + FAILURE = 0, + RECV = 1 << 0, + SEND = 1 << 1, + }; + Type type; + void *data; + + Event() + { + type = FAILURE; + data = nullptr; + } + + Event(Type t, void* p=nullptr) + { + type = t; + data = p; + } + + friend std::ostream& operator<<(std::ostream& os, Event::Type const& type); + friend std::ostream& operator<<(std::ostream& os, Event const& event); +}; + +template +class EventLoop { + void *ptrs[N]; + struct pollfd bufs[N]; + int count; + int cursor; + + int find_socket_index(const Socket& sock) + { + for (int i = 0; i < count; i++) + if (bufs[i].fd == sock.fd_) + return i; + return -1; + } + + static int convert_event_flags(int in) + { + int out = 0; + if (in & Event::RECV) out |= POLLIN; // Could OR POLLPRI but it's not supported by windows + if (in & Event::SEND) out |= POLLOUT; + return out; + } + +public: + + EventLoop() + { + count = 0; + cursor = 0; + } + + ~EventLoop() + { + } + + bool add(const Socket& sock, int events, void *ptr=nullptr) + { + if (count == N) + return false; + + bufs[count].fd = sock.fd_; + bufs[count].events = convert_event_flags(events); + bufs[count].revents = 0; + ptrs[count] = ptr; + + count++; + return true; + } + + void add_events(const Socket& sock, int events) + { + int i = find_socket_index(sock); + if (i < 0) return; // Not found + + bufs[i].events |= convert_event_flags(events); + } + + void remove_events(const Socket& sock, int events) + { + int i = find_socket_index(sock); + if (i < 0) return; // Not found + + bufs[i].events &= ~convert_event_flags(events); + } + + bool remove(const Socket& sock) + { + int i = find_socket_index(sock); + if (i < 0) return false; // Not found + + bufs[i] = bufs[count-1]; + ptrs[i] = ptrs[count-1]; + count--; + + if (cursor > i) cursor--; + + // TODO: Remove all buffered events that refer + // to this socket. + + return true; + } + + // Move the cursor forward until a struct + // with some reported events is found. If + // no such structs exists, then "cursor" + // reaches "count". + void skip() + { + while (cursor < count && bufs[cursor].revents == 0) + cursor++; + } + + Event wait() + { + skip(); + + // If no more buffers have events, poll for more events + while (cursor == count) { + + int n = POLL(bufs, count, -1); + if (n < 0) + return Event(Event::FAILURE); + + cursor = 0; + skip(); + } + assert(cursor < count); + + // At this point we know the cursor refers a struct + // with at least one reported event. + + void* ptr = ptrs[cursor]; + auto& revents = bufs[cursor].revents; + assert(revents != 0); + + // Report to the caller only one of those events + // at the time. If report RECV events. Once those + // are reported, at the next iteration SEND events + // will be reported. + + // We assume POLLPRI isn't reported (Windows doesn't + // support it). + assert((revents & POLLPRI) == 0); + + if (revents & POLLIN) { + revents &= ~POLLIN; + return Event(Event::RECV, ptr); + } + + if (revents & POLLOUT) { + revents &= ~POLLOUT; + return Event(Event::SEND, ptr); + } + + // Report other events as errors + revents = 0; + return Event(Event::FAILURE, ptr); + } +}; + +#endif \ No newline at end of file diff --git a/test/fuzz_parse_ipv4.cpp b/test/fuzz_parse_ipv4.cpp new file mode 100644 index 0000000..a79f59e --- /dev/null +++ b/test/fuzz_parse_ipv4.cpp @@ -0,0 +1,33 @@ +#include +#include "test_utils.hpp" +#include "../src/netutils.hpp" +#include +#include + +extern "C" +int LLVMFuzzerTestOneInput(const char *data, + size_t size) +{ + IPv4 ip; + bool ok = ip.parse(data, size); + + char buf[256]; + if (size < sizeof(buf)) { + memcpy(buf, data, size); + buf[size] = '\0'; + struct in_addr buf2; + switch (inet_pton(AF_INET, buf, &buf2)) { + + case 1: + test(ok); + test(ip.data == buf2.s_addr); + break; + + case 0: + case -1: + test(!ok); + break; + } + } + return 0; // Values other than 0 and -1 are reserved for future use. +} \ No newline at end of file diff --git a/test/fuzz_parse_ipv6.cpp b/test/fuzz_parse_ipv6.cpp new file mode 100644 index 0000000..62a263b --- /dev/null +++ b/test/fuzz_parse_ipv6.cpp @@ -0,0 +1,34 @@ +#include +#include "test_utils.hpp" +#include "../src/netutils.hpp" +#include +#include + +extern "C" +int LLVMFuzzerTestOneInput(const char *data, + size_t size) +{ + IPv6 ip; + bool ok = ip.parse(data, size); + + char buf[512]; + if (size < sizeof(buf)) { + memcpy(buf, data, size); + buf[size] = '\0'; + struct in6_addr buf2; + switch (inet_pton(AF_INET6, buf, &buf2)) { + + case 1: + test(ok); + test(!memcmp(&ip.data, &buf2, 16)); + break; + + case 0: + case -1: + test(!ok); + break; + } + + } + return 0; // Values other than 0 and -1 are reserved for future use. +} \ No newline at end of file diff --git a/test/test_parse_ipv4.cpp b/test/test_parse_ipv4.cpp new file mode 100644 index 0000000..54d3411 --- /dev/null +++ b/test/test_parse_ipv4.cpp @@ -0,0 +1,16 @@ +#include +#include "test_utils.hpp" +#include "../src/netutils.hpp" + +int main() +{ + IPv4 ip; + test(ip.parse("") == false); + test(ip.parse("@") == false); + test(ip.parse("1") == false); + test(ip.parse("500") == false); + test(ip.parse("45.") == false); + test(ip.parse("45.54.56.98") == true); + std::cout << "Passed\n"; + return 0; +} \ No newline at end of file diff --git a/test/test_queue.cpp b/test/test_queue.cpp new file mode 100644 index 0000000..b751c70 --- /dev/null +++ b/test/test_queue.cpp @@ -0,0 +1,109 @@ +#include +#include +#include "test_utils.hpp" +#include "../src/queue.hpp" + +int main() +{ + { + Queue q; + test(q.push(10) == false); + test(q.size() == 0); + test(q.empty() == true); + test(q.pop() == false); + } + + { + Queue q; + test(q.size() == 0); + test(q.empty() == true); + test(q.push(10) == true); + test(q.size() == 1); + test(q.empty() == false); + test(q.push(4) == false); + test(q.size() == 1); + test(q.empty() == false); + test(q.pop() == true); + test(q.pop() == false); + } + + { + Queue q; + + test(q.push(1) == true); + test(q.size() == 1); + + test(q.push(2) == true); + test(q.size() == 2); + + test(q.push(3) == true); + test(q.size() == 3); + + test(q.push(4) == true); + test(q.size() == 4); + + int x; + + test(q.pop(x) == true); + test(x == 1); + test(q.size() == 3); + + test(q.push(5) == true); + test(q.size() == 4); + + test(q.pop(x) == true); + test(x == 2); + test(q.size() == 3); + + test(q.push(6) == true); + test(q.size() == 4); + + test(q.pop(x) == true); + test(x == 3); + test(q.size() == 3); + + test(q.push(7) == true); + test(q.size() == 4); + + test(q.pop(x) == true); + test(x == 4); + test(q.size() == 3); + + test(q.push(8) == true); + test(q.size() == 4); + + test(q.pop(x) == true); + test(x == 5); + test(q.size() == 3); + + test(q.push(9) == true); + test(q.size() == 4); + + test(q.pop(x) == true); + test(x == 6); + test(q.size() == 3); + + test(q.push(10) == true); + test(q.size() == 4); + + test(q.pop(x) == true); + test(x == 7); + test(q.size() == 3); + + test(q.pop(x) == true); + test(x == 8); + test(q.size() == 2); + + test(q.pop(x) == true); + test(x == 9); + test(q.size() == 1); + + test(q.pop(x) == true); + test(x == 10); + test(q.size() == 0); + + test(q.pop(x) == false); + } + + std::cout << "Passed\n"; +} \ No newline at end of file diff --git a/test/test_utils.cpp b/test/test_utils.cpp new file mode 100644 index 0000000..31ebc03 --- /dev/null +++ b/test/test_utils.cpp @@ -0,0 +1,10 @@ +#include +#include + +void test_(bool expr, const char *text, const char *file, int line) +{ + if (!expr) { + std::cout << "Failure in " << file << ":" << line << " [" << text << "]\n"; + abort(); + } +} \ No newline at end of file diff --git a/test/test_utils.hpp b/test/test_utils.hpp new file mode 100644 index 0000000..6637001 --- /dev/null +++ b/test/test_utils.hpp @@ -0,0 +1,3 @@ + +void test_(bool expr, const char *text, const char *file, int line); +#define test(expr) test_(expr, #expr, __FILE__, __LINE__) \ No newline at end of file