Files
capy-bearer/scripts/test_fcgi_forward_native.cpp
T

248 lines
10 KiB
C++

#include "src/lib/types.cpp"
#include "src/lib/dvalue.cpp"
#include "src/lib/functionlib.cpp"
#include "src/lib/fcgi_forward.h"
#include <sys/wait.h>
#include <cassert>
#include <chrono>
static String record(unsigned char type, const String& content, int padding = 0)
{
String out;
unsigned char header[8] = {1, type, 0, 1, (unsigned char)(content.size() >> 8), (unsigned char)content.size(), (unsigned char)padding, 0};
out.append((const char*)header, 8);
return out + content + String(padding, 'p');
}
static void send_all(int fd, const String& bytes, bool fragment = false)
{
for(size_t pos = 0; pos < bytes.size();)
{
ssize_t n = send(fd, bytes.data() + pos, fragment ? 1 : bytes.size() - pos, MSG_NOSIGNAL);
if(n <= 0) return;
pos += n;
}
}
static FcgiRequestResult run(int family, int mode, u64 limit = 8 * 1024 * 1024, bool adapter = false)
{
FcgiRequestOptions options;
options.timeout_ms = mode == 7 || mode == 9 ? 150 : 2000;
options.max_response_bytes = limit;
int listener = socket(family, SOCK_STREAM, 0);
assert(listener >= 0);
String path = "/tmp/bearer-fcgi-native-" + std::to_string(getpid()) + ".sock";
sockaddr_storage address{};
socklen_t length;
if(family == AF_UNIX)
{
options.socket_path = path;
auto& a = (sockaddr_un&)address;
a.sun_family = family;
memcpy(a.sun_path, path.c_str(), path.size() + 1);
length = sizeof(a);
unlink(path.c_str());
}
else if(family == AF_INET)
{
options.host = "127.0.0.1";
auto& a = (sockaddr_in&)address;
a.sin_family = family;
inet_pton(family, options.host.c_str(), &a.sin_addr);
length = sizeof(a);
}
else
{
options.host = "::1";
auto& a = (sockaddr_in6&)address;
a.sin6_family = family;
inet_pton(family, options.host.c_str(), &a.sin6_addr);
length = sizeof(a);
}
if(bind(listener, (sockaddr*)&address, length) != 0)
{
assert(family == AF_INET6 && (errno == EADDRNOTAVAIL || errno == EAFNOSUPPORT));
close(listener);
return {};
}
assert(listen(listener, 2) == 0);
assert(getsockname(listener, (sockaddr*)&address, &length) == 0);
if(family == AF_INET) options.port = ntohs(((sockaddr_in&)address).sin_port);
if(family == AF_INET6) options.port = ntohs(((sockaddr_in6&)address).sin6_port);
StringMap env;
env[String(140, 'K')] = String(70000, 'V');
String body(mode == 9 ? 4 * 1024 * 1024 : 300000, 'b'); body[17] = 0;
String expected = fcgi_build_request(env, body);
pid_t child = fork();
assert(child >= 0);
if(child == 0)
{
alarm(4);
int fd = accept(listener, nullptr, nullptr);
assert(fd >= 0);
if(mode == 9)
{
usleep(300000);
close(fd); close(listener); _exit(0);
}
if(mode == 8)
{
// Fill the output before consuming the large request.
String output;
fcgi_forward_stream(output, 6, String(300000, 'o'));
send_all(fd, output);
}
String input;
char buffer[65536];
while(input.size() < expected.size())
{
ssize_t n = read(fd, buffer, sizeof(buffer));
if(n <= 0) _exit(2);
input.append(buffer, n);
}
assert(input == expected);
String end(8, 0);
if(mode == 1) { end[3] = 42; end[4] = 2; }
String output = record(6, String("X: a\r\nX: b\r\n\r\na\0b", 17), 7) + record(7, "diagnostic", 3);
if(mode == 8) output = record(7, "diagnostic");
if(mode == 13) output = record(6, "Status: 201 Created\r\nContent-Type: text/plain\r\n\r\ncreated");
if(mode == 14) output = record(6, "unframed worker output");
if(mode == 2) output[0] = 2;
if(mode == 3) output[3] = 2;
if(mode == 10) output = record(6, "") + record(6, "after close");
if(mode == 11) { output[6] = 255; output.resize(12); }
if(mode == 12) output[1] = 4;
if(mode == 4) output += record(3, String(7, 0));
else if(mode == 5) { end[4] = 4; output += record(3, end); }
else if(mode == 6) output += record(3, end).substr(0, 12);
else if(mode == 7)
{
for(int i = 0; i < 500; ++i)
{
send_all(fd, record(7, "x"));
usleep(1000);
}
}
else output += record(3, end);
if(mode != 7) send_all(fd, output, mode == 0);
// A second connection would repeat a side effect. Reject retries.
pollfd item{listener, POLLIN, 0};
assert(poll(&item, 1, 30) == 0);
close(fd); close(listener); _exit(0);
}
close(listener);
auto start = std::chrono::steady_clock::now();
FcgiRequestResult result;
if(adapter)
{
FcgiForwardResult forwarded = fcgi_forward_request(path, env, body, 2, limit);
assert(forwarded.ok == (mode == 0 || mode == 13 || mode == 14));
if(mode == 0)
{
assert(forwarded.status == 200 && forwarded.headers["X"] == "b");
assert(forwarded.body == String("a\0b", 3));
}
if(mode == 13)
{
assert(forwarded.status == 201 && forwarded.headers["Content-Type"] == "text/plain");
assert(forwarded.body == "created");
}
if(mode == 14)
{
assert(forwarded.status == 200 && forwarded.headers.empty());
assert(forwarded.body == "unframed worker output");
}
}
else result = fcgi_request(options, env, body);
auto elapsed = std::chrono::steady_clock::now() - start;
assert(elapsed < std::chrono::seconds(2));
int status; assert(waitpid(child, &status, 0) == child);
assert(WIFEXITED(status) && WEXITSTATUS(status) == 0);
unlink(path.c_str());
if(adapter) return result;
if(mode == 0 || mode == 1)
{
if(limit < 27) assert(!result.ok && result.error_code == "output_limit");
else
{
assert(result.ok && result.stderr_data == "diagnostic");
assert(result.stdout_data == String("X: a\r\nX: b\r\n\r\na\0b", 17));
assert(result.app_status == (mode == 1 ? 42u : 0u));
assert(result.protocol_status == (mode == 1 ? 2 : 0));
}
}
else if(mode == 8) assert(result.ok && result.stdout_data == String(300000, 'o'));
else assert(!result.ok);
if(mode >= 2 && mode <= 5) assert(result.error_code == "protocol");
if(mode == 6 || mode == 11) assert(result.error_code == "truncated");
if(mode == 7 || mode == 9) assert(result.error_code == "timeout");
if(mode == 10 || mode == 12) assert(result.error_code == "protocol");
return result;
}
int main()
{
signal(SIGPIPE, SIG_IGN);
auto handshake = [](String path, String cookie, String origin) {
StringMap params = split_http_headers("GET " + path + " HTTP/1.1\r\n"
"Host: socket.example.test\r\nCookie: discarded=1\r\ncOoKiE: " + cookie + "\r\n"
"Origin: https://discarded.example.test\r\noRiGiN: " + origin + "\r\n"
"Authorization: Bearer test-token\r\nX-Forwarded-Proto: https\r\n"
"Bearer-CLI: 1\r\nContent-Length: 4\r\nContent-Type: text/plain\r\n\r\n");
params["SCRIPT_FILENAME"] = "/srv/site" + params["DOCUMENT_URI"];
params["DOCUMENT_ROOT"] = "/srv/site";
params["REMOTE_ADDR"] = "192.0.2.1";
params["HTTPS"] = "on";
params["BEARER_CLI"] = "1";
params["BEARER_WS_DISPATCH"] = "1";
params["BEARER_WS_STATE"] = "stale-worker-state";
params["ROUTE_PAGE"] = "stale-route";
return(params);
};
const StringMap alice = handshake("/alice.capy?room=one&%2Fprivate", "session=alice; csrf=a", "https://alice.example.test");
const StringMap bob = handshake("/bob.capy?room=two", "session=bob; csrf=b", "https://bob.example.test");
StringMap alice_frame = fcgi_websocket_request_context(alice);
StringMap bob_frame = fcgi_websocket_request_context(bob);
assert(alice_frame.at("HTTP_COOKIE") == "session=alice; csrf=a");
assert(alice_frame.at("HTTP_ORIGIN") == "https://alice.example.test");
assert(bob_frame.at("HTTP_COOKIE") == "session=bob; csrf=b");
assert(bob_frame.at("HTTP_ORIGIN") == "https://bob.example.test");
for(const StringMap* source : {&alice, &bob})
{
StringMap frame = fcgi_websocket_request_context(*source);
for(const String key : {"REQUEST_URI", "DOCUMENT_URI", "SCRIPT_FILENAME", "QUERY_STRING", "DOCUMENT_ROOT", "REMOTE_ADDR", "HTTPS", "HTTP_AUTHORIZATION", "HTTP_X_FORWARDED_PROTO"})
assert(frame.at(key) == source->at(key));
for(const String key : {"CONTENT_LENGTH", "CONTENT_TYPE", "REQUEST_METHOD", "BEARER_CLI", "BEARER_WS_DISPATCH", "BEARER_WS_STATE", "ROUTE_PAGE"})
assert(!frame.contains(key));
assert(frame.at("HTTP_BEARER_CLI") == "1");
}
alice_frame["HTTP_COOKIE"] = "session=forged";
alice_frame["QUERY_STRING"] = "room=other";
assert(fcgi_websocket_request_context(alice).at("HTTP_COOKIE") == "session=alice; csrf=a");
assert(fcgi_websocket_request_context(alice).at("QUERY_STRING") == "room=one&%2Fprivate");
assert(bob_frame.at("HTTP_COOKIE") == "session=bob; csrf=b");
assert(fcgi_websocket_request_context(split_http_headers("GET /anonymous.capy HTTP/1.1\r\nHost: socket.example.test\r\n\r\n")).count("HTTP_COOKIE") == 0);
String lengths;
fcgi_forward_put_len(lengths, 127);
fcgi_forward_put_len(lengths, 128);
fcgi_forward_put_len(lengths, 70000);
assert(lengths == String("\x7f\x80\x00\x00\x80\x80\x01\x11\x70", 9));
for(int mode = 0; mode <= 12; ++mode) run(AF_UNIX, mode);
run(AF_UNIX, 0, 25);
run(AF_UNIX, 0, 1024, true);
run(AF_UNIX, 1, 1024, true);
run(AF_UNIX, 13, 1024, true);
run(AF_UNIX, 14, 1024, true);
run(AF_INET, 0);
run(AF_INET6, 0);
FcgiRequestOptions options;
assert(fcgi_request(options, {}, "").error_code == "invalid_options");
options.socket_path = "/unused"; options.host = "127.0.0.1";
assert(fcgi_request(options, {}, "").error_code == "invalid_options");
options.socket_path = ""; options.host = "localhost"; options.port = 9000;
assert(fcgi_request(options, {}, "").error_code == "invalid_options");
options.host = "127.0.0.1"; options.timeout_ms = 0;
assert(fcgi_request(options, {}, "").error_code == "invalid_options");
return 0;
}