Files

591 lines
19 KiB
Plaintext

////////////
//SECURITY//
////////////
#define LIMITER_SIZE 5
#define CURRENT_SECOND 1
#define SECOND_COUNT 2
#define CURRENT_MINUTE 3
#define MINUTE_COUNT 4
#define ADMINSWARNED_AT 5
/**
* A 'hijack' / intercept proc.
* Override this and return TRUE if you handled it.
* This allows for clients to not have an overloaded Topic().
*
* The downside is additional overhead is generated due to the proc call overheads.
*
* * Logging & topic spam prevention is ran before this proc.
*
* @params
* * raw_href - the raw ?querystring that's sent by the client
* * href_list - the decoded, key-value query sent by client
* * raw_src - the raw src resolved by BYOND.
*/
/client/proc/on_topic_hook(raw_href, list/href_list, raw_src)
return FALSE
/*
When somebody clicks a link in game, this Topic is called first.
It does the stuff in this proc and then is redirected to the Topic() proc for the src=[0xWhatever]
(if specified in the link). ie locate(hsrc).Topic()
Such links can be spoofed.
Because of this certain things MUST be considered whenever adding a Topic() for something:
- Can it be fed harmful values which could cause runtimes?
- Is the Topic call an admin-only thing?
- If so, does it have checks to see if the person who called it (usr.client) is an admin?
- Are the processes being called by Topic() particularly laggy?
- If so, is there any protection against somebody spam-clicking a link?
If you have any questions about this stuff feel free to ask. ~Carn
*/
/client/Topic(href, href_list, hsrc, hsrc_command)
if(!usr || usr != mob) //stops us calling Topic for somebody else's client. Also helps prevent usr=null
return
#ifndef TESTING
if (lowertext(hsrc_command) == "_debug") //disable the integrated byond vv in the client side debugging tools since it doesn't respect vv read protections
return
#endif
// Rate limiting
var/mtl = config_legacy.minute_topic_limit //CONFIG_GET(number/minute_topic_limit)
if (!holder && mtl)
var/minute = round(world.time, 600)
if (!topiclimiter)
topiclimiter = new(LIMITER_SIZE)
if (minute != topiclimiter[CURRENT_MINUTE])
topiclimiter[CURRENT_MINUTE] = minute
topiclimiter[MINUTE_COUNT] = 0
topiclimiter[MINUTE_COUNT] += 1
if (topiclimiter[MINUTE_COUNT] > mtl)
var/msg = "Your previous action was ignored because you've done too many in a minute."
if (minute != topiclimiter[ADMINSWARNED_AT]) //only one admin message per-minute. (if they spam the admins can just boot/ban them)
topiclimiter[ADMINSWARNED_AT] = minute
msg += " Administrators have been informed."
log_game("[key_name(src)] Has hit the per-minute topic limit of [mtl] topic calls in a given game minute")
message_admins("[ADMIN_LOOKUPFLW(usr)] [ADMIN_KICK(usr)] Has hit the per-minute topic limit of [mtl] topic calls in a given game minute")
to_chat(src, "<span class='danger'>[msg]</span>")
return
var/stl = config_legacy.second_topic_limit //CONFIG_GET(number/second_topic_limit)
if (!holder && stl)
var/second = round(world.time, 10)
if (!topiclimiter)
topiclimiter = new(LIMITER_SIZE)
if (second != topiclimiter[CURRENT_SECOND])
topiclimiter[CURRENT_SECOND] = second
topiclimiter[SECOND_COUNT] = 0
topiclimiter[SECOND_COUNT] += 1
if (topiclimiter[SECOND_COUNT] > stl)
to_chat(src, "<span class='danger'>Your previous action was ignored because you've done too many in a second</span>")
return
// Tgui Topic middleware
if(tgui_topic(href_list))
if(CONFIG_GET(flag/emergency_tgui_logging))
log_href("[src] (usr:[usr]\[[COORD(usr)]\]) : [hsrc ? "[hsrc] " : ""][href]")
return
//? Normal HREF handling go below
// Log
log_href("[src] (usr:[usr]\[[COORD(usr)]\]) : [hsrc ? "[hsrc] " : ""][href]")
// Run normal hooks.
if(on_topic_hook(href, href_list, hsrc))
return
// Route statpanel
if(href_list["statpanel"])
_statpanel_act(href_list["statpanel"], href_list)
return
//Admin PM
if(href_list["priv_msg"])
var/client/C = locate(href_list["priv_msg"])
if(ismob(C)) //Old stuff can feed-in mobs instead of clients
var/mob/M = C
C = M.client
cmd_admin_pm(C,null)
return
// Depricated. go use TGS
if(href_list["irc_msg"])
if(!holder && received_irc_pm < world.time - 6000) //Worse they can do is spam IRC for 10 minutes
to_chat(usr, "<span class='warning'>You are no longer able to use this, it's been more then 10 minutes since an admin on IRC has responded to you</span>")
return
if(mute_irc)
to_chat(usr, "<span class='warning'You cannot use this as your client has been muted from sending messages to the admins on IRC</span>")
return
send2irc("AHELP", href_list["irc_msg"])
return
switch(href_list["_src_"])
if("holder")
hsrc = holder
if("usr")
hsrc = mob
if("prefs")
return prefs.process_link(usr,href_list)
if("vars")
return view_var_Topic(href,href_list,hsrc)
if("stat")
return _statpanel_act(href_list["act"], href_list)
switch(href_list["action"])
if("openLink")
src << link(href_list["link"])
if (hsrc)
var/datum/real_src = hsrc
if(QDELETED(real_src))
return
if(href_list["month"] && !(player.player_flags & PLAYER_FLAG_AGE_VERIFIED))
handle_age_gate(href_list["month"], href_list["year"])
..() //redirect to hsrc.Topic()
///////////
//CONNECT//
///////////
/**
* Linter check, do not call.
*/
/proc/lint__check_client_new_doesnt_sleep()
SHOULD_NOT_SLEEP(TRUE)
var/client/C
C.New()
/client/New(TopicData)
//* pre-connect-ish *//
// Block client.Topic() calls from connect.
TopicData = null
// Kick invalid connections.
if(connection != "seeker" && connection != "web")
return null
//! legacy: kick out guests !//
if(!config_legacy.guests_allowed && is_guest() && !is_localhost())
security_kick(
message = "This server doesn't allow guest accounts to play. Please go to http://www.byond.com/ and register for a key.",
tell_user = TRUE,
immediate = TRUE,
)
return null
// Queue pre-connect greeting
spawn(0.5 SECONDS)
to_chat(src, "<font color='red'>If the title screen is black, resources are still downloading. Please be patient until the title screen appears.</font>")
// Register in globals.
GLOB.clients += src
GLOB.directory[ckey] = src
//* record their existence (tm)
// log & lookup updates
var/full_version = "[byond_version].[byond_build ? byond_build : "xxx"]"
// log connection in text file
log_access("Login: [key_name(src)] from [address ? address : "localhost"]-[computer_id] || BYOND v[full_version]")
// log to db
log_connection_to_db()
// log to player lookup
update_lookup_in_db()
//* Resolve storage datums *//
persistent = resolve_client_data(ckey, key)
player = resolve_player_data(ckey, key)
player.log_connect()
preferences = SSpreferences.resolve_game_preferences(key, ckey)
//? WARNING: SHITCODE ALERT ?//
// we wait until it inits to do this
// todo: is there a better way this is kind of awful
preferences.active = src
preferences.on_reconnect()
//? END ?//
//* Create interface UI *//
if(byond_version >= 516)
winset(src, null, list("browser-options" = "find,refresh,byondstorage"))
// todo: move top level menu here, for now it has to be under prefs.
tgui_stat = new(src, SKIN_BROWSER_ID_STAT)
tgui_panel = new(src, SKIN_BROWSER_ID_CHAT)
// Instantiate cutscene system
spawn(1)
init_cutscene_system()
//* Setup on-map HUDs *//
action_drawer = new(src)
actor_huds = new(src)
action_holder = new /datum/action_holder/client_actor(src)
action_drawer.register_holder(action_holder)
//* Setup admin tooling *//
// Notify tickets they logged in
GLOB.ahelp_tickets.ClientLogin(src)
// Give them admin if they're an admin
// TODO: Maybe don't do it if they're deadminned..
var/datum/admins/maybe_holder = admin_datums[ckey]
maybe_holder?.associate(src)
//! TODO: This is shitcode, fix it.
if(is_localhost() && CONFIG_GET(flag/enable_localhost_rank) && !holder)
holder = new /datum/admins("!localhost!", ALL, ckey)
holder.associate(src)
//! END
// todo: refactor and hoist
//preferences datum - also holds some persistent data for the client (because we may as well keep these datums to a minimum)
prefs = GLOB.preferences_datums[ckey]
if(!prefs)
prefs = new /datum/preferences(src)
GLOB.preferences_datums[ckey] = prefs
prefs.client = src
// todo: refactor
prefs.last_ip = address //these are gonna be used for banning
prefs.last_id = computer_id //these are gonna be used for banning
//fps = prefs.clientfps //(prefs.clientfps < 0) ? RECOMMENDED_FPS : prefs.clientfps
// todo: hoist
// build top level menu
GLOB.main_window_menu.setup(src)
//* WARNING: mob.login is always called async, aka immediately returns on sleep.
//* we cannot enforce nosleep due to SDMM limitations.
//* therefore, DO NOT PUT ANYTHING YOU WILL RELY ON LATER IN THIS PROC IN LOGIN!
. = ..() //calls mob.Login()
//* Connection Security *//
// start caching it immediately
INVOKE_ASYNC(SSipintel, TYPE_PROC_REF(/datum/controller/subsystem/ipintel, vpn_connection_check), address, ckey)
// run onboarding gauntlet
INVOKE_ASYNC(src, PROC_REF(onboarding))
//* Initialize Input *//
if(SSinput.initialized)
set_macros()
update_movement_keys()
//* Initialize UI *//
// initialize statbrowser
tgui_stat.initialize()
// Initialize tgui panel
tgui_panel.initialize()
// initialize cutscene browser
// - (we don't, the JS does it for us.) -
//This is down here because of the browse() calls in tooltip/New()
if(!tooltips)
tooltips = new /datum/tooltip(src)
connection_time = world.time
connection_realtime = world.realtime
connection_timeofday = world.timeofday
//* Misc *//
// force hardware graphics on
spawn(5)
winset(src, null, "command=\".configure graphics-hwmode on\"")
if(holder)
admin_memo_show()
// to_chat(src, get_message_output("memo"))
// adminGreet()
if(custom_event_msg && custom_event_msg != "")
to_chat(src, "<h1 class='alert'>Custom Event</h1>")
to_chat(src, "<h2 class='alert'>A custom event is taking place. OOC Info:</h2>")
to_chat(src, "<span class='alert'>[custom_event_msg]</span>")
to_chat(src, "<br>")
// Preload resources.
// todo: re-evaluate this
spawn(0)
send_resources()
//? Startup rendering
pre_init_viewport()
mob.reload_rendering()
// todo: this is dead because changelog is dead but we should fix it tbvqh
// if(prefs.lastchangelog != GLOB.changelog_hash) //bolds the changelog button on the interface so we know there are updates.
// to_chat(src, "<span class='info'>You have unread updates in the changelog.</span>")
// winset(src, "infowindow.changelog", "background-color=#eaeaea;font-style=bold")
// if(config_legacy.aggressive_changelog)
// changelog_async()
// run post-init 'lint'-like checks
// this is on a spawn() to force a separate call chain
spawn(0)
invoke_hooks__client_stability_check(src)
// todo: fuck you voreprefs
spawn(0)
prefs_vr = new /datum/vore_preferences(src)
if(config_legacy.paranoia_logging)
if(isnum(player.player_age) && player.player_age == -1)
log_and_message_admins("PARANOIA: [key_name(src)] has connected here for the first time.")
if(isnum(persistent.account_age) && persistent.account_age <= 2)
log_and_message_admins("PARANOIA: [key_name(src)] has a very new BYOND account ([persistent.account_age] days).")
//* Finalize *//
// set initialized if we're not queued for a security kick
if(!queued_security_kick || panic_bunker_pending)
initialized = TRUE
else
addtimer(CALLBACK(src, PROC_REF(deferred_initialization_block)), 0)
// show any migration errors
// todo: this shouldn't be here
prefs.auto_flush_errors()
// update our hub label
// todo: this should be a global signal that the subsystem hooks
SSserver_maint.queue_hub_update()
//////////////
//DISCONNECT//
//////////////
/**
* Linter check, do not call.
*/
/proc/lint__check_client_del_doesnt_sleep()
SHOULD_NOT_SLEEP(TRUE)
var/client/C
C.Del()
/client/Del()
if(!gc_destroyed)
Destroy() //Clean up signals and timers.
return ..()
/client/Destroy()
// get rid of context menus
QDEL_NULL(context_menu)
// Unregister globals
GLOB.clients -= src
GLOB.directory -= ckey
// log
log_access("Logout: [key_name(src)]")
// unreference storage datums
persistent = null
player = null
if(preferences)
preferences.active = null
preferences = null
//* unsorted
GLOB.ahelp_tickets.ClientLogout(src)
if(prefs)
prefs.client = null
prefs = null
SSserver_maint.UpdateHubStatus()
holder?.disassociate()
//* Cleanup rendering *//
if(using_perspective)
set_perspective(null)
clear_atom_hud_providers()
//* Cleanup interface UI *//
QDEL_NULL(tgui_stat)
cleanup_cutscene_system()
QDEL_NULL(tgui_panel)
QDEL_NULL(tooltips)
//* Cleanup on-map HUDs *//
QDEL_NULL(actor_huds)
QDEL_NULL(action_holder)
QDEL_NULL(action_drawer)
//* logout *//
mob?.pre_logout(src)
//* cleanup from SSinput *//
SSinput.currentrun?.Remove(src)
//* cleanup from SSping *//
SSping.currentrun?.Remove(src)
. = ..() //Even though we're going to be hard deleted there are still some things that want to know the destroy is happening
return QDEL_HINT_HARDDEL_NOW
// here because it's similar to below
/client/proc/add_system_note(system_ckey, message)
notes_add(ckey, message)
/*
var/sql_system_ckey = sanitizeSQL(system_ckey)
var/sql_ckey = sanitizeSQL(ckey)
//check to see if we noted them in the last day.
var/datum/DBQuery/query_get_notes = SSdbcore.NewQuery("SELECT id FROM [DB_PREFIX_TABLE_NAME("messages")] WHERE type = 'note' AND targetckey = '[sql_ckey]' AND adminckey = '[sql_system_ckey]' AND timestamp + INTERVAL 1 DAY < NOW() AND deleted = 0 AND expire_timestamp > NOW()")
if(!query_get_notes.Execute())
qdel(query_get_notes)
return
if(query_get_notes.NextRow())
qdel(query_get_notes)
return
qdel(query_get_notes)
//regardless of above, make sure their last note is not from us, as no point in repeating the same note over and over.
query_get_notes = SSdbcore.NewQuery("SELECT adminckey FROM [DB_PREFIX_TABLE_NAME("messages")] WHERE targetckey = '[sql_ckey]' AND deleted = 0 AND expire_timestamp > NOW() ORDER BY timestamp DESC LIMIT 1")
if(!query_get_notes.Execute())
qdel(query_get_notes)
return
if(query_get_notes.NextRow())
if (query_get_notes.item[1] == system_ckey)
qdel(query_get_notes)
return
qdel(query_get_notes)
create_message("note", key, system_ckey, message, null, null, 0, 0, null, 0, 0)
*/
//checks if a client is afk
//3000 frames = 5 minutes
/client/proc/is_afk(duration=3000)
if(inactivity > duration)
return inactivity
return 0
// Byond seemingly calls stat, each tick.
// Calling things each tick can get expensive real quick.
// So we slow this down a little.
// See: http://www.byond.com/docs/ref/info.html#/client/proc/Stat
/client/Stat()
. = ..()
if (holder)
sleep(1)
else
stoplag(5)
/client/Click(atom/object, atom/location, control, params)
var/ab = FALSE
var/list/L = params2list(params)
if (object && object == middragatom && L["left"])
ab = max(0, 5 SECONDS-(world.time-middragtime)*0.1)
var/mcl = config_legacy.minute_click_limit //CONFIG_GET(number/minute_click_limit)
if (!holder && mcl)
var/minute = round(world.time, 600)
if (!clicklimiter)
clicklimiter = new(LIMITER_SIZE)
if (minute != clicklimiter[CURRENT_MINUTE])
clicklimiter[CURRENT_MINUTE] = minute
clicklimiter[MINUTE_COUNT] = 0
clicklimiter[MINUTE_COUNT] += 1+(ab)
if (clicklimiter[MINUTE_COUNT] > mcl)
var/msg = "Your previous click was ignored because you've done too many in a minute."
if (minute != clicklimiter[ADMINSWARNED_AT]) //only one admin message per-minute. (if they spam the admins can just boot/ban them)
clicklimiter[ADMINSWARNED_AT] = minute
msg += " Administrators have been informed."
if (ab)
log_game("[key_name(src)] is using the middle click aimbot exploit")
message_admins("[ADMIN_LOOKUPFLW(src)] [ADMIN_KICK(usr)] is using the middle click aimbot exploit")
add_system_note("aimbot", "Is using the middle click aimbot exploit")
log_click("DROPPED: [ckey] middle click aimbot on [middragatom]:[object]")
log_game("[key_name(src)] Has hit the per-minute click limit of [mcl] clicks in a given game minute")
message_admins("[ADMIN_LOOKUPFLW(src)] [ADMIN_KICK(usr)] Has hit the per-minute click limit of [mcl] clicks in a given game minute")
to_chat(src, "<span class='danger'>[msg]</span>")
return
var/scl = config_legacy.second_click_limit //CONFIG_GET(number/second_click_limit)
if (!holder && scl)
var/second = round(world.time, 10)
if (!clicklimiter)
clicklimiter = new(LIMITER_SIZE)
if (second != clicklimiter[CURRENT_SECOND])
clicklimiter[CURRENT_SECOND] = second
clicklimiter[SECOND_COUNT] = 0
clicklimiter[SECOND_COUNT] += 1+(!!ab)
if (clicklimiter[SECOND_COUNT] > scl)
to_chat(src, "<span class='danger'>Your previous click was ignored because you've done too many in a second</span>")
return
if(ab) //Citadel edit, things with stuff.
return
if (preferences.is_hotkeys_mode())
// If hotkey mode is enabled, then clicking the map will automatically
// unfocus the text bar. This removes the red color from the text bar
// so that the visual focus indicator matches reality.
winset(src, null, "input.background-color=[COLOR_INPUT_DISABLED]")
if(GLOB.log_clicks)
log_click("CLICK: [ckey] [object]~[location]~[control]~[params]")
return ..()
GLOBAL_VAR_INIT(log_clicks, FALSE)
/client/proc/last_activity_seconds()
return inactivity / 10
//send resources to the client. It's here in its own proc so we can move it around easiliy if need be
/client/proc/send_resources()
// force them to download rsc's from configured paths if needed
#if (PRELOAD_RSC == 0)
var/static/next_external_rsc = 0
var/list/external_rsc_urls = CONFIG_GET(keyed_list/external_rsc_urls)
if(length(external_rsc_urls))
next_external_rsc = WRAP(next_external_rsc+1, 1, external_rsc_urls.len+1)
preload_rsc = external_rsc_urls[next_external_rsc]
#endif
INVOKE_ASYNC(SSassets, TYPE_PROC_REF(/datum/controller/subsystem/assets, preload_client_assets), src)
//Hook, override it to run code when dir changes
//Like for /atoms, but clients are their own snowflake FUCK
/client/proc/setDir(newdir)
dir = newdir
/mob/proc/MayRespawn()
return 0
/client/proc/MayRespawn()
if(mob)
return mob.MayRespawn()
// Something went wrong, client is usually kicked or transfered to a new mob at this point
return 0
/client/proc/AnnouncePR(announcement)
to_chat(src, announcement)
// TODO: this shoudln't be on client.
/client/proc/getAlertDesc()
var/color
var/desc
//borrow the same colors from the fire alarms
switch(get_security_level())
if("green")
color = "#00ff00"
desc = "" //no special description if nothing special is going on
if("yellow")
color = "#ffff00"
desc = CONFIG_GET(string/alert_desc_yellow_upto)
if("violet")
color = "#9933ff"
desc = CONFIG_GET(string/alert_desc_violet_upto)
if("orange")
color = "#ff9900"
desc = CONFIG_GET(string/alert_desc_orange_upto)
if("blue")
color = "#1024A9"
desc = CONFIG_GET(string/alert_desc_blue_upto)
if("red")
color = "#ff0000"
desc = CONFIG_GET(string/alert_desc_red_upto)
if("delta")
color = "#FF6633"
desc = CONFIG_GET(string/alert_desc_delta)
. = SPAN_NOTICE("<br>The alert level on \the [station_name()] is currently: <font color=[color]>Code [capitalize(get_security_level())]</font>. [desc]")