diff --git a/init b/init index 140cd5e..46fa86d 100755 --- a/init +++ b/init @@ -7,8 +7,3 @@ cd build cmake .. cd .. -# Install react dependencies -cd react -npm install -cd .. - diff --git a/react-config b/react-config new file mode 100755 index 0000000..86b4bab --- /dev/null +++ b/react-config @@ -0,0 +1,5 @@ +#!/usr/bin/bash +# Install react dependencies +cd react +npm install +cd .. diff --git a/react/package-lock.json b/react/package-lock.json index a97c828..9dd8c5f 100644 --- a/react/package-lock.json +++ b/react/package-lock.json @@ -6449,11 +6449,11 @@ } }, "ext": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", - "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.5.0.tgz", + "integrity": "sha512-+ONcYoWj/SoQwUofMr94aGu05Ou4FepKi7N7b+O8T4jVfyIsZQV1/xeS8jpaBzF0csAk0KLXoHCxU7cKYZjo1Q==", "requires": { - "type": "^2.0.0" + "type": "^2.5.0" }, "dependencies": { "type": { @@ -13007,9 +13007,9 @@ "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" }, "resolve-url-loader": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-3.1.3.tgz", - "integrity": "sha512-WbDSNFiKPPLem1ln+EVTE+bFUBdTTytfQZWbmghroaFNFaAVmGq0Saqw6F/306CwgPXsGwXVxbODE+3xAo/YbA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-3.1.4.tgz", + "integrity": "sha512-D3sQ04o0eeQEySLrcz4DsX3saHfsr8/N6tfhblxgZKXxMT2Louargg12oGNfoTRLV09GXhVUe5/qgA5vdgNigg==", "requires": { "adjust-sourcemap-loader": "3.0.0", "camelcase": "5.3.1", @@ -13017,7 +13017,7 @@ "convert-source-map": "1.7.0", "es6-iterator": "2.0.3", "loader-utils": "1.2.3", - "postcss": "7.0.21", + "postcss": "7.0.36", "rework": "1.0.1", "rework-visit": "1.0.0", "source-map": "0.6.1" @@ -13059,28 +13059,10 @@ "json5": "^1.0.1" } }, - "postcss": { - "version": "7.0.21", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.21.tgz", - "integrity": "sha512-uIFtJElxJo29QC753JzhidoAhvp/e/Exezkdhfmt8AymWT6/5B7W1WmponYWkHk2eg6sONyTch0A3nkMPun3SQ==", - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - }, - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "requires": { - "has-flag": "^3.0.0" - } } } }, @@ -14369,9 +14351,9 @@ "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==" }, "tar": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz", - "integrity": "sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==", + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", + "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", "requires": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -14961,9 +14943,9 @@ } }, "url-parse": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz", - "integrity": "sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz", + "integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==", "requires": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" diff --git a/src/BeastServer.cpp b/src/BeastServer.cpp index c0ec89c..0189c33 100644 --- a/src/BeastServer.cpp +++ b/src/BeastServer.cpp @@ -1,53 +1,33 @@ -// -// Copyright (c) 2016-2017 Vinnie Falco (vinnie dot falco at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/boostorg/beast -// - -//------------------------------------------------------------------------------ -// -// Example: Advanced server -// -//------------------------------------------------------------------------------ #include "pch.h" -#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include #include #include #include #include #include +#include #include #include #include #include "HtmlHelper.hpp" #include "Lv2Log.hpp" +#include #include "BeastServer.hpp" #include "Uri.hpp" -using namespace pipedal; +#include -using tcp = boost::asio::ip::tcp; // from -namespace http = boost::beast::http; // from -namespace websocket = boost::beast::websocket; // from +#include + +using namespace pipedal; +using namespace std; + +using tcp = boost::asio::ip::tcp; // from const size_t MAX_READ_SIZE = 1 * 1024 * 204; using namespace boost; @@ -68,12 +48,12 @@ pipedal::last_modified(const std::filesystem::path &path) } } -std::map extensionsToMimeType = { - {".htm", "text/html"}, - {".html", "text/html"}, +static std::map extensionsToMimeType = { + {".htm", "text/html; charset=UTF-8"}, + {".html", "text/html; charset=UTF-8"}, {".php", "text/html"}, {".css", "text/css"}, - {".txt", "text/plain"}, + {".txt", "text/plain; charset=UTF-8"}, {".js", "application/javascript"}, {".json", "application/json"}, {".xml", "application/xml"}, @@ -88,9 +68,9 @@ std::map extensionsToMimeType = { {".tif", "image/tiff"}, {".svg", "image/svg+xml"}, {".svgz", "image/svg+xml"}, - {".woff", "font/woff2" }, - {".woff2", "font/woff2"} -}; + {".woff", "font/woff2"}, + {".woff2", "font/woff2"}}; + // Return a reasonable mime type based on the extension of a file. std::string mime_type(const std::filesystem::path &path) @@ -106,1144 +86,518 @@ mime_type(const std::filesystem::path &path) } } -// Append an HTTP rel-path to a local filesystem path. -// The returned path is normalized for the platform. -std::filesystem::path -path_cat( - boost::beast::string_view base, - const std::vector &segments) -{ - - std::filesystem::path result = base.to_string(); - - for (const std::string &segment : segments) - { - if (segment.find("..") != -1) - { - throw std::invalid_argument("Invalid segment."); - } - if (segment.find('/') != -1) - throw std::invalid_argument("Invalid segment."); - if (segment.find('\\') != -1) - throw std::invalid_argument("Invalid segment."); - if (segment.find('/') != -1) - throw std::invalid_argument("Invalid segment."); - if (segment.find(':') != -1) - throw std::invalid_argument("Invalid segment."); - - result /= segment; - } - return result; -} - std::string GetFromAddress(const tcp::socket &socket) { std::stringstream s; s << socket.remote_endpoint().address().to_string() << ':' << socket.remote_endpoint().port(); return s.str(); } -// This function produces an HTTP response for the given -// request. The type of the response object depends on the -// contents of the request, so the interface requires the -// caller to pass a generic lambda for receiving the response. -template < - class Body, class Allocator, - class Send> -void handle_request( - const tcp::socket &socket_, - BeastServer *server, - http::request> &&req, - Send &&send) +namespace pipedal { + +class BeastServerImpl : public BeastServer { +private: + std::string address; + int port = -1; + std::filesystem::path rootPath; + int threads = 1; + std::thread *pBgThread = nullptr; + std::recursive_mutex io_mutex; - // Returns a bad request response - auto const bad_request = - [&req, &socket_](boost::beast::string_view why) + boost::asio::io_context *pIoContext = nullptr; + + typedef websocketpp::connection_hdl connection_hdl; + typedef websocketpp::server server; + + class HttpRequestImpl : public HttpRequest { - Lv2Log::error("http - (%s) Bad request: %s,", GetFromAddress(socket_).c_str(), why.to_string().c_str()); - http::response res{http::status::bad_request, req.version()}; - res.set(http::field::server, BOOST_BEAST_VERSION_STRING); - res.set(http::field::content_type, "text/plain"); - 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, &socket_](boost::beast::string_view target) - { - Lv2Log::error("http - (%s) Not found: %s,", GetFromAddress(socket_).c_str(), target.to_string().c_str()); - http::response res{http::status::not_found, req.version()}; - res.set(http::field::server, BOOST_BEAST_VERSION_STRING); - res.set(http::field::content_type, "text/plain"); - 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, &socket_](boost::beast::string_view what) - { - Lv2Log::error("http - (%s) Server error: %s,", GetFromAddress(socket_).c_str(), what.to_string().c_str()); - http::response res{http::status::not_found, req.version()}; - res.set(http::field::server, BOOST_BEAST_VERSION_STRING); - res.set(http::field::content_type, "text/plain"); - res.keep_alive(req.keep_alive()); - res.body() = what.to_string(); - res.prepare_payload(); - return res; - }; - - if (server->HasAccessPointGateway()) - { - auto &gateway = server->GetAccessPointGateway(); - auto remoteAddress = socket_.remote_endpoint().address(); - if (remoteAddress.is_v4()) - { - auto v4Address = remoteAddress.to_v4(); - unsigned int gwAddr = (gateway.netmask().to_uint() & gateway.address().to_uint()); - unsigned int ipAddr = (gateway.netmask().to_uint() & v4Address.to_uint()); - - if ((gateway.netmask().to_uint() & gateway.address().to_uint()) == (gateway.netmask().to_uint() & v4Address.to_uint())) - { - std::string host = req[http::field::host].to_string(); - std::string gatewayHost = server->GetAccessPointServerAddress(); - if (host != gatewayHost) - { - - uri requestUri; - requestUri.set(req.target().begin(), req.target().end()); - if (requestUri.segment_count() != 0) { - return send(not_found(req.target())); - } - uri_builder builder(requestUri); - builder.set_authority(gatewayHost); - - std::string redirectUri = builder.str(); - Lv2Log::info("Access point redirect from" + host + " to " + redirectUri); - - - // redirect to the gateway host. - http::response res{http::status::temporary_redirect, req.version()}; - res.set(http::field::server, BOOST_BEAST_VERSION_STRING); - res.set(http::field::location, redirectUri); - res.prepare_payload(); - return send(std::move(res)); - - } - } - } - } - - if (req.method() == http::verb::options) - { - http::response res{http::status::no_content, req.version()}; - res.set(http::field::server, BOOST_BEAST_VERSION_STRING); - res.set(http::field::access_control_allow_origin, req[http::field::origin]); - res.set(http::field::access_control_allow_methods, "POST, GET, OPTIONS, HEADERS"); - res.set(http::field::access_control_allow_headers, "Content-Type"); - res.keep_alive(req.keep_alive()); - return send(std::move(res)); - } - - uri request_uri; - try - { - request_uri.set(req.target().begin(), req.target().end()); - std::string host = req[http::field::host].to_string(); - int pos = host.find_last_of(':'); - if (pos != std::string::npos) - { - host = host.substr(0, pos); - } - uri_builder builder(request_uri); - builder.set_authority(host); - builder.set_port(socket_.local_endpoint().port()); - // stub: set htts scheme once we implement it. - builder.set_scheme("http"); - - std::string strUri = builder.str(); - request_uri = uri(strUri.c_str()); - } - catch (const std::exception &_) - { - return send((bad_request("Malformed target."))); - } - - for (auto requestHandler : server->RequestHandlers()) - { - - if (requestHandler->wants(req.method(), request_uri)) - { - - if (req.method() == http::verb::head) - { - Lv2Log::info("http - (%s) head %s", GetFromAddress(socket_), request_uri.str().c_str()); - - boost::beast::error_code ec; - http::response res{http::status::ok, req.version()}; - res.set(http::field::server, BOOST_BEAST_VERSION_STRING); - res.set(http::field::date, HtmlHelper::timeToHttpDate(time(nullptr))); - res.set(http::field::access_control_allow_origin, req[http::field::origin]); - requestHandler->head_response(request_uri, req, res, ec); - res.keep_alive(req.keep_alive()); - // 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())); - - if (ec) - { - return send(server_error(ec.message())); - } - req.prepare_payload(); - return send(std::move(res)); - } - else if (req.method() == http::verb::get) - { - if (server->LogHttpRequests()) - { - Lv2Log::info("http - (%s) get %s", GetFromAddress(socket_).c_str(), request_uri.str().c_str()); - } - - boost::beast::error_code ec; - http::response res{http::status::ok, req.version()}; - res.set(http::field::server, BOOST_BEAST_VERSION_STRING); - res.set(http::field::date, HtmlHelper::timeToHttpDate(time(nullptr))); - res.set(http::field::access_control_allow_origin, req[http::field::origin]); - requestHandler->get_response(request_uri, req, res, ec); - res.keep_alive(req.keep_alive()); - if (ec == boost::system::errc::no_such_file_or_directory) - return send(not_found(req.target())); - - if (ec) - { - return send(server_error(ec.message())); - } - res.prepare_payload(); - return send(std::move(res)); - } - else if (req.method() == http::verb::post) - { - if (server->LogHttpRequests()) - { - Lv2Log::info("http - (%s) post %s", GetFromAddress(socket_).c_str(), request_uri.str().c_str()); - } - - boost::beast::error_code ec; - http::response res{http::status::ok, req.version()}; - res.set(http::field::server, BOOST_BEAST_VERSION_STRING); - res.set(http::field::date, HtmlHelper::timeToHttpDate(time(nullptr))); - res.set(http::field::access_control_allow_origin, req[http::field::origin]); - requestHandler->post_response(request_uri, req, res, ec); - res.keep_alive(false); // bug in Beast Server -- posted body not reset. - if (ec == boost::system::errc::no_such_file_or_directory) - return send(not_found(req.target())); - - if (ec) - { - return send(server_error(ec.message())); - } - res.prepare_payload(); - return send(std::move(res)); - } - return send(bad_request("Unknown HTTP-method")); - } - } - // 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(not_found(req.target())); // security: report not found to avoid disclosing info. - - // Build the path to the requested file - auto path = path_cat(server->GetDocRoot().c_str(), request_uri.segments()); - if (req.target().back() == '/' || req.target().size() == 0) - path /= "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())); - - // Respond to HEAD request - if (req.method() == http::verb::head) - { - if (server->LogHttpRequests()) - { - Lv2Log::info("http - (%s) head %s", GetFromAddress(socket_).c_str(), request_uri.str().c_str()); - } - - http::response res{http::status::ok, req.version()}; - res.set(http::field::server, BOOST_BEAST_VERSION_STRING); - res.set(http::field::date, HtmlHelper::timeToHttpDate(time(nullptr))); - res.set(http::field::content_type, mime_type(path)); - res.set(http::field::last_modified, last_modified(path)); - res.content_length(body.size()); - res.keep_alive(req.keep_alive()); - return send(std::move(res)); - } - else if (req.method() == http::verb::get) - { - if (server->LogHttpRequests()) - { - Lv2Log::info("http - (%s) get %s", GetFromAddress(socket_).c_str(), request_uri.str().c_str()); - } - - // Respond to GET request - http::response 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::date, HtmlHelper::timeToHttpDate(time(nullptr))); - res.set(http::field::content_type, mime_type(path)); - res.set(http::field::last_modified, last_modified(path)); - res.content_length(body.size()); - res.keep_alive(req.keep_alive()); - return send(std::move(res)); - } - else - { - return send(bad_request("Unknown HTTP-method")); - } -} - -//------------------------------------------------------------------------------ - -// Echoes back all received WebSocket messages -class websocket_session : public std::enable_shared_from_this, private SocketHandler::IWriteCallback -{ - websocket::stream ws_; - boost::asio::strand< - boost::asio::io_context::executor_type> - strand_; - boost::asio::steady_timer timer_; - boost::beast::multi_buffer read_buffer_; - boost::beast::multi_buffer write_buffer_; - - BeastServer *beastServer_; - std::shared_ptr socket_handler_; - - std::queue writeQueue; - - bool writePending = false; - ; - std::mutex writeQueueMutex; - std::string fromAddress; - -public: - ~websocket_session() - { - Lv2Log::info("http - (%s) Websocket closed.", fromAddress.c_str()); - } - virtual void close() - { - this->ws_.close(websocket::close_reason()); - } - virtual std::string getFromAddress() const { - return this->fromAddress; - } - virtual void writeCallback(const std::string &text) - { - { - std::lock_guard guard(writeQueueMutex); - if (writePending) - { - writeQueue.push(text); - return; - } - writePending = true; - } - do_write(text); - } - void do_write(const std::string &text) - { - size_t n = buffer_copy(write_buffer_.prepare(text.size()), boost::asio::buffer(text)); - write_buffer_.commit(n); - ws_.text(true); - - ws_.async_write( - write_buffer_.data(), - boost::asio::bind_executor( - strand_, - std::bind( - &websocket_session::on_write, - shared_from_this(), - std::placeholders::_1, - std::placeholders::_2))); - } - // Take ownership of the socket - explicit websocket_session( - const std::string &fromAddress, - BeastServer *beastServer, - std::shared_ptr &&socketHandler, - tcp::socket socket) - : beastServer_(beastServer), socket_handler_(socketHandler), - fromAddress(fromAddress), - ws_(std::move(socket)), - strand_(ws_.get_executor()), - read_buffer_(MAX_READ_SIZE), - timer_(ws_.get_executor().context(), - (std::chrono::steady_clock::time_point::max)()) - { - } - - // Start the asynchronous operation - template - void - run(http::request> req) - { - // Run the timer. The timer is operated - // continuously, this simplifies the code. - on_timer({}); - - // Set the timer - timer_.expires_after(std::chrono::seconds(15)); - // Accept the websocket handshake - - ws_.async_accept( - req, - boost::asio::bind_executor( - strand_, - std::bind( - &websocket_session::on_accept, - shared_from_this(), - std::placeholders::_1))); - } - - // Called when the timer expires. - void - on_timer(boost::system::error_code ec) - { - if (ec && ec != boost::asio::error::operation_aborted) - this->beastServer_->onError(ec, "timer"); - return; - - // Verify that the timer really expired since the deadline may have moved. - if (timer_.expiry() <= std::chrono::steady_clock::now()) - { - // Closing the socket cancels all outstanding operations. They - // will complete with boost::asio::error::operation_aborted - ws_.next_layer().shutdown(tcp::socket::shutdown_both, ec); - ws_.next_layer().close(ec); - return; - } - - // Wait on the timer - timer_.async_wait( - boost::asio::bind_executor( - strand_, - std::bind( - &websocket_session::on_timer, - shared_from_this(), - std::placeholders::_1))); - } - - void - on_accept(boost::system::error_code ec) - { - // Happens when the timer closes the socket - if (ec == boost::asio::error::operation_aborted) - return; - - if (ec) - { - beastServer_->onError(ec, "accept"); - return; - } - if (socket_handler_) - { - auto t = socket_handler_.get(); - t->setWriteCallback(this); - socket_handler_->onAttach(); - } - - // Read a message - do_read(); - } - - void - do_read() - { - // Set the timer - timer_.expires_after(std::chrono::seconds(15)); - - // Read a message into our buffer - ws_.async_read( - read_buffer_, - boost::asio::bind_executor( - strand_, - std::bind( - &websocket_session::on_read, - shared_from_this(), - std::placeholders::_1, - std::placeholders::_2))); - } - - void - on_read( - boost::system::error_code ec, - std::size_t bytes_transferred) - { - - // Happens when the timer closes the socket - if (ec == boost::asio::error::operation_aborted) - return; - - // This indicates that the websocket_session was closed - if (ec == websocket::error::closed) - return; - - if (ec) - { - beastServer_->onError(ec, "read"); - return; - } - - // Echo the message - if (ws_.got_text()) - { - std::string data(beast::buffers_to_string(read_buffer_.data())); - this->socket_handler_->onReceive(data); - read_buffer_.consume(bytes_transferred); - } - else - { - read_buffer_.consume(bytes_transferred); // ignore binary messages - } - - do_read(); - // ws_.text(ws_.got_text()); - // ws_.async_write( - // buffer_.data(), - // boost::asio::bind_executor( - // strand_, - // std::bind( - // &websocket_session::on_write, - // shared_from_this(), - // std::placeholders::_1, - // std::placeholders::_2))); - } - - void fail(boost::system::error_code ec, const char *what) - { - beastServer_->onError(ec, what); - } - void - on_write( - boost::system::error_code ec, - std::size_t bytes_transferred) - { - boost::ignore_unused(bytes_transferred); - - // Happens when the timer closes the socket - if (ec == boost::asio::error::operation_aborted) - return; - - if (ec) - { - fail(ec, "write"); - } - - // Clear the buffer - std::string nextWrite; - write_buffer_.consume(write_buffer_.size()); - - { - std::lock_guard guard(this->writeQueueMutex); - if (writeQueue.size() != 0) - { - nextWrite = writeQueue.front(); - writeQueue.pop(); - } - else - { - writePending = false; - return; - } - } - do_write(nextWrite); - } -}; - -// Handles an HTTP server connection -class http_session : public std::enable_shared_from_this -{ - // This queue is used for HTTP pipelining. - class queue - { - enum - { - // Maximum number of responses we will queue - limit = 8 - }; - - // The type-erased, saved work item - struct work - { - virtual ~work() = default; - virtual void operator()() = 0; - }; - - http_session &self_; - std::vector> items_; + private: + const server::connection_type::request_type &m_request; public: - explicit queue(http_session &self) - : self_(self) + HttpRequestImpl(const server::connection_type::request_type &request) + : m_request(request) { - static_assert(limit > 0, "queue limit must be positive"); - items_.reserve(limit); } - // Returns `true` if we have reached the queue limit - bool - is_full() const + virtual const std::string &body() const { return m_request.get_body(); } + virtual const std::string &method() const { return m_request.get_method(); } + virtual const std::string &get(const std::string &key) const { return m_request.get_header(key); } + virtual bool keepAlive() const { - return items_.size() >= limit; + m_request.get_version() != "1.0" || m_request.get_header("Connection") == "keep-alive"; + return true; } + }; + class HttpResponseImpl : public HttpResponse + { + server::connection_type &request; - // Called when a message finishes sending - // Returns `true` if the caller should initiate a read - bool - on_write() + public: + HttpResponseImpl(server::connection_type &request) + : request(request) { - BOOST_ASSERT(!items_.empty()); - auto const was_full = is_full(); - items_.erase(items_.begin()); - if (!items_.empty()) - (*items_.front())(); - return was_full; } - - // Called by the HTTP handler to send a response. - template - void - operator()(http::message &&msg) + virtual void set(const std::string &key, const std::string &value) { request.replace_header(key, value); } + virtual void setContentLength(size_t size) { - // This holds a work item - struct work_impl : work + std::stringstream ss; + ss << size; + request.replace_header(HttpField::content_length, ss.str()); + } + virtual void setBody(const std::string &body) { request.set_body(body); } + virtual void keepAlive(bool value) + { + if (!value) { - http_session &self_; - http::message msg_; - - work_impl( - http_session &self, - http::message &&msg) - : self_(self), msg_(std::move(msg)) - { - } - - void - operator()() - { - http::async_write( - self_.socket_, - msg_, - boost::asio::bind_executor( - self_.strand_, - std::bind( - &http_session::on_write, - self_.shared_from_this(), - std::placeholders::_1, - msg_.need_eof()))); - } - }; - - // Allocate and store the work - items_.emplace_back(new work_impl(self_, std::move(msg))); - - // If there was no previous work, start this one - if (items_.size() == 1) - (*items_.front())(); + set("Connection", "close"); + } } }; - tcp::socket socket_; - boost::asio::strand< - boost::asio::io_context::executor_type> - strand_; - boost::asio::steady_timer timer_; - boost::beast::flat_buffer buffer_; - http::request req_; - queue queue_; - BeastServer *beastServer_; + server m_endpoint; + bool logHttpRequests = false; -public: - // Take ownership of the socket - explicit http_session( - BeastServer *beastServer, - tcp::socket socket) - : beastServer_(beastServer), socket_(std::move(socket)), strand_(socket_.get_executor()), timer_(socket_.get_executor().context(), - (std::chrono::steady_clock::time_point::max)()), - queue_(*this) + + class WebSocketSession : public std::enable_shared_from_this, public SocketHandler::IWriteCallback { - } - - // Start the asynchronous operation - void - run() - { - // Run the timer. The timer is operated - // continuously, this simplifies the code. - on_timer({}); - - do_read(); - } - - void - do_read() - { - // make the request empty before reading - // otherwise the operation behavour is undefined. - req_ = {}; - - // Set the time - timer_.expires_after(std::chrono::seconds(15)); - - // Read a request - http::async_read(socket_, buffer_, req_, - boost::asio::bind_executor( - strand_, - std::bind( - &http_session::on_read, - shared_from_this(), - std::placeholders::_1))); - } - - // Called when the timer expires. - void - on_timer(boost::system::error_code ec) - { - if (ec && ec != boost::asio::error::operation_aborted) - return fail(ec, "timer"); - - // Verify that the timer really expired since the deadline may have moved. - if (timer_.expiry() <= std::chrono::steady_clock::now()) - { - // Closing the socket cancels all outstanding operations. They - // will complete with boost::asio::error::operation_aborted - if (!beastServer_->Stopping()) - { - socket_.shutdown(tcp::socket::shutdown_both, ec); - } - socket_.close(ec); - return; + BeastServerImpl *pServer; + server::connection_ptr webSocket; + std::string fromAddress; + std::shared_ptr socketHandler; + private: + // IWriteCallback + virtual void close() { webSocket->close(websocketpp::close::status::normal, ""); } + + virtual void writeCallback(const std::string& text) { + webSocket->send(text,websocketpp::frame::opcode::text); } + virtual std::string getFromAddress() const { + return fromAddress; + } + public: + ~WebSocketSession() { - // Wait on the timer - timer_.async_wait( - boost::asio::bind_executor( - strand_, - std::bind( - &http_session::on_timer, - shared_from_this(), - std::placeholders::_1))); - } - - void fail(boost::system::error_code ec, const char *what) - { - beastServer_->onError(ec, what); - } - - void - on_read(boost::system::error_code ec) - { - // Happens when the timer closes the socket - if (ec == boost::asio::error::operation_aborted) - return; - - // This means they closed the connection - if (ec == http::error::end_of_stream) - return do_close(); - - if (ec) - { - fail(ec, "read"); - return; } - // See if it is a WebSocket Upgrade - if (websocket::is_upgrade(req_)) + using ptr = std::shared_ptr; + WebSocketSession(BeastServerImpl *pServer, server::connection_ptr &webSocket) + : pServer(pServer), + webSocket(webSocket) { - uri request_uri; - try + uri requestUri(webSocket->get_uri()->str().c_str()); + fromAddress = requestUri.authority(); + + auto pFactory = pServer->GetSocketFactory(requestUri); + if (!pFactory) { - request_uri.set(req_.target().begin(), req_.target().end()); - } - catch (const std::exception &_) - { - fail(ec, "socket uri"); + Lv2Log::error("Invalid request: " + requestUri.str()); + webSocket->close(websocketpp::close::status::normal, "Invalid request."); return; } - - std::shared_ptr socketFactory = beastServer_->GetSocketFactory(request_uri); - if (!socketFactory) - { - fail(http::error::bad_target, "Not found"); - return; - } - - Lv2Log::info("http - (%s) Websocket opened: %s", GetFromAddress(socket_).c_str(), request_uri.str().c_str()); - // Create a WebSocket websocket_session by transferring the socket - std::string fromName = GetFromAddress(socket_); - std::make_shared( - fromName, - this->beastServer_, - socketFactory ? socketFactory->CreateHandler(request_uri) : nullptr, - std::move(socket_)) - ->run(std::move(req_)); - return; - } - // Send the response - handle_request( - this->socket_, - beastServer_, - std::move(req_), - queue_); - - // If we aren't at the queue limit, try to pipeline another request - if (!queue_.is_full()) - do_read(); - } - - void - on_write(boost::system::error_code ec, bool close) - { - // Happens when the timer closes the socket - if (ec == boost::asio::error::operation_aborted) - return; - - if (ec) - return fail(ec, "write"); - - if (close) - { - // This means we should close the connection, usually because - // the response indicated the "Connection: close" semantic. - return do_close(); - } - - // Inform the queue that a write completed - if (queue_.on_write()) - { - // Read another request - do_read(); - } - } - - void - do_close() - { - // Send a TCP shutdown - boost::system::error_code ec; - if (!beastServer_->Stopping()) - { - socket_.shutdown(tcp::socket::shutdown_send, ec); - } - socket_.close(); - - - } -}; - -//------------------------------------------------------------------------------ - -// Accepts incoming connections and launches the sessions -class listener : public std::enable_shared_from_this -{ - tcp::acceptor acceptor_; - tcp::socket socket_; - BeastServer *beastServer_; - -public: - ~listener() - { - beastServer_->onListenerClosed(); - } - listener( - BeastServer *server, - boost::asio::io_context &ioc, - tcp::endpoint endpoint) - : beastServer_(server), acceptor_(ioc), socket_(ioc) - { - boost::system::error_code ec; - - // Open the acceptor - acceptor_.open(endpoint.protocol(), ec); - if (ec) - { - fail(ec, "open"); - return; - } - - boost::asio::socket_base::reuse_address option(true); - acceptor_.set_option(option); - // Lv2Log::warning("http - Accepting connections with reuse_adddress to allow faster debug turns. Should NOT be enabled in production use. "); - - // Leftover client sockets in TIME_WAIT state prevent binding of the server address. - // Retry for up to 140 seconds in order to allow the TIME_WAIT sockets to expire. - // If we still can't establish a connection, we can be sure that somebody else or an existing instance is servicing the address, so - // quit after that. - int retries = 0; - int retryTime = 1; - int totalRetryTime = 0; - int MAX_RETRY_TIME = 240; // socket-timeout-ish. - - while (true) - { - // Bind to the server address - acceptor_.bind(endpoint, ec); - if (!ec) - { - break; - } else { - ++retries; - if (totalRetryTime > MAX_RETRY_TIME) + using websocketpp::lib::bind; + using websocketpp::lib::placeholders::_1; + using websocketpp::lib::placeholders::_2; + + webSocket->set_message_handler(bind(&WebSocketSession::on_message, this, _1, _2)); + webSocket->set_close_handler(bind(&WebSocketSession::on_close,this,_1)); + + socketHandler = pFactory->CreateHandler(requestUri); + socketHandler->setWriteCallback(this); + } + } + void on_close(connection_hdl hdl) + { + auto shThis = this->shared_from_this(); // we will destruct as we return. + pServer->on_session_closed(shThis,hdl); + } + void on_message(connection_hdl hdl, server::message_ptr msg) + { + const std::string& data = msg->get_payload(); + if (socketHandler) + { + std::string_view stringView(data.c_str()); + socketHandler->onReceive(stringView); + } + } + }; + + std::set > m_sessions; + + void on_session_closed(WebSocketSession::ptr session,connection_hdl hConnection) + { + m_sessions.erase(session); + m_connections.erase(hConnection); + + } + void NotFound(server::connection_type &connection, const std::string &filename) + { + // 404 error + std::stringstream ss; + + ss << "" + << "Error 404 (Resource not found)" + << "

