Authentication, more styles
This commit is contained in:
parent
ab42817c2b
commit
8d3b3eb23c
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1 +1,2 @@
|
|||
/target
|
||||
/router-db-0-34-3
|
||||
|
|
44
Cargo.lock
generated
44
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -6,3 +6,7 @@ internal = [
|
|||
|
||||
[network]
|
||||
shared-internal = true
|
||||
|
||||
[server]
|
||||
debug = true
|
||||
secret = "8tr5C1kCmWpce9Exk2he+g0C72juZdxvxF+xKrRyeBFhVzYbJmxIxcE0ND4nP3il"
|
||||
|
|
108
scss/index.scss
108
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;
|
||||
}
|
||||
}
|
||||
|
|
160
src/main.rs
160
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<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
22
src/middleware.rs
Normal 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
57
src/session.rs
Normal 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(())
|
||||
}
|
|
@ -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
24
templates/home.rs.html
Normal 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
17
templates/layout.rs.html
Normal 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
27
templates/login.rs.html
Normal 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()
|
||||
})
|
7
templates/return_home.rs.html
Normal file
7
templates/return_home.rs.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
@()
|
||||
|
||||
<section>
|
||||
<div class="content">
|
||||
<a href="/">Return Home</a>
|
||||
</div>
|
||||
</section>
|
|
@ -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()
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue