diff options
| author | Roland Reichwein <mail@reichwein.it> | 2020-04-03 12:39:00 +0200 | 
|---|---|---|
| committer | Roland Reichwein <mail@reichwein.it> | 2020-04-03 12:39:00 +0200 | 
| commit | e60bb89a6d1392c0007a1fbc03faf007faf76167 (patch) | |
| tree | 05820683872664bf644a41f8b34f18173f11b6db | |
| parent | f27702d3cf183f7fbaf88e584bae63a6514d9e65 (diff) | |
Added Debian systemd service file
| -rw-r--r-- | Makefile | 30 | ||||
| -rw-r--r-- | TODO | 2 | ||||
| -rw-r--r-- | debian/control | 2 | ||||
| -rw-r--r-- | debian/webserver.service | 13 | ||||
| -rw-r--r-- | http.cpp | 555 | ||||
| -rw-r--r-- | http.h | 4 | ||||
| -rw-r--r-- | http_debian10.cpp | 529 | ||||
| -rw-r--r-- | webserver.cpp | 547 | 
8 files changed, 1132 insertions, 550 deletions
@@ -1,7 +1,16 @@ +DISTROS=debian10 +VERSION=$(shell dpkg-parsechangelog --show-field Version)  PROJECTNAME=webserver  CXX=clang++-10 -#CXX=g++-9 + +ifeq ($(shell which $(CXX)),) +CXX=clang++ +endif + +ifeq ($(shell which $(CXX)),) +CXX=g++-9 +endif  ifeq ($(CXXFLAGS),)  #CXXFLAGS=-O2 -DNDEBUG @@ -47,7 +56,8 @@ LIBS+= \  endif  PROGSRC=\ -    http.cpp +    http.cpp \ +    http_debian10.cpp  TESTSRC=\      test-webserver.cpp \ @@ -87,6 +97,18 @@ install:  	cp webserver $(DESTDIR)/usr/bin  # misc --------------------------------------------------- +deb: +	# build binary deb package +	dpkg-buildpackage -us -uc -rfakeroot + +deb-src: +	dpkg-source -b . + +$(DISTROS): deb-src +	sudo pbuilder build --basetgz /var/cache/pbuilder/$@.tgz --buildresult result/$@ ../webserver_$(VERSION).dsc ; \ + +debs: $(DISTROS) +  clean:  	-rm -f test-$(PROJECTNAME) $(PROJECTNAME)  	-find . -name '*.o' -o -name '*.d' -o -name '*.gcno' -o -name '*.gcda' | xargs rm -f @@ -96,6 +118,8 @@ zip: clean  	zip -r ../$(PROJECTNAME).zip *  	ls -l ../$(PROJECTNAME).zip -.PHONY: clean all zip + + +.PHONY: clean all zip install deb deb-src debs all $(DISTROS)  -include $(wildcard $(SRC:.cpp=.d)) @@ -3,4 +3,4 @@ HTTP+HTTPS: https://www.boost.org/doc/libs/1_72_0/libs/beast/doc/html/beast/exam  Config: https://www.boost.org/doc/libs/1_72_0/doc/html/property_tree/tutorial.html  Certbot: https://certbot.eff.org/lets-encrypt/debianbuster-other -  +Configfile configurable diff --git a/debian/control b/debian/control index e5f62ff..3d5d617 100644 --- a/debian/control +++ b/debian/control @@ -2,7 +2,7 @@ Source: webserver  Section: web  Priority: extra  Maintainer: Roland Reichwein <mail@reichwein.it> -Build-Depends: debhelper (>= 9), libssl-dev, libboost-all-dev | libboost1.71-all-dev +Build-Depends: debhelper (>= 9), libssl-dev, libssl-dev, libboost-all-dev | libboost1.71-all-dev, clang  Standards-Version: 4.1.3  Homepage: http://www.reichwein.it/webserver/ diff --git a/debian/webserver.service b/debian/webserver.service new file mode 100644 index 0000000..fc30893 --- /dev/null +++ b/debian/webserver.service @@ -0,0 +1,13 @@ +[Unit] +Description=Webserver +After=network.target + +[Service] +Type=simple +# Restart=always +ExecStart=/usr/bin/webserver 127.0.0.1 80 /tmp 10 +#User=www-data +#Group=www-data + +[Install] +WantedBy=multi-user.target @@ -0,0 +1,555 @@ +#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> + +// 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, BOOST_BEAST_VERSION_STRING); +        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, BOOST_BEAST_VERSION_STRING); +        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, BOOST_BEAST_VERSION_STRING); +        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, BOOST_BEAST_VERSION_STRING); +        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, BOOST_BEAST_VERSION_STRING); +    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(); +    } +}; + +//------------------------------------------------------------------------------ + +int http_server(int argc, char* argv[]) +{ +    // 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])); + +    // 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(); + +    // 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; +} + +#endif @@ -0,0 +1,4 @@ +#pragma once + +int http_server(int argc, char* argv[]); + diff --git a/http_debian10.cpp b/http_debian10.cpp new file mode 100644 index 0000000..252cf56 --- /dev/null +++ b/http_debian10.cpp @@ -0,0 +1,529 @@ +#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/asio/bind_executor.hpp> +#include <boost/asio/ip/tcp.hpp> +#include <boost/asio/ssl/stream.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> + +using tcp = boost::asio::ip::tcp;       // from <boost/asio/ip/tcp.hpp> +namespace ssl = boost::asio::ssl;       // from <boost/asio/ssl.hpp> +namespace http = boost::beast::http;    // from <boost/beast/http.hpp> + +// Return a reasonable mime type based on the extension of a file. +boost::beast::string_view +mime_type(boost::beast::string_view path) +{ +    using boost::beast::iequals; +    auto const ext = [&path] +    { +        auto const pos = path.rfind("."); +        if(pos == boost::beast::string_view::npos) +            return boost::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( +    boost::beast::string_view base, +    boost::beast::string_view path) +{ +    if(base.empty()) +        return path.to_string(); +    std::string result = base.to_string(); +#if 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( +    boost::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](boost::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::content_type, "text/html"); +        res.keep_alive(req.keep_alive()); +        res.body() = why.to_string(); +        res.prepare_payload(); +        return res; +    }; + +    // Returns a not found response +    auto const not_found = +    [&req](boost::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::content_type, "text/html"); +        res.keep_alive(req.keep_alive()); +        res.body() = "The resource '" + target.to_string() + "' was not found."; +        res.prepare_payload(); +        return res; +    }; + +    // Returns a server error response +    auto const server_error = +    [&req](boost::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::content_type, "text/html"); +        res.keep_alive(req.keep_alive()); +        res.body() = "An error occurred: '" + what.to_string() + "'"; +        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("..") != boost::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 +    boost::beast::error_code ec; +    http::file_body::value_type body; +    body.open(path.c_str(), boost::beast::file_mode::scan, ec); + +    // Handle the case where the file doesn't exist +    if(ec == boost::system::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, BOOST_BEAST_VERSION_STRING); +        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, BOOST_BEAST_VERSION_STRING); +    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(boost::system::error_code ec, char const* what) +{ +    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, +                boost::asio::bind_executor( +                    self_.strand_, +                    std::bind( +                        &session::on_write, +                        self_.shared_from_this(), +                        std::placeholders::_1, +                        std::placeholders::_2, +                        sp->need_eof()))); +        } +    }; + +    tcp::socket socket_; +    ssl::stream<tcp::socket&> stream_; +    boost::asio::strand< +        boost::asio::io_context::executor_type> strand_; +    boost::beast::flat_buffer buffer_; +    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::string const& doc_root) +        : socket_(std::move(socket)) +        , stream_(socket_, ctx) +        , strand_(socket_.get_executor()) +        , doc_root_(doc_root) +        , lambda_(*this) +    { +    } + +    // Start the asynchronous operation +    void +    run() +    { +        // Perform the SSL handshake +        stream_.async_handshake( +            ssl::stream_base::server, +            boost::asio::bind_executor( +                strand_, +                std::bind( +                    &session::on_handshake, +                    shared_from_this(), +                    std::placeholders::_1))); +    } + +    void +    on_handshake(boost::system::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_ = {}; + +        // Read a request +        http::async_read(stream_, buffer_, req_, +            boost::asio::bind_executor( +                strand_, +                std::bind( +                    &session::on_read, +                    shared_from_this(), +                    std::placeholders::_1, +                    std::placeholders::_2))); +    } + +    void +    on_read( +        boost::system::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( +        boost::system::error_code ec, +        std::size_t bytes_transferred, +        bool close) +    { +        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() +    { +        // Perform the SSL shutdown +        stream_.async_shutdown( +            boost::asio::bind_executor( +                strand_, +                std::bind( +                    &session::on_shutdown, +                    shared_from_this(), +                    std::placeholders::_1))); +    } + +    void +    on_shutdown(boost::system::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> +{ +    ssl::context& ctx_; +    tcp::acceptor acceptor_; +    tcp::socket socket_; +    std::string const& doc_root_; + +public: +    listener( +        boost::asio::io_context& ioc, +        ssl::context& ctx, +        tcp::endpoint endpoint, +        std::string const& doc_root) +        : ctx_(ctx) +        , acceptor_(ioc) +        , socket_(ioc) +        , doc_root_(doc_root) +    { +        boost::system::error_code ec; + +        // Open the acceptor +        acceptor_.open(endpoint.protocol(), ec); +        if(ec) +        { +            fail(ec, "open"); +            return; +        } + +        // Allow address reuse +        acceptor_.set_option(boost::asio::socket_base::reuse_address(true)); +        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( +            boost::asio::socket_base::max_listen_connections, ec); +        if(ec) +        { +            fail(ec, "listen"); +            return; +        } +    } + +    // Start accepting incoming connections +    void +    run() +    { +        if(! acceptor_.is_open()) +            return; +        do_accept(); +    } + +    void +    do_accept() +    { +        acceptor_.async_accept( +            socket_, +            std::bind( +                &listener::on_accept, +                shared_from_this(), +                std::placeholders::_1)); +    } + +    void +    on_accept(boost::system::error_code ec) +    { +        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(); +    } +}; + +//------------------------------------------------------------------------------ + +int http_server(int argc, char* argv[]) +{ +    // 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 = boost::asio::ip::make_address(argv[1]); +    auto const port = static_cast<unsigned short>(std::atoi(argv[2])); +    std::string const doc_root = argv[3]; +    auto const threads = std::max<int>(1, std::atoi(argv[4])); + +    // The io_context is required for all I/O +    boost::asio::io_context ioc{threads}; + +    // The SSL context is required, and holds certificates +    ssl::context ctx{ssl::context::sslv23}; + +    // 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(); + +    // 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; +} + +#endif diff --git a/webserver.cpp b/webserver.cpp index d2d1bee..4b6a89c 100644 --- a/webserver.cpp +++ b/webserver.cpp @@ -1,549 +1,6 @@ -#include "server_certificate.h" - -#include <boost/beast/core.hpp> -#include <boost/beast/http.hpp> -#include <boost/beast/ssl.hpp> -#include <boost/beast/version.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> - -// 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, BOOST_BEAST_VERSION_STRING); -        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, BOOST_BEAST_VERSION_STRING); -        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, BOOST_BEAST_VERSION_STRING); -        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, BOOST_BEAST_VERSION_STRING); -        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, BOOST_BEAST_VERSION_STRING); -    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(); -    } -}; - -//------------------------------------------------------------------------------ +#include "http.h"  int main(int argc, char* argv[])  { -    // 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])); - -    // 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(); - -    // 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; + return http_server(argc, argv);  }  | 
