Skip to content

Commit 9421586

Browse files
Fail Firestore client if we can't establish SSL connection (#508)
1 parent 44d0645 commit 9421586

File tree

5 files changed

+66
-2
lines changed

5 files changed

+66
-2
lines changed

firebase-firestore/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
# Unreleased
2+
- [changed] Instead of failing silently, Firestore now crashes the client app
3+
if it fails to load SSL Ciphers. To avoid these crashes, you must bundle
4+
Conscrypt to support non-GMSCore devices on Android KitKat or JellyBean (see
5+
https://github.com/grpc/grpc-java/blob/master/SECURITY.md#tls-on-android).
26

37
# 20.1.0
48
- [changed] SSL and gRPC initialization now happens on a separate thread, which

firebase-firestore/src/main/java/com/google/firebase/firestore/remote/AbstractStream.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
package com.google.firebase.firestore.remote;
1616

17+
import static com.google.firebase.firestore.remote.Datastore.SSL_DEPENDENCY_ERROR_MESSAGE;
1718
import static com.google.firebase.firestore.util.Assert.hardAssert;
1819

1920
import androidx.annotation.Nullable;
@@ -24,6 +25,7 @@
2425
import com.google.firebase.firestore.util.AsyncQueue.TimerId;
2526
import com.google.firebase.firestore.util.ExponentialBackoff;
2627
import com.google.firebase.firestore.util.Logger;
28+
import com.google.firebase.firestore.util.Util;
2729
import io.grpc.ClientCall;
2830
import io.grpc.Metadata;
2931
import io.grpc.MethodDescriptor;
@@ -269,6 +271,13 @@ private void close(State finalState, Status status) {
269271
"Can't provide an error when not in an error state.");
270272
workerQueue.verifyIsCurrentThread();
271273

274+
if (Datastore.isSslHandshakeError(status)) {
275+
// The Android device is missing required SSL Ciphers. This error is non-recoverable and must
276+
// be addressed by the app developer (see https://bit.ly/2XFpdma).
277+
Util.crashMainThread(
278+
new IllegalStateException(SSL_DEPENDENCY_ERROR_MESSAGE, status.getCause()));
279+
}
280+
272281
// Cancel any outstanding timers (they're guaranteed not to execute).
273282
cancelIdleCheck();
274283
this.backoff.cancel();

firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package com.google.firebase.firestore.remote;
1616

1717
import android.content.Context;
18+
import android.os.Build;
1819
import com.google.android.gms.tasks.Task;
1920
import com.google.firebase.firestore.FirebaseFirestoreException;
2021
import com.google.firebase.firestore.auth.CredentialsProvider;
@@ -31,13 +32,15 @@
3132
import com.google.firestore.v1.CommitResponse;
3233
import com.google.firestore.v1.FirestoreGrpc;
3334
import io.grpc.Status;
35+
import java.net.ConnectException;
3436
import java.util.ArrayList;
3537
import java.util.Arrays;
3638
import java.util.HashMap;
3739
import java.util.HashSet;
3840
import java.util.List;
3941
import java.util.Map;
4042
import java.util.Set;
43+
import javax.net.ssl.SSLHandshakeException;
4144

4245
/**
4346
* Datastore represents a proxy for the remote server, hiding details of the RPC layer. It:
@@ -54,6 +57,16 @@
5457
*/
5558
public class Datastore {
5659

60+
/**
61+
* Error message to surface when Firestore fails to establish an SSL connection. A failed SSL
62+
* connection likely indicates that the developer needs to provide an updated OpenSSL stack as
63+
* part of their app's dependencies.
64+
*/
65+
static final String SSL_DEPENDENCY_ERROR_MESSAGE =
66+
"The Firestore SDK failed to establish a secure connection. This is likely a problem with "
67+
+ "your app, rather than with Firestore itself. See https://bit.ly/2XFpdma for "
68+
+ "instructions on how to enable TLS on Android 4.x devices.";
69+
5770
/** Set of lowercase, white-listed headers for logging purposes. */
5871
static final Set<String> WHITE_LISTED_HEADERS =
5972
new HashSet<>(
@@ -208,6 +221,22 @@ public static boolean isPermanentError(Status status) {
208221
}
209222
}
210223

224+
/**
225+
* Determine whether the given status maps to the error that GRPC-Java throws when an Android
226+
* device is missing required SSL Ciphers.
227+
*
228+
* <p>This error is non-recoverable and must be addressed by the app developer.
229+
*/
230+
public static boolean isSslHandshakeError(Status status) {
231+
Status.Code code = status.getCode();
232+
Throwable t = status.getCause();
233+
234+
return Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP
235+
&& code.equals(Status.Code.UNAVAILABLE)
236+
&& (t instanceof SSLHandshakeException
237+
|| (t instanceof ConnectException && t.getMessage().contains("EHOSTUNREACH")));
238+
}
239+
211240
/**
212241
* Determines whether the given status has an error code that represents a permanent error when
213242
* received in response to a write operation.

firebase-firestore/src/main/java/com/google/firebase/firestore/remote/FirestoreChannel.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ public void onClose(Status status, Metadata trailers) {
199199
if (status.isOk()) {
200200
tcs.setResult(results);
201201
} else {
202-
tcs.setException(Util.exceptionFromStatus(status));
202+
tcs.setException(exceptionFromStatus(status));
203203
}
204204
}
205205
},
@@ -244,7 +244,7 @@ public void onClose(Status status, Metadata trailers) {
244244
Code.INTERNAL));
245245
}
246246
} else {
247-
tcs.setException(Util.exceptionFromStatus(status));
247+
tcs.setException(exceptionFromStatus(status));
248248
}
249249
}
250250
},
@@ -262,6 +262,17 @@ public void onClose(Status status, Metadata trailers) {
262262
return tcs.getTask();
263263
}
264264

265+
private FirebaseFirestoreException exceptionFromStatus(Status status) {
266+
if (Datastore.isSslHandshakeError(status)) {
267+
return new FirebaseFirestoreException(
268+
Datastore.SSL_DEPENDENCY_ERROR_MESSAGE,
269+
Code.fromValue(status.getCode().value()),
270+
status.getCause());
271+
}
272+
273+
return Util.exceptionFromStatus(status);
274+
}
275+
265276
public void invalidateToken() {
266277
credentialsProvider.invalidateToken();
267278
}

firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
package com.google.firebase.firestore.util;
1616

17+
import android.os.Handler;
18+
import android.os.Looper;
1719
import androidx.annotation.Nullable;
1820
import com.google.android.gms.tasks.Continuation;
1921
import com.google.cloud.datastore.core.number.NumberComparisonHelper;
@@ -211,4 +213,13 @@ public static String toDebugString(ByteString bytes) {
211213
public static String typeName(@Nullable Object obj) {
212214
return obj == null ? "null" : obj.getClass().getName();
213215
}
216+
217+
/** Raises an exception on Android's UI Thread and crashes the end user's app. */
218+
public static void crashMainThread(RuntimeException exception) {
219+
new Handler(Looper.getMainLooper())
220+
.post(
221+
() -> {
222+
throw exception;
223+
});
224+
}
214225
}

0 commit comments

Comments
 (0)