mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-05-31 02:03:35 -07:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a23df84848 | |||
| 4e862841b3 | |||
| 2cc8404b13 | |||
| 35ae2962f2 | |||
| 1134361cca | |||
| bec680f93d | |||
| 968af93b69 | |||
| ee75326912 | |||
| 3b9a001e88 | |||
| 78d33b2cff | |||
| 6c237e884c | |||
| f3e4091e1d | |||
| 16f705f29c | |||
| a6fce6d568 | |||
| fcac6fdf16 | |||
| df84faa1f9 | |||
| c59fb7c013 | |||
| ca4f49b15f | |||
| 861aaedd47 |
@@ -8,16 +8,16 @@ env:
|
|||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_serial:
|
build_serial_and_check:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
platform:
|
platform:
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
build_name: serial
|
serial_build_name: serial
|
||||||
- os: windows-latest
|
check_build_name: rayhunter-check
|
||||||
build_name: serial.exe
|
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
build_name: serial
|
serial_build_name: serial
|
||||||
|
check_build_name: rayhunter-check
|
||||||
runs-on: ${{ matrix.platform.os }}
|
runs-on: ${{ matrix.platform.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -26,7 +26,15 @@ jobs:
|
|||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: serial-${{ matrix.platform.os }}
|
name: serial-${{ matrix.platform.os }}
|
||||||
path: ./target/release/${{ matrix.platform.build_name }}
|
path: ./target/release/${{ matrix.platform.serial_build_name }}
|
||||||
|
if-no-files-found: error
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Build check
|
||||||
|
run: cargo build --bin rayhunter-check --release
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: rayhunter-check-${{ matrix.platform.os }}
|
||||||
|
path: ./target/release/${{ matrix.platform.check_build_name }}
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
build_rootshell_and_rayhunter:
|
build_rootshell_and_rayhunter:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -56,14 +64,14 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
build_release_zip:
|
build_release_zip:
|
||||||
needs:
|
needs:
|
||||||
- build_serial
|
- build_serial_and_check
|
||||||
- build_rootshell_and_rayhunter
|
- build_rootshell_and_rayhunter
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
- name: Fix executable permissions on binaries
|
- name: Fix executable permissions on binaries
|
||||||
run: chmod +x serial-*/serial rayhunter-daemon/rayhunter-daemon
|
run: chmod +x serial-*/serial rayhunter-check-*/rayhunter-check rayhunter-daemon/rayhunter-daemon
|
||||||
- name: Setup release directory
|
- name: Setup release directory
|
||||||
run: mv rayhunter-daemon/rayhunter-daemon rootshell/rootshell serial-* dist
|
run: mv rayhunter-daemon/rayhunter-daemon rootshell/rootshell serial-* dist
|
||||||
- name: Archive release directory
|
- name: Archive release directory
|
||||||
|
|||||||
@@ -35,10 +35,11 @@ linux/qualcom devices but this is the only one we have tested on. Buy the orbic
|
|||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
1. Install the Android Debug Bridge (ADB) on your computer (don't worry about instructions for installing it on a phone/device yet). You can find instructions for doing so on your platform [here](https://www.xda-developers.com/install-adb-windows-macos-linux/#how-to-set-up-adb-on-your-computer).
|
*NOTE: We don't currently support automated installs on windows, you will have to follow the manual install instructions below*
|
||||||
2. Download the latest [rayhunter release bundle](https://github.com/EFForg/rayhunter/releases) and extract it (on Windows use 7zip).
|
|
||||||
3. Run the install script inside the bundle corresponding to your platform (`install-linux.sh`, `install-mac.sh`).
|
1. Download the latest [rayhunter release bundle](https://github.com/EFForg/rayhunter/releases) and extract it.
|
||||||
4. Once finished, rayhunter should be running! You can verify this by visiting the web UI as described below.
|
2. Run the install script inside the bundle corresponding to your platform (`install-linux.sh`, `install-mac.sh`).
|
||||||
|
3. Once finished, rayhunter should be running! You can verify this by visiting the web UI as described below.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -46,10 +47,14 @@ Once installed, rayhunter will run automatically whenever your Orbic device is r
|
|||||||
|
|
||||||
1. Over wifi: Connect your phone/laptop to the Orbic's wifi network and visit `http://192.168.1.1:8080` (click past your browser warning you about the connection not being secure, rayhunter doesn't have HTTPS yet!)
|
1. Over wifi: Connect your phone/laptop to the Orbic's wifi network and visit `http://192.168.1.1:8080` (click past your browser warning you about the connection not being secure, rayhunter doesn't have HTTPS yet!)
|
||||||
* Note that you'll need the Orbic's wifi password for this, which can be retrieved by pressing the "MENU" button on the device and opening the 2.4 GHz menu.
|
* Note that you'll need the Orbic's wifi password for this, which can be retrieved by pressing the "MENU" button on the device and opening the 2.4 GHz menu.
|
||||||
2. Over usb: Connect the Orbic device to your laptop via usb. Run `adb forward tcp:8080 tcp:8080`, then visit `http://localhost:8080`.
|
2. Over usb: Connect the Orbic device to your laptop via usb. Run `adb forward tcp:8080 tcp:8080`, then visit `http://localhost:8080`. For this you will need to install the Android Debug Bridge (ADB) on your computer, you can copy the version that was downloaded inside the releases/platform-tools/` folder to somewhere else in your path or you can install it manually. You can find instructions for doing so on your platform [here](https://www.xda-developers.com/install-adb-windows-macos-linux/#how-to-set-up-adb-on-your-computer), (don't worry about instructions for installing it on a phone/device yet).
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
* Install ADB on your computer using the instructions above.
|
* Install ADB on your computer using the instructions above, and make sure it's in your terminal's PATH
|
||||||
|
* You can verify if ADB is in your PATH by running `which adb` in a terminal. If it prints the filepath to where ADB is installed, you're set! Otherwise, try following one of these guides:
|
||||||
|
* [linux](https://askubuntu.com/questions/652936/adding-android-sdk-platform-tools-to-path-downloaded-from-umake)
|
||||||
|
* [macOS](https://www.repeato.app/setting-up-adb-on-macos-a-step-by-step-guide/)
|
||||||
|
* [Windows](https://medium.com/@yadav-ajay/a-step-by-step-guide-to-setting-up-adb-path-on-windows-0b833faebf18)
|
||||||
|
|
||||||
### If your are on x86 linux
|
### If your are on x86 linux
|
||||||
* on your linux laptop install rust the usual way and then install cross compiling dependences.
|
* on your linux laptop install rust the usual way and then install cross compiling dependences.
|
||||||
|
|||||||
@@ -0,0 +1,248 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use std::{future, pin};
|
||||||
|
|
||||||
|
use axum::Json;
|
||||||
|
use axum::{
|
||||||
|
extract::{Path, State},
|
||||||
|
http::StatusCode,
|
||||||
|
};
|
||||||
|
use futures::TryStreamExt;
|
||||||
|
use log::{debug, error, info};
|
||||||
|
use rayhunter::analysis::analyzer::Harness;
|
||||||
|
use rayhunter::diag::{DataType, MessagesContainer};
|
||||||
|
use rayhunter::qmdl::QmdlReader;
|
||||||
|
use serde::Serialize;
|
||||||
|
use tokio::fs::File;
|
||||||
|
use tokio::io::{AsyncWriteExt, BufWriter};
|
||||||
|
use tokio::sync::mpsc::Receiver;
|
||||||
|
use tokio::sync::{RwLock, RwLockWriteGuard};
|
||||||
|
use tokio_util::task::TaskTracker;
|
||||||
|
|
||||||
|
use crate::qmdl_store::RecordingStore;
|
||||||
|
use crate::server::ServerState;
|
||||||
|
use crate::dummy_analyzer::TestAnalyzer;
|
||||||
|
|
||||||
|
pub struct AnalysisWriter {
|
||||||
|
writer: BufWriter<File>,
|
||||||
|
harness: Harness,
|
||||||
|
bytes_written: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
// We write our analysis results to a file immediately to minimize the amount of
|
||||||
|
// state Rayhunter has to keep track of in memory. The analysis file's format is
|
||||||
|
// Newline Delimited JSON
|
||||||
|
// (https://docs.mulesoft.com/dataweave/latest/dataweave-formats-ndjson), which
|
||||||
|
// lets us simply append new rows to the end without parsing the entire JSON
|
||||||
|
// object beforehand.
|
||||||
|
impl AnalysisWriter {
|
||||||
|
pub async fn new(file: File, enable_dummy_analyzer: bool) -> Result<Self, std::io::Error> {
|
||||||
|
let mut harness = Harness::new_with_all_analyzers();
|
||||||
|
if enable_dummy_analyzer {
|
||||||
|
harness.add_analyzer(Box::new(TestAnalyzer { count: 0 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut result = Self {
|
||||||
|
writer: BufWriter::new(file),
|
||||||
|
bytes_written: 0,
|
||||||
|
harness,
|
||||||
|
};
|
||||||
|
let metadata = result.harness.get_metadata();
|
||||||
|
result.write(&metadata).await?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs the analysis harness on the given container, serializing the results
|
||||||
|
// to the analysis file and returning the file's new length.
|
||||||
|
pub async fn analyze(&mut self, container: MessagesContainer) -> Result<(usize, bool), std::io::Error> {
|
||||||
|
let row = self.harness.analyze_qmdl_messages(container);
|
||||||
|
if !row.is_empty() {
|
||||||
|
self.write(&row).await?;
|
||||||
|
}
|
||||||
|
Ok((self.bytes_written, row.contains_warnings()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write<T: Serialize>(&mut self, value: &T) -> Result<(), std::io::Error> {
|
||||||
|
let mut value_str = serde_json::to_string(value).unwrap();
|
||||||
|
value_str.push('\n');
|
||||||
|
self.bytes_written += value_str.len();
|
||||||
|
self.writer.write_all(value_str.as_bytes()).await?;
|
||||||
|
self.writer.flush().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flushes any pending I/O to disk before dropping the writer
|
||||||
|
pub async fn close(mut self) -> Result<(), std::io::Error> {
|
||||||
|
self.writer.flush().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Clone, Default)]
|
||||||
|
pub struct AnalysisStatus {
|
||||||
|
queued: Vec<String>,
|
||||||
|
running: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum AnalysisCtrlMessage {
|
||||||
|
NewFilesQueued,
|
||||||
|
Exit,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn queued_len(analysis_status_lock: Arc<RwLock<AnalysisStatus>>) -> usize {
|
||||||
|
analysis_status_lock.read().await.queued.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn dequeue_to_running(analysis_status_lock: Arc<RwLock<AnalysisStatus>>) -> String {
|
||||||
|
let mut analysis_status = analysis_status_lock.write().await;
|
||||||
|
let name = analysis_status.queued.remove(0);
|
||||||
|
assert!(analysis_status.running.is_none());
|
||||||
|
analysis_status.running = Some(name.clone());
|
||||||
|
name
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn clear_running(analysis_status_lock: Arc<RwLock<AnalysisStatus>>) {
|
||||||
|
let mut analysis_status = analysis_status_lock.write().await;
|
||||||
|
analysis_status.running = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn perform_analysis(
|
||||||
|
name: &str,
|
||||||
|
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
||||||
|
enable_dummy_analyzer: bool,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
info!("Opening QMDL and analysis file for {}...", name);
|
||||||
|
let (analysis_file, qmdl_file, entry_index) = {
|
||||||
|
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||||
|
let (entry_index, _) = qmdl_store
|
||||||
|
.entry_for_name(&name)
|
||||||
|
.ok_or(format!("failed to find QMDL store entry for {}", name))?;
|
||||||
|
let analysis_file = qmdl_store
|
||||||
|
.clear_and_open_entry_analysis(entry_index)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("{:?}", e))?;
|
||||||
|
let qmdl_file = qmdl_store
|
||||||
|
.open_entry_qmdl(entry_index)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("{:?}", e))?;
|
||||||
|
|
||||||
|
(analysis_file, qmdl_file, entry_index)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut analysis_writer = AnalysisWriter::new(analysis_file, enable_dummy_analyzer)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("{:?}", e))?;
|
||||||
|
let file_size = qmdl_file
|
||||||
|
.metadata()
|
||||||
|
.await
|
||||||
|
.expect("failed to get QMDL file metadata")
|
||||||
|
.len();
|
||||||
|
let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(file_size as usize));
|
||||||
|
let mut qmdl_stream = pin::pin!(qmdl_reader
|
||||||
|
.as_stream()
|
||||||
|
.try_filter(|container| future::ready(container.data_type == DataType::UserSpace)));
|
||||||
|
|
||||||
|
info!("Starting analysis for {}...", name);
|
||||||
|
while let Some(container) = qmdl_stream
|
||||||
|
.try_next()
|
||||||
|
.await
|
||||||
|
.expect("failed getting QMDL container")
|
||||||
|
{
|
||||||
|
let (size_bytes, _) = analysis_writer
|
||||||
|
.analyze(container)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("{:?}", e))?;
|
||||||
|
debug!("{} analysis: {} bytes written", name, size_bytes);
|
||||||
|
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||||
|
qmdl_store
|
||||||
|
.update_entry_analysis_size(entry_index, size_bytes)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("{:?}", e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
analysis_writer
|
||||||
|
.close()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("{:?}", e))?;
|
||||||
|
info!("Analysis for {} complete!", name);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_analysis_thread(
|
||||||
|
task_tracker: &TaskTracker,
|
||||||
|
mut analysis_rx: Receiver<AnalysisCtrlMessage>,
|
||||||
|
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
||||||
|
analysis_status_lock: Arc<RwLock<AnalysisStatus>>,
|
||||||
|
enable_dummy_analyzer: bool,
|
||||||
|
) {
|
||||||
|
task_tracker.spawn(async move {
|
||||||
|
loop {
|
||||||
|
match analysis_rx.recv().await {
|
||||||
|
Some(AnalysisCtrlMessage::NewFilesQueued) => {
|
||||||
|
let count = queued_len(analysis_status_lock.clone()).await;
|
||||||
|
for _ in 0..count {
|
||||||
|
let name = dequeue_to_running(analysis_status_lock.clone()).await;
|
||||||
|
if let Err(err) = perform_analysis(&name, qmdl_store_lock.clone(), enable_dummy_analyzer).await {
|
||||||
|
error!("failed to analyze {}: {}", name, err);
|
||||||
|
}
|
||||||
|
clear_running(analysis_status_lock.clone()).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(AnalysisCtrlMessage::Exit) | None => return,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_analysis_status(
|
||||||
|
State(state): State<Arc<ServerState>>,
|
||||||
|
) -> Result<Json<AnalysisStatus>, (StatusCode, String)> {
|
||||||
|
Ok(Json(state.analysis_status_lock.read().await.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn queue_qmdl(name: &str, analysis_status: &mut RwLockWriteGuard<AnalysisStatus>) -> bool {
|
||||||
|
if analysis_status.queued.iter().any(|n| n == name)
|
||||||
|
|| analysis_status.running.iter().any(|n| n == name)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
analysis_status.queued.push(name.to_string());
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start_analysis(
|
||||||
|
State(state): State<Arc<ServerState>>,
|
||||||
|
Path(qmdl_name): Path<String>,
|
||||||
|
) -> Result<(StatusCode, Json<AnalysisStatus>), (StatusCode, String)> {
|
||||||
|
let mut analysis_status = state.analysis_status_lock.write().await;
|
||||||
|
let store = state.qmdl_store_lock.read().await;
|
||||||
|
let queued = if qmdl_name.is_empty() {
|
||||||
|
let mut entry_names: Vec<&str> = store
|
||||||
|
.manifest
|
||||||
|
.entries
|
||||||
|
.iter()
|
||||||
|
.map(|e| e.name.as_str())
|
||||||
|
.collect();
|
||||||
|
if let Some(current_entry) = store.current_entry {
|
||||||
|
entry_names.remove(current_entry);
|
||||||
|
}
|
||||||
|
entry_names
|
||||||
|
.iter()
|
||||||
|
.any(|name| queue_qmdl(name, &mut analysis_status))
|
||||||
|
} else {
|
||||||
|
queue_qmdl(&qmdl_name, &mut analysis_status)
|
||||||
|
};
|
||||||
|
if queued {
|
||||||
|
state
|
||||||
|
.analysis_sender
|
||||||
|
.send(AnalysisCtrlMessage::NewFilesQueued)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("failed to queue new analysis files: {:?}", e),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Ok((StatusCode::ACCEPTED, Json(analysis_status.clone())))
|
||||||
|
}
|
||||||
+64
-11
@@ -1,14 +1,57 @@
|
|||||||
use std::{future, path::PathBuf, pin::pin};
|
use std::{collections::HashMap, future, path::PathBuf, pin::pin};
|
||||||
use rayhunter::{analysis::analyzer::Harness, diag::DataType, qmdl::QmdlReader};
|
use rayhunter::{analysis::analyzer::Harness, diag::DataType, qmdl::QmdlReader};
|
||||||
use tokio::fs::File;
|
use tokio::fs::{metadata, read_dir, File};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use futures::TryStreamExt;
|
use futures::TryStreamExt;
|
||||||
|
|
||||||
|
mod dummy_analyzer;
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(version, about)]
|
#[command(version, about)]
|
||||||
struct Args {
|
struct Args {
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
qmdl_path: PathBuf,
|
qmdl_path: PathBuf,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
show_skipped: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
enable_dummy_analyzer: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn analyze_file(harness: &mut Harness, qmdl_path: &str, show_skipped: bool) {
|
||||||
|
let qmdl_file = &mut File::open(&qmdl_path).await.expect("failed to open file");
|
||||||
|
let file_size = qmdl_file.metadata().await.expect("failed to get QMDL file metadata").len();
|
||||||
|
let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(file_size as usize));
|
||||||
|
let mut qmdl_stream = pin!(qmdl_reader.as_stream()
|
||||||
|
.try_filter(|container| future::ready(container.data_type == DataType::UserSpace)));
|
||||||
|
let mut skipped_reasons: HashMap<String, i32> = HashMap::new();
|
||||||
|
let mut total_messages = 0;
|
||||||
|
let mut warnings = 0;
|
||||||
|
let mut skipped = 0;
|
||||||
|
while let Some(container) = qmdl_stream.try_next().await.expect("failed getting QMDL container") {
|
||||||
|
let row = harness.analyze_qmdl_messages(container);
|
||||||
|
total_messages += 1;
|
||||||
|
for reason in row.skipped_message_reasons {
|
||||||
|
*skipped_reasons.entry(reason).or_insert(0) += 1;
|
||||||
|
skipped += 1;
|
||||||
|
}
|
||||||
|
for analysis in row.analysis {
|
||||||
|
for maybe_event in analysis.events {
|
||||||
|
if let Some(event) = maybe_event {
|
||||||
|
warnings += 1;
|
||||||
|
println!("{}: {:?}", analysis.timestamp, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if show_skipped && skipped > 0 {
|
||||||
|
println!("{}: messages skipped:", qmdl_path);
|
||||||
|
for (reason, count) in skipped_reasons.iter() {
|
||||||
|
println!(" - {}: \"{}\"", count, reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("{}: {} messages analyzed, {} warnings, {} messages skipped", qmdl_path, total_messages, warnings, skipped);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -17,15 +60,25 @@ async fn main() {
|
|||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
let mut harness = Harness::new_with_all_analyzers();
|
let mut harness = Harness::new_with_all_analyzers();
|
||||||
|
if args.enable_dummy_analyzer {
|
||||||
|
harness.add_analyzer(Box::new(dummy_analyzer::TestAnalyzer { count: 0 }));
|
||||||
|
}
|
||||||
|
println!("Analyzers:");
|
||||||
|
for analyzer in harness.get_metadata().analyzers {
|
||||||
|
println!(" - {}: {}", analyzer.name, analyzer.description);
|
||||||
|
}
|
||||||
|
|
||||||
let qmdl_file = File::open(args.qmdl_path).await.expect("failed to open QMDL file");
|
let metadata = metadata(&args.qmdl_path).await.expect("failed to get metadata");
|
||||||
let file_size = qmdl_file.metadata().await.expect("failed to get QMDL file metadata").len();
|
if metadata.is_dir() {
|
||||||
let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(file_size as usize));
|
let mut dir = read_dir(&args.qmdl_path).await.expect("failed to read dir");
|
||||||
let mut qmdl_stream = pin!(qmdl_reader.as_stream()
|
while let Some(entry) = dir.next_entry().await.expect("failed to get entry") {
|
||||||
.try_filter(|container| future::ready(container.data_type == DataType::UserSpace)));
|
let name = entry.file_name();
|
||||||
println!("{}\n", serde_json::to_string(&harness.get_metadata()).expect("failed to serialize report metadata"));
|
let name_str = name.to_str().unwrap();
|
||||||
while let Some(container) = qmdl_stream.try_next().await.expect("failed getting QMDL container") {
|
if name_str.ends_with(".qmdl") {
|
||||||
let row = harness.analyze_qmdl_messages(container);
|
analyze_file(&mut harness, entry.path().to_str().unwrap(), args.show_skipped).await;
|
||||||
println!("{}\n", serde_json::to_string(&row).expect("failed to serialize row"));
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
analyze_file(&mut harness, args.qmdl_path.to_str().unwrap(), args.show_skipped).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-7
@@ -6,16 +6,18 @@ use serde::Deserialize;
|
|||||||
struct ConfigFile {
|
struct ConfigFile {
|
||||||
qmdl_store_path: Option<String>,
|
qmdl_store_path: Option<String>,
|
||||||
port: Option<u16>,
|
port: Option<u16>,
|
||||||
readonly_mode: Option<bool>,
|
debug_mode: Option<bool>,
|
||||||
ui_level: Option<u8>,
|
ui_level: Option<u8>,
|
||||||
|
enable_dummy_analyzer: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub qmdl_store_path: String,
|
pub qmdl_store_path: String,
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
pub readonly_mode: bool,
|
pub debug_mode: bool,
|
||||||
pub ui_level: u8,
|
pub ui_level: u8,
|
||||||
|
pub enable_dummy_analyzer: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
@@ -23,8 +25,9 @@ impl Default for Config {
|
|||||||
Config {
|
Config {
|
||||||
qmdl_store_path: "/data/rayhunter/qmdl".to_string(),
|
qmdl_store_path: "/data/rayhunter/qmdl".to_string(),
|
||||||
port: 8080,
|
port: 8080,
|
||||||
readonly_mode: false,
|
debug_mode: false,
|
||||||
ui_level: 1,
|
ui_level: 1,
|
||||||
|
enable_dummy_analyzer: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,10 +37,11 @@ pub fn parse_config<P>(path: P) -> Result<Config, RayhunterError> where P: AsRef
|
|||||||
if let Ok(config_file) = std::fs::read_to_string(&path) {
|
if let Ok(config_file) = std::fs::read_to_string(&path) {
|
||||||
let parsed_config: ConfigFile = toml::from_str(&config_file)
|
let parsed_config: ConfigFile = toml::from_str(&config_file)
|
||||||
.map_err(RayhunterError::ConfigFileParsingError)?;
|
.map_err(RayhunterError::ConfigFileParsingError)?;
|
||||||
if let Some(path) = parsed_config.qmdl_store_path { config.qmdl_store_path = path }
|
parsed_config.qmdl_store_path.map(|v| config.qmdl_store_path = v);
|
||||||
if let Some(port) = parsed_config.port { config.port = port }
|
parsed_config.port.map(|v| config.port = v);
|
||||||
if let Some(readonly_mode) = parsed_config.readonly_mode { config.readonly_mode = readonly_mode }
|
parsed_config.debug_mode.map(|v| config.debug_mode = v);
|
||||||
if let Some(ui_level) = parsed_config.ui_level { config.ui_level = ui_level }
|
parsed_config.ui_level.map(|v| config.ui_level = v);
|
||||||
|
parsed_config.enable_dummy_analyzer.map(|v| config.enable_dummy_analyzer = v);
|
||||||
}
|
}
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|||||||
+59
-23
@@ -1,3 +1,4 @@
|
|||||||
|
mod analysis;
|
||||||
mod config;
|
mod config;
|
||||||
mod error;
|
mod error;
|
||||||
mod pcap;
|
mod pcap;
|
||||||
@@ -6,6 +7,7 @@ mod stats;
|
|||||||
mod qmdl_store;
|
mod qmdl_store;
|
||||||
mod diag;
|
mod diag;
|
||||||
mod framebuffer;
|
mod framebuffer;
|
||||||
|
mod dummy_analyzer;
|
||||||
|
|
||||||
use crate::config::{parse_config, parse_args};
|
use crate::config::{parse_config, parse_args};
|
||||||
use crate::diag::run_diag_read_thread;
|
use crate::diag::run_diag_read_thread;
|
||||||
@@ -16,6 +18,7 @@ use crate::stats::get_system_stats;
|
|||||||
use crate::error::RayhunterError;
|
use crate::error::RayhunterError;
|
||||||
use crate::framebuffer::Framebuffer;
|
use crate::framebuffer::Framebuffer;
|
||||||
|
|
||||||
|
use analysis::{get_analysis_status, run_analysis_thread, start_analysis, AnalysisCtrlMessage, AnalysisStatus};
|
||||||
use axum::response::Redirect;
|
use axum::response::Redirect;
|
||||||
use diag::{get_analysis_report, start_recording, stop_recording, DiagDeviceCtrlMessage};
|
use diag::{get_analysis_report, start_recording, stop_recording, DiagDeviceCtrlMessage};
|
||||||
use log::{info, error};
|
use log::{info, error};
|
||||||
@@ -23,7 +26,7 @@ use rayhunter::diag_device::DiagDevice;
|
|||||||
use axum::routing::{get, post};
|
use axum::routing::{get, post};
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use stats::get_qmdl_manifest;
|
use stats::get_qmdl_manifest;
|
||||||
use tokio::sync::mpsc::{self, Sender};
|
use tokio::sync::mpsc::{self, Sender, Receiver};
|
||||||
use tokio::sync::oneshot::error::TryRecvError;
|
use tokio::sync::oneshot::error::TryRecvError;
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use tokio_util::task::TaskTracker;
|
use tokio_util::task::TaskTracker;
|
||||||
@@ -43,12 +46,19 @@ async fn run_server(
|
|||||||
config: &config::Config,
|
config: &config::Config,
|
||||||
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
||||||
server_shutdown_rx: oneshot::Receiver<()>,
|
server_shutdown_rx: oneshot::Receiver<()>,
|
||||||
diag_device_sender: Sender<DiagDeviceCtrlMessage>
|
ui_update_tx: Sender<framebuffer::DisplayState>,
|
||||||
|
diag_device_sender: Sender<DiagDeviceCtrlMessage>,
|
||||||
|
analysis_sender: Sender<AnalysisCtrlMessage>,
|
||||||
|
analysis_status_lock: Arc<RwLock<AnalysisStatus>>,
|
||||||
) -> JoinHandle<()> {
|
) -> JoinHandle<()> {
|
||||||
|
info!("spinning up server");
|
||||||
let state = Arc::new(ServerState {
|
let state = Arc::new(ServerState {
|
||||||
qmdl_store_lock,
|
qmdl_store_lock,
|
||||||
diag_device_ctrl_sender: diag_device_sender,
|
diag_device_ctrl_sender: diag_device_sender,
|
||||||
readonly_mode: config.readonly_mode
|
ui_update_sender: ui_update_tx,
|
||||||
|
debug_mode: config.debug_mode,
|
||||||
|
analysis_status_lock,
|
||||||
|
analysis_sender,
|
||||||
});
|
});
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
@@ -58,7 +68,9 @@ async fn run_server(
|
|||||||
.route("/api/qmdl-manifest", get(get_qmdl_manifest))
|
.route("/api/qmdl-manifest", get(get_qmdl_manifest))
|
||||||
.route("/api/start-recording", post(start_recording))
|
.route("/api/start-recording", post(start_recording))
|
||||||
.route("/api/stop-recording", post(stop_recording))
|
.route("/api/stop-recording", post(stop_recording))
|
||||||
.route("/api/analysis-report", get(get_analysis_report))
|
.route("/api/analysis-report/*name", get(get_analysis_report))
|
||||||
|
.route("/api/analysis", get(get_analysis_status))
|
||||||
|
.route("/api/analysis/*name", post(start_analysis))
|
||||||
.route("/", get(|| async { Redirect::permanent("/index.html") }))
|
.route("/", get(|| async { Redirect::permanent("/index.html") }))
|
||||||
.route("/*path", get(serve_static))
|
.route("/*path", get(serve_static))
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
@@ -78,12 +90,12 @@ async fn server_shutdown_signal(server_shutdown_rx: oneshot::Receiver<()>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Loads a QmdlStore if one exists, and if not, only create one if we're not in
|
// Loads a QmdlStore if one exists, and if not, only create one if we're not in
|
||||||
// readonly mode.
|
// debug mode.
|
||||||
async fn init_qmdl_store(config: &config::Config) -> Result<RecordingStore, RayhunterError> {
|
async fn init_qmdl_store(config: &config::Config) -> Result<RecordingStore, RayhunterError> {
|
||||||
match (RecordingStore::exists(&config.qmdl_store_path).await?, config.readonly_mode) {
|
match (RecordingStore::exists(&config.qmdl_store_path).await?, config.debug_mode) {
|
||||||
(true, _) => Ok(RecordingStore::load(&config.qmdl_store_path).await?),
|
(true, _) => Ok(RecordingStore::load(&config.qmdl_store_path).await?),
|
||||||
(false, false) => Ok(RecordingStore::create(&config.qmdl_store_path).await?),
|
(false, false) => Ok(RecordingStore::create(&config.qmdl_store_path).await?),
|
||||||
(false, true) => Err(RayhunterError::NoStoreReadonlyMode(config.qmdl_store_path.clone())),
|
(false, true) => Err(RayhunterError::NoStoreDebugMode(config.qmdl_store_path.clone())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,8 +106,9 @@ fn run_ctrl_c_thread(
|
|||||||
task_tracker: &TaskTracker,
|
task_tracker: &TaskTracker,
|
||||||
diag_device_sender: Sender<DiagDeviceCtrlMessage>,
|
diag_device_sender: Sender<DiagDeviceCtrlMessage>,
|
||||||
server_shutdown_tx: oneshot::Sender<()>,
|
server_shutdown_tx: oneshot::Sender<()>,
|
||||||
ui_shutdown_tx: oneshot::Sender<()>,
|
maybe_ui_shutdown_tx: Option<oneshot::Sender<()>>,
|
||||||
qmdl_store_lock: Arc<RwLock<RecordingStore>>
|
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
||||||
|
analysis_tx: Sender<AnalysisCtrlMessage>,
|
||||||
) -> JoinHandle<Result<(), RayhunterError>> {
|
) -> JoinHandle<Result<(), RayhunterError>> {
|
||||||
task_tracker.spawn(async move {
|
task_tracker.spawn(async move {
|
||||||
match tokio::signal::ctrl_c().await {
|
match tokio::signal::ctrl_c().await {
|
||||||
@@ -110,10 +123,14 @@ fn run_ctrl_c_thread(
|
|||||||
server_shutdown_tx.send(())
|
server_shutdown_tx.send(())
|
||||||
.expect("couldn't send server shutdown signal");
|
.expect("couldn't send server shutdown signal");
|
||||||
info!("sending UI shutdown");
|
info!("sending UI shutdown");
|
||||||
ui_shutdown_tx.send(())
|
if let Some(ui_shutdown_tx) = maybe_ui_shutdown_tx {
|
||||||
.expect("couldn't send ui shutdown signal");
|
ui_shutdown_tx.send(())
|
||||||
|
.expect("couldn't send ui shutdown signal");
|
||||||
|
}
|
||||||
diag_device_sender.send(DiagDeviceCtrlMessage::Exit).await
|
diag_device_sender.send(DiagDeviceCtrlMessage::Exit).await
|
||||||
.expect("couldn't send Exit message to diag thread");
|
.expect("couldn't send Exit message to diag thread");
|
||||||
|
analysis_tx.send(AnalysisCtrlMessage::Exit).await
|
||||||
|
.expect("couldn't send Exit message to analysis thread");
|
||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Unable to listen for shutdown signal: {}", err);
|
error!("Unable to listen for shutdown signal: {}", err);
|
||||||
@@ -123,13 +140,15 @@ fn run_ctrl_c_thread(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_ui(task_tracker: &TaskTracker, config: &config::Config, mut ui_shutdown_rx: oneshot::Receiver<()>){
|
fn update_ui(task_tracker: &TaskTracker, config: &config::Config, mut ui_shutdown_rx: oneshot::Receiver<()>, mut ui_update_rx: Receiver<framebuffer::DisplayState>) -> JoinHandle<()> {
|
||||||
static IMAGE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/static/images/");
|
static IMAGE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/static/images/");
|
||||||
let display_level = config.ui_level;
|
let display_level = config.ui_level;
|
||||||
if display_level == 0 {
|
if display_level == 0 {
|
||||||
info!("Invisible mode, not spawning UI.");
|
info!("Invisible mode, not spawning UI.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut display_color = framebuffer::Color565::Green;
|
||||||
|
|
||||||
task_tracker.spawn_blocking(move || {
|
task_tracker.spawn_blocking(move || {
|
||||||
let mut fb: Framebuffer = Framebuffer::new();
|
let mut fb: Framebuffer = Framebuffer::new();
|
||||||
// this feels wrong, is there a more rusty way to do this?
|
// this feels wrong, is there a more rusty way to do this?
|
||||||
@@ -147,8 +166,15 @@ async fn update_ui(task_tracker: &TaskTracker, config: &config::Config, mut ui_
|
|||||||
},
|
},
|
||||||
Err(TryRecvError::Empty) => {},
|
Err(TryRecvError::Empty) => {},
|
||||||
Err(e) => panic!("error receiving shutdown message: {e}")
|
Err(e) => panic!("error receiving shutdown message: {e}")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
match ui_update_rx.try_recv() {
|
||||||
|
Ok(state) => {
|
||||||
|
display_color = state.into();
|
||||||
|
},
|
||||||
|
Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {},
|
||||||
|
Err(e) => error!("error receiving framebuffer update message: {e}")
|
||||||
|
}
|
||||||
|
|
||||||
match display_level {
|
match display_level {
|
||||||
2 => {
|
2 => {
|
||||||
fb.draw_gif(img.unwrap());
|
fb.draw_gif(img.unwrap());
|
||||||
@@ -164,13 +190,12 @@ async fn update_ui(task_tracker: &TaskTracker, config: &config::Config, mut ui_
|
|||||||
fb.draw_line(framebuffer::Color565::Cyan, 25);
|
fb.draw_line(framebuffer::Color565::Cyan, 25);
|
||||||
},
|
},
|
||||||
1 | _ => {
|
1 | _ => {
|
||||||
fb.draw_line(framebuffer::Color565::Green, 2);
|
fb.draw_line(display_color, 2);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
sleep(Duration::from_millis(100));
|
sleep(Duration::from_millis(1000));
|
||||||
}
|
}
|
||||||
}).await.unwrap();
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -183,25 +208,36 @@ async fn main() -> Result<(), RayhunterError> {
|
|||||||
// TaskTrackers give us an interface to spawn tokio threads, and then
|
// TaskTrackers give us an interface to spawn tokio threads, and then
|
||||||
// eventually await all of them ending
|
// eventually await all of them ending
|
||||||
let task_tracker = TaskTracker::new();
|
let task_tracker = TaskTracker::new();
|
||||||
|
println!("R A Y H U N T E R 🐳");
|
||||||
|
|
||||||
let qmdl_store_lock = Arc::new(RwLock::new(init_qmdl_store(&config).await?));
|
let qmdl_store_lock = Arc::new(RwLock::new(init_qmdl_store(&config).await?));
|
||||||
let (tx, rx) = mpsc::channel::<DiagDeviceCtrlMessage>(1);
|
let (tx, rx) = mpsc::channel::<DiagDeviceCtrlMessage>(1);
|
||||||
if !config.readonly_mode {
|
let (ui_update_tx, ui_update_rx) = mpsc::channel::<framebuffer::DisplayState>(1);
|
||||||
|
let (analysis_tx, analysis_rx) = mpsc::channel::<AnalysisCtrlMessage>(5);
|
||||||
|
let mut maybe_ui_shutdown_tx = None;
|
||||||
|
if !config.debug_mode {
|
||||||
|
let (ui_shutdown_tx, ui_shutdown_rx) = oneshot::channel();
|
||||||
|
maybe_ui_shutdown_tx = Some(ui_shutdown_tx);
|
||||||
let mut dev = DiagDevice::new().await
|
let mut dev = DiagDevice::new().await
|
||||||
.map_err(RayhunterError::DiagInitError)?;
|
.map_err(RayhunterError::DiagInitError)?;
|
||||||
dev.config_logs().await
|
dev.config_logs().await
|
||||||
.map_err(RayhunterError::DiagInitError)?;
|
.map_err(RayhunterError::DiagInitError)?;
|
||||||
|
|
||||||
run_diag_read_thread(&task_tracker, dev, rx, qmdl_store_lock.clone());
|
info!("Starting Diag Thread");
|
||||||
|
run_diag_read_thread(&task_tracker, dev, rx, ui_update_tx.clone(), qmdl_store_lock.clone(), config.enable_dummy_analyzer);
|
||||||
|
info!("Starting UI");
|
||||||
|
update_ui(&task_tracker, &config, ui_shutdown_rx, ui_update_rx);
|
||||||
}
|
}
|
||||||
let (ui_shutdown_tx, ui_shutdown_rx) = oneshot::channel();
|
|
||||||
let (server_shutdown_tx, server_shutdown_rx) = oneshot::channel::<()>();
|
let (server_shutdown_tx, server_shutdown_rx) = oneshot::channel::<()>();
|
||||||
run_ctrl_c_thread(&task_tracker, tx.clone(), server_shutdown_tx, ui_shutdown_tx, qmdl_store_lock.clone());
|
info!("create shutdown thread");
|
||||||
run_server(&task_tracker, &config, qmdl_store_lock.clone(), server_shutdown_rx, tx).await;
|
let analysis_status_lock = Arc::new(RwLock::new(AnalysisStatus::default()));
|
||||||
update_ui(&task_tracker, &config, ui_shutdown_rx).await;
|
run_analysis_thread(&task_tracker, analysis_rx, qmdl_store_lock.clone(), analysis_status_lock.clone(), config.enable_dummy_analyzer);
|
||||||
|
run_ctrl_c_thread(&task_tracker, tx.clone(), server_shutdown_tx, maybe_ui_shutdown_tx, qmdl_store_lock.clone(), analysis_tx.clone());
|
||||||
|
run_server(&task_tracker, &config, qmdl_store_lock.clone(), server_shutdown_rx, ui_update_tx, tx, analysis_tx, analysis_status_lock).await;
|
||||||
|
|
||||||
task_tracker.close();
|
task_tracker.close();
|
||||||
task_tracker.wait().await;
|
task_tracker.wait().await;
|
||||||
|
|
||||||
|
info!("see you space cowboy...");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-69
@@ -2,26 +2,25 @@ use std::pin::pin;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use axum::body::Body;
|
use axum::body::Body;
|
||||||
use axum::extract::State;
|
use axum::extract::{Path, State};
|
||||||
use axum::http::header::CONTENT_TYPE;
|
use axum::http::header::CONTENT_TYPE;
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
use rayhunter::analysis::analyzer::Harness;
|
use rayhunter::diag::DataType;
|
||||||
use rayhunter::diag::{DataType, MessagesContainer};
|
|
||||||
use rayhunter::diag_device::DiagDevice;
|
use rayhunter::diag_device::DiagDevice;
|
||||||
use serde::Serialize;
|
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use tokio::sync::mpsc::Receiver;
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
use rayhunter::qmdl::QmdlWriter;
|
use rayhunter::qmdl::QmdlWriter;
|
||||||
use log::{debug, error, info};
|
use log::{debug, error, info};
|
||||||
use tokio::fs::File;
|
use tokio::fs::File;
|
||||||
use tokio::io::{BufWriter, AsyncWriteExt};
|
|
||||||
use tokio_util::io::ReaderStream;
|
use tokio_util::io::ReaderStream;
|
||||||
use tokio_util::task::TaskTracker;
|
use tokio_util::task::TaskTracker;
|
||||||
use futures::{StreamExt, TryStreamExt};
|
use futures::{StreamExt, TryStreamExt};
|
||||||
|
|
||||||
|
use crate::framebuffer;
|
||||||
use crate::qmdl_store::RecordingStore;
|
use crate::qmdl_store::RecordingStore;
|
||||||
use crate::server::ServerState;
|
use crate::server::ServerState;
|
||||||
|
use crate::analysis::AnalysisWriter;
|
||||||
|
|
||||||
pub enum DiagDeviceCtrlMessage {
|
pub enum DiagDeviceCtrlMessage {
|
||||||
StopRecording,
|
StopRecording,
|
||||||
@@ -29,67 +28,19 @@ pub enum DiagDeviceCtrlMessage {
|
|||||||
Exit,
|
Exit,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AnalysisWriter {
|
|
||||||
writer: BufWriter<File>,
|
|
||||||
harness: Harness,
|
|
||||||
bytes_written: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
// We write our analysis results to a file immediately to minimize the amount of
|
|
||||||
// state Rayhunter has to keep track of in memory. The analysis file's format is
|
|
||||||
// Newline Delimited JSON
|
|
||||||
// (https://docs.mulesoft.com/dataweave/latest/dataweave-formats-ndjson), which
|
|
||||||
// lets us simply append new rows to the end without parsing the entire JSON
|
|
||||||
// object beforehand.
|
|
||||||
impl AnalysisWriter {
|
|
||||||
pub async fn new(file: File) -> Result<Self, std::io::Error> {
|
|
||||||
let mut result = Self {
|
|
||||||
writer: BufWriter::new(file),
|
|
||||||
harness: Harness::new_with_all_analyzers(),
|
|
||||||
bytes_written: 0,
|
|
||||||
};
|
|
||||||
let metadata = result.harness.get_metadata();
|
|
||||||
result.write(&metadata).await?;
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Runs the analysis harness on the given container, serializing the results
|
|
||||||
// to the analysis file and returning the file's new length.
|
|
||||||
pub async fn analyze(&mut self, container: MessagesContainer) -> Result<usize, std::io::Error> {
|
|
||||||
let row = self.harness.analyze_qmdl_messages(container);
|
|
||||||
if !row.is_empty() {
|
|
||||||
self.write(&row).await?;
|
|
||||||
}
|
|
||||||
Ok(self.bytes_written)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn write<T: Serialize>(&mut self, value: &T) -> Result<(), std::io::Error> {
|
|
||||||
let mut value_str = serde_json::to_string(value).unwrap();
|
|
||||||
value_str.push('\n');
|
|
||||||
self.bytes_written += value_str.len();
|
|
||||||
self.writer.write_all(value_str.as_bytes()).await?;
|
|
||||||
self.writer.flush().await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flushes any pending I/O to disk before dropping the writer
|
|
||||||
pub async fn close(mut self) -> Result<(), std::io::Error> {
|
|
||||||
self.writer.flush().await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run_diag_read_thread(
|
pub fn run_diag_read_thread(
|
||||||
task_tracker: &TaskTracker,
|
task_tracker: &TaskTracker,
|
||||||
mut dev: DiagDevice,
|
mut dev: DiagDevice,
|
||||||
mut qmdl_file_rx: Receiver<DiagDeviceCtrlMessage>,
|
mut qmdl_file_rx: Receiver<DiagDeviceCtrlMessage>,
|
||||||
qmdl_store_lock: Arc<RwLock<RecordingStore>>
|
ui_update_sender: Sender<framebuffer::DisplayState>,
|
||||||
|
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
||||||
|
enable_dummy_analyzer: bool,
|
||||||
) {
|
) {
|
||||||
task_tracker.spawn(async move {
|
task_tracker.spawn(async move {
|
||||||
let (initial_qmdl_file, initial_analysis_file) = qmdl_store_lock.write().await.new_entry().await.expect("failed creating QMDL file entry");
|
let (initial_qmdl_file, initial_analysis_file) = qmdl_store_lock.write().await.new_entry().await.expect("failed creating QMDL file entry");
|
||||||
let mut maybe_qmdl_writer: Option<QmdlWriter<File>> = Some(QmdlWriter::new(initial_qmdl_file));
|
let mut maybe_qmdl_writer: Option<QmdlWriter<File>> = Some(QmdlWriter::new(initial_qmdl_file));
|
||||||
let mut diag_stream = pin!(dev.as_stream().into_stream());
|
let mut diag_stream = pin!(dev.as_stream().into_stream());
|
||||||
let mut maybe_analysis_writer = Some(AnalysisWriter::new(initial_analysis_file).await
|
let mut maybe_analysis_writer = Some(AnalysisWriter::new(initial_analysis_file, enable_dummy_analyzer).await
|
||||||
.expect("failed to create analysis writer"));
|
.expect("failed to create analysis writer"));
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
@@ -100,7 +51,7 @@ pub fn run_diag_read_thread(
|
|||||||
if let Some(analysis_writer) = maybe_analysis_writer {
|
if let Some(analysis_writer) = maybe_analysis_writer {
|
||||||
analysis_writer.close().await.expect("failed to close analysis writer");
|
analysis_writer.close().await.expect("failed to close analysis writer");
|
||||||
}
|
}
|
||||||
maybe_analysis_writer = Some(AnalysisWriter::new(new_analysis_file).await
|
maybe_analysis_writer = Some(AnalysisWriter::new(new_analysis_file, enable_dummy_analyzer).await
|
||||||
.expect("failed to write to analysis file"));
|
.expect("failed to write to analysis file"));
|
||||||
},
|
},
|
||||||
Some(DiagDeviceCtrlMessage::StopRecording) => {
|
Some(DiagDeviceCtrlMessage::StopRecording) => {
|
||||||
@@ -143,8 +94,14 @@ pub fn run_diag_read_thread(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(analysis_writer) = maybe_analysis_writer.as_mut() {
|
if let Some(analysis_writer) = maybe_analysis_writer.as_mut() {
|
||||||
let analysis_file_len = analysis_writer.analyze(container).await
|
let analysis_output = analysis_writer.analyze(container).await
|
||||||
.expect("failed to analyze container");
|
.expect("failed to analyze container");
|
||||||
|
let (analysis_file_len, heuristic_warning) = analysis_output;
|
||||||
|
if heuristic_warning {
|
||||||
|
info!("a heuristic triggered on this run!");
|
||||||
|
ui_update_sender.send(framebuffer::DisplayState::WarningDetected).await
|
||||||
|
.expect("couldn't send ui update message: {}");
|
||||||
|
}
|
||||||
let mut qmdl_store = qmdl_store_lock.write().await;
|
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||||
let index = qmdl_store.current_entry.expect("DiagDevice had qmdl_writer, but QmdlStore didn't have current entry???");
|
let index = qmdl_store.current_entry.expect("DiagDevice had qmdl_writer, but QmdlStore didn't have current entry???");
|
||||||
qmdl_store.update_entry_analysis_size(index, analysis_file_len as usize).await
|
qmdl_store.update_entry_analysis_size(index, analysis_file_len as usize).await
|
||||||
@@ -163,8 +120,8 @@ pub fn run_diag_read_thread(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start_recording(State(state): State<Arc<ServerState>>) -> Result<(StatusCode, String), (StatusCode, String)> {
|
pub async fn start_recording(State(state): State<Arc<ServerState>>) -> Result<(StatusCode, String), (StatusCode, String)> {
|
||||||
if state.readonly_mode {
|
if state.debug_mode {
|
||||||
return Err((StatusCode::FORBIDDEN, "server is in readonly mode".to_string()));
|
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
|
||||||
}
|
}
|
||||||
let mut qmdl_store = state.qmdl_store_lock.write().await;
|
let mut qmdl_store = state.qmdl_store_lock.write().await;
|
||||||
let (qmdl_file, analysis_file) = qmdl_store.new_entry().await
|
let (qmdl_file, analysis_file) = qmdl_store.new_entry().await
|
||||||
@@ -172,30 +129,39 @@ pub async fn start_recording(State(state): State<Arc<ServerState>>) -> Result<(S
|
|||||||
let qmdl_writer = QmdlWriter::new(qmdl_file);
|
let qmdl_writer = QmdlWriter::new(qmdl_file);
|
||||||
state.diag_device_ctrl_sender.send(DiagDeviceCtrlMessage::StartRecording((qmdl_writer, analysis_file))).await
|
state.diag_device_ctrl_sender.send(DiagDeviceCtrlMessage::StartRecording((qmdl_writer, analysis_file))).await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't send stop recording message: {}", e)))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't send stop recording message: {}", e)))?;
|
||||||
|
state.ui_update_sender.send(framebuffer::DisplayState::Recording).await
|
||||||
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't send ui update message: {}", e)))?;
|
||||||
Ok((StatusCode::ACCEPTED, "ok".to_string()))
|
Ok((StatusCode::ACCEPTED, "ok".to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn stop_recording(State(state): State<Arc<ServerState>>) -> Result<(StatusCode, String), (StatusCode, String)> {
|
pub async fn stop_recording(State(state): State<Arc<ServerState>>) -> Result<(StatusCode, String), (StatusCode, String)> {
|
||||||
if state.readonly_mode {
|
if state.debug_mode {
|
||||||
return Err((StatusCode::FORBIDDEN, "server is in readonly mode".to_string()));
|
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
|
||||||
}
|
}
|
||||||
let mut qmdl_store = state.qmdl_store_lock.write().await;
|
let mut qmdl_store = state.qmdl_store_lock.write().await;
|
||||||
qmdl_store.close_current_entry().await
|
qmdl_store.close_current_entry().await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't close current qmdl entry: {}", e)))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't close current qmdl entry: {}", e)))?;
|
||||||
state.diag_device_ctrl_sender.send(DiagDeviceCtrlMessage::StopRecording).await
|
state.diag_device_ctrl_sender.send(DiagDeviceCtrlMessage::StopRecording).await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't send stop recording message: {}", e)))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't send stop recording message: {}", e)))?;
|
||||||
|
state.ui_update_sender.send(framebuffer::DisplayState::Paused).await
|
||||||
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't send ui update message: {}", e)))?;
|
||||||
Ok((StatusCode::ACCEPTED, "ok".to_string()))
|
Ok((StatusCode::ACCEPTED, "ok".to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_analysis_report(State(state): State<Arc<ServerState>>) -> Result<Response, (StatusCode, String)> {
|
pub async fn get_analysis_report(State(state): State<Arc<ServerState>>, Path(qmdl_name): Path<String>) -> Result<Response, (StatusCode, String)> {
|
||||||
let qmdl_store = state.qmdl_store_lock.read().await;
|
let qmdl_store = state.qmdl_store_lock.read().await;
|
||||||
let Some(entry) = qmdl_store.get_current_entry() else {
|
let (entry_index, _) = if qmdl_name == "live" {
|
||||||
return Err((
|
qmdl_store.get_current_entry().ok_or((
|
||||||
StatusCode::SERVICE_UNAVAILABLE,
|
StatusCode::SERVICE_UNAVAILABLE,
|
||||||
"No QMDL data's being recorded to analyze, try starting a new recording!".to_string()
|
"No QMDL data's being recorded to analyze, try starting a new recording!".to_string()
|
||||||
));
|
))?
|
||||||
|
} else {
|
||||||
|
qmdl_store.entry_for_name(&qmdl_name).ok_or((
|
||||||
|
StatusCode::NOT_FOUND,
|
||||||
|
format!("Couldn't find QMDL entry with name \"{}\"", qmdl_name)
|
||||||
|
))?
|
||||||
};
|
};
|
||||||
let analysis_file = qmdl_store.open_entry_analysis(entry).await
|
let analysis_file = qmdl_store.open_entry_analysis(entry_index).await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", e)))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", e)))?;
|
||||||
let analysis_stream = ReaderStream::new(analysis_file);
|
let analysis_stream = ReaderStream::new(analysis_file);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use rayhunter::telcom_parser::lte_rrc::{PCCH_MessageType, PCCH_MessageType_c1, PagingUE_Identity};
|
||||||
|
|
||||||
|
use rayhunter::analysis::analyzer::{Analyzer, Event, EventType, Severity};
|
||||||
|
use rayhunter::analysis::information_element::{InformationElement, LteInformationElement};
|
||||||
|
|
||||||
|
pub struct TestAnalyzer{
|
||||||
|
pub count: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Analyzer for TestAnalyzer{
|
||||||
|
fn get_name(&self) -> Cow<str> {
|
||||||
|
Cow::from("Example Analyzer")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_description(&self) -> Cow<str> {
|
||||||
|
Cow::from("Always returns true, if you are seeing this you are either a developer or you are about to have problems.")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> {
|
||||||
|
self.count += 1;
|
||||||
|
if self.count % 100 == 0 {
|
||||||
|
return Some(Event {
|
||||||
|
event_type: EventType::Informational ,
|
||||||
|
message: "multiple of 100 events processed".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
let InformationElement::LTE(LteInformationElement::PCCH(pcch_msg)) = ie else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let PCCH_MessageType::C1(PCCH_MessageType_c1::Paging(paging)) = &pcch_msg.message else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
for record in &paging.paging_record_list.as_ref()?.0 {
|
||||||
|
if let PagingUE_Identity::S_TMSI(_) = record.ue_identity {
|
||||||
|
return Some(Event {
|
||||||
|
event_type: EventType::QualitativeWarning { severity: Severity::Low },
|
||||||
|
message: "TMSI was provided to cell".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -13,6 +13,6 @@ pub enum RayhunterError{
|
|||||||
TokioError(#[from] tokio::io::Error),
|
TokioError(#[from] tokio::io::Error),
|
||||||
#[error("QmdlStore error: {0}")]
|
#[error("QmdlStore error: {0}")]
|
||||||
QmdlStoreError(#[from] RecordingStoreError),
|
QmdlStoreError(#[from] RecordingStoreError),
|
||||||
#[error("No QMDL store found at path {0}, but can't create a new one due to readonly mode")]
|
#[error("No QMDL store found at path {0}, but can't create a new one due to debug mode")]
|
||||||
NoStoreReadonlyMode(String),
|
NoStoreDebugMode(String),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ struct Dimensions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
pub enum Color565 {
|
pub enum Color565 {
|
||||||
Red = 0b1111100000000000,
|
Red = 0b1111100000000000,
|
||||||
Green = 0b0000011111100000,
|
Green = 0b0000011111100000,
|
||||||
@@ -22,6 +23,22 @@ pub enum Color565 {
|
|||||||
Pink = 0b1111010010011111,
|
Pink = 0b1111010010011111,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum DisplayState {
|
||||||
|
Recording,
|
||||||
|
Paused,
|
||||||
|
WarningDetected,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DisplayState> for Color565 {
|
||||||
|
fn from(state: DisplayState) -> Self {
|
||||||
|
match state {
|
||||||
|
DisplayState::Paused => Color565::White,
|
||||||
|
DisplayState::Recording => Color565::Green,
|
||||||
|
DisplayState::WarningDetected => Color565::Red,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
pub struct Framebuffer<'a> {
|
pub struct Framebuffer<'a> {
|
||||||
dimensions: Dimensions,
|
dimensions: Dimensions,
|
||||||
|
|||||||
+4
-4
@@ -21,7 +21,7 @@ use futures::TryStreamExt;
|
|||||||
// pcap data to a channel that's piped to the client.
|
// pcap data to a channel that's piped to the client.
|
||||||
pub async fn get_pcap(State(state): State<Arc<ServerState>>, Path(qmdl_name): Path<String>) -> Result<Response, (StatusCode, String)> {
|
pub async fn get_pcap(State(state): State<Arc<ServerState>>, Path(qmdl_name): Path<String>) -> Result<Response, (StatusCode, String)> {
|
||||||
let qmdl_store = state.qmdl_store_lock.read().await;
|
let qmdl_store = state.qmdl_store_lock.read().await;
|
||||||
let entry = qmdl_store.entry_for_name(&qmdl_name)
|
let (entry_index, entry) = qmdl_store.entry_for_name(&qmdl_name)
|
||||||
.ok_or((StatusCode::NOT_FOUND, format!("couldn't find qmdl file with name {}", qmdl_name)))?;
|
.ok_or((StatusCode::NOT_FOUND, format!("couldn't find qmdl file with name {}", qmdl_name)))?;
|
||||||
if entry.qmdl_size_bytes == 0 {
|
if entry.qmdl_size_bytes == 0 {
|
||||||
return Err((
|
return Err((
|
||||||
@@ -29,8 +29,8 @@ pub async fn get_pcap(State(state): State<Arc<ServerState>>, Path(qmdl_name): Pa
|
|||||||
"QMDL file is empty, try again in a bit!".to_string()
|
"QMDL file is empty, try again in a bit!".to_string()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
let qmdl_size_bytes = entry.qmdl_size_bytes;
|
||||||
let qmdl_file = qmdl_store.open_entry_qmdl(&entry).await
|
let qmdl_file = qmdl_store.open_entry_qmdl(entry_index).await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", e)))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", e)))?;
|
||||||
// the QMDL reader should stop at the last successfully written data chunk
|
// the QMDL reader should stop at the last successfully written data chunk
|
||||||
// (entry.size_bytes)
|
// (entry.size_bytes)
|
||||||
@@ -39,7 +39,7 @@ pub async fn get_pcap(State(state): State<Arc<ServerState>>, Path(qmdl_name): Pa
|
|||||||
pcap_writer.write_iface_header().await.unwrap();
|
pcap_writer.write_iface_header().await.unwrap();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let mut reader = QmdlReader::new(qmdl_file, Some(entry.qmdl_size_bytes));
|
let mut reader = QmdlReader::new(qmdl_file, Some(qmdl_size_bytes));
|
||||||
let mut messages_stream = pin!(reader.as_stream()
|
let mut messages_stream = pin!(reader.as_stream()
|
||||||
.try_filter(|container| future::ready(container.data_type == DataType::UserSpace)));
|
.try_filter(|container| future::ready(container.data_type == DataType::UserSpace)));
|
||||||
|
|
||||||
|
|||||||
+129
-47
@@ -1,8 +1,11 @@
|
|||||||
use std::path::{PathBuf, Path};
|
|
||||||
use thiserror::Error;
|
|
||||||
use tokio::{fs::{self, File, try_exists}, io::AsyncWriteExt};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use chrono::{DateTime, Local};
|
use chrono::{DateTime, Local};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use thiserror::Error;
|
||||||
|
use tokio::{
|
||||||
|
fs::{self, try_exists, File, OpenOptions},
|
||||||
|
io::AsyncWriteExt,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum RecordingStoreError {
|
pub enum RecordingStoreError {
|
||||||
@@ -19,7 +22,7 @@ pub enum RecordingStoreError {
|
|||||||
#[error("Couldn't write manifest file: {0}")]
|
#[error("Couldn't write manifest file: {0}")]
|
||||||
WriteManifestError(tokio::io::Error),
|
WriteManifestError(tokio::io::Error),
|
||||||
#[error("Couldn't parse QMDL store manifest file: {0}")]
|
#[error("Couldn't parse QMDL store manifest file: {0}")]
|
||||||
ParseManifestError(toml::de::Error)
|
ParseManifestError(toml::de::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RecordingStore {
|
pub struct RecordingStore {
|
||||||
@@ -70,16 +73,26 @@ impl ManifestEntry {
|
|||||||
impl RecordingStore {
|
impl RecordingStore {
|
||||||
// Returns whether a directory with a "manifest.toml" exists at the given
|
// Returns whether a directory with a "manifest.toml" exists at the given
|
||||||
// path (though doesn't check if that manifest is valid)
|
// path (though doesn't check if that manifest is valid)
|
||||||
pub async fn exists<P>(path: P) -> Result<bool, RecordingStoreError> where P: AsRef<Path> {
|
pub async fn exists<P>(path: P) -> Result<bool, RecordingStoreError>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
let manifest_path = path.as_ref().join("manifest.toml");
|
let manifest_path = path.as_ref().join("manifest.toml");
|
||||||
let dir_exists = try_exists(path).await.map_err(RecordingStoreError::OpenDirError)?;
|
let dir_exists = try_exists(path)
|
||||||
let manifest_exists = try_exists(manifest_path).await.map_err(RecordingStoreError::ReadManifestError)?;
|
.await
|
||||||
|
.map_err(RecordingStoreError::OpenDirError)?;
|
||||||
|
let manifest_exists = try_exists(manifest_path)
|
||||||
|
.await
|
||||||
|
.map_err(RecordingStoreError::ReadManifestError)?;
|
||||||
Ok(dir_exists && manifest_exists)
|
Ok(dir_exists && manifest_exists)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loads an existing RecordingStore at the given path. Errors if no store exists,
|
// Loads an existing RecordingStore at the given path. Errors if no store exists,
|
||||||
// or if it's malformed.
|
// or if it's malformed.
|
||||||
pub async fn load<P>(path: P) -> Result<Self, RecordingStoreError> where P: AsRef<Path> {
|
pub async fn load<P>(path: P) -> Result<Self, RecordingStoreError>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
let path: PathBuf = path.as_ref().to_path_buf();
|
let path: PathBuf = path.as_ref().to_path_buf();
|
||||||
let manifest = RecordingStore::read_manifest(&path).await?;
|
let manifest = RecordingStore::read_manifest(&path).await?;
|
||||||
Ok(RecordingStore {
|
Ok(RecordingStore {
|
||||||
@@ -91,26 +104,38 @@ impl RecordingStore {
|
|||||||
|
|
||||||
// Creates a new RecordingStore at the given path. This involves creating a dir
|
// Creates a new RecordingStore at the given path. This involves creating a dir
|
||||||
// and writing an empty manifest.
|
// and writing an empty manifest.
|
||||||
pub async fn create<P>(path: P) -> Result<Self, RecordingStoreError> where P: AsRef<Path> {
|
pub async fn create<P>(path: P) -> Result<Self, RecordingStoreError>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
let manifest_path = path.as_ref().join("manifest.toml");
|
let manifest_path = path.as_ref().join("manifest.toml");
|
||||||
fs::create_dir_all(&path).await
|
fs::create_dir_all(&path)
|
||||||
|
.await
|
||||||
.map_err(RecordingStoreError::OpenDirError)?;
|
.map_err(RecordingStoreError::OpenDirError)?;
|
||||||
let mut manifest_file = File::create(&manifest_path).await
|
let mut manifest_file = File::create(&manifest_path)
|
||||||
|
.await
|
||||||
.map_err(RecordingStoreError::WriteManifestError)?;
|
.map_err(RecordingStoreError::WriteManifestError)?;
|
||||||
let empty_manifest = Manifest { entries: Vec::new() };
|
let empty_manifest = Manifest {
|
||||||
let empty_manifest_contents = toml::to_string_pretty(&empty_manifest)
|
entries: Vec::new(),
|
||||||
.expect("failed to serialize manifest");
|
};
|
||||||
manifest_file.write_all(empty_manifest_contents.as_bytes()).await
|
let empty_manifest_contents =
|
||||||
|
toml::to_string_pretty(&empty_manifest).expect("failed to serialize manifest");
|
||||||
|
manifest_file
|
||||||
|
.write_all(empty_manifest_contents.as_bytes())
|
||||||
|
.await
|
||||||
.map_err(RecordingStoreError::WriteManifestError)?;
|
.map_err(RecordingStoreError::WriteManifestError)?;
|
||||||
RecordingStore::load(path).await
|
RecordingStore::load(path).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn read_manifest<P>(path: P) -> Result<Manifest, RecordingStoreError> where P: AsRef<Path> {
|
async fn read_manifest<P>(path: P) -> Result<Manifest, RecordingStoreError>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
let manifest_path = path.as_ref().join("manifest.toml");
|
let manifest_path = path.as_ref().join("manifest.toml");
|
||||||
let file_contents = fs::read_to_string(&manifest_path).await
|
let file_contents = fs::read_to_string(&manifest_path)
|
||||||
|
.await
|
||||||
.map_err(RecordingStoreError::ReadManifestError)?;
|
.map_err(RecordingStoreError::ReadManifestError)?;
|
||||||
toml::from_str(&file_contents)
|
toml::from_str(&file_contents).map_err(RecordingStoreError::ParseManifestError)
|
||||||
.map_err(RecordingStoreError::ParseManifestError)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Closes the current entry (if needed), creates a new entry based on the
|
// Closes the current entry (if needed), creates a new entry based on the
|
||||||
@@ -126,13 +151,15 @@ impl RecordingStore {
|
|||||||
let qmdl_file = File::options()
|
let qmdl_file = File::options()
|
||||||
.create(true)
|
.create(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
.open(&qmdl_filepath).await
|
.open(&qmdl_filepath)
|
||||||
|
.await
|
||||||
.map_err(RecordingStoreError::CreateFileError)?;
|
.map_err(RecordingStoreError::CreateFileError)?;
|
||||||
let analysis_filepath = new_entry.get_analysis_filepath(&self.path);
|
let analysis_filepath = new_entry.get_analysis_filepath(&self.path);
|
||||||
let analysis_file = File::options()
|
let analysis_file = File::options()
|
||||||
.create(true)
|
.create(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
.open(&analysis_filepath).await
|
.open(&analysis_filepath)
|
||||||
|
.await
|
||||||
.map_err(RecordingStoreError::CreateFileError)?;
|
.map_err(RecordingStoreError::CreateFileError)?;
|
||||||
self.manifest.entries.push(new_entry);
|
self.manifest.entries.push(new_entry);
|
||||||
self.current_entry = Some(self.manifest.entries.len() - 1);
|
self.current_entry = Some(self.manifest.entries.len() - 1);
|
||||||
@@ -141,37 +168,71 @@ impl RecordingStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Returns the corresponding QMDL file for a given entry
|
// Returns the corresponding QMDL file for a given entry
|
||||||
pub async fn open_entry_qmdl(&self, entry: &ManifestEntry) -> Result<File, RecordingStoreError> {
|
pub async fn open_entry_qmdl(
|
||||||
File::open(entry.get_qmdl_filepath(&self.path)).await
|
&self,
|
||||||
|
entry_index: usize,
|
||||||
|
) -> Result<File, RecordingStoreError> {
|
||||||
|
let entry = &self.manifest.entries[entry_index];
|
||||||
|
File::open(entry.get_qmdl_filepath(&self.path))
|
||||||
|
.await
|
||||||
.map_err(RecordingStoreError::ReadFileError)
|
.map_err(RecordingStoreError::ReadFileError)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the corresponding QMDL file for a given entry
|
// Returns the corresponding QMDL file for a given entry
|
||||||
pub async fn open_entry_analysis(&self, entry: &ManifestEntry) -> Result<File, RecordingStoreError> {
|
pub async fn open_entry_analysis(
|
||||||
File::open(entry.get_analysis_filepath(&self.path)).await
|
&self,
|
||||||
|
entry_index: usize,
|
||||||
|
) -> Result<File, RecordingStoreError> {
|
||||||
|
let entry = &self.manifest.entries[entry_index];
|
||||||
|
File::open(entry.get_analysis_filepath(&self.path))
|
||||||
|
.await
|
||||||
.map_err(RecordingStoreError::ReadFileError)
|
.map_err(RecordingStoreError::ReadFileError)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn clear_and_open_entry_analysis(
|
||||||
|
&mut self,
|
||||||
|
entry_index: usize,
|
||||||
|
) -> Result<File, RecordingStoreError> {
|
||||||
|
let entry = &self.manifest.entries[entry_index];
|
||||||
|
let file = OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.truncate(true)
|
||||||
|
.open(entry.get_analysis_filepath(&self.path))
|
||||||
|
.await
|
||||||
|
.map_err(RecordingStoreError::ReadFileError)?;
|
||||||
|
self.update_entry_analysis_size(entry_index, 0)
|
||||||
|
.await?;
|
||||||
|
Ok(file)
|
||||||
|
}
|
||||||
|
|
||||||
// Unsets the current entry
|
// Unsets the current entry
|
||||||
pub async fn close_current_entry(&mut self) -> Result<(), RecordingStoreError> {
|
pub async fn close_current_entry(&mut self) -> Result<(), RecordingStoreError> {
|
||||||
match self.current_entry {
|
match self.current_entry {
|
||||||
Some(_) => {
|
Some(_) => {
|
||||||
self.current_entry = None;
|
self.current_entry = None;
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}
|
||||||
None => Err(RecordingStoreError::NoCurrentEntry)
|
None => Err(RecordingStoreError::NoCurrentEntry),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sets the given entry's size and updates the last_message_time to now, updating the manifest
|
// Sets the given entry's size and updates the last_message_time to now, updating the manifest
|
||||||
pub async fn update_entry_qmdl_size(&mut self, entry_index: usize, size_bytes: usize) -> Result<(), RecordingStoreError> {
|
pub async fn update_entry_qmdl_size(
|
||||||
|
&mut self,
|
||||||
|
entry_index: usize,
|
||||||
|
size_bytes: usize,
|
||||||
|
) -> Result<(), RecordingStoreError> {
|
||||||
self.manifest.entries[entry_index].qmdl_size_bytes = size_bytes;
|
self.manifest.entries[entry_index].qmdl_size_bytes = size_bytes;
|
||||||
self.manifest.entries[entry_index].last_message_time = Some(Local::now());
|
self.manifest.entries[entry_index].last_message_time = Some(Local::now());
|
||||||
self.write_manifest().await
|
self.write_manifest().await
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sets the given entry's analysis file size
|
// Sets the given entry's analysis file size
|
||||||
pub async fn update_entry_analysis_size(&mut self, entry_index: usize, size_bytes: usize) -> Result<(), RecordingStoreError> {
|
pub async fn update_entry_analysis_size(
|
||||||
|
&mut self,
|
||||||
|
entry_index: usize,
|
||||||
|
size_bytes: usize,
|
||||||
|
) -> Result<(), RecordingStoreError> {
|
||||||
self.manifest.entries[entry_index].analysis_size_bytes = size_bytes;
|
self.manifest.entries[entry_index].analysis_size_bytes = size_bytes;
|
||||||
self.write_manifest().await
|
self.write_manifest().await
|
||||||
}
|
}
|
||||||
@@ -179,32 +240,37 @@ impl RecordingStore {
|
|||||||
async fn write_manifest(&mut self) -> Result<(), RecordingStoreError> {
|
async fn write_manifest(&mut self) -> Result<(), RecordingStoreError> {
|
||||||
let mut manifest_file = File::options()
|
let mut manifest_file = File::options()
|
||||||
.write(true)
|
.write(true)
|
||||||
.open(self.path.join("manifest.toml")).await
|
.open(self.path.join("manifest.toml"))
|
||||||
|
.await
|
||||||
.map_err(RecordingStoreError::WriteManifestError)?;
|
.map_err(RecordingStoreError::WriteManifestError)?;
|
||||||
let manifest_contents = toml::to_string_pretty(&self.manifest)
|
let manifest_contents =
|
||||||
.expect("failed to serialize manifest");
|
toml::to_string_pretty(&self.manifest).expect("failed to serialize manifest");
|
||||||
manifest_file.write_all(manifest_contents.as_bytes()).await
|
manifest_file
|
||||||
|
.write_all(manifest_contents.as_bytes())
|
||||||
|
.await
|
||||||
.map_err(RecordingStoreError::WriteManifestError)?;
|
.map_err(RecordingStoreError::WriteManifestError)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finds an entry by filename
|
// Finds an entry by filename
|
||||||
pub fn entry_for_name(&self, name: &str) -> Option<ManifestEntry> {
|
pub fn entry_for_name(&self, name: &str) -> Option<(usize, &ManifestEntry)> {
|
||||||
self.manifest.entries.iter()
|
let entry_index = self.manifest
|
||||||
.find(|entry| entry.name == name)
|
.entries
|
||||||
.cloned()
|
.iter()
|
||||||
|
.position(|entry| entry.name == name)?;
|
||||||
|
Some((entry_index, &self.manifest.entries[entry_index]))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_current_entry(&self) -> Option<&ManifestEntry> {
|
pub fn get_current_entry(&self) -> Option<(usize, &ManifestEntry)> {
|
||||||
let entry_index = self.current_entry?;
|
let entry_index = self.current_entry?;
|
||||||
self.manifest.entries.get(entry_index)
|
Some((entry_index, &self.manifest.entries[entry_index]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use tempfile::{TempDir, Builder};
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use tempfile::{Builder, TempDir};
|
||||||
|
|
||||||
fn make_temp_dir() -> TempDir {
|
fn make_temp_dir() -> TempDir {
|
||||||
Builder::new().prefix("qmdl_store_test").tempdir().unwrap()
|
Builder::new().prefix("qmdl_store_test").tempdir().unwrap()
|
||||||
@@ -226,17 +292,33 @@ mod tests {
|
|||||||
let mut store = RecordingStore::create(dir.path()).await.unwrap();
|
let mut store = RecordingStore::create(dir.path()).await.unwrap();
|
||||||
let _ = store.new_entry().await.unwrap();
|
let _ = store.new_entry().await.unwrap();
|
||||||
let entry_index = store.current_entry.unwrap();
|
let entry_index = store.current_entry.unwrap();
|
||||||
assert_eq!(RecordingStore::read_manifest(dir.path()).await.unwrap(), store.manifest);
|
assert_eq!(
|
||||||
assert!(store.manifest.entries[entry_index].last_message_time.is_none());
|
RecordingStore::read_manifest(dir.path()).await.unwrap(),
|
||||||
|
store.manifest
|
||||||
|
);
|
||||||
|
assert!(store.manifest.entries[entry_index]
|
||||||
|
.last_message_time
|
||||||
|
.is_none());
|
||||||
|
|
||||||
store.update_entry_qmdl_size(entry_index, 1000).await.unwrap();
|
store
|
||||||
let entry = store.entry_for_name(&store.manifest.entries[entry_index].name).unwrap();
|
.update_entry_qmdl_size(entry_index, 1000)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let (entry_index, entry) = store
|
||||||
|
.entry_for_name(&store.manifest.entries[entry_index].name)
|
||||||
|
.unwrap();
|
||||||
assert!(entry.last_message_time.is_some());
|
assert!(entry.last_message_time.is_some());
|
||||||
assert_eq!(store.manifest.entries[entry_index].qmdl_size_bytes, 1000);
|
assert_eq!(store.manifest.entries[entry_index].qmdl_size_bytes, 1000);
|
||||||
assert_eq!(RecordingStore::read_manifest(dir.path()).await.unwrap(), store.manifest);
|
assert_eq!(
|
||||||
|
RecordingStore::read_manifest(dir.path()).await.unwrap(),
|
||||||
|
store.manifest
|
||||||
|
);
|
||||||
|
|
||||||
store.close_current_entry().await.unwrap();
|
store.close_current_entry().await.unwrap();
|
||||||
assert!(matches!(store.close_current_entry().await, Err(RecordingStoreError::NoCurrentEntry)));
|
assert!(matches!(
|
||||||
|
store.close_current_entry().await,
|
||||||
|
Err(RecordingStoreError::NoCurrentEntry)
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
+39
-5
@@ -4,6 +4,7 @@ use axum::extract::State;
|
|||||||
use axum::http::{StatusCode, HeaderValue};
|
use axum::http::{StatusCode, HeaderValue};
|
||||||
use axum::response::{Response, IntoResponse};
|
use axum::response::{Response, IntoResponse};
|
||||||
use axum::extract::Path;
|
use axum::extract::Path;
|
||||||
|
use tokio::fs::File;
|
||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::AsyncReadExt;
|
||||||
use tokio::sync::mpsc::Sender;
|
use tokio::sync::mpsc::Sender;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -11,20 +12,24 @@ use tokio::sync::RwLock;
|
|||||||
use tokio_util::io::ReaderStream;
|
use tokio_util::io::ReaderStream;
|
||||||
use include_dir::{include_dir, Dir};
|
use include_dir::{include_dir, Dir};
|
||||||
|
|
||||||
use crate::DiagDeviceCtrlMessage;
|
use crate::{framebuffer, DiagDeviceCtrlMessage};
|
||||||
|
use crate::analysis::{AnalysisCtrlMessage, AnalysisStatus};
|
||||||
use crate::qmdl_store::RecordingStore;
|
use crate::qmdl_store::RecordingStore;
|
||||||
|
|
||||||
pub struct ServerState {
|
pub struct ServerState {
|
||||||
pub qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
pub qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
||||||
pub diag_device_ctrl_sender: Sender<DiagDeviceCtrlMessage>,
|
pub diag_device_ctrl_sender: Sender<DiagDeviceCtrlMessage>,
|
||||||
pub readonly_mode: bool
|
pub ui_update_sender: Sender<framebuffer::DisplayState>,
|
||||||
|
pub analysis_status_lock: Arc<RwLock<AnalysisStatus>>,
|
||||||
|
pub analysis_sender: Sender<AnalysisCtrlMessage>,
|
||||||
|
pub debug_mode: bool
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_qmdl(State(state): State<Arc<ServerState>>, Path(qmdl_name): Path<String>) -> Result<Response, (StatusCode, String)> {
|
pub async fn get_qmdl(State(state): State<Arc<ServerState>>, Path(qmdl_name): Path<String>) -> Result<Response, (StatusCode, String)> {
|
||||||
let qmdl_store = state.qmdl_store_lock.read().await;
|
let qmdl_store = state.qmdl_store_lock.read().await;
|
||||||
let entry = qmdl_store.entry_for_name(&qmdl_name)
|
let (entry_index, entry) = qmdl_store.entry_for_name(&qmdl_name)
|
||||||
.ok_or((StatusCode::NOT_FOUND, format!("couldn't find qmdl file with name {}", qmdl_name)))?;
|
.ok_or((StatusCode::NOT_FOUND, format!("couldn't find qmdl file with name {}", qmdl_name)))?;
|
||||||
let qmdl_file = qmdl_store.open_entry_qmdl(&entry).await
|
let qmdl_file = qmdl_store.open_entry_qmdl(entry_index).await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("error opening QMDL file: {}", e)))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("error opening QMDL file: {}", e)))?;
|
||||||
let limited_qmdl_file = qmdl_file.take(entry.qmdl_size_bytes as u64);
|
let limited_qmdl_file = qmdl_file.take(entry.qmdl_size_bytes as u64);
|
||||||
let qmdl_stream = ReaderStream::new(limited_qmdl_file);
|
let qmdl_stream = ReaderStream::new(limited_qmdl_file);
|
||||||
@@ -37,10 +42,39 @@ pub async fn get_qmdl(State(state): State<Arc<ServerState>>, Path(qmdl_name): Pa
|
|||||||
// Bundles the server's static files (html/css/js) into the binary for easy distribution
|
// 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");
|
static STATIC_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/static");
|
||||||
|
|
||||||
pub async fn serve_static(Path(path): Path<String>) -> impl IntoResponse {
|
pub async fn serve_static(State(state): State<Arc<ServerState>>, Path(path): Path<String>) -> impl IntoResponse {
|
||||||
let path = path.trim_start_matches('/');
|
let path = path.trim_start_matches('/');
|
||||||
let mime_type = mime_guess::from_path(path).first_or_text_plain();
|
let mime_type = mime_guess::from_path(path).first_or_text_plain();
|
||||||
|
|
||||||
|
// if we're in debug mode, return the files from the build directory so we
|
||||||
|
// don't have to rebuild every time the JS/HTML change
|
||||||
|
if state.debug_mode {
|
||||||
|
let mut build_path = std::path::PathBuf::new();
|
||||||
|
build_path.push("bin");
|
||||||
|
build_path.push("static");
|
||||||
|
for part in path.split("/") {
|
||||||
|
build_path.push(part);
|
||||||
|
}
|
||||||
|
return match File::open(build_path).await {
|
||||||
|
Ok(mut file) => {
|
||||||
|
let mut body = String::new();
|
||||||
|
file.read_to_string(&mut body).await.expect("failed to read file");
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.header(
|
||||||
|
header::CONTENT_TYPE,
|
||||||
|
HeaderValue::from_str(mime_type.as_ref()).unwrap(),
|
||||||
|
)
|
||||||
|
.body(Body::from(body))
|
||||||
|
.unwrap()
|
||||||
|
},
|
||||||
|
Err(_) => Response::builder()
|
||||||
|
.status(StatusCode::NOT_FOUND)
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
match STATIC_DIR.get_file(path) {
|
match STATIC_DIR.get_file(path) {
|
||||||
None => Response::builder()
|
None => Response::builder()
|
||||||
.status(StatusCode::NOT_FOUND)
|
.status(StatusCode::NOT_FOUND)
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ th[scope='row'] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tr.current {
|
tr.current {
|
||||||
|
background-color: #53fe7b;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.warning {
|
||||||
background-color: #fe537b;
|
background-color: #fe537b;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,15 +27,16 @@
|
|||||||
<th scope="col">Size (bytes)</th>
|
<th scope="col">Size (bytes)</th>
|
||||||
<th scope="col">PCAP</th>
|
<th scope="col">PCAP</th>
|
||||||
<th scope="col">QMDL</th>
|
<th scope="col">QMDL</th>
|
||||||
|
<th scope="col">Analysis Result</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
</table>
|
</table>
|
||||||
<div>
|
<div>
|
||||||
<h3>System stats</h3>
|
<h3>Live System stats</h3>
|
||||||
<pre id="system-stats">Loading...</pre>
|
<pre id="system-stats">Loading...</pre>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3>Analysis Report</h3>
|
<h3>Analysis Report of Current Capture</h3>
|
||||||
<pre id="analysis-report">Loading...</pre>
|
<pre id="analysis-report">Loading...</pre>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+119
-19
@@ -1,16 +1,100 @@
|
|||||||
|
const STATUS_RUNNING = 'running';
|
||||||
|
const STATUS_QUEUED = 'queued';
|
||||||
|
const STATUS_NEEDS_UPDATE = 'needs-update';
|
||||||
|
const STATUS_COMPLETE = 'complete';
|
||||||
|
|
||||||
async function populateDivs() {
|
async function populateDivs() {
|
||||||
const systemStats = await getSystemStats();
|
const systemStats = await getSystemStats();
|
||||||
const systemStatsDiv = document.getElementById('system-stats');
|
const systemStatsDiv = document.getElementById('system-stats');
|
||||||
systemStatsDiv.innerHTML = JSON.stringify(systemStats, null, 2);
|
systemStatsDiv.innerHTML = JSON.stringify(systemStats, null, 2);
|
||||||
|
|
||||||
const analysisReport = await getAnalysisReport();
|
|
||||||
const analysisReportDiv = document.getElementById('analysis-report');
|
const analysisReportDiv = document.getElementById('analysis-report');
|
||||||
analysisReportDiv.innerHTML = JSON.stringify(analysisReport, null, 2);
|
try {
|
||||||
|
const analysisReport = await getAnalysisReport('live');
|
||||||
|
analysisReportDiv.innerHTML = JSON.stringify(analysisReport, null, 2);
|
||||||
|
} catch (e) {
|
||||||
|
analysisReportDiv.innerHTML = e.toString();
|
||||||
|
}
|
||||||
|
|
||||||
const qmdlManifest = await getQmdlManifest();
|
const qmdlManifest = await getQmdlManifest();
|
||||||
|
await updateAnalysisStatus(qmdlManifest);
|
||||||
|
await updateAnalysisResults(qmdlManifest);
|
||||||
updateQmdlManifestTable(qmdlManifest);
|
updateQmdlManifestTable(qmdlManifest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setStatus(qmdlManifest, name, status) {
|
||||||
|
// ignore qmdlManifest.current_entry, it's always running
|
||||||
|
for (const entry of qmdlManifest.entries) {
|
||||||
|
if (entry.name === name) {
|
||||||
|
entry['status'] = status;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateAnalysisStatus(qmdlManifest) {
|
||||||
|
const status = JSON.parse(await req('GET', '/api/analysis'));
|
||||||
|
if (status.running) {
|
||||||
|
setStatus(qmdlManifest, status.running, STATUS_RUNNING);
|
||||||
|
}
|
||||||
|
for (const queued in status.queued) {
|
||||||
|
setStatus(qmdlManifest, queued, STATUS_QUEUED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseNewlineDelimitedJSON(inputStr) {
|
||||||
|
const lines = inputStr.split('\n');
|
||||||
|
const result = [];
|
||||||
|
let currentLine = '';
|
||||||
|
while (lines.length > 0) {
|
||||||
|
currentLine += lines.shift();
|
||||||
|
try {
|
||||||
|
const entry = JSON.parse(currentLine);
|
||||||
|
result.push(entry);
|
||||||
|
currentLine = '';
|
||||||
|
// if this chunk wasn't valid JSON, there was an escaped newline in the
|
||||||
|
// JSON line, so simply continue to the next one
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateEntryAnalysisResult(entry) {
|
||||||
|
entry.analysis = {
|
||||||
|
warnings: [],
|
||||||
|
};
|
||||||
|
const report = parseNewlineDelimitedJSON(await req('GET', `/api/analysis-report/${entry.name}`));
|
||||||
|
for (const row of report) {
|
||||||
|
if (row["analysis"]) {
|
||||||
|
const timestamp = new Date(row["timestamp"]);
|
||||||
|
const analysis = row["analysis"];
|
||||||
|
for (const warning of analysis) {
|
||||||
|
entry.analysis.warnings.push({
|
||||||
|
timestamp,
|
||||||
|
warning,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (entry.analysis.warnings.length === 0) {
|
||||||
|
entry.analysis_result = `0 warnings!`;
|
||||||
|
} else {
|
||||||
|
entry.analysis_result = `!!! ${entry.analysis.warnings.length} warnings !!!`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateAnalysisResults(qmdlManifest) {
|
||||||
|
if (qmdlManifest.current_entry) {
|
||||||
|
await updateEntryAnalysisResult(qmdlManifest.current_entry);
|
||||||
|
}
|
||||||
|
for (const entry of qmdlManifest.entries) {
|
||||||
|
if (entry.status === STATUS_NEEDS_UPDATE) {
|
||||||
|
await updateEntryAnalysisResult(entry);
|
||||||
|
entry.status = STATUS_COMPLETE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateQmdlManifestTable(manifest) {
|
function updateQmdlManifestTable(manifest) {
|
||||||
const table = document.getElementById('qmdl-manifest-table');
|
const table = document.getElementById('qmdl-manifest-table');
|
||||||
const numRows = table.rows.length;
|
const numRows = table.rows.length;
|
||||||
@@ -18,43 +102,55 @@ function updateQmdlManifestTable(manifest) {
|
|||||||
table.deleteRow(1);
|
table.deleteRow(1);
|
||||||
}
|
}
|
||||||
if (manifest.current_entry) {
|
if (manifest.current_entry) {
|
||||||
const row = createEntryRow(manifest.current_entry);
|
const row = createEntryRow(manifest.current_entry, true);
|
||||||
row.classList.add('current');
|
row.classList.add('current');
|
||||||
table.appendChild(row)
|
table.appendChild(row)
|
||||||
}
|
}
|
||||||
for (let entry of manifest.entries) {
|
for (let entry of manifest.entries) {
|
||||||
table.appendChild(createEntryRow(entry));
|
table.appendChild(createEntryRow(entry), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEntryRow(entry) {
|
function createLink(uri, text) {
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = uri;
|
||||||
|
link.innerText = text;
|
||||||
|
return link;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEntryRow(entry, isCurrent) {
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
const name = document.createElement('th');
|
const name = document.createElement('th');
|
||||||
name.scope = 'row';
|
name.scope = 'row';
|
||||||
name.innerText = entry.name;
|
name.innerText = entry.name;
|
||||||
row.appendChild(name);
|
row.appendChild(name);
|
||||||
|
|
||||||
for (const key of ['start_time', 'last_message_time', 'qmdl_size_bytes']) {
|
for (const key of ['start_time', 'last_message_time', 'qmdl_size_bytes']) {
|
||||||
const td = document.createElement('td');
|
const td = document.createElement('td');
|
||||||
td.innerText = entry[key];
|
td.innerText = entry[key];
|
||||||
row.appendChild(td);
|
row.appendChild(td);
|
||||||
}
|
}
|
||||||
const pcap_td = document.createElement('td');
|
|
||||||
const pcap_link = document.createElement('a');
|
const pcapTd = document.createElement('td');
|
||||||
pcap_link.href = `/api/pcap/${entry.name}`;
|
pcapTd.appendChild(createLink(`/api/pcap/${entry.name}`, 'pcap'));
|
||||||
pcap_link.innerText = 'pcap';
|
row.appendChild(pcapTd);
|
||||||
pcap_td.appendChild(pcap_link);
|
|
||||||
row.appendChild(pcap_td);
|
const qmdlTd = document.createElement('td');
|
||||||
const qmdl_td = document.createElement('td');
|
qmdlTd.appendChild(createLink(`/api/qmdl/${entry.name}`, 'qmdl'));
|
||||||
const qmdl_link = document.createElement('a');
|
row.appendChild(qmdlTd);
|
||||||
qmdl_link.href = `/api/qmdl/${entry.name}`;
|
|
||||||
qmdl_link.innerText = 'qmdl';
|
const analysisResult = document.createElement('td');
|
||||||
qmdl_td.appendChild(qmdl_link);
|
analysisResult.innerText = entry.analysis_result;
|
||||||
row.appendChild(qmdl_td);
|
if (entry.analysis.warnings.length > 0) {
|
||||||
|
row.classList.add("warning");
|
||||||
|
}
|
||||||
|
row.appendChild(analysisResult);
|
||||||
|
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAnalysisReport() {
|
async function getAnalysisReport(name) {
|
||||||
const rows = await req('GET', '/api/analysis-report');
|
const rows = await req('GET', `/api/analysis-report/${name}`);
|
||||||
return rows.split('\n')
|
return rows.split('\n')
|
||||||
.filter(row => row.length > 0)
|
.filter(row => row.length > 0)
|
||||||
.map(row => JSON.parse(row));
|
.map(row => JSON.parse(row));
|
||||||
@@ -67,6 +163,8 @@ async function getSystemStats() {
|
|||||||
async function getQmdlManifest() {
|
async function getQmdlManifest() {
|
||||||
const manifest = JSON.parse(await req('GET', '/api/qmdl-manifest'));
|
const manifest = JSON.parse(await req('GET', '/api/qmdl-manifest'));
|
||||||
if (manifest.current_entry) {
|
if (manifest.current_entry) {
|
||||||
|
manifest.current_entry.status = STATUS_NEEDS_UPDATE;
|
||||||
|
manifest.current_entry.analysis_result = 'Waiting...';
|
||||||
manifest.current_entry.start_time = new Date(manifest.current_entry.start_time);
|
manifest.current_entry.start_time = new Date(manifest.current_entry.start_time);
|
||||||
if (manifest.current_entry.last_message_time === undefined) {
|
if (manifest.current_entry.last_message_time === undefined) {
|
||||||
manifest.current_entry.last_message_time = "N/A";
|
manifest.current_entry.last_message_time = "N/A";
|
||||||
@@ -75,6 +173,8 @@ async function getQmdlManifest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (entry of manifest.entries) {
|
for (entry of manifest.entries) {
|
||||||
|
entry.status = STATUS_NEEDS_UPDATE;
|
||||||
|
entry.analysis_result = 'Waiting...';
|
||||||
entry.start_time = new Date(entry.start_time);
|
entry.start_time = new Date(entry.start_time);
|
||||||
entry.last_message_time = new Date(entry.last_message_time);
|
entry.last_message_time = new Date(entry.last_message_time);
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
-1
@@ -1,7 +1,6 @@
|
|||||||
# cat config.toml
|
# cat config.toml
|
||||||
qmdl_store_path = "/data/rayhunter/qmdl"
|
qmdl_store_path = "/data/rayhunter/qmdl"
|
||||||
port = 8080
|
port = 8080
|
||||||
readonly_mode = false
|
|
||||||
# UI Levels:
|
# UI Levels:
|
||||||
# 0 = invisible mode, no indicator that rayhunter is running
|
# 0 = invisible mode, no indicator that rayhunter is running
|
||||||
# 1 = Subtle mode, display a green line at the top of the screen when rayhunter is running
|
# 1 = Subtle mode, display a green line at the top of the screen when rayhunter is running
|
||||||
|
|||||||
Vendored
+30
-26
@@ -1,25 +1,21 @@
|
|||||||
#!/bin/env bash
|
#!/usr/bin/env bash
|
||||||
install() {
|
install() {
|
||||||
if [[ -z "${SERIAL_PATH}" ]]; then
|
if [[ -z "${SERIAL_PATH}" ]]; then
|
||||||
echo "SERIAL_PATH not set, did you run this from install-linux.sh or install-mac.sh?"
|
echo "\$SERIAL_PATH not set, did you run this from install-linux.sh or install-mac.sh?"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ -z "${ADB}" ]]; then
|
||||||
|
echo "\$ADB not set, did you run this from install-linux.sh or install-mac.sh?"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
check_adb
|
|
||||||
force_debug_mode
|
force_debug_mode
|
||||||
setup_rootshell
|
setup_rootshell
|
||||||
setup_rayhunter
|
setup_rayhunter
|
||||||
test_rayhunter
|
test_rayhunter
|
||||||
}
|
}
|
||||||
|
|
||||||
check_adb() {
|
|
||||||
if ! command -v adb &> /dev/null
|
|
||||||
then
|
|
||||||
echo "adb not found, please ensure it's installed or check the README.md"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
force_debug_mode() {
|
force_debug_mode() {
|
||||||
|
echo "Using adb at $ADB"
|
||||||
echo "Force a switch into the debug mode to enable ADB"
|
echo "Force a switch into the debug mode to enable ADB"
|
||||||
"$SERIAL_PATH" --root
|
"$SERIAL_PATH" --root
|
||||||
echo -n "adb enabled, waiting for reboot..."
|
echo -n "adb enabled, waiting for reboot..."
|
||||||
@@ -31,14 +27,14 @@ force_debug_mode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
wait_for_atfwd_daemon() {
|
wait_for_atfwd_daemon() {
|
||||||
until [ -n "$(adb shell 'pgrep atfwd_daemon')" ]
|
until [ -n "$(_adb_shell 'pgrep atfwd_daemon')" ]
|
||||||
do
|
do
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
wait_for_adb_shell() {
|
wait_for_adb_shell() {
|
||||||
until adb shell true 2> /dev/null
|
until _adb_shell true 2> /dev/null
|
||||||
do
|
do
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
@@ -46,34 +42,42 @@ wait_for_adb_shell() {
|
|||||||
|
|
||||||
setup_rootshell() {
|
setup_rootshell() {
|
||||||
_adb_push rootshell /tmp/
|
_adb_push rootshell /tmp/
|
||||||
"$SERIAL_PATH" "AT+SYSCMD=cp /tmp/rootshell /bin/rootshell"
|
_at_syscmd "cp /tmp/rootshell /bin/rootshell"
|
||||||
sleep 1
|
sleep 1
|
||||||
"$SERIAL_PATH" "AT+SYSCMD=chown root /bin/rootshell"
|
_at_syscmd "chown root /bin/rootshell"
|
||||||
sleep 1
|
sleep 1
|
||||||
"$SERIAL_PATH" "AT+SYSCMD=chmod 4755 /bin/rootshell"
|
_at_syscmd "chmod 4755 /bin/rootshell"
|
||||||
adb shell /bin/rootshell -c id
|
_adb_shell '/bin/rootshell -c id'
|
||||||
echo "we have root!"
|
echo "we have root!"
|
||||||
}
|
}
|
||||||
|
|
||||||
_adb_push() {
|
_adb_push() {
|
||||||
adb push "$(dirname "$0")/$1" "$2"
|
"$ADB" push "$(dirname "$0")/$1" "$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
_adb_shell() {
|
||||||
|
"$ADB" shell "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
_at_syscmd() {
|
||||||
|
"$SERIAL_PATH" "AT+SYSCMD=$1"
|
||||||
}
|
}
|
||||||
|
|
||||||
setup_rayhunter() {
|
setup_rayhunter() {
|
||||||
adb shell '/bin/rootshell -c "mkdir -p /data/rayhunter"'
|
_at_syscmd "mkdir -p /data/rayhunter"
|
||||||
_adb_push config.toml.example /data/rayhunter/config.toml
|
_adb_push config.toml.example /data/rayhunter/config.toml
|
||||||
_adb_push rayhunter-daemon /data/rayhunter/
|
_adb_push rayhunter-daemon /data/rayhunter/
|
||||||
_adb_push scripts/rayhunter_daemon /tmp/rayhunter_daemon
|
_adb_push scripts/rayhunter_daemon /tmp/rayhunter_daemon
|
||||||
_adb_push scripts/misc-daemon /tmp/misc-daemon
|
_adb_push scripts/misc-daemon /tmp/misc-daemon
|
||||||
adb shell '/bin/rootshell -c "cp /tmp/rayhunter_daemon /etc/init.d/rayhunter_daemon"'
|
_at_syscmd "cp /tmp/rayhunter_daemon /etc/init.d/rayhunter_daemon"
|
||||||
adb shell '/bin/rootshell -c "cp /tmp/misc-daemon /etc/init.d/misc-daemon"'
|
_at_syscmd "cp /tmp/misc-daemon /etc/init.d/misc-daemon"
|
||||||
adb shell '/bin/rootshell -c "chmod 755 /etc/init.d/rayhunter_daemon"'
|
_at_syscmd "chmod 755 /etc/init.d/rayhunter_daemon"
|
||||||
adb shell '/bin/rootshell -c "chmod 755 /etc/init.d/misc-daemon"'
|
_at_syscmd "chmod 755 /etc/init.d/misc-daemon"
|
||||||
echo -n "waiting for reboot..."
|
echo -n "waiting for reboot..."
|
||||||
adb shell '/bin/rootshell -c reboot'
|
_at_syscmd reboot
|
||||||
|
|
||||||
# first wait for shutdown (it can take ~10s)
|
# first wait for shutdown (it can take ~10s)
|
||||||
until ! adb shell true 2> /dev/null
|
until ! _adb_shell true 2> /dev/null
|
||||||
do
|
do
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
@@ -86,7 +90,7 @@ setup_rayhunter() {
|
|||||||
|
|
||||||
test_rayhunter() {
|
test_rayhunter() {
|
||||||
URL="http://localhost:8080"
|
URL="http://localhost:8080"
|
||||||
adb forward tcp:8080 tcp:8080 > /dev/null
|
"$ADB" forward tcp:8080 tcp:8080 > /dev/null
|
||||||
echo -n "checking for rayhunter server..."
|
echo -n "checking for rayhunter server..."
|
||||||
|
|
||||||
SECONDS=0
|
SECONDS=0
|
||||||
|
|||||||
Vendored
+11
@@ -1,6 +1,17 @@
|
|||||||
#!/bin/env bash
|
#!/bin/env bash
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
if ! command -v adb &> /dev/null; then
|
||||||
|
if [ ! -d ./platform-tools ] ; then
|
||||||
|
echo "adb not found, downloading local copy"
|
||||||
|
curl -O "https://dl.google.com/android/repository/platform-tools-latest-linux.zip"
|
||||||
|
unzip platform-tools-latest-linux.zip
|
||||||
|
fi
|
||||||
|
export ADB="./platform-tools/adb"
|
||||||
|
else
|
||||||
|
export ADB=`which adb`
|
||||||
|
fi
|
||||||
|
|
||||||
export SERIAL_PATH="./serial-ubuntu-latest/serial"
|
export SERIAL_PATH="./serial-ubuntu-latest/serial"
|
||||||
. "$(dirname "$0")"/install-common.sh
|
. "$(dirname "$0")"/install-common.sh
|
||||||
install
|
install
|
||||||
|
|||||||
Vendored
+11
@@ -1,6 +1,17 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
if ! command -v adb &> /dev/null; then
|
||||||
|
if [ ! -d ./platform-tools ]; then
|
||||||
|
echo "adb not found, downloading local copy"
|
||||||
|
curl -O "https://dl.google.com/android/repository/platform-tools-latest-darwin.zip"
|
||||||
|
unzip platform-tools-latest-darwin.zip
|
||||||
|
fi
|
||||||
|
export ADB="./platform-tools/adb"
|
||||||
|
else
|
||||||
|
export ADB=`which adb`
|
||||||
|
fi
|
||||||
|
|
||||||
export SERIAL_PATH="./serial-macos-latest/serial"
|
export SERIAL_PATH="./serial-macos-latest/serial"
|
||||||
. "$(dirname "$0")"/install-common.sh
|
. "$(dirname "$0")"/install-common.sh
|
||||||
install
|
install
|
||||||
|
|||||||
@@ -60,19 +60,19 @@ pub trait Analyzer {
|
|||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
#[derive(Serialize, Debug)]
|
||||||
pub struct AnalyzerMetadata {
|
pub struct AnalyzerMetadata {
|
||||||
name: String,
|
pub name: String,
|
||||||
description: String,
|
pub description: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
#[derive(Serialize, Debug)]
|
||||||
pub struct ReportMetadata {
|
pub struct ReportMetadata {
|
||||||
analyzers: Vec<AnalyzerMetadata>,
|
pub analyzers: Vec<AnalyzerMetadata>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Debug, Clone)]
|
#[derive(Serialize, Debug, Clone)]
|
||||||
pub struct PacketAnalysis {
|
pub struct PacketAnalysis {
|
||||||
timestamp: DateTime<FixedOffset>,
|
pub timestamp: DateTime<FixedOffset>,
|
||||||
events: Vec<Option<Event>>,
|
pub events: Vec<Option<Event>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
#[derive(Serialize, Debug)]
|
||||||
@@ -86,6 +86,19 @@ impl AnalysisRow {
|
|||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.skipped_message_reasons.is_empty() && self.analysis.is_empty()
|
self.skipped_message_reasons.is_empty() && self.analysis.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn contains_warnings(&self) -> bool {
|
||||||
|
for analysis in &self.analysis {
|
||||||
|
for maybe_event in &analysis.events {
|
||||||
|
if let Some(event) = maybe_event {
|
||||||
|
if matches!(event.event_type, EventType::QualitativeWarning { .. }) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Harness {
|
pub struct Harness {
|
||||||
@@ -102,6 +115,7 @@ impl Harness {
|
|||||||
harness.add_analyzer(Box::new(LteSib6And7DowngradeAnalyzer{}));
|
harness.add_analyzer(Box::new(LteSib6And7DowngradeAnalyzer{}));
|
||||||
harness.add_analyzer(Box::new(ImsiProvidedAnalyzer{}));
|
harness.add_analyzer(Box::new(ImsiProvidedAnalyzer{}));
|
||||||
harness.add_analyzer(Box::new(NullCipherAnalyzer{}));
|
harness.add_analyzer(Box::new(NullCipherAnalyzer{}));
|
||||||
|
|
||||||
harness
|
harness
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +189,7 @@ impl Harness {
|
|||||||
|
|
||||||
pub fn get_metadata(&self) -> ReportMetadata {
|
pub fn get_metadata(&self) -> ReportMetadata {
|
||||||
let names = self.get_names();
|
let names = self.get_names();
|
||||||
let descriptions = self.get_names();
|
let descriptions = self.get_descriptions();
|
||||||
let mut analyzers = Vec::new();
|
let mut analyzers = Vec::new();
|
||||||
for (name, description) in names.iter().zip(descriptions.iter()) {
|
for (name, description) in names.iter().zip(descriptions.iter()) {
|
||||||
analyzers.push(AnalyzerMetadata {
|
analyzers.push(AnalyzerMetadata {
|
||||||
|
|||||||
@@ -63,11 +63,15 @@ const MEMORY_DEVICE_MODE: i32 = 2;
|
|||||||
const DIAG_IOCTL_REMOTE_DEV: u32 = 32;
|
const DIAG_IOCTL_REMOTE_DEV: u32 = 32;
|
||||||
#[cfg(target_arch = "x86_64")]
|
#[cfg(target_arch = "x86_64")]
|
||||||
const DIAG_IOCTL_REMOTE_DEV: u64 = 32;
|
const DIAG_IOCTL_REMOTE_DEV: u64 = 32;
|
||||||
|
#[cfg(target_arch = "aarch64")]
|
||||||
|
const DIAG_IOCTL_REMOTE_DEV: u64 = 32;
|
||||||
|
|
||||||
#[cfg(target_arch = "arm")]
|
#[cfg(target_arch = "arm")]
|
||||||
const DIAG_IOCTL_SWITCH_LOGGING: u32 = 7;
|
const DIAG_IOCTL_SWITCH_LOGGING: u32 = 7;
|
||||||
#[cfg(target_arch = "x86_64")]
|
#[cfg(target_arch = "x86_64")]
|
||||||
const DIAG_IOCTL_SWITCH_LOGGING: u64 = 7;
|
const DIAG_IOCTL_SWITCH_LOGGING: u64 = 7;
|
||||||
|
#[cfg(target_arch = "aarch64")]
|
||||||
|
const DIAG_IOCTL_SWITCH_LOGGING: u64 = 7;
|
||||||
|
|
||||||
pub struct DiagDevice {
|
pub struct DiagDevice {
|
||||||
file: File,
|
file: File,
|
||||||
|
|||||||
@@ -7,3 +7,6 @@ pub mod gsmtap;
|
|||||||
pub mod gsmtap_parser;
|
pub mod gsmtap_parser;
|
||||||
pub mod pcap;
|
pub mod pcap;
|
||||||
pub mod analysis;
|
pub mod analysis;
|
||||||
|
|
||||||
|
// re-export telcom_parser, since we use its types in our API
|
||||||
|
pub use telcom_parser;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
cargo build --release --target="armv7-unknown-linux-gnueabihf"
|
cargo build --release --target="armv7-unknown-linux-gnueabihf" #--features debug
|
||||||
adb push target/armv7-unknown-linux-gnueabihf/release/rayhunter-daemon /data/rayhunter/rayhunter-daemon
|
adb push target/armv7-unknown-linux-gnueabihf/release/rayhunter-daemon /data/rayhunter/rayhunter-daemon
|
||||||
adb shell '/bin/rootshell -c "/etc/init.d/rayhunter_daemon restart"'
|
adb shell '/bin/rootshell -c "/etc/init.d/rayhunter_daemon restart"'
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use std::process::Command;
|
|||||||
use std::os::unix::process::CommandExt;
|
use std::os::unix::process::CommandExt;
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
|
#[cfg(target_arch = "arm")]
|
||||||
use nix::unistd::Gid;
|
use nix::unistd::Gid;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@@ -14,11 +15,13 @@ fn main() {
|
|||||||
// Android's "paranoid network" feature restricts network access to
|
// Android's "paranoid network" feature restricts network access to
|
||||||
// processes in specific groups. More info here:
|
// processes in specific groups. More info here:
|
||||||
// https://www.elinux.org/Android_Security#Paranoid_network-ing
|
// https://www.elinux.org/Android_Security#Paranoid_network-ing
|
||||||
let gids = &[
|
#[cfg(target_arch = "arm")] {
|
||||||
Gid::from_raw(3003), // AID_INET
|
let gids = &[
|
||||||
Gid::from_raw(3004), // AID_NET_RAW
|
Gid::from_raw(3003), // AID_INET
|
||||||
];
|
Gid::from_raw(3004), // AID_NET_RAW
|
||||||
nix::unistd::setgroups(gids).expect("setgroups failed");
|
];
|
||||||
|
nix::unistd::setgroups(gids).expect("setgroups failed");
|
||||||
|
}
|
||||||
|
|
||||||
// discard argv[0]
|
// discard argv[0]
|
||||||
let _ = args.next();
|
let _ = args.next();
|
||||||
|
|||||||
+1
-2
@@ -6,5 +6,4 @@ edition = "2021"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rusb = "0.9.3"
|
rusb = { version = "0.9.3", features = ["vendored"] }
|
||||||
|
|
||||||
|
|||||||
+6
-2
@@ -113,12 +113,16 @@ fn enable_command_mode<T: UsbContext>(context: &mut T) {
|
|||||||
/// Get a handle and contet for the orbic device
|
/// Get a handle and contet for the orbic device
|
||||||
fn open_orbic<T: UsbContext>(context: &mut T) -> Option<DeviceHandle<T>> {
|
fn open_orbic<T: UsbContext>(context: &mut T) -> Option<DeviceHandle<T>> {
|
||||||
// Device after initial mode switch
|
// Device after initial mode switch
|
||||||
if let Some(handle) = open_device(context, 0x05c6, 0xf601) {
|
if let Some(mut handle) = open_device(context, 0x05c6, 0xf601) {
|
||||||
|
handle.set_auto_detach_kernel_driver(true).expect("set_auto_detach_kernel_driver failed");
|
||||||
|
handle.claim_interface(1).expect("claim_interface(1) failed");
|
||||||
return Some(handle);
|
return Some(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Device with rndis enabled as well
|
// Device with rndis enabled as well
|
||||||
if let Some(handle) = open_device(context, 0x05c6, 0xf622) {
|
if let Some(mut handle) = open_device(context, 0x05c6, 0xf622) {
|
||||||
|
handle.set_auto_detach_kernel_driver(true).expect("set_auto_detach_kernel_driver failed");
|
||||||
|
handle.claim_interface(1).expect("claim_interface(1) failed");
|
||||||
return Some(handle);
|
return Some(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user