Merge pull request 'dev/nm17' (#2) from dev/nm17 into master
Reviewed-on: nm17/iotishnik-server#2
This commit is contained in:
commit
dc81c21ea9
20 changed files with 353 additions and 265 deletions
62
Cargo.lock
generated
62
Cargo.lock
generated
|
@ -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"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
[package]
|
[package]
|
||||||
name = "narodmon-server"
|
name = "iotishnik-server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ derive_more = "0.99.17"
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
fred = { version = "6.3.0", features = ["nom"] }
|
fred = { version = "6.3.0", features = ["nom"] }
|
||||||
heapless = { version = "0.7.16", features = ["ufmt-impl"] }
|
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"
|
hifitime = "3.8.2"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
nom = { version = "7.1.3", default-features = false, features = ["std", "alloc"] }
|
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"] }
|
smallstr = { version = "0.3.0", features = ["std", "union"] }
|
||||||
thiserror = "1.0.40"
|
thiserror = "1.0.40"
|
||||||
tokio = { version = "1.28.2", features = ["full"] }
|
tokio = { version = "1.28.2", features = ["full"] }
|
||||||
ufmt = "0.2.0"
|
ufmt = { version = "0.2.0", features = ["std"] }
|
||||||
|
|
22
README.md
Normal file
22
README.md
Normal 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 для входа используя чужие сервисы и не только. И многое другое что я мб забыл упомянуть
|
|
@ -8,6 +8,8 @@
|
||||||
- Поля
|
- Поля
|
||||||
- Вся информация о девайсе
|
- Вся информация о девайсе
|
||||||
- `devices_{device_id}_{tai_timestamp}_{sensor_id}`
|
- `devices_{device_id}_{tai_timestamp}_{sensor_id}`
|
||||||
|
- Только значение
|
||||||
|
- `devices_{device_id}`
|
||||||
- Поля
|
- Поля
|
||||||
- `value`
|
- `exists`: bool
|
||||||
- `unit`
|
- `unit`: str
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
//! Модуль для парсинга всего, что связанно с данными от устройств.
|
||||||
|
|
||||||
pub mod error;
|
pub mod error;
|
||||||
mod packet_types;
|
mod packet_types;
|
||||||
pub mod parser;
|
pub mod parser;
|
||||||
|
|
|
@ -1,16 +1,34 @@
|
||||||
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;
|
||||||
|
|
||||||
use hifitime::Epoch;
|
use hifitime::Epoch;
|
||||||
use rust_decimal::Decimal;
|
use rust_decimal::Decimal;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_with::formats::Separator;
|
|
||||||
use serde_with::serde_as;
|
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 +44,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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ use nom::bytes::complete::tag;
|
||||||
use nom::bytes::complete::take_until1;
|
use nom::bytes::complete::take_until1;
|
||||||
use nom::bytes::complete::{take, take_while, take_while1};
|
use nom::bytes::complete::{take, take_while, take_while1};
|
||||||
use nom::character::complete::hex_digit1;
|
use nom::character::complete::hex_digit1;
|
||||||
use nom::{Parser};
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use crate::ingest_protocol::error::Error;
|
use crate::ingest_protocol::error::Error;
|
||||||
|
|
60
src/main.rs
60
src/main.rs
|
@ -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]
|
||||||
|
|
|
@ -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,
|
|
@ -1,22 +1,22 @@
|
||||||
use derive_more::Display;
|
use derive_more::Display;
|
||||||
use fred::prelude::*;
|
use fred::prelude::*;
|
||||||
|
|
||||||
use ntex::http::{StatusCode};
|
use ntex::http::StatusCode;
|
||||||
use ntex::web;
|
use ntex::web;
|
||||||
|
|
||||||
use ntex::web::{HttpRequest, HttpResponse};
|
use ntex::web::{HttpRequest, HttpResponse};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
|
||||||
use crate::insert_header;
|
use crate::insert_header;
|
||||||
use crate::web_server::old_devices_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")]
|
||||||
|
@ -48,6 +48,9 @@ pub enum AppError {
|
||||||
json_err: Option<serde_json::Error>,
|
json_err: Option<serde_json::Error>,
|
||||||
query_error: Option<serde_qs::Error>,
|
query_error: Option<serde_qs::Error>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#[display(fmt = "IDK")]
|
||||||
|
DeviceNotFound(String)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl web::error::WebResponseError for AppError {
|
impl web::error::WebResponseError for AppError {
|
||||||
|
@ -61,6 +64,7 @@ impl web::error::WebResponseError for AppError {
|
||||||
AppError::ApiKeyInvalid { .. } => StatusCode::BAD_REQUEST,
|
AppError::ApiKeyInvalid { .. } => StatusCode::BAD_REQUEST,
|
||||||
AppError::UnknownBody { .. } => StatusCode::BAD_REQUEST,
|
AppError::UnknownBody { .. } => StatusCode::BAD_REQUEST,
|
||||||
AppError::QSError(..) => StatusCode::BAD_REQUEST,
|
AppError::QSError(..) => StatusCode::BAD_REQUEST,
|
||||||
|
AppError::DeviceNotFound(..) => StatusCode::BAD_REQUEST
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,6 +80,7 @@ impl web::error::WebResponseError for AppError {
|
||||||
"Can't figure out where and in what encoding the main data is"
|
"Can't figure out where and in what encoding the main data is"
|
||||||
}
|
}
|
||||||
AppError::QSError(..) => "UrlEncoded body or query params are incorrect",
|
AppError::QSError(..) => "UrlEncoded body or query params are incorrect",
|
||||||
|
AppError::DeviceNotFound(..) => "Device not found",
|
||||||
};
|
};
|
||||||
|
|
||||||
let status_code = self.status_code();
|
let status_code = self.status_code();
|
||||||
|
|
|
@ -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_devices_api;
|
pub mod old_device_sensor_api;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -14,7 +21,7 @@ pub struct NMAppState {
|
||||||
pub redis_client: RedisClient,
|
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;
|
use ntex::web;
|
||||||
|
|
||||||
|
|
|
@ -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")
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
||||||
|
|
124
src/web_server/old_device_sensor_api/mod.rs
Normal file
124
src/web_server/old_device_sensor_api/mod.rs
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
//! Модуль обработки данных с устройств, которые используют старый API.
|
||||||
|
|
||||||
|
pub mod qs_parser;
|
||||||
|
|
||||||
|
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;
|
||||||
|
use crate::web_server::old_device_sensor_api::qs_parser::QSParserError;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("Device not found")]
|
||||||
|
DeviceNotFound(String),
|
||||||
|
#[error("Time sent with the device is way too 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,
|
||||||
|
app_state: State<NMAppState>,
|
||||||
|
) -> Result<web::HttpResponse, AppError> {
|
||||||
|
let mut real_body: Option<NMJsonPacket> = 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::<NMJsonPacket>(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::<NMDeviceDataPacket>(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::<NMDeviceDataPacket>(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 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!(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(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_{}_{}_{}", hex::encode(device.mac), device_tai_timestamp.to_string(), sensor.mac);
|
||||||
|
|
||||||
|
app_state.redis_client.set(
|
||||||
|
device_key_str.as_str(), sensor.value.to_string(), None, None, false,
|
||||||
|
).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(AppError::UnknownBody {
|
||||||
|
json_err: json_error,
|
||||||
|
query_error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(web::HttpResponseBuilder::new(StatusCode::OK)
|
||||||
|
.finish())
|
||||||
|
}
|
|
@ -1,21 +1,14 @@
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::num::ParseFloatError;
|
use std::num::ParseFloatError;
|
||||||
use std::str::FromStr;
|
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 std::sync::Arc;
|
||||||
use fred::bytes_utils::Str;
|
|
||||||
use hifitime::efmt::consts::ISO8601;
|
|
||||||
use hifitime::Epoch;
|
use hifitime::Epoch;
|
||||||
|
use ntex::util::HashMap;
|
||||||
use rust_decimal::Decimal;
|
use rust_decimal::Decimal;
|
||||||
use thiserror::Error;
|
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], но это слишком большая морока
|
/// В иделае было бы хорошо сделать всё как у [serde_json::Error], но это слишком большая морока
|
||||||
#[derive(Error, Clone, Debug)]
|
#[derive(Error, Clone, Debug)]
|
||||||
|
@ -47,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();
|
||||||
|
|
||||||
|
@ -119,55 +117,3 @@ pub async fn parse_nm_qs_format(input: &str) -> Result<NMDeviceDataPacket, QSPar
|
||||||
|
|
||||||
Ok(device_data)
|
Ok(device_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn device_handler<'a>(
|
|
||||||
request: web::HttpRequest,
|
|
||||||
body: Bytes,
|
|
||||||
) -> Result<web::HttpResponse, AppError> {
|
|
||||||
let mut real_body: Option<NMJsonPacket> = 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::<NMJsonPacket>(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::<NMDeviceDataPacket>(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::<NMDeviceDataPacket>(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())
|
|
||||||
}
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
//! Сборник полезных функций которые используются в многих местах одновременно или слишком
|
||||||
|
//! неспециализированные.
|
||||||
|
|
||||||
pub mod redis;
|
pub mod redis;
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Add table
Reference in a new issue