From 8c59eef257bcef9d9866ed7c009784d6b586c77a Mon Sep 17 00:00:00 2001 From: Daniel James Smith Date: Mon, 24 Oct 2022 19:26:50 +0200 Subject: [PATCH] [PS-1123] Improve hostname and domain retrieval (#3168) * Add test cases from previous PR https://github.com/bitwarden/jslib/pull/547 * Install tldts as replacement for tldjs * Use tldts for hostname and domain retrieval/validation * Remove usage of old tldjs.noop-implementation * Add handling of about protocol * Remove usage of tldEndingRegex and use tldts check instead * Uninstall @types/tldjs and tldjs * Updated package-lock.json * Fix accessibility cookie check * Rename loginUriView.spec to login-uri-view.spec * Add test for getDomain failing file links * getHostName - Return null when given, data, about or file links --- apps/cli/package-lock.json | 47 +++-- apps/cli/package.json | 2 +- .../accessibility-cookie.component.ts | 2 +- apps/desktop/tsconfig.json | 1 - apps/web/tsconfig.json | 1 - libs/common/spec/misc/utils.spec.ts | 177 +++++++++++++++++- libs/common/spec/view/login-uri-view.spec.ts | 65 +++++++ libs/common/src/misc/tldjs.noop.ts | 7 - libs/common/src/misc/utils.ts | 96 +++++----- libs/common/src/models/view/login-uri.view.ts | 4 +- package-lock.json | 60 +++--- package.json | 3 +- 12 files changed, 337 insertions(+), 128 deletions(-) create mode 100644 libs/common/spec/view/login-uri-view.spec.ts delete mode 100644 libs/common/src/misc/tldjs.noop.ts diff --git a/apps/cli/package-lock.json b/apps/cli/package-lock.json index b9ef73baba..fb1053bfbc 100644 --- a/apps/cli/package-lock.json +++ b/apps/cli/package-lock.json @@ -32,7 +32,7 @@ "papaparse": "^5.3.2", "proper-lockfile": "^4.1.2", "rxjs": "^7.5.5", - "tldjs": "^2.3.1", + "tldts": "^5.7.84", "zxcvbn": "^4.4.2" }, "bin": { @@ -1433,11 +1433,6 @@ "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" }, - "node_modules/punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" - }, "node_modules/qs": { "version": "6.10.5", "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.5.tgz", @@ -1715,18 +1710,22 @@ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, - "node_modules/tldjs": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tldjs/-/tldjs-2.3.1.tgz", - "integrity": "sha512-W/YVH/QczLUxVjnQhFC61Iq232NWu3TqDdO0S/MtXVz4xybejBov4ud+CIwN9aYqjOecEqIy0PscGkwpG9ZyTw==", - "hasInstallScript": true, + "node_modules/tldts": { + "version": "5.7.84", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-5.7.84.tgz", + "integrity": "sha512-PGkhyObqEEntI/xUDJdagtvNW+O7+5j8vbXSOXL+563At8gH/4LHxHjdPNv7qrahYuL6y6ZgjBxcvWdut7/LtA==", "dependencies": { - "punycode": "^1.4.1" + "tldts-core": "^5.7.84" }, - "engines": { - "node": ">= 4" + "bin": { + "tldts": "bin/cli.js" } }, + "node_modules/tldts-core": { + "version": "5.7.84", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-5.7.84.tgz", + "integrity": "sha512-1qKxwSDjmWdqG81cnXGvKI+FHPiVRT6w5DORjp2+YVqDdzFUZO0oQoWu0zbDtWA6HXVk+B15yY9DKbVKZ6fRLg==" + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -3035,11 +3034,6 @@ "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" - }, "qs": { "version": "6.10.5", "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.5.tgz", @@ -3258,14 +3252,19 @@ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, - "tldjs": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tldjs/-/tldjs-2.3.1.tgz", - "integrity": "sha512-W/YVH/QczLUxVjnQhFC61Iq232NWu3TqDdO0S/MtXVz4xybejBov4ud+CIwN9aYqjOecEqIy0PscGkwpG9ZyTw==", + "tldts": { + "version": "5.7.84", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-5.7.84.tgz", + "integrity": "sha512-PGkhyObqEEntI/xUDJdagtvNW+O7+5j8vbXSOXL+563At8gH/4LHxHjdPNv7qrahYuL6y6ZgjBxcvWdut7/LtA==", "requires": { - "punycode": "^1.4.1" + "tldts-core": "^5.7.84" } }, + "tldts-core": { + "version": "5.7.84", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-5.7.84.tgz", + "integrity": "sha512-1qKxwSDjmWdqG81cnXGvKI+FHPiVRT6w5DORjp2+YVqDdzFUZO0oQoWu0zbDtWA6HXVk+B15yY9DKbVKZ6fRLg==" + }, "tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", diff --git a/apps/cli/package.json b/apps/cli/package.json index b41baeec41..154858eaa2 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -67,7 +67,7 @@ "papaparse": "^5.3.2", "proper-lockfile": "^4.1.2", "rxjs": "^7.5.5", - "tldjs": "^2.3.1", + "tldts": "^5.7.84", "zxcvbn": "^4.4.2" } } diff --git a/apps/desktop/src/app/accounts/accessibility-cookie.component.ts b/apps/desktop/src/app/accounts/accessibility-cookie.component.ts index 6993b17077..33c8243748 100644 --- a/apps/desktop/src/app/accounts/accessibility-cookie.component.ts +++ b/apps/desktop/src/app/accounts/accessibility-cookie.component.ts @@ -79,7 +79,7 @@ export class AccessibilityCookieComponent { } async submit() { - if (Utils.getDomain(this.accessibilityForm.value.link) !== "accounts.hcaptcha.com") { + if (Utils.getHostname(this.accessibilityForm.value.link) !== "accounts.hcaptcha.com") { this.platformUtilsService.showToast( "error", this.i18nService.t("errorOccurred"), diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json index f753858f69..6bca2f5739 100644 --- a/apps/desktop/tsconfig.json +++ b/apps/desktop/tsconfig.json @@ -10,7 +10,6 @@ "types": [], "baseUrl": ".", "paths": { - "tldjs": ["../../libs/common/src/misc/tldjs.noop"], "@bitwarden/common/*": ["../../libs/common/src/*"], "@bitwarden/angular/*": ["../../libs/angular/src/*"], "@bitwarden/electron/*": ["../../libs/electron/src/*"] diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 3e1d962f92..aeb99ec349 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -5,7 +5,6 @@ "module": "ES2020", "resolveJsonModule": true, "paths": { - "tldjs": ["../../libs/common/src/misc/tldjs.noop"], "@bitwarden/web-vault/*": ["src/*"], "@bitwarden/common/*": ["../../libs/common/src/*"], "@bitwarden/angular/*": ["../../libs/angular/src/*"], diff --git a/libs/common/spec/misc/utils.spec.ts b/libs/common/spec/misc/utils.spec.ts index 3978f9cfed..e1de265532 100644 --- a/libs/common/spec/misc/utils.spec.ts +++ b/libs/common/spec/misc/utils.spec.ts @@ -14,27 +14,105 @@ describe("Utils Service", () => { expect(Utils.getDomain("data:image/jpeg;base64,AAA")).toBeNull(); }); + it("should fail for about urls", () => { + expect(Utils.getDomain("about")).toBeNull(); + expect(Utils.getDomain("about:")).toBeNull(); + expect(Utils.getDomain("about:blank")).toBeNull(); + }); + + it("should fail for file url", () => { + expect(Utils.getDomain("file:///C://somefolder/form.pdf")).toBeNull(); + }); + it("should handle urls without protocol", () => { expect(Utils.getDomain("bitwarden.com")).toBe("bitwarden.com"); expect(Utils.getDomain("wrong://bitwarden.com")).toBe("bitwarden.com"); }); it("should handle valid urls", () => { - expect(Utils.getDomain("https://bitwarden")).toBe("bitwarden"); - expect(Utils.getDomain("https://bitwarden.com")).toBe("bitwarden.com"); + expect(Utils.getDomain("bitwarden.com")).toBe("bitwarden.com"); expect(Utils.getDomain("http://bitwarden.com")).toBe("bitwarden.com"); + expect(Utils.getDomain("https://bitwarden.com")).toBe("bitwarden.com"); + + expect(Utils.getDomain("www.bitwarden.com")).toBe("bitwarden.com"); + expect(Utils.getDomain("http://www.bitwarden.com")).toBe("bitwarden.com"); + expect(Utils.getDomain("https://www.bitwarden.com")).toBe("bitwarden.com"); + + expect(Utils.getDomain("vault.bitwarden.com")).toBe("bitwarden.com"); expect(Utils.getDomain("http://vault.bitwarden.com")).toBe("bitwarden.com"); + expect(Utils.getDomain("https://vault.bitwarden.com")).toBe("bitwarden.com"); + + expect(Utils.getDomain("www.vault.bitwarden.com")).toBe("bitwarden.com"); + expect(Utils.getDomain("http://www.vault.bitwarden.com")).toBe("bitwarden.com"); + expect(Utils.getDomain("https://www.vault.bitwarden.com")).toBe("bitwarden.com"); + + expect( + Utils.getDomain("user:password@bitwarden.com:8080/password/sites?and&query#hash") + ).toBe("bitwarden.com"); + expect( + Utils.getDomain("http://user:password@bitwarden.com:8080/password/sites?and&query#hash") + ).toBe("bitwarden.com"); expect( Utils.getDomain("https://user:password@bitwarden.com:8080/password/sites?and&query#hash") ).toBe("bitwarden.com"); + + expect(Utils.getDomain("bitwarden.unknown")).toBe("bitwarden.unknown"); + expect(Utils.getDomain("http://bitwarden.unknown")).toBe("bitwarden.unknown"); expect(Utils.getDomain("https://bitwarden.unknown")).toBe("bitwarden.unknown"); }); - it("should support localhost and IP", () => { + it("should handle valid urls with an underscore in subdomain", () => { + expect(Utils.getDomain("my_vault.bitwarden.com/")).toBe("bitwarden.com"); + expect(Utils.getDomain("http://my_vault.bitwarden.com/")).toBe("bitwarden.com"); + expect(Utils.getDomain("https://my_vault.bitwarden.com/")).toBe("bitwarden.com"); + }); + + it("should support urls containing umlauts", () => { + expect(Utils.getDomain("bütwarden.com")).toBe("bütwarden.com"); + expect(Utils.getDomain("http://bütwarden.com")).toBe("bütwarden.com"); + expect(Utils.getDomain("https://bütwarden.com")).toBe("bütwarden.com"); + + expect(Utils.getDomain("subdomain.bütwarden.com")).toBe("bütwarden.com"); + expect(Utils.getDomain("http://subdomain.bütwarden.com")).toBe("bütwarden.com"); + expect(Utils.getDomain("https://subdomain.bütwarden.com")).toBe("bütwarden.com"); + }); + + it("should support punycode urls", () => { + expect(Utils.getDomain("xn--btwarden-65a.com")).toBe("xn--btwarden-65a.com"); + expect(Utils.getDomain("xn--btwarden-65a.com")).toBe("xn--btwarden-65a.com"); + expect(Utils.getDomain("xn--btwarden-65a.com")).toBe("xn--btwarden-65a.com"); + + expect(Utils.getDomain("subdomain.xn--btwarden-65a.com")).toBe("xn--btwarden-65a.com"); + expect(Utils.getDomain("http://subdomain.xn--btwarden-65a.com")).toBe("xn--btwarden-65a.com"); + expect(Utils.getDomain("https://subdomain.xn--btwarden-65a.com")).toBe( + "xn--btwarden-65a.com" + ); + }); + + it("should support localhost", () => { + expect(Utils.getDomain("localhost")).toBe("localhost"); + expect(Utils.getDomain("http://localhost")).toBe("localhost"); expect(Utils.getDomain("https://localhost")).toBe("localhost"); + }); + + it("should support localhost with subdomain", () => { + expect(Utils.getDomain("subdomain.localhost")).toBe("localhost"); + expect(Utils.getDomain("http://subdomain.localhost")).toBe("localhost"); + expect(Utils.getDomain("https://subdomain.localhost")).toBe("localhost"); + }); + + it("should support IPv4", () => { + expect(Utils.getDomain("192.168.1.1")).toBe("192.168.1.1"); + expect(Utils.getDomain("http://192.168.1.1")).toBe("192.168.1.1"); expect(Utils.getDomain("https://192.168.1.1")).toBe("192.168.1.1"); }); + it("should support IPv6", () => { + expect(Utils.getDomain("[2620:fe::fe]")).toBe("2620:fe::fe"); + expect(Utils.getDomain("http://[2620:fe::fe]")).toBe("2620:fe::fe"); + expect(Utils.getDomain("https://[2620:fe::fe]")).toBe("2620:fe::fe"); + }); + it("should reject invalid hostnames", () => { expect(Utils.getDomain("https://mywebsite.com$.mywebsite.com")).toBeNull(); expect(Utils.getDomain("https://mywebsite.com!.mywebsite.com")).toBeNull(); @@ -47,20 +125,107 @@ describe("Utils Service", () => { expect(Utils.getHostname(undefined)).toBeNull(); expect(Utils.getHostname(" ")).toBeNull(); expect(Utils.getHostname('https://bit!:"_&ward.com')).toBeNull(); - expect(Utils.getHostname("bitwarden")).toBeNull(); + }); + + it("should fail for data urls", () => { + expect(Utils.getHostname("data:image/jpeg;base64,AAA")).toBeNull(); + }); + + it("should fail for about urls", () => { + expect(Utils.getHostname("about")).toBe("about"); + expect(Utils.getHostname("about:")).toBeNull(); + expect(Utils.getHostname("about:blank")).toBeNull(); + }); + + it("should fail for file url", () => { + expect(Utils.getHostname("file:///C:/somefolder/form.pdf")).toBeNull(); }); it("should handle valid urls", () => { + expect(Utils.getHostname("bitwarden")).toBe("bitwarden"); + expect(Utils.getHostname("http://bitwarden")).toBe("bitwarden"); + expect(Utils.getHostname("https://bitwarden")).toBe("bitwarden"); + expect(Utils.getHostname("bitwarden.com")).toBe("bitwarden.com"); - expect(Utils.getHostname("https://bitwarden.com")).toBe("bitwarden.com"); expect(Utils.getHostname("http://bitwarden.com")).toBe("bitwarden.com"); + expect(Utils.getHostname("https://bitwarden.com")).toBe("bitwarden.com"); + + expect(Utils.getHostname("www.bitwarden.com")).toBe("www.bitwarden.com"); + expect(Utils.getHostname("http://www.bitwarden.com")).toBe("www.bitwarden.com"); + expect(Utils.getHostname("https://www.bitwarden.com")).toBe("www.bitwarden.com"); + + expect(Utils.getHostname("vault.bitwarden.com")).toBe("vault.bitwarden.com"); expect(Utils.getHostname("http://vault.bitwarden.com")).toBe("vault.bitwarden.com"); + expect(Utils.getHostname("https://vault.bitwarden.com")).toBe("vault.bitwarden.com"); + + expect(Utils.getHostname("www.vault.bitwarden.com")).toBe("www.vault.bitwarden.com"); + expect(Utils.getHostname("http://www.vault.bitwarden.com")).toBe("www.vault.bitwarden.com"); + expect(Utils.getHostname("https://www.vault.bitwarden.com")).toBe("www.vault.bitwarden.com"); + + expect( + Utils.getHostname("user:password@bitwarden.com:8080/password/sites?and&query#hash") + ).toBe("bitwarden.com"); + expect( + Utils.getHostname("https://user:password@bitwarden.com:8080/password/sites?and&query#hash") + ).toBe("bitwarden.com"); + expect(Utils.getHostname("https://bitwarden.unknown")).toBe("bitwarden.unknown"); }); - it("should support localhost and IP", () => { + it("should handle valid urls with an underscore in subdomain", () => { + expect(Utils.getHostname("my_vault.bitwarden.com/")).toBe("my_vault.bitwarden.com"); + expect(Utils.getHostname("http://my_vault.bitwarden.com/")).toBe("my_vault.bitwarden.com"); + expect(Utils.getHostname("https://my_vault.bitwarden.com/")).toBe("my_vault.bitwarden.com"); + }); + + it("should support urls containing umlauts", () => { + expect(Utils.getHostname("bütwarden.com")).toBe("bütwarden.com"); + expect(Utils.getHostname("http://bütwarden.com")).toBe("bütwarden.com"); + expect(Utils.getHostname("https://bütwarden.com")).toBe("bütwarden.com"); + + expect(Utils.getHostname("subdomain.bütwarden.com")).toBe("subdomain.bütwarden.com"); + expect(Utils.getHostname("http://subdomain.bütwarden.com")).toBe("subdomain.bütwarden.com"); + expect(Utils.getHostname("https://subdomain.bütwarden.com")).toBe("subdomain.bütwarden.com"); + }); + + it("should support punycode urls", () => { + expect(Utils.getHostname("xn--btwarden-65a.com")).toBe("xn--btwarden-65a.com"); + expect(Utils.getHostname("xn--btwarden-65a.com")).toBe("xn--btwarden-65a.com"); + expect(Utils.getHostname("xn--btwarden-65a.com")).toBe("xn--btwarden-65a.com"); + + expect(Utils.getHostname("subdomain.xn--btwarden-65a.com")).toBe( + "subdomain.xn--btwarden-65a.com" + ); + expect(Utils.getHostname("http://subdomain.xn--btwarden-65a.com")).toBe( + "subdomain.xn--btwarden-65a.com" + ); + expect(Utils.getHostname("https://subdomain.xn--btwarden-65a.com")).toBe( + "subdomain.xn--btwarden-65a.com" + ); + }); + + it("should support localhost", () => { + expect(Utils.getHostname("localhost")).toBe("localhost"); + expect(Utils.getHostname("http://localhost")).toBe("localhost"); expect(Utils.getHostname("https://localhost")).toBe("localhost"); + }); + + it("should support localhost with subdomain", () => { + expect(Utils.getHostname("subdomain.localhost")).toBe("subdomain.localhost"); + expect(Utils.getHostname("http://subdomain.localhost")).toBe("subdomain.localhost"); + expect(Utils.getHostname("https://subdomain.localhost")).toBe("subdomain.localhost"); + }); + + it("should support IPv4", () => { + expect(Utils.getHostname("192.168.1.1")).toBe("192.168.1.1"); + expect(Utils.getHostname("http://192.168.1.1")).toBe("192.168.1.1"); expect(Utils.getHostname("https://192.168.1.1")).toBe("192.168.1.1"); }); + + it("should support IPv6", () => { + expect(Utils.getHostname("[2620:fe::fe]")).toBe("2620:fe::fe"); + expect(Utils.getHostname("http://[2620:fe::fe]")).toBe("2620:fe::fe"); + expect(Utils.getHostname("https://[2620:fe::fe]")).toBe("2620:fe::fe"); + }); }); describe("newGuid", () => { diff --git a/libs/common/spec/view/login-uri-view.spec.ts b/libs/common/spec/view/login-uri-view.spec.ts new file mode 100644 index 0000000000..7e29651b1e --- /dev/null +++ b/libs/common/spec/view/login-uri-view.spec.ts @@ -0,0 +1,65 @@ +import { UriMatchType } from "@bitwarden/common/enums/uriMatchType"; +import { LoginUriView } from "@bitwarden/common/models/view/login-uri.view"; + +const testData = [ + { + match: UriMatchType.Host, + uri: "http://example.com/login", + expected: "http://example.com/login", + }, + { + match: UriMatchType.Host, + uri: "bitwarden.com", + expected: "http://bitwarden.com", + }, + { + match: UriMatchType.Host, + uri: "bitwarden.de", + expected: "http://bitwarden.de", + }, + { + match: UriMatchType.Host, + uri: "bitwarden.br", + expected: "http://bitwarden.br", + }, +]; + +describe("LoginUriView", () => { + it("isWebsite() given an invalid domain should return false", async () => { + const uri = new LoginUriView(); + Object.assign(uri, { match: UriMatchType.Host, uri: "bit!:_&ward.com" }); + expect(uri.isWebsite).toBe(false); + }); + + testData.forEach((data) => { + it(`isWebsite() given ${data.uri} should return true`, async () => { + const uri = new LoginUriView(); + Object.assign(uri, { match: data.match, uri: data.uri }); + expect(uri.isWebsite).toBe(true); + }); + + it(`launchUri() given ${data.uri} should return ${data.expected}`, async () => { + const uri = new LoginUriView(); + Object.assign(uri, { match: data.match, uri: data.uri }); + expect(uri.launchUri).toBe(data.expected); + }); + + it(`canLaunch() given ${data.uri} should return true`, async () => { + const uri = new LoginUriView(); + Object.assign(uri, { match: data.match, uri: data.uri }); + expect(uri.canLaunch).toBe(true); + }); + }); + + it(`canLaunch should return false when MatchDetection is set to Regex`, async () => { + const uri = new LoginUriView(); + Object.assign(uri, { match: UriMatchType.RegularExpression, uri: "bitwarden.com" }); + expect(uri.canLaunch).toBe(false); + }); + + it(`canLaunch() should return false when the given protocol does not match CanLaunchWhiteList`, async () => { + const uri = new LoginUriView(); + Object.assign(uri, { match: UriMatchType.Host, uri: "someprotocol://bitwarden.com" }); + expect(uri.canLaunch).toBe(false); + }); +}); diff --git a/libs/common/src/misc/tldjs.noop.ts b/libs/common/src/misc/tldjs.noop.ts deleted file mode 100644 index b6273a617c..0000000000 --- a/libs/common/src/misc/tldjs.noop.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function getDomain(host: string): string | null { - return null; -} - -export function isValid(host: string): boolean { - return true; -} diff --git a/libs/common/src/misc/utils.ts b/libs/common/src/misc/utils.ts index f3f03c30ee..537c8e58e1 100644 --- a/libs/common/src/misc/utils.ts +++ b/libs/common/src/misc/utils.ts @@ -1,5 +1,5 @@ /* eslint-disable no-useless-escape */ -import * as tldjs from "tldjs"; +import { getHostname, parse } from "tldts"; import { AbstractEncryptService } from "../abstractions/abstractEncrypt.service"; import { CryptoService } from "../abstractions/crypto.service"; @@ -24,11 +24,10 @@ export class Utils { static isMobileBrowser = false; static isAppleMobileBrowser = false; static global: typeof global = null; - static tldEndingRegex = - /.*\.(com|net|org|edu|uk|gov|ca|de|jp|fr|au|ru|ch|io|es|us|co|xyz|info|ly|mil)$/; // Transpiled version of /\p{Emoji_Presentation}/gu using https://mothereff.in/regexpu. Used for compatability in older browsers. static regexpEmojiPresentation = /(?:[\u231A\u231B\u23E9-\u23EC\u23F0\u23F3\u25FD\u25FE\u2614\u2615\u2648-\u2653\u267F\u2693\u26A1\u26AA\u26AB\u26BD\u26BE\u26C4\u26C5\u26CE\u26D4\u26EA\u26F2\u26F3\u26F5\u26FA\u26FD\u2705\u270A\u270B\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B\u2B1C\u2B50\u2B55]|\uD83C[\uDC04\uDCCF\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF93\uDFA0-\uDFCA\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF4\uDFF8-\uDFFF]|\uD83D[\uDC00-\uDC3E\uDC40\uDC42-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDD7A\uDD95\uDD96\uDDA4\uDDFB-\uDE4F\uDE80-\uDEC5\uDECC\uDED0-\uDED2\uDED5-\uDED7\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB]|\uD83E[\uDD0C-\uDD3A\uDD3C-\uDD45\uDD47-\uDD78\uDD7A-\uDDCB\uDDCD-\uDDFF\uDE70-\uDE74\uDE78-\uDE7A\uDE80-\uDE86\uDE90-\uDEA8\uDEB0-\uDEB6\uDEC0-\uDEC2\uDED0-\uDED6])/g; + static readonly validHosts: string[] = ["localhost"]; static init() { if (Utils.inited) { @@ -214,12 +213,39 @@ export class Utils { } static getHostname(uriString: string): string { - const url = Utils.getUrl(uriString); + if (Utils.isNullOrWhitespace(uriString)) { + return null; + } + + uriString = uriString.trim(); + + if (uriString.startsWith("data:")) { + return null; + } + + if (uriString.startsWith("about:")) { + return null; + } + + if (uriString.startsWith("file:")) { + return null; + } + + // Does uriString contain invalid characters + // TODO Needs to possibly be extended, although '!' is a reserved character + if (uriString.indexOf("!") > 0) { + return null; + } + try { - return url != null && url.hostname !== "" ? url.hostname : null; + const hostname = getHostname(uriString, { validHosts: this.validHosts }); + if (hostname != null) { + return hostname; + } } catch { return null; } + return null; } static getHost(uriString: string): string { @@ -232,60 +258,35 @@ export class Utils { } static getDomain(uriString: string): string { - if (uriString == null) { + if (Utils.isNullOrWhitespace(uriString)) { return null; } uriString = uriString.trim(); - if (uriString === "") { - return null; - } if (uriString.startsWith("data:")) { return null; } - let httpUrl = uriString.startsWith("http://") || uriString.startsWith("https://"); - if ( - !httpUrl && - uriString.indexOf("://") < 0 && - Utils.tldEndingRegex.test(uriString) && - uriString.indexOf("@") < 0 - ) { - uriString = "http://" + uriString; - httpUrl = true; - } - - if (httpUrl) { - try { - const url = Utils.getUrlObject(uriString); - const validHostname = tldjs?.isValid != null ? tldjs.isValid(url.hostname) : true; - if (!validHostname) { - return null; - } - - if (url.hostname === "localhost" || Utils.validIpAddress(url.hostname)) { - return url.hostname; - } - - const urlDomain = - tldjs != null && tldjs.getDomain != null ? tldjs.getDomain(url.hostname) : null; - return urlDomain != null ? urlDomain : url.hostname; - } catch (e) { - // Invalid domain, try another approach below. - } + if (uriString.startsWith("about:")) { + return null; } try { - const domain = tldjs != null && tldjs.getDomain != null ? tldjs.getDomain(uriString) : null; + const parseResult = parse(uriString, { validHosts: this.validHosts }); + if (parseResult != null && parseResult.hostname != null) { + if (parseResult.hostname === "localhost" || parseResult.isIp) { + return parseResult.hostname; + } - if (domain != null) { - return domain; + if (parseResult.domain != null) { + return parseResult.domain; + } + return null; } } catch { return null; } - return null; } @@ -358,14 +359,11 @@ export class Utils { } static getUrl(uriString: string): URL { - if (uriString == null) { + if (this.isNullOrWhitespace(uriString)) { return null; } uriString = uriString.trim(); - if (uriString === "") { - return null; - } let url = Utils.getUrlObject(uriString); if (url == null) { @@ -425,12 +423,6 @@ export class Utils { return this.global.bitwardenContainerService; } - private static validIpAddress(ipString: string): boolean { - const ipRegex = - /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; - return ipRegex.test(ipString); - } - private static isMobile(win: Window) { let mobile = false; ((a) => { diff --git a/libs/common/src/models/view/login-uri.view.ts b/libs/common/src/models/view/login-uri.view.ts index 9d795f0721..b02d36cb3a 100644 --- a/libs/common/src/models/view/login-uri.view.ts +++ b/libs/common/src/models/view/login-uri.view.ts @@ -100,7 +100,7 @@ export class LoginUriView implements View { this.uri != null && (this.uri.indexOf("http://") === 0 || this.uri.indexOf("https://") === 0 || - (this.uri.indexOf("://") < 0 && Utils.tldEndingRegex.test(this.uri))) + (this.uri.indexOf("://") < 0 && !Utils.isNullOrWhitespace(Utils.getDomain(this.uri)))) ); } @@ -122,7 +122,7 @@ export class LoginUriView implements View { } get launchUri(): string { - return this.uri.indexOf("://") < 0 && Utils.tldEndingRegex.test(this.uri) + return this.uri.indexOf("://") < 0 && !Utils.isNullOrWhitespace(Utils.getDomain(this.uri)) ? "http://" + this.uri : this.uri; } diff --git a/package-lock.json b/package-lock.json index 3f8b841fd2..08ba1f19b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,7 +61,7 @@ "qrious": "4.0.2", "rxjs": "^7.5.5", "sweetalert2": "^10.16.6", - "tldjs": "^2.3.1", + "tldts": "^5.7.84", "utf-8-validate": "^5.0.9", "whatwg-fetch": "^3.6.2", "zone.js": "^0.11.4", @@ -103,7 +103,6 @@ "@types/papaparse": "^5.3.2", "@types/proper-lockfile": "^4.1.2", "@types/retry": "^0.12.2", - "@types/tldjs": "^2.3.1", "@types/webcrypto": "^0.0.28", "@types/zxcvbn": "^4.4.1", "@typescript-eslint/eslint-plugin": "^5.22.0", @@ -211,7 +210,7 @@ "papaparse": "^5.3.2", "proper-lockfile": "^4.1.2", "rxjs": "^7.5.5", - "tldjs": "^2.3.1", + "tldts": "^5.7.84", "zxcvbn": "^4.4.2" }, "bin": { @@ -13193,12 +13192,6 @@ "@types/node": "*" } }, - "node_modules/@types/tldjs": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@types/tldjs/-/tldjs-2.3.1.tgz", - "integrity": "sha512-BQR04zLE0ve2eNrqxXw/Qp/f6LxvNrj/4A8ZgdQi3SzbBqxFhleI7N4DS/mSjDnODrUaEGgoWg4grAZR1kVj8w==", - "dev": true - }, "node_modules/@types/tough-cookie": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", @@ -35577,7 +35570,8 @@ "node_modules/punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true }, "node_modules/pupa": { "version": "2.1.1", @@ -40264,18 +40258,22 @@ "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", "dev": true }, - "node_modules/tldjs": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tldjs/-/tldjs-2.3.1.tgz", - "integrity": "sha512-W/YVH/QczLUxVjnQhFC61Iq232NWu3TqDdO0S/MtXVz4xybejBov4ud+CIwN9aYqjOecEqIy0PscGkwpG9ZyTw==", - "hasInstallScript": true, + "node_modules/tldts": { + "version": "5.7.92", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-5.7.92.tgz", + "integrity": "sha512-m5G56S4iN7TrERADxPD3bmgYhKpdg1w86SR5zckRFiEpwIEI/Hy5V2NlR+WVFK8LFqz9sHJ+0QKuUTJQ2Ji7uQ==", "dependencies": { - "punycode": "^1.4.1" + "tldts-core": "^5.7.92" }, - "engines": { - "node": ">= 4" + "bin": { + "tldts": "bin/cli.js" } }, + "node_modules/tldts-core": { + "version": "5.7.92", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-5.7.92.tgz", + "integrity": "sha512-bQWaWBY9o5MmkLAgC0Yy3Qq0NxmTPK3PbZ6Bwz9ORicYCHHYcT4EvN914m7SfpveqlSLOK1qT9BCULTrnyGorg==" + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -45125,7 +45123,7 @@ "papaparse": "^5.3.2", "proper-lockfile": "^4.1.2", "rxjs": "^7.5.5", - "tldjs": "^2.3.1", + "tldts": "^5.7.84", "zxcvbn": "^4.4.2" } }, @@ -53055,12 +53053,6 @@ "@types/node": "*" } }, - "@types/tldjs": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@types/tldjs/-/tldjs-2.3.1.tgz", - "integrity": "sha512-BQR04zLE0ve2eNrqxXw/Qp/f6LxvNrj/4A8ZgdQi3SzbBqxFhleI7N4DS/mSjDnODrUaEGgoWg4grAZR1kVj8w==", - "dev": true - }, "@types/tough-cookie": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", @@ -70295,7 +70287,8 @@ "punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true }, "pupa": { "version": "2.1.1", @@ -74033,14 +74026,19 @@ "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", "dev": true }, - "tldjs": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tldjs/-/tldjs-2.3.1.tgz", - "integrity": "sha512-W/YVH/QczLUxVjnQhFC61Iq232NWu3TqDdO0S/MtXVz4xybejBov4ud+CIwN9aYqjOecEqIy0PscGkwpG9ZyTw==", + "tldts": { + "version": "5.7.92", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-5.7.92.tgz", + "integrity": "sha512-m5G56S4iN7TrERADxPD3bmgYhKpdg1w86SR5zckRFiEpwIEI/Hy5V2NlR+WVFK8LFqz9sHJ+0QKuUTJQ2Ji7uQ==", "requires": { - "punycode": "^1.4.1" + "tldts-core": "^5.7.92" } }, + "tldts-core": { + "version": "5.7.92", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-5.7.92.tgz", + "integrity": "sha512-bQWaWBY9o5MmkLAgC0Yy3Qq0NxmTPK3PbZ6Bwz9ORicYCHHYcT4EvN914m7SfpveqlSLOK1qT9BCULTrnyGorg==" + }, "tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", diff --git a/package.json b/package.json index 2325d25e1e..220c3198a4 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,6 @@ "@types/papaparse": "^5.3.2", "@types/proper-lockfile": "^4.1.2", "@types/retry": "^0.12.2", - "@types/tldjs": "^2.3.1", "@types/webcrypto": "^0.0.28", "@types/zxcvbn": "^4.4.1", "@typescript-eslint/eslint-plugin": "^5.22.0", @@ -186,7 +185,7 @@ "qrious": "4.0.2", "rxjs": "^7.5.5", "sweetalert2": "^10.16.6", - "tldjs": "^2.3.1", + "tldts": "^5.7.84", "utf-8-validate": "^5.0.9", "whatwg-fetch": "^3.6.2", "zone.js": "^0.11.4",