Compare scheme and host case-insensitively in isCloudWorkstation - #10235
Compare scheme and host case-insensitively in isCloudWorkstation#10235Zuhef wants to merge 1 commit into
Conversation
isCloudWorkstation matched both the scheme prefix and the host suffix with
case-sensitive comparisons, but URL schemes and host names are both
case-insensitive. That produced two wrong answers:
isCloudWorkstation('ABC.CloudWorkstations.dev')
// false, although 'abc.cloudworkstations.dev' is true and DNS does not
// distinguish the two
isCloudWorkstation('HTTPS://example.com/abc.cloudworkstations.dev')
// true, because 'HTTPS://' did not match the lower-case prefixes, so the
// whole URL was treated as a host name and the path matched the suffix.
// The same URL with a lower-case scheme correctly returns false.
Lower-case the value before both comparisons. Negative cases are unaffected:
'evil-cloudworkstations.dev' and 'abc.cloudworkstations.dev.example.com'
still return false, since the suffix check keeps its leading dot and stays
anchored to the end.
packages/util/src/url.ts had no test file, so add one.
🦋 Changeset detectedLatest commit: d25efc9 The changes in this PR will be included in the next version bump. This PR includes changesets to release 30 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
Code Review
This pull request updates the isCloudWorkstation() utility function to compare URL schemes and hostnames case-insensitively, preventing issues with mixed-case hosts or upper-case schemes. It also adds comprehensive unit tests to verify this behavior. The reviewer suggested an optimization to avoid a redundant .toLowerCase() call on the extracted hostname, leveraging the fact that the WHATWG URL specification guarantees new URL(url).hostname is already lowercase.
| const lowerCaseUrl = url.toLowerCase(); | ||
| const host = | ||
| url.startsWith('http://') || url.startsWith('https://') | ||
| lowerCaseUrl.startsWith('http://') || lowerCaseUrl.startsWith('https://') | ||
| ? new URL(url).hostname | ||
| : url; | ||
| return host.endsWith('.cloudworkstations.dev'); | ||
| return host.toLowerCase().endsWith('.cloudworkstations.dev'); |
There was a problem hiding this comment.
Since new URL(url).hostname is guaranteed to be lowercase by the WHATWG URL specification, and we already have lowerCaseUrl computed, we can use lowerCaseUrl as the fallback when no protocol is present. This allows us to avoid the redundant .toLowerCase() call on host before checking the suffix.
| const lowerCaseUrl = url.toLowerCase(); | |
| const host = | |
| url.startsWith('http://') || url.startsWith('https://') | |
| lowerCaseUrl.startsWith('http://') || lowerCaseUrl.startsWith('https://') | |
| ? new URL(url).hostname | |
| : url; | |
| return host.endsWith('.cloudworkstations.dev'); | |
| return host.toLowerCase().endsWith('.cloudworkstations.dev'); | |
| const lowerCaseUrl = url.toLowerCase(); | |
| const host = | |
| lowerCaseUrl.startsWith('http://') || lowerCaseUrl.startsWith('https://') | |
| ? new URL(url).hostname | |
| : lowerCaseUrl; | |
| return host.endsWith('.cloudworkstations.dev'); |
Problem
isCloudWorkstation()compares both the scheme prefix and the host suffix case-sensitively, but URL schemes and host names are both case-insensitive (RFC 3986 §3.1, RFC 4343). That produces two wrong answers:The second one is the direction that matters more: it makes a non-cloud-workstation URL look like a cloud workstation host, and callers use that answer to decide cloud-workstation-specific behaviour (
packages/util/src/url.tsalso exposespingServer, which sendscredentials: 'include').Note the mixed-case host only fails in the bare-hostname branch —
new URL(url).hostnamealready lower-cases, sohttps://ABC.CloudWorkstations.devhappens to work whileABC.CloudWorkstations.devdoes not. So the two documented call styles disagree with each other today.Fix
Lower-case the value before both comparisons.
Negative cases are unaffected — the suffix check keeps its leading dot and stays anchored to the end, so these still return
false:evil-cloudworkstations.devabc.cloudworkstations.dev.example.comhttps://example.com/abc.cloudworkstations.devFull matrix I verified, before vs after:
abc.cloudworkstations.devABC.CloudWorkstations.devhttps://abc.cloudworkstations.devhttps://ABC.CloudWorkstations.devHTTPS://abc.cloudworkstations.devHTTPS://example.com/abc.cloudworkstations.devhttps://example.com/abc.cloudworkstations.devevil-cloudworkstations.devabc.cloudworkstations.dev.example.com''Tests
packages/util/src/url.tshad no test file, so this addspackages/util/test/url.test.tscovering the accepted forms, the case-insensitivity of both scheme and host, and the negative cases above.Baseline on an unmodified checkout is
58 passing, so all 6 additions are new coverage. Reverting onlysrc/url.tsfails exactly the two bug cases:prettier --checkpasses on both files, andtsc --noEmitreports nothing for either — the only diagnostic in my environment comes from a newer@types/nodethan TypeScript 5.5.4 expects, inurl.d.ts.I did not touch
pingServeror any caller, and no existing test referencedisCloudWorkstation.