changeset 780:560b62f6b0a7

Core: rework socket layer, closes #939 @6h Rework the ip_connector and ip_acceptor to be more easy to use. Also, ip_connector will now use a resolver to find appropriate endpoints. Bring back full support for IPv6/IPv4 with all possible combinations. The tls_stream class now owns a shared ssl::context that is copied from the acceptor or the connector. The tls_connector and tls_acceptor wraps basic ones for convenience and simplicity. Irccd and irccdctl now support local SSL sockets.
author David Demelier <markand@malikania.fr>
date Sun, 04 Nov 2018 17:26:05 +0100
parents 317c66a131be
children baf9258e9cbb
files CHANGES.md MIGRATING.md irccdctl/main.cpp libirccd-core/CMakeLists.txt libirccd-core/irccd/acceptor.hpp libirccd-core/irccd/connector.hpp libirccd-core/irccd/socket_acceptor.hpp libirccd-core/irccd/socket_connector.hpp libirccd-core/irccd/socket_stream.hpp libirccd-core/irccd/stream.hpp libirccd-core/irccd/tls_acceptor.hpp libirccd-core/irccd/tls_connector.hpp libirccd-core/irccd/tls_stream.hpp libirccd-ctl/irccd/ctl/controller.cpp libirccd-ctl/irccd/ctl/controller.hpp libirccd-test/irccd/test/cli_fixture.cpp libirccd-test/irccd/test/command_fixture.cpp libirccd/irccd/daemon/server.cpp libirccd/irccd/daemon/transport_client.cpp libirccd/irccd/daemon/transport_client.hpp libirccd/irccd/daemon/transport_util.cpp tests/src/libcommon/stream/CMakeLists.txt tests/src/libcommon/stream/main.cpp
diffstat 23 files changed, 1348 insertions(+), 1048 deletions(-) [+]
line wrap: on
line diff
--- a/CHANGES.md	Thu Nov 01 10:34:21 2018 +0100
+++ b/CHANGES.md	Sun Nov 04 17:26:05 2018 +0100
@@ -12,7 +12,8 @@
   standard paths for both irccd and plugins (#611),
 - If Mercurial is found, the version is bundled in `irccd --version`,
 - Irccd no longer supports uid, gid, pid and daemon features (#846).
-- Sections `[identity]` and `[server]` have been merged (#905).
+- Sections `[identity]` and `[server]` have been merged (#905),
+- Local transports support SSL too (#939).
 
 Irccd test:
 
--- a/MIGRATING.md	Thu Nov 01 10:34:21 2018 +0100
+++ b/MIGRATING.md	Sun Nov 04 17:26:05 2018 +0100
@@ -9,7 +9,8 @@
 ### Irccdctl
 
   - The functions `server-cnotice` and `server-cmode` have been removed, use
-    `server-notice` and `server-mode` instead.
+    `server-notice` and `server-mode` instead,
+  - The option `connect.host` has been renamed to `connect.hostname` (#941).
 
 ### Plugins
 
--- a/irccdctl/main.cpp	Thu Nov 01 10:34:21 2018 +0100
+++ b/irccdctl/main.cpp	Sun Nov 04 17:26:05 2018 +0100
@@ -16,8 +16,6 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-// TODO: don't create a temporary list of endpoints.
-
 #include <irccd/sysconfig.hpp>
 
 #include <iostream>
@@ -28,16 +26,12 @@
 #include <boost/predef/os.h>
 
 #include <irccd/config.hpp>
+#include <irccd/connector.hpp>
 #include <irccd/json_util.hpp>
 #include <irccd/options.hpp>
-#include <irccd/socket_connector.hpp>
 #include <irccd/string_util.hpp>
 #include <irccd/system.hpp>
 
-#if defined(IRCCD_HAVE_SSL)
-#	include <irccd/tls_connector.hpp>
-#endif
-
 #include <irccd/daemon/transport_server.hpp>
 
 #include <irccd/ctl/controller.hpp>
@@ -48,17 +42,7 @@
 using boost::format;
 using boost::str;
 
-using boost::asio::ip::tcp;
-
-#if !BOOST_OS_WINDOWS
-
-using boost::asio::local::stream_protocol;
-
-#endif
-
-namespace irccd {
-
-namespace ctl {
+namespace irccd::ctl {
 
 namespace {
 
@@ -86,25 +70,6 @@
 }
 
 /*
- * resolve
- * ------------------------------------------------------------------
- *
- * Block unless host/port has been resolved.
- */
-auto resolve(const std::string& host, const std::string& name) -> std::vector<tcp::endpoint>
-{
-	std::vector<tcp::endpoint> endpoints;
-
-	tcp::resolver resolver(service);
-	tcp::resolver::query query(host, name);
-
-	for (auto it = resolver.resolve(query); it != tcp::resolver::iterator(); ++it)
-		endpoints.push_back(*it);
-
-	return endpoints;
-}
-
-/*
  * read_connect_ip
  * -------------------------------------------------------------------
  *
@@ -112,35 +77,49 @@
  *
  * [connect]
  * type = "ip"
- * host = "ip or hostname"
+ * hostname = "ip or hostname"
  * port = "port number or service"
- * domain = "ipv4 or ipv6" (Optional, default: ipv4)
+ * family = "ipv4, ipv6" (Optional, default: ipv4)
  * ssl = true | false
  */
 auto read_connect_ip(const ini::section& sc) -> std::unique_ptr<connector>
 {
-	const auto host = sc.get("host").get_value();
+	const auto hostname = sc.get("hostname").get_value();
 	const auto port = sc.get("port").get_value();
+	bool ipv4 = true;
+	bool ipv6 = true;
 
-	if (host.empty())
+	if (auto it = sc.find("family"); it != sc.end()) {
+		ipv4 = ipv6 = false;
+
+		for (auto v : *it) {
+			if (v == "ipv4")
+				ipv4 = true;
+			else if (v == "ipv6")
+				ipv6 = true;
+		}
+	}
+
+	if (!ipv4 && !ipv6)
+		throw transport_error(transport_error::invalid_family);
+	if (hostname.empty())
 		throw transport_error(transport_error::invalid_hostname);
 	if (port.empty())
 		throw transport_error(transport_error::invalid_port);
 
-	const auto endpoints = resolve(host, port);
-
 	if (string_util::is_boolean(sc.get("ssl").get_value())) {
 #if defined(IRCCD_HAVE_SSL)
 		// TODO: support more parameters.
-		boost::asio::ssl::context ctx(boost::asio::ssl::context::sslv23);
+		boost::asio::ssl::context ctx(boost::asio::ssl::context::tlsv12);
 
-		return std::make_unique<tls_connector<>>(std::move(ctx), service, endpoints);
+		return std::make_unique<tls_ip_connector>(std::move(ctx),
+			service, hostname, port, ipv4, ipv6);
 #else
 		throw std::runtime_error("SSL disabled");
 #endif
 	}
 
-	return std::make_unique<ip_connector>(service, endpoints);
+	return std::make_unique<ip_connector>(service, hostname, port, ipv4, ipv6);
 }
 
 /*
@@ -163,6 +142,17 @@
 	if (it == sc.end())
 		throw std::invalid_argument("missing path parameter");
 
+	if (string_util::is_boolean(sc.get("ssl").get_value())) {
+#if defined(IRCCD_HAVE_SSL)
+		// TODO: support more parameters.
+		boost::asio::ssl::context ctx(boost::asio::ssl::context::tlsv12);
+
+		return std::make_unique<tls_local_connector>(std::move(ctx), service, it->get_value());
+#else
+		throw std::runtime_error("SSL disabled");
+#endif
+	}
+
 	return std::make_unique<local_connector>(service, it->get_value());
 #else
 	(void)sc;
@@ -291,7 +281,7 @@
  * -h host or ip
  * -p port
  */
-auto parse_connect_ip(const option::result& options) -> std::unique_ptr<connector>
+auto parse_connect_ip(std::string_view type, const option::result& options) -> std::unique_ptr<connector>
 {
 	option::result::const_iterator it;
 
@@ -299,7 +289,7 @@
 	if ((it = options.find("-h")) == options.end() && (it = options.find("--host")) == options.end())
 		throw transport_error(transport_error::invalid_hostname);
 
-	const auto host = it->second;
+	const auto hostname = it->second;
 
 	// Port (-p or --port).
 	if ((it = options.find("-p")) == options.end() && (it = options.find("--port")) == options.end())
@@ -307,7 +297,11 @@
 
 	const auto port = it->second;
 
-	return std::make_unique<ip_connector>(service, resolve(host, port));
+	// Type (-t or --type).
+	const auto ipv4 = type == "ip";
+	const auto ipv6 = type == "ipv6";
+
+	return std::make_unique<ip_connector>(service, hostname, port, ipv4, ipv6);
 }
 
 /*
@@ -352,7 +346,7 @@
 	std::unique_ptr<connector> connector;
 
 	if (it->second == "ip" || it->second == "ipv6")
-		connector = parse_connect_ip(options);
+		connector = parse_connect_ip(it->second, options);
 	if (it->second == "unix")
 		connector = parse_connect_local(options);
 	else
@@ -483,9 +477,9 @@
 
 		if (verbose) {
 			const json_util::deserializer doc(info);
-			const auto major = doc.get<int>("/major");
-			const auto minor = doc.get<int>("/minor");
-			const auto patch = doc.get<int>("/patch");
+			const auto major = doc.get<int>("major");
+			const auto minor = doc.get<int>("minor");
+			const auto patch = doc.get<int>("patch");
 
 			if (!major || !minor || !patch)
 				std::cout << "connected to irccd (unknown version)" << std::endl;
@@ -514,9 +508,7 @@
 
 } // !namespace
 
-} // !ctl
-
-} // !irccd
+} // !irccd::ctl
 
 int main(int argc, char** argv)
 {
--- a/libirccd-core/CMakeLists.txt	Thu Nov 01 10:34:21 2018 +0100
+++ b/libirccd-core/CMakeLists.txt	Sun Nov 04 17:26:05 2018 +0100
@@ -30,15 +30,9 @@
 	${libirccd-core_SOURCE_DIR}/irccd/ini_util.hpp
 	${libirccd-core_SOURCE_DIR}/irccd/json_util.hpp
 	${libirccd-core_SOURCE_DIR}/irccd/options.hpp
-	${libirccd-core_SOURCE_DIR}/irccd/socket_acceptor.hpp
-	${libirccd-core_SOURCE_DIR}/irccd/socket_connector.hpp
-	${libirccd-core_SOURCE_DIR}/irccd/socket_stream.hpp
 	${libirccd-core_SOURCE_DIR}/irccd/stream.hpp
 	${libirccd-core_SOURCE_DIR}/irccd/string_util.hpp
 	${libirccd-core_SOURCE_DIR}/irccd/system.hpp
-	${libirccd-core_SOURCE_DIR}/irccd/tls_acceptor.hpp
-	${libirccd-core_SOURCE_DIR}/irccd/tls_connector.hpp
-	${libirccd-core_SOURCE_DIR}/irccd/tls_stream.hpp
 	${libirccd-core_SOURCE_DIR}/irccd/xdg.hpp
 )
 
--- a/libirccd-core/irccd/acceptor.hpp	Thu Nov 01 10:34:21 2018 +0100
+++ b/libirccd-core/irccd/acceptor.hpp	Sun Nov 04 17:26:05 2018 +0100
@@ -24,13 +24,23 @@
  * \brief Abstract stream acceptor interface.
  */
 
+#include <irccd/sysconfig.hpp>
+
+#include <cassert>
 #include <functional>
 #include <memory>
 #include <system_error>
 
-namespace irccd {
+#include <boost/asio.hpp>
+#include <boost/filesystem/path.hpp>
 
-class stream;
+#if defined(IRCCD_HAVE_SSL)
+#	include <boost/asio/ssl.hpp>
+#endif
+
+#include "stream.hpp"
+
+namespace irccd {
 
 /**
  * \brief Abstract stream acceptor interface.
@@ -69,6 +79,422 @@
 	virtual void accept(handler handler) = 0;
 };
 
+// {{{ basic_socket_acceptor
+
+/**
+ * \brief Convenient acceptor owner.
+ */
+template <typename Acceptor>
+class basic_socket_acceptor : public acceptor {
+public:
+	/**
+	 * Underlying socket type.
+	 */
+	using socket_type = typename Acceptor::protocol_type::socket;
+
+private:
+#if !defined(NDEBUG)
+	bool is_accepting_{false};
+#endif
+
+protected:
+	/**
+	 * \brief The I/O context.
+	 */
+	boost::asio::io_context& service_;
+
+	/**
+	 * \brief The underlying acceptor.
+	 */
+	Acceptor acceptor_;
+
+public:
+	/**
+	 * Construct a basic_socket_acceptor.
+	 *
+	 * \param service the I/O context
+	 */
+	basic_socket_acceptor(boost::asio::io_context& service);
+
+	/**
+	 * Construct a basic_socket_acceptor with a already bound native
+	 * acceptor.
+	 *
+	 * \param service the I/O context
+	 * \param acceptor the acceptor
+	 */
+	basic_socket_acceptor(boost::asio::io_context& service, Acceptor acceptor) noexcept;
+
+	/**
+	 * Get the I/O context.
+	 *
+	 * \return the context
+	 */
+	auto get_service() const noexcept -> const boost::asio::io_context&;
+
+	/**
+	 * Overloaded function.
+	 *
+	 * \return the context
+	 */
+	auto get_service() noexcept -> boost::asio::io_context&;
+
+	/**
+	 * Get the underlying acceptor.
+	 *
+	 * \return the acceptor
+	 */
+	auto get_acceptor() const noexcept -> const Acceptor&;
+
+	/**
+	 * Overloaded function.
+	 *
+	 * \return the acceptor
+	 */
+	auto get_acceptor() noexcept -> Acceptor&;
+
+	/**
+	 * Accept a new client.
+	 *
+	 * \pre another accept call must not be running
+	 * \param sc the socket type
+	 * \param handler the handler
+	 * \note implemented for SocketAcceptor concept
+	 */
+	template <typename Socket, typename Handler>
+	void accept(Socket& sc, Handler handler);
+};
+
+template <typename Acceptor>
+inline basic_socket_acceptor<Acceptor>::basic_socket_acceptor(boost::asio::io_context& service)
+	: service_(service)
+	, acceptor_(service)
+{
+}
+
+template <typename Acceptor>
+inline basic_socket_acceptor<Acceptor>::basic_socket_acceptor(boost::asio::io_context& service, Acceptor acceptor) noexcept
+	: service_(service)
+	, acceptor_(std::move(acceptor))
+{
+}
+
+template <typename Acceptor>
+inline auto basic_socket_acceptor<Acceptor>::get_service() const noexcept -> const boost::asio::io_context&
+{
+	return service_;
+}
+
+template <typename Acceptor>
+inline auto basic_socket_acceptor<Acceptor>::get_service() noexcept -> boost::asio::io_context&
+{
+	return service_;
+}
+
+template <typename Acceptor>
+inline auto basic_socket_acceptor<Acceptor>::get_acceptor() const noexcept -> const Acceptor&
+{
+	return acceptor_;
+}
+
+template <typename Acceptor>
+inline auto basic_socket_acceptor<Acceptor>::get_acceptor() noexcept -> Acceptor&
+{
+	return acceptor_;
+}
+
+template <typename Acceptor>
+template <typename Socket, typename Handler>
+inline void basic_socket_acceptor<Acceptor>::accept(Socket& sc, Handler handler)
+{
+#if !defined(NDEBUG)
+	assert(!is_accepting_);
+
+	is_accepting_ = true;
+#endif
+
+	assert(acceptor_.is_open());
+
+	acceptor_.async_accept(sc, [this, handler] (auto code) {
+#if !defined(NDEBUG)
+		is_accepting_ = false;
+#endif
+		(void)this;
+		handler(std::move(code));
+	});
+}
+
+// }}}
+
+// {{{ ip_acceptor
+
+/**
+ * \brief TCP/IP acceptor.
+ */
+class ip_acceptor : public basic_socket_acceptor<boost::asio::ip::tcp::acceptor> {
+private:
+	void open(bool ipv4, bool ipv6);
+	void set(bool ipv4, bool ipv6);
+	void bind(const std::string& address, std::uint16_t port, bool ipv4, bool ipv6);
+
+public:
+	/**
+	 * \pre at least ipv4 or ipv6 must be true
+	 * \param service the I/O service
+	 * \param address the address to bind or * for any
+	 * \param port the port number
+	 * \param ipv4 enable ipv4
+	 * \param ipv6 enable ipv6
+	 */
+	ip_acceptor(boost::asio::io_context& service,
+	            std::string address,
+	            std::uint16_t port,
+	            bool ipv4 = true,
+	            bool ipv6 = true);
+
+	/**
+	 * Inherited constructors.
+	 */
+	using basic_socket_acceptor::basic_socket_acceptor;
+
+	/**
+	 * Inherited functions.
+	 */
+	using basic_socket_acceptor::accept;
+
+	/**
+	 * \copydoc acceptor::accept
+	 */
+	void accept(handler handler) override;
+};
+
+inline void ip_acceptor::open(bool ipv4, bool ipv6)
+{
+	using boost::asio::ip::tcp;
+
+	if (ipv6)
+		acceptor_.open(tcp::v6());
+	else
+		acceptor_.open(tcp::v4());
+}
+
+inline void ip_acceptor::set(bool ipv4, bool ipv6)
+{
+	using boost::asio::socket_base;
+	using boost::asio::ip::v6_only;
+
+	if (ipv6)
+		acceptor_.set_option(v6_only(!ipv4));
+
+	acceptor_.set_option(socket_base::reuse_address(true));
+}
+
+inline void ip_acceptor::bind(const std::string& address, std::uint16_t port, bool ipv4, bool ipv6)
+{
+	using boost::asio::ip::make_address_v4;
+	using boost::asio::ip::make_address_v6;
+	using boost::asio::ip::tcp;
+
+	tcp::endpoint ep;
+
+	if (address == "*")
+		ep = tcp::endpoint(ipv6 ? tcp::v6() : tcp::v4(), port);
+	else if (ipv6)
+		ep = tcp::endpoint(make_address_v6(address), port);
+	else
+		ep = tcp::endpoint(make_address_v4(address), port);
+
+	acceptor_.bind(ep);
+	acceptor_.listen();
+}
+
+inline ip_acceptor::ip_acceptor(boost::asio::io_context& service,
+                                std::string address,
+                                std::uint16_t port,
+                                bool ipv4,
+                                bool ipv6)
+	: basic_socket_acceptor(service)
+{
+	open(ipv4, ipv6);
+	set(ipv4, ipv6);
+	bind(address, port, ipv4, ipv6);
+}
+
+inline void ip_acceptor::accept(handler handler)
+{
+	auto stream = std::make_shared<ip_stream>(service_);
+
+	basic_socket_acceptor::accept(stream->get_socket(), [handler, stream] (auto code) {
+		if (code)
+			handler(std::move(code), nullptr);
+		else
+			handler(std::move(code), std::move(stream));
+	});
+}
+
+// }}}
+
+// {{{ local_acceptor
+
+#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS)
+
+class local_acceptor : public basic_socket_acceptor<boost::asio::local::stream_protocol::acceptor> {
+public:
+	/**
+	 * Construct a local acceptor.
+	 *
+	 * \param service the I/O service
+	 * \param path the unix socket file
+	 */
+	local_acceptor(boost::asio::io_context& service,
+	               const boost::filesystem::path& path);
+
+	/**
+	 * Inherited constructors.
+	 */
+	using basic_socket_acceptor::basic_socket_acceptor;
+
+	/**
+	 * Inherited functions.
+	 */
+	using basic_socket_acceptor::accept;
+
+	/**
+	 * \copydoc acceptor::accept
+	 */
+	void accept(handler handler) override;
+};
+
+inline local_acceptor::local_acceptor(boost::asio::io_context& service,
+                                      const boost::filesystem::path& path)
+	: basic_socket_acceptor(service)
+{
+	using boost::asio::socket_base;
+
+	std::remove(path.string().c_str());
+
+	acceptor_.open();
+	acceptor_.set_option(socket_base::reuse_address(true));
+	acceptor_.bind({ path.string() });
+	acceptor_.listen();
+}
+
+inline void local_acceptor::accept(handler handler)
+{
+	auto stream = std::make_shared<local_stream>(service_);
+
+	basic_socket_acceptor::accept(stream->get_socket(), [handler, stream] (auto code) {
+		if (code)
+			handler(std::move(code), nullptr);
+		else
+			handler(std::move(code), std::move(stream));
+	});
+}
+
+#endif
+
+// }}}
+
+// {{{ tls_acceptor
+
+#if defined(IRCCD_HAVE_SSL)
+
+/**
+ * \brief TLS/SSL acceptors.
+ * \tparam SocketAcceptor the socket connector (e.g. ip_acceptor)
+ *
+ * Wrap a SocketAcceptor object.
+ *
+ * The SocketAcceptor object must have the following types:
+ *
+ * ```cpp
+ * using socket_type = implementation-defined
+ * ```
+ *
+ * The following function:
+ *
+ * ```cpp
+ * template <typename Handler>
+ * void accept(socket_type& sc, Handler handler);
+ *
+ * auto get_context() -> boost::asio::io_context&
+ * ```
+ *
+ * The Handler callback must have the signature
+ * `void f(const std::error_code&)`.
+ */
+template <typename SocketAcceptor>
+class tls_acceptor : public acceptor {
+private:
+	using socket_type = typename SocketAcceptor::socket_type;
+
+	std::shared_ptr<boost::asio::ssl::context> context_;
+	SocketAcceptor acceptor_;
+
+public:
+	/**
+	 * Construct a secure layer transport server.
+	 *
+	 * \param context the SSL context
+	 * \param args the arguments to SocketAcceptor constructor
+	 */
+	template <typename... Args>
+	tls_acceptor(boost::asio::ssl::context context, Args&&... args);
+
+	/**
+	 * \copydoc acceptor::accept
+	 */
+	void accept(handler handler) override;
+};
+
+template <typename SocketAcceptor>
+template <typename... Args>
+inline tls_acceptor<SocketAcceptor>::tls_acceptor(boost::asio::ssl::context context, Args&&... args)
+	: context_(std::make_shared<boost::asio::ssl::context>(std::move(context)))
+	, acceptor_(std::forward<Args>(args)...)
+{
+}
+
+template <typename SocketAcceptor>
+inline void tls_acceptor<SocketAcceptor>::accept(handler handler)
+{
+	auto client = std::make_shared<tls_stream<socket_type>>(acceptor_.get_service(), context_);
+
+	acceptor_.accept(client->get_socket().lowest_layer(), [handler, client] (auto code) {
+		using boost::asio::ssl::stream_base;
+
+		if (code) {
+			handler(std::move(code), nullptr);
+			return;
+		}
+
+		client->get_socket().async_handshake(stream_base::server, [handler, client] (auto code) {
+			if (code)
+				handler(std::move(code), nullptr);
+			else
+				handler(std::move(code), std::move(client));
+		});
+	});
+}
+
+/**
+ * \brief Convenient alias.
+ */
+using tls_ip_acceptor = tls_acceptor<ip_acceptor>;
+
+#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS)
+
+/**
+ * \brief Convenient alias.
+ */
+using tls_local_acceptor = tls_acceptor<local_acceptor>;
+
+#endif // !BOOST_ASIO_HAS_LOCAL_SOCKETS
+
+#endif // !IRCCD_HAVE_SSL
+
+// }}}
+
 } // !irccd
 
 #endif // !IRCCD_ACCEPTOR_HPP
--- a/libirccd-core/irccd/connector.hpp	Thu Nov 01 10:34:21 2018 +0100
+++ b/libirccd-core/irccd/connector.hpp	Sun Nov 04 17:26:05 2018 +0100
@@ -24,13 +24,22 @@
  * \brief Abstract connection interface.
  */
 
+#include <cassert>
 #include <functional>
 #include <memory>
 #include <system_error>
 
-namespace irccd {
+#include <boost/asio.hpp>
+
+#if defined(IRCCD_HAVE_SSL)
+#	include <boost/asio/ssl.hpp>
+#endif
 
-class stream;
+#include <boost/filesystem/path.hpp>
+
+#include "stream.hpp"
+
+namespace irccd {
 
 /**
  * \brief Abstract connection interface.
@@ -70,6 +79,357 @@
 	virtual void connect(handler handler) = 0;
 };
 
+// {{{ socket_connector_base
+
+/**
+ * \brief Provide convenient functions for connectors.
+ */
+class socket_connector_base : public connector {
+protected:
+	boost::asio::io_context& service_;
+
+public:
+	/**
+	 * Construct the connector
+	 *
+	 * \param service the service
+	 */
+	socket_connector_base(boost::asio::io_context& service);
+
+	/**
+	 * Get the I/O service.
+	 *
+	 * \return the service
+	 */
+	auto get_service() const noexcept -> const boost::asio::io_context&;
+
+	/**
+	 * Overloaded function.
+	 *
+	 * \return the service
+	 */
+	auto get_service() noexcept -> boost::asio::io_context&;
+};
+
+inline socket_connector_base::socket_connector_base(boost::asio::io_context& service)
+	: service_(service)
+{
+}
+
+inline auto socket_connector_base::get_service() const noexcept -> const boost::asio::io_context&
+{
+	return service_;
+}
+
+inline auto socket_connector_base::get_service() noexcept -> boost::asio::io_context&
+{
+	return service_;
+}
+
+// }}}
+
+// {{{ ip_connector
+
+/**
+ * \brief TCP/IP connector.
+ */
+class ip_connector : public socket_connector_base {
+public:
+	/**
+	 * Underlying socket type.
+	 */
+	using socket_type = boost::asio::ip::tcp::socket;
+
+private:
+	boost::asio::ip::tcp::resolver resolver_;
+
+	std::string hostname_;
+	std::string port_;
+
+	bool ipv4_;
+	bool ipv6_;
+
+#if !defined(NDEBUG)
+	bool is_connecting_{false};
+#endif
+
+	template <typename Handler>
+	void resolve(Handler handler);
+
+public:
+	/**
+	 * Construct the TCP/IP connector.
+	 *
+	 * \pre at least ipv4 or ipv6 must be true
+	 * \param service the I/O context
+	 * \param hostname the hostname
+	 * \param port the port or service name
+	 * \param ipv4 enable IPv4
+	 * \param ipv6 enable IPv6
+	 */
+	ip_connector(boost::asio::io_context& service,
+	             std::string hostname,
+	             std::string port,
+	             bool ipv4 = true,
+	             bool ipv6 = true) noexcept;
+
+	/**
+	 * Connect to the given socket.
+	 *
+	 * \param sc the socket type
+	 * \param handler the handler
+	 * \note implemented for SocketConnector concept
+	 */
+	template <typename Socket, typename Handler>
+	void connect(Socket& sc, Handler handler);
+
+	/**
+	 * \copydoc connector::connect
+	 */
+	void connect(handler handler);
+};
+
+template <typename Handler>
+inline void ip_connector::resolve(Handler handler)
+{
+	using boost::asio::ip::tcp;
+
+	if (ipv6_ && ipv4_)
+		resolver_.async_resolve(hostname_, port_, handler);
+	else if (ipv6_)
+		resolver_.async_resolve(tcp::v6(), hostname_, port_, handler);
+	else
+		resolver_.async_resolve(tcp::v4(), hostname_, port_, handler);
+}
+
+inline ip_connector::ip_connector(boost::asio::io_context& service,
+                                  std::string host,
+                                  std::string port,
+                                  bool ipv4,
+                                  bool ipv6) noexcept
+	: socket_connector_base(service)
+	, resolver_(service)
+	, hostname_(std::move(host))
+	, port_(std::move(port))
+	, ipv4_(ipv4)
+	, ipv6_(ipv6)
+{
+	assert(!hostname_.empty());
+	assert(!port_.empty());
+	assert(ipv4 || ipv6);
+}
+
+template <typename Socket, typename Handler>
+inline void ip_connector::connect(Socket& sc, Handler handler)
+{
+#if !defined(NDEBUG)
+	assert(!is_connecting_);
+
+	is_connecting_ = true;
+#endif
+
+	resolve([this, &sc, handler] (auto code, auto res) {
+#if !defined(NDEBUG)
+		is_connecting_ = false;
+#endif
+		(void)this;
+
+		if (code) {
+			handler(std::move(code));
+			return;
+		}
+
+		async_connect(sc, res, [handler] (auto code, auto) {
+			handler(std::move(code));
+		});
+	});
+}
+
+inline void ip_connector::connect(handler handler)
+{
+	auto stream = std::make_shared<ip_stream>(service_);
+
+	connect(stream->get_socket(), [handler, stream] (auto code) {
+		if (code)
+			handler(std::move(code), nullptr);
+		else
+			handler(std::move(code), std::move(stream));
+	});
+}
+
+// }}}
+
+// {{{ local_connector
+
+#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS)
+
+/**
+ * \brief Unix domain connector.
+ */
+class local_connector : public socket_connector_base {
+public:
+	/**
+	 * Underlying socket type.
+	 */
+	using socket_type = boost::asio::local::stream_protocol::socket;
+
+private:
+	boost::filesystem::path path_;
+
+#if !defined(NDEBUG)
+	bool is_connecting_{false};
+#endif
+
+public:
+	/**
+	 * Construct a local connector.
+	 *
+	 * \param service the service
+	 * \param path the path to the file
+	 */
+	local_connector(boost::asio::io_context& service,
+	                boost::filesystem::path path) noexcept;
+
+	/**
+	 * Connect to the given socket.
+	 *
+	 * \param sc the socket type
+	 * \param handler the handler
+	 * \note implemented for SocketConnector concept
+	 */
+	template <typename Socket, typename Handler>
+	void connect(Socket& sc, Handler handler) noexcept;
+
+	/**
+	 * \copydoc connector::connect
+	 */
+	void connect(handler handler);
+};
+
+inline local_connector::local_connector(boost::asio::io_context& service,
+                                        boost::filesystem::path path) noexcept
+	: socket_connector_base(service)
+	, path_(std::move(path))
+{
+}
+
+template <typename Socket, typename Handler>
+inline void local_connector::connect(Socket& sc, Handler handler) noexcept
+{
+#if !defined(NDEBUG)
+	assert(!is_connecting_);
+
+	is_connecting_ = true;
+#endif
+
+	sc.async_connect({ path_.string() }, [this, handler] (auto code) {
+#if !defined(NDEBUG)
+		is_connecting_ = false;
+#endif
+		(void)this;
+		handler(std::move(code));
+	});
+}
+
+inline void local_connector::connect(handler handler)
+{
+	auto stream = std::make_shared<local_stream>(service_);
+
+	connect(stream->get_socket(), [handler, stream] (auto code) {
+		if (code)
+			handler(std::move(code), nullptr);
+		else
+			handler(std::move(code), std::move(stream));
+	});
+}
+
+#endif // !BOOST_ASIO_HAS_LOCAL_SOCKETS
+
+// }}}
+
+// {{{ tls_connector
+
+#if defined(IRCCD_HAVE_SSL)
+
+/**
+ * \brief TLS/SSL connectors.
+ * \tparam SocketConnector the socket connector (e.g. ip_connector)
+ */
+template <typename SocketConnector>
+class tls_connector : public connector {
+public:
+	using socket_type = typename SocketConnector::socket_type;
+
+private:
+	std::shared_ptr<boost::asio::ssl::context> context_;
+	SocketConnector connector_;
+
+public:
+	/**
+	 * Construct a secure layer transport server.
+	 *
+	 * \param context the SSL context
+	 * \param args the arguments to SocketConnector constructor
+	 */
+	template <typename... Args>
+	tls_connector(boost::asio::ssl::context context, Args&&... args);
+
+	/**
+	 * \copydoc socket_connector::connect
+	 */
+	void connect(handler handler) override;
+};
+
+template <typename SocketConnector>
+template <typename... Args>
+inline tls_connector<SocketConnector>::tls_connector(boost::asio::ssl::context context, Args&&... args)
+	: context_(std::make_shared<boost::asio::ssl::context>(std::move(context)))
+	, connector_(std::forward<Args>(args)...)
+{
+}
+
+template <typename SocketConnector>
+inline void tls_connector<SocketConnector>::connect(handler handler)
+{
+	using boost::asio::ssl::stream_base;
+
+	assert(handler);
+
+	auto stream = std::make_shared<tls_stream<socket_type>>(connector_.get_service(), context_);
+
+	connector_.connect(stream->get_socket().lowest_layer(), [handler, stream] (auto code) {
+		if (code) {
+			handler(code, nullptr);
+			return;
+		}
+
+		stream->get_socket().async_handshake(stream_base::client, [handler, stream] (auto code) {
+			if (code)
+				handler(std::move(code), nullptr);
+			else
+				handler(std::move(code), std::move(stream));
+		});
+	});
+}
+
+/**
+ * \brief Convenient alias.
+ */
+using tls_ip_connector = tls_connector<ip_connector>;
+
+#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS)
+
+/**
+ * \brief Convenient alias.
+ */
+using tls_local_connector = tls_connector<local_connector>;
+
+#endif // !BOOST_ASIO_HAS_LOCAL_SOCKETS
+
+#endif // !IRCCD_HAVE_SSL
+
+// }}}
+
 } // !irccd
 
 #endif // !IRCCD_CONNECTOR_HPP
--- a/libirccd-core/irccd/socket_acceptor.hpp	Thu Nov 01 10:34:21 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,169 +0,0 @@
-/*
- * socket_acceptor.hpp -- socket stream acceptor interface
- *
- * Copyright (c) 2013-2018 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef IRCCD_SOCKET_ACCEPTOR_HPP
-#define IRCCD_SOCKET_ACCEPTOR_HPP
-
-/**
- * \file socket_acceptor.hpp
- * \brief Socket stream acceptor interface.
- */
-
-#include <irccd/sysconfig.hpp>
-
-#include "acceptor.hpp"
-#include "socket_stream.hpp"
-
-namespace irccd {
-
-/**
- * \brief Socket stream acceptor interface.
- * \tparam Protocol a Boost.Asio compatible protocol (e.g. ip::tcp)
- */
-template <typename Protocol>
-class socket_acceptor : public acceptor {
-public:
-	/**
-	 * Convenient endpoint alias.
-	 */
-	using endpoint = typename Protocol::endpoint;
-
-	/**
-	 * Convenient acceptor alias.
-	 */
-	using acceptor = typename Protocol::acceptor;
-
-	/**
-	 * Convenient socket alias.
-	 */
-	using socket = typename Protocol::socket;
-
-private:
-	acceptor acceptor_;
-
-#if !defined(NDEBUG)
-	bool is_accepting_{false};
-#endif
-
-protected:
-	/**
-	 * Helper to accept on the real underlying socket.
-	 *
-	 * \param socket the real socket
-	 * \param handler the handler
-	 */
-	template <typename Socket, typename Handler>
-	void do_accept(Socket& socket, Handler handler);
-
-public:
-	/**
-	 * Construct the socket_acceptor.
-	 *
-	 * \pre acceptor must be ready (is_open() returns true)
-	 * \param acceptor the Boost.Asio acceptor
-	 */
-	socket_acceptor(acceptor acceptor) noexcept;
-
-	/**
-	 * Get the underlying acceptor.
-	 *
-	 * \return the acceptor
-	 */
-	auto get_acceptor() const noexcept -> const acceptor&;
-
-	/**
-	 * Overloaded function.
-	 *
-	 * \return the acceptor
-	 */
-	auto get_acceptor() noexcept -> acceptor&;
-
-	/**
-	 * \copydoc acceptor::accept
-	 */
-
-	void accept(handler handler) override;
-};
-
-template <typename Protocol>
-template <typename Socket, typename Handler>
-void socket_acceptor<Protocol>::do_accept(Socket& socket, Handler handler)
-{
-#if !defined(NDEBUG)
-	assert(!is_accepting_);
-
-	is_accepting_ = true;
-#endif
-
-	acceptor_.async_accept(socket, [this, handler] (auto code) {
-#if !defined(NDEBUG)
-		is_accepting_ = false;
-#endif
-		(void)this;
-		handler(code);
-	});
-}
-
-template <typename Protocol>
-socket_acceptor<Protocol>::socket_acceptor(acceptor acceptor) noexcept
-	: acceptor_(std::move(acceptor))
-{
-	assert(acceptor_.is_open());
-}
-
-template <typename Protocol>
-auto socket_acceptor<Protocol>::get_acceptor() const noexcept -> const acceptor&
-{
-	return acceptor_;
-}
-
-template <typename Protocol>
-auto socket_acceptor<Protocol>::get_acceptor() noexcept -> acceptor&
-{
-	return acceptor_;
-}
-
-template <typename Protocol>
-void socket_acceptor<Protocol>::accept(handler handler)
-{
-	assert(handler);
-
-	const auto client = std::make_shared<socket_stream<socket>>(acceptor_.get_io_service());
-
-	do_accept(client->get_socket(), [client, handler] (auto code) {
-		handler(std::move(code), code ? nullptr : std::move(client));
-	});
-}
-
-/**
- * Convenient TCP/IP acceptor type.
- */
-using ip_acceptor = socket_acceptor<boost::asio::ip::tcp>;
-
-#if !BOOST_OS_WINDOWS
-
-/**
- * Convenient Unix acceptor type.
- */
-using local_acceptor = socket_acceptor<boost::asio::local::stream_protocol>;
-
-#endif
-
-} // !irccd
-
-#endif // !IRCCD_SOCKET_ACCEPTOR_HPP
--- a/libirccd-core/irccd/socket_connector.hpp	Thu Nov 01 10:34:21 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,184 +0,0 @@
-/*
- * socket_connector.hpp -- socket connection interface
- *
- * Copyright (c) 2013-2018 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef IRCCD_SOCKET_CONNECTOR_HPP
-#define IRCCD_SOCKET_CONNECTOR_HPP
-
-/**
- * \file socket_connector.hpp
- * \brief Socket connection interface.
- */
-
-#include <irccd/sysconfig.hpp>
-
-#include <vector>
-
-#include "connector.hpp"
-#include "socket_stream.hpp"
-
-namespace irccd {
-
-/**
- * \brief Socket connection interface.
- * \tparam Protocol a Boost.Asio compatible protocol (e.g. ip::tcp)
- */
-template <typename Protocol>
-class socket_connector : public connector {
-public:
-	/**
-	 * Convenient endpoint alias.
-	 */
-	using endpoint = typename Protocol::endpoint;
-
-	/**
-	 * Convenient socket alias.
-	 */
-	using socket = typename Protocol::socket;
-
-private:
-	boost::asio::io_service& service_;
-	std::vector<endpoint> endpoints_;
-
-#if !defined(NDEBUG)
-	bool is_connecting_{false};
-#endif
-
-protected:
-	/**
-	 * Start trying to connect to all endpoints.
-	 *
-	 * \param socket the underlying socket
-	 * \param handler handler with `void f(std::error_code)` signature
-	 */
-	template <typename Socket, typename Handler>
-	void do_connect(Socket& socket, Handler handler);
-
-public:
-	/**
-	 * Construct the socket connector with only one endpoint.
-	 *
-	 * \param service the service
-	 * \param endpoint the unique endpoint
-	 */
-	socket_connector(boost::asio::io_service& service, endpoint endpoint) noexcept;
-
-	/**
-	 * Construct the socket connection.
-	 *
-	 * \param service the service
-	 * \param eps the endpoints
-	 */
-	socket_connector(boost::asio::io_service& service, std::vector<endpoint> eps) noexcept;
-
-	/**
-	 * Get the underlying I/O service.
-	 *
-	 * \return the I/O service
-	 */
-	auto get_io_service() const noexcept -> const boost::asio::io_service&;
-
-	/**
-	 * Overloaded function.
-	 *
-	 * \return the I/O service
-	 */
-	auto get_io_service() noexcept -> boost::asio::io_service&;
-
-	/**
-	 * \copydoc connector::connect
-	 */
-	void connect(handler handler);
-};
-
-template <typename Protocol>
-template <typename Socket, typename Handler>
-void socket_connector<Protocol>::do_connect(Socket& socket, Handler handler)
-{
-#if !defined(NDEBUG)
-	assert(!is_connecting_);
-
-	is_connecting_ = true;
-#endif
-
-	boost::asio::async_connect(socket, endpoints_.begin(), endpoints_.end(), [this, handler] (auto code, auto ep) {
-#if !defined(NDEBUG)
-		is_connecting_ = false;
-#endif
-
-		if (ep == endpoints_.end())
-			handler(make_error_code(std::errc::host_unreachable));
-		else
-			handler(code);
-	});
-}
-
-template <typename Protocol>
-socket_connector<Protocol>::socket_connector(boost::asio::io_service& service, endpoint endpoint) noexcept
-	: service_(service)
-	, endpoints_{std::move(endpoint)}
-{
-}
-
-template <typename Protocol>
-socket_connector<Protocol>::socket_connector(boost::asio::io_service& service, std::vector<endpoint> eps) noexcept
-	: service_(service)
-	, endpoints_(std::move(eps))
-{
-}
-
-template <typename Protocol>
-auto socket_connector<Protocol>::get_io_service() const noexcept -> const boost::asio::io_service&
-{
-	return service_;
-}
-
-template <typename Protocol>
-auto socket_connector<Protocol>::get_io_service() noexcept -> boost::asio::io_service&
-{
-	return service_;
-}
-
-template <typename Protocol>
-void socket_connector<Protocol>::connect(handler handler)
-{
-	assert(handler);
-
-	const auto stream = std::make_shared<socket_stream<socket>>(service_);
-
-	do_connect(stream->get_socket(), [handler, stream] (auto code) {
-		handler(code, code ? nullptr : std::move(stream));
-	});
-}
-
-/**
- * Convenient TCP/IP connector type.
- */
-using ip_connector = socket_connector<boost::asio::ip::tcp>;
-
-#if !BOOST_OS_WINDOWS
-
-/**
- * Convenient Unix conncetor type.
- */
-using local_connector = socket_connector<boost::asio::local::stream_protocol>;
-
-#endif
-
-} // !irccd
-
-#endif // !IRCCD_SOCKET_CONNECTOR_HPP
--- a/libirccd-core/irccd/socket_stream.hpp	Thu Nov 01 10:34:21 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,229 +0,0 @@
-/*
- * socket_stream.hpp -- socket stream interface
- *
- * Copyright (c) 2013-2018 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef IRCCD_SOCKET_STREAM_HPP
-#define IRCCD_SOCKET_STREAM_HPP
-
-/**
- * \file socket_stream.hpp
- * \brief Socket stream interface.
- */
-
-#include <irccd/sysconfig.hpp>
-
-#include <cstddef>
-#include <cassert>
-#include <string>
-#include <utility>
-
-#include <boost/asio.hpp>
-#include <boost/predef/os.h>
-
-#include "stream.hpp"
-
-namespace irccd {
-
-/**
- * \brief Socket implementation interface.
- * \tparam Socket the Boost.Asio compatible socket.
- *
- * This class reimplements stream for Boost.Asio sockets.
- */
-template <typename Socket>
-class socket_stream : public stream {
-private:
-	Socket socket_;
-	boost::asio::streambuf input_;
-	std::string output_;
-
-#if !defined(NDEBUG)
-	bool is_receiving_{false};
-	bool is_sending_{false};
-#endif
-
-	void handle_read(std::error_code, std::size_t, stream::read_handler);
-	void handle_write(std::error_code, std::size_t, stream::write_handler);
-
-public:
-	/**
-	 * Create the socket stream.
-	 *
-	 * \param args the Socket constructor arguments
-	 */
-	template <typename... Args>
-	socket_stream(Args&&... args);
-
-	/**
-	 * Get the underlying socket.
-	 *
-	 * \return the socket
-	 */
-	auto get_socket() const noexcept -> const Socket&;
-
-	/**
-	 * Overloaded function
-	 *
-	 * \return the socket
-	 */
-	auto get_socket() noexcept -> Socket&;
-
-	/**
-	 * \copydoc stream::read
-	 */
-	void read(read_handler handler) override;
-
-	/**
-	 * \copydoc stream::write
-	 */
-	void write(const nlohmann::json& json, write_handler handler) override;
-};
-
-template <typename Socket>
-void socket_stream<Socket>::handle_read(std::error_code code,
-                                        std::size_t xfer,
-                                        stream::read_handler handler)
-{
-#if !defined(NDEBUG)
-	is_receiving_ = false;
-#endif
-
-	if (xfer == 0U) {
-		handler(make_error_code(std::errc::not_connected), nullptr);
-		return;
-	}
-	if (code) {
-		handler(code, nullptr);
-		return;
-	}
-
-	// 1. Convert the buffer safely.
-	std::string buffer;
-
-	try {
-		buffer = std::string(
-			boost::asio::buffers_begin(input_.data()),
-			boost::asio::buffers_begin(input_.data()) + xfer - /* \r\n\r\n */ 4
-		);
-
-		input_.consume(xfer);
-	} catch (const std::bad_alloc&) {
-		handler(make_error_code(std::errc::not_enough_memory), nullptr);
-		return;
-	}
-
-	// 2. Convert to JSON.
-	nlohmann::json doc;
-
-	try {
-		doc = nlohmann::json::parse(buffer);
-	} catch (const std::exception&) {
-		handler(make_error_code(std::errc::invalid_argument), nullptr);
-		return;
-	}
-
-	if (!doc.is_object())
-		handler(make_error_code(std::errc::invalid_argument), nullptr);
-	else
-		handler(std::error_code(), std::move(doc));
-}
-
-template <typename Socket>
-void socket_stream<Socket>::handle_write(std::error_code code,
-                                         std::size_t xfer,
-                                         stream::write_handler handler)
-{
-#if !defined(NDEBUG)
-	is_sending_ = false;
-#endif
-
-	if (xfer == 0)
-		handler(make_error_code(std::errc::not_connected));
-	else
-		handler(code);
-}
-
-template <typename Socket>
-template <typename... Args>
-socket_stream<Socket>::socket_stream(Args&&... args)
-	: socket_(std::forward<Args>(args)...)
-{
-}
-
-template <typename Socket>
-auto socket_stream<Socket>::get_socket() const noexcept -> const Socket&
-{
-	return socket_;
-}
-
-template <typename Socket>
-auto socket_stream<Socket>::get_socket() noexcept -> Socket&
-{
-	return socket_;
-}
-
-template <typename Socket>
-void socket_stream<Socket>::read(stream::read_handler handler)
-{
-#if !defined(NDEBUG)
-	assert(!is_receiving_);
-	assert(handler);
-
-	is_receiving_ = true;
-#endif
-
-	boost::asio::async_read_until(get_socket(), input_, "\r\n\r\n", [this, handler] (auto code, auto xfer) {
-		handle_read(code, xfer, std::move(handler));
-	});
-}
-
-template <typename Socket>
-void socket_stream<Socket>::write(const nlohmann::json& json, stream::write_handler handler)
-{
-#if !defined(NDEBUG)
-	assert(!is_sending_);
-	assert(handler);
-
-	is_sending_ = true;
-#endif
-
-	output_ = json.dump(0) + "\r\n\r\n";
-
-	const auto buffer = boost::asio::buffer(output_.data(), output_.size());
-
-	boost::asio::async_write(get_socket(), buffer, [this, handler] (auto code, auto xfer) {
-		handle_write(code, xfer, std::move(handler));
-	});
-}
-
-/**
- * Convenient TCP/IP stream type.
- */
-using ip_stream = socket_stream<boost::asio::ip::tcp::socket>;
-
-#if !BOOST_OS_WINDOWS
-
-/**
- * Convenient Unix stream type.
- */
-using local_stream = socket_stream<boost::asio::local::stream_protocol::socket>;
-
-#endif
-
-} // !irccd
-
-#endif // !IRCCD_SOCKET_STREAM_HPP
--- a/libirccd-core/irccd/stream.hpp	Thu Nov 01 10:34:21 2018 +0100
+++ b/libirccd-core/irccd/stream.hpp	Sun Nov 04 17:26:05 2018 +0100
@@ -24,8 +24,21 @@
  * \brief Abstract stream interface.
  */
 
+#include <irccd/sysconfig.hpp>
+
+#include <cassert>
+#include <cstddef>
 #include <functional>
+#include <ostream>
+#include <string>
 #include <system_error>
+#include <utility>
+
+#include <boost/asio.hpp>
+
+#if defined(IRCCD_HAVE_SSL)
+#	include <boost/asio/ssl.hpp>
+#endif
 
 #include "json.hpp"
 
@@ -37,19 +50,19 @@
  * Abstract I/O interface that allows reading/writing from a stream in an
  * asynchronous manner.
  *
- * The derived classes must implement non-blocking read and write operations.
+ * The derived classes must implement non-blocking recv and send operations.
  */
 class stream {
 public:
 	/**
 	 * \brief Read completion handler.
 	 */
-	using read_handler = std::function<void (std::error_code, nlohmann::json)>;
+	using recv_handler = std::function<void (std::error_code, nlohmann::json)>;
 
 	/**
 	 * \brief Write completion handler.
 	 */
-	using write_handler = std::function<void (std::error_code)>;
+	using send_handler = std::function<void (std::error_code)>;
 
 	/**
 	 * Default constructor.
@@ -68,7 +81,7 @@
 	 * \pre handler != nullptr
 	 * \param handler the handler
 	 */
-	virtual void read(read_handler handler) = 0;
+	virtual void recv(recv_handler handler) = 0;
 
 	/**
 	 * Start asynchronous write.
@@ -79,9 +92,342 @@
 	 * \param json the JSON message
 	 * \param handler the handler
 	 */
-	virtual void write(const nlohmann::json& json, write_handler handler) = 0;
+	virtual void send(const nlohmann::json& json, send_handler handler) = 0;
+};
+
+// {{{ socket_stream_base
+
+/**
+ * \brief Abstract base interface for Boost.Asio sockets.
+ *
+ * This class provides convenient functions for underlying sockets.
+ *
+ * \see basic_socket_stream
+ */
+class socket_stream_base : public stream {
+private:
+	boost::asio::streambuf input_{2048};
+	boost::asio::streambuf output_;
+
+#if !defined(NDEBUG)
+	bool is_receiving_{false};
+	bool is_sending_{false};
+#endif
+
+	void handle_recv(boost::system::error_code, std::size_t, recv_handler);
+	void handle_send(boost::system::error_code, std::size_t, send_handler);
+
+protected:
+	/**
+	 * Convenient function for receiving for the underlying socket.
+	 *
+	 * \param sc the socket
+	 * \param handler the handler
+	 */
+	template <typename Socket>
+	void recv(Socket& sc, recv_handler handler);
+
+	/**
+	 * Convenient function for sending for the underlying socket.
+	 *
+	 * \param json the JSON object
+	 * \param sc the socket
+	 * \param handler the handler
+	 */
+	template <typename Socket>
+	void send(const nlohmann::json& json, Socket& sc, send_handler handler);
 };
 
+inline void socket_stream_base::handle_recv(boost::system::error_code code,
+                                            std::size_t xfer,
+                                            recv_handler handler)
+{
+#if !defined(NDEBUG)
+	is_receiving_ = false;
+#endif
+
+	if (code == boost::asio::error::not_found) {
+		handler(make_error_code(std::errc::argument_list_too_long), nullptr);
+		return;
+	}
+	if (code == boost::asio::error::eof || xfer == 0) {
+		handler(make_error_code(std::errc::connection_reset), nullptr);
+		return;
+	}
+	if (code) {
+		handler(std::move(code), nullptr);
+		return;
+	}
+
+	// 1. Convert the buffer safely.
+	std::string buffer;
+
+	try {
+		buffer = std::string(
+			boost::asio::buffers_begin(input_.data()),
+			boost::asio::buffers_begin(input_.data()) + xfer - /* \r\n\r\n */ 4
+		);
+
+		input_.consume(xfer);
+	} catch (const std::bad_alloc&) {
+		handler(make_error_code(std::errc::not_enough_memory), nullptr);
+		return;
+	}
+
+	// 2. Convert to JSON.
+	nlohmann::json doc;
+
+	try {
+		doc = nlohmann::json::parse(buffer);
+	} catch (const std::exception&) {
+		handler(make_error_code(std::errc::invalid_argument), nullptr);
+		return;
+	}
+
+	if (!doc.is_object())
+		handler(make_error_code(std::errc::invalid_argument), nullptr);
+	else
+		handler(std::error_code(), std::move(doc));
+}
+
+inline void socket_stream_base::handle_send(boost::system::error_code code,
+                                            std::size_t xfer,
+                                            send_handler handler)
+{
+#if !defined(NDEBUG)
+	is_sending_ = false;
+#endif
+
+	if (code == boost::asio::error::eof || xfer == 0) {
+		handler(make_error_code(std::errc::connection_reset));
+		return;
+	}
+	else
+		handler(std::move(code));
+}
+
+template <typename Socket>
+inline void socket_stream_base::recv(Socket& sc, recv_handler handler)
+{
+#if !defined(NDEBUG)
+	assert(!is_receiving_);
+
+	is_receiving_ = true;
+#endif
+
+	assert(handler);
+
+	async_read_until(sc, input_, "\r\n\r\n", [this, handler] (auto code, auto xfer) {
+		handle_recv(code, xfer, handler);
+	});
+}
+
+template <typename Socket>
+inline void socket_stream_base::send(const nlohmann::json& json, Socket& sc, send_handler handler)
+{
+#if !defined(NDEBUG)
+	assert(!is_sending_);
+
+	is_sending_ = true;
+#endif
+
+	assert(json.is_object());
+	assert(handler);
+
+	std::ostream out(&output_);
+
+	out << json.dump(0);
+	out << "\r\n\r\n";
+	out << std::flush;
+
+	async_write(sc, output_, [this, handler] (auto code, auto xfer) {
+		handle_send(code, xfer, handler);
+	});
+}
+
+// }}}
+
+// {{{ basic_socket_stream
+
+/**
+ * \brief Complete implementation for basic sockets
+ * \tparam Socket Boost.Asio socket (e.g. boost::asio::ip::tcp::socket)
+ */
+template <typename Socket>
+class basic_socket_stream : public socket_stream_base {
+private:
+	Socket socket_;
+
+public:
+	/**
+	 * Construct a socket stream.
+	 *
+	 * \param service the I/O service
+	 */
+	basic_socket_stream(boost::asio::io_context& service);
+
+	/**
+	 * Get the underlying socket.
+	 *
+	 * \return the socket
+	 */
+	auto get_socket() const noexcept -> const Socket&;
+
+	/**
+	 * Overloaded function.
+	 *
+	 * \return the socket
+	 */
+	auto get_socket() noexcept -> Socket&;
+
+	/**
+	 * \copydoc stream::recv
+	 */
+	void recv(recv_handler handler) override;
+
+	/**
+	 * \copydoc stream::send
+	 */
+	void send(const nlohmann::json& json, send_handler handler) override;
+};
+
+template <typename Socket>
+inline basic_socket_stream<Socket>::basic_socket_stream(boost::asio::io_context& ctx)
+	: socket_(ctx)
+{
+}
+
+template <typename Socket>
+inline auto basic_socket_stream<Socket>::get_socket() const noexcept -> const Socket&
+{
+	return socket_;
+}
+
+template <typename Socket>
+inline auto basic_socket_stream<Socket>::get_socket() noexcept -> Socket&
+{
+	return socket_;
+}
+
+template <typename Socket>
+inline void basic_socket_stream<Socket>::recv(recv_handler handler)
+{
+	socket_stream_base::recv(socket_, handler);
+}
+
+template <typename Socket>
+inline void basic_socket_stream<Socket>::send(const nlohmann::json& json, send_handler handler)
+{
+	socket_stream_base::send(json, socket_, handler);
+}
+
+// }}}
+
+// {{{ ip_stream
+
+/**
+ * \brief Convenient alias for boost::asio::ip::tcp::socket
+ */
+using ip_stream = basic_socket_stream<boost::asio::ip::tcp::socket>;
+
+// }}}
+
+// {{{ local_stream
+
+#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS)
+
+/**
+ * \brief Convenient alias for boost::asio::local::stream_protocol::socket
+ */
+using local_stream = basic_socket_stream<boost::asio::local::stream_protocol::socket>;
+
+#endif // !BOOST_ASIO_HAS_LOCAL_SOCKETS
+
+// }}}
+
+// {{{ tls_stream
+
+#if defined(IRCCD_HAVE_SSL)
+
+/**
+ * \brief TLS/SSL streams.
+ * \tparam Socket the Boost.Asio compatible socket.
+ */
+template <typename Socket>
+class tls_stream : public socket_stream_base {
+private:
+	boost::asio::ssl::stream<Socket> socket_;
+	std::shared_ptr<boost::asio::ssl::context> context_;
+
+public:
+	/**
+	 * Constructor.
+	 *
+	 * \param service the I/O service
+	 * \param ctx the shared context
+	 */
+	tls_stream(boost::asio::io_context& service, std::shared_ptr<boost::asio::ssl::context> ctx);
+
+	/**
+	 * Get the SSL socket.
+	 *
+	 * \return the socket
+	 */
+	auto get_socket() const noexcept -> const boost::asio::ssl::stream<Socket>&;
+
+	/**
+	 * Overloaded function.
+	 *
+	 * \return the socket
+	 */
+	auto get_socket() noexcept -> boost::asio::ssl::stream<Socket>&;
+
+	/**
+	 * \copydoc stream::recv
+	 */
+	void recv(recv_handler handler) override;
+
+	/**
+	 * \copydoc stream::send
+	 */
+	void send(const nlohmann::json& json, send_handler handler) override;
+};
+
+template <typename Socket>
+inline tls_stream<Socket>::tls_stream(boost::asio::io_context& service, std::shared_ptr<boost::asio::ssl::context> ctx)
+	: socket_(service, *ctx)
+	, context_(std::move(ctx))
+{
+}
+
+template <typename Socket>
+inline auto tls_stream<Socket>::get_socket() const noexcept -> const boost::asio::ssl::stream<Socket>&
+{
+	return socket_;
+}
+
+template <typename Socket>
+inline auto tls_stream<Socket>::get_socket() noexcept -> boost::asio::ssl::stream<Socket>&
+{
+	return socket_;
+}
+
+template <typename Socket>
+inline void tls_stream<Socket>::recv(recv_handler handler)
+{
+	socket_stream_base::recv(socket_, handler);
+}
+
+template <typename Socket>
+inline void tls_stream<Socket>::send(const nlohmann::json& json, send_handler handler)
+{
+	socket_stream_base::send(json, socket_, handler);
+}
+
+#endif // !IRCCD_HAVE_SSL
+
+// }}}
+
 } // !irccd
 
 #endif // !IRCCD_STREAM_HPP
--- a/libirccd-core/irccd/tls_acceptor.hpp	Thu Nov 01 10:34:21 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,96 +0,0 @@
-/*
- * tls_acceptor.hpp -- TLS/SSL acceptors
- *
- * Copyright (c) 2013-2018 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef IRCCD_TLS_ACCEPTOR_HPP
-#define IRCCD_TLS_ACCEPTOR_HPP
-
-/**
- * \file tls_acceptor.hpp
- * \brief TLS/SSL acceptors.
- */
-
-#include <irccd/sysconfig.hpp>
-
-#if defined(IRCCD_HAVE_SSL)
-
-#include "socket_acceptor.hpp"
-#include "tls_stream.hpp"
-
-namespace irccd {
-
-/**
- * \brief TLS/SSL acceptors.
- * \tparam Protocol a Boost.Asio compatible protocol (e.g. ip::tcp)
- */
-template <typename Protocol = boost::asio::ip::tcp>
-class tls_acceptor : public socket_acceptor<Protocol> {
-private:
-	using socket = typename Protocol::socket;
-
-	boost::asio::ssl::context context_;
-
-public:
-	/**
-	 * Construct a secure layer transport server.
-	 *
-	 * \param context the SSL context
-	 * \param args the socket_acceptor arguments
-	 */
-	template <typename... Args>
-	tls_acceptor(boost::asio::ssl::context context, Args&&... args);
-
-	/**
-	 * \copydoc acceptor::accept
-	 */
-	void accept(acceptor::handler handler) override;
-};
-
-template <typename Protocol>
-void tls_acceptor<Protocol>::accept(acceptor::handler handler)
-{
-	assert(handler);
-
-	auto client = std::make_shared<tls_stream<socket>>(this->get_acceptor().get_io_service(), this->context_);
-
-	socket_acceptor<Protocol>::do_accept(client->get_socket().lowest_layer(), [handler, client] (auto code) {
-		using boost::asio::ssl::stream_base;
-
-		if (code) {
-			handler(code, nullptr);
-			return;
-		}
-
-		client->get_socket().async_handshake(stream_base::server, [handler, client] (auto code) {
-			handler(code, code ? nullptr : std::move(client));
-		});
-	});
-}
-
-template <typename Protocol>
-template <typename... Args>
-tls_acceptor<Protocol>::tls_acceptor(boost::asio::ssl::context context, Args&&... args)
-	: socket_acceptor<Protocol>(std::forward<Args>(args)...)
-	, context_(std::move(context))
-{
-}
-
-} // !irccd
-
-#endif // !IRCCD_HAVE_SSL
-
-#endif // !IRCCD_TLS_ACCEPTOR_HPP
--- a/libirccd-core/irccd/tls_connector.hpp	Thu Nov 01 10:34:21 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,95 +0,0 @@
-/*
- * tls_connector.hpp -- TLS/SSL connectors
- *
- * Copyright (c) 2013-2018 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef IRCCD_TLS_CONNECTOR_HPP
-#define IRCCD_TLS_CONNECTOR_HPP
-
-/**
- * \file tls_connector.hpp
- * \brief TLS/SSL connectors.
- */
-
-#include <irccd/sysconfig.hpp>
-
-#if defined(IRCCD_HAVE_SSL)
-
-#include "socket_connector.hpp"
-#include "tls_stream.hpp"
-
-namespace irccd {
-
-/**
- * \brief TLS/SSL connectors.
- * \tparam Protocol a Boost.Asio compatible protocol (e.g. ip::tcp)
- */
-template <typename Protocol = boost::asio::ip::tcp>
-class tls_connector : public socket_connector<Protocol> {
-private:
-	boost::asio::ssl::context context_;
-
-public:
-	/**
-	 * Construct a secure layer transport server.
-	 *
-	 * \param context the SSL context
-	 * \param args the arguments to socket_connector<Socket> constructor
-	 */
-	template <typename... Args>
-	tls_connector(boost::asio::ssl::context context, Args&&... args);
-
-	/**
-	 * \copydoc socket_connector::connect
-	 */
-	void connect(connector::handler handler) override;
-};
-
-template <typename Protocol>
-template <typename... Args>
-tls_connector<Protocol>::tls_connector(boost::asio::ssl::context context, Args&&... args)
-	: socket_connector<Protocol>(std::forward<Args>(args)...)
-	, context_(std::move(context))
-{
-}
-
-template <typename Protocol>
-void tls_connector<Protocol>::connect(connector::handler handler)
-{
-	using boost::asio::ssl::stream_base;
-	using socket = typename Protocol::socket;
-
-	assert(handler);
-
-	const auto stream = std::make_shared<tls_stream<socket>>(this->get_io_service(), context_);
-
-	socket_connector<Protocol>::do_connect(stream->get_socket().lowest_layer(), [handler, stream] (auto code) {
-		if (code) {
-			handler(code, nullptr);
-			return;
-		}
-
-		stream->get_socket().async_handshake(stream_base::client, [handler, stream] (auto code) {
-			handler(code, code ? nullptr : std::move(stream));
-		});
-	});
-}
-
-} // !irccd
-
-#endif // !IRCCD_HAVE_SSL
-
-#endif // !IRCCD_TLS_CONNECTOR_HPP
--- a/libirccd-core/irccd/tls_stream.hpp	Thu Nov 01 10:34:21 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,60 +0,0 @@
-/*
- * tls_stream.hpp -- TLS/SSL streams
- *
- * Copyright (c) 2013-2018 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef IRCCD_TLS_STREAM_HPP
-#define IRCCD_TLS_STREAM_HPP
-
-/**
- * \file tls_stream.hpp
- * \brief TLS/SSL streams.
- */
-
-#include <irccd/sysconfig.hpp>
-
-#if defined(IRCCD_HAVE_SSL)
-
-#include <boost/asio/ssl.hpp>
-
-#include "socket_stream.hpp"
-
-namespace irccd {
-
-/**
- * \brief TLS/SSL streams.
- * \tparam Socket the Boost.Asio compatible socket.
- */
-template <typename Socket = boost::asio::ip::tcp::socket>
-class tls_stream : public socket_stream<boost::asio::ssl::stream<Socket>> {
-public:
-	/**
-	 * Constructor.
-	 *
-	 * \param args the arguments to boost::asio::ssl::stream<Socket>
-	 */
-	template <typename... Args>
-	tls_stream(Args&&... args)
-		: socket_stream<boost::asio::ssl::stream<Socket>>(std::forward<Args>(args)...)
-	{
-	}
-};
-
-} // !irccd
-
-#endif // !IRCCD_HAVE_SSL
-
-#endif // !IRCCD_TLS_STREAM_HPP
--- a/libirccd-ctl/irccd/ctl/controller.cpp	Thu Nov 01 10:34:21 2018 +0100
+++ b/libirccd-ctl/irccd/ctl/controller.cpp	Sun Nov 04 17:26:05 2018 +0100
@@ -106,14 +106,14 @@
 	});
 }
 
-void controller::read(stream::read_handler handler)
+void controller::read(stream::recv_handler handler)
 {
 	assert(handler);
 	assert(stream_);
 
 	auto stream = stream_;
 
-	stream_->read([this, handler, stream] (auto code, auto msg) {
+	stream_->recv([this, handler, stream] (auto code, auto msg) {
 		if (code) {
 			stream_ = nullptr;
 			handler(std::move(code), std::move(msg));
@@ -139,14 +139,14 @@
 	});
 }
 
-void controller::write(nlohmann::json message, stream::write_handler handler)
+void controller::write(nlohmann::json message, stream::send_handler handler)
 {
 	assert(message.is_object());
 	assert(stream_);
 
 	auto stream = stream_;
 
-	stream_->write(std::move(message), [this, stream, handler] (auto code) {
+	stream_->send(std::move(message), [this, stream, handler] (auto code) {
 		if (code)
 			stream_ = nullptr;
 		if (handler)
--- a/libirccd-ctl/irccd/ctl/controller.hpp	Thu Nov 01 10:34:21 2018 +0100
+++ b/libirccd-ctl/irccd/ctl/controller.hpp	Sun Nov 04 17:26:05 2018 +0100
@@ -104,7 +104,7 @@
 	 * \pre handler != nullptr
 	 * \param handler the recv handler
 	 */
-	void read(stream::read_handler handler);
+	void read(stream::recv_handler handler);
 
 	/**
 	 * Queue a send operation, if receive operations are already running, it
@@ -114,7 +114,7 @@
 	 * \param message the JSON message
 	 * \param handler the optional completion handler
 	 */
-	void write(nlohmann::json message, stream::write_handler handler = nullptr);
+	void write(nlohmann::json message, stream::send_handler handler = nullptr);
 };
 
 } // !irccd::ctl
--- a/libirccd-test/irccd/test/cli_fixture.cpp	Thu Nov 01 10:34:21 2018 +0100
+++ b/libirccd-test/irccd/test/cli_fixture.cpp	Sun Nov 04 17:26:05 2018 +0100
@@ -22,7 +22,7 @@
 #include <boost/process.hpp>
 
 #include <irccd/string_util.hpp>
-#include <irccd/socket_acceptor.hpp>
+#include <irccd/acceptor.hpp>
 
 #include <irccd/daemon/command.hpp>
 #include <irccd/daemon/transport_service.hpp>
@@ -37,17 +37,13 @@
 cli_fixture::cli_fixture()
 	: server_(new mock_server(irccd_.get_service(), "test", "localhost"))
 {
-	std::remove(CMAKE_BINARY_DIR "/tmp/irccd.sock");
-
-	local_acceptor::endpoint endpoint(CMAKE_BINARY_DIR "/tmp/irccd.sock");
-	local_acceptor::acceptor acceptor(service_, std::move(endpoint));
+	auto acceptor = std::make_unique<local_acceptor>(irccd_.get_service(), CMAKE_BINARY_DIR "/tmp/irccd.sock");
 
 	for (const auto& f : command::registry)
 		irccd_.transports().get_commands().push_back(f());
 
 	irccd_.servers().add(server_);
-	irccd_.transports().add(std::make_unique<transport_server>(
-		std::make_unique<local_acceptor>(std::move(acceptor))));
+	irccd_.transports().add(std::make_unique<transport_server>(std::move(acceptor)));
 	server_->disconnect();
 	server_->clear();
 }
--- a/libirccd-test/irccd/test/command_fixture.cpp	Thu Nov 01 10:34:21 2018 +0100
+++ b/libirccd-test/irccd/test/command_fixture.cpp	Sun Nov 04 17:26:05 2018 +0100
@@ -16,8 +16,8 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-#include <irccd/socket_acceptor.hpp>
-#include <irccd/socket_connector.hpp>
+#include <irccd/acceptor.hpp>
+#include <irccd/connector.hpp>
 
 #include <irccd/daemon/command.hpp>
 #include <irccd/daemon/transport_server.hpp>
@@ -56,23 +56,20 @@
 	: server_(new mock_server(ctx_, "test", "localhost"))
 	, plugin_(new mock_plugin("test"))
 {
-	using boost::asio::ip::tcp;
+	const auto path = CMAKE_BINARY_DIR "/tmp/irccd.sock";
+
+	auto acceptor = std::make_unique<local_acceptor>(irccd_.get_service(), path);
+	auto connector = std::make_unique<local_connector>(irccd_.get_service(), path);
 
 	// 1. Add all commands.
 	for (const auto& f : command::registry)
 		irccd_.transports().get_commands().push_back(f());
 
-	// 2. Bind to a random port.
-	tcp::endpoint ep(tcp::v4(), 0);
-	tcp::acceptor acc(ctx_, ep);
+	// 2. Create controller and transport server.
+	ctl_ = std::make_unique<ctl::controller>(std::move(connector));
+	irccd_.transports().add(std::make_unique<transport_server>(std::move(acceptor)));
 
-	// 3. Create controller and transport server.
-	ctl_ = std::make_unique<ctl::controller>(
-		std::make_unique<ip_connector>(ctx_, acc.local_endpoint()));
-	irccd_.transports().add(std::make_unique<transport_server>(
-		std::make_unique<ip_acceptor>(std::move(acc))));
-
-	// Wait for controller to connect.
+	// 3. Wait for controller to connect.
 	boost::asio::deadline_timer timer(ctx_);
 
 	timer.expires_from_now(boost::posix_time::seconds(10));
--- a/libirccd/irccd/daemon/server.cpp	Thu Nov 01 10:34:21 2018 +0100
+++ b/libirccd/irccd/daemon/server.cpp	Sun Nov 04 17:26:05 2018 +0100
@@ -561,7 +561,7 @@
 void server::set_options(options flags) noexcept
 {
 #if !defined(IRCCD_HAVE_SSL)
-	assert(!(flags & ssl));
+	assert((flags & options::ssl) != options::ssl);
 #endif
 
 	flags_ = flags;
--- a/libirccd/irccd/daemon/transport_client.cpp	Thu Nov 01 10:34:21 2018 +0100
+++ b/libirccd/irccd/daemon/transport_client.cpp	Sun Nov 04 17:26:05 2018 +0100
@@ -30,7 +30,7 @@
 
 	const auto self = shared_from_this();
 
-	stream_->write(queue_.front().first, [this, self] (auto code) {
+	stream_->send(queue_.front().first, [this, self] (auto code) {
 		if (queue_.front().second)
 			queue_.front().second(code);
 
@@ -69,14 +69,14 @@
 	state_ = state;
 }
 
-void transport_client::read(stream::read_handler handler)
+void transport_client::read(stream::recv_handler handler)
 {
 	assert(handler);
 
 	if (state_ != state::closing) {
 		const auto self = shared_from_this();
 
-		stream_->read([this, self, handler] (auto code, auto msg) {
+		stream_->recv([this, self, handler] (auto code, auto msg) {
 			handler(code, msg);
 
 			if (code)
@@ -85,7 +85,7 @@
 	}
 }
 
-void transport_client::write(nlohmann::json json, stream::write_handler handler)
+void transport_client::write(nlohmann::json json, stream::send_handler handler)
 {
 	const auto in_progress = queue_.size() > 0;
 
@@ -95,19 +95,19 @@
 		flush();
 }
 
-void transport_client::success(const std::string& cname, stream::write_handler handler)
+void transport_client::success(const std::string& cname, stream::send_handler handler)
 {
 	assert(!cname.empty());
 
 	write({{ "command", cname }}, std::move(handler));
 }
 
-void transport_client::error(std::error_code code, stream::write_handler handler)
+void transport_client::error(std::error_code code, stream::send_handler handler)
 {
 	error(std::move(code), "", std::move(handler));
 }
 
-void transport_client::error(std::error_code code, std::string_view cname, stream::write_handler handler)
+void transport_client::error(std::error_code code, std::string_view cname, stream::send_handler handler)
 {
 	assert(code);
 
--- a/libirccd/irccd/daemon/transport_client.hpp	Thu Nov 01 10:34:21 2018 +0100
+++ b/libirccd/irccd/daemon/transport_client.hpp	Sun Nov 04 17:26:05 2018 +0100
@@ -49,7 +49,7 @@
 	state state_{state::authenticating};
 	std::weak_ptr<transport_server> parent_;
 	std::shared_ptr<stream> stream_;
-	std::deque<std::pair<nlohmann::json, stream::write_handler>> queue_;
+	std::deque<std::pair<nlohmann::json, stream::send_handler>> queue_;
 
 	void flush();
 	void erase();
@@ -92,7 +92,7 @@
 	 * \param handler the handler
 	 * \warning Another read operation MUST NOT be running.
 	 */
-	void read(stream::read_handler handler);
+	void read(stream::recv_handler handler);
 
 	/**
 	 * Start sending if not closed.
@@ -106,7 +106,7 @@
 	 * \param handler the optional handler
 	 * \note If a write operation is running, it is postponed once ready.
 	 */
-	void write(nlohmann::json json, stream::write_handler handler = nullptr);
+	void write(nlohmann::json json, stream::send_handler handler = nullptr);
 
 	/**
 	 * Convenient success message.
@@ -115,7 +115,7 @@
 	 * \param handler the optional handler
 	 * \note If a write operation is running, it is postponed once ready.
 	 */
-	void success(const std::string& command, stream::write_handler handler = nullptr);
+	void success(const std::string& command, stream::send_handler handler = nullptr);
 
 	/**
 	 * Send an error code to the client.
@@ -125,7 +125,7 @@
 	 * \param handler the optional handler
 	 * \note If a write operation is running, it is postponed once ready.
 	 */
-	void error(std::error_code code, stream::write_handler handler = nullptr);
+	void error(std::error_code code, stream::send_handler handler = nullptr);
 
 	/**
 	 * Send an error code to the client.
@@ -136,7 +136,7 @@
 	 * \param handler the optional handler
 	 * \note If a write operation is running, it is postponed once ready.
 	 */
-	void error(std::error_code code, std::string_view command, stream::write_handler handler = nullptr);
+	void error(std::error_code code, std::string_view command, stream::send_handler handler = nullptr);
 };
 
 } // !irccd
--- a/libirccd/irccd/daemon/transport_util.cpp	Thu Nov 01 10:34:21 2018 +0100
+++ b/libirccd/irccd/daemon/transport_util.cpp	Sun Nov 04 17:26:05 2018 +0100
@@ -20,15 +20,9 @@
 
 #include <cassert>
 
-#include <boost/predef/os.h>
-
+#include <irccd/acceptor.hpp>
 #include <irccd/ini_util.hpp>
 #include <irccd/string_util.hpp>
-#include <irccd/socket_acceptor.hpp>
-
-#if defined(IRCCD_HAVE_SSL)
-#	include <irccd/tls_acceptor.hpp>
-#endif
 
 #include "transport_util.hpp"
 #include "transport_server.hpp"
@@ -39,7 +33,7 @@
 
 namespace {
 
-auto from_config_load_ip_protocol(const ini::section& sc) -> asio::ip::tcp
+auto from_config_load_ip_protocols(const ini::section& sc) -> std::pair<bool, bool>
 {
 	bool ipv4 = true, ipv6 = false;
 
@@ -66,89 +60,79 @@
 	if (!ipv4 && !ipv6)
 		throw transport_error(transport_error::invalid_family);
 
-	return ipv4 ? asio::ip::tcp::v4() : asio::ip::tcp::v6();
+	return { ipv4, ipv6 };
 }
 
-auto from_config_load_ip_endpoint(const ini::section& sc) -> asio::ip::tcp::endpoint
+#if defined(IRCCD_HAVE_SSL)
+
+auto from_config_load_ssl(const ini::section& sc) -> boost::asio::ssl::context
 {
+	const auto key = sc.get("key").get_value();
+	const auto cert = sc.get("certificate").get_value();
+
+	if (key.empty())
+		throw transport_error(transport_error::invalid_private_key);
+	if (cert.empty())
+		throw transport_error(transport_error::invalid_certificate);
+
+	boost::asio::ssl::context ctx(boost::asio::ssl::context::tlsv12);
+
+	ctx.use_private_key_file(key, boost::asio::ssl::context::pem);
+	ctx.use_certificate_file(cert, boost::asio::ssl::context::pem);
+
+	return ctx;
+}
+
+#endif // !IRCCD_HAVE_SSL
+
+auto from_config_load_ip(asio::io_service& service, const ini::section& sc) -> std::unique_ptr<acceptor>
+{
+	assert(sc.get_key() == "transport");
+
 	const auto port = ini_util::get_uint<std::uint16_t>(sc, "port");
 	const auto address = ini_util::optional_string(sc, "address", "*");
+	const auto [ ipv4, ipv6 ] = from_config_load_ip_protocols(sc);
 
 	if (!port)
 		throw transport_error(transport_error::invalid_port);
 	if (address.empty())
 		throw transport_error(transport_error::invalid_address);
 
-	const auto protocol = from_config_load_ip_protocol(sc);
+	if (string_util::is_boolean(sc.get("ssl").get_value()))
+#if !defined(IRCCD_HAVE_SSL)
+	throw transport_error(transport_error::ssl_disabled);
+#else
+		return std::make_unique<tls_ip_acceptor>(from_config_load_ssl(sc),
+			service, address, *port, ipv4, ipv6);
+#endif // !IRCCD_HAVE_SSL
 
-	return address == "*"
-		? asio::ip::tcp::endpoint(protocol, *port)
-		: asio::ip::tcp::endpoint(asio::ip::address::from_string(address), *port);
+	return std::make_unique<ip_acceptor>(service, address, *port, ipv4, ipv6);
 }
 
-auto from_config_load_ip_acceptor(asio::io_service& service, const ini::section& sc) -> asio::ip::tcp::acceptor
-{
-	return asio::ip::tcp::acceptor(service, from_config_load_ip_endpoint(sc), true);
-}
-
-auto from_config_load_ip(asio::io_service& service, const ini::section& sc) -> std::unique_ptr<transport_server>
+auto from_config_load_local(asio::io_service& service, const ini::section& sc) -> std::unique_ptr<acceptor>
 {
 	assert(sc.get_key() == "transport");
 
-	auto acceptor = from_config_load_ip_acceptor(service, sc);
-
-	if (string_util::is_boolean(sc.get("ssl").get_value())) {
-#if !defined(IRCCD_HAVE_SSL)
-		throw transport_error(transport_error::ssl_disabled);
-#else
-		const auto key = sc.get("key").get_value();
-		const auto cert = sc.get("certificate").get_value();
-
-		if (key.empty())
-			throw transport_error(transport_error::invalid_private_key);
-		if (cert.empty())
-			throw transport_error(transport_error::invalid_certificate);
-
-		boost::asio::ssl::context ctx(boost::asio::ssl::context::sslv23);
-
-		ctx.use_private_key_file(key, boost::asio::ssl::context::pem);
-		ctx.use_certificate_file(cert, boost::asio::ssl::context::pem);
-
-		return std::make_unique<transport_server>(
-			std::make_unique<tls_acceptor<>>(std::move(ctx), std::move(acceptor)));
-#endif
-	}
-
-	return std::make_unique<transport_server>(
-		std::make_unique<ip_acceptor>(std::move(acceptor)));
-}
-
-auto from_config_load_unix(asio::io_service& service, const ini::section& sc) -> std::unique_ptr<transport_server>
-{
-	assert(sc.get_key() == "transport");
-
-#if !BOOST_OS_WINDOWS
-	using boost::asio::local::stream_protocol;
-
+#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS)
 	const auto path = sc.get("path").get_value();
 
 	if (path.empty())
 		throw transport_error(transport_error::invalid_path);
 
-	// Remove the file first.
-	std::remove(path.c_str());
+	if (string_util::is_boolean(sc.get("ssl").get_value()))
+#if !defined(IRCCD_HAVE_SSL)
+	throw transport_error(transport_error::ssl_disabled);
+#else
+		return std::make_unique<tls_local_acceptor>(from_config_load_ssl(sc), service, path);
+#endif // !IRCCD_HAVE_SSL
 
-	stream_protocol::endpoint endpoint(path);
-	stream_protocol::acceptor acceptor(service, std::move(endpoint));
-
-	return std::make_unique<transport_server>(
-		std::make_unique<local_acceptor>(std::move(acceptor)));
+	return std::make_unique<local_acceptor>(service, path);
 #else
 	(void)service;
 	(void)sc;
 
 	throw transport_error(transport_error::not_supported);
-#endif
+#endif // !BOOST_ASIO_HAS_LOCAL_SOCKETS
 }
 
 } // !namespace
@@ -163,15 +147,17 @@
 	if (type.empty())
 		throw transport_error(transport_error::not_supported);
 
-	std::unique_ptr<transport_server> transport;
+	std::unique_ptr<acceptor> acceptor;
 
 	if (type == "ip")
-		transport = from_config_load_ip(service, sc);
+		acceptor = from_config_load_ip(service, sc);
 	else if (type == "unix")
-		transport = from_config_load_unix(service, sc);
+		acceptor = from_config_load_local(service, sc);
 	else
 		throw transport_error(transport_error::not_supported);
 
+	auto transport = std::make_unique<transport_server>(std::move(acceptor));
+
 	transport->set_password(password);
 
 	return transport;
--- a/tests/src/libcommon/stream/CMakeLists.txt	Thu Nov 01 10:34:21 2018 +0100
+++ b/tests/src/libcommon/stream/CMakeLists.txt	Sun Nov 04 17:26:05 2018 +0100
@@ -17,7 +17,7 @@
 #
 
 irccd_define_test(
-	NAME io
+	NAME stream
 	SOURCES main.cpp
 	LIBRARIES libirccd-core
 )
--- a/tests/src/libcommon/stream/main.cpp	Thu Nov 01 10:34:21 2018 +0100
+++ b/tests/src/libcommon/stream/main.cpp	Sun Nov 04 17:26:05 2018 +0100
@@ -1,5 +1,5 @@
 /*
- * main.cpp -- test io classes
+ * main.cpp -- test network classes
  *
  * Copyright (c) 2013-2018 David Demelier <markand@malikania.fr>
  *
@@ -23,15 +23,9 @@
 
 #include <irccd/sysconfig.hpp>
 
-#include <irccd/socket_acceptor.hpp>
-#include <irccd/socket_connector.hpp>
-#include <irccd/socket_stream.hpp>
-
-#if defined(IRCCD_HAVE_SSL)
-#	include <irccd/tls_acceptor.hpp>
-#	include <irccd/tls_connector.hpp>
-#	include <irccd/tls_stream.hpp>
-#endif // !IRCCD_HAVE_SSL
+#include <irccd/acceptor.hpp>
+#include <irccd/connector.hpp>
+#include <irccd/stream.hpp>
 
 using boost::asio::io_service;
 using boost::asio::ip::tcp;
@@ -40,10 +34,6 @@
 using boost::asio::ssl::context;
 #endif
 
-#if !BOOST_OS_WINDOWS
-using boost::asio::local::stream_protocol;
-#endif
-
 namespace irccd {
 
 namespace {
@@ -100,7 +90,7 @@
 
 		endpoint_ = acceptor.local_endpoint();
 
-		return std::make_unique<ip_acceptor>(std::move(acceptor));
+		return std::make_unique<ip_acceptor>(service_, std::move(acceptor));
 	}
 
 	/**
@@ -108,13 +98,16 @@
 	 */
 	auto create_connector() -> std::unique_ptr<connector> override
 	{
-		return std::make_unique<ip_connector>(service_, endpoint_);
+		const auto hostname = "127.0.0.1";
+		const auto port = std::to_string(endpoint_.port());
+
+		return std::make_unique<ip_connector>(service_, hostname, port, true, false);
 	}
 };
 
 #if defined(IRCCD_HAVE_SSL)
 
-class ssl_stream_fixture : public stream_fixture {
+class tls_ip_stream_fixture : public stream_fixture {
 private:
 	tcp::endpoint endpoint_;
 
@@ -124,7 +117,7 @@
 	 */
 	auto create_acceptor() -> std::unique_ptr<acceptor> override
 	{
-		context context(context::sslv23);
+		context context(context::tlsv12);
 
 		context.use_certificate_file(TESTS_SOURCE_DIR "/data/test.crt", context::pem);
 		context.use_private_key_file(TESTS_SOURCE_DIR "/data/test.key", context::pem);
@@ -134,7 +127,40 @@
 
 		endpoint_ = acceptor.local_endpoint();
 
-		return std::make_unique<tls_acceptor<>>(std::move(context), std::move(acceptor));
+		return std::make_unique<tls_acceptor<ip_acceptor>>(std::move(context), service_, std::move(acceptor));
+	}
+
+	/**
+	 * \copydoc io_fixture::create_connector
+	 */
+	auto create_connector() -> std::unique_ptr<connector> override
+	{
+		context context(context::tlsv12);
+
+		const auto hostname = "127.0.0.1";
+		const auto port = std::to_string(endpoint_.port());
+
+		return std::make_unique<tls_connector<ip_connector>>(std::move(context),
+			service_, hostname, port, true, false);
+	}
+};
+
+#endif // !IRCCD_HAVE_SSL
+
+#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS)
+
+class local_stream_fixture : public stream_fixture {
+private:
+	const std::string path_{CMAKE_BINARY_DIR "/tmp/stream-test.sock"};
+
+public:
+
+	/**
+	 * \copydoc io_fixture::create_acceptor
+	 */
+	auto create_acceptor() -> std::unique_ptr<acceptor> override
+	{
+		return std::make_unique<local_acceptor>(service_, path_);
 	}
 
 	/**
@@ -142,26 +168,29 @@
 	 */
 	auto create_connector() -> std::unique_ptr<connector> override
 	{
-		return std::make_unique<tls_connector<>>(context(context::sslv23), service_, endpoint_);
+		return std::make_unique<local_connector>(service_, path_);
 	}
 };
 
-#endif // !IRCCD_HAVE_SSL
+#if defined(IRCCD_HAVE_SSL)
 
-#if !BOOST_OS_WINDOWS
+class tls_local_stream_fixture : public stream_fixture {
+private:
+	const std::string path_{CMAKE_BINARY_DIR "/tmp/stream-test.sock"};
 
-class local_stream_fixture : public stream_fixture {
 public:
+
 	/**
 	 * \copydoc io_fixture::create_acceptor
 	 */
 	auto create_acceptor() -> std::unique_ptr<acceptor> override
 	{
-		std::remove(CMAKE_BINARY_DIR "/tmp/io-test.sock");
+		context context(context::tlsv12);
 
-		stream_protocol::acceptor acceptor(service_, CMAKE_BINARY_DIR "/tmp/io-test.sock");
+		context.use_certificate_file(TESTS_SOURCE_DIR "/data/test.crt", context::pem);
+		context.use_private_key_file(TESTS_SOURCE_DIR "/data/test.key", context::pem);
 
-		return std::make_unique<local_acceptor>(std::move(acceptor));
+		return std::make_unique<tls_acceptor<local_acceptor>>(std::move(context), service_, path_);
 	}
 
 	/**
@@ -169,11 +198,13 @@
 	 */
 	auto create_connector() -> std::unique_ptr<connector> override
 	{
-		return std::make_unique<local_connector>(service_, CMAKE_BINARY_DIR "/tmp/io-test.sock");
+		return std::make_unique<tls_connector<local_connector>>(context(context::tlsv12), service_, path_);
 	}
 };
 
-#endif // !BOOST_OS_WINDOWS
+#endif // !IRCCD_HAVE_SSL
+
+#endif // !BOOST_ASIO_HAS_LOCAL_SOCKETS
 
 /**
  * List of fixtures to tests.
@@ -181,10 +212,13 @@
 using list = boost::mpl::list<
 	ip_stream_fixture
 #if defined(IRCCD_HAVE_SSL)
-	, ssl_stream_fixture
+	, tls_ip_stream_fixture
 #endif
-#if !BOOST_OS_WINDOWS
+#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS)
 	, local_stream_fixture
+#	if defined(IRCCD_HAVE_SSL)
+	, tls_local_stream_fixture
+#	endif
 #endif
 >;
 
@@ -198,25 +232,25 @@
 	};
 
 	fixture.init();
-	fixture.stream1_->read([] (auto code, auto message) {
+	fixture.stream1_->recv([] (auto code, auto message) {
 		BOOST_TEST(!code);
 		BOOST_TEST(message.is_object());
 		BOOST_TEST(message["abc"].template get<int>() == 123);
 		BOOST_TEST(message["def"].template get<int>() == 456);
 	});
-	fixture.stream2_->write(message, [] (auto code) {
+	fixture.stream2_->send(message, [] (auto code) {
 		BOOST_TEST(!code);
 	});
 	fixture.service_.run();
 }
 
-BOOST_AUTO_TEST_CASE_TEMPLATE(network_down, Test, list)
+BOOST_AUTO_TEST_CASE_TEMPLATE(connection_reset, Test, list)
 {
 	Test fixture;
 
 	fixture.init();
-	fixture.stream1_->read([] (auto code, auto message) {
-		BOOST_TEST(code.value() == static_cast<int>(std::errc::not_connected));
+	fixture.stream1_->recv([] (auto code, auto message) {
+		BOOST_TEST(code.value() == static_cast<int>(std::errc::connection_reset));
 		BOOST_TEST(message.is_null());
 	});
 	fixture.stream2_ = nullptr;