From ac99b78fc40a572517c529f506f149378396540f Mon Sep 17 00:00:00 2001 From: Kashargul <144968721+Kashargul@users.noreply.github.com> Date: Thu, 20 Mar 2025 18:52:06 +0100 Subject: [PATCH] Server logs (#17264) * Ports TGs server log verb * . * those never worked * we have tht just not as glob * Update admin.dm --------- Co-authored-by: Cameron Lennox --- code/__defines/admin.dm | 5 + code/__defines/text.dm | 3 + code/_global_vars/_regexes.dm | 5 + code/_helpers/files.dm | 121 +++++++++++++++++---- code/_helpers/icons.dm | 3 +- code/_helpers/text.dm | 12 +++ code/modules/admin/admin_verb_lists_vr.dm | 8 -- code/modules/admin/verbs/getlogs.dm | 125 ++++------------------ code/modules/awaymissions/pamphlet.dm | 8 +- 9 files changed, 153 insertions(+), 137 deletions(-) diff --git a/code/__defines/admin.dm b/code/__defines/admin.dm index 8f0189008f1..4c3aa6ddbd5 100644 --- a/code/__defines/admin.dm +++ b/code/__defines/admin.dm @@ -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" diff --git a/code/__defines/text.dm b/code/__defines/text.dm index a638832cb99..cbb6cf67cf9 100644 --- a/code/__defines/text.dm +++ b/code/__defines/text.dm @@ -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) diff --git a/code/_global_vars/_regexes.dm b/code/_global_vars/_regexes.dm index 1d92bc8c52e..a227920ba25 100644 --- a/code/_global_vars/_regexes.dm +++ b/code/_global_vars/_regexes.dm @@ -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. diff --git a/code/_helpers/files.dm b/code/_helpers/files.dm index d18f6f4623e..ec2fb1ca741 100644 --- a/code/_helpers/files.dm +++ b/code/_helpers/files.dm @@ -12,52 +12,112 @@ 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 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) diff --git a/code/_helpers/icons.dm b/code/_helpers/icons.dm index 464ad55806d..3a75bda7f6f 100644 --- a/code/_helpers/icons.dm +++ b/code/_helpers/icons.dm @@ -663,8 +663,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) diff --git a/code/_helpers/text.dm b/code/_helpers/text.dm index 4d1a0679b6f..540535383c3 100644 --- a/code/_helpers/text.dm +++ b/code/_helpers/text.dm @@ -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 :( diff --git a/code/modules/admin/admin_verb_lists_vr.dm b/code/modules/admin/admin_verb_lists_vr.dm index c09f7b95c95..af8b77506e2 100644 --- a/code/modules/admin/admin_verb_lists_vr.dm +++ b/code/modules/admin/admin_verb_lists_vr.dm @@ -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, @@ -232,7 +229,6 @@ var/list/admin_verbs_server = list( ) var/list/admin_verbs_debug = list( - /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, @@ -319,8 +315,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, @@ -420,8 +414,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 ) diff --git a/code/modules/admin/verbs/getlogs.dm b/code/modules/admin/verbs/getlogs.dm index f224a6ab409..ad29bb8ca36 100644 --- a/code/modules/admin/verbs/getlogs.dm +++ b/code/modules/admin/verbs/getlogs.dm @@ -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]") - 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) ) - 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("
[html_encode(file2text(file(path)))]
", list2params(list("window" = "viewfile.[path]"))) + if ("Open") + src << run(file(path)) + if ("Download") + src << ftp(file(path)) + else + return + to_chat(src, "Attempting to send [path], this may take a fair few minutes if the file is very large.", confidential = TRUE) diff --git a/code/modules/awaymissions/pamphlet.dm b/code/modules/awaymissions/pamphlet.dm index 6ac224d1225..9059e05a45a 100644 --- a/code/modules/awaymissions/pamphlet.dm +++ b/code/modules/awaymissions/pamphlet.dm @@ -10,7 +10,7 @@ info = "They don't make you kill people. There, we said it. Now get back to work!" /obj/item/paper/pamphlet/gateway - info = "Welcome to the Nanotrasen Gateway project...
\ + info = span_bold("Welcome to the Nanotrasen Gateway project...") + "
\ Congratulations! If you're reading this, you and your superiors have decided that you're \ ready to commit to a life spent colonising the rolling hills of far away worlds. You \ must be ready for a lifetime of adventure, a little bit of hard work, and an award \ @@ -20,7 +20,7 @@ been fully scanned by a Nanotrasen expeditionary team, and are certified to be 100% safe. \ We've even left a case of space beer along with the basic materials you'll need to expand \ Nanotrasen's operational area and start your new life.

\ - Gateway Operation Basics
\ + " + span_bold("Gateway Operation Basics") + "
\ All Nanotrasen approved Gateways operate on the same basic principals. They operate off \ area equipment power as you would expect, and without this supply, it cannot safely function, \ causinng it to reject all attempts at operation.

\ @@ -29,14 +29,14 @@ interface will give you an estimate accurate to the minute. Power loss will not interrupt the \ searching process. Influenza will not interrupt the searching process. Temporal anomalies \ may cause the estimate to be inaccurate, but will not interrupt the searching process.

\ - Life On The Other Side
\ + " + span_bold("Life On The Other Side") + "
\ Once you have traversed the Gateway, you may experience some disorientation. Do not panic. \ This is a normal side effect of travelling vast distances in a short period of time. You should \ survey the immediate area, and attempt to locate your complimentary case of space beer. Our \ expeditionary teams have ensured the complete safety of all away locations, but in a small \ number of cases, the Gateway they have established may not be immediately obvious. \ Do not panic if you cannot locate the return Gateway. Begin colonisation of the destination. \ -

A New World
\ +

" + span_bold("A New World") + "
\ As a participant in the Nanotrasen Gateway Project, you will be on the frontiers of space. \ Though complete safety is assured, participants are advised to prepare for inhospitable \ environs."