mirror of
https://github.com/telemt/telemt.git
synced 2026-09-05 18:16:06 +03:00
e774bc8c9a
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
371 lines
10 KiB
Rust
371 lines
10 KiB
Rust
use super::*;
|
|
|
|
#[derive(Debug)]
|
|
/// One rejected configuration path and its optional nearest known key.
|
|
pub(super) struct UnknownConfigKey {
|
|
/// Fully qualified configuration path.
|
|
pub(super) path: String,
|
|
/// Nearest known key when edit distance is sufficiently small.
|
|
pub(super) suggestion: Option<String>,
|
|
}
|
|
|
|
fn table_at<'a>(value: &'a toml::Value, path: &[&str]) -> Option<&'a toml::Table> {
|
|
let mut current = value;
|
|
for segment in path {
|
|
current = current.get(*segment)?;
|
|
}
|
|
current.as_table()
|
|
}
|
|
|
|
/// Reads strict-key enforcement without deserializing the full configuration.
|
|
pub(super) fn is_strict_config(parsed_toml: &toml::Value) -> bool {
|
|
table_at(parsed_toml, &["general"])
|
|
.and_then(|table| table.get("config_strict"))
|
|
.and_then(toml::Value::as_bool)
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
fn known_config_keys_for_suggestion() -> Vec<&'static str> {
|
|
let mut keys = Vec::new();
|
|
for group in [
|
|
TOP_LEVEL_CONFIG_KEYS,
|
|
GENERAL_CONFIG_KEYS,
|
|
NETWORK_CONFIG_KEYS,
|
|
SERVER_CONFIG_KEYS,
|
|
API_CONFIG_KEYS,
|
|
CONNTRACK_CONTROL_CONFIG_KEYS,
|
|
LISTENER_CONFIG_KEYS,
|
|
WEB_CONFIG_KEYS,
|
|
WEB_LIMITS_CONFIG_KEYS,
|
|
WEB_DEBUG_CONFIG_KEYS,
|
|
WEB_TIMEOUTS_CONFIG_KEYS,
|
|
WEB_VHOST_CONFIG_KEYS,
|
|
WEB_DECOY_CONFIG_KEYS,
|
|
WEB_PROFILE_CONFIG_KEYS,
|
|
TIMEOUTS_CONFIG_KEYS,
|
|
CENSORSHIP_CONFIG_KEYS,
|
|
TLS_FETCH_CONFIG_KEYS,
|
|
ACCESS_CONFIG_KEYS,
|
|
RATE_LIMIT_BPS_CONFIG_KEYS,
|
|
UPSTREAM_CONFIG_KEYS,
|
|
PROXY_MODES_CONFIG_KEYS,
|
|
TELEMETRY_CONFIG_KEYS,
|
|
LINKS_CONFIG_KEYS,
|
|
LOGGING_CONFIG_KEYS,
|
|
] {
|
|
keys.extend_from_slice(group);
|
|
}
|
|
keys
|
|
}
|
|
|
|
fn levenshtein_distance(a: &str, b: &str) -> usize {
|
|
let b_chars: Vec<char> = b.chars().collect();
|
|
let mut prev: Vec<usize> = (0..=b_chars.len()).collect();
|
|
let mut curr = vec![0usize; b_chars.len() + 1];
|
|
|
|
for (i, ca) in a.chars().enumerate() {
|
|
curr[0] = i + 1;
|
|
for (j, cb) in b_chars.iter().enumerate() {
|
|
let replace = if ca == *cb { prev[j] } else { prev[j] + 1 };
|
|
curr[j + 1] = (prev[j + 1] + 1).min(curr[j] + 1).min(replace);
|
|
}
|
|
std::mem::swap(&mut prev, &mut curr);
|
|
}
|
|
|
|
prev[b_chars.len()]
|
|
}
|
|
|
|
fn unknown_key_suggestion(key: &str, known_keys: &[&'static str]) -> Option<String> {
|
|
let normalized = key.to_ascii_lowercase();
|
|
let mut best: Option<(&str, usize)> = None;
|
|
for known in known_keys {
|
|
let distance = levenshtein_distance(&normalized, known);
|
|
let is_better = match best {
|
|
Some((_, best_distance)) => distance < best_distance,
|
|
None => true,
|
|
};
|
|
if distance <= 4 && is_better {
|
|
best = Some((known, distance));
|
|
}
|
|
}
|
|
best.map(|(known, _)| known.to_string())
|
|
}
|
|
|
|
fn push_unknown_keys(
|
|
unknown: &mut Vec<UnknownConfigKey>,
|
|
known_for_suggestion: &[&'static str],
|
|
path: &str,
|
|
table: &toml::Table,
|
|
allowed: &[&str],
|
|
) {
|
|
for key in table.keys() {
|
|
if !allowed.contains(&key.as_str()) {
|
|
let full_path = if path.is_empty() {
|
|
key.clone()
|
|
} else {
|
|
format!("{path}.{key}")
|
|
};
|
|
unknown.push(UnknownConfigKey {
|
|
path: full_path,
|
|
suggestion: unknown_key_suggestion(key, known_for_suggestion),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
fn check_known_table(
|
|
parsed_toml: &toml::Value,
|
|
unknown: &mut Vec<UnknownConfigKey>,
|
|
known_for_suggestion: &[&'static str],
|
|
path: &[&str],
|
|
allowed: &[&str],
|
|
) {
|
|
if let Some(table) = table_at(parsed_toml, path) {
|
|
push_unknown_keys(
|
|
unknown,
|
|
known_for_suggestion,
|
|
&path.join("."),
|
|
table,
|
|
allowed,
|
|
);
|
|
}
|
|
}
|
|
|
|
fn check_nested_table_value(
|
|
unknown: &mut Vec<UnknownConfigKey>,
|
|
known_for_suggestion: &[&'static str],
|
|
path: String,
|
|
value: &toml::Value,
|
|
allowed: &[&str],
|
|
) {
|
|
if let Some(table) = value.as_table() {
|
|
push_unknown_keys(unknown, known_for_suggestion, &path, table, allowed);
|
|
}
|
|
}
|
|
|
|
/// Collects unknown keys across every supported nested configuration table.
|
|
pub(super) fn collect_unknown_config_keys(parsed_toml: &toml::Value) -> Vec<UnknownConfigKey> {
|
|
let known_for_suggestion = known_config_keys_for_suggestion();
|
|
let mut unknown = Vec::new();
|
|
|
|
if let Some(root) = parsed_toml.as_table() {
|
|
push_unknown_keys(
|
|
&mut unknown,
|
|
&known_for_suggestion,
|
|
"",
|
|
root,
|
|
TOP_LEVEL_CONFIG_KEYS,
|
|
);
|
|
}
|
|
|
|
check_known_table(
|
|
parsed_toml,
|
|
&mut unknown,
|
|
&known_for_suggestion,
|
|
&["general"],
|
|
GENERAL_CONFIG_KEYS,
|
|
);
|
|
check_known_table(
|
|
parsed_toml,
|
|
&mut unknown,
|
|
&known_for_suggestion,
|
|
&["general", "modes"],
|
|
PROXY_MODES_CONFIG_KEYS,
|
|
);
|
|
check_known_table(
|
|
parsed_toml,
|
|
&mut unknown,
|
|
&known_for_suggestion,
|
|
&["general", "telemetry"],
|
|
TELEMETRY_CONFIG_KEYS,
|
|
);
|
|
check_known_table(
|
|
parsed_toml,
|
|
&mut unknown,
|
|
&known_for_suggestion,
|
|
&["general", "links"],
|
|
LINKS_CONFIG_KEYS,
|
|
);
|
|
check_known_table(
|
|
parsed_toml,
|
|
&mut unknown,
|
|
&known_for_suggestion,
|
|
&["logging"],
|
|
LOGGING_CONFIG_KEYS,
|
|
);
|
|
check_known_table(
|
|
parsed_toml,
|
|
&mut unknown,
|
|
&known_for_suggestion,
|
|
&["network"],
|
|
NETWORK_CONFIG_KEYS,
|
|
);
|
|
check_known_table(
|
|
parsed_toml,
|
|
&mut unknown,
|
|
&known_for_suggestion,
|
|
&["server"],
|
|
SERVER_CONFIG_KEYS,
|
|
);
|
|
check_known_table(
|
|
parsed_toml,
|
|
&mut unknown,
|
|
&known_for_suggestion,
|
|
&["server", "api"],
|
|
API_CONFIG_KEYS,
|
|
);
|
|
check_known_table(
|
|
parsed_toml,
|
|
&mut unknown,
|
|
&known_for_suggestion,
|
|
&["server", "admin_api"],
|
|
API_CONFIG_KEYS,
|
|
);
|
|
check_known_table(
|
|
parsed_toml,
|
|
&mut unknown,
|
|
&known_for_suggestion,
|
|
&["server", "conntrack_control"],
|
|
CONNTRACK_CONTROL_CONFIG_KEYS,
|
|
);
|
|
check_known_table(
|
|
parsed_toml,
|
|
&mut unknown,
|
|
&known_for_suggestion,
|
|
&["web"],
|
|
WEB_CONFIG_KEYS,
|
|
);
|
|
check_known_table(
|
|
parsed_toml,
|
|
&mut unknown,
|
|
&known_for_suggestion,
|
|
&["web", "limits"],
|
|
WEB_LIMITS_CONFIG_KEYS,
|
|
);
|
|
check_known_table(
|
|
parsed_toml,
|
|
&mut unknown,
|
|
&known_for_suggestion,
|
|
&["web", "debug"],
|
|
WEB_DEBUG_CONFIG_KEYS,
|
|
);
|
|
check_known_table(
|
|
parsed_toml,
|
|
&mut unknown,
|
|
&known_for_suggestion,
|
|
&["web", "timeouts"],
|
|
WEB_TIMEOUTS_CONFIG_KEYS,
|
|
);
|
|
check_known_table(
|
|
parsed_toml,
|
|
&mut unknown,
|
|
&known_for_suggestion,
|
|
&["timeouts"],
|
|
TIMEOUTS_CONFIG_KEYS,
|
|
);
|
|
check_known_table(
|
|
parsed_toml,
|
|
&mut unknown,
|
|
&known_for_suggestion,
|
|
&["censorship"],
|
|
CENSORSHIP_CONFIG_KEYS,
|
|
);
|
|
check_known_table(
|
|
parsed_toml,
|
|
&mut unknown,
|
|
&known_for_suggestion,
|
|
&["censorship", "tls_fetch"],
|
|
TLS_FETCH_CONFIG_KEYS,
|
|
);
|
|
check_known_table(
|
|
parsed_toml,
|
|
&mut unknown,
|
|
&known_for_suggestion,
|
|
&["access"],
|
|
ACCESS_CONFIG_KEYS,
|
|
);
|
|
|
|
if let Some(listeners) = table_at(parsed_toml, &["server"])
|
|
.and_then(|table| table.get("listeners"))
|
|
.and_then(toml::Value::as_array)
|
|
{
|
|
for (idx, listener) in listeners.iter().enumerate() {
|
|
check_nested_table_value(
|
|
&mut unknown,
|
|
&known_for_suggestion,
|
|
format!("server.listeners[{idx}]"),
|
|
listener,
|
|
LISTENER_CONFIG_KEYS,
|
|
);
|
|
}
|
|
}
|
|
|
|
if let Some(vhosts) = table_at(parsed_toml, &["web"])
|
|
.and_then(|table| table.get("vhosts"))
|
|
.and_then(toml::Value::as_array)
|
|
{
|
|
for (vhost_idx, vhost) in vhosts.iter().enumerate() {
|
|
check_nested_table_value(
|
|
&mut unknown,
|
|
&known_for_suggestion,
|
|
format!("web.vhosts[{vhost_idx}]"),
|
|
vhost,
|
|
WEB_VHOST_CONFIG_KEYS,
|
|
);
|
|
if let Some(vhost) = vhost.as_table() {
|
|
if let Some(decoy) = vhost.get("decoy") {
|
|
check_nested_table_value(
|
|
&mut unknown,
|
|
&known_for_suggestion,
|
|
format!("web.vhosts[{vhost_idx}].decoy"),
|
|
decoy,
|
|
WEB_DECOY_CONFIG_KEYS,
|
|
);
|
|
}
|
|
if let Some(profiles) = vhost.get("profiles").and_then(toml::Value::as_array) {
|
|
for (profile_idx, profile) in profiles.iter().enumerate() {
|
|
check_nested_table_value(
|
|
&mut unknown,
|
|
&known_for_suggestion,
|
|
format!("web.vhosts[{vhost_idx}].profiles[{profile_idx}]"),
|
|
profile,
|
|
WEB_PROFILE_CONFIG_KEYS,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(upstreams) = parsed_toml.get("upstreams").and_then(toml::Value::as_array) {
|
|
for (idx, upstream) in upstreams.iter().enumerate() {
|
|
check_nested_table_value(
|
|
&mut unknown,
|
|
&known_for_suggestion,
|
|
format!("upstreams[{idx}]"),
|
|
upstream,
|
|
UPSTREAM_CONFIG_KEYS,
|
|
);
|
|
}
|
|
}
|
|
|
|
for access_map in ["user_rate_limits", "cidr_rate_limits"] {
|
|
if let Some(table) = table_at(parsed_toml, &["access"])
|
|
.and_then(|access| access.get(access_map))
|
|
.and_then(toml::Value::as_table)
|
|
{
|
|
for (entry_name, value) in table {
|
|
check_nested_table_value(
|
|
&mut unknown,
|
|
&known_for_suggestion,
|
|
format!("access.{access_map}.{entry_name}"),
|
|
value,
|
|
RATE_LIMIT_BPS_CONFIG_KEYS,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
unknown
|
|
}
|