[MIRROR] Server logs (#10486)

Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com>
This commit is contained in:
CHOMPStation2StaffMirrorBot
2025-03-21 16:39:07 -07:00
committed by GitHub
parent 37ad599827
commit 11a5a81208
8 changed files with 151 additions and 135 deletions

View File

@@ -77,5 +77,10 @@
#define AHELP_CLOSED 2
#define AHELP_RESOLVED 3
// LOG BROWSE TYPES
#define BROWSE_ROOT_ALL_LOGS 1
#define BROWSE_ROOT_RUNTIME_LOGS 2
#define BROWSE_ROOT_CURRENT_LOGS 3
/// A value for /datum/admins/cached_feedback_link to indicate empty, rather than unobtained
#define NO_FEEDBACK_LINK "no_feedback_link"

View File

@@ -12,6 +12,9 @@
/// Simply removes the < and > characters, and limits the length of the message.
#define STRIP_HTML_SIMPLE(text, limit) (GLOB.angular_brackets.Replace(copytext(text, 1, limit), ""))
/// Removes characters incompatible with file names.
#define SANITIZE_FILENAME(text) (GLOB.filename_forbidden_chars.Replace(text, ""))
#define MAX_MESSAGE_CHUNKS 130
#define MAX_TGUI_INPUT (MAX_MESSAGE_CHUNKS * 1024)

View File

@@ -7,3 +7,8 @@ GLOBAL_DATUM_INIT(is_valid_url, /regex, regex("((?:https://)\[-a-zA-Z0-9@:%._+~#
GLOBAL_DATUM_INIT(angular_brackets, /regex, regex(@"[<>]", "g"))
GLOBAL_DATUM_INIT(is_color, /regex, regex("^#\[0-9a-fA-F]{6}$"))
//All characters forbidden by filenames: ", \, \n, \t, /, ?, %, *, :, |, <, >, ..
GLOBAL_DATUM_INIT(filename_forbidden_chars, /regex, regex(@{""|[\\\n\t/?%*:|<>]|\.\."}, "g"))
GLOBAL_PROTECT(filename_forbidden_chars)
// had to use the OR operator for quotes instead of putting them in the character class because it breaks the syntax highlighting otherwise.

View File

@@ -12,18 +12,28 @@
return text
//Sends resource files to client cache
/client/proc/getFiles()
for(var/file in args)
src << browse_rsc(file)
/**
* For FTP requests. (i.e. downloading runtime logs.)
*
* However it'd be ok to use for accessing attack logs and such too, which are even laggier.
*/
GLOBAL_VAR_INIT(fileaccess_timer, 0)
/client/proc/browse_files(root="data/logs/", max_iterations=10, list/valid_extensions=list(".txt",".log",".htm"))
/client/proc/browse_files(root_type=BROWSE_ROOT_ALL_LOGS, max_iterations=10, list/valid_extensions=list("txt","log","htm", "html", "gz", "json"))
// wow why was this ever a parameter
var/root = "data/logs/"
switch(root_type)
if(BROWSE_ROOT_ALL_LOGS)
root = "data/logs/"
if(BROWSE_ROOT_CURRENT_LOGS)
root = log_path
var/path = root
for(var/i=0, i<max_iterations, i++)
var/list/choices = sortList(flist(path))
for(var/i in 1 to max_iterations)
var/list/choices = flist(path)
if(path != root)
choices.Insert(1,"/")
choices = sortList(choices) + "Download Folder"
var/choice = tgui_input_list(src,"Choose a file to access:","Download",choices)
switch(choice)
@@ -32,32 +42,82 @@
if("/")
path = root
continue
if("Download Folder")
var/list/comp_flist = flist(path)
var/confirmation = tgui_alert(src, "Are you SURE you want to download all the files in this folder? (This will open [length(comp_flist)] prompt[length(comp_flist) == 1 ? "" : "s"])", "Confirmation", list("Yes", "No"))
if(confirmation != "Yes")
continue
for(var/file in comp_flist)
src << ftp(path + file)
return
path += choice
if(copytext(path,-1,0) != "/") //didn't choose a directory, no need to iterate again
if(copytext_char(path, -1) != "/") //didn't choose a directory, no need to iterate again
break
var/extension = copytext(path,-4,0)
if( !fexists(path) || !(extension in valid_extensions) )
var/extensions
for(var/i in valid_extensions)
if(extensions)
extensions += "|"
extensions += "[i]"
var/regex/valid_ext = new("\\.([extensions])$", "i")
if( !fexists(path) || !(valid_ext.Find(path)) )
to_chat(src, span_red("Error: browse_files(): File not found/Invalid file([path])."))
return
return path
#define FTPDELAY 200 //200 tick delay to discourage spam
#define ADMIN_FTPDELAY_MODIFIER 0.5 //Admins get to spam files faster since we ~trust~ them!
/* This proc is a failsafe to prevent spamming of file requests.
It is just a timer that only permits a download every [FTPDELAY] ticks.
This can be changed by modifying FTPDELAY's value above.
PLEASE USE RESPONSIBLY, Some log files can reach sizes of 4MB! */
/client/proc/file_spam_check()
var/time_to_wait = fileaccess_timer - world.time
var/time_to_wait = GLOB.fileaccess_timer - world.time
if(time_to_wait > 0)
to_chat(src, span_red("Error: file_spam_check(): Spam. Please wait [round(time_to_wait/10)] seconds."))
return 1
fileaccess_timer = world.time + FTPDELAY
return 0
to_chat(src, span_red("Error: file_spam_check(): Spam. Please wait [DisplayTimeText(time_to_wait)]."))
return TRUE
var/delay = FTPDELAY
if(holder)
delay *= ADMIN_FTPDELAY_MODIFIER
GLOB.fileaccess_timer = world.time + delay
return FALSE
#undef FTPDELAY
#undef ADMIN_FTPDELAY_MODIFIER
/**
* Takes a directory and returns every file within every sub directory.
* If extensions_filter is provided then only files that end in that extension are given back.
* If extensions_filter is a list, any file that matches at least one entry is given back.
*/
/proc/pathwalk(path, extensions_filter)
var/list/jobs = list(path)
var/list/filenames = list()
while(jobs.len)
var/current_dir = pop(jobs)
var/list/new_filenames = flist(current_dir)
for(var/new_filename in new_filenames)
// if filename ends in / it is a directory, append to currdir
if(findtext(new_filename, "/", -1))
jobs += "[current_dir][new_filename]"
continue
// filename extension filtering
if(extensions_filter)
if(islist(extensions_filter))
for(var/allowed_extension in extensions_filter)
if(endswith(new_filename, allowed_extension))
filenames += "[current_dir][new_filename]"
break
else if(endswith(new_filename, extensions_filter))
filenames += "[current_dir][new_filename]"
else
filenames += "[current_dir][new_filename]"
return filenames
/proc/pathflatten(path)
return replacetext(path, "/", "_")
/// Returns the md5 of a file at a given path.
/proc/md5filepath(path)
@@ -65,6 +125,7 @@
/// Save file as an external file then md5 it.
/// Used because md5ing files stored in the rsc sometimes gives incorrect md5 results.
/// https://www.byond.com/forum/post/2611357
/proc/md5asfile(file)
var/static/notch = 0
// its importaint this code can handle md5filepath sleeping instead of hard blocking, if it's converted to use rust_g.
@@ -73,3 +134,23 @@
fcopy(file, filename)
. = md5filepath(filename)
fdel(filename)
/**
* Sanitizes the name of each node in the path.
*
* Im case you are wondering when to use this proc and when to use SANITIZE_FILENAME,
*
* You use SANITIZE_FILENAME to sanitize the name of a file [e.g. example.txt]
*
* You use sanitize_filepath sanitize the path of a file [e.g. root/node/example.txt]
*
* If you use SANITIZE_FILENAME to sanitize a file path things will break.
*/
/proc/sanitize_filepath(path)
. = ""
var/delimiter = "/" //Very much intentionally hardcoded
var/list/all_nodes = splittext(path, delimiter)
for(var/node in all_nodes)
if(.)
. += delimiter // Add the delimiter before each successive node.
. += SANITIZE_FILENAME(node)

View File

@@ -661,8 +661,7 @@ GLOBAL_LIST_EMPTY(cached_examine_icons)
if (!isicon(icon2collapse))
if (isfile(thing)) //special snowflake
//var/name = SANITIZE_FILENAME("[generate_asset_name(thing)].png")
var/name = "[generate_asset_name(thing)].png"
var/name = SANITIZE_FILENAME("[generate_asset_name(thing)].png")
if (!SSassets.cache[name])
SSassets.transport.register_asset(name, thing)
for (var/thing2 in targets)

View File

@@ -632,6 +632,18 @@ GLOBAL_LIST_EMPTY(text_tag_cache)
var/static/regex/regex = new(@"[^a-zA-Z0-9]","g")
return replacetext(name, regex, "")
/// Returns TRUE if the input_text ends with the ending
/proc/endswith(input_text, ending)
var/input_length = LAZYLEN(ending)
return !!findtext(input_text, ending, -input_length)
/// Returns TRUE if the input_text starts with any of the beginnings
/proc/starts_with_any(input_text, list/beginnings)
for(var/beginning in beginnings)
if(!!findtext(input_text, beginning, 1, LAZYLEN(beginning)+1))
return TRUE
return FALSE
//finds the first occurrence of one of the characters from needles argument inside haystack
//it may appear this can be optimised, but it really can't. findtext() is so much faster than anything you can do in byondcode.
//stupid byond :(

View File

@@ -45,8 +45,6 @@ var/list/admin_verbs_admin = list(
/client/proc/mark_datum_mapview, //VOREStation Add,
/client/proc/cmd_check_new_players, //allows us to see every new player, //VOREStation Add,
/client/proc/toggle_view_range, //changes how far we can see,
/datum/admins/proc/view_txt_log, //shows the server log (diary) for today,
/datum/admins/proc/view_atk_log, //shows the server combat-log, doesn't do anything presently,
/client/proc/cmd_admin_pm_context, //right-click adminPM interface,
/client/proc/cmd_admin_pm_panel, //admin-pm list,
/client/proc/cmd_admin_subtle_message, //send an message to somebody as a 'voice in their head',
@@ -55,7 +53,6 @@ var/list/admin_verbs_admin = list(
/client/proc/cmd_admin_check_player_logs, //checks a player's attack logs,
/client/proc/cmd_admin_check_dialogue_logs, //checks a player's dialogue logs,
/datum/admins/proc/access_news_network, //allows access of newscasters,
/client/proc/giveruntimelog, //allows us to give access to runtime logs to somebody,
/client/proc/getserverlog, //allows us to fetch server logs (diary) for other days,
/client/proc/jumptocoord, //we ghost and jump to a coordinate,
/client/proc/Getmob, //teleports a mob to our location,
@@ -236,8 +233,6 @@ var/list/admin_verbs_server = list(
)
var/list/admin_verbs_debug = list(
/client/proc/reload_configuration, // CHOMPEdit
/client/proc/getruntimelog, //allows us to access runtime logs to somebody,
/client/proc/cmd_admin_list_open_jobs,
/client/proc/Debug2,
/client/proc/kill_air,
@@ -298,7 +293,8 @@ var/list/admin_verbs_debug = list(
/datum/admins/proc/quick_nif, //CHOMPStation Add,
/datum/admins/proc/quick_authentic_nif, //CHOMPStation add
/client/proc/reload_jobwhitelist, //ChompADD
/client/proc/reload_alienwhitelist //ChompADD
/client/proc/reload_alienwhitelist, //ChompADD
/client/proc/reload_configuration //CHOMPAdd
)
var/list/admin_verbs_paranoid_debug = list(
@@ -328,8 +324,6 @@ var/list/admin_verbs_hideable = list(
/datum/admins/proc/announce,
/client/proc/admin_ghost,
/client/proc/toggle_view_range,
/datum/admins/proc/view_txt_log,
/datum/admins/proc/view_atk_log,
/client/proc/cmd_admin_subtle_message,
/client/proc/cmd_admin_check_contents,
/client/proc/cmd_admin_check_player_logs,
@@ -429,8 +423,6 @@ var/list/admin_verbs_mod = list(
/datum/admins/proc/sendFax,
/client/proc/getserverlog, //allows us to fetch server logs (diary) for other days,
/datum/admins/proc/view_persistent_data,
/datum/admins/proc/view_txt_log, //shows the server log (diary) for today,
/datum/admins/proc/view_atk_log, //shows the server combat-log, doesn't do anything presently,
/client/proc/start_vote,
/datum/admins/proc/quick_nif, //CHOMPStation Add,
/client/proc/reload_jobwhitelist, //ChompADD

View File

@@ -1,70 +1,20 @@
/*
HOW DO I LOG RUNTIMES?
Firstly, start dreamdeamon if it isn't already running. Then select "world>Log Session" (or press the F3 key)
navigate the popup window to the data/logs/runtime/ folder from where your tgstation .dmb is located.
(you may have to make this folder yourself)
OPTIONAL: you can select the little checkbox down the bottom to make dreamdeamon save the log everytime you
start a world. Just remember to repeat these steps with a new name when you update to a new revision!
Save it with the name of the revision your server uses (e.g. r3459.txt).
Game Masters will now be able to grant access any runtime logs you have archived this way!
This will allow us to gather information on bugs across multiple servers and make maintaining the TG
codebase for the entire /TG/station commuity a TONNE easier :3 Thanks for your help!
*/
//This proc allows Game Masters to grant a client access to the .getruntimelog verb
//Permissions expire at the end of each round.
//Runtimes can be used to meta or spot game-crashing exploits so it's advised to only grant coders that
//you trust access. Also, it may be wise to ensure that they are not going to play in the current round.
/client/proc/giveruntimelog()
set name = ".giveruntimelog"
set desc = "Give somebody access to any session logfiles saved to the /log/runtime/ folder."
set category = null
if(!src.holder)
to_chat(src, span_red("Only Admins may use this command."))
return
var/client/target = tgui_input_list(src,"Choose somebody to grant access to the server's runtime logs (permissions expire at the end of each round):","Grant Permissions", GLOB.clients)
if(!istype(target,/client))
to_chat(src, span_red("Error: giveruntimelog(): Client not found."))
return
add_verb(target, /client/proc/getruntimelog)
to_chat(target, span_red("You have been granted access to runtime logs. Please use them responsibly or risk being banned."))
return
//This proc allows download of runtime logs saved within the data/logs/ folder by dreamdeamon.
//It works similarly to show-server-log.
/client/proc/getruntimelog()
set name = ".getruntimelog"
set desc = "Retrieve any session logfiles saved by dreamdeamon."
set category = null
var/path = browse_files("data/logs/runtime/")
if(!path)
return
if(file_spam_check())
return
message_admins("[key_name_admin(src)] accessed file: [path]")
src << run( file(path) )
to_chat(src, "Attempting to send file, this may take a fair few minutes if the file is very large.")
return
//This proc allows download of past server logs saved within the data/logs/ folder.
//It works similarly to show-server-log.
/client/proc/getserverlog()
set name = ".getserverlog"
set name = "Get Server Logs"
set desc = "Fetch logfiles from data/logs"
set category = null
set category = "Admin.Logs"
browseserverlogs()
var/path = browse_files("data/logs/")
/client/proc/browseserverlogs(current=FALSE, runtimes=FALSE)
var/log_choice = BROWSE_ROOT_ALL_LOGS
if(current)
log_choice = BROWSE_ROOT_CURRENT_LOGS
else if (runtimes)
log_choice = BROWSE_ROOT_RUNTIME_LOGS
var/path = browse_files(log_choice)
feedback_add_details("admin_verb","VTL") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
if(!path)
return
@@ -72,44 +22,13 @@
return
message_admins("[key_name_admin(src)] accessed file: [path]")
switch(tgui_alert(src,"View (in game), Open (in your system's text editor), or Download?", path, list("View", "Open", "Download")))
if ("View")
src << browse("<html><pre style='word-wrap: break-word;'>[html_encode(file2text(file(path)))]</pre></html>", list2params(list("window" = "viewfile.[path]")))
if ("Open")
src << run(file(path))
to_chat(src, "Attempting to send file, this may take a fair few minutes if the file is very large.")
return
//Other log stuff put here for the sake of organisation
//Shows today's server log
/datum/admins/proc/view_txt_log()
set category = "Admin.Logs"
set name = "Show Server Log"
set desc = "Shows today's server log."
var/path = "[log_path].log"
if( fexists(path) )
src << run( file(path) )
if ("Download")
src << ftp(file(path))
else
to_chat(src, span_red("Error: view_txt_log(): File not found/Invalid path([path])."))
return
feedback_add_details("admin_verb","VTL") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
return
//Shows today's attack log
/datum/admins/proc/view_atk_log()
set category = "Admin.Logs"
set name = "Show Server Attack Log"
set desc = "Shows today's server attack log."
to_chat(usr, "This verb doesn't actually do anything.")
/*
var/path = "data/logs/[time2text(world.realtime,"YYYY/MM-Month/DD-Day")] Attack.log"
if( fexists(path) )
src << run( file(path) )
else
to_chat(src, span_red("<Error: view_atk_log(): File not found/Invalid path([path])."))
return
usr << run( file(path) )
feedback_add_details("admin_verb","SSAL") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
return
*/
to_chat(src, "Attempting to send [path], this may take a fair few minutes if the file is very large.", confidential = TRUE)