diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/config.rs | 5 | ||||
-rw-r--r-- | src/db.rs | 11 | ||||
-rw-r--r-- | src/error.rs | 3 | ||||
-rw-r--r-- | src/main.rs | 98 | ||||
-rw-r--r-- | src/routes.rs | 2 | ||||
-rw-r--r-- | src/routes/device.rs | 91 | ||||
-rw-r--r-- | src/routes/start.rs | 16 | ||||
-rw-r--r-- | src/services/ping.rs | 31 |
8 files changed, 199 insertions, 58 deletions
diff --git a/src/config.rs b/src/config.rs index 4319ffc..9605361 100644 --- a/src/config.rs +++ b/src/config.rs | |||
@@ -7,6 +7,8 @@ pub struct Config { | |||
7 | pub apikey: String, | 7 | pub apikey: String, |
8 | pub serveraddr: String, | 8 | pub serveraddr: String, |
9 | pub pingtimeout: i64, | 9 | pub pingtimeout: i64, |
10 | pub pingthreshold: i64, | ||
11 | pub timeoffset: i8, | ||
10 | } | 12 | } |
11 | 13 | ||
12 | impl Config { | 14 | impl Config { |
@@ -14,6 +16,8 @@ impl Config { | |||
14 | let config = config::Config::builder() | 16 | let config = config::Config::builder() |
15 | .set_default("serveraddr", "0.0.0.0:7229")? | 17 | .set_default("serveraddr", "0.0.0.0:7229")? |
16 | .set_default("pingtimeout", 10)? | 18 | .set_default("pingtimeout", 10)? |
19 | .set_default("pingthreshold", 1)? | ||
20 | .set_default("timeoffset", 0)? | ||
17 | .add_source(File::with_name("config.toml").required(false)) | 21 | .add_source(File::with_name("config.toml").required(false)) |
18 | .add_source(File::with_name("config.dev.toml").required(false)) | 22 | .add_source(File::with_name("config.dev.toml").required(false)) |
19 | .add_source(config::Environment::with_prefix("WEBOL").prefix_separator("_")) | 23 | .add_source(config::Environment::with_prefix("WEBOL").prefix_separator("_")) |
@@ -22,4 +26,3 @@ impl Config { | |||
22 | config.try_deserialize() | 26 | config.try_deserialize() |
23 | } | 27 | } |
24 | } | 28 | } |
25 | |||
@@ -1,6 +1,7 @@ | |||
1 | use serde::Serialize; | 1 | use serde::Serialize; |
2 | use sqlx::{PgPool, postgres::PgPoolOptions, types::{ipnetwork::IpNetwork, mac_address::MacAddress}}; | 2 | use sqlx::{PgPool, postgres::PgPoolOptions, types::{ipnetwork::IpNetwork, mac_address::MacAddress}}; |
3 | use tracing::{debug, info}; | 3 | use tracing::{debug, info}; |
4 | use utoipa::ToSchema; | ||
4 | 5 | ||
5 | #[derive(Serialize, Debug)] | 6 | #[derive(Serialize, Debug)] |
6 | pub struct Device { | 7 | pub struct Device { |
@@ -11,6 +12,16 @@ pub struct Device { | |||
11 | pub times: Option<Vec<i64>> | 12 | pub times: Option<Vec<i64>> |
12 | } | 13 | } |
13 | 14 | ||
15 | #[derive(ToSchema)] | ||
16 | #[schema(as = Device)] | ||
17 | pub struct DeviceSchema { | ||
18 | pub id: String, | ||
19 | pub mac: String, | ||
20 | pub broadcast_addr: String, | ||
21 | pub ip: String, | ||
22 | pub times: Option<Vec<i64>> | ||
23 | } | ||
24 | |||
14 | pub async fn init_db_pool(db_url: &str) -> PgPool { | 25 | pub async fn init_db_pool(db_url: &str) -> PgPool { |
15 | debug!("attempt to connect dbPool to '{}'", db_url); | 26 | debug!("attempt to connect dbPool to '{}'", db_url); |
16 | 27 | ||
diff --git a/src/error.rs b/src/error.rs index 513b51b..006fcdb 100644 --- a/src/error.rs +++ b/src/error.rs | |||
@@ -5,10 +5,11 @@ use axum::response::{IntoResponse, Response}; | |||
5 | use axum::Json; | 5 | use axum::Json; |
6 | use mac_address::MacParseError; | 6 | use mac_address::MacParseError; |
7 | use serde_json::json; | 7 | use serde_json::json; |
8 | use utoipa::ToSchema; | ||
8 | use std::io; | 9 | use std::io; |
9 | use tracing::error; | 10 | use tracing::error; |
10 | 11 | ||
11 | #[derive(Debug, thiserror::Error)] | 12 | #[derive(Debug, thiserror::Error, ToSchema)] |
12 | pub enum Error { | 13 | pub enum Error { |
13 | #[error("db: {source}")] | 14 | #[error("db: {source}")] |
14 | Db { | 15 | Db { |
diff --git a/src/main.rs b/src/main.rs index d17984f..00fc6ce 100644 --- a/src/main.rs +++ b/src/main.rs | |||
@@ -1,21 +1,30 @@ | |||
1 | use crate::config::Config; | 1 | use crate::{ |
2 | use crate::db::init_db_pool; | 2 | config::Config, |
3 | use crate::routes::device; | 3 | db::init_db_pool, |
4 | use crate::routes::start::start; | 4 | routes::{device, start, status}, |
5 | use crate::routes::status::status; | 5 | services::ping::{BroadcastCommand, StatusMap}, |
6 | use crate::services::ping::StatusMap; | 6 | }; |
7 | use axum::middleware::from_fn_with_state; | 7 | use axum::{ |
8 | use axum::routing::{get, put}; | 8 | middleware::from_fn_with_state, |
9 | use axum::{routing::post, Router}; | 9 | routing::{get, post}, |
10 | Router, | ||
11 | }; | ||
10 | use dashmap::DashMap; | 12 | use dashmap::DashMap; |
11 | use services::ping::BroadcastCommand; | ||
12 | use sqlx::PgPool; | 13 | use sqlx::PgPool; |
13 | use std::env; | 14 | use time::UtcOffset; |
14 | use std::sync::Arc; | 15 | use std::{env, sync::Arc}; |
15 | use tokio::sync::broadcast::{channel, Sender}; | 16 | use tokio::sync::broadcast::{channel, Sender}; |
16 | use tracing::{info, level_filters::LevelFilter}; | 17 | use tracing::{info, level_filters::LevelFilter}; |
17 | use tracing_subscriber::fmt::time::UtcTime; | 18 | use tracing_subscriber::{ |
18 | use tracing_subscriber::{fmt, prelude::*, EnvFilter}; | 19 | fmt::{self, time::OffsetTime}, |
20 | prelude::*, | ||
21 | EnvFilter, | ||
22 | }; | ||
23 | use utoipa::{ | ||
24 | openapi::security::{ApiKey, ApiKeyValue, SecurityScheme}, | ||
25 | Modify, OpenApi, | ||
26 | }; | ||
27 | use utoipa_swagger_ui::SwaggerUi; | ||
19 | 28 | ||
20 | mod config; | 29 | mod config; |
21 | mod db; | 30 | mod db; |
@@ -25,20 +34,62 @@ mod routes; | |||
25 | mod services; | 34 | mod services; |
26 | mod wol; | 35 | mod wol; |
27 | 36 | ||
37 | #[derive(OpenApi)] | ||
38 | #[openapi( | ||
39 | paths( | ||
40 | start::start, | ||
41 | device::get, | ||
42 | device::get_path, | ||
43 | device::post, | ||
44 | device::put, | ||
45 | ), | ||
46 | components( | ||
47 | schemas( | ||
48 | start::Payload, | ||
49 | start::Response, | ||
50 | device::PutDevicePayload, | ||
51 | device::GetDevicePayload, | ||
52 | device::PostDevicePayload, | ||
53 | db::DeviceSchema, | ||
54 | ) | ||
55 | ), | ||
56 | modifiers(&SecurityAddon), | ||
57 | tags( | ||
58 | (name = "Webol", description = "Webol API") | ||
59 | ) | ||
60 | )] | ||
61 | struct ApiDoc; | ||
62 | |||
63 | struct SecurityAddon; | ||
64 | |||
65 | impl Modify for SecurityAddon { | ||
66 | fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { | ||
67 | if let Some(components) = openapi.components.as_mut() { | ||
68 | components.add_security_scheme( | ||
69 | "api_key", | ||
70 | SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("Authorization"))), | ||
71 | ); | ||
72 | } | ||
73 | } | ||
74 | } | ||
75 | |||
28 | #[tokio::main] | 76 | #[tokio::main] |
77 | #[allow(deprecated)] | ||
29 | async fn main() -> color_eyre::eyre::Result<()> { | 78 | async fn main() -> color_eyre::eyre::Result<()> { |
30 | color_eyre::install()?; | 79 | color_eyre::install()?; |
31 | 80 | ||
81 | let config = Config::load()?; | ||
82 | |||
32 | let time_format = | 83 | let time_format = |
33 | time::macros::format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"); | 84 | time::macros::format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"); |
34 | let loc = UtcTime::new(time_format); | 85 | let time = OffsetTime::new(UtcOffset::from_hms(config.timeoffset, 0, 0)?, time_format); |
35 | 86 | ||
36 | let file_appender = tracing_appender::rolling::daily("logs", "webol.log"); | 87 | let file_appender = tracing_appender::rolling::daily("logs", "webol.log"); |
37 | let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); | 88 | let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); |
38 | 89 | ||
39 | tracing_subscriber::registry() | 90 | tracing_subscriber::registry() |
40 | .with(fmt::layer().with_writer(non_blocking).with_ansi(false)) | 91 | .with(fmt::layer().with_writer(non_blocking).with_ansi(false)) |
41 | .with(fmt::layer().with_timer(loc)) | 92 | .with(fmt::layer().with_timer(time)) |
42 | .with( | 93 | .with( |
43 | EnvFilter::builder() | 94 | EnvFilter::builder() |
44 | .with_default_directive(LevelFilter::INFO.into()) | 95 | .with_default_directive(LevelFilter::INFO.into()) |
@@ -48,8 +99,6 @@ async fn main() -> color_eyre::eyre::Result<()> { | |||
48 | 99 | ||
49 | let version = env!("CARGO_PKG_VERSION"); | 100 | let version = env!("CARGO_PKG_VERSION"); |
50 | 101 | ||
51 | let config = Config::load()?; | ||
52 | |||
53 | info!("start webol v{}", version); | 102 | info!("start webol v{}", version); |
54 | 103 | ||
55 | let db = init_db_pool(&config.database_url).await; | 104 | let db = init_db_pool(&config.database_url).await; |
@@ -67,12 +116,15 @@ async fn main() -> color_eyre::eyre::Result<()> { | |||
67 | }; | 116 | }; |
68 | 117 | ||
69 | let app = Router::new() | 118 | let app = Router::new() |
70 | .route("/start", post(start)) | 119 | .route("/start", post(start::start)) |
71 | .route("/device", get(device::get)) | 120 | .route( |
72 | .route("/device", put(device::put)) | 121 | "/device", |
73 | .route("/device", post(device::post)) | 122 | post(device::post).get(device::get).put(device::put), |
74 | .route("/status", get(status)) | 123 | ) |
124 | .route("/device/:id", get(device::get_path)) | ||
125 | .route("/status", get(status::status)) | ||
75 | .route_layer(from_fn_with_state(shared_state.clone(), extractors::auth)) | 126 | .route_layer(from_fn_with_state(shared_state.clone(), extractors::auth)) |
127 | .merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi())) | ||
76 | .with_state(Arc::new(shared_state)); | 128 | .with_state(Arc::new(shared_state)); |
77 | 129 | ||
78 | let addr = config.serveraddr; | 130 | let addr = config.serveraddr; |
diff --git a/src/routes.rs b/src/routes.rs index d5ab0d6..a72f27b 100644 --- a/src/routes.rs +++ b/src/routes.rs | |||
@@ -1,3 +1,3 @@ | |||
1 | pub mod start; | 1 | pub mod start; |
2 | pub mod device; | 2 | pub mod device; |
3 | pub mod status; \ No newline at end of file | 3 | pub mod status; |
diff --git a/src/routes/device.rs b/src/routes/device.rs index d39d98e..d01d9f0 100644 --- a/src/routes/device.rs +++ b/src/routes/device.rs | |||
@@ -1,14 +1,25 @@ | |||
1 | use crate::db::Device; | 1 | use crate::db::Device; |
2 | use crate::error::Error; | 2 | use crate::error::Error; |
3 | use axum::extract::State; | 3 | use axum::extract::{Path, State}; |
4 | use axum::Json; | 4 | use axum::Json; |
5 | use mac_address::MacAddress; | 5 | use mac_address::MacAddress; |
6 | use serde::{Deserialize, Serialize}; | 6 | use serde::Deserialize; |
7 | use serde_json::{json, Value}; | 7 | use serde_json::{json, Value}; |
8 | use sqlx::types::ipnetwork::IpNetwork; | 8 | use sqlx::types::ipnetwork::IpNetwork; |
9 | use std::{sync::Arc, str::FromStr}; | 9 | use std::{str::FromStr, sync::Arc}; |
10 | use tracing::{debug, info}; | 10 | use tracing::{debug, info}; |
11 | use utoipa::ToSchema; | ||
11 | 12 | ||
13 | #[utoipa::path( | ||
14 | get, | ||
15 | path = "/device", | ||
16 | request_body = GetDevicePayload, | ||
17 | responses( | ||
18 | (status = 200, description = "Get `Device` information", body = [Device]) | ||
19 | ), | ||
20 | security(("api_key" = [])) | ||
21 | )] | ||
22 | #[deprecated] | ||
12 | pub async fn get( | 23 | pub async fn get( |
13 | State(state): State<Arc<crate::AppState>>, | 24 | State(state): State<Arc<crate::AppState>>, |
14 | Json(payload): Json<GetDevicePayload>, | 25 | Json(payload): Json<GetDevicePayload>, |
@@ -31,11 +42,53 @@ pub async fn get( | |||
31 | Ok(Json(json!(device))) | 42 | Ok(Json(json!(device))) |
32 | } | 43 | } |
33 | 44 | ||
34 | #[derive(Deserialize)] | 45 | #[utoipa::path( |
46 | get, | ||
47 | path = "/device/{id}", | ||
48 | responses( | ||
49 | (status = 200, description = "Get `Device` information", body = [Device]) | ||
50 | ), | ||
51 | params( | ||
52 | ("id" = String, Path, description = "Device id") | ||
53 | ), | ||
54 | security(("api_key" = [])) | ||
55 | )] | ||
56 | pub async fn get_path( | ||
57 | State(state): State<Arc<crate::AppState>>, | ||
58 | Path(path): Path<String>, | ||
59 | ) -> Result<Json<Value>, Error> { | ||
60 | info!("get device from path {}", path); | ||
61 | let device = sqlx::query_as!( | ||
62 | Device, | ||
63 | r#" | ||
64 | SELECT id, mac, broadcast_addr, ip, times | ||
65 | FROM devices | ||
66 | WHERE id = $1; | ||
67 | "#, | ||
68 | path | ||
69 | ) | ||
70 | .fetch_one(&state.db) | ||
71 | .await?; | ||
72 | |||
73 | debug!("got device {:?}", device); | ||
74 | |||
75 | Ok(Json(json!(device))) | ||
76 | } | ||
77 | |||
78 | #[derive(Deserialize, ToSchema)] | ||
35 | pub struct GetDevicePayload { | 79 | pub struct GetDevicePayload { |
36 | id: String, | 80 | id: String, |
37 | } | 81 | } |
38 | 82 | ||
83 | #[utoipa::path( | ||
84 | put, | ||
85 | path = "/device", | ||
86 | request_body = PutDevicePayload, | ||
87 | responses( | ||
88 | (status = 200, description = "List matching todos by query", body = [DeviceSchema]) | ||
89 | ), | ||
90 | security(("api_key" = [])) | ||
91 | )] | ||
39 | pub async fn put( | 92 | pub async fn put( |
40 | State(state): State<Arc<crate::AppState>>, | 93 | State(state): State<Arc<crate::AppState>>, |
41 | Json(payload): Json<PutDevicePayload>, | 94 | Json(payload): Json<PutDevicePayload>, |
@@ -44,26 +97,28 @@ pub async fn put( | |||
44 | "add device {} ({}, {}, {})", | 97 | "add device {} ({}, {}, {})", |
45 | payload.id, payload.mac, payload.broadcast_addr, payload.ip | 98 | payload.id, payload.mac, payload.broadcast_addr, payload.ip |
46 | ); | 99 | ); |
47 | 100 | ||
48 | let ip = IpNetwork::from_str(&payload.ip)?; | 101 | let ip = IpNetwork::from_str(&payload.ip)?; |
49 | let mac = MacAddress::from_str(&payload.mac)?; | 102 | let mac = MacAddress::from_str(&payload.mac)?; |
50 | sqlx::query!( | 103 | let device = sqlx::query_as!( |
104 | Device, | ||
51 | r#" | 105 | r#" |
52 | INSERT INTO devices (id, mac, broadcast_addr, ip) | 106 | INSERT INTO devices (id, mac, broadcast_addr, ip) |
53 | VALUES ($1, $2, $3, $4); | 107 | VALUES ($1, $2, $3, $4) |
108 | RETURNING id, mac, broadcast_addr, ip, times; | ||
54 | "#, | 109 | "#, |
55 | payload.id, | 110 | payload.id, |
56 | mac, | 111 | mac, |
57 | payload.broadcast_addr, | 112 | payload.broadcast_addr, |
58 | ip | 113 | ip |
59 | ) | 114 | ) |
60 | .execute(&state.db) | 115 | .fetch_one(&state.db) |
61 | .await?; | 116 | .await?; |
62 | 117 | ||
63 | Ok(Json(json!(PutDeviceResponse { success: true }))) | 118 | Ok(Json(json!(device))) |
64 | } | 119 | } |
65 | 120 | ||
66 | #[derive(Deserialize)] | 121 | #[derive(Deserialize, ToSchema)] |
67 | pub struct PutDevicePayload { | 122 | pub struct PutDevicePayload { |
68 | id: String, | 123 | id: String, |
69 | mac: String, | 124 | mac: String, |
@@ -71,11 +126,15 @@ pub struct PutDevicePayload { | |||
71 | ip: String, | 126 | ip: String, |
72 | } | 127 | } |
73 | 128 | ||
74 | #[derive(Serialize)] | 129 | #[utoipa::path( |
75 | pub struct PutDeviceResponse { | 130 | post, |
76 | success: bool, | 131 | path = "/device", |
77 | } | 132 | request_body = PostDevicePayload, |
78 | 133 | responses( | |
134 | (status = 200, description = "List matching todos by query", body = [DeviceSchema]) | ||
135 | ), | ||
136 | security(("api_key" = [])) | ||
137 | )] | ||
79 | pub async fn post( | 138 | pub async fn post( |
80 | State(state): State<Arc<crate::AppState>>, | 139 | State(state): State<Arc<crate::AppState>>, |
81 | Json(payload): Json<PostDevicePayload>, | 140 | Json(payload): Json<PostDevicePayload>, |
@@ -104,7 +163,7 @@ pub async fn post( | |||
104 | Ok(Json(json!(device))) | 163 | Ok(Json(json!(device))) |
105 | } | 164 | } |
106 | 165 | ||
107 | #[derive(Deserialize)] | 166 | #[derive(Deserialize, ToSchema)] |
108 | pub struct PostDevicePayload { | 167 | pub struct PostDevicePayload { |
109 | id: String, | 168 | id: String, |
110 | mac: String, | 169 | mac: String, |
diff --git a/src/routes/start.rs b/src/routes/start.rs index d4c0802..ef6e8f2 100644 --- a/src/routes/start.rs +++ b/src/routes/start.rs | |||
@@ -6,10 +6,20 @@ use axum::extract::State; | |||
6 | use axum::Json; | 6 | use axum::Json; |
7 | use serde::{Deserialize, Serialize}; | 7 | use serde::{Deserialize, Serialize}; |
8 | use serde_json::{json, Value}; | 8 | use serde_json::{json, Value}; |
9 | use utoipa::ToSchema; | ||
9 | use std::sync::Arc; | 10 | use std::sync::Arc; |
10 | use tracing::{debug, info}; | 11 | use tracing::{debug, info}; |
11 | use uuid::Uuid; | 12 | use uuid::Uuid; |
12 | 13 | ||
14 | #[utoipa::path( | ||
15 | post, | ||
16 | path = "/start", | ||
17 | request_body = Payload, | ||
18 | responses( | ||
19 | (status = 200, description = "List matching todos by query", body = [Response]) | ||
20 | ), | ||
21 | security(("api_key" = [])) | ||
22 | )] | ||
13 | pub async fn start( | 23 | pub async fn start( |
14 | State(state): State<Arc<crate::AppState>>, | 24 | State(state): State<Arc<crate::AppState>>, |
15 | Json(payload): Json<Payload>, | 25 | Json(payload): Json<Payload>, |
@@ -88,14 +98,14 @@ fn setup_ping(state: Arc<crate::AppState>, device: Device) -> String { | |||
88 | uuid_ret | 98 | uuid_ret |
89 | } | 99 | } |
90 | 100 | ||
91 | #[derive(Deserialize)] | 101 | #[derive(Deserialize, ToSchema)] |
92 | pub struct Payload { | 102 | pub struct Payload { |
93 | id: String, | 103 | id: String, |
94 | ping: Option<bool>, | 104 | ping: Option<bool>, |
95 | } | 105 | } |
96 | 106 | ||
97 | #[derive(Serialize)] | 107 | #[derive(Serialize, ToSchema)] |
98 | struct Response { | 108 | pub struct Response { |
99 | id: String, | 109 | id: String, |
100 | boot: bool, | 110 | boot: bool, |
101 | uuid: Option<String>, | 111 | uuid: Option<String>, |
diff --git a/src/services/ping.rs b/src/services/ping.rs index 9191f86..8cf6072 100644 --- a/src/services/ping.rs +++ b/src/services/ping.rs | |||
@@ -49,22 +49,27 @@ pub async fn spawn( | |||
49 | }; | 49 | }; |
50 | } | 50 | } |
51 | 51 | ||
52 | trace!(?msg); | ||
53 | |||
52 | let msg = msg.expect("fatal error"); | 54 | let msg = msg.expect("fatal error"); |
53 | 55 | ||
54 | let _ = tx.send(msg.clone()); | 56 | let _ = tx.send(msg.clone()); |
55 | if let BroadcastCommands::Success = msg.command { | 57 | if msg.command == BroadcastCommands::Success { |
56 | sqlx::query!( | 58 | if timer.elapsed().whole_seconds() > config.pingthreshold { |
57 | r#" | 59 | sqlx::query!( |
58 | UPDATE devices | 60 | r#" |
59 | SET times = array_append(times, $1) | 61 | UPDATE devices |
60 | WHERE id = $2; | 62 | SET times = array_append(times, $1) |
61 | "#, | 63 | WHERE id = $2; |
62 | timer.elapsed().whole_seconds(), | 64 | "#, |
63 | device.id | 65 | timer.elapsed().whole_seconds(), |
64 | ) | 66 | device.id |
65 | .execute(db) | 67 | ) |
66 | .await | 68 | .execute(db) |
67 | .unwrap(); | 69 | .await |
70 | .unwrap(); | ||
71 | } | ||
72 | |||
68 | ping_map.insert( | 73 | ping_map.insert( |
69 | uuid.clone(), | 74 | uuid.clone(), |
70 | Value { | 75 | Value { |