Test Failed
Push — master ( fcf1be...4a2af5 )
by Heiko 'riot'
04:20
created

ClientBaseManager.read()   F

Complexity

Conditions 15

Size

Total Lines 82
Code Lines 56

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 15
eloc 56
nop 2
dl 0
loc 82
rs 2.9998
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like isomer.ui.clientmanager.basemanager.ClientBaseManager.read() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
#!/usr/bin/env python
2
# -*- coding: UTF-8 -*-
3
4
# Isomer - The distributed application framework
5
# ==============================================
6
# Copyright (C) 2011-2020 Heiko 'riot' Weinen <[email protected]> and others.
7
#
8
# This program is free software: you can redistribute it and/or modify
9
# it under the terms of the GNU Affero General Public License as published by
10
# the Free Software Foundation, either version 3 of the License, or
11
# (at your option) any later version.
12
#
13
# This program is distributed in the hope that it will be useful,
14
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
# GNU Affero General Public License for more details.
17
#
18
# You should have received a copy of the GNU Affero General Public License
19
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
21
"""
22
23
24
Module clientmanager.basemanager
25
================================
26
27
Basic client management functionality and component set up.
28
29
30
"""
31
32
import json
33
from base64 import b64decode
34
from time import time
35
from uuid import uuid4
36
from socket import socket
37
38
from circuits.net.events import write
39
from isomer.component import ConfigurableComponent, handler
40
from isomer.database import objectmodels
41
from isomer.events.client import clientdisconnect, userlogout, send
42
43
from isomer.logger import debug, critical, verbose, error, warn, network
44
from isomer.ui.clientobjects import Socket, Client
45
46
from isomer.ui.clientmanager.encoder import ComplexEncoder
47
48
49
class ClientBaseManager(ConfigurableComponent):
50
    """
51
    Handles client connections and requests as well as client-outbound
52
    communication.
53
    """
54
55
    channel = "isomer-web"
56
57
    def __init__(self, *args, **kwargs):
58
        super(ClientBaseManager, self).__init__("CM", *args, **kwargs)
59
60
        self._clients = {}
61
        self._sockets = {}
62
        self._users = {}
63
        self._count = 0
64
        self._user_mapping = {}
65
66
        self._erroneous_clients = {}
67
        self._bans = {}
68
69
    @handler("disconnect", channel="wsserver")
70
    def disconnect(self, sock):
71
        """Handles socket disconnections"""
72
73
        self.log("Disconnect ", sock, lvl=debug)
74
75
        try:
76
            if sock in self._sockets:
77
                self.log("Getting socket", lvl=debug)
78
                socket_object = self._sockets[sock]
79
                self.log("Getting clientuuid", lvl=debug)
80
                clientuuid = socket_object.clientuuid
81
                self.log("getting useruuid", lvl=debug)
82
                useruuid = self._clients[clientuuid].useruuid
83
84
                self.log("Firing disconnect event", lvl=debug)
85
                self.fireEvent(
86
                    clientdisconnect(clientuuid, self._clients[clientuuid].useruuid)
87
                )
88
89
                self.log("Logging out relevant client", lvl=debug)
90
                if useruuid is not None:
91
                    self.log("Client was logged in", lvl=debug)
92
                    try:
93
                        self._logout_client(useruuid, clientuuid)
94
                        self.log("Client logged out", useruuid, clientuuid)
95
                    except Exception as e:
96
                        self.log(
97
                            "Couldn't clean up logged in user! ",
98
                            self._users[useruuid],
99
                            e,
100
                            type(e),
101
                            lvl=critical,
102
                        )
103
                self.log("Deleting Client (", self._clients.keys, ")", lvl=debug)
104
                del self._clients[clientuuid]
105
                self.log("Deleting Socket", lvl=debug)
106
                del self._sockets[sock]
107
        except Exception as e:
108
            self.log("Error during disconnect handling: ", e, type(e), lvl=critical)
109
110
    def _logout_client(self, useruuid, clientuuid):
111
        """Log out a client and possibly associated user"""
112
113
        self.log("Cleaning up client of logged in user.", lvl=debug)
114
        try:
115
            self._users[useruuid].clients.remove(clientuuid)
116
            if len(self._users[useruuid].clients) == 0:
117
                self.log("Last client of user disconnected.", lvl=verbose)
118
119
                self.fireEvent(userlogout(useruuid, clientuuid))
120
                del self._users[useruuid]
121
122
            self._clients[clientuuid].useruuid = None
123
        except Exception as e:
124
            self.log(
125
                "Error during client logout: ",
126
                e,
127
                type(e),
128
                clientuuid,
129
                useruuid,
130
                lvl=error,
131
                exc=True,
132
            )
133
134
    @handler("connect", channel="wsserver")
135
    def connect(self, *args):
136
        """Registers new sockets and their clients and allocates uuids"""
137
138
        self.log("Connect ", args, lvl=verbose)
139
140
        try:
141
            sock = args[0]
142
            ip = args[1]
143
144
            if sock not in self._sockets:
145
                self.log("New client connected:", ip, lvl=debug)
