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/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/include/http/utils/HttpUtils.hpp b/include/http/utils/HttpUtils.hpp new file mode 100644 index 0000000..dbadd9a --- /dev/null +++ b/include/http/utils/HttpUtils.hpp @@ -0,0 +1,10 @@ +#ifndef HTTP_UTILS_HPP +#define HTTP_UTILS_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/cgi/env.cpp b/src/http/cgi/env.cpp index 37fe454..43b5475 100644 --- a/src/http/cgi/env.cpp +++ b/src/http/cgi/env.cpp @@ -1,46 +1,45 @@ #include "../../../include/http/CgiHandler.hpp" +#include "../../../include/http/utils/StringUtils.hpp" #include -static std::string getQueryString(const std::string& uri) +std::vector CgiHandler::buildEnv(const HttpRequest& request, + const std::string& scriptPath) { - std::size_t q = uri.find('?'); - if (q == std::string::npos) - return ""; - return uri.substr(q + 1); -} + std::vector envVars; -std::vector CgiHandler::buildEnv(const HttpRequest& req, const std::string& scriptPath) -{ - std::vector env; + envVars.push_back("REQUEST_METHOD=" + request.method); + envVars.push_back("QUERY_STRING=" + extractQueryString(request.uri)); + envVars.push_back("SCRIPT_FILENAME=" + scriptPath); - 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::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 it; + std::map::const_iterator headerIt; - it = req.headers.find("Content-Type"); - if (it != req.headers.end()) - env.push_back("CONTENT_TYPE=" + it->second); + 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..289139f 100644 --- a/src/http/cgi/execute.cpp +++ b/src/http/cgi/execute.cpp @@ -1,136 +1,163 @@ #include "../../../include/http/CgiHandler.hpp" +#include "../../../include/http/utils/HttpUtils.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) +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"); + 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(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) - return buildError(500, "Internal Server Error"); + int stdinPipe[2]; + int stdoutPipe[2]; + if (pipe(stdinPipe) == -1 || pipe(stdoutPipe) == -1) + return buildHttpError(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]); - return buildError(500, "Internal Server Error"); + close(stdinPipe[0]); close(stdinPipe[1]); + close(stdoutPipe[0]); close(stdoutPipe[1]); + return buildHttpError(500, "Internal Server Error"); } - if (pid == 0) - runChild(interpreter, scriptPath, argv, envp.data(), stdin_pipe, stdout_pipe); - - close(stdin_pipe[0]); - close(stdout_pipe[1]); - - if (!req.body.empty()) - write(stdin_pipe[1], req.body.c_str(), req.body.size()); - close(stdin_pipe[1]); + if (childPid == 0) + runChildProcess(interpreter, scriptPath, argv, envPointers.data(), stdinPipe, stdoutPipe); - std::string output = readOutputWithTimeout(stdout_pipe[0], pid); + close(stdinPipe[0]); + close(stdoutPipe[1]); - if (output.empty()) + if (!request.body.empty()) { - // timeout ou sortie vide — le waitpid a déjà été fait dans readOutputWithTimeout - return buildError(504, "Gateway Timeout"); + 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]); + + bool hasTimedOut = false; + std::string cgiOutput = readCgiOutputWithTimeout(stdoutPipe[0], childPid, hasTimedOut); + + if (hasTimedOut) + return buildHttpError(504, "Gateway Timeout"); + + int childExitStatus = 0; + waitpid(childPid, &childExitStatus, 0); + + if (WIFEXITED(childExitStatus) && WEXITSTATUS(childExitStatus) != 0) + return buildHttpError(500, "Internal Server Error"); - int status; - waitpid(pid, &status, 0); + if (WIFSIGNALED(childExitStatus)) + return buildHttpError(500, "Internal Server Error"); - if (WIFEXITED(status) && WEXITSTATUS(status) != 0) - return buildError(500, "Internal Server Error"); + if (cgiOutput.empty()) + return buildHttpError(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..a8c362f 100644 --- a/src/http/cgi/output.cpp +++ b/src/http/cgi/output.cpp @@ -1,84 +1,75 @@ #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); - if (key == "Status") + std::size_t valueStart = 0; + while (valueStart < headerValue.size() && headerValue[valueStart] == ' ') + valueStart++; + headerValue = headerValue.substr(valueStart); + + 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(); - - return res; -} - -HttpResponse CgiHandler::buildError(int code, const std::string& msg) -{ - HttpResponse res; - std::ostringstream ss; + std::ostringstream contentLengthStream; + contentLengthStream << response.body.size(); + response.headers["Content-Length"] = contentLengthStream.str(); - 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; + return response; } diff --git a/src/http/handlers/DeleteHandler.cpp b/src/http/handlers/DeleteHandler.cpp index f11d7d2..525b1c5 100644 --- a/src/http/handlers/DeleteHandler.cpp +++ b/src/http/handlers/DeleteHandler.cpp @@ -1,27 +1,25 @@ #include "../../../include/http/MethodHandler.hpp" +#include "../../../include/http/utils/HttpUtils.hpp" #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 fileInfo; + if (stat(filePath.c_str(), &fileInfo) == -1) + return buildHttpError(404, "Not Found"); - struct stat st; - if (stat(path.c_str(), &st) == -1) - return buildError(404, "Not Found"); + if (S_ISDIR(fileInfo.st_mode)) + return buildHttpError(403, "Forbidden"); - if (S_ISDIR(st.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"); + return buildHttpError(403, "Forbidden"); } diff --git a/src/http/handlers/GetHandler.cpp b/src/http/handlers/GetHandler.cpp index 1540c46..13a05c9 100644 --- a/src/http/handlers/GetHandler.cpp +++ b/src/http/handlers/GetHandler.cpp @@ -1,102 +1,85 @@ #include "../../../include/http/MethodHandler.hpp" +#include "../../../include/http/utils/HttpUtils.hpp" #include #include #include #include #include -static std::string getContentType(const std::string& path) +static HttpResponse buildAutoindex(const std::string& directoryPath, const std::string& requestUri) { - std::string ext; - std::size_t dot = path.rfind('.'); - if (dot != std::string::npos) - ext = path.substr(dot); - - 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"; - return "application/octet-stream"; -} - -static HttpResponse buildAutoindex(const std::string& path, const std::string& uri) -{ - 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"); + return buildHttpError(403, "Forbidden"); } - int fd = open(path.c_str(), O_RDONLY); - if (fd == -1) - return buildError(404, "Not Found"); + int fileDescriptor = open(filePath.c_str(), O_RDONLY); + if (fileDescriptor == -1) + return buildHttpError(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..5a7e7d2 100644 --- a/src/http/handlers/MethodHandler.cpp +++ b/src/http/handlers/MethodHandler.cpp @@ -1,70 +1,72 @@ #include "../../../include/http/MethodHandler.hpp" #include "../../../include/http/CgiHandler.hpp" +#include "../../../include/http/utils/HttpUtils.hpp" +#include "../../../include/http/utils/StringUtils.hpp" #include -#include -static bool isCgiRequest(const HttpRequest& req, const LocationConfig& loc) +static bool isCgiRequest(const HttpRequest& request, const LocationConfig& location) { - if (loc.getCgiExtension().empty() || loc.getCgiPath().empty()) + 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; +} + +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::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 buildHttpError(400, "Bad Request"); + + if (location.getPath().empty()) + return buildHttpError(404, "Not Found"); + + if (!isMethodAllowed(request.method, location)) + return buildHttpError(405, "Method Not Allowed"); - if (!isMethodAllowed(req.method, loc)) - return buildError(405, "Method Not Allowed"); + bool bodyLimitIsSet = server.getMaxBodySize() > 0; + bool bodyExceedsLimit = request.body.size() > server.getMaxBodySize(); + if (bodyLimitIsSet && bodyExceedsLimit) + return buildHttpError(413, "Payload Too Large"); - if (!loc.getRedirectUrl().empty()) + 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); - - return buildError(405, "Method Not Allowed"); -} - -bool MethodHandler::isMethodAllowed(const std::string& method, const LocationConfig& loc) -{ - const std::vector& methods = loc.getAllowedMethods(); - return std::find(methods.begin(), methods.end(), method) != methods.end(); -} - -HttpResponse MethodHandler::buildError(int code, const std::string& msg) -{ - HttpResponse res; - std::ostringstream ss; + if (request.method == "GET") + return handleGet(request, location); + if (request.method == "POST") + return handlePost(request, location); + if (request.method == "DELETE") + return handleDelete(request, location); - 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; + return buildHttpError(405, "Method Not Allowed"); } diff --git a/src/http/handlers/PostHandler.cpp b/src/http/handlers/PostHandler.cpp index b4b785c..454c9f5 100644 --- a/src/http/handlers/PostHandler.cpp +++ b/src/http/handlers/PostHandler.cpp @@ -1,64 +1,51 @@ #include "../../../include/http/MethodHandler.hpp" +#include "../../../include/http/utils/HttpUtils.hpp" +#include "../../../include/http/utils/StringUtils.hpp" #include #include #include -static std::string extractFilename(const std::string& uri) +HttpResponse MethodHandler::handlePost(const HttpRequest& request, const LocationConfig& location) { - std::size_t slash = uri.rfind('/'); - if (slash == std::string::npos || slash + 1 >= uri.size()) - return ""; - return uri.substr(slash + 1); -} - -static bool hasPathTraversal(const std::string& uri) -{ - return uri.find("..") != std::string::npos; -} + if (location.getUploadPath().empty()) + return buildHttpError(500, "Internal Server Error"); -HttpResponse MethodHandler::handlePost(const HttpRequest& req, const LocationConfig& loc) -{ - if (loc.getUploadPath().empty()) - return buildError(500, "Internal Server Error"); - - if (req.body.empty()) - return buildError(400, "Bad Request"); + if (request.body.empty()) + return buildHttpError(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"); + return buildHttpError(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) - return buildError(403, "Forbidden"); + int fileDescriptor = open(destinationPath.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fileDescriptor == -1) + return buildHttpError(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); - return buildError(507, "Insufficient Storage"); + close(fileDescriptor); + return buildHttpError(507, "Insufficient Storage"); } - written += n; + totalBytesWritten += static_cast(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..03e12a1 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) +bool RequestParser::isValid(const std::string& rawRequest) { - HttpRequest req; - - if (!isValid(raw)) - return req; - - std::size_t firstLineEnd = raw.find("\r\n"); - std::size_t headerBodySep = raw.find("\r\n\r\n"); - - parseFirstLine(raw.substr(0, firstLineEnd), req); - parseHeaders(raw, req, firstLineEnd, headerBodySep); - parseBody(raw, req, headerBodySep); - return req; -} - -bool RequestParser::isValid(const std::string& raw) -{ - 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); +} + +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; } 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/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); +} diff --git a/tests/http/integration/test_adversarial.cpp b/tests/http/integration/test_adversarial.cpp new file mode 100644 index 0000000..6699f47 --- /dev/null +++ b/tests/http/integration/test_adversarial.cpp @@ -0,0 +1,338 @@ +#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; + // 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); + } + + // 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")