From 8d3b3eb23c2fd71c033974699f155408cd46faa9 Mon Sep 17 00:00:00 2001 From: asonix Date: Sat, 12 Sep 2020 17:12:48 -0500 Subject: [PATCH] Authentication, more styles --- .gitignore | 1 + Cargo.lock | 44 +++++++- Cargo.toml | 4 + config.toml | 4 + scss/index.scss | 108 ++++++++++++++++++- src/main.rs | 160 ++++++++++++++++++++++++---- src/middleware.rs | 22 ++++ src/session.rs | 57 ++++++++++ src/startup/mod.rs | 46 +++++++- templates/home.rs.html | 24 +++++ templates/layout.rs.html | 17 +++ templates/login.rs.html | 27 +++++ templates/return_home.rs.html | 7 ++ templates/rules.rs.html | 191 +++++++++++++++++----------------- 14 files changed, 590 insertions(+), 122 deletions(-) create mode 100644 src/middleware.rs create mode 100644 src/session.rs create mode 100644 templates/home.rs.html create mode 100644 templates/layout.rs.html create mode 100644 templates/login.rs.html create mode 100644 templates/return_home.rs.html diff --git a/.gitignore b/.gitignore index ea8c4bf..488a291 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +/router-db-0-34-3 diff --git a/Cargo.lock b/Cargo.lock index 33def1c..e6baa36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,7 +32,7 @@ checksum = "f7001367fde4c768a19d1029f0a8be5abd9308e1119846d5bd9ad26297b8faf5" dependencies = [ "aes-soft", "aesni", - "block-cipher", + "block-cipher 0.7.1", ] [[package]] @@ -43,7 +43,7 @@ checksum = "86f5007801316299f922a6198d1d09a0bae95786815d066d5880d13f7c45ead1" dependencies = [ "aead", "aes", - "block-cipher", + "block-cipher 0.7.1", "ghash", "subtle", ] @@ -54,7 +54,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4925647ee64e5056cf231608957ce7c81e12d6d6e316b9ce1404778cc1d35fa7" dependencies = [ - "block-cipher", + "block-cipher 0.7.1", "byteorder", "opaque-debug 0.2.3", ] @@ -65,7 +65,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d050d39b0b7688b3a3254394c3e30a9d66c41dcf9b05b0e2dbdc623f6505d264" dependencies = [ - "block-cipher", + "block-cipher 0.7.1", "opaque-debug 0.2.3", ] @@ -327,6 +327,18 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" +[[package]] +name = "bcrypt" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2cab630912253fb9dc92c0e2fabd0a7b51f5a5a4007177cfa31e517015b7204" +dependencies = [ + "base64", + "blowfish", + "byteorder", + "getrandom", +] + [[package]] name = "bincode" version = "1.3.1" @@ -376,6 +388,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-cipher" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f337a3e6da609650eb74e02bc9fac7b735049f7623ab12f2e4c719316fcc7e80" +dependencies = [ + "generic-array", +] + [[package]] name = "blocking" version = "0.5.2" @@ -403,6 +424,17 @@ dependencies = [ "waker-fn", ] +[[package]] +name = "blowfish" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f06850ba969bc59388b2cc0a4f186fc6d9d37208863b15b84ae3866ac90ac06" +dependencies = [ + "block-cipher 0.8.0", + "byteorder", + "opaque-debug 0.3.0", +] + [[package]] name = "bumpalo" version = "3.4.0" @@ -1360,11 +1392,15 @@ version = "0.1.0" dependencies = [ "anyhow", "async-process", + "async-trait", + "base64", + "bcrypt", "blocking 1.0.0", "config", "futures-lite 1.3.0", "mime", "once_cell", + "rand", "regex", "ructe", "serde 1.0.115", diff --git a/Cargo.toml b/Cargo.toml index 3078218..df51090 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,11 +10,15 @@ build = "src/build.rs" [dependencies] anyhow = "1.0" async-process = "1.0.0" +async-trait = "0.1.40" +base64 = "0.12.3" +bcrypt = "0.8.2" blocking = "1.0.0" config = { version = "0.10.1", features = ["toml"] } futures-lite = "1.1.0" mime = "0.3" once_cell = "1.4.1" +rand = "0.7.3" regex = "1.3.9" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/config.toml b/config.toml index 4a51d76..62c0282 100644 --- a/config.toml +++ b/config.toml @@ -6,3 +6,7 @@ internal = [ [network] shared-internal = true + +[server] +debug = true +secret = "8tr5C1kCmWpce9Exk2he+g0C72juZdxvxF+xKrRyeBFhVzYbJmxIxcE0ND4nP3il" diff --git a/scss/index.scss b/scss/index.scss index 8a671a3..49f5d17 100644 --- a/scss/index.scss +++ b/scss/index.scss @@ -1,3 +1,7 @@ +* { + box-sizing: border-box; +} + body { margin: 0; background-color: #f5f5f5; @@ -27,6 +31,13 @@ section { border-radius: 2px; } +.content { + background-color: #fff; + border: 1px solid #ddd; + border-radius: 2px; + padding: 16px; +} + table { border-collapse: collapse; width: 100%; @@ -81,6 +92,97 @@ form { } } +a { + text-decoration: none; + font-weight: 500; + + &, &:focus, &:hover, &:visited { + color: #dd08dd; + } + + &:hover { + color: #dd08dd; + text-decoration: underline; + } +} + +.select-wrapper { + position: relative; + width: 150px; + border-radius: 2px; + + &:before { + position: absolute; + display: block; + content: '▾'; + top: 7px; + right: 10px; + } + select { + position: relative; + width: 100%; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background: transparent; + border: 1px solid #ddd; + border-radius: 2px; + padding: 8px 24px 8px 8px; + font-weight: 600; + &:hover { + border: 1px solid #eae; + } + &:focus { + border: 1px solid #eae; + } + } + &:hover { + background-color: #fff3fc; + } + &:focus { + background-color: #fff3fc; + box-shadow: 0 1px 3px #9d0f9d1f; + } +} + +button[type="submit"] { + padding: 8px 24px; + background: #992d99; + color: #fff; + border: none; + border-radius: 2px; + font-size: 14px; + + &:hover { + background: #b034b0; + } +} + +input[type="text"], +input[type="number"], +input[type="password"] { + width: 150px; + background: #fff; + border: 1px solid #ddd; + border-radius: 2px; + padding: 8px; + box-shadow: inset 0 0 3px #9d0f9d1f; + &:focus { + border: 1px solid #eae; + box-shadow: inset 0 0 3px #9d0f9d1f, 0 1px 3px #9d0f9d1f; + } +} + +p { + margin: 0; +} + +ul { + list-style: none; + padding: 0; + margin: 0; +} + @media(max-width: 650px) { body { padding: 8px 0 48px; @@ -100,9 +202,13 @@ form { .table-wrapper { overflow-x: auto; + border-left: none; + border-right: none; } - table, form { + .content, .table-wrapper, form { border-radius: 0; + border-left: none; + border-right: none; } } diff --git a/src/main.rs b/src/main.rs index b0898c7..ea3a7ec 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,19 +2,36 @@ use blocking::unblock; use futures_lite::*; use once_cell::sync::Lazy; use sled::Db; -use tide::log::{self, LogMiddleware}; +use std::time::Duration; +use tide::{ + log::{self, LogMiddleware}, + sessions::{MemoryStore, Session, SessionMiddleware}, +}; include!(concat!(env!("OUT_DIR"), "/templates.rs")); mod iptables; +mod middleware; mod rules; +mod session; mod startup; -use self::{rules::Rule, startup::Interfaces, templates::statics::StaticFile}; +use self::{ + middleware::AuthMiddleware, + rules::Rule, + startup::{Config, Interfaces, ServerState}, + templates::statics::StaticFile, +}; + +static CONFIG: Lazy = Lazy::new(|| Config::read().unwrap()); + +static SERVER: Lazy = Lazy::new(|| ServerState::init(&CONFIG).unwrap()); static INTERFACES: Lazy = Lazy::new(|| { - let interfaces = Interfaces::init_blocking().unwrap(); - interfaces.reset_blocking().unwrap(); + let interfaces = Interfaces::init_blocking(&CONFIG).unwrap(); + if !SERVER.debug { + interfaces.reset_blocking().unwrap(); + } interfaces }); @@ -46,7 +63,9 @@ async fn save_rule(mut req: tide::Request<()>) -> tide::Result { rules::save(&DB, &rule).await?; - rules::apply(&INTERFACES, rule).await?; + if !SERVER.debug { + rules::apply(&INTERFACES, rule).await?; + } Ok(to_rules_page()) } @@ -54,17 +73,101 @@ async fn save_rule(mut req: tide::Request<()>) -> tide::Result { async fn delete_rule(req: tide::Request<()>) -> tide::Result { let id = req.param("id")?; let rule = rules::delete(&DB, id).await?; - rules::unset(&INTERFACES, rule).await?; + + if !SERVER.debug { + rules::unset(&INTERFACES, rule).await?; + } Ok(to_rules_page()) } -fn to_rules_page() -> tide::Response { - tide::Response::builder(301) - .header("Location", "/rules") +#[derive(serde::Deserialize)] +struct LoginForm { + username: String, + password: String, +} + +async fn login(mut req: tide::Request<()>) -> tide::Result { + tide::log::debug!("LOGIN"); + let body_string = req.body_string().await?; + + let login_form: LoginForm = QS.deserialize_str(&body_string)?; + + session::login(&DB, login_form.username, login_form.password)?; + + let session = req.session_mut(); + + session.insert("logged_in", true)?; + + if let Some(path) = session.get("return_to") { + let path: String = path; // tell the compiler what type i want it to be + session.remove("return_to"); + Ok(to_custom(&path)) + } else { + Ok(to_home()) + } +} + +async fn login_page(req: tide::Request<()>) -> tide::Result { + tide::log::debug!("LOGIN PAGE"); + let session = req.session(); + let logged_in: bool = session.get("logged_in").unwrap_or(false); + + if logged_in { + tide::log::debug!("TO HOME"); + return Ok(to_home()); + } + + let mut html = Vec::new(); + + templates::login(&mut html)?; + + Ok(tide::Response::builder(200) + .body(html) + .content_type( + "text/html;charset=utf-8" + .parse::() + .unwrap(), + ) + .build()) +} + +async fn home_page(_: tide::Request<()>) -> tide::Result { + let mut html = Vec::new(); + + templates::home_html(&mut html, INTERFACES.external.ip.to_string())?; + + Ok(tide::Response::builder(200) + .body(html) + .content_type( + "text/html;charset=utf-8" + .parse::() + .unwrap(), + ) + .build()) +} + +fn to_custom(location: &str) -> tide::Response { + tide::Response::builder(302) + .header("Location", location) + .header("Cache-Control", "no-store") .build() } +fn to_home() -> tide::Response { + to_custom("/") +} + +fn to_login(session: &mut Session, from: &str) -> tide::Result { + session.insert("return_to", from)?; + + Ok(to_custom("/login")) +} + +fn to_rules_page() -> tide::Response { + to_custom("/rules") +} + async fn statics(req: tide::Request<()>) -> tide::Result { let file: String = req.param("file")?; @@ -84,21 +187,42 @@ fn main() -> Result<(), anyhow::Error> { log::with_level(log::LevelFilter::Debug); - rules::apply_all(&DB, &INTERFACES).await?; + session::create_admin(&DB).await?; + if !SERVER.debug { + rules::apply_all(&DB, &INTERFACES).await?; + } let mut app = tide::new(); + app.with( + SessionMiddleware::new(MemoryStore::new(), &SERVER.secret) + .with_cookie_name("router.cookie") + .with_session_ttl(Some(Duration::from_secs(60 * 15))), + ); app.with(LogMiddleware::new()); app.at("/static/:file").get(statics); - app.at("/rules").get(rules_page).post(save_rule); - app.at("/rules/:id").get(delete_rule); + app.at("/login").get(login_page).post(login); + app.at("/rules") + .with(AuthMiddleware) + .get(rules_page) + .post(save_rule) + .nest({ + let mut app = tide::new(); + app.at("/:id").get(delete_rule); + app + }); + app.at("/").get(home_page); - let listeners: Vec = INTERFACES - .internal - .iter() - .map(|info| format!("{}:8080", info.ip)) - .collect(); + if SERVER.debug { + app.listen("127.0.0.1:8080").await?; + } else { + let listeners: Vec = INTERFACES + .internal + .iter() + .map(|info| format!("{}:80", info.ip)) + .collect(); - app.listen(listeners).await?; + app.listen(listeners).await?; + } Ok(()) as Result<(), anyhow::Error> })?; diff --git a/src/middleware.rs b/src/middleware.rs new file mode 100644 index 0000000..19ed152 --- /dev/null +++ b/src/middleware.rs @@ -0,0 +1,22 @@ +use crate::to_login; +use tide::{Middleware, Next, Request}; + +pub(crate) struct AuthMiddleware; + +#[async_trait::async_trait] +impl Middleware for AuthMiddleware +where + State: Clone + Send + Sync + 'static, +{ + async fn handle(&self, mut request: Request, next: Next<'_, State>) -> tide::Result { + let path = request.url().path().to_string(); + let session = request.session_mut(); + let logged_in: bool = session.get("logged_in").unwrap_or(false); + + if logged_in { + return Ok(next.run(request).await); + } + + to_login(session, &path) + } +} diff --git a/src/session.rs b/src/session.rs new file mode 100644 index 0000000..9035abd --- /dev/null +++ b/src/session.rs @@ -0,0 +1,57 @@ +use once_cell::sync::OnceCell; +use sled::{Db, Tree}; + +static USER_TREE: OnceCell = OnceCell::new(); + +fn user_tree(db: &Db) -> &'static Tree { + USER_TREE.get_or_init(|| db.open_tree("users").unwrap()) +} + +pub(crate) fn login(db: &Db, username: String, password: String) -> Result<(), anyhow::Error> { + let tree = user_tree(db); + + let hash = tree + .get(username.as_bytes())? + .ok_or_else(|| anyhow::anyhow!("Missing user {}", username))?; + + let password_hash = String::from_utf8_lossy(&hash); + + if bcrypt::verify(password, &password_hash)? { + return Ok(()); + } + + Err(anyhow::anyhow!("Invalid password")) +} + +async fn add_user(db: &Db, username: String, password: String) -> Result<(), anyhow::Error> { + let tree = user_tree(db); + + let hash = bcrypt::hash(&password, bcrypt::DEFAULT_COST)?; + + if tree + .compare_and_swap(username, None as Option<&[u8]>, Some(hash.as_bytes()))? + .is_err() + { + return Err(anyhow::anyhow!("User already exists")); + } + + tree.flush_async().await?; + + Ok(()) +} + +pub(crate) async fn create_admin(db: &Db) -> Result<(), anyhow::Error> { + use rand::Rng; + + let password = rand::thread_rng() + .sample_iter(rand::distributions::Alphanumeric) + .take(16) + .collect::(); + if add_user(db, String::from("admin"), password.clone()) + .await + .is_ok() + { + println!("Admin password is '{}'", password); + } + Ok(()) +} diff --git a/src/startup/mod.rs b/src/startup/mod.rs index fb908af..4d8e59f 100644 --- a/src/startup/mod.rs +++ b/src/startup/mod.rs @@ -9,9 +9,10 @@ use std::{ mod preload; #[derive(serde::Deserialize)] -struct Config { +pub(crate) struct Config { interface: InterfaceConfig, network: NetworkConfig, + server: ServerConfig, } #[derive(serde::Deserialize)] @@ -26,6 +27,12 @@ struct NetworkConfig { shared_internal: bool, } +#[derive(serde::Deserialize)] +struct ServerConfig { + debug: bool, + secret: String, +} + pub(crate) struct Interfaces { pub(crate) external: InterfaceInfo, pub(crate) internal: Vec, @@ -38,8 +45,24 @@ pub(crate) struct InterfaceInfo { pub(crate) mask: u8, } +pub(crate) struct ServerState { + pub(crate) debug: bool, + pub(crate) secret: Vec, +} + +impl ServerState { + pub(crate) fn init(config: &Config) -> Result { + let secret = base64::decode(&config.server.secret)?; + + Ok(ServerState { + debug: config.server.debug, + secret, + }) + } +} + impl Config { - fn read() -> Result { + pub(crate) fn read() -> Result { let mut config = config::Config::new(); config.merge(config::File::with_name("config.toml"))?; @@ -48,8 +71,23 @@ impl Config { } impl Interfaces { - pub(crate) fn init_blocking() -> Result { - let config = Config::read()?; + pub(crate) fn init_blocking(config: &Config) -> Result { + if config.server.debug { + // Fake Data from Fake Things for Faking :tm: + return Ok(Interfaces { + external: InterfaceInfo { + interface: String::from("eth0"), + ip: "123.123.123.123".parse()?, + mask: 20, + }, + internal: vec![InterfaceInfo { + interface: String::from("enp1s0"), + ip: "192.168.6.1".parse()?, + mask: 24, + }], + shared_internal: false, + }); + } let output = Command::new("ip").arg("addr").output()?; let output = String::from_utf8_lossy(&output.stdout); diff --git a/templates/home.rs.html b/templates/home.rs.html new file mode 100644 index 0000000..ae30b60 --- /dev/null +++ b/templates/home.rs.html @@ -0,0 +1,24 @@ +@use crate::templates::layout_html; + +@(ip: String) + +@:layout_html("Router", { +
+