Error 404

" + << "

The requested URL " << HtmlHelper::HtmlEncode(filename) << " was not found on this server.

" + << ""; + + std::string body = ss.str(); + connection.set_body(body); + std::stringstream ssLen; + ssLen << body.length(); + connection.replace_header(HttpField::content_length, ssLen.str()); + connection.set_status(websocketpp::http::status_code::not_found); + return; + }; + void ServerError(server::connection_type &connection, const std::string &error) + { + // 404 error + std::stringstream ss; + + ss << "" + << "Error 500 (Server error)" + << "

Error 500

" + << "

" << HtmlHelper::HtmlEncode(error) << "

" + << ""; + std::string body = ss.str(); + connection.set_body(body); + std::stringstream ssLen; + ssLen << body.length(); + connection.replace_header(HttpField::content_length, ssLen.str()); + + connection.set_status(websocketpp::http::status_code::internal_server_error); + return; + }; + + void on_http(connection_hdl hdl) + { + // Upgrade our connection handle to a full connection_ptr + + server::connection_ptr con = m_endpoint.get_con_from_hdl(hdl); + auto &request = con->get_request(); + + std::string origin = con->get_request_header(HttpField::origin); + + if (logHttpRequests) + { + tcp::endpoint fromAddress = con->get_socket().remote_endpoint(); + stringstream ss; + ss << "http - " << fromAddress << "; " << request.get_method() << "; " << con->get_uri()->str(); + } + std::shared_ptr connectionUri = con->get_uri(); + + uri requestUri(con->get_uri()->str().c_str()); + + HttpRequestImpl req(con->get_request()); + HttpResponseImpl res((*con)); + + if (req.method() == HttpVerb::options) + { + res.set(HttpField::access_control_allow_origin, origin); + res.set(HttpField::access_control_allow_methods, "POST, GET, OPTIONS, HEADERS"); + res.set(HttpField::access_control_allow_headers, "Content-Type"); + res.keepAlive(req.keepAlive()); + con->set_status(websocketpp::http::status_code::ok); + return; + } + for (auto requestHandler : this->request_handlers) + { + + if (requestHandler->wants(req.method(), requestUri)) + { + + if (req.method() == HttpVerb::head) { - fail(ec, "bind"); + std::error_code ec; + res.set(HttpField::date, HtmlHelper::timeToHttpDate(time(nullptr))); + res.set(HttpField::access_control_allow_origin, origin); + + requestHandler->head_response(requestUri, req, res, ec); + res.keepAlive(req.keepAlive()); + if (ec == std::errc::no_such_file_or_directory) + { + NotFound(*con, requestUri.str()); + return; + } + + if (ec) + { + ServerError(*con, ec.message()); + } + con->set_status(websocketpp::http::status_code::ok); return; } - Lv2Log::info("http - bind: %s. (port %d) Retrying...", ec.message().c_str(), endpoint.port()); - sleep(retryTime); - retryTime *= 2; - if (retryTime > 10) - retryTime = 10; - totalRetryTime += retryTime; + else if (req.method() == HttpVerb::get) + { + std::error_code ec; + res.set(HttpField::date, HtmlHelper::timeToHttpDate(time(nullptr))); + res.set(HttpField::access_control_allow_origin, origin); + requestHandler->get_response(requestUri, req, res, ec); + res.keepAlive(req.keepAlive()); + + if (ec == std::errc::no_such_file_or_directory) + { + NotFound(*con, requestUri.str()); + return; + } + + if (ec) + { + ServerError(*con, ec.message()); + return; + } + con->set_status(websocketpp::http::status_code::ok); + return; + } + else if (req.method() == HttpVerb::post) + { + std::error_code ec; + res.keepAlive(req.keepAlive()); + res.set(HttpField::date, HtmlHelper::timeToHttpDate(time(nullptr))); + res.set(HttpField::access_control_allow_origin, origin); + + requestHandler->post_response(requestUri, req, res, ec); + + if (ec == std::errc::no_such_file_or_directory) + { + NotFound(*con, requestUri.str()); + return; + } + + if (ec) + { + ServerError(*con, ec.message()); + return; + } + con->set_status(websocketpp::http::status_code::ok); + + return; + } + ServerError(*con, "Unknown HTTP-Method"); + return; } } - Lv2Log::info("http - Server listening on %s:%d", endpoint.address().to_string().c_str(), (int)endpoint.port()); - - // Start listening for connections - acceptor_.listen( - boost::asio::socket_base::max_listen_connections, ec); - if (ec) + std::ifstream file; + std::filesystem::path filename = con->get_resource(); + std::string response; + if (requestUri.segment_count() == 0) { - fail(ec, "listen"); - return; - } - } - - void - stop() - { - acceptor_.cancel(); - } - - // Start accepting incoming connections - void - run() - { - if (!acceptor_.is_open()) - return; - do_accept(); - } - - void - do_accept() - { - acceptor_.async_accept( - socket_, - std::bind( - &listener::on_accept, - shared_from_this(), - std::placeholders::_1)); - } - - void fail(boost::system::error_code ec, const char *what) - { - beastServer_->onError(ec, what); - } - void - on_accept(boost::system::error_code ec) - { - if (ec) - { - fail(ec, "accept"); - return; + filename = this->rootPath / "index.html"; } else { - // Create the http_session and run it - std::make_shared( - beastServer_, - std::move(socket_)) - ->run(); + filename = this->rootPath; + for (size_t i = 0; i < requestUri.segment_count(); ++i) + { + filename /= requestUri.segment(i); + } } - // Accept another connection - do_accept(); + if (req.method() != HttpVerb::get) + { + ServerError(*con, "Unknown HTTP-Method"); + return; + } + std::string mimeType = mime_type(filename); + + file.open(filename.c_str(), std::ios::in); + if (!file) + { + NotFound(*con, requestUri.str()); + return; + } + + file.seekg(0, std::ios::end); + response.reserve(file.tellg()); + file.seekg(0, std::ios::beg); + + response.assign((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + + res.set("Content-Type", mimeType); + + res.set(HttpField::access_control_allow_origin, origin); + res.set(HttpField::date, HtmlHelper::timeToHttpDate(time(nullptr))); + res.setContentLength(response.length()); + + con->set_body(response); + con->set_status(websocketpp::http::status_code::ok); } -}; -//------------------------------------------------------------------------------ + typedef std::set> con_list; -BeastServer::BeastServer(const boost::asio::ip::address &address, int port, const char *rootPath, int threads) - : address(address), - port((unsigned short)port), - doc_root(rootPath), - threads(threads), - pBgThread(nullptr) -{ -} + con_list m_connections; -void BeastServer::Stop() -{ - if (stopping) return; - stopping = true; - listener *p = nullptr; + + void on_open(connection_hdl hdl) { - std::unique_lock lk(io_mutex); - p = (listener*)this->pListener; - this->pListener = nullptr; + m_connections.insert(hdl); + + server::connection_ptr webSocket = m_endpoint.get_con_from_hdl(hdl); + + WebSocketSession::ptr socketSession = std::make_shared(this, webSocket); + m_sessions.insert(socketSession); } - if (p != nullptr) + + void on_fail(connection_hdl hdl) { - p->stop(); } - boost::asio::io_context *pIoContext = nullptr; + void on_close(connection_hdl hdl) { - std::unique_lock lk(io_mutex); - pIoContext = this->pIoContext; - this->pIoContext = nullptr; + m_connections.erase(hdl); } - if (pIoContext != nullptr) + + void Run() { - pIoContext->stop(); - }; -} + try + { + // The io_context is required for all I/O + boost::asio::io_service ioc{threads}; + //********************************* -void BeastServer::onListenerClosed() -{ - std::unique_lock lk(io_mutex); - this->pListener = nullptr; -} -static void ThreadProc(BeastServer *server) -{ - server->Run(); -} + m_endpoint.set_reuse_addr(true); -void BeastServer::RunInBackground() -{ - if (this->pBgThread != nullptr) - { - throw std::runtime_error("Bad state."); + + m_endpoint.clear_access_channels(websocketpp::log::alevel::all); + //m_endpoint.set_access_channels(websocketpp::log::alevel::access_core); + m_endpoint.set_access_channels(websocketpp::log::alevel::fail); + + m_endpoint.init_asio(&ioc); + + // Bind the handlers we are using + using websocketpp::lib::bind; + using websocketpp::lib::placeholders::_1; + using websocketpp::lib::placeholders::_2; + m_endpoint.set_open_handler(bind(&BeastServerImpl::on_open, this, _1)); + m_endpoint.set_close_handler(bind(&BeastServerImpl::on_close, this, _1)); + m_endpoint.set_http_handler(bind(&BeastServerImpl::on_http, this, _1)); + + std::stringstream ss; + ss << port; + + m_endpoint.listen(this->address, ss.str()); + m_endpoint.start_accept(); + + // Start IOC service threads. + std::vector v; + v.reserve(threads - 1); + for (auto i = threads - 1; i > 0; --i) + v.emplace_back( + [&ioc] + { + ioc.run(); + }); + + // Start the ASIO io_service run loop + + { + std::stringstream ss; + ss << "Listening on " << this->address << ":" << this->port; + Lv2Log::info(ss.str()); + } + m_endpoint.run(); + //ioc.run(); + /****************** */ + + ioc.stop(); + for (auto &thread : v) + { + thread.join(); + } + Lv2Log::info("Web server terminated."); + + } + catch (websocketpp::exception const &e) + { + std::cout << e.what() << std::endl; + return; + } } - this->pBgThread = new std::thread(ThreadProc, this); -} -void BeastServer::Join() -{ - if (this->pBgThread != nullptr) + static void ThreadProc(BeastServerImpl *server) { - Stop(); - this->pBgThread->join(); + server->Run(); + } + std::shared_ptr GetSocketFactory(const uri &requestUri); + + std::vector> request_handlers; + std::vector> socket_factories; + + virtual void AddRequestHandler(std::shared_ptr requestHandler) + { + request_handlers.push_back(requestHandler); + } + + virtual void AddSocketFactory(std::shared_ptr &socketHandler) + { + socket_factories.push_back(socketHandler); + } + +public: + virtual void SetLogHttpRequests(bool enableLogging) + { + this->logHttpRequests = enableLogging; + } + + virtual void ShutDown(int timeoutMs) + { + + m_endpoint.stop_listening(); + for (auto &connection : m_connections) + { + + for (auto it = m_connections.begin(); it != m_connections.end(); ++it) + { + m_endpoint.close(*it, websocketpp::close::status::normal, ""); + } + } + } + virtual void Join() + { + if (this->pBgThread) + { + this->pBgThread->join(); + } this->pBgThread = nullptr; } -} - -void BeastServer::Run() -{ - // The io_context is required for all I/O - boost::asio::io_context ioc{threads}; + virtual void RunInBackground() { - // save a reference for use by stop() - std::unique_lock lk(io_mutex); - pIoContext = &ioc; + if (this->pBgThread != nullptr) + { + throw std::runtime_error("Bad state."); + } + this->pBgThread = new std::thread(ThreadProc, this); } - // Create and launch a listening port - std::shared_ptr listenerInstance = std::make_shared( - this, - ioc, - tcp::endpoint{address, port}); - listenerInstance->run(); - - // save a reference for use by stop() + BeastServerImpl(const std::string &address, int port, const char *rootPath, int threads) + : address(address), + rootPath(rootPath), + port(port), + threads(threads) { - std::unique_lock lk(io_mutex); - this->pListener = (void *)&(*listenerInstance); } +}; +} // namespace pipedal - // Run the I/O service on the requested number of threads - std::vector v; - v.reserve(threads - 1); - for (auto i = threads - 1; i > 0; --i) - v.emplace_back( - [&ioc] - { - ioc.run(); - }); - ioc.run(); - - ioc.stop(); - for (auto &thread : v) - { - thread.join(); - } - { - std::unique_lock lk(io_mutex); - this->pListener = nullptr; - this->pIoContext = nullptr; - } -} - -BeastServer::~BeastServer() +std::shared_ptr BeastServerImpl::GetSocketFactory(const uri &requestUri) { - // Shutdown if running in background. - Stop(); - Join(); -} -void BeastServer::onWarning(boost::system::error_code ec, char const *what) -{ - std::stringstream s; - s << "http - " << what << ": " << ec.message() << " (" << ec.value() << ')'; - Lv2Log::warning(s.str().c_str()); -} -void BeastServer::onInfo(char const *message) -{ - Lv2Log::info("%s", message); -} - -void BeastServer::onError(boost::system::error_code ec, char const *what) -{ - std::stringstream s; - s << "http - " << what << ": " << ec.message() << " (" << ec.value() << ')'; - Lv2Log::warning(s.str().c_str()); -} - -std::shared_ptr BeastServer::GetSocketFactory(const uri &request_uri) -{ for (auto factory : this->socket_factories) { - if (factory->wants(request_uri)) + if (factory->wants(requestUri)) { return factory; } } return nullptr; } + +std::shared_ptr pipedal::createBeastServer(const boost::asio::ip::address &address, int port, const char *rootPath, int threads) +{ + return std::shared_ptr(new BeastServerImpl(address.to_string(), port, rootPath, threads)); +} diff --git a/src/BeastServer.hpp b/src/BeastServer.hpp index 185108f..d279055 100644 --- a/src/BeastServer.hpp +++ b/src/BeastServer.hpp @@ -1,7 +1,10 @@ #pragma once + +#include + #include -#include #include +#include #include @@ -11,17 +14,63 @@ #include + + + // forward declaration. class websocket_session; namespace pipedal { -using HttpRequest =boost::beast::http::request; +class HttpRequest { +public: + virtual const std::string&body() const = 0; + virtual const std::string &method() const = 0; + virtual const std::string&get(const std::string&key) const = 0; + virtual bool keepAlive() const = 0; +}; + +class HttpResponse { +public: + virtual void set(const std::string&key, const std::string&value) = 0; + virtual void setContentLength(size_t size) = 0; + virtual void setBody(const std::string&body) = 0; + virtual void keepAlive(bool value) = 0; +}; + + +struct HttpVerb { + constexpr static const char * options = "OPTIONS"; + constexpr static const char * get = "GET"; + constexpr static const char * post = "POST"; + constexpr static const char * head = "HEAD"; +}; + + +class HttpField { +public: + constexpr static const char * LastModified = "Last-Modified"; + constexpr static const char* content_length = "Content-Length"; + constexpr static const char* content_type = "Content-Type"; + constexpr static const char* cache_control = "Cache-Control"; + constexpr static const char* content_disposition = "Content-Disposition"; + constexpr static const char* access_control_allow_origin = "Access-Control-Allow-Origin"; + constexpr static const char* access_control_allow_methods= "Access-Control-Allow-Methods"; + constexpr static const char* access_control_allow_headers = "Acess-Control-Allow-Headers"; + constexpr static const char* origin = "Origin"; + constexpr static const char* date = "Date"; +}; + + +//xxx move this to HtmlHelpers. std::string last_modified(const std::filesystem::path& path); +class BeastServerImpl; + class SocketHandler { + friend class BeastServerImpl; public: class IWriteCallback { @@ -33,16 +82,15 @@ public: }; private: - friend class ::websocket_session; IWriteCallback *writeCallback_; - +public: void setWriteCallback(IWriteCallback *writeCallback) { writeCallback_ = writeCallback; } -protected: +public: virtual void onReceive(const std::string_view&text) = 0; public: std::string getFromAddress() const { return writeCallback_->getFromAddress(); } @@ -76,6 +124,7 @@ class RequestHandler { private: uri target_url_; public: + RequestHandler(const char*target_url) : target_url_(target_url) { @@ -92,7 +141,7 @@ public: return requestUri; } - virtual bool wants(boost::beast::http::verb verb,const uri &request_uri) const { + virtual bool wants(const std::string& method,const uri &request_uri) const { if (request_uri.segment_count() < target_url_.segment_count()) { return false; @@ -107,105 +156,48 @@ public: return true; } + virtual void head_response( const uri&request_uri, - const boost::beast::http::request &req, - boost::beast::http::response &res, - boost::beast::error_code &ec) = 0; + const HttpRequest &req, + HttpResponse &res, + std::error_code &ec) = 0; virtual void get_response( const uri&request_uri, - const boost::beast::http::request &req, - boost::beast::http::response &res, - boost::beast::error_code &ec) = 0; + const HttpRequest &req, + HttpResponse &res, + std::error_code &ec) = 0; virtual void post_response( const uri&request_uri, - const boost::beast::http::request &req, - boost::beast::http::response &res, - boost::beast::error_code &ec) + const HttpRequest &req, + HttpResponse &res, + std::error_code &ec) { get_response(request_uri,req,res,ec); } - - }; + class BeastServer { -private: - boost::asio::ip::address address; - unsigned short port; - std::string doc_root; - int threads; - std::mutex log_mutex; - std::vector > request_handlers; - std::vector > socket_factories; - std::mutex io_mutex; - boost::asio::io_context *pIoContext = nullptr; - void*pListener = nullptr; - std::thread*pBgThread = nullptr; - - bool logHttpRequests = false; - - bool hasAccessPointGateway = false; - boost::asio::ip::network_v4 accessPointGateway; - std::string accessPointServerAddress; - bool stopping = false; - - public: + virtual ~BeastServer() { } - BeastServer( - const boost::asio::ip::address& address, - int port, - const char*rootPath, - int threads = 10); + virtual void SetLogHttpRequests(bool enableLogging) = 0; - void Run(); - void RunInBackground(); - bool Stopping() const { return this->stopping; } - void Stop(); - void Join(); + virtual void AddRequestHandler(std::shared_ptr requestHandler) = 0; + virtual void AddSocketFactory(std::shared_ptr &socketHandler) = 0; - void SetLogHttpRequests(bool logRequests) { this->logHttpRequests = logRequests; } - bool LogHttpRequests() const { return this->logHttpRequests; } - boost::asio::io_context& IoContext() { - return *pIoContext; - } + virtual void ShutDown(int timeoutMs) = 0; + virtual void Join() = 0; - void AddRequestHandler(std::shared_ptr requestHandler) - { - request_handlers.push_back(requestHandler); - } - - void AddSocketFactory(const std::shared_ptr &socketHandler) - { - socket_factories.push_back(socketHandler); - } - - std::vector > &RequestHandlers() { return request_handlers; } - - const std::string&GetDocRoot() { return doc_root; } - - virtual ~BeastServer(); - - std::shared_ptr GetSocketFactory(const uri &request_uri); - - void SetAccessPointGateway(const boost::asio::ip::network_v4 &gateway, const std::string&accessPointServerAddress) - { - this->accessPointGateway = gateway; - this->hasAccessPointGateway = true; - this->accessPointServerAddress = accessPointServerAddress; - } - bool HasAccessPointGateway() const { return hasAccessPointGateway; } - boost::asio::ip::network_v4 &GetAccessPointGateway() { return this->accessPointGateway; } - const std::string & GetAccessPointServerAddress() const { return this->accessPointServerAddress; } - -public: - virtual void onError(boost::system::error_code ec, char const* what); - virtual void onWarning(boost::system::error_code ec, char const* what); - virtual void onInfo(char const* what); - - virtual void onListenerClosed(); + virtual void RunInBackground() = 0; }; + +std::shared_ptr createBeastServer(const boost::asio::ip::address &address, int port, const char *rootPath, int threads); + + + + } // namespace pipedal; \ No newline at end of file diff --git a/test/BeastServerTest.cpp b/src/BeastServerTest.cpp similarity index 86% rename from test/BeastServerTest.cpp rename to src/BeastServerTest.cpp index 45c1b98..cf1b475 100644 --- a/test/BeastServerTest.cpp +++ b/src/BeastServerTest.cpp @@ -22,20 +22,21 @@ #include "BeastServer.hpp" -using namespace piddle; +using namespace pipedal; -TEST_CASE( "BeastServer shutodwn", "[beastServerShutdown]" ) { +TEST_CASE( "BeastServer shutdown", "[beastServerShutdown]" ) { auto const address = boost::asio::ip::make_address("0.0.0.0"); auto const port = 8081; - std::string doc_root = "/bad"; + std::string doc_root = "."; auto const threads = 3; - auto server = std::make_shared( + auto server = createBeastServer( address,port,doc_root.c_str(),threads); server->RunInBackground(); - sleep(3); - server->Stop(); + sleep(30000); + server->ShutDown(1000); + sleep(1); server->Join(); } diff --git a/src/BeastServer~.cpp.bak b/src/BeastServer~.cpp.bak new file mode 100644 index 0000000..97d2d18 --- /dev/null +++ b/src/BeastServer~.cpp.bak @@ -0,0 +1,1248 @@ +// +// Copyright (c) 2016-2017 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/boostorg/beast +// + +//------------------------------------------------------------------------------ +// +// Example: Advanced server +// +//------------------------------------------------------------------------------ + +#include "pch.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "HtmlHelper.hpp" +#include "Lv2Log.hpp" + +#include "BeastServer.hpp" + +#include "Uri.hpp" + +using namespace pipedal; + +using tcp = boost::asio::ip::tcp; // from +namespace http = boost::beast::http; // from +namespace websocket = boost::beast::websocket; // from + +const size_t MAX_READ_SIZE = 1 * 1024 * 204; +using namespace boost; + +std::string +pipedal::last_modified(const std::filesystem::path &path) +{ + auto cPath = path.c_str(); + + struct stat fStat; + if (stat(cPath, &fStat) == 0) + { + return HtmlHelper::timeToHttpDate(fStat.st_mtim.tv_sec); + } + else + { + return HtmlHelper::timeToHttpDate(0); + } +} + +std::map extensionsToMimeType = { + {".htm", "text/html"}, + {".html", "text/html"}, + {".php", "text/html"}, + {".css", "text/css"}, + {".txt", "text/plain"}, + {".js", "application/javascript"}, + {".json", "application/json"}, + {".xml", "application/xml"}, + {".png", "image/png"}, + {".jpe", "image/jpeg"}, + {".jpeg", "image/jpeg"}, + {".jpg", "image/jpeg"}, + {".gif", "image/gif"}, + {".bmp", "image/bmp"}, + {".ico", "image/x-icon"}, + {".tiff", "image/tiff"}, + {".tif", "image/tiff"}, + {".svg", "image/svg+xml"}, + {".svgz", "image/svg+xml"}, + {".woff", "font/woff2" }, + {".woff2", "font/woff2"} +}; +// Return a reasonable mime type based on the extension of a file. +std::string +mime_type(const std::filesystem::path &path) +{ + auto const ext = path.extension(); + try + { + return extensionsToMimeType.at(ext); + } + catch (const std::exception &) + { + return "application/octet-stream"; + } +} + +// Append an HTTP rel-path to a local filesystem path. +// The returned path is normalized for the platform. +std::filesystem::path +path_cat( + boost::beast::string_view base, + const std::vector &segments) +{ + + std::filesystem::path result = base.to_string(); + + for (const std::string &segment : segments) + { + if (segment.find("..") != -1) + { + throw std::invalid_argument("Invalid segment."); + } + if (segment.find('/') != -1) + throw std::invalid_argument("Invalid segment."); + if (segment.find('\\') != -1) + throw std::invalid_argument("Invalid segment."); + if (segment.find('/') != -1) + throw std::invalid_argument("Invalid segment."); + if (segment.find(':') != -1) + throw std::invalid_argument("Invalid segment."); + + result /= segment; + } + return result; +} + +std::string GetFromAddress(const tcp::socket &socket) +{ + std::stringstream s; + s << socket.remote_endpoint().address().to_string() << ':' << socket.remote_endpoint().port(); + return s.str(); +} +// This function produces an HTTP response for the given +// request. The type of the response object depends on the +// contents of the request, so the interface requires the +// caller to pass a generic lambda for receiving the response. +template < + class Body, class Allocator, + class Send> +void handle_request( + const tcp::socket &socket_, + BeastServer *server, + http::request> &&req, + Send &&send) +{ + + + // Returns a bad request response + auto const bad_request = + [&req, &socket_](boost::beast::string_view why) + { + Lv2Log::error("http - (%s) Bad request: %s,", GetFromAddress(socket_).c_str(), why.to_string().c_str()); + http::response res{http::status::bad_request, req.version()}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, "text/plain"); + 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, &socket_](boost::beast::string_view target) + { + Lv2Log::error("http - (%s) Not found: %s,", GetFromAddress(socket_).c_str(), target.to_string().c_str()); + http::response res{http::status::not_found, req.version()}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, "text/plain"); + 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, &socket_](boost::beast::string_view what) + { + Lv2Log::error("http - (%s) Server error: %s,", GetFromAddress(socket_).c_str(), what.to_string().c_str()); + http::response res{http::status::not_found, req.version()}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, "text/plain"); + res.keep_alive(req.keep_alive()); + res.body() = what.to_string(); + res.prepare_payload(); + return res; + }; + + if (server->HasAccessPointGateway()) + { + auto &gateway = server->GetAccessPointGateway(); + auto remoteAddress = socket_.remote_endpoint().address(); + if (remoteAddress.is_v4()) + { + auto v4Address = remoteAddress.to_v4(); + unsigned int gwAddr = (gateway.netmask().to_uint() & gateway.address().to_uint()); + unsigned int ipAddr = (gateway.netmask().to_uint() & v4Address.to_uint()); + + if ((gateway.netmask().to_uint() & gateway.address().to_uint()) == (gateway.netmask().to_uint() & v4Address.to_uint())) + { + std::string host = req[http::field::host].to_string(); + std::string gatewayHost = server->GetAccessPointServerAddress(); + if (host != gatewayHost) + { + + uri requestUri; + requestUri.set(req.target().begin(), req.target().end()); + if (requestUri.segment_count() != 0) { + return send(not_found(req.target())); + } + uri_builder builder(requestUri); + builder.set_authority(gatewayHost); + + std::string redirectUri = builder.str(); + Lv2Log::info("Access point redirect from" + host + " to " + redirectUri); + + + // redirect to the gateway host. + http::response res{http::status::temporary_redirect, req.version()}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::location, redirectUri); + res.prepare_payload(); + return send(std::move(res)); + + } + } + } + } + + if (req.method() == http::verb::options) + { + http::response res{http::status::no_content, req.version()}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::access_control_allow_origin, req[http::field::origin]); + res.set(http::field::access_control_allow_methods, "POST, GET, OPTIONS, HEADERS"); + res.set(http::field::access_control_allow_headers, "Content-Type"); + res.keep_alive(req.keep_alive()); + return send(std::move(res)); + } + + uri request_uri; + try + { + request_uri.set(req.target().begin(), req.target().end()); + std::string host = req[http::field::host].to_string(); + int pos = host.find_last_of(':'); + if (pos != std::string::npos) + { + host = host.substr(0, pos); + } + uri_builder builder(request_uri); + builder.set_authority(host); + builder.set_port(socket_.local_endpoint().port()); + // stub: set htts scheme once we implement it. + builder.set_scheme("http"); + + std::string strUri = builder.str(); + request_uri = uri(strUri.c_str()); + } + catch (const std::exception &_) + { + return send((bad_request("Malformed target."))); + } + + for (auto requestHandler : server->RequestHandlers()) + { + + if (requestHandler->wants(req.method(), request_uri)) + { + + if (req.method() == http::verb::head) + { + Lv2Log::info("http - (%s) head %s", GetFromAddress(socket_), request_uri.str().c_str()); + + boost::beast::error_code ec; + http::response res{http::status::ok, req.version()}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::date, HtmlHelper::timeToHttpDate(time(nullptr))); + res.set(http::field::access_control_allow_origin, req[http::field::origin]); + requestHandler->head_response(request_uri, req, res, ec); + res.keep_alive(req.keep_alive()); + // 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())); + + if (ec) + { + return send(server_error(ec.message())); + } + req.prepare_payload(); + return send(std::move(res)); + } + else if (req.method() == http::verb::get) + { + if (server->LogHttpRequests()) + { + Lv2Log::info("http - (%s) get %s", GetFromAddress(socket_).c_str(), request_uri.str().c_str()); + } + + boost::beast::error_code ec; + http::response res{http::status::ok, req.version()}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::date, HtmlHelper::timeToHttpDate(time(nullptr))); + res.set(http::field::access_control_allow_origin, req[http::field::origin]); + requestHandler->get_response(request_uri, req, res, ec); + res.keep_alive(req.keep_alive()); + if (ec == boost::system::errc::no_such_file_or_directory) + return send(not_found(req.target())); + + if (ec) + { + return send(server_error(ec.message())); + } + res.prepare_payload(); + return send(std::move(res)); + } + else if (req.method() == http::verb::post) + { + if (server->LogHttpRequests()) + { + Lv2Log::info("http - (%s) post %s", GetFromAddress(socket_).c_str(), request_uri.str().c_str()); + } + + boost::beast::error_code ec; + http::response res{http::status::ok, req.version()}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::date, HtmlHelper::timeToHttpDate(time(nullptr))); + res.set(http::field::access_control_allow_origin, req[http::field::origin]); + requestHandler->post_response(request_uri, req, res, ec); + res.keep_alive(false); // bug in Beast Server -- posted body not reset. + if (ec == boost::system::errc::no_such_file_or_directory) + return send(not_found(req.target())); + + if (ec) + { + return send(server_error(ec.message())); + } + res.prepare_payload(); + return send(std::move(res)); + } + return send(bad_request("Unknown HTTP-method")); + } + } + // 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(not_found(req.target())); // security: report not found to avoid disclosing info. + + // Build the path to the requested file + auto path = path_cat(server->GetDocRoot().c_str(), request_uri.segments()); + if (req.target().back() == '/' || req.target().size() == 0) + path /= "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())); + + // Respond to HEAD request + if (req.method() == http::verb::head) + { + if (server->LogHttpRequests()) + { + Lv2Log::info("http - (%s) head %s", GetFromAddress(socket_).c_str(), request_uri.str().c_str()); + } + + http::response res{http::status::ok, req.version()}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::date, HtmlHelper::timeToHttpDate(time(nullptr))); + res.set(http::field::content_type, mime_type(path)); + res.set(http::field::last_modified, last_modified(path)); + res.content_length(body.size()); + res.keep_alive(req.keep_alive()); + return send(std::move(res)); + } + else if (req.method() == http::verb::get) + { + if (server->LogHttpRequests()) + { + Lv2Log::info("http - (%s) get %s", GetFromAddress(socket_).c_str(), request_uri.str().c_str()); + } + + // Respond to GET request + http::response 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::date, HtmlHelper::timeToHttpDate(time(nullptr))); + res.set(http::field::content_type, mime_type(path)); + res.set(http::field::last_modified, last_modified(path)); + res.content_length(body.size()); + res.keep_alive(req.keep_alive()); + return send(std::move(res)); + } + else + { + return send(bad_request("Unknown HTTP-method")); + } +} + +//------------------------------------------------------------------------------ + +// Echoes back all received WebSocket messages +class websocket_session : public std::enable_shared_from_this, private SocketHandler::IWriteCallback +{ + websocket::stream ws_; + boost::asio::strand< + boost::asio::io_context::executor_type> + strand_; + boost::asio::steady_timer timer_; + boost::beast::multi_buffer read_buffer_; + boost::beast::multi_buffer write_buffer_; + + BeastServer *beastServer_; + std::shared_ptr socket_handler_; + + std::queue writeQueue; + + bool writePending = false; + ; + std::mutex writeQueueMutex; + std::string fromAddress; + +public: + ~websocket_session() + { + Lv2Log::info("http - (%s) Websocket closed.", fromAddress.c_str()); + } + virtual void close() + { + this->ws_.close(websocket::close_reason()); + } + virtual std::string getFromAddress() const { + return this->fromAddress; + } + virtual void writeCallback(const std::string &text) + { + { + std::lock_guard guard(writeQueueMutex); + if (writePending) + { + writeQueue.push(text); + return; + } + writePending = true; + } + do_write(text); + } + void do_write(const std::string &text) + { + size_t n = buffer_copy(write_buffer_.prepare(text.size()), boost::asio::buffer(text)); + write_buffer_.commit(n); + ws_.text(true); + + ws_.async_write( + write_buffer_.data(), + boost::asio::bind_executor( + strand_, + std::bind( + &websocket_session::on_write, + shared_from_this(), + std::placeholders::_1, + std::placeholders::_2))); + } + // Take ownership of the socket + explicit websocket_session( + const std::string &fromAddress, + BeastServer *beastServer, + std::shared_ptr &&socketHandler, + tcp::socket socket) + : beastServer_(beastServer), socket_handler_(socketHandler), + fromAddress(fromAddress), + ws_(std::move(socket)), + strand_(ws_.get_executor()), + read_buffer_(MAX_READ_SIZE), + timer_(ws_.get_executor().context(), + (std::chrono::steady_clock::time_point::max)()) + { + } + + // Start the asynchronous operation + template + void + run(http::request> req) + { + // Run the timer. The timer is operated + // continuously, this simplifies the code. + on_timer({}); + + // Set the timer + timer_.expires_after(std::chrono::seconds(15)); + // Accept the websocket handshake + + ws_.async_accept( + req, + boost::asio::bind_executor( + strand_, + std::bind( + &websocket_session::on_accept, + shared_from_this(), + std::placeholders::_1))); + } + + // Called when the timer expires. + void + on_timer(boost::system::error_code ec) + { + if (ec && ec != boost::asio::error::operation_aborted) + this->beastServer_->onError(ec, "timer"); + return; + + // Verify that the timer really expired since the deadline may have moved. + if (timer_.expiry() <= std::chrono::steady_clock::now()) + { + // Closing the socket cancels all outstanding operations. They + // will complete with boost::asio::error::operation_aborted + ws_.next_layer().shutdown(tcp::socket::shutdown_both, ec); + ws_.next_layer().close(ec); + return; + } + + // Wait on the timer + timer_.async_wait( + boost::asio::bind_executor( + strand_, + std::bind( + &websocket_session::on_timer, + shared_from_this(), + std::placeholders::_1))); + } + + void + on_accept(boost::system::error_code ec) + { + // Happens when the timer closes the socket + if (ec == boost::asio::error::operation_aborted) + return; + + if (ec) + { + beastServer_->onError(ec, "accept"); + return; + } + if (socket_handler_) + { + auto t = socket_handler_.get(); + t->setWriteCallback(this); + socket_handler_->onAttach(); + } + + // Read a message + do_read(); + } + + void + do_read() + { + // Set the timer + timer_.expires_after(std::chrono::seconds(15)); + + // Read a message into our buffer + ws_.async_read( + read_buffer_, + boost::asio::bind_executor( + strand_, + std::bind( + &websocket_session::on_read, + shared_from_this(), + std::placeholders::_1, + std::placeholders::_2))); + } + + void + on_read( + boost::system::error_code ec, + std::size_t bytes_transferred) + { + + // Happens when the timer closes the socket + if (ec == boost::asio::error::operation_aborted) + return; + + // This indicates that the websocket_session was closed + if (ec == websocket::error::closed) + return; + + if (ec) + { + beastServer_->onError(ec, "read"); + return; + } + + // Echo the message + if (ws_.got_text()) + { + std::string data(beast::buffers_to_string(read_buffer_.data())); + this->socket_handler_->onReceive(data); + read_buffer_.consume(bytes_transferred); + } + else + { + read_buffer_.consume(bytes_transferred); // ignore binary messages + } + + do_read(); + // ws_.text(ws_.got_text()); + // ws_.async_write( + // buffer_.data(), + // boost::asio::bind_executor( + // strand_, + // std::bind( + // &websocket_session::on_write, + // shared_from_this(), + // std::placeholders::_1, + // std::placeholders::_2))); + } + + void fail(boost::system::error_code ec, const char *what) + { + beastServer_->onError(ec, what); + } + void + on_write( + boost::system::error_code ec, + std::size_t bytes_transferred) + { + boost::ignore_unused(bytes_transferred); + + // Happens when the timer closes the socket + if (ec == boost::asio::error::operation_aborted) + return; + + if (ec) + { + fail(ec, "write"); + } + + // Clear the buffer + std::string nextWrite; + write_buffer_.consume(write_buffer_.size()); + + { + std::lock_guard guard(this->writeQueueMutex); + if (writeQueue.size() != 0) + { + nextWrite = writeQueue.front(); + writeQueue.pop(); + } + else + { + writePending = false; + return; + } + } + do_write(nextWrite); + } +}; + +// Handles an HTTP server connection +class http_session : public std::enable_shared_from_this +{ + // This queue is used for HTTP pipelining. + class queue + { + enum + { + // Maximum number of responses we will queue + limit = 8 + }; + + // The type-erased, saved work item + struct work + { + virtual ~work() = default; + virtual void operator()() = 0; + }; + + http_session &self_; + std::vector> items_; + + public: + explicit queue(http_session &self) + : self_(self) + { + static_assert(limit > 0, "queue limit must be positive"); + items_.reserve(limit); + } + + // Returns `true` if we have reached the queue limit + bool + is_full() const + { + return items_.size() >= limit; + } + + // Called when a message finishes sending + // Returns `true` if the caller should initiate a read + bool + on_write() + { + BOOST_ASSERT(!items_.empty()); + auto const was_full = is_full(); + items_.erase(items_.begin()); + if (!items_.empty()) + (*items_.front())(); + return was_full; + } + + // Called by the HTTP handler to send a response. + template + void + operator()(http::message &&msg) + { + // This holds a work item + struct work_impl : work + { + http_session &self_; + http::message msg_; + + work_impl( + http_session &self, + http::message &&msg) + : self_(self), msg_(std::move(msg)) + { + } + + void + operator()() + { + http::async_write( + self_.socket_, + msg_, + boost::asio::bind_executor( + self_.strand_, + std::bind( + &http_session::on_write, + self_.shared_from_this(), + std::placeholders::_1, + msg_.need_eof()))); + } + }; + + // Allocate and store the work + items_.emplace_back(new work_impl(self_, std::move(msg))); + + // If there was no previous work, start this one + if (items_.size() == 1) + (*items_.front())(); + } + }; + + tcp::socket socket_; + boost::asio::strand< + boost::asio::io_context::executor_type> + strand_; + boost::asio::steady_timer timer_; + boost::beast::flat_buffer buffer_; + http::request req_; + queue queue_; + BeastServer *beastServer_; + +public: + // Take ownership of the socket + explicit http_session( + BeastServer *beastServer, + tcp::socket socket) + : beastServer_(beastServer), socket_(std::move(socket)), strand_(socket_.get_executor()), timer_(socket_.get_executor().context(), + (std::chrono::steady_clock::time_point::max)()), + queue_(*this) + { + } + + // Start the asynchronous operation + void + run() + { + // Run the timer. The timer is operated + // continuously, this simplifies the code. + on_timer({}); + + do_read(); + } + void + do_read() + { + // make the request empty before reading + // otherwise the operation behavour is undefined. + req_ = {}; + + // Set the time + timer_.expires_after(std::chrono::seconds(15)); + + // Read a request + http::async_read(socket_, buffer_, req_, + boost::asio::bind_executor( + strand_, + std::bind( + &http_session::on_read, + shared_from_this(), + std::placeholders::_1))); + } + + // Called when the timer expires. + void + on_timer(boost::system::error_code ec) + { + if (ec && ec != boost::asio::error::operation_aborted) + return fail(ec, "timer"); + + // Verify that the timer really expired since the deadline may have moved. + if (timer_.expiry() <= std::chrono::steady_clock::now()) + { + // Closing the socket cancels all outstanding operations. They + // will complete with boost::asio::error::operation_aborted + if (!beastServer_->Stopping()) + { + socket_.shutdown(tcp::socket::shutdown_both, ec); + } + socket_.close(ec); + return; + } + + // Wait on the timer + timer_.async_wait( + boost::asio::bind_executor( + strand_, + std::bind( + &http_session::on_timer, + shared_from_this(), + std::placeholders::_1))); + } + + void fail(boost::system::error_code ec, const char *what) + { + beastServer_->onError(ec, what); + } + + void + on_read(boost::system::error_code ec) + { + // Happens when the timer closes the socket + if (ec == boost::asio::error::operation_aborted) + return; + + // This means they closed the connection + if (ec == http::error::end_of_stream) + return do_close(); + + if (ec) + { + fail(ec, "read"); + return; + } + // See if it is a WebSocket Upgrade + if (websocket::is_upgrade(req_)) + { + uri request_uri; + try + { + request_uri.set(req_.target().begin(), req_.target().end()); + } + catch (const std::exception &_) + { + fail(ec, "socket uri"); + return; + } + + std::shared_ptr socketFactory = beastServer_->GetSocketFactory(request_uri); + if (!socketFactory) + { + fail(http::error::bad_target, "Not found"); + return; + } + + Lv2Log::info("http - (%s) Websocket opened: %s", GetFromAddress(socket_).c_str(), request_uri.str().c_str()); + // Create a WebSocket websocket_session by transferring the socket + std::string fromName = GetFromAddress(socket_); + std::make_shared( + fromName, + this->beastServer_, + socketFactory ? socketFactory->CreateHandler(request_uri) : nullptr, + std::move(socket_)) + ->run(std::move(req_)); + return; + } + // Send the response + handle_request( + this->socket_, + beastServer_, + std::move(req_), + queue_); + + // If we aren't at the queue limit, try to pipeline another request + if (!queue_.is_full()) + do_read(); + } + + void + on_write(boost::system::error_code ec, bool close) + { + // Happens when the timer closes the socket + if (ec == boost::asio::error::operation_aborted) + return; + + if (ec) + return fail(ec, "write"); + + if (close) + { + // This means we should close the connection, usually because + // the response indicated the "Connection: close" semantic. + return do_close(); + } + + // Inform the queue that a write completed + if (queue_.on_write()) + { + // Read another request + do_read(); + } + } + + void + do_close() + { + // Send a TCP shutdown + boost::system::error_code ec; + if (!beastServer_->Stopping()) + { + socket_.shutdown(tcp::socket::shutdown_send, ec); + } + socket_.close(); + + + } +}; + +//------------------------------------------------------------------------------ + +// Accepts incoming connections and launches the sessions +class listener : public std::enable_shared_from_this +{ + tcp::acceptor acceptor_; + tcp::socket socket_; + BeastServer *beastServer_; + +public: + ~listener() + { + beastServer_->onListenerClosed(); + } + listener( + BeastServer *server, + boost::asio::io_context &ioc, + tcp::endpoint endpoint) + : beastServer_(server), acceptor_(ioc), socket_(ioc) + { + boost::system::error_code ec; + + // Open the acceptor + acceptor_.open(endpoint.protocol(), ec); + if (ec) + { + fail(ec, "open"); + return; + } + + boost::asio::socket_base::reuse_address option(true); + acceptor_.set_option(option); + // Lv2Log::warning("http - Accepting connections with reuse_adddress to allow faster debug turns. Should NOT be enabled in production use. "); + + // Leftover client sockets in TIME_WAIT state prevent binding of the server address. + // Retry for up to 140 seconds in order to allow the TIME_WAIT sockets to expire. + // If we still can't establish a connection, we can be sure that somebody else or an existing instance is servicing the address, so + // quit after that. + int retries = 0; + int retryTime = 1; + int totalRetryTime = 0; + int MAX_RETRY_TIME = 240; // socket-timeout-ish. + + while (true) + { + // Bind to the server address + acceptor_.bind(endpoint, ec); + if (!ec) + { + break; + } + else + { + ++retries; + if (totalRetryTime > MAX_RETRY_TIME) + { + fail(ec, "bind"); + return; + } + Lv2Log::info("http - bind: %s. (port %d) Retrying...", ec.message().c_str(), endpoint.port()); + sleep(retryTime); + retryTime *= 2; + if (retryTime > 10) + retryTime = 10; + totalRetryTime += retryTime; + } + } + + Lv2Log::info("http - Server listening on %s:%d", endpoint.address().to_string().c_str(), (int)endpoint.port()); + + // Start listening for connections + acceptor_.listen( + boost::asio::socket_base::max_listen_connections, ec); + if (ec) + { + fail(ec, "listen"); + return; + } + } + + void + stop() + { + acceptor_.cancel(); + } + + // Start accepting incoming connections + void + run() + { + if (!acceptor_.is_open()) + return; + do_accept(); + } + + void + do_accept() + { + acceptor_.async_accept( + socket_, + std::bind( + &listener::on_accept, + shared_from_this(), + std::placeholders::_1)); + } + + void fail(boost::system::error_code ec, const char *what) + { + beastServer_->onError(ec, what); + } + void + on_accept(boost::system::error_code ec) + { + if (ec) + { + fail(ec, "accept"); + return; + } + else + { + // Create the http_session and run it + std::make_shared( + beastServer_, + std::move(socket_)) + ->run(); + } + + // Accept another connection + do_accept(); + } +}; + +//------------------------------------------------------------------------------ + +BeastServer::BeastServer(const boost::asio::ip::address &address, int port, const char *rootPath, int threads) + : address(address), + port((unsigned short)port), + doc_root(rootPath), + threads(threads), + pBgThread(nullptr) +{ +} + +void BeastServer::Stop() +{ + if (stopping) return; + stopping = true; + listener *p = nullptr; + { + std::unique_lock lk(io_mutex); + p = (listener*)this->pListener; + this->pListener = nullptr; + } + if (p != nullptr) + { + p->stop(); + } + boost::asio::io_context *pIoContext = nullptr; + { + std::unique_lock lk(io_mutex); + pIoContext = this->pIoContext; + this->pIoContext = nullptr; + } + if (pIoContext != nullptr) + { + pIoContext->stop(); + }; +} + +void BeastServer::onListenerClosed() +{ + std::unique_lock lk(io_mutex); + this->pListener = nullptr; +} + +static void ThreadProc(BeastServer *server) +{ + server->Run(); +} + +void BeastServer::RunInBackground() +{ + if (this->pBgThread != nullptr) + { + throw std::runtime_error("Bad state."); + } + this->pBgThread = new std::thread(ThreadProc, this); +} + +void BeastServer::Join() +{ + if (this->pBgThread != nullptr) + { + Stop(); + this->pBgThread->join(); + this->pBgThread = nullptr; + } +} + +void BeastServer::Run() +{ + // The io_context is required for all I/O + boost::asio::io_context ioc{threads}; + + { + // save a reference for use by stop() + std::unique_lock lk(io_mutex); + pIoContext = &ioc; + } + + // Create and launch a listening port + std::shared_ptr listenerInstance = std::make_shared( + this, + ioc, + tcp::endpoint{address, port}); + listenerInstance->run(); + + // save a reference for use by stop() + { + std::unique_lock lk(io_mutex); + this->pListener = (void *)&(*listenerInstance); + } + + // Run the I/O service on the requested number of threads + std::vector v; + v.reserve(threads - 1); + for (auto i = threads - 1; i > 0; --i) + v.emplace_back( + [&ioc] + { + ioc.run(); + }); + ioc.run(); + + ioc.stop(); + for (auto &thread : v) + { + thread.join(); + } + { + std::unique_lock lk(io_mutex); + this->pListener = nullptr; + this->pIoContext = nullptr; + } +} + +BeastServer::~BeastServer() +{ + // Shutdown if running in background. + Stop(); + Join(); +} + +void BeastServer::onWarning(boost::system::error_code ec, char const *what) +{ + std::stringstream s; + s << "http - " << what << ": " << ec.message() << " (" << ec.value() << ')'; + Lv2Log::warning(s.str().c_str()); +} +void BeastServer::onInfo(char const *message) +{ + Lv2Log::info("%s", message); +} + +void BeastServer::onError(boost::system::error_code ec, char const *what) +{ + std::stringstream s; + s << "http - " << what << ": " << ec.message() << " (" << ec.value() << ')'; + Lv2Log::warning(s.str().c_str()); +} + +std::shared_ptr BeastServer::GetSocketFactory(const uri &request_uri) +{ + for (auto factory : this->socket_factories) + { + if (factory->wants(request_uri)) + { + return factory; + } + } + return nullptr; +} diff --git a/src/BeastServer~.hpp.bak b/src/BeastServer~.hpp.bak new file mode 100644 index 0000000..74c12f2 --- /dev/null +++ b/src/BeastServer~.hpp.bak @@ -0,0 +1,217 @@ +#pragma once + + +// avoid breaking changes from 1.64 to.71 +// Note that raspbian only prvides 1.64; ubuntu studio only provides 1.71 +#define BOOST_BEAST_ALLOW_DEPRECATED + +#include +#include +#include + + +#include +#include +#include "Uri.hpp" +#include +#include + + +// forward declaration. + +class websocket_session; + +namespace pipedal { +using HttpRequest =boost::beast::http::request; + + +std::string last_modified(const std::filesystem::path& path); + +class SocketHandler { +public: + + class IWriteCallback { + public: + virtual void close() = 0; + + virtual void writeCallback(const std::string& text) = 0; + virtual std::string getFromAddress() const = 0; + }; + +private: + friend class ::websocket_session; + + + IWriteCallback *writeCallback_; + + void setWriteCallback(IWriteCallback *writeCallback) { + writeCallback_ = writeCallback; + } + +protected: + virtual void onReceive(const std::string_view&text) = 0; +public: + std::string getFromAddress() const { return writeCallback_->getFromAddress(); } + void receive(const std::string_view&text) { + onReceive(text); + } + void send(const std::string &text) { + if (writeCallback_ != nullptr) + { + writeCallback_->writeCallback(text); + } + } + virtual void Close() + { + writeCallback_->close(); + } + + virtual ~SocketHandler() = default; + + virtual void onAttach() { } + +}; + +class ISocketFactory { +public: + virtual bool wants(const uri &request) = 0; + virtual std::shared_ptr CreateHandler(const uri& request) = 0; +}; + +class RequestHandler { +private: + uri target_url_; +public: + RequestHandler(const char*target_url) + : target_url_(target_url) + { + + } + ~RequestHandler() = default; + + virtual bool wantsRedirect(const uri& requestUri) + { + return false; + } + uri GetRedirect(const uri& requestUri) + { + return requestUri; + } + + virtual bool wants(boost::beast::http::verb verb,const uri &request_uri) const { + if (request_uri.segment_count() < target_url_.segment_count()) + { + return false; + } + for (int i = 0; i < target_url_.segment_count(); ++i) + { + if (request_uri.segment(i) != target_url_.segment(i)) + { + return false; + } + } + return true; + } + + virtual void head_response( + const uri&request_uri, + const boost::beast::http::request &req, + boost::beast::http::response &res, + boost::beast::error_code &ec) = 0; + + virtual void get_response( + const uri&request_uri, + const boost::beast::http::request &req, + boost::beast::http::response &res, + boost::beast::error_code &ec) = 0; + + virtual void post_response( + const uri&request_uri, + const boost::beast::http::request &req, + boost::beast::http::response &res, + boost::beast::error_code &ec) + { + get_response(request_uri,req,res,ec); + } + + +}; +class BeastServer { +private: + boost::asio::ip::address address; + unsigned short port; + std::string doc_root; + int threads; + std::mutex log_mutex; + std::vector > request_handlers; + std::vector > socket_factories; + std::mutex io_mutex; + boost::asio::io_context *pIoContext = nullptr; + void*pListener = nullptr; + std::thread*pBgThread = nullptr; + + bool logHttpRequests = false; + + bool hasAccessPointGateway = false; + boost::asio::ip::network_v4 accessPointGateway; + std::string accessPointServerAddress; + bool stopping = false; + + +public: + + BeastServer( + const boost::asio::ip::address& address, + int port, + const char*rootPath, + int threads = 10); + + void Run(); + void RunInBackground(); + bool Stopping() const { return this->stopping; } + void Stop(); + void Join(); + + void SetLogHttpRequests(bool logRequests) { this->logHttpRequests = logRequests; } + bool LogHttpRequests() const { return this->logHttpRequests; } + boost::asio::io_context& IoContext() { + return *pIoContext; + } + + void AddRequestHandler(std::shared_ptr requestHandler) + { + request_handlers.push_back(requestHandler); + } + + void AddSocketFactory(const std::shared_ptr &socketHandler) + { + socket_factories.push_back(socketHandler); + } + + std::vector > &RequestHandlers() { return request_handlers; } + + const std::string&GetDocRoot() { return doc_root; } + + virtual ~BeastServer(); + + std::shared_ptr GetSocketFactory(const uri &request_uri); + + void SetAccessPointGateway(const boost::asio::ip::network_v4 &gateway, const std::string&accessPointServerAddress) + { + this->accessPointGateway = gateway; + this->hasAccessPointGateway = true; + this->accessPointServerAddress = accessPointServerAddress; + } + bool HasAccessPointGateway() const { return hasAccessPointGateway; } + boost::asio::ip::network_v4 &GetAccessPointGateway() { return this->accessPointGateway; } + const std::string & GetAccessPointServerAddress() const { return this->accessPointServerAddress; } + +public: + virtual void onError(boost::system::error_code ec, char const* what); + virtual void onWarning(boost::system::error_code ec, char const* what); + virtual void onInfo(char const* what); + + virtual void onListenerClosed(); +}; + +} // namespace pipedal; \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 048924b..0fdea7b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -149,6 +149,7 @@ add_executable(pipedaltest ${PIPEDAL_SOURCES} testMain.cpp SystemConfigFile.hpp SystemConfigFile.cpp SystemConfigFileTest.cpp + BeastServerTest.cpp ) configure_file(config.hpp.in config.hpp) @@ -211,12 +212,21 @@ set (REACT_NOTICES_FILE ${CMAKE_CURRENT_SOURCE_DIR}/../build/src/notices.txt) set (DEBIAN_COPYRIGHT_FILE ${CMAKE_CURRENT_SOURCE_DIR}/../debian/copyright) # generate Copyright section of settings page. +# warning: there may be multiple versions. Pick the latest. +if(EXISTS "/usr/share/doc/libboost1.71-dev") + set (BOOST_COPYRIGHT_DIR "libboost1.71-dev") +elseif(EXISTS "/usr/share/doc/libboost1.67-dev") + set (BOOST_COPYRIGHT_DIR "libboost1.67-dev") +else() + message(ERROR "Boost libary version has changed. Please update me.") +endif() + add_custom_command(OUTPUT ${REACT_NOTICES_FILE} COMMAND "$" --output ${REACT_NOTICES_FILE} --projectCopyright ${DEBIAN_COPYRIGHT_FILE} - liblilv-0-0 libboost1.67-dev libnl-3-dev libnl-3-dev lv2-dev + liblilv-0-0 ${BOOST_COPYRIGHT_DIR} libnl-3-dev libnl-3-dev lv2-dev WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} DEPENDS ${DEBIAN_COPYRIGHT_FILE} "$" COMMENT "Updating copyright notices." diff --git a/src/LogFeature.hpp b/src/LogFeature.hpp index 9021cde..136544a 100644 --- a/src/LogFeature.hpp +++ b/src/LogFeature.hpp @@ -19,12 +19,10 @@ #pragma once -#include "lv2/lv2core.lv2/lv2.h" +#include "lv2.h" #include "lv2/atom.lv2/atom.h" #include "lv2/atom.lv2/util.h" -#include "lv2/lv2core.lv2/lv2.h" -#include "lv2/log.lv2/log.h" #include "lv2/log.lv2/logger.h" #include "lv2/midi.lv2/midi.h" #include "lv2/urid.lv2/urid.h" diff --git a/src/Lv2Effect.cpp b/src/Lv2Effect.cpp index d194072..cb3ba4f 100644 --- a/src/Lv2Effect.cpp +++ b/src/Lv2Effect.cpp @@ -24,7 +24,7 @@ #include #include "lv2/atom.lv2/atom.h" #include "lv2/atom.lv2/util.h" -#include "lv2/lv2core.lv2/lv2.h" +#include "lv2.h" #include "lv2/log.lv2/log.h" #include "lv2/log.lv2/logger.h" #include "lv2/midi.lv2/midi.h" diff --git a/src/Lv2EventBufferWriter.hpp b/src/Lv2EventBufferWriter.hpp index faa58b2..306f2be 100644 --- a/src/Lv2EventBufferWriter.hpp +++ b/src/Lv2EventBufferWriter.hpp @@ -41,7 +41,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. #include #include -#include +#include #include #include #include diff --git a/src/Lv2Host.cpp b/src/Lv2Host.cpp index 44dfaf7..fde6853 100644 --- a/src/Lv2Host.cpp +++ b/src/Lv2Host.cpp @@ -31,7 +31,7 @@ #include "OptionsFeature.hpp" #include "JackConfiguration.hpp" #include "lv2/urid.lv2/urid.h" -#include "lv2/lv2core.lv2/lv2.h" +#include "lv2.h" #include "lv2/atom.lv2/atom.h" #include "lv2/lv2plug.in/ns/ext/buf-size/buf-size.h" #include "lv2/lv2plug.in/ns/ext/presets/presets.h" diff --git a/src/Lv2Host.hpp b/src/Lv2Host.hpp index 4e456f6..9a9c4f7 100644 --- a/src/Lv2Host.hpp +++ b/src/Lv2Host.hpp @@ -31,7 +31,7 @@ #include #include -#include "lv2/lv2core.lv2/lv2.h" +#include "lv2.h" #include "Units.hpp" diff --git a/src/MapFeature.hpp b/src/MapFeature.hpp index c4dbe78..15b13ea 100644 --- a/src/MapFeature.hpp +++ b/src/MapFeature.hpp @@ -21,7 +21,6 @@ #include "lv2.h" -#include "lv2/lv2core.lv2/lv2.h" #include "lv2/log.lv2/log.h" #include "lv2/log.lv2/logger.h" #include "lv2/midi.lv2/midi.h" diff --git a/src/ShutdownMain.cpp b/src/ShutdownMain.cpp index 4fa6b4c..a48cc48 100644 --- a/src/ShutdownMain.cpp +++ b/src/ShutdownMain.cpp @@ -125,13 +125,11 @@ private: std::string response; public: - template // either execution_context (boost 1.74) or io_context (boost 1.63) - Reader(T &ios) + Reader(asio::io_service &ios) : socket(ios) { } - template // either execution_context (boost 1.74) or io_context (boost 1.63) - static std::shared_ptr Create(T &ios) + static std::shared_ptr Create(asio::io_service &ios) { return std::make_shared(ios); } @@ -290,6 +288,7 @@ class Server { private: tcp::acceptor acceptor_; + boost::asio::io_service &io_service; void HandleAccept(std::shared_ptr connection, const boost::system::error_code &err) { @@ -303,7 +302,7 @@ private: void StartAccept() { // socket - std::shared_ptr reader = Reader::Create(acceptor_.get_executor().context()); + std::shared_ptr reader = Reader::Create(io_service); // asynchronous accept operation and wait for a new connection. acceptor_.async_accept(reader->Socket(), @@ -314,7 +313,9 @@ private: public: //constructor for accepting connection from client Server(boost::asio::io_service &io_service, const tcp::endpoint &endpoint) - : acceptor_(io_service, endpoint) + : acceptor_(io_service, endpoint), + io_service(io_service) + { StartAccept(); } diff --git a/src/SysExec.cpp b/src/SysExec.cpp index 8a6e5cc..b2e18f2 100644 --- a/src/SysExec.cpp +++ b/src/SysExec.cpp @@ -29,8 +29,11 @@ using namespace pipedal; using namespace std; +#pragma GCC diagnostic ignored "-Wunused-result" + // find on path, but ONLY /usr/bin and /usr/sbin + static std::filesystem::path findOnSystemPath(const std::string &command) { if (command.length() != 0 && command[0] == '/') @@ -76,7 +79,6 @@ void pipedal::SilentSysExec(const char *szCommand) pclose(output); } } - int pipedal::SysExec(const char *szCommand) { char *args = strdup(szCommand); diff --git a/src/Worker.hpp b/src/Worker.hpp index 81f7f50..b72aebb 100644 --- a/src/Worker.hpp +++ b/src/Worker.hpp @@ -21,11 +21,11 @@ #pragma once #include -#include "lv2/lv2core.lv2/lv2.h" +#include "lv2.h" #include "lv2/atom.lv2/atom.h" #include "lv2/atom.lv2/util.h" -#include "lv2/lv2core.lv2/lv2.h" +#include "lv2.h" #include "lv2/log.lv2/log.h" #include "lv2/log.lv2/logger.h" #include "lv2/midi.lv2/midi.h" diff --git a/src/main.cpp b/src/main.cpp index 79e9989..1a7471e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -40,7 +40,6 @@ #include using namespace pipedal; -using namespace boost::beast; #define PRESET_EXTENSION ".piPreset" #define BANK_EXTENSION ".piBank" @@ -83,7 +82,7 @@ public: { } - virtual bool wants(boost::beast::http::verb verb, const uri &request_uri) const + virtual bool wants(const std::string& method, const uri &request_uri) const { if (request_uri.segment_count() != 2 || request_uri.segment(0) != "var") { @@ -156,9 +155,9 @@ public: virtual void head_response( const uri &request_uri, - const boost::beast::http::request &req, - boost::beast::http::response &res, - boost::beast::error_code &ec) + const HttpRequest &req, + HttpResponse &res, + std::error_code &ec) { try { @@ -168,10 +167,10 @@ public: std::string content; GetPreset(request_uri, &name, &content); - res.set(http::field::content_type, "application/octet-stream"); - res.set(http::field::cache_control, "no-cache"); - res.set(http::field::content_length, content.length()); - res.set(http::field::content_disposition, GetContentDispositionHeader(name, PRESET_EXTENSION)); + res.set(HttpField::content_type, "application/octet-stream"); + res.set(HttpField::cache_control, "no-cache"); + res.setContentLength(content.length()); + res.set(HttpField::content_disposition, GetContentDispositionHeader(name, PRESET_EXTENSION)); return; } if (request_uri.segment(1) == "downloadBank") @@ -180,10 +179,10 @@ public: std::string content; GetBank(request_uri, &name, &content); - res.set(http::field::content_type, "application/octet-stream"); - res.set(http::field::cache_control, "no-cache"); - res.set(http::field::content_length, content.length()); - res.set(http::field::content_disposition, GetContentDispositionHeader(name, BANK_EXTENSION)); + res.set(HttpField::content_type, "application/octet-stream"); + res.set(HttpField::cache_control, "no-cache"); + res.set(HttpField::content_disposition, GetContentDispositionHeader(name, BANK_EXTENSION)); + res.setContentLength(content.length()); return; } throw PiPedalException("Not found."); @@ -203,9 +202,9 @@ public: virtual void get_response( const uri &request_uri, - const boost::beast::http::request &req, - boost::beast::http::response &res, - boost::beast::error_code &ec) + const HttpRequest &req, + HttpResponse &res, + std::error_code &ec) { try { @@ -215,11 +214,11 @@ public: std::string content; GetPreset(request_uri, &name, &content); - res.set(http::field::content_type, "application/octet-stream"); - res.set(http::field::cache_control, "no-cache"); - res.set(http::field::content_length, content.length()); - res.set(http::field::content_disposition, GetContentDispositionHeader(name, PRESET_EXTENSION)); - res.body() = content; + res.set(HttpField::content_type, "application/octet-stream"); + res.set(HttpField::cache_control, "no-cache"); + res.setContentLength(content.length()); + res.set(HttpField::content_disposition, GetContentDispositionHeader(name, PRESET_EXTENSION)); + res.setBody(content); } else if (request_uri.segment(1) == "downloadBank") { @@ -227,11 +226,11 @@ public: std::string content; GetBank(request_uri, &name, &content); - res.set(http::field::content_type, "application/octet-stream"); - res.set(http::field::cache_control, "no-cache"); - res.set(http::field::content_length, content.length()); - res.set(http::field::content_disposition, GetContentDispositionHeader(name, BANK_EXTENSION)); - res.body() = content; + res.set(HttpField::content_type, "application/octet-stream"); + res.set(HttpField::cache_control, "no-cache"); + res.setContentLength(content.length()); + res.set(HttpField::content_disposition, GetContentDispositionHeader(name, BANK_EXTENSION)); + res.setBody(content); } else { @@ -252,15 +251,15 @@ public: } virtual void post_response( const uri &request_uri, - const boost::beast::http::request &req, - boost::beast::http::response &res, - boost::beast::error_code &ec) + const HttpRequest &req, + HttpResponse &res, + std::error_code &ec) { try { if (request_uri.segment(1) == "uploadPreset") { - std::string presetBody = req.body(); + const std::string& presetBody = req.body(); std::stringstream s(presetBody); json_reader reader(s); @@ -276,19 +275,19 @@ public: uint64_t instanceId = model->uploadPreset(bankFile, uploadAfter); - res.set(http::field::content_type, "application/json"); - res.set(http::field::cache_control, "no-cache"); + res.set(HttpField::content_type, "application/json"); + res.set(HttpField::cache_control, "no-cache"); std::stringstream sResult; sResult << instanceId; std::string result = sResult.str(); - res.set(http::field::content_length, result.length()); + res.setContentLength(result.length()); - res.body() = result; + res.setBody(result); } else if (request_uri.segment(1) == "uploadBank") { - std::string presetBody = req.body(); - std::stringstream s(presetBody); + const std::string& presetBody = req.body(); + std::istringstream s(presetBody); json_reader reader(s); uint64_t uploadAfter = -1; @@ -303,14 +302,14 @@ public: uint64_t instanceId = model->uploadBank(bankFile, uploadAfter); - res.set(http::field::content_type, "application/json"); - res.set(http::field::cache_control, "no-cache"); + res.set(HttpField::content_type, "application/json"); + res.set(HttpField::cache_control, "no-cache"); std::stringstream sResult; sResult << instanceId; std::string result = sResult.str(); - res.set(http::field::content_length, result.length()); + res.setContentLength(result.length()); - res.body() = result; + res.setBody(result); } else { @@ -354,81 +353,31 @@ public: } virtual ~InterceptConfig() {} - virtual void head_response( - const uri &request_uri, - const boost::beast::http::request &req, - boost::beast::http::response &res, - boost::beast::error_code &ec) + virtual void head_response( + const uri&request_uri, + const HttpRequest &req, + HttpResponse &res, + std::error_code &ec) { - res.set(http::field::content_type, "application/json"); - res.set(http::field::cache_control, "no-cache"); - res.content_length(response.length()); + res.set(HttpField::content_type, "application/json"); + res.set(HttpField::cache_control, "no-cache"); + res.setContentLength(response.length()); return; } virtual void get_response( const uri &request_uri, - const boost::beast::http::request &req, - boost::beast::http::response &res, - boost::beast::error_code &ec) + const HttpRequest &req, + HttpResponse &res, + std::error_code &ec) { - res.set(http::field::content_type, "application/json"); - res.set(http::field::cache_control, "no-cache"); - res.content_length(response.length()); - res.body() = response; + res.set(HttpField::content_type, "application/json"); + res.set(HttpField::cache_control, "no-cache"); + res.setContentLength(response.length()); + res.setBody(response); } }; -class ReactRedirector : public RequestHandler -{ - PiPedalConfiguration config; - -public: - ReactRedirector(const PiPedalConfiguration &config) - : RequestHandler("/"), - config(config) - - { - } - virtual ~ReactRedirector() {} - - virtual bool wantsRedirect(const uri &requestUri) - { - if (requestUri.segment_count() == 0 || (requestUri.segment_count() == 1 && requestUri.segment(0) == "index.htm")) - { - return true; - } - return false; - } - uri GetRedirect(const uri &requestUri) - { - return requestUri; - } - - virtual bool wants(boost::beast::http::verb verb, const uri &request_uri) const - { - return false; - } - - virtual void head_response( - const uri &request_uri, - const boost::beast::http::request &req, - boost::beast::http::response &res, - boost::beast::error_code &ec) - { - ec = errc::make_error_code(errc::no_such_file_or_directory); - return; - } - - virtual void get_response( - const uri &request_uri, - const boost::beast::http::request &req, - boost::beast::http::response &res, - boost::beast::error_code &ec) - { - ec = errc::make_error_code(errc::no_such_file_or_directory); - } -}; static bool isJackServiceRunning() { @@ -553,18 +502,13 @@ int main(int argc, char *argv[]) auto const threads = std::max(1, configuration.GetThreads()); - server = std::make_shared( + server = createBeastServer( address, port, web_root.c_str(), threads); Lv2Log::info("Document root: %s Threads: %d", doc_root.c_str(), (int)threads); server->SetLogHttpRequests(configuration.LogHttpRequests()); - boost::asio::ip::network_v4 gateway; - if (configuration.GetAccessPointGateway(&gateway)) - { - server->SetAccessPointGateway(gateway, configuration.GetAccessPointServerAddress()); - } } catch (const std::exception &e) { @@ -655,7 +599,7 @@ int main(int argc, char *argv[]) Lv2Log::info("Shutting down gracefully."); model.Close(); Lv2Log::info("Stopping web server."); - server->Stop(); + server->ShutDown(5000); server->Join(); Lv2Log::info("Shutdown complete."); }