From c0ccf16c69d43a89674640c61d13ec2c02b128d6 Mon Sep 17 00:00:00 2001 From: Roland Reichwein Date: Fri, 10 Apr 2020 15:36:59 +0200 Subject: First working plugin: static-files --- TODO | 1 + config.cpp | 89 +++++++++++++++++---- config.h | 17 ++-- http.cpp | 68 +++------------- https.cpp | 65 +++------------ plugin.cpp | 24 ++++-- plugin_interface.h | 4 +- plugins/static-files/static-files.cpp | 68 +++++++++++++++- response.cpp | 145 +++++++++++++--------------------- response.h | 29 +------ webserver.conf | 7 +- webserver.cpp | 2 +- 12 files changed, 249 insertions(+), 270 deletions(-) diff --git a/TODO b/TODO index 42ac228..a0de147 100644 --- a/TODO +++ b/TODO @@ -4,3 +4,4 @@ Debian 10 Speed up DocRoot, use string_view read: The socket was closed due to a timeout +statistics diff --git a/config.cpp b/config.cpp index 9468e39..78f33e9 100644 --- a/config.cpp +++ b/config.cpp @@ -46,14 +46,7 @@ void Config::readConfigfile(std::string filename) Path path; auto attrs = x.second.get_child(""); path.requested = attrs.get("requested"); - std::string type = attrs.get("type"); - if (type == "files") { - path.type = Files; - } else if (type == "plugin") { - path.type = Plugin; - } else - throw std::runtime_error("Unknown type: "s + type); - for (const auto& param: x.second) { + for (const auto& param: x.second) { // get all sub-elements of if (param.first.size() > 0 && param.first[0] != '<') // exclude meta-elements like path.params[param.first.data()] = param.second.data(); } @@ -163,7 +156,7 @@ void Config::dump() const if (site.paths.size() == 0) std::cout << " Warning: No paths configured." << std::endl; for (const auto& path: site.paths) { - std::cout << " Path: " << path.requested << " -> " << ((path.type == Files) ? "files" : "plugin") << std::endl; + std::cout << " Path: " << path.requested << std::endl; for (const auto& param: path.params) { std::cout << " " << param.first << ": " << param.second << std::endl; } @@ -190,6 +183,7 @@ std::string Config::DocRoot(const Socket& socket, const std::string& requested_h // TODO: speed this up std::string host{requested_host}; std::string result; + size_t path_len{0}; // find longest matching prefix auto pos {host.find(':')}; if (pos != host.npos) { @@ -201,10 +195,9 @@ std::string Config::DocRoot(const Socket& socket, const std::string& requested_h for (const auto& m_host: site.hosts) { if (m_host == host) { for (const auto& path: site.paths) { - if (boost::starts_with(requested_path, path.requested)) { - const auto& root { path.params.at("target")}; - if (root.size() > result.size()) - result = root; + if (boost::starts_with(requested_path, path.requested) && path.requested.size() > path_len) { + path_len = path.requested.size(); + result = path.params.at("target"); } } } @@ -220,6 +213,7 @@ std::string Config::GetPlugin(const Socket& socket, const std::string& requested // TODO: speed this up std::string host{requested_host}; std::string result; + size_t path_len{0}; auto pos {host.find(':')}; if (pos != host.npos) { @@ -231,10 +225,9 @@ std::string Config::GetPlugin(const Socket& socket, const std::string& requested for (const auto& m_host: site.hosts) { if (m_host == host) { for (const auto& path: site.paths) { - if (boost::starts_with(requested_path, path.requested)) { - const auto& root { path.params.at("plugin")}; - if (root.size() > result.size()) - result = root; + if (boost::starts_with(requested_path, path.requested) && path.requested.size() > path_len) { + path_len = path.requested.size(); + result = path.params.at("plugin"); } } } @@ -245,6 +238,68 @@ std::string Config::GetPlugin(const Socket& socket, const std::string& requested return result; } +std::string Config::GetPluginPath(const Socket& socket, const std::string& requested_host, const std::string& requested_path) const +{ + // TODO: speed this up + std::string host{requested_host}; + std::string result; + size_t path_len{0}; + + auto pos {host.find(':')}; + if (pos != host.npos) { + host = host.substr(0, pos); + } + + for (const auto& site: m_sites) { + if (std::find(socket.serve_sites.begin(), socket.serve_sites.end(), site.name) != socket.serve_sites.end()) { + for (const auto& m_host: site.hosts) { + if (m_host == host) { + for (const auto& path: site.paths) { + if (boost::starts_with(requested_path, path.requested) && path.requested.size() > path_len) { + path_len = path.requested.size(); + result = path.requested; + } + } + } + } + } + } + + return result; +} + +std::string Config::GetRelativePath(const Socket& socket, const std::string& requested_host, const std::string& requested_path) const +{ + // TODO: speed this up + std::string host{requested_host}; + std::string result; + size_t path_len{0}; + + auto pos {host.find(':')}; + if (pos != host.npos) { + host = host.substr(0, pos); + } + + for (const auto& site: m_sites) { + if (std::find(socket.serve_sites.begin(), socket.serve_sites.end(), site.name) != socket.serve_sites.end()) { + for (const auto& m_host: site.hosts) { + if (m_host == host) { + for (const auto& path: site.paths) { + if (boost::starts_with(requested_path, path.requested) && path.requested.size() > path_len) { + path_len = path.requested.size(); + result = requested_path.substr(path_len); + } + } + } + } + } + } + + if (result.size() > 0 && result[0] == '/') + return result.substr(1); + return result; +} + bool Config::PluginIsConfigured(const std::string& name) const { for (const auto& site: m_sites) { diff --git a/config.h b/config.h index 4f788d8..61ad1ba 100644 --- a/config.h +++ b/config.h @@ -8,17 +8,11 @@ namespace fs = std::filesystem; -enum PathType -{ - Files, // serve files - Plugin // delegate to plugin -}; - struct Path { std::string requested; // the requested path - PathType type; - std::unordered_map params; // what to serve, e.g. which filesystem path, or which plugin + // default entries: "plugin", "target" + std::unordered_map params; // what to serve, e.g. which filesystem path (target), and which plugin }; struct Site @@ -74,10 +68,17 @@ class Config // secondary calculation functions // + /// Root path in server's file system /// param[in] requested_host e.g. www.domain.com:8080 or www.domain.com std::string DocRoot(const Socket& socket, const std::string& requested_host, const std::string& requested_path) const; + + /// Get name of plugin std::string GetPlugin(const Socket& socket, const std::string& requested_host, const std::string& requested_path) const; + /// requested_path = GetPluginPath() / GetRelativePath() + std::string GetPluginPath(const Socket& socket, const std::string& requested_host, const std::string& requested_path) const; + std::string GetRelativePath(const Socket& socket, const std::string& requested_host, const std::string& requested_path) const; + // return true iff plugin "name" is mentioned in config bool PluginIsConfigured(const std::string& name) const; diff --git a/http.cpp b/http.cpp index 714bcc4..18a8397 100644 --- a/http.cpp +++ b/http.cpp @@ -71,68 +71,20 @@ template< class Send> void handle_request( - Server& server, + ::Server& server, http::request>&& req, Send&& send) { - // Returns a bad request response - auto const bad_request = - [&req](beast::string_view why) - { - http::response res{http::status::bad_request, req.version()}; - res.set(http::field::server, VersionString); - res.set(http::field::content_type, "text/html"); - res.keep_alive(req.keep_alive()); - res.body() = std::string(why); - res.prepare_payload(); - return res; - }; - - // Returns a not found response - auto const not_found = - [&req](beast::string_view target) - { - http::response res{http::status::not_found, req.version()}; - res.set(http::field::server, VersionString); - res.set(http::field::content_type, "text/html"); - res.keep_alive(req.keep_alive()); - res.body() = "The resource '" + std::string(target) + "' was not found."; - res.prepare_payload(); - return res; - }; - - // Returns a server error response - auto const server_error = - [&req](beast::string_view what) - { - http::response res{http::status::internal_server_error, req.version()}; - res.set(http::field::server, VersionString); - res.set(http::field::content_type, "text/html"); - res.keep_alive(req.keep_alive()); - res.body() = "An error occurred: '" + std::string(what) + "'"; - res.prepare_payload(); - return res; - }; - - try { - http::response res{http::status::ok, req.version()}; - res.set(http::field::server, VersionString); - res.set(http::field::content_type, mime_type(extend_index_html(std::string(req.target())))); - res.keep_alive(req.keep_alive()); - std::string res_data = generate_response(req, res, server); - if (req.method() != http::verb::head) { - res.body() = res_data; - res.content_length(res_data.size()); - } - return send(std::move(res)); - } catch(const bad_request_exception& ex) { - return send(bad_request(ex.what())); - } catch(const not_found_exception& ex) { - return send(not_found(ex.what())); - } catch(const server_error_exception& ex) { - return send(server_error(ex.what())); + http::response res{http::status::ok, req.version()}; + res.set(http::field::server, VersionString); + res.set(http::field::content_type, mime_type(extend_index_html(std::string(req.target())))); + res.keep_alive(req.keep_alive()); + std::string res_data = generate_response(req, res, server); + if (req.method() != http::verb::head) { + res.body() = res_data; + res.prepare_payload(); } - + return send(std::move(res)); } //------------------------------------------------------------------------------ diff --git a/https.cpp b/https.cpp index f312295..d410bb3 100644 --- a/https.cpp +++ b/https.cpp @@ -88,63 +88,16 @@ handle_request( http::request>&& req, Send&& send) { - // Returns a bad request response - auto const bad_request = - [&req](beast::string_view why) - { - http::response res{http::status::bad_request, req.version()}; - res.set(http::field::server, VersionString); - res.set(http::field::content_type, "text/html"); - res.keep_alive(req.keep_alive()); - res.body() = std::string(why); - res.prepare_payload(); - return res; - }; - - // Returns a not found response - auto const not_found = - [&req](beast::string_view target) - { - http::response res{http::status::not_found, req.version()}; - res.set(http::field::server, VersionString); - res.set(http::field::content_type, "text/html"); - res.keep_alive(req.keep_alive()); - res.body() = "The resource '" + std::string(target) + "' was not found."; - res.prepare_payload(); - return res; - }; - - // Returns a server error response - auto const server_error = - [&req](beast::string_view what) - { - http::response res{http::status::internal_server_error, req.version()}; - res.set(http::field::server, VersionString); - res.set(http::field::content_type, "text/html"); - res.keep_alive(req.keep_alive()); - res.body() = "An error occurred: '" + std::string(what) + "'"; - res.prepare_payload(); - return res; - }; - - try { - http::response res{http::status::ok, req.version()}; - res.set(http::field::server, VersionString); - res.set(http::field::content_type, mime_type(extend_index_html(std::string(req.target())))); - res.keep_alive(req.keep_alive()); - std::string res_data = generate_response(req, res, server); - if (req.method() != http::verb::head) { - res.body() = res_data; - res.content_length(res_data.size()); - } - return send(std::move(res)); - } catch(const bad_request_exception& ex) { - return send(bad_request(ex.what())); - } catch(const not_found_exception& ex) { - return send(not_found(ex.what())); - } catch(const server_error_exception& ex) { - return send(server_error(ex.what())); + http::response res{http::status::ok, req.version()}; + res.set(http::field::server, VersionString); + res.set(http::field::content_type, mime_type(extend_index_html(std::string(req.target())))); + res.keep_alive(req.keep_alive()); + std::string res_data = generate_response(req, res, server); + if (req.method() != http::verb::head) { + res.body() = res_data; + res.prepare_payload(); } + return send(std::move(res)); } //------------------------------------------------------------------------------ diff --git a/plugin.cpp b/plugin.cpp index 8e4182c..db0632d 100644 --- a/plugin.cpp +++ b/plugin.cpp @@ -60,13 +60,25 @@ bool PluginLoader::validate_config() for (const auto& site: sites) { for (const auto& path: site.paths) { - if (path.type == Plugin) { - std::string plugin {path.params.at("plugin")}; + // path must contain target and plugin + auto it {path.params.find("target")}; + if (it == path.params.end()) { + std::cout << "Path " << path.requested << " for site " << site.name << " is missing target specification." << std::endl; + return false; + } - if (!m_plugins.contains(plugin)) { - std::cout << "Configured plugin " << plugin << " not found" << std::endl; - return false; - } + it = path.params.find("plugin"); + if (it == path.params.end()) { + std::cout << "Path " << path.requested << " for site " << site.name << " is missing plugin specification." << std::endl; + return false; + } + + std::string plugin {it->second}; + + // check if plugin exists + if (!m_plugins.contains(plugin)) { + std::cout << "Configured plugin " << plugin << " not found" << std::endl; + return false; } } } diff --git a/plugin_interface.h b/plugin_interface.h index 9fc2085..c0a95b9 100644 --- a/plugin_interface.h +++ b/plugin_interface.h @@ -5,8 +5,8 @@ #include #include -typedef std::string(*plugin_interface_getter_type)(const std::string& key); -typedef void(*plugin_interface_setter_type)(const std::string& key, const std::string& value); +typedef std::string(plugin_interface_getter_type)(const std::string& key); +typedef void(plugin_interface_setter_type)(const std::string& key, const std::string& value); class BOOST_SYMBOL_VISIBLE webserver_plugin_interface { public: diff --git a/plugins/static-files/static-files.cpp b/plugins/static-files/static-files.cpp index 358e239..ad85f22 100644 --- a/plugins/static-files/static-files.cpp +++ b/plugins/static-files/static-files.cpp @@ -1,8 +1,49 @@ #include "static-files.h" +#include +#include #include +#include using namespace std::string_literals; +namespace fs = std::filesystem; + +namespace { + +std::string getFile(const fs::path& filename) +{ + std::ifstream file(filename.string(), std::ios::in | std::ios::binary | std::ios::ate); + + if (file.is_open()) { + std::ifstream::pos_type fileSize = file.tellg(); + file.seekg(0, std::ios::beg); + + std::string bytes(fileSize, ' '); + file.read(reinterpret_cast(bytes.data()), fileSize); + + return bytes; + + } else { + throw std::runtime_error("Opening "s + filename.string() + " for reading"); + } +} + +std::string extend_index_html(std::string path) +{ + if (path.size() == 0 || (path.size() && path.back() == '/')) + path.append("index.html"); + return path; +} + +// 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; +} + +} std::string static_files_plugin::name() { @@ -26,9 +67,32 @@ std::string static_files_plugin::generate_page( ) { try { - return "Static Files "s + GetServerParam("path"s); + // Make sure we can handle the method + std::string method {GetRequestParam("method")}; + if (method != "GET" && method != "HEAD") + return HttpStatus("400", "Unknown HTTP method", SetResponseHeader); + + // Request path must be absolute and not contain "..". + std::string rel_target{GetRequestParam("rel_target")}; + if (rel_target.find("..") != std::string::npos) { + std::string target{GetRequestParam("target")}; + return HttpStatus("400", "Illegal request: "s + target, SetResponseHeader); + } + + // Build the path to the requested file + std::string doc_root{GetRequestParam("doc_root")}; + std::string path {fs::path{doc_root} / extend_index_html(rel_target)}; + + try { + return getFile(path); + } 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 "Error: "s + ex.what(); + return HttpStatus("500", "Unknown Error: "s + ex.what(), SetResponseHeader); } } diff --git a/response.cpp b/response.cpp index 507b2d7..6b7ae6a 100644 --- a/response.cpp +++ b/response.cpp @@ -2,61 +2,11 @@ #include "file.h" #include +#include +#include using namespace std::placeholders; -namespace { - -// Append an HTTP rel-path to a local filesystem path. -// The returned path is normalized for the platform. -std::string -path_cat( - beast::string_view base, - beast::string_view path) -{ - if(base.empty()) - return std::string(path); - std::string result(base); -#ifdef BOOST_MSVC - char constexpr path_separator = '\\'; - if(result.back() == path_separator) - result.resize(result.size() - 1); - result.append(path.data(), path.size()); - for(auto& c : result) - if(c == '/') - c = path_separator; -#else - char constexpr path_separator = '/'; - if(result.back() == path_separator) - result.resize(result.size() - 1); - result.append(path.data(), path.size()); -#endif - return result; -} - -} - -http_exception::http_exception(std::string message): m_message(message) -{ -} - -const char* http_exception::what() const noexcept -{ - return m_message.data(); -} - -bad_request_exception::bad_request_exception(std::string message): http_exception(message) -{ -} - -not_found_exception::not_found_exception(std::string message): http_exception(message) -{ -} - -server_error_exception::server_error_exception(std::string message): http_exception(message) -{ -} - std::string extend_index_html(std::string path) { if (path.size() && path.back() == '/') @@ -68,61 +18,74 @@ namespace { std::string GetServerParam(const std::string& key, Server& server) { - return ""; + // following are the supported fields: + // ... + throw std::runtime_error("Unsupported server param: "s + key); } -std::string GetRequestParam(const std::string& key, http::request& req) +std::unordered_map> GetRequestParamFunctions{ + // following are the supported fields: + {"target", [](request_type& req, Server& server){return std::string{req.target()};}}, + + {"rel_target", [](request_type& req, Server& server){ + std::string host{req["host"]}; + std::string target{req.target()}; + return server.GetConfig().GetRelativePath(server.GetSocket(), host, target); + }}, + + {"doc_root", [](request_type& req, Server& server) { + std::string host{req["host"]}; + std::string target{req.target()}; + return server.GetConfig().DocRoot(server.GetSocket(), host, target); + }}, + + {"method", [](request_type& req, Server& server){ + if (req.method() == http::verb::get) + return "GET"; + else if (req.method() == http::verb::post) + return "POST"; + else if (req.method() == http::verb::head) + return "HEAD"; + else + return ""; + }}, +}; + +std::string GetRequestParam(const std::string& key, request_type& req, Server& server) { - return ""; + auto it = GetRequestParamFunctions.find(key); + if (it != GetRequestParamFunctions.end()) + return it->second(req, server); + throw std::runtime_error("Unsupported request param: "s + key); } -void SetResponseHeader(const std::string& key, const std::string& value) +void SetResponseHeader(const std::string& key, const std::string& value, response_type& res) { + // following are the supported fields: + + if (key == "status") { // HTTP Status, e.g. "200" (OK) + res.result(unsigned(stoul(value))); + } else if (key == "server") { // Server name/version string + res.set(http::field::server, value); + } else if (key == "content_type") { // e.g. text/html + res.set(http::field::content_type, value); + } else + throw std::runtime_error("Unsupported response field: "s + key); } -} +} // anonymous namespace -std::string generate_response(http::request& req, http::response& res, Server& server) +std::string generate_response(request_type& req, response_type& res, Server& server) { -#if 0 - std::string host{req["host"]}; // TODO: just use string_view + std::string host{req["host"]}; std::string target{req.target()}; std::string plugin_name { server.GetConfig().GetPlugin(server.GetSocket(), host, target)}; plugin_type plugin{server.GetPlugin(plugin_name)}; auto GetServerParamFunction {std::function(std::bind(GetServerParam, _1, std::ref(server)))}; - auto GetRequestParamFunction {std::function(std::bind(GetRequestParam, _1, req))}; - auto SetResponseHeaderFunction{std::function(SetResponseHeader)}; + auto GetRequestParamFunction {std::function(std::bind(GetRequestParam, _1, std::ref(req), std::ref(server)))}; + auto SetResponseHeaderFunction{std::function(std::bind(SetResponseHeader, _1, _2, std::ref(res)))}; return plugin->generate_page(GetServerParamFunction, GetRequestParamFunction, SetResponseHeaderFunction); - -#else - // Make sure we can handle the method - if( req.method() != http::verb::get && - req.method() != http::verb::head) - throw bad_request_exception("Unknown HTTP-method"); - - // Request path must be absolute and not contain "..". - if( req.target().empty() || - req.target()[0] != '/' || - req.target().find("..") != beast::string_view::npos) - throw bad_request_exception("Illegal request-target"); - - // Build the path to the requested file - std::string host{req["host"]}; // TODO: just use string_view - std::string target{req.target()}; - std::string path = path_cat(server.GetConfig().DocRoot(server.GetSocket(), host, target), extend_index_html(std::string(req.target()))); - - std::string result; - try { - result = File::getFile(path); - } catch (const std::runtime_error& ex) { - throw not_found_exception(std::string(req.target())); - } catch (const std::exception& ex) { - throw server_error_exception(ex.what()); - } - - return result; -#endif } diff --git a/response.h b/response.h index a877944..c47a980 100644 --- a/response.h +++ b/response.h @@ -10,31 +10,8 @@ namespace beast = boost::beast; // from namespace http = beast::http; // from -class http_exception: public std::exception -{ - std::string m_message; -public: - http_exception(std::string message); - virtual const char* what() const noexcept; -}; - -class bad_request_exception: public http_exception -{ -public: - bad_request_exception(std::string message); -}; - -class not_found_exception: public http_exception -{ -public: - not_found_exception(std::string message); -}; - -class server_error_exception: public http_exception -{ -public: - server_error_exception(std::string message); -}; +typedef http::request request_type; +typedef http::response response_type; std::string extend_index_html(std::string path); -std::string generate_response(http::request& req, http::response& res, Server& server); +std::string generate_response(request_type& req, response_type& res, Server& server); diff --git a/webserver.conf b/webserver.conf index 0981c0d..a50dc6d 100644 --- a/webserver.conf +++ b/webserver.conf @@ -13,12 +13,12 @@ lists.antcom.de antcom.de www.antcom.de - + static-files /home/ernie/homepage/test