146
                clientuuid = str(uuid4())
147
                self._sockets[sock] = Socket(ip, clientuuid)
148
                # Key uuid is temporary, until signin, will then be replaced
149
                #  with account uuid
150
151
                self._clients[clientuuid] = Client(
152
                    sock=sock, ip=ip, clientuuid=clientuuid
153
                )
154
155
                self.log("Client connected:", clientuuid, lvl=debug)
156
            else:
157
                self.log("Old IP reconnected!", lvl=warn)
158
                #     self.fireEvent(write(sock, "Another client is
159
                # connecting from your IP!"))
160
                #     self._sockets[sock] = (ip, uuid.uuid4())
161
        except Exception as e:
162
            self.log("Error during connect: ", e, type(e), lvl=critical)
163
164
    def send(self, event):
165
        """Sends a packet to an already known user or one of his clients by
166
        UUID"""
167
168
        try:
169
            jsonpacket = json.dumps(event.packet, cls=ComplexEncoder)
170
            if event.sendtype == "user":
171
                # TODO: I think, caching a user name <-> uuid table would
172
                # make sense instead of looking this up all the time.
173
174
                if event.uuid is None:
175
                    userobject = objectmodels["user"].find_one({"name": event.username})
176
                else:
177
                    userobject = objectmodels["user"].find_one({"uuid": event.uuid})
178
179
                if userobject is None:
180
                    self.log("No user by that name known.", lvl=warn)
181
                    return
182
                else:
183
                    uuid = userobject.uuid
184
185
                self.log(
186
                    "Broadcasting to all of users clients: '%s': '%s"
187
                    % (uuid, str(event.packet)[:20]),
188
                    lvl=network,
189
                )
190
                if uuid not in self._users:
191
                    self.log("User not connected!", event, lvl=critical)
192
                    return
193
                clients = self._users[uuid].clients
194
195
                for clientuuid in clients:
196
                    sock = self._clients[clientuuid].sock
197
198
                    if not event.raw:
199
                        self.log("Sending json to client", jsonpacket[:50], lvl=network)
200
201
                        self.fireEvent(write(sock, jsonpacket), "wsserver")
202
                    else:
203
                        self.log("Sending raw data to client")
204
                        self.fireEvent(write(sock, event.packet), "wsserver")
205
            else:  # only to client
206
                self.log(
207
                    "Sending to user's client: '%s': '%s'"
208
                    % (event.uuid, jsonpacket[:50]),
209
                    lvl=network,
210
                )
211
                if event.uuid not in self._clients:
212
                    if not event.fail_quiet:
213
                        self.log("Unknown client!", event.uuid, lvl=critical)
214
                        self.log("Clients:", self._clients, lvl=debug)
215
                    return
216
217
                sock = self._clients[event.uuid].sock
218
                if not event.raw:
219
                    self.fireEvent(write(sock, jsonpacket), "wsserver")
220
                else:
221
                    self.log("Sending raw data to client", lvl=network)
222
                    self.fireEvent(write(sock, event.packet[:20]), "wsserver")
223
224
        except Exception as e:
225
            self.log(
226
                "Exception during sending: %s (%s)" % (e, type(e)),
227
                lvl=critical,
228
                exc=True,
229
            )
230
231
    def broadcast(self, event):
232
        """Broadcasts an event either to all users or clients or a given group,
233
        depending on event flag"""
234
        try:
235
            if event.broadcasttype == "users":
236
                if len(self._users) > 0:
237
                    self.log("Broadcasting to all users:", event.content, lvl=network)
238
                    for useruuid in self._users.keys():
239
                        self.fireEvent(send(useruuid, event.content, sendtype="user"))
240
                        # else:
241
                        #    self.log("Not broadcasting, no users connected.",
242
                        #            lvl=debug)
243
244
            elif event.broadcasttype == "clients":
245
                if len(self._clients) > 0:
246
                    self.log(
247
                        "Broadcasting to all clients: ", event.content, lvl=network
248
                    )
249
                    for client in self._clients.values():
250
                        self.fireEvent(write(client.sock, event.content), "wsserver")
251
                        # else:
252
                        #    self.log("Not broadcasting, no clients
253
                        # connected.",
254
                        #            lvl=debug)
255
            elif event.broadcasttype in ("usergroup", "clientgroup"):
256
                if len(event.group) > 0:
257
                    self.log(
258
                        "Broadcasting to group: ", event.content, event.group,
259
                        lvl=network
260
                    )
261
                    for participant in set(event.group):
262
                        if event.broadcasttype == 'usergroup':
263
                            broadcast_type = "user"
264
                        else:
265
                            broadcast_type = "client"
266
267
                        broadcast = send(participant, event.content,
268
                                         sendtype=broadcast_type)
269
                        self.fireEvent(broadcast)
270
            elif event.broadcasttype == "socks":
271
                if len(self._sockets) > 0:
272
                    self.log("Emergency?! Broadcasting to all sockets: ", event.content)
273
                    for sock in self._sockets:
274
                        self.fireEvent(write(sock, event.content), "wsserver")
