Skip to content

Commit e2f7323

Browse files
committed
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)
1 parent e7fdf2d commit e2f7323

File tree

6 files changed

+189
-35
lines changed

6 files changed

+189
-35
lines changed

src/include/mink_err_codes.h

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ namespace mink {
2020
EC_GDT_PUSH_FAILED = -3,
2121
EC_AUTH_INVALID_METHOD = -4,
2222
EC_AUTH_FAILED = -5,
23+
EC_AUTH_UNKNOWN_USER = -6,
24+
EC_AUTH_USER_BANNED = -7,
2325
EC_UNKNOWN = -9999
2426
};
2527
}

src/include/mink_sqlite.h

+2-2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ namespace mink_db {
3333
class CmdSpecificAuth {
3434
public:
3535
CmdSpecificAuth() = default;
36-
~CmdSpecificAuth() = default;
36+
virtual ~CmdSpecificAuth() = default;
3737
CmdSpecificAuth(const CmdSpecificAuth &o) = delete;
3838
CmdSpecificAuth &operator=(const CmdSpecificAuth &o) = delete;
3939
// cmd handlers implemented in derived classes
@@ -57,7 +57,7 @@ namespace mink_db {
5757

5858
bool cmd_auth(const int cmd_id, const std::string &u);
5959
bool cmd_specific_auth(const vpmap &vp, const std::string &u);
60-
std::tuple<bool, int, int> user_auth(const std::string &u, const std::string &p);
60+
std::tuple<int, int, int> user_auth(const std::string &u, const std::string &p);
6161
void connect(const std::string &db_f);
6262

6363
// static constants

src/services/json_rpc/jrpc.cpp

+22-2
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ JsonRpcdDescriptor::JsonRpcdDescriptor(const char *_type,
4242
dparams.set_bool(4, false);
4343
// -u
4444
dparams.set_bool(5, false);
45+
// -t
46+
dparams.set_int(6, 3);
47+
dparams.set_int(7, 15);
4548
}
4649

4750
JsonRpcdDescriptor::~JsonRpcdDescriptor(){
@@ -51,6 +54,7 @@ JsonRpcdDescriptor::~JsonRpcdDescriptor(){
5154
void JsonRpcdDescriptor::process_args(int argc, char **argv){
5255
std::regex addr_regex("(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}):(\\d+)");
5356
std::regex ipv4_regex("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}");
57+
std::regex log_sec_regex("(\\d+):(\\d+)");
5458
int opt;
5559
int option_index = 0;
5660
struct option long_options[] = {{"gdt-streams", required_argument, 0, 0},
@@ -64,7 +68,7 @@ void JsonRpcdDescriptor::process_args(int argc, char **argv){
6468
exit(EXIT_FAILURE);
6569
}
6670

67-
while ((opt = getopt_long(argc, argv, "?c:h:i:w:s:C:DSu", long_options,
71+
while ((opt = getopt_long(argc, argv, "?c:h:i:w:s:C:DSt:u", long_options,
6872
&option_index)) != -1) {
6973
switch (opt) {
7074
// long options
@@ -167,7 +171,6 @@ void JsonRpcdDescriptor::process_args(int argc, char **argv){
167171
case 'C':
168172
try {
169173
// path and certificates
170-
int fsz = 0;
171174
std::string cpath(optarg);
172175
std::string f_cert(cpath + "/cert.pem");
173176
std::string f_key(cpath + "/key.pem");
@@ -217,6 +220,22 @@ void JsonRpcdDescriptor::process_args(int argc, char **argv){
217220
dparams.set_bool(4, true);
218221
break;
219222

223+
// login attempts and ban time
224+
case 't': {
225+
std::smatch rgxg;
226+
std::string s(optarg);
227+
if (!std::regex_match(s, rgxg, log_sec_regex)) {
228+
std::cout << "ERROR: Invalid login counter format '" << optarg
229+
<< "'!" << std::endl;
230+
exit(EXIT_FAILURE);
231+
232+
} else {
233+
dparams.set_int(6, std::stoi(rgxg[1]));
234+
dparams.set_int(7, std::stoi(rgxg[2]));
235+
}
236+
break;
237+
}
238+
220239
// enable unencrypted ws
221240
case 'u':
222241
dparams.set_bool(5, true);
@@ -253,6 +272,7 @@ void JsonRpcdDescriptor::print_help(){
253272
std::cout << " -C\tcertificates path (key.pem, cert.pem, dh.pem)" << std::endl;
254273
std::cout << " -D\tstart in debug mode" << std::endl;
255274
std::cout << " -S\tsingle session mode" << std::endl;
275+
std::cout << " -t\tlogin counter value (max_attempts:ban_time_in_minutes)" << std::endl;
256276
std::cout << " -u\tenable unencrypted WebSocket (ws://)" << std::endl;
257277
std::cout << std::endl;
258278
std::cout << "GDT Options:" << std::endl;

src/services/json_rpc/ws_server.cpp

+33-3
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ void fail(beast::error_code ec, char const *what) {
2121
std::cerr << what << ": " << ec.message() << "\n";
2222
}
2323

24-
#ifdef ENABLE_WS_SINGLE_SESSION
2524
/************/
2625
/* UserList */
2726
/************/
@@ -33,6 +32,38 @@ usr_info_t UserList::exists(const std::string &u){
3332
return std::make_tuple("", 0, nullptr, 0);
3433
}
3534

35+
UserBanInfo *UserList::add_attempt(const std::string &u){
36+
std::unique_lock<std::mutex> lock(m);
37+
auto it = ban_lst.find(u);
38+
// add new user
39+
if(it == ban_lst.end()){
40+
// get unix timestamp (part of user tuple)
41+
auto ts_now = stdc::system_clock::now().time_since_epoch();
42+
uint64_t ts_msec = stdc::duration_cast<stdc::milliseconds>(ts_now).count();
43+
ban_lst.emplace(u, UserBanInfo{u, 1, ts_msec, false});
44+
45+
// user exists
46+
} else {
47+
++it->second.attemtps;
48+
}
49+
return &ban_lst.find(u)->second;
50+
}
51+
52+
UserBanInfo *UserList::get_banned(const std::string &u){
53+
std::unique_lock<std::mutex> lock(m);
54+
auto it = ban_lst.find(u);
55+
if(it != ban_lst.end()){
56+
m.unlock();
57+
return &it->second;
58+
}
59+
return nullptr;
60+
}
61+
62+
void UserList::lift_ban(const std::string &u){
63+
std::unique_lock<std::mutex> lock(m);
64+
ban_lst.erase(u);
65+
}
66+
3667
bool UserList::add(const usr_info_t &u){
3768
std::unique_lock<std::mutex> lock(m);
3869
users.push_back(u);
@@ -72,7 +103,6 @@ std::size_t UserList::count(){
72103

73104
// static list of users
74105
UserList USERS;
75-
#endif
76106

77107
/*************************/
78108
/* SSL WebSocket Session */
@@ -182,7 +212,7 @@ void SSLHTTPSesssion::on_shutdown(beast::error_code ec){
182212
}
183213

184214

185-
std::tuple<int, std::string, std::string, bool, int> user_auth_jrpc(const std::string &crdt){
215+
std::tuple<int, std::string, std::string, int, int> user_auth_jrpc(const std::string &crdt){
186216
// extract user and pwd hash
187217
std::string user;
188218
std::string pwd;

src/services/json_rpc/ws_server.h

+108-20
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#include <boost/beast/core/detail/base64.hpp>
2323
#include <algorithm>
2424
#include <cstdlib>
25+
#include <ctime>
2526
#include <functional>
2627
#include <memory>
2728
#include <thread>
@@ -32,6 +33,7 @@
3233
#include <mink_err_codes.h>
3334
#include "jrpc.h"
3435
#include <gdt.pb.enums_only.h>
36+
#include <vector>
3537

3638

3739
// boost beast/asio
@@ -41,13 +43,13 @@ namespace websocket = beast::websocket;
4143
namespace net = boost::asio;
4244
namespace ssl = boost::asio::ssl;
4345
namespace base64 = boost::beast::detail::base64;
46+
namespace stdc = std::chrono;
4447
using tcp = boost::asio::ip::tcp;
4548
using Jrpc = json_rpc::JsonRpc;
46-
namespace chrono = std::chrono;
47-
using usr_info_t = std::tuple<std::string,
48-
int,
49-
WebSocketBase *,
50-
std::time_t>;
49+
using usr_info_t = std::tuple<std::string, // username
50+
int, // user flags
51+
WebSocketBase *, // connection pointer
52+
uint64_t>; // last timestamp
5153

5254
//using wss = websocket::stream<beast::ssl_stream<beast::tcp_stream>>;
5355
class WebSocketBase;
@@ -93,12 +95,19 @@ class EVUserCB: public gdt::GDTCallbackMethod {
9395
void fail(beast::error_code ec, char const *what);
9496
//bool user_auth_prepare(boost::string_view &auth_str, int type);
9597
//std::tuple<std::string, std::string, bool, int> user_auth(boost::string_view &auth_hdr);
96-
std::tuple<int, std::string, std::string, bool, int> user_auth_jrpc(const std::string &crdt);
98+
std::tuple<int, std::string, std::string, int, int> user_auth_jrpc(const std::string &crdt);
9799

98-
#ifdef ENABLE_WS_SINGLE_SESSION
99100
/*******************************/
100101
/* List of authenticated users */
101102
/*******************************/
103+
struct UserBanInfo {
104+
std::string username;
105+
int attemtps;
106+
uint64_t ts_msec;
107+
uint64_t ts_banned_until;
108+
bool banned;
109+
};
110+
102111
class UserList {
103112
public:
104113
UserList() = default;
@@ -108,6 +117,9 @@ class UserList {
108117

109118
usr_info_t exists(const std::string &u);
110119
bool add(const usr_info_t &u);
120+
UserBanInfo *add_attempt(const std::string &u);
121+
UserBanInfo *get_banned(const std::string &u);
122+
void lift_ban(const std::string &u);
111123
void remove(const std::string &u, const uint64_t &ts);
112124
void remove_all();
113125
void process_all(const std::function<void(const usr_info_t &)> &f);
@@ -116,13 +128,13 @@ class UserList {
116128
private:
117129
std::mutex m;
118130
std::vector<usr_info_t> users;
131+
std::map<std::string, UserBanInfo> ban_lst;
119132
};
120133

121134
/*************/
122135
/* User list */
123136
/*************/
124137
extern UserList USERS;
125-
#endif
126138

127139
/*****************/
128140
/* WebSocketBase */
@@ -138,12 +150,13 @@ class WebSocketBase {
138150
USERS.remove(std::get<0>(usr_info_), std::get<3>(usr_info_));
139151
}
140152

141-
usr_info_t usr_info_;
142153
#endif
143154
virtual beast::flat_buffer &get_buffer() = 0;
144155
virtual std::mutex &get_mtx() = 0;
145156
virtual void async_buffer_send(const std::string &d) = 0;
146157
virtual void do_close() = 0;
158+
159+
usr_info_t usr_info_ = {"", 0, nullptr, 0};
147160
};
148161

149162
/**********************************/
@@ -302,6 +315,8 @@ class WebSocketSession : public WebSocketBase {
302315
int id = 0;
303316
// request timeout
304317
int req_tmt = 2000;
318+
// daemon
319+
auto dd = static_cast<JsonRpcdDescriptor*>(mink::CURRENT_DAEMON);
305320
// verify if json is a valid json rpc data
306321
try {
307322
jrpc.verify(true);
@@ -322,17 +337,82 @@ class WebSocketSession : public WebSocketBase {
322337
const std::string &crdts = jrpc.get_auth_crdts();
323338

324339
// user auth info
325-
std::tuple<int, std::string, std::string, bool, int> ua;
340+
std::tuple<int, std::string, std::string, int, int> ua;
326341
// connect with DB
327342
ua = user_auth_jrpc(crdts);
328-
if (!std::get<3>(ua))
343+
344+
/**************************************/
345+
/* tuple index [3] = user auth status */
346+
/**************************************/
347+
// -1 = invalid user
348+
// 0 = user found, invalid password
349+
// 1 = user found and authenticated
350+
if (std::get<3>(ua) == -1)
351+
throw AuthException(mink::error::EC_AUTH_UNKNOWN_USER);
352+
353+
/**************/
354+
/* user found */
355+
/**************/
356+
// get unix timestamp (part of user tuple)
357+
auto ts_now = stdc::system_clock::now().time_since_epoch();
358+
// now ts msec
359+
uint64_t now_msec = stdc::duration_cast<stdc::milliseconds>(ts_now).count();
360+
361+
// invalid password check
362+
if (std::get<3>(ua) == 0){
363+
// find user
364+
UserBanInfo *bi = USERS.get_banned(std::get<1>(ua));
365+
366+
// add to list if not found
367+
if (!bi)
368+
bi = USERS.add_attempt(std::get<1>(ua));
369+
370+
// if found, inc attempts
371+
else
372+
++bi->attemtps;
373+
374+
// check if banned
375+
if (bi->banned){
376+
// check if ban can be lifted
377+
if(now_msec - bi->ts_msec > bi->ts_banned_until){
378+
std::cout << "Ban lifted" << std::endl;
379+
USERS.lift_ban(bi->username);
380+
bi = nullptr;
381+
382+
}else{
383+
// too many failed attempts
384+
throw AuthException(mink::error::EC_AUTH_USER_BANNED);
385+
}
386+
387+
// check if ban needs to be set
388+
}else{
389+
if(bi->attemtps >= dd->dparams.get_pval<int>(6)){
390+
bi->banned = true;
391+
bi->ts_banned_until = now_msec + (dd->dparams.get_pval<int>(7) * 60 * 1000);
392+
// user is now banned
393+
throw AuthException(mink::error::EC_AUTH_USER_BANNED);
394+
}
395+
}
396+
397+
// invalid password
329398
throw AuthException(mink::error::EC_AUTH_FAILED);
330399

331-
// save session credentials
332-
set_credentials(std::get<0>(ua),
333-
std::get<1>(ua),
334-
std::get<2>(ua),
335-
std::get<4>(ua));
400+
// password ok, check if user was banned
401+
}else{
402+
// find
403+
UserBanInfo *bi = USERS.get_banned(std::get<1>(ua));
404+
// user found, check if ban can be lifted
405+
if(bi && bi->banned){
406+
if(bi->ts_banned_until <= now_msec){
407+
USERS.lift_ban(bi->username);
408+
409+
// ban can't be lifted just yet
410+
}else{
411+
throw AuthException(mink::error::EC_AUTH_USER_BANNED);
412+
}
413+
}
414+
}
415+
336416
#ifdef ENABLE_WS_SINGLE_SESSION
337417
if (USERS.count() > 0) {
338418
// new user is admin, logout other users
@@ -369,16 +449,24 @@ class WebSocketSession : public WebSocketBase {
369449
}
370450
}
371451
}
372-
// get unix timestamp (part of user tuple)
373-
auto ts_now = chrono::system_clock::now();
452+
#endif
453+
454+
455+
// save session credentials
456+
set_credentials(std::get<0>(ua),
457+
std::get<1>(ua),
458+
std::get<2>(ua),
459+
std::get<4>(ua));
460+
374461
// add to user list
375462
auto new_usr = std::make_tuple(std::get<1>(ua),
376463
std::get<4>(ua),
377464
this,
378-
ts_now.time_since_epoch().count());
465+
now_msec);
379466
USERS.add(new_usr);
467+
468+
// set current user for this connection
380469
usr_info_ = new_usr;
381-
#endif
382470

383471
// generate response
384472
auto j_res = json_rpc::JsonRpc::gen_response(id);

0 commit comments

Comments
 (0)