From e2f7323e6683d35dd0343ddad165cec05a4df204 Mon Sep 17 00:00:00 2001 From: Damir Franusic Date: Tue, 25 Jan 2022 10:38:07 +0100 Subject: [PATCH] feat(jrpcd): add "user-ban" feature Prevent user from logging in after X number of failed attempts. Add ban period and number of attempts as an optional argument for jrpcd (-t, defaults to 3 attempts and 15 minute ban period) --- src/include/mink_err_codes.h | 2 + src/include/mink_sqlite.h | 4 +- src/services/json_rpc/jrpc.cpp | 24 +++++- src/services/json_rpc/ws_server.cpp | 36 +++++++- src/services/json_rpc/ws_server.h | 128 +++++++++++++++++++++++----- src/utils/mink_sqlite.cpp | 30 +++++-- 6 files changed, 189 insertions(+), 35 deletions(-) diff --git a/src/include/mink_err_codes.h b/src/include/mink_err_codes.h index d169b5a..62e2be5 100644 --- a/src/include/mink_err_codes.h +++ b/src/include/mink_err_codes.h @@ -20,6 +20,8 @@ namespace mink { EC_GDT_PUSH_FAILED = -3, EC_AUTH_INVALID_METHOD = -4, EC_AUTH_FAILED = -5, + EC_AUTH_UNKNOWN_USER = -6, + EC_AUTH_USER_BANNED = -7, EC_UNKNOWN = -9999 }; } diff --git a/src/include/mink_sqlite.h b/src/include/mink_sqlite.h index 648c0c2..8575005 100644 --- a/src/include/mink_sqlite.h +++ b/src/include/mink_sqlite.h @@ -33,7 +33,7 @@ namespace mink_db { class CmdSpecificAuth { public: CmdSpecificAuth() = default; - ~CmdSpecificAuth() = default; + virtual ~CmdSpecificAuth() = default; CmdSpecificAuth(const CmdSpecificAuth &o) = delete; CmdSpecificAuth &operator=(const CmdSpecificAuth &o) = delete; // cmd handlers implemented in derived classes @@ -57,7 +57,7 @@ namespace mink_db { bool cmd_auth(const int cmd_id, const std::string &u); bool cmd_specific_auth(const vpmap &vp, const std::string &u); - std::tuple user_auth(const std::string &u, const std::string &p); + std::tuple user_auth(const std::string &u, const std::string &p); void connect(const std::string &db_f); // static constants diff --git a/src/services/json_rpc/jrpc.cpp b/src/services/json_rpc/jrpc.cpp index f9962e1..0bdff63 100644 --- a/src/services/json_rpc/jrpc.cpp +++ b/src/services/json_rpc/jrpc.cpp @@ -42,6 +42,9 @@ JsonRpcdDescriptor::JsonRpcdDescriptor(const char *_type, dparams.set_bool(4, false); // -u dparams.set_bool(5, false); + // -t + dparams.set_int(6, 3); + dparams.set_int(7, 15); } JsonRpcdDescriptor::~JsonRpcdDescriptor(){ @@ -51,6 +54,7 @@ JsonRpcdDescriptor::~JsonRpcdDescriptor(){ void JsonRpcdDescriptor::process_args(int argc, char **argv){ std::regex addr_regex("(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}):(\\d+)"); std::regex ipv4_regex("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}"); + std::regex log_sec_regex("(\\d+):(\\d+)"); int opt; int option_index = 0; struct option long_options[] = {{"gdt-streams", required_argument, 0, 0}, @@ -64,7 +68,7 @@ void JsonRpcdDescriptor::process_args(int argc, char **argv){ exit(EXIT_FAILURE); } - while ((opt = getopt_long(argc, argv, "?c:h:i:w:s:C:DSu", long_options, + while ((opt = getopt_long(argc, argv, "?c:h:i:w:s:C:DSt:u", long_options, &option_index)) != -1) { switch (opt) { // long options @@ -167,7 +171,6 @@ void JsonRpcdDescriptor::process_args(int argc, char **argv){ case 'C': try { // path and certificates - int fsz = 0; std::string cpath(optarg); std::string f_cert(cpath + "/cert.pem"); std::string f_key(cpath + "/key.pem"); @@ -217,6 +220,22 @@ void JsonRpcdDescriptor::process_args(int argc, char **argv){ dparams.set_bool(4, true); break; + // login attempts and ban time + case 't': { + std::smatch rgxg; + std::string s(optarg); + if (!std::regex_match(s, rgxg, log_sec_regex)) { + std::cout << "ERROR: Invalid login counter format '" << optarg + << "'!" << std::endl; + exit(EXIT_FAILURE); + + } else { + dparams.set_int(6, std::stoi(rgxg[1])); + dparams.set_int(7, std::stoi(rgxg[2])); + } + break; + } + // enable unencrypted ws case 'u': dparams.set_bool(5, true); @@ -253,6 +272,7 @@ void JsonRpcdDescriptor::print_help(){ std::cout << " -C\tcertificates path (key.pem, cert.pem, dh.pem)" << std::endl; std::cout << " -D\tstart in debug mode" << std::endl; std::cout << " -S\tsingle session mode" << std::endl; + std::cout << " -t\tlogin counter value (max_attempts:ban_time_in_minutes)" << std::endl; std::cout << " -u\tenable unencrypted WebSocket (ws://)" << std::endl; std::cout << std::endl; std::cout << "GDT Options:" << std::endl; diff --git a/src/services/json_rpc/ws_server.cpp b/src/services/json_rpc/ws_server.cpp index 278ea4b..1e65805 100644 --- a/src/services/json_rpc/ws_server.cpp +++ b/src/services/json_rpc/ws_server.cpp @@ -21,7 +21,6 @@ void fail(beast::error_code ec, char const *what) { std::cerr << what << ": " << ec.message() << "\n"; } -#ifdef ENABLE_WS_SINGLE_SESSION /************/ /* UserList */ /************/ @@ -33,6 +32,38 @@ usr_info_t UserList::exists(const std::string &u){ return std::make_tuple("", 0, nullptr, 0); } +UserBanInfo *UserList::add_attempt(const std::string &u){ + std::unique_lock lock(m); + auto it = ban_lst.find(u); + // add new user + if(it == ban_lst.end()){ + // get unix timestamp (part of user tuple) + auto ts_now = stdc::system_clock::now().time_since_epoch(); + uint64_t ts_msec = stdc::duration_cast(ts_now).count(); + ban_lst.emplace(u, UserBanInfo{u, 1, ts_msec, false}); + + // user exists + } else { + ++it->second.attemtps; + } + return &ban_lst.find(u)->second; +} + +UserBanInfo *UserList::get_banned(const std::string &u){ + std::unique_lock lock(m); + auto it = ban_lst.find(u); + if(it != ban_lst.end()){ + m.unlock(); + return &it->second; + } + return nullptr; +} + +void UserList::lift_ban(const std::string &u){ + std::unique_lock lock(m); + ban_lst.erase(u); +} + bool UserList::add(const usr_info_t &u){ std::unique_lock lock(m); users.push_back(u); @@ -72,7 +103,6 @@ std::size_t UserList::count(){ // static list of users UserList USERS; -#endif /*************************/ /* SSL WebSocket Session */ @@ -182,7 +212,7 @@ void SSLHTTPSesssion::on_shutdown(beast::error_code ec){ } -std::tuple user_auth_jrpc(const std::string &crdt){ +std::tuple user_auth_jrpc(const std::string &crdt){ // extract user and pwd hash std::string user; std::string pwd; diff --git a/src/services/json_rpc/ws_server.h b/src/services/json_rpc/ws_server.h index 6ea918d..53d240b 100644 --- a/src/services/json_rpc/ws_server.h +++ b/src/services/json_rpc/ws_server.h @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -32,6 +33,7 @@ #include #include "jrpc.h" #include +#include // boost beast/asio @@ -41,13 +43,13 @@ namespace websocket = beast::websocket; namespace net = boost::asio; namespace ssl = boost::asio::ssl; namespace base64 = boost::beast::detail::base64; +namespace stdc = std::chrono; using tcp = boost::asio::ip::tcp; using Jrpc = json_rpc::JsonRpc; -namespace chrono = std::chrono; -using usr_info_t = std::tuple; +using usr_info_t = std::tuple; // last timestamp //using wss = websocket::stream>; class WebSocketBase; @@ -93,12 +95,19 @@ class EVUserCB: public gdt::GDTCallbackMethod { void fail(beast::error_code ec, char const *what); //bool user_auth_prepare(boost::string_view &auth_str, int type); //std::tuple user_auth(boost::string_view &auth_hdr); -std::tuple user_auth_jrpc(const std::string &crdt); +std::tuple user_auth_jrpc(const std::string &crdt); -#ifdef ENABLE_WS_SINGLE_SESSION /*******************************/ /* List of authenticated users */ /*******************************/ +struct UserBanInfo { + std::string username; + int attemtps; + uint64_t ts_msec; + uint64_t ts_banned_until; + bool banned; +}; + class UserList { public: UserList() = default; @@ -108,6 +117,9 @@ class UserList { usr_info_t exists(const std::string &u); bool add(const usr_info_t &u); + UserBanInfo *add_attempt(const std::string &u); + UserBanInfo *get_banned(const std::string &u); + void lift_ban(const std::string &u); void remove(const std::string &u, const uint64_t &ts); void remove_all(); void process_all(const std::function &f); @@ -116,13 +128,13 @@ class UserList { private: std::mutex m; std::vector users; + std::map ban_lst; }; /*************/ /* User list */ /*************/ extern UserList USERS; -#endif /*****************/ /* WebSocketBase */ @@ -138,12 +150,13 @@ class WebSocketBase { USERS.remove(std::get<0>(usr_info_), std::get<3>(usr_info_)); } - usr_info_t usr_info_; #endif virtual beast::flat_buffer &get_buffer() = 0; virtual std::mutex &get_mtx() = 0; virtual void async_buffer_send(const std::string &d) = 0; virtual void do_close() = 0; + + usr_info_t usr_info_ = {"", 0, nullptr, 0}; }; /**********************************/ @@ -302,6 +315,8 @@ class WebSocketSession : public WebSocketBase { int id = 0; // request timeout int req_tmt = 2000; + // daemon + auto dd = static_cast(mink::CURRENT_DAEMON); // verify if json is a valid json rpc data try { jrpc.verify(true); @@ -322,17 +337,82 @@ class WebSocketSession : public WebSocketBase { const std::string &crdts = jrpc.get_auth_crdts(); // user auth info - std::tuple ua; + std::tuple ua; // connect with DB ua = user_auth_jrpc(crdts); - if (!std::get<3>(ua)) + + /**************************************/ + /* tuple index [3] = user auth status */ + /**************************************/ + // -1 = invalid user + // 0 = user found, invalid password + // 1 = user found and authenticated + if (std::get<3>(ua) == -1) + throw AuthException(mink::error::EC_AUTH_UNKNOWN_USER); + + /**************/ + /* user found */ + /**************/ + // get unix timestamp (part of user tuple) + auto ts_now = stdc::system_clock::now().time_since_epoch(); + // now ts msec + uint64_t now_msec = stdc::duration_cast(ts_now).count(); + + // invalid password check + if (std::get<3>(ua) == 0){ + // find user + UserBanInfo *bi = USERS.get_banned(std::get<1>(ua)); + + // add to list if not found + if (!bi) + bi = USERS.add_attempt(std::get<1>(ua)); + + // if found, inc attempts + else + ++bi->attemtps; + + // check if banned + if (bi->banned){ + // check if ban can be lifted + if(now_msec - bi->ts_msec > bi->ts_banned_until){ + std::cout << "Ban lifted" << std::endl; + USERS.lift_ban(bi->username); + bi = nullptr; + + }else{ + // too many failed attempts + throw AuthException(mink::error::EC_AUTH_USER_BANNED); + } + + // check if ban needs to be set + }else{ + if(bi->attemtps >= dd->dparams.get_pval(6)){ + bi->banned = true; + bi->ts_banned_until = now_msec + (dd->dparams.get_pval(7) * 60 * 1000); + // user is now banned + throw AuthException(mink::error::EC_AUTH_USER_BANNED); + } + } + + // invalid password throw AuthException(mink::error::EC_AUTH_FAILED); - // save session credentials - set_credentials(std::get<0>(ua), - std::get<1>(ua), - std::get<2>(ua), - std::get<4>(ua)); + // password ok, check if user was banned + }else{ + // find + UserBanInfo *bi = USERS.get_banned(std::get<1>(ua)); + // user found, check if ban can be lifted + if(bi && bi->banned){ + if(bi->ts_banned_until <= now_msec){ + USERS.lift_ban(bi->username); + + // ban can't be lifted just yet + }else{ + throw AuthException(mink::error::EC_AUTH_USER_BANNED); + } + } + } + #ifdef ENABLE_WS_SINGLE_SESSION if (USERS.count() > 0) { // new user is admin, logout other users @@ -369,16 +449,24 @@ class WebSocketSession : public WebSocketBase { } } } - // get unix timestamp (part of user tuple) - auto ts_now = chrono::system_clock::now(); +#endif + + + // save session credentials + set_credentials(std::get<0>(ua), + std::get<1>(ua), + std::get<2>(ua), + std::get<4>(ua)); + // add to user list auto new_usr = std::make_tuple(std::get<1>(ua), std::get<4>(ua), this, - ts_now.time_since_epoch().count()); + now_msec); USERS.add(new_usr); + + // set current user for this connection usr_info_ = new_usr; -#endif // generate response auto j_res = json_rpc::JsonRpc::gen_response(id); diff --git a/src/utils/mink_sqlite.cpp b/src/utils/mink_sqlite.cpp index 7446668..eb1dc48 100644 --- a/src/utils/mink_sqlite.cpp +++ b/src/utils/mink_sqlite.cpp @@ -26,10 +26,18 @@ using ptype = asn1::ParameterType; /******************/ // authenticate user const char *msqlm::SQL_USER_AUTH = - "SELECT a.id, a.flags " + "SELECT a.id, " + " a.flags, " + " a.username, " + " iif(b.username is NULL, 0, 1) as auth " "FROM user a " - "WHERE a.username = ? AND " - "password = ?"; + "LEFT JOIN " + " (SELECT flags, username " + " FROM user a " + " WHERE username = ? AND " + " password = ?) b " + "ON a.username = b.username " + "WHERE a.username = ?"; // add new user const char *msqlm::SQL_USER_ADD = @@ -239,7 +247,7 @@ bool msqlm::cmd_auth(const int cmd_id, const std::string &u){ return res; } -std::tuple msqlm::user_auth(const std::string &u, const std::string &p){ +std::tuple msqlm::user_auth(const std::string &u, const std::string &p){ if (!db) throw std::invalid_argument("invalid db connection"); @@ -261,14 +269,19 @@ std::tuple msqlm::user_auth(const std::string &u, const std::str if (sqlite3_bind_text(stmt, 2, p.c_str(), p.size(), SQLITE_STATIC)) throw std::invalid_argument("sql:cannot bind password"); + // username + if (sqlite3_bind_text(stmt, 3, u.c_str(), u.size(), SQLITE_STATIC)) + throw std::invalid_argument("sql:cannot bind username"); + + // step - bool res = false; int usr_flags = 0; - int usr_id = 0; + int usr_id = -1; + int auth = -1; if (sqlite3_step(stmt) == SQLITE_ROW) { - res = sqlite3_data_count(stmt) > 0; usr_id = sqlite3_column_int(stmt, 0); usr_flags = sqlite3_column_int(stmt, 1); + auth = sqlite3_column_int(stmt, 3); } // cleanup if(sqlite3_clear_bindings(stmt)) @@ -278,8 +291,9 @@ std::tuple msqlm::user_auth(const std::string &u, const std::str if(sqlite3_finalize(stmt)) throw std::invalid_argument("sql:cannot finalize statement"); + // default auth value - return std::make_tuple(res, usr_id, usr_flags); + return std::make_tuple(auth, usr_id, usr_flags); } void msqlm::connect(const std::string &db_f){