/* * 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 com.google.firebase.database.logging.LogWrapper; import java.util.HashMap; import java.util.Map; class Connection implements WebsocketConnection.Delegate { private static final String REQUEST_TYPE = "t"; private static final String REQUEST_TYPE_DATA = "d"; private static final String REQUEST_PAYLOAD = "d"; private static final String SERVER_ENVELOPE_TYPE = "t"; private static final String SERVER_DATA_MESSAGE = "d"; private static final String SERVER_CONTROL_MESSAGE = "c"; private static final String SERVER_ENVELOPE_DATA = "d"; private static final String SERVER_CONTROL_MESSAGE_TYPE = "t"; private static final String SERVER_CONTROL_MESSAGE_SHUTDOWN = "s"; private static final String SERVER_CONTROL_MESSAGE_RESET = "r"; private static final String SERVER_CONTROL_MESSAGE_HELLO = "h"; private static final String SERVER_CONTROL_MESSAGE_DATA = "d"; private static final String SERVER_HELLO_TIMESTAMP = "ts"; private static final String SERVER_HELLO_HOST = "h"; private static final String SERVER_HELLO_SESSION_ID = "s"; private static long connectionIds = 0; private final LogWrapper logger; private HostInfo hostInfo; private WebsocketConnection conn; private Delegate delegate; private State state; public Connection( ConnectionContext context, HostInfo hostInfo, String cachedHost, Delegate delegate, String optLastSessionId) { long connId = connectionIds++; this.hostInfo = hostInfo; this.delegate = delegate; this.logger = new LogWrapper(context.getLogger(), "Connection", "conn_" + connId); this.state = State.REALTIME_CONNECTING; this.conn = new WebsocketConnection(context, hostInfo, cachedHost, this, optLastSessionId); } public void open() { if (logger.logsDebug()) { logger.debug("Opening a connection"); } conn.open(); } public void close(DisconnectReason reason) { if (state != State.REALTIME_DISCONNECTED) { if (logger.logsDebug()) { logger.debug("closing realtime connection"); } state = State.REALTIME_DISCONNECTED; if (conn != null) { conn.close(); conn = null; } delegate.onDisconnect(reason); } } public void close() { close(DisconnectReason.OTHER); } public void sendRequest(Map<String, Object> message, boolean isSensitive) { // This came from the persistent connection. Wrap it in an envelope and send it Map<String, Object> request = new HashMap<>(); request.put(REQUEST_TYPE, REQUEST_TYPE_DATA); request.put(REQUEST_PAYLOAD, message); sendData(request, isSensitive); } @Override public void onMessage(Map<String, Object> message) { try { String messageType = (String) message.get(SERVER_ENVELOPE_TYPE); if (messageType != null) { if (messageType.equals(SERVER_DATA_MESSAGE)) { @SuppressWarnings("unchecked") Map<String, Object> data = (Map<String, Object>) message.get(SERVER_ENVELOPE_DATA); onDataMessage(data); } else if (messageType.equals(SERVER_CONTROL_MESSAGE)) { @SuppressWarnings("unchecked") Map<String, Object> data = (Map<String, Object>) message.get(SERVER_ENVELOPE_DATA); onControlMessage(data); } else { if (logger.logsDebug()) { logger.debug("Ignoring unknown server message type: " + messageType); } } } else { if (logger.logsDebug()) { logger.debug( "Failed to parse server message: missing message type:" + message.toString()); } close(); } } catch (ClassCastException e) { if (logger.logsDebug()) { logger.debug("Failed to parse server message: " + e.toString()); } close(); } } @Override public void onDisconnect(boolean wasEverConnected) { conn = null; if (!wasEverConnected && state == State.REALTIME_CONNECTING) { if (logger.logsDebug()) { logger.debug("Realtime connection failed"); } } else { if (logger.logsDebug()) { logger.debug("Realtime connection lost"); } } close(); } private void onDataMessage(Map<String, Object> data) { if (logger.logsDebug()) { logger.debug("received data message: " + data.toString()); } // We don't do anything with data messages, just kick them up a level delegate.onDataMessage(data); } private void onControlMessage(Map<String, Object> data) { if (logger.logsDebug()) { logger.debug("Got control message: " + data.toString()); } try { String messageType = (String) data.get(SERVER_CONTROL_MESSAGE_TYPE); if (messageType != null) { if (messageType.equals(SERVER_CONTROL_MESSAGE_SHUTDOWN)) { String reason = (String) data.get(SERVER_CONTROL_MESSAGE_DATA); onConnectionShutdown(reason); } else if (messageType.equals(SERVER_CONTROL_MESSAGE_RESET)) { String host = (String) data.get(SERVER_CONTROL_MESSAGE_DATA); onReset(host); } else if (messageType.equals(SERVER_CONTROL_MESSAGE_HELLO)) { @SuppressWarnings("unchecked") Map<String, Object> handshakeData = (Map<String, Object>) data.get(SERVER_CONTROL_MESSAGE_DATA); onHandshake(handshakeData); } else { if (logger.logsDebug()) { logger.debug("Ignoring unknown control message: " + messageType); } } } else { if (logger.logsDebug()) { logger.debug("Got invalid control message: " + data.toString()); } close(); } } catch (ClassCastException e) { if (logger.logsDebug()) { logger.debug("Failed to parse control message: " + e.toString()); } close(); } } private void onConnectionShutdown(String reason) { if (logger.logsDebug()) { logger.debug("Connection shutdown command received. Shutting down..."); } delegate.onKill(reason); close(); } private void onHandshake(Map<String, Object> handshake) { long timestamp = (Long) handshake.get(SERVER_HELLO_TIMESTAMP); String host = (String) handshake.get(SERVER_HELLO_HOST); delegate.onCacheHost(host); String sessionId = (String) handshake.get(SERVER_HELLO_SESSION_ID); if (state == State.REALTIME_CONNECTING) { conn.start(); onConnectionReady(timestamp, sessionId); } } private void onConnectionReady(long timestamp, String sessionId) { if (logger.logsDebug()) { logger.debug("realtime connection established"); } state = State.REALTIME_CONNECTED; delegate.onReady(timestamp, sessionId); } private void onReset(String host) { if (logger.logsDebug()) { logger.debug( "Got a reset; killing connection to " + this.hostInfo.getHost() + "; Updating internalHost to " + host); } delegate.onCacheHost(host); // Explicitly close the connection with SERVER_RESET so calling code knows to reconnect // immediately. close(DisconnectReason.SERVER_RESET); } private void sendData(Map<String, Object> data, boolean isSensitive) { if (state != State.REALTIME_CONNECTED) { logger.debug("Tried to send on an unconnected connection"); } else { if (isSensitive) { logger.debug("Sending data (contents hidden)"); } else { logger.debug("Sending data: %s", data); } conn.send(data); } } // For testing public void injectConnectionFailure() { this.close(); } public enum DisconnectReason { SERVER_RESET, OTHER } private enum State { REALTIME_CONNECTING, REALTIME_CONNECTED, REALTIME_DISCONNECTED } public interface Delegate { void onCacheHost(String host); void onReady(long timestamp, String sessionId); void onDataMessage(Map<String, Object> message); void onDisconnect(DisconnectReason reason); void onKill(String reason); } }