~widelands-dev/widelands-website/trunk

« back to all changes in this revision

Viewing changes to wlms/protocol.py

  • Committer: Holger Rapp
  • Date: 2009-03-15 20:19:52 UTC
  • mto: This revision was merged to the branch mainline in revision 64.
  • Revision ID: sirver@kallisto.local-20090315201952-eaug9ff2ec8qx1au
Fixed a bug with broken notification support

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/env python
2
 
# encoding: utf-8
3
 
 
4
 
import logging
5
 
import re
6
 
 
7
 
from twisted.internet import reactor
8
 
from twisted.internet.endpoints import TCP4ClientEndpoint
9
 
from twisted.internet.protocol import Protocol, ClientFactory
10
 
 
11
 
from wlms.errors import MSCriticalError, MSError, MSGarbageError
12
 
from wlms.utils import make_packet, Packet
13
 
 
14
 
class Game(object):
15
 
    __slots__ = ("host", "max_players", "name", "buildid", "players", "_opening_time", "state")
16
 
 
17
 
    def __init__(self, opening_time, host, name, max_players, buildid):
18
 
        """
19
 
        Representing a currently running game.
20
 
 
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.
24
 
            Includes the host
25
 
        """
26
 
        self.host = host
27
 
        self.max_players = max_players
28
 
        self.players = set((host,))
29
 
        self.name = name
30
 
        self.buildid = buildid
31
 
 
32
 
        self.state = "ping_pending"
33
 
        self._opening_time = opening_time
34
 
 
35
 
    def __lt__(self, o):
36
 
        return self._opening_time < o._opening_time
37
 
 
38
 
    def start(self):
39
 
        self.state = "running"
40
 
 
41
 
    @property
42
 
    def connectable(self):
43
 
        if self.state != "accepting_connections":
44
 
            return False
45
 
        return not self.full
46
 
 
47
 
    @property
48
 
    def full(self):
49
 
        return len(self.players) >= self.max_players
50
 
 
51
 
import time
52
 
NETCMD_METASERVER_PING = "\x00\x03@"
53
 
class GamePing(Protocol):
54
 
 
55
 
    def __init__(self, fac, client_protocol, timeout):
56
 
        self._client_protocol = client_protocol
57
 
        self._noreplycall = self._client_protocol.callLater(
58
 
            timeout, fac.no_reply
59
 
        )
60
 
        self._fac = fac
61
 
 
62
 
    def connectionMade(self):
63
 
        self.transport.write(NETCMD_METASERVER_PING)
64
 
 
65
 
    def dataReceived(self, data):
66
 
        self._noreplycall.cancel()
67
 
        if data != NETCMD_METASERVER_PING:
68
 
            self._fac.no_reply()
69
 
            return
70
 
 
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
74
 
        # this silently.
75
 
        if game is None:
76
 
            return
77
 
 
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)
83
 
 
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,
89
 
        )
90
 
 
91
 
class GamePingFactory(ClientFactory):
92
 
    def __init__(self, client_protocol, timeout):
93
 
        self._client_protocol = client_protocol
94
 
        self._timeout = timeout
95
 
 
96
 
    def no_reply(self):
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"
102
 
        else:
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")
106
 
 
107
 
    def clientConnectionFailed(self, connector, reason):
108
 
        self.no_reply()
109
 
 
110
 
    def buildProtocol(self, addr):
111
 
        return GamePing(self, self._client_protocol, self._timeout)
112
 
 
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)
115
 
 
116
 
class MSProtocol(Protocol):
117
 
    _ALLOWED_PACKAGES = {
118
 
        "handshake": set(("LOGIN","DISCONNECT", "RELOGIN")),
119
 
        "lobby": set((
120
 
            "DISCONNECT", "CHAT", "CLIENTS", "RELOGIN", "PONG", "GAME_OPEN",
121
 
            "GAME_CONNECT", "GAMES", "MOTD", "ANNOUNCEMENT",
122
 
        )),
123
 
        "ingame": set((
124
 
            "DISCONNECT", "CHAT", "CLIENTS", "RELOGIN", "PONG", "GAMES",
125
 
            "GAME_DISCONNECT", "MOTD", "ANNOUNCEMENT", "GAME_START"
126
 
        )),
127
 
    }
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)
135
 
 
136
 
    def __init__(self, ms):
137
 
        self._login_time = self.seconds()
138
 
        self._pinger = None
139
 
        self._cleaner = None
140
 
        self._have_sended_ping = False
141
 
        self._ms = ms
142
 
        self._name = None
143
 
        self._state = "handshake"
144
 
        self._recently_disconnected = False
145
 
        self._game = ""
146
 
        self._buildid = None
147
 
        self._permissions = None
