Passed
Pull Request — master (#286)
by Juan José
01:25
created

gvm.connections   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 429
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 56
eloc 218
dl 0
loc 429
rs 5.5199
c 0
b 0
f 0

28 Methods

Rating   Name   Duplication   Size   Complexity  
A GvmConnection._read() 0 2 1
B XmlReader._is_end_xml() 0 12 7
A XmlReader._start_xml() 0 6 1
A GvmConnection.__init__() 0 3 1
A XmlReader._feed_xml() 0 9 2
A GvmConnection.connect() 0 3 1
A SSHConnection.send() 0 2 1
A SSHConnection.finish_send() 0 3 1
A SSHConnection.__init__() 0 15 1
A SSHConnection.connect() 0 27 2
A GvmConnection.finish_send() 0 4 1
B GvmConnection.read() 0 36 7
A SSHConnection._read() 0 2 1
A GvmConnection.disconnect() 0 7 3
A SSHConnection._send_all() 0 9 3
A GvmConnection.send() 0 14 3
A SSHConnection.disconnect() 0 10 4
A TLSConnection._new_socket() 0 23 4
A DebugConnection.__init__() 0 2 1
A TLSConnection.connect() 0 3 1
A UnixSocketConnection.connect() 0 16 3
A DebugConnection.send() 0 6 1
A DebugConnection.connect() 0 4 1
A DebugConnection.finish_send() 0 4 1
A UnixSocketConnection.__init__() 0 9 1
A DebugConnection.read() 0 7 1
A DebugConnection.disconnect() 0 4 1
A TLSConnection.__init__() 0 19 1

How to fix   Complexity   

Complexity

Complex classes like gvm.connections 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
# -*- coding: utf-8 -*-
2
# Copyright (C) 2018 - 2019 Greenbone Networks GmbH
3
#
4
# SPDX-License-Identifier: GPL-3.0-or-later
5
#
6
# This program is free software: you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation, either version 3 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
18
"""
19
Module for connections to GVM server daemons like gvmd and ospd.
20
"""
21
import logging
22
import socket as socketlib
23
import ssl
24
import time
25
26
from typing import Optional, Union
27
28
import paramiko
29
30
from lxml import etree
31
32
from gvm.errors import GvmError
33
34
35
logger = logging.getLogger(__name__)
36
37
BUF_SIZE = 16 * 1024
38
DEFAULT_READ_TIMEOUT = 60  # in seconds
39
DEFAULT_TIMEOUT = 60  # in seconds
40
DEFAULT_GVM_PORT = 9390
41
DEFAULT_UNIX_SOCKET_PATH = "/var/run/gvmd.sock"
42
DEFAULT_SSH_PORT = 22
43
DEFAULT_HOSTNAME = '127.0.0.1'
44
MAX_SSH_DATA_LENGTH = 4095
45
46
47
class XmlReader:
48
    """
49
    Read a XML command until its closing element
50
    """
51
52
    def _start_xml(self):
53
        self._first_element = None
54
        # act on start and end element events and
55
        # allow huge text data (for report content)
56
        self._parser = etree.XMLPullParser(
57
            events=("start", "end"), huge_tree=True
58
        )
59
60
    def _is_end_xml(self):
61
        for action, obj in self._parser.read_events():
62
            if not self._first_element and action in "start":
63
                self._first_element = obj.tag
64
65
            if (
66
                self._first_element
67
                and action in "end"
68
                and str(self._first_element) == str(obj.tag)
69
            ):
70
                return True
71
        return False
72
73
    def _feed_xml(self, data):
74
        try:
75
            self._parser.feed(data)
76
        except etree.ParseError as e:
77
            raise GvmError(
78
                "Cannot parse XML response. Response data "
79
                "read {0}".format(data),
80
                e,
81
            ) from None
82
83
84
class GvmConnection(XmlReader):
85
    """
86
    Base class for establishing a connection to a remote server daemon.
87
88
    Arguments:
89
        timeout: Timeout in seconds for the connection. None to
90
            wait indefinitely
91
    """
92
93
    def __init__(self, timeout: Optional[int] = DEFAULT_TIMEOUT):
94
        self._socket = None
95
        self._timeout = timeout
96
97
    def _read(self):
98
        return self._socket.recv(BUF_SIZE)
99
100
    def connect(self):
101
        """Establish a connection to a remote server"""
102
        raise NotImplementedError
103
104
    def send(self, data: Union[bytes, str]):
105
        """Send data to the connected remote server
106
107
        Arguments:
108
            data: Data to be send to the server. Either utf-8 encoded string or
109
                bytes.
110
        """
111
        if self._socket is None:
112
            raise GvmError("Socket is not connected")
113
114
        if isinstance(data, str):
115
            self._socket.sendall(data.encode())
116
        else:
117
            self._socket.sendall(data)
118
119
    def read(self) -> str:
120
        """Read data from the remote server
121
122
        Returns:
123
            str: data as utf-8 encoded string
124
        """
125
        response = ""
126
127
        self._start_xml()
128
129
        if self._timeout is not None:
130
            now = time.time()
131
132
            break_timeout = now + self._timeout
133
134
        while True:
135
            data = self._read()
136
137
            if not data:
138
                # Connection was closed by server
139
                raise GvmError("Remote closed the connection")
140
141
            self._feed_xml(data)
142
143
            response += data.decode("utf-8", errors="ignore")
144
145
            if self._is_end_xml():
146
                break
147
148
            if self._timeout is not None:
149
                now = time.time()
150
151
                if now > break_timeout:
0 ignored issues
show
introduced by
The variable break_timeout does not seem to be defined in case self._timeout is not None on line 129 is False. Are you sure this can never be the case?
Loading history...
152
                    raise GvmError("Timeout while reading the response")
153
154
        return response
155
156
    def disconnect(self):
157
        """Disconnect and close the connection to the remote server"""
158
        try:
159
            if self._socket is not None:
160
                self._socket.close()
161
        except OSError as e:
162
            logger.debug("Connection closing error: %s", e)
163
164
    def finish_send(self):
165
        """Indicate to the remote server you are done with sending data"""
166
        # shutdown socket for sending. only allow reading data afterwards
167
        self._socket.shutdown(socketlib.SHUT_WR)
168
169
170
class SSHConnection(GvmConnection):
171
    """
172
    SSH Class to connect, read and write from GVM via SSH
173
174
    Arguments:
175
        timeout: Timeout in seconds for the connection.
176
        hostname: DNS name or IP address of the remote server. Default is
177
            127.0.0.1.
178
        port: Port of the remote SSH server. Default is port 22.
179
        username: Username to use for SSH login. Default is "gmp".
180
        password: Passwort to use for SSH login. Default is "".
181
    """
182
183
    def __init__(
184
        self,
185
        *,
186
        timeout: Optional[int] = DEFAULT_TIMEOUT,
187
        hostname: Optional[str] = DEFAULT_HOSTNAME,
188
        port: Optional[int] = DEFAULT_SSH_PORT,
189
        username: Optional[str] = "gmp",
190
        password: Optional[str] = ""
191
    ):
192
        super().__init__(timeout=timeout)
193
194
        self.hostname = hostname
195
        self.port = int(port)
196
        self.username = username
197
        self.password = password
198
199
    def _send_all(self, data):
200
        while data:
201
            sent = self._stdin.channel.send(data)
202
203
            if not sent:
204
                # Connection was closed by server
205
                raise GvmError("Remote closed the connection")
206
207
            data = data[sent:]
208
209
    def connect(self):
210
        """
211
        Connect to the SSH server and authenticate to it
212
        """
213
        self._socket = paramiko.SSHClient()
214
        self._socket.set_missing_host_key_policy(paramiko.AutoAddPolicy())
215
216
        try:
217
            self._socket.connect(
218
                hostname=self.hostname,
219
                username=self.username,
220
                password=self.password,
221
                timeout=self._timeout,
222
                port=int(self.port),
223
                allow_agent=False,
224
                look_for_keys=False,
225
            )
226
            self._stdin, self._stdout, self._stderr = self._socket.exec_command(
227
                "", get_pty=False
228
            )
229
230
        except (
231
            paramiko.BadHostKeyException,
232
            paramiko.AuthenticationException,
233
            paramiko.SSHException,
234
        ) as e:
235
            raise GvmError("SSH Connection failed", e) from None
236
237
    def _read(self):
238
        return self._stdout.channel.recv(BUF_SIZE)
239
240
    def send(self, data: Union[bytes, str]):
241
        self._send_all(data)
242
243
    def finish_send(self):
244
        # shutdown socket for sending. only allow reading data afterwards
245
        self._stdout.channel.shutdown(socketlib.SHUT_WR)
246
247
    def disconnect(self):
248
        """Disconnect and close the connection to the remote server"""
249
        try:
250
            if self._socket is not None:
251
                self._socket.close()
252
        except OSError as e:
253
            logger.debug("Connection closing error: %s", e)
254
255
        if self._socket:
256
            del self._socket, self._stdin, self._stdout, self._stderr
257
258
259
class TLSConnection(GvmConnection):
260
    """
261
    TLS class to connect, read and write from a remote GVM daemon via TLS
262
    secured socket.
263
264
    Arguments:
265
        timeout: Timeout in seconds for the connection.
266
        hostname: DNS name or IP address of the remote TLS server.
267
        port: Port for the TLS connection. Default is 9390.
268
        certfile: Path to PEM encoded certificate file. See
269
            `python certificates`_ for details.
270
        cafile: Path to PEM encoded CA file. See `python certificates`_
271
            for details.
272
        keyfile: Path to PEM encoded private key. See `python certificates`_
273
            for details.
274
        password: Password for the private key. If the password argument is not
275
            specified and a password is required it will be interactively prompt
276
            the user for a password.
277
278
    .. _python certificates:
279
        https://docs.python.org/3/library/ssl.html#certificates
280
    """
281
282
    def __init__(
283
        self,
284
        *,
285
        certfile: Optional[str] = None,
286
        cafile: Optional[str] = None,
287
        keyfile: Optional[str] = None,
288
        hostname: Optional[str] = DEFAULT_HOSTNAME,
289
        port: Optional[int] = DEFAULT_GVM_PORT,
290
        password: Optional[str] = None,
291
        timeout: Optional[int] = DEFAULT_TIMEOUT
292
    ):
293
        super().__init__(timeout=timeout)
294
295
        self.hostname = hostname
296
        self.port = port
297
        self.certfile = certfile
298
        self.cafile = cafile
299
        self.keyfile = keyfile
300
        self.password = password
301
302
    def _new_socket(self):
303
        transport_socket = socketlib.socket(
304
            socketlib.AF_INET, socketlib.SOCK_STREAM
305
        )
306
307
        if self.certfile and self.cafile and self.keyfile:
308
            context = ssl.create_default_context(
309
                ssl.Purpose.SERVER_AUTH, cafile=self.cafile
310
            )
311
            context.check_hostname = False
312
            context.load_cert_chain(
313
                certfile=self.certfile,
314
                keyfile=self.keyfile,
315
                password=self.password,
316
            )
317
            sock = context.wrap_socket(transport_socket, server_side=False)
318
        else:
319
            context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
320
            sock = context.wrap_socket(transport_socket)
321
322
        sock.settimeout(self._timeout)
323
324
        return sock
325
326
    def connect(self):
327
        self._socket = self._new_socket()
328
        self._socket.connect((self.hostname, int(self.port)))
329
330
331
class UnixSocketConnection(GvmConnection):
332
    """
333
    UNIX-Socket class to connect, read, write from a GVM server daemon via
334
    direct communicating UNIX-Socket
335
336
    Arguments:
337
        path: Path to the socket. Default is "/var/run/gvmd.sock".
338
        timeout: Timeout in seconds for the connection. Default is 60 seconds.
339
    """
340
341
    def __init__(
342
        self,
343
        *,
344
        path: Optional[str] = DEFAULT_UNIX_SOCKET_PATH,
345
        timeout: Optional[int] = DEFAULT_TIMEOUT
346
    ):
347
        super().__init__(timeout=timeout)
348
349
        self.path = path
350
351
    def connect(self):
352
        """Connect to the UNIX socket"""
353
        self._socket = socketlib.socket(
354
            socketlib.AF_UNIX, socketlib.SOCK_STREAM
355
        )
356
        self._socket.settimeout(self._timeout)
357
        try:
358
            self._socket.connect(self.path)
359
        except FileNotFoundError:
360
            raise GvmError(
361
                "Socket {path} does not exist".format(path=self.path)
362
            ) from None
363
        except ConnectionError:
364
            raise GvmError(
365
                "Could not connect to socket {}".format(self.path)
366
            ) from None
367
368
369
class DebugConnection:
370
    """Wrapper around a connection for debugging purposes
371
372
    Allows to debug the connection flow including send and read data. Internally
373
    it uses the python `logging`_ framework to create debug messages. Please
374
    take a look at `the logging tutorial
375
    <https://docs.python.org/3/howto/logging.html#logging-basic-tutorial>`_
376
    for further details.
377
378
    Usage example:
379
380
    .. code-block:: python
381
382
        import logging
383
384
        logging.basicConfig(level=logging.DEBUG)
385
386
        socketconnection = UnixSocketConnection(path='/var/run/gvm.sock')
387
        connection = DebugConnection(socketconnection)
388
        gmp = Gmp(connection=connection)
389
390
    Arguments:
391
        connection: GvmConnection to observe
392
393
    .. _logging:
394
        https://docs.python.org/3/library/logging.html
395
    """
396
397
    def __init__(self, connection: GvmConnection):
398
        self._connection = connection
399
400
    def read(self) -> str:
401
        data = self._connection.read()
402
403
        logger.debug("Read %s characters. Data %s", len(data), data)
404
405
        self.last_read_data = data
406
        return data
407
408
    def send(self, data):
409
        self.last_send_data = data
410
411
        logger.debug("Sending %s characters. Data %s", len(data), data)
412
413
        return self._connection.send(data)
414
415
    def connect(self):
416
        logger.debug("Connecting")
417
418
        return self._connection.connect()
419
420
    def disconnect(self):
421
        logger.debug("Disconnecting")
422
423
        return self._connection.disconnect()
424
425
    def finish_send(self):
426
        logger.debug("Finish send")
427
428
        self._connection.finish_send()
429