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 += "
Current warning level: [config.ipintel_rating_bad ? "[config.ipintel_rating_bad]" : "DISABLED"]. Edit
"
+ data += "
Current kick level: [config.ipintel_rating_kick ? "[config.ipintel_rating_kick]" : "DISABLED"]. Edit
"
+ data += "
"
+
+ data += "
Player Age Settings:
"
+ data += "
New players: [config.access_deny_new_players ? "ALLOWED" : "DENIED"]. Toggle
VM identifier count to warn on: [config.access_warn_vms ? "[config.access_warn_vms]" : "DISABLED"]. Edit
"
+ data += "
VM identifier count to kick on: [config.access_deny_vms ? "[config.access_deny_vms]" : "DISABLED"]. Edit
"
+ data += "
"
+
+ 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;rAurorastation Welcome Screen
-
+
@@ -57,6 +57,7 @@
+