diff options
-rw-r--r-- | TODO | 1 | ||||
-rw-r--r-- | config.cpp | 89 | ||||
-rw-r--r-- | config.h | 17 | ||||
-rw-r--r-- | http.cpp | 68 | ||||
-rw-r--r-- | https.cpp | 65 | ||||
-rw-r--r-- | plugin.cpp | 24 | ||||
-rw-r--r-- | plugin_interface.h | 4 | ||||
-rw-r--r-- | plugins/static-files/static-files.cpp | 68 | ||||
-rw-r--r-- | response.cpp | 145 | ||||
-rw-r--r-- | response.h | 29 | ||||
-rw-r--r-- | webserver.conf | 7 | ||||
-rw-r--r-- | webserver.cpp | 2 |
12 files changed, 249 insertions, 270 deletions
@@ -4,3 +4,4 @@ Debian 10 Speed up DocRoot, use string_view read: The socket was closed due to a timeout +statistics @@ -46,14 +46,7 @@ void Config::readConfigfile(std::string filename) Path path; auto attrs = x.second.get_child("<xmlattr>"); path.requested = attrs.get<std::string>("requested"); - std::string type = attrs.get<std::string>("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 <path> if (param.first.size() > 0 && param.first[0] != '<') // exclude meta-elements like <xmlattr> 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) { @@ -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<std::string, std::string> params; // what to serve, e.g. which filesystem path, or which plugin + // default entries: "plugin", "target" + std::unordered_map<std::string, std::string> 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; @@ -71,68 +71,20 @@ template< class Send> void handle_request( - Server& server, + ::Server& server, http::request<Body, http::basic_fields<Allocator>>&& req, Send&& send) { - // Returns a bad request response - auto const bad_request = - [&req](beast::string_view why) - { - http::response<http::string_body> 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<http::string_body> 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<http::string_body> 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<http::string_body> 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<http::string_body> 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)); } //------------------------------------------------------------------------------ @@ -88,63 +88,16 @@ handle_request( http::request<Body, http::basic_fields<Allocator>>&& req, Send&& send) { - // Returns a bad request response - auto const bad_request = - [&req](beast::string_view why) - { - http::response<http::string_body> 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<http::string_body> 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<http::string_body> 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<http::string_body> 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<http::string_body> 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)); } //------------------------------------------------------------------------------ @@ -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 <string> #include <functional> -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 <filesystem> +#include <fstream> #include <iostream> +#include <string> 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<char*>(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<plugin_interface_setter_type>& 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 <functional> +#include <iostream> +#include <unordered_map> 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<http::string_body>& req) +std::unordered_map<std::string, std::function<std::string(request_type&, Server&)>> 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<http::string_body>& req, http::response<http::string_body>& 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::string(const std::string& key)>(std::bind(GetServerParam, _1, std::ref(server)))}; - auto GetRequestParamFunction {std::function<std::string(const std::string& key)>(std::bind(GetRequestParam, _1, req))}; - auto SetResponseHeaderFunction{std::function<void(const std::string& key, const std::string& value)>(SetResponseHeader)}; + auto GetRequestParamFunction {std::function<std::string(const std::string& key)>(std::bind(GetRequestParam, _1, std::ref(req), std::ref(server)))}; + auto SetResponseHeaderFunction{std::function<void(const std::string& key, const std::string& value)>(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 } @@ -10,31 +10,8 @@ namespace beast = boost::beast; // from <boost/beast.hpp> namespace http = beast::http; // from <boost/beast/http.hpp> -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<http::string_body> request_type; +typedef http::response<http::string_body> response_type; std::string extend_index_html(std::string path); -std::string generate_response(http::request<http::string_body>& req, http::response<http::string_body>& 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 @@ <host>lists.antcom.de</host> <host>antcom.de</host> <host>www.antcom.de</host> - <path requested="/" type="files"> + <path requested="/"> <plugin>static-files</plugin> <target>/home/ernie/homepage/test</target> </path> <!-- - <path requested="/webbox" type="plugin"> + <path requested="/webbox"> <plugin>webbox</plugin> <target>/var/lib/webbox</target> </path> @@ -29,7 +29,8 @@ <site> <name>marx</name> <host>marx.antcom.de</host> - <path requested="/" type="files"> + <path requested="/"> + <plugin>static-files</plugin> <target>/home/ernie/homepage/test1</target> </path> <certpath>/home/ernie/code/webserver/cert.pem</certpath> diff --git a/webserver.cpp b/webserver.cpp index 71829bb..51dc6a7 100644 --- a/webserver.cpp +++ b/webserver.cpp @@ -38,7 +38,7 @@ int main(int argc, char* argv[]) plugin_loader.load_plugins(); if (!plugin_loader.validate_config()) - throw std::runtime_error("Couldn't find all configured plugins."); + throw std::runtime_error("Config validation error."); return run_server(config, plugin_loader.get_plugins()); } catch (const std::exception& ex) { |