--- /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");
+ }
+}
+