config visibility, PR review fixes, XSS hardening
Config visibility:
- startup banner shows config path, data dir, services path
- config search: ./numa.toml → ~/.config/numa/ → /usr/local/var/numa/
- /stats API exposes config_path and data_dir, dashboard footer renders them
- GET /ca.pem endpoint serves CA cert for cross-device TLS trust
- load_config returns ConfigLoad with found flag, warns on not-found
- ServerCtx stores PathBuf for config_dir/data_dir, string conversion at boundaries
PR review fixes:
- add explicit parens in resolve_route operator precedence (service_store.rs)
- hostname portability: drop -s flag, trim domain with split('.') (lan.rs)
- serve_ca uses spawn_blocking instead of sync fs::read in async handler
- load_config: remove TOCTOU exists() check, read directly and handle NotFound
XSS hardening:
- HTML-escape all user-controlled interpolations in dashboard (service names,
route paths, ports, URLs, block check domain/reason)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -875,6 +875,8 @@ async function refresh() {
|
|||||||
document.getElementById('uptime').textContent = formatUptime(stats.uptime_secs);
|
document.getElementById('uptime').textContent = formatUptime(stats.uptime_secs);
|
||||||
document.getElementById('uptimeSub').textContent = formatUptimeSub(stats.uptime_secs);
|
document.getElementById('uptimeSub').textContent = formatUptimeSub(stats.uptime_secs);
|
||||||
document.getElementById('footerUpstream').textContent = stats.upstream || '';
|
document.getElementById('footerUpstream').textContent = stats.upstream || '';
|
||||||
|
document.getElementById('footerConfig').textContent = stats.config_path || '';
|
||||||
|
document.getElementById('footerData').textContent = stats.data_dir || '';
|
||||||
|
|
||||||
// LAN status indicator
|
// LAN status indicator
|
||||||
const lanEl = document.getElementById('lanToggle');
|
const lanEl = document.getElementById('lanToggle');
|
||||||
@@ -1006,14 +1008,16 @@ async function checkDomain(event) {
|
|||||||
if (result.blocked) {
|
if (result.blocked) {
|
||||||
el.style.background = 'rgba(181, 68, 58, 0.1)';
|
el.style.background = 'rgba(181, 68, 58, 0.1)';
|
||||||
el.style.color = 'var(--rose)';
|
el.style.color = 'var(--rose)';
|
||||||
el.innerHTML = `<strong>Blocked</strong> — ${result.reason}` +
|
const hd = s => String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
||||||
(result.matched_rule ? `<br>Rule: <code>${result.matched_rule}</code>` : '') +
|
el.innerHTML = `<strong>Blocked</strong> — ${hd(result.reason)}` +
|
||||||
` <button class="btn-delete" onclick="allowDomain('${domain}')" style="color:var(--emerald);font-size:0.7rem;margin-left:0.4rem;">allow</button>`;
|
(result.matched_rule ? `<br>Rule: <code>${hd(result.matched_rule)}</code>` : '') +
|
||||||
|
` <button class="btn-delete" onclick="allowDomain('${hd(domain)}')" style="color:var(--emerald);font-size:0.7rem;margin-left:0.4rem;">allow</button>`;
|
||||||
} else {
|
} else {
|
||||||
|
const hd = s => String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
||||||
el.style.background = 'rgba(82, 122, 82, 0.1)';
|
el.style.background = 'rgba(82, 122, 82, 0.1)';
|
||||||
el.style.color = 'var(--emerald)';
|
el.style.color = 'var(--emerald)';
|
||||||
el.innerHTML = `<strong>Allowed</strong> — ${result.reason}` +
|
el.innerHTML = `<strong>Allowed</strong> — ${hd(result.reason)}` +
|
||||||
(result.matched_rule ? `<br>Rule: <code>${result.matched_rule}</code>` : '');
|
(result.matched_rule ? `<br>Rule: <code>${hd(result.matched_rule)}</code>` : '');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
el.style.display = 'block';
|
el.style.display = 'block';
|
||||||
@@ -1118,26 +1122,27 @@ function renderServices(entries) {
|
|||||||
? '<span class="lan-badge shared" title="Reachable from other devices on the network">LAN</span>'
|
? '<span class="lan-badge shared" title="Reachable from other devices on the network">LAN</span>'
|
||||||
: '<span class="lan-badge local-only" title="Bound to localhost — not reachable from other devices. Start with 0.0.0.0 to share on LAN.">local only</span>')
|
: '<span class="lan-badge local-only" title="Bound to localhost — not reachable from other devices. Start with 0.0.0.0 to share on LAN.">local only</span>')
|
||||||
: '';
|
: '';
|
||||||
const esc = s => s.replace(/'/g, "\\'").replace(/"/g, '"');
|
const h = s => String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
||||||
const routeLines = (e.routes || []).map(r =>
|
const routeLines = (e.routes || []).map(r =>
|
||||||
`<div class="service-port" style="color:var(--text-dim);display:flex;align-items:center;gap:0.3rem;">` +
|
`<div class="service-port" style="color:var(--text-dim);display:flex;align-items:center;gap:0.3rem;">` +
|
||||||
`<span style="display:inline-block;min-width:60px;">${r.path}</span> ` +
|
`<span style="display:inline-block;min-width:60px;">${h(r.path)}</span> ` +
|
||||||
`→ :${r.port}` +
|
`→ :${parseInt(r.port)||0}` +
|
||||||
(r.strip ? ` <span style="opacity:0.6;">(strip)</span>` : '') +
|
(r.strip ? ` <span style="opacity:0.6;">(strip)</span>` : '') +
|
||||||
(e.name === 'numa' ? '' : ` <button class="btn-delete" onclick="deleteRoute('${esc(e.name)}','${esc(r.path)}')" title="Remove route" style="font-size:0.65rem;padding:0 0.25rem;min-width:auto;opacity:0.5;">×</button>`) +
|
(e.name === 'numa' ? '' : ` <button class="btn-delete" onclick="deleteRoute('${h(e.name)}','${h(r.path)}')" title="Remove route" style="font-size:0.65rem;padding:0 0.25rem;min-width:auto;opacity:0.5;">×</button>`) +
|
||||||
`</div>`
|
`</div>`
|
||||||
).join('');
|
).join('');
|
||||||
const deletable = e.source !== 'config' && e.name !== 'numa';
|
const deletable = e.source !== 'config' && e.name !== 'numa';
|
||||||
|
const name = h(e.name);
|
||||||
return `
|
return `
|
||||||
<div class="service-item">
|
<div class="service-item">
|
||||||
<span class="health-dot ${e.healthy ? 'up' : 'down'}" title="${e.healthy ? 'running' : 'not reachable'}"></span>
|
<span class="health-dot ${e.healthy ? 'up' : 'down'}" title="${e.healthy ? 'running' : 'not reachable'}"></span>
|
||||||
<div class="service-info">
|
<div class="service-info">
|
||||||
<div class="service-name"><a href="${e.url}" target="_blank">${e.name}.numa</a>${lanBadge}</div>
|
<div class="service-name"><a href="${h(e.url)}" target="_blank">${name}.numa</a>${lanBadge}</div>
|
||||||
<div class="service-port">localhost:${e.target_port} → proxied</div>
|
<div class="service-port">localhost:${parseInt(e.target_port)||0} → proxied</div>
|
||||||
${routeLines}
|
${routeLines}
|
||||||
${e.name === 'numa' ? '' : `<div style="margin-top:0.3rem;"><button onclick="toggleRouteForm('${e.name}')" style="font-size:0.7rem;padding:0.1rem 0.4rem;background:var(--emerald);color:var(--bg);border:none;border-radius:4px;cursor:pointer;">+ route</button><div id="routeForm-${e.name}" style="display:none;margin-top:0.3rem;"><div style="display:flex;gap:0.3rem;align-items:center;"><input type="text" id="routePath-${e.name}" placeholder="/path" style="flex:2;padding:0.25rem 0.4rem;font-size:0.75rem;"><input type="number" id="routePort-${e.name}" value="${e.target_port}" min="1" max="65535" style="flex:1;padding:0.25rem 0.4rem;font-size:0.75rem;"><label style="font-size:0.7rem;color:var(--text-dim);display:flex;align-items:center;gap:0.2rem;"><input type="checkbox" id="routeStrip-${e.name}">strip</label><button onclick="addRoute('${e.name}')" style="font-size:0.7rem;padding:0.2rem 0.5rem;background:var(--emerald);color:var(--bg);border:none;border-radius:4px;cursor:pointer;">add</button></div><div class="override-error" id="routeError-${e.name}" style="display:none;font-size:0.7rem;"></div></div></div>`}
|
${e.name === 'numa' ? '' : `<div style="margin-top:0.3rem;"><button onclick="toggleRouteForm('${name}')" style="font-size:0.7rem;padding:0.1rem 0.4rem;background:var(--emerald);color:var(--bg);border:none;border-radius:4px;cursor:pointer;">+ route</button><div id="routeForm-${name}" style="display:none;margin-top:0.3rem;"><div style="display:flex;gap:0.3rem;align-items:center;"><input type="text" id="routePath-${name}" placeholder="/path" style="flex:2;padding:0.25rem 0.4rem;font-size:0.75rem;"><input type="number" id="routePort-${name}" value="${parseInt(e.target_port)||0}" min="1" max="65535" style="flex:1;padding:0.25rem 0.4rem;font-size:0.75rem;"><label style="font-size:0.7rem;color:var(--text-dim);display:flex;align-items:center;gap:0.2rem;"><input type="checkbox" id="routeStrip-${name}">strip</label><button onclick="addRoute('${name}')" style="font-size:0.7rem;padding:0.2rem 0.5rem;background:var(--emerald);color:var(--bg);border:none;border-radius:4px;cursor:pointer;">add</button></div><div class="override-error" id="routeError-${name}" style="display:none;font-size:0.7rem;"></div></div></div>`}
|
||||||
</div>
|
</div>
|
||||||
${deletable ? `<button class="btn-delete" onclick="deleteService('${e.name}')" title="Remove service">×</button>` : ''}
|
${deletable ? `<button class="btn-delete" onclick="deleteService('${name}')" title="Remove service">×</button>` : ''}
|
||||||
</div>
|
</div>
|
||||||
`}).join('');
|
`}).join('');
|
||||||
}
|
}
|
||||||
@@ -1222,8 +1227,9 @@ setInterval(refresh, 2000);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div style="text-align:center;padding:0.8rem;font-family:var(--font-mono);font-size:0.68rem;color:var(--text-dim);">
|
<div style="text-align:center;padding:0.8rem;font-family:var(--font-mono);font-size:0.68rem;color:var(--text-dim);">
|
||||||
Upstream: <span id="footerUpstream" style="user-select:all;color:var(--emerald);"></span>
|
Config: <span id="footerConfig" style="user-select:all;color:var(--emerald);"></span>
|
||||||
· Logs: <span id="logPath" style="user-select:all;">macOS: /usr/local/var/log/numa.log · Linux: journalctl -u numa -f</span>
|
· Data: <span id="footerData" style="user-select:all;color:var(--emerald);"></span>
|
||||||
|
· Upstream: <span id="footerUpstream" style="user-select:all;color:var(--emerald);"></span>
|
||||||
· <a href="https://github.com/razvandimescu/numa" target="_blank" rel="noopener" style="color:var(--amber);text-decoration:none;">GitHub</a>
|
· <a href="https://github.com/razvandimescu/numa" target="_blank" rel="noopener" style="color:var(--amber);text-decoration:none;">GitHub</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
23
src/api.rs
23
src/api.rs
@@ -49,6 +49,7 @@ pub fn router(ctx: Arc<ServerCtx>) -> Router {
|
|||||||
.route("/services/{name}/routes", get(list_routes))
|
.route("/services/{name}/routes", get(list_routes))
|
||||||
.route("/services/{name}/routes", post(add_route))
|
.route("/services/{name}/routes", post(add_route))
|
||||||
.route("/services/{name}/routes", delete(remove_route))
|
.route("/services/{name}/routes", delete(remove_route))
|
||||||
|
.route("/ca.pem", get(serve_ca))
|
||||||
.with_state(ctx)
|
.with_state(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,6 +131,8 @@ struct QueryLogResponse {
|
|||||||
struct StatsResponse {
|
struct StatsResponse {
|
||||||
uptime_secs: u64,
|
uptime_secs: u64,
|
||||||
upstream: String,
|
upstream: String,
|
||||||
|
config_path: String,
|
||||||
|
data_dir: String,
|
||||||
queries: QueriesStats,
|
queries: QueriesStats,
|
||||||
cache: CacheStats,
|
cache: CacheStats,
|
||||||
overrides: OverrideStats,
|
overrides: OverrideStats,
|
||||||
@@ -451,6 +454,8 @@ async fn stats(State(ctx): State<Arc<ServerCtx>>) -> Json<StatsResponse> {
|
|||||||
Json(StatsResponse {
|
Json(StatsResponse {
|
||||||
uptime_secs: snap.uptime_secs,
|
uptime_secs: snap.uptime_secs,
|
||||||
upstream,
|
upstream,
|
||||||
|
config_path: ctx.config_path.clone(),
|
||||||
|
data_dir: ctx.data_dir.to_string_lossy().to_string(),
|
||||||
queries: QueriesStats {
|
queries: QueriesStats {
|
||||||
total: snap.total,
|
total: snap.total,
|
||||||
forwarded: snap.forwarded,
|
forwarded: snap.forwarded,
|
||||||
@@ -810,6 +815,24 @@ async fn remove_route(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn serve_ca(State(ctx): State<Arc<ServerCtx>>) -> Result<impl IntoResponse, StatusCode> {
|
||||||
|
let ca_path = ctx.data_dir.join("ca.pem");
|
||||||
|
let bytes = tokio::task::spawn_blocking(move || std::fs::read(ca_path))
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||||
|
.map_err(|_| StatusCode::NOT_FOUND)?;
|
||||||
|
Ok((
|
||||||
|
[
|
||||||
|
(header::CONTENT_TYPE, "application/x-pem-file"),
|
||||||
|
(
|
||||||
|
header::CONTENT_DISPOSITION,
|
||||||
|
"attachment; filename=\"numa-ca.pem\"",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
bytes,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
async fn check_tcp(addr: std::net::SocketAddr) -> bool {
|
async fn check_tcp(addr: std::net::SocketAddr) -> bool {
|
||||||
tokio::time::timeout(
|
tokio::time::timeout(
|
||||||
std::time::Duration::from_millis(100),
|
std::time::Duration::from_millis(100),
|
||||||
|
|||||||
@@ -316,13 +316,62 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_config(path: &str) -> Result<Config> {
|
pub struct ConfigLoad {
|
||||||
if !Path::new(path).exists() {
|
pub config: Config,
|
||||||
return Ok(Config::default());
|
pub path: String,
|
||||||
|
pub found: bool,
|
||||||
}
|
}
|
||||||
let contents = std::fs::read_to_string(path)?;
|
|
||||||
|
fn resolve_path(path: &str) -> String {
|
||||||
|
// canonicalize gives the real absolute path for existing files;
|
||||||
|
// for non-existent files, build an absolute path manually
|
||||||
|
std::fs::canonicalize(path)
|
||||||
|
.or_else(|_| std::env::current_dir().map(|cwd| cwd.join(path)))
|
||||||
|
.unwrap_or_else(|_| Path::new(path).to_path_buf())
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_config(path: &str) -> Result<ConfigLoad> {
|
||||||
|
// Try the given path first, then well-known locations (for service mode where cwd is /)
|
||||||
|
let candidates: Vec<std::path::PathBuf> = {
|
||||||
|
let p = Path::new(path);
|
||||||
|
let mut v = vec![p.to_path_buf()];
|
||||||
|
if p.is_relative() {
|
||||||
|
let filename = p.file_name().unwrap_or(p.as_os_str());
|
||||||
|
v.push(crate::config_dir().join(filename));
|
||||||
|
v.push(crate::data_dir().join(filename));
|
||||||
|
}
|
||||||
|
v
|
||||||
|
};
|
||||||
|
|
||||||
|
for candidate in &candidates {
|
||||||
|
match std::fs::read_to_string(candidate) {
|
||||||
|
Ok(contents) => {
|
||||||
|
let resolved = resolve_path(&candidate.to_string_lossy());
|
||||||
let config: Config = toml::from_str(&contents)?;
|
let config: Config = toml::from_str(&contents)?;
|
||||||
Ok(config)
|
return Ok(ConfigLoad {
|
||||||
|
config,
|
||||||
|
path: resolved,
|
||||||
|
found: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
|
||||||
|
Err(e) => return Err(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show config_dir candidate as the "expected" path — it's actionable
|
||||||
|
let display_path = candidates
|
||||||
|
.get(1)
|
||||||
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_else(|| resolve_path(path));
|
||||||
|
log::info!("config not found, using defaults (create {})", display_path);
|
||||||
|
Ok(ConfigLoad {
|
||||||
|
config: Config::default(),
|
||||||
|
path: display_path,
|
||||||
|
found: false,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type ZoneMap = HashMap<String, HashMap<QueryType, Vec<DnsRecord>>>;
|
pub type ZoneMap = HashMap<String, HashMap<QueryType, Vec<DnsRecord>>>;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use std::time::{Duration, Instant, SystemTime};
|
use std::time::{Duration, Instant, SystemTime};
|
||||||
|
|
||||||
@@ -40,6 +41,10 @@ pub struct ServerCtx {
|
|||||||
pub proxy_tld: String,
|
pub proxy_tld: String,
|
||||||
pub proxy_tld_suffix: String, // pre-computed ".{tld}" to avoid per-query allocation
|
pub proxy_tld_suffix: String, // pre-computed ".{tld}" to avoid per-query allocation
|
||||||
pub lan_enabled: bool,
|
pub lan_enabled: bool,
|
||||||
|
pub config_path: String,
|
||||||
|
pub config_found: bool,
|
||||||
|
pub config_dir: PathBuf,
|
||||||
|
pub data_dir: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_query(
|
pub async fn handle_query(
|
||||||
|
|||||||
@@ -85,11 +85,10 @@ pub fn detect_lan_ip() -> Option<Ipv4Addr> {
|
|||||||
|
|
||||||
fn get_hostname() -> String {
|
fn get_hostname() -> String {
|
||||||
std::process::Command::new("hostname")
|
std::process::Command::new("hostname")
|
||||||
.arg("-s")
|
|
||||||
.output()
|
.output()
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|o| String::from_utf8(o.stdout).ok())
|
.and_then(|o| String::from_utf8(o.stdout).ok())
|
||||||
.map(|h| h.trim().to_string())
|
.map(|h| h.trim().split('.').next().unwrap_or("numa").to_string())
|
||||||
.filter(|h| !h.is_empty())
|
.filter(|h| !h.is_empty())
|
||||||
.unwrap_or_else(|| "numa".to_string())
|
.unwrap_or_else(|| "numa".to_string())
|
||||||
}
|
}
|
||||||
|
|||||||
21
src/main.rs
21
src/main.rs
@@ -8,7 +8,7 @@ use tokio::net::UdpSocket;
|
|||||||
use numa::blocklist::{download_blocklists, parse_blocklist, BlocklistStore};
|
use numa::blocklist::{download_blocklists, parse_blocklist, BlocklistStore};
|
||||||
use numa::buffer::BytePacketBuffer;
|
use numa::buffer::BytePacketBuffer;
|
||||||
use numa::cache::DnsCache;
|
use numa::cache::DnsCache;
|
||||||
use numa::config::{build_zone_map, load_config};
|
use numa::config::{build_zone_map, load_config, ConfigLoad};
|
||||||
use numa::ctx::{handle_query, ServerCtx};
|
use numa::ctx::{handle_query, ServerCtx};
|
||||||
use numa::override_store::OverrideStore;
|
use numa::override_store::OverrideStore;
|
||||||
use numa::query_log::QueryLog;
|
use numa::query_log::QueryLog;
|
||||||
@@ -96,7 +96,11 @@ async fn main() -> numa::Result<()> {
|
|||||||
} else {
|
} else {
|
||||||
arg1 // treat as config path for backwards compatibility
|
arg1 // treat as config path for backwards compatibility
|
||||||
};
|
};
|
||||||
let config = load_config(&config_path)?;
|
let ConfigLoad {
|
||||||
|
config,
|
||||||
|
path: resolved_config_path,
|
||||||
|
found: config_found,
|
||||||
|
} = load_config(&config_path)?;
|
||||||
|
|
||||||
// Discover system DNS in a single pass (upstream + forwarding rules)
|
// Discover system DNS in a single pass (upstream + forwarding rules)
|
||||||
let system_dns = discover_system_dns();
|
let system_dns = discover_system_dns();
|
||||||
@@ -160,6 +164,10 @@ async fn main() -> numa::Result<()> {
|
|||||||
},
|
},
|
||||||
proxy_tld: config.proxy.tld.clone(),
|
proxy_tld: config.proxy.tld.clone(),
|
||||||
lan_enabled: config.lan.enabled,
|
lan_enabled: config.lan.enabled,
|
||||||
|
config_path: resolved_config_path,
|
||||||
|
config_found,
|
||||||
|
config_dir: numa::config_dir(),
|
||||||
|
data_dir: numa::data_dir(),
|
||||||
});
|
});
|
||||||
|
|
||||||
let zone_count: usize = ctx.zone_map.values().map(|m| m.len()).sum();
|
let zone_count: usize = ctx.zone_map.values().map(|m| m.len()).sum();
|
||||||
@@ -193,6 +201,15 @@ async fn main() -> numa::Result<()> {
|
|||||||
eprintln!("\x1b[38;2;192;98;58m ║\x1b[0m \x1b[38;2;107;124;78mRouting\x1b[0m {:<30}\x1b[38;2;192;98;58m║\x1b[0m",
|
eprintln!("\x1b[38;2;192;98;58m ║\x1b[0m \x1b[38;2;107;124;78mRouting\x1b[0m {:<30}\x1b[38;2;192;98;58m║\x1b[0m",
|
||||||
format!("{} conditional rules", ctx.forwarding_rules.len()));
|
format!("{} conditional rules", ctx.forwarding_rules.len()));
|
||||||
}
|
}
|
||||||
|
eprintln!("\x1b[38;2;192;98;58m ╠──────────────────────────────────────────╣\x1b[0m");
|
||||||
|
let config_label = if ctx.config_found {
|
||||||
|
ctx.config_path.clone()
|
||||||
|
} else {
|
||||||
|
format!("{} (defaults)", ctx.config_path)
|
||||||
|
};
|
||||||
|
eprintln!("\x1b[38;2;192;98;58m ║\x1b[0m \x1b[38;2;163;152;136mConfig\x1b[0m {:<30}\x1b[38;2;192;98;58m║\x1b[0m", config_label);
|
||||||
|
eprintln!("\x1b[38;2;192;98;58m ║\x1b[0m \x1b[38;2;163;152;136mData\x1b[0m {:<30}\x1b[38;2;192;98;58m║\x1b[0m", ctx.data_dir.display());
|
||||||
|
eprintln!("\x1b[38;2;192;98;58m ║\x1b[0m \x1b[38;2;163;152;136mServices\x1b[0m {:<30}\x1b[38;2;192;98;58m║\x1b[0m", ctx.config_dir.join("services.json").display());
|
||||||
eprintln!("\x1b[38;2;192;98;58m ╚══════════════════════════════════════════╝\x1b[0m\n");
|
eprintln!("\x1b[38;2;192;98;58m ╚══════════════════════════════════════════╝\x1b[0m\n");
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ impl ServiceEntry {
|
|||||||
.iter()
|
.iter()
|
||||||
.filter(|r| {
|
.filter(|r| {
|
||||||
request_path == r.path
|
request_path == r.path
|
||||||
|| request_path.starts_with(&r.path)
|
|| (request_path.starts_with(&r.path)
|
||||||
&& (r.path.ends_with('/')
|
&& (r.path.ends_with('/')
|
||||||
|| request_path.as_bytes().get(r.path.len()) == Some(&b'/'))
|
|| request_path.as_bytes().get(r.path.len()) == Some(&b'/')))
|
||||||
})
|
})
|
||||||
.max_by_key(|r| r.path.len());
|
.max_by_key(|r| r.path.len());
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user