Mercurial > irccd
changeset 239:554a542f0a82
Irccd: add experimental authentication support
author | David Demelier <markand@malikania.fr> |
---|---|
date | Wed, 17 Aug 2016 15:20:50 +0200 |
parents | 772656d4ba8d |
children | b25176b3bb80 |
files | lib/irccd/CMakeSources.cmake lib/irccd/config.cpp lib/irccd/conn-state-auth.cpp lib/irccd/conn-state-auth.hpp lib/irccd/conn-state-checking.cpp lib/irccd/connection.hpp lib/irccd/irccdctl.cpp lib/irccd/service-transport.cpp lib/irccd/transport.cpp lib/irccd/transport.hpp |
diffstat | 10 files changed, 464 insertions(+), 92 deletions(-) [+] |
line wrap: on
line diff
--- a/lib/irccd/CMakeSources.cmake Mon Aug 15 15:26:08 2016 +0200 +++ b/lib/irccd/CMakeSources.cmake Wed Aug 17 15:20:50 2016 +0200 @@ -2,6 +2,7 @@ HEADERS ${CMAKE_CURRENT_LIST_DIR}/alias.hpp ${CMAKE_CURRENT_LIST_DIR}/connection.hpp + ${CMAKE_CURRENT_LIST_DIR}/conn-state-auth.hpp ${CMAKE_CURRENT_LIST_DIR}/conn-state-checking.hpp ${CMAKE_CURRENT_LIST_DIR}/conn-state-connecting.hpp ${CMAKE_CURRENT_LIST_DIR}/conn-state-disconnected.hpp @@ -83,6 +84,7 @@ SOURCES ${CMAKE_CURRENT_LIST_DIR}/alias.cpp ${CMAKE_CURRENT_LIST_DIR}/connection.cpp + ${CMAKE_CURRENT_LIST_DIR}/conn-state-auth.cpp ${CMAKE_CURRENT_LIST_DIR}/conn-state-checking.cpp ${CMAKE_CURRENT_LIST_DIR}/conn-state-connecting.cpp ${CMAKE_CURRENT_LIST_DIR}/conn-state-disconnected.cpp
--- a/lib/irccd/config.cpp Mon Aug 15 15:26:08 2016 +0200 +++ b/lib/irccd/config.cpp Wed Aug 17 15:20:50 2016 +0200 @@ -231,6 +231,9 @@ else throw std::invalid_argument("transport: invalid type given: {}"_format(it->value())); + if ((it = sc.find("password")) != sc.end()) + transport->setPassword(it->value()); + return transport; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/irccd/conn-state-auth.cpp Wed Aug 17 15:20:50 2016 +0200 @@ -0,0 +1,124 @@ +/* + * conn-state-auth.cpp -- connection is authenticating + * + * Copyright (c) 2013-2016 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. + */ + +#include "conn-state-auth.hpp" +#include "conn-state-disconnected.hpp" +#include "conn-state-ready.hpp" + +namespace irccd { + +void Connection::AuthState::send(Connection &cnt) noexcept +{ + try { + auto n = cnt.send(m_output.data(), m_output.size()); + + if (n == 0) { + m_output.clear(); + throw std::runtime_error("connection lost"); + } + + m_output.erase(0, n); + + if (m_output.empty()) + m_auth = Checking; + } catch (const std::exception &ex) { + cnt.m_state = std::make_unique<DisconnectedState>(); + cnt.onDisconnect(ex.what()); + } +} + +void Connection::AuthState::check(Connection &cnt) noexcept +{ + cnt.syncInput(); + + auto msg = util::nextNetwork(cnt.m_input); + + if (msg.empty()) + return; + + try { + auto doc = nlohmann::json::parse(msg); + + if (!doc.is_object()) + throw std::invalid_argument("invalid argument"); + + auto cmd = doc.find("response"); + + if (cmd == doc.end() || !cmd->is_string() || *cmd != "auth") + throw std::invalid_argument("authentication result expected"); + + auto result = doc.find("result"); + + if (result == doc.end() || !result->is_boolean()) + throw std::invalid_argument("bad protocol"); + + if (!*result) + throw std::runtime_error("authentication failed"); + + cnt.m_state = std::make_unique<ReadyState>(); + } catch (const std::exception &ex) { + cnt.m_state = std::make_unique<DisconnectedState>(); + cnt.onDisconnect(ex.what()); + } +} + +Connection::Status Connection::AuthState::status() const noexcept +{ + return Authenticating; +} + +void Connection::AuthState::prepare(Connection &cnt, fd_set &in, fd_set &out) +{ + switch (m_auth) { + case Created: + m_auth = Sending; + m_output += nlohmann::json({ + { "command", "auth" }, + { "password", cnt.m_password } + }).dump(); + m_output += "\r\n\r\n"; + + // FALLTHROUGH + case Sending: + FD_SET(cnt.m_socket.handle(), &out); + break; + case Checking: + FD_SET(cnt.m_socket.handle(), &in); + break; + default: + break; + } +} + +void Connection::AuthState::sync(Connection &cnt, fd_set &in, fd_set &out) +{ + switch (m_auth) { + case Sending: + if (FD_ISSET(cnt.m_socket.handle(), &out)) + send(cnt); + break; + case Checking: + if (FD_ISSET(cnt.m_socket.handle(), &in)) + check(cnt); + break; + default: + break; + } +} + +} // !irccd
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/irccd/conn-state-auth.hpp Wed Aug 17 15:20:50 2016 +0200 @@ -0,0 +1,70 @@ +/* + * conn-state-auth.hpp -- connection is authenticating + * + * Copyright (c) 2013-2016 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_CONN_STATE_AUTH_HPP +#define IRCCD_CONN_STATE_AUTH_HPP + +/** + * \file conn-state-auth.hpp + * \brief Connection is authenticating. + */ + +#include "conn-state.hpp" + +namespace irccd { + +/** + * \brief Authentication in progress. + * + * This state emit the authentication command and receives the response to see + * if authentication succeeded. + */ +class Connection::AuthState : public Connection::State { +private: + enum { + Created, + Sending, + Checking + } m_auth{Created}; + + std::string m_output; + + void send(Connection &cnt) noexcept; + void check(Connection &cnt) noexcept; + +public: + + /** + * \copydoc State::status + */ + Status status() const noexcept override; + + /** + * \copydoc State::prepare + */ + void prepare(Connection &cnt, fd_set &in, fd_set &out) override; + + /** + * \copydoc State::sync + */ + void sync(Connection &cnt, fd_set &in, fd_set &out) override; +}; + +} // !irccd + +#endif // !IRCCD_CONN_STATE_AUTH_HPP
--- a/lib/irccd/conn-state-checking.cpp Mon Aug 15 15:26:08 2016 +0200 +++ b/lib/irccd/conn-state-checking.cpp Wed Aug 17 15:20:50 2016 +0200 @@ -18,6 +18,7 @@ #include <format.h> +#include "conn-state-auth.hpp" #include "conn-state-checking.hpp" #include "conn-state-disconnected.hpp" #include "conn-state-ready.hpp" @@ -59,7 +60,11 @@ IRCCD_VERSION_MAJOR, IRCCD_VERSION_MINOR, IRCCD_VERSION_PATCH)); // Successfully connected. - cnx.m_stateNext = std::make_unique<ReadyState>(); + if (cnx.m_password.empty()) + cnx.m_stateNext = std::make_unique<ReadyState>(); + else + cnx.m_stateNext = std::make_unique<AuthState>(); + cnx.onConnect(info); }
--- a/lib/irccd/connection.hpp Mon Aug 15 15:26:08 2016 +0200 +++ b/lib/irccd/connection.hpp Wed Aug 17 15:20:50 2016 +0200 @@ -117,9 +117,11 @@ private: std::string m_input; std::string m_output; + std::string m_password; public: class State; + class AuthState; class DisconnectedState; class ConnectingState; class CheckingState; @@ -160,6 +162,26 @@ virtual ~Connection(); /** + * Get the optional password. + * + * \return the password + */ + inline const std::string &password() const noexcept + { + return m_password; + } + + /** + * Set the optional password + * + * \param password the password + */ + inline void setPassword(std::string password) noexcept + { + m_password = std::move(password); + } + + /** * Convenient wrapper around recv(). * * Must be used in sync() function.
--- a/lib/irccd/irccdctl.cpp Mon Aug 15 15:26:08 2016 +0200 +++ b/lib/irccd/irccdctl.cpp Wed Aug 17 15:20:50 2016 +0200 @@ -172,6 +172,11 @@ readConnectLocal(sc); else throw std::invalid_argument("invalid type given: " + it->value()); + + auto password = sc.find("password"); + + if (password != sc.end()) + m_connection->setPassword(password->value()); } /* @@ -394,7 +399,7 @@ auto it = std::find_if(m_input.begin(), m_input.end(), [&] (const auto &v) { auto rt = v.find("response"); - if (rt != v.end() && rt->is_string() && *rt == id) + if (v.count("error") > 0 || (rt != v.end() && rt->is_string() && *rt == id)) return true; return false; @@ -407,6 +412,11 @@ } } + auto error = value.find("error"); + + if (error != value.end() && error->is_string()) + throw std::runtime_error(error->template get<std::string>()); + return value; }
--- a/lib/irccd/service-transport.cpp Mon Aug 15 15:26:08 2016 +0200 +++ b/lib/irccd/service-transport.cpp Wed Aug 17 15:20:50 2016 +0200 @@ -114,6 +114,16 @@ { using namespace std::placeholders; + // Transport clients. + for (const auto &client : m_clients) { + try { + client->sync(in, out); + } catch (const std::exception &ex) { + log::info() << "transport: client disconnected: " << ex.what() << std::endl; + handleDie(client); + } + } + // Transport servers. for (const auto &transport : m_servers) { if (!FD_ISSET(transport->handle(), &in)) @@ -124,24 +134,7 @@ std::shared_ptr<TransportClient> client = transport->accept(); std::weak_ptr<TransportClient> ptr(client); - // Send some information. - auto object = nlohmann::json::object({ - { "program", "irccd" }, - { "major", IRCCD_VERSION_MAJOR }, - { "minor", IRCCD_VERSION_MINOR }, - { "patch", IRCCD_VERSION_PATCH } - }); - -#if defined(WITH_JS) - object.push_back({"javascript", true}); -#endif -#if defined(WITH_SSL) - object.push_back({"ssl", true}); -#endif - try { - client->send(object); - // Connect signals. client->onCommand.connect(std::bind(&TransportService::handleCommand, this, ptr, _1)); client->onDie.connect(std::bind(&TransportService::handleDie, this, ptr)); @@ -152,16 +145,6 @@ log::info() << "transport: client disconnected: " << ex.what() << std::endl; } } - - // Transport clients. - for (const auto &client : m_clients) { - try { - client->sync(in, out); - } catch (const std::exception &ex) { - log::info() << "transport: client disconnected: " << ex.what() << std::endl; - handleDie(client); - } - } } void TransportService::add(std::shared_ptr<TransportServer> ts) @@ -173,9 +156,9 @@ { assert(json.is_object()); - // Asynchronous send. for (const auto &client : m_clients) - client->send(json); + if (client->state() == TransportClient::Ready) + client->send(json); } } // !irccd
--- a/lib/irccd/transport.cpp Mon Aug 15 15:26:08 2016 +0200 +++ b/lib/irccd/transport.cpp Wed Aug 17 15:20:50 2016 +0200 @@ -28,25 +28,74 @@ * ------------------------------------------------------------------ */ -void TransportClient::parse(const std::string &message) +void TransportClient::error(const std::string &msg) +{ + m_state = Closing; + + send({{ "error", msg }}); +} + +void TransportClient::flush() noexcept { - auto document = nlohmann::json::parse(message); + for (std::size_t pos; (pos = m_input.find("\r\n\r\n")) != std::string::npos; ) { + auto message = m_input.substr(0, pos); + + m_input.erase(m_input.begin(), m_input.begin() + pos + 4); + + try { + auto document = nlohmann::json::parse(message); - if (document.is_object()) - onCommand(document); + if (!document.is_object()) + error("invalid argument"); + else + onCommand(document); + } catch (const std::exception &ex) { + error(ex.what()); + } + } } -unsigned TransportClient::recv(char *buffer, unsigned length) +void TransportClient::authenticate() noexcept { - return m_socket.recv(buffer, length); + auto pos = m_input.find("\r\n\r\n"); + + if (pos == std::string::npos) + return; + + auto msg = m_input.substr(0, pos); + + m_input.erase(m_input.begin(), m_input.begin() + pos + 4); + + try { + auto doc = nlohmann::json::parse(msg); + + if (!doc.is_object()) + error("invalid argument"); + + auto cmd = doc.find("command"); + + if (cmd == doc.end() || !cmd->is_string() || *cmd != "auth") + error("authentication required"); + + auto pw = doc.find("password"); + auto result = true; + + if (pw == doc.end() || !pw->is_string() || *pw != m_parent.password()) { + m_state = Closing; + result = false; + } else + m_state = Ready; + + send({ + { "response", "auth" }, + { "result", result } + }); + } catch (const std::exception &ex) { + error(ex.what()); + } } -unsigned TransportClient::send(const char *buffer, unsigned length) -{ - return m_socket.send(buffer, length); -} - -void TransportClient::syncInput() +void TransportClient::recv() noexcept { try { std::string buffer; @@ -63,7 +112,7 @@ } } -void TransportClient::syncOutput() +void TransportClient::send() noexcept { try { auto ns = send(&m_output[0], m_output.size()); @@ -77,32 +126,101 @@ } } +unsigned TransportClient::recv(void *buffer, unsigned length) +{ + return m_socket.recv(buffer, length); +} + +unsigned TransportClient::send(const void *buffer, unsigned length) +{ + return m_socket.send(buffer, length); +} + +TransportClient::TransportClient(TransportServer &parent, net::TcpSocket socket) + : m_parent(parent) + , m_socket(std::move(socket)) +{ + assert(m_socket.isOpen()); + + m_socket.set(net::option::SockBlockMode(false)); + + // Send some information. + auto object = nlohmann::json::object({ + { "program", "irccd" }, + { "major", IRCCD_VERSION_MAJOR }, + { "minor", IRCCD_VERSION_MINOR }, + { "patch", IRCCD_VERSION_PATCH } + }); + +#if defined(WITH_JS) + object.push_back({"javascript", true}); +#endif +#if defined(WITH_SSL) + object.push_back({"ssl", true}); +#endif + + send(object); +} + void TransportClient::prepare(fd_set &in, fd_set &out, net::Handle &max) { if (m_socket.handle() > max) max = m_socket.handle(); - FD_SET(m_socket.handle(), &in); + switch (m_state) { + case Greeting: + FD_SET(m_socket.handle(), &out); + break; + case Authenticating: + FD_SET(m_socket.handle(), &in); + break; + case Ready: + FD_SET(m_socket.handle(), &in); - if (!m_output.empty()) - FD_SET(m_socket.handle(), &out); + if (!m_output.empty()) + FD_SET(m_socket.handle(), &out); + break; + case Closing: + if (!m_output.empty()) + FD_SET(m_socket.handle(), &out); + else + onDie(); + break; + default: + break; + } } void TransportClient::sync(fd_set &in, fd_set &out) { - // Do some I/O. - if (FD_ISSET(m_socket.handle(), &in)) - syncInput(); - if (FD_ISSET(m_socket.handle(), &out)) - syncOutput(); + switch (m_state) { + case Greeting: + send(); + + if (m_output.empty()) + m_state = m_parent.password().empty() ? Ready : Authenticating; + + break; + case Authenticating: + if (FD_ISSET(m_socket.handle(), &in)) + recv(); - // Flush the queue. - for (std::size_t pos; (pos = m_input.find("\r\n\r\n")) != std::string::npos; ) { - auto message = m_input.substr(0, pos); + authenticate(); + break; + case Ready: + if (FD_ISSET(m_socket.handle(), &in)) + recv(); + if (FD_ISSET(m_socket.handle(), &out)) + send(); - m_input.erase(m_input.begin(), m_input.begin() + pos + 4); - - parse(message); + flush(); + break; + case Closing: + if (FD_ISSET(m_socket.handle(), &out)) + send(); + break; + default: + break; } } @@ -135,8 +253,9 @@ TransportClientTls::TransportClientTls(const std::string &pkey, const std::string &cert, + TransportServer &server, net::TcpSocket socket) - : TransportClient(std::move(socket)) + : TransportClient(server, std::move(socket)) , m_ssl(m_socket) { m_ssl.setPrivateKey(pkey); @@ -145,7 +264,7 @@ handshake(); } -unsigned TransportClientTls::recv(char *buffer, unsigned length) +unsigned TransportClientTls::recv(void *buffer, unsigned length) { unsigned nread = 0; @@ -160,7 +279,7 @@ return nread; } -unsigned TransportClientTls::send(const char *buffer, unsigned length) +unsigned TransportClientTls::send(const void *buffer, unsigned length) { unsigned nsent = 0; @@ -256,7 +375,7 @@ std::unique_ptr<TransportClient> TransportServerTls::accept() { - return std::make_unique<TransportClientTls>(m_privatekey, m_cert, m_socket.accept()); + return std::make_unique<TransportClientTls>(m_privatekey, m_cert, *this, m_socket.accept()); } /*
--- a/lib/irccd/transport.hpp Mon Aug 15 15:26:08 2016 +0200 +++ b/lib/irccd/transport.hpp Wed Aug 17 15:20:50 2016 +0200 @@ -36,6 +36,8 @@ namespace irccd { +class TransportServer; + /** * \class TransportClient * \brief Client connected to irccd. @@ -45,6 +47,16 @@ class TransportClient { public: /** + * \brief Client state + */ + enum State { + Greeting, //!< client is getting irccd info + Authenticating, //!< client requires authentication + Ready, //!< client is ready to use + Closing //!< client must disconnect + }; + + /** * Signal: onCommand * ---------------------------------------------------------- * @@ -61,19 +73,28 @@ */ Signal<> onDie; +private: + void error(const std::string &msg); + void flush() noexcept; + void authenticate() noexcept; + protected: + State m_state{Greeting}; //!< current client state + TransportServer &m_parent; //!< parent transport server net::TcpSocket m_socket; //!< socket std::string m_input; //!< input buffer std::string m_output; //!< output buffer /** - * Parse input buffer. - * - * \param buffer the buffer. + * Fill the input buffer with available data. */ - void parse(const std::string &buffer); + void recv() noexcept; -protected: + /** + * Flush the output buffer from available pending data. + */ + void send() noexcept; + /** * Try to receive some data into the given buffer. * @@ -81,7 +102,7 @@ * \param length the buffer length * \return the number of bytes received */ - IRCCD_EXPORT virtual unsigned recv(char *buffer, unsigned length); + IRCCD_EXPORT virtual unsigned recv(void *buffer, unsigned length); /** * Try to send some data into the given buffer. @@ -90,21 +111,17 @@ * \param length the buffer length * \return the number of bytes sent */ - IRCCD_EXPORT virtual unsigned send(const char *buffer, unsigned length); + IRCCD_EXPORT virtual unsigned send(const void *buffer, unsigned length); public: /** * Create a transport client from the socket. * * \pre socket must be valid + * \param parent the parent server + * \param socket the new socket */ - inline TransportClient(net::TcpSocket socket) - : m_socket(std::move(socket)) - { - assert(m_socket.isOpen()); - - m_socket.set(net::option::SockBlockMode(false)); - } + IRCCD_EXPORT TransportClient(TransportServer &parent, net::TcpSocket socket); /** * Virtual destructor defaulted. @@ -112,18 +129,14 @@ virtual ~TransportClient() = default; /** - * Convenient wrapper around recv(). + * Get the client state. * - * Must be used in sync() function. + * \return the client state */ - IRCCD_EXPORT void syncInput(); - - /** - * Convenient wrapper around send(). - * - * Must be used in sync() function. - */ - IRCCD_EXPORT void syncOutput(); + inline State state() const noexcept + { + return m_state; + } /** * Append some data to the output queue. @@ -168,12 +181,12 @@ /** * \copydoc TransportClient::recv */ - unsigned recv(char *buffer, unsigned length) override; + unsigned recv(void *buffer, unsigned length) override; /** * \copydoc TransportClient::send */ - unsigned send(const char *buffer, unsigned length) override; + unsigned send(const void *buffer, unsigned length) override; public: /** @@ -183,9 +196,12 @@ * \param pkey the private key * \param cert the certificate file * \param socket the accepted socket + * \param parent the parent server + * \param socket the new socket */ IRCCD_EXPORT TransportClientTls(const std::string &pkey, const std::string &cert, + TransportServer &server, net::TcpSocket socket); /** @@ -227,10 +243,8 @@ TransportServer &operator=(TransportServer &&) = delete; protected: - /** - * The socket handle. - */ net::TcpSocket m_socket; + std::string m_password; public: /** @@ -252,6 +266,26 @@ } /** + * Get the password. + * + * \return the password + */ + inline const std::string &password() const noexcept + { + return m_password; + } + + /** + * Set an optional password. + * + * \return the password + */ + inline void setPassword(std::string password) noexcept + { + m_password = std::move(password); + } + + /** * Destructor defaulted. */ virtual ~TransportServer() = default; @@ -263,7 +297,7 @@ */ virtual std::unique_ptr<TransportClient> accept() { - return std::make_unique<TransportClient>(m_socket.accept()); + return std::make_unique<TransportClient>(*this, m_socket.accept()); } };