天天看點

C++ https server based on boost asio and beast

目前在做的項目需要一個C++版本的https server,隻能求助于boost庫。幸運的是确實存在。并且提供了協程版本,本着學習的精神拿來改造一下,就獲得了如下成果。

AsyncHttpServerV2.hpp

//
// Created by chuanqin on 7/5/21.
//

#ifndef CBRS_UT_TOOL_ASYNCHTTPSERVERV2_HPP_
#define CBRS_UT_TOOL_ASYNCHTTPSERVERV2_HPP_

#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <memory>
#include <string>
#include <thread>
#include <vector>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/ssl/stream.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <boost/config.hpp>

#include "common/Logger.hpp"

namespace beast = boost::beast;
namespace http = beast::http;
namespace net = boost::asio;
namespace ssl = boost::asio::ssl;
using tcp = boost::asio::ip::tcp;

namespace cbrs
{
    namespace ut
    {
        namespace tool
        {
            class AsyncHttpServerV2 : public std::enable_shared_from_this<AsyncHttpServerV2>
            {
            public:
                AsyncHttpServerV2(unsigned short port);
                ~AsyncHttpServerV2() {}
                void start();
                void stop();

            private:
                void load_server_certificate();
                void do_listen(
                    boost::asio::yield_context yield);
                void do_session(
                    tcp::socket &socket,
                    boost::asio::yield_context yield);
                void fail(boost::system::error_code ec, char const *what);

                template <class Body, class Allocator, class Send>
                void handle_request(
                    http::request<Body, http::basic_fields<Allocator>> &&req,
                    Send &&send);
                std::string path_cat(boost::beast::string_view base, boost::beast::string_view path);
                boost::beast::string_view mime_type(boost::beast::string_view path);

                net::ip::address address;
                unsigned short port;
                std::shared_ptr<std::string> doc_root;
                unsigned short threads;
                net::io_context ioc;
                ssl::context ctx;
                std::string tls12CipherSuite;
                logging::Logger logger_;
                std::string certChain;
                std::string privateKey;
                std::vector<std::thread> threadList;
            };

            template <class Stream>
            struct send_lambda
            {
                Stream &stream_;
                bool &close_;
                boost::system::error_code &ec_;
                boost::asio::yield_context yield_;

                explicit send_lambda(
                    Stream &stream,
                    bool &close,
                    boost::system::error_code &ec,
                    boost::asio::yield_context yield)
                    : stream_(stream), close_(close), ec_(ec), yield_(yield)
                {
                }

                template <bool isRequest, class Body, class Fields>
                void operator()(http::message<isRequest, Body, Fields> &&msg) const
                {
                    // Determine if we should close the connection after
                    close_ = msg.need_eof();

                    // We need the serializer here because the serializer requires
                    // a non-const file_body, and the message oriented version of
                    // http::write only works with const messages.
                    http::serializer<isRequest, Body, Fields> sr{msg};
                    http::async_write(stream_, sr, yield_[ec_]);
                }
            };

        } // namespace tool

    } // namespace ut
} // namespace cbrs

#endif // CBRS_UT_TOOL_ASYNCHTTPSERVERV2_HPP_

           

AsyncHttpServerV2.cpp

#include "AsyncHttpServerV2.hpp"

#include "CertificateInfo.hpp"
#include "common/Logger.hpp"

#include <boost/assert.hpp>

namespace boost
{
    void assertion_failed_msg(
        char const *expr, char const *msg, char const *function, char const *file, long line)
    {
    }
} // namespace boost

namespace cbrs
{
    namespace ut
    {
        namespace tool
        {
            AsyncHttpServerV2::AsyncHttpServerV2(unsigned short port)
                : address(net::ip::make_address("127.0.0.1")), port(port), doc_root(std::make_shared<std::string>(".")), threads(1), ioc{threads}, ctx{ssl::context::tlsv12_server}, tls12CipherSuite("AES128-GCM-SHA256:AES256-GCM-SHA384"), logger_("tool::AsyncHttpServerV2")
            {
                certChain = "please add the certs chain here";

                privateKey = "add the private key here";
                logger_ << info << "AsyncHttpServerV2 construct";
                load_server_certificate();
                threadList.reserve(threads);
                logger_ << info << "ssl::context loaded";
            }

            void AsyncHttpServerV2::load_server_certificate()
            {
                ctx.set_options(
                    boost::asio::ssl::context::default_workarounds | boost::asio::ssl::context::no_sslv2 | boost::asio::ssl::context::single_dh_use);
                SSL_CTX_set_cipher_list(ctx.native_handle(), tls12CipherSuite.c_str());

                boost::system::error_code ecCert;
                ctx.use_certificate_chain(boost::asio::buffer(certChain.data(), certChain.size()), ecCert);

                if (ecCert)
                    return fail(ecCert, "cert chain");
                boost::system::error_code ecKey;
                ctx.use_private_key(
                    boost::asio::buffer(privateKey.data(), privateKey.size()),
                    boost::asio::ssl::context::pem,
                    ecKey);
                if (ecKey)
                    return fail(ecKey, "private key");
                ctx.set_verify_mode(ssl::verify_none);
            }

            void AsyncHttpServerV2::start()
            {
                boost::asio::spawn(
                    ioc,
                    std::bind(
                        &AsyncHttpServerV2::do_listen,
                        shared_from_this(),
                        std::placeholders::_1));

                // Run the I/O service on the requested number of threads
                for (auto i = threads; i > 0; --i)
                    threadList.emplace_back([this]
                                            { ioc.run(); });
                logger_ << info << "ready to provide the service";
            }

            void AsyncHttpServerV2::stop()
            {
                logger_ << info << "stopping the https server";
                ioc.stop();
                for (auto &item : threadList)
                {
                    item.join();
                }
                logger_ << info << "stopped the https server";
            }

            void AsyncHttpServerV2::do_listen(
                boost::asio::yield_context yield)
            {
                boost::system::error_code ec;

                // Open the acceptor
                tcp::acceptor acceptor(ioc);
                auto endpoint = tcp::endpoint{address, port};
                acceptor.open(endpoint.protocol(), ec);
                if (ec)
                    return fail(ec, "open");

                // Allow address reuse
                acceptor.set_option(boost::asio::socket_base::reuse_address(true), ec);
                if (ec)
                    return fail(ec, "set_option");

                // Bind to the server address
                acceptor.bind(endpoint, ec);
                if (ec)
                    return fail(ec, "bind");

                // Start listening for connections
                acceptor.listen(boost::asio::socket_base::max_listen_connections, ec);
                if (ec)
                    return fail(ec, "listen");

                for (;;)
                {
                    tcp::socket socket(ioc);
                    acceptor.async_accept(socket, yield[ec]);
                    if (ec)
                        fail(ec, "accept");
                    else
                    {
                        logger_ << info << "accept success";
                        boost::asio::spawn(
                            acceptor.get_executor().context(),
                            std::bind(
                                &AsyncHttpServerV2::do_session,
                                shared_from_this(),
                                std::move(socket),
                                std::placeholders::_1));
                    }
                }
            }

            void AsyncHttpServerV2::do_session(
                tcp::socket &socket,
                boost::asio::yield_context yield)
            {
                bool close = false;
                boost::system::error_code ec;

                // Construct the stream around the socket
                ssl::stream<tcp::socket &> stream{socket, ctx};

                // Perform the SSL handshake
                stream.async_handshake(ssl::stream_base::server, yield[ec]);
                if (ec)
                    return fail(ec, "handshake");
                logger_ << info << "handshake success";

                // This buffer is required to persist across reads
                boost::beast::flat_buffer buffer;

                // This lambda is used to send messages
                send_lambda<ssl::stream<tcp::socket &>> lambda{stream, close, ec, yield};

                for (;;)
                {
                    // Read a request
                    http::request<http::string_body> req;
                    http::async_read(stream, buffer, req, yield[ec]);
                    if (ec == http::error::end_of_stream)
                        break;
                    if (ec)
                        return fail(ec, "read");

                    // Send the response
                    handle_request(std::move(req), lambda);
                    if (ec)
                        return fail(ec, "write");
                    if (close)
                    {
                        // This means we should close the connection, usually because
                        // the response indicated the "Connection: close" semantic.
                        break;
                    }
                }

                // Perform the SSL shutdown
                stream.async_shutdown(yield[ec]);
                if (ec)
                    return fail(ec, "shutdown");

                // At shared_from_this() point the connection is closed gracefully
            }

            void AsyncHttpServerV2::fail(boost::system::error_code ec, char const *what)
            {
                std::cerr << what << ": " << ec.message() << "\n";
            }

            template <class Body, class Allocator, class Send>
            void AsyncHttpServerV2::handle_request(
                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));
            }

            std::string AsyncHttpServerV2::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();
                char constexpr path_separator = '/';
                if (result.back() == path_separator)
                    result.resize(result.size() - 1);
                result.append(path.data(), path.size());

                return result;
            }

            boost::beast::string_view AsyncHttpServerV2::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";
            }

        } // namespace tool

    } // namespace ut
} // namespace cbrs

           

testAsync.cxx

#include "AsyncHttpServerV2.hpp"

using AsyncHttpServerV2 = cbrs::ut::tool::AsyncHttpServerV2;
int main()
{
    std::shared_ptr<AsyncHttpServerV2> server = std::make_shared<AsyncHttpServerV2>();
    server->start();
    sleep(100);
    server->stop();
    return 0;
}

           

最後來個簡單的

CMakeLists.txt

project(asyncserver)
cmake_minimum_required(VERSION 3.17)

set(Boost_COMPONENTS date_time regex coroutine2)
set(BOOST_ROOT "/usr")
find_package(Boost)
find_package(PkgConfig)
find_package(OpenSSL REQUIRED)

add_executable(asyncserver
AsyncHttpServerV2.cpp
testAsync.cxx)

target_link_directories(asyncserver PRIVATE "/usr/lib/x86_64-linux-gnu/")
target_link_libraries(asyncserver PRIVATE boost_system  boost_coroutine boost_thread pthread boost_context ssl crypto)
           

等我有空加點注釋。