//////////// //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, "[msg]") 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, "Your previous action was ignored because you've done too many in a second") 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, "You are no longer able to use this, it's been more then 10 minutes since an admin on IRC has responded to you") return if(mute_irc) to_chat(usr, "") 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, "If the title screen is black, resources are still downloading. Please be patient until the title screen appears.") // 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, "

Custom Event

") to_chat(src, "

A custom event is taking place. OOC Info:

") to_chat(src, "[custom_event_msg]") to_chat(src, "
") // 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, "You have unread updates in the changelog.") // 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, "[msg]") 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, "Your previous click was ignored because you've done too many in a second") 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("
The alert level on \the [station_name()] is currently: Code [capitalize(get_security_level())]. [desc]")