#include "cgi.h" #include "libcommon/mime.h" #include #include #include #include #include #include #include #include #include using namespace std::string_literals; namespace bp = boost::process; namespace fs = std::filesystem; namespace { const std::string gateway_interface{"CGI/1.1"}; struct CGIContext { std::function& GetServerParam; std::function& GetRequestParam; // request including body (POST...) std::function& SetResponseHeader; // to be added to result string fs::path& path; fs::path& file_path; fs::path& path_info; CGIContext(std::function& p_GetServerParam, std::function& p_GetRequestParam, std::function& p_SetResponseHeader, fs::path& p_path, fs::path& p_file_path, fs::path& p_path_info ) : GetServerParam(p_GetServerParam) , GetRequestParam(p_GetRequestParam) , SetResponseHeader(p_SetResponseHeader) , path(p_path) , file_path(p_file_path) , path_info(p_path_info) { } }; 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, CGIContext& c){ c.SetResponseHeader("cache_control", v); } }, { "CONTENT-TYPE", [](std::string& v, CGIContext& c){ c.SetResponseHeader("content_type", v); } }, { "SET-COOKIE", [](std::string& v, CGIContext& c){ c.SetResponseHeader("set_cookie", v); } }, { "STATUS", [](std::string& v, CGIContext& c) { std::string status{"500"}; if (v.size() >= 3) { status = v.substr(0, 3); } c.SetResponseHeader("status", status); } } }; void handleHeader(const std::string& s, CGIContext& 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, CGIContext& 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.path_info.string(); 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; 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 executeFile(CGIContext& context) { bp::pipe is_in; bp::ipstream is_out; bp::environment env {boost::this_process::environment()}; setCGIEnvironment(env, context); bp::child child(context.path.string(), 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; 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) && !is_out.eof()) { 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& SetResponseHeader) { SetResponseHeader("status", status); SetResponseHeader("content_type", "text/html"); return status + " " + message; } } // anonymous namespace std::string cgi_plugin::name() { return "cgi"; } cgi_plugin::cgi_plugin() { //std::cout << "Plugin constructor" << std::endl; } cgi_plugin::~cgi_plugin() { //std::cout << "Plugin destructor" << std::endl; } std::string cgi_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); } // Build the path to the requested file std::string doc_root{GetRequestParam("doc_root")}; fs::path path {fs::path{doc_root} / rel_target}; if (target.size() && target.back() != '/' && fs::is_directory(path)) { std::string location{GetRequestParam("location") + "/"s}; SetResponseHeader("location", location); return HttpStatus("301", "Correcting directory path", SetResponseHeader); } fs::path path_info{}; fs::path file_path{target}; try { // file file name part and separate path_info while (!fs::is_regular_file(path) && path != "/" && path != "") { if (path_info.string() == "") path_info = fs::path{"/"} / path.filename(); else path_info = fs::path{"/"} / path.filename() / path_info.relative_path(); path = path.parent_path(); file_path = file_path.parent_path(); } if (!fs::is_regular_file(path)) { return HttpStatus("500", "Bad Script: "s + rel_target, SetResponseHeader); } } catch (const std::exception& ex) { return HttpStatus("500", "Bad file access: "s + rel_target, SetResponseHeader); } try { if ((fs::status(path).permissions() & fs::perms::others_exec) == fs::perms::none) { return HttpStatus("500", "Script not executable: "s + rel_target, SetResponseHeader); } } catch (const std::exception& ex) { return HttpStatus("500", "Bad file status access: "s + rel_target, SetResponseHeader); } SetResponseHeader("content_type", mime_type(path.string())); CGIContext context(GetServerParam, GetRequestParam, SetResponseHeader, path, file_path, path_info); try { return executeFile(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); } } bool cgi_plugin::has_own_authentication() { return false; }