diff --git a/CHANGELOG.md b/CHANGELOG.md index c3b0fd3b..e13b366f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ 1.0.0-b39 +WebSocket: + +* Add websocket async echo ssl server test: + API Changes: * Refactor http::header contents diff --git a/CMakeLists.txt b/CMakeLists.txt index b0156411..6121b551 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -176,6 +176,7 @@ if (NOT OPENSSL_FOUND) message("OpenSSL not found. Not building SSL tests and examples") else() add_subdirectory (examples/ssl) + add_subdirectory (test/websocket/ssl) endif() add_subdirectory (test) diff --git a/test/websocket/ssl/CMakeLists.txt b/test/websocket/ssl/CMakeLists.txt new file mode 100644 index 00000000..9c972179 --- /dev/null +++ b/test/websocket/ssl/CMakeLists.txt @@ -0,0 +1,22 @@ +# Part of Beast + +GroupSources(extras/beast extras) +GroupSources(include/beast beast) + +GroupSources(test/websocket/ssl "/") + +include_directories(${OPENSSL_INCLUDE_DIR}) + +add_executable (websocket-ssl-tests + ${BEAST_INCLUDES} + ${EXTRAS_INCLUDES} + ../../../extras/beast/unit_test/main.cpp + websocket_async_ssl_echo_server.hpp + ssl_server.cpp +) + +target_link_libraries(websocket-ssl-tests ${OPENSSL_LIBRARIES}) + +if (NOT WIN32) + target_link_libraries(websocket-ssl-tests ${Boost_LIBRARIES} Threads::Threads) +endif() diff --git a/test/websocket/ssl/ssl_server.cpp b/test/websocket/ssl/ssl_server.cpp new file mode 100644 index 00000000..0303e4a4 --- /dev/null +++ b/test/websocket/ssl/ssl_server.cpp @@ -0,0 +1,158 @@ +// +// 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) +// + +#include "websocket_async_ssl_echo_server.hpp" + +#include +#include +#include +#include +#include +#include + +namespace beast { +namespace websocket { + +class ssl_server_test + : public beast::unit_test::suite + , public test::enable_yield_to +{ +public: + using self = ssl_server_test; + using endpoint_type = boost::asio::ip::tcp::endpoint; + using address_type = boost::asio::ip::address; + using socket_type = boost::asio::ip::tcp::socket; + + void + run() override + { + /* + 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"; + + using endpoint_type = boost::asio::ip::tcp::endpoint; + using address_type = boost::asio::ip::address; + ::websocket::async_ssl_echo_server server{ + &log, 1, cert, key, dh}; + error_code ec; + server.open(endpoint_type{ + address_type::from_string("127.0.0.1"), 6000 }, ec); + auto const ep = server.local_endpoint(); + + using boost::asio::connect; + using socket = boost::asio::ip::tcp::socket; + using io_service = boost::asio::io_service; + namespace ssl = boost::asio::ssl; + + // Perform SSL handshaking + io_service ios; + using stream_type = ssl::stream; + ssl::context ctx{ssl::context::sslv23}; + stream_type stream{ios_, ctx}; + stream.next_layer().connect(ep); + stream.set_verify_mode(ssl::verify_none); + stream.handshake(ssl::stream_base::client); + + // Secure WebSocket connect and send message using Beast + beast::websocket::stream ws{stream}; + ws.handshake("localhost", "/"); + ws.write(boost::asio::buffer("Hello, world!")); + + // Receive Secure WebSocket message, print and close using Beast + beast::streambuf sb; + beast::websocket::opcode op; + ws.read(op, sb); + ws.close(beast::websocket::close_code::normal); + try + { + for(;;) + { + ws.read(op, sb); + sb.consume(sb.size()); + } + } + catch(system_error const& se) + { + if(se.code() != beast::websocket::error::closed) + throw; + } + log << to_string(sb.data()) << std::endl; + + pass(); + } +}; + +BEAST_DEFINE_TESTSUITE(ssl_server,websocket,beast); + +} // websocket +} // beast diff --git a/test/websocket/ssl/websocket_async_ssl_echo_server.hpp b/test/websocket/ssl/websocket_async_ssl_echo_server.hpp new file mode 100644 index 00000000..7e605f87 --- /dev/null +++ b/test/websocket/ssl/websocket_async_ssl_echo_server.hpp @@ -0,0 +1,308 @@ +// +// 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 WEBSOCKET_ASYNC_SSL_ECHO_SERVER_HPP +#define WEBSOCKET_ASYNC_SSL_ECHO_SERVER_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace websocket { + +/** Asynchronous WebSocket echo client/server +*/ +class async_ssl_echo_server +{ +public: + using error_code = beast::error_code; + using address_type = boost::asio::ip::address; + using socket_type = boost::asio::ip::tcp::socket; + using endpoint_type = boost::asio::ip::tcp::endpoint; + +private: + std::ostream* log_; + boost::asio::io_service ios_; + socket_type sock_; + endpoint_type ep_; + boost::asio::ip::tcp::acceptor acceptor_; + std::vector thread_; + boost::optional work_; + boost::asio::ssl::context ctx_; + +public: + async_ssl_echo_server(async_ssl_echo_server const&) = delete; + async_ssl_echo_server& operator=(async_ssl_echo_server const&) = delete; + + /** Constructor. + + @param log A pointer to a stream to log to, or `nullptr` + to disable logging. + + @param threads The number of threads in the io_service. + */ + async_ssl_echo_server(std::ostream* log, + std::size_t threads, std::string const& cert, + std::string const& key, std::string const& tmp_dh) + : log_(log) + , sock_(ios_) + , acceptor_(ios_) + , work_(ios_) + , ctx_(boost::asio::ssl::context::sslv23) + + { + 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(tmp_dh.data(), tmp_dh.size())); + + thread_.reserve(threads); + for(std::size_t i = 0; i < threads; ++i) + thread_.emplace_back( + [&]{ ios_.run(); }); + } + + /** Destructor. + */ + ~async_ssl_echo_server() + { + work_ = boost::none; + error_code ec; + ios_.dispatch( + [&]{ acceptor_.close(ec); }); + for(auto& t : thread_) + t.join(); + } + + /** Return the listening endpoint. + */ + endpoint_type + local_endpoint() const + { + return acceptor_.local_endpoint(); + } + + /** Open a listening port. + + @param ep The address and port to bind to. + + @param ec Set to the error, if any occurred. + */ + void + open(endpoint_type const& ep, error_code& ec) + { + acceptor_.open(ep.protocol(), ec); + if(ec) + return fail("open", ec); + acceptor_.set_option( + boost::asio::socket_base::reuse_address{true}); + acceptor_.bind(ep, ec); + if(ec) + return fail("bind", ec); + acceptor_.listen( + boost::asio::socket_base::max_connections, ec); + if(ec) + return fail("listen", ec); + acceptor_.async_accept(sock_, ep_, + std::bind(&async_ssl_echo_server::on_accept, this, + beast::asio::placeholders::error)); + } + +private: + class connection + { + struct data + { + async_ssl_echo_server& server; + endpoint_type ep; + int state = 0; + beast::websocket::stream< + boost::asio::ssl::stream> ws; + boost::asio::io_service::strand strand; + beast::websocket::opcode op; + beast::streambuf db; + std::size_t id; + + data(async_ssl_echo_server& server_, + endpoint_type const& ep_, + socket_type&& sock_) + : server(server_) + , ep(ep_) + , ws(sock_.get_io_service(), server_.ctx_) + , strand(ws.get_io_service()) + , id([] + { + static std::atomic n{0}; + return ++n; + }()) + { + // VFALCO This hack works around + // ssl::stream broken ctors + ws.next_layer().next_layer() = std::move(sock_); + } + }; + + // VFALCO This could be unique_ptr in [Net.TS] + std::shared_ptr d_; + + public: + connection(connection&&) = default; + connection(connection const&) = default; + connection& operator=(connection&&) = delete; + connection& operator=(connection const&) = delete; + + template + explicit + connection(async_ssl_echo_server& server, + endpoint_type const& ep, socket_type&& sock, + Args&&... args) + : d_(std::make_shared(server, ep, + std::forward(sock), + std::forward(args)...)) + { + } + + void + run() + { + (*this)(error_code{}); + } + + void + operator()(error_code ec) + { + auto& d = *d_; + switch(d.state) + { + // SSL handshake + case 0: + d.state = 1; + d.ws.next_layer().async_handshake( + boost::asio::ssl::stream_base::server, + d.strand.wrap(std::move(*this))); + return; + + // WebSocket handshake + case 1: + if(ec) + return fail("async_handshake", ec); + d.state = 2; + d.ws.async_accept_ex( + [](beast::websocket::response_type& res) + { + res.fields.insert( + "Server", "async_ssl_echo_server"); + }, + d.strand.wrap(std::move(*this))); + return; + + case 2: + if(ec) + return fail("async_handshake", ec); + // [[fallthrough]] + + // WebSocket read + case 3: + if(ec) + return fail("async_write", ec); + d.db.consume(d.db.size()); + d.state = 4; + d.ws.async_read(d.op, d.db, + d.strand.wrap(std::move(*this))); + return; + + // WebSocket write + case 4: + if(ec == beast::websocket::error::closed) + return; + if(ec) + return fail("async_read", ec); + d.state = 3; + d.ws.set_option( + beast::websocket::message_type(d.op)); + d.ws.async_write(d.db.data(), + d.strand.wrap(std::move(*this))); + return; + } + } + + private: + void + fail(std::string what, error_code ec) + { + auto& d = *d_; + if(d.server.log_) + if(ec != beast::websocket::error::closed) + d.server.fail("[#" + std::to_string(d.id) + + " " + boost::lexical_cast(d.ep) + + "] " + what, ec); + } + }; + + void + fail(std::string what, error_code ec) + { + if(log_) + { + static std::mutex m; + std::lock_guard lock{m}; + (*log_) << what << ": " << + ec.message() << std::endl; + } + } + + void + on_accept(error_code ec) + { + if(! acceptor_.is_open()) + return; + if(ec == boost::asio::error::operation_aborted) + return; + if(ec) + fail("accept", ec); + connection{*this, ep_, std::move(sock_)}.run(); + acceptor_.async_accept(sock_, ep_, + std::bind(&async_ssl_echo_server::on_accept, this, + beast::asio::placeholders::error)); + } +}; + +} // websocket + +#endif