/* 88888888 888888888888 88888888888888 8888888888888888 888888888888888888 888888 8888 888888 88888 88 88888 888888 8888 888888 88888888888888888888 88888888888888888888 8888888888888888888888 8888888888888888888888888888 88888888888888888888888888888888 88888888888888888888 888888888888888888888888 888888 8888888888 888888 888 8888 8888 888 888 888 OCTOBANANA Belle 0.5.1 An HTTP / Websocket library in C++17 using Boost.Beast and Boost.ASIO. https://octobanana.com/software/belle Licensed under the MIT License Copyright (c) 2018 Brett Robinson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #ifndef OB_BELLE_HH #define OB_BELLE_HH #define OB_BELLE_VERSION_MAJOR 0 #define OB_BELLE_VERSION_MINOR 5 #define OB_BELLE_VERSION_PATCH 1 // Config Begin // compile with -DOB_BELLE_CONFIG_ or // comment out defines to alter the library // ssl support #ifndef OB_BELLE_CONFIG_SSL_OFF #define OB_BELLE_CONFIG_SSL_ON #endif // OB_BELLE_CONFIG_SSL_OFF // client support #ifndef OB_BELLE_CONFIG_CLIENT_OFF #define OB_BELLE_CONFIG_CLIENT_ON #endif // OB_BELLE_CONFIG_CLIENT_OFF // server support #ifndef OB_BELLE_CONFIG_SERVER_OFF #define OB_BELLE_CONFIG_SERVER_ON #endif // OB_BELLE_CONFIG_SERVER_OFF // Config End #include #include #include #ifdef OB_BELLE_CONFIG_SSL_ON #include #endif // OB_BELLE_CONFIG_SSL_ON #include #include #include #include #include #ifdef OB_BELLE_CONFIG_CLIENT_ON #include #endif // OB_BELLE_CONFIG_CLIENT_ON #ifdef OB_BELLE_CONFIG_SSL_ON #include #include #endif // OB_BELLE_CONFIG_SSL_ON #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace OB::Belle { // aliases namespace net = boost::asio; namespace beast = boost::beast; namespace http = boost::beast::http; namespace websocket = boost::beast::websocket; #ifdef OB_BELLE_CONFIG_SSL_ON namespace ssl = boost::asio::ssl; #endif // OB_BELLE_CONFIG_SSL_ON using tcp = boost::asio::ip::tcp; using error_code = boost::system::error_code; using Method = boost::beast::http::verb; using Status = boost::beast::http::status; using Header = boost::beast::http::field; using Headers = boost::beast::http::fields; // Ordered_Map: an insert ordered map // enables fast random lookup and insert ordered iterators // unordered map stores key value pairs // queue holds insert ordered iterators to each key in the unordered map template class Ordered_Map { public: // map iterators using m_iterator = typename std::unordered_map::iterator; using m_const_iterator = typename std::unordered_map::const_iterator; // index iterators using i_iterator = typename std::deque::iterator; using i_const_iterator = typename std::deque::const_iterator; Ordered_Map() { } Ordered_Map(std::initializer_list> const& lst) { for (auto const& [key, val] : lst) { _it.emplace_back(_map.insert({key, val}).first); } } ~Ordered_Map() { } Ordered_Map& operator()(K const& k, V const& v) { auto it = _map.insert_or_assign(k, v); if (it.second) { _it.emplace_back(it.first); } return *this; } i_iterator operator[](std::size_t index) { return _it[index]; } i_const_iterator const operator[](std::size_t index) const { return _it[index]; } V& at(K const& k) { return _map.at(k); } V const& at(K const& k) const { return _map.at(k); } m_iterator find(K const& k) { return _map.find(k); } m_const_iterator find(K const& k) const { return _map.find(k); } std::size_t size() const { return _it.size(); } bool empty() const { return _it.empty(); } Ordered_Map& clear() { _it.clear(); _map.clear(); return *this; } Ordered_Map& erase(K const& k) { auto it = _map.find(k); if (it != _map.end()) { for (auto e = _it.begin(); e < _it.end(); ++e) { if ((*e) == it) { _it.erase(e); break; } } _map.erase(it); } return *this; } i_iterator begin() { return _it.begin(); } i_const_iterator begin() const { return _it.begin(); } i_const_iterator cbegin() const { return _it.cbegin(); } i_iterator end() { return _it.end(); } i_const_iterator end() const { return _it.end(); } i_const_iterator cend() const { return _it.cend(); } m_iterator map_begin() { return _map.begin(); } m_const_iterator map_begin() const { return _map.begin(); } m_const_iterator map_cbegin() const { return _map.cbegin(); } m_iterator map_end() { return _map.end(); } m_const_iterator map_end() const { return _map.end(); } m_const_iterator map_cend() const { return _map.cend(); } private: std::unordered_map _map; std::deque _it; }; // class Ordered_Map namespace Detail { // prototypes inline std::string lowercase(std::string str); inline std::optional extension(std::string const& path); inline std::vector split(std::string const& str, std::string const& delim, std::size_t size = std::numeric_limits::max()); // string to lowercase inline std::string lowercase(std::string str) { auto const to_lower = [](char& c) { if (c >= 'A' && c <= 'Z') { c += 'a' - 'A'; } return c; }; for (char& c : str) { c = to_lower(c); } return str; } // find extension if present in a string path inline std::optional extension(std::string const& path) { if (path.empty() || path.size() < 2) { return {}; } auto const pos = path.rfind("."); if (pos == std::string::npos || pos == path.size() - 1) { return {}; } return path.substr(pos + 1); } // split a string by a delimiter 'n' times inline std::vector split(std::string const& str, std::string const& delim, std::size_t times) { std::vector vtok; std::size_t start {0}; auto end = str.find(delim); while ((times-- > 0) && (end != std::string::npos)) { vtok.emplace_back(str.substr(start, end - start)); start = end + delim.length(); end = str.find(delim, start); } vtok.emplace_back(str.substr(start, end)); return vtok; } // convert object into a string template inline std::string to_string(T const& t) { std::stringstream ss; ss << t; return ss.str(); } #ifdef OB_BELLE_CONFIG_SSL_ON // TODO switch to boost::beast::ssl_stream when it moves out of experimental template class ssl_stream : public ssl::stream_base { // This class (ssl_stream) is a derivative work based on Boost.Beast, // orignal copyright below: /* Copyright (c) 2016-2017 Vinnie Falco (vinnie dot falco at gmail dot com) Boost Software License - Version 1.0 - August 17th, 2003 Permission is hereby granted, free of charge, to any person or organization obtaining a copy of the software and accompanying documentation covered by this license (the "Software") to use, reproduce, display, distribute, execute, and transmit the Software, and to prepare derivative works of the Software, and to permit third-parties to whom the Software is furnished to do so, all subject to the following: The copyright notices in the Software and this entire statement, including the above license grant, this restriction and the following disclaimer, must be included in all copies of the Software, in whole or in part, and all derivative works of the Software, unless such copies or derivative works are solely in the form of machine-executable object code generated by a source language processor. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ using stream_type = ssl::stream; public: using native_handle_type = typename stream_type::native_handle_type; using impl_struct = typename stream_type::impl_struct; using next_layer_type = typename stream_type::next_layer_type; using lowest_layer_type = typename stream_type::lowest_layer_type; using executor_type = typename stream_type::executor_type; ssl_stream(Next_Layer&& arg, ssl::context& ctx) : _ptr {std::make_unique(std::move(arg), ctx)} { } executor_type get_executor() noexcept { return _ptr->get_executor(); } native_handle_type native_handle() { return _ptr->native_handle(); } next_layer_type const& next_layer() const { return _ptr->next_layer(); } next_layer_type& next_layer() { return _ptr->next_layer(); } lowest_layer_type& lowest_layer() { return _ptr->lowest_layer(); } lowest_layer_type const& lowest_layer() const { return _ptr->lowest_layer(); } void set_verify_mode(ssl::verify_mode v) { _ptr->set_verify_mode(v); } void set_verify_mode(ssl::verify_mode v, error_code& ec) { _ptr->set_verify_mode(v, ec); } void set_verify_depth(int depth) { _ptr->set_verify_depth(depth); } void set_verify_depth(int depth, error_code& ec) { _ptr->set_verify_depth(depth, ec); } template void set_verify_callback(VerifyCallback callback) { _ptr->set_verify_callback(callback); } template void set_verify_callback(VerifyCallback callback, error_code& ec) { _ptr->set_verify_callback(callback, ec); } void handshake(handshake_type type) { _ptr->handshake(type); } void handshake(handshake_type type, error_code& ec) { _ptr->handshake(type, ec); } template void handshake(handshake_type type, ConstBufferSequence const& buffers) { _ptr->handshake(type, buffers); } template void handshake(handshake_type type, ConstBufferSequence const& buffers, error_code& ec) { _ptr->handshake(type, buffers, ec); } template BOOST_ASIO_INITFN_RESULT_TYPE(HandshakeHandler, void(error_code)) async_handshake(handshake_type type, BOOST_ASIO_MOVE_ARG(HandshakeHandler) handler) { return _ptr->async_handshake(type, BOOST_ASIO_MOVE_CAST(HandshakeHandler)(handler)); } template BOOST_ASIO_INITFN_RESULT_TYPE(BufferedHandshakeHandler, void (error_code, std::size_t)) async_handshake(handshake_type type, ConstBufferSequence const& buffers, BOOST_ASIO_MOVE_ARG(BufferedHandshakeHandler) handler) { return _ptr->async_handshake(type, buffers, BOOST_ASIO_MOVE_CAST(BufferedHandshakeHandler)(handler)); } void shutdown() { _ptr->shutdown(); } void shutdown(error_code& ec) { _ptr->shutdown(ec); } template BOOST_ASIO_INITFN_RESULT_TYPE(ShutdownHandler, void (error_code)) async_shutdown(BOOST_ASIO_MOVE_ARG(ShutdownHandler) handler) { return _ptr->async_shutdown(BOOST_ASIO_MOVE_CAST(ShutdownHandler)(handler)); } template std::size_t write_some(ConstBufferSequence const& buffers) { return _ptr->write_some(buffers); } template std::size_t write_some(ConstBufferSequence const& buffers, error_code& ec) { return _ptr->write_some(buffers, ec); } template BOOST_ASIO_INITFN_RESULT_TYPE(WriteHandler, void (error_code, std::size_t)) async_write_some(ConstBufferSequence const& buffers, BOOST_ASIO_MOVE_ARG(WriteHandler) handler) { return _ptr->async_write_some(buffers, BOOST_ASIO_MOVE_CAST(WriteHandler)(handler)); } template std::size_t read_some(MutableBufferSequence const& buffers) { return _ptr->read_some(buffers); } template std::size_t read_some(MutableBufferSequence const& buffers, error_code& ec) { return _ptr->read_some(buffers, ec); } template BOOST_ASIO_INITFN_RESULT_TYPE(ReadHandler, void(error_code, std::size_t)) async_read_some(MutableBufferSequence const& buffers, BOOST_ASIO_MOVE_ARG(ReadHandler) handler) { return _ptr->async_read_some(buffers, BOOST_ASIO_MOVE_CAST(ReadHandler)(handler)); } template friend void teardown(websocket::role_type, ssl_stream& stream, error_code& ec); template friend void async_teardown(websocket::role_type, ssl_stream& stream, TeardownHandler&& handler); private: std::unique_ptr _ptr; }; // class ssl_stream template inline void teardown(websocket::role_type role, ssl_stream& stream, error_code& ec) { websocket::teardown(role, *stream._ptr, ec); } template inline void async_teardown(websocket::role_type role, ssl_stream& stream, TeardownHandler&& handler) { websocket::async_teardown(role, *stream._ptr, std::forward(handler)); } #endif // OB_BELLE_CONFIG_SSL_ON } // namespace Detail std::unordered_map const mime_types { {"html", "text/html"}, {"htm", "text/html"}, {"shtml", "text/html"}, {"css", "text/css"}, {"xml", "text/xml"}, {"gif", "image/gif"}, {"jpg", "image/jpg"}, {"jpeg", "image/jpg"}, {"js", "application/javascript"}, {"atom", "application/atom+xml"}, {"rss", "application/rss+xml"}, {"mml", "text/mathml"}, {"txt", "text/plain"}, {"jad", "text/vnd.sun.j2me.app-descriptor"}, {"wml", "text/vnd.wap.wml"}, {"htc", "text/x-component"}, {"png", "image/png"}, {"tif", "image/tiff"}, {"tiff", "image/tiff"}, {"wbmp", "image/vnd.wap.wbmp"}, {"ico", "image/x-icon"}, {"jng", "image/x-jng"}, {"bmp", "image/x-ms-bmp"}, {"svg", "image/svg+xml"}, {"svgz", "image/svg+xml"}, {"webp", "image/webp"}, {"woff", "application/font-woff"}, {"jar", "application/java-archive"}, {"war", "application/java-archive"}, {"ear", "application/java-archive"}, {"json", "application/json"}, {"hqx", "application/mac-binhex40"}, {"doc", "application/msword"}, {"pdf", "application/pdf"}, {"ps", "application/postscript"}, {"eps", "application/postscript"}, {"ai", "application/postscript"}, {"rtf", "application/rtf"}, {"m3u8", "application/vnd.apple.mpegurl"}, {"xls", "application/vnd.ms-excel"}, {"eot", "application/vnd.ms-fontobject"}, {"ppt", "application/vnd.ms-powerpoint"}, {"wmlc", "application/vnd.wap.wmlc"}, {"kml", "application/vnd.google-earth.kml+xml"}, {"kmz", "application/vnd.google-earth.kmz"}, {"7z", "application/x-7z-compressed"}, {"cco", "application/x-cocoa"}, {"jardiff", "application/x-java-archive-diff"}, {"jnlp", "application/x-java-jnlp-file"}, {"run", "application/x-makeself"}, {"pm", "application/x-perl"}, {"pl", "application/x-perl"}, {"pdb", "application/x-pilot"}, {"prc", "application/x-pilot"}, {"rar", "application/x-rar-compressed"}, {"rpm", "application/x-redhat-package-manager"}, {"sea", "application/x-sea"}, {"swf", "application/x-shockwave-flash"}, {"sit", "application/x-stuffit"}, {"tk", "application/x-tcl"}, {"tcl", "application/x-tcl"}, {"crt", "application/x-x509-ca-cert"}, {"pem", "application/x-x509-ca-cert"}, {"der", "application/x-x509-ca-cert"}, {"xpi", "application/x-xpinstall"}, {"xhtml", "application/xhtml+xml"}, {"xspf", "application/xspf+xml"}, {"zip", "application/zip"}, {"dll", "application/octet-stream"}, {"exe", "application/octet-stream"}, {"bin", "application/octet-stream"}, {"deb", "application/octet-stream"}, {"dmg", "application/octet-stream"}, {"img", "application/octet-stream"}, {"iso", "application/octet-stream"}, {"msm", "application/octet-stream"}, {"msp", "application/octet-stream"}, {"msi", "application/octet-stream"}, {"docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, {"xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, {"pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"}, {"kar", "audio/midi"}, {"midi", "audio/midi"}, {"mid", "audio/midi"}, {"mp3", "audio/mpeg"}, {"ogg", "audio/ogg"}, {"m4a", "audio/x-m4a"}, {"ra", "audio/x-realaudio"}, {"3gp", "video/3gpp"}, {"3gpp", "video/3gpp"}, {"ts", "video/mp2t"}, {"mp4", "video/mp4"}, {"mpg", "video/mpeg"}, {"mpeg", "video/mpeg"}, {"mov", "video/quicktime"}, {"webm", "video/webm"}, {"flv", "video/x-flv"}, {"m4v", "video/x-m4v"}, {"mng", "video/x-mng"}, {"asf", "video/x-ms-asf"}, {"asx", "video/x-ms-asf"}, {"wmv", "video/x-ms-wmv"}, {"avi", "video/x-msvideo"}, }; // prototypes inline std::string mime_type(std::string const& path); // find the mime type of a string path inline std::string mime_type(std::string const& path) { if (auto ext = Detail::extension(path)) { auto const str = Detail::lowercase(ext.value()); if (mime_types.find(str) != mime_types.end()) { return mime_types.at(str); } } return "application/octet-stream"; } class Request : public http::request { using Base = http::request; public: using Path = std::vector; using Params = std::unordered_multimap; // inherit base constructors using http::request::message; // default constructor Request() = default; // copy constructor Request(Request const&) = default; // move constructor Request(Request&&) = default; // copy assignment Request& operator=(Request const&) = default; // move assignment Request& operator=(Request&& rhs) = default; // default deconstructor ~Request() = default; Request&& move() noexcept { return std::move(*this); } // get the path Path& path() { return _path; } // get the query parameters Params& params() { return _params; } // serialize path and query parameters to the target void params_serialize() { std::string path {target().to_string()}; _path.clear(); _path.emplace_back(path); if (! _params.empty()) { path += "?"; auto it = _params.begin(); for (; it != _params.end(); ++it) { path += url_encode(it->first) + "=" + url_encode(it->second) + "&"; } path.pop_back(); } target(path); } // parse the query parameters from the target void params_parse() { std::string path {target().to_string()}; // separate the query params auto params = Detail::split(path, "?", 1); // set params if (params.size() == 2) { auto kv = Detail::split(params.at(1), "&"); for (auto const& e : kv) { if (e.empty()) { continue; } auto k_v = Detail::split(e, "=", 1); if (k_v.size() == 1) { _params.emplace(url_decode(e), ""); } else if (k_v.size() == 2) { _params.emplace(url_decode(k_v.at(0)), url_decode(k_v.at(1))); } continue; } } } private: std::string hex_encode(char const c) { char s[3]; if (c & 0x80) { std::snprintf(&s[0], 3, "%02X", static_cast(c & 0xff) ); } else { std::snprintf(&s[0], 3, "%02X", static_cast(c) ); } return std::string(s); } char hex_decode(std::string const& s) { unsigned int n; std::sscanf(s.data(), "%x", &n); return static_cast(n); } std::string url_encode(std::string const& str) { std::string res; res.reserve(str.size()); for (auto const& e : str) { if (e == ' ') { res += "+"; } else if (std::isalnum(static_cast(e)) || e == '-' || e == '_' || e == '.' || e == '~') { res += e; } else { res += "%" + hex_encode(e); } } return res; } std::string url_decode(std::string const& str) { std::string res; res.reserve(str.size()); for (std::size_t i = 0; i < str.size(); ++i) { if (str[i] == '+') { res += " "; } else if (str[i] == '%' && i + 2 < str.size() && std::isxdigit(static_cast(str[i + 1])) && std::isxdigit(static_cast(str[i + 2]))) { res += hex_decode(str.substr(i + 1, 2)); i += 2; } else { res += str[i]; } } return res; } Path _path {}; Params _params {}; }; // Request // store a type erased websocket struct Websocket_Session { // default deconstructor virtual ~Websocket_Session() = default; // send a message virtual void send(std::string const&&) = 0; }; // struct Websocket_Session #ifdef OB_BELLE_CONFIG_SERVER_ON class Server { public: // NOTE Channel implementation is NOT thread safe class Channel { public: Channel() { } void join(Websocket_Session& socket_) { _sockets.insert(&socket_); } void leave(Websocket_Session& socket_) { _sockets.erase(&socket_); } void broadcast(std::string const&& str_) const { for(auto const e : _sockets) { e->send(std::move(str_)); } } std::size_t size() const { return _sockets.size(); } private: std::unordered_set _sockets; }; // class Channel // NOTE Channels implementation is NOT thread safe using Channels = std::unordered_map; template struct Http_Ctx_Basic { Request req {}; http::response res {}; std::shared_ptr data {nullptr}; }; // class Http_Ctx_Basic using Http_Ctx = Http_Ctx_Basic; class Websocket_Ctx { public: Websocket_Ctx(Websocket_Session& socket_, Request&& req_, Channels& channels_) : socket {&socket_}, req {std::move(req_)}, channels {channels_} { } ~Websocket_Ctx() { } void send(std::string const&& str_) const { socket->send(std::move(str_)); } void broadcast(std::string const&& str_) const { for (auto const& e : channels) { e.second.broadcast(std::move(str_)); } } Websocket_Session* socket; Request req; Channels& channels; std::string msg {}; std::shared_ptr data {nullptr}; }; // class Websocket_Ctx // callbacks using fn_on_signal = std::function; using fn_on_http = std::function; using fn_on_websocket = std::function; struct fns_on_websocket { fns_on_websocket(fn_on_websocket const& begin_, fn_on_websocket const& data_, fn_on_websocket const& end_) : begin {begin_}, data {data_}, end {end_} { } fn_on_websocket begin {}; fn_on_websocket data {}; fn_on_websocket end {}; }; // struct fns_on_websocket // aliases using Http_Routes = Ordered_Map>; using Websocket_Routes = std::vector>; private: struct Attr { #ifdef OB_BELLE_CONFIG_SSL_ON // use ssl bool ssl {false}; // ssl context ssl::context ssl_context {ssl::context::tlsv12_server}; #endif // OB_BELLE_CONFIG_SSL_ON // the public directory for serving static files std::string public_dir {}; // default index filename for the public directory std::string index_file {"index.html"}; // socket timeout std::chrono::seconds timeout {10}; // serve static files from public directory bool http_static {true}; // serve dynamic content bool http_dynamic {true}; // upgrade http to websocket connection bool websocket {true}; // default http headers Headers http_headers {}; // http routes Http_Routes http_routes {}; // websocket routes Websocket_Routes websocket_routes {}; // callbacks for http fn_on_http on_http_error {}; fn_on_http on_http_connect {}; fn_on_http on_http_disconnect {}; // callbacks for websocket fn_on_websocket on_websocket_error {}; fn_on_websocket on_websocket_connect {}; fn_on_websocket on_websocket_disconnect {}; // websocket channels Channels channels {}; }; // struct Attr template class Websocket_Base : public Websocket_Session { Derived& derived() { return static_cast(*this); } public: Websocket_Base(net::io_context& io_, std::shared_ptr const attr_, Request&& req_, fns_on_websocket const& on_websocket_) : _attr {attr_}, _ctx {static_cast(*this), std::move(req_), _attr->channels}, _on_websocket {on_websocket_}, _strand {io_.get_executor()} { } ~Websocket_Base() { // leave channel _attr->channels.at(_ctx.req.path().at(0)).leave(derived()); if (_on_websocket.end) { try { // run user function _on_websocket.end(_ctx); } catch (...) { this->handle_error(); } } if (_attr->on_websocket_disconnect) { try { // run user function _attr->on_websocket_disconnect(_ctx); } catch (...) { this->handle_error(); } } } void send(std::string const&& str_) { auto const pstr = std::make_shared(std::move(str_)); _que.emplace_back(pstr); if (_que.size() > 1) { return; } derived().socket().async_write(net::buffer(*_que.front()), [self = derived().shared_from_this()](error_code ec, std::size_t bytes) { self->on_write(ec, bytes); } ); } void handle_error() { if (_attr->on_websocket_error) { try { // run user function _attr->on_websocket_error(_ctx); } catch (...) { } } } void do_accept() { derived().socket().control_callback( [this](websocket::frame_type type, boost::beast::string_view data) { this->on_control_callback(type, data); } ); derived().socket().async_accept_ex(_ctx.req, [&](auto& res) { for (auto const& e : _attr->http_headers) { res.insert(e.name_string(), e.value()); } }, net::bind_executor(_strand, [self = derived().shared_from_this()](error_code ec) { self->on_accept(ec); } ) ); } void on_accept(error_code ec_) { if (ec_ == net::error::operation_aborted) { return; } if (ec_) { // TODO log here return; } // join channel if (_attr->channels.find(_ctx.req.path().at(0)) == _attr->channels.end()) { _attr->channels[_ctx.req.path().at(0)] = Channel(); } _attr->channels.at(_ctx.req.path().at(0)).join(derived()); if (_attr->on_websocket_connect) { try { // run user function _attr->on_websocket_connect(_ctx); } catch (...) { this->handle_error(); } } if (_on_websocket.begin) { try { // run user function _on_websocket.begin(_ctx); } catch (...) { this->handle_error(); } } this->do_read(); } void on_control_callback(websocket::frame_type type_, boost::beast::string_view data_) { boost::ignore_unused(type_, data_); } void do_read() { derived().socket().async_read(_buf, net::bind_executor(_strand, [self = derived().shared_from_this()](error_code ec, std::size_t bytes) { self->on_read(ec, bytes); } ) ); } void on_read(error_code ec_, std::size_t bytes_) { boost::ignore_unused(bytes_); // socket closed by the timer if (ec_ == net::error::operation_aborted) { return; } // socket closed if (ec_ == websocket::error::closed) { return; } if (ec_) { // TODO log here return; } if (_on_websocket.data) { try { _ctx.msg = boost::beast::buffers_to_string(_buf.data()); // run user function _on_websocket.data(_ctx); } catch (...) { handle_error(); } } // clear the request object _ctx.req.clear(); // clear the buffers _buf.consume(_buf.size()); this->do_read(); } void on_write(error_code ec_, std::size_t bytes_) { boost::ignore_unused(bytes_); // happens when the timer closes the socket if (ec_ == net::error::operation_aborted) { return; } if (ec_) { // TODO log here return; } // remove sent message from the queue _que.pop_front(); if (_que.empty()) { return; } derived().socket().async_write(net::buffer(*_que.front()), [self = derived().shared_from_this()](error_code ec, std::size_t bytes) { self->on_write(ec, bytes); } ); } std::shared_ptr const _attr; Websocket_Ctx _ctx; fns_on_websocket const& _on_websocket; net::strand _strand; boost::beast::multi_buffer _buf; std::deque> _que {}; }; // class Websocket_Base class Websocket : public Websocket_Base, public std::enable_shared_from_this { public: Websocket(tcp::socket&& socket_, std::shared_ptr const attr_, Request&& req_, fns_on_websocket const& on_websocket_) : Websocket_Base {socket_.get_executor().context(), attr_, std::move(req_), on_websocket_}, _socket {std::move(socket_)} { } ~Websocket() { } websocket::stream& socket() { return _socket; } void run() { this->do_accept(); } void do_timeout() { this->do_shutdown(); } void do_shutdown() { _socket.async_close(websocket::close_code::normal, net::bind_executor(this->_strand, [self = this->shared_from_this()](error_code ec) { self->on_shutdown(ec); } ) ); } void on_shutdown(error_code ec_) { if (ec_) { // TODO log here return; } } private: websocket::stream _socket; }; // class Websocket #ifdef OB_BELLE_CONFIG_SSL_ON class Websockets : public Websocket_Base, public std::enable_shared_from_this { public: Websockets(Detail::ssl_stream&& socket_, std::shared_ptr const attr_, Request&& req_, fns_on_websocket const& on_websocket_) : Websocket_Base {socket_.get_executor().context(), attr_, std::move(req_), on_websocket_}, _socket {std::move(socket_)} { } ~Websockets() { } websocket::stream>& socket() { return _socket; } void run() { this->do_accept(); } void do_timeout() { this->do_shutdown(); } void do_shutdown() { _socket.async_close(websocket::close_code::normal, net::bind_executor(this->_strand, [self = this->shared_from_this()](error_code ec) { self->on_shutdown(ec); } ) ); } void on_shutdown(error_code ec_) { if (ec_) { // TODO log here return; } } private: websocket::stream> _socket; }; // class Websockets #endif // OB_BELLE_CONFIG_SSL_ON template class Http_Base { Derived& derived() { return static_cast(*this); } public: Http_Base(net::io_context& io_, std::shared_ptr const attr_) : _strand {io_.get_executor()}, _timer {io_, (std::chrono::steady_clock::time_point::max)()}, _attr {attr_} { } ~Http_Base() { } // TODO remove shim once visual studio supports generic lambdas #ifdef _MSC_VER template static void constexpr send(Self self, Res&& res) #else // generic lambda for sending different types of responses static auto constexpr send = [](auto self, auto&& res) -> void #endif // _MSC_VER { using item_type = std::remove_reference_t; auto ptr = std::make_shared(std::move(res)); self->_res = ptr; http::async_write(self->derived().socket(), *ptr, net::bind_executor(self->_strand, [self, close = ptr->need_eof()] (error_code ec, std::size_t bytes) { self->on_write(ec, bytes, close); } ) ); }; int serve_static() { if (! _attr->http_static || _attr->public_dir.empty()) { return 404; } if ((_ctx.req.method() != http::verb::get) && (_ctx.req.method() != http::verb::head)) { return 404; } std::string path {_attr->public_dir + _ctx.req.target().to_string()}; if (path.back() == '/') { path += _attr->index_file; } error_code ec; http::file_body::value_type body; body.open(path.data(), beast::file_mode::scan, ec); if (ec) { return 404; } // head request if (_ctx.req.method() == http::verb::head) { http::response res {}; res.base() = http::response_header<>(_attr->http_headers); res.version(_ctx.req.version()); res.keep_alive(_ctx.req.keep_alive()); res.content_length(body.size()); res.set(Header::content_type, mime_type(path)); send(derived().shared_from_this(), std::move(res)); return 0; } // get request auto const size = body.size(); http::response res { std::piecewise_construct, std::make_tuple(std::move(body)), std::make_tuple(_attr->http_headers) }; res.version(_ctx.req.version()); res.keep_alive(_ctx.req.keep_alive()); res.content_length(size); res.set(Header::content_type, mime_type(path)); send(derived().shared_from_this(), std::move(res)); return 0; } int serve_dynamic() { if (! _attr->http_dynamic || _attr->http_routes.empty()) { return 404; } // regex variables std::smatch rx_match {}; std::regex_constants::syntax_option_type const rx_opts {std::regex::ECMAScript}; std::regex_constants::match_flag_type const rx_flgs {std::regex_constants::match_not_null}; // the request path std::string path {_ctx.req.target().to_string()}; // separate the query parameters auto params = Detail::split(path, "?", 1); path = params.at(0); // iterate over routes for (auto const& regex_method : _attr->http_routes) { bool method_match {false}; auto match = (*regex_method).second.find(0); if (match != (*regex_method).second.end()) { method_match = true; } else { match = (*regex_method).second.find(static_cast(_ctx.req.method())); if (match != (*regex_method).second.end()) { method_match = true; } } if (method_match) { std::regex rx_str {(*regex_method).first, rx_opts}; if (std::regex_match(path, rx_match, rx_str, rx_flgs)) { // set the path for (auto const& e : rx_match) { _ctx.req.path().emplace_back(e.str()); } // parse target params _ctx.req.params_parse(); // set callback function auto const& user_func = match->second; try { // run user function user_func(_ctx); _ctx.res.content_length(_ctx.res.body().size()); send(derived().shared_from_this(), std::move(_ctx.res)); return 0; } catch (int const e) { return e; } catch (unsigned int const e) { return static_cast(e); } catch (Status const e) { return static_cast(e); } catch (std::exception const&) { return 500; } catch (...) { return 500; } } } } return 404; } void serve_error(int err) { _ctx.res.result(static_cast(err)); if (_attr->on_http_error) { try { // run user function _attr->on_http_error(_ctx); _ctx.res.content_length(_ctx.res.body().size()); send(derived().shared_from_this(), std::move(_ctx.res)); return; } catch (int const e) { _ctx.res.result(static_cast(e)); } catch (unsigned int const e) { _ctx.res.result(e); } catch (Status const e) { _ctx.res.result(e); } catch (std::exception const&) { _ctx.res.result(500); } catch (...) { _ctx.res.result(500); } } _ctx.res.set(Header::content_type, "text/plain"); _ctx.res.body() = "Error: " + std::to_string(_ctx.res.result_int()); _ctx.res.content_length(_ctx.res.body().size()); send(derived().shared_from_this(), std::move(_ctx.res)); }; void handle_request() { // set default response values _ctx.res.version(_ctx.req.version()); _ctx.res.keep_alive(_ctx.req.keep_alive()); if (_ctx.req.target().empty()) { _ctx.req.target() = "/"; } if (_ctx.req.target().at(0) != '/' || _ctx.req.target().find("..") != boost::beast::string_view::npos) { this->serve_error(404); return; } // serve dynamic content auto dyna = this->serve_dynamic(); // success if (dyna == 0) { return; } // error if (dyna != 404) { this->serve_error(dyna); return; } // serve static content auto stat = this->serve_static(); if (stat != 0) { this->serve_error(stat); return; } } bool handle_websocket() { // the request path std::string path {_ctx.req.target().to_string()}; // separate the query parameters auto params = Detail::split(path, "?", 1); path = params.at(0); // regex variables std::smatch rx_match {}; std::regex_constants::syntax_option_type const rx_opts {std::regex::ECMAScript}; std::regex_constants::match_flag_type const rx_flgs {std::regex_constants::match_not_null}; // check for matching route for (auto const& [regex, callback] : _attr->websocket_routes) { std::regex rx_str {regex, rx_opts}; if (std::regex_match(path, rx_match, rx_str, rx_flgs)) { // set the path for (auto const& e : rx_match) { _ctx.req.path().emplace_back(e.str()); } // parse target params _ctx.req.params_parse(); // create websocket std::make_shared (derived().socket_move(), _attr, std::move(_ctx.req), callback) ->run(); return true; } } return false; } void cancel_timer() { // set the timer to expire immediately _timer.expires_at((std::chrono::steady_clock::time_point::min)()); } void do_timer() { // wait on the timer _timer.async_wait( net::bind_executor(_strand, [self = derived().shared_from_this()](error_code ec) { self->on_timer(ec); } ) ); } void on_timer(error_code ec_ = {}) { if (ec_ && ec_ != net::error::operation_aborted) { // TODO log here return; } // check if socket has been upgraded or closed if (_timer.expires_at() == (std::chrono::steady_clock::time_point::min)()) { return; } // check expiry if (_timer.expiry() <= std::chrono::steady_clock::now()) { derived().do_timeout(); return; } } void do_read() { _timer.expires_after(_attr->timeout); _res = nullptr; _ctx = {}; _ctx.res.base() = http::response_header<>(_attr->http_headers); http::async_read(derived().socket(), _buf, _ctx.req, net::bind_executor(_strand, [self = derived().shared_from_this()](error_code ec, std::size_t bytes) { self->on_read(ec, bytes); } ) ); } void on_read(error_code ec_, std::size_t bytes_) { boost::ignore_unused(bytes_); // the timer has closed the socket if (ec_ == net::error::operation_aborted) { return; } // the connection has been closed if (ec_ == http::error::end_of_stream) { derived().do_shutdown(); return; } if (ec_) { // TODO log here return; } // check for websocket upgrade if (websocket::is_upgrade(_ctx.req)) { if (! _attr->websocket || _attr->websocket_routes.empty()) { derived().do_shutdown(); return; } // upgrade to websocket if (handle_websocket()) { this->cancel_timer(); return; } else { derived().do_shutdown(); return; } } if (_attr->on_http_connect) { try { // run user func _attr->on_http_connect(_ctx); } catch (...) { } } this->handle_request(); if (_attr->on_http_disconnect) { try { // run user func _attr->on_http_disconnect(_ctx); } catch (...) { } } } void on_write(error_code ec_, std::size_t bytes_, bool close_) { boost::ignore_unused(bytes_); // the timer has closed the socket if (ec_ == net::error::operation_aborted) { return; } if (ec_) { // TODO log here return; } if (close_) { derived().do_shutdown(); return; } // read another request this->do_read(); } net::strand _strand; net::steady_timer _timer; boost::beast::flat_buffer _buf; std::shared_ptr const _attr; Http_Ctx _ctx {}; std::shared_ptr _res {nullptr}; bool _close {false}; }; // class Http_Base class Http : public Http_Base, public std::enable_shared_from_this { public: Http(tcp::socket socket_, std::shared_ptr const attr_) : Http_Base {socket_.get_executor().context(), attr_}, _socket {std::move(socket_)} { } ~Http() { } tcp::socket& socket() { return _socket; } tcp::socket&& socket_move() { return std::move(_socket); } void run() { this->do_timer(); this->do_read(); } void do_timeout() { this->do_shutdown(); } void do_shutdown() { error_code ec; // send a tcp shutdown _socket.shutdown(tcp::socket::shutdown_send, ec); this->cancel_timer(); if (ec) { // TODO log here return; } } private: tcp::socket _socket; }; // class Http #ifdef OB_BELLE_CONFIG_SSL_ON class Https : public Http_Base, public std::enable_shared_from_this { public: Https(tcp::socket&& socket_, std::shared_ptr const attr_) : Http_Base {socket_.get_executor().context(), attr_}, _socket {std::move(socket_), attr_->ssl_context} { this->_close = true; } ~Https() { } Detail::ssl_stream& socket() { return _socket; } Detail::ssl_stream&& socket_move() { return std::move(_socket); } void run() { this->do_timer(); this->do_handshake(); } void do_timeout() { // timed out on handshake or shutdown if (this->_close) { return; } // reset the timer this->_timer.expires_at((std::chrono::steady_clock::time_point::max)()); this->do_timer(); this->do_shutdown(); } void do_handshake() { this->_timer.expires_after(this->_attr->timeout); _socket.async_handshake(ssl::stream_base::server, net::bind_executor(this->_strand, [self = this->shared_from_this()](error_code ec) { self->on_handshake(ec); } ) ); } void on_handshake(error_code ec_) { // the timer has closed the socket if (ec_ == net::error::operation_aborted) { return; } if (ec_) { // TODO log here return; } this->_close = false; this->do_read(); } void do_shutdown() { this->_timer.expires_after(this->_attr->timeout); this->_close = true; // shutdown the socket _socket.async_shutdown( net::bind_executor(this->_strand, [self = this->shared_from_this()](error_code ec) { self->on_shutdown(ec); } ) ); } void on_shutdown(error_code ec_) { this->cancel_timer(); // the timer has closed the socket if (ec_ == net::error::operation_aborted) { return; } if (ec_) { // TODO log here return; } } private: Detail::ssl_stream _socket; }; // class Https #endif // OB_BELLE_CONFIG_SSL_ON template class Listener : public std::enable_shared_from_this> { public: Listener(net::io_context& io_, tcp::endpoint endpoint_, std::shared_ptr const attr_) : _acceptor {io_}, _socket {io_}, _attr {attr_} { error_code ec; // open the acceptor _acceptor.open(endpoint_.protocol(), ec); if (ec) { // TODO log here return; } // allow address reuse _acceptor.set_option(net::socket_base::reuse_address(true), ec); if (ec) { // TODO log here return; } // bind to the server address _acceptor.bind(endpoint_, ec); if (ec) { // TODO log here return; } // start listening for connections _acceptor.listen(net::socket_base::max_listen_connections, ec); if (ec) { // TODO log here return; } } void run() { if (! _acceptor.is_open()) { // TODO log here return; } do_accept(); } private: void do_accept() { _acceptor.async_accept(_socket, [self = this->shared_from_this()](error_code ec) { self->on_accept(ec); } ); } void on_accept(error_code ec_) { if (ec_) { // TODO log here } else { // create an Http obj and run it std::make_shared(std::move(_socket), _attr)->run(); } // accept another connection do_accept(); } private: tcp::acceptor _acceptor; tcp::socket _socket; std::shared_ptr const _attr; }; // class Listener public: // default constructor Server() { } // constructor with address and port Server(std::string address_, unsigned short port_) : _address {address_}, _port {port_} { } #ifdef OB_BELLE_CONFIG_SSL_ON // constructor with address, port, and ssl Server(std::string address_, unsigned short port_, bool ssl_) : _address {address_}, _port {port_} { _attr->ssl = true; } #endif // OB_BELLE_CONFIG_SSL_ON // destructor ~Server() { } // set the listening address Server& address(std::string address_) { _address = address_; return *this; } // get the listening address std::string address() { return _address; } // set the listening port Server& port(unsigned short port_) { _port = port_; return *this; } // get the listening port unsigned short port() { return _port; } // set the public directory for serving static files Server& public_dir(std::string public_dir_) { if (! public_dir_.empty() && public_dir_.back() == '/') { public_dir_.pop_back(); } if (public_dir_.empty()) { public_dir_ = "."; } _attr->public_dir = public_dir_; return *this; } // get the public directory for serving static files std::string public_dir() { return _attr->public_dir; } // set the default index filename Server& index_file(std::string index_file_) { if (index_file_.empty()) { _attr->index_file = "index.html"; } else { _attr->index_file = index_file_; } return *this; } // get the default index filename std::string index_file() { return _attr->index_file; } // set the number of threads Server& threads(unsigned int threads_) { _threads = std::max(1, threads_); return *this; } // get the number of threads unsigned int threads() { return _threads; } #ifdef OB_BELLE_CONFIG_SSL_ON // set ssl Server& ssl(bool ssl_) { _attr->ssl = ssl_; return *this; } // get ssl bool ssl() { return _attr->ssl; } // get the ssl context ssl::context& ssl_context() { return _attr->ssl_context; } // set the ssl context Server& ssl_context(ssl::context&& ctx_) { _attr->ssl_context = std::move(ctx_); return *this; } #endif // OB_BELLE_CONFIG_SSL_ON // set http static Server& http_static(bool val_) { _attr->http_static = val_; return *this; } // get http static bool http_static() { return _attr->http_static; } // set http dynamic Server& http_dynamic(bool val_) { _attr->http_dynamic = val_; return *this; } // get http dynamic bool http_dynamic() { return _attr->http_dynamic; } // set http static and dynamic Server& http(bool val_) { _attr->http_static = val_; _attr->http_dynamic = val_; return *this; } // set websocket upgrade Server& websocket(bool val_) { _attr->websocket = val_; return *this; } // get websocket upgrade bool websocket() { return _attr->websocket; } // set the socket timeout Server& timeout(std::chrono::seconds timeout_) { _attr->timeout = timeout_; return *this; } // get the socket timeout std::chrono::seconds timeout() { return _attr->timeout; } // get the io_context net::io_context& io() { return _io; } // set signals to capture Server& signals(std::vector signals_) { for (auto const& e : signals_) { _signals.add(e); } return *this; } // set signal callback // called when a captured signal is received Server& on_signal(fn_on_signal on_signal_) { _on_signal = on_signal_; _signals.async_wait( [this](error_code const& ec, int sig) { this->_on_signal(ec, sig); } ); return *this; } // set http callback matching a single method // called after http read Server& on_http(std::string route_, Method method_, fn_on_http on_http_) { if (_attr->http_routes.find(route_) == _attr->http_routes.map_end()) { _attr->http_routes(route_, {{static_cast(method_), on_http_}}); } else { _attr->http_routes.at(route_)[static_cast(method_)] = on_http_; } return *this; } // set http callback matching multiple methods // called after http read Server& on_http(std::string route_, std::vector methods_, fn_on_http on_http_) { for (auto const& e : methods_) { if (_attr->http_routes.find(route_) == _attr->http_routes.map_end()) { _attr->http_routes(route_, {{static_cast(e), on_http_}}); } else { _attr->http_routes.at(route_)[static_cast(e)] = on_http_; } } return *this; } // set http callback matching all methods // called after http read Server& on_http(std::string route_, fn_on_http on_http_) { if (_attr->http_routes.find(route_) == _attr->http_routes.map_end()) { _attr->http_routes(route_, {{0, on_http_}}); } else { _attr->http_routes.at(route_)[0] = on_http_; } return *this; } // set http error callback // called when an exception or error occurs Server& on_http_error(fn_on_http on_http_error_) { _attr->on_http_error = on_http_error_; return *this; } // set http connect callback // called at the very beginning of every http connection Server& on_http_connect(fn_on_http on_http_connect_) { _attr->on_http_connect = on_http_connect_; return *this; } // set http disconnect callback // called at the very end of every http connection Server& on_http_disconnect(fn_on_http on_http_disconnect_) { _attr->on_http_disconnect = on_http_disconnect_; return *this; } // set websocket data callback // data: called after every websocket read Server& on_websocket(std::string route_, fn_on_websocket data_) { _attr->websocket_routes.emplace_back( std::make_pair(route_, fns_on_websocket(nullptr, data_, nullptr))); return *this; } // set websocket begin, data, and end callbacks // begin: called once after connected // data: called after every websocket read // end: called once after disconnected Server& on_websocket(std::string route_, fn_on_websocket begin_, fn_on_websocket data_, fn_on_websocket end_) { _attr->websocket_routes.emplace_back( std::make_pair(route_, fns_on_websocket(begin_, data_, end_))); return *this; } // set websocket error callback // called when an exception or error occurs Server& on_websocket_error(fn_on_websocket on_websocket_error_) { _attr->on_websocket_error = on_websocket_error_; return *this; } // set websocket connect callback // called once at the very beginning after connected Server& on_websocket_connect(fn_on_websocket on_websocket_connect_) { _attr->on_websocket_connect = on_websocket_connect_; return *this; } // set websocket disconnect callback // called once at the very end after disconnected Server& on_websocket_disconnect(fn_on_websocket on_websocket_disconnect_) { _attr->on_websocket_disconnect = on_websocket_disconnect_; return *this; } // get http routes Http_Routes& http_routes() { return _attr->http_routes; } // get websocket routes Websocket_Routes& websocket_routes() { return _attr->websocket_routes; } // set default http headers Server& http_headers(Headers const& headers_) { _attr->http_headers = headers_; return *this; } // get default http headers Headers& http_headers() { return _attr->http_headers; } // get websocket channels Channels& channels() { return _attr->channels; } // check if address:port is already in use static bool available(std::string const& address_, unsigned short port_) { error_code ec; net::io_context io; tcp::acceptor acceptor(io); auto endpoint = tcp::endpoint(net::ip::make_address(address_), port_); acceptor.open(endpoint.protocol(), ec); if (ec) { return false; } acceptor.bind(endpoint, ec); if (ec) { return false; } return true; }; // check if address:port is already in use bool available() const { error_code ec; net::io_context io; tcp::acceptor acceptor(io); auto endpoint = tcp::endpoint(net::ip::make_address(_address), _port); acceptor.open(endpoint.protocol(), ec); if (ec) { return false; } acceptor.bind(endpoint, ec); if (ec) { return false; } return true; }; // start the server void listen(std::string address_ = "", unsigned short port_ = 0) { // set the listening address if (! address_.empty()) { _address = address_; } // set the listening port if (port_ != 0) { _port = port_; } // set default server header value if not present if (_attr->http_headers.find(Header::server) == _attr->http_headers.end()) { _attr->http_headers.set(Header::server, "Belle"); } // websocket channels are not threadsafe, limit to 1 thread if (_attr->websocket && _threads > 1) { _threads = 1; } // create the listener #ifdef OB_BELLE_CONFIG_SSL_ON if (_attr->ssl) { // use https std::make_shared> (_io, tcp::endpoint(net::ip::make_address(_address), _port), _attr) ->run(); } else #endif // OB_BELLE_CONFIG_SSL_ON { // use http std::make_shared> (_io, tcp::endpoint(net::ip::make_address(_address), _port), _attr) ->run(); } // thread pool std::vector io_threads; // create and start threads if needed if (_threads > 1) { io_threads.reserve(static_cast(_threads) - 1); for (unsigned int i = 1; i < _threads; ++i) { io_threads.emplace_back( [this]() { // run the io context on the new thread this->_io.run(); } ); } } // run the io context on the current thread _io.run(); // wait on threads to return for (auto& t : io_threads) { t.join(); } } private: // hold the server attributes shared by each socket connection std::shared_ptr const _attr {std::make_shared()}; // the address to listen on std::string _address {"127.0.0.1"}; // the port to listen on unsigned short _port {8080}; // the number of threads to run on unsigned int _threads {1}; // the io context net::io_context _io {}; // signals net::signal_set _signals {_io}; // callback for signals fn_on_signal _on_signal {}; }; // class Server #endif // OB_BELLE_CONFIG_SERVER_ON #ifdef OB_BELLE_CONFIG_CLIENT_ON class Client { public: struct Http_Ctx { // http request Request* req {nullptr}; // http response http::response res {}; }; // struct Http_Ctx struct Error_Ctx { // error code error_code const& ec; }; // struct Error_Ctx // callbacks using fn_on_http = std::function; using fn_on_http_error = std::function; struct Req_Ctx { // http request object Request req {}; // http callback fn_on_http on_http {}; }; // struct Req_Ctx struct Attr { #ifdef OB_BELLE_CONFIG_SSL_ON // use ssl bool ssl {false}; // ssl context ssl::context ssl_context {ssl::context::tlsv12_client}; #endif // OB_BELLE_CONFIG_SSL_ON // socket timeout std::chrono::seconds timeout {10}; // address to connect to std::string address {"127.0.0.1"}; // port to connect to unsigned short port {8080}; // http request queue std::deque que; // http error callback fn_on_http_error on_http_error {}; }; // struct Attr template class Http_Base { Derived& derived() { return static_cast(*this); } public: Http_Base(net::io_context& io_, std::shared_ptr const attr_) : _resolver {io_}, _strand {io_.get_executor()}, _timer {io_, (std::chrono::steady_clock::time_point::max)()}, _attr {attr_} { } ~Http_Base() { } void cancel_timer() { // set the timer to expire immediately _timer.expires_at((std::chrono::steady_clock::time_point::min)()); } void do_timer() { // wait on the timer _timer.async_wait( net::bind_executor(_strand, [self = derived().shared_from_this()](error_code ec) { self->on_timer(ec); } ) ); } void on_timer(error_code ec_) { if (ec_ && ec_ != net::error::operation_aborted) { Error_Ctx err {ec_}; _attr->on_http_error(err); return; } // check if socket has been closed if (_timer.expires_at() == (std::chrono::steady_clock::time_point::min)()) { return; } // check expiry if (_timer.expiry() <= std::chrono::steady_clock::now()) { derived().do_close(); return; } if (_close) { return; } } void do_resolve() { _timer.expires_after(_attr->timeout); // domain name server lookup _resolver.async_resolve(_attr->address, Detail::to_string(_attr->port), net::bind_executor(_strand, [self = derived().shared_from_this()] (error_code ec, tcp::resolver::results_type results) { self->on_resolve(ec, results); } ) ); } void on_resolve(error_code ec_, tcp::resolver::results_type results_) { if (ec_) { cancel_timer(); Error_Ctx err {ec_}; _attr->on_http_error(err); return; } // connect to the endpoint net::async_connect(derived().socket().lowest_layer(), results_.begin(), results_.end(), net::bind_executor(_strand, [self = derived().shared_from_this()](error_code ec, auto) { self->derived().on_connect(ec); } ) ); } void prepare_request() { _ctx = {}; _ctx.req = &_attr->que.front().req; // serialize target and params _ctx.req->params_serialize(); // set default user-agent header value if not present if (_ctx.req->find(Header::user_agent) == _ctx.req->end()) { _ctx.req->set(Header::user_agent, "Belle"); } // set default host header value if not present if (_ctx.req->find(Header::host) == _ctx.req->end()) { _ctx.req->set(Header::host, _attr->address); } // set connection close if last request in the queue if (_attr->que.size() == 1) { _ctx.req->keep_alive(false); } // prepare the payload _ctx.req->prepare_payload(); } void do_write() { prepare_request(); _timer.expires_after(_attr->timeout); // Send the HTTP request http::async_write(derived().socket(), *_ctx.req, net::bind_executor(_strand, [self = derived().shared_from_this()](error_code ec, std::size_t bytes) { self->on_write(ec, bytes); } ) ); } void on_write(error_code ec_, std::size_t bytes_) { boost::ignore_unused(bytes_); if (ec_) { cancel_timer(); Error_Ctx err {ec_}; _attr->on_http_error(err); return; } do_read(); } void do_read() { // Receive the HTTP response http::async_read(derived().socket(), _buf, _ctx.res, net::bind_executor(_strand, [self = derived().shared_from_this()](error_code ec, std::size_t bytes) { self->on_read(ec, bytes); } ) ); } void on_read(error_code ec_, std::size_t bytes_) { boost::ignore_unused(bytes_); if (ec_) { cancel_timer(); Error_Ctx err {ec_}; _attr->on_http_error(err); return; } // run user function _attr->que.front().on_http(_ctx); // remove request from queue _attr->que.pop_front(); if (_attr->que.empty()) { derived().do_close(); } else { do_write(); } } tcp::resolver _resolver; net::strand _strand; net::steady_timer _timer; std::shared_ptr const _attr; Http_Ctx _ctx {}; beast::flat_buffer _buf {}; bool _close {false}; }; // class Http_Base class Http : public Http_Base, public std::enable_shared_from_this { public: Http(net::io_context& io_, std::shared_ptr attr_) : Http_Base(io_, attr_), _socket {io_} { } ~Http() { } tcp::socket& socket() { return _socket; } tcp::socket&& socket_move() { return std::move(_socket); } void run() { do_timer(); do_resolve(); } void on_connect(error_code ec_) { if (ec_) { cancel_timer(); Error_Ctx err {ec_}; _attr->on_http_error(err); return; } do_write(); } void do_close() { error_code ec; // shutdown the socket _socket.shutdown(tcp::socket::shutdown_both, ec); _socket.close(ec); // ignore not_connected error if (ec && ec != boost::system::errc::not_connected) { cancel_timer(); Error_Ctx err {ec}; _attr->on_http_error(err); return; } // the connection is now closed } private: tcp::socket _socket; }; // class Http #ifdef OB_BELLE_CONFIG_SSL_ON class Https : public Http_Base, public std::enable_shared_from_this { public: Https(net::io_context& io_, std::shared_ptr attr_) : Http_Base(io_, attr_), _socket {std::move(tcp::socket(io_)), attr_->ssl_context} { _close = true; } ~Https() { } Detail::ssl_stream& socket() { return _socket; } Detail::ssl_stream&& socket_move() { return std::move(_socket); } void run() { // start the timer do_timer(); // set server name indication // use SSL_ctrl instead of SSL_set_tlsext_host_name macro // to avoid old style C cast to char* // if (! SSL_set_tlsext_host_name(_socket.native_handle(), _attr->address.data())) if (! SSL_ctrl(_socket.native_handle(), SSL_CTRL_SET_TLSEXT_HOSTNAME, TLSEXT_NAMETYPE_host_name, _attr->address.data())) { error_code ec { static_cast(ERR_get_error()), net::error::get_ssl_category() }; cancel_timer(); Error_Ctx err {ec}; _attr->on_http_error(err); return; } do_resolve(); } void on_connect(error_code ec_) { if (ec_) { cancel_timer(); Error_Ctx err {ec_}; _attr->on_http_error(err); return; } do_handshake(); } void do_handshake() { // perform the ssl handshake _socket.async_handshake(ssl::stream_base::client, net::bind_executor(_strand, [self = this->shared_from_this()](error_code ec) { self->on_handshake(ec); } ) ); } void on_handshake(error_code ec_) { if (ec_) { cancel_timer(); Error_Ctx err {ec_}; _attr->on_http_error(err); return; } _close = false; do_write(); } void do_close() { if (_close) { return; } _close = true; // shutdown the socket _socket.async_shutdown( net::bind_executor(_strand, [self = this->shared_from_this()](error_code ec) { self->on_shutdown(ec); } ) ); } void on_shutdown(error_code ec_) { cancel_timer(); // ignore eof error if (ec_ == net::error::eof) { ec_.assign(0, ec_.category()); } // ignore not_connected error if (ec_ && ec_ != boost::system::errc::not_connected) { return; } // close the socket _socket.next_layer().close(ec_); // ignore not_connected error if (ec_ && ec_ != boost::system::errc::not_connected) { return; } // the connection is now closed } private: Detail::ssl_stream _socket; }; // class Https #endif // OB_BELLE_CONFIG_SSL_ON // default constructor Client() { } // constructor with address and port Client(std::string address_, unsigned short port_) { _attr->address = address_; _attr->port = port_; } #ifdef OB_BELLE_CONFIG_SSL_ON // constructor with address, port, and ssl Client(std::string address_, unsigned short port_, bool ssl_) { _attr->address = address_; _attr->port = port_; _attr->ssl = ssl_; } #endif // OB_BELLE_CONFIG_SSL_ON // destructor ~Client() { } // set the address to connect to Client& address(std::string address_) { _attr->address = address_; return *this; } // get the address to connect to std::string address() { return _attr->address; } // set the port to connect to Client& port(unsigned short port_) { _attr->port = port_; return *this; } // get the port to connect to unsigned short port() { return _attr->port; } // set the socket timeout Client& timeout(std::chrono::seconds timeout_) { _attr->timeout = timeout_; return *this; } // get the socket timeout std::chrono::seconds timeout() { return _attr->timeout; } // set the max timeout Client& timeout_max(std::chrono::milliseconds timeout_max_) { _timeout_max = timeout_max_; return *this; } // get the max timeout std::chrono::milliseconds timeout_max() { return _timeout_max; } // get request queue std::deque& queue() { return _attr->que; } // get the io_context net::io_context& io() { return _io; } #ifdef OB_BELLE_CONFIG_SSL_ON // set ssl Client& ssl(bool ssl_) { _attr->ssl = ssl_; return *this; } // get ssl bool ssl() { return _attr->ssl; } // get the ssl context ssl::context& ssl_context() { return _attr->ssl_context; } // set the ssl context Client& ssl_context(ssl::context&& ctx_) { _attr->ssl_context = std::move(ctx_); return *this; } #endif // OB_BELLE_CONFIG_SSL_ON Client& on_http(Request const& req_, fn_on_http on_http_) { _attr->que.emplace_back(Req_Ctx()); auto& ctx = _attr->que.back(); ctx.req = req_; ctx.on_http = on_http_; return *this; } Client& on_http(Request&& req_, fn_on_http on_http_) { _attr->que.emplace_back(Req_Ctx()); auto& ctx = _attr->que.back(); ctx.req = std::move(req_); ctx.on_http = on_http_; return *this; } Client& on_http(std::string const& target_, fn_on_http on_http_) { this->on_http_impl(Method::get, target_, Request::Params(), Headers(), {}, on_http_); return *this; } Client& on_http(std::string const& target_, Request::Params const& params_, fn_on_http on_http_) { this->on_http_impl(Method::get, target_, params_, Headers(), {}, on_http_); return *this; } Client& on_http(std::string const& target_, Headers const& headers_, fn_on_http on_http_) { this->on_http_impl(Method::get, target_, Request::Params(), headers_, {}, on_http_); return *this; } Client& on_http(std::string const& target_, Request::Params const& params_, Headers const& headers_, fn_on_http on_http_) { this->on_http_impl(Method::get, target_, params_, headers_, {}, on_http_); return *this; } Client& on_http(Method method_, std::string const& target_, std::string const& body_, fn_on_http on_http_) { this->on_http_impl(method_, target_, Request::Params(), Headers(), body_, on_http_); return *this; } Client& on_http(Method method_, std::string const& target_, Request::Params const& params_, std::string const& body_, fn_on_http on_http_) { this->on_http_impl(method_, target_, params_, Headers(), body_, on_http_); return *this; } Client& on_http(Method method_, std::string const& target_, Headers const& headers_, std::string const& body_, fn_on_http on_http_) { this->on_http_impl(method_, target_, Request::Params(), headers_, body_, on_http_); return *this; } Client& on_http(Method method_, std::string const& target_, Request::Params const& params_, Headers const& headers_, std::string const& body_, fn_on_http on_http_) { this->on_http_impl(method_, target_, params_, headers_, body_, on_http_); return *this; } Client& on_http_error(fn_on_http_error on_http_error_) { _attr->on_http_error = on_http_error_; return *this; } std::size_t connect() { if (_attr->que.empty()) { return 0; } #ifdef OB_BELLE_CONFIG_SSL_ON if (_attr->ssl) { // use https std::make_shared(_io, _attr)->run(); } else #endif // OB_BELLE_CONFIG_SSL_ON { // use http std::make_shared(_io, _attr)->run(); } std::size_t size_begin {_attr->que.size()}; if (_timeout_max > std::chrono::milliseconds(0)) { // run for max 'n' amount of time _io.run_until(std::chrono::steady_clock::now() + _timeout_max); } else { _io.run(); } // reset the io_context _io.restart(); std::size_t size_end {_attr->que.size()}; return size_begin - size_end; } private: Client& on_http_impl(Method method_, std::string const& target_, Request::Params const& params_, Headers const& headers_, std::string const& body_, fn_on_http on_http_) { _attr->que.emplace_back(Req_Ctx()); auto& ctx = _attr->que.back(); Request req {method_, target_, 11, body_, headers_}; req.params() = params_; ctx.req = std::move(req); ctx.on_http = on_http_; return *this; } // hold the client attributes std::shared_ptr const _attr {std::make_shared()}; // the io context net::io_context _io {}; // timeout all requests after specified number of milliseconds std::chrono::milliseconds _timeout_max {0}; }; // class Client #endif // OB_BELLE_CONFIG_CLIENT_ON } // namespace OB::Belle #endif // OB_BELLE_HH