// Project Includes #include "client.hpp" #include "file_descriptor.hpp" #include "http.hpp" // Standard Library Includes #include #include #include #include #include #include #include #include #include #include #include #include #include [[nodiscard]] static std::expected decode_url_path(std::string_view url_path) { std::string decoded; decoded.reserve(url_path.size()); auto hex_value = [](char c) -> int { if (c >= '0' && c <= '9') return c - '0'; c = static_cast(std::toupper(static_cast(c))); if (c >= 'A' && c <= 'F') return c - 'A' + 10; return -1; }; for (std::size_t i = 0; i < url_path.size(); ++i) { const char c = url_path[i]; if (c == '%') { if (i + 2 >= url_path.size()) { return std::unexpected("Invalid percent-encoding (truncated)"); } const int hi = hex_value(url_path[i + 1]); const int lo = hex_value(url_path[i + 2]); if (hi < 0 || lo < 0) { return std::unexpected("Invalid percent-encoding (non-hex)"); } const char decoded_char = static_cast((hi << 4) | lo); if (decoded_char == '\0') { return std::unexpected("Embedded NUL in URL path"); } decoded.push_back(decoded_char); i += 2; } else if (c == '\\') { // Disallow Windows-style separators in URL paths return std::unexpected("Backslash not allowed in URL path"); } else if (c == '\0') { return std::unexpected("Embedded NUL in URL path"); } else { decoded.push_back(c); } } return decoded; } [[nodiscard]] std::expected sanitize_path( std::string_view url_path, std::filesystem::path const &root_directory) { // Basic validation if (url_path.empty() || url_path.front() != '/') { return std::unexpected("URL path must start with '/'"); } // Strip query and fragment: /foo/bar?x=1#frag → /foo/bar if (const auto pos = url_path.find_first_of("?#"); pos != std::string_view::npos) { url_path = url_path.substr(0, pos); } // Drop leading '/' url_path.remove_prefix(1); // Decode percent-encodings auto decoded_result = decode_url_path(url_path); if (!decoded_result) { return std::unexpected(decoded_result.error()); } const std::string &decoded_path = decoded_result.value(); // Build requested path under the root std::filesystem::path requested_path = root_directory; for (const auto &part : std::filesystem::path(decoded_path)) { const auto &native = part.native(); if (native.empty() || native == ".") { continue; } if (native == "..") { return std::unexpected("Path traversal detected"); } requested_path /= part; } std::error_code ec; // Canonicalize root and requested paths to defend against symlink escapes const auto canonical_root = std::filesystem::weakly_canonical(root_directory, ec); if (ec) { return std::unexpected("Failed to canonicalize root directory: " + ec.message()); } if (std::filesystem::is_directory(requested_path, ec) && !ec) { requested_path /= "index.html"; } const auto canonical_requested = std::filesystem::weakly_canonical(requested_path, ec); if (ec) { return std::unexpected("Failed to canonicalize requested path: " + ec.message()); } // Ensure the requested path remains inside the document root const auto root_str = canonical_root.native(); const auto req_str = canonical_requested.native(); if (req_str.size() < root_str.size() || !std::equal(root_str.begin(), root_str.end(), req_str.begin()) || (req_str.size() > root_str.size() && req_str[root_str.size()] != std::filesystem::path::preferred_separator)) { return std::unexpected("Requested path escapes document root"); } return canonical_requested; } namespace detail { // Centralized MIME table, constexpr and case-insensitive [[nodiscard]] inline std::string_view mime_type_for_extension(std::string_view ext) { constexpr std::array kMimeTable{ std::pair{std::string_view{".html"}, "text/html"}, std::pair{std::string_view{".htm"}, "text/html"}, std::pair{std::string_view{".css"}, "text/css"}, std::pair{std::string_view{".js"}, "application/javascript"}, std::pair{std::string_view{".json"}, "application/json"}, std::pair{std::string_view{".png"}, "image/png"}, std::pair{std::string_view{".jpg"}, "image/jpeg"}, std::pair{std::string_view{".jpeg"}, "image/jpeg"}, std::pair{std::string_view{".gif"}, "image/gif"}, std::pair{std::string_view{".svg"}, "image/svg+xml"}, std::pair{std::string_view{".txt"}, "text/plain"}, }; // Normalize to lowercase to avoid case-sensitivity issues std::string lower_ext(ext); std::ranges::transform(lower_ext, lower_ext.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); if (auto it = std::ranges::find_if(kMimeTable, [&lower_ext](auto const &p) { return p.first == lower_ext; }); it != kMimeTable.end()) { return it->second; } return "application/octet-stream"; } // Utility for generating error responses [[nodiscard]] inline HttpResponse make_error_response(uint16_t code, std::string_view reason, std::string_view body_msg = {}) { HttpResponse r{}; r.http_major = 1; r.http_minor = 1; r.status_code = code; r.reason_phrase = std::string(reason); if (!body_msg.empty()) { const auto *body_bytes = std::bit_cast(body_msg.data()); const auto body_size = body_msg.size(); r.body.assign(body_bytes, body_bytes + body_size); r.headers.emplace_back("Content-Length", std::to_string(r.body.size())); r.headers.emplace_back("Content-Type", "text/plain; charset=utf-8"); } else { r.headers.emplace_back("Content-Length", "0"); } // Basic security hardening header r.headers.emplace_back("X-Content-Type-Options", "nosniff"); return r; } } // namespace detail HttpResponse handle_get_request(HttpRequest const &request, std::filesystem::path const &root_directory) { using namespace detail; // In production you may want to *respond* with 405 rather than assert. assert(request.method == "GET"); auto sanitized_path_result = sanitize_path(request.url, root_directory); if (!sanitized_path_result.has_value()) { std::println(std::cerr, "Error sanitizing path: {}", sanitized_path_result.error()); return make_error_response(400, "Bad Request", "Invalid path.\n"); } const std::filesystem::path &file_path = sanitized_path_result.value(); std::error_code ec; if (!std::filesystem::exists(file_path, ec) || !std::filesystem::is_regular_file(file_path, ec)) { return make_error_response(404, "Not Found", "File not found.\n"); } const auto file_size_u = std::filesystem::file_size(file_path, ec); if (ec) { std::println(std::cerr, "file_size error: {}", ec.message()); return make_error_response(500, "Internal Server Error"); } // Guard against overflow when converting to size_t if (file_size_u > static_cast(std::numeric_limits::max())) { std::println(std::cerr, "File too large to serve: {}", file_path.string()); return make_error_response(413, "Payload Too Large", "Requested file is too large.\n"); } const auto file_size = static_cast(file_size_u); HttpResponse response{}; response.http_major = request.http_major ? request.http_major : 1; response.http_minor = request.http_minor ? request.http_minor : 1; response.status_code = 200; response.reason_phrase = "OK"; response.body.resize(file_size); const auto ext = file_path.extension().string(); const auto mime = mime_type_for_extension(ext); response.headers.emplace_back("Content-Type", std::string(mime)); response.headers.emplace_back("Content-Length", std::to_string(file_size)); response.headers.emplace_back("X-Content-Type-Options", "nosniff"); { std::ifstream file(file_path, std::ios::binary); if (!file) { std::println(std::cerr, "Failed to open {}", file_path.string()); return make_error_response(500, "Internal Server Error"); } file.read(std::bit_cast(response.body.data()), static_cast(file_size)); if (!file) { std::println(std::cerr, "Error reading {}", file_path.string()); return make_error_response(500, "Internal Server Error"); } } return response; } [[nodiscard]] std::vector generate_responses(RequestList const &requests, std::filesystem::path const &root_directory) { using namespace detail; std::vector responses; responses.reserve(requests.size()); for (auto const &request : requests) { if (request.method == "GET") { responses.push_back(handle_get_request(request, root_directory)); } else { auto response = make_error_response(405, "Method Not Allowed", "Only GET is supported.\n"); response.headers.emplace_back("Allow", "GET"); responses.push_back(std::move(response)); } } return responses; } kev::task read_requests(Client &client) { using namespace stdexec; auto &read_buffer = client.m_buffer; while (client.m_parser.get_number_of_completed_requests() == 0 && !client.m_disconnect_requested) { size_t bytes_read = co_await client.m_uring_ctx.async_read(client.m_fd.get(), std::span(read_buffer.data(), read_buffer.size())); if (bytes_read == 0) { client.m_disconnect_requested = true; break; } auto const data = std::span(read_buffer.data(), bytes_read); auto result = client.m_parser.parse(data); if (!result.has_value()) { std::println(std::cerr, "Error parsing HTTP request. Message: {}", result.error()); std::println("Resetting parser state."); client.m_parser.reset(); throw std::runtime_error("Error parsing HTTP request"); } } RequestList requests; client.m_parser.get_completed_requests(requests); co_return requests; } kev::task handle_requests(Client const &client, ServerContext &server_context, RequestList requests) { using namespace stdexec; std::vector responses = generate_responses(requests, server_context.m_root_directory); std::vector bytes_to_write; bytes_to_write.reserve(4096); for (auto const &response : responses) { response.serialize_into(bytes_to_write); } co_await client.m_uring_ctx.async_write_all( client.m_fd.get(), std::span(bytes_to_write.data(), bytes_to_write.size())); } kev::task handle_connection_coroutine(FileDescriptor client_fd, ServerContext &ctx, UringContext &uring_ctx) { // Client will get hoisted into the coroutine frame and it's lifetime is managed automatically // Passing by reference to read_requests_coroutine and handle_requests_coroutine is safe because // they are called from within this coroutine and thus cannot outlive it. auto client = Client(std::move(client_fd), uring_ctx); while (!client.m_disconnect_requested) { RequestList requests = co_await read_requests(client); co_await handle_requests(client, ctx, std::move(requests)); } }