dev/nm17 #2

Merged
nm17 merged 3 commits from dev/nm17 into master 2024-05-30 13:33:18 +03:00
18 changed files with 223 additions and 191 deletions
Showing only changes of commit e4287bb2ac - Show all commits

62
Cargo.lock generated
View file

@ -984,6 +984,37 @@ dependencies = [
"windows-sys 0.48.0", "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]] [[package]]
name = "is-terminal" name = "is-terminal"
version = "0.4.7" version = "0.4.7"
@ -1156,37 +1187,6 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" 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]] [[package]]
name = "nom" name = "nom"
version = "7.1.3" version = "7.1.3"

View file

@ -1,5 +1,5 @@
[package] [package]
name = "narodmon-server" name = "iotishnik-server"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"

22
README.md Normal file
View file

@ -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 для входа используя чужие сервисы и не только. И многое другое что я мб забыл упомянуть

View file

@ -1,3 +1,5 @@
//! Модуль для парсинга всего, что связанно с данными от устройств.
pub mod error; pub mod error;
mod packet_types; mod packet_types;
pub mod parser; pub mod parser;

View file

@ -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::error::Error;
use crate::ingest_protocol::parser::parse_mac_address; use crate::ingest_protocol::parser::parse_mac_address;
@ -11,6 +19,17 @@ use serde_with::serde_as;
use std::collections::HashSet; use std::collections::HashSet;
use std::hash::{Hash, Hasher}; 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)] #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SensorValue { pub struct SensorValue {
pub mac: String, pub mac: String,
@ -26,14 +45,9 @@ impl Hash for SensorValue {
} }
} }
pub struct DashSeparator {} /// Функция-помощник для [MacAsArray], предназначенный для использования с [serde_with].
///
impl Separator for DashSeparator { /// Преобразует MAC-адрес.
fn separator() -> &'static str {
"-"
}
}
fn mac_as_array(value: &str) -> Result<[u8; 6], Error<&str>> { fn mac_as_array(value: &str) -> Result<[u8; 6], Error<&str>> {
Ok(parse_mac_address(value)?.1) Ok(parse_mac_address(value)?.1)
} }

View file

@ -1,69 +1,13 @@
/* #![doc = include_str!("../README.md")]
Три датчика реалтайм:
#26-94-1D-75-C2-F8
#T1#6.93
#H1#21
#P1#700.91
##
Формат пакета данных:
#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)] #![feature(try_blocks)]
extern crate core; extern crate core;
mod hashes; mod utils;
mod ingest_protocol; mod ingest_protocol;
mod web_server; mod web_server;
use crate::web_server::server_main; use crate::web_server::server_main;
/*fn parse_sensor_value(input: Vec<&str>) -> MyIError<Vec<&str>, NarodMonValues> {
Ok(
(input, NarodMonValues {
mac: Default::default(),
value: Default::default(),
time: None,
name: None,
})
)
}*/
struct Params {} struct Params {}
#[ntex::main] #[ntex::main]

View file

