Bind remote config to libsignal-net

This commit is contained in:
trevor-signal
2026-01-16 16:20:36 -05:00
committed by GitHub
parent 3f98b4cc8f
commit 1c8b7fc45d
9 changed files with 157 additions and 40 deletions

View 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);
}
});
}
);
}

View File

@@ -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']);
}

View File

@@ -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();

View File

@@ -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();
}

View File

@@ -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: [] }));
});

View File

@@ -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;

View File

@@ -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];

View File

@@ -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();

View File

@@ -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