From 979b2687f9f6cce133f760bf2f9a4d5ab4dfac01 Mon Sep 17 00:00:00 2001 From: Truman Kilen Date: Wed, 5 Jun 2024 11:58:05 -0500 Subject: [PATCH] WIP WebSocket API --- Cargo.lock | 113 ++++++++-- .../Content/_mint/WebSocketConnection.uasset | Bin 0 -> 1780 bytes .../Content/_mint/WebSocketConnection.uexp | Bin 0 -> 592 bytes .../Content/_mint/WebSocketEventType.uasset | Bin 0 -> 1140 bytes .../FSD/Content/_mint/WebSocketEventType.uexp | Bin 0 -> 491 bytes hook/Cargo.toml | 3 + hook/src/hooks/mod.rs | 6 +- hook/src/hooks/ws.rs | 208 ++++++++++++++++++ 8 files changed, 316 insertions(+), 14 deletions(-) create mode 100644 assets/integration/FSD/Content/_mint/WebSocketConnection.uasset create mode 100644 assets/integration/FSD/Content/_mint/WebSocketConnection.uexp create mode 100644 assets/integration/FSD/Content/_mint/WebSocketEventType.uasset create mode 100644 assets/integration/FSD/Content/_mint/WebSocketEventType.uexp create mode 100644 hook/src/hooks/ws.rs diff --git a/Cargo.lock b/Cargo.lock index cb9f16fb..4185c702 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1196,6 +1196,12 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + [[package]] name = "der" version = "0.7.8" @@ -2117,7 +2123,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.11", "indexmap 2.2.6", "slab", "tokio", @@ -2186,6 +2192,8 @@ dependencies = [ "anyhow", "bitflags 2.4.1", "fs-err", + "futures", + "futures-util", "hook_resolvers", "mint_lib", "patternsleuth", @@ -2195,6 +2203,7 @@ dependencies = [ "serde", "serde_json", "tokio", + "tokio-tungstenite", "tracing", "widestring", "windows 0.52.0", @@ -2219,6 +2228,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.4.6" @@ -2226,7 +2246,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http", + "http 0.2.11", "pin-project-lite", ] @@ -2253,7 +2273,7 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", + "http 0.2.11", "http-body", "httparse", "httpdate", @@ -2273,9 +2293,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", - "http", + "http 0.2.11", "hyper", - "rustls", + "rustls 0.21.10", "tokio", "tokio-rustls", ] @@ -2837,7 +2857,7 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http", + "http 0.2.11", "mime", "pin-project-lite", "reqwest", @@ -3790,7 +3810,7 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", + "http 0.2.11", "http-body", "hyper", "hyper-rustls", @@ -3802,7 +3822,7 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls", + "rustls 0.21.10", "rustls-pemfile", "serde", "serde_json", @@ -3829,7 +3849,7 @@ checksum = "88a3e86aa6053e59030e7ce2d2a3b258dd08fc2d337d52f73f6cb480f5858690" dependencies = [ "anyhow", "async-trait", - "http", + "http 0.2.11", "reqwest", "serde", "task-local-extensions", @@ -3954,10 +3974,23 @@ checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" dependencies = [ "log", "ring", - "rustls-webpki", + "rustls-webpki 0.101.7", "sct", ] +[[package]] +name = "rustls" +version = "0.23.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a218f0f6d05669de4eabfb24f31ce802035c952429d037507b4a4a39f0e60c5b" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki 0.102.4", + "subtle", + "zeroize", +] + [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -3967,6 +4000,12 @@ dependencies = [ "base64 0.21.5", ] +[[package]] +name = "rustls-pki-types" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" + [[package]] name = "rustls-webpki" version = "0.101.7" @@ -3977,6 +4016,17 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustls-webpki" +version = "0.102.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.14" @@ -4734,8 +4784,21 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls", + "rustls 0.21.10", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "becd34a233e7e31a3dbf7c7241b38320f57393dcae8e7324b0167d21b8e320b0" +dependencies = [ + "futures-util", + "log", + "rustls 0.23.9", "tokio", + "tungstenite", ] [[package]] @@ -4888,6 +4951,24 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" +[[package]] +name = "tungstenite" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.1.0", + "httparse", + "log", + "rand", + "sha1", + "thiserror", + "utf-8", +] + [[package]] name = "twox-hash" version = "1.6.3" @@ -5139,8 +5220,8 @@ dependencies = [ "flate2", "log", "once_cell", - "rustls", - "rustls-webpki", + "rustls 0.21.10", + "rustls-webpki 0.101.7", "url", "webpki-roots", ] @@ -5163,6 +5244,12 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8parse" version = "0.2.1" diff --git a/assets/integration/FSD/Content/_mint/WebSocketConnection.uasset b/assets/integration/FSD/Content/_mint/WebSocketConnection.uasset new file mode 100644 index 0000000000000000000000000000000000000000..9c98f7dd41e5cc8f2e9e508f1c41c2ee6a20d751 GIT binary patch literal 1780 zcmbVMT}V_>5I&owX<3#(NhNAOLWXP+R#cR=>T3Stx)q|{-O7hV)Fh*a zq^O6ahbWK;G${Ic2qhyQ0wXa9!JyIyB`K4#G`IQg?y*f)tOo9xIrH<)Idf)iXMN)J z?{2rdG=Q;?1x%xbwC?FF-Z7c+A@EZJyI*r6F#)>9=$O)mJnNl--l*HJpAU!XNw==A zu;oc!LTh?Z{Nm81+DN+bK*kmk&$Y{(7^{t;WrYr+ zhhS`_EFr`%(qY;uU^d0-jwP?p9_+8ldV0R~v$Y@Lhy&K&(1np*1Mur2?3Ca?BbLy% zoAhzS^+u_R>r1LCWL1BNmznIAO0F90vdk@Ng13O(Ig&zQuU2`!Vs~&wt!6{HNgb$`$3;DLx_L2np7XNgYO@nXN|jWJ1!o!t;<4dJ zC6`StDVZygu4l`(mA;PYF4=SW8*L#eVZIjcjk>}f51}YC(0(dcVqo^#x`#pJ=DqL( zUFr~CVg_Crd(;Glk*iuI+0CM;4g_RKvem{}!N}<0rL@hI)tLg^>fL@2SI*&1r&P{a zcfE2KSJs+sJm6U<;=RY@Ss!)q&6g~d@O2u`MZ(v_Wh=XSCU6?yCRLdM-`%NffP?m= zwdu+`jDoo!n-!(yOrQ{ECs*jw^nBJd*;T~`6UTC+Nbmpt?~PZ!g!@{5_sgqc_b7F~ zMo0qsMzbk8)jOU{OvVJ+mU-NmKO1@vG}{JDfiG+V2bdT^kgy3H*f1s_VT<%^wN*^B zKbz1A?i(lWEcj|KCjLunz4A245GRS_?08WSau#cn=O5!0|BC@@b{;$=$Tv@cNbWJs zyNbF9M}K+nF95;zMw93l_>Xx|Tf~ce;8RqupY(wMy9Nspda6y67;C~j{3QVs(*VMI xiT4!!^uwD6Qv}*dpMc*f8d!*DufRi|uss zYXzv4(i@HB=q>!AoJePA1VDF_t4KLDI^~T(%+(jl|4+SyVx_96DyDR!m^E$fgjdg$%Y%us74F*3ulwQo1-m5`&j0`b literal 0 HcmV?d00001 diff --git a/assets/integration/FSD/Content/_mint/WebSocketEventType.uasset b/assets/integration/FSD/Content/_mint/WebSocketEventType.uasset new file mode 100644 index 0000000000000000000000000000000000000000..6b408848fca3d6cea4d35e79d576be92f76e2c5f GIT binary patch literal 1140 zcmX@utTpfF|Ns9Jm>C$jm>9qS$YiiN7-#<5;|be`7KW{Lg<5JXKr#E0Kj#}l_4N?89=k6*-&+W z{Fwt3mj}}N?uogn`tiA$c_sScsY${4$=RtTu4SotB_WjssSM1H&T~XS%7T-NG7C!d zo%4%QL;aJoQj<#eca~I>It&YIVZ8Wn4zS+VxlCF=aQP1SejE3 zA0JwrS_I?(Ri?P+mF6-$%iQn|WQt2>aY0UEr615azKI147iLFp;QP=gG4DcVK7;J@255nZ()W)ysu*fgK6K6i=dS1=n_#pd0b^!qETr_e3 literal 0 HcmV?d00001 diff --git a/assets/integration/FSD/Content/_mint/WebSocketEventType.uexp b/assets/integration/FSD/Content/_mint/WebSocketEventType.uexp new file mode 100644 index 0000000000000000000000000000000000000000..35f1dd98951252f77a26f344b3474465cfc452cc GIT binary patch literal 491 zcmZvZJ5mH85QbTGeOp%fj7^PrGz^3^bZ4t)Fqks2cC48rcn|O5%^X;Q38n+S{=4Yr z2e#r6r;WSKAvtUMwy|P6ujk7`-TX3FqLjWfu?tc$C?5lN3K&5G>Y#NJrS)yt_u^u; z-x);-w9d{NAj23?=V%jjnVpeTkKOgt^zqq?%heH#EIpxf$pC;Rw3-1;$i!?tu_#xh zaU4c*wK_nNt%ifi9mGIB8OxY5W}AqEsdsq)?)Tz)y8}NW38ip*B3(_l6 v)j;|hre54E1{7O&3tiI3rrT$Tr>2h@?;DRB9~w^@pBu}om)QLMy#D?J{}M8p literal 0 HcmV?d00001 diff --git a/hook/Cargo.toml b/hook/Cargo.toml index da9f14d5..fe9f7bb0 100644 --- a/hook/Cargo.toml +++ b/hook/Cargo.toml @@ -32,3 +32,6 @@ mint_lib = { path = "../mint_lib" } bitflags = "2.4.1" widestring = "1.0.2" tokio = { workspace = true, features = ["full"] } +tokio-tungstenite = { version = "0.23.0", features = ["rustls"] } +futures-util = "0.3.30" +futures = "0.3.30" diff --git a/hook/src/hooks/mod.rs b/hook/src/hooks/mod.rs index 90308d23..39312f83 100644 --- a/hook/src/hooks/mod.rs +++ b/hook/src/hooks/mod.rs @@ -1,5 +1,7 @@ #![allow(clippy::missing_transmute_annotations)] +mod ws; + use std::{ ffi::c_void, path::{Path, PathBuf}, @@ -49,7 +51,9 @@ pub unsafe fn initialize() -> Result<()> { exec_print_string as ExecFn, ), ] - .into_iter() + .iter() + .chain(ws::hooks().iter()) + .cloned() .collect::>(); HookUFunctionBind.initialize( diff --git a/hook/src/hooks/ws.rs b/hook/src/hooks/ws.rs new file mode 100644 index 00000000..ccaf2c9b --- /dev/null +++ b/hook/src/hooks/ws.rs @@ -0,0 +1,208 @@ +use futures_util::{SinkExt, StreamExt as _}; +use std::collections::HashMap; +use std::ffi::c_void; +use std::sync::{Mutex, MutexGuard, OnceLock}; +use tokio::sync::mpsc; + +use crate::hooks::ExecFn; +use crate::ue::{self, FString}; + +pub fn hooks() -> &'static [(&'static str, ExecFn)] { + &[ + ( + "/Game/_mint/WebSocketConnection.WebSocketConnection_C:Connect", + exec_connect as ExecFn, + ), + ( + "/Game/_mint/WebSocketConnection.WebSocketConnection_C:Send", + exec_send as ExecFn, + ), + ( + "/Game/_mint/WebSocketConnection.WebSocketConnection_C:GetEvent", + exec_get_event as ExecFn, + ), + ] +} + +// TODO potential bug if object gets freed and another gets allocated with same address +// need to implement and leverage TWeakPtr +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct Handle(*mut ue::UObject); +unsafe impl Send for Handle {} + +struct State { + rt_handle: tokio::runtime::Handle, + handles: HashMap, +} +impl Default for State { + fn default() -> Self { + let (tx, rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + + tx.send(rt.handle().clone()).unwrap(); + + rt.block_on(futures::future::pending::<()>()) + }); + let rt_handle = rx.recv().unwrap(); + tracing::info!("created tokio runtime"); + Self { + rt_handle, + handles: Default::default(), + } + } +} + +struct Connection { + tx: mpsc::Sender, + rx: mpsc::Receiver, +} + +struct Message { + type_: EventType, + data: String, +} + +#[derive(Default, Debug, Clone, Copy)] +#[repr(u8)] +enum EventType { + #[default] + None, + Open, + Close, + Message, + Error, +} + +static STATE: OnceLock> = OnceLock::new(); +fn get_state() -> MutexGuard<'static, State> { + STATE.get_or_init(Default::default).lock().unwrap() +} + +unsafe extern "system" fn exec_connect( + context: *mut ue::UObject, + stack: *mut ue::kismet::FFrame, + _result: *mut c_void, +) { + let stack = stack.as_mut().unwrap(); + + let url: FString = stack.arg(); + let url = url.to_string(); + + tracing::info!("connecting to {url}"); + + let handle = Handle(context); + + let state = get_state(); + state.rt_handle.spawn(async move { + let (socket, response) = tokio_tungstenite::connect_async(url) + .await + .expect("Can't connect"); + + tracing::info!("Connected to the server"); + tracing::info!("Response HTTP code: {}", response.status()); + tracing::info!("Response contains the following headers:"); + for (ref header, _value) in response.headers() { + tracing::info!("* {}", header); + } + + let (recv_tx, recv_rx) = mpsc::channel(10); + let (send_tx, mut send_rx) = mpsc::channel(10); + + // TODO handle cleaning up existing connection + get_state().handles.insert( + handle, + Connection { + tx: send_tx, + rx: recv_rx, + }, + ); + + let (mut write, mut read) = socket.split(); + + write.send("Hello WebSocket".into()).await.unwrap(); + + let a = tokio::task::spawn(async move { + while let Some(msg) = send_rx.recv().await { + write.send(msg.into()).await.unwrap(); + } + }); + + let b = tokio::task::spawn(async move { + while let Some(msg) = read.next().await { + let msg = match msg { + Ok(data) => Message { + type_: EventType::Message, + data: data.to_string(), + }, + Err(err) => Message { + type_: EventType::Error, + data: err.to_string(), + }, + }; + // TODO consider if sending can fail (because connection closed) + recv_tx.send(msg).await.unwrap(); + } + }); + + a.await.unwrap(); + b.await.unwrap(); + }); + + if !stack.code.is_null() { + stack.code = stack.code.add(1); + } +} +unsafe extern "system" fn exec_send( + context: *mut ue::UObject, + stack: *mut ue::kismet::FFrame, + _result: *mut c_void, +) { + let stack = stack.as_mut().unwrap(); + + let message: FString = stack.arg(); + let message = message.to_string(); + + tracing::info!("sending message {message}"); + + if let Some(connection) = get_state().handles.get_mut(&Handle(context)) { + connection.tx.try_send(message).unwrap(); + } else { + tracing::warn!("tried to send data but connection does not exist") + } + + if !stack.code.is_null() { + stack.code = stack.code.add(1); + } +} +unsafe extern "system" fn exec_get_event( + context: *mut ue::UObject, + stack: *mut ue::kismet::FFrame, + _result: *mut c_void, +) { + let stack = stack.as_mut().unwrap(); + + stack.arg::(); + let type_: &mut EventType = &mut *(stack.most_recent_property_address as *mut EventType); + *type_ = EventType::None; + + drop(stack.arg::()); + let data: &mut FString = &mut *(stack.most_recent_property_address as *mut FString); + *data = FString::new(); + + if let Some(connection) = get_state().handles.get_mut(&Handle(context)) { + if let Ok(msg) = connection.rx.try_recv() { + *type_ = msg.type_; + *data = msg.data.as_str().into(); + } + } else { + tracing::warn!("tried to recv data but connection does not exist") + } + + if !stack.code.is_null() { + stack.code = stack.code.add(1); + } +}