mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-15 18:35:47 +00:00
513695297c
* [OHOS.with ai] 将配置管理/配置分享/路由聚合/实例状态解析下沉至 Rust 内核,收敛职责并提升性能 (#2209) * feat: add ohrs config store and startup error logging * feat: full ability core for ohos * feat: full ability core for ohos * feat: clean code --------- Co-authored-by: FrankHan <frankhan@FrankHans-Mac-mini.local> * fix: 添加缺失文件 * fix: 修复更新路由启动两次TUN问题,并调整日志 * fix: rustfmt * fix: 适配Cidr忽略/32格式路由 * fix: 修复Option适配错误 * fix: rustfmt * fix: rustfmt --------- Co-authored-by: FrankHan <frankhan@FrankHans-Mac-mini.local>
198 lines
6.9 KiB
Rust
198 lines
6.9 KiB
Rust
use crate::config::repository::{get_config_record, save_config_record};
|
|
use crate::config::services::schema_service::get_network_config_field_mappings;
|
|
use crate::config::types::stored_config::SharedConfigLinkPayload;
|
|
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
|
|
use easytier::proto::api::manage::NetworkConfig;
|
|
use flate2::{Compression, read::ZlibDecoder, write::ZlibEncoder};
|
|
use gethostname::gethostname;
|
|
use std::collections::HashMap;
|
|
use std::io::{Read, Write};
|
|
use url::Url;
|
|
use uuid::Uuid;
|
|
|
|
const SHARE_LINK_HOST: &str = "easytier.cn";
|
|
const SHARE_LINK_PATH: &str = "/comp_cfg";
|
|
|
|
fn field_name_to_id_map() -> HashMap<String, String> {
|
|
get_network_config_field_mappings()
|
|
.into_iter()
|
|
.map(|mapping| (mapping.field_name, mapping.field_number.to_string()))
|
|
.collect()
|
|
}
|
|
|
|
fn field_id_to_name_map() -> HashMap<String, String> {
|
|
get_network_config_field_mappings()
|
|
.into_iter()
|
|
.map(|mapping| (mapping.field_number.to_string(), mapping.field_name))
|
|
.collect()
|
|
}
|
|
|
|
fn prune_empty(value: &serde_json::Value) -> Option<serde_json::Value> {
|
|
match value {
|
|
serde_json::Value::Null => None,
|
|
serde_json::Value::Array(values) if values.is_empty() => None,
|
|
_ => Some(value.clone()),
|
|
}
|
|
}
|
|
|
|
fn map_config_json(config: &NetworkConfig) -> Result<String, String> {
|
|
let field_name_to_id = field_name_to_id_map();
|
|
let raw = serde_json::to_value(config).map_err(|err| err.to_string())?;
|
|
let mut mapped = serde_json::Map::new();
|
|
|
|
for (key, value) in raw.as_object().cloned().unwrap_or_default() {
|
|
let Some(value) = prune_empty(&value) else {
|
|
continue;
|
|
};
|
|
let mapped_key = field_name_to_id.get(&key).cloned().unwrap_or(key);
|
|
mapped.insert(mapped_key, value);
|
|
}
|
|
|
|
serde_json::to_string(&mapped).map_err(|err| err.to_string())
|
|
}
|
|
|
|
fn unmap_config_json(raw: &str) -> Result<NetworkConfig, String> {
|
|
let field_id_to_name = field_id_to_name_map();
|
|
let value = serde_json::from_str::<serde_json::Value>(raw).map_err(|err| err.to_string())?;
|
|
let mut mapped = serde_json::Map::new();
|
|
for (key, value) in value.as_object().cloned().unwrap_or_default() {
|
|
let field_name = field_id_to_name.get(&key).cloned().unwrap_or(key);
|
|
mapped.insert(field_name, value);
|
|
}
|
|
serde_json::from_value(serde_json::Value::Object(mapped)).map_err(|err| err.to_string())
|
|
}
|
|
|
|
fn compress_to_base64url(raw: &str) -> Result<String, String> {
|
|
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::best());
|
|
encoder
|
|
.write_all(raw.as_bytes())
|
|
.map_err(|err| err.to_string())?;
|
|
let compressed = encoder.finish().map_err(|err| err.to_string())?;
|
|
Ok(URL_SAFE_NO_PAD.encode(compressed))
|
|
}
|
|
|
|
fn decompress_from_base64url(raw: &str) -> Result<String, String> {
|
|
let compressed = URL_SAFE_NO_PAD.decode(raw).map_err(|err| err.to_string())?;
|
|
let mut decoder = ZlibDecoder::new(compressed.as_slice());
|
|
let mut out = String::new();
|
|
decoder
|
|
.read_to_string(&mut out)
|
|
.map_err(|err| err.to_string())?;
|
|
Ok(out)
|
|
}
|
|
|
|
pub fn build_config_share_link(
|
|
config_id: &str,
|
|
display_name: Option<String>,
|
|
only_start: bool,
|
|
) -> Option<String> {
|
|
let record = get_config_record(config_id)?;
|
|
let config = serde_json::from_str::<NetworkConfig>(&record.config_json).ok()?;
|
|
let mapped_json = map_config_json(&config).ok()?;
|
|
let compressed = compress_to_base64url(&mapped_json).ok()?;
|
|
let final_name = display_name
|
|
.or(Some(record.meta.display_name))
|
|
.filter(|name| !name.is_empty());
|
|
|
|
let mut url = Url::parse(&format!("https://{SHARE_LINK_HOST}{SHARE_LINK_PATH}")).ok()?;
|
|
url.query_pairs_mut().append_pair("cfg", &compressed);
|
|
if let Some(name) = final_name {
|
|
url.query_pairs_mut().append_pair("name", &name);
|
|
}
|
|
if only_start {
|
|
url.query_pairs_mut().append_pair("only_start", "true");
|
|
}
|
|
Some(url.to_string())
|
|
}
|
|
|
|
pub fn parse_config_share_link(share_link: &str) -> Option<SharedConfigLinkPayload> {
|
|
let url = Url::parse(share_link).ok()?;
|
|
if url.host_str()? != SHARE_LINK_HOST || url.path() != SHARE_LINK_PATH {
|
|
return None;
|
|
}
|
|
|
|
let cfg = url
|
|
.query_pairs()
|
|
.find(|(key, _)| key == "cfg")?
|
|
.1
|
|
.to_string();
|
|
let mapped_json = decompress_from_base64url(&cfg).ok()?;
|
|
let mut config = unmap_config_json(&mapped_json).ok()?;
|
|
config.instance_id = Some(Uuid::new_v4().to_string());
|
|
let hostname = gethostname().to_string_lossy().to_string();
|
|
if !hostname.is_empty() {
|
|
config.hostname = Some(hostname);
|
|
}
|
|
|
|
let config_json = serde_json::to_string(&config).ok()?;
|
|
let display_name = url
|
|
.query_pairs()
|
|
.find(|(key, _)| key == "name")
|
|
.map(|(_, value)| value.to_string())
|
|
.filter(|name| !name.is_empty());
|
|
let only_start = url
|
|
.query_pairs()
|
|
.find(|(key, _)| key == "only_start")
|
|
.map(|(_, value)| value == "true")
|
|
.unwrap_or(false);
|
|
|
|
Some(SharedConfigLinkPayload {
|
|
config_json,
|
|
display_name,
|
|
only_start,
|
|
})
|
|
}
|
|
|
|
pub fn import_config_share_link(
|
|
share_link: &str,
|
|
display_name_override: Option<String>,
|
|
) -> Option<String> {
|
|
let payload = parse_config_share_link(share_link)?;
|
|
let config = serde_json::from_str::<NetworkConfig>(&payload.config_json).ok()?;
|
|
let config_id = config.instance_id.clone()?;
|
|
let display_name = display_name_override
|
|
.filter(|name| !name.is_empty())
|
|
.or(payload.display_name)
|
|
.unwrap_or_else(|| config_id.clone());
|
|
|
|
save_config_record(config_id.clone(), display_name, payload.config_json)?;
|
|
Some(config_id)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::config_repo::{create_config_record, init_config_store};
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
fn test_root() -> String {
|
|
let unique = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_nanos();
|
|
std::env::temp_dir()
|
|
.join(format!("easytier_ohrs_share_test_{unique}"))
|
|
.to_string_lossy()
|
|
.into_owned()
|
|
}
|
|
|
|
#[test]
|
|
fn share_link_roundtrip_works() {
|
|
assert!(init_config_store(test_root()));
|
|
create_config_record("cfg-share".to_string(), "share-demo".to_string())
|
|
.expect("create config");
|
|
|
|
let link = build_config_share_link("cfg-share", None, true).expect("share link");
|
|
let payload = parse_config_share_link(&link).expect("parse link");
|
|
let config =
|
|
serde_json::from_str::<NetworkConfig>(&payload.config_json).expect("config json");
|
|
|
|
assert!(payload.only_start);
|
|
assert_eq!(payload.display_name.as_deref(), Some("share-demo"));
|
|
assert_ne!(config.instance_id.as_deref(), Some("cfg-share"));
|
|
|
|
let imported_id = import_config_share_link(&link, None).expect("import link");
|
|
assert_ne!(imported_id, "cfg-share");
|
|
}
|
|
}
|