diff --git a/src/url.ts b/src/url.ts index c600e47..7a3de3e 100644 --- a/src/url.ts +++ b/src/url.ts @@ -58,7 +58,7 @@ for (let c = 0x61; c <= 0x7a; c++) { safeHostChar[c] = 1 } ;(() => { - const chars = '.-_' + const chars = '.-_:' for (let i = 0; i < chars.length; i++) { safeHostChar[chars.charCodeAt(i)] = 1 } @@ -74,6 +74,31 @@ export const buildUrl = (scheme: string, host: string, incomingUrl: string) => { needsHostValidationByURL = true break } + if (c === 0x3a) { + // ':' + i++ + const firstDigit = host.charCodeAt(i) + + // if the number starts with 1-9 and ranges from 1000-59999, then there is no need for normalization, so proceed + if ( + firstDigit < 0x31 || + firstDigit > 0x39 || + i + 4 > len || + i + (firstDigit < 0x36 ? 5 : 4) < len + ) { + needsHostValidationByURL = true + break + } + for (; i < len; i++) { + const c = host.charCodeAt(i) + if (c < 0x30 || c > 0x39) { + needsHostValidationByURL = true + break + } + } + + // valid port number + } } if (needsHostValidationByURL) { diff --git a/test/url.test.ts b/test/url.test.ts index e1aeffc..38ef76d 100644 --- a/test/url.test.ts +++ b/test/url.test.ts @@ -18,41 +18,47 @@ describe('buildUrl', () => { describe('URL normalization', () => { test.each([ - ['[::1]', '/foo.txt'], - ['[::1]:8080', '/foo.txt'], - ['localhost', '/'], - ['localhost', '/foo/bar/baz'], - ['localhost', '/foo_bar'], - ['localhost', '/foo//bar'], - ['localhost', '/static/%2e%2e/foo.txt'], - ['localhost', '/static\\..\\foo.txt'], - ['localhost', '/..'], - ['localhost', '/foo/.'], - ['localhost', '/foo/bar/..'], - ['localhost', '/a/b/../../c'], - ['localhost', '/a/../../../b'], - ['localhost', '/a/b/c/../../../'], - ['localhost', '/./foo.txt'], - ['localhost', '/foo/../bar.txt'], - ['localhost', '/a/./b/../c?q=%2E%2E#hash'], - ['localhost', '/foo/%2E/bar/../baz'], - ['localhost', '/hello%20world'], - ['localhost', '/foo%23bar'], - ['localhost', '/foo"bar'], - ['localhost', '/%2e%2E/foo'], - ['localhost', '/caf%C3%A9'], - ['localhost', '/foo%2fbar/..//baz'], - ['localhost', '/foo?q=../bar'], - ['localhost', '/path?q=hello%20world'], - ['localhost', '/file.txt'], - ['localhost', ''], - ['LOCALHOST', '/foo.txt'], - ['LOCALHOST:80', '/foo.txt'], - ['LOCALHOST:443', '/foo.txt'], - ['LOCALHOST:8080', '/foo.txt'], - ['Localhost:3000', '/foo.txt'], - ])('Should normalize %s to %s', async (host, url) => { - expect(buildUrl('http', host, url)).toBe(new URL(url, `http://${host}`).href) + ['https', '[::1]', '/foo.txt'], + ['https', '[::1]:8080', '/foo.txt'], + ['https', 'localhost', '/'], + ['https', 'localhost', '/foo/bar/baz'], + ['https', 'localhost', '/foo_bar'], + ['https', 'localhost', '/foo//bar'], + ['https', 'localhost', '/static/%2e%2e/foo.txt'], + ['https', 'localhost', '/static\\..\\foo.txt'], + ['https', 'localhost', '/..'], + ['https', 'localhost', '/foo/.'], + ['https', 'localhost', '/foo/bar/..'], + ['https', 'localhost', '/a/b/../../c'], + ['https', 'localhost', '/a/../../../b'], + ['https', 'localhost', '/a/b/c/../../../'], + ['https', 'localhost', '/./foo.txt'], + ['https', 'localhost', '/foo/../bar.txt'], + ['https', 'localhost', '/a/./b/../c?q=%2E%2E#hash'], + ['https', 'localhost', '/foo/%2E/bar/../baz'], + ['https', 'localhost', '/hello%20world'], + ['https', 'localhost', '/foo%23bar'], + ['https', 'localhost', '/foo"bar'], + ['https', 'localhost', '/%2e%2E/foo'], + ['https', 'localhost', '/caf%C3%A9'], + ['https', 'localhost', '/foo%2fbar/..//baz'], + ['https', 'localhost', '/foo?q=../bar'], + ['https', 'localhost', '/path?q=hello%20world'], + ['https', 'localhost', '/file.txt'], + ['https', 'localhost', ''], + ['http', 'localhost:080', '/foo.txt'], + ['http', 'localhost:08080', '/foo.txt'], + ['http', 'localhost:80', '/foo.txt'], + ['https', 'localhost:80', '/foo.txt'], + ['http', 'localhost:443', '/foo.txt'], + ['https', 'localhost:443', '/foo.txt'], + ['https', 'LOCALHOST', '/foo.txt'], + ['https', 'LOCALHOST:80', '/foo.txt'], + ['https', 'LOCALHOST:443', '/foo.txt'], + ['https', 'LOCALHOST:8080', '/foo.txt'], + ['https', 'Localhost:3000', '/foo.txt'], + ])('Should successfully normalize `%s://%s%s`', async (scheme, host, path) => { + expect(buildUrl(scheme, host, path)).toBe(new URL(path, `${scheme}://${host}`).href) }) it('Should throw a RequestError for non-origin-form request-target', async () => {