diff --git a/.gitignore b/.gitignore index ef84509..4843405 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.o +obj/ # test binaries (built locally from tests/) test_parser diff --git a/Makefile b/Makefile index ca7b5db..1507670 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ CXX = c++ CXXFLAGS = -Wall -Wextra -Werror -std=c++98 SRCS = $(shell find src -name "*.cpp") -OBJS = $(SRCS:.cpp=.o) +OBJS = $(SRCS:src/%.cpp=obj/%.o) INCS = -I include all: $(NAME) @@ -11,11 +11,12 @@ all: $(NAME) $(NAME): $(OBJS) $(CXX) $(CXXFLAGS) $(OBJS) -o $(NAME) -%.o: %.cpp +obj/%.o: src/%.cpp + @mkdir -p $(dir $@) $(CXX) $(CXXFLAGS) $(INCS) -c $< -o $@ clean: - rm -f $(OBJS) + rm -rf obj fclean: clean rm -f $(NAME) diff --git a/include/http/RequestParser.hpp b/include/http/RequestParser.hpp index 2bb219e..806553a 100644 --- a/include/http/RequestParser.hpp +++ b/include/http/RequestParser.hpp @@ -3,16 +3,29 @@ #include "HttpRequest.hpp" #include +#include class RequestParser { public: + class ParseException : public std::exception { + public: + ParseException(int code, const std::string& msg) : _code(code), _msg(msg) {} + ~ParseException() throw() {} + int getCode() const { return _code; } + const char* what() const throw() { return _msg.c_str(); } + private: + int _code; + std::string _msg; + }; + HttpRequest parse(const std::string& raw); private: - bool isValid(const std::string& raw); - void parseFirstLine(const std::string& line, HttpRequest& req); - void parseHeaders(const std::string& raw, HttpRequest& req, std::size_t firstLineEnd, std::size_t headerBodySep); - void parseBody(const std::string& raw, HttpRequest& req, std::size_t headerBodySep); + void parseRequestLine(const std::string& firstLine, HttpRequest& req); + void parseHeaderLine(const std::string& line, std::string& key, std::string& value); + void validateHostHeader(const std::string& value, bool& hasHostHeader); + void parseHeaders(const std::string& raw, HttpRequest& req, std::size_t firstLineEnd, std::size_t headerBodySep); + void parseBody(const std::string& raw, HttpRequest& req, std::size_t headerBodySep); }; #endif \ No newline at end of file diff --git a/include/http/builders/HttpBuilders.hpp b/include/http/builders/HttpBuilders.hpp new file mode 100644 index 0000000..27e1cf5 --- /dev/null +++ b/include/http/builders/HttpBuilders.hpp @@ -0,0 +1,13 @@ +#ifndef HTTP_BUILDERS_HPP +#define HTTP_BUILDERS_HPP + +#include "../HttpResponse.hpp" +#include + +HttpResponse buildHttpError(int statusCode, const std::string& statusMessage); +HttpResponse buildHttpOk(const std::string& body, const std::string& contentType); +HttpResponse buildHttpCreated(const std::string& body); +HttpResponse buildHttpNoContent(); +HttpResponse buildRedirect(const std::string& url); + +#endif diff --git a/include/http/processHttp.hpp b/include/http/processHttp.hpp new file mode 100644 index 0000000..4165366 --- /dev/null +++ b/include/http/processHttp.hpp @@ -0,0 +1,9 @@ +#ifndef PROCESS_HTTP_HPP +#define PROCESS_HTTP_HPP + +#include "../config/ServerConfig.hpp" +#include + +std::string processHttp(const std::string& rawRequest, const ServerConfig& server); + +#endif diff --git a/include/http/utils/HttpUtils.hpp b/include/http/utils/HttpUtils.hpp index dbadd9a..2bdd32c 100644 --- a/include/http/utils/HttpUtils.hpp +++ b/include/http/utils/HttpUtils.hpp @@ -1,10 +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); +bool writeFdFromString(int fd, const std::string& data); +bool readFdToString(int fd, std::string& body); +std::string getContentType(const std::string& filePath); #endif diff --git a/include/http/utils/StringUtils.hpp b/include/http/utils/StringUtils.hpp index 1900df8..390f8fb 100644 --- a/include/http/utils/StringUtils.hpp +++ b/include/http/utils/StringUtils.hpp @@ -5,6 +5,7 @@ std::string urlDecode(const std::string& encoded); bool hasPathTraversal(const std::string& uri); +std::string extractUriPath(const std::string& uri); std::string extractQueryString(const std::string& uri); std::string extractFilename(const std::string& uri); diff --git a/src/http/builders/buildHttpCreated.cpp b/src/http/builders/buildHttpCreated.cpp new file mode 100644 index 0000000..86a9abf --- /dev/null +++ b/src/http/builders/buildHttpCreated.cpp @@ -0,0 +1,16 @@ +#include "../../../include/http/builders/HttpBuilders.hpp" +#include + +HttpResponse buildHttpCreated(const std::string& body) +{ + HttpResponse response; + std::ostringstream contentLength; + + response.status_code = 201; + response.status_msg = "Created"; + response.body = body; + response.headers["Content-Type"] = "text/plain"; + contentLength << body.size(); + response.headers["Content-Length"] = contentLength.str(); + return response; +} diff --git a/src/http/builders/buildHttpError.cpp b/src/http/builders/buildHttpError.cpp new file mode 100644 index 0000000..59f2994 --- /dev/null +++ b/src/http/builders/buildHttpError.cpp @@ -0,0 +1,16 @@ +#include "../../../include/http/builders/HttpBuilders.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/builders/buildHttpNoContent.cpp b/src/http/builders/buildHttpNoContent.cpp new file mode 100644 index 0000000..3593475 --- /dev/null +++ b/src/http/builders/buildHttpNoContent.cpp @@ -0,0 +1,10 @@ +#include "../../../include/http/builders/HttpBuilders.hpp" + +HttpResponse buildHttpNoContent() +{ + HttpResponse response; + response.status_code = 204; + response.status_msg = "No Content"; + response.headers["Content-Length"] = "0"; + return response; +} diff --git a/src/http/builders/buildHttpOk.cpp b/src/http/builders/buildHttpOk.cpp new file mode 100644 index 0000000..6f1063d --- /dev/null +++ b/src/http/builders/buildHttpOk.cpp @@ -0,0 +1,16 @@ +#include "../../../include/http/builders/HttpBuilders.hpp" +#include + +HttpResponse buildHttpOk(const std::string& body, const std::string& contentType) +{ + HttpResponse response; + std::ostringstream contentLength; + + response.status_code = 200; + response.status_msg = "OK"; + response.body = body; + response.headers["Content-Type"] = contentType; + contentLength << body.size(); + response.headers["Content-Length"] = contentLength.str(); + return response; +} diff --git a/src/http/builders/buildRedirect.cpp b/src/http/builders/buildRedirect.cpp new file mode 100644 index 0000000..82ee840 --- /dev/null +++ b/src/http/builders/buildRedirect.cpp @@ -0,0 +1,11 @@ +#include "../../../include/http/builders/HttpBuilders.hpp" + +HttpResponse buildRedirect(const std::string& url) +{ + HttpResponse response; + response.status_code = 301; + response.status_msg = "Moved Permanently"; + response.headers["Location"] = url; + response.headers["Content-Length"] = "0"; + return response; +} diff --git a/src/http/cgi/env.cpp b/src/http/cgi/env.cpp index 43b5475..5e2a4b5 100644 --- a/src/http/cgi/env.cpp +++ b/src/http/cgi/env.cpp @@ -11,15 +11,11 @@ std::vector CgiHandler::buildEnv(const HttpRequest& request, 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); + envVars.push_back("PATH_INFO=" + extractUriPath(request.uri)); std::map::const_iterator headerIt; - headerIt = request.headers.find("Content-Type"); + headerIt = request.headers.find("content-type"); if (headerIt != request.headers.end()) envVars.push_back("CONTENT_TYPE=" + headerIt->second); else @@ -34,7 +30,7 @@ std::vector CgiHandler::buildEnv(const HttpRequest& request, else envVars.push_back("CONTENT_LENGTH=0"); - headerIt = request.headers.find("Host"); + headerIt = request.headers.find("host"); if (headerIt != request.headers.end()) envVars.push_back("HTTP_HOST=" + headerIt->second); diff --git a/src/http/cgi/execute.cpp b/src/http/cgi/execute.cpp index 23f5567..3cfe73a 100644 --- a/src/http/cgi/execute.cpp +++ b/src/http/cgi/execute.cpp @@ -1,4 +1,6 @@ #include "../../../include/http/CgiHandler.hpp" +#include "../../../include/http/builders/HttpBuilders.hpp" +#include "../../../include/http/utils/StringUtils.hpp" #include "../../../include/http/utils/HttpUtils.hpp" #include #include @@ -6,31 +8,59 @@ #include #include +// durée maximale d'exécution d'un script CGI avant SIGKILL + 504 #define CGI_TIMEOUT_SEC 5 -static std::string buildScriptPath(const HttpRequest& request, const LocationConfig& location) +// vérifie que le script existe (404) et est exécutable (403) +// retourne une réponse vide (status_code == 0) si tout est OK +static HttpResponse validateScript(const std::string& scriptPath) +{ + if (access(scriptPath.c_str(), F_OK) == -1) + return buildHttpError(404, "Not Found"); + if (access(scriptPath.c_str(), X_OK) == -1) + return buildHttpError(403, "Forbidden"); + HttpResponse ok; + return ok; +} + +// inspecte le code de sortie du process enfant après waitpid() +// retourne une réponse vide si le process s'est terminé normalement (exit 0) +static HttpResponse checkChildStatus(int exitStatus) { - std::string uriWithoutQuery = request.uri; - std::size_t queryStart = uriWithoutQuery.find('?'); - if (queryStart != std::string::npos) - uriWithoutQuery = uriWithoutQuery.substr(0, queryStart); + // Erreur: le script a retourné un code non-zéro + if (WIFEXITED(exitStatus) && WEXITSTATUS(exitStatus) != 0) + return buildHttpError(500, "Internal Server Error"); + // Erreur: le script a été tué par un signal (ex: SIGSEGV) + if (WIFSIGNALED(exitStatus)) + return buildHttpError(500, "Internal Server Error"); + HttpResponse ok; + return ok; +} +// construit le chemin absolu du script +// ex: cwd="/srv/webserv" + root="www" + uri="/cgi-bin/hello.py" → "/srv/webserv/www/cgi-bin/hello.py" +static std::string buildScriptPath(const HttpRequest& request, const LocationConfig& location) +{ char currentWorkingDir[4096]; if (getcwd(currentWorkingDir, sizeof(currentWorkingDir)) == NULL) return ""; - return std::string(currentWorkingDir) + "/" + location.getRoot() + uriWithoutQuery; + return std::string(currentWorkingDir) + "/" + location.getRoot() + extractUriPath(request.uri); } +// exécuté dans le process enfant après fork() : +// redirige stdin/stdout vers les pipes, change de répertoire, puis execve le script static void runChildProcess(const std::string& interpreter, const std::string& scriptPath, char** argv, char** envp, int stdinPipe[2], int stdoutPipe[2]) { + // redirige stdin/stdout vers les pipes dup2(stdinPipe[0], STDIN_FILENO); dup2(stdoutPipe[1], STDOUT_FILENO); close(stdinPipe[0]); close(stdinPipe[1]); close(stdoutPipe[0]); close(stdoutPipe[1]); + // se place dans le répertoire du script (certains scripts utilisent des chemins relatifs) std::string scriptDirectory = scriptPath.substr(0, scriptPath.rfind('/')); chdir(scriptDirectory.c_str()); @@ -38,6 +68,9 @@ static void runChildProcess(const std::string& interpreter, const std::string& s _exit(1); } +// lit la sortie du script CGI avec un timeout +// utilise un read() non-bloquant + usleep pour éviter un second poll() +// si le timeout est dépassé : SIGKILL + sets hasTimedOut = true + retourne "" static std::string readCgiOutputWithTimeout(int pipeReadEnd, pid_t childPid, bool& hasTimedOut) { std::string cgiOutput; @@ -54,14 +87,17 @@ static std::string readCgiOutputWithTimeout(int pipeReadEnd, pid_t childPid, boo if (bytesRead > 0) { + // données disponibles : on accumule cgiOutput.append(readBuffer, bytesRead); } else if (bytesRead == 0) { + // EOF : le script a fermé son stdout, on a tout lu break; } else { + // EAGAIN : pas encore de données, on vérifie le timeout if (time(NULL) >= timeoutDeadline) { hasTimedOut = true; @@ -80,17 +116,17 @@ static std::string readCgiOutputWithTimeout(int pipeReadEnd, pid_t childPid, boo HttpResponse CgiHandler::execute(const HttpRequest& request, const LocationConfig& location) { - std::string scriptPath = buildScriptPath(request, location); - - if (access(scriptPath.c_str(), F_OK) == -1) - return buildHttpError(404, "Not Found"); - if (access(scriptPath.c_str(), X_OK) == -1) - return buildHttpError(403, "Forbidden"); + // 1. valider le script + std::string scriptPath = buildScriptPath(request, location); + HttpResponse scriptError = validateScript(scriptPath); + if (scriptError.status_code != 0) + return scriptError; + // 2. préparer les variables d'environnement CGI et argv 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())); + for (std::size_t i = 0; i < envVars.size(); i++) + envPointers.push_back(const_cast(envVars[i].c_str())); envPointers.push_back(NULL); std::string interpreter = location.getCgiPath(); @@ -100,17 +136,18 @@ HttpResponse CgiHandler::execute(const HttpRequest& request, const LocationConfi NULL }; + // 3. créer les pipes stdin/stdout int stdinPipe[2]; int stdoutPipe[2]; if (pipe(stdinPipe) == -1) return buildHttpError(500, "Internal Server Error"); if (pipe(stdoutPipe) == -1) { - close(stdinPipe[0]); - close(stdinPipe[1]); + close(stdinPipe[0]); close(stdinPipe[1]); return buildHttpError(500, "Internal Server Error"); } + // 4. fork : le child exécute le script, le parent gère la communication pid_t childPid = fork(); if (childPid == -1) { @@ -122,44 +159,32 @@ HttpResponse CgiHandler::execute(const HttpRequest& request, const LocationConfi if (childPid == 0) runChildProcess(interpreter, scriptPath, argv, &envPointers[0], stdinPipe, stdoutPipe); + // 5. parent : ferme les extrémités inutiles, ignore SIGPIPE (broken pipe si le script quitte tôt) close(stdinPipe[0]); close(stdoutPipe[1]); - signal(SIGPIPE, SIG_IGN); + // 6. envoie le body de la requête sur stdin du script 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); - } - } + writeFdFromString(stdinPipe[1], request.body); close(stdinPipe[1]); + // 7. lit la sortie du script avec timeout bool hasTimedOut = false; std::string cgiOutput = readCgiOutputWithTimeout(stdoutPipe[0], childPid, hasTimedOut); if (hasTimedOut) return buildHttpError(504, "Gateway Timeout"); + // 8. attend la fin du process et vérifie son code de sortie int childExitStatus = 0; waitpid(childPid, &childExitStatus, 0); - if (WIFEXITED(childExitStatus) && WEXITSTATUS(childExitStatus) != 0) - return buildHttpError(500, "Internal Server Error"); - - if (WIFSIGNALED(childExitStatus)) - return buildHttpError(500, "Internal Server Error"); + HttpResponse statusError = checkChildStatus(childExitStatus); + if (statusError.status_code != 0) + return statusError; + // 9. parse la sortie CGI (headers + body) et retourne la réponse HTTP if (cgiOutput.empty()) return buildHttpError(500, "Internal Server Error"); diff --git a/src/http/cgi/output.cpp b/src/http/cgi/output.cpp index f75478c..25833b1 100644 --- a/src/http/cgi/output.cpp +++ b/src/http/cgi/output.cpp @@ -1,4 +1,5 @@ #include "../../../include/http/CgiHandler.hpp" +#include "../../../include/http/builders/HttpBuilders.hpp" #include HttpResponse CgiHandler::parseOutput(const std::string& cgiOutput) @@ -10,17 +11,9 @@ HttpResponse CgiHandler::parseOutput(const std::string& cgiOutput) if (separatorPos == std::string::npos) separatorPos = cgiOutput.find("\n\n"); + // Pas de séparateur header/body → tout le output est le body if (separatorPos == std::string::npos) - { - 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; - } + return buildHttpOk(cgiOutput, "text/html"); std::size_t bodyStart = separatorPos + (usesDoubleCRLF ? 4 : 2); diff --git a/src/http/handlers/DeleteHandler.cpp b/src/http/handlers/DeleteHandler.cpp index 290ec7d..9527899 100644 --- a/src/http/handlers/DeleteHandler.cpp +++ b/src/http/handlers/DeleteHandler.cpp @@ -1,18 +1,16 @@ #include "../../../include/http/MethodHandler.hpp" +#include "../../../include/http/builders/HttpBuilders.hpp" #include "../../../include/http/utils/HttpUtils.hpp" +#include "../../../include/http/utils/StringUtils.hpp" #include #include #include -HttpResponse MethodHandler::handleDelete(const HttpRequest& request, const LocationConfig& location) -{ - std::string uriPath = request.uri; - std::size_t queryPos = uriPath.find('?'); - if (queryPos != std::string::npos) - uriPath = uriPath.substr(0, queryPos); - - std::string filePath = location.getRoot() + uriPath; +// vérifie l'existence du fichier et le supprime +// retourne 204 No Content ou une erreur +static HttpResponse deleteFile(const std::string& filePath) +{ struct stat fileInfo; if (stat(filePath.c_str(), &fileInfo) == -1) { @@ -21,15 +19,18 @@ HttpResponse MethodHandler::handleDelete(const HttpRequest& request, const Locat return buildHttpError(404, "Not Found"); } + // Erreur: on ne peut pas supprimer un répertoire if (S_ISDIR(fileInfo.st_mode)) return buildHttpError(403, "Forbidden"); if (unlink(filePath.c_str()) == 0) - { - HttpResponse response; - response.status_code = 204; - response.status_msg = "No Content"; - return response; - } + return buildHttpNoContent(); return buildHttpError(403, "Forbidden"); } + +HttpResponse MethodHandler::handleDelete(const HttpRequest& request, const LocationConfig& location) +{ + std::string uriPath = extractUriPath(request.uri); + std::string filePath = location.getRoot() + uriPath; + return deleteFile(filePath); +} diff --git a/src/http/handlers/GetHandler.cpp b/src/http/handlers/GetHandler.cpp index 90f4335..5987d12 100644 --- a/src/http/handlers/GetHandler.cpp +++ b/src/http/handlers/GetHandler.cpp @@ -1,5 +1,7 @@ #include "../../../include/http/MethodHandler.hpp" +#include "../../../include/http/builders/HttpBuilders.hpp" #include "../../../include/http/utils/HttpUtils.hpp" +#include "../../../include/http/utils/StringUtils.hpp" #include #include #include @@ -7,6 +9,7 @@ #include #include +// génère une page HTML listant le contenu d'un répertoire static HttpResponse buildAutoindex(const std::string& directoryPath, const std::string& requestUri) { DIR* directory = opendir(directoryPath.c_str()); @@ -30,50 +33,45 @@ static HttpResponse buildAutoindex(const std::string& directoryPath, const std:: closedir(directory); listingHtml += ""; - 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; + return buildHttpOk(listingHtml, "text/html"); } -HttpResponse MethodHandler::handleGet(const HttpRequest& request, const LocationConfig& location) + +// retourne le path complet du fichier index s'il existe, sinon "" +// ex: dirPath="/var/www/", index="index.html" → "/var/www/index.html" si le fichier existe +static std::string resolveIndexPath(const std::string& dirPath, const LocationConfig& location) { - std::string uriPath = request.uri; - std::size_t queryPos = uriPath.find('?'); - if (queryPos != std::string::npos) - uriPath = uriPath.substr(0, queryPos); + if (location.getIndex().empty()) + return ""; - std::string filePath = location.getRoot() + uriPath; + std::string indexPath = dirPath; + if (indexPath[indexPath.size() - 1] != '/') + indexPath += '/'; + indexPath += location.getIndex(); - struct stat fileInfo; - if (stat(filePath.c_str(), &fileInfo) == 0 && S_ISDIR(fileInfo.st_mode)) - { - if (!location.getIndex().empty()) - { - std::string indexPath = filePath; - if (indexPath[indexPath.size() - 1] != '/') - indexPath += '/'; - indexPath += location.getIndex(); - - struct stat indexInfo; - if (stat(indexPath.c_str(), &indexInfo) == 0) - filePath = indexPath; - else if (location.getAutoindex()) - return buildAutoindex(filePath, uriPath); - else - return buildHttpError(404, "Not Found"); - } - else if (location.getAutoindex()) - return buildAutoindex(filePath, uriPath); - else - return buildHttpError(403, "Forbidden"); - } + struct stat indexInfo; + if (stat(indexPath.c_str(), &indexInfo) == 0) + return indexPath; + return ""; +} + +// gère le cas répertoire quand aucun index n'a été trouvé +// priorité : autoindex → 404 (index configuré mais absent) → 403 (pas d'index du tout) +static HttpResponse handleDirectory(const std::string& dirPath, const std::string& uriPath, + const LocationConfig& location) +{ + if (location.getAutoindex()) + return buildAutoindex(dirPath, uriPath); + if (!location.getIndex().empty()) + return buildHttpError(404, "Not Found"); + + return buildHttpError(403, "Forbidden"); +} + +// ouvre et lit un fichier, retourne une réponse 200 avec le body et les headers corrects +static HttpResponse buildFileResponse(const std::string& filePath) +{ int fileDescriptor = open(filePath.c_str(), O_RDONLY); if (fileDescriptor == -1) { @@ -82,25 +80,26 @@ HttpResponse MethodHandler::handleGet(const HttpRequest& request, const Location return buildHttpError(404, "Not Found"); } - HttpResponse response; - char readBuffer[4096]; - ssize_t bytesRead; + std::string body; + if (!readFdToString(fileDescriptor, body)) + return buildHttpError(500, "Internal Server Error"); - while ((bytesRead = read(fileDescriptor, readBuffer, sizeof(readBuffer))) > 0) - response.body.append(readBuffer, bytesRead); - if (bytesRead < 0) + return buildHttpOk(body, getContentType(filePath)); +} + +HttpResponse MethodHandler::handleGet(const HttpRequest& request, const LocationConfig& location) +{ + std::string uriPath = extractUriPath(request.uri); + std::string filePath = location.getRoot() + uriPath; + + struct stat fileInfo; + if (stat(filePath.c_str(), &fileInfo) == 0 && S_ISDIR(fileInfo.st_mode)) { - close(fileDescriptor); - return buildHttpError(500, "Internal Server Error"); + std::string indexPath = resolveIndexPath(filePath, location); + if (!indexPath.empty()) + return buildFileResponse(indexPath); + return handleDirectory(filePath, uriPath, location); } - close(fileDescriptor); - - response.status_code = 200; - response.status_msg = "OK"; - response.headers["Content-Type"] = getContentType(filePath); - std::ostringstream contentLength; - contentLength << response.body.size(); - response.headers["Content-Length"] = contentLength.str(); - return response; + return buildFileResponse(filePath); } diff --git a/src/http/handlers/MethodHandler.cpp b/src/http/handlers/MethodHandler.cpp index c079cf7..0ae0c85 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/builders/HttpBuilders.hpp" #include "../../../include/http/utils/HttpUtils.hpp" #include "../../../include/http/utils/StringUtils.hpp" #include @@ -7,12 +8,16 @@ #include #include +// vérifie si l'URI cible un script CGI en comparant son extension avec celle configurée +// ex: uri="/cgi-bin/form.py", cgiExtension=".py" → true static bool isCgiRequest(const HttpRequest& request, const LocationConfig& location) { bool noCgiConfigured = location.getCgiExtension().empty() || location.getCgiPath().empty(); if (noCgiConfigured) return false; + // strip le query string avant de comparer l'extension + // ex: "/cgi-bin/form.py?name=foo" → "/cgi-bin/form.py" std::string uriWithoutQuery = request.uri; std::size_t queryStart = uriWithoutQuery.find('?'); if (queryStart != std::string::npos) @@ -23,6 +28,7 @@ static bool isCgiRequest(const HttpRequest& request, const LocationConfig& locat if (uriTooShort) return false; + // compare les derniers N chars de l'URI avec l'extension configurée std::string uriExtension = uriWithoutQuery.substr(uriWithoutQuery.size() - cgiExtension.size()); return uriExtension == cgiExtension; } @@ -33,11 +39,14 @@ bool MethodHandler::isMethodAllowed(const std::string& method, const LocationCon return std::find(allowedMethods.begin(), allowedMethods.end(), method) != allowedMethods.end(); } +// remplace le body de l'erreur par le fichier HTML configuré dans error_page +// ex: error_page 404 /errors/404.html → lit root + /errors/404.html et l'envoie +// si le fichier n'existe pas ou si le status n'est pas une erreur → retourne la réponse originale static HttpResponse applyCustomErrorPage(const HttpResponse& response, const ServerConfig& server, const LocationConfig& location) { - if (response.status_code < 400 || location.getRoot().empty()) + if (response.status_code < 400) return response; const std::map& errorPages = server.getErrorPages(); @@ -45,6 +54,7 @@ static HttpResponse applyCustomErrorPage(const HttpResponse& response, if (pageIt == errorPages.end()) return response; + // ex: root="/var/www" + path="/errors/404.html" = "/var/www/errors/404.html" std::string filePath = location.getRoot() + pageIt->second; int fd = open(filePath.c_str(), O_RDONLY); if (fd == -1) @@ -52,17 +62,8 @@ static HttpResponse applyCustomErrorPage(const HttpResponse& response, HttpResponse customResponse = response; customResponse.body = ""; - char buf[4096]; - ssize_t bytesRead; - - while ((bytesRead = read(fd, buf, sizeof(buf))) > 0) - customResponse.body.append(buf, bytesRead); - if (bytesRead < 0) - { - close(fd); + if (!readFdToString(fd, customResponse.body)) return response; - } - close(fd); customResponse.headers["Content-Type"] = getContentType(filePath); std::ostringstream contentLength; @@ -76,21 +77,20 @@ HttpResponse MethodHandler::handle(const HttpRequest& request, const LocationCon HttpResponse response; if (hasPathTraversal(request.uri)) + // Erreur: "GET /../../../etc/passwd HTTP/1.1" response = buildHttpError(400, "Bad Request"); else if (location.getPath().empty()) + // aucune location ne matche l'URI → le router a retourné une LocationConfig vide response = buildHttpError(404, "Not Found"); - else if (!isMethodAllowed(request.method, location)) + else if (!MethodHandler::isMethodAllowed(request.method, location)) + // ex: DELETE sur une location qui n'autorise que GET/POST response = buildHttpError(405, "Method Not Allowed"); else if (server.getMaxBodySize() > 0 && request.body.size() > server.getMaxBodySize()) + // ex: client_max_body_size 1m et body = 2Mo response = buildHttpError(413, "Payload Too Large"); else if (!location.getRedirectUrl().empty()) - { - response.status_code = 301; - response.status_msg = "Moved Permanently"; - response.headers["Location"] = location.getRedirectUrl(); - response.headers["Content-Length"] = "0"; - return response; - } + // retour direct : la redirect ne passe pas par applyCustomErrorPage + return buildRedirect(location.getRedirectUrl()); else if (isCgiRequest(request, location)) { CgiHandler cgiHandler; @@ -105,5 +105,6 @@ HttpResponse MethodHandler::handle(const HttpRequest& request, const LocationCon else response = buildHttpError(405, "Method Not Allowed"); + // remplace le body d'erreur par la page HTML custom si configurée return applyCustomErrorPage(response, server, location); } diff --git a/src/http/handlers/PostHandler.cpp b/src/http/handlers/PostHandler.cpp index 4811c21..e09fa29 100644 --- a/src/http/handlers/PostHandler.cpp +++ b/src/http/handlers/PostHandler.cpp @@ -1,61 +1,48 @@ #include "../../../include/http/MethodHandler.hpp" -#include "../../../include/http/utils/HttpUtils.hpp" +#include "../../../include/http/builders/HttpBuilders.hpp" #include "../../../include/http/utils/StringUtils.hpp" +#include "../../../include/http/utils/HttpUtils.hpp" #include #include -#include #include -HttpResponse MethodHandler::handlePost(const HttpRequest& request, const LocationConfig& location) -{ - if (location.getUploadPath().empty()) - return buildHttpError(500, "Internal Server Error"); - - if (request.body.empty()) - return buildHttpError(400, "Bad Request"); - - std::string uriPath = request.uri; - std::size_t queryPos = uriPath.find('?'); - if (queryPos != std::string::npos) - uriPath = uriPath.substr(0, queryPos); - - std::string filename = extractFilename(uriPath); - if (filename.empty()) - 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) +// ouvre le fichier de destination et écrit le body dedans +// retourne 201 Created avec Location, ou une erreur +static HttpResponse writeBodyToFile(const std::string& destPath, const std::string& body, + const std::string& filename) +{ + int fd = open(destPath.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd == -1) { if (errno == EACCES || errno == EPERM) return buildHttpError(403, "Forbidden"); return buildHttpError(500, "Internal Server Error"); } - const char* bodyData = request.body.data(); - std::size_t totalBytesToWrite = request.body.size(); - std::size_t totalBytesWritten = 0; - - while (totalBytesWritten < totalBytesToWrite) + if (!writeFdFromString(fd, body)) { - ssize_t bytesWritten = write(fileDescriptor, bodyData + totalBytesWritten, - totalBytesToWrite - totalBytesWritten); - if (bytesWritten <= 0) - { - close(fileDescriptor); - return buildHttpError(507, "Insufficient Storage"); - } - totalBytesWritten += static_cast(bytesWritten); + close(fd); + return buildHttpError(507, "Insufficient Storage"); } - close(fileDescriptor); + close(fd); - 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(); + HttpResponse response = buildHttpCreated(""); + response.headers["Location"] = "/" + filename; return response; } + +HttpResponse MethodHandler::handlePost(const HttpRequest& request, const LocationConfig& location) +{ + // Erreur: location sans upload_store configuré + if (location.getUploadPath().empty()) + return buildHttpError(500, "Internal Server Error"); + + std::string uriPath = extractUriPath(request.uri); + std::string filename = extractFilename(uriPath); + if (filename.empty()) + return buildHttpError(400, "Bad Request"); + + std::string destPath = location.getUploadPath() + "/" + filename; + return writeBodyToFile(destPath, request.body, filename); +} diff --git a/src/http/parser/RequestParser.cpp b/src/http/parser/RequestParser.cpp index 03e12a1..013ccaa 100644 --- a/src/http/parser/RequestParser.cpp +++ b/src/http/parser/RequestParser.cpp @@ -1,107 +1,152 @@ #include "../../../include/http/RequestParser.hpp" #include -bool RequestParser::isValid(const std::string& rawRequest) +void RequestParser::parseRequestLine(const std::string& firstLine, HttpRequest& request) { - if (rawRequest.find("\r\n\r\n") == std::string::npos) - return false; + std::istringstream lineStream(firstLine); + std::string extraToken; - 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); + // Erreur: "GET /\r\n\r\n" (version manquante, moins de 3 tokens) + if (!(lineStream >> request.method >> request.uri >> request.version)) + throw ParseException(400, "Bad Request"); - std::istringstream firstLineStream(firstLine); - std::string method, uri, version; - if (!(firstLineStream >> method >> uri >> version)) - return false; + // Erreur: "GET / HTTP/1.1 extra\r\n\r\n" (4ème token sur la request-line) + if (lineStream >> extraToken) + throw ParseException(400, "Bad Request"); - std::string extraToken; - if (firstLineStream >> extraToken) - return false; + // Erreur: "get / HTTP/1.1\r\n\r\n" (méthode en minuscules) + for (std::size_t i = 0; i < request.method.size(); i++) + if (request.method[i] < 'A' || request.method[i] > 'Z') + throw ParseException(400, "Bad Request"); - if (version.substr(0, 5) != "HTTP/") - return false; + // Erreur: "GET / MYPROTO/1.1\r\n\r\n" (protocole inconnu) + if (request.version.substr(0, 5) != "HTTP/") + throw ParseException(400, "Bad Request"); - bool hasHostHeader = false; - std::size_t currentPosition = firstLineEnd + 2; + // Erreur: "GET / HTTP/2.0\r\nHost: localhost\r\n\r\n" (version non supportée) + if (request.version != "HTTP/1.0" && request.version != "HTTP/1.1") + throw ParseException(505, "HTTP Version Not Supported"); +} - while (currentPosition < headerBodySeparator) - { - std::size_t lineEndPosition = rawRequest.find("\r\n", currentPosition); - std::string currentLine = rawRequest.substr(currentPosition, lineEndPosition - currentPosition); +void RequestParser::parseHeaderLine(const std::string& line, + std::string& headerKey, std::string& headerValue) +{ + std::size_t colonPosition = line.find(':'); + if (colonPosition == std::string::npos) + return; - std::size_t colonPosition = currentLine.find(':'); - if (colonPosition != std::string::npos) - { - bool hasTabAfterColon = (colonPosition + 1 < currentLine.size() - && currentLine[colonPosition + 1] == '\t'); - if (hasTabAfterColon) - return false; - if (currentLine.substr(0, colonPosition) == "Host") - hasHostHeader = true; - } - currentPosition = lineEndPosition + 2; - } + // Erreur: "Host:\tlocalhost" (tab juste après le colon) + if (colonPosition + 1 < line.size() && line[colonPosition + 1] == '\t') + throw ParseException(400, "Bad Request"); - if (version == "HTTP/1.1" && !hasHostHeader) - return false; + headerKey = line.substr(0, colonPosition); - return true; + std::size_t valueStart = colonPosition + 1; + while (valueStart < line.size() && line[valueStart] == ' ') + valueStart++; + + if (valueStart < line.size()) + headerValue = line.substr(valueStart); + else + headerValue = ""; } -void RequestParser::parseFirstLine(const std::string& firstLine, HttpRequest& request) +void RequestParser::validateHostHeader(const std::string& headerValue, bool& hasHostHeader) { - std::istringstream lineStream(firstLine); - lineStream >> request.method; - lineStream >> request.uri; - lineStream >> request.version; + // Erreur: "Host:\r\n" ou "Host: \r\n" (valeur vide ou whitespace-only) + if (headerValue.empty()) + throw ParseException(400, "Bad Request"); + + // Erreur: double Host header + if (hasHostHeader) + throw ParseException(400, "Bad Request"); + + hasHostHeader = true; } void RequestParser::parseHeaders(const std::string& rawRequest, HttpRequest& request, std::size_t firstLineEnd, std::size_t headerBodySeparator) { - std::size_t currentPosition = firstLineEnd + 2; - std::size_t headersEnd = headerBodySeparator; + bool hasHostHeader = false; + bool hasContentLengthHeader = false; + std::size_t currentPosition = firstLineEnd + 2; - while (currentPosition < headersEnd) + while (currentPosition < headerBodySeparator) { std::size_t lineEndPosition = rawRequest.find("\r\n", currentPosition); std::string currentLine = rawRequest.substr(currentPosition, lineEndPosition - currentPosition); - std::size_t colonPosition = currentLine.find(':'); - if (colonPosition != std::string::npos) + std::string headerKey; + std::string headerValue; + RequestParser::parseHeaderLine(currentLine, headerKey, headerValue); + + if (!headerKey.empty()) { - 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; + // les noms de headers sont case-insensitive : host/HOST/HoSt sont tous valides pour NGINX + // on normalise en lowercase pour que tous les lookups (parseBody, env.cpp) fonctionnent + // quelle que soit la casse envoyée par le client + std::string headerKeyLower = headerKey; + for (std::size_t i = 0; i < headerKeyLower.size(); i++) + headerKeyLower[i] = std::tolower(static_cast(headerKeyLower[i])); + + request.headers[headerKeyLower] = headerValue; + if (headerKeyLower == "host") + RequestParser::validateHostHeader(headerValue, hasHostHeader); + + // Erreur: double Content-Length + if (headerKeyLower == "content-length") + { + if (hasContentLengthHeader) + throw ParseException(400, "Bad Request"); + hasContentLengthHeader = true; + } } currentPosition = lineEndPosition + 2; } + + // Erreur: "GET / HTTP/1.1\r\nContent-Type: text/html\r\n\r\n" (pas de Host, obligatoire en HTTP/1.1) + if (request.version == "HTTP/1.1" && !hasHostHeader) + throw ParseException(400, "Bad Request"); } void RequestParser::parseBody(const std::string& rawRequest, HttpRequest& request, std::size_t headerBodySeparator) { - request.body = rawRequest.substr(headerBodySeparator + 4); + std::string rawBody = rawRequest.substr(headerBodySeparator + 4); + + std::map::const_iterator it = request.headers.find("content-length"); + if (it != request.headers.end()) + { + const std::string& clValue = it->second; + + // Erreur: "Content-Length: abc" ou "Content-Length: -1" (pas un entier positif) + for (std::size_t i = 0; i < clValue.size(); i++) + if (clValue[i] < '0' || clValue[i] > '9') + throw ParseException(400, "Bad Request"); + + std::istringstream iss(clValue); + std::size_t contentLength = 0; + iss >> contentLength; + request.body = rawBody.substr(0, contentLength); + } + else + request.body = rawBody; } HttpRequest RequestParser::parse(const std::string& rawRequest) { - HttpRequest request; + std::size_t headerBodySeparator = rawRequest.find("\r\n\r\n"); - if (!isValid(rawRequest)) - return request; + // Erreur: "GET / HTTP/1.1\r\nHost: localhost\r\n" (headers jamais fermés) + if (headerBodySeparator == std::string::npos) + throw ParseException(400, "Bad Request"); - std::size_t firstLineEnd = rawRequest.find("\r\n"); - std::size_t headerBodySeparator = rawRequest.find("\r\n\r\n"); + HttpRequest request; + std::size_t firstLineEnd = rawRequest.find("\r\n"); + + RequestParser::parseRequestLine(rawRequest.substr(0, firstLineEnd), request); + RequestParser::parseHeaders(rawRequest, request, firstLineEnd, headerBodySeparator); + RequestParser::parseBody(rawRequest, request, headerBodySeparator); - parseFirstLine(rawRequest.substr(0, firstLineEnd), request); - parseHeaders(rawRequest, request, firstLineEnd, headerBodySeparator); - parseBody(rawRequest, request, headerBodySeparator); return request; } diff --git a/src/http/processHttp.cpp b/src/http/processHttp.cpp new file mode 100644 index 0000000..7cd98fe --- /dev/null +++ b/src/http/processHttp.cpp @@ -0,0 +1,26 @@ +#include "../../include/http/processHttp.hpp" +#include "../../include/http/RequestParser.hpp" +#include "../../include/http/Router.hpp" +#include "../../include/http/MethodHandler.hpp" +#include "../../include/http/ResponseBuilder.hpp" +#include "../../include/http/builders/HttpBuilders.hpp" + +std::string processHttp(const std::string& rawRequest, const ServerConfig& server) +{ + ResponseBuilder builder; + RequestParser parser; + + try + { + HttpRequest request = parser.parse(rawRequest); + Router router; + LocationConfig location = router.route(request, server); + MethodHandler handler; + HttpResponse response = handler.handle(request, location, server); + return builder.build(response); + } + catch (const RequestParser::ParseException& e) + { + return builder.build(buildHttpError(e.getCode(), e.what())); + } +} diff --git a/src/http/response/ResponseBuilder.cpp b/src/http/response/ResponseBuilder.cpp index 85911f3..128e074 100644 --- a/src/http/response/ResponseBuilder.cpp +++ b/src/http/response/ResponseBuilder.cpp @@ -6,19 +6,28 @@ std::string ResponseBuilder::build(const HttpResponse& response) { std::ostringstream rawOutput; + // ex: "HTTP/1.1 200 OK" ou "HTTP/1.1 404 Not Found" rawOutput << "HTTP/1.1 " << response.status_code << " " << response.status_msg << "\r\n"; + // headers fixes présents dans toutes les réponses + rawOutput << "Server: webserv/1.0\r\n"; + + // date au format HTTP : "Fri, 19 Jun 2026 14:42:00 GMT" time_t now = time(NULL); struct tm* gmt = gmtime(&now); char dateBuf[64]; strftime(dateBuf, sizeof(dateBuf), "%a, %d %b %Y %H:%M:%S GMT", gmt); rawOutput << "Date: " << dateBuf << "\r\n"; + + // on ferme la connexion après chaque réponse rawOutput << "Connection: close\r\n"; + // headers variables : Content-Type, Content-Length, Location, etc. std::map::const_iterator headerIt; for (headerIt = response.headers.begin(); headerIt != response.headers.end(); ++headerIt) rawOutput << headerIt->first << ": " << headerIt->second << "\r\n"; + // ligne vide obligatoire qui sépare les headers du body rawOutput << "\r\n"; rawOutput << response.body; diff --git a/src/http/router/Router.cpp b/src/http/router/Router.cpp index 462f3ea..391c03e 100644 --- a/src/http/router/Router.cpp +++ b/src/http/router/Router.cpp @@ -1,9 +1,15 @@ #include "../../../include/http/Router.hpp" +// vérifie si une location de config matche l'URI de la requête +// ex: locationPath="/uploads", requestUri="/uploads/photo.jpg" → true +// ex: locationPath="/uploads", requestUri="/uploadsfoo" → false (pas de / après le prefix) static bool matchesLocation(const std::string& locationPath, const std::string& requestUri) { + // le prefix doit tenir dans l'URI if (locationPath.size() > requestUri.size()) return false; + + // l'URI doit commencer par le prefix exact if (requestUri.substr(0, locationPath.size()) != locationPath) return false; @@ -11,6 +17,7 @@ static bool matchesLocation(const std::string& locationPath, const std::string& bool nextCharIsSlash = requestUri[locationPath.size()] == '/'; bool locationIsRoot = locationPath == "/"; + // Erreur: "/uploadsfoo" ne doit pas matcher "/uploads" (le char suivant doit être /) if (uriContinuesAfterPrefix && !nextCharIsSlash && !locationIsRoot) return false; @@ -21,6 +28,8 @@ LocationConfig Router::route(const HttpRequest& request, const ServerConfig& ser { const std::vector& locations = server.getLocations(); + // strip le query string pour ne comparer que le path + // ex: "/uploads/photo.jpg?size=large" → "/uploads/photo.jpg" std::string uriPath = request.uri; std::size_t queryPos = uriPath.find('?'); if (queryPos != std::string::npos) @@ -29,17 +38,21 @@ LocationConfig Router::route(const HttpRequest& request, const ServerConfig& ser LocationConfig bestMatch; std::size_t longestMatchLength = 0; + // longest prefix match : la location la plus spécifique gagne + // ex: "/uploads/images" gagne sur "/uploads" pour "/uploads/images/foo.png" for (std::vector::const_iterator locationIt = locations.begin(); locationIt != locations.end(); ++locationIt) { + // strip le trailing slash de la location sauf si c'est "/" + // ex: "/uploads/" → "/uploads" std::string locationPath = locationIt->getPath(); if (locationPath.size() > 1 && locationPath[locationPath.size() - 1] == '/') locationPath = locationPath.substr(0, locationPath.size() - 1); if (matchesLocation(locationPath, uriPath) && locationPath.size() > longestMatchLength) { - bestMatch = *locationIt; - longestMatchLength = locationPath.size(); + bestMatch = *locationIt; + longestMatchLength = locationPath.size(); } } return bestMatch; diff --git a/src/http/utils/HttpUtils.cpp b/src/http/utils/HttpUtils.cpp index 44895ec..4419cca 100644 --- a/src/http/utils/HttpUtils.cpp +++ b/src/http/utils/HttpUtils.cpp @@ -1,18 +1,33 @@ #include "../../../include/http/utils/HttpUtils.hpp" -#include +#include -HttpResponse buildHttpError(int statusCode, const std::string& statusMessage) +// écrit le contenu de data dans fd en boucle jusqu'à épuisement +// retourne true si tout a été écrit, false si write() a échoué +bool writeFdFromString(int fd, const std::string& data) { - HttpResponse response; - std::ostringstream contentLength; + const char* ptr = data.data(); + std::size_t totalToWrite = data.size(); + std::size_t totalWritten = 0; - 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; + while (totalWritten < totalToWrite) + { + ssize_t written = write(fd, ptr + totalWritten, totalToWrite - totalWritten); + if (written <= 0) + return false; + totalWritten += static_cast(written); + } + return true; +} + +bool readFdToString(int fd, std::string& body) +{ + char buf[4096]; + ssize_t bytesRead = 0; + + while ((bytesRead = read(fd, buf, sizeof(buf))) > 0) + body.append(buf, bytesRead); + close(fd); + return bytesRead == 0; } std::string getContentType(const std::string& filePath) diff --git a/src/http/utils/StringUtils.cpp b/src/http/utils/StringUtils.cpp index eec38fe..4b3a12d 100644 --- a/src/http/utils/StringUtils.cpp +++ b/src/http/utils/StringUtils.cpp @@ -49,6 +49,14 @@ bool hasPathTraversal(const std::string& uri) return false; } +std::string extractUriPath(const std::string& uri) +{ + std::size_t queryPos = uri.find('?'); + if (queryPos != std::string::npos) + return uri.substr(0, queryPos); + return uri; +} + std::string extractQueryString(const std::string& uri) { std::size_t queryStart = uri.find('?'); diff --git a/tests/http/integration/test_adversarial.cpp b/tests/http/integration/test_adversarial.cpp index 6699f47..8a7d146 100644 --- a/tests/http/integration/test_adversarial.cpp +++ b/tests/http/integration/test_adversarial.cpp @@ -23,15 +23,24 @@ static HttpRequest makeReq(const std::string& method, const std::string& uri, req.method = method; req.uri = uri; req.version = "HTTP/1.1"; - req.headers["Host"] = host; + req.headers["host"] = host; if (!body.empty()) { req.body = body; - req.headers["Content-Type"] = "text/plain"; + req.headers["content-type"] = "text/plain"; } return req; } +// wrapper qui absorbe ParseException — utilisé pour les tests de parsing invalide +static HttpRequest safeParse(const std::string& raw) +{ + RequestParser p; + HttpRequest r; + try { r = p.parse(raw); } catch (const RequestParser::ParseException&) {} + return r; +} + static LocationConfig makeGetLoc(const std::string& root = "www") { LocationConfig loc; @@ -67,93 +76,82 @@ int main() // requête vide { - RequestParser p; - HttpRequest r = p.parse(""); + HttpRequest r = safeParse(""); 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"); + HttpRequest r = safeParse("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"); + HttpRequest r = safeParse("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"); + HttpRequest r = safeParse("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"); + HttpRequest r = safeParse("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"); + HttpRequest r = safeParse("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"); + HttpRequest r = safeParse("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); + safeParse(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); + safeParse(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); + HttpRequest r = safeParse(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); + HttpRequest r = safeParse(raw); check("parse: body 1MB → pas de crash", !r.method.empty()); check("parse: body 1MB → body correct", r.body.size() == body.size()); } diff --git a/tests/http/integration/test_cgi_advanced.cpp b/tests/http/integration/test_cgi_advanced.cpp index 0bac6fe..1632461 100644 --- a/tests/http/integration/test_cgi_advanced.cpp +++ b/tests/http/integration/test_cgi_advanced.cpp @@ -39,7 +39,7 @@ static HttpRequest makeGet(const std::string& uri, const std::string& host = "lo req.method = "GET"; req.uri = uri; req.version = "HTTP/1.1"; - req.headers["Host"] = host; + req.headers["host"] = host; return req; } @@ -51,8 +51,8 @@ static HttpRequest makePost(const std::string& uri, req.method = "POST"; req.uri = uri; req.version = "HTTP/1.1"; - req.headers["Host"] = "localhost"; - req.headers["Content-Type"] = ctype; + req.headers["host"] = "localhost"; + req.headers["content-type"] = ctype; req.body = body; return req; } diff --git a/tests/http/integration/test_post_handler.cpp b/tests/http/integration/test_post_handler.cpp index 4e0d499..d462b16 100644 --- a/tests/http/integration/test_post_handler.cpp +++ b/tests/http/integration/test_post_handler.cpp @@ -72,12 +72,12 @@ int main() check("POST sans upload_store: 500", res.status_code == 500); } - // ── CAS 3 : POST body vide → 400 ──────────────────────────────── + // ── CAS 3 : POST body vide → 201 (fichier vide créé, valide HTTP) ─ { LocationConfig loc = makeLoc("/tmp"); MethodHandler handler; HttpResponse res = handler.handle(makeReq("POST", "/uploads/empty.txt", ""), loc, server); - check("POST body vide: 400", res.status_code == 400); + check("POST body vide: 201", res.status_code == 201); } // ── CAS 4 : POST URI sans filename → 400 ──────────────────────── diff --git a/tests/http/unit/test_custom_error_page.cpp b/tests/http/unit/test_custom_error_page.cpp new file mode 100644 index 0000000..40f3c39 --- /dev/null +++ b/tests/http/unit/test_custom_error_page.cpp @@ -0,0 +1,115 @@ +#include "../../../include/http/MethodHandler.hpp" +#include "../../../include/config/ServerConfig.hpp" +#include "../../../include/config/LocationConfig.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) +{ + HttpRequest req; + req.method = method; + req.uri = uri; + req.version = "HTTP/1.1"; + return req; +} + +// location SANS root — simule /cgi-bin ou /old +static LocationConfig makeLocationNoRoot() +{ + LocationConfig loc; + loc.setPath("/cgi-bin"); + // pas de root, pas de méthodes → tout appel → 405 + return loc; +} + +// location AVEC root — cas normal +static LocationConfig makeLocationWithRoot() +{ + LocationConfig loc; + loc.setPath("/"); + loc.setRoot("/tmp/www"); // root = /tmp/www + loc.addMethod("GET"); + return loc; +} + +static ServerConfig makeServerWithCustomErrorPage() +{ + ServerConfig server; + server.setPort(8080); + server.setHost("127.0.0.1"); + // filePath = root + path = "/tmp/www" + "/errors/404.html" = "/tmp/www/errors/404.html" + server.addErrorPage(405, "/errors/404.html"); + return server; +} + +// TEST 1 : location sans root → avec le guard actuel, la custom error page est ignorée +static void test_no_root_location_skips_error_page() +{ + MethodHandler handler; + ServerConfig server = makeServerWithCustomErrorPage(); + LocationConfig location = makeLocationNoRoot(); + + HttpResponse response = handler.handle(makeReq("GET", "/cgi-bin/script"), location, server); + + std::cout << "\n[TEST] location sans root → status=" << response.status_code + << " body='" << response.body.substr(0, 30) << "'" << std::endl; + + check("status 405", response.status_code == 405); + // avec le guard location.getRoot().empty() → body = default error body + // sans le guard → body = contenu de /tmp/test_custom_404.html + bool usedCustomPage = response.body.find("Custom 404") != std::string::npos; + std::cout << " custom error page utilisée : " << (usedCustomPage ? "OUI" : "NON") << std::endl; +} + +// TEST 2 : location avec root → la custom error page doit être utilisée +static void test_with_root_location_uses_error_page() +{ + MethodHandler handler; + ServerConfig server = makeServerWithCustomErrorPage(); + LocationConfig location = makeLocationWithRoot(); + + // on force une 405 en envoyant DELETE sur une location qui n'autorise que GET + HttpResponse response = handler.handle(makeReq("DELETE", "/"), location, server); + + std::cout << "\n[TEST] location avec root → status=" << response.status_code + << " body='" << response.body.substr(0, 30) << "'" << std::endl; + + check("status 405", response.status_code == 405); + bool usedCustomPage = response.body.find("Custom 404") != std::string::npos; + check("custom error page utilisée", usedCustomPage); +} + +int main() +{ + // crée /tmp/www/errors/404.html avec le contenu attendu par les tests + ::mkdir("/tmp/www", 0755); + ::mkdir("/tmp/www/errors", 0755); + std::ofstream f("/tmp/www/errors/404.html"); + f << "Custom 404\n"; + f.close(); + + test_no_root_location_skips_error_page(); + test_with_root_location_uses_error_page(); + + std::cout << "\n" << passed << " passed, " << failed << " failed" << std::endl; + return failed == 0 ? 0 : 1; +} diff --git a/tests/http/unit/test_request_parser.cpp b/tests/http/unit/test_request_parser.cpp index 84855f4..06edf59 100644 --- a/tests/http/unit/test_request_parser.cpp +++ b/tests/http/unit/test_request_parser.cpp @@ -28,7 +28,7 @@ static void test_get_simple() check("GET simple: method", req.method == "GET"); check("GET simple: uri", req.uri == "/index.html"); check("GET simple: version", req.version == "HTTP/1.1"); - check("GET simple: host header", req.headers["Host"] == "localhost"); + check("GET simple: host header", req.headers["host"] == "localhost"); check("GET simple: body vide", req.body == ""); } @@ -45,7 +45,7 @@ static void test_post_with_body() check("POST body: method", req.method == "POST"); check("POST body: uri", req.uri == "/upload"); - check("POST body: Content-Length header", req.headers["Content-Length"] == "27"); + check("POST body: Content-Length header", req.headers["content-length"] == "27"); check("POST body: body", req.body == "username=toto&password=1234"); } @@ -53,7 +53,8 @@ static void test_invalid_no_separator() { RequestParser parser; std::string raw = "GET /index.html HTTP/1.1\r\nHost: localhost"; - HttpRequest req = parser.parse(raw); + HttpRequest req; + try { req = parser.parse(raw); } catch (const RequestParser::ParseException&) {} check("Invalide sans separator: method vide", req.method.empty()); } @@ -62,7 +63,8 @@ static void test_invalid_bad_version() { RequestParser parser; std::string raw = "GET /index.html NOTHTTP\r\n\r\n"; - HttpRequest req = parser.parse(raw); + HttpRequest req; + try { req = parser.parse(raw); } catch (const RequestParser::ParseException&) {} check("Invalide mauvaise version: method vide", req.method.empty()); } @@ -71,7 +73,8 @@ static void test_invalid_missing_uri() { RequestParser parser; std::string raw = "GET\r\n\r\n"; - HttpRequest req = parser.parse(raw); + HttpRequest req; + try { req = parser.parse(raw); } catch (const RequestParser::ParseException&) {} check("Invalide uri manquante: method vide", req.method.empty()); } @@ -87,8 +90,8 @@ static void test_multiple_headers() "\r\n"; HttpRequest req = parser.parse(raw); - check("Multi-headers: Accept", req.headers["Accept"] == "text/html"); - check("Multi-headers: Connection", req.headers["Connection"] == "keep-alive"); + check("Multi-headers: Accept", req.headers["accept"] == "text/html"); + check("Multi-headers: Connection", req.headers["connection"] == "keep-alive"); } static void test_query_string() @@ -111,7 +114,7 @@ static void test_header_value_with_colon() "\r\n"; HttpRequest req = parser.parse(raw); - check("Header avec colon: valeur correcte", req.headers["Date"] == "Mon, 16 Jun 2026 00:00:00 GMT"); + check("Header avec colon: valeur correcte", req.headers["date"] == "Mon, 16 Jun 2026 00:00:00 GMT"); } static void test_post_empty_body() @@ -150,7 +153,8 @@ static void test_root_uri() static void test_invalid_empty() { RequestParser parser; - HttpRequest req = parser.parse(""); + HttpRequest req; + try { req = parser.parse(""); } catch (const RequestParser::ParseException&) {} check("Invalide string vide: method vide", req.method.empty()); } @@ -159,7 +163,8 @@ static void test_invalid_extra_token_first_line() { RequestParser parser; std::string raw = "GET /index.html HTTP/1.1 EXTRA\r\n\r\n"; - HttpRequest req = parser.parse(raw); + HttpRequest req; + try { req = parser.parse(raw); } catch (const RequestParser::ParseException&) {} check("Invalide token en trop: method vide", req.method.empty()); } @@ -169,14 +174,15 @@ static void test_header_no_space_after_colon() std::string raw = "GET / HTTP/1.1\r\nHost:localhost\r\n\r\n"; HttpRequest req = parser.parse(raw); - check("Header sans espace: valeur correcte", req.headers["Host"] == "localhost"); + check("Header sans espace: valeur correcte", req.headers["host"] == "localhost"); } static void test_header_tab_after_colon() { RequestParser parser; std::string raw = "GET / HTTP/1.1\r\nHost:\tlocalhost\r\n\r\n"; - HttpRequest req = parser.parse(raw); + HttpRequest req; + try { req = parser.parse(raw); } catch (const RequestParser::ParseException&) {} check("Header avec tab: rejeté comme nginx", req.method.empty()); } @@ -185,7 +191,8 @@ static void test_http11_without_host() { RequestParser parser; std::string raw = "GET / HTTP/1.1\r\n\r\n"; - HttpRequest req = parser.parse(raw); + HttpRequest req; + try { req = parser.parse(raw); } catch (const RequestParser::ParseException&) {} check("HTTP/1.1 sans Host: rejeté comme nginx", req.method.empty()); }