feat: added beacon installer generation, download

This commit is contained in:
Andrew Rioux 2025-02-02 02:37:53 -05:00
parent b416f35b63
commit 0576c4fd3b
Signed by: andrew.rioux
GPG Key ID: 9B8BAC47C17ABB94
22 changed files with 554 additions and 87 deletions

182
Cargo.lock generated
View File

@ -95,6 +95,45 @@ version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
[[package]]
name = "asn1-rs"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048"
dependencies = [
"asn1-rs-derive",
"asn1-rs-impl",
"displaydoc",
"nom",
"num-traits",
"rusticata-macros",
"thiserror 1.0.69",
"time",
]
[[package]]
name = "asn1-rs-derive"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
"synstructure",
]
[[package]]
name = "asn1-rs-impl"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
]
[[package]]
name = "async-compression"
version = "0.4.18"
@ -797,6 +836,20 @@ dependencies = [
"zeroize",
]
[[package]]
name = "der-parser"
version = "9.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553"
dependencies = [
"asn1-rs",
"displaydoc",
"nom",
"num-bigint",
"num-traits",
"rusticata-macros",
]
[[package]]
name = "deranged"
version = "0.3.11"
@ -1675,7 +1728,7 @@ dependencies = [
"oco_ref",
"or_poisoned",
"paste",
"rand",
"rand 0.8.5",
"reactive_graph",
"rustc-hash 2.1.0",
"send_wrapper",
@ -2107,6 +2160,16 @@ dependencies = [
"winapi",
]
[[package]]
name = "num-bigint"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-bigint-dig"
version = "0.8.4"
@ -2119,7 +2182,7 @@ dependencies = [
"num-integer",
"num-iter",
"num-traits",
"rand",
"rand 0.8.5",
"smallvec",
"zeroize",
]
@ -2189,6 +2252,15 @@ dependencies = [
"thiserror 1.0.69",
]
[[package]]
name = "oid-registry"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9"
dependencies = [
"asn1-rs",
]
[[package]]
name = "once_cell"
version = "1.20.2"
@ -2247,7 +2319,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
dependencies = [
"base64ct",
"rand_core",
"rand_core 0.6.4",
"subtle",
]
@ -2383,7 +2455,7 @@ version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
dependencies = [
"zerocopy",
"zerocopy 0.7.35",
]
[[package]]
@ -2513,8 +2585,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.0",
"zerocopy 0.8.14",
]
[[package]]
@ -2524,7 +2607,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.0",
]
[[package]]
@ -2536,6 +2629,16 @@ dependencies = [
"getrandom 0.2.15",
]
[[package]]
name = "rand_core"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff"
dependencies = [
"getrandom 0.3.1",
"zerocopy 0.8.14",
]
[[package]]
name = "rcgen"
version = "0.13.2"
@ -2546,6 +2649,7 @@ dependencies = [
"ring",
"rustls-pki-types",
"time",
"x509-parser",
"yasna",
]
@ -2690,7 +2794,7 @@ dependencies = [
"num-traits",
"pkcs1",
"pkcs8",
"rand_core",
"rand_core 0.6.4",
"signature",
"spki",
"subtle",
@ -2740,6 +2844,15 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497"
[[package]]
name = "rusticata-macros"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
dependencies = [
"nom",
]
[[package]]
name = "rustix"
version = "0.38.44"
@ -2760,6 +2873,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb9263ab4eb695e42321db096e3b8fbd715a59b154d5c88d82db2175b681ba7"
dependencies = [
"aws-lc-rs",
"log",
"once_cell",
"rustls-pki-types",
"rustls-webpki",
@ -3017,7 +3131,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"digest",
"rand_core",
"rand_core 0.6.4",
]
[[package]]
@ -3079,6 +3193,7 @@ dependencies = [
"axum",
"axum-server",
"futures",
"rustls",
"serde",
"serde_json",
"sqlx",
@ -3114,10 +3229,13 @@ dependencies = [
"leptos_meta",
"leptos_router",
"pbkdf2",
"rand 0.9.0",
"rcgen",
"rpassword",
"rustls-pki-types",
"serde",
"sha2",
"sparse-actions",
"sparse-handler",
"sqlx",
"structopt",
@ -3287,7 +3405,7 @@ dependencies = [
"memchr",
"once_cell",
"percent-encoding",
"rand",
"rand 0.8.5",
"rsa",
"serde",
"sha1",
@ -3326,7 +3444,7 @@ dependencies = [
"md-5",
"memchr",
"once_cell",
"rand",
"rand 0.8.5",
"serde",
"serde_json",
"sha2",
@ -3934,7 +4052,7 @@ dependencies = [
"http",
"httparse",
"log",
"rand",
"rand 0.8.5",
"sha1",
"thiserror 1.0.69",
"utf-8",
@ -4465,6 +4583,24 @@ version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "x509-parser"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69"
dependencies = [
"asn1-rs",
"data-encoding",
"der-parser",
"lazy_static",
"nom",
"oid-registry",
"ring",
"rusticata-macros",
"thiserror 1.0.69",
"time",
]
[[package]]
name = "xxhash-rust"
version = "0.8.15"
@ -4517,7 +4653,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"byteorder",
"zerocopy-derive",
"zerocopy-derive 0.7.35",
]
[[package]]
name = "zerocopy"
version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468"
dependencies = [
"zerocopy-derive 0.8.14",
]
[[package]]
@ -4531,6 +4676,17 @@ dependencies = [
"syn 2.0.96",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
]
[[package]]
name = "zerofrom"
version = "0.1.5"

View File

@ -11,6 +11,9 @@ lto = true
codegen-units = 1
panic = "abort"
[profile.dev]
panic = "abort"
[profile.wasm-release]
inherits = "release"
strip = true

View File

@ -99,6 +99,7 @@
craneLib = (crane.mkLib pkgs).overrideToolchain (p:
p.rust-bin.nightly.latest.default.override {
extensions = [ "rust-src" ];
targets = [
"x86_64-unknown-linux-gnu"
"x86_64-unknown-linux-musl"
@ -138,9 +139,10 @@
set -eux
${setup-zig-freebsd}/bin/setup-zig-freebsd
cargo build -p sparse-windows-beacon --target=x86_64-pc-windows-gnu
cargo build -p sparse-unix-beacon --target=x86_64-unknown-freebsd
cargo build -p sparse-unix-beacon --target=x86_64-unknown-linux-musl
mkdir -p target/x86_64-{unknown-{linux-musl,freebsd},pc-windows-gnu}/debug
touch target/x86_64-unknown-{linux-musl,freebsd}/debug/sparse-unix-{beacon,installer}
touch target/x86_64-pc-windows-gnu/debug/sparse-windows-{beacon.dll,installer.exe}
'';
in {
packages = outputs.packages;
@ -155,15 +157,18 @@
# Added to make development easier
SPARSE_INSTALLER_LINUX =
"../../target/x86_64-unknown-linux-musl/debug/sparse-unix-beacon";
"../../target/x86_64-unknown-linux-musl/debug/sparse-unix-installer";
SPARSE_INSTALLER_FREEBSD =
"../../target/x86_64-unknown-freebsd/debug/sparse-unix-beacon";
"../../target/x86_64-unknown-freebsd/debug/sparse-unix-installer";
SPARSE_INSTALLER_WINDOWS =
"../../target/x86_64-pc-windows-gnu/debug/sparse-windows-installer.exe";
SPARSE_BEACON_LINUX =
"../../target/x86_64-unknown-linux-musl/debug/sparse-unix-beacon";
SPARSE_BEACON_FREEBSD =
"../../target/x86_64-unknown-freebsd/debug/sparse-unix-beacon";
SPARSE_BEACON_WINDOWS =
"../../target/x86_64-pc-windows-gnu/debug/sparse-windows-beacon.exe";
"../../../target/x86_64-pc-windows-gnu/debug/sparse-windows-beacon.dll";
shellHook = ''
export DATABASE_URL="sqlite://$(${pkgs.git}/bin/git rev-parse --show-toplevel)/sparse-server/db.sqlite"

View File

@ -288,8 +288,9 @@ let
outputs = rec {
packages = {
inherit sparse-server sparse-installer-linux sparse-installer-freebsd
sparse-installer-windows;
inherit sparse-server sparse-beacon-linux sparse-beacon-freebsd
sparse-beacon-windows linux-loader freebsd-loader sparse-installer-linux
sparse-installer-freebsd sparse-installer-windows;
inherit freebsd-zig-libc;

View File

@ -14,3 +14,4 @@ sqlx = { version = "0.8", default-features = false, features = ["chrono", "macro
serde = "1.0"
serde_json = "1.0"
axum-server = { version = "^0.7", features = ["tokio-rustls", "tls-rustls"] }
rustls = "0.23"

View File

@ -4,6 +4,7 @@ pub enum Error {
Sqlx(sqlx::Error),
TokioJoin(tokio::task::JoinError),
Io(std::io::Error),
Rustls(rustls::Error),
}
impl std::fmt::Display for Error {
@ -21,6 +22,9 @@ impl std::fmt::Display for Error {
Error::Io(err) => {
write!(f, "io error: {err:?}")
}
Error::Rustls(err) => {
write!(f, "rustls error: {err:?}")
}
}
}
}
@ -31,6 +35,7 @@ impl std::error::Error for Error {
Error::Sqlx(err) => Some(err),
Error::TokioJoin(err) => Some(err),
Error::Io(err) => Some(err),
Error::Rustls(err) => Some(err),
_ => None,
}
}
@ -61,3 +66,9 @@ impl From<std::io::Error> for Error {
Self::Io(err)
}
}
impl From<rustls::Error> for Error {
fn from(err: rustls::Error) -> Self {
Self::Rustls(err)
}
}

View File

@ -4,7 +4,6 @@ use std::{
};
use axum::routing::{get, post, Router};
use axum_server::tls_rustls::RustlsConfig;
use sqlx::SqlitePool;
use tokio::task::JoinHandle;
@ -36,8 +35,6 @@ impl std::ops::Deref for BeaconListenerMap {
}
pub async fn start_all_listeners(beacon_listener_map: BeaconListenerMap, db: SqlitePool) -> Result<(), crate::error::Error> {
tracing::debug!("Typeid: {:?}", std::any::TypeId::of::<BeaconListenerMap>());
let listener_ids = sqlx::query!("SELECT listener_id FROM beacon_listener")
.fetch_all(&db)
.await?;
@ -56,6 +53,15 @@ struct ListenerState {
db: SqlitePool
}
struct Listener {
listener_id: i64,
port: i64,
public_ip: String,
domain_name: String,
certificate: Vec<u8>,
privkey: Vec<u8>
}
pub async fn start_listener(beacon_listener_map: BeaconListenerMap, listener_id: i64, db: SqlitePool) -> Result<(), crate::error::Error> {
{
let Ok(blm_handle) = beacon_listener_map.read() else {
@ -66,7 +72,7 @@ pub async fn start_listener(beacon_listener_map: BeaconListenerMap, listener_id:
return Err(crate::error::Error::Generic("Beacon listener already started".to_string()));
}
}
let listener = sqlx::query!("SELECT * FROM beacon_listener WHERE listener_id = ?", listener_id)
let listener = sqlx::query_as!(Listener, "SELECT * FROM beacon_listener WHERE listener_id = ?", listener_id)
.fetch_one(&db)
.await?;
@ -84,17 +90,30 @@ pub async fn start_listener(beacon_listener_map: BeaconListenerMap, listener_id:
let hidden_app = Router::new().nest("/hidden_sparse", app);
let tls_config = RustlsConfig::from_pem(
listener.certificate.as_bytes().to_vec(),
listener.privkey.as_bytes().to_vec()
).await?;
let keypair = match rustls::pki_types::PrivateKeyDer::try_from(listener.privkey.clone()) {
Ok(pk) => pk,
Err(e) => {
return Err(crate::error::Error::Generic(format!("Could not parse private key: {e}")));
}
};
let cert = rustls::pki_types::CertificateDer::from(listener.certificate.clone());
let mut tls_config = rustls::ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(vec![cert], keypair)?;
tls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
let addr = std::net::SocketAddr::from(([0, 0, 0, 0], listener.port as u16));
tracing::debug!("Starting listener {}, {}, on port {}", listener_id, listener.domain_name, listener.port);
let join_handle = tokio::task::spawn(async move {
let res = axum_server::tls_rustls::bind_rustls(addr, tls_config)
let res = axum_server::tls_rustls::bind_rustls(
addr,
axum_server::tls_rustls::RustlsConfig::from_config(
Arc::new(tls_config)
)
)
.serve(hidden_app.into_make_service())
.await;

View File

@ -9,12 +9,12 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
leptos = { version = "^0.7", features = ["nightly"] }
leptos_router = { version = "^0.7", features = ["nightly"] }
axum = { version = "^0.7", features = ["ws", "macros"], optional = true }
axum = { version = "^0.7", features = ["ws", "macros", "http2"], optional = true }
axum-extra = { version = "^0.9", features = ["cookie"], optional = true }
console_error_panic_hook = "0.1"
leptos_axum = { version = "^0.7", optional = true }
leptos_meta = { version = "^0.7" }
tokio = { version = "1", features = ["rt-multi-thread", "signal"], optional = true }
tokio = { version = "1", features = ["rt-multi-thread", "signal", "fs", "io-std", "io-util"], optional = true }
tower = { version = "0.4", optional = true }
tower-http = { version = "0.5", features = ["fs", "compression-br", "compression-deflate", "compression-gzip", "compression-zstd", "trace"], optional = true }
wasm-bindgen = "0.2"
@ -39,9 +39,12 @@ sha2 = { version = "0.10", optional = true }
hex = { version = "0.4", optional = true }
serde = "1.0"
cfg-if = "1.0.0"
rcgen = { version = "0.13.2", optional = true }
rcgen = { version = "0.13.2", features = ["pem", "x509-parser", "crypto"], optional = true }
cron = { version = "0.15.0", optional = true }
rustls-pki-types = { version = "1.7", optional = true }
rand = { version = "0.9", optional = true }
sparse-actions = { path = "../sparse-actions", optional = true }
sparse-handler = { path = "../sparse-handler", optional = true }
[features]
@ -67,6 +70,9 @@ ssr = [
"dep:rcgen",
"dep:cron",
"dep:sparse-handler",
"dep:rustls-pki-types",
"dep:sparse-actions",
"dep:rand",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",

View File

@ -0,0 +1,14 @@
-- Add migration script here
ALTER TABLE beacon_template ADD COLUMN client_key blob NOT NULL DEFAULT '';
ALTER TABLE beacon_template ADD COLUMN client_cert blob NOT NULL DEFAULT '';
DROP TABLE beacon_listener;
CREATE TABLE beacon_listener (
listener_id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
port int NOT NULL,
public_ip varchar NOT NULL,
domain_name varchar NOT NULL,
certificate blob NOT NULL,
privkey blob NOT NULL
);

View File

@ -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() {

View File

@ -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>

View File

@ -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]

View File

@ -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>

View File

@ -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)
}
}

View File

@ -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();

View File

@ -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[..],
&parameters_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())

View File

@ -1,5 +1,6 @@
main.beacons div.categories {
padding: 10px;
overflow-y: scroll;
fieldset {
display: grid;

View File

@ -1,5 +1,6 @@
main.beacons div.config {
padding: 10px;
overflow-y: scroll;
fieldset {
display: grid;

View File

@ -1,4 +1,6 @@
main.beacons div.listeners {
overflow-y: scroll;
form, p, h2 {
margin: 10px;
}

View File

@ -1,5 +1,6 @@
main.beacons div.templates {
padding: 10px;
overflow-y: scroll;
fieldset {
display: grid;

View File

@ -70,7 +70,11 @@ main {
input[type="submit"],
input[type="button"],
button {
button,
a.button {
font-family: sans-serif;
font-size: 0.9rem;
color: black;
background-color: #fff;
border: 1px solid black;
cursor: pointer;
@ -82,4 +86,13 @@ button {
&:hover {
background-color: #ccc;
}
&.warning {
background-color: #f00;
color: white;
&:hover {
background-color: #c00;
}
}
}

View File

@ -10,12 +10,12 @@ typedef struct {
typedef union SourceIp {
struct {
char mode; // set to 0
char source_mac[6];
unsigned char source_mac[6];
ipaddr_t source_ip;
} use_host_networking;
struct {
char mode; // set to 1
char source_mac[6];
unsigned char source_mac[6];
unsigned short netmask;
ipaddr_t source_ip;
ipaddr_t gateway;
@ -27,12 +27,17 @@ typedef struct Parameters {
SourceIp_t source_ip;
unsigned short destination_port;
unsigned short pubkey_cert_size;
unsigned short template_name_length;
unsigned short domain_name_length;
unsigned short beacon_name_length;
char pubkey_cert[1024];
char beacon_identifier[64];
char template_name[128];
char domain_name[128];
char beacon_name[128];
unsigned short client_key_length;
unsigned short client_cert_length;
unsigned short template_id;
unsigned char delay_seconds_min;
unsigned char delay_seconds_max;
unsigned char pubkey_cert[512];
unsigned char client_key[256];
unsigned char client_cert[384];
unsigned char beacon_identifier[64];
unsigned char domain_name[64];
unsigned char beacon_name[128];
} Parameters_t;