src/cgi.cpp
browsing at commit = e6fed232320fc1bd4c7f35df8325080183937ba4
#ifndef _GNU_SOURCE
#define _GNU_SOURCE // for execvpe
#endif
#include "cgi.h"
#include <cstdlib>
#include <cstring>
#include <stdexcept>
#include <fcntl.h>
#include <sys/wait.h>
#include <unistd.h>
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<char *> to_c_array(const std::vector<std::string> &strings) {
std::vector<char *> out;
out.reserve(strings.size() + 1);
for (const std::string &s : strings)
out.push_back(const_cast<char *>(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<std::string> &argv,
const std::vector<std::string> &env, int input_fd,
const std::function<std::ostream &(const CgiResult &)> &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<char *> c_argv = to_c_array(argv);
std::vector<char *> 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");
}