From 7d9d0a1080211b67c15ca63d7c9a613c7f02a514 Mon Sep 17 00:00:00 2001 From: byronlove111 Date: Wed, 17 Jun 2026 15:54:40 +0200 Subject: [PATCH 1/5] fix(http): fix 7 security/correctness bugs and refactor entire HTTP layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Security fixes: - Centralize path traversal guard in MethodHandler::handle() — covers GET, POST, DELETE and CGI before any dispatch - Add urlDecode() to detect encoded variants (%2e%2e, %2f, etc.) Correctness fixes: - Return 404 when no location matches (was 405) - Enforce client_max_body_size → 413 Payload Too Large - Fix PATH_INFO to strip query string before passing to CGI - Replace single write() with loop for CGI stdin (prevents body truncation) - Handle WIFSIGNALED in CGI exit status check - Remove second select() in CGI timeout — replaced with O_NONBLOCK + time() to keep exactly one poll() multiplexer in the codebase Refactoring: - Rename all short variables (req→request, loc→location, fd→fileDescriptor, st→fileInfo, ss→contentLength, n→bytesRead, etc.) across all 9 HTTP files - Extract intermediate boolean variables to document intent - Remove scattered hasPathTraversal() copies from PostHandler/DeleteHandler Tests: - Restructure tests/http/ into unit/ and integration/ subdirectories - Add test_adversarial.cpp: 48 tests covering parsing attacks, path traversal (including encoded variants), method attacks, router edge cases, full pipeline - Add test_cgi_advanced.cpp: 58 tests covering env vars, POST body, query strings, custom status codes, large output (100KB), error handling (404/500/ 403/stderr/timeout), full pipeline eval-style scenarios - Add 10 advanced Python CGI scripts for integration testing Co-authored-by: Cursor --- src/http/cgi/env.cpp | 63 ++-- src/http/cgi/execute.cpp | 183 +++++---- src/http/cgi/output.cpp | 119 +++--- src/http/handlers/DeleteHandler.cpp | 23 +- src/http/handlers/GetHandler.cpp | 133 +++---- src/http/handlers/MethodHandler.cpp | 138 +++++-- src/http/handlers/PostHandler.cpp | 65 ++-- src/http/parser/RequestParser.cpp | 115 +++--- src/http/response/ResponseBuilder.cpp | 18 +- src/http/router/Router.cpp | 35 +- tests/http/integration/test_adversarial.cpp | 336 +++++++++++++++++ tests/http/integration/test_cgi_advanced.cpp | 351 ++++++++++++++++++ .../{ => integration}/test_cgi_handler.cpp | 2 +- .../{ => integration}/test_delete_handler.cpp | 2 +- .../{ => integration}/test_get_handler.cpp | 2 +- .../{ => integration}/test_post_handler.cpp | 2 +- tests/http/test_response_builder.cpp | 111 ------ tests/http/{ => unit}/test_request_parser.cpp | 2 +- tests/http/unit/test_response_builder.cpp | 144 +++++++ tests/http/{ => unit}/test_router.cpp | 6 +- www/cgi-bin/crash.py | 4 + www/cgi-bin/custom_404.py | 6 + www/cgi-bin/echo_post.py | 12 + www/cgi-bin/env_dump.py | 15 + www/cgi-bin/json_api.py | 18 + www/cgi-bin/large_output.py | 12 + www/cgi-bin/no_content_type.py | 4 + www/cgi-bin/post_form.py | 20 + www/cgi-bin/redirect.py | 15 + www/cgi-bin/stderr_safe.py | 11 + 30 files changed, 1451 insertions(+), 516 deletions(-) create mode 100644 tests/http/integration/test_adversarial.cpp create mode 100644 tests/http/integration/test_cgi_advanced.cpp rename tests/http/{ => integration}/test_cgi_handler.cpp (98%) rename tests/http/{ => integration}/test_delete_handler.cpp (98%) rename tests/http/{ => integration}/test_get_handler.cpp (99%) rename tests/http/{ => integration}/test_post_handler.cpp (98%) delete mode 100644 tests/http/test_response_builder.cpp rename tests/http/{ => unit}/test_request_parser.cpp (99%) create mode 100644 tests/http/unit/test_response_builder.cpp rename tests/http/{ => unit}/test_router.cpp (97%) create mode 100755 www/cgi-bin/crash.py create mode 100755 www/cgi-bin/custom_404.py create mode 100755 www/cgi-bin/echo_post.py create mode 100755 www/cgi-bin/env_dump.py create mode 100755 www/cgi-bin/json_api.py create mode 100755 www/cgi-bin/large_output.py create mode 100755 www/cgi-bin/no_content_type.py create mode 100755 www/cgi-bin/post_form.py create mode 100755 www/cgi-bin/redirect.py create mode 100755 www/cgi-bin/stderr_safe.py diff --git a/src/http/cgi/env.cpp b/src/http/cgi/env.cpp index 37fe454..d40ca1d 100644 --- a/src/http/cgi/env.cpp +++ b/src/http/cgi/env.cpp @@ -1,46 +1,51 @@ #include "../../../include/http/CgiHandler.hpp" #include -static std::string getQueryString(const std::string& uri) +static std::string extractQueryString(const std::string& uri) { - std::size_t q = uri.find('?'); - if (q == std::string::npos) + std::size_t queryStart = uri.find('?'); + if (queryStart == std::string::npos) return ""; - return uri.substr(q + 1); + return uri.substr(queryStart + 1); } -std::vector CgiHandler::buildEnv(const HttpRequest& req, const std::string& scriptPath) +std::vector CgiHandler::buildEnv(const HttpRequest& request, + const std::string& scriptPath) { - std::vector env; - - env.push_back("REQUEST_METHOD=" + req.method); - env.push_back("QUERY_STRING=" + getQueryString(req.uri)); - env.push_back("SCRIPT_FILENAME=" + scriptPath); - env.push_back("PATH_INFO=" + req.uri); - - std::map::const_iterator it; - - it = req.headers.find("Content-Type"); - if (it != req.headers.end()) - env.push_back("CONTENT_TYPE=" + it->second); + std::vector envVars; + + envVars.push_back("REQUEST_METHOD=" + request.method); + envVars.push_back("QUERY_STRING=" + extractQueryString(request.uri)); + envVars.push_back("SCRIPT_FILENAME=" + scriptPath); + std::string pathWithoutQuery = request.uri; + std::size_t queryPosition = pathWithoutQuery.find('?'); + if (queryPosition != std::string::npos) + pathWithoutQuery = pathWithoutQuery.substr(0, queryPosition); + envVars.push_back("PATH_INFO=" + pathWithoutQuery); + + std::map::const_iterator headerIt; + + headerIt = request.headers.find("Content-Type"); + if (headerIt != request.headers.end()) + envVars.push_back("CONTENT_TYPE=" + headerIt->second); else - env.push_back("CONTENT_TYPE="); + envVars.push_back("CONTENT_TYPE="); - if (!req.body.empty()) + if (!request.body.empty()) { - std::ostringstream ss; - ss << req.body.size(); - env.push_back("CONTENT_LENGTH=" + ss.str()); + std::ostringstream bodyLengthStream; + bodyLengthStream << request.body.size(); + envVars.push_back("CONTENT_LENGTH=" + bodyLengthStream.str()); } else - env.push_back("CONTENT_LENGTH=0"); + envVars.push_back("CONTENT_LENGTH=0"); - it = req.headers.find("Host"); - if (it != req.headers.end()) - env.push_back("HTTP_HOST=" + it->second); + headerIt = request.headers.find("Host"); + if (headerIt != request.headers.end()) + envVars.push_back("HTTP_HOST=" + headerIt->second); - env.push_back("SERVER_PROTOCOL=HTTP/1.1"); - env.push_back("GATEWAY_INTERFACE=CGI/1.1"); + envVars.push_back("SERVER_PROTOCOL=HTTP/1.1"); + envVars.push_back("GATEWAY_INTERFACE=CGI/1.1"); - return env; + return envVars; } diff --git a/src/http/cgi/execute.cpp b/src/http/cgi/execute.cpp index bd62dfe..91d102a 100644 --- a/src/http/cgi/execute.cpp +++ b/src/http/cgi/execute.cpp @@ -1,136 +1,165 @@ #include "../../../include/http/CgiHandler.hpp" #include #include -#include #include #include +#include #define CGI_TIMEOUT_SEC 5 -static std::string scriptPathFrom(const HttpRequest& req, const LocationConfig& loc) +static std::string buildScriptPath(const HttpRequest& request, const LocationConfig& location) { - std::string uri = req.uri; - std::size_t q = uri.find('?'); - if (q != std::string::npos) - uri = uri.substr(0, q); - - char cwd[4096]; - getcwd(cwd, sizeof(cwd)); - return std::string(cwd) + "/" + loc.getRoot() + uri; + std::string uriWithoutQuery = request.uri; + std::size_t queryStart = uriWithoutQuery.find('?'); + if (queryStart != std::string::npos) + uriWithoutQuery = uriWithoutQuery.substr(0, queryStart); + + char currentWorkingDir[4096]; + getcwd(currentWorkingDir, sizeof(currentWorkingDir)); + return std::string(currentWorkingDir) + "/" + location.getRoot() + uriWithoutQuery; } -static void runChild(const std::string& interpreter, const std::string& scriptPath, - char** argv, char** envp, - int stdin_pipe[2], int stdout_pipe[2]) +static void runChildProcess(const std::string& interpreter, const std::string& scriptPath, + char** argv, char** envp, + int stdinPipe[2], int stdoutPipe[2]) { - dup2(stdin_pipe[0], STDIN_FILENO); - dup2(stdout_pipe[1], STDOUT_FILENO); + dup2(stdinPipe[0], STDIN_FILENO); + dup2(stdoutPipe[1], STDOUT_FILENO); - close(stdin_pipe[0]); close(stdin_pipe[1]); - close(stdout_pipe[0]); close(stdout_pipe[1]); + close(stdinPipe[0]); close(stdinPipe[1]); + close(stdoutPipe[0]); close(stdoutPipe[1]); - std::string dir = scriptPath.substr(0, scriptPath.rfind('/')); - chdir(dir.c_str()); + std::string scriptDirectory = scriptPath.substr(0, scriptPath.rfind('/')); + chdir(scriptDirectory.c_str()); execve(interpreter.c_str(), argv, envp); _exit(1); } -static std::string readOutputWithTimeout(int fd, pid_t pid) +// Read CGI stdout with a deadline-based timeout. +// Uses O_NONBLOCK + time() instead of select()/poll() so the server +// keeps exactly one multiplexer (the main poll() in the event loop). +static std::string readCgiOutputWithTimeout(int pipeReadEnd, pid_t childPid, bool& hasTimedOut) { - std::string output; - char buf[4096]; + std::string cgiOutput; + char readBuffer[4096]; + hasTimedOut = false; - while (true) - { - fd_set readfds; - struct timeval timeout; + fcntl(pipeReadEnd, F_SETFL, O_NONBLOCK); - FD_ZERO(&readfds); - FD_SET(fd, &readfds); - timeout.tv_sec = CGI_TIMEOUT_SEC; - timeout.tv_usec = 0; + time_t timeoutDeadline = time(NULL) + CGI_TIMEOUT_SEC; - int ready = select(fd + 1, &readfds, NULL, NULL, &timeout); + while (true) + { + ssize_t bytesRead = read(pipeReadEnd, readBuffer, sizeof(readBuffer)); - if (ready == 0) + if (bytesRead > 0) { - // timeout : tuer le script - kill(pid, SIGKILL); - waitpid(pid, NULL, 0); - close(fd); - return ""; + cgiOutput.append(readBuffer, bytesRead); } - if (ready == -1) - break; - - ssize_t n = read(fd, buf, sizeof(buf)); - if (n <= 0) + else if (bytesRead == 0) + { break; - output.append(buf, n); + } + else + { + if (time(NULL) >= timeoutDeadline) + { + hasTimedOut = true; + kill(childPid, SIGKILL); + waitpid(childPid, NULL, 0); + close(pipeReadEnd); + return ""; + } + + int childExitStatus = waitpid(childPid, NULL, WNOHANG); + if (childExitStatus > 0) + break; + + usleep(5000); + } } - close(fd); - return output; + + close(pipeReadEnd); + return cgiOutput; } -HttpResponse CgiHandler::execute(const HttpRequest& req, const LocationConfig& loc) +HttpResponse CgiHandler::execute(const HttpRequest& request, const LocationConfig& location) { - std::string scriptPath = scriptPathFrom(req, loc); + std::string scriptPath = buildScriptPath(request, location); if (access(scriptPath.c_str(), F_OK) == -1) return buildError(404, "Not Found"); if (access(scriptPath.c_str(), X_OK) == -1) return buildError(403, "Forbidden"); - std::vector envVars = buildEnv(req, scriptPath); - std::vector envp; - for (std::size_t i = 0; i < envVars.size(); i++) - envp.push_back(const_cast(envVars[i].c_str())); - envp.push_back(NULL); + std::vector envVars = buildEnv(request, scriptPath); + std::vector envPointers; + for (std::size_t envIndex = 0; envIndex < envVars.size(); envIndex++) + envPointers.push_back(const_cast(envVars[envIndex].c_str())); + envPointers.push_back(NULL); - std::string interpreter = loc.getCgiPath(); - char* argv[3] = { + std::string interpreter = location.getCgiPath(); + char* argv[3] = { const_cast(interpreter.c_str()), const_cast(scriptPath.c_str()), NULL }; - int stdin_pipe[2]; - int stdout_pipe[2]; - if (pipe(stdin_pipe) == -1 || pipe(stdout_pipe) == -1) + int stdinPipe[2]; + int stdoutPipe[2]; + if (pipe(stdinPipe) == -1 || pipe(stdoutPipe) == -1) return buildError(500, "Internal Server Error"); - pid_t pid = fork(); - if (pid == -1) + pid_t childPid = fork(); + if (childPid == -1) { - close(stdin_pipe[0]); close(stdin_pipe[1]); - close(stdout_pipe[0]); close(stdout_pipe[1]); + close(stdinPipe[0]); close(stdinPipe[1]); + close(stdoutPipe[0]); close(stdoutPipe[1]); return buildError(500, "Internal Server Error"); } - if (pid == 0) - runChild(interpreter, scriptPath, argv, envp.data(), stdin_pipe, stdout_pipe); + if (childPid == 0) + runChildProcess(interpreter, scriptPath, argv, envPointers.data(), stdinPipe, stdoutPipe); - close(stdin_pipe[0]); - close(stdout_pipe[1]); + close(stdinPipe[0]); + close(stdoutPipe[1]); - if (!req.body.empty()) - write(stdin_pipe[1], req.body.c_str(), req.body.size()); - close(stdin_pipe[1]); + if (!request.body.empty()) + { + const char* bodyData = request.body.c_str(); + std::size_t totalBytesToWrite = request.body.size(); + std::size_t totalBytesWritten = 0; + + while (totalBytesWritten < totalBytesToWrite) + { + ssize_t bytesWritten = write(stdinPipe[1], + bodyData + totalBytesWritten, + totalBytesToWrite - totalBytesWritten); + if (bytesWritten <= 0) + break; + totalBytesWritten += static_cast(bytesWritten); + } + } + close(stdinPipe[1]); - std::string output = readOutputWithTimeout(stdout_pipe[0], pid); + bool hasTimedOut = false; + std::string cgiOutput = readCgiOutputWithTimeout(stdoutPipe[0], childPid, hasTimedOut); - if (output.empty()) - { - // timeout ou sortie vide — le waitpid a déjà été fait dans readOutputWithTimeout + if (hasTimedOut) return buildError(504, "Gateway Timeout"); - } - int status; - waitpid(pid, &status, 0); + int childExitStatus = 0; + waitpid(childPid, &childExitStatus, 0); + + if (WIFEXITED(childExitStatus) && WEXITSTATUS(childExitStatus) != 0) + return buildError(500, "Internal Server Error"); + + if (WIFSIGNALED(childExitStatus)) + return buildError(500, "Internal Server Error"); - if (WIFEXITED(status) && WEXITSTATUS(status) != 0) + if (cgiOutput.empty()) return buildError(500, "Internal Server Error"); - return parseOutput(output); + return parseOutput(cgiOutput); } diff --git a/src/http/cgi/output.cpp b/src/http/cgi/output.cpp index 1c4d234..4346144 100644 --- a/src/http/cgi/output.cpp +++ b/src/http/cgi/output.cpp @@ -1,84 +1,89 @@ #include "../../../include/http/CgiHandler.hpp" #include -HttpResponse CgiHandler::parseOutput(const std::string& raw) +HttpResponse CgiHandler::parseOutput(const std::string& cgiOutput) { - HttpResponse res; + HttpResponse response; - std::size_t sep = raw.find("\r\n\r\n"); - if (sep == std::string::npos) - sep = raw.find("\n\n"); - if (sep == std::string::npos) + std::size_t separatorPos = cgiOutput.find("\r\n\r\n"); + if (separatorPos == std::string::npos) + separatorPos = cgiOutput.find("\n\n"); + + if (separatorPos == std::string::npos) { - res.status_code = 200; - res.status_msg = "OK"; - res.body = raw; - res.headers["Content-Type"] = "text/html"; - std::ostringstream ss; - ss << res.body.size(); - res.headers["Content-Length"] = ss.str(); - return res; + std::ostringstream contentLengthStream; + response.status_code = 200; + response.status_msg = "OK"; + response.body = cgiOutput; + response.headers["Content-Type"] = "text/html"; + contentLengthStream << response.body.size(); + response.headers["Content-Length"] = contentLengthStream.str(); + return response; } - std::string headersPart = raw.substr(0, sep); - std::size_t bodyStart = sep + (raw[sep + 1] == '\n' ? 2 : 4); - res.body = raw.substr(bodyStart); + bool usesDoubleCRLF = (cgiOutput[separatorPos + 1] != '\n'); + std::size_t bodyStart = separatorPos + (usesDoubleCRLF ? 4 : 2); + + std::string headersSection = cgiOutput.substr(0, separatorPos); + response.body = cgiOutput.substr(bodyStart); + response.status_code = 200; + response.status_msg = "OK"; - res.status_code = 200; - res.status_msg = "OK"; + std::istringstream headerStream(headersSection); + std::string headerLine; - std::istringstream stream(headersPart); - std::string line; - while (std::getline(stream, line)) + while (std::getline(headerStream, headerLine)) { - if (!line.empty() && line[line.size() - 1] == '\r') - line.erase(line.size() - 1); - if (line.empty()) + if (!headerLine.empty() && headerLine[headerLine.size() - 1] == '\r') + headerLine.erase(headerLine.size() - 1); + if (headerLine.empty()) continue; - std::size_t colon = line.find(':'); - if (colon == std::string::npos) + std::size_t colonPos = headerLine.find(':'); + if (colonPos == std::string::npos) continue; - std::string key = line.substr(0, colon); - std::string value = line.substr(colon + 1); - std::size_t vs = 0; - while (vs < value.size() && value[vs] == ' ') - vs++; - value = value.substr(vs); + std::string headerKey = headerLine.substr(0, colonPos); + std::string headerValue = headerLine.substr(colonPos + 1); + + std::size_t valueStart = 0; + while (valueStart < headerValue.size() && headerValue[valueStart] == ' ') + valueStart++; + headerValue = headerValue.substr(valueStart); - if (key == "Status") + if (headerKey == "Status") { - std::istringstream ss(value); - ss >> res.status_code; - std::size_t sp = value.find(' '); - if (sp != std::string::npos) - res.status_msg = value.substr(sp + 1); + std::istringstream statusStream(headerValue); + statusStream >> response.status_code; + + std::size_t statusSpacePos = headerValue.find(' '); + if (statusSpacePos != std::string::npos) + response.status_msg = headerValue.substr(statusSpacePos + 1); } else - res.headers[key] = value; + response.headers[headerKey] = headerValue; } - if (res.headers.find("Content-Type") == res.headers.end()) - res.headers["Content-Type"] = "text/html"; + if (response.headers.find("Content-Type") == response.headers.end()) + response.headers["Content-Type"] = "text/html"; - std::ostringstream ss; - ss << res.body.size(); - res.headers["Content-Length"] = ss.str(); + std::ostringstream contentLengthStream; + contentLengthStream << response.body.size(); + response.headers["Content-Length"] = contentLengthStream.str(); - return res; + return response; } -HttpResponse CgiHandler::buildError(int code, const std::string& msg) +HttpResponse CgiHandler::buildError(int statusCode, const std::string& statusMessage) { - HttpResponse res; - std::ostringstream ss; - - res.status_code = code; - res.status_msg = msg; - res.body = "

