diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5ea8b5837..5d8177c23 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,9 +6,6 @@ updates: interval: weekly open-pull-requests-limit: 10 rebase-strategy: disabled - ignore: - - dependency-name: normalize-url - update-types: [ version-update:semver-major ] - package-ecosystem: github-actions directory: / diff --git a/jest.config.js b/jest.config.js index 3a6e9d7d0..ddc159075 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,15 +3,12 @@ module.exports = { testEnvironment: 'node', runner: 'groups', - // normalize-url exports an ES module so we need to transform js files - // and change the ignore pattern so it is transformed preset: 'ts-jest/presets/js-with-ts', globals: { 'ts-jest': { tsconfig: { allowJs: true } } }, - transformIgnorePatterns: [ '/node_modules/(?!(normalize-url)/)' ], // Coverage options collectCoverageFrom: [ 'src/**' ], diff --git a/package-lock.json b/package-lock.json index 00d7c8d1e..f09541ac2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5272,11 +5272,6 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, - "normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==" - }, "npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", diff --git a/package.json b/package.json index 2615250ec..d4ad70c3f 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,6 @@ }, "dependencies": { "axios": "^0.27.2", - "compare-versions": "^4.0.0", - "normalize-url": "^6.1.0" + "compare-versions": "^4.0.0" } } diff --git a/src/utils/__tests__/address-candidates.test.ts b/src/utils/__tests__/address-candidates.test.ts index 41bf30ea1..f1fae5583 100644 --- a/src/utils/__tests__/address-candidates.test.ts +++ b/src/utils/__tests__/address-candidates.test.ts @@ -38,10 +38,9 @@ describe('Address Candidates', () => { expect(candidates[1]).toBe('http://example.com:8888/'); }); - it('should return the entered url non http(s) protocols', () => { + it('should return an empty list for urls with non http(s) protocols', () => { const candidates = getAddressCandidates('ftp://example.com'); - expect(candidates).toHaveLength(1); - expect(candidates[0]).toBe('ftp://example.com/'); + expect(candidates).toHaveLength(0); }); it('should return an empty list for invalid urls', () => { diff --git a/src/utils/__tests__/normalize-url.test.ts b/src/utils/__tests__/normalize-url.test.ts new file mode 100644 index 000000000..7a4f5f004 --- /dev/null +++ b/src/utils/__tests__/normalize-url.test.ts @@ -0,0 +1,53 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import normalizeUrl from '../normalize-url'; + +/** + * Url normalizing tests. + * + * @group unit/utils + */ +describe('Normalize URLs', () => { + it('should normalize URLs correctly', () => { + expect(normalizeUrl('http://example.com/')).toBe('http://example.com/'); + expect(normalizeUrl('http://example.com')).toBe('http://example.com/'); + expect(normalizeUrl('https://example.com/')).toBe('https://example.com/'); + expect(normalizeUrl('http://example.com/foo/bar/')).toBe('http://example.com/foo/bar'); + + // Don't reduce double slashes if part of a protocol + expect(normalizeUrl('https://example.com/https://jellyfin.org')).toBe('https://example.com/https://jellyfin.org'); + expect(normalizeUrl('https://example.com/https://jellyfin.org/foo//bar')).toBe('https://example.com/https://jellyfin.org/foo/bar'); + expect(normalizeUrl('https://example.com/http://jellyfin.org')).toBe('https://example.com/http://jellyfin.org'); + expect(normalizeUrl('https://example.com/http://jellyfin.org/foo//bar')).toBe('https://example.com/http://jellyfin.org/foo/bar'); + + // Strip trailing dots in domain names + expect(normalizeUrl('http://example.com./')).toBe('http://example.com/'); + + // Strip hashes from URLs + expect(normalizeUrl('http://example.com/#/hash/path')).toBe('http://example.com/'); + }); + + it('should default to using http protocol when not specified', () => { + expect(normalizeUrl('//example.com/')).toBe('http://example.com/'); + expect(normalizeUrl('example.com/')).toBe('http://example.com/'); + expect(normalizeUrl('example.com')).toBe('http://example.com/'); + }); + + it('should throw for non http(s) protocols', () => { + expect(() => { + normalizeUrl('data:ASDF'); + }).toThrow('data URLs are not supported'); + + expect(() => { + normalizeUrl('view-source:example.com'); + }).toThrow('`view-source:` is not supported as it is a non-standard protocol'); + + expect(() => { + normalizeUrl('ftp://example.com'); + }).toThrow('only http or https protocols are supported'); + }); +}); diff --git a/src/utils/normalize-url.ts b/src/utils/normalize-url.ts new file mode 100644 index 000000000..a8e53fa3c --- /dev/null +++ b/src/utils/normalize-url.ts @@ -0,0 +1,115 @@ +/** + * MIT License + * + * Copyright (c) 2022 Jellyfin Contributors + * Copyright (c) 2015 - 2022 Sindre Sorhus (https://sindresorhus.com) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS + * OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { HTTP_PROTOCOL } from './url'; + +/* + * A fork of https://github.com/sindresorhus/normalize-url ported to typescript with all unneeded features removed. + * This was necessary due to v7 only providing ES module builds that are poorly supported and v6 using poorly supported + * regex features. + */ + +export default function normalizeUrl(urlString: string): string { + urlString = urlString.trim(); + + // Data URL + if (/^data:/i.test(urlString)) { + throw new Error('data URLs are not supported'); + } + + if (/^view-source:/i.test(urlString)) { + throw new Error('`view-source:` is not supported as it is a non-standard protocol'); + } + + const hasRelativeProtocol = urlString.startsWith('//'); + const isRelativeUrl = !hasRelativeProtocol && /^\.*\//.test(urlString); + + // Prepend protocol + if (!isRelativeUrl) { + urlString = urlString.replace(/^(?!(?:\w+:)?\/\/)|^\/\//, HTTP_PROTOCOL); + } + + if (!/^https?:/i.test(urlString)) { + throw new Error('only http or https protocols are supported'); + } + + const urlObject = new URL(urlString); + + // Remove hash + urlObject.hash = ''; + + // Remove duplicate slashes if not preceded by a protocol + // NOTE: This could be implemented using a single negative lookbehind + // regex, but we avoid that to maintain compatibility with older js engines + // which do not have support for that feature. + if (urlObject.pathname) { + // TODO: Replace everything below with `urlObject.pathname = urlObject.pathname.replace(/(?