add TLS, service persistence, blocking panel, query types
- Local TLS: auto-generated CA + per-service certs (explicit SANs, not wildcards — browsers reject *.numa under single-label TLDs). HTTPS proxy on :443 via rustls/tokio-rustls. `numa install` trusts CA in macOS Keychain / Linux ca-certificates. - Service persistence: user-added services saved to ~/.config/numa/services.json, survive restarts. - Blocking panel: renamed "Check Domain" to "Blocking" with sources display, allowlist management UI, unpause button. - Query types: recognize SOA, PTR, TXT, SRV, HTTPS (type 65) instead of logging as UNKNOWN. - Blocklist gzip: reqwest now decompresses gzip responses from CDNs. - Unified config_dir() in lib.rs for consistent path resolution under sudo and launchd. TLS certs use /usr/local/var/numa/ (writable as root daemon). - Dashboard UX: panel subtitles differentiating overrides vs services, better placeholders, proxy route display, 600px query log height. - Deploy: make deploy handles build+copy+codesign+restart cycle. - Demo: scripts/record-demo.sh for recording hero GIF with CDP. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::Serialize;
|
||||
use log::{info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct ServiceEntry {
|
||||
pub name: String,
|
||||
pub target_port: u16,
|
||||
@@ -10,6 +12,9 @@ pub struct ServiceEntry {
|
||||
|
||||
pub struct ServiceStore {
|
||||
entries: HashMap<String, ServiceEntry>,
|
||||
/// Services defined in numa.toml (not persisted to user file)
|
||||
config_services: std::collections::HashSet<String>,
|
||||
persist_path: PathBuf,
|
||||
}
|
||||
|
||||
impl Default for ServiceStore {
|
||||
@@ -20,13 +25,18 @@ impl Default for ServiceStore {
|
||||
|
||||
impl ServiceStore {
|
||||
pub fn new() -> Self {
|
||||
let persist_path = dirs_path();
|
||||
ServiceStore {
|
||||
entries: HashMap::new(),
|
||||
config_services: std::collections::HashSet::new(),
|
||||
persist_path,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, name: &str, target_port: u16) {
|
||||
/// Insert a service from numa.toml config (not persisted)
|
||||
pub fn insert_from_config(&mut self, name: &str, target_port: u16) {
|
||||
let key = name.to_lowercase();
|
||||
self.config_services.insert(key.clone());
|
||||
self.entries.insert(
|
||||
key.clone(),
|
||||
ServiceEntry {
|
||||
@@ -36,12 +46,30 @@ impl ServiceStore {
|
||||
);
|
||||
}
|
||||
|
||||
/// Insert a user-defined service (persisted to ~/.config/numa/services.json)
|
||||
pub fn insert(&mut self, name: &str, target_port: u16) {
|
||||
let key = name.to_lowercase();
|
||||
self.entries.insert(
|
||||
key.clone(),
|
||||
ServiceEntry {
|
||||
name: key,
|
||||
target_port,
|
||||
},
|
||||
);
|
||||
self.save();
|
||||
}
|
||||
|
||||
pub fn lookup(&self, name: &str) -> Option<&ServiceEntry> {
|
||||
self.entries.get(&name.to_lowercase())
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, name: &str) -> bool {
|
||||
self.entries.remove(&name.to_lowercase()).is_some()
|
||||
let key = name.to_lowercase();
|
||||
let removed = self.entries.remove(&key).is_some();
|
||||
if removed {
|
||||
self.save();
|
||||
}
|
||||
removed
|
||||
}
|
||||
|
||||
pub fn list(&self) -> Vec<&ServiceEntry> {
|
||||
@@ -49,4 +77,56 @@ impl ServiceStore {
|
||||
entries.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
entries
|
||||
}
|
||||
|
||||
/// Load user-defined services from ~/.config/numa/services.json
|
||||
pub fn load_persisted(&mut self) {
|
||||
if !self.persist_path.exists() {
|
||||
return;
|
||||
}
|
||||
match std::fs::read_to_string(&self.persist_path) {
|
||||
Ok(contents) => match serde_json::from_str::<Vec<ServiceEntry>>(&contents) {
|
||||
Ok(entries) => {
|
||||
let count = entries.len();
|
||||
for entry in entries {
|
||||
let key = entry.name.to_lowercase();
|
||||
// Don't overwrite config-defined services
|
||||
if !self.config_services.contains(&key) {
|
||||
self.entries.insert(key, entry);
|
||||
}
|
||||
}
|
||||
if count > 0 {
|
||||
info!("loaded {} persisted services from {:?}", count, self.persist_path);
|
||||
}
|
||||
}
|
||||
Err(e) => warn!("failed to parse {:?}: {}", self.persist_path, e),
|
||||
},
|
||||
Err(e) => warn!("failed to read {:?}: {}", self.persist_path, e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Save user-defined services (excluding config and "numa") to disk
|
||||
fn save(&self) {
|
||||
let user_services: Vec<&ServiceEntry> = self
|
||||
.entries
|
||||
.values()
|
||||
.filter(|e| !self.config_services.contains(&e.name))
|
||||
.collect();
|
||||
|
||||
if let Some(parent) = self.persist_path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
|
||||
match serde_json::to_string_pretty(&user_services) {
|
||||
Ok(json) => {
|
||||
if let Err(e) = std::fs::write(&self.persist_path, json) {
|
||||
warn!("failed to save services to {:?}: {}", self.persist_path, e);
|
||||
}
|
||||
}
|
||||
Err(e) => warn!("failed to serialize services: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dirs_path() -> PathBuf {
|
||||
crate::config_dir().join("services.json")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user