" + msg + "

"; - res.headers["Content-Type"] = "text/html"; - ss << res.body.size(); - res.headers["Content-Length"] = ss.str(); - return res; + HttpResponse response; + std::ostringstream contentLengthStream; + + response.status_code = statusCode; + response.status_msg = statusMessage; + response.body = "

" + statusMessage + "

"; + response.headers["Content-Type"] = "text/html"; + contentLengthStream << response.body.size(); + response.headers["Content-Length"] = contentLengthStream.str(); + return response; } diff --git a/src/http/handlers/DeleteHandler.cpp b/src/http/handlers/DeleteHandler.cpp index f11d7d2..66cb402 100644 --- a/src/http/handlers/DeleteHandler.cpp +++ b/src/http/handlers/DeleteHandler.cpp @@ -2,26 +2,23 @@ #include #include -HttpResponse MethodHandler::handleDelete(const HttpRequest& req, const LocationConfig& loc) +HttpResponse MethodHandler::handleDelete(const HttpRequest& request, const LocationConfig& location) { - std::string path = loc.getRoot() + req.uri; + std::string filePath = location.getRoot() + request.uri; - if (req.uri.find("..") != std::string::npos) - return buildError(400, "Bad Request"); - - struct stat st; - if (stat(path.c_str(), &st) == -1) + struct stat fileInfo; + if (stat(filePath.c_str(), &fileInfo) == -1) return buildError(404, "Not Found"); - if (S_ISDIR(st.st_mode)) + if (S_ISDIR(fileInfo.st_mode)) return buildError(403, "Forbidden"); - if (unlink(path.c_str()) == 0) + if (unlink(filePath.c_str()) == 0) { - HttpResponse res; - res.status_code = 204; - res.status_msg = "No Content"; - return res; + HttpResponse response; + response.status_code = 204; + response.status_msg = "No Content"; + return response; } return buildError(403, "Forbidden"); } diff --git a/src/http/handlers/GetHandler.cpp b/src/http/handlers/GetHandler.cpp index 1540c46..37e4c99 100644 --- a/src/http/handlers/GetHandler.cpp +++ b/src/http/handlers/GetHandler.cpp @@ -5,98 +5,99 @@ #include #include -static std::string getContentType(const std::string& path) +static std::string getContentType(const std::string& filePath) { - std::string ext; - std::size_t dot = path.rfind('.'); - if (dot != std::string::npos) - ext = path.substr(dot); + std::string fileExtension; + std::size_t dotPosition = filePath.rfind('.'); + if (dotPosition != std::string::npos) + fileExtension = filePath.substr(dotPosition); - if (ext == ".html" || ext == ".htm") return "text/html"; - if (ext == ".css") return "text/css"; - if (ext == ".js") return "application/javascript"; - if (ext == ".json") return "application/json"; - if (ext == ".png") return "image/png"; - if (ext == ".jpg" || ext == ".jpeg") return "image/jpeg"; - if (ext == ".gif") return "image/gif"; - if (ext == ".ico") return "image/x-icon"; - if (ext == ".txt") return "text/plain"; + if (fileExtension == ".html" || fileExtension == ".htm") return "text/html"; + if (fileExtension == ".css") return "text/css"; + if (fileExtension == ".js") return "application/javascript"; + if (fileExtension == ".json") return "application/json"; + if (fileExtension == ".png") return "image/png"; + if (fileExtension == ".jpg" || fileExtension == ".jpeg") return "image/jpeg"; + if (fileExtension == ".gif") return "image/gif"; + if (fileExtension == ".ico") return "image/x-icon"; + if (fileExtension == ".txt") return "text/plain"; return "application/octet-stream"; } -static HttpResponse buildAutoindex(const std::string& path, const std::string& uri) +static HttpResponse buildAutoindex(const std::string& directoryPath, const std::string& requestUri) { - DIR* dir = opendir(path.c_str()); - if (!dir) + DIR* directory = opendir(directoryPath.c_str()); + if (!directory) { - HttpResponse res; - res.status_code = 403; - res.status_msg = "Forbidden"; - return res; + HttpResponse response; + response.status_code = 403; + response.status_msg = "Forbidden"; + return response; } - std::string html = "

Index of " + uri + "

    "; - struct dirent* entry; - while ((entry = readdir(dir)) != NULL) + std::string listingHtml = "

    Index of " + requestUri + "

      "; + + struct dirent* dirEntry; + while ((dirEntry = readdir(directory)) != NULL) { - std::string name = entry->d_name; - std::string href = uri; - if (href[href.size() - 1] != '/') - href += '/'; - href += name; - html += "
    • " + name + "
    • "; + std::string entryName = dirEntry->d_name; + std::string entryLink = requestUri; + if (entryLink[entryLink.size() - 1] != '/') + entryLink += '/'; + entryLink += entryName; + listingHtml += "
    • " + entryName + "
    • "; } - closedir(dir); - html += "
    "; + closedir(directory); + listingHtml += "
"; - HttpResponse res; - std::ostringstream ss; - res.status_code = 200; - res.status_msg = "OK"; - res.body = html; - res.headers["Content-Type"] = "text/html"; - ss << html.size(); - res.headers["Content-Length"] = ss.str(); - return res; + HttpResponse response; + std::ostringstream contentLength; + response.status_code = 200; + response.status_msg = "OK"; + response.body = listingHtml; + response.headers["Content-Type"] = "text/html"; + contentLength << listingHtml.size(); + response.headers["Content-Length"] = contentLength.str(); + return response; } -HttpResponse MethodHandler::handleGet(const HttpRequest& req, const LocationConfig& loc) +HttpResponse MethodHandler::handleGet(const HttpRequest& request, const LocationConfig& location) { - std::string path = loc.getRoot() + req.uri; + std::string filePath = location.getRoot() + request.uri; - struct stat st; - if (stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode)) + struct stat fileInfo; + if (stat(filePath.c_str(), &fileInfo) == 0 && S_ISDIR(fileInfo.st_mode)) { - if (!loc.getIndex().empty()) + if (!location.getIndex().empty()) { - if (path[path.size() - 1] != '/') - path += '/'; - path += loc.getIndex(); + if (filePath[filePath.size() - 1] != '/') + filePath += '/'; + filePath += location.getIndex(); } - else if (loc.getAutoindex()) - return buildAutoindex(path, req.uri); + else if (location.getAutoindex()) + return buildAutoindex(filePath, request.uri); else return buildError(403, "Forbidden"); } - int fd = open(path.c_str(), O_RDONLY); - if (fd == -1) + int fileDescriptor = open(filePath.c_str(), O_RDONLY); + if (fileDescriptor == -1) return buildError(404, "Not Found"); - HttpResponse res; - char buf[4096]; - ssize_t n; + HttpResponse response; + char readBuffer[4096]; + ssize_t bytesRead; - while ((n = read(fd, buf, sizeof(buf))) > 0) - res.body.append(buf, n); - close(fd); + while ((bytesRead = read(fileDescriptor, readBuffer, sizeof(readBuffer))) > 0) + response.body.append(readBuffer, bytesRead); + close(fileDescriptor); - res.status_code = 200; - res.status_msg = "OK"; - res.headers["Content-Type"] = getContentType(path); + response.status_code = 200; + response.status_msg = "OK"; + response.headers["Content-Type"] = getContentType(filePath); - std::ostringstream ss; - ss << res.body.size(); - res.headers["Content-Length"] = ss.str(); - return res; + std::ostringstream contentLength; + contentLength << response.body.size(); + response.headers["Content-Length"] = contentLength.str(); + return response; } diff --git a/src/http/handlers/MethodHandler.cpp b/src/http/handlers/MethodHandler.cpp index 907ba52..64dc638 100644 --- a/src/http/handlers/MethodHandler.cpp +++ b/src/http/handlers/MethodHandler.cpp @@ -3,68 +3,124 @@ #include #include -static bool isCgiRequest(const HttpRequest& req, const LocationConfig& loc) +static std::string urlDecode(const std::string& encoded) { - if (loc.getCgiExtension().empty() || loc.getCgiPath().empty()) + std::string decoded; + + for (std::size_t pos = 0; pos < encoded.size(); ++pos) + { + bool isPercentSign = encoded[pos] == '%'; + bool hasTwoCharAfter = pos + 2 < encoded.size(); + + if (!isPercentSign || !hasTwoCharAfter) + { + decoded += encoded[pos]; + continue; + } + + char hexSequence[3] = { encoded[pos + 1], encoded[pos + 2], '\0' }; + char* parseEnd; + int decodedChar = std::strtol(hexSequence, &parseEnd, 16); + + bool validHex = (parseEnd == hexSequence + 2); + if (!validHex) + { + decoded += encoded[pos]; + continue; + } + + decoded += static_cast(decodedChar); + pos += 2; + } + + return decoded; +} + +static bool hasPathTraversal(const std::string& uri) +{ + std::string decodedUri = urlDecode(uri); + bool containsDotDot = decodedUri.find("..") != std::string::npos; + + return containsDotDot; +} + +static bool isCgiRequest(const HttpRequest& request, const LocationConfig& location) +{ + bool noCgiConfigured = location.getCgiExtension().empty() || location.getCgiPath().empty(); + if (noCgiConfigured) return false; - std::string uri = req.uri; - std::size_t q = uri.find('?'); - if (q != std::string::npos) - uri = uri.substr(0, q); - const std::string& ext = loc.getCgiExtension(); - if (uri.size() < ext.size()) + + std::string uriWithoutQuery = request.uri; + std::size_t queryStart = uriWithoutQuery.find('?'); + if (queryStart != std::string::npos) + uriWithoutQuery = uriWithoutQuery.substr(0, queryStart); + + const std::string& cgiExtension = location.getCgiExtension(); + bool uriTooShort = uriWithoutQuery.size() < cgiExtension.size(); + if (uriTooShort) return false; - return uri.substr(uri.size() - ext.size()) == ext; + + std::string uriExtension = uriWithoutQuery.substr(uriWithoutQuery.size() - cgiExtension.size()); + return uriExtension == cgiExtension; } -HttpResponse MethodHandler::handle(const HttpRequest& req, const LocationConfig& loc, const ServerConfig& server) +HttpResponse MethodHandler::handle(const HttpRequest& request, const LocationConfig& location, const ServerConfig& server) { - (void)server; + if (hasPathTraversal(request.uri)) + return buildError(400, "Bad Request"); + + if (location.getPath().empty()) + return buildError(404, "Not Found"); - if (!isMethodAllowed(req.method, loc)) + if (!isMethodAllowed(request.method, location)) return buildError(405, "Method Not Allowed"); - if (!loc.getRedirectUrl().empty()) + bool bodyLimitIsSet = server.getMaxBodySize() > 0; + bool bodyExceedsLimit = request.body.size() > server.getMaxBodySize(); + if (bodyLimitIsSet && bodyExceedsLimit) + return buildError(413, "Payload Too Large"); + + if (!location.getRedirectUrl().empty()) { - HttpResponse res; - res.status_code = 301; - res.status_msg = "Moved Permanently"; - res.headers["Location"] = loc.getRedirectUrl(); - return res; + HttpResponse redirectResponse; + redirectResponse.status_code = 301; + redirectResponse.status_msg = "Moved Permanently"; + redirectResponse.headers["Location"] = location.getRedirectUrl(); + return redirectResponse; } - if (isCgiRequest(req, loc)) + if (isCgiRequest(request, location)) { - CgiHandler cgi; - return cgi.execute(req, loc); + CgiHandler cgiHandler; + return cgiHandler.execute(request, location); } - if (req.method == "GET") - return handleGet(req, loc); - if (req.method == "POST") - return handlePost(req, loc); - if (req.method == "DELETE") - return handleDelete(req, loc); + if (request.method == "GET") + return handleGet(request, location); + if (request.method == "POST") + return handlePost(request, location); + if (request.method == "DELETE") + return handleDelete(request, location); return buildError(405, "Method Not Allowed"); } -bool MethodHandler::isMethodAllowed(const std::string& method, const LocationConfig& loc) +bool MethodHandler::isMethodAllowed(const std::string& method, const LocationConfig& location) { - const std::vector& methods = loc.getAllowedMethods(); - return std::find(methods.begin(), methods.end(), method) != methods.end(); + const std::vector& allowedMethods = location.getAllowedMethods(); + return std::find(allowedMethods.begin(), allowedMethods.end(), method) != allowedMethods.end(); } -HttpResponse MethodHandler::buildError(int code, const std::string& msg) +HttpResponse MethodHandler::buildError(int statusCode, const std::string& statusMessage) { - HttpResponse res; - std::ostringstream ss; - - res.status_code = code; - res.status_msg = msg; - res.body = "

