Browse Source

Web authentication token feature (#5093)


Co-authored-by: Lemongrass3110 <lemongrass@kstp.at>
Co-authored-by: Aleos <aleos89@users.noreply.github.com>
Jittapan Pluemsumran 4 years ago
parent
commit
00d6014c8c

+ 4 - 0
conf/login_athena.conf

@@ -163,6 +163,10 @@ dnsbl_servers: bl.blocklist.de, socks.dnsbl.sorbs.net
 // Note: see 'doc/md5_hashcheck.txt' for more details.
 client_hash_check: off
 
+// Enable web authentication token system
+// This is required for new clients that get data via an additional API over HTTP
+use_web_auth_token: yes
+
 // Client MD5 hashes
 // The client with the specified hash can be used to log in by players with
 // a group_id equal to or greater than the given value.

+ 4 - 1
sql-files/main.sql

@@ -754,8 +754,11 @@ CREATE TABLE IF NOT EXISTS `login` (
   `pincode_change` int(11) unsigned NOT NULL DEFAULT '0',
   `vip_time` int(11) unsigned NOT NULL default '0',
   `old_group` tinyint(3) NOT NULL default '0',
+  `web_auth_token` varchar(17) null,
+  `web_auth_token_enabled` tinyint(2) NOT NULL default '0',
   PRIMARY KEY  (`account_id`),
-  KEY `name` (`userid`)
+  KEY `name` (`userid`),
+  UNIQUE KEY `web_auth_token_key` (`web_auth_token`)
 ) ENGINE=MyISAM AUTO_INCREMENT=2000000; 
 
 -- added standard accounts for servers, VERY INSECURE!!!

+ 5 - 0
sql-files/upgrades/upgrade_20200625.sql

@@ -0,0 +1,5 @@
+ALTER TABLE `login`
+	ADD COLUMN `web_auth_token` VARCHAR(17) NULL AFTER `old_group`,
+	ADD COLUMN `web_auth_token_enabled` tinyint(2) NOT NULL default '0' AFTER `web_auth_token`,
+	ADD UNIQUE KEY `web_auth_token_key` (`web_auth_token`)
+;

+ 1 - 1
src/char/char.cpp

@@ -1966,7 +1966,7 @@ void char_auth_ok(int fd, struct char_session_data *sd) {
 		{	//Character already online. KICK KICK KICK
 			mapif_disconnectplayer(map_server[character->server].fd, character->account_id, character->char_id, 2);
 			if (character->waiting_disconnect == INVALID_TIMER)
-				character->waiting_disconnect = add_timer(gettick()+20000, char_chardb_waiting_disconnect, character->account_id, 0);
+				character->waiting_disconnect = add_timer(gettick()+AUTH_TIMEOUT, char_chardb_waiting_disconnect, character->account_id, 0);
 			chclif_send_auth_result(fd,8);
 			return;
 		}

+ 8 - 0
src/common/sql.cpp

@@ -93,6 +93,14 @@ Sql* Sql_Malloc(void)
 }
 
 
+/**
+ * Retrieves the last error number.
+ * @param self : sql handle
+ * @return last error number
+ */
+unsigned int Sql_GetError( Sql* self ){
+	return mysql_errno( &self->handle );
+}
 
 static int Sql_P_Keepalive(Sql* self);
 

+ 5 - 0
src/common/sql.hpp

@@ -68,6 +68,11 @@ struct Sql* Sql_Malloc(void);
 
 
 
+/// Retrieves the last error number.
+unsigned int Sql_GetError( Sql* self );
+
+
+
 /// Establishes a connection.
 ///
 /// @return SQL_SUCCESS or SQL_ERROR

+ 88 - 1
src/login/account.cpp

@@ -14,6 +14,8 @@
 #include "../common/sql.hpp"
 #include "../common/strlib.hpp"
 
+#include "login.hpp" // login_config
+
 /// global defines
 
 /// internal structure
@@ -49,6 +51,9 @@ static bool account_db_sql_get_property(AccountDB* self, const char* key, char*
 static bool account_db_sql_set_property(AccountDB* self, const char* option, const char* value);
 static bool account_db_sql_create(AccountDB* self, struct mmo_account* acc);
 static bool account_db_sql_remove(AccountDB* self, const uint32 account_id);
+static bool account_db_sql_enable_webtoken( AccountDB* self, const uint32 account_id );
+static bool account_db_sql_disable_webtoken( AccountDB* self, const uint32 account_id );
+static bool account_db_sql_remove_webtokens( AccountDB* self );
 static bool account_db_sql_save(AccountDB* self, const struct mmo_account* acc);
 static bool account_db_sql_load_num(AccountDB* self, struct mmo_account* acc, const uint32 account_id);
 static bool account_db_sql_load_str(AccountDB* self, struct mmo_account* acc, const char* userid);
@@ -71,6 +76,9 @@ AccountDB* account_db_sql(void) {
 	db->vtable.save         = &account_db_sql_save;
 	db->vtable.create       = &account_db_sql_create;
 	db->vtable.remove       = &account_db_sql_remove;
+	db->vtable.enable_webtoken = &account_db_sql_enable_webtoken;
+	db->vtable.disable_webtoken = &account_db_sql_disable_webtoken;
+	db->vtable.remove_webtokens = &account_db_sql_remove_webtokens;
 	db->vtable.load_num     = &account_db_sql_load_num;
 	db->vtable.load_str     = &account_db_sql_load_str;
 	db->vtable.iterator     = &account_db_sql_iterator;
@@ -134,6 +142,8 @@ static bool account_db_sql_init(AccountDB* self) {
 	if( codepage[0] != '\0' && SQL_ERROR == Sql_SetEncoding(sql_handle, codepage) )
 		Sql_ShowDebug(sql_handle);
 
+	self->remove_webtokens( self );
+
 	return true;
 }
 
@@ -144,6 +154,10 @@ static bool account_db_sql_init(AccountDB* self) {
 static void account_db_sql_destroy(AccountDB* self){
 	AccountDB_SQL* db = (AccountDB_SQL*)self;
 
+	if( SQL_ERROR == Sql_Query( db->accounts, "UPDATE `%s` SET `web_auth_token` = NULL", db->account_db ) ){
+		Sql_ShowDebug( db->accounts );
+	}
+
 	Sql_Free(db->accounts);
 	db->accounts = NULL;
 	aFree(db);
@@ -483,7 +497,7 @@ static bool account_db_sql_iter_next(AccountDBIterator* self, struct mmo_account
 }
 
 /**
- * Fetch a struct mmo_account from sql.
+ * Fetch a struct mmo_account from sql, excluding web_auth_token.
  * @param db: pointer to db
  * @param acc: pointer of mmo_account to fill
  * @param account_id: id of user account to take data from
@@ -533,6 +547,7 @@ static bool mmo_auth_fromsql(AccountDB_SQL* db, struct mmo_account* acc, uint32
 	Sql_GetData(sql_handle, 17, &data, NULL); acc->old_group = atoi(data);
 #endif
 	Sql_FreeResult(sql_handle);
+	acc->web_auth_token[0] = '\0';
 
 	return true;
 }
@@ -629,6 +644,45 @@ static bool mmo_auth_tosql(AccountDB_SQL* db, const struct mmo_account* acc, boo
 		}
 	}
 
+	if( acc->sex != 'S' && login_config.use_web_auth_token ){
+		const int MAX_RETRIES = 20;
+		int i = 0;
+		bool success = false;
+
+		// Retry it for a maximum number of retries
+		do{
+			if( SQL_SUCCESS == Sql_Query( sql_handle, "UPDATE `%s` SET `web_auth_token` = LEFT( SHA2( CONCAT( UUID(), RAND() ), 256 ), %d ), `web_auth_token_enabled` = '1' WHERE `account_id` = '%d'", db->account_db, WEB_AUTH_TOKEN_LENGTH - 1, acc->account_id ) ){
+				success = true;
+				break;
+			}
+		}while( i < MAX_RETRIES && Sql_GetError( sql_handle ) == 1062 );
+
+		if( !success ){
+			if( i == MAX_RETRIES ){
+				ShowError( "Failed to generate a unique web_auth_token with %d retries...\n", i );
+			}else{
+				Sql_ShowDebug( sql_handle );
+			}
+
+			break;
+		}
+
+		char* data;
+		size_t len;
+
+		if( SQL_SUCCESS != Sql_Query( sql_handle, "SELECT `web_auth_token` from `%s` WHERE `account_id` = '%d'", db->account_db, acc->account_id ) ||
+			SQL_SUCCESS != Sql_NextRow( sql_handle ) ||
+			SQL_SUCCESS != Sql_GetData( sql_handle, 0, &data, &len )
+			){
+			Sql_ShowDebug( sql_handle );
+			break;
+		}
+
+		safestrncpy( (char *)&acc->web_auth_token, data, sizeof( acc->web_auth_token ) );
+
+		Sql_FreeResult( sql_handle );
+	}
+
 	// if we got this far, everything was successful
 	result = true;
 
@@ -829,3 +883,36 @@ void mmo_send_global_accreg(AccountDB* self, int fd, uint32 account_id, uint32 c
 
 	Sql_FreeResult(sql_handle);
 }
+
+bool account_db_sql_enable_webtoken( AccountDB* self, const uint32 account_id ){
+	AccountDB_SQL* db = (AccountDB_SQL*)self;
+
+	if( SQL_ERROR == Sql_Query( db->accounts, "UPDATE `%s` SET `web_auth_token_enabled` = '1' WHERE `account_id` = '%u'", db->account_db, account_id ) ){
+		Sql_ShowDebug( db->accounts );
+		return false;
+	}
+
+	return true;
+}
+
+bool account_db_sql_disable_webtoken( AccountDB* self, const uint32 account_id ){
+	AccountDB_SQL* db = (AccountDB_SQL*)self;
+
+	if( SQL_ERROR == Sql_Query( db->accounts, "UPDATE `%s` SET `web_auth_token_enabled` = '0' WHERE `account_id` = '%u'", db->account_db, account_id ) ){
+		Sql_ShowDebug( db->accounts );
+		return false;
+	}
+
+	return true;
+}
+
+bool account_db_sql_remove_webtokens( AccountDB* self ){
+	AccountDB_SQL* db = (AccountDB_SQL*)self;
+
+	if( SQL_ERROR == Sql_Query( db->accounts, "UPDATE `%s` SET `web_auth_token` = NULL, `web_auth_token_enabled` = '0'", db->account_db ) ){
+		Sql_ShowDebug( db->accounts );
+		return false;
+	}
+
+	return true;
+}

+ 14 - 0
src/login/account.hpp

@@ -8,6 +8,10 @@
 #include "../common/mmo.hpp" // ACCOUNT_REG2_NUM
 #include "../config/core.hpp"
 
+#ifndef WEB_AUTH_TOKEN_LENGTH
+#define WEB_AUTH_TOKEN_LENGTH 16+1
+#endif
+
 typedef struct AccountDB AccountDB;
 typedef struct AccountDBIterator AccountDBIterator;
 
@@ -32,6 +36,7 @@ struct mmo_account {
 	char birthdate[10+1];   // assigned birth date (format: YYYY-MM-DD)
 	char pincode[PINCODE_LENGTH+1];		// pincode system
 	time_t pincode_change;	// (timestamp): last time of pincode change
+	char web_auth_token[WEB_AUTH_TOKEN_LENGTH]; // web authentication token (randomized on each login)
 #ifdef VIP_ENABLE
 	int old_group;
 	time_t vip_time;
@@ -101,6 +106,15 @@ struct AccountDB {
 	/// @return true if successful
 	bool (*remove)(AccountDB* self, const uint32 account_id);
 
+	/// Enables the web auth token for the given account id
+	bool (*enable_webtoken)(AccountDB* self, const uint32 account_id);
+
+	/// Disables the web auth token for the given account id
+	bool (*disable_webtoken)(AccountDB* self, const uint32 account_id);
+
+	/// Removes the web auth token for all accounts
+	bool (*remove_webtokens)(AccountDB* self);
+
 	/// Modifies the data of an existing account.
 	/// Uses acc->account_id to identify the account.
 	///

+ 11 - 1
src/login/login.cpp

@@ -98,6 +98,8 @@ struct online_login_data* login_add_online_user(int char_server, uint32 account_
 		}
 	}
 
+	accounts->enable_webtoken( accounts, account_id );
+
 	return p;
 }
 
@@ -118,6 +120,8 @@ void login_remove_online_user(uint32 account_id) {
 		delete_timer( p->waiting_disconnect, login_waiting_disconnect_timer );
 	}
 
+	accounts->disable_webtoken( accounts, account_id );
+
 	online_db.erase( account_id );
 }
 
@@ -409,9 +413,12 @@ int login_mmo_auth(struct login_session_data* sd, bool isServer) {
 	safestrncpy(acc.last_ip, ip, sizeof(acc.last_ip));
 	acc.unban_time = 0;
 	acc.logincount++;
-
 	accounts->save(accounts, &acc);
 
+	if( login_config.use_web_auth_token ){
+		safestrncpy( sd->web_auth_token, acc.web_auth_token, WEB_AUTH_TOKEN_LENGTH );
+	}
+
 	if( sd->sex != 'S' && sd->account_id < START_ACCOUNT_NUM )
 		ShowWarning("Account %s has account id %d! Account IDs must be over %d to work properly!\n", sd->userid, sd->account_id, START_ACCOUNT_NUM);
 
@@ -637,6 +644,8 @@ bool login_config_read(const char* cfgName, bool normal) {
 			login_config.ip_sync_interval = (unsigned int)1000*60*atoi(w2); //w2 comes in minutes.
 		else if(!strcmpi(w1, "client_hash_check"))
 			login_config.client_hash_check = config_switch(w2);
+		else if(!strcmpi(w1, "use_web_auth_token"))
+			login_config.use_web_auth_token = config_switch(w2);
 		else if(!strcmpi(w1, "client_hash")) {
 			int group = 0;
 			char md5[33];
@@ -751,6 +760,7 @@ void login_set_defaults() {
 	login_config.vip_sys.char_increase = MAX_CHAR_VIP;
 	login_config.vip_sys.group = 5;
 #endif
+	login_config.use_web_auth_token = true;
 
 	//other default conf
 	safestrncpy(login_config.loginconf_name, "conf/login_athena.conf", sizeof(login_config.loginconf_name));

+ 3 - 0
src/login/login.hpp

@@ -45,6 +45,8 @@ struct login_session_data {
 	int has_client_hash;		///client ha sent an hash
 
 	int fd;				///socket of client
+
+	char web_auth_token[WEB_AUTH_TOKEN_LENGTH]; /// web authentication token
 };
 
 #define MAX_SERVERS 5 //max number of mapserv that could be attach
@@ -109,6 +111,7 @@ struct Login_Config {
 		unsigned int char_increase;					/// number of char-slot to increase in VIP state
 	} vip_sys;
 #endif
+	bool use_web_auth_token;						/// Enable web authentication token system
 };
 extern struct Login_Config login_config;
 

+ 1 - 1
src/login/loginclif.cpp

@@ -130,7 +130,7 @@ static void logclif_auth_ok(struct login_session_data* sd) {
 	WFIFOW(fd,44) = 0; // unknown
 	WFIFOB(fd,46) = sex_str2num(sd->sex);
 #if PACKETVER >= 20170315
-	memset(WFIFOP(fd,47),0,17); // Unknown
+	safestrncpy( WFIFOCP( fd, 47 ), sd->web_auth_token, WEB_AUTH_TOKEN_LENGTH ); // web authentication token
 #endif
 	for( i = 0, n = 0; i < ARRAYLENGTH(ch_server); ++i ) {
 		if( !session_isValid(ch_server[i].fd) )