Completed
Push — master ( 691ff3...bb0f98 )
by
unknown
24s
created

gvm.connections   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 341
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 171
dl 0
loc 341
rs 8.64
c 0
b 0
f 0
wmc 47

27 Methods

Rating   Name   Duplication   Size   Complexity  
B XmlReader._is_end_xml() 0 9 7
A XmlReader._start_xml() 0 3 1
A XmlReader._feed_xml() 0 6 2
A GvmConnection.connect() 0 4 1
A GvmConnection._read() 0 2 1
A GvmConnection.read() 0 34 5
A GvmConnection.disconnect() 0 8 3
A GvmConnection.send() 0 11 2
A GvmConnection.__init__() 0 3 1
A SSHConnection.__init__() 0 8 1
A GvmConnection.finish_send() 0 5 1
A TLSConnection._new_socket() 0 19 4
A SSHConnection.send() 0 2 1
A DebugConnection.__init__() 0 2 1
A SSHConnection.finish_send() 0 3 1
A TLSConnection.connect() 0 3 1
A UnixSocketConnection.connect() 0 7 1
A DebugConnection.send() 0 6 1
A SSHConnection.connect() 0 24 2
A DebugConnection.connect() 0 4 1
A DebugConnection.finish_send() 0 4 1
A UnixSocketConnection.__init__() 0 6 1
A DebugConnection.read() 0 7 1
A DebugConnection.disconnect() 0 4 1
A SSHConnection._read() 0 2 1
A TLSConnection.__init__() 0 11 1
A SSHConnection._send_all() 0 9 3

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 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
import paramiko
27
28
from lxml import etree
29
30
from gvm.errors import GvmError
31
32
33
logger = logging.getLogger(__name__)
34
35
BUF_SIZE = 1024
36
DEFAULT_READ_TIMEOUT = 60 # in seconds
37
DEFAULT_TIMEOUT = 60 # in seconds
38
DEFAULT_GVM_PORT = 9390
39
DEFAULT_UNIX_SOCKET_PATH = '/usr/local/var/run/gvmd.sock'
40
MAX_SSH_DATA_LENGTH = 4095
41
42
class XmlReader:
43
    """
44
    Read a XML command until its closing element
45
    """
46
47
    def _start_xml(self):
48
        self._first_element = None
49
        self._parser = etree.XMLPullParser(('start', 'end'))
50
51
    def _is_end_xml(self):
52
        for action, obj in self._parser.read_events():
53
            if not self._first_element and action in 'start':
54
                self._first_element = obj.tag
55
56
            if self._first_element and action in 'end' and \
57
                    str(self._first_element) == str(obj.tag):
58
                return True
59
        return False
60
61
    def _feed_xml(self, data):
62
        try:
63
            self._parser.feed(data)
64
        except etree.ParseError as e:
65
            raise GvmError("Can't parse xml response. Response data "
66
                           "read {0}".format(data), e)
67
68
69
class GvmConnection(XmlReader):
70
    """
71
    Base class for establishing a connection to a remote server daemon.
72
73
    Arguments:
74
        timeout (int, optional): Timeout in seconds for the connection.
75
    """
76
77
    def __init__(self, timeout=DEFAULT_TIMEOUT):
78
        self._socket = None
79
        self._timeout = timeout
80
81
    def _read(self):
82
        return self._socket.recv(BUF_SIZE)
83
84
    def connect(self):
85
        """Establish a connection to a remote server
86
        """
87
        raise NotImplementedError
88
89
    def send(self, data):
90
        """Send data to the connected remote server
91
92
        Arguments:
93
            data (str or bytes): Data to be send to the server. Either utf-8
94
                encoded string or bytes.
95
        """
96
        if isinstance(data, str):
97
            self._socket.sendall(data.encode())
98
        else:
99
            self._socket.sendall(data)
100
101
    def read(self):
102
        """Read data from the remote server
103
104
        Returns:
105
            str: data as utf-8 encoded string
106
        """
107
        response = ''
108
109
        self._start_xml()
110
111
        now = time.time()
112
113
        break_timeout = now + self._timeout
114
115
        while True:
116
            data = self._read()
117
118
            if not data:
119
                # Connection was closed by server
120
                raise GvmError('Remote closed the connection')
121
122
            self._feed_xml(data)
123
124
            response += data.decode('utf-8', errors='ignore')
125
126
            if self._is_end_xml():
127
                break
128
129
            now = time.time()
130
131
            if now > break_timeout:
132
                raise GvmError('Timeout while reading the response')
133
134
        return response
135
136
    def disconnect(self):
137
        """Disconnect and close the connection to the remote server
138
        """
139
        try:
140
            if self._socket is not None:
141
                self._socket.close()
142
        except OSError as e:
143
            logger.debug('Connection closing error: %s', e)
144
145
    def finish_send(self):
146
        """Indicate to the remote server you are done with sending data
147
        """
148
        # shutdown socket for sending. only allow reading data afterwards
149
        self._socket.shutdown(socketlib.SHUT_WR)
150
151
152
class SSHConnection(GvmConnection):
153
    """
154
    SSH Class to connect, read and write from GVM via SSH
155
156
    Arguments:
157
        timeout (int, optional): Timeout in seconds for the connection.
158
        hostname (str, optional): DNS name or IP address of the remote server.
159
            Default is 127.0.0.1.
160
        port (int, optional): Port of the remote SSH server.
161
        username (str, optional): Username to use for SSH login.
162
        password (str, optional): Passwort to use for SSH login.
163
    """
164
165
    def __init__(self, *, timeout=DEFAULT_TIMEOUT, hostname='127.0.0.1',
166
                 port=22, username='gmp', password=''):
