Passed
Pull Request — master (#63)
by Juan José
01:52
created

gvm.connections.GvmConnection._read()   A

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nop 1
dl 0
loc 2
rs 10
c 0
b 0
f 0
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. None to
75
            wait indefinitely
76
    """
77
78
    def __init__(self, timeout=DEFAULT_TIMEOUT):
79
        self._socket = None
80
        self._timeout = timeout
81
82
    def _read(self):
83
        return self._socket.recv(BUF_SIZE)
84
85
    def connect(self):
86
        """Establish a connection to a remote server
87
        """
88
        raise NotImplementedError
89
90
    def send(self, data):
91
        """Send data to the connected remote server
92
93
        Arguments:
94
            data (str or bytes): Data to be send to the server. Either utf-8
95
                encoded string or bytes.
96
        """
97
        if isinstance(data, str):
98
            self._socket.sendall(data.encode())
99
        else:
100
            self._socket.sendall(data)
101
102
    def read(self):
103
        """Read data from the remote server
104
105
        Returns:
106
            str: data as utf-8 encoded string
107
        """
108
        response = ''
109
110
        self._start_xml()
111
112
113
        if self._timeout is not None:
114
            now = time.time()
115
116
            break_timeout = now + self._timeout
117
118
        while True:
119
            data = self._read()
120
121
            if not data:
122
                # Connection was closed by server
123
                raise GvmError('Remote closed the connection')
124
125
            self._feed_xml(data)
126
127
            response += data.decode('utf-8', errors='ignore')
128
129
            if self._is_end_xml():
130
                break
131
132
            if self._timeout is not None:
133
                now = time.time()
134
135
                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 113 is False. Are you sure this can never be the case?
Loading history...
136
                    raise GvmError('Timeout while reading the response')
137
138
        return response
139
140
    def disconnect(self):
141
        """Disconnect and close the connection to the remote server
142
        """
143
        try:
144
            if self._socket is not None:
145
                self._socket.close()
146
        except OSError as e:
147
            logger.debug('Connection closing error: %s', e)
148
149
    def finish_send(self):
150
        """Indicate to the remote server you are done with sending data
151
        """
152
        # shutdown socket for sending. only allow reading data afterwards
153
        self._socket.shutdown(socketlib.SHUT_WR)
154
155
156
class SSHConnection(GvmConnection):
157
    """
158
    SSH Class to connect, read and write from GVM via SSH
159
160
    Arguments:
161
        timeout (int, optional): Timeout in seconds for the connection.
162
        hostname (str, optional): DNS name or IP address of the remote server.
163
            Default is 127.0.0.1.
164
        port (int, optional): Port of the remote SSH server.
165
        username (str, optional): Username to use for SSH login.
166
        password (str, optional): Passwort to use for SSH login.
167
    """
168
169
    def __init__(self, *, timeout=DEFAULT_TIMEOUT, hostname='127.0.0.1',
170
                 port=22, username='gmp', password=''):
171
        super().__init__(timeout=timeout)
172
173
        self.hostname = hostname
174
        self.port = int(port)
175
        self.username = username
176
        self.password = password
177
178
    def _send_all(self, data):
179
        while data:
180
            sent = self._stdin.channel.send(data)
181
182
            if not sent:
183
                # Connection was closed by server
184
                raise GvmError('Remote closed the connection')
185
186
            data = data[sent:]
187
188
    def connect(self):
189
        """
190
        Connect to the SSH server and authenticate to it
191
        """
192
        self._socket = paramiko.SSHClient()
193
        self._socket.set_missing_host_key_policy(paramiko.AutoAddPolicy())
194
195
        try:
196
            self._socket.connect(
197
                hostname=self.hostname,
198
                username=self.username,
199
                password=self.password,
200
                timeout=self._timeout,
201
                port=int(self.port),
202
                allow_agent=False,
203
                look_for_keys=False)
204
            self._stdin, self._stdout, self._stderr = self._socket.exec_command(
205
                "", get_pty=False)
206
207
        except (paramiko.BadHostKeyException,
208
                paramiko.AuthenticationException,
209
                paramiko.SSHException,
210
                ) as e:
211
            raise GvmError('SSH Connection failed', e)
212
213
    def _read(self):
214
        return self._stdout.channel.recv(BUF_SIZE)
215
216
    def send(self, data):
217
        self._send_all(data)
218
219
    def finish_send(self):
220
        # shutdown socket for sending. only allow reading data afterwards
221
        self._stdout.channel.shutdown(socketlib.SHUT_WR)
222
223
224
class TLSConnection(GvmConnection):
225
    """
226
    TLS class to connect, read and write from a remote GVM daemon via TLS
227
    secured socket.
228
229
    Arguments:
230
        timeout (int, optional): Timeout in seconds for the connection.
231
        hostname (str, optional): DNS name or IP address of the remote TLS
232
            server.
233
        port (str, optional): Port for the TLS connection. Default is 9390.
234
        certfile (str, optional): Path to PEM encoded certificate file. See
235
            `python certificates`_ for details.
236
        cafile (str, optional): Path to PEM encoded CA file. See
237
            `python certificates`_ for details.
238
        keyfile (str, optional): Path to PEM encoded private key. See
239
            `python certificates`_ for details.
240
        password (str, optional): Password for the private key. If the password
241
            argument is not specified and a password is required it will be
242
            interactively prompt the user for a password.
243
244
    .. _python certificates:
245
        https://docs.python.org/3/library/ssl.html#certificates
246
    """
247
248
    def __init__(self, *, certfile=None, cafile=None, keyfile=None,
249
                 hostname='127.0.0.1', port=DEFAULT_GVM_PORT, password=None,
250
                 timeout=DEFAULT_TIMEOUT):
251
        super().__init__(timeout=timeout)
252
253
        self.hostname = hostname
254
        self.port = port
255
        self.certfile = certfile
256
        self.cafile = cafile
257
        self.keyfile = keyfile
258
        self.password = password
259
260
    def _new_socket(self):
261
        transport_socket = socketlib.socket(socketlib.AF_INET,
262
                                            socketlib.SOCK_STREAM)
263
264
        if self.certfile and self.cafile and self.keyfile:
265
            context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH,
266
                                                 cafile=self.cafile)
267
            context.check_hostname = False
268
            context.load_cert_chain(
269
                certfile=self.certfile, keyfile=self.keyfile,
270
                password=self.password)
271
            sock = context.wrap_socket(transport_socket, server_side=False)
272
        else:
273
            context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
274
            sock = context.wrap_socket(transport_socket)
275
276
        sock.settimeout(self._timeout)
277
278
        return sock
279
280
    def connect(self):
281
        self._socket = self._new_socket()
282
        self._socket.connect((self.hostname, int(self.port)))
283
284
285
class UnixSocketConnection(GvmConnection):
286
    """
287
    UNIX-Socket class to connect, read, write from a GVM server daemon via
288
    direct communicating UNIX-Socket
289
290
    Arguments:
291
        path (str, optional): Path to the socket.
292
        timeout (int, optional): Timeout in seconds for the connection.
293
    """
294
295
    def __init__(self, *, path=DEFAULT_UNIX_SOCKET_PATH,
296
                 timeout=DEFAULT_TIMEOUT, read_timeout=DEFAULT_READ_TIMEOUT):
297
        super().__init__(timeout=timeout)
298
299
        self.read_timeout = read_timeout
300
        self.path = path
301
302
    def connect(self):
303
        """Connect to the UNIX socket
304
        """
305
        self._socket = socketlib.socket(
306
            socketlib.AF_UNIX, socketlib.SOCK_STREAM)
307
        self._socket.settimeout(self._timeout)
308
        self._socket.connect(self.path)
309
310
311
class DebugConnection:
312
    """Wrapper around a connection for debugging purposes
313
314
    Allows to debug the connection flow including send and read data. Internally
315
    it uses the python `logging`_ framework to create debug messages. Please
316
    take a look at `the logging tutorial <https://docs.python.org/3/howto/logging.html#logging-basic-tutorial>`_
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (112/80).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
317
    for further details.
318
319
    Usage example:
320
321
    .. code-block:: python
322
323
        import logging
324
325
        logging.basicConfig(level=logging.DEBUG)
326
327
        socketconnection = UnixSocketConnection(path='/var/run/gvm.sock')
328
        connection = DebugConnection(socketconnection)
329
        gmp = Gmp(connection=connection)
330
331
    Arguments:
332
        connection (GvmConnection): GvmConnection to observe
333
334
    .. _logging:
335
        https://docs.python.org/3/library/logging.html
336
    """
337
338
    def __init__(self, connection):
339
        self._connection = connection
340
341
    def read(self):
342
        data = self._connection.read()
343
344
        logger.debug('Read %s characters. Data %s', len(data), data)
345
346
        self.last_read_data = data
347
        return data
348
349
    def send(self, data):
350
        self.last_send_data = data
351
352
        logger.debug('Sending %s characters. Data %s', len(data), data)
353
354
        return self._connection.send(data)
355
356
    def connect(self):
357
        logger.debug('Connecting')
358
359
        return self._connection.connect()
360
361
    def disconnect(self):
362
        logger.debug('Disconnecting')
363
364
        return self._connection.disconnect()
365
366
    def finish_send(self):
367
        logger.debug('Finish send')
368
369
        self._connection.finish_send()
370