Handle signal.me links

This commit is contained in:
Evan Hahn 2021-08-28 08:27:38 -05:00 committed by GitHub
parent 4273ddb6d0
commit 6f242eca57
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 195 additions and 36 deletions

38
main.js
View File

@ -1744,25 +1744,25 @@ function handleSgnlHref(incomingHref) {
({ command, args, hash } = parseSignalHttpsLink(incomingHref, logger));
}
if (command === 'addstickers' && mainWindow && mainWindow.webContents) {
console.log('Opening sticker pack from sgnl protocol link');
const packId = args.get('pack_id');
const packKeyHex = args.get('pack_key');
const packKey = packKeyHex
? Buffer.from(packKeyHex, 'hex').toString('base64')
: '';
mainWindow.webContents.send('show-sticker-pack', { packId, packKey });
} else if (
command === 'signal.group' &&
hash &&
mainWindow &&
mainWindow.webContents
) {
console.log('Showing group from sgnl protocol link');
mainWindow.webContents.send('show-group-via-link', { hash });
} else if (mainWindow && mainWindow.webContents) {
console.log('Showing warning that we cannot process link');
mainWindow.webContents.send('unknown-sgnl-link');
if (mainWindow && mainWindow.webContents) {
if (command === 'addstickers') {
console.log('Opening sticker pack from sgnl protocol link');
const packId = args.get('pack_id');
const packKeyHex = args.get('pack_key');
const packKey = packKeyHex
? Buffer.from(packKeyHex, 'hex').toString('base64')
: '';
mainWindow.webContents.send('show-sticker-pack', { packId, packKey });
} else if (command === 'signal.group' && hash) {
console.log('Showing group from sgnl protocol link');
mainWindow.webContents.send('show-group-via-link', { hash });
} else if (command === 'signal.me' && hash) {
console.log('Showing conversation from sgnl protocol link');
mainWindow.webContents.send('show-conversation-via-signal.me', { hash });
} else {
console.log('Showing warning that we cannot process link');
mainWindow.webContents.send('unknown-sgnl-link');
}
} else {
console.error('Unhandled sgnl link');
}

View File

@ -12,6 +12,7 @@ try {
const electron = require('electron');
const semver = require('semver');
const _ = require('lodash');
const { strictAssert } = require('./ts/util/assert');
// It is important to call this as early as possible
require('./ts/windows/context');
@ -301,6 +302,16 @@ try {
}
});
ipc.on('show-conversation-via-signal.me', (_event, info) => {
const { hash } = info;
strictAssert(typeof hash === 'string', 'Got an invalid hash over IPC');
const { showConversationViaSignalDotMe } = window.Events;
if (showConversationViaSignalDotMe) {
showConversationViaSignalDotMe(hash);
}
});
ipc.on('unknown-sgnl-link', () => {
const { unknownSignalLink } = window.Events;
if (unknownSignalLink) {
@ -398,8 +409,6 @@ try {
};
window.isValidGuid = isValidGuid;
// https://stackoverflow.com/a/23299989
window.isValidE164 = maybeE164 => /^\+?[1-9]\d{1,14}$/.test(maybeE164);
window.React = require('react');
window.ReactDOM = require('react-dom');

View File

@ -33,6 +33,7 @@ import { isConversationSMSOnly } from '../util/isConversationSMSOnly';
import { isConversationUnregistered } from '../util/isConversationUnregistered';
import { missingCaseError } from '../util/missingCaseError';
import { sniffImageMimeType } from '../util/sniffImageMimeType';
import { isValidE164 } from '../util/isValidE164';
import { MIMEType, IMAGE_WEBP } from '../types/MIME';
import {
arrayBufferToBase64,
@ -232,7 +233,7 @@ export class ConversationModel extends window.Backbone
}
initialize(attributes: Partial<ConversationAttributesType> = {}): void {
if (window.isValidE164(attributes.id)) {
if (isValidE164(attributes.id, false)) {
this.set({ id: window.getGuid(), e164: attributes.id });
}

View File

@ -0,0 +1,35 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { isValidE164 } from '../../util/isValidE164';
describe('isValidE164', () => {
it('returns false for non-strings', () => {
assert.isFalse(isValidE164(undefined, false));
assert.isFalse(isValidE164(18885551234, false));
assert.isFalse(isValidE164(['+18885551234'], false));
});
it('returns false for invalid E164s', () => {
assert.isFalse(isValidE164('', false));
assert.isFalse(isValidE164('+05551234', false));
assert.isFalse(isValidE164('+1800ENCRYPT', false));
assert.isFalse(isValidE164('+1-888-555-1234', false));
assert.isFalse(isValidE164('+1 (888) 555-1234', false));
assert.isFalse(isValidE164('+1012345678901234', false));
assert.isFalse(isValidE164('+18885551234extra', false));
});
it('returns true for E164s that look valid', () => {
assert.isTrue(isValidE164('+18885551234', false));
assert.isTrue(isValidE164('+123456789012', false));
assert.isTrue(isValidE164('+12', false));
});
it('can make the leading + optional or required', () => {
assert.isTrue(isValidE164('18885551234', false));
assert.isFalse(isValidE164('18885551234', true));
});
});

View File

@ -1,4 +1,4 @@
// Copyright 2020 Signal Messenger, LLC
// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
@ -11,6 +11,7 @@ import {
isSignalHttpsLink,
parseSgnlHref,
parseCaptchaHref,
parseE164FromSignalDotMeHash,
parseSignalHttpsLink,
} from '../../util/sgnlHref';
@ -131,10 +132,16 @@ describe('sgnlHref', () => {
);
});
it('returns false if the URL is not a valid Signal URL', () => {
assert.isFalse(isSignalHttpsLink('https://signal.org', explodingLogger));
assert.isFalse(isSignalHttpsLink('https://example.com', explodingLogger));
});
it('returns true if the protocol is "https:"', () => {
assert.isTrue(isSignalHttpsLink('https://signal.group', explodingLogger));
assert.isTrue(isSignalHttpsLink('https://signal.art', explodingLogger));
assert.isTrue(isSignalHttpsLink('HTTPS://signal.art', explodingLogger));
assert.isTrue(isSignalHttpsLink('https://signal.me', explodingLogger));
});
it('returns false if username or password are set', () => {
@ -288,6 +295,34 @@ describe('sgnlHref', () => {
});
});
describe('parseE164FromSignalDotMeHash', () => {
it('returns undefined for invalid inputs', () => {
[
'',
' p/+18885551234',
'p/+18885551234 ',
'x/+18885551234',
'p/+notanumber',
'p/7c7e87a0-3b74-4efd-9a00-6eb8b1dd5be8',
'p/+08885551234',
'p/18885551234',
].forEach(hash => {
assert.isUndefined(parseE164FromSignalDotMeHash(hash));
});
});
it('returns the E164 for valid inputs', () => {
assert.strictEqual(
parseE164FromSignalDotMeHash('p/+18885551234'),
'+18885551234'
);
assert.strictEqual(
parseE164FromSignalDotMeHash('p/+441632960104'),
'+441632960104'
);
});
});
describe('parseSignalHttpsLink', () => {
it('returns a null command for invalid URLs', () => {
['', 'https', 'https://example/?foo=bar'].forEach(href => {
@ -329,5 +364,19 @@ describe('sgnlHref', () => {
}
);
});
it('handles signal.me links', () => {
assert.deepEqual(
parseSignalHttpsLink(
'https://signal.me/#p/+18885551234',
explodingLogger
),
{
command: 'signal.me',
args: new Map<never, never>(),
hash: 'p/+18885551234',
}
);
});
});
});

View File

@ -21,6 +21,7 @@ import { ConversationType } from '../state/ducks/conversations';
import { calling } from '../services/calling';
import { getConversationsWithCustomColorSelector } from '../state/selectors/conversations';
import { getCustomColors } from '../state/selectors/items';
import { trigger } from '../shims/events';
import { themeChanged } from '../shims/themeChanged';
import { renderClearingDataView } from '../shims/renderClearingDataView';
@ -30,6 +31,7 @@ import { PhoneNumberSharingMode } from './phoneNumberSharingMode';
import { assert } from './assert';
import * as durations from './durations';
import { isPhoneNumberSharingEnabled } from './isPhoneNumberSharingEnabled';
import { parseE164FromSignalDotMeHash } from './sgnlHref';
type ThemeType = 'light' | 'dark' | 'system';
type NotificationSettingType = 'message' | 'name' | 'count' | 'off';
@ -92,6 +94,7 @@ export type IPCEventsCallbacksType = {
removeDarkOverlay: () => void;
resetAllChatColors: () => void;
resetDefaultChatColor: () => void;
showConversationViaSignalDotMe: (hash: string) => void;
showKeyboardShortcuts: () => void;
showGroupViaLink: (x: string) => Promise<void>;
showStickerPack: (packId: string, key: string) => void;
@ -465,19 +468,33 @@ export function createIPCEvents(
}
window.isShowingModal = false;
},
showConversationViaSignalDotMe(hash: string) {
if (!window.Signal.Util.Registration.everDone()) {
window.log.info(
'showConversationViaSignalDotMe: Not registered, returning early'
);
return;
}
const maybeE164 = parseE164FromSignalDotMeHash(hash);
if (maybeE164) {
trigger('showConversation', maybeE164);
return;
}
window.log.info('showConversationViaSignalDotMe: invalid E164');
if (window.isShowingModal) {
window.log.info(
'showConversationViaSignalDotMe: a modal is already showing. Doing nothing'
);
} else {
showUnknownSgnlLinkModal();
}
},
unknownSignalLink: () => {
window.log.warn('unknownSignalLink: Showing error dialog');
const errorView = new window.Whisper.ReactWrapperView({
className: 'error-modal-wrapper',
Component: window.Signal.Components.ErrorModal,
props: {
description: window.i18n('unknown-sgnl-link'),
onClose: () => {
errorView.remove();
},
},
});
showUnknownSgnlLinkModal();
},
installStickerPack: async (packId, key) => {
@ -494,3 +511,16 @@ export function createIPCEvents(
...overrideEvents,
};
}
function showUnknownSgnlLinkModal(): void {
const errorView = new window.Whisper.ReactWrapperView({
className: 'error-modal-wrapper',
Component: window.Signal.Components.ErrorModal,
props: {
description: window.i18n('unknown-sgnl-link'),
onClose: () => {
errorView.remove();
},
},
});
}

22
ts/util/isValidE164.ts Normal file
View File

@ -0,0 +1,22 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/**
* Returns `true` if the input looks like a valid E164, and `false` otherwise. Note that
* this may return false positives, as it is a fairly naïve check.
*
* See <https://www.twilio.com/docs/glossary/what-e164#regex-matching-for-e164> and
* <https://stackoverflow.com/a/23299989>.
*/
export function isValidE164(
value: unknown,
mustStartWithPlus: boolean
): value is string {
if (typeof value !== 'string') {
return false;
}
const regex = mustStartWithPlus ? /^\+[1-9]\d{1,14}$/ : /^\+?[1-9]\d{1,14}$/;
return regex.test(value);
}

View File

@ -3,6 +3,9 @@
import { LoggerType } from '../types/Logging';
import { maybeParseUrl } from './url';
import { isValidE164 } from './isValidE164';
const SIGNAL_DOT_ME_HASH_PREFIX = 'p/';
function parseUrl(value: string | URL, logger: LoggerType): undefined | URL {
if (value instanceof URL) {
@ -41,7 +44,9 @@ export function isSignalHttpsLink(
!url.password &&
!url.port &&
url.protocol === 'https:' &&
(url.host === 'signal.group' || url.host === 'signal.art')
(url.host === 'signal.group' ||
url.host === 'signal.art' ||
url.host === 'signal.me')
);
}
@ -119,7 +124,7 @@ export function parseSignalHttpsLink(
};
}
if (url.host === 'signal.group') {
if (url.host === 'signal.group' || url.host === 'signal.me') {
return {
command: url.host,
args: new Map<string, string>(),
@ -129,3 +134,12 @@ export function parseSignalHttpsLink(
return { command: null, args: new Map<never, never>() };
}
export function parseE164FromSignalDotMeHash(hash: string): undefined | string {
if (!hash.startsWith(SIGNAL_DOT_ME_HASH_PREFIX)) {
return;
}
const maybeE164 = hash.slice(SIGNAL_DOT_ME_HASH_PREFIX.length);
return isValidE164(maybeE164, true) ? maybeE164 : undefined;
}

1
ts/window.d.ts vendored
View File

@ -229,7 +229,6 @@ declare global {
isBeforeVersion: (version: string, anotherVersion: string) => boolean;
isFullScreen: () => boolean;
isValidGuid: typeof isValidGuid;
isValidE164: (maybeE164: unknown) => boolean;
libphonenumber: {
util: {
getRegionCodeForNumber: (number: string) => string;