Home

+
+
+

Info

+
+

IP: @ip

+
+
+
+

Admin

+ +
+}) + diff --git a/templates/layout.rs.html b/templates/layout.rs.html new file mode 100644 index 0000000..2a840e7 --- /dev/null +++ b/templates/layout.rs.html @@ -0,0 +1,17 @@ +@use crate::templates::statics::index_css; + +@(title: &str, content: Content) + + + + + + + + @title + + + + @:content() + + diff --git a/templates/login.rs.html b/templates/login.rs.html new file mode 100644 index 0000000..39c4d0f --- /dev/null +++ b/templates/login.rs.html @@ -0,0 +1,27 @@ +@use crate::templates::{layout_html, return_home_html}; + +@() + +@:layout_html("Login", { +
+

Login

+
+
+
+
+ + +
+
+ +
+
+
+ @:return_home_html() +}) diff --git a/templates/return_home.rs.html b/templates/return_home.rs.html new file mode 100644 index 0000000..e1add4c --- /dev/null +++ b/templates/return_home.rs.html @@ -0,0 +1,7 @@ +@() + +
+ +
diff --git a/templates/rules.rs.html b/templates/rules.rs.html index 1e4e156..1907126 100644 --- a/templates/rules.rs.html +++ b/templates/rules.rs.html @@ -1,102 +1,103 @@ @use crate::{ rules::Rule, - templates::statics::index_css, + templates::{layout_html, return_home_html}, }; @(rules: &[(String, Rule)]) - - - - - - - Rules - - - -
-

Rules

-
-
-
- - - - - - - - - - - @for (id, rule) in rules { - - @if let Some((dest_ip, dest_port)) = rule.as_forward() { - - - - - - } else { - - - - - - } - - - } - -
KindProtocolPortDestination IPDestination Port
Forward@rule.proto@rule.port@dest_ip@dest_portAccept@rule.proto@rule.portDelete
+@:layout_html("Rules", { +
+

Rules

+
+
+
+ + + + + + + + + + + @for (id, rule) in rules { + + @if let Some((dest_ip, dest_port)) = rule.as_forward() { + + + + + + } else { + + + + + + } + + + } + +
KindProtocolPortDestination IPDestination Port
Forward@rule.proto@rule.port@dest_ip@dest_portAccept@rule.proto@rule.portDelete
+
+
+
+

Forward Port

+
+
+ + + + +
-
-
-

Forward Port

- -
- - - - - -
-
- -
- -
-
-

Accept Port

-
-
- - - -
-
- -
-
-
- - +
+ +
+ +
+
+

Accept Port

+
+
+ + + +
+
+ +
+
+
+ @:return_home_html() +})