pub mod bridge;
pub mod client;
pub mod error;
pub mod hub;
pub mod ilink;
pub mod mcp;
pub mod metrics;
pub mod paths;
pub mod relay;
pub mod runtime;
pub mod server;
pub mod store;
pub use error::HubError;
pub use hub::queue::InMemoryQueue;
pub use hub::queue::MessageQueue;
pub use hub::HubState;
pub use ilink::QrLoginUiEvent;
pub use runtime::serve::{run_serve, ServeOptions};
pub fn redact_token(t: &str) -> String {
let prefix: String = t.chars().take(8).collect();
format!("{prefix}…")
}
pub fn redact_database_url(url: &str) -> String {
match url::Url::parse(url) {
Ok(mut parsed) => {
if parsed.password().is_some() {
let _ = parsed.set_password(Some("***"));
}
redact_sensitive_query_params(&mut parsed);
parsed.to_string()
}
Err(_) => {
if let Some(scheme_sep) = url.find("://") {
let after_scheme = &url[scheme_sep + 3..];
if let Some(at) = after_scheme.find('@') {
let creds = &after_scheme[..at];
if let Some(colon) = creds.find(':') {
let user = &creds[..colon];
return format!(
"{}://{}:***@{}",
&url[..scheme_sep],
user,
&after_scheme[at + 1..]
);
}
}
}
url.to_string()
}
}
}
fn is_sensitive_query_key(key: &str) -> bool {
matches!(
key.to_ascii_lowercase().as_str(),
"password" | "passwd" | "pwd" | "sslpassword" | "key" | "secret" | "token"
)
}
fn redact_sensitive_query_params(url: &mut url::Url) {
let pairs: Vec<(String, String)> = url
.query_pairs()
.map(|(k, v)| (k.into_owned(), v.into_owned()))
.collect();
if pairs.is_empty() {
return;
}
if !pairs.iter().any(|(k, _)| is_sensitive_query_key(k)) {
return;
}
url.set_query(None);
{
let mut query = url.query_pairs_mut();
for (k, v) in pairs {
if is_sensitive_query_key(&k) {
query.append_pair(&k, "***");
} else {
query.append_pair(&k, &v);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_redact_token_safety() {
assert_eq!(redact_token(""), "…");
assert_eq!(redact_token("abc"), "abc…");
assert_eq!(redact_token("12345678"), "12345678…");
assert_eq!(redact_token("abcdefghijkl"), "abcdefgh…");
assert_eq!(redact_token("🦀🦀🦀🦀🦀🦀🦀🦀🦀🦀"), "🦀🦀🦀🦀🦀🦀🦀🦀…");
assert_eq!(redact_token("测试token长度校验"), "测试token长…");
}
#[test]
fn test_redact_database_url_masks_password() {
let redacted = redact_database_url("mysql://user:secret@host/db");
assert!(
!redacted.contains("secret"),
"password must not appear in redacted URL: {redacted}"
);
assert!(
redacted.contains("***"),
"expected *** placeholder: {redacted}"
);
assert!(
redacted.contains("mysql://"),
"scheme preserved: {redacted}"
);
assert!(redacted.contains("host"), "host preserved: {redacted}");
assert!(redacted.contains("db"), "db name preserved: {redacted}");
assert!(redacted.contains("user"), "username preserved: {redacted}");
}
#[test]
fn test_redact_database_url_no_password_unchanged_shape() {
let url = "mysql://user@host/db";
let redacted = redact_database_url(url);
assert!(!redacted.contains("***"));
assert!(redacted.contains("mysql://"));
assert!(redacted.contains("host"));
}
#[test]
fn test_redact_database_url_sqlite_memory() {
let redacted = redact_database_url("sqlite::memory:");
assert_eq!(redacted, "sqlite::memory:");
}
#[test]
fn test_redact_database_url_query_password_params() {
let redacted = redact_database_url("mysql://user@host/db?password=secret&sslkey=abc");
assert!(
!redacted.contains("secret"),
"query password must be redacted: {redacted}"
);
assert!(
redacted.contains("password=***")
|| redacted.contains("password%3D***")
|| redacted.contains("***")
);
let redacted2 = redact_database_url("postgres://host/db?password=secret");
assert!(
!redacted2.contains("secret"),
"postgres query password must be redacted: {redacted2}"
);
let redacted3 = redact_database_url("sqlite:///tmp/x.db?key=supersecret");
assert!(
!redacted3.contains("supersecret"),
"SQLCipher key query must be redacted: {redacted3}"
);
}
}