@ -1,11 +1,20 @@
//! Глобальный модуль для вспомогательных типов и утилит.
//!
use phf::phf_map; use phf::phf_map;
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::borrow::Cow; use std::borrow::Cow;
/// Поддерживаемые типы.
///
/// TODO: Решить необходимо ли к данным прикреплять единицы измерения.
/// TODO: Сейчас вообще сомнительно оставлять ли это или нет.
#[derive(Debug, Clone, Hash, PartialEq, Eq)] #[derive(Debug, Clone, Hash, PartialEq, Eq)]
#[repr(u64)] #[repr(u64)]
pub enum SupportedUnit { pub enum SupportedUnit {
Celsius, // Needs verification > 273.15 Celsius, // Needs verification > -273.15
Percentage, // Needs verification >= 100 && <= 0 Percentage, // Needs verification >= 100 && <= 0
MillimeterHg, // Needs verification MillimeterHg, // Needs verification
UVIndex, // 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! { static STR_TO_UNITS: phf::Map<&'static str, SupportedUnit> = phf_map! {
"C" => SupportedUnit::Celsius, "C" => SupportedUnit::Celsius,
"%" => SupportedUnit::Percentage, "%" => SupportedUnit::Percentage,

View file

@ -12,11 +12,14 @@ use thiserror::Error;
use crate::insert_header; 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 rust_decimal::Decimal;
use serde_json::json; use serde_json::json;
/// Главный объект ошибки [std::error::Error] для всего Web API.
///
/// В целом, все Result у Web сервера должны использовать этот Error.
#[derive(Debug, Error, Display)] #[derive(Debug, Error, Display)]
pub enum AppError { pub enum AppError {
#[display(fmt = "IDK")] #[display(fmt = "IDK")]

View file

@ -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::bytes_utils::Str;
use fred::prelude::*; use fred::prelude::*;
pub(crate) mod app_error; pub mod app_error;
pub mod old_app_api; pub mod old_app_api;
mod old_device_sensor_api; pub mod old_device_sensor_api;
pub mod utils; pub mod utils;
#[derive(Clone)] #[derive(Clone)]

View file

@ -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<NMAppState>,
body_bytes: Bytes,
) -> Result<impl web::Responder, AppError> {
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")
}

View file

@ -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<web::HttpResponse, AppError> {
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<web::HttpResponse, AppError> {
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)
}

View file

@ -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<web::HttpResponse, AppError> {
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<web::HttpResponse, AppError> {
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)
}

View file

@ -1,5 +1,53 @@
mod config_app; //! Модуль обработки данных для приложений конечных пользователей, которые используют старый API.
mod handlers; mod handlers;
mod types; 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<NMAppState>,
body_bytes: Bytes,
) -> Result<impl web::Responder, AppError> {
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")
}

View file

@ -1,4 +1,3 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::borrow::Cow; use std::borrow::Cow;
@ -24,6 +23,11 @@ pub struct AddLikeRequest {
pub version: u64, pub version: u64,
} }
/// Обязательные параметры у JSON app API.
///
/// При обработке входящих данных производиться два запроса [serde_json::from_str]. Один вызов пытается
/// получить [MandatoryParams], другой в зависимости от `cmd`. Это позволяет не добвалять поля из
/// этой структуры в каждом специфичном типе.
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MandatoryParams<'a> { pub struct MandatoryParams<'a> {
#[serde(borrow)] #[serde(borrow)]
@ -32,6 +36,11 @@ pub struct MandatoryParams<'a> {
#[serde(borrow)] #[serde(borrow)]
pub lang: Cow<'a, str>, pub lang: Cow<'a, str>,
/// Уникальный ID клиента.
///
/// Используется на подобии как куки PHPSESSID в php.
///
/// См. также: <https://www.php.net/manual/en/book.session.php>
#[serde(borrow)] #[serde(borrow)]
pub uuid: Cow<'a, str>, pub uuid: Cow<'a, str>,

View file

@ -1,4 +1,6 @@
mod qs_parser; //! Модуль обработки данных с устройств, которые используют старый API.
pub mod qs_parser;
use std::str::FromStr; use std::str::FromStr;
use crate::ingest_protocol::{NMDeviceDataPacket, NMJsonPacket}; use crate::ingest_protocol::{NMDeviceDataPacket, NMJsonPacket};
@ -13,6 +15,7 @@ use hifitime::Epoch;
use thiserror::Error; use thiserror::Error;
use ufmt::uwrite; use ufmt::uwrite;
use crate::web_server::NMAppState; use crate::web_server::NMAppState;
use crate::web_server::old_device_sensor_api::qs_parser::QSParserError;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum Error { pub enum Error {
@ -20,9 +23,17 @@ pub enum Error {
DeviceNotFound(String), DeviceNotFound(String),
#[error("Time sent with the device is way to behind now")] #[error("Time sent with the device is way to behind now")]
TimeIsLongBehindNow, TimeIsLongBehindNow,
#[error("{0}")]
QSParserError(#[from] QSParserError)
} }
/// Обработчик данных датчиков с устройств.
///
/// Слушает /post и /get.
/// Для того чтобы пользователям было легче, на оба пути можно отправлять и POST и GET.
///
/// На POST можно отправлять JSON или url-encoded тело, на GET - только через Query String.
pub async fn device_handler<'a>( pub async fn device_handler<'a>(
request: web::HttpRequest, request: web::HttpRequest,
body: Bytes, body: Bytes,

View file

@ -40,6 +40,11 @@ impl From<serde_qs::Error> for QSParserError {
} }
} }
/// Преобразование оставшихся параметров в urlencoded теле или query string в данные с датчиков
/// [SensorValue].
///
/// Формат: `<SENSOR_MAC>=<SENSOR_VALUE>`.
/// Других данных на подобии названия и времени нет.
pub fn qs_rest_to_values(parsed: HashMap<String, String>) -> Result<HashSet<SensorValue>, QSParserError> { pub fn qs_rest_to_values(parsed: HashMap<String, String>) -> Result<HashSet<SensorValue>, QSParserError> {
let mut hashset = HashSet::new(); let mut hashset = HashSet::new();

View file

@ -1,3 +1,6 @@
//! Сборник полезных функций которые используются в многих местах одновременно или слишком
//! неспециализированные.
pub mod redis; pub mod redis;
#[macro_export] #[macro_export]

View file

@ -1,3 +1,5 @@
//! Сборник утилит для работы с Redis.
use crate::web_server::app_error::AppError; use crate::web_server::app_error::AppError;
use fred::prelude::*; use fred::prelude::*;
use heapless::String as HeaplessString; use heapless::String as HeaplessString;
@ -6,13 +8,17 @@ use regex::Regex;
use ufmt::uwrite; use ufmt::uwrite;
lazy_static! { lazy_static! {
/// Разрешённые знаки для API ключа.
static ref ALLOWED_API_KEY_CHARACTERS: Regex = Regex::new("[a-zA-Z0-9]{13}").unwrap(); static ref ALLOWED_API_KEY_CHARACTERS: Regex = Regex::new("[a-zA-Z0-9]{13}").unwrap();
} }
/// Описание полей в KV DB у `apikey_{}`.
pub struct ApiKeyDescription { pub struct ApiKeyDescription {
/// ID владельца API ключа.
apikey_owner: i64, apikey_owner: i64,
} }
/// Проверка API ключа на валидность.
pub async fn is_api_key_valid( pub async fn is_api_key_valid(
client: &RedisClient, client: &RedisClient,
api_key: &str, api_key: &str,