7
from twisted.internet import reactor
8
from twisted.internet.endpoints import TCP4ClientEndpoint
9
from twisted.internet.protocol import Protocol, ClientFactory
11
from wlms.errors import MSCriticalError, MSError, MSGarbageError
12
from wlms.utils import make_packet, Packet
15
__slots__ = ("host", "max_players", "name", "buildid", "players", "_opening_time", "state")
17
def __init__(self, opening_time, host, name, max_players, buildid):
19
Representing a currently running game.
21
:host: The name of the hosting player
22
:max_players: Number of players the game can hold
23
:players: a set of the player names currently in the game.
27
self.max_players = max_players
28
self.players = set((host,))
30
self.buildid = buildid
32
self.state = "ping_pending"
33
self._opening_time = opening_time
36
return self._opening_time < o._opening_time
39
self.state = "running"
42
def connectable(self):
43
if self.state != "accepting_connections":
49
return len(self.players) >= self.max_players
52
NETCMD_METASERVER_PING = "\x00\x03@"
53
class GamePing(Protocol):
55
def __init__(self, fac, client_protocol, timeout):
56
self._client_protocol = client_protocol
57
self._noreplycall = self._client_protocol.callLater(
62
def connectionMade(self):
63
self.transport.write(NETCMD_METASERVER_PING)
65
def dataReceived(self, data):
66
self._noreplycall.cancel()
67
if data != NETCMD_METASERVER_PING:
71
game = self._client_protocol._ms.games.get(self._client_protocol._game, None)
72
# This could be a game ping for a game that has been ended and a new
73
# one has already started. If we know nothing about the game, ignore
78
if game.state == "ping_pending": # Game is valid. Let's go
79
game.state = "accepting_connections"
80
self._client_protocol.send("GAME_OPEN")
81
self._client_protocol._ms.broadcast("GAMES_UPDATE")
82
logging.info("Game Pong for %s received. Game is connectable!", game.name)
84
self._client_protocol.callLater(
85
self._client_protocol.GAME_PING_PAUSE,
86
self._client_protocol.create_game_pinger,
87
self._client_protocol,
88
self._client_protocol.GAME_PING_PAUSE,
91
class GamePingFactory(ClientFactory):
92
def __init__(self, client_protocol, timeout):
93
self._client_protocol = client_protocol
94
self._timeout = timeout
97
game = self._client_protocol._ms.games[self._client_protocol._game]
98
if game.state == "ping_pending": # Game is valid. Let's go
99
self._client_protocol.send("ERROR", "GAME_OPEN", "GAME_TIMEOUT")
100
logging.info("Game Pong for %s not received. Game is unreachable after opening!", game.name)
101
game.state = "unreachable"
103
logging.info("Game Pong for %s not received. Game is no longer reachable, so we assume it is over.", game.name)
104
del self._client_protocol._ms.games[game.name]
105
self._client_protocol._ms.broadcast("GAMES_UPDATE")
107
def clientConnectionFailed(self, connector, reason):
110
def buildProtocol(self, addr):
111
return GamePing(self, self._client_protocol, self._timeout)
113
def _create_game_pinger(pc, timeout):
114
reactor.connectTCP(pc.transport.client[0], 7396, GamePingFactory(pc, timeout), pc.GAME_PING_TIME_FOR_FIRST_REPLY)
116
class MSProtocol(Protocol):
117
_ALLOWED_PACKAGES = {
118
"handshake": set(("LOGIN","DISCONNECT", "RELOGIN")),
120
"DISCONNECT", "CHAT", "CLIENTS", "RELOGIN", "PONG", "GAME_OPEN",
121
"GAME_CONNECT", "GAMES", "MOTD", "ANNOUNCEMENT",
124
"DISCONNECT", "CHAT", "CLIENTS", "RELOGIN", "PONG", "GAMES",
125
"GAME_DISCONNECT", "MOTD", "ANNOUNCEMENT", "GAME_START"
128
REMEMBER_CLIENT_FOR = 60*5
129
PING_WHEN_SILENT_FOR = 10
130
GAME_PING_TIME_FOR_FIRST_REPLY = 5
131
GAME_PING_PAUSE = 120
132
callLater = reactor.callLater
133
seconds = reactor.seconds
134
create_game_pinger = staticmethod(_create_game_pinger)
136
def __init__(self, ms):
137
self._login_time = self.seconds()
140
self._have_sended_ping = False
143
self._state = "handshake"
144
self._recently_disconnected = False
147
self._permissions = None
151
def _copy_attr(self, o):
152
self._login_time = o._login_time
155
self._state = o._state
157
self._buildid = o._buildid
158
self._permissions = o._permissions
162
return self._login_time < o._login_time
166
return not self._recently_disconnected
168
def connectionLost(self, reason):
169
logging.info("%r disconnected: %s", self._name, reason.getErrorMessage())
171
self._pinger.cancel()
173
if self._state != "DISCONNECTED":
174
self._cleaner = self.callLater(self.REMEMBER_CLIENT_FOR, self._forget_me, True)
175
self._recently_disconnected = True
178
self._ms.broadcast("CLIENTS_UPDATE")
181
def dataReceived(self, data):
185
self._pinger.reset(self.PING_WHEN_SILENT_FOR)
186
self._have_sended_ping = False
189
packet = self._read_packet()
192
logging.debug("<- %s: %s", self._name, packet)
193
packet = Packet(packet)
196
cmd = packet.string()
197
if cmd in self._ALLOWED_PACKAGES[self._state]:
198
func = getattr(self, "_handle_%s" % cmd)
201
except MSGarbageError as e:
202
e.args = (cmd,) + e.args[1:]
205
e.args = (cmd,) + e.args
208
raise MSGarbageError("INVALID_CMD")
209
except MSCriticalError as e:
210
self.send("ERROR", *e.args)
211
logging.warning("Terminating connection to %r: %s", self._name, e.args)
212
self.transport.loseConnection()
215
self.send("ERROR", *e.args)
217
def send(self, *args):
218
logging.debug("-> %s: %s", self._name, args)
219
self.transport.write(make_packet(*args))
221
# Private Functions {{{
222
def _read_packet(self):
223
if len(self._d) < 2: # Length there?
226
size = (ord(self._d[0]) << 8) + ord(self._d[1])
227
if len(self._d) < size: # All of packet there?
230
packet_data = self._d[2:2+(size-2)]
231
self._d = self._d[size:]
233
return packet_data[:-1].split('\x00')
235
def _ping_or_disconnect(self):
236
if not self._have_sended_ping:
238
self._have_sended_ping = True
240
self.send("DISCONNECT", "CLIENT_TIMEOUT")
241
self.transport.loseConnection()
242
self._pinger = self.callLater(self.PING_WHEN_SILENT_FOR, self._ping_or_disconnect)
244
def _forget_me(self, from_cleaner = False): # We have been disconnected for a long time, finally forget me
245
if not from_cleaner and self._cleaner:
246
self._cleaner.cancel()
248
if self._name in self._ms.users:
249
del self._ms.users[self._name]
251
def _handle_LOGIN(self, p, cmdname = "LOGIN"):
252
self._protocol_version, name, self._buildid, is_registered = p.unpack("issb")
254
if self._protocol_version != 0:
255
raise MSCriticalError("UNSUPPORTED_PROTOCOL")
258
rv = self._ms.db.check_user(name, p.string())
260
raise MSError("WRONG_PASSWORD")
261
if name in self._ms.users:
262
ou = self._ms.users[name]
263
if ou._recently_disconnected:
266
raise MSError("ALREADY_LOGGED_IN")
268
self._permissions = rv
270
# Find a name that is not yet in use
273
while temp in self._ms.users or self._ms.db.user_exists(temp):
277
self._permissions = "UNREGISTERED"
279
self.send("LOGIN", self._name, self._permissions)
280
self._state = "lobby"
281
self._login_time = self.seconds()
282
self._pinger = self.callLater(self.PING_WHEN_SILENT_FOR, self._ping_or_disconnect)
284
self.send("TIME", int(self.seconds()))
286
logging.info("%r has logged in as %s", self._name, self._permissions)
287
self._ms.users[self._name] = self
288
self._ms.broadcast("CLIENTS_UPDATE")
291
self.send("CHAT", '', self._ms.motd, "system")
294
def _handle_MOTD(self, p):
296
if self._permissions != "SUPERUSER":
297
logging.warning("%r tried setting MOTD with permission %s. Denied.", self._name, self._permissions)
298
raise MSError("DEFICIENT_PERMISSION")
300
self._ms.broadcast("CHAT", '', self._ms.motd, "system")
302
def _handle_ANNOUNCEMENT(self, p):
304
if self._permissions != "SUPERUSER":
305
logging.warning("%r tried to send an announcement with permission %s. Denied.", self._name, self._permissions)
306
raise MSError("DEFICIENT_PERMISSION")
307
self._ms.broadcast("CHAT", '', msg, "system")
310
def _handle_PONG(self, p):
311
if self._name in self._ms.users_wanting_to_relogin:
312
# No, you can't relogin. Sorry
313
pself, new, defered = self._ms.users_wanting_to_relogin.pop(self._name)
314
assert(pself is self)
315
new.send("ERROR", "RELOGIN", "CONNECTION_STILL_ALIVE")
318
def _handle_RELOGIN(self, p):
319
pv, name, build_id, is_registered = p.unpack("issb")
320
if name not in self._ms.users:
321
raise MSError("NOT_LOGGED_IN")
323
u = self._ms.users[name]
324
if (u._protocol_version != pv or u._buildid != build_id):
325
raise MSError("WRONG_INFORMATION")
326
if (is_registered and u._permissions == "UNREGISTERED" or
327
(not is_registered and u._permissions == "REGISTERED")):
328
raise MSError("WRONG_INFORMATION")
330
if is_registered and not self._ms.db.check_user(name, p.string()):
331
raise MSError("WRONG_INFORMATION")
334
logging.info("User %s has not answered relogin ping. Kicking and replacing!", u._name)
335
self._ms.users_wanting_to_relogin.pop(u._name, None)
336
if not u._recently_disconnected:
337
u.send("DISCONNECT", "CLIENT_TIMEOUT")
338
u.transport.loseConnection()
341
self._ms.users[self._name] = self
344
logging.info("%s wants to relogin.", name)
345
if u._recently_disconnected:
349
defered = self.callLater(5, _try_relogin)
350
self._ms.users_wanting_to_relogin[u._name] = (u, self, defered)
352
def _handle_CLIENTS(self, p):
355
for u in sorted(self._ms.users.values()):
356
if not u._recently_disconnected:
357
tempnumber = tempnumber + 1
358
args.extend(str(tempnumber))
359
for u in sorted(self._ms.users.values()):
360
if u._recently_disconnected:
362
args.extend((u._name, u._buildid, u._game, u._permissions, ''))
365
def _handle_CHAT(self, p):
366
msg, recipient = p.unpack("ss")
368
# Sanitize the msg: remove < and replace via '<'
369
msg = msg.replace("<", "<")
371
if not recipient: # Public Message
372
self._ms.broadcast("CHAT", self._name, msg, "public")
374
if recipient in self._ms.users:
375
self._ms.users[recipient].send("CHAT", self._name, msg, "private")
377
self.send("ERROR", "CHAT", "NO_SUCH_USER", recipient)
379
def _handle_GAME_OPEN(self, p):
380
name, max_players = p.unpack("si")
382
if name in self._ms.games:
383
raise MSError("GAME_EXISTS")
385
game = Game(self.seconds(), self._name, name, max_players, self._buildid)
386
self._ms.games[name] = game
387
self._ms.broadcast("GAMES_UPDATE")
390
self._state = "ingame"
391
self._ms.broadcast("CLIENTS_UPDATE")
393
self.create_game_pinger(self, self.GAME_PING_TIME_FOR_FIRST_REPLY)
395
logging.info("%r has opened a new game called %r. Waiting for game pong.", self._name, game.name)
397
def _handle_GAMES(self, p):
398
args = ["GAMES", len(self._ms.games)]
399
for game in sorted(self._ms.games.values()):
400
con = "true" if game.connectable else "false"
401
args.extend((game.name, game.buildid, con))
404
def _handle_GAME_CONNECT(self, p):
405
name, = p.unpack("s")
407
if name not in self._ms.games:
408
raise MSError("NO_SUCH_GAME")
409
game = self._ms.games[name]
410
if game.host not in self._ms.users:
411
raise MSError("INVALID_HOST")
413
raise MSError("GAME_FULL")
415
self.send("GAME_CONNECT", self._ms.users[game.host].transport.client[0])
416
game.players.add(self._name)
417
logging.info("%r has joined the game %r", self._name, game.name)
419
self._game = game.name
420
self._state = "ingame"
421
self._ms.broadcast("CLIENTS_UPDATE")
423
def _handle_GAME_DISCONNECT(self, p):
424
send_games_update = False
425
if self._game in self._ms.games:
426
game = self._ms.games[self._game]
428
game.players.remove(self._name)
429
if game.host == self._name:
430
del self._ms.games[game.name]
431
send_games_update = True
432
logging.info("%r has left the game %r", self._name, game.name)
435
self._state = "lobby"
436
self._ms.broadcast("CLIENTS_UPDATE")
437
if send_games_update:
438
self._ms.broadcast("GAMES_UPDATE")
440
def _handle_GAME_START(self, p):
441
if self._game in self._ms.games:
442
game = self._ms.games[self._game]
443
if game.host != self._name:
444
raise MSError("DEFICIENT_PERMISSION")
448
self.send("GAME_START")
449
self._ms.broadcast("GAMES_UPDATE")
451
def _handle_DISCONNECT(self, p):
453
logging.info("%r left: %s", self._name, reason)
454
self._state = "DISCONNECTED"
455
self.transport.loseConnection()
456
# End: Private Functions }}}