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) {  | 
