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/CMakeLists.txt b/CMakeLists.txt index 9bc5860..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,9 +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 + include/HttpParser.h + include/PathParams.h + include/Route.h ) target_include_directories(${LIBRARY} PUBLIC include) target_compile_features(${LIBRARY} PRIVATE cxx_std_17) @@ -25,7 +30,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/include/AtomicQueue.h b/include/AtomicQueue.h index 711b21c..320cb1a 100644 --- a/include/AtomicQueue.h +++ b/include/AtomicQueue.h @@ -1,45 +1,47 @@ -#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; + } }; diff --git a/include/HttpParser.h b/include/HttpParser.h new file mode 100644 index 0000000..d2bd34e --- /dev/null +++ b/include/HttpParser.h @@ -0,0 +1,32 @@ +#pragma once + +#include "HttpServer.h" +#include "Route.h" + +#include +#include + +class HttpParser { + public: + static bool match_route( + const HttpUtils::Route& route, + HttpServer::Request& request + ); + + static HttpUtils::Route path_to_route(std::string_view path); + + 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); + + 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 7c8b411..356aa6b 100644 --- a/include/HttpServer.h +++ b/include/HttpServer.h @@ -1,90 +1,118 @@ #pragma once +#include "AtomicQueue.h" +#include "PathParams.h" +#include "Route.h" + +#include #include -#include -#include +#include #include -#include #include -#include "../include/AtomicQueue.h" +#include +#include class HttpServer { -public: - HttpServer(); + public: + HttpServer(); + + ~HttpServer(); + + std::atomic stop_flag{}; + + struct Request { + PathParams path_params; + std::string_view method; + std::string_view route; + std::string_view body; + }; - ~HttpServer(); + struct Response { + std::string_view header; + std::string body; + 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 + }; - std::atomic stop_flag {}; + using Handler = std::function; - struct Request { - std::string_view route; - std::string body; - }; + static bool is_valid_request(std::string &request_buffer, + ssize_t bytes_read); - struct Response { - std::string_view header; - std::string body; - int status; - }; + void get_mapping(std::string_view route, const Handler &fn); - using Handler = std::function; + void post_mapping(std::string_view route, const Handler &fn); - void get_mapping(std::string_view route, const Handler& fn); + void put_mapping(std::string_view route, const Handler &fn); - void post_mapping(std::string_view route, const Handler& fn); + void patch_mapping(std::string_view route, const Handler &fn); - void put_mapping(std::string_view route, const Handler& fn); + void delete_mapping(std::string_view route, const Handler &fn); - void patch_mapping(std::string_view route, const Handler& fn); + void head_mapping(std::string_view route, const Handler &fn); - void delete_mapping(std::string_view route, const Handler& fn); + void options_mapping(std::string_view route, const Handler &fn); - void head_mapping(std::string_view route, const Handler& fn); + void connect_mapping(std::string_view route, const Handler &fn); - void options_mapping(std::string_view route, const Handler& fn); + void trace_mapping(std::string_view route, const Handler &fn); - void connect_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); - void trace_mapping(std::string_view route, const Handler& fn); + /** + * 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 start listening/accepting requests from a specified port. This function is blocking. - */ - void listen(int port); + /** + * Tells the server to stop listening/accepting requests. + */ + void stop_listening(); - /** - * 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); + private: + AtomicQueue queue; - /** - * Tells the server to stop listening/accepting requests. - */ - void stop_listening(); + std::vector threads; -private: - AtomicQueue queue; + std::unordered_map>> + routes; - std::vector threads; + // + // std::unordered_map> routes; - std::unordered_map> routes; + std::unordered_map> + route_path_params; - void store_conn_fd(int conn_fd); + void store_conn_fd(int conn_fd); - int listener_fd {-1}; + int listener_fd{-1}; - /* - * @brief return a listener socket file descriptor - */ - int get_listener_socket(int port); + std::string_view get_method(int conn_fd, std::string_view path, + size_t &method_itr, bool &continues); - /** - * @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 - */ - void handle_client(); + /* + * @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(); }; diff --git a/include/PathParams.h b/include/PathParams.h new file mode 100644 index 0000000..02e2f55 --- /dev/null +++ b/include/PathParams.h @@ -0,0 +1,29 @@ +// +// Created by james on 12/26/25. +// + +#pragma once + +#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; + +}; \ No newline at end of file diff --git a/include/Route.h b/include/Route.h new file mode 100644 index 0000000..1e9e647 --- /dev/null +++ b/include/Route.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +namespace HttpUtils { + +struct RouteSegment { + enum class Type { Literal, Parameter }; + + Type type; + std::string value; +}; + +struct Route { + std::vector segments; +}; +} \ No newline at end of file diff --git a/src/HttpParser.cpp b/src/HttpParser.cpp new file mode 100644 index 0000000..5680509 --- /dev/null +++ b/src/HttpParser.cpp @@ -0,0 +1,151 @@ +// +// Created by james on 12/17/25. +// + +#include "HttpParser.h" + +#include +#include +#include + +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() == + '}') { + 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); + } + } + + return route; +} + +/** + * + * @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()) { + return false; + } + + // Compare segment by segment + for (size_t i = 0; i < route.segments.size(); ++i) { + const HttpUtils::RouteSegment route_seg = route.segments[i]; + const std::string_view req_seg = request_segments[i]; + + if (route_seg.type == HttpUtils::RouteSegment::Type::Literal) { + // Literal must match exactly + if (route_seg.value != req_seg) { + return false; + } + } + } + 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; +} + +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; +} + +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; + 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; +} \ No newline at end of file diff --git a/src/HttpServer.cpp b/src/HttpServer.cpp index 82e7a04..60404a1 100644 --- a/src/HttpServer.cpp +++ b/src/HttpServer.cpp @@ -1,6 +1,7 @@ -#include "../include/HttpServer.h" -#include "../include/constants.h" -#include +#include "HttpServer.h" +#include "constants.h" + +#include #include #include #include @@ -9,319 +10,303 @@ #include #include -constexpr size_t compile_time_method_hash(std::string_view method) { - size_t hash = 0; - for (char c : method) { - hash += c; - } - return hash; +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 method_hash(std::string_view method) { - size_t hash = 0; - for (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; } 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 (int i = 0; i < threads.size(); i++) { - threads[i].join(); - // std::cout << "thread removed: " << i << "\n"; - } - threads.clear(); + this->stop_listening(); + for (auto &thread : threads) { + thread.join(); + } + 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)}; - - 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::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; +} void HttpServer::handle_client() { - while (!stop_flag.load()) { - // Read the incoming HTTP request - char request_buffer[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, 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) { - 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) { - 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)) { - 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]; - 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 = "{\"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"): { - const Request req { path, std::string(req_body)}; - if (routes[method].find(route) != routes[method].end()) { - Handler route_fn = routes[method][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 = "{\"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 = "{\"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); - - // std::cout << request_buffer << "\n"; - } - } - int 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; - } - // std::cout << request_buffer << "\n"; - 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 (!HttpParser::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 = ""; + 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; + res.status_name = "OK"; + route_fn(req, res); + break; + } + } + 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"): { + 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; + res.status_name = "OK"; + route_fn(req, res); + break; + } + } + 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.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, res.str().c_str(), res.str().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(int port) { - std::string port_str = std::to_string(port); - struct addrinfo hints {}; - struct addrinfo* addrinfo_ptr {}; - struct 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; +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; } -void HttpServer::get_mapping(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"].emplace_back(HttpParser::path_to_route(route), fn); } -void HttpServer::post_mapping(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"].emplace_back(HttpParser::path_to_route(route), fn); } -void HttpServer::put_mapping(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"].emplace_back(HttpParser::path_to_route(route), fn); } -void HttpServer::patch_mapping(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"].emplace_back(HttpParser::path_to_route(route), fn); } -void HttpServer::delete_mapping(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"].emplace_back(HttpParser::path_to_route(route), fn); } -void HttpServer::head_mapping(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"].emplace_back(HttpParser::path_to_route(route), fn); } -void HttpServer::options_mapping(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"].emplace_back(HttpParser::path_to_route(route), fn); } -void HttpServer::connect_mapping(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"].emplace_back(HttpParser::path_to_route(route), fn); } -void HttpServer::trace_mapping(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"].emplace_back(HttpParser::path_to_route(route), fn); +} \ No newline at end of file diff --git a/src/PathParams.cpp b/src/PathParams.cpp new file mode 100644 index 0000000..cc53d01 --- /dev/null +++ b/src/PathParams.cpp @@ -0,0 +1,22 @@ +// +// Created by james on 12/26/25. +// + +#include "PathParams.h" + +#include +#include + +std::optional PathParams::get_path_param( + const std::string_view 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[std::string{key}] = value; +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 56589fe..7f23215 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,22 +1,24 @@ #include -#include "../include/HttpServer.h" +#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/HttpParserTest.cpp b/test/HttpParserTest.cpp new file mode 100644 index 0000000..6fe9311 --- /dev/null +++ b/test/HttpParserTest.cpp @@ -0,0 +1,138 @@ +#include +#include "../include/HttpParser.h" + +#include +#include +#include + +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"); +} + +TEST(HttpParserTest, ShouldHaveRootPath) { + const std::string path = "/"; + const std::vector segments = HttpParser::split_path(path); + + EXPECT_EQ(segments.size(), 1); + EXPECT_EQ(segments[0], "/"); +} + +TEST(HttpParserTest, ShouldHavePathParameters) { + try { + HttpServer server{}; + server.get_mapping( + "/foo/{id}/{user}", [](HttpServer::Request &req, + 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"; + res.body = ss.str(); + } + ); + + 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_TRUE(result.find("id: 123") != std::string::npos); + EXPECT_TRUE(result.find("user: james") != std::string::npos); + + close(sock); + } catch (const std::exception &e) { + 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(); + } +} \ No newline at end of file diff --git a/test/HttpServerTest.cpp b/test/HttpServerTest.cpp index 2af9e60..aadd2d9 100644 --- a/test/HttpServerTest.cpp +++ b/test/HttpServerTest.cpp @@ -6,241 +6,275 @@ class HttpServerTest : public ::testing::Test { public: - static constexpr int port {8081}; + 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]; - int 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] {}; - int 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] {}; - int 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); + 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] {}; - int 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"); - - const std::string methods[9] = { "GET", "POST", "PUT", "PATCH", "OPTIONS", "HEAD", "DELETE", "CONNECT", "TRACE" }; - for (int i = 0; i < 9; i++) { - 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] {}; - int 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] {}; - int 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 +282,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] {}; - int 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] {}; - int 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); +} \ No newline at end of file