1
1
#!/usr/bin/env python
4
from twisted.internet.protocol import Factory, Protocol
4
from optparse import OptionParser
5
6
from twisted.internet import reactor
8
from wlms import MetaServer
12
9
from wlms.db.flatfile import FlatFileDatabase
14
11
# TODO: PING regularly when no data came around
16
12
# TODO: GAME_START / GAME_END
18
16
# TODO: check chat messages for richtext tags and deny them if they include them. Only systemmessages like
19
17
# the motd may contain them.
21
class MSError(Exception):
22
def __init__(self, *args):
24
class MSCriticalError(MSError):
25
def __init__(self, *args):
27
class MSGarbageError(MSCriticalError):
28
def __init__(self, *args):
29
MSCriticalError.__init__(self, "GARBAGE_RECEIVED", *args)
35
raise MSGarbageError("Wanted a string but got no arguments left")
42
raise MSGarbageError("Invalid integer: %r" % s)
46
if s == "1" or s.lower() == "true":
48
elif s == "0" or s.lower() == "false":
50
raise MSGarbageError("Invalid bool: %r" % s)
57
def _unpack(codes, p):
58
return [ __CODES2FUNC[c](p) for c in codes ]
60
def make_packet(*args):
61
pstr = ''.join(str(x) + '\x00' for x in args)
63
return chr(size >> 8) + chr(size & 0xff) + pstr
66
__slots__ = ("host", "max_players", "name", "buildid", "players", "_opening_time")
68
def __init__(self, host, name, max_players, buildid):
70
Representing a currently running game.
72
:host: The name of the hosting player
73
:max_players: Number of players the game can hold
74
:players: a set of the player names currently in the game.
78
self.max_players = max_players
79
self.players = set((host,))
81
self.buildid = buildid
83
self._opening_time = time.time()
86
return self._opening_time < o._opening_time
89
def connectable(self): # TODO check connectability
94
return len(self.players) >= self.max_players
97
class MSConnection(Protocol):
99
"HANDSHAKE": set(("LOGIN","DISCONNECT", "RELOGIN")),
101
"DISCONNECT", "CHAT", "CLIENTS", "RELOGIN", "PONG", "GAME_OPEN",
102
"GAME_CONNECT", "GAMES", "MOTD",
105
"DISCONNECT", "CHAT", "CLIENTS", "RELOGIN", "PONG", "GAMES",
106
"GAME_DISCONNECT", "MOTD",
109
callLater = reactor.callLater
111
def __init__(self, ms):
112
self._login_time = time.time()
115
self._state = "HANDSHAKE"
121
return self._login_time < o._login_time
123
def connectionLost(self, reason):
124
self._ms.disconnected(self)
126
def _read_packet(self):
127
if len(self._d) < 2: # Length there?
130
size = (ord(self._d[0]) << 8) + ord(self._d[1])
131
if len(self._d) < size: # All of packet there?
134
packet_data = self._d[2:2+(size-2)]
135
self._d = self._d[size:]
137
return packet_data[:-1].split('\x00')
139
def dataReceived(self, data):
143
packet = self._read_packet()
148
cmd = _string(packet)
149
if cmd in self._ALLOWED_PACKAGES[self._state]:
150
func = getattr(self, "_handle_%s" % cmd)
153
except MSGarbageError as e:
154
e.args = (cmd,) + e.args[1:]
157
e.args = (cmd,) + e.args
160
raise MSGarbageError("INVALID_CMD")
161
except MSCriticalError as e:
162
self.send("ERROR", *e.args)
163
self.transport.loseConnection()
166
self.send("ERROR", *e.args)
169
def send(self, *args):
170
self.transport.write(make_packet(*args))
172
def _handle_LOGIN(self, p, cmdname = "LOGIN"):
173
self._protocol_version, name, self._buildid, is_registered = _unpack("issb", p)
175
if self._protocol_version != 0:
176
raise MSCriticalError("UNSUPPORTED_PROTOCOL")
179
rv = self._ms.db.check_user(name, _string(p))
181
raise MSError("WRONG_PASSWORD")
182
if name in self._ms.users:
183
raise MSError("ALREADY_LOGGED_IN")
185
self._permissions = rv
187
# Find a name that is not yet in use
190
while temp in self._ms.users or self._ms.db.user_exists(temp):
194
self._permissions = "UNREGISTERED"
196
self.send("LOGIN", self._name, self._permissions)
197
self._state = "LOBBY"
198
self._login_time = time.time()
199
self.send("TIME", int(time.time()))
200
self._ms.connected(self)
203
self.send("CHAT", '', self._ms.motd, "false", "true")
206
def _handle_MOTD(self, p):
208
if self._permissions != "SUPERUSER":
209
raise MSError("DEFICIENT_PERMISSION")
211
self._ms.broadcast("CHAT", '', self._ms.motd, "false", "true")
214
def _handle_PONG(self, p):
215
if self._name in self._ms.users_wanting_to_relogin:
216
# No, you can't relogin. Sorry
217
pself, new, defered = self._ms.users_wanting_to_relogin.pop(self._name)
218
assert(pself is self)
219
new.send("ERROR", "RELOGIN", "CONNECTION_STILL_ALIVE")
222
def _handle_RELOGIN(self, p):
223
pv, name, build_id, is_registered = _unpack("issb", p)
224
if name not in self._ms.users:
225
raise MSError("NOT_LOGGED_IN")
227
u = self._ms.users[name]
228
if (u._protocol_version != pv or u._buildid != build_id):
229
raise MSError("WRONG_INFORMATION")
230
if (is_registered and u._permissions == "UNREGISTERED" or
231
(not is_registered and u._permissions == "REGISTERED")):
232
raise MSError("WRONG_INFORMATION")
234
if is_registered and not self._ms.db.check_user(name, _string(p)):
235
raise MSError("WRONG_INFORMATION")
240
del self._ms.users_wanting_to_relogin[u._name]
241
u.send("DISCONNECT", "TIMEOUT")
242
u.transport.loseConnection()
243
self._ms.users[self._name] = self
245
defered = self.callLater(5, _try_relogin)
246
self._ms.users_wanting_to_relogin[u._name] = (u, self, defered)
248
def _handle_CLIENTS(self, p):
249
args = ["CLIENTS", len(self._ms.users)]
250
for u in sorted(self._ms.users.values()):
251
args.extend((u._name, u._buildid, u._game, u._permissions, ''))
254
def _handle_CHAT(self, p):
255
msg, recipient = _unpack("ss", p)
256
if not recipient: # Public Message
257
self._ms.broadcast("CHAT", self._name, msg, "false", "false")
259
if recipient in self._ms.users:
260
self._ms.users[recipient].send("CHAT", self._name, msg, "true", "false")
262
self.send("ERROR", "CHAT", "NO_SUCH_USER", recipient)
264
def _handle_GAME_OPEN(self, p):
265
name, max_players = _unpack("si", p)
267
if name in self._ms.games:
268
raise MSError("GAME_EXISTS")
270
game = Game(self._name, name, max_players, self._buildid)
271
self._ms.games[name] = game
272
self._ms.broadcast("GAMES_UPDATE")
275
self._state = "INGAME"
276
self._ms.broadcast("CLIENTS_UPDATE")
278
self.send("GAME_OPEN")
280
def _handle_GAMES(self, p):
281
args = ["GAMES", len(self._ms.games)]
282
for game in sorted(self._ms.games.values()):
283
con = "true" if game.connectable else "false"
284
args.extend((game.name, game.buildid, con))
287
def _handle_GAME_CONNECT(self, p):
288
name, = _unpack("s", p)
290
if name not in self._ms.games:
291
raise MSError("NO_SUCH_GAME")
292
game = self._ms.games[name]
293
if game.host not in self._ms.users:
294
raise MSError("INVALID_HOST")
296
raise MSError("GAME_FULL")
298
self.send("GAME_CONNECT", self._ms.users[game.host].transport.client[0])
299
game.players.add(self._name)
301
self._game = game.name
302
self._state = "INGAME"
303
self._ms.broadcast("CLIENTS_UPDATE")
305
def _handle_GAME_DISCONNECT(self, p):
307
send_games_update = False
308
if self._game in self._ms.games:
309
game = self._ms.games[self._game]
311
game.players.remove(self._name)
312
if game.host == self._name:
313
del self._ms.games[game.name]
314
send_games_update = True
317
self.send("GAME_DISCONNECT")
318
self._state = "LOBBY"
319
self._ms.broadcast("CLIENTS_UPDATE")
320
if send_games_update:
321
self._ms.broadcast("GAMES_UPDATE")
323
def _handle_DISCONNECT(self, p):
324
reason = _string(p) # TODO: do somethinwith the reason
325
self.transport.loseConnection()
326
self._ms.disconnected(self)
330
class MetaServer(Factory):
331
def __init__(self, db):
334
self.users_wanting_to_relogin = {}
338
def buildProtocol(self, addr):
339
return MSConnection(self)
342
def disconnected(self, con):
343
if con._name in self.users:
344
del self.users[con._name]
345
self.broadcast("CLIENTS_UPDATE")
347
def connected(self, con):
348
self.users[con._name] = con
349
self.broadcast("CLIENTS_UPDATE")
351
def broadcast(self, *args):
352
"""Send a message to all connected clients"""
353
for con in self.users.values():
356
from optparse import OptionParser
359
20
parser = OptionParser()
360
21
parser.add_option("-d", "--dbfile", default="",
361
22
help="Use flatfile database. File format is 'user\\tpassword\\tpermissions\n'", metavar="DB")
362
23
parser.add_option("-p", "--port", type=int, default=7395,
363
24
help="Listen on this port")
365
return parser.parse_args()
25
parser.add_option("-l", "--log", type=str, default="warning",
26
help="level of logging. Can be debug, info, warning, error, critical. [%default]")
27
parser.add_option("-f", "--logfile", type=str, default=None,
28
help="Logfile to use. Otherwise, logging goes to the console.")
30
o, args = parser.parse_args()
32
numeric_level = getattr(logging, o.log.upper(), None)
33
if not isinstance(numeric_level, int):
34
raise ValueError('Invalid log level: %s' % loglevel)
35
logging.basicConfig(format='%(asctime)s %(levelname)s:%(message)s',
36
level=numeric_level, filename=o.logfile)
368
41
o, args = parse_args()