uce/src/lib/fcgi_forward.h
2026-06-27 20:58:07 +00:00

173 lines
5.5 KiB
C++

#pragma once
// Minimal FastCGI client used by the connection brokers (the custom HTTP server
// dispatcher and the websocket exec child) to render a request through a normal
// worker on FCGI_SOCKET_PATH instead of rendering wasm in the broker's own forked
// process. Wasmtime cannot be safely re-created across fork, so the broker owns
// the connection but forwards the actual unit invocation to a clean-engine
// worker — the "broker holds connections, units respond like RENDER()" model.
//
// Native-only; compiled into the main object. Needs String/StringMap (uce_lib.h)
// plus the socket headers below.
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <cstring>
#include <algorithm>
struct FcgiForwardResult {
bool ok = false;
String error;
int status = 200; // parsed from a CGI "Status:" header if present
StringMap headers; // response headers (excluding Status)
String body; // response body
};
// One FastCGI stream (PARAMS/STDIN): data records (<=64KB each) then the
// terminating empty record the protocol requires to close the stream.
inline void fcgi_forward_stream(String& out, unsigned char type, const String& content)
{
size_t off = 0, len = content.size();
while(off < len)
{
size_t chunk = std::min(len - off, (size_t)0xffff);
unsigned char hdr[8] = { 1, type, 0, 1, (unsigned char)((chunk >> 8) & 0xff), (unsigned char)(chunk & 0xff), 0, 0 };
out.append((const char*)hdr, 8);
out.append(content.data() + off, chunk);
off += chunk;
}
unsigned char term[8] = { 1, type, 0, 1, 0, 0, 0, 0 };
out.append((const char*)term, 8);
}
// FastCGI name/value pair length prefix: 1 byte if < 128, else 4 bytes (high bit set).
inline void fcgi_forward_put_len(String& out, size_t n)
{
if(n < 128)
out.push_back((char)(unsigned char)n);
else
{
out.push_back((char)(unsigned char)(((n >> 24) & 0xff) | 0x80));
out.push_back((char)(unsigned char)((n >> 16) & 0xff));
out.push_back((char)(unsigned char)((n >> 8) & 0xff));
out.push_back((char)(unsigned char)(n & 0xff));
}
}
// Build the full FastCGI request bytes (BEGIN_REQUEST + PARAMS + STDIN) for a
// RESPONDER request id 1. The broker reuses this to fire renders non-blocking.
inline String fcgi_build_request(const StringMap& params, const String& stdin_body)
{
String request;
unsigned char begin[16] = { 1, 1, 0, 1, 0, 8, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 };
request.append((const char*)begin, 16);
String params_encoded;
for(auto& kv : params)
{
fcgi_forward_put_len(params_encoded, kv.first.size());
fcgi_forward_put_len(params_encoded, kv.second.size());
params_encoded.append(kv.first);
params_encoded.append(kv.second);
}
fcgi_forward_stream(request, 4 /*FCGI_PARAMS*/, params_encoded);
fcgi_forward_stream(request, 5 /*FCGI_STDIN*/, stdin_body);
return(request);
}
// Forward `params` + `stdin_body` to the FastCGI responder at unix `socket_path`
// and return the parsed CGI response. Times out (recv) at `timeout_seconds`.
inline FcgiForwardResult fcgi_forward_request(const String& socket_path,
const StringMap& params, const String& stdin_body, u32 timeout_seconds = 30)
{
FcgiForwardResult result;
int fd = ::socket(AF_UNIX, SOCK_STREAM, 0);
if(fd < 0)
{
result.error = "fcgi_forward: socket() failed";
return(result);
}
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, socket_path.c_str(), sizeof(addr.sun_path) - 1);
if(::connect(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0)
{
::close(fd);
result.error = "fcgi_forward: connect(" + socket_path + ") failed";
return(result);
}
struct timeval tv;
tv.tv_sec = timeout_seconds;
tv.tv_usec = 0;
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));
String request = fcgi_build_request(params, stdin_body);
if(::send(fd, request.data(), request.size(), MSG_NOSIGNAL) != (ssize_t)request.size())
{
::close(fd);
result.error = "fcgi_forward: short write to responder";
return(result);
}
// Read records; collect FCGI_STDOUT content until FCGI_END_REQUEST or EOF.
String inbuf, stdout_data;
char buf[65536];
bool ended = false;
while(!ended)
{
ssize_t n = ::recv(fd, buf, sizeof(buf), 0);
if(n <= 0)
break;
inbuf.append(buf, n);
while(inbuf.size() >= 8)
{
const unsigned char* h = (const unsigned char*)inbuf.data();
size_t content = ((size_t)h[4] << 8) + h[5];
size_t padding = h[6];
size_t record_len = 8 + content + padding;
if(inbuf.size() < record_len)
break;
unsigned char type = h[1];
if(type == 6 /*FCGI_STDOUT*/)
stdout_data.append(inbuf.data() + 8, content);
else if(type == 3 /*FCGI_END_REQUEST*/)
ended = true;
inbuf.erase(0, record_len);
}
}
::close(fd);
// Parse the CGI response: header block, then body. Status: header (if any)
// sets the HTTP status; everything else is a response header.
size_t sep = stdout_data.find("\r\n\r\n");
size_t sep_len = 4;
if(sep == String::npos)
{
sep = stdout_data.find("\n\n");
sep_len = 2;
}
String header_block = sep == String::npos ? String() : stdout_data.substr(0, sep);
result.body = sep == String::npos ? stdout_data : stdout_data.substr(sep + sep_len);
for(String line : split(header_block, "\n"))
{
line = trim(line);
if(line == "")
continue;
size_t colon = line.find(":");
if(colon == String::npos)
continue;
String name = trim(line.substr(0, colon));
String value = trim(line.substr(colon + 1));
if(to_lower(name) == "status")
result.status = (int)int_val(value);
else
result.headers[name] = value;
}
result.ok = true;
return(result);
}