/
mcmare
/
RustAPI
Обзор
Документация
Войти
/
mcmare
/
RustAPI
Код
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
engine/src/script/host_fns.rs
112 строк
4 KB
mcmare
Add Rhai scripting layer: host functions (db/hash/jwt), per-script transactions
18 июл 2026, 16:32
18 июл 2026, 16:32
4bfc229
Код
Авторство
О чём код?
use argon2::{ password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, Argon2, }; use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation}; use rhai::{Dynamic, Engine, EvalAltResult}; use serde_json::Value; use super::convert::{dynamic_to_json, json_to_dynamic}; pub(super) fn script_err(msg: impl Into<String>) -> Box<EvalAltResult> { Box::new(EvalAltResult::ErrorRuntime(Dynamic::from(msg.into()), rhai::Position::NONE)) } /// What a script's final expression must evaluate to (via `response.json(...)` / /// `response.error(...)`) for its result to become the HTTP response. #[derive(Clone)] pub struct ScriptResponse { pub status: u16, pub body: Value, } #[derive(Clone)] pub(super) struct ResponseBuilder; fn parse_ttl(s: &str) -> Result<chrono::Duration, String> { let s = s.trim(); if s.len() < 2 { return Err(format!("invalid ttl '{s}' (expected e.g. '15m', '7d')")); } let (num, unit) = s.split_at(s.len() - 1); let n: i64 = num.parse().map_err(|_| format!("invalid ttl '{s}'"))?; match unit { "s" => Ok(chrono::Duration::seconds(n)), "m" => Ok(chrono::Duration::minutes(n)), "h" => Ok(chrono::Duration::hours(n)), "d" => Ok(chrono::Duration::days(n)), _ => Err(format!("invalid ttl unit in '{s}' (expected s/m/h/d)")), } } pub fn register(engine: &mut Engine, jwt_secret: String) { engine.register_type_with_name::<ScriptResponse>("ScriptResponse"); engine.register_type_with_name::<ResponseBuilder>("ResponseBuilder"); engine.register_fn("json", |_b: &mut ResponseBuilder, value: Dynamic| ScriptResponse { status: 200, body: dynamic_to_json(&value), }); engine.register_fn( "json", |_b: &mut ResponseBuilder, value: Dynamic, status: i64| ScriptResponse { status: status as u16, body: dynamic_to_json(&value), }, ); engine.register_fn("error", |_b: &mut ResponseBuilder, status: i64, message: &str| ScriptResponse { status: status as u16, body: serde_json::json!({ "error": { "code": "SCRIPT_ERROR", "message": message } }), }); engine.register_fn("hash_password", |plain: &str| -> Result<String, Box<EvalAltResult>> { let salt = SaltString::generate(&mut OsRng); Argon2::default() .hash_password(plain.as_bytes(), &salt) .map(|h| h.to_string()) .map_err(|e| script_err(format!("hash_password: {e}"))) }); engine.register_fn( "verify_password", |plain: &str, hash: &str| -> Result<bool, Box<EvalAltResult>> { let parsed = PasswordHash::new(hash).map_err(|e| script_err(format!("verify_password: invalid hash: {e}")))?; Ok(Argon2::default().verify_password(plain.as_bytes(), &parsed).is_ok()) }, ); engine.register_fn("uuid", || uuid::Uuid::new_v4().to_string()); engine.register_fn("now", || chrono::Utc::now().to_rfc3339()); let sign_secret = jwt_secret.clone(); engine.register_fn( "jwt_sign", move |claims: Dynamic, ttl: &str| -> Result<String, Box<EvalAltResult>> { let mut claims_json = dynamic_to_json(&claims); let obj = claims_json .as_object_mut() .ok_or_else(|| script_err("jwt_sign: claims must be an object map"))?; let duration = parse_ttl(ttl).map_err(script_err)?; let exp = (chrono::Utc::now() + duration).timestamp(); obj.insert("exp".to_string(), Value::Number(exp.into())); encode( &Header::new(Algorithm::HS256), &claims_json, &EncodingKey::from_secret(sign_secret.as_bytes()), ) .map_err(|e| script_err(format!("jwt_sign: {e}"))) }, ); let verify_secret = jwt_secret; engine.register_fn("jwt_verify", move |token: &str| -> Result<Dynamic, Box<EvalAltResult>> { let data = decode::<Value>( token, &DecodingKey::from_secret(verify_secret.as_bytes()), &Validation::new(Algorithm::HS256), ) .map_err(|e| script_err(format!("jwt_verify: {e}")))?; Ok(json_to_dynamic(&data.claims)) }); }