Big update
This repository is no longer meant for just radarflow, thus it will be renamed. I have split the SDK from radarflow, allowing for simpler use with new projects. Other than that, radarflow is functionally the same. - Fixed bug in radarflow where the entity loop didn't include the last entity.
This commit is contained in:
40
radarflow/Cargo.toml
Normal file
40
radarflow/Cargo.toml
Normal file
@@ -0,0 +1,40 @@
|
||||
[package]
|
||||
name = "radarflow"
|
||||
version = "0.2.0"
|
||||
authors = ["Janek S <development@superyu.xyz>"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
csflow = { path = "../csflow" }
|
||||
|
||||
# cli
|
||||
clap = { version = "4.3.19", features = ["derive", "string"] }
|
||||
|
||||
# tokio
|
||||
tokio = { version = "1.29.1", features = ["full"] }
|
||||
tokio-timerfd = "0.2.0"
|
||||
|
||||
# serde
|
||||
serde = { version = "1.0.181", features = ["derive"] }
|
||||
serde_json = "1.0.104"
|
||||
|
||||
# networking
|
||||
axum = { version = "0.6.20", features = ["ws"] }
|
||||
tower-http = { version = "0.4.3", features = ["fs"] }
|
||||
tower = "0.4.13"
|
||||
local-ip-address = "0.5.4"
|
||||
|
||||
# other
|
||||
|
||||
|
||||
# error handling
|
||||
anyhow = "1.0.77"
|
||||
|
||||
# logging
|
||||
log = "0.4.19"
|
||||
simple_logger = "4.2.0"
|
||||
|
||||
[build-dependencies]
|
||||
vergen = { version = "8.0.0", features = ["build", "cargo", "git", "gitcl", "rustc", "si"] }
|
||||
44
radarflow/README.md
Normal file
44
radarflow/README.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# radarflow
|
||||
A Web radar for CS2 using [memflow](https://github.com/memflow/memflow)
|
||||
|
||||
## How can I run this?
|
||||
There is two ways to run this, first way is using a KVM/QEMU setup to target a running VM to read memory out of it. The second way is using pcileech hardware, like a PCIe Screamer.
|
||||
|
||||
> [!NOTE]
|
||||
> The pcileech method is untested. However, I have ordered hardware, and will test soon.
|
||||
|
||||
### The KVM/QEMU method
|
||||
First, you need to set up a virtual machine on linux using qemu.
|
||||
How to set up a VM on linux is way out of scope for this. You can find plenty of information online on how to do it.
|
||||
|
||||
Before you begin, install the necessary memflow plugins using memflowup from the *stable 0.2.0 channel!*
|
||||
|
||||
Clone the repo on your vm host:
|
||||
`git clone https://github.com/superyu1337/radarflow2.git`
|
||||
|
||||
Run radarflow:
|
||||
`cargo run --release`
|
||||
|
||||
For an overview of CLI commands, run this:
|
||||
`cargo run --release -- --help`
|
||||
|
||||
### The pcileech method
|
||||
|
||||
> [!WARNING]
|
||||
> The pcileech method is untested.
|
||||
|
||||
Install your pcileech hardware in your target pc. On your attacking pc, install the necessary memflow plugins using memflowup from the *stable 0.2.0 channel!*
|
||||
|
||||
Clone the repo on your attacking pc:
|
||||
`git clone https://github.com/superyu1337/radarflow2.git`
|
||||
|
||||
Run radarflow:
|
||||
`cargo run --release`
|
||||
|
||||
For an overview of CLI commands, run this:
|
||||
`cargo run --release -- --help`
|
||||
|
||||
## Detection Status
|
||||
VAC: ✅ (Undetected)
|
||||
FaceIt: ❓ (Unknown, could work with proper spoofing on pcileech method)
|
||||
ESEA: ❓ (Unknown, could work with proper spoofing on pcileech method)
|
||||
14
radarflow/build.rs
Normal file
14
radarflow/build.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use std::error::Error;
|
||||
use vergen::EmitBuilder;
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
EmitBuilder::builder()
|
||||
.git_sha(true)
|
||||
.git_commit_date()
|
||||
.cargo_debug()
|
||||
.cargo_target_triple()
|
||||
.rustc_semver()
|
||||
.rustc_llvm_version()
|
||||
.emit()?;
|
||||
Ok(())
|
||||
}
|
||||
114
radarflow/src/cli.rs
Normal file
114
radarflow/src/cli.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::{Parser, ValueEnum};
|
||||
use csflow::{Connector, memflow::Inventory};
|
||||
|
||||
const PORT_RANGE: std::ops::RangeInclusive<usize> = 8000..=65535;
|
||||
const POLL_RANGE: std::ops::RangeInclusive<usize> = 1..=1000;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(author, version = version(), about, long_about = None)]
|
||||
pub struct Cli {
|
||||
/// Specifies the connector type for DMA
|
||||
#[clap(value_enum, short, long, ignore_case = true, default_value_t = Connector::Qemu)]
|
||||
pub connector: Connector,
|
||||
|
||||
/// Name of the Pcileech device
|
||||
#[clap(long, default_value_t = String::from("FPGA"))]
|
||||
pub pcileech_device: String,
|
||||
|
||||
/// Port number for the Webserver to run on
|
||||
#[arg(short, long, default_value_t = 8000, value_parser = port_in_range)]
|
||||
pub port: u16,
|
||||
|
||||
/// Path to the directory served by the Webserver
|
||||
#[arg(short, long, default_value = "./webradar", value_parser = valid_path)]
|
||||
pub web_path: PathBuf,
|
||||
|
||||
/// Polling frequency in times per second for the DMA thread
|
||||
#[arg(short = 'r', long, default_value_t = 60, value_parser = poll_in_range)]
|
||||
pub poll_rate: u16,
|
||||
|
||||
/// Verbosity level for logging to the console
|
||||
#[arg(value_enum, long, short, ignore_case = true, default_value_t = Loglevel::Warn)]
|
||||
pub loglevel: Loglevel,
|
||||
}
|
||||
|
||||
fn version() -> String {
|
||||
let pkg_ver = env!("CARGO_PKG_VERSION");
|
||||
let git_hash = option_env!("VERGEN_GIT_SHA").unwrap_or("unknown");
|
||||
let commit_date = option_env!("VERGEN_GIT_COMMIT_DATE").unwrap_or("unknown");
|
||||
let avail_cons = {
|
||||
let inventory = Inventory::scan();
|
||||
inventory.available_connectors().join(", ")
|
||||
};
|
||||
|
||||
format!(" {pkg_ver} (rev {git_hash})\nCommit Date: {commit_date}\nAvailable Connectors: {avail_cons}")
|
||||
}
|
||||
|
||||
fn port_in_range(s: &str) -> Result<u16, String> {
|
||||
let port: usize = s
|
||||
.parse()
|
||||
.map_err(|_| format!("`{s}` isn't a port number"))?;
|
||||
if PORT_RANGE.contains(&port) {
|
||||
Ok(port as u16)
|
||||
} else {
|
||||
Err(format!(
|
||||
"port not in range {}-{}",
|
||||
PORT_RANGE.start(),
|
||||
PORT_RANGE.end()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn valid_path(s: &str) -> Result<PathBuf, String> {
|
||||
let path = PathBuf::from(s);
|
||||
|
||||
if !path.exists() {
|
||||
return Err("Path does not exist".to_string())
|
||||
}
|
||||
|
||||
if !path.is_dir() {
|
||||
return Err("Path is not a directory".to_string())
|
||||
}
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn poll_in_range(s: &str) -> Result<u16, String> {
|
||||
let port: usize = s
|
||||
.parse()
|
||||
.map_err(|_| format!("`{s}` isn't a valid number"))?;
|
||||
if POLL_RANGE.contains(&port) {
|
||||
Ok(port as u16)
|
||||
} else {
|
||||
Err(format!(
|
||||
"not in range {}-{}",
|
||||
POLL_RANGE.start(),
|
||||
POLL_RANGE.end()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper because log::LevelFilter doesn't implement ValueEnum
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Default)]
|
||||
pub enum Loglevel {
|
||||
Error,
|
||||
#[default]
|
||||
Warn,
|
||||
Info,
|
||||
Debug,
|
||||
Trace,
|
||||
}
|
||||
|
||||
impl From<Loglevel> for log::LevelFilter {
|
||||
fn from(val: Loglevel) -> Self {
|
||||
match val {
|
||||
Loglevel::Error => log::LevelFilter::Error,
|
||||
Loglevel::Warn => log::LevelFilter::Warn,
|
||||
Loglevel::Info => log::LevelFilter::Info,
|
||||
Loglevel::Debug => log::LevelFilter::Debug,
|
||||
Loglevel::Trace => log::LevelFilter::Trace,
|
||||
}
|
||||
}
|
||||
}
|
||||
68
radarflow/src/comms.rs
Normal file
68
radarflow/src/comms.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use csflow::{enums::PlayerType, structs::Vec3};
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PlayerData {
|
||||
pos: Vec3,
|
||||
yaw: f32,
|
||||
#[serde(rename = "playerType")]
|
||||
player_type: PlayerType,
|
||||
|
||||
#[serde(rename = "hasBomb")]
|
||||
has_bomb: bool
|
||||
}
|
||||
|
||||
impl PlayerData {
|
||||
pub fn new(pos: Vec3, yaw: f32, player_type: PlayerType, has_bomb: bool) -> PlayerData {
|
||||
PlayerData { pos, yaw, player_type, has_bomb }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BombData {
|
||||
pos: Vec3,
|
||||
#[serde(rename = "isPlanted")]
|
||||
is_planted: bool
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl BombData {
|
||||
pub fn new(pos: Vec3, is_planted: bool) -> BombData {
|
||||
BombData { pos, is_planted }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum EntityData {
|
||||
Player(PlayerData),
|
||||
Bomb(BombData)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RadarData {
|
||||
ingame: bool,
|
||||
|
||||
#[serde(rename = "mapName")]
|
||||
map_name: String,
|
||||
|
||||
#[serde(rename(serialize = "entityData"))]
|
||||
player_data: Vec<EntityData>,
|
||||
|
||||
//#[serde(rename(serialize = "localYaw"))]
|
||||
//local_yaw: f32,
|
||||
}
|
||||
|
||||
impl RadarData {
|
||||
pub fn new(ingame: bool, map_name: String, player_data: Vec<EntityData>) -> RadarData {
|
||||
RadarData { ingame, map_name, player_data }
|
||||
}
|
||||
|
||||
/// Returns empty RadarData, it's also the same data that gets sent to clients when not ingame
|
||||
pub fn empty() -> RadarData {
|
||||
RadarData {
|
||||
ingame: false,
|
||||
map_name: String::new(),
|
||||
player_data: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
85
radarflow/src/dma/cache.rs
Normal file
85
radarflow/src/dma/cache.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use csflow::{memflow::Address, enums::PlayerType};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum CachedEntity {
|
||||
Bomb {ptr: Address},
|
||||
Player {ptr: Address, player_type: PlayerType},
|
||||
}
|
||||
|
||||
pub struct Cache {
|
||||
timestamp: std::time::Instant,
|
||||
entity_cache: Vec<CachedEntity>,
|
||||
map_name: String,
|
||||
entity_list: Address,
|
||||
}
|
||||
|
||||
impl Cache {
|
||||
pub fn is_valid(&self) -> bool {
|
||||
if self.timestamp.elapsed() > std::time::Duration::from_millis(250) {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn new_invalid() -> Cache {
|
||||
Cache {
|
||||
timestamp: std::time::Instant::now().checked_sub(std::time::Duration::from_millis(500)).unwrap(),
|
||||
entity_cache: Vec::new(),
|
||||
map_name: String::new(),
|
||||
entity_list: Address::null(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn entity_cache(&mut self) -> Vec<CachedEntity> {
|
||||
self.entity_cache.clone()
|
||||
}
|
||||
|
||||
pub fn map_name(&self) -> String {
|
||||
self.map_name.clone()
|
||||
}
|
||||
|
||||
pub fn entity_list(&self) -> Address {
|
||||
self.entity_list
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CacheBuilder {
|
||||
entity_cache: Option<Vec<CachedEntity>>,
|
||||
map_name: Option<String>,
|
||||
entity_list: Option<Address>
|
||||
}
|
||||
|
||||
impl CacheBuilder {
|
||||
pub fn new() -> CacheBuilder {
|
||||
CacheBuilder {
|
||||
entity_cache: None,
|
||||
map_name: None,
|
||||
entity_list: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn entity_cache(mut self, entity_cache: Vec<CachedEntity>) -> CacheBuilder {
|
||||
self.entity_cache = Some(entity_cache);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn map_name(mut self, map_name: String) -> CacheBuilder {
|
||||
self.map_name = Some(map_name);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn entity_list(mut self, entity_list: Address) -> CacheBuilder {
|
||||
self.entity_list = Some(entity_list);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> anyhow::Result<Cache> {
|
||||
Ok(Cache {
|
||||
timestamp: std::time::Instant::now(),
|
||||
entity_cache: self.entity_cache.ok_or(anyhow::anyhow!("entity_cache not set on builder"))?,
|
||||
map_name: self.map_name.ok_or(anyhow::anyhow!("map_name not set on builder"))?,
|
||||
entity_list: self.entity_list.ok_or(anyhow::anyhow!("entity_list not set on builder"))?,
|
||||
})
|
||||
}
|
||||
}
|
||||
202
radarflow/src/dma/mod.rs
Normal file
202
radarflow/src/dma/mod.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use csflow::{CheatCtx, Connector, memflow::Process, traits::{MemoryClass, BaseEntity}, enums::PlayerType, structs::{CBaseEntity, CPlayerController}};
|
||||
use tokio::{sync::RwLock, time::{Duration, Instant}};
|
||||
|
||||
use crate::{comms::{RadarData, EntityData, BombData, PlayerData}, dma::cache::CacheBuilder};
|
||||
|
||||
use self::cache::Cache;
|
||||
|
||||
mod cache;
|
||||
|
||||
const SECOND_AS_NANO: u64 = 1000*1000*1000;
|
||||
static ONCE: std::sync::Once = std::sync::Once::new();
|
||||
|
||||
pub async fn run(connector: Connector, pcileech_device: String, poll_rate: u16, data_lock: Arc<RwLock<RadarData>>) -> anyhow::Result<()> {
|
||||
let mut ctx = CheatCtx::setup(connector, pcileech_device)?;
|
||||
|
||||
println!("---------------------------------------------------");
|
||||
println!("Found cs2.exe at {:X}", ctx.process.info().address);
|
||||
println!("Found engine module at cs2.exe+{:X}", ctx.engine_module.base);
|
||||
println!("Found client module at cs2.exe+{:X}", ctx.client_module.base);
|
||||
println!("---------------------------------------------------");
|
||||
|
||||
// Avoid printing warnings and other stuff before the initial prints are complete
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
|
||||
// For poll rate timing
|
||||
let should_time = poll_rate != 0;
|
||||
|
||||
let target_interval = Duration::from_nanos(SECOND_AS_NANO / poll_rate as u64);
|
||||
let mut last_iteration_time = Instant::now();
|
||||
let mut missmatch_count = 0;
|
||||
|
||||
let mut cache = Cache::new_invalid();
|
||||
|
||||
loop {
|
||||
if ctx.process.state().is_dead() {
|
||||
break;
|
||||
}
|
||||
|
||||
if !cache.is_valid() {
|
||||
let mut cached_entities = Vec::new();
|
||||
|
||||
let globals = ctx.get_globals()?;
|
||||
let highest_index = ctx.highest_entity_index()?;
|
||||
let map_name = ctx.map_name(globals)?;
|
||||
let entity_list = ctx.get_entity_list()?;
|
||||
|
||||
let local = ctx.get_local()?;
|
||||
|
||||
if local.get_pawn(&mut ctx, entity_list)?.is_some() {
|
||||
cached_entities.push(cache::CachedEntity::Player {
|
||||
ptr: local.ptr(),
|
||||
player_type: PlayerType::Local
|
||||
});
|
||||
|
||||
for idx in 1..=highest_index {
|
||||
if let Some(entity) = CBaseEntity::from_index(&mut ctx, entity_list, idx)? {
|
||||
|
||||
let class_name = entity.class_name(&mut ctx)?;
|
||||
|
||||
match class_name.as_str() {
|
||||
"weapon_c4" => {
|
||||
cached_entities.push(cache::CachedEntity::Bomb {
|
||||
ptr: entity.ptr()
|
||||
})
|
||||
},
|
||||
"cs_player_controller" => {
|
||||
let controller = entity.to_player_controller();
|
||||
|
||||
let player_type = {
|
||||
match controller.get_player_type(&mut ctx, &local)? {
|
||||
Some(t) => {
|
||||
if t == PlayerType::Spectator { continue } else { t }
|
||||
},
|
||||
None => { continue },
|
||||
}
|
||||
};
|
||||
|
||||
cached_entities.push(cache::CachedEntity::Player {
|
||||
ptr: entity.ptr(),
|
||||
player_type,
|
||||
})
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cache = CacheBuilder::new()
|
||||
.entity_cache(cached_entities)
|
||||
.entity_list(entity_list)
|
||||
.map_name(map_name)
|
||||
.build()?;
|
||||
|
||||
log::debug!("Rebuilt cache.");
|
||||
}
|
||||
|
||||
if ctx.network_is_ingame()? {
|
||||
let mut radar_data = Vec::with_capacity(64);
|
||||
|
||||
if ctx.is_bomb_planted()? {
|
||||
let bomb = ctx.get_plantedc4()?;
|
||||
let bomb_pos = bomb.pos(&mut ctx)?;
|
||||
radar_data.push(
|
||||
EntityData::Bomb(BombData::new(
|
||||
bomb_pos,
|
||||
true
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
for cached_data in cache.entity_cache() {
|
||||
match cached_data {
|
||||
cache::CachedEntity::Bomb { ptr } => {
|
||||
if ctx.is_bomb_dropped()? {
|
||||
let bomb_entity = CBaseEntity::new(ptr);
|
||||
let pos = bomb_entity.pos(&mut ctx)?;
|
||||
|
||||
radar_data.push(
|
||||
EntityData::Bomb(
|
||||
BombData::new(
|
||||
pos,
|
||||
false
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
cache::CachedEntity::Player { ptr, player_type } => {
|
||||
let controller = CPlayerController::new(ptr);
|
||||
if let Some(pawn) = controller.get_pawn(&mut ctx, cache.entity_list())? {
|
||||
if pawn.is_alive(&mut ctx)? {
|
||||
let pos = pawn.pos(&mut ctx)?;
|
||||
let yaw = pawn.angles(&mut ctx)?.y;
|
||||
let has_bomb = pawn.has_c4(&mut ctx, cache.entity_list())?;
|
||||
|
||||
radar_data.push(
|
||||
EntityData::Player(
|
||||
PlayerData::new(
|
||||
pos,
|
||||
yaw,
|
||||
player_type,
|
||||
has_bomb
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let mut data = data_lock.write().await;
|
||||
if cache.map_name() == "<empty>" || cache.map_name().is_empty() {
|
||||
*data = RadarData::empty()
|
||||
} else {
|
||||
*data = RadarData::new(true, cache.map_name(), radar_data)
|
||||
}
|
||||
} else {
|
||||
let mut data = data_lock.write().await;
|
||||
*data = RadarData::empty()
|
||||
}
|
||||
|
||||
if should_time {
|
||||
let elapsed = last_iteration_time.elapsed();
|
||||
|
||||
let remaining = match target_interval.checked_sub(elapsed) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
if missmatch_count >= 25 {
|
||||
ONCE.call_once(|| {
|
||||
log::warn!("Remaining time till target interval was negative more than 25 times");
|
||||
log::warn!("You should decrease your poll rate.");
|
||||
log::warn!("elapsed: {}ns", elapsed.as_nanos());
|
||||
log::warn!("target: {}ns", target_interval.as_nanos());
|
||||
});
|
||||
} else {
|
||||
missmatch_count += 1;
|
||||
}
|
||||
Duration::from_nanos(0)
|
||||
},
|
||||
};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
tokio_timerfd::sleep(remaining).await?;
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
tokio::time::sleep(remaining).await;
|
||||
|
||||
log::info!("poll rate: {:.2}Hz", SECOND_AS_NANO as f64 / last_iteration_time.elapsed().as_nanos() as f64);
|
||||
log::trace!("elapsed: {}ns", elapsed.as_nanos());
|
||||
log::trace!("target: {}ns", target_interval.as_nanos());
|
||||
log::trace!("missmatch count: {}", missmatch_count);
|
||||
|
||||
last_iteration_time = Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
57
radarflow/src/main.rs
Normal file
57
radarflow/src/main.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use cli::Cli;
|
||||
use comms::RadarData;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
mod comms;
|
||||
mod cli;
|
||||
|
||||
mod dma;
|
||||
mod websocket;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
simple_logger::SimpleLogger::new()
|
||||
.with_level(cli.loglevel.into())
|
||||
.init()
|
||||
.expect("Initializing logger");
|
||||
|
||||
let rwlock = Arc::new(
|
||||
RwLock::new(
|
||||
RadarData::empty()
|
||||
)
|
||||
);
|
||||
|
||||
let rwlock_clone = rwlock.clone();
|
||||
let dma_handle = tokio::spawn(async move {
|
||||
if let Err(err) = dma::run(cli.connector, cli.pcileech_device, cli.poll_rate, rwlock_clone).await {
|
||||
log::error!("Error in dma thread: [{}]", err.to_string());
|
||||
}
|
||||
});
|
||||
|
||||
tokio::spawn(async move {
|
||||
let future = websocket::run(cli.web_path, cli.port, rwlock);
|
||||
|
||||
if let Ok(my_local_ip) = local_ip_address::local_ip() {
|
||||
let address = format!("http://{}:{}", my_local_ip, cli.port);
|
||||
println!("Launched webserver at {}", address);
|
||||
} else {
|
||||
let address = format!("http://0.0.0.0:{}", cli.port);
|
||||
println!("launched webserver at! {}", address);
|
||||
}
|
||||
|
||||
if let Err(err) = future.await {
|
||||
log::error!("Error in websocket server: [{}]", err.to_string());
|
||||
}
|
||||
});
|
||||
|
||||
if let Err(err) = dma_handle.await {
|
||||
log::error!("Error when waiting for dma thread: {}", err.to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
66
radarflow/src/websocket.rs
Normal file
66
radarflow/src/websocket.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use std::{sync::Arc, path::PathBuf};
|
||||
|
||||
use axum::{
|
||||
extract::{ws::{WebSocketUpgrade, WebSocket, Message}, State},
|
||||
response::Response,
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
|
||||
use tokio::sync::RwLock;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
use crate::comms::RadarData;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
data_lock: Arc<RwLock<RadarData>>
|
||||
}
|
||||
|
||||
async fn ws_handler(ws: WebSocketUpgrade, State(state): State<AppState>) -> Response {
|
||||
ws.on_upgrade(|socket| handle_socket(socket, state))
|
||||
}
|
||||
|
||||
async fn handle_socket(mut socket: WebSocket, state: AppState) {
|
||||
while let Some(msg) = socket.recv().await {
|
||||
if let Ok(msg) = msg {
|
||||
if msg == Message::Text("requestInfo".to_string()) {
|
||||
let data = state.data_lock.read().await;
|
||||
|
||||
let str = {
|
||||
match serde_json::to_string(&*data) {
|
||||
Ok(json) => json,
|
||||
Err(e) => {
|
||||
log::error!("Could not serialize data into json: {}", e.to_string());
|
||||
log::error!("Sending \"error\" instead");
|
||||
"error".to_string()
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
if socket.send(Message::Text(str)).await.is_err() {
|
||||
// client disconnected
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// client disconnected
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(path: PathBuf, port: u16, data_lock: Arc<RwLock<RadarData>>) -> anyhow::Result<()> {
|
||||
let app = Router::new()
|
||||
.nest_service("/", ServeDir::new(path))
|
||||
.route("/ws", get(ws_handler))
|
||||
.with_state(AppState { data_lock });
|
||||
|
||||
let address = format!("0.0.0.0:{}", port);
|
||||
|
||||
axum::Server::bind(&address.parse()?)
|
||||
.serve(app.into_make_service())
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user