#include "fcgi.h" #include "fastcgi.h" #include "socket.h" #include #include #include #include #include #include #include #include #include #include #include using namespace std::string_literals; namespace bp = boost::process; namespace fs = std::filesystem; using boost::asio::ip::tcp; struct FCGIContext { std::function& GetServerParam; std::function& GetRequestParam; // request including body (POST...) std::function& SetResponseHeader; // to be added to result string FCGIContext(std::function& p_GetServerParam, std::function& p_GetRequestParam, std::function& p_SetResponseHeader ) : GetServerParam(p_GetServerParam) , GetRequestParam(p_GetRequestParam) , SetResponseHeader(p_SetResponseHeader) { } }; namespace { const std::string gateway_interface{"CGI/1.1"}; typedef boost::coroutines2::coroutine coro_t; // returns true iff std::string is empty or contains newline bool isEmpty(const std::string& s) { return s.empty() || s == "\r" || s == "\n"s || s == "\r\n"s; } void trimLinebreak(std::string& s) { size_t pos = s.find_last_not_of("\r\n"); if (pos != s.npos) s = s.substr(0, pos + 1); } std::unordered_map> headerMap { { "CACHE-CONTROL", [](std::string& v, FCGIContext& c){ c.SetResponseHeader("cache_control", v); } }, { "CONTENT-TYPE", [](std::string& v, FCGIContext& c){ c.SetResponseHeader("content_type", v); } }, { "SET-COOKIE", [](std::string& v, FCGIContext& c){ c.SetResponseHeader("set_cookie", v); } }, { "STATUS", [](std::string& v, FCGIContext& c) { std::string status{"500"}; if (v.size() >= 3) { status = v.substr(0, 3); } c.SetResponseHeader("status", status); } } }; void handleHeader(const std::string& s, FCGIContext& context) { size_t pos = s.find(": "); if (pos == s.npos) return; std::string key {s.substr(0, pos)}; std::string value {s.substr(pos + 2)}; std::transform(key.begin(), key.end(), key.begin(), ::toupper); auto it {headerMap.find(key)}; if (it == headerMap.end()) std::cout << "Warning: Unhandled CGI header: " << s << std::endl; else it->second(value, context); } void setFCGIEnvironment(std::unordered_map& env, FCGIContext& c) { std::string authorization {c.GetRequestParam("authorization")}; if (!authorization.empty()) env["AUTH_TYPE"] = c.GetRequestParam("authorization"); env["CONTENT_LENGTH"] = c.GetRequestParam("content_length"); env["CONTENT_TYPE"] = c.GetRequestParam("content_type"); env["GATEWAY_INTERFACE"] = gateway_interface; std::string target {c.GetRequestParam("target")}; size_t query_pos {target.find("?")}; std::string query; if (query_pos != target.npos) { query = target.substr(query_pos + 1); target = target.substr(0, query_pos); } env["PATH_INFO"] = c.GetRequestParam("rel_target"); env["PATH_TRANSLATED"] = fs::path{c.GetRequestParam("doc_root")} / c.GetRequestParam("rel_target"); env["QUERY_STRING"] = query; env["REMOTE_ADDR"] = ""; env["REMOTE_HOST"] = ""; env["REMOTE_IDENT"] = ""; env["REMOTE_USER"] = ""; env["REQUEST_METHOD"] = c.GetRequestParam("method"); env["DOCUMENT_ROOT"] = c.GetRequestParam("doc_root"); env["DOCUMENT_URI"] = target; env["REQUEST_URI"] = target; env["SCRIPT_NAME"] = c.GetRequestParam("target"); env["SCRIPT_FILENAME"] = fs::path{c.GetRequestParam("doc_root")} / c.GetRequestParam("target"); env["SERVER_NAME"] = c.GetRequestParam("host"); env["SERVER_PORT"] = c.GetServerParam("port"); env["SERVER_PROTOCOL"] = c.GetRequestParam("http_version"); env["SERVER_SOFTWARE"] = c.GetServerParam("version"); env["HTTP_ACCEPT"] = c.GetRequestParam("http_accept"); env["HTTP_ACCEPT_CHARSET"] = c.GetRequestParam("http_accept_charset"); env["HTTP_ACCEPT_ENCODING"] = c.GetRequestParam("http_accept_encoding"); env["HTTP_ACCEPT_LANGUAGE"] = c.GetRequestParam("http_accept_language"); env["HTTP_CONNECTION"] = c.GetRequestParam("http_connection"); env["HTTP_HOST"] = c.GetRequestParam("http_host"); env["HTTP_USER_AGENT"] = c.GetRequestParam("http_user_agent"); env["HTTP_REFERER"] = c.GetRequestParam("referer"); env["HTTP_COOKIE"] = c.GetRequestParam("cookie"); env["HTTPS"] = c.GetRequestParam("https"); } class FCGI_Record { std::vector m_data; public: // create record to send FCGI_Record(unsigned char type, uint16_t id, unsigned char arg1, unsigned char arg2) { if (type == FCGI_BEGIN_REQUEST) { size_t size {sizeof(FCGI_BeginRequestRecord)}; m_data.resize(size, 0); FCGI_BeginRequestRecord& r{*reinterpret_cast(m_data.data())}; r.header.version = FCGI_VERSION_1; r.header.type = type; r.header.requestIdB1 = id >> 8; r.header.requestIdB0 = id & 0xFF; r.header.contentLengthB1 = sizeof(r.body) >> 8; r.header.contentLengthB0 = sizeof(r.body) & 0xFF; r.body.roleB1 = 0; r.body.roleB0 = arg1; r.body.flags = arg2; } else throw std::runtime_error("Bad FCGI type: "s + std::to_string(type)); } // create record to send FCGI_Record(unsigned char type, uint16_t id, const std::string& data) { if (type == FCGI_PARAMS || type == FCGI_STDIN || type == FCGI_GET_VALUES || type == FCGI_DATA) { size_t size {sizeof(FCGI_Header) + data.size()}; m_data.resize(size); FCGI_Header& h{*reinterpret_cast(m_data.data())}; h.version = FCGI_VERSION_1; h.type = type; h.requestIdB1 = id >> 8; h.requestIdB0 = id & 0xFF; h.contentLengthB1 = data.size() >> 8; h.contentLengthB0 = data.size() & 0xFF; memcpy((void*)&m_data[sizeof(FCGI_Header)], (void*)data.data(), data.size()); } else throw std::runtime_error("Bad FCGI type: "s + std::to_string(type)); } // parse record FCGI_Record(std::vector& v, size_t& start_at) { if (v.size() - start_at < sizeof(FCGI_Header)) throw std::length_error("No full FCGI header available"); FCGI_Header& r{*reinterpret_cast(v.data() + start_at)}; size_t content_length {((size_t)r.contentLengthB1) << 8 | r.contentLengthB0}; size_t record_size {sizeof(FCGI_Header) + content_length + r.paddingLength}; if (v.size() - start_at < record_size) throw std::length_error("No full FCGI record available"); m_data = std::vector(v.begin() + start_at, v.begin() + start_at + record_size - r.paddingLength); start_at += record_size; } std::vector& getBuffer() { return m_data; } unsigned char getType() { return reinterpret_cast(m_data.data())->type; } std::string getContent() { if (m_data.size() < sizeof(FCGI_Header)) throw std::runtime_error("No data available in FCGI_Record: "s + std::to_string(m_data.size()) + " of "s + std::to_string(sizeof(FCGI_Header)) + " bytes"s); return std::string(m_data.data() + sizeof(FCGI_Header), m_data.size() - sizeof(FCGI_Header)); } }; std::string encode_u8(size_t v) { unsigned char c {static_cast(v)}; return std::string(reinterpret_cast(&c), 1); } std::string encode_u32(size_t v) { uint32_t x {static_cast(v)}; x |= 0x80000000; boost::endian::native_to_big_inplace(x); return std::string(reinterpret_cast(&x), sizeof(x)); } void FCGI_EncodeEnv(const std::unordered_map& map, std::string& s) { s.clear(); for (auto&[key, value]: map) { if (key.size() > 127) s += encode_u32(key.size()); else s += encode_u8(key.size()); if (value.size() > 127) s += encode_u32(value.size()); else s += encode_u8(value.size()); s += key; s += value; } } bool decode_size(const std::string& s, size_t& pos, size_t& size) { uint8_t byte {static_cast(s[pos])}; if (byte <= 127) { size = byte; pos += 1; } else { if (s.size() < pos + 4) { std::cerr << "FCGI Error: Broken FCGI_GET_VALUES data (size)" << std::endl; return false; } uint32_t v {*reinterpret_cast(&s[pos])}; boost::endian::big_to_native_inplace(v); v &= ~0x80000000; size = static_cast(v); pos += 4; } return true; } void FCGI_DecodeEnv(const std::string& s, std::unordered_map& map) { map.clear(); size_t pos{0}; while (pos < s.size()) { size_t keysize{}; size_t valuesize{}; if (!decode_size(s, pos, keysize)) return; if (!decode_size(s, pos, valuesize)) return; if (s.size() < pos + keysize) { std::cerr << "FCGI Error: Broken FCGI_GET_VALUES data (key)" << std::endl; return; } std::string key{s.substr(pos, keysize)}; pos += keysize; if (s.size() < pos + valuesize) { std::cerr << "FCGI Error: Broken FCGI_GET_VALUES data (value) for key " << key << std::endl; return; } std::string value{s.substr(pos, valuesize)}; pos += valuesize; if (map.find(key) != map.end()) { std::cerr << "FCGI Warning: Double key in FCGI_GET_VALUES: " << key << std::endl; } else { map[key] = value; } } } // Used to return errors by generating response page and HTTP status code std::string HttpStatus(std::string status, std::string message, std::function& SetResponseHeader) { SetResponseHeader("status", status); SetResponseHeader("content_type", "text/plain"); return status + " " + message; } } // anonymous namespace std::string fcgi_plugin::fcgiQuery(FCGIContext& context) { // host:port or unix domain socket std::string app_addr{context.GetRequestParam("doc_root")}; std::string output_data; std::unordered_map system_config{{"FCGI_MAX_CONNS", ""}, {"FCGI_MAX_REQS", ""}, {"FCGI_MPXS_CONNS", ""}}; std::string system_config_bytes; FCGI_EncodeEnv(system_config, system_config_bytes); std::unordered_map env; setFCGIEnvironment(env, context); std::string env_bytes; FCGI_EncodeEnv(env, env_bytes); std::unordered_map app_values; // will be read by FCGI_GET_VALUES auto it {m_sockets.find(app_addr)}; std::shared_ptr socket; if (it == m_sockets.end()) { // add new element socket = m_socket_factory.create(app_addr); if (!socket) { std::cerr << "FCGI Error: Invalid app_addr." << std::endl; return HttpStatus("500", "FCGI configuration", context.SetResponseHeader); } m_sockets[app_addr] = socket; } else { // use already existing element socket = it->second; } bool opening{false}; std::lock_guard socket_lock{socket->getMutex()}; if (!socket->is_open()) { std::cout << "FCGI: Opening new socket to " << app_addr << std::endl; socket->open(); opening = true; } if (!socket->is_open()) { return HttpStatus("500", "FCGI connection", context.SetResponseHeader); } FCGI_ID_Guard id_guard(socket->fcgi_id()); uint16_t id {id_guard.getID()}; try { std::vector buffer; if (opening) { FCGI_Record get_values{FCGI_GET_VALUES, 0, system_config_bytes}; buffer.insert(buffer.end(), get_values.getBuffer().begin(), get_values.getBuffer().end()); } FCGI_Record begin_request{FCGI_BEGIN_REQUEST, id, FCGI_RESPONDER, FCGI_KEEP_CONN}; buffer.insert(buffer.end(), begin_request.getBuffer().begin(), begin_request.getBuffer().end()); FCGI_Record params{FCGI_PARAMS, id, env_bytes}; buffer.insert(buffer.end(), params.getBuffer().begin(), params.getBuffer().end()); if (env_bytes.size()) { FCGI_Record params_end{FCGI_PARAMS, id, std::string{}}; buffer.insert(buffer.end(), params_end.getBuffer().begin(), params_end.getBuffer().end()); } std::string body {context.GetRequestParam("body")}; FCGI_Record stdin_{FCGI_STDIN, id, body}; buffer.insert(buffer.end(), stdin_.getBuffer().begin(), stdin_.getBuffer().end()); if (body.size()) { FCGI_Record stdin_end{FCGI_STDIN, id, std::string{}}; buffer.insert(buffer.end(), stdin_end.getBuffer().begin(), stdin_end.getBuffer().end()); } if (socket->write(buffer) != buffer.size()) std::cerr << "Warning: Not all bytes written" << std::endl; } catch (const fcgi_eof_error&) { std::cerr << "FCGI Error: EOF on write" << std::endl; // seems to be ok here m_sockets.erase(app_addr); // force new socket next time return HttpStatus("500", "FCGI connection: EOF on write", context.SetResponseHeader); } catch (const fcgi_broken_pipe_error&) { std::cerr << "FCGI Error: Broken pipe on write" << std::endl; // seems to be ok here m_sockets.erase(app_addr); // force new socket next time return HttpStatus("500", "FCGI connection: Broken pipe on write", context.SetResponseHeader); } #if 0 FCGI_Record data{FCGI_DATA, id, std::string{}}; if (socket->write(data.getBuffer()) != data.getBuffer().size()) std::cerr << "Warning: Not all bytes written 8" << std::endl; #endif std::vector inbuf; size_t processed{0}; bool ended{false}; while (!ended) { try { socket->read(inbuf); } catch (const fcgi_eof_error&) { std::cerr << "FCGI Warning: Early EOF from application server. Break." << std::endl; // seems to be ok here m_sockets.erase(app_addr); // force new socket next time ended = true; //return HttpStatus("500", "FCGI connection: EOF on read", context.SetResponseHeader); } while (inbuf.size() - processed > 0) { try { FCGI_Record r{inbuf, processed}; if (r.getType() == FCGI_END_REQUEST) { ended = true; } else if (r.getType() == FCGI_STDOUT) { output_data += r.getContent(); } else if (r.getType() == FCGI_STDERR) { std::string msg {r.getContent()}; if (!msg.empty()) // final FCGI_STDERR sends empty message: ignore std::cerr << "FCGI STDERR: " << msg << std::endl; } else if (r.getType() == FCGI_GET_VALUES_RESULT) { FCGI_DecodeEnv(r.getContent(), app_values); } else throw std::runtime_error("Unhandled FCGI type: "s + std::to_string(r.getType())); } catch (const std::length_error& ex) { // ignore FCGI_Record construction if not enough data available for full record yet break; } } } std::istringstream is_out{output_data}; std::string line; // TODO: C++20 coroutine coro_t::push_type processLine( [&](coro_t::pull_type& in){ std::string line; // read header lines while (in && !isEmpty(line = in.get())) { trimLinebreak(line); handleHeader(line, context); in(); } // read empty line if (!isEmpty(line)) throw std::runtime_error("Missing empty line between CGI header and body"); if (in) in(); }); do { std::getline(is_out, line); processLine(line); } while (!is_out.eof() && !isEmpty(line)); std::string r = output_data.substr(is_out.tellg()); return r; } std::string fcgi_plugin::name() { return "fcgi"; } fcgi_plugin::fcgi_plugin() { //std::cout << "Plugin constructor" << std::endl; } fcgi_plugin::~fcgi_plugin() { //std::cout << "Plugin destructor" << std::endl; } std::string fcgi_plugin::generate_page( std::function& GetServerParam, std::function& GetRequestParam, // request including body (POST...) std::function& SetResponseHeader // to be added to result string ) { try { // Request path must not contain "..". std::string rel_target{GetRequestParam("rel_target")}; size_t query_pos{rel_target.find("?")}; if (query_pos != rel_target.npos) rel_target = rel_target.substr(0, query_pos); std::string target{GetRequestParam("target")}; if (rel_target.find("..") != std::string::npos) { return HttpStatus("400", "Illegal request: "s + target, SetResponseHeader); } SetResponseHeader("content_type", "text/html"); FCGIContext context(GetServerParam, GetRequestParam, SetResponseHeader); try { return fcgiQuery(context); } catch (const std::exception& ex) { return HttpStatus("500", "Internal Server Error: "s + ex.what(), SetResponseHeader); } } catch (const std::exception& ex) { return HttpStatus("500", "Unknown Error: "s + ex.what(), SetResponseHeader); } } bool fcgi_plugin::has_own_authentication() { return false; }