" + msg + "

"; - res.headers["Content-Type"] = "text/html"; - ss << res.body.size(); - res.headers["Content-Length"] = ss.str(); - return res; + HttpResponse response; + std::ostringstream contentLength; + + response.status_code = statusCode; + response.status_msg = statusMessage; + response.body = "

" + statusMessage + "

"; + response.headers["Content-Type"] = "text/html"; + contentLength << response.body.size(); + response.headers["Content-Length"] = contentLength.str(); + return response; } diff --git a/src/http/handlers/PostHandler.cpp b/src/http/handlers/PostHandler.cpp index b4b785c..fcd364d 100644 --- a/src/http/handlers/PostHandler.cpp +++ b/src/http/handlers/PostHandler.cpp @@ -5,60 +5,53 @@ static std::string extractFilename(const std::string& uri) { - std::size_t slash = uri.rfind('/'); - if (slash == std::string::npos || slash + 1 >= uri.size()) + std::size_t lastSlashPosition = uri.rfind('/'); + if (lastSlashPosition == std::string::npos || lastSlashPosition + 1 >= uri.size()) return ""; - return uri.substr(slash + 1); + return uri.substr(lastSlashPosition + 1); } -static bool hasPathTraversal(const std::string& uri) +HttpResponse MethodHandler::handlePost(const HttpRequest& request, const LocationConfig& location) { - return uri.find("..") != std::string::npos; -} - -HttpResponse MethodHandler::handlePost(const HttpRequest& req, const LocationConfig& loc) -{ - if (loc.getUploadPath().empty()) + if (location.getUploadPath().empty()) return buildError(500, "Internal Server Error"); - if (req.body.empty()) + if (request.body.empty()) return buildError(400, "Bad Request"); - if (hasPathTraversal(req.uri)) - return buildError(400, "Bad Request"); - - std::string filename = extractFilename(req.uri); + std::string filename = extractFilename(request.uri); if (filename.empty()) return buildError(400, "Bad Request"); - std::string path = loc.getUploadPath() + "/" + filename; + std::string destinationPath = location.getUploadPath() + "/" + filename; - int fd = open(path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644); - if (fd == -1) + int fileDescriptor = open(destinationPath.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fileDescriptor == -1) return buildError(403, "Forbidden"); - const char* data = req.body.data(); - std::size_t total = req.body.size(); - std::size_t written = 0; + const char* bodyData = request.body.data(); + std::size_t totalBytesToWrite = request.body.size(); + std::size_t totalBytesWritten = 0; - while (written < total) + while (totalBytesWritten < totalBytesToWrite) { - ssize_t n = write(fd, data + written, total - written); - if (n <= 0) + ssize_t bytesWritten = write(fileDescriptor, bodyData + totalBytesWritten, + totalBytesToWrite - totalBytesWritten); + if (bytesWritten <= 0) { - close(fd); + close(fileDescriptor); return buildError(507, "Insufficient Storage"); } - written += n; + totalBytesWritten += bytesWritten; } - close(fd); - - HttpResponse res; - res.status_code = 201; - res.status_msg = "Created"; - res.headers["Location"] = "/" + filename; - std::ostringstream ss; - ss << res.body.size(); - res.headers["Content-Length"] = ss.str(); - return res; + close(fileDescriptor); + + HttpResponse response; + std::ostringstream contentLength; + response.status_code = 201; + response.status_msg = "Created"; + response.headers["Location"] = "/" + filename; + contentLength << response.body.size(); + response.headers["Content-Length"] = contentLength.str(); + return response; } diff --git a/src/http/parser/RequestParser.cpp b/src/http/parser/RequestParser.cpp index 70d2d41..f166451 100644 --- a/src/http/parser/RequestParser.cpp +++ b/src/http/parser/RequestParser.cpp @@ -1,102 +1,107 @@ #include "../../../include/http/RequestParser.hpp" #include -HttpRequest RequestParser::parse(const std::string& raw) +HttpRequest RequestParser::parse(const std::string& rawRequest) { - HttpRequest req; + HttpRequest request; - if (!isValid(raw)) - return req; + if (!isValid(rawRequest)) + return request; - std::size_t firstLineEnd = raw.find("\r\n"); - std::size_t headerBodySep = raw.find("\r\n\r\n"); + std::size_t firstLineEnd = rawRequest.find("\r\n"); + std::size_t headerBodySeparator = rawRequest.find("\r\n\r\n"); - parseFirstLine(raw.substr(0, firstLineEnd), req); - parseHeaders(raw, req, firstLineEnd, headerBodySep); - parseBody(raw, req, headerBodySep); - return req; + parseFirstLine(rawRequest.substr(0, firstLineEnd), request); + parseHeaders(rawRequest, request, firstLineEnd, headerBodySeparator); + parseBody(rawRequest, request, headerBodySeparator); + return request; } -bool RequestParser::isValid(const std::string& raw) +bool RequestParser::isValid(const std::string& rawRequest) { - if (raw.find("\r\n\r\n") == std::string::npos) + if (rawRequest.find("\r\n\r\n") == std::string::npos) return false; - std::size_t firstLineEnd = raw.find("\r\n"); - std::size_t headerBodySep = raw.find("\r\n\r\n"); - std::string firstLine = raw.substr(0, firstLineEnd); + std::size_t firstLineEnd = rawRequest.find("\r\n"); + std::size_t headerBodySeparator = rawRequest.find("\r\n\r\n"); + std::string firstLine = rawRequest.substr(0, firstLineEnd); - std::istringstream ss(firstLine); + std::istringstream firstLineStream(firstLine); std::string method, uri, version; - if (!(ss >> method >> uri >> version)) + if (!(firstLineStream >> method >> uri >> version)) return false; - std::string extra; - if (ss >> extra) + std::string extraToken; + if (firstLineStream >> extraToken) return false; if (version.substr(0, 5) != "HTTP/") return false; - bool hasHost = false; - std::size_t start = firstLineEnd + 2; - while (start < headerBodySep) + bool hasHostHeader = false; + std::size_t currentPosition = firstLineEnd + 2; + + while (currentPosition < headerBodySeparator) { - std::size_t lineEnd = raw.find("\r\n", start); - std::string line = raw.substr(start, lineEnd - start); + std::size_t lineEndPosition = rawRequest.find("\r\n", currentPosition); + std::string currentLine = rawRequest.substr(currentPosition, lineEndPosition - currentPosition); - std::size_t colon = line.find(':'); - if (colon != std::string::npos) + std::size_t colonPosition = currentLine.find(':'); + if (colonPosition != std::string::npos) { - if (colon + 1 < line.size() && line[colon + 1] == '\t') + bool hasTabAfterColon = (colonPosition + 1 < currentLine.size() + && currentLine[colonPosition + 1] == '\t'); + if (hasTabAfterColon) return false; - if (line.substr(0, colon) == "Host") - hasHost = true; + if (currentLine.substr(0, colonPosition) == "Host") + hasHostHeader = true; } - start = lineEnd + 2; + currentPosition = lineEndPosition + 2; } - if (version == "HTTP/1.1" && !hasHost) + if (version == "HTTP/1.1" && !hasHostHeader) return false; return true; } -void RequestParser::parseFirstLine(const std::string& line, HttpRequest& req) +void RequestParser::parseFirstLine(const std::string& firstLine, HttpRequest& request) { - std::istringstream ss(line); - ss >> req.method; - ss >> req.uri; - ss >> req.version; + std::istringstream lineStream(firstLine); + lineStream >> request.method; + lineStream >> request.uri; + lineStream >> request.version; } -void RequestParser::parseHeaders(const std::string& raw, HttpRequest& req, std::size_t firstLineEnd, std::size_t headerBodySep) +void RequestParser::parseHeaders(const std::string& rawRequest, HttpRequest& request, + std::size_t firstLineEnd, std::size_t headerBodySeparator) { - std::size_t start = firstLineEnd + 2; - std::size_t end = headerBodySep; + std::size_t currentPosition = firstLineEnd + 2; + std::size_t headersEnd = headerBodySeparator; - while (start < end) + while (currentPosition < headersEnd) { - std::size_t lineEnd = raw.find("\r\n", start); - std::string line = raw.substr(start, lineEnd - start); + std::size_t lineEndPosition = rawRequest.find("\r\n", currentPosition); + std::string currentLine = rawRequest.substr(currentPosition, lineEndPosition - currentPosition); - std::size_t colon = line.find(':'); - if (colon != std::string::npos) + std::size_t colonPosition = currentLine.find(':'); + if (colonPosition != std::string::npos) { - std::string key = line.substr(0, colon); - std::string value; - std::size_t valStart = colon + 1; - while (valStart < line.size() && line[valStart] == ' ') - valStart++; - if (valStart < line.size()) - value = line.substr(valStart); - req.headers[key] = value; + std::string headerKey = currentLine.substr(0, colonPosition); + std::string headerValue; + std::size_t valueStart = colonPosition + 1; + while (valueStart < currentLine.size() && currentLine[valueStart] == ' ') + valueStart++; + if (valueStart < currentLine.size()) + headerValue = currentLine.substr(valueStart); + request.headers[headerKey] = headerValue; } - start = lineEnd + 2; + currentPosition = lineEndPosition + 2; } } -void RequestParser::parseBody(const std::string& raw, HttpRequest& req, std::size_t headerBodySep) +void RequestParser::parseBody(const std::string& rawRequest, HttpRequest& request, + std::size_t headerBodySeparator) { - req.body = raw.substr(headerBodySep + 4); + request.body = rawRequest.substr(headerBodySeparator + 4); } diff --git a/src/http/response/ResponseBuilder.cpp b/src/http/response/ResponseBuilder.cpp index 87af793..7b9be6d 100644 --- a/src/http/response/ResponseBuilder.cpp +++ b/src/http/response/ResponseBuilder.cpp @@ -1,18 +1,18 @@ #include "../../../include/http/ResponseBuilder.hpp" #include -std::string ResponseBuilder::build(const HttpResponse& res) +std::string ResponseBuilder::build(const HttpResponse& response) { - std::ostringstream out; + std::ostringstream rawOutput; - out << "HTTP/1.1 " << res.status_code << " " << res.status_msg << "\r\n"; + rawOutput << "HTTP/1.1 " << response.status_code << " " << response.status_msg << "\r\n"; - std::map::const_iterator it; - for (it = res.headers.begin(); it != res.headers.end(); ++it) - out << it->first << ": " << it->second << "\r\n"; + std::map::const_iterator headerIt; + for (headerIt = response.headers.begin(); headerIt != response.headers.end(); ++headerIt) + rawOutput << headerIt->first << ": " << headerIt->second << "\r\n"; - out << "\r\n"; - out << res.body; + rawOutput << "\r\n"; + rawOutput << response.body; - return out.str(); + return rawOutput.str(); } diff --git a/src/http/router/Router.cpp b/src/http/router/Router.cpp index 9560200..ba1026a 100644 --- a/src/http/router/Router.cpp +++ b/src/http/router/Router.cpp @@ -1,32 +1,39 @@ #include "../../../include/http/Router.hpp" -static bool matches(const std::string& locationPath, const std::string& uri) +static bool matchesLocation(const std::string& locationPath, const std::string& requestUri) { - if (locationPath.size() > uri.size()) + if (locationPath.size() > requestUri.size()) return false; - if (uri.substr(0, locationPath.size()) != locationPath) + if (requestUri.substr(0, locationPath.size()) != locationPath) return false; - if (locationPath.size() < uri.size() && uri[locationPath.size()] != '/' && locationPath != "/") + + bool uriContinuesAfterPrefix = locationPath.size() < requestUri.size(); + bool nextCharIsSlash = requestUri[locationPath.size()] == '/'; + bool locationIsRoot = locationPath == "/"; + + if (uriContinuesAfterPrefix && !nextCharIsSlash && !locationIsRoot) return false; + return true; } -LocationConfig Router::route(const HttpRequest& req, const ServerConfig& server) +LocationConfig Router::route(const HttpRequest& request, const ServerConfig& server) { - const std::vector& locs = server.getLocations(); + const std::vector& locations = server.getLocations(); - LocationConfig best; - std::size_t bestLen = 0; + LocationConfig bestMatch; + std::size_t longestMatchLength = 0; - for (std::vector::const_iterator it = locs.begin(); it != locs.end(); ++it) + for (std::vector::const_iterator locationIt = locations.begin(); + locationIt != locations.end(); ++locationIt) { - const std::string& path = it->getPath(); + const std::string& locationPath = locationIt->getPath(); - if (matches(path, req.uri) && path.size() > bestLen) + if (matchesLocation(locationPath, request.uri) && locationPath.size() > longestMatchLength) { - best = *it; - bestLen = path.size(); + bestMatch = *locationIt; + longestMatchLength = locationPath.size(); } } - return best; + return bestMatch; } diff --git a/tests/http/integration/test_adversarial.cpp b/tests/http/integration/test_adversarial.cpp new file mode 100644 index 0000000..f2ba458 --- /dev/null +++ b/tests/http/integration/test_adversarial.cpp @@ -0,0 +1,336 @@ +#include "../../../include/http/RequestParser.hpp" +#include "../../../include/http/Router.hpp" +#include "../../../include/http/MethodHandler.hpp" +#include "../../../include/http/ResponseBuilder.hpp" +#include +#include +#include +#include + +static int passed = 0; +static int failed = 0; + +static void check(const std::string& label, bool condition) +{ + if (condition) { std::cout << "[OK] " << label << std::endl; passed++; } + else { std::cout << "[KO] " << label << std::endl; failed++; } +} + +static HttpRequest makeReq(const std::string& method, const std::string& uri, + const std::string& body = "", const std::string& host = "localhost") +{ + HttpRequest req; + req.method = method; + req.uri = uri; + req.version = "HTTP/1.1"; + req.headers["Host"] = host; + if (!body.empty()) + { + req.body = body; + req.headers["Content-Type"] = "text/plain"; + } + return req; +} + +static LocationConfig makeGetLoc(const std::string& root = "www") +{ + LocationConfig loc; + loc.setPath("/"); + loc.setRoot(root); + loc.setIndex("index.html"); + loc.addMethod("GET"); + loc.addMethod("POST"); + loc.addMethod("DELETE"); + return loc; +} + +static LocationConfig makeUploadLoc() +{ + LocationConfig loc; + loc.setPath("/uploads"); + loc.setRoot("/tmp"); + loc.setUploadPath("/tmp"); + loc.addMethod("POST"); + loc.addMethod("DELETE"); + loc.addMethod("GET"); + return loc; +} + +int main() +{ + ServerConfig server; + MethodHandler handler; + ResponseBuilder builder; + + // ════════════════════════════════════════════════════════════ + std::cout << "\n── PARSING ATTACKS ─────────────────────────────────────\n"; + + // requête vide + { + RequestParser p; + HttpRequest r = p.parse(""); + check("parse: string vide → method vide", r.method.empty()); + } + + // pas de \r\n\r\n + { + RequestParser p; + HttpRequest r = p.parse("GET / HTTP/1.1\r\nHost: x"); + check("parse: pas de separateur → rejeté", r.method.empty()); + } + + // méthode avec espace + { + RequestParser p; + HttpRequest r = p.parse("G ET / HTTP/1.1\r\nHost: x\r\n\r\n"); + check("parse: methode avec espace → rejeté", r.method.empty()); + } + + // trop de tokens sur la première ligne + { + RequestParser p; + HttpRequest r = p.parse("GET / HTTP/1.1 EXTRA\r\nHost: x\r\n\r\n"); + check("parse: trop de tokens ligne 1 → rejeté", r.method.empty()); + } + + // tab après colon (Nginx rejette) + { + RequestParser p; + HttpRequest r = p.parse("GET / HTTP/1.1\r\nHost:\tlocalhost\r\n\r\n"); + check("parse: tab apres colon → rejeté", r.method.empty()); + } + + // HTTP/1.1 sans Host + { + RequestParser p; + HttpRequest r = p.parse("GET / HTTP/1.1\r\nAccept: *\r\n\r\n"); + check("parse: HTTP/1.1 sans Host → rejeté", r.method.empty()); + } + + // version invalide + { + RequestParser p; + HttpRequest r = p.parse("GET / HTTPS/1.1\r\nHost: x\r\n\r\n"); + check("parse: version invalide → rejeté", r.method.empty()); + } + + // header avec null byte dans la valeur (survit au parsing sans crash) + { + RequestParser p; + std::string raw = "GET / HTTP/1.1\r\nHost: local\0host\r\n\r\n"; + raw[raw.find('\0')] = 'x'; // on peut pas tester null byte proprement en C++ + HttpRequest r = p.parse(raw); + check("parse: header avec null → pas de crash", true); + } + + // URI très longue (8KB) + { + RequestParser p; + std::string uri(8192, 'a'); + std::string raw = "GET /" + uri + " HTTP/1.1\r\nHost: x\r\n\r\n"; + HttpRequest r = p.parse(raw); + check("parse: URI 8KB → pas de crash", true); + } + + // beaucoup de headers + { + RequestParser p; + std::string raw = "GET / HTTP/1.1\r\nHost: x\r\n"; + for (int i = 0; i < 100; i++) { + std::ostringstream ss; ss << i; + raw += "X-Header-" + ss.str() + ": value\r\n"; + } + raw += "\r\n"; + HttpRequest r = p.parse(raw); + check("parse: 100 headers → pas de crash", !r.method.empty()); + } + + // body énorme (1MB) + { + RequestParser p; + std::string body(1024 * 1024, 'X'); + std::ostringstream cl; cl << body.size(); + std::string raw = "POST / HTTP/1.1\r\nHost: x\r\nContent-Length: " + cl.str() + "\r\n\r\n" + body; + HttpRequest r = p.parse(raw); + check("parse: body 1MB → pas de crash", !r.method.empty()); + check("parse: body 1MB → body correct", r.body.size() == body.size()); + } + + // ════════════════════════════════════════════════════════════ + std::cout << "\n── PATH TRAVERSAL ATTACKS ──────────────────────────────\n"; + + { + LocationConfig loc = makeGetLoc(); + server.addLocation(loc); + + std::string traversals[] = { + "/../etc/passwd", + "/../../etc/shadow", + "/../../../etc/hosts", + "/uploads/../../etc/passwd", + "/./././../etc/passwd", + "/%2e%2e/etc/passwd", + "/..%2fetc%2fpasswd", + "/%2e%2e%2fetc%2fpasswd" + }; + + for (int i = 0; i < 8; i++) + { + HttpResponse res = handler.handle(makeReq("GET", traversals[i]), loc, server); + check("GET traversal: " + traversals[i].substr(0, 30) + " → pas 200", + res.status_code != 200); + } + } + + // POST path traversal sur upload + { + LocationConfig loc = makeUploadLoc(); + std::string traversals[] = { + "/uploads/../../../etc/cron", + "/uploads/../../tmp/evil", + "/uploads/../passwd" + }; + for (int i = 0; i < 3; i++) + { + HttpResponse res = handler.handle(makeReq("POST", traversals[i], "evil"), loc, server); + check("POST traversal: " + traversals[i].substr(0, 35) + " → 400", + res.status_code == 400); + } + } + + // DELETE path traversal + { + LocationConfig loc = makeGetLoc("/tmp"); + std::string traversals[] = { + "/../etc/passwd", + "/../../root/.ssh/authorized_keys", + "/../tmp/../etc/hostname" + }; + for (int i = 0; i < 3; i++) + { + HttpResponse res = handler.handle(makeReq("DELETE", traversals[i]), loc, server); + check("DELETE traversal → 400", res.status_code == 400); + } + } + + // ════════════════════════════════════════════════════════════ + std::cout << "\n── METHOD ATTACKS ──────────────────────────────────────\n"; + + { + LocationConfig loc = makeGetLoc(); + std::string methods[] = { "PUT", "PATCH", "OPTIONS", "HEAD", "TRACE", "CONNECT", "FOOBAR" }; + for (int i = 0; i < 7; i++) + { + HttpResponse res = handler.handle(makeReq(methods[i], "/index.html"), loc, server); + check("methode inconnue " + methods[i] + " → 405", res.status_code == 405); + } + } + + // ════════════════════════════════════════════════════════════ + std::cout << "\n── RESPONSE BUILDER ATTACKS ────────────────────────────\n"; + + // headers avec valeurs vides + { + HttpResponse res; + res.status_code = 200; + res.status_msg = "OK"; + res.headers["Content-Type"] = ""; + res.headers["X-Empty"] = ""; + res.body = "test"; + std::string raw = builder.build(res); + check("builder: headers vides → pas de crash", !raw.empty()); + check("builder: contient status line", raw.find("HTTP/1.1 200") != std::string::npos); + } + + // status message avec caractères spéciaux + { + HttpResponse res; + res.status_code = 418; + res.status_msg = "I'm a teapot!"; + res.body = ""; + std::string raw = builder.build(res); + check("builder: status message special → correct", raw.find("418") != std::string::npos); + } + + // body avec null bytes + { + HttpResponse res; + res.status_code = 200; + res.status_msg = "OK"; + res.body = std::string("hel\0lo", 6); + res.headers["Content-Length"] = "6"; + std::string raw = builder.build(res); + check("builder: body avec null bytes → taille correcte", raw.size() >= 6); + } + + // ════════════════════════════════════════════════════════════ + std::cout << "\n── ROUTER EDGE CASES ───────────────────────────────────\n"; + + { + ServerConfig srv; + LocationConfig loc1; loc1.setPath("/api"); loc1.setRoot("/a"); loc1.addMethod("GET"); + LocationConfig loc2; loc2.setPath("/api/v1"); loc2.setRoot("/b"); loc2.addMethod("GET"); + LocationConfig loc3; loc3.setPath("/api/v1/users"); loc3.setRoot("/c"); loc3.addMethod("GET"); + srv.addLocation(loc1); + srv.addLocation(loc2); + srv.addLocation(loc3); + + Router router; + + HttpRequest r1; r1.uri = "/api/v1/users/123"; + check("router: /api/v1/users/123 → /api/v1/users", router.route(r1, srv).getRoot() == "/c"); + + HttpRequest r2; r2.uri = "/api/v1/settings"; + check("router: /api/v1/settings → /api/v1", router.route(r2, srv).getRoot() == "/b"); + + HttpRequest r3; r3.uri = "/apiv2/test"; + check("router: /apiv2 ne match pas /api → vide", router.route(r3, srv).getPath().empty()); + + HttpRequest r4; r4.uri = "/API/v1"; + check("router: case sensitive → vide", router.route(r4, srv).getPath().empty()); + } + + // ════════════════════════════════════════════════════════════ + std::cout << "\n── FULL PIPELINE FUZZ ──────────────────────────────────\n"; + + // requêtes HTTP/1.0 (sans Host) passent au GET + { + RequestParser p; + HttpRequest r = p.parse("GET /index.html HTTP/1.0\r\n\r\n"); + check("HTTP/1.0 sans Host → accepté", !r.method.empty()); + + LocationConfig loc = makeGetLoc(); + ServerConfig srv; srv.addLocation(loc); + HttpResponse res = handler.handle(r, loc, srv); + check("HTTP/1.0 GET index → 200", res.status_code == 200); + } + + // pipeline GET → ResponseBuilder → format correct + { + RequestParser p; + HttpRequest r = p.parse("GET /index.html HTTP/1.1\r\nHost: localhost\r\n\r\n"); + LocationConfig loc = makeGetLoc(); + ServerConfig srv; srv.addLocation(loc); + HttpResponse res = handler.handle(r, loc, srv); + std::string raw = builder.build(res); + + check("pipeline: commence par HTTP/1.1", raw.substr(0, 8) == "HTTP/1.1"); + check("pipeline: contient \\r\\n\\r\\n", raw.find("\r\n\r\n") != std::string::npos); + check("pipeline: body apres separateur", raw.find("\r\n\r\n 0 ? 1 : 0; +} diff --git a/tests/http/integration/test_cgi_advanced.cpp b/tests/http/integration/test_cgi_advanced.cpp new file mode 100644 index 0000000..0bac6fe --- /dev/null +++ b/tests/http/integration/test_cgi_advanced.cpp @@ -0,0 +1,351 @@ +#include "../../../include/http/CgiHandler.hpp" +#include "../../../include/http/MethodHandler.hpp" +#include "../../../include/config/LocationConfig.hpp" +#include "../../../include/config/ServerConfig.hpp" +#include +#include +#include +#include + +// ── Counters ────────────────────────────────────────────────────────────────── +static int passed = 0; +static int failed = 0; + +static void check(const std::string& label, bool cond) +{ + if (cond) { std::cout << "[OK] " << label << "\n"; ++passed; } + else { std::cout << "[KO] " << label << "\n"; ++failed; } +} + +// ── Helpers ─────────────────────────────────────────────────────────────────── +static LocationConfig makeCgiLoc(const std::string& ext = ".py", + const std::string& interp = "/usr/bin/python3", + const std::string& root = "www") +{ + LocationConfig loc; + loc.setPath("/cgi-bin"); + loc.setRoot(root); + loc.setCgiExtension(ext); + loc.setCgiPath(interp); + loc.addMethod("GET"); + loc.addMethod("POST"); + loc.addMethod("DELETE"); + return loc; +} + +static HttpRequest makeGet(const std::string& uri, const std::string& host = "localhost") +{ + HttpRequest req; + req.method = "GET"; + req.uri = uri; + req.version = "HTTP/1.1"; + req.headers["Host"] = host; + return req; +} + +static HttpRequest makePost(const std::string& uri, + const std::string& body, + const std::string& ctype = "application/x-www-form-urlencoded") +{ + HttpRequest req; + req.method = "POST"; + req.uri = uri; + req.version = "HTTP/1.1"; + req.headers["Host"] = "localhost"; + req.headers["Content-Type"] = ctype; + req.body = body; + return req; +} + +// ── Section helpers ─────────────────────────────────────────────────────────── +static void section(const std::string& title) +{ + std::cout << "\n── " << title << " "; + for (int i = (int)title.size(); i < 50; i++) std::cout << '-'; + std::cout << "\n"; +} + +// ── Main ────────────────────────────────────────────────────────────────────── +int main() +{ + CgiHandler cgi; + MethodHandler handler; + ServerConfig server; + LocationConfig loc = makeCgiLoc(); + server.addLocation(loc); + + // ══════════════════════════════════════════════════════════════════════════ + section("ENV VARS TRANSMISSION"); + + // REQUEST_METHOD GET + { + HttpResponse res = cgi.execute(makeGet("/cgi-bin/env_dump.py"), loc); + check("env: status 200", res.status_code == 200); + check("env: REQUEST_METHOD=GET", res.body.find("REQUEST_METHOD=GET") != std::string::npos); + check("env: GATEWAY_INTERFACE=CGI/1.1", res.body.find("GATEWAY_INTERFACE=CGI/1.1") != std::string::npos); + check("env: SERVER_PROTOCOL=HTTP/1.1", res.body.find("SERVER_PROTOCOL=HTTP/1.1") != std::string::npos); + check("env: SCRIPT_FILENAME non vide", res.body.find("SCRIPT_FILENAME=__MISSING__") == std::string::npos); + } + + // REQUEST_METHOD POST + { + HttpResponse res = cgi.execute(makePost("/cgi-bin/env_dump.py", "x=1"), loc); + check("env POST: REQUEST_METHOD=POST", res.body.find("REQUEST_METHOD=POST") != std::string::npos); + check("env POST: CONTENT_LENGTH=3", res.body.find("CONTENT_LENGTH=3") != std::string::npos); + check("env POST: CONTENT_TYPE present", res.body.find("CONTENT_TYPE=application/x-www-form-urlencoded") != std::string::npos); + } + + // QUERY_STRING + { + HttpResponse res = cgi.execute(makeGet("/cgi-bin/env_dump.py?name=Byron&lang=cpp"), loc); + check("env: QUERY_STRING=name=Byron&lang=cpp", + res.body.find("QUERY_STRING=name=Byron&lang=cpp") != std::string::npos); + } + + // HTTP_HOST + { + HttpResponse res = cgi.execute(makeGet("/cgi-bin/env_dump.py", "myserver:8080"), loc); + check("env: HTTP_HOST=myserver:8080", + res.body.find("HTTP_HOST=myserver:8080") != std::string::npos); + } + + // ══════════════════════════════════════════════════════════════════════════ + section("POST BODY TRANSMISSION"); + + // echo exact body + { + std::string body = "hello_webserv_42"; + HttpResponse res = cgi.execute(makePost("/cgi-bin/echo_post.py", body), loc); + check("POST echo: status 200", res.status_code == 200); + check("POST echo: body identique", res.body == body); + } + + // body avec caracteres speciaux + { + std::string body = "data=hello%20world&value=42&special=!@#$"; + HttpResponse res = cgi.execute(makePost("/cgi-bin/echo_post.py", body), loc); + check("POST echo: special chars transmis", res.body == body); + } + + // body vide + { + HttpResponse res = cgi.execute(makePost("/cgi-bin/echo_post.py", ""), loc); + check("POST echo: body vide → 200", res.status_code == 200); + check("POST echo: body vide → response vide", res.body.empty()); + } + + // body large (10KB) + { + std::string body(10 * 1024, 'Z'); + HttpResponse res = cgi.execute(makePost("/cgi-bin/echo_post.py", body), loc); + check("POST echo: 10KB body transmis", res.body.size() == body.size()); + check("POST echo: contenu correct", res.body == body); + } + + // POST form parsing + { + HttpResponse res = cgi.execute( + makePost("/cgi-bin/post_form.py", "username=alice&age=30&city=Paris"), loc); + check("POST form: username=alice", res.body.find("username=alice") != std::string::npos); + check("POST form: age=30", res.body.find("age=30") != std::string::npos); + check("POST form: city=Paris", res.body.find("city=Paris") != std::string::npos); + check("POST form: CONTENT_TYPE present", + res.body.find("CONTENT_TYPE=application/x-www-form-urlencoded") != std::string::npos); + } + + // POST avec Content-Type: application/json + { + std::string json = "{\"key\":\"value\",\"num\":42}"; + HttpResponse res = cgi.execute(makePost("/cgi-bin/echo_post.py", json, "application/json"), loc); + check("POST json: body correct", res.body == json); + } + + // ══════════════════════════════════════════════════════════════════════════ + section("QUERY STRING PARSING"); + + { + HttpResponse res = cgi.execute(makeGet("/cgi-bin/hello.py?name=Byron"), loc); + check("query: name dans body", res.body.find("Byron") != std::string::npos); + } + + { + HttpResponse res = cgi.execute(makeGet("/cgi-bin/json_api.py?name=Alice%20Smith"), loc); + check("query: URL-decoded name", res.body.find("Alice Smith") != std::string::npos); + check("query: content-type JSON", + res.headers.count("Content-Type") > 0 && + res.headers.at("Content-Type").find("application/json") != std::string::npos); + } + + { + HttpResponse res = cgi.execute(makeGet("/cgi-bin/env_dump.py?"), loc); + check("query vide: QUERY_STRING=", res.body.find("QUERY_STRING=\n") != std::string::npos || + res.body.find("QUERY_STRING=\r\n") != std::string::npos); + } + + // ══════════════════════════════════════════════════════════════════════════ + section("CUSTOM STATUS CODES FROM CGI"); + + // 302 redirect + { + HttpResponse res = cgi.execute(makeGet("/cgi-bin/redirect.py?to=https://42.fr"), loc); + check("CGI 302: status 302", res.status_code == 302); + check("CGI 302: Location header present", res.headers.count("Location") > 0); + check("CGI 302: Location correct", + res.headers.count("Location") > 0 && + res.headers.at("Location") == "https://42.fr"); + } + + // 404 from CGI + { + HttpResponse res = cgi.execute(makeGet("/cgi-bin/custom_404.py"), loc); + check("CGI custom 404: status 404", res.status_code == 404); + check("CGI custom 404: body present", res.body.find("Custom 404") != std::string::npos); + } + + // ══════════════════════════════════════════════════════════════════════════ + section("CONTENT-TYPE PROPAGATION"); + + { + HttpResponse res = cgi.execute(makeGet("/cgi-bin/json_api.py"), loc); + check("JSON CT: application/json", res.headers.count("Content-Type") > 0 && + res.headers.at("Content-Type").find("application/json") != std::string::npos); + check("JSON CT: body is JSON", res.body.find("{\"status\":\"ok\"") != std::string::npos); + } + + // No Content-Type → server adds default text/html + { + HttpResponse res = cgi.execute(makeGet("/cgi-bin/no_content_type.py"), loc); + check("no CT: status 200", res.status_code == 200); + check("no CT: Content-Type added by server", res.headers.count("Content-Type") > 0); + check("no CT: body present", res.body.find("body without") != std::string::npos); + } + + // ══════════════════════════════════════════════════════════════════════════ + section("LARGE OUTPUT"); + + { + HttpResponse res = cgi.execute(makeGet("/cgi-bin/large_output.py"), loc); + check("large: status 200", res.status_code == 200); + check("large: body = 100KB", res.body.size() == 100 * 1024); + check("large: content correct", res.body.find_first_not_of('A') == std::string::npos); + } + + // ══════════════════════════════════════════════════════════════════════════ + section("ERROR HANDLING"); + + // script inexistant → 404 + { + HttpResponse res = cgi.execute(makeGet("/cgi-bin/doesnotexist.py"), loc); + check("404: script inexistant → 404", res.status_code == 404); + } + + // script crash → 500 + { + HttpResponse res = cgi.execute(makeGet("/cgi-bin/crash.py"), loc); + check("500: script crash → 500", res.status_code == 500); + } + + // script non executable → 403 + { + std::ofstream f("www/cgi-bin/noperm.py"); + f << "#!/usr/bin/env python3\nprint('Content-Type: text/html\\n\\nhello')\n"; + f.close(); + chmod("www/cgi-bin/noperm.py", 0644); + + HttpResponse res = cgi.execute(makeGet("/cgi-bin/noperm.py"), loc); + check("403: non executable → 403", res.status_code == 403); + remove("www/cgi-bin/noperm.py"); + } + + // script qui ecrit sur stderr → pas de crash, 200 + { + HttpResponse res = cgi.execute(makeGet("/cgi-bin/stderr_safe.py"), loc); + check("stderr: pas de crash", res.status_code == 200); + check("stderr: body correct", res.body.find("stderr_ok") != std::string::npos); + } + + // script infinite (timeout) → 504 + { + std::cout << " [wait] timeout test (5s)..." << std::flush; + HttpResponse res = cgi.execute(makeGet("/cgi-bin/infinite.py"), loc); + std::cout << "\n"; + check("timeout: 504 Gateway Timeout", res.status_code == 504); + } + + // ══════════════════════════════════════════════════════════════════════════ + section("METHOD DISPATCH (via MethodHandler)"); + + // GET CGI via MethodHandler + { + HttpResponse res = handler.handle(makeGet("/cgi-bin/hello.py"), loc, server); + check("MH: GET CGI → 200", res.status_code == 200); + check("MH: GET CGI body html", res.body.find("CGI fonctionne") != std::string::npos); + } + + // POST CGI via MethodHandler + { + HttpResponse res = handler.handle( + makePost("/cgi-bin/hello.py", "message=test42"), loc, server); + check("MH: POST CGI → 200", res.status_code == 200); + check("MH: POST CGI body recu", res.body.find("message=test42") != std::string::npos); + check("MH: POST CGI methode", res.body.find("POST") != std::string::npos); + } + + // DELETE non autorise sur route CGI → 405 + { + // Build a location that explicitly allows only GET and POST (no DELETE) + LocationConfig locGetPost; + locGetPost.setPath("/cgi-bin"); + locGetPost.setRoot("www"); + locGetPost.setCgiExtension(".py"); + locGetPost.setCgiPath("/usr/bin/python3"); + locGetPost.addMethod("GET"); + locGetPost.addMethod("POST"); + ServerConfig srv2; srv2.addLocation(locGetPost); + + HttpRequest delReq; + delReq.method = "DELETE"; + delReq.uri = "/cgi-bin/hello.py"; + delReq.version = "HTTP/1.1"; + delReq.headers["Host"] = "localhost"; + + HttpResponse res = handler.handle(delReq, locGetPost, srv2); + check("MH: DELETE non autorise → 405", res.status_code == 405); + } + + // ══════════════════════════════════════════════════════════════════════════ + section("EVAL-STYLE END-TO-END"); + + // GET → JSON API + { + HttpResponse res = cgi.execute(makeGet("/cgi-bin/json_api.py?name=Evaluator"), loc); + check("e2e: JSON API status 200", res.status_code == 200); + check("e2e: JSON content-type", + res.headers.count("Content-Type") > 0 && + res.headers.at("Content-Type").find("application/json") != std::string::npos); + check("e2e: JSON body wellformed", + res.body.find("{") == 0 && res.body.rfind("}") == res.body.size() - 2); + check("e2e: name dans JSON", res.body.find("Evaluator") != std::string::npos); + } + + // POST → echo dans redirect (prove CGI chain works) + { + HttpResponse res = cgi.execute(makeGet("/cgi-bin/redirect.py"), loc); + check("e2e: redirect default location", + res.headers.count("Location") > 0 && + res.headers.at("Location") == "https://example.com"); + } + + // POST form-data round-trip + { + HttpResponse res = cgi.execute( + makePost("/cgi-bin/post_form.py", "login=student&project=webserv"), loc); + check("e2e: form login=student", res.body.find("login=student") != std::string::npos); + check("e2e: form project=webserv", res.body.find("project=webserv") != std::string::npos); + } + + // ══════════════════════════════════════════════════════════════════════════ + std::cout << "\n──────────────────────────────────────────────────────────\n"; + std::cout << passed << " passed, " << failed << " failed\n"; + return failed > 0 ? 1 : 0; +} diff --git a/tests/http/test_cgi_handler.cpp b/tests/http/integration/test_cgi_handler.cpp similarity index 98% rename from tests/http/test_cgi_handler.cpp rename to tests/http/integration/test_cgi_handler.cpp index 47267d5..d5d87bc 100644 --- a/tests/http/test_cgi_handler.cpp +++ b/tests/http/integration/test_cgi_handler.cpp @@ -1,4 +1,4 @@ -#include "../../include/http/MethodHandler.hpp" +#include "../../../include/http/MethodHandler.hpp" #include #include #include diff --git a/tests/http/test_delete_handler.cpp b/tests/http/integration/test_delete_handler.cpp similarity index 98% rename from tests/http/test_delete_handler.cpp rename to tests/http/integration/test_delete_handler.cpp index 6fbe4e8..fafe3a2 100644 --- a/tests/http/test_delete_handler.cpp +++ b/tests/http/integration/test_delete_handler.cpp @@ -1,4 +1,4 @@ -#include "../../include/http/MethodHandler.hpp" +#include "../../../include/http/MethodHandler.hpp" #include #include #include diff --git a/tests/http/test_get_handler.cpp b/tests/http/integration/test_get_handler.cpp similarity index 99% rename from tests/http/test_get_handler.cpp rename to tests/http/integration/test_get_handler.cpp index 96d929c..c2248b0 100644 --- a/tests/http/test_get_handler.cpp +++ b/tests/http/integration/test_get_handler.cpp @@ -1,4 +1,4 @@ -#include "../../include/http/MethodHandler.hpp" +#include "../../../include/http/MethodHandler.hpp" #include #include diff --git a/tests/http/test_post_handler.cpp b/tests/http/integration/test_post_handler.cpp similarity index 98% rename from tests/http/test_post_handler.cpp rename to tests/http/integration/test_post_handler.cpp index 28e3045..4e0d499 100644 --- a/tests/http/test_post_handler.cpp +++ b/tests/http/integration/test_post_handler.cpp @@ -1,4 +1,4 @@ -#include "../../include/http/MethodHandler.hpp" +#include "../../../include/http/MethodHandler.hpp" #include #include #include diff --git a/tests/http/test_response_builder.cpp b/tests/http/test_response_builder.cpp deleted file mode 100644 index 0a334cc..0000000 --- a/tests/http/test_response_builder.cpp +++ /dev/null @@ -1,111 +0,0 @@ -#include "../../include/http/ResponseBuilder.hpp" -#include "../../include/http/MethodHandler.hpp" -#include - -static int passed = 0; -static int failed = 0; - -static void check(const std::string& label, bool condition) -{ - if (condition) { std::cout << "[OK] " << label << std::endl; passed++; } - else { std::cout << "[KO] " << label << std::endl; failed++; } -} - -static bool startsWith(const std::string& s, const std::string& prefix) -{ - return s.substr(0, prefix.size()) == prefix; -} - -static bool contains(const std::string& s, const std::string& sub) -{ - return s.find(sub) != std::string::npos; -} - -int main() -{ - ResponseBuilder builder; - - // ── CAS 1 : 200 OK avec body ───────────────────────────────────── - { - HttpResponse res; - res.status_code = 200; - res.status_msg = "OK"; - res.headers["Content-Type"] = "text/html"; - res.headers["Content-Length"] = "13"; - res.body = "Hello World!\n"; - - std::string raw = builder.build(res); - check("200 OK: status line", startsWith(raw, "HTTP/1.1 200 OK\r\n")); - check("200 OK: Content-Type header", contains(raw, "Content-Type: text/html\r\n")); - check("200 OK: Content-Length header", contains(raw, "Content-Length: 13\r\n")); - check("200 OK: separateur vide", contains(raw, "\r\n\r\n")); - check("200 OK: body", contains(raw, "Hello World!\n")); - } - - // ── CAS 2 : 404 Not Found ──────────────────────────────────────── - { - HttpResponse res; - res.status_code = 404; - res.status_msg = "Not Found"; - res.body = "

