-
-
Notifications
You must be signed in to change notification settings - Fork 4
/
Protocol.py
executable file
·2757 lines (2400 loc) · 97.6 KB
/
Protocol.py
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
import inspect, time, re
import base64
try: from hashlib import md5
except: md5 = __import__('md5').new
import traceback, sys, os
import socket
ranks = (5, 15, 30, 100, 300, 1000, 3000)
restricted = {
'disabled':[],
'everyone':['TOKENIZE','TELNET','HASH','EXIT','PING'],
'fresh':['LOGIN','REGISTER','REQUESTUPDATEFILE'],
'agreement':['CONFIRMAGREEMENT'],
'user':[
########
# battle
'ADDBOT',
'ADDSTARTRECT',
'DISABLEUNITS',
'ENABLEUNITS',
'ENABLEALLUNITS',
'FORCEALLYNO',
'FORCESPECTATORMODE',
'FORCETEAMCOLOR',
'FORCETEAMNO',
'HANDICAP',
'JOINBATTLE',
'JOINBATTLEACCEPT',
'JOINBATTLEDENY',
'FORCEJOINBATTLE',
'KICKFROMBATTLE',
'LEAVEBATTLE',
'MAPGRADES',
'MYBATTLESTATUS',
'OPENBATTLE',
'OPENBATTLEEX',
'REMOVEBOT',
'REMOVESCRIPTTAGS',
'REMOVESTARTRECT',
'RING',
'SAYBATTLE',
'SAYBATTLEHOOKED',
'SAYBATTLEEX',
'SAYBATTLEPRIVATE',
'SAYBATTLEPRIVATEEX',
'SCRIPT',
'SCRIPTEND',
'SCRIPTSTART',
'SETSCRIPTTAGS',
'UPDATEBATTLEINFO',
'UPDATEBOT',
'UPDATEBATTLEDETAILS',
#########
# channel
'CHANNELMESSAGE',
'CHANNELS',
'CHANNELTOPIC',
'FORCELEAVECHANNEL',
'JOIN',
'LEAVE',
'MUTE',
'MUTELIST',
'SAY',
'SAYHOOKED',
'SAYEX',
'SAYPRIVATE',
'SAYPRIVATEEX',
'SAYPRIVATEHOOKED',
'SETCHANNELKEY',
'UNMUTE',
########
# meta
'CHANGEPASSWORD',
'GETINGAMETIME',
'GETREGISTRATIONDATE',
'HOOK',
'KILLALL',
'MYSTATUS',
'PORTTEST',
'UPTIME',
'RENAMEACCOUNT'],
'mod':[
'BAN', 'BANUSER', 'BANIP', 'UNBAN', 'UNBANIP', 'BANLIST',
'CHANGEACCOUNTPASS',
'KICKUSER', 'FINDIP', 'GETIP', 'GETLASTLOGINTIME','GETUSERID'
'FORCECLOSEBATTLE', 'SETBOTMODE', 'TESTLOGIN'
],
'admin':[
#########
# channel
'ALIAS','UNALIAS','ALIASLIST',
#########
# server
'ADMINBROADCAST', 'BROADCAST','BROADCASTEX','RELOAD',
#########
# users
'FORGEMSG','FORGEREVERSEMSG',
'GETLOBBYVERSION', 'GETSENDBUFFERSIZE',
'GETACCOUNTINFO', 'GETLASTLOGINTIME',
'GETACCOUNTACCESS',
'SETACCESS','DEBUG','PYTHON',
'SETINGAMETIME',],
}
restricted_list = []
for level in restricted:
restricted_list += restricted[level]
ipRegex = r"^([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])$"
re_ip = re.compile(ipRegex)
def validateIP(ipAddress):
return re_ip.match(ipAddress)
def int32(x):
val = int(x)
if val > 2147483647 : raise OverflowError
if val < -2147483648 : raise OverflowError
return val
class AutoDict:
# method 1
# def __getitem__(self, item):
# return self.__getattribute__(item)
#
# def __setitem__(self, item, value):
# return self.__setattr__(item, value)
# method 2
# def __getitem__(self, item):
# item = str(item)
# if not '__' in item and hasattr(self, item):
# return getattr(self, item)
#
# def __setitem__(self, item, value):
# item = str(item)
# if not '__' in item and hasattr(self, item):
# setattr(self, item, value)
def keys(self):
return filter(lambda x: not '__' in x, self.dir)
def update(self, **kwargs):
keys = self.keys()
for key in kwargs:
if key in keys:
setattr(self, key, kwargs[key])
def copy(self):
d = {}
for key in self.keys():
d[key] = getattr(self, key)
return d
def __AutoDictInit__(self):
self.dir = dir(self)
for key in self.keys():
new = getattr(self, key)
ntype = type(new)
if ntype in (list, dict, set):
new = ntype(new)
setattr(self, key, new)
class Battle(AutoDict):
def __init__(self, root, id, type, natType, password, port, maxplayers,
hashcode, rank, maphash, map, title, modname,
passworded, host, users, spectators=0,
startrects={}, disabled_units=[], pending_users=set(),
authed_users=set(), bots={}, script_tags={},
replay_script={}, replay=False,
sending_replay_script=False, locked=False,
engine=None, version=None, extended=False):
self._root = root
self.id = id
self.type = type
self.natType = natType
self.password = password
self.port = port
self.maxplayers = maxplayers
self.spectators = spectators
self.hashcode = hashcode
self.rank = rank
self.maphash = maphash
self.map = map
self.title = title
self.modname = modname
self.passworded = passworded
self.users = users
self.host = host
self.startrects = startrects
self.disabled_units = disabled_units
self.pending_users = pending_users
self.authed_users = authed_users
self.engine = (engine or 'spring').lower()
self.version = version or root.latestspringversion
self.extended = extended
self.bots = bots
self.script_tags = script_tags
self.replay_script = replay_script
self.replay = replay
self.sending_replay_script = sending_replay_script
self.locked = locked
self.spectators = 0
self.__AutoDictInit__()
class Channel(AutoDict):
def __init__(self, root, chan, users=[], blindusers=[], admins=[],
ban={}, allow=[], autokick='ban', chanserv=False,
owner='', mutelist={}, antispam=False,
censor=False, antishock=False, topic=None,
key=None, **kwargs):
self._root = root
self.chan = chan
self.users = users
self.blindusers = blindusers
self.admins = admins
self.ban = ban
self.allow = allow
self.autokick = autokick
self.chanserv = chanserv
self.owner = owner
self.mutelist = mutelist
self.antispam = antispam
self.censor = censor
self.antishock = antishock
self.topic = topic
self.key = key
self.__AutoDictInit__()
if chanserv and self._root.chanserv and not chan in self._root.channels:
self._root.chanserv.Send('JOIN %s' % self.chan)
def broadcast(self, message):
self._root.broadcast(message, self.chan)
def channelMessage(self, message):
self.broadcast('CHANNELMESSAGE %s %s' % (self.chan, message))
def register(self, client, owner):
self.owner = owner.db_id
def addUser(self, client):
username = client.username
if not username in self.users:
self.users.append(username)
self.broadcast('JOINED %s %s' % (self.chan, username))
def removeUser(self, client, reason=''):
chan = self.chan
username = client.username
if username in self.users:
self.users.remove(username)
if username in self.blindusers:
self.blindusers.remove(username)
if self.chan in client.channels:
client.channels.remove(chan)
self._root.broadcast('LEFT %s %s' % (chan, username), chan, self.blindusers)
def isAdmin(self, client):
return client and ('admin' in client.accesslevels)
def isMod(self, client):
return client and (('mod' in client.accesslevels) or self.isAdmin(client))
def isFounder(self, client):
return client and ((client.db_id == self.owner) or self.isMod(client))
def isOp(self, client):
return client and ((client.db_id in self.admins) or self.isFounder(client))
def getAccess(self, client): # return client's security clearance
return 'mod' if self.isMod(client) else\
('founder' if self.isFounder(client) else\
('op' if self.isOp(client) else\
'normal'))
def isMuted(self, client):
return client.db_id in self.mutelist
def getMuteMessage(self, client):
if self.isMuted(client):
m = self.mutelist[client.db_id]
if m['expires'] == 0:
return 'muted forever'
else:
# TODO: move format_time, bin2dec, etc to a utilities class or module
return 'muted for the next %s.' % (client._protocol._time_until(m['expires']))
else:
return 'not muted'
def isAllowed(self, client):
if self.autokick == 'allow':
return (self.isOp(client) or (client.db_id in self.allow)) or 'not allowed here'
elif self.autokick == 'ban':
return (self.isOp(client) or (client.db_id not in self.ban)) or self.ban[client.db_id]
def setTopic(self, client, topic):
self.topic = topic
if topic in ('*', None):
if self.topic:
self.channelMessage('Topic disabled.')
topicdict = {}
else:
self.channelMessage('Topic changed.')
topicdict = {'user':client.username, 'text':topic, 'time':'%s'%(int(time.time())*1000)}
self.broadcast('CHANNELTOPIC %s %s %s %s'%(self.chan, client.username, topicdict['time'], topic))
self.topic = topicdict
def setKey(self, client, key):
if key in ('*', None):
if self.key:
self.key = None
self.channelMessage('<%s> unlocked this channel' % client.username)
else:
self.key = key
self.channelMessage('<%s> locked this channel with a password' % client.username)
def setFounder(self, client, target):
if not target: return
self.owner = target.db_id
self.channelMessage("<%s> has just been set as this channel's founder by <%s>" % (target.username, client.username))
def opUser(self, client, target):
if target and not target.db_id in self.admins:
self.admins.append(target.db_id)
self.channelMessage("<%s> has just been added to this channel's operator list by <%s>" % (target.username, client.username))
def deopUser(self, client, target):
if target and target.db_id in self.admins:
self.admins.remove(target.db_id)
self.channelMessage("<%s> has just been removed from this channel's operator list by <%s>" % (target.username, client.username))
def kickUser(self, client, target, reason=''):
if self.isFounder(target): return
if target and target.username in self.users:
target.Send('FORCELEAVECHANNEL %s %s %s' % (self.chan, client.username, reason))
self.channelMessage('<%s> has kicked <%s> from the channel%s' % (client.username, target.username, (' (reason: %s)'%reason if reason else '')))
self.removeUser(target, 'kicked from channel%s' % (' (reason: %s)'%reason if reason else ''))
def banUser(self, client, target, reason=''):
if self.isFounder(target): return
if target and not target.db_id in self.ban:
self.ban[target.db_id] = reason
self.kickUser(client, target, reason)
self.channelMessage('<%s> has been banned from this channel by <%s>' % (target.username, client.username))
def unbanUser(self, client, target):
if target and target.db_id in self.ban:
del self.ban[target.db_id]
self.channelMessage('<%s> has been unbanned from this channel by <%s>' % (target.username, client.username))
def allowUser(self, client, target):
if target and not client.db_id in self.allow:
self.allow.append(client.db_id)
self.channelMessage('<%s> has been allowed in this channel by <%s>' % (target.username, client.username))
def disallowUser(self, client, target):
if target and client.db_id in self.allow:
self.allow.remove(client.db_id)
self.channelMessage('<%s> has been disallowed in this channel by <%s>' % (target.username, client.username))
def muteUser(self, client, target, duration=0, ip=False, quiet=False):
if self.isFounder(target): return
if target and not client.db_id in self.mutelist:
if not quiet:
self.channelMessage('<%s> has muted <%s>' % (client.username, target.username))
try:
duration = float(duration)*60
if duration < 1:
duration = 0
else:
duration = time.time() + duration
except: duration = 0
self.mutelist[target.db_id] = {'expires':duration, 'ip':ip, 'quiet':quiet}
def unmuteUser(self, client, target):
if target and target.db_id in self.mutelist:
del self.mutelist[target.db_id]
self.channelMessage('<%s> has unmuted <%s>' % (client.username, target.username))
class Protocol:
def __init__(self, root, handler):
self._root = root
self.handler = handler
self.userdb = root.getUserDB()
self.SayHooks = root.SayHooks
self.dir = dir(self)
def _new(self, client):
if self._root.dbtype == 'lan': lan = '1'
else: lan = '0'
login_string = ' '.join((self._root.server, str(self._root.server_version), self._root.latestspringversion, str(self._root.natport), lan))
client.SendNow(login_string)
def _remove(self, client, reason='Quit'):
if client.username and client.username in self._root.usernames:
if client.removing: return
if client.static: return # static clients don't disconnect
client.removing = True
user = client.username
if not client == self._root.usernames[user]:
client.removing = False # 'cause we really aren't anymore
return
self.userdb.end_session(client.db_id)
channels = list(client.channels)
del self._root.usernames[user]
if client.db_id in self._root.db_ids:
del self._root.db_ids[client.db_id]
for chan in channels:
channel = self._root.channels[chan]
if user in channel.users:
channel.users.remove(user)
if user in channel.blindusers:
channel.blindusers.remove(user)
self._root.broadcast('LEFT %s %s %s'%(chan, user, reason), chan, user)
battle_id = client.current_battle
if battle_id:
self.in_LEAVEBATTLE(client)
self.broadcast_RemoveUser(client)
if client.session_id in self._root.clients: del self._root.clients[client.session_id]
def _handle(self, client, msg):
if msg.startswith('#'):
test = msg.split(' ')[0][1:]
if test.isdigit():
msg_id = '#%s '%test
msg = ' '.join(msg.split(' ')[1:])
else:
msg_id = ''
else:
msg_id = ''
# client.Send() prepends client.msg_id if the current thread
# is the same thread as the client's handler.
# this works because handling is done in order for each ClientHandler thread
# so we can be sure client.Send() was performed in the client's own handling code.
client.msg_id = msg_id
numspaces = msg.count(' ')
if numspaces:
command,args = msg.split(' ',1)
else:
command = msg
command = command.upper()
access = []
for level in client.accesslevels:
access += restricted[level]
if command in restricted_list:
if not command in access:
client.Send('SERVERMSG %s failed. Insufficient rights.'%command)
return False
else:
if not 'user' in client.accesslevels:
client.Send('SERVERMSG %s failed. Insufficient rights.'%command)
return False
command = 'in_%s' % command
if command in self.dir:
function = getattr(self, command)
else:
client.Send('SERVERMSG %s failed. Command does not exist.'%(command.split('_',1)[1]))
return False
function_info = inspect.getargspec(function)
total_args = len(function_info[0])-2
# if there are no arguments, just call the function
if not total_args:
function(client)
return True
# check for optional arguments
optional_args = 0
if function_info[3]:
optional_args = len(function_info[3])
# check if we've got enough words for filling the required args
required_args = total_args - optional_args
if numspaces < required_args:
client.Send('SERVERMSG %s failed. Incorrect arguments.'%('_'.join(command.split('_')[1:])))
return False
if required_args == 0 and numspaces == 0:
function(client)
return True
# bunch the last words together if there are too many of them
if numspaces > total_args-1:
arguments = args.split(' ',total_args-1)
else:
arguments = args.split(' ')
function(*([client]+arguments))
# TODO: check the exception line... if it's "function(*([client]+arguments))"
# then it was incorrect arguments. if not, log the error, as it was a code problem
#try:
# function(*([client]+arguments))
#except TypeError:
# client.Send('SERVERMSG %s failed. Incorrect arguments.'%command.partition('in_')[2])
return True
def _bin2dec(self, s):
return int(s, 2)
def _dec2bin(self, i, bits=None):
i = int(i)
b = ''
while i > 0:
j = i & 1
b = str(j) + b
i >>= 1
if bits:
b = b.rjust(bits,'0')
return b
def _udp_packet(self, username, ip, udpport):
if username in self._root.usernames:
client = self._root.usernames[username]
if ip == client.local_ip or ip == client.ip_address:
client.Send('UDPSOURCEPORT %i'%udpport)
battle_id = client.current_battle
if not battle_id in self._root.battles: return
battle = self._root.battles[battle_id]
if battle:
client.udpport = udpport
client.hostport = udpport
host = battle.host
if not host == username:
self._root.usernames[host].SendBattle(battle, 'CLIENTIPPORT %s %s %s'%(username, ip, udpport))
else:
client.udpport = udpport
else:
self._root.admin_broadcast('NAT spoof from %s pretending to be <%s>'%(ip,username))
def _calc_access_status(self, client):
self._calc_access(client)
self._calc_status(client, client.status)
def _calc_access(self, client):
userlevel = client.access
inherit = {'mod':['user'], 'admin':['mod', 'user']}
if userlevel in inherit:
inherited = inherit[userlevel]
else:
inherited = [userlevel]
if not client.access in inherited: inherited.append(client.access)
client.accesslevels = inherited+['everyone']
def _calc_status(self, client, status):
status = self._dec2bin(status, 7)
bot, access, rank1, rank2, rank3, away, ingame = status[-7:]
rank1, rank2, rank3 = self._dec2bin(6, 3)
accesslist = {'user':0, 'mod':1, 'admin':1}
access = client.access
if access in accesslist:
access = accesslist[access]
else:
access = 0
bot = int(client.bot)
ingame_time = float(client.ingame_time/60) # hours
rank = 0
for t in ranks:
if ingame_time >= t:
rank += 1
rank1, rank2, rank3 = self._dec2bin(rank, 3)
client.is_ingame = (ingame == '1')
client.away = (away == '1')
status = self._bin2dec('%s%s%s%s%s%s%s'%(bot, access, rank1, rank2, rank3, away, ingame))
client.status = status
return status
def _calc_battlestatus(self, client):
battlestatus = client.battlestatus
status = self._bin2dec('0000%s%s0000%s%s%s%s%s0'%(battlestatus['side'],
battlestatus['sync'], battlestatus['handicap'],
battlestatus['mode'], battlestatus['ally'],
battlestatus['id'], battlestatus['ready']))
return status
def _new_channel(self, chan, **kwargs):
# any updates to channels from the SQL database from a web interface
# would possibly need to call a RELOAD-type function
# unless we want to do way more SQL lookups for channel info
try:
if not kwargs: raise KeyError
channel = Channel(self._root, chan, **kwargs)
except: channel = Channel(self._root, chan)
return channel
def _time_format(self, seconds):
'given a duration in seconds, returns a human-readable relative time'
minutesleft = float(seconds) / 60
hoursleft = minutesleft / 60
daysleft = hoursleft / 24
if daysleft > 7:
message = '%0.2f weeks' % (daysleft / 7)
elif daysleft == 7:
message = 'a week'
elif daysleft > 1:
message = '%0.2f days' % daysleft
elif daysleft == 1:
message = 'a day'
elif hoursleft > 1:
message = '%0.2f hours' % hoursleft
elif hoursleft == 1:
message = 'an hour'
elif minutesleft > 1:
message = '%0.1f minutes' % minutesleft
elif minutesleft == 1:
message = 'a minute'
else:
message = '%0.0f second(s)'%(float(seconds))
return message
def _time_until(self, timestamp):
'given a future timestamp, as returned by time.time(), returns a human-readable relative time'
seconds = timestamp - time.time()
if seconds <= 0:
return 'forever'
else:
seconds = seconds - time.time()
return self._time_format(seconds)
def _time_since(self, timestamp):
'given a past timestamp, as returned by time.time(), returns a readable relative time as a string'
seconds = time.time() - timestamp
return self._time_format(seconds)
def clientFromID(self, db_id):
'given a user database id, returns a client object from memory or the database'
return self._root.clientFromID(db_id) or self.userdb.clientFromID(db_id)
def clientFromUsername(self, username):
'given a username, returns a client object from memory or the database'
client = self._root.clientFromUsername(username)
if not client:
client = self.userdb.clientFromUsername(username)
if client:
client.db_id = client.id
self._calc_access(client)
return client
def broadcast_AddBattle(self, battle):
'queues the protocol for adding a battle - experiment in loose thread-safety'
users = dict(self._root.usernames)
for name in users:
users[name].AddBattle(battle)
def broadcast_RemoveBattle(self, battle):
'queues the protocol for removing a battle - experiment in loose thread-safety'
users = dict(self._root.usernames)
for name in users:
users[name].RemoveBattle(battle)
def broadcast_SendBattle(self, battle, data):
'queues the protocol for sending text in a battle - experiment in loose thread-safety'
users = list(battle.users)
for name in users:
if name in self._root.usernames:
self._root.usernames[name].SendBattle(battle, data)
def broadcast_AddUser(self, user):
'queues the protocol for adding a user - experiment in loose thread-safety'
users = dict(self._root.usernames)
for name in users:
if not name == user.username:
users[name].AddUser(user)
def broadcast_RemoveUser(self, user):
'queues the protocol for removing a user - experiment in loose thread-safety'
users = dict(self._root.usernames)
for name in users:
if not name == user.username:
users[name].RemoveUser(user)
def broadcast_SendUser(self, user, data):
'queues the protocol for receiving a user-specific message - experiment in loose thread-safety'
users = dict(self._root.usernames)
for name in users:
users[name].SendUser(user, data)
def client_AddUser(self, client, user):
'sends the protocol for adding a user'
if client.compat['accountIDs']:
client.Send('ADDUSER %s %s %s %s' % (user.username, user.country_code, user.cpu, user.db_id))
else:
client.Send('ADDUSER %s %s %s' % (user.username, user.country_code, user.cpu))
def client_RemoveUser(self, client, user):
'sends the protocol for removing a user'
client.Send('REMOVEUSER %s' % user.username)
def client_AddBattle(self, client, battle):
'sends the protocol for adding a battle'
ubattle = battle.copy()
if not battle.host in self._root.usernames: return
host = self._root.usernames[battle.host]
if host.ip_address == client.ip_address: # translates the ip to always be compatible with the client
translated_ip = host.local_ip
else:
translated_ip = host.ip_address
ubattle.update({'ip':translated_ip})
if client.compat['extendedBattles']:
client.Send('BATTLEOPENEDEX %(id)s %(type)s %(natType)s %(host)s %(ip)s %(port)s %(maxplayers)s %(passworded)s %(rank)s %(maphash)s %(engine)s %(version)s %(map)s\t%(title)s\t%(modname)s' % ubattle)
else:
if not (battle.engine == 'spring' and (battle.version == self._root.latestspringversion or battle.version == self._root.latestspringversion + '.0')):
ubattle['title'] = 'Incompatible (%(engine)s %(version)s) %(title)s' % ubattle
client.Send('BATTLEOPENED %(id)s %(type)s %(natType)s %(host)s %(ip)s %(port)s %(maxplayers)s %(passworded)s %(rank)s %(maphash)s %(map)s\t%(title)s\t%(modname)s' % ubattle)
def client_RemoveBattle(self, client, battle):
'sends the protocol for removing a battle'
client.Send('BATTLECLOSED %s' % battle.id)
# Begin incoming protocol section #
#
# any function definition beginning with in_ and ending with capital letters
# is a definition of an incoming command.
#
# any text arguments passed by the client are automatically split and passed to the method
# keyword arguments are treated as optional
# this is done in the _handle() method above
#
# example (note, this is not the actual in_SAY method used in the server):
#
# def in_SAY(self, client, channel, message=None):
# if message:
# sendToChannel(channel, message)
# else:
# sendToChannel(channel, "I'm too cool to send a message")
#
# if the client sends "SAY foo bar", the server calls in_SAY(client, "foo", "bar")
# if the client sends "SAY foo", the server will call in_SAY(client, "foo")
#
# however, if the client sends "SAY",
# the server will notice the client didn't send enough text to fill the arguments
# and return an error message to the client
def in_PING(self, client, reply=None):
'''
Tell the server you are in fact still connected.
The server will reply with PONG, useful for testing latency.
@optional.str reply: Reply to send client
'''
if reply:
client.Send('PONG %s'%reply)
else:
client.Send('PONG')
def in_PORTTEST(self, client, port):
'''
Connect to client on specified UDP port and send the string 'Port testing...'
@required.int port: UDP port to connect to for port testing
'''
host = client.ip_address
port = int(port)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto('Port testing...', (host, port))
def in_REGISTER(self, client, username, password):
'''
Register a new user in the account database.
@required.str username: Username to register
@required.str password: Password to use, usually encoded md5+base64
'''
for char in username:
if not char.lower() in 'abcdefghijklmnopqrstuvwzyx[]_1234567890':
client.Send('REGISTRATIONDENIED Unicode names are currently disallowed.')
return
if len(username) > 20:
client.Send('REGISTRATIONDENIED Username is too long.')
return
good, reason = self.userdb.register_user(username, password, client.ip_address, client.country_code)
if good:
self._root.console_write('Handler %s: Successfully registered user <%s> on session %s.'%(client.handler.num, username, client.session_id))
client.Send('REGISTRATIONACCEPTED')
self.userdb.clientFromUsername(username).access = 'agreement'
else:
self._root.console_write('Handler %s: Registration failed for user <%s> on session %s.'%(client.handler.num, username, client.session_id))
client.Send('REGISTRATIONDENIED %s'%reason)
def in_TELNET(self, client):
'''
Set the client to a telnet client, which provides a simple interface for speaking in one channel.
'''
client.telnet = True
client.Send('Welcome, telnet user.')
def in_HASH(self, client):
'''
After this command has been used, the password argument to LOGIN will be automatically hashed with md5+base64.
'''
client.hashpw = True
if client.telnet:
client.Send('Your password will be hashed for you when you login.')
def in_LOGIN(self, client, username, password='', cpu='0', local_ip='', sentence_args=''):
'''
Attempt to login the active client.
@required.str username: Username
@required.str password: Password, usually encoded md5+base64
@optional.int cpu: CPU speed
@optional.ip local_ip: LAN IP address, sent to clients when they have the same WAN IP as host
@optional.sentence.str lobby_id: Lobby name and version
@optional.sentence.int user_id: User ID provided by lobby
@optional.sentence.str compat_flags: Compatibility flags, sent in space-separated form, as follows:
flag: description
-----------------
a: Send account IDs as an additional parameter to ADDUSER. Account IDs persist across renames.
b: If client is hosting a battle, prompts them with JOINBATTLEREQUEST when a user tries to join their battle
sp: If client is hosting a battle, sends them other clients' script passwords as an additional argument to JOINEDBATTLE.
et: When client joins a channel, sends NOCHANNELTOPIC if the channel has no topic.
eb: Enables receiving extended battle commands, like BATTLEOPENEDEX
'''
if not username:
client.Send('DENIED Invalid username.')
return
try: int(cpu)
except: cpu = '0'
if not validateIP(local_ip): local_ip = client.ip_address
if '\t' in sentence_args:
lobby_id, user_id = sentence_args.split('\t',1)
if '\t' in user_id:
user_id, compFlags = user_id.split('\t', 1)
flags = set()
for flag in compFlags.split(' '):
if flag in ('ab', 'ba'):
flags.add('a')
flags.add('b')
else:
flags.add(flag)
flag_map = {
'a': 'accountIDs', # send account IDs in ADDUSER
'b': 'battleAuth', # JOINBATTLEREQUEST/ACCEPT/DENY
'sp': 'scriptPassword', # scriptPassword in JOINEDBATTLE
'et': 'sendEmptyTopic', # send NOCHANNELTOPIC on join if channel has no topic
'eb': 'extendedBattles', # extended battle commands with support for engine/version
'm': 'matchmaking', # FORCEJOINBATTLE from battle hosts for matchmaking
}
for flag in flags:
if flag in flag_map:
client.compat[flag_map[flag]] = True
if user_id.replace('-','',1).isdigit():
user_id = int(user_id)
if user_id > 2147483647:
user_id &= 2147483647
user_id *= -1
else: user_id = None
else:
lobby_id = sentence_args
user_id = 0
if client.hashpw:
m = md5(password)
password = base64.b64encode(m.digest())
good, reason = self.userdb.login_user(username, password, client.ip_address, lobby_id, user_id, cpu, local_ip, client.country_code)
if good: username = reason.username
if not username in self._root.usernames:
if good:
client.logged_in = True
client.access = reason.access
self._calc_access(client)
client.username = username
if client.access == 'agreement':
self._root.console_write('Handler %s: Sent user <%s> the terms of service on session %s.'%(client.handler.num, username, client.session_id))
agreement = ['AGREEMENT {\\rtf1\\ansi\\ansicpg1250\\deff0\\deflang1060{\\fonttbl{\\f0\\fswiss\\fprq2\\fcharset238 Verdana;}{\\f1\\fswiss\\fprq2\\fcharset238{\\*\\fname Arial;}Arial CE;}{\\f2\\fswiss\\fcharset238{\\*\\fname Arial;}Arial CE;}}',
'AGREEMENT {\\*\\generator Msftedit 5.41.15.1507;}\\viewkind4\\uc1\\pard\\ul\\b\\f0\\fs22 Terms of Use\\ulnone\\b0\\f1\\fs20\\par',
'AGREEMENT \\f2\\par',
'AGREEMENT \\f0\\fs16 While the administrators and moderators of this server will attempt to keep spammers and players violating this agreement off the server, it is impossible for them to maintain order at all times. Therefore you acknowledge that any messages in our channels express the views and opinions of the author and not the administrators or moderators (except for messages by these people) and hence will not be held liable.\\par',
'AGREEMENT \\par',
'AGREEMENT You agree not to use any abusive, obscene, vulgar, slanderous, hateful, threatening, sexually-oriented or any other material that may violate any applicable laws. Doing so may lead to you being immediately and permanently banned (and your service provider being informed). You agree that the administrators and moderators of this server have the right to mute, kick or ban you at any time should they see fit. As a user you agree to any information you have entered above being stored in a database. While this information will not be disclosed to any third party without your consent administrators and moderators cannot be held responsible for any hacking attempt that may lead to the data being compromised. Passwords are sent and stored in encoded form. Any personal information such as personal statistics will be kept privately and will not be disclosed to any third party.\\par',
'AGREEMENT \\par',
'AGREEMENT By using this service you hereby agree to all of the above terms.\\fs18\\par',
'AGREEMENT \\f2\\fs20\\par',
'AGREEMENT }',
'AGREEMENTEND']
for line in agreement: client.Send(line)
return
self._root.console_write('Handler %s: Successfully logged in user <%s> on session %s.'%(client.handler.num, username, client.session_id))
if client.ip_address in self._root.trusted_proxies:
client.setFlagByIP(local_ip, False)
if reason.id == None:
client.db_id = client.session_id
else:
client.db_id = reason.id
self._root.db_ids[client.db_id] = client
client.ingame_time = int(reason.ingame_time)
client.bot = reason.bot
client.last_login = reason.last_login
client.register_date = reason.register_date
client.hook = reason.hook_chars
client.username = username
client.password = password
client.cpu = cpu
client.local_ip = None
if local_ip.startswith('127.') or not validateIP(local_ip):
client.local_ip = client.ip_address
else:
client.local_ip = local_ip
client.lobby_id = lobby_id
self._root.usernames[username] = client
client.Send('ACCEPTED %s'%username)
client.Send('MOTD Welcome, %s!' % username)
client.Send('MOTD There are currently %i clients connected' % len(self._root.clients))
client.Send('MOTD to the server talking in %i open channels' % len(self._root.channels))
client.Send('MOTD and participating in %i battles.' % len(self._root.battles))
client.Send('MOTD Server\'s uptime is %s' % self._time_since(self._root.start_time))
if self._root.motd:
client.Send('MOTD')
for line in list(self._root.motd):
client.Send('MOTD %s' % line)
self.broadcast_AddUser(client)
usernames = dict(self._root.usernames) # cache them here in case anyone joins/leaves or hosts/closes a battle
for user in usernames:
addclient = usernames[user]
client.AddUser(addclient)
battles = dict(self._root.battles)
for battle in battles:
battle = battles[battle]
ubattle = battle.copy()
client.AddBattle(battle)
client.SendBattle(battle, 'UPDATEBATTLEINFO %(id)s %(spectators)i %(locked)i %(maphash)s %(map)s' % ubattle)
for user in battle.users:
if not user == battle.host:
client.SendBattle(battle, 'JOINEDBATTLE %s %s' % (battle.id, user))
client.status = self._calc_status(client, 0)
self.broadcast_SendUser(client, 'CLIENTSTATUS %s %s'%(username, client.status))
for user in usernames:
if user == username: continue # potential problem spot, might need to check to make sure username is still in user db
client.SendUser(user, 'CLIENTSTATUS %s %s'%(user, usernames[user].status))
client.Send('LOGININFOEND')
else:
self._root.console_write('Handler %s: Failed to log in user <%s> on session %s. (rejected by database)'%(client.handler.num, username, client.session_id))
client.Send('DENIED %s'%reason)
else:
oldclient = self._root.usernames[username]
if oldclient.static:
client.Send('DENIED Cannot ghost static users.')
if time.time() - oldclient.lastdata > 15:
if self._root.dbtype == 'lan' and not oldclient.password == password:
client.Send('DENIED Would ghost old user, but we are in LAN mode and your password does not match.')
return
# kicks old user and logs in new user
oldclient.Remove('Ghosted')
self._root.console_write('Handler %s: Old client inactive, ghosting user <%s> from session %s.'%(client.handler.num, username, client.session_id))
self.in_LOGIN(client, username, password, cpu, local_ip, sentence_args)
else:
self._root.console_write('Handler %s: Failed to log in user <%s> on session %s. (already logged in)'%(client.handler.num, username, client.session_id))
client.Send('DENIED Already logged in.')
def in_CONFIRMAGREEMENT(self, client):
'Confirm the terms of service as shown with the AGREEMENT commands. Users must accept the terms of service to use their account.'
if client.access == 'agreement':
client.access = 'user'
self.userdb.save_user(client)
client.access = 'fresh'
self._calc_access_status(client)
def in_HOOK(self, client, chars=''):