diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/build.rs | 10 | ||||
-rw-r--r-- | src/config.rs | 54 | ||||
-rw-r--r-- | src/errors.rs | 49 | ||||
-rw-r--r-- | src/handlers/auth.rs | 117 | ||||
-rw-r--r-- | src/handlers/mod.rs | 24 | ||||
-rw-r--r-- | src/handlers/nets.rs | 50 | ||||
-rw-r--r-- | src/main.rs | 185 | ||||
-rw-r--r-- | src/models.rs | 24 | ||||
-rw-r--r-- | src/utils.rs | 51 |
9 files changed, 0 insertions, 564 deletions
diff --git a/src/build.rs b/src/build.rs deleted file mode 100644 index d7d6882..0000000 --- a/src/build.rs +++ /dev/null @@ -1,10 +0,0 @@ -use ructe::{Ructe, RucteError}; - -fn main() -> Result<(), RucteError> { - let mut ructe = Ructe::from_env()?; - let mut statics = ructe.statics()?; - statics.add_files("statics")?; - statics.add_sass_file("scss/style.scss")?; - - ructe.compile_templates("templates") -} diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index c153085..0000000 --- a/src/config.rs +++ /dev/null @@ -1,54 +0,0 @@ -use serde::Deserialize; -use tracing::error; - -use crate::errors::ServiceError; -use std::fs; -use std::str; - -#[derive(Deserialize)] -pub struct Config { - pub server: ServerConfig, - pub database: DbConfig, - pub email: EmailConfig, -} - -#[derive(Deserialize)] -pub struct ServerConfig { - pub bind_addr: String, - pub admin_email: String -} - -#[derive(Deserialize, Clone)] -pub struct DbConfig { - pub postgres_url: String, - pub max_connections: u32, -} - -#[derive(Deserialize)] -pub struct EmailConfig { - pub smtp_server: String, - pub smtp_port: u16, - pub smtp_tls: bool, - pub smtp_starttls: bool, - pub smtp_username: Option<String>, - pub smtp_password: Option<String>, - pub email_from: String, - pub email_helo: String, -} - -impl Config { - pub fn init(path: String) -> Result<Config, ServiceError> { - if let Ok(c) = fs::read(path) { - if c.len() == 0 { - error!("Config file empty."); - return Err(ServiceError::MissingConfig); - } else { - let config: Config = toml::from_str(str::from_utf8(&c).unwrap())?; - return Ok(config); - } - } else { - error!("Unable to read from config file."); - return Err(ServiceError::MissingConfig); - } - } -} diff --git a/src/errors.rs b/src/errors.rs deleted file mode 100644 index dcc0ae8..0000000 --- a/src/errors.rs +++ /dev/null @@ -1,49 +0,0 @@ -use axum::body; -use axum::response::{IntoResponse, Response, Html}; -use hyper::StatusCode; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum ServiceError { - #[error("No config file")] - MissingConfig, - #[error("Invalid config file: {0}")] - InvalidConfig(#[from] toml::de::Error), - #[error("Not Authorized")] - NotAuthorized, - #[error("Not Found")] - NotFound, - #[error("Axum: {0}")] - Axum(#[from] axum::Error), - #[error("SQL: {0}")] - Sql(#[from] sqlx::Error), - #[error("Bcrypt: {0}")] - Bcrypt(#[from] bcrypt::BcryptError), - #[error("Parse Error: {0}")] - Parse(String), - #[error("Email: {0}")] - Email(String), -} - -pub type StringResult<T = String> = Result<T, ServiceError>; -pub type HtmlResult<T = Html<String>> = Result<T, ServiceError>; -pub type JsonResult<T> = Result<T, ServiceError>; - -impl IntoResponse for ServiceError { - fn into_response(self) -> axum::response::Response { - let body = body::boxed(body::Full::from(self.to_string())); - - let status = match self { - ServiceError::NotFound => StatusCode::NOT_FOUND, - ServiceError::NotAuthorized => StatusCode::UNAUTHORIZED, - _ => StatusCode::INTERNAL_SERVER_ERROR, - }; - Response::builder().status(status).body(body).unwrap() - } -} - -impl From<ipnetwork::IpNetworkError> for ServiceError { - fn from(e: ipnetwork::IpNetworkError) -> Self { - Self::Parse(e.to_string()) - } -} diff --git a/src/handlers/auth.rs b/src/handlers/auth.rs deleted file mode 100644 index ab72bc8..0000000 --- a/src/handlers/auth.rs +++ /dev/null @@ -1,117 +0,0 @@ -use std::sync::Arc; - -use axum::{response::{IntoResponse, Html, Redirect}, Form, Extension}; -use axum_extra::extract::{PrivateCookieJar, cookie::Cookie}; -use serde::Deserialize; -use sqlx::{query, query_as, pool::PoolConnection, Postgres}; -use tracing::{debug, instrument}; -use crate::{errors::ServiceError, State, models::DbUser}; -use chrono::prelude::*; - -#[derive(Deserialize, Debug)] -pub struct RegisterForm { - pub email: String, - pub prefname: String, - pub password: String, - #[serde(rename="password-confirm")] - pub password_confirm: String -} - -#[derive(Deserialize)] -pub struct LoginForm { - pub email: String, - pub password: String, -} - -pub async fn login() -> impl IntoResponse { - let mut buf = Vec::new(); - crate::templates::login_html(&mut buf).unwrap(); - - Html(buf) -} - -pub async fn login_post(Form(login): Form<LoginForm>, state: Extension<Arc<State>>, jar: PrivateCookieJar) -> Result<(PrivateCookieJar, Redirect), ServiceError> { - let mut conn = state.conn.acquire().await?; - - let user: DbUser = query_as("SELECT * FROM users WHERE email=$1").bind(login.email) - .fetch_one(&mut conn) - .await?; - - if bcrypt::verify(login.password, &user.pw_hash)? { - debug!("Logged in ID {} (email {})", user.id, user.email); - query("UPDATE users SET last_login=$1 WHERE id=$2").bind(Utc::now()).bind(user.id.clone()) - .execute(&mut conn) - .await?; - - let updated_jar = jar.add(Cookie::build("user-id", user.id.clone()) - .path("/") - .finish()); - - Ok((updated_jar, Redirect::to("/"))) - } else { - let updated_jar = jar; - Ok((updated_jar, Redirect::to("/dash/auth/login"))) - } -} - -pub async fn register() -> impl IntoResponse { - let mut buf = Vec::new(); - crate::templates::register_html(&mut buf).unwrap(); - - Html(buf) -} - -pub async fn register_post(Form(reg): Form<RegisterForm>, state: Extension<Arc<State>>) -> impl IntoResponse { - if reg.password_confirm == reg.password { - let hash = match bcrypt::hash(reg.password, bcrypt::DEFAULT_COST) { - Ok(h) => h, - Err(e) => { - return Err(ServiceError::NotAuthorized); - } - }; - - let mut conn = state.conn.acquire().await?; - let ulid = ulid::Ulid::new(); - - query("INSERT INTO users (id, email, pref_name, pw_hash, last_login) VALUES ($1, $2, $3, $4, $5)").bind(ulid.to_string()).bind(reg.email.clone()).bind(reg.prefname).bind(hash).bind(Utc::now()) - .execute(&mut conn) - .await?; - - } - - Ok(Redirect::to("/dash/auth/login")) -} - -pub async fn logout_post(jar: PrivateCookieJar) -> Result<(PrivateCookieJar, Redirect), ServiceError> { - if let Some(id) = jar.get("user-id") { - debug!("Found user {}", id); - - let updated_jar = jar.remove(id); - - Ok((updated_jar, Redirect::to("/dash/auth/login"))) - } else { - Ok((jar, Redirect::to("/"))) - } -} - -#[instrument] -pub async fn get_user_or_403(jar: PrivateCookieJar, conn: &mut PoolConnection<Postgres>) -> Result<DbUser, ServiceError> { - debug!("Starting middleware get_user_or_403"); - debug!("Displaying all cookies"); - for c in jar.iter() { - debug!("{}={}", c.name(), c.value()); - } - if let Some(id) = jar.get("user-id") { - debug!("Found user {}", id); - - let user: DbUser = query_as("SELECT * FROM users WHERE id=$1").bind(id.value()) - .fetch_one(conn) - .await?; - - Ok(user) - - } else { - debug!("No user found"); - Err(ServiceError::NotAuthorized) - } -} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs deleted file mode 100644 index 24db540..0000000 --- a/src/handlers/mod.rs +++ /dev/null @@ -1,24 +0,0 @@ -use axum::{Router, routing::{get, post}}; - -pub mod auth; -mod nets; - -pub async fn gen_routers() -> Router { - - Router::new() - .nest("/auth", auth_routes().await) - .nest("/nets", net_routes().await) -} - -async fn auth_routes() -> Router { - - Router::new() - .route("/login", get(auth::login).post(auth::login_post)) - .route("/register", get(auth::register).post(auth::register_post)) - .route("/logout", post(auth::logout_post)) -} - -async fn net_routes() -> Router { - Router::new() - .route("/new", get(nets::new).post(nets::new_post)) -} diff --git a/src/handlers/nets.rs b/src/handlers/nets.rs deleted file mode 100644 index 6010787..0000000 --- a/src/handlers/nets.rs +++ /dev/null @@ -1,50 +0,0 @@ -use std::sync::Arc; - -use axum::{Extension, Form}; -use axum::response::{Html, IntoResponse, Redirect}; -use axum_extra::extract::PrivateCookieJar; -use sqlx::query; -use sqlx::types::ipnetwork::IpNetwork; -use serde::Deserialize; - -use crate::State; - -use crate::errors::{HtmlResult, ServiceError}; - -use super::auth::get_user_or_403; - -#[derive(Deserialize)] -pub struct NewNetForm { - pub subnet: String, - pub description: String, -} - -pub async fn new(jar: PrivateCookieJar, state: Extension<Arc<State>>) -> Result<Html<Vec<u8>>, ServiceError> { - let mut conn = state.conn.acquire().await?; - - let _ = get_user_or_403(jar, &mut conn).await?; - - let mut buf = Vec::new(); - crate::templates::new_net_html(&mut buf).unwrap(); - - Ok(Html(buf)) -} - -pub async fn new_post(Form(new): Form<NewNetForm>, jar: PrivateCookieJar, state: Extension<Arc<State>>) -> Result<Redirect, ServiceError> { - let mut conn = state.conn.acquire().await?; - - let _ = get_user_or_403(jar, &mut conn).await?; - - let id = ulid::Ulid::new(); - - let cidr: IpNetwork = match new.subnet.parse() { - Ok(c) => c, - Err(e) => { - return Err(ServiceError::Parse(e.to_string())); - } - }; - - query("INSERT INTO networks (subnet, description, id) VALUES ($1, $2, $3)").bind(cidr).bind(new.description).bind(id.to_string()).execute(&mut conn).await?; - - Ok(Redirect::to("/")) -} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 6ebe145..0000000 --- a/src/main.rs +++ /dev/null @@ -1,185 +0,0 @@ -mod config; -mod errors; -mod handlers; -mod models; -mod utils; - -use std::{net::SocketAddr, str::FromStr, sync::Arc, time::Duration}; - -use axum::body; -use axum::extract::Path; -use axum::{error_handling::HandleErrorLayer, routing::get, BoxError, Extension, Router}; -use axum::response::{Html, IntoResponse, Response}; -use axum_extra::extract::{PrivateCookieJar, SignedCookieJar}; -use axum_extra::extract::cookie::Key; -use errors::{StringResult, HtmlResult}; -use hyper::StatusCode; -use models::{Peer, Network}; -use sqlx::query_as; -use sqlx::{PgPool, postgres::PgPoolOptions}; -use tower::ServiceBuilder; -use tower_http::trace::TraceLayer; -use tracing::{error, info, debug, trace}; -use crate::errors::ServiceError; -use tracing_subscriber::prelude::*; -use crate::models::DbUser; -use crate::handlers::auth::get_user_or_403; - -pub struct State { - pub config: config::Config, - pub conn: PgPool -} - -#[tokio::main] -async fn main() { - color_eyre::install().unwrap(); - tracing_subscriber::registry() - .with(tracing_subscriber::EnvFilter::new("debug")) - .with(tracing_subscriber::fmt::layer()) - .init(); - let config = match config::Config::init("/etc/nccd/config.toml".to_owned()) { - Ok(c) => c, - Err(e) => { - error!("Config Error: {:?}", e); - std::process::exit(1); - } - }; - - let bind_addr = config.server.bind_addr.clone(); - - let db_config = config.database.clone(); - - let conn = PgPoolOptions::new() - .max_connections(db_config.max_connections) - .connect(&db_config.postgres_url) - .await.unwrap(); - - let shared_state = Arc::new(State { config, conn }); - - let key = load_or_gen_keypair().unwrap(); - - let app = Router::new() - .route("/health", get(health_check)) - .route("/", get(index)) - .nest("/dash", handlers::gen_routers().await) - .route("/static/:name", get(statics)) - .layer( - ServiceBuilder::new() - .layer(HandleErrorLayer::new(|error: BoxError| async move { - if error.is::<tower::timeout::error::Elapsed>() { - Ok(StatusCode::REQUEST_TIMEOUT) - } else { - Err(( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Unhandled internal error: {}", error), - )) - } - })) - .timeout(Duration::from_secs(10)) - .layer(TraceLayer::new_for_http()) - .into_inner(), - ) - .layer(Extension(shared_state)) - .layer(Extension(key)); - - let addr = match SocketAddr::from_str(&bind_addr) { - Ok(a) => a, - Err(e) => { - error!("Invalid bind addr: {:?}", e); - std::process::exit(1); - } - }; - - info!("Listening on {}", addr); - - axum::Server::bind(&addr) - .serve(app.into_make_service()) - .await - .unwrap(); -} - -async fn health_check() -> &'static str { - "OK" -} - -#[axum_macros::debug_handler] -async fn index(state: Extension<Arc<State>>, jar: PrivateCookieJar) -> HtmlResult { - let mut conn = state.conn.acquire().await?; - let user: Option<DbUser> = match get_user_or_403(jar, &mut conn).await { - Ok(u) => Some(u), - Err(_) => None, - }; - - let peers: Vec<Peer> = query_as("SELECT * FROM peers").fetch_all(&mut conn).await?; - let nets: Vec<Network> = query_as("SELECT * FROM networks").fetch_all(&mut conn).await?; - - let mut buf = Vec::new(); - crate::templates::index_html(&mut buf, user, peers, nets).unwrap(); - - match String::from_utf8(buf) { - Ok(s) => Ok(Html(s)), - Err(_) => Err(ServiceError::NotFound), - } -} - -async fn test_email() -> Result<(), ServiceError> { - utils::send_email("csalter@carathe.dev".to_string(), "Test Email".to_string(), "Hi, test".to_string()).await?; - - Ok(()) -} - -async fn statics(Path(name): Path<String>) -> Result<Response, ServiceError> { - for s in templates::statics::STATICS { - trace!("Name: {}\nContents:\n{:?}\n\n", s.name, s.content); - } - - match templates::statics::StaticFile::get(&name) { - Some(s) => match String::from_utf8(s.content.to_vec()) { - Ok(c) => { - let body = body::boxed(body::Full::from(c)); - - Ok(Response::builder() - .header("Content-Type", "text/css") - .status(StatusCode::OK) - .body(body).unwrap()) - }, - Err(_) => Err(ServiceError::NotFound), - }, - None => Err(ServiceError::NotFound), - } -} - -use std::fs::{self, File}; -fn load_or_gen_keypair() -> Result<Key, ServiceError> { - let kp: Key; - let mut file = match File::open(".keypair") { - Ok(f) => f, - Err(_) => { - debug!("File does not exist, creating at .keypair"); - File::create(".keypair").unwrap() - } - }; - if let Ok(c) = fs::read(".keypair") { - if c.len() == 0 { - debug!("No keypair found. Generating..."); - let key = Key::generate(); - fs::write(".keypair", key.master().as_ref()).unwrap(); - debug!("Written keypair {:?} to .keypair", key.master().as_ref()); - kp = key; - } else { - debug!("Found keypair file, contents: {:?}", c); - kp = Key::from(&c); - debug!("Loaded keypair from file"); - } - } else { - debug!("Generating new keypair"); - let key = Key::generate(); - fs::write(".keypair", key.master().as_ref()).unwrap(); - debug!("Written keypair {:?} to .keypair", key.master().as_ref()); - kp = key; - } - Ok(kp) -} - - -include!(concat!(env!("OUT_DIR"), "/templates.rs")); diff --git a/src/models.rs b/src/models.rs deleted file mode 100644 index 2295154..0000000 --- a/src/models.rs +++ /dev/null @@ -1,24 +0,0 @@ -use sqlx::{FromRow, types::ipnetwork::IpNetwork}; - -#[derive(Debug, FromRow, Clone)] -pub struct DbUser { - pub id: String, - pub email: String, - pub pref_name: String, - pub pw_hash: String, - pub last_login: chrono::NaiveDateTime -} - -#[derive(Debug, FromRow, Clone)] -pub struct Peer { - pub id: String, - pub addr: IpNetwork, - pub public_key: String, -} - -#[derive(Debug, FromRow, Clone)] -pub struct Network { - pub id: String, - pub subnet: IpNetwork, - pub description: Option<String> -} diff --git a/src/utils.rs b/src/utils.rs deleted file mode 100644 index e723db8..0000000 --- a/src/utils.rs +++ /dev/null @@ -1,51 +0,0 @@ -use lettre::{Message, transport::smtp::{authentication::Credentials, extension::ClientId}, transport::smtp::AsyncSmtpTransport, AsyncTransport, Tokio1Executor}; - -use crate::{errors::ServiceError, config}; - -pub async fn send_email(to: String, subject: String, content: String) -> Result<(), ServiceError> { - let config = config::Config::init("/etc/nccd/config.toml".to_string()).unwrap(); - let email = if let Ok(e) = Message::builder() - .from(config.email.email_from.parse().unwrap()) - .to(to.parse().unwrap()) - .subject(subject) - .body(content) { - e - } else { - return Err(ServiceError::Email("Invalid email content".to_string())); - }; - let mailer: AsyncSmtpTransport<Tokio1Executor>; - let helo = ClientId::Domain(config.email.email_helo); - if let Some(u) = config.email.smtp_username { - let creds = Credentials::new(u, config.email.smtp_password.unwrap()); - if config.email.smtp_starttls { - mailer = AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(&config.email.smtp_server) - .unwrap() - .credentials(creds) - .hello_name(helo) - .build(); - } else { - mailer = AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(&config.email.smtp_server) - .credentials(creds) - .hello_name(helo) - .build(); - } - } else { - if config.email.smtp_tls && config.email.smtp_starttls { - mailer = AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(&config.email.smtp_server).unwrap().hello_name(helo) - .build(); - } else if config.email.smtp_tls { - mailer = AsyncSmtpTransport::<Tokio1Executor>::relay(&config.email.smtp_server).unwrap().hello_name(helo).build(); - } else { - mailer = AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(&config.email.smtp_server).hello_name(helo).build(); - } - } - - if let Err(e) = mailer.test_connection().await { - return Err(ServiceError::Email(e.to_string())); - } else { - if let Err(e) = mailer.send(email).await { - return Err(ServiceError::Email(e.to_string())); - } - } - Ok(()) -} |