Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions lib/src/components/wall/browser-url.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,19 @@ describe('normalizeNavUrl', () => {
expect(normalizeNavUrl('app.localhost:3000')).toBe('http://app.localhost:3000');
});

it('adds http:// for a bracketed IPv6 loopback literal, with or without a port', () => {
// `[::1]` is loopback whether or not a port pins it to http: splitting the
// authority on the first `:` would read the hostname as `[` and fall through
// to https, which just SSL-errors there.
expect(normalizeNavUrl('[::1]')).toBe('http://[::1]');
expect(normalizeNavUrl('[::1]/app?q=1')).toBe('http://[::1]/app?q=1');
expect(normalizeNavUrl('[::1]:5173')).toBe('http://[::1]:5173');
});

it('adds https:// for a bare non-loopback IPv6 literal', () => {
expect(normalizeNavUrl('[2001:db8::1]')).toBe('https://[2001:db8::1]');
});

it('adds http:// for any host with an explicit port (matches the dor CLI)', () => {
// The port is the dev/infra-server signal — LAN and Tailnet hosts speak http.
expect(normalizeNavUrl('example.com:8080')).toBe('http://example.com:8080');
Expand Down
12 changes: 11 additions & 1 deletion lib/src/components/wall/browser-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,21 @@ export function normalizeNavUrl(raw: string): string {
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(trimmed)) return trimmed;
if (/^(about|data|blob|mailto|tel|javascript|view-source|chrome):/i.test(trimmed)) return trimmed;
const authority = trimmed.split(/[/?#]/, 1)[0];
const hostname = authority.split(':', 1)[0];
const hostname = authorityHostname(authority);
const scheme = /:\d+$/.test(authority) || isLoopbackHostname(hostname) ? 'http' : 'https';
return `${scheme}://${trimmed}`;
}

/** The host part of a schemeless authority, minus any `:port`. An IPv6 literal
* is bracketed and full of colons, so splitting on the first `:` would yield
* `[` — take everything through the closing bracket instead, which keeps the
* form `isLoopbackHostname` recognizes. */
function authorityHostname(authority: string): string {
if (!authority.startsWith('[')) return authority.split(':', 1)[0];
const close = authority.indexOf(']');
return close === -1 ? authority : authority.slice(0, close + 1);
}

/** True for hostnames that resolve to the local machine. `*.localhost` is
* included because browsers route it to loopback per the RFC. */
function isLoopbackHostname(hostname: string): boolean {
Expand Down