diff --git a/.editorconfig b/.editorconfig index 1770177310d..ec762b7964c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,14 +1,17 @@ -[*] -indent_style = tab -indent_size = 4 -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true -end_of_line = lf - -[*.yml] -indent_style = space -indent_size = 2 - -[*.py] -indent_style = space +[*] +indent_style = tab +indent_size = 4 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +end_of_line = lf + +[*.yml] +indent_style = space +indent_size = 2 + +[*.py] +indent_style = space + +[*.rs] +indent_style = space diff --git a/.vscode/launch.json b/.vscode/launch.json index 349a859281d..e469e9c390f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -21,6 +21,12 @@ "request": "launch", "name": "DS Debug", "dmb": "${workspaceFolder}/${command:CurrentDMB}" + }, + { + "type": "cppvsdbg", + "request": "attach", + "name": "Rust Attach", + "processId": "${command:pickProcess}" } ] } diff --git a/code/__DEFINES/rust.dm b/code/__DEFINES/rust.dm index a87b76e7f6e..fa50b736f15 100644 --- a/code/__DEFINES/rust.dm +++ b/code/__DEFINES/rust.dm @@ -39,6 +39,8 @@ return __rustlib = "librustlibs[RUSTLIBS_SUFFIX].so" else // First check if it's built in the usual place. + if(fexists("./rust/target/i686-pc-windows-msvc/debug/rustlibs.dll")) + return __rustlib = "./rust/target/i686-pc-windows-msvc/debug/rustlibs.dll" if(fexists("./rust/target/i686-pc-windows-msvc/release/rustlibs.dll")) return __rustlib = "./rust/target/i686-pc-windows-msvc/release/rustlibs.dll" // Then check in the current directory. @@ -190,6 +192,32 @@ /proc/rustlibs_redis_publish(channel, message) return RUSTLIB_CALL(redis_publish, channel, message) + +// MARK: HTTP +#define RUSTLIBS_HTTP_METHOD_GET "get" +#define RUSTLIBS_HTTP_METHOD_PUT "put" +#define RUSTLIBS_HTTP_METHOD_DELETE "delete" +#define RUSTLIBS_HTTP_METHOD_PATCH "patch" +#define RUSTLIBS_HTTP_METHOD_HEAD "head" +#define RUSTLIBS_HTTP_METHOD_POST "post" + +/proc/rustlibs_http_send_request(datum/http_request/request) + return RUSTLIB_CALL(http_submit_async_request, request) + +/proc/rustlibs_http_check_request(datum/http_request/request) + return RUSTLIB_CALL(http_check_job, request) + +/proc/rustlibs_http_start_client(datum/http_request) + return RUSTLIB_CALL(http_start_client) + +/proc/rustlibs_http_shutdown_client(datum/http_request) + return RUSTLIB_CALL(http_shutdown_client) + +// MARK: Jobs +#define RUSTLIBS_JOB_NO_RESULTS_YET "NO RESULTS YET" +#define RUSTLIBS_JOB_NO_SUCH_JOB "NO SUCH JOB" +#define RUSTLIBS_JOB_ERROR "JOB PANICKED" + #undef RUSTLIB_CALL // Indexes for Tiles and InterestingTiles diff --git a/code/__DEFINES/rust_g.dm b/code/__DEFINES/rust_g.dm index e44c53894b2..42e76f465dc 100644 --- a/code/__DEFINES/rust_g.dm +++ b/code/__DEFINES/rust_g.dm @@ -68,20 +68,6 @@ /proc/rustg_git_commit_date_head(format = "%F") return RUSTG_CALL(RUST_G, "rg_git_commit_date_head")(format) -// HTTP Operations // - -#define RUSTG_HTTP_METHOD_GET "get" -#define RUSTG_HTTP_METHOD_PUT "put" -#define RUSTG_HTTP_METHOD_DELETE "delete" -#define RUSTG_HTTP_METHOD_PATCH "patch" -#define RUSTG_HTTP_METHOD_HEAD "head" -#define RUSTG_HTTP_METHOD_POST "post" -#define rustg_http_request_blocking(method, url, body, headers, options) RUSTG_CALL(RUST_G, "http_request_blocking")(method, url, body, headers, options) -#define rustg_http_request_async(method, url, body, headers, options) RUSTG_CALL(RUST_G, "http_request_async")(method, url, body, headers, options) -#define rustg_http_check_request(req_id) RUSTG_CALL(RUST_G, "http_check_request")(req_id) -/proc/rustg_create_async_http_client() return RUSTG_CALL(RUST_G, "start_http_client")() -/proc/rustg_close_async_http_client() return RUSTG_CALL(RUST_G, "shutdown_http_client")() - // Jobs Defines // #define RUSTG_JOB_NO_RESULTS_YET "NO RESULTS YET" diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm index 7aa089617a2..9df28929539 100644 --- a/code/__HELPERS/unsorted.dm +++ b/code/__HELPERS/unsorted.dm @@ -1878,7 +1878,7 @@ GLOBAL_DATUM_INIT(dview_mob, /mob/dview, new) return "Surgery Sounds" /** - * HTTP Get (Powered by RUSTG) + * HTTP Get (Powered by rustlibs) * * This proc should be used as a replacement for [/world/proc/Export] due to an underlying issue with it. * See: https://www.byond.com/forum/post/2772166 @@ -1891,7 +1891,7 @@ GLOBAL_DATUM_INIT(dview_mob, /mob/dview, new) */ /proc/HTTPGet(url) var/datum/http_request/req = new - req.prepare(RUSTG_HTTP_METHOD_GET, url) + req.prepare(RUSTLIBS_HTTP_METHOD_GET, url) req.begin_async() // Check if we are complete diff --git a/code/controllers/subsystem/SShttp.dm b/code/controllers/subsystem/SShttp.dm index 061dbd35479..70ca5cec81d 100644 --- a/code/controllers/subsystem/SShttp.dm +++ b/code/controllers/subsystem/SShttp.dm @@ -15,7 +15,7 @@ SUBSYSTEM_DEF(http) /datum/controller/subsystem/http/PreInit() . = ..() - rustg_create_async_http_client() // Open the door + rustlibs_http_start_client() // Open the door /datum/controller/subsystem/http/get_stat_details() return "P: [length(active_async_requests)] | T: [total_requests]" diff --git a/code/controllers/subsystem/SSmetrics.dm b/code/controllers/subsystem/SSmetrics.dm index 2c033d3bc24..de6c397c499 100644 --- a/code/controllers/subsystem/SSmetrics.dm +++ b/code/controllers/subsystem/SSmetrics.dm @@ -14,7 +14,7 @@ SUBSYSTEM_DEF(metrics) /datum/controller/subsystem/metrics/fire(resumed) - SShttp.create_async_request(RUSTG_HTTP_METHOD_POST, GLOB.configuration.metrics.metrics_endpoint, get_metrics_json(), list( + SShttp.create_async_request(RUSTLIBS_HTTP_METHOD_POST, GLOB.configuration.metrics.metrics_endpoint, get_metrics_json(), list( "Authorization" = "ApiKey [GLOB.configuration.metrics.metrics_api_token]", "Content-Type" = "application/json" )) diff --git a/code/datums/discord/discord_manager.dm b/code/datums/discord/discord_manager.dm index 006922315c1..41e0df881d4 100644 --- a/code/datums/discord/discord_manager.dm +++ b/code/datums/discord/discord_manager.dm @@ -22,7 +22,7 @@ GLOBAL_DATUM_INIT(discord_manager, /datum/discord_manager, new()) var/datum/discord_webhook_payload/dwp = new() dwp.webhook_content = "**\[[GLOB.configuration.system.instance_id]]** [content]" for(var/url in webhook_urls) - SShttp.create_async_request(RUSTG_HTTP_METHOD_POST, url, dwp.serialize2json(), list("content-type" = "application/json")) + SShttp.create_async_request(RUSTLIBS_HTTP_METHOD_POST, url, dwp.serialize2json(), list("content-type" = "application/json")) // This one is designed to take in a [/datum/discord_webhook_payload] which was prepared beforehand /datum/discord_manager/proc/send2discord_complex(destination, datum/discord_webhook_payload/dwp) @@ -37,7 +37,7 @@ GLOBAL_DATUM_INIT(discord_manager, /datum/discord_manager, new()) if(DISCORD_WEBHOOK_MENTOR) webhook_urls = GLOB.configuration.discord.mentor_webhook_urls for(var/url in webhook_urls) - SShttp.create_async_request(RUSTG_HTTP_METHOD_POST, url, dwp.serialize2json(), list("content-type" = "application/json")) + SShttp.create_async_request(RUSTLIBS_HTTP_METHOD_POST, url, dwp.serialize2json(), list("content-type" = "application/json")) // This one is for sending messages to the admin channel if no admins are active, complete with a ping to the game admins role /datum/discord_manager/proc/send2discord_simple_noadmins(content, check_send_always = FALSE) @@ -68,7 +68,7 @@ GLOBAL_DATUM_INIT(discord_manager, /datum/discord_manager, new()) var/datum/discord_webhook_payload/dwp = new() dwp.webhook_content = "**\[[GLOB.configuration.system.instance_id]]** [message]" for(var/url in GLOB.configuration.discord.admin_webhook_urls) - SShttp.create_async_request(RUSTG_HTTP_METHOD_POST, url, dwp.serialize2json(), list("content-type" = "application/json")) + SShttp.create_async_request(RUSTLIBS_HTTP_METHOD_POST, url, dwp.serialize2json(), list("content-type" = "application/json")) /datum/discord_manager/proc/send2discord_simple_mentor(content) var/alerttext @@ -89,7 +89,7 @@ GLOBAL_DATUM_INIT(discord_manager, /datum/discord_manager, new()) var/datum/discord_webhook_payload/dwp = new() dwp.webhook_content = "**\[[GLOB.configuration.system.instance_id]]** [message]" for(var/url in GLOB.configuration.discord.mentor_webhook_urls) - SShttp.create_async_request(RUSTG_HTTP_METHOD_POST, url, dwp.serialize2json(), list("content-type" = "application/json")) + SShttp.create_async_request(RUSTLIBS_HTTP_METHOD_POST, url, dwp.serialize2json(), list("content-type" = "application/json")) // Helper to make administrator ping easier /datum/discord_manager/proc/handle_administrator_ping() diff --git a/code/datums/http.dm b/code/datums/http.dm index 6679b859c65..4434bd2e7fd 100644 --- a/code/datums/http.dm +++ b/code/datums/http.dm @@ -1,3 +1,8 @@ +/* + DO NOT FUCK WITH THE DATUMS IN THIS FILE AS THEY ARE ALSO USED BY THE RUSTLIB DIRECTLY + SEE rust\src\rustlibs_http\mod.rs FOR DETAILS +*/ + /** * # HTTP Request * @@ -16,13 +21,13 @@ /// Body of the request being sent var/body /// Request headers being sent - var/headers + var/list/headers = list() /// URL that the request is being sent to var/url - /// If present, response body will be saved to this file. - var/output_file - /// The raw response, which will be decoeded into a [/datum/http_response] - var/_raw_response + /// Job error code, if any + var/error_code + /// The response for the request + var/datum/http_response/response_obj /// Callback for executing after async requests. Will be called with an argument of [/datum/http_response] as first argument var/datum/callback/cb @@ -44,26 +49,13 @@ THE METHODS IN THIS FILE ARE TO BE USED BY THE SUBSYSTEM AS A MANGEMENT HUB * * _body - The body of the request, if applicable * * _headers - Associative list of HTTP headers to send, if applicab;e */ -/datum/http_request/proc/prepare(_method, _url, _body = "", list/_headers, _output_file) - if(!length(_headers)) - headers = "" - else - headers = json_encode(_headers) +/datum/http_request/proc/prepare(_method, _url, _body = "", list/_headers) + if(istype(_headers)) + headers =_headers method = _method url = _url body = _body - output_file = _output_file - -/** - * Blocking executor - * - * Remains as a proof of concept to show it works, but should NEVER be used to do FFI halting the entire DD process up - * Async rqeuests are much preferred, but also require the subsystem to be firing for them to be answered - */ -/datum/http_request/proc/execute_blocking() - CRASH("Attempted to execute a blocking HTTP request") - // _raw_response = rustg_http_request_blocking(method, url, body, headers, build_options()) /** * Async execution starter @@ -73,26 +65,7 @@ THE METHODS IN THIS FILE ARE TO BE USED BY THE SUBSYSTEM AS A MANGEMENT HUB * As such, you cannot use this for events which may happen at roundstart (EG: IPIntel, BYOND account tracking, etc) */ /datum/http_request/proc/begin_async() - if(in_progress) - CRASH("Attempted to re-use a request object.") - - id = rustg_http_request_async(method, url, body, headers, build_options()) - - if(isnull(text2num(id))) - _raw_response = "Proc error: [id]" - CRASH("Proc error: [id]") - else - in_progress = TRUE - -/** - * Options builder - * - * Builds options for if we want to download files with SShttp - */ -/datum/http_request/proc/build_options() - if(output_file) - return json_encode(list("output_filename" = output_file, "body_filename" = null)) - return null + rustlibs_http_send_request(src) /** * Async completion checker @@ -111,15 +84,14 @@ THE METHODS IN THIS FILE ARE TO BE USED BY THE SUBSYSTEM AS A MANGEMENT HUB return TRUE // We got here, so check the status - var/result = rustg_http_check_request(id) + var/result = rustlibs_http_check_request(src) // If we have no result, were not finished - if(result == RUSTG_JOB_NO_RESULTS_YET) + if(error_code == RUSTLIBS_JOB_NO_RESULTS_YET) return FALSE else // If we got here, we have a result to parse - _raw_response = result - in_progress = FALSE + response_obj = result return TRUE /** @@ -130,18 +102,10 @@ THE METHODS IN THIS FILE ARE TO BE USED BY THE SUBSYSTEM AS A MANGEMENT HUB * Can be called on async and blocking requests */ /datum/http_request/proc/into_response() - var/datum/http_response/R = new() + if(!response_obj) + CRASH("Called into_response() while response_obj is null") + return response_obj - try - var/list/L = json_decode(_raw_response) - R.status_code = L["status_code"] - R.headers = L["headers"] - R.body = L["body"] - catch - R.errored = TRUE - R.error = _raw_response - - return R /** * # HTTP Response diff --git a/code/game/world.dm b/code/game/world.dm index 733eee0ce5a..885e98c0b3a 100644 --- a/code/game/world.dm +++ b/code/game/world.dm @@ -9,6 +9,7 @@ GLOBAL_DATUM(test_runner, /datum/test_runner) // If you do any SQL operations inside this proc, they must ***NOT*** be ran async. Otherwise players can join mid query // This is BAD. + SSmetrics.world_init_time = REALTIMEOFDAY // Do sanity checks to ensure RUST actually exists @@ -295,7 +296,7 @@ GLOBAL_LIST_EMPTY(world_topic_handlers) F << GLOB.log_directory /world/Del() - rustg_close_async_http_client() // Close the HTTP client. If you dont do this, youll get phantom threads which can crash DD from memory access violations + rustlibs_http_shutdown_client() // Close the HTTP client. If you dont do this, youll get phantom threads which can crash DD from memory access violations disable_auxtools_debugger() // Disables the debugger if running. See above comment if(SSredis.connected) diff --git a/code/modules/admin/centcom_ban_db.dm b/code/modules/admin/centcom_ban_db.dm index 5a1477dadc6..e1115aa9532 100644 --- a/code/modules/admin/centcom_ban_db.dm +++ b/code/modules/admin/centcom_ban_db.dm @@ -22,7 +22,7 @@ return var/datum/callback/cb = CALLBACK(src, TYPE_PROC_REF(/datum/admins, ccbdb_lookup_callback), usr, ckey) - SShttp.create_async_request(RUSTG_HTTP_METHOD_GET, "[GLOB.configuration.url.centcom_ban_db_url][ckey]", proc_callback=cb) + SShttp.create_async_request(RUSTLIBS_HTTP_METHOD_GET, "[GLOB.configuration.url.centcom_ban_db_url][ckey]", proc_callback=cb) /** * CCBDB Lookup Callback diff --git a/code/modules/admin/verbs/playsound.dm b/code/modules/admin/verbs/playsound.dm index 78784e7028f..12beb8fbe8a 100644 --- a/code/modules/admin/verbs/playsound.dm +++ b/code/modules/admin/verbs/playsound.dm @@ -158,7 +158,7 @@ GLOBAL_LIST_EMPTY(sounds_cache) // Send the request off var/datum/http_request/media_poll_request = new() // The fact we are using GET with a body offends me - media_poll_request.prepare(RUSTG_HTTP_METHOD_GET, GLOB.configuration.system.ytdlp_url, json_encode(request_body)) + media_poll_request.prepare(RUSTLIBS_HTTP_METHOD_GET, GLOB.configuration.system.ytdlp_url, json_encode(request_body)) // Start it off and wait media_poll_request.begin_async() UNTIL(media_poll_request.is_complete()) diff --git a/code/modules/client/2fa.dm b/code/modules/client/2fa.dm index 390c408193b..a40cffcace3 100644 --- a/code/modules/client/2fa.dm +++ b/code/modules/client/2fa.dm @@ -6,7 +6,7 @@ // Client does not have 2FA enabled. Set it up. if(prefs._2fa_status == _2FA_DISABLED) // Get us an auth token - var/datum/http_response/qrcr = MakeAPICall(RUSTG_HTTP_METHOD_GET, "2fa/generate_qr?ckey=[ckey]") + var/datum/http_response/qrcr = MakeAPICall(RUSTLIBS_HTTP_METHOD_GET, "2fa/generate_qr?ckey=[ckey]") // If this fails, shits gone bad if(qrcr.errored) alert(usr, "Something has gone VERY wrong ingame. Please inform the server host.\nError details: [qrcr.error]") @@ -33,7 +33,7 @@ B.close() return - var/datum/http_response/vr = MakeAPICall(RUSTG_HTTP_METHOD_GET, "2fa/validate_code?ckey=[ckey]&code=[entered_code]") + var/datum/http_response/vr = MakeAPICall(RUSTLIBS_HTTP_METHOD_GET, "2fa/validate_code?ckey=[ckey]&code=[entered_code]") // If this fails, shits gone bad if(vr.errored) alert(usr, "Something has gone VERY wrong ingame. Please inform the server host.\nError details: [vr.error]") @@ -86,7 +86,7 @@ alert(usr, "2FA deactivation aborted!") return - var/datum/http_response/vr = MakeAPICall(RUSTG_HTTP_METHOD_GET, "2fa/validate_code?ckey=[ckey]&code=[entered_code]") + var/datum/http_response/vr = MakeAPICall(RUSTLIBS_HTTP_METHOD_GET, "2fa/validate_code?ckey=[ckey]&code=[entered_code]") // If this fails, shits gone bad if(vr.errored) alert(usr, "Something has gone VERY wrong ingame. Please inform the server host.\nError details: [vr.error]") diff --git a/rust/Cargo.lock b/rust/Cargo.lock index bcf3f0d577b..daafecb24d8 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -102,6 +102,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bindgen" version = "0.71.1" @@ -216,9 +222,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.6" +version = "1.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d6dbb628b8f8555f86d0323c2eb39e3ec81901f4b83e091db8a6a76d316a333" +checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362" dependencies = [ "shlex", ] @@ -811,9 +817,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.154" +version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" [[package]] name = "libloading" @@ -1295,6 +1301,20 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "untrusted", + "windows-sys", +] + [[package]] name = "rustc-hash" version = "2.1.1" @@ -1326,9 +1346,42 @@ dependencies = [ "serde_json", "thread-priority", "toml 0.8.20", + "ureq", "walkdir", ] +[[package]] +name = "rustls" +version = "0.23.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" + +[[package]] +name = "rustls-webpki" +version = "0.103.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.16" @@ -1466,6 +1519,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "1.0.109" @@ -1592,6 +1651,28 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "url", + "webpki-roots", +] + [[package]] name = "url" version = "2.5.4" @@ -1691,6 +1772,15 @@ version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" +[[package]] +name = "webpki-roots" +version = "0.26.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1899,6 +1989,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + [[package]] name = "zerovec" version = "0.10.4" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index e1029d47aa7..07d114616d0 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -34,6 +34,7 @@ serde_json = { version = "1.0" } walkdir = "2.5.0" # regex regex = "1.10.5" +ureq = "2.12" png = "0.17.16" chrono = "0.4.39" toml = "0.8.20" diff --git a/rust/README.MD b/rust/README.MD index c3a5c733b77..d6a1412b66a 100644 --- a/rust/README.MD +++ b/rust/README.MD @@ -10,6 +10,17 @@ It currently handles: - Atmospherics (`milla`) - DMM manipulation stuff (`mapmanip`) +It also imports and tweaks the following [rust-g](https://github.com/tgstation/rust-g) features: + +- DMI +- File +- HTTP +- JSON +- Logging +- Noisegen +- Redis PubSub +- TOML + ## Building BYOND 516.1651 introduced breaking changes to ByondAPI, the interop system to get data other than strings in and out of DLLs. diff --git a/rust/src/jobs/mod.rs b/rust/src/jobs/mod.rs new file mode 100644 index 00000000000..db14d739391 --- /dev/null +++ b/rust/src/jobs/mod.rs @@ -0,0 +1,64 @@ +//! Job system +use flume::{Receiver, TryRecvError}; +use std::{ + cell::RefCell, + collections::hash_map::{Entry, HashMap}, + thread, +}; + +type Output = String; +type JobID = usize; + +struct Job { + rx: Receiver, + handle: thread::JoinHandle<()>, +} + +pub const NO_RESULTS_YET: &str = "NO RESULTS YET"; +pub const NO_SUCH_JOB: &str = "NO SUCH JOB"; +pub const JOB_PANICKED: &str = "JOB PANICKED"; + +#[derive(Default)] +struct Jobs { + map: HashMap, + next_job: usize, +} + +impl Jobs { + fn start Output + Send + 'static>(&mut self, f: F) -> usize { + let (tx, rx) = flume::unbounded(); + let handle = thread::spawn(move || { + let _ = tx.send(f()); + }); + let id = self.next_job; + self.next_job += 1; + self.map.insert(id.clone(), Job { rx, handle }); + id + } + + fn check(&mut self, id: &usize) -> Option> { + let entry = match self.map.entry(id.to_owned()) { + Entry::Occupied(occupied) => occupied, + Entry::Vacant(_) => return None, + }; + let result = match entry.get().rx.try_recv() { + Ok(result) => Ok(result), + Err(TryRecvError::Empty) => return Some(Err(TryRecvError::Empty)), + Err(TryRecvError::Disconnected) => Err(TryRecvError::Disconnected), + }; + let _ = entry.remove().handle.join(); + Some(result) + } +} + +thread_local! { + static JOBS: RefCell = RefCell::default(); +} + +pub fn start Output + Send + 'static>(f: F) -> usize { + JOBS.with(|jobs| jobs.borrow_mut().start(f)) +} + +pub fn check(id: &usize) -> Option> { + JOBS.with(|jobs| jobs.borrow_mut().check(id)) +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 3355dcfcfd9..38ba35388fc 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -1,12 +1,14 @@ +mod jobs; mod logging; mod mapmanip; mod milla; -mod redis_pubsub; mod rustlibs_dmi; mod rustlibs_file; +mod rustlibs_http; mod rustlibs_json; mod rustlibs_logging; mod rustlibs_noisegen; +mod rustlibs_redispubsub; mod rustlibs_toml; #[cfg(all(not(feature = "byond-515"), not(feature = "byond-516")))] diff --git a/rust/src/rustlibs_http/mod.rs b/rust/src/rustlibs_http/mod.rs new file mode 100644 index 00000000000..f4cc874a556 --- /dev/null +++ b/rust/src/rustlibs_http/mod.rs @@ -0,0 +1,244 @@ +use crate::jobs; +use crate::logging; +use byondapi::value::ByondValue; +use eyre::Result; +use serde::{Deserialize, Serialize}; +use std::cell::RefCell; +use std::collections::{BTreeMap, HashMap}; +// ---------------------------------------------------------------------------- +// Interface + +#[derive(Serialize, Deserialize)] +struct Response { + status_code: u16, + headers: HashMap, + body: Option, +} + +// If the response can be deserialized -> success. +// If the response can't be deserialized -> failure or WIP. + +// ---------------------------------------------------------------------------- +// Shared HTTP client state + +const VERSION: &str = env!("CARGO_PKG_VERSION"); +const PKG_NAME: &str = env!("CARGO_PKG_NAME"); + +thread_local! { + pub static HTTP_CLIENT: RefCell> = RefCell::new(Some(ureq::agent())); +} + +// ---------------------------------------------------------------------------- +// Request construction and execution + +struct RequestPrep { + req: ureq::Request, + body: Vec, +} + +fn construct_request( + method: &str, + url: &str, + body: &str, + headers: &Option>, +) -> Option { + HTTP_CLIENT.with(|cell| { + let borrow = cell.borrow_mut(); + match &*borrow { + Some(client) => { + let mut req = match method { + "post" => client.post(url), + "put" => client.put(url), + "patch" => client.patch(url), + "delete" => client.delete(url), + "head" => client.head(url), + _ => client.get(url), + } + .set("User-Agent", &format!("{PKG_NAME}/{VERSION}")) + .timeout(std::time::Duration::from_secs(5)); + + let final_body = body.as_bytes().to_vec(); + + match headers { + Some(h) => { + for (key, value) in h { + req = req.set(key, value); + } + } + None => {} + } + + Some(RequestPrep { + req, + body: final_body, + }) + } + + // If we got here we royally fucked up + None => None, + } + }) +} + +fn submit_request(prep: RequestPrep) -> Result { + // Send the request + // TODO: this is a sinful hack, rewrite this as soon as the module is stable on live + let response = match prep.req.send_bytes(&prep.body) { + Ok(r) => r, + Err(ureq::Error::Status(code, r)) => r, + Err(ureq::Error::Transport(t)) => { + let mut resp = Response { + status_code: 0, + headers: HashMap::new(), + body: None, + }; + resp.body = Some(t.to_string()); + return Ok(serde_json::to_string(&resp)?); + } + }; + let body; + let mut resp = Response { + status_code: response.status(), + headers: HashMap::new(), + body: None, + }; + + for key in response.headers_names() { + let Some(value) = response.header(&key) else { + continue; + }; + + resp.headers.insert(key, value.to_owned()); + } + + body = response.into_string()?; + resp.body = Some(body); + + Ok(serde_json::to_string(&resp)?) +} + +// Exported methods +#[byondapi::bind] +fn http_start_client() -> Result { + HTTP_CLIENT.with(|cell| cell.replace(Some(ureq::agent()))); + Ok(ByondValue::null()) +} + +#[byondapi::bind] +fn http_shutdown_client() -> Result { + HTTP_CLIENT.with(|cell| cell.replace(None)); + Ok(ByondValue::null()) +} + +#[byondapi::bind] +fn http_submit_async_request(mut request: ByondValue) -> Result { + let method = request.read_var("method")?.get_string()?; + let url = request.read_var("url")?.get_string()?; + let body = request.read_var("body")?.get_string()?; + + let headers = request.read_var("headers")?.get_list()?; + + let mut real_headers = None; + + if headers.len() > 0 { + let mut btm: BTreeMap = BTreeMap::new(); + + for pair in headers.chunks(2) { + if let [k, v] = pair { + let kstr = k.get_string()?; + let vstr = v.get_string()?; + btm.insert(kstr, vstr); + } + } + + real_headers = Some(btm); + } + + let req = match construct_request(method.as_str(), url.as_str(), body.as_str(), &real_headers) { + Some(r) => r, + None => return Ok(ByondValue::null()), + }; + + // Start the request as a job on a new thread + let job_id = jobs::start(move || match submit_request(req) { + Ok(r) => r, + Err(e) => e.to_string(), + }); + + // Write job id back to BYOND + request.write_var("id", &ByondValue::new_num(job_id as f32))?; + request.write_var("in_progress", &ByondValue::new_num(1f32))?; + + Ok(ByondValue::null()) +} + +#[byondapi::bind] +fn http_check_job(mut request: ByondValue) -> Result { + // logging::setup_panic_handler(); + let id = request.read_var("id")?.get_number()? as usize; + match jobs::check(&id) { + // Job id exists, check progress + Some(res) => match res { + // Request completed, parse it + Ok(res) => { + // We are no longer in progress + request.write_var("in_progress", &ByondValue::new_num(0f32))?; + request + .write_var("error_code", &ByondValue::null()) + .unwrap(); + + // Decode response + let web_response: Response = serde_json::from_str(&res).unwrap(); + // We have a response - assemble our HTML datum + let target_type = ByondValue::new_str("/datum/http_response")?; + let mut response_datum = ByondValue::builtin_new(target_type, &[])?; + + // Write primitives + response_datum + .write_var( + "status_code", + &ByondValue::new_num(web_response.status_code as f32), + ) + .unwrap(); + response_datum + .write_var("body", &ByondValue::new_str(web_response.body.unwrap())?)?; + + // Headers are more complicated since its an assoc list + let mut headers_list = ByondValue::new_list().unwrap(); + for header_k in web_response.headers.keys() { + // Get the key as a BV + let hk_bv = ByondValue::new_str(header_k.to_string())?; + + // Get the value as a BV + let hv = web_response.headers.get(header_k).unwrap(); + let hv_bv = ByondValue::new_str(hv.to_string())?; + + headers_list.write_list_index(hk_bv, hv_bv)?; + } + + // Send it back + Ok(response_datum) + } + // Request is still being made + Err(flume::TryRecvError::Empty) => { + request.write_var("error_code", &ByondValue::new_str(jobs::NO_RESULTS_YET)?)?; + return Ok(ByondValue::null()); + } + // Something bad happened during the request + Err(flume::TryRecvError::Disconnected) => { + request.write_var("error_code", &ByondValue::new_str(jobs::JOB_PANICKED)?)?; + request.write_var("in_progress", &ByondValue::new_num(0f32))?; + return Ok(ByondValue::null()); + } + }, + // Job id does not exist + None => { + request.write_var( + "error_code", + &ByondValue::new_str(jobs::NO_SUCH_JOB).unwrap(), + )?; + + return Ok(ByondValue::null()); + } + } +} diff --git a/rust/src/redis_pubsub/mod.rs b/rust/src/rustlibs_redispubsub/mod.rs similarity index 100% rename from rust/src/redis_pubsub/mod.rs rename to rust/src/rustlibs_redispubsub/mod.rs diff --git a/rustlibs_515.dll b/rustlibs_515.dll index c637067d43a..bfb83bf3360 100644 Binary files a/rustlibs_515.dll and b/rustlibs_515.dll differ diff --git a/rustlibs_515_prod.dll b/rustlibs_515_prod.dll index 6ebbf7d5695..b101c4fef5e 100644 Binary files a/rustlibs_515_prod.dll and b/rustlibs_515_prod.dll differ diff --git a/rustlibs_516.dll b/rustlibs_516.dll index 3b4ef7d03c8..bdb32020672 100644 Binary files a/rustlibs_516.dll and b/rustlibs_516.dll differ diff --git a/rustlibs_516_prod.dll b/rustlibs_516_prod.dll index ab03fa796e6..f414f24a93d 100644 Binary files a/rustlibs_516_prod.dll and b/rustlibs_516_prod.dll differ diff --git a/tools/ci/librustlibs_ci_515.so b/tools/ci/librustlibs_ci_515.so index a15d87fe489..e5db6d479de 100644 Binary files a/tools/ci/librustlibs_ci_515.so and b/tools/ci/librustlibs_ci_515.so differ diff --git a/tools/ci/librustlibs_ci_516.so b/tools/ci/librustlibs_ci_516.so index eb97862d752..832602e301a 100644 Binary files a/tools/ci/librustlibs_ci_516.so and b/tools/ci/librustlibs_ci_516.so differ