Not Found

"; - res.headers["Content-Type"] = "text/html"; - res.headers["Content-Length"] = "44"; - - std::string raw = builder.build(res); - check("404: status line", startsWith(raw, "HTTP/1.1 404 Not Found\r\n")); - check("404: body present", contains(raw, "Not Found")); - } - - // ── CAS 3 : 201 Created sans body ─────────────────────────────── - { - HttpResponse res; - res.status_code = 201; - res.status_msg = "Created"; - res.headers["Location"] = "/uploads/file.txt"; - res.headers["Content-Length"] = "0"; - - std::string raw = builder.build(res); - check("201 Created: status line", startsWith(raw, "HTTP/1.1 201 Created\r\n")); - check("201 Created: Location header", contains(raw, "Location: /uploads/file.txt\r\n")); - check("201 Created: body vide apres separateur", raw.substr(raw.find("\r\n\r\n") + 4).empty()); - } - - // ── CAS 4 : 301 Redirect ──────────────────────────────────────── - { - HttpResponse res; - res.status_code = 301; - res.status_msg = "Moved Permanently"; - res.headers["Location"] = "https://example.com"; - - std::string raw = builder.build(res); - check("301: status line", startsWith(raw, "HTTP/1.1 301 Moved Permanently\r\n")); - check("301: Location header", contains(raw, "Location: https://example.com\r\n")); - } - - // ── CAS 5 : integration avec MethodHandler ─────────────────────── - { - LocationConfig loc; - loc.setPath("/"); - loc.setRoot("www"); - loc.setIndex("index.html"); - loc.addMethod("GET"); - - ServerConfig server; - server.addLocation(loc); - - HttpRequest req; - req.method = "GET"; - req.uri = "/index.html"; - req.version = "HTTP/1.1"; - - MethodHandler handler; - HttpResponse res = handler.handle(req, loc, server); - std::string raw = builder.build(res); - - check("integration GET: commence par HTTP/1.1", startsWith(raw, "HTTP/1.1 200 OK\r\n")); - check("integration GET: contient body html", contains(raw, "")); - } - - std::cout << std::endl << passed << " passed, " << failed << " failed" << std::endl; - return failed > 0 ? 1 : 0; -} diff --git a/tests/http/test_request_parser.cpp b/tests/http/unit/test_request_parser.cpp similarity index 99% rename from tests/http/test_request_parser.cpp rename to tests/http/unit/test_request_parser.cpp index 4842f48..84855f4 100644 --- a/tests/http/test_request_parser.cpp +++ b/tests/http/unit/test_request_parser.cpp @@ -1,4 +1,4 @@ -#include "../../include/http/RequestParser.hpp" +#include "../../../include/http/RequestParser.hpp" #include #include diff --git a/tests/http/unit/test_response_builder.cpp b/tests/http/unit/test_response_builder.cpp new file mode 100644 index 0000000..4ad80cb --- /dev/null +++ b/tests/http/unit/test_response_builder.cpp @@ -0,0 +1,144 @@ +#include "../../../include/http/ResponseBuilder.hpp" +#include + +static int passed = 0; +static int failed = 0; + +static void check(const std::string& label, bool condition) +{ + if (condition) { std::cout << "[OK] " << label << std::endl; passed++; } + else { std::cout << "[KO] " << label << std::endl; failed++; } +} + +static bool startsWith(const std::string& s, const std::string& prefix) +{ + return s.substr(0, prefix.size()) == prefix; +} + +static bool contains(const std::string& s, const std::string& sub) +{ + return s.find(sub) != std::string::npos; +} + +int main() +{ + ResponseBuilder builder; + + // ── CAS 1 : 200 OK avec body ───────────────────────────────────── + { + HttpResponse res; + res.status_code = 200; + res.status_msg = "OK"; + res.headers["Content-Type"] = "text/html"; + res.headers["Content-Length"] = "13"; + res.body = "Hello World!\n"; + + std::string raw = builder.build(res); + check("200 OK: status line", startsWith(raw, "HTTP/1.1 200 OK\r\n")); + check("200 OK: Content-Type header", contains(raw, "Content-Type: text/html\r\n")); + check("200 OK: Content-Length header", contains(raw, "Content-Length: 13\r\n")); + check("200 OK: separateur vide", contains(raw, "\r\n\r\n")); + check("200 OK: body", contains(raw, "Hello World!\n")); + } + + // ── CAS 2 : 404 Not Found ──────────────────────────────────────── + { + HttpResponse res; + res.status_code = 404; + res.status_msg = "Not Found"; + res.body = "

