Support server-determined build expiration

This commit is contained in:
Josh Perez 2020-09-09 18:50:44 -04:00 committed by Scott Nonnenberg
parent a04f9a0e51
commit d87335f5a6
14 changed files with 147 additions and 28 deletions

View file

@ -1543,6 +1543,25 @@
await window.Signal.RemoteConfig.maybeRefreshRemoteConfig();
});
// Listen for changes to the `desktop.clientExpiration` remote flag
window.Signal.RemoteConfig.onChange(
'desktop.clientExpiration',
({ value }) => {
const remoteBuildExpirationTimestamp = window.Signal.Util.parseRemoteClientExpiration(
value
);
if (remoteBuildExpirationTimestamp) {
window.storage.put(
'remoteBuildExpiration',
remoteBuildExpirationTimestamp
);
window.reduxActions.expiration.hydrateExpirationStatus(
window.Signal.Util.hasExpired()
);
}
}
);
// Listen for changes to the `desktop.messageRequests` remote configuration flag
const removeMessageRequestListener = window.Signal.RemoteConfig.onChange(
'desktop.messageRequests',

View file

@ -34,7 +34,15 @@ try {
window.getEnvironment = () => config.environment;
window.getAppInstance = () => config.appInstance;
window.getVersion = () => config.version;
window.getExpiration = () => config.buildExpiration;
window.getExpiration = () => {
const remoteBuildExpiration = window.storage.get('remoteBuildExpiration');
if (remoteBuildExpiration) {
return remoteBuildExpiration < config.buildExpiration
? remoteBuildExpiration
: config.buildExpiration;
}
return config.buildExpiration;
};
window.getNodeVersion = () => config.node_version;
window.getHostName = () => config.hostname;
window.getServerTrustRoot = () => config.serverTrustRoot;

View file

@ -1,11 +1,16 @@
import { get, throttle } from 'lodash';
import { WebAPIType } from './textsecure/WebAPI';
type ConfigKeyType = 'desktop.messageRequests' | 'desktop.gv2' | 'desktop.cds';
type ConfigKeyType =
| 'desktop.messageRequests'
| 'desktop.gv2'
| 'desktop.cds'
| 'desktop.clientExpiration';
type ConfigValueType = {
name: ConfigKeyType;
enabled: boolean;
enabledAt?: number;
value?: unknown;
};
type ConfigMapType = { [key: string]: ConfigValueType };
type ConfigListenerType = (value: ConfigValueType) => unknown;
@ -51,31 +56,36 @@ export const refreshRemoteConfig = async () => {
// The old configuration is not set as the initial value in reduce because
// flags may have been deleted
const oldConfig = config;
config = newConfig.reduce((previous, { name, enabled }) => {
config = newConfig.reduce((acc, { name, enabled, value }) => {
const previouslyEnabled: boolean = get(oldConfig, [name, 'enabled'], false);
const previousValue: unknown = get(oldConfig, [name, 'value'], undefined);
// If a flag was previously not enabled and is now enabled, record the time it was enabled
const enabledAt: number | undefined =
previouslyEnabled && enabled ? now : get(oldConfig, [name, 'enabledAt']);
const value = {
const configValue = {
name: name as ConfigKeyType,
enabled,
enabledAt,
value,
};
const hasChanged =
previouslyEnabled !== enabled || previousValue !== configValue.value;
// If enablement changes at all, notify listeners
const currentListeners = listeners[name] || [];
if (previouslyEnabled !== enabled) {
window.log.info(`Remote Config: Flag ${name} has been enabled`);
if (hasChanged) {
window.log.info(`Remote Config: Flag ${name} has changed`);
currentListeners.forEach(listener => {
listener(value);
listener(configValue);
});
}
// Return new configuration object
return {
...previous,
[name]: value,
...acc,
[name]: configValue,
};
}, {});

View file

@ -62,7 +62,7 @@ type ToggleVerifiedFulfilledActionType = {
payload: ToggleVerifiedAsyncActionType;
};
export type SafetyNumberActionTypes =
export type SafetyNumberActionType =
| GenerateActionType
| GenerateFulfilledActionType
| ToggleVerifiedActionType
@ -161,7 +161,7 @@ function getEmptyState(): SafetyNumberStateType {
export function reducer(
state: SafetyNumberStateType = getEmptyState(),
action: SafetyNumberActionTypes
action: SafetyNumberActionType
): SafetyNumberStateType {
if (action.type === TOGGLE_VERIFIED_PENDING) {
const { contact } = action.payload;

View file

@ -106,7 +106,7 @@ type SearchInConversationActionType = {
};
};
export type SEARCH_TYPES =
export type SearchActionType =
| SearchMessagesResultsKickoffActionType
| SearchDiscussionsResultsKickoffActionType
| SearchMessagesResultsFulfilledActionType
@ -336,7 +336,7 @@ function getEmptyState(): SearchStateType {
// tslint:disable-next-line cyclomatic-complexity max-func-body-length
export function reducer(
state: SearchStateType = getEmptyState(),
action: SEARCH_TYPES
action: SearchActionType
): SearchStateType {
if (action.type === 'SHOW_ARCHIVED_CONVERSATIONS') {
return getEmptyState();

View file

@ -32,12 +32,12 @@ import {
} from './ducks/network';
import {
reducer as safetyNumber,
SafetyNumberActionTypes,
SafetyNumberActionType,
SafetyNumberStateType,
} from './ducks/safetyNumber';
import {
reducer as search,
SEARCH_TYPES as SearchActionType,
SearchActionType,
SearchStateType,
} from './ducks/search';
import {
@ -73,7 +73,7 @@ export type ActionsType =
| ConversationActionType
| ItemsActionType
| NetworkActionType
| SafetyNumberActionTypes
| SafetyNumberActionType
| StickersActionType
| SearchActionType
| UpdatesActionType;

25
ts/state/types.ts Normal file
View file

@ -0,0 +1,25 @@
import { actions as calling } from './ducks/calling';
import { actions as conversations } from './ducks/conversations';
import { actions as emojis } from './ducks/emojis';
import { actions as expiration } from './ducks/expiration';
import { actions as items } from './ducks/items';
import { actions as network } from './ducks/network';
import { actions as safetyNumber } from './ducks/safetyNumber';
import { actions as search } from './ducks/search';
import { actions as stickers } from './ducks/stickers';
import { actions as updates } from './ducks/updates';
import { actions as user } from './ducks/user';
export type ReduxActions = {
calling: typeof calling;
conversations: typeof conversations;
emojis: typeof emojis;
expiration: typeof expiration;
items: typeof items;
network: typeof network;
safetyNumber: typeof safetyNumber;
search: typeof search;
stickers: typeof stickers;
updates: typeof updates;
user: typeof user;
};

View file

@ -30,6 +30,7 @@ import {
getRandomValue,
splitUuids,
} from '../Crypto';
import { getUserAgent } from '../util/getUserAgent';
import PQueue from 'p-queue';
import { v4 as getGuid } from 'uuid';
@ -243,7 +244,8 @@ function _createSocket(
{
certificateAuthority,
proxyUrl,
}: { certificateAuthority: string; proxyUrl?: string }
version,
}: { certificateAuthority: string; proxyUrl?: string; version: string }
) {
let requestOptions;
if (proxyUrl) {
@ -256,8 +258,10 @@ function _createSocket(
ca: certificateAuthority,
};
}
return new WebSocket(url, undefined, undefined, undefined, requestOptions, {
const headers = {
'User-Agent': getUserAgent(version),
};
return new WebSocket(url, undefined, undefined, headers, requestOptions, {
maxReceivedFrameSize: 0x210000,
});
}
@ -366,7 +370,7 @@ async function _promiseAjax(
method: options.type,
body: options.data,
headers: {
'User-Agent': `Signal Desktop ${options.version}`,
'User-Agent': getUserAgent(options.version),
'X-Signal-Agent': 'OWD',
...options.headers,
} as HeaderListType,
@ -412,6 +416,13 @@ async function _promiseAjax(
fetch(url, fetchOptions)
// tslint:disable-next-line max-func-body-length
.then(async response => {
// Build expired!
if (response.status === 499) {
window.log.error('Error: build expired');
window.storage.put('remoteBuildExpiration', Date.now());
window.reduxActions.expiration.hydrateExpirationStatus(true);
}
let resultPromise;
if (
(options.responseType === 'json' ||
@ -797,7 +808,9 @@ export type WebAPIType = {
options: GroupCredentialsType
) => Promise<string>;
whoami: () => Promise<any>;
getConfig: () => Promise<Array<{ name: string; enabled: boolean }>>;
getConfig: () => Promise<
Array<{ name: string; enabled: boolean; value: string | null }>
>;
};
export type SignedPreKeyType = {
@ -1021,7 +1034,7 @@ export function initialize({
async function getConfig() {
type ResType = {
config: Array<{ name: string; enabled: boolean }>;
config: Array<{ name: string; enabled: boolean; value: string | null }>;
};
const res: ResType = await _ajax({
call: 'config',
@ -2023,7 +2036,7 @@ export function initialize({
return _createSocket(
`${fixedScheme}/v1/websocket/?login=${login}&password=${pass}&agent=OWD&version=${clientVersion}`,
{ certificateAuthority, proxyUrl }
{ certificateAuthority, proxyUrl, version }
);
}
@ -2037,7 +2050,7 @@ export function initialize({
return _createSocket(
`${fixedScheme}/v1/websocket/provisioning/?agent=OWD&version=${clientVersion}`,
{ certificateAuthority, proxyUrl }
{ certificateAuthority, proxyUrl, version }
);
}

View file

@ -22,6 +22,7 @@ import { app, BrowserWindow, dialog, ipcMain } from 'electron';
import { getTempPath } from '../../app/attachments';
import { Dialogs } from '../types/Dialogs';
import { getUserAgent } from '../util/getUserAgent';
// @ts-ignore
import * as packageJson from '../../package.json';
@ -324,7 +325,7 @@ function getGotOptions(): GotOptions<null> {
ca,
headers: {
'Cache-Control': 'no-cache',
'User-Agent': `Signal Desktop ${packageJson.version}`,
'User-Agent': getUserAgent(packageJson.version),
},
useElectronNet: false,
};

3
ts/util/getUserAgent.ts Normal file
View file

@ -0,0 +1,3 @@
export function getUserAgent(appVersion: string): string {
return `Signal-Desktop/${appVersion}`;
}

View file

@ -11,11 +11,13 @@ import {
getPlaceholder as getSafetyNumberPlaceholder,
} from './safetyNumber';
import { getStringForProfileChange } from './getStringForProfileChange';
import { getUserAgent } from './getUserAgent';
import { hasExpired } from './hasExpired';
import { isFileDangerous } from './isFileDangerous';
import { makeLookup } from './makeLookup';
import { migrateColor } from './migrateColor';
import { missingCaseError } from './missingCaseError';
import { parseRemoteClientExpiration } from './parseRemoteClientExpiration';
import * as zkgroup from './zkgroup';
export {
@ -28,12 +30,14 @@ export {
generateSecurityNumber,
getSafetyNumberPlaceholder,
getStringForProfileChange,
getUserAgent,
GoogleChrome,
hasExpired,
isFileDangerous,
makeLookup,
migrateColor,
missingCaseError,
parseRemoteClientExpiration,
Registration,
zkgroup,
};

View file

@ -12920,7 +12920,7 @@
"rule": "jQuery-wrap(",
"path": "ts/textsecure/WebAPI.js",
"line": " const byteBuffer = window.dcodeIO.ByteBuffer.wrap(quote, 'binary', window.dcodeIO.ByteBuffer.LITTLE_ENDIAN);",
"lineNumber": 1213,
"lineNumber": 1223,
"reasonCategory": "falseMatch",
"updated": "2020-09-08T23:07:22.682Z"
},
@ -12928,8 +12928,8 @@
"rule": "jQuery-wrap(",
"path": "ts/textsecure/WebAPI.ts",
"line": " const byteBuffer = window.dcodeIO.ByteBuffer.wrap(",
"lineNumber": 2063,
"lineNumber": 2076,
"reasonCategory": "falseMatch",
"updated": "2020-09-08T23:07:22.682Z"
}
]
]

View file

@ -0,0 +1,33 @@
import semver from 'semver';
type RemoteVersion = {
'min-version': string;
iso8601: string;
};
export function parseRemoteClientExpiration(
remoteExpirationValue: string
): number | null {
const remoteVersions = JSON.parse(remoteExpirationValue) || [];
const ourVersion = window.getVersion();
return remoteVersions.reduce(
(acc: number | null, remoteVersion: RemoteVersion) => {
const minVersion = remoteVersion['min-version'];
const { iso8601 } = remoteVersion;
if (semver.gt(minVersion, ourVersion)) {
const timestamp = new Date(iso8601).getTime();
if (!acc) {
return timestamp;
}
return timestamp < acc ? timestamp : acc;
}
return acc;
},
null
);
}

3
ts/window.d.ts vendored
View file

@ -22,6 +22,7 @@ import { LocalizerType } from './types/Util';
import { CallHistoryDetailsType } from './types/Calling';
import { ColorType } from './types/Colors';
import { ConversationController } from './ConversationController';
import { ReduxActions } from './state/types';
import { SendOptionsType } from './textsecure/SendMessage';
import AccountManager from './textsecure/AccountManager';
import Data from './sql/Client';
@ -49,6 +50,7 @@ declare global {
getSocketStatus: () => number;
getTitle: () => string;
waitForEmptyEventQueue: () => Promise<void>;
getVersion: () => string;
showCallingPermissionsPopup: (forCamera: boolean) => Promise<void>;
i18n: LocalizerType;
isValidGuid: (maybeGuid: string) => boolean;
@ -65,6 +67,7 @@ declare global {
};
normalizeUuids: (obj: any, paths: Array<string>, context: string) => any;
platform: string;
reduxActions: ReduxActions;
restart: () => void;
showWindow: () => void;
setBadgeCount: (count: number) => void;