feat: event management and websocket for updates
This commit is contained in:
parent
005048f1ce
commit
faaa4d2d1a
73
Cargo.lock
generated
73
Cargo.lock
generated
@ -329,6 +329,19 @@ dependencies = [
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-msgpack"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dd328722bb555cc4e7306591aae2d80be83b64c71b183b0e9dd9a16412dc55a"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"hyper",
|
||||
"mime",
|
||||
"rmp-serde",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-server"
|
||||
version = "0.7.1"
|
||||
@ -545,6 +558,8 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d3ad3122b0001c7f140cf4d605ef9a9e2c24d96ab0b4fb4347b76de2425f445"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
@ -2891,6 +2906,28 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rmp"
|
||||
version = "0.8.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"num-traits",
|
||||
"paste",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rmp-serde"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"rmp",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rpassword"
|
||||
version = "7.3.1"
|
||||
@ -3122,18 +3159,27 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.217"
|
||||
version = "1.0.218"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
|
||||
checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.217"
|
||||
name = "serde_bytes"
|
||||
version = "0.11.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
|
||||
checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.218"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -3142,9 +3188,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.138"
|
||||
version = "1.0.139"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
|
||||
checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
@ -3379,6 +3425,9 @@ name = "sparse-actions"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
"chrono",
|
||||
"serde",
|
||||
"serde_bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3387,8 +3436,10 @@ version = "0.7.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"cron",
|
||||
"futures",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
@ -3397,8 +3448,11 @@ dependencies = [
|
||||
"pcap-sys",
|
||||
"pin-project",
|
||||
"rand 0.9.0",
|
||||
"rmp-serde",
|
||||
"rustls",
|
||||
"rustls-openssl",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"simple_logger",
|
||||
"smoltcp",
|
||||
"sparse-actions",
|
||||
@ -3414,13 +3468,16 @@ version = "2.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"axum-msgpack",
|
||||
"axum-server",
|
||||
"chrono",
|
||||
"futures",
|
||||
"rcgen",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sparse-actions",
|
||||
"sqlx",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
@ -3454,7 +3511,9 @@ dependencies = [
|
||||
"rcgen",
|
||||
"rpassword",
|
||||
"rustls-pki-types",
|
||||
"send_wrapper",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"sparse-actions",
|
||||
"sparse-handler",
|
||||
|
||||
@ -4,6 +4,9 @@ edition = "2021"
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4.39", features = ["serde"] }
|
||||
serde = { version = "1.0.218", features = ["derive"] }
|
||||
serde_bytes = "0.11.15"
|
||||
|
||||
[build-dependencies]
|
||||
bindgen = "0.69"
|
||||
|
||||
1
sparse-actions/src/actions.rs
Normal file
1
sparse-actions/src/actions.rs
Normal file
@ -0,0 +1 @@
|
||||
pub trait Action {}
|
||||
@ -3,3 +3,6 @@
|
||||
pub mod payload_types {
|
||||
include!(concat!(std::env!("OUT_DIR"), "/bindings.rs"));
|
||||
}
|
||||
|
||||
pub mod actions;
|
||||
pub mod messages;
|
||||
|
||||
40
sparse-actions/src/messages.rs
Normal file
40
sparse-actions/src/messages.rs
Normal file
@ -0,0 +1,40 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct RegisterBeacon {
|
||||
pub beacon_id: String,
|
||||
pub template_id: u16,
|
||||
pub cwd: PathBuf,
|
||||
pub operating_system: String,
|
||||
pub userent: String,
|
||||
pub hostname: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub enum CronTimezone {
|
||||
Utc,
|
||||
Local,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub enum RuntimeConfig {
|
||||
Oneshot,
|
||||
Random {
|
||||
interval_min: u64,
|
||||
interval_max: u64,
|
||||
},
|
||||
Regular {
|
||||
interval: u64,
|
||||
},
|
||||
Cron {
|
||||
schedule: String,
|
||||
timezone: CronTimezone,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct BeaconConfig {
|
||||
pub runtime_config: RuntimeConfig,
|
||||
}
|
||||
@ -8,7 +8,7 @@ publish = false
|
||||
hyper = { version = "1.6.0", features = ["client", "http1", "http2"] }
|
||||
smoltcp = { version = "0.12.0", default-features = false, features = ["async", "log", "medium-ethernet", "proto-ipv4", "proto-ipv4-fragmentation", "socket-raw", "socket-tcp", "std"] }
|
||||
thiserror = "2.0.11"
|
||||
tokio = { version = "1.43.0", features = ["fs", "io-std", "io-util", "net", "process", "rt", "sync", "tokio-macros"] }
|
||||
tokio = { version = "1.43.0", features = ["fs", "io-std", "io-util", "net", "process", "rt", "sync", "time", "tokio-macros"] }
|
||||
async-trait = "0.1.86"
|
||||
tracing = "0.1.41"
|
||||
rand = "0.9.0"
|
||||
@ -27,6 +27,11 @@ http-body-util = "0.1.2"
|
||||
pcap-sys = { version = "0.1.0", path = "../pcap-sys" }
|
||||
sparse-actions = { version = "2.0.0", path = "../sparse-actions" }
|
||||
packets = { version = "0.1.0", path = "../packets" }
|
||||
serde_json = "1.0.139"
|
||||
serde = "1.0.217"
|
||||
http-body = "1.0.1"
|
||||
rmp-serde = "1.3.0"
|
||||
cron = "0.15.0"
|
||||
|
||||
[features]
|
||||
openssl = ["dep:rustls-openssl"]
|
||||
|
||||
@ -26,7 +26,13 @@ pub struct BeaconInterface {
|
||||
pub trait BeaconAdapter {
|
||||
type Error: error::AdapterError + Send + Sync;
|
||||
|
||||
const OPERATING_SYSTEM: &'static str;
|
||||
|
||||
fn interface_name_from_interface(interface: &BeaconInterface) -> Vec<u8>;
|
||||
|
||||
fn networking_info(&self) -> Result<BeaconNetworkingInfo, error::BeaconError<Self::Error>>;
|
||||
|
||||
async fn get_username(&self) -> Result<String, error::BeaconError<Self::Error>>;
|
||||
|
||||
async fn get_hostname(&self) -> Result<String, error::BeaconError<Self::Error>>;
|
||||
}
|
||||
|
||||
@ -52,6 +52,8 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub type SClient<T, B> = Client<hyper_rustls::HttpsConnector<ServerConnector<T>>, B>;
|
||||
|
||||
pub async fn obtain_https_client<T, B>(
|
||||
adapter: &T,
|
||||
parameters: &Parameters,
|
||||
|
||||
@ -29,4 +29,12 @@ where
|
||||
Rustls(#[from] rustls::Error),
|
||||
#[error("adapter error")]
|
||||
Adapter(#[from] T),
|
||||
#[error("http error from server")]
|
||||
SparseServerHttpError(http::StatusCode),
|
||||
#[error("message pack encode error")]
|
||||
RmpSerdeEncode(#[from] rmp_serde::encode::Error),
|
||||
#[error("message pack decode error")]
|
||||
RmpSerdeDecode(#[from] rmp_serde::decode::Error),
|
||||
#[error("http error")]
|
||||
Hyper(#[from] hyper::Error),
|
||||
}
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
use sparse_actions::payload_types::Parameters;
|
||||
|
||||
use http_body_util::{BodyExt, Empty};
|
||||
use hyper::Request;
|
||||
use http_body_util::{BodyExt, Full};
|
||||
use hyper::{Request, Method};
|
||||
|
||||
use sparse_actions::messages;
|
||||
|
||||
mod callback;
|
||||
mod socket;
|
||||
mod tcp;
|
||||
mod params;
|
||||
|
||||
pub mod adapter;
|
||||
pub mod error;
|
||||
@ -19,6 +22,37 @@ pub fn install_rustls() {
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
}
|
||||
|
||||
pub async fn make_request<A, Req, Resp>(
|
||||
client: &callback::SClient<A, Full<bytes::Bytes>>,
|
||||
uri: hyper::Uri,
|
||||
req_body: Req
|
||||
) -> Result<Resp, BeaconError<A::Error>>
|
||||
where
|
||||
A: adapter::BeaconAdapter + Clone + Send + Sync + 'static,
|
||||
Req: serde::Serialize + Clone + Send + Sync + 'static,
|
||||
Resp: for<'a> serde::Deserialize<'a> + Clone + Send + Sync + 'static,
|
||||
{
|
||||
let mut body_buf = Vec::new();
|
||||
req_body.serialize(&mut rmp_serde::Serializer::new(&mut body_buf))?;
|
||||
|
||||
let req = Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri(uri)
|
||||
.header("content-type", "application/msgpack")
|
||||
.body(Full::<bytes::Bytes>::from(body_buf))?;
|
||||
|
||||
let resp = client.request(req).await?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(BeaconError::SparseServerHttpError(resp.status()));
|
||||
}
|
||||
|
||||
let body = resp.into_body();
|
||||
let body = body.collect().await?;
|
||||
|
||||
rmp_serde::from_slice(&body.to_bytes()).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn run_beacon_step<A>(
|
||||
host_adapter: A,
|
||||
params: Parameters,
|
||||
@ -26,19 +60,54 @@ pub async fn run_beacon_step<A>(
|
||||
where
|
||||
A: adapter::BeaconAdapter + Clone + Send + Sync + 'static,
|
||||
{
|
||||
let hostname = host_adapter.get_hostname().await.unwrap_or("(unknown)".to_string());
|
||||
let userent = host_adapter.get_username().await.unwrap_or("(unknown)".to_string());
|
||||
|
||||
let mut config: messages::BeaconConfig = {
|
||||
let client = callback::obtain_https_client(&host_adapter, ¶ms).await?;
|
||||
|
||||
for _ in 1..5 {
|
||||
let req = Request::builder()
|
||||
.uri("https://sparse.com/hidden_sparse/test".parse::<hyper::Uri>()?)
|
||||
.body(Empty::<bytes::Bytes>::new())?;
|
||||
let resp = client.request(req).await?;
|
||||
dbg!(¶ms.beacon_identifier);
|
||||
|
||||
println!("{:?} {:?}", resp.version(), resp.status());
|
||||
let body = resp.into_body();
|
||||
let body = body.collect().await;
|
||||
println!("{:?}", body);
|
||||
make_request(
|
||||
&client,
|
||||
format!("https://{}/checkin", params::domain_name::<A>(¶ms)?).parse()?,
|
||||
messages::RegisterBeacon {
|
||||
beacon_id: std::str::from_utf8(¶ms.beacon_identifier)?.to_owned(),
|
||||
template_id: params.template_id,
|
||||
cwd: std::env::current_dir()?,
|
||||
operating_system: A::OPERATING_SYSTEM.to_string(),
|
||||
userent: userent.clone(),
|
||||
hostname: hostname.clone()
|
||||
}
|
||||
).await?
|
||||
};
|
||||
|
||||
loop {
|
||||
// let client = callback::obtain_https_client(&host_adapter, ¶ms).await?;
|
||||
|
||||
use messages::RuntimeConfig as RC;
|
||||
let target_wake_time = match &config.runtime_config {
|
||||
RC::Oneshot => { break; },
|
||||
RC::Random { interval_min, interval_max } => {},
|
||||
RC::Regular { interval } => {},
|
||||
RC::Cron { schedule, timezone } => {
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// for _ in 1..5 {
|
||||
// let req = Request::builder()
|
||||
// .uri("https://sparse.com/hidden_sparse/test".parse::<hyper::Uri>()?)
|
||||
// .method()
|
||||
// .body(Empty::<bytes::Bytes>::new())?;
|
||||
// let resp = client.request(req).await?;
|
||||
|
||||
// println!("{:?} {:?}", resp.version(), resp.status());
|
||||
// let body = resp.into_body();
|
||||
// let body = body.collect().await;
|
||||
// println!("{:?}", body);
|
||||
// }
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
12
sparse-beacon/src/params.rs
Normal file
12
sparse-beacon/src/params.rs
Normal file
@ -0,0 +1,12 @@
|
||||
use sparse_actions::payload_types::Parameters;
|
||||
|
||||
use crate::adapter::BeaconAdapter;
|
||||
use crate::error::BeaconError;
|
||||
|
||||
pub fn domain_name<'a, T>(params: &'a Parameters) -> Result<&'a str, BeaconError<T::Error>>
|
||||
where
|
||||
T: BeaconAdapter,
|
||||
{
|
||||
std::str::from_utf8(¶ms.domain_name[..params.domain_name_length as usize])
|
||||
.map_err(Into::into)
|
||||
}
|
||||
21
sparse-beacon/src/prelude.rs
Normal file
21
sparse-beacon/src/prelude.rs
Normal file
@ -0,0 +1,21 @@
|
||||
use sparse_actions::payload_types::Parameters;
|
||||
|
||||
use crate::adapter::BeaconAdapter;
|
||||
use crate::error::{AdapterError, BeaconError};
|
||||
|
||||
pub trait UsableParameters<T>
|
||||
where
|
||||
T: BeaconAdapter,
|
||||
{
|
||||
fn domain_name_str(&self) -> Result<&str, BeaconError<T::Error>>;
|
||||
}
|
||||
|
||||
impl<T> UsableParameters<T> for Parameters
|
||||
where
|
||||
T: BeaconAdapter,
|
||||
{
|
||||
fn domain_name_str(&self) -> Result<&str, BeaconError<T::Error>> {
|
||||
std::str::from_utf8(&self.domain_name[..self.domain_name_length as usize])
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
@ -25,8 +25,6 @@ impl RawSocket {
|
||||
|
||||
let mtu = a_interface.mtu as usize + if cfg!(unix) { 14 } else { 0 };
|
||||
|
||||
dbg!(promisc);
|
||||
|
||||
lower.set_promisc(promisc)?;
|
||||
lower.set_buffer_size(mtu as i32)?;
|
||||
lower.set_non_blocking(true)?;
|
||||
|
||||
50
sparse-handler/.sqlx/query-04472775affd2b1694c05f5ab8125528bb5448e0378a7b9cf3ed58eaa2101d1e.json
generated
Normal file
50
sparse-handler/.sqlx/query-04472775affd2b1694c05f5ab8125528bb5448e0378a7b9cf3ed58eaa2101d1e.json
generated
Normal file
@ -0,0 +1,50 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT c.mode, c.regular_interval, c.random_min_time, c.random_max_time, c.cron_schedule, c.cron_mode\n FROM beacon_template t\n INNER JOIN beacon_config c ON c.config_id = t.config_id\n WHERE t.template_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "mode",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "regular_interval",
|
||||
"ordinal": 1,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "random_min_time",
|
||||
"ordinal": 2,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "random_max_time",
|
||||
"ordinal": 3,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "cron_schedule",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "cron_mode",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "04472775affd2b1694c05f5ab8125528bb5448e0378a7b9cf3ed58eaa2101d1e"
|
||||
}
|
||||
12
sparse-handler/.sqlx/query-6e5e4ddd5c8b60c2180ca4cadf1449dc8b8b37a8887cd5b01f1ca9d0a411b897.json
generated
Normal file
12
sparse-handler/.sqlx/query-6e5e4ddd5c8b60c2180ca4cadf1449dc8b8b37a8887cd5b01f1ca9d0a411b897.json
generated
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT INTO beacon_checkin (beacon_id, checkin_date) VALUES (?, ?)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6e5e4ddd5c8b60c2180ca4cadf1449dc8b8b37a8887cd5b01f1ca9d0a411b897"
|
||||
}
|
||||
50
sparse-handler/.sqlx/query-bb07fb691f373dea848c0368d4c36e4c2b079d2d1efc0006d43d6833023101a3.json
generated
Normal file
50
sparse-handler/.sqlx/query-bb07fb691f373dea848c0368d4c36e4c2b079d2d1efc0006d43d6833023101a3.json
generated
Normal file
@ -0,0 +1,50 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT c.mode as mode, c.regular_interval as regular_interval, c.random_min_time as random_min_time,\n c.random_max_time as random_max_time, c.cron_schedule as cron_schedule, c.cron_mode as cron_mode\n FROM beacon_instance i\n INNER JOIN beacon_config c ON c.config_id = i.config_id\n WHERE i.beacon_id = ?\n UNION\n SELECT c.mode as mode, c.regular_interval as regular_interval, c.random_min_time as random_min_time,\n c.random_max_time as random_max_time, c.cron_schedule as cron_schedule, c.cron_mode as cron_mode\n FROM beacon_instance i\n INNER JOIN beacon_template t ON i.template_id = t.template_id\n INNER JOIN beacon_config c ON t.config_id = c.config_id\n WHERE i.beacon_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "mode",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "regular_interval",
|
||||
"ordinal": 1,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "random_min_time",
|
||||
"ordinal": 2,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "random_max_time",
|
||||
"ordinal": 3,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "cron_schedule",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "cron_mode",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "bb07fb691f373dea848c0368d4c36e4c2b079d2d1efc0006d43d6833023101a3"
|
||||
}
|
||||
@ -1,36 +1,26 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT * FROM beacon_listener WHERE listener_id = ?",
|
||||
"query": "SELECT port, domain_name, certificate, privkey FROM beacon_listener WHERE listener_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "listener_id",
|
||||
"name": "port",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "port",
|
||||
"ordinal": 1,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "public_ip",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "domain_name",
|
||||
"ordinal": 3,
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "certificate",
|
||||
"ordinal": 4,
|
||||
"ordinal": 2,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "privkey",
|
||||
"ordinal": 5,
|
||||
"ordinal": 3,
|
||||
"type_info": "Blob"
|
||||
}
|
||||
],
|
||||
@ -38,13 +28,11 @@
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "e7dc753795b8976b14b5c4baec20d16eff715a4d2ffe93c6723bad368483fb69"
|
||||
"hash": "f130d1b3b891a4f5d57b69bcf111230b85d7fb636517e825a42c2a59fc8b8311"
|
||||
}
|
||||
12
sparse-handler/.sqlx/query-fa45ebf8fb26791336d5ab3701a3fc6fdf17d3eaddb0e5cff099e0396fe4dddd.json
generated
Normal file
12
sparse-handler/.sqlx/query-fa45ebf8fb26791336d5ab3701a3fc6fdf17d3eaddb0e5cff099e0396fe4dddd.json
generated
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT INTO beacon_instance\n (beacon_id, template_id, peer_ip, nickname, cwd, operating_system, beacon_userent, hostname)\n VALUES\n (?, ?, ?, \"\", ?, ?, ?, ?)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 7
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "fa45ebf8fb26791336d5ab3701a3fc6fdf17d3eaddb0e5cff099e0396fe4dddd"
|
||||
}
|
||||
@ -15,5 +15,8 @@ serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
axum-server = { version = "^0.7", features = ["tokio-rustls", "tls-rustls-no-provider"] }
|
||||
rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
|
||||
rcgen = "0.13.2"
|
||||
rcgen = { version = "0.13.2", features = ["pem", "x509-parser", "crypto"] }
|
||||
rustls-pki-types = "1.11.0"
|
||||
axum-msgpack = "0.4.0"
|
||||
sparse-actions = { version = "2.0.0", path = "../sparse-actions" }
|
||||
chrono = { version = "0.4.39", features = ["serde"] }
|
||||
|
||||
@ -1,3 +1,8 @@
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Generic(String),
|
||||
@ -51,6 +56,12 @@ impl std::error::Error for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for Error {
|
||||
fn into_response(self) -> Response {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, format!("{self}")).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for Error {
|
||||
type Err = Self;
|
||||
|
||||
|
||||
@ -1,18 +1,25 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, RwLock},
|
||||
collections::HashMap, net::SocketAddr, sync::{Arc, RwLock}
|
||||
};
|
||||
|
||||
use axum::routing::{Router, get, post};
|
||||
use rcgen::{Certificate, CertificateParams, KeyPair};
|
||||
use rcgen::{CertificateParams, KeyPair};
|
||||
use rustls::{RootCertStore, server::WebPkiClientVerifier};
|
||||
use sqlx::SqlitePool;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::{sync::broadcast, task::JoinHandle};
|
||||
|
||||
pub mod error;
|
||||
|
||||
mod router;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum BeaconEvent {
|
||||
NewBeacon(String),
|
||||
Checkin(String)
|
||||
}
|
||||
|
||||
pub struct BeaconListenerHandle {
|
||||
join_handle: JoinHandle<()>,
|
||||
events_broadcast: broadcast::Sender<BeaconEvent>,
|
||||
}
|
||||
|
||||
impl BeaconListenerHandle {
|
||||
@ -23,6 +30,10 @@ impl BeaconListenerHandle {
|
||||
pub fn abort(&self) {
|
||||
self.join_handle.abort()
|
||||
}
|
||||
|
||||
pub fn event_subscribe(&self) -> broadcast::Receiver<BeaconEvent> {
|
||||
self.events_broadcast.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
@ -39,6 +50,7 @@ impl std::ops::Deref for BeaconListenerMap {
|
||||
pub async fn start_all_listeners(
|
||||
beacon_listener_map: BeaconListenerMap,
|
||||
db: SqlitePool,
|
||||
beacon_event_broadcast: tokio::sync::broadcast::Sender::<BeaconEvent>
|
||||
) -> Result<(), crate::error::Error> {
|
||||
rustls::crypto::ring::default_provider().install_default().expect("could not set up rustls");
|
||||
|
||||
@ -53,6 +65,7 @@ pub async fn start_all_listeners(
|
||||
beacon_listener_map.clone(),
|
||||
listener.listener_id,
|
||||
db.clone(),
|
||||
beacon_event_broadcast.clone(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@ -60,15 +73,8 @@ pub async fn start_all_listeners(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ListenerState {
|
||||
db: SqlitePool,
|
||||
}
|
||||
|
||||
struct Listener {
|
||||
listener_id: i64,
|
||||
port: i64,
|
||||
public_ip: String,
|
||||
domain_name: String,
|
||||
certificate: Vec<u8>,
|
||||
privkey: Vec<u8>,
|
||||
@ -78,6 +84,7 @@ pub async fn start_listener(
|
||||
beacon_listener_map: BeaconListenerMap,
|
||||
listener_id: i64,
|
||||
db: SqlitePool,
|
||||
beacon_event_broadcast: tokio::sync::broadcast::Sender::<BeaconEvent>
|
||||
) -> Result<(), crate::error::Error> {
|
||||
{
|
||||
let Ok(blm_handle) = beacon_listener_map.read() else {
|
||||
@ -94,29 +101,15 @@ pub async fn start_listener(
|
||||
}
|
||||
let listener = sqlx::query_as!(
|
||||
Listener,
|
||||
"SELECT * FROM beacon_listener WHERE listener_id = ?",
|
||||
"SELECT port, domain_name, certificate, privkey FROM beacon_listener WHERE listener_id = ?",
|
||||
listener_id
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
|
||||
let app: Router<()> = Router::new()
|
||||
.route(
|
||||
"/register_beacon",
|
||||
post(|| async {
|
||||
tracing::info!("Beacon attempting to register");
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/test",
|
||||
get(|| async {
|
||||
tracing::info!("Hello");
|
||||
"hi there"
|
||||
}),
|
||||
)
|
||||
.with_state(ListenerState { db });
|
||||
let sender = broadcast::Sender::new(128);
|
||||
|
||||
let hidden_app = Router::new().nest("/hidden_sparse", app);
|
||||
let app = router::get_router(db, sender.clone());
|
||||
|
||||
let ca_cert = rustls::pki_types::CertificateDer::from(listener.certificate.clone());
|
||||
|
||||
@ -175,7 +168,7 @@ pub async fn start_listener(
|
||||
addr,
|
||||
axum_server::tls_rustls::RustlsConfig::from_config(Arc::new(tls_config)),
|
||||
)
|
||||
.serve(hidden_app.into_make_service())
|
||||
.serve(app.into_make_service_with_connect_info::<SocketAddr>())
|
||||
.await;
|
||||
|
||||
if let Err(e) = res {
|
||||
@ -189,7 +182,7 @@ pub async fn start_listener(
|
||||
));
|
||||
};
|
||||
|
||||
blm_handle.insert(listener_id, BeaconListenerHandle { join_handle });
|
||||
blm_handle.insert(listener_id, BeaconListenerHandle { join_handle, events_broadcast: sender });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
149
sparse-handler/src/router.rs
Normal file
149
sparse-handler/src/router.rs
Normal file
@ -0,0 +1,149 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use axum::{extract::{State, ConnectInfo}, routing::post, Router};
|
||||
use axum_msgpack::MsgPack;
|
||||
use sqlx::SqlitePool;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use sparse_actions::messages;
|
||||
|
||||
use crate::{BeaconEvent, error};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ListenerState {
|
||||
db: SqlitePool,
|
||||
event_publisher: broadcast::Sender<BeaconEvent>,
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
pub async fn handle_checkin(
|
||||
State(state): State<ListenerState>,
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
MsgPack(reg): MsgPack<messages::RegisterBeacon>,
|
||||
) -> Result<MsgPack<messages::BeaconConfig>, error::Error> {
|
||||
struct DbBeaconConfig {
|
||||
mode: Option<String>,
|
||||
regular_interval: Option<i64>,
|
||||
random_min_time: Option<i64>,
|
||||
random_max_time: Option<i64>,
|
||||
cron_schedule: Option<String>,
|
||||
cron_mode: Option<String>,
|
||||
}
|
||||
|
||||
use messages::{CronTimezone, RuntimeConfig as RC};
|
||||
|
||||
fn parse_db_config(rec: DbBeaconConfig) -> Option<RC> {
|
||||
Some(match &*rec.mode? {
|
||||
"single" => RC::Oneshot,
|
||||
"regular" => RC::Regular { interval: rec.regular_interval? as u64 },
|
||||
"random" => RC::Random {
|
||||
interval_min: rec.random_min_time? as u64,
|
||||
interval_max: rec.random_max_time? as u64
|
||||
},
|
||||
"cron" => RC::Cron {
|
||||
schedule: rec.cron_schedule?,
|
||||
timezone: match &*rec.cron_mode? {
|
||||
"utc" => CronTimezone::Utc,
|
||||
"local" => CronTimezone::Local,
|
||||
_ => None?
|
||||
}
|
||||
} ,
|
||||
_ => None?
|
||||
})
|
||||
}
|
||||
|
||||
tracing::info!("Beacon {} connecting from {addr}", ®.beacon_id);
|
||||
|
||||
let current_beacon_reg = sqlx::query_as!(
|
||||
DbBeaconConfig,
|
||||
r"SELECT c.mode as mode, c.regular_interval as regular_interval, c.random_min_time as random_min_time,
|
||||
c.random_max_time as random_max_time, c.cron_schedule as cron_schedule, c.cron_mode as cron_mode
|
||||
FROM beacon_instance i
|
||||
INNER JOIN beacon_config c ON c.config_id = i.config_id
|
||||
WHERE i.beacon_id = ?
|
||||
UNION
|
||||
SELECT c.mode as mode, c.regular_interval as regular_interval, c.random_min_time as random_min_time,
|
||||
c.random_max_time as random_max_time, c.cron_schedule as cron_schedule, c.cron_mode as cron_mode
|
||||
FROM beacon_instance i
|
||||
INNER JOIN beacon_template t ON i.template_id = t.template_id
|
||||
INNER JOIN beacon_config c ON t.config_id = c.config_id
|
||||
WHERE i.beacon_id = ?"r,
|
||||
reg.beacon_id,
|
||||
reg.beacon_id
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await?;
|
||||
|
||||
let current_beacon_reg = match current_beacon_reg {
|
||||
Some(rec) => {
|
||||
parse_db_config(rec)
|
||||
},
|
||||
None => {
|
||||
let ip = format!("{}", addr.ip());
|
||||
let cwd = reg
|
||||
.cwd
|
||||
.to_str()
|
||||
.unwrap_or("(unknown)");
|
||||
sqlx::query!(
|
||||
r#"INSERT INTO beacon_instance
|
||||
(beacon_id, template_id, peer_ip, nickname, cwd, operating_system, beacon_userent, hostname)
|
||||
VALUES
|
||||
(?, ?, ?, "", ?, ?, ?, ?)"#r,
|
||||
reg.beacon_id,
|
||||
reg.template_id,
|
||||
ip,
|
||||
cwd,
|
||||
reg.operating_system,
|
||||
reg.userent,
|
||||
reg.hostname
|
||||
)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
|
||||
let rec = sqlx::query_as!(
|
||||
DbBeaconConfig,
|
||||
r"SELECT c.mode, c.regular_interval, c.random_min_time, c.random_max_time, c.cron_schedule, c.cron_mode
|
||||
FROM beacon_template t
|
||||
INNER JOIN beacon_config c ON c.config_id = t.config_id
|
||||
WHERE t.template_id = ?",
|
||||
reg.template_id
|
||||
)
|
||||
.fetch_one(&state.db)
|
||||
.await?;
|
||||
|
||||
parse_db_config(rec)
|
||||
}
|
||||
};
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
sqlx::query!(
|
||||
r"INSERT INTO beacon_checkin (beacon_id, checkin_date) VALUES (?, ?)"r,
|
||||
reg.beacon_id,
|
||||
now
|
||||
)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
|
||||
let current_beacon_reg = current_beacon_reg
|
||||
.ok_or(error::Error::Generic("could not load configuration".to_string()))?;
|
||||
|
||||
Ok(MsgPack(messages::BeaconConfig {
|
||||
runtime_config: current_beacon_reg
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn get_router(db: SqlitePool, event_publisher: broadcast::Sender<BeaconEvent>) -> Router<()> {
|
||||
Router::new()
|
||||
.route(
|
||||
"/checkin",
|
||||
post(handle_checkin),
|
||||
)
|
||||
.route(
|
||||
"/upload/:beaconid/:commandid",
|
||||
post(|| async {
|
||||
tracing::info!("Hello");
|
||||
"hi there"
|
||||
}),
|
||||
)
|
||||
.with_state(ListenerState { db, event_publisher })
|
||||
}
|
||||
50
sparse-server/.sqlx/query-04472775affd2b1694c05f5ab8125528bb5448e0378a7b9cf3ed58eaa2101d1e.json
generated
Normal file
50
sparse-server/.sqlx/query-04472775affd2b1694c05f5ab8125528bb5448e0378a7b9cf3ed58eaa2101d1e.json
generated
Normal file
@ -0,0 +1,50 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT c.mode, c.regular_interval, c.random_min_time, c.random_max_time, c.cron_schedule, c.cron_mode\n FROM beacon_template t\n INNER JOIN beacon_config c ON c.config_id = t.config_id\n WHERE t.template_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "mode",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "regular_interval",
|
||||
"ordinal": 1,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "random_min_time",
|
||||
"ordinal": 2,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "random_max_time",
|
||||
"ordinal": 3,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "cron_schedule",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "cron_mode",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "04472775affd2b1694c05f5ab8125528bb5448e0378a7b9cf3ed58eaa2101d1e"
|
||||
}
|
||||
12
sparse-server/.sqlx/query-6e5e4ddd5c8b60c2180ca4cadf1449dc8b8b37a8887cd5b01f1ca9d0a411b897.json
generated
Normal file
12
sparse-server/.sqlx/query-6e5e4ddd5c8b60c2180ca4cadf1449dc8b8b37a8887cd5b01f1ca9d0a411b897.json
generated
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT INTO beacon_checkin (beacon_id, checkin_date) VALUES (?, ?)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6e5e4ddd5c8b60c2180ca4cadf1449dc8b8b37a8887cd5b01f1ca9d0a411b897"
|
||||
}
|
||||
68
sparse-server/.sqlx/query-75edc2bc9adda52aa7e9cd68f980db95744690cb3fc1b9cccfb3ab6f63d0ab25.json
generated
Normal file
68
sparse-server/.sqlx/query-75edc2bc9adda52aa7e9cd68f980db95744690cb3fc1b9cccfb3ab6f63d0ab25.json
generated
Normal file
@ -0,0 +1,68 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT beacon_id, template_id, peer_ip, nickname, cwd, operating_system, beacon_userent, hostname, config_id FROM beacon_instance",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "beacon_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "template_id",
|
||||
"ordinal": 1,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "peer_ip",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "nickname",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "cwd",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "operating_system",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "beacon_userent",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "hostname",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "config_id",
|
||||
"ordinal": 8,
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "75edc2bc9adda52aa7e9cd68f980db95744690cb3fc1b9cccfb3ab6f63d0ab25"
|
||||
}
|
||||
50
sparse-server/.sqlx/query-bb07fb691f373dea848c0368d4c36e4c2b079d2d1efc0006d43d6833023101a3.json
generated
Normal file
50
sparse-server/.sqlx/query-bb07fb691f373dea848c0368d4c36e4c2b079d2d1efc0006d43d6833023101a3.json
generated
Normal file
@ -0,0 +1,50 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT c.mode as mode, c.regular_interval as regular_interval, c.random_min_time as random_min_time,\n c.random_max_time as random_max_time, c.cron_schedule as cron_schedule, c.cron_mode as cron_mode\n FROM beacon_instance i\n INNER JOIN beacon_config c ON c.config_id = i.config_id\n WHERE i.beacon_id = ?\n UNION\n SELECT c.mode as mode, c.regular_interval as regular_interval, c.random_min_time as random_min_time,\n c.random_max_time as random_max_time, c.cron_schedule as cron_schedule, c.cron_mode as cron_mode\n FROM beacon_instance i\n INNER JOIN beacon_template t ON i.template_id = t.template_id\n INNER JOIN beacon_config c ON t.config_id = c.config_id\n WHERE i.beacon_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "mode",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "regular_interval",
|
||||
"ordinal": 1,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "random_min_time",
|
||||
"ordinal": 2,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "random_max_time",
|
||||
"ordinal": 3,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "cron_schedule",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "cron_mode",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "bb07fb691f373dea848c0368d4c36e4c2b079d2d1efc0006d43d6833023101a3"
|
||||
}
|
||||
26
sparse-server/.sqlx/query-e7dddc194dcb297f672a9270f801ea23192e9fa53559b9fcfcffddeb3f571d15.json
generated
Normal file
26
sparse-server/.sqlx/query-e7dddc194dcb297f672a9270f801ea23192e9fa53559b9fcfcffddeb3f571d15.json
generated
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT beacon_id, category_id FROM beacon_category_assignment",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "beacon_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "category_id",
|
||||
"ordinal": 1,
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "e7dddc194dcb297f672a9270f801ea23192e9fa53559b9fcfcffddeb3f571d15"
|
||||
}
|
||||
@ -1,36 +1,26 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT * FROM beacon_listener WHERE listener_id = ?",
|
||||
"query": "SELECT port, domain_name, certificate, privkey FROM beacon_listener WHERE listener_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "listener_id",
|
||||
"name": "port",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "port",
|
||||
"ordinal": 1,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "public_ip",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "domain_name",
|
||||
"ordinal": 3,
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "certificate",
|
||||
"ordinal": 4,
|
||||
"ordinal": 2,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "privkey",
|
||||
"ordinal": 5,
|
||||
"ordinal": 3,
|
||||
"type_info": "Blob"
|
||||
}
|
||||
],
|
||||
@ -38,13 +28,11 @@
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "e7dc753795b8976b14b5c4baec20d16eff715a4d2ffe93c6723bad368483fb69"
|
||||
"hash": "f130d1b3b891a4f5d57b69bcf111230b85d7fb636517e825a42c2a59fc8b8311"
|
||||
}
|
||||
12
sparse-server/.sqlx/query-fa45ebf8fb26791336d5ab3701a3fc6fdf17d3eaddb0e5cff099e0396fe4dddd.json
generated
Normal file
12
sparse-server/.sqlx/query-fa45ebf8fb26791336d5ab3701a3fc6fdf17d3eaddb0e5cff099e0396fe4dddd.json
generated
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT INTO beacon_instance\n (beacon_id, template_id, peer_ip, nickname, cwd, operating_system, beacon_userent, hostname)\n VALUES\n (?, ?, ?, \"\", ?, ?, ?, ?)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 7
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "fa45ebf8fb26791336d5ab3701a3fc6fdf17d3eaddb0e5cff099e0396fe4dddd"
|
||||
}
|
||||
@ -30,7 +30,7 @@ futures-util = { version = "0.3", optional = true }
|
||||
tracing = { version = "0.1", optional = true }
|
||||
web-sys = { version = "0.3", features = ["WebSocket"] }
|
||||
leptos-use = { version = "0.15", default-features = false, features = ["use_websocket", "use_interval"] }
|
||||
codee = "0.2"
|
||||
codee = { version = "0.2", features = ["json_serde"] }
|
||||
sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "sqlx-sqlite"], optional = true }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
rpassword = { version = "7.3", optional = true }
|
||||
@ -46,6 +46,8 @@ rand = { version = "0.9", optional = true }
|
||||
|
||||
sparse-actions = { path = "../sparse-actions", optional = true }
|
||||
sparse-handler = { path = "../sparse-handler", optional = true }
|
||||
serde_json = "1.0.139"
|
||||
send_wrapper = "0.6.0"
|
||||
|
||||
[features]
|
||||
embed-beacons = []
|
||||
|
||||
@ -0,0 +1,19 @@
|
||||
DROP TABLE beacon_config_assignment;
|
||||
DROP TABLE beacon_instance;
|
||||
|
||||
CREATE TABLE beacon_instance (
|
||||
beacon_id varchar PRIMARY KEY NOT NULL,
|
||||
template_id int NOT NULL,
|
||||
peer_ip varchar NOT NULL,
|
||||
nickname varchar NOT NULL,
|
||||
|
||||
cwd varchar NOT NULL,
|
||||
operating_system varchar NOT NULL,
|
||||
beacon_userent varchar NOT NULL,
|
||||
hostname varchar NOT NULL,
|
||||
|
||||
config_id int,
|
||||
|
||||
FOREIGN KEY (template_id) REFERENCES beacon_template,
|
||||
FOREIGN KEY (config_id) REFERENCES beacon_config
|
||||
);
|
||||
11
sparse-server/migrations/20250222071958_new_assignments.sql
Normal file
11
sparse-server/migrations/20250222071958_new_assignments.sql
Normal file
@ -0,0 +1,11 @@
|
||||
DROP TABLE beacon_category_assignment;
|
||||
|
||||
CREATE TABLE beacon_category_assignment (
|
||||
category_id int NOT NULL,
|
||||
beacon_id varchar NOT NULL,
|
||||
|
||||
PRIMARY KEY (category_id, beacon_id),
|
||||
|
||||
FOREIGN KEY (category_id) REFERENCES beacon_category,
|
||||
FOREIGN KEY (beacon_id) REFERENCES beacon_instance
|
||||
);
|
||||
@ -56,6 +56,7 @@ pub fn App() -> impl IntoView {
|
||||
|
||||
let login = ServerAction::<Login>::new();
|
||||
|
||||
#[cfg_attr(not(feature = "hydrate"), allow(unused_variables))]
|
||||
let (user_res, set_user_res) = signal(None::<User>);
|
||||
|
||||
let user = Resource::new(move || login.version().get(), |_| async { me().await });
|
||||
@ -77,11 +78,11 @@ pub fn App() -> impl IntoView {
|
||||
|
||||
<Router>
|
||||
<nav>
|
||||
<h1>"Sparse control"</h1>
|
||||
<h1>"Sparse Control"</h1>
|
||||
<A href="/">"Home"</A>
|
||||
{move || match user_res.get() {
|
||||
Some(_) => Either::Left(view! {
|
||||
<A href="/beacons">"Beacon management"</A>
|
||||
<A href="/beacons">"Beacon Management"</A>
|
||||
<A href="/users">"Users"</A>
|
||||
<a
|
||||
href="#"
|
||||
@ -101,18 +102,18 @@ pub fn App() -> impl IntoView {
|
||||
}}
|
||||
</nav>
|
||||
|
||||
<crate::beacons::BeaconSidebar />
|
||||
<crate::beacons::sidebar::BeaconSidebar />
|
||||
|
||||
<Routes fallback=|| "Page not found.".into_view()>
|
||||
<Route path=path!("users") view=crate::users::UserView />
|
||||
<Route path=path!("login") view=move || view! { <LoginPage login/> }/>
|
||||
<ParentRoute path=path!("beacons") view=crate::beacons::BeaconView>
|
||||
<Route path=path!("categories") view=crate::beacons::CategoriesView/>
|
||||
<Route path=path!("commands") view=crate::beacons::CommandsView/>
|
||||
<Route path=path!("configs") view=crate::beacons::ConfigsView/>
|
||||
<Route path=path!("templates") view=crate::beacons::TemplatesView/>
|
||||
<Route path=path!("instances") view=crate::beacons::InstancesView/>
|
||||
<Route path=path!("listeners") view=crate::beacons::ListenersView/>
|
||||
<Route path=path!("categories") view=crate::beacons::categories::CategoriesView/>
|
||||
<Route path=path!("commands") view=crate::beacons::commands::CommandsView/>
|
||||
<Route path=path!("configs") view=crate::beacons::configs::ConfigsView/>
|
||||
<Route path=path!("templates") view=crate::beacons::templates::TemplatesView/>
|
||||
<Route path=path!("instances") view=crate::beacons::instances::InstancesView/>
|
||||
<Route path=path!("listeners") view=crate::beacons::listeners::ListenersView/>
|
||||
<Route path=path!("") view=|| view! {
|
||||
<p>"Select a menu item on the left to get started"</p>
|
||||
}/>
|
||||
|
||||
@ -1,25 +1,13 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::{components::A, nested_router::Outlet};
|
||||
|
||||
mod categories;
|
||||
mod commands;
|
||||
mod configs;
|
||||
mod instances;
|
||||
mod listeners;
|
||||
mod templates;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub use categories::CategoriesView;
|
||||
#[allow(dead_code)]
|
||||
pub use commands::CommandsView;
|
||||
#[allow(dead_code)]
|
||||
pub use configs::ConfigsView;
|
||||
#[allow(dead_code)]
|
||||
pub use instances::InstancesView;
|
||||
#[allow(dead_code)]
|
||||
pub use listeners::ListenersView;
|
||||
#[allow(dead_code)]
|
||||
pub use templates::TemplatesView;
|
||||
pub mod categories;
|
||||
pub mod commands;
|
||||
pub mod configs;
|
||||
pub mod instances;
|
||||
pub mod listeners;
|
||||
pub mod templates;
|
||||
pub mod sidebar;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BeaconResources {
|
||||
@ -39,6 +27,9 @@ pub struct BeaconResources {
|
||||
templates: Resource<Result<Vec<templates::BeaconTemplate>, ServerFnError>>,
|
||||
}
|
||||
|
||||
// For some reason, this function "isn't used"
|
||||
// See app.rs:72
|
||||
#[allow(dead_code)]
|
||||
pub fn provide_beacon_resources() {
|
||||
let user = expect_context::<ReadSignal<Option<crate::users::User>>>();
|
||||
|
||||
@ -146,53 +137,3 @@ pub fn BeaconView() -> impl IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
enum SortMethod {
|
||||
Listener,
|
||||
Config,
|
||||
Category,
|
||||
Template,
|
||||
}
|
||||
|
||||
impl std::str::FromStr for SortMethod {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"Listener" => Ok(Self::Listener),
|
||||
"Config" => Ok(Self::Config),
|
||||
"Category" => Ok(Self::Category),
|
||||
"Template" => Ok(Self::Template),
|
||||
&_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::string::ToString for SortMethod {
|
||||
fn to_string(&self) -> String {
|
||||
use SortMethod as SM;
|
||||
match self {
|
||||
SM::Listener => "Listener",
|
||||
SM::Config => "Config",
|
||||
SM::Category => "Category",
|
||||
SM::Template => "Template",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn BeaconSidebar() -> impl IntoView {
|
||||
let (sort_method, set_sort_method) = signal(SortMethod::Category);
|
||||
let search_input = RwSignal::new("".to_string());
|
||||
|
||||
view! {
|
||||
<aside class="beacons">
|
||||
<div class="sort-method">
|
||||
|
||||
</div>
|
||||
<div class="search">
|
||||
|
||||
</div>
|
||||
</aside>
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use leptos::{either::Either, prelude::*};
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(feature = "ssr")]
|
||||
use {crate::db::user, leptos::server_fn::error::NoCustomError, sqlx::SqlitePool};
|
||||
use {crate::db::user, leptos::server_fn::error::NoCustomError};
|
||||
|
||||
use super::BeaconResources;
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
|
||||
use {
|
||||
crate::db::user,
|
||||
leptos::server_fn::error::NoCustomError,
|
||||
sqlx::{sqlite::SqliteRow, FromRow, Row, SqlitePool},
|
||||
sqlx::{sqlite::SqliteRow, FromRow, Row},
|
||||
std::str::FromStr,
|
||||
};
|
||||
|
||||
|
||||
@ -7,9 +7,7 @@ use serde::{Deserialize, Serialize};
|
||||
use {
|
||||
crate::db::user,
|
||||
leptos::server_fn::error::NoCustomError,
|
||||
rcgen::{generate_simple_self_signed, CertifiedKey},
|
||||
sparse_handler::BeaconListenerMap,
|
||||
sqlx::SqlitePool,
|
||||
sparse_handler::BeaconListenerMap
|
||||
};
|
||||
|
||||
use super::BeaconResources;
|
||||
@ -185,7 +183,12 @@ pub async fn start_listener(listener_id: i64) -> Result<(), ServerFnError> {
|
||||
));
|
||||
}
|
||||
|
||||
sparse_handler::start_listener(expect_context(), listener_id, expect_context()).await?;
|
||||
sparse_handler::start_listener(
|
||||
expect_context(),
|
||||
listener_id,
|
||||
expect_context(),
|
||||
expect_context()
|
||||
).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
311
sparse-server/src/beacons/sidebar.rs
Normal file
311
sparse-server/src/beacons/sidebar.rs
Normal file
@ -0,0 +1,311 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use leptos::prelude::*;
|
||||
#[cfg(feature = "hydrate")]
|
||||
use leptos_use::{use_websocket, UseWebSocketReturn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::beacons::BeaconResources;
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
use super::templates::BeaconTemplate;
|
||||
|
||||
#[derive(Clone)]
|
||||
enum SortMethod {
|
||||
Listener,
|
||||
Config,
|
||||
Category,
|
||||
Template,
|
||||
}
|
||||
|
||||
impl std::str::FromStr for SortMethod {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"Listener" => Ok(Self::Listener),
|
||||
"Config" => Ok(Self::Config),
|
||||
"Category" => Ok(Self::Category),
|
||||
"Template" => Ok(Self::Template),
|
||||
&_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::string::ToString for SortMethod {
|
||||
fn to_string(&self) -> String {
|
||||
use SortMethod as SM;
|
||||
match self {
|
||||
SM::Listener => "Listener",
|
||||
SM::Config => "Config",
|
||||
SM::Category => "Category",
|
||||
SM::Template => "Template",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct CurrentBeaconInstance {
|
||||
pub beacon_id: String,
|
||||
pub ip: String,
|
||||
pub nickname: String,
|
||||
pub cwd: String,
|
||||
pub operating_system: String,
|
||||
pub userent: String,
|
||||
pub hostname: String,
|
||||
pub last_checkin: chrono::DateTime<chrono::Utc>,
|
||||
pub config_id: Option<i64>,
|
||||
pub template_id: i64,
|
||||
pub category_ids: Vec<i64>,
|
||||
}
|
||||
|
||||
// Safety: the only time this comes up is on the client, which is
|
||||
// not a multi-threaded environment
|
||||
unsafe impl Send for CurrentBeaconInstance {}
|
||||
unsafe impl Send for SidebarEvents {}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub enum SidebarEvents {
|
||||
BeaconList(Vec<CurrentBeaconInstance>),
|
||||
NewBeacon(CurrentBeaconInstance),
|
||||
Checkin(String),
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn BeaconSidebar() -> impl IntoView {
|
||||
let BeaconResources {
|
||||
listeners,
|
||||
templates,
|
||||
configs,
|
||||
categories,
|
||||
..
|
||||
} = expect_context::<BeaconResources>();
|
||||
|
||||
let current_beacons = RwSignal::new(None::<Vec<CurrentBeaconInstance>>);
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
let (web_socket, rebuild_websocket) = signal(use_websocket::<
|
||||
(),
|
||||
SidebarEvents,
|
||||
codee::string::JsonSerdeCodec,
|
||||
>("/api/subscribe/listener"));
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
Effect::new(move |_| {
|
||||
web_socket.with(move |uwsr| {
|
||||
uwsr.message.with(move |message| {
|
||||
let Some(m) = message else {
|
||||
return;
|
||||
};
|
||||
|
||||
match m {
|
||||
SidebarEvents::BeaconList(bs) => {
|
||||
let mut bs = bs.to_vec();
|
||||
bs.sort_by_key(|b| b.last_checkin);
|
||||
current_beacons.set(Some(bs));
|
||||
}
|
||||
SidebarEvents::NewBeacon(b) => {
|
||||
current_beacons.update(|bso| {
|
||||
if let Some(ref mut bs) = bso {
|
||||
bs.push(b.clone())
|
||||
}
|
||||
});
|
||||
}
|
||||
SidebarEvents::Checkin(bid) => current_beacons.update(|bs| {
|
||||
let Some(ref mut bs) = bs else {
|
||||
return;
|
||||
};
|
||||
if let Some(ref mut b) = bs.iter_mut().find(|b| b.beacon_id == *bid) {
|
||||
b.last_checkin = chrono::Utc::now();
|
||||
}
|
||||
}),
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
Effect::new(move |_| {
|
||||
let user = expect_context::<ReadSignal<Option<crate::users::User>>>();
|
||||
user.with(move |_| {
|
||||
rebuild_websocket(use_websocket::<
|
||||
(),
|
||||
SidebarEvents,
|
||||
codee::string::JsonSerdeCodec,
|
||||
>("/api/subscribe/listener"));
|
||||
});
|
||||
});
|
||||
|
||||
let (sort_method, set_sort_method) = signal(None::<SortMethod>);
|
||||
let search_input = RwSignal::new("".to_string());
|
||||
|
||||
struct BeaconPartition {
|
||||
title: Option<String>,
|
||||
beacons: Arc<dyn Fn() -> Vec<CurrentBeaconInstance> + Send + Sync>,
|
||||
}
|
||||
|
||||
// Safety: BeaconPartition is only ever constructed on the client side,
|
||||
// where there is only one thread
|
||||
unsafe impl Send for BeaconPartition {}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
let partitions = move || -> Vec<BeaconPartition> {
|
||||
leptos::logging::log!(
|
||||
"There are {:?} beacons",
|
||||
current_beacons.read().as_ref().map(Vec::len)
|
||||
);
|
||||
let sm = sort_method.read();
|
||||
|
||||
//let Some(Ok(ref listeners)) = *listeners.read() else {
|
||||
// return vec![];
|
||||
//};
|
||||
//let Some(Ok(ref templates)) = *templates.read() else {
|
||||
// return vec![];
|
||||
//};
|
||||
//let Some(Ok(ref categories)) = *categories.read() else {
|
||||
// return vec![];
|
||||
//};
|
||||
|
||||
match *sm {
|
||||
Some(SortMethod::Config) => {
|
||||
let Some(Ok(ref configs)) = *configs.read() else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
configs
|
||||
.iter()
|
||||
.map(|config| {
|
||||
let config = config.clone();
|
||||
BeaconPartition {
|
||||
title: Some(config.config_name.clone()),
|
||||
beacons: Arc::new(move || {
|
||||
let Some(Ok(ref templates)) = *templates.read() else {
|
||||
return vec![];
|
||||
};
|
||||
current_beacons
|
||||
.get()
|
||||
.unwrap_or(vec![])
|
||||
.iter()
|
||||
.filter(|b| {
|
||||
b.config_id.or(templates
|
||||
.iter()
|
||||
.find(|t| t.template_id == b.template_id)
|
||||
.map(|t| t.config_id))
|
||||
== Some(config.config_id)
|
||||
})
|
||||
.map(Clone::clone)
|
||||
.collect()
|
||||
}),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
Some(SortMethod::Listener) => {
|
||||
let Some(Ok(ref listeners)) = *listeners.read() else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
listeners
|
||||
.iter()
|
||||
.map(|listener| {
|
||||
let listener = listener.clone();
|
||||
BeaconPartition {
|
||||
title: Some(listener.domain_name.clone()),
|
||||
beacons: Arc::new(move || {
|
||||
let Some(Ok(ref templates)) = *templates.read() else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
current_beacons
|
||||
.get()
|
||||
.unwrap_or(vec![])
|
||||
.iter()
|
||||
.filter(|b| {
|
||||
templates
|
||||
.iter()
|
||||
.find(|t| t.template_id == b.template_id)
|
||||
.map(|t| t.listener_id == listener.listener_id)
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.map(Clone::clone)
|
||||
.collect()
|
||||
}),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
_ => vec![BeaconPartition {
|
||||
title: None,
|
||||
beacons: Arc::new(move || current_beacons.get().unwrap_or(vec![]).clone()),
|
||||
}],
|
||||
}
|
||||
};
|
||||
|
||||
// Safety: because this constructs nothing, it maintains the Safety
|
||||
// invariants above
|
||||
#[cfg(feature = "ssr")]
|
||||
let partitions = || {
|
||||
vec![BeaconPartition {
|
||||
title: None,
|
||||
beacons: Arc::new(move || vec![]),
|
||||
}]
|
||||
};
|
||||
|
||||
view! {
|
||||
<aside class="beacons">
|
||||
<div class="sort-method">
|
||||
<p>"Sort beacons by:"</p>
|
||||
<select
|
||||
name="beacon-sort"
|
||||
on:change:target=move |ev| {
|
||||
set_sort_method(ev.target().value().parse().ok());
|
||||
}
|
||||
prop:value=move || sort_method
|
||||
.get()
|
||||
.as_ref()
|
||||
.map(SortMethod::to_string)
|
||||
.unwrap_or("".to_string())
|
||||
>
|
||||
<option value="">"---"</option>
|
||||
<option value="Listener">"Listener"</option>
|
||||
<option value="Config">"Config"</option>
|
||||
<option value="Category">"Category"</option>
|
||||
<option value="Template">"Template"</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="search">
|
||||
<input bind:value=search_input name="beacon-search" placeholder="Search..." />
|
||||
</div>
|
||||
|
||||
<Suspense fallback=|| view! { "Loading..." }>
|
||||
<div class="beacon-list">
|
||||
{move || partitions()
|
||||
.iter()
|
||||
.map(|partition| view! {
|
||||
<div class="beacon-partition">
|
||||
{partition.title.as_ref().map(|title| view! {
|
||||
<div class="partition-title">
|
||||
{title.clone()}
|
||||
</div>
|
||||
})}
|
||||
|
||||
<For
|
||||
each={
|
||||
let beacons = Arc::clone(&partition.beacons);
|
||||
move || (beacons)()
|
||||
}
|
||||
key=|b| b.beacon_id.clone()
|
||||
let:beacon
|
||||
>
|
||||
<div>{beacon.beacon_id.clone()}</div>
|
||||
</For>
|
||||
</div>
|
||||
})
|
||||
.collect_view()}
|
||||
</div>
|
||||
</Suspense>
|
||||
</aside>
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
|
||||
use {
|
||||
crate::db::user,
|
||||
leptos::server_fn::error::NoCustomError,
|
||||
sqlx::{sqlite::SqliteRow, FromRow, Row, SqlitePool},
|
||||
sqlx::{sqlite::SqliteRow, FromRow, Row},
|
||||
std::net::Ipv4Addr,
|
||||
};
|
||||
|
||||
@ -35,18 +35,18 @@ impl FromRow<'_, SqliteRow> for BeaconSourceMode {
|
||||
#[cfg_attr(feature = "ssr", derive(FromRow))]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct BeaconTemplate {
|
||||
template_id: i64,
|
||||
template_name: String,
|
||||
operating_system: String,
|
||||
pub template_id: i64,
|
||||
pub template_name: String,
|
||||
pub operating_system: String,
|
||||
|
||||
source_ip: String,
|
||||
source_mac: Option<String>,
|
||||
pub source_ip: String,
|
||||
pub source_mac: Option<String>,
|
||||
#[cfg_attr(feature = "ssr", sqlx(flatten))]
|
||||
source_mode: BeaconSourceMode,
|
||||
pub source_mode: BeaconSourceMode,
|
||||
|
||||
config_id: i64,
|
||||
listener_id: i64,
|
||||
default_category: Option<i64>,
|
||||
pub config_id: i64,
|
||||
pub listener_id: i64,
|
||||
pub default_category: Option<i64>,
|
||||
}
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
@ -113,12 +113,12 @@ pub async fn add_template(
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
|
||||
use rcgen::{Certificate, CertificateParams, KeyPair};
|
||||
use rcgen::{CertificateParams, KeyPair};
|
||||
|
||||
let keypair = KeyPair::from_der_and_sign_algo(
|
||||
match &rustls_pki_types::PrivateKeyDer::try_from(&*listener.privkey) {
|
||||
Ok(pk) => pk,
|
||||
Err(e) => {
|
||||
Err(_) => {
|
||||
srverr!("Could not parse private key: {e}");
|
||||
}
|
||||
},
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
use axum_extra::extract::cookie::CookieJar;
|
||||
use leptos::{prelude::*, server_fn::error::NoCustomError};
|
||||
use leptos_axum::{extract, ResponseOptions};
|
||||
use pbkdf2::{
|
||||
@ -196,15 +197,10 @@ pub async fn destroy_auth_session() -> Result<(), ServerFnError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_auth_session() -> Result<Option<User>, ServerFnError> {
|
||||
use axum_extra::extract::cookie::CookieJar;
|
||||
|
||||
println!("In get auth session");
|
||||
let owner = leptos::prelude::Owner::current().unwrap();
|
||||
|
||||
let db = crate::db::get_db()?;
|
||||
let jar = extract::<CookieJar>().await?;
|
||||
|
||||
pub async fn get_auth_session_inner(
|
||||
db: SqlitePool,
|
||||
jar: CookieJar
|
||||
) -> Result<Option<User>, crate::error::Error> {
|
||||
let Some(cookie) = jar.get(SESSION_ID_KEY) else {
|
||||
return Ok(None);
|
||||
};
|
||||
@ -252,3 +248,12 @@ pub async fn get_auth_session() -> Result<Option<User>, ServerFnError> {
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
pub async fn get_auth_session() -> Result<Option<User>, ServerFnError> {
|
||||
let db = crate::db::get_db()?;
|
||||
let jar = extract::<CookieJar>().await?;
|
||||
|
||||
get_auth_session_inner(db, jar)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
@ -10,7 +10,10 @@ pub enum Error {
|
||||
Pbkdf2(pbkdf2::password_hash::errors::Error),
|
||||
#[cfg(feature = "ssr")]
|
||||
Io(std::io::Error),
|
||||
#[cfg(feature = "ssr")]
|
||||
Axum(axum::Error),
|
||||
AddrParse(std::net::AddrParseError),
|
||||
Json(serde_json::Error),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
@ -41,6 +44,13 @@ impl std::fmt::Display for Error {
|
||||
Error::AddrParse(err) => {
|
||||
write!(f, "ip address parse error: {err:?}")
|
||||
}
|
||||
Error::Json(err) => {
|
||||
write!(f, "json encode/decode error: {err:?}")
|
||||
}
|
||||
#[cfg(feature = "ssr")]
|
||||
Error::Axum(err) => {
|
||||
write!(f, "axum error: {err:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -55,6 +65,9 @@ impl std::error::Error for Error {
|
||||
#[cfg(feature = "ssr")]
|
||||
Error::Io(err) => Some(err),
|
||||
Error::AddrParse(err) => Some(err),
|
||||
Error::Json(err) => Some(err),
|
||||
#[cfg(feature = "ssr")]
|
||||
Error::Axum(err) => Some(err),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@ -112,3 +125,16 @@ impl From<std::net::AddrParseError> for Error {
|
||||
Self::AddrParse(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for Error {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
Self::Json(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
impl From<axum::Error> for Error {
|
||||
fn from(err: axum::Error) -> Self {
|
||||
Self::Axum(err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,7 +21,7 @@ async fn main() -> anyhow::Result<std::process::ExitCode> {
|
||||
tracing_subscriber::registry()
|
||||
.with(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
||||
format!("{}=debug,sparse_handler=debug,tower_http=trace", env!("CARGO_CRATE_NAME")).into()
|
||||
format!("{}=debug,sparse_handler=debug", env!("CARGO_CRATE_NAME")).into()
|
||||
}),
|
||||
)
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
|
||||
1
sparse-server/src/socket.rs
Normal file
1
sparse-server/src/socket.rs
Normal file
@ -0,0 +1 @@
|
||||
|
||||
@ -2,7 +2,7 @@ use chrono::{offset::Utc, DateTime};
|
||||
use leptos::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(feature = "ssr")]
|
||||
use {crate::db::user, leptos::server_fn::error::NoCustomError, sqlx::SqlitePool};
|
||||
use {crate::db::user, leptos::server_fn::error::NoCustomError};
|
||||
|
||||
pub fn format_delta(time: chrono::TimeDelta) -> String {
|
||||
let seconds = time.num_seconds();
|
||||
@ -98,9 +98,9 @@ async fn reset_password(user_id: i64, password: String) -> Result<(), ServerFnEr
|
||||
pub fn RenderUser(refresh_user_list: Action<(), ()>, user: PubUser) -> impl IntoView {
|
||||
use leptos_use::{use_interval, UseIntervalReturn};
|
||||
|
||||
#[cfg_attr(feature = "ssr", allow(unused_variables))]
|
||||
#[cfg(feature = "hydrate")]
|
||||
let UseIntervalReturn { counter, .. } = use_interval(1000);
|
||||
#[cfg_attr(feature = "ssr", allow(unused_variables))]
|
||||
#[cfg_attr(not(feature = "hydrate"), allow(unused_variables))]
|
||||
let (time_ago, set_time_ago) = signal(
|
||||
user.last_active
|
||||
.map(|active| format_delta(Utc::now() - active)),
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
use std::{net::SocketAddrV4, process::ExitCode};
|
||||
|
||||
use axum::{
|
||||
extract::{FromRef, Path, Query, State},
|
||||
extract::{ws, FromRef, Path, Query, State},
|
||||
response::IntoResponse,
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use axum_extra::extract::cookie::CookieJar;
|
||||
use leptos::prelude::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use serde::Deserialize;
|
||||
@ -94,10 +95,12 @@ pub async fn get_beacon(btype: &str) -> Result<Vec<u8>, crate::error::Error> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromRef, Clone, Debug)]
|
||||
#[derive(FromRef, Clone)]
|
||||
pub struct AppState {
|
||||
db: SqlitePool,
|
||||
leptos_options: leptos::config::LeptosOptions,
|
||||
beacon_listeners: sparse_handler::BeaconListenerMap,
|
||||
beacon_event_broadcast: tokio::sync::broadcast::Sender<sparse_handler::BeaconEvent>,
|
||||
}
|
||||
|
||||
async fn get_parameters_bytes(
|
||||
@ -237,7 +240,10 @@ pub async fn download_beacon(
|
||||
State(db): State<AppState>,
|
||||
Query(beacon_params): Query<BeaconDownloadParams>,
|
||||
) -> Result<impl IntoResponse, crate::error::Error> {
|
||||
let (parameters_bytes, operating_system) = get_parameters_bytes(template_id, db.db).await?;
|
||||
use rand::{rngs::OsRng, TryRngCore};
|
||||
use sparse_actions::payload_types::{Parameters_t, XOR_KEY};
|
||||
|
||||
let (mut parameters_bytes, operating_system) = get_parameters_bytes(template_id, db.db).await?;
|
||||
|
||||
let binary = if beacon_params.use_svc.unwrap_or_default() {
|
||||
tracing::debug!("Downloading windows service");
|
||||
@ -252,6 +258,22 @@ pub async fn download_beacon(
|
||||
|
||||
let installer_bytes = get_beacon(&binary).await?;
|
||||
|
||||
let parameters: &mut Parameters_t =
|
||||
unsafe { std::mem::transmute(parameters_bytes.as_mut_ptr()) };
|
||||
let mut identifier = [0u8; 32];
|
||||
OsRng
|
||||
.try_fill_bytes(&mut identifier)
|
||||
.expect("Could not generate beacon identifier");
|
||||
|
||||
let hex_ident = hex::encode(&identifier)
|
||||
.as_bytes()
|
||||
.iter()
|
||||
.map(|b| b ^ (XOR_KEY as u8))
|
||||
.collect::<Vec<_>>();
|
||||
parameters
|
||||
.beacon_identifier
|
||||
.copy_from_slice(&hex_ident);
|
||||
|
||||
use axum::http::header;
|
||||
|
||||
Ok((
|
||||
@ -301,6 +323,118 @@ pub async fn download_beacon_installer(
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn subscribe_to_listener_events(
|
||||
State(state): State<AppState>,
|
||||
cookie_jar: CookieJar,
|
||||
ws: ws::WebSocketUpgrade
|
||||
) -> axum::response::Response {
|
||||
let user = match crate::db::user::get_auth_session_inner(
|
||||
state.db.clone(),
|
||||
cookie_jar
|
||||
).await {
|
||||
Ok(u) => u,
|
||||
Err(e) => {
|
||||
tracing::warn!("Could not load user session: {e:?}");
|
||||
return axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if user.is_none() {
|
||||
return axum::http::StatusCode::UNAUTHORIZED.into_response();
|
||||
}
|
||||
|
||||
ws
|
||||
.on_upgrade(move |socket: ws::WebSocket| async move {
|
||||
if let Err(e) = handle_listener_events(socket, state).await {
|
||||
tracing::warn!("Encountered error when handling event subscriber: {e}");
|
||||
};
|
||||
})
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn handle_listener_events(
|
||||
mut socket: ws::WebSocket,
|
||||
state: AppState,
|
||||
) -> Result<(), crate::error::Error> {
|
||||
use sqlx::{sqlite::SqliteRow, Row};
|
||||
|
||||
use crate::beacons::sidebar::{CurrentBeaconInstance, SidebarEvents};
|
||||
|
||||
{
|
||||
let beacons = sqlx::query!(
|
||||
"SELECT beacon_id, template_id, peer_ip, nickname, cwd, operating_system, beacon_userent, hostname, config_id FROM beacon_instance"
|
||||
)
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
|
||||
struct CheckinResult {
|
||||
beacon_id: String,
|
||||
checkin_date: chrono::DateTime<chrono::Utc>
|
||||
}
|
||||
|
||||
impl sqlx::FromRow<'_, SqliteRow> for CheckinResult {
|
||||
fn from_row(row: &SqliteRow) -> sqlx::Result<Self> {
|
||||
Ok(CheckinResult {
|
||||
beacon_id: row.get("beacon_id"),
|
||||
checkin_date: row.get("checkin_date")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let last_checkin: Vec<CheckinResult> = sqlx::query_as(
|
||||
"SELECT beacon_id, MAX(checkin_date) as checkin_date FROM beacon_checkin
|
||||
GROUP BY beacon_id"
|
||||
)
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
|
||||
let category_ids = sqlx::query!(
|
||||
"SELECT beacon_id, category_id FROM beacon_category_assignment"
|
||||
)
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
|
||||
let beacons = SidebarEvents::BeaconList(
|
||||
beacons
|
||||
.into_iter()
|
||||
.map(|b| CurrentBeaconInstance {
|
||||
beacon_id: b.beacon_id.clone(),
|
||||
template_id: b.template_id,
|
||||
ip: b.peer_ip,
|
||||
nickname: b.nickname,
|
||||
cwd: b.cwd,
|
||||
operating_system: b.operating_system,
|
||||
userent: b.beacon_userent,
|
||||
hostname: b.hostname,
|
||||
config_id: b.config_id,
|
||||
last_checkin: last_checkin
|
||||
.iter()
|
||||
.find(|ch| ch.beacon_id == b.beacon_id)
|
||||
.clone()
|
||||
.map(|ch| ch.checkin_date)
|
||||
.unwrap_or_else(|| chrono::Utc::now()),
|
||||
category_ids: category_ids
|
||||
.iter()
|
||||
.filter(|cat| cat.beacon_id == b.beacon_id)
|
||||
.map(|cat| cat.category_id)
|
||||
.collect()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
let json = serde_json::to_string(&beacons)?;
|
||||
|
||||
socket.send(ws::Message::Text(json.into())).await?;
|
||||
}
|
||||
|
||||
let mut event_receiver = state.beacon_event_broadcast.subscribe();
|
||||
|
||||
loop {
|
||||
let event = event_receiver.recv().await;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn serve_web(
|
||||
management_address: SocketAddrV4,
|
||||
db: SqlitePool,
|
||||
@ -308,6 +442,7 @@ pub async fn serve_web(
|
||||
let conf = get_configuration(None).unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let routes = generate_route_list(App);
|
||||
let beacon_event_broadcast = tokio::sync::broadcast::Sender::<sparse_handler::BeaconEvent>::new(128);
|
||||
let beacon_listeners = sparse_handler::BeaconListenerMap::default();
|
||||
|
||||
let compression_layer = tower_http::compression::CompressionLayer::new()
|
||||
@ -316,11 +451,17 @@ pub async fn serve_web(
|
||||
.br(true)
|
||||
.zstd(true);
|
||||
|
||||
sparse_handler::start_all_listeners(beacon_listeners.clone(), db.clone()).await?;
|
||||
sparse_handler::start_all_listeners(
|
||||
beacon_listeners.clone(),
|
||||
db.clone(),
|
||||
beacon_event_broadcast.clone()
|
||||
).await?;
|
||||
|
||||
let state = AppState {
|
||||
leptos_options: leptos_options.clone(),
|
||||
db: db.clone(),
|
||||
beacon_listeners: beacon_listeners.clone(),
|
||||
beacon_event_broadcast: beacon_event_broadcast.clone()
|
||||
};
|
||||
|
||||
let app = Router::new()
|
||||
@ -329,6 +470,10 @@ pub async fn serve_web(
|
||||
get(download_beacon_installer),
|
||||
)
|
||||
.route("/binaries/beacon/:template_id", get(download_beacon))
|
||||
.route(
|
||||
"/api/subscribe/listener",
|
||||
axum::routing::any(subscribe_to_listener_events)
|
||||
)
|
||||
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
||||
.leptos_routes_with_context(
|
||||
&state,
|
||||
@ -336,6 +481,7 @@ pub async fn serve_web(
|
||||
move || {
|
||||
provide_context(beacon_listeners.clone());
|
||||
provide_context(db.clone());
|
||||
provide_context(beacon_event_broadcast.clone());
|
||||
},
|
||||
{
|
||||
let leptos_options = leptos_options.clone();
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
@use 'beacons/templates';
|
||||
@use 'beacons/instances';
|
||||
@use 'beacons/commands';
|
||||
@use 'beacons/sidebar';
|
||||
|
||||
main.beacons {
|
||||
display: grid;
|
||||
|
||||
6
sparse-server/style/beacons/_sidebar.scss
Normal file
6
sparse-server/style/beacons/_sidebar.scss
Normal file
@ -0,0 +1,6 @@
|
||||
aside.beacons {
|
||||
grid-area: beacons;
|
||||
background-color: #11111c;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
@ -22,6 +22,8 @@ pub struct LinuxAdapter;
|
||||
impl BeaconAdapter for LinuxAdapter {
|
||||
type Error = LinuxAdapterError;
|
||||
|
||||
const OPERATING_SYSTEM: &'static str = "Linux";
|
||||
|
||||
fn interface_name_from_interface(interface: &BeaconInterface) -> Vec<u8> {
|
||||
interface.name.clone()
|
||||
}
|
||||
@ -88,4 +90,33 @@ impl BeaconAdapter for LinuxAdapter {
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_username(&self) -> Result<String, error::BeaconError<Self::Error>> {
|
||||
let passwd = tokio::fs::read_to_string("/etc/passwd").await?;
|
||||
let uid = unsafe { libc::getuid() };
|
||||
|
||||
Ok(passwd
|
||||
.split("\n")
|
||||
.find_map(|row| -> Option<String> {
|
||||
let mut entries = row.split(":");
|
||||
|
||||
let name = entries.next()?;
|
||||
entries.next()?;
|
||||
let euid = entries.next()?.parse::<u32>().ok()?;
|
||||
|
||||
if euid == uid {
|
||||
Some(name.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or("(unknown)".to_string()))
|
||||
}
|
||||
|
||||
async fn get_hostname(&self) -> Result<String, error::BeaconError<Self::Error>> {
|
||||
Ok(tokio::fs::read_to_string("/etc/hostname")
|
||||
.await?
|
||||
.trim()
|
||||
.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user