Not Found

"; + res.headers["Content-Type"] = "text/html"; + res.headers["Content-Length"] = "44"; + + std::string raw = builder.build(res); + check("404: status line", startsWith(raw, "HTTP/1.1 404 Not Found\r\n")); + check("404: body present", contains(raw, "Not Found")); + } + + // ── CAS 3 : 201 Created sans body ─────────────────────────────── + { + HttpResponse res; + res.status_code = 201; + res.status_msg = "Created"; + res.headers["Location"] = "/uploads/file.txt"; + res.headers["Content-Length"] = "0"; + + std::string raw = builder.build(res); + check("201 Created: status line", + startsWith(raw, "HTTP/1.1 201 Created\r\n")); + check("201 Created: Location header", + contains(raw, "Location: /uploads/file.txt\r\n")); + check("201 Created: body vide apres separateur", + raw.substr(raw.find("\r\n\r\n") + 4).empty()); + } + + // ── CAS 4 : 301 Redirect ──────────────────────────────────────── + { + HttpResponse res; + res.status_code = 301; + res.status_msg = "Moved Permanently"; + res.headers["Location"] = "https://example.com"; + + std::string raw = builder.build(res); + check("301: status line", startsWith(raw, "HTTP/1.1 301 Moved Permanently\r\n")); + check("301: Location header", contains(raw, "Location: https://example.com\r\n")); + } + + // ── CAS 5 : 405 Method Not Allowed ────────────────────────────── + { + HttpResponse res; + res.status_code = 405; + res.status_msg = "Method Not Allowed"; + res.headers["Content-Type"] = "text/html"; + res.headers["Content-Length"] = "0"; + + std::string raw = builder.build(res); + check("405: status line", startsWith(raw, "HTTP/1.1 405 Method Not Allowed\r\n")); + } + + // ── CAS 6 : 500 Internal Server Error ─────────────────────────── + { + HttpResponse res; + res.status_code = 500; + res.status_msg = "Internal Server Error"; + res.body = "

