|
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 |