package ru.denull.mtproto;
import java.io.IOException;
import java.math.BigInteger;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.ClosedByInterruptException;
import java.nio.channels.SocketChannel;
import java.security.MessageDigest;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import ru.denull.mtproto.Auth.AuthCallback;
import ru.denull.mtproto.Auth.AuthState;
import ru.denull.mtproto.DataService.ConnectTaskCallback;
import ru.denull.mtproto.DataService.ReadTaskCallback;
import ru.denull.mtproto.DataService.WriteTaskCallback;
import ru.denull.mtproto.Server.RPCCallback;
import ru.denull.wire.model.Config;
import tl.*;
import tl.account.UpdateStatus;
import tl.help.GetConfig;
import static ru.denull.mtproto.CryptoUtils.*;
public class Server implements ReadTaskCallback {
private static final String TAG = "Server";
public Preferences pref;
public DataService service;
public Transport transport;
public String address;
public int port;
public Auth auth;
private long session_id = 1;
public long old_session_id = 0;
private int client_seqno = 1;
private long minimal_message_id = 0; // used to prevent duplicate msg_ids when sending lots of requests simultaneously
public long time_diff;
public boolean timer_running = false;
public boolean resolving_problem = false;
public long last_problem_time = 0;
public int resolve_try = 0;
public Server(DataService service, String address, int port) throws IOException {
this.service = service;
this.address = address;
this.port = port;
reconnect();
Random rand = new Random();
session_id = rand.nextLong();
//Log.i(TAG, "Using session #" + session_id);
System.out.println("New connection to server " + address + ":" + port + " (session " + session_id + ")");
//pref = service.getSharedPreferences(service.getString(R.string.preferences_auth) + "-" + address + ":" + port, Context.MODE_PRIVATE);
pref = Preferences.userRoot().node("wire/server/" + address + ":" + port);
old_session_id = pref.getLong("session_id", 0);
pref.putLong("session_id", session_id);
try {
pref.sync();
} catch (BackingStoreException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
auth = new Auth(this, pref);
// Start reading
service.read(this, this);
}
public void disconnect() {
if (transport != null) {
transport.close();
}
}
public void reconnect() throws IOException {
disconnect();
transport = new TCPTransport(address, port);
}
// true if problem fixed
// rightNow = true: instantly fails if there was another try not too long ago
// rightNow = false: if already tried to reconnect, sleeps for some time and tries then
public boolean resolveNetworkProblem(boolean rightNow) {
resolving_problem = true;
long time = System.currentTimeMillis();
if (time - last_problem_time > 120000) {
resolve_try = 0;
}
last_problem_time = time;
resolve_try++;
long time_to_sleep = 0;
/*if (service.networkDown) {
time_to_sleep = 300000;
} else
if (resolve_try > 20) {
time_to_sleep = 60000;
} else
if (resolve_try > 10) {
time_to_sleep = 30000;
} else
if (resolve_try > 7) {
time_to_sleep = 20000;
} else
if (resolve_try > 5) {
time_to_sleep = 10000;
} else
if (resolve_try > 3) {
time_to_sleep = 4000;
} else*/
if (resolve_try > 2) {
time_to_sleep = 2000;
} else
if (resolve_try > 1) {
time_to_sleep = 1000;
}
if (time_to_sleep > 0) {
if (rightNow) {
//Log.i(TAG, "Unable to fix network problem right now...");
return false;
}
try {
//Log.i(TAG, "Will try to fix problem after " + time_to_sleep + "ms");
Thread.sleep(time_to_sleep);
} catch (InterruptedException e) {
//Log.i(TAG, "Thread was interrupted...");
return false;
}
}
try {
//Log.i(TAG, "Resolving problem...");
if (resolving_problem) { // we may have resolved it from another thread while this was sleeping
reconnect();
//Log.i(TAG, "Successfully restored connection");
} else {
//Log.i(TAG, "Ah. Already resolved it, nice.");
}
resolve_try = 0;
return true;
} catch (IOException e) {
//Log.i(TAG, "Unable to fix network problem...");
return false;
}
}
private ArrayList<RPCCall> queuedCalls = new ArrayList<RPCCall>(); // messages that will be sent after next sendQueued call
private ArrayList<RPCCall> sentCalls = new ArrayList<RPCCall>(); // messages waiting for result
public class RPCCall {
TLObject payload;
boolean encrypted;
boolean significant;
RPCCallback callback;
// filled when sent
long sent_time = 0;
long message_id = 0;
public RPCCall(TLObject payload, boolean encrypted, boolean significant, RPCCallback callback) {
super();
this.payload = payload;
this.encrypted = encrypted;
this.significant = significant;
this.callback = callback;
}
}
public interface RPCCallback<T extends TLObject> {
public void done(T result);
public void error(int code, String message);
}
synchronized public void sendQueued() throws Exception {
if (queuedCalls.size() == 0) {
return;
}
ArrayList<RPCCall> _queuedCalls = new ArrayList<Server.RPCCall>(queuedCalls);
queuedCalls = new ArrayList<RPCCall>();
long message_id = (long)((System.currentTimeMillis() + time_diff * 1000) * 4294967.296) & ~3L;
if (_queuedCalls.size() == 1) {
// send single call, no need to pack it into container
final RPCCall call = _queuedCalls.get(0);
if (call.encrypted) {
if (auth.state == AuthState.FAILED) {
call.callback.error(501, "auth_key missing");
return;
} else
if (auth.state != AuthState.COMPLETE){
return;
}
}
final Message message = call.encrypted ?
new EncryptedMessage(
Math.max(minimal_message_id, message_id),
call.payload,
session_id,
auth.auth_key_id,
auth.server_salt,
client_seqno,
call.significant) :
new Message(
Math.max(minimal_message_id, message_id),
call.payload);
minimal_message_id = Math.max(minimal_message_id, message_id) + 4;
if (call.significant) {
client_seqno++;
}
service.write(this, message.encrypt(auth.auth_key), new WriteTaskCallback() {
public void error(int code, String message) {
//Log.e(TAG, "Unable to send call " + call.payload + ": " + message);
if (call.callback != null) {
sentCalls.remove(call);
}
queuedCalls.add(call);
}
public void done(Server server) {
// ok
}
});
//Log.i(TAG, "=> " + Long.toHexString(message.message_id) + ": " + message.payload);
if (call.callback != null) {
call.sent_time = System.currentTimeMillis();
call.message_id = message.message_id;
sentCalls.add(call);
}
} else {
// pack all queuedCalls into one container and send them
final ArrayList<TransportMessage> messages = new ArrayList<TransportMessage>(queuedCalls.size());
ArrayList<RPCCall> unprocessedCalls = new ArrayList<RPCCall>();
final ArrayList<RPCCall> processedCalls = new ArrayList<RPCCall>();
for (int i = 0; i < _queuedCalls.size(); i++) {
RPCCall call = _queuedCalls.get(i);
if (call.encrypted) {
if (auth.state == AuthState.FAILED) {
call.callback.error(501, "auth_key missing");
continue;
} else
if (auth.state != AuthState.COMPLETE){
unprocessedCalls.add(call);
continue;
}
}
processedCalls.add(call);
TransportMessage message = new TransportMessage(
Math.max(minimal_message_id, message_id),
(client_seqno << 1) + (call.significant ? 1 : 0),
call.payload.length(true),
call.payload
);
messages.add(message);
//System.out.println("==> " + Long.toHexString(messages[i].msg_id) + ": " + call.payload);
minimal_message_id = Math.max(minimal_message_id, message_id) + 4;
if (call.significant) {
client_seqno++;
}
if (call.callback != null) {
call.sent_time = System.currentTimeMillis();
call.message_id = message.msg_id;
sentCalls.add(call);
}
}
if (unprocessedCalls.size() == 0) {
Message message =
new EncryptedMessage(
Math.max(minimal_message_id, message_id),
new MsgContainer(messages.toArray(new TransportMessage[messages.size()])),
session_id,
auth.auth_key_id,
auth.server_salt,
client_seqno,
false);
minimal_message_id = Math.max(minimal_message_id, message_id) + 4;
service.write(this, message.encrypt(auth.auth_key), new WriteTaskCallback() {
public void error(int code, String message) {
//Log.e(TAG, "Unable to send calls " + new MsgContainer(messages.toArray(new TransportMessage[messages.size()])) + ": " + message);
for (RPCCall call : processedCalls) {
if (call.callback != null) {
sentCalls.remove(call);
}
queuedCalls.add(call);
}
}
public void done(Server server) {
// ok
}
});
//Log.i(TAG, "=> " + Long.toString(message.message_id, 16) + ": " + message.payload);
} else {
//Log.w(TAG, "There are unsent calls (" + unprocessedCalls.toString() + ") due to incomplete auth process");
for (TransportMessage transport : messages) {
Message message =
new Message(
Math.max(minimal_message_id, message_id),
transport);
minimal_message_id = Math.max(minimal_message_id, message_id) + 4;
//Log.i(TAG, "=> " + Long.toString(message.message_id, 16) + ": " + message.payload);
service.write(this, message.encrypt(auth.auth_key), new WriteTaskCallback() {
public void error(int code, String message) {
//Log.e(TAG, "Unable to send call");
}
public void done(Server server) {
// ok
}
});
}
}
queuedCalls.addAll(unprocessedCalls);
}
if (queuedCalls.size() > 0 && !timer_running) {
service.scheduledPool.schedule(new Callable<Boolean>() {
public Boolean call() throws Exception {
timer_running = false;
sendQueued();
return true;
}
}, 15, TimeUnit.SECONDS);
timer_running = true;
}
}
public void queue(TLObject object) throws Exception {
queue(object, true, false, null);
}
public void queue(TLObject object, boolean encrypted, boolean significant, RPCCallback response) throws Exception {
queuedCalls.add(new RPCCall(object, encrypted, significant, response));
if (queuedCalls.size() > 15) {
sendQueued();
} else
if (!timer_running) {
service.scheduledPool.schedule(new Callable<Boolean>() {
public Boolean call() throws Exception {
timer_running = false;
sendQueued();
return true;
}
}, 15, TimeUnit.SECONDS);
timer_running = true;
}
}
public void send(TLObject object) throws Exception {
send(object, true, true, null);
}
public void send(TLObject object, boolean encrypted) throws Exception {
send(object, encrypted, encrypted, null);
}
public void send(TLObject object, boolean encrypted, boolean significant, RPCCallback callback) throws Exception {
queue(object, encrypted, significant, callback);
sendQueued();
}
private boolean initPerformed = false;
public void call(TLFunction request, RPCCallback callback, boolean latestLayer) {
try {
if (latestLayer && !initPerformed) {
send(new InvokeWithLayer9(new InitConnection(Config.api_id, Config.getDeviceModel(), Config.getSystemVersion(), Config.app_version, "ru", request)), true, true, callback);
initPerformed = true;
} else {
send(request, true, true, callback);
}
} catch (Exception e) {
e.printStackTrace();
callback.error(0, e.getMessage());
}
}
public void call(TLFunction request, RPCCallback callback) {
call(request, callback, true);
}
public void call(TLFunction request) {
call(request, new RPCCallback<TLObject>() {
public void done(TLObject result) {
}
public void error(int code, String message) {
}
}, true);
}
private void processMessage(TLObject message, long msg_id) throws Exception {
//Log.i(TAG, "<= " + message);
//System.out.println("<= " + msg_id);
ArrayList<Long> confirm = new ArrayList<Long>();
if (message instanceof MsgContainer) {
for (TLObject child : ((MsgContainer) message).messages) {
if ((((TransportMessage) child).seqno & 1) == 1) {
confirm.add(((TransportMessage) child).msg_id);
}
//Log.i(TAG, "<== " + Long.toString(((TransportMessage) child).msg_id, 16) + ": " + ((TransportMessage) child).body);
processMessage(((TransportMessage) child).body, ((TransportMessage) child).msg_id);
}
} else
if (message instanceof BadMsgNotification) {
Log.w(TAG, "Bad message: " + ((BadMsgNotification) message).error_code);
if (((BadMsgNotification) message).error_code == 16 || ((BadMsgNotification) message).error_code == 17) {
System.out.println("Invalid msg_id (was " + ((BadMsgNotification) message).bad_msg_id + ", received " + msg_id + "), old time_diff: " + time_diff);
time_diff = (int) (((msg_id & ~3L) / 4294967.296 - System.currentTimeMillis()) / 1000);
minimal_message_id = 0;
System.out.println("new time_diff: " + time_diff);
for (RPCCall call : sentCalls) {
if (call.message_id == ((BadMsgNotification) message).bad_msg_id) {
sentCalls.remove(call);
send(call.payload, call.encrypted, call.significant, call.callback);
break;
}
}
}
} else
if (message instanceof BadServerSalt) {
//Log.w(TAG, "Bad server salt, updating");
BadServerSalt result = (BadServerSalt) message;
auth.server_salt = result.new_server_salt;
pref.putLong("server_salt", auth.server_salt);
// resend broken call
for (RPCCall call : sentCalls) {
if (call.message_id == result.bad_msg_id) {
sentCalls.remove(call);
send(call.payload, call.encrypted, call.significant, call.callback);
break;
}
}
} else
if (message instanceof RpcResult) {
RpcResult result = ((RpcResult) message);
//Log.i(TAG, "<<< " + result.result);
ArrayList<RPCCall> sentCallsCopy = new ArrayList<RPCCall>(sentCalls);
for (final RPCCall call : sentCallsCopy) {
if (call.message_id == result.req_msg_id) {
if (result.result instanceof RpcError) {
RpcError error = ((RpcError) result.result);
//Log.e(TAG, "<= " + result.result);
Pattern p = Pattern.compile("MIGRATE_(\\d+)");
Matcher m = p.matcher(error.error_message);
if (error.error_code == 303 && m.find()) { // MIGRATE
final int dc = Integer.parseInt(m.group(1));
Log.e(TAG, "Reconnecting");
service.mainServerID = dc;
service.connectAndPrepare(dc, true, false, false, new AuthCallback() {
public void done(Server server, byte[] auth_key) {
// ...and perform call once again
service.mainServerID = dc;
try {
server.send(call.payload, call.encrypted, call.significant, call.callback);
} catch (Exception e) {
e.printStackTrace();
call.callback.error(0, "Error while resending request");
}
}
public void error() {
Log.e(TAG, "Unable to generate new auth key");
call.callback.error(0, "Unable to generate new auth key");
}
});
} else {
call.callback.error(error.error_code, error.error_message);
}
} else {
call.callback.done(result.result);
}
sentCalls.remove(call);
break;
}
}
} else
if (message instanceof TUpdates) {
service.processUpdates((TUpdates) message);
} else {
//Log.i(TAG, "Unprocessed message: " + message.toString());
}
if (confirm.size() > 0) {
long[] array = new long[confirm.size()];
for (int i = 0; i < array.length; i++) {
array[i] = confirm.get(i);
}
queue(new MsgsAck(array));
}
}
public void done(Server server) {
disconnect();
}
public void incoming(ByteBuffer packet) {
try {
Message message = Message.decrypt(packet, auth.auth_key, auth.auth_key_id);
if (message == null) {
return;
}
if (message.error != 0) {
Log.e(TAG, "<= error " + message.error);
} else {
//Log.i(TAG, "<= " + Long.toString(message.message_id, 16) + ": " + message.payload);
}
// process message
if (auth.state != AuthState.NONE && auth.state != AuthState.FAILED && auth.state != AuthState.COMPLETE) {
auth.generateKey(message.payload);
}
processMessage(message.payload, message.message_id);
if (message instanceof EncryptedMessage) {
if ((((EncryptedMessage) message).seq_no & 1) == 1) {
queue(new MsgsAck(new long[]{ message.message_id }));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
public void error(int code, String message) {
//
}
}