mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-04-03 22:44:49 -04:00
Add single hostname support to credential matcher logic (#1667)
This commit is contained in:
committed by
Leendert de Borst
parent
fbb359c18d
commit
7f5dff41be
@@ -61,12 +61,14 @@ export async function filterItems(
|
||||
await ensureInit();
|
||||
|
||||
// Map Items to the format expected by the WASM filter
|
||||
const credentials = items.map(item => ({
|
||||
Id: item.Id,
|
||||
ItemName: item.Name ?? '',
|
||||
ItemUrls: getFieldValues(item, FieldKey.LoginUrl)
|
||||
}));
|
||||
|
||||
const result = wasmFilterItems({
|
||||
credentials: items.map(item => ({
|
||||
Id: item.Id,
|
||||
ItemName: item.Name ?? '',
|
||||
ItemUrls: getFieldValues(item, FieldKey.LoginUrl)
|
||||
})),
|
||||
credentials,
|
||||
current_url: currentUrl,
|
||||
page_title: pageTitle,
|
||||
matching_mode: matchingMode
|
||||
|
||||
@@ -131,7 +131,8 @@ pub fn extract_domain_with_port(url: &str) -> DomainWithPort {
|
||||
|
||||
let mut domain = url.to_lowercase();
|
||||
|
||||
// Check if it has a protocol
|
||||
// Check if it has a protocol - this is important for allowing single-word hostnames
|
||||
// like "http://plex" or "https://nas" which are common in self-hosted/homelab setups
|
||||
let has_protocol = domain.starts_with("http://") || domain.starts_with("https://");
|
||||
|
||||
// If no protocol and starts with TLD + dot, it's likely an app package name
|
||||
@@ -179,15 +180,18 @@ pub fn extract_domain_with_port(url: &str) -> DomainWithPort {
|
||||
None
|
||||
};
|
||||
|
||||
// Basic domain validation - must contain at least one dot and valid characters
|
||||
if !domain.contains('.') {
|
||||
// Domain validation:
|
||||
// - If URL had a protocol (http:// or https://), allow single-word hostnames
|
||||
// like "localhost", "plex", "nas", "router" - common in self-hosted/homelab setups
|
||||
// - If no protocol, require at least one dot to distinguish from random text
|
||||
if !domain.contains('.') && !has_protocol {
|
||||
return DomainWithPort {
|
||||
domain: String::new(),
|
||||
port: None,
|
||||
};
|
||||
}
|
||||
|
||||
// Check for valid domain characters
|
||||
// Check for valid domain characters (alphanumeric, dots, hyphens)
|
||||
if !domain
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-')
|
||||
@@ -198,7 +202,7 @@ pub fn extract_domain_with_port(url: &str) -> DomainWithPort {
|
||||
};
|
||||
}
|
||||
|
||||
// Ensure valid domain structure
|
||||
// Ensure valid domain structure (no leading/trailing dots, no consecutive dots)
|
||||
if domain.starts_with('.') || domain.ends_with('.') || domain.contains("..") {
|
||||
return DomainWithPort {
|
||||
domain: String::new(),
|
||||
@@ -206,6 +210,14 @@ pub fn extract_domain_with_port(url: &str) -> DomainWithPort {
|
||||
};
|
||||
}
|
||||
|
||||
// Ensure domain is not empty after all processing
|
||||
if domain.is_empty() {
|
||||
return DomainWithPort {
|
||||
domain: String::new(),
|
||||
port: None,
|
||||
};
|
||||
}
|
||||
|
||||
DomainWithPort { domain, port }
|
||||
}
|
||||
|
||||
@@ -315,6 +327,62 @@ mod tests {
|
||||
// Invalid domains
|
||||
assert_eq!(extract_domain(""), "");
|
||||
assert_eq!(extract_domain("nodot"), "");
|
||||
|
||||
// Single-word hostnames WITH protocol should be supported
|
||||
// (common in self-hosted/homelab setups with local DNS or /etc/hosts)
|
||||
assert_eq!(extract_domain("http://localhost"), "localhost");
|
||||
assert_eq!(extract_domain("https://localhost"), "localhost");
|
||||
assert_eq!(extract_domain("http://localhost/path"), "localhost");
|
||||
assert_eq!(extract_domain("http://localhost?query=1"), "localhost");
|
||||
assert_eq!(extract_domain("http://plex"), "plex");
|
||||
assert_eq!(extract_domain("https://nas"), "nas");
|
||||
assert_eq!(extract_domain("http://router"), "router");
|
||||
assert_eq!(extract_domain("http://homeassistant"), "homeassistant");
|
||||
assert_eq!(extract_domain("http://pihole/admin"), "pihole");
|
||||
|
||||
// Single-word hostnames WITHOUT protocol should NOT be accepted
|
||||
// (to avoid matching random text as domains)
|
||||
assert_eq!(extract_domain("localhost"), "");
|
||||
assert_eq!(extract_domain("plex"), "");
|
||||
assert_eq!(extract_domain("randomword"), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_domain_single_word_hostname_with_port() {
|
||||
// Single-word hostnames with port (common for self-hosted services)
|
||||
assert_eq!(extract_domain("http://localhost:8080"), "localhost");
|
||||
assert_eq!(extract_domain("http://localhost:81"), "localhost");
|
||||
assert_eq!(extract_domain("http://localhost:3000/path"), "localhost");
|
||||
assert_eq!(extract_domain("http://plex:32400"), "plex");
|
||||
assert_eq!(extract_domain("https://nas:5001"), "nas");
|
||||
assert_eq!(extract_domain("http://router:8080/admin"), "router");
|
||||
|
||||
// Without protocol - should NOT work (could be ambiguous)
|
||||
assert_eq!(extract_domain("localhost:8080"), "");
|
||||
assert_eq!(extract_domain("plex:32400"), "");
|
||||
|
||||
// Test DomainWithPort struct with localhost
|
||||
let result = extract_domain_with_port("http://localhost:81");
|
||||
assert_eq!(result.domain, "localhost");
|
||||
assert_eq!(result.port, Some("81".to_string()));
|
||||
assert_eq!(result.with_port(), "localhost:81");
|
||||
|
||||
let result = extract_domain_with_port("http://localhost:8080/path");
|
||||
assert_eq!(result.domain, "localhost");
|
||||
assert_eq!(result.port, Some("8080".to_string()));
|
||||
|
||||
let result = extract_domain_with_port("http://localhost/path");
|
||||
assert_eq!(result.domain, "localhost");
|
||||
assert_eq!(result.port, None);
|
||||
|
||||
// Test with other single-word hostnames
|
||||
let result = extract_domain_with_port("http://plex:32400");
|
||||
assert_eq!(result.domain, "plex");
|
||||
assert_eq!(result.port, Some("32400".to_string()));
|
||||
|
||||
let result = extract_domain_with_port("https://nas:5001/files");
|
||||
assert_eq!(result.domain, "nas");
|
||||
assert_eq!(result.port, Some("5001".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -876,3 +876,133 @@ fn test_ignore_port_ip_with_multiple_ports_and_no_port() {
|
||||
let small_android = filter_ignore_port(small_credentials.clone(), "https://192.168.1.10", "");
|
||||
assert_eq!(small_android.len(), 2, "With ignore_port, both credentials match");
|
||||
}
|
||||
|
||||
/// [#41] - Localhost matching with ports (development scenarios)
|
||||
/// Common scenario: developers running multiple services on localhost with different ports
|
||||
#[test]
|
||||
fn test_localhost_matching_with_ports() {
|
||||
let credentials = vec![
|
||||
create_test_credential("Local API", "http://localhost:3000", "dev@api"),
|
||||
create_test_credential("Local Frontend", "http://localhost:8080", "dev@frontend"),
|
||||
create_test_credential("Local Backend", "http://localhost:81", "dev@backend"),
|
||||
create_test_credential("Local No Port", "http://localhost", "dev@default"),
|
||||
];
|
||||
|
||||
// Visiting localhost:81 should only match the :81 credential
|
||||
let matches_81 = filter(credentials.clone(), "http://localhost:81", "");
|
||||
assert_eq!(matches_81.len(), 1, "Should only return exact localhost:81 match");
|
||||
assert_eq!(matches_81[0].item_name.as_deref(), Some("Local Backend"));
|
||||
|
||||
// Visiting localhost:3000 should only match the :3000 credential
|
||||
let matches_3000 = filter(credentials.clone(), "http://localhost:3000", "");
|
||||
assert_eq!(matches_3000.len(), 1, "Should only return exact localhost:3000 match");
|
||||
assert_eq!(matches_3000[0].item_name.as_deref(), Some("Local API"));
|
||||
|
||||
// Visiting localhost:8080 should only match the :8080 credential
|
||||
let matches_8080 = filter(credentials.clone(), "http://localhost:8080", "");
|
||||
assert_eq!(matches_8080.len(), 1, "Should only return exact localhost:8080 match");
|
||||
assert_eq!(matches_8080[0].item_name.as_deref(), Some("Local Frontend"));
|
||||
|
||||
// Visiting localhost (no port) should only match the no-port credential
|
||||
let matches_no_port = filter(credentials.clone(), "http://localhost", "");
|
||||
assert_eq!(matches_no_port.len(), 1, "Should only return localhost without port match");
|
||||
assert_eq!(matches_no_port[0].item_name.as_deref(), Some("Local No Port"));
|
||||
|
||||
// Visiting localhost with a port not in credentials should show domain matches (up to 3)
|
||||
let matches_unknown_port = filter(credentials.clone(), "http://localhost:5000", "");
|
||||
assert_eq!(matches_unknown_port.len(), 3, "Should return domain matches when no exact port match");
|
||||
}
|
||||
|
||||
/// [#42] - Localhost matching - exact URL stored
|
||||
/// User's specific scenario: credential with exactly "http://localhost:81" should match
|
||||
#[test]
|
||||
fn test_localhost_exact_url_stored() {
|
||||
let credentials = vec![
|
||||
create_test_credential("My Local Service", "http://localhost:81", "user@local"),
|
||||
];
|
||||
|
||||
// Should match when visiting exactly http://localhost:81
|
||||
let matches = filter(credentials.clone(), "http://localhost:81", "");
|
||||
assert_eq!(matches.len(), 1, "Should match exact localhost:81 URL");
|
||||
assert_eq!(matches[0].item_name.as_deref(), Some("My Local Service"));
|
||||
|
||||
// Should also match with path
|
||||
let matches_with_path = filter(credentials.clone(), "http://localhost:81/some/path", "");
|
||||
assert_eq!(matches_with_path.len(), 1, "Should match localhost:81 with path");
|
||||
|
||||
// Should also match with query string
|
||||
let matches_with_query = filter(credentials.clone(), "http://localhost:81?debug=true", "");
|
||||
assert_eq!(matches_with_query.len(), 1, "Should match localhost:81 with query");
|
||||
}
|
||||
|
||||
/// [#43] - Single-word hostname matching (homelab/self-hosted scenarios)
|
||||
/// Common scenario: self-hosted services with local DNS or /etc/hosts entries
|
||||
/// like "plex", "nas", "router", "homeassistant", etc.
|
||||
#[test]
|
||||
fn test_single_word_hostname_matching() {
|
||||
let credentials = vec![
|
||||
create_test_credential("Plex Media Server", "http://plex:32400", "admin@plex"),
|
||||
create_test_credential("Synology NAS", "https://nas:5001", "admin@nas"),
|
||||
create_test_credential("Pi-hole", "http://pihole/admin", "admin@pihole"),
|
||||
create_test_credential("Home Assistant", "http://homeassistant:8123", "admin@ha"),
|
||||
create_test_credential("Router Admin", "http://router", "admin@router"),
|
||||
];
|
||||
|
||||
// Plex with exact port
|
||||
let plex_matches = filter(credentials.clone(), "http://plex:32400", "");
|
||||
assert_eq!(plex_matches.len(), 1, "Should match plex:32400");
|
||||
assert_eq!(plex_matches[0].item_name.as_deref(), Some("Plex Media Server"));
|
||||
|
||||
// Plex with path
|
||||
let plex_path = filter(credentials.clone(), "http://plex:32400/web/index.html", "");
|
||||
assert_eq!(plex_path.len(), 1, "Should match plex:32400 with path");
|
||||
|
||||
// NAS with HTTPS
|
||||
let nas_matches = filter(credentials.clone(), "https://nas:5001", "");
|
||||
assert_eq!(nas_matches.len(), 1, "Should match nas:5001");
|
||||
assert_eq!(nas_matches[0].item_name.as_deref(), Some("Synology NAS"));
|
||||
|
||||
// Pi-hole with path
|
||||
let pihole_matches = filter(credentials.clone(), "http://pihole/admin/index.php", "");
|
||||
assert_eq!(pihole_matches.len(), 1, "Should match pihole with path");
|
||||
assert_eq!(pihole_matches[0].item_name.as_deref(), Some("Pi-hole"));
|
||||
|
||||
// Home Assistant
|
||||
let ha_matches = filter(credentials.clone(), "http://homeassistant:8123/dashboard", "");
|
||||
assert_eq!(ha_matches.len(), 1, "Should match homeassistant:8123");
|
||||
assert_eq!(ha_matches[0].item_name.as_deref(), Some("Home Assistant"));
|
||||
|
||||
// Router without port
|
||||
let router_matches = filter(credentials.clone(), "http://router", "");
|
||||
assert_eq!(router_matches.len(), 1, "Should match router without port");
|
||||
assert_eq!(router_matches[0].item_name.as_deref(), Some("Router Admin"));
|
||||
|
||||
// Router with different port should still match via domain
|
||||
let router_port = filter(credentials.clone(), "http://router:8080", "");
|
||||
assert_eq!(router_port.len(), 1, "Should match router with different port via domain match");
|
||||
}
|
||||
|
||||
/// [#44] - Single-word hostnames require protocol for extraction
|
||||
/// Credentials stored WITH protocol should match current URLs with protocol.
|
||||
/// Note: Browser always sends URLs with protocol, so this mainly affects
|
||||
/// what URLs can be stored in credentials.
|
||||
#[test]
|
||||
fn test_single_word_hostname_extraction_requires_protocol() {
|
||||
// Credential stored WITHOUT protocol - should NOT be matchable
|
||||
let credentials_no_protocol = vec![
|
||||
create_test_credential("Plex No Protocol", "plex:32400", "admin@plex"),
|
||||
];
|
||||
|
||||
// Even with protocol in current URL, credential without protocol won't match
|
||||
// because the credential URL "plex:32400" can't be extracted as a domain
|
||||
let no_match = filter(credentials_no_protocol.clone(), "http://plex:32400", "");
|
||||
assert_eq!(no_match.len(), 0, "Credential without protocol should not be matchable");
|
||||
|
||||
// Credential stored WITH protocol - should match
|
||||
let credentials_with_protocol = vec![
|
||||
create_test_credential("Plex With Protocol", "http://plex:32400", "admin@plex"),
|
||||
];
|
||||
|
||||
let with_match = filter(credentials_with_protocol.clone(), "http://plex:32400", "");
|
||||
assert_eq!(with_match.len(), 1, "Credential with protocol should match");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user