From 882cb84d5ecfeb1c29d7d19b18058730c6c08dc5 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Fri, 4 Nov 2022 14:55:14 +0700 Subject: [PATCH] Ignore URLs with custom protocols (#178) --- index.d.ts | 2 ++ index.js | 19 +++++++++++++++++-- readme.md | 2 ++ test.js | 31 ++++++++++++------------------- 4 files changed, 33 insertions(+), 21 deletions(-) diff --git a/index.d.ts b/index.d.ts index bf356b3..2bd85e8 100644 --- a/index.d.ts +++ b/index.d.ts @@ -285,6 +285,8 @@ export interface Options { /** [Normalize](https://en.wikipedia.org/wiki/URL_normalization) a URL. +URLs with custom protocols are not normalized and just passed through by default. Supported protocols are: `https`, `http`, `file`, and `data`. + @param url - URL to normalize, including [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs). @example diff --git a/index.js b/index.js index 33203d8..3a24713 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,21 @@ const DATA_URL_DEFAULT_CHARSET = 'us-ascii'; const testParameter = (name, filters) => filters.some(filter => filter instanceof RegExp ? filter.test(name) : filter === name); +const supportedProtocols = new Set([ + 'https:', + 'http:', + 'file:', +]); + +const hasCustomProtocol = urlString => { + try { + const {protocol} = new URL(urlString); + return protocol.endsWith(':') && !supportedProtocols.has(protocol); + } catch { + return false; + } +}; + const normalizeDataURL = (urlString, {stripHash}) => { const match = /^data:(?[^,]*?),(?[^#]*?)(?:#(?.*))?$/.exec(urlString); @@ -81,8 +96,8 @@ export default function normalizeUrl(urlString, options) { return normalizeDataURL(urlString, options); } - if (/^view-source:/i.test(urlString)) { - throw new Error('`view-source:` is not supported as it is a non-standard protocol'); + if (hasCustomProtocol(urlString)) { + return urlString; } const hasRelativeProtocol = urlString.startsWith('//'); diff --git a/readme.md b/readme.md index c330903..4b61182 100644 --- a/readme.md +++ b/readme.md @@ -30,6 +30,8 @@ normalizeUrl('//www.sindresorhus.com:80/../baz?b=bar&a=foo'); ### normalizeUrl(url, options?) +URLs with custom protocols are not normalized and just passed through by default. Supported protocols are: `https`, `http`, `file`, and `data`. + #### url Type: `string` diff --git a/test.js b/test.js index 902ecf1..9b80824 100644 --- a/test.js +++ b/test.js @@ -12,7 +12,6 @@ test('main', t => { t.is(normalizeUrl('http://sindresorhus.com'), 'http://sindresorhus.com'); t.is(normalizeUrl('http://sindresorhus.com:80'), 'http://sindresorhus.com'); t.is(normalizeUrl('https://sindresorhus.com:443'), 'https://sindresorhus.com'); - t.is(normalizeUrl('ftp://sindresorhus.com:21'), 'ftp://sindresorhus.com'); t.is(normalizeUrl('http://www.sindresorhus.com'), 'http://sindresorhus.com'); t.is(normalizeUrl('www.com'), 'http://www.com'); t.is(normalizeUrl('http://www.www.sindresorhus.com'), 'http://www.www.sindresorhus.com'); @@ -37,9 +36,9 @@ test('main', t => { t.is(normalizeUrl('http://sindresorhus.com/foo#bar:~:text=hello%20world', {stripHash: true}), 'http://sindresorhus.com/foo'); t.is(normalizeUrl('http://sindresorhus.com/foo/bar/../baz'), 'http://sindresorhus.com/foo/baz'); t.is(normalizeUrl('http://sindresorhus.com/foo/bar/./baz'), 'http://sindresorhus.com/foo/bar/baz'); - t.is(normalizeUrl('sindre://www.sorhus.com'), 'sindre://sorhus.com'); - t.is(normalizeUrl('sindre://www.sorhus.com/'), 'sindre://sorhus.com'); - t.is(normalizeUrl('sindre://www.sorhus.com/foo/bar'), 'sindre://sorhus.com/foo/bar'); + // t.is(normalizeUrl('sindre://www.sorhus.com'), 'sindre://sorhus.com'); + // t.is(normalizeUrl('sindre://www.sorhus.com/'), 'sindre://sorhus.com'); + // t.is(normalizeUrl('sindre://www.sorhus.com/foo/bar'), 'sindre://sorhus.com/foo/bar'); t.is(normalizeUrl('https://i.vimeocdn.com/filter/overlay?src0=https://i.vimeocdn.com/video/598160082_1280x720.jpg&src1=https://f.vimeocdn.com/images_v6/share/play_icon_overlay.png'), 'https://i.vimeocdn.com/filter/overlay?src0=https://i.vimeocdn.com/video/598160082_1280x720.jpg&src1=https://f.vimeocdn.com/images_v6/share/play_icon_overlay.png'); }); @@ -47,17 +46,15 @@ test('stripAuthentication option', t => { t.is(normalizeUrl('http://user:password@www.sindresorhus.com'), 'http://sindresorhus.com'); t.is(normalizeUrl('https://user:password@www.sindresorhus.com'), 'https://sindresorhus.com'); t.is(normalizeUrl('https://user:password@www.sindresorhus.com/@user'), 'https://sindresorhus.com/@user'); - t.is(normalizeUrl('user:password@sindresorhus.com'), 'http://sindresorhus.com'); t.is(normalizeUrl('http://user:password@www.êxample.com'), 'http://xn--xample-hva.com'); - t.is(normalizeUrl('sindre://user:password@www.sorhus.com'), 'sindre://sorhus.com'); + // t.is(normalizeUrl('sindre://user:password@www.sorhus.com'), 'sindre://sorhus.com'); const options = {stripAuthentication: false}; t.is(normalizeUrl('http://user:password@www.sindresorhus.com', options), 'http://user:password@sindresorhus.com'); t.is(normalizeUrl('https://user:password@www.sindresorhus.com', options), 'https://user:password@sindresorhus.com'); t.is(normalizeUrl('https://user:password@www.sindresorhus.com/@user', options), 'https://user:password@sindresorhus.com/@user'); - t.is(normalizeUrl('user:password@sindresorhus.com', options), 'http://user:password@sindresorhus.com'); t.is(normalizeUrl('http://user:password@www.êxample.com', options), 'http://user:password@xn--xample-hva.com'); - t.is(normalizeUrl('sindre://user:password@www.sorhus.com', options), 'sindre://user:password@sorhus.com'); + // t.is(normalizeUrl('sindre://user:password@www.sorhus.com', options), 'sindre://user:password@sorhus.com'); }); test('stripProtocol option', t => { @@ -66,8 +63,6 @@ test('stripProtocol option', t => { t.is(normalizeUrl('http://sindresorhus.com', options), 'sindresorhus.com'); t.is(normalizeUrl('https://www.sindresorhus.com', options), 'sindresorhus.com'); t.is(normalizeUrl('//www.sindresorhus.com', options), 'sindresorhus.com'); - t.is(normalizeUrl('sindre://user:password@www.sorhus.com', options), 'sindre://sorhus.com'); - t.is(normalizeUrl('sindre://www.sorhus.com', options), 'sindre://sorhus.com'); }); test('stripTextFragment option', t => { @@ -98,7 +93,7 @@ test('stripWWW option', t => { t.is(normalizeUrl('http://www.sindresorhus.com', options), 'http://www.sindresorhus.com'); t.is(normalizeUrl('www.sindresorhus.com', options), 'http://www.sindresorhus.com'); t.is(normalizeUrl('http://www.êxample.com', options), 'http://www.xn--xample-hva.com'); - t.is(normalizeUrl('sindre://www.sorhus.com', options), 'sindre://www.sorhus.com'); + // t.is(normalizeUrl('sindre://www.sorhus.com', options), 'sindre://www.sorhus.com'); const options2 = {stripWWW: true}; t.is(normalizeUrl('http://www.vue.amsterdam', options2), 'http://vue.amsterdam'); @@ -393,14 +388,6 @@ test('prevents homograph attack', t => { t.is(normalizeUrl('https://ebаy.com'), 'https://xn--eby-7cd.com'); }); -test('view-source URL', t => { - t.throws(() => { - normalizeUrl('view-source:https://www.sindresorhus.com'); - }, { - message: '`view-source:` is not supported as it is a non-standard protocol', - }); -}); - test('does not have exponential performance for data URLs', t => { for (let index = 0; index < 1000; index += 50) { const url = 'data:' + Array.from({length: index}).fill(',#').join('') + '\ra'; @@ -414,3 +401,9 @@ test('does not have exponential performance for data URLs', t => { t.true(difference < 100, `Execution time: ${difference}`); } }); + +test('ignore custom schemes', t => { + t.is(normalizeUrl('tel:004346382763'), 'tel:004346382763'); + t.is(normalizeUrl('mailto:office@foo.com'), 'mailto:office@foo.com'); + t.is(normalizeUrl('sindre://www.sindresorhus.com'), 'sindre://www.sindresorhus.com'); +});