# HG changeset patch # User David Demelier # Date 1496861077 -7200 # Node ID 4031eda60e11ddc49a966a6120eb2cf72d7e4a53 # Parent 79a2a75ca0910fde8b0ad11611b0e3d65d6c8136 Misc: switch to JSON, #650 diff -r 79a2a75ca091 -r 4031eda60e11 client/main.cpp --- a/client/main.cpp Tue Apr 25 12:44:18 2017 +0200 +++ b/client/main.cpp Wed Jun 07 20:44:37 2017 +0200 @@ -16,37 +16,19 @@ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ -#include - -#include -#include - -#include -#include -#include -#include -#include -#include -#include +#include +#include int main() { - mlk::client::window win; + boost::asio::io_service service; - auto f = std::make_shared(); - auto w = std::make_shared("Malikania"); - auto l = std::make_shared(w); + mlk::client::connection cn(service); + mlk::client::client client(service, cn); - f->move({50, 50}); - f->set_layout(l); - win.add_frame(f); + client.connect("localhost", 3320); - while (win.is_open()) { - win.poll(); - win.set_drawing_color({255, 255, 255, 255}); - win.clear(); - win.draw_frames(); - win.present(); - std::this_thread::sleep_for(std::chrono::milliseconds(50)); + for (;;) { + service.run(); } } diff -r 79a2a75ca091 -r 4031eda60e11 libclient/CMakeLists.txt --- a/libclient/CMakeLists.txt Tue Apr 25 12:44:18 2017 +0200 +++ b/libclient/CMakeLists.txt Wed Jun 07 20:44:37 2017 +0200 @@ -23,7 +23,9 @@ ${libmlk-client_SOURCE_DIR}/malikania/client/animation.hpp ${libmlk-client_SOURCE_DIR}/malikania/client/animator.hpp ${libmlk-client_SOURCE_DIR}/malikania/client/button.hpp + ${libmlk-client_SOURCE_DIR}/malikania/client/client.hpp ${libmlk-client_SOURCE_DIR}/malikania/client/color.hpp + ${libmlk-client_SOURCE_DIR}/malikania/client/connection.hpp ${libmlk-client_SOURCE_DIR}/malikania/client/font.hpp ${libmlk-client_SOURCE_DIR}/malikania/client/frame.hpp ${libmlk-client_SOURCE_DIR}/malikania/client/image.hpp @@ -51,7 +53,9 @@ ${libmlk-client_SOURCE_DIR}/malikania/client/animator.cpp ${libmlk-client_SOURCE_DIR}/malikania/client/button.cpp ${libmlk-client_SOURCE_DIR}/malikania/client/loader.cpp + ${libmlk-client_SOURCE_DIR}/malikania/client/client.cpp ${libmlk-client_SOURCE_DIR}/malikania/client/color.cpp + ${libmlk-client_SOURCE_DIR}/malikania/client/connection.cpp ${libmlk-client_SOURCE_DIR}/malikania/client/font.cpp ${libmlk-client_SOURCE_DIR}/malikania/client/frame.cpp ${libmlk-client_SOURCE_DIR}/malikania/client/image.cpp @@ -98,5 +102,5 @@ $ $ ${INCLUDES} - LIBRARIES libmlk-common ${LIBRARIES} + LIBRARIES libmlk-common OpenSSL::SSL OpenSSL::Crypto ${LIBRARIES} ) diff -r 79a2a75ca091 -r 4031eda60e11 libclient/malikania/client/client.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libclient/malikania/client/client.cpp Wed Jun 07 20:44:37 2017 +0200 @@ -0,0 +1,63 @@ +/* + * client.cpp -- main client game clas + * + * Copyright (c) 2013-2017 Malikania Authors + * + * 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 + +#include "client.hpp" +#include "connection.hpp" +#include "state.hpp" + +namespace mlk { + +namespace client { + +client::client(boost::asio::io_service& service, mlk::client::connection& connection) noexcept + : service_(service) + , connection_(connection) +{ +} + +client::~client() noexcept = default; + +void client::handle_error(boost::system::error_code) +{ + // TODO: add error +} + +void client::handle_connect(boost::system::error_code) +{ + connection_.do_read(*this); +} + +void client::handle_read(boost::system::error_code, nlohmann::json msg) +{ + connection_.do_read(*this); + + if (state_) { + state_->handle_message(*this, std::move(msg)); + } +} + +void client::connect(std::string host, uint16_t port) +{ + connection_.do_connect(*this, std::move(host), port); +} + +} // !client + +} // !mlk diff -r 79a2a75ca091 -r 4031eda60e11 libclient/malikania/client/client.hpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libclient/malikania/client/client.hpp Wed Jun 07 20:44:37 2017 +0200 @@ -0,0 +1,118 @@ +/* + * client.hpp -- main client game clas + * + * Copyright (c) 2013-2017 Malikania Authors + * + * 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 MALIKANIA_CLIENT_CLIENT_HPP +#define MALIKANIA_CLIENT_CLIENT_HPP + +/** + * \file client.hpp + * \brief Main client game class. + */ + +#include + +#include + +namespace mlk { + +namespace client { + +class connection; +class state; + +/** + * \brief Main client game class + */ +class client { +private: + boost::asio::io_service& service_; + mlk::client::connection& connection_; + std::unique_ptr state_; + +public: + /** + * Construct the client, no connection attempt will be made yet. + * + * \param service the I/O service + * \param the connection object + */ + client(boost::asio::io_service& service, mlk::client::connection& connection) noexcept; + + /** + * Virtual destructor defaulted. + */ + virtual ~client() noexcept; + + /** + * Get the connection object. + * + * \return the connection + */ + inline mlk::client::connection& connection() noexcept + { + return connection_; + } + + /** + * Get the connection object. + * + * \return the connection + */ + inline const mlk::client::connection& connection() const noexcept + { + return connection_; + } + + /** + * Connect to the server. + * + * This is an alias of connection_.connect(*this, host, port); + * + * \param host the hostname + * \param port the port number + */ + void connect(std::string host, std::uint16_t port); + + /** + * Handle unexpected network error. + * + * \param code the error code + */ + virtual void handle_error(boost::system::error_code code); + + /** + * Handle connection. + * + * \param code the possible error code + */ + virtual void handle_connect(boost::system::error_code code); + + /** + * Handle reading operation. + * + * \param code the possible error code + * \param msg the received message + */ + virtual void handle_read(boost::system::error_code, nlohmann::json msg); +}; + +} // !client + +} // !mlk + +#endif // !MALIKANIA_CLIENT_CLIENT_HPP diff -r 79a2a75ca091 -r 4031eda60e11 libclient/malikania/client/connection.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libclient/malikania/client/connection.cpp Wed Jun 07 20:44:37 2017 +0200 @@ -0,0 +1,130 @@ +/* + * connection.hpp -- connection to server + * + * Copyright (c) 2013-2017 Malikania Authors + * + * 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 + +#include "client.hpp" +#include "connection.hpp" + +namespace mlk { + +namespace client { + +connection::connection(boost::asio::io_service& service) + : service_(service) + , context_(boost::asio::ssl::context::sslv23) + , socket_(service_, context_) +{ +} + +void connection::handshake(client& client) +{ + socket_.async_handshake(boost::asio::ssl::stream_base::client, [this, &client] (auto code) { + if (code) { + client.handle_error(code); + } else { + client.handle_connect(code); + } + }); +} + +void connection::do_connect(client& client, std::string host, std::uint16_t port) +{ + using namespace std::placeholders; + using tcp = boost::asio::ip::tcp; + + auto resolver = std::make_shared(service_); + auto str = std::to_string(port); + + resolver->async_resolve(tcp::resolver::query(host, str), [this, resolver, &client] (auto code, auto ep) { + if (code) { + client.handle_error(code); + } else { + boost::asio::async_connect(socket_.lowest_layer(), ep, [this, &client] (auto code, auto) { + if (code) { + client.handle_error(code); + } else { + handshake(client); + } + }); + } + }); +} + +void connection::flush(client& client) +{ + auto buffer = boost::asio::buffer(output_.front().data(), output_.front().length()); + + boost::asio::async_write(socket_, buffer, [this, &client] (auto code, auto) { + output_.pop_front(); + + if (code) { + client.handle_error(code); + } else if (!output_.empty()) { + flush(client); + } + }); +} + +void connection::do_send(client& client, nlohmann::json message) +{ + assert(message.is_object()); + + auto in_progress = !output_.empty(); + + output_.push_back(message.dump() + "\r\n\r\n"); + + if (!in_progress) { + flush(client); + } +} + +void connection::do_read(client& client) +{ +#if !defined(NDEBUG) + assert(!is_reading); + + is_reading = true; +#endif + + boost::asio::async_read_until(socket_, input_, "\r\n\r\n", [this, &client] (auto code, auto xfer) { +#if !defined(NDEBUG) + is_reading = false; +#endif + if (code || xfer == 0) { + client.handle_error(code); + } else { + std::string command{ + boost::asio::buffers_begin(input_.data()), + boost::asio::buffers_begin(input_.data()) + xfer - /* \r\n\r\n */ 4 + }; + + input_.consume(xfer); + + try { + client.handle_read(std::move(code), nlohmann::json::parse(command)); + } catch (const std::exception& ex) { + // TODO: log error + } + } + }); +} + +} // !client + +} // !mlk diff -r 79a2a75ca091 -r 4031eda60e11 libclient/malikania/client/connection.hpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libclient/malikania/client/connection.hpp Wed Jun 07 20:44:37 2017 +0200 @@ -0,0 +1,96 @@ +/* + * connection.hpp -- connection to server + * + * Copyright (c) 2013-2017 Malikania Authors + * + * 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 MALIKANIA_CLIENT_CONNECTION_HPP +#define MALIKANIA_CLIENT_CONNECTION_HPP + +/** + * \file connection.hpp + * \brief Connection to server. + */ + +#include + +#include +#include + +#include + +namespace mlk { + +namespace client { + +class client; + +/** + * \brief connection to server. + */ +class connection { +public: +private: + boost::asio::io_service& service_; + boost::asio::ssl::context context_; + boost::asio::ssl::stream socket_; + boost::asio::streambuf input_; + std::deque output_; + +#if !defined(NDEBUG) + bool is_reading{false}; +#endif + + void handshake(client&); + void flush(client&); + +public: + /** + * Create a connection object. + */ + connection(boost::asio::io_service& service); + + /** + * Connect to the given end point. + * + * \pre handler != nullptr + * \param host the hostname + * \param port the port number + * \param handler the handler + */ + virtual void do_connect(client& client, std::string host, std::uint16_t port); + + /** + * Send the given message to the server. + * + * \pre message.is_object() + * \pre handler != nullptr + * \param message the message object + */ + virtual void do_send(client& client, nlohmann::json message); + + /** + * Request for a read operation. + * + * \pre no reading operation must be pending + */ + virtual void do_read(client& client); +}; + +} // !client + +} // !mlk + +#endif // !MALIKANIA_CLIENT_CONNECTION_HPP diff -r 79a2a75ca091 -r 4031eda60e11 libclient/malikania/client/state.hpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libclient/malikania/client/state.hpp Wed Jun 07 20:44:37 2017 +0200 @@ -0,0 +1,67 @@ +/* + * state.hpp -- game client state + * + * Copyright (c) 2013-2017 Malikania Authors + * + * 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 MALIKANIA_CLIENT_STATE_HPP +#define MALIKANIA_CLIENT_STATE_HPP + +/** + * \file state.hpp + * \brief Game client state. + */ + +#include + +namespace mlk { + +namespace client { + +class client; + +/** + * \brief Game client state. + */ +class state { +public: + /** + * Default constructor. + */ + state() noexcept = default; + + /** + * Virtual destructor defaulted. + */ + virtual ~state() noexcept = default; + + /** + * Handle a network message. + * + * \param client the client + * \param msg the network message + */ + virtual void handle_message(client& client, nlohmann::json msg) + { + (void)client; + (void)msg; + } +}; + +} // !client + +} // !mlk + +#endif // !MALIKANIA_CLIENT_STATE_HPP diff -r 79a2a75ca091 -r 4031eda60e11 libserver/CMakeLists.txt --- a/libserver/CMakeLists.txt Tue Apr 25 12:44:18 2017 +0200 +++ b/libserver/CMakeLists.txt Wed Jun 07 20:44:37 2017 +0200 @@ -20,6 +20,7 @@ set( HEADERS + ${libmlk-server_SOURCE_DIR}/malikania/server/net/auth_handler.hpp ${libmlk-server_SOURCE_DIR}/malikania/server/account.hpp ${libmlk-server_SOURCE_DIR}/malikania/server/account_dao.hpp ${libmlk-server_SOURCE_DIR}/malikania/server/client.hpp @@ -29,6 +30,7 @@ set( SOURCES + ${libmlk-server_SOURCE_DIR}/malikania/server/net/auth_handler.cpp ${libmlk-server_SOURCE_DIR}/malikania/server/account_dao.cpp ${libmlk-server_SOURCE_DIR}/malikania/server/client.cpp ${libmlk-server_SOURCE_DIR}/malikania/server/database.cpp @@ -47,4 +49,6 @@ PUBLIC_INCLUDES ${Boost_INCLUDE_DIRS} ${libmlk-server_SOURCE_DIR} + PRIVATE_INCLUDES + ${libmlk-server_SOURCE_DIR}/malikania/server ) diff -r 79a2a75ca091 -r 4031eda60e11 libserver/malikania/server/client.cpp --- a/libserver/malikania/server/client.cpp Tue Apr 25 12:44:18 2017 +0200 +++ b/libserver/malikania/server/client.cpp Wed Jun 07 20:44:37 2017 +0200 @@ -26,59 +26,26 @@ namespace server { -/* - * client_proxy_writer - * ------------------------------------------------------------------ - */ - -client_proxy_writer::client_proxy_writer(std::shared_ptr client, std::string init) - : m_client(std::move(client)) -{ - assert(m_client); - - m_output << init; -} - -client_proxy_writer::~client_proxy_writer() -{ - m_client->send(m_output.str()); -} - -/* - * client_proxy_writer - * ------------------------------------------------------------------ - */ - void client::handle_read(boost::system::error_code code, std::size_t xfer) { // TODO: use fixed size stream + verify exceed. if (code) { - m_server.handle_disconnect(shared_from_this()); + server_.handle_disconnect(shared_from_this()); } else { - std::istringstream iss(std::string( - boost::asio::buffers_begin(m_input.data()), - boost::asio::buffers_begin(m_input.data()) + xfer - 4 - )); + std::string message( + boost::asio::buffers_begin(input_.data()), + boost::asio::buffers_begin(input_.data()) + xfer - 4 + ); // Remove early in case of errors. - m_input.consume(xfer); + input_.consume(xfer); - // Extract command name. - std::string cmd; - iss >> cmd >> std::ws; - - if (!iss) { - return; + try { + server_.handle_read(shared_from_this(), nlohmann::json::parse(message)); + } catch (const std::exception& ex) { + std::cerr << ex.what() << std::endl; } - // Rest of the data. - std::string data(std::istreambuf_iterator(iss), {}); - - if (!iss) { - return; - } - - m_server.handle_message(shared_from_this(), std::move(cmd), std::move(data)); do_read(); } } @@ -86,12 +53,14 @@ void client::handle_write(boost::system::error_code code, std::size_t) { if (code) { - m_server.handle_disconnect(shared_from_this()); + server_.handle_disconnect(shared_from_this()); } else { - m_output.pop_front(); + output_.pop_front(); - if (!m_output.empty()) { + if (!output_.empty()) { do_write(); + } else if (state_ == state::closing) { + server_.handle_disconnect(shared_from_this()); } } } @@ -100,25 +69,25 @@ { auto self = shared_from_this(); - boost::asio::async_read_until(m_socket, m_input, "\r\n\r\n", [self] (auto code, auto xfer) { + boost::asio::async_read_until(socket_, input_, "\r\n\r\n", [self] (auto code, auto xfer) { self->handle_read(code, xfer); }); } void client::do_write() { - assert(!m_output.empty()); + assert(!output_.empty()); auto self = shared_from_this(); - boost::asio::async_write(m_socket, boost::asio::buffer(m_output[0]), [self] (auto code, auto xfer) { + boost::asio::async_write(socket_, boost::asio::buffer(output_[0]), [self] (auto code, auto xfer) { self->handle_write(code, xfer); }); } client::client(server& server, boost::asio::io_service& service, boost::asio::ssl::context& context) - : m_server(server) - , m_socket(service, context) + : server_(server) + , socket_(service, context) { } @@ -126,7 +95,7 @@ { auto self = shared_from_this(); - m_socket.async_handshake(boost::asio::ssl::stream_base::server, [self] (auto code) { + socket_.async_handshake(boost::asio::ssl::stream_base::server, [self] (auto code) { if (code) { std::cerr << "handshake failure: " << code << std::endl; } else { @@ -140,40 +109,59 @@ do_read(); } -void client::send(std::string cmd, std::string data) -{ - send_raw(cmd + " " + data); -} - -void client::ok(std::string cmd) -{ - send_raw(cmd + " ok"); -} - -void client::error(std::string cmd, std::string reason) +void client::send(nlohmann::json message) { - send_raw(cmd + " error " + reason); -} - -client_proxy_writer client::send(std::string cmd) -{ - return client_proxy_writer(shared_from_this(), cmd + " "); -} + assert(message.is_object()); -void client::send_raw(std::string data) -{ - assert(!data.empty()); + if (state_ == state::closing) { + return; + } - auto in_progress = !m_output.empty(); + auto in_progress = !output_.empty(); - data += "\r\n\r\n"; - m_output.push_back(std::move(data)); + output_.push_back(message.dump() + "\r\n\r\n"); if (!in_progress) { do_write(); } } +void client::ok(std::string cmd, nlohmann::json message) +{ + assert(message.is_null() || message.is_object()); + + if (!message.is_object()) { + message = nlohmann::json::object(); + } + + message["command"] = std::move(cmd); + message["status"] = true; + + send(std::move(message)); +} + +void client::error(std::string cmd, nlohmann::json args) +{ + assert(args.is_object() || args.is_string()); + + if (args.is_string()) { + args = nlohmann::json::object({ + { "error", args.get() } + }); + } + + args["command"] = std::move(cmd); + args["status"] = false; + + send(std::move(args)); +} + +void client::fatal(std::string cmd, nlohmann::json args) +{ + error(std::move(cmd), std::move(args)); + state_ = state::closing; +} + } // !server } // !mlk diff -r 79a2a75ca091 -r 4031eda60e11 libserver/malikania/server/client.hpp --- a/libserver/malikania/server/client.hpp Tue Apr 25 12:44:18 2017 +0200 +++ b/libserver/malikania/server/client.hpp Wed Jun 07 20:44:37 2017 +0200 @@ -19,6 +19,11 @@ #ifndef MALIKANIA_SERVER_CLIENT_HPP #define MALIKANIA_SERVER_CLIENT_HPP +/** + * \file client.hpp + * \brief Client connected to the server. + */ + #include #include #include @@ -28,48 +33,51 @@ #include #include +#include + namespace mlk { namespace server { -class client; class server; -class client_proxy_writer { - std::shared_ptr m_client; - std::ostringstream m_output; - -public: - client_proxy_writer(std::shared_ptr client, std::string init); - - client_proxy_writer(client_proxy_writer&&) = default; - - ~client_proxy_writer(); - - template - inline client_proxy_writer& operator<<(Arg&& arg) - { - m_output << std::forward(arg); - - return *this; - } -}; - +/** + * \brief Client connected to the server. + */ class client : public std::enable_shared_from_this { public: friend class server; + /** + * \brief Describe client current state + */ enum class state : std::uint8_t { + /** + * The client is authenticating. + */ authenticating, - ready + + /** + * The client is ready for I/O + */ + ready, + + /** + * The client is closing. + * + * No more messages are accepted for output, the pending queue is sent + * to the client and then disconnected. + */ + closing }; private: - server& m_server; - state m_state{state::authenticating}; - boost::asio::ssl::stream m_socket; - boost::asio::streambuf m_input; - std::deque m_output; + server& server_; + state state_{state::authenticating}; + + boost::asio::ssl::stream socket_; + boost::asio::streambuf input_; + std::deque output_; void handle_read(boost::system::error_code, std::size_t); void handle_write(boost::system::error_code, std::size_t); @@ -91,49 +99,64 @@ client(server& server, boost::asio::io_service& service, boost::asio::ssl::context& context); /** + * Get the client state. + * + * \return the state + */ + inline enum state state() const noexcept + { + return state_; + } + + /** + * Set the client state. + * + * \param new_state the state + */ + inline void set_state(enum state new_state) noexcept + { + state_ = new_state; + } + + /** * Send a command and its arguments. * * \pre !cmd.empty() - * \param cmd the command - * \param args the arguments + * \param message the message to send */ - void send(std::string cmd, std::string args); + void send(nlohmann::json message); /** * Send successful command result. * * \pre !cmd.empty() + * \pre args.is_object() || args.is_object() * \param cmd the command name + * \param args the extra data */ - void ok(std::string cmd); + void ok(std::string cmd, nlohmann::json args = nullptr); /** * Send a error command result. * * \pre !cmd.empty() && !reason.empty() + * \pre args.is_object() || args.is_string() * \param cmd the command name - * \param reason the reason string + * \param args the extra data (can be a string) */ - void error(std::string cmd, std::string reason); + void error(std::string cmd, nlohmann::json args); /** - * Convenient function for sending data using operator<<. + * Send a fatal error result. + * + * The client state is changed to closing. * - * The returned have operator<< defined for the user and will append all - * data inserted that way when the appropriate client_proxy_write is - * destroyed. - * + * \pre !cmd.empty() && !reason.empty() + * \pre args.is_object() || args.is_string() * \param cmd the command name + * \param args the extra data (can be a string) */ - client_proxy_writer send(std::string cmd); - - /** - * Send a raw data. - * - * \pre !data.empty() - * \param data the data - */ - void send_raw(std::string data); + void fatal(std::string cmd, nlohmann::json args); }; } // !server diff -r 79a2a75ca091 -r 4031eda60e11 libserver/malikania/server/net/auth_handler.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libserver/malikania/server/net/auth_handler.cpp Wed Jun 07 20:44:37 2017 +0200 @@ -0,0 +1,51 @@ +/* + * auth_handler.cpp -- handle "auth" network command + * + * Copyright (c) 2013-2017 David Demelier + * + * 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 + +#include "account_dao.hpp" +#include "auth_handler.hpp" +#include "client.hpp" +#include "server.hpp" + +namespace mlk { + +namespace server { + +void auth_handler::exec(server& server, std::shared_ptr clt, nlohmann::json object) noexcept +{ + try { + auto login = util::json::require_string(object, "/login"_json_pointer); + auto password = util::json::require_string(object, "/password"_json_pointer); + + account_dao dao(server.database()); + + if (!dao.authenticate(login, password)) { + clt->error("auth", "invalid credentials"); + } else { + clt->set_state(client::state::ready); + clt->ok("auth"); + } + } catch (const std::exception& ex) { + clt->error("auth", ex.what()); + } +} + +} // !server + +} // !mlk diff -r 79a2a75ca091 -r 4031eda60e11 libserver/malikania/server/net/auth_handler.hpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libserver/malikania/server/net/auth_handler.hpp Wed Jun 07 20:44:37 2017 +0200 @@ -0,0 +1,48 @@ +/* + * auth_handler.hpp -- handle "auth" network command + * + * Copyright (c) 2013-2017 David Demelier + * + * 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 MALIKANIA_SERVER_AUTH_HANDLER_HPP +#define MALIKANIA_SERVER_AUTH_HANDLER_HPP + +/** + * \file auth_handler.hpp + * \brief Handle "auth" network command. + */ + +#include "handler.hpp" + +namespace mlk { + +namespace server { + +/** + * \brief Handle "auth" network command. + */ +class auth_handler : public handler { +public: + /** + * \copydoc handler::exec + */ + void exec(server &server, std::shared_ptr clt, nlohmann::json object) noexcept override; +}; + +} // !server + +} // !mlk + +#endif // !MALIKANIA_SERVER_AUTH_HANDLER_HPP diff -r 79a2a75ca091 -r 4031eda60e11 libserver/malikania/server/net/handler.hpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libserver/malikania/server/net/handler.hpp Wed Jun 07 20:44:37 2017 +0200 @@ -0,0 +1,69 @@ +/* + * handler.hpp -- server side remote command + * + * Copyright (c) 2013-2017 Malikania Authors + * + * 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 MALIKANIA_SERVER_HANDLER_HPP +#define MALIKANIA_SERVER_HANDLER_HPP + +/** + * \file handler.hpp + * \brief Server side remote command. + */ + +#include + +#include + +namespace mlk { + +namespace server { + +class server; +class client; + +/** + * \brief Server side remote command. + */ +class handler { +public: + /** + * Default constructor. + */ + handler() noexcept = default; + + /** + * Virtual destructor defaulted. + */ + virtual ~handler() noexcept = default; + + /** + * Execute the function from the given client. + * + * \param server the global object server + * \param client the client + * \param message the network message + */ + virtual void exec(server& server, + std::shared_ptr client, + nlohmann::json message) noexcept = 0; +}; + +} // !server + +} // !mlk + +#endif // !MALIKANIA_SERVER_HANDLER_HPP diff -r 79a2a75ca091 -r 4031eda60e11 libserver/malikania/server/server.cpp --- a/libserver/malikania/server/server.cpp Tue Apr 25 12:44:18 2017 +0200 +++ b/libserver/malikania/server/server.cpp Wed Jun 07 20:44:37 2017 +0200 @@ -23,85 +23,134 @@ #include "server.hpp" #include "util.hpp" +#include "net/auth_handler.hpp" + namespace mlk { namespace server { -void server::handle_auth(std::shared_ptr clt, std::string args) -{ - std::cout << "== auth ==" << std::endl; - - auto list = util::net::split(args); +/* + * server::load + * ------------------------------------------------------------------ + */ - if (list.size() != 2) { - clt->error("auth", "2 arguments required"); - } else { - account_dao dao(m_database); +void server::load() +{ + handle("auth", std::make_unique()); +} - if (!dao.authenticate(list[0], list[1])) { - clt->error("auth", "invalid credential or inexistant account"); - } else { - clt->ok("auth"); - } - } -} +/* + * server::start + * ------------------------------------------------------------------ + * + * Start an asynchronous accept(2) call. + */ void server::start() { - auto clt = std::make_shared(*this, m_service, m_context); + auto clt = std::make_shared(*this, service_, context_); - m_acceptor.async_accept(clt->m_socket.lowest_layer(), [this, clt] (auto code) { + acceptor_.async_accept(clt->socket_.lowest_layer(), [this, clt] (auto code) { this->handle_accept(std::move(clt), code); }); } -boost::asio::ip::tcp::endpoint server::endpoint(const server_settings ¶ms) const +/* + * server::endpoint + * ------------------------------------------------------------------ + * + * Create an endpoint according to the parameters. + */ + +boost::asio::ip::tcp::endpoint server::endpoint(const settings& params) const { // TODO: add more settings there. - return boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), params.port); + return boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), params.port()); } -server::server(boost::asio::io_service& service, - const server_settings& sv_params, - const database_settings& db_params) - : m_service(service) - , m_acceptor(service, endpoint(sv_params)) - , m_context(boost::asio::ssl::context::sslv23) - , m_handlers{ - { "auth", std::bind(&server::handle_auth, this, std::placeholders::_1, std::placeholders::_2) } - } - , m_database(db_params) +/* + * server + * ------------------------------------------------------------------ + * + * Construct a server object and start listening for clients. + */ + +server::server(boost::asio::io_service& service, mlk::server::database& db, const settings& settings) + : service_(service) + , acceptor_(service, endpoint(settings)) + , context_(boost::asio::ssl::context::sslv23) + , database_(db) { - m_context.use_certificate_chain_file(sv_params.certificate); - m_context.use_private_key_file(sv_params.key, boost::asio::ssl::context::pem); + context_.use_certificate_chain_file(settings.certificate()); + context_.use_private_key_file(settings.key(), boost::asio::ssl::context::pem); + load(); start(); } -void server::add_handler(std::string cmd, handler func) +/* + * server::handle + * ------------------------------------------------------------------ + */ + +void server::handle(std::string name, std::unique_ptr handler) { - m_handlers.emplace(std::move(cmd), std::move(func)); + assert(!name.empty()); + assert(handler); + + handlers_.emplace(std::move(name), std::move(handler)); } +/* + * server::handle_disconnect + * ------------------------------------------------------------------ + * + * Asynchronous function called when a client has been detected as disconnected. + * The default implementation just removes the client from the server. + */ + void server::handle_disconnect(std::shared_ptr clt) { std::cout << "client disconnected" << std::endl; - m_clients.erase(clt); + clients_.erase(std::move(clt)); } -void server::handle_message(std::shared_ptr clt, std::string cmd, std::string args) +/* + * server::handle_read + * ------------------------------------------------------------------ + */ + +void server::handle_read(std::shared_ptr clt, nlohmann::json message) { - std::cout << "client sent:\n"; - std::cout << " -> cmd [" << cmd << "]\n"; - std::cout << " -> args [" << args << "]\n"; + assert(message.is_object()); + + auto cmd = message.find("command"); - auto it = m_handlers.find(cmd); + if (cmd == message.end() || !cmd->is_string()) { + std::cerr << "client sent invalid message" << std::endl; + return; + } - if (it != m_handlers.end()) { - it->second(std::move(clt), std::move(args)); + auto it = handlers_.find(*cmd); + + // TODO: try-catch + if (it == handlers_.end()) { + std::cerr << "client send an unknown or unhandled command" << std::endl; + } else { + it->second->exec(*this, std::move(clt), std::move(message)); } } +/* + * server::handle_accept + * ------------------------------------------------------------------ + * + * Asynchronous function called once an accept(2) called has been finished with + * or without errors. + * + * On success, request a handshake on the client. + */ + void server::handle_accept(std::shared_ptr clt, boost::system::error_code code) { if (code) { @@ -109,7 +158,7 @@ } else { std::cout << "new client connected" << std::endl; clt->handshake(); - m_clients.insert(std::move(clt)); + clients_.insert(std::move(clt)); } start(); diff -r 79a2a75ca091 -r 4031eda60e11 libserver/malikania/server/server.hpp --- a/libserver/malikania/server/server.hpp Tue Apr 25 12:44:18 2017 +0200 +++ b/libserver/malikania/server/server.hpp Wed Jun 07 20:44:37 2017 +0200 @@ -20,7 +20,6 @@ #define MALIKANIA_SERVER_SERVER_HPP #include -#include #include #include #include @@ -29,51 +28,131 @@ #include #include -#include "database.hpp" +#include + +#include "net/handler.hpp" namespace mlk { namespace server { class client; +class database; -class server_settings { +/** + * \brief Server parameters + */ +class settings { +private: + std::uint16_t port_; + std::string certificate_; + std::string key_; + public: - std::uint16_t port; - std::string certificate; - std::string key; + /** + * Constructor. + * + * \param port the port + * \param certificate the certificate file + * \param key the private key file + * \pre !certificate.empty() + * \pre !key.empty() + */ + inline settings(std::uint16_t port, std::string certificate, std::string key) noexcept + : port_(port) + , certificate_(std::move(certificate)) + , key_(std::move(key)) + { + assert(!certificate_.empty()); + assert(!key_.empty()); + } + + /** + * Get the associated port. + * + * \return the port + */ + inline std::uint16_t port() const noexcept + { + return port_; + } + + /** + * Get the certificate file. + * + * \return the path to the certificate file + */ + inline const std::string& certificate() const noexcept + { + return certificate_; + } + + /** + * Get the key file. + * + * \return the path to the key file + */ + inline const std::string& key() const noexcept + { + return key_; + } }; class server { -public: - using handler = std::function, std::string)>; +private: + boost::asio::io_service& service_; + boost::asio::ip::tcp::acceptor acceptor_; + boost::asio::ssl::context context_; -private: - boost::asio::io_service& m_service; - boost::asio::ip::tcp::acceptor m_acceptor; - boost::asio::ssl::context m_context; - std::unordered_set> m_clients; - std::unordered_map m_handlers; + std::unordered_set> clients_; + std::unordered_map> handlers_; - database m_database; + mlk::server::database& database_; - void handle_auth(std::shared_ptr, std::string); + void load(); void start(); - boost::asio::ip::tcp::endpoint endpoint(const server_settings& params) const; + boost::asio::ip::tcp::endpoint endpoint(const settings& params) const; + +protected: + void handle_auth(std::shared_ptr, nlohmann::json); public: - server(boost::asio::io_service& service, - const server_settings& sv_params, - const database_settings& db_params); + server(boost::asio::io_service& service, mlk::server::database& db, const settings& settings); - void add_handler(std::string cmd, handler func); + inline mlk::server::database& database() noexcept + { + return database_; + } + + inline const mlk::server::database& database() const noexcept + { + return database_; + } - void handle_disconnect(std::shared_ptr); + /** + * Add network handler. + * + * If a handler is already present, it will be replaced with the new one. + * + * \pre handler != nullptr + * \pre !name.empty() + * \param handler the handler + */ + void handle(std::string name, std::unique_ptr handler); - void handle_message(std::shared_ptr, std::string, std::string); + virtual void handle_disconnect(std::shared_ptr); - void handle_accept(std::shared_ptr, boost::system::error_code); + /** + * Asynchronous function called once a message has been completely and + * successfully received from the client. + * + * The default implementation searches for a handler defined for this + * message. + */ + virtual void handle_read(std::shared_ptr, nlohmann::json message); + + virtual void handle_accept(std::shared_ptr, boost::system::error_code); }; } // !server diff -r 79a2a75ca091 -r 4031eda60e11 server/main.cpp --- a/server/main.cpp Tue Apr 25 12:44:18 2017 +0200 +++ b/server/main.cpp Wed Jun 07 20:44:37 2017 +0200 @@ -18,25 +18,26 @@ #include +#include #include int main() { - mlk::server::server_settings sv_params; + mlk::server::settings sv_params{ + 3320, + "/home/markand/null/server.crt", + "/home/markand/null/server.key", + }; - sv_params.port = 3320; - sv_params.certificate = "/home/markand/null/server.crt"; - sv_params.key = "/home/markand/null/server.key"; - - mlk::server::database_settings db_params; - - db_params["type"] = "sqlite"; - db_params["path"] = "/home/markand/kingdom.db"; + mlk::server::database db({ + { "type", "sqlite" }, + { "path", "/home/markand/kingdom.db" } + }); boost::asio::io_service service; try { - mlk::server::server server(service, sv_params, db_params); + mlk::server::server server(service, db, sv_params); for (;;) { service.run();