/
TASServer.java
3171 lines (2753 loc) · 128 KB
/
TASServer.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* Created on 2005.6.16
*
*
* ---- INTERNAL CHANGELOG ----
* *** 0.35 ***
* * added 'servermode' argument to TASSERVER command
* *** 0.34 ***
* * message IDs are now actually working
* * added TESTLOGIN, TESTLOGINACCEPT and TESTLOGINDENY commands
* *** 0.33 ***
* * added "update properties" (updateProperties object)
* * added SETLATESTSPRINGVERSION and RELOADUPDATEPROPERTIES commands
* *** 0.32 ***
* * added option to mute by IP
* * replaced CLIENTPORT command with CLIENTOIPPORT command and also
* removed IP field from the ADDUSER command (this way IPs are no longer
* public unless you join a battle that uses nat traversal, where host
* needs to know your IP in order for the nat traversal trick to work)
* *** 0.31 ***
* * added new bot mode for accounts (increases traffic limit when using bot mode)
* *** 0.30 ***
* * added MAPGRADES command
* * added FORCESPECTATORMODE command
* *** 0.26 ***
* * fixed some charset bug
* * added UPDATEMOTD command
* * fixed small bug with JOINBATTLE command not checking if battle is already in-game
* * fixed minor bug with mute entries not expiring on the fly
* * added MUTELISTSTART, MUTELIST, MUTELISTEND commands
* *** 0.25 ***
* * added -LANADMIN switch
* * modified protocol to support arbitrary colors (RGB format)
* *** 0.23 ***
* * channel mute list now gets updated when user renames his account
* *** 0.22 ***
* * added SETCHANNELKEY command, also modified JOIN command to accept extra
* argument for locked channels
* * added FORCELEAVECHANNEL command
* * LEFT command now contains (optional) "reason" parameter
* * replaced CHANNELS command with CHANNEL and ENDOFCHANNELS commands (see
* protocol description)
* * limited maximum length of chat messages
* *** 0.20 ***
* * added CHANGEPASSWORD command
* * GETINGAMETIME now also accepts no argument (to return your own in-game time)
* * CHANNELTOPIC command now includes author name and time
* * added -LOGMAIN switch
* * added GETLASTIP command, FINDIP is available to privileged users as well now
* * fixed bug with /me being available even after being muted
* * CHANNELMESSAGE command now available to moderators as well
* *** 0.195 ***
* * fixed RING command not working for battle hosts
* *** 0.194 ***
* * integrated ploticus graphics generator and a simple web server to give access to server's
* statistics.
* * fixed RING command (only host can ring players participating in his own battle, unless
* the target is host himself)
* * fixed KICK command so that now player who's been kicked is notified about it
* (also kick command accepts "reason" string now)
* * added "GETLASTLOGINTIME" command (for moderators)
* * fixed bug with clients behind same NAT not getting local IPs in certain cases
* * added simple UDP server to help with NAT traversing (see NATHelpServer.java)
* * added UDPSOURCEPORT, CLIENTPORT and HOSTPORT commands (used with NAT traversing)
* *** 0.191 ***
* * fixed bug with server allowing clients to have several battles open at the same time
* *** 0.19 ***
* * improved server code (meaning less "ambigious" commands)
* * added RENAMEACCOUNT command, also username may now contain "[" and "]" characters
* * added CHANNELMESSAGE command
* * added MUTE, UNMUTE and MUTELIST commands
* * clients behind same NAT now get local IPs instead of external one (from the server).
* This should resolve some issues with people playing games behind same NAT.
* * added "agreement"
* *** 0.18 ***
* * multiple mod side support (battle status bits have changed)
* * user who flood are now automatically banned by server
* *** 0.17 ***
* * server now keeps in-game time even after player has reached maximum level
* (max. in-game time server can record is 2^20 minutes)
* * rewrote the network code to use java.nio classes. This fixes several known
* problems with server and also fixes multiplayer replay option.
* * implemented simple anti-flood protection
* * removed old file transfer commands
* *** 0.16 ***
* * added new host option - diminishing metal maker returns
* * switched to Webnet77's ip-to-country database, which seems to be more frequently
* updated: http://software77.net/cgi-bin/ip-country/geo-ip.pl
* * added "locked" parameter to UPDATEBATTLEINFO command
* * added "multiplayer replays" support
* *** 0.152 ***
* * fixed small bug with server not updating rank when maximum level has been reached
* * added ban list
* *** 0.151 ***
* * added OFFERUPDATEEX command
* * added country code support
* * added simple protection against rank exploiters
* * added cpu info (LOGIN command now requires additional parameter)
* * limited usernames/passwords to 20 chars
* *** 0.141 ***
* * fixed issue with server not notifying users about user's rank on login
* * added command: CHANNELTOPIC
* *** 0.14 ***
* * added FORCETEAMCOLOR command
* * fixed bug which allowed users to register accounts with username/password containing
* chars from 43 to 57 (dec), which should be numbers (the correct number range is 48
* to 57). Invalid chars are "+", ",", "-", "." and "/".
* * added ranking system
* *** 0.13 ***
* * added AI support
* * added KICKUSER command (admins only)
* * fixed bug when server did not allow client to change its ally number if someone
* else used it, even if that was only a spectator.
* * added away status bit
* * fixed bug when server denied request to battle, if there were maxplayers+1 players
* already in the battle.
* * added new commands: SERVERMSG, SERVERMSGBOX, REQUESTUPDATEFILE, GETFILE
* * added some admin commands
* * changed registration process so that now you can't register username which is same
* as someone elses, if we ignore case. Usernames are still case-sensitive though.
*
*
* ---- NOTES ----
*
* * Client may participate in only one battle at the same time. If he is hosting a battle,
* he may not participate in other battles at the same time. Server checks for that
* automatically.
*
* * Lines sent and received may be of any length. I've tested it with 600 KB long strings
* and it worked in both directions. Nevertheless, commands like "CLIENTS" still try to
* divide data into several lines, just to make sure client will receive it. Since delphi
* lobby client now supports lines of any length, dividing data into several lines is not
* needed anymore. Nevertheless I kept it just in case, to be compatible with other clients
* which may emerge in the future. I don't divide data when sending info on battles
* and clients in battles. This lines may get long, but not longer than a couple of hundreds
* of bytes (they should always be under 1 KB in length).
*
* * Sentences must be separated by TAB characters. This also means there should be no TABs
* present in your sentences, since TABs are delimiters. That is why you should always
* replace any TABs with spaces (2 or 8 usually).
*
* * Syncing works by clients comparing host's hash code with their own. If the two codes
* match, client should update his battle status and so telling other clients in the battle
* that he is synced (or unsynced otherwise). Hash code comes from hashing mod's file
* and probably all the dependences too.
*
* * Try not to edit account file manually! If you do, don't forget that access numbers
* must be in binary form!
*
* * Team colors are currently set by players, perhaps it would be better if only host would
* be able to change them?
*
* * Whenever you use killClient() within a for loop, don't forget to decrease loop
* counter as you will skip next client in the list otherwise. (this was the cause
* for some of the "ambigious data" errors). Or better, use the killClientDelayed()
* method.
*
* * Note that access to long-s is not guaranteed to be atomic, but you should use synchronization
* anyway if you use multiple threads.
*
*
* ---- LINKS ----
*
* Great article on how to handle network timeouts in Java: http://www.javacoffeebreak.com/articles/network_timeouts/
*
* Another one on network timeouts and alike: http://www.mindprod.com/jgloss/socket.html
*
* Great article on thread synchronization: http://today.java.net/pub/a/today/2004/08/02/sync1.html
*
* Throwing exceptions: http://java.sun.com/docs/books/tutorial/essential/exceptions/throwing.html
*
* Sun's tutorial on sockets: http://java.sun.com/docs/books/tutorial/networking/sockets/
*
* How to redirect program's output by duplicating handles in windows' command prompt: http://www.microsoft.com/resources/documentation/windows/xp/all/proddocs/en-us/redirection.mspx
*
* How to get local IP address (like "192.168.1.1" and not "127.0.0.1"): http://forum.java.sun.com/thread.jspa?threadID=619056&messageID=3477258
*
* IP-to-country databases: http://ip-to-country.webhosting.info, http://software77.net/cgi-bin/ip-country/geo-ip.pl
*
* Another set of 232 country flags: http://www.ip2location.com/free.asp
*
* Some source code on how to build client-server with java.nio classes (I used ChatterServer.java code from this link): http://brackeen.com/javagamebook/ch06src.zip
* (found it through this link: http://www.gamedev.net/community/forums/topic.asp?topic_id=318099)
*
* Source for some simple threaded UDP server: http://java.sun.com/docs/books/tutorial/networking/datagrams/example-1dot1/QuoteServerThread.java
*
* How to properly document thread-safety when writing classes: http://www-128.ibm.com/developerworks/java/library/j-jtp09263.html
*
* Good article on immutables (like String etc.): http://macchiato.com/columns/Durable2.html
*
* General info on thread-safety in java: http://mindprod.com/jgloss/threadsafe.html
*
* How to use ZIP with java: http://java.sun.com/developer/technicalArticles/Programming/compression/
*
* How to download file from URL: http://schmidt.devlib.org/java/file-download.html
*
* Very good article on exceptions: http://www.freshsources.com/Apr01.html
*
* Short introduction to generics in JDK 1.5.0: http://java.sun.com/j2se/1.5.0/docs/guide/language/generics.html
*
* ---- NAT TRAVERSAL ----
*
* Primary NAT traversal technique that this lobby server/client implements is "hole punching"
* technique. See these links for more info:
*
* http://www.brynosaurus.com/pub/net/p2pnat/
* http://www.potaroo.net/ietf/idref/draft-ford-natp2p/
* http://www.newport-networks.com/whitepapers/nat-traversal1.html
*
* See source code for implementation details.
*
*
* ---- PROTOCOL ----
*
* [this section was moved to the Documentation folder in SVN]
*
*/
/**
* @author Betalord
*
*/
import java.util.*;
import java.io.*;
import java.net.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
import java.util.regex.*;
public class TASServer {
static final String VERSION = "0.35";
static byte DEBUG = 1; // 0 - no verbose, 1 - normal verbose, 2 - extensive verbose
static String MOTD = "Enjoy your stay :-)";
static String agreement = ""; // agreement which is sent to user upon first login. User must send CONFIRMAGREEMENT command to confirm the agreement before server allows him to log in. See LOGIN command implementation for more details.
static long upTime;
static String latestSpringVersion = "*"; // this is sent via welcome message to every new client who connects to the server
static final String MOTD_FILENAME = "motd.txt";
static final String AGREEMENT_FILENAME = "agreement.rtf";
static final String ACCOUNTS_INFO_FILEPATH = "accounts.txt";
static final String SERVER_NOTIFICATION_FOLDER = "./notifs";
static final String IP2COUNTRY_FILENAME = "ip2country.dat";
static final String UPDATE_PROPERTIES_FILENAME = "updates.xml";
static final int DEFAULT_SERVER_PORT = 8200; // default server (TCP) port
static int serverPort = DEFAULT_SERVER_PORT; // actual server (TCP) port to be used (or currently in use)
static int NAT_TRAVERSAL_PORT = 8201; // default UDP port used with some NAT traversal technique. If this port is not forwarded, hole punching technique will not work.
static final int TIMEOUT_CHECK = 5000;
static int timeoutLength = 50000; // in milliseconds
static boolean LAN_MODE = false;
static boolean redirect = false; // if true, server is redirection clients to new IP
static String redirectToIP = ""; // new IP to which clients are redirected if (redirected==true)
static boolean RECORD_STATISTICS = false; // if true, statistics are saved to disk on regular intervals
static String PLOTICUS_FULLPATH = "./ploticus/bin/pl"; // see http://ploticus.sourceforge.net/ for more info on ploticus
static String STATISTICS_FOLDER = "./stats/";
static long saveStatisticsInterval = 1000 * 60 * 20; // in milliseconds
static boolean LOG_MAIN_CHANNEL = false; // if true, server will keep a log of all conversations from channel #main (in file "MainChanLog.log")
static PrintStream mainChanLog;
static String lanAdminUsername = "admin"; // default lan admin account. Can be overwritten with -LANADMIN switch. Used only when server is running in lan mode!
static String lanAdminPassword = Misc.encodePassword("admin");
private static HashMap<String,Integer>registrationTimes = new HashMap<String, Integer>();
private static LinkedList<String> whiteList=new LinkedList<String>();
static long purgeMutesInterval = 1000 * 3; // in miliseconds. On this interval, all channels' mute lists will be checked for expirations and purged accordingly.
static long lastMutesPurgeTime = System.currentTimeMillis(); // time when we last purged mute lists of all channels
static String[] reservedAccountNames = {"TASServer", "Server", "server"}; // accounts with these names cannot be registered (since they may be used internally by the server)
static final long minSleepTimeBetweenMapGrades = 5; // minimum time (in seconds) required between two consecutive MAPGRADES command sent by the client. We need this to ensure that client doesn't send MAPGRADES command too often as it creates much load on the server.
private static int MAX_TEAMS = 16; // max. teams/allies numbers supported by Spring
public static boolean initializationFinished = false; // we set this to 'true' just before we enter the main loop. We need this information when saving accounts for example, so that we don't dump empty accounts to disk when an error has occured before initialization has been completed
static ArrayList<FailedLoginAttempt> failedLoginAttempts = new ArrayList<FailedLoginAttempt>(); // here we store information on latest failed login attempts. We use it to block users from brute-forcing other accounts
static long lastFailedLoginsPurgeTime = System.currentTimeMillis(); // time when we last purged list of failed login attempts
// database related:
public static DBInterface database;
private static String DB_URL = "jdbc:mysql://127.0.0.1/spring";
private static String DB_username = "";
private static String DB_password = "";
private static final int READ_BUFFER_SIZE = 256; // size of the ByteBuffer used to read data from the socket channel. This size doesn't really matter - server will work with any size (tested with READ_BUFFER_SIZE==1), but too small buffer size may impact the performance.
private static final int SEND_BUFFER_SIZE = 8192*2; // socket's send buffer size
private static final long MAIN_LOOP_SLEEP = 10L;
public static final int NO_MSG_ID = -1; // meaning message isn't using an ID (see protocol description on message/command IDs)
private static int recvRecordPeriod = 10; // in seconds. Length of time period for which we keep record of bytes received from client. Used with anti-flood protection.
private static int maxBytesAlert = 20000; // maximum number of bytes received in the last recvRecordPeriod seconds from a single client before we raise "flood alert". Used with anti-flood protection.
private static int maxBytesAlertForBot = 50000; // same as 'maxBytesAlert' but is used for clients in "bot mode" only (see client.status bits)
private static long lastFloodCheckedTime = System.currentTimeMillis(); // time (in same format as System.currentTimeMillis) when we last updated it. Used with anti-flood protection.
private static long maxChatMessageLength = 1024; // used with basic anti-flood protection. Any chat messages (channel or private chat messages) longer than this are considered flooding. Used with following commands: SAY, SAYEX, SAYPRIVATE, SAYBATTLE, SAYBATTLEEX
public static boolean regEnabled=true;
public static boolean loginEnabled=true;
private static long lastTimeoutCheck = System.currentTimeMillis(); // time (System.currentTimeMillis()) when we last checked for timeouts from clients
private static ServerSocketChannel sSockChan;
private static Selector readSelector;
//***private static SelectionKey selectKey;
private static boolean running;
private static ByteBuffer readBuffer = ByteBuffer.allocateDirect(READ_BUFFER_SIZE); // see http://java.sun.com/j2se/1.5.0/docs/api/java/nio/ByteBuffer.html for difference between direct and non-direct buffers. In this case we should use direct buffers, this is also used by the author of java.nio chat example (see links) upon which this code is built on.
public static CharsetDecoder asciiDecoder;
public static CharsetEncoder asciiEncoder;
/* in 'updateProperties' we store a list of Spring versions and server responses to them.
* We use it when client doesn't have the latest Spring version or the lobby program
* and requests an update from us. The XML file should normally contain at least the "default" key
* which contains a standard response in case no suitable response is found.
* Each text field associated with a key contains a full string that will be send to the client
* as a response, so it should contain a full server command.
*/
private static Properties updateProperties = new Properties();
static NATHelpServer helpUDPsrvr;
public static void writeMainChanLog(String text) {
if (!LOG_MAIN_CHANNEL) return;
try {
mainChanLog.println(Misc.easyDateFormat("<HH:mm:ss> ") + text);
} catch (Exception e) {
TASServer.LOG_MAIN_CHANNEL = false;
System.out.println("$ERROR: Unable to write main channel log file (MainChanLog.log)");
}
}
/* reads MOTD from disk (if file is found) */
private static boolean readMOTD(String fileName)
{
String newMOTD = "";
try {
BufferedReader in = new BufferedReader(new FileReader(fileName));
String line;
while ((line = in.readLine()) != null) {
newMOTD = newMOTD.concat(line + '\n');
}
in.close();
} catch (IOException e) {
System.out.println("Couldn't find " + fileName + ". Using default MOTD");
return false;
}
MOTD = newMOTD;
return true;
}
private static boolean readUpdateProperties(String fileName) {
FileInputStream fStream = null;
try {
fStream = new FileInputStream(fileName);
updateProperties.loadFromXML(fStream);
} catch (IOException e) {
return false;
} finally {
if (fStream != null) {
try {
fStream.close();
} catch (IOException e) {
}
}
}
return true;
}
private static boolean writeUpdateProperties(String fileName) {
FileOutputStream fStream = null;
try {
fStream = new FileOutputStream(fileName);
updateProperties.storeToXML(fStream, null);
} catch (IOException e) {
return false;
} finally {
if (fStream != null) {
try {
fStream.close();
} catch (IOException e) {
}
}
}
return true;
}
/* reads agreement from disk (if file is found) */
private static void readAgreement()
{
String newAgreement = "";
try {
BufferedReader in = new BufferedReader(new FileReader(AGREEMENT_FILENAME));
String line;
while ((line = in.readLine()) != null) {
newAgreement = newAgreement.concat(line + '\n');
}
in.close();
} catch (IOException e) {
System.out.println("Couldn't find " + AGREEMENT_FILENAME + ". Using no agreement.");
return ;
}
if (newAgreement.length() > 2) agreement = newAgreement;
}
public static void closeServerAndExit() {
System.out.println("Server stopped.");
if (!LAN_MODE && initializationFinished) Accounts.saveAccounts(true); // we need to check if initialization has completed so that we don't save empty accounts array and so overwrite actual accounts
if (helpUDPsrvr != null && helpUDPsrvr.isAlive()) {
helpUDPsrvr.stopServer();
try {
helpUDPsrvr.join(1000); // give it 1 second to shut down gracefully
} catch (InterruptedException e) {
}
}
if (LOG_MAIN_CHANNEL) try {
mainChanLog.close();
// add server notification:
ServerNotification sn = new ServerNotification("Server stopped");
sn.addLine("Server has just been stopped. See server log for more info.");
ServerNotifications.addNotification(sn);
} catch(Exception e) {
// nevermind
}
try {
database.shutdownDriver();
} catch (Exception e) {
// ignore
}
running = false;
System.exit(0);
}
private static boolean changeCharset(String newCharset) throws IllegalCharsetNameException, UnsupportedCharsetException {
CharsetDecoder dec;
CharsetEncoder enc;
dec = Charset.forName(newCharset).newDecoder();
enc = Charset.forName(newCharset).newEncoder();
asciiDecoder = dec;
asciiDecoder.replaceWith("?");
asciiDecoder.onUnmappableCharacter(CodingErrorAction.REPLACE);
asciiDecoder.onMalformedInput(CodingErrorAction.REPLACE);
asciiEncoder = enc;
asciiEncoder.replaceWith(new byte[] { (byte)'?' });
asciiEncoder.onUnmappableCharacter(CodingErrorAction.REPLACE);
asciiEncoder.onMalformedInput(CodingErrorAction.REPLACE);
return true;
}
private static boolean startServer(int port) {
try {
changeCharset("ISO-8859-1"); // initializes asciiDecoder and asciiEncoder
// open a non-blocking server socket channel
sSockChan = ServerSocketChannel.open();
sSockChan.configureBlocking(false);
// bind to localhost on designated port
//***InetAddress addr = InetAddress.getLocalHost();
//***sSockChan.socket().bind(new InetSocketAddress(addr, port));
sSockChan.socket().bind(new InetSocketAddress(port));
// get a selector for multiplexing the client channels
readSelector = Selector.open();
} catch (IOException e) {
System.out.println("Could not listen on port: " + port);
return false;
}
System.out.println("Port " + port + " is open\n" +
"Listening for connections ...");
return true;
}
private static void acceptNewConnections() {
try {
SocketChannel clientChannel;
// since sSockChan is non-blocking, this will return immediately
// regardless of whether there is a connection available
while ((clientChannel = sSockChan.accept()) != null) {
if (redirect) {
if (DEBUG > 0) System.out.println("Client redirected to " + redirectToIP + ": " + clientChannel.socket().getInetAddress().getHostAddress());
redirectAndKill(clientChannel.socket());
continue;
}
Client client = Clients.addNewClient(clientChannel, readSelector, SEND_BUFFER_SIZE);
if (client == null) continue;
// from this point on, we know that client has been successfully connected
client.sendWelcomeMessage();
if (DEBUG > 0) System.out.println("New client connected: " + client.IP);
}
}
catch (IOException ioe) {
System.out.println("error during accept(): " + ioe.toString());
}
catch (Exception e) {
System.out.println("exception in acceptNewConnections()" + e.toString());
}
}
private static void readIncomingMessages() {
Client client = null;
try {
// non-blocking select, returns immediately regardless of how many keys are ready
readSelector.selectNow();
// fetch the keys
Set readyKeys = readSelector.selectedKeys();
// run through the keys and process each one
Iterator i = readyKeys.iterator();
while (i.hasNext()) {
SelectionKey key = (SelectionKey)i.next();
i.remove();
SocketChannel channel = (SocketChannel)key.channel();
client = (Client)key.attachment();
if (client.halfDead) continue;
readBuffer.clear();
client.timeOfLastReceive = System.currentTimeMillis();
// read from the channel into our buffer
long nbytes = channel.read(readBuffer);
client.dataOverLastTimePeriod += nbytes;
// basic anti-flood protection:
if ((client.account.accessLevel() < Account.ADMIN_ACCESS)
&& (((client.getBotModeFromStatus() == false) && (client.dataOverLastTimePeriod > TASServer.maxBytesAlert)) ||
((client.getBotModeFromStatus() == true) && (client.dataOverLastTimePeriod > TASServer.maxBytesAlertForBot)))) {
System.out.println("WARNING: Flooding detected from " + client.IP + " (" + client.account.user + ")");
Clients.sendToAllAdministrators("SERVERMSG [broadcast to all admins]: Flooding has been detected from " + client.IP + " (" + client.account.user + "). User has been kicked.");
Clients.killClient(client, "Disconnected due to excessive flooding");
// add server notification:
ServerNotification sn = new ServerNotification("Flooding detected");
sn.addLine("Flooding detected from " + client.IP + " (" + client.account.user + ").");
sn.addLine("User has been kicked from the server.");
ServerNotifications.addNotification(sn);
continue;
}
// check for end-of-stream
if (nbytes == -1) {
if (DEBUG > 0) System.out.println ("Socket disconnected - killing client");
channel.close();
Clients.killClient(client); // will also close the socket channel
} else {
// use a CharsetDecoder to turn those bytes into a string
// and append to client's StringBuilder
readBuffer.flip();
String str = asciiDecoder.decode(readBuffer).toString();
readBuffer.clear();
client.recvBuf.append(str);
// check for a full line
String line = client.recvBuf.toString();
while ((line.indexOf('\n') != -1) || (line.indexOf('\r') != -1)) {
int pos = line.indexOf('\r');
int npos = line.indexOf('\n');
if (pos == -1 || ((npos != -1) && (npos < pos))) pos = npos;
String command = line.substring(0, pos);
while (pos+1 < line.length() && (line.charAt(pos+1) == '\r' || line.charAt(pos+1) == '\n')) ++pos;
client.recvBuf.delete(0, pos+1);
long time = System.currentTimeMillis();
tryToExecCommand(command, client);
time = System.currentTimeMillis() - time;
if (time > 200) {
Clients.sendToAllAdministrators("SERVERMSG [broadcast to all admins]: (DEBUG) User <" + client.account.user + "> caused " + time + " ms load on the server. Command issued: " + command);
}
if (!client.alive) break; // in case client was killed within tryToExecCommand() method
line = client.recvBuf.toString();
}
}
}
} catch(IOException ioe) {
System.out.println("exception during select(): possibly due to force disconnect. Killing the client ...");
try {
if (client != null) Clients.killClient(client, "Quit: connection lost");
} catch (Exception e) {
}
} catch(Exception e) {
System.out.println("exception in readIncomingMessages(): killing the client ... (" + e.toString() + ")");
try {
if (client != null) Clients.killClient(client, "Quit: connection lost");
e.printStackTrace(); //*** DEBUG
} catch (Exception ex) {
}
}
}
private static Account verifyLogin(String user, String pass) {
Account acc = Accounts.getAccount(user);
if (acc == null) return null;
if (acc.pass.equals(pass)) return acc;
else return null;
}
private static void recordFailedLoginAttempt(String username) {
FailedLoginAttempt attempt = findFailedLoginAttempt(username);
if (attempt == null) {
attempt = new FailedLoginAttempt(username, 0, 0);
failedLoginAttempts.add(attempt);
}
attempt.timeOfLastFailedAttempt = System.currentTimeMillis();
attempt.numOfFailedAttempts++;
}
/** return null if no record found */
private static FailedLoginAttempt findFailedLoginAttempt(String username) {
for (int i = 0; i < failedLoginAttempts.size(); i++) {
if (failedLoginAttempts.get(i).username.equals(username)) {
return failedLoginAttempts.get(i);
}
}
return null;
}
/* Sends "message of the day" (MOTD) to client */
private static boolean sendMOTDToClient(Client client) {
client.beginFastWrite();
client.sendLine("MOTD Welcome, " + client.account.user + "!");
client.sendLine("MOTD There are currently " + (Clients.getClientsSize()-1) + " clients connected"); // -1 is because we shouldn't count the client to which we are sending MOTD
client.sendLine("MOTD to server talking in " + Channels.getChannelsSize() + " open channels and");
client.sendLine("MOTD participating in " + Battles.getBattlesSize() + " battles.");
client.sendLine("MOTD Server's uptime is " + Misc.timeToDHM(System.currentTimeMillis() - upTime) + ".");
client.sendLine("MOTD");
String[] sl = MOTD.split("\n");
for (int i = 0; i < sl.length; i++) {
client.sendLine("MOTD " + sl[i]);
}
client.endFastWrite();
return true;
}
private static void sendAgreementToClient(Client client) {
client.beginFastWrite();
String[] sl = agreement.split("\n");
for (int i = 0; i < sl.length; i++) {
client.sendLine("AGREEMENT " + sl[i]);
}
client.sendLine("AGREEMENTEND");
client.endFastWrite();
}
public static boolean redirectAndKill(Socket socket) {
if (!redirect) return false;
try {
(new PrintWriter(socket.getOutputStream(), true)).println("REDIRECT " + redirectToIP);
socket.close();
} catch (Exception e) {
return false;
}
return true;
}
/* Note: this method is not synchronized!
* Note2: this method may be called recursively! */
public static boolean tryToExecCommand(String command, Client client) {
command = command.trim();
if (command.equals("")) return false;
if (DEBUG > 1)
if (client.account.accessLevel() != Account.NIL_ACCESS) System.out.println("[<-" + client.account.user + "]" + " \"" + command + "\"");
else System.out.println("[<-" + client.IP + "]" + " \"" + command + "\"");
int ID = NO_MSG_ID;
if (command.charAt(0) == '#') try {
if (!command.matches("^#\\d+\\s[\\s\\S]*")) return false; // malformed command
ID = Integer.parseInt(command.substring(1).split("\\s")[0]);
// remove ID field from the rest of command:
command = command.replaceFirst("#\\d+\\s", "");
} catch (NumberFormatException e) {
return false; // this means that the command is malformed
} catch (PatternSyntaxException e) {
return false; // this means that the command is malformed
}
// parse command into tokens:
String[] commands = command.split(" ");
commands[0] = commands[0].toUpperCase();
client.setSendMsgID(ID);
try {
if (commands[0].equals("PING")) {
//***if (client.account.accessLevel() < Account.NORMAL_ACCESS) return false;
client.sendLine("PONG");
}
if(commands[0].equals("CREATEACCOUNT")) {
if(client.account.accessLevel() != Account.ADMIN_ACCESS) return false;
if(commands.length!=3) {
client.sendLine("SERVERMSG bad params");
return false;
}
String valid = Accounts.isOldUsernameValid(commands[1]);
if (valid != null) {
client.sendLine("SERVERMSG Invalid username (reason: " + valid + ")");
return false;
}
// validate password:
valid = Accounts.isPasswordValid(commands[2]);
if (valid != null) {
client.sendLine("SERVERMSG Invalid password (reason: " + valid + ")");
return false;
}
Account acc = Accounts.findAccountNoCase(commands[1]);
if (acc != null) {
client.sendLine("SERVERMSG Account already exists");
return false;
}
for (int i = 0; i < TASServer.reservedAccountNames.length; i++)
if (TASServer.reservedAccountNames[i].equals(commands[1])) {
client.sendLine("SERVERMSG Invalid account name - you are trying to register a reserved account name");
return false;
}
acc = new Account(commands[1], commands[2], Account.NORMAL_ACCESS, Account.NO_USER_ID, System.currentTimeMillis(), client.IP, System.currentTimeMillis(), client.country);
Accounts.addAccount(acc);
Accounts.saveAccounts(false); // let's save new accounts info to disk
client.sendLine("SERVERMSG Account created.");
}
if (commands[0].equals("REGISTER")) {
if (commands.length != 3) {
client.sendLine("REGISTRATIONDENIED Bad command arguments");
return false;
}
if(!regEnabled) {
client.sendLine("REGISTRATIONDENIED Sorry, account registration is currently disabled");
return false;
}
if (client.account.accessLevel() != Account.NIL_ACCESS) { // only clients which aren't logged-in can register
client.sendLine("REGISTRATIONDENIED You are already logged-in, no need to register new account");
return false;
}
if (LAN_MODE) { // no need to register account in LAN mode since it accepts any username
client.sendLine("REGISTRATIONDENIED Can't register in LAN-mode. Login with any username and password to proceed");
return false;
}
// validate username:
String valid = Accounts.isOldUsernameValid(commands[1]);
if (valid != null) {
client.sendLine("REGISTRATIONDENIED Invalid username (reason: " + valid + ")");
return false;
}
// validate password:
valid = Accounts.isPasswordValid(commands[2]);
if (valid != null) {
client.sendLine("REGISTRATIONDENIED Invalid password (reason: " + valid + ")");
return false;
}
Account acc = Accounts.findAccountNoCase(commands[1]);
if (acc != null) {
client.sendLine("REGISTRATIONDENIED Account already exists");
return false;
}
// check for reserved names:
for (int i = 0; i < TASServer.reservedAccountNames.length; i++)
if (TASServer.reservedAccountNames[i].equals(commands[1])) {
client.sendLine("REGISTRATIONDENIED Invalid account name - you are trying to register a reserved account name");
return false;
}
if(!whiteList.contains(client.IP)) {
/*if (registrationTimes.containsKey(client.IP)
&& (int)(registrationTimes.get(client.IP)) + 3600 > (System.currentTimeMillis()/1000)) {
client.sendLine("REGISTRATIONDENIED This ip has already registered an account recently");
Clients.sendToAllAdministrators("SERVERMSG Client at " + client.IP + "'s registration of " + commands[1] + " was blocked due to register spam");
return false;
}
registrationTimes.put(client.IP, (int)(System.currentTimeMillis()/1000));*/
/*String proxyDNS = "dnsbl.dronebl.org"; //Bot checks this with the broadcast, no waiting for a response
String[] ipChunks = client.IP.split("\\.");
for (int i = 0; i < 4; i++) {
proxyDNS = ipChunks[i] + "." + proxyDNS;
}
try {
InetAddress.getByName(proxyDNS);
client.sendLine("REGISTRATIONDENIED Using a known proxy ip");
Clients.sendToAllAdministrators("SERVERMSG Client at " + client.IP + "'s registration of " + commands[1] + " was blocked as it is a proxy IP");
return false;
} catch (UnknownHostException e) {
}*/
}
Clients.sendToAllAdministrators("SERVERMSG New registration of <" + commands[1] + "> at " + client.IP);
acc = new Account(commands[1], commands[2], Account.NORMAL_ACCESS, Account.NO_USER_ID, System.currentTimeMillis(), client.IP, System.currentTimeMillis(), client.country);
Accounts.addAccount(acc);
Accounts.saveAccounts(false); // let's save new accounts info to disk
client.sendLine("REGISTRATIONACCEPTED");
}
else if (commands[0].equals("UPTIME")) {
if (client.account.accessLevel() < Account.NORMAL_ACCESS) return false;
if (commands.length != 1) return false;
client.sendLine("SERVERMSG Server's uptime is " + Misc.timeToDHM(System.currentTimeMillis() - upTime));
}
/* some admin/moderator specific commands: */
else if (commands[0].equals("KICKUSER")) {
if (client.account.accessLevel() < Account.PRIVILEGED_ACCESS) return false;
if (commands.length < 2) return false;
Client target = Clients.getClient(commands[1]);
String reason = "";
if (commands.length > 2) reason = " (reason: " + Misc.makeSentence(commands, 2) + ")";
if (target == null) return false;
for (int i = 0; i < Channels.getChannelsSize(); i++) {
if (Channels.getChannel(i).isClientInThisChannel(target)) {
Channels.getChannel(i).broadcast("<" + client.account.user + "> has kicked <" + target.account.user + "> from server" + reason);
}
}
target.sendLine("SERVERMSG You've been kicked from server by <" + client.account.user + ">" + reason);
Clients.killClient(target, "Quit: kicked from server");
}
else if (commands[0].equals("FLOODLEVEL")) {
if(client.account.accessLevel() < Account.ADMIN_ACCESS) return false;
if(commands.length==3) {
if(commands[1].toUpperCase().equals("PERIOD")) {
recvRecordPeriod = Integer.parseInt(commands[2]);
client.sendLine("SERVERMSG The antiflood period is now " + commands[2] + " seconds.");
} else if(commands[1].toUpperCase().equals("USER")) {
maxBytesAlert = Integer.parseInt(commands[2]);
client.sendLine("SERVERMSG The antiflood amount for a normal user is now " + commands[2] + " bytes.");
} else if(commands[1].toUpperCase().equals("BOT")) {
maxBytesAlertForBot = Integer.parseInt(commands[2]);
client.sendLine("SERVERMSG The antiflood amount for a bot is now " + commands[2] + " bytes.");
}
}
}
else if (commands[0].equals("KILL")) {
if(client.account.accessLevel() < Account.ADMIN_ACCESS) return false;
if(commands.length<2) return false;
Client target=Clients.getClient(commands[1]);
if(target==null) return false;
Clients.killClient(target);
}
else if (commands[0].equals("KILLIP")) {
if(client.account.accessLevel() < Account.ADMIN_ACCESS) return false;
if(commands.length!=2) return false;
String IP = commands[1];
String[] sp1 = IP.split("\\.");
if (sp1.length != 4) {
client.sendLine("SERVERMSG Invalid IP address/range: " + IP);
return false;
}
for (int i = 0; i < Clients.getClientsSize(); i++) {
String[] sp2 = Clients.getClient(i).IP.split("\\.");
if (!sp1[0].equals("*")) if (!sp1[0].equals(sp2[0])) continue;
if (!sp1[1].equals("*")) if (!sp1[1].equals(sp2[1])) continue;
if (!sp1[2].equals("*")) if (!sp1[2].equals(sp2[2])) continue;
if (!sp1[3].equals("*")) if (!sp1[3].equals(sp2[3])) continue;
Clients.killClient(Clients.getClient(i));
}
}
else if (commands[0].equals("WHITELIST")) {
if(client.account.accessLevel() < Account.ADMIN_ACCESS) return false;
if(commands.length==2) {
whiteList.add(commands[1]);
client.sendLine("SERVERMSG IP successfully whitelisted from REGISTER constraints");
}
else
client.sendLine("SERVERMSG Whitelist is: " + whiteList.toString());
}
else if (commands[0].equals("UNWHITELIST")) {
if(client.account.accessLevel() < Account.ADMIN_ACCESS) return false;
if(commands.length==2) {
client.sendLine((whiteList.remove(commands[1]))?"SERVERMSG IP removed from whitelist":"SERVERMSG IP not in whitelist");
}
else
client.sendLine("SERVERMSG Bad command- UNWHITELIST IP");
}
else if (commands[0].equals("ENABLELOGIN")) {
if(client.account.accessLevel() < Account.ADMIN_ACCESS) return false;
if(commands.length==2)
loginEnabled=(commands[1].equals("1"));
client.sendLine("SERVERMSG The LOGIN command is " + (loginEnabled?"enabled":"disabled") + " for non-moderators");
}
else if (commands[0].equals("ENABLEREGISTER")) {
if(client.account.accessLevel() < Account.ADMIN_ACCESS) return false;
if(commands.length==2)
regEnabled=(commands[1].equals("1"));
client.sendLine("SERVERMSG The REGISTER command is " + (regEnabled?"enabled":"disabled"));
}
else if (commands[0].equals("SETTIMEOUT")) {
if(client.account.accessLevel() < Account.ADMIN_ACCESS) return false;
if(commands.length==2) {
timeoutLength = Integer.parseInt(commands[1]) * 1000;
client.sendLine("SERVERMSG Timeout length is now " + commands[1] + " seconds.");
}
}
else if (commands[0].equals("REMOVEACCOUNT")) {
if (client.account.accessLevel() < Account.ADMIN_ACCESS) return false;
if (commands.length != 2) return false;
if (!Accounts.removeAccount(commands[1])) return false;
// if any user is connected to this account, kick him:
for (int j = 0; j < Clients.getClientsSize(); j++) {
if (Clients.getClient(j).account.user.equals(commands[1])) {
Clients.killClient(Clients.getClient(j));
j--;
}
}
Accounts.saveAccounts(false); // let's save new accounts info to disk
client.sendLine("SERVERMSG You have successfully removed <" + commands[1] + "> account!");
}
else if (commands[0].equals("STOPSERVER")) {
// stop server gracefully:
if (client.account.accessLevel() < Account.ADMIN_ACCESS) return false;
running = false;
}
else if (commands[0].equals("FORCESTOPSERVER")) {
if (client.account.accessLevel() < Account.ADMIN_ACCESS) return false;
closeServerAndExit();
}
else if (commands[0].equals("SAVEACCOUNTS")) {
if (client.account.accessLevel() < Account.ADMIN_ACCESS) return false;
Accounts.saveAccounts(false);
client.sendLine("SERVERMSG Accounts will be saved in a background thread.");
}
else if (commands[0].equals("CHANGEACCOUNTPASS")) {
if (client.account.accessLevel() < Account.ADMIN_ACCESS) return false;
if (commands.length != 3) return false;
Account acc = Accounts.getAccount(commands[1]);
if (acc == null) return false;
// validate password:
if (Accounts.isPasswordValid(commands[2]) != null) return false;
acc.pass = commands[2];
Accounts.saveAccounts(false); // save changes
// add server notification:
ServerNotification sn = new ServerNotification("Account password changed by admin");
sn.addLine("Admin <" + client.account.user + "> has changed password for account <" + acc.user + ">");
ServerNotifications.addNotification(sn);
}
else if (commands[0].equals("CHANGEACCOUNTACCESS")) {
if (client.account.accessLevel() < Account.ADMIN_ACCESS) return false;
if (commands.length != 3) return false;
int value;
try {
value = Integer.parseInt(commands[2]);
} catch (NumberFormatException e) {
return false;
}
Account acc = Accounts.getAccount(commands[1]);
if (acc == null) return false;
int oldAccess = acc.access;
acc.access = value;
Accounts.saveAccounts(false); // save changes
// just in case if rank got changed: FIXME?
//Client target=Clients.getClient(commands[1]);
//target.setRankToStatus(client.account.getRank());
//if(target.alive)
// Clients.notifyClientsOfNewClientStatus(target);
client.sendLine("SERVERMSG You have changed ACCESS for <" + commands[1] + "> successfully.");
// add server notification:
ServerNotification sn = new ServerNotification("Account access changed by admin");
sn.addLine("Admin <" + client.account.user + "> has changed access/status bits for account <" + acc.user + ">.");
sn.addLine("Old access code: " + oldAccess + ". New code: " + value);
ServerNotifications.addNotification(sn);
}
else if (commands[0].equals("GETACCOUNTACCESS")) {
if (client.account.accessLevel() < Account.ADMIN_ACCESS) return false;
if (commands.length != 2) return false;
Account acc = Accounts.getAccount(commands[1]);
if (acc == null) {
client.sendLine("SERVERMSG User <" + commands[1] + "> not found!");
return false;
}
client.sendLine("SERVERMSG " + commands[1] + "'s access code is " + acc.access);