148
 
 
149
 
        self._d = ""
150
 
 
151
 
    def _copy_attr(self, o):
152
 
        self._login_time = o._login_time
153
 
        self._ms = o._ms
154
 
        self._name = o._name
155
 
        self._state = o._state
156
 
        self._game = o._game
157
 
        self._buildid = o._buildid
158
 
        self._permissions = o._permissions
159
 
 
160
 
 
161
 
    def __lt__(self, o):
162
 
        return self._login_time < o._login_time
163
 
 
164
 
    @property
165
 
    def active(self):
166
 
        return not self._recently_disconnected
167
 
 
168
 
    def connectionLost(self, reason):
169
 
        logging.info("%r disconnected: %s", self._name, reason.getErrorMessage())
170
 
        if self._pinger:
171
 
            self._pinger.cancel()
172
 
            self._pinger = None
173
 
        if self._state != "DISCONNECTED":
174
 
            self._cleaner = self.callLater(self.REMEMBER_CLIENT_FOR, self._forget_me, True)
175
 
            self._recently_disconnected = True
176
 
        else:
177
 
            self._forget_me()
178
 
            self._ms.broadcast("CLIENTS_UPDATE")
179
 
 
180
 
 
181
 
    def dataReceived(self, data):
182
 
        self._d += data
183
 
 
184
 
        if self._pinger:
185
 
            self._pinger.reset(self.PING_WHEN_SILENT_FOR)
186
 
            self._have_sended_ping = False
187
 
 
188
 
        while True:
189
 
            packet = self._read_packet()
190
 
            if packet is None:
191
 
                break
192
 
            logging.debug("<- %s: %s", self._name, packet)
193
 
            packet = Packet(packet)
194
 
 
195
 
            try:
196
 
                cmd = packet.string()
197
 
                if cmd in self._ALLOWED_PACKAGES[self._state]:
198
 
                    func = getattr(self, "_handle_%s" % cmd)
199
 
                    try:
200
 
                        func(packet)
201
 
                    except MSGarbageError as e:
202
 
                        e.args = (cmd,) + e.args[1:]
203
 
                        raise e
204
 
                    except MSError as e:
205
 
                        e.args = (cmd,) + e.args
206
 
                        raise e
207
 
                else:
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()
213
 
                return
214
 
            except MSError as e:
215
 
                self.send("ERROR", *e.args)
216
 
 
217
 
    def send(self, *args):
218
 
        logging.debug("-> %s: %s", self._name, args)
219
 
        self.transport.write(make_packet(*args))
220
 
 
221
 
    # Private Functions {{{
222
 
    def _read_packet(self):
223
 
        if len(self._d) < 2: # Length there?
224
 
            return
225
 
 
226
 
        size = (ord(self._d[0]) << 8) + ord(self._d[1])
227
 
        if len(self._d) < size: # All of packet there?
228
 
            return
229
 
 
230
 
        packet_data = self._d[2:2+(size-2)]
231
 
        self._d = self._d[size:]
232
 
 
233
 
        return packet_data[:-1].split('\x00')
234
 
 
235
 
    def _ping_or_disconnect(self):
236
 
        if not self._have_sended_ping:
237
 
            self.send("PING")
238
 
            self._have_sended_ping = True
239
 
        else:
240
 
            self.send("DISCONNECT", "CLIENT_TIMEOUT")
241
 
            self.transport.loseConnection()
242
 
        self._pinger = self.callLater(self.PING_WHEN_SILENT_FOR, self._ping_or_disconnect)
243
 
 
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()
247
 
        self._cleaner = None
248
 
        if self._name in self._ms.users:
249
 
            del self._ms.users[self._name]
250
 
 
251
 
    def _handle_LOGIN(self, p, cmdname = "LOGIN"):
252
 
        self._protocol_version, name, self._buildid, is_registered = p.unpack("issb")
253
 
 
254
 
        if self._protocol_version != 0:
255
 
            raise MSCriticalError("UNSUPPORTED_PROTOCOL")
256
 
 
257
 
        if is_registered:
258
 
            rv = self._ms.db.check_user(name, p.string())
259
 
            if rv is False:
260
 
                raise MSError("WRONG_PASSWORD")
261
 
            if name in self._ms.users:
262
 
                ou = self._ms.users[name]
263
 
                if ou._recently_disconnected:
264
 
                    ou._forget_me()
265
 
                else:
266
 
                    raise MSError("ALREADY_LOGGED_IN")
267
 
            self._name = name
268
 
            self._permissions = rv
269
 
        else:
270
 
            # Find a name that is not yet in use
271
 
            temp = name
272
 
            n = 1
273
 
            while temp in self._ms.users or self._ms.db.user_exists(temp):
