From b7758bed8e0e3283bf2d47e20d152a53fc4a31ee Mon Sep 17 00:00:00 2001 From: byronlove111 Date: Thu, 18 Jun 2026 22:26:54 +0200 Subject: [PATCH 01/20] refactor(http): add ParseException, processHttp facade and harden isValid - Introduce RequestParser::ParseException with status code (400/505) - isValid() now throws instead of returning bool, distinguishing malformed requests (400) from unsupported versions (505) - Add processHttp() as single entry point for the HTTP layer: raw request + ServerConfig in, serialized response string out - Fix case-insensitive Host header check (host/HOST/HoSt all valid) - Add inline comments with bad-request examples on each validation check Co-authored-by: Cursor --- include/http/RequestParser.hpp | 19 +++++++++++--- include/http/processHttp.hpp | 9 +++++++ src/http/parser/RequestParser.cpp | 43 +++++++++++++++++++++---------- src/http/processHttp.cpp | 26 +++++++++++++++++++ 4 files changed, 79 insertions(+), 18 deletions(-) create mode 100644 include/http/processHttp.hpp create mode 100644 src/http/processHttp.cpp diff --git a/include/http/RequestParser.hpp b/include/http/RequestParser.hpp index 2bb219e..389831b 100644 --- a/include/http/RequestParser.hpp +++ b/include/http/RequestParser.hpp @@ -3,16 +3,27 @@ #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) {} + 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 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); }; #endif \ No newline at end of file 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/src/http/parser/RequestParser.cpp b/src/http/parser/RequestParser.cpp index 03e12a1..fd53fde 100644 --- a/src/http/parser/RequestParser.cpp +++ b/src/http/parser/RequestParser.cpp @@ -1,10 +1,11 @@ #include "../../../include/http/RequestParser.hpp" #include -bool RequestParser::isValid(const std::string& rawRequest) +void RequestParser::isValid(const std::string& rawRequest) { + // Erreur: "GET / HTTP/1.1\r\nHost: localhost\r\n" (headers jamais fermés) if (rawRequest.find("\r\n\r\n") == std::string::npos) - return false; + throw ParseException(400, "Bad Request"); std::size_t firstLineEnd = rawRequest.find("\r\n"); std::size_t headerBodySeparator = rawRequest.find("\r\n\r\n"); @@ -12,19 +13,29 @@ bool RequestParser::isValid(const std::string& rawRequest) std::istringstream firstLineStream(firstLine); std::string method, uri, version; + + // Erreur: "GET /\r\n\r\n" (version manquante, moins de 3 tokens) if (!(firstLineStream >> method >> uri >> version)) - return false; + throw ParseException(400, "Bad Request"); std::string extraToken; + // Erreur: "GET / HTTP/1.1 extra\r\n\r\n" (4ème token sur la request-line) if (firstLineStream >> extraToken) - return false; + throw ParseException(400, "Bad Request"); + // Erreur: "GET / MYPROTO/1.1\r\n\r\n" (protocole inconnu) if (version.substr(0, 5) != "HTTP/") - return false; + throw ParseException(400, "Bad Request"); + + // Erreur: "GET / HTTP/2.0\r\nHost: localhost\r\n\r\n" (version non supportée) + if (version != "HTTP/1.0" && version != "HTTP/1.1") + throw ParseException(505, "HTTP Version Not Supported"); bool hasHostHeader = false; std::size_t currentPosition = firstLineEnd + 2; + // vérifie chaque header : pas de tab après le colon, détecte la présence du header Host + // ex: "Host: localhost\r\nContent-Type: text/html\r\n" → deux passages dans la boucle while (currentPosition < headerBodySeparator) { std::size_t lineEndPosition = rawRequest.find("\r\n", currentPosition); @@ -33,20 +44,26 @@ bool RequestParser::isValid(const std::string& rawRequest) std::size_t colonPosition = currentLine.find(':'); if (colonPosition != std::string::npos) { + // Erreur: "Host:\tlocalhost" (tab juste après le colon) bool hasTabAfterColon = (colonPosition + 1 < currentLine.size() && currentLine[colonPosition + 1] == '\t'); if (hasTabAfterColon) - return false; - if (currentLine.substr(0, colonPosition) == "Host") + throw ParseException(400, "Bad Request"); + + // les noms de headers sont case-insensitive : host/HOST/HoSt sont tous valides + std::string headerName = currentLine.substr(0, colonPosition); + std::string headerNameLower = headerName; + for (std::size_t i = 0; i < headerNameLower.size(); i++) + headerNameLower[i] = std::tolower(headerNameLower[i]); + if (headerNameLower == "host") hasHostHeader = 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 (version == "HTTP/1.1" && !hasHostHeader) - return false; - - return true; + throw ParseException(400, "Bad Request"); } void RequestParser::parseFirstLine(const std::string& firstLine, HttpRequest& request) @@ -92,11 +109,9 @@ void RequestParser::parseBody(const std::string& rawRequest, HttpRequest& reques HttpRequest RequestParser::parse(const std::string& rawRequest) { - HttpRequest request; - - if (!isValid(rawRequest)) - return request; + isValid(rawRequest); + HttpRequest request; std::size_t firstLineEnd = rawRequest.find("\r\n"); std::size_t headerBodySeparator = rawRequest.find("\r\n\r\n"); diff --git a/src/http/processHttp.cpp b/src/http/processHttp.cpp new file mode 100644 index 0000000..b51daa2 --- /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/utils/HttpUtils.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())); + } +} From ff335ccc4df68bf427e1f8f252b2c0b336a348a7 Mon Sep 17 00:00:00 2001 From: byronlove111 Date: Fri, 19 Jun 2026 01:45:27 +0200 Subject: [PATCH 02/20] refactor(parser): single-pass parse, split helpers, harden validation - Merge isValid() into parse() for a single scan of the raw request - Split parseHeaders() into parseHeaderLine() and validateHostHeader() - Add lowercase method check (NGINX rejects non-uppercase methods) - Add Host value empty/whitespace check - Add duplicate Host header detection - Add duplicate Content-Length detection (request smuggling) - Add comments with bad-request examples on each validation check Co-authored-by: Cursor --- include/http/RequestParser.hpp | 6 +- src/http/parser/RequestParser.cpp | 145 ++++++++++++++++-------------- src/http/processHttp.cpp | 2 +- 3 files changed, 81 insertions(+), 72 deletions(-) diff --git a/include/http/RequestParser.hpp b/include/http/RequestParser.hpp index 389831b..806553a 100644 --- a/include/http/RequestParser.hpp +++ b/include/http/RequestParser.hpp @@ -10,6 +10,7 @@ class RequestParser { 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: @@ -20,8 +21,9 @@ class RequestParser { HttpRequest parse(const std::string& raw); private: - void isValid(const std::string& raw); - void parseFirstLine(const std::string& line, HttpRequest& req); + 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); }; diff --git a/src/http/parser/RequestParser.cpp b/src/http/parser/RequestParser.cpp index fd53fde..ddac6ce 100644 --- a/src/http/parser/RequestParser.cpp +++ b/src/http/parser/RequestParser.cpp @@ -1,104 +1,107 @@ #include "../../../include/http/RequestParser.hpp" #include -void RequestParser::isValid(const std::string& rawRequest) +void RequestParser::parseRequestLine(const std::string& firstLine, HttpRequest& request) { - // Erreur: "GET / HTTP/1.1\r\nHost: localhost\r\n" (headers jamais fermés) - if (rawRequest.find("\r\n\r\n") == 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"); - std::string firstLine = rawRequest.substr(0, firstLineEnd); - - std::istringstream firstLineStream(firstLine); - std::string method, uri, version; + std::istringstream lineStream(firstLine); + std::string extraToken; // Erreur: "GET /\r\n\r\n" (version manquante, moins de 3 tokens) - if (!(firstLineStream >> method >> uri >> version)) + if (!(lineStream >> request.method >> request.uri >> request.version)) throw ParseException(400, "Bad Request"); - std::string extraToken; // Erreur: "GET / HTTP/1.1 extra\r\n\r\n" (4ème token sur la request-line) - if (firstLineStream >> extraToken) + if (lineStream >> extraToken) throw ParseException(400, "Bad Request"); + // 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"); + // Erreur: "GET / MYPROTO/1.1\r\n\r\n" (protocole inconnu) - if (version.substr(0, 5) != "HTTP/") + if (request.version.substr(0, 5) != "HTTP/") throw ParseException(400, "Bad Request"); // Erreur: "GET / HTTP/2.0\r\nHost: localhost\r\n\r\n" (version non supportée) - if (version != "HTTP/1.0" && version != "HTTP/1.1") + if (request.version != "HTTP/1.0" && request.version != "HTTP/1.1") throw ParseException(505, "HTTP Version Not Supported"); +} - bool hasHostHeader = false; - std::size_t currentPosition = firstLineEnd + 2; +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; - // vérifie chaque header : pas de tab après le colon, détecte la présence du header Host - // ex: "Host: localhost\r\nContent-Type: text/html\r\n" → deux passages dans la boucle - while (currentPosition < headerBodySeparator) - { - std::size_t lineEndPosition = rawRequest.find("\r\n", currentPosition); - std::string currentLine = rawRequest.substr(currentPosition, lineEndPosition - currentPosition); + // Erreur: "Host:\tlocalhost" (tab juste après le colon) + if (colonPosition + 1 < line.size() && line[colonPosition + 1] == '\t') + throw ParseException(400, "Bad Request"); - std::size_t colonPosition = currentLine.find(':'); - if (colonPosition != std::string::npos) - { - // Erreur: "Host:\tlocalhost" (tab juste après le colon) - bool hasTabAfterColon = (colonPosition + 1 < currentLine.size() - && currentLine[colonPosition + 1] == '\t'); - if (hasTabAfterColon) - throw ParseException(400, "Bad Request"); - - // les noms de headers sont case-insensitive : host/HOST/HoSt sont tous valides - std::string headerName = currentLine.substr(0, colonPosition); - std::string headerNameLower = headerName; - for (std::size_t i = 0; i < headerNameLower.size(); i++) - headerNameLower[i] = std::tolower(headerNameLower[i]); - if (headerNameLower == "host") - hasHostHeader = true; - } - currentPosition = lineEndPosition + 2; - } + headerKey = line.substr(0, colonPosition); - // Erreur: "GET / HTTP/1.1\r\nContent-Type: text/html\r\n\r\n" (pas de Host, obligatoire en HTTP/1.1) - if (version == "HTTP/1.1" && !hasHostHeader) - throw ParseException(400, "Bad Request"); + std::size_t valueStart = colonPosition + 1; + while (valueStart < line.size() && line[valueStart] == ' ') + valueStart++; + + headerValue = (valueStart < line.size()) ? line.substr(valueStart) : ""; } -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 + std::string headerKeyLower = headerKey; + for (std::size_t i = 0; i < headerKeyLower.size(); i++) + headerKeyLower[i] = std::tolower(headerKeyLower[i]); + 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, @@ -109,14 +112,18 @@ void RequestParser::parseBody(const std::string& rawRequest, HttpRequest& reques HttpRequest RequestParser::parse(const std::string& rawRequest) { - isValid(rawRequest); + std::size_t headerBodySeparator = rawRequest.find("\r\n\r\n"); + + // Erreur: "GET / HTTP/1.1\r\nHost: localhost\r\n" (headers jamais fermés) + if (headerBodySeparator == std::string::npos) + throw ParseException(400, "Bad Request"); HttpRequest request; - std::size_t firstLineEnd = rawRequest.find("\r\n"); - std::size_t headerBodySeparator = rawRequest.find("\r\n\r\n"); + 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 index b51daa2..b00960d 100644 --- a/src/http/processHttp.cpp +++ b/src/http/processHttp.cpp @@ -18,7 +18,7 @@ std::string processHttp(const std::string& rawRequest, const ServerConfig& serve 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())); From e743cee1a3414594d6ab51e3e93835c81f205951 Mon Sep 17 00:00:00 2001 From: byronlove111 Date: Fri, 19 Jun 2026 01:53:54 +0200 Subject: [PATCH 03/20] style(parser): replace ternary with if/else in parseHeaderLine Co-authored-by: Cursor --- src/http/parser/RequestParser.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/http/parser/RequestParser.cpp b/src/http/parser/RequestParser.cpp index ddac6ce..f7f3672 100644 --- a/src/http/parser/RequestParser.cpp +++ b/src/http/parser/RequestParser.cpp @@ -45,7 +45,10 @@ void RequestParser::parseHeaderLine(const std::string& line, while (valueStart < line.size() && line[valueStart] == ' ') valueStart++; - headerValue = (valueStart < line.size()) ? line.substr(valueStart) : ""; + if (valueStart < line.size()) + headerValue = line.substr(valueStart); + else + headerValue = ""; } void RequestParser::validateHostHeader(const std::string& headerValue, bool& hasHostHeader) From 2c4b260ec87f4453083a6765f1b5330c2e0b3cf9 Mon Sep 17 00:00:00 2001 From: byronlove111 Date: Fri, 19 Jun 2026 16:22:04 +0200 Subject: [PATCH 04/20] fix(parser): respect Content-Length in body parsing, validate it's a positive integer Co-authored-by: Cursor --- src/http/parser/RequestParser.cpp | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/http/parser/RequestParser.cpp b/src/http/parser/RequestParser.cpp index f7f3672..cf7fab5 100644 --- a/src/http/parser/RequestParser.cpp +++ b/src/http/parser/RequestParser.cpp @@ -110,7 +110,25 @@ void RequestParser::parseHeaders(const std::string& rawRequest, HttpRequest& req 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) From ad43ed4d80c17c928b1efd0c9b6f3fbbdcfba014 Mon Sep 17 00:00:00 2001 From: byronlove111 Date: Fri, 19 Jun 2026 16:40:44 +0200 Subject: [PATCH 05/20] docs(router): add inline comments explaining longest prefix match logic Co-authored-by: Cursor --- src/http/router/Router.cpp | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) 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; From c00766d301ee6e4e998864761e3d9011d4431c00 Mon Sep 17 00:00:00 2001 From: byronlove111 Date: Fri, 19 Jun 2026 16:43:18 +0200 Subject: [PATCH 06/20] chore(build): update Makefile to output object files in 'obj/' directory and adjust clean target - Changed object file output path from current directory to 'obj/' for better organization. - Updated clean target to remove the 'obj' directory instead of individual object files. - Added 'obj/' to .gitignore to prevent tracking of generated object files. --- .gitignore | 1 + Makefile | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) 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) From b93ea931cde2a519067b75307b3a8e31a291db2c Mon Sep 17 00:00:00 2001 From: byronlove111 Date: Fri, 19 Jun 2026 16:50:08 +0200 Subject: [PATCH 07/20] feat(response): add Server: webserv/1.0 header to all responses Co-authored-by: Cursor --- src/http/response/ResponseBuilder.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/http/response/ResponseBuilder.cpp b/src/http/response/ResponseBuilder.cpp index 85911f3..8c8b532 100644 --- a/src/http/response/ResponseBuilder.cpp +++ b/src/http/response/ResponseBuilder.cpp @@ -8,6 +8,8 @@ std::string ResponseBuilder::build(const HttpResponse& response) rawOutput << "HTTP/1.1 " << response.status_code << " " << response.status_msg << "\r\n"; + rawOutput << "Server: webserv/1.0\r\n"; + time_t now = time(NULL); struct tm* gmt = gmtime(&now); char dateBuf[64]; From 1784db736abec846a871c797080ad8b8fc993305 Mon Sep 17 00:00:00 2001 From: byronlove111 Date: Fri, 19 Jun 2026 18:44:33 +0200 Subject: [PATCH 08/20] docs(response): add inline comments to ResponseBuilder::build Co-authored-by: Cursor --- src/http/response/ResponseBuilder.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/http/response/ResponseBuilder.cpp b/src/http/response/ResponseBuilder.cpp index 8c8b532..128e074 100644 --- a/src/http/response/ResponseBuilder.cpp +++ b/src/http/response/ResponseBuilder.cpp @@ -6,21 +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; From 84b7a5ee02e0f5b18b5073adf5e8a4643e25b256 Mon Sep 17 00:00:00 2001 From: byronlove111 Date: Fri, 19 Jun 2026 18:50:06 +0200 Subject: [PATCH 09/20] refactor(handler): extract buildRedirect helper, remove inline 301 construction Co-authored-by: Cursor --- include/http/utils/HttpUtils.hpp | 1 + src/http/handlers/MethodHandler.cpp | 8 +------- src/http/utils/HttpUtils.cpp | 10 ++++++++++ 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/include/http/utils/HttpUtils.hpp b/include/http/utils/HttpUtils.hpp index dbadd9a..5c508ed 100644 --- a/include/http/utils/HttpUtils.hpp +++ b/include/http/utils/HttpUtils.hpp @@ -5,6 +5,7 @@ #include HttpResponse buildHttpError(int statusCode, const std::string& statusMessage); +HttpResponse buildRedirect(const std::string& url); std::string getContentType(const std::string& filePath); #endif diff --git a/src/http/handlers/MethodHandler.cpp b/src/http/handlers/MethodHandler.cpp index c079cf7..95f988e 100644 --- a/src/http/handlers/MethodHandler.cpp +++ b/src/http/handlers/MethodHandler.cpp @@ -84,13 +84,7 @@ HttpResponse MethodHandler::handle(const HttpRequest& request, const LocationCon else if (server.getMaxBodySize() > 0 && request.body.size() > server.getMaxBodySize()) 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; - } + return buildRedirect(location.getRedirectUrl()); else if (isCgiRequest(request, location)) { CgiHandler cgiHandler; diff --git a/src/http/utils/HttpUtils.cpp b/src/http/utils/HttpUtils.cpp index 44895ec..5bb0f21 100644 --- a/src/http/utils/HttpUtils.cpp +++ b/src/http/utils/HttpUtils.cpp @@ -15,6 +15,16 @@ HttpResponse buildHttpError(int statusCode, const std::string& statusMessage) return response; } +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; +} + std::string getContentType(const std::string& filePath) { std::string fileExtension; From 3eaf71062e0583dbebe59c350ee57e6bb84061ba Mon Sep 17 00:00:00 2001 From: byronlove111 Date: Fri, 19 Jun 2026 19:07:56 +0200 Subject: [PATCH 10/20] fix(handler): init bytesRead=0, remove redundant location root guard in applyCustomErrorPage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ssize_t bytesRead uninitialized when file is empty → undefined behavior on if (bytesRead < 0) - location.getRoot().empty() guard was skipping error page lookup for locations without root (CGI, redirect) even when open() would gracefully handle the missing path itself Co-authored-by: Cursor --- src/http/handlers/MethodHandler.cpp | 6 +- tests/http/unit/test_custom_error_page.cpp | 105 +++++++++++++++++++++ 2 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 tests/http/unit/test_custom_error_page.cpp diff --git a/src/http/handlers/MethodHandler.cpp b/src/http/handlers/MethodHandler.cpp index 95f988e..d2d0973 100644 --- a/src/http/handlers/MethodHandler.cpp +++ b/src/http/handlers/MethodHandler.cpp @@ -37,7 +37,7 @@ 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(); @@ -53,7 +53,7 @@ static HttpResponse applyCustomErrorPage(const HttpResponse& response, HttpResponse customResponse = response; customResponse.body = ""; char buf[4096]; - ssize_t bytesRead; + ssize_t bytesRead = 0; while ((bytesRead = read(fd, buf, sizeof(buf))) > 0) customResponse.body.append(buf, bytesRead); @@ -79,7 +79,7 @@ HttpResponse MethodHandler::handle(const HttpRequest& request, const LocationCon response = buildHttpError(400, "Bad Request"); else if (location.getPath().empty()) response = buildHttpError(404, "Not Found"); - else if (!isMethodAllowed(request.method, location)) + else if (!MethodHandler::isMethodAllowed(request.method, location)) response = buildHttpError(405, "Method Not Allowed"); else if (server.getMaxBodySize() > 0 && request.body.size() > server.getMaxBodySize()) response = buildHttpError(413, "Payload Too Large"); 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..05f0ce9 --- /dev/null +++ b/tests/http/unit/test_custom_error_page.cpp @@ -0,0 +1,105 @@ +#include "../../../include/http/MethodHandler.hpp" +#include "../../../include/config/ServerConfig.hpp" +#include "../../../include/config/LocationConfig.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 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() +{ + 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; +} From 1a05908e1248254b4dadd54debf8f6dc7e49b729 Mon Sep 17 00:00:00 2001 From: byronlove111 Date: Fri, 19 Jun 2026 19:17:22 +0200 Subject: [PATCH 11/20] docs(handler): add inline comments to MethodHandler, isCgiRequest and applyCustomErrorPage Co-authored-by: Cursor --- src/http/handlers/MethodHandler.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/http/handlers/MethodHandler.cpp b/src/http/handlers/MethodHandler.cpp index d2d0973..77e050e 100644 --- a/src/http/handlers/MethodHandler.cpp +++ b/src/http/handlers/MethodHandler.cpp @@ -7,12 +7,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 +27,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,6 +38,9 @@ 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) @@ -45,6 +53,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) @@ -76,14 +85,19 @@ 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 (!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()) + // retour direct : la redirect ne passe pas par applyCustomErrorPage return buildRedirect(location.getRedirectUrl()); else if (isCgiRequest(request, location)) { @@ -99,5 +113,7 @@ 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); } +2 \ No newline at end of file From cb0e95d6ffad304765994338dbb00a01e8d23b2e Mon Sep 17 00:00:00 2001 From: byronlove111 Date: Fri, 19 Jun 2026 19:48:56 +0200 Subject: [PATCH 12/20] refactor(http): split HttpUtils into builders/ + utils/ Chaque builder HTTP a son propre fichier dans src/http/builders/. HttpUtils.cpp ne contient plus que les vrais utilitaires (readFdToString, getContentType). Umbrella header HttpBuilders.hpp pour importer tous les builders en un include. Co-authored-by: Cursor --- include/http/builders/HttpBuilders.hpp | 13 +++ include/http/utils/HttpUtils.hpp | 6 +- src/http/builders/buildHttpCreated.cpp | 16 ++++ src/http/builders/buildHttpError.cpp | 16 ++++ src/http/builders/buildHttpNoContent.cpp | 10 ++ src/http/builders/buildHttpOk.cpp | 16 ++++ src/http/builders/buildRedirect.cpp | 11 +++ src/http/cgi/execute.cpp | 2 +- src/http/cgi/output.cpp | 13 +-- src/http/handlers/DeleteHandler.cpp | 9 +- src/http/handlers/GetHandler.cpp | 115 ++++++++++++----------- src/http/handlers/MethodHandler.cpp | 13 +-- src/http/handlers/PostHandler.cpp | 11 +-- src/http/processHttp.cpp | 2 +- src/http/utils/HttpUtils.cpp | 29 ++---- 15 files changed, 165 insertions(+), 117 deletions(-) create mode 100644 include/http/builders/HttpBuilders.hpp create mode 100644 src/http/builders/buildHttpCreated.cpp create mode 100644 src/http/builders/buildHttpError.cpp create mode 100644 src/http/builders/buildHttpNoContent.cpp create mode 100644 src/http/builders/buildHttpOk.cpp create mode 100644 src/http/builders/buildRedirect.cpp 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/utils/HttpUtils.hpp b/include/http/utils/HttpUtils.hpp index 5c508ed..d3d9406 100644 --- a/include/http/utils/HttpUtils.hpp +++ b/include/http/utils/HttpUtils.hpp @@ -1,11 +1,9 @@ #ifndef HTTP_UTILS_HPP #define HTTP_UTILS_HPP -#include "../HttpResponse.hpp" #include -HttpResponse buildHttpError(int statusCode, const std::string& statusMessage); -HttpResponse buildRedirect(const std::string& url); -std::string getContentType(const std::string& filePath); +bool readFdToString(int fd, std::string& body); +std::string getContentType(const std::string& filePath); #endif 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/execute.cpp b/src/http/cgi/execute.cpp index 23f5567..bf6c0d4 100644 --- a/src/http/cgi/execute.cpp +++ b/src/http/cgi/execute.cpp @@ -1,5 +1,5 @@ #include "../../../include/http/CgiHandler.hpp" -#include "../../../include/http/utils/HttpUtils.hpp" +#include "../../../include/http/builders/HttpBuilders.hpp" #include #include #include 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..a3f8af4 100644 --- a/src/http/handlers/DeleteHandler.cpp +++ b/src/http/handlers/DeleteHandler.cpp @@ -1,5 +1,5 @@ #include "../../../include/http/MethodHandler.hpp" -#include "../../../include/http/utils/HttpUtils.hpp" +#include "../../../include/http/builders/HttpBuilders.hpp" #include #include #include @@ -25,11 +25,6 @@ HttpResponse MethodHandler::handleDelete(const HttpRequest& request, const Locat 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"); } diff --git a/src/http/handlers/GetHandler.cpp b/src/http/handlers/GetHandler.cpp index 90f4335..ead9df4 100644 --- a/src/http/handlers/GetHandler.cpp +++ b/src/http/handlers/GetHandler.cpp @@ -1,4 +1,5 @@ #include "../../../include/http/MethodHandler.hpp" +#include "../../../include/http/builders/HttpBuilders.hpp" #include "../../../include/http/utils/HttpUtils.hpp" #include #include @@ -7,6 +8,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 +32,54 @@ 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) +// strip le query string de l'URI +// ex: "/index.html?foo=bar" → "/index.html" +static std::string extractUriPath(const std::string& uri) { - std::string uriPath = request.uri; - std::size_t queryPos = uriPath.find('?'); + std::size_t queryPos = uri.find('?'); if (queryPos != std::string::npos) - uriPath = uriPath.substr(0, queryPos); + return uri.substr(0, queryPos); + return uri; +} - std::string filePath = location.getRoot() + uriPath; +// 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) +{ + if (location.getIndex().empty()) + return ""; - 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"); - } + std::string indexPath = dirPath; + if (indexPath[indexPath.size() - 1] != '/') + indexPath += '/'; + indexPath += location.getIndex(); + + 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 +88,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"); + + 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; - while ((bytesRead = read(fileDescriptor, readBuffer, sizeof(readBuffer))) > 0) - response.body.append(readBuffer, bytesRead); - if (bytesRead < 0) + 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 77e050e..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 @@ -61,17 +62,8 @@ static HttpResponse applyCustomErrorPage(const HttpResponse& response, HttpResponse customResponse = response; customResponse.body = ""; - char buf[4096]; - ssize_t bytesRead = 0; - - 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; @@ -116,4 +108,3 @@ HttpResponse MethodHandler::handle(const HttpRequest& request, const LocationCon // remplace le body d'erreur par la page HTML custom si configurée return applyCustomErrorPage(response, server, location); } -2 \ No newline at end of file diff --git a/src/http/handlers/PostHandler.cpp b/src/http/handlers/PostHandler.cpp index 4811c21..dab1aeb 100644 --- a/src/http/handlers/PostHandler.cpp +++ b/src/http/handlers/PostHandler.cpp @@ -1,5 +1,5 @@ #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 @@ -50,12 +50,7 @@ HttpResponse MethodHandler::handlePost(const HttpRequest& request, const Locatio } 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(); + HttpResponse response = buildHttpCreated(""); + response.headers["Location"] = "/" + filename; return response; } diff --git a/src/http/processHttp.cpp b/src/http/processHttp.cpp index b00960d..9b47e1b 100644 --- a/src/http/processHttp.cpp +++ b/src/http/processHttp.cpp @@ -3,7 +3,7 @@ #include "../../include/http/Router.hpp" #include "../../include/http/MethodHandler.hpp" #include "../../include/http/ResponseBuilder.hpp" -#include "../../include/http/utils/HttpUtils.hpp" +#include "../../include/http/builders/HttpBuilders.hpp" std::string processHttp(const std::string& rawRequest, const ServerConfig& server) { diff --git a/src/http/utils/HttpUtils.cpp b/src/http/utils/HttpUtils.cpp index 5bb0f21..9884f7b 100644 --- a/src/http/utils/HttpUtils.cpp +++ b/src/http/utils/HttpUtils.cpp @@ -1,28 +1,15 @@ #include "../../../include/http/utils/HttpUtils.hpp" -#include +#include -HttpResponse buildHttpError(int statusCode, const std::string& statusMessage) +bool readFdToString(int fd, std::string& body) { - HttpResponse response; - std::ostringstream contentLength; + char buf[4096]; + ssize_t bytesRead = 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; -} - -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; + while ((bytesRead = read(fd, buf, sizeof(buf))) > 0) + body.append(buf, bytesRead); + close(fd); + return bytesRead == 0; } std::string getContentType(const std::string& filePath) From 85bf91a7ff8065f6f9faa93803591d7ceb16b8e6 Mon Sep 17 00:00:00 2001 From: byronlove111 Date: Fri, 19 Jun 2026 19:57:47 +0200 Subject: [PATCH 13/20] refactor(handler): extract URI path extraction and file deletion logic - Introduced `extractUriPath` to handle query string removal from URIs in both DeleteHandler and PostHandler. - Added `deleteFile` function to encapsulate file existence check and deletion logic in DeleteHandler. - Updated `handleDelete` and `handlePost` methods to utilize the new utility functions for cleaner code structure. Co-authored-by: Cursor --- src/http/handlers/DeleteHandler.cpp | 25 ++++++++--- src/http/handlers/PostHandler.cpp | 69 ++++++++++++++++------------- 2 files changed, 58 insertions(+), 36 deletions(-) diff --git a/src/http/handlers/DeleteHandler.cpp b/src/http/handlers/DeleteHandler.cpp index a3f8af4..5d64896 100644 --- a/src/http/handlers/DeleteHandler.cpp +++ b/src/http/handlers/DeleteHandler.cpp @@ -4,15 +4,20 @@ #include #include -HttpResponse MethodHandler::handleDelete(const HttpRequest& request, const LocationConfig& location) +// strip le query string de l'URI +// ex: "/files/photo.jpg?v=2" → "/files/photo.jpg" +static std::string extractUriPath(const std::string& uri) { - std::string uriPath = request.uri; - std::size_t queryPos = uriPath.find('?'); + std::size_t queryPos = uri.find('?'); if (queryPos != std::string::npos) - uriPath = uriPath.substr(0, queryPos); - - std::string filePath = location.getRoot() + uriPath; + return uri.substr(0, queryPos); + return uri; +} +// 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,6 +26,7 @@ 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"); @@ -28,3 +34,10 @@ HttpResponse MethodHandler::handleDelete(const HttpRequest& request, const Locat 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/PostHandler.cpp b/src/http/handlers/PostHandler.cpp index dab1aeb..d82555f 100644 --- a/src/http/handlers/PostHandler.cpp +++ b/src/http/handlers/PostHandler.cpp @@ -3,54 +3,63 @@ #include "../../../include/http/utils/StringUtils.hpp" #include #include -#include #include -HttpResponse MethodHandler::handlePost(const HttpRequest& request, const LocationConfig& location) +// strip le query string de l'URI +// ex: "/upload/file.txt?foo=bar" → "/upload/file.txt" +static std::string extractUriPath(const std::string& uri) { - 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('?'); + std::size_t queryPos = uri.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; + return uri.substr(0, queryPos); + return uri; +} - 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; + const char* data = body.data(); + std::size_t totalToWrite = body.size(); + std::size_t totalWritten = 0; - while (totalBytesWritten < totalBytesToWrite) + while (totalWritten < totalToWrite) { - ssize_t bytesWritten = write(fileDescriptor, bodyData + totalBytesWritten, - totalBytesToWrite - totalBytesWritten); - if (bytesWritten <= 0) + ssize_t written = write(fd, data + totalWritten, totalToWrite - totalWritten); + if (written <= 0) { - close(fileDescriptor); + close(fd); return buildHttpError(507, "Insufficient Storage"); } - totalBytesWritten += static_cast(bytesWritten); + totalWritten += static_cast(written); } - close(fileDescriptor); + close(fd); - HttpResponse response = buildHttpCreated(""); + 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); +} From 6cea68057c0b258fe5dcf8db35514bcf5af58370 Mon Sep 17 00:00:00 2001 From: byronlove111 Date: Fri, 19 Jun 2026 20:07:08 +0200 Subject: [PATCH 14/20] feat(utils): add extractUriPath function for URI processing - Implemented `extractUriPath` in StringUtils to streamline URI query string removal. - Updated DeleteHandler, GetHandler, and PostHandler to utilize the new utility function, enhancing code clarity and reducing redundancy. Co-authored-by: Cursor --- include/http/utils/StringUtils.hpp | 1 + src/http/handlers/DeleteHandler.cpp | 11 ++--------- src/http/handlers/GetHandler.cpp | 10 +--------- src/http/handlers/PostHandler.cpp | 10 +--------- src/http/utils/StringUtils.cpp | 8 ++++++++ 5 files changed, 13 insertions(+), 27 deletions(-) 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/handlers/DeleteHandler.cpp b/src/http/handlers/DeleteHandler.cpp index 5d64896..9527899 100644 --- a/src/http/handlers/DeleteHandler.cpp +++ b/src/http/handlers/DeleteHandler.cpp @@ -1,18 +1,11 @@ #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 -// strip le query string de l'URI -// ex: "/files/photo.jpg?v=2" → "/files/photo.jpg" -static 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; -} // vérifie l'existence du fichier et le supprime // retourne 204 No Content ou une erreur diff --git a/src/http/handlers/GetHandler.cpp b/src/http/handlers/GetHandler.cpp index ead9df4..5987d12 100644 --- a/src/http/handlers/GetHandler.cpp +++ b/src/http/handlers/GetHandler.cpp @@ -1,6 +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 @@ -35,15 +36,6 @@ static HttpResponse buildAutoindex(const std::string& directoryPath, const std:: return buildHttpOk(listingHtml, "text/html"); } -// strip le query string de l'URI -// ex: "/index.html?foo=bar" → "/index.html" -static 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; -} // 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 diff --git a/src/http/handlers/PostHandler.cpp b/src/http/handlers/PostHandler.cpp index d82555f..b6b452e 100644 --- a/src/http/handlers/PostHandler.cpp +++ b/src/http/handlers/PostHandler.cpp @@ -1,19 +1,11 @@ #include "../../../include/http/MethodHandler.hpp" #include "../../../include/http/builders/HttpBuilders.hpp" #include "../../../include/http/utils/StringUtils.hpp" +#include "../../../include/http/utils/HttpUtils.hpp" #include #include #include -// strip le query string de l'URI -// ex: "/upload/file.txt?foo=bar" → "/upload/file.txt" -static 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; -} // ouvre le fichier de destination et écrit le body dedans // retourne 201 Created avec Location, ou une erreur 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('?'); From 7558ebc78283362cd6470f817fa2c253db8daa57 Mon Sep 17 00:00:00 2001 From: byronlove111 Date: Fri, 19 Jun 2026 22:21:55 +0200 Subject: [PATCH 15/20] feat(http): implement writeFdFromString utility for writing data to file descriptors - Added `writeFdFromString` function to `HttpUtils` for writing string data to file descriptors in a loop until completion. - Updated `PostHandler` to utilize the new utility function for writing request bodies, improving code clarity and error handling. - Enhanced `execute` method in `CgiHandler` to streamline the process of sending request body data to CGI scripts. Co-authored-by: Cursor --- include/http/utils/HttpUtils.hpp | 1 + src/http/cgi/execute.cpp | 99 +++++++++++++++++++------------ src/http/handlers/PostHandler.cpp | 17 ++---- src/http/utils/HttpUtils.cpp | 18 ++++++ 4 files changed, 85 insertions(+), 50 deletions(-) diff --git a/include/http/utils/HttpUtils.hpp b/include/http/utils/HttpUtils.hpp index d3d9406..2bdd32c 100644 --- a/include/http/utils/HttpUtils.hpp +++ b/include/http/utils/HttpUtils.hpp @@ -3,6 +3,7 @@ #include +bool writeFdFromString(int fd, const std::string& data); bool readFdToString(int fd, std::string& body); std::string getContentType(const std::string& filePath); diff --git a/src/http/cgi/execute.cpp b/src/http/cgi/execute.cpp index bf6c0d4..3cfe73a 100644 --- a/src/http/cgi/execute.cpp +++ b/src/http/cgi/execute.cpp @@ -1,36 +1,66 @@ #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 #include #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/handlers/PostHandler.cpp b/src/http/handlers/PostHandler.cpp index b6b452e..e09fa29 100644 --- a/src/http/handlers/PostHandler.cpp +++ b/src/http/handlers/PostHandler.cpp @@ -20,23 +20,14 @@ static HttpResponse writeBodyToFile(const std::string& destPath, const std::stri return buildHttpError(500, "Internal Server Error"); } - const char* data = body.data(); - std::size_t totalToWrite = body.size(); - std::size_t totalWritten = 0; - - while (totalWritten < totalToWrite) + if (!writeFdFromString(fd, body)) { - ssize_t written = write(fd, data + totalWritten, totalToWrite - totalWritten); - if (written <= 0) - { - close(fd); - return buildHttpError(507, "Insufficient Storage"); - } - totalWritten += static_cast(written); + close(fd); + return buildHttpError(507, "Insufficient Storage"); } close(fd); - HttpResponse response = buildHttpCreated(""); + HttpResponse response = buildHttpCreated(""); response.headers["Location"] = "/" + filename; return response; } diff --git a/src/http/utils/HttpUtils.cpp b/src/http/utils/HttpUtils.cpp index 9884f7b..4419cca 100644 --- a/src/http/utils/HttpUtils.cpp +++ b/src/http/utils/HttpUtils.cpp @@ -1,6 +1,24 @@ #include "../../../include/http/utils/HttpUtils.hpp" #include +// é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) +{ + const char* ptr = data.data(); + std::size_t totalToWrite = data.size(); + std::size_t totalWritten = 0; + + 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]; From c47132607f2ad62df64deaabe84221b82657c629 Mon Sep 17 00:00:00 2001 From: byronlove111 Date: Sat, 20 Jun 2026 01:54:38 +0200 Subject: [PATCH 16/20] refactor(cgi): use extractUriPath in buildEnv for PATH_INFO Co-authored-by: Cursor --- src/http/cgi/env.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/http/cgi/env.cpp b/src/http/cgi/env.cpp index 43b5475..0d0cf94 100644 --- a/src/http/cgi/env.cpp +++ b/src/http/cgi/env.cpp @@ -11,11 +11,7 @@ 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; From c910a8f9413477e9b1bd076d154a777fa9f676a8 Mon Sep 17 00:00:00 2001 From: byronlove111 Date: Sat, 20 Jun 2026 15:49:04 +0200 Subject: [PATCH 17/20] fix(http): correct indentation in processHttp function - Adjusted the closing brace indentation in the processHttp function for improved code readability and consistency. --- src/http/processHttp.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/http/processHttp.cpp b/src/http/processHttp.cpp index 9b47e1b..7cd98fe 100644 --- a/src/http/processHttp.cpp +++ b/src/http/processHttp.cpp @@ -18,7 +18,7 @@ std::string processHttp(const std::string& rawRequest, const ServerConfig& serve 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())); From bc0a6a6f52db7ad67d3741c6f0ae5572c3827f55 Mon Sep 17 00:00:00 2001 From: byronlove111 Date: Sat, 20 Jun 2026 16:08:36 +0200 Subject: [PATCH 18/20] fix(parser): normalize header keys to lowercase on storage Headers are case-insensitive per HTTP/1.1. Store all keys in lowercase so that lookups in parseBody (content-length) and env.cpp (content-type, host) work regardless of client casing. Also fix tolower() call to cast through unsigned char to avoid UB on high-ASCII characters. Co-authored-by: Cursor --- src/http/cgi/env.cpp | 4 ++-- src/http/parser/RequestParser.cpp | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/http/cgi/env.cpp b/src/http/cgi/env.cpp index 0d0cf94..5e2a4b5 100644 --- a/src/http/cgi/env.cpp +++ b/src/http/cgi/env.cpp @@ -15,7 +15,7 @@ std::vector CgiHandler::buildEnv(const HttpRequest& request, 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 @@ -30,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/parser/RequestParser.cpp b/src/http/parser/RequestParser.cpp index cf7fab5..013ccaa 100644 --- a/src/http/parser/RequestParser.cpp +++ b/src/http/parser/RequestParser.cpp @@ -82,12 +82,14 @@ void RequestParser::parseHeaders(const std::string& rawRequest, HttpRequest& req if (!headerKey.empty()) { - 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(headerKeyLower[i]); + headerKeyLower[i] = std::tolower(static_cast(headerKeyLower[i])); + + request.headers[headerKeyLower] = headerValue; if (headerKeyLower == "host") RequestParser::validateHostHeader(headerValue, hasHostHeader); @@ -112,7 +114,7 @@ void RequestParser::parseBody(const std::string& rawRequest, HttpRequest& reques { std::string rawBody = rawRequest.substr(headerBodySeparator + 4); - std::map::const_iterator it = request.headers.find("Content-Length"); + std::map::const_iterator it = request.headers.find("content-length"); if (it != request.headers.end()) { const std::string& clValue = it->second; From a93cd8685f0bb97d77b8e3a3bbf54a47454b1bac Mon Sep 17 00:00:00 2001 From: byronlove111 Date: Sat, 20 Jun 2026 16:35:17 +0200 Subject: [PATCH 19/20] fix(tests): update unit tests for lowercase header key normalization - Replace PascalCase lookups (Host, Content-Length, Accept, etc.) with lowercase equivalents to match the new storage convention in parseHeaders - Wrap invalid-request tests in try-catch to handle ParseException instead of expecting an empty HttpRequest on parsing failure - Create /tmp/www/errors/404.html in test_custom_error_page main() so the custom error page test passes in CI without pre-existing fixtures Co-authored-by: Cursor --- tests/http/unit/test_custom_error_page.cpp | 10 +++++++ tests/http/unit/test_request_parser.cpp | 33 +++++++++++++--------- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/tests/http/unit/test_custom_error_page.cpp b/tests/http/unit/test_custom_error_page.cpp index 05f0ce9..40f3c39 100644 --- a/tests/http/unit/test_custom_error_page.cpp +++ b/tests/http/unit/test_custom_error_page.cpp @@ -2,6 +2,9 @@ #include "../../../include/config/ServerConfig.hpp" #include "../../../include/config/LocationConfig.hpp" #include +#include +#include +#include static int passed = 0; static int failed = 0; @@ -97,6 +100,13 @@ static void test_with_root_location_uses_error_page() 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(); 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()); } From 9499e1f394a7ccc1ba390292543823c12eb5eae2 Mon Sep 17 00:00:00 2001 From: byronlove111 Date: Sat, 20 Jun 2026 16:40:08 +0200 Subject: [PATCH 20/20] fix(tests): align integration tests with lowercase header convention MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - test_adversarial: add safeParse() helper to absorb ParseException in all invalid-request parse tests; update makeReq to use lowercase keys - test_cgi_advanced: makeGet/makePost now store headers with lowercase keys (host, content-type) to match parseHeaders normalization - test_post_handler: POST with empty body now expects 201 (file created) instead of 400 — empty body is valid HTTP, check was intentionally removed Co-authored-by: Cursor --- tests/http/integration/test_adversarial.cpp | 46 ++++++++++---------- tests/http/integration/test_cgi_advanced.cpp | 6 +-- tests/http/integration/test_post_handler.cpp | 4 +- 3 files changed, 27 insertions(+), 29 deletions(-) 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 ────────────────────────