Support server-determined build expiration
This commit is contained in:
parent
a04f9a0e51
commit
d87335f5a6
14 changed files with 147 additions and 28 deletions
|
@ -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',
|
||||
|
|
10
preload.js
10
preload.js
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}, {});
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
25
ts/state/types.ts
Normal 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;
|
||||
};
|
|
@ -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 }
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
3
ts/util/getUserAgent.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export function getUserAgent(appVersion: string): string {
|
||||
return `Signal-Desktop/${appVersion}`;
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
|
33
ts/util/parseRemoteClientExpiration.ts
Normal file
33
ts/util/parseRemoteClientExpiration.ts
Normal 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
3
ts/window.d.ts
vendored
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue