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.cpp284
1 files changed, 284 insertions, 0 deletions
diff --git a/plugins/fcgi/fcgi.cpp b/plugins/fcgi/fcgi.cpp
new file mode 100644
index 0000000..d301579
--- /dev/null
+++ b/plugins/fcgi/fcgi.cpp
@@ -0,0 +1,284 @@
+// WIP!
+#include "fcgi.h"
+
+#include "fastcgi.h"
+
+#include <boost/algorithm/string/predicate.hpp>
+#include <boost/coroutine2/coroutine.hpp>
+#include <boost/process.hpp>
+
+#include <algorithm>
+#include <filesystem>
+#include <fstream>
+#include <iostream>
+#include <string>
+#include <unordered_map>
+
+using namespace std::string_literals;
+namespace bp = boost::process;
+namespace fs = std::filesystem;
+
+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)
+ {
+ 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<std::string> 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<std::string, std::function<void(std::string&, FCGIContext&)>> 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 setCGIEnvironment(bp::environment& 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);
+ }
+
+ //TODO: env["PATH_INFO"] = c.path_info.string();
+ //TODO: env["PATH_TRANSLATED"] = c.path.string();
+ 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;
+ //TODO: env["SCRIPT_NAME"] = c.file_path;
+ 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");
+ }
+
+ std::string fcgiQuery(FCGIContext& context)
+ {
+ 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);
+ }
+
+ child.wait();
+
+ return output;
+ }
+
+ // Used to return errors by generating response page and HTTP status code
+ std::string HttpStatus(std::string status, std::string message, std::function<plugin_interface_setter_type>& SetResponseHeader)
+ {
+ SetResponseHeader("status", status);
+ SetResponseHeader("content_type", "text/html");
+ return status + " " + message;
+ }
+
+} // anonymous namespace
+
+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<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
+)
+{
+ 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);
+ }
+
+ // Build the path to the requested file
+ std::string app_addr{GetRequestParam("doc_root")};
+
+ 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);
+ }
+}
+