diff options
| author | Roland Reichwein <mail@reichwein.it> | 2020-04-04 16:32:10 +0200 | 
|---|---|---|
| committer | Roland Reichwein <mail@reichwein.it> | 2020-04-04 16:32:10 +0200 | 
| commit | 938fbe7a2f2f10a3abb530a9463e57fc20f40038 (patch) | |
| tree | 62ee0c285c672b10a42b0690a011ede7a0bf00b6 | |
| parent | 95d5acc8c7e60255b19e7084e374eb26cc5d0ba3 (diff) | |
HTTP and HTTPs
| -rw-r--r-- | Makefile | 9 | ||||
| -rw-r--r-- | TODO | 6 | ||||
| -rw-r--r-- | config.cpp | 30 | ||||
| -rw-r--r-- | config.h | 11 | ||||
| -rw-r--r-- | file.cpp | 46 | ||||
| -rw-r--r-- | file.h | 15 | ||||
| -rw-r--r-- | http.cpp | 141 | ||||
| -rw-r--r-- | http.h | 7 | ||||
| -rw-r--r-- | https.cpp | 558 | ||||
| -rw-r--r-- | https.h | 9 | ||||
| -rw-r--r-- | server.cpp | 10 | ||||
| -rw-r--r-- | server.h | 9 | ||||
| -rw-r--r-- | server_certificate.h | 69 | ||||
| -rw-r--r-- | webserver.conf | 13 | ||||
| -rw-r--r-- | webserver.cpp | 4 | 
15 files changed, 749 insertions, 188 deletions
@@ -9,7 +9,7 @@ CXX=clang++  endif  ifeq ($(shell which $(CXX)),) -#CXX=g++-9 +CXX=g++-9  endif  ifeq ($(CXXFLAGS),) @@ -19,7 +19,7 @@ endif  # -fprofile-instr-generate -fcoverage-mapping  # gcc:--coverage -CXXFLAGS+= -Wall -I. +CXXFLAGS+= -Wall -I. -DVERSION=\"$(VERSION)\"  CXXFLAGS+= -pthread  ifeq ($(CXX),clang++-10) @@ -58,9 +58,12 @@ endif  PROGSRC=\      config.cpp \ +    file.cpp \      http.cpp \ +    https.cpp \      http_debian10.cpp \ -    plugin.cpp +    plugin.cpp \ +    server.cpp  TESTSRC=\      test-webserver.cpp \ @@ -1,3 +1,7 @@ -Plugin: https://www.boost.org/doc/libs/1_72_0/doc/html/boost_dll/tutorial.html#boost_dll.tutorial.symbol_shadowing_problem__linux_  HTTP+HTTPS: https://www.boost.org/doc/libs/1_72_0/libs/beast/doc/html/beast/examples.html#beast.examples.servers  Certbot: https://certbot.eff.org/lets-encrypt/debianbuster-other +Webbox +Debian 10 +alternative hosts www, lists, ... + +Selective sites per Socket @@ -20,8 +20,11 @@ void Config::readConfigfile(std::string filename)   // mandatory   m_user = tree.get<std::string>("webserver.user"); +    m_group = tree.get<std::string>("webserver.group"); + m_threads = tree.get<int>("webserver.threads"); +   // optional entries   auto elements = tree.get_child_optional("webserver");   if (elements) { @@ -71,11 +74,15 @@ void Config::readConfigfile(std::string filename)         socket_struct.port = x.second.data();        } else if (x.first == "protocol"s) {         if (x.second.data() == "http"s) -        socket_struct.protocol = HTTP; +        socket_struct.protocol = SocketProtocol::HTTP;         else if (x.second.data() == "https"s) -        socket_struct.protocol = HTTPS; +        socket_struct.protocol = SocketProtocol::HTTPS;         else          throw std::runtime_error("Unknown protocol: "s + x.second.data()); +      } else if (x.first == "certpath"s) { +       socket_struct.cert_path = x.second.data(); +      } else if (x.first == "keypath"s) { +       socket_struct.key_path = x.second.data();        } else         throw std::runtime_error("Unknown element: "s + x.first);       } @@ -102,6 +109,11 @@ std::string Config::Group() const   return m_group;  } +int Config::Threads() const +{ + return m_threads; +} +  const std::vector<std::string>& Config::PluginDirectories() const  {   return m_plugin_directories; @@ -122,6 +134,8 @@ void Config::dump() const   std::cout << "=== Configuration ===========================" << std::endl;   std::cout << "User: " << m_user << std::endl;   std::cout << "Group: " << m_user << std::endl; +  + std::cout << "Threads: " << m_threads << std::endl;   std::cout << "Plugin Directories:";   for (const auto& dir: m_plugin_directories) @@ -131,17 +145,21 @@ void Config::dump() const   for (const auto& site: m_sites) {    std::cout << "Site: " << site.name << ": " << site.host << std::endl;    if (site.paths.size() == 0) -   std::cout << "    Warning: No paths configured." << std::endl; +   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 << " -> " << ((path.type == Files) ? "files" : "plugin") << std::endl;     for (const auto& param: path.params) { -    std::cout << "        " << param.first << ": " << param.second << std::endl; +    std::cout << "    " << param.first << ": " << param.second << std::endl;     }    }   }   for (const auto& socket: m_sockets) { -  std::cout << "Socket: " << socket.address << ":" << socket.port << " (" << (socket.protocol == HTTP ? "HTTP" : "HTTPS") << ")" << std::endl; +  std::cout << "Socket: " << socket.address << ":" << socket.port << " (" << (socket.protocol == SocketProtocol::HTTP ? "HTTP" : "HTTPS") << ")" << std::endl; +  if (socket.protocol == SocketProtocol::HTTPS) { +   std::cout << "  Key: " << socket.key_path.generic_string() << std::endl; +   std::cout << "  Cert: " << socket.cert_path.generic_string() << std::endl; +  }   }   std::cout << "=============================================" << std::endl;  } @@ -1,9 +1,12 @@  #pragma once +#include <filesystem>  #include <string>  #include <unordered_map>  #include <vector> +namespace fs = std::filesystem; +  enum PathType  {   Files, // serve files @@ -24,7 +27,7 @@ struct Site   std::vector<Path> paths;  }; -enum SocketProtocol +enum class SocketProtocol  {   HTTP,   HTTPS @@ -35,6 +38,9 @@ struct Socket   std::string address;   std::string port;   SocketProtocol protocol; + std::vector<std::string> serve_sites; // if empty, serve all configured sites // TODO: implement + fs::path cert_path; + fs::path key_path;  };  class Config @@ -45,6 +51,7 @@ class Config   std::string m_user;   std::string m_group; + int m_threads;   std::vector<std::string> m_plugin_directories;   std::vector<Site> m_sites;   std::vector<Socket> m_sockets; @@ -56,6 +63,8 @@ class Config    std::string User() const;    std::string Group() const; +  int Threads() const; +    const std::vector<std::string>& PluginDirectories() const;    const std::vector<Site>& Sites() const;    const std::vector<Socket>& Sockets() const; diff --git a/file.cpp b/file.cpp new file mode 100644 index 0000000..47ab8be --- /dev/null +++ b/file.cpp @@ -0,0 +1,46 @@ +#include "file.h" + +#include <fstream> + +namespace fs = std::filesystem; + +using namespace std::string_literals; + +std::string File::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"); + } +} + +void File::setFile(const fs::path& filename, const std::string& s) +{ + File::setFile(filename, s.data(), s.size()); +} + +void File::setFile(const fs::path& filename, const char* data, size_t size) +{ + std::ofstream file(filename.string(), std::ios::out | std::ios::binary); + if (file.is_open()) { +  file.write(data, size); + } else { +  throw std::runtime_error("Opening "s + filename.string() + " for writing"); + } +} + +void File::setFile(const fs::path& filename, const std::vector<uint8_t>& data) +{ + File::setFile(filename, reinterpret_cast<const char*>(data.data()), data.size()); +} + @@ -0,0 +1,15 @@ +#pragma once + +#include <cstdint> +#include <filesystem> +#include <string> +#include <vector> + +namespace File { + +std::string getFile(const std::filesystem::path& filename); +void setFile(const std::filesystem::path& filename, const std::string& s); +void setFile(const std::filesystem::path& filename, const char* data, size_t size); +void setFile(const std::filesystem::path& filename, const std::vector<uint8_t>& data); + +} @@ -1,13 +1,11 @@ -#include <boost/beast/version.hpp> - -#if BOOST_VERSION == 107100 +#include "http.h" -#include "server_certificate.h" +#include "server.h" +#include <boost/beast/version.hpp>  #include <boost/beast/core.hpp>  #include <boost/beast/http.hpp>  #include <boost/beast/version.hpp> -#include <boost/beast/ssl.hpp>  #include <boost/asio/dispatch.hpp>  #include <boost/asio/strand.hpp>  #include <boost/config.hpp> @@ -23,9 +21,10 @@  namespace beast = boost::beast;         // from <boost/beast.hpp>  namespace http = beast::http;           // from <boost/beast/http.hpp>  namespace net = boost::asio;            // from <boost/asio.hpp> -namespace ssl = boost::asio::ssl;       // from <boost/asio/ssl.hpp>  using tcp = boost::asio::ip::tcp;       // from <boost/asio/ip/tcp.hpp> +namespace { +  // Return a reasonable mime type based on the extension of a file.  beast::string_view  mime_type(beast::string_view path) @@ -107,7 +106,7 @@ handle_request(      [&req](beast::string_view why)      {          http::response<http::string_body> res{http::status::bad_request, req.version()}; -        res.set(http::field::server, BOOST_BEAST_VERSION_STRING); +        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); @@ -120,7 +119,7 @@ handle_request(      [&req](beast::string_view target)      {          http::response<http::string_body> res{http::status::not_found, req.version()}; -        res.set(http::field::server, BOOST_BEAST_VERSION_STRING); +        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."; @@ -133,7 +132,7 @@ handle_request(      [&req](beast::string_view what)      {          http::response<http::string_body> res{http::status::internal_server_error, req.version()}; -        res.set(http::field::server, BOOST_BEAST_VERSION_STRING); +        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) + "'"; @@ -177,7 +176,7 @@ handle_request(      if(req.method() == http::verb::head)      {          http::response<http::empty_body> res{http::status::ok, req.version()}; -        res.set(http::field::server, BOOST_BEAST_VERSION_STRING); +        res.set(http::field::server, VersionString);          res.set(http::field::content_type, mime_type(path));          res.content_length(size);          res.keep_alive(req.keep_alive()); @@ -189,7 +188,7 @@ handle_request(          std::piecewise_construct,          std::make_tuple(std::move(body)),          std::make_tuple(http::status::ok, req.version())}; -    res.set(http::field::server, BOOST_BEAST_VERSION_STRING); +    res.set(http::field::server, VersionString);      res.set(http::field::content_type, mime_type(path));      res.content_length(size);      res.keep_alive(req.keep_alive()); @@ -202,26 +201,6 @@ handle_request(  void  fail(beast::error_code ec, char const* what)  { -    // ssl::error::stream_truncated, also known as an SSL "short read", -    // indicates the peer closed the connection without performing the -    // required closing handshake (for example, Google does this to -    // improve performance). Generally this can be a security issue, -    // but if your communication protocol is self-terminated (as -    // it is with both HTTP and WebSocket) then you may simply -    // ignore the lack of close_notify. -    // -    // https://github.com/boostorg/beast/issues/38 -    // -    // https://security.stackexchange.com/questions/91435/how-to-handle-a-malicious-ssl-tls-shutdown -    // -    // When a short read would cut off the end of an HTTP message, -    // Beast returns the error beast::http::error::partial_message. -    // Therefore, if we see a short read here, it has occurred -    // after the message has been completed, so it is safe to ignore it. - -    if(ec == net::ssl::error::stream_truncated) -        return; -      std::cerr << what << ": " << ec.message() << "\n";  } @@ -265,7 +244,7 @@ class session : public std::enable_shared_from_this<session>          }      }; -    beast::ssl_stream<beast::tcp_stream> stream_; +    beast::tcp_stream stream_;      beast::flat_buffer buffer_;      std::shared_ptr<std::string const> doc_root_;      http::request<http::string_body> req_; @@ -273,13 +252,11 @@ class session : public std::enable_shared_from_this<session>      send_lambda lambda_;  public: -    // Take ownership of the socket -    explicit +    // Take ownership of the stream      session(          tcp::socket&& socket, -        ssl::context& ctx,          std::shared_ptr<std::string const> const& doc_root) -        : stream_(std::move(socket), ctx) +        : stream_(std::move(socket))          , doc_root_(doc_root)          , lambda_(*this)      { @@ -293,35 +270,10 @@ public:          // on the I/O objects in this session. Although not strictly necessary          // for single-threaded contexts, this example code is written to be          // thread-safe by default. -        net::dispatch( -            stream_.get_executor(), -            beast::bind_front_handler( -                &session::on_run, -                shared_from_this())); -    } - -    void -    on_run() -    { -        // Set the timeout. -        beast::get_lowest_layer(stream_).expires_after( -            std::chrono::seconds(30)); - -        // Perform the SSL handshake -        stream_.async_handshake( -            ssl::stream_base::server, -            beast::bind_front_handler( -                &session::on_handshake, -                shared_from_this())); -    } - -    void -    on_handshake(beast::error_code ec) -    { -        if(ec) -            return fail(ec, "handshake"); - -        do_read(); +        net::dispatch(stream_.get_executor(), +                      beast::bind_front_handler( +                          &session::do_read, +                          shared_from_this()));      }      void @@ -332,7 +284,7 @@ public:          req_ = {};          // Set the timeout. -        beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(30)); +        stream_.expires_after(std::chrono::seconds(30));          // Read a request          http::async_read(stream_, buffer_, req_, @@ -387,21 +339,9 @@ public:      void      do_close()      { -        // Set the timeout. -        beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(30)); - -        // Perform the SSL shutdown -        stream_.async_shutdown( -            beast::bind_front_handler( -                &session::on_shutdown, -                shared_from_this())); -    } - -    void -    on_shutdown(beast::error_code ec) -    { -        if(ec) -            return fail(ec, "shutdown"); +        // Send a TCP shutdown +        beast::error_code ec; +        stream_.socket().shutdown(tcp::socket::shutdown_send, ec);          // At this point the connection is closed gracefully      } @@ -413,19 +353,16 @@ public:  class listener : public std::enable_shared_from_this<listener>  {      net::io_context& ioc_; -    ssl::context& ctx_;      tcp::acceptor acceptor_;      std::shared_ptr<std::string const> doc_root_;  public:      listener(          net::io_context& ioc, -        ssl::context& ctx,          tcp::endpoint endpoint,          std::shared_ptr<std::string const> const& doc_root)          : ioc_(ioc) -        , ctx_(ctx) -        , acceptor_(ioc) +        , acceptor_(net::make_strand(ioc))          , doc_root_(doc_root)      {          beast::error_code ec; @@ -495,7 +432,6 @@ private:              // Create the session and run it              std::make_shared<session>(                  std::move(socket), -                ctx_,                  doc_root_)->run();          } @@ -504,37 +440,26 @@ private:      }  }; +} // anonymous namespace +  //------------------------------------------------------------------------------ -int http_server(int argc, char* argv[]) +namespace HTTP { + +int server(Config& config)  { -    // Check command line arguments. -    if (argc != 5) -    { -        std::cerr << -            "Usage: http-server-async-ssl <address> <port> <doc_root> <threads>\n" << -            "Example:\n" << -            "    http-server-async-ssl 0.0.0.0 8080 . 1\n"; -        return EXIT_FAILURE; -    } -    auto const address = net::ip::make_address(argv[1]); -    auto const port = static_cast<unsigned short>(std::atoi(argv[2])); -    auto const doc_root = std::make_shared<std::string>(argv[3]); -    auto const threads = std::max<int>(1, std::atoi(argv[4])); +    // TODO: Config +    auto const address = net::ip::make_address(config.Sockets()[0].address); +    auto const port = static_cast<unsigned short>(std::atoi(config.Sockets()[0].port.data())); +    auto const doc_root = std::make_shared<std::string>(config.Sites()[0].paths[0].params.at("target")); +    auto const threads = std::max<int>(1, config.Threads());      // The io_context is required for all I/O      net::io_context ioc{threads}; -    // The SSL context is required, and holds certificates -    ssl::context ctx{ssl::context::tlsv12}; - -    // This holds the self-signed certificate used by the server -    load_server_certificate(ctx); -      // Create and launch a listening port      std::make_shared<listener>(          ioc, -        ctx,          tcp::endpoint{address, port},          doc_root)->run(); @@ -552,4 +477,4 @@ int http_server(int argc, char* argv[])      return EXIT_SUCCESS;  } -#endif +} // namespace HTTP @@ -1,4 +1,9 @@  #pragma once -int http_server(int argc, char* argv[]); +#include "config.h" +namespace HTTP { + +int server(Config& config); + +} // namespace HTTP diff --git a/https.cpp b/https.cpp new file mode 100644 index 0000000..0c3b97b --- /dev/null +++ b/https.cpp @@ -0,0 +1,558 @@ +#include "https.h" + +#include "server.h" + +#include <boost/beast/version.hpp> + +#if BOOST_VERSION == 107100 + +#include "server_certificate.h" + +#include <boost/beast/core.hpp> +#include <boost/beast/http.hpp> +#include <boost/beast/version.hpp> +#include <boost/beast/ssl.hpp> +#include <boost/asio/dispatch.hpp> +#include <boost/asio/strand.hpp> +#include <boost/config.hpp> +#include <algorithm> +#include <cstdlib> +#include <functional> +#include <iostream> +#include <memory> +#include <string> +#include <thread> +#include <vector> + +namespace beast = boost::beast;         // from <boost/beast.hpp> +namespace http = beast::http;           // from <boost/beast/http.hpp> +namespace net = boost::asio;            // from <boost/asio.hpp> +namespace ssl = boost::asio::ssl;       // from <boost/asio/ssl.hpp> +using tcp = boost::asio::ip::tcp;       // from <boost/asio/ip/tcp.hpp> + +namespace { + +// Return a reasonable mime type based on the extension of a file. +beast::string_view +mime_type(beast::string_view path) +{ +    using beast::iequals; +    auto const ext = [&path] +    { +        auto const pos = path.rfind("."); +        if(pos == beast::string_view::npos) +            return beast::string_view{}; +        return path.substr(pos); +    }(); +    if(iequals(ext, ".htm"))  return "text/html"; +    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"; +} + +// 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; +} + +// This function produces an HTTP response for the given +// request. The type of the response object depends on the +// contents of the request, so the interface requires the +// caller to pass a generic lambda for receiving the response. +template< +    class Body, class Allocator, +    class Send> +void +handle_request( +    beast::string_view doc_root, +    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; +    }; + +    // Make sure we can handle the method +    if( req.method() != http::verb::get && +        req.method() != http::verb::head) +        return send(bad_request("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) +        return send(bad_request("Illegal request-target")); + +    // Build the path to the requested file +    std::string path = path_cat(doc_root, req.target()); +    if(req.target().back() == '/') +        path.append("index.html"); + +    // Attempt to open the file +    beast::error_code ec; +    http::file_body::value_type body; +    body.open(path.c_str(), beast::file_mode::scan, ec); + +    // Handle the case where the file doesn't exist +    if(ec == beast::errc::no_such_file_or_directory) +        return send(not_found(req.target())); + +    // Handle an unknown error +    if(ec) +        return send(server_error(ec.message())); + +    // Cache the size since we need it after the move +    auto const size = body.size(); + +    // Respond to HEAD request +    if(req.method() == http::verb::head) +    { +        http::response<http::empty_body> res{http::status::ok, req.version()}; +        res.set(http::field::server, VersionString); +        res.set(http::field::content_type, mime_type(path)); +        res.content_length(size); +        res.keep_alive(req.keep_alive()); +        return send(std::move(res)); +    } + +    // Respond to GET request +    http::response<http::file_body> res{ +        std::piecewise_construct, +        std::make_tuple(std::move(body)), +        std::make_tuple(http::status::ok, req.version())}; +    res.set(http::field::server, VersionString); +    res.set(http::field::content_type, mime_type(path)); +    res.content_length(size); +    res.keep_alive(req.keep_alive()); +    return send(std::move(res)); +} + +//------------------------------------------------------------------------------ + +// Report a failure +void +fail(beast::error_code ec, char const* what) +{ +    // ssl::error::stream_truncated, also known as an SSL "short read", +    // indicates the peer closed the connection without performing the +    // required closing handshake (for example, Google does this to +    // improve performance). Generally this can be a security issue, +    // but if your communication protocol is self-terminated (as +    // it is with both HTTP and WebSocket) then you may simply +    // ignore the lack of close_notify. +    // +    // https://github.com/boostorg/beast/issues/38 +    // +    // https://security.stackexchange.com/questions/91435/how-to-handle-a-malicious-ssl-tls-shutdown +    // +    // When a short read would cut off the end of an HTTP message, +    // Beast returns the error beast::http::error::partial_message. +    // Therefore, if we see a short read here, it has occurred +    // after the message has been completed, so it is safe to ignore it. + +    if(ec == net::ssl::error::stream_truncated) +        return; + +    std::cerr << what << ": " << ec.message() << "\n"; +} + +// Handles an HTTP server connection +class session : public std::enable_shared_from_this<session> +{ +    // This is the C++11 equivalent of a generic lambda. +    // The function object is used to send an HTTP message. +    struct send_lambda +    { +        session& self_; + +        explicit +        send_lambda(session& self) +            : self_(self) +        { +        } + +        template<bool isRequest, class Body, class Fields> +        void +        operator()(http::message<isRequest, Body, Fields>&& msg) const +        { +            // The lifetime of the message has to extend +            // for the duration of the async operation so +            // we use a shared_ptr to manage it. +            auto sp = std::make_shared< +                http::message<isRequest, Body, Fields>>(std::move(msg)); + +            // Store a type-erased version of the shared +            // pointer in the class to keep it alive. +            self_.res_ = sp; + +            // Write the response +            http::async_write( +                self_.stream_, +                *sp, +                beast::bind_front_handler( +                    &session::on_write, +                    self_.shared_from_this(), +                    sp->need_eof())); +        } +    }; + +    beast::ssl_stream<beast::tcp_stream> stream_; +    beast::flat_buffer buffer_; +    std::shared_ptr<std::string const> doc_root_; +    http::request<http::string_body> req_; +    std::shared_ptr<void> res_; +    send_lambda lambda_; + +public: +    // Take ownership of the socket +    explicit +    session( +        tcp::socket&& socket, +        ssl::context& ctx, +        std::shared_ptr<std::string const> const& doc_root) +        : stream_(std::move(socket), ctx) +        , doc_root_(doc_root) +        , lambda_(*this) +    { +    } + +    // Start the asynchronous operation +    void +    run() +    { +        // We need to be executing within a strand to perform async operations +        // on the I/O objects in this session. Although not strictly necessary +        // for single-threaded contexts, this example code is written to be +        // thread-safe by default. +        net::dispatch( +            stream_.get_executor(), +            beast::bind_front_handler( +                &session::on_run, +                shared_from_this())); +    } + +    void +    on_run() +    { +        // Set the timeout. +        beast::get_lowest_layer(stream_).expires_after( +            std::chrono::seconds(30)); + +        // Perform the SSL handshake +        stream_.async_handshake( +            ssl::stream_base::server, +            beast::bind_front_handler( +                &session::on_handshake, +                shared_from_this())); +    } + +    void +    on_handshake(beast::error_code ec) +    { +        if(ec) +            return fail(ec, "handshake"); + +        do_read(); +    } + +    void +    do_read() +    { +        // Make the request empty before reading, +        // otherwise the operation behavior is undefined. +        req_ = {}; + +        // Set the timeout. +        beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(30)); + +        // Read a request +        http::async_read(stream_, buffer_, req_, +            beast::bind_front_handler( +                &session::on_read, +                shared_from_this())); +    } + +    void +    on_read( +        beast::error_code ec, +        std::size_t bytes_transferred) +    { +        boost::ignore_unused(bytes_transferred); + +        // This means they closed the connection +        if(ec == http::error::end_of_stream) +            return do_close(); + +        if(ec) +            return fail(ec, "read"); + +        // Send the response +        handle_request(*doc_root_, std::move(req_), lambda_); +    } + +    void +    on_write( +        bool close, +        beast::error_code ec, +        std::size_t bytes_transferred) +    { +        boost::ignore_unused(bytes_transferred); + +        if(ec) +            return fail(ec, "write"); + +        if(close) +        { +            // This means we should close the connection, usually because +            // the response indicated the "Connection: close" semantic. +            return do_close(); +        } + +        // We're done with the response so delete it +        res_ = nullptr; + +        // Read another request +        do_read(); +    } + +    void +    do_close() +    { +        // Set the timeout. +        beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(30)); + +        // Perform the SSL shutdown +        stream_.async_shutdown( +            beast::bind_front_handler( +                &session::on_shutdown, +                shared_from_this())); +    } + +    void +    on_shutdown(beast::error_code ec) +    { +        if(ec) +            return fail(ec, "shutdown"); + +        // At this point the connection is closed gracefully +    } +}; + +//------------------------------------------------------------------------------ + +// Accepts incoming connections and launches the sessions +class listener : public std::enable_shared_from_this<listener> +{ +    net::io_context& ioc_; +    ssl::context& ctx_; +    tcp::acceptor acceptor_; +    std::shared_ptr<std::string const> doc_root_; + +public: +    listener( +        net::io_context& ioc, +        ssl::context& ctx, +        tcp::endpoint endpoint, +        std::shared_ptr<std::string const> const& doc_root) +        : ioc_(ioc) +        , ctx_(ctx) +        , acceptor_(ioc) +        , doc_root_(doc_root) +    { +        beast::error_code ec; + +        // Open the acceptor +        acceptor_.open(endpoint.protocol(), ec); +        if(ec) +        { +            fail(ec, "open"); +            return; +        } + +        // Allow address reuse +        acceptor_.set_option(net::socket_base::reuse_address(true), ec); +        if(ec) +        { +            fail(ec, "set_option"); +            return; +        } + +        // Bind to the server address +        acceptor_.bind(endpoint, ec); +        if(ec) +        { +            fail(ec, "bind"); +            return; +        } + +        // Start listening for connections +        acceptor_.listen( +            net::socket_base::max_listen_connections, ec); +        if(ec) +        { +            fail(ec, "listen"); +            return; +        } +    } + +    // Start accepting incoming connections +    void +    run() +    { +        do_accept(); +    } + +private: +    void +    do_accept() +    { +        // The new connection gets its own strand +        acceptor_.async_accept( +            net::make_strand(ioc_), +            beast::bind_front_handler( +                &listener::on_accept, +                shared_from_this())); +    } + +    void +    on_accept(beast::error_code ec, tcp::socket socket) +    { +        if(ec) +        { +            fail(ec, "accept"); +        } +        else +        { +            // Create the session and run it +            std::make_shared<session>( +                std::move(socket), +                ctx_, +                doc_root_)->run(); +        } + +        // Accept another connection +        do_accept(); +    } +}; + +} // anonymous namespace +//------------------------------------------------------------------------------ + +namespace HTTPS { + +int server(Config& config) +{ +    // TODO: Config +    auto const address = net::ip::make_address(config.Sockets()[0].address); +    auto const port = static_cast<unsigned short>(std::atoi(config.Sockets()[0].port.data())); +    auto const doc_root = std::make_shared<std::string>(config.Sites()[0].paths[0].params.at("target")); +    auto const threads = std::max<int>(1, config.Threads()); + +    // The io_context is required for all I/O +    net::io_context ioc{threads}; + +    // The SSL context is required, and holds certificates +    ssl::context ctx{ssl::context::tlsv13}; + +    // This holds the self-signed certificate used by the server +    load_server_certificate(ctx, config.Sockets()[0].cert_path, config.Sockets()[0].key_path); // TODO: config + +    // Create and launch a listening port +    std::make_shared<listener>( +        ioc, +        ctx, +        tcp::endpoint{address, port}, +        doc_root)->run(); + +    // Run the I/O service on the requested number of threads +    std::vector<std::thread> v; +    v.reserve(threads - 1); +    for(auto i = threads - 1; i > 0; --i) +        v.emplace_back( +        [&ioc] +        { +            ioc.run(); +        }); +    ioc.run(); + +    return EXIT_SUCCESS; +} + +} // namespace HTTPS +#endif + @@ -0,0 +1,9 @@ +#pragma once + +#include "config.h" + +namespace HTTPS { + +int server(Config& config); + +} diff --git a/server.cpp b/server.cpp new file mode 100644 index 0000000..2ad9bcd --- /dev/null +++ b/server.cpp @@ -0,0 +1,10 @@ +#include "server.h" + +#include "http.h" +#include "https.h" + +int server(Config& config) +{ + //return HTTP::server(config); + return HTTPS::server(config); +} diff --git a/server.h b/server.h new file mode 100644 index 0000000..5ad21ff --- /dev/null +++ b/server.h @@ -0,0 +1,9 @@ +#pragma once + +#include "config.h" + +using namespace std::string_literals; + +static const std::string VersionString{ "Webserver "s + std::string{VERSION} }; + +int server(Config& config); diff --git a/server_certificate.h b/server_certificate.h index a20110e..1dc12a4 100644 --- a/server_certificate.h +++ b/server_certificate.h @@ -1,12 +1,3 @@ -// -// Copyright (c) 2016-2019 Vinnie Falco (vinnie dot falco at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/boostorg/beast -// -  #ifndef BOOST_BEAST_EXAMPLE_COMMON_SERVER_CERTIFICATE_HPP  #define BOOST_BEAST_EXAMPLE_COMMON_SERVER_CERTIFICATE_HPP @@ -15,6 +6,9 @@  #include <cstddef>  #include <memory> +#include "config.h" +#include "file.h" +  /*  Load a signed certificate into the ssl context, and configure      the context for use with a server. @@ -26,7 +20,7 @@  */  inline  void -load_server_certificate(boost::asio::ssl::context& ctx) +load_server_certificate(boost::asio::ssl::context& ctx, fs::path cert_path, fs::path key_path)  {      /*          The certificate was generated from CMD.EXE on Windows 10 using: @@ -35,59 +29,8 @@ load_server_certificate(boost::asio::ssl::context& ctx)          winpty openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 10000 -out cert.pem -subj "//C=US\ST=CA\L=Los Angeles\O=Beast\CN=www.example.com"      */ -    std::string const cert = -        "-----BEGIN CERTIFICATE-----\n" -        "MIIDaDCCAlCgAwIBAgIJAO8vBu8i8exWMA0GCSqGSIb3DQEBCwUAMEkxCzAJBgNV\n" -        "BAYTAlVTMQswCQYDVQQIDAJDQTEtMCsGA1UEBwwkTG9zIEFuZ2VsZXNPPUJlYXN0\n" -        "Q049d3d3LmV4YW1wbGUuY29tMB4XDTE3MDUwMzE4MzkxMloXDTQ0MDkxODE4Mzkx\n" -        "MlowSTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMS0wKwYDVQQHDCRMb3MgQW5n\n" -        "ZWxlc089QmVhc3RDTj13d3cuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUA\n" -        "A4IBDwAwggEKAoIBAQDJ7BRKFO8fqmsEXw8v9YOVXyrQVsVbjSSGEs4Vzs4cJgcF\n" -        "xqGitbnLIrOgiJpRAPLy5MNcAXE1strVGfdEf7xMYSZ/4wOrxUyVw/Ltgsft8m7b\n" -        "Fu8TsCzO6XrxpnVtWk506YZ7ToTa5UjHfBi2+pWTxbpN12UhiZNUcrRsqTFW+6fO\n" -        "9d7xm5wlaZG8cMdg0cO1bhkz45JSl3wWKIES7t3EfKePZbNlQ5hPy7Pd5JTmdGBp\n" -        "yY8anC8u4LPbmgW0/U31PH0rRVfGcBbZsAoQw5Tc5dnb6N2GEIbq3ehSfdDHGnrv\n" -        "enu2tOK9Qx6GEzXh3sekZkxcgh+NlIxCNxu//Dk9AgMBAAGjUzBRMB0GA1UdDgQW\n" -        "BBTZh0N9Ne1OD7GBGJYz4PNESHuXezAfBgNVHSMEGDAWgBTZh0N9Ne1OD7GBGJYz\n" -        "4PNESHuXezAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCmTJVT\n" -        "LH5Cru1vXtzb3N9dyolcVH82xFVwPewArchgq+CEkajOU9bnzCqvhM4CryBb4cUs\n" -        "gqXWp85hAh55uBOqXb2yyESEleMCJEiVTwm/m26FdONvEGptsiCmF5Gxi0YRtn8N\n" -        "V+KhrQaAyLrLdPYI7TrwAOisq2I1cD0mt+xgwuv/654Rl3IhOMx+fKWKJ9qLAiaE\n" -        "fQyshjlPP9mYVxWOxqctUdQ8UnsUKKGEUcVrA08i1OAnVKlPFjKBvk+r7jpsTPcr\n" -        "9pWXTO9JrYMML7d+XRSZA1n3856OqZDX4403+9FnXCvfcLZLLKTBvwwFgEFGpzjK\n" -        "UEVbkhd5qstF6qWK\n" -        "-----END CERTIFICATE-----\n"; - -    std::string const key = -        "-----BEGIN PRIVATE KEY-----\n" -        "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDJ7BRKFO8fqmsE\n" -        "Xw8v9YOVXyrQVsVbjSSGEs4Vzs4cJgcFxqGitbnLIrOgiJpRAPLy5MNcAXE1strV\n" -        "GfdEf7xMYSZ/4wOrxUyVw/Ltgsft8m7bFu8TsCzO6XrxpnVtWk506YZ7ToTa5UjH\n" -        "fBi2+pWTxbpN12UhiZNUcrRsqTFW+6fO9d7xm5wlaZG8cMdg0cO1bhkz45JSl3wW\n" -        "KIES7t3EfKePZbNlQ5hPy7Pd5JTmdGBpyY8anC8u4LPbmgW0/U31PH0rRVfGcBbZ\n" -        "sAoQw5Tc5dnb6N2GEIbq3ehSfdDHGnrvenu2tOK9Qx6GEzXh3sekZkxcgh+NlIxC\n" -        "Nxu//Dk9AgMBAAECggEBAK1gV8uETg4SdfE67f9v/5uyK0DYQH1ro4C7hNiUycTB\n" -        "oiYDd6YOA4m4MiQVJuuGtRR5+IR3eI1zFRMFSJs4UqYChNwqQGys7CVsKpplQOW+\n" -        "1BCqkH2HN/Ix5662Dv3mHJemLCKUON77IJKoq0/xuZ04mc9csykox6grFWB3pjXY\n" -        "OEn9U8pt5KNldWfpfAZ7xu9WfyvthGXlhfwKEetOuHfAQv7FF6s25UIEU6Hmnwp9\n" -        "VmYp2twfMGdztz/gfFjKOGxf92RG+FMSkyAPq/vhyB7oQWxa+vdBn6BSdsfn27Qs\n" -        "bTvXrGe4FYcbuw4WkAKTljZX7TUegkXiwFoSps0jegECgYEA7o5AcRTZVUmmSs8W\n" -        "PUHn89UEuDAMFVk7grG1bg8exLQSpugCykcqXt1WNrqB7x6nB+dbVANWNhSmhgCg\n" -        "VrV941vbx8ketqZ9YInSbGPWIU/tss3r8Yx2Ct3mQpvpGC6iGHzEc/NHJP8Efvh/\n" -        "CcUWmLjLGJYYeP5oNu5cncC3fXUCgYEA2LANATm0A6sFVGe3sSLO9un1brA4zlZE\n" -        "Hjd3KOZnMPt73B426qUOcw5B2wIS8GJsUES0P94pKg83oyzmoUV9vJpJLjHA4qmL\n" -        "CDAd6CjAmE5ea4dFdZwDDS8F9FntJMdPQJA9vq+JaeS+k7ds3+7oiNe+RUIHR1Sz\n" -        "VEAKh3Xw66kCgYB7KO/2Mchesu5qku2tZJhHF4QfP5cNcos511uO3bmJ3ln+16uR\n" -        "GRqz7Vu0V6f7dvzPJM/O2QYqV5D9f9dHzN2YgvU9+QSlUeFK9PyxPv3vJt/WP1//\n" -        "zf+nbpaRbwLxnCnNsKSQJFpnrE166/pSZfFbmZQpNlyeIuJU8czZGQTifQKBgHXe\n" -        "/pQGEZhVNab+bHwdFTxXdDzr+1qyrodJYLaM7uFES9InVXQ6qSuJO+WosSi2QXlA\n" -        "hlSfwwCwGnHXAPYFWSp5Owm34tbpp0mi8wHQ+UNgjhgsE2qwnTBUvgZ3zHpPORtD\n" -        "23KZBkTmO40bIEyIJ1IZGdWO32q79nkEBTY+v/lRAoGBAI1rbouFYPBrTYQ9kcjt\n" -        "1yfu4JF5MvO9JrHQ9tOwkqDmNCWx9xWXbgydsn/eFtuUMULWsG3lNjfst/Esb8ch\n" -        "k5cZd6pdJZa4/vhEwrYYSuEjMCnRb0lUsm7TsHxQrUd6Fi/mUuFU/haC0o0chLq7\n" -        "pVOUFq5mW8p0zbtfHbjkgxyF\n" -        "-----END PRIVATE KEY-----\n"; - +    std::string const cert = File::getFile(cert_path); +    std::string const key = File::getFile(key_path);      std::string const dh =          "-----BEGIN DH PARAMETERS-----\n"          "MIIBCAKCAQEArzQc5mpm0Fs8yahDeySj31JZlwEphUdZ9StM2D8+Fo7TMduGtSi+\n" diff --git a/webserver.conf b/webserver.conf index 91ea90b..2036f67 100644 --- a/webserver.conf +++ b/webserver.conf @@ -1,6 +1,7 @@  <webserver>   <user>www-data</user>   <group>www-data</group> + <threads>10</threads>   <!--   <plugin-directory><a c="d">b<e>f</e></a>/usr/lib/webserver/plugins</plugin-directory>   <plugin-directory>/usr/local/lib/webserver/plugins</plugin-directory> @@ -9,14 +10,16 @@   <sites>    <site>     <name>antcom.de</name> -   <host>antcom.de</host> +   <host>lists.antcom.de</host>     <path requested="/" type="files"> -    <target>/var/www/antcom.de</target> +    <target>/home/ernie/homepage/test</target>     </path> +   <!--     <path requested="/webbox" type="plugin">      <plugin>webbox</plugin>      <target>/var/lib/webbox</target>     </path> +   -->    </site>    <!--    <site> @@ -31,18 +34,22 @@   <sockets>    <socket>     <address>127.0.0.1</address> -   <port>80</port> +   <port>8080</port>     <protocol>http</protocol> +   <certpath>/home/ernie/code/webserver/fullchain.pem</certpath> +   <keypath>/home/ernie/code/webserver/privkey.pem</keypath>     <!--     <site>antcom.de</site>     <site>reichwein.it</site>     -->    </socket> +  <!--    <socket>     <address>127.0.0.1</address>     <port>443</port>     <protocol>https</protocol>    </socket> +  -->   </sockets>  </webserver> diff --git a/webserver.cpp b/webserver.cpp index 3f7a2a3..dd06021 100644 --- a/webserver.cpp +++ b/webserver.cpp @@ -1,5 +1,5 @@  #include "config.h" -#include "http.h" +#include "server.h"  #include "plugin.h"  #include <exception> @@ -40,7 +40,7 @@ int main(int argc, char* argv[])    if (!plugin_loader.validate_config())     throw std::runtime_error("Couldn't find all configured plugins."); -  return http_server(argc, argv); +  return server(config);   } catch (const std::exception& ex) {    std::cout << "Error: " << ex.what() << std::endl;    return 1;  | 
