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
5 changes: 1 addition & 4 deletions src/net-util/export/HostInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,7 @@ export class HostInfo extends IntfDeconstructable {
constructor(nameString, portNumber) {
super();

// Note: The regex is a bit lenient, though notably it _does_ at least
// guarantee that there are no uppercase letters. TODO: Maybe it should be
// more restrictive?
this.#nameString = MustBe.string(nameString, /^[-_.:a-z0-9]+$/);
this.#nameString = HostUtil.mustBeHostname(nameString);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, super-duper sorry about this (I am still paging this code back into my head):

The word "maybe" in the TODO comment is pretty load-bearing in this case, and the point of the note wasn't to say "this should definitely happen, but with low priority," but rather, "I am really not sure if it should be done at all." Specifically, the only way this constructor ever gets invoked in practice is through the static methods on the class, and all of those do canonicalization and validation of some sort or another. To the extent that we add more here, it shouldn't be duplicative of anything already done. So, the right path forward will look more like doing stuff here (including canonicalization, not just error checking) and also getting rid of the stuff in the clients of this constructor which is thereby rendered moot.

As such, arguably we don't need the new method in HostUtil.

I'm leaving the comments I made in HostUtil (which I made before writing this one), more for reference/advice about general stuff than necessarily to be acted on in this PR.


this.#portNumber = AskIf.string(portNumber, /^0*[0-9]{1,5}$/)
? Number(portNumber)
Expand Down
58 changes: 52 additions & 6 deletions src/net-util/export/HostUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,27 +149,73 @@ export class HostUtil {
static parseHostnameElseNull(name, allowWildcard = false) {
MustBe.string(name);

const result = this.#pathArrayFromHostnameElseNull(name, allowWildcard);

if (!result) {
return null;
}

return new PathKey(result.pathArray, result.wildcard);
}

/**
* Validates that the given value is a valid hostname string, returning it
* (canonicalized) if so. This accepts both DNS names and IP addresses
* (including bracketed IPv6), but not wildcards.
*
* @param {*} value Value to check.
* @returns {string} `value` canonicalized if it is a valid hostname.
* @throws {Error} Thrown if `value` is not a valid hostname string.
*/
static mustBeHostname(value) {
if (typeof value !== 'string') {
Copy link
Owner

@danfuzz danfuzz Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More apologies (same reason as stated in HostInfo): There is already canonicalizeHostnameElseNull(), which would have been a better starting point for mustBeHostname() than parse...(). Specifically, given that this is a mustBe...() method the pattern in the system is that it returns the given value per se, and so there's no need to actually split up the string etc. etc.

throw new Error(`Expected hostname string, got: ${typeof value}`);
}

const result = this.canonicalizeHostnameElseNull(value, false);

if (result === null) {
throw new Error(`Invalid hostname: ${value}`);
}

return result;
}

/**
* Private helper that validates a hostname and returns its path array
* representation, or `null` if invalid.
*
* @param {*} name Hostname to parse.
* @param {boolean} allowWildcard Is a wildcard form allowed?
* @returns {?{pathArray: string[], wildcard: boolean}} The path array and
* wildcard flag, or `null` if invalid.
*/
static #pathArrayFromHostnameElseNull(name, allowWildcard) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This no longer needs to be extracted.

if (typeof name !== 'string') {
return null;
}

// Handle IP address cases.
const canonicalIp = EndpointAddress.canonicalizeAddressElseNull(name, false);
if (canonicalIp) {
return new PathKey([canonicalIp], false);
return { pathArray: [canonicalIp], wildcard: false };
}

if (!AskIf.string(name, this.#HOSTNAME_REGEX)) {
return null;
}

const path = name.toLowerCase().split('.').reverse();
const pathArray = name.toLowerCase().split('.').reverse();

if (path[path.length - 1] === '*') {
if (pathArray[pathArray.length - 1] === '*') {
if (allowWildcard) {
path.pop();
return new PathKey(path, true);
pathArray.pop();
return { pathArray, wildcard: true };
} else {
return null;
}
} else {
return new PathKey(path, false);
return { pathArray, wildcard: false };
}
}
}
31 changes: 26 additions & 5 deletions src/net-util/tests/HostInfo.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@ describe('constructor', () => {
${1}
${['x']}
${''}
${'[a::b]'} // IPv6 addresses must not use brackets.
${'--bad'} // Starts with hyphen.
${'.leading'} // Starts with dot.
${'trailing.'} // Ends with dot.
${'double..dot'} // Consecutive dots.
${'-start'} // Label starts with hyphen.
${'end-'} // Label ends with hyphen.
${'mid.-bad'} // Label starts with hyphen after dot.
${'bad-.mid'} // Label ends with hyphen before dot.
`('fails when passing name as $arg', ({ arg }) => {
expect(() => new HostInfo(arg, 123)).toThrow();
});
Expand Down Expand Up @@ -82,10 +89,18 @@ describe('constructor', () => {
arg
${'host'}
${'host.sub'}
${'1.2.3.4'}
${'01.02.03.04'}
${'a:b::c:d'}
${'0a:0123:0:0::987a'}
${'a'} // Single character.
${'localhost'} // Single label.
${'my-host'} // Hyphen mid-label.
${'a1.b2.c3'} // Alphanumeric labels.
${'example.com'} // Typical domain.
${'sub.example.com'} // Multi-level domain.
${'1.2.3.4'} // IPv4 address.
${'01.02.03.04'} // IPv4 with leading zeros.
${'a:b::c:d'} // IPv6 address.
${'0a:0123:0:0::987a'} // IPv6 with leading zeros.
${'::1'} // IPv6 loopback.
${'[a::b]'} // Bracketed IPv6 (brackets stripped on canonicalization).
`('accepts valid host name $arg', ({ arg }) => {
expect(() => new HostInfo(arg, 1)).not.toThrow();
});
Expand Down Expand Up @@ -120,6 +135,12 @@ describe('.nameString', () => {

expect(hi.nameString).toBe(name);
});

test('canonicalizes bracketed IPv6 to bracket-free form', () => {
const hi = new HostInfo('[a::b]', 123);

expect(hi.nameString).toBe('a::b');
});
});

describe('.namePortString', () => {
Expand Down
Loading