274
 
                temp = name + str(n)
275
 
                n += 1
276
 
            self._name = temp
277
 
            self._permissions = "UNREGISTERED"
278
 
 
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)
283
 
 
284
 
        self.send("TIME", int(self.seconds()))
285
 
 
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")
289
 
 
290
 
        if self._ms.motd:
291
 
            self.send("CHAT", '', self._ms.motd, "system")
292
 
 
293
 
 
294
 
    def _handle_MOTD(self, p):
295
 
        motd = p.string()
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")
299
 
        self._ms.motd = motd
300
 
        self._ms.broadcast("CHAT", '', self._ms.motd, "system")
301
 
 
302
 
    def _handle_ANNOUNCEMENT(self, p):
303
 
        msg = p.string()
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")
308
 
 
309
 
 
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")
316
 
            defered.cancel()
317
 
 
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")
322
 
 
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")
329
 
 
330
 
        if is_registered and not self._ms.db.check_user(name, p.string()):
331
 
            raise MSError("WRONG_INFORMATION")
332
 
 
333
 
        def _try_relogin():
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()
339
 
            u._forget_me()
340
 
            self._copy_attr(u)
341
 
            self._ms.users[self._name] = self
342
 
            self.send("RELOGIN")
343
 
 
344
 
        logging.info("%s wants to relogin.", name)
345
 
        if u._recently_disconnected:
346
 
            _try_relogin()
347
 
        else:
348
 
            u.send("PING")
349
 
            defered = self.callLater(5, _try_relogin)
350
 
            self._ms.users_wanting_to_relogin[u._name] = (u, self, defered)
351
 
 
352
 
    def _handle_CLIENTS(self, p):
353
 
        args = ["CLIENTS"]
354
 
        tempnumber = 0
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:
361
 
                continue
362
 
            args.extend((u._name, u._buildid, u._game, u._permissions, ''))
363
 
        self.send(*args)
364
 
 
365
 
    def _handle_CHAT(self, p):
366
 
        msg, recipient = p.unpack("ss")
367
 
 
368
 
        # Sanitize the msg: remove < and replace via '&lt;'
369
 
        msg = msg.replace("<", "&lt;")
370
 
 
371
 
        if not recipient: # Public Message
372
 
            self._ms.broadcast("CHAT", self._name, msg, "public")
373
 
        else:
374
 
            if recipient in self._ms.users:
375
 
                self._ms.users[recipient].send("CHAT", self._name, msg, "private")
376
 
            else:
377
 
                self.send("ERROR", "CHAT", "NO_SUCH_USER", recipient)
378
 
 
379
 
    def _handle_GAME_OPEN(self, p):
380
 
        name, max_players = p.unpack("si")
381
 
 
382
 
        if name in self._ms.games:
383
 
            raise MSError("GAME_EXISTS")
384
 
 
385
 
        game = Game(self.seconds(), self._name, name, max_players, self._buildid)
386
 
        self._ms.games[name] = game
387
 
        self._ms.broadcast("GAMES_UPDATE")
388
 
 
389
 
        self._game = name
390
 
        self._state = "ingame"
391
 
        self._ms.broadcast("CLIENTS_UPDATE")
392
 
 
393
 
        self.create_game_pinger(self, self.GAME_PING_TIME_FOR_FIRST_REPLY)
394
 
 
395
 
        logging.info("%r has opened a new game called %r. Waiting for game pong.", self._name, game.name)
396
 
 
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))
402
 
        self.send(*args)
403
 
 
404
 
    def _handle_GAME_CONNECT(self, p):
405
 
        name, = p.unpack("s")
406
 
 
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")
412
 
        if game.full:
413
 
            raise MSError("GAME_FULL")
414
 
 
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)
418
 
 
419
 
        self._game = game.name
420
 
        self._state = "ingame"
421
 
        self._ms.broadcast("CLIENTS_UPDATE")
422
 
 
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]
427
 
 
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)
433
 
 
434
 
        self._game = ""
435
 
        self._state = "lobby"
436
 
        self._ms.broadcast("CLIENTS_UPDATE")
437
 
        if send_games_update:
438
 
            self._ms.broadcast("GAMES_UPDATE")
439
 
 
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")
445
 
                return
446
 
 
447
 
            game.start()
448
 
            self.send("GAME_START")
449
 
            self._ms.broadcast("GAMES_UPDATE")
450
 
 
451
 
    def _handle_DISCONNECT(self, p):
452
 
        reason = p.string()
453
 
        logging.info("%r left: %s", self._name, reason)
454
 
        self._state = "DISCONNECTED"
455
 
        self.transport.loseConnection()
456
 
    # End: Private Functions }}}
457
 
 
458
 
 
459