Skip to content

Commit 0a58e80

Browse files
authored
Add support for a retry later duration in rate limit challenge responses
1 parent c2db790 commit 0a58e80

File tree

16 files changed

+175
-28
lines changed

16 files changed

+175
-28
lines changed

RELEASE_NOTES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
v0.90.0
22

3+
- Add support for a retry later duration in rate limit responses
4+
35
- keytrans: Simplify the top-level API
46

57
- Use CDSI enclave 3a1ac5e5 in staging.

java/client/src/main/java/org/signal/libsignal/net/RateLimitChallengeException.kt

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package org.signal.libsignal.net
77

88
import org.signal.libsignal.internal.CalledFromNative
9+
import java.time.Duration
910
import java.util.EnumSet
1011

1112
/**
@@ -17,10 +18,30 @@ import java.util.EnumSet
1718
public class RateLimitChallengeException : ChatServiceException {
1819
public val token: String
1920
public val options: Set<ChallengeOption>
21+
public val retryLater: Duration?
2022

21-
@CalledFromNative
22-
public constructor(message: String, token: String, options: Array<ChallengeOption>) : super(message) {
23+
public constructor(
24+
message: String,
25+
token: String,
26+
options: Array<ChallengeOption>,
27+
retryLater: Duration?,
28+
) : super(message) {
2329
this.token = token
2430
this.options = EnumSet.copyOf(options.asList())
31+
this.retryLater = retryLater
32+
}
33+
34+
@CalledFromNative
35+
internal constructor(
36+
message: String,
37+
token: String,
38+
options: Array<ChallengeOption>,
39+
retryLater: Long,
40+
) : this(
41+
message,
42+
token,
43+
options,
44+
if (retryLater < 0) null else Duration.ofSeconds(retryLater),
45+
) {
2546
}
2647
}

java/client/src/test/java/org/signal/libsignal/net/RegistrationServiceTest.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,11 +266,18 @@ private static void assertIsServerSideError(ThrowingConsumer<String> throwError)
266266
}
267267

268268
private static void assertIsPushChallengeError(ThrowingConsumer<String> throwError) {
269-
RateLimitChallengeException e =
269+
final RateLimitChallengeException e =
270270
assertRegistrationSessionErrorIs(
271271
"PushChallenge", RateLimitChallengeException.class, throwError);
272272
assertEquals(e.getToken(), "token");
273273
assertEquals(e.getOptions(), EnumSet.of(ChallengeOption.PUSH_CHALLENGE));
274+
assertEquals(e.getRetryLater(), null);
275+
final RateLimitChallengeException e2 =
276+
assertRegistrationSessionErrorIs(
277+
"PushChallengeRetryAfter42Seconds", RateLimitChallengeException.class, throwError);
278+
assertEquals(e2.getToken(), "token42");
279+
assertEquals(e2.getOptions(), EnumSet.of(ChallengeOption.PUSH_CHALLENGE));
280+
assertEquals(e2.getRetryLater(), Duration.ofSeconds(42));
274281
}
275282

276283
@Test

node/ts/Errors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@ export type RateLimitChallengeError = LibSignalErrorBase & {
307307
code: ErrorCode.RateLimitChallengeError;
308308
readonly token: string;
309309
readonly options: Set<'pushChallenge' | 'captcha'>;
310+
readonly retryAfterSecs: number | null;
310311
};
311312

312313
export type ChatServiceInactive = LibSignalErrorBase & {

node/ts/test/RegistrationTest.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,16 @@ describe('Registration types', () => {
133133
code: ErrorCode.RateLimitChallengeError,
134134
token: 'token',
135135
options: new Set(['pushChallenge']),
136+
retryAfterSecs: null,
137+
},
138+
];
139+
const rateLimitRetryChallengeCase: [string, object] = [
140+
'PushChallengeRetryAfter42Seconds',
141+
{
142+
code: ErrorCode.RateLimitChallengeError,
143+
token: 'token42',
144+
options: new Set(['pushChallenge']),
145+
retryAfterSecs: 42,
136146
},
137147
];
138148
const cases: Array<{
@@ -150,6 +160,7 @@ describe('Registration types', () => {
150160
timeoutCase,
151161
serverSideErrorCase,
152162
rateLimitChallengeCase,
163+
rateLimitRetryChallengeCase,
153164
],
154165
},
155166
{
@@ -162,6 +173,7 @@ describe('Registration types', () => {
162173
timeoutCase,
163174
serverSideErrorCase,
164175
rateLimitChallengeCase,
176+
rateLimitRetryChallengeCase,
165177
],
166178
},
167179
{

rust/bridge/ffi/src/error.rs

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use libsignal_bridge::ffi::{
1212
use libsignal_bridge::{IllegalArgumentError, ffi_arg_type, ffi_result_type};
1313
use libsignal_bridge_macros::bridge_fn;
1414
use libsignal_core::ProtocolAddress;
15+
use libsignal_net::infra::errors::RetryLater;
1516
use libsignal_net_chat::api::ChallengeOption;
1617
use libsignal_net_chat::api::messages::MismatchedDeviceError;
1718
use uuid::Uuid;
@@ -146,17 +147,28 @@ fn Error_GetTriesRemaining(err: &SignalFfiError) -> Result<u32, IllegalArgumentE
146147
})
147148
}
148149

150+
#[allow(clippy::type_complexity)]
149151
#[bridge_fn(jni = false, node = false)]
150152
fn Error_GetRateLimitChallenge(
151153
err: &SignalFfiError,
152-
) -> Result<(String, Box<[ChallengeOption]>), IllegalArgumentError> {
153-
let libsignal_net_chat::api::RateLimitChallenge { token, options } =
154-
err.provide_rate_limit_challenge().map_err(|_| {
155-
IllegalArgumentError::new(format!(
156-
"cannot get rate limit challenge error from error ({err})"
157-
))
158-
})?;
159-
Ok((token.clone(), options[..].into()))
154+
) -> Result<((String, Box<[ChallengeOption]>), i64), IllegalArgumentError> {
155+
let libsignal_net_chat::api::RateLimitChallenge {
156+
token,
157+
options,
158+
retry_later,
159+
} = err.provide_rate_limit_challenge().map_err(|_| {
160+
IllegalArgumentError::new(format!(
161+
"cannot get rate limit challenge error from error ({err})"
162+
))
163+
})?;
164+
let retry_later = retry_later
165+
.map(
166+
|RetryLater {
167+
retry_after_seconds,
168+
}| i64::from(retry_after_seconds),
169+
)
170+
.unwrap_or(-1);
171+
Ok(((token.clone(), options[..].into()), retry_later))
160172
}
161173

162174
#[bridge_fn(jni = false, node = false)]

rust/bridge/shared/testing/src/net/registration.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,12 @@ impl<TestE: for<'a> TryFrom<&'a str, Error = strum::ParseError>> TryFrom<String>
189189
"PushChallenge" => RequestError::Challenge(RateLimitChallenge {
190190
token: "token".to_owned(),
191191
options: vec![ChallengeOption::PushChallenge],
192+
retry_later: None,
193+
}),
194+
"PushChallengeRetryAfter42Seconds" => RequestError::Challenge(RateLimitChallenge {
195+
token: "token42".to_owned(),
196+
options: vec![ChallengeOption::PushChallenge],
197+
retry_later: Some(RETRY_AFTER_42_SECONDS),
192198
}),
193199
"ServerSideError" => RequestError::ServerSideError,
194200
_ => TestE::try_from(&value)

rust/bridge/shared/types/src/ffi/convert.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1381,6 +1381,7 @@ trivial!(u8);
13811381
trivial!(u16);
13821382
trivial!(u32);
13831383
trivial!(u64);
1384+
trivial!(i64);
13841385
trivial!(usize);
13851386
trivial!(bool);
13861387

@@ -1493,6 +1494,7 @@ macro_rules! ffi_result_type {
14931494
(u32) => (u32);
14941495
(Option<u32>) => (u32);
14951496
(u64) => (u64);
1497+
(i64) => (i64);
14961498
(Option<u64>) => (u64);
14971499
(bool) => (bool);
14981500
(&str) => (*const std::ffi::c_char);

rust/bridge/shared/types/src/jni/mod.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,18 +1031,32 @@ impl JniError for RateLimitChallenge {
10311031
&self,
10321032
env: &mut JNIEnv<'a>,
10331033
) -> Result<JThrowable<'a>, BridgeLayerError> {
1034-
let Self { token, options } = self;
1034+
let Self {
1035+
token,
1036+
options,
1037+
retry_later,
1038+
} = self;
10351039
let (message, token) =
10361040
try_scoped(|| Ok((env.new_string(self.to_string())?, env.new_string(token)?)))
10371041
.check_exceptions(env, "RateLimitChallenge")?;
10381042
let options = options.as_slice().convert_into(env)?;
1043+
let retry_later = retry_later
1044+
.as_ref()
1045+
.map(
1046+
|RetryLater {
1047+
retry_after_seconds,
1048+
}| i64::from(*retry_after_seconds),
1049+
)
1050+
.unwrap_or(-1);
10391051
new_instance(
10401052
env,
10411053
ClassName("org.signal.libsignal.net.RateLimitChallengeException"),
10421054
jni_args!((
10431055
message => java.lang.String,
10441056
token => java.lang.String,
1045-
options => [org.signal.libsignal.net.ChallengeOption]) -> void),
1057+
options => [org.signal.libsignal.net.ChallengeOption],
1058+
retry_later => long,
1059+
) -> void),
10461060
)
10471061
.map(Into::into)
10481062
}

rust/bridge/shared/types/src/node/error.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
use std::borrow::Cow;
77
use std::fmt;
88

9-
use libsignal_net::infra::errors::TransportConnectError;
9+
use libsignal_net::infra::errors::{RetryLater, TransportConnectError};
1010
use libsignal_net::infra::ws::WebSocketConnectError;
1111
use libsignal_net_chat::api::keys::GetPreKeysFailure;
1212
use neon::thread::LocalKey;
@@ -521,15 +521,28 @@ impl SignalNodeError for libsignal_net_chat::api::RateLimitChallenge {
521521
operation_name: &str,
522522
) -> Handle<'a, JsError> {
523523
let message = self.to_string();
524-
let Self { token, options } = self;
524+
let Self {
525+
token,
526+
options,
527+
retry_later,
528+
} = self;
525529
let properties = move |cx: &mut C| {
526530
let token = cx.string(token);
527531
let options = options.into_boxed_slice().convert_into(cx)?.upcast();
528532
let set_constructor: Handle<'_, JsFunction> = cx.global("Set")?;
529533
let options = set_constructor.construct(cx, [options])?;
530534
let props = cx.empty_object();
535+
let retry_later = retry_later
536+
.map(
537+
|RetryLater {
538+
retry_after_seconds,
539+
}| cx.number(retry_after_seconds),
540+
)
541+
.map(|x| x.as_value(cx))
542+
.unwrap_or_else(|| cx.null().as_value(cx));
531543
props.set(cx, "token", token)?;
532544
props.set(cx, "options", options)?;
545+
props.set(cx, "retryAfterSecs", retry_later)?;
533546
Ok(props.upcast())
534547
};
535548
new_js_error(

0 commit comments

Comments
 (0)