diff --git a/SQL/paradise_schema.sql b/SQL/paradise_schema.sql index 0379517190f..e0f33226e8c 100644 --- a/SQL/paradise_schema.sql +++ b/SQL/paradise_schema.sql @@ -266,6 +266,8 @@ CREATE TABLE `player` ( `exp` mediumtext, `clientfps` smallint(4) DEFAULT '0', `atklog` smallint(4) DEFAULT '0', + `fuid` BIGINT(20) NULL DEFAULT NULL, + `fupdate` SMALLINT(4) NULL DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `ckey` (`ckey`) ) ENGINE=InnoDB AUTO_INCREMENT=32446 DEFAULT CHARSET=latin1; @@ -523,15 +525,27 @@ CREATE TABLE `memo` ( /*!40101 SET character_set_client = @saved_cs_client */; -- --- Table structure for table `discord` +-- Table structure for table `ipintel` -- -DROP TABLE IF EXISTS `discord`; +DROP TABLE IF EXISTS `ipintel`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; -CREATE TABLE IF NOT EXISTS `discord` ( - `ckey` varchar(32) NOT NULL, - `discord_id` bigint(20) NOT NULL, - `notify` int(11) NOT NULL, - PRIMARY KEY (`ckey`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; +CREATE TABLE `ipintel` ( +`ip` INT UNSIGNED NOT NULL , +`date` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL , +`intel` REAL NOT NULL DEFAULT '0', +PRIMARY KEY ( `ip` ) +) ENGINE = INNODB; /*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `vpn_whitelist` +-- +DROP TABLE IF EXISTS `vpn_whitelist`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `vpn_whitelist` ( + `ckey` VARCHAR(32) NOT NULL, + `reason` text + PRIMARY KEY (`ckey`) +) ENGINE=INNODB; diff --git a/SQL/paradise_schema_prefixed.sql b/SQL/paradise_schema_prefixed.sql index e9a622cf037..ecb28a867e4 100644 --- a/SQL/paradise_schema_prefixed.sql +++ b/SQL/paradise_schema_prefixed.sql @@ -265,6 +265,8 @@ CREATE TABLE `SS13_player` ( `exp` mediumtext, `clientfps` smallint(4) DEFAULT '0', `atklog` smallint(4) DEFAULT '0', + `fuid` BIGINT(20) NULL DEFAULT NULL, + `fupdate` SMALLINT(4) NULL DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `ckey` (`ckey`) ) ENGINE=InnoDB AUTO_INCREMENT=32446 DEFAULT CHARSET=latin1; @@ -522,15 +524,27 @@ CREATE TABLE `SS13_memo` ( /*!40101 SET character_set_client = @saved_cs_client */; -- --- Table structure for table `SS13_discord` +-- Table structure for table `SS13_ipintel` -- -DROP TABLE IF EXISTS `SS13_discord`; +DROP TABLE IF EXISTS `SS13_ipintel`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; -CREATE TABLE IF NOT EXISTS `SS13_discord` ( - `ckey` varchar(32) NOT NULL, - `discord_id` bigint(20) NOT NULL, - `notify` int(11) NOT NULL, - PRIMARY KEY (`ckey`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; +CREATE TABLE `SS13_ipintel` ( +`ip` INT UNSIGNED NOT NULL , +`date` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL , +`intel` REAL NOT NULL DEFAULT '0', +PRIMARY KEY ( `ip` ) +) ENGINE = INNODB; /*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `SS13_vpn_whitelist` +-- +DROP TABLE IF EXISTS `SS13_vpn_whitelist`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `SS13_vpn_whitelist` ( + `ckey` VARCHAR(32) NOT NULL, + `reason` text, + PRIMARY KEY (`ckey`) +) ENGINE=INNODB; \ No newline at end of file diff --git a/SQL/updates/5-6.sql b/SQL/updates/5-6.sql new file mode 100644 index 00000000000..91bf4328a2a --- /dev/null +++ b/SQL/updates/5-6.sql @@ -0,0 +1,34 @@ +#Updating the SQL from version 5 to version 6. -Kyep + +#Make a table to track the results of VPN/proxy lookups for IPs (IPINTEL, TG PORT) +CREATE TABLE `ipintel` ( +`ip` INT UNSIGNED NOT NULL , +`date` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL , +`intel` REAL NOT NULL DEFAULT '0', +PRIMARY KEY ( `ip` ) +) ENGINE = INNODB; + +#Make a table to track which ckeys are whitelisted for use of VPNs (IPINTEL, CUSTOM) +CREATE TABLE `vpn_whitelist` ( + `ckey` VARCHAR(32) NOT NULL, + `reason` text, + PRIMARY KEY (`ckey`) +) ENGINE=INNODB; + +# Add fuid (forum userid) which enables quick lookup of which ckey is associated with a specific forum account. (FORUM LINK) +ALTER TABLE `player` ADD `fuid` BIGINT(20) NULL DEFAULT NULL; +ALTER TABLE `player` ADD INDEX(`fuid`); + +# Add fupdate (forum update required) which flags specific ckeys as having been banned/unbanned, which requires an update of their forum/etc permissions (FORUM LINK) +ALTER TABLE `player` ADD `fupdate` SMALLINT(4) NULL DEFAULT 0; +ALTER TABLE `player` ADD INDEX(`fupdate`); + +#Make a table to track oauth tokens for linking forum/web accounts (FORUM LINK) +CREATE TABLE `oauth_tokens` ( + `ckey` VARCHAR(32) NOT NULL, + `token` VARCHAR(32) NOT NULL, + PRIMARY KEY (`token`) +) ENGINE=INNODB; + +#Drop the old 'discord' table that is not used anymore +DROP TABLE `discord`; diff --git a/code/__DEFINES/misc.dm b/code/__DEFINES/misc.dm index 1c390bdf5ff..3418938d955 100644 --- a/code/__DEFINES/misc.dm +++ b/code/__DEFINES/misc.dm @@ -326,7 +326,7 @@ #define INVESTIGATE_BOMB "bombs" // The SQL version required by this version of the code -#define SQL_VERSION 5 +#define SQL_VERSION 6 // Vending machine stuff #define CAT_NORMAL 1 diff --git a/code/controllers/configuration.dm b/code/controllers/configuration.dm index 13d3fb2a4bb..3f155fa8031 100644 --- a/code/controllers/configuration.dm +++ b/code/controllers/configuration.dm @@ -129,6 +129,18 @@ var/slime_delay = 0 var/animal_delay = 0 + //IP Intel vars + var/ipintel_email + var/ipintel_rating_bad = 1 + var/ipintel_save_good = 12 + var/ipintel_save_bad = 1 + var/ipintel_domain = "check.getipintel.net" + var/ipintel_maxplaytime = 0 + var/ipintel_whitelist = 0 + var/ipintel_detailsurl = "https://iphub.info/?ip=" + + var/forum_link_url + var/admin_legacy_system = 0 //Defines whether the server uses the legacy admin system with admins.txt or the SQL system. Config option in config.txt var/ban_legacy_system = 0 //Defines whether the server uses the legacy banning system with the files in /data or the SQL system. Config option in config.txt var/use_age_restriction_for_jobs = 0 //Do jobs use account age restrictions? --requires database @@ -304,10 +316,30 @@ if("auto_cryo_ssd_mins") config.auto_cryo_ssd_mins = text2num(value) - if("ssd_warning") config.ssd_warning = 1 + if("ipintel_email") + if(value != "ch@nge.me") + config.ipintel_email = value + if("ipintel_rating_bad") + config.ipintel_rating_bad = text2num(value) + if("ipintel_domain") + config.ipintel_domain = value + if("ipintel_save_good") + config.ipintel_save_good = text2num(value) + if("ipintel_save_bad") + config.ipintel_save_bad = text2num(value) + if("ipintel_maxplaytime") + config.ipintel_maxplaytime = text2num(value) + if("ipintel_whitelist") + config.ipintel_whitelist = 1 + if("ipintel_detailsurl") + config.ipintel_detailsurl = value + + if("forum_link_url") + config.forum_link_url = value + if("log_ooc") config.log_ooc = 1 diff --git a/code/controllers/subsystem/ipintel.dm b/code/controllers/subsystem/ipintel.dm new file mode 100644 index 00000000000..92f02a404a1 --- /dev/null +++ b/code/controllers/subsystem/ipintel.dm @@ -0,0 +1,15 @@ +SUBSYSTEM_DEF(ipintel) + name = "XKeyScore" + wait = 1 + flags = SS_NO_FIRE + init_order = INIT_ORDER_XKEYSCORE // 10 + var/enabled = 0 //disable at round start to avoid checking reconnects + var/throttle = 0 + var/errors = 0 + + var/list/cache = list() + +/datum/controller/subsystem/ipintel/Initialize(timeofday, zlevel) + enabled = 1 + . = ..() + diff --git a/code/modules/admin/DB ban/functions.dm b/code/modules/admin/DB ban/functions.dm index 5eec9d76bdd..1ce6c2b90f0 100644 --- a/code/modules/admin/DB ban/functions.dm +++ b/code/modules/admin/DB ban/functions.dm @@ -150,6 +150,8 @@ datum/admins/proc/DB_ban_record(var/bantype, var/mob/banned_mob, var/duration = if(isjobban) jobban_client_fullban(ckey, job) + else + flag_account_for_forum_sync(ckey) datum/admins/proc/DB_ban_unban(var/ckey, var/bantype, var/job = "") @@ -228,6 +230,8 @@ datum/admins/proc/DB_ban_unban(var/ckey, var/bantype, var/job = "") DB_ban_unban_by_id(ban_id) if(isjobban) jobban_unban_client(ckey, job) + else + flag_account_for_forum_sync(ckey) datum/admins/proc/DB_ban_edit(var/banid = null, var/param = null) @@ -333,6 +337,8 @@ datum/admins/proc/DB_ban_unban_by_id(var/id) var/DBQuery/query_update = dbcon.NewQuery(sql_update) query_update.Execute() + flag_account_for_forum_sync(pckey) + /client/proc/DB_ban_panel() set category = "Admin" @@ -566,4 +572,12 @@ datum/admins/proc/DB_ban_unban_by_id(var/id) output += "" - usr << browse(output,"window=lookupbans;size=900x700") \ No newline at end of file + usr << browse(output,"window=lookupbans;size=900x700") + +/proc/flag_account_for_forum_sync(ckey) + if(!dbcon) + return + var/skey = sanitizeSQL(ckey) + var/sql = "UPDATE [format_table_name("player")] SET fupdate = 1 WHERE ckey = '[skey]'" + var/DBQuery/adm_query = dbcon.NewQuery(sql) + adm_query.Execute() diff --git a/code/modules/admin/IsBanned.dm b/code/modules/admin/IsBanned.dm index ec9fa9af05e..9245f5174ce 100644 --- a/code/modules/admin/IsBanned.dm +++ b/code/modules/admin/IsBanned.dm @@ -1,5 +1,12 @@ //Blocks an attempt to connect before even creating our client datum thing. -world/IsBanned(key,address,computer_id) +world/IsBanned(key, address, computer_id, check_ipintel = TRUE) + + if(!config.ban_legacy_system) + if(address) + address = sanitizeSQL(address) + if(computer_id) + computer_id = sanitizeSQL(computer_id) + if(!key || !address || !computer_id) log_adminwarn("Failed Login (invalid data): [key] [address]-[computer_id]") return list("reason"="invalid login data", "desc"="Error: Could not check ban status, please try again. Error message: Your computer provided invalid or blank information to the server on connection (BYOND Username, IP, and Computer ID). Provided information for reference: Username: '[key]' IP: '[address]' Computer ID: '[computer_id]'. If you continue to get this error, please restart byond or contact byond support.") @@ -31,6 +38,14 @@ world/IsBanned(key,address,computer_id) mistakemessage = "\nIf you believe this is a mistake, please request help at [config.banappeals]." return list("reason"="using Tor", "desc"="\nReason: The network you are using to connect has been banned.[mistakemessage]") + //check if the IP address is a known proxy/vpn, and the user is not whitelisted + if(check_ipintel && config.ipintel_email && config.ipintel_whitelist && ipintel_is_banned(key, address)) + log_adminwarn("Failed Login: [key] [computer_id] [address] - Proxy/VPN") + var/mistakemessage = "" + if(config.banappeals) + mistakemessage = "\nIf you have to use one, request whitelisting at: [config.banappeals]" + return list("reason"="using proxy or vpn", "desc"="\nReason: Proxies/VPNs are not allowed here. [mistakemessage]") + if(config.ban_legacy_system) //Ban Checking diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm index 1a9f38b1486..c9c03303f7e 100644 --- a/code/modules/admin/admin.dm +++ b/code/modules/admin/admin.dm @@ -276,6 +276,15 @@ var/global/nologevent = 0 show_note(key) +/datum/admins/proc/vpn_whitelist() + set category = "Admin" + set name = "VPN Ckey Whitelist" + if(!check_rights(R_ADMIN)) + return + var/key = stripped_input(usr, "Enter ckey to add/remove, or leave blank to cancel:", "VPN Whitelist add/remove", max_length=32) + if(key) + vpn_whitelist_panel(key) + /datum/admins/proc/access_news_network() //MARKER set category = "Event" set name = "Access Newscaster Network" diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index 4a455cd8e5d..06c2361b90a 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -53,6 +53,7 @@ var/list/admin_verbs_admin = list( /datum/admins/proc/PlayerNotes, /client/proc/cmd_mentor_say, /datum/admins/proc/show_player_notes, + /datum/admins/proc/vpn_whitelist, /client/proc/free_slot, /*frees slot for chosen job*/ /client/proc/toggleattacklogs, /client/proc/toggleadminlogs, diff --git a/code/modules/admin/ipintel.dm b/code/modules/admin/ipintel.dm new file mode 100644 index 00000000000..75f2422681a --- /dev/null +++ b/code/modules/admin/ipintel.dm @@ -0,0 +1,228 @@ +/datum/ipintel + var/ip + var/intel = 0 + var/cache = FALSE + var/cacheminutesago = 0 + var/cachedate = "" + var/cacherealtime = 0 + +/datum/ipintel/New() + cachedate = SQLtime() + cacherealtime = world.realtime + +/datum/ipintel/proc/is_valid() + . = FALSE + if(intel < 0) + return + if(intel <= config.ipintel_rating_bad) + if(world.realtime < cacherealtime + (config.ipintel_save_good HOURS)) + return TRUE + else + if(world.realtime < cacherealtime + (config.ipintel_save_bad HOURS)) + return TRUE + +/proc/get_ip_intel(ip, bypasscache = FALSE, updatecache = TRUE) + var/datum/ipintel/res = new() + ip = sanitizeSQL(ip) + res.ip = ip + . = res + if(!ip || !config.ipintel_email || !SSipintel.enabled) + return + if(!bypasscache) + var/datum/ipintel/cachedintel = SSipintel.cache[ip] + if(cachedintel && cachedintel.is_valid()) + cachedintel.cache = TRUE + return cachedintel + + if(dbcon.IsConnected()) + var/rating_bad = config.ipintel_rating_bad + var/DBQuery/query_get_ip_intel = dbcon.NewQuery({" + SELECT date, intel, TIMESTAMPDIFF(MINUTE,date,NOW()) + FROM [format_table_name("ipintel")] + WHERE + ip = INET_ATON('[ip]') + AND (( + intel < [rating_bad] + AND + date + INTERVAL [config.ipintel_save_good] HOUR > NOW() + ) OR ( + intel >= [rating_bad] + AND + date + INTERVAL [config.ipintel_save_bad] HOUR > NOW() + )) + "}) + if(!query_get_ip_intel.Execute()) + qdel(query_get_ip_intel) + return + if(query_get_ip_intel.NextRow()) + res.cache = TRUE + res.cachedate = query_get_ip_intel.item[1] + res.intel = text2num(query_get_ip_intel.item[2]) + res.cacheminutesago = text2num(query_get_ip_intel.item[3]) + res.cacherealtime = world.realtime - (text2num(query_get_ip_intel.item[3])*10*60) + SSipintel.cache[ip] = res + qdel(query_get_ip_intel) + return + qdel(query_get_ip_intel) + res.intel = ip_intel_query(ip) + if(updatecache && res.intel >= 0) + SSipintel.cache[ip] = res + if(dbcon.IsConnected()) + var/DBQuery/query_add_ip_intel = dbcon.NewQuery("INSERT INTO [format_table_name("ipintel")] (ip, intel) VALUES (INET_ATON('[ip]'), [res.intel]) ON DUPLICATE KEY UPDATE intel = VALUES(intel), date = NOW()") + query_add_ip_intel.Execute() + qdel(query_add_ip_intel) + + +/proc/ip_intel_query(ip, var/retryed=0) + . = -1 //default + if(!ip) + return + if(SSipintel.throttle > world.timeofday) + return + if(!SSipintel.enabled) + return + + var/list/http[] = world.Export("http://[config.ipintel_domain]/check.php?ip=[ip]&contact=[config.ipintel_email]&format=json&flags=b") + + if(http) + var/status = text2num(http["STATUS"]) + + if(status == 200) + var/response = json_decode(file2text(http["CONTENT"])) + if(response) + if(response["status"] == "success") + var/intelnum = text2num(response["result"]) + if(isnum(intelnum)) + return text2num(response["result"]) + else + ipintel_handle_error("Bad intel from server: [response["result"]].", ip, retryed) + if(!retryed) + sleep(25) + return .(ip, 1) + else + ipintel_handle_error("Bad response from server: [response["status"]].", ip, retryed) + if(!retryed) + sleep(25) + return .(ip, 1) + + else if(status == 429) + ipintel_handle_error("Error #429: We have exceeded the rate limit.", ip, 1) + return + else + ipintel_handle_error("Unknown status code: [status].", ip, retryed) + if(!retryed) + sleep(25) + return .(ip, 1) + else + ipintel_handle_error("Unable to connect to API.", ip, retryed) + if(!retryed) + sleep(25) + return .(ip, 1) + + +/proc/ipintel_handle_error(error, ip, retryed) + if(retryed) + SSipintel.errors++ + error += " Could not check [ip]. Disabling IPINTEL for [SSipintel.errors] minute[( SSipintel.errors == 1 ? "" : "s" )]" + SSipintel.throttle = world.timeofday + (2 * SSipintel.errors MINUTES) + else + error += " Attempting retry on [ip]." + log_ipintel(error) + +/proc/log_ipintel(text) + log_game("IPINTEL: [text]") + log_debug("IPINTEL: [text]") + + +/proc/ipintel_is_banned(t_ckey, t_ip) + if(!config.ipintel_email) + return FALSE + if(!config.ipintel_whitelist) + return FALSE + if(!dbcon.IsConnected()) + return FALSE + if(!ipintel_badip_check(t_ip)) + return FALSE + if(vpn_whitelist_check(t_ckey)) + return FALSE + return TRUE + +/proc/ipintel_badip_check(target_ip) + var/rating_bad = config.ipintel_rating_bad + if(!rating_bad) + log_debug("ipintel_badip_check reports misconfigured rating_bad directive") + return FALSE + var/valid_hours = config.ipintel_save_bad + if(!valid_hours) + log_debug("ipintel_badip_check reports misconfigured ipintel_save_bad directive") + return FALSE + target_ip = sanitizeSQL(target_ip) + rating_bad = sanitizeSQL(rating_bad) + valid_hours = sanitizeSQL(valid_hours) + var/check_sql = {"SELECT * FROM [format_table_name("ipintel")] WHERE ip = INET_ATON('[target_ip]') AND intel >= [rating_bad] AND (date + INTERVAL [valid_hours] HOUR) > NOW()"} + var/DBQuery/query_get_ip_intel = dbcon.NewQuery(check_sql) + if(!query_get_ip_intel.Execute()) + log_debug("ipintel_badip_check reports failed query execution") + qdel(query_get_ip_intel) + return FALSE + if(!query_get_ip_intel.NextRow()) + qdel(query_get_ip_intel) + return FALSE + qdel(query_get_ip_intel) + return TRUE + +/proc/vpn_whitelist_check(target_ckey) + if(!config.ipintel_whitelist) + return FALSE + var/target_sql_ckey = ckey(target_ckey) + var/DBQuery/query_whitelist_check = dbcon.NewQuery("SELECT * FROM [format_table_name("vpn_whitelist")] WHERE ckey = '[target_sql_ckey]'") + if(!query_whitelist_check.Execute()) + var/err = query_whitelist_check.ErrorMsg() + log_debug("SQL ERROR on proc/vpn_whitelist_check for ckey '[target_sql_ckey]'. Error : \[[err]\]\n") + return FALSE + if(query_whitelist_check.NextRow()) + return TRUE // At least one row in the whitelist names their ckey. That means they are whitelisted. + return FALSE + +/proc/vpn_whitelist_add(target_ckey) + var/target_sql_ckey = ckey(target_ckey) + var/reason_string = input(usr, "Enter link to the URL of their whitelist request on the forum.","Reason required") as message|null + if(!reason_string) + return FALSE + reason_string = sanitizeSQL(reason_string) + var/DBQuery/query_whitelist_add = dbcon.NewQuery("INSERT INTO [format_table_name("vpn_whitelist")] (ckey,reason) VALUES ('[target_sql_ckey]','[reason_string]')") + if(!query_whitelist_add.Execute()) + var/err = query_whitelist_add.ErrorMsg() + log_debug("SQL ERROR on proc/vpn_whitelist_add for ckey '[target_sql_ckey]'. Error : \[[err]\]\n") + return FALSE + return TRUE + +/proc/vpn_whitelist_remove(target_ckey) + var/target_sql_ckey = ckey(target_ckey) + var/DBQuery/query_whitelist_remove = dbcon.NewQuery("DELETE FROM [format_table_name("vpn_whitelist")] WHERE ckey = '[target_sql_ckey]'") + if(!query_whitelist_remove.Execute()) + var/err = query_whitelist_remove.ErrorMsg() + log_debug("SQL ERROR on proc/vpn_whitelist_remove for ckey '[target_sql_ckey]'. Error : \[[err]\]\n") + return FALSE + return TRUE + +/proc/vpn_whitelist_panel(target_ckey as text) + if(!check_rights(R_ADMIN)) + return + if(!target_ckey) + return + var/is_already_whitelisted = vpn_whitelist_check(target_ckey) + if(is_already_whitelisted) + var/confirm = alert("[target_ckey] is already whitelisted. Remove them?", "Confirm Removal", "No", "Yes") + if(!confirm || confirm != "Yes") + to_chat(usr, "VPN whitelist alteration cancelled.") + return + else if(vpn_whitelist_remove(target_ckey)) + to_chat(usr, "[target_ckey] was removed from the VPN whitelist.") + else + to_chat(usr, "VPN whitelist unchanged.") + else + if(vpn_whitelist_add(target_ckey)) + to_chat(usr, "[target_ckey] was added to the VPN whitelist.") + else + to_chat(usr, "VPN whitelist unchanged.") \ No newline at end of file diff --git a/code/modules/client/client defines.dm b/code/modules/client/client defines.dm index a6bc558842b..995ee519474 100644 --- a/code/modules/client/client defines.dm +++ b/code/modules/client/client defines.dm @@ -72,6 +72,8 @@ control_freak = CONTROL_FREAK_ALL | CONTROL_FREAK_SKIN | CONTROL_FREAK_MACROS + var/ip_intel = "Disabled" + var/datum/click_intercept/click_intercept = null //datum that controls the displaying and hiding of tooltips diff --git a/code/modules/client/client procs.dm b/code/modules/client/client procs.dm index c6ab57bae4d..922502698fa 100644 --- a/code/modules/client/client procs.dm +++ b/code/modules/client/client procs.dm @@ -229,7 +229,8 @@ if(href_list["ssdwarning"]) ssd_warning_acknowledged = TRUE to_chat(src, "SSD warning acknowledged.") - + if(href_list["link_forum_account"]) + link_forum_account() switch(href_list["action"]) if("openLink") src << link(href_list["link"]) @@ -320,8 +321,7 @@ //Admin Authorisation // Automatically makes localhost connection an admin if(!config.disable_localhost_admin) - var/localhost_addresses = list("127.0.0.1", "::1") // Adresses - if(!isnull(address) && address in localhost_addresses) + if(is_connecting_from_localhost()) new /datum/admins("!LOCALHOST!", R_HOST, ckey) // Makes localhost rank holder = admin_datums[ckey] if(holder) @@ -370,8 +370,9 @@ to_chat(src, message) clientmessages.Remove(ckey) - donator_check() + donator_check() + check_ip_intel() send_resources() if(prefs.toggles & UI_DARKMODE) // activates dark mode if its flagged. -AA07 @@ -393,6 +394,8 @@ generate_clickcatcher() apply_clickcatcher() + check_forum_link() + if(custom_event_msg && custom_event_msg != "") to_chat(src, "

Custom Event

") to_chat(src, "

A custom event is taking place. OOC Info:

") @@ -409,6 +412,12 @@ Master.UpdateTickRate() +/client/proc/is_connecting_from_localhost() + var/localhost_addresses = list("127.0.0.1", "::1") // Adresses + if(!isnull(address) && address in localhost_addresses) + return TRUE + return FALSE + ////////////// //DISCONNECT// ////////////// @@ -539,6 +548,81 @@ var/DBQuery/query_accesslog = dbcon.NewQuery("INSERT INTO `[format_table_name("connection_log")]`(`id`,`datetime`,`serverip`,`ckey`,`ip`,`computerid`) VALUES(null,Now(),'[serverip]','[ckey]','[sql_ip]','[sql_computerid]');") query_accesslog.Execute() +/client/proc/check_ip_intel() + set waitfor = 0 //we sleep when getting the intel, no need to hold up the client connection while we sleep + if(config.ipintel_email) + if(config.ipintel_maxplaytime && config.use_exp_tracking) + var/living_hours = get_exp_type_num(EXP_TYPE_LIVING) / 60 + if(living_hours >= config.ipintel_maxplaytime) + return + + if(is_connecting_from_localhost()) + log_debug("check_ip_intel: skip check for player [key_name_admin(src)] connecting from localhost.") + return + + if(vpn_whitelist_check(ckey)) + log_debug("check_ip_intel: skip check for player [key_name_admin(src)] [address] on whitelist.") + return + + var/datum/ipintel/res = get_ip_intel(address) + ip_intel = res.intel + verify_ip_intel() + +/client/proc/verify_ip_intel() + if(ip_intel >= config.ipintel_rating_bad) + var/detailsurl = config.ipintel_detailsurl ? "(IP Info)" : "" + if(config.ipintel_whitelist) + spawn(40) // This is necessary because without it, they won't see the message, and addtimer cannot be used because the timer system may not have initialized yet + message_admins("IPIntel: [key_name_admin(src)] on IP [address] was rejected. [detailsurl]") + var/blockmsg = "Error: proxy/VPN detected. Proxy/VPN use is not allowed here. Deactivate it before you reconnect." + if(config.banappeals) + blockmsg += "\nIf you are not actually using a proxy/VPN, or have no choice but to use one, request whitelisting at: [config.banappeals]" + to_chat(src, blockmsg) + qdel(src) + else + message_admins("IPIntel: [key_name_admin(src)] on IP [address] is likely to be using a Proxy/VPN. [detailsurl]") + + +/client/proc/check_forum_link() + if(config.forum_link_url && prefs && !prefs.fuid) + to_chat(src, "You do not have your forum account linked. LINK FORUM ACCOUNT") + +/client/proc/create_oauth_token() + var/DBQuery/query_find_token = dbcon.NewQuery("SELECT token FROM [format_table_name("oauth_tokens")] WHERE ckey = '[ckey]' limit 1") + if(!query_find_token.Execute()) + log_debug("create_oauth_token: failed db read") + return + if(query_find_token.NextRow()) + return query_find_token.item[1] + var/tokenstr = md5("[ckey][rand()]") + var/DBQuery/query_insert_token = dbcon.NewQuery("INSERT INTO [format_table_name("oauth_tokens")] (ckey, token) VALUES('[ckey]','[tokenstr]')") + if(!query_insert_token.Execute()) + return + return tokenstr + +/client/proc/link_forum_account() + if(IsGuestKey(key)) + to_chat(src, "Guest keys cannot be linked.") + return + if(prefs && prefs.fuid) + to_chat(src, "Your forum account is already set.") + return + var/DBQuery/query_find_link = dbcon.NewQuery("SELECT fuid FROM [format_table_name("player")] WHERE ckey = '[ckey]' limit 1") + if(!query_find_link.Execute()) + log_debug("link_forum_account: failed db read") + return + if(query_find_link.NextRow()) + if(query_find_link.item[1]) + to_chat(src, "Your forum account is already set. (" + query_find_link.item[1] + ")") + return + var/tokenid = create_oauth_token() + if(!tokenid) + to_chat(src, "link_forum_account: unable to create token") + return + var/url = "[config.forum_link_url][tokenid]" + to_chat(src, {"Now opening a windows to verify your information with the forums. If the window does not load, please go to: [url]."}) + src << link(url) + return #undef TOPIC_SPAM_DELAY #undef UPLOAD_LIMIT diff --git a/code/modules/client/preference/preferences.dm b/code/modules/client/preference/preferences.dm index 4b24d267f29..75e0f549a1d 100644 --- a/code/modules/client/preference/preferences.dm +++ b/code/modules/client/preference/preferences.dm @@ -92,6 +92,7 @@ var/global/list/special_role_times = list( //minimum age (in days) for accounts var/windowflashing = TRUE var/clientfps = 0 var/atklog = ATKLOG_ALL + var/fuid // forum userid //ghostly preferences var/ghost_anonsay = 0 diff --git a/code/modules/client/preference/preferences_mysql.dm b/code/modules/client/preference/preferences_mysql.dm index 6b72d71bc9b..90a4bed6f14 100644 --- a/code/modules/client/preference/preferences_mysql.dm +++ b/code/modules/client/preference/preferences_mysql.dm @@ -18,7 +18,8 @@ ghost_anonsay, exp, clientfps, - atklog + atklog, + fuid FROM [format_table_name("player")] WHERE ckey='[C.ckey]'"} ) @@ -50,6 +51,7 @@ exp = query.item[16] clientfps = text2num(query.item[17]) atklog = text2num(query.item[18]) + fuid = text2num(query.item[19]) //Sanitize ooccolor = sanitize_hexcolor(ooccolor, initial(ooccolor)) @@ -69,6 +71,7 @@ exp = sanitize_text(exp, initial(exp)) clientfps = sanitize_integer(clientfps, 0, 1000, initial(clientfps)) atklog = sanitize_integer(atklog, 0, 100, initial(atklog)) + fuid = sanitize_integer(fuid, 0, 10000000, initial(fuid)) return 1 /datum/preferences/proc/save_preferences(client/C) diff --git a/code/modules/discord/accountlink.dm b/code/modules/discord/accountlink.dm deleted file mode 100644 index c65d7da1440..00000000000 --- a/code/modules/discord/accountlink.dm +++ /dev/null @@ -1,52 +0,0 @@ -// DONT TOUCH ANYTHING IN HERE UNLESS YOU KNOW WHAT YOU ARE DOING -affected -/client/verb/linkdiscord() - set category = "Special Verbs" - set name = "Link Discord Account" - set desc = "Link your discord account to your BYOND account." - var/user_ckey = sanitizeSQL(usr.ckey) // Probably not neccassary but better safe than sorry - if(!config.sql_enabled) - to_chat(src, "This is feature requires the SQL backend") - return - var/DBQuery/db_discord_id = dbcon.NewQuery("SELECT discord_id FROM [format_table_name("discord")] WHERE ckey = '[user_ckey]'") - if(!db_discord_id.Execute()) - var/err = db_discord_id.ErrorMsg() - log_game("SQL ERROR while selecting discord account. Error : \[[err]\]\n") - to_chat(src, "Error checking Discord account. Please inform an administrator") - return - var/stored_id - while(db_discord_id.NextRow()) - stored_id = db_discord_id.item[1] - if(!stored_id) // Not linked - var/know_how = alert("Do you know how to get a discord user ID? This ID is NOT your discord username and numbers!","Question","Yes","No") - if(know_how == "No") // Opens discord support on how to collect IDs - src << link("https://support.discordapp.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID") - var/entered_id = input("Please enter your Discord ID.", "Enter Discord ID", null, null) as text|null - var/sql_id = sanitizeSQL(entered_id) - var/DBQuery/store_discord_id = dbcon.NewQuery("INSERT INTO [format_table_name("discord")] (ckey, discord_id, notify) VALUES ('[user_ckey]', [sql_id], 0)") - if(!store_discord_id.Execute()) - var/err = db_discord_id.ErrorMsg() - log_game("SQL ERROR while linking discord account. Error : \[[err]\]\n") - to_chat(src, "Error linking Discord account. Please inform an administrator") - return - to_chat(src, "Successfully linked discord account [entered_id] to [user_ckey]") - else // Linked - var/choice = alert("You already have the Discord Account [stored_id] linked to [user_ckey]. Would you like to unlink or replace","Already Linked","Unlink","Replace") - switch(choice) - if("Unlink") - var/DBQuery/unlink_discord_id = dbcon.NewQuery("DELETE FROM [format_table_name("discord")] WHERE ckey = '[user_ckey]'") - if(!unlink_discord_id.Execute()) - var/err = unlink_discord_id.ErrorMsg() - log_game("SQL ERROR while unlinking discord account. Error : \[[err]\]\n") - to_chat(src, "Error unlinking Discord account. Please inform an administrator") - return - to_chat(src, "Successfully unlinked discord account") - if("Replace") - var/entered_id = input("Please enter your Discord ID. Instructions can be found at https://bit.ly/2AfUu40", "Enter Discord ID", null, null) as text|null - var/sql_id = sanitizeSQL(entered_id) - var/DBQuery/store_discord_id = dbcon.NewQuery("UPDATE [format_table_name("discord")] SET discord_id = '[sql_id]' WHERE ckey='[user_ckey]'") - if(!store_discord_id.Execute()) - var/err = db_discord_id.ErrorMsg() - to_chat(src, "Error replacing Discord account. Please inform an administrator") - log_game("SQL ERROR while linking discord account. Error : \[[err]\]\n") - return - to_chat(src, "Successfully linked discord account [entered_id] to [user_ckey]") diff --git a/code/modules/discord/notify.dm b/code/modules/discord/notify.dm deleted file mode 100644 index a5bfac04940..00000000000 --- a/code/modules/discord/notify.dm +++ /dev/null @@ -1,48 +0,0 @@ -// DONT TOUCH ANYTHING IN HERE UNLESS YOU KNOW WHAT YOU ARE DOING -affected -/client/verb/notify_restart() - set category = "Special Verbs" - set name = "Notify Restart" - set desc = "Notifies you on Discord when the server restarts." - if(!config.sql_enabled) - to_chat(src, "This is feature requires the SQL backend") - return - var/DBQuery/db_discord_id = dbcon.NewQuery("SELECT discord_id FROM [format_table_name("discord")] WHERE ckey = '[ckey]'") - if(!db_discord_id.Execute()) - var/err = db_discord_id.ErrorMsg() - log_game("SQL ERROR while selecting discord account. Error : \[[err]\]\n") - to_chat(src, "Error checking Discord account. Please inform an administrator") - return - var/stored_id - while(db_discord_id.NextRow()) - stored_id = db_discord_id.item[1] - if(!stored_id) // Not linked - to_chat(src, "This requires you to link your Discord account with the \"Link Discord Account\" verb.") - return - else // Linked - var/DBQuery/toggle_status = dbcon.NewQuery("SELECT notify FROM [format_table_name("discord")] WHERE ckey = '[ckey]'") - if(!toggle_status.Execute()) - var/err = toggle_status.ErrorMsg() - log_game("SQL ERROR while getting discord account. Error : \[[err]\]\n") - var/notify_status - while(toggle_status.NextRow()) - notify_status = toggle_status.item[1] - if(notify_status) // Data is there - switch(notify_status) - if("0") - var/DBQuery/update_notify = dbcon.NewQuery("UPDATE [format_table_name("discord")] SET notify = 1 WHERE ckey='[ckey]'") - if(!update_notify.Execute()) - var/err = db_discord_id.ErrorMsg() - to_chat(src, "Error updating notify status. Please inform an administrator") - log_game("SQL ERROR while updating notify status. Error : \[[err]\]\n") - return - to_chat(src, "You will now be notified on discord when the next round is starting") - if("1") - var/DBQuery/update_notify = dbcon.NewQuery("UPDATE [format_table_name("discord")] SET notify = 0 WHERE ckey='[ckey]'") - if(!update_notify.Execute()) - var/err = db_discord_id.ErrorMsg() - to_chat(src, "Error updating notify status. Please inform an administrator") - log_game("SQL ERROR while updating notify status. Error : \[[err]\]\n") - return - to_chat(src, "You will no longer be notified on discord when the next round is starting") - else // Oh fuck what the hell happened - to_chat(src, "Something has gone VERY wrong, or affected cant code.") diff --git a/code/modules/ext_scripts/irc.dm b/code/modules/ext_scripts/irc.dm index 0e8c72496d2..7f87413694a 100644 --- a/code/modules/ext_scripts/irc.dm +++ b/code/modules/ext_scripts/irc.dm @@ -19,20 +19,3 @@ if(config.admin_irc) send2irc(config.admin_irc, msg) return - -/hook/startup/proc/ircNotify() - var/people_to_ping = "" - var/DBQuery/pull_notify = dbcon.NewQuery("SELECT discord_id FROM [format_table_name("discord")] WHERE notify = 1") - if(!pull_notify.Execute()) - var/err = pull_notify.ErrorMsg() - log_game("SQL ERROR while pulling notify people. Error : \[[err]\]\n") - else - while(pull_notify.NextRow()) - people_to_ping += "<@" + pull_notify.item[1] + "> " - send2mainirc("Server starting up on [station_name()]. Connect to: "+people_to_ping, 7355608) - // Set notify to 0 so people arent pinged round after round - var/DBQuery/reset_notify = dbcon.NewQuery("UPDATE [format_table_name("discord")] SET notify = 0 WHERE notify = 1") - if(!reset_notify.Execute()) - var/err = reset_notify.ErrorMsg() - log_game("SQL ERROR while resetting notify status. Error : \[[err]\]\n") - return 1 diff --git a/code/modules/mob/new_player/new_player.dm b/code/modules/mob/new_player/new_player.dm index 436911d30fa..a87cbf6c8d3 100644 --- a/code/modules/mob/new_player/new_player.dm +++ b/code/modules/mob/new_player/new_player.dm @@ -182,7 +182,8 @@ return 0 if(alert(src,"Are you sure you wish to observe? You cannot normally join the round after doing this!","Player Setup","Yes","No") == "Yes") - if(!client) return 1 + if(!client) + return 1 var/mob/dead/observer/observer = new() src << browse(null, "window=playersetup") spawning = 1 @@ -220,7 +221,6 @@ if(!SSticker || SSticker.current_state != GAME_STATE_PLAYING) to_chat(usr, "The round is either not ready, or has already finished...") return - if(client.prefs.species in GLOB.whitelisted_species) if(!is_alien_whitelisted(src, client.prefs.species) && config.usealienwhitelist) diff --git a/config/example/config.txt b/config/example/config.txt index f3f7b230094..87ed0ce5cf1 100644 --- a/config/example/config.txt +++ b/config/example/config.txt @@ -185,6 +185,28 @@ GUEST_JOBBAN ## Uncomment this to stop people connecting to your server without a registered ckey. (i.e. guest-* are all blocked from connecting) GUEST_BAN +### IPINTEL: +### This allows you to detect likely proxies by checking ips against getipintel.net +## Rating to warn at: (0.90 is good, 1 is 100% likely to be a spammer/proxy, 0.8 is 80%, etc) anything equal to or higher then this number triggers an admin warning +#IPINTEL_RATING_BAD 0.90 +## Contact email, (required to use the service, leaving blank or default disables IPINTEL) +#IPINTEL_EMAIL ch@nge.me +## How long to save good matches (ipintel rate limits to 15 per minute and 500 per day. so this shouldn't be too low, getipintel.net suggests 6 hours, time is in hours) (Your ip will get banned if you go over 500 a day too many times) +#IPINTEL_SAVE_GOOD 72 +## How long to save bad matches (these numbers can change as ips change hands, best not to save these for too long in case somebody gets a new ip used by a spammer/proxy before.) +#IPINTEL_SAVE_BAD 24 +## Domain name to query (leave commented out for the default, only needed if you pay getipintel.net for more querys) +#IPINTEL_DOMAIN check.getipintel.net +## Ignore players with this many hours of playtime. Requires USE_EXP_TRACKING +#IPINTEL_MAXPLAYTIME 10 +## Require players (except the previous ones with playtime, if enabled) to be whitelisted to use proxies/VPNs. +#IPINTEL_WHITELIST +## URL to show to admins to provide more information about an IP address. Leave undefined for default. +#IPINTEL_DETAILSURL https://iphub.info/?ip= + +## URL to use to link forum accounts. If not set, no link option will be offered. +#FORUM_LINK_URL https://example.com/link.php?token= + ## Comment to disable checking for the cid randomizer dll. (disabled if database isn't enabled or connected) CHECK_RANDOMIZER diff --git a/config/example/dbconfig.txt b/config/example/dbconfig.txt index 6ca1f0201e8..8b411db15e1 100644 --- a/config/example/dbconfig.txt +++ b/config/example/dbconfig.txt @@ -9,7 +9,7 @@ ## This value must be set to the version of the paradise schema in use. ## If this value does not match, the SQL database will not be loaded and an error will be generated. ## Roundstart will be delayed. -DB_VERSION 5 +DB_VERSION 6 ## Server the MySQL database can be found at. # Examples: localhost, 200.135.5.43, www.mysqldb.com, etc. diff --git a/goon/code/datums/browserOutput.dm b/goon/code/datums/browserOutput.dm index 28a2690b5f6..3456e0a2ce9 100644 --- a/goon/code/datums/browserOutput.dm +++ b/goon/code/datums/browserOutput.dm @@ -149,7 +149,7 @@ var/list/chatResources = list( var/list/row = connectionHistory[i] if(!row || row.len < 3 || !(row["ckey"] && row["compid"] && row["ip"])) return - if(world.IsBanned(row["ckey"], row["compid"], row["ip"])) + if(world.IsBanned(row["ckey"], row["compid"], row["ip"], FALSE)) found = row break diff --git a/html/changelogs/AutoChangeLog-pr-11359.yml b/html/changelogs/AutoChangeLog-pr-11359.yml new file mode 100644 index 00000000000..3f93a5677ef --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-11359.yml @@ -0,0 +1,6 @@ +author: "Kyep" +delete-after: True +changes: + - rscadd: "Ported XKeyScore from TG." + - rscadd: "New system to securely link ingame/forum accounts" + - rscdel: "Removed old insecure 'link discord account' system" diff --git a/paradise.dme b/paradise.dme index ee5b889e244..1bf74ad556c 100644 --- a/paradise.dme +++ b/paradise.dme @@ -207,6 +207,7 @@ #include "code\controllers\subsystem\holiday.dm" #include "code\controllers\subsystem\icon_smooth.dm" #include "code\controllers\subsystem\idlenpcpool.dm" +#include "code\controllers\subsystem\ipintel.dm" #include "code\controllers\subsystem\jobs.dm" #include "code\controllers\subsystem\lighting.dm" #include "code\controllers\subsystem\machinery.dm" @@ -1146,6 +1147,7 @@ #include "code\modules\admin\create_poll.dm" #include "code\modules\admin\create_turf.dm" #include "code\modules\admin\holder2.dm" +#include "code\modules\admin\ipintel.dm" #include "code\modules\admin\IsBanned.dm" #include "code\modules\admin\machine_upgrade.dm" #include "code\modules\admin\NewBan.dm" @@ -1399,8 +1401,6 @@ #include "code\modules\detective_work\evidence.dm" #include "code\modules\detective_work\footprints_and_rag.dm" #include "code\modules\detective_work\scanner.dm" -#include "code\modules\discord\accountlink.dm" -#include "code\modules\discord\notify.dm" #include "code\modules\economy\Accounts.dm" #include "code\modules\economy\Accounts_DB.dm" #include "code\modules\economy\ATM.dm"