package net.sf.colossus.webserver; import java.net.Socket; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import net.sf.colossus.common.Options; import net.sf.colossus.util.Glob; import net.sf.colossus.util.Split; import net.sf.colossus.webclient.WebClient; import net.sf.colossus.webcommon.GameInfo; import net.sf.colossus.webcommon.IWebClient; import net.sf.colossus.webcommon.IWebServer; import net.sf.colossus.webcommon.User; /** * This class represents an actual WebServer client. * * Mostly it contains the client's state data (logged in, client version, * user and user name, and such). * * It holds the reference to the actual socket thread that is waiting * for input from client. * * This class here provides the parsing of commands coming from clients * to convert them into actual calls to to on the server object, and it * also provides the methods which the server calls on the client * (=translates method calls into text to send them over the socket). * * TODO: * The "parse messages" still contains some blocks and processing * which should rather be in the actual socket thread object * (during split into client and actual socket thread just copied the * whole if-elseif-else block to here). * */ public class WebServerClient implements IWebClient { private static final Logger LOGGER = Logger .getLogger(WebServerClient.class.getName()); private final static String sep = IWebServer.WebProtocolSeparator; /** The client socket thread that handled the low-level connection stuff */ private final WebServerClientSocketThread cst; /** The web server object that is managing all WebServerClients */ private WebServer server; /** Whether or not this WebServerClient is at the moment logged in */ private boolean loggedIn = false; /** Client side version */ private int clientVersion; /** The user associated with this WebClient connection */ private User user = null; /** * During registration request and sending of confirmation code, * we do not have a user yet. The parseLine sets then this variable * according to the username argument which was send from client. */ private String unverifiedUsername = null; /** Time when last gameStartsNowSent was sent (in ms since epoch) */ private long gameStartsNowSent = -1; /** Time when last gameStartsSoonSent was sent (in ms since epoch) */ private long gameStartsSoonSent = -1; public WebServerClient(WebServer server, Socket socket) { // default initialization for clients that do not send this setClientVersion(0); this.server = server; this.cst = new WebServerClientSocketThread(this, socket); } public void startThread() { cst.start(); } public WebServerClientSocketThread getWSCSThread() { return cst; } private void setClientVersion(int version) { clientVersion = version; } public int getClientVersion() { return clientVersion; } private void setUser(User u) { this.user = u; } User getUser() { return this.user; } String getUsername() { if (user != null) { return user.getName(); } else { return "<username undefined?>"; } } public void setUnverifiedUsername(String name) { this.unverifiedUsername = name; } public String getUnverifiedUsername() { return unverifiedUsername; } public void requestPingIfNeeded(long now) { cst.requestPingIfNeeded(now); } public void requestPingNow() { cst.requestPingNow(); } public void setLoggedIn(boolean val) { this.loggedIn = val; } public boolean getLoggedIn() { return this.loggedIn; } public void handleLogout() { // if login did not succeed (wrong password, or duplicate name and without force), // user will still be null; skip all this here then: if (user != null) { LOGGER.info("Handling logout for user " + getUsername()); user.updateLastLogout(); server.writeBackUsers(); if (user.getWebserverClient() == this) { // after here, user is not in loggedInUsersList any more, i.e. // game updates during game cancelling are NOT sent to him. user.setWebClient(null); server.updateLoggedinStatus(user, null); if (!cst.wasForcedLogout()) { server.cancelIfNecessary(user); } setUser(null); } else { String name = ((WebServerClient)user.getWebserverClient()) .getUsername(); LOGGER.warning("handleLogout called for a " + "different WebServerClient: " + name + " than ourself."); } } if (server != null) { server.updateUserCounts(); server = null; } } public boolean parseLine(String fromClient) { boolean done = false; boolean ok = true; String reason = null; GameInfo gi = null; String[] tokens = fromClient.split(sep); String command = tokens[0]; if (!command.equals(IWebServer.PingResponse)) { cst.clearIdleWarningsSent(); } if (user == null && unverifiedUsername == null) { unverifiedUsername = "<unknown>"; } if (!loggedIn && command.equals(IWebServer.Login)) { ok = false; if (tokens.length >= 4) { String username = tokens[1]; unverifiedUsername = username; String password = tokens[2]; boolean force = Boolean.valueOf(tokens[3]).booleanValue(); if (tokens.length >= 5) { // Only clients version 1 or later send this, // it remains 0 (default initialization). setClientVersion(Integer.parseInt(tokens[4])); } LOGGER.info("User " + username + " attempts login with client version " + clientVersion); if (clientVersion < 2) { reason = "Your Colossus software is too old. Please upgrade to Release 0.10.3!"; } else { reason = server.verifyLogin(username, password); } if (reason == null) { reason = ensureNotAlreadyLoggedIn(username, force); } // login accepted if (reason == null) { setUser(server.findUserByName(username)); loggedIn = true; user.updateLastLogin(); server.writeBackUsers(); ok = true; user.setWebClient(this); server.updateLoggedinStatus(user, this); cst.setName("WSCST " + username); LOGGER.info("User successfully logged in: " + cst.getClientInfo()); } else { LOGGER.info("Login for " + unverifiedUsername + " not accepted, setting done to true."); ok = false; done = true; } } else { LOGGER.log(Level.FINEST, "A client attempted login with too few arguments."); reason = "Username, password or 'force' parameter is missing."; ok = false; done = true; } if (!ok) { // Login rejected - close connection immediately: done = true; } } else if (!loggedIn && command.equals(IWebServer.ConfirmRegistration)) { ok = false; if (tokens.length >= 3) { String username = tokens[1]; unverifiedUsername = username; String confCode = tokens[2]; reason = server.confirmRegistration(username, confCode); if (reason == null) { ok = true; } } else { reason = "Username or confirmation code missing."; ok = false; } done = true; } else if (!loggedIn && command.equals(IWebServer.RegisterUser)) { ok = false; if (tokens.length >= 4) { String username = tokens[1]; unverifiedUsername = username; String password = tokens[2]; String email = tokens[3]; reason = server.registerUser(username, password, email); // TODO in practice this can never be true. // in best/standard case it will be PROV_CONFCODE if (reason == null) { ok = true; } } else { reason = "Username, password or email missing."; ok = false; } done = true; } else if (!loggedIn) { ok = false; reason = "You are not logged in"; done = true; } else if (command.equals(IWebServer.Logout)) { LOGGER.info("Received Logout request from user " + cst.getClientInfo()); ok = true; done = true; } else if (command.equals(IWebServer.Propose)) { String initiator = tokens[1]; String variant = tokens[2]; String viewmode = tokens[3]; long startAt = Long.parseLong(tokens[4]); int duration = Integer.parseInt(tokens[5]); String summary = tokens[6]; String expire = tokens[7]; int nmin = Integer.parseInt(tokens[10]); int ntarget = Integer.parseInt(tokens[11]); int nmax = Integer.parseInt(tokens[12]); List<String> gameOptions = new ArrayList<String>(); List<String> teleportOptions = new ArrayList<String>(); if (getClientVersion() >= WebClient.WC_VERSION_SUPPORTS_EXTRA_OPTIONS) { String gameOptionsString = tokens[8]; gameOptions.addAll(Split.split(Glob.sep, gameOptionsString)); String tpOptionsString = tokens[9]; teleportOptions.addAll(Split.split(Glob.sep, tpOptionsString)); } else { // earlier arg 8+9 were mulligans and tower options booleans if (Boolean.valueOf(tokens[8]).booleanValue()) { gameOptions.add(Options.unlimitedMulligans); } if (Boolean.valueOf(tokens[9]).booleanValue()) { gameOptions.add(Options.balancedTowers); } } gi = server.proposeGame(initiator, variant, viewmode, startAt, duration, summary, expire, gameOptions, teleportOptions, nmin, ntarget, nmax); } else if (command.equals(IWebServer.Enroll)) { String gameId = tokens[1]; String username = tokens[2]; server.enrollUserToGame(gameId, username); } else if (command.equals(IWebServer.Unenroll)) { String gameId = tokens[1]; String username = tokens[2]; server.unenrollUserFromGame(gameId, username); } else if (command.equals(IWebServer.Cancel)) { String gameId = tokens[1]; String byUser = tokens[2]; server.cancelGame(gameId, byUser); } else if (command.equals(IWebServer.Start)) { String gameId = tokens[1]; User byUser = user; if (tokens.length >= 3) { String byUserName = tokens[2]; if (byUserName.equalsIgnoreCase(user.getName())) { byUserName = user.getName(); } if (!byUserName.equals(user.getName())) { LOGGER.warning("startGame received byUserName is '" + byUserName + "', but received from user '" + user.getName() + "'?!?"); } else { byUser = server.findUserByName(byUserName); } } server.startGame(gameId, byUser); } else if (command.equals(IWebServer.Resume)) { String gameId = tokens[1]; String filename = tokens[2]; User byUser = server.findUserByName(tokens[3]); server.resumeGame(gameId, filename, byUser); } else if (command.equals(IWebServer.DeleteSuspendedGame)) { String gameId = tokens[1]; User user = server.findUserByName(tokens[2]); server.deleteSuspendedGame(gameId, user); } else if (command.equals(IWebServer.StartAtPlayer)) { String gameId = tokens[1]; String hostingPlayer = tokens[2]; String playerHost = tokens[3]; int port = Integer.parseInt(tokens[4]); server.startGameOnPlayerHost(gameId, hostingPlayer, playerHost, port); } else if (command.equals(IWebServer.StartedByPlayer)) { String gameId = tokens[1]; server.informStartedByPlayer(gameId); } else if (command.equals(IWebServer.LocallyGameOver)) { String gameId = tokens[1]; server.informLocallyGameOver(gameId); } else if (command.equals(IWebServer.ChatSubmit)) { String chatId = tokens[1]; String sender = tokens[2]; String message = tokens[3]; processChatLine(chatId, sender, message); } else if (command.equals(IWebServer.ChangePassword)) { String username = tokens[1]; String oldPassword = tokens[2]; String newPassword = tokens[3]; String email = null; Boolean isAdminObj = null; reason = server.changeProperties(username, oldPassword, newPassword, email, isAdminObj); if (reason != null) { ok = false; } } else if (command.equals(IWebServer.ShutdownServer)) { if (user.isAdmin()) { cst.createStopper(new Runnable() { public void run() { server.initiateShutdown(user.getName()); } }); } else { LOGGER.log(Level.INFO, "Non-admin user " + user.getName() + " requested shutdown - ignored."); } } else if (command.equals(IWebServer.RequestUserAttention)) { if (user.isAdmin()) { long when = Long.parseLong(tokens[1]); String sender = tokens[2]; boolean isAdmin = Boolean.valueOf(tokens[3]).booleanValue(); String recipient = tokens[4]; String message = tokens[5]; int beepCount = Integer.parseInt(tokens[6]); long beepInterval = Long.parseLong(tokens[7]); boolean windows = Boolean.valueOf(tokens[8]).booleanValue(); server.requestUserAttention(when, sender, isAdmin, recipient, message, beepCount, beepInterval, windows); } else { LOGGER.warning("Non-admin user " + user.getName() + " attempted to use RequestUserAttention method!"); } } else if (command.equals(IWebServer.WatchGame)) { String gameId = tokens[1]; String userName = tokens[2]; server.watchGame(gameId, userName); } else if (command.equals(IWebServer.DumpInfo)) { server.dumpInfo(); } else if (command.equals(IWebServer.PingResponse)) { long requestSentTime = Long.parseLong(tokens[1]); int counter = Integer.parseInt(tokens[2]); String name = getUsername(); long requestResponseArriveTime = new Date().getTime(); long roundtripTime = requestResponseArriveTime - requestSentTime; String msg = "Received ping response #" + counter + " from user " + name + ", request roundtrip time is " + roundtripTime + " ms."; if (roundtripTime > 3000) { LOGGER.warning(msg); } else { LOGGER.fine(msg); } cst.storeEntry(requestResponseArriveTime, roundtripTime); } else if (command.equals(IWebServer.ConfirmCommand)) { long now = new Date().getTime(); // long confirmationSentTime = Long.parseLong(tokens[1]); String cmd = tokens[2]; /* String arg1 = tokens[3]; String arg2 = tokens[4]; String arg3 = tokens[5]; */ long cmdRTT = 0; if (cmd.equals(gameStartsSoon)) { if (gameStartsSoonSent != -1) { cmdRTT = now - gameStartsSoonSent; } else { LOGGER.warning("Got ConfirmCommand " + cmd + " but no cmdSent time set!"); } gameStartsSoonSent = -1; } if (cmd.equals(gameStartsNow)) { if (gameStartsNowSent != -1) { cmdRTT = now - gameStartsNowSent; } else { LOGGER.warning("Got ConfirmCommand " + cmd + " but no cmdSent time set!"); } gameStartsNowSent = -1; } LOGGER.info("Got confirmCommand for command " + cmd + " - time between cmd sent and conf got is " + cmdRTT); } else if (command.equals(IWebServer.RereadLoginMessage)) { if (user.isAdmin()) { server.rereadLoginMessage(); } else { LOGGER.log(Level.INFO, "Non-admin user " + user.getName() + " used rereadLoginMessage command - ignored."); } } else if (command.equals(IWebServer.Echo)) { if (user.isAdmin()) { sendToClient(fromClient); } else { LOGGER.log(Level.INFO, "Non-admin user " + user.getName() + " used echo command - ignored."); } } else if (command.equals(IWebServer.MessageToAdmin)) { long when = Long.parseLong(tokens[1]); String fromUser = tokens[2]; String fromMail = tokens[3]; String globbedMessage = tokens[4]; List<String> message = Split.split(Glob.sep, globbedMessage); server.messageToAdmin(when, fromUser, fromMail, message); } else { LOGGER.log(Level.WARNING, "Unexpected command '" + command + "' from client"); ok = false; } /* ============================================================ * Act based on the ok or not ok value * ============================================================ */ if (ok) { if (command.equals(IWebServer.Logout)) { // Client cannot handle both the ACK:Logout and // ConnectionClosed before connection is gone... // so don't send a ACK for this one. } else { // TODO: do we really need to send ACK for *every* line we // got from client?!? sendToClient("ACK: " + command + sep + reason); } } else { LOGGER.log(Level.FINE, "NACK: " + command + sep + reason); sendToClient("NACK: " + command + sep + reason); } // after them connection will be closed. Make sure everything // from output queue is written first. if (!ok && command.equals(IWebServer.Login) || command.equals(IWebServer.RegisterUser) || command.equals(IWebServer.ConfirmRegistration)) { cst.flushMessages(); } // TODO: why is this done after the if-elseif, and not inside the // proposeGame block? // (perhaps because client needs the ACK first??) if (command.equals(IWebServer.Propose)) { if (gi != null) { server.enrollUserToGame(gi.getGameId(), user.getName()); } else { long now = new Date().getTime(); requestAttention(now, "SYSTEM", true, "Don't click 'Propose' multiple times!", 1, 500, true); } } if (ok && loggedIn && command.equals(IWebServer.Login)) { LOGGER.log(Level.FINEST, "a new user logged in, sending proposed Games"); if (user.isAdmin()) { grantAdminStatus(); } server.tellAllProposedGamesToOne(this); server.tellAllRunningGamesToOne(this); if (clientVersion >= WebClient.WC_VERSION_RESUME) { server.tellAllSuspendedGamesToOne(this); } tellOwnInfo(getUser().getEmail()); server.tellLastChatMessagesToOne(this, IWebServer.generalChatName); server.sendMessageOfTheDayToOne(this, IWebServer.generalChatName); if (clientVersion < WebClient.WC_VERSION_RESUME) { server.sendOldVersionWarningToOne(this, getUsername(), IWebServer.generalChatName); } server.reEnrollIfNecessary(this); server.updateUserCounts(); LOGGER.info("loggedIn postprocessing for user " + user.getName() + " completed!"); // just before readline() command will print a log message that // thread is now back available to read input from client cst.setLastWasLogin(); // Request a Ping, so we see when client was earliest able to respond requestPingNow(); if (user.getName().equalsIgnoreCase(unverifiedUsername) && !user.getName().equals(unverifiedUsername)) { List<String> lines = makeCaseMismatchWarning(unverifiedUsername); String message = "NOTE: Login name case (upper/lower) mismatch. See explanation in chat window!"; long when = new Date().getTime(); server.requestUserAttention(when, "SYSTEM", false, user.getName(), message, 1, 500, true); server.getGeneralChat().sendLinesToClient( IWebServer.generalChatName, this, lines, true, ""); } } server.saveGamesIfNeeded(); return done; } private List<String> makeCaseMismatchWarning(String name) { ArrayList<String> lines = new ArrayList<String>(); lines.add("NOTE:"); lines.add("Currently you are logged in as '" + name + "', "); lines.add("but the official name with which you registered to " + "the server is '" + user.getName() + "'" + " (there is a difference in lowercase/uppercase)."); lines.add("Please adjust the name in 'Login id' field in the 'Server'" + " tab to use the correct name to login!"); lines.add("(=> Logout, correct the name, and Login once again - " + "a successful login stores the name and password for future " + "sessions.)"); lines.add(""); lines.add("Thanks, your friendly CPGS admin!"); return lines; } public void processChatLine(String chatId, String sender, String message) { if (!chatId.equals(IWebServer.generalChatName)) { LOGGER.log(Level.WARNING, "Chat for chatId " + chatId + " not implemented."); return; } LOGGER.finest("Chat msg from user " + sender + ": " + message); String msgAllLower = message.toLowerCase(); if (msgAllLower.startsWith("/?") || msgAllLower.startsWith("/h") || msgAllLower.startsWith("/help")) { server.getGeneralChat() .sendHelpToClient(msgAllLower, chatId, this); } else if (msgAllLower.startsWith("/ping \"")) { server.handlePingQuotedName(sender, message); } else if (msgAllLower.startsWith("/userinfo")) { server.getGeneralChat().handleShowInfo(this, this.user); } else if (msgAllLower.startsWith("/ping")) { server.handlePing(sender, message); } else if (msgAllLower.startsWith("/contact")) { server.getGeneralChat().showContactHelp(chatId, this); } else if (msgAllLower.startsWith("/ignore") || msgAllLower.startsWith("/ingore")) { server.getGeneralChat().handleIgnore(msgAllLower, this.user); } else if (msgAllLower.startsWith("/unignore") || msgAllLower.startsWith("/uningore")) { server.getGeneralChat().handleUnignore(msgAllLower, this.user); } else if (msgAllLower.startsWith("/")) { server.getGeneralChat().handleUnknownCommand(msgAllLower, chatId, this, message); } else { server.chatSubmit(chatId, sender, message); } } /** * if password is okay, check first whether same user is already * logged in with another connection; if yes, * when force is not set (1st try), send back the "already logged in"; * reacting on that, client will prompt whether to force the old * connection out, and if user answers yes, will send a 2nd login * message, this time with force flag set. */ private String ensureNotAlreadyLoggedIn(String username, boolean force) { String reason = null; // Do not set the real user here, otherwise in the re-login case // the first reject would lead to autoCancelling games, too. WebServerClientSocketThread otherCst = null; User tmpUser = server.findUserByName(username); WebServerClient otherWsc = (WebServerClient)tmpUser .getWebserverClient(); if (otherWsc != null) { otherCst = otherWsc.getWSCSThread(); } if (otherCst != null) { if (force) { LOGGER.fine("User " + username + " already logged in (" + otherCst + ") - forcing Logout"); otherCst.forceLogout(otherCst); } else { LOGGER.fine("User " + username + " already logged in (" + otherCst + ") " + "- replying with alreadyLoggedIn reject message"); reason = IWebClient.alreadyLoggedIn; } } else { LOGGER.finest("ok, " + username + " is not logged in at the moment"); } return reason; } public void systemMessage(long now, String message) { if (getClientVersion() >= WebClient.WC_VERSION_GENERAL_MESSAGE) { // probably should/could use deliverGeneralMessage() here, // don't dare to change it right now chatDeliver(IWebServer.generalChatName, now, "SYSTEM", message, false); // sendToClient(systemMessage + sep + now + sep + message); } else { chatDeliver(IWebServer.generalChatName, now, "SYSTEM", message, false); } } private void sendToClient(String s) { cst.sendToClient(s); } public void grantAdminStatus() { sendToClient(grantAdmin); } public void didEnroll(String gameId, String username) { sendToClient(didEnroll + sep + gameId + sep + username); } public void didUnenroll(String gameId, String username) { sendToClient(didUnenroll + sep + gameId + sep + username); } public void gameCancelled(String gameId, String byUser) { sendToClient(gameCancelled + sep + gameId + sep + byUser); } public void userInfo(int loggedin, int enrolled, int playing, int dead, long ago, String text) { sendToClient(userInfo + sep + loggedin + sep + enrolled + sep + playing + sep + dead + sep + ago + sep + text); } public void tellOwnInfo(String email) { sendToClient(tellOwnInfo + sep + email); } public void gameInfo(GameInfo gi) { String giString; giString = gi.toStringCheckClientVersion(getUsername(), getClientVersion(), sep); sendToClient(gameInfo + sep + giString); } public void gameStartsSoon(String gameId, String byUser) { gameStartsSoonSent = new Date().getTime(); sendToClient(gameStartsSoon + sep + gameId + sep + byUser); // long spentTime = new Date().getTime() - gameStartsSoonSent; // LOGGER.finest("Sending gameStartsSoon to " + getUsername() + " took " // + spentTime + " milliseconds."); } public void gameStartsNow(String gameId, int port, String hostingHost, int inactivityCheckInterval, int inactivityWarningInterval, int inactivityTimeout) { gameStartsNowSent = new Date().getTime(); sendToClient(gameStartsNow + sep + gameId + sep + port + sep + hostingHost + sep + inactivityCheckInterval + sep + inactivityWarningInterval + sep + inactivityTimeout); // long spentTime = new Date().getTime() - gameStartsNowSent; // LOGGER.info("Sending gameStartsNow to " + getUsername() + " took " // + spentTime + " milliseconds."); } public void chatDeliver(String chatId, long when, String sender, String message, boolean resent) { // LOGGER.log(Level.FINEST, "chatDeliver() to client " + user.getName() // + ": " + chatId + ", " + sender + ": " + message); sendToClient(chatDeliver + sep + chatId + sep + when + sep + sender + sep + message + sep + resent); } public void deliverGeneralMessage(long when, boolean error, String title, String message) { sendToClient(generalMessage + sep + when + sep + error + sep + title + sep + message); } public void requestAttention(long when, String byUser, boolean byAdmin, String message, int beepCount, long beepInterval, boolean windows) { sendToClient(requestAttention + sep + when + sep + byUser + sep + byAdmin + sep + message + sep + beepCount + sep + beepInterval + sep + windows); } public void watchGameInfo(String gameId, String host, int port) { sendToClient(watchGameInfo + sep + gameId + sep + host + sep + port); } // TODO should this be rather totally in clientsocketthread? public void requestPing(String arg1, String arg2, String arg3) { sendToClient(pingRequest + sep + arg1 + sep + arg2 + sep + arg3); } public void connectionReset(boolean forcedLogout) { // Not really needed here, only on client side. // Only implemented to satisfy the interface } }