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
|
|
|
|
|
2021-10-26 19:15:33 +00:00
|
|
|
import type { LoggerType } from '../types/Logging';
|
2021-05-13 17:18:51 +00:00
|
|
|
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']);
|
2021-08-28 13:27:38 +00:00
|
|
|
const SIGNAL_DOT_ME_HASH_PREFIX = 'p/';
|
2020-08-27 19:44:51 +00:00
|
|
|
|
2021-05-13 17:18: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;
|
2020-09-14 21:56:35 +00:00
|
|
|
}
|
2021-05-13 17:18:51 +00:00
|
|
|
|
2020-09-14 21:56:35 +00:00
|
|
|
if (typeof value === 'string') {
|
2021-05-13 17:18:51 +00:00
|
|
|
return maybeParseUrl(value);
|
2020-08-27 19:44:51 +00:00
|
|
|
}
|
2021-05-13 17:18:51 +00:00
|
|
|
|
2020-08-27 19:44:51 +00:00
|
|
|
logger.warn('Tried to parse a sgnl:// URL but got an unexpected type');
|
2021-05-13 17:18:51 +00:00
|
|
|
return undefined;
|
2020-08-27 19:44:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export function isSgnlHref(value: string | URL, logger: LoggerType): boolean {
|
|
|
|
const url = parseUrl(value, logger);
|
2021-05-13 17:18:51 +00:00
|
|
|
return Boolean(url?.protocol === 'sgnl:');
|
2020-08-27 19:44:51 +00:00
|
|
|
}
|
|
|
|
|
2021-05-06 00:09:29 +00:00
|
|
|
export function isCaptchaHref(
|
|
|
|
value: string | URL,
|
|
|
|
logger: LoggerType
|
|
|
|
): boolean {
|
|
|
|
const url = parseUrl(value, logger);
|
2021-05-13 17:18:51 +00:00
|
|
|
return Boolean(url?.protocol === 'signalcaptcha:');
|
2021-05-06 00:09:29 +00:00
|
|
|
}
|
|
|
|
|
2021-02-10 22:39:26 +00:00
|
|
|
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:' &&
|
2022-02-02 18:29:01 +00:00
|
|
|
SIGNAL_HOSTS.has(url.host)
|
2021-02-10 22:39:26 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
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 }
|
2021-01-29 22:16:48 +00:00
|
|
|
| { 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);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2021-01-29 22:16:48 +00:00
|
|
|
return {
|
|
|
|
command: url.host,
|
|
|
|
args,
|
|
|
|
hash: url.hash ? url.hash.slice(1) : undefined,
|
|
|
|
};
|
2020-08-27 19:44:51 +00:00
|
|
|
}
|
2021-02-10 22:39:26 +00:00
|
|
|
|
2021-05-06 00:09:29 +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,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-02-10 22:39:26 +00:00
|
|
|
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 };
|
2021-02-10 22:39:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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 };
|
2021-02-10 22:39:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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') {
|
2021-02-10 22:39:26 +00:00
|
|
|
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-02-10 22:39:26 +00:00
|
|
|
}
|
2021-08-28 13:27:38 +00:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
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;
|
|
|
|
}
|