Authentication, more styles

This commit is contained in:
asonix 2020-09-12 17:12:48 -05:00
parent ab42817c2b
commit 8d3b3eb23c
14 changed files with 590 additions and 122 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
/target
/router-db-0-34-3

44
Cargo.lock generated
View file

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

View file

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

View file

@ -6,3 +6,7 @@ internal = [
[network]
shared-internal = true
[server]
debug = true
secret = "8tr5C1kCmWpce9Exk2he+g0C72juZdxvxF+xKrRyeBFhVzYbJmxIxcE0ND4nP3il"

View file

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

View file

@ -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<Config> = Lazy::new(|| Config::read().unwrap());
static SERVER: Lazy<ServerState> = Lazy::new(|| ServerState::init(&CONFIG).unwrap());
static INTERFACES: Lazy<Interfaces> = 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::<tide::http::Mime>()
.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::<tide::http::Mime>()
.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<String> = 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<String> = INTERFACES
.internal
.iter()
.map(|info| format!("{}:80", info.ip))
.collect();
app.listen(listeners).await?;
app.listen(listeners).await?;
}
Ok(()) as Result<(), anyhow::Error>
})?;

22
src/middleware.rs Normal file
View file

@ -0,0 +1,22 @@
use crate::to_login;
use tide::{Middleware, Next, Request};
pub(crate) struct AuthMiddleware;
#[async_trait::async_trait]
impl<State> Middleware<State> for AuthMiddleware
where
State: Clone + Send + Sync + 'static,
{
async fn handle(&self, mut request: Request<State>, 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)
}
}

57
src/session.rs Normal file
View file

@ -0,0 +1,57 @@
use once_cell::sync::OnceCell;
use sled::{Db, Tree};
static USER_TREE: OnceCell<Tree> = 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::<String>();
if add_user(db, String::from("admin"), password.clone())
.await
.is_ok()
{
println!("Admin password is '{}'", password);
}
Ok(())
}

View file

