tools/hw2irc/net/ercatec/hw/ProtocolConnection.java
changeset 15784 823cf18be1fc
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tools/hw2irc/net/ercatec/hw/ProtocolConnection.java	Wed May 26 16:32:43 2021 -0400
@@ -0,0 +1,649 @@
+/*
+ * Java net client for Hedgewars, a free turn based strategy game
+ * Copyright (c) 2011 Richard Karolyi <sheepluva@ercatec.net>
+ *
+ * This program 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 2 of the License
+ *
+ * This program 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
+ */
+
+package net.ercatec.hw;
+
+import java.lang.*;
+import java.lang.IllegalArgumentException;
+import java.lang.Runnable;
+import java.lang.Thread;
+import java.io.*;
+import java.net.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Scanner;
+import java.util.Vector;
+// for auth
+import java.math.BigInteger;
+import java.security.MessageDigest;
+import java.security.SecureRandom;
+import java.security.NoSuchAlgorithmException;
+
+public final class ProtocolConnection implements Runnable
+{
+    private static final String DEFAULT_HOST = "netserver.hedgewars.org";
+    private static final int DEFAULT_PORT = 46631;
+    private static final String PROTOCOL_VERSION = "53";
+
+    private final Socket socket;
+    private BufferedReader fromSvr;
+    private PrintWriter toSvr;
+    private final INetClient netClient;
+    private boolean quit;
+    private boolean debug;
+
+    private final String host;
+    private final int port;
+
+    private String nick;
+
+    public ProtocolConnection(INetClient netClient) throws Exception {
+        this(netClient, DEFAULT_HOST);
+    }
+
+    public ProtocolConnection(INetClient netClient, String host) throws Exception {
+        this(netClient, host, DEFAULT_PORT);
+    }
+
+    public ProtocolConnection(INetClient netClient, String host, int port) throws Exception {
+        this.netClient = netClient;
+        this.host = host;
+        this.port = port;
+        this.nick = nick = "";
+        this.quit = false;
+
+        fromSvr = null;
+        toSvr = null;
+
+        try {
+            socket = new Socket(host, port);
+            fromSvr = new BufferedReader(new InputStreamReader(socket.getInputStream()));
+            toSvr = new PrintWriter(socket.getOutputStream(), true);
+        }
+        catch(Exception ex) {
+            throw ex;
+        }
+
+        ProtocolMessage firstMsg = processNextMessage();
+        if (firstMsg.getType() != ProtocolMessage.Type.CONNECTED) {
+            closeConnection();
+            throw new Exception("First Message wasn't CONNECTED.");
+        }
+
+    }
+
+    public void run() {
+
+        try {
+            while (!quit) {
+                processNextMessage();
+            }
+        }
+        catch(Exception ex) {
+            netClient.logError("FATAL: Run loop died unexpectedly!");
+            ex.printStackTrace();
+            handleConnectionLoss();
+        }
+
+        // only gets here when connection was closed
+    }
+
+    public void processMessages() {
+        processMessages(false);
+    }
+
+    public Thread processMessages(boolean inNewThread)
+    {
+        if (inNewThread)
+            return new Thread(this);
+
+        run();
+        return null;
+    }
+
+    public void processNextClientFlagsMessages()
+    {
+        while (!quit) {
+            if (!processNextMessage(true).isValid)
+                break;
+        }
+    }
+
+    private ProtocolMessage processNextMessage() {
+        return processNextMessage(false);
+    }
+
+    private void handleConnectionLoss() {
+        closeConnection();
+        netClient.onConnectionLoss();
+    }
+
+    public void close() {
+        this.closeConnection();
+    }
+
+    private synchronized void closeConnection() {
+        if (quit)
+            return;
+
+        quit = true;
+        try {
+            if (fromSvr != null)
+                fromSvr.close();
+        } catch(Exception ex) {};
+        try {
+            if (toSvr != null)
+                toSvr.close();
+        } catch(Exception ex) {};
+        try {
+            socket.close();
+        } catch(Exception ex) {};
+    }
+
+    private String resumeLine = "";
+
+    private ProtocolMessage processNextMessage(boolean onlyIfClientFlags)
+    {
+        String line;
+        final List<String> parts = new ArrayList<String>(32);
+
+        while (!quit) {
+
+            if (!resumeLine.isEmpty()) {
+                    line = resumeLine;
+                    resumeLine = "";
+                }
+            else {
+                try {
+                    line = fromSvr.readLine();
+                    
+                    if (onlyIfClientFlags && (parts.size() == 0)
+                        && !line.equals("CLIENT_FLAGS")) {
+                            resumeLine = line;
+                            // return invalid message
+                            return new ProtocolMessage();
+                        }
+                }
+                catch(Exception whoops) {
+                    handleConnectionLoss();
+                    break;
+                }
+            }
+
+            if (line == null) {
+                handleConnectionLoss();
+                // return invalid message
+                return new ProtocolMessage();
+            }
+
+            if (!quit && line.isEmpty()) {
+
+                if (parts.size() > 0) {
+
+                    ProtocolMessage msg = new ProtocolMessage(parts);
+
+                    netClient.logDebug("Server: " + msg.toString());
+
+                    if (!msg.isValid()) {
+                        netClient.onMalformedMessage(msg.toString());
+                        if (msg.getType() != ProtocolMessage.Type.BYE)
+                            continue;
+                    }
+
+                    final String[] args = msg.getArguments();
+                    netClient.sanitizeInputs(args);
+
+
+                    final int argc = args.length;
+
+                    try {
+                        switch (msg.getType()) {
+
+                            case PING:
+                                netClient.onPing();
+                                break;
+
+                            case LOBBY__JOINED:
+                                try {
+                                    assertAuthNotIncomplete();
+                                }
+                                catch (Exception ex) {
+                                    disconnect();
+                                    netClient.onDisconnect(ex.getMessage());
+                                }
+                                netClient.onLobbyJoin(args);
+                                break;
+
+                            case LOBBY__LEFT:
+                                netClient.onLobbyLeave(args[0], args[1]);
+                                break;
+
+                            case CLIENT_FLAGS:
+                                String user;
+                                final String flags = args[0];
+                                if (flags.length() < 2) {
+                                    //netClient.onMalformedMessage(msg.toString());
+                                    break;
+                                }
+                                final char mode = flags.charAt(0);
+                                if ((mode != '-') && (mode != '+')) {
+                                    //netClient.onMalformedMessage(msg.toString());
+                                    break;
+                                }
+
+                                final int l = flags.length();
+
+                                for (int i = 1; i < l; i++) {
+                                    // set flag type
+                                    final INetClient.UserFlagType flag;
+                                    // TODO support more flags
+                                    switch (flags.charAt(i)) {
+                                        case 'a':
+                                            flag = INetClient.UserFlagType.ADMIN;
+                                            break;
+                                        case 'i':
+                                            flag = INetClient.UserFlagType.INROOM;
+                                            break;
+                                        case 'u':
+                                            flag = INetClient.UserFlagType.REGISTERED;
+                                            break;
+                                        default:
+                                            flag = INetClient.UserFlagType.UNKNOWN;
+                                            break;
+                                        }
+
+                                    for (int j = 1; j < args.length; j++) {
+                                        netClient.onUserFlagChange(args[j], flag, mode=='+');
+                                    }
+                                }
+                                break;
+
+                            case CHAT:
+                                netClient.onChat(args[0], args[1]);
+                                break;
+
+                            case INFO:
+                                netClient.onUserInfo(args[0], args[1], args[2], args[3]);
+                                break;
+
+                            case PONG:
+                                netClient.onPong();
+                                break;
+
+                            case NICK:
+                                final String newNick = args[0];
+                                if (!newNick.equals(this.nick)) {
+                                    this.nick = newNick;
+                                }
+                                    netClient.onNickSet(this.nick);
+                                sendMessage(new String[] { "PROTO", PROTOCOL_VERSION });
+                                break;
+
+                            case NOTICE:
+                                // nickname collision
+                                if (args[0].equals("0"))
+                                    setNick(netClient.onNickCollision(this.nick));
+                                break;
+
+                            case ASKPASSWORD:
+                                try {
+                                    final String pwHash = netClient.onPasswordHashNeededForAuth();
+                                    doAuthPart1(pwHash, args[0]);
+                                }
+                                catch (Exception ex) {
+                                    disconnect();
+                                    netClient.onDisconnect(ex.getMessage());
+                                }
+                                break;
+
+                            case ROOMS:
+                                final int nf = ProtocolMessage.ROOM_FIELD_COUNT;
+                                for (int a = 0; a < argc; a += nf) {
+                                    handleRoomInfo(args[a+1], Arrays.copyOfRange(args, a, a + nf));
+                                }
+
+                            case ROOM_ADD:
+                                handleRoomInfo(args[1], args);
+                                break;
+
+                            case ROOM_DEL:
+                                netClient.onRoomDel(args[0]);
+                                break;
+
+                            case ROOM_UPD:
+                                handleRoomInfo(args[0], Arrays.copyOfRange(args, 1, args.length));
+                                break;
+
+                            case BYE:
+                                closeConnection();
+                                if (argc > 0)
+                                    netClient.onDisconnect(args[0]);
+                                else
+                                    netClient.onDisconnect("");
+                                break;
+
+                            case SERVER_AUTH:
+                                try {
+                                    doAuthPart2(args[0]);
+                                }
+                                catch (Exception ex) {
+                                    disconnect();
+                                    netClient.onDisconnect(ex.getMessage());
+                                }
+                                break;
+                        }
+                        // end of message
+                        return msg;
+                    }
+                    catch(IllegalArgumentException ex) {
+
+                        netClient.logError("Illegal arguments! "
+                            + ProtocolMessage.partsToString(parts)
+                            + "caused: " + ex.getMessage());
+
+                        return new ProtocolMessage();
+                    }
+                }
+            }
+            else
+            {
+                parts.add(line);
+            }
+        }
+
+        netClient.logError("WARNING: Message wasn't parsed correctly: "
+                            + ProtocolMessage.partsToString(parts));
+        // return invalid message
+        return new ProtocolMessage(); // never to be reached
+    }
+
+    private void handleRoomInfo(final String name, final String[] info) throws IllegalArgumentException
+    {
+        // TODO room flags enum array
+
+        final int nUsers;
+        final int nTeams;
+        
+        try {
+            nUsers = Integer.parseInt(info[2]);
+        }
+        catch(IllegalArgumentException ex) {
+            throw new IllegalArgumentException(
+                "Player count is not an valid integer!",
+                ex);
+        }
+
+        try {
+            nTeams = Integer.parseInt(info[3]);
+        }
+        catch(IllegalArgumentException ex) {
+            throw new IllegalArgumentException(
+                "Team count is not an valid integer!",
+                ex);
+        }
+
+        netClient.onRoomInfo(name, info[0], info[1], nUsers, nTeams,
+                             info[4], info[5], info[6], info[7], info[8]);
+    }
+
+    private static final String AUTH_SALT = PROTOCOL_VERSION + "!hedgewars";
+    private static final int PASSWORD_HASH_LENGTH = 32;
+    public static final int SERVER_SALT_MIN_LENGTH = 16;
+    private static final String AUTH_ALG = "SHA-1";
+    private String serverAuthHash = "";
+
+    private void assertAuthNotIncomplete() throws Exception {
+        if (!serverAuthHash.isEmpty()) {
+            netClient.logError("AUTH-ERROR: assertAuthNotIncomplete() found that authentication was not completed!");
+            throw new Exception("Authentication was not finished properly!");
+        }
+        serverAuthHash = "";
+    }
+
+    private void doAuthPart2(final String serverAuthHash) throws Exception {
+        if (!this.serverAuthHash.equals(serverAuthHash)) {
+            netClient.logError("AUTH-ERROR: Server's authentication hash is incorrect!");
+            throw new Exception("Server failed mutual authentication! (wrong hash provided by server)");
+        }
+        netClient.logDebug("Auth: Mutual authentication successful.");
+        this.serverAuthHash = "";
+    }
+
+    private void doAuthPart1(final String pwHash, final String serverSalt) throws Exception {
+        if ((pwHash == null) || pwHash.isEmpty()) {
+            netClient.logDebug("AUTH: Password required, but no password hash was provided.");
+            throw new Exception("Auth: Password needed, but none specified.");
+        }
+        if (pwHash.length() != PASSWORD_HASH_LENGTH) {
+            netClient.logError("AUTH-ERROR: Your password hash has an unexpected length! Should be "
+                               + PASSWORD_HASH_LENGTH + " but is " + pwHash.length()
+                              );
+            throw new Exception("Auth: Your password hash length seems wrong.");
+        }
+        if (serverSalt.length() < SERVER_SALT_MIN_LENGTH) {
+            netClient.logError("AUTH-ERROR: Salt provided by server is too short! Should be at least "
+                               + SERVER_SALT_MIN_LENGTH + " but is " + serverSalt.length()
+                              );
+            throw new Exception("Auth: Server violated authentication protocol! (auth salt too short)");
+        }
+
+        final MessageDigest sha1Digest;
+
+        try {
+             sha1Digest = MessageDigest.getInstance(AUTH_ALG);
+        }
+        catch(NoSuchAlgorithmException ex) {
+            netClient.logError("AUTH-ERROR: Algorithm required for authentication ("
+                                      + AUTH_ALG + ") not available!"
+                                     );
+            return;
+        } 
+        
+
+        // generate 130 bit base32 encoded value
+        // base32 = 5bits/char => 26 chars, which is more than min req
+        final String clientSalt =
+            new BigInteger(130, new SecureRandom()).toString(32);
+
+        final String saltedPwHash =
+            clientSalt + serverSalt + pwHash + AUTH_SALT;
+
+        final String saltedPwHash2 =
+            serverSalt + clientSalt + pwHash + AUTH_SALT;
+
+        final String clientAuthHash =
+            new BigInteger(1, sha1Digest.digest(saltedPwHash.getBytes("UTF-8"))).toString(16);
+
+        serverAuthHash =
+            new BigInteger(1, sha1Digest.digest(saltedPwHash2.getBytes("UTF-8"))).toString(16);
+
+        sendMessage(new String[] { "PASSWORD", clientAuthHash, clientSalt });
+
+/* When we got password hash, and server asked us for a password, perform mutual authentication:
+ * at this point we have salt chosen by server
+ * client sends client salt and hash of secret (password hash) salted with client salt, server salt,
+ * and static salt (predefined string + protocol number)
+ * server should respond with hash of the same set in different order.
+
+    if(m_passwordHash.isEmpty() || m_serverSalt.isEmpty())
+        return;
+
+    QString hash = QCryptographicHash::hash(
+                m_clientSalt.toAscii()
+                .append(m_serverSalt.toAscii())
+                .append(m_passwordHash)
+                .append(cProtoVer->toAscii())
+                .append("!hedgewars")
+                , QCryptographicHash::Sha1).toHex();
+
+    m_serverHash = QCryptographicHash::hash(
+                m_serverSalt.toAscii()
+                .append(m_clientSalt.toAscii())
+                .append(m_passwordHash)
+                .append(cProtoVer->toAscii())
+                .append("!hedgewars")
+                , QCryptographicHash::Sha1).toHex();
+
+    RawSendNet(QString("PASSWORD%1%2%1%3").arg(delimiter).arg(hash).arg(m_clientSalt));
+
+Server:  ("ASKPASSWORD", "5S4q9Dd0Qrn1PNsxymtRhupN") 
+Client:  ("PASSWORD", "297a2b2f8ef83bcead4056b4df9313c27bb948af", "{cc82f4ca-f73c-469d-9ab7-9661bffeabd1}") 
+Server:  ("SERVER_AUTH", "06ecc1cc23b2c9ebd177a110b149b945523752ae") 
+
+ */
+    }
+
+    public void sendCommand(final String command)
+    {
+        String cmd = command;
+
+        // don't execute empty commands
+        if (cmd.length() < 1)
+            return;
+
+        // replace all newlines since they violate protocol
+        cmd = cmd.replace('\n', ' ');
+
+        // parameters are separated by one or more spaces.
+        final String[] parts = cmd.split(" +");
+
+        // command is always CAPS
+        parts[0] = parts[0].toUpperCase();
+
+        sendMessage(parts);
+    }
+
+    public void sendPing()
+    {
+        sendMessage("PING");
+    }
+
+    public void sendPong()
+    {
+        sendMessage("PONG");
+    }
+
+    private void sendMessage(final String msg)
+    {
+        sendMessage(new String[] { msg });
+    }
+
+    private void sendMessage(final String[] parts)
+    {
+        if (quit)
+            return;
+
+        netClient.logDebug("Client: " + messagePartsToString(parts));
+
+        boolean malformed = false;
+        String msg = "";
+
+        for (final String part : parts)
+        {
+            msg += part + '\n';
+            if (part.isEmpty() || (part.indexOf('\n') >= 0)) {
+                malformed = true;
+                break;
+            }
+        }
+
+        if (malformed) {
+            netClient.onMalformedMessage(messagePartsToString(parts));
+            return;
+        }
+
+        try {
+            toSvr.print(msg + '\n'); // don't use println, since we always want '\n'
+            toSvr.flush();
+        }
+        catch(Exception ex) {
+            netClient.logError("FATAL: Couldn't send message! " + ex.getMessage());
+            ex.printStackTrace();
+            handleConnectionLoss();
+        }
+    }
+
+    private String messagePartsToString(String[] parts) {
+
+        if (parts.length == 0)
+            return "([empty message])";
+
+        String result = "(\"" + parts[0] + '"';
+        for (int i=1; i < parts.length; i++)
+        {
+            result += ", \"" + parts[i] + '"';
+        }
+        result += ')';
+
+        return result;
+    }
+
+    public void disconnect() {
+        sendMessage(new String[] { "QUIT", "Client quit" });
+        closeConnection();
+    }
+
+    public void disconnect(final String reason) {
+        sendMessage(new String[] { "QUIT", reason.isEmpty()?"-":reason });
+        closeConnection();
+    }
+
+    public void sendChat(String message) {
+
+        String[] lines = message.split("\n");
+
+        for (String line : lines)
+        {
+            if (!message.trim().isEmpty())
+                sendMessage(new String[] { "CHAT", line });
+        }
+    }
+
+    public void joinRoom(final String roomName) {
+
+        sendMessage(new String[] { "JOIN_ROOM", roomName });
+    }
+
+    public void leaveRoom(final String roomName) {
+
+        sendMessage("PART");
+    }
+
+    public void requestInfo(final String user) {
+
+        sendMessage(new String[] { "INFO", user });
+    }
+
+    public void setNick(final String nick) {
+
+        this.nick = nick;
+        sendMessage(new String[] { "NICK", nick });
+    }
+
+    public void kick(final String nick) {
+
+        sendMessage(new String[] { "KICK", nick });
+    }
+
+    public void requestRoomsList() {
+
+        sendMessage("LIST");
+    }
+}
+