From 0f7e668ef0d201b27fdbf7cabb4ab0c3f2a2d39a Mon Sep 17 00:00:00 2001 From: James Yab Date: Wed, 17 Dec 2025 20:08:24 -0500 Subject: [PATCH 01/22] Refactor parsing logic into multiple functions --- include/HttpServer.h | 15 +++++- src/HttpServer.cpp | 123 ++++++++++++++++++++++++------------------- 2 files changed, 82 insertions(+), 56 deletions(-) diff --git a/include/HttpServer.h b/include/HttpServer.h index 7c8b411..e840718 100644 --- a/include/HttpServer.h +++ b/include/HttpServer.h @@ -17,8 +17,9 @@ class HttpServer { std::atomic stop_flag {}; struct Request { + std::string_view method; std::string_view route; - std::string body; + std::string_view body; }; struct Response { @@ -29,6 +30,8 @@ class HttpServer { using Handler = std::function; + static bool is_valid_request(std::string &request_buffer, ssize_t bytes_read); + void get_mapping(std::string_view route, const Handler& fn); void post_mapping(std::string_view route, const Handler& fn); @@ -74,6 +77,16 @@ class HttpServer { int listener_fd {-1}; + std::string_view get_method(int conn_fd, std::string_view path, size_t &method_itr, bool &continues); + + static bool parse(std::string_view buffer, Request& out); + + static bool parse_method(std::string_view buffer, std::string_view& method, size_t& offset); + + static bool parse_route(std::string_view buffer, std::string_view& route, size_t& offset); + + static bool parse_body(std::string_view buffer, std::string_view& body, const size_t& offset); + /* * @brief return a listener socket file descriptor */ diff --git a/src/HttpServer.cpp b/src/HttpServer.cpp index 82e7a04..8082634 100644 --- a/src/HttpServer.cpp +++ b/src/HttpServer.cpp @@ -1,6 +1,5 @@ #include "../include/HttpServer.h" #include "../include/constants.h" -#include #include #include #include @@ -52,7 +51,7 @@ void HttpServer::listen(int port) { struct sockaddr_storage incoming_addr {}; socklen_t addr_size {sizeof(incoming_addr)}; - int conn_file_descriptor = accept(listener_fd, reinterpret_cast(&incoming_addr), &addr_size); + const int conn_file_descriptor = accept(listener_fd, reinterpret_cast(&incoming_addr), &addr_size); if (conn_file_descriptor == -1) { // If we're stopping, accept failures are expected; don't spam logs. if (stop_flag.load()) break; @@ -94,10 +93,66 @@ void HttpServer::store_conn_fd(int conn_fd) { queue.push(conn_fd); } +bool HttpServer::parse(const std::string_view buffer, Request& out) { + size_t offset {0}; + if (parse_method(buffer, out.method, offset) && + parse_route(buffer, out.route, offset) && + parse_body(buffer, out.body, offset)) { + return true; + } + return false; +} + +bool HttpServer::is_valid_request(std::string &request_buffer, ssize_t bytes_read) { + // Check if the request is empty + if (bytes_read <= 0) { + std::cerr << "Invalid request formatting: 0 bytes read\n"; + return false; + } + request_buffer[bytes_read] = '\0'; // Null-terminate for safety + return true; +} + +bool HttpServer::parse_method(const std::string_view buffer, std::string_view& method, size_t& offset) { + offset = buffer.find(' '); + if (offset == std::string_view::npos) { + std::cerr << "Invalid request formatting: no spaces\n"; + return false; + } + method = buffer.substr(0, offset); + offset++; + return true; +} + +bool HttpServer::parse_route(const std::string_view buffer, std::string_view& route, size_t& offset) { + const size_t route_start_itr = offset; + offset = buffer.find(' ', route_start_itr); + if (offset == std::string_view::npos) { + std::cerr << "Invalid request formatting: no valid route\n"; + return false; + } + route = buffer.substr(route_start_itr, offset - route_start_itr); + offset++; + return true; +} + +bool HttpServer::parse_body(std::string_view buffer, std::string_view& body, const size_t& offset) { + size_t body_start_itr = buffer.find("\r\n\r\n", offset); + if (body_start_itr == std::string_view::npos) { + std::cerr << "Invalid request formatting: the start of the request body is malformed\n"; + return false; + } + body_start_itr += 4; + body = buffer.substr(body_start_itr, buffer.size() - body_start_itr); + return true; +} + void HttpServer::handle_client() { while (!stop_flag.load()) { // Read the incoming HTTP request - char request_buffer[4096]; + std::string request_buffer; + request_buffer.resize(4096); + int conn_fd {}; // if queue is empty @@ -106,68 +161,30 @@ void HttpServer::handle_client() { continue; } - ssize_t bytes_read = recv(conn_fd, request_buffer, sizeof(request_buffer) - 1, 0); - - if (bytes_read <= 0) { - close(conn_fd); - if (stop_flag.load()) return; - std::cerr << "Invalid request formatting: 0 bytes read\n"; - continue; - } - request_buffer[bytes_read] = '\0'; // Null-terminate for safety - - std::string_view path {request_buffer}; - - // find the method - size_t method_itr = path.find(' '); - if (method_itr == std::string_view::npos) { + ssize_t bytes_read = recv(conn_fd, request_buffer.data(), request_buffer.size(), 0); + if (!is_valid_request(request_buffer, bytes_read)) { close(conn_fd); - std::cerr << "Invalid request formatting: no spaces\n"; continue; } - // check for valid method - std::string_view method = path.substr(0, method_itr); - // std::cout << "method: " << method << '\n'; - - // get the route which is the second word - size_t route_start = method_itr + 1; - size_t route_end = path.find(' ', route_start); - if (route_end == std::string_view::npos) { + Request req {}; + if (!parse(request_buffer, req)) { close(conn_fd); - std::cerr << "Invalid request formatting: no valid route\n"; continue; } - std::string_view route = path.substr(route_start, route_end - route_start); - // std::cout << "route: " << route << '\n'; - - // get body - size_t req_body_delimiter = path.find("\r\n\r\n"); - if (req_body_delimiter == std::string_view::npos) { - close (conn_fd); - std::cerr << "Invalid request formatting: the start of the request body is malformed\n"; - continue; - } - - size_t req_body_start = req_body_delimiter + 4; - std::string_view req_body = path.substr(req_body_start, path.size() - req_body_start); - // std::cout << "body: " << req_body << '\n'; - - // TODO: create a map that has a key route and function pointer - Response res {}; std::string response {}; - switch (method_hash(method)) { + switch (method_hash(req.method)) { case compile_time_method_hash("GET"): case compile_time_method_hash("DELETE"): case compile_time_method_hash("HEAD"): case compile_time_method_hash("OPTIONS"): case compile_time_method_hash("CONNECT"): case compile_time_method_hash("TRACE"): { - const Request req { path, ""}; - if (routes[method].find(route) != routes[method].end()) { - Handler route_fn = routes[method][route]; + req.body = ""; + if (routes[req.method].find(req.route) != routes[req.method].end()) { + Handler route_fn = routes[req.method][req.route]; route_fn(req, res); response = "HTTP/1.1 200 OK\r\n" @@ -189,9 +206,8 @@ void HttpServer::handle_client() { case compile_time_method_hash("POST"): case compile_time_method_hash("PUT"): case compile_time_method_hash("PATCH"): { - const Request req { path, std::string(req_body)}; - if (routes[method].find(route) != routes[method].end()) { - Handler route_fn = routes[method][route]; + if (routes[req.method].find(req.route) != routes[req.method].end()) { + Handler route_fn = routes[req.method][req.route]; if (route_fn != nullptr) { route_fn(req, res); } @@ -220,8 +236,6 @@ void HttpServer::handle_client() { "Connection: close\r\n" "\r\n" + std::string(res.body); - - // std::cout << request_buffer << "\n"; } } int bytes_sent = send(conn_fd, response.c_str(), response.size(), 0); @@ -230,7 +244,6 @@ void HttpServer::handle_client() { std::cerr << "\n\n" << strerror(errno) << ": issue sending message to connection\n"; continue; } - // std::cout << request_buffer << "\n"; close(conn_fd); } } From 788a8fb8088a672add7c0015815abdb0435f09cd Mon Sep 17 00:00:00 2001 From: James Yab Date: Wed, 17 Dec 2025 20:34:04 -0500 Subject: [PATCH 02/22] Refactor: range for-loops, add const and static class methods where possible --- include/HttpServer.h | 44 +++++++++++++++++-------------------- src/HttpServer.cpp | 48 ++++++++++++++++++++--------------------- test/HttpServerTest.cpp | 28 ++++++++++++------------ 3 files changed, 58 insertions(+), 62 deletions(-) diff --git a/include/HttpServer.h b/include/HttpServer.h index e840718..4640fde 100644 --- a/include/HttpServer.h +++ b/include/HttpServer.h @@ -14,7 +14,7 @@ class HttpServer { ~HttpServer(); - std::atomic stop_flag {}; + std::atomic stop_flag{}; struct Request { std::string_view method; @@ -28,27 +28,27 @@ class HttpServer { int status; }; - using Handler = std::function; + using Handler = std::function; static bool is_valid_request(std::string &request_buffer, ssize_t bytes_read); - void get_mapping(std::string_view route, const Handler& fn); + void get_mapping(std::string_view route, const Handler &fn); - void post_mapping(std::string_view route, const Handler& fn); + void post_mapping(std::string_view route, const Handler &fn); - void put_mapping(std::string_view route, const Handler& fn); + void put_mapping(std::string_view route, const Handler &fn); - void patch_mapping(std::string_view route, const Handler& fn); + void patch_mapping(std::string_view route, const Handler &fn); - void delete_mapping(std::string_view route, const Handler& fn); + void delete_mapping(std::string_view route, const Handler &fn); - void head_mapping(std::string_view route, const Handler& fn); + void head_mapping(std::string_view route, const Handler &fn); - void options_mapping(std::string_view route, const Handler& fn); + void options_mapping(std::string_view route, const Handler &fn); - void connect_mapping(std::string_view route, const Handler& fn); + void connect_mapping(std::string_view route, const Handler &fn); - void trace_mapping(std::string_view route, const Handler& fn); + void trace_mapping(std::string_view route, const Handler &fn); /** * Tells the server to start listening/accepting requests from a specified port. This function is blocking. @@ -71,33 +71,29 @@ class HttpServer { std::vector threads; - std::unordered_map> routes; + std::unordered_map > routes; void store_conn_fd(int conn_fd); - int listener_fd {-1}; + int listener_fd{-1}; std::string_view get_method(int conn_fd, std::string_view path, size_t &method_itr, bool &continues); - static bool parse(std::string_view buffer, Request& out); + static bool parse(std::string_view buffer, Request &out); - static bool parse_method(std::string_view buffer, std::string_view& method, size_t& offset); + static bool parse_method(std::string_view buffer, std::string_view &method, size_t &offset); - static bool parse_route(std::string_view buffer, std::string_view& route, size_t& offset); + static bool parse_route(std::string_view buffer, std::string_view &route, size_t &offset); - static bool parse_body(std::string_view buffer, std::string_view& body, const size_t& offset); + static bool parse_body(std::string_view buffer, std::string_view &body, const size_t &offset); /* * @brief return a listener socket file descriptor */ - int get_listener_socket(int port); + static int get_listener_socket(int port); /** - * @brief Should be passed into a thread() worker to send a response back to an HTTP client. - * A side-effect is that it will toggle the occupancy in the thread_pool_occupied member array - * @param thread_pool_id Tells the SendResponse function which array index to toggle in the thread_pool_occupied array - * @param conn_file_descriptor Used to send the response through the associated socket - */ + * @brief Should be passed into a thread() worker to send a response back to an HTTP client. + */ void handle_client(); - }; diff --git a/src/HttpServer.cpp b/src/HttpServer.cpp index 8082634..c1ed7b2 100644 --- a/src/HttpServer.cpp +++ b/src/HttpServer.cpp @@ -8,17 +8,17 @@ #include #include -constexpr size_t compile_time_method_hash(std::string_view method) { +constexpr size_t compile_time_method_hash(const std::string_view method) { size_t hash = 0; - for (char c : method) { + for (const char c : method) { hash += c; } return hash; } -size_t method_hash(std::string_view method) { +size_t method_hash(const std::string_view method) { size_t hash = 0; - for (char c : method) { + for (const char c : method) { hash += c; } return hash; @@ -33,8 +33,8 @@ HttpServer::HttpServer() { HttpServer::~HttpServer() { this->stop_listening(); - for (int i = 0; i < threads.size(); i++) { - threads[i].join(); + for (auto & thread : threads) { + thread.join(); // std::cout << "thread removed: " << i << "\n"; } threads.clear(); @@ -193,7 +193,7 @@ void HttpServer::handle_client() { "\r\n" + std::string(res.body); } else { - res.body = "{\"error\": \"The requested API route does not exist\"}"; + res.body = R"({"error": "The requested API route does not exist"})"; response = "HTTP/1.1 404 Not Found\r\n" "Content-Length: " + std::to_string(res.body.size()) + "\r\n" @@ -218,7 +218,7 @@ void HttpServer::handle_client() { "\r\n" + std::string(res.body); } else { - res.body = "{\"error\": \"The requested endpoint does not exist\"}"; + res.body = R"({\"error\": \"The requested endpoint does not exist\"})"; response = "HTTP/1.1 404 Not Found\r\n" "Content-Length: " + std::to_string(res.body.size()) + "\r\n" @@ -229,7 +229,7 @@ void HttpServer::handle_client() { break; } default: { - res.body = "{\"error\": \"The request does not have a valid HTTP method\"}"; + res.body = R"({\"error\": \"The request does not have a valid HTTP method\"})"; response = "HTTP/1.1 500 Error\r\n" "Content-Length: " + std::to_string(res.body.size()) + "\r\n" @@ -238,7 +238,7 @@ void HttpServer::handle_client() { std::string(res.body); } } - int bytes_sent = send(conn_fd, response.c_str(), response.size(), 0); + const ssize_t bytes_sent = send(conn_fd, response.c_str(), response.size(), 0); if (bytes_sent == -1) { close(conn_fd); std::cerr << "\n\n" << strerror(errno) << ": issue sending message to connection\n"; @@ -248,11 +248,11 @@ void HttpServer::handle_client() { } } -int HttpServer::get_listener_socket(int port) { - std::string port_str = std::to_string(port); - struct addrinfo hints {}; - struct addrinfo* addrinfo_ptr {}; - struct addrinfo* results {}; +int HttpServer::get_listener_socket(const int port) { + const std::string port_str = std::to_string(port); + addrinfo hints {}; + const addrinfo* addrinfo_ptr {}; + addrinfo* results {}; int socket_file_descriptor {}; hints.ai_family = AF_UNSPEC; // can be IPv4 or 6 @@ -303,38 +303,38 @@ int HttpServer::get_listener_socket(int port) { return socket_file_descriptor; } -void HttpServer::get_mapping(std::string_view route, const Handler& fn) { +void HttpServer::get_mapping(const std::string_view route, const Handler& fn) { routes["GET"][route] = fn; } -void HttpServer::post_mapping(std::string_view route, const Handler& fn) { +void HttpServer::post_mapping(const std::string_view route, const Handler& fn) { routes["POST"][route] = fn; } -void HttpServer::put_mapping(std::string_view route, const Handler& fn) { +void HttpServer::put_mapping(const std::string_view route, const Handler& fn) { routes["PUT"][route] = fn; } -void HttpServer::patch_mapping(std::string_view route, const Handler& fn) { +void HttpServer::patch_mapping(const std::string_view route, const Handler& fn) { routes["PATCH"][route] = fn; } -void HttpServer::delete_mapping(std::string_view route, const Handler& fn) { +void HttpServer::delete_mapping(const std::string_view route, const Handler& fn) { routes["DELETE"][route] = fn; } -void HttpServer::head_mapping(std::string_view route, const Handler& fn) { +void HttpServer::head_mapping(const std::string_view route, const Handler& fn) { routes["HEAD"][route] = fn; } -void HttpServer::options_mapping(std::string_view route, const Handler& fn) { +void HttpServer::options_mapping(const std::string_view route, const Handler& fn) { routes["OPTIONS"][route] = fn; } -void HttpServer::connect_mapping(std::string_view route, const Handler& fn) { +void HttpServer::connect_mapping(const std::string_view route, const Handler& fn) { routes["CONNECT"][route] = fn; } -void HttpServer::trace_mapping(std::string_view route, const Handler& fn) { +void HttpServer::trace_mapping(const std::string_view route, const Handler& fn) { routes["TRACE"][route] = fn; } diff --git a/test/HttpServerTest.cpp b/test/HttpServerTest.cpp index 2af9e60..23f9afe 100644 --- a/test/HttpServerTest.cpp +++ b/test/HttpServerTest.cpp @@ -39,7 +39,7 @@ TEST(HttpServerTest, AcceptsHttpRequest) { send(sock, request, strlen(request), 0); char buffer[1024]; - int bytes = recv(sock, buffer, sizeof(buffer), 0); + const ssize_t bytes = recv(sock, buffer, sizeof(buffer), 0); EXPECT_GT(bytes, 0); // Server sent response @@ -68,7 +68,7 @@ TEST(HttpServerTest, AcceptGetRequest) { send(sock, request, strlen(request), 0); char buffer[1024] {}; - int bytes = recv(sock, buffer, sizeof(buffer), 0); + const ssize_t bytes = recv(sock, buffer, sizeof(buffer), 0); std::string result = std::string(buffer); EXPECT_GT(bytes, 0); @@ -99,8 +99,8 @@ TEST(HttpServerTest, IgnoreGetReqBody) { send(sock, request, strlen(request), 0); char buffer[1024] {}; - int bytes = recv(sock, buffer, sizeof(buffer), 0); - std::string result = std::string(buffer); + const ssize_t bytes = recv(sock, buffer, sizeof(buffer), 0); + const std::string result = std::string(buffer); EXPECT_GT(bytes, 0); @@ -139,7 +139,7 @@ TEST(HttpServerTest, DoesntIgnorePostReqBody) { send(sock, request.c_str(), request.size(), 0); char buffer[1024] {}; - int bytes = recv(sock, buffer, sizeof(buffer), 0); + const ssize_t bytes = recv(sock, buffer, sizeof(buffer), 0); std::string result = std::string(buffer); EXPECT_GT(bytes, 0); @@ -192,20 +192,20 @@ TEST(HttpServerTest, AllUniqueReqMethods) { addr.sin_port = htons(HttpServerTest::port); addr.sin_addr.s_addr = inet_addr("127.0.0.1"); - const std::string methods[9] = { "GET", "POST", "PUT", "PATCH", "OPTIONS", "HEAD", "DELETE", "CONNECT", "TRACE" }; for (int i = 0; i < 9; i++) { + const std::string methods[9] = { "GET", "POST", "PUT", "PATCH", "OPTIONS", "HEAD", "DELETE", "CONNECT", "TRACE" }; std::string request = methods[i] + " /foo HTTP/1.1\r\n" - "Host: localhost\r\n" - "Connection: keep-alive\r\n" - "Content-Length: 0\r\n" - "\r\n"; + "Host: localhost\r\n" + "Connection: keep-alive\r\n" + "Content-Length: 0\r\n" + "\r\n"; int listener_fd = socket(AF_INET, SOCK_STREAM, 0); ASSERT_EQ(connect(listener_fd, reinterpret_cast(&addr), sizeof(addr)), 0); send(listener_fd, request.c_str(), request.size(), 0); char buffer[1024] {}; - int bytes = recv(listener_fd, buffer, sizeof(buffer), 0); + const ssize_t bytes = recv(listener_fd, buffer, sizeof(buffer), 0); std::string result = std::string(buffer); EXPECT_GT(bytes, 0); @@ -235,7 +235,7 @@ TEST(HttpServerTest, HandleNonExistentGetRoute) { send(listener_fd, request.c_str(), request.size(), 0); char buffer[1024] {}; - int bytes = recv(listener_fd, buffer, sizeof(buffer), 0); + const ssize_t bytes = recv(listener_fd, buffer, sizeof(buffer), 0); std::string result = std::string(buffer); EXPECT_GT(bytes, 0); @@ -268,7 +268,7 @@ TEST(HttpServerTest, HandleNonExistentPostRoute) { send(listener_fd, request.c_str(), request.size(), 0); char buffer[1024] {}; - int bytes = recv(listener_fd, buffer, sizeof(buffer), 0); + const ssize_t bytes = recv(listener_fd, buffer, sizeof(buffer), 0); std::string result = std::string(buffer); EXPECT_GT(bytes, 0); @@ -297,7 +297,7 @@ TEST(HttpServerTest, HandleNonExistentHttpMethod) { send(listener_fd, request.c_str(), request.size(), 0); char buffer[1024] {}; - int bytes = recv(listener_fd, buffer, sizeof(buffer), 0); + const ssize_t bytes = recv(listener_fd, buffer, sizeof(buffer), 0); std::string result = std::string(buffer); EXPECT_GT(bytes, 0); From c79ce8d7a89c55846451615ea504e93a5526aeac Mon Sep 17 00:00:00 2001 From: James Yab Date: Wed, 17 Dec 2025 20:43:08 -0500 Subject: [PATCH 03/22] Reformat using clang-format --- .clang-format | 4 + include/HttpServer.h | 132 ++++----- src/HttpServer.cpp | 591 +++++++++++++++++++++------------------- src/main.cpp | 28 +- test/HttpServerTest.cpp | 506 +++++++++++++++++----------------- 5 files changed, 667 insertions(+), 594 deletions(-) create mode 100644 .clang-format diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..a4bc0b4 --- /dev/null +++ b/.clang-format @@ -0,0 +1,4 @@ +BasedOnStyle: LLVM +IndentWidth: 4 +TabWidth: 4 +UseTab: Always \ No newline at end of file diff --git a/include/HttpServer.h b/include/HttpServer.h index 4640fde..254467d 100644 --- a/include/HttpServer.h +++ b/include/HttpServer.h @@ -1,99 +1,109 @@ #pragma once +#include "../include/AtomicQueue.h" +#include #include -#include -#include #include -#include #include -#include "../include/AtomicQueue.h" +#include +#include class HttpServer { -public: - HttpServer(); + public: + HttpServer(); - ~HttpServer(); + ~HttpServer(); - std::atomic stop_flag{}; + std::atomic stop_flag{}; - struct Request { - std::string_view method; - std::string_view route; - std::string_view body; - }; + struct Request { + std::string_view method; + std::string_view route; + std::string_view body; + }; - struct Response { - std::string_view header; - std::string body; - int status; - }; + struct Response { + std::string_view header; + std::string body; + int status; + }; - using Handler = std::function; + using Handler = std::function; - static bool is_valid_request(std::string &request_buffer, ssize_t bytes_read); + static bool is_valid_request(std::string &request_buffer, + ssize_t bytes_read); - void get_mapping(std::string_view route, const Handler &fn); + void get_mapping(std::string_view route, const Handler &fn); - void post_mapping(std::string_view route, const Handler &fn); + void post_mapping(std::string_view route, const Handler &fn); - void put_mapping(std::string_view route, const Handler &fn); + void put_mapping(std::string_view route, const Handler &fn); - void patch_mapping(std::string_view route, const Handler &fn); + void patch_mapping(std::string_view route, const Handler &fn); - void delete_mapping(std::string_view route, const Handler &fn); + void delete_mapping(std::string_view route, const Handler &fn); - void head_mapping(std::string_view route, const Handler &fn); + void head_mapping(std::string_view route, const Handler &fn); - void options_mapping(std::string_view route, const Handler &fn); + void options_mapping(std::string_view route, const Handler &fn); - void connect_mapping(std::string_view route, const Handler &fn); + void connect_mapping(std::string_view route, const Handler &fn); - void trace_mapping(std::string_view route, const Handler &fn); + void trace_mapping(std::string_view route, const Handler &fn); - /** - * Tells the server to start listening/accepting requests from a specified port. This function is blocking. - */ - void listen(int port); + /** + * Tells the server to start listening/accepting requests from a specified + * port. This function is blocking. + */ + void listen(int port); - /** - * Initializes a thread to start listening/accepting requests from a specified port. This function is non-blocking, - * so only use one active `listen()` or `start_listening()` method call for any given time. - */ - void start_listening(int port); + /** + * Initializes a thread to start listening/accepting requests from a + * specified port. This function is non-blocking, so only use one active + * `listen()` or `start_listening()` method call for any given time. + */ + void start_listening(int port); - /** - * Tells the server to stop listening/accepting requests. - */ - void stop_listening(); + /** + * Tells the server to stop listening/accepting requests. + */ + void stop_listening(); -private: - AtomicQueue queue; + private: + AtomicQueue queue; - std::vector threads; + std::vector threads; - std::unordered_map > routes; + std::unordered_map> + routes; - void store_conn_fd(int conn_fd); + void store_conn_fd(int conn_fd); - int listener_fd{-1}; + int listener_fd{-1}; - std::string_view get_method(int conn_fd, std::string_view path, size_t &method_itr, bool &continues); + std::string_view get_method(int conn_fd, std::string_view path, + size_t &method_itr, bool &continues); - static bool parse(std::string_view buffer, Request &out); + static bool parse(std::string_view buffer, Request &out); - static bool parse_method(std::string_view buffer, std::string_view &method, size_t &offset); + static bool parse_method(std::string_view buffer, std::string_view &method, + size_t &offset); - static bool parse_route(std::string_view buffer, std::string_view &route, size_t &offset); + static bool parse_route(std::string_view buffer, std::string_view &route, + size_t &offset); - static bool parse_body(std::string_view buffer, std::string_view &body, const size_t &offset); + static bool parse_body(std::string_view buffer, std::string_view &body, + const size_t &offset); - /* - * @brief return a listener socket file descriptor - */ - static int get_listener_socket(int port); + /* + * @brief return a listener socket file descriptor + */ + static int get_listener_socket(int port); - /** - * @brief Should be passed into a thread() worker to send a response back to an HTTP client. - */ - void handle_client(); + /** + * @brief Should be passed into a thread() worker to send a response back to + * an HTTP client. + */ + void handle_client(); }; diff --git a/src/HttpServer.cpp b/src/HttpServer.cpp index c1ed7b2..29ce00d 100644 --- a/src/HttpServer.cpp +++ b/src/HttpServer.cpp @@ -9,332 +9,375 @@ #include constexpr size_t compile_time_method_hash(const std::string_view method) { - size_t hash = 0; - for (const char c : method) { - hash += c; - } - return hash; + size_t hash = 0; + for (const char c : method) { + hash += c; + } + return hash; } size_t method_hash(const std::string_view method) { - size_t hash = 0; - for (const char c : method) { - hash += c; - } - return hash; + size_t hash = 0; + for (const char c : method) { + hash += c; + } + return hash; } HttpServer::HttpServer() { - stop_flag.store(false); - for (int i = 0; i < Constants::max_worker_count; i++) { - threads.emplace_back(&HttpServer::handle_client, this); - } + stop_flag.store(false); + for (int i = 0; i < Constants::max_worker_count; i++) { + threads.emplace_back(&HttpServer::handle_client, this); + } } HttpServer::~HttpServer() { - this->stop_listening(); - for (auto & thread : threads) { - thread.join(); - // std::cout << "thread removed: " << i << "\n"; - } - threads.clear(); + this->stop_listening(); + for (auto &thread : threads) { + thread.join(); + // std::cout << "thread removed: " << i << "\n"; + } + threads.clear(); } void HttpServer::listen(int port) { - listener_fd = get_listener_socket(port); - if (listener_fd < 0) { - throw std::runtime_error("unable to obtain listener socket, exiting\n"); - } - - std::cout << "server listening now...\n"; - while (!stop_flag.load()) { - struct sockaddr_storage incoming_addr {}; - socklen_t addr_size {sizeof(incoming_addr)}; - - const int conn_file_descriptor = accept(listener_fd, reinterpret_cast(&incoming_addr), &addr_size); - if (conn_file_descriptor == -1) { - // If we're stopping, accept failures are expected; don't spam logs. - if (stop_flag.load()) break; - - // Interrupted system call - retry. - if (errno == EINTR) continue; - - // If socket was closed or not valid, stop the loop quietly. - if (errno == EBADF || errno == EINVAL || errno == EOPNOTSUPP) break; - - // Otherwise log and continue or break as appropriate. - throw std::runtime_error("unable to obtain a valid connection file descriptor, exiting\n"); - } - this->store_conn_fd(conn_file_descriptor); - } - - if (listener_fd != -1) { - close(listener_fd); - listener_fd = -1; - } + listener_fd = get_listener_socket(port); + if (listener_fd < 0) { + throw std::runtime_error("unable to obtain listener socket, exiting\n"); + } + + std::cout << "server listening now...\n"; + while (!stop_flag.load()) { + struct sockaddr_storage incoming_addr{}; + socklen_t addr_size{sizeof(incoming_addr)}; + + const int conn_file_descriptor = accept( + listener_fd, reinterpret_cast(&incoming_addr), + &addr_size); + if (conn_file_descriptor == -1) { + // If we're stopping, accept failures are expected; don't spam logs. + if (stop_flag.load()) + break; + + // Interrupted system call - retry. + if (errno == EINTR) + continue; + + // If socket was closed or not valid, stop the loop quietly. + if (errno == EBADF || errno == EINVAL || errno == EOPNOTSUPP) + break; + + // Otherwise log and continue or break as appropriate. + throw std::runtime_error("unable to obtain a valid connection file " + "descriptor, exiting\n"); + } + this->store_conn_fd(conn_file_descriptor); + } + + if (listener_fd != -1) { + close(listener_fd); + listener_fd = -1; + } } void HttpServer::start_listening(int port) { - threads.emplace_back(&HttpServer::listen, this, port); + threads.emplace_back(&HttpServer::listen, this, port); } void HttpServer::stop_listening() { - stop_flag.store(true); + stop_flag.store(true); - if (listener_fd != -1) { - shutdown(listener_fd, SHUT_RDWR); // interrupt the accept() - close(listener_fd); - listener_fd = -1; - } + if (listener_fd != -1) { + shutdown(listener_fd, SHUT_RDWR); // interrupt the accept() + close(listener_fd); + listener_fd = -1; + } } +void HttpServer::store_conn_fd(int conn_fd) { queue.push(conn_fd); } -void HttpServer::store_conn_fd(int conn_fd) { - queue.push(conn_fd); - } - -bool HttpServer::parse(const std::string_view buffer, Request& out) { - size_t offset {0}; - if (parse_method(buffer, out.method, offset) && - parse_route(buffer, out.route, offset) && - parse_body(buffer, out.body, offset)) { - return true; - } - return false; +bool HttpServer::parse(const std::string_view buffer, Request &out) { + size_t offset{0}; + if (parse_method(buffer, out.method, offset) && + parse_route(buffer, out.route, offset) && + parse_body(buffer, out.body, offset)) { + return true; + } + return false; } -bool HttpServer::is_valid_request(std::string &request_buffer, ssize_t bytes_read) { - // Check if the request is empty - if (bytes_read <= 0) { - std::cerr << "Invalid request formatting: 0 bytes read\n"; - return false; - } - request_buffer[bytes_read] = '\0'; // Null-terminate for safety - return true; +bool HttpServer::is_valid_request(std::string &request_buffer, + ssize_t bytes_read) { + // Check if the request is empty + if (bytes_read <= 0) { + std::cerr << "Invalid request formatting: 0 bytes read\n"; + return false; + } + request_buffer[bytes_read] = '\0'; // Null-terminate for safety + return true; } -bool HttpServer::parse_method(const std::string_view buffer, std::string_view& method, size_t& offset) { - offset = buffer.find(' '); - if (offset == std::string_view::npos) { - std::cerr << "Invalid request formatting: no spaces\n"; - return false; - } - method = buffer.substr(0, offset); - offset++; - return true; +bool HttpServer::parse_method(const std::string_view buffer, + std::string_view &method, size_t &offset) { + offset = buffer.find(' '); + if (offset == std::string_view::npos) { + std::cerr << "Invalid request formatting: no spaces\n"; + return false; + } + method = buffer.substr(0, offset); + offset++; + return true; } -bool HttpServer::parse_route(const std::string_view buffer, std::string_view& route, size_t& offset) { - const size_t route_start_itr = offset; - offset = buffer.find(' ', route_start_itr); - if (offset == std::string_view::npos) { - std::cerr << "Invalid request formatting: no valid route\n"; - return false; - } - route = buffer.substr(route_start_itr, offset - route_start_itr); - offset++; - return true; +bool HttpServer::parse_route(const std::string_view buffer, + std::string_view &route, size_t &offset) { + const size_t route_start_itr = offset; + offset = buffer.find(' ', route_start_itr); + if (offset == std::string_view::npos) { + std::cerr << "Invalid request formatting: no valid route\n"; + return false; + } + route = buffer.substr(route_start_itr, offset - route_start_itr); + offset++; + return true; } -bool HttpServer::parse_body(std::string_view buffer, std::string_view& body, const size_t& offset) { - size_t body_start_itr = buffer.find("\r\n\r\n", offset); - if (body_start_itr == std::string_view::npos) { - std::cerr << "Invalid request formatting: the start of the request body is malformed\n"; - return false; - } - body_start_itr += 4; - body = buffer.substr(body_start_itr, buffer.size() - body_start_itr); - return true; +bool HttpServer::parse_body(std::string_view buffer, std::string_view &body, + const size_t &offset) { + size_t body_start_itr = buffer.find("\r\n\r\n", offset); + if (body_start_itr == std::string_view::npos) { + std::cerr << "Invalid request formatting: the start of the request " + "body is malformed\n"; + return false; + } + body_start_itr += 4; + body = buffer.substr(body_start_itr, buffer.size() - body_start_itr); + return true; } void HttpServer::handle_client() { - while (!stop_flag.load()) { - // Read the incoming HTTP request - std::string request_buffer; - request_buffer.resize(4096); - - int conn_fd {}; - - // if queue is empty - if (!queue.pop(conn_fd, stop_flag)) { - if (stop_flag.load()) return; - continue; - } - - ssize_t bytes_read = recv(conn_fd, request_buffer.data(), request_buffer.size(), 0); - if (!is_valid_request(request_buffer, bytes_read)) { - close(conn_fd); - continue; - } - - Request req {}; - if (!parse(request_buffer, req)) { - close(conn_fd); - continue; - } - - Response res {}; - std::string response {}; - switch (method_hash(req.method)) { - case compile_time_method_hash("GET"): - case compile_time_method_hash("DELETE"): - case compile_time_method_hash("HEAD"): - case compile_time_method_hash("OPTIONS"): - case compile_time_method_hash("CONNECT"): - case compile_time_method_hash("TRACE"): { - req.body = ""; - if (routes[req.method].find(req.route) != routes[req.method].end()) { - Handler route_fn = routes[req.method][req.route]; - route_fn(req, res); - response = - "HTTP/1.1 200 OK\r\n" - "Content-Length: " + std::to_string(res.body.size()) + "\r\n" - "Connection: close\r\n" - "\r\n" + - std::string(res.body); - } else { - res.body = R"({"error": "The requested API route does not exist"})"; - response = - "HTTP/1.1 404 Not Found\r\n" - "Content-Length: " + std::to_string(res.body.size()) + "\r\n" - "Connection: close\r\n" - "\r\n" + - std::string(res.body); - } - break; - } - case compile_time_method_hash("POST"): - case compile_time_method_hash("PUT"): - case compile_time_method_hash("PATCH"): { - if (routes[req.method].find(req.route) != routes[req.method].end()) { - Handler route_fn = routes[req.method][req.route]; - if (route_fn != nullptr) { - route_fn(req, res); - } - response = - "HTTP/1.1 200 OK\r\n" - "Content-Length: " + std::to_string(res.body.size()) + "\r\n" - "Connection: close\r\n" - "\r\n" + - std::string(res.body); - } else { - res.body = R"({\"error\": \"The requested endpoint does not exist\"})"; - response = - "HTTP/1.1 404 Not Found\r\n" - "Content-Length: " + std::to_string(res.body.size()) + "\r\n" - "Connection: close\r\n" - "\r\n" + - std::string(res.body); - } - break; - } - default: { - res.body = R"({\"error\": \"The request does not have a valid HTTP method\"})"; - response = - "HTTP/1.1 500 Error\r\n" - "Content-Length: " + std::to_string(res.body.size()) + "\r\n" - "Connection: close\r\n" - "\r\n" + - std::string(res.body); - } - } - const ssize_t bytes_sent = send(conn_fd, response.c_str(), response.size(), 0); - if (bytes_sent == -1) { - close(conn_fd); - std::cerr << "\n\n" << strerror(errno) << ": issue sending message to connection\n"; - continue; - } - close(conn_fd); - } + while (!stop_flag.load()) { + // Read the incoming HTTP request + std::string request_buffer; + request_buffer.resize(4096); + + int conn_fd{}; + + // if queue is empty + if (!queue.pop(conn_fd, stop_flag)) { + if (stop_flag.load()) + return; + continue; + } + + ssize_t bytes_read = + recv(conn_fd, request_buffer.data(), request_buffer.size(), 0); + if (!is_valid_request(request_buffer, bytes_read)) { + close(conn_fd); + continue; + } + + Request req{}; + if (!parse(request_buffer, req)) { + close(conn_fd); + continue; + } + + Response res{}; + std::string response{}; + switch (method_hash(req.method)) { + case compile_time_method_hash("GET"): + case compile_time_method_hash("DELETE"): + case compile_time_method_hash("HEAD"): + case compile_time_method_hash("OPTIONS"): + case compile_time_method_hash("CONNECT"): + case compile_time_method_hash("TRACE"): { + req.body = ""; + if (routes[req.method].find(req.route) != + routes[req.method].end()) { + Handler route_fn = routes[req.method][req.route]; + route_fn(req, res); + response = "HTTP/1.1 200 OK\r\n" + "Content-Length: " + + std::to_string(res.body.size()) + + "\r\n" + "Connection: close\r\n" + "\r\n" + + std::string(res.body); + } else { + res.body = + R"({"error": "The requested API route does not exist"})"; + response = "HTTP/1.1 404 Not Found\r\n" + "Content-Length: " + + std::to_string(res.body.size()) + + "\r\n" + "Connection: close\r\n" + "\r\n" + + std::string(res.body); + } + break; + } + case compile_time_method_hash("POST"): + case compile_time_method_hash("PUT"): + case compile_time_method_hash("PATCH"): { + if (routes[req.method].find(req.route) != + routes[req.method].end()) { + Handler route_fn = routes[req.method][req.route]; + if (route_fn != nullptr) { + route_fn(req, res); + } + response = "HTTP/1.1 200 OK\r\n" + "Content-Length: " + + std::to_string(res.body.size()) + + "\r\n" + "Connection: close\r\n" + "\r\n" + + std::string(res.body); + } else { + res.body = + R"({\"error\": \"The requested endpoint does not exist\"})"; + response = "HTTP/1.1 404 Not Found\r\n" + "Content-Length: " + + std::to_string(res.body.size()) + + "\r\n" + "Connection: close\r\n" + "\r\n" + + std::string(res.body); + } + break; + } + default: { + res.body = + R"({\"error\": \"The request does not have a valid HTTP method\"})"; + response = "HTTP/1.1 500 Error\r\n" + "Content-Length: " + + std::to_string(res.body.size()) + + "\r\n" + "Connection: close\r\n" + "\r\n" + + std::string(res.body); + } + } + const ssize_t bytes_sent = + send(conn_fd, response.c_str(), response.size(), 0); + if (bytes_sent == -1) { + close(conn_fd); + std::cerr << "\n\n" + << strerror(errno) + << ": issue sending message to connection\n"; + continue; + } + close(conn_fd); + } } int HttpServer::get_listener_socket(const int port) { - const std::string port_str = std::to_string(port); - addrinfo hints {}; - const addrinfo* addrinfo_ptr {}; - addrinfo* results {}; - int socket_file_descriptor {}; - - hints.ai_family = AF_UNSPEC; // can be IPv4 or 6 - hints.ai_socktype = SOCK_STREAM; // TCP stream sockets - hints.ai_flags = AI_PASSIVE; // fill in IP for us - - int status = getaddrinfo(Constants::hostname, port_str.c_str(), &hints, &results); - if (status != 0) { - throw std::runtime_error("gai error: " + std::string(gai_strerror(status))); - } - - // find the first file descriptor that does not fail - for (addrinfo_ptr = results; addrinfo_ptr != nullptr; addrinfo_ptr = addrinfo_ptr->ai_next) { - socket_file_descriptor = socket(addrinfo_ptr->ai_family, addrinfo_ptr->ai_socktype, addrinfo_ptr->ai_protocol); - if (socket_file_descriptor == -1) { - std::cerr << "\n\n" << strerror(errno) << ": issue fetching the socket file descriptor\n"; - continue; - } - - // set socket options - int yes = 1; - int sockopt_status = setsockopt(socket_file_descriptor, SOL_SOCKET,SO_REUSEADDR, &yes, sizeof(int)); - if (sockopt_status == -1) { - throw std::runtime_error(std::string(strerror(errno)) + ": issue setting socket options"); - } - - // associate the socket descriptor with the port passed into getaddrinfo() - int bind_status = bind(socket_file_descriptor, addrinfo_ptr->ai_addr, addrinfo_ptr->ai_addrlen); - if (bind_status == -1) { - std::cerr << "\n\n" << strerror(errno) << ": issue binding the socket descriptor with a port"; - continue; - } - - break; - } - - freeaddrinfo(results); - - if (addrinfo_ptr == nullptr) { - throw std::runtime_error(std::string(strerror(errno)) + ": failed to bind port to socket"); - } - - int listen_status = ::listen(socket_file_descriptor, Constants::backlog); - if (listen_status == -1) { - throw std::runtime_error(std::string(strerror(errno)) + ": issue trying to call listen()"); - } - - return socket_file_descriptor; + const std::string port_str = std::to_string(port); + addrinfo hints{}; + const addrinfo *addrinfo_ptr{}; + addrinfo *results{}; + int socket_file_descriptor{}; + + hints.ai_family = AF_UNSPEC; // can be IPv4 or 6 + hints.ai_socktype = SOCK_STREAM; // TCP stream sockets + hints.ai_flags = AI_PASSIVE; // fill in IP for us + + int status = + getaddrinfo(Constants::hostname, port_str.c_str(), &hints, &results); + if (status != 0) { + throw std::runtime_error("gai error: " + + std::string(gai_strerror(status))); + } + + // find the first file descriptor that does not fail + for (addrinfo_ptr = results; addrinfo_ptr != nullptr; + addrinfo_ptr = addrinfo_ptr->ai_next) { + socket_file_descriptor = + socket(addrinfo_ptr->ai_family, addrinfo_ptr->ai_socktype, + addrinfo_ptr->ai_protocol); + if (socket_file_descriptor == -1) { + std::cerr << "\n\n" + << strerror(errno) + << ": issue fetching the socket file descriptor\n"; + continue; + } + + // set socket options + int yes = 1; + int sockopt_status = setsockopt(socket_file_descriptor, SOL_SOCKET, + SO_REUSEADDR, &yes, sizeof(int)); + if (sockopt_status == -1) { + throw std::runtime_error(std::string(strerror(errno)) + + ": issue setting socket options"); + } + + // associate the socket descriptor with the port passed into + // getaddrinfo() + int bind_status = bind(socket_file_descriptor, addrinfo_ptr->ai_addr, + addrinfo_ptr->ai_addrlen); + if (bind_status == -1) { + std::cerr << "\n\n" + << strerror(errno) + << ": issue binding the socket descriptor with a port"; + continue; + } + + break; + } + + freeaddrinfo(results); + + if (addrinfo_ptr == nullptr) { + throw std::runtime_error(std::string(strerror(errno)) + + ": failed to bind port to socket"); + } + + int listen_status = ::listen(socket_file_descriptor, Constants::backlog); + if (listen_status == -1) { + throw std::runtime_error(std::string(strerror(errno)) + + ": issue trying to call listen()"); + } + + return socket_file_descriptor; } -void HttpServer::get_mapping(const std::string_view route, const Handler& fn) { - routes["GET"][route] = fn; +void HttpServer::get_mapping(const std::string_view route, const Handler &fn) { + routes["GET"][route] = fn; } -void HttpServer::post_mapping(const std::string_view route, const Handler& fn) { - routes["POST"][route] = fn; +void HttpServer::post_mapping(const std::string_view route, const Handler &fn) { + routes["POST"][route] = fn; } -void HttpServer::put_mapping(const std::string_view route, const Handler& fn) { - routes["PUT"][route] = fn; +void HttpServer::put_mapping(const std::string_view route, const Handler &fn) { + routes["PUT"][route] = fn; } -void HttpServer::patch_mapping(const std::string_view route, const Handler& fn) { - routes["PATCH"][route] = fn; +void HttpServer::patch_mapping(const std::string_view route, + const Handler &fn) { + routes["PATCH"][route] = fn; } -void HttpServer::delete_mapping(const std::string_view route, const Handler& fn) { - routes["DELETE"][route] = fn; +void HttpServer::delete_mapping(const std::string_view route, + const Handler &fn) { + routes["DELETE"][route] = fn; } -void HttpServer::head_mapping(const std::string_view route, const Handler& fn) { - routes["HEAD"][route] = fn; +void HttpServer::head_mapping(const std::string_view route, const Handler &fn) { + routes["HEAD"][route] = fn; } -void HttpServer::options_mapping(const std::string_view route, const Handler& fn) { - routes["OPTIONS"][route] = fn; +void HttpServer::options_mapping(const std::string_view route, + const Handler &fn) { + routes["OPTIONS"][route] = fn; } -void HttpServer::connect_mapping(const std::string_view route, const Handler& fn) { - routes["CONNECT"][route] = fn; +void HttpServer::connect_mapping(const std::string_view route, + const Handler &fn) { + routes["CONNECT"][route] = fn; } -void HttpServer::trace_mapping(const std::string_view route, const Handler& fn) { - routes["TRACE"][route] = fn; +void HttpServer::trace_mapping(const std::string_view route, + const Handler &fn) { + routes["TRACE"][route] = fn; } diff --git a/src/main.cpp b/src/main.cpp index 56589fe..1cde408 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,20 +3,22 @@ #include "../include/HttpServer.h" int main() { - HttpServer server {}; + HttpServer server{}; - server.get_mapping("/test", [](const HttpServer::Request&, HttpServer::Response& res){ - res.body = "testing new api route"; - }); + server.get_mapping( + "/test", [](const HttpServer::Request &, HttpServer::Response &res) { + res.body = "testing new api route"; + }); - server.get_mapping("/test2", [](const HttpServer::Request&, HttpServer::Response& res){ - res.body = "this is the other route"; - }); + server.get_mapping( + "/test2", [](const HttpServer::Request &, HttpServer::Response &res) { + res.body = "this is the other route"; + }); - try { - server.listen(3490); - } catch (const std::exception& err) { - std::cerr << err.what() << '\n'; - return EXIT_FAILURE; - } + try { + server.listen(3490); + } catch (const std::exception &err) { + std::cerr << err.what() << '\n'; + return EXIT_FAILURE; + } } diff --git a/test/HttpServerTest.cpp b/test/HttpServerTest.cpp index 23f9afe..b21f0f9 100644 --- a/test/HttpServerTest.cpp +++ b/test/HttpServerTest.cpp @@ -5,242 +5,252 @@ #include class HttpServerTest : public ::testing::Test { -public: - static constexpr int port {8081}; + public: + static constexpr int port{8081}; }; -TEST(ServerTest, ConstructorDestructorTest) { - HttpServer server {}; -} +TEST(ServerTest, ConstructorDestructorTest) { HttpServer server{}; } TEST(HttpServerTest, AcceptsHttpRequest) { - HttpServer server {}; - server.get_mapping("/", [](const HttpServer::Request&, HttpServer::Response& res) { - res.body = "test"; - }); - server.start_listening(HttpServerTest::port); + HttpServer server{}; + server.get_mapping("/", + [](const HttpServer::Request &, + HttpServer::Response &res) { res.body = "test"; }); + server.start_listening(HttpServerTest::port); - std::this_thread::sleep_for(std::chrono::milliseconds(1)); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); - int sock = socket(AF_INET, SOCK_STREAM, 0); + int sock = socket(AF_INET, SOCK_STREAM, 0); - sockaddr_in addr{}; - addr.sin_family = AF_INET; - addr.sin_port = htons(HttpServerTest::port); - addr.sin_addr.s_addr = inet_addr("127.0.0.1"); + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(HttpServerTest::port); + addr.sin_addr.s_addr = inet_addr("127.0.0.1"); - ASSERT_EQ(connect(sock, reinterpret_cast(&addr), sizeof(addr)), 0); + ASSERT_EQ(connect(sock, reinterpret_cast(&addr), sizeof(addr)), + 0); - const char* request = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n" - "Host: localhost\r\n" - "Connection: keep-alive\r\n" - "Content-Length: 0\r\n" - "\r\n"; - send(sock, request, strlen(request), 0); + const char *request = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n" + "Host: localhost\r\n" + "Connection: keep-alive\r\n" + "Content-Length: 0\r\n" + "\r\n"; + send(sock, request, strlen(request), 0); - char buffer[1024]; - const ssize_t bytes = recv(sock, buffer, sizeof(buffer), 0); + char buffer[1024]; + const ssize_t bytes = recv(sock, buffer, sizeof(buffer), 0); - EXPECT_GT(bytes, 0); // Server sent response + EXPECT_GT(bytes, 0); // Server sent response - close(sock); + close(sock); } TEST(HttpServerTest, AcceptGetRequest) { - HttpServer server {}; - server.get_mapping("/hello", [](const HttpServer::Request&, HttpServer::Response& res){ - res.body = "hello, world"; - }); - server.start_listening(HttpServerTest::port); + HttpServer server{}; + server.get_mapping( + "/hello", [](const HttpServer::Request &, HttpServer::Response &res) { + res.body = "hello, world"; + }); + server.start_listening(HttpServerTest::port); - std::this_thread::sleep_for(std::chrono::milliseconds(1)); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); - int sock = socket(AF_INET, SOCK_STREAM, 0); + int sock = socket(AF_INET, SOCK_STREAM, 0); - sockaddr_in addr{}; - addr.sin_family = AF_INET; - addr.sin_port = htons(HttpServerTest::port); - addr.sin_addr.s_addr = inet_addr("127.0.0.1"); + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(HttpServerTest::port); + addr.sin_addr.s_addr = inet_addr("127.0.0.1"); - ASSERT_EQ(connect(sock, reinterpret_cast(&addr), sizeof(addr)), 0); + ASSERT_EQ(connect(sock, reinterpret_cast(&addr), sizeof(addr)), + 0); - const char* request = "GET /hello HTTP/1.1\r\n\r\n"; - send(sock, request, strlen(request), 0); + const char *request = "GET /hello HTTP/1.1\r\n\r\n"; + send(sock, request, strlen(request), 0); - char buffer[1024] {}; - const ssize_t bytes = recv(sock, buffer, sizeof(buffer), 0); - std::string result = std::string(buffer); + char buffer[1024]{}; + const ssize_t bytes = recv(sock, buffer, sizeof(buffer), 0); + std::string result = std::string(buffer); - EXPECT_GT(bytes, 0); - ASSERT_TRUE(result.find("hello, world") != std::string::npos); + EXPECT_GT(bytes, 0); + ASSERT_TRUE(result.find("hello, world") != std::string::npos); - close(sock); + close(sock); } TEST(HttpServerTest, IgnoreGetReqBody) { - HttpServer server {}; - server.get_mapping("/hello", [](const HttpServer::Request& req, HttpServer::Response& res){ - res.body = req.body; - }); - server.start_listening(HttpServerTest::port); + HttpServer server{}; + server.get_mapping("/hello", + [](const HttpServer::Request &req, + HttpServer::Response &res) { res.body = req.body; }); + server.start_listening(HttpServerTest::port); - std::this_thread::sleep_for(std::chrono::milliseconds(1)); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); - int sock = socket(AF_INET, SOCK_STREAM, 0); + int sock = socket(AF_INET, SOCK_STREAM, 0); - sockaddr_in addr{}; - addr.sin_family = AF_INET; - addr.sin_port = htons(HttpServerTest::port); - addr.sin_addr.s_addr = inet_addr("127.0.0.1"); + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(HttpServerTest::port); + addr.sin_addr.s_addr = inet_addr("127.0.0.1"); - ASSERT_EQ(connect(sock, reinterpret_cast(&addr), sizeof(addr)), 0); + ASSERT_EQ(connect(sock, reinterpret_cast(&addr), sizeof(addr)), + 0); - const char* request = "GET /hello HTTP/1.1\r\n\r\nhello, world"; - send(sock, request, strlen(request), 0); + const char *request = "GET /hello HTTP/1.1\r\n\r\nhello, world"; + send(sock, request, strlen(request), 0); - char buffer[1024] {}; - const ssize_t bytes = recv(sock, buffer, sizeof(buffer), 0); - const std::string result = std::string(buffer); + char buffer[1024]{}; + const ssize_t bytes = recv(sock, buffer, sizeof(buffer), 0); + const std::string result = std::string(buffer); - EXPECT_GT(bytes, 0); + EXPECT_GT(bytes, 0); - // Should not find "hello, world" as setting the request body is ignored - ASSERT_FALSE(result.find("hello, world") != std::string::npos); + // Should not find "hello, world" as setting the request body is ignored + ASSERT_FALSE(result.find("hello, world") != std::string::npos); - close(sock); + close(sock); } TEST(HttpServerTest, DoesntIgnorePostReqBody) { - try { - HttpServer server {}; - server.post_mapping("/foo", [](const HttpServer::Request& req, HttpServer::Response& res){ - res.body = req.body; - }); - server.start_listening(HttpServerTest::port); - - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - - int sock = socket(AF_INET, SOCK_STREAM, 0); - - sockaddr_in addr{}; - addr.sin_family = AF_INET; - addr.sin_port = htons(HttpServerTest::port); - addr.sin_addr.s_addr = inet_addr("127.0.0.1"); - - ASSERT_EQ(connect(sock, reinterpret_cast(&addr), sizeof(addr)), 0); - - std::string request = "POST /foo HTTP/1.1\r\n" - "Host: localhost\r\n" - "Connection: keep-alive\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"; - - send(sock, request.c_str(), request.size(), 0); - - char buffer[1024] {}; - const ssize_t bytes = recv(sock, buffer, sizeof(buffer), 0); - std::string result = std::string(buffer); - - EXPECT_GT(bytes, 0); - ASSERT_TRUE(result.find("hello") != std::string::npos); - - close(sock); - } catch (const std::exception& e) { - FAIL() << "Exception occurred: " << e.what(); - } + try { + HttpServer server{}; + server.post_mapping( + "/foo", [](const HttpServer::Request &req, + HttpServer::Response &res) { res.body = req.body; }); + server.start_listening(HttpServerTest::port); + + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + + int sock = socket(AF_INET, SOCK_STREAM, 0); + + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(HttpServerTest::port); + addr.sin_addr.s_addr = inet_addr("127.0.0.1"); + + ASSERT_EQ( + connect(sock, reinterpret_cast(&addr), sizeof(addr)), + 0); + + std::string request = "POST /foo HTTP/1.1\r\n" + "Host: localhost\r\n" + "Connection: keep-alive\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"; + + send(sock, request.c_str(), request.size(), 0); + + char buffer[1024]{}; + const ssize_t bytes = recv(sock, buffer, sizeof(buffer), 0); + std::string result = std::string(buffer); + + EXPECT_GT(bytes, 0); + ASSERT_TRUE(result.find("hello") != std::string::npos); + + close(sock); + } catch (const std::exception &e) { + FAIL() << "Exception occurred: " << e.what(); + } } TEST(HttpServerTest, AllUniqueReqMethods) { - // this will test all different http methods with the same route name - HttpServer server {}; - - server.get_mapping("/foo", [](const HttpServer::Request& req, HttpServer::Response& res){ - res.body = "0"; - }); - server.post_mapping("/foo", [](const HttpServer::Request& req, HttpServer::Response& res){ - res.body = "1"; - }); - server.put_mapping("/foo", [](const HttpServer::Request& req, HttpServer::Response& res){ - res.body = "2"; - }); - server.patch_mapping("/foo", [](const HttpServer::Request& req, HttpServer::Response& res){ - res.body = "3"; - }); - server.options_mapping("/foo", [](const HttpServer::Request& req, HttpServer::Response& res){ - res.body = "4"; - }); - server.head_mapping("/foo", [](const HttpServer::Request& req, HttpServer::Response& res){ - res.body = "5"; - }); - server.delete_mapping("/foo", [](const HttpServer::Request& req, HttpServer::Response& res){ - res.body = "6"; - }); - server.connect_mapping("/foo", [](const HttpServer::Request& req, HttpServer::Response& res){ - res.body = "7"; - }); - server.trace_mapping("/foo", [](const HttpServer::Request& req, HttpServer::Response& res){ - res.body = "8"; - }); - - server.start_listening(HttpServerTest::port); - - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - - sockaddr_in addr{}; - addr.sin_family = AF_INET; - addr.sin_port = htons(HttpServerTest::port); - addr.sin_addr.s_addr = inet_addr("127.0.0.1"); - - for (int i = 0; i < 9; i++) { - const std::string methods[9] = { "GET", "POST", "PUT", "PATCH", "OPTIONS", "HEAD", "DELETE", "CONNECT", "TRACE" }; - std::string request = methods[i] + " /foo HTTP/1.1\r\n" - "Host: localhost\r\n" - "Connection: keep-alive\r\n" - "Content-Length: 0\r\n" - "\r\n"; - - int listener_fd = socket(AF_INET, SOCK_STREAM, 0); - ASSERT_EQ(connect(listener_fd, reinterpret_cast(&addr), sizeof(addr)), 0); - send(listener_fd, request.c_str(), request.size(), 0); - - char buffer[1024] {}; - const ssize_t bytes = recv(listener_fd, buffer, sizeof(buffer), 0); - std::string result = std::string(buffer); - - EXPECT_GT(bytes, 0); - ASSERT_TRUE(result.find(std::to_string(i)) != std::string::npos); - ASSERT_TRUE(close(listener_fd) != -1); - } + // this will test all different http methods with the same route name + HttpServer server{}; + + server.get_mapping("/foo", + [](const HttpServer::Request &req, + HttpServer::Response &res) { res.body = "0"; }); + server.post_mapping("/foo", + [](const HttpServer::Request &req, + HttpServer::Response &res) { res.body = "1"; }); + server.put_mapping("/foo", + [](const HttpServer::Request &req, + HttpServer::Response &res) { res.body = "2"; }); + server.patch_mapping("/foo", + [](const HttpServer::Request &req, + HttpServer::Response &res) { res.body = "3"; }); + server.options_mapping("/foo", + [](const HttpServer::Request &req, + HttpServer::Response &res) { res.body = "4"; }); + server.head_mapping("/foo", + [](const HttpServer::Request &req, + HttpServer::Response &res) { res.body = "5"; }); + server.delete_mapping("/foo", + [](const HttpServer::Request &req, + HttpServer::Response &res) { res.body = "6"; }); + server.connect_mapping("/foo", + [](const HttpServer::Request &req, + HttpServer::Response &res) { res.body = "7"; }); + server.trace_mapping("/foo", + [](const HttpServer::Request &req, + HttpServer::Response &res) { res.body = "8"; }); + + server.start_listening(HttpServerTest::port); + + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(HttpServerTest::port); + addr.sin_addr.s_addr = inet_addr("127.0.0.1"); + + for (int i = 0; i < 9; i++) { + const std::string methods[9] = {"GET", "POST", "PUT", + "PATCH", "OPTIONS", "HEAD", + "DELETE", "CONNECT", "TRACE"}; + std::string request = methods[i] + " /foo HTTP/1.1\r\n" + "Host: localhost\r\n" + "Connection: keep-alive\r\n" + "Content-Length: 0\r\n" + "\r\n"; + + int listener_fd = socket(AF_INET, SOCK_STREAM, 0); + ASSERT_EQ(connect(listener_fd, reinterpret_cast(&addr), + sizeof(addr)), + 0); + send(listener_fd, request.c_str(), request.size(), 0); + + char buffer[1024]{}; + const ssize_t bytes = recv(listener_fd, buffer, sizeof(buffer), 0); + std::string result = std::string(buffer); + + EXPECT_GT(bytes, 0); + ASSERT_TRUE(result.find(std::to_string(i)) != std::string::npos); + ASSERT_TRUE(close(listener_fd) != -1); + } } TEST(HttpServerTest, HandleNonExistentGetRoute) { - HttpServer server {}; - server.start_listening(HttpServerTest::port); - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - - sockaddr_in addr{}; - addr.sin_family = AF_INET; - addr.sin_port = htons(HttpServerTest::port); - addr.sin_addr.s_addr = inet_addr("127.0.0.1"); - - std::string request = "GET /foo HTTP/1.1\r\n" - "Host: localhost\r\n" - "Connection: keep-alive\r\n" - "Content-Length: 0\r\n" - "\r\n"; - - int listener_fd = socket(AF_INET, SOCK_STREAM, 0); - ASSERT_EQ(connect(listener_fd, reinterpret_cast(&addr), sizeof(addr)), 0); - send(listener_fd, request.c_str(), request.size(), 0); - - char buffer[1024] {}; - const ssize_t bytes = recv(listener_fd, buffer, sizeof(buffer), 0); - std::string result = std::string(buffer); - - EXPECT_GT(bytes, 0); - ASSERT_TRUE(result.find("404 Not Found") != std::string::npos); - ASSERT_TRUE(close(listener_fd) != -1); + HttpServer server{}; + server.start_listening(HttpServerTest::port); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(HttpServerTest::port); + addr.sin_addr.s_addr = inet_addr("127.0.0.1"); + + std::string request = "GET /foo HTTP/1.1\r\n" + "Host: localhost\r\n" + "Connection: keep-alive\r\n" + "Content-Length: 0\r\n" + "\r\n"; + + int listener_fd = socket(AF_INET, SOCK_STREAM, 0); + ASSERT_EQ( + connect(listener_fd, reinterpret_cast(&addr), sizeof(addr)), + 0); + send(listener_fd, request.c_str(), request.size(), 0); + + char buffer[1024]{}; + const ssize_t bytes = recv(listener_fd, buffer, sizeof(buffer), 0); + std::string result = std::string(buffer); + + EXPECT_GT(bytes, 0); + ASSERT_TRUE(result.find("404 Not Found") != std::string::npos); + ASSERT_TRUE(close(listener_fd) != -1); } /* @@ -248,64 +258,68 @@ TEST(HttpServerTest, HandleNonExistentGetRoute) { * because POST requests can handle the request body */ TEST(HttpServerTest, HandleNonExistentPostRoute) { - HttpServer server {}; - server.start_listening(HttpServerTest::port); - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - - sockaddr_in addr{}; - addr.sin_family = AF_INET; - addr.sin_port = htons(HttpServerTest::port); - addr.sin_addr.s_addr = inet_addr("127.0.0.1"); - - std::string request = "POST /foo HTTP/1.1\r\n" - "Host: localhost\r\n" - "Connection: keep-alive\r\n" - "Content-Length: 0\r\n" - "\r\n"; - - int listener_fd = socket(AF_INET, SOCK_STREAM, 0); - ASSERT_EQ(connect(listener_fd, reinterpret_cast(&addr), sizeof(addr)), 0); - send(listener_fd, request.c_str(), request.size(), 0); - - char buffer[1024] {}; - const ssize_t bytes = recv(listener_fd, buffer, sizeof(buffer), 0); - std::string result = std::string(buffer); - - EXPECT_GT(bytes, 0); - ASSERT_TRUE(result.find("404 Not Found") != std::string::npos); - ASSERT_TRUE(close(listener_fd) != -1); + HttpServer server{}; + server.start_listening(HttpServerTest::port); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(HttpServerTest::port); + addr.sin_addr.s_addr = inet_addr("127.0.0.1"); + + std::string request = "POST /foo HTTP/1.1\r\n" + "Host: localhost\r\n" + "Connection: keep-alive\r\n" + "Content-Length: 0\r\n" + "\r\n"; + + int listener_fd = socket(AF_INET, SOCK_STREAM, 0); + ASSERT_EQ( + connect(listener_fd, reinterpret_cast(&addr), sizeof(addr)), + 0); + send(listener_fd, request.c_str(), request.size(), 0); + + char buffer[1024]{}; + const ssize_t bytes = recv(listener_fd, buffer, sizeof(buffer), 0); + std::string result = std::string(buffer); + + EXPECT_GT(bytes, 0); + ASSERT_TRUE(result.find("404 Not Found") != std::string::npos); + ASSERT_TRUE(close(listener_fd) != -1); } TEST(HttpServerTest, HandleNonExistentHttpMethod) { - HttpServer server {}; - server.start_listening(HttpServerTest::port); - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - - sockaddr_in addr{}; - addr.sin_family = AF_INET; - addr.sin_port = htons(HttpServerTest::port); - addr.sin_addr.s_addr = inet_addr("127.0.0.1"); - - std::string request = "FOO /foo HTTP/1.1\r\n" - "Host: localhost\r\n" - "Connection: keep-alive\r\n" - "Content-Length: 0\r\n" - "\r\n"; - - int listener_fd = socket(AF_INET, SOCK_STREAM, 0); - ASSERT_EQ(connect(listener_fd, reinterpret_cast(&addr), sizeof(addr)), 0); - send(listener_fd, request.c_str(), request.size(), 0); - - char buffer[1024] {}; - const ssize_t bytes = recv(listener_fd, buffer, sizeof(buffer), 0); - std::string result = std::string(buffer); - - EXPECT_GT(bytes, 0); - ASSERT_TRUE(result.find("500 Error") != std::string::npos); - ASSERT_TRUE(close(listener_fd) != -1); + HttpServer server{}; + server.start_listening(HttpServerTest::port); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(HttpServerTest::port); + addr.sin_addr.s_addr = inet_addr("127.0.0.1"); + + std::string request = "FOO /foo HTTP/1.1\r\n" + "Host: localhost\r\n" + "Connection: keep-alive\r\n" + "Content-Length: 0\r\n" + "\r\n"; + + int listener_fd = socket(AF_INET, SOCK_STREAM, 0); + ASSERT_EQ( + connect(listener_fd, reinterpret_cast(&addr), sizeof(addr)), + 0); + send(listener_fd, request.c_str(), request.size(), 0); + + char buffer[1024]{}; + const ssize_t bytes = recv(listener_fd, buffer, sizeof(buffer), 0); + std::string result = std::string(buffer); + + EXPECT_GT(bytes, 0); + ASSERT_TRUE(result.find("500 Error") != std::string::npos); + ASSERT_TRUE(close(listener_fd) != -1); } TEST(HttpServerTest, ListenThrowsIfSocketInvalid) { - HttpServer server {}; - EXPECT_THROW(server.listen(-1), std::runtime_error); + HttpServer server{}; + EXPECT_THROW(server.listen(-1), std::runtime_error); } From 4ef4a2432197d946a5d0afa5db31540f2501e806 Mon Sep 17 00:00:00 2001 From: James Yab Date: Wed, 17 Dec 2025 21:03:52 -0500 Subject: [PATCH 04/22] Reformat AtomicQueue.h using clang-format --- include/AtomicQueue.h | 83 ++++++++++++++++++++++--------------------- 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/include/AtomicQueue.h b/include/AtomicQueue.h index 711b21c..22ff645 100644 --- a/include/AtomicQueue.h +++ b/include/AtomicQueue.h @@ -1,45 +1,46 @@ -#include +#pragma once +#include #include +#include #include -template -class AtomicQueue { -private: - std::mutex mutex; - std::condition_variable cond_var; - std::queue queue; - -public: - AtomicQueue() = default; - ~AtomicQueue() = default; - - // Push an item and notify one waiting consumer. - void push(const T& value) { - { - // lock ends when going out of scope - std::lock_guard lock(mutex); - queue.push(value); - } - cond_var.notify_one(); - } - - bool pop(T& result, const std::atomic& stop_flag) { - std::unique_lock lock(mutex); - - // when notified and if queue not empty, then proceed - // if the stop_flag is true, then it will instantly return from the pop() - while (queue.empty()) { - if (stop_flag.load()) { - return false; - } - cond_var.wait_for(lock, std::chrono::milliseconds(100)); - } - - result = queue.front(); - queue.pop(); - - // unlock out of scope - return true; - } - +template class AtomicQueue { + private: + std::mutex mutex; + std::condition_variable cond_var; + std::queue queue; + + public: + AtomicQueue() = default; + ~AtomicQueue() = default; + + // Push an item and notify one waiting consumer. + void push(const T &value) { + { + // lock ends when going out of scope + std::lock_guard lock(mutex); + queue.push(value); + } + cond_var.notify_one(); + } + + bool pop(T &result, const std::atomic &stop_flag) { + std::unique_lock lock(mutex); + + // when notified and if queue not empty, then proceed + // if the stop_flag is true, then it will instantly return from the + // pop() + while (queue.empty()) { + if (stop_flag.load()) { + return false; + } + cond_var.wait_for(lock, std::chrono::milliseconds(100)); + } + + result = queue.front(); + queue.pop(); + + // unlock out of scope + return true; + } }; From 98b8c505ffe72eaedbebc51530eda847452375c6 Mon Sep 17 00:00:00 2001 From: James Yab Date: Wed, 17 Dec 2025 23:41:14 -0500 Subject: [PATCH 05/22] Separate parsing logic into a different class --- CMakeLists.txt | 2 ++ include/HttpParser.h | 18 ++++++++++++++ include/HttpServer.h | 15 +++--------- src/HttpParser.cpp | 58 ++++++++++++++++++++++++++++++++++++++++++++ src/HttpServer.cpp | 53 +++------------------------------------- 5 files changed, 85 insertions(+), 61 deletions(-) create mode 100644 include/HttpParser.h create mode 100644 src/HttpParser.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 9bc5860..bb0bb89 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,8 @@ add_library(${LIBRARY} include/HttpServer.h include/AtomicQueue.h include/constants.h + src/HttpParser.cpp + include/HttpParser.h ) target_include_directories(${LIBRARY} PUBLIC include) target_compile_features(${LIBRARY} PRIVATE cxx_std_17) diff --git a/include/HttpParser.h b/include/HttpParser.h new file mode 100644 index 0000000..74e98f6 --- /dev/null +++ b/include/HttpParser.h @@ -0,0 +1,18 @@ +#pragma once +#include "../include/HttpServer.h" +#include + +class HttpParser { + public: + static bool parse(std::string_view buffer, HttpServer::Request &out); + + private: + static bool parse_method(std::string_view buffer, std::string_view &method, + size_t &offset); + + static bool parse_route(std::string_view buffer, std::string_view &route, + size_t &offset); + + static bool parse_body(std::string_view buffer, std::string_view &body, + const size_t &offset); +}; diff --git a/include/HttpServer.h b/include/HttpServer.h index 254467d..836ffa2 100644 --- a/include/HttpServer.h +++ b/include/HttpServer.h @@ -20,6 +20,7 @@ class HttpServer { std::string_view method; std::string_view route; std::string_view body; + std::unordered_map path_params; }; struct Response { @@ -78,6 +79,9 @@ class HttpServer { std::unordered_map> routes; + std::unordered_map> + route_path_params; + void store_conn_fd(int conn_fd); int listener_fd{-1}; @@ -85,17 +89,6 @@ class HttpServer { std::string_view get_method(int conn_fd, std::string_view path, size_t &method_itr, bool &continues); - static bool parse(std::string_view buffer, Request &out); - - static bool parse_method(std::string_view buffer, std::string_view &method, - size_t &offset); - - static bool parse_route(std::string_view buffer, std::string_view &route, - size_t &offset); - - static bool parse_body(std::string_view buffer, std::string_view &body, - const size_t &offset); - /* * @brief return a listener socket file descriptor */ diff --git a/src/HttpParser.cpp b/src/HttpParser.cpp new file mode 100644 index 0000000..9b7c4b6 --- /dev/null +++ b/src/HttpParser.cpp @@ -0,0 +1,58 @@ +// +// Created by james on 12/17/25. +// + +#include "HttpParser.h" + +#include +#include +#include + +bool HttpParser::parse(const std::string_view buffer, + HttpServer::Request &out) { + size_t offset{0}; + if (parse_method(buffer, out.method, offset) && + parse_route(buffer, out.route, offset) && + parse_body(buffer, out.body, offset)) { + return true; + } + return false; +} + +bool HttpParser::parse_method(const std::string_view buffer, + std::string_view &method, size_t &offset) { + offset = buffer.find(' '); + if (offset == std::string_view::npos) { + std::cerr << "Invalid request formatting: no spaces\n"; + return false; + } + method = buffer.substr(0, offset); + offset++; + return true; +} + +bool HttpParser::parse_route(const std::string_view buffer, + std::string_view &route, size_t &offset) { + const size_t route_start_itr = offset; + offset = buffer.find(' ', route_start_itr); + if (offset == std::string_view::npos) { + std::cerr << "Invalid request formatting: no valid route\n"; + return false; + } + route = buffer.substr(route_start_itr, offset - route_start_itr); + offset++; + return true; +} + +bool HttpParser::parse_body(std::string_view buffer, std::string_view &body, + const size_t &offset) { + size_t body_start_itr = buffer.find("\r\n\r\n", offset); + if (body_start_itr == std::string_view::npos) { + std::cerr << "Invalid request formatting: the start of the request " + "body is malformed\n"; + return false; + } + body_start_itr += 4; + body = buffer.substr(body_start_itr, buffer.size() - body_start_itr); + return true; +} diff --git a/src/HttpServer.cpp b/src/HttpServer.cpp index 29ce00d..b21399d 100644 --- a/src/HttpServer.cpp +++ b/src/HttpServer.cpp @@ -1,5 +1,7 @@ #include "../include/HttpServer.h" #include "../include/constants.h" + +#include #include #include #include @@ -96,16 +98,6 @@ void HttpServer::stop_listening() { void HttpServer::store_conn_fd(int conn_fd) { queue.push(conn_fd); } -bool HttpServer::parse(const std::string_view buffer, Request &out) { - size_t offset{0}; - if (parse_method(buffer, out.method, offset) && - parse_route(buffer, out.route, offset) && - parse_body(buffer, out.body, offset)) { - return true; - } - return false; -} - bool HttpServer::is_valid_request(std::string &request_buffer, ssize_t bytes_read) { // Check if the request is empty @@ -116,45 +108,6 @@ bool HttpServer::is_valid_request(std::string &request_buffer, request_buffer[bytes_read] = '\0'; // Null-terminate for safety return true; } - -bool HttpServer::parse_method(const std::string_view buffer, - std::string_view &method, size_t &offset) { - offset = buffer.find(' '); - if (offset == std::string_view::npos) { - std::cerr << "Invalid request formatting: no spaces\n"; - return false; - } - method = buffer.substr(0, offset); - offset++; - return true; -} - -bool HttpServer::parse_route(const std::string_view buffer, - std::string_view &route, size_t &offset) { - const size_t route_start_itr = offset; - offset = buffer.find(' ', route_start_itr); - if (offset == std::string_view::npos) { - std::cerr << "Invalid request formatting: no valid route\n"; - return false; - } - route = buffer.substr(route_start_itr, offset - route_start_itr); - offset++; - return true; -} - -bool HttpServer::parse_body(std::string_view buffer, std::string_view &body, - const size_t &offset) { - size_t body_start_itr = buffer.find("\r\n\r\n", offset); - if (body_start_itr == std::string_view::npos) { - std::cerr << "Invalid request formatting: the start of the request " - "body is malformed\n"; - return false; - } - body_start_itr += 4; - body = buffer.substr(body_start_itr, buffer.size() - body_start_itr); - return true; -} - void HttpServer::handle_client() { while (!stop_flag.load()) { // Read the incoming HTTP request @@ -178,7 +131,7 @@ void HttpServer::handle_client() { } Request req{}; - if (!parse(request_buffer, req)) { + if (!HttpParser::parse(request_buffer, req)) { close(conn_fd); continue; } From cc63d5217d9c212fd7e6be83b71458dbaaea192b Mon Sep 17 00:00:00 2001 From: James Yab Date: Mon, 22 Dec 2025 17:30:12 -0500 Subject: [PATCH 06/22] Add a split_path method --- include/HttpParser.h | 8 +++++++- src/HttpParser.cpp | 25 +++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/include/HttpParser.h b/include/HttpParser.h index 74e98f6..14ed210 100644 --- a/include/HttpParser.h +++ b/include/HttpParser.h @@ -1,11 +1,16 @@ #pragma once -#include "../include/HttpServer.h" + +#include "HttpServer.h" + #include +#include class HttpParser { public: static bool parse(std::string_view buffer, HttpServer::Request &out); + static std::vector split_path(std::string_view path); + private: static bool parse_method(std::string_view buffer, std::string_view &method, size_t &offset); @@ -15,4 +20,5 @@ class HttpParser { static bool parse_body(std::string_view buffer, std::string_view &body, const size_t &offset); + }; diff --git a/src/HttpParser.cpp b/src/HttpParser.cpp index 9b7c4b6..71aeac4 100644 --- a/src/HttpParser.cpp +++ b/src/HttpParser.cpp @@ -56,3 +56,28 @@ bool HttpParser::parse_body(std::string_view buffer, std::string_view &body, body = buffer.substr(body_start_itr, buffer.size() - body_start_itr); return true; } + +std::vector HttpParser::split_path(std::string_view path) { + std::vector segments; + size_t start = 0; + + while (start < path.size()) { + if (path[start] == '/') { + ++start; + continue; + } + + size_t end = path.find('/', start); + + // found the last segment + if (end == std::string_view::npos) { + segments.push_back(path.substr(start)); + break; + } + + segments.push_back(path.substr(start, end - start)); + start = end; + } + + return segments; +} From e767dbfd98796f1cb6c1fec3b8bb11e83865ce56 Mon Sep 17 00:00:00 2001 From: James Yab Date: Mon, 22 Dec 2025 17:30:53 -0500 Subject: [PATCH 07/22] Add testing for the HttpParser class --- CMakeLists.txt | 5 ++++- test/HttpParserTest.cpp | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 test/HttpParserTest.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index bb0bb89..815cc04 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,7 +27,10 @@ target_link_libraries(${LIBRARY} PRIVATE Threads::Threads) # unit tests executable enable_testing() -add_executable(${TEST_TARGET} test/HttpServerTest.cpp) +add_executable(${TEST_TARGET} + test/HttpServerTest.cpp + test/HttpParserTest.cpp +) target_link_libraries(${TEST_TARGET} PRIVATE ${LIBRARY} GTest::gtest_main ) diff --git a/test/HttpParserTest.cpp b/test/HttpParserTest.cpp new file mode 100644 index 0000000..4eee603 --- /dev/null +++ b/test/HttpParserTest.cpp @@ -0,0 +1,16 @@ +#include +#include "../include/HttpParser.h" + +class HttpParserTest : public ::testing::Test { + public: + static constexpr int port{8081}; +}; + +TEST(HttpParserTest, ShouldSplitPath) { + std::string path = "/foo/foo2"; + std::vector segments = HttpParser::split_path(path); + + EXPECT_EQ(segments.size(), 2); + EXPECT_EQ(segments[0], "foo"); + EXPECT_EQ(segments[1], "foo2"); +} From 5228670f0dd06cdb150a86e57987871063dd4a25 Mon Sep 17 00:00:00 2001 From: James Yab Date: Mon, 22 Dec 2025 17:32:41 -0500 Subject: [PATCH 08/22] Reformat #include directives by removing relative file paths --- include/AtomicQueue.h | 1 + include/HttpServer.h | 3 ++- src/HttpServer.cpp | 5 +++-- src/main.cpp | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/include/AtomicQueue.h b/include/AtomicQueue.h index 22ff645..320cb1a 100644 --- a/include/AtomicQueue.h +++ b/include/AtomicQueue.h @@ -1,4 +1,5 @@ #pragma once + #include #include #include diff --git a/include/HttpServer.h b/include/HttpServer.h index 836ffa2..6742afc 100644 --- a/include/HttpServer.h +++ b/include/HttpServer.h @@ -1,6 +1,7 @@ #pragma once -#include "../include/AtomicQueue.h" +#include "AtomicQueue.h" + #include #include #include diff --git a/src/HttpServer.cpp b/src/HttpServer.cpp index b21399d..32b01a1 100644 --- a/src/HttpServer.cpp +++ b/src/HttpServer.cpp @@ -1,5 +1,5 @@ -#include "../include/HttpServer.h" -#include "../include/constants.h" +#include "HttpServer.h" +#include "constants.h" #include #include @@ -108,6 +108,7 @@ bool HttpServer::is_valid_request(std::string &request_buffer, request_buffer[bytes_read] = '\0'; // Null-terminate for safety return true; } + void HttpServer::handle_client() { while (!stop_flag.load()) { // Read the incoming HTTP request diff --git a/src/main.cpp b/src/main.cpp index 1cde408..7f23215 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,6 @@ #include -#include "../include/HttpServer.h" +#include "HttpServer.h" int main() { HttpServer server{}; From 8d036f3ab63f585c27cf400b668bca2f9d490c1d Mon Sep 17 00:00:00 2001 From: James Yab Date: Thu, 25 Dec 2025 01:24:01 -0500 Subject: [PATCH 09/22] Add condition for parsing '/' route --- src/HttpParser.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/HttpParser.cpp b/src/HttpParser.cpp index 71aeac4..0387349 100644 --- a/src/HttpParser.cpp +++ b/src/HttpParser.cpp @@ -61,6 +61,12 @@ std::vector HttpParser::split_path(std::string_view path) { std::vector segments; size_t start = 0; + // If there is only the root '/' for the client request + if (path == "/") { + segments.push_back(path); + return segments; + } + while (start < path.size()) { if (path[start] == '/') { ++start; From 132bba076df728f2d781fe06084840e35c4679e9 Mon Sep 17 00:00:00 2001 From: James Yab Date: Thu, 25 Dec 2025 01:27:25 -0500 Subject: [PATCH 10/22] Implement a test for using split_path on "/" route --- test/HttpParserTest.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/HttpParserTest.cpp b/test/HttpParserTest.cpp index 4eee603..eb610a1 100644 --- a/test/HttpParserTest.cpp +++ b/test/HttpParserTest.cpp @@ -14,3 +14,11 @@ TEST(HttpParserTest, ShouldSplitPath) { EXPECT_EQ(segments[0], "foo"); EXPECT_EQ(segments[1], "foo2"); } + +TEST(HttpParserTest, ShouldHaveRootPath) { + const std::string path = "/"; + const std::vector segments = HttpParser::split_path(path); + + EXPECT_EQ(segments.size(), 1); + EXPECT_EQ(segments[0], "/"); +} From 4db1a9c9322e36b077b768b964be5c19dbb67d7f Mon Sep 17 00:00:00 2001 From: James Yab Date: Thu, 25 Dec 2025 01:29:04 -0500 Subject: [PATCH 11/22] Implement a way to match client request routes and open server routes --- include/HttpParser.h | 23 +++++++++++++++++++ src/HttpParser.cpp | 54 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/include/HttpParser.h b/include/HttpParser.h index 14ed210..3c9b887 100644 --- a/include/HttpParser.h +++ b/include/HttpParser.h @@ -7,6 +7,29 @@ class HttpParser { public: + struct RouteSegment { + enum class Type { Literal, Parameter }; + Type type; + std::string value; + }; + + struct Route { + std::vector segments; + }; + + struct PathParam { + std::string_view name; + std::string_view value; + }; + + static bool match_route( + const Route& route, + std::string_view request_path, + std::vector& out_params + ); + + static Route compile_route(std::string_view path); + static bool parse(std::string_view buffer, HttpServer::Request &out); static std::vector split_path(std::string_view path); diff --git a/src/HttpParser.cpp b/src/HttpParser.cpp index 0387349..be3654a 100644 --- a/src/HttpParser.cpp +++ b/src/HttpParser.cpp @@ -8,8 +8,60 @@ #include #include +HttpParser::Route HttpParser::compile_route(const std::string_view path) { + Route route {}; + for (std::string_view segment : split_path(path)) { + if (segment.size() > 2 && segment.front() == '{' && segment.back() == '}') { + RouteSegment route_segment = { + RouteSegment::Type::Parameter, + std::string(segment.substr(1, segment.size() - 2)) + }; + route.segments.push_back(route_segment); + } + RouteSegment route_segment = { + RouteSegment::Type::Literal, + std::string(segment) + }; + route.segments.push_back(route_segment); + } + + return route; +} + +bool HttpParser::match_route( + const Route& route, + const std::string_view request_path, + std::vector& out_params +) { + const std::vector request_segments = split_path(request_path); + + // Segment count must match + if (request_segments.size() != route.segments.size()) { + return false; + } + + // Compare segment by segment + for (size_t i = 0; i < route.segments.size(); ++i) { + const RouteSegment route_seg = route.segments[i]; + const std::string_view req_seg = request_segments[i]; + + if (route_seg.type == RouteSegment::Type::Literal) { + // Literal must match exactly + if (route_seg.value != req_seg) { + return false; + } + } else { + // Parameter always matches capture value + const PathParam path_param {route_seg.value, req_seg}; + out_params.push_back(path_param); + } + } + + return true; +} + bool HttpParser::parse(const std::string_view buffer, - HttpServer::Request &out) { + HttpServer::Request &out) { size_t offset{0}; if (parse_method(buffer, out.method, offset) && parse_route(buffer, out.route, offset) && From 62131c2077e98a679186cc1e09a82d78f1d39298 Mon Sep 17 00:00:00 2001 From: James Yab Date: Fri, 26 Dec 2025 11:07:13 -0500 Subject: [PATCH 12/22] Refactor structs into a utils header file --- include/HttpParser.h | 16 +--------------- include/HttpUtils.h | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 15 deletions(-) create mode 100644 include/HttpUtils.h diff --git a/include/HttpParser.h b/include/HttpParser.h index 3c9b887..0794e51 100644 --- a/include/HttpParser.h +++ b/include/HttpParser.h @@ -1,27 +1,13 @@ #pragma once #include "HttpServer.h" +#include "HttpUtils.h" #include #include class HttpParser { public: - struct RouteSegment { - enum class Type { Literal, Parameter }; - Type type; - std::string value; - }; - - struct Route { - std::vector segments; - }; - - struct PathParam { - std::string_view name; - std::string_view value; - }; - static bool match_route( const Route& route, std::string_view request_path, diff --git a/include/HttpUtils.h b/include/HttpUtils.h new file mode 100644 index 0000000..0c55a64 --- /dev/null +++ b/include/HttpUtils.h @@ -0,0 +1,20 @@ +#pragma once +#include +#include +#include + +struct RouteSegment { + enum class Type { Literal, Parameter }; + Type type; + std::string value; +}; + +struct Route { + std::vector segments; +}; + +struct PathParam { + std::string_view name; + std::string_view value; +}; + From ee00d053b6ee2dda802e3e2b4ad71564959d1409 Mon Sep 17 00:00:00 2001 From: James Yab Date: Sat, 27 Dec 2025 11:34:44 -0500 Subject: [PATCH 13/22] Refactor data structures into new files and namespace --- CMakeLists.txt | 7 +++++-- include/HttpServer.h | 6 ++++-- include/PathParams.h | 21 +++++++++++++++++++++ include/{HttpUtils.h => Route.h} | 13 +++++-------- src/PathParams.cpp | 18 ++++++++++++++++++ 5 files changed, 53 insertions(+), 12 deletions(-) create mode 100644 include/PathParams.h rename include/{HttpUtils.h => Route.h} (62%) create mode 100644 src/PathParams.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 815cc04..c1ed2b3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.15) -project(http_server VERSION 1.0 LANGUAGES CXX) +project(http_server VERSION 1.0 LANGUAGES C CXX) set(LIBRARY "server_library") set(TEST_TARGET "server_tests_bin") @@ -13,11 +13,14 @@ find_package(Threads REQUIRED) # server library add_library(${LIBRARY} src/HttpServer.cpp + src/HttpParser.cpp + src/PathParams.cpp include/HttpServer.h include/AtomicQueue.h include/constants.h - src/HttpParser.cpp include/HttpParser.h + include/PathParams.h + include/Route.h ) target_include_directories(${LIBRARY} PUBLIC include) target_compile_features(${LIBRARY} PRIVATE cxx_std_17) diff --git a/include/HttpServer.h b/include/HttpServer.h index 6742afc..a03793f 100644 --- a/include/HttpServer.h +++ b/include/HttpServer.h @@ -1,6 +1,8 @@ #pragma once #include "AtomicQueue.h" +#include "PathParams.h" +#include "Route.h" #include #include @@ -21,7 +23,7 @@ class HttpServer { std::string_view method; std::string_view route; std::string_view body; - std::unordered_map path_params; + PathParams path_params; }; struct Response { @@ -77,7 +79,7 @@ class HttpServer { std::vector threads; std::unordered_map> + std::vector>> routes; std::unordered_map> diff --git a/include/PathParams.h b/include/PathParams.h new file mode 100644 index 0000000..231326f --- /dev/null +++ b/include/PathParams.h @@ -0,0 +1,21 @@ +// +// Created by james on 12/26/25. +// + +#pragma once + +#include +#include +#include + + +class PathParams { +public: + std::optional get_path_param(std::string_view key); + + void add_param(std::string_view key, std::string_view value); + +private: + std::unordered_map path_params; + +}; \ No newline at end of file diff --git a/include/HttpUtils.h b/include/Route.h similarity index 62% rename from include/HttpUtils.h rename to include/Route.h index 0c55a64..1e9e647 100644 --- a/include/HttpUtils.h +++ b/include/Route.h @@ -1,10 +1,12 @@ #pragma once + #include -#include -#include + +namespace HttpUtils { struct RouteSegment { enum class Type { Literal, Parameter }; + Type type; std::string value; }; @@ -12,9 +14,4 @@ struct RouteSegment { struct Route { std::vector segments; }; - -struct PathParam { - std::string_view name; - std::string_view value; -}; - +} \ No newline at end of file diff --git a/src/PathParams.cpp b/src/PathParams.cpp new file mode 100644 index 0000000..8a34238 --- /dev/null +++ b/src/PathParams.cpp @@ -0,0 +1,18 @@ +// +// Created by james on 12/26/25. +// + +#include "PathParams.h" + +std::optional PathParams::get_path_param( + const std::string_view key) { + if (path_params.find(key) != path_params.end()) { + return path_params[key]; + } + return std::nullopt; +} + +void PathParams::add_param(std::string_view key, + std::string_view value) { + path_params[key] = value; +} \ No newline at end of file From 18c6ab67a5de2ba3b4a9d8b88c02fda964556fa0 Mon Sep 17 00:00:00 2001 From: James Yab Date: Sun, 28 Dec 2025 01:08:32 -0500 Subject: [PATCH 14/22] Implement a PathParams data structure --- include/PathParams.h | 10 +++++++++- src/PathParams.cpp | 10 +++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/include/PathParams.h b/include/PathParams.h index 231326f..02e2f55 100644 --- a/include/PathParams.h +++ b/include/PathParams.h @@ -6,16 +6,24 @@ #include #include +#include #include class PathParams { public: + explicit PathParams( + const std::unordered_map& path_params) + : path_params(path_params) { + } + + PathParams() = default; + std::optional get_path_param(std::string_view key); void add_param(std::string_view key, std::string_view value); private: - std::unordered_map path_params; + std::unordered_map path_params; }; \ No newline at end of file diff --git a/src/PathParams.cpp b/src/PathParams.cpp index 8a34238..cc53d01 100644 --- a/src/PathParams.cpp +++ b/src/PathParams.cpp @@ -4,15 +4,19 @@ #include "PathParams.h" +#include +#include + std::optional PathParams::get_path_param( const std::string_view key) { - if (path_params.find(key) != path_params.end()) { - return path_params[key]; + std::string skey{key}; + if (path_params.find(std::string{key}) != path_params.end()) { + return path_params[skey]; } return std::nullopt; } void PathParams::add_param(std::string_view key, std::string_view value) { - path_params[key] = value; + path_params[std::string{key}] = value; } \ No newline at end of file From e6a906d363bef485c823a6513f1abf8357b1e08d Mon Sep 17 00:00:00 2001 From: James Yab Date: Sun, 28 Dec 2025 11:02:53 -0500 Subject: [PATCH 15/22] Refactor match_route to only modify the request path params if there is a match --- src/HttpParser.cpp | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/HttpParser.cpp b/src/HttpParser.cpp index be3654a..18925d5 100644 --- a/src/HttpParser.cpp +++ b/src/HttpParser.cpp @@ -28,12 +28,14 @@ HttpParser::Route HttpParser::compile_route(const std::string_view path) { return route; } -bool HttpParser::match_route( - const Route& route, - const std::string_view request_path, - std::vector& out_params -) { - const std::vector request_segments = split_path(request_path); +/** + * + * @param route + * @param request request.path_params will be modified by match_route + * @return + */ +bool HttpParser::match_route(const HttpUtils::Route& route, HttpServer::Request& request) { + const std::vector request_segments = split_path(request.route); // Segment count must match if (request_segments.size() != route.segments.size()) { @@ -42,20 +44,23 @@ bool HttpParser::match_route( // Compare segment by segment for (size_t i = 0; i < route.segments.size(); ++i) { - const RouteSegment route_seg = route.segments[i]; - const std::string_view req_seg = request_segments[i]; + const HttpUtils::RouteSegment route_seg = route.segments[i]; + const std::string_view req_seg = request_segments[i]; - if (route_seg.type == RouteSegment::Type::Literal) { + if (route_seg.type == HttpUtils::RouteSegment::Type::Literal) { // Literal must match exactly if (route_seg.value != req_seg) { return false; } - } else { - // Parameter always matches capture value - const PathParam path_param {route_seg.value, req_seg}; - out_params.push_back(path_param); } } + for (size_t i = 0; i < route.segments.size(); ++i) { + if (route.segments[i].type == HttpUtils::RouteSegment::Type::Literal) continue; + + const HttpUtils::RouteSegment route_seg = route.segments[i]; + const std::string_view req_seg = request_segments[i]; + request.path_params.add_param(route_seg.value, req_seg); + } return true; } From faad4b129c3bf48ef360c8c6ff41dab0ebd91cff Mon Sep 17 00:00:00 2001 From: James Yab Date: Sun, 28 Dec 2025 11:04:34 -0500 Subject: [PATCH 16/22] Refactor Route struct to a different file --- include/HttpParser.h | 9 ++++----- src/HttpParser.cpp | 19 ++++++++++--------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/include/HttpParser.h b/include/HttpParser.h index 0794e51..d2bd34e 100644 --- a/include/HttpParser.h +++ b/include/HttpParser.h @@ -1,7 +1,7 @@ #pragma once #include "HttpServer.h" -#include "HttpUtils.h" +#include "Route.h" #include #include @@ -9,12 +9,11 @@ class HttpParser { public: static bool match_route( - const Route& route, - std::string_view request_path, - std::vector& out_params + const HttpUtils::Route& route, + HttpServer::Request& request ); - static Route compile_route(std::string_view path); + static HttpUtils::Route path_to_route(std::string_view path); static bool parse(std::string_view buffer, HttpServer::Request &out); diff --git a/src/HttpParser.cpp b/src/HttpParser.cpp index 18925d5..c9bfc7f 100644 --- a/src/HttpParser.cpp +++ b/src/HttpParser.cpp @@ -8,21 +8,22 @@ #include #include -HttpParser::Route HttpParser::compile_route(const std::string_view path) { - Route route {}; +HttpUtils::Route HttpParser::path_to_route(const std::string_view path) { + HttpUtils::Route route {}; for (std::string_view segment : split_path(path)) { if (segment.size() > 2 && segment.front() == '{' && segment.back() == '}') { - RouteSegment route_segment = { - RouteSegment::Type::Parameter, + HttpUtils::RouteSegment route_segment = { + HttpUtils::RouteSegment::Type::Parameter, std::string(segment.substr(1, segment.size() - 2)) }; route.segments.push_back(route_segment); + } else { + HttpUtils::RouteSegment route_segment = { + HttpUtils::RouteSegment::Type::Literal, + std::string(segment) + }; + route.segments.push_back(route_segment); } - RouteSegment route_segment = { - RouteSegment::Type::Literal, - std::string(segment) - }; - route.segments.push_back(route_segment); } return route; From 4e70a7a64aee5dbda6613c3bc79b6c8152dbbf2b Mon Sep 17 00:00:00 2001 From: James Yab Date: Sun, 28 Dec 2025 11:05:57 -0500 Subject: [PATCH 17/22] Refactor handle_client to use the new route and path params matching implementation --- include/HttpServer.h | 5 +- src/HttpServer.cpp | 110 ++++++++++++++++++++++--------------------- 2 files changed, 61 insertions(+), 54 deletions(-) diff --git a/include/HttpServer.h b/include/HttpServer.h index a03793f..ed50890 100644 --- a/include/HttpServer.h +++ b/include/HttpServer.h @@ -32,7 +32,7 @@ class HttpServer { int status; }; - using Handler = std::function; + using Handler = std::function; static bool is_valid_request(std::string &request_buffer, ssize_t bytes_read); @@ -82,6 +82,9 @@ class HttpServer { std::vector>> routes; + // + // std::unordered_map> routes; + std::unordered_map> route_path_params; diff --git a/src/HttpServer.cpp b/src/HttpServer.cpp index 32b01a1..28bf08b 100644 --- a/src/HttpServer.cpp +++ b/src/HttpServer.cpp @@ -37,7 +37,6 @@ HttpServer::~HttpServer() { this->stop_listening(); for (auto &thread : threads) { thread.join(); - // std::cout << "thread removed: " << i << "\n"; } threads.clear(); } @@ -147,57 +146,62 @@ void HttpServer::handle_client() { case compile_time_method_hash("CONNECT"): case compile_time_method_hash("TRACE"): { req.body = ""; - if (routes[req.method].find(req.route) != - routes[req.method].end()) { - Handler route_fn = routes[req.method][req.route]; - route_fn(req, res); - response = "HTTP/1.1 200 OK\r\n" - "Content-Length: " + - std::to_string(res.body.size()) + - "\r\n" - "Connection: close\r\n" - "\r\n" + - std::string(res.body); - } else { - res.body = - R"({"error": "The requested API route does not exist"})"; - response = "HTTP/1.1 404 Not Found\r\n" - "Content-Length: " + - std::to_string(res.body.size()) + - "\r\n" - "Connection: close\r\n" - "\r\n" + - std::string(res.body); + bool is_ok = false; + for (const auto& [route, route_fn] : routes[req.method]) { + if (HttpParser::match_route(route, req)) { + // when the route is matched, fn should run and allow fn definitions to use pathParams + is_ok = true; + route_fn(req, res); + response = "HTTP/1.1 200 OK\r\n" + "Content-Length: " + + std::to_string(res.body.size()) + + "\r\n" + "Connection: close\r\n" + "\r\n" + + std::string(res.body); + break; + } } - break; + if (is_ok) break; + res.body = + R"({"error": "The requested endpoint does not exist"})"; + response = "HTTP/1.1 404 Not Found\r\n" + "Content-Length: " + + std::to_string(res.body.size()) + + "\r\n" + "Connection: close\r\n" + "\r\n" + + std::string(res.body); } case compile_time_method_hash("POST"): case compile_time_method_hash("PUT"): case compile_time_method_hash("PATCH"): { - if (routes[req.method].find(req.route) != - routes[req.method].end()) { - Handler route_fn = routes[req.method][req.route]; - if (route_fn != nullptr) { + bool is_ok = false; + for (const auto& [route, route_fn] : routes[req.method]) { + if (HttpParser::match_route(route, req)) { + // when the route is matched, fn should run and allow fn definitions to use pathParams + is_ok = true; route_fn(req, res); + response = "HTTP/1.1 200 OK\r\n" + "Content-Length: " + + std::to_string(res.body.size()) + + "\r\n" + "Connection: close\r\n" + "\r\n" + + std::string(res.body); + break; } - response = "HTTP/1.1 200 OK\r\n" - "Content-Length: " + - std::to_string(res.body.size()) + - "\r\n" - "Connection: close\r\n" - "\r\n" + - std::string(res.body); - } else { - res.body = - R"({\"error\": \"The requested endpoint does not exist\"})"; - response = "HTTP/1.1 404 Not Found\r\n" - "Content-Length: " + - std::to_string(res.body.size()) + - "\r\n" - "Connection: close\r\n" - "\r\n" + - std::string(res.body); } + if (is_ok) break; + res.body = + R"({"error": "The requested endpoint does not exist"})"; + response = "HTTP/1.1 404 Not Found\r\n" + "Content-Length: " + + std::to_string(res.body.size()) + + "\r\n" + "Connection: close\r\n" + "\r\n" + + std::string(res.body); break; } default: { @@ -296,42 +300,42 @@ int HttpServer::get_listener_socket(const int port) { } void HttpServer::get_mapping(const std::string_view route, const Handler &fn) { - routes["GET"][route] = fn; + routes["GET"].emplace_back(HttpParser::path_to_route(route), fn); } void HttpServer::post_mapping(const std::string_view route, const Handler &fn) { - routes["POST"][route] = fn; + routes["POST"].emplace_back(HttpParser::path_to_route(route), fn); } void HttpServer::put_mapping(const std::string_view route, const Handler &fn) { - routes["PUT"][route] = fn; + routes["PUT"].emplace_back(HttpParser::path_to_route(route), fn); } void HttpServer::patch_mapping(const std::string_view route, const Handler &fn) { - routes["PATCH"][route] = fn; + routes["PATCH"].emplace_back(HttpParser::path_to_route(route), fn); } void HttpServer::delete_mapping(const std::string_view route, const Handler &fn) { - routes["DELETE"][route] = fn; + routes["DELETE"].emplace_back(HttpParser::path_to_route(route), fn); } void HttpServer::head_mapping(const std::string_view route, const Handler &fn) { - routes["HEAD"][route] = fn; + routes["HEAD"].emplace_back(HttpParser::path_to_route(route), fn); } void HttpServer::options_mapping(const std::string_view route, const Handler &fn) { - routes["OPTIONS"][route] = fn; + routes["OPTIONS"].emplace_back(HttpParser::path_to_route(route), fn); } void HttpServer::connect_mapping(const std::string_view route, const Handler &fn) { - routes["CONNECT"][route] = fn; + routes["CONNECT"].emplace_back(HttpParser::path_to_route(route), fn); } void HttpServer::trace_mapping(const std::string_view route, const Handler &fn) { - routes["TRACE"][route] = fn; + routes["TRACE"].emplace_back(HttpParser::path_to_route(route), fn); } From 9a1f7662cc0270db17be11756808bb29ebc8e46b Mon Sep 17 00:00:00 2001 From: James Yab Date: Sun, 28 Dec 2025 11:06:48 -0500 Subject: [PATCH 18/22] Add testing for path parameters --- test/HttpParserTest.cpp | 51 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/test/HttpParserTest.cpp b/test/HttpParserTest.cpp index eb610a1..f1ee368 100644 --- a/test/HttpParserTest.cpp +++ b/test/HttpParserTest.cpp @@ -1,6 +1,10 @@ #include #include "../include/HttpParser.h" +#include +#include +#include + class HttpParserTest : public ::testing::Test { public: static constexpr int port{8081}; @@ -22,3 +26,50 @@ TEST(HttpParserTest, ShouldHaveRootPath) { EXPECT_EQ(segments.size(), 1); EXPECT_EQ(segments[0], "/"); } + +TEST(HttpParserTest, ShouldHavePathParameters) { + try { + HttpServer server{}; + server.get_mapping( + "/foo/{id}", [](HttpServer::Request &req, + HttpServer::Response &res) { + res.body = req.path_params.get_path_param("id").value(); + } + ); + + server.start_listening(8081); + + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + + int sock = socket(AF_INET, SOCK_STREAM, 0); + + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(8081); + addr.sin_addr.s_addr = inet_addr("127.0.0.1"); + + ASSERT_EQ( + connect(sock, reinterpret_cast(&addr), sizeof(addr)), + 0); + + const std::string request = "GET /foo/123 HTTP/1.1\r\n" + "Host: localhost\r\n" + "Connection: keep-alive\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"; + + send(sock, request.c_str(), request.size(), 0); + + char buffer[1024]{}; + const ssize_t bytes = recv(sock, buffer, sizeof(buffer), 0); + const std::string result = std::string(buffer); + + EXPECT_GT(bytes, 0); + ASSERT_TRUE(result.find("123") != std::string::npos); + + close(sock); + } catch (const std::exception &e) { + FAIL() << "Exception occurred: " << e.what(); + } +} From 0abeb41e1367b488008b489c8ea54b79a44d26d8 Mon Sep 17 00:00:00 2001 From: James Yab Date: Sun, 28 Dec 2025 11:31:47 -0500 Subject: [PATCH 19/22] Update path parameter test --- test/HttpParserTest.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/HttpParserTest.cpp b/test/HttpParserTest.cpp index f1ee368..eaaa496 100644 --- a/test/HttpParserTest.cpp +++ b/test/HttpParserTest.cpp @@ -31,9 +31,12 @@ TEST(HttpParserTest, ShouldHavePathParameters) { try { HttpServer server{}; server.get_mapping( - "/foo/{id}", [](HttpServer::Request &req, + "/foo/{id}/{user}", [](HttpServer::Request &req, HttpServer::Response &res) { - res.body = req.path_params.get_path_param("id").value(); + std::stringstream ss; + ss << "id: " << req.path_params.get_path_param("id").value() << "\n"; + ss << "user: " << req.path_params.get_path_param("user").value() << "\n"; + res.body = ss.str(); } ); @@ -52,7 +55,7 @@ TEST(HttpParserTest, ShouldHavePathParameters) { connect(sock, reinterpret_cast(&addr), sizeof(addr)), 0); - const std::string request = "GET /foo/123 HTTP/1.1\r\n" + const std::string request = "GET /foo/123/james HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: keep-alive\r\n" "Content-Length: 5\r\n" @@ -66,7 +69,8 @@ TEST(HttpParserTest, ShouldHavePathParameters) { const std::string result = std::string(buffer); EXPECT_GT(bytes, 0); - ASSERT_TRUE(result.find("123") != std::string::npos); + EXPECT_TRUE(result.find("id: 123") != std::string::npos); + EXPECT_TRUE(result.find("user: james") != std::string::npos); close(sock); } catch (const std::exception &e) { From cf9df2b4d3b129bfa93113196ed39bc91291ddb8 Mon Sep 17 00:00:00 2001 From: James Yab Date: Sun, 28 Dec 2025 11:37:46 -0500 Subject: [PATCH 20/22] Add a test to handle no path parameters --- test/HttpParserTest.cpp | 55 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/test/HttpParserTest.cpp b/test/HttpParserTest.cpp index eaaa496..2680d3c 100644 --- a/test/HttpParserTest.cpp +++ b/test/HttpParserTest.cpp @@ -77,3 +77,58 @@ TEST(HttpParserTest, ShouldHavePathParameters) { FAIL() << "Exception occurred: " << e.what(); } } + +TEST(HttpParserTest, ShouldHandleNoPathParameters) { + try { + HttpServer server{}; + server.get_mapping( + "/foo/{id}/{user}", [](HttpServer::Request &req, + HttpServer::Response &res) { + std::stringstream ss; + try { + ss << req.path_params.get_path_param("foo").value() << "\n"; + res.body = ss.str(); + } catch (const std::bad_optional_access &e) { + res.body = "could not get path parameter foo"; + } + } + ); + + server.start_listening(8081); + + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + + int sock = socket(AF_INET, SOCK_STREAM, 0); + + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(8081); + addr.sin_addr.s_addr = inet_addr("127.0.0.1"); + + ASSERT_EQ( + connect(sock, reinterpret_cast(&addr), sizeof(addr)), + 0); + + const std::string request = "GET /foo/123/james HTTP/1.1\r\n" + "Host: localhost\r\n" + "Connection: keep-alive\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"; + + send(sock, request.c_str(), request.size(), 0); + + char buffer[1024]{}; + const ssize_t bytes = recv(sock, buffer, sizeof(buffer), 0); + const std::string result = std::string(buffer); + + EXPECT_GT(bytes, 0); + EXPECT_FALSE(result.find("id: 123") != std::string::npos); + EXPECT_FALSE(result.find("user: james") != std::string::npos); + EXPECT_TRUE(result.find("could not get path parameter foo") != std::string::npos); + + close(sock); + } catch (const std::exception &e) { + FAIL() << "Exception occurred: " << e.what(); + } +} From 5ad24bd9fed170a7fb39cba8e18237caa61590c8 Mon Sep 17 00:00:00 2001 From: James Yab Date: Sun, 28 Dec 2025 14:30:25 -0500 Subject: [PATCH 21/22] Add a function to the Response struct to convert it into a string --- include/HttpServer.h | 14 ++++++++-- src/HttpServer.cpp | 64 +++++++++++--------------------------------- 2 files changed, 28 insertions(+), 50 deletions(-) diff --git a/include/HttpServer.h b/include/HttpServer.h index ed50890..356aa6b 100644 --- a/include/HttpServer.h +++ b/include/HttpServer.h @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -20,16 +21,25 @@ class HttpServer { std::atomic stop_flag{}; struct Request { + PathParams path_params; std::string_view method; std::string_view route; std::string_view body; - PathParams path_params; }; struct Response { std::string_view header; std::string body; - int status; + std::string status_name; + std::string str() { + std::stringstream ss; + ss << "HTTP/1.1 " << status << " " << status_name << " \r\n"; + ss << "Content-Length: " << body.size() << "\r\n"; + ss << "Connection: close\r\n\r\n"; + ss << body; + return ss.str(); + } + int status; // e.g. 200, 404, 500 }; using Handler = std::function; diff --git a/src/HttpServer.cpp b/src/HttpServer.cpp index 28bf08b..7ac2cfa 100644 --- a/src/HttpServer.cpp +++ b/src/HttpServer.cpp @@ -146,78 +146,46 @@ void HttpServer::handle_client() { case compile_time_method_hash("CONNECT"): case compile_time_method_hash("TRACE"): { req.body = ""; - bool is_ok = false; for (const auto& [route, route_fn] : routes[req.method]) { if (HttpParser::match_route(route, req)) { // when the route is matched, fn should run and allow fn definitions to use pathParams - is_ok = true; + res.status = 200; + res.status_name = "OK"; route_fn(req, res); - response = "HTTP/1.1 200 OK\r\n" - "Content-Length: " + - std::to_string(res.body.size()) + - "\r\n" - "Connection: close\r\n" - "\r\n" + - std::string(res.body); break; } } - if (is_ok) break; - res.body = - R"({"error": "The requested endpoint does not exist"})"; - response = "HTTP/1.1 404 Not Found\r\n" - "Content-Length: " + - std::to_string(res.body.size()) + - "\r\n" - "Connection: close\r\n" - "\r\n" + - std::string(res.body); + if (res.status == 200) break; + res.status = 404; + res.status_name = "Not Found"; + res.body = R"({"error": "The requested endpoint does not exist"})"; } case compile_time_method_hash("POST"): case compile_time_method_hash("PUT"): case compile_time_method_hash("PATCH"): { - bool is_ok = false; for (const auto& [route, route_fn] : routes[req.method]) { if (HttpParser::match_route(route, req)) { // when the route is matched, fn should run and allow fn definitions to use pathParams - is_ok = true; + res.status = 200; + res.status_name = "OK"; route_fn(req, res); - response = "HTTP/1.1 200 OK\r\n" - "Content-Length: " + - std::to_string(res.body.size()) + - "\r\n" - "Connection: close\r\n" - "\r\n" + - std::string(res.body); break; } } - if (is_ok) break; - res.body = - R"({"error": "The requested endpoint does not exist"})"; - response = "HTTP/1.1 404 Not Found\r\n" - "Content-Length: " + - std::to_string(res.body.size()) + - "\r\n" - "Connection: close\r\n" - "\r\n" + - std::string(res.body); + if (res.status == 200) break; + res.status = 404; + res.status_name = "Not Found"; + res.body = R"({"error": "The requested endpoint does not exist"})"; break; } default: { - res.body = - R"({\"error\": \"The request does not have a valid HTTP method\"})"; - response = "HTTP/1.1 500 Error\r\n" - "Content-Length: " + - std::to_string(res.body.size()) + - "\r\n" - "Connection: close\r\n" - "\r\n" + - std::string(res.body); + res.status = 500; + res.status_name = "Error"; + res.body = R"({\"error\": \"The request does not have a valid HTTP method\"})"; } } const ssize_t bytes_sent = - send(conn_fd, response.c_str(), response.size(), 0); + send(conn_fd, res.str().c_str(), res.str().size(), 0); if (bytes_sent == -1) { close(conn_fd); std::cerr << "\n\n" From 9608887d9b42017a66e3344001471bf386420a8f Mon Sep 17 00:00:00 2001 From: James Yab Date: Sun, 28 Dec 2025 14:33:31 -0500 Subject: [PATCH 22/22] Clang reformat --- src/HttpParser.cpp | 62 +++++++++--------- src/HttpServer.cpp | 61 ++++++++--------- test/HttpParserTest.cpp | 52 ++++++++------- test/HttpServerTest.cpp | 140 +++++++++++++++++++++++----------------- 4 files changed, 175 insertions(+), 140 deletions(-) diff --git a/src/HttpParser.cpp b/src/HttpParser.cpp index c9bfc7f..5680509 100644 --- a/src/HttpParser.cpp +++ b/src/HttpParser.cpp @@ -9,9 +9,10 @@ #include HttpUtils::Route HttpParser::path_to_route(const std::string_view path) { - HttpUtils::Route route {}; + HttpUtils::Route route{}; for (std::string_view segment : split_path(path)) { - if (segment.size() > 2 && segment.front() == '{' && segment.back() == '}') { + if (segment.size() > 2 && segment.front() == '{' && segment.back() == + '}') { HttpUtils::RouteSegment route_segment = { HttpUtils::RouteSegment::Type::Parameter, std::string(segment.substr(1, segment.size() - 2)) @@ -35,8 +36,10 @@ HttpUtils::Route HttpParser::path_to_route(const std::string_view path) { * @param request request.path_params will be modified by match_route * @return */ -bool HttpParser::match_route(const HttpUtils::Route& route, HttpServer::Request& request) { - const std::vector request_segments = split_path(request.route); +bool HttpParser::match_route(const HttpUtils::Route &route, + HttpServer::Request &request) { + const std::vector request_segments = split_path( + request.route); // Segment count must match if (request_segments.size() != route.segments.size()) { @@ -56,7 +59,8 @@ bool HttpParser::match_route(const HttpUtils::Route& route, HttpServer::Request& } } for (size_t i = 0; i < route.segments.size(); ++i) { - if (route.segments[i].type == HttpUtils::RouteSegment::Type::Literal) continue; + if (route.segments[i].type == HttpUtils::RouteSegment::Type::Literal) + continue; const HttpUtils::RouteSegment route_seg = route.segments[i]; const std::string_view req_seg = request_segments[i]; @@ -70,15 +74,15 @@ bool HttpParser::parse(const std::string_view buffer, HttpServer::Request &out) { size_t offset{0}; if (parse_method(buffer, out.method, offset) && - parse_route(buffer, out.route, offset) && - parse_body(buffer, out.body, offset)) { + parse_route(buffer, out.route, offset) && + parse_body(buffer, out.body, offset)) { return true; } return false; } bool HttpParser::parse_method(const std::string_view buffer, - std::string_view &method, size_t &offset) { + std::string_view &method, size_t &offset) { offset = buffer.find(' '); if (offset == std::string_view::npos) { std::cerr << "Invalid request formatting: no spaces\n"; @@ -90,7 +94,7 @@ bool HttpParser::parse_method(const std::string_view buffer, } bool HttpParser::parse_route(const std::string_view buffer, - std::string_view &route, size_t &offset) { + std::string_view &route, size_t &offset) { const size_t route_start_itr = offset; offset = buffer.find(' ', route_start_itr); if (offset == std::string_view::npos) { @@ -103,11 +107,11 @@ bool HttpParser::parse_route(const std::string_view buffer, } bool HttpParser::parse_body(std::string_view buffer, std::string_view &body, - const size_t &offset) { + const size_t &offset) { size_t body_start_itr = buffer.find("\r\n\r\n", offset); if (body_start_itr == std::string_view::npos) { std::cerr << "Invalid request formatting: the start of the request " - "body is malformed\n"; + "body is malformed\n"; return false; } body_start_itr += 4; @@ -116,8 +120,8 @@ bool HttpParser::parse_body(std::string_view buffer, std::string_view &body, } std::vector HttpParser::split_path(std::string_view path) { - std::vector segments; - size_t start = 0; + std::vector segments; + size_t start = 0; // If there is only the root '/' for the client request if (path == "/") { @@ -125,23 +129,23 @@ std::vector HttpParser::split_path(std::string_view path) { return segments; } - while (start < path.size()) { - if (path[start] == '/') { - ++start; - continue; - } + while (start < path.size()) { + if (path[start] == '/') { + ++start; + continue; + } - size_t end = path.find('/', start); + size_t end = path.find('/', start); - // found the last segment - if (end == std::string_view::npos) { - segments.push_back(path.substr(start)); - break; - } + // found the last segment + if (end == std::string_view::npos) { + segments.push_back(path.substr(start)); + break; + } - segments.push_back(path.substr(start, end - start)); - start = end; - } + segments.push_back(path.substr(start, end - start)); + start = end; + } - return segments; -} + return segments; +} \ No newline at end of file diff --git a/src/HttpServer.cpp b/src/HttpServer.cpp index 7ac2cfa..60404a1 100644 --- a/src/HttpServer.cpp +++ b/src/HttpServer.cpp @@ -70,7 +70,7 @@ void HttpServer::listen(int port) { // Otherwise log and continue or break as appropriate. throw std::runtime_error("unable to obtain a valid connection file " - "descriptor, exiting\n"); + "descriptor, exiting\n"); } this->store_conn_fd(conn_file_descriptor); } @@ -98,7 +98,7 @@ void HttpServer::stop_listening() { void HttpServer::store_conn_fd(int conn_fd) { queue.push(conn_fd); } bool HttpServer::is_valid_request(std::string &request_buffer, - ssize_t bytes_read) { + ssize_t bytes_read) { // Check if the request is empty if (bytes_read <= 0) { std::cerr << "Invalid request formatting: 0 bytes read\n"; @@ -146,7 +146,7 @@ void HttpServer::handle_client() { case compile_time_method_hash("CONNECT"): case compile_time_method_hash("TRACE"): { req.body = ""; - for (const auto& [route, route_fn] : routes[req.method]) { + for (const auto &[route, route_fn] : routes[req.method]) { if (HttpParser::match_route(route, req)) { // when the route is matched, fn should run and allow fn definitions to use pathParams res.status = 200; @@ -155,7 +155,8 @@ void HttpServer::handle_client() { break; } } - if (res.status == 200) break; + if (res.status == 200) + break; res.status = 404; res.status_name = "Not Found"; res.body = R"({"error": "The requested endpoint does not exist"})"; @@ -163,7 +164,7 @@ void HttpServer::handle_client() { case compile_time_method_hash("POST"): case compile_time_method_hash("PUT"): case compile_time_method_hash("PATCH"): { - for (const auto& [route, route_fn] : routes[req.method]) { + for (const auto &[route, route_fn] : routes[req.method]) { if (HttpParser::match_route(route, req)) { // when the route is matched, fn should run and allow fn definitions to use pathParams res.status = 200; @@ -172,7 +173,8 @@ void HttpServer::handle_client() { break; } } - if (res.status == 200) break; + if (res.status == 200) + break; res.status = 404; res.status_name = "Not Found"; res.body = R"({"error": "The requested endpoint does not exist"})"; @@ -181,7 +183,8 @@ void HttpServer::handle_client() { default: { res.status = 500; res.status_name = "Error"; - res.body = R"({\"error\": \"The request does not have a valid HTTP method\"})"; + res.body = + R"({\"error\": \"The request does not have a valid HTTP method\"})"; } } const ssize_t bytes_sent = @@ -189,8 +192,8 @@ void HttpServer::handle_client() { if (bytes_sent == -1) { close(conn_fd); std::cerr << "\n\n" - << strerror(errno) - << ": issue sending message to connection\n"; + << strerror(errno) + << ": issue sending message to connection\n"; continue; } close(conn_fd); @@ -204,47 +207,47 @@ int HttpServer::get_listener_socket(const int port) { addrinfo *results{}; int socket_file_descriptor{}; - hints.ai_family = AF_UNSPEC; // can be IPv4 or 6 + hints.ai_family = AF_UNSPEC; // can be IPv4 or 6 hints.ai_socktype = SOCK_STREAM; // TCP stream sockets - hints.ai_flags = AI_PASSIVE; // fill in IP for us + hints.ai_flags = AI_PASSIVE; // fill in IP for us int status = getaddrinfo(Constants::hostname, port_str.c_str(), &hints, &results); if (status != 0) { throw std::runtime_error("gai error: " + - std::string(gai_strerror(status))); + std::string(gai_strerror(status))); } // find the first file descriptor that does not fail for (addrinfo_ptr = results; addrinfo_ptr != nullptr; - addrinfo_ptr = addrinfo_ptr->ai_next) { + addrinfo_ptr = addrinfo_ptr->ai_next) { socket_file_descriptor = socket(addrinfo_ptr->ai_family, addrinfo_ptr->ai_socktype, - addrinfo_ptr->ai_protocol); + addrinfo_ptr->ai_protocol); if (socket_file_descriptor == -1) { std::cerr << "\n\n" - << strerror(errno) - << ": issue fetching the socket file descriptor\n"; + << strerror(errno) + << ": issue fetching the socket file descriptor\n"; continue; } // set socket options int yes = 1; int sockopt_status = setsockopt(socket_file_descriptor, SOL_SOCKET, - SO_REUSEADDR, &yes, sizeof(int)); + SO_REUSEADDR, &yes, sizeof(int)); if (sockopt_status == -1) { throw std::runtime_error(std::string(strerror(errno)) + - ": issue setting socket options"); + ": issue setting socket options"); } // associate the socket descriptor with the port passed into // getaddrinfo() int bind_status = bind(socket_file_descriptor, addrinfo_ptr->ai_addr, - addrinfo_ptr->ai_addrlen); + addrinfo_ptr->ai_addrlen); if (bind_status == -1) { std::cerr << "\n\n" - << strerror(errno) - << ": issue binding the socket descriptor with a port"; + << strerror(errno) + << ": issue binding the socket descriptor with a port"; continue; } @@ -255,13 +258,13 @@ int HttpServer::get_listener_socket(const int port) { if (addrinfo_ptr == nullptr) { throw std::runtime_error(std::string(strerror(errno)) + - ": failed to bind port to socket"); + ": failed to bind port to socket"); } int listen_status = ::listen(socket_file_descriptor, Constants::backlog); if (listen_status == -1) { throw std::runtime_error(std::string(strerror(errno)) + - ": issue trying to call listen()"); + ": issue trying to call listen()"); } return socket_file_descriptor; @@ -280,12 +283,12 @@ void HttpServer::put_mapping(const std::string_view route, const Handler &fn) { } void HttpServer::patch_mapping(const std::string_view route, - const Handler &fn) { + const Handler &fn) { routes["PATCH"].emplace_back(HttpParser::path_to_route(route), fn); } void HttpServer::delete_mapping(const std::string_view route, - const Handler &fn) { + const Handler &fn) { routes["DELETE"].emplace_back(HttpParser::path_to_route(route), fn); } @@ -294,16 +297,16 @@ void HttpServer::head_mapping(const std::string_view route, const Handler &fn) { } void HttpServer::options_mapping(const std::string_view route, - const Handler &fn) { + const Handler &fn) { routes["OPTIONS"].emplace_back(HttpParser::path_to_route(route), fn); } void HttpServer::connect_mapping(const std::string_view route, - const Handler &fn) { + const Handler &fn) { routes["CONNECT"].emplace_back(HttpParser::path_to_route(route), fn); } void HttpServer::trace_mapping(const std::string_view route, - const Handler &fn) { + const Handler &fn) { routes["TRACE"].emplace_back(HttpParser::path_to_route(route), fn); -} +} \ No newline at end of file diff --git a/test/HttpParserTest.cpp b/test/HttpParserTest.cpp index 2680d3c..6fe9311 100644 --- a/test/HttpParserTest.cpp +++ b/test/HttpParserTest.cpp @@ -6,17 +6,17 @@ #include class HttpParserTest : public ::testing::Test { - public: +public: static constexpr int port{8081}; }; TEST(HttpParserTest, ShouldSplitPath) { - std::string path = "/foo/foo2"; - std::vector segments = HttpParser::split_path(path); + std::string path = "/foo/foo2"; + std::vector segments = HttpParser::split_path(path); - EXPECT_EQ(segments.size(), 2); - EXPECT_EQ(segments[0], "foo"); - EXPECT_EQ(segments[1], "foo2"); + EXPECT_EQ(segments.size(), 2); + EXPECT_EQ(segments[0], "foo"); + EXPECT_EQ(segments[1], "foo2"); } TEST(HttpParserTest, ShouldHaveRootPath) { @@ -32,13 +32,15 @@ TEST(HttpParserTest, ShouldHavePathParameters) { HttpServer server{}; server.get_mapping( "/foo/{id}/{user}", [](HttpServer::Request &req, - HttpServer::Response &res) { + HttpServer::Response &res) { std::stringstream ss; - ss << "id: " << req.path_params.get_path_param("id").value() << "\n"; - ss << "user: " << req.path_params.get_path_param("user").value() << "\n"; + ss << "id: " << req.path_params.get_path_param("id").value() << + "\n"; + ss << "user: " << req.path_params.get_path_param("user").value() + << "\n"; res.body = ss.str(); } - ); + ); server.start_listening(8081); @@ -56,11 +58,11 @@ TEST(HttpParserTest, ShouldHavePathParameters) { 0); const std::string request = "GET /foo/123/james HTTP/1.1\r\n" - "Host: localhost\r\n" - "Connection: keep-alive\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"; + "Host: localhost\r\n" + "Connection: keep-alive\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"; send(sock, request.c_str(), request.size(), 0); @@ -83,7 +85,7 @@ TEST(HttpParserTest, ShouldHandleNoPathParameters) { HttpServer server{}; server.get_mapping( "/foo/{id}/{user}", [](HttpServer::Request &req, - HttpServer::Response &res) { + HttpServer::Response &res) { std::stringstream ss; try { ss << req.path_params.get_path_param("foo").value() << "\n"; @@ -92,7 +94,7 @@ TEST(HttpParserTest, ShouldHandleNoPathParameters) { res.body = "could not get path parameter foo"; } } - ); + ); server.start_listening(8081); @@ -110,11 +112,11 @@ TEST(HttpParserTest, ShouldHandleNoPathParameters) { 0); const std::string request = "GET /foo/123/james HTTP/1.1\r\n" - "Host: localhost\r\n" - "Connection: keep-alive\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"; + "Host: localhost\r\n" + "Connection: keep-alive\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"; send(sock, request.c_str(), request.size(), 0); @@ -125,10 +127,12 @@ TEST(HttpParserTest, ShouldHandleNoPathParameters) { EXPECT_GT(bytes, 0); EXPECT_FALSE(result.find("id: 123") != std::string::npos); EXPECT_FALSE(result.find("user: james") != std::string::npos); - EXPECT_TRUE(result.find("could not get path parameter foo") != std::string::npos); + EXPECT_TRUE( + result.find("could not get path parameter foo") != std::string:: + npos); close(sock); } catch (const std::exception &e) { FAIL() << "Exception occurred: " << e.what(); } -} +} \ No newline at end of file diff --git a/test/HttpServerTest.cpp b/test/HttpServerTest.cpp index b21f0f9..aadd2d9 100644 --- a/test/HttpServerTest.cpp +++ b/test/HttpServerTest.cpp @@ -5,7 +5,7 @@ #include class HttpServerTest : public ::testing::Test { - public: +public: static constexpr int port{8081}; }; @@ -14,8 +14,10 @@ TEST(ServerTest, ConstructorDestructorTest) { HttpServer server{}; } TEST(HttpServerTest, AcceptsHttpRequest) { HttpServer server{}; server.get_mapping("/", - [](const HttpServer::Request &, - HttpServer::Response &res) { res.body = "test"; }); + [](const HttpServer::Request &, + HttpServer::Response &res) { + res.body = "test"; + }); server.start_listening(HttpServerTest::port); std::this_thread::sleep_for(std::chrono::milliseconds(1)); @@ -28,13 +30,13 @@ TEST(HttpServerTest, AcceptsHttpRequest) { addr.sin_addr.s_addr = inet_addr("127.0.0.1"); ASSERT_EQ(connect(sock, reinterpret_cast(&addr), sizeof(addr)), - 0); + 0); const char *request = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n" - "Host: localhost\r\n" - "Connection: keep-alive\r\n" - "Content-Length: 0\r\n" - "\r\n"; + "Host: localhost\r\n" + "Connection: keep-alive\r\n" + "Content-Length: 0\r\n" + "\r\n"; send(sock, request, strlen(request), 0); char buffer[1024]; @@ -63,7 +65,7 @@ TEST(HttpServerTest, AcceptGetRequest) { addr.sin_addr.s_addr = inet_addr("127.0.0.1"); ASSERT_EQ(connect(sock, reinterpret_cast(&addr), sizeof(addr)), - 0); + 0); const char *request = "GET /hello HTTP/1.1\r\n\r\n"; send(sock, request, strlen(request), 0); @@ -81,8 +83,10 @@ TEST(HttpServerTest, AcceptGetRequest) { TEST(HttpServerTest, IgnoreGetReqBody) { HttpServer server{}; server.get_mapping("/hello", - [](const HttpServer::Request &req, - HttpServer::Response &res) { res.body = req.body; }); + [](const HttpServer::Request &req, + HttpServer::Response &res) { + res.body = req.body; + }); server.start_listening(HttpServerTest::port); std::this_thread::sleep_for(std::chrono::milliseconds(1)); @@ -95,7 +99,7 @@ TEST(HttpServerTest, IgnoreGetReqBody) { addr.sin_addr.s_addr = inet_addr("127.0.0.1"); ASSERT_EQ(connect(sock, reinterpret_cast(&addr), sizeof(addr)), - 0); + 0); const char *request = "GET /hello HTTP/1.1\r\n\r\nhello, world"; send(sock, request, strlen(request), 0); @@ -117,7 +121,9 @@ TEST(HttpServerTest, DoesntIgnorePostReqBody) { HttpServer server{}; server.post_mapping( "/foo", [](const HttpServer::Request &req, - HttpServer::Response &res) { res.body = req.body; }); + HttpServer::Response &res) { + res.body = req.body; + }); server.start_listening(HttpServerTest::port); std::this_thread::sleep_for(std::chrono::milliseconds(1)); @@ -134,11 +140,11 @@ TEST(HttpServerTest, DoesntIgnorePostReqBody) { 0); std::string request = "POST /foo HTTP/1.1\r\n" - "Host: localhost\r\n" - "Connection: keep-alive\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"; + "Host: localhost\r\n" + "Connection: keep-alive\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"; send(sock, request.c_str(), request.size(), 0); @@ -160,32 +166,50 @@ TEST(HttpServerTest, AllUniqueReqMethods) { HttpServer server{}; server.get_mapping("/foo", - [](const HttpServer::Request &req, - HttpServer::Response &res) { res.body = "0"; }); + [](const HttpServer::Request &req, + HttpServer::Response &res) { + res.body = "0"; + }); server.post_mapping("/foo", - [](const HttpServer::Request &req, - HttpServer::Response &res) { res.body = "1"; }); + [](const HttpServer::Request &req, + HttpServer::Response &res) { + res.body = "1"; + }); server.put_mapping("/foo", - [](const HttpServer::Request &req, - HttpServer::Response &res) { res.body = "2"; }); + [](const HttpServer::Request &req, + HttpServer::Response &res) { + res.body = "2"; + }); server.patch_mapping("/foo", - [](const HttpServer::Request &req, - HttpServer::Response &res) { res.body = "3"; }); + [](const HttpServer::Request &req, + HttpServer::Response &res) { + res.body = "3"; + }); server.options_mapping("/foo", - [](const HttpServer::Request &req, - HttpServer::Response &res) { res.body = "4"; }); + [](const HttpServer::Request &req, + HttpServer::Response &res) { + res.body = "4"; + }); server.head_mapping("/foo", - [](const HttpServer::Request &req, - HttpServer::Response &res) { res.body = "5"; }); + [](const HttpServer::Request &req, + HttpServer::Response &res) { + res.body = "5"; + }); server.delete_mapping("/foo", - [](const HttpServer::Request &req, - HttpServer::Response &res) { res.body = "6"; }); + [](const HttpServer::Request &req, + HttpServer::Response &res) { + res.body = "6"; + }); server.connect_mapping("/foo", - [](const HttpServer::Request &req, - HttpServer::Response &res) { res.body = "7"; }); + [](const HttpServer::Request &req, + HttpServer::Response &res) { + res.body = "7"; + }); server.trace_mapping("/foo", - [](const HttpServer::Request &req, - HttpServer::Response &res) { res.body = "8"; }); + [](const HttpServer::Request &req, + HttpServer::Response &res) { + res.body = "8"; + }); server.start_listening(HttpServerTest::port); @@ -197,19 +221,19 @@ TEST(HttpServerTest, AllUniqueReqMethods) { addr.sin_addr.s_addr = inet_addr("127.0.0.1"); for (int i = 0; i < 9; i++) { - const std::string methods[9] = {"GET", "POST", "PUT", - "PATCH", "OPTIONS", "HEAD", - "DELETE", "CONNECT", "TRACE"}; + const std::string methods[9] = {"GET", "POST", "PUT", + "PATCH", "OPTIONS", "HEAD", + "DELETE", "CONNECT", "TRACE"}; std::string request = methods[i] + " /foo HTTP/1.1\r\n" - "Host: localhost\r\n" - "Connection: keep-alive\r\n" - "Content-Length: 0\r\n" - "\r\n"; + "Host: localhost\r\n" + "Connection: keep-alive\r\n" + "Content-Length: 0\r\n" + "\r\n"; int listener_fd = socket(AF_INET, SOCK_STREAM, 0); ASSERT_EQ(connect(listener_fd, reinterpret_cast(&addr), - sizeof(addr)), - 0); + sizeof(addr)), + 0); send(listener_fd, request.c_str(), request.size(), 0); char buffer[1024]{}; @@ -233,10 +257,10 @@ TEST(HttpServerTest, HandleNonExistentGetRoute) { addr.sin_addr.s_addr = inet_addr("127.0.0.1"); std::string request = "GET /foo HTTP/1.1\r\n" - "Host: localhost\r\n" - "Connection: keep-alive\r\n" - "Content-Length: 0\r\n" - "\r\n"; + "Host: localhost\r\n" + "Connection: keep-alive\r\n" + "Content-Length: 0\r\n" + "\r\n"; int listener_fd = socket(AF_INET, SOCK_STREAM, 0); ASSERT_EQ( @@ -268,10 +292,10 @@ TEST(HttpServerTest, HandleNonExistentPostRoute) { addr.sin_addr.s_addr = inet_addr("127.0.0.1"); std::string request = "POST /foo HTTP/1.1\r\n" - "Host: localhost\r\n" - "Connection: keep-alive\r\n" - "Content-Length: 0\r\n" - "\r\n"; + "Host: localhost\r\n" + "Connection: keep-alive\r\n" + "Content-Length: 0\r\n" + "\r\n"; int listener_fd = socket(AF_INET, SOCK_STREAM, 0); ASSERT_EQ( @@ -299,10 +323,10 @@ TEST(HttpServerTest, HandleNonExistentHttpMethod) { addr.sin_addr.s_addr = inet_addr("127.0.0.1"); std::string request = "FOO /foo HTTP/1.1\r\n" - "Host: localhost\r\n" - "Connection: keep-alive\r\n" - "Content-Length: 0\r\n" - "\r\n"; + "Host: localhost\r\n" + "Connection: keep-alive\r\n" + "Content-Length: 0\r\n" + "\r\n"; int listener_fd = socket(AF_INET, SOCK_STREAM, 0); ASSERT_EQ( @@ -322,4 +346,4 @@ TEST(HttpServerTest, HandleNonExistentHttpMethod) { TEST(HttpServerTest, ListenThrowsIfSocketInvalid) { HttpServer server{}; EXPECT_THROW(server.listen(-1), std::runtime_error); -} +} \ No newline at end of file