summaryrefslogtreecommitdiffhomepage
path: root/plugins/fcgi/fcgi.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/fcgi/fcgi.cpp')
-rw-r--r--plugins/fcgi/fcgi.cpp303
1 files changed, 232 insertions, 71 deletions
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 <boost/algorithm/string/predicate.hpp>
+#include <boost/array.hpp>
+#include <boost/asio.hpp>
+#include <boost/endian/conversion.hpp>
#include <boost/coroutine2/coroutine.hpp>
#include <boost/process.hpp>
@@ -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<std::string(const std::string& key)>& GetServerParam;
+ std::function<std::string(const std::string& key)>& GetRequestParam; // request including body (POST...)
+ std::function<void(const std::string& key, const std::string& value)>& SetResponseHeader; // to be added to result string
+
+ FCGIContext(std::function<std::string(const std::string& key)>& p_GetServerParam,
+ std::function<std::string(const std::string& key)>& p_GetRequestParam,
+ std::function<void(const std::string& key, const std::string& value)>& p_SetResponseHeader
+ )
+ : GetServerParam(p_GetServerParam)
+ , GetRequestParam(p_GetRequestParam)
+ , SetResponseHeader(p_SetResponseHeader)
+ {
+ }
+};
+
namespace {
const std::string gateway_interface{"CGI/1.1"};
- struct FCGIContext
- {
- std::function<std::string(const std::string& key)>& GetServerParam;
- std::function<std::string(const std::string& key)>& GetRequestParam; // request including body (POST...)
- std::function<void(const std::string& key, const std::string& value)>& SetResponseHeader; // to be added to result string
-
- FCGIContext(std::function<std::string(const std::string& key)>& p_GetServerParam,
- std::function<std::string(const std::string& key)>& p_GetRequestParam,
- std::function<void(const std::string& key, const std::string& value)>& 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<std::string, std::string>& 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<char> 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<FCGI_BeginRequestRecord*>(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<FCGI_Header*>(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<char>& v)
+ {
+ if (v.size() < sizeof(FCGI_Header))
+ throw std::length_error("No full FCGI header available");
+
+ FCGI_Header& r{*reinterpret_cast<FCGI_Header*>(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<char>& getBuffer()
+ {
+ return m_data;
+ }
+
+ unsigned char getType() { return reinterpret_cast<FCGI_Header*>(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<unsigned char>(v)};
+ return std::string(reinterpret_cast<char*>(&c), 1);
+ }
- return output;
+ std::string encode_u32(size_t v)
+ {
+ uint32_t x {static_cast<uint32_t>(v)};
+ boost::endian::native_to_big_inplace(x);
+ return std::string(reinterpret_cast<char*>(&x), sizeof(x));
+ }
+
+ void FCGI_EncodeEnv(const std::unordered_map<std::string, std::string>& 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<std::string, std::string> 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<char> inbuf;
+ while (!ended) {
+ std::vector<char> 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);