From d562c5bcbfe99aa782fd5d7f9f6494d5bf9921cd Mon Sep 17 00:00:00 2001 From: Quinten Date: Wed, 15 Oct 2025 18:34:42 +0200 Subject: [PATCH] feat: CGI --- htdocs/site-3/index.php | 2 +- webserv/client/Client.cpp | 111 +++++++++++++++++++++++++++------ webserv/client/Client.hpp | 15 +++-- webserv/handler/CgiProcess.cpp | 50 ++++++++++----- webserv/handler/CgiProcess.hpp | 2 +- webserv/server/Server.cpp | 17 ++--- webserv/socket/ASocket.hpp | 9 ++- webserv/socket/CgiSocket.cpp | 23 +++++++ webserv/socket/CgiSocket.hpp | 3 + 9 files changed, 174 insertions(+), 58 deletions(-) diff --git a/htdocs/site-3/index.php b/htdocs/site-3/index.php index bb5f6a6..f35a8ef 100644 --- a/htdocs/site-3/index.php +++ b/htdocs/site-3/index.php @@ -1,3 +1,3 @@ socket, Server &server) : httpRequest_(std::make_unique(this)), httpResponse_(std::make_unique()), - router_(std::make_unique(this)), client_socket_(std::move(socket)), server_(std::ref(server)) + router_(std::make_unique(this)), clientSocket_(std::move(socket)), server_(std::ref(server)) { Log::trace(LOCATION); - Log::info("New client connected, fd: " + std::to_string(client_socket_->getFd())); - client_socket_->setCallback([this]() { request(); }); + Log::info("New client connected, fd: " + std::to_string(clientSocket_->getFd())); + clientSocket_->setCallback([this]() { request(); }); } Client::~Client() { Log::trace(LOCATION); - Log::info("Client disconnected, fd: " + std::to_string(client_socket_->getFd())); - server_.remove(*client_socket_); + Log::info("Client disconnected, fd: " + std::to_string(clientSocket_->getFd())); + server_.remove(*clientSocket_); }; int Client::getStatusCode() const @@ -47,13 +47,17 @@ void Client::setStatusCode(int code) ASocket &Client::getSocket(int fd) const { - if (fd == -1 || client_socket_->getFd() == fd) + if (fd == -1 || clientSocket_->getFd() == fd) { - return *client_socket_; + return *clientSocket_; } - if (cgi_socket_ && cgi_socket_->getFd() == fd) + if (cgiStdIn_ && cgiStdIn_->getFd() == fd) { - return *client_socket_; // TODO return cgi socket + return *cgiStdIn_; + } + if (cgiStdOut_ && cgiStdOut_->getFd() == fd) + { + return *cgiStdOut_; } Log::error("Socket not found for fd: " + std::to_string(fd)); throw std::runtime_error("Socket not found for fd: " + std::to_string(fd)); @@ -63,7 +67,7 @@ void Client::request() { Log::trace(LOCATION); char buffer[bufferSize_] = {}; // NOLINT(cppcoreguidelines-avoid-c-arrays) - ssize_t bytesRead = client_socket_->read( + ssize_t bytesRead = clientSocket_->read( buffer, sizeof(buffer) - 1); // NOLINT(cppcoreguidelines-pro-bounds-array-to-pointer-decay) if (bytesRead < 0) { @@ -72,7 +76,7 @@ void Client::request() } if (bytesRead == 0) { - Log::info("Client closed connection, fd: " + std::to_string(client_socket_->getFd())); + Log::info("Client closed connection, fd: " + std::to_string(clientSocket_->getFd())); server_.disconnect(*this); // CRITICAL: RETURN IMMEDIATELY return; } @@ -105,10 +109,76 @@ void Client::request() } } -void Client::setCgiSocket(std::unique_ptr cgiSocket) +void Client::writeToCgi() { - server_.add(*cgiSocket, EPOLLIN, this); - cgi_socket_ = std::move(cgiSocket); + Log::trace(LOCATION); + if (cgiStdIn_ == nullptr) + { + Log::error("CGI stdin socket is null"); + return; + } + if (httpRequest_->getBody().empty()) + { + Log::debug("No body to write to CGI stdin, fd: " + std::to_string(cgiStdIn_->getFd())); + server_.remove(*cgiStdIn_); + cgiStdIn_ = nullptr; + return; + } + ssize_t bytesWritten = cgiStdIn_->write(httpRequest_->getBody().data(), httpRequest_->getBody().size()); + if (bytesWritten < 0) + { + Log::error("Failed to write to CGI stdin, fd: " + std::to_string(cgiStdIn_->getFd())); + } + else + { + Log::debug("Wrote " + std::to_string(bytesWritten) + + " bytes to CGI stdin, fd: " + std::to_string(cgiStdIn_->getFd())); + } +} + +void Client::readFromCgi() +{ + Log::trace(LOCATION); + if (cgiStdOut_ == nullptr) + { + Log::error("CGI stdout socket is null"); + return; + } + char buffer[bufferSize_] = {}; // NOLINT(cppcoreguidelines-avoid-c-arrays) + ssize_t bytesRead + = cgiStdOut_->read(buffer, sizeof(buffer) - 1); // NOLINT(cppcoreguidelines-pro-bounds-array-to-pointer-decay) + if (bytesRead < 0) + { + Log::error("Failed to read from CGI stdout, fd: " + std::to_string(cgiStdOut_->getFd())); + } + else if (bytesRead == 0) + { + Log::info("CGI process closed stdout, fd: " + std::to_string(cgiStdOut_->getFd())); + server_.remove(*cgiStdOut_); + cgiStdOut_ = nullptr; + return; + } + else + { + buffer[bytesRead] = '\0'; // NOLINT(cppcoreguidelines-pro-bounds-constant-array-index) + httpResponse_->addHeader("Content-Type", "text/html"); + httpResponse_->setBody(std::string(buffer, static_cast(bytesRead))); + Log::debug("Read " + std::to_string(bytesRead) + + " bytes from CGI stdout, fd: " + std::to_string(cgiStdOut_->getFd())); + } +} + +void Client::setCgiSockets(std::unique_ptr cgiStdIn, std::unique_ptr cgiStdOut) +{ + cgiStdIn->setCallback([this]() { writeToCgi(); }); + cgiStdOut->setCallback([this]() { readFromCgi(); }); + + cgiStdOut_ = std::move(cgiStdOut); + cgiStdIn_ = std::move(cgiStdIn); + + server_.add(*cgiStdOut_, EPOLLIN, this); // read + server_.add(*cgiStdIn_, EPOLLOUT, this); // write + // TODO add to handler } @@ -116,24 +186,25 @@ void Client::poll() const { if (httpResponse_->isComplete()) { - Log::info("Response is ready to be sent to client, fd: " + std::to_string(client_socket_->getFd())); - client_socket_->setCallback([this]() { respond(); }); - server_.responseReady(client_socket_->getFd()); + Log::info("Response is ready to be sent to client, fd: " + std::to_string(clientSocket_->getFd())); + clientSocket_->setCallback([this]() { respond(); }); + server_.responseReady(clientSocket_->getFd()); } } void Client::respond() const { auto payload = httpResponse_->toBytes(); - ssize_t bytesSent = send(client_socket_->getFd(), payload.data(), payload.size(), 0); + ssize_t bytesSent = send(clientSocket_->getFd(), payload.data(), payload.size(), 0); if (bytesSent < 0) { - Log::error("Send failed for fd: " + std::to_string(client_socket_->getFd())); + Log::error("Send failed for fd: " + std::to_string(clientSocket_->getFd())); } else { - Log::debug("Sent " + std::to_string(bytesSent) + " bytes to fd: " + std::to_string(client_socket_->getFd())); + Log::debug("Sent " + std::to_string(bytesSent) + " bytes to fd: " + std::to_string(clientSocket_->getFd())); } + server_.disconnect(*this); // ! CRITICAL: RETURN IMMEDIATELY } HttpRequest &Client::getHttpRequest() const diff --git a/webserv/client/Client.hpp b/webserv/client/Client.hpp index 55ebb01..d57dd91 100644 --- a/webserv/client/Client.hpp +++ b/webserv/client/Client.hpp @@ -13,9 +13,7 @@ #include // for ClientSocket #include // for size_t -#include // for uint8_t #include // for unique_ptr -#include // for vector class Server; class ClientSocket; @@ -36,6 +34,10 @@ class Client void request(); void respond() const; + + void writeToCgi(); + void readFromCgi(); + void poll() const; [[nodiscard]] int getStatusCode() const; @@ -43,18 +45,19 @@ class Client [[nodiscard]] ASocket &getSocket(int fd = -1) const; void setStatusCode(int code); - void setCgiSocket(std::unique_ptr cgiSocket); + void setCgiSockets(std::unique_ptr cgiStdIn, std::unique_ptr cgiStdOut); [[nodiscard]] HttpRequest &getHttpRequest() const; [[nodiscard]] HttpResponse &getHttpResponse() const; private: int statusCode_ = Http::StatusCode::OK; - constexpr static size_t bufferSize_ = 4096; + constexpr static size_t bufferSize_ = 8192 * 300; // 8KB buffer for reading CGI output std::unique_ptr httpRequest_; std::unique_ptr httpResponse_; std::unique_ptr router_; - std::unique_ptr client_socket_; - std::unique_ptr cgi_socket_ = nullptr; + std::unique_ptr clientSocket_; + std::unique_ptr cgiStdIn_ = nullptr; + std::unique_ptr cgiStdOut_ = nullptr; Server &server_; }; \ No newline at end of file diff --git a/webserv/handler/CgiProcess.cpp b/webserv/handler/CgiProcess.cpp index b8b08f0..7e4d8da 100644 --- a/webserv/handler/CgiProcess.cpp +++ b/webserv/handler/CgiProcess.cpp @@ -5,13 +5,14 @@ #include +#include #include #include #include #include -CgiProcess::CgiProcess(const HttpRequest &request) : request_(request), _pid(-1), _cgiFd(-1) +CgiProcess::CgiProcess(const HttpRequest &request) : request_(request), _pid(-1) { if (!request_.getUri().isCgi()) { @@ -27,41 +28,56 @@ void CgiProcess::spawn() auto cgiPath = uri.getCgiPath(); auto environment = uri.getCGIEnvironment(); - int sv[2]; - if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0) + // pipes + + int pipeStdin[2]; + int pipeStdout[2]; + + if (pipe(pipeStdin) == -1 || pipe(pipeStdout) == -1) { - throw std::runtime_error("Failed to create socket pair"); + throw std::runtime_error("Failed to create pipes"); } - _cgiFd = sv[1]; _pid = fork(); if (_pid < 0) { - close(sv[0]); - close(sv[1]); + close(pipeStdin[0]); + close(pipeStdin[1]); + close(pipeStdout[0]); + close(pipeStdout[1]); throw std::runtime_error("Failed to fork"); } if (_pid == 0) { - dup2(sv[1], STDIN_FILENO); - dup2(sv[1], STDOUT_FILENO); + dup2(pipeStdin[0], STDIN_FILENO); + dup2(pipeStdout[1], STDOUT_FILENO); - close(sv[0]); - close(sv[1]); + close(pipeStdin[0]); + close(pipeStdin[1]); + close(pipeStdout[0]); + close(pipeStdout[1]); + + // Log::debug("Executing CGI: " + cgiPath); // Prepare arguments std::string fullPath = uri.getFullPath(); - char *args[] = {const_cast(cgiPath.c_str()), const_cast(fullPath.c_str())}; + char *args[] = {const_cast(cgiPath.c_str()), const_cast(fullPath.c_str()), nullptr}; + // Log::debug("With args:", {args[0], args[1]}); + + // TODO: Close all FDs execve(const_cast(cgiPath.c_str()), args, nullptr); + exit(1); } else { // Parent process - auto cgiSocket = std::make_unique(sv[0]); // CgiSocket wraps sv[0] - close(sv[1]); + auto cgiStdIn = std::make_unique(pipeStdin[1]); + auto cgiStdOut = std::make_unique(pipeStdout[0]); + close(pipeStdin[0]); + close(pipeStdout[1]); - // cgiSocket->write(request_.getBody().data(), request_.getBody().size()); - request_.getClient().setCgiSocket(std::move(cgiSocket)); // move the socket to the client - // _cgiFd can be used to communicate with the CGI process + Log::debug("CGI process forked with PID: " + std::to_string(_pid)); + + request_.getClient().setCgiSockets(std::move(cgiStdIn), std::move(cgiStdOut)); // move the sockets to the client } } \ No newline at end of file diff --git a/webserv/handler/CgiProcess.hpp b/webserv/handler/CgiProcess.hpp index 5ff87e9..8b6639c 100644 --- a/webserv/handler/CgiProcess.hpp +++ b/webserv/handler/CgiProcess.hpp @@ -19,7 +19,7 @@ class CgiProcess const HttpRequest &request_; int _pid; - int _cgiFd; + // int _cgiFd; void spawn(); }; \ No newline at end of file diff --git a/webserv/server/Server.cpp b/webserv/server/Server.cpp index 4a86fbe..16f0425 100644 --- a/webserv/server/Server.cpp +++ b/webserv/server/Server.cpp @@ -76,6 +76,7 @@ void Server::add(const ASocket &socket, uint32_t events, Client *client) Log::error("epoll_ctl ADD failed for fd: " + std::to_string(fd) + " with error: " + std::strerror(errno)); throw std::runtime_error("epoll_ctl ADD failed"); } + Log::debug("Socket added to epoll, fd: " + std::to_string(fd)); socketToClient_[fd] = client; } @@ -144,7 +145,7 @@ ServerSocket &Server::getListener(int fd) const return *listener; } } - Log::error("Listener not found for fd: " + std::to_string(fd)); + Log::error("Listener not found for fd: " + std::to_string(fd) + ": " + std::strerror(errno)); throw std::runtime_error("Listener not found for fd: " + std::to_string(fd)); } @@ -165,8 +166,7 @@ void Server::handleRequest(struct epoll_event *event) const Client &client = getClient(client_fd); - client.getSocket().callback(); - + client.getSocket(client_fd).callback(); } void Server::responseReady(int client_fd) const @@ -185,10 +185,11 @@ void Server::responseReady(int client_fd) const void Server::handleResponse(struct epoll_event *event) { - Log::debug("Attempting to send data to fd: " + std::to_string(event->data.fd)); - Client &client = getClient(event->data.fd); - client.getSocket().callback(); - disconnect(client); + int socket_fd = event->data.fd; + Log::debug("Attempting to send data to fd: " + std::to_string(socket_fd)); + Client &client = getClient(socket_fd); + client.getSocket(socket_fd).callback(); + // disconnect(client); } void Server::handleEvent(struct epoll_event *event) @@ -196,7 +197,7 @@ void Server::handleEvent(struct epoll_event *event) Log::trace(LOCATION); if ((event->events & EPOLLERR) > 0 || (event->events & EPOLLHUP) > 0) { - Log::error("Epoll error on fd " + std::to_string(event->data.fd)); + Log::error("Epoll error on fd " + std::to_string(event->data.fd) + ": " + std::strerror(errno)); remove(getListener(event->data.fd)); close(event->data.fd); } diff --git a/webserv/socket/ASocket.hpp b/webserv/socket/ASocket.hpp index 4beaf51..c5c8afa 100644 --- a/webserv/socket/ASocket.hpp +++ b/webserv/socket/ASocket.hpp @@ -1,9 +1,8 @@ #pragma once -#include // for function - #include // for size_t #include +#include // for function #include // for ssize_t @@ -29,12 +28,12 @@ class ASocket [[nodiscard]] virtual Type getType() const = 0; [[nodiscard]] int getFd() const; - + void callback() const; void setCallback(std::function callback); - ssize_t read(void *buf, size_t len) const; - ssize_t write(const void *buf, size_t len) const; + virtual ssize_t read(void *buf, size_t len) const; + virtual ssize_t write(const void *buf, size_t len) const; protected: void setNonBlocking() const; diff --git a/webserv/socket/CgiSocket.cpp b/webserv/socket/CgiSocket.cpp index cc464d0..59a644b 100644 --- a/webserv/socket/CgiSocket.cpp +++ b/webserv/socket/CgiSocket.cpp @@ -1,6 +1,7 @@ #include // for LOCATION, Log #include // for ASocket #include +#include CgiSocket::CgiSocket(int fd) : ASocket(fd) { @@ -11,3 +12,25 @@ ASocket::Type CgiSocket::getType() const { return ASocket::Type::CGI_SOCKET; } + +ssize_t CgiSocket::read(void *buf, size_t len) const +{ + Log::trace(LOCATION); + ssize_t bytesRead = ::read(getFd(), buf, len); + if (bytesRead == -1) + { + throw std::system_error(errno, std::generic_category(), "Socket: Read error"); + } + return bytesRead; +} + +ssize_t CgiSocket::write(const void *buf, size_t len) const +{ + Log::trace(LOCATION); + ssize_t bytesSent = ::write(getFd(), buf, len); + if (bytesSent == -1) + { + throw std::system_error(errno, std::generic_category(), "Socket: Write error"); + } + return bytesSent; +} diff --git a/webserv/socket/CgiSocket.hpp b/webserv/socket/CgiSocket.hpp index b69b5c0..8e05020 100644 --- a/webserv/socket/CgiSocket.hpp +++ b/webserv/socket/CgiSocket.hpp @@ -12,4 +12,7 @@ class CgiSocket : public ASocket explicit CgiSocket(int fd); [[nodiscard]] ASocket::Type getType() const override; + + ssize_t read(void *buf, size_t len) const override; + ssize_t write(const void *buf, size_t len) const override; }; \ No newline at end of file