signal-desktop/ts/util/sgnlHref.ts

196 lines
5.1 KiB
TypeScript
Raw Normal View History

2022-02-02 18:29:01 +00:00
// Copyright 2020-2022 Signal Messenger, LLC
2020-10-30 20:34:04 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
import type { LoggerType } from '../types/Logging';
import { maybeParseUrl } from './url';
2021-08-28 13:27:38 +00:00
import { isValidE164 } from './isValidE164';
2022-02-02 18:29:01 +00:00
const SIGNAL_HOSTS = new Set(['signal.group', 'signal.art', 'signal.me']);
2022-10-18 17:12:02 +00:00
const SIGNAL_DOT_ME_E164_PREFIX = 'p/';
const SIGNAL_DOT_ME_USERNAME_PREFIX = 'u/';
2020-08-27 19:44:51 +00:00
function parseUrl(value: string | URL, logger: LoggerType): undefined | URL {
2020-08-27 19:44:51 +00:00
if (value instanceof URL) {
return value;
}
if (typeof value === 'string') {
return maybeParseUrl(value);
2020-08-27 19:44:51 +00:00
}
2020-08-27 19:44:51 +00:00
logger.warn('Tried to parse a sgnl:// URL but got an unexpected type');
return undefined;
2020-08-27 19:44:51 +00:00
}
export function isSgnlHref(value: string | URL, logger: LoggerType): boolean {
const url = parseUrl(value, logger);
return Boolean(url?.protocol === 'sgnl:');
2020-08-27 19:44:51 +00:00
}
export function isCaptchaHref(
value: string | URL,
logger: LoggerType
): boolean {
const url = parseUrl(value, logger);
return Boolean(url?.protocol === 'signalcaptcha:');
}
// A link to a signal 'action' domain with private data in path/hash/query. We could
// open a browser, but it will just link back to us. We will parse it locally instead.
export function isSignalHttpsLink(
value: string | URL,
logger: LoggerType
): boolean {
const url = parseUrl(value, logger);
return Boolean(
url &&
!url.username &&
!url.password &&
!url.port &&
url.protocol === 'https:' &&
SIGNAL_HOSTS.has(url.host) &&
(url.hash || url.pathname !== '/' || url.search)
);
}
2020-08-27 19:44:51 +00:00
type ParsedSgnlHref =
2021-10-01 18:49:59 +00:00
| { command: null; args: Map<never, never>; hash: undefined }
| { command: string; args: Map<string, string>; hash: string | undefined };
2020-08-27 19:44:51 +00:00
export function parseSgnlHref(
href: string,
logger: LoggerType
): ParsedSgnlHref {
const url = parseUrl(href, logger);
if (!url || !isSgnlHref(url, logger)) {
2021-10-01 18:49:59 +00:00
return { command: null, args: new Map<never, never>(), hash: undefined };
2020-08-27 19:44:51 +00:00
}
const args = new Map<string, string>();
url.searchParams.forEach((value, key) => {
if (!args.has(key)) {
args.set(key, value);
}
});
return {
command: url.host,
args,
hash: url.hash ? url.hash.slice(1) : undefined,
};
2020-08-27 19:44:51 +00:00
}
type ParsedCaptchaHref = {
readonly captcha: string;
};
export function parseCaptchaHref(
href: URL | string,
logger: LoggerType
): ParsedCaptchaHref {
const url = parseUrl(href, logger);
if (!url || !isCaptchaHref(url, logger)) {
throw new Error('Not a captcha href');
}
return {
captcha: url.host,
};
}
export function parseSignalHttpsLink(
href: string,
logger: LoggerType
): ParsedSgnlHref {
const url = parseUrl(href, logger);
if (!url || !isSignalHttpsLink(url, logger)) {
2021-10-01 18:49:59 +00:00
return { command: null, args: new Map<never, never>(), hash: undefined };
}
if (url.host === 'signal.art') {
const hash = url.hash.slice(1);
const hashParams = new URLSearchParams(hash);
const args = new Map<string, string>();
hashParams.forEach((value, key) => {
if (!args.has(key)) {
args.set(key, value);
}
});
if (!args.get('pack_id') || !args.get('pack_key')) {
2021-10-01 18:49:59 +00:00
return { command: null, args: new Map<never, never>(), hash: undefined };
}
return {
command: url.pathname.replace(/\//g, ''),
args,
hash: url.hash ? url.hash.slice(1) : undefined,
};
}
2021-08-28 13:27:38 +00:00
if (url.host === 'signal.group' || url.host === 'signal.me') {
return {
command: url.host,
args: new Map<string, string>(),
hash: url.hash ? url.hash.slice(1) : undefined,
};
}
2021-10-01 18:49:59 +00:00
return { command: null, args: new Map<never, never>(), hash: undefined };
}
2021-08-28 13:27:38 +00:00
export function parseE164FromSignalDotMeHash(hash: string): undefined | string {
2022-10-18 17:12:02 +00:00
if (!hash.startsWith(SIGNAL_DOT_ME_E164_PREFIX)) {
2021-08-28 13:27:38 +00:00
return;
}
2022-10-18 17:12:02 +00:00
const maybeE164 = hash.slice(SIGNAL_DOT_ME_E164_PREFIX.length);
2021-08-28 13:27:38 +00:00
return isValidE164(maybeE164, true) ? maybeE164 : undefined;
}
2022-02-02 18:29:01 +00:00
2022-10-18 17:12:02 +00:00
export function parseUsernameFromSignalDotMeHash(
hash: string
): undefined | string {
if (!hash.startsWith(SIGNAL_DOT_ME_USERNAME_PREFIX)) {
return;
}
return decodeURIComponent(hash.slice(SIGNAL_DOT_ME_USERNAME_PREFIX.length));
}
2022-02-02 18:29:01 +00:00
/**
* Converts `http://signal.group/#abc` to `https://signal.group/#abc`. Does the same for
* other Signal hosts, like signal.me. Does nothing to other URLs. Expects a valid href.
*/
export function rewriteSignalHrefsIfNecessary(href: string): string {
const resultUrl = new URL(href);
const isHttp = resultUrl.protocol === 'http:';
const isHttpOrHttps = isHttp || resultUrl.protocol === 'https:';
if (SIGNAL_HOSTS.has(resultUrl.host) && isHttpOrHttps) {
if (isHttp) {
resultUrl.protocol = 'https:';
}
resultUrl.username = '';
resultUrl.password = '';
return resultUrl.href;
}
return href;
}
2022-10-18 17:12:02 +00:00
export type GenerateUsernameLinkOptionsType = Readonly<{
short?: boolean;
}>;
export function generateUsernameLink(
username: string,
{ short = false }: GenerateUsernameLinkOptionsType = {}
): string {
const shortVersion = `signal.me/#u/${encodeURIComponent(username)}`;
if (short) {
return shortVersion;
}
return `https://${shortVersion}`;
}