167
        super().__init__(timeout=timeout)
168
169
        self.hostname = hostname
170
        self.port = int(port)
171
        self.username = username
172
        self.password = password
173
174
    def _send_all(self, data):
175
        while data:
176
            sent = self._stdin.channel.send(data)
177
178
            if not sent:
179
                # Connection was closed by server
180
                raise GvmError('Remote closed the connection')
181
182
            data = data[sent:]
183
184
    def connect(self):
185
        """
186
        Connect to the SSH server and authenticate to it
187
        """
188
        self._socket = paramiko.SSHClient()
189
        self._socket.set_missing_host_key_policy(paramiko.AutoAddPolicy())
190
191
        try:
192
            self._socket.connect(
193
                hostname=self.hostname,
194
                username=self.username,
195
                password=self.password,
196
                timeout=self._timeout,
197
                port=int(self.port),
198
                allow_agent=False,
199
                look_for_keys=False)
200
            self._stdin, self._stdout, self._stderr = self._socket.exec_command(
201
                "", get_pty=False)
202
203
        except (paramiko.BadHostKeyException,
204
                paramiko.AuthenticationException,
205
                paramiko.SSHException,
206
                ) as e:
207
            raise GvmError('SSH Connection failed', e)
208
209
    def _read(self):
210
        return self._stdout.channel.recv(BUF_SIZE)
211
212
    def send(self, data):
213
        self._send_all(data)
214
215
    def finish_send(self):
216
        # shutdown socket for sending. only allow reading data afterwards
217
        self._stdout.channel.shutdown(socketlib.SHUT_WR)
218
219
220
class TLSConnection(GvmConnection):
221
    """
222
    TLS class to connect, read and write from a remote GVM daemon via TLS
223
    secured socket.
224
225
    Arguments:
226
        timeout (int, optional): Timeout in seconds for the connection.
227
        hostname (str, optional): DNS name or IP address of the remote TLS
228
            server.
229
        port (str, optional): Port for the TLS connection. Default is 9390.
230
        certfile (str, optional): Path to PEM encoded certificate file. See
231
            `python certificates`_ for details.
232
        cafile (str, optional): Path to PEM encoded CA file. See
233
            `python certificates`_ for details.
234
        keyfile (str, optional): Path to PEM encoded private key. See
235
            `python certificates`_ for details.
236
        password (str, optional): Password for the private key. If the password
237
            argument is not specified and a password is required it will be
238
            interactively prompt the user for a password.
239
240
    .. _python certificates:
241
        https://docs.python.org/3.5/library/ssl.html#certificates
242
    """
243
244
    def __init__(self, *, certfile=None, cafile=None, keyfile=None,
245
                 hostname='127.0.0.1', port=DEFAULT_GVM_PORT, password=None,
246
                 timeout=DEFAULT_TIMEOUT):
247
        super().__init__(timeout=timeout)
248
249
        self.hostname = hostname
250
        self.port = port
251
        self.certfile = certfile
252
        self.cafile = cafile
253
        self.keyfile = keyfile
254
        self.password = password
255
256
    def _new_socket(self):
257
        transport_socket = socketlib.socket(socketlib.AF_INET,
258
                                            socketlib.SOCK_STREAM)
259
260
        if self.certfile and self.cafile and self.keyfile:
261
            context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH,
262
                                                 cafile=self.cafile)
263
            context.check_hostname = False
264
            context.load_cert_chain(
265
                certfile=self.certfile, keyfile=self.keyfile,
266
                password=self.password)
267
            sock = context.wrap_socket(transport_socket, server_side=False)
268
        else:
269
            context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
270
            sock = context.wrap_socket(transport_socket)
271
272
        sock.settimeout(self._timeout)
273
274
        return sock
275
276
    def connect(self):
277
        self._socket = self._new_socket()
278
        self._socket.connect((self.hostname, int(self.port)))
279
280
281
class UnixSocketConnection(GvmConnection):
282
    """
283
    UNIX-Socket class to connect, read, write from a GVM server daemon via
284
    direct communicating UNIX-Socket
285
286
    Arguments:
287
        path (str, optional): Path to the socket.
288
        timeout (int, optional): Timeout in seconds for the connection.
289
    """
290
291
    def __init__(self, *, path=DEFAULT_UNIX_SOCKET_PATH,
292
                 timeout=DEFAULT_TIMEOUT, read_timeout=DEFAULT_READ_TIMEOUT):
293
        super().__init__(timeout=timeout)
294
295
        self.read_timeout = read_timeout
296
        self.path = path
297
298
    def connect(self):
299
        """Connect to the UNIX socket
300
        """
301
        self._socket = socketlib.socket(
302
            socketlib.AF_UNIX, socketlib.SOCK_STREAM)
303
        self._socket.settimeout(self._timeout)
304
        self._socket.connect(self.path)
305
306
307
class DebugConnection:
308
309
    def __init__(self, connection):
310
        self._connection = connection
311
312
    def read(self):
313
        data = self._connection.read()
314
315
        logger.debug('Read %s characters. Data %s', len(data), data)
316
317
        self.last_read_data = data
318
        return data
319
320
    def send(self, data):
321
        self.last_send_data = data
322
323
        logger.debug('Sending %s characters. Data %s', len(data), data)
324
325
        return self._connection.send(data)
326
327
    def connect(self):
328
        logger.debug('Connecting')
329
330
        return self._connection.connect()
331
332
    def disconnect(self):
333
        logger.debug('Disconnecting')
334
335
        return self._connection.disconnect()
336
337
    def finish_send(self):
338
        logger.debug('Finish send')
339
340
        self._connection.finish_send()
341