#ifndef _GNU_SOURCE #define _GNU_SOURCE // for execvpe #endif #include "cgi.h" #include #include #include #include #include #include namespace { constexpr size_t CHUNK = 64 * 1024; // Builds a null-terminated char* array from strings for execvp/environ. The // pointers borrow from `strings`, which must outlive the returned vector. std::vector to_c_array(const std::vector &strings) { std::vector out; out.reserve(strings.size() + 1); for (const std::string &s : strings) out.push_back(const_cast(s.c_str())); out.push_back(nullptr); return out; } // Splits one CGI header line "Name: value" into a trimmed pair. Returns false // for malformed lines (no colon). bool parse_header_line(const std::string &line, std::string &name, std::string &value) { const size_t colon = line.find(':'); if (colon == std::string::npos) return false; name = line.substr(0, colon); size_t start = colon + 1; while (start < line.size() && (line[start] == ' ' || line[start] == '\t')) ++start; value = line.substr(start); return true; } // Parses the accumulated CGI header block (already stripped of the trailing // blank line) into `result`. A "Status: NNN ..." header sets the HTTP status; // every other header is preserved verbatim. void parse_headers(const std::string &block, CgiResult &result) { size_t pos = 0; while (pos < block.size()) { size_t end = block.find('\n', pos); if (end == std::string::npos) end = block.size(); std::string line = block.substr(pos, end - pos); pos = end + 1; if (!line.empty() && line.back() == '\r') line.pop_back(); if (line.empty()) continue; std::string name, value; if (!parse_header_line(line, name, value)) continue; if (name == "Status") result.status = std::atoi(value.c_str()); else result.headers.emplace_back(std::move(name), std::move(value)); } } } // namespace void run_cgi(const std::vector &argv, const std::vector &env, int input_fd, const std::function &on_headers) { int out_pipe[2]; if (pipe(out_pipe) != 0) throw std::runtime_error("cgi: pipe failed"); const pid_t pid = fork(); if (pid < 0) { close(out_pipe[0]); close(out_pipe[1]); throw std::runtime_error("cgi: fork failed"); } if (pid == 0) { // Child: wire up stdin/stdout, then exec. const int in = input_fd >= 0 ? input_fd : open("/dev/null", O_RDONLY); if (in >= 0) dup2(in, STDIN_FILENO); dup2(out_pipe[1], STDOUT_FILENO); close(out_pipe[0]); close(out_pipe[1]); std::vector c_argv = to_c_array(argv); std::vector c_env = to_c_array(env); execvpe(c_argv[0], c_argv.data(), c_env.data()); _exit(127); // exec failed } // Parent: read the child's stdout. close(out_pipe[1]); std::string header_block; std::ostream *body = nullptr; // set once headers are parsed char buf[CHUNK]; ssize_t n; while ((n = read(out_pipe[0], buf, sizeof(buf))) > 0) { if (body != nullptr) { body->write(buf, n); continue; } header_block.append(buf, n); const size_t sep_crlf = header_block.find("\r\n\r\n"); const size_t sep_lf = header_block.find("\n\n"); size_t sep = std::string::npos; size_t skip = 0; if (sep_crlf != std::string::npos && (sep_lf == std::string::npos || sep_crlf <= sep_lf)) { sep = sep_crlf; skip = 4; } else if (sep_lf != std::string::npos) { sep = sep_lf; skip = 2; } if (sep != std::string::npos) { const std::string rest = header_block.substr(sep + skip); header_block.resize(sep); CgiResult result; parse_headers(header_block, result); body = &on_headers(result); body->write(rest.data(), rest.size()); } } close(out_pipe[0]); // No blank line ever arrived: still surface whatever headers we have. if (body == nullptr) { CgiResult result; parse_headers(header_block, result); on_headers(result); } int wstatus = 0; waitpid(pid, &wstatus, 0); if (!WIFEXITED(wstatus) || WEXITSTATUS(wstatus) != 0) throw std::runtime_error("cgi: child exited abnormally"); }