From 65932ee343f5ee7a5725d35e7a847f4ae2a2624e Mon Sep 17 00:00:00 2001 From: Vinnie Falco Date: Sat, 17 Jun 2017 13:42:13 -0700 Subject: [PATCH] Add server-framework SSL HTTP and WebSocket ports --- CHANGELOG.md | 1 + CMakeLists.txt | 11 +- doc/2_examples.qbk | 4 + example/server-framework/CMakeLists.txt | 8 + example/server-framework/http_async_port.hpp | 126 ++++-- example/server-framework/http_sync_port.hpp | 185 ++++++-- example/server-framework/https_ports.hpp | 368 ++++++++++++++++ example/server-framework/main.cpp | 153 +++++++ example/server-framework/ssl_certificate.hpp | 146 ++++++ .../server-framework/{ssl => }/ssl_stream.hpp | 104 ++++- example/server-framework/ws_async_port.hpp | 102 +++-- example/server-framework/ws_sync_port.hpp | 154 ++++--- example/server-framework/wss_ports.hpp | 414 ++++++++++++++++++ test/server/CMakeLists.txt | 17 + test/server/Jamfile | 3 + test/server/https_ports.cpp | 13 + test/server/ssl_certificate.cpp | 13 + test/server/ssl_stream.cpp | 13 + test/server/wss_ports.cpp | 13 + 19 files changed, 1648 insertions(+), 200 deletions(-) create mode 100644 example/server-framework/https_ports.hpp create mode 100644 example/server-framework/ssl_certificate.hpp rename example/server-framework/{ssl => }/ssl_stream.hpp (68%) create mode 100644 example/server-framework/wss_ports.hpp create mode 100644 test/server/https_ports.cpp create mode 100644 test/server/ssl_certificate.cpp create mode 100644 test/server/ssl_stream.cpp create mode 100644 test/server/wss_ports.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index b53420de..73126935 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Version 61: * Tidy up names in error categories * Flush the output stream in the example * Clean close in Secure WebSocket client +* Add server-framework SSL HTTP and WebSocket ports API Changes: diff --git a/CMakeLists.txt b/CMakeLists.txt index 77dd4cda..daa75df1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -70,6 +70,13 @@ endif() find_package(OpenSSL) +if (OPENSSL_FOUND) + add_definitions (-DBEAST_USE_OPENSSL=1) +else() + add_definitions (-DBEAST_USE_OPENSSL=0) + message("OpenSSL not found.") +endif() + # #------------------------------------------------------------------------------- @@ -182,9 +189,7 @@ add_subdirectory (test) add_subdirectory (example) -if (NOT OPENSSL_FOUND) - message("OpenSSL not found. Not building SSL tests and examples") -else() +if (OPENSSL_FOUND) add_subdirectory (example/ssl-http-client) add_subdirectory (example/ssl-websocket-client) add_subdirectory (test/websocket/ssl) diff --git a/doc/2_examples.qbk b/doc/2_examples.qbk index b2576952..dc12f50a 100644 --- a/doc/2_examples.qbk +++ b/doc/2_examples.qbk @@ -113,14 +113,18 @@ for writing their own servers. It serves both HTTP and WebSocket. * [repo_file example/server-framework/http_async_port.hpp] * [repo_file example/server-framework/http_base.hpp] * [repo_file example/server-framework/http_sync_port.hpp] +* [repo_file example/server-framework/https_ports.hpp] * [repo_file example/server-framework/main.cpp] * [repo_file example/server-framework/rfc7231.hpp] * [repo_file example/server-framework/server.hpp] * [repo_file example/server-framework/service_list.hpp] +* [repo_file example/server-framework/ssl_certificate.hpp] +* [repo_file example/server-framework/ssl_stream.hpp] * [repo_file example/server-framework/write_msg.hpp] * [repo_file example/server-framework/ws_async_port.hpp] * [repo_file example/server-framework/ws_sync_port.hpp] * [repo_file example/server-framework/ws_upgrade_service.hpp] +* [repo_file example/server-framework/wss_ports.hpp] [endsect] diff --git a/example/server-framework/CMakeLists.txt b/example/server-framework/CMakeLists.txt index 70dbbc03..a51ed907 100644 --- a/example/server-framework/CMakeLists.txt +++ b/example/server-framework/CMakeLists.txt @@ -4,6 +4,10 @@ GroupSources(include/beast beast) GroupSources(example/server-framework "/") +if (OPENSSL_FOUND) + include_directories(${OPENSSL_INCLUDE_DIR}) +endif() + add_executable (server-framework ${BEAST_INCLUDES} ${SERVER_INCLUDES} @@ -15,3 +19,7 @@ target_link_libraries( Beast ${Boost_PROGRAM_OPTIONS_LIBRARY} ${Boost_FILESYSTEM_LIBRARY}) + +if (OPENSSL_FOUND) + target_link_libraries(server-framework ${OPENSSL_LIBRARIES}) +endif() diff --git a/example/server-framework/http_async_port.hpp b/example/server-framework/http_async_port.hpp index 5b266c91..f766bcdd 100644 --- a/example/server-framework/http_async_port.hpp +++ b/example/server-framework/http_async_port.hpp @@ -118,7 +118,7 @@ make_queued_http_write( @tparam Services The list of services this connection will support. */ template -class async_http_con : public http_base +class async_http_con_base : public http_base { // This function lets us access members of the derived class Derived& @@ -161,7 +161,7 @@ protected: public: // Constructor - async_http_con( + async_http_con_base( beast::string_view server_name, std::ostream& log, service_list const& services, @@ -180,15 +180,36 @@ public: { } - // Called by the port after creating the object void run() { - // Start reading the header for the first request. + // Give the derived class a chance to do stuff // + impl().do_handshake(); + } + +protected: + void + do_run() + { do_read_header(); } + // Called when a failure occurs + // + void + fail(std::string what, error_code ec) + { + // Don't log operation aborted since those happen normally. + // + if(ec && ec != boost::asio::error::operation_aborted) + { + log_ << + "[#" << id_ << " " << ep_ << "] " << + what << ": " << ec.message() << std::endl; + } + } + private: // Perform an asynchronous read for the next request header // @@ -214,7 +235,7 @@ private: buffer_, *parser_, strand_.wrap(std::bind( - &async_http_con::on_read_header, + &async_http_con_base::on_read_header, impl().shared_from_this(), std::placeholders::_1))); } @@ -228,12 +249,12 @@ private: struct send_lambda { // holds "this" - async_http_con& self_; + async_http_con_base& self_; public: // capture "this" explicit - send_lambda(async_http_con& self) + send_lambda(async_http_con_base& self) : self_(self) { } @@ -251,9 +272,17 @@ private: void on_read_header(error_code ec) { + // This happens when the other end closes gracefully + // + if(ec == beast::http::error::end_of_stream) + { + // VFALCO what about the write queue? + return impl().do_shutdown(); + } + // On failure we just return, the shared_ptr that is bound // into the completion will go out of scope and eventually - // we will get destroyed. + // this will get destroyed. // if(ec) return fail("on_read", ec); @@ -281,7 +310,7 @@ private: buffer_, *parser_, strand_.wrap(std::bind( - &async_http_con::on_read, + &async_http_con_base::on_read, impl().shared_from_this(), std::placeholders::_1))); } @@ -290,6 +319,13 @@ private: void on_read(error_code ec) { + // Shouldn't be getting end_of_stream here; + // that would mean that we got an incomplete + // message, counting as an error. + // + if(ec) + return fail("on_read", ec); + // Grab a reference to the request again auto& req = parser_->get(); @@ -298,10 +334,10 @@ private: // send_lambda send{*this}; - // Give each services a chance to handle the request + // Give each service a chance to handle the request // if(! services_.respond( - impl().stream(), + std::move(impl().stream()), ep_, std::move(req), send)) @@ -319,7 +355,7 @@ private: if(! impl().stream().lowest_layer().is_open()) { // They took ownership so just return and - // let this async_http_con object get destroyed. + // let this async_http_con_base object get destroyed. // return; } @@ -357,7 +393,7 @@ private: impl().stream(), std::move(res), strand_.wrap(std::bind( - &async_http_con::on_write, + &async_http_con_base::on_write, impl().shared_from_this(), std::placeholders::_1))); } @@ -369,7 +405,7 @@ private: impl().stream(), std::move(res), strand_.wrap(std::bind( - &async_http_con::on_write, + &async_http_con_base::on_write, impl().shared_from_this(), std::placeholders::_1)))); } @@ -381,6 +417,14 @@ private: // Make sure our state is what we think it is BOOST_ASSERT(writing_); + // This happens when we send an HTTP message + // whose semantics indicate that the connection + // should be closed afterwards. For example if + // we send a Connection: close. + // + if(ec == beast::http::error::end_of_stream) + return impl().do_shutdown(); + // On failure just log and return if(ec) return fail("on_write", ec); @@ -403,23 +447,6 @@ private: // Delete the item since we used it queue_.erase(queue_.begin()); } - - // Called when a failure occurs - // - void - fail(std::string what, error_code ec) - { - // Don't log end of stream or operation aborted - // since those happen under normal circumstances. - // - if( ec != beast::http::error::end_of_stream && - ec != boost::asio::error::operation_aborted) - { - log_ << - "[#" << id_ << " " << ep_ << "] " << - what << ": " << ec.message() << std::endl; - } - } }; //------------------------------------------------------------------------------ @@ -428,7 +455,7 @@ private: // uses a plain TCP/IP socket (no encryption) as the stream. // template -class async_http_con_plain +class async_http_con // Note that we give this object the enable_shared_from_this, and have // the base class call impl().shared_from_this(). The reason we do that @@ -436,7 +463,7 @@ class async_http_con_plain // derived class (this class) use its members in calls to std::bind, // without an ugly call to `dynamic_downcast` or other nonsense. // - : public std::enable_shared_from_this> + : public std::enable_shared_from_this> // We want the socket to be created before the base class so we use // the "base from member" idiom which Boost provides as a class. @@ -445,17 +472,17 @@ class async_http_con_plain // Declare this base last now that everything else got set up first. // - , public async_http_con, Services...> + , public async_http_con_base, Services...> { public: // Construct the plain connection. // template - async_http_con_plain( + async_http_con( socket_type&& sock, Args&&... args) : base_from_member(std::move(sock)) - , async_http_con, Services...>( + , async_http_con_base, Services...>( std::forward(args)...) { } @@ -469,6 +496,31 @@ public: { return this->member; } + +private: + // Base class needs to be a friend to call our private members + friend class async_http_con_base, Services...>; + + // This is called by the base before running the main loop. + // + void + do_handshake() + { + // Run the main loop right away + // + this->do_run(); + } + + // This is called when the other end closes the connection gracefully. + // + void + do_shutdown() + { + error_code ec; + stream().shutdown(socket_type::shutdown_both, ec); + if(ec) + return this->fail("shutdown", ec); + } }; //------------------------------------------------------------------------------ @@ -539,7 +591,7 @@ public: // Create a plain http connection object // and transfer ownership of the socket. // - std::make_shared>( + std::make_shared>( std::move(sock), "http_async_port", log_, diff --git a/example/server-framework/http_sync_port.hpp b/example/server-framework/http_sync_port.hpp index 891619e2..b1adfbb2 100644 --- a/example/server-framework/http_sync_port.hpp +++ b/example/server-framework/http_sync_port.hpp @@ -42,7 +42,7 @@ namespace framework { @tparam Services The list of services this connection will support. */ template -class sync_http_con +class sync_http_con_base : public http_base { // This function lets us access members of the derived class @@ -71,7 +71,7 @@ class sync_http_con public: /// Constructor - sync_http_con( + sync_http_con_base( beast::string_view server_name, std::ostream& log, service_list const& services, @@ -90,19 +90,36 @@ public: { } + // This is called to start the connection after + // it is accepted. + // void run() { // Bind a shared pointer into the lambda for the - // thread, so the sync_http_con is destroyed after + // thread, so the sync_http_con_base is destroyed after // the thread function exits. // std::thread{ - &sync_http_con::do_run, + &sync_http_con_base::do_run, impl().shared_from_this() }.detach(); } +protected: + // Called when a failure occurs + // + void + fail(std::string what, error_code ec) + { + if(ec) + { + log_ << + "[#" << id_ << " " << ep_ << "] " << + what << ": " << ec.message() << std::endl; + } + } + private: // This lambda is passed to the service list to handle // the case of sending request objects of varying types. @@ -113,7 +130,7 @@ private: struct send_lambda { // holds "this" - sync_http_con& self_; + sync_http_con_base& self_; // holds the captured error code error_code& ec_; @@ -123,7 +140,7 @@ private: // // Capture "this" and "ec" // - send_lambda(sync_http_con& self, error_code& ec) + send_lambda(sync_http_con_base& self, error_code& ec) : self_(self) , ec_(ec) { @@ -146,6 +163,16 @@ private: void do_run() { + error_code ec; + + // Give the derived class a chance to do stuff before we + // enter the main loop. This is for SSL connections really. + // + impl().do_handshake(ec); + + if(ec) + return fail("handshake", ec); + // The main connection loop, we alternate between // reading a request and sending a response. On // error we log and return, which destroys the thread @@ -153,8 +180,6 @@ private: // for(;;) { - error_code ec; - // Arguments passed to the parser constructor are // forwarded to the message object. A single argument // is forwarded to the body constructor. @@ -167,8 +192,20 @@ private: // Read the header first beast::http::read_header(impl().stream(), buffer_, parser, ec); + // This happens when the other end closes gracefully + // + if(ec == beast::http::error::end_of_stream) + { + // Give the derived class a chance to do stuff + impl().do_shutdown(ec); + if(ec) + return fail("shutdown", ec); + return; + } + + // Any other error and we fail the connection if(ec) - return fail("on_read", ec); + return fail("read_header", ec); send_lambda send{*this, ec}; @@ -182,58 +219,105 @@ private: // so send the appropriate response synchronously. // send(this->continue_100(req)); + + // This happens when we send an HTTP message + // whose semantics indicate that the connection + // should be closed afterwards. For example if + // we send a Connection: close. + // + if(ec == beast::http::error::end_of_stream) + { + // Give the derived class a chance to do stuff + impl().do_shutdown(ec); + if(ec) + return fail("shutdown", ec); + return; + } + + // Have to check the error every time we call the lambda + // + if(ec) + return fail("write", ec); } // Read the rest of the message, if any. // beast::http::read(impl().stream(), buffer_, parser, ec); - // Give each services a chance to handle the request + // Shouldn't be getting end_of_stream here; + // that would mean that we got an incomplete + // message, counting as an error. + // + if(ec) + return fail("read", ec); + + // Give each service a chance to handle the request // if(! services_.respond( - impl().stream(), - ep_, - std::move(req), - send)) + std::move(impl().stream()), + ep_, + std::move(req), + send)) { // No service handled the request, // send a Bad Request result to the client. // send(this->bad_request(req)); + + // This happens when we send an HTTP message + // whose semantics indicate that the connection + // should be closed afterwards. For example if + // we send a Connection: close. + // + if(ec == beast::http::error::end_of_stream) + { + // Give the derived class a chance to do stuff + impl().do_shutdown(ec); + if(ec) + return fail("shutdown", ec); + return; + } + + // Have to check the error every time we call the lambda + // + if(ec) + return fail("write", ec); } else { + // This happens when we send an HTTP message + // whose semantics indicate that the connection + // should be closed afterwards. For example if + // we send a Connection: close. + // + if(ec == beast::http::error::end_of_stream) + { + // Give the derived class a chance to do stuff + impl().do_shutdown(ec); + if(ec) + return fail("shutdown", ec); + return; + } + + // Have to check the error every time we call the lambda + // + if(ec) + return fail("write", ec); + // See if the service that handled the // response took ownership of the stream. - if(! impl().stream().is_open()) + if(! impl().stream().lowest_layer().is_open()) { // They took ownership so just return and - // let this sync_http_con object get destroyed. + // let this sync_http_con_base object get destroyed. return; } } - if(ec) - return fail("on_write", ec); - // Theres no pipelining possible in a synchronous server // because we can't do reads and writes at the same time. } } - - // Called when a failure occurs - // - void - fail(std::string what, error_code ec) - { - if( ec != beast::http::error::end_of_stream && - ec != boost::asio::error::operation_aborted) - { - log_ << - "[#" << id_ << " " << ep_ << "] " << - what << ": " << ec.message() << std::endl; - } - } }; //------------------------------------------------------------------------------ @@ -242,12 +326,12 @@ private: // uses a plain TCP/IP socket (no encryption) as the stream. // template -class sync_http_con_plain +class sync_http_con // Note that we give this object the `enable_shared_from_this`, and have // the base class call `impl().shared_from_this()` when needed. // - : public std::enable_shared_from_this> + : public std::enable_shared_from_this> // We want the socket to be created before the base class so we use // the "base from member" idiom which Boost provides as a class. @@ -256,17 +340,17 @@ class sync_http_con_plain // Declare this base last now that everything else got set up first. // - , public sync_http_con, Services...> + , public sync_http_con_base, Services...> { public: // Construct the plain connection. // template - sync_http_con_plain( + sync_http_con( socket_type&& sock, Args&&... args) : base_from_member(std::move(sock)) - , sync_http_con, Services...>( + , sync_http_con_base, Services...>( std::forward(args)...) { } @@ -281,6 +365,29 @@ public: { return this->member; } + +private: + // Base class needs to be a friend to call our private members + friend class sync_http_con_base, Services...>; + + // This is called by the base before running the main loop. + // There's nothing to do for a plain connection. + // + void + do_handshake(error_code& ec) + { + // This is required by the specifications for error_code + // + ec = {}; + } + + // This is called when the other end closes the connection gracefully. + // + void + do_shutdown(error_code& ec) + { + stream().shutdown(socket_type::shutdown_both, ec); + } }; //------------------------------------------------------------------------------ @@ -345,7 +452,7 @@ public: // Create a plain http connection object // and transfer ownership of the socket. // - std::make_shared>( + std::make_shared>( std::move(sock), "http_sync_port", log_, diff --git a/example/server-framework/https_ports.hpp b/example/server-framework/https_ports.hpp new file mode 100644 index 00000000..a1e06792 --- /dev/null +++ b/example/server-framework/https_ports.hpp @@ -0,0 +1,368 @@ +// +// Copyright (c) 2013-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) +// + +#ifndef BEAST_EXAMPLE_SERVER_HTTPS_PORTS_HPP +#define BEAST_EXAMPLE_SERVER_HTTPS_PORTS_HPP + +#include "http_sync_port.hpp" +#include "http_async_port.hpp" +#include "ssl_stream.hpp" + +#include + +namespace framework { + +//------------------------------------------------------------------------------ + +// This class represents a synchronous HTTP connection which +// uses an OpenSSL socket as the stream. +// +template +class sync_https_con + + // Note that we give this object the enable_shared_from_this, and have + // the base class call impl().shared_from_this(). The reason we do that + // is so that the shared_ptr has the correct type. This lets the + // derived class (this class) use its members in calls to std::bind, + // without an ugly call to `dynamic_downcast` or other nonsense. + // + : public std::enable_shared_from_this> + + // We want the socket to be created before the base class so we use + // the "base from member" idiom which Boost provides as a class. + // + , public base_from_member> + + // Declare this base last now that everything else got set up first. + // + , public sync_http_con_base, Services...> +{ +public: + // Construct the plain connection. + // + template + sync_https_con( + socket_type&& sock, + boost::asio::ssl::context& ctx, + Args&&... args) + : base_from_member>(std::move(sock), ctx) + , sync_http_con_base, Services...>( + std::forward(args)...) + { + } + + // Returns the stream. + // The base class calls this to obtain the object to + // use for reading and writing HTTP messages. + // + ssl_stream& + stream() + { + return this->member; + } + +private: + friend class sync_http_con_base, Services...>; + + // This is called by the base before running the main loop. + // + void + do_handshake(error_code& ec) + { + // Perform the SSL handshake + // + stream().handshake(boost::asio::ssl::stream_base::server, ec); + } + + // This is called when the other end closes the connection gracefully. + // + void + do_shutdown(error_code& ec) + { + // Note that this is an SSL shutdown + // + stream().shutdown(ec); + if(ec) + return this->fail("shutdown", ec); + } +}; + +//------------------------------------------------------------------------------ + +// This class represents an asynchronous HTTP connection which +// uses an OpenSSL socket as the stream. +// +template +class async_https_con + + // Note that we give this object the enable_shared_from_this, and have + // the base class call impl().shared_from_this(). The reason we do that + // is so that the shared_ptr has the correct type. This lets the + // derived class (this class) use its members in calls to std::bind, + // without an ugly call to `dynamic_downcast` or other nonsense. + // + : public std::enable_shared_from_this> + + // We want the socket to be created before the base class so we use + // the "base from member" idiom which Boost provides as a class. + // + , public base_from_member> + + // Declare this base last now that everything else got set up first. + // + , public async_http_con_base, Services...> +{ +public: + // Construct the plain connection. + // + template + async_https_con( + socket_type&& sock, + boost::asio::ssl::context& ctx, + Args&&... args) + : base_from_member>(std::move(sock), ctx) + , async_http_con_base, Services...>( + std::forward(args)...) + { + } + + // Returns the stream. + // The base class calls this to obtain the object to + // use for reading and writing HTTP messages. + // + ssl_stream& + stream() + { + return this->member; + } + +private: + friend class async_http_con_base, Services...>; + + // Called by the base class before starting the main loop. + // + void + do_handshake() + { + // This is SSL so perform the handshake + // + stream().async_handshake( + boost::asio::ssl::stream_base::server, + this->strand_.wrap(std::bind( + &async_https_con::on_handshake, + this->shared_from_this(), + std::placeholders::_1))); + } + + // Called when the SSL handshake completes + void + on_handshake(error_code ec) + { + if(ec) + return this->fail("on_handshake", ec); + + // No error so run the main loop + this->do_run(); + } + + // Called when the end of stream is reached + void + do_shutdown() + { + // This is an SSL shutdown + // + stream().async_shutdown( + this->strand_.wrap(std::bind( + &async_https_con::on_shutdown, + this->shared_from_this(), + std::placeholders::_1))); + } + + // Called when the SSL shutdown completes + void + on_shutdown(error_code ec) + { + if(ec) + return this->fail("on_shutdown", ec); + } +}; + +//------------------------------------------------------------------------------ + +/* A synchronous HTTPS port handler + + This type meets the requirements of @b PortHandler. It supports + variable list of HTTP services in its template parameter list, + and provides a synchronous connection implementation to service +*/ +template +class https_sync_port +{ + // Reference to the server instance that made us + server& instance_; + + // The stream to log to + std::ostream& log_; + + // The list of services connections created from this port will support + service_list services_; + + // The SSL context containing the server's credentials + boost::asio::ssl::context& ctx_; + +public: + /** Constructor + + @param instance The server instance which owns this port + + @param log The stream to use for logging + + @param ctx The SSL context holding the SSL certificates to use + */ + https_sync_port( + server& instance, + std::ostream& log, + boost::asio::ssl::context& ctx) + : instance_(instance) + , log_(log) + , ctx_(ctx) + { + } + + /** Initialize a service + + Every service in the list must be initialized exactly once. + + @param args Optional arguments forwarded to the service + constructor. + + @tparam Index The 0-based index of the service to initialize. + + @return A reference to the service list. This permits + calls to be chained in a single expression. + */ + template + void + init(error_code& ec, Args&&... args) + { + services_.template init( + ec, + std::forward(args)...); + } + + /** Called by the server to provide ownership of the socket for a new connection + + @param sock The socket whose ownership is to be transferred + + @ep The remote endpoint + */ + void + on_accept(socket_type&& sock, endpoint_type ep) + { + // Create an HTTPS connection object + // and transfer ownership of the socket. + // + std::make_shared>( + std::move(sock), + ctx_, + "https_sync_port", + log_, + services_, + instance_.next_id(), + ep + )->run(); + } +}; + +//------------------------------------------------------------------------------ + +/* An asynchronous HTTPS port handler + + This type meets the requirements of @b PortHandler. It supports + variable list of HTTP services in its template parameter list, + and provides a synchronous connection implementation to service +*/ +template +class https_async_port +{ + // Reference to the server instance that made us + server& instance_; + + // The stream to log to + std::ostream& log_; + + // The list of services connections created from this port will support + service_list services_; + + // The SSL context containing the server's credentials + boost::asio::ssl::context& ctx_; + +public: + /** Constructor + + @param instance The server instance which owns this port + + @param log The stream to use for logging + */ + https_async_port( + server& instance, + std::ostream& log, + boost::asio::ssl::context& ctx) + : instance_(instance) + , log_(log) + , ctx_(ctx) + { + } + + /** Initialize a service + + Every service in the list must be initialized exactly once. + + @param args Optional arguments forwarded to the service + constructor. + + @tparam Index The 0-based index of the service to initialize. + + @return A reference to the service list. This permits + calls to be chained in a single expression. + */ + template + void + init(error_code& ec, Args&&... args) + { + services_.template init( + ec, + std::forward(args)...); + } + + /** Called by the server to provide ownership of the socket for a new connection + + @param sock The socket whose ownership is to be transferred + + @ep The remote endpoint + */ + void + on_accept(socket_type&& sock, endpoint_type ep) + { + // Create an HTTPS connection object + // and transfer ownership of the socket. + // + std::make_shared>( + std::move(sock), + ctx_, + "https_async_port", + log_, + services_, + instance_.next_id(), + ep + )->run(); + } +}; + +} // framework + +#endif diff --git a/example/server-framework/main.cpp b/example/server-framework/main.cpp index aaeb96f2..ff14ac7d 100644 --- a/example/server-framework/main.cpp +++ b/example/server-framework/main.cpp @@ -12,6 +12,12 @@ #include "ws_async_port.hpp" #include "ws_sync_port.hpp" +#if BEAST_USE_OPENSSL +#include "https_ports.hpp" +#include "wss_ports.hpp" +#include "ssl_certificate.hpp" +#endif + #include "file_service.hpp" #include "ws_upgrade_service.hpp" @@ -133,6 +139,13 @@ main( // Create our server instance with the specified number of threads server instance{threads}; + //-------------------------------------------------------------------------- + // + // Asynchronous WebSocket HTTP + // + // port port + 1 + // + //-------------------------------------------------------------------------- { // Install an asynchronous WebSocket echo port handler // @@ -185,6 +198,13 @@ main( return fail("http_async_port/file_service", ec); } + //-------------------------------------------------------------------------- + // + // Synchronous WebSocket HTTP + // + // port + 2 port + 3 + // + //-------------------------------------------------------------------------- { // Install a synchronous WebSocket echo port handler // @@ -238,5 +258,138 @@ main( return fail("http_sync_port/file_service", ec); } + // + // If OpenSSL is available then install some SSL-enabled ports + // + +#if BEAST_USE_OPENSSL + + ssl_certificate cert; + + //-------------------------------------------------------------------------- + // + // Asynchronous Secure WebSocket HTTPS + // + // port + 4 port + 5 + // + //-------------------------------------------------------------------------- + { + // Install an asynchronous Secure WebSocket echo port handler + // + auto wsp = instance.make_port( + ec, + endpoint_type{addr, + static_cast(port + 4)}, + instance, + std::cout, + cert.get(), + set_ws_options{pmd} + ); + + if(ec) + return fail("ws_async_port", ec); + + // Install an asynchronous HTTPS port handler + // + auto sp = instance.make_port, + file_service + >>( + ec, + endpoint_type{addr, + static_cast(port + 5)}, + instance, + std::cout, + cert.get()); + + if(ec) + return fail("https_async_port", ec); + + // Set up the ws_upgrade_service. We will route upgrade + // requests to the websocket port handler created earlier. + // + sp->template init<0>( + ec, + wsp // The websocket port handler + ); + + if(ec) + return fail("https_async_port/ws_upgrade_service", ec); + + // Set up the file_service to point to the root path. + // + sp->template init<1>( + ec, + root, // The root path + "https_async_port" // The value for the Server field + ); + + if(ec) + return fail("https_async_port/file_service", ec); + } + + //-------------------------------------------------------------------------- + // + // Synchronous Secure WebSocket HTTPS + // + // port + 6 port + 7 + // + //-------------------------------------------------------------------------- + { + // Install a synchronous Secure WebSocket echo port handler + // + auto wsp = instance.make_port( + ec, + endpoint_type{addr, + static_cast(port + 6)}, + instance, + std::cout, + cert.get(), + set_ws_options{pmd}); + + if(ec) + return fail("wss_sync_port", ec); + + // Install a synchronous HTTPS port handler + // + auto sp = instance.make_port, + file_service + >>( + ec, + endpoint_type{addr, + static_cast(port + 7)}, + instance, + std::cout, + cert.get()); + + if(ec) + return fail("https_sync_port", ec); + + // Set up the ws_upgrade_service. We will route upgrade + // requests to the websocket port handler created earlier. + // + sp->template init<0>( + ec, + wsp // The websocket port handler + ); + + if(ec) + return fail("http_sync_port/ws_upgrade_service", ec); + + // Set up the file_service to point to the root path. + // + sp->template init<1>( + ec, + root, + "https_sync_port" + ); + + if(ec) + return fail("https_sync_port/file_service", ec); + } + +#endif + sig_wait(); } diff --git a/example/server-framework/ssl_certificate.hpp b/example/server-framework/ssl_certificate.hpp new file mode 100644 index 00000000..d8733779 --- /dev/null +++ b/example/server-framework/ssl_certificate.hpp @@ -0,0 +1,146 @@ +// +// Copyright (c) 2013-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) +// + +#ifndef BEAST_EXAMPLE_SERVER_SSL_CERTIFICATE_HPP +#define BEAST_EXAMPLE_SERVER_SSL_CERTIFICATE_HPP + +#include +#include +#include +#include + +namespace framework { + +// This sets up the self-signed certificate that the server +// uses for its encrypted connections + +class ssl_certificate +{ + // The template argument is gratuitous, to + // make the definition header-only without + // also making it inline. + // + template + void + construct(); + + boost::asio::ssl::context ctx_; + +public: + ssl_certificate() + : ctx_(boost::asio::ssl::context::sslv23) + { + construct(); + } + + boost::asio::ssl::context& + get() + { + return ctx_; + } +}; + +template +void +ssl_certificate::construct() +{ + /* + The certificate was generated from CMD.EXE on Windows 10 using: + + winpty openssl dhparam -out dh.pem 2048 + winpty openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 10000 -out cert.pem -subj "//C=US\ST=CA\L=Los Angeles\O=Beast\CN=www.example.com" + */ + + std::string const cert = + "-----BEGIN CERTIFICATE-----\n" + "MIIDaDCCAlCgAwIBAgIJAO8vBu8i8exWMA0GCSqGSIb3DQEBCwUAMEkxCzAJBgNV\n" + "BAYTAlVTMQswCQYDVQQIDAJDQTEtMCsGA1UEBwwkTG9zIEFuZ2VsZXNPPUJlYXN0\n" + "Q049d3d3LmV4YW1wbGUuY29tMB4XDTE3MDUwMzE4MzkxMloXDTQ0MDkxODE4Mzkx\n" + "MlowSTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMS0wKwYDVQQHDCRMb3MgQW5n\n" + "ZWxlc089QmVhc3RDTj13d3cuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUA\n" + "A4IBDwAwggEKAoIBAQDJ7BRKFO8fqmsEXw8v9YOVXyrQVsVbjSSGEs4Vzs4cJgcF\n" + "xqGitbnLIrOgiJpRAPLy5MNcAXE1strVGfdEf7xMYSZ/4wOrxUyVw/Ltgsft8m7b\n" + "Fu8TsCzO6XrxpnVtWk506YZ7ToTa5UjHfBi2+pWTxbpN12UhiZNUcrRsqTFW+6fO\n" + "9d7xm5wlaZG8cMdg0cO1bhkz45JSl3wWKIES7t3EfKePZbNlQ5hPy7Pd5JTmdGBp\n" + "yY8anC8u4LPbmgW0/U31PH0rRVfGcBbZsAoQw5Tc5dnb6N2GEIbq3ehSfdDHGnrv\n" + "enu2tOK9Qx6GEzXh3sekZkxcgh+NlIxCNxu//Dk9AgMBAAGjUzBRMB0GA1UdDgQW\n" + "BBTZh0N9Ne1OD7GBGJYz4PNESHuXezAfBgNVHSMEGDAWgBTZh0N9Ne1OD7GBGJYz\n" + "4PNESHuXezAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCmTJVT\n" + "LH5Cru1vXtzb3N9dyolcVH82xFVwPewArchgq+CEkajOU9bnzCqvhM4CryBb4cUs\n" + "gqXWp85hAh55uBOqXb2yyESEleMCJEiVTwm/m26FdONvEGptsiCmF5Gxi0YRtn8N\n" + "V+KhrQaAyLrLdPYI7TrwAOisq2I1cD0mt+xgwuv/654Rl3IhOMx+fKWKJ9qLAiaE\n" + "fQyshjlPP9mYVxWOxqctUdQ8UnsUKKGEUcVrA08i1OAnVKlPFjKBvk+r7jpsTPcr\n" + "9pWXTO9JrYMML7d+XRSZA1n3856OqZDX4403+9FnXCvfcLZLLKTBvwwFgEFGpzjK\n" + "UEVbkhd5qstF6qWK\n" + "-----END CERTIFICATE-----\n"; + + std::string const key = + "-----BEGIN PRIVATE KEY-----\n" + "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDJ7BRKFO8fqmsE\n" + "Xw8v9YOVXyrQVsVbjSSGEs4Vzs4cJgcFxqGitbnLIrOgiJpRAPLy5MNcAXE1strV\n" + "GfdEf7xMYSZ/4wOrxUyVw/Ltgsft8m7bFu8TsCzO6XrxpnVtWk506YZ7ToTa5UjH\n" + "fBi2+pWTxbpN12UhiZNUcrRsqTFW+6fO9d7xm5wlaZG8cMdg0cO1bhkz45JSl3wW\n" + "KIES7t3EfKePZbNlQ5hPy7Pd5JTmdGBpyY8anC8u4LPbmgW0/U31PH0rRVfGcBbZ\n" + "sAoQw5Tc5dnb6N2GEIbq3ehSfdDHGnrvenu2tOK9Qx6GEzXh3sekZkxcgh+NlIxC\n" + "Nxu//Dk9AgMBAAECggEBAK1gV8uETg4SdfE67f9v/5uyK0DYQH1ro4C7hNiUycTB\n" + "oiYDd6YOA4m4MiQVJuuGtRR5+IR3eI1zFRMFSJs4UqYChNwqQGys7CVsKpplQOW+\n" + "1BCqkH2HN/Ix5662Dv3mHJemLCKUON77IJKoq0/xuZ04mc9csykox6grFWB3pjXY\n" + "OEn9U8pt5KNldWfpfAZ7xu9WfyvthGXlhfwKEetOuHfAQv7FF6s25UIEU6Hmnwp9\n" + "VmYp2twfMGdztz/gfFjKOGxf92RG+FMSkyAPq/vhyB7oQWxa+vdBn6BSdsfn27Qs\n" + "bTvXrGe4FYcbuw4WkAKTljZX7TUegkXiwFoSps0jegECgYEA7o5AcRTZVUmmSs8W\n" + "PUHn89UEuDAMFVk7grG1bg8exLQSpugCykcqXt1WNrqB7x6nB+dbVANWNhSmhgCg\n" + "VrV941vbx8ketqZ9YInSbGPWIU/tss3r8Yx2Ct3mQpvpGC6iGHzEc/NHJP8Efvh/\n" + "CcUWmLjLGJYYeP5oNu5cncC3fXUCgYEA2LANATm0A6sFVGe3sSLO9un1brA4zlZE\n" + "Hjd3KOZnMPt73B426qUOcw5B2wIS8GJsUES0P94pKg83oyzmoUV9vJpJLjHA4qmL\n" + "CDAd6CjAmE5ea4dFdZwDDS8F9FntJMdPQJA9vq+JaeS+k7ds3+7oiNe+RUIHR1Sz\n" + "VEAKh3Xw66kCgYB7KO/2Mchesu5qku2tZJhHF4QfP5cNcos511uO3bmJ3ln+16uR\n" + "GRqz7Vu0V6f7dvzPJM/O2QYqV5D9f9dHzN2YgvU9+QSlUeFK9PyxPv3vJt/WP1//\n" + "zf+nbpaRbwLxnCnNsKSQJFpnrE166/pSZfFbmZQpNlyeIuJU8czZGQTifQKBgHXe\n" + "/pQGEZhVNab+bHwdFTxXdDzr+1qyrodJYLaM7uFES9InVXQ6qSuJO+WosSi2QXlA\n" + "hlSfwwCwGnHXAPYFWSp5Owm34tbpp0mi8wHQ+UNgjhgsE2qwnTBUvgZ3zHpPORtD\n" + "23KZBkTmO40bIEyIJ1IZGdWO32q79nkEBTY+v/lRAoGBAI1rbouFYPBrTYQ9kcjt\n" + "1yfu4JF5MvO9JrHQ9tOwkqDmNCWx9xWXbgydsn/eFtuUMULWsG3lNjfst/Esb8ch\n" + "k5cZd6pdJZa4/vhEwrYYSuEjMCnRb0lUsm7TsHxQrUd6Fi/mUuFU/haC0o0chLq7\n" + "pVOUFq5mW8p0zbtfHbjkgxyF\n" + "-----END PRIVATE KEY-----\n"; + + std::string const dh = + "-----BEGIN DH PARAMETERS-----\n" + "MIIBCAKCAQEArzQc5mpm0Fs8yahDeySj31JZlwEphUdZ9StM2D8+Fo7TMduGtSi+\n" + "/HRWVwHcTFAgrxVdm+dl474mOUqqaz4MpzIb6+6OVfWHbQJmXPepZKyu4LgUPvY/\n" + "4q3/iDMjIS0fLOu/bLuObwU5ccZmDgfhmz1GanRlTQOiYRty3FiOATWZBRh6uv4u\n" + "tff4A9Bm3V9tLx9S6djq31w31Gl7OQhryodW28kc16t9TvO1BzcV3HjRPwpe701X\n" + "oEEZdnZWANkkpR/m/pfgdmGPU66S2sXMHgsliViQWpDCYeehrvFRHEdR9NV+XJfC\n" + "QMUk26jPTIVTLfXmmwU0u8vUkpR7LQKkwwIBAg==\n" + "-----END DH PARAMETERS-----\n"; + + ctx_.set_password_callback( + [](std::size_t size, + boost::asio::ssl::context_base::password_purpose) + { + return "test"; + }); + + ctx_.set_options( + boost::asio::ssl::context::default_workarounds | + boost::asio::ssl::context::no_sslv2 | + boost::asio::ssl::context::single_dh_use); + + ctx_.use_certificate_chain( + boost::asio::buffer(cert.data(), cert.size())); + + ctx_.use_private_key( + boost::asio::buffer(key.data(), key.size()), + boost::asio::ssl::context::file_format::pem); + + ctx_.use_tmp_dh( + boost::asio::buffer(dh.data(), dh.size())); +} + +} // framework + +#endif diff --git a/example/server-framework/ssl/ssl_stream.hpp b/example/server-framework/ssl_stream.hpp similarity index 68% rename from example/server-framework/ssl/ssl_stream.hpp rename to example/server-framework/ssl_stream.hpp index 715cb498..98052031 100644 --- a/example/server-framework/ssl/ssl_stream.hpp +++ b/example/server-framework/ssl_stream.hpp @@ -8,24 +8,40 @@ #ifndef BEAST_EXAMPLE_SERVER_SSL_STREAM_HPP #define BEAST_EXAMPLE_SERVER_SSL_STREAM_HPP +// This include is necessary to work with `ssl::stream` and `beast::websocket::stream` +#include + #include #include +#include #include +#include +#include -namespace beast { - -/** Movable SSL socket wrapper +/** C++11 enabled SSL socket wrapper This wrapper provides an interface identical to `boost::asio::ssl::stream`, - which is additionally move constructible and move assignable. + with the following additional properties: + + @li Satisfies @b MoveConstructible + + @li Satisfies @b MoveAssignable + + @li Constructible from a moved socket. */ template class ssl_stream : public boost::asio::ssl::stream_base { + // only works for boost::asio::ip::tcp::socket + // for now because of the move limitations + static_assert(std::is_same::value, + "NextLayer requirements not met"); + using stream_type = boost::asio::ssl::stream; std::unique_ptr p_; + boost::asio::ssl::context* ctx_; public: /// The native handle type of the SSL stream. @@ -43,15 +59,30 @@ public: /// The type of the lowest layer. using lowest_layer_type = typename stream_type::lowest_layer_type; - ssl_stream(ssl_stream&&) = default; - ssl_stream(ssl_stream const&) = delete; - ssl_stream& operator=(ssl_stream&&) = default; - ssl_stream& operator=(ssl_stream const&) = delete; - - template - ssl_stream(Args&&... args) - : p_(new stream_type{std::forward(args)...) + ssl_stream(boost::asio::ip::tcp::socket&& sock, boost::asio::ssl::context& ctx) + : p_(new stream_type{sock.get_io_service(), ctx}) + , ctx_(&ctx) { + p_->next_layer() = std::move(sock); + } + + ssl_stream(ssl_stream&& other) + : p_(new stream_type(other.get_io_service(), *other.ctx_)) + , ctx_(other.ctx_) + { + using std::swap; + swap(p_, other.p_); + } + + ssl_stream& operator=(ssl_stream&& other) + { + std::unique_ptr p( + new stream_type{other.get_io_service(), other.ctx_}); + using std::swap; + swap(p_, p); + swap(p_, other.p_); + ctx_ = other.ctx_; + return *this; } boost::asio::io_service& @@ -184,7 +215,7 @@ public: BOOST_ASIO_MOVE_ARG(BufferedHandshakeHandler) handler) { return p_->async_handshake(type, buffers, - BOOST_ASIO__MOVE_CAST(BufferedHandshakeHandler)(handler)); + BOOST_ASIO_MOVE_CAST(BufferedHandshakeHandler)(handler)); } void @@ -251,14 +282,53 @@ public: template BOOST_ASIO_INITFN_RESULT_TYPE(ReadHandler, void(boost::system::error_code, std::size_t)) - async_read_some(MutableBufferSequence& buffers, + async_read_some(MutableBufferSequence const& buffers, BOOST_ASIO_MOVE_ARG(ReadHandler) handler) { return p_->async_read_some(buffers, - BOOST_ASIO_MOVE_CAST(ReadHandler)(handler)) + BOOST_ASIO_MOVE_CAST(ReadHandler)(handler)); } + + template + friend + void + teardown(beast::websocket::teardown_tag, + ssl_stream& stream, + boost::system::error_code& ec); + + template + friend + void + async_teardown(beast::websocket::teardown_tag, + ssl_stream& stream, TeardownHandler&& handler); }; -} // beast +// These hooks are used to inform beast::websocket::stream on +// how to tear down the connection as part of the WebSocket +// protocol specifications -#endif \ No newline at end of file +template +inline +void +teardown(beast::websocket::teardown_tag, + ssl_stream& stream, + boost::system::error_code& ec) +{ + // Just forward it to the wrapped ssl::stream + using beast::websocket::teardown; + teardown(beast::websocket::teardown_tag{}, *stream.p_, ec); +} + +template +inline +void +async_teardown(beast::websocket::teardown_tag, + ssl_stream& stream, TeardownHandler&& handler) +{ + // Just forward it to the wrapped ssl::stream + using beast::websocket::async_teardown; + async_teardown(beast::websocket::teardown_tag{}, + *stream.p_, std::forward(handler)); +} + +#endif diff --git a/example/server-framework/ws_async_port.hpp b/example/server-framework/ws_async_port.hpp index ff607e4a..667de190 100644 --- a/example/server-framework/ws_async_port.hpp +++ b/example/server-framework/ws_async_port.hpp @@ -23,7 +23,7 @@ namespace framework { // // template -class async_ws_con +class async_ws_con_base { // This function lets us access members of the derived class Derived& @@ -58,7 +58,7 @@ protected: public: // Constructor template - async_ws_con( + async_ws_con_base( beast::string_view server_name, std::ostream& log, std::size_t id, @@ -72,9 +72,9 @@ public: // Limit of 1MB on messages , buffer_(1024 * 1024) - , strand_(impl().ws().get_io_service()) + , strand_(impl().stream().get_io_service()) { - cb(impl().ws()); + cb(impl().stream()); } // Run the connection @@ -82,18 +82,7 @@ public: void run() { - // Read the WebSocket upgrade request and attempt - // to send back the response. - // - impl().ws().async_accept_ex( - [&](beast::websocket::response_type& res) - { - res.set(beast::http::field::server, server_name_); - }, - strand_.wrap(std::bind( - &async_ws_con::on_accept, - impl().shared_from_this(), - std::placeholders::_1))); + impl().do_handshake(); } // Run the connection. @@ -109,17 +98,47 @@ public: // the request by parameter, instead of reading // it from the network. // - impl().ws().async_accept_ex(req, + impl().stream().async_accept_ex(req, [&](beast::websocket::response_type& res) { res.set(beast::http::field::server, server_name_); }, strand_.wrap(std::bind( - &async_ws_con::on_accept, + &async_ws_con_base::on_accept, impl().shared_from_this(), std::placeholders::_1))); } +protected: + // Performs the WebSocket handshake + void + do_accept() + { + // Read the WebSocket upgrade request and attempt + // to send back the response. + // + impl().stream().async_accept_ex( + [&](beast::websocket::response_type& res) + { + res.set(beast::http::field::server, server_name_); + }, + strand_.wrap(std::bind( + &async_ws_con_base::on_accept, + impl().shared_from_this(), + std::placeholders::_1))); + } + + // This helper reports failures + // + void + fail(std::string what, error_code ec) + { + if(ec != beast::websocket::error::closed) + log_ << + "[#" << id_ << " " << ep_ << "] " << + what << ": " << ec.message() << std::endl; + } + private: // Called when accept_ex completes // @@ -136,10 +155,10 @@ private: void do_read() { - impl().ws().async_read( + impl().stream().async_read( buffer_, strand_.wrap(std::bind( - &async_ws_con::on_read, + &async_ws_con_base::on_read, impl().shared_from_this(), std::placeholders::_1))); } @@ -155,14 +174,14 @@ private: // Set the outgoing message type. We will use // the same setting as the message we just read. // - impl().ws().binary(impl().ws().got_binary()); + impl().stream().binary(impl().stream().got_binary()); // Now echo back the message // - impl().ws().async_write( + impl().stream().async_write( buffer_.data(), strand_.wrap(std::bind( - &async_ws_con::on_write, + &async_ws_con_base::on_write, impl().shared_from_this(), std::placeholders::_1))); } @@ -184,17 +203,6 @@ private: // do_read(); } - - // This helper reports failures - // - void - fail(std::string what, error_code ec) - { - if(ec != beast::websocket::error::closed) - log_ << - "[#" << id_ << " " << ep_ << "] " << - what << ": " << ec.message() << std::endl; - } }; //------------------------------------------------------------------------------ @@ -202,12 +210,12 @@ private: // This class represents an asynchronous WebSocket connection // which uses a plain TCP/IP socket (no encryption) as the stream. // -class async_ws_con_plain +class async_ws_con // Note that we give this object the `enable_shared_from_this`, and have // the base class call `impl().shared_from_this()` when needed. // - : public std::enable_shared_from_this + : public std::enable_shared_from_this // We want the socket to be created before the base class so we use // the "base from member" idiom which Boost provides as a class. @@ -216,18 +224,18 @@ class async_ws_con_plain // Declare this base last now that everything else got set up first. // - , public async_ws_con + , public async_ws_con_base { public: // Construct the plain connection. // template explicit - async_ws_con_plain( + async_ws_con( socket_type&& sock, Args&&... args) : base_from_member>(std::move(sock)) - , async_ws_con(std::forward(args)...) + , async_ws_con_base(std::forward(args)...) { } @@ -236,10 +244,20 @@ public: // The base class calls this to obtain the websocket stream object. // beast::websocket::stream& - ws() + stream() { return this->member; } + +private: + // Base class needs to be a friend to call our private members + friend async_ws_con_base; + + void + do_handshake() + { + do_accept(); + } }; //------------------------------------------------------------------------------ @@ -304,7 +322,7 @@ public: socket_type&& sock, endpoint_type ep) { - std::make_shared( + std::make_shared( std::move(sock), "ws_async_port", log_, @@ -332,7 +350,7 @@ public: endpoint_type ep, beast::http::request&& req) { - std::make_shared( + std::make_shared( std::move(sock), "ws_async_port", log_, diff --git a/example/server-framework/ws_sync_port.hpp b/example/server-framework/ws_sync_port.hpp index 4d128a80..b4102972 100644 --- a/example/server-framework/ws_sync_port.hpp +++ b/example/server-framework/ws_sync_port.hpp @@ -30,7 +30,7 @@ namespace framework { for plain and SSL stream objects. */ template -class sync_ws_con +class sync_ws_con_base { // This function lets us access members of the derived class Derived& @@ -56,7 +56,7 @@ class sync_ws_con public: // Constructor template - sync_ws_con( + sync_ws_con_base( beast::string_view server_name, std::ostream& log, std::size_t id, @@ -67,22 +67,23 @@ public: , id_(id) , ep_(ep) { - cb(impl().ws()); + cb(impl().stream()); } - // Run the connection. + // Run the connection. This is called for the case + // where we have not received the upgrade request yet. // void run() { - // We run the do_accept function in its own thread, + // We run the do_run function in its own thread, // and bind a shared pointer to the connection object // into the function. The last reference to the shared // pointer will go away when the thread exits, thus // destroying the connection object. // std::thread{ - &sync_ws_con::do_accept, + &sync_ws_con_base::do_accept, impl().shared_from_this() }.detach(); } @@ -106,7 +107,58 @@ public: }}.detach(); } +protected: + // Called when a failure occurs + // + void + fail(std::string what, error_code ec) + { + // Don't report the "closed" error since that + // happens under normal circumstances. + // + if(ec && ec != beast::websocket::error::closed) + { + log_ << + "[#" << id_ << " " << ep_ << "] " << + what << ": " << ec.message() << std::endl; + log_.flush(); + } + } + private: + // This function performs the WebSocket handshake + // and runs the main loop upon success. + void + do_accept() + { + error_code ec; + + // Give the derived class a chance to do stuff before we + // enter the main loop. This is for SSL connections really. + // + impl().do_handshake(ec); + + if(ec) + return fail("handshake", ec); + + // Read the WebSocket upgrade request and attempt + // to send back the response. + // + impl().stream().accept_ex( + [&](beast::websocket::response_type& res) + { + res.insert(beast::http::field::server, server_name_); + }, + ec); + + if(ec) + return fail("accept", ec); + + // Run the connection + // + do_run(); + } + // This is the lambda used when launching a connection from // an already-received request. In C++14 we could simply use // a lambda capture but this example requires only C++11 so @@ -116,7 +168,7 @@ private: template class lambda { - std::shared_ptr self_; + std::shared_ptr self_; beast::http::request req_; public: @@ -125,7 +177,7 @@ private: // This is the equivalent of the capture section of the lambda. // lambda( - std::shared_ptr self, + std::shared_ptr self, beast::http::request&& req) : self_(std::move(self)) , req_(std::move(req)) @@ -151,7 +203,7 @@ private: // the request by parameter, instead of reading // it from the network. // - self_->impl().ws().accept_ex(req, + self_->impl().stream().accept_ex(req, [&](beast::websocket::response_type& res) { res.insert(beast::http::field::server, self_->server_name_); @@ -159,55 +211,18 @@ private: ec); } - // Run the connection - // - self_->do_run(ec); + if(ec) + return self_->fail("accept", ec); + + self_->do_run(); } }; void - do_accept() + do_run() { error_code ec; - // Read the WebSocket upgrade request and attempt - // to send back the response. - // - impl().ws().accept_ex( - [&](beast::websocket::response_type& res) - { - res.insert(beast::http::field::server, server_name_); - }, - ec); - - // Run the connection - // - do_run(ec); - } - - void - do_run(error_code ec) - { - // Helper lambda to report a failure - // - auto const fail = - [&](std::string const& what, error_code ev) - { - if(ev != beast::websocket::error::closed) - log_ << - "[#" << id_ << " " << ep_ << "] " << - what << ": " << ev.message() << std::endl; - }; - - // Check for an error upon entry. This will - // come from one of the two calls to accept() - // - if(ec) - { - fail("accept", ec); - return; - } - // Loop, reading messages and echoing them back. // for(;;) @@ -219,7 +234,7 @@ private: // Read the message // - impl().ws().read(buffer, ec); + impl().stream().read(buffer, ec); if(ec) return fail("read", ec); @@ -227,11 +242,11 @@ private: // Set the outgoing message type. We will use // the same setting as the message we just read. // - impl().ws().binary(impl().ws().got_binary()); + impl().stream().binary(impl().stream().got_binary()); // Now echo back the message // - impl().ws().write(buffer.data(), ec); + impl().stream().write(buffer.data(), ec); if(ec) return fail("write", ec); @@ -244,12 +259,12 @@ private: // This class represents a synchronous WebSocket connection // which uses a plain TCP/IP socket (no encryption) as the stream. // -class sync_ws_con_plain +class sync_ws_con // Note that we give this object the `enable_shared_from_this`, and have // the base class call `impl().shared_from_this()` when needed. // - : public std::enable_shared_from_this + : public std::enable_shared_from_this // We want the socket to be created before the base class so we use // the "base from member" idiom which Boost provides as a class. @@ -258,18 +273,18 @@ class sync_ws_con_plain // Declare this base last now that everything else got set up first. // - , public sync_ws_con + , public sync_ws_con_base { public: // Construct the plain connection. // template explicit - sync_ws_con_plain( + sync_ws_con( socket_type&& sock, Args&&... args) : base_from_member>(std::move(sock)) - , sync_ws_con(std::forward(args)...) + , sync_ws_con_base(std::forward(args)...) { } @@ -278,10 +293,25 @@ public: // The base class calls this to obtain the websocket stream object. // beast::websocket::stream& - ws() + stream() { return this->member; } + +private: + // Base class needs to be a friend to call our private members + friend class sync_ws_con_base; + + // This is called by the base before running the main loop. + // There's nothing to do for a plain connection. + // + void + do_handshake(error_code& ec) + { + // This is required by the specifications for error_code + // + ec = {}; + } }; //------------------------------------------------------------------------------ @@ -346,7 +376,7 @@ public: { // Create our connection object and run it // - std::make_shared( + std::make_shared( std::move(sock), "ws_sync_port", log_, @@ -377,7 +407,7 @@ public: // Create the connection object and run it, // transferring ownershop of the ugprade request. // - std::make_shared( + std::make_shared( std::move(sock), "ws_sync_port", log_, diff --git a/example/server-framework/wss_ports.hpp b/example/server-framework/wss_ports.hpp new file mode 100644 index 00000000..77e8ce2f --- /dev/null +++ b/example/server-framework/wss_ports.hpp @@ -0,0 +1,414 @@ +// +// Copyright (c) 2013-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) +// + +#ifndef BEAST_EXAMPLE_SERVER_WSS_PORTS_HPP +#define BEAST_EXAMPLE_SERVER_WSS_PORTS_HPP + +#include "ws_sync_port.hpp" +#include "ws_async_port.hpp" +#include "ssl_stream.hpp" + +#include +#include + +namespace framework { + +//------------------------------------------------------------------------------ + +// This class represents a synchronous Secure WebSocket connection +// +class sync_wss_con + + // Note that we give this object the `enable_shared_from_this`, and have + // the base class call `impl().shared_from_this()` when needed. + // + : public std::enable_shared_from_this + + // We want the socket to be created before the base class so we use + // the "base from member" idiom which Boost provides as a class. + // + , public base_from_member>> + + // Declare this base last now that everything else got set up first. + // + , public sync_ws_con_base +{ +public: + // Construct the plain connection. + // + template + explicit + sync_wss_con( + socket_type&& sock, + boost::asio::ssl::context& ctx, + Args&&... args) + : base_from_member>>(std::move(sock), ctx) + , sync_ws_con_base(std::forward(args)...) + { + } + + // Construct the connection from an existing, handshaked SSL stream + // + template + sync_wss_con( + ssl_stream&& stream, + Args&&... args) + : base_from_member>>(std::move(stream)) + , sync_ws_con_base(std::forward(args)...) + { + } + + // Returns the stream. + // + // The base class calls this to obtain the websocket stream object. + // + beast::websocket::stream>& + stream() + { + return this->member; + } + +private: + friend class sync_ws_con_base; + + // This is called by the base before running the main loop. + // + void + do_handshake(error_code& ec) + { + // Perform the SSL handshake + // + // We use next_layer() to get at the underlying ssl_stream + // + stream().next_layer().handshake(boost::asio::ssl::stream_base::server, ec); + } +}; + +//------------------------------------------------------------------------------ + +// This class represents an asynchronous Secure WebSocket +// connection which uses an OpenSSL socket as the stream. +// +class async_wss_con + + // Note that we give this object the enable_shared_from_this, and have + // the base class call impl().shared_from_this(). The reason we do that + // is so that the shared_ptr has the correct type. This lets the + // derived class (this class) use its members in calls to std::bind, + // without an ugly call to `dynamic_downcast` or other nonsense. + // + : public std::enable_shared_from_this + + // We want the socket to be created before the base class so we use + // the "base from member" idiom which Boost provides as a class. + // + , public base_from_member>> + + // Declare this base last now that everything else got set up first. + // + , public async_ws_con_base +{ +public: + // Construct the connection. + // + template + async_wss_con( + socket_type&& sock, + boost::asio::ssl::context& ctx, + Args&&... args) + : base_from_member>>(std::move(sock), ctx) + , async_ws_con_base(std::forward(args)...) + { + } + + // Construct the connection from an existing, handshaked SSL stream + // + template + async_wss_con( + ssl_stream&& stream, + Args&&... args) + : base_from_member>>(std::move(stream)) + , async_ws_con_base(std::forward(args)...) + { + } + + // Returns the stream. + // The base class calls this to obtain the object to + // use for reading and writing HTTP messages. + // + beast::websocket::stream>& + stream() + { + return this->member; + } + +private: + friend class async_ws_con_base; + + // Called by the port to start the connection + // after creating the object + // + void + do_handshake() + { + // This is SSL so perform the handshake first + // + stream().next_layer().async_handshake( + boost::asio::ssl::stream_base::server, + this->strand_.wrap(std::bind( + &async_wss_con::on_handshake, + this->shared_from_this(), + std::placeholders::_1))); + } + + // Called when the SSL handshake completes + void + on_handshake(error_code ec) + { + if(ec) + return this->fail("on_handshake", ec); + + // Move on to accepting the WebSocket handshake + // + this->do_accept(); + } +}; + +//------------------------------------------------------------------------------ + +/** A synchronous Secure WebSocket @b PortHandler which implements echo. + + This is a port handler which accepts Secure WebSocket upgrade + HTTP requests and implements the echo protocol. All received + WebSocket messages will be echoed back to the remote host. +*/ +class wss_sync_port +{ + // VFALCO We use boost::function to work around a compiler + // crash with gcc and clang using libstdc++ + + // The types of the on_stream callback + using on_new_stream_cb1 = boost::function< + void(beast::websocket::stream&)>; + using on_new_stream_cb2 = boost::function< + void(beast::websocket::stream>&)>; + + server& instance_; + std::ostream& log_; + boost::asio::ssl::context& ctx_; + on_new_stream_cb1 cb1_; + on_new_stream_cb2 cb2_; + +public: + /** Constructor + + @param instance The server instance which owns this port + + @param log The stream to use for logging + + @param ctx The SSL context holding the SSL certificates to use + + @param cb A callback which will be invoked for every new + WebSocket connection. This provides an opportunity to change + the settings on the stream before it is used. The callback + should have this equivalent signature: + @code + template + void callback(beast::websocket::stream&); + @endcode + In C++14 this can be accomplished with a generic lambda. In + C++11 it will be necessary to write out a lambda manually, + with a templated operator(). + */ + template + wss_sync_port( + server& instance, + std::ostream& log, + boost::asio::ssl::context& ctx, + Callback const& cb) + : instance_(instance) + , log_(log) + , ctx_(ctx) + , cb1_(cb) + , cb2_(cb) + { + } + + /** Accept a TCP/IP connection. + + This function is called when the server has accepted an + incoming connection. + + @param sock The connected socket. + + @param ep The endpoint of the remote host. + */ + void + on_accept(socket_type&& sock, endpoint_type ep) + { + // Create our connection object and run it + // + std::make_shared( + std::move(sock), + ctx_, + "wss_sync_port", + log_, + instance_.next_id(), + ep, + cb2_ + )->run(); + } + + /** Accept a WebSocket upgrade request. + + This is used to accept a connection that has already + delivered the handshake. + + @param stream The stream corresponding to the connection. + + @param ep The remote endpoint. + + @param req The upgrade request. + */ + template + void + accept( + ssl_stream&& stream, + endpoint_type ep, + beast::http::request&& req) + { + // Create the connection object and run it, + // transferring ownershop of the ugprade request. + // + std::make_shared( + std::move(stream), + "wss_sync_port", + log_, + instance_.next_id(), + ep, + cb2_ + )->run(std::move(req)); + } +}; + +//------------------------------------------------------------------------------ + +/** An asynchronous WebSocket @b PortHandler which implements echo. + + This is a port handler which accepts WebSocket upgrade HTTP + requests and implements the echo protocol. All received + WebSocket messages will be echoed back to the remote host. +*/ +class wss_async_port +{ + // VFALCO We use boost::function to work around a compiler + // crash with gcc and clang using libstdc++ + + // The types of the on_stream callback + using on_new_stream_cb1 = boost::function< + void(beast::websocket::stream&)>; + using on_new_stream_cb2 = boost::function< + void(beast::websocket::stream>&)>; + + server& instance_; + std::ostream& log_; + boost::asio::ssl::context& ctx_; + on_new_stream_cb1 cb1_; + on_new_stream_cb2 cb2_; + +public: + /** Constructor + + @param instance The server instance which owns this port + + @param log The stream to use for logging + + @param ctx The SSL context holding the SSL certificates to use + + @param cb A callback which will be invoked for every new + WebSocket connection. This provides an opportunity to change + the settings on the stream before it is used. The callback + should have this equivalent signature: + @code + template + void callback(beast::websocket::stream&); + @endcode + In C++14 this can be accomplished with a generic lambda. In + C++11 it will be necessary to write out a lambda manually, + with a templated operator(). + */ + template + wss_async_port( + server& instance, + std::ostream& log, + boost::asio::ssl::context& ctx, + Callback const& cb) + : instance_(instance) + , log_(log) + , ctx_(ctx) + , cb1_(cb) + , cb2_(cb) + { + } + + /** Accept a TCP/IP connection. + + This function is called when the server has accepted an + incoming connection. + + @param sock The connected socket. + + @param ep The endpoint of the remote host. + */ + void + on_accept( + socket_type&& sock, + endpoint_type ep) + { + std::make_shared( + std::move(sock), + ctx_, + "wss_async_port", + log_, + instance_.next_id(), + ep, + cb2_ + )->run(); + } + + /** Accept a WebSocket upgrade request. + + This is used to accept a connection that has already + delivered the handshake. + + @param stream The stream corresponding to the connection. + + @param ep The remote endpoint. + + @param req The upgrade request. + */ + template + void + accept( + ssl_stream&& stream, + endpoint_type ep, + beast::http::request&& req) + { + std::make_shared( + std::move(stream), + "wss_async_port", + log_, + instance_.next_id(), + ep, + cb2_ + )->run(std::move(req)); + } +}; + +} // framework + +#endif diff --git a/test/server/CMakeLists.txt b/test/server/CMakeLists.txt index f0fa20a9..e1a49780 100644 --- a/test/server/CMakeLists.txt +++ b/test/server/CMakeLists.txt @@ -5,6 +5,16 @@ GroupSources(include/beast beast) GroupSources(test/server "/") +if (OPENSSL_FOUND) + include_directories(${OPENSSL_INCLUDE_DIR}) + set(SSL_SOURCES + https_ports.cpp + ssl_stream.cpp + ) +else () + set(SSL_SOURCES "") +endif() + add_executable (server-test ${BEAST_INCLUDES} ${SERVER_INCLUDES} @@ -14,15 +24,22 @@ add_executable (server-test http_async_port.cpp http_base.cpp http_sync_port.cpp + https_ports.cpp main.cpp rfc7231.cpp server.cpp service_list.cpp + ssl_certificate write_msg.cpp ws_async_port.cpp ws_sync_port.cpp ws_upgrade_service.cpp + wss_ports.cpp + ${SSL_SOURCES} ) target_link_libraries(server-test Beast) +if (OPENSSL_FOUND) + target_link_libraries(server-test ${OPENSSL_LIBRARIES}) +endif() diff --git a/test/server/Jamfile b/test/server/Jamfile index 17fcc5cd..92b50ecc 100644 --- a/test/server/Jamfile +++ b/test/server/Jamfile @@ -20,4 +20,7 @@ exe server-test : ws_async_port.cpp ws_sync_port.cpp ws_upgrade_service.cpp + #https_ports.cpp + #ssl_certificate.cpp + #ssl_stream.cpp ; diff --git a/test/server/https_ports.cpp b/test/server/https_ports.cpp new file mode 100644 index 00000000..f8414cfc --- /dev/null +++ b/test/server/https_ports.cpp @@ -0,0 +1,13 @@ +// +// Copyright (c) 2013-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) +// + +#if BEAST_USE_OPENSSL + +// Test that header file is self-contained. +#include "../../example/server-framework/https_ports.hpp" + +#endif diff --git a/test/server/ssl_certificate.cpp b/test/server/ssl_certificate.cpp new file mode 100644 index 00000000..19b0baee --- /dev/null +++ b/test/server/ssl_certificate.cpp @@ -0,0 +1,13 @@ +// +// Copyright (c) 2013-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) +// + +#if BEAST_USE_OPENSSL + +// Test that header file is self-contained. +#include "../../example/server-framework/ssl_certificate.hpp" + +#endif diff --git a/test/server/ssl_stream.cpp b/test/server/ssl_stream.cpp new file mode 100644 index 00000000..871c32bd --- /dev/null +++ b/test/server/ssl_stream.cpp @@ -0,0 +1,13 @@ +// +// Copyright (c) 2013-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) +// + +#if BEAST_USE_OPENSSL + +// Test that header file is self-contained. +#include "../../example/server-framework/ssl_stream.hpp" + +#endif diff --git a/test/server/wss_ports.cpp b/test/server/wss_ports.cpp new file mode 100644 index 00000000..6307f532 --- /dev/null +++ b/test/server/wss_ports.cpp @@ -0,0 +1,13 @@ +// +// Copyright (c) 2013-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) +// + +#if BEAST_USE_OPENSSL + +// Test that header file is self-contained. +#include "../../example/server-framework/wss_ports.hpp" + +#endif