unrealircd

- supernets unrealircd source & configuration
git clone git://git.acid.vegas/unrealircd.git
Log | Files | Refs | Archive | README | LICENSE

reputation.c (38555B)

      1 /*
      2  * reputation - Provides a scoring system for "known users".
      3  * (C) Copyright 2015-2019 Bram Matthys (Syzop) and the UnrealIRCd team.
      4  * License: GPLv2 or later
      5  *
      6  * How this works is simple:
      7  * Every 5 minutes the IP address of all the connected users receive
      8  * a point. Registered users receive 2 points every 5 minutes.
      9  * The total reputation score is then later used, by other modules, for
     10  * example to make decisions such as to reject or allow a user if the
     11  * server is under attack.
     12  * The reputation scores are saved in a database. By default this file
     13  * is data/reputation.db (often ~/unrealircd/data/reputation.db).
     14  *
     15  * See also https://www.unrealircd.org/docs/Connthrottle
     16  */
     17 
     18 #include "unrealircd.h"
     19 
     20 #define REPUTATION_VERSION "1.2"
     21 
     22 /* Change to #define to benchmark. Note that this will add random
     23  * reputation entries so should never be used on production servers!!!
     24  */
     25 #undef BENCHMARK
     26 #undef TEST
     27 
     28 /* Benchmark results (2GHz Xeon Skylake, compiled with -O2, Linux):
     29  * 10k random IP's with various expire times:
     30  * - load db:  23 ms
     31  * - expiry:    1 ms
     32  * - save db:   7 ms
     33  * 100k random IP's with various expire times:
     34  * - load db: 103 ms
     35  * - expiry:   10 ms
     36  * - save db:  32 ms
     37  * So, even for 100,000 unique IP's, the initial load of the database
     38  * would delay the UnrealIRCd boot process only for 0.1 second.
     39  * The writing of the db, which happens every 5 minutes, for such
     40  * amount of IP's takes 32ms (0.03 second).
     41  * Of course, exact figures will depend on the storage and cache.
     42  * That being said, the file for 100k random IP's is slightly under
     43  * 3MB, so not big, which likely means the timing will be similar
     44  * for a broad number of (storage) systems.
     45  */
     46  
     47 #ifndef TEST
     48  #define BUMP_SCORE_EVERY	300
     49  #define DELETE_OLD_EVERY	605
     50  #define SAVE_DB_EVERY		902
     51 #else
     52  #define BUMP_SCORE_EVERY 	3
     53  #define DELETE_OLD_EVERY	3
     54  #define SAVE_DB_EVERY		3
     55 #endif
     56 
     57 #ifndef CALLBACKTYPE_REPUTATION_STARTTIME
     58  #define CALLBACKTYPE_REPUTATION_STARTTIME 5
     59 #endif
     60 
     61 ModuleHeader MOD_HEADER
     62   = {
     63 	"reputation",
     64 	REPUTATION_VERSION,
     65 	"Known IP's scoring system",
     66 	"UnrealIRCd Team",
     67 	"unrealircd-6",
     68     };
     69 
     70 /* Defines */
     71 
     72 #define MAXEXPIRES 10
     73 
     74 #define REPUTATION_SCORE_CAP 10000
     75 
     76 #define UPDATE_SCORE_MARGIN 1
     77 
     78 #define REPUTATION_HASH_TABLE_SIZE 2048
     79 
     80 #define Reputation(client)	moddata_client(client, reputation_md).l
     81 
     82 #define WARN_WRITE_ERROR(fname) \
     83 	do { \
     84 		unreal_log(ULOG_ERROR, "reputation", "REPUTATION_FILE_WRITE_ERROR", NULL, \
     85 			   "[reputation] Error writing to temporary database file $filename: $system_error", \
     86 			   log_data_string("filename", fname), \
     87 			   log_data_string("system_error", unrealdb_get_error_string())); \
     88 	} while(0)
     89 
     90 #define W_SAFE(x) \
     91 	do { \
     92 		if (!(x)) { \
     93 			WARN_WRITE_ERROR(tmpfname); \
     94 			unrealdb_close(db); \
     95 			return 0; \
     96 		} \
     97 	} while(0)
     98 
     99 
    100 /* Definitions (structs, etc.) */
    101 
    102 struct cfgstruct {
    103 	int expire_score[MAXEXPIRES];
    104 	long expire_time[MAXEXPIRES];
    105 	char *database;
    106 	char *db_secret;
    107 };
    108 
    109 typedef struct ReputationEntry ReputationEntry;
    110 
    111 struct ReputationEntry {
    112 	ReputationEntry *prev, *next;
    113 	unsigned short score; /**< score for the user */
    114 	long last_seen; /**< user last seen (unix timestamp) */
    115 	int marker; /**< internal marker, not written to db */
    116 	char ip[1]; /*< ip address */
    117 };
    118 
    119 /* Global variables */
    120 
    121 static struct cfgstruct cfg; /**< Current configuration */
    122 static struct cfgstruct test; /**< Testing configuration (not active yet) */
    123 long reputation_starttime = 0;
    124 long reputation_writtentime = 0;
    125 
    126 static ReputationEntry *ReputationHashTable[REPUTATION_HASH_TABLE_SIZE];
    127 static char siphashkey_reputation[SIPHASH_KEY_LENGTH];
    128 
    129 static ModuleInfo ModInf;
    130 
    131 ModDataInfo *reputation_md; /* Module Data structure which we acquire */
    132 
    133 /* Forward declarations */
    134 void reputation_md_free(ModData *m);
    135 const char *reputation_md_serialize(ModData *m);
    136 void reputation_md_unserialize(const char *str, ModData *m);
    137 void reputation_config_setdefaults(struct cfgstruct *cfg);
    138 void reputation_free_config(struct cfgstruct *cfg);
    139 CMD_FUNC(reputation_cmd);
    140 CMD_FUNC(reputationunperm);
    141 int reputation_whois(Client *client, Client *target, NameValuePrioList **list);
    142 int reputation_set_on_connect(Client *client);
    143 int reputation_pre_lconnect(Client *client);
    144 int reputation_ip_change(Client *client, const char *oldip);
    145 int reputation_connect_extinfo(Client *client, NameValuePrioList **list);
    146 int reputation_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs);
    147 int reputation_config_run(ConfigFile *cf, ConfigEntry *ce, int type);
    148 int reputation_config_posttest(int *errs);
    149 static uint64_t hash_reputation_entry(const char *ip);
    150 ReputationEntry *find_reputation_entry(const char *ip);
    151 void add_reputation_entry(ReputationEntry *e);
    152 EVENT(delete_old_records);
    153 EVENT(add_scores);
    154 EVENT(reputation_save_db_evt);
    155 int reputation_load_db(void);
    156 int reputation_save_db(void);
    157 int reputation_starttime_callback(void);
    158 
    159 MOD_TEST()
    160 {
    161 	memcpy(&ModInf, modinfo, modinfo->size);
    162 	memset(&cfg, 0, sizeof(cfg));
    163 	memset(&test, 0, sizeof(cfg));
    164 	reputation_config_setdefaults(&test);
    165 	HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, reputation_config_test);
    166 	HookAdd(modinfo->handle, HOOKTYPE_CONFIGPOSTTEST, 0, reputation_config_posttest);
    167 	CallbackAdd(modinfo->handle, CALLBACKTYPE_REPUTATION_STARTTIME, reputation_starttime_callback);
    168 	return MOD_SUCCESS;
    169 }
    170 
    171 MOD_INIT()
    172 {
    173 	ModDataInfo mreq;
    174 
    175 	MARK_AS_OFFICIAL_MODULE(modinfo);
    176 	ModuleSetOptions(modinfo->handle, MOD_OPT_PERM, 1);
    177 
    178 	memset(&ReputationHashTable, 0, sizeof(ReputationHashTable));
    179 	siphash_generate_key(siphashkey_reputation);
    180 
    181 	memset(&mreq, 0, sizeof(mreq));
    182 	mreq.name = "reputation";
    183 	mreq.free = reputation_md_free;
    184 	mreq.serialize = reputation_md_serialize;
    185 	mreq.unserialize = reputation_md_unserialize;
    186 	mreq.sync = 0; /* local! */
    187 	mreq.type = MODDATATYPE_CLIENT;
    188 	reputation_md = ModDataAdd(modinfo->handle, mreq);
    189 	if (!reputation_md)
    190 		abort();
    191 
    192 	reputation_config_setdefaults(&cfg);
    193 	HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, reputation_config_run);
    194 	HookAdd(modinfo->handle, HOOKTYPE_WHOIS, 0, reputation_whois);
    195 	HookAdd(modinfo->handle, HOOKTYPE_HANDSHAKE, 0, reputation_set_on_connect);
    196 	HookAdd(modinfo->handle, HOOKTYPE_IP_CHANGE, 0, reputation_ip_change);
    197 	HookAdd(modinfo->handle, HOOKTYPE_PRE_LOCAL_CONNECT, 2000000000, reputation_pre_lconnect); /* (prio: last) */
    198 	HookAdd(modinfo->handle, HOOKTYPE_REMOTE_CONNECT, -1000000000, reputation_set_on_connect); /* (prio: near-first) */
    199 	HookAdd(modinfo->handle, HOOKTYPE_CONNECT_EXTINFO, 0, reputation_connect_extinfo); /* (prio: near-first) */
    200 	CommandAdd(ModInf.handle, "REPUTATION", reputation_cmd, MAXPARA, CMD_USER|CMD_SERVER);
    201 	CommandAdd(ModInf.handle, "REPUTATIONUNPERM", reputationunperm, MAXPARA, CMD_USER|CMD_SERVER);
    202 	return MOD_SUCCESS;
    203 }
    204 
    205 #ifdef BENCHMARK
    206 void reputation_benchmark(int entries)
    207 {
    208 	char ip[64];
    209 	int i;
    210 	ReputationEntry *e;
    211 
    212 	srand(1234); // fixed seed
    213 
    214 	for (i = 0; i < entries; i++)
    215 	{
    216 		ReputationEntry *e = safe_alloc(sizeof(ReputationEntry) + 64);
    217 		snprintf(e->ip, 63, "%d.%d.%d.%d", rand()%255, rand()%255, rand()%255, rand()%255);
    218 		e->score = rand()%255 + 1;
    219 		e->last_seen = TStime();
    220 		if (find_reputation_entry(e->ip))
    221 		{
    222 			safe_free(e);
    223 			continue;
    224 		}
    225 		add_reputation_entry(e);
    226 	}
    227 }
    228 #endif
    229 MOD_LOAD()
    230 {
    231 	reputation_load_db();
    232 	if (reputation_starttime == 0)
    233 		reputation_starttime = TStime();
    234 	EventAdd(ModInf.handle, "delete_old_records", delete_old_records, NULL, DELETE_OLD_EVERY*1000, 0);
    235 	EventAdd(ModInf.handle, "add_scores", add_scores, NULL, BUMP_SCORE_EVERY*1000, 0);
    236 	EventAdd(ModInf.handle, "reputation_save_db", reputation_save_db_evt, NULL, SAVE_DB_EVERY*1000, 0);
    237 #ifdef BENCHMARK
    238 	reputation_benchmark(10000);
    239 #endif
    240 	return MOD_SUCCESS;
    241 }
    242 
    243 MOD_UNLOAD()
    244 {
    245 	if (loop.terminating)
    246 		reputation_save_db();
    247 	reputation_free_config(&test);
    248 	reputation_free_config(&cfg);
    249 	return MOD_SUCCESS;
    250 }
    251 
    252 void reputation_config_setdefaults(struct cfgstruct *cfg)
    253 {
    254 	/* data/reputation.db */
    255 	safe_strdup(cfg->database, "reputation.db");
    256 	convert_to_absolute_path(&cfg->database, PERMDATADIR);
    257 
    258 	/* EXPIRES the following entries if the IP does appear for some time: */
    259 	/* <=2 points after 1 hour */
    260 	cfg->expire_score[0] = 2;
    261 #ifndef TEST
    262 	cfg->expire_time[0]   = 3600;
    263 #else
    264 	cfg->expire_time[0]   = 36;
    265 #endif
    266 	/* <=6 points after 7 days */
    267 	cfg->expire_score[1] = 6;
    268 	cfg->expire_time[1]   = 86400*7;
    269 	/* ANY result that has not been seen for 30 days */
    270 	cfg->expire_score[2] = -1;
    271 	cfg->expire_time[2]   = 86400*30;
    272 }
    273 
    274 void reputation_free_config(struct cfgstruct *cfg)
    275 {
    276 	safe_free(cfg->database);
    277 	safe_free(cfg->db_secret);
    278 }
    279 
    280 int reputation_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
    281 {
    282 	int errors = 0;
    283 	ConfigEntry *cep;
    284 
    285 	if (type != CONFIG_SET)
    286 		return 0;
    287 
    288 	/* We are only interrested in set::reputation.. */
    289 	if (!ce || strcmp(ce->name, "reputation"))
    290 		return 0;
    291 
    292 	for (cep = ce->items; cep; cep = cep->next)
    293 	{
    294 		if (!cep->value)
    295 		{
    296 			config_error("%s:%i: blank set::reputation::%s without value",
    297 				cep->file->filename, cep->line_number, cep->name);
    298 			errors++;
    299 			continue;
    300 		} else
    301 		if (!strcmp(cep->name, "database"))
    302 		{
    303 			convert_to_absolute_path(&cep->value, PERMDATADIR);
    304 			safe_strdup(test.database, cep->value);
    305 		} else
    306 		if (!strcmp(cep->name, "db-secret"))
    307 		{
    308 			const char *err;
    309 			if ((err = unrealdb_test_secret(cep->value)))
    310 			{
    311 				config_error("%s:%i: set::channeldb::db-secret: %s", cep->file->filename, cep->line_number, err);
    312 				errors++;
    313 				continue;
    314 			}
    315 			safe_strdup(test.db_secret, cep->value);
    316 		} else
    317 		{
    318 			config_error("%s:%i: unknown directive set::reputation::%s",
    319 				cep->file->filename, cep->line_number, cep->name);
    320 			errors++;
    321 			continue;
    322 		}
    323 	}
    324 
    325 	*errs = errors;
    326 	return errors ? -1 : 1;
    327 }
    328 
    329 int reputation_config_run(ConfigFile *cf, ConfigEntry *ce, int type)
    330 {
    331 	ConfigEntry *cep;
    332 
    333 	if (type != CONFIG_SET)
    334 		return 0;
    335 
    336 	/* We are only interrested in set::reputation.. */
    337 	if (!ce || strcmp(ce->name, "reputation"))
    338 		return 0;
    339 
    340 	for (cep = ce->items; cep; cep = cep->next)
    341 	{
    342 		if (!strcmp(cep->name, "database"))
    343 		{
    344 			safe_strdup(cfg.database, cep->value);
    345 		} else
    346 		if (!strcmp(cep->name, "db-secret"))
    347 		{
    348 			safe_strdup(cfg.db_secret, cep->value);
    349 		}
    350 	}
    351 	return 1;
    352 }
    353 
    354 int reputation_config_posttest(int *errs)
    355 {
    356 	int errors = 0;
    357 	char *errstr;
    358 
    359 	if (test.database && ((errstr = unrealdb_test_db(test.database, test.db_secret))))
    360 	{
    361 		config_error("[reputation] %s", errstr);
    362 		errors++;
    363 	}
    364 
    365 	*errs = errors;
    366 	return errors ? -1 : 1;
    367 }
    368 
    369 /** Parse database header and set variables appropriately */
    370 int parse_db_header_old(char *buf)
    371 {
    372 	char *header=NULL, *version=NULL, *starttime=NULL, *writtentime=NULL;
    373 	char *p=NULL;
    374 
    375 	if (strncmp(buf, "REPDB", 5))
    376 		return 0;
    377 
    378 	header = strtoken(&p, buf, " ");
    379 	if (!header)
    380 		return 0;
    381 
    382 	version = strtoken(&p, NULL, " ");
    383 	if (!version || (atoi(version) != 1))
    384 		return 0;
    385 
    386 	starttime = strtoken(&p, NULL, " ");
    387 	if (!starttime)
    388 		return 0;
    389 
    390 	writtentime = strtoken(&p, NULL, " ");
    391 	if (!writtentime)
    392 		return 0;
    393 
    394 	reputation_starttime = atol(starttime);
    395 	reputation_writtentime = atol(writtentime);
    396 
    397 	return 1;
    398 }
    399 
    400 void reputation_load_db_old(void)
    401 {
    402 	FILE *fd;
    403 	char buf[512], *p;
    404 #ifdef BENCHMARK
    405 	struct timeval tv_alpha, tv_beta;
    406 
    407 	gettimeofday(&tv_alpha, NULL);
    408 #endif
    409 
    410 	fd = fopen(cfg.database, "r");
    411 	if (!fd)
    412 	{
    413 		config_warn("WARNING: Could not open/read database '%s': %s", cfg.database, strerror(ERRNO));
    414 		return;
    415 	}
    416 
    417 	memset(buf, 0, sizeof(buf));
    418 	if (fgets(buf, 512, fd) == NULL)
    419 	{
    420 		config_error("WARNING: Database file corrupt ('%s')", cfg.database);
    421 		fclose(fd);
    422 		return;
    423 	}
    424 
    425 	/* Header contains: REPDB <version> <starttime> <writtentime>
    426 	 * Where:
    427 	 * REPDB:        Literally the string "REPDB".
    428 	 * <version>     This is version 1 at the time of this writing.
    429 	 * <starttime>   The time that recording of reputation started,
    430 	 *               in other words: when this module was first loaded, ever.
    431 	 * <writtentime> Time that the database was last written.
    432 	 */
    433 	if (!parse_db_header_old(buf))
    434 	{
    435 		config_error("WARNING: Cannot load database %s. Error reading header. "
    436 		             "Database corrupt? Or are you downgrading from a newer "
    437 		             "UnrealIRCd version perhaps? This is not supported.",
    438 		             cfg.database);
    439 		fclose(fd);
    440 		return;
    441 	}
    442 
    443 	while(fgets(buf, 512, fd) != NULL)
    444 	{
    445 		char *ip = NULL, *score = NULL, *last_seen = NULL;
    446 		ReputationEntry *e;
    447 
    448 		stripcrlf(buf);
    449 		/* Format: <ip> <score> <last seen> */
    450 		ip = strtoken(&p, buf, " ");
    451 		if (!ip)
    452 			continue;
    453 		score = strtoken(&p, NULL, " ");
    454 		if (!score)
    455 			continue;
    456 		last_seen = strtoken(&p, NULL, " ");
    457 		if (!last_seen)
    458 			continue;
    459 
    460 		e = safe_alloc(sizeof(ReputationEntry)+strlen(ip));
    461 		strcpy(e->ip, ip); /* safe, see alloc above */
    462 		e->score = atoi(score);
    463 		e->last_seen = atol(last_seen);
    464 
    465 		add_reputation_entry(e);
    466 	}
    467 	fclose(fd);
    468 
    469 #ifdef BENCHMARK
    470 	gettimeofday(&tv_beta, NULL);
    471 	unreal_log(ULOG_DEBUG, "reputation", "REPUTATION_BENCHMARK", NULL,
    472 	           "[reputation] Benchmark: LOAD DB: $time_msec microseconds",
    473 	           log_data_integer("time_msec", ((tv_beta.tv_sec - tv_alpha.tv_sec) * 1000000) + (tv_beta.tv_usec - tv_alpha.tv_usec)));
    474 #endif
    475 }
    476 
    477 #define R_SAFE(x) \
    478 	do { \
    479 		if (!(x)) { \
    480 			config_warn("[reputation] Read error from database file '%s' (possible corruption): %s", cfg.database, unrealdb_get_error_string()); \
    481 			unrealdb_close(db); \
    482 			safe_free(ip); \
    483 			return 0; \
    484 		} \
    485 	} while(0)
    486 
    487 int reputation_load_db_new(UnrealDB *db)
    488 {
    489 	uint64_t l_db_version = 0;
    490 	uint64_t l_starttime = 0;
    491 	uint64_t l_writtentime = 0;
    492 	uint64_t count = 0;
    493 	uint64_t i;
    494 	char *ip = NULL;
    495 	uint16_t score;
    496 	uint64_t last_seen;
    497 	ReputationEntry *e;
    498 #ifdef BENCHMARK
    499 	struct timeval tv_alpha, tv_beta;
    500 
    501 	gettimeofday(&tv_alpha, NULL);
    502 #endif
    503 
    504 	R_SAFE(unrealdb_read_int64(db, &l_db_version)); /* reputation db version */
    505 	if (l_db_version > 2)
    506 	{
    507 		config_error("[reputation] Reputation DB is of a newer version (%ld) than supported by us (%ld). "
    508 		             "Did you perhaps downgrade your UnrealIRCd?",
    509 		             (long)l_db_version, (long)2);
    510 		unrealdb_close(db);
    511 		return 0;
    512 	}
    513 	R_SAFE(unrealdb_read_int64(db, &l_starttime)); /* starttime of data gathering */
    514 	R_SAFE(unrealdb_read_int64(db, &l_writtentime)); /* current time */
    515 	R_SAFE(unrealdb_read_int64(db, &count)); /* number of entries */
    516 
    517 	reputation_starttime = l_starttime;
    518 	reputation_writtentime = l_writtentime;
    519 
    520 	for (i=0; i < count; i++)
    521 	{
    522 		R_SAFE(unrealdb_read_str(db, &ip));
    523 		R_SAFE(unrealdb_read_int16(db, &score));
    524 		R_SAFE(unrealdb_read_int64(db, &last_seen));
    525 
    526 		e = safe_alloc(sizeof(ReputationEntry)+strlen(ip));
    527 		strcpy(e->ip, ip); /* safe, see alloc above */
    528 		e->score = score;
    529 		e->last_seen = last_seen;
    530 		add_reputation_entry(e);
    531 		safe_free(ip);
    532 	}
    533 	unrealdb_close(db);
    534 #ifdef BENCHMARK
    535 	gettimeofday(&tv_beta, NULL);
    536 	unreal_log(ULOG_DEBUG, "reputation", "REPUTATION_BENCHMARK", NULL,
    537 	           "Reputation benchmark: LOAD DB: $time_msec microseconds",
    538 	           log_data_integer("time_msec", ((tv_beta.tv_sec - tv_alpha.tv_sec) * 1000000) + (tv_beta.tv_usec - tv_alpha.tv_usec)));
    539 #endif
    540 	return 1;
    541 }
    542 
    543 /** Load the reputation DB.
    544  * Strategy is:
    545  * 1) Check for the old header "REPDB 1", if so then call reputation_load_db_old().
    546  * 2) Otherwise, open with unrealdb routine
    547  * 3) If that fails due to a password provided but the file is unrealdb without password
    548  *    then fallback to open without a password (so users can easily upgrade to encrypted)
    549  */
    550 int reputation_load_db(void)
    551 {
    552 	FILE *fd;
    553 	UnrealDB *db;
    554 	char buf[512];
    555 
    556 	fd = fopen(cfg.database, "r");
    557 	if (!fd)
    558 	{
    559 		/* Database does not exist. Could be first boot */
    560 		config_warn("[reputation] No database present at '%s', will start a new one", cfg.database);
    561 		return 1;
    562 	}
    563 
    564 	*buf = '\0';
    565 	if (fgets(buf, sizeof(buf), fd) == NULL)
    566 	{
    567 		fclose(fd);
    568 		config_warn("[reputation] Database at '%s' is 0 bytes", cfg.database);
    569 		return 1;
    570 	}
    571 	fclose(fd);
    572 	if (!strncmp(buf, "REPDB 1 ", 8))
    573 	{
    574 		reputation_load_db_old();
    575 		return 1; /* not so good to always pretend succes */
    576 	}
    577 
    578 	/* Otherwise, it is an unrealdb, crypted or not */
    579 	db = unrealdb_open(cfg.database, UNREALDB_MODE_READ, cfg.db_secret);
    580 	if (!db)
    581 	{
    582 		if (unrealdb_get_error_code() == UNREALDB_ERROR_FILENOTFOUND)
    583 		{
    584 			/* Database does not exist. Could be first boot */
    585 			config_warn("[reputation] No database present at '%s', will start a new one", cfg.database);
    586 			return 1;
    587 		} else
    588 		if (unrealdb_get_error_code() == UNREALDB_ERROR_NOTCRYPTED)
    589 		{
    590 			db = unrealdb_open(cfg.database, UNREALDB_MODE_READ, NULL);
    591 		}
    592 		if (!db)
    593 		{
    594 			config_error("[reputation] Unable to open the database file '%s' for reading: %s", cfg.database, unrealdb_get_error_string());
    595 			return 0;
    596 		}
    597 	}
    598 	return reputation_load_db_new(db);
    599 }
    600 
    601 int reputation_save_db_old(void)
    602 {
    603 	FILE *fd;
    604 	char tmpfname[512];
    605 	int i;
    606 	ReputationEntry *e;
    607 #ifdef BENCHMARK
    608 	struct timeval tv_alpha, tv_beta;
    609 
    610 	gettimeofday(&tv_alpha, NULL);
    611 #endif
    612 
    613 	/* We write to a temporary file. Only to rename it later if everything was ok */
    614 	snprintf(tmpfname, sizeof(tmpfname), "%s.%x.tmp", cfg.database, getrandom32());
    615 
    616 	fd = fopen(tmpfname, "w");
    617 	if (!fd)
    618 	{
    619 		config_error("ERROR: Could not open/write database '%s': %s -- DATABASE *NOT* SAVED!!!", tmpfname, strerror(ERRNO));
    620 		return 0;
    621 	}
    622 
    623 	if (fprintf(fd, "REPDB 1 %lld %lld\n", (long long)reputation_starttime, (long long)TStime()) < 0)
    624 		goto write_fail;
    625 
    626 	for (i = 0; i < REPUTATION_HASH_TABLE_SIZE; i++)
    627 	{
    628 		for (e = ReputationHashTable[i]; e; e = e->next)
    629 		{
    630 			if (fprintf(fd, "%s %d %lld\n", e->ip, (int)e->score, (long long)e->last_seen) < 0)
    631 			{
    632 write_fail:
    633 				config_error("ERROR writing to '%s': %s -- DATABASE *NOT* SAVED!!!", tmpfname, strerror(ERRNO));
    634 				fclose(fd);
    635 				return 0;
    636 			}
    637 		}
    638 	}
    639 
    640 	if (fclose(fd) < 0)
    641 	{
    642 		config_error("ERROR writing to '%s': %s -- DATABASE *NOT* SAVED!!!", tmpfname, strerror(ERRNO));
    643 		return 0;
    644 	}
    645 
    646 	/* Everything went fine. We rename our temporary file to the existing
    647 	 * DB file (will overwrite), which is more or less an atomic operation.
    648 	 */
    649 #ifdef _WIN32
    650 	/* The rename operation cannot be atomic on Windows as it will cause a "file exists" error */
    651 	unlink(cfg.database);
    652 #endif
    653 	if (rename(tmpfname, cfg.database) < 0)
    654 	{
    655 		config_error("ERROR renaming '%s' to '%s': %s -- DATABASE *NOT* SAVED!!!",
    656 			tmpfname, cfg.database, strerror(ERRNO));
    657 		return 0;
    658 	}
    659 
    660 	reputation_writtentime = TStime();
    661 
    662 #ifdef BENCHMARK
    663 	gettimeofday(&tv_beta, NULL);
    664 	unreal_log(ULOG_DEBUG, "reputation", "REPUTATION_BENCHMARK", NULL,
    665 	           "Reputation benchmark: SAVE DB: $time_msec microseconds",
    666 	           log_data_integer("time_msec", ((tv_beta.tv_sec - tv_alpha.tv_sec) * 1000000) + (tv_beta.tv_usec - tv_alpha.tv_usec)));
    667 #endif
    668 
    669 	return 1;
    670 }
    671 
    672 int reputation_save_db(void)
    673 {
    674 	UnrealDB *db;
    675 	char tmpfname[512];
    676 	int i;
    677 	uint64_t count;
    678 	ReputationEntry *e;
    679 #ifdef BENCHMARK
    680 	struct timeval tv_alpha, tv_beta;
    681 
    682 	gettimeofday(&tv_alpha, NULL);
    683 #endif
    684 
    685 #ifdef TEST
    686 	unreal_log(ULOG_DEBUG, "reputation", "REPUTATION_TEST", NULL, "Reputation in running in test mode. Saving DB's....");
    687 #endif
    688 
    689 	/* Comment this out after one or more releases (means you cannot downgrade to <=5.0.9.1 anymore) */
    690 	if (cfg.db_secret == NULL)
    691 		return reputation_save_db_old();
    692 
    693 	/* We write to a temporary file. Only to rename it later if everything was ok */
    694 	snprintf(tmpfname, sizeof(tmpfname), "%s.%x.tmp", cfg.database, getrandom32());
    695 
    696 	db = unrealdb_open(tmpfname, UNREALDB_MODE_WRITE, cfg.db_secret);
    697 	if (!db)
    698 	{
    699 		WARN_WRITE_ERROR(tmpfname);
    700 		return 0;
    701 	}
    702 
    703 	/* Write header */
    704 	W_SAFE(unrealdb_write_int64(db, 2)); /* reputation db version */
    705 	W_SAFE(unrealdb_write_int64(db, reputation_starttime)); /* starttime of data gathering */
    706 	W_SAFE(unrealdb_write_int64(db, TStime())); /* current time */
    707 
    708 	/* Count entries */
    709 	count = 0;
    710 	for (i = 0; i < REPUTATION_HASH_TABLE_SIZE; i++)
    711 		for (e = ReputationHashTable[i]; e; e = e->next)
    712 			count++;
    713 	W_SAFE(unrealdb_write_int64(db, count)); /* Number of DB entries */
    714 
    715 	/* Now write the actual individual entries: */
    716 	for (i = 0; i < REPUTATION_HASH_TABLE_SIZE; i++)
    717 	{
    718 		for (e = ReputationHashTable[i]; e; e = e->next)
    719 		{
    720 			W_SAFE(unrealdb_write_str(db, e->ip));
    721 			W_SAFE(unrealdb_write_int16(db, e->score));
    722 			W_SAFE(unrealdb_write_int64(db, e->last_seen));
    723 		}
    724 	}
    725 
    726 	if (!unrealdb_close(db))
    727 	{
    728 		WARN_WRITE_ERROR(tmpfname);
    729 		return 0;
    730 	}
    731 
    732 	/* Everything went fine. We rename our temporary file to the existing
    733 	 * DB file (will overwrite), which is more or less an atomic operation.
    734 	 */
    735 #ifdef _WIN32
    736 	/* The rename operation cannot be atomic on Windows as it will cause a "file exists" error */
    737 	unlink(cfg.database);
    738 #endif
    739 	if (rename(tmpfname, cfg.database) < 0)
    740 	{
    741 		config_error("ERROR renaming '%s' to '%s': %s -- DATABASE *NOT* SAVED!!!",
    742 			tmpfname, cfg.database, strerror(ERRNO));
    743 		return 0;
    744 	}
    745 
    746 	reputation_writtentime = TStime();
    747 
    748 #ifdef BENCHMARK
    749 	gettimeofday(&tv_beta, NULL);
    750 	unreal_log(ULOG_DEBUG, "reputation", "REPUTATION_BENCHMARK", NULL,
    751 	           "Reputation benchmark: SAVE DB: $time_msec microseconds",
    752 	           log_data_integer("time_msec", ((tv_beta.tv_sec - tv_alpha.tv_sec) * 1000000) + (tv_beta.tv_usec - tv_alpha.tv_usec)));
    753 #endif
    754 	return 1;
    755 }
    756 
    757 static uint64_t hash_reputation_entry(const char *ip)
    758 {
    759 	return siphash(ip, siphashkey_reputation) % REPUTATION_HASH_TABLE_SIZE;
    760 }
    761 
    762 void add_reputation_entry(ReputationEntry *e)
    763 {
    764 	int hashv = hash_reputation_entry(e->ip);
    765 
    766 	AddListItem(e, ReputationHashTable[hashv]);
    767 }
    768 
    769 ReputationEntry *find_reputation_entry(const char *ip)
    770 {
    771 	ReputationEntry *e;
    772 	int hashv = hash_reputation_entry(ip);
    773 
    774 	for (e = ReputationHashTable[hashv]; e; e = e->next)
    775 		if (!strcmp(e->ip, ip))
    776 			return e;
    777 
    778 	return NULL;
    779 }
    780 
    781 int reputation_lookup_score_and_set(Client *client)
    782 {
    783 	char *ip = client->ip;
    784 	ReputationEntry *e;
    785 
    786 	Reputation(client) = 0; /* (re-)set to zero (yes, important!) */
    787 	if (ip)
    788 	{
    789 		e = find_reputation_entry(ip);
    790 		if (e)
    791 		{
    792 			Reputation(client) = e->score; /* SET MODDATA */
    793 		}
    794 	}
    795 	return Reputation(client);
    796 }
    797 
    798 /** Called when the user connects.
    799  * Locally: very early, just after the TCP/IP connection has
    800  * been established, before any data.
    801  * Remote user: early in the HOOKTYPE_REMOTE_CONNECT hook.
    802  */
    803 int reputation_set_on_connect(Client *client)
    804 {
    805 	reputation_lookup_score_and_set(client);
    806 	return 0;
    807 }
    808 
    809 int reputation_ip_change(Client *client, const char *oldip)
    810 {
    811 	reputation_lookup_score_and_set(client);
    812 	return 0;
    813 }
    814 
    815 int reputation_pre_lconnect(Client *client)
    816 {
    817 	/* User will likely be accepted. Inform other servers about the score
    818 	 * we have for this user. For more information about this type of
    819 	 * server to server traffic, see the reputation_server_cmd function.
    820 	 *
    821 	 * Note that we use reputation_lookup_score_and_set() here
    822 	 * and not Reputation(client) because we want to RE-LOOKUP
    823 	 * the score for the IP in the database. We do this because
    824 	 * between reputation_set_on_connect() and reputation_pre_lconnect()
    825 	 * the IP of the user may have been changed due to IP-spoofing
    826 	 * (WEBIRC).
    827 	 */
    828 	int score = reputation_lookup_score_and_set(client);
    829 
    830 	sendto_server(NULL, 0, 0, NULL, ":%s REPUTATION %s %d", me.id, GetIP(client), score);
    831 
    832 	return 0;
    833 }
    834 
    835 EVENT(add_scores)
    836 {
    837 	static int marker = 0;
    838 	char *ip;
    839 	Client *client;
    840 	ReputationEntry *e;
    841 
    842 	/* This marker is used so we only bump score for an IP entry
    843 	 * once and not twice (or more) if there are multiple users
    844 	 * with the same IP address.
    845 	 */
    846 	marker += 2;
    847 
    848 	/* These macros make the code below easier to read. Also,
    849 	 * this explains why we just did marker+=2 and not marker++.
    850 	 */
    851 	#define MARKER_UNREGISTERED_USER (marker)
    852 	#define MARKER_REGISTERED_USER (marker+1)
    853 
    854 	list_for_each_entry(client, &client_list, client_node)
    855 	{
    856 		if (!IsUser(client))
    857 			continue; /* skip servers, unknowns, etc.. */
    858 
    859 		ip = client->ip;
    860 		if (!ip)
    861 			continue;
    862 
    863 		e = find_reputation_entry(ip);
    864 		if (!e)
    865 		{
    866 			/* Create */
    867 			e = safe_alloc(sizeof(ReputationEntry)+strlen(ip));
    868 			strcpy(e->ip, ip); /* safe, allocated above */
    869 			add_reputation_entry(e);
    870 		}
    871 
    872 		/* If this is not a duplicate entry, then bump the score.. */
    873 		if ((e->marker != MARKER_UNREGISTERED_USER) && (e->marker != MARKER_REGISTERED_USER))
    874 		{
    875 			e->marker = MARKER_UNREGISTERED_USER;
    876 			if (e->score < REPUTATION_SCORE_CAP)
    877 			{
    878 				/* Regular users receive a point. */
    879 				e->score++;
    880 				/* Registered users receive an additional point */
    881 				if (IsLoggedIn(client) && (e->score < REPUTATION_SCORE_CAP))
    882 				{
    883 					e->score++;
    884 					e->marker = MARKER_REGISTERED_USER;
    885 				}
    886 			}
    887 		} else
    888 		if ((e->marker == MARKER_UNREGISTERED_USER) && IsLoggedIn(client) && (e->score < REPUTATION_SCORE_CAP))
    889 		{
    890 			/* This is to catch a special case:
    891 			 * If there are 2 or more users with the same IP
    892 			 * address and the first user was not registered
    893 			 * then the IP entry only received a score bump of +1.
    894 			 * If the 2nd user (with same IP) is a registered
    895 			 * user then the IP should actually receive a
    896 			 * score bump of +2 (in total).
    897 			 */
    898 			e->score++;
    899 			e->marker = MARKER_REGISTERED_USER;
    900 		}
    901 
    902 		e->last_seen = TStime();
    903 		Reputation(client) = e->score; /* update moddata */
    904 	}
    905 }
    906 
    907 /** Is this entry expired? */
    908 static inline int is_reputation_expired(ReputationEntry *e)
    909 {
    910 	int i;
    911 	for (i = 0; i < MAXEXPIRES; i++)
    912 	{
    913 		if (cfg.expire_time[i] == 0)
    914 			break; /* end of all entries */
    915 		if ((e->score <= cfg.expire_score[i]) && (TStime() - e->last_seen > cfg.expire_time[i]))
    916 			return 1;
    917 	}
    918 	return 0;
    919 }
    920 
    921 /** If the reputation changed (due to server syncing) then update the
    922  * individual users reputation score as well.
    923  */
    924 void reputation_changed_update_users(ReputationEntry *e)
    925 {
    926 	Client *client;
    927 
    928 	list_for_each_entry(client, &client_list, client_node)
    929 	{
    930 		if (client->ip && !strcmp(e->ip, client->ip))
    931 		{
    932 			/* With some (possibly unneeded) care to only go forward */
    933 			if (Reputation(client) < e->score)
    934 				Reputation(client) = e->score;
    935 		}
    936 	}
    937 }
    938 
    939 EVENT(delete_old_records)
    940 {
    941 	int i;
    942 	ReputationEntry *e, *e_next;
    943 #ifdef BENCHMARK
    944 	struct timeval tv_alpha, tv_beta;
    945 
    946 	gettimeofday(&tv_alpha, NULL);
    947 #endif
    948 
    949 	for (i = 0; i < REPUTATION_HASH_TABLE_SIZE; i++)
    950 	{
    951 		for (e = ReputationHashTable[i]; e; e = e_next)
    952 		{
    953 			e_next = e->next;
    954 
    955 			if (is_reputation_expired(e))
    956 			{
    957 #ifdef DEBUGMODE
    958 				unreal_log(ULOG_DEBUG, "reputation", "REPUTATION_EXPIRY", NULL,
    959 				           "Deleting expired entry for $ip (score $score, last seen $time_delta seconds ago)",
    960 				           log_data_string("ip", e->ip),
    961 				           log_data_integer("score", e->score),
    962 				           log_data_integer("time_delta", TStime() - e->last_seen));
    963 #endif
    964 				DelListItem(e, ReputationHashTable[i]);
    965 				safe_free(e);
    966 			}
    967 		}
    968 	}
    969 
    970 #ifdef BENCHMARK
    971 	gettimeofday(&tv_beta, NULL);
    972 	unreal_log(ULOG_DEBUG, "reputation", "REPUTATION_BENCHMARK", NULL,
    973 	           "Reputation benchmark: EXPIRY IN MEM: $time_msec microseconds",
    974 	           log_data_integer("time_msec", ((tv_beta.tv_sec - tv_alpha.tv_sec) * 1000000) + (tv_beta.tv_usec - tv_alpha.tv_usec)));
    975 #endif
    976 }
    977 
    978 EVENT(reputation_save_db_evt)
    979 {
    980 	reputation_save_db();
    981 }
    982 
    983 CMD_FUNC(reputationunperm)
    984 {
    985 	if (!IsOper(client))
    986 	{
    987 		sendnumeric(client, ERR_NOPRIVILEGES);
    988 		return;
    989 	}
    990 
    991 	ModuleSetOptions(ModInf.handle, MOD_OPT_PERM, 0);
    992 
    993 	unreal_log(ULOG_INFO, "reputation", "REPUTATIONUNPERM_COMMAND", client,
    994 	           "$client used /REPUTATIONUNPERM. On next REHASH the module can be RELOADED or UNLOADED. "
    995 	           "Note however that for a few minutes the scoring may be skipped, so don't do this too often.");
    996 }
    997 
    998 int reputation_connect_extinfo(Client *client, NameValuePrioList **list)
    999 {
   1000 	add_fmt_nvplist(list, 0, "reputation", "%d", GetReputation(client));
   1001 	return 0;
   1002 }
   1003 
   1004 int count_reputation_records(void)
   1005 {
   1006 	int i;
   1007 	ReputationEntry *e;
   1008 	int total = 0;
   1009 
   1010 	for (i = 0; i < REPUTATION_HASH_TABLE_SIZE; i++)
   1011 		for (e = ReputationHashTable[i]; e; e = e->next)
   1012 			total++;
   1013 
   1014 	return total;
   1015 }
   1016 
   1017 void reputation_channel_query(Client *client, Channel *channel)
   1018 {
   1019 	Member *m;
   1020 	char buf[512];
   1021 	char tbuf[256];
   1022 	char **nicks;
   1023 	int *scores;
   1024 	int cnt = 0, i, j;
   1025 	ReputationEntry *e;
   1026 
   1027 	sendtxtnumeric(client, "Users and reputation scores for %s:", channel->name);
   1028 
   1029 	/* Step 1: build a list of nicks and their reputation */
   1030 	nicks = safe_alloc((channel->users+1) * sizeof(char *));
   1031 	scores = safe_alloc((channel->users+1) * sizeof(int));
   1032 	for (m = channel->members; m; m = m->next)
   1033 	{
   1034 		nicks[cnt] = m->client->name;
   1035 		if (m->client->ip)
   1036 		{
   1037 			e = find_reputation_entry(m->client->ip);
   1038 			if (e)
   1039 				scores[cnt] = e->score;
   1040 		}
   1041 		if (++cnt > channel->users)
   1042 		{
   1043 			unreal_log(ULOG_WARNING, "bug", "REPUTATION_CHANNEL_QUERY_BUG", client,
   1044 				   "[BUG] reputation_channel_query() expected $expected_users users, but $found_users (or more) users were present in $channel",
   1045 				   log_data_integer("expected_users", channel->users),
   1046 				   log_data_integer("found_users", cnt),
   1047 				   log_data_string("channel", channel->name));
   1048 #ifdef DEBUGMODE
   1049 			abort();
   1050 #endif
   1051 			break; /* safety net */
   1052 		}
   1053 	}
   1054 
   1055 	/* Step 2: lazy selection sort */
   1056 	for (i = 0; i < cnt && nicks[i]; i++)
   1057 	{
   1058 		for (j = i+1; j < cnt && nicks[j]; j++)
   1059 		{
   1060 			if (scores[i] < scores[j])
   1061 			{
   1062 				char *nick_tmp;
   1063 				int score_tmp;
   1064 				nick_tmp = nicks[i];
   1065 				score_tmp = scores[i];
   1066 				nicks[i] = nicks[j];
   1067 				scores[i] = scores[j];
   1068 				nicks[j] = nick_tmp;
   1069 				scores[j] = score_tmp;
   1070 			}
   1071 		}
   1072 	}
   1073 
   1074 	/* Step 3: send the (ordered) list to the user */
   1075 	*buf = '\0';
   1076 	for (i = 0; i < cnt && nicks[i]; i++)
   1077 	{
   1078 		snprintf(tbuf, sizeof(tbuf), "%s\00314(%d)\003 ", nicks[i], scores[i]);
   1079 		if ((strlen(tbuf)+strlen(buf) > 400) || !nicks[i+1])
   1080 		{
   1081 			sendtxtnumeric(client, "%s%s", buf, tbuf);
   1082 			*buf = '\0';
   1083 		} else {
   1084 			strlcat(buf, tbuf, sizeof(buf));
   1085 		}
   1086 	}
   1087 	sendtxtnumeric(client, "End of list.");
   1088 	safe_free(nicks);
   1089 	safe_free(scores);
   1090 }
   1091 
   1092 void reputation_list_query(Client *client, int maxscore)
   1093 {
   1094 	Client *target;
   1095 	ReputationEntry *e;
   1096 
   1097 	sendtxtnumeric(client, "Users and reputation scores <%d:", maxscore);
   1098 
   1099 	list_for_each_entry(target, &client_list, client_node)
   1100 	{
   1101 		int score = 0;
   1102 
   1103 		if (!IsUser(target) || IsULine(target) || !target->ip)
   1104 			continue;
   1105 
   1106 		e = find_reputation_entry(target->ip);
   1107 		if (e)
   1108 			score = e->score;
   1109 		if (score >= maxscore)
   1110 			continue;
   1111 		sendtxtnumeric(client, "%s!%s@%s [%s] \017(score: %d)",
   1112 			target->name,
   1113 			target->user->username,
   1114 			target->user->realhost,
   1115 			target->ip,
   1116 			score);
   1117 	}
   1118 	sendtxtnumeric(client, "End of list.");
   1119 }
   1120 
   1121 CMD_FUNC(reputation_user_cmd)
   1122 {
   1123 	ReputationEntry *e;
   1124 	const char *ip;
   1125 
   1126 	if (!IsOper(client))
   1127 	{
   1128 		sendnumeric(client, ERR_NOPRIVILEGES);
   1129 		return;
   1130 	}
   1131 
   1132 	if ((parc < 2) || BadPtr(parv[1]))
   1133 	{
   1134 		sendnotice(client, "Reputation module statistics:");
   1135 		sendnotice(client, "Recording for: %lld seconds (since unixtime %lld)",
   1136 			(long long)(TStime() - reputation_starttime),
   1137 			(long long)reputation_starttime);
   1138 		if (reputation_writtentime)
   1139 		{
   1140 			sendnotice(client, "Last successful db write: %lld seconds ago (unixtime %lld)",
   1141 				(long long)(TStime() - reputation_writtentime),
   1142 				(long long)reputation_writtentime);
   1143 		} else {
   1144 			sendnotice(client, "Last successful db write: never");
   1145 		}
   1146 		sendnotice(client, "Current number of records (IP's): %d", count_reputation_records());
   1147 		sendnotice(client, "-");
   1148 		sendnotice(client, "Available commands:");
   1149 		sendnotice(client, "/REPUTATION [nick]     Show reputation info about nick name");
   1150 		sendnotice(client, "/REPUTATION [ip]       Show reputation info about IP address");
   1151 		sendnotice(client, "/REPUTATION [channel]  List users in channel along with their reputation score");
   1152 		sendnotice(client, "/REPUTATION <NN        List users with reputation score below value NN");
   1153 		return;
   1154 	}
   1155 
   1156 	if (strchr(parv[1], '.') || strchr(parv[1], ':'))
   1157 	{
   1158 		ip = parv[1];
   1159 	} else
   1160 	if (parv[1][0] == '#')
   1161 	{
   1162 		Channel *channel = find_channel(parv[1]);
   1163 		if (!channel)
   1164 		{
   1165 			sendnumeric(client, ERR_NOSUCHCHANNEL, parv[1]);
   1166 			return;
   1167 		}
   1168 		/* corner case: ircop without proper permissions and not in channel */
   1169 		if (!ValidatePermissionsForPath("channel:see:names:invisible",client,NULL,NULL,NULL) && !IsMember(client,channel))
   1170 		{
   1171 			sendnumeric(client, ERR_NOTONCHANNEL, channel->name);
   1172 			return;
   1173 		}
   1174 		reputation_channel_query(client, channel);
   1175 		return;
   1176 	} else
   1177 	if (parv[1][0] == '<')
   1178 	{
   1179 		int max = atoi(parv[1] + 1);
   1180 		if (max < 1)
   1181 		{
   1182 			sendnotice(client, "REPUTATION: Invalid search value specified. Use for example '/REPUTATION <5' to search on less-than-five");
   1183 			return;
   1184 		}
   1185 		reputation_list_query(client, max);
   1186 		return;
   1187 	} else {
   1188 		Client *target = find_user(parv[1], NULL);
   1189 		if (!target)
   1190 		{
   1191 			sendnumeric(client, ERR_NOSUCHNICK, parv[1]);
   1192 			return;
   1193 		}
   1194 		ip = target->ip;
   1195 		if (!ip)
   1196 		{
   1197 			sendnotice(client, "No IP address information available for user '%s'.", parv[1]); /* e.g. services */
   1198 			return;
   1199 		}
   1200 	}
   1201 
   1202 	e = find_reputation_entry(ip);
   1203 	if (!e)
   1204 	{
   1205 		sendnotice(client, "No reputation record found for IP %s", ip);
   1206 		return;
   1207 	}
   1208 
   1209 	sendnotice(client, "****************************************************");
   1210 	sendnotice(client, "Reputation record for IP %s:", ip);
   1211 	sendnotice(client, "    Score: %hd", e->score);
   1212 	sendnotice(client, "Last seen: %lld seconds ago (unixtime: %lld)",
   1213 		(long long)(TStime() - e->last_seen),
   1214 		(long long)e->last_seen);
   1215 	sendnotice(client, "****************************************************");
   1216 }
   1217 
   1218 /** The REPUTATION server command handler.
   1219  * Syntax: :server REPUTATION <ip> <score>
   1220  * Where the <score> may be prefixed by an asterisk (*).
   1221  *
   1222  * The best way to explain this command is to illustrate by example:
   1223  * :servera REPUTATION 1.2.3.4 0
   1224  * Then serverb, which might have a score of 2 for this IP, will:
   1225  * - Send back to the servera direction:  :serverb REPUTATION 1.2.3.4 *2
   1226  *   So the original server (and direction) receive a score update.
   1227  * - Propagate to non-servera direction: :servera REPUTATION 1.2.3.4 2
   1228  *   So use the new higher score (2 rather than 0).
   1229  * Then the next server may do the same. It MUST propagate to non-serverb
   1230  * direction and MAY (again) update the score even higher.
   1231  *
   1232  * If the score is not prefixed by * then the server may do as above and
   1233  * send back to the uplink an "update" of the score. If, however, the
   1234  * score is prefixed by * then the server will NEVER send back to the
   1235  * uplink, it may only propagate. This is to prevent loops.
   1236  *
   1237  * Note that some margin is used when deciding if the server should send
   1238  * back score updates. This is defined by UPDATE_SCORE_MARGIN.
   1239  * If this is for example set to 1 then a point difference of 1 will not
   1240  * yield a score update since such a minor score update is not worth the
   1241  * server to server traffic. Also, due to timing differences a score
   1242  * difference of 1 is quite likely to hapen in normal circumstances.
   1243  */
   1244 CMD_FUNC(reputation_server_cmd)
   1245 {
   1246 	ReputationEntry *e;
   1247 	const char *ip;
   1248 	int score;
   1249 	int allow_reply;
   1250 
   1251 	/* :server REPUTATION <ip> <score> */
   1252 	if ((parc < 3) || BadPtr(parv[2]))
   1253 	{
   1254 		sendnumeric(client, ERR_NEEDMOREPARAMS, "REPUTATION");
   1255 		return;
   1256 	}
   1257 
   1258 	ip = parv[1];
   1259 
   1260 	if (parv[2][0] == '*')
   1261 	{
   1262 		allow_reply = 0;
   1263 		score = atoi(parv[2]+1);
   1264 	} else {
   1265 		allow_reply = 1;
   1266 		score = atoi(parv[2]);
   1267 	}
   1268 
   1269 	if (score > REPUTATION_SCORE_CAP)
   1270 		score = REPUTATION_SCORE_CAP;
   1271 
   1272 	e = find_reputation_entry(ip);
   1273 	if (allow_reply && e && (e->score > score) && (e->score - score > UPDATE_SCORE_MARGIN))
   1274 	{
   1275 		/* We have a higher score, inform the client direction about it.
   1276 		 * This will prefix the score with a * so servers will never reply to it.
   1277 		 */
   1278 		sendto_one(client, NULL, ":%s REPUTATION %s *%d", me.id, parv[1], e->score);
   1279 #ifdef DEBUGMODE
   1280 		unreal_log(ULOG_DEBUG, "reputation", "REPUTATION_DIFFERS", client,
   1281 			   "Reputation score for for $ip from $client is $their_score, but we have $score, sending back $score",
   1282 			   log_data_string("ip", ip),
   1283 			   log_data_integer("their_score", score),
   1284 			   log_data_integer("score", e->score));
   1285 #endif
   1286 		score = e->score; /* Update for propagation in the non-client direction */
   1287 	}
   1288 
   1289 	/* Update our score if sender has a higher score */
   1290 	if (e && (score > e->score))
   1291 	{
   1292 #ifdef DEBUGMODE
   1293 		unreal_log(ULOG_DEBUG, "reputation", "REPUTATION_DIFFERS", client,
   1294 			   "Reputation score for for $ip from $client is $their_score, but we have $score, updating our score to $score",
   1295 			   log_data_string("ip", ip),
   1296 			   log_data_integer("their_score", score),
   1297 			   log_data_integer("score", e->score));
   1298 #endif
   1299 		e->score = score;
   1300 		reputation_changed_update_users(e);
   1301 	}
   1302 
   1303 	/* If we don't have any entry for this IP, add it now. */
   1304 	if (!e && (score > 0))
   1305 	{
   1306 #ifdef DEBUGMODE
   1307 		unreal_log(ULOG_DEBUG, "reputation", "REPUTATION_NEW", client,
   1308 			   "Reputation score for for $ip from $client is $their_score, we had no entry, adding it",
   1309 			   log_data_string("ip", ip),
   1310 			   log_data_integer("their_score", score),
   1311 			   log_data_integer("score", 0));
   1312 #endif
   1313 		e = safe_alloc(sizeof(ReputationEntry)+strlen(ip));
   1314 		strcpy(e->ip, ip); /* safe, see alloc above */
   1315 		e->score = score;
   1316 		e->last_seen = TStime();
   1317 		add_reputation_entry(e);
   1318 		reputation_changed_update_users(e);
   1319 	}
   1320 
   1321 	/* Propagate to the non-client direction (score may be updated) */
   1322 	sendto_server(client, 0, 0, NULL,
   1323 	              ":%s REPUTATION %s %s%d",
   1324 	              client->id,
   1325 	              parv[1],
   1326 	              allow_reply ? "" : "*",
   1327 	              score);
   1328 }
   1329 
   1330 CMD_FUNC(reputation_cmd)
   1331 {
   1332 	if (MyUser(client))
   1333 		CALL_CMD_FUNC(reputation_user_cmd);
   1334 	else if (IsServer(client) || IsMe(client))
   1335 		CALL_CMD_FUNC(reputation_server_cmd);
   1336 }
   1337 
   1338 int reputation_whois(Client *client, Client *target, NameValuePrioList **list)
   1339 {
   1340 	int reputation;
   1341 
   1342 	if (whois_get_policy(client, target, "reputation") != WHOIS_CONFIG_DETAILS_FULL)
   1343 		return 0;
   1344 
   1345 	reputation = Reputation(target);
   1346 	if (reputation > 0)
   1347 	{
   1348 		add_nvplist_numeric_fmt(list, 0, "reputation", client, RPL_WHOISSPECIAL,
   1349 		                        "%s :is using an IP with a reputation score of %d",
   1350 		                        target->name, reputation);
   1351 	}
   1352 	return 0;
   1353 }
   1354 
   1355 void reputation_md_free(ModData *m)
   1356 {
   1357 	/* we have nothing to free actually, but we must set to zero */
   1358 	m->l = 0;
   1359 }
   1360 
   1361 const char *reputation_md_serialize(ModData *m)
   1362 {
   1363 	static char buf[32];
   1364 	if (m->i == 0)
   1365 		return NULL; /* not set (reputation always starts at 1) */
   1366 	snprintf(buf, sizeof(buf), "%d", m->i);
   1367 	return buf;
   1368 }
   1369 
   1370 void reputation_md_unserialize(const char *str, ModData *m)
   1371 {
   1372 	m->i = atoi(str);
   1373 }
   1374 
   1375 int reputation_starttime_callback(void)
   1376 {
   1377 	/* NB: fix this by 2038 */
   1378 	return (int)reputation_starttime;
   1379 }