From 5dd4766490e4b29634ea0b3ff35e5d124f657f9c Mon Sep 17 00:00:00 2001 From: Roland Reichwein Date: Sat, 2 May 2020 11:20:37 +0200 Subject: Added FCGI --- plugins/fcgi/fcgi.cpp | 303 ++++++++++++++++++++++++++++++++++++++------------ plugins/fcgi/fcgi.h | 53 +++++++++ 2 files changed, 285 insertions(+), 71 deletions(-) (limited to 'plugins/fcgi') diff --git a/plugins/fcgi/fcgi.cpp b/plugins/fcgi/fcgi.cpp index d301579..eb9abe2 100644 --- a/plugins/fcgi/fcgi.cpp +++ b/plugins/fcgi/fcgi.cpp @@ -1,9 +1,11 @@ -// WIP! #include "fcgi.h" #include "fastcgi.h" #include +#include +#include +#include #include #include @@ -18,27 +20,29 @@ 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"}; - 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) - { - } - }; - // Return a reasonable mime type based on the extension of a file. std::string mime_type(fs::path path) { @@ -122,7 +126,7 @@ namespace { it->second(value, context); } - void setCGIEnvironment(bp::environment& env, FCGIContext& c) + void setFCGIEnvironment(std::unordered_map& env, FCGIContext& c) { std::string authorization {c.GetRequestParam("authorization")}; if (!authorization.empty()) @@ -140,8 +144,8 @@ namespace { target = target.substr(0, query_pos); } - //TODO: env["PATH_INFO"] = c.path_info.string(); - //TODO: env["PATH_TRANSLATED"] = c.path.string(); + 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"] = ""; @@ -149,7 +153,7 @@ namespace { env["REMOTE_USER"] = ""; env["REQUEST_METHOD"] = c.GetRequestParam("method"); env["REQUEST_URI"] = target; - //TODO: env["SCRIPT_NAME"] = c.file_path; + 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"); @@ -167,56 +171,112 @@ namespace { env["HTTPS"] = c.GetRequestParam("https"); } - std::string fcgiQuery(FCGIContext& context) + class FCGI_Record { - bp::pipe is_in; - bp::ipstream is_out; - - bp::environment env {boost::this_process::environment()}; - setCGIEnvironment(env, context); - - bp::child child("", env, bp::std_out > is_out, bp::std_err > stderr, bp::std_in < is_in); - - std::string body{ context.GetRequestParam("body") }; - is_in.write(body.data(), body.size()); - is_in.close(); - - 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 (child.running() && std::getline(is_out, line)) { - processLine(line); + 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)); } + }; - child.wait(); + std::string encode_u8(size_t v) + { + unsigned char c {static_cast(v)}; + return std::string(reinterpret_cast(&c), 1); + } - return output; + 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 @@ -229,6 +289,110 @@ namespace { } // 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"; @@ -262,9 +426,6 @@ std::string fcgi_plugin::generate_page( return HttpStatus("400", "Illegal request: "s + target, SetResponseHeader); } - // Build the path to the requested file - std::string app_addr{GetRequestParam("doc_root")}; - SetResponseHeader("content_type", "text/html"); FCGIContext context(GetServerParam, GetRequestParam, SetResponseHeader); diff --git a/plugins/fcgi/fcgi.h b/plugins/fcgi/fcgi.h index 7edfe91..164fecb 100644 --- a/plugins/fcgi/fcgi.h +++ b/plugins/fcgi/fcgi.h @@ -2,8 +2,60 @@ #include "../../plugin_interface.h" +#include +#include + +// TODO: multithreading +class FCGI_ID +{ + std::setm_unused; + uint16_t m_current_max{}; + +public: + FCGI_ID(){} + + // starting at 1 + uint16_t getID(){ + if (m_unused.empty()) { + m_current_max++; + return m_current_max; + } else { + uint16_t result{*m_unused.begin()}; + m_unused.erase(m_unused.begin()); + return result; + } + } + + void putID(uint16_t id){ + m_unused.insert(id); + } +}; + +// automatically reserves ID, and releases it via RAII +class FCGI_ID_Guard +{ + FCGI_ID& m_fcgi_id; + uint16_t m_id; + +public: + FCGI_ID_Guard(FCGI_ID& fcgi_id): m_fcgi_id(fcgi_id), m_id(fcgi_id.getID()) + { + } + + ~FCGI_ID_Guard() + { + m_fcgi_id.putID(m_id); + } + + uint16_t getID() const { return m_id; } +}; + +struct FCGIContext; + class fcgi_plugin: public webserver_plugin_interface { + FCGI_ID m_fcgi_id; + public: fcgi_plugin(); ~fcgi_plugin(); @@ -15,6 +67,7 @@ public: std::function& SetResponseHeader // to be added to result string ); + std::string fcgiQuery(FCGIContext& context); }; extern "C" BOOST_SYMBOL_EXPORT fcgi_plugin webserver_plugin; -- cgit v1.2.3