Skip to content

Commit 2ebd7cd

Browse files
authored
Check for X-Signal-Alert header, and report it in a callback
This on-connect header will be used for lightweight "alerts" from the server to an authenticated client. For now, it's only threaded through to the Node implementation; the iOS and Android ones will come later.
1 parent 3580994 commit 2ebd7cd

File tree

28 files changed

+312
-41
lines changed

28 files changed

+312
-41
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ derive-where = "1.2.5"
107107
derive_more = "1.0.0"
108108
displaydoc = "0.2"
109109
ed25519-dalek = "2.1.0"
110+
either = "1.10.0"
110111
env_logger = "0.11.4"
111112
futures = "0.3"
112113
futures-util = "0.3"

RELEASE_NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ v0.67.2
55
- Switch message chain key storage to store seed value rather than IV/MAC-key/key.
66
- Abstract Server(Private/Public)Params from endorsements. Reduces dependencies in clients and issuing servers.
77
- Add EndorsementPublicRootKey accessor to ServerPublicParams.
8+
- Node: ChatListener has a new optional callback for server alerts. (iOS and Android coming later.)

java/client/proguard-usage.txt.expected

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ org.signal.libsignal.metadata.SealedSessionCipher:
66
org.signal.libsignal.metadata.certificate.CertificateValidator:
77
41:48:void validate(org.signal.libsignal.metadata.certificate.ServerCertificate)
88
org.signal.libsignal.net.AuthenticatedChatConnection$FakeChatRemote:
9-
104:111:public org.signal.libsignal.internal.CompletableFuture getNextIncomingRequest()
10-
114:121:private static synthetic org.signal.libsignal.protocol.util.Pair lambda$getNextIncomingRequest$2(java.lang.Long)
11-
107:107:private synthetic org.signal.libsignal.internal.CompletableFuture lambda$getNextIncomingRequest$1(long)
12-
109:109:private static synthetic org.signal.libsignal.internal.CompletableFuture lambda$getNextIncomingRequest$0(long,long)
9+
115:122:public org.signal.libsignal.internal.CompletableFuture getNextIncomingRequest()
10+
125:132:private static synthetic org.signal.libsignal.protocol.util.Pair lambda$getNextIncomingRequest$2(java.lang.Long)
11+
118:118:private synthetic org.signal.libsignal.internal.CompletableFuture lambda$getNextIncomingRequest$1(long)
12+
120:120:private static synthetic org.signal.libsignal.internal.CompletableFuture lambda$getNextIncomingRequest$0(long,long)
1313
org.signal.libsignal.net.CdsiLookup:
1414
18:18:public static org.signal.libsignal.internal.CompletableFuture start(org.signal.libsignal.net.Network,java.lang.String,java.lang.String,org.signal.libsignal.net.CdsiLookupRequest)
1515
org.signal.libsignal.protocol.ServiceId:

