/*
(C) 2007-2012 yura.net
This file is part of Lobby.
Lobby is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; version 3 of the License
Lobby is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.yura.lobby.client;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.security.KeyFactory;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.crypto.Cipher;
import net.yura.lobby.gen.ProtoLobby;
import net.yura.lobby.model.Game;
import net.yura.lobby.model.GameType;
import net.yura.lobby.model.Message;
import net.yura.lobby.model.Player;
import net.yura.lobby.util.ByteBufferInputStream;
import net.yura.mobile.util.StringUtil;
public class LobbyCom implements Connection, ProtoAccess.ObjectProvider {
protected static final Logger logger = Logger.getLogger(LobbyCom.class.getName());
private static final int PROTOCOL_VERSION = 1;
private LobbyClient myclient;
private AndroidLobbyClient androidClient;
private String username;
private String password;
private RSAPublicKey pubKey;
private Cipher encrypt;
private Map gameTypeMap;
private String tempRegName;
private String tempRegPass;
private ProtoAccess access = new ProtoAccess(this);
//SocketClient client;
TcpClient client;
private final String uuid,appName,appVersion;
public LobbyCom(String uuid,String appName,String appVersion) {
this.uuid = uuid;
this.appName = appName;
this.appVersion = appVersion;
try {
// for android we NEED "RSA/ECB/PKCS1Padding" as "RSA" does not work
// when the server tries to decrypt it, it throws a BadPaddingException
encrypt = Cipher.getInstance("RSA/ECB/PKCS1Padding");
}
catch (Exception e) {
e.printStackTrace();
}
gameTypeMap = new HashMap();
}
void connected() {
Map hello = new HashMap();
hello.put("version", new Integer(PROTOCOL_VERSION) );
hello.put("uuid", uuid );
hello.put("appName", appName);
hello.put("appVersion", appVersion);
hello.put("locale", Locale.getDefault().toString() );
send(ProtoLobby.REQUEST_HELLO, hello );
}
void disconnected() {
// TODO maybe need to clear other things too
gameTypeMap.clear();
myclient.disconnected();
}
public Object getObjectId(Object object) {
if (object instanceof GameType) {
return new Integer( ((GameType)object).getId() );
}
throw new IllegalArgumentException("unknown object: "+object);
}
public Object getObjetById(Object id, Class clas) {
if (clas == GameType.class) {
return gameTypeMap.get(id);
}
throw new IllegalArgumentException("unknown class: "+clas);
}
final List sendQueue = new java.util.Vector();
Thread writeThread;
void send(String command,Object param) {
if (writeThread==null) {
writeThread = new Thread() {
public void run() {
try {
while ( !Thread.interrupted() ) {
Message message;
synchronized (sendQueue) {
while (sendQueue.isEmpty()) {
sendQueue.wait();
}
message = (Message)sendQueue.remove(0);
}
try {
// int size = access.computeAnonymousObjectSize(message);
// ByteArrayOutputStream bytes = new ByteArrayOutputStream(size);
// access.save(bytes, message);
// client.sendToNetwork(bytes.toByteArray());
TcpClient cl = client;
if (cl!=null) {
int size = access.computeAnonymousObjectSize(message);
ByteArrayOutputStream bytes = new ByteArrayOutputStream(size+4);
new DataOutputStream(bytes).writeInt(size);
access.save(bytes, message);
cl.send( ByteBuffer.wrap( bytes.toByteArray() ) );
}
}
catch (InterruptedException in) {
throw in; // do not want to catch this
}
catch (Exception ex) {
Level level = (ex instanceof IOException && "not connected".equals(ex.getMessage())) ||
(ex instanceof SocketException && "Broken pipe".equals(ex.getMessage())) ||
(ex instanceof SocketException && "sendto failed: EPIPE (Broken pipe)".equals(ex.getMessage()))
?Level.INFO:Level.WARNING;
logger.log(level,"could not send "+message,ex);
}
}
}
catch (InterruptedException ex) { }
}
};
writeThread.start();
}
System.out.println("sending message "+command+" "+param);
Message message = new Message();
message.setCommand(command);
message.setParam(param);
if (wait!=0) {
message.setWait(new Integer(wait));
}
synchronized (sendQueue) {
sendQueue.add(message);
sendQueue.notify();
}
}
private int wait;
public void setWait(int wait) {
this.wait = wait;
}
private boolean canEncrypt() {
return encrypt != null;
}
private byte[] encrypt(String name) {
try {
return encrypt.doFinal( name.getBytes( "UTF-8" ) );
}
catch (Exception ex) {
throw new RuntimeException(ex);
}
}
private void serverPublicKey(byte[] message) {
// this method is called when the server wants to give the client a new public key
if (canEncrypt()) {
RSAPublicKey previousKey = pubKey;
try {
pubKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(message));
encrypt.init(Cipher.ENCRYPT_MODE, pubKey);
}
catch (Exception e) {
e.printStackTrace();
pubKey = previousKey;
return;
}
}
}
void messageFromServer(Message msg) {
String command = msg.getCommand();
Object param = msg.getParam();
System.out.println("got message "+command+" "+param);
try {
if (ProtoLobby.COMMAND_KEY.equals(command)) {
Map map = (Map)param;
String newHost = (String)map.get("newHost");
if (newHost!=null) {
int index = newHost.indexOf(':');
String server = index<0?newHost:newHost.substring(0, index);
int port = index<0?client.getPort():Integer.parseInt( newHost.substring(index+1) );
disconnect();
connect(server, port);
}
else {
String guestName = (String)map.get("username");
Integer userType = (Integer)map.get("usertype");
serverPublicKey( (byte[])map.get("key") );
String u=username,p=password;
username = guestName;
myclient.setUsername(guestName, userType.intValue());
myclient.connecting("Handshake Complete");
myclient.connecting("You are now connected");
myclient.connected();
if (u!=null && p!=null) {
login(u, p);
}
}
}
else if (ProtoLobby.COMMAND_ALL_GAMETYPES.equals(command)) {
List gametypes = (List)param;
for (int c=0;c<gametypes.size();c++) {
GameType gt = (GameType)gametypes.get(c);
gameTypeMap.put( new Integer(gt.getId()), gt);
}
myclient.addGameType(gametypes);
}
else if (ProtoLobby.COMMAND_ADD_OR_UPDATE_GAME.equals(command)) {
myclient.addOrUpdateGame( (Game)param );
}
else if (ProtoLobby.COMMAND_REMOVE_GAME.equals(command)) {
Map map = (Map)param;
myclient.removeGame( ((Integer)map.get("game_id")).intValue() );
}
else if (ProtoLobby.COMMAND_GAME_MESSAGE.equals(command)) {
Map map = (Map)param;
myclient.messageForGame( ((Integer)map.get("game_id")).intValue(), map.get("message"));
}
else if (ProtoLobby.COMMAND_LOGIN_OK.equals(command)) {
Map map = (Map)param;
Integer userType = (Integer)map.get("userType");
String newName = (String)map.get("username");
username = newName==null?tempRegName:newName;
password = tempRegPass;
myclient.setUsername(username, userType.intValue() );
}
else if (ProtoLobby.COMMAND_LOGIN_ERROR.equals(command)) {
myclient.error( "login error: "+param );
}
else if (ProtoLobby.COMMAND_SERVER_ERROR.equals(command)) {
myclient.error( "server error: "+param );
}
else if (ProtoLobby.COMMAND_LOGOUT_OK.equals(command)) {
Map map = (Map)param;
String guestUser = (String)map.get("username");
username = guestUser;
password = null;
myclient.setUsername( guestUser , Player.PLAYER_GUEST );
}
else if (ProtoLobby.COMMAND_RENAME_PLAYER.equals(command)) {
Map map = (Map)param;
myclient.renamePlayer((String)map.get("oldName"), (String)map.get("newName"), ((Integer)map.get("userType")).intValue());
}
else if (ProtoLobby.COMMAND_CHAT_MESSAGE.equals(command)) {
Map map = (Map)param;
String message = (String)map.get("message");
String sender = (String)map.get("sender");
Integer room_id = (Integer)map.get("room_id");
if (room_id!=null) {
myclient.incomingChat( room_id.intValue(), sender , message);
}
else {
myclient.incomingChat(sender,message);
}
}
else if (ProtoLobby.COMMAND_PLAYER_ADDED.equals(command)) {
Map map = (Map)param;
Player player = (Player)map.get("player");
Integer room_id = (Integer)map.get("room_id");
if (room_id==null) {
myclient.addPlayer(player);
}
else {
myclient.addPlayer( room_id.intValue() , player );
}
}
else if (ProtoLobby.COMMAND_PLAYER_REMOVE.equals(command)) {
Map map = (Map)param;
String playerName = (String)map.get("playerName");
Integer room_id = (Integer)map.get("room_id");
if (room_id==null) {
myclient.removePlayer(playerName);
}
else {
myclient.removePlayer( room_id.intValue() , playerName );
}
}
else if (ProtoLobby.COMMAND_GAME_STARTED.equals(command)) {
Map map = (Map)param;
myclient.gameStarted( ((Integer)map.get("game_id")).intValue() );
}
else if (ProtoLobby.COMMAND_ANDROID_REGISTER_DONE.equals(command)) {
androidClient.registerDone();
}
else if (ProtoLobby.COMMAND_ANDROID_UNREGISTER_DONE.equals(command)) {
androidClient.unregisterDone();
}
else {
myclient.error("unknown command "+command+" "+param);
}
}
catch (Exception ex) {
System.err.println("error handling command "+msg+" error: "+ex);
ex.printStackTrace();
}
}
//##########################################################################
//######################### Connection interface ###########################
//##########################################################################
public void addEventListener(LobbyClient lc) {
myclient = lc;
}
public void addAndroidEventListener(AndroidLobbyClient lc) {
androidClient = lc;
}
public void connect(String sev, int p) {
// client = new SocketClient() {
// void log(String msg) {
// myclient.connecting(msg);
// }
// void connected() {
// LobbyCom.this.connected();
// }
// void disconnected() {
// LobbyCom.this.disconnected();
// }
// void gotMessage(byte[] bytes) {
// ByteArrayInputStream messageBytes = new ByteArrayInputStream( bytes );
// Message message = (Message)access.load(messageBytes, bytes.length);
// messageFromServer(message);
// }
// };
// client.connect(sev, p);
client = new TcpClient() {
// ByteInputStream data = new ByteInputStream();
int size = -1;
protected void onRead(final ByteBuffer buf) throws Exception {
// byte[] bytes = new byte[ buf.remaining() ];
// buf.get(bytes);
// data.addBytes(bytes);
ByteBufferInputStream data = new ByteBufferInputStream(buf);
while (true) {
if (size==-1 && data.available() >= 4) {
size = data.readInt();
}
else if (size>=0 && data.available() >= size) {
Message message = (Message)access.load(data, size);
size = -1;
messageFromServer(message);
}
else {
break;
}
}
}
protected void onConnected() throws Exception {
connected();
}
protected void onDisconnected() {
disconnected();
size = -1; // reset the read size var
}
};
client.setAddress(sev, p);
client.start();
}
public void disconnect() {
//client.disconnect();
client.stop();
client = null;
if (writeThread!=null) {
writeThread.interrupt();
writeThread=null;
}
}
public void setNick(String name) {
name = checkUsername(name);
if (name.equals(username)) return; // this is already our name, do nothing
tempRegName = name;
Map login = new HashMap();
login.put("username", canEncrypt()? (Object) encrypt(name):(Object) name );
send( ProtoLobby.REQUEST_SET_NICK , login );
}
/**
* we do not like double spaces, or single spaces at the start or end of a username
*/
public static String checkUsername(String name) {
name = name.trim();
while (name.indexOf(" ")>=0) {
name = StringUtil.replaceAll(name, " ", " ");
}
return name;
}
public void login(String name, String pass) {
if (!canEncrypt()) {
myclient.error("Please update your java to version 1.5 or higher to be able to register and login \nhttp://java.sun.com/javase/downloads/");
}
else {
tempRegName = name;
tempRegPass = pass;
Map login = new HashMap();
login.put("username", encrypt(name) );
login.put("password", encrypt(pass) );
send( ProtoLobby.REQUEST_LOGIN , login );
}
}
public void register(String username, String email, String password) {
if (!canEncrypt()) {
myclient.error("Please update your java to version 1.5 or higher to be able to register and login \nhttp://java.sun.com/javase/downloads/");
}
else {
tempRegName = username;
tempRegPass = password;
Map register = new HashMap();
register.put("username", encrypt(username) );
register.put("password", encrypt(password) );
register.put("email", encrypt(email) );
send( ProtoLobby.REQUEST_REGISTER , register);
}
}
public void logout() {
send( ProtoLobby.REQUEST_LOGOUT , null );
}
public void getGameTypes() {
send( ProtoLobby.REQUEST_ALL_GAMETYPES , null );
}
public void getGames(GameType gameType) {
Map map = new HashMap();
map.put("game_type_id", new Integer(gameType.getId()) );
send( ProtoLobby.REQUEST_GET_GAMES , map );
}
public void createNewGame(Game game) {
send( ProtoLobby.REQUEST_CREATE_NEW_GAME , game );
}
public void joinGame(int game_id) {
Map map = new HashMap();
map.put("game_id", new Integer(game_id) );
send( ProtoLobby.REQUEST_JOIN_GAME , map );
}
public void leaveGame(int game_id) {
Map map = new HashMap();
map.put("game_id", new Integer(game_id) );
send( ProtoLobby.REQUEST_LEAVE_GAME , map );
}
public void playGame(int gameId) {
Map map = new HashMap();
map.put("game_id", new Integer(gameId) );
send( ProtoLobby.REQUEST_PLAY_GAME, map );
}
public void sendGameMessage(int game_id, Object message) {
Map map = new HashMap();
map.put("game_id", new Integer(game_id) );
map.put("message", message );
send( ProtoLobby.REQUEST_GAME_MESSAGE, map );
}
public void closeGame(int game_id) {
Map map = new HashMap();
map.put("game_id", new Integer(game_id) );
send( ProtoLobby.REQUEST_CLOSE_GAME , map );
}
public void sendChat(int roomid, String message) {
Map map = new HashMap();
if (roomid>=0) {
map.put("room_id", new Integer(roomid) );
}
map.put("message", message );
send( ProtoLobby.COMMAND_CHAT_MESSAGE , map );
}
public void androidRegister(String registrationId) {
send( ProtoLobby.REQUEST_ANDROID_REGISTER , registrationId );
}
public void androidUnregister(String registrationId) {
send( ProtoLobby.REQUEST_ANDROID_UNREGISTER , registrationId );
}
@Override
public void delGame(int gameId) {
Map map = new HashMap();
map.put("game_id", new Integer(gameId) );
send( ProtoLobby.REQUEST_DEL_GAME , map );
}
@Override
public void sendAdminCommand(String command, Map params) {
send(command, params);
}
}