From e367930505ee304abc42c352fbb256ee3be53271 Mon Sep 17 00:00:00 2001 From: nm17 Date: Wed, 6 Mar 2024 23:14:29 +0400 Subject: [PATCH 1/3] feat(old_device_sensor_api): initial work --- docs/kv_db_arch.md | 3 + src/web_server/app_error.rs | 2 +- src/web_server/mod.rs | 4 +- src/web_server/old_device_sensor_api/mod.rs | 105 ++++++++++++++++++ .../qs_parser.rs} | 69 +----------- 5 files changed, 116 insertions(+), 67 deletions(-) create mode 100644 src/web_server/old_device_sensor_api/mod.rs rename src/web_server/{old_devices_api/mod.rs => old_device_sensor_api/qs_parser.rs} (61%) diff --git a/docs/kv_db_arch.md b/docs/kv_db_arch.md index c468b02..0f35c79 100644 --- a/docs/kv_db_arch.md +++ b/docs/kv_db_arch.md @@ -11,3 +11,6 @@ - Поля - `value` - `unit` + - `devices_{device_id}` + - Поля + - `exists`: bool diff --git a/src/web_server/app_error.rs b/src/web_server/app_error.rs index 1ff7276..5195244 100644 --- a/src/web_server/app_error.rs +++ b/src/web_server/app_error.rs @@ -12,7 +12,7 @@ use thiserror::Error; use crate::insert_header; -use crate::web_server::old_devices_api::QSParserError; +use crate::web_server::old_device_sensor_api::QSParserError; use rust_decimal::Decimal; use serde_json::json; diff --git a/src/web_server/mod.rs b/src/web_server/mod.rs index e6a12d5..bcc3d93 100644 --- a/src/web_server/mod.rs +++ b/src/web_server/mod.rs @@ -6,7 +6,7 @@ use fred::prelude::*; pub(crate) mod app_error; pub mod old_app_api; -mod old_devices_api; +mod old_device_sensor_api; pub mod utils; #[derive(Clone)] @@ -14,7 +14,7 @@ pub struct NMAppState { pub redis_client: RedisClient, } -use crate::web_server::old_devices_api::device_handler; +use crate::web_server::old_device_sensor_api::device_handler; use ntex::web; diff --git a/src/web_server/old_device_sensor_api/mod.rs b/src/web_server/old_device_sensor_api/mod.rs new file mode 100644 index 0000000..e0bb540 --- /dev/null +++ b/src/web_server/old_device_sensor_api/mod.rs @@ -0,0 +1,105 @@ +mod qs_parser; + +use std::str::FromStr; +use crate::ingest_protocol::{NMDeviceDataPacket, NMJsonPacket}; +use crate::web_server::app_error::AppError; + + +use ntex::http::{HttpMessage, StatusCode}; +use ntex::util::{Bytes, HashMap}; +use ntex::{http, web}; +use fred::prelude::*; +use hifitime::Epoch; +use thiserror::Error; +use ufmt::uwrite; +use crate::web_server::NMAppState; + +#[derive(Error, Debug)] +pub enum Error { + #[error("Device not found")] + DeviceNotFound(String), + #[error("Time sent with the device is way to behind now")] + TimeIsLongBehindNow, +} + + +pub async fn device_handler<'a>( + request: web::HttpRequest, + body: Bytes, + app_state: &NMAppState, +) -> Result { + let mut real_body: Option = None; + let mut json_error = None; + let mut query_error = None; + + dbg!(&request.content_type()); + dbg!(&request.query_string()); + + if request.method() == http::Method::POST { + match request.content_type() { + "application/json" => match serde_json::from_slice::(body.as_ref()) { + Ok(json_body) => real_body = Some(json_body), + Err(error) => json_error = Some(error), + }, + "application/x-www-form-urlencoded" => { + match serde_qs::from_bytes::(body.as_ref()) { + Ok(qs_body) => { + real_body = Some(NMJsonPacket { + devices: Vec::from([qs_body]), + }) + } + Err(error) => query_error = Some(error), + } + } + _ => {} + } + } else if request.method() == http::Method::GET { + match serde_qs::from_str::(request.query_string()) { + Ok(qs_body) => { + real_body = Some(NMJsonPacket { + devices: Vec::from([qs_body]), + }) + } + Err(error) => query_error = Some(error), + } + } + + if let Some(real_body) = real_body { + for device in real_body.devices { + let mut device_key_str = String::new(); + + let device_tai_timestamp = device.time + .unwrap_or(Epoch::now().unwrap()) + .to_duration_since_j1900() + .to_seconds(); + + uwrite!(&mut device_key_str, "devices_{}", device.mac)?; + + + let device_exists: bool = app_state.redis_client.hget(device_key_str.as_str(), "exists").await?; + + if !device_exists { + return Err(Error::DeviceNotFound(device.mac)) + } + + // devices_{device_id}_{tai_timestamp}_{sensor_id} + for sensor in device.values { + let mut device_report_key_str = String::new(); + uwrite!(&mut device_report_key_str, "devices_{}_{}_{}", device.mac, device_tai_timestamp, sensor.mac)?; + let mut device_report_key_str = String::new(); + + + app_state.redis_client.hset(device_key_str.as_str(), HashMap::from([ + // TODO + ])).await?; + } + } + } else { + return Err(AppError::UnknownBody { + json_err: json_error, + query_error, + }); + } + + Ok(web::HttpResponse::build(StatusCode::OK).finish()) +} diff --git a/src/web_server/old_devices_api/mod.rs b/src/web_server/old_device_sensor_api/qs_parser.rs similarity index 61% rename from src/web_server/old_devices_api/mod.rs rename to src/web_server/old_device_sensor_api/qs_parser.rs index 1410e13..d517356 100644 --- a/src/web_server/old_devices_api/mod.rs +++ b/src/web_server/old_device_sensor_api/qs_parser.rs @@ -1,21 +1,14 @@ use std::collections::HashSet; use std::num::ParseFloatError; use std::str::FromStr; -use crate::ingest_protocol::error::Error; -use crate::ingest_protocol::parser::parse_mac_address; -use crate::ingest_protocol::{NMDeviceDataPacket, NMJsonPacket, SensorValue}; -use crate::web_server::app_error::AppError; - - -use ntex::http::{HttpMessage, StatusCode}; -use ntex::util::{Bytes, HashMap}; -use ntex::{http, web}; use std::sync::Arc; -use fred::bytes_utils::Str; -use hifitime::efmt::consts::ISO8601; use hifitime::Epoch; +use ntex::util::HashMap; use rust_decimal::Decimal; use thiserror::Error; +use crate::ingest_protocol::error::Error; +use crate::ingest_protocol::{NMDeviceDataPacket, SensorValue}; +use crate::ingest_protocol::parser::parse_mac_address; /// В иделае было бы хорошо сделать всё как у [serde_json::Error], но это слишком большая морока #[derive(Error, Clone, Debug)] @@ -118,56 +111,4 @@ pub async fn parse_nm_qs_format(input: &str) -> Result( - request: web::HttpRequest, - body: Bytes, -) -> Result { - let mut real_body: Option = None; - let mut json_error = None; - let mut query_error = None; - - dbg!(&request.content_type()); - dbg!(&request.query_string()); - - if request.method() == http::Method::POST { - match request.content_type() { - "application/json" => match serde_json::from_slice::(body.as_ref()) { - Ok(json_body) => real_body = Some(json_body), - Err(error) => json_error = Some(error), - }, - "application/x-www-form-urlencoded" => { - match serde_qs::from_bytes::(body.as_ref()) { - Ok(qs_body) => { - real_body = Some(NMJsonPacket { - devices: Vec::from([qs_body]), - }) - } - Err(error) => query_error = Some(error), - } - } - _ => {} - } - } else if request.method() == http::Method::GET { - match serde_qs::from_str::(request.query_string()) { - Ok(qs_body) => { - real_body = Some(NMJsonPacket { - devices: Vec::from([qs_body]), - }) - } - Err(error) => query_error = Some(error), - } - } - - if let Some(_body) = real_body { - // TODO - } else { - return Err(AppError::UnknownBody { - json_err: json_error, - query_error, - }); - } - - Ok(web::HttpResponse::build(StatusCode::OK).finish()) -} +} \ No newline at end of file From e4287bb2ac66f9677180ab6f3f3bd92b1efa7f5c Mon Sep 17 00:00:00 2001 From: nm17 Date: Fri, 8 Mar 2024 23:07:10 +0400 Subject: [PATCH 2/3] docs: documented most of the code --- Cargo.lock | 62 +++++++++---------- Cargo.toml | 2 +- README.md | 22 +++++++ src/ingest_protocol/mod.rs | 2 + src/ingest_protocol/packet_types.rs | 32 +++++++--- src/main.rs | 60 +----------------- src/{hashes.rs => utils.rs} | 12 +++- src/web_server/app_error.rs | 5 +- src/web_server/mod.rs | 13 +++- src/web_server/old_app_api/config_app.rs | 45 -------------- .../old_app_api/handlers/methods.rs | 36 ----------- src/web_server/old_app_api/handlers/mod.rs | 33 +++++++++- src/web_server/old_app_api/mod.rs | 52 +++++++++++++++- src/web_server/old_app_api/types/mod.rs | 11 +++- src/web_server/old_device_sensor_api/mod.rs | 13 +++- .../old_device_sensor_api/qs_parser.rs | 5 ++ src/web_server/utils/mod.rs | 3 + src/web_server/utils/redis.rs | 6 ++ 18 files changed, 223 insertions(+), 191 deletions(-) create mode 100644 README.md rename src/{hashes.rs => utils.rs} (78%) delete mode 100644 src/web_server/old_app_api/config_app.rs delete mode 100644 src/web_server/old_app_api/handlers/methods.rs diff --git a/Cargo.lock b/Cargo.lock index 36d94d0..3e852f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -984,6 +984,37 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "iotishnik-server" +version = "0.1.0" +dependencies = [ + "anyhow", + "bstr", + "bytes", + "chrono", + "clap", + "derive_more", + "dotenvy", + "fred", + "heapless", + "hex", + "hifitime", + "lazy_static", + "nom", + "ntex", + "phf", + "regex", + "rust_decimal", + "serde", + "serde_json", + "serde_qs", + "serde_with", + "smallstr", + "thiserror", + "tokio", + "ufmt", +] + [[package]] name = "is-terminal" version = "0.4.7" @@ -1156,37 +1187,6 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" -[[package]] -name = "narodmon-server" -version = "0.1.0" -dependencies = [ - "anyhow", - "bstr", - "bytes", - "chrono", - "clap", - "derive_more", - "dotenvy", - "fred", - "heapless", - "hex", - "hifitime", - "lazy_static", - "nom", - "ntex", - "phf", - "regex", - "rust_decimal", - "serde", - "serde_json", - "serde_qs", - "serde_with", - "smallstr", - "thiserror", - "tokio", - "ufmt", -] - [[package]] name = "nom" version = "7.1.3" diff --git a/Cargo.toml b/Cargo.toml index a909712..00757bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "narodmon-server" +name = "iotishnik-server" version = "0.1.0" edition = "2021" diff --git a/README.md b/README.md new file mode 100644 index 0000000..c0e2290 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# IoTishnik +Платформа для обработки данных окружающей среды с IoT устройств. + +## Описание идеи +> ### Что есть сейчас: +> +> Есть куча девайсов, которые сейчас со своих сенсоров, для публичного и в некоторых случаях частного использования выкладывают на сервер narodmon.ru данные. +> +> ### В чём проблема narodmon.ru: +> +>1. Там всё такоееее легаси, что ты не представляешь. Речь идёт о cgi скриптах написанных на bash. +>2. Разрабы ищут помощь в виде лиц, в которых можно плевать без последствий сколько угодно. +>3. Отсутствие тех поддержки для обычных пользователей от слову совсем. +>4. Ужасное API которое ужасное не только из-за своей кривизны. В нём нужно отправлять телеметрию устройств, админ панель разработчика считай отсутствует. О OAuth2 вообще говорить не стоит. И если ты отправишь неправильные запросы с клиента своего, твой "api ключ" приложения заблокируют, и пофиг на то, что его можно спиздить с чужих приложений. +>5. Интерфейс просто ужас. Можешь сам зайти и посмотреть, даже писать про это не буду. Даже сам UX в изоляции от UI ужасный. +>6. Документация API не соблюдается. Даже когда мы с Андреем написали, что, мол, у вас поля изменились, им было плевать. +>7. Работа с приватными датчиками ужастная и требует денег для того, чтобы оно вообще работало. +>8. Некоторые очень важные API для устройств (такие как MQTT, на которых работают большое количество готовых продуктов не заточенные под narodmon.ru) доступны тоже только по подписке разрабам. +> +> ### Наше решение: +> +> Сделать с нуля своё решение, которое будет горизонтально масштабируемое, с поддержкой старых API для поддержки устройств сделанных под narodmon.ru . Выдать разрабам которые хотят новое и мощное API это API вместе с SDK в виде либ под ардуинку и т.п. Сделать поддержку OAuth для входа используя чужие сервисы и не только. И многое другое что я мб забыл упомянуть \ No newline at end of file diff --git a/src/ingest_protocol/mod.rs b/src/ingest_protocol/mod.rs index 7ea31e5..627fa9f 100644 --- a/src/ingest_protocol/mod.rs +++ b/src/ingest_protocol/mod.rs @@ -1,3 +1,5 @@ +//! Модуль для парсинга всего, что связанно с данными от устройств. + pub mod error; mod packet_types; pub mod parser; diff --git a/src/ingest_protocol/packet_types.rs b/src/ingest_protocol/packet_types.rs index 902964b..a8538cd 100644 --- a/src/ingest_protocol/packet_types.rs +++ b/src/ingest_protocol/packet_types.rs @@ -1,4 +1,12 @@ -use crate::hashes::SupportedUnit; +//! Сборник типов для внутренней обработки данных с датчиков. +//! +//! Предполагается, что struct-ы будут совместимы с JSON API для передачи данных датчиков. + +// Не забывайте про: +// #[serde(rename = "...")] +// #[serde(alias = "...")] + +use crate::utils::SupportedUnit; use crate::ingest_protocol::error::Error; use crate::ingest_protocol::parser::parse_mac_address; @@ -11,6 +19,17 @@ use serde_with::serde_as; use std::collections::HashSet; use std::hash::{Hash, Hasher}; +/// Данные с одного датчика. +/// +/// Основной идентификатор - поле `mac`. В случае с [crate::web_server::old_device_sensor_api], +/// `mac` может быть не полем, а названием ключа в параметрах. Из-за двусмысленности документации +/// NarodMon, `mac` может означать EUI-48 совместимый MAC адрес, или же просто +/// уникальный идентификатор. +/// +/// Парсинг этих данных отличается в разных транспортных протоколах. +/// Для HTTP /post или /get: см. [crate::web_server::old_device_sensor_api] +/// Для TCP/UDP: TODO +/// Для MQTT: TODO #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct SensorValue { pub mac: String, @@ -26,14 +45,9 @@ impl Hash for SensorValue { } } -pub struct DashSeparator {} - -impl Separator for DashSeparator { - fn separator() -> &'static str { - "-" - } -} - +/// Функция-помощник для [MacAsArray], предназначенный для использования с [serde_with]. +/// +/// Преобразует MAC-адрес. fn mac_as_array(value: &str) -> Result<[u8; 6], Error<&str>> { Ok(parse_mac_address(value)?.1) } diff --git a/src/main.rs b/src/main.rs index 12c99ed..5539a55 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,69 +1,13 @@ -/* -Три датчика реалтайм: -#26-94-1D-75-C2-F8 -#T1#6.93 -#H1#21 -#P1#700.91 -## +#![doc = include_str!("../README.md")] -Формат пакета данных: -#MAC[#NAME]\n -#mac1#value1[#time1][#name1]\n -... -#macN#valueN[#timeN][#nameN]\n -## - -Загрузка истории показаний: -#26-94-1D-75-C2-F8 -#T1#6.93#1687006667 -#T1#10.17#1687006067 -#T1#27.26#1687005467 -## - -C названием и координатами: -#26-94-1D-75-C2-F8#Метео -#OWNER#nm17 -#T1#6.93#Улица -#T2#27.26#Дом -#P1#700.91#Барометр -#LAT#54.308997 -#LON#48.395861 -#ALT#233 -## - */ #![feature(try_blocks)] extern crate core; -mod hashes; +mod utils; mod ingest_protocol; mod web_server; - - - - - - - - - - - - - use crate::web_server::server_main; - -/*fn parse_sensor_value(input: Vec<&str>) -> MyIError, NarodMonValues> { - Ok( - (input, NarodMonValues { - mac: Default::default(), - value: Default::default(), - time: None, - name: None, - }) - ) -}*/ - struct Params {} #[ntex::main] diff --git a/src/hashes.rs b/src/utils.rs similarity index 78% rename from src/hashes.rs rename to src/utils.rs index 77fdb8c..0f4e19a 100644 --- a/src/hashes.rs +++ b/src/utils.rs @@ -1,11 +1,20 @@ +//! Глобальный модуль для вспомогательных типов и утилит. +//! + + use phf::phf_map; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::borrow::Cow; + +/// Поддерживаемые типы. +/// +/// TODO: Решить необходимо ли к данным прикреплять единицы измерения. +/// TODO: Сейчас вообще сомнительно оставлять ли это или нет. #[derive(Debug, Clone, Hash, PartialEq, Eq)] #[repr(u64)] pub enum SupportedUnit { - Celsius, // Needs verification > 273.15 + Celsius, // Needs verification > -273.15 Percentage, // Needs verification >= 100 && <= 0 MillimeterHg, // Needs verification UVIndex, // Needs verification @@ -56,6 +65,7 @@ impl<'de> Deserialize<'de> for SupportedUnit { } } +/// Таблица преобразования текстового представления единиц в значения [SupportedUnit]. static STR_TO_UNITS: phf::Map<&'static str, SupportedUnit> = phf_map! { "C" => SupportedUnit::Celsius, "%" => SupportedUnit::Percentage, diff --git a/src/web_server/app_error.rs b/src/web_server/app_error.rs index 5195244..aa9ea10 100644 --- a/src/web_server/app_error.rs +++ b/src/web_server/app_error.rs @@ -12,11 +12,14 @@ use thiserror::Error; use crate::insert_header; -use crate::web_server::old_device_sensor_api::QSParserError; +use crate::web_server::old_device_sensor_api::qs_parser::QSParserError; use rust_decimal::Decimal; use serde_json::json; +/// Главный объект ошибки [std::error::Error] для всего Web API. +/// +/// В целом, все Result у Web сервера должны использовать этот Error. #[derive(Debug, Error, Display)] pub enum AppError { #[display(fmt = "IDK")] diff --git a/src/web_server/mod.rs b/src/web_server/mod.rs index bcc3d93..8e591d0 100644 --- a/src/web_server/mod.rs +++ b/src/web_server/mod.rs @@ -1,12 +1,19 @@ -use crate::web_server::old_app_api::old_api_handler; +//! Модуль веб сервера. +//! +//! Все модули отвечают только за Web сторону. Такие вещи как +//! [crate::web_server::old_device_sensor_api] отвечают только за веб версию. +//! +//! TODO: Начать работу над TCP/UDP и MQTT сервером + +use old_app_api::old_api_handler; use fred::bytes_utils::Str; use fred::prelude::*; -pub(crate) mod app_error; +pub mod app_error; pub mod old_app_api; -mod old_device_sensor_api; +pub mod old_device_sensor_api; pub mod utils; #[derive(Clone)] diff --git a/src/web_server/old_app_api/config_app.rs b/src/web_server/old_app_api/config_app.rs deleted file mode 100644 index 961c9b9..0000000 --- a/src/web_server/old_app_api/config_app.rs +++ /dev/null @@ -1,45 +0,0 @@ - - -use crate::web_server::app_error::AppError; -use crate::web_server::old_app_api::handlers::{app_init, version}; -use crate::web_server::old_app_api::types::{AppInitRequest, MandatoryParams}; -use crate::web_server::utils::redis::is_api_key_valid; -use crate::web_server::NMAppState; -use nom::AsBytes; -use ntex::util::Bytes; -use ntex::web; -use ntex::web::types::State; - - -pub async fn old_api_handler( - app_state: State, - body_bytes: Bytes, -) -> Result { - if body_bytes.len() > 10 * 1024 { - // 10 KiB - return Err(AppError::RequestTooLarge); - } - - let body_bytes = body_bytes.as_bytes(); - - let mandatory_params: MandatoryParams<'_> = serde_json::from_slice(body_bytes)?; // TODO: Simd-JSON - - // Ignore clippy singlematch - match mandatory_params.cmd.as_ref() { - "version" => return version((), &app_state).await, - _ => {} - } - - is_api_key_valid(&app_state.redis_client, mandatory_params.api_key.as_ref()).await?; - - match mandatory_params.cmd.as_ref() { - "appInit" => { - let body: AppInitRequest = serde_json::from_slice(body_bytes)?; - - app_init(body, &app_state).await - } - _ => Err(AppError::UnknownMethod(mandatory_params.cmd.to_string())), - } - - //Ok("fuck") -} diff --git a/src/web_server/old_app_api/handlers/methods.rs b/src/web_server/old_app_api/handlers/methods.rs deleted file mode 100644 index e1a9893..0000000 --- a/src/web_server/old_app_api/handlers/methods.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::web_server::app_error::AppError; -use crate::web_server::old_app_api::types::AppInitRequest; -use crate::web_server::NMAppState; - -use serde_json::{json}; - - -use crate::insert_header; -use fred::interfaces::KeysInterface; -use ntex::http::StatusCode; -use ntex::web; - - - -pub async fn app_init( - _body: AppInitRequest<'_>, - app_state: &NMAppState, -) -> Result { - let _: () = app_state - .redis_client - .set("test", 123, None, None, true) - .await?; - - Ok(web::HttpResponse::build(StatusCode::OK).body("Hello world!")) -} - -pub async fn version(_body: (), _app_state: &NMAppState) -> Result { - let mut resp = web::HttpResponse::build(StatusCode::OK).json(&json!({ - "version": "indev", - "iotishnik": true - })); - - insert_header!(resp.headers_mut(), "Cache-Control", "no-cache"); - - Ok(resp) -} diff --git a/src/web_server/old_app_api/handlers/mod.rs b/src/web_server/old_app_api/handlers/mod.rs index 9cb9400..e1a9893 100644 --- a/src/web_server/old_app_api/handlers/mod.rs +++ b/src/web_server/old_app_api/handlers/mod.rs @@ -1,7 +1,36 @@ -mod methods; +use crate::web_server::app_error::AppError; +use crate::web_server::old_app_api::types::AppInitRequest; +use crate::web_server::NMAppState; + +use serde_json::{json}; -pub use methods::*; +use crate::insert_header; +use fred::interfaces::KeysInterface; +use ntex::http::StatusCode; +use ntex::web; +pub async fn app_init( + _body: AppInitRequest<'_>, + app_state: &NMAppState, +) -> Result { + let _: () = app_state + .redis_client + .set("test", 123, None, None, true) + .await?; + + Ok(web::HttpResponse::build(StatusCode::OK).body("Hello world!")) +} + +pub async fn version(_body: (), _app_state: &NMAppState) -> Result { + let mut resp = web::HttpResponse::build(StatusCode::OK).json(&json!({ + "version": "indev", + "iotishnik": true + })); + + insert_header!(resp.headers_mut(), "Cache-Control", "no-cache"); + + Ok(resp) +} diff --git a/src/web_server/old_app_api/mod.rs b/src/web_server/old_app_api/mod.rs index c1572dd..6abc3f3 100644 --- a/src/web_server/old_app_api/mod.rs +++ b/src/web_server/old_app_api/mod.rs @@ -1,5 +1,53 @@ -mod config_app; +//! Модуль обработки данных для приложений конечных пользователей, которые используют старый API. + mod handlers; mod types; -pub use config_app::old_api_handler; +use ntex::web::types::State; +use ntex::util::Bytes; +use ntex::web; +use nom::AsBytes; +use crate::web_server::app_error::AppError; +use crate::web_server::NMAppState; +use crate::web_server::old_app_api::handlers::{app_init, version}; +use crate::web_server::old_app_api::types::{AppInitRequest, MandatoryParams}; +use crate::web_server::utils::redis::is_api_key_valid; + + +/// Обработчик запросов от приложений. +/// +/// Отвечает за разделение на функции по `cmd`. +/// +/// Вызывается напрямую из ntex приложения. +pub async fn old_api_handler( + app_state: State, + body_bytes: Bytes, +) -> Result { + if body_bytes.len() > 10 * 1024 { + // 10 KiB + return Err(AppError::RequestTooLarge); + } + + let body_bytes = body_bytes.as_bytes(); + + let mandatory_params: MandatoryParams<'_> = serde_json::from_slice(body_bytes)?; // TODO: Simd-JSON + + // Ignore clippy singlematch + match mandatory_params.cmd.as_ref() { + "version" => return version((), &app_state).await, + _ => {} + } + + is_api_key_valid(&app_state.redis_client, mandatory_params.api_key.as_ref()).await?; + + match mandatory_params.cmd.as_ref() { + "appInit" => { + let body: AppInitRequest = serde_json::from_slice(body_bytes)?; + + app_init(body, &app_state).await + } + _ => Err(AppError::UnknownMethod(mandatory_params.cmd.to_string())), + } + + //Ok("fuck") +} diff --git a/src/web_server/old_app_api/types/mod.rs b/src/web_server/old_app_api/types/mod.rs index 31d83e3..de9b9bb 100644 --- a/src/web_server/old_app_api/types/mod.rs +++ b/src/web_server/old_app_api/types/mod.rs @@ -1,4 +1,3 @@ - use serde::{Deserialize, Serialize}; use std::borrow::Cow; @@ -24,6 +23,11 @@ pub struct AddLikeRequest { pub version: u64, } +/// Обязательные параметры у JSON app API. +/// +/// При обработке входящих данных производиться два запроса [serde_json::from_str]. Один вызов пытается +/// получить [MandatoryParams], другой в зависимости от `cmd`. Это позволяет не добвалять поля из +/// этой структуры в каждом специфичном типе. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct MandatoryParams<'a> { #[serde(borrow)] @@ -32,6 +36,11 @@ pub struct MandatoryParams<'a> { #[serde(borrow)] pub lang: Cow<'a, str>, + /// Уникальный ID клиента. + /// + /// Используется на подобии как куки PHPSESSID в php. + /// + /// См. также: #[serde(borrow)] pub uuid: Cow<'a, str>, diff --git a/src/web_server/old_device_sensor_api/mod.rs b/src/web_server/old_device_sensor_api/mod.rs index e0bb540..fe109eb 100644 --- a/src/web_server/old_device_sensor_api/mod.rs +++ b/src/web_server/old_device_sensor_api/mod.rs @@ -1,4 +1,6 @@ -mod qs_parser; +//! Модуль обработки данных с устройств, которые используют старый API. + +pub mod qs_parser; use std::str::FromStr; use crate::ingest_protocol::{NMDeviceDataPacket, NMJsonPacket}; @@ -13,6 +15,7 @@ use hifitime::Epoch; use thiserror::Error; use ufmt::uwrite; use crate::web_server::NMAppState; +use crate::web_server::old_device_sensor_api::qs_parser::QSParserError; #[derive(Error, Debug)] pub enum Error { @@ -20,9 +23,17 @@ pub enum Error { DeviceNotFound(String), #[error("Time sent with the device is way to behind now")] TimeIsLongBehindNow, + #[error("{0}")] + QSParserError(#[from] QSParserError) } +/// Обработчик данных датчиков с устройств. +/// +/// Слушает /post и /get. +/// Для того чтобы пользователям было легче, на оба пути можно отправлять и POST и GET. +/// +/// На POST можно отправлять JSON или url-encoded тело, на GET - только через Query String. pub async fn device_handler<'a>( request: web::HttpRequest, body: Bytes, diff --git a/src/web_server/old_device_sensor_api/qs_parser.rs b/src/web_server/old_device_sensor_api/qs_parser.rs index d517356..1bf1e3e 100644 --- a/src/web_server/old_device_sensor_api/qs_parser.rs +++ b/src/web_server/old_device_sensor_api/qs_parser.rs @@ -40,6 +40,11 @@ impl From for QSParserError { } } +/// Преобразование оставшихся параметров в urlencoded теле или query string в данные с датчиков +/// [SensorValue]. +/// +/// Формат: `=`. +/// Других данных на подобии названия и времени нет. pub fn qs_rest_to_values(parsed: HashMap) -> Result, QSParserError> { let mut hashset = HashSet::new(); diff --git a/src/web_server/utils/mod.rs b/src/web_server/utils/mod.rs index 5fed630..672afe1 100644 --- a/src/web_server/utils/mod.rs +++ b/src/web_server/utils/mod.rs @@ -1,3 +1,6 @@ +//! Сборник полезных функций которые используются в многих местах одновременно или слишком +//! неспециализированные. + pub mod redis; #[macro_export] diff --git a/src/web_server/utils/redis.rs b/src/web_server/utils/redis.rs index fe6a9da..cbb6a46 100644 --- a/src/web_server/utils/redis.rs +++ b/src/web_server/utils/redis.rs @@ -1,3 +1,5 @@ +//! Сборник утилит для работы с Redis. + use crate::web_server::app_error::AppError; use fred::prelude::*; use heapless::String as HeaplessString; @@ -6,13 +8,17 @@ use regex::Regex; use ufmt::uwrite; lazy_static! { + /// Разрешённые знаки для API ключа. static ref ALLOWED_API_KEY_CHARACTERS: Regex = Regex::new("[a-zA-Z0-9]{13}").unwrap(); } +/// Описание полей в KV DB у `apikey_{}`. pub struct ApiKeyDescription { + /// ID владельца API ключа. apikey_owner: i64, } +/// Проверка API ключа на валидность. pub async fn is_api_key_valid( client: &RedisClient, api_key: &str, From de396ed05f8ac9dec7a3edaa0b4ae5a847e49462 Mon Sep 17 00:00:00 2001 From: nm17 Date: Thu, 30 May 2024 14:28:42 +0400 Subject: [PATCH 3/3] feat(old_device_sensor_api): commit to a different key naming convention --- Cargo.toml | 4 +-- docs/kv_db_arch.md | 5 ++- src/ingest_protocol/packet_types.rs | 1 - src/ingest_protocol/parser.rs | 1 - src/web_server/app_error.rs | 10 +++--- src/web_server/old_device_sensor_api/mod.rs | 40 ++++++++++++--------- 6 files changed, 34 insertions(+), 27 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 00757bd..e76204a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ derive_more = "0.99.17" dotenvy = "0.15.7" fred = { version = "6.3.0", features = ["nom"] } heapless = { version = "0.7.16", features = ["ufmt-impl"] } -hex = { version = "0.4.3", default-features = false } +hex = { version = "0.4.3", default-features = false, features = ["std", "alloc"] } hifitime = "3.8.2" lazy_static = "1.4.0" nom = { version = "7.1.3", default-features = false, features = ["std", "alloc"] } @@ -30,4 +30,4 @@ serde_with = { version = "3.6.1", features = ["hex"] } smallstr = { version = "0.3.0", features = ["std", "union"] } thiserror = "1.0.40" tokio = { version = "1.28.2", features = ["full"] } -ufmt = "0.2.0" +ufmt = { version = "0.2.0", features = ["std"] } diff --git a/docs/kv_db_arch.md b/docs/kv_db_arch.md index 0f35c79..74a3cff 100644 --- a/docs/kv_db_arch.md +++ b/docs/kv_db_arch.md @@ -8,9 +8,8 @@ - Поля - Вся информация о девайсе - `devices_{device_id}_{tai_timestamp}_{sensor_id}` - - Поля - - `value` - - `unit` + - Только значение - `devices_{device_id}` - Поля - `exists`: bool + - `unit`: str diff --git a/src/ingest_protocol/packet_types.rs b/src/ingest_protocol/packet_types.rs index a8538cd..e1c07b5 100644 --- a/src/ingest_protocol/packet_types.rs +++ b/src/ingest_protocol/packet_types.rs @@ -13,7 +13,6 @@ use crate::ingest_protocol::parser::parse_mac_address; use hifitime::Epoch; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; -use serde_with::formats::Separator; use serde_with::serde_as; use std::collections::HashSet; diff --git a/src/ingest_protocol/parser.rs b/src/ingest_protocol/parser.rs index 2b4fdfe..ffc4133 100644 --- a/src/ingest_protocol/parser.rs +++ b/src/ingest_protocol/parser.rs @@ -3,7 +3,6 @@ use nom::bytes::complete::tag; use nom::bytes::complete::take_until1; use nom::bytes::complete::{take, take_while, take_while1}; use nom::character::complete::hex_digit1; -use nom::{Parser}; use std::str::FromStr; use crate::ingest_protocol::error::Error; diff --git a/src/web_server/app_error.rs b/src/web_server/app_error.rs index aa9ea10..38bcad5 100644 --- a/src/web_server/app_error.rs +++ b/src/web_server/app_error.rs @@ -1,16 +1,13 @@ use derive_more::Display; use fred::prelude::*; -use ntex::http::{StatusCode}; +use ntex::http::StatusCode; use ntex::web; use ntex::web::{HttpRequest, HttpResponse}; - - use thiserror::Error; - use crate::insert_header; use crate::web_server::old_device_sensor_api::qs_parser::QSParserError; use rust_decimal::Decimal; @@ -51,6 +48,9 @@ pub enum AppError { json_err: Option, query_error: Option, }, + + #[display(fmt = "IDK")] + DeviceNotFound(String) } impl web::error::WebResponseError for AppError { @@ -64,6 +64,7 @@ impl web::error::WebResponseError for AppError { AppError::ApiKeyInvalid { .. } => StatusCode::BAD_REQUEST, AppError::UnknownBody { .. } => StatusCode::BAD_REQUEST, AppError::QSError(..) => StatusCode::BAD_REQUEST, + AppError::DeviceNotFound(..) => StatusCode::BAD_REQUEST } } @@ -79,6 +80,7 @@ impl web::error::WebResponseError for AppError { "Can't figure out where and in what encoding the main data is" } AppError::QSError(..) => "UrlEncoded body or query params are incorrect", + AppError::DeviceNotFound(..) => "Device not found", }; let status_code = self.status_code(); diff --git a/src/web_server/old_device_sensor_api/mod.rs b/src/web_server/old_device_sensor_api/mod.rs index fe109eb..9a140df 100644 --- a/src/web_server/old_device_sensor_api/mod.rs +++ b/src/web_server/old_device_sensor_api/mod.rs @@ -2,16 +2,17 @@ pub mod qs_parser; -use std::str::FromStr; use crate::ingest_protocol::{NMDeviceDataPacket, NMJsonPacket}; use crate::web_server::app_error::AppError; +use fred::types::RedisMap; use ntex::http::{HttpMessage, StatusCode}; use ntex::util::{Bytes, HashMap}; use ntex::{http, web}; use fred::prelude::*; use hifitime::Epoch; +use ntex::web::types::State; use thiserror::Error; use ufmt::uwrite; use crate::web_server::NMAppState; @@ -21,10 +22,10 @@ use crate::web_server::old_device_sensor_api::qs_parser::QSParserError; pub enum Error { #[error("Device not found")] DeviceNotFound(String), - #[error("Time sent with the device is way to behind now")] + #[error("Time sent with the device is way too behind now")] TimeIsLongBehindNow, #[error("{0}")] - QSParserError(#[from] QSParserError) + QSParserError(#[from] QSParserError), } @@ -37,7 +38,7 @@ pub enum Error { pub async fn device_handler<'a>( request: web::HttpRequest, body: Bytes, - app_state: &NMAppState, + app_state: State, ) -> Result { let mut real_body: Option = None; let mut json_error = None; @@ -79,30 +80,36 @@ pub async fn device_handler<'a>( for device in real_body.devices { let mut device_key_str = String::new(); - let device_tai_timestamp = device.time - .unwrap_or(Epoch::now().unwrap()) + let now = Epoch::now().unwrap(); + let mut device_time = device.time + .unwrap_or(now); + + // TODO: Добавить гистерезис + // Отчёт совместимости: отсутствует + if device_time > now { + device_time = now; + } + + let device_tai_timestamp = device_time .to_duration_since_j1900() .to_seconds(); - uwrite!(&mut device_key_str, "devices_{}", device.mac)?; - + uwrite!(device_key_str, "devices_{}", hex::encode(device.mac)); let device_exists: bool = app_state.redis_client.hget(device_key_str.as_str(), "exists").await?; if !device_exists { - return Err(Error::DeviceNotFound(device.mac)) + return Err(AppError::DeviceNotFound(hex::encode(&device.mac))); } // devices_{device_id}_{tai_timestamp}_{sensor_id} for sensor in device.values { let mut device_report_key_str = String::new(); - uwrite!(&mut device_report_key_str, "devices_{}_{}_{}", device.mac, device_tai_timestamp, sensor.mac)?; - let mut device_report_key_str = String::new(); + uwrite!(&mut device_report_key_str, "devices_{}_{}_{}", hex::encode(device.mac), device_tai_timestamp.to_string(), sensor.mac); - - app_state.redis_client.hset(device_key_str.as_str(), HashMap::from([ - // TODO - ])).await?; + app_state.redis_client.set( + device_key_str.as_str(), sensor.value.to_string(), None, None, false, + ).await?; } } } else { @@ -112,5 +119,6 @@ pub async fn device_handler<'a>( }); } - Ok(web::HttpResponse::build(StatusCode::OK).finish()) + Ok(web::HttpResponseBuilder::new(StatusCode::OK) + .finish()) }