diff --git a/SQL/migrate/V003__Ipintel.sql b/SQL/migrate/V003__Ipintel.sql new file mode 100644 index 00000000000..6a5a7917303 --- /dev/null +++ b/SQL/migrate/V003__Ipintel.sql @@ -0,0 +1,14 @@ +-- +-- Adds the IP intel database table. +-- + +CREATE TABLE `ss13_ipintel` ( + `ip` varbinary(16) NOT NULL, + `date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `intel` double NOT NULL DEFAULT '0', + PRIMARY KEY (`ip`), + KEY `idx_ipintel` (`ip`,`intel`,`date`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; + +ALTER TABLE `ss13_player` + ADD `account_join_date` DATE NULL DEFAULT NULL AFTER `whitelist_status`; diff --git a/baystation12.dme b/baystation12.dme index ef2edf3301b..7638750075e 100644 --- a/baystation12.dme +++ b/baystation12.dme @@ -154,6 +154,7 @@ #include "code\controllers\subsystems\garbage.dm" #include "code\controllers\subsystems\icon_smooth.dm" #include "code\controllers\subsystems\icon_updates.dm" +#include "code\controllers\subsystems\ipintel.dm" #include "code\controllers\subsystems\job.dm" #include "code\controllers\subsystems\law.dm" #include "code\controllers\subsystems\lighting.dm" @@ -951,6 +952,7 @@ #include "code\modules\admin\create_object.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\map_capture.dm" #include "code\modules\admin\NewBan.dm" @@ -1000,6 +1002,7 @@ #include "code\modules\admin\secrets\random_events\gravity.dm" #include "code\modules\admin\secrets\random_events\trigger_cordical_borer_infestation.dm" #include "code\modules\admin\secrets\random_events\trigger_xenomorph_infestation.dm" +#include "code\modules\admin\verbs\access_control.dm" #include "code\modules\admin\verbs\adminhelp.dm" #include "code\modules\admin\verbs\adminjump.dm" #include "code\modules\admin\verbs\adminpm.dm" diff --git a/code/_helpers/time.dm b/code/_helpers/time.dm index f863e586172..c25f6f67cd0 100644 --- a/code/_helpers/time.dm +++ b/code/_helpers/time.dm @@ -6,7 +6,7 @@ var/roundstart_hour = 0 //Returns the world time in english -proc/worldtime2text(time = world.time, timeshift = 1) +/proc/worldtime2text(time = world.time, timeshift = 1) if(!roundstart_hour) roundstart_hour = rand(0, 23) return timeshift ? time2text(time+(36000*roundstart_hour), "hh:mm") : time2text(time, "hh:mm") @@ -15,14 +15,14 @@ proc/worldtime2text(time = world.time, timeshift = 1) worldtime2text() . = text2num(time2text(world.time + (36000 * roundstart_hour), "hh")) -proc/worlddate2text() +/proc/worlddate2text() return num2text(game_year) + "-" + time2text(world.timeofday, "MM-DD") -proc/time_stamp() +/proc/time_stamp() return time2text(world.timeofday, "hh:mm:ss") /* Returns 1 if it is the selected month and day */ -proc/isDay(var/month, var/day) +/proc/isDay(var/month, var/day) if(isnum(month) && isnum(day)) var/MM = text2num(time2text(world.timeofday, "MM")) // get the current month var/DD = text2num(time2text(world.timeofday, "DD")) // get the current day @@ -35,7 +35,7 @@ proc/isDay(var/month, var/day) var/next_duration_update = 0 var/last_round_duration = 0 -proc/round_duration() +/proc/round_duration() if(last_round_duration && world.time < next_duration_update) return last_round_duration @@ -57,3 +57,9 @@ proc/round_duration() if (world.timeofday < rollovercheck_last_timeofday) //TIME IS GOING BACKWARDS! return midnight_rollovers++ return midnight_rollovers + +//returns timestamp in a sql and ISO 8601 friendly format +/proc/SQLtime(timevar) + if(!timevar) + timevar = world.realtime + return time2text(timevar, "YYYY-MM-DD hh:mm:ss") diff --git a/code/controllers/configuration.dm b/code/controllers/configuration.dm index e2cfe234043..261de545213 100644 --- a/code/controllers/configuration.dm +++ b/code/controllers/configuration.dm @@ -265,6 +265,20 @@ var/list/gamemode_cache = list() var/log_gelf_ip = "" var/log_gelf_port = "" + //IP Intel vars + var/ipintel_email + var/ipintel_rating_bad = 1 + var/ipintel_rating_kick = 0 + var/ipintel_save_good = 12 + var/ipintel_save_bad = 1 + var/ipintel_domain = "check.getipintel.net" + + // Access control/Panic bunker settings. + var/access_deny_new_players = 0 + var/access_deny_new_accounts = -1 + var/access_deny_vms = 0 + var/access_warn_vms = 0 + /datum/configuration/New() var/list/L = typesof(/datum/game_mode) - /datum/game_mode for (var/T in L) @@ -819,6 +833,27 @@ var/list/gamemode_cache = list() if("log_gelf_port") config.log_gelf_port = value + if("ipintel_email") + if (value != "ch@nge.me") + ipintel_email = value + if("ipintel_rating_bad") + ipintel_rating_bad = text2num(value) + if("ipintel_rating_kick") + ipintel_rating_kick = text2num(value) + if("ipintel_domain") + ipintel_domain = value + if("ipintel_save_good") + ipintel_save_good = text2num(value) + if("ipintel_save_bad") + ipintel_save_bad = text2num(value) + + if("access_deny_new_accounts") + access_deny_new_accounts = text2num(value) >= 0 ? text2num(value) : -1 + if("access_deny_vms") + access_deny_vms = text2num(value) + if("access_warn_vms") + access_warn_vms = text2num(value) + else log_misc("Unknown setting in configuration: '[name]'") diff --git a/code/controllers/subsystems/ipintel.dm b/code/controllers/subsystems/ipintel.dm new file mode 100644 index 00000000000..0e43ef90689 --- /dev/null +++ b/code/controllers/subsystems/ipintel.dm @@ -0,0 +1,18 @@ +var/datum/controller/subsystem/ipintel/SSipintel + +/datum/controller/subsystem/ipintel + name = "XKeyScore" + init_order = SS_INIT_MISC_FIRST + flags = SS_NO_FIRE + 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/New() + NEW_SS_GLOBAL(SSipintel) + +/datum/controller/subsystem/ipintel/Initialize() + enabled = 1 + . = ..() diff --git a/code/modules/admin/DB ban/ban_mirroring.dm b/code/modules/admin/DB ban/ban_mirroring.dm index 34a2bf36a98..452a92d4d00 100644 --- a/code/modules/admin/DB ban/ban_mirroring.dm +++ b/code/modules/admin/DB ban/ban_mirroring.dm @@ -135,7 +135,33 @@ update_connection_data(C) return - var/list/conn_info = json_decode(data) + var/list/data_object = json_decode(data) + + if (!data_object || !data_object.len) + log_debug("CONN DATA: [C.ckey] has no connection data to showcase.") + return + + if (data_object["vms"]) + var/A = data_object["vms"] + var/count = 0 + if (A & 1) + count++ + if (A & 2) + count++ + + if (config.access_deny_vms && count >= config.access_deny_vms) + log_access("Failed Login: [C.ckey] [C.address] [C.computer_id] - Matching [count]/[config.access_deny_vms] VM identifiers. IDs: [A].", ckey = C.ckey) + message_admins("Failed Login: [C.ckey] [C.address] [C.computer_id] - Matching [count]/[config.access_deny_vms] VM identifiers. IDs: [A].") + spawn(20) + if (C) + del(C) + return + + if (config.access_warn_vms && count >= config.access_warn_vms) + log_access("Notice: [key_name(C)] [C.address] [C.computer_id] - Matching [count] VM identifiers. IDs: [A].", ckey = C.ckey) + message_admins("Notice: [key_name(C)] [C.address] [C.computer_id] - Matching [count] VM identifiers. IDs: [A].") + + var/list/conn_info = data_object["conn"] if (!conn_info || !conn_info.len) return diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index 287e096e04d..8cfdfb87580 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -169,7 +169,8 @@ var/list/admin_verbs_server = list( /client/proc/admin_edit_motd, /client/proc/toggle_recursive_explosions, /client/proc/restart_controller, - /client/proc/cmd_ss_panic + /client/proc/cmd_ss_panic, + /client/proc/configure_access_control ) var/list/admin_verbs_debug = list( /client/proc/getruntimelog, // allows us to access runtime logs to somebody, diff --git a/code/modules/admin/ipintel.dm b/code/modules/admin/ipintel.dm new file mode 100644 index 00000000000..ec7b63e5f46 --- /dev/null +++ b/code/modules/admin/ipintel.dm @@ -0,0 +1,129 @@ +/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*60*60*10)) + return TRUE + else + if (world.realtime < cacherealtime+(config.ipintel_save_bad*60*60*10)) + return TRUE + +/proc/get_ip_intel(ip, bypasscache = FALSE, updatecache = TRUE) + var/datum/ipintel/res = new() + 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(establish_db_connection(dbcon)) + var/DBQuery/query_get_ip_intel = dbcon.NewQuery({" + SELECT date, intel, TIMESTAMPDIFF(MINUTE,date,NOW()) + FROM ss13_ipintel + WHERE + ip = INET6_ATON('[ip]') + AND (( + intel < [config.ipintel_rating_bad] + AND + date + INTERVAL [config.ipintel_save_good] HOUR > NOW() + ) OR ( + intel >= [config.ipintel_rating_bad] + AND + date + INTERVAL [config.ipintel_save_bad] HOUR > NOW() + )) + "}) + if(!query_get_ip_intel.Execute()) + 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 + return + res.intel = ip_intel_query(ip) + if (updatecache && res.intel >= 0) + SSipintel.cache[ip] = res + if(establish_db_connection(dbcon)) + var/DBQuery/query_add_ip_intel = dbcon.NewQuery("INSERT INTO ss13_ipintel (ip, intel) VALUES (INET6_ATON('[ip]'), [res.intel]) ON DUPLICATE KEY UPDATE intel = VALUES(intel), date = NOW()") + query_add_ip_intel.Execute() + + + +/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=f") + + 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 + (10 * 120 * SSipintel.errors) + else + error += " Attempting retry on [ip]." + log_ipintel(error) + +/proc/log_ipintel(text) + log_game("IPINTEL: [text]") + log_debug("IPINTEL: [text]") diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm index a09b920bd49..0fb5594c93f 100644 --- a/code/modules/admin/topic.dm +++ b/code/modules/admin/topic.dm @@ -1606,6 +1606,10 @@ return + else if(href_list["access_control"]) + access_control_topic(href_list["access_control"]) + return + mob/living/proc/can_centcom_reply() return 0 diff --git a/code/modules/admin/verbs/access_control.dm b/code/modules/admin/verbs/access_control.dm new file mode 100644 index 00000000000..da20407fae6 --- /dev/null +++ b/code/modules/admin/verbs/access_control.dm @@ -0,0 +1,99 @@ +/client/proc/configure_access_control() + set category = "Server" + set name = "Configure Access Control" + + if (!check_rights(R_SERVER)) + return + + var/datum/browser/config_window = new(usr, "access_control", "Access Control") + config_window.add_head_content("Access Control") + + var/data = "These settings control who can access the server during this round.
" + data += "They must be reset every single time the server restarts.
" + data += "If you do not know what these do, you shouldn't be touching them!
" + + data += "

IP Intel Settings:



" + + data += "

Player Age Settings:



" + + data += "

VM Detection Settings:


" + + config_window.set_user(src.mob) + config_window.set_content(data) + config_window.open() + +/datum/admins/proc/access_control_topic(control) + if (!control) + to_chat(usr, "No control option sent. Cancelling.") + return + + if (!check_rights(R_SERVER)) + log_and_message_admins("has tried editing access control without the permissions to do so.") + return + + switch(control) + if ("intel_bad") + var/num = input("Please set the new threshold for warning based on IPintel (0 to disable).", "New Threshold", config.ipintel_rating_kick) as num + if (num < 0 || num > 1) + to_chat(usr, "Invalid number. Cancelling.") + return + + config.ipintel_rating_bad = num + if (num) + log_and_message_admins("has set the IPIntel warn threshold to [num].") + else + log_and_message_admins("has disabled warn based on IPIntel.") + if ("intel_kick") + var/num = input("Please set the new threshold for kicking based on IPintel (0 to disable).", "New Threshold", config.ipintel_rating_kick) as num + if (num < 0 || num > 1) + to_chat(usr, "Invalid number. Cancelling.") + return + + config.ipintel_rating_kick = num + if (num) + log_and_message_admins("has set the IPIntel kick threshold to [num].") + else + log_and_message_admins("has disabled kicking based on IPIntel.") + if ("new_players") + config.access_deny_new_players = !config.access_deny_new_players + log_and_message_admins("has [config.access_deny_new_players ? "ENABLED" : "DISABLED"] the kicking of new players.") + if ("new_accounts") + var/num = input("Please set the new threshold for denying access based on BYOND account age. (-1 to disable.)", "New Threshold", config.access_deny_new_accounts) as num + if (num < 0 && num != -1) + to_chat(usr, "Invalid number. Cancelling.") + return + + config.access_deny_new_accounts = num + if (num != -1) + log_and_message_admins("has set the access barrier for new BYOND accounts to [num] days.") + else + log_and_message_admins("has disabled kicking based on BYOND account age.") + if ("vm_warn") + var/num = input("Please set the new threshold for warning on login based on positive VM identifiers. (0 to disable.)", "New Threshold") in list(0, 1, 2) + + config.access_warn_vms = num + if (num) + log_and_message_admins("has set players with [config.access_warn_vms] positive identifiers out of 2 for VM usage to be warned.") + else + log_and_message_admins("has disabled warnings based on potential VM usage.") + if ("vm_kick") + var/num = input("Please set the new threshold for warning on login based on positive VM identifiers. (0 to disable.)", "New Threshold") in list(0, 1, 2) + + config.access_deny_vms = num + if (num) + log_and_message_admins("has set players with [config.access_deny_vms] positive identifiers out of 2 for VM usage to be warned.") + else + log_and_message_admins("has disabled warnings based on potential VM usage.") + else + to_chat(usr, "Unknown control message sent. Cancelling.") + + owner.configure_access_control() diff --git a/code/modules/admin/verbs/modifyvariables.dm b/code/modules/admin/verbs/modifyvariables.dm index fb5a3ecdbb2..5e48b99206a 100644 --- a/code/modules/admin/verbs/modifyvariables.dm +++ b/code/modules/admin/verbs/modifyvariables.dm @@ -9,6 +9,19 @@ var/list/VVlocked = list("vars", "holder", "client", "virus", "viruses", "cuffed var/list/VVicon_edit_lock = list("icon", "icon_state", "overlays", "underlays") var/list/VVckey_edit = list("key", "ckey") +// The paranoia box. Specify a var name => bitflags required to edit it. +// Allows the securing of specific variables for, for example, config. That alter +// how players could join! Definitely not something you want folks to touch if they +// don't have the perms. +var/list/VVdynamic_lock = list( + "access_deny_new_players" = R_SERVER, + "access_deny_new_accounts" = R_SERVER, + "access_deny_vms" = R_SERVER, + "access_warn_vms" = R_SERVER, + "ipintel_rating_bad" = R_SERVER, + "ipintel_rating_kick" = R_SERVER +) + /* /client/proc/cmd_modify_object_variables(obj/O as obj|mob|turf|area in world) // Acceptable 'in world', as VV would be incredibly hampered otherwise set category = "Debug" @@ -171,6 +184,8 @@ var/list/VVckey_edit = list("key", "ckey") if(!check_rights(R_SPAWN|R_DEBUG|R_DEV)) return if(variable in VVicon_edit_lock) if(!check_rights(R_FUN|R_DEBUG|R_DEV)) return + if(VVdynamic_lock[variable]) + if(!check_rights(VVdynamic_lock[variable])) return if(isnull(variable)) usr << "Unable to determine variable type." @@ -364,6 +379,8 @@ var/list/VVckey_edit = list("key", "ckey") if(!check_rights(R_SPAWN|R_DEBUG|R_DEV)) return if(param_var_name in VVicon_edit_lock) if(!check_rights(R_FUN|R_DEBUG|R_DEV)) return + if(VVdynamic_lock[variable]) + if(!check_rights(VVdynamic_lock[variable])) return variable = param_var_name @@ -426,6 +443,8 @@ var/list/VVckey_edit = list("key", "ckey") if(!check_rights(R_SPAWN|R_DEBUG|R_DEV)) return if(variable in VVicon_edit_lock) if(!check_rights(R_FUN|R_DEBUG|R_DEV)) return + if(VVdynamic_lock[variable]) + if(!check_rights(VVdynamic_lock[variable])) return if(!autodetect_class) diff --git a/code/modules/admin/verbs/randomverbs.dm b/code/modules/admin/verbs/randomverbs.dm index 27357258cfc..bfb6cafb703 100644 --- a/code/modules/admin/verbs/randomverbs.dm +++ b/code/modules/admin/verbs/randomverbs.dm @@ -197,6 +197,7 @@ proc/cmd_admin_mute(mob/M as mob, mute_type, automute = 0) if(M.client.prefs.muted & mute_type) muteunmute = "unmuted" M.client.prefs.muted &= ~mute_type + M.client.spam_alert = 0 else muteunmute = "muted" M.client.prefs.muted |= mute_type @@ -623,7 +624,7 @@ Traitors and the like can also be revived with the previous role mostly intact. if (!holder) src << "Only administrators may use this command." return - + for(var/datum/job/job in SSjobs.occupations) src << "[job.title]: [job.total_positions]" feedback_add_details("admin_verb","LFS") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! diff --git a/code/modules/client/asset_cache.dm b/code/modules/client/asset_cache.dm index fb84b0f4f21..d0489e32955 100644 --- a/code/modules/client/asset_cache.dm +++ b/code/modules/client/asset_cache.dm @@ -178,7 +178,8 @@ var/list/asset_datums = list() "bootstrap.min.css" = 'html/bootstrap/css/bootstrap.min.css', "bootstrap.min.js" = 'html/bootstrap/js/bootstrap.min.js', "jquery-2.0.0.min.js" = 'html/jquery/jquery-2.0.0.min.js', - "ie-truth.min.js" = 'html/iestats/ie-truth.min.js' + "ie-truth.min.js" = 'html/iestats/ie-truth.min.js', + "conninfo.min.js" = 'html/iestats/conninfo.min.js' ) /datum/asset/simple/paper @@ -221,7 +222,7 @@ var/list/asset_datums = list() var/name = "pill[i].png" register_asset(name, icon('icons/obj/chemical.dmi', "pill[i]")) assets += name - + for (var/sprite in bottle_sprites) var/name = "[sprite].png" register_asset(name, icon('icons/obj/chemical.dmi', sprite)) diff --git a/code/modules/client/client defines.dm b/code/modules/client/client defines.dm index bf996843542..9b227dd1865 100644 --- a/code/modules/client/client defines.dm +++ b/code/modules/client/client defines.dm @@ -1,6 +1,6 @@ /client parent_type = /datum - + //////////////// //ADMIN THINGS// //////////////// @@ -41,6 +41,7 @@ var/info_sent = 0 // comment out the line below when debugging locally to enable the options & messages menu //control_freak = 1 + var/ip_intel = "Disabled" var/received_discord_pm = -99999 var/discord_admin //IRC- no more IRC, K? Discord admin that spoke with them last. @@ -55,6 +56,8 @@ var/related_accounts_cid = "Requires database" //So admins know why it isn't working - Used to determine what other accounts previously logged in from this computer id var/whitelist_status = 0 //Used to determine what whitelists the player has access to. Uses bitflag values! var/need_saves_migrated = "Requires database" //Used to determine whether or not the ckey needs their saves migrated over to the database. Default is 0 upon successful connection. + var/account_age = -1 // Age on the BYOND account in days. + var/account_join_date = null // Date of the BYOND account creation in ISO 8601 format. preload_rsc = 0 // This is 0 so we can set it to an URL once the player logs in and have them download the resources from a different server. diff --git a/code/modules/client/client procs.dm b/code/modules/client/client procs.dm index 91a50dfcd37..332e0f80416 100644 --- a/code/modules/client/client procs.dm +++ b/code/modules/client/client procs.dm @@ -222,6 +222,7 @@ src << "You have tripped the macro filter. An auto-mute was applied." last_message_time = world.time + spam_alert = 4 return 1 else spam_alert = max(0, spam_alert--) @@ -292,6 +293,35 @@ log_client_to_db() + // New player, and we don't want any. + if (!holder) + if (config.access_deny_new_players && !player_age) + log_access("Failed Login: [key] [computer_id] [address] - New player attempting connection during panic bunker.", ckey = ckey) + message_admins("Failed Login: [key] [computer_id] [address] - New player attempting connection during panic bunker.") + to_chat(src, "Apologies, but the server is currently not accepting connections from never before seen players.") + del(src) + return 0 + + // Check if the account is too young. + if (config.access_deny_new_accounts != -1 && account_age != -1 && account_age <= config.access_deny_new_accounts) + log_access("Failed Login: [key] [computer_id] [address] - Account too young to play. [account_age] days.", ckey = ckey) + message_admins("Failed Login: [key] [computer_id] [address] - Account too young to play. [account_age] days.") + to_chat(src, "Apologies, but the server is currently not accepting connections from BYOND accounts this young.") + del(src) + return 0 + + if (byond_version < config.client_error_version) + src << "Your version of BYOND is too old!" + src << config.client_error_message + src << "Your version: [byond_version]." + src << "Required version: [config.client_error_version] or later." + src << "Visit http://www.byond.com/download/ to get the latest version of BYOND." + if (holder) + src << "Admins get a free pass. However, please update your BYOND as soon as possible. Certain things may cause crashes if you play with your present version." + else + del(src) + return 0 + //preferences datum - also holds some persistant data for the client (because we may as well keep these datums to a minimum) prefs = preferences_datums[ckey] if(!prefs) @@ -307,19 +337,6 @@ prefs.sanitize_preferences() - if (byond_version < config.client_error_version) - src << "Your version of BYOND is too old!" - src << config.client_error_message - src << "Your version: [byond_version]." - src << "Required version: [config.client_error_version] or later." - src << "Visit http://www.byond.com/download/ to get the latest version of BYOND." - if (holder) - src << "Admins get a free pass. However, please update your BYOND as soon as possible. Certain things may cause crashes if you play with your present version." - else - del(src) - return 0 - - if(holder) add_admin_verbs() @@ -340,6 +357,8 @@ // Check code/modules/admin/verbs/antag-ooc.dm for definition add_aooc_if_necessary() + check_ip_intel() + ////////////// //DISCONNECT// ////////////// @@ -372,26 +391,38 @@ return -1 /client/proc/log_client_to_db() - - if ( IsGuestKey(src.key) ) + if (IsGuestKey(src.key)) return - establish_db_connection(dbcon) - if(!dbcon.IsConnected()) + if(!establish_db_connection(dbcon)) return var/sql_ckey = sql_sanitize_text(src.ckey) - var/DBQuery/query = dbcon.NewQuery("SELECT id, datediff(Now(),firstseen) as age, whitelist_status, migration_status FROM ss13_player WHERE ckey = '[sql_ckey]'") - query.Execute() - var/sql_id = 0 + var/DBQuery/query = dbcon.NewQuery("SELECT datediff(Now(),firstseen) as age, whitelist_status, account_join_date, DATEDIFF(NOW(), account_join_date) FROM ss13_player WHERE ckey = '[sql_ckey]'") + + if(!query.Execute()) + return + + var/found = 0 player_age = 0 // New players won't have an entry so knowing we have a connection we set this to zero to be updated if their is a record. - while(query.NextRow()) - sql_id = query.item[1] - player_age = text2num(query.item[2]) - whitelist_status = text2num(query.item[3]) - need_saves_migrated = text2num(query.item[4]) - break + + if (query.NextRow()) + found = 1 + player_age = text2num(query.item[1]) + whitelist_status = text2num(query.item[2]) + account_join_date = query.item[3] + account_age = text2num(query.item[4]) + if (!account_age) + account_join_date = sanitizeSQL(findJoinDate()) + if (!account_join_date) + account_age = -1 + else + var/DBQuery/query_datediff = dbcon.NewQuery("SELECT DATEDIFF(NOW(), [account_join_date])") + if (!query_datediff.Execute()) + return + if (query_datediff.NextRow()) + account_age = text2num(query_datediff.item[1]) var/DBQuery/query_ip = dbcon.NewQuery("SELECT ckey FROM ss13_player WHERE ip = '[address]'") query_ip.Execute() @@ -407,13 +438,6 @@ related_accounts_cid += "[query_cid.item[1]], " break - //Just the standard check to see if it's actually a number - if(sql_id) - if(istext(sql_id)) - sql_id = text2num(sql_id) - if(!isnum(sql_id)) - return - var/admin_rank = "Player" if(src.holder) admin_rank = src.holder.rank @@ -422,16 +446,18 @@ var/sql_computerid = sql_sanitize_text(src.computer_id) var/sql_admin_rank = sql_sanitize_text(admin_rank) - - if(sql_id) + if(found) //Player already identified previously, we need to just update the 'lastseen', 'ip' and 'computer_id' variables - var/DBQuery/query_update = dbcon.NewQuery("UPDATE ss13_player SET lastseen = Now(), ip = '[sql_ip]', computerid = '[sql_computerid]', lastadminrank = '[sql_admin_rank]' WHERE id = [sql_id]") + var/DBQuery/query_update = dbcon.NewQuery("UPDATE ss13_player SET lastseen = Now(), ip = '[sql_ip]', computerid = '[sql_computerid]', lastadminrank = '[sql_admin_rank]', account_join_date = [account_join_date ? "'[account_join_date]'" : "NULL"] WHERE ckey = '[sql_ckey]'") query_update.Execute() else //New player!! Need to insert all the stuff - var/DBQuery/query_insert = dbcon.NewQuery("INSERT INTO ss13_player (id, ckey, firstseen, lastseen, ip, computerid, lastadminrank) VALUES (null, '[sql_ckey]', Now(), Now(), '[sql_ip]', '[sql_computerid]', '[sql_admin_rank]')") + var/DBQuery/query_insert = dbcon.NewQuery("INSERT INTO ss13_player (ckey, firstseen, lastseen, ip, computerid, lastadminrank, account_join_date) VALUES ('[sql_ckey]', Now(), Now(), '[sql_ip]', '[sql_computerid]', '[sql_admin_rank]', [account_join_date ? "'[account_join_date]'" : "NULL"])") query_insert.Execute() + if (!account_join_date) + account_join_date = "Error" + //Logging player access var/serverip = "[world.internet_address]:[world.port]" var/DBQuery/query_accesslog = dbcon.NewQuery("INSERT INTO `ss13_connection_log`(`id`,`datetime`,`serverip`,`ckey`,`ip`,`computerid`) VALUES(null,Now(),'[serverip]','[sql_ckey]','[sql_ip]','[sql_computerid]');") @@ -585,6 +611,32 @@ server_greeting.display_to_client(src) +/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) + var/datum/ipintel/res = get_ip_intel(address) + if (config.ipintel_rating_kick && res.intel >= config.ipintel_rating_kick) + message_admins("Proxy Detection: [key_name_admin(src)] IP intel rated [res.intel*100]% likely to be a proxy/VPN. They are being kicked because of this.") + log_admin("Proxy Detection: [key_name_admin(src)] IP intel rated [res.intel*100]% likely to be a proxy/VPN. They are being kicked because of this.") + to_chat(src, "Usage of proxies is not permitted by the rules. You are being kicked because of this.") + del(src) + else if (res.intel >= config.ipintel_rating_bad) + message_admins("Proxy Detection: [key_name_admin(src)] IP intel rated [res.intel*100]% likely to be a Proxy/VPN.") + ip_intel = res.intel + +/client/proc/findJoinDate() + var/list/http = world.Export("http://byond.com/members/[ckey]?format=text") + if(!http) + log_debug("ACCESS CONTROL: Failed to connect to byond age check for [ckey]") + return + var/F = file2text(http["CONTENT"]) + if(F) + var/regex/R = regex("joined = \"(\\d{4}-\\d{2}-\\d{2})\"") + if(R.Find(F)) + . = R.group[1] + else + CRASH("Age check regex failed for [src.ckey]") + // Byond seemingly calls stat, each tick. // Calling things each tick can get expensive real quick. // So we slow this down a little. diff --git a/config/example/config.txt b/config/example/config.txt index f48bf8ecea9..6566696c991 100644 --- a/config/example/config.txt +++ b/config/example/config.txt @@ -448,3 +448,32 @@ API_RATE_LIMIT_WHITELIST 127.0.0.1 LOG_GELF_ENABLED 0 LOG_GELF_IP 192.168.100.80 LOG_GELF_PORT 12201 + +### 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 +## Rating to kick at. Keeps kicking a person if they join with a certain level of intel. Recommended value is 1. Leave out if you don't want to enable automatic kicking. +#IPINTEL_RATING_KICK 1 +## 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 12 +## 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 3 +## 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 + +### ACCESS CONTROL: +### This allows you to adjust various parametres by which potentially malicious users can by identified. +### Note that none of these are 100% accurate, and should only be used in cases where a "panic bunker" is warranted. +### They don't apply bans, at will, at worst, kick folks. +## This determined whether or not a player's account can be denied entry based on their BYOND account age. +## If their account is younger than the amount of days specified here, they cannot enter. This is inclusive. +# ACCESS_DENY_NEW_ACCOUNTS 1 +## This determines how many VM identifiers must be spotted on a user before they're kicked. Currently 2 are implemented. +## Leave at 0 to disable, or commented out. +# ACCESS_DENY_VMS 0 +## This determines how many VM identifiers must be spotted on a user before admins are warned about them joining. +## Leave at 0 or commented out to disable. +# ACCESS_WARN_VMS 2 diff --git a/html/iestats/conninfo.min.js b/html/iestats/conninfo.min.js new file mode 100644 index 00000000000..cea0d795a29 --- /dev/null +++ b/html/iestats/conninfo.min.js @@ -0,0 +1 @@ +function setCookie(e,t,n){var r=new Date;r.setTime(r.getTime()+24*n*60*60*1e3);var o="expires="+r.toUTCString();document.cookie=e+"="+t+";"+o+";path=/"}function getCookie(e){for(var t=e+"=",n=document.cookie.split(";"),r=0;r Aurorastation Welcome Screen - + @@ -57,6 +57,7 @@ +