feat: added beacon installer generation, download
This commit is contained in:
@@ -112,6 +112,7 @@ pub fn provide_beacon_resources() {
|
||||
|
||||
#[component]
|
||||
pub fn BeaconView() -> impl IntoView {
|
||||
#[cfg(feature = "hydrate")]
|
||||
Effect::new(move || {
|
||||
let user = expect_context::<ReadSignal<Option<crate::users::User>>>();
|
||||
if user.get().is_none() {
|
||||
|
||||
@@ -101,7 +101,6 @@ pub async fn rename_category(id: i64, name: String) -> Result<(), ServerFnError>
|
||||
pub fn CategoriesView() -> impl IntoView {
|
||||
let BeaconResources { add_category, categories, .. } = expect_context();
|
||||
|
||||
|
||||
view! {
|
||||
<div class="categories">
|
||||
<h2>"Categories"</h2>
|
||||
|
||||
@@ -71,6 +71,17 @@ pub async fn get_listeners() -> Result<Vec<PubListener>, ServerFnError> {
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn generate_cert_from_keypair(kp: &rcgen::KeyPair, names: Vec<String>) -> Result<rcgen::Certificate, rcgen::Error> {
|
||||
use rcgen::CertificateParams;
|
||||
|
||||
let mut params = CertificateParams::new(names)?;
|
||||
|
||||
params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
|
||||
|
||||
params.self_signed(&kp)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn add_listener(public_ip: String, port: i16, domain_name: String) -> Result<(), ServerFnError> {
|
||||
let user = user::get_auth_session().await?;
|
||||
@@ -84,15 +95,19 @@ pub async fn add_listener(public_ip: String, port: i16, domain_name: String) ->
|
||||
}
|
||||
|
||||
let subject_alt_names = vec![public_ip.to_string(), domain_name.clone()];
|
||||
let CertifiedKey { cert, key_pair } = tokio::task::spawn_blocking(|| {
|
||||
generate_simple_self_signed(subject_alt_names)
|
||||
|
||||
|
||||
let (key_pair, cert) = tokio::task::spawn_blocking(|| {
|
||||
rcgen::KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)
|
||||
.and_then(|keypair|
|
||||
generate_cert_from_keypair(&keypair, subject_alt_names).map(|cert| (keypair, cert)))
|
||||
}).await??;
|
||||
|
||||
let db = expect_context::<SqlitePool>();
|
||||
|
||||
let public_ip = public_ip.to_string();
|
||||
let cert = cert.pem().to_string();
|
||||
let key_pair = key_pair.serialize_pem().to_string();
|
||||
let cert = cert.der().to_vec();
|
||||
let key_pair = key_pair.serialize_der();
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO beacon_listener (port, public_ip, domain_name, certificate, privkey) VALUES (?, ?, ?, ?, ?)",
|
||||
@@ -116,23 +131,32 @@ pub async fn remove_listener(listener_id: i64) -> Result<(), ServerFnError> {
|
||||
return Err(ServerFnError::<NoCustomError>::ServerError("You are not signed in!".to_owned()));
|
||||
}
|
||||
|
||||
let pool = expect_context::<SqlitePool>();
|
||||
let blm = expect_context::<BeaconListenerMap>();
|
||||
{
|
||||
let blm = expect_context::<BeaconListenerMap>();
|
||||
|
||||
let Ok(mut blm_handle) = blm.write() else {
|
||||
return Err(ServerFnError::<NoCustomError>::ServerError("Failed to get write handle for beacon listener map".to_owned()));
|
||||
};
|
||||
let Ok(mut blm_handle) = blm.write() else {
|
||||
return Err(ServerFnError::<NoCustomError>::ServerError("Failed to get write handle for beacon listener map".to_owned()));
|
||||
};
|
||||
|
||||
if let Some(mut bl) = blm_handle.get_mut(&listener_id) {
|
||||
bl.abort();
|
||||
} else {
|
||||
return Err(ServerFnError::<NoCustomError>::ServerError("Failed to get write handle for beacon listener map".to_owned()));
|
||||
if let Some(bl) = blm_handle.get_mut(&listener_id) {
|
||||
bl.abort();
|
||||
} else {
|
||||
return Err(ServerFnError::<NoCustomError>::ServerError("Failed to get write handle for beacon listener map".to_owned()));
|
||||
}
|
||||
|
||||
blm_handle.remove(&listener_id);
|
||||
}
|
||||
|
||||
blm_handle.remove(&listener_id);
|
||||
drop(blm_handle);
|
||||
let pool = expect_context::<SqlitePool>();
|
||||
|
||||
unimplemented!()
|
||||
sqlx::query!(
|
||||
"DELETE FROM beacon_listener WHERE listener_id = ?",
|
||||
listener_id
|
||||
)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server]
|
||||
|
||||
@@ -103,6 +103,34 @@ pub async fn add_template(
|
||||
|
||||
let db = expect_context::<SqlitePool>();
|
||||
|
||||
let listener = sqlx::query!(
|
||||
"SELECT certificate, privkey FROM beacon_listener WHERE listener_id = ?",
|
||||
listener_id
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
|
||||
use rcgen::{Certificate, CertificateParams, KeyPair};
|
||||
|
||||
let keypair = KeyPair::from_der_and_sign_algo(
|
||||
match &rustls_pki_types::PrivateKeyDer::try_from(&*listener.privkey) {
|
||||
Ok(pk) => pk,
|
||||
Err(e) => {
|
||||
srverr!("Could not parse private key: {e}");
|
||||
}
|
||||
},
|
||||
&rcgen::PKCS_ECDSA_P256_SHA256
|
||||
)?;
|
||||
let ca_params = CertificateParams::from_ca_cert_der(&(*listener.certificate).into())?;
|
||||
let ca_cert = ca_params.self_signed(&keypair)?;
|
||||
|
||||
let client_key = KeyPair::generate()?;
|
||||
let client_params = CertificateParams::default();
|
||||
let client_cert = client_params.signed_by(&client_key, &ca_cert, &keypair)?;
|
||||
|
||||
let client_key_der = client_key.serialize_der();
|
||||
let client_cert_der = client_cert.der().to_vec();
|
||||
|
||||
match &*source_mode {
|
||||
"host" => {
|
||||
let source_mac = Some(source_mac).filter(|mac| mac != "00:00:00:00:00:00");
|
||||
@@ -110,16 +138,18 @@ pub async fn add_template(
|
||||
|
||||
sqlx::query!(
|
||||
r"INSERT INTO beacon_template
|
||||
(template_name, operating_system, config_id, listener_id, source_ip, source_mac, source_mode, default_category)
|
||||
(template_name, operating_system, config_id, listener_id, source_ip, source_mac, source_mode, default_category, client_key, client_cert)
|
||||
VALUES
|
||||
(?, ?, ?, ?, ?, ?, 'host', ?)",
|
||||
(?, ?, ?, ?, ?, ?, 'host', ?, ?, ?)",
|
||||
template_name,
|
||||
operating_system,
|
||||
config_id,
|
||||
listener_id,
|
||||
source_ip,
|
||||
source_mac,
|
||||
default_category
|
||||
default_category,
|
||||
client_key_der,
|
||||
client_cert_der
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
@@ -132,9 +162,9 @@ pub async fn add_template(
|
||||
|
||||
sqlx::query!(
|
||||
r"INSERT INTO beacon_template
|
||||
(template_name, operating_system, config_id, listener_id, source_ip, source_mac, source_mode, source_netmask, source_gateway, default_category)
|
||||
(template_name, operating_system, config_id, listener_id, source_ip, source_mac, source_mode, source_netmask, source_gateway, default_category, client_key, client_cert)
|
||||
VALUES
|
||||
(?, ?, ?, ?, ?, ?, 'host', ?, ?, ?)",
|
||||
(?, ?, ?, ?, ?, ?, 'host', ?, ?, ?, ?, ?)",
|
||||
template_name,
|
||||
operating_system,
|
||||
config_id,
|
||||
@@ -143,7 +173,9 @@ pub async fn add_template(
|
||||
source_mac,
|
||||
source_netmask,
|
||||
source_gateway,
|
||||
default_category
|
||||
default_category,
|
||||
client_key_der,
|
||||
client_cert_der
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
@@ -376,9 +408,13 @@ pub fn DisplayTemplates(
|
||||
")"
|
||||
</h4>
|
||||
<div>
|
||||
<button>
|
||||
<a
|
||||
class="button"
|
||||
download=""
|
||||
href=format!("/installer/{}", template.template_id)
|
||||
>
|
||||
"Download installer"
|
||||
</button>
|
||||
</a>
|
||||
<button
|
||||
on:click={
|
||||
let template_id = template.template_id;
|
||||
@@ -386,6 +422,7 @@ pub fn DisplayTemplates(
|
||||
remove_template.dispatch(RemoveTemplate { template_id });
|
||||
}
|
||||
}
|
||||
class="warning"
|
||||
>
|
||||
"Delete template"
|
||||
</button>
|
||||
|
||||
@@ -10,6 +10,7 @@ pub enum Error {
|
||||
Pbkdf2(pbkdf2::password_hash::errors::Error),
|
||||
#[cfg(feature = "ssr")]
|
||||
Io(std::io::Error),
|
||||
AddrParse(std::net::AddrParseError),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
@@ -37,6 +38,9 @@ impl std::fmt::Display for Error {
|
||||
Error::Io(err) => {
|
||||
write!(f, "io error: {err:?}")
|
||||
}
|
||||
Error::AddrParse(err) => {
|
||||
write!(f, "ip address parse error: {err:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,11 +54,23 @@ impl std::error::Error for Error {
|
||||
Error::TokioJoin(err) => Some(err),
|
||||
#[cfg(feature = "ssr")]
|
||||
Error::Io(err) => Some(err),
|
||||
Error::AddrParse(err) => Some(err),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
impl axum::response::IntoResponse for Error {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
(
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("{:?}", self),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for Error {
|
||||
type Err = Self;
|
||||
|
||||
@@ -90,3 +106,9 @@ impl From<std::io::Error> for Error {
|
||||
Self::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::net::AddrParseError> for Error {
|
||||
fn from(err: std::net::AddrParseError) -> Self {
|
||||
Self::AddrParse(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,3 @@
|
||||
#[cfg(feature = "hydrate")]
|
||||
pub(crate) mod beacon_binaries {
|
||||
#[allow(dead_code)]
|
||||
pub const LINUX_BEACON: &'static [u8] = include_bytes!(std::env!("SPARSE_BEACON_LINUX"));
|
||||
#[allow(dead_code)]
|
||||
pub const FREEBSD_BEACON: &'static [u8] = include_bytes!(std::env!("SPARSE_BEACON_FREEBSD"));
|
||||
#[allow(dead_code)]
|
||||
pub const WINDOWS_BEACON: &'static [u8] = include_bytes!(std::env!("SPARSE_BEACON_WINDOWS"));
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub const LINUX_INSTALLER: &'static [u8] = include_bytes!(std::env!("SPARSE_INSTALLER_LINUX"));
|
||||
#[allow(dead_code)]
|
||||
pub const FREEBSD_INSTALLER: &'static [u8] = include_bytes!(std::env!("SPARSE_INSTALLER_FREEBSD"));
|
||||
#[allow(dead_code)]
|
||||
pub const WINDOWS_INSTALLER: &'static [u8] = include_bytes!(std::env!("SPARSE_INSTALLER_WINDOWS"));
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
mod cli;
|
||||
@@ -29,6 +13,7 @@ pub mod db;
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<std::process::ExitCode> {
|
||||
|
||||
use std::{path::PathBuf, process::ExitCode, str::FromStr};
|
||||
|
||||
use structopt::StructOpt;
|
||||
@@ -38,7 +23,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()),
|
||||
.unwrap_or_else(|_| format!("{}=debug,sparse_handler=debug", env!("CARGO_CRATE_NAME")).into()),
|
||||
)
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
|
||||
@@ -1,13 +1,167 @@
|
||||
use std::{net::SocketAddrV4, process::ExitCode};
|
||||
|
||||
use sqlx::sqlite::SqlitePool;
|
||||
use axum::Router;
|
||||
use axum::{extract::{FromRef, Path, State}, response::IntoResponse, Router, routing::get};
|
||||
use leptos::prelude::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use tokio::signal;
|
||||
|
||||
use sparse_server::app::*;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
pub(crate) mod beacon_binaries {
|
||||
pub const LINUX_INSTALLER: &'static [u8] = include_bytes!(std::env!("SPARSE_INSTALLER_LINUX"));
|
||||
pub const FREEBSD_INSTALLER: &'static [u8] = include_bytes!(std::env!("SPARSE_INSTALLER_FREEBSD"));
|
||||
pub const WINDOWS_INSTALLER: &'static [u8] = include_bytes!(std::env!("SPARSE_INSTALLER_WINDOWS"));
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub async fn get_installer(btype: &str) -> Result<Vec<u8>, crate::error::Error> {
|
||||
let path = match btype {
|
||||
"linux" => "target/x86_64-unknown-linux-musl/debug/sparse-unix-installer",
|
||||
"freebsd" => "target/x86_64-unknown-freebsd/debug/sparse-unix-installer",
|
||||
"windows" => "target/x86_64-pc-windows-gnu/debug/sparse-unix-installer",
|
||||
other => return Err(crate::error::Error::Generic(format!("unknown beacon type: {other}"))),
|
||||
};
|
||||
|
||||
Ok(tokio::fs::read(path).await?)
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
pub async fn get_installer(btype: &str) -> Result<Vec<u8>, crate::error::Error> {
|
||||
match btype {
|
||||
"linux" => Ok(beacon_binaries::LINUX_INSTALLER.to_owned()),
|
||||
"windows" => Ok(beacon_binaries::WINDOWS_INSTALLER.to_owned()),
|
||||
"freebsd" => Ok(beacon_binaries::FREEBSD_INSTALLER.to_owned()),
|
||||
other => Err(crate::error::Error::Generic(format!("unknown beacon type: {other}")))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromRef, Clone, Debug)]
|
||||
pub struct AppState {
|
||||
db: SqlitePool,
|
||||
leptos_options: leptos::config::LeptosOptions
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
pub async fn download_beacon_installer(
|
||||
Path(template_id): Path<i64>,
|
||||
State(db): State<AppState>
|
||||
) -> Result<impl IntoResponse, crate::error::Error> {
|
||||
use rand::{rngs::OsRng, TryRngCore};
|
||||
use sparse_actions::payload_types::Parameters_t;
|
||||
|
||||
let mut parameters_buffer = vec![0u8; std::mem::size_of::<Parameters_t>()];
|
||||
//let _ = OsRng.try_fill_bytes(&mut parameters_buffer);
|
||||
|
||||
let parameters: &mut Parameters_t = unsafe { std::mem::transmute(parameters_buffer.as_mut_ptr()) };
|
||||
|
||||
let template = sqlx::query!(
|
||||
r"SELECT operating_system, source_ip, source_mac, source_mode, source_netmask,
|
||||
source_gateway, port, public_ip, domain_name, certificate, client_cert, client_key
|
||||
FROM beacon_template JOIN beacon_listener"
|
||||
)
|
||||
.fetch_one(&db.db)
|
||||
.await?;
|
||||
|
||||
let dest_ip = template.public_ip.parse::<std::net::Ipv4Addr>()?;
|
||||
let src_ip = template.source_ip.parse::<std::net::Ipv4Addr>()?;
|
||||
|
||||
let dest_octets = dest_ip.octets();
|
||||
parameters.destination_ip.a = dest_octets[0];
|
||||
parameters.destination_ip.b = dest_octets[1];
|
||||
parameters.destination_ip.c = dest_octets[2];
|
||||
parameters.destination_ip.d = dest_octets[3];
|
||||
|
||||
let src_mac: [u8; 6] = template
|
||||
.source_mac
|
||||
.unwrap_or("00:00:00:00:00:00".to_string())
|
||||
.split(":")
|
||||
.map(|by| u8::from_str_radix(by, 16))
|
||||
.collect::<Result<Vec<u8>, _>>()
|
||||
.map_err(|_| crate::error::Error::Generic("Could not parse source MAC address".to_string()))
|
||||
.and_then(
|
||||
|bytes| bytes.try_into().map_err(|_| crate::error::Error::Generic("Could not parse source MAC address".to_string()))
|
||||
)?;
|
||||
|
||||
let src_octets = src_ip.octets();
|
||||
match (template.source_mode.as_deref(), template.source_netmask, template.source_gateway) {
|
||||
(Some("custom"), Some(nm), Some(ip)) => unsafe {
|
||||
let gateway = ip.parse::<std::net::Ipv4Addr>()?;
|
||||
let gw_octets = gateway.octets();
|
||||
|
||||
parameters.source_ip.custom_networking.mode = 0;
|
||||
parameters.source_ip.custom_networking.source_mac.copy_from_slice(&src_mac[..]);
|
||||
parameters.source_ip.custom_networking.netmask = nm as u16;
|
||||
parameters.source_ip.custom_networking.source_ip.a = src_octets[0];
|
||||
parameters.source_ip.custom_networking.source_ip.b = src_octets[1];
|
||||
parameters.source_ip.custom_networking.source_ip.c = src_octets[2];
|
||||
parameters.source_ip.custom_networking.source_ip.d = src_octets[3];
|
||||
parameters.source_ip.custom_networking.gateway.a = gw_octets[0];
|
||||
parameters.source_ip.custom_networking.gateway.b = gw_octets[1];
|
||||
parameters.source_ip.custom_networking.gateway.c = gw_octets[2];
|
||||
parameters.source_ip.custom_networking.gateway.d = gw_octets[3];
|
||||
}
|
||||
(Some("host"), _, _) => unsafe {
|
||||
parameters.source_ip.use_host_networking.mode = 1;
|
||||
parameters.source_ip.use_host_networking.source_mac.copy_from_slice(&src_mac[..]);
|
||||
parameters.source_ip.use_host_networking.source_ip.a = src_octets[0];
|
||||
parameters.source_ip.use_host_networking.source_ip.b = src_octets[1];
|
||||
parameters.source_ip.use_host_networking.source_ip.c = src_octets[2];
|
||||
parameters.source_ip.use_host_networking.source_ip.d = src_octets[3];
|
||||
}
|
||||
_ => {
|
||||
return Err(crate::error::Error::Generic("Could not parse host networking configuration".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
parameters.destination_port = template.port as u16;
|
||||
parameters.template_id = template_id as u16;
|
||||
|
||||
let pubkey_cert = template.certificate;
|
||||
parameters.pubkey_cert[..pubkey_cert.len()].copy_from_slice(&pubkey_cert[..]);
|
||||
parameters.pubkey_cert_size = pubkey_cert.len() as u16;
|
||||
|
||||
let client_key = template.client_key;
|
||||
parameters.client_key[..client_key.len()].copy_from_slice(&client_key[..]);
|
||||
parameters.client_key_length = client_key.len() as u16;
|
||||
|
||||
let client_cert = template.client_cert;
|
||||
parameters.client_cert[..client_cert.len()].copy_from_slice(&client_cert[..]);
|
||||
parameters.client_cert_length = client_cert.len() as u16;
|
||||
|
||||
let domain_name = template.domain_name.as_bytes();
|
||||
parameters.domain_name[..domain_name.len()].copy_from_slice(&domain_name[..]);
|
||||
parameters.domain_name_length = domain_name.len() as u16;
|
||||
|
||||
let installer_bytes = get_installer(&template.operating_system).await?;
|
||||
|
||||
use axum::http::header;
|
||||
|
||||
Ok((
|
||||
[
|
||||
(
|
||||
header::CONTENT_TYPE,
|
||||
"application/octet-stream".to_string()
|
||||
),
|
||||
(
|
||||
header::CONTENT_DISPOSITION,
|
||||
format!(
|
||||
r#"attachement; filename="sparse-installer{}""#,
|
||||
if template.operating_system == "windows" {
|
||||
".exe"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
)
|
||||
)
|
||||
],
|
||||
[
|
||||
&installer_bytes[..],
|
||||
¶meters_buffer[..]
|
||||
].concat()
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn serve_web(management_address: SocketAddrV4, db: SqlitePool) -> anyhow::Result<ExitCode> {
|
||||
let conf = get_configuration(None).unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
@@ -22,9 +176,15 @@ pub async fn serve_web(management_address: SocketAddrV4, db: SqlitePool) -> anyh
|
||||
|
||||
sparse_handler::start_all_listeners(beacon_listeners.clone(), db.clone()).await?;
|
||||
|
||||
let state = AppState {
|
||||
leptos_options: leptos_options.clone(),
|
||||
db: db.clone()
|
||||
};
|
||||
|
||||
let app = Router::new()
|
||||
.route("/installer/:template_id", get(download_beacon_installer))
|
||||
.leptos_routes_with_context(
|
||||
&leptos_options,
|
||||
&state,
|
||||
routes,
|
||||
move || {
|
||||
provide_context(beacon_listeners.clone());
|
||||
@@ -36,7 +196,7 @@ pub async fn serve_web(management_address: SocketAddrV4, db: SqlitePool) -> anyh
|
||||
}
|
||||
)
|
||||
.fallback(leptos_axum::file_and_error_handler::<leptos::config::LeptosOptions, _>(shell))
|
||||
.with_state(leptos_options)
|
||||
.with_state(state)
|
||||
.layer(
|
||||
tower::ServiceBuilder::new()
|
||||
.layer(tower_http::trace::TraceLayer::new_for_http())
|
||||
|
||||
Reference in New Issue
Block a user