mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-02-14 23:18:54 +00:00
Bind remote config to libsignal-net
This commit is contained in:
63
ts/LibsignalNetRemoteConfig.preload.ts
Normal file
63
ts/LibsignalNetRemoteConfig.preload.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import {
|
||||
type Net,
|
||||
BuildVariant,
|
||||
REMOTE_CONFIG_KEYS as KeysExpectedByLibsignalNet,
|
||||
} from '@signalapp/libsignal-client/dist/net.js';
|
||||
|
||||
import { isProduction } from './util/version.std.js';
|
||||
import * as RemoteConfig from './RemoteConfig.dom.js';
|
||||
import type { AddPrefix, ArrayValues } from './types/Util.std.js';
|
||||
import { createLogger } from './logging/log.std.js';
|
||||
|
||||
const log = createLogger('LibsignalNetRemoteConfig');
|
||||
|
||||
function convertToDesktopRemoteConfigKey<
|
||||
K extends ArrayValues<typeof KeysExpectedByLibsignalNet>,
|
||||
>(key: K): AddPrefix<K, 'desktop.libsignalNet.'> {
|
||||
return `desktop.libsignalNet.${key}`;
|
||||
}
|
||||
|
||||
export function bindRemoteConfigToLibsignalNet(
|
||||
libsignalNet: Net,
|
||||
appVersion: string
|
||||
): void {
|
||||
// Calls setLibsignalRemoteConfig and is reset when any libsignal remote
|
||||
// config key changes. Doing that asynchronously allows the callbacks for
|
||||
// multiple keys that are triggered by the same config fetch from the server
|
||||
// to be coalesced into a single timeout.
|
||||
let reloadRemoteConfig: NodeJS.Immediate | undefined;
|
||||
const libsignalBuildVariant = isProduction(appVersion)
|
||||
? BuildVariant.Production
|
||||
: BuildVariant.Beta;
|
||||
|
||||
const setLibsignalRemoteConfig = () => {
|
||||
const remoteConfigs = KeysExpectedByLibsignalNet.reduce((output, key) => {
|
||||
const value = RemoteConfig.getValue(convertToDesktopRemoteConfigKey(key));
|
||||
if (value !== undefined) {
|
||||
output.set(key, value);
|
||||
}
|
||||
return output;
|
||||
}, new Map<(typeof KeysExpectedByLibsignalNet)[number], string>());
|
||||
|
||||
log.info(
|
||||
'Setting libsignal-net remote config',
|
||||
Object.fromEntries(remoteConfigs)
|
||||
);
|
||||
libsignalNet.setRemoteConfig(remoteConfigs, libsignalBuildVariant);
|
||||
reloadRemoteConfig = undefined;
|
||||
};
|
||||
|
||||
setLibsignalRemoteConfig();
|
||||
|
||||
KeysExpectedByLibsignalNet.map(convertToDesktopRemoteConfigKey).forEach(
|
||||
key => {
|
||||
RemoteConfig.onChange(key, () => {
|
||||
if (reloadRemoteConfig === undefined) {
|
||||
reloadRemoteConfig = setImmediate(setLibsignalRemoteConfig);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import lodash from 'lodash';
|
||||
import semver from 'semver';
|
||||
import type { REMOTE_CONFIG_KEYS as KeysExpectedByLibsignalNet } from '@signalapp/libsignal-client/dist/net.js';
|
||||
|
||||
import type { getConfig } from './textsecure/WebAPI.preload.js';
|
||||
import { createLogger } from './logging/log.std.js';
|
||||
@@ -16,6 +17,12 @@ import { getCountryCode } from './types/PhoneNumber.std.js';
|
||||
import { parseRemoteClientExpiration } from './util/parseRemoteClientExpiration.dom.js';
|
||||
import type { StorageInterface } from './types/Storage.d.ts';
|
||||
import { ToastType } from './types/Toast.dom.js';
|
||||
import { assertDev, strictAssert } from './util/assert.std.js';
|
||||
import type {
|
||||
ArrayValues,
|
||||
AssertSameMembers,
|
||||
StripPrefix,
|
||||
} from './types/Util.std.js';
|
||||
|
||||
const { get, throttle } = lodash;
|
||||
|
||||
@@ -35,7 +42,7 @@ const SemverKeys = [
|
||||
'desktop.retireAccessKeyGroupSend.prod',
|
||||
] as const;
|
||||
|
||||
export type SemverKeyType = (typeof SemverKeys)[number];
|
||||
export type SemverKeyType = ArrayValues<typeof SemverKeys>;
|
||||
|
||||
const ScalarKeys = [
|
||||
'desktop.callQualitySurveyPPM',
|
||||
@@ -52,11 +59,6 @@ const ScalarKeys = [
|
||||
'desktop.retryRespondMaxAge',
|
||||
'desktop.senderKey.retry',
|
||||
'desktop.senderKeyMaxAge',
|
||||
'desktop.libsignalNet.enforceMinimumTls',
|
||||
'desktop.libsignalNet.shadowUnauthChatWithNoise',
|
||||
'desktop.libsignalNet.shadowAuthChatWithNoise',
|
||||
'desktop.libsignalNet.chatPermessageDeflate',
|
||||
'desktop.libsignalNet.chatPermessageDeflate.prod',
|
||||
'desktop.pollReceive.alpha',
|
||||
'desktop.pollReceive.beta1',
|
||||
'desktop.pollReceive.prod1',
|
||||
@@ -76,9 +78,39 @@ const ScalarKeys = [
|
||||
'global.textAttachmentLimitBytes',
|
||||
] as const;
|
||||
|
||||
const KnownConfigKeys = [...SemverKeys, ...ScalarKeys] as const;
|
||||
// These keys should always match those in Net.REMOTE_CONFIG_KEYS, prefixed by
|
||||
// `desktop.libsignalNet`
|
||||
const KnownDesktopLibsignalNetKeys = [
|
||||
'desktop.libsignalNet.chatPermessageDeflate.beta',
|
||||
'desktop.libsignalNet.chatPermessageDeflate.prod',
|
||||
'desktop.libsignalNet.chatPermessageDeflate',
|
||||
'desktop.libsignalNet.chatRequestConnectionCheckTimeoutMillis.beta',
|
||||
'desktop.libsignalNet.chatRequestConnectionCheckTimeoutMillis',
|
||||
'desktop.libsignalNet.disableNagleAlgorithm.beta',
|
||||
'desktop.libsignalNet.disableNagleAlgorithm',
|
||||
'desktop.libsignalNet.useH2ForUnauthChat.beta',
|
||||
'desktop.libsignalNet.useH2ForUnauthChat',
|
||||
] as const;
|
||||
|
||||
export type ConfigKeyType = (typeof KnownConfigKeys)[number];
|
||||
type KnownLibsignalKeysType = StripPrefix<
|
||||
ArrayValues<typeof KnownDesktopLibsignalNetKeys>,
|
||||
'desktop.libsignalNet.'
|
||||
>;
|
||||
type ExpectedLibsignalKeysType = ArrayValues<typeof KeysExpectedByLibsignalNet>;
|
||||
|
||||
const _assertLibsignalKeysMatch: AssertSameMembers<
|
||||
KnownLibsignalKeysType,
|
||||
ExpectedLibsignalKeysType
|
||||
> = true;
|
||||
strictAssert(_assertLibsignalKeysMatch, 'Libsignal keys match');
|
||||
|
||||
const KnownConfigKeys = [
|
||||
...SemverKeys,
|
||||
...ScalarKeys,
|
||||
...KnownDesktopLibsignalNetKeys,
|
||||
] as const;
|
||||
|
||||
export type ConfigKeyType = ArrayValues<typeof KnownConfigKeys>;
|
||||
|
||||
type ConfigValueType = {
|
||||
name: ConfigKeyType;
|
||||
@@ -92,7 +124,7 @@ type ConfigListenersMapType = {
|
||||
[key: string]: Array<ConfigListenerType>;
|
||||
};
|
||||
|
||||
let config: ConfigMapType = {};
|
||||
let config: ConfigMapType | undefined;
|
||||
const listeners: ConfigListenersMapType = {};
|
||||
|
||||
export type OptionsType = Readonly<{
|
||||
@@ -272,6 +304,10 @@ export function isEnabled(
|
||||
// when called from UI component, provide redux config (items.remoteConfig)
|
||||
reduxConfig?: ConfigMapType
|
||||
): boolean {
|
||||
assertDev(
|
||||
reduxConfig != null || config != null,
|
||||
'getValue called before remote config is ready'
|
||||
);
|
||||
return get(reduxConfig ?? config, [name, 'enabled'], false);
|
||||
}
|
||||
|
||||
@@ -279,6 +315,10 @@ export function getValue(
|
||||
name: ConfigKeyType, // when called from UI component, provide redux config (items.remoteConfig)
|
||||
reduxConfig?: ConfigMapType
|
||||
): string | undefined {
|
||||
assertDev(
|
||||
reduxConfig != null || config != null,
|
||||
'getValue called before remote config is ready'
|
||||
);
|
||||
return get(reduxConfig ?? config, [name, 'value']);
|
||||
}
|
||||
|
||||
|
||||
@@ -285,6 +285,8 @@ import {
|
||||
import { JobCancelReason } from './jobs/types.std.js';
|
||||
import { itemStorage } from './textsecure/Storage.preload.js';
|
||||
import { isPinnedMessagesReceiveEnabled } from './util/isPinnedMessagesEnabled.dom.js';
|
||||
import { initMessageCleanup } from './services/messageStateCleanup.dom.js';
|
||||
import { MessageCache } from './services/MessageCache.preload.js';
|
||||
|
||||
const { isNumber, throttle } = lodash;
|
||||
|
||||
@@ -551,6 +553,9 @@ export async function startApp(): Promise<void> {
|
||||
storage: itemStorage,
|
||||
});
|
||||
|
||||
MessageCache.install();
|
||||
initMessageCleanup();
|
||||
|
||||
window.Whisper.events.on('firstEnvelope', checkFirstEnvelope);
|
||||
|
||||
const buildExpirationService = new BuildExpirationService();
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
import * as durations from '../util/durations/index.std.js';
|
||||
import { isEnabled } from '../RemoteConfig.dom.js';
|
||||
import { MessageCache } from './MessageCache.preload.js';
|
||||
|
||||
const TEN_MINUTES = 10 * durations.MINUTE;
|
||||
|
||||
@@ -12,6 +11,4 @@ export function initMessageCleanup(): void {
|
||||
() => window.MessageCache.deleteExpiredMessages(TEN_MINUTES),
|
||||
isEnabled('desktop.messageCleanup') ? TEN_MINUTES : durations.HOUR
|
||||
);
|
||||
|
||||
MessageCache.install();
|
||||
}
|
||||
@@ -16,7 +16,8 @@ describe('isConversationTooBigToRing', () => {
|
||||
const fakeMemberships = (count: number) =>
|
||||
times(count, () => ({ aci: generateAci(), isAdmin: false }));
|
||||
|
||||
it('returns false if there are no memberships (i.e., for a direct conversation)', () => {
|
||||
it('returns false if there are no memberships (i.e., for a direct conversation)', async () => {
|
||||
await updateRemoteConfig([]);
|
||||
assert.isFalse(isConversationTooBigToRing({}));
|
||||
assert.isFalse(isConversationTooBigToRing({ memberships: [] }));
|
||||
});
|
||||
|
||||
@@ -77,7 +77,6 @@ import {
|
||||
import type { CDSAuthType, CDSResponseType } from './cds/Types.d.ts';
|
||||
import { CDSI } from './cds/CDSI.node.js';
|
||||
import { SignalService as Proto } from '../protobuf/index.std.js';
|
||||
import { isEnabled as isRemoteConfigEnabled } from '../RemoteConfig.dom.js';
|
||||
|
||||
import type {
|
||||
WebAPICredentials,
|
||||
@@ -120,6 +119,7 @@ import {
|
||||
RemoteMegaphoneCtaDataSchema,
|
||||
type RemoteMegaphoneId,
|
||||
} from '../types/Megaphone.std.js';
|
||||
import { bindRemoteConfigToLibsignalNet } from '../LibsignalNetRemoteConfig.preload.js';
|
||||
|
||||
const { escapeRegExp, isNumber, throttle } = lodash;
|
||||
|
||||
@@ -1701,27 +1701,6 @@ const PARSE_RANGE_HEADER = /\/(\d+)$/;
|
||||
const PARSE_GROUP_LOG_RANGE_HEADER =
|
||||
/^versions\s+(\d{1,10})-(\d{1,10})\/(\d{1,10})/;
|
||||
|
||||
const libsignalRemoteConfig = new Map();
|
||||
if (isRemoteConfigEnabled('desktop.libsignalNet.enforceMinimumTls')) {
|
||||
log.info('libsignal net will require TLS 1.3');
|
||||
libsignalRemoteConfig.set('enforceMinimumTls', 'true');
|
||||
}
|
||||
if (isRemoteConfigEnabled('desktop.libsignalNet.shadowUnauthChatWithNoise')) {
|
||||
log.info('libsignal net will shadow unauth chat connections');
|
||||
libsignalRemoteConfig.set('shadowUnauthChatWithNoise', 'true');
|
||||
}
|
||||
if (isRemoteConfigEnabled('desktop.libsignalNet.shadowAuthChatWithNoise')) {
|
||||
log.info('libsignal net will shadow auth chat connections');
|
||||
libsignalRemoteConfig.set('shadowAuthChatWithNoise', 'true');
|
||||
}
|
||||
const perMessageDeflateConfigKey = isProduction(version)
|
||||
? 'desktop.libsignalNet.chatPermessageDeflate.prod'
|
||||
: 'desktop.libsignalNet.chatPermessageDeflate';
|
||||
if (isRemoteConfigEnabled(perMessageDeflateConfigKey)) {
|
||||
libsignalRemoteConfig.set('chatPermessageDeflate', 'true');
|
||||
}
|
||||
libsignalNet.setRemoteConfig(libsignalRemoteConfig);
|
||||
|
||||
const socketManager = new SocketManager(libsignalNet, {
|
||||
url: chatServiceUrl,
|
||||
certificateAuthority,
|
||||
@@ -1776,6 +1755,8 @@ export async function connect({
|
||||
hasStoriesDisabled,
|
||||
hasBuildExpired,
|
||||
}: WebAPIConnectOptionsType): Promise<void> {
|
||||
bindRemoteConfigToLibsignalNet(getLibsignalNet(), window.getVersion());
|
||||
|
||||
username = initialUsername;
|
||||
password = initialPassword;
|
||||
|
||||
|
||||
@@ -131,3 +131,32 @@ export type ExactKeys<T, K extends ReadonlyArray<string>> =
|
||||
? T
|
||||
: 'Error: Array has fields not present in object type'
|
||||
: 'Error: Object type has keys not present in array';
|
||||
|
||||
export type StripPrefix<
|
||||
T extends string,
|
||||
Prefix extends string,
|
||||
> = T extends `${Prefix}${infer Rest}` ? Rest : T;
|
||||
|
||||
export type AddPrefix<
|
||||
T extends string,
|
||||
Prefix extends string,
|
||||
> = `${Prefix}${T}`;
|
||||
|
||||
type Missing<A, B> = Exclude<B, A>;
|
||||
type Extra<A, B> = Exclude<A, B>;
|
||||
|
||||
export type AssertSameMembers<Actual, Expected> = [
|
||||
Missing<Actual, Expected>,
|
||||
] extends [never]
|
||||
? [Extra<Actual, Expected>] extends [never]
|
||||
? true
|
||||
: {
|
||||
error: 'Extra keys';
|
||||
extra: Extra<Actual, Expected>;
|
||||
}
|
||||
: {
|
||||
error: 'Missing keys';
|
||||
missing: Missing<Actual, Expected>;
|
||||
};
|
||||
|
||||
export type ArrayValues<T extends ReadonlyArray<unknown>> = T[number];
|
||||
|
||||
@@ -14,12 +14,13 @@ import chaiAsPromised from 'chai-as-promised';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { reporters, type MochaOptions } from 'mocha';
|
||||
|
||||
import { initMessageCleanup } from '../../services/messageStateCleanup.preload.js';
|
||||
import { initializeMessageCounter } from '../../util/incrementMessageCounter.preload.js';
|
||||
import { initializeRedux } from '../../state/initializeRedux.preload.js';
|
||||
import * as Stickers from '../../types/Stickers.preload.js';
|
||||
import { ThemeType } from '../../types/Util.std.js';
|
||||
import { itemStorage } from '../../textsecure/Storage.preload.js';
|
||||
import { MessageCache } from '../../services/MessageCache.preload.js';
|
||||
import { updateRemoteConfig } from '../../test-helpers/RemoteConfigStub.dom.js';
|
||||
|
||||
chai.use(chaiAsPromised);
|
||||
|
||||
@@ -95,7 +96,10 @@ window.testUtilities = {
|
||||
},
|
||||
|
||||
async initialize() {
|
||||
initMessageCleanup();
|
||||
// Since background.preload.ts is not loaded in tests, we need to do some minimal
|
||||
// setup
|
||||
MessageCache.install();
|
||||
await updateRemoteConfig([]);
|
||||
await initializeMessageCounter();
|
||||
await Stickers.load();
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ import type {
|
||||
import { cdsLookup, getSocketStatus } from '../../textsecure/WebAPI.preload.js';
|
||||
import type { FeatureFlagType } from '../../window.d.ts';
|
||||
import type { StorageAccessType } from '../../types/Storage.d.ts';
|
||||
import { initMessageCleanup } from '../../services/messageStateCleanup.preload.js';
|
||||
import { calling } from '../../services/calling.preload.js';
|
||||
import { Environment, getEnvironment } from '../../environment.std.js';
|
||||
import { isProduction } from '../../util/version.std.js';
|
||||
@@ -57,8 +56,6 @@ if (window.SignalContext.config.proxyUrl) {
|
||||
log.info('Using provided proxy url');
|
||||
}
|
||||
|
||||
initMessageCleanup();
|
||||
|
||||
if (
|
||||
!isProduction(window.SignalContext.getVersion()) ||
|
||||
window.SignalContext.config.devTools
|
||||
|
||||
Reference in New Issue
Block a user