diff --git a/src/test/unit/url.test.ts b/src/test/unit/url.test.ts new file mode 100644 index 0000000000..2a3837dca2 --- /dev/null +++ b/src/test/unit/url.test.ts @@ -0,0 +1,61 @@ +import { isLocalURL } from 'firefox-profiler/utils/url'; + +describe('isLocalURL', () => { + it('should return true for localhost', () => { + expect(isLocalURL('http://localhost')).toBe(true); + expect(isLocalURL('http://localhost:3000')).toBe(true); + }); + + it('should return true for 127.0.0.1', () => { + expect(isLocalURL('http://127.0.0.1')).toBe(true); + }); + + it('should return true for ::1', () => { + expect(isLocalURL('http://[::1]')).toBe(true); + }); + + it('should return true for IPv6 local addresses', () => { + expect(isLocalURL('http://[fe80::1]')).toBe(true); + expect(isLocalURL('http://[fd00::1]')).toBe(true); + expect(isLocalURL('http://[fc00::1]')).toBe(true); + expect(isLocalURL('http://[ff00::1]')).toBe(false); + expect(isLocalURL('http://[::ffff:1.1.1.1]')).toBe(false); + }); + + it('should return true for LAN addresses', () => { + expect(isLocalURL('http://10.0.0.1')).toBe(true); + expect(isLocalURL('http://10.255.255.255')).toBe(true); + expect(isLocalURL('http://172.16.0.1')).toBe(true); + expect(isLocalURL('http://172.31.255.255')).toBe(true); + expect(isLocalURL('http://192.168.1.1')).toBe(true); + expect(isLocalURL('http://100.100.100.100')).toBe(true); + expect(isLocalURL('http://169.254.1.1')).toBe(true); + expect(isLocalURL('http://198.18.0.1')).toBe(true); + expect(isLocalURL('http://127.0.0.2')).toBe(true); + }); + + it('should return false for other public IP addresses', () => { + expect(isLocalURL('http://8.8.8.8')).toBe(false); + expect(isLocalURL('http://172.15.255.255')).toBe(false); + expect(isLocalURL('http://172.32.0.0')).toBe(false); + }); + + it('should return true for .local domains', () => { + expect(isLocalURL('http://myserver.local')).toBe(true); + expect(isLocalURL('http://test.local:8080')).toBe(true); + }); + + it('should return true for hostnames without dots', () => { + expect(isLocalURL('http://mycomputer')).toBe(true); + expect(isLocalURL('http://dev-server:8000')).toBe(true); + }); + + it('should return false for public domains', () => { + expect(isLocalURL('https://firefox.com')).toBe(false); + expect(isLocalURL('https://profiler.firefox.com')).toBe(false); + }); + + it('should return false for invalid URLs', () => { + expect(isLocalURL('not-a-url')).toBe(false); + }); +}); diff --git a/src/utils/url.ts b/src/utils/url.ts index c42e9272f2..e5fbdb7a22 100644 --- a/src/utils/url.ts +++ b/src/utils/url.ts @@ -5,7 +5,7 @@ export const localhostHostnames: readonly string[] = [ 'localhost', '127.0.0.1', - '::1', + '[::1]', ]; // Used to determine if a URL (usually one that's provided a profile) @@ -15,12 +15,69 @@ export const localhostHostnames: readonly string[] = [ export function isLocalURL(url: string | URL): boolean { try { const parsedUrl = url instanceof URL ? url : new URL(url); - return localhostHostnames.includes(parsedUrl.hostname); + const hostname = parsedUrl.hostname; + if (localhostHostnames.includes(hostname)) { + return true; + } + // IPv4 ranges: + if ( + /^127\./.test(hostname) || // Loopback 127.0.0.0/8 + /^10\./.test(hostname) || // Private 10.0.0.0/8 + /^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(hostname) || // Private 172.16.0.0/12 + /^192\.168\./.test(hostname) || // Private 192.168.0.0/16 + /^100\.(6[4-9]|[7-9][0-9]|1[0-1][0-9]|12[0-7])\./.test(hostname) || // CGNAT 100.64.0.0/10 + /^169\.254\./.test(hostname) || // Link-local 169.254.0.0/16 + /^198\.(1[8-9])\./.test(hostname) // Benchmark 198.18.0.0/15 + ) { + return true; + } + // IPv6 local addresses: + // [fe80::...] (Link-local) + // [fc00::...] or [fd00::...] (Unique Local Address) + if ( + hostname.startsWith('[fe80:') || + hostname.startsWith('[fc00:') || + hostname.startsWith('[fd00:') + ) { + return true; + } + if (isLocalHostName(hostname)) { + return true; + } + return false; } catch (_e) { return false; } } +/** + * http://hostname => true + * https://hostname => true + * http://hostname:8080 => true + * https://hostname.local => true + * http://hostname.local:8080 => true + * http://xxx.com=> false + * http://xxx.com:8080 => false + * http://1.1.1.1=> false + * http://1.1.1.1:8080 => false + * http://[::1]=> false + * http://[::1]:8080 => false + */ +function isLocalHostName(hostname: string): boolean { + if (!hostname) { + return false; + } + + // IPv6 literals are bracketed when parsed from a URL, and IPv4 literals are + // dot-delimited numeric segments. Neither should be treated as local + // hostnames here; those cases are handled separately in isLocalURL. + if (hostname.startsWith('[') || /^\d+(?:\.\d+){3}$/.test(hostname)) { + return false; + } + + return hostname.endsWith('.local') || !hostname.includes('.'); +} + /** * Escape a URL string so it can be safely embedded inside a double-quoted CSS * url("...").