/*
* Copyright (C) 2015 Actor LLC. <https://actor.im>
*/
package im.actor.runtime.mtproto;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Random;
import im.actor.runtime.Crypto;
import im.actor.runtime.Log;
import im.actor.runtime.bser.DataInput;
import im.actor.runtime.bser.DataOutput;
import im.actor.runtime.crypto.CRC32;
import im.actor.runtime.threading.CommonTimer;
public class ManagedConnection implements Connection {
public static final int CONNECTION_TIMEOUT = 15 * 1000;
private static final int HANDSHAKE_TIMEOUT = 15 * 1000;
private static final int RESPONSE_TIMEOUT = 15 * 1000;
private static final int PING_TIMEOUT = 5 * 60 * 1000;
private static final int HEADER_PROTO = 0;
private static final int HEADER_PING = 1;
private static final int HEADER_PONG = 2;
private static final int HEADER_DROP = 3;
private static final int HEADER_REDIRECT = 4;
private static final int HEADER_ACK = 6;
private static final int HEADER_HANDSHAKE_REQUEST = 0xFF;
private static final int HEADER_HANDSHAKE_RESPONSE = 0xFE;
private static final Random RANDOM = new Random();
private final AsyncConnectionInterface connectionInterface = new ConnectionInterface();
private final CRC32 CRC32_ENGINE = new CRC32();
private final String TAG;
private final AsyncConnection rawConnection;
private final ConnectionCallback callback;
private final ManagedConnectionCreateCallback factoryCallback;
private final int connectionId;
private final int mtprotoVersion;
private final int apiMajorVersion;
private final int apiMinorVersion;
private int receivedPackages = 0;
private int sentPackages = 0;
private boolean isClosed = false;
private boolean isOpened = false;
private boolean isHandshakePerformed = false;
private byte[] handshakeRandomData;
private CommonTimer connectionTimeout;
private CommonTimer handshakeTimeout;
private CommonTimer pingTask;
private final HashMap<Long, CommonTimer> schedulledPings = new HashMap<>();
private final HashMap<Integer, CommonTimer> packageTimers = new HashMap<>();
public ManagedConnection(int connectionId,
int mtprotoVersion,
int apiMajorVersion,
int apiMinorVersion,
ConnectionEndpoint endpoint,
ConnectionCallback callback,
ManagedConnectionCreateCallback factoryCallback,
AsyncConnectionFactory connectionFactory) {
this.TAG = "Connection#" + connectionId;
this.connectionId = connectionId;
this.mtprotoVersion = mtprotoVersion;
this.apiMajorVersion = apiMajorVersion;
this.apiMinorVersion = apiMinorVersion;
this.callback = callback;
this.factoryCallback = factoryCallback;
this.rawConnection = connectionFactory.createConnection(connectionId, endpoint, connectionInterface);
// Log.d(TAG, "Starting connection");
handshakeTimeout = new CommonTimer(new TimeoutRunnable());
pingTask = new CommonTimer(new PingRunnable());
connectionTimeout = new CommonTimer(new TimeoutRunnable());
connectionTimeout.schedule(CONNECTION_TIMEOUT);
this.rawConnection.doConnect();
}
// Handshake
private synchronized void sendHandshakeRequest() {
// Log.d(TAG, "Starting handshake");
DataOutput handshakeRequest = new DataOutput();
handshakeRequest.writeByte(mtprotoVersion);
handshakeRequest.writeByte(apiMajorVersion);
handshakeRequest.writeByte(apiMinorVersion);
handshakeRandomData = new byte[32];
synchronized (RANDOM) {
RANDOM.nextBytes(handshakeRandomData);
}
handshakeRequest.writeInt(handshakeRandomData.length);
handshakeRequest.writeBytes(handshakeRandomData, 0, handshakeRandomData.length);
handshakeTimeout.schedule(HANDSHAKE_TIMEOUT);
rawPost(HEADER_HANDSHAKE_REQUEST, handshakeRequest.toByteArray());
}
private synchronized void onHandshakePackage(byte[] data) throws IOException {
// Log.d(TAG, "Handshake response received");
DataInput handshakeResponse = new DataInput(data);
int protoVersion = handshakeResponse.readByte();
int apiMajor = handshakeResponse.readByte();
int apiMinor = handshakeResponse.readByte();
byte[] sha256 = handshakeResponse.readBytes(32);
byte[] localSha256 = Crypto.SHA256(handshakeRandomData);
if (!Arrays.equals(sha256, localSha256)) {
Log.w(TAG, "SHA 256 is incorrect");
// Log.d(TAG, "Random data: " + CryptoUtils.hex(handshakeRandomData));
// Log.d(TAG, "Remote SHA256: " + CryptoUtils.hex(sha256));
// Log.d(TAG, "Local SHA256: " + CryptoUtils.hex(localSha256));
throw new IOException("SHA 256 is incorrect");
}
if (protoVersion != mtprotoVersion) {
Log.w(TAG, "Incorrect Proto Version, expected: " + mtprotoVersion + ", got " + protoVersion + ";");
throw new IOException("Incorrect Proto Version, expected: " + mtprotoVersion + ", got " + protoVersion + ";");
}
if (apiMajor != apiMajorVersion) {
Log.w(TAG, "Incorrect Api Major Version, expected: " + apiMajor + ", got " + apiMajor + ";");
throw new IOException("Incorrect Api Major Version, expected: " + apiMajor + ", got " + apiMajor + ";");
}
if (apiMinor != apiMinorVersion) {
Log.w(TAG, "Incorrect Api Minor Version, expected: " + apiMinor + ", got " + apiMinor + ";");
throw new IOException("Incorrect Api Minor Version, expected: " + apiMinor + ", got " + apiMinor + ";");
}
// Log.d(TAG, "Handshake successful");
isHandshakePerformed = true;
factoryCallback.onConnectionCreated(this);
handshakeTimeout.cancel();
pingTask.schedule(PING_TIMEOUT);
}
// Proto package
private synchronized void onProtoPackage(byte[] data) throws IOException {
callback.onMessage(data, 0, data.length);
refreshTimeouts();
}
private synchronized void sendProtoPackage(byte[] data, int offset, int len) throws IOException {
if (isClosed) {
return;
}
rawPost(HEADER_PROTO, data, offset, len);
}
// Ping/Pong
private synchronized void onPingPackage(byte[] data) throws IOException {
// Just send pong package to server
rawPost(HEADER_PONG, data);
refreshTimeouts();
}
private synchronized void onPongPackage(byte[] data) throws IOException {
DataInput dataInput = new DataInput(data);
int size = dataInput.readInt();
if (size != 8) {
Log.w(TAG, "Received incorrect pong");
throw new IOException("Incorrect pong payload size");
}
long pingId = dataInput.readLong();
// Log.d(TAG, "Received pong #" + pingId + "...");
CommonTimer timeoutTask = schedulledPings.remove(pingId);
if (timeoutTask == null) {
return;
}
timeoutTask.cancel();
refreshTimeouts();
}
private synchronized void sendPingMessage() {
if (isClosed) {
return;
}
final long pingId = RANDOM.nextLong();
DataOutput dataOutput = new DataOutput();
dataOutput.writeInt(8);
synchronized (RANDOM) {
dataOutput.writeLong(pingId);
}
CommonTimer pingTimeoutTask = new CommonTimer(new TimeoutRunnable());
schedulledPings.put(pingId, pingTimeoutTask);
pingTimeoutTask.schedule(RESPONSE_TIMEOUT);
// Log.d(TAG, "Performing ping #" + pingId + "... " + pingTimeoutTask);
rawPost(HEADER_PING, dataOutput.toByteArray());
}
private void refreshTimeouts() {
// Settings all timeouts to now+RESPONSE_TIMEOUT
// Simple, but need some logic improvements to support detecting of frame lost.
for (CommonTimer ping : schedulledPings.values()) {
ping.schedule(RESPONSE_TIMEOUT);
}
for (CommonTimer ackTimeout : packageTimers.values()) {
ackTimeout.schedule(RESPONSE_TIMEOUT);
}
pingTask.schedule(PING_TIMEOUT);
}
// Ack
private synchronized void onAckPackage(byte[] data) throws IOException {
DataInput ackContent = new DataInput(data);
int frameId = ackContent.readInt();
CommonTimer timerCompat = packageTimers.remove(frameId);
if (timerCompat == null) {
return;
}
timerCompat.cancel();
refreshTimeouts();
}
private synchronized void sendAckPackage(int receivedIndex) throws IOException {
if (isClosed) {
return;
}
DataOutput ackPackage = new DataOutput();
ackPackage.writeInt(receivedIndex);
rawPost(HEADER_ACK, ackPackage.toByteArray());
}
// Drop
private synchronized void onDropPackage(byte[] data) throws IOException {
DataInput drop = new DataInput(data);
long messageId = drop.readLong();
int errorCode = drop.readByte();
int messageLen = drop.readInt();
String message = new String(drop.readBytes(messageLen), "UTF-8");
Log.w(TAG, "Drop received: " + message);
throw new IOException("Drop received: " + message);
}
// Raw callbacks
private synchronized void onRawConnected() {
// Log.d(TAG, "onConnected");
if (isClosed) {
// Log.d(TAG, "onConnected:isClosed");
return;
}
if (isOpened) {
// Log.d(TAG, "onConnected:isOpened");
return;
}
isOpened = true;
connectionTimeout.cancel();
sendHandshakeRequest();
}
private synchronized void onRawReceived(byte[] data) {
if (isClosed) {
return;
}
// Log.w(TAG, "onRawReceived");
try {
DataInput dataInput = new DataInput(data);
int packageIndex = dataInput.readInt();
if (receivedPackages != packageIndex) {
Log.w(TAG, "Invalid package index. Expected: " + receivedPackages + ", got: " + packageIndex);
throw new IOException("Invalid package index. Expected: " + receivedPackages + ", got: " + packageIndex);
}
receivedPackages++;
int header = dataInput.readByte();
int dataLength = dataInput.readInt();
byte[] content = dataInput.readBytes(dataLength);
int crc32 = dataInput.readInt();
CRC32_ENGINE.reset();
CRC32_ENGINE.update(content);
if (((int) CRC32_ENGINE.getValue()) != crc32) {
Log.w(TAG, "Incorrect CRC32");
throw new IOException("Incorrect CRC32");
}
// Log.w(TAG, "Received package: " + header);
if (header == HEADER_HANDSHAKE_RESPONSE) {
if (isHandshakePerformed) {
throw new IOException("Double Handshake");
}
onHandshakePackage(content);
} else {
if (!isHandshakePerformed) {
throw new IOException("Package before Handshake");
}
if (header == HEADER_PROTO) {
onProtoPackage(content);
sendAckPackage(packageIndex);
} else if (header == HEADER_PING) {
onPingPackage(content);
} else if (header == HEADER_PONG) {
onPongPackage(content);
} else if (header == HEADER_DROP) {
onDropPackage(content);
} else if (header == HEADER_ACK) {
onAckPackage(content);
} else {
Log.w(TAG, "Received unknown package #" + header);
}
}
} catch (IOException e) {
Log.e(TAG, e);
close();
}
// Log.w(TAG, "onRawReceived:end");
}
private synchronized void onRawClosed() {
// Log.w(TAG, "Received closed event");
close();
}
// Raw send
private synchronized void rawPost(int header, byte[] data) {
rawPost(header, data, 0, data.length);
}
private synchronized void rawPost(int header, byte[] data, int offset, int len) {
// Log.w(TAG, "rawPost");
int packageId = sentPackages++;
DataOutput dataOutput = new DataOutput();
dataOutput.writeInt(packageId);
dataOutput.writeByte(header);
dataOutput.writeInt(data.length);
dataOutput.writeBytes(data, offset, len);
CRC32_ENGINE.reset();
CRC32_ENGINE.update(data, offset, len);
dataOutput.writeInt((int) CRC32_ENGINE.getValue());
if (header == HEADER_PROTO) {
CommonTimer timeoutTask = new CommonTimer(new TimeoutRunnable());
packageTimers.put(packageId, timeoutTask);
timeoutTask.schedule(RESPONSE_TIMEOUT);
}
rawConnection.doSend(dataOutput.toByteArray());
}
// Public methods
@Override
public synchronized void post(byte[] data, int offset, int len) {
// Log.w(TAG, "post");
if (isClosed) {
return;
}
try {
sendProtoPackage(data, offset, len);
} catch (IOException e) {
Log.e(TAG, e);
close();
}
}
@Override
public synchronized boolean isClosed() {
return isClosed;
}
@Override
public synchronized void close() {
// Log.w(TAG, "close");
if (isClosed) {
return;
}
isClosed = true;
rawConnection.doClose();
synchronized (packageTimers) {
for (Integer id : packageTimers.keySet()) {
packageTimers.get(id).cancel();
}
for (Long ping : schedulledPings.keySet()) {
schedulledPings.get(ping).cancel();
}
schedulledPings.clear();
packageTimers.clear();
}
pingTask.cancel();
connectionTimeout.cancel();
handshakeTimeout.cancel();
if (!isOpened || !isHandshakePerformed) {
factoryCallback.onConnectionCreateError(this);
} else {
callback.onConnectionDie();
}
}
@Override
public void checkConnection() {
pingTask.schedule(0);
}
// Connection callback
private class ConnectionInterface implements AsyncConnectionInterface {
@Override
public void onConnected() {
ManagedConnection.this.onRawConnected();
}
@Override
public void onReceived(byte[] data) {
ManagedConnection.this.onRawReceived(data);
}
@Override
public void onClosed() {
ManagedConnection.this.onRawClosed();
}
}
// Timer runanbles
private class PingRunnable implements Runnable {
@Override
public void run() {
sendPingMessage();
}
}
private class TimeoutRunnable implements Runnable {
@Override
public void run() {
// Log.d(TAG, "Timeout " + this);
close();
}
}
}