/* * Copyright (c) 2013, Peter Thorson. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the WebSocket++ Project nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL PETER THORSON BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * */ #ifndef WEBSOCKETPP_TRANSPORT_ASIO_HPP #define WEBSOCKETPP_TRANSPORT_ASIO_HPP #include #include #include #include #include #include #include #include #include namespace websocketpp { namespace transport { namespace asio { /// Boost Asio based endpoint transport component /** * transport::asio::endpoint implements an endpoint transport component using * Boost ASIO. */ template class endpoint : public config::socket_type { public: /// Type of this endpoint transport component typedef endpoint type; /// Type of the concurrency policy typedef typename config::concurrency_type concurrency_type; /// Type of the socket policy typedef typename config::socket_type socket_type; /// Type of the error logging policy typedef typename config::elog_type elog_type; /// Type of the access logging policy typedef typename config::alog_type alog_type; /// Type of the socket connection component typedef typename socket_type::socket_con_type socket_con_type; /// Type of a shared pointer to the socket connection component typedef typename socket_con_type::ptr socket_con_ptr; /// Type of the connection transport component associated with this /// endpoint transport component typedef asio::connection transport_con_type; /// Type of a shared pointer to the connection transport component /// associated with this endpoint transport component typedef typename transport_con_type::ptr transport_con_ptr; /// Type of a pointer to the ASIO io_service being used typedef boost::asio::io_service* io_service_ptr; /// Type of a shared pointer to the acceptor being used typedef lib::shared_ptr acceptor_ptr; /// Type of a shared pointer to the resolver being used typedef lib::shared_ptr resolver_ptr; /// Type of timer handle typedef lib::shared_ptr timer_ptr; // generate and manage our own io_service explicit endpoint() : m_external_io_service(false) , m_state(UNINITIALIZED) { //std::cout << "transport::asio::endpoint constructor" << std::endl; } ~endpoint() { // clean up our io_service if we were initialized with an internal one. m_acceptor.reset(); if (m_state != UNINITIALIZED && !m_external_io_service) { delete m_io_service; } } /// transport::asio objects are moveable but not copyable or assignable. /// The following code sets this situation up based on whether or not we /// have C++11 support or not #ifdef _WEBSOCKETPP_DELETED_FUNCTIONS_ endpoint(const endpoint& src) = delete; endpoint& operator= (const endpoint & rhs) = delete; #else private: endpoint(const endpoint& src); endpoint& operator= (const endpoint & rhs); public: #endif #ifdef _WEBSOCKETPP_RVALUE_REFERENCES_ endpoint (endpoint&& src) : m_io_service(src.m_io_service) , m_external_io_service(src.m_external_io_service) , m_acceptor(src.m_acceptor) , m_state(src.m_state) { src.m_io_service = NULL; src.m_external_io_service = false; src.m_acceptor = NULL; src.m_state = UNINITIALIZED; } endpoint& operator= (const endpoint && rhs) { if (this != &rhs) { m_io_service = rhs.m_io_service; m_external_io_service = rhs.m_external_io_service; m_acceptor = rhs.m_acceptor; m_state = rhs.m_state; rhs.m_io_service = NULL; rhs.m_external_io_service = false; rhs.m_acceptor = NULL; rhs.m_state = UNINITIALIZED; } return *this; } #endif /// Return whether or not the endpoint produces secure connections. bool is_secure() const { return socket_type::is_secure(); } /// initialize asio transport with external io_service (exception free) /** * Initialize the ASIO transport policy for this endpoint using the provided * io_service object. asio_init must be called exactly once on any endpoint * that uses transport::asio before it can be used. * * @param ptr A pointer to the io_service to use for asio events * @param ec Set to indicate what error occurred, if any. */ void init_asio(io_service_ptr ptr, lib::error_code & ec) { if (m_state != UNINITIALIZED) { m_elog->write(log::elevel::library, "asio::init_asio called from the wrong state"); using websocketpp::error::make_error_code; ec = make_error_code(websocketpp::error::invalid_state); return; } m_alog->write(log::alevel::devel,"asio::init_asio"); m_io_service = ptr; m_external_io_service = true; m_acceptor.reset(new boost::asio::ip::tcp::acceptor(*m_io_service)); m_state = READY; ec = lib::error_code(); } /// initialize asio transport with external io_service /** * Initialize the ASIO transport policy for this endpoint using the provided * io_service object. asio_init must be called exactly once on any endpoint * that uses transport::asio before it can be used. * * @param ptr A pointer to the io_service to use for asio events */ void init_asio(io_service_ptr ptr) { lib::error_code ec; init_asio(ptr,ec); if (ec) { throw ec; } } /// Initialize asio transport with internal io_service (exception free) /** * This method of initialization will allocate and use an internally managed * io_service. * * @see init_asio(io_service_ptr ptr) * * @param ec Set to indicate what error occurred, if any. */ void init_asio(lib::error_code & ec) { init_asio(new boost::asio::io_service(),ec); m_external_io_service = false; } /// Initialize asio transport with internal io_service /** * This method of initialization will allocate and use an internally managed * io_service. * * @see init_asio(io_service_ptr ptr) */ void init_asio() { init_asio(new boost::asio::io_service()); m_external_io_service = false; } /// Retrieve a reference to the endpoint's io_service /** * The io_service may be an internal or external one. This may be used to * call methods of the io_service that are not explicitly wrapped by the * endpoint. * * This method is only valid after the endpoint has been initialized with * `init_asio`. No error will be returned if it isn't. * * @return A reference to the endpoint's io_service */ boost::asio::io_service & get_io_service() { return *m_io_service; } /// Sets the tcp init handler /** * The tcp init handler is called after the tcp connection has been * established. * * @see WebSocket++ handler documentation for more information about * handlers. * * @param h The handler to call on tcp init. */ void set_tcp_init_handler(tcp_init_handler h) { m_tcp_init_handler = h; } /// Set up endpoint for listening manually (exception free) /** * Bind the internal acceptor using the specified settings. The endpoint * must have been initialized by calling init_asio before listening. * * @param ep An endpoint to read settings from * @param ec Set to indicate what error occurred, if any. */ void listen(boost::asio::ip::tcp::endpoint const & ep, lib::error_code & ec) { if (m_state != READY) { m_elog->write(log::elevel::library, "asio::listen called from the wrong state"); using websocketpp::error::make_error_code; ec = make_error_code(websocketpp::error::invalid_state); return; } m_alog->write(log::alevel::devel,"asio::listen"); m_acceptor->open(ep.protocol()); m_acceptor->set_option(boost::asio::socket_base::reuse_address(true)); m_acceptor->bind(ep); m_acceptor->listen(); m_state = LISTENING; ec = lib::error_code(); } /// Set up endpoint for listening manually /** * Bind the internal acceptor using the settings specified by the endpoint e * * @param ep An endpoint to read settings from */ void listen(boost::asio::ip::tcp::endpoint const & ep) { lib::error_code ec; listen(ep,ec); if (ec) { throw ec; } } /// Set up endpoint for listening with protocol and port (exception free) /** * Bind the internal acceptor using the given internet protocol and port. * The endpoint must have been initialized by calling init_asio before * listening. * * Common options include: * - IPv6 with mapped IPv4 for dual stack hosts boost::asio::ip::tcp::v6() * - IPv4 only: boost::asio::ip::tcp::v4() * * @param internet_protocol The internet protocol to use. * @param port The port to listen on. * @param ec Set to indicate what error occurred, if any. */ template void listen(InternetProtocol const & internet_protocol, uint16_t port, lib::error_code & ec) { boost::asio::ip::tcp::endpoint ep(internet_protocol, port); listen(ep,ec); } /// Set up endpoint for listening with protocol and port /** * Bind the internal acceptor using the given internet protocol and port. * The endpoint must have been initialized by calling init_asio before * listening. * * Common options include: * - IPv6 with mapped IPv4 for dual stack hosts boost::asio::ip::tcp::v6() * - IPv4 only: boost::asio::ip::tcp::v4() * * @param internet_protocol The internet protocol to use. * @param port The port to listen on. */ template void listen(InternetProtocol const & internet_protocol, uint16_t port) { boost::asio::ip::tcp::endpoint ep(internet_protocol, port); listen(ep); } /// Set up endpoint for listening on a port (exception free) /** * Bind the internal acceptor using the given port. The IPv6 protocol with * mapped IPv4 for dual stack hosts will be used. If you need IPv4 only use * the overload that allows specifying the protocol explicitly. * * The endpoint must have been initialized by calling init_asio before * listening. * * @param port The port to listen on. * @param ec Set to indicate what error occurred, if any. */ void listen(uint16_t port, lib::error_code & ec) { listen(boost::asio::ip::tcp::v6(), port, ec); } /// Set up endpoint for listening on a port /** * Bind the internal acceptor using the given port. The IPv6 protocol with * mapped IPv4 for dual stack hosts will be used. If you need IPv4 only use * the overload that allows specifying the protocol explicitly. * * The endpoint must have been initialized by calling init_asio before * listening. * * @param port The port to listen on. * @param ec Set to indicate what error occurred, if any. */ void listen(uint16_t port) { listen(boost::asio::ip::tcp::v6(), port); } /// Set up endpoint for listening on a host and service (exception free) /** * Bind the internal acceptor using the given host and service. More details * about what host and service can be are available in the boost asio * documentation for ip::basic_resolver_query::basic_resolver_query's * constructors. * * The endpoint must have been initialized by calling init_asio before * listening. * * @param host A string identifying a location. May be a descriptive name or * a numeric address string. * @param service A string identifying the requested service. This may be a * descriptive name or a numeric string corresponding to a port number. * @param ec Set to indicate what error occurred, if any. */ void listen(std::string const & host, std::string const & service, lib::error_code & ec) { using boost::asio::ip::tcp; tcp::resolver r(*m_io_service); tcp::resolver::query query(host, service); tcp::resolver::iterator endpoint_iterator = r.resolve(query); tcp::resolver::iterator end; if (endpoint_iterator == end) { m_elog->write(log::elevel::library, "asio::listen could not resolve the supplied host or service"); ec = make_error_code(error::invalid_host_service); return; } listen(*endpoint_iterator,ec); } /// Set up endpoint for listening on a host and service /** * Bind the internal acceptor using the given host and service. More details * about what host and service can be are available in the boost asio * documentation for ip::basic_resolver_query::basic_resolver_query's * constructors. * * The endpoint must have been initialized by calling init_asio before * listening. * * @param host A string identifying a location. May be a descriptive name or * a numeric address string. * @param service A string identifying the requested service. This may be a * descriptive name or a numeric string corresponding to a port number. * @param ec Set to indicate what error occurred, if any. */ void listen(std::string const & host, std::string const & service) { lib::error_code ec; listen(host,service,ec); if (ec) { throw ec; } } /// Stop listening (exception free) /** * Stop listening and accepting new connections. This will not end any * existing connections. * * @since 0.3.0-alpha4 * @param ec A status code indicating an error, if any. */ void stop_listening(lib::error_code & ec) { if (m_state != LISTENING) { m_elog->write(log::elevel::library, "asio::listen called from the wrong state"); using websocketpp::error::make_error_code; ec = make_error_code(websocketpp::error::invalid_state); return; } m_acceptor->close(); m_state = READY; ec = lib::error_code(); } /// Stop listening /** * Stop listening and accepting new connections. This will not end any * existing connections. * * @since 0.3.0-alpha4 */ void stop_listening() { lib::error_code ec; stop_listening(ec); if (ec) { throw ec; } } /// wraps the run method of the internal io_service object std::size_t run() { return m_io_service->run(); } /// wraps the run_one method of the internal io_service object /** * @since 0.3.0-alpha4 */ std::size_t run_one() { return m_io_service->run_one(); } /// wraps the stop method of the internal io_service object void stop() { m_io_service->stop(); } /// wraps the poll method of the internal io_service object std::size_t poll() { return m_io_service->poll(); } /// wraps the poll_one method of the internal io_service object std::size_t poll_one() { return m_io_service->poll_one(); } /// wraps the reset method of the internal io_service object void reset() { m_io_service->reset(); } /// wraps the stopped method of the internal io_service object bool stopped() const { return m_io_service->stopped(); } /// Call back a function after a period of time. /** * Sets a timer that calls back a function after the specified period of * milliseconds. Returns a handle that can be used to cancel the timer. * A cancelled timer will return the error code error::operation_aborted * A timer that expired will return no error. * * @param duration Length of time to wait in milliseconds * @param callback The function to call back when the timer has expired * @return A handle that can be used to cancel the timer if it is no longer * needed. */ timer_ptr set_timer(long duration, timer_handler callback) { timer_ptr new_timer( new boost::asio::deadline_timer( *m_io_service, boost::posix_time::milliseconds(duration) ) ); new_timer->async_wait( lib::bind( &type::handle_timer, this, new_timer, callback, lib::placeholders::_1 ) ); return new_timer; } /// Timer callback /** * The timer pointer is included to ensure the timer isn't destroyed until * after it has expired. * * @param t Pointer to the timer in question * @param callback The function to call back * @param ec A status code indicating an error, if any. */ void handle_timer(timer_ptr t, timer_handler callback, boost::system::error_code const & ec) { if (ec) { if (ec == boost::asio::error::operation_aborted) { callback(make_error_code(transport::error::operation_aborted)); } else { m_elog->write(log::elevel::info, "asio handle_timer error: "+ec.message()); log_err(log::elevel::info,"asio handle_timer",ec); callback(make_error_code(error::pass_through)); } } else { callback(lib::error_code()); } } /// Accept the next connection attempt and assign it to con (exception free) /** * @param tcon The connection to accept into. * @param callback The function to call when the operation is complete. * @param ec A status code indicating an error, if any. */ void async_accept(transport_con_ptr tcon, accept_handler callback, lib::error_code & ec) { if (m_state != LISTENING) { m_elog->write(log::elevel::library, "asio::async_accept called from the wrong state"); using websocketpp::error::make_error_code; ec = make_error_code(websocketpp::error::invalid_state); return; } m_alog->write(log::alevel::devel, "asio::async_accept"); m_acceptor->async_accept( tcon->get_raw_socket(), lib::bind( &type::handle_accept, this, tcon->get_handle(), callback, lib::placeholders::_1 ) ); } /// Accept the next connection attempt and assign it to con. /** * @param tcon The connection to accept into. * @param callback The function to call when the operation is complete. */ void async_accept(transport_con_ptr tcon, accept_handler callback) { lib::error_code ec; async_accept(tcon,callback,ec); if (ec) { throw ec; } } protected: /// Initialize logging /** * The loggers are located in the main endpoint class. As such, the * transport doesn't have direct access to them. This method is called * by the endpoint constructor to allow shared logging from the transport * component. These are raw pointers to member variables of the endpoint. * In particular, they cannot be used in the transport constructor as they * haven't been constructed yet, and cannot be used in the transport * destructor as they will have been destroyed by then. */ void init_logging(alog_type* a, elog_type* e) { m_alog = a; m_elog = e; } void handle_accept(connection_hdl hdl, accept_handler callback, const boost::system::error_code& error) { if (error) { //con->terminate(); // TODO: Better translation of errors at this point callback(hdl,make_error_code(error::pass_through)); return; } //con->start(); callback(hdl,lib::error_code()); } /// Initiate a new connection // TODO: there have to be some more failure conditions here void async_connect(transport_con_ptr tcon, uri_ptr u, connect_handler cb) { using namespace boost::asio::ip; // Create a resolver if (!m_resolver) { m_resolver.reset(new boost::asio::ip::tcp::resolver(*m_io_service)); } std::string proxy = tcon->get_proxy(); std::string host; std::string port; if (proxy.empty()) { host = u->get_host(); port = u->get_port_str(); } else { lib::error_code ec; uri_ptr pu(new uri(proxy)); if (!pu->get_valid()) { cb(tcon->get_handle(),make_error_code(error::proxy_invalid)); return; } ec = tcon->proxy_init(u->get_authority()); if (ec) { cb(tcon->get_handle(),ec); return; } host = pu->get_host(); port = pu->get_port_str(); } tcp::resolver::query query(host,port); if (m_alog->static_test(log::alevel::devel)) { m_alog->write(log::alevel::devel, "starting async DNS resolve for "+host+":"+port); } timer_ptr dns_timer; dns_timer = set_timer( config::timeout_dns_resolve, lib::bind( &type::handle_resolve_timeout, this, tcon, dns_timer, cb, lib::placeholders::_1 ) ); m_resolver->async_resolve( query, lib::bind( &type::handle_resolve, this, tcon, dns_timer, cb, lib::placeholders::_1, lib::placeholders::_2 ) ); } void handle_resolve_timeout(transport_con_ptr tcon, timer_ptr dns_timer, connect_handler callback, const lib::error_code & ec) { lib::error_code ret_ec; if (ec) { if (ec == transport::error::operation_aborted) { m_alog->write(log::alevel::devel, "asio handle_resolve_timeout timer cancelled"); return; } log_err(log::elevel::devel,"asio handle_resolve_timeout",ec); ret_ec = ec; } else { ret_ec = make_error_code(transport::error::timeout); } m_alog->write(log::alevel::devel,"DNS resolution timed out"); m_resolver->cancel(); callback(tcon->get_handle(),ret_ec); } void handle_resolve(transport_con_ptr tcon, timer_ptr dns_timer, connect_handler callback, const boost::system::error_code& ec, boost::asio::ip::tcp::resolver::iterator iterator) { if (ec == boost::asio::error::operation_aborted || dns_timer->expires_from_now().is_negative()) { m_alog->write(log::alevel::devel,"async_resolve cancelled"); return; } dns_timer->cancel(); if (ec) { log_err(log::elevel::info,"asio async_resolve",ec); callback(tcon->get_handle(),make_error_code(error::pass_through)); return; } if (m_alog->static_test(log::alevel::devel)) { std::stringstream s; s << "Async DNS resolve successful. Results: "; boost::asio::ip::tcp::resolver::iterator it, end; for (it = iterator; it != end; ++it) { s << (*it).endpoint() << " "; } m_alog->write(log::alevel::devel,s.str()); } m_alog->write(log::alevel::devel,"Starting async connect"); timer_ptr con_timer; con_timer = set_timer( config::timeout_connect, lib::bind( &type::handle_resolve_timeout, this, tcon, con_timer, callback, lib::placeholders::_1 ) ); boost::asio::async_connect( tcon->get_raw_socket(), iterator, lib::bind( &type::handle_connect, this, tcon, con_timer, callback, lib::placeholders::_1 ) ); } void handle_connect_timeout(transport_con_ptr tcon, timer_ptr con_timer, connect_handler callback, const lib::error_code & ec) { lib::error_code ret_ec; if (ec) { if (ec == transport::error::operation_aborted) { m_alog->write(log::alevel::devel, "asio handle_connect_timeout timer cancelled"); return; } log_err(log::elevel::devel,"asio handle_connect_timeout",ec); ret_ec = ec; } else { ret_ec = make_error_code(transport::error::timeout); } m_alog->write(log::alevel::devel,"TCP connect timed out"); tcon->cancel_socket(); callback(tcon->get_handle(),ret_ec); } void handle_connect(transport_con_ptr tcon, timer_ptr con_timer, connect_handler callback, const boost::system::error_code& ec) { if (ec == boost::asio::error::operation_aborted || con_timer->expires_from_now().is_negative()) { m_alog->write(log::alevel::devel,"async_connect cancelled"); return; } con_timer->cancel(); if (ec) { log_err(log::elevel::info,"asio async_connect",ec); callback(tcon->get_handle(),make_error_code(error::pass_through)); return; } if (m_alog->static_test(log::alevel::devel)) { m_alog->write(log::alevel::devel, "Async connect to "+tcon->get_remote_endpoint()+" successful."); } callback(tcon->get_handle(),lib::error_code()); } bool is_listening() const { return (m_state == LISTENING); } /// Initialize a connection /** * init is called by an endpoint once for each newly created connection. * It's purpose is to give the transport policy the chance to perform any * transport specific initialization that couldn't be done via the default * constructor. * * @param tcon A pointer to the transport portion of the connection. * * @return A status code indicating the success or failure of the operation */ lib::error_code init(transport_con_ptr tcon) { m_alog->write(log::alevel::devel, "transport::asio::init"); // Initialize the connection socket component socket_type::init(lib::static_pointer_cast(tcon)); lib::error_code ec; ec = tcon->init_asio(m_io_service); if (ec) {return ec;} tcon->set_tcp_init_handler(m_tcp_init_handler); return lib::error_code(); } private: /// Convenience method for logging the code and message for an error_code template void log_err(log::level l, char const * msg, error_type const & ec) { std::stringstream s; s << msg << " error: " << ec << " (" << ec.message() << ")"; m_elog->write(l,s.str()); } enum state { UNINITIALIZED = 0, READY = 1, LISTENING = 2 }; // Handlers tcp_init_handler m_tcp_init_handler; // Network Resources io_service_ptr m_io_service; bool m_external_io_service; acceptor_ptr m_acceptor; resolver_ptr m_resolver; elog_type* m_elog; alog_type* m_alog; // Transport state state m_state; }; } // namespace asio } // namespace transport } // namespace websocketpp #endif // WEBSOCKETPP_TRANSPORT_ASIO_HPP