Remove many instances of deprecated url.parse

This commit is contained in:
Evan Hahn 2021-05-13 12:18:51 -05:00 committed by Scott Nonnenberg
parent 2fc3e4c698
commit 18abe93022
10 changed files with 220 additions and 89 deletions

View File

@ -1,4 +1,4 @@
// Copyright 2020 Signal Messenger, LLC // Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable strict */ /* eslint-disable strict */
@ -7,6 +7,7 @@ const { Menu, clipboard, nativeImage } = require('electron');
const osLocale = require('os-locale'); const osLocale = require('os-locale');
const { uniq } = require('lodash'); const { uniq } = require('lodash');
const url = require('url'); const url = require('url');
const { maybeParseUrl } = require('../ts/util/url');
function getLanguages(userLocale, availableLocales) { function getLanguages(userLocale, availableLocales) {
const baseLocale = userLocale.split('-')[0]; const baseLocale = userLocale.split('-')[0];
@ -97,7 +98,8 @@ exports.setup = (browserWindow, messages) => {
label = messages.contextMenuCopyLink.message; label = messages.contextMenuCopyLink.message;
} else if (isImage) { } else if (isImage) {
click = () => { click = () => {
if (url.parse(params.srcURL).protocol !== 'file:') { const parsedSrcUrl = maybeParseUrl(params.srcURL);
if (!parsedSrcUrl || parsedSrcUrl.protocol !== 'file:') {
return; return;
} }

View File

@ -1,11 +1,10 @@
// Copyright 2019-2020 Signal Messenger, LLC // Copyright 2019-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* global URL */
const { isNumber, compact, isEmpty, range } = require('lodash'); const { isNumber, compact, isEmpty, range } = require('lodash');
const nodeUrl = require('url'); const nodeUrl = require('url');
const LinkifyIt = require('linkify-it'); const LinkifyIt = require('linkify-it');
const { maybeParseUrl } = require('../../ts/util/url');
const linkify = LinkifyIt(); const linkify = LinkifyIt();
@ -18,16 +17,8 @@ module.exports = {
isStickerPack, isStickerPack,
}; };
function maybeParseHref(href) {
try {
return new URL(href);
} catch (err) {
return null;
}
}
function isLinkSafeToPreview(href) { function isLinkSafeToPreview(href) {
const url = maybeParseHref(href); const url = maybeParseUrl(href);
return Boolean(url && url.protocol === 'https:' && !isLinkSneaky(href)); return Boolean(url && url.protocol === 'https:' && !isLinkSneaky(href));
} }
@ -64,7 +55,7 @@ function findLinks(text, caretLocation) {
} }
function getDomain(href) { function getDomain(href) {
const url = maybeParseHref(href); const url = maybeParseUrl(href);
return url ? url.hostname : null; return url ? url.hostname : null;
} }
@ -109,7 +100,7 @@ function isLinkSneaky(href) {
return true; return true;
} }
const url = maybeParseHref(href); const url = maybeParseUrl(href);
// If we can't parse it, it's sneaky. // If we can't parse it, it's sneaky.
if (!url) { if (!url) {

View File

@ -1,4 +1,4 @@
// Copyright 2019-2020 Signal Messenger, LLC // Copyright 2019-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* global /* global
@ -8,7 +8,6 @@
navigator, navigator,
reduxStore, reduxStore,
reduxActions, reduxActions,
URL,
URLSearchParams URLSearchParams
*/ */
@ -38,6 +37,7 @@ const pMap = require('p-map');
const Queue = require('p-queue').default; const Queue = require('p-queue').default;
const { makeLookup } = require('../../ts/util/makeLookup'); const { makeLookup } = require('../../ts/util/makeLookup');
const { maybeParseUrl } = require('../../ts/util/url');
const { const {
base64ToArrayBuffer, base64ToArrayBuffer,
deriveStickerPackKey, deriveStickerPackKey,
@ -96,10 +96,8 @@ async function load() {
} }
function getDataFromLink(link) { function getDataFromLink(link) {
let url; const url = maybeParseUrl(link);
try { if (!url) {
url = new URL(link);
} catch (err) {
return null; return null;
} }

118
main.js
View File

@ -4,7 +4,7 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
const path = require('path'); const path = require('path');
const url = require('url'); const { pathToFileURL } = require('url');
const os = require('os'); const os = require('os');
const fs = require('fs-extra'); const fs = require('fs-extra');
const crypto = require('crypto'); const crypto = require('crypto');
@ -123,6 +123,7 @@ const {
} = require('./ts/types/Settings'); } = require('./ts/types/Settings');
const { Environment } = require('./ts/environment'); const { Environment } = require('./ts/environment');
const { ChallengeMainHandler } = require('./ts/main/challengeMain'); const { ChallengeMainHandler } = require('./ts/main/challengeMain');
const { maybeParseUrl, setUrlSearchParams } = require('./ts/util/url');
const sql = new MainSQL(); const sql = new MainSQL();
const challengeHandler = new ChallengeMainHandler(); const challengeHandler = new ChallengeMainHandler();
@ -228,49 +229,57 @@ const loadLocale = require('./app/locale').load;
let logger; let logger;
let locale; let locale;
function prepareURL(pathSegments, moreKeys) { function prepareFileUrl(
const parsed = url.parse(path.join(...pathSegments)); pathSegments /* : ReadonlyArray<string> */,
moreKeys /* : undefined | Record<string, unknown> */
) /* : string */ {
const filePath = path.join(...pathSegments);
const fileUrl = pathToFileURL(filePath);
return prepareUrl(fileUrl, moreKeys);
}
return url.format({ function prepareUrl(
...parsed, url /* : URL */,
protocol: parsed.protocol || 'file:', moreKeys = {} /* : undefined | Record<string, unknown> */
slashes: true, ) /* : string */ {
query: { return setUrlSearchParams(url, {
name: packageJson.productName, name: packageJson.productName,
locale: locale.name, locale: locale.name,
version: app.getVersion(), version: app.getVersion(),
buildExpiration: config.get('buildExpiration'), buildExpiration: config.get('buildExpiration'),
serverUrl: config.get('serverUrl'), serverUrl: config.get('serverUrl'),
storageUrl: config.get('storageUrl'), storageUrl: config.get('storageUrl'),
directoryUrl: config.get('directoryUrl'), directoryUrl: config.get('directoryUrl'),
directoryEnclaveId: config.get('directoryEnclaveId'), directoryEnclaveId: config.get('directoryEnclaveId'),
directoryTrustAnchor: config.get('directoryTrustAnchor'), directoryTrustAnchor: config.get('directoryTrustAnchor'),
cdnUrl0: config.get('cdn').get('0'), cdnUrl0: config.get('cdn').get('0'),
cdnUrl2: config.get('cdn').get('2'), cdnUrl2: config.get('cdn').get('2'),
certificateAuthority: config.get('certificateAuthority'), certificateAuthority: config.get('certificateAuthority'),
environment: enableCI ? 'production' : config.environment, environment: enableCI ? 'production' : config.environment,
enableCI: enableCI ? true : undefined, enableCI: enableCI ? 'true' : '',
node_version: process.versions.node, node_version: process.versions.node,
hostname: os.hostname(), hostname: os.hostname(),
appInstance: process.env.NODE_APP_INSTANCE, appInstance: process.env.NODE_APP_INSTANCE,
proxyUrl: process.env.HTTPS_PROXY || process.env.https_proxy, proxyUrl: process.env.HTTPS_PROXY || process.env.https_proxy,
contentProxyUrl: config.contentProxyUrl, contentProxyUrl: config.contentProxyUrl,
sfuUrl: config.get('sfuUrl'), sfuUrl: config.get('sfuUrl'),
importMode: importMode ? true : undefined, // for stringify() importMode: importMode ? 'true' : '',
reducedMotionSetting: animationSettings.prefersReducedMotion reducedMotionSetting: animationSettings.prefersReducedMotion ? 'true' : '',
? true serverPublicParams: config.get('serverPublicParams'),
: undefined, serverTrustRoot: config.get('serverTrustRoot'),
serverPublicParams: config.get('serverPublicParams'), appStartInitialSpellcheckSetting,
serverTrustRoot: config.get('serverTrustRoot'), ...moreKeys,
appStartInitialSpellcheckSetting, }).href;
...moreKeys,
},
});
} }
async function handleUrl(event, target) { async function handleUrl(event, target) {
event.preventDefault(); event.preventDefault();
const { protocol, hostname } = url.parse(target); const parsedUrl = maybeParseUrl(target);
if (!parsedUrl) {
return;
}
const { protocol, hostname } = parsedUrl;
const isDevServer = config.enableHttp && hostname === 'localhost'; const isDevServer = config.enableHttp && hostname === 'localhost';
// We only want to specially handle urls that aren't requesting the dev server // We only want to specially handle urls that aren't requesting the dev server
if (isSgnlHref(target) || isSignalHttpsLink(target)) { if (isSgnlHref(target) || isSignalHttpsLink(target)) {
@ -459,13 +468,20 @@ async function createWindow() {
}; };
if (config.environment === 'test') { if (config.environment === 'test') {
mainWindow.loadURL(prepareURL([__dirname, 'test', 'index.html'], moreKeys)); mainWindow.loadURL(
prepareFileUrl([__dirname, 'test', 'index.html'], moreKeys)
);
} else if (config.environment === 'test-lib') { } else if (config.environment === 'test-lib') {
mainWindow.loadURL( mainWindow.loadURL(
prepareURL([__dirname, 'libtextsecure', 'test', 'index.html'], moreKeys) prepareFileUrl(
[__dirname, 'libtextsecure', 'test', 'index.html'],
moreKeys
)
); );
} else { } else {
mainWindow.loadURL(prepareURL([__dirname, 'background.html'], moreKeys)); mainWindow.loadURL(
prepareFileUrl([__dirname, 'background.html'], moreKeys)
);
} }
if (!enableCI && config.get('openDevTools')) { if (!enableCI && config.get('openDevTools')) {
@ -766,7 +782,7 @@ function showAbout() {
handleCommonWindowEvents(aboutWindow); handleCommonWindowEvents(aboutWindow);
aboutWindow.loadURL(prepareURL([__dirname, 'about.html'])); aboutWindow.loadURL(prepareFileUrl([__dirname, 'about.html']));
aboutWindow.on('closed', () => { aboutWindow.on('closed', () => {
aboutWindow = null; aboutWindow = null;
@ -823,7 +839,7 @@ function showSettingsWindow() {
handleCommonWindowEvents(settingsWindow); handleCommonWindowEvents(settingsWindow);
settingsWindow.loadURL(prepareURL([__dirname, 'settings.html'])); settingsWindow.loadURL(prepareFileUrl([__dirname, 'settings.html']));
settingsWindow.on('closed', () => { settingsWindow.on('closed', () => {
removeDarkOverlay(); removeDarkOverlay();
@ -895,8 +911,10 @@ async function showStickerCreator() {
handleCommonWindowEvents(stickerCreatorWindow); handleCommonWindowEvents(stickerCreatorWindow);
const appUrl = config.enableHttp const appUrl = config.enableHttp
? prepareURL(['http://localhost:6380/sticker-creator/dist/index.html']) ? prepareUrl(
: prepareURL([__dirname, 'sticker-creator/dist/index.html']); new URL('http://localhost:6380/sticker-creator/dist/index.html')
)
: prepareFileUrl([__dirname, 'sticker-creator/dist/index.html']);
stickerCreatorWindow.loadURL(appUrl); stickerCreatorWindow.loadURL(appUrl);
@ -947,7 +965,9 @@ async function showDebugLogWindow() {
handleCommonWindowEvents(debugLogWindow); handleCommonWindowEvents(debugLogWindow);
debugLogWindow.loadURL(prepareURL([__dirname, 'debug_log.html'], { theme })); debugLogWindow.loadURL(
prepareFileUrl([__dirname, 'debug_log.html'], { theme })
);
debugLogWindow.on('closed', () => { debugLogWindow.on('closed', () => {
removeDarkOverlay(); removeDarkOverlay();
@ -1000,7 +1020,7 @@ function showPermissionsPopupWindow(forCalling, forCamera) {
handleCommonWindowEvents(permissionsPopupWindow); handleCommonWindowEvents(permissionsPopupWindow);
permissionsPopupWindow.loadURL( permissionsPopupWindow.loadURL(
prepareURL([__dirname, 'permissions_popup.html'], { prepareFileUrl([__dirname, 'permissions_popup.html'], {
theme, theme,
forCalling, forCalling,
forCamera, forCamera,
@ -1175,7 +1195,7 @@ app.on('ready', async () => {
loadingWindow = null; loadingWindow = null;
}); });
loadingWindow.loadURL(prepareURL([__dirname, 'loading.html'])); loadingWindow.loadURL(prepareFileUrl([__dirname, 'loading.html']));
}); });
// Run window preloading in parallel with database initialization. // Run window preloading in parallel with database initialization.

View File

@ -7,6 +7,7 @@ import { gzip } from 'zlib';
import pify from 'pify'; import pify from 'pify';
import got, { Response } from 'got'; import got, { Response } from 'got';
import { getUserAgent } from '../util/getUserAgent'; import { getUserAgent } from '../util/getUserAgent';
import { maybeParseUrl } from '../util/url';
const BASE_URL = 'https://debuglogs.org'; const BASE_URL = 'https://debuglogs.org';
@ -22,10 +23,8 @@ const parseTokenBody = (
): { fields: Record<string, unknown>; url: string } => { ): { fields: Record<string, unknown>; url: string } => {
const body = tokenBodySchema.parse(rawBody); const body = tokenBodySchema.parse(rawBody);
let parsedUrl: URL; const parsedUrl = maybeParseUrl(body.url);
try { if (!parsedUrl) {
parsedUrl = new URL(body.url);
} catch (err) {
throw new Error("Token body's URL was not a valid URL"); throw new Error("Token body's URL was not a valid URL");
} }
if (parsedUrl.protocol !== 'https:') { if (parsedUrl.protocol !== 'https:') {

View File

@ -0,0 +1,87 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { size } from '../../util/iterables';
import { maybeParseUrl, setUrlSearchParams } from '../../util/url';
describe('URL utilities', () => {
describe('maybeParseUrl', () => {
it('parses valid URLs', () => {
[
'https://example.com',
'https://example.com:123/pathname?query=string#hash',
'file:///path/to/file.txt',
].forEach(href => {
assert.deepEqual(maybeParseUrl(href), new URL(href));
});
});
it('returns undefined for invalid URLs', () => {
['', 'example.com'].forEach(href => {
assert.isUndefined(maybeParseUrl(href));
});
});
it('handles non-strings for compatibility, returning undefined', () => {
[undefined, null, 123, ['https://example.com']].forEach(value => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
assert.isUndefined(maybeParseUrl(value as any));
});
});
});
describe('setUrlSearchParams', () => {
it('returns a new URL with updated search params', () => {
const params = {
normal_string: 'foo',
empty_string: '',
number: 123,
true_bool: true,
false_bool: false,
null_value: null,
undefined_value: undefined,
array: ['ok', 'wow'],
stringified: { toString: () => 'bar' },
};
const newUrl = setUrlSearchParams(
new URL('https://example.com/path?should_be=overwritten#hash'),
params
);
assert(newUrl.href.startsWith('https://example.com/path?'));
assert.strictEqual(newUrl.hash, '#hash');
assert.strictEqual(
size(newUrl.searchParams.entries()),
Object.keys(params).length
);
assert.strictEqual(newUrl.searchParams.get('normal_string'), 'foo');
assert.strictEqual(newUrl.searchParams.get('empty_string'), '');
assert.strictEqual(newUrl.searchParams.get('number'), '123');
assert.strictEqual(newUrl.searchParams.get('true_bool'), 'true');
assert.strictEqual(newUrl.searchParams.get('false_bool'), 'false');
assert.strictEqual(newUrl.searchParams.get('null_value'), '');
assert.strictEqual(newUrl.searchParams.get('undefined_value'), '');
assert.strictEqual(newUrl.searchParams.get('array'), 'ok,wow');
assert.strictEqual(newUrl.searchParams.get('stringified'), 'bar');
});
it("doesn't touch the original URL or its params", () => {
const originalHref = 'https://example.com/path?query=string';
const originalUrl = new URL(originalHref);
const params = { foo: 'bar' };
const newUrl = setUrlSearchParams(originalUrl, params);
assert.notStrictEqual(originalUrl, newUrl);
assert.strictEqual(originalUrl.href, originalHref);
params.foo = 'should be ignored';
assert.strictEqual(newUrl.search, '?foo=bar');
});
});
});

View File

@ -1,26 +1,25 @@
// Copyright 2020 Signal Messenger, LLC // Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { LoggerType } from '../types/Logging'; import { LoggerType } from '../types/Logging';
import { maybeParseUrl } from './url';
function parseUrl(value: unknown, logger: LoggerType): null | URL { function parseUrl(value: string | URL, logger: LoggerType): undefined | URL {
if (value instanceof URL) { if (value instanceof URL) {
return value; return value;
} }
if (typeof value === 'string') { if (typeof value === 'string') {
try { return maybeParseUrl(value);
return new URL(value);
} catch (err) {
return null;
}
} }
logger.warn('Tried to parse a sgnl:// URL but got an unexpected type'); logger.warn('Tried to parse a sgnl:// URL but got an unexpected type');
return null; return undefined;
} }
export function isSgnlHref(value: string | URL, logger: LoggerType): boolean { export function isSgnlHref(value: string | URL, logger: LoggerType): boolean {
const url = parseUrl(value, logger); const url = parseUrl(value, logger);
return url !== null && url.protocol === 'sgnl:'; return Boolean(url?.protocol === 'sgnl:');
} }
export function isCaptchaHref( export function isCaptchaHref(
@ -28,7 +27,7 @@ export function isCaptchaHref(
logger: LoggerType logger: LoggerType
): boolean { ): boolean {
const url = parseUrl(value, logger); const url = parseUrl(value, logger);
return url !== null && url.protocol === 'signalcaptcha:'; return Boolean(url?.protocol === 'signalcaptcha:');
} }
export function isSignalHttpsLink( export function isSignalHttpsLink(

35
ts/util/url.ts Normal file
View File

@ -0,0 +1,35 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { mapValues } from 'lodash';
export function maybeParseUrl(value: string): undefined | URL {
if (typeof value === 'string') {
try {
return new URL(value);
} catch (err) {
/* Errors are ignored. */
}
}
return undefined;
}
export function setUrlSearchParams(
url: Readonly<URL>,
searchParams: Readonly<Record<string, unknown>>
): URL {
const result = cloneUrl(url);
result.search = new URLSearchParams(
mapValues(searchParams, stringifySearchParamValue)
).toString();
return result;
}
function cloneUrl(url: Readonly<URL>): URL {
return new URL(url.href);
}
function stringifySearchParamValue(value: unknown): string {
return value == null ? '' : String(value);
}

View File

@ -11,6 +11,7 @@ import { MediaItemType } from '../components/LightboxGallery';
import { MessageModel } from '../models/messages'; import { MessageModel } from '../models/messages';
import { MessageType } from '../state/ducks/conversations'; import { MessageType } from '../state/ducks/conversations';
import { assert } from '../util/assert'; import { assert } from '../util/assert';
import { maybeParseUrl } from '../util/url';
type GetLinkPreviewImageResult = { type GetLinkPreviewImageResult = {
data: ArrayBuffer; data: ArrayBuffer;
@ -3934,10 +3935,8 @@ Whisper.ConversationView = Whisper.View.extend({
url: string, url: string,
abortSignal: any abortSignal: any
): Promise<null | GetLinkPreviewResult> { ): Promise<null | GetLinkPreviewResult> {
let urlObject; const urlObject = maybeParseUrl(url);
try { if (!urlObject) {
urlObject = new URL(url);
} catch (err) {
return null; return null;
} }

View File

@ -6,6 +6,7 @@
// Specify library files to be included in the compilation. // Specify library files to be included in the compilation.
"lib": [ "lib": [
"dom", // Required to access `window` "dom", // Required to access `window`
"dom.iterable",
"es2020" "es2020"
], ],
"incremental": true, "incremental": true,