Skip to content
Clone
d6dc02f
93 lines 3.1 KB Rust
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use serde::Deserialize;
/// Web Push(RFC8030 + RFC8292)投递配置。
///
/// `vapid_private_key` / `vapid_public_key` 都是 base64url(无填充)的 P-256
/// 密钥材料:私钥是 32 字节标量,公钥是 65 字节非压缩点。这里只做形状校验
/// (长度与编码),密钥本身能否构成一对由 `push` crate 在 worker 启动时用
/// 真正的曲线运算确认——那一步失败会让 apihost 直接退出,不静默丢推送。
#[derive(Clone, Debug, Deserialize)]
#[serde(default)]
pub struct PushConfig {
pub enabled: bool,
pub vapid_public_key: String,
pub vapid_private_key: String,
/// VAPID `sub` 声明:通常是 `mailto:ops@example.com` 或站点 URL。
pub vapid_subject: String,
pub workers: usize,
pub max_attempts: u32,
pub retry_backoff_base_secs: u64,
pub request_timeout_secs: u64,
/// 推送消息在推送服务上的存活时间(TTL 头)。
pub ttl_secs: u64,
}
impl Default for PushConfig {
fn default() -> Self {
Self {
enabled: false,
vapid_public_key: String::new(),
vapid_private_key: String::new(),
vapid_subject: String::new(),
workers: 2,
max_attempts: 3,
retry_backoff_base_secs: 30,
request_timeout_secs: 10,
ttl_secs: 86_400,
}
}
}
impl PushConfig {
pub fn is_valid(&self) -> Result<(), String> {
if !self.enabled {
return Ok(());
}
let public = decode_key(&self.vapid_public_key)
.ok_or_else(|| "push.vapid_public_key must be base64url".to_string())?;
if public.len() != 65 || public.first() != Some(&4) {
return Err(
"push.vapid_public_key must be a 65-byte uncompressed P-256 point".to_string(),
);
}
let private = decode_key(&self.vapid_private_key)
.ok_or_else(|| "push.vapid_private_key must be base64url".to_string())?;
if private.len() != 32 {
return Err("push.vapid_private_key must decode to a 32-byte scalar".to_string());
}
if !self.vapid_subject.trim().starts_with("mailto:")
&& !self.vapid_subject.trim().starts_with("https://")
{
return Err("push.vapid_subject must be a mailto: address or https:// URL".to_string());
}
if self.workers == 0
|| self.max_attempts == 0
|| self.request_timeout_secs == 0
|| self.ttl_secs == 0
{
return Err("push numeric settings must be greater than zero".to_string());
}
Ok(())
}
pub fn public_key_bytes(&self) -> Option<Vec<u8>> {
decode_key(&self.vapid_public_key)
}
pub fn private_key_bytes(&self) -> Option<Vec<u8>> {
decode_key(&self.vapid_private_key)
}
}
fn decode_key(raw: &str) -> Option<Vec<u8>> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return None;
}
URL_SAFE_NO_PAD
.decode(trimmed.trim_end_matches('='))
.ok()
.filter(|bytes| !bytes.is_empty())
}