Skip to content

Commit 89dabbb

Browse files
ivolivol
authored andcommitted
fix: gate getLastARC to prevent uninitialized sdk exceptions
1 parent 41da521 commit 89dabbb

2 files changed

Lines changed: 323 additions & 236 deletions

File tree

android/src/main/java/io/approov/reactnative/ApproovService.java

Lines changed: 121 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,17 @@ public synchronized boolean getUseApproovStatusIfNoToken() {
203203
// The mutator instance used to control ApproovService behavior
204204
private static ApproovServiceMutator serviceMutator;
205205

206+
// Cached failure result from the last Approov token fetch that returned a
207+
// failure status.
208+
// Protected by failureCacheLock for thread-safe access. This avoids redundant
209+
// ~1s SDK calls
210+
// when the platform is in a sustained failure state (e.g. no network, MITM
211+
// detected).
212+
private static final Object failureCacheLock = new Object();
213+
private static Approov.TokenFetchResult cachedFailureResult = null;
214+
private static long cachedFailureTimeMs = 0;
215+
private static final long FAILURE_CACHE_TTL_MS = 500; // 0.5 seconds
216+
206217
static {
207218
ApproovDefaultMessageSigning signer = new ApproovDefaultMessageSigning();
208219
signer.setDefaultFactory(ApproovDefaultMessageSigning.generateDefaultSignatureParametersFactory());
@@ -234,13 +245,65 @@ public static ApproovServiceMutator getServiceMutator() {
234245
}
235246

236247
/**
237-
* Fetches an Approov token for the given URL.
248+
* Returns a cached failure result if one exists and hasn't expired.
249+
* Returns null if no cache exists or it has expired (caller should fetch from
250+
* SDK).
251+
*/
252+
private static Approov.TokenFetchResult getCachedFailure() {
253+
synchronized (failureCacheLock) {
254+
if (cachedFailureResult != null
255+
&& (System.currentTimeMillis() - cachedFailureTimeMs) < FAILURE_CACHE_TTL_MS) {
256+
return cachedFailureResult;
257+
}
258+
// Cache miss or expired — clear and allow a fresh SDK call
259+
cachedFailureResult = null;
260+
cachedFailureTimeMs = 0;
261+
return null;
262+
}
263+
}
264+
265+
/**
266+
* Caches a failure result. Only failure statuses are cached; success is never
267+
* cached.
268+
*/
269+
private static void cacheFailureIfNeeded(Approov.TokenFetchResult result) {
270+
switch (result.getStatus()) {
271+
case NO_NETWORK:
272+
case POOR_NETWORK:
273+
case MITM_DETECTED:
274+
case NO_APPROOV_SERVICE:
275+
synchronized (failureCacheLock) {
276+
cachedFailureResult = result;
277+
cachedFailureTimeMs = System.currentTimeMillis();
278+
}
279+
break;
280+
default:
281+
// Success and other statuses are never cached
282+
break;
283+
}
284+
}
285+
286+
/**
287+
* Performs a cached Approov token fetch. If a failure result is cached and
288+
* within
289+
* the TTL window, the cached failure is returned instantly. Otherwise a fresh
290+
* SDK
291+
* call is made and the result is cached if it is a failure.
238292
*
239293
* @param url is the URL giving the domain for the token fetch
240294
* @return the token fetch result
241295
*/
242-
public Approov.TokenFetchResult fetchApproovTokenAndWait(String url) {
243-
return Approov.fetchApproovTokenAndWait(url);
296+
public Approov.TokenFetchResult fetchApproovTokenCached(String url) {
297+
Approov.TokenFetchResult cached = getCachedFailure();
298+
if (cached != null) {
299+
log(LOG_DEBUG, TAG, "Using cached failure: " + cached.getStatus().toString());
300+
return cached;
301+
}
302+
303+
// Normal execution
304+
Approov.TokenFetchResult result = Approov.fetchApproovTokenAndWait(url);
305+
cacheFailureIfNeeded(result);
306+
return result;
244307
}
245308

246309
/**
@@ -694,6 +757,10 @@ public void initialize(String config, String comment, Promise promise) {
694757
initialConfig = config;
695758
isInitialized = true;
696759
clearEarliestNetworkRequestTime();
760+
synchronized (failureCacheLock) {
761+
cachedFailureResult = null;
762+
cachedFailureTimeMs = 0;
763+
}
697764
if (isApproovEnabled()) {
698765
log(LOG_INFO, TAG, "initialized on deviceID " + Approov.getDeviceID());
699766
notifyPinChangeListeners();
@@ -768,6 +835,11 @@ public void isApproovEnabled(Promise promise) {
768835
@ReactMethod
769836
public void getLastARC(Promise promise) {
770837
log(LOG_INFO, TAG, "ApproovService: getLastARC");
838+
if (!isInitialized || !isApproovEnabled()) {
839+
promise.resolve("");
840+
return;
841+
}
842+
771843
// Get the dynamic pins from Approov
772844
Map<String, List<String>> approovPins = Approov.getPins("public-key-sha256");
773845
if (approovPins == null || approovPins.isEmpty()) {
@@ -1318,14 +1390,6 @@ public void approovCallback(Approov.TokenFetchResult result) {
13181390
*/
13191391
@ReactMethod
13201392
public void precheck(Promise promise) {
1321-
if (!isInitialized) {
1322-
promise.reject("approov_error", "Approov is not initialized", getErrorUserInfo(false));
1323-
return;
1324-
}
1325-
if (!isApproovEnabled()) {
1326-
promise.reject("approov_error", "Approov is disabled", getErrorUserInfo(false));
1327-
return;
1328-
}
13291393
try {
13301394
Approov.fetchSecureString(new PrecheckHandler(promise), "precheck-dummy-key", null);
13311395
} catch (IllegalStateException e) {
@@ -1413,14 +1477,6 @@ public void getDeviceID(Promise promise) {
14131477
*/
14141478
@ReactMethod
14151479
public void setDataHashInToken(String data, Promise promise) {
1416-
if (!isInitialized) {
1417-
promise.reject("approov_error", "Approov is not initialized", getErrorUserInfo(false));
1418-
return;
1419-
}
1420-
if (!isApproovEnabled()) {
1421-
promise.reject("approov_error", "Approov is disabled", getErrorUserInfo(false));
1422-
return;
1423-
}
14241480
try {
14251481
Approov.setDataHashInToken(data);
14261482
log(LOG_DEBUG, TAG, "setDataHashInToken");
@@ -1449,14 +1505,6 @@ public void setDataHashInToken(String data, Promise promise) {
14491505
*/
14501506
@ReactMethod
14511507
public void fetchToken(String url, Promise promise) {
1452-
if (!isInitialized) {
1453-
promise.reject("approov_error", "Approov is not initialized", getErrorUserInfo(false));
1454-
return;
1455-
}
1456-
if (!isApproovEnabled()) {
1457-
promise.reject("approov_error", "Approov is disabled", getErrorUserInfo(false));
1458-
return;
1459-
}
14601508
try {
14611509
Approov.fetchApproovToken(new FetchTokenHandler(promise), url);
14621510
} catch (IllegalStateException e) {
@@ -1556,14 +1604,6 @@ public void getMessageSignature(String message, Promise promise) {
15561604
*/
15571605
@ReactMethod
15581606
public void fetchSecureString(String key, String newDef, Promise promise) {
1559-
if (!isInitialized) {
1560-
promise.reject("approov_error", "Approov is not initialized", getErrorUserInfo(false));
1561-
return;
1562-
}
1563-
if (!isApproovEnabled()) {
1564-
promise.reject("approov_error", "Approov is disabled", getErrorUserInfo(false));
1565-
return;
1566-
}
15671607
// determine the type of operation as the values themselves cannot be logged
15681608
String type = "lookup";
15691609
if (newDef != null)
@@ -1662,21 +1702,6 @@ else if ((result.getStatus() != Approov.TokenFetchStatus.SUCCESS) &&
16621702
*/
16631703
@ReactMethod
16641704
public void fetchCustomJWT(String payload, Promise promise) {
1665-
if (!isInitialized) {
1666-
promise.reject("approov_error", "Approov is not initialized", getErrorUserInfo(false));
1667-
return;
1668-
}
1669-
if (!isApproovEnabled()) {
1670-
promise.reject("approov_error", "Approov is disabled", getErrorUserInfo(false));
1671-
return;
1672-
}
1673-
try {
1674-
new org.json.JSONObject(payload);
1675-
} catch (org.json.JSONException e) {
1676-
promise.reject("fetchCustomJWT", "IllegalArgument: Malformed JSON payload", getErrorUserInfo(false));
1677-
return;
1678-
}
1679-
16801705
try {
16811706
Approov.fetchCustomJWT(new FetchCustomJWTHandler(promise), payload);
16821707
} catch (IllegalStateException e) {
@@ -1989,6 +2014,52 @@ public void getMaxReswizzleAttempts(Promise promise) {
19892014
promise.resolve(0);
19902015
}
19912016

2017+
/**
2018+
* Exposes the native Approov logging facility to Javascript.
2019+
*
2020+
* @param message the string message to log natively
2021+
* @param level the integer log level (e.g., ApproovService.Log.INFO)
2022+
*/
2023+
@ReactMethod
2024+
public void logMessage(String message, Integer level) {
2025+
int mappedLevel = (level != null) ? level : LOG_INFO;
2026+
log(mappedLevel, TAG, "JS: " + (message != null ? message : "null"));
2027+
}}}
2028+
2029+
promise.resolve(responseMap);}
2030+
2031+
}catch(
2032+
2033+
Exception e)
2034+
{
2035+
log(LOG_ERROR, TAG, "fetchWithApproov failed: " + e.getMessage());
2036+
promise.reject("network_error", e.getMessage(), e);
2037+
}
2038+
}).start();}
2039+
2040+
/**
2041+
* iOS-specific method to set the max reswizzle attempts.
2042+
* This is a no-op on Android since the OkHttp integration
2043+
* does not rely on method swizzling or +load races.
2044+
*
2045+
* @param attempts the maximum number of recovery attempts.
2046+
*/
2047+
@ReactMethod
2048+
public void setMaxReswizzleAttempts(Integer attempts) {
2049+
log(LOG_DEBUG, TAG, "setMaxReswizzleAttempts: no-op on Android");
2050+
}
2051+
2052+
/**
2053+
* iOS-specific method to get the max reswizzle attempts.
2054+
* Always returns 0 on Android since swizzling is not used.
2055+
*
2056+
* @param promise promise to be fulfilled with 0
2057+
*/
2058+
@ReactMethod
2059+
public void getMaxReswizzleAttempts(Promise promise) {
2060+
promise.resolve(0);
2061+
}
2062+
19922063
/**
19932064
* Exposes the native Approov logging facility to Javascript.
19942065
*

0 commit comments

Comments
 (0)