275
                        # else:
276
                        #    self.log("Not broadcasting, no sockets
277
                        # connected.",
278
                        #            lvl=debug)
279
280
        except Exception as e:
281
            self.log("Error during broadcast: ", e, type(e), lvl=critical)
282
283
    @handler("read", channel="wsserver")
284
    def read(self, *args):
285
        """Handles raw client requests and distributes them to the
286
        appropriate components"""
287
288
        self.log("Beginning new transaction: ", args, lvl=network)
289
290
        sock = msg = user = password = client = client_uuid = \
291
            user_uuid = request_data = request_action = None
292
293
        try:
294
            sock = args[0]  # type: socket
295
            msg = args[1]
296
297
            # self.log("", msg)
298
299
            # TODO: Harmonize this with flood protection and other still to do
300
            #  protections and optimize administrative
301
            ip = sock.getpeername()[0]
302
            if ip in self._bans:
303
                return
304
305
            if self._erroneous_clients.get(ip, 0) > 5:
306
                self.log('Ignoring erroneous client that sent too much garbage before', lvl=warn)
307
                self._bans[ip] = time()
308
                return
309
310
            client_uuid = self._sockets[sock].clientuuid
311
        except Exception as e:
312
            self.log("Receiving error: ", e, type(e), lvl=error, exc=True)
313
            return
314
315
        if sock is None or msg is None:
316
            self.log("Socket or message are invalid!", lvl=error)
317
            return
318
319
        if client_uuid in self._flooding:
320
            return
321
322
        try:
323
            msg = json.loads(msg)
324
            self.log("Message from client received: ", msg, lvl=network)
325
        except Exception as e:
326
            self.log("JSON Decoding failed! %s (%s of %s)" % (msg, e, type(e)))
327
            ip = sock.getpeername()[0]
328
            if ip in self._erroneous_clients:
329
                self._erroneous_clients[ip] += 1
330
            else:
331
                self._erroneous_clients[ip] = 1
332
333
            return
334
335
        try:
336
            request_component = msg["component"]
337
            request_action = msg["action"]
338
        except (KeyError, AttributeError) as e:
339
            self.log("Unpacking error: ", msg, e, type(e), lvl=error)
340
            return
341
342
        if self._check_flood_protection(request_component, request_action, client_uuid):
343
            self.log("Flood protection triggered")
344
            self._flooding[client_uuid] = time()
345
346
        try:
347
            # TODO: Do not unpickle or decode anything from unsafe events
348
            request_data = msg["data"]
349
            if isinstance(request_data, (dict, list)) and "raw" in request_data:
350
                # self.log(request_data['raw'], lvl=critical)
351
                request_data["raw"] = b64decode(request_data["raw"])
352
                # self.log(request_data['raw'])
353
        except (KeyError, AttributeError) as e:
354
            self.log("No payload.", lvl=network)
355
            request_data = None
356
357
        if request_component == "auth":
358
            self._handle_authentication_events(
359
                request_data, request_action, client_uuid, sock
360
            )
361
            return
362
        else:
363
            self._forward_event(
364
                client_uuid, request_component, request_action, request_data
365
            )
366
367
    def _forward_event(
368
        self, client_uuid, request_component, request_action, request_data
369
    ):
370
        """Determine what exactly to do with the event and forward it to its
371
        destination"""
372
        try:
373
            client = self._clients[client_uuid]
374
        except KeyError as e:
375
            self.log("Could not get client for request!", e, type(e), lvl=warn)
376
            return
377
378
        if (
379
            request_component in self.anonymous_events
380
            and request_action in self.anonymous_events[request_component]
381
        ):
382
            self.log("Executing anonymous event:", request_component, request_action)
383
            try:
384
                self._handle_anonymous_events(
385
                    request_component, request_action, request_data, client
386
                )
387
            except Exception as e:
388
                self.log("Anonymous request failed:", e, type(e), lvl=warn, exc=True)
389
            return
390
391
        elif request_component in self.authorized_events:
392
            try:
393
                user_uuid = client.useruuid
394
                self.log(
395
                    "Authenticated operation requested by ",
396
                    user_uuid,
397
                    client.config,
398
                    lvl=network,
399
                )
400
            except Exception as e:
401
                self.log("No user_uuid!", e, type(e), lvl=critical)
402
                return
403
404
            self.log("Checking if user is logged in", lvl=verbose)
405
406
            try:
407
                user = self._users[user_uuid]
408
            except KeyError:
409
                if not (
410
                    request_action == "ping"
411
                    and request_component == "isomer.ui.clientmanager.latency"
412
                ):
413
                    self.log("User not logged in.", lvl=warn)
414
415
                return
416
417
            self.log("Handling event:", request_component, request_action, lvl=verbose)
418
            try:
419
                self._handle_authorized_events(
420
                    request_component, request_action, request_data, user, client
421
                )
422
            except Exception as e:
423
                self.log("User request failed: ", e, type(e), lvl=warn, exc=True)
424
        else:
425
            self.log(
426
                "Invalid event received:", request_component, request_action, lvl=warn
427
            )
428