mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-05-05 19:39:08 -07:00
Merge remote-tracking branch 'origin/main'
This commit is contained in:
134
Cargo.lock
generated
134
Cargo.lock
generated
@@ -43,13 +43,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.75"
|
||||
version = "0.1.77"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdf6721fb0140e4f897002dd086c06f6c27775df19cfe1fccb21181a48fd2c98"
|
||||
checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.43",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -60,9 +60,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "202651474fe73c62d9e0a56c6133f7a0ff1dc1c8cf7a5b03381af2a26553ac9d"
|
||||
checksum = "d09dbe0e490df5da9d69b36dca48a76635288a82f92eca90024883a56202026d"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core",
|
||||
@@ -89,13 +89,14 @@ dependencies = [
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.4.1"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77cb22c689c44d4c07b0ab44ebc25d69d8ae601a2f28fb8d672d344178fa17aa"
|
||||
checksum = "e87c8503f93e6d144ee5690907ba22db7ba79ab001a932ab99034f0fe836b3df"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
@@ -109,6 +110,7 @@ dependencies = [
|
||||
"sync_wrapper",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -514,9 +516,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.58"
|
||||
version = "0.1.59"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20"
|
||||
checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
@@ -541,6 +543,25 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "include_dir"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18762faeff7122e89e0857b02f7ce6fcc0d101d5e9ad2ad7846cc01d61b7f19e"
|
||||
dependencies = [
|
||||
"include_dir_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "include_dir_macros"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b139284b5cf57ecfa712bcc66950bb635b31aff41c188e8a4cfc758eca374a3f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.1.0"
|
||||
@@ -553,13 +574,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.9"
|
||||
version = "0.4.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
|
||||
checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"rustix",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -625,9 +646,9 @@ checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.6.4"
|
||||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
|
||||
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
@@ -635,6 +656,16 @@ version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mime_guess"
|
||||
version = "2.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
|
||||
dependencies = [
|
||||
"mime",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.7.1"
|
||||
@@ -761,7 +792,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.43",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -794,18 +825,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.71"
|
||||
version = "1.0.75"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75cb1540fadbd5b8fbccc4dddad2734eba435053f725621c070711a14bb5f4b8"
|
||||
checksum = "907a61bd0f64c2f29cd1cf1dc34d05176426a3f504a78010f08416ddb7b13708"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.33"
|
||||
version = "1.0.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
|
||||
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -907,29 +938,29 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.193"
|
||||
version = "1.0.194"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89"
|
||||
checksum = "0b114498256798c94a0689e1a15fec6005dee8ac1f41de56404b67afc2a4b773"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.193"
|
||||
version = "1.0.194"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
|
||||
checksum = "a3385e45322e8f9931410f01b3031ec534c3947d0e94c18049af4d9f9907d4e0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.43",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.108"
|
||||
version = "1.0.111"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
|
||||
checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
@@ -938,9 +969,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_path_to_error"
|
||||
version = "0.1.14"
|
||||
version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335"
|
||||
checksum = "ebd154a240de39fdebcf5775d2675c204d7c13cf39a4c697be6493c8e734337c"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"serde",
|
||||
@@ -1027,9 +1058,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.43"
|
||||
version = "2.0.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee659fb5f3d355364e1f3e5bc10fb82068efbf824a1e9d1c9504244a6469ad53"
|
||||
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1059,22 +1090,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.52"
|
||||
version = "1.0.56"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83a48fd946b02c0a526b2e9481c8e2a17755e47039164a86c4070446e3a4614d"
|
||||
checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.52"
|
||||
version = "1.0.56"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7fbe9b594d6568a6a1443250a7e67d80b74e1e96f6d1715e1e21cc1888291d3"
|
||||
checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.43",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1104,7 +1135,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.43",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1214,6 +1245,15 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
|
||||
dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
@@ -1226,6 +1266,12 @@ version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
@@ -1253,7 +1299,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.43",
|
||||
"syn 2.0.48",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -1275,7 +1321,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.43",
|
||||
"syn 2.0.48",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@@ -1293,7 +1339,9 @@ dependencies = [
|
||||
"axum",
|
||||
"env_logger",
|
||||
"futures-core",
|
||||
"include_dir",
|
||||
"log",
|
||||
"mime_guess",
|
||||
"orca",
|
||||
"serde",
|
||||
"thiserror",
|
||||
@@ -1335,11 +1383,11 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.51.1"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64"
|
||||
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
|
||||
dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
"windows-targets 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1476,9 +1524,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.31"
|
||||
version = "0.5.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97a4882e6b134d6c28953a387571f1acdd3496830d5e36c5e3a1075580ea641c"
|
||||
checksum = "8434aeec7b290e8da5c3f0d628cb0eac6cabcb31d14bb74f779a08109a5914d6"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
15
make.sh
15
make.sh
@@ -1,11 +1,10 @@
|
||||
cargo build
|
||||
cargo build --release
|
||||
# Force a switch into the debug mode to enable ADB
|
||||
target/x86_64-unknown-linux-gnu/debug/serial AT
|
||||
adb push target/armv7-unknown-linux-gnueabihf/debug/rootshell /tmp/
|
||||
target/x86_64-unknown-linux-gnu/debug/serial "AT+SYSCMD=mv /tmp/rootshell /bin/rootshell"
|
||||
target/x86_64-unknown-linux-gnu/release/serial AT
|
||||
adb push target/armv7-unknown-linux-gnueabihf/release/rootshell /tmp/
|
||||
target/x86_64-unknown-linux-gnu/release/serial "AT+SYSCMD=mv /tmp/rootshell /bin/rootshell"
|
||||
sleep 1
|
||||
target/x86_64-unknown-linux-gnu/debug/serial "AT+SYSCMD=chown root /bin/rootshell"
|
||||
target/x86_64-unknown-linux-gnu/release/serial "AT+SYSCMD=chown root /bin/rootshell"
|
||||
sleep 1
|
||||
target/x86_64-unknown-linux-gnu/debug/serial "AT+SYSCMD=chmod 4755 /bin/rootshell"
|
||||
adb push target/armv7-unknown-linux-gnueabihf/debug/wavehunter /data/wavehunter/wavehunter
|
||||
adb push target/armv7-unknown-linux-gnueabihf/debug/wavehunter-reader /data/wavehunter/wavehunter-reader
|
||||
target/x86_64-unknown-linux-gnu/release/serial "AT+SYSCMD=chmod 4755 /bin/rootshell"
|
||||
adb push target/armv7-unknown-linux-gnueabihf/release/wavehunter /data/wavehunter/wavehunter
|
||||
|
||||
@@ -16,3 +16,5 @@ thiserror = "1.0.52"
|
||||
log = "0.4.20"
|
||||
env_logger = "0.10.1"
|
||||
tokio-util = { version = "0.7.10", features = ["io"] }
|
||||
include_dir = "0.7.3"
|
||||
mime_guess = "2.0.4"
|
||||
|
||||
@@ -1,24 +1,29 @@
|
||||
mod config;
|
||||
mod error;
|
||||
mod pcap;
|
||||
mod server;
|
||||
mod stats;
|
||||
|
||||
use crate::config::{parse_config, parse_args};
|
||||
use crate::server::{ServerState, serve_pcap, serve_qmdl};
|
||||
use crate::server::{ServerState, get_qmdl, serve_static};
|
||||
use crate::pcap::get_pcap;
|
||||
use crate::stats::{get_system_stats, get_diag_stats};
|
||||
use crate::error::WavehunterError;
|
||||
|
||||
use log::debug;
|
||||
use axum::response::Redirect;
|
||||
use orca::diag_device::DiagDevice;
|
||||
use orca::diag_reader::DiagReader;
|
||||
|
||||
use axum::routing::get;
|
||||
use axum::Router;
|
||||
use tokio::fs::File;
|
||||
use log::debug;
|
||||
use std::net::SocketAddr;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::task::JoinHandle;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn run_diag_read_thread(mut dev: DiagDevice, bytes_read_lock: Arc<RwLock<usize>>) -> tokio::task::JoinHandle<Result<(), WavehunterError>> {
|
||||
fn run_diag_read_thread(mut dev: DiagDevice, bytes_read_lock: Arc<RwLock<usize>>) -> JoinHandle<Result<(), WavehunterError>> {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
loop {
|
||||
// TODO: once we're actually doing analysis, we'll wanna use the messages
|
||||
@@ -45,8 +50,12 @@ async fn run_server(config: &config::Config, qmdl_bytes_written: Arc<RwLock<usiz
|
||||
});
|
||||
|
||||
let app = Router::new()
|
||||
.route("/output.pcap", get(serve_pcap))
|
||||
.route("/output.qmdl", get(serve_qmdl))
|
||||
.route("/api/pcap/latest.pcap", get(get_pcap))
|
||||
.route("/api/qmdl/latest.qmdl", get(get_qmdl))
|
||||
.route("/api/system-stats", get(get_system_stats))
|
||||
.route("/api/diag-stats", get(get_diag_stats))
|
||||
.route("/", get(|| async { Redirect::permanent("/index.html") }))
|
||||
.route("/*path", get(serve_static))
|
||||
.with_state(state);
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], config.port));
|
||||
let listener = TcpListener::bind(&addr).await.unwrap();
|
||||
|
||||
109
wavehunter/src/pcap.rs
Normal file
109
wavehunter/src/pcap.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use crate::ServerState;
|
||||
|
||||
use orca::gsmtap_parser::GsmtapParser;
|
||||
use orca::pcap::GsmtapPcapWriter;
|
||||
use orca::qmdl::{QmdlReader, QmdlReaderError};
|
||||
use orca::diag_reader::DiagReader;
|
||||
use axum::body::Body;
|
||||
use axum::http::header::CONTENT_TYPE;
|
||||
use axum::extract::State;
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{Response, IntoResponse};
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::{Poll, Context};
|
||||
use futures_core::Stream;
|
||||
use log::error;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
// Streams a pcap file chunk-by-chunk to the client by reading the QMDL data
|
||||
// written so far. This is done by spawning a blocking thread (a tokio thread
|
||||
// capable of handling blocking operations) which streams chunks of pcap data to
|
||||
// a channel that's piped to the client.
|
||||
pub async fn get_pcap(State(state): State<Arc<ServerState>>) -> Result<Response, (StatusCode, String)> {
|
||||
let qmdl_bytes_written = *state.qmdl_bytes_written.read().await;
|
||||
if qmdl_bytes_written == 0 {
|
||||
return Err((
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
"QMDL file is empty, try again in a bit!".to_string()
|
||||
));
|
||||
}
|
||||
|
||||
let (tx, rx) = mpsc::channel(1);
|
||||
let channel_reader = ChannelReader { rx };
|
||||
let channel_writer = ChannelWriter { tx };
|
||||
tokio::task::spawn_blocking(move || {
|
||||
// the QMDL reader should stop at the last successfully written data
|
||||
// chunk (qmdl_bytes_written)
|
||||
let qmdl_file = File::open(&state.qmdl_path).unwrap();
|
||||
let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(qmdl_bytes_written));
|
||||
|
||||
let mut gsmtap_parser = GsmtapParser::new();
|
||||
let mut pcap_writer = GsmtapPcapWriter::new(channel_writer).unwrap();
|
||||
pcap_writer.write_iface_header().unwrap();
|
||||
loop {
|
||||
match qmdl_reader.read_response() {
|
||||
Ok(messages) => {
|
||||
for maybe_msg in messages {
|
||||
match maybe_msg {
|
||||
Ok(msg) => {
|
||||
let maybe_gsmtap_msg = gsmtap_parser.recv_message(msg)
|
||||
.expect("error parsing gsmtap message");
|
||||
if let Some((timestamp, gsmtap_msg)) = maybe_gsmtap_msg {
|
||||
pcap_writer.write_gsmtap_message(gsmtap_msg, timestamp)
|
||||
.expect("error writing pcap packet");
|
||||
}
|
||||
},
|
||||
Err(e) => error!("error parsing message: {:?}", e),
|
||||
}
|
||||
}
|
||||
},
|
||||
// this is expected, and just means we've reached the end of the
|
||||
// safely written QMDL data
|
||||
Err(QmdlReaderError::MaxBytesReached(_)) => break,
|
||||
Err(e) => {
|
||||
error!("error reading qmdl file: {:?}", e);
|
||||
break;
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let headers = [(CONTENT_TYPE, "application/vnd.tcpdump.pcap")];
|
||||
let body = Body::from_stream(channel_reader);
|
||||
Ok((headers, body).into_response())
|
||||
}
|
||||
|
||||
struct ChannelWriter {
|
||||
tx: mpsc::Sender<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl Write for ChannelWriter {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
self.tx.blocking_send(buf.to_vec())
|
||||
.map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, "channel closed"))?;
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct ChannelReader {
|
||||
rx: mpsc::Receiver<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl Stream for ChannelReader {
|
||||
type Item = Result<Vec<u8>, String>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
match self.rx.poll_recv(cx) {
|
||||
Poll::Ready(Some(msg)) => Poll::Ready(Some(Ok(msg))),
|
||||
Poll::Ready(None) => Poll::Ready(None),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,117 +1,22 @@
|
||||
use orca::gsmtap_parser::GsmtapParser;
|
||||
use orca::pcap::GsmtapPcapWriter;
|
||||
use orca::qmdl::{QmdlReader, QmdlReaderError};
|
||||
use orca::diag_reader::DiagReader;
|
||||
|
||||
use axum::body::Body;
|
||||
use axum::http::header::CONTENT_TYPE;
|
||||
use axum::http::header::{CONTENT_TYPE, self};
|
||||
use axum::extract::State;
|
||||
use axum::http::StatusCode;
|
||||
use axum::http::{StatusCode, HeaderValue};
|
||||
use axum::response::{Response, IntoResponse};
|
||||
use axum::extract::Path;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::fs::File as AsyncFile;
|
||||
use tokio_util::io::ReaderStream;
|
||||
use std::task::{Poll, Context};
|
||||
use futures_core::Stream;
|
||||
use log::error;
|
||||
use tokio::sync::mpsc;
|
||||
use include_dir::{include_dir, Dir};
|
||||
|
||||
// Streams a pcap file chunk-by-chunk to the client by reading the QMDL data
|
||||
// written so far. This is done by spawning a blocking thread (a tokio thread
|
||||
// capable of handling blocking operations) which streams chunks of pcap data to
|
||||
// a channel that's piped to the client.
|
||||
pub async fn serve_pcap(State(state): State<Arc<ServerState>>) -> Result<Response, (StatusCode, String)> {
|
||||
let qmdl_bytes_written = *state.qmdl_bytes_written.read().await;
|
||||
if qmdl_bytes_written == 0 {
|
||||
return Err((
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
"QMDL file is empty, try again in a bit!".to_string()
|
||||
));
|
||||
}
|
||||
|
||||
let (tx, rx) = mpsc::channel(1);
|
||||
let channel_reader = ChannelReader { rx };
|
||||
let channel_writer = ChannelWriter { tx };
|
||||
tokio::task::spawn_blocking(move || {
|
||||
// the QMDL reader should stop at the last successfully written data
|
||||
// chunk (qmdl_bytes_written)
|
||||
let qmdl_file = File::open(&state.qmdl_path).unwrap();
|
||||
let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(qmdl_bytes_written));
|
||||
|
||||
let mut gsmtap_parser = GsmtapParser::new();
|
||||
let mut pcap_writer = GsmtapPcapWriter::new(channel_writer).unwrap();
|
||||
pcap_writer.write_iface_header().unwrap();
|
||||
loop {
|
||||
match qmdl_reader.read_response() {
|
||||
Ok(messages) => {
|
||||
for maybe_msg in messages {
|
||||
match maybe_msg {
|
||||
Ok(msg) => {
|
||||
let maybe_gsmtap_msg = gsmtap_parser.recv_message(msg)
|
||||
.expect("error parsing gsmtap message");
|
||||
if let Some((timestamp, gsmtap_msg)) = maybe_gsmtap_msg {
|
||||
pcap_writer.write_gsmtap_message(gsmtap_msg, timestamp)
|
||||
.expect("error writing pcap packet");
|
||||
}
|
||||
},
|
||||
Err(e) => error!("error parsing message: {:?}", e),
|
||||
}
|
||||
}
|
||||
},
|
||||
// this is expected, and just means we've reached the end of the
|
||||
// safely written QMDL data
|
||||
Err(QmdlReaderError::MaxBytesReached(_)) => break,
|
||||
Err(e) => {
|
||||
error!("error reading qmdl file: {:?}", e);
|
||||
break;
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let headers = [(CONTENT_TYPE, "application/vnd.tcpdump.pcap")];
|
||||
let body = Body::from_stream(channel_reader);
|
||||
Ok((headers, body).into_response())
|
||||
pub struct ServerState {
|
||||
pub qmdl_bytes_written: Arc<RwLock<usize>>,
|
||||
pub qmdl_path: String,
|
||||
}
|
||||
|
||||
struct ChannelWriter {
|
||||
tx: mpsc::Sender<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl Write for ChannelWriter {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
self.tx.blocking_send(buf.to_vec())
|
||||
.map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, "channel closed"))?;
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct ChannelReader {
|
||||
rx: mpsc::Receiver<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl Stream for ChannelReader {
|
||||
type Item = Result<Vec<u8>, String>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
match self.rx.poll_recv(cx) {
|
||||
Poll::Ready(Some(msg)) => Poll::Ready(Some(Ok(msg))),
|
||||
Poll::Ready(None) => Poll::Ready(None),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn serve_qmdl(State(state): State<Arc<ServerState>>) -> Result<Response, (StatusCode, String)> {
|
||||
pub async fn get_qmdl(State(state): State<Arc<ServerState>>) -> Result<Response, (StatusCode, String)> {
|
||||
let qmdl_file = AsyncFile::open(&state.qmdl_path).await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("error opening QMDL file: {}", e)))?;
|
||||
let qmdl_bytes_written = *state.qmdl_bytes_written.read().await;
|
||||
@@ -123,7 +28,25 @@ pub async fn serve_qmdl(State(state): State<Arc<ServerState>>) -> Result<Respons
|
||||
Ok((headers, body).into_response())
|
||||
}
|
||||
|
||||
pub struct ServerState {
|
||||
pub qmdl_bytes_written: Arc<RwLock<usize>>,
|
||||
pub qmdl_path: String,
|
||||
// Bundles the server's static files (html/css/js) into the binary for easy distribution
|
||||
static STATIC_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/static");
|
||||
|
||||
pub async fn serve_static(Path(path): Path<String>) -> impl IntoResponse {
|
||||
let path = path.trim_start_matches('/');
|
||||
let mime_type = mime_guess::from_path(path).first_or_text_plain();
|
||||
|
||||
match STATIC_DIR.get_file(path) {
|
||||
None => Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
Some(file) => Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_str(mime_type.as_ref()).unwrap(),
|
||||
)
|
||||
.body(Body::from(file.contents()))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
121
wavehunter/src/stats.rs
Normal file
121
wavehunter/src/stats.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
use crate::server::ServerState;
|
||||
|
||||
use axum::Json;
|
||||
use axum::extract::State;
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::IntoResponse;
|
||||
use std::sync::Arc;
|
||||
use log::error;
|
||||
use serde::Serialize;
|
||||
use tokio::process::Command;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct SystemStats {
|
||||
pub disk_stats: DiskStats,
|
||||
pub memory_stats: MemoryStats,
|
||||
}
|
||||
|
||||
impl SystemStats {
|
||||
pub async fn new(qmdl_path: &str) -> Result<Self, String> {
|
||||
Ok(Self {
|
||||
disk_stats: DiskStats::new(qmdl_path).await?,
|
||||
memory_stats: MemoryStats::new().await?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct DiskStats {
|
||||
partition: String,
|
||||
total_size: String,
|
||||
used_size: String,
|
||||
available_size: String,
|
||||
used_percent: String,
|
||||
mounted_on: String,
|
||||
}
|
||||
|
||||
impl DiskStats {
|
||||
// runs "df -h <qmdl_path>" to get storage statistics for the partition containing
|
||||
// the QMDL file
|
||||
pub async fn new(qmdl_path: &str) -> Result<Self, String> {
|
||||
let mut df_cmd = Command::new("df");
|
||||
df_cmd.arg("-h");
|
||||
df_cmd.arg(qmdl_path);
|
||||
let stdout = get_cmd_output(df_cmd).await?;
|
||||
let mut parts = stdout.split_whitespace().skip(7).to_owned();
|
||||
Ok(Self {
|
||||
partition: parts.next().ok_or("error parsing df output")?.to_string(),
|
||||
total_size: parts.next().ok_or("error parsing df output")?.to_string(),
|
||||
used_size: parts.next().ok_or("error parsing df output")?.to_string(),
|
||||
available_size: parts.next().ok_or("error parsing df output")?.to_string(),
|
||||
used_percent: parts.next().ok_or("error parsing df output")?.to_string(),
|
||||
mounted_on: parts.next().ok_or("error parsing df output")?.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct MemoryStats {
|
||||
total: String,
|
||||
used: String,
|
||||
free: String,
|
||||
}
|
||||
|
||||
// runs the given command and returns its stdout as a string
|
||||
async fn get_cmd_output(mut cmd: Command) -> Result<String, String> {
|
||||
let cmd_str = format!("{:?}", &cmd);
|
||||
let output = cmd.output().await
|
||||
.map_err(|e| format!("error running command {}: {}", &cmd_str, e))?;
|
||||
if !output.status.success() {
|
||||
return Err(format!("command {} failed with exit code {}", &cmd_str, output.status.code().unwrap()));
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
}
|
||||
|
||||
impl MemoryStats {
|
||||
// runs "free -k" and parses the output to retrieve memory stats
|
||||
pub async fn new() -> Result<Self, String> {
|
||||
let mut free_cmd = Command::new("free");
|
||||
free_cmd.arg("-k");
|
||||
let stdout = get_cmd_output(free_cmd).await?;
|
||||
let mut numbers = stdout.split_whitespace()
|
||||
.flat_map(|part| part.parse::<usize>());
|
||||
Ok(Self {
|
||||
total: humanize_kb(numbers.next().ok_or("error parsing free output")?),
|
||||
used: humanize_kb(numbers.next().ok_or("error parsing free output")?),
|
||||
free: humanize_kb(numbers.next().ok_or("error parsing free output")?),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// turns a number of kilobytes (like 28293) into a human-readable string (like "28.3M")
|
||||
fn humanize_kb(kb: usize) -> String {
|
||||
if kb < 1000{
|
||||
return format!("{}K", kb);
|
||||
}
|
||||
format!("{:.1}M", kb as f64 / 1024.0)
|
||||
}
|
||||
|
||||
pub async fn get_system_stats(State(state): State<Arc<ServerState>>) -> Result<Json<SystemStats>, (StatusCode, String)> {
|
||||
match SystemStats::new(&state.qmdl_path).await {
|
||||
Ok(stats) => Ok(Json(stats)),
|
||||
Err(err) => {
|
||||
error!("error getting system stats: {}", err);
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"error getting system stats".to_string()
|
||||
));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct DiagStats {
|
||||
bytes_written: usize,
|
||||
}
|
||||
|
||||
pub async fn get_diag_stats(State(state): State<Arc<ServerState>>) -> impl IntoResponse {
|
||||
Json(DiagStats {
|
||||
bytes_written: *state.qmdl_bytes_written.read().await,
|
||||
})
|
||||
}
|
||||
0
wavehunter/static/css/style.css
Normal file
0
wavehunter/static/css/style.css
Normal file
22
wavehunter/static/index.html
Normal file
22
wavehunter/static/index.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>ORCA</title>
|
||||
<link rel="stylesheet" type="text/css" href="css/style.css">
|
||||
<script src="js/main.js"></script>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
setInterval(function() {
|
||||
populateDivs();
|
||||
}, 1000);
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="downloads">
|
||||
<span><a href="/api/pcap/latest.pcap">Latest PCAP</a></span>
|
||||
<span><a href="/api/qmdl/latest.qmdl">Latest QMDL</a></span>
|
||||
</div>
|
||||
<pre id="system-stats">Loading...</pre>
|
||||
<pre id="diag-stats">Loading...</pre>
|
||||
</body>
|
||||
</html>
|
||||
24
wavehunter/static/js/main.js
Normal file
24
wavehunter/static/js/main.js
Normal file
@@ -0,0 +1,24 @@
|
||||
async function populateDivs() {
|
||||
const systemStats = await getSystemStats();
|
||||
const diagStats = await getDiagStats();
|
||||
|
||||
const systemStatsDiv = document.getElementById('system-stats');
|
||||
const diagStatsDiv = document.getElementById('diag-stats');
|
||||
|
||||
systemStatsDiv.innerHTML = JSON.stringify(systemStats, null, 2);
|
||||
diagStatsDiv.innerHTML = JSON.stringify(diagStats, null, 2);
|
||||
}
|
||||
|
||||
async function getSystemStats() {
|
||||
return await getJson('/api/system-stats');
|
||||
}
|
||||
|
||||
async function getDiagStats() {
|
||||
return await getJson('/api/diag-stats');
|
||||
}
|
||||
|
||||
async function getJson(url) {
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
Reference in New Issue
Block a user