Update
This commit is contained in:
parent
76b43a0386
commit
220a446fb6
21 changed files with 677 additions and 111 deletions
|
@ -11,11 +11,16 @@ chrono-tz = { version = "0.10.0", features = ["serde"] }
|
|||
clap = { version = "4.5.20", features = ["derive"] }
|
||||
cron = { version = "0.13.0", features = ["serde"] }
|
||||
env_logger = "0.11.5"
|
||||
envconfig = "0.11.0"
|
||||
futures = "0.3.31"
|
||||
futures-util = "0.3.31"
|
||||
jsonwebtoken = "9.3.0"
|
||||
kdash_protocol = { path = "../kdash_protocol" }
|
||||
reqwest = { version = "0.12.9", features = ["stream"] }
|
||||
rumqttc = "0.24.0"
|
||||
serde = { version = "1.0.215", features = ["derive"] }
|
||||
serde_json = "1.0.132"
|
||||
serde_with = { version = "3.11.0", features = ["chrono_0_4"] }
|
||||
tokio = { version = "1.41.1", features = ["full"] }
|
||||
url = { version = "2.5.3", features = ["serde"] }
|
||||
uuid = { version = "1.11.0", features = ["serde"] }
|
||||
|
|
22
kdash_server/src/config.rs
Normal file
22
kdash_server/src/config.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
use envconfig::Envconfig;
|
||||
|
||||
#[derive(Envconfig, Clone)]
|
||||
pub struct Config {
|
||||
#[envconfig(from = "KDASH_MQTT_SERVER")]
|
||||
pub kdash_mqtt_server: String,
|
||||
|
||||
#[envconfig(from = "KDASH_MQTT_PORT")]
|
||||
pub kdash_mqtt_port: u16,
|
||||
|
||||
#[envconfig(from = "KDASH_MQTT_USERNAME")]
|
||||
pub kdash_mqtt_username: String,
|
||||
|
||||
#[envconfig(from = "KDASH_MQTT_PASSWORD")]
|
||||
pub kdash_mqtt_password: String,
|
||||
|
||||
#[envconfig(from = "KDASH_MQTT_TOPIC")]
|
||||
pub kdash_mqtt_topic: String,
|
||||
|
||||
#[envconfig(from = "KDASH_MQTT_DISCOVERY_PREFIX")]
|
||||
pub kdash_mqtt_discovery_prefix: Option<String>,
|
||||
}
|
|
@ -1,31 +1,24 @@
|
|||
use actix_web::{dev::PeerAddr, get, http, web, Error, HttpRequest, HttpResponse};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{AppState, Claims};
|
||||
use actix_web::{dev::PeerAddr, get, post, web, Error, HttpResponse};
|
||||
|
||||
use crate::{
|
||||
AppState, Claims, MqttClient, MqttDeviceDiscovery, MqttDeviceDiscoveryComponent,
|
||||
MqttDeviceDiscoveryDevice, MqttDeviceDiscoveryOrigin, MqttState, VERSION,
|
||||
};
|
||||
|
||||
#[get("/config")]
|
||||
pub async fn get_config(req: HttpRequest, data: web::Data<AppState>) -> HttpResponse {
|
||||
let token = match req
|
||||
.headers()
|
||||
.get(http::header::AUTHORIZATION)
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.filter(|s| s.starts_with("Bearer "))
|
||||
.map(|s| &s[7..])
|
||||
{
|
||||
Some(s) => s,
|
||||
pub async fn get_config(data: web::Data<AppState>, claims: Option<Claims>) -> HttpResponse {
|
||||
let claims = match claims {
|
||||
Some(x) => x,
|
||||
None => return HttpResponse::Unauthorized().finish(),
|
||||
};
|
||||
|
||||
let decoded =
|
||||
match jsonwebtoken::decode::<Claims>(token, &data.jwt_decoding_key, &data.jwt_validation) {
|
||||
Ok(x) => x,
|
||||
Err(_) => return HttpResponse::BadRequest().finish(),
|
||||
};
|
||||
|
||||
if !data.device_ids.contains(&decoded.claims.sub) {
|
||||
if !data.device_ids.contains(&claims.sub) {
|
||||
return HttpResponse::Unauthorized().finish();
|
||||
}
|
||||
|
||||
let buf = match data.devices_api_json.get(&decoded.claims.sub) {
|
||||
let buf = match data.devices_api_json.get(&claims.sub) {
|
||||
Some(x) => x,
|
||||
None => return HttpResponse::InternalServerError().finish(),
|
||||
};
|
||||
|
@ -35,35 +28,153 @@ pub async fn get_config(req: HttpRequest, data: web::Data<AppState>) -> HttpResp
|
|||
.body(buf.clone())
|
||||
}
|
||||
|
||||
#[post("/config")]
|
||||
pub async fn post_config(
|
||||
claims: Option<Claims>,
|
||||
data: web::Data<AppState>,
|
||||
mqtt_client: web::Data<MqttClient>,
|
||||
device_state: web::Json<kdash_protocol::DeviceStatePost>,
|
||||
) -> HttpResponse {
|
||||
let claims = match claims {
|
||||
Some(x) => x,
|
||||
None => return HttpResponse::Unauthorized().finish(),
|
||||
};
|
||||
|
||||
if !data.device_ids.contains(&claims.sub) {
|
||||
return HttpResponse::Unauthorized().finish();
|
||||
}
|
||||
|
||||
let (config, config_buf) = match (
|
||||
data.devices_api.get(&claims.sub),
|
||||
data.devices_api_json.get(&claims.sub),
|
||||
) {
|
||||
(Some(config), Some(config_buf)) => (config, config_buf),
|
||||
_ => return HttpResponse::InternalServerError().finish(),
|
||||
};
|
||||
|
||||
let device_id = claims.sub.to_string();
|
||||
|
||||
let state_topic = format!("{}/device/{}/state", mqtt_client.topic, device_id);
|
||||
|
||||
let state_payload = match serde_json::to_vec(&MqttState {
|
||||
battery_charging: device_state.battery_charging,
|
||||
battery_level: device_state.battery_level,
|
||||
battery_current: device_state.battery_current,
|
||||
battery_voltage: device_state.battery_voltage,
|
||||
}) {
|
||||
Ok(x) => x,
|
||||
Err(_) => return HttpResponse::InternalServerError().finish(),
|
||||
};
|
||||
|
||||
if mqtt_client
|
||||
.client
|
||||
.publish(
|
||||
&state_topic,
|
||||
rumqttc::v5::mqttbytes::QoS::AtLeastOnce,
|
||||
false,
|
||||
state_payload,
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return HttpResponse::InternalServerError().finish();
|
||||
}
|
||||
|
||||
if let Some(discovery_topic) = &mqtt_client.discovery_topic {
|
||||
let unique_id = format!("kdash_{}", device_id);
|
||||
let unique_id_battery_level = format!("{}_battery_level", unique_id);
|
||||
let unique_id_battery_current = format!("{}_battery_current", unique_id);
|
||||
let unique_id_battery_voltage = format!("{}_battery_voltage", unique_id);
|
||||
|
||||
let mut components = HashMap::new();
|
||||
components.insert(
|
||||
"battery_level".to_string(),
|
||||
MqttDeviceDiscoveryComponent {
|
||||
platform: "sensor",
|
||||
device_class: "battery",
|
||||
unit_of_measurement: Some("%"),
|
||||
value_template: "{{ value_json.battery_level }}",
|
||||
unique_id: &unique_id_battery_level,
|
||||
},
|
||||
);
|
||||
components.insert(
|
||||
"battery_current".to_string(),
|
||||
MqttDeviceDiscoveryComponent {
|
||||
platform: "sensor",
|
||||
device_class: "current",
|
||||
unit_of_measurement: Some("mA"),
|
||||
value_template: "{{ value_json.battery_current }}",
|
||||
unique_id: &unique_id_battery_current,
|
||||
},
|
||||
);
|
||||
components.insert(
|
||||
"battery_voltage".to_string(),
|
||||
MqttDeviceDiscoveryComponent {
|
||||
platform: "sensor",
|
||||
device_class: "voltage",
|
||||
unit_of_measurement: Some("mV"),
|
||||
value_template: "{{ value_json.battery_voltage }}",
|
||||
unique_id: &unique_id_battery_voltage,
|
||||
},
|
||||
);
|
||||
|
||||
let discovery = MqttDeviceDiscovery {
|
||||
device: MqttDeviceDiscoveryDevice {
|
||||
identifiers: &unique_id,
|
||||
name: config.name.to_owned(),
|
||||
},
|
||||
origin: MqttDeviceDiscoveryOrigin {
|
||||
name: "kdash",
|
||||
software_version: VERSION,
|
||||
url: "https://git.dergrimm.net/dergrimm/kdash",
|
||||
},
|
||||
components,
|
||||
state_topic: &state_topic,
|
||||
qos: 2,
|
||||
};
|
||||
|
||||
let discovery_payload = match serde_json::to_vec(&discovery) {
|
||||
Ok(x) => x,
|
||||
Err(_) => return HttpResponse::InternalServerError().finish(),
|
||||
};
|
||||
|
||||
if mqtt_client
|
||||
.client
|
||||
.publish(
|
||||
format!("{}/device/kdash/{}/config", discovery_topic, device_id),
|
||||
rumqttc::v5::mqttbytes::QoS::AtLeastOnce,
|
||||
false,
|
||||
discovery_payload,
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return HttpResponse::InternalServerError().finish();
|
||||
}
|
||||
}
|
||||
|
||||
HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(config_buf.clone())
|
||||
}
|
||||
|
||||
#[get("/image")]
|
||||
pub async fn get_image(
|
||||
req: HttpRequest,
|
||||
claims: Option<Claims>,
|
||||
peer_addr: Option<PeerAddr>,
|
||||
data: web::Data<AppState>,
|
||||
client: web::Data<reqwest::Client>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let token = match req
|
||||
.headers()
|
||||
.get(http::header::AUTHORIZATION)
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.filter(|s| s.starts_with("Bearer "))
|
||||
.map(|s| &s[7..])
|
||||
{
|
||||
Some(s) => s,
|
||||
let claims = match claims {
|
||||
Some(x) => x,
|
||||
None => return Ok(HttpResponse::Unauthorized().finish()),
|
||||
};
|
||||
|
||||
let decoded =
|
||||
match jsonwebtoken::decode::<Claims>(token, &data.jwt_decoding_key, &data.jwt_validation) {
|
||||
Ok(x) => x,
|
||||
Err(_) => return Ok(HttpResponse::BadRequest().finish()),
|
||||
};
|
||||
|
||||
if !data.device_ids.contains(&decoded.claims.sub) {
|
||||
if !data.device_ids.contains(&claims.sub) {
|
||||
return Ok(HttpResponse::Unauthorized().finish());
|
||||
}
|
||||
|
||||
let config = match data.devices.get(&decoded.claims.sub) {
|
||||
let config = match data.devices.get(&claims.sub) {
|
||||
Some(x) => x,
|
||||
None => return Ok(HttpResponse::InternalServerError().finish()),
|
||||
};
|
||||
|
|
|
@ -1,14 +1,24 @@
|
|||
use actix_web::dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform};
|
||||
use actix_web::{http, FromRequest, HttpMessage};
|
||||
use anyhow::Result;
|
||||
use futures::future::{err, ok, LocalBoxFuture};
|
||||
use jsonwebtoken::{DecodingKey, Validation};
|
||||
use kdash_protocol::Percent;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fs;
|
||||
use std::future::{ready, Ready};
|
||||
use std::marker::PhantomData;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub mod config;
|
||||
pub mod device;
|
||||
pub mod handlers;
|
||||
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct Config {
|
||||
pub devices: HashMap<Uuid, device::Config>,
|
||||
|
@ -35,3 +45,177 @@ pub struct Claims {
|
|||
pub exp: i64,
|
||||
pub sub: Uuid,
|
||||
}
|
||||
|
||||
impl FromRequest for Claims {
|
||||
type Error = actix_web::Error;
|
||||
type Future = futures::future::Ready<Result<Self, Self::Error>>;
|
||||
|
||||
fn from_request(
|
||||
req: &actix_web::HttpRequest,
|
||||
_payload: &mut actix_web::dev::Payload,
|
||||
) -> Self::Future {
|
||||
match req.extensions().get::<Claims>() {
|
||||
Some(claims) => ok(claims.clone()),
|
||||
None => err(actix_web::error::ErrorUnauthorized("Unauthorized")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Authority {
|
||||
pub decoding_key: DecodingKey,
|
||||
pub validation: Validation,
|
||||
}
|
||||
|
||||
pub struct JwtAuth<Claims> {
|
||||
pub auth: Arc<Authority>,
|
||||
claims_marker: PhantomData<Claims>,
|
||||
}
|
||||
|
||||
impl<Claims> JwtAuth<Claims> {
|
||||
pub fn new(auth: Arc<Authority>) -> Self {
|
||||
Self {
|
||||
auth,
|
||||
claims_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, B, Claims> Transform<S, ServiceRequest> for JwtAuth<Claims>
|
||||
where
|
||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error>,
|
||||
S::Future: 'static,
|
||||
B: 'static,
|
||||
{
|
||||
type Response = ServiceResponse<B>;
|
||||
type Error = actix_web::Error;
|
||||
type InitError = ();
|
||||
type Transform = JwtAuthMiddleware<S, Claims>;
|
||||
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
||||
|
||||
fn new_transform(&self, service: S) -> Self::Future {
|
||||
ready(Ok(JwtAuthMiddleware {
|
||||
service,
|
||||
auth: Arc::clone(&self.auth),
|
||||
claims_marker: PhantomData,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct JwtAuthMiddleware<S, Claims> {
|
||||
service: S,
|
||||
auth: Arc<Authority>,
|
||||
claims_marker: PhantomData<Claims>,
|
||||
}
|
||||
|
||||
impl<S, B, C> Service<ServiceRequest> for JwtAuthMiddleware<S, C>
|
||||
where
|
||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error>,
|
||||
S::Future: 'static,
|
||||
B: 'static,
|
||||
{
|
||||
type Response = ServiceResponse<B>;
|
||||
type Error = actix_web::Error;
|
||||
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||
|
||||
forward_ready!(service);
|
||||
|
||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||
let decoded = req
|
||||
.headers()
|
||||
.get(http::header::AUTHORIZATION)
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.filter(|s| s.starts_with("Bearer "))
|
||||
.map(|s| &s[7..])
|
||||
.and_then(|token| {
|
||||
jsonwebtoken::decode::<Claims>(
|
||||
token,
|
||||
&self.auth.decoding_key,
|
||||
&self.auth.validation,
|
||||
)
|
||||
.ok()
|
||||
});
|
||||
|
||||
if let Some(data) = decoded {
|
||||
req.extensions_mut().insert(data.claims);
|
||||
dbg!(req.extensions());
|
||||
}
|
||||
|
||||
Box::pin(self.service.call(req))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MqttClient {
|
||||
pub client: rumqttc::v5::AsyncClient,
|
||||
pub topic: String,
|
||||
pub discovery_topic: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct MqttState {
|
||||
pub battery_charging: bool,
|
||||
pub battery_level: Percent,
|
||||
pub battery_current: i16,
|
||||
pub battery_voltage: i16,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct MqttDeviceDiscovery<'a> {
|
||||
#[serde(rename = "dev")]
|
||||
pub device: MqttDeviceDiscoveryDevice<'a>,
|
||||
|
||||
#[serde(rename = "o")]
|
||||
pub origin: MqttDeviceDiscoveryOrigin<'a>,
|
||||
|
||||
#[serde(rename = "cmps")]
|
||||
pub components: HashMap<String, MqttDeviceDiscoveryComponent<'a>>,
|
||||
|
||||
pub state_topic: &'a str,
|
||||
|
||||
pub qos: u8,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct MqttDeviceDiscoveryDevice<'a> {
|
||||
#[serde(rename = "ids")]
|
||||
pub identifiers: &'a str,
|
||||
|
||||
pub name: String,
|
||||
// #[serde(rename = "mf")]
|
||||
// pub manufacturer: &'a str,
|
||||
|
||||
// #[serde(rename = "mdl")]
|
||||
// pub default_manufacturer: &'a str,
|
||||
|
||||
// #[serde(rename = "sw")]
|
||||
// pub sw_version: &'a str,
|
||||
|
||||
// #[serde(rename = "sn")]
|
||||
// pub serial_number: &'a str,
|
||||
|
||||
// #[serde(rename = "hw")]
|
||||
// pub hw_version: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct MqttDeviceDiscoveryOrigin<'a> {
|
||||
pub name: &'a str,
|
||||
|
||||
#[serde(rename = "sw")]
|
||||
pub software_version: &'a str,
|
||||
|
||||
pub url: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct MqttDeviceDiscoveryComponent<'a> {
|
||||
#[serde(rename = "p")]
|
||||
pub platform: &'a str,
|
||||
pub device_class: &'a str,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default)]
|
||||
pub unit_of_measurement: Option<&'a str>,
|
||||
|
||||
pub value_template: &'a str,
|
||||
pub unique_id: &'a str,
|
||||
}
|
||||
|
|
|
@ -2,11 +2,14 @@ use actix_web::{middleware::Logger, web, App, HttpServer};
|
|||
use anyhow::Result;
|
||||
use chrono::prelude::*;
|
||||
use clap::{Parser, Subcommand};
|
||||
use envconfig::Envconfig;
|
||||
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
fs,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
|
@ -40,6 +43,8 @@ async fn main() -> Result<()> {
|
|||
|
||||
let cli = Cli::parse();
|
||||
|
||||
let env_config = kdash_server::config::Config::init_from_env()?;
|
||||
|
||||
let config = kdash_server::read_config(&cli.config)?;
|
||||
|
||||
let sk = fs::read(cli.private_key)?;
|
||||
|
@ -61,6 +66,25 @@ async fn main() -> Result<()> {
|
|||
println!("{}", token);
|
||||
}
|
||||
Commands::Start => {
|
||||
let mut mqtt_options = rumqttc::v5::MqttOptions::new(
|
||||
"kdash-server",
|
||||
env_config.kdash_mqtt_server,
|
||||
env_config.kdash_mqtt_port,
|
||||
);
|
||||
mqtt_options.set_credentials(
|
||||
env_config.kdash_mqtt_username,
|
||||
env_config.kdash_mqtt_password,
|
||||
);
|
||||
mqtt_options.set_keep_alive(Duration::from_secs(10));
|
||||
|
||||
let (mqtt_client, mut eventloop) = rumqttc::v5::AsyncClient::new(mqtt_options, 20);
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
eventloop.poll().await.unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
HttpServer::new(move || {
|
||||
let devices = config.devices.to_owned();
|
||||
let device_ids: HashSet<Uuid> = config.devices.keys().cloned().collect();
|
||||
|
@ -88,8 +112,20 @@ async fn main() -> Result<()> {
|
|||
jwt_validation: Validation::new(ALGORITHM),
|
||||
}))
|
||||
.app_data(web::Data::new(reqwest_client))
|
||||
.app_data(web::Data::new(kdash_server::MqttClient {
|
||||
client: mqtt_client.to_owned(),
|
||||
topic: env_config.kdash_mqtt_topic.to_owned(),
|
||||
discovery_topic: env_config.kdash_mqtt_discovery_prefix.to_owned(),
|
||||
}))
|
||||
.wrap(Logger::default())
|
||||
.wrap(kdash_server::JwtAuth::<kdash_server::Claims>::new(
|
||||
Arc::new(kdash_server::Authority {
|
||||
decoding_key: decoding_key.to_owned(),
|
||||
validation: Validation::new(ALGORITHM),
|
||||
}),
|
||||
))
|
||||
.service(kdash_server::handlers::get_config)
|
||||
.service(kdash_server::handlers::post_config)
|
||||
.service(kdash_server::handlers::get_image)
|
||||
})
|
||||
.bind(("0.0.0.0", 8080))?
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue