This commit is contained in:
Dominic Grimm 2024-11-15 23:02:35 +01:00
commit 76b43a0386
38 changed files with 5003 additions and 0 deletions

1
kdash_server/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

21
kdash_server/Cargo.toml Normal file
View file

@ -0,0 +1,21 @@
[package]
name = "kdash_server"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-web = "4.9.0"
anyhow = { version = "1.0.93", features = ["backtrace"] }
chrono = "0.4.38"
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"
jsonwebtoken = "9.3.0"
kdash_protocol = { path = "../kdash_protocol" }
reqwest = { version = "0.12.9", features = ["stream"] }
serde = { version = "1.0.215", features = ["derive"] }
serde_json = "1.0.132"
serde_with = { version = "3.11.0", features = ["chrono_0_4"] }
url = { version = "2.5.3", features = ["serde"] }
uuid = { version = "1.11.0", features = ["serde"] }

View file

@ -0,0 +1,97 @@
use chrono::Duration;
use chrono_tz::Tz;
use cron::Schedule;
use serde::{Deserialize, Serialize};
use url::Url;
use kdash_protocol::Percent;
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Config {
pub name: String,
pub image: ImageConfiguration,
pub battery: BatteryConfig,
pub time: TimeConfig,
pub device: DeviceConfig,
}
impl From<Config> for kdash_protocol::Config {
fn from(value: Config) -> Self {
Self {
name: value.name,
battery: value.battery.into(),
time: value.time.into(),
device: value.device.into(),
}
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ImageConfiguration {
pub url: Url,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct BatteryConfig {
pub alert: Percent,
pub low: Percent,
pub restart_powerd_threshold: Percent,
}
impl From<BatteryConfig> for kdash_protocol::BatteryConfig {
fn from(value: BatteryConfig) -> Self {
Self {
alert: value.alert,
low: value.low,
restart_powerd_threshold: value.restart_powerd_threshold,
}
}
}
#[serde_with::serde_as]
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct TimeConfig {
pub timezone: Tz,
pub update_schedule: Schedule,
#[serde_as(as = "serde_with::DurationSeconds<i64>")]
pub delay_on_error: Duration,
#[serde_as(as = "serde_with::DurationSeconds<i64>")]
pub delay_before_suspend: Duration,
#[serde_as(as = "serde_with::DurationSeconds<i64>")]
pub delay_low_battery: Duration,
}
impl From<TimeConfig> for kdash_protocol::TimeConfig {
fn from(value: TimeConfig) -> Self {
Self {
timezone: value.timezone,
update_schedule: value.update_schedule,
delay_on_error: value.delay_on_error,
delay_before_suspend: value.delay_before_suspend,
delay_low_battery: value.delay_low_battery,
}
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct DeviceConfig {
pub use_rtc: bool,
pub full_refresh: bool,
pub orientation: kdash_protocol::Orientation,
pub status_box_location: kdash_protocol::Corner,
}
impl From<DeviceConfig> for kdash_protocol::DeviceConfig {
fn from(value: DeviceConfig) -> Self {
Self {
use_rtc: value.use_rtc,
full_refresh: value.full_refresh,
orientation: value.orientation,
status_box_location: value.status_box_location,
}
}
}

View file

@ -0,0 +1,97 @@
use actix_web::{dev::PeerAddr, get, http, web, Error, HttpRequest, HttpResponse};
use crate::{AppState, Claims};
#[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,
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) {
return HttpResponse::Unauthorized().finish();
}
let buf = match data.devices_api_json.get(&decoded.claims.sub) {
Some(x) => x,
None => return HttpResponse::InternalServerError().finish(),
};
HttpResponse::Ok()
.content_type("application/json")
.body(buf.clone())
}
#[get("/image")]
pub async fn get_image(
req: HttpRequest,
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,
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) {
return Ok(HttpResponse::Unauthorized().finish());
}
let config = match data.devices.get(&decoded.claims.sub) {
Some(x) => x,
None => return Ok(HttpResponse::InternalServerError().finish()),
};
let mut forwarded_req = client.get(config.image.url.to_owned());
forwarded_req = match peer_addr {
Some(PeerAddr(addr)) => forwarded_req.header("x-forwarded-for", addr.ip().to_string()),
None => forwarded_req,
};
let resp = forwarded_req
.send()
.await
.map_err(actix_web::error::ErrorInternalServerError)?;
let mut client_resp = HttpResponse::build(
actix_web::http::StatusCode::from_u16(resp.status().as_u16())
.map_err(actix_web::error::ErrorInternalServerError)?,
);
// Remove `Connection` as per
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection#Directives
for (header_name, header_value) in resp.headers().iter().filter(|(h, _)| *h != "connection") {
client_resp.insert_header((
actix_web::http::header::HeaderName::from_bytes(header_name.as_ref()).unwrap(),
actix_web::http::header::HeaderValue::from_bytes(header_value.as_ref()).unwrap(),
));
}
Ok(client_resp.streaming(resp.bytes_stream()))
}

37
kdash_server/src/lib.rs Normal file
View file

@ -0,0 +1,37 @@
use anyhow::Result;
use jsonwebtoken::{DecodingKey, Validation};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::Path;
use uuid::Uuid;
pub mod device;
pub mod handlers;
#[derive(Debug, Deserialize, Clone)]
pub struct Config {
pub devices: HashMap<Uuid, device::Config>,
}
pub fn read_config(path: &Path) -> Result<Config> {
let buf = fs::read(path)?;
let config = serde_json::from_slice(&buf)?;
Ok(config)
}
pub struct AppState {
pub devices: HashMap<Uuid, device::Config>,
pub devices_api: HashMap<Uuid, kdash_protocol::Config>,
pub devices_api_json: HashMap<Uuid, Vec<u8>>,
pub device_ids: HashSet<Uuid>,
pub jwt_decoding_key: DecodingKey,
pub jwt_validation: Validation,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Claims {
pub exp: i64,
pub sub: Uuid,
}

102
kdash_server/src/main.rs Normal file
View file

@ -0,0 +1,102 @@
use actix_web::{middleware::Logger, web, App, HttpServer};
use anyhow::Result;
use chrono::prelude::*;
use clap::{Parser, Subcommand};
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation};
use std::{
collections::{HashMap, HashSet},
fs,
path::PathBuf,
};
use uuid::Uuid;
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
#[arg(long, value_name = "FILE")]
config: PathBuf,
#[arg(long, value_name = "FILE")]
private_key: PathBuf,
#[arg(long, value_name = "FILE")]
public_key: PathBuf,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
GenJwt { id: Uuid },
Start,
}
const ALGORITHM: Algorithm = Algorithm::RS512;
#[actix_web::main]
async fn main() -> Result<()> {
env_logger::init();
let cli = Cli::parse();
let config = kdash_server::read_config(&cli.config)?;
let sk = fs::read(cli.private_key)?;
let encoding_key = EncodingKey::from_rsa_pem(&sk)?;
let pk = fs::read(cli.public_key)?;
let decoding_key = DecodingKey::from_rsa_pem(&pk)?;
match cli.command {
Commands::GenJwt { id: sub } => {
let exp = Utc::now() + chrono::Duration::days(10 * 365);
let exp_ts = exp.timestamp();
let token = jsonwebtoken::encode(
&Header::new(ALGORITHM),
&kdash_server::Claims { exp: exp_ts, sub },
&encoding_key,
)?;
println!("{}", token);
}
Commands::Start => {
HttpServer::new(move || {
let devices = config.devices.to_owned();
let device_ids: HashSet<Uuid> = config.devices.keys().cloned().collect();
let devices_api: HashMap<Uuid, kdash_protocol::Config> = devices
.iter()
.map(|(k, v)| (k.to_owned(), kdash_protocol::Config::from(v.to_owned())))
.collect();
let devices_api_json: HashMap<Uuid, Vec<u8>> = devices_api
.iter()
.map(|(k, v)| Ok((k.to_owned(), serde_json::to_vec(v)?)))
.collect::<Result<_>>()
.unwrap();
let reqwest_client = reqwest::Client::default();
App::new()
.app_data(web::Data::new(kdash_server::AppState {
devices,
devices_api,
devices_api_json,
device_ids,
jwt_decoding_key: decoding_key.to_owned(),
jwt_validation: Validation::new(ALGORITHM),
}))
.app_data(web::Data::new(reqwest_client))
.wrap(Logger::default())
.service(kdash_server::handlers::get_config)
.service(kdash_server::handlers::get_image)
})
.bind(("0.0.0.0", 8080))?
.run()
.await?;
}
}
Ok(())
}