欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

C++ https server based on boost asio and beast

程序员文章站 2022-06-01 09:20:12
...

目前在做的项目需要一个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)

等我有空加点注释。