diff --git a/code/controllers/subsystem/stickyban.dm b/code/controllers/subsystem/stickyban.dm
new file mode 100644
index 00000000000..bed43b52cc1
--- /dev/null
+++ b/code/controllers/subsystem/stickyban.dm
@@ -0,0 +1,36 @@
+var/datum/subsystem/stickyban/SSstickyban
+
+/datum/subsystem/stickyban
+ name = "Sticky Ban"
+ init_order = -10
+ flags = SS_NO_FIRE
+
+ var/list/cache = list()
+
+/datum/subsystem/stickyban/New()
+ NEW_SS_GLOBAL(SSstickyban)
+
+/datum/subsystem/stickyban/Initialize(timeofday)
+ var/list/bannedkeys = world.GetConfig("ban")
+ //sanitize the sticky ban list
+ for (var/bannedkey in bannedkeys)
+ var/ckey = ckey(bannedkey)
+ var/list/ban = stickyban2list(world.GetConfig("ban", bannedkey))
+
+ //byond stores sticky bans by key, that can end up confusing things
+ //i also remove it here so that if any stickybans cause a runtime, they just stop existing
+ world.SetConfig("ban", bannedkey, null)
+
+ if (!ban["ckey"])
+ ban["ckey"] = ckey
+
+ //storing these can break things and isn't needed for sticky ban tracking
+ ban -= "IP"
+ ban -= "computer_id"
+
+ world.SetConfig("ban", ckey, list2stickyban(ban))
+
+ ban["matches_this_round"] = list()
+ ban["existing_user_matches_this_round"] = list()
+ ban["admin_matches_this_round"] = list()
+ cache[ckey] = ban
diff --git a/code/modules/admin/IsBanned.dm b/code/modules/admin/IsBanned.dm
index 75eaeab65fb..9b229eb107b 100644
--- a/code/modules/admin/IsBanned.dm
+++ b/code/modules/admin/IsBanned.dm
@@ -1,11 +1,17 @@
//Blocks an attempt to connect before even creating our client datum thing.
+//How many new ckey matches before we revert the stickyban to it's roundstart state
+//These are exclusive, so once it goes over one of these numbers, it reverts the ban
+#define STICKYBAN_MAX_MATCHES 20
+#define STICKYBAN_MAX_EXISTING_USER_MATCHES 5 //ie, users who were connected before the ban triggered
+#define STICKYBAN_MAX_ADMIN_MATCHES 2
+
/world/IsBanned(key,address,computer_id)
if (!key || !address || !computer_id)
log_access("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.)")
-
- if (text2num(computer_id) == 2147483647) //this cid causes stickybans to go haywire
+
+ if (text2num(computer_id) == 2147483647) //this cid causes stickybans to go haywire
log_access("Failed Login (invalid cid): [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 an invalid Computer ID.)")
var/admin = 0
@@ -99,17 +105,83 @@
log_access("Failed Login: [key] [computer_id] [address] - Banned [.["reason"]]")
return .
- . = ..() //default pager ban stuff
- if (.)
+ var/list/ban = ..() //default pager ban stuff
+ if (ban)
+ var/bannedckey = "ERROR"
+ if (ban["ckey"])
+ bannedckey = ban["ckey"]
+
+ var/newmatch = FALSE
+ var/client/C = directory[ckey]
+ var/cachedban = SSstickyban.cache[bannedckey]
+
+ //rogue ban in the process of being reverted.
+ if (cachedban && cachedban["reverting"])
+ return null
+
+ if (cachedban && ckey != bannedckey)
+ newmatch = TRUE
+ if (cachedban["keys"])
+ if (cachedban["keys"][ckey])
+ newmatch = FALSE
+ if (cachedban["matches_this_round"][ckey])
+ newmatch = FALSE
+
+ if (newmatch && cachedban)
+ var/list/newmatches = cachedban["matches_this_round"]
+ var/list/newmatches_connected = cachedban["existing_user_matches_this_round"]
+ var/list/newmatches_admin = cachedban["admin_matches_this_round"]
+
+ newmatches[ckey] = ckey
+ if (C)
+ newmatches_connected[ckey] = ckey
+ if (admin)
+ newmatches_admin[ckey] = ckey
+
+ if (\
+ newmatches.len > STICKYBAN_MAX_MATCHES || \
+ newmatches_connected.len > STICKYBAN_MAX_EXISTING_USER_MATCHES || \
+ newmatches_admin.len > STICKYBAN_MAX_ADMIN_MATCHES \
+ )
+ if (cachedban["reverting"])
+ return null
+ cachedban["reverting"] = TRUE
+
+ world.SetConfig("ban", bannedckey, null)
+
+ log_game("Stickyban on [bannedckey] detected as rogue, reverting to it's roundstart state")
+ message_admins("Stickyban on [bannedckey] detected as rogue, reverting to it's roundstart state")
+ //do not convert to timer.
+ spawn (5)
+ world.SetConfig("ban", bannedckey, null)
+ sleep(1)
+ world.SetConfig("ban", bannedckey, null)
+ cachedban["matches_this_round"] = list()
+ cachedban["existing_user_matches_this_round"] = list()
+ cachedban["admin_matches_this_round"] = list()
+ cachedban -= "reverting"
+ world.SetConfig("ban", bannedckey, list2stickyban(cachedban))
+ return null
+
//byond will not trigger isbanned() for "global" host bans,
//ie, ones where the "apply to this game only" checkbox is not checked (defaults to not checked)
//So it's safe to let admins walk thru host/sticky bans here
if (admin)
- log_admin("The admin [key] has been allowed to bypass a matching host/sticky ban")
- message_admins("The admin [key] has been allowed to bypass a matching host/sticky ban")
- addclientmessage(ckey,"You have been allowed to bypass a matching host/sticky ban")
+ log_admin("The admin [key] has been allowed to bypass a matching host/sticky ban on [bannedckey]")
+ message_admins("The admin [key] has been allowed to bypass a matching host/sticky ban on [bannedckey]")
+ addclientmessage(ckey,"You have been allowed to bypass a matching host/sticky ban on [bannedckey]")
return null
- else
- log_access("Failed Login: [key] [computer_id] [address] - Banned [.["message"]]")
+
+ if (C) //user is already connected!.
+ C << "You are about to get disconnected for matching a sticky ban after you connected. If this turns out to be the ban evasion detection system going haywire, we will automatically detect this and revert the matches. if you feel that this is the case, please wait EXACTLY 6 seconds then reconnect using file -> reconnect to see if the match was reversed."
+
+ var/desc = "\nReason:(StickyBan) You, or another user of this computer or connection ([bannedckey]) is banned from playing here. The ban reason is:\n[ban["message"]]\nThis ban was applied by [ban["admin"]]\nThis is a BanEvasion Detection System ban, if you think this ban is a mistake, please wait EXACTLY 6 seconds, then try again before filing an appeal.\n"
+ . = list("reason" = "Stickyban", "desc" = desc)
+ log_access("Failed Login: [key] [computer_id] [address] - StickyBanned [ban["message"]] Target Username: [bannedckey] Placed by [ban["admin"]]")
return .
+
+
+#undef STICKYBAN_MAX_MATCHES
+#undef STICKYBAN_MAX_EXISTING_USER_MATCHES
+#undef STICKYBAN_MAX_ADMIN_MATCHES
diff --git a/code/modules/admin/stickyban.dm b/code/modules/admin/stickyban.dm
index e570fa89fb3..3e01e50fd59 100644
--- a/code/modules/admin/stickyban.dm
+++ b/code/modules/admin/stickyban.dm
@@ -18,6 +18,8 @@
if (!ckey)
return
ckey = ckey(ckey)
+ ban["ckey"] = ckey
+
if (get_stickyban_from_ckey(ckey))
usr << "Error: Can not add a stickyban: User already has a current sticky ban"
@@ -125,9 +127,37 @@
log_admin("[key_name(usr)] has edited [ckey]'s sticky ban reason from [oldreason] to [reason]")
message_admins("[key_name_admin(usr)] has edited [ckey]'s sticky ban reason from [oldreason] to [reason]")
+ if ("revert")
+ if (!data["ckey"])
+ return
+ var/ckey = data["ckey"]
+ if (alert("Are you sure you want to revert the sticky ban on [ckey] to its state at round start?","Are you sure","Yes","No") == "No")
+ return
+ var/ban = get_stickyban_from_ckey(ckey)
+ if (!ban)
+ usr << "Error: No sticky ban for [ckey] found!"
+ return
+ var/cached_ban = SSstickyban.cache[ckey]
+ if (!cached_ban)
+ usr << "Error: No cached sticky ban for [ckey] found!"
+ world.SetConfig("ban",ckey,null)
+
+ log_admin("[key_name(usr)] has reverted [ckey]'s sticky ban to it's state at round start.")
+ message_admins("[key_name_admin(usr)] has reverted [ckey]'s sticky ban to it's state at round start.")
+ //revert is mostly used when shit goes rouge, so we have to set it to null
+ // and wait a byond tick before assigning it to ensure byond clears its shit.
+ sleep(world.tick_lag)
+ world.SetConfig("ban",ckey,list2stickyban(cached_ban))
+
+
/datum/admins/proc/stickyban_gethtml(ckey, ban)
- . = "\[-\][ckey]
"
- . += "[ban["message"]] \[Edit\]
"
+ . = {"
+ \[-\]
+ \[revert\]
+ [ckey]
+
"
+ [ban["message"]] \[Edit\]
+ "}
if (ban["admin"])
. += "[ban["admin"]]
"
else
@@ -175,7 +205,13 @@
if (!ban)
return null
. = params2list(ban)
- .["keys"] = splittext(.["keys"], ",")
+ if (.["keys"])
+ var/keys = splittext(.["keys"], ",")
+ var/ckeys = list()
+ for (var/key in keys)
+ var/ckey = ckey(key)
+ ckeys[ckey] = ckey //to make searching faster.
+ .["keys"] = ckeys
.["type"] = splittext(.["type"], ",")
.["IP"] = splittext(.["IP"], ",")
.["computer_id"] = splittext(.["computer_id"], ",")
@@ -189,10 +225,18 @@
.["keys"] = jointext(.["keys"], ",")
if (.["type"])
.["type"] = jointext(.["type"], ",")
- if (.["IP"])
- .["IP"] = jointext(.["IP"], ",")
- if (.["computer_id"])
- .["computer_id"] = jointext(.["computer_id"], ",")
+
+ //internal tracking only, shouldn't be stored
+ . -= "existing_user_matches_this_round"
+ . -= "admin_matches_this_round"
+ . -= "matches_this_round"
+ . -= "reverting"
+
+ //storing these can sometimes cause sticky bans to start matching everybody
+ // and isn't even needed for sticky ban matching, as the hub tracks these seperately
+ . -= "IP"
+ . -= "computer_id"
+
. = list2params(.)
diff --git a/tgstation.dme b/tgstation.dme
index d572a21bf0c..a2b76c76024 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -154,6 +154,7 @@
#include "code\controllers\subsystem\radio.dm"
#include "code\controllers\subsystem\server_maintenance.dm"
#include "code\controllers\subsystem\shuttles.dm"
+#include "code\controllers\subsystem\stickyban.dm"
#include "code\controllers\subsystem\sun.dm"
#include "code\controllers\subsystem\tgui.dm"
#include "code\controllers\subsystem\ticker.dm"