//////////// //SECURITY// //////////// #define UPLOAD_LIMIT 1048576 //Restricts client uploads to the server to 1MB //Could probably do with being lower. #define LIMITER_SIZE 5 #define CURRENT_SECOND 1 #define SECOND_COUNT 2 #define CURRENT_MINUTE 3 #define MINUTE_COUNT 4 #define ADMINSWARNED_AT 5 /* 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 // asset_cache if(href_list["asset_cache_confirm_arrival"]) //src << "ASSET JOB [href_list["asset_cache_confirm_arrival"]] ARRIVED." var/job = text2num(href_list["asset_cache_confirm_arrival"]) //because we skip the limiter, we have to make sure this is a valid arrival and not somebody tricking us // into letting append to a list without limit. if (job && job <= last_asset_job && !(job in completed_asset_jobs)) completed_asset_jobs += job return if (!holder && config.minutetopiclimit) 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] > config.minutetopiclimit) 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 [config.minutetopiclimit] topic calls in a given game minute") message_admins("[key_name_admin(src)] [ADMIN_KICK(usr)] Has hit the per-minute topic limit of [config.minutetopiclimit] topic calls in a given game minute") src << "[msg]" return if (!holder && config.secondtopiclimit) 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] > config.secondtopiclimit) src << "Your previous action was ignored because you've done too many in a second" return //Logs all hrefs if(config && config.log_hrefs && href_logfile) href_logfile << "[time2text(world.timeofday,"hh:mm")] [src] (usr:[usr]) || [hsrc ? "[hsrc] " : ""][href]
" // Admin PM if(href_list["priv_msg"]) if (href_list["ahelp_reply"]) cmd_ahelp_reply(href_list["priv_msg"]) return cmd_admin_pm(href_list["priv_msg"],null) return switch(href_list["_src_"]) if("holder") hsrc = holder if("usr") hsrc = mob if("prefs") if (inprefs) return inprefs = TRUE . = prefs.process_link(usr,href_list) inprefs = FALSE return if("vars") return view_var_Topic(href,href_list,hsrc) ..() //redirect to hsrc.Topic() /client/proc/is_content_unlocked() if(!prefs.unlock_content) src << "Become a BYOND member to access member-perks and features, as well as support the engine that makes this game possible. Only 10 bucks for 3 months! Click Here to find out more." return 0 return 1 /client/proc/handle_spam_prevention(message, mute_type) if(config.automute_on && !holder && src.last_message == message) src.last_message_count++ if(src.last_message_count >= SPAM_TRIGGER_AUTOMUTE) src << "You have exceeded the spam filter limit for identical messages. An auto-mute was applied." cmd_admin_mute(src, mute_type, 1) return 1 if(src.last_message_count >= SPAM_TRIGGER_WARNING) src << "You are nearing the spam filter limit for identical messages." return 0 else last_message = message src.last_message_count = 0 return 0 //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) 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) src << "Error: AllowUpload(): Spam prevention. Please wait [round(time_to_wait/10)] seconds." return 0 fileaccess_timer = world.time + FTPDELAY */ return 1 /////////// //CONNECT// /////////// #if (PRELOAD_RSC == 0) var/list/external_rsc_urls var/next_external_rsc = 0 #endif /client/New(TopicData) var/tdata = TopicData //save this for later use TopicData = null //Prevent calls to client.Topic from connect if(connection != "seeker" && connection != "web")//Invalid connection type. return null #if (PRELOAD_RSC == 0) if(external_rsc_urls && external_rsc_urls.len) next_external_rsc = Wrap(next_external_rsc+1, 1, external_rsc_urls.len+1) preload_rsc = external_rsc_urls[next_external_rsc] #endif clients += src directory[ckey] = src //Admin Authorisation var/localhost_addresses = list("127.0.0.1", "::1") if(address && (address in localhost_addresses)) var/datum/admin_rank/localhost_rank = new("!localhost!", 65535) if(localhost_rank) var/datum/admins/localhost_holder = new(localhost_rank, ckey) localhost_holder.associate(src) if(protected_config.autoadmin) if(!admin_datums[ckey]) var/datum/admin_rank/autorank for(var/datum/admin_rank/R in admin_ranks) if(R.name == protected_config.autoadmin_rank) autorank = R break if(!autorank) world << "Autoadmin rank not found" else var/datum/admins/D = new(autorank, ckey) admin_datums[ckey] = D holder = admin_datums[ckey] if(holder) admins |= src holder.owner = src //preferences datum - also holds some persistent 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 if(world.byond_version >= 511 && byond_version >= 511 && prefs.clientfps) vars["fps"] = prefs.clientfps sethotkeys(1) //set hoykeys from preferences (from_pref = 1) . = ..() //calls mob.Login() connection_time = world.time connection_realtime = world.realtime connection_timeofday = world.timeofday if (byond_version < config.client_error_version) //Out of date client. src << "Your version of byond is too old:" src << config.client_error_message src << "Your version: [byond_version]" src << "Required version: [config.client_error_version] or later" src << "Visit http://www.byond.com/download/ to get the latest version of byond." if (holder) src << "Because you are an admin, you are being allowed to walk past this limitation, But it is still STRONGLY suggested you upgrade" else qdel(src) return 0 else if (byond_version < config.client_warn_version) //We have words for this client. src << "Your version of byond may be getting out of date:" src << config.client_warn_message src << "Your version: [byond_version]" src << "Required version to remove this message: [config.client_warn_version] or later" src << "Visit http://www.byond.com/download/ to get the latest version of byond." if (connection == "web" && !holder) if (!config.allowwebclient) src << "Web client is disabled" qdel(src) return 0 if (config.webclientmembersonly && !IsByondMember()) src << "Sorry, but the web client is restricted to byond members only." qdel(src) return 0 if( (world.address == address || !address) && !host ) host = key world.update_status() if(holder) add_admin_verbs() src << get_message_output("memo") adminGreet() if((global.comms_key == "default_pwd" || length(global.comms_key) <= 6) && global.comms_allowed) //It's the default value or less than 6 characters long, but it somehow didn't disable comms. src << "The server's API key is either too short or is the default value! Consider changing it immediately!" add_verbs_from_config() set_client_age_from_db() if (isnum(player_age) && player_age == -1) //first connection if (config.panic_bunker && !holder && !(ckey in deadmins)) log_access("Failed Login: [key] - New account attempting to connect during panic bunker") message_admins("Failed Login: [key] - New account attempting to connect during panic bunker") src << "Sorry but the server is currently not accepting connections from never before seen players." if(config.allow_panic_bunker_bounce && tdata != "redirect") src << "Sending you to [config.panic_server_name]." winset(src, null, "command=.options") src << link("[config.panic_address]?redirect") qdel(src) return 0 if (config.notify_new_player_age >= 0) message_admins("New user: [key_name_admin(src)] is connecting here for the first time.") if (config.irc_first_connection_alert) send2irc_adminless_only("New-user", "[key_name(src)] is connecting for the first time!") player_age = 0 // set it from -1 to 0 so the job selection code doesn't have a panic attack else if (isnum(player_age) && player_age < config.notify_new_player_age) message_admins("New user: [key_name_admin(src)] just connected with an age of [player_age] day[(player_age==1?"":"s")]") if(!IsGuestKey(key) && dbcon.IsConnected()) findJoinDate() sync_client_with_db(tdata) get_message_output("watchlist entry", ckey) check_ip_intel() send_resources() if(!void) void = new() screen += void if(prefs.lastchangelog != changelog_hash) //bolds the changelog button on the interface so we know there are updates. src << "You have unread updates in the changelog." if(config.aggressive_changelog) changelog() else winset(src, "infowindow.changelog", "font-style=bold") if(ckey in clientmessages) for(var/message in clientmessages[ckey]) src << message clientmessages.Remove(ckey) if(config && config.autoconvert_notes) convert_notes_sql(ckey) src << get_message_output("message", ckey) if(!winexists(src, "asset_cache_browser")) // The client is using a custom skin, tell them. 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." //This is down here because of the browse() calls in tooltip/New() if(!tooltips) tooltips = new /datum/tooltip(src) ////////////// //DISCONNECT// ////////////// /client/Del() if(holder) adminGreet(1) holder.owner = null admins -= src directory -= ckey clients -= src if(movingmob != null) movingmob.client_mobs_in_contents -= mob UNSETEMPTY(movingmob.client_mobs_in_contents) return ..() /client/Destroy() return QDEL_HINT_HARDDEL_NOW /client/proc/set_client_age_from_db() if (IsGuestKey(src.key)) return establish_db_connection() if(!dbcon.IsConnected()) return var/sql_ckey = sanitizeSQL(src.ckey) var/DBQuery/query = dbcon.NewQuery("SELECT id, datediff(Now(),firstseen) as age FROM [format_table_name("player")] WHERE ckey = '[sql_ckey]'") if (!query.Execute()) return while (query.NextRow()) player_age = text2num(query.item[2]) return //no match mark it as a first connection for use in client/New() player_age = -1 /client/proc/sync_client_with_db(connectiontopic) if (IsGuestKey(src.key)) return establish_db_connection() if (!dbcon.IsConnected()) return var/sql_ckey = sanitizeSQL(ckey) var/DBQuery/query_ip = dbcon.NewQuery("SELECT ckey FROM [format_table_name("player")] WHERE ip = '[address]' AND ckey != '[sql_ckey]'") query_ip.Execute() related_accounts_ip = "" while(query_ip.NextRow()) related_accounts_ip += "[query_ip.item[1]], " var/DBQuery/query_cid = dbcon.NewQuery("SELECT ckey FROM [format_table_name("player")] WHERE computerid = '[computer_id]' AND ckey != '[sql_ckey]'") query_cid.Execute() related_accounts_cid = "" while (query_cid.NextRow()) related_accounts_cid += "[query_cid.item[1]], " var/admin_rank = "Player" if (src.holder && src.holder.rank) admin_rank = src.holder.rank.name else if (check_randomizer(connectiontopic)) return var/sql_ip = sanitizeSQL(src.address) var/sql_computerid = sanitizeSQL(src.computer_id) var/sql_admin_rank = sanitizeSQL(admin_rank) var/DBQuery/query_insert = dbcon.NewQuery("INSERT INTO [format_table_name("player")] (id, ckey, firstseen, lastseen, ip, computerid, lastadminrank) VALUES (null, '[sql_ckey]', Now(), Now(), '[sql_ip]', '[sql_computerid]', '[sql_admin_rank]') ON DUPLICATE KEY UPDATE lastseen = VALUES(lastseen), ip = VALUES(ip), computerid = VALUES(computerid), lastadminrank = VALUES(lastadminrank)") query_insert.Execute() //Logging player access var/serverip = "[world.internet_address]:[world.port]" var/DBQuery/query_accesslog = dbcon.NewQuery("INSERT INTO `[format_table_name("connection_log")]` (`id`,`datetime`,`serverip`,`ckey`,`ip`,`computerid`) VALUES(null,Now(),'[serverip]','[sql_ckey]','[sql_ip]','[sql_computerid]');") query_accesslog.Execute() /client/proc/check_randomizer(topic) . = FALSE if (connection != "seeker") return topic = params2list(topic) if (!config.check_randomizer) return var/static/cidcheck = list() var/static/tokens = list() var/static/cidcheck_failedckeys = list() //to avoid spamming the admins if the same guy keeps trying. var/static/cidcheck_spoofckeys = list() var/oldcid = cidcheck[ckey] if (oldcid) if (!topic || !topic["token"] || !tokens[ckey] || topic["token"] != tokens[ckey]) if (!cidcheck_spoofckeys[ckey]) message_admins("[key_name(src)] appears to have attempted to spoof a cid randomizer check.") cidcheck_spoofckeys[ckey] = TRUE cidcheck[ckey] = computer_id tokens[ckey] = cid_check_reconnect() sleep(10) //browse is queued, we don't want them to disconnect before getting the browse() command. qdel(src) return TRUE if (oldcid != computer_id) //IT CHANGED!!! cidcheck -= ckey //so they can try again after removing the cid randomizer. src << "Connection Error:" src << "Invalid ComputerID(spoofed). Please remove the ComputerID spoofer from your byond installation and try again." if (!cidcheck_failedckeys[ckey]) message_admins("[key_name(src)] has been detected as using a cid randomizer. Connection rejected.") send2irc_adminless_only("CidRandomizer", "[key_name(src)] has been detected as using a cid randomizer. Connection rejected.") cidcheck_failedckeys[ckey] = TRUE note_randomizer_user() log_access("Failed Login: [key] [computer_id] [address] - CID randomizer confirmed (oldcid: [oldcid])") qdel(src) return TRUE else if (cidcheck_failedckeys[ckey]) message_admins("[key_name_admin(src)] has been allowed to connect after showing they removed their cid randomizer") send2irc_adminless_only("CidRandomizer", "[key_name(src)] has been allowed to connect after showing they removed their cid randomizer.") cidcheck_failedckeys -= ckey if (cidcheck_spoofckeys[ckey]) message_admins("[key_name_admin(src)] has been allowed to connect after appearing to have attempted to spoof a cid randomizer check because it appears they aren't spoofing one this time") cidcheck_spoofckeys -= ckey cidcheck -= ckey else var/sql_ckey = sanitizeSQL(ckey) var/DBQuery/query_cidcheck = dbcon.NewQuery("SELECT computerid FROM [format_table_name("player")] WHERE ckey = '[sql_ckey]'") query_cidcheck.Execute() var/lastcid if (query_cidcheck.NextRow()) lastcid = query_cidcheck.item[1] if (computer_id != lastcid) cidcheck[ckey] = computer_id tokens[ckey] = cid_check_reconnect() sleep(10) //browse is queued, we don't want them to disconnect before getting the browse() command. qdel(src) return TRUE /client/proc/cid_check_reconnect() var/token = md5("[rand(0,9999)][world.time][rand(0,9999)][ckey][rand(0,9999)][address][rand(0,9999)][computer_id][rand(0,9999)]") . = token log_access("Failed Login: [key] [computer_id] [address] - CID randomizer check") var/url = winget(src, null, "url") //special javascript to make them reconnect under a new window. src << browse("byond://[url]?token=[token]", "border=0;titlebar=0;size=1x1") src << "You will be automatically taken to the game, if not, click here to be taken manually" /client/proc/note_randomizer_user() var/const/adminckey = "CID-Error" var/sql_ckey = sanitizeSQL(ckey) //check to see if we noted them in the last day. var/DBQuery/query_get_notes = dbcon.NewQuery("SELECT id FROM [format_table_name("messages")] WHERE type = 'note' AND targetckey = '[sql_ckey]' AND adminckey = '[adminckey]' AND timestamp + INTERVAL 1 DAY < NOW()") if(!query_get_notes.Execute()) var/err = query_get_notes.ErrorMsg() log_game("SQL ERROR obtaining id from messages table. Error : \[[err]\]\n") return if (query_get_notes.NextRow()) return //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 = dbcon.NewQuery("SELECT adminckey FROM [format_table_name("messages")] WHERE targetckey = '[sql_ckey]' ORDER BY timestamp DESC LIMIT 1") if(!query_get_notes.Execute()) var/err = query_get_notes.ErrorMsg() log_game("SQL ERROR obtaining adminckey from notes table. Error : \[[err]\]\n") return if (query_get_notes.NextRow()) if (query_get_notes.item[1] == adminckey) return create_message("note", sql_ckey, adminckey, "Detected as using a cid randomizer.", null, null, 0, 0) /client/proc/check_ip_intel() set waitfor = 0 //we sleep when getting the intel, no need to hold up the client connection while we sleep if (config.ipintel_email) var/datum/ipintel/res = get_ip_intel(address) if (res.intel >= config.ipintel_rating_bad) message_admins("Proxy Detection: [key_name_admin(src)] IP intel rated [res.intel*100]% likely to be a Proxy/VPN.") ip_intel = res.intel /client/proc/add_verbs_from_config() if(config.see_own_notes) verbs += /client/proc/self_notes #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 // 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 sleep(5) stoplag() //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() //get the common files getFiles( 'html/search.js', 'html/panels.css', 'html/browser/common.css', 'html/browser/scannernew.css', 'html/browser/playeroptions.css', ) 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, SSasset.cache, register_asset = FALSE) //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 /client/vv_edit_var(var_name, var_value) switch (var_name) if ("holder") return FALSE if ("ckey") return FALSE if ("key") return FALSE