java/client/src/main/java/org/signal/libsignal/net/AuthenticatedChatConnection.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,23 @@ void setChat(ChatConnection chat) {
7070
*/
7171
public static Pair<AuthenticatedChatConnection, FakeChatRemote> fakeConnect(
7272
final TokioAsyncContext tokioAsyncContext, ChatConnectionListener listener) {
73+
return fakeConnect(tokioAsyncContext, listener, new String[0]);
74+
}
75+
76+
/**
77+
* Test-only method to create a {@code AuthenticatedChatConnection} connected to a fake remote.
78+
*
79+
* <p>The returned {@link FakeChatRemote} can be used to send messages to the connection.
80+
*/
81+
public static Pair<AuthenticatedChatConnection, FakeChatRemote> fakeConnect(
82+
final TokioAsyncContext tokioAsyncContext, ChatConnectionListener listener, String[] alerts) {
7383

7484
return tokioAsyncContext.guardedMap(
7585
asyncContextHandle -> {
7686
SetChatLaterListenerBridge bridgeListener = new SetChatLaterListenerBridge();
7787
long fakeChatConnection =
78-
NativeTesting.TESTING_FakeChatConnection_Create(asyncContextHandle, bridgeListener);
88+
NativeTesting.TESTING_FakeChatConnection_Create(
89+
asyncContextHandle, bridgeListener, String.join("\n", alerts));
7990
AuthenticatedChatConnection chat =
8091
new AuthenticatedChatConnection(
8192
tokioAsyncContext,

java/shared/java/org/signal/libsignal/internal/NativeTesting.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ private NativeTesting() {}
6767
public static native Object TESTING_ErrorOnReturnAsync(Object needsCleanup);
6868
public static native CompletableFuture<Object> TESTING_ErrorOnReturnIo(long asyncRuntime, Object needsCleanup);
6969
public static native Object TESTING_ErrorOnReturnSync(Object needsCleanup);
70-
public static native long TESTING_FakeChatConnection_Create(long tokio, BridgeChatListener listener);
70+
public static native long TESTING_FakeChatConnection_Create(long tokio, BridgeChatListener listener, String alertsJoinedByNewlines);
7171
public static native long TESTING_FakeChatConnection_TakeAuthenticatedChat(long chat);
7272
public static native long TESTING_FakeChatConnection_TakeRemote(long chat);
7373
public static native void TESTING_FakeChatRemoteEnd_InjectConnectionInterrupted(long chat);

node/Native.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ type ChatListener = {
123123
ack: ServerMessageAck
124124
): void;
125125
_queue_empty(): void;
126+
_received_alerts(alerts: string[]): void;
126127
_connection_interrupted(
127128
// A LibSignalError or null, but not naming the type to avoid circular import dependencies.
128129
reason: Error | null
@@ -532,7 +533,7 @@ export function TESTING_ErrorOnBorrowSync(_input: null): void;
532533
export function TESTING_ErrorOnReturnAsync(_needsCleanup: null): Promise<null>;
533534
export function TESTING_ErrorOnReturnIo(asyncRuntime: Wrapper<NonSuspendingBackgroundThreadRuntime>, _needsCleanup: null): CancellablePromise<null>;
534535
export function TESTING_ErrorOnReturnSync(_needsCleanup: null): null;
535-
export function TESTING_FakeChatConnection_Create(tokio: Wrapper<TokioAsyncContext>, listener: ChatListener): FakeChatConnection;
536+
export function TESTING_FakeChatConnection_Create(tokio: Wrapper<TokioAsyncContext>, listener: ChatListener, alertsJoinedByNewlines: string): FakeChatConnection;
536537
export function TESTING_FakeChatConnection_TakeAuthenticatedChat(chat: Wrapper<FakeChatConnection>): AuthenticatedChatConnection;
537538
export function TESTING_FakeChatConnection_TakeRemote(chat: Wrapper<FakeChatConnection>): FakeChatRemoteEnd;
538539
export function TESTING_FakeChatRemoteEnd_InjectConnectionInterrupted(chat: Wrapper<FakeChatRemoteEnd>): void;

node/ts/net/Chat.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ export interface ChatServiceListener extends ConnectionEventsListener {
6262
* were in the queue *when the connection was established* have been delivered.
6363
*/
6464
onQueueEmpty(): void;
65+
66+
/**
67+
* Called when the server has alerts for the current device.
68+
*
69+
* In practice this happens as part of the connecting process.
70+
*/
71+
onReceivedAlerts?(alerts: string[]): void;
6572
}
6673

6774
/**
@@ -229,18 +236,22 @@ export class AuthenticatedChatConnection implements ChatConnection {
229236
*
230237
* @param asyncContext the async runtime to use
231238
* @param listener the listener to send events to
239+
* @param alerts alerts to send immediately upon connect
232240
* @returns an {@link AuthenticatedChatConnection} and handle for the remote
233241
* end of the fake connection.
234242
*/
235243
public static fakeConnect(
236244
asyncContext: TokioAsyncContext,
237-
listener: ChatServiceListener
245+
listener: ChatServiceListener,
246+
alerts?: ReadonlyArray<string>
238247
): [AuthenticatedChatConnection, Wrapper<Native.FakeChatRemoteEnd>] {
239248
const nativeChatListener = makeNativeChatListener(asyncContext, listener);
249+
240250
const fakeChat = newNativeHandle(
241251
Native.TESTING_FakeChatConnection_Create(
242252
asyncContext,
243-
new WeakListenerWrapper(nativeChatListener)
253+
new WeakListenerWrapper(nativeChatListener),
254+
alerts?.join('\n') ?? ''
244255
)
245256
);
246257

@@ -326,6 +337,9 @@ class WeakListenerWrapper implements Native.ChatListener {
326337
_queue_empty(): void {
327338
this.listener.deref()?._queue_empty();
328339
}
340+
_received_alerts(alerts: string[]): void {
341+
this.listener.deref()?._received_alerts(alerts);
342+
}
329343
}
330344

331345
function makeNativeChatListener(
@@ -348,6 +362,9 @@ function makeNativeChatListener(
348362
_queue_empty(): void {
349363
listener.onQueueEmpty();
350364
},
365+
_received_alerts(alerts: string[]): void {
366+
listener.onReceivedAlerts?.(alerts);
367+
},
351368
_connection_interrupted(cause: Error | null): void {
352369
listener.onConnectionInterrupted(cause as LibSignalError | null);
353370
},
@@ -365,6 +382,9 @@ function makeNativeChatListener(
365382
_queue_empty(): void {
366383
throw new Error('Event not supported on unauthenticated connection');
367384
},
385+
_received_alerts(_alerts: string[]): void {
386+
throw new Error('Event not supported on unauthenticated connection');
387+
},
368388
_connection_interrupted(cause: LibSignalError | null): void {
369389
listener.onConnectionInterrupted(cause);
370390
},

node/ts/test/ChatTest.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ describe('chat connection to mock server', () => {
128128
_ack: ChatServerMessageAck
129129
) => {},
130130
onQueueEmpty: () => {},
131+
onReceivedAlerts: (_alerts: string[]) => {},
131132
...listener,
132133
};
133134
const device = chatServer.device?.device;

node/ts/test/NetTest.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,12 +421,25 @@ describe('chat service api', () => {
421421
const listener = {
422422
onIncomingMessage: sinon.stub(),
423423
onQueueEmpty: sinon.stub(),
424+
onReceivedAlerts: sinon.stub(),
424425
onConnectionInterrupted: sinon.stub(),
425426
};
427+
428+
// We have to set this up ahead of time because the callback is scheduled as part of the
429+
// connect action.
430+
const receivedAlerts = new CompletablePromise();
431+
listener.onReceivedAlerts.callsFake(receivedAlerts.resolve);
432+
426433
const tokio = new TokioAsyncContext(Native.TokioAsyncContext_new());
427434
const [_chat, fakeRemote] = AuthenticatedChatConnection.fakeConnect(
428435
tokio,
429-
listener
436+
listener,
437+
['UPPERcase', 'lowercase']
438+
);
439+
440+
await receivedAlerts.done();
441+
expect(listener.onReceivedAlerts).to.have.been.calledOnceWith(
442+
sinon.match.array.deepEquals(['UPPERcase', 'lowercase'])
430443
);
431444

432445
// a helper function to check that the message has been passed to the listener
@@ -475,6 +488,9 @@ describe('chat service api', () => {
475488
onQueueEmpty(): void {
476489
recordCall('_queue_empty');
477490
},
491+
onReceivedAlerts(alerts: string[]): void {
492+
recordCall('_received_alerts', alerts);
493+
},
478494
onConnectionInterrupted(cause: object | null): void {
479495
recordCall('_connection_interrupted', cause);
480496
},
@@ -499,6 +515,10 @@ describe('chat service api', () => {
499515
];
500516
const callsReceived: [string, (object | null)[]][] = [];
501517
const callsExpected: [string, ((value: object | null) => void)[]][] = [
518+
[
519+
'_received_alerts',
520+
[(value: object | null) => expect(value).deep.equals([])],
521+
],
502522
['_incoming_message', []],
503523
['_queue_empty', []],
504524
['_incoming_message', []],
@@ -550,6 +570,9 @@ describe('chat service api', () => {
550570
onQueueEmpty(): void {
551571
fail('unexpected call');
552572
},
573+
onReceivedAlerts(_alerts: string[]): void {
574+
fail('unexpected call');
575+
},
553576
onConnectionInterrupted(cause: object | null): void {
554577
connectionInterruptedReasons.push(cause);
555578
completable.complete();
@@ -571,6 +594,7 @@ describe('chat service api', () => {
571594
const [chat, fakeRemote] = AuthenticatedChatConnection.fakeConnect(tokio, {
572595
onIncomingMessage: () => {},
573596
onQueueEmpty: () => {},
597+
onReceivedAlerts() {},
574598
onConnectionInterrupted: () => {},
575599
});
576600

0 commit comments

Comments
 (0)