1pub mod bridge;
2pub mod client;
3pub mod error;
4pub mod hub;
5pub mod ilink;
6pub mod mcp;
7pub mod metrics;
8pub mod paths;
9pub mod relay;
10pub mod runtime;
11pub mod server;
12pub mod store;
13
14pub use error::HubError;
15pub use hub::queue::InMemoryQueue;
16pub use hub::queue::MessageQueue;
17pub use hub::HubState;
18pub use ilink::QrLoginUiEvent;
19pub use runtime::serve::{run_serve, ServeOptions};
20
21pub fn redact_token(t: &str) -> String {
25 let prefix: String = t.chars().take(8).collect();
26 format!("{prefix}…")
27}
28
29pub fn redact_database_url(url: &str) -> String {
37 match url::Url::parse(url) {
38 Ok(mut parsed) => {
39 if parsed.password().is_some() {
40 let _ = parsed.set_password(Some("***"));
41 }
42 redact_sensitive_query_params(&mut parsed);
43 parsed.to_string()
44 }
45 Err(_) => {
46 if let Some(scheme_sep) = url.find("://") {
48 let after_scheme = &url[scheme_sep + 3..];
49 if let Some(at) = after_scheme.find('@') {
50 let creds = &after_scheme[..at];
51 if let Some(colon) = creds.find(':') {
52 let user = &creds[..colon];
53 return format!(
54 "{}://{}:***@{}",
55 &url[..scheme_sep],
56 user,
57 &after_scheme[at + 1..]
58 );
59 }
60 }
61 }
62 url.to_string()
63 }
64 }
65}
66
67fn is_sensitive_query_key(key: &str) -> bool {
68 matches!(
69 key.to_ascii_lowercase().as_str(),
70 "password" | "passwd" | "pwd" | "sslpassword" | "key" | "secret" | "token"
71 )
72}
73
74fn redact_sensitive_query_params(url: &mut url::Url) {
75 let pairs: Vec<(String, String)> = url
76 .query_pairs()
77 .map(|(k, v)| (k.into_owned(), v.into_owned()))
78 .collect();
79 if pairs.is_empty() {
80 return;
81 }
82 if !pairs.iter().any(|(k, _)| is_sensitive_query_key(k)) {
83 return;
84 }
85 url.set_query(None);
86 {
87 let mut query = url.query_pairs_mut();
88 for (k, v) in pairs {
89 if is_sensitive_query_key(&k) {
90 query.append_pair(&k, "***");
91 } else {
92 query.append_pair(&k, &v);
93 }
94 }
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 #[test]
103 fn test_redact_token_safety() {
104 assert_eq!(redact_token(""), "…");
106 assert_eq!(redact_token("abc"), "abc…");
108 assert_eq!(redact_token("12345678"), "12345678…");
110 assert_eq!(redact_token("abcdefghijkl"), "abcdefgh…");
112 assert_eq!(redact_token("🦀🦀🦀🦀🦀🦀🦀🦀🦀🦀"), "🦀🦀🦀🦀🦀🦀🦀🦀…");
115 assert_eq!(redact_token("测试token长度校验"), "测试token长…");
116 }
117
118 #[test]
119 fn test_redact_database_url_masks_password() {
120 let redacted = redact_database_url("mysql://user:secret@host/db");
121 assert!(
122 !redacted.contains("secret"),
123 "password must not appear in redacted URL: {redacted}"
124 );
125 assert!(
126 redacted.contains("***"),
127 "expected *** placeholder: {redacted}"
128 );
129 assert!(
130 redacted.contains("mysql://"),
131 "scheme preserved: {redacted}"
132 );
133 assert!(redacted.contains("host"), "host preserved: {redacted}");
134 assert!(redacted.contains("db"), "db name preserved: {redacted}");
135 assert!(redacted.contains("user"), "username preserved: {redacted}");
136 }
137
138 #[test]
139 fn test_redact_database_url_no_password_unchanged_shape() {
140 let url = "mysql://user@host/db";
141 let redacted = redact_database_url(url);
142 assert!(!redacted.contains("***"));
143 assert!(redacted.contains("mysql://"));
144 assert!(redacted.contains("host"));
145 }
146
147 #[test]
148 fn test_redact_database_url_sqlite_memory() {
149 let redacted = redact_database_url("sqlite::memory:");
151 assert_eq!(redacted, "sqlite::memory:");
152 }
153
154 #[test]
155 fn test_redact_database_url_query_password_params() {
156 let redacted = redact_database_url("mysql://user@host/db?password=secret&sslkey=abc");
157 assert!(
158 !redacted.contains("secret"),
159 "query password must be redacted: {redacted}"
160 );
161 assert!(
164 redacted.contains("password=***")
165 || redacted.contains("password%3D***")
166 || redacted.contains("***")
167 );
168
169 let redacted2 = redact_database_url("postgres://host/db?password=secret");
170 assert!(
171 !redacted2.contains("secret"),
172 "postgres query password must be redacted: {redacted2}"
173 );
174
175 let redacted3 = redact_database_url("sqlite:///tmp/x.db?key=supersecret");
176 assert!(
177 !redacted3.contains("supersecret"),
178 "SQLCipher key query must be redacted: {redacted3}"
179 );
180 }
181}