tools/hw2irc/net/ercatec/hw/ProtocolConnection.java
changeset 15784 823cf18be1fc
equal deleted inserted replaced
15782:6409d756e9da 15784:823cf18be1fc
       
     1 /*
       
     2  * Java net client for Hedgewars, a free turn based strategy game
       
     3  * Copyright (c) 2011 Richard Karolyi <sheepluva@ercatec.net>
       
     4  *
       
     5  * This program is free software; you can redistribute it and/or modify
       
     6  * it under the terms of the GNU General Public License as published by
       
     7  * the Free Software Foundation; version 2 of the License
       
     8  *
       
     9  * This program is distributed in the hope that it will be useful,
       
    10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
       
    11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
       
    12  * GNU General Public License for more details.
       
    13  *
       
    14  * You should have received a copy of the GNU General Public License
       
    15  * along with this program; if not, write to the Free Software
       
    16  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
       
    17  */
       
    18 
       
    19 package net.ercatec.hw;
       
    20 
       
    21 import java.lang.*;
       
    22 import java.lang.IllegalArgumentException;
       
    23 import java.lang.Runnable;
       
    24 import java.lang.Thread;
       
    25 import java.io.*;
       
    26 import java.net.*;
       
    27 import java.util.ArrayList;
       
    28 import java.util.Arrays;
       
    29 import java.util.List;
       
    30 import java.util.Scanner;
       
    31 import java.util.Vector;
       
    32 // for auth
       
    33 import java.math.BigInteger;
       
    34 import java.security.MessageDigest;
       
    35 import java.security.SecureRandom;
       
    36 import java.security.NoSuchAlgorithmException;
       
    37 
       
    38 public final class ProtocolConnection implements Runnable
       
    39 {
       
    40     private static final String DEFAULT_HOST = "netserver.hedgewars.org";
       
    41     private static final int DEFAULT_PORT = 46631;
       
    42     private static final String PROTOCOL_VERSION = "53";
       
    43 
       
    44     private final Socket socket;
       
    45     private BufferedReader fromSvr;
       
    46     private PrintWriter toSvr;
       
    47     private final INetClient netClient;
       
    48     private boolean quit;
       
    49     private boolean debug;
       
    50 
       
    51     private final String host;
       
    52     private final int port;
       
    53 
       
    54     private String nick;
       
    55 
       
    56     public ProtocolConnection(INetClient netClient) throws Exception {
       
    57         this(netClient, DEFAULT_HOST);
       
    58     }
       
    59 
       
    60     public ProtocolConnection(INetClient netClient, String host) throws Exception {
       
    61         this(netClient, host, DEFAULT_PORT);
       
    62     }
       
    63 
       
    64     public ProtocolConnection(INetClient netClient, String host, int port) throws Exception {
       
    65         this.netClient = netClient;
       
    66         this.host = host;
       
    67         this.port = port;
       
    68         this.nick = nick = "";
       
    69         this.quit = false;
       
    70 
       
    71         fromSvr = null;
       
    72         toSvr = null;
       
    73 
       
    74         try {
       
    75             socket = new Socket(host, port);
       
    76             fromSvr = new BufferedReader(new InputStreamReader(socket.getInputStream()));
       
    77             toSvr = new PrintWriter(socket.getOutputStream(), true);
       
    78         }
       
    79         catch(Exception ex) {
       
    80             throw ex;
       
    81         }
       
    82 
       
    83         ProtocolMessage firstMsg = processNextMessage();
       
    84         if (firstMsg.getType() != ProtocolMessage.Type.CONNECTED) {
       
    85             closeConnection();
       
    86             throw new Exception("First Message wasn't CONNECTED.");
       
    87         }
       
    88 
       
    89     }
       
    90 
       
    91     public void run() {
       
    92 
       
    93         try {
       
    94             while (!quit) {
       
    95                 processNextMessage();
       
    96             }
       
    97         }
       
    98         catch(Exception ex) {
       
    99             netClient.logError("FATAL: Run loop died unexpectedly!");
       
   100             ex.printStackTrace();
       
   101             handleConnectionLoss();
       
   102         }
       
   103 
       
   104         // only gets here when connection was closed
       
   105     }
       
   106 
       
   107     public void processMessages() {
       
   108         processMessages(false);
       
   109     }
       
   110 
       
   111     public Thread processMessages(boolean inNewThread)
       
   112     {
       
   113         if (inNewThread)
       
   114             return new Thread(this);
       
   115 
       
   116         run();
       
   117         return null;
       
   118     }
       
   119 
       
   120     public void processNextClientFlagsMessages()
       
   121     {
       
   122         while (!quit) {
       
   123             if (!processNextMessage(true).isValid)
       
   124                 break;
       
   125         }
       
   126     }
       
   127 
       
   128     private ProtocolMessage processNextMessage() {
       
   129         return processNextMessage(false);
       
   130     }
       
   131 
       
   132     private void handleConnectionLoss() {
       
   133         closeConnection();
       
   134         netClient.onConnectionLoss();
       
   135     }
       
   136 
       
   137     public void close() {
       
   138         this.closeConnection();
       
   139     }
       
   140 
       
   141     private synchronized void closeConnection() {
       
   142         if (quit)
       
   143             return;
       
   144 
       
   145         quit = true;
       
   146         try {
       
   147             if (fromSvr != null)
       
   148                 fromSvr.close();
       
   149         } catch(Exception ex) {};
       
   150         try {
       
   151             if (toSvr != null)
       
   152                 toSvr.close();
       
   153         } catch(Exception ex) {};
       
   154         try {
       
   155             socket.close();
       
   156         } catch(Exception ex) {};
       
   157     }
       
   158 
       
   159     private String resumeLine = "";
       
   160 
       
   161     private ProtocolMessage processNextMessage(boolean onlyIfClientFlags)
       
   162     {
       
   163         String line;
       
   164         final List<String> parts = new ArrayList<String>(32);
       
   165 
       
   166         while (!quit) {
       
   167 
       
   168             if (!resumeLine.isEmpty()) {
       
   169                     line = resumeLine;
       
   170                     resumeLine = "";
       
   171                 }
       
   172             else {
       
   173                 try {
       
   174                     line = fromSvr.readLine();
       
   175                     
       
   176                     if (onlyIfClientFlags && (parts.size() == 0)
       
   177                         && !line.equals("CLIENT_FLAGS")) {
       
   178                             resumeLine = line;
       
   179                             // return invalid message
       
   180                             return new ProtocolMessage();
       
   181                         }
       
   182                 }
       
   183                 catch(Exception whoops) {
       
   184                     handleConnectionLoss();
       
   185                     break;
       
   186                 }
       
   187             }
       
   188 
       
   189             if (line == null) {
       
   190                 handleConnectionLoss();
       
   191                 // return invalid message
       
   192                 return new ProtocolMessage();
       
   193             }
       
   194 
       
   195             if (!quit && line.isEmpty()) {
       
   196 
       
   197                 if (parts.size() > 0) {
       
   198 
       
   199                     ProtocolMessage msg = new ProtocolMessage(parts);
       
   200 
       
   201                     netClient.logDebug("Server: " + msg.toString());
       
   202 
       
   203                     if (!msg.isValid()) {
       
   204                         netClient.onMalformedMessage(msg.toString());
       
   205                         if (msg.getType() != ProtocolMessage.Type.BYE)
       
   206                             continue;
       
   207                     }
       
   208 
       
   209                     final String[] args = msg.getArguments();
       
   210                     netClient.sanitizeInputs(args);
       
   211 
       
   212 
       
   213                     final int argc = args.length;
       
   214 
       
   215                     try {
       
   216                         switch (msg.getType()) {
       
   217 
       
   218                             case PING:
       
   219                                 netClient.onPing();
       
   220                                 break;
       
   221 
       
   222                             case LOBBY__JOINED:
       
   223                                 try {
       
   224                                     assertAuthNotIncomplete();
       
   225                                 }
       
   226                                 catch (Exception ex) {
       
   227                                     disconnect();
       
   228                                     netClient.onDisconnect(ex.getMessage());
       
   229                                 }
       
   230                                 netClient.onLobbyJoin(args);
       
   231                                 break;
       
   232 
       
   233                             case LOBBY__LEFT:
       
   234                                 netClient.onLobbyLeave(args[0], args[1]);
       
   235                                 break;
       
   236 
       
   237                             case CLIENT_FLAGS:
       
   238                                 String user;
       
   239                                 final String flags = args[0];
       
   240                                 if (flags.length() < 2) {
       
   241                                     //netClient.onMalformedMessage(msg.toString());
       
   242                                     break;
       
   243                                 }
       
   244                                 final char mode = flags.charAt(0);
       
   245                                 if ((mode != '-') && (mode != '+')) {
       
   246                                     //netClient.onMalformedMessage(msg.toString());
       
   247                                     break;
       
   248                                 }
       
   249 
       
   250                                 final int l = flags.length();
       
   251 
       
   252                                 for (int i = 1; i < l; i++) {
       
   253                                     // set flag type
       
   254                                     final INetClient.UserFlagType flag;
       
   255                                     // TODO support more flags
       
   256                                     switch (flags.charAt(i)) {
       
   257                                         case 'a':
       
   258                                             flag = INetClient.UserFlagType.ADMIN;
       
   259                                             break;
       
   260                                         case 'i':
       
   261                                             flag = INetClient.UserFlagType.INROOM;
       
   262                                             break;
       
   263                                         case 'u':
       
   264                                             flag = INetClient.UserFlagType.REGISTERED;
       
   265                                             break;
       
   266                                         default:
       
   267                                             flag = INetClient.UserFlagType.UNKNOWN;
       
   268                                             break;
       
   269                                         }
       
   270 
       
   271                                     for (int j = 1; j < args.length; j++) {
       
   272                                         netClient.onUserFlagChange(args[j], flag, mode=='+');
       
   273                                     }
       
   274                                 }
       
   275                                 break;
       
   276 
       
   277                             case CHAT:
       
   278                                 netClient.onChat(args[0], args[1]);
       
   279                                 break;
       
   280 
       
   281                             case INFO:
       
   282                                 netClient.onUserInfo(args[0], args[1], args[2], args[3]);
       
   283                                 break;
       
   284 
       
   285                             case PONG:
       
   286                                 netClient.onPong();
       
   287                                 break;
       
   288 
       
   289                             case NICK:
       
   290                                 final String newNick = args[0];
       
   291                                 if (!newNick.equals(this.nick)) {
       
   292                                     this.nick = newNick;
       
   293                                 }
       
   294                                     netClient.onNickSet(this.nick);
       
   295                                 sendMessage(new String[] { "PROTO", PROTOCOL_VERSION });
       
   296                                 break;
       
   297 
       
   298                             case NOTICE:
       
   299                                 // nickname collision
       
   300                                 if (args[0].equals("0"))
       
   301                                     setNick(netClient.onNickCollision(this.nick));
       
   302                                 break;
       
   303 
       
   304                             case ASKPASSWORD:
       
   305                                 try {
       
   306                                     final String pwHash = netClient.onPasswordHashNeededForAuth();
       
   307                                     doAuthPart1(pwHash, args[0]);
       
   308                                 }
       
   309                                 catch (Exception ex) {
       
   310                                     disconnect();
       
   311                                     netClient.onDisconnect(ex.getMessage());
       
   312                                 }
       
   313                                 break;
       
   314 
       
   315                             case ROOMS:
       
   316                                 final int nf = ProtocolMessage.ROOM_FIELD_COUNT;
       
   317                                 for (int a = 0; a < argc; a += nf) {
       
   318                                     handleRoomInfo(args[a+1], Arrays.copyOfRange(args, a, a + nf));
       
   319                                 }
       
   320 
       
   321                             case ROOM_ADD:
       
   322                                 handleRoomInfo(args[1], args);
       
   323                                 break;
       
   324 
       
   325                             case ROOM_DEL:
       
   326                                 netClient.onRoomDel(args[0]);
       
   327                                 break;
       
   328 
       
   329                             case ROOM_UPD:
       
   330                                 handleRoomInfo(args[0], Arrays.copyOfRange(args, 1, args.length));
       
   331                                 break;
       
   332 
       
   333                             case BYE:
       
   334                                 closeConnection();
       
   335                                 if (argc > 0)
       
   336                                     netClient.onDisconnect(args[0]);
       
   337                                 else
       
   338                                     netClient.onDisconnect("");
       
   339                                 break;
       
   340 
       
   341                             case SERVER_AUTH:
       
   342                                 try {
       
   343                                     doAuthPart2(args[0]);
       
   344                                 }
       
   345                                 catch (Exception ex) {
       
   346                                     disconnect();
       
   347                                     netClient.onDisconnect(ex.getMessage());
       
   348                                 }
       
   349                                 break;
       
   350                         }
       
   351                         // end of message
       
   352                         return msg;
       
   353                     }
       
   354                     catch(IllegalArgumentException ex) {
       
   355 
       
   356                         netClient.logError("Illegal arguments! "
       
   357                             + ProtocolMessage.partsToString(parts)
       
   358                             + "caused: " + ex.getMessage());
       
   359 
       
   360                         return new ProtocolMessage();
       
   361                     }
       
   362                 }
       
   363             }
       
   364             else
       
   365             {
       
   366                 parts.add(line);
       
   367             }
       
   368         }
       
   369 
       
   370         netClient.logError("WARNING: Message wasn't parsed correctly: "
       
   371                             + ProtocolMessage.partsToString(parts));
       
   372         // return invalid message
       
   373         return new ProtocolMessage(); // never to be reached
       
   374     }
       
   375 
       
   376     private void handleRoomInfo(final String name, final String[] info) throws IllegalArgumentException
       
   377     {
       
   378         // TODO room flags enum array
       
   379 
       
   380         final int nUsers;
       
   381         final int nTeams;
       
   382         
       
   383         try {
       
   384             nUsers = Integer.parseInt(info[2]);
       
   385         }
       
   386         catch(IllegalArgumentException ex) {
       
   387             throw new IllegalArgumentException(
       
   388                 "Player count is not an valid integer!",
       
   389                 ex);
       
   390         }
       
   391 
       
   392         try {
       
   393             nTeams = Integer.parseInt(info[3]);
       
   394         }
       
   395         catch(IllegalArgumentException ex) {
       
   396             throw new IllegalArgumentException(
       
   397                 "Team count is not an valid integer!",
       
   398                 ex);
       
   399         }
       
   400 
       
   401         netClient.onRoomInfo(name, info[0], info[1], nUsers, nTeams,
       
   402                              info[4], info[5], info[6], info[7], info[8]);
       
   403     }
       
   404 
       
   405     private static final String AUTH_SALT = PROTOCOL_VERSION + "!hedgewars";
       
   406     private static final int PASSWORD_HASH_LENGTH = 32;
       
   407     public static final int SERVER_SALT_MIN_LENGTH = 16;
       
   408     private static final String AUTH_ALG = "SHA-1";
       
   409     private String serverAuthHash = "";
       
   410 
       
   411     private void assertAuthNotIncomplete() throws Exception {
       
   412         if (!serverAuthHash.isEmpty()) {
       
   413             netClient.logError("AUTH-ERROR: assertAuthNotIncomplete() found that authentication was not completed!");
       
   414             throw new Exception("Authentication was not finished properly!");
       
   415         }
       
   416         serverAuthHash = "";
       
   417     }
       
   418 
       
   419     private void doAuthPart2(final String serverAuthHash) throws Exception {
       
   420         if (!this.serverAuthHash.equals(serverAuthHash)) {
       
   421             netClient.logError("AUTH-ERROR: Server's authentication hash is incorrect!");
       
   422             throw new Exception("Server failed mutual authentication! (wrong hash provided by server)");
       
   423         }
       
   424         netClient.logDebug("Auth: Mutual authentication successful.");
       
   425         this.serverAuthHash = "";
       
   426     }
       
   427 
       
   428     private void doAuthPart1(final String pwHash, final String serverSalt) throws Exception {
       
   429         if ((pwHash == null) || pwHash.isEmpty()) {
       
   430             netClient.logDebug("AUTH: Password required, but no password hash was provided.");
       
   431             throw new Exception("Auth: Password needed, but none specified.");
       
   432         }
       
   433         if (pwHash.length() != PASSWORD_HASH_LENGTH) {
       
   434             netClient.logError("AUTH-ERROR: Your password hash has an unexpected length! Should be "
       
   435                                + PASSWORD_HASH_LENGTH + " but is " + pwHash.length()
       
   436                               );
       
   437             throw new Exception("Auth: Your password hash length seems wrong.");
       
   438         }
       
   439         if (serverSalt.length() < SERVER_SALT_MIN_LENGTH) {
       
   440             netClient.logError("AUTH-ERROR: Salt provided by server is too short! Should be at least "
       
   441                                + SERVER_SALT_MIN_LENGTH + " but is " + serverSalt.length()
       
   442                               );
       
   443             throw new Exception("Auth: Server violated authentication protocol! (auth salt too short)");
       
   444         }
       
   445 
       
   446         final MessageDigest sha1Digest;
       
   447 
       
   448         try {
       
   449              sha1Digest = MessageDigest.getInstance(AUTH_ALG);
       
   450         }
       
   451         catch(NoSuchAlgorithmException ex) {
       
   452             netClient.logError("AUTH-ERROR: Algorithm required for authentication ("
       
   453                                       + AUTH_ALG + ") not available!"
       
   454                                      );
       
   455             return;
       
   456         } 
       
   457         
       
   458 
       
   459         // generate 130 bit base32 encoded value
       
   460         // base32 = 5bits/char => 26 chars, which is more than min req
       
   461         final String clientSalt =
       
   462             new BigInteger(130, new SecureRandom()).toString(32);
       
   463 
       
   464         final String saltedPwHash =
       
   465             clientSalt + serverSalt + pwHash + AUTH_SALT;
       
   466 
       
   467         final String saltedPwHash2 =
       
   468             serverSalt + clientSalt + pwHash + AUTH_SALT;
       
   469 
       
   470         final String clientAuthHash =
       
   471             new BigInteger(1, sha1Digest.digest(saltedPwHash.getBytes("UTF-8"))).toString(16);
       
   472 
       
   473         serverAuthHash =
       
   474             new BigInteger(1, sha1Digest.digest(saltedPwHash2.getBytes("UTF-8"))).toString(16);
       
   475 
       
   476         sendMessage(new String[] { "PASSWORD", clientAuthHash, clientSalt });
       
   477 
       
   478 /* When we got password hash, and server asked us for a password, perform mutual authentication:
       
   479  * at this point we have salt chosen by server
       
   480  * client sends client salt and hash of secret (password hash) salted with client salt, server salt,
       
   481  * and static salt (predefined string + protocol number)
       
   482  * server should respond with hash of the same set in different order.
       
   483 
       
   484     if(m_passwordHash.isEmpty() || m_serverSalt.isEmpty())
       
   485         return;
       
   486 
       
   487     QString hash = QCryptographicHash::hash(
       
   488                 m_clientSalt.toAscii()
       
   489                 .append(m_serverSalt.toAscii())
       
   490                 .append(m_passwordHash)
       
   491                 .append(cProtoVer->toAscii())
       
   492                 .append("!hedgewars")
       
   493                 , QCryptographicHash::Sha1).toHex();
       
   494 
       
   495     m_serverHash = QCryptographicHash::hash(
       
   496                 m_serverSalt.toAscii()
       
   497                 .append(m_clientSalt.toAscii())
       
   498                 .append(m_passwordHash)
       
   499                 .append(cProtoVer->toAscii())
       
   500                 .append("!hedgewars")
       
   501                 , QCryptographicHash::Sha1).toHex();
       
   502 
       
   503     RawSendNet(QString("PASSWORD%1%2%1%3").arg(delimiter).arg(hash).arg(m_clientSalt));
       
   504 
       
   505 Server:  ("ASKPASSWORD", "5S4q9Dd0Qrn1PNsxymtRhupN") 
       
   506 Client:  ("PASSWORD", "297a2b2f8ef83bcead4056b4df9313c27bb948af", "{cc82f4ca-f73c-469d-9ab7-9661bffeabd1}") 
       
   507 Server:  ("SERVER_AUTH", "06ecc1cc23b2c9ebd177a110b149b945523752ae") 
       
   508 
       
   509  */
       
   510     }
       
   511 
       
   512     public void sendCommand(final String command)
       
   513     {
       
   514         String cmd = command;
       
   515 
       
   516         // don't execute empty commands
       
   517         if (cmd.length() < 1)
       
   518             return;
       
   519 
       
   520         // replace all newlines since they violate protocol
       
   521         cmd = cmd.replace('\n', ' ');
       
   522 
       
   523         // parameters are separated by one or more spaces.
       
   524         final String[] parts = cmd.split(" +");
       
   525 
       
   526         // command is always CAPS
       
   527         parts[0] = parts[0].toUpperCase();
       
   528 
       
   529         sendMessage(parts);
       
   530     }
       
   531 
       
   532     public void sendPing()
       
   533     {
       
   534         sendMessage("PING");
       
   535     }
       
   536 
       
   537     public void sendPong()
       
   538     {
       
   539         sendMessage("PONG");
       
   540     }
       
   541 
       
   542     private void sendMessage(final String msg)
       
   543     {
       
   544         sendMessage(new String[] { msg });
       
   545     }
       
   546 
       
   547     private void sendMessage(final String[] parts)
       
   548     {
       
   549         if (quit)
       
   550             return;
       
   551 
       
   552         netClient.logDebug("Client: " + messagePartsToString(parts));
       
   553 
       
   554         boolean malformed = false;
       
   555         String msg = "";
       
   556 
       
   557         for (final String part : parts)
       
   558         {
       
   559             msg += part + '\n';
       
   560             if (part.isEmpty() || (part.indexOf('\n') >= 0)) {
       
   561                 malformed = true;
       
   562                 break;
       
   563             }
       
   564         }
       
   565 
       
   566         if (malformed) {
       
   567             netClient.onMalformedMessage(messagePartsToString(parts));
       
   568             return;
       
   569         }
       
   570 
       
   571         try {
       
   572             toSvr.print(msg + '\n'); // don't use println, since we always want '\n'
       
   573             toSvr.flush();
       
   574         }
       
   575         catch(Exception ex) {
       
   576             netClient.logError("FATAL: Couldn't send message! " + ex.getMessage());
       
   577             ex.printStackTrace();
       
   578             handleConnectionLoss();
       
   579         }
       
   580     }
       
   581 
       
   582     private String messagePartsToString(String[] parts) {
       
   583 
       
   584         if (parts.length == 0)
       
   585             return "([empty message])";
       
   586 
       
   587         String result = "(\"" + parts[0] + '"';
       
   588         for (int i=1; i < parts.length; i++)
       
   589         {
       
   590             result += ", \"" + parts[i] + '"';
       
   591         }
       
   592         result += ')';
       
   593 
       
   594         return result;
       
   595     }
       
   596 
       
   597     public void disconnect() {
       
   598         sendMessage(new String[] { "QUIT", "Client quit" });
       
   599         closeConnection();
       
   600     }
       
   601 
       
   602     public void disconnect(final String reason) {
       
   603         sendMessage(new String[] { "QUIT", reason.isEmpty()?"-":reason });
       
   604         closeConnection();
       
   605     }
       
   606 
       
   607     public void sendChat(String message) {
       
   608 
       
   609         String[] lines = message.split("\n");
       
   610 
       
   611         for (String line : lines)
       
   612         {
       
   613             if (!message.trim().isEmpty())
       
   614                 sendMessage(new String[] { "CHAT", line });
       
   615         }
       
   616     }
       
   617 
       
   618     public void joinRoom(final String roomName) {
       
   619 
       
   620         sendMessage(new String[] { "JOIN_ROOM", roomName });
       
   621     }
       
   622 
       
   623     public void leaveRoom(final String roomName) {
       
   624 
       
   625         sendMessage("PART");
       
   626     }
       
   627 
       
   628     public void requestInfo(final String user) {
       
   629 
       
   630         sendMessage(new String[] { "INFO", user });
       
   631     }
       
   632 
       
   633     public void setNick(final String nick) {
       
   634 
       
   635         this.nick = nick;
       
   636         sendMessage(new String[] { "NICK", nick });
       
   637     }
       
   638 
       
   639     public void kick(final String nick) {
       
   640 
       
   641         sendMessage(new String[] { "KICK", nick });
       
   642     }
       
   643 
       
   644     public void requestRoomsList() {
       
   645 
       
   646         sendMessage("LIST");
       
   647     }
       
   648 }
       
   649