package games.strategy.net; import java.io.IOException; import java.io.Serializable; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.channels.ClosedChannelException; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; import games.strategy.engine.chat.ChatController; import games.strategy.engine.chat.IChatChannel; import games.strategy.engine.lobby.server.login.LobbyLoginValidator; import games.strategy.engine.lobby.server.userDB.MutedMacController; import games.strategy.engine.lobby.server.userDB.MutedUsernameController; import games.strategy.engine.message.HubInvoke; import games.strategy.engine.message.RemoteMethodCall; import games.strategy.engine.message.RemoteName; import games.strategy.engine.message.SpokeInvoke; import games.strategy.net.nio.NIOSocket; import games.strategy.net.nio.NIOSocketListener; import games.strategy.net.nio.QuarantineConversation; import games.strategy.net.nio.ServerQuarantineConversation; /** * A Messenger that can have many clients connected to it. */ public class ServerMessenger implements IServerMessenger, NIOSocketListener { private static Logger logger = Logger.getLogger(ServerMessenger.class.getName()); private final Selector acceptorSelector; private final ServerSocketChannel socketChannel; private final Node node; private boolean shutdown = false; private final NIOSocket nioSocket; private final List<IMessageListener> listeners = new CopyOnWriteArrayList<>(); private final List<IMessengerErrorListener> errorListeners = new CopyOnWriteArrayList<>(); private final List<IConnectionChangeListener> connectionListeners = new CopyOnWriteArrayList<>(); private boolean acceptNewConnection = false; private ILoginValidator loginValidator; // all our nodes private final Map<INode, SocketChannel> nodeToChannel = new ConcurrentHashMap<>(); private final Map<SocketChannel, INode> channelToNode = new ConcurrentHashMap<>(); // A hack, till I think of something better public ServerMessenger(final String name, final int portNumber, final IObjectStreamFactory streamFactory) throws IOException { socketChannel = ServerSocketChannel.open(); socketChannel.configureBlocking(false); socketChannel.socket().setReuseAddress(true); socketChannel.socket().bind(new InetSocketAddress(portNumber), 10); nioSocket = new NIOSocket(streamFactory, this, "Server"); acceptorSelector = Selector.open(); if (IPFinder.findInetAddress() != null) { node = new Node(name, IPFinder.findInetAddress(), portNumber); } else { node = new Node(name, InetAddress.getLocalHost(), portNumber); } final Thread t = new Thread(new ConnectionHandler(), "Server Messenger Connection Handler"); t.start(); } @Override public void setLoginValidator(final ILoginValidator loginValidator) { this.loginValidator = loginValidator; } @Override public ILoginValidator getLoginValidator() { return loginValidator; } /** Creates new ServerMessenger. */ public ServerMessenger(final String name, final int portNumber) throws IOException { this(name, portNumber, new DefaultObjectStreamFactory()); } @Override public void addMessageListener(final IMessageListener listener) { listeners.add(listener); } @Override public void removeMessageListener(final IMessageListener listener) { listeners.remove(listener); } /** * Get a list of nodes. */ @Override public Set<INode> getNodes() { final Set<INode> rVal = new HashSet<>(nodeToChannel.keySet()); rVal.add(node); return rVal; } @Override public synchronized void shutDown() { if (!shutdown) { shutdown = true; nioSocket.shutDown(); try { socketChannel.close(); } catch (final Exception e) { // ignore } if (acceptorSelector != null) { acceptorSelector.wakeup(); } } } public synchronized boolean isShutDown() { return shutdown; } @Override public boolean isConnected() { return !shutdown; } /** * Send a message to the given node. */ @Override public void send(final Serializable msg, final INode to) { if (shutdown) { return; } if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "Sending" + msg + " to:" + to); } final MessageHeader header = new MessageHeader(to, node, msg); final SocketChannel socketChannel = nodeToChannel.get(to); // the socket was removed if (socketChannel == null) { if (logger.isLoggable(Level.FINER)) { logger.log(Level.FINER, "no channel for node:" + to + " dropping message:" + msg); } // the socket has not been added yet return; } nioSocket.send(socketChannel, header); } /** * Send a message to all nodes. */ @Override public void broadcast(final Serializable msg) { final MessageHeader header = new MessageHeader(node, msg); forwardBroadcast(header); } private boolean isLobby() { return loginValidator instanceof LobbyLoginValidator; } private boolean isGame() { return !isLobby(); } private final Object m_cachedListLock = new Object(); private final HashMap<String, String> m_cachedMacAddresses = new HashMap<>(); @Override public String getPlayerMac(final String name) { synchronized (m_cachedListLock) { String mac = m_cachedMacAddresses.get(name); if (mac == null) { mac = m_playersThatLeftMacs_Last10.get(name); } return mac; } } // We need to cache whether players are muted, because otherwise the database would have to be accessed each time a // message was sent, // which can be very slow private final List<String> m_liveMutedUsernames = new ArrayList<>(); public boolean IsUsernameMuted(final String username) { synchronized (m_cachedListLock) { return m_liveMutedUsernames.contains(username); } } @Override public void NotifyUsernameMutingOfPlayer(final String username, final Date muteExpires) { synchronized (m_cachedListLock) { if (!m_liveMutedUsernames.contains(username)) { m_liveMutedUsernames.add(username); } if (muteExpires != null) { ScheduleUsernameUnmuteAt(username, muteExpires.getTime()); } } } @Override public void NotifyIPMutingOfPlayer(final String ip, final Date muteExpires) { // TODO: remove if no backwards compat issues } private final List<String> m_liveMutedMacAddresses = new ArrayList<>(); public boolean IsMacMuted(final String mac) { synchronized (m_cachedListLock) { return m_liveMutedMacAddresses.contains(mac); } } @Override public void NotifyMacMutingOfPlayer(final String mac, final Date muteExpires) { synchronized (m_cachedListLock) { if (!m_liveMutedMacAddresses.contains(mac)) { m_liveMutedMacAddresses.add(mac); } if (muteExpires != null) { ScheduleMacUnmuteAt(mac, muteExpires.getTime()); } } } private void ScheduleUsernameUnmuteAt(final String username, final long checkTime) { final Timer unmuteUsernameTimer = new Timer("Username unmute timer"); unmuteUsernameTimer.schedule(getUsernameUnmuteTask(username), new Date(checkTime)); } private void ScheduleMacUnmuteAt(final String mac, final long checkTime) { final Timer unmuteMacTimer = new Timer("Mac unmute timer"); unmuteMacTimer.schedule(getMacUnmuteTask(mac), new Date(checkTime)); } // TODO: remove 'ip' parameter if can confirm no backwards compat issues public void NotifyPlayerLogin(final String uniquePlayerName, final String ip, final String mac) { synchronized (m_cachedListLock) { m_cachedMacAddresses.put(uniquePlayerName, mac); if (isLobby()) { final String realName = uniquePlayerName.split(" ")[0]; if (!m_liveMutedUsernames.contains(realName)) { final long muteTill = new MutedUsernameController().getUsernameUnmuteTime(realName); if (muteTill != -1 && muteTill <= System.currentTimeMillis()) { // Signal the player as muted m_liveMutedUsernames.add(realName); ScheduleUsernameUnmuteAt(realName, muteTill); } } if (!m_liveMutedMacAddresses.contains(mac)) { final long muteTill = new MutedMacController().getMacUnmuteTime(mac); if (muteTill != -1 && muteTill <= System.currentTimeMillis()) { // Signal the player as muted m_liveMutedMacAddresses.add(mac); ScheduleMacUnmuteAt(mac, muteTill); } } } } } private final HashMap<String, String> m_playersThatLeftMacs_Last10 = new HashMap<>(); public HashMap<String, String> getPlayersThatLeftMacs_Last10() { return m_playersThatLeftMacs_Last10; } private void NotifyPlayerRemoval(final INode node) { synchronized (m_cachedListLock) { m_playersThatLeftMacs_Last10.put(node.getName(), m_cachedMacAddresses.get(node.getName())); if (m_playersThatLeftMacs_Last10.size() > 10) { m_playersThatLeftMacs_Last10.remove(m_playersThatLeftMacs_Last10.entrySet().iterator().next().toString()); } m_cachedMacAddresses.remove(node.getName()); } } // Special character to stop spoofing by server public static final String YOU_HAVE_BEEN_MUTED_LOBBY = "?YOUR LOBBY CHATTING HAS BEEN TEMPORARILY 'MUTED' BY THE ADMINS, TRY AGAIN LATER"; // Special character to stop spoofing by host public static final String YOU_HAVE_BEEN_MUTED_GAME = "?YOUR CHATTING IN THIS GAME HAS BEEN 'MUTED' BY THE HOST"; @Override public void messageReceived(final MessageHeader msg, final SocketChannel channel) { final INode expectedReceive = channelToNode.get(channel); if (!expectedReceive.equals(msg.getFrom())) { throw new IllegalStateException("Expected: " + expectedReceive + " not: " + msg.getFrom()); } if (msg.getMessage() instanceof HubInvoke) { // Chat messages are always HubInvoke's if (isLobby() && ((HubInvoke) msg.getMessage()).call.getRemoteName().equals("_ChatCtrl_LOBBY_CHAT")) { final String realName = msg.getFrom().getName().split(" ")[0]; if (IsUsernameMuted(realName)) { bareBonesSendChatMessage(YOU_HAVE_BEEN_MUTED_LOBBY, msg.getFrom()); return; } else if (IsMacMuted(getPlayerMac(msg.getFrom().getName()))) { bareBonesSendChatMessage(YOU_HAVE_BEEN_MUTED_LOBBY, msg.getFrom()); return; } } else if (isGame() && ((HubInvoke) msg.getMessage()).call.getRemoteName() .equals("_ChatCtrlgames.strategy.engine.framework.ui.ServerStartup.CHAT_NAME")) { final String realName = msg.getFrom().getName().split(" ")[0]; if (IsUsernameMuted(realName)) { bareBonesSendChatMessage(YOU_HAVE_BEEN_MUTED_GAME, msg.getFrom()); return; } if (IsMacMuted(getPlayerMac(msg.getFrom().getName()))) { bareBonesSendChatMessage(YOU_HAVE_BEEN_MUTED_GAME, msg.getFrom()); return; } } } if (msg.getFor() == null) { forwardBroadcast(msg); notifyListeners(msg); } else if (msg.getFor().equals(node)) { notifyListeners(msg); } else { forward(msg); } } private void bareBonesSendChatMessage(final String message, final INode to) { final List<Object> args = new ArrayList<>(); final Class<? extends Object>[] argTypes = new Class<?>[1]; args.add(message); argTypes[0] = args.get(0).getClass(); RemoteName rn; if (isLobby()) { rn = new RemoteName(ChatController.getChatChannelName("_LOBBY_CHAT"), IChatChannel.class); } else { rn = new RemoteName( ChatController.getChatChannelName("games.strategy.engine.framework.ui.ServerStartup.CHAT_NAME"), IChatChannel.class); } final RemoteMethodCall call = new RemoteMethodCall(rn.getName(), "chatOccured", args.toArray(), argTypes, rn.getClazz()); final SpokeInvoke spokeInvoke = new SpokeInvoke(null, false, call, getServerNode()); send(spokeInvoke, to); } // The following code is used in hosted lobby games by the host for player mini-banning and mini-muting private final List<String> m_miniBannedUsernames = new ArrayList<>(); @Override public boolean IsUsernameMiniBanned(final String username) { synchronized (m_cachedListLock) { return m_miniBannedUsernames.contains(username); } } @Override public void NotifyUsernameMiniBanningOfPlayer(final String username, final Date expires) { synchronized (m_cachedListLock) { if (!m_miniBannedUsernames.contains(username)) { m_miniBannedUsernames.add(username); } if (expires != null) { final Timer unbanUsernameTimer = new Timer("Username unban timer"); unbanUsernameTimer.schedule(new TimerTask() { @Override public void run() { synchronized (m_cachedListLock) { m_miniBannedUsernames.remove(username); } } }, new Date(expires.getTime())); } } } private final List<String> m_miniBannedIpAddresses = new ArrayList<>(); @Override public boolean IsIpMiniBanned(final String ip) { synchronized (m_cachedListLock) { return m_miniBannedIpAddresses.contains(ip); } } @Override public void NotifyIPMiniBanningOfPlayer(final String ip, final Date expires) { synchronized (m_cachedListLock) { if (!m_miniBannedIpAddresses.contains(ip)) { m_miniBannedIpAddresses.add(ip); } if (expires != null) { final Timer unbanIpTimer = new Timer("IP unban timer"); unbanIpTimer.schedule(new TimerTask() { @Override public void run() { synchronized (m_cachedListLock) { m_miniBannedIpAddresses.remove(ip); } } }, new Date(expires.getTime())); } } } private final List<String> m_miniBannedMacAddresses = new ArrayList<>(); @Override public boolean IsMacMiniBanned(final String mac) { synchronized (m_cachedListLock) { return m_miniBannedMacAddresses.contains(mac); } } @Override public void NotifyMacMiniBanningOfPlayer(final String mac, final Date expires) { synchronized (m_cachedListLock) { if (!m_miniBannedMacAddresses.contains(mac)) { m_miniBannedMacAddresses.add(mac); } if (expires != null) { final Timer unbanMacTimer = new Timer("Mac unban timer"); unbanMacTimer.schedule(new TimerTask() { @Override public void run() { synchronized (m_cachedListLock) { m_miniBannedMacAddresses.remove(mac); } } }, new Date(expires.getTime())); } } } private void forward(final MessageHeader msg) { if (shutdown) { return; } final SocketChannel socketChannel = nodeToChannel.get(msg.getFor()); if (socketChannel == null) { throw new IllegalStateException("No channel for:" + msg.getFor() + " all channels:" + socketChannel); } nioSocket.send(socketChannel, msg); } private void forwardBroadcast(final MessageHeader msg) { if (shutdown) { return; } final SocketChannel fromChannel = nodeToChannel.get(msg.getFrom()); final List<SocketChannel> nodes = new ArrayList<>(nodeToChannel.values()); if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "broadcasting to" + nodes); } for (final SocketChannel channel : nodes) { if (channel != fromChannel) { nioSocket.send(channel, msg); } } } private boolean isNameTaken(final String nodeName) { for (final INode node : getNodes()) { if (node.getName().equalsIgnoreCase(nodeName)) { return true; } } return false; } public String getUniqueName(String currentName) { if (currentName.length() > 50) { currentName = currentName.substring(0, 50); } if (currentName.length() < 2) { currentName = "aa" + currentName; } synchronized (node) { if (isNameTaken(currentName)) { int i = 1; while (true) { final String newName = currentName + " (" + i + ")"; if (!isNameTaken(newName)) { currentName = newName; break; } i++; } } } return currentName; } private void notifyListeners(final MessageHeader msg) { final Iterator<IMessageListener> iter = listeners.iterator(); while (iter.hasNext()) { final IMessageListener listener = iter.next(); listener.messageReceived(msg.getMessage(), msg.getFrom()); } } @Override public void addErrorListener(final IMessengerErrorListener listener) { errorListeners.add(listener); } @Override public void removeErrorListener(final IMessengerErrorListener listener) { errorListeners.remove(listener); } @Override public void addConnectionChangeListener(final IConnectionChangeListener listener) { connectionListeners.add(listener); } @Override public void removeConnectionChangeListener(final IConnectionChangeListener listener) { connectionListeners.remove(listener); } private void notifyConnectionsChanged(final boolean added, final INode node) { final Iterator<IConnectionChangeListener> iter = connectionListeners.iterator(); while (iter.hasNext()) { if (added) { iter.next().connectionAdded(node); } else { iter.next().connectionRemoved(node); } } } @Override public void setAcceptNewConnections(final boolean accept) { acceptNewConnection = accept; } @Override public boolean isAcceptNewConnections() { return acceptNewConnection; } @Override public INode getLocalNode() { return node; } private class ConnectionHandler implements Runnable { @Override public void run() { try { socketChannel.register(acceptorSelector, SelectionKey.OP_ACCEPT); } catch (final ClosedChannelException e) { logger.log(Level.SEVERE, "socket closed", e); shutDown(); } while (!shutdown) { try { acceptorSelector.select(); } catch (final IOException e) { logger.log(Level.SEVERE, "Could not accept on server", e); shutDown(); } if (shutdown) { continue; } final Set<SelectionKey> keys = acceptorSelector.selectedKeys(); final Iterator<SelectionKey> iter = keys.iterator(); while (iter.hasNext()) { final SelectionKey key = iter.next(); iter.remove(); if (key.isAcceptable() && key.isValid()) { final ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel(); // Accept the connection and make it non-blocking SocketChannel socketChannel = null; try { socketChannel = serverSocketChannel.accept(); if (socketChannel == null) { continue; } socketChannel.configureBlocking(false); socketChannel.socket().setKeepAlive(true); } catch (final IOException e) { logger.log(Level.FINE, "Could not accept channel", e); try { if (socketChannel != null) { socketChannel.close(); } } catch (final IOException e2) { logger.log(Level.FINE, "Could not close channel", e2); } continue; } // we are not accepting connections if (!acceptNewConnection) { try { socketChannel.close(); } catch (final IOException e) { logger.log(Level.FINE, "Could not close channel", e); } continue; } final ServerQuarantineConversation conversation = new ServerQuarantineConversation(loginValidator, socketChannel, nioSocket, ServerMessenger.this); nioSocket.add(socketChannel, conversation); } else if (!key.isValid()) { key.cancel(); } } } } } private TimerTask getUsernameUnmuteTask(final String username) { return createUnmuteTimerTask( () -> (isLobby() && new MutedUsernameController().getUsernameUnmuteTime(username) == -1) || (isGame()), () -> m_liveMutedUsernames.remove(username)); } private TimerTask createUnmuteTimerTask(final Supplier<Boolean> runCondition, final Runnable action) { return new TimerTask() { @Override public void run() { if (runCondition.get()) { synchronized (m_cachedListLock) { action.run(); } } } }; } private TimerTask getMacUnmuteTask(final String mac) { return createUnmuteTimerTask( () -> (isLobby() && new MutedMacController().getMacUnmuteTime(mac) == -1) || (isGame()), () -> m_liveMutedMacAddresses.remove(mac)); } @Override public boolean isServer() { return true; } @Override public void removeConnection(final INode nodeToRemove) { if (nodeToRemove.equals(this.node)) { throw new IllegalArgumentException("Cant remove ourself!"); } NotifyPlayerRemoval(nodeToRemove); final SocketChannel channel = nodeToChannel.remove(nodeToRemove); if (channel == null) { logger.info("Could not remove connection to node:" + nodeToRemove); return; } channelToNode.remove(channel); nioSocket.close(channel); notifyConnectionsChanged(false, nodeToRemove); logger.info("Connection removed:" + nodeToRemove); } @Override public INode getServerNode() { return node; } @Override public void socketError(final SocketChannel channel, final Exception error) { if (channel == null) { throw new IllegalArgumentException("Null channel"); } // already closed, dont report it again final INode node = channelToNode.get(channel); if (node != null) { removeConnection(node); } } @Override public void socketUnqaurantined(final SocketChannel channel, final QuarantineConversation conversation) { final ServerQuarantineConversation con = (ServerQuarantineConversation) conversation; final INode remote = new Node(con.getRemoteName(), (InetSocketAddress) channel.socket().getRemoteSocketAddress()); if (logger.isLoggable(Level.FINER)) { logger.log(Level.FINER, "Unquarntined node:" + remote); } nodeToChannel.put(remote, channel); channelToNode.put(channel, remote); notifyConnectionsChanged(true, remote); logger.info("Connection added to:" + remote); } @Override public INode getRemoteNode(final SocketChannel channel) { return channelToNode.get(channel); } @Override public InetSocketAddress getRemoteServerSocketAddress() { return node.getSocketAddress(); } @Override public String toString() { return "ServerMessenger LocalNode:" + node + " ClientNodes:" + nodeToChannel.keySet(); } }