mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
Introduce a second discovery module that passively listens for mDNS multicast announcements (224.0.0.251:5353) and feeds discovered devices into the existing devices/events/notifications pipeline, running in its own task alongside the ARP scanner. Since mDNS carries an IP and hostname but no MAC (the device key), the module resolves IP->MAC via the OS ARP cache with a targeted ARP-probe fallback. The advertised hostname is stored in a new optional `name` column on devices (blank for ARP-only devices); the single `update` writes `name` only when set so ARP rescans never clobber it. Device names are included in event/notification messages. Adds a GET /api/mdns_scanner/status endpoint (is_listening, devices_seen, last_device_seen_seconds_ago) wired into OpenAPI, an optional [mdns_scanner] config section, and the corresponding Nix module option. Also aligns the Nix module's stale `timings` section with the current `arp_scanner` schema. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
210 lines
6.5 KiB
Rust
210 lines
6.5 KiB
Rust
use std::collections::HashMap;
|
|
|
|
use axum::extract::Path;
|
|
use axum::response::IntoResponse;
|
|
use axum::{Json, extract::Query, http::StatusCode};
|
|
use chrono::{DateTime, Utc};
|
|
use log::{debug, error};
|
|
use serde::Deserialize;
|
|
use utoipa::ToSchema;
|
|
|
|
use crate::{
|
|
db,
|
|
model::devices::{Device, DeviceSummary},
|
|
};
|
|
|
|
use crate::web_server::utils;
|
|
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/api/devices",
|
|
tag = "devices",
|
|
params(
|
|
("is_registered" = Option<bool>, Query, description = "Filter by registration status"),
|
|
("last_seen_from" = Option<String>, Query, description = "Filter devices seen after this datetime (RFC3339)"),
|
|
("last_seen_to" = Option<String>, Query, description = "Filter devices seen before this datetime (RFC3339)"),
|
|
("owner" = Option<String>, Query, description = "Filter by owner"),
|
|
("device_type" = Option<String>, Query, description = "Filter by device type"),
|
|
("vendor" = Option<String>, Query, description = "Filter by vendor"),
|
|
),
|
|
responses(
|
|
(status = 200, description = "List of devices", body = Vec<Device>),
|
|
(status = 500, description = "Internal server error"),
|
|
),
|
|
security(("bearer_auth" = []))
|
|
)]
|
|
pub async fn list(
|
|
Query(params): Query<HashMap<String, String>>,
|
|
) -> Result<Json<Vec<Device>>, StatusCode> {
|
|
let is_registered: Option<bool> = utils::parse_parameter_bool(¶ms, "is_registered");
|
|
let last_seen_from: Option<DateTime<Utc>> =
|
|
utils::parse_parameter_date(¶ms, "last_seen_from");
|
|
let last_seen_to: Option<DateTime<Utc>> = utils::parse_parameter_date(¶ms, "last_seen_to");
|
|
let owner: Option<String> = utils::parse_parameter_string(¶ms, "owner");
|
|
let device_type: Option<String> = utils::parse_parameter_string(¶ms, "device_type");
|
|
let vendor: Option<String> = utils::parse_parameter_string(¶ms, "vendor");
|
|
|
|
match db::devices::list_devices(
|
|
is_registered,
|
|
last_seen_from,
|
|
last_seen_to,
|
|
owner,
|
|
device_type,
|
|
vendor,
|
|
) {
|
|
Ok(value) => Ok(Json(value)),
|
|
Err(err) => {
|
|
error!("Error listing devices: {}", err);
|
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/api/devices/{mac_address}",
|
|
tag = "devices",
|
|
params(
|
|
("mac_address" = String, Path, description = "MAC address of the device"),
|
|
),
|
|
responses(
|
|
(status = 200, description = "Device found", body = Device),
|
|
(status = 404, description = "Device not found"),
|
|
),
|
|
security(("bearer_auth" = []))
|
|
)]
|
|
pub async fn read(Path(mac_address): Path<String>) -> Result<Json<Device>, StatusCode> {
|
|
match db::devices::read(mac_address) {
|
|
Some(value) => Ok(Json(value)),
|
|
None => Err(StatusCode::NOT_FOUND),
|
|
}
|
|
}
|
|
|
|
#[utoipa::path(
|
|
put,
|
|
path = "/api/devices",
|
|
tag = "devices",
|
|
request_body = RegisterDevicePayload,
|
|
responses(
|
|
(status = 201, description = "Device registered"),
|
|
(status = 404, description = "Device not found"),
|
|
(status = 409, description = "Device already registered"),
|
|
(status = 500, description = "Internal server error"),
|
|
),
|
|
security(("bearer_auth" = []))
|
|
)]
|
|
pub async fn register(Json(payload): Json<RegisterDevicePayload>) -> impl IntoResponse {
|
|
debug!(
|
|
"Device registration received: mac_address={}, owner={}, device_type={}",
|
|
payload.mac_address, payload.owner, payload.device_type
|
|
);
|
|
|
|
let mut device = match db::devices::read(payload.mac_address) {
|
|
Some(value) => value,
|
|
None => {
|
|
return (
|
|
axum::http::StatusCode::NOT_FOUND,
|
|
"Device not found or could not be read",
|
|
);
|
|
}
|
|
};
|
|
|
|
if device.is_registered {
|
|
return (
|
|
axum::http::StatusCode::CONFLICT,
|
|
"Device already registered",
|
|
);
|
|
}
|
|
|
|
device.is_registered = true;
|
|
device.owner = payload.owner;
|
|
device.device_type = payload.device_type;
|
|
|
|
match db::devices::update(device) {
|
|
Ok(_) => (axum::http::StatusCode::CREATED, "Device registered"),
|
|
Err(err) => {
|
|
error!("Error registering device in the database: {}", err);
|
|
(
|
|
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
|
"Error registering device in the server, check your logs",
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[utoipa::path(
|
|
delete,
|
|
path = "/api/devices/{mac_address}",
|
|
tag = "devices",
|
|
params(
|
|
("mac_address" = String, Path, description = "MAC address of the device"),
|
|
),
|
|
responses(
|
|
(status = 200, description = "Device unregistered"),
|
|
(status = 404, description = "Device not found"),
|
|
(status = 409, description = "Device is not registered"),
|
|
(status = 500, description = "Internal server error"),
|
|
),
|
|
security(("bearer_auth" = []))
|
|
)]
|
|
pub async fn unregister(Path(mac_address): Path<String>) -> impl IntoResponse {
|
|
let mut device = match db::devices::read(mac_address) {
|
|
Some(value) => value,
|
|
None => {
|
|
return (
|
|
axum::http::StatusCode::NOT_FOUND,
|
|
"Device not found or could not be read",
|
|
);
|
|
}
|
|
};
|
|
|
|
if !device.is_registered {
|
|
return (
|
|
axum::http::StatusCode::CONFLICT,
|
|
"Device not registered, you cannot un-register it again",
|
|
);
|
|
}
|
|
|
|
device.is_registered = false;
|
|
device.owner = "".to_string();
|
|
|
|
match db::devices::update(device) {
|
|
Ok(_) => (axum::http::StatusCode::OK, "Device un-registered"),
|
|
Err(err) => {
|
|
error!("Error updating device in the database: {}", err);
|
|
(
|
|
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
|
"Error updating device in the server, check your logs",
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/api/devices/summary",
|
|
tag = "devices",
|
|
responses(
|
|
(status = 200, description = "Device summary counts", body = DeviceSummary),
|
|
(status = 500, description = "Internal server error"),
|
|
),
|
|
security(("bearer_auth" = []))
|
|
)]
|
|
pub async fn summary() -> Result<Json<DeviceSummary>, StatusCode> {
|
|
match db::devices::get_summary() {
|
|
Ok(value) => Ok(Json(value)),
|
|
Err(err) => {
|
|
error!("Error getting device summary: {}", err);
|
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Payload structs
|
|
#[derive(Deserialize, ToSchema)]
|
|
pub struct RegisterDevicePayload {
|
|
mac_address: String,
|
|
owner: String,
|
|
device_type: String,
|
|
}
|