feat: added tcp

sorry Judah
This commit is contained in:
Andrew Rioux
2025-02-12 17:49:31 -05:00
parent e388b2eefa
commit f9ff9f266a
37 changed files with 1939 additions and 902 deletions

View File

@@ -0,0 +1,2 @@
-- Add migration script here
ALTER TABLE beacon_template ADD COLUMN source_interface blob DEFAULT '';

View File

@@ -1,9 +1,9 @@
use leptos::{either::Either, prelude::*};
use leptos_meta::{provide_meta_context, MetaTags, Stylesheet, Title};
use leptos_router::{
components::{A, ParentRoute, Route, Router, Routes},
components::{ParentRoute, Route, Router, Routes, A},
hooks::use_query_map,
path
path,
};
use crate::users::User;
@@ -29,7 +29,7 @@ pub async fn me() -> Result<Option<User>, ServerFnError> {
Ok(user.map(|user| User {
user_id: user.user_id,
user_name: user.user_name
user_name: user.user_name,
}))
}
@@ -73,10 +73,7 @@ pub fn App() -> impl IntoView {
let (user_res, set_user_res) = signal(None::<User>);
let user = Resource::new(
move || login.version().get(),
|_| async { me().await }
);
let user = Resource::new(move || login.version().get(), |_| async { me().await });
#[cfg(feature = "hydrate")]
Effect::new(move || {
@@ -143,7 +140,12 @@ pub fn App() -> impl IntoView {
#[component]
fn LoginPage(login: ServerAction<Login>) -> impl IntoView {
let next = move || use_query_map().read().get("next").unwrap_or("/".to_string());
let next = move || {
use_query_map()
.read()
.get("next")
.unwrap_or("/".to_string())
};
view! {
<main class="login">

View File

@@ -36,7 +36,7 @@ pub struct BeaconResources {
listeners: Resource<Result<Vec<listeners::PubListener>, ServerFnError>>,
categories: Resource<Result<Vec<categories::Category>, ServerFnError>>,
configs: Resource<Result<Vec<configs::BeaconConfig>, ServerFnError>>,
templates: Resource<Result<Vec<templates::BeaconTemplate>, ServerFnError>>
templates: Resource<Result<Vec<templates::BeaconTemplate>, ServerFnError>>,
}
pub fn provide_beacon_resources() {
@@ -56,40 +56,48 @@ pub fn provide_beacon_resources() {
let remove_template = ServerAction::<templates::RemoveTemplate>::new();
let listeners = Resource::new(
move || (
user.get(),
add_listener.version().get(),
remove_listener.version().get(),
),
|_| async { listeners::get_listeners().await }
move || {
(
user.get(),
add_listener.version().get(),
remove_listener.version().get(),
)
},
|_| async { listeners::get_listeners().await },
);
let categories = Resource::new(
move || (
user.get(),
add_category.version().get(),
remove_category.version().get(),
rename_category.version().get(),
),
|_| async { categories::get_categories().await }
move || {
(
user.get(),
add_category.version().get(),
remove_category.version().get(),
rename_category.version().get(),
)
},
|_| async { categories::get_categories().await },
);
let configs = Resource::new(
move || (
user.get(),
add_beacon_config.version().get(),
remove_beacon_config.version().get(),
),
|_| async { configs::get_beacon_configs().await }
move || {
(
user.get(),
add_beacon_config.version().get(),
remove_beacon_config.version().get(),
)
},
|_| async { configs::get_beacon_configs().await },
);
let templates = Resource::new(
move || (
user.get(),
add_template.version().get(),
remove_template.version().get()
),
|_| async { templates::get_templates().await }
move || {
(
user.get(),
add_template.version().get(),
remove_template.version().get(),
)
},
|_| async { templates::get_templates().await },
);
provide_context(BeaconResources {
@@ -106,7 +114,7 @@ pub fn provide_beacon_resources() {
listeners,
categories,
configs,
templates
templates,
});
}
@@ -142,7 +150,7 @@ enum SortMethod {
Listener,
Config,
Category,
Template
Template,
}
impl std::str::FromStr for SortMethod {
@@ -154,7 +162,7 @@ impl std::str::FromStr for SortMethod {
"Config" => Ok(Self::Config),
"Category" => Ok(Self::Category),
"Template" => Ok(Self::Template),
&_ => Err(())
&_ => Err(()),
}
}
}
@@ -167,7 +175,8 @@ impl std::string::ToString for SortMethod {
SM::Config => "Config",
SM::Category => "Category",
SM::Template => "Template",
}.to_string()
}
.to_string()
}
}

View File

@@ -1,18 +1,14 @@
use leptos::{either::Either, prelude::*};
use serde::{Serialize, Deserialize};
use serde::{Deserialize, Serialize};
#[cfg(feature = "ssr")]
use {
sqlx::SqlitePool,
leptos::server_fn::error::NoCustomError,
crate::db::user,
};
use {crate::db::user, leptos::server_fn::error::NoCustomError, sqlx::SqlitePool};
use super::BeaconResources;
#[derive(Clone, Serialize, Deserialize)]
pub struct Category {
pub category_id: i64,
pub category_name: String
pub category_name: String,
}
#[server]
@@ -20,28 +16,26 @@ pub async fn get_categories() -> Result<Vec<Category>, ServerFnError> {
let user = user::get_auth_session().await?;
if user.is_none() {
return Err(ServerFnError::<NoCustomError>::ServerError("You are not signed in!".to_owned()));
return Err(ServerFnError::<NoCustomError>::ServerError(
"You are not signed in!".to_owned(),
));
}
let db = expect_context::<SqlitePool>();
Ok(
sqlx::query_as!(
Category,
"SELECT * FROM beacon_category"
)
.fetch_all(&db)
.await?
)
Ok(sqlx::query_as!(Category, "SELECT * FROM beacon_category")
.fetch_all(&db)
.await?)
}
#[server]
pub async fn add_category(name: String) -> Result<(), ServerFnError> {
let user = user::get_auth_session().await?;
if user.is_none() {
return Err(ServerFnError::<NoCustomError>::ServerError("You are not signed in!".to_owned()));
return Err(ServerFnError::<NoCustomError>::ServerError(
"You are not signed in!".to_owned(),
));
}
let db = expect_context::<SqlitePool>();
@@ -50,8 +44,8 @@ pub async fn add_category(name: String) -> Result<(), ServerFnError> {
"INSERT INTO beacon_category (category_name) VALUES (?)",
name
)
.execute(&db)
.await?;
.execute(&db)
.await?;
Ok(())
}
@@ -61,15 +55,14 @@ pub async fn remove_category(id: i64) -> Result<(), ServerFnError> {
let user = user::get_auth_session().await?;
if user.is_none() {
return Err(ServerFnError::<NoCustomError>::ServerError("You are not signed in!".to_owned()));
return Err(ServerFnError::<NoCustomError>::ServerError(
"You are not signed in!".to_owned(),
));
}
let db = expect_context::<SqlitePool>();
sqlx::query!(
"DELETE FROM beacon_category WHERE category_id = ?",
id
)
sqlx::query!("DELETE FROM beacon_category WHERE category_id = ?", id)
.execute(&db)
.await?;
@@ -81,7 +74,9 @@ pub async fn rename_category(id: i64, name: String) -> Result<(), ServerFnError>
let user = user::get_auth_session().await?;
if user.is_none() {
return Err(ServerFnError::<NoCustomError>::ServerError("You are not signed in!".to_owned()));
return Err(ServerFnError::<NoCustomError>::ServerError(
"You are not signed in!".to_owned(),
));
}
let db = expect_context::<SqlitePool>();
@@ -91,15 +86,19 @@ pub async fn rename_category(id: i64, name: String) -> Result<(), ServerFnError>
name,
id
)
.execute(&db)
.await?;
.execute(&db)
.await?;
Ok(())
}
#[component]
pub fn CategoriesView() -> impl IntoView {
let BeaconResources { add_category, categories, .. } = expect_context();
let BeaconResources {
add_category,
categories,
..
} = expect_context();
view! {
<div class="categories">
@@ -148,7 +147,11 @@ pub fn CategoriesView() -> impl IntoView {
#[component]
fn DisplayCategories(categories: Vec<Category>) -> impl IntoView {
let BeaconResources { remove_category, rename_category, .. } = expect_context();
let BeaconResources {
remove_category,
rename_category,
..
} = expect_context();
let (target_rename_id, set_target_rename_id) = signal(0);
let target_rename_name = RwSignal::new("".to_owned());
@@ -157,55 +160,59 @@ fn DisplayCategories(categories: Vec<Category>) -> impl IntoView {
let categories_view = categories
.iter()
.map(|category| view! {
<li>
{category.category_id}
": "
{category.category_name.clone()}
<button
on:click={
let id = category.category_id;
let name = category.category_name.clone();
let set_target_rename_id = set_target_rename_id.clone();
let target_rename_name = target_rename_name.clone();
move |_| {
set_target_rename_id(id);
target_rename_name.set(name.clone());
.map(|category| {
view! {
<li>
{category.category_id}
": "
{category.category_name.clone()}
<button
on:click={
let id = category.category_id;
let name = category.category_name.clone();
let set_target_rename_id = set_target_rename_id.clone();
let target_rename_name = target_rename_name.clone();
move |_| {
set_target_rename_id(id);
target_rename_name.set(name.clone());
if let Some(dialog) = dialog_ref.get() {
let _ = dialog.show_modal();
if let Some(dialog) = dialog_ref.get() {
let _ = dialog.show_modal();
}
}
}
}
>
"rename"
</button>
<button
on:click={
let id = category.category_id;
move |_| {
remove_category.dispatch(RemoveCategory { id });
>
"rename"
</button>
<button
on:click={
let id = category.category_id;
move |_| {
remove_category.dispatch(RemoveCategory { id });
}
}
}
>
"delete"
</button>
</li>
>
"delete"
</button>
</li>
}
})
.collect_view();
Effect::watch(
move || (
rename_category.version().get(),
rename_category.value().get(),
dialog_ref.get()
),
move |(_, res,dialog_ref),_,_| {
move || {
(
rename_category.version().get(),
rename_category.value().get(),
dialog_ref.get(),
)
},
move |(_, res, dialog_ref), _, _| {
if let (Some(Ok(())), Some(dialog)) = (res, dialog_ref) {
let _ = dialog.close();
}
},
false
false,
);
view! {

View File

@@ -2,12 +2,10 @@ use leptos::{either::Either, prelude::*};
use serde::{Deserialize, Serialize};
#[cfg(feature = "ssr")]
use {
std::str::FromStr,
sqlx::{sqlite::SqliteRow, FromRow, Row, SqlitePool},
crate::db::user,
leptos::server_fn::error::NoCustomError,
crate::db::user
sqlx::{sqlite::SqliteRow, FromRow, Row, SqlitePool},
std::str::FromStr,
};
use super::BeaconResources;
@@ -32,7 +30,7 @@ impl FromRow<'_, SqliteRow> for BeaconConfigTypes {
)),
"cron" => Ok(Self::CronSchedule(
row.try_get("cron_schedule")?,
row.try_get("cron_mode")?
row.try_get("cron_mode")?,
)),
type_name => Err(sqlx::Error::TypeNotFound {
type_name: type_name.to_string(),
@@ -55,7 +53,9 @@ pub async fn get_beacon_configs() -> Result<Vec<BeaconConfig>, ServerFnError> {
let user = user::get_auth_session().await?;
if user.is_none() {
return Err(ServerFnError::<NoCustomError>::ServerError("You are not signed in!".to_owned()));
return Err(ServerFnError::<NoCustomError>::ServerError(
"You are not signed in!".to_owned(),
));
}
let db = expect_context::<SqlitePool>();
@@ -78,7 +78,9 @@ pub async fn add_beacon_config(
let user = user::get_auth_session().await?;
if user.is_none() {
return Err(ServerFnError::<NoCustomError>::ServerError("You are not signed in!".to_owned()));
return Err(ServerFnError::<NoCustomError>::ServerError(
"You are not signed in!".to_owned(),
));
}
let db = expect_context::<SqlitePool>();
@@ -89,14 +91,16 @@ pub async fn add_beacon_config(
"INSERT INTO beacon_config (config_name, mode) VALUES (?, 'single')",
name
)
.execute(&db)
.await?;
.execute(&db)
.await?;
Ok(())
},
}
"regular" => {
if regular_interval < 1 {
return Err(ServerFnError::<NoCustomError>::ServerError("Invalid interval provided".to_owned()))
return Err(ServerFnError::<NoCustomError>::ServerError(
"Invalid interval provided".to_owned(),
));
}
sqlx::query!(
@@ -108,10 +112,12 @@ pub async fn add_beacon_config(
.await?;
Ok(())
},
}
"random" => {
if random_min_time < 1 || random_max_time < random_min_time {
return Err(ServerFnError::<NoCustomError>::ServerError("Invalid random interval provided".to_owned()))
return Err(ServerFnError::<NoCustomError>::ServerError(
"Invalid random interval provided".to_owned(),
));
}
sqlx::query!(
@@ -124,19 +130,21 @@ pub async fn add_beacon_config(
.await?;
Ok(())
},
}
"cron" => {
if let Err(e) = cron::Schedule::from_str(&cron_schedule) {
return Err(ServerFnError::<NoCustomError>::ServerError(format!(
"Could not parse cron expression: {}",
e
)))
)));
}
match &*cron_mode {
"local" | "utc" => {},
"local" | "utc" => {}
_ => {
return Err(ServerFnError::<NoCustomError>::ServerError("Unrecognized timezone specifier for cron".to_string()))
return Err(ServerFnError::<NoCustomError>::ServerError(
"Unrecognized timezone specifier for cron".to_string(),
))
}
}
@@ -150,10 +158,10 @@ pub async fn add_beacon_config(
.await?;
Ok(())
},
_ => {
Err(ServerFnError::<NoCustomError>::ServerError("Invalid mode supplied".to_owned()))
}
_ => Err(ServerFnError::<NoCustomError>::ServerError(
"Invalid mode supplied".to_owned(),
)),
}
}
@@ -162,15 +170,14 @@ pub async fn remove_beacon_config(id: i64) -> Result<(), ServerFnError> {
let user = user::get_auth_session().await?;
if user.is_none() {
return Err(ServerFnError::<NoCustomError>::ServerError("You are not signed in!".to_owned()));
return Err(ServerFnError::<NoCustomError>::ServerError(
"You are not signed in!".to_owned(),
));
}
let db = expect_context::<SqlitePool>();
sqlx::query!(
"DELETE FROM beacon_config WHERE config_id = ?",
id
)
sqlx::query!("DELETE FROM beacon_config WHERE config_id = ?", id)
.execute(&db)
.await?;
@@ -179,7 +186,11 @@ pub async fn remove_beacon_config(id: i64) -> Result<(), ServerFnError> {
#[component]
pub fn ConfigsView() -> impl IntoView {
let BeaconResources { add_beacon_config, configs, .. } = expect_context();
let BeaconResources {
add_beacon_config,
configs,
..
} = expect_context();
view! {
<div class="config">
@@ -260,42 +271,47 @@ pub fn ConfigsView() -> impl IntoView {
#[component]
fn DisplayConfigs(configs: Vec<BeaconConfig>) -> impl IntoView {
let BeaconResources { remove_beacon_config, .. } = expect_context();
let BeaconResources {
remove_beacon_config,
..
} = expect_context();
let configs_view = configs
.iter()
.map(|config| view! {
<li>
{config.config_id}
": "
{config.config_name.clone()}
" ("
{match &config.config_type {
BeaconConfigTypes::Single => {
format!("Single")
},
BeaconConfigTypes::Regular(int) => {
format!("Regular; every {int} seconds")
},
BeaconConfigTypes::Random(min, max) => {
format!("Random; regularly between {min} and {max} seconds")
},
BeaconConfigTypes::CronSchedule(sch, mode) => {
format!("{mode} cron; schedule: {sch}")
}
}}
") "
<button
on:click={
let id = config.config_id;
move |_| {
remove_beacon_config.dispatch(RemoveBeaconConfig { id });
.map(|config| {
view! {
<li>
{config.config_id}
": "
{config.config_name.clone()}
" ("
{match &config.config_type {
BeaconConfigTypes::Single => {
format!("Single")
},
BeaconConfigTypes::Regular(int) => {
format!("Regular; every {int} seconds")
},
BeaconConfigTypes::Random(min, max) => {
format!("Random; regularly between {min} and {max} seconds")
},
BeaconConfigTypes::CronSchedule(sch, mode) => {
format!("{mode} cron; schedule: {sch}")
}
}}
") "
<button
on:click={
let id = config.config_id;
move |_| {
remove_beacon_config.dispatch(RemoveBeaconConfig { id });
}
}
}
>
"delete"
</button>
</li>
>
"delete"
</button>
</li>
}
})
.collect_view();

View File

@@ -2,17 +2,14 @@
use std::net::Ipv4Addr;
use leptos::{either::Either, prelude::*};
use serde::{Serialize, Deserialize};
use serde::{Deserialize, Serialize};
#[cfg(feature = "ssr")]
use {
sqlx::SqlitePool,
crate::db::user,
leptos::server_fn::error::NoCustomError,
rcgen::{generate_simple_self_signed, CertifiedKey},
sparse_handler::BeaconListenerMap,
crate::db::user,
sqlx::SqlitePool,
};
use super::BeaconResources;
@@ -22,7 +19,7 @@ struct DbListener {
listener_id: i64,
port: i64,
public_ip: String,
domain_name: String
domain_name: String,
}
#[derive(Clone, Serialize, Deserialize)]
@@ -31,7 +28,7 @@ pub struct PubListener {
pub port: i64,
pub public_ip: String,
pub domain_name: String,
pub active: bool
pub active: bool,
}
#[server]
@@ -39,7 +36,9 @@ pub async fn get_listeners() -> Result<Vec<PubListener>, ServerFnError> {
let user = user::get_auth_session().await?;
if user.is_none() {
return Err(ServerFnError::<NoCustomError>::ServerError("You are not signed in!".to_owned()));
return Err(ServerFnError::<NoCustomError>::ServerError(
"You are not signed in!".to_owned(),
));
}
let db = expect_context::<SqlitePool>();
@@ -49,8 +48,8 @@ pub async fn get_listeners() -> Result<Vec<PubListener>, ServerFnError> {
DbListener,
"SELECT listener_id, port, public_ip, domain_name FROM beacon_listener"
)
.fetch_all(&db)
.await?;
.fetch_all(&db)
.await?;
let Ok(beacon_handles_handle) = beacon_handles.read() else {
return Err(ServerFnError::<NoCustomError>::ServerError("".to_string()));
@@ -66,13 +65,16 @@ pub async fn get_listeners() -> Result<Vec<PubListener>, ServerFnError> {
active: beacon_handles_handle
.get(&b.listener_id)
.map(|h| !h.is_finished())
.unwrap_or(false)
.unwrap_or(false),
})
.collect())
}
#[cfg(feature = "ssr")]
pub fn generate_cert_from_keypair(kp: &rcgen::KeyPair, names: Vec<String>) -> Result<rcgen::Certificate, rcgen::Error> {
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)?;
@@ -83,25 +85,33 @@ pub fn generate_cert_from_keypair(kp: &rcgen::KeyPair, names: Vec<String>) -> Re
}
#[server]
pub async fn add_listener(public_ip: String, port: i16, domain_name: String) -> Result<(), ServerFnError> {
pub async fn add_listener(
public_ip: String,
port: i16,
domain_name: String,
) -> Result<(), ServerFnError> {
let user = user::get_auth_session().await?;
if user.is_none() {
return Err(ServerFnError::<NoCustomError>::ServerError("You are not signed in!".to_owned()));
return Err(ServerFnError::<NoCustomError>::ServerError(
"You are not signed in!".to_owned(),
));
}
if public_ip.parse::<Ipv4Addr>().is_err() {
return Err(ServerFnError::<NoCustomError>::ServerError("Unable to parse public IP address".to_owned()));
return Err(ServerFnError::<NoCustomError>::ServerError(
"Unable to parse public IP address".to_owned(),
));
}
let subject_alt_names = vec![public_ip.to_string(), domain_name.clone()];
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??;
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>();
@@ -128,20 +138,26 @@ pub async fn remove_listener(listener_id: i64) -> Result<(), ServerFnError> {
let user = user::get_auth_session().await?;
if user.is_none() {
return Err(ServerFnError::<NoCustomError>::ServerError("You are not signed in!".to_owned()));
return Err(ServerFnError::<NoCustomError>::ServerError(
"You are not signed in!".to_owned(),
));
}
{
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()));
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()));
return Err(ServerFnError::<NoCustomError>::ServerError(
"Failed to get write handle for beacon listener map".to_owned(),
));
}
blm_handle.remove(&listener_id);
@@ -153,8 +169,8 @@ pub async fn remove_listener(listener_id: i64) -> Result<(), ServerFnError> {
"DELETE FROM beacon_listener WHERE listener_id = ?",
listener_id
)
.execute(&pool)
.await?;
.execute(&pool)
.await?;
Ok(())
}
@@ -164,21 +180,23 @@ pub async fn start_listener(listener_id: i64) -> Result<(), ServerFnError> {
let user = user::get_auth_session().await?;
if user.is_none() {
return Err(ServerFnError::<NoCustomError>::ServerError("You are not signed in!".to_owned()));
return Err(ServerFnError::<NoCustomError>::ServerError(
"You are not signed in!".to_owned(),
));
}
sparse_handler::start_listener(
expect_context(),
listener_id,
expect_context()
).await?;
sparse_handler::start_listener(expect_context(), listener_id, expect_context()).await?;
Ok(())
}
#[component]
pub fn ListenersView() -> impl IntoView {
let super::BeaconResources { add_listener, listeners, .. } = expect_context::<super::BeaconResources>();
let super::BeaconResources {
add_listener,
listeners,
..
} = expect_context::<super::BeaconResources>();
view! {
<div class="listeners">
@@ -230,7 +248,11 @@ pub fn ListenersView() -> impl IntoView {
#[component]
fn DisplayListeners(listeners: Vec<PubListener>) -> impl IntoView {
let BeaconResources { listeners: listener_resource, remove_listener, .. } = expect_context::<BeaconResources>();
let BeaconResources {
listeners: listener_resource,
remove_listener,
..
} = expect_context::<BeaconResources>();
let (error_msg, set_error_msg) = signal(None);
let start_listener_action = Action::new(move |&id: &i64| async move {
@@ -246,46 +268,48 @@ fn DisplayListeners(listeners: Vec<PubListener>) -> impl IntoView {
let listeners_view = listeners
.iter()
.map(|listener| view! {
<li>
{listener.listener_id}
": "
{listener.domain_name.clone()}
" ("
{listener.public_ip.clone()}
":"
{listener.port}
") "
{match listener.active {
true => Either::Left(view! {
<span>"active!"</span>
}),
false => Either::Right(view! {
<button
on:click={
let id = listener.listener_id;
move |e| {
let _ = e.prevent_default();
start_listener_action.dispatch(id);
.map(|listener| {
view! {
<li>
{listener.listener_id}
": "
{listener.domain_name.clone()}
" ("
{listener.public_ip.clone()}
":"
{listener.port}
") "
{match listener.active {
true => Either::Left(view! {
<span>"active!"</span>
}),
false => Either::Right(view! {
<button
on:click={
let id = listener.listener_id;
move |e| {
let _ = e.prevent_default();
start_listener_action.dispatch(id);
}
}
>
"activate"
</button>
})
}}
<button
on:click={
let id = listener.listener_id;
move |e| {
let _ = e.prevent_default();
remove_listener.dispatch(RemoveListener { listener_id: id });
}
>
"activate"
</button>
})
}}
<button
on:click={
let id = listener.listener_id;
move |e| {
let _ = e.prevent_default();
remove_listener.dispatch(RemoveListener { listener_id: id });
}
}
>
"delete"
</button>
</li>
>
"delete"
</button>
</li>
}
})
.collect_view();

View File

@@ -1,13 +1,11 @@
use leptos::{either::Either, prelude::*};
use serde::{Serialize, Deserialize};
use serde::{Deserialize, Serialize};
#[cfg(feature = "ssr")]
use {
std::net::Ipv4Addr,
sqlx::{sqlite::SqliteRow, FromRow, Row, SqlitePool},
crate::db::user,
leptos::server_fn::error::NoCustomError,
crate::db::user
sqlx::{sqlite::SqliteRow, FromRow, Row, SqlitePool},
std::net::Ipv4Addr,
};
use crate::beacons::BeaconResources;
@@ -15,7 +13,7 @@ use crate::beacons::BeaconResources;
#[derive(Clone, Serialize, Deserialize)]
pub enum BeaconSourceMode {
Host,
Custom(i64, String)
Custom(i64, String),
}
#[cfg(feature = "ssr")]
@@ -25,7 +23,7 @@ impl FromRow<'_, SqliteRow> for BeaconSourceMode {
"host" => Ok(Self::Host),
"custom" => Ok(Self::Custom(
row.try_get("source_netmask")?,
row.try_get("source_gateway")?
row.try_get("source_gateway")?,
)),
type_name => Err(sqlx::Error::TypeNotFound {
type_name: type_name.to_string(),
@@ -48,7 +46,7 @@ pub struct BeaconTemplate {
config_id: i64,
listener_id: i64,
default_category: Option<i64>
default_category: Option<i64>,
}
cfg_if::cfg_if! {
@@ -72,7 +70,8 @@ pub async fn add_template(
source_mac: String,
source_mode: String,
source_netmask: i64,
source_gateway: String
source_gateway: String,
source_interface: String,
) -> Result<(), ServerFnError> {
let user = user::get_auth_session().await?;
@@ -97,7 +96,11 @@ pub async fn add_template(
}
let mac_parts = source_mac.split(":").collect::<Vec<_>>();
if mac_parts.len() != 6 || mac_parts.iter().any(|p| p.len() != 2 || u8::from_str_radix(p, 16).is_err()) {
if mac_parts.len() != 6
|| mac_parts
.iter()
.any(|p| p.len() != 2 || u8::from_str_radix(p, 16).is_err())
{
srverr!("Source MAC address is formatted incorrectly");
}
@@ -107,8 +110,8 @@ pub async fn add_template(
"SELECT certificate, privkey FROM beacon_listener WHERE listener_id = ?",
listener_id
)
.fetch_one(&db)
.await?;
.fetch_one(&db)
.await?;
use rcgen::{Certificate, CertificateParams, KeyPair};
@@ -119,7 +122,7 @@ pub async fn add_template(
srverr!("Could not parse private key: {e}");
}
},
&rcgen::PKCS_ECDSA_P256_SHA256
&rcgen::PKCS_ECDSA_P256_SHA256,
)?;
let ca_params = CertificateParams::from_ca_cert_der(&(*listener.certificate).into())?;
let ca_cert = ca_params.self_signed(&keypair)?;
@@ -131,6 +134,8 @@ pub async fn add_template(
let client_key_der = client_key.serialize_der();
let client_cert_der = client_cert.der().to_vec();
let interface = Some(source_interface).filter(|s| !s.is_empty());
match &*source_mode {
"host" => {
let source_mac = Some(source_mac).filter(|mac| mac != "00:00:00:00:00:00");
@@ -138,9 +143,11 @@ 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, client_key, client_cert)
(template_name, operating_system, config_id, listener_id, source_ip,
source_mac, source_mode, default_category, client_key, client_cert,
source_interface)
VALUES
(?, ?, ?, ?, ?, ?, 'host', ?, ?, ?)",
(?, ?, ?, ?, ?, ?, 'host', ?, ?, ?, ?)",
template_name,
operating_system,
config_id,
@@ -149,22 +156,25 @@ pub async fn add_template(
source_mac,
default_category,
client_key_der,
client_cert_der
client_cert_der,
interface
)
.execute(&db)
.await?;
.execute(&db)
.await?;
Ok(())
},
}
"custom" => {
let source_mac = Some(source_mac).filter(|mac| mac != "00:00:00:00:00:00");
let default_category = Some(default_category).filter(|dc| *dc != 0);
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, client_key, client_cert)
(template_name, operating_system, config_id, listener_id, source_ip,
source_mac, source_mode, source_netmask, source_gateway, default_category,
client_key, client_cert, source_interface)
VALUES
(?, ?, ?, ?, ?, ?, 'host', ?, ?, ?, ?, ?)",
(?, ?, ?, ?, ?, ?, 'host', ?, ?, ?, ?, ?, ?)",
template_name,
operating_system,
config_id,
@@ -175,13 +185,14 @@ pub async fn add_template(
source_gateway,
default_category,
client_key_der,
client_cert_der
client_cert_der,
interface
)
.execute(&db)
.await?;
.execute(&db)
.await?;
Ok(())
},
}
_other => {
srverr!("Invalid type of source mode provided");
}
@@ -198,9 +209,12 @@ pub async fn remove_template(template_id: i64) -> Result<(), ServerFnError> {
let db = expect_context::<SqlitePool>();
sqlx::query!("DELETE FROM beacon_template WHERE template_id = ?", template_id)
.execute(&db)
.await?;
sqlx::query!(
"DELETE FROM beacon_template WHERE template_id = ?",
template_id
)
.execute(&db)
.await?;
Ok(())
}
@@ -210,7 +224,9 @@ pub async fn get_templates() -> Result<Vec<BeaconTemplate>, ServerFnError> {
let user = user::get_auth_session().await?;
if user.is_none() {
return Err(ServerFnError::<NoCustomError>::ServerError("You are not signed in!".to_owned()));
return Err(ServerFnError::<NoCustomError>::ServerError(
"You are not signed in!".to_owned(),
));
}
let db = expect_context::<SqlitePool>();
@@ -222,7 +238,13 @@ pub async fn get_templates() -> Result<Vec<BeaconTemplate>, ServerFnError> {
#[component]
pub fn TemplatesView() -> impl IntoView {
let BeaconResources { configs, listeners, categories, templates, .. } = expect_context();
let BeaconResources {
configs,
listeners,
categories,
templates,
..
} = expect_context();
view! {
<div class="templates">
@@ -379,6 +401,8 @@ pub fn AddTemplateForm(
<input class="mode-custom" type="number" name="source_netmask" value="24"/>
<label class="mode-custom">"Network gateway"</label>
<input class="mode-custom" name="source_gateway"/>
<label class="mode-custom">"Network interface name"</label>
<input class="mode-custom" name="source_interface"/>
<div></div>
<input type="submit" value="Submit" />
</fieldset>
@@ -391,9 +415,11 @@ pub fn DisplayTemplates(
configs: Vec<super::configs::BeaconConfig>,
listeners: Vec<super::listeners::PubListener>,
categories: Vec<super::categories::Category>,
templates: Vec<BeaconTemplate>
templates: Vec<BeaconTemplate>,
) -> impl IntoView {
let BeaconResources { remove_template, .. } = expect_context();
let BeaconResources {
remove_template, ..
} = expect_context();
let templates_view = templates
.iter()

View File

@@ -9,7 +9,7 @@ pub async fn handle_user_command(user_command: UC, db: SqlitePool) -> anyhow::Re
match user_command {
UC::List {} => list_users(db).await,
UC::Create { user_name } => create_user(db, user_name).await,
UC::ResetPassword { user_id } => reset_password(&db, user_id).await
UC::ResetPassword { user_id } => reset_password(&db, user_id).await,
}
}
@@ -51,7 +51,7 @@ async fn create_user(db: SqlitePool, name: String) -> anyhow::Result<ExitCode> {
async fn reset_password<'a, E>(db: E, id: i16) -> anyhow::Result<ExitCode>
where
E: sqlx::SqliteExecutor<'a>
E: sqlx::SqliteExecutor<'a>,
{
let password = get_password()?;

View File

@@ -1,7 +1,13 @@
use leptos::prelude::ServerFnError;
use leptos::{prelude::expect_context, server_fn::error::NoCustomError};
use leptos_axum::{extract, ResponseOptions};
use leptos::prelude::ServerFnError;
use pbkdf2::{Pbkdf2, password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, rand_core::{OsRng, RngCore}, SaltString}};
use pbkdf2::{
password_hash::{
rand_core::{OsRng, RngCore},
PasswordHash, PasswordHasher, PasswordVerifier, SaltString,
},
Pbkdf2,
};
use sqlx::SqlitePool;
use crate::error::Error;
@@ -11,7 +17,7 @@ pub struct User {
pub user_id: i64,
pub user_name: String,
password_hash: String,
pub last_active: Option<i64>
pub last_active: Option<i64>,
}
impl std::fmt::Debug for User {
@@ -29,12 +35,13 @@ async fn hash_password(pass: &[u8]) -> Result<String, Error> {
let pass = pass.to_owned();
let salt = SaltString::generate(&mut OsRng);
move ||
Pbkdf2.hash_password(
&*pass,
&salt,
).map(|hash| hash.serialize().as_str().to_string())
}).await??)
move || {
Pbkdf2
.hash_password(&*pass, &salt)
.map(|hash| hash.serialize().as_str().to_string())
}
})
.await??)
}
async fn verify_password(pass: &str, hash: &str) -> Result<bool, Error> {
@@ -42,46 +49,49 @@ async fn verify_password(pass: &str, hash: &str) -> Result<bool, Error> {
let pass = pass.to_owned();
let hash = hash.to_owned();
move ||
move || {
PasswordHash::new(&*hash)
.map(|parsed| Pbkdf2.verify_password(
&pass.as_bytes(),
&parsed
).is_ok())
}).await??)
.map(|parsed| Pbkdf2.verify_password(&pass.as_bytes(), &parsed).is_ok())
}
})
.await??)
}
pub async fn reset_password<'a, E>(pool: E, id: i16, password: String) -> Result<(), crate::error::Error>
pub async fn reset_password<'a, E>(
pool: E,
id: i16,
password: String,
) -> Result<(), crate::error::Error>
where
E: sqlx::SqliteExecutor<'a>
E: sqlx::SqliteExecutor<'a>,
{
let password_string = hash_password(
password.as_bytes()
).await?;
let password_string = hash_password(password.as_bytes()).await?;
sqlx::query!(
"UPDATE users SET password_hash = ? WHERE user_id = ?",
password_string,
id
)
.execute(pool)
.await?;
.execute(pool)
.await?;
Ok(())
}
pub async fn create_user<'a, E>(acq: E, name: String, password: String) -> Result<(), crate::error::Error>
pub async fn create_user<'a, E>(
acq: E,
name: String,
password: String,
) -> Result<(), crate::error::Error>
where
E: sqlx::Acquire<'a, Database = sqlx::Sqlite>
E: sqlx::Acquire<'a, Database = sqlx::Sqlite>,
{
let mut tx = acq.begin().await?;
let previous_user_check = sqlx::query_scalar!(
"SELECT COUNT(*) FROM users WHERE user_name = ?",
name
)
.fetch_one(&mut *tx)
.await?;
let previous_user_check =
sqlx::query_scalar!("SELECT COUNT(*) FROM users WHERE user_name = ?", name)
.fetch_one(&mut *tx)
.await?;
if previous_user_check > 0 {
return Err(Error::UserCreate("User already exists".to_string()));
@@ -93,9 +103,9 @@ where
r#"INSERT INTO users (user_name, password_hash) VALUES (?, "")"#,
name
)
.execute(&mut *tx)
.await?
.last_insert_rowid();
.execute(&mut *tx)
.await?
.last_insert_rowid();
reset_password(&mut *tx, new_id as i16, password).await?;
@@ -108,37 +118,30 @@ const SESSION_ID_KEY: &'static str = "session_id";
const SESSION_AGE: i64 = 30 * 60;
pub async fn create_auth_session(username: String, password: String) -> Result<(), ServerFnError> {
use axum_extra::extract::cookie::{Cookie, SameSite};
use axum::http::{header, HeaderValue};
use axum_extra::extract::cookie::{Cookie, SameSite};
let db = expect_context::<SqlitePool>();
let resp = expect_context::<ResponseOptions>();
let user: Option<User> = sqlx::query_as!(
User,
"SELECT * FROM users WHERE user_name = ?",
username
)
.fetch_optional(&db)
.await?;
let user: Option<User> =
sqlx::query_as!(User, "SELECT * FROM users WHERE user_name = ?", username)
.fetch_optional(&db)
.await?;
let Some(user) = user else {
return Err(ServerFnError::<NoCustomError>::ServerError("Invalid credentials".to_string()));
return Err(ServerFnError::<NoCustomError>::ServerError(
"Invalid credentials".to_string(),
));
};
let good_hash = verify_password(
&password,
&user.password_hash
).await?;
let good_hash = verify_password(&password, &user.password_hash).await?;
if good_hash {
let now = chrono::Utc::now().timestamp();
let expires = now + SESSION_AGE;
sqlx::query!(
"UPDATE users SET last_active = ?",
now
)
sqlx::query!("UPDATE users SET last_active = ?", now)
.execute(&db)
.await?;
@@ -146,7 +149,8 @@ pub async fn create_auth_session(username: String, password: String) -> Result<(
let mut key = [0u8; 32];
OsRng.fill_bytes(&mut key);
hex::encode(&key[..])
}).await?;
})
.await?;
sqlx::query!(
"INSERT INTO sessions (session_id, user_id, expires) VALUES (?, ?, ?)",
@@ -154,8 +158,8 @@ pub async fn create_auth_session(username: String, password: String) -> Result<(
user.user_id,
expires
)
.execute(&db)
.await?;
.execute(&db)
.await?;
let cookie = Cookie::build((SESSION_ID_KEY, &session_id))
.http_only(true)
@@ -168,7 +172,9 @@ pub async fn create_auth_session(username: String, password: String) -> Result<(
Ok(())
} else {
Err(ServerFnError::<NoCustomError>::ServerError("Invalid credentials".to_string()))
Err(ServerFnError::<NoCustomError>::ServerError(
"Invalid credentials".to_string(),
))
}
}
@@ -184,10 +190,7 @@ pub async fn destroy_auth_session() -> Result<(), ServerFnError> {
let session_id = cookie.value();
sqlx::query!(
"DELETE FROM sessions WHERE session_id = ?",
session_id
)
sqlx::query!("DELETE FROM sessions WHERE session_id = ?", session_id)
.execute(&db)
.await?;
@@ -217,8 +220,8 @@ pub async fn get_auth_session() -> Result<Option<User>, ServerFnError> {
session_id,
now
)
.fetch_optional(&db)
.await?;
.fetch_optional(&db)
.await?;
if let Some(u) = &user {
let now = chrono::Utc::now().timestamp();
@@ -229,22 +232,19 @@ pub async fn get_auth_session() -> Result<Option<User>, ServerFnError> {
now,
u.user_id
)
.execute(&db)
.await?;
.execute(&db)
.await?;
sqlx::query!(
"UPDATE sessions SET expires = ? WHERE session_id = ?",
expires,
session_id
)
.execute(&db)
.await?;
.execute(&db)
.await?;
}
sqlx::query!(
"DELETE FROM sessions WHERE expires < ?",
now
)
sqlx::query!("DELETE FROM sessions WHERE expires < ?", now)
.execute(&db)
.await?;

View File

@@ -1,36 +1,37 @@
#[cfg(feature = "ssr")]
mod cli;
#[cfg(feature = "ssr")]
mod webserver;
#[cfg(feature = "ssr")]
mod beacons;
#[cfg(feature = "ssr")]
pub mod users;
pub mod error;
mod cli;
pub mod db;
pub mod error;
#[cfg(feature = "ssr")]
pub mod users;
#[cfg(feature = "ssr")]
mod webserver;
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() -> anyhow::Result<std::process::ExitCode> {
use std::{path::PathBuf, process::ExitCode, str::FromStr};
use sqlx::sqlite::{SqliteConnectOptions, SqlitePool};
use structopt::StructOpt;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use sqlx::sqlite::{SqlitePool, SqliteConnectOptions};
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| format!("{}=debug,sparse_handler=debug", env!("CARGO_CRATE_NAME")).into()),
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
format!("{}=debug,sparse_handler=debug", env!("CARGO_CRATE_NAME")).into()
}),
)
.with(tracing_subscriber::fmt::layer())
.init();
let options = cli::Options::from_args();
let db_location = options.db_location.clone()
let db_location = options
.db_location
.clone()
.or(std::env::var("DATABASE_URL")
.map(|p| p.replace("sqlite://", ""))
.map(PathBuf::from)
@@ -45,7 +46,7 @@ async fn main() -> anyhow::Result<std::process::ExitCode> {
if !options.init_ok {
tracing::error!("Database doesn't exist, and initialization not allowed!");
tracing::error!("{:?}", e);
return Ok(ExitCode::FAILURE)
return Ok(ExitCode::FAILURE);
}
tracing::info!("Database doesn't exist, readying initialization");
@@ -53,14 +54,13 @@ async fn main() -> anyhow::Result<std::process::ExitCode> {
let pool = SqlitePool::connect_with(
SqliteConnectOptions::from_str(&format!("sqlite://{}", db_location.to_string_lossy()))?
.create_if_missing(options.init_ok)
).await?;
.create_if_missing(options.init_ok),
)
.await?;
tracing::info!("Running database migrations...");
sqlx::migrate!()
.run(&pool)
.await?;
sqlx::migrate!().run(&pool).await?;
tracing::info!("Done running database migrations!");
@@ -69,12 +69,8 @@ async fn main() -> anyhow::Result<std::process::ExitCode> {
tracing::info!("Performing requested action, acting as web server");
webserver::serve_web(management_address, pool).await
}
Some(cli::Command::ExtractPubKey { }) => {
Ok(ExitCode::SUCCESS)
}
Some(cli::Command::User { command }) => {
cli::user::handle_user_command(command, pool).await
}
Some(cli::Command::ExtractPubKey {}) => Ok(ExitCode::SUCCESS),
Some(cli::Command::User { command }) => cli::user::handle_user_command(command, pool).await,
None => {
use std::net::{Ipv4Addr, SocketAddrV4};

View File

@@ -1,29 +1,33 @@
use chrono::{DateTime, offset::Utc};
use chrono::{offset::Utc, DateTime};
use leptos::prelude::*;
use serde::{Serialize, Deserialize};
use serde::{Deserialize, Serialize};
#[cfg(feature = "ssr")]
use {
sqlx::SqlitePool,
leptos::server_fn::error::NoCustomError,
crate::db::user
};
use {crate::db::user, leptos::server_fn::error::NoCustomError, sqlx::SqlitePool};
pub fn format_delta(time: chrono::TimeDelta) -> String {
let seconds = time.num_seconds();
match seconds {
0..=59 => format!("{} second{} ago", seconds, if seconds == 1 {""} else {"s"}),
0..=59 => format!(
"{} second{} ago",
seconds,
if seconds == 1 { "" } else { "s" }
),
60..=3599 => {
let minutes = seconds / 60;
format!("{} minute{} ago", minutes, if minutes == 1 {""} else {"s"})
format!(
"{} minute{} ago",
minutes,
if minutes == 1 { "" } else { "s" }
)
}
3600..=86399 => {
let hours = seconds / 3600;
format!("{} hours{} ago", hours, if hours == 1 {""} else {"s"})
format!("{} hours{} ago", hours, if hours == 1 { "" } else { "s" })
}
_ => {
let days = seconds / 86400;
format!("{} day{} ago", days, if days == 1 {""} else {"s"})
format!("{} day{} ago", days, if days == 1 { "" } else { "s" })
}
}
}
@@ -44,14 +48,14 @@ impl std::cmp::PartialEq for User {
pub struct DbUser {
user_id: i64,
user_name: String,
last_active: Option<i64>
last_active: Option<i64>,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct PubUser {
user_id: i64,
user_name: String,
last_active: Option<DateTime<Utc>>
last_active: Option<DateTime<Utc>>,
}
#[server]
@@ -59,15 +63,14 @@ async fn delete_user(user_id: i64) -> Result<(), ServerFnError> {
let user = user::get_auth_session().await?;
if user.is_none() {
return Err(ServerFnError::<NoCustomError>::ServerError("You are not signed in!".to_owned()));
return Err(ServerFnError::<NoCustomError>::ServerError(
"You are not signed in!".to_owned(),
));
}
let pool = expect_context::<SqlitePool>();
sqlx::query!(
"DELETE FROM users WHERE user_id = ?",
user_id
)
sqlx::query!("DELETE FROM users WHERE user_id = ?", user_id)
.execute(&pool)
.await?;
@@ -79,7 +82,9 @@ async fn reset_password(user_id: i64, password: String) -> Result<(), ServerFnEr
let user = user::get_auth_session().await?;
if user.is_none() {
return Err(ServerFnError::<NoCustomError>::ServerError("You are not signed in!".to_owned()));
return Err(ServerFnError::<NoCustomError>::ServerError(
"You are not signed in!".to_owned(),
));
}
let pool = expect_context::<SqlitePool>();
@@ -96,15 +101,21 @@ pub fn RenderUser(refresh_user_list: Action<(), ()>, user: PubUser) -> impl Into
#[cfg_attr(feature = "ssr", allow(unused_variables))]
let UseIntervalReturn { counter, .. } = use_interval(1000);
#[cfg_attr(feature = "ssr", allow(unused_variables))]
let (time_ago, set_time_ago) = signal(user.last_active.map(|active| format_delta(Utc::now() - active)));
let (time_ago, set_time_ago) = signal(
user.last_active
.map(|active| format_delta(Utc::now() - active)),
);
#[cfg(feature = "hydrate")]
Effect::watch(
move || counter.get(),
move |_, _, _| {
set_time_ago(user.last_active.map(|active| format_delta(Utc::now() - active)));
set_time_ago(
user.last_active
.map(|active| format_delta(Utc::now() - active)),
);
},
false
false,
);
let dialog_ref = NodeRef::<leptos::html::Dialog>::new();
@@ -220,23 +231,27 @@ async fn list_users() -> Result<Vec<PubUser>, ServerFnError> {
let user = user::get_auth_session().await?;
if user.is_none() {
return Err(ServerFnError::<NoCustomError>::ServerError("You are not signed in!".to_owned()));
return Err(ServerFnError::<NoCustomError>::ServerError(
"You are not signed in!".to_owned(),
));
}
use futures::stream::StreamExt;
let pool = expect_context::<SqlitePool>();
let users = sqlx::query_as!(
DbUser,
"SELECT user_id, user_name, last_active FROM users"
)
let users = sqlx::query_as!(DbUser, "SELECT user_id, user_name, last_active FROM users")
.fetch(&pool)
.map(|user| user.map(|u| PubUser {
user_id: u.user_id,
user_name: u.user_name,
last_active: u.last_active.map(|ts| DateTime::from_timestamp(ts, 0)).flatten()
}))
.map(|user| {
user.map(|u| PubUser {
user_id: u.user_id,
user_name: u.user_name,
last_active: u
.last_active
.map(|ts| DateTime::from_timestamp(ts, 0))
.flatten(),
})
})
.collect::<Vec<Result<_, _>>>()
.await;
@@ -250,7 +265,9 @@ async fn add_user(name: String, password: String) -> Result<(), ServerFnError> {
let user = user::get_auth_session().await?;
if user.is_none() {
return Err(ServerFnError::<NoCustomError>::ServerError("You are not signed in!".to_owned()));
return Err(ServerFnError::<NoCustomError>::ServerError(
"You are not signed in!".to_owned(),
));
}
let pool = expect_context::<SqlitePool>();

View File

@@ -1,9 +1,14 @@
use std::{net::SocketAddrV4, process::ExitCode};
use sqlx::sqlite::SqlitePool;
use axum::{extract::{FromRef, Path, State}, response::IntoResponse, Router, routing::get};
use axum::{
extract::{FromRef, Path, State},
response::IntoResponse,
routing::get,
Router,
};
use leptos::prelude::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use sqlx::sqlite::SqlitePool;
use tokio::signal;
use sparse_server::app::*;
@@ -11,8 +16,10 @@ 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"));
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)]
@@ -21,7 +28,11 @@ pub async fn get_installer(btype: &str) -> Result<Vec<u8>, crate::error::Error>
"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}"))),
other => {
return Err(crate::error::Error::Generic(format!(
"unknown beacon type: {other}"
)))
}
};
Ok(tokio::fs::read(path).await?)
@@ -32,20 +43,22 @@ pub async fn get_installer(btype: &str) -> Result<Vec<u8>, crate::error::Error>
"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}")))
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
leptos_options: leptos::config::LeptosOptions,
}
#[axum::debug_handler]
pub async fn download_beacon_installer(
Path(template_id): Path<i64>,
State(db): State<AppState>
State(db): State<AppState>,
) -> Result<impl IntoResponse, crate::error::Error> {
use rand::{rngs::OsRng, TryRngCore};
use sparse_actions::payload_types::{Parameters_t, XOR_KEY};
@@ -53,15 +66,17 @@ pub async fn download_beacon_installer(
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 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
source_gateway, port, public_ip, domain_name, certificate, client_cert, client_key,
source_interface
FROM beacon_template JOIN beacon_listener"
)
.fetch_one(&db.db)
.await?;
.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>()?;
@@ -79,19 +94,29 @@ pub async fn download_beacon_installer(
.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()))
)?;
.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) {
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_mac
.copy_from_slice(&src_mac[..]);
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];
@@ -100,17 +125,31 @@ pub async fn download_beacon_installer(
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];
}
if let Some(intf) = &template.source_interface {
parameters.source_ip.custom_networking.interface[..intf.len()]
.copy_from_slice(&intf[..]);
parameters.source_ip.custom_networking.interface_len = intf.len() as u8;
} else {
parameters.source_ip.custom_networking.interface_len = 0;
}
},
(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_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()));
return Err(crate::error::Error::Generic(
"Could not parse host networking configuration".to_string(),
));
}
}
@@ -144,10 +183,7 @@ pub async fn download_beacon_installer(
Ok((
[
(
header::CONTENT_TYPE,
"application/octet-stream".to_string()
),
(header::CONTENT_TYPE, "application/octet-stream".to_string()),
(
header::CONTENT_DISPOSITION,
format!(
@@ -157,17 +193,17 @@ pub async fn download_beacon_installer(
} else {
""
}
)
)
),
),
],
[
&installer_bytes[..],
&parameters_bytes[..]
].concat()
[&installer_bytes[..], &parameters_bytes[..]].concat(),
))
}
pub async fn serve_web(management_address: SocketAddrV4, db: SqlitePool) -> anyhow::Result<ExitCode> {
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;
let routes = generate_route_list(App);
@@ -183,7 +219,7 @@ pub async fn serve_web(management_address: SocketAddrV4, db: SqlitePool) -> anyh
let state = AppState {
leptos_options: leptos_options.clone(),
db: db.clone()
db: db.clone(),
};
let app = Router::new()
@@ -198,20 +234,26 @@ pub async fn serve_web(management_address: SocketAddrV4, db: SqlitePool) -> anyh
{
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())
}
},
)
.fallback(leptos_axum::file_and_error_handler::<leptos::config::LeptosOptions, _>(shell))
.fallback(leptos_axum::file_and_error_handler::<
leptos::config::LeptosOptions,
_,
>(shell))
.with_state(state)
.layer(
tower::ServiceBuilder::new()
.layer(tower_http::trace::TraceLayer::new_for_http())
.layer(compression_layer)
.layer(compression_layer),
);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
let management_listener = tokio::net::TcpListener::bind(&management_address).await?;
tracing::info!("management interface listening on http://{}", &management_address);
tracing::info!(
"management interface listening on http://{}",
&management_address
);
axum::serve(management_listener, app.into_make_service())
.with_graceful_shutdown(shutdown_signal())