Skip to content

Fail Firestore client if we can't establish SSL connection #508

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jun 19, 2019
Merged
3 changes: 2 additions & 1 deletion firebase-firestore/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
- [feature] Added `clearPersistence()`, which clears the persistent storage
including pending writes and cached documents. This is intended to help
write reliable tests (https://github.com/firebase/firebase-js-sdk/issues/449).

- [changed] Firestore now provides a clear error message when it fails to load
SSL Ciphers during a connection attempt.

# 19.0.1
- [fixed] Fixed an issue that prevented schema migrations for clients with
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.net.ssl.SSLHandshakeException;

/**
* Datastore represents a proxy for the remote server, hiding details of the RPC layer. It:
Expand All @@ -54,6 +55,16 @@
*/
public class Datastore {

/**
* Error message to surface when Firestore fails to establish an SSL connection. A failed SSL
* connection likely indicates that the developer needs to provide an updated OpenSSL stack as
* part of their app's dependencies.
*/
static final String SSL_DEPENDENCY_ERROR_MESSAGE =
"Could not establish a secure connection to the Firestore backend. This is likely a problem "
+ "with your app, rather than with Firestore itself. See https://bit.ly/2XFpdma for "
+ "instructions on how to enable TLS on Android 4.x devices.";

/** Set of lowercase, white-listed headers for logging purposes. */
static final Set<String> WHITE_LISTED_HEADERS =
new HashSet<>(
Expand Down Expand Up @@ -208,6 +219,17 @@ public static boolean isPermanentError(Status status) {
}
}

/**
* Determine whether the given status maps to the error that GRPC-Java throws when an Android
* device is missing required SSL Ciphers.
*
* <p>This error is non-recoverable and must be addressed by the app developer.
*/
public static boolean isSslHandshakeError(Status status) {
return status.getCode().equals(Status.Code.UNAVAILABLE)
&& status.getCause() instanceof SSLHandshakeException;
}

/**
* Determines whether the given status has an error code that represents a permanent error when
* received in response to a write operation.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ public void onClose(Status status, Metadata trailers) {
if (status.isOk()) {
tcs.setResult(results);
} else {
tcs.setException(Util.exceptionFromStatus(status));
tcs.setException(exceptionFromStatus(status));
}
}
},
Expand Down Expand Up @@ -244,7 +244,7 @@ public void onClose(Status status, Metadata trailers) {
Code.INTERNAL));
}
} else {
tcs.setException(Util.exceptionFromStatus(status));
tcs.setException(exceptionFromStatus(status));
}
}
},
Expand All @@ -262,6 +262,17 @@ public void onClose(Status status, Metadata trailers) {
return tcs.getTask();
}

private FirebaseFirestoreException exceptionFromStatus(Status status) {
if (Datastore.isSslHandshakeError(status)) {
return new FirebaseFirestoreException(
Datastore.SSL_DEPENDENCY_ERROR_MESSAGE,
Code.fromValue(status.getCode().value()),
status.getCause());
}

return Util.exceptionFromStatus(status);
}

public void invalidateToken() {
credentialsProvider.invalidateToken();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

package com.google.firebase.firestore.remote;

import static com.google.firebase.firestore.remote.Datastore.SSL_DEPENDENCY_ERROR_MESSAGE;
import static com.google.firebase.firestore.util.Assert.hardAssert;

import android.support.annotation.Nullable;
Expand Down Expand Up @@ -464,6 +465,10 @@ private void handleWatchStreamClose(Status status) {
!shouldStartWatchStream(), "Watch stream was stopped gracefully while still needed.");
}

if (Datastore.isSslHandshakeError(status)) {
throw new IllegalStateException(SSL_DEPENDENCY_ERROR_MESSAGE, status.getCause());
}

cleanUpWatchStreamState();

// If we still need the watch stream, retry the connection.
Expand Down Expand Up @@ -677,6 +682,11 @@ private void handleWriteStreamClose(Status status) {

private void handleWriteHandshakeError(Status status) {
hardAssert(!status.isOk(), "Handling write error with status OK.");

if (Datastore.isSslHandshakeError(status)) {
throw new IllegalStateException(SSL_DEPENDENCY_ERROR_MESSAGE, status.getCause());
}

// Reset the token if it's a permanent error, signaling the write stream is no longer valid.
// Note that the handshake does not count as a write: see comments on isPermanentWriteError for
// details.
Expand Down