Files
Letter N 37c6a7d1fc time, is it really that time (#7482)
<!-- Write **BELOW** The Headers and **ABOVE** The comments else it may
not be viewable. -->
<!-- You can view Contributing.MD for a detailed description of the pull
request process. -->

## About The Pull Request
update time related procs & glob too (i was originally targeting that
lmao).
also **stop** using byondtime for sql stuff, we have `NOW()`!!
<!-- Describe The Pull Request. Please be sure every change is
documented or this can delay review and even discourage maintainers from
merging your PR! -->

## Why It's Good For The Game

<!-- Argue for the merits of your changes and how they benefit the game,
especially if they are controversial and/or far reaching. If you can't
actually explain WHY what you are doing will improve the game, then it
probably isn't good for the game in the first place. -->

## Changelog

<!-- If your PR modifies aspects of the game that can be concretely
observed by players or admins you should add a changelog. If your change
does NOT meet this description, remove this section. Please note that
maintainers freely reserve the right to remove and add tags should they
deem it appropriate. You can attempt to finagle the system all you want,
but it's best to shoot for clear communication right off the bat. -->

<!-- Both 🆑's are required for the changelog to work! You can put
your name to the right of the first 🆑 if you want to overwrite your
GitHub username as author ingame. -->
<!-- You can use multiple of the same prefix (they're only used for the
icon ingame) and delete the unneeded ones. Despite some of the tags,
changelogs should generally represent how a player might be affected by
the changes rather than a summary of the PR's contents. -->
2026-01-13 22:06:30 -05:00

217 lines
6.4 KiB
Plaintext

/**
* IPIntel Subsystem
*/
SUBSYSTEM_DEF(ipintel)
name = "IPIntel"
subsystem_flags = SS_NO_FIRE | SS_NO_INIT
/// is ipintel enabled?
var/enabled = FALSE
/// threshold for blocking vpns
var/vpn_threshold
/// ip (as client.address form) to cache entry
var/static/list/vpn_cache = list()
/// current consequetive errors
var/consequetive_errors = 0
/// next time before we try again once errored
var/next_attempt = 0
/// retry delay
var/retry_delay = 4 SECONDS
/// max retries
var/max_retries = 1
/datum/controller/subsystem/ipintel/on_config_loaded()
. = ..()
enabled = !!CONFIG_GET(flag/ipintel_enabled)
consequetive_errors = 0
next_attempt = 0
vpn_threshold = CONFIG_GET(number/ipintel_rating_bad)
/datum/controller/subsystem/ipintel/proc/vpn_connection_check(address, ckey)
if(!CONFIG_GET(flag/ipintel_enabled))
return
var/score = vpn_score(address)
if(isnull(score))
log_and_message_admins("Unable to check IPIntel for [ckey].")
log_access("[ckey] ([address]) could not be checked by IPIntel.")
return
if(score >= vpn_threshold)
log_and_message_admins("[ckey] detected to likely be using a vpn ([score] >= [vpn_threshold])")
log_access("[ckey] ([address]) is likely using a vpn ([score] >= [vpn_threshold])")
/datum/controller/subsystem/ipintel/proc/vpn_score(address)
var/datum/ipintel/cached = vpn_cache[address]
if(isnull(cached))
var/datum/ipintel/fetched = ipintel_cache_fetch(address)
if(!isnull(fetched))
log_ipintel("successfully fetched cache for [address]")
cached = fetched
vpn_cache[address] = fetched
if(cached?.is_valid())
log_ipintel("using valid cache for [address]")
return cached.intel
log_ipintel("using api for [address]")
var/score = ipintel_query(address)
if(isnull(score))
return
var/datum/ipintel/result = new
result.intel = score
result.address = address
ipintel_cache_store(result)
vpn_cache[address] = result
return result.intel
/datum/controller/subsystem/ipintel/proc/vpn_check(address)
return vpn_score(address) >= vpn_threshold
/datum/controller/subsystem/ipintel/proc/ipintel_query(address, retries)
PRIVATE_PROC(TRUE)
// bruh it's localhost
if(address == "127.0.0.1" || isnull(address))
return 0
// no flooding API without cache being available
if(!SSdbcore.Connect())
log_ipintel("ipintel: no DB")
message_admins("IPIntel failed due to lack of database. Yell at your hosts.")
return
if(retries > max_retries)
log_ipintel("ipintel: bailing for [address] due to [retries] > [max_retries].")
return
if(!address)
return
if(next_attempt > REALTIMEOFDAY)
return
if(!enabled)
return
var/list/http[] = world.Export("http://[CONFIG_GET(string/ipintel_domain)]/check.php?ip=[address]&contact=[CONFIG_GET(string/ipintel_email)]&format=json&flags=f")
if(isnull(http))
ipintel_error(address, "Unable to connect", retries)
retries++
sleep(retry_delay)
return .()
var/status = text2num(http["STATUS"])
if(status == 200)
// success
var/response = json_decode(file2text(http["CONTENT"]))
if(isnull(response))
ipintel_error(address, "Code 400, but no response. Bailing out.")
return
if(response["status"] == "success")
var/parsed = text2num(response["result"])
if(isnum(parsed))
// reset error counts
consequetive_errors = 0
next_attempt = 0
return parsed
ipintel_error(address, "Bad intel from server: [response["result"]]", retries)
retries++
sleep(retry_delay)
return .()
else
ipintel_error(address, "Bad response from server: [response["status"]]", retries)
retries++
sleep(retry_delay)
return .()
else if(status == 429)
// ratelimited
ipintel_error(address, "Code 429: Ratelimited")
return
else
ipintel_error(address, "Code [status]: Unknown", retries)
retries++
sleep(retry_delay)
return .()
/datum/controller/subsystem/ipintel/proc/ipintel_cache_fetch(address)
PRIVATE_PROC(TRUE)
if(!SSdbcore.Connect())
return
// admin proccall guard override - there's no volatile args here
var/old_usr = usr
usr = null
. = ipintel_cache_fetch_impl(address)
usr = old_usr
/datum/controller/subsystem/ipintel/proc/ipintel_cache_fetch_impl(address)
PRIVATE_PROC(TRUE)
var/datum/db_query/fetch = SSdbcore.NewQuery({"
SELECT date, intel, TIMESTAMPDIFF(MINUTE,date,NOW())
FROM [DB_PREFIX_TABLE_NAME("ipintel")]
WHERE ip = INET_ATON(:ip)
"}, list("ip" = address)
)
fetch.Execute()
if(fetch.NextRow())
var/datum/ipintel/fetched = new /datum/ipintel
. = fetched
fetched.address = address
fetched.intel = text2num(fetch.item[2])
fetched.cached_timestamp = fetch.item[1]
fetched.cached_realtime = world.realtime - (text2num(fetch.item[3]) * 10 * 60)
qdel(fetch)
/datum/controller/subsystem/ipintel/proc/ipintel_cache_store(datum/ipintel/entry)
PRIVATE_PROC(TRUE)
if(!SSdbcore.Connect())
return
// admin proccall guard override - there's no volatile args here
var/old_usr = usr
usr = null
. = ipintel_cache_store_impl(entry)
usr = old_usr
/datum/controller/subsystem/ipintel/proc/ipintel_cache_store_impl(datum/ipintel/entry)
PRIVATE_PROC(TRUE)
var/datum/db_query/update = SSdbcore.NewQuery(
"INSERT INTO [DB_PREFIX_TABLE_NAME("ipintel")] (ip, intel) VALUES (INET_ATON(:ip), :intel) \
ON DUPLICATE KEY UPDATE intel = VALUES(intel), date = NOW()",
list(
"ip" = entry.address,
"intel" = entry.intel,
)
)
update.Execute()
qdel(update)
/datum/controller/subsystem/ipintel/proc/ipintel_error(address, error, retries)
PRIVATE_PROC(TRUE)
var/str = "IPIntel error handling on [address]: "
if(retries)
consequetive_errors++
var/how_long = consequetive_errors * 2 MINUTES
str += "Could not check [address]. Disabling IPIntel for [DisplayTimeText(how_long)]."
next_attempt = REALTIMEOFDAY + how_long
else
str += "Attempting to retry."
/datum/ipintel
var/address
var/intel
var/cached_timestamp
var/cached_realtime
/datum/ipintel/New()
cached_timestamp = ISOtime()
cached_realtime = world.realtime
/datum/ipintel/proc/is_valid()
. = FALSE
var/allowable_hours = intel < SSipintel.vpn_threshold? CONFIG_GET(number/ipintel_save_good) : CONFIG_GET(number/ipintel_save_bad)
return world.realtime < cached_realtime + (allowable_hours HOURS)
/**
/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)
*/