//////////// //SECURITY// //////////// #define UPLOAD_LIMIT 10485760 //Restricts client uploads to the server to 10MB //Boosted this thing. What's the worst that can happen? #define MIN_CLIENT_VERSION 0 //Just an ambiguously low version for now, I don't want to suddenly stop people playing. //I would just like the code ready should it ever need to be used. //#define TOPIC_DEBUGGING 1 /* 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) if(!usr || usr != mob) //stops us calling Topic for somebody else's client. Also helps prevent usr=null return #if defined(TOPIC_DEBUGGING) to_world("[src]'s Topic: [href] destined for [hsrc].") if(href_list["nano_err"]) //nano throwing errors to_world("## NanoUI, Subject [src]: " + html_decode(href_list["nano_err")]) //NANO DEBUG HOOK #endif if(href_list["asset_cache_confirm_arrival"]) var/job = text2num(href_list["asset_cache_confirm_arrival"]) completed_asset_jobs += job return //search the href for script injection if( findtext(href,"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 send2adminirc(href_list["irc_msg"]) return //Logs all hrefs if(config && config.log_hrefs && href_logfile) WRITE_LOG(href_logfile, "[src] (usr:[usr]) || [hsrc ? "[hsrc] " : ""][href]") 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("chat") return chatOutput.Topic(href, href_list) switch(href_list["action"]) if("openLink") src << link(href_list["link"]) ..() //redirect to hsrc.Topic() //This stops files larger than UPLOAD_LIMIT being sent from client to server via input(), client.Import() etc. /client/AllowUpload(filename, filelength) if(filelength > UPLOAD_LIMIT) to_chat(src, "Error: AllowUpload(): File Upload too large. Upload Limit: [UPLOAD_LIMIT/1024]KiB.") return 0 /* //Don't need this at the moment. But it's here if it's needed later. //Helps prevent multiple files being uploaded at once. Or right after eachother. var/time_to_wait = fileaccess_timer - world.time if(time_to_wait > 0) to_chat(src, "Error: AllowUpload(): Spam prevention. Please wait [round(time_to_wait/10)] seconds.") return 0 fileaccess_timer = world.time + FTPDELAY */ return 1 /////////// //CONNECT// /////////// /client/New(TopicData) TopicData = null //Prevent calls to client.Topic from connect if(!(connection in list("seeker", "web"))) //Invalid connection type. return null if(byond_version < MIN_CLIENT_VERSION) //Out of date client. return null if(!config.guests_allowed && IsGuestKey(key)) alert(src,"This server doesn't allow guest accounts to play. Please go to http://www.byond.com/ and register for a key.","Guest","OK") del(src) return chatOutput = new /datum/chatOutput(src) //veechat chatOutput.send_resources() spawn() chatOutput.start() //Only show this if they are put into a new_player mob. Otherwise, "what title screen?" if(isnewplayer(src.mob)) to_chat(src, "If the title screen is black, resources are still downloading. Please be patient until the title screen appears.") GLOB.clients += src GLOB.directory[ckey] = src GLOB.ahelp_tickets.ClientLogin(src) //Admin Authorisation holder = admin_datums[ckey] if(holder) admins += src holder.owner = src //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) prefs = new /datum/preferences(src) preferences_datums[ckey] = prefs prefs.last_ip = address //these are gonna be used for banning prefs.last_id = computer_id //these are gonna be used for banning . = ..() //calls mob.Login() prefs.sanitize_preferences() 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, "
") if(!winexists(src, "asset_cache_browser")) // The client is using a custom skin, tell them. to_chat(src, "Unable to access asset cache browser, if you are using a custom skin file, please allow DS to download the updated version, if you are not, then make a bug report. This is not a critical issue but can cause issues with resource downloading, as it is impossible to know when extra resources arrived to you.") if(holder) add_admin_verbs() admin_memo_show() // Forcibly enable hardware-accelerated graphics, as we need them for the lighting overlays. // (but turn them off first, since sometimes BYOND doesn't turn them on properly otherwise) spawn(5) // And wait a half-second, since it sounds like you can do this too fast. if(src) winset(src, null, "command=\".configure graphics-hwmode off\"") sleep(2) // wait a bit more, possibly fixes hardware mode not re-activating right winset(src, null, "command=\".configure graphics-hwmode on\"") log_client_to_db() send_resources() if(!void) void = new() void.MakeGreed() screen += void if((prefs.lastchangelog != changelog_hash) && isnewplayer(src.mob)) //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, "rpane.changelog", "background-color=#eaeaea;font-style=bold") if(config.aggressive_changelog) src.changes() hook_vr("client_new",list(src)) //VOREStation Code if(config.paranoia_logging) var/alert = FALSE //VOREStation Edit start. if(isnum(player_age) && player_age == 0) log_and_message_admins("PARANOIA: [key_name(src)] has connected here for the first time.") alert = TRUE if(isnum(account_age) && account_age <= 2) log_and_message_admins("PARANOIA: [key_name(src)] has a very new BYOND account ([account_age] days).") alert = TRUE if(alert) for(var/client/X in admins) if(X.is_preference_enabled(/datum/client_preference/holder/play_adminhelp_ping)) X << 'sound/voice/bcriminal.ogg' window_flash(X) //VOREStation Edit end. ////////////// //DISCONNECT// ////////////// /client/Del() if(holder) holder.owner = null admins -= src GLOB.ahelp_tickets.ClientLogout(src) GLOB.directory -= ckey GLOB.clients -= src return ..() /client/Destroy() ..() return QDEL_HINT_HARDDEL_NOW // here because it's similar to below // Returns null if no DB connection can be established, or -1 if the requested key was not found in the database /proc/get_player_age(key) establish_db_connection() if(!dbcon.IsConnected()) return null var/sql_ckey = sql_sanitize_text(ckey(key)) var/DBQuery/query = dbcon.NewQuery("SELECT datediff(Now(),firstseen) as age FROM erro_player WHERE ckey = '[sql_ckey]'") query.Execute() if(query.NextRow()) return text2num(query.item[1]) else return -1 /client/proc/log_client_to_db() if ( IsGuestKey(src.key) ) return establish_db_connection() if(!dbcon.IsConnected()) return var/sql_ckey = sql_sanitize_text(src.ckey) var/DBQuery/query = dbcon.NewQuery("SELECT id, datediff(Now(),firstseen) as age FROM erro_player WHERE ckey = '[sql_ckey]'") query.Execute() var/sql_id = 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]) break account_join_date = sanitizeSQL(findJoinDate()) if(account_join_date && dbcon.IsConnected()) var/DBQuery/query_datediff = dbcon.NewQuery("SELECT DATEDIFF(Now(),'[account_join_date]')") if(query_datediff.Execute() && query_datediff.NextRow()) account_age = text2num(query_datediff.item[1]) var/DBQuery/query_ip = dbcon.NewQuery("SELECT ckey FROM erro_player WHERE ip = '[address]'") query_ip.Execute() related_accounts_ip = "" while(query_ip.NextRow()) related_accounts_ip += "[query_ip.item[1]], " break var/DBQuery/query_cid = dbcon.NewQuery("SELECT ckey FROM erro_player WHERE computerid = '[computer_id]'") query_cid.Execute() related_accounts_cid = "" while(query_cid.NextRow()) 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 var/sql_ip = sql_sanitize_text(src.address) var/sql_computerid = sql_sanitize_text(src.computer_id) var/sql_admin_rank = sql_sanitize_text(admin_rank) //Panic bunker code if (isnum(player_age) && player_age == 0) //first connection if (config.panic_bunker && !holder && !deadmin_holder) log_adminwarn("Failed Login: [key] - New account attempting to connect during panic bunker") message_admins("Failed Login: [key] - New account attempting to connect during panic bunker") to_chat(src, "Sorry but the server is currently not accepting connections from never before seen players.") qdel(src) return 0 // IP Reputation Check if(config.ip_reputation) if(config.ipr_allow_existing && player_age >= config.ipr_minimum_age) log_admin("Skipping IP reputation check on [key] with [address] because of player age") else if(update_ip_reputation()) //It is set now if(ip_reputation >= config.ipr_bad_score) //It's bad //Log it if(config.paranoia_logging) //We don't block, but we want paranoia log messages log_and_message_admins("[key] at [address] has bad IP reputation: [ip_reputation]. Will be kicked if enabled in config.") else //We just log it log_admin("[key] at [address] has bad IP reputation: [ip_reputation]. Will be kicked if enabled in config.") //Take action if required if(config.ipr_block_bad_ips && config.ipr_allow_existing) //We allow players of an age, but you don't meet it to_chat(src, "Sorry, we only allow VPN/Proxy/Tor usage for players who have spent at least [config.ipr_minimum_age] days on the server. If you are unable to use the internet without your VPN/Proxy/Tor, please contact an admin out-of-game to let them know so we can accommodate this.") qdel(src) return 0 else if(config.ipr_block_bad_ips) //We don't allow players of any particular age to_chat(src, "Sorry, we do not accept connections from users via VPN/Proxy/Tor connections.") qdel(src) return 0 else log_admin("Couldn't perform IP check on [key] with [address]") // VOREStation Edit Start - Department Hours if(config.time_off) var/DBQuery/query_hours = dbcon.NewQuery("SELECT department, hours FROM vr_player_hours WHERE ckey = '[sql_ckey]'") query_hours.Execute() while(query_hours.NextRow()) LAZYINITLIST(department_hours) department_hours[query_hours.item[1]] = text2num(query_hours.item[2]) // VOREStation Edit End - Department Hours if(sql_id) //Player already identified previously, we need to just update the 'lastseen', 'ip' and 'computer_id' variables var/DBQuery/query_update = dbcon.NewQuery("UPDATE erro_player SET lastseen = Now(), ip = '[sql_ip]', computerid = '[sql_computerid]', lastadminrank = '[sql_admin_rank]' WHERE id = [sql_id]") query_update.Execute() else //New player!! Need to insert all the stuff var/DBQuery/query_insert = dbcon.NewQuery("INSERT INTO erro_player (id, ckey, firstseen, lastseen, ip, computerid, lastadminrank) VALUES (null, '[sql_ckey]', Now(), Now(), '[sql_ip]', '[sql_computerid]', '[sql_admin_rank]')") query_insert.Execute() //Logging player access var/serverip = "[world.internet_address]:[world.port]" var/DBQuery/query_accesslog = dbcon.NewQuery("INSERT INTO `erro_connection_log`(`id`,`datetime`,`serverip`,`ckey`,`ip`,`computerid`) VALUES(null,Now(),'[serverip]','[sql_ckey]','[sql_ip]','[sql_computerid]');") query_accesslog.Execute() #undef TOPIC_SPAM_DELAY #undef UPLOAD_LIMIT #undef MIN_CLIENT_VERSION //checks if a client is afk //3000 frames = 5 minutes /client/proc/is_afk(duration=3000) if(inactivity > duration) return inactivity return 0 //Called when the client performs a drag-and-drop operation. /client/MouseDrop(start_object,end_object,start_location,end_location,start_control,end_control,params) if(buildmode && start_control == "mapwindow.map" && start_control == end_control) build_drag(src,buildmode,start_object,end_object,start_location,end_location,start_control,end_control,params) else . = ..() // 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/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() spawn (10) //removing this spawn causes all clients to not get verbs. //Precache the client with all other assets slowly, so as to not block other browse() calls getFilesSlow(src, SSassets.preload, register_asset = FALSE) 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/verb/character_setup() set name = "Character Setup" set category = "Preferences" if(prefs) prefs.ShowChoices(usr) /client/proc/findJoinDate() var/list/http = world.Export("http://byond.com/members/[ckey]?format=text") if(!http) log_world("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]") /client/vv_edit_var(var_name, var_value) if(var_name == NAMEOF(src, holder)) return FALSE return ..() /client/verb/reload_vchat() set name = "Reload VChat" set category = "OOC" //Timing if(src.chatOutputLoadedAt > (world.time - 10 SECONDS)) alert(src, "You can only try to reload VChat every 10 seconds at most.") return //Log, disable log_debug("[key_name(src)] reloaded VChat.") winset(src, null, "outputwindow.htmloutput.is-visible=false;outputwindow.oldoutput.is-visible=false;outputwindow.chatloadlabel.is-visible=true") //The hard way qdel_null(src.chatOutput) chatOutput = new /datum/chatOutput(src) //veechat chatOutput.send_resources() spawn() chatOutput.start() //This is for getipintel.net. //You're welcome to replace this proc with your own that does your own cool stuff. //Just set the client's ip_reputation var and make sure it makes sense with your config settings (higher numbers are worse results) /client/proc/update_ip_reputation() var/request = "http://check.getipintel.net/check.php?ip=[address]&contact=[config.ipr_email]" var/http[] = world.Export(request) /* Debug to_world_log("Requested this: [request]") for(var/entry in http) to_world_log("[entry] : [http[entry]]") */ if(!http || !islist(http)) //If we couldn't check, the service might be down, fail-safe. log_admin("Couldn't connect to getipintel.net to check [address] for [key]") return FALSE //429 is rate limit exceeded if(text2num(http["STATUS"]) == 429) log_and_message_admins("getipintel.net reports HTTP status 429. IP reputation checking is now disabled. If you see this, let a developer know.") config.ip_reputation = FALSE return FALSE var/content = file2text(http["CONTENT"]) //world.Export actually returns a file object in CONTENT var/score = text2num(content) if(isnull(score)) return FALSE //Error handling if(score < 0) var/fatal = TRUE var/ipr_error = "getipintel.net IP reputation check error while checking [address] for [key]: " switch(score) if(-1) ipr_error += "No input provided" if(-2) fatal = FALSE ipr_error += "Invalid IP provided" if(-3) fatal = FALSE ipr_error += "Unroutable/private IP (spoofing?)" if(-4) fatal = FALSE ipr_error += "Unable to reach database" if(-5) ipr_error += "Our IP is banned or otherwise forbidden" if(-6) ipr_error += "Missing contact info" log_and_message_admins(ipr_error) if(fatal) config.ip_reputation = FALSE log_and_message_admins("With this error, IP reputation checking is disabled for this shift. Let a developer know.") return FALSE //Went fine else ip_reputation = score return TRUE