use std::{sync::Arc, time::Duration};
use axum::{extract::State, response::IntoResponse};
use axum_extra::{
headers::{CacheControl, ContentType},
TypedHeader,
};
use eui48::MacAddress;
use futures::stream::{FuturesUnordered, StreamExt};
use http::StatusCode;
use crate::{
mikrotik_api::dhcp::DhcpEntry,
oui::{MacDescription, OuiDb},
AppState,
};
pub async fn list_leases(State(state): State<Arc<AppState>>) -> impl IntoResponse {
let router_results = state
.routers
.iter()
.map(|(name, api, _)| async move { (name, api.endpoint(), api.get_dhcp_leases().await) })
.collect::<FuturesUnordered<_>>()
.collect::<Vec<_>>()
.await;
let mut leases = vec![];
let mut errors = vec![];
for (name, endpoint, result) in router_results {
match result {
Ok(router_leases) => {
leases.extend(router_leases.into_iter().map(|l| (name.clone(), l)));
},
Err(err) => errors.push((name, endpoint, err)),
}
}
leases.sort_by_key(|(_, l)| l.address);
let table_body = leases
.into_iter()
.map(|(router_name, lease)| lease_to_row(&router_name, lease, &state.oui))
.fold(String::new(), |a, b| a + &b + "\n");
let errors = if errors.is_empty() {
String::new()
} else {
let errors = errors
.into_iter()
.map(|(router_name, endpoint, err)| {
format!("<li>{router_name} <code>[{endpoint}]</code>: {err}</li>")
})
.fold(String::new(), |a, b| a + &b + "\n");
format!("<hr/><h2>Errors when connecting to routers:</h2><ul>{errors}</ul>")
};
let body = String::from_utf8_lossy(include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../assets/html/index.html"
)))
.replace("{TBODY}", &table_body)
.replace("{ERRORS}", &errors);
(
StatusCode::OK,
TypedHeader(
CacheControl::new()
.with_no_cache()
.with_no_store()
.with_max_age(Duration::from_secs(0)),
),
TypedHeader(ContentType::html()),
body,
)
}
pub fn lease_to_row(router_name: &str, lease: DhcpEntry, oui: &OuiDb) -> String {
let DhcpEntry {
active,
id: _,
address,
address_lists: _,
blocked: _,
client_id: _,
comment,
dhcp_option: _,
disabled,
dynamic,
last_seen,
mac_address,
radius: _,
server,
status,
host_name,
agent_circuit_id,
agent_remote_id,
} = lease;
let mac_address = mac_address.unwrap_or(MacAddress::default());
let last_seen = last_seen
.split_inclusive(&['w', 'd', 'h', 'm', 's'])
.take(2)
.fold(String::new(), |a, b| a + b);
let host_name = host_name.unwrap_or_else(|| "--".into());
let comment = comment.unwrap_or_default();
let lease_type_short = if dynamic { "D" } else { "S" };
let lease_type = if dynamic { "dynamic" } else { "static" };
let agent_circuit_id = agent_circuit_id.unwrap_or_default();
let agent_remote_id = agent_remote_id.unwrap_or_default();
let (active_short, active, ip_addr, lease_time) = if let Some(active) = active {
let address = if active.address == address {
address.to_string()
} else {
format!(
"<strike title=\"Active\">{active}</strike><br/><span title=\"Next \
address\">{address}</span>",
active = active.address
)
};
("A", "active", address, active.expires_after)
} else {
assert_ne!(status, "bound");
("", "", address.to_string(), String::new())
};
let disabled = if disabled { "disabled" } else { "" };
let (vendor, vendor_tooltip): (String, String) = match oui.mac_description(mac_address) {
MacDescription::Laa => {
(
"<small>LAA</small>".into(),
"Locally Administered Addresses".into(),
)
},
MacDescription::Broadcast => ("Broadcast".into(), String::default()),
MacDescription::Multicast => ("Multicast".into(), String::default()),
MacDescription::Oui(oui) => oui.short_long_desc(),
MacDescription::Unknown => ("🤷".into(), "Unknown".into()),
};
let server = server.unwrap_or("-".to_owned());
format!(
r#"
<tr class="entry {disabled}" title="{disabled}">
<td title="{lease_type}">{lease_type_short}</td>
<td title="{active}">{active_short}</td>
<td>{ip_addr}</td>
<td>{comment}</td>
<td>{host_name}</td>
<td>{mac_address}</td>
<td>{lease_time}</td>
<td>{last_seen}</td>
<td title="{vendor_tooltip}">{vendor}</td>
<td>{agent_circuit_id}</td>
<td>{agent_remote_id}</td>
<td>{router_name}</td>
<td>{server}</td>
<tr>
"#,
mac_address = mac_address.to_hex_string()
)
}