Skip to content

Commit

Permalink
feat(jrpcd): add "user-ban" feature
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
dfranusic committed Jan 25, 2022
1 parent e7fdf2d commit e2f7323
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 35 deletions.
2 changes: 2 additions & 0 deletions src/include/mink_err_codes.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
}
Expand Down
4 changes: 2 additions & 2 deletions src/include/mink_sqlite.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<bool, int, int> user_auth(const std::string &u, const std::string &p);
std::tuple<int, int, int> user_auth(const std::string &u, const std::string &p);
void connect(const std::string &db_f);

// static constants
Expand Down
24 changes: 22 additions & 2 deletions src/services/json_rpc/jrpc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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(){
Expand All @@ -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},
Expand All @@ -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
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
36 changes: 33 additions & 3 deletions src/services/json_rpc/ws_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
/************/
Expand All @@ -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<std::mutex> 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<stdc::milliseconds>(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<std::mutex> 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<std::mutex> lock(m);
ban_lst.erase(u);
}

bool UserList::add(const usr_info_t &u){
std::unique_lock<std::mutex> lock(m);
users.push_back(u);
Expand Down Expand Up @@ -72,7 +103,6 @@ std::size_t UserList::count(){

// static list of users
UserList USERS;
#endif

/*************************/
/* SSL WebSocket Session */
Expand Down Expand Up @@ -182,7 +212,7 @@ void SSLHTTPSesssion::on_shutdown(beast::error_code ec){
}


std::tuple<int, std::string, std::string, bool, int> user_auth_jrpc(const std::string &crdt){
std::tuple<int, std::string, std::string, int, int> user_auth_jrpc(const std::string &crdt){
// extract user and pwd hash
std::string user;
std::string pwd;
Expand Down
128 changes: 108 additions & 20 deletions src/services/json_rpc/ws_server.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include <boost/beast/core/detail/base64.hpp>
#include <algorithm>
#include <cstdlib>
#include <ctime>
#include <functional>
#include <memory>
#include <thread>
Expand All @@ -32,6 +33,7 @@
#include <mink_err_codes.h>
#include "jrpc.h"
#include <gdt.pb.enums_only.h>
#include <vector>


// boost beast/asio
Expand All @@ -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<std::string,
int,
WebSocketBase *,
std::time_t>;
using usr_info_t = std::tuple<std::string, // username
int, // user flags
WebSocketBase *, // connection pointer
uint64_t>; // last timestamp

//using wss = websocket::stream<beast::ssl_stream<beast::tcp_stream>>;
class WebSocketBase;
Expand Down Expand Up @@ -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<std::string, std::string, bool, int> user_auth(boost::string_view &auth_hdr);
std::tuple<int, std::string, std::string, bool, int> user_auth_jrpc(const std::string &crdt);
std::tuple<int, std::string, std::string, int, int> 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;
Expand All @@ -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<void(const usr_info_t &)> &f);
Expand All @@ -116,13 +128,13 @@ class UserList {
private:
std::mutex m;
std::vector<usr_info_t> users;
std::map<std::string, UserBanInfo> ban_lst;
};

/*************/
/* User list */
/*************/
extern UserList USERS;
#endif

/*****************/
/* WebSocketBase */
Expand All @@ -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};
};

/**********************************/
Expand Down Expand Up @@ -302,6 +315,8 @@ class WebSocketSession : public WebSocketBase {
int id = 0;
// request timeout
int req_tmt = 2000;
// daemon
auto dd = static_cast<JsonRpcdDescriptor*>(mink::CURRENT_DAEMON);
// verify if json is a valid json rpc data
try {
jrpc.verify(true);
Expand All @@ -322,17 +337,82 @@ class WebSocketSession : public WebSocketBase {
const std::string &crdts = jrpc.get_auth_crdts();

// user auth info
std::tuple<int, std::string, std::string, bool, int> ua;
std::tuple<int, std::string, std::string, int, int> 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<stdc::milliseconds>(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<int>(6)){
bi->banned = true;
bi->ts_banned_until = now_msec + (dd->dparams.get_pval<int>(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
Expand Down Expand Up @@ -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);
Expand Down
Loading

0 comments on commit e2f7323

Please sign in to comment.