diff --git a/include/http/CgiHandler.hpp b/include/http/CgiHandler.hpp new file mode 100644 index 0000000..e62f412 --- /dev/null +++ b/include/http/CgiHandler.hpp @@ -0,0 +1,20 @@ +#ifndef CGI_HANDLER_HPP +#define CGI_HANDLER_HPP + +#include "HttpRequest.hpp" +#include "HttpResponse.hpp" +#include "../../include/config/LocationConfig.hpp" +#include +#include + +class CgiHandler { +public: + HttpResponse execute(const HttpRequest& req, const LocationConfig& loc); + +private: + std::vector buildEnv(const HttpRequest& req, const std::string& scriptPath); + HttpResponse parseOutput(const std::string& raw); + HttpResponse buildError(int code, const std::string& msg); +}; + +#endif diff --git a/src/http/README.md b/src/http/README.md index 08fd270..ce1e30e 100644 --- a/src/http/README.md +++ b/src/http/README.md @@ -12,7 +12,7 @@ RequestParser parser; HttpRequest req = parser.parse(rawString); // rawString = bytes du socket if (req.method.empty()) - // requête invalide → envoyer 400 + // requête invalide → envoyer 400 (voir exemple plus bas) Router router; LocationConfig loc = router.route(req, serverConfig); @@ -62,12 +62,50 @@ send(fd, raw.c_str(), raw.size(), 0); | Code | Cas | |---|---| -| 200 | Fichier servi | +| 200 | Fichier servi / CGI OK | | 201 | Upload POST réussi | | 204 | DELETE réussi | | 301 | Redirection | | 400 | Requête invalide (path traversal, body vide...) | -| 403 | Accès interdit | +| 403 | Accès interdit (dossier, permissions) | | 404 | Fichier introuvable | | 405 | Méthode non autorisée sur cette route | -| 500 | Erreur serveur (ex: upload_store manquant) | +| 500 | Erreur serveur (ex: upload_store manquant, fork fail) | +| 507 | Disque plein (write incomplet sur POST upload) | + +--- + +## CGI + +Le CGI est déclenché automatiquement si la `LocationConfig` a `cgi_extension` et `cgi_path` définis, et que l'URI se termine par cette extension. + +Config exemple : +```nginx +location /cgi-bin { + methods GET POST; + cgi_extension .py; + cgi_path /usr/bin/python3; +} +``` + +Un script CGI doit écrire sur stdout : +``` +Content-Type: text/html\n +\n +... +``` + +Un script de démo est disponible dans `www/cgi-bin/hello.py`. + +--- + +## Structure des fichiers + +``` +src/http/ + parser/ — RequestParser + router/ — Router + handlers/ — MethodHandler, GetHandler, PostHandler, DeleteHandler + cgi/ — CgiHandler (execute, env, output) + response/ — ResponseBuilder +``` diff --git a/src/http/Router.cpp b/src/http/Router.cpp deleted file mode 100644 index 4cd3fdf..0000000 --- a/src/http/Router.cpp +++ /dev/null @@ -1,30 +0,0 @@ -#include "../../include/http/Router.hpp" - -LocationConfig Router::route(const HttpRequest& req, const ServerConfig& server) -{ - const std::vector& locs = server.getLocations(); - - LocationConfig best; - std::size_t bestLen = 0; - - for (std::vector::const_iterator it = locs.begin(); it != locs.end(); ++it) - { - const std::string& path = it->getPath(); - - if (path.size() > req.uri.size()) - continue; - - if (req.uri.substr(0, path.size()) != path) - continue; - - if (path.size() < req.uri.size() && req.uri[path.size()] != '/' && path != "/") - continue; - - if (path.size() > bestLen) - { - best = *it; - bestLen = path.size(); - } - } - return best; -} diff --git a/src/http/cgi/env.cpp b/src/http/cgi/env.cpp new file mode 100644 index 0000000..37fe454 --- /dev/null +++ b/src/http/cgi/env.cpp @@ -0,0 +1,46 @@ +#include "../../../include/http/CgiHandler.hpp" +#include + +static std::string getQueryString(const std::string& uri) +{ + std::size_t q = uri.find('?'); + if (q == std::string::npos) + return ""; + return uri.substr(q + 1); +} + +std::vector CgiHandler::buildEnv(const HttpRequest& req, const std::string& scriptPath) +{ + std::vector env; + + env.push_back("REQUEST_METHOD=" + req.method); + env.push_back("QUERY_STRING=" + getQueryString(req.uri)); + env.push_back("SCRIPT_FILENAME=" + scriptPath); + env.push_back("PATH_INFO=" + req.uri); + + std::map::const_iterator it; + + it = req.headers.find("Content-Type"); + if (it != req.headers.end()) + env.push_back("CONTENT_TYPE=" + it->second); + else + env.push_back("CONTENT_TYPE="); + + if (!req.body.empty()) + { + std::ostringstream ss; + ss << req.body.size(); + env.push_back("CONTENT_LENGTH=" + ss.str()); + } + else + env.push_back("CONTENT_LENGTH=0"); + + it = req.headers.find("Host"); + if (it != req.headers.end()) + env.push_back("HTTP_HOST=" + it->second); + + env.push_back("SERVER_PROTOCOL=HTTP/1.1"); + env.push_back("GATEWAY_INTERFACE=CGI/1.1"); + + return env; +} diff --git a/src/http/cgi/execute.cpp b/src/http/cgi/execute.cpp new file mode 100644 index 0000000..b95ec2f --- /dev/null +++ b/src/http/cgi/execute.cpp @@ -0,0 +1,101 @@ +#include "../../../include/http/CgiHandler.hpp" +#include +#include +#include + +static std::string scriptPathFrom(const HttpRequest& req, const LocationConfig& loc) +{ + std::string uri = req.uri; + std::size_t q = uri.find('?'); + if (q != std::string::npos) + uri = uri.substr(0, q); + + char cwd[4096]; + getcwd(cwd, sizeof(cwd)); + return std::string(cwd) + "/" + loc.getRoot() + uri; +} + +static void runChild(const std::string& interpreter, const std::string& scriptPath, + char** argv, char** envp, + int stdin_pipe[2], int stdout_pipe[2]) +{ + dup2(stdin_pipe[0], STDIN_FILENO); + dup2(stdout_pipe[1], STDOUT_FILENO); + + close(stdin_pipe[0]); close(stdin_pipe[1]); + close(stdout_pipe[0]); close(stdout_pipe[1]); + + std::string dir = scriptPath.substr(0, scriptPath.rfind('/')); + chdir(dir.c_str()); + + execve(interpreter.c_str(), argv, envp); + _exit(1); +} + +static std::string readOutput(int stdout_pipe[2]) +{ + std::string output; + char buf[4096]; + ssize_t n; + while ((n = read(stdout_pipe[0], buf, sizeof(buf))) > 0) + output.append(buf, n); + close(stdout_pipe[0]); + return output; +} + +HttpResponse CgiHandler::execute(const HttpRequest& req, const LocationConfig& loc) +{ + std::string scriptPath = scriptPathFrom(req, loc); + + if (access(scriptPath.c_str(), F_OK) == -1) + return buildError(404, "Not Found"); + if (access(scriptPath.c_str(), X_OK) == -1) + return buildError(403, "Forbidden"); + + std::vector envVars = buildEnv(req, scriptPath); + std::vector envp; + for (std::size_t i = 0; i < envVars.size(); i++) + envp.push_back(const_cast(envVars[i].c_str())); + envp.push_back(NULL); + + std::string interpreter = loc.getCgiPath(); + char* argv[3] = { + const_cast(interpreter.c_str()), + const_cast(scriptPath.c_str()), + NULL + }; + + int stdin_pipe[2]; + int stdout_pipe[2]; + if (pipe(stdin_pipe) == -1 || pipe(stdout_pipe) == -1) + return buildError(500, "Internal Server Error"); + + pid_t pid = fork(); + if (pid == -1) + { + close(stdin_pipe[0]); close(stdin_pipe[1]); + close(stdout_pipe[0]); close(stdout_pipe[1]); + return buildError(500, "Internal Server Error"); + } + + if (pid == 0) + runChild(interpreter, scriptPath, argv, envp.data(), stdin_pipe, stdout_pipe); + + // parent + close(stdin_pipe[0]); + close(stdout_pipe[1]); + + if (!req.body.empty()) + write(stdin_pipe[1], req.body.c_str(), req.body.size()); + close(stdin_pipe[1]); + + std::string output = readOutput(stdout_pipe); + + int status; + waitpid(pid, &status, 0); + + if (WIFEXITED(status) && WEXITSTATUS(status) != 0) + return buildError(500, "Internal Server Error"); + + return parseOutput(output); +} diff --git a/src/http/cgi/output.cpp b/src/http/cgi/output.cpp new file mode 100644 index 0000000..1c4d234 --- /dev/null +++ b/src/http/cgi/output.cpp @@ -0,0 +1,84 @@ +#include "../../../include/http/CgiHandler.hpp" +#include + +HttpResponse CgiHandler::parseOutput(const std::string& raw) +{ + HttpResponse res; + + std::size_t sep = raw.find("\r\n\r\n"); + if (sep == std::string::npos) + sep = raw.find("\n\n"); + if (sep == std::string::npos) + { + res.status_code = 200; + res.status_msg = "OK"; + res.body = raw; + res.headers["Content-Type"] = "text/html"; + std::ostringstream ss; + ss << res.body.size(); + res.headers["Content-Length"] = ss.str(); + return res; + } + + std::string headersPart = raw.substr(0, sep); + std::size_t bodyStart = sep + (raw[sep + 1] == '\n' ? 2 : 4); + res.body = raw.substr(bodyStart); + + res.status_code = 200; + res.status_msg = "OK"; + + std::istringstream stream(headersPart); + std::string line; + while (std::getline(stream, line)) + { + if (!line.empty() && line[line.size() - 1] == '\r') + line.erase(line.size() - 1); + if (line.empty()) + continue; + + std::size_t colon = line.find(':'); + if (colon == std::string::npos) + continue; + + std::string key = line.substr(0, colon); + std::string value = line.substr(colon + 1); + std::size_t vs = 0; + while (vs < value.size() && value[vs] == ' ') + vs++; + value = value.substr(vs); + + if (key == "Status") + { + std::istringstream ss(value); + ss >> res.status_code; + std::size_t sp = value.find(' '); + if (sp != std::string::npos) + res.status_msg = value.substr(sp + 1); + } + else + res.headers[key] = value; + } + + if (res.headers.find("Content-Type") == res.headers.end()) + res.headers["Content-Type"] = "text/html"; + + std::ostringstream ss; + ss << res.body.size(); + res.headers["Content-Length"] = ss.str(); + + return res; +} + +HttpResponse CgiHandler::buildError(int code, const std::string& msg) +{ + HttpResponse res; + std::ostringstream ss; + + res.status_code = code; + res.status_msg = msg; + res.body = "

