Init
This commit is contained in:
commit
76b43a0386
38 changed files with 5003 additions and 0 deletions
1
kdash_server/.gitignore
vendored
Normal file
1
kdash_server/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
21
kdash_server/Cargo.toml
Normal file
21
kdash_server/Cargo.toml
Normal 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"] }
|
97
kdash_server/src/device.rs
Normal file
97
kdash_server/src/device.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
97
kdash_server/src/handlers.rs
Normal file
97
kdash_server/src/handlers.rs
Normal 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
37
kdash_server/src/lib.rs
Normal 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
102
kdash_server/src/main.rs
Normal 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(())
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue