////////////
//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