mirror of
https://github.com/LibreQoE/LibreQoS.git
synced 2025-02-25 18:55:32 -06:00
Adds an authentication system.
* The new Rust utility "webusers" manages /opt/libreqos/webusers.toml. * You can add/update/remove/list users from that tool. * The "allow anonymous" option in webusers.toml permits access for unauthenticated users, but won't let them change anything. This is for payne demonstrations. * All web APIs and pages should now be secured, requiring a login. * The login requires cookies. Signed-off-by: Herbert Wolverson <herberticus@gmail.com>
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -48,6 +48,7 @@ src/tinsStats.json
|
||||
src/linux_tc.txt
|
||||
src/lastRun.txt
|
||||
src/liblqos_python.so
|
||||
src/webusers.toml
|
||||
|
||||
# Ignore Rust build artifacts
|
||||
src/rust/target
|
||||
@@ -57,6 +58,7 @@ src/bin/lqtop
|
||||
src/bin/xdp_iphash_to_cpu_cmdline
|
||||
src/bin/xdp_pping
|
||||
src/bin/lqos_node_manager
|
||||
src/bin/webusers
|
||||
src/bin/Rocket.toml
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
# automatically.
|
||||
#
|
||||
# Don't forget to setup `/etc/lqos`
|
||||
PROGS="lqosd lqtop xdp_iphash_to_cpu_cmdline xdp_pping lqos_node_manager"
|
||||
PROGS="lqosd lqtop xdp_iphash_to_cpu_cmdline xdp_pping lqos_node_manager webusers"
|
||||
mkdir -p bin/static
|
||||
pushd rust
|
||||
#cargo clean
|
||||
|
||||
52
src/rust/Cargo.lock
generated
52
src/rust/Cargo.lock
generated
@@ -205,6 +205,15 @@ version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.3"
|
||||
@@ -377,7 +386,7 @@ dependencies = [
|
||||
"hmac",
|
||||
"percent-encoding",
|
||||
"rand",
|
||||
"sha2",
|
||||
"sha2 0.10.6",
|
||||
"subtle",
|
||||
"time",
|
||||
"version_check",
|
||||
@@ -598,13 +607,22 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"block-buffer 0.10.3",
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
]
|
||||
@@ -912,7 +930,7 @@ version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"digest 0.10.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1216,7 +1234,9 @@ dependencies = [
|
||||
"ip_network",
|
||||
"ip_network_table",
|
||||
"serde",
|
||||
"sha2 0.9.9",
|
||||
"toml",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2078,6 +2098,19 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.9.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
|
||||
dependencies = [
|
||||
"block-buffer 0.9.0",
|
||||
"cfg-if 1.0.0",
|
||||
"cpufeatures",
|
||||
"digest 0.9.0",
|
||||
"opaque-debug",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.6"
|
||||
@@ -2086,7 +2119,7 @@ checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
"digest 0.10.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2556,6 +2589,8 @@ version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"rand",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -2604,6 +2639,15 @@ version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "webusers"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap 4.0.29",
|
||||
"lqos_config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "3.1.1"
|
||||
|
||||
@@ -20,4 +20,5 @@ members = [
|
||||
"xdp_pping", # Rust port of cpumap's `xdp_pping` tool, for compatibility
|
||||
"lqos_node_manager", # A lightweight web interface for management and local monitoring
|
||||
"lqos_python", # Python bindings for using the Rust bus directly
|
||||
"webusers", # CLI control for managing the web user list
|
||||
]
|
||||
@@ -10,3 +10,5 @@ serde = { version = "1.0", features = [ "derive" ] }
|
||||
csv = "1"
|
||||
ip_network_table = "0"
|
||||
ip_network = "0"
|
||||
sha2 = "0"
|
||||
uuid = { version = "1", features = ["v4", "fast-rng" ] }
|
||||
|
||||
205
src/rust/lqos_config/src/authentication.rs
Normal file
205
src/rust/lqos_config/src/authentication.rs
Normal file
@@ -0,0 +1,205 @@
|
||||
use anyhow::{Error, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::{
|
||||
fs::{read_to_string, OpenOptions, remove_file},
|
||||
io::Write,
|
||||
path::{Path, PathBuf}, fmt::Display,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub enum UserRole {
|
||||
ReadOnly,
|
||||
Admin,
|
||||
}
|
||||
|
||||
impl From<&str> for UserRole {
|
||||
fn from(s: &str) -> Self {
|
||||
let s = s.to_lowercase();
|
||||
if s == "admin" {
|
||||
UserRole::Admin
|
||||
} else {
|
||||
UserRole::ReadOnly
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for UserRole {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
UserRole::Admin => write!(f, "admin"),
|
||||
UserRole::ReadOnly => write!(f, "read-only"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
struct WebUser {
|
||||
username: String,
|
||||
password_hash: String,
|
||||
role: UserRole,
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct WebUsers {
|
||||
allow_unauthenticated_to_view: bool,
|
||||
users: Vec<WebUser>,
|
||||
}
|
||||
|
||||
impl Default for WebUsers {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
users: Vec::new(),
|
||||
allow_unauthenticated_to_view: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WebUsers {
|
||||
fn path() -> Result<PathBuf> {
|
||||
let base_path = crate::EtcLqos::load()?.lqos_directory;
|
||||
let filename = Path::new(&base_path).join("webusers.toml");
|
||||
Ok(filename)
|
||||
}
|
||||
|
||||
fn save_to_disk(&self) -> Result<()> {
|
||||
let path = Self::path()?;
|
||||
let new_contents = toml::to_string(&self)?;
|
||||
if path.exists() {
|
||||
remove_file(&path)?;
|
||||
}
|
||||
let mut file = OpenOptions::new().write(true).create_new(true).open(path)?;
|
||||
file.write_all(&new_contents.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn does_users_file_exist() -> Result<bool> {
|
||||
Ok(Self::path()?.exists())
|
||||
}
|
||||
|
||||
pub fn load_or_create() -> Result<Self> {
|
||||
let path = Self::path()?;
|
||||
if !path.exists() {
|
||||
// Create a new users file, save it and return the
|
||||
// empty file
|
||||
let new_users = Self::default();
|
||||
new_users.save_to_disk()?;
|
||||
Ok(new_users)
|
||||
} else {
|
||||
// Load from disk
|
||||
let raw = read_to_string(path)?;
|
||||
let users = toml::from_str(&raw)?;
|
||||
Ok(users)
|
||||
}
|
||||
}
|
||||
|
||||
fn hash_password(password: &str) -> String {
|
||||
let salted = format!("!x{password}_LibreQosLikesPasswordsForDinner");
|
||||
let mut sha256 = Sha256::new();
|
||||
sha256.update(salted);
|
||||
format!("{:X}", sha256.finalize())
|
||||
}
|
||||
|
||||
pub fn add_or_update_user(
|
||||
&mut self,
|
||||
username: &str,
|
||||
password: &str,
|
||||
role: UserRole,
|
||||
) -> Result<String> {
|
||||
let token ; // Assigned in a branch
|
||||
if let Some(mut user) = self.users.iter_mut().find(|u| u.username == username) {
|
||||
user.password_hash = Self::hash_password(password);
|
||||
user.role = role;
|
||||
token = user.token.clone();
|
||||
} else {
|
||||
token = Uuid::new_v4().to_string();
|
||||
let new_user = WebUser {
|
||||
username: username.to_string(),
|
||||
password_hash: Self::hash_password(password),
|
||||
role,
|
||||
token: token.clone(),
|
||||
};
|
||||
self.users.push(new_user);
|
||||
}
|
||||
|
||||
self.save_to_disk()?;
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
pub fn remove_user(&mut self, username: &str) -> Result<()> {
|
||||
let old_len = self.users.len();
|
||||
self.users.retain(|u| u.username != username);
|
||||
if old_len == self.users.len() {
|
||||
return Err(Error::msg(format!("User {} was not found", username)));
|
||||
}
|
||||
self.save_to_disk()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn login(&self, username: &str, password: &str) -> Result<String> {
|
||||
let hash = Self::hash_password(password);
|
||||
if let Some(user) = self
|
||||
.users
|
||||
.iter()
|
||||
.find(|u| u.username == username && u.password_hash == hash)
|
||||
{
|
||||
Ok(user.token.clone())
|
||||
} else {
|
||||
if self.allow_unauthenticated_to_view {
|
||||
Ok("default".to_string())
|
||||
} else {
|
||||
Err(Error::msg("Invalid Login"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_role_from_token(&self, token: &str) -> Result<UserRole> {
|
||||
if let Some(user) = self
|
||||
.users
|
||||
.iter()
|
||||
.find(|u| u.token == token)
|
||||
{
|
||||
Ok(user.role)
|
||||
} else {
|
||||
if self.allow_unauthenticated_to_view {
|
||||
Ok(UserRole::ReadOnly)
|
||||
} else {
|
||||
Err(Error::msg("Unknown user token"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_username(&self, token: &str) -> String {
|
||||
if let Some(user) = self
|
||||
.users
|
||||
.iter()
|
||||
.find(|u| u.token == token)
|
||||
{
|
||||
user.username.clone()
|
||||
} else {
|
||||
"Anonymous".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_users(&self) -> Result<()> {
|
||||
self
|
||||
.users
|
||||
.iter()
|
||||
.for_each(|u| {
|
||||
println!("{:<40} {:<10}", u.username, u.role.to_string());
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn allow_anonymous(&mut self, allow: bool) -> Result<()> {
|
||||
self.allow_unauthenticated_to_view = allow;
|
||||
self.save_to_disk()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn do_we_allow_anonymous(&self) -> bool {
|
||||
self.allow_unauthenticated_to_view
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,10 @@ mod etc;
|
||||
mod libre_qos_config;
|
||||
mod shaped_devices;
|
||||
mod program_control;
|
||||
mod authentication;
|
||||
|
||||
pub use libre_qos_config::LibreQoSConfig;
|
||||
pub use shaped_devices::{ConfigShapedDevices, ShapedDevice};
|
||||
pub use program_control::load_libreqos;
|
||||
pub use etc::{EtcLqos, BridgeConfig, Tunables, BridgeInterface, BridgeVlan};
|
||||
pub use authentication::{WebUsers, UserRole};
|
||||
|
||||
128
src/rust/lqos_node_manager/src/auth_guard.rs
Normal file
128
src/rust/lqos_node_manager/src/auth_guard.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use anyhow::Error;
|
||||
use lazy_static::*;
|
||||
use lqos_config::{UserRole, WebUsers};
|
||||
use parking_lot::Mutex;
|
||||
use rocket::{
|
||||
http::{Status, CookieJar, Cookie},
|
||||
request::{FromRequest, Outcome},
|
||||
Request
|
||||
};
|
||||
use rocket::serde::{json::Json, Serialize, Deserialize};
|
||||
|
||||
lazy_static! {
|
||||
static ref WEB_USERS: Mutex<Option<WebUsers>> = Mutex::new(None);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AuthGuard {
|
||||
Admin,
|
||||
ReadOnly,
|
||||
FirstUse,
|
||||
}
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for AuthGuard {
|
||||
type Error = anyhow::Error; // Decorated because Error=Error looks odd
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let mut lock = WEB_USERS.lock();
|
||||
if lock.is_none() {
|
||||
if WebUsers::does_users_file_exist().unwrap() {
|
||||
*lock = Some(WebUsers::load_or_create().unwrap());
|
||||
} else {
|
||||
// There is no user list, so we're redirecting to the
|
||||
// new user page.
|
||||
return Outcome::Success(AuthGuard::FirstUse);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(users) = &*lock {
|
||||
if let Some(token) = request.cookies().get("User-Token") {
|
||||
match users.get_role_from_token(token.value()) {
|
||||
Ok(UserRole::Admin) => return Outcome::Success(AuthGuard::Admin),
|
||||
Ok(UserRole::ReadOnly) => return Outcome::Success(AuthGuard::ReadOnly),
|
||||
_ => {
|
||||
return Outcome::Failure((
|
||||
Status::Unauthorized,
|
||||
Error::msg("Invalid token"),
|
||||
))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If no login, do we allow anonymous?
|
||||
if users.do_we_allow_anonymous() {
|
||||
return Outcome::Success(AuthGuard::ReadOnly);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Outcome::Failure((Status::Unauthorized, Error::msg("Access Denied")))
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthGuard {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct FirstUser {
|
||||
pub allow_anonymous: bool,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[post("/api/create_first_user", data="<info>")]
|
||||
pub fn create_first_user(cookies: &CookieJar, info: Json<FirstUser>) -> Json<String> {
|
||||
if WebUsers::does_users_file_exist().unwrap() {
|
||||
return Json("ERROR".to_string());
|
||||
}
|
||||
let mut lock = WEB_USERS.lock();
|
||||
let mut users = WebUsers::load_or_create().unwrap();
|
||||
users.allow_anonymous(info.allow_anonymous).unwrap();
|
||||
let token = users.add_or_update_user(&info.username, &info.password, UserRole::Admin).unwrap();
|
||||
cookies.add(Cookie::new("User-Token", token));
|
||||
*lock = Some(users);
|
||||
Json("OK".to_string())
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct LoginAttempt {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[post("/api/login", data="<info>")]
|
||||
pub fn login(cookies: &CookieJar, info: Json<LoginAttempt>) -> Json<String> {
|
||||
let mut lock = WEB_USERS.lock();
|
||||
if lock.is_none() {
|
||||
if WebUsers::does_users_file_exist().unwrap() {
|
||||
*lock = Some(WebUsers::load_or_create().unwrap());
|
||||
}
|
||||
}
|
||||
if let Some(users) = &*lock {
|
||||
if let Ok(token) = users.login(&info.username, &info.password) {
|
||||
cookies.add(Cookie::new("User-Token", token));
|
||||
return Json("OK".to_string());
|
||||
}
|
||||
}
|
||||
Json("ERROR".to_string())
|
||||
}
|
||||
|
||||
#[get("/api/admin_check")]
|
||||
pub fn admin_check(auth: AuthGuard) -> Json<bool> {
|
||||
match auth {
|
||||
AuthGuard::Admin => Json(true),
|
||||
_ => Json(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/api/username")]
|
||||
pub fn username(auth: AuthGuard, cookies: &CookieJar) -> Json<String> {
|
||||
if let Some(token) = cookies.get("User-Token") {
|
||||
let lock = WEB_USERS.lock();
|
||||
if let Some(users) = &*lock {
|
||||
return Json(users.get_username(token.value()));
|
||||
}
|
||||
}
|
||||
Json("Anonymous".to_string())
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
use default_net::get_interfaces;
|
||||
use lqos_config::{LibreQoSConfig, EtcLqos};
|
||||
use rocket::{fs::NamedFile, serde::json::Json};
|
||||
use crate::cache_control::NoCache;
|
||||
use crate::{cache_control::NoCache, auth_guard::AuthGuard};
|
||||
|
||||
// Note that NoCache can be replaced with a cache option
|
||||
// once the design work is complete.
|
||||
#[get("/config")]
|
||||
pub async fn config_page<'a>() -> NoCache<Option<NamedFile>> {
|
||||
pub async fn config_page<'a>(_auth: AuthGuard) -> NoCache<Option<NamedFile>> {
|
||||
NoCache::new(NamedFile::open("static/config.html").await.ok())
|
||||
}
|
||||
|
||||
#[get("/api/list_nics")]
|
||||
pub async fn get_nic_list<'a>() -> NoCache<Json<Vec<(String, String, String)>>> {
|
||||
pub async fn get_nic_list<'a>(_auth: AuthGuard) -> NoCache<Json<Vec<(String, String, String)>>> {
|
||||
let mut result = Vec::new();
|
||||
for eth in get_interfaces().iter() {
|
||||
let mac = if let Some(mac) = ð.mac_addr {
|
||||
@@ -29,14 +29,14 @@ pub async fn get_nic_list<'a>() -> NoCache<Json<Vec<(String, String, String)>>>
|
||||
}
|
||||
|
||||
#[get("/api/python_config")]
|
||||
pub async fn get_current_python_config() -> NoCache<Json<LibreQoSConfig>> {
|
||||
pub async fn get_current_python_config(_auth: AuthGuard) -> NoCache<Json<LibreQoSConfig>> {
|
||||
let config = lqos_config::LibreQoSConfig::load().unwrap();
|
||||
println!("{:#?}", config);
|
||||
NoCache::new(Json(config))
|
||||
}
|
||||
|
||||
#[get("/api/lqosd_config")]
|
||||
pub async fn get_current_lqosd_config() -> NoCache<Json<EtcLqos>> {
|
||||
pub async fn get_current_lqosd_config(_auth: AuthGuard) -> NoCache<Json<EtcLqos>> {
|
||||
let config = lqos_config::EtcLqos::load().unwrap();
|
||||
println!("{:#?}", config);
|
||||
NoCache::new(Json(config))
|
||||
|
||||
@@ -8,6 +8,7 @@ mod cache_control;
|
||||
use rocket_async_compression::Compression;
|
||||
mod queue_info;
|
||||
mod config_control;
|
||||
mod auth_guard;
|
||||
|
||||
#[launch]
|
||||
fn rocket() -> _ {
|
||||
@@ -18,6 +19,7 @@ fn rocket() -> _ {
|
||||
rocket::tokio::spawn(tracker::update_tracking());
|
||||
})
|
||||
}))
|
||||
.register("/", catchers![static_pages::login])
|
||||
.mount("/", routes![
|
||||
static_pages::index,
|
||||
static_pages::shaped_devices_csv_page,
|
||||
@@ -54,6 +56,11 @@ fn rocket() -> _ {
|
||||
config_control::get_nic_list,
|
||||
config_control::get_current_python_config,
|
||||
config_control::get_current_lqosd_config,
|
||||
auth_guard::create_first_user,
|
||||
auth_guard::login,
|
||||
auth_guard::admin_check,
|
||||
static_pages::login_page,
|
||||
auth_guard::username,
|
||||
|
||||
// Supporting files
|
||||
static_pages::bootsrap_css,
|
||||
|
||||
@@ -3,12 +3,13 @@ use rocket::response::content::RawJson;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::tokio::io::{AsyncWriteExt, AsyncReadExt};
|
||||
use rocket::tokio::net::TcpStream;
|
||||
use crate::auth_guard::AuthGuard;
|
||||
use crate::cache_control::NoCache;
|
||||
use crate::tracker::SHAPED_DEVICES;
|
||||
use std::net::IpAddr;
|
||||
|
||||
#[get("/api/circuit_name/<circuit_id>")]
|
||||
pub async fn circuit_name(circuit_id: String) -> NoCache<Json<String>> {
|
||||
pub async fn circuit_name(circuit_id: String, _auth: AuthGuard) -> NoCache<Json<String>> {
|
||||
if let Some(device) = SHAPED_DEVICES.read().devices.iter().find(|d| d.circuit_id == circuit_id) {
|
||||
NoCache::new(Json(device.circuit_name.clone()))
|
||||
} else {
|
||||
@@ -18,7 +19,7 @@ pub async fn circuit_name(circuit_id: String) -> NoCache<Json<String>> {
|
||||
}
|
||||
|
||||
#[get("/api/circuit_throughput/<circuit_id>")]
|
||||
pub async fn current_circuit_throughput(circuit_id: String) -> NoCache<Json<Vec<(String, u64, u64)>>> {
|
||||
pub async fn current_circuit_throughput(circuit_id: String, _auth: AuthGuard) -> NoCache<Json<Vec<(String, u64, u64)>>> {
|
||||
let mut result = Vec::new();
|
||||
// Get a list of host counts
|
||||
// This is really inefficient, but I'm struggling to find a better way.
|
||||
@@ -65,7 +66,7 @@ pub async fn current_circuit_throughput(circuit_id: String) -> NoCache<Json<Vec<
|
||||
}
|
||||
|
||||
#[get("/api/raw_queue_by_circuit/<circuit_id>")]
|
||||
pub async fn raw_queue_by_circuit(circuit_id: String) -> NoCache<RawJson<String>> {
|
||||
pub async fn raw_queue_by_circuit(circuit_id: String, _auth: AuthGuard) -> NoCache<RawJson<String>> {
|
||||
let mut stream = TcpStream::connect(BUS_BIND_ADDRESS).await.unwrap();
|
||||
let test = BusSession {
|
||||
auth_cookie: 1234,
|
||||
|
||||
@@ -3,6 +3,7 @@ use lqos_config::ShapedDevice;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::tokio::io::{AsyncWriteExt, AsyncReadExt};
|
||||
use rocket::tokio::net::TcpStream;
|
||||
use crate::auth_guard::AuthGuard;
|
||||
use crate::cache_control::NoCache;
|
||||
use crate::tracker::SHAPED_DEVICES;
|
||||
use lazy_static::*;
|
||||
@@ -13,24 +14,24 @@ lazy_static! {
|
||||
}
|
||||
|
||||
#[get("/api/all_shaped_devices")]
|
||||
pub fn all_shaped_devices() -> NoCache<Json<Vec<ShapedDevice>>> {
|
||||
pub fn all_shaped_devices(_auth: AuthGuard) -> NoCache<Json<Vec<ShapedDevice>>> {
|
||||
NoCache::new(Json(SHAPED_DEVICES.read().devices.clone()))
|
||||
}
|
||||
|
||||
#[get("/api/shaped_devices_count")]
|
||||
pub fn shaped_devices_count() -> NoCache<Json<usize>> {
|
||||
pub fn shaped_devices_count(_auth: AuthGuard) -> NoCache<Json<usize>> {
|
||||
NoCache::new(Json(SHAPED_DEVICES.read().devices.len()))
|
||||
}
|
||||
|
||||
#[get("/api/shaped_devices_range/<start>/<end>")]
|
||||
pub fn shaped_devices_range(start: usize, end: usize) -> NoCache<Json<Vec<ShapedDevice>>> {
|
||||
pub fn shaped_devices_range(start: usize, end: usize, _auth: AuthGuard) -> NoCache<Json<Vec<ShapedDevice>>> {
|
||||
let reader = SHAPED_DEVICES.read();
|
||||
let result: Vec<ShapedDevice> = reader.devices.iter().skip(start).take(end).cloned().collect();
|
||||
NoCache::new(Json(result))
|
||||
}
|
||||
|
||||
#[get("/api/shaped_devices_search/<term>")]
|
||||
pub fn shaped_devices_search(term: String) -> NoCache<Json<Vec<ShapedDevice>>> {
|
||||
pub fn shaped_devices_search(term: String, _auth: AuthGuard) -> NoCache<Json<Vec<ShapedDevice>>> {
|
||||
let term = term.trim().to_lowercase();
|
||||
let reader = SHAPED_DEVICES.read();
|
||||
let result: Vec<ShapedDevice> = reader
|
||||
@@ -51,7 +52,10 @@ pub fn reload_required() -> NoCache<Json<bool>> {
|
||||
}
|
||||
|
||||
#[get("/api/reload_libreqos")]
|
||||
pub async fn reload_libreqos() -> NoCache<Json<String>> {
|
||||
pub async fn reload_libreqos(auth: AuthGuard) -> NoCache<Json<String>> {
|
||||
if auth != AuthGuard::Admin {
|
||||
return NoCache::new(Json("Not authorized".to_string()));
|
||||
}
|
||||
// Send request to lqosd
|
||||
let mut stream = TcpStream::connect(BUS_BIND_ADDRESS).await.unwrap();
|
||||
let test = BusSession {
|
||||
|
||||
@@ -1,38 +1,55 @@
|
||||
use rocket::fs::NamedFile;
|
||||
use crate::cache_control::{LongCache, NoCache};
|
||||
use crate::{cache_control::{LongCache, NoCache}, auth_guard::AuthGuard};
|
||||
|
||||
// Note that NoCache can be replaced with a cache option
|
||||
// once the design work is complete.
|
||||
#[get("/")]
|
||||
pub async fn index<'a>() -> NoCache<Option<NamedFile>> {
|
||||
NoCache::new(NamedFile::open("static/main.html").await.ok())
|
||||
pub async fn index<'a>(auth: AuthGuard) -> NoCache<Option<NamedFile>> {
|
||||
match auth {
|
||||
AuthGuard::FirstUse => NoCache::new(NamedFile::open("static/first_run.html").await.ok()),
|
||||
_ => NoCache::new(NamedFile::open("static/main.html").await.ok())
|
||||
}
|
||||
}
|
||||
|
||||
// Note that NoCache can be replaced with a cache option
|
||||
// once the design work is complete.
|
||||
#[catch(401)]
|
||||
pub async fn login<'a>() -> NoCache<Option<NamedFile>> {
|
||||
NoCache::new(NamedFile::open("static/login.html").await.ok())
|
||||
}
|
||||
|
||||
// Note that NoCache can be replaced with a cache option
|
||||
// once the design work is complete.
|
||||
#[get("/login")]
|
||||
pub async fn login_page<'a>() -> NoCache<Option<NamedFile>> {
|
||||
NoCache::new(NamedFile::open("static/login.html").await.ok())
|
||||
}
|
||||
|
||||
// Note that NoCache can be replaced with a cache option
|
||||
// once the design work is complete.
|
||||
#[get("/shaped")]
|
||||
pub async fn shaped_devices_csv_page<'a>() -> NoCache<Option<NamedFile>> {
|
||||
pub async fn shaped_devices_csv_page<'a>(_auth: AuthGuard) -> NoCache<Option<NamedFile>> {
|
||||
NoCache::new(NamedFile::open("static/shaped.html").await.ok())
|
||||
}
|
||||
|
||||
// Note that NoCache can be replaced with a cache option
|
||||
// once the design work is complete.
|
||||
#[get("/circuit_queue")]
|
||||
pub async fn circuit_queue<'a>() -> NoCache<Option<NamedFile>> {
|
||||
pub async fn circuit_queue<'a>(_auth: AuthGuard) -> NoCache<Option<NamedFile>> {
|
||||
NoCache::new(NamedFile::open("static/circuit_queue.html").await.ok())
|
||||
}
|
||||
|
||||
// Note that NoCache can be replaced with a cache option
|
||||
// once the design work is complete.
|
||||
#[get("/unknown")]
|
||||
pub async fn unknown_devices_page<'a>() -> NoCache<Option<NamedFile>> {
|
||||
pub async fn unknown_devices_page<'a>(_auth: AuthGuard) -> NoCache<Option<NamedFile>> {
|
||||
NoCache::new(NamedFile::open("static/unknown-ip.html").await.ok())
|
||||
}
|
||||
|
||||
// Note that NoCache can be replaced with a cache option
|
||||
// once the design work is complete.
|
||||
#[get("/shaped-add")]
|
||||
pub async fn shaped_devices_add_page<'a>() -> NoCache<Option<NamedFile>> {
|
||||
pub async fn shaped_devices_add_page<'a>(_auth: AuthGuard) -> NoCache<Option<NamedFile>> {
|
||||
NoCache::new(NamedFile::open("static/shaped-add.html").await.ok())
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ pub use cache_manager::update_tracking;
|
||||
use std::net::IpAddr;
|
||||
use lqos_bus::{IpStats, TcHandle};
|
||||
use rocket::serde::{json::Json, Serialize, Deserialize};
|
||||
use crate::tracker::cache::ThroughputPerSecond;
|
||||
use crate::{tracker::cache::ThroughputPerSecond, auth_guard::AuthGuard};
|
||||
use self::cache::{CURRENT_THROUGHPUT, THROUGHPUT_BUFFER, CPU_USAGE, MEMORY_USAGE, TOP_10_DOWNLOADERS, WORST_10_RTT, RTT_HISTOGRAM, HOST_COUNTS};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
@@ -49,50 +49,50 @@ impl From<&IpStats> for IpStatsWithPlan {
|
||||
}
|
||||
|
||||
#[get("/api/current_throughput")]
|
||||
pub fn current_throughput() -> Json<ThroughputPerSecond> {
|
||||
pub fn current_throughput(_auth: AuthGuard) -> Json<ThroughputPerSecond> {
|
||||
let result = CURRENT_THROUGHPUT.read().clone();
|
||||
Json(result)
|
||||
}
|
||||
|
||||
#[get("/api/throughput_ring")]
|
||||
pub fn throughput_ring() -> Json<Vec<ThroughputPerSecond>> {
|
||||
pub fn throughput_ring(_auth: AuthGuard) -> Json<Vec<ThroughputPerSecond>> {
|
||||
let result = THROUGHPUT_BUFFER.read().get_result();
|
||||
Json(result)
|
||||
}
|
||||
|
||||
#[get("/api/cpu")]
|
||||
pub fn cpu_usage() -> Json<Vec<f32>> {
|
||||
pub fn cpu_usage(_auth: AuthGuard) -> Json<Vec<f32>> {
|
||||
let cpu_usage = CPU_USAGE.read().clone();
|
||||
|
||||
Json(cpu_usage)
|
||||
}
|
||||
|
||||
#[get("/api/ram")]
|
||||
pub fn ram_usage() -> Json<Vec<u64>> {
|
||||
pub fn ram_usage(_auth: AuthGuard) -> Json<Vec<u64>> {
|
||||
let ram_usage = MEMORY_USAGE.read().clone();
|
||||
Json(ram_usage)
|
||||
}
|
||||
|
||||
#[get("/api/top_10_downloaders")]
|
||||
pub fn top_10_downloaders() -> Json<Vec<IpStatsWithPlan>> {
|
||||
pub fn top_10_downloaders(_auth: AuthGuard) -> Json<Vec<IpStatsWithPlan>> {
|
||||
let tt : Vec<IpStatsWithPlan> = TOP_10_DOWNLOADERS.read().iter().map(|tt| tt.into()).collect();
|
||||
Json(tt)
|
||||
}
|
||||
|
||||
#[get("/api/worst_10_rtt")]
|
||||
pub fn worst_10_rtt() -> Json<Vec<IpStatsWithPlan>> {
|
||||
pub fn worst_10_rtt(_auth: AuthGuard) -> Json<Vec<IpStatsWithPlan>> {
|
||||
let tt : Vec<IpStatsWithPlan> = WORST_10_RTT.read().iter().map(|tt| tt.into()).collect();
|
||||
Json(tt)
|
||||
}
|
||||
|
||||
|
||||
#[get("/api/rtt_histogram")]
|
||||
pub fn rtt_histogram() -> Json<Vec<u32>> {
|
||||
pub fn rtt_histogram(_auth: AuthGuard) -> Json<Vec<u32>> {
|
||||
Json(RTT_HISTOGRAM.read().clone())
|
||||
}
|
||||
|
||||
#[get("/api/host_counts")]
|
||||
pub fn host_counts() -> Json<(u32, u32)> {
|
||||
pub fn host_counts(_auth: AuthGuard) -> Json<(u32, u32)> {
|
||||
let shaped_reader = SHAPED_DEVICES.read();
|
||||
let n_devices = shaped_reader.devices.len();
|
||||
let host_counts = HOST_COUNTS.read();
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
use lqos_bus::IpStats;
|
||||
use rocket::serde::json::Json;
|
||||
use crate::{cache_control::NoCache, tracker::UNKNOWN_DEVICES};
|
||||
use crate::{cache_control::NoCache, tracker::UNKNOWN_DEVICES, auth_guard::AuthGuard};
|
||||
|
||||
#[get("/api/all_unknown_devices")]
|
||||
pub fn all_unknown_devices() -> NoCache<Json<Vec<IpStats>>> {
|
||||
pub fn all_unknown_devices(_auth: AuthGuard) -> NoCache<Json<Vec<IpStats>>> {
|
||||
NoCache::new(Json(UNKNOWN_DEVICES.read().clone()))
|
||||
}
|
||||
|
||||
#[get("/api/unknown_devices_count")]
|
||||
pub fn unknown_devices_count() -> NoCache<Json<usize>> {
|
||||
pub fn unknown_devices_count(_auth: AuthGuard) -> NoCache<Json<usize>> {
|
||||
NoCache::new(Json(UNKNOWN_DEVICES.read().len()))
|
||||
}
|
||||
|
||||
#[get("/api/unknown_devices_range/<start>/<end>")]
|
||||
pub fn unknown_devices_range(start: usize, end: usize) -> NoCache<Json<Vec<IpStats>>> {
|
||||
pub fn unknown_devices_range(start: usize, end: usize, _auth: AuthGuard) -> NoCache<Json<Vec<IpStats>>> {
|
||||
let reader = UNKNOWN_DEVICES.read();
|
||||
let result: Vec<IpStats> = reader.iter().skip(start).take(end).cloned().collect();
|
||||
NoCache::new(Json(result))
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/"><i class="fa fa-home"></i> Dashboard</a>
|
||||
</li>
|
||||
<li class="nav-item" id="currentLogin"></li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#"><i class="fa fa-globe"></i> Network Layout</a>
|
||||
</li>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/"><i class="fa fa-home"></i> Dashboard</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item" id="currentLogin"></li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#"><i class="fa fa-globe"></i> Network Layout</a>
|
||||
</li>
|
||||
@@ -56,7 +56,7 @@
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="fa fa-users"></i> Configuration</h5>
|
||||
|
||||
<div class="col-sm-8 mx-auto" style="padding: 4px; margin-bottom: 4px;">
|
||||
<div class="col-sm-8 mx-auto" style="padding: 4px; margin-bottom: 4px;" id="controls">
|
||||
<a href="#" class="btn btn-primary"><i class="fa fa-save"></i> Save ispConfig.py</a>
|
||||
<a href="#" class="btn btn-danger"><i class="fa fa-save"></i> Save /etc/lqos</a>
|
||||
<a href="#" class="btn btn-primary"><i class="fa fa-refresh"></i> Reload LibreQoS</a>
|
||||
@@ -71,6 +71,7 @@
|
||||
<button class="nav-link" id="v-pills-tuning-tab" data-bs-toggle="pill" data-bs-target="#v-pills-tuning" type="button" role="tab" aria-controls="v-pills-settings" aria-selected="false"><i class="fa fa-warning"></i> Tuning</button>
|
||||
<button class="nav-link" id="v-pills-spylnx-tab" data-bs-toggle="pill" data-bs-target="#v-pills-spylnx" type="button" role="tab" aria-controls="v-pills-settings" aria-selected="false"><i class="fa fa-eye"></i> Spylnx</button>
|
||||
<button class="nav-link" id="v-pills-uisp-tab" data-bs-toggle="pill" data-bs-target="#v-pills-uisp" type="button" role="tab" aria-controls="v-pills-settings" aria-selected="false"><i class="fa fa-eye"></i> UISP</button>
|
||||
<button class="nav-link" id="v-pills-users-tab" data-bs-toggle="pill" data-bs-target="#v-pills-users" type="button" role="tab" aria-controls="v-pills-settings" aria-selected="false"><i class="fa fa-users"></i> LibreQoS Users</button>
|
||||
</div>
|
||||
<div class="tab-content" id="v-pills-tabContent">
|
||||
<div class="tab-pane fade show active" id="v-pills-home" role="tabpanel" aria-labelledby="v-pills-home-tab">
|
||||
@@ -353,6 +354,10 @@
|
||||
UISP Settings
|
||||
...
|
||||
</div>
|
||||
<div class="tab-pane fade" id="v-pills-users" role="tabpanel" aria-labelledby="v-pills-users-tab">
|
||||
<h2><i class="fa fa-users"></i> LibreQos Web Interface Users</h2>
|
||||
<div id="userManager"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -374,84 +379,100 @@
|
||||
function start() {
|
||||
colorReloadButton();
|
||||
updateHostCounts();
|
||||
$.get("/api/python_config", (data) => {
|
||||
python_config = data;
|
||||
$.get("/api/lqosd_config", (data) => {
|
||||
lqosd_config = data;
|
||||
$.get("/api/list_nics", (data) => {
|
||||
nics = data;
|
||||
console.log(nics);
|
||||
fillNicList("nicCore", python_config.isp_interface);
|
||||
fillNicList("nicInternet", python_config.internet_interface);
|
||||
$.get("/api/admin_check", (is_admin) => {
|
||||
if (!is_admin) {
|
||||
$("#controls").html("<p class='alert alert-danger' role='alert'>You have to be an administrative user to change configuration.");
|
||||
$("#userManager").html("<p class='alert alert-danger' role='alert'>Only administrators can see/change user information.");
|
||||
}
|
||||
$.get("/api/python_config", (data) => {
|
||||
python_config = data;
|
||||
$.get("/api/lqosd_config", (data) => {
|
||||
lqosd_config = data;
|
||||
$.get("/api/list_nics", (data) => {
|
||||
nics = data;
|
||||
console.log(nics);
|
||||
fillNicList("nicCore", python_config.isp_interface);
|
||||
fillNicList("nicInternet", python_config.internet_interface);
|
||||
|
||||
$("#onAStick").prop('checked', python_config.on_a_stick_mode);
|
||||
$("#StickVLANCore").val(python_config.stick_vlans[0]);
|
||||
$("#StickVLANInternet").val(python_config.stick_vlans[1]);
|
||||
if (lqosd_config.bridge != null) {
|
||||
$("#useKernelBridge").prop('checked', lqosd_config.bridge.use_kernel_bridge);
|
||||
$("#onAStick").prop('checked', python_config.on_a_stick_mode);
|
||||
$("#StickVLANCore").val(python_config.stick_vlans[0]);
|
||||
$("#StickVLANInternet").val(python_config.stick_vlans[1]);
|
||||
if (lqosd_config.bridge != null) {
|
||||
$("#useKernelBridge").prop('checked', lqosd_config.bridge.use_kernel_bridge);
|
||||
|
||||
// Map Bifrost Interfaces
|
||||
let html = "<h4>Interface Mapping</h4>";
|
||||
html += "<table class='table'>";
|
||||
html += "<thead><th>Input Interface</th><th>Output Interface</th><th>Scan VLANs?</th></thead>";
|
||||
html += "<tbody>";
|
||||
for (let i=0; i<lqosd_config.bridge.interface_mapping.length; i++) {
|
||||
html += "<tr>";
|
||||
html += "<td>" + buildNICList('bfIn_' + i, lqosd_config.bridge.interface_mapping[i].name) + "</td>";
|
||||
html += "<td>" + buildNICList('bfOut_' + i, lqosd_config.bridge.interface_mapping[i].redirect_to) + "</td>";
|
||||
html += "<td><input type='checkbox' class='form-check-input' id='bfScanVLAN_" + i + "'";
|
||||
if (lqosd_config.bridge.interface_mapping[i].scan_vlans) {
|
||||
html += ' checked';
|
||||
// Map Bifrost Interfaces
|
||||
let html = "<h4>Interface Mapping</h4>";
|
||||
html += "<table class='table'>";
|
||||
html += "<thead><th>Input Interface</th><th>Output Interface</th><th>Scan VLANs?</th></thead>";
|
||||
html += "<tbody>";
|
||||
for (let i=0; i<lqosd_config.bridge.interface_mapping.length; i++) {
|
||||
html += "<tr>";
|
||||
html += "<td>" + buildNICList('bfIn_' + i, lqosd_config.bridge.interface_mapping[i].name) + "</td>";
|
||||
html += "<td>" + buildNICList('bfOut_' + i, lqosd_config.bridge.interface_mapping[i].redirect_to) + "</td>";
|
||||
html += "<td><input type='checkbox' class='form-check-input' id='bfScanVLAN_" + i + "'";
|
||||
if (lqosd_config.bridge.interface_mapping[i].scan_vlans) {
|
||||
html += ' checked';
|
||||
}
|
||||
html += "/></td>";
|
||||
html += "</tr>";
|
||||
}
|
||||
html += "/></td>";
|
||||
html += "</tr>";
|
||||
}
|
||||
html += "</tbody></table>";
|
||||
$("#bifrostInterfaces").html(html);
|
||||
html += "</tbody></table>";
|
||||
$("#bifrostInterfaces").html(html);
|
||||
|
||||
// Map Bifrost VLAN mappings
|
||||
html = "<h4>VLAN Mapping</h4>";
|
||||
html += "<table class='table'>";
|
||||
html += "<thead><th>Parent Interface</th><th>Input Tag</th><th>Remapped Tag</th></thead>";
|
||||
html += "<tbody>";
|
||||
for (let i=0; i<lqosd_config.bridge.vlan_mapping.length; i++) {
|
||||
html += "<tr>";
|
||||
html += "<td>" + buildNICList('bfvlanif_' + i, lqosd_config.bridge.vlan_mapping[i].parent) + "</td>";
|
||||
html += "<td><input id='bfvlantag_" + i + "' type='number' min='0' max='4094' value='" + lqosd_config.bridge.vlan_mapping[i].tag + "' /></td>";
|
||||
html += "<td><input id='bfvlanout_" + i + "' type='number' min='0' max='4094' value='" + lqosd_config.bridge.vlan_mapping[i].redirect_to + "' /></td>";
|
||||
html += "</tr>";
|
||||
// Map Bifrost VLAN mappings
|
||||
html = "<h4>VLAN Mapping</h4>";
|
||||
html += "<table class='table'>";
|
||||
html += "<thead><th>Parent Interface</th><th>Input Tag</th><th>Remapped Tag</th></thead>";
|
||||
html += "<tbody>";
|
||||
for (let i=0; i<lqosd_config.bridge.vlan_mapping.length; i++) {
|
||||
html += "<tr>";
|
||||
html += "<td>" + buildNICList('bfvlanif_' + i, lqosd_config.bridge.vlan_mapping[i].parent) + "</td>";
|
||||
html += "<td><input id='bfvlantag_" + i + "' type='number' min='0' max='4094' value='" + lqosd_config.bridge.vlan_mapping[i].tag + "' /></td>";
|
||||
html += "<td><input id='bfvlanout_" + i + "' type='number' min='0' max='4094' value='" + lqosd_config.bridge.vlan_mapping[i].redirect_to + "' /></td>";
|
||||
html += "</tr>";
|
||||
}
|
||||
html += "</tbody></table>";
|
||||
$("#bifrostVlans").html(html);
|
||||
}
|
||||
html += "</tbody></table>";
|
||||
$("#bifrostVlans").html(html);
|
||||
}
|
||||
$("#sqmMode option[value='" + python_config.sqm + "']").prop("selected", true);
|
||||
$("#maxDownload").val(python_config.total_download_mbps);
|
||||
$("#maxUpload").val(python_config.total_upload_mbps);
|
||||
$("#monitorMode").prop('checked', python_config.monitor_mode);
|
||||
$("#generatedDownload").val(python_config.generated_download_mbps);
|
||||
$("#generatedUpload").val(python_config.generated_upload_mbps);
|
||||
$("#binpacking").prop('checked', python_config.use_binpacking);
|
||||
$("#queuecheckms").val(lqosd_config.queue_check_period_ms);
|
||||
$("#actualShellCommands").prop('checked', python_config.enable_shell_commands);
|
||||
$("#useSudo").prop('checked', python_config.run_as_sudo);
|
||||
$("#overrideQueues").val(python_config.override_queue_count);
|
||||
$("#stopIrqBalance").prop('checked', lqosd_config.tuning.stop_irq_balance);
|
||||
$("#netDevUsec").val(lqosd_config.tuning.netdev_budget_usecs);
|
||||
$("#netDevPackets").val(lqosd_config.tuning.netdev_budget_packets);
|
||||
$("#rxUsecs").val(lqosd_config.tuning.rx_usecs);
|
||||
$("#txUsecs").val(lqosd_config.tuning.tx_usecs);
|
||||
$("#disableRxVlan").prop('checked', lqosd_config.tuning.disable_rxvlan);
|
||||
$("#disableTxVlan").prop('checked', lqosd_config.tuning.disable_txvlan);
|
||||
let offloads = "";
|
||||
for (let i=0; i<lqosd_config.tuning.disable_offload.length; i++) {
|
||||
offloads += lqosd_config.tuning.disable_offload[i] + " ";
|
||||
}
|
||||
$("#disableOffloadList").val(offloads);
|
||||
});
|
||||
});
|
||||
$("#sqmMode option[value='" + python_config.sqm + "']").prop("selected", true);
|
||||
$("#maxDownload").val(python_config.total_download_mbps);
|
||||
$("#maxUpload").val(python_config.total_upload_mbps);
|
||||
$("#monitorMode").prop('checked', python_config.monitor_mode);
|
||||
$("#generatedDownload").val(python_config.generated_download_mbps);
|
||||
$("#generatedUpload").val(python_config.generated_upload_mbps);
|
||||
$("#binpacking").prop('checked', python_config.use_binpacking);
|
||||
$("#queuecheckms").val(lqosd_config.queue_check_period_ms);
|
||||
$("#actualShellCommands").prop('checked', python_config.enable_shell_commands);
|
||||
$("#useSudo").prop('checked', python_config.run_as_sudo);
|
||||
$("#overrideQueues").val(python_config.override_queue_count);
|
||||
$("#stopIrqBalance").prop('checked', lqosd_config.tuning.stop_irq_balance);
|
||||
$("#netDevUsec").val(lqosd_config.tuning.netdev_budget_usecs);
|
||||
$("#netDevPackets").val(lqosd_config.tuning.netdev_budget_packets);
|
||||
$("#rxUsecs").val(lqosd_config.tuning.rx_usecs);
|
||||
$("#txUsecs").val(lqosd_config.tuning.tx_usecs);
|
||||
$("#disableRxVlan").prop('checked', lqosd_config.tuning.disable_rxvlan);
|
||||
$("#disableTxVlan").prop('checked', lqosd_config.tuning.disable_txvlan);
|
||||
let offloads = "";
|
||||
for (let i=0; i<lqosd_config.tuning.disable_offload.length; i++) {
|
||||
offloads += lqosd_config.tuning.disable_offload[i] + " ";
|
||||
}
|
||||
$("#disableOffloadList").val(offloads);
|
||||
|
||||
// User management
|
||||
if (is_admin) {
|
||||
userManager();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function userManager() {
|
||||
let html = "<p>For now, please use <em>bin/webusers</em> to manage users.</p>";
|
||||
$("#userManager").html(html);
|
||||
}
|
||||
|
||||
function fillNicList(id, selected) {
|
||||
let select = $("#" + id);
|
||||
let html = "";
|
||||
|
||||
92
src/rust/lqos_node_manager/static/first_run.html
Normal file
92
src/rust/lqos_node_manager/static/first_run.html
Normal file
@@ -0,0 +1,92 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="/vendor/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/vendor/solid.min.css">
|
||||
<title>LibreQoS - Local Node Manager</title>
|
||||
<script src="/lqos.js"></script>
|
||||
<script src="/vendor/plotly-2.16.1.min.js"></script>
|
||||
<script src="/vendor/jquery.min.js"></script>
|
||||
</head>
|
||||
<body class="bg-secondary">
|
||||
|
||||
<div id="container" style="padding: 4px;">
|
||||
<div class="row">
|
||||
<div class="col-sm-4"></div>
|
||||
<div class="col-sm-4">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">First Login</h5>
|
||||
<p>
|
||||
No <em>webusers.toml</em> file was found. This is probably the first time you've run
|
||||
the LibreQoS web system. If it isn't, then please check permissions on that file and
|
||||
use the "bin/webusers" command to verify that your system is working.
|
||||
</p>
|
||||
<p class="alert alert-warning" role="alert">
|
||||
This site will use a cookie to store your identification. If that's not ok,
|
||||
please don't use the site.
|
||||
</p>
|
||||
<p>Let's create a new user, and set some parameters:</p>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<input class="form-check-input" type="checkbox" value="" id="allowAnonymous">
|
||||
<label class="form-check-label" for="allowAnonymous">
|
||||
Allow anonymous users to view (but not change) settings.
|
||||
</label>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td>
|
||||
Your Username
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" id="username" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Your password</td>
|
||||
<td><input type="password" id="password" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
<a class="btn btn-primary" id="btnCreateUser">Create User Account</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>Copyright (c) 2022, LibreQoE LLC</footer>
|
||||
|
||||
<script>
|
||||
function start() {
|
||||
$("#btnCreateUser").on('click', (data) => {
|
||||
let newUser = {
|
||||
allow_anonymous: $("#allowAnonymous").prop('checked'),
|
||||
username: $("#username").val(),
|
||||
password: $("#password").val(),
|
||||
};
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/api/create_first_user",
|
||||
data: JSON.stringify(newUser),
|
||||
success: (data) => {
|
||||
if (data == "ERROR") {
|
||||
alert("Unable to create a first user.")
|
||||
} else {
|
||||
window.location.href = "/";
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(start);
|
||||
</script>
|
||||
|
||||
<!-- Leave to last -->
|
||||
<script src="vendor/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
72
src/rust/lqos_node_manager/static/login.html
Normal file
72
src/rust/lqos_node_manager/static/login.html
Normal file
@@ -0,0 +1,72 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="/vendor/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/vendor/solid.min.css">
|
||||
<title>LibreQoS - Local Node Manager</title>
|
||||
<script src="/lqos.js"></script>
|
||||
<script src="/vendor/plotly-2.16.1.min.js"></script>
|
||||
<script src="/vendor/jquery.min.js"></script>
|
||||
</head>
|
||||
<body class="bg-secondary">
|
||||
|
||||
<div id="container" style="padding: 4px;">
|
||||
<div class="row">
|
||||
<div class="col-sm-4"></div>
|
||||
<div class="col-sm-4">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Login</h5>
|
||||
<p>Please enter a username and password to access LibreQoS.</p>
|
||||
<p>You can control access locally with <em>bin/webusers</em> from the console.</p>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<td>Username</td>
|
||||
<td><input type="text" id="username" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Password</td>
|
||||
<td><input type="password" id="password" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
<a class="btn btn-primary" id="btnLogin">Login</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>Copyright (c) 2022, LibreQoE LLC</footer>
|
||||
|
||||
<script>
|
||||
function start() {
|
||||
$("#btnLogin").on('click', (data) => {
|
||||
let newUser = {
|
||||
username: $("#username").val(),
|
||||
password: $("#password").val(),
|
||||
};
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/api/login",
|
||||
data: JSON.stringify(newUser),
|
||||
success: (data) => {
|
||||
if (data == "ERROR") {
|
||||
alert("Invalid login")
|
||||
} else {
|
||||
window.location.href = "/";
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(start);
|
||||
</script>
|
||||
|
||||
<!-- Leave to last -->
|
||||
<script src="vendor/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -44,12 +44,33 @@ function bindColorToggle() {
|
||||
});
|
||||
}
|
||||
|
||||
function deleteAllCookies() {
|
||||
const cookies = document.cookie.split(";");
|
||||
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i];
|
||||
const eqPos = cookie.indexOf("=");
|
||||
const name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie;
|
||||
document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT";
|
||||
}
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function updateHostCounts() {
|
||||
$.get("/api/host_counts", (hc) => {
|
||||
$("#shapedCount").text(hc[0]);
|
||||
$("#unshapedCount").text(hc[1]);
|
||||
setTimeout(updateHostCounts, 5000);
|
||||
});
|
||||
$.get("/api/username", (un) => {
|
||||
let html = "";
|
||||
if (un == "Anonymous") {
|
||||
html = "<a class='nav-link' href='/login'><i class='fa fa-user'></i> Login</a>";
|
||||
} else {
|
||||
html = "<a class='nav-link' onclick='deleteAllCookies();'><i class='fa fa-user'></i> Logout " + un + "</a>";
|
||||
}
|
||||
$("#currentLogin").html(html);
|
||||
})
|
||||
}
|
||||
|
||||
function colorReloadButton() {
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="/"><i class="fa fa-home"></i> Dashboard</a>
|
||||
</li>
|
||||
<li class="nav-item" id="currentLogin"></li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#"><i class="fa fa-globe"></i> Network Layout</a>
|
||||
</li>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/"><i class="fa fa-home"></i> Dashboard</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item" id="currentLogin"></li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#"><i class="fa fa-globe"></i> Network Layout</a>
|
||||
</li>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/"><i class="fa fa-home"></i> Dashboard</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item" id="currentLogin"></li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#"><i class="fa fa-globe"></i> Network Layout</a>
|
||||
</li>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/"><i class="fa fa-home"></i> Dashboard</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item" id="currentLogin"></li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#"><i class="fa fa-globe"></i> Network Layout</a>
|
||||
</li>
|
||||
|
||||
9
src/rust/webusers/Cargo.toml
Normal file
9
src/rust/webusers/Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "webusers"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
lqos_config = { path = "../lqos_config" }
|
||||
anyhow = "1"
|
||||
59
src/rust/webusers/src/main.rs
Normal file
59
src/rust/webusers/src/main.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use std::process::exit;
|
||||
use clap::{Parser, Subcommand};
|
||||
use anyhow::Result;
|
||||
use lqos_config::{WebUsers, UserRole};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command()]
|
||||
struct Args {
|
||||
#[command(subcommand)]
|
||||
command: Option<Commands>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Add or update a user
|
||||
Add {
|
||||
/// Username
|
||||
#[arg(long)]
|
||||
username: String,
|
||||
|
||||
/// Role
|
||||
#[arg(long)]
|
||||
role: UserRole,
|
||||
|
||||
/// CPU id to connect
|
||||
#[arg(long)]
|
||||
password: String,
|
||||
},
|
||||
/// Remove a user
|
||||
Del {
|
||||
/// Username to remove
|
||||
username: String,
|
||||
},
|
||||
/// List all mapped IPs.
|
||||
List,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let cli = Args::parse();
|
||||
let mut users = WebUsers::load_or_create()?;
|
||||
match cli.command {
|
||||
Some(Commands::Add { username, role, password }) => {
|
||||
users.add_or_update_user(&username, &password, role)?;
|
||||
}
|
||||
Some(Commands::Del { username }) => {
|
||||
users.remove_user(&username)?;
|
||||
}
|
||||
Some(Commands::List) => {
|
||||
println!("All Users\n");
|
||||
users.print_users()?;
|
||||
}
|
||||
None => {
|
||||
println!("Run with --help to see instructions");
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user