Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.google.firebase.database.connection.PersistentConnectionImpl Maven / Gradle / Ivy
/*
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.firebase.database.connection;
import static com.google.firebase.database.connection.ConnectionUtils.hardAssert;
import com.google.firebase.database.connection.util.RetryHelper;
import com.google.firebase.database.util.GAuthToken;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PersistentConnectionImpl implements Connection.Delegate, PersistentConnection {
private static final String REQUEST_ERROR = "error";
private static final String REQUEST_QUERIES = "q";
private static final String REQUEST_TAG = "t";
private static final String REQUEST_STATUS = "s";
private static final String REQUEST_PATH = "p";
private static final String REQUEST_NUMBER = "r";
private static final String REQUEST_PAYLOAD = "b";
private static final String REQUEST_COUNTERS = "c";
private static final String REQUEST_DATA_PAYLOAD = "d";
private static final String REQUEST_DATA_HASH = "h";
private static final String REQUEST_COMPOUND_HASH = "ch";
private static final String REQUEST_COMPOUND_HASH_PATHS = "ps";
private static final String REQUEST_COMPOUND_HASH_HASHES = "hs";
private static final String REQUEST_CREDENTIAL = "cred";
private static final String REQUEST_AUTHVAR = "authvar";
private static final String REQUEST_ACTION = "a";
private static final String REQUEST_ACTION_STATS = "s";
private static final String REQUEST_ACTION_QUERY = "q";
private static final String REQUEST_ACTION_PUT = "p";
private static final String REQUEST_ACTION_MERGE = "m";
private static final String REQUEST_ACTION_QUERY_UNLISTEN = "n";
private static final String REQUEST_ACTION_ONDISCONNECT_PUT = "o";
private static final String REQUEST_ACTION_ONDISCONNECT_MERGE = "om";
private static final String REQUEST_ACTION_ONDISCONNECT_CANCEL = "oc";
private static final String REQUEST_ACTION_AUTH = "auth";
private static final String REQUEST_ACTION_GAUTH = "gauth";
private static final String REQUEST_ACTION_UNAUTH = "unauth";
private static final String REQUEST_NOAUTH = "noauth";
private static final String RESPONSE_FOR_REQUEST = "b";
private static final String SERVER_ASYNC_ACTION = "a";
private static final String SERVER_ASYNC_PAYLOAD = "b";
private static final String SERVER_ASYNC_DATA_UPDATE = "d";
private static final String SERVER_ASYNC_DATA_MERGE = "m";
private static final String SERVER_ASYNC_DATA_RANGE_MERGE = "rm";
private static final String SERVER_ASYNC_AUTH_REVOKED = "ac";
private static final String SERVER_ASYNC_LISTEN_CANCELLED = "c";
private static final String SERVER_ASYNC_SECURITY_DEBUG = "sd";
private static final String SERVER_DATA_UPDATE_PATH = "p";
private static final String SERVER_DATA_UPDATE_BODY = "d";
private static final String SERVER_DATA_START_PATH = "s";
private static final String SERVER_DATA_END_PATH = "e";
private static final String SERVER_DATA_RANGE_MERGE = "m";
private static final String SERVER_DATA_TAG = "t";
private static final String SERVER_DATA_WARNINGS = "w";
private static final String SERVER_RESPONSE_DATA = "d";
/** Delay after which a established connection is considered successful. */
private static final long SUCCESSFUL_CONNECTION_ESTABLISHED_DELAY = 30 * 1000;
private static final long IDLE_TIMEOUT = 60 * 1000;
/** If auth fails repeatedly, we'll assume something is wrong and log a warning / back off. */
private static final long INVALID_AUTH_TOKEN_THRESHOLD = 3;
private static final String SERVER_KILL_INTERRUPT_REASON = "server_kill";
private static final String IDLE_INTERRUPT_REASON = "connection_idle";
private static final String TOKEN_REFRESH_INTERRUPT_REASON = "token_refresh";
private static final Logger logger = LoggerFactory.getLogger(PersistentConnection.class);
private static long connectionIds = 0;
private final Delegate delegate;
private final HostInfo hostInfo;
private final ConnectionContext context;
private final ConnectionFactory connFactory;
private final ConnectionAuthTokenProvider authTokenProvider;
private final ScheduledExecutorService executorService;
private final RetryHelper retryHelper;
private final String label;
private String cachedHost;
private HashSet interruptReasons = new HashSet<>();
private boolean firstConnection = true;
private long lastConnectionEstablishedTime;
private Connection realtime;
private ConnectionState connectionState = ConnectionState.Disconnected;
private long writeCounter = 0;
private long requestCounter = 0;
private Map requestCBHash;
private List onDisconnectRequestQueue;
private Map outstandingPuts;
private Map listens;
private String authToken;
private boolean forceAuthTokenRefresh;
private String lastSessionId;
/** Counter to check whether the callback is for the last getToken call. */
private long currentGetTokenAttempt = 0;
private int invalidAuthTokenCount = 0;
private ScheduledFuture> inactivityTimer = null;
private long lastWriteTimestamp;
private boolean hasOnDisconnects;
public PersistentConnectionImpl(ConnectionContext context, HostInfo info, Delegate delegate) {
this(context, info, delegate, new DefaultConnectionFactory());
}
PersistentConnectionImpl(
ConnectionContext context, HostInfo info, Delegate delegate, ConnectionFactory connFactory) {
this.context = context;
this.hostInfo = info;
this.delegate = delegate;
this.connFactory = connFactory;
this.executorService = context.getExecutorService();
this.authTokenProvider = context.getAuthTokenProvider();
this.listens = new HashMap<>();
this.requestCBHash = new HashMap<>();
this.outstandingPuts = new HashMap<>();
this.onDisconnectRequestQueue = new ArrayList<>();
this.retryHelper =
new RetryHelper.Builder(this.executorService, RetryHelper.class)
.withMinDelayAfterFailure(1000)
.withRetryExponent(1.3)
.withMaxDelay(30 * 1000)
.withJitterFactor(0.7)
.build();
long connId = connectionIds++;
this.label = "[pc_" + connId + "]";
this.lastSessionId = null;
doIdleCheck();
}
// Connection.Delegate methods
@Override
public void onReady(long timestamp, String sessionId) {
logger.debug("{} onReady", label);
lastConnectionEstablishedTime = System.currentTimeMillis();
handleTimestamp(timestamp);
if (this.firstConnection) {
sendConnectStats();
}
restoreAuth();
this.firstConnection = false;
this.lastSessionId = sessionId;
delegate.onConnect();
}
@Override
public void onCacheHost(String host) {
this.cachedHost = host;
}
@Override
public void listen(
List path,
Map queryParams,
ListenHashProvider currentHashFn,
Long tag,
RequestResultCallback listener) {
ListenQuerySpec query = new ListenQuerySpec(path, queryParams);
logger.debug("{} Listening on {}", label, query);
// TODO: Fix this somehow?
//hardAssert(query.isDefault() || !query.loadsAllData(), "listen() called for non-default but "
// + "complete query");
hardAssert(!listens.containsKey(query), "listen() called twice for same QuerySpec.");
logger.debug("{} Adding listen query: {}", label, query);
OutstandingListen outstandingListen =
new OutstandingListen(listener, query, tag, currentHashFn);
listens.put(query, outstandingListen);
if (connected()) {
sendListen(outstandingListen);
}
doIdleCheck();
}
@Override
public void initialize() {
this.tryScheduleReconnect();
}
@Override
public void shutdown() {
this.interrupt("shutdown");
}
@Override
public void put(List path, Object data, RequestResultCallback onComplete) {
putInternal(REQUEST_ACTION_PUT, path, data, /*hash=*/ null, onComplete);
}
@Override
public void compareAndPut(
List path, Object data, String hash, RequestResultCallback onComplete) {
putInternal(REQUEST_ACTION_PUT, path, data, hash, onComplete);
}
@Override
public void merge(List path, Map data, RequestResultCallback onComplete) {
putInternal(REQUEST_ACTION_MERGE, path, data, /*hash=*/ null, onComplete);
}
@Override
public void purgeOutstandingWrites() {
for (OutstandingPut put : this.outstandingPuts.values()) {
if (put.onComplete != null) {
put.onComplete.onRequestResult("write_canceled", null);
}
}
for (OutstandingDisconnect onDisconnect : this.onDisconnectRequestQueue) {
if (onDisconnect.onComplete != null) {
onDisconnect.onComplete.onRequestResult("write_canceled", null);
}
}
this.outstandingPuts.clear();
this.onDisconnectRequestQueue.clear();
// Only if we are not connected can we reliably determine that we don't have onDisconnects
// (outstanding) anymore. Otherwise we leave the flag untouched.
if (!connected()) {
this.hasOnDisconnects = false;
}
doIdleCheck();
}
@Override
public void onDataMessage(Map message) {
if (message.containsKey(REQUEST_NUMBER)) {
// this is a response to a request we sent
// TODO: this is a hack. Make the json parser give us a Long
long rn = (Integer) message.get(REQUEST_NUMBER);
ConnectionRequestCallback responseListener = requestCBHash.remove(rn);
if (responseListener != null) {
// jackson gives up Map for json objects
@SuppressWarnings("unchecked")
Map response = (Map) message.get(RESPONSE_FOR_REQUEST);
responseListener.onResponse(response);
}
} else if (message.containsKey(REQUEST_ERROR)) {
// TODO: log the error? probably shouldn't throw here...
} else if (message.containsKey(SERVER_ASYNC_ACTION)) {
String action = (String) message.get(SERVER_ASYNC_ACTION);
// jackson gives up Map for json objects
@SuppressWarnings("unchecked")
Map body = (Map) message.get(SERVER_ASYNC_PAYLOAD);
onDataPush(action, body);
} else {
logger.debug("{} Ignoring unknown message: {}", label, message);
}
}
@Override
public void onDisconnect(Connection.DisconnectReason reason) {
logger.debug("{} Got on disconnect due to {}", label, reason.name());
this.connectionState = ConnectionState.Disconnected;
this.realtime = null;
this.hasOnDisconnects = false;
requestCBHash.clear();
if (inactivityTimer != null) {
logger.debug("{} Cancelling idle time checker", label);
inactivityTimer.cancel(false);
inactivityTimer = null;
}
cancelSentTransactions();
if (shouldReconnect()) {
long timeSinceLastConnectSucceeded =
System.currentTimeMillis() - lastConnectionEstablishedTime;
boolean lastConnectionWasSuccessful;
if (lastConnectionEstablishedTime > 0) {
lastConnectionWasSuccessful =
timeSinceLastConnectSucceeded > SUCCESSFUL_CONNECTION_ESTABLISHED_DELAY;
} else {
lastConnectionWasSuccessful = false;
}
if (reason == Connection.DisconnectReason.SERVER_RESET || lastConnectionWasSuccessful) {
retryHelper.signalSuccess();
}
tryScheduleReconnect();
}
lastConnectionEstablishedTime = 0;
delegate.onDisconnect();
}
@Override
public void onKill(String reason) {
logger.debug(
"{} Firebase Database connection was forcefully killed by the server. Will not attempt "
+ "reconnect. Reason: {}", label, reason);
interrupt(SERVER_KILL_INTERRUPT_REASON);
}
@Override
public void unlisten(List path, Map queryParams) {
ListenQuerySpec query = new ListenQuerySpec(path, queryParams);
logger.debug("{} Unlistening on {}", label, query);
// TODO: fix this by understanding query params?
//Utilities.hardAssert(query.isDefault() || !query.loadsAllData(),
// "unlisten() called for non-default but complete query");
OutstandingListen listen = removeListen(query);
if (listen != null && connected()) {
sendUnlisten(listen);
}
doIdleCheck();
}
private boolean connected() {
return connectionState == ConnectionState.Authenticating
|| connectionState == ConnectionState.Connected;
}
@Override
public void onDisconnectPut(List path, Object data, RequestResultCallback onComplete) {
this.hasOnDisconnects = true;
if (canSendWrites()) {
sendOnDisconnect(REQUEST_ACTION_ONDISCONNECT_PUT, path, data, onComplete);
} else {
onDisconnectRequestQueue.add(
new OutstandingDisconnect(REQUEST_ACTION_ONDISCONNECT_PUT, path, data, onComplete));
}
doIdleCheck();
}
private boolean canSendWrites() {
return connectionState == ConnectionState.Connected;
}
@Override
public void onDisconnectMerge(
List path, Map updates, final RequestResultCallback onComplete) {
this.hasOnDisconnects = true;
if (canSendWrites()) {
sendOnDisconnect(REQUEST_ACTION_ONDISCONNECT_MERGE, path, updates, onComplete);
} else {
onDisconnectRequestQueue.add(
new OutstandingDisconnect(REQUEST_ACTION_ONDISCONNECT_MERGE, path, updates, onComplete));
}
doIdleCheck();
}
@Override
public void onDisconnectCancel(List path, RequestResultCallback onComplete) {
// We do not mark hasOnDisconnects true here, because we only are removing disconnects.
// However, we can also not reliably determine whether we had onDisconnects, so we can't
// and do not reset the flag.
if (canSendWrites()) {
sendOnDisconnect(REQUEST_ACTION_ONDISCONNECT_CANCEL, path, null, onComplete);
} else {
onDisconnectRequestQueue.add(
new OutstandingDisconnect(REQUEST_ACTION_ONDISCONNECT_CANCEL, path, null, onComplete));
}
doIdleCheck();
}
@Override
public void interrupt(String reason) {
logger.debug("{} Connection interrupted for: {}", label, reason);
interruptReasons.add(reason);
if (realtime != null) {
// Will call onDisconnect and set the connection state to Disconnected
realtime.close();
realtime = null;
} else {
retryHelper.cancel();
this.connectionState = ConnectionState.Disconnected;
}
// Reset timeouts
retryHelper.signalSuccess();
}
@Override
public void resume(String reason) {
logger.debug("{} Connection no longer interrupted for: {}", label, reason);
interruptReasons.remove(reason);
if (shouldReconnect() && connectionState == ConnectionState.Disconnected) {
tryScheduleReconnect();
}
}
@Override
public boolean isInterrupted(String reason) {
return interruptReasons.contains(reason);
}
private boolean shouldReconnect() {
return interruptReasons.size() == 0;
}
@Override
public void refreshAuthToken() {
// Old versions of the database client library didn't have synchronous access to the
// new token and call this instead of the overload that includes the new token.
// After a refresh token any subsequent operations are expected to have the authentication
// status at the point of this call. To avoid race conditions with delays after getToken,
// we close the connection to make sure any writes/listens are queued until the connection
// is reauthed with the current token after reconnecting. Note that this will trigger
// onDisconnects which isn't ideal.
logger.debug("{} Auth token refresh requested", label);
// By using interrupt instead of closing the connection we make sure there are no race
// conditions with other fetch token attempts (interrupt/resume is expected to handle those
// correctly)
interrupt(TOKEN_REFRESH_INTERRUPT_REASON);
resume(TOKEN_REFRESH_INTERRUPT_REASON);
}
@Override
public void refreshAuthToken(String token) {
logger.debug("{} Auth token refreshed.", label);
this.authToken = token;
if (connected()) {
if (token != null) {
upgradeAuth();
} else {
sendUnauth();
}
}
}
private void tryScheduleReconnect() {
if (shouldReconnect()) {
hardAssert(
this.connectionState == ConnectionState.Disconnected,
"Not in disconnected state: %s",
this.connectionState);
final boolean forceRefresh = this.forceAuthTokenRefresh;
logger.debug("{} Scheduling connection attempt", label);
this.forceAuthTokenRefresh = false;
retryHelper.retry(
new Runnable() {
@Override
public void run() {
logger.debug("{} Trying to fetch auth token", label);
hardAssert(
connectionState == ConnectionState.Disconnected,
"Not in disconnected state: %s",
connectionState);
connectionState = ConnectionState.GettingToken;
currentGetTokenAttempt++;
final long thisGetTokenAttempt = currentGetTokenAttempt;
authTokenProvider.getToken(
forceRefresh,
new ConnectionAuthTokenProvider.GetTokenCallback() {
@Override
public void onSuccess(String token) {
if (thisGetTokenAttempt == currentGetTokenAttempt) {
// Someone could have interrupted us while fetching the token,
// marking the connection as Disconnected
if (connectionState == ConnectionState.GettingToken) {
logger.debug("{} Successfully fetched token, opening connection", label);
openNetworkConnection(token);
} else {
hardAssert(
connectionState == ConnectionState.Disconnected,
"Expected connection state disconnected, but was %s",
connectionState);
logger.debug(
"{} Not opening connection after token refresh, because connection "
+ "was set to disconnected", label);
}
} else {
logger.debug(
"{} Ignoring getToken result, because this was not the "
+ "latest attempt.", label);
}
}
@Override
public void onError(String error) {
if (thisGetTokenAttempt == currentGetTokenAttempt) {
connectionState = ConnectionState.Disconnected;
logger.debug("{} Error fetching token: {}", label, error);
tryScheduleReconnect();
} else {
logger.debug(
"{} Ignoring getToken error, because this was not the "
+ "latest attempt.", label);
}
}
});
}
});
}
}
private void openNetworkConnection(String token) {
hardAssert(
connectionState == ConnectionState.GettingToken,
"Trying to open network connection while in the wrong state: %s",
connectionState);
// User might have logged out. Positive auth status is handled after authenticating with
// the server
if (token == null) {
delegate.onAuthStatus(false);
}
authToken = token;
connectionState = ConnectionState.Connecting;
realtime = connFactory.newConnection(this);
realtime.open();
}
private void sendOnDisconnect(
String action, List path, Object data, final RequestResultCallback onComplete) {
Map request = new HashMap<>();
request.put(REQUEST_PATH, ConnectionUtils.pathToString(path));
request.put(REQUEST_DATA_PAYLOAD, data);
//
//if (logger.logsDebug()) logger.debug("onDisconnect " + action + " " + request);
sendAction(
action,
request,
new ConnectionRequestCallback() {
@Override
public void onResponse(Map response) {
String status = (String) response.get(REQUEST_STATUS);
String errorMessage = null;
String errorCode = null;
if (!status.equals("ok")) {
errorCode = status;
errorMessage = (String) response.get(SERVER_DATA_UPDATE_BODY);
}
if (onComplete != null) {
onComplete.onRequestResult(errorCode, errorMessage);
}
}
});
}
private void cancelSentTransactions() {
List cancelledTransactionWrites = new ArrayList<>();
Iterator> iter = outstandingPuts.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = iter.next();
OutstandingPut put = entry.getValue();
if (put.getRequest().containsKey(REQUEST_DATA_HASH) && put.wasSent()) {
cancelledTransactionWrites.add(put);
iter.remove();
}
}
for (OutstandingPut put : cancelledTransactionWrites) {
// onRequestResult() may invoke rerunTransactions() and enqueue new writes. We defer calling
// it until we've finished enumerating all existing writes.
put.getOnComplete().onRequestResult("disconnected", null);
}
}
private void sendUnlisten(OutstandingListen listen) {
Map request = new HashMap<>();
request.put(REQUEST_PATH, ConnectionUtils.pathToString(listen.query.path));
Long tag = listen.getTag();
if (tag != null) {
request.put(REQUEST_QUERIES, listen.getQuery().queryParams);
request.put(REQUEST_TAG, tag);
}
sendAction(REQUEST_ACTION_QUERY_UNLISTEN, request, null);
}
private OutstandingListen removeListen(ListenQuerySpec query) {
logger.debug("{} removing query {}", label, query);
if (!listens.containsKey(query)) {
logger.debug(
"{} Trying to remove listener for QuerySpec {} but no listener exists.", label, query);
return null;
} else {
OutstandingListen oldListen = listens.get(query);
listens.remove(query);
doIdleCheck();
return oldListen;
}
}
private Collection removeListens(List path) {
logger.debug("{} Removing all listens at path {}", label, path);
List removedListens = new ArrayList<>();
for (Map.Entry entry : listens.entrySet()) {
ListenQuerySpec query = entry.getKey();
OutstandingListen listen = entry.getValue();
if (query.path.equals(path)) {
removedListens.add(listen);
}
}
for (OutstandingListen toRemove : removedListens) {
listens.remove(toRemove.getQuery());
}
doIdleCheck();
return removedListens;
}
private void onDataPush(String action, Map body) {
logger.debug("{} handleServerMessage: {} {}", label, action, body);
if (action.equals(SERVER_ASYNC_DATA_UPDATE) || action.equals(SERVER_ASYNC_DATA_MERGE)) {
boolean isMerge = action.equals(SERVER_ASYNC_DATA_MERGE);
String pathString = (String) body.get(SERVER_DATA_UPDATE_PATH);
Object payloadData = body.get(SERVER_DATA_UPDATE_BODY);
Long tagNumber = ConnectionUtils.longFromObject(body.get(SERVER_DATA_TAG));
// ignore empty merges
if (isMerge && (payloadData instanceof Map) && ((Map) payloadData).size() == 0) {
logger.debug("{} Ignoring empty merge for path {}", label, pathString);
} else {
List path = ConnectionUtils.stringToPath(pathString);
delegate.onDataUpdate(path, payloadData, isMerge, tagNumber);
}
} else if (action.equals(SERVER_ASYNC_DATA_RANGE_MERGE)) {
String pathString = (String) body.get(SERVER_DATA_UPDATE_PATH);
List path = ConnectionUtils.stringToPath(pathString);
Object payloadData = body.get(SERVER_DATA_UPDATE_BODY);
Long tag = ConnectionUtils.longFromObject(body.get(SERVER_DATA_TAG));
@SuppressWarnings("unchecked")
List> ranges = (List>) payloadData;
List rangeMerges = new ArrayList<>();
for (Map range : ranges) {
String startString = (String) range.get(SERVER_DATA_START_PATH);
String endString = (String) range.get(SERVER_DATA_END_PATH);
List start = startString != null ? ConnectionUtils.stringToPath(startString) : null;
List end = endString != null ? ConnectionUtils.stringToPath(endString) : null;
Object update = range.get(SERVER_DATA_RANGE_MERGE);
rangeMerges.add(new RangeMerge(start, end, update));
}
if (rangeMerges.isEmpty()) {
logger.debug("{} Ignoring empty range merge for path {}", label, pathString);
} else {
this.delegate.onRangeMergeUpdate(path, rangeMerges, tag);
}
} else if (action.equals(SERVER_ASYNC_LISTEN_CANCELLED)) {
String pathString = (String) body.get(SERVER_DATA_UPDATE_PATH);
List path = ConnectionUtils.stringToPath(pathString);
onListenRevoked(path);
} else if (action.equals(SERVER_ASYNC_AUTH_REVOKED)) {
String status = (String) body.get(REQUEST_STATUS);
String reason = (String) body.get(SERVER_DATA_UPDATE_BODY);
onAuthRevoked(status, reason);
} else if (action.equals(SERVER_ASYNC_SECURITY_DEBUG)) {
onSecurityDebugPacket(body);
} else {
logger.debug("{} Unrecognized action from server: {}", label, action);
}
}
private void onListenRevoked(List path) {
// Remove the listen and manufacture a "permission denied" error for the failed listen
Collection listens = removeListens(path);
// The listen may have already been removed locally. If so, skip it
if (listens != null) {
for (OutstandingListen listen : listens) {
listen.resultCallback.onRequestResult("permission_denied", null);
}
}
}
private void onAuthRevoked(String errorCode, String errorMessage) {
// This might be for an earlier token than we just recently sent. But since we need to close
// the connection anyways, we can set it to null here and we will refresh the token later
// on reconnect.
logger.debug("{} Auth token revoked: {} ({})", label, errorCode, errorMessage);
this.authToken = null;
this.forceAuthTokenRefresh = true;
this.delegate.onAuthStatus(false);
// Close connection and reconnect
this.realtime.close();
}
private void onSecurityDebugPacket(Map message) {
// TODO: implement on iOS too
logger.info("{} {}", label, message.get("msg"));
}
private void upgradeAuth() {
sendAuthHelper(/*restoreStateAfterComplete=*/ false);
}
private void sendAuthAndRestoreState() {
sendAuthHelper(/*restoreStateAfterComplete=*/ true);
}
private void sendAuthHelper(final boolean restoreStateAfterComplete) {
hardAssert(connected(), "Must be connected to send auth, but was: %s", this.connectionState);
hardAssert(this.authToken != null, "Auth token must be set to authenticate!");
ConnectionRequestCallback onComplete =
new ConnectionRequestCallback() {
@Override
public void onResponse(Map response) {
connectionState = ConnectionState.Connected;
String status = (String) response.get(REQUEST_STATUS);
if (status.equals("ok")) {
invalidAuthTokenCount = 0;
delegate.onAuthStatus(true);
if (restoreStateAfterComplete) {
restoreState();
}
} else {
authToken = null;
forceAuthTokenRefresh = true;
delegate.onAuthStatus(false);
String reason = (String) response.get(SERVER_RESPONSE_DATA);
logger.debug("{} Authentication failed: {} ({})", label, status, reason);
realtime.close();
if (status.equals("invalid_token") || status.equals("permission_denied")) {
// We'll wait a couple times before logging the warning / increasing the
// retry period since oauth tokens will report as "invalid" if they're
// just expired. Plus there may be transient issues that resolve themselves.
invalidAuthTokenCount++;
if (invalidAuthTokenCount >= INVALID_AUTH_TOKEN_THRESHOLD) {
// Set a long reconnect delay because recovery is unlikely.
retryHelper.setMaxDelay();
logger.warn(
"{} Provided authentication credentials are invalid. This "
+ "usually indicates your FirebaseApp instance was not initialized "
+ "correctly. Make sure your database URL is correct and that your "
+ "service account is for the correct project and is authorized to "
+ "access it.", label);
}
}
}
}
};
Map request = new HashMap<>();
GAuthToken googleAuthToken = GAuthToken.tryParseFromString(this.authToken);
if (googleAuthToken != null) {
request.put(REQUEST_CREDENTIAL, googleAuthToken.getToken());
if (googleAuthToken.getAuth() != null) {
if (!googleAuthToken.getAuth().isEmpty()) {
request.put(REQUEST_AUTHVAR, googleAuthToken.getAuth());
}
} else {
request.put(REQUEST_NOAUTH, true);
}
sendSensitive(REQUEST_ACTION_GAUTH, /*isSensitive=*/ true, request, onComplete);
} else {
request.put(REQUEST_CREDENTIAL, authToken);
sendSensitive(REQUEST_ACTION_AUTH, /*isSensitive=*/ true, request, onComplete);
}
}
private void sendUnauth() {
hardAssert(connected(), "Must be connected to send unauth.");
hardAssert(authToken == null, "Auth token must not be set.");
sendAction(REQUEST_ACTION_UNAUTH, Collections.emptyMap(), null);
}
private void restoreAuth() {
logger.debug("{} Calling restore state", label);
hardAssert(
this.connectionState == ConnectionState.Connecting,
"Wanted to restore auth, but was in wrong state: %s",
this.connectionState);
if (authToken == null) {
logger.debug("{} Not restoring auth because token is null.", label);
this.connectionState = ConnectionState.Connected;
restoreState();
} else {
logger.debug("{} Restoring auth.", label);
this.connectionState = ConnectionState.Authenticating;
sendAuthAndRestoreState();
}
}
private void restoreState() {
hardAssert(
this.connectionState == ConnectionState.Connected,
"Should be connected if we're restoring state, but we are: %s",
this.connectionState);
// Restore listens
logger.debug("{} Restoring outstanding listens", label);
for (OutstandingListen listen : listens.values()) {
logger.debug("{} Restoring listen {}", label, listen.getQuery());
sendListen(listen);
}
logger.debug("{} Restoring writes.", label);
// Restore puts
ArrayList outstanding = new ArrayList<>(outstandingPuts.keySet());
// Make sure puts are restored in order
Collections.sort(outstanding);
for (Long put : outstanding) {
sendPut(put);
}
// Restore disconnect operations
for (OutstandingDisconnect disconnect : onDisconnectRequestQueue) {
sendOnDisconnect(
disconnect.getAction(),
disconnect.getPath(),
disconnect.getData(),
disconnect.getOnComplete());
}
onDisconnectRequestQueue.clear();
}
private void handleTimestamp(long timestamp) {
logger.debug("{} Handling timestamp", label);
long timestampDelta = timestamp - System.currentTimeMillis();
Map updates = new HashMap<>();
updates.put(Constants.DOT_INFO_SERVERTIME_OFFSET, timestampDelta);
delegate.onServerInfoUpdate(updates);
}
private Map getPutObject(List path, Object data, String hash) {
Map request = new HashMap<>();
request.put(REQUEST_PATH, ConnectionUtils.pathToString(path));
request.put(REQUEST_DATA_PAYLOAD, data);
if (hash != null) {
request.put(REQUEST_DATA_HASH, hash);
}
return request;
}
private void putInternal(
String action,
List path,
Object data,
String hash,
RequestResultCallback onComplete) {
Map request = getPutObject(path, data, hash);
// local to PersistentConnection
long writeId = this.writeCounter++;
outstandingPuts.put(writeId, new OutstandingPut(action, request, onComplete));
if (canSendWrites()) {
sendPut(writeId);
}
this.lastWriteTimestamp = System.currentTimeMillis();
doIdleCheck();
}
private void sendPut(final long putId) {
assert canSendWrites()
: "sendPut called when we can't send writes (we're disconnected or writes are paused).";
final OutstandingPut put = outstandingPuts.get(putId);
final RequestResultCallback onComplete = put.getOnComplete();
final String action = put.getAction();
put.markSent();
sendAction(
action,
put.getRequest(),
new ConnectionRequestCallback() {
@Override
public void onResponse(Map response) {
logger.debug("{} {} response: {}", label, action, response);
OutstandingPut currentPut = outstandingPuts.get(putId);
if (currentPut == put) {
outstandingPuts.remove(putId);
if (onComplete != null) {
String status = (String) response.get(REQUEST_STATUS);
if (status.equals("ok")) {
onComplete.onRequestResult(null, null);
} else {
String errorMessage = (String) response.get(SERVER_DATA_UPDATE_BODY);
onComplete.onRequestResult(status, errorMessage);
}
}
} else {
logger.debug("{} Ignoring on complete for put {} because it was removed already.",
label, putId);
}
doIdleCheck();
}
});
}
private void sendListen(final OutstandingListen listen) {
Map request = new HashMap<>();
request.put(REQUEST_PATH, ConnectionUtils.pathToString(listen.getQuery().path));
Long tag = listen.getTag();
// Only bother to send query if it's non-default
if (tag != null) {
request.put(REQUEST_QUERIES, listen.query.queryParams);
request.put(REQUEST_TAG, tag);
}
ListenHashProvider hashFunction = listen.getHashFunction();
request.put(REQUEST_DATA_HASH, hashFunction.getSimpleHash());
if (hashFunction.shouldIncludeCompoundHash()) {
CompoundHash compoundHash = hashFunction.getCompoundHash();
List posts = new ArrayList<>();
for (List path : compoundHash.getPosts()) {
posts.add(ConnectionUtils.pathToString(path));
}
Map hash = new HashMap<>();
hash.put(REQUEST_COMPOUND_HASH_HASHES, compoundHash.getHashes());
hash.put(REQUEST_COMPOUND_HASH_PATHS, posts);
request.put(REQUEST_COMPOUND_HASH, hash);
}
sendAction(
REQUEST_ACTION_QUERY,
request,
new ConnectionRequestCallback() {
@Override
public void onResponse(Map response) {
String status = (String) response.get(REQUEST_STATUS);
// log warnings in any case, even if listener was already removed
if (status.equals("ok")) {
@SuppressWarnings("unchecked")
Map serverBody =
(Map) response.get(SERVER_DATA_UPDATE_BODY);
if (serverBody.containsKey(SERVER_DATA_WARNINGS)) {
@SuppressWarnings("unchecked")
List warnings = (List) serverBody.get(SERVER_DATA_WARNINGS);
warnOnListenerWarnings(warnings, listen.query);
}
}
OutstandingListen currentListen = listens.get(listen.getQuery());
// only trigger actions if the listen hasn't been removed (and maybe readded)
if (currentListen == listen) {
if (!status.equals("ok")) {
removeListen(listen.getQuery());
String errorMessage = (String) response.get(SERVER_DATA_UPDATE_BODY);
listen.resultCallback.onRequestResult(status, errorMessage);
} else {
listen.resultCallback.onRequestResult(null, null);
}
}
}
});
}
private void sendStats(final Map stats) {
if (!stats.isEmpty()) {
Map request = new HashMap<>();
request.put(REQUEST_COUNTERS, stats);
sendAction(
REQUEST_ACTION_STATS,
request,
new ConnectionRequestCallback() {
@Override
public void onResponse(Map response) {
String status = (String) response.get(REQUEST_STATUS);
if (!status.equals("ok")) {
String errorMessage = (String) response.get(SERVER_DATA_UPDATE_BODY);
logger.debug(
"{} Failed to send stats: {} (message: {})", label, stats, errorMessage);
}
}
});
} else {
logger.debug("{} Not sending stats because stats are empty", label);
}
}
@SuppressWarnings("unchecked")
private void warnOnListenerWarnings(List warnings, ListenQuerySpec query) {
if (warnings.contains("no_index")) {
String indexSpec = "\".indexOn\": \"" + query.queryParams.get("i") + '\"';
logger.warn(
"{} Using an unspecified index. Consider adding '{}' at {} to your security and "
+ "Firebase Database rules for better performance",
label, indexSpec, ConnectionUtils.pathToString(query.path));
}
}
private void sendConnectStats() {
Map stats = new HashMap<>();
assert !this.context.isPersistenceEnabled()
: "Stats for persistence on JVM missing (persistence not yet supported)";
stats.put("sdk.admin_java." + context.getClientSdkVersion().replace('.', '-'), 1);
logger.debug("{} Sending first connection stats", label);
sendStats(stats);
}
private void sendAction(
String action, Map message, ConnectionRequestCallback onResponse) {
sendSensitive(action, /*isSensitive=*/ false, message, onResponse);
}
private void sendSensitive(
String action,
boolean isSensitive,
Map message,
ConnectionRequestCallback onResponse) {
long rn = nextRequestNumber();
Map request = new HashMap<>();
request.put(REQUEST_NUMBER, rn);
request.put(REQUEST_ACTION, action);
request.put(REQUEST_PAYLOAD, message);
realtime.sendRequest(request, isSensitive);
requestCBHash.put(rn, onResponse);
}
private long nextRequestNumber() {
return requestCounter++;
}
private void doIdleCheck() {
if (isIdle()) {
if (this.inactivityTimer != null) {
this.inactivityTimer.cancel(false);
}
this.inactivityTimer =
this.executorService.schedule(
new Runnable() {
@Override
public void run() {
inactivityTimer = null;
if (idleHasTimedOut()) {
interrupt(IDLE_INTERRUPT_REASON);
} else {
doIdleCheck();
}
}
},
IDLE_TIMEOUT,
TimeUnit.MILLISECONDS);
} else if (isInterrupted(IDLE_INTERRUPT_REASON)) {
hardAssert(!isIdle());
this.resume(IDLE_INTERRUPT_REASON);
}
}
/**
* @return Returns true if the connection is currently not being used (for listen, outstanding
* operations).
*/
private boolean isIdle() {
return this.listens.isEmpty()
&& this.requestCBHash.isEmpty()
&& !this.hasOnDisconnects
&& this.outstandingPuts.isEmpty();
}
private boolean idleHasTimedOut() {
long now = System.currentTimeMillis();
return isIdle() && now > (this.lastWriteTimestamp + IDLE_TIMEOUT);
}
private enum ConnectionState {
Disconnected,
GettingToken,
Connecting,
Authenticating,
Connected
}
private interface ConnectionRequestCallback {
void onResponse(Map response);
}
private static class ListenQuerySpec {
private final List path;
private final Map queryParams;
public ListenQuerySpec(List path, Map queryParams) {
this.path = path;
this.queryParams = queryParams;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof ListenQuerySpec)) {
return false;
}
ListenQuerySpec that = (ListenQuerySpec) o;
if (!path.equals(that.path)) {
return false;
}
return queryParams.equals(that.queryParams);
}
@Override
public int hashCode() {
int result = path.hashCode();
result = 31 * result + queryParams.hashCode();
return result;
}
@Override
public String toString() {
return ConnectionUtils.pathToString(this.path) + " (params: " + queryParams + ")";
}
}
private static class OutstandingListen {
private final RequestResultCallback resultCallback;
private final ListenQuerySpec query;
private final ListenHashProvider hashFunction;
private final Long tag;
private OutstandingListen(
RequestResultCallback callback,
ListenQuerySpec query,
Long tag,
ListenHashProvider hashFunction) {
this.resultCallback = callback;
this.query = query;
this.hashFunction = hashFunction;
this.tag = tag;
}
ListenQuerySpec getQuery() {
return query;
}
Long getTag() {
return this.tag;
}
ListenHashProvider getHashFunction() {
return this.hashFunction;
}
@Override
public String toString() {
return query.toString() + " (Tag: " + this.tag + ")";
}
}
private static class OutstandingPut {
private String action;
private Map request;
private RequestResultCallback onComplete;
private boolean sent;
private OutstandingPut(
String action, Map request, RequestResultCallback onComplete) {
this.action = action;
this.request = request;
this.onComplete = onComplete;
}
String getAction() {
return action;
}
Map getRequest() {
return request;
}
RequestResultCallback getOnComplete() {
return onComplete;
}
void markSent() {
this.sent = true;
}
boolean wasSent() {
return this.sent;
}
}
private static class OutstandingDisconnect {
private final String action;
private final List path;
private final Object data;
private final RequestResultCallback onComplete;
private OutstandingDisconnect(
String action, List path, Object data, RequestResultCallback onComplete) {
this.action = action;
this.path = path;
this.data = data;
this.onComplete = onComplete;
}
public String getAction() {
return action;
}
public List getPath() {
return path;
}
public Object getData() {
return data;
}
public RequestResultCallback getOnComplete() {
return onComplete;
}
}
interface ConnectionFactory {
Connection newConnection(PersistentConnectionImpl delegate);
}
private static class DefaultConnectionFactory implements ConnectionFactory {
@Override
public Connection newConnection(PersistentConnectionImpl delegate) {
return new Connection(delegate.context, delegate.hostInfo, delegate.cachedHost,
delegate, delegate.lastSessionId);
}
}
}