" + msg + "

"; + res.headers["Content-Type"] = "text/html"; + ss << res.body.size(); + res.headers["Content-Length"] = ss.str(); + return res; +} diff --git a/src/http/DeleteHandler.cpp b/src/http/handlers/DeleteHandler.cpp similarity index 92% rename from src/http/DeleteHandler.cpp rename to src/http/handlers/DeleteHandler.cpp index ec66181..f11d7d2 100644 --- a/src/http/DeleteHandler.cpp +++ b/src/http/handlers/DeleteHandler.cpp @@ -1,4 +1,4 @@ -#include "../../include/http/MethodHandler.hpp" +#include "../../../include/http/MethodHandler.hpp" #include #include diff --git a/src/http/GetHandler.cpp b/src/http/handlers/GetHandler.cpp similarity index 94% rename from src/http/GetHandler.cpp rename to src/http/handlers/GetHandler.cpp index ea32b70..1540c46 100644 --- a/src/http/GetHandler.cpp +++ b/src/http/handlers/GetHandler.cpp @@ -1,4 +1,4 @@ -#include "../../include/http/MethodHandler.hpp" +#include "../../../include/http/MethodHandler.hpp" #include #include #include @@ -49,7 +49,7 @@ static HttpResponse buildAutoindex(const std::string& path, const std::string& u closedir(dir); html += ""; - HttpResponse res; + HttpResponse res; std::ostringstream ss; res.status_code = 200; res.status_msg = "OK"; @@ -83,9 +83,9 @@ HttpResponse MethodHandler::handleGet(const HttpRequest& req, const LocationConf if (fd == -1) return buildError(404, "Not Found"); - HttpResponse res; - char buf[4096]; - ssize_t n; + HttpResponse res; + char buf[4096]; + ssize_t n; while ((n = read(fd, buf, sizeof(buf))) > 0) res.body.append(buf, n); diff --git a/src/http/MethodHandler.cpp b/src/http/handlers/MethodHandler.cpp similarity index 66% rename from src/http/MethodHandler.cpp rename to src/http/handlers/MethodHandler.cpp index 15dab13..907ba52 100644 --- a/src/http/MethodHandler.cpp +++ b/src/http/handlers/MethodHandler.cpp @@ -1,7 +1,22 @@ -#include "../../include/http/MethodHandler.hpp" +#include "../../../include/http/MethodHandler.hpp" +#include "../../../include/http/CgiHandler.hpp" #include #include +static bool isCgiRequest(const HttpRequest& req, const LocationConfig& loc) +{ + if (loc.getCgiExtension().empty() || loc.getCgiPath().empty()) + return false; + std::string uri = req.uri; + std::size_t q = uri.find('?'); + if (q != std::string::npos) + uri = uri.substr(0, q); + const std::string& ext = loc.getCgiExtension(); + if (uri.size() < ext.size()) + return false; + return uri.substr(uri.size() - ext.size()) == ext; +} + HttpResponse MethodHandler::handle(const HttpRequest& req, const LocationConfig& loc, const ServerConfig& server) { (void)server; @@ -18,6 +33,12 @@ HttpResponse MethodHandler::handle(const HttpRequest& req, const LocationConfig& return res; } + if (isCgiRequest(req, loc)) + { + CgiHandler cgi; + return cgi.execute(req, loc); + } + if (req.method == "GET") return handleGet(req, loc); if (req.method == "POST") @@ -36,7 +57,7 @@ bool MethodHandler::isMethodAllowed(const std::string& method, const LocationCon HttpResponse MethodHandler::buildError(int code, const std::string& msg) { - HttpResponse res; + HttpResponse res; std::ostringstream ss; res.status_code = code; @@ -47,5 +68,3 @@ HttpResponse MethodHandler::buildError(int code, const std::string& msg) res.headers["Content-Length"] = ss.str(); return res; } - - diff --git a/src/http/PostHandler.cpp b/src/http/handlers/PostHandler.cpp similarity index 88% rename from src/http/PostHandler.cpp rename to src/http/handlers/PostHandler.cpp index 7a13b61..b4b785c 100644 --- a/src/http/PostHandler.cpp +++ b/src/http/handlers/PostHandler.cpp @@ -1,4 +1,4 @@ -#include "../../include/http/MethodHandler.hpp" +#include "../../../include/http/MethodHandler.hpp" #include #include #include @@ -37,9 +37,9 @@ HttpResponse MethodHandler::handlePost(const HttpRequest& req, const LocationCon if (fd == -1) return buildError(403, "Forbidden"); - const char* data = req.body.data(); - std::size_t total = req.body.size(); - std::size_t written = 0; + const char* data = req.body.data(); + std::size_t total = req.body.size(); + std::size_t written = 0; while (written < total) { @@ -53,7 +53,7 @@ HttpResponse MethodHandler::handlePost(const HttpRequest& req, const LocationCon } close(fd); - HttpResponse res; + HttpResponse res; res.status_code = 201; res.status_msg = "Created"; res.headers["Location"] = "/" + filename; diff --git a/src/http/RequestParser.cpp b/src/http/parser/RequestParser.cpp similarity index 98% rename from src/http/RequestParser.cpp rename to src/http/parser/RequestParser.cpp index 6154b88..70d2d41 100644 --- a/src/http/RequestParser.cpp +++ b/src/http/parser/RequestParser.cpp @@ -1,4 +1,4 @@ -#include "../../include/http/RequestParser.hpp" +#include "../../../include/http/RequestParser.hpp" #include HttpRequest RequestParser::parse(const std::string& raw) @@ -65,7 +65,6 @@ bool RequestParser::isValid(const std::string& raw) void RequestParser::parseFirstLine(const std::string& line, HttpRequest& req) { std::istringstream ss(line); - ss >> req.method; ss >> req.uri; ss >> req.version; diff --git a/src/http/ResponseBuilder.cpp b/src/http/response/ResponseBuilder.cpp similarity index 89% rename from src/http/ResponseBuilder.cpp rename to src/http/response/ResponseBuilder.cpp index fbd9477..87af793 100644 --- a/src/http/ResponseBuilder.cpp +++ b/src/http/response/ResponseBuilder.cpp @@ -1,4 +1,4 @@ -#include "../../include/http/ResponseBuilder.hpp" +#include "../../../include/http/ResponseBuilder.hpp" #include std::string ResponseBuilder::build(const HttpResponse& res) diff --git a/src/http/router/Router.cpp b/src/http/router/Router.cpp new file mode 100644 index 0000000..9560200 --- /dev/null +++ b/src/http/router/Router.cpp @@ -0,0 +1,32 @@ +#include "../../../include/http/Router.hpp" + +static bool matches(const std::string& locationPath, const std::string& uri) +{ + if (locationPath.size() > uri.size()) + return false; + if (uri.substr(0, locationPath.size()) != locationPath) + return false; + if (locationPath.size() < uri.size() && uri[locationPath.size()] != '/' && locationPath != "/") + return false; + return true; +} + +LocationConfig Router::route(const HttpRequest& req, const ServerConfig& server) +{ + const std::vector& locs = server.getLocations(); + + LocationConfig best; + std::size_t bestLen = 0; + + for (std::vector::const_iterator it = locs.begin(); it != locs.end(); ++it) + { + const std::string& path = it->getPath(); + + if (matches(path, req.uri) && path.size() > bestLen) + { + best = *it; + bestLen = path.size(); + } + } + return best; +} diff --git a/tests/http/test_cgi_handler.cpp b/tests/http/test_cgi_handler.cpp new file mode 100644 index 0000000..4d7e308 --- /dev/null +++ b/tests/http/test_cgi_handler.cpp @@ -0,0 +1,115 @@ +#include "../../include/http/MethodHandler.hpp" +#include +#include +#include + +static int passed = 0; +static int failed = 0; + +static void check(const std::string& label, bool condition) +{ + if (condition) { std::cout << "[OK] " << label << std::endl; passed++; } + else { std::cout << "[KO] " << label << std::endl; failed++; } +} + +static LocationConfig makeCgiLoc() +{ + LocationConfig loc; + loc.setPath("/cgi-bin"); + loc.setRoot("www"); + loc.setCgiExtension(".py"); + loc.setCgiPath("/usr/bin/python3"); + loc.addMethod("GET"); + loc.addMethod("POST"); + return loc; +} + +static HttpRequest makeReq(const std::string& method, const std::string& uri, const std::string& body = "") +{ + HttpRequest req; + req.method = method; + req.uri = uri; + req.version = "HTTP/1.1"; + req.headers["Host"] = "localhost:8080"; + if (!body.empty()) + { + req.body = body; + req.headers["Content-Type"] = "text/plain"; + } + return req; +} + +int main() +{ + ServerConfig server; + LocationConfig loc = makeCgiLoc(); + server.addLocation(loc); + MethodHandler handler; + + // ── CAS 1 : GET simple → 200 + body HTML ──────────────────────── + { + HttpResponse res = handler.handle(makeReq("GET", "/cgi-bin/hello.py"), loc, server); + check("CGI GET: status 200", res.status_code == 200); + check("CGI GET: body non vide", !res.body.empty()); + check("CGI GET: Content-Type present", res.headers.count("Content-Type") > 0); + } + + // ── CAS 2 : GET avec query string → name dans le body ─────────── + { + HttpResponse res = handler.handle(makeReq("GET", "/cgi-bin/hello.py?name=Byron"), loc, server); + check("CGI GET query: 200", res.status_code == 200); + check("CGI GET query: name dans body", res.body.find("Byron") != std::string::npos); + } + + // ── CAS 3 : POST avec body → body recu dans la reponse ────────── + { + HttpResponse res = handler.handle(makeReq("POST", "/cgi-bin/hello.py", "message=bonjour"), loc, server); + check("CGI POST: 200", res.status_code == 200); + check("CGI POST: body recu", res.body.find("message=bonjour") != std::string::npos); + check("CGI POST: methode correcte", res.body.find("POST") != std::string::npos); + } + + // ── CAS 4 : script inexistant → 404 ───────────────────────────── + { + HttpResponse res = handler.handle(makeReq("GET", "/cgi-bin/notfound.py"), loc, server); + check("CGI 404: status 404", res.status_code == 404); + } + + // ── CAS 5 : methode non autorisee → 405 ───────────────────────── + { + LocationConfig locGet = makeCgiLoc(); + locGet.addMethod("GET"); + HttpResponse res = handler.handle(makeReq("DELETE", "/cgi-bin/hello.py"), locGet, server); + check("CGI 405: methode non autorisee", res.status_code == 405); + } + + // ── CAS 6 : fichier non executable → 403 ──────────────────────── + { + // cree un script sans permission d'execution + std::ofstream f("www/cgi-bin/noperm.py"); + f << "#!/usr/bin/env python3\nprint('Content-Type: text/html\\n\\nhello')\n"; + f.close(); + chmod("www/cgi-bin/noperm.py", 0644); + + HttpResponse res = handler.handle(makeReq("GET", "/cgi-bin/noperm.py"), loc, server); + check("CGI 403: script non executable", res.status_code == 403); + + remove("www/cgi-bin/noperm.py"); + } + + // ── CAS 7 : URI sans extension CGI → pas de CGI ───────────────── + { + LocationConfig locGet; + locGet.setPath("/"); + locGet.setRoot("www"); + locGet.setIndex("index.html"); + locGet.addMethod("GET"); + + HttpResponse res = handler.handle(makeReq("GET", "/index.html"), locGet, server); + check("Non-CGI: index.html servi normalement", res.status_code == 200); + check("Non-CGI: pas de CGI pour .html", res.body.find("WebServ") != std::string::npos); + } + + std::cout << std::endl << passed << " passed, " << failed << " failed" << std::endl; + return failed > 0 ? 1 : 0; +} diff --git a/www/cgi-bin/hello.py b/www/cgi-bin/hello.py new file mode 100755 index 0000000..779d40c --- /dev/null +++ b/www/cgi-bin/hello.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +import os +import sys + +method = os.environ.get("REQUEST_METHOD", "GET") +query = os.environ.get("QUERY_STRING", "") + +name = "World" +for param in query.split("&"): + if param.startswith("name="): + name = param.split("=", 1)[1] + +body_data = "" +if method == "POST": + length = int(os.environ.get("CONTENT_LENGTH", 0)) + if length > 0: + body_data = sys.stdin.read(length) + +print("Content-Type: text/html") +print("") +print("") +print("

CGI fonctionne !

") +print("

Methode : " + method + "

") +print("

Bonjour " + name + " !

") +if body_data: + print("

Body recu : " + body_data + "

") +print("")