@ -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<InterfaceInfo>,
@ -38,8 +45,24 @@ pub(crate) struct InterfaceInfo {
pub(crate) mask: u8,
}
pub(crate) struct ServerState {
pub(crate) debug: bool,
pub(crate) secret: Vec<u8>,
}
impl ServerState {
pub(crate) fn init(config: &Config) -> Result<Self, anyhow::Error> {
let secret = base64::decode(&config.server.secret)?;
Ok(ServerState {
debug: config.server.debug,
secret,
})
}
}
impl Config {
fn read() -> Result<Self, anyhow::Error> {
pub(crate) fn read() -> Result<Self, anyhow::Error> {
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<Self, anyhow::Error> {
let config = Config::read()?;
pub(crate) fn init_blocking(config: &Config) -> Result<Self, anyhow::Error> {
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);

24
templates/home.rs.html Normal file
View file

@ -0,0 +1,24 @@
@use crate::templates::layout_html;
@(ip: String)
@:layout_html("Router", {
<section>
<h1>Home</h1>
</section>
<section>
<h4>Info</h4>
<div class="content">
<p>IP: @ip</p>
</div>
</section>
<section>
<h4>Admin</h4>
<nav class="content">
<ul>
<li><a href="/rules">Add traffic rules</a></li>
</ul>
</nav>
</section>
})

17
templates/layout.rs.html Normal file
View file

@ -0,0 +1,17 @@
@use crate::templates::statics::index_css;
@(title: &str, content: Content)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>@title</title>
<link rel="stylesheet" href="/static/@index_css.name" type="text/css" />
</head>
<body>
@:content()
</body>
</html>

27
templates/login.rs.html Normal file
View file

@ -0,0 +1,27 @@
@use crate::templates::{layout_html, return_home_html};
@()
@:layout_html("Login", {
<section>
<h1>Login</h1>
</section>
<section>
<form method="POST" action="/login">
<div class="form-body">
<label for="username">
<h4>Username</h4>
<input name="username" type="text" />
</label>
<label for="password">
<h4>Password</h4>
<input name="password" type="password" />
</label>
</div>
<div class="submit">
<button type="submit">Login!</button>
</div>
</form>
</section>
@:return_home_html()
})

View file

@ -0,0 +1,7 @@
@()
<section>
<div class="content">
<a href="/">Return Home</a>
</div>
</section>

View file

@ -1,102 +1,103 @@
@use crate::{
rules::Rule,
templates::statics::index_css,
templates::{layout_html, return_home_html},
};
@(rules: &[(String, Rule)])
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Rules</title>
<link rel="stylesheet" href="/static/@index_css.name" type="text/css" />
</head>
<body>
<section>
<h1>Rules</h1>
</section>
<section>
<div class="table-wrapper">
<table>
<thead>
<th>Kind</th>
<th>Protocol</th>
<th class="port">Port</th>
<th>Destination IP</th>
<th class="port">Destination Port</th>
<th></th>
</thead>
</tbody>
@for (id, rule) in rules {
<tr>
@if let Some((dest_ip, dest_port)) = rule.as_forward() {
<td>Forward</td>
<td>@rule.proto</td>
<td class="port">@rule.port</td>
<td>@dest_ip</td>
<td class="port">@dest_port</td>
} else {
<td>Accept</td>
<td>@rule.proto</td>
<td class="port">@rule.port</td>
<td></td>
<td></td>
}
<td class="delete"><a href="/rules/@id">Delete</a></td>
</tr>
}
</tbody>
</table>
@:layout_html("Rules", {
<section>
<h1>Rules</h1>
</section>
<section>
<div class="table-wrapper">
<table>
<thead>
<th>Kind</th>
<th>Protocol</th>
<th class="port">Port</th>
<th>Destination IP</th>
<th class="port">Destination Port</th>
<th></th>
</thead>
</tbody>
@for (id, rule) in rules {
<tr>
@if let Some((dest_ip, dest_port)) = rule.as_forward() {
<td>Forward</td>
<td>@rule.proto</td>
<td class="port">@rule.port</td>
<td>@dest_ip</td>
<td class="port">@dest_port</td>
} else {
<td>Accept</td>
<td>@rule.proto</td>
<td class="port">@rule.port</td>
<td></td>
<td></td>
}
<td class="delete"><a href="/rules/@id">Delete</a></td>
</tr>
}
</tbody>
</table>
</div>
</section>
<section>
<h4>Forward Port</h4>
<form method="POST" action="/rules">
<div class="form-body">
<label for="proto">
<h4>Protocol</h4>
<div class="select-wrapper">
<select name="proto">
<option value="Tcp">TCP</option>
<option value="Udp">UDP</option>
</select>
</div>
</label>
<label for="port">
<h4>External Port</h4>
<input name="port" type="number" />
</label>
<input name="kind[type]" value="Forward" type="hidden" />
<label for="kind[dest_port]">
<h4>Internal Port</h4>
<input name="kind[dest_port]" type="number" />
</label>
<label for="kind[dest_ip]">
<h4>IP Address</h4>
<input name="kind[dest_ip]" type="text" />
</label>
</div>
</section>
<section>
<h4>Forward Port</h4>
<form method="POST" action="/rules">
<div class="form-body">
<select name="proto">
<option value="Tcp">TCP</option>
<option value="Udp">UDP</option>
</select>
<label for="port">
<h4>External Port</h4>
<input name="port" type="number" />
</label>
<input name="kind[type]" value="Forward" type="hidden" />
<label for="kind[dest_port]">
<h4>Internal Port</h4>
<input name="kind[dest_port]" type="number" />
</label>
<label for="kind[dest_ip]">
<h4>IP Address</h4>
<input name="kind[dest_ip]" type="text" />
</label>
</div>
<div class="submit">
<button type="submit">Forward!</button>
</div>
</form>
</section>
<section>
<h4>Accept Port</h4>
<form method="POST" action="/rules">
<div class="form-body">
<select name="proto">
<option value="Tcp">TCP</option>
<option value="Udp">UDP</option>
</select>
<label for="port">
<h4>External Port</h4>
<input name="port" type="number" />
</label>
<input name="kind[type]" value="Accept" type="hidden" />
</div>
<div class="submit">
<button type="submit">Accept!</button>
</div>
</form>
</section>
</body>
</html>
<div class="submit">
<button type="submit">Forward!</button>
</div>
</form>
</section>
<section>
<h4>Accept Port</h4>
<form method="POST" action="/rules">
<div class="form-body">
<label for="proto">
<h4>Protocol</h4>
<div class="select-wrapper">
<select name="proto">
<option value="Tcp">TCP</option>
<option value="Udp">UDP</option>
</select>
</div>
</label>
<label for="port">
<h4>External Port</h4>
<input name="port" type="number" />
</label>
<input name="kind[type]" value="Accept" type="hidden" />
</div>
<div class="submit">
<button type="submit">Accept!</button>
</div>
</form>
</section>
@:return_home_html()
})