Internal Server Error

"; + res.headers["Content-Type"] = "text/html"; + + std::string raw = builder.build(res); + check("500: status line", startsWith(raw, "HTTP/1.1 500 Internal Server Error\r\n")); + check("500: body present", contains(raw, "Internal Server Error")); + } + + // ── CAS 7 : body vide → pas de contenu apres separateur ───────── + { + HttpResponse res; + res.status_code = 204; + res.status_msg = "No Content"; + + std::string raw = builder.build(res); + check("204: status line", startsWith(raw, "HTTP/1.1 204 No Content\r\n")); + check("204: separateur present", contains(raw, "\r\n\r\n")); + check("204: body vide", raw.substr(raw.find("\r\n\r\n") + 4).empty()); + } + + // ── CAS 8 : headers multiples ──────────────────────────────────── + { + HttpResponse res; + res.status_code = 200; + res.status_msg = "OK"; + res.headers["Content-Type"] = "application/json"; + res.headers["Content-Length"] = "2"; + res.headers["X-Custom"] = "webserv"; + res.body = "{}"; + + std::string raw = builder.build(res); + check("multi-headers: Content-Type JSON", + contains(raw, "Content-Type: application/json\r\n")); + check("multi-headers: X-Custom present", + contains(raw, "X-Custom: webserv\r\n")); + check("multi-headers: body JSON", contains(raw, "{}")); + } + + std::cout << "\n" << passed << " passed, " << failed << " failed" << std::endl; + return failed > 0 ? 1 : 0; +} diff --git a/tests/http/test_router.cpp b/tests/http/unit/test_router.cpp similarity index 97% rename from tests/http/test_router.cpp rename to tests/http/unit/test_router.cpp index 4f650ce..36280b0 100644 --- a/tests/http/test_router.cpp +++ b/tests/http/unit/test_router.cpp @@ -1,6 +1,6 @@ -#include "../../include/http/Router.hpp" -#include "../../include/config/ServerConfig.hpp" -#include "../../include/config/LocationConfig.hpp" +#include "../../../include/http/Router.hpp" +#include "../../../include/config/ServerConfig.hpp" +#include "../../../include/config/LocationConfig.hpp" #include static int passed = 0; diff --git a/www/cgi-bin/crash.py b/www/cgi-bin/crash.py new file mode 100755 index 0000000..d75d43e --- /dev/null +++ b/www/cgi-bin/crash.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +"""Exit immediately with non-zero — server must return 500.""" +import sys +sys.exit(1) diff --git a/www/cgi-bin/custom_404.py b/www/cgi-bin/custom_404.py new file mode 100755 index 0000000..1cf0848 --- /dev/null +++ b/www/cgi-bin/custom_404.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +"""Return Status: 404 — used to verify arbitrary status code from CGI.""" +print("Status: 404 Not Found") +print("Content-Type: text/html") +print("") +print("

Custom 404 from CGI

") diff --git a/www/cgi-bin/echo_post.py b/www/cgi-bin/echo_post.py new file mode 100755 index 0000000..cba47b3 --- /dev/null +++ b/www/cgi-bin/echo_post.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 +"""Echo the POST body verbatim — used to verify body transmission.""" +import os +import sys + +length = int(os.environ.get("CONTENT_LENGTH", 0)) +body = sys.stdin.read(length) if length > 0 else "" + +print("Content-Type: text/plain") +print("") +sys.stdout.write(body) +sys.stdout.flush() diff --git a/www/cgi-bin/env_dump.py b/www/cgi-bin/env_dump.py new file mode 100755 index 0000000..8368a3b --- /dev/null +++ b/www/cgi-bin/env_dump.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +"""Dump all CGI environment variables — used to verify env transmission.""" +import os +import sys + +print("Content-Type: text/plain") +print("") + +keys = [ + "REQUEST_METHOD", "QUERY_STRING", "CONTENT_TYPE", "CONTENT_LENGTH", + "HTTP_HOST", "SERVER_PROTOCOL", "GATEWAY_INTERFACE", + "SCRIPT_FILENAME", "PATH_INFO", +] +for k in keys: + print(k + "=" + os.environ.get(k, "__MISSING__")) diff --git a/www/cgi-bin/json_api.py b/www/cgi-bin/json_api.py new file mode 100755 index 0000000..cbb1079 --- /dev/null +++ b/www/cgi-bin/json_api.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +"""Return JSON — used to verify non-HTML content-type propagation.""" +import os + +method = os.environ.get("REQUEST_METHOD", "GET") +query = os.environ.get("QUERY_STRING", "") + +name = "anonymous" +for part in query.split("&"): + if part.startswith("name="): + name = part[5:].replace("%20", " ") + +body = '{"status":"ok","method":"' + method + '","name":"' + name + '"}' + +print("Content-Type: application/json") +print("Content-Length: " + str(len(body))) +print("") +print(body) diff --git a/www/cgi-bin/large_output.py b/www/cgi-bin/large_output.py new file mode 100755 index 0000000..c55bbf4 --- /dev/null +++ b/www/cgi-bin/large_output.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 +"""Output 100 KB of data — used to verify large response handling.""" +import sys + +chunk = "A" * 1024 +body = chunk * 100 # 100 KB + +print("Content-Type: text/plain") +print("Content-Length: " + str(len(body))) +print("") +sys.stdout.write(body) +sys.stdout.flush() diff --git a/www/cgi-bin/no_content_type.py b/www/cgi-bin/no_content_type.py new file mode 100755 index 0000000..3cc8fc6 --- /dev/null +++ b/www/cgi-bin/no_content_type.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +"""Output body without Content-Type — server must add a default.""" +print("") +print("body without content-type header") diff --git a/www/cgi-bin/post_form.py b/www/cgi-bin/post_form.py new file mode 100755 index 0000000..2dde70a --- /dev/null +++ b/www/cgi-bin/post_form.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +"""Parse application/x-www-form-urlencoded POST body and reflect values.""" +import os +import sys + +length = int(os.environ.get("CONTENT_LENGTH", 0)) +body = sys.stdin.read(length) if length > 0 else "" +ctype = os.environ.get("CONTENT_TYPE", "") + +params = {} +for part in body.split("&"): + if "=" in part: + k, v = part.split("=", 1) + params[k] = v.replace("+", " ").replace("%20", " ") + +print("Content-Type: text/plain") +print("") +print("CONTENT_TYPE=" + ctype) +for k, v in sorted(params.items()): + print(k + "=" + v) diff --git a/www/cgi-bin/redirect.py b/www/cgi-bin/redirect.py new file mode 100755 index 0000000..287654b --- /dev/null +++ b/www/cgi-bin/redirect.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +"""Return a 302 redirect — used to verify custom Status header parsing.""" +import os + +target = os.environ.get("QUERY_STRING", "") +location = "https://example.com" +for part in target.split("&"): + if part.startswith("to="): + location = part[3:] + +print("Status: 302 Found") +print("Location: " + location) +print("Content-Type: text/html") +print("") +print("Redirecting to " + location + "") diff --git a/www/cgi-bin/stderr_safe.py b/www/cgi-bin/stderr_safe.py new file mode 100755 index 0000000..28460ed --- /dev/null +++ b/www/cgi-bin/stderr_safe.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +"""Write to stderr then respond normally — server must not crash.""" +import sys + +sys.stderr.write("this is an error log line\n") +sys.stderr.write("another stderr message\n") +sys.stderr.flush() + +print("Content-Type: text/plain") +print("") +print("stderr_ok") From 70072d2c214ac3f77d4ffa2bc30a33cf4ce95fde Mon Sep 17 00:00:00 2001 From: byronlove111 Date: Wed, 17 Jun 2026 16:11:01 +0200 Subject: [PATCH 2/5] fix(tests): fix heap corruption in test_adversarial null-byte test std::string(const char*) stops at the first \0, so raw.find('\0') returns npos and raw[npos] is an out-of-bounds write. On macOS/clang this passes silently; on Ubuntu/glibc it triggers 'double free or corruption' and aborts. Fix: use the length-based std::string(buf, len) constructor so the embedded \0 is actually included in the string. Co-authored-by: Cursor --- tests/http/integration/test_adversarial.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/http/integration/test_adversarial.cpp b/tests/http/integration/test_adversarial.cpp index f2ba458..6699f47 100644 --- a/tests/http/integration/test_adversarial.cpp +++ b/tests/http/integration/test_adversarial.cpp @@ -117,8 +117,10 @@ int main() // header avec null byte dans la valeur (survit au parsing sans crash) { RequestParser p; - std::string raw = "GET / HTTP/1.1\r\nHost: local\0host\r\n\r\n"; - raw[raw.find('\0')] = 'x'; // on peut pas tester null byte proprement en C++ + // Use the length-based constructor to include the embedded null byte — + // the const char* constructor would stop at the first \0. + const char buffer[] = "GET / HTTP/1.1\r\nHost: local\x00host\r\n\r\n"; + std::string raw(buffer, sizeof(buffer) - 1); HttpRequest r = p.parse(raw); check("parse: header avec null → pas de crash", true); } From ed6ff37aa8620c30a65ef8218ef11aad8f148b3f Mon Sep 17 00:00:00 2001 From: byronlove111 Date: Wed, 17 Jun 2026 16:15:10 +0200 Subject: [PATCH 3/5] refactor(http): move entry-point functions to bottom of each file MethodHandler.cpp : isMethodAllowed + buildError avant handle() RequestParser.cpp : isValid + parseFirstLine + parseHeaders + parseBody avant parse() output.cpp : buildError avant parseOutput() Co-authored-by: Cursor --- src/http/cgi/output.cpp | 28 ++++++++++---------- src/http/handlers/MethodHandler.cpp | 40 ++++++++++++++--------------- src/http/parser/RequestParser.cpp | 32 +++++++++++------------ 3 files changed, 50 insertions(+), 50 deletions(-) diff --git a/src/http/cgi/output.cpp b/src/http/cgi/output.cpp index 4346144..e7d4d9c 100644 --- a/src/http/cgi/output.cpp +++ b/src/http/cgi/output.cpp @@ -1,6 +1,20 @@ #include "../../../include/http/CgiHandler.hpp" #include +HttpResponse CgiHandler::buildError(int statusCode, const std::string& statusMessage) +{ + HttpResponse response; + std::ostringstream contentLengthStream; + + response.status_code = statusCode; + response.status_msg = statusMessage; + response.body = "

" + statusMessage + "

"; + response.headers["Content-Type"] = "text/html"; + contentLengthStream << response.body.size(); + response.headers["Content-Length"] = contentLengthStream.str(); + return response; +} + HttpResponse CgiHandler::parseOutput(const std::string& cgiOutput) { HttpResponse response; @@ -73,17 +87,3 @@ HttpResponse CgiHandler::parseOutput(const std::string& cgiOutput) return response; } - -HttpResponse CgiHandler::buildError(int statusCode, const std::string& statusMessage) -{ - HttpResponse response; - std::ostringstream contentLengthStream; - - response.status_code = statusCode; - response.status_msg = statusMessage; - response.body = "

" + statusMessage + "

"; - response.headers["Content-Type"] = "text/html"; - contentLengthStream << response.body.size(); - response.headers["Content-Length"] = contentLengthStream.str(); - return response; -} diff --git a/src/http/handlers/MethodHandler.cpp b/src/http/handlers/MethodHandler.cpp index 64dc638..1b997dc 100644 --- a/src/http/handlers/MethodHandler.cpp +++ b/src/http/handlers/MethodHandler.cpp @@ -64,6 +64,26 @@ static bool isCgiRequest(const HttpRequest& request, const LocationConfig& locat return uriExtension == cgiExtension; } +bool MethodHandler::isMethodAllowed(const std::string& method, const LocationConfig& location) +{ + const std::vector& allowedMethods = location.getAllowedMethods(); + return std::find(allowedMethods.begin(), allowedMethods.end(), method) != allowedMethods.end(); +} + +HttpResponse MethodHandler::buildError(int statusCode, const std::string& statusMessage) +{ + HttpResponse response; + std::ostringstream contentLength; + + response.status_code = statusCode; + response.status_msg = statusMessage; + response.body = "

" + statusMessage + "

"; + response.headers["Content-Type"] = "text/html"; + contentLength << response.body.size(); + response.headers["Content-Length"] = contentLength.str(); + return response; +} + HttpResponse MethodHandler::handle(const HttpRequest& request, const LocationConfig& location, const ServerConfig& server) { if (hasPathTraversal(request.uri)) @@ -104,23 +124,3 @@ HttpResponse MethodHandler::handle(const HttpRequest& request, const LocationCon return buildError(405, "Method Not Allowed"); } - -bool MethodHandler::isMethodAllowed(const std::string& method, const LocationConfig& location) -{ - const std::vector& allowedMethods = location.getAllowedMethods(); - return std::find(allowedMethods.begin(), allowedMethods.end(), method) != allowedMethods.end(); -} - -HttpResponse MethodHandler::buildError(int statusCode, const std::string& statusMessage) -{ - HttpResponse response; - std::ostringstream contentLength; - - response.status_code = statusCode; - response.status_msg = statusMessage; - response.body = "

" + statusMessage + "

"; - response.headers["Content-Type"] = "text/html"; - contentLength << response.body.size(); - response.headers["Content-Length"] = contentLength.str(); - return response; -} diff --git a/src/http/parser/RequestParser.cpp b/src/http/parser/RequestParser.cpp index f166451..03e12a1 100644 --- a/src/http/parser/RequestParser.cpp +++ b/src/http/parser/RequestParser.cpp @@ -1,22 +1,6 @@ #include "../../../include/http/RequestParser.hpp" #include -HttpRequest RequestParser::parse(const std::string& rawRequest) -{ - HttpRequest request; - - if (!isValid(rawRequest)) - return request; - - std::size_t firstLineEnd = rawRequest.find("\r\n"); - std::size_t headerBodySeparator = rawRequest.find("\r\n\r\n"); - - parseFirstLine(rawRequest.substr(0, firstLineEnd), request); - parseHeaders(rawRequest, request, firstLineEnd, headerBodySeparator); - parseBody(rawRequest, request, headerBodySeparator); - return request; -} - bool RequestParser::isValid(const std::string& rawRequest) { if (rawRequest.find("\r\n\r\n") == std::string::npos) @@ -105,3 +89,19 @@ void RequestParser::parseBody(const std::string& rawRequest, HttpRequest& reques { request.body = rawRequest.substr(headerBodySeparator + 4); } + +HttpRequest RequestParser::parse(const std::string& rawRequest) +{ + HttpRequest request; + + if (!isValid(rawRequest)) + return request; + + std::size_t firstLineEnd = rawRequest.find("\r\n"); + std::size_t headerBodySeparator = rawRequest.find("\r\n\r\n"); + + parseFirstLine(rawRequest.substr(0, firstLineEnd), request); + parseHeaders(rawRequest, request, firstLineEnd, headerBodySeparator); + parseBody(rawRequest, request, headerBodySeparator); + return request; +} From 358addb18f1e844cc7615a81a427bdcff5544ae4 Mon Sep 17 00:00:00 2001 From: byronlove111 Date: Wed, 17 Jun 2026 16:20:15 +0200 Subject: [PATCH 4/5] refactor(http): extract buildError as shared free function buildHttpError MethodHandler::buildError et CgiHandler::buildError \u00e9taient identiques. Extrait en fonction libre buildHttpError() dans HttpUtils.hpp/.cpp. Supprim\u00e9 des deux classes, tous les appels mis \u00e0 jour. Co-authored-by: Cursor --- include/http/CgiHandler.hpp | 1 - include/http/HttpUtils.hpp | 9 +++++++++ include/http/MethodHandler.hpp | 3 +-- src/http/HttpUtils.cpp | 16 ++++++++++++++++ src/http/cgi/execute.cpp | 20 +++++++++----------- src/http/cgi/output.cpp | 15 +-------------- src/http/handlers/DeleteHandler.cpp | 7 ++++--- src/http/handlers/GetHandler.cpp | 5 +++-- src/http/handlers/MethodHandler.cpp | 25 ++++++------------------- src/http/handlers/PostHandler.cpp | 11 ++++++----- 10 files changed, 55 insertions(+), 57 deletions(-) create mode 100644 include/http/HttpUtils.hpp create mode 100644 src/http/HttpUtils.cpp diff --git a/include/http/CgiHandler.hpp b/include/http/CgiHandler.hpp index e62f412..8b43b8c 100644 --- a/include/http/CgiHandler.hpp +++ b/include/http/CgiHandler.hpp @@ -14,7 +14,6 @@ class CgiHandler { private: std::vector buildEnv(const HttpRequest& req, const std::string& scriptPath); HttpResponse parseOutput(const std::string& raw); - HttpResponse buildError(int code, const std::string& msg); }; #endif diff --git a/include/http/HttpUtils.hpp b/include/http/HttpUtils.hpp new file mode 100644 index 0000000..f58428c --- /dev/null +++ b/include/http/HttpUtils.hpp @@ -0,0 +1,9 @@ +#ifndef HTTP_UTILS_HPP +#define HTTP_UTILS_HPP + +#include "HttpResponse.hpp" +#include + +HttpResponse buildHttpError(int statusCode, const std::string& statusMessage); + +#endif diff --git a/include/http/MethodHandler.hpp b/include/http/MethodHandler.hpp index 8c82bc1..73be68a 100644 --- a/include/http/MethodHandler.hpp +++ b/include/http/MethodHandler.hpp @@ -15,8 +15,7 @@ class MethodHandler { HttpResponse handlePost(const HttpRequest& req, const LocationConfig& loc); HttpResponse handleDelete(const HttpRequest& req, const LocationConfig& loc); - bool isMethodAllowed(const std::string& method, const LocationConfig& loc); - HttpResponse buildError(int code, const std::string& msg); + bool isMethodAllowed(const std::string& method, const LocationConfig& loc); }; #endif diff --git a/src/http/HttpUtils.cpp b/src/http/HttpUtils.cpp new file mode 100644 index 0000000..d49bf57 --- /dev/null +++ b/src/http/HttpUtils.cpp @@ -0,0 +1,16 @@ +#include "../../include/http/HttpUtils.hpp" +#include + +HttpResponse buildHttpError(int statusCode, const std::string& statusMessage) +{ + HttpResponse response; + std::ostringstream contentLength; + + response.status_code = statusCode; + response.status_msg = statusMessage; + response.body = "

" + statusMessage + "

"; + response.headers["Content-Type"] = "text/html"; + contentLength << response.body.size(); + response.headers["Content-Length"] = contentLength.str(); + return response; +} diff --git a/src/http/cgi/execute.cpp b/src/http/cgi/execute.cpp index 91d102a..9ef0e16 100644 --- a/src/http/cgi/execute.cpp +++ b/src/http/cgi/execute.cpp @@ -1,4 +1,5 @@ #include "../../../include/http/CgiHandler.hpp" +#include "../../../include/http/HttpUtils.hpp" #include #include #include @@ -36,9 +37,6 @@ static void runChildProcess(const std::string& interpreter, const std::string& s _exit(1); } -// Read CGI stdout with a deadline-based timeout. -// Uses O_NONBLOCK + time() instead of select()/poll() so the server -// keeps exactly one multiplexer (the main poll() in the event loop). static std::string readCgiOutputWithTimeout(int pipeReadEnd, pid_t childPid, bool& hasTimedOut) { std::string cgiOutput; @@ -89,9 +87,9 @@ HttpResponse CgiHandler::execute(const HttpRequest& request, const LocationConfi std::string scriptPath = buildScriptPath(request, location); if (access(scriptPath.c_str(), F_OK) == -1) - return buildError(404, "Not Found"); + return buildHttpError(404, "Not Found"); if (access(scriptPath.c_str(), X_OK) == -1) - return buildError(403, "Forbidden"); + return buildHttpError(403, "Forbidden"); std::vector envVars = buildEnv(request, scriptPath); std::vector envPointers; @@ -109,14 +107,14 @@ HttpResponse CgiHandler::execute(const HttpRequest& request, const LocationConfi int stdinPipe[2]; int stdoutPipe[2]; if (pipe(stdinPipe) == -1 || pipe(stdoutPipe) == -1) - return buildError(500, "Internal Server Error"); + return buildHttpError(500, "Internal Server Error"); pid_t childPid = fork(); if (childPid == -1) { close(stdinPipe[0]); close(stdinPipe[1]); close(stdoutPipe[0]); close(stdoutPipe[1]); - return buildError(500, "Internal Server Error"); + return buildHttpError(500, "Internal Server Error"); } if (childPid == 0) @@ -147,19 +145,19 @@ HttpResponse CgiHandler::execute(const HttpRequest& request, const LocationConfi std::string cgiOutput = readCgiOutputWithTimeout(stdoutPipe[0], childPid, hasTimedOut); if (hasTimedOut) - return buildError(504, "Gateway Timeout"); + return buildHttpError(504, "Gateway Timeout"); int childExitStatus = 0; waitpid(childPid, &childExitStatus, 0); if (WIFEXITED(childExitStatus) && WEXITSTATUS(childExitStatus) != 0) - return buildError(500, "Internal Server Error"); + return buildHttpError(500, "Internal Server Error"); if (WIFSIGNALED(childExitStatus)) - return buildError(500, "Internal Server Error"); + return buildHttpError(500, "Internal Server Error"); if (cgiOutput.empty()) - return buildError(500, "Internal Server Error"); + return buildHttpError(500, "Internal Server Error"); return parseOutput(cgiOutput); } diff --git a/src/http/cgi/output.cpp b/src/http/cgi/output.cpp index e7d4d9c..fee3283 100644 --- a/src/http/cgi/output.cpp +++ b/src/http/cgi/output.cpp @@ -1,20 +1,7 @@ #include "../../../include/http/CgiHandler.hpp" +#include "../../../include/http/HttpUtils.hpp" #include -HttpResponse CgiHandler::buildError(int statusCode, const std::string& statusMessage) -{ - HttpResponse response; - std::ostringstream contentLengthStream; - - response.status_code = statusCode; - response.status_msg = statusMessage; - response.body = "

" + statusMessage + "

"; - response.headers["Content-Type"] = "text/html"; - contentLengthStream << response.body.size(); - response.headers["Content-Length"] = contentLengthStream.str(); - return response; -} - HttpResponse CgiHandler::parseOutput(const std::string& cgiOutput) { HttpResponse response; diff --git a/src/http/handlers/DeleteHandler.cpp b/src/http/handlers/DeleteHandler.cpp index 66cb402..6bea2b9 100644 --- a/src/http/handlers/DeleteHandler.cpp +++ b/src/http/handlers/DeleteHandler.cpp @@ -1,4 +1,5 @@ #include "../../../include/http/MethodHandler.hpp" +#include "../../../include/http/HttpUtils.hpp" #include #include @@ -8,10 +9,10 @@ HttpResponse MethodHandler::handleDelete(const HttpRequest& request, const Locat struct stat fileInfo; if (stat(filePath.c_str(), &fileInfo) == -1) - return buildError(404, "Not Found"); + return buildHttpError(404, "Not Found"); if (S_ISDIR(fileInfo.st_mode)) - return buildError(403, "Forbidden"); + return buildHttpError(403, "Forbidden"); if (unlink(filePath.c_str()) == 0) { @@ -20,5 +21,5 @@ HttpResponse MethodHandler::handleDelete(const HttpRequest& request, const Locat response.status_msg = "No Content"; return response; } - return buildError(403, "Forbidden"); + return buildHttpError(403, "Forbidden"); } diff --git a/src/http/handlers/GetHandler.cpp b/src/http/handlers/GetHandler.cpp index 37e4c99..8fccb0c 100644 --- a/src/http/handlers/GetHandler.cpp +++ b/src/http/handlers/GetHandler.cpp @@ -1,4 +1,5 @@ #include "../../../include/http/MethodHandler.hpp" +#include "../../../include/http/HttpUtils.hpp" #include #include #include @@ -77,12 +78,12 @@ HttpResponse MethodHandler::handleGet(const HttpRequest& request, const Location else if (location.getAutoindex()) return buildAutoindex(filePath, request.uri); else - return buildError(403, "Forbidden"); + return buildHttpError(403, "Forbidden"); } int fileDescriptor = open(filePath.c_str(), O_RDONLY); if (fileDescriptor == -1) - return buildError(404, "Not Found"); + return buildHttpError(404, "Not Found"); HttpResponse response; char readBuffer[4096]; diff --git a/src/http/handlers/MethodHandler.cpp b/src/http/handlers/MethodHandler.cpp index 1b997dc..cdbe136 100644 --- a/src/http/handlers/MethodHandler.cpp +++ b/src/http/handlers/MethodHandler.cpp @@ -1,5 +1,6 @@ #include "../../../include/http/MethodHandler.hpp" #include "../../../include/http/CgiHandler.hpp" +#include "../../../include/http/HttpUtils.hpp" #include #include @@ -70,35 +71,21 @@ bool MethodHandler::isMethodAllowed(const std::string& method, const LocationCon return std::find(allowedMethods.begin(), allowedMethods.end(), method) != allowedMethods.end(); } -HttpResponse MethodHandler::buildError(int statusCode, const std::string& statusMessage) -{ - HttpResponse response; - std::ostringstream contentLength; - - response.status_code = statusCode; - response.status_msg = statusMessage; - response.body = "

" + statusMessage + "

"; - response.headers["Content-Type"] = "text/html"; - contentLength << response.body.size(); - response.headers["Content-Length"] = contentLength.str(); - return response; -} - HttpResponse MethodHandler::handle(const HttpRequest& request, const LocationConfig& location, const ServerConfig& server) { if (hasPathTraversal(request.uri)) - return buildError(400, "Bad Request"); + return buildHttpError(400, "Bad Request"); if (location.getPath().empty()) - return buildError(404, "Not Found"); + return buildHttpError(404, "Not Found"); if (!isMethodAllowed(request.method, location)) - return buildError(405, "Method Not Allowed"); + return buildHttpError(405, "Method Not Allowed"); bool bodyLimitIsSet = server.getMaxBodySize() > 0; bool bodyExceedsLimit = request.body.size() > server.getMaxBodySize(); if (bodyLimitIsSet && bodyExceedsLimit) - return buildError(413, "Payload Too Large"); + return buildHttpError(413, "Payload Too Large"); if (!location.getRedirectUrl().empty()) { @@ -122,5 +109,5 @@ HttpResponse MethodHandler::handle(const HttpRequest& request, const LocationCon if (request.method == "DELETE") return handleDelete(request, location); - return buildError(405, "Method Not Allowed"); + return buildHttpError(405, "Method Not Allowed"); } diff --git a/src/http/handlers/PostHandler.cpp b/src/http/handlers/PostHandler.cpp index fcd364d..5445431 100644 --- a/src/http/handlers/PostHandler.cpp +++ b/src/http/handlers/PostHandler.cpp @@ -1,4 +1,5 @@ #include "../../../include/http/MethodHandler.hpp" +#include "../../../include/http/HttpUtils.hpp" #include #include #include @@ -14,20 +15,20 @@ static std::string extractFilename(const std::string& uri) HttpResponse MethodHandler::handlePost(const HttpRequest& request, const LocationConfig& location) { if (location.getUploadPath().empty()) - return buildError(500, "Internal Server Error"); + return buildHttpError(500, "Internal Server Error"); if (request.body.empty()) - return buildError(400, "Bad Request"); + return buildHttpError(400, "Bad Request"); std::string filename = extractFilename(request.uri); if (filename.empty()) - return buildError(400, "Bad Request"); + return buildHttpError(400, "Bad Request"); std::string destinationPath = location.getUploadPath() + "/" + filename; int fileDescriptor = open(destinationPath.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fileDescriptor == -1) - return buildError(403, "Forbidden"); + return buildHttpError(403, "Forbidden"); const char* bodyData = request.body.data(); std::size_t totalBytesToWrite = request.body.size(); @@ -40,7 +41,7 @@ HttpResponse MethodHandler::handlePost(const HttpRequest& request, const Locatio if (bytesWritten <= 0) { close(fileDescriptor); - return buildError(507, "Insufficient Storage"); + return buildHttpError(507, "Insufficient Storage"); } totalBytesWritten += bytesWritten; } From 453b3cdbd97242d4fb0bc6d01efe89a3336a356c Mon Sep 17 00:00:00 2001 From: byronlove111 Date: Wed, 17 Jun 2026 16:24:25 +0200 Subject: [PATCH 5/5] refactor(http): create utils/ folder with HttpUtils and StringUtils StringUtils.hpp/.cpp : urlDecode, hasPathTraversal, extractQueryString, extractFilename HttpUtils.hpp/.cpp : buildHttpError, getContentType Supprime les fonctions statiques dupliquees dans chaque handler/cgi. Ancien HttpUtils a la racine remplace par include/http/utils/ et src/http/utils/. Co-authored-by: Cursor --- include/http/{ => utils}/HttpUtils.hpp | 3 +- include/http/utils/StringUtils.hpp | 11 +++++ src/http/HttpUtils.cpp | 16 ------- src/http/cgi/env.cpp | 10 +---- src/http/cgi/execute.cpp | 2 +- src/http/cgi/output.cpp | 1 - src/http/handlers/DeleteHandler.cpp | 2 +- src/http/handlers/GetHandler.cpp | 23 +--------- src/http/handlers/MethodHandler.cpp | 49 ++------------------- src/http/handlers/PostHandler.cpp | 15 ++----- src/http/utils/HttpUtils.cpp | 35 +++++++++++++++ src/http/utils/StringUtils.cpp | 59 ++++++++++++++++++++++++++ 12 files changed, 121 insertions(+), 105 deletions(-) rename include/http/{ => utils}/HttpUtils.hpp (63%) create mode 100644 include/http/utils/StringUtils.hpp delete mode 100644 src/http/HttpUtils.cpp create mode 100644 src/http/utils/HttpUtils.cpp create mode 100644 src/http/utils/StringUtils.cpp diff --git a/include/http/HttpUtils.hpp b/include/http/utils/HttpUtils.hpp similarity index 63% rename from include/http/HttpUtils.hpp rename to include/http/utils/HttpUtils.hpp index f58428c..dbadd9a 100644 --- a/include/http/HttpUtils.hpp +++ b/include/http/utils/HttpUtils.hpp @@ -1,9 +1,10 @@ #ifndef HTTP_UTILS_HPP #define HTTP_UTILS_HPP -#include "HttpResponse.hpp" +#include "../HttpResponse.hpp" #include HttpResponse buildHttpError(int statusCode, const std::string& statusMessage); +std::string getContentType(const std::string& filePath); #endif diff --git a/include/http/utils/StringUtils.hpp b/include/http/utils/StringUtils.hpp new file mode 100644 index 0000000..1900df8 --- /dev/null +++ b/include/http/utils/StringUtils.hpp @@ -0,0 +1,11 @@ +#ifndef STRING_UTILS_HPP +#define STRING_UTILS_HPP + +#include + +std::string urlDecode(const std::string& encoded); +bool hasPathTraversal(const std::string& uri); +std::string extractQueryString(const std::string& uri); +std::string extractFilename(const std::string& uri); + +#endif diff --git a/src/http/HttpUtils.cpp b/src/http/HttpUtils.cpp deleted file mode 100644 index d49bf57..0000000 --- a/src/http/HttpUtils.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include "../../include/http/HttpUtils.hpp" -#include - -HttpResponse buildHttpError(int statusCode, const std::string& statusMessage) -{ - HttpResponse response; - std::ostringstream contentLength; - - response.status_code = statusCode; - response.status_msg = statusMessage; - response.body = "

" + statusMessage + "

"; - response.headers["Content-Type"] = "text/html"; - contentLength << response.body.size(); - response.headers["Content-Length"] = contentLength.str(); - return response; -} diff --git a/src/http/cgi/env.cpp b/src/http/cgi/env.cpp index d40ca1d..43b5475 100644 --- a/src/http/cgi/env.cpp +++ b/src/http/cgi/env.cpp @@ -1,14 +1,7 @@ #include "../../../include/http/CgiHandler.hpp" +#include "../../../include/http/utils/StringUtils.hpp" #include -static std::string extractQueryString(const std::string& uri) -{ - std::size_t queryStart = uri.find('?'); - if (queryStart == std::string::npos) - return ""; - return uri.substr(queryStart + 1); -} - std::vector CgiHandler::buildEnv(const HttpRequest& request, const std::string& scriptPath) { @@ -17,6 +10,7 @@ std::vector CgiHandler::buildEnv(const HttpRequest& request, envVars.push_back("REQUEST_METHOD=" + request.method); envVars.push_back("QUERY_STRING=" + extractQueryString(request.uri)); envVars.push_back("SCRIPT_FILENAME=" + scriptPath); + std::string pathWithoutQuery = request.uri; std::size_t queryPosition = pathWithoutQuery.find('?'); if (queryPosition != std::string::npos) diff --git a/src/http/cgi/execute.cpp b/src/http/cgi/execute.cpp index 9ef0e16..289139f 100644 --- a/src/http/cgi/execute.cpp +++ b/src/http/cgi/execute.cpp @@ -1,5 +1,5 @@ #include "../../../include/http/CgiHandler.hpp" -#include "../../../include/http/HttpUtils.hpp" +#include "../../../include/http/utils/HttpUtils.hpp" #include #include #include diff --git a/src/http/cgi/output.cpp b/src/http/cgi/output.cpp index fee3283..a8c362f 100644 --- a/src/http/cgi/output.cpp +++ b/src/http/cgi/output.cpp @@ -1,5 +1,4 @@ #include "../../../include/http/CgiHandler.hpp" -#include "../../../include/http/HttpUtils.hpp" #include HttpResponse CgiHandler::parseOutput(const std::string& cgiOutput) diff --git a/src/http/handlers/DeleteHandler.cpp b/src/http/handlers/DeleteHandler.cpp index 6bea2b9..525b1c5 100644 --- a/src/http/handlers/DeleteHandler.cpp +++ b/src/http/handlers/DeleteHandler.cpp @@ -1,5 +1,5 @@ #include "../../../include/http/MethodHandler.hpp" -#include "../../../include/http/HttpUtils.hpp" +#include "../../../include/http/utils/HttpUtils.hpp" #include #include diff --git a/src/http/handlers/GetHandler.cpp b/src/http/handlers/GetHandler.cpp index 8fccb0c..13a05c9 100644 --- a/src/http/handlers/GetHandler.cpp +++ b/src/http/handlers/GetHandler.cpp @@ -1,30 +1,11 @@ #include "../../../include/http/MethodHandler.hpp" -#include "../../../include/http/HttpUtils.hpp" +#include "../../../include/http/utils/HttpUtils.hpp" #include #include #include #include #include -static std::string getContentType(const std::string& filePath) -{ - std::string fileExtension; - std::size_t dotPosition = filePath.rfind('.'); - if (dotPosition != std::string::npos) - fileExtension = filePath.substr(dotPosition); - - if (fileExtension == ".html" || fileExtension == ".htm") return "text/html"; - if (fileExtension == ".css") return "text/css"; - if (fileExtension == ".js") return "application/javascript"; - if (fileExtension == ".json") return "application/json"; - if (fileExtension == ".png") return "image/png"; - if (fileExtension == ".jpg" || fileExtension == ".jpeg") return "image/jpeg"; - if (fileExtension == ".gif") return "image/gif"; - if (fileExtension == ".ico") return "image/x-icon"; - if (fileExtension == ".txt") return "text/plain"; - return "application/octet-stream"; -} - static HttpResponse buildAutoindex(const std::string& directoryPath, const std::string& requestUri) { DIR* directory = opendir(directoryPath.c_str()); @@ -45,7 +26,7 @@ static HttpResponse buildAutoindex(const std::string& directoryPath, const std:: std::string entryLink = requestUri; if (entryLink[entryLink.size() - 1] != '/') entryLink += '/'; - entryLink += entryName; + entryLink += entryName; listingHtml += "
  • " + entryName + "
  • "; } closedir(directory); diff --git a/src/http/handlers/MethodHandler.cpp b/src/http/handlers/MethodHandler.cpp index cdbe136..5a7e7d2 100644 --- a/src/http/handlers/MethodHandler.cpp +++ b/src/http/handlers/MethodHandler.cpp @@ -1,49 +1,8 @@ #include "../../../include/http/MethodHandler.hpp" #include "../../../include/http/CgiHandler.hpp" -#include "../../../include/http/HttpUtils.hpp" +#include "../../../include/http/utils/HttpUtils.hpp" +#include "../../../include/http/utils/StringUtils.hpp" #include -#include - -static std::string urlDecode(const std::string& encoded) -{ - std::string decoded; - - for (std::size_t pos = 0; pos < encoded.size(); ++pos) - { - bool isPercentSign = encoded[pos] == '%'; - bool hasTwoCharAfter = pos + 2 < encoded.size(); - - if (!isPercentSign || !hasTwoCharAfter) - { - decoded += encoded[pos]; - continue; - } - - char hexSequence[3] = { encoded[pos + 1], encoded[pos + 2], '\0' }; - char* parseEnd; - int decodedChar = std::strtol(hexSequence, &parseEnd, 16); - - bool validHex = (parseEnd == hexSequence + 2); - if (!validHex) - { - decoded += encoded[pos]; - continue; - } - - decoded += static_cast(decodedChar); - pos += 2; - } - - return decoded; -} - -static bool hasPathTraversal(const std::string& uri) -{ - std::string decodedUri = urlDecode(uri); - bool containsDotDot = decodedUri.find("..") != std::string::npos; - - return containsDotDot; -} static bool isCgiRequest(const HttpRequest& request, const LocationConfig& location) { @@ -82,8 +41,8 @@ HttpResponse MethodHandler::handle(const HttpRequest& request, const LocationCon if (!isMethodAllowed(request.method, location)) return buildHttpError(405, "Method Not Allowed"); - bool bodyLimitIsSet = server.getMaxBodySize() > 0; - bool bodyExceedsLimit = request.body.size() > server.getMaxBodySize(); + bool bodyLimitIsSet = server.getMaxBodySize() > 0; + bool bodyExceedsLimit = request.body.size() > server.getMaxBodySize(); if (bodyLimitIsSet && bodyExceedsLimit) return buildHttpError(413, "Payload Too Large"); diff --git a/src/http/handlers/PostHandler.cpp b/src/http/handlers/PostHandler.cpp index 5445431..454c9f5 100644 --- a/src/http/handlers/PostHandler.cpp +++ b/src/http/handlers/PostHandler.cpp @@ -1,17 +1,10 @@ #include "../../../include/http/MethodHandler.hpp" -#include "../../../include/http/HttpUtils.hpp" +#include "../../../include/http/utils/HttpUtils.hpp" +#include "../../../include/http/utils/StringUtils.hpp" #include #include #include -static std::string extractFilename(const std::string& uri) -{ - std::size_t lastSlashPosition = uri.rfind('/'); - if (lastSlashPosition == std::string::npos || lastSlashPosition + 1 >= uri.size()) - return ""; - return uri.substr(lastSlashPosition + 1); -} - HttpResponse MethodHandler::handlePost(const HttpRequest& request, const LocationConfig& location) { if (location.getUploadPath().empty()) @@ -30,7 +23,7 @@ HttpResponse MethodHandler::handlePost(const HttpRequest& request, const Locatio if (fileDescriptor == -1) return buildHttpError(403, "Forbidden"); - const char* bodyData = request.body.data(); + const char* bodyData = request.body.data(); std::size_t totalBytesToWrite = request.body.size(); std::size_t totalBytesWritten = 0; @@ -43,7 +36,7 @@ HttpResponse MethodHandler::handlePost(const HttpRequest& request, const Locatio close(fileDescriptor); return buildHttpError(507, "Insufficient Storage"); } - totalBytesWritten += bytesWritten; + totalBytesWritten += static_cast(bytesWritten); } close(fileDescriptor); diff --git a/src/http/utils/HttpUtils.cpp b/src/http/utils/HttpUtils.cpp new file mode 100644 index 0000000..44895ec --- /dev/null +++ b/src/http/utils/HttpUtils.cpp @@ -0,0 +1,35 @@ +#include "../../../include/http/utils/HttpUtils.hpp" +#include + +HttpResponse buildHttpError(int statusCode, const std::string& statusMessage) +{ + HttpResponse response; + std::ostringstream contentLength; + + response.status_code = statusCode; + response.status_msg = statusMessage; + response.body = "

    " + statusMessage + "

    "; + response.headers["Content-Type"] = "text/html"; + contentLength << response.body.size(); + response.headers["Content-Length"] = contentLength.str(); + return response; +} + +std::string getContentType(const std::string& filePath) +{ + std::string fileExtension; + std::size_t dotPosition = filePath.rfind('.'); + if (dotPosition != std::string::npos) + fileExtension = filePath.substr(dotPosition); + + if (fileExtension == ".html" || fileExtension == ".htm") return "text/html"; + if (fileExtension == ".css") return "text/css"; + if (fileExtension == ".js") return "application/javascript"; + if (fileExtension == ".json") return "application/json"; + if (fileExtension == ".png") return "image/png"; + if (fileExtension == ".jpg" || fileExtension == ".jpeg") return "image/jpeg"; + if (fileExtension == ".gif") return "image/gif"; + if (fileExtension == ".ico") return "image/x-icon"; + if (fileExtension == ".txt") return "text/plain"; + return "application/octet-stream"; +} diff --git a/src/http/utils/StringUtils.cpp b/src/http/utils/StringUtils.cpp new file mode 100644 index 0000000..16cebb4 --- /dev/null +++ b/src/http/utils/StringUtils.cpp @@ -0,0 +1,59 @@ +#include "../../../include/http/utils/StringUtils.hpp" +#include + +std::string urlDecode(const std::string& encoded) +{ + std::string decoded; + + for (std::size_t pos = 0; pos < encoded.size(); ++pos) + { + bool isPercentSign = encoded[pos] == '%'; + bool hasTwoCharAfter = pos + 2 < encoded.size(); + + if (!isPercentSign || !hasTwoCharAfter) + { + decoded += encoded[pos]; + continue; + } + + char hexSequence[3] = { encoded[pos + 1], encoded[pos + 2], '\0' }; + char* parseEnd; + int decodedChar = std::strtol(hexSequence, &parseEnd, 16); + + bool validHex = (parseEnd == hexSequence + 2); + if (!validHex) + { + decoded += encoded[pos]; + continue; + } + + decoded += static_cast(decodedChar); + pos += 2; + } + + return decoded; +} + +bool hasPathTraversal(const std::string& uri) +{ + std::string decodedUri = urlDecode(uri); + bool containsDotDot = decodedUri.find("..") != std::string::npos; + + return containsDotDot; +} + +std::string extractQueryString(const std::string& uri) +{ + std::size_t queryStart = uri.find('?'); + if (queryStart == std::string::npos) + return ""; + return uri.substr(queryStart + 1); +} + +std::string extractFilename(const std::string& uri) +{ + std::size_t lastSlashPosition = uri.rfind('/'); + if (lastSlashPosition == std::string::npos || lastSlashPosition + 1 >= uri.size()) + return ""; + return uri.substr(lastSlashPosition + 1); +}