#include "fcgi.h" #include "fastcgi.h" #include #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"}; // Return a reasonable mime type based on the extension of a file. std::string mime_type(fs::path path) { using boost::algorithm::iequals; auto const ext = [&path] { size_t pos = path.string().rfind("."); if (pos == std::string::npos) return std::string{}; return path.string().substr(pos); }(); if(iequals(ext, ".htm")) return "text/html"; // TODO: unordered_map if(iequals(ext, ".html")) return "text/html"; if(iequals(ext, ".php")) return "text/html"; if(iequals(ext, ".css")) return "text/css"; if(iequals(ext, ".txt")) return "text/plain"; if(iequals(ext, ".js")) return "application/javascript"; if(iequals(ext, ".json")) return "application/json"; if(iequals(ext, ".xml")) return "application/xml"; if(iequals(ext, ".swf")) return "application/x-shockwave-flash"; if(iequals(ext, ".flv")) return "video/x-flv"; if(iequals(ext, ".png")) return "image/png"; if(iequals(ext, ".jpe")) return "image/jpeg"; if(iequals(ext, ".jpeg")) return "image/jpeg"; if(iequals(ext, ".jpg")) return "image/jpeg"; if(iequals(ext, ".gif")) return "image/gif"; if(iequals(ext, ".bmp")) return "image/bmp"; if(iequals(ext, ".ico")) return "image/vnd.microsoft.icon"; if(iequals(ext, ".tiff")) return "image/tiff"; if(iequals(ext, ".tif")) return "image/tiff"; if(iequals(ext, ".svg")) return "image/svg+xml"; if(iequals(ext, ".svgz")) return "image/svg+xml"; return "application/text"; } 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("rel_target")} / 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["REQUEST_URI"] = target; env["SCRIPT_NAME"] = 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); 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 = 0; r.header.contentLengthB0 = sizeof(r.body); 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) { size_t size {sizeof(FCGI_Header) + data.size()}; m_data.resize(size); FCGI_Header& r{*reinterpret_cast(m_data.data())}; r.version = FCGI_VERSION_1; r.type = type; r.requestIdB1 = id >> 8; r.requestIdB0 = id & 0xFF; r.contentLengthB1 = 0; r.contentLengthB0 = data.size(); 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) { if (v.size() < sizeof(FCGI_Header)) throw std::length_error("No full FCGI header available"); FCGI_Header& r{*reinterpret_cast(v.data())}; size_t content_length {((size_t)r.contentLengthB1) << 8 | r.contentLengthB0}; size_t record_size {sizeof(FCGI_Header) + content_length}; if (v.size() < record_size) throw std::length_error("No full FCGI record available"); m_data = std::vector(v.begin(), v.begin() + record_size); v.erase(v.begin(), v.begin() + 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)}; 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; } } // 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/html"); 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 env; setFCGIEnvironment(env, context); std::string env_bytes; FCGI_EncodeEnv(env, env_bytes); size_t pos { app_addr.find(':') }; if (pos != app_addr.npos) { // host:port boost::asio::io_context io_context; // TODO: member? tcp::resolver resolver(io_context); auto endpoints{resolver.resolve(app_addr.substr(0, pos), app_addr.substr(pos + 1))}; tcp::socket socket(io_context); boost::asio::connect(socket, endpoints); if (!socket.is_open()) { return HttpStatus("500", "FCGI connection", context.SetResponseHeader); } FCGI_ID_Guard id_guard(m_fcgi_id); uint16_t id{id_guard.getID()}; FCGI_Record begin_request{FCGI_BEGIN_REQUEST, id, FCGI_RESPONDER, FCGI_KEEP_CONN}; socket.write_some(boost::asio::buffer(begin_request.getBuffer())); FCGI_Record params{FCGI_PARAMS, id, env_bytes}; socket.write_some(boost::asio::buffer(params.getBuffer())); FCGI_Record params_end{FCGI_PARAMS, id, std::string{}}; socket.write_some(boost::asio::buffer(params_end.getBuffer())); FCGI_Record stdin_{FCGI_PARAMS, id, context.GetRequestParam("body")}; socket.write_some(boost::asio::buffer(stdin_.getBuffer())); FCGI_Record stdin_end{FCGI_PARAMS, id, std::string{}}; socket.write_some(boost::asio::buffer(stdin_end.getBuffer())); bool ended{false}; std::vector inbuf; while (!ended) { std::vector inbuf_part(1024); size_t got {socket.read_some(boost::asio::buffer(inbuf_part))}; inbuf.insert(inbuf.end(), inbuf_part.begin(), inbuf_part.begin() + got); try { FCGI_Record r{inbuf}; 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::cerr << "FCGI Error: " << r.getContent(); } else throw std::runtime_error("Unhandled FCGI type: "s + std::to_string(r.getType())); } catch (const std::length_error& ex) { // ignore if not enough data available yet } } } else { // Unix domain socket, or file to start // TODO } std::istringstream is_out{output_data}; std::string output; 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(); // read remainder while (in) { line = in.get(); output += line + '\n'; in(); } throw std::runtime_error("Input missing on processing CGI body"); }); while (std::getline(is_out, line)) { processLine(line); } return output; } 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::runtime_error& ex) { return HttpStatus("404", "Not found: "s + GetRequestParam("target"), SetResponseHeader); } 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); } }