gglib_core/
is_local_origin.rs1use url::Url;
7
8pub fn is_local_origin(origin: &str) -> bool {
18 if let Some(stripped) = origin
20 .strip_prefix("tauri://")
21 .or_else(|| origin.strip_prefix("asset://"))
22 {
23 let host = stripped.trim_end_matches('/');
24 if host.contains('@') {
27 return false;
28 }
29 return matches!(host, "localhost" | "127.0.0.1" | "[::1]" | "::1");
30 }
31
32 let Ok(parsed) = Url::parse(origin) else {
33 return false;
34 };
35
36 if !matches!(parsed.scheme(), "http" | "https") {
38 return false;
39 }
40
41 let Some(host_str) = parsed.host_str() else {
42 return false;
43 };
44
45 if !parsed.username().is_empty() || parsed.password().is_some() {
48 return false;
49 }
50
51 let host = host_str.trim_start_matches('[').trim_end_matches(']');
54
55 matches!(
58 host.to_lowercase().as_str(),
59 "localhost" | "127.0.0.1" | "::1" | "tauri.localhost"
60 )
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66
67 #[test]
68 fn accepts_localhost_http() {
69 assert!(is_local_origin("http://localhost"));
70 }
71
72 #[test]
73 fn accepts_localhost_https() {
74 assert!(is_local_origin("https://localhost"));
75 }
76
77 #[test]
78 fn accepts_localhost_with_port() {
79 assert!(is_local_origin("http://localhost:3000"));
80 assert!(is_local_origin("https://localhost:8080"));
81 }
82
83 #[test]
84 fn accepts_127_0_0_1() {
85 assert!(is_local_origin("http://127.0.0.1"));
86 assert!(is_local_origin("https://127.0.0.1"));
87 assert!(is_local_origin("http://127.0.0.1:9887"));
88 }
89
90 #[test]
91 fn accepts_ipv6_loopback() {
92 assert!(is_local_origin("http://[::1]"));
93 assert!(is_local_origin("https://[::1]"));
94 assert!(is_local_origin("http://[::1]:3000"));
95 }
96
97 #[test]
98 fn rejects_subdomain_of_localhost() {
99 assert!(!is_local_origin("http://localhost.evil.com"));
100 }
101
102 #[test]
103 fn rejects_notlocalhost() {
104 assert!(!is_local_origin("http://notlocalhost"));
105 }
106
107 #[test]
108 fn rejects_non_loopback_ip() {
109 assert!(!is_local_origin("http://127.0.0.2"));
110 }
111
112 #[test]
113 fn rejects_external_host() {
114 assert!(!is_local_origin("https://example.com"));
115 }
116
117 #[test]
118 fn rejects_non_loopback_ipv6() {
119 assert!(!is_local_origin("http://[::2]"));
120 }
121
122 #[test]
123 fn rejects_empty_string() {
124 assert!(!is_local_origin(""));
125 }
126
127 #[test]
128 fn rejects_malformed_url() {
129 assert!(!is_local_origin("not-a-url"));
130 }
131
132 #[test]
133 fn rejects_non_http_scheme() {
134 assert!(!is_local_origin("ftp://localhost"));
135 }
136
137 #[test]
138 fn rejects_url_encoded_bypass() {
139 assert!(!is_local_origin("http://localhost:8080@evil.com"));
141 }
142
143 #[test]
144 fn accepts_tauri_scheme_localhost() {
145 assert!(is_local_origin("tauri://localhost"));
146 assert!(is_local_origin("tauri://localhost/"));
147 }
148
149 #[test]
150 fn accepts_asset_scheme_localhost() {
151 assert!(is_local_origin("asset://localhost"));
152 }
153
154 #[test]
155 fn accepts_http_tauri_localhost() {
156 assert!(is_local_origin("http://tauri.localhost"));
157 assert!(is_local_origin("http://tauri.localhost:3000"));
158 }
159
160 #[test]
161 fn rejects_userinfo_localhost() {
162 assert!(!is_local_origin("http://user@localhost"));
164 }
165
166 #[test]
167 fn rejects_userinfo_with_password() {
168 assert!(!is_local_origin("http://user:pass@localhost"));
169 }
170
171 #[test]
172 fn rejects_tauri_scheme_userinfo() {
173 assert!(!is_local_origin("tauri://user@localhost"));
174 }
175
176 #[test]
177 fn rejects_asset_scheme_external_host() {
178 assert!(!is_local_origin("asset://evil.com"));
179 }
180
181 #[test]
182 fn accepts_https_tauri_localhost() {
183 assert!(is_local_origin("https://tauri.localhost"));
184 }
185
186 #[test]
187 fn accepts_uppercase_localhost() {
188 assert!(is_local_origin("http://LOCALHOST"));
189 }
190}