mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-05-30 10:19:26 -07:00
Compare commits
4 Commits
fix-roamin
...
4d54ea03e8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d54ea03e8 | ||
|
|
2b427c64d7 | ||
|
|
ed3ad389d0 | ||
|
|
e2fd9de62d |
20
.github/dependabot.yml
vendored
20
.github/dependabot.yml
vendored
@@ -1,7 +1,3 @@
|
||||
# open-pull-requests-limit is used to disable automated version updates
|
||||
# security updates are unaffected. see
|
||||
# * https://docs.github.com/en/code-security/how-tos/secure-your-supply-chain/secure-your-dependencies/configuring-dependabot-version-updates#disabling-dependabot-version-updates
|
||||
# * https://docs.github.com/en/code-security/reference/supply-chain-security/dependabot-options-reference#open-pull-requests-limit-
|
||||
version: 2
|
||||
updates:
|
||||
# Rust dependencies
|
||||
@@ -9,10 +5,8 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 0
|
||||
groups:
|
||||
security:
|
||||
applies-to: "security-updates"
|
||||
dependency-type:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
@@ -21,10 +15,8 @@ updates:
|
||||
directory: "/tools"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 0
|
||||
groups:
|
||||
security:
|
||||
applies-to: "security-updates"
|
||||
dependency-type:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
@@ -33,10 +25,8 @@ updates:
|
||||
directory: "/daemon/web"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 0
|
||||
groups:
|
||||
security:
|
||||
applies-to: "security-updates"
|
||||
dependency-type:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
@@ -45,9 +35,7 @@ updates:
|
||||
directory: "/installer-gui"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 0
|
||||
groups:
|
||||
security:
|
||||
applies-to: "security-updates"
|
||||
dependency-type:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
14
.github/workflows/main.yml
vendored
14
.github/workflows/main.yml
vendored
@@ -141,13 +141,13 @@ jobs:
|
||||
npm install
|
||||
npm run build
|
||||
popd
|
||||
cargo check --verbose
|
||||
NO_FIRMWARE_BIN=true cargo check --verbose
|
||||
- name: Run tests
|
||||
run: |
|
||||
cargo test --verbose
|
||||
NO_FIRMWARE_BIN=true cargo test --verbose
|
||||
- name: Run clippy
|
||||
run: |
|
||||
cargo clippy --verbose
|
||||
NO_FIRMWARE_BIN=true cargo clippy --verbose
|
||||
|
||||
installer_gui_check:
|
||||
# we test the GUI installer separately to:
|
||||
@@ -173,9 +173,9 @@ jobs:
|
||||
# fmt --all runs on all workspace packages so this is handled by
|
||||
# check_and_test above
|
||||
- name: Check
|
||||
run: cargo check --package installer-gui --verbose
|
||||
run: NO_FIRMWARE_BIN=true cargo check --package installer-gui --verbose
|
||||
- name: Run clippy
|
||||
run: cargo clippy --package installer-gui --verbose
|
||||
run: NO_FIRMWARE_BIN=true cargo clippy --package installer-gui --verbose
|
||||
|
||||
test_daemon_frontend:
|
||||
needs: files_changed
|
||||
@@ -227,12 +227,12 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
cd installer
|
||||
cargo check --verbose
|
||||
NO_FIRMWARE_BIN=true cargo check --verbose
|
||||
- name: cargo test
|
||||
shell: bash
|
||||
run: |
|
||||
cd installer
|
||||
cargo test --verbose --no-default-features
|
||||
NO_FIRMWARE_BIN=true cargo test --verbose --no-default-features
|
||||
|
||||
build_rayhunter_check:
|
||||
if: needs.files_changed.outputs.daemon_changed == 'true'
|
||||
|
||||
@@ -76,7 +76,7 @@ This one is for maintainers of Rayhunter.
|
||||
|
||||
1. Make a PR changing the versions in `Cargo.toml` and other files.
|
||||
This could be automated better but right now it's manual. You can do this easily with sed:
|
||||
`sed -i "" -E 's/x.x.x/y.y.y/g' */Cargo.toml installer-gui/src-tauri/Cargo.toml`
|
||||
`sed -i "" -E 's/x.x.x/y.y.y/g' */Cargo.toml`
|
||||
|
||||
2. Merge PR and make a tag.
|
||||
|
||||
|
||||
18
Cargo.lock
generated
18
Cargo.lock
generated
@@ -4705,7 +4705,6 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
@@ -4724,14 +4723,12 @@ dependencies = [
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams 0.4.2",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
]
|
||||
@@ -4766,7 +4763,7 @@ dependencies = [
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams 0.5.0",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
@@ -6630,19 +6627,6 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-streams"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-streams"
|
||||
version = "0.5.0"
|
||||
|
||||
@@ -40,7 +40,7 @@ image = { version = "0.25.1", default-features = false, features = ["png", "gif
|
||||
tempfile = "3.10.2"
|
||||
async_zip = { version = "0.0.17", features = ["tokio"] }
|
||||
anyhow = "1.0.98"
|
||||
reqwest = { version = "0.12.20", default-features = false, features = ["stream"] }
|
||||
reqwest = { version = "0.12.20", default-features = false }
|
||||
rustls-rustcrypto = { version = "0.0.2-alpha", optional = true }
|
||||
rustls-post-quantum = { version = "0.2.4", optional = true }
|
||||
async-trait = "0.1.88"
|
||||
|
||||
@@ -50,43 +50,6 @@ pub struct Config {
|
||||
pub firewall_restrict_outbound: bool,
|
||||
/// Vector containing additional wifi client firewall ports to open
|
||||
pub firewall_allowed_ports: Option<Vec<u16>>,
|
||||
/// Optional WebDAV upload configuration. When unset, no upload worker runs.
|
||||
pub webdav: Option<WebdavConfig>,
|
||||
}
|
||||
|
||||
/// Configuration for uploading finished QMDL recordings to a WebDAV server.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
#[cfg_attr(feature = "apidocs", derive(utoipa::ToSchema))]
|
||||
pub struct WebdavConfig {
|
||||
/// WebDAV server base URL, e.g. "https://example.com/remote.php/files/untitaker/my-subfolder/"
|
||||
pub url: String,
|
||||
/// Optional username for HTTP Basic auth
|
||||
pub username: Option<String>,
|
||||
/// Optional password for HTTP Basic auth
|
||||
pub password: Option<String>,
|
||||
/// Timeout (in seconds) for each upload request
|
||||
pub upload_timeout_secs: u64,
|
||||
/// How often (in seconds) the worker scans for entries to upload
|
||||
pub poll_interval_secs: u64,
|
||||
/// Minimum age (in seconds) an entry must have before it becomes eligible for upload
|
||||
pub min_age_secs: i64,
|
||||
/// Delete the file locally after a successful upload
|
||||
pub delete_on_upload: bool,
|
||||
}
|
||||
|
||||
impl Default for WebdavConfig {
|
||||
fn default() -> Self {
|
||||
WebdavConfig {
|
||||
url: String::new(),
|
||||
username: None,
|
||||
password: None,
|
||||
upload_timeout_secs: 300,
|
||||
poll_interval_secs: 3600,
|
||||
min_age_secs: 86400,
|
||||
delete_on_upload: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
@@ -111,7 +74,6 @@ impl Default for Config {
|
||||
dns_servers: None,
|
||||
firewall_restrict_outbound: true,
|
||||
firewall_allowed_ports: None,
|
||||
webdav: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ use axum::http::header::CONTENT_TYPE;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use futures::{StreamExt, TryStreamExt, future};
|
||||
use log::{debug, error, info, warn};
|
||||
use rayhunter::Device;
|
||||
use tokio::fs::File;
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
@@ -371,7 +370,7 @@ impl DiagTask {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn run_diag_read_thread(
|
||||
task_tracker: &TaskTracker,
|
||||
device: Device,
|
||||
mut dev: DiagDevice,
|
||||
mut qmdl_file_rx: Receiver<DiagDeviceCtrlMessage>,
|
||||
qmdl_file_tx: Sender<DiagDeviceCtrlMessage>,
|
||||
ui_update_sender: Sender<display::DisplayState>,
|
||||
@@ -383,21 +382,8 @@ pub fn run_diag_read_thread(
|
||||
min_space_to_continue_mb: u64,
|
||||
) {
|
||||
task_tracker.spawn(async move {
|
||||
info!("Using configuration for device: {0:?}", device);
|
||||
let mut dev = DiagDevice::new(&device)
|
||||
.await?;
|
||||
dev.config_logs()
|
||||
.await?;
|
||||
|
||||
let mut diag_stream = pin!(dev.as_stream().into_stream());
|
||||
let mut diag_task = DiagTask::new(
|
||||
ui_update_sender,
|
||||
analysis_sender,
|
||||
analyzer_config,
|
||||
notification_channel,
|
||||
min_space_to_start_mb,
|
||||
min_space_to_continue_mb
|
||||
);
|
||||
let mut diag_task = DiagTask::new(ui_update_sender, analysis_sender, analyzer_config, notification_channel, min_space_to_start_mb, min_space_to_continue_mb);
|
||||
qmdl_file_tx
|
||||
.send(DiagDeviceCtrlMessage::StartRecording { response_tx: None })
|
||||
.await
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use rayhunter::diag_device::DiagDeviceError;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::qmdl_store::RecordingStoreError;
|
||||
@@ -6,6 +7,8 @@ use crate::qmdl_store::RecordingStoreError;
|
||||
pub enum RayhunterError {
|
||||
#[error("Config file parsing error: {0}")]
|
||||
ConfigFileParsingError(#[from] toml::de::Error),
|
||||
#[error("Diag intialization error: {0}")]
|
||||
DiagInitError(DiagDeviceError),
|
||||
#[error("Tokio error: {0}")]
|
||||
TokioError(#[from] tokio::io::Error),
|
||||
#[error("QmdlStore error: {0}")]
|
||||
|
||||
@@ -12,7 +12,6 @@ pub mod pcap;
|
||||
pub mod qmdl_store;
|
||||
pub mod server;
|
||||
pub mod stats;
|
||||
pub mod webdav;
|
||||
|
||||
#[cfg(feature = "apidocs")]
|
||||
use utoipa::OpenApi;
|
||||
|
||||
@@ -12,8 +12,6 @@ mod pcap;
|
||||
mod qmdl_store;
|
||||
mod server;
|
||||
mod stats;
|
||||
mod webdav;
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -29,7 +27,6 @@ use crate::server::{
|
||||
scan_wifi, serve_static, set_config, set_time_offset, test_notification,
|
||||
};
|
||||
use crate::stats::{get_qmdl_manifest, get_system_stats};
|
||||
use crate::webdav::run_webdav_upload_worker;
|
||||
use wifi_station::WifiStatus;
|
||||
|
||||
use analysis::{
|
||||
@@ -45,6 +42,7 @@ use diag::{
|
||||
use log::{error, info};
|
||||
use qmdl_store::RecordingStoreError;
|
||||
use rayhunter::Device;
|
||||
use rayhunter::diag_device::DiagDevice;
|
||||
use stats::get_log;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::select;
|
||||
@@ -216,10 +214,18 @@ async fn run_with_config(
|
||||
let notification_service = NotificationService::new(config.ntfy_url.clone());
|
||||
|
||||
if !config.debug_mode {
|
||||
info!("Using configuration for device: {0:?}", config.device);
|
||||
let mut dev = DiagDevice::new(&config.device)
|
||||
.await
|
||||
.map_err(RayhunterError::DiagInitError)?;
|
||||
dev.config_logs()
|
||||
.await
|
||||
.map_err(RayhunterError::DiagInitError)?;
|
||||
|
||||
info!("Starting Diag Thread");
|
||||
run_diag_read_thread(
|
||||
&task_tracker,
|
||||
config.device.clone(),
|
||||
dev,
|
||||
diag_rx,
|
||||
diag_tx.clone(),
|
||||
ui_update_tx.clone(),
|
||||
@@ -290,15 +296,6 @@ async fn run_with_config(
|
||||
);
|
||||
firewall::apply(&config).await;
|
||||
|
||||
if let Some(webdav_config) = config.webdav.clone() {
|
||||
run_webdav_upload_worker(
|
||||
&task_tracker,
|
||||
shutdown_token.clone(),
|
||||
qmdl_store_lock.clone(),
|
||||
webdav_config.into(),
|
||||
);
|
||||
}
|
||||
|
||||
let state = Arc::new(ServerState {
|
||||
config_path: args.config_path.clone(),
|
||||
config,
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::io::{self, ErrorKind};
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use chrono::{DateTime, Local, TimeDelta};
|
||||
use chrono::{DateTime, Local};
|
||||
use log::{info, warn};
|
||||
use rayhunter::util::RuntimeMetadata;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -67,9 +67,6 @@ pub struct ManifestEntry {
|
||||
pub arch: Option<String>,
|
||||
#[serde(default)]
|
||||
pub stop_reason: Option<String>,
|
||||
/// When the manifest was uploaded to a WebDAV server
|
||||
#[cfg_attr(feature = "apidocs", schema(value_type = String))]
|
||||
pub upload_time: Option<DateTime<Local>>,
|
||||
}
|
||||
|
||||
impl ManifestEntry {
|
||||
@@ -85,7 +82,6 @@ impl ManifestEntry {
|
||||
system_os: Some(metadata.system_os),
|
||||
arch: Some(metadata.arch),
|
||||
stop_reason: None,
|
||||
upload_time: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,7 +212,6 @@ impl RecordingStore {
|
||||
system_os: None,
|
||||
arch: None,
|
||||
stop_reason: None,
|
||||
upload_time: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -347,23 +342,6 @@ impl RecordingStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_next_unuploaded_entry(&self, min_age: TimeDelta) -> Option<String> {
|
||||
let now = rayhunter::clock::get_adjusted_now();
|
||||
self.manifest
|
||||
.entries
|
||||
.iter()
|
||||
.filter_map(|entry| {
|
||||
if self.is_current_entry(&entry.name) || entry.upload_time.is_some() {
|
||||
return None;
|
||||
}
|
||||
let age = now - entry.last_message_time.unwrap_or(entry.start_time);
|
||||
|
||||
(age > min_age).then_some((&entry.name, age))
|
||||
})
|
||||
.max_by_key(|(_, age)| *age)
|
||||
.map(|(name, _)| name.clone())
|
||||
}
|
||||
|
||||
// Finds an entry by filename
|
||||
pub fn entry_for_name(&self, name: &str) -> Option<(usize, &ManifestEntry)> {
|
||||
let entry_index = self
|
||||
@@ -390,22 +368,6 @@ impl RecordingStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn mark_entry_as_uploaded(
|
||||
&mut self,
|
||||
name: &str,
|
||||
upload_time: DateTime<Local>,
|
||||
) -> Result<(), RecordingStoreError> {
|
||||
let entry_index = self
|
||||
.manifest
|
||||
.entries
|
||||
.iter()
|
||||
.position(|entry| entry.name == name)
|
||||
.ok_or(RecordingStoreError::NoSuchEntryError)?;
|
||||
self.manifest.entries[entry_index].upload_time = Some(upload_time);
|
||||
self.write_manifest().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_current_entry(&self, name: &str) -> bool {
|
||||
match self.current_entry {
|
||||
Some(idx) => match self.manifest.entries.get(idx) {
|
||||
@@ -582,78 +544,4 @@ mod tests {
|
||||
store.delete_all_entries().await.unwrap();
|
||||
assert!(store.current_entry.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mark_entry_as_uploaded_sets_time_and_persists() {
|
||||
let dir = make_temp_dir();
|
||||
let mut store = RecordingStore::create(dir.path()).await.unwrap();
|
||||
let _ = store.new_entry().await.unwrap();
|
||||
let name = store.manifest.entries[0].name.clone();
|
||||
store.close_current_entry().await.unwrap();
|
||||
|
||||
let upload_time = Local::now();
|
||||
store
|
||||
.mark_entry_as_uploaded(&name, upload_time)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(store.manifest.entries[0].upload_time, Some(upload_time));
|
||||
|
||||
let reloaded = RecordingStore::load(dir.path()).await.unwrap();
|
||||
assert_eq!(reloaded.manifest.entries[0].upload_time, Some(upload_time));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mark_entry_as_uploaded_missing_entry() {
|
||||
let dir = make_temp_dir();
|
||||
let mut store = RecordingStore::create(dir.path()).await.unwrap();
|
||||
assert!(matches!(
|
||||
store.mark_entry_as_uploaded("nope", Local::now()).await,
|
||||
Err(RecordingStoreError::NoSuchEntryError)
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_next_unuploaded_entry() {
|
||||
let dir = make_temp_dir();
|
||||
let mut store = RecordingStore::create(dir.path()).await.unwrap();
|
||||
|
||||
for _ in 0..3 {
|
||||
let _ = store.new_entry().await.unwrap();
|
||||
}
|
||||
|
||||
store.manifest.entries[0].name = "entry-0".to_owned();
|
||||
store.manifest.entries[0].start_time = Local::now() - TimeDelta::seconds(10);
|
||||
store.manifest.entries[0].last_message_time = None;
|
||||
|
||||
store.manifest.entries[1].name = "entry-1".to_owned();
|
||||
store.manifest.entries[1].start_time = Local::now() - TimeDelta::seconds(10);
|
||||
store.manifest.entries[1].last_message_time = Some(Local::now() - TimeDelta::seconds(5));
|
||||
|
||||
store.manifest.entries[2].name = "entry-2".to_owned();
|
||||
store.manifest.entries[2].start_time = Local::now() - TimeDelta::seconds(10);
|
||||
store.manifest.entries[2].last_message_time = Some(Local::now() - TimeDelta::seconds(1));
|
||||
|
||||
assert_eq!(
|
||||
store.get_next_unuploaded_entry(TimeDelta::seconds(3600)),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
store.get_next_unuploaded_entry(TimeDelta::seconds(3)),
|
||||
Some("entry-0".to_owned())
|
||||
);
|
||||
store
|
||||
.mark_entry_as_uploaded("entry-0", Local::now())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
store.get_next_unuploaded_entry(TimeDelta::seconds(3)),
|
||||
Some("entry-1".to_owned())
|
||||
);
|
||||
store
|
||||
.mark_entry_as_uploaded("entry-1", Local::now())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(store.get_next_unuploaded_entry(TimeDelta::seconds(3)), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,446 +0,0 @@
|
||||
use std::fmt::Display;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use chrono::TimeDelta;
|
||||
use log::{info, warn};
|
||||
use reqwest::header::{CONTENT_LENGTH, CONTENT_TYPE};
|
||||
use reqwest::{Body, Client, Response};
|
||||
use tokio::fs::File;
|
||||
use tokio::join;
|
||||
use tokio::{select, sync::RwLock, time};
|
||||
use tokio_util::io::ReaderStream;
|
||||
use tokio_util::{sync::CancellationToken, task::TaskTracker};
|
||||
|
||||
use crate::config::WebdavConfig;
|
||||
use crate::qmdl_store::RecordingStore;
|
||||
|
||||
pub struct WebdavUploadWorkerConfig {
|
||||
poll_interval: Duration,
|
||||
min_age: TimeDelta,
|
||||
url: String,
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
timeout: Duration,
|
||||
delete_on_upload: bool,
|
||||
}
|
||||
|
||||
impl From<WebdavConfig> for WebdavUploadWorkerConfig {
|
||||
fn from(value: WebdavConfig) -> Self {
|
||||
WebdavUploadWorkerConfig {
|
||||
poll_interval: Duration::from_secs(value.poll_interval_secs),
|
||||
min_age: TimeDelta::seconds(value.min_age_secs),
|
||||
url: value.url,
|
||||
username: value.username,
|
||||
password: value.password,
|
||||
timeout: Duration::from_secs(value.upload_timeout_secs),
|
||||
delete_on_upload: value.delete_on_upload,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum FileKind {
|
||||
Analysis,
|
||||
Qmdl,
|
||||
}
|
||||
|
||||
impl FileKind {
|
||||
fn as_extension(&self) -> &'static str {
|
||||
match self {
|
||||
FileKind::Analysis => ".ndjson",
|
||||
FileKind::Qmdl => ".qmdl",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for FileKind {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
FileKind::Analysis => write!(f, "analysis"),
|
||||
FileKind::Qmdl => write!(f, "QMDL"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct WebDavClient {
|
||||
client: Client,
|
||||
url: String,
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
}
|
||||
|
||||
impl WebDavClient {
|
||||
fn new(
|
||||
mut url: String,
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
timeout: Duration,
|
||||
) -> Result<Self, reqwest::Error> {
|
||||
if !url.ends_with('/') {
|
||||
url.push('/');
|
||||
}
|
||||
Ok(Self {
|
||||
client: reqwest::Client::builder().timeout(timeout).build()?,
|
||||
url,
|
||||
username,
|
||||
password,
|
||||
})
|
||||
}
|
||||
|
||||
async fn try_upload_file(&self, file: File, name: &str) -> anyhow::Result<Response> {
|
||||
let file_size = file.metadata().await?.len();
|
||||
|
||||
let stream = ReaderStream::new(file);
|
||||
let body = Body::wrap_stream(stream);
|
||||
|
||||
let target = format!("{}{}", self.url, name);
|
||||
|
||||
let client = self
|
||||
.client
|
||||
.put(&target)
|
||||
.header(CONTENT_TYPE, "application/octet-stream")
|
||||
.header(CONTENT_LENGTH, file_size);
|
||||
|
||||
let client = match (&self.username, &self.password) {
|
||||
(Some(username), Some(password)) => client.basic_auth(username, Some(password)),
|
||||
(Some(username), None) => client.basic_auth(username, None::<&str>),
|
||||
(None, None) => client,
|
||||
(None, Some(_)) => {
|
||||
warn!(
|
||||
"Got WebDAV auth setting with no username but with a password, skipping authentication"
|
||||
);
|
||||
client
|
||||
}
|
||||
};
|
||||
|
||||
let resp = client.body(body).send().await?.error_for_status();
|
||||
Ok(resp?)
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_upload_entry(
|
||||
client: WebDavClient,
|
||||
store: Arc<RwLock<RecordingStore>>,
|
||||
entry_name: String,
|
||||
file_kind: FileKind,
|
||||
shutdown_token: CancellationToken,
|
||||
) -> Option<()> {
|
||||
let read_lock = store.read().await;
|
||||
let entry_idx = read_lock.entry_for_name(&entry_name)?.0;
|
||||
let file = match file_kind {
|
||||
FileKind::Analysis => read_lock.open_entry_analysis(entry_idx).await,
|
||||
FileKind::Qmdl => read_lock.open_entry_qmdl(entry_idx).await,
|
||||
};
|
||||
drop(read_lock);
|
||||
|
||||
let Ok(file) = file.map_err(|err| {
|
||||
warn!(
|
||||
"Unable to open entry: {} {} file: {:?}",
|
||||
entry_name, file_kind, err
|
||||
)
|
||||
}) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let file_name = format!("{}{}", entry_name, file_kind.as_extension());
|
||||
|
||||
let res = select! {
|
||||
_ = shutdown_token.cancelled() => {
|
||||
warn!(
|
||||
"Cancelling upload for entry {} {} file: received shutdown signal",
|
||||
entry_name, file_kind
|
||||
);
|
||||
return None;
|
||||
},
|
||||
res = client.try_upload_file(file, &file_name) => res,
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(_) => {
|
||||
info!("Uploaded {} file for entry {}", file_kind, entry_name);
|
||||
Some(())
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"Failed to upload {} file for entry {}: {:?}",
|
||||
file_kind, entry_name, err
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_webdav_upload_worker(
|
||||
task_tracker: &TaskTracker,
|
||||
shutdown_token: CancellationToken,
|
||||
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
||||
config: WebdavUploadWorkerConfig,
|
||||
) {
|
||||
task_tracker.spawn(async move {
|
||||
let mut interval = time::interval(config.poll_interval);
|
||||
interval.set_missed_tick_behavior(time::MissedTickBehavior::Skip);
|
||||
|
||||
let webdav_client = match WebDavClient::new(
|
||||
config.url,
|
||||
config.username,
|
||||
config.password,
|
||||
config.timeout,
|
||||
) {
|
||||
Ok(client) => client,
|
||||
Err(err) => {
|
||||
warn!("Unable to create WebDAV client: {:?}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
loop {
|
||||
select! {
|
||||
_ = shutdown_token.cancelled() => break,
|
||||
_ = interval.tick() => {
|
||||
loop {
|
||||
let Some(unuploaded_entry) = qmdl_store_lock
|
||||
.read()
|
||||
.await
|
||||
.get_next_unuploaded_entry(config.min_age) else {
|
||||
break;
|
||||
};
|
||||
|
||||
let (Some(()), Some(())) = join!(
|
||||
try_upload_entry(
|
||||
webdav_client.clone(),
|
||||
qmdl_store_lock.clone(),
|
||||
unuploaded_entry.clone(),
|
||||
FileKind::Qmdl,
|
||||
shutdown_token.clone(),
|
||||
),
|
||||
try_upload_entry(
|
||||
webdav_client.clone(),
|
||||
qmdl_store_lock.clone(),
|
||||
unuploaded_entry.clone(),
|
||||
FileKind::Analysis,
|
||||
shutdown_token.clone()
|
||||
),
|
||||
) else {
|
||||
break;
|
||||
};
|
||||
|
||||
if config.delete_on_upload {
|
||||
match qmdl_store_lock.write().await.delete_entry(&unuploaded_entry).await {
|
||||
Ok(_) => info!("Successfully deleted entry: {} after upload to WebDAV", unuploaded_entry),
|
||||
Err(err) => warn!("Unable to delete entry: {} after upload to WebDAV: {}", unuploaded_entry, err),
|
||||
}
|
||||
} else {
|
||||
match qmdl_store_lock.write().await.mark_entry_as_uploaded(&unuploaded_entry, rayhunter::clock::get_adjusted_now()).await {
|
||||
Ok(_) => info!("Successfully marked entry: {} as uploaded", unuploaded_entry),
|
||||
Err(err) => warn!("Unable to mark entry: {} as uploaded: {}", unuploaded_entry, err),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use axum::{
|
||||
Router,
|
||||
body::Bytes,
|
||||
extract::{Path as AxumPath, State},
|
||||
http::{HeaderMap, StatusCode},
|
||||
routing::put,
|
||||
};
|
||||
use tempfile::Builder;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct RecordedPut {
|
||||
path: String,
|
||||
auth: Option<String>,
|
||||
body: Vec<u8>,
|
||||
}
|
||||
|
||||
async fn capture_put(
|
||||
State(state): State<Arc<Mutex<Vec<RecordedPut>>>>,
|
||||
AxumPath(path): AxumPath<String>,
|
||||
headers: HeaderMap,
|
||||
body: Bytes,
|
||||
) -> StatusCode {
|
||||
let auth = headers
|
||||
.get("authorization")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(String::from);
|
||||
state.lock().await.push(RecordedPut {
|
||||
path,
|
||||
auth,
|
||||
body: body.to_vec(),
|
||||
});
|
||||
StatusCode::CREATED
|
||||
}
|
||||
|
||||
async fn setup_webdav_server() -> (Arc<Mutex<Vec<RecordedPut>>>, String) {
|
||||
crate::crypto_provider::install_default();
|
||||
|
||||
let state = Arc::new(Mutex::new(Vec::new()));
|
||||
let app = Router::new()
|
||||
.route("/{*path}", put(capture_put))
|
||||
.with_state(state.clone());
|
||||
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let url = format!("http://{}/dav", addr);
|
||||
|
||||
tokio::spawn(async move {
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
});
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
(state, url)
|
||||
}
|
||||
|
||||
async fn cleanup_worker(shutdown: CancellationToken, tracker: TaskTracker) {
|
||||
shutdown.cancel();
|
||||
tracker.close();
|
||||
tracker.wait().await;
|
||||
}
|
||||
|
||||
async fn make_store_with_closed_entry(
|
||||
dir: &std::path::Path,
|
||||
) -> (Arc<RwLock<RecordingStore>>, String) {
|
||||
let mut store = RecordingStore::create(dir).await.unwrap();
|
||||
let (mut qmdl_file, mut analysis_file) = store.new_entry().await.unwrap();
|
||||
qmdl_file.write_all(b"fake qmdl payload").await.unwrap();
|
||||
qmdl_file.flush().await.unwrap();
|
||||
analysis_file
|
||||
.write_all(b"fake ndjson payload")
|
||||
.await
|
||||
.unwrap();
|
||||
analysis_file.flush().await.unwrap();
|
||||
let entry_index = store.current_entry.unwrap();
|
||||
let name = store.manifest.entries[entry_index].name.clone();
|
||||
store.update_entry_qmdl_size(entry_index, 17).await.unwrap();
|
||||
store.close_current_entry().await.unwrap();
|
||||
(Arc::new(RwLock::new(store)), name)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_webdav_upload_worker_uploads_entry() {
|
||||
let (captured, url) = setup_webdav_server().await;
|
||||
|
||||
let dir = Builder::new().prefix("webdav_test").tempdir().unwrap();
|
||||
let (store, entry_name) = make_store_with_closed_entry(dir.path()).await;
|
||||
|
||||
let shutdown = CancellationToken::new();
|
||||
let tracker = TaskTracker::new();
|
||||
let config = WebdavUploadWorkerConfig {
|
||||
poll_interval: Duration::from_millis(50),
|
||||
min_age: TimeDelta::seconds(-1),
|
||||
url,
|
||||
username: Some("user".to_string()),
|
||||
password: Some("password".to_string()),
|
||||
timeout: Duration::from_secs(1),
|
||||
delete_on_upload: false,
|
||||
};
|
||||
|
||||
run_webdav_upload_worker(&tracker, shutdown.clone(), store.clone(), config);
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
cleanup_worker(shutdown, tracker).await;
|
||||
|
||||
let recorded = captured.lock().await;
|
||||
assert_eq!(recorded.len(), 2);
|
||||
let paths: Vec<&str> = recorded.iter().map(|r| r.path.as_str()).collect();
|
||||
let qmdl_path = format!("dav/{}.qmdl", entry_name);
|
||||
let ndjson_path = format!("dav/{}.ndjson", entry_name);
|
||||
assert!(paths.contains(&qmdl_path.as_str()));
|
||||
assert!(paths.contains(&ndjson_path.as_str()));
|
||||
for put in recorded.iter() {
|
||||
assert_eq!(put.auth.as_deref(), Some("Basic dXNlcjpwYXNzd29yZA=="));
|
||||
}
|
||||
let qmdl_body = recorded
|
||||
.iter()
|
||||
.find(|r| r.path == qmdl_path)
|
||||
.unwrap()
|
||||
.body
|
||||
.clone();
|
||||
let ndjson_body = recorded
|
||||
.iter()
|
||||
.find(|r| r.path == ndjson_path)
|
||||
.unwrap()
|
||||
.body
|
||||
.clone();
|
||||
drop(recorded);
|
||||
assert_eq!(qmdl_body, b"fake qmdl payload");
|
||||
assert_eq!(ndjson_body, b"fake ndjson payload");
|
||||
|
||||
let store_read = store.read().await;
|
||||
let (_, entry) = store_read.entry_for_name(&entry_name).unwrap();
|
||||
assert!(entry.upload_time.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_webdav_upload_worker_deletes_when_configured() {
|
||||
let (captured, url) = setup_webdav_server().await;
|
||||
|
||||
let dir = Builder::new().prefix("webdav_test").tempdir().unwrap();
|
||||
let (store, entry_name) = make_store_with_closed_entry(dir.path()).await;
|
||||
|
||||
let shutdown = CancellationToken::new();
|
||||
let tracker = TaskTracker::new();
|
||||
let config = WebdavUploadWorkerConfig {
|
||||
poll_interval: Duration::from_millis(50),
|
||||
min_age: TimeDelta::seconds(-1),
|
||||
url,
|
||||
username: None,
|
||||
password: None,
|
||||
timeout: Duration::from_secs(1),
|
||||
delete_on_upload: true,
|
||||
};
|
||||
|
||||
run_webdav_upload_worker(&tracker, shutdown.clone(), store.clone(), config);
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
cleanup_worker(shutdown, tracker).await;
|
||||
|
||||
assert_eq!(captured.lock().await.len(), 2);
|
||||
|
||||
let store_read = store.read().await;
|
||||
assert!(store_read.entry_for_name(&entry_name).is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_webdav_upload_worker_respects_min_age() {
|
||||
let (captured, url) = setup_webdav_server().await;
|
||||
|
||||
let dir = Builder::new().prefix("webdav_test").tempdir().unwrap();
|
||||
let (store, entry_name) = make_store_with_closed_entry(dir.path()).await;
|
||||
|
||||
let shutdown = CancellationToken::new();
|
||||
let tracker = TaskTracker::new();
|
||||
let config = WebdavUploadWorkerConfig {
|
||||
poll_interval: Duration::from_millis(50),
|
||||
min_age: TimeDelta::seconds(3600),
|
||||
url,
|
||||
username: None,
|
||||
password: None,
|
||||
timeout: Duration::from_secs(1),
|
||||
delete_on_upload: false,
|
||||
};
|
||||
|
||||
run_webdav_upload_worker(&tracker, shutdown.clone(), store.clone(), config);
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
cleanup_worker(shutdown, tracker).await;
|
||||
|
||||
assert!(captured.lock().await.is_empty());
|
||||
|
||||
let store_read = store.read().await;
|
||||
let (_, entry) = store_read.entry_for_name(&entry_name).unwrap();
|
||||
assert!(entry.upload_time.is_none());
|
||||
}
|
||||
}
|
||||
612
daemon/web/package-lock.json
generated
612
daemon/web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -18,23 +18,23 @@
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@sveltejs/adapter-auto": "^7.0.1",
|
||||
"@sveltejs/adapter-static": "^3.0.5",
|
||||
"@sveltejs/kit": "^2.58.0",
|
||||
"@sveltejs/kit": "^2.57.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@types/eslint": "^9.6.0",
|
||||
"@types/node": "^25.6.0",
|
||||
"eslint": "^10.2.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-svelte": "^3.17.1",
|
||||
"eslint-plugin-svelte": "^3.17.0",
|
||||
"globals": "^17.5.0",
|
||||
"prettier": "^3.8.3",
|
||||
"prettier-plugin-svelte": "^3.5.1",
|
||||
"svelte": "^5.55.5",
|
||||
"svelte": "^5.55.4",
|
||||
"svelte-check": "^4.4.6",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.59.0",
|
||||
"vite": "^8.0.10",
|
||||
"vitest": "^4.1.5"
|
||||
"typescript-eslint": "^8.58.2",
|
||||
"vite": "^8.0.9",
|
||||
"vitest": "^4.1.4"
|
||||
}
|
||||
}
|
||||
|
||||
25
dist/config.toml.in
vendored
25
dist/config.toml.in
vendored
@@ -55,31 +55,6 @@ firewall_restrict_outbound = true
|
||||
# Example: allow HTTP (80) and SSH (22).
|
||||
# firewall_allowed_ports = [80, 22]
|
||||
|
||||
# WebDAV Upload
|
||||
# If a [webdav] section is present, finished recordings (both the raw .qmdl file
|
||||
# and its .ndjson analysis output) are uploaded in the background to a WebDAV
|
||||
# server once they've been closed for at least min_age_secs. After a successful
|
||||
# upload the entry is either marked as uploaded in the manifest, or deleted
|
||||
# locally if delete_on_upload = true. With no [webdav] section, no upload
|
||||
# worker runs.
|
||||
#
|
||||
# [webdav]
|
||||
# host = "https://dav.example.com"
|
||||
# remote_path = "/rayhunter"
|
||||
# # HTTP Basic auth. Both fields are optional; a password without a username is
|
||||
# # rejected and the request is sent unauthenticated.
|
||||
# username = "user"
|
||||
# password = "pass"
|
||||
# # Timeout in seconds for each upload request (default 300).
|
||||
# upload_timeout_secs = 300
|
||||
# # How often the worker scans for eligible entries (default 3600).
|
||||
# poll_interval_secs = 3600
|
||||
# # Minimum age in seconds before an entry becomes eligible for upload
|
||||
# # (default 86400 = 1 day).
|
||||
# min_age_secs = 86400
|
||||
# # Delete the entry locally after a successful upload (default false).
|
||||
# delete_on_upload = false
|
||||
|
||||
# Analyzer Configuration
|
||||
# Enable/disable specific IMSI catcher detection heuristics
|
||||
# See https://github.com/EFForg/rayhunter/blob/main/doc/heuristics.md for details
|
||||
|
||||
@@ -55,39 +55,4 @@ You can also configure WiFi during installation:
|
||||
|
||||
- **Restrict outbound traffic** limits what the device can send over the network. When enabled, only DNS, DHCP, and HTTPS traffic is allowed; everything else is blocked. This is enabled by default and prevents the device from phoning home to the carrier over cellular. If you need to allow additional ports (for example, port 80 for HTTP or port 22 for SSH), add them to the **Additional allowed ports** list.
|
||||
|
||||
## WebDAV Upload
|
||||
|
||||
Rayhunter can automatically upload finished recordings to a WebDAV server. When a `[webdav]` section is present in `config.toml`, a background worker periodically scans the recording store and uploads any closed entry that is older than `min_age_secs`. Each eligible entry uploads two files: the raw `.qmdl` capture and its `.ndjson` analysis output. After a successful upload the entry is either marked as uploaded in the manifest (and skipped on subsequent polls), or deleted locally if `delete_on_upload = true`. With no `[webdav]` section, no upload worker runs.
|
||||
|
||||
WebDAV upload is currently configurable only by editing `config.toml` — there is no web UI control for it yet.
|
||||
|
||||
| Key | Required | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `url` | yes | — | WebDAV server base URL, e.g. `https://example.com/remote.php/files/user/rayhunter/` |
|
||||
| `username` | no | — | HTTP Basic auth username |
|
||||
| `password` | no | — | HTTP Basic auth password |
|
||||
| `upload_timeout_secs` | no | `300` | Timeout (seconds) for each upload request |
|
||||
| `poll_interval_secs` | no | `3600` | How often (seconds) the worker scans for eligible entries |
|
||||
| `min_age_secs` | no | `86400` | Minimum age (seconds) an entry must have before it becomes eligible for upload |
|
||||
| `delete_on_upload` | no | `false` | Delete the entry locally after a successful upload |
|
||||
|
||||
Example:
|
||||
|
||||
```toml
|
||||
[webdav]
|
||||
url = "https://dav.example.com/rayhunter/"
|
||||
username = "user"
|
||||
password = "pass"
|
||||
upload_timeout_secs = 300
|
||||
poll_interval_secs = 3600
|
||||
min_age_secs = 86400
|
||||
delete_on_upload = false
|
||||
```
|
||||
|
||||
A few notes on behavior:
|
||||
|
||||
- **Auth:** HTTP Basic. Supplying a `password` without a `username` is rejected — the request is sent unauthenticated and a warning is logged.
|
||||
- **Retries and overwrites:** each entry's two files (`.qmdl` and `.ndjson`) must both upload successfully before the entry is marked as uploaded in the manifest. If one upload fails, the entry stays unmarked and both files are retried on the next poll — the one that previously succeeded will be overwritten on the server. Once an entry is marked as uploaded, Rayhunter will not upload it again.
|
||||
- **Currently-recording entry:** the active recording is never uploaded; only closed entries are eligible.
|
||||
|
||||
If you prefer editing `config.toml` file, you need to obtain a shell on your [Orbic](./orbic.md#obtaining-a-shell) or [TP-Link](./tplink-m7350.md#obtaining-a-shell) device and edit the file manually. You can view the [default configuration file on GitHub](https://github.com/EFForg/rayhunter/blob/main/dist/config.toml.in).
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::path::Path;
|
||||
use std::process::exit;
|
||||
|
||||
fn main() {
|
||||
println!("cargo::rerun-if-env-changed=NO_FIRMWARE_BIN");
|
||||
println!("cargo::rerun-if-env-changed=FIRMWARE_PROFILE");
|
||||
let profile = std::env::var("FIRMWARE_PROFILE").unwrap_or_else(|_| {
|
||||
// Default to firmware-devel for debug builds, firmware for release builds
|
||||
@@ -24,19 +26,24 @@ fn main() {
|
||||
|
||||
fn set_binary_var(include_dir: &Path, var: &str, file: &str) {
|
||||
println!("cargo::rerun-if-env-changed={var}");
|
||||
if std::env::var_os(var).is_some() {
|
||||
if std::env::var_os("NO_FIRMWARE_BIN").is_some() {
|
||||
let out_dir = std::env::var("OUT_DIR").unwrap();
|
||||
std::fs::create_dir_all(&out_dir).unwrap();
|
||||
let blank = Path::new(&out_dir).join("blank");
|
||||
std::fs::write(&blank, []).unwrap();
|
||||
println!("cargo::rustc-env={var}={}", blank.display());
|
||||
return;
|
||||
}
|
||||
let binary = include_dir.join(file);
|
||||
println!("cargo::rerun-if-changed={}", binary.display());
|
||||
if binary.exists() {
|
||||
if std::env::var_os(var).is_none() {
|
||||
let binary = include_dir.join(file);
|
||||
println!("cargo::rerun-if-changed={}", binary.display());
|
||||
if !binary.exists() {
|
||||
println!(
|
||||
"cargo::error=Firmware binary {file} not present at {}",
|
||||
binary.display()
|
||||
);
|
||||
exit(0);
|
||||
}
|
||||
println!("cargo::rustc-env={var}={}", binary.display());
|
||||
} else {
|
||||
println!(
|
||||
"cargo::warning=Firmware binary {file} not present at {}; \
|
||||
installers that need it will fail",
|
||||
binary.display()
|
||||
);
|
||||
println!("cargo::rustc-env={var}=");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,27 +47,20 @@ pub async fn install_config<C: DeviceConnection>(
|
||||
///
|
||||
/// Skips any binary that is already present on the device (e.g. provided by firmware),
|
||||
/// since those may be newer or better-integrated than the bundled versions.
|
||||
///
|
||||
/// In debug builds the wpa-supplicant binaries may not be bundled (build.rs sets the
|
||||
/// env vars to empty in that case); when so, this is a no-op so devs don't have to
|
||||
/// build wpa-supplicant just to install on Orbic.
|
||||
pub async fn install_wifi_tools<C: DeviceConnection>(conn: &mut C) -> Result<()> {
|
||||
if env!("FILE_WPA_SUPPLICANT").is_empty() {
|
||||
println!("wifi tools were not built into this installer, skipping");
|
||||
return Ok(());
|
||||
}
|
||||
pub async fn install_wifi_tools<C: DeviceConnection>(
|
||||
conn: &mut C,
|
||||
wpa_supplicant: &[u8],
|
||||
wpa_cli: &[u8],
|
||||
iw: &[u8],
|
||||
) -> Result<()> {
|
||||
let tools: &[(&str, &str, &[u8])] = &[
|
||||
(
|
||||
"wpa_supplicant",
|
||||
"/data/rayhunter/bin/wpa_supplicant",
|
||||
crate::get_file!("FILE_WPA_SUPPLICANT"),
|
||||
wpa_supplicant,
|
||||
),
|
||||
(
|
||||
"wpa_cli",
|
||||
"/data/rayhunter/bin/wpa_cli",
|
||||
crate::get_file!("FILE_WPA_CLI"),
|
||||
),
|
||||
("iw", "/data/rayhunter/bin/iw", crate::get_file!("FILE_IW")),
|
||||
("wpa_cli", "/data/rayhunter/bin/wpa_cli", wpa_cli),
|
||||
("iw", "/data/rayhunter/bin/iw", iw),
|
||||
];
|
||||
for &(name, dest, payload) in tools {
|
||||
if device_has_binary(conn, name).await {
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
#[cfg(debug_assertions)]
|
||||
macro_rules! get_file {
|
||||
($var:literal) => {{
|
||||
let path = env!($var);
|
||||
match ::std::fs::read(path) {
|
||||
Ok(bytes) => bytes.leak() as &'static [u8],
|
||||
Err(e) => panic!("Failed to read file for {}: {}", $var, e),
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
macro_rules! get_file {
|
||||
($var:literal) => {{
|
||||
const _: () = assert!(
|
||||
!env!($var).is_empty(),
|
||||
concat!($var, " was not bundled at build time"),
|
||||
);
|
||||
include_bytes!(env!($var)) as &'static [u8]
|
||||
}};
|
||||
}
|
||||
|
||||
pub(crate) use get_file;
|
||||
@@ -6,9 +6,6 @@ use env_logger::Env;
|
||||
use anyhow::bail;
|
||||
|
||||
mod connection;
|
||||
mod files;
|
||||
pub(crate) use files::*;
|
||||
|
||||
mod moxee;
|
||||
#[cfg(not(target_os = "android"))]
|
||||
mod orbic;
|
||||
|
||||
@@ -136,7 +136,7 @@ async fn force_debug_mode() -> Result<ADBUSBDevice> {
|
||||
}
|
||||
|
||||
async fn setup_rootshell(adb_device: &mut ADBUSBDevice) -> Result<()> {
|
||||
let rootshell_bin = crate::get_file!("FILE_ROOTSHELL");
|
||||
let rootshell_bin = include_bytes!(env!("FILE_ROOTSHELL"));
|
||||
|
||||
install_file(adb_device, "/bin/rootshell", rootshell_bin).await?;
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
@@ -151,7 +151,7 @@ async fn setup_rootshell(adb_device: &mut ADBUSBDevice) -> Result<()> {
|
||||
}
|
||||
|
||||
async fn setup_rayhunter(mut adb_device: ADBUSBDevice, reset_config: bool) -> Result<ADBUSBDevice> {
|
||||
let rayhunter_daemon_bin = crate::get_file!("FILE_RAYHUNTER_DAEMON");
|
||||
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
|
||||
|
||||
adb_at_syscmd(
|
||||
&mut adb_device,
|
||||
@@ -170,7 +170,13 @@ async fn setup_rayhunter(mut adb_device: ADBUSBDevice, reset_config: bool) -> Re
|
||||
device: &mut adb_device,
|
||||
};
|
||||
install_config(&mut conn, "orbic", reset_config).await?;
|
||||
install_wifi_tools(&mut conn).await?;
|
||||
install_wifi_tools(
|
||||
&mut conn,
|
||||
include_bytes!(env!("FILE_WPA_SUPPLICANT")),
|
||||
include_bytes!(env!("FILE_WPA_CLI")),
|
||||
include_bytes!(env!("FILE_IW")),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
install_file(
|
||||
|
||||
@@ -216,7 +216,7 @@ async fn wait_for_telnet(admin_ip: &str) -> Result<()> {
|
||||
|
||||
async fn setup_rayhunter(admin_ip: &str, reset_config: bool, data_dir: &str) -> Result<()> {
|
||||
let addr = SocketAddr::from_str(&format!("{admin_ip}:{TELNET_PORT}"))?;
|
||||
let rayhunter_daemon_bin = crate::get_file!("FILE_RAYHUNTER_DAEMON");
|
||||
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
|
||||
|
||||
// Remount filesystem as read-write to allow modifications
|
||||
// This is really only necessary for the Moxee Hotspot
|
||||
@@ -248,7 +248,13 @@ async fn setup_rayhunter(admin_ip: &str, reset_config: bool, data_dir: &str) ->
|
||||
)
|
||||
.await?;
|
||||
|
||||
install_wifi_tools(&mut conn).await?;
|
||||
install_wifi_tools(
|
||||
&mut conn,
|
||||
include_bytes!(env!("FILE_WPA_SUPPLICANT")),
|
||||
include_bytes!(env!("FILE_WPA_CLI")),
|
||||
include_bytes!(env!("FILE_IW")),
|
||||
)
|
||||
.await?;
|
||||
|
||||
install_config(&mut conn, "orbic", reset_config).await?;
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ pub async fn install() -> Result<()> {
|
||||
run_command_expect(&mut adb, "mount -o remount,rw /", "exit code 0").await?;
|
||||
run_command_expect(&mut adb, "mkdir -p /data/rayhunter", "exit code 0").await?;
|
||||
|
||||
let rayhunter_daemon_bin = crate::get_file!("FILE_RAYHUNTER_DAEMON");
|
||||
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
|
||||
adb.write_file("/data/rayhunter/rayhunter-daemon", rayhunter_daemon_bin)
|
||||
.await?;
|
||||
adb.write_file(
|
||||
|
||||
@@ -48,7 +48,7 @@ async fn run_install(admin_ip: String, admin_password: String) -> Result<()> {
|
||||
)
|
||||
.await?;
|
||||
|
||||
let rayhunter_daemon_bin = crate::get_file!("FILE_RAYHUNTER_DAEMON");
|
||||
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
|
||||
telnet_send_file(
|
||||
addr,
|
||||
"/data/rayhunter/rayhunter-daemon",
|
||||
|
||||
@@ -188,7 +188,7 @@ async fn tplink_run_install(
|
||||
|
||||
install_config(&mut conn, "tplink", reset_config).await?;
|
||||
|
||||
let rayhunter_daemon_bin = crate::get_file!("FILE_RAYHUNTER_DAEMON");
|
||||
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
|
||||
|
||||
telnet_send_file(
|
||||
addr,
|
||||
|
||||
@@ -148,7 +148,7 @@ async fn install_rayhunter_files(adb_device: &mut ADBUSBDevice) -> Result<()> {
|
||||
adb_device.shell_command(&["mount", "-o", "remount,rw", "/system"], &mut buf)?;
|
||||
|
||||
// Install rayhunter daemon binary with verification
|
||||
let rayhunter_daemon_bin = crate::get_file!("FILE_RAYHUNTER_DAEMON");
|
||||
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
|
||||
install_file(
|
||||
adb_device,
|
||||
"/data/rayhunter/rayhunter-daemon",
|
||||
|
||||
@@ -26,6 +26,7 @@ pub async fn install(
|
||||
Args {
|
||||
admin_ip,
|
||||
admin_password,
|
||||
..
|
||||
}: Args,
|
||||
) -> Result<()> {
|
||||
wingtech_run_install(admin_ip, admin_password).await
|
||||
@@ -104,7 +105,7 @@ async fn wingtech_run_install(admin_ip: String, admin_password: String) -> Resul
|
||||
)
|
||||
.await?;
|
||||
|
||||
let rayhunter_daemon_bin = crate::get_file!("FILE_RAYHUNTER_DAEMON");
|
||||
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
|
||||
telnet_send_file(
|
||||
addr,
|
||||
"/data/rayhunter/rayhunter-daemon",
|
||||
|
||||
@@ -7,13 +7,8 @@ use super::analyzer::{Analyzer, Event, EventType};
|
||||
use super::information_element::{InformationElement, LteInformationElement};
|
||||
use log::debug;
|
||||
|
||||
use pycrate_rs::nas::generated::emm::emm_attach_reject::EMMCauseEMMCause as AttachRejectEMMCause;
|
||||
use pycrate_rs::nas::generated::emm::emm_attach_request::TAI;
|
||||
use telcom_parser::lte_rrc::{BCCH_DL_SCH_MessageType, BCCH_DL_SCH_MessageType_c1};
|
||||
use telcom_parser::lte_rrc::{MCC_MNC_Digit, PLMN_Identity, PLMN_IdentityList};
|
||||
use telcom_parser::lte_rrc::{
|
||||
/* DL_DCCH_MessageType, DL_DCCH_MessageType_c1,*/ UL_CCCH_MessageType,
|
||||
UL_CCCH_MessageType_c1,
|
||||
DL_DCCH_MessageType, DL_DCCH_MessageType_c1, UL_CCCH_MessageType, UL_CCCH_MessageType_c1,
|
||||
};
|
||||
|
||||
const TIMEOUT_THRESHHOLD: usize = 50;
|
||||
@@ -31,8 +26,6 @@ pub struct ImsiRequestedAnalyzer {
|
||||
state: State,
|
||||
timeout_counter: usize,
|
||||
flag: Option<Event>,
|
||||
likely_enb_plmn: String,
|
||||
likely_ue_plmn: String,
|
||||
}
|
||||
|
||||
impl Default for ImsiRequestedAnalyzer {
|
||||
@@ -47,10 +40,6 @@ impl ImsiRequestedAnalyzer {
|
||||
state: State::Unattached,
|
||||
timeout_counter: 0,
|
||||
flag: None,
|
||||
// You will likely wonder why this isn't an Option<PLMN{mcc: u32, mnc: u32}>
|
||||
// The answer is that I like strings.
|
||||
likely_enb_plmn: "Unknown".to_string(),
|
||||
likely_ue_plmn: "Unknown".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,20 +72,10 @@ impl ImsiRequestedAnalyzer {
|
||||
|
||||
// IMSI to Disconnect without AuthAccept
|
||||
(State::IdentityRequest, State::Disconnect) => {
|
||||
if self.likely_enb_plmn == self.likely_ue_plmn {
|
||||
self.flag = Some(Event {
|
||||
event_type: EventType::High,
|
||||
message: "Disconnected after Identity Request without Auth Accept on home network!".to_string(),
|
||||
});
|
||||
} else {
|
||||
self.flag = Some(Event {
|
||||
event_type: EventType::Low,
|
||||
message: format!(
|
||||
"Disconnected after Identity Request without Auth Accept, but this could be a false positive roaming issue - Tower PLMN: {}, UE PLMN: {}",
|
||||
self.likely_enb_plmn, self.likely_ue_plmn
|
||||
),
|
||||
});
|
||||
}
|
||||
self.flag = Some(Event {
|
||||
event_type: EventType::High,
|
||||
message: "Disconnected after Identity Request without Auth Accept".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
(_, State::IdentityRequest) => {
|
||||
@@ -113,71 +92,6 @@ impl ImsiRequestedAnalyzer {
|
||||
}
|
||||
self.state = next_state;
|
||||
}
|
||||
|
||||
// Sometimes an ENB can have multiple PLMNS
|
||||
fn format_plmn_list(&mut self, plmn_list: &PLMN_IdentityList) -> String {
|
||||
plmn_list
|
||||
.0
|
||||
.iter()
|
||||
.map(|info| self.plmn_identity_to_str(&info.plmn_identity))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
|
||||
// PLMN is represented in two very different ways in the LTE spec so we need
|
||||
// two very different functions to decode them. I hate this.
|
||||
fn plmn_identity_to_str(&mut self, plmn: &PLMN_Identity) -> String {
|
||||
let mcc_digits: String = plmn
|
||||
.mcc
|
||||
.as_ref()
|
||||
.map(|mcc| {
|
||||
mcc.0
|
||||
.iter()
|
||||
.map(|MCC_MNC_Digit(n)| n.to_string())
|
||||
.collect::<String>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let mnc_digits: String = plmn
|
||||
.mnc
|
||||
.0
|
||||
.iter()
|
||||
.map(|MCC_MNC_Digit(n)| n.to_string())
|
||||
.collect::<String>();
|
||||
|
||||
format!("{}-{}", mcc_digits, mnc_digits)
|
||||
}
|
||||
|
||||
fn plmn_vec_to_str(&mut self, bytes: &[u8]) -> String {
|
||||
let mcc_digit1 = bytes[0] & 0x0F;
|
||||
let mcc_digit2 = (bytes[0] >> 4) & 0x0F;
|
||||
let mcc_digit3 = bytes[1] & 0x0F;
|
||||
|
||||
let mnc_digit1 = bytes[2] & 0x0F;
|
||||
let mnc_digit2 = (bytes[2] >> 4) & 0x0F;
|
||||
let mnc_digit3 = (bytes[1] >> 4) & 0x0F;
|
||||
|
||||
let mcc = mcc_digit1 as u32 * 100 + mcc_digit2 as u32 * 10 + mcc_digit3 as u32;
|
||||
|
||||
let mcc_str = format!("{:03}", mcc);
|
||||
let mnc_str = if mnc_digit3 == 0xF {
|
||||
format!("{:02}", mnc_digit1 * 10 + mnc_digit2)
|
||||
} else {
|
||||
format!(
|
||||
"{:03}",
|
||||
mnc_digit1 as u32 * 100 + mnc_digit2 as u32 * 10 + mnc_digit3 as u32
|
||||
)
|
||||
};
|
||||
|
||||
format!("{}-{}", mcc_str, mnc_str)
|
||||
}
|
||||
|
||||
fn extract_plmn(&mut self, old_tai: &Option<TAI>) -> String {
|
||||
match old_tai {
|
||||
Some(t) => self.plmn_vec_to_str(&t.plmn),
|
||||
None => "Unknown".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Analyzer for ImsiRequestedAnalyzer {
|
||||
@@ -192,7 +106,7 @@ impl Analyzer for ImsiRequestedAnalyzer {
|
||||
}
|
||||
|
||||
fn get_version(&self) -> u32 {
|
||||
4
|
||||
3
|
||||
}
|
||||
|
||||
fn analyze_information_element(
|
||||
@@ -200,29 +114,11 @@ impl Analyzer for ImsiRequestedAnalyzer {
|
||||
ie: &InformationElement,
|
||||
packet_num: usize,
|
||||
) -> Option<Event> {
|
||||
// Set the enodeb plmn to the last sib1 we got, we should improve this once we have PCI data, this
|
||||
// is a naive approach.
|
||||
if let InformationElement::LTE(lte_ie) = ie
|
||||
&& let LteInformationElement::BcchDlSch(sch_msg) = &**lte_ie
|
||||
&& let BCCH_DL_SCH_MessageType::C1(c1) = &sch_msg.message
|
||||
&& let BCCH_DL_SCH_MessageType_c1::SystemInformationBlockType1(sib1) = c1
|
||||
{
|
||||
let plmn = &sib1.cell_access_related_info.plmn_identity_list;
|
||||
self.likely_enb_plmn = self.format_plmn_list(plmn);
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
if let InformationElement::LTE(inner) = ie {
|
||||
match &**inner {
|
||||
LteInformationElement::NAS(payload) => match payload {
|
||||
NASMessage::EMMMessage(EMMMessage::EMMAttachRequest(request)) => {
|
||||
if self.likely_ue_plmn == "Unknown" {
|
||||
self.likely_ue_plmn = self.extract_plmn(&request.old_tai.inner);
|
||||
}
|
||||
self.transition(State::AttachRequest, packet_num);
|
||||
}
|
||||
NASMessage::EMMMessage(EMMMessage::EMMExtServiceRequest(_)) => {
|
||||
NASMessage::EMMMessage(EMMMessage::EMMExtServiceRequest(_))
|
||||
| NASMessage::EMMMessage(EMMMessage::EMMAttachRequest(_)) => {
|
||||
self.transition(State::AttachRequest, packet_num);
|
||||
}
|
||||
NASMessage::EMMMessage(EMMMessage::EMMIdentityRequest(_)) => {
|
||||
@@ -233,22 +129,12 @@ impl Analyzer for ImsiRequestedAnalyzer {
|
||||
self.transition(State::AuthAccept, packet_num);
|
||||
}
|
||||
NASMessage::EMMMessage(EMMMessage::EMMServiceReject(_))
|
||||
| NASMessage::EMMMessage(EMMMessage::EMMAttachReject(_))
|
||||
| NASMessage::EMMMessage(EMMMessage::EMMDetachRequestMO(_))
|
||||
| NASMessage::EMMMessage(EMMMessage::EMMDetachRequestMT(_))
|
||||
| NASMessage::EMMMessage(EMMMessage::EMMTrackingAreaUpdateReject(_)) => {
|
||||
self.transition(State::Disconnect, packet_num);
|
||||
}
|
||||
NASMessage::EMMMessage(EMMMessage::EMMAttachReject(reject)) => {
|
||||
self.transition(State::Disconnect, packet_num);
|
||||
if reject.emm_cause.inner
|
||||
== AttachRejectEMMCause::EPSServicesAndNonEPSServicesNotAllowed
|
||||
{
|
||||
self.flag = Some(Event {
|
||||
event_type: EventType::Low,
|
||||
message: "Identity requested without authentication but its likely a false positive unless your SIM card has an active plan".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
|
||||
@@ -262,9 +148,6 @@ impl Analyzer for ImsiRequestedAnalyzer {
|
||||
_ => {}
|
||||
},
|
||||
|
||||
// This causes two messages in the event of a false positive when we should always get an attach reject anyway so
|
||||
// I'm commentingit out until I figure out a smarter way to deal with it.
|
||||
/*
|
||||
LteInformationElement::DlDcch(rrc_payload) => {
|
||||
if let DL_DCCH_MessageType::C1(DL_DCCH_MessageType_c1::RrcConnectionRelease(
|
||||
_,
|
||||
@@ -273,7 +156,6 @@ impl Analyzer for ImsiRequestedAnalyzer {
|
||||
self.transition(State::Disconnect, packet_num)
|
||||
}
|
||||
}
|
||||
*/
|
||||
_ => {}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -52,10 +52,10 @@ build_wifi_tools() {
|
||||
fi
|
||||
|
||||
if ! command -v arm-linux-musleabihf-gcc &> /dev/null; then
|
||||
echo "Warning: Skipping building WiFi tools due to missing C crosscompiler."
|
||||
echo "arm-linux-musleabihf-gcc not found."
|
||||
echo "Error: arm-linux-musleabihf-gcc not found."
|
||||
echo "Install with: brew install FiloSottile/musl-cross/musl-cross"
|
||||
return
|
||||
echo "(Required because the installer bundles wpa_supplicant, wpa_cli, and iw for orbic-family devices.)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Building WiFi tools..."
|
||||
|
||||
Reference in New Issue
Block a user