Passed
Pull Request — master (#119)
by
unknown
02:34
created

gmp.connections.GmpConnection.read()   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nop 1
dl 0
loc 4
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 gvmd daemon.
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 gmp.errors import GmpError
31
32
logger = logging.getLogger(__name__)
0 ignored issues
show
Coding Style Naming introduced by
The name logger does not conform to the constant naming conventions ((([A-Z_][A-Z0-9_]*)|(__.*__))$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
33
34
BUF_SIZE = 1024
35
DEFAULT_READ_TIMEOUT = 60 # in seconds
36
DEFAULT_TIMEOUT = 60 # in seconds
37
DEFAULT_GVM_PORT = 9390
38
DEFAULT_UNIX_SOCKET_PATH = '/usr/local/var/run/gvmd.sock'
39
MAX_SSH_DATA_LENGTH = 4095
40
41
class XmlReader:
0 ignored issues
show
Unused Code introduced by
The variable __class__ seems to be unused.
Loading history...
42
    """
43
    Read a XML command until its closing element
44
    """
45
46
    def _start_xml(self):
47
        self._first_element = None
0 ignored issues
show
Coding Style introduced by
The attribute _first_element was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
48
        self._parser = etree.XMLPullParser(('start', 'end'))
0 ignored issues
show
Bug introduced by
The Module lxml.etree does not seem to have a member named XMLPullParser.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
Coding Style introduced by
The attribute _parser was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
49
50
    def _is_end_xml(self):
51
        for action, obj in self._parser.read_events():
52
            if not self._first_element and action in 'start':
53
                self._first_element = obj.tag
0 ignored issues
show
Coding Style introduced by
The attribute _first_element was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
54
55
            if self._first_element and action in 'end' and \
56
                    str(self._first_element) == str(obj.tag):
57
                return True
58
        return False
59
60
    def _feed_xml(self, data):
61
        try:
62
            self._parser.feed(data)
63
        except etree.ParseError as e:
0 ignored issues
show
Bug introduced by
The Module lxml.etree does not seem to have a member named ParseError.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
Coding Style Naming introduced by
The name e does not conform to the variable naming conventions ((([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
64
            raise GmpError("Can't parse xml response. Response data "
65
                           "read {0}".format(data), e)
66
67
68
class GmpConnection:
0 ignored issues
show
Unused Code introduced by
The variable __class__ seems to be unused.
Loading history...
69
    """
70
    Base class for establishing a connection to gvmd.
71
    """
72
73
    def __init__(self, socket, timeout=DEFAULT_TIMEOUT):
74
        """
75
          Arguments:
76
            socket -- A socket
77
        """
78
        self._socket = socket
79
        self._timeout = timeout
80
81
    def connect(self):
82
        """Establish a connection to gvmd
83
        """
84
        raise NotImplementedError
85
86
    def send(self, data):
87
        """Send data to gvmd
88
        """
89
        if isinstance(data, str):
90
            self._socket.send(data.encode())
91
        else:
92
            self._socket.send(data)
93
94
    def read(self):
95
        """Read data from gvmd
96
        """
97
        raise NotImplementedError
98
99
    def disconnect(self):
100
        """Close the connection to gvmd
101
        """
102
        try:
103
            if self._socket is not None:
104
                self._socket.close()
105
        except OSError as e:
0 ignored issues
show
Coding Style Naming introduced by
The name e does not conform to the variable naming conventions ((([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
106
            logger.debug('Connection closing error: %s', e)
107
108
109
class SSHConnection(GmpConnection, XmlReader):
0 ignored issues
show
Unused Code introduced by
The variable __class__ seems to be unused.
Loading history...
110
    """
111
    SSH Class to connect, read and write from GVM via SSH
112
    """
113
114
    def __init__(self, timeout=DEFAULT_TIMEOUT, hostname='127.0.0.1', port=22,
0 ignored issues
show
best-practice introduced by
Too many arguments (6/5)
Loading history...
115
                 username='gmp', password=''):
116
        socket = paramiko.SSHClient()
117
        socket.set_missing_host_key_policy(paramiko.AutoAddPolicy())
118
119
        super().__init__(socket, timeout=timeout)
120
121
        self.hostname = hostname
122
        self.port = int(port)
123
        self.username = username
124
        self.password = password
125
126
    def _send_in_chunks(self, data, chunk_size):
127
        i_start = 0
128
        i_end = chunk_size
129
        sent_bytes = 0
130
        length = len(data)
131
132
        while sent_bytes < length:
133
            time.sleep(0.01)
134
135
            self._stdin.channel.send(data[i_start:i_end])
136
137
            i_start = i_end
138
            if i_end > length:
139
                i_end = length
140
            else:
141
                i_end = i_end + chunk_size
142
143
            sent_bytes += (i_end - i_start)
144
145
        return sent_bytes
146
147
    def connect(self):
148
        try:
149
            self._socket.connect(
150
                hostname=self.hostname,
151
                username=self.username,
152
                password=self.password,
153
                timeout=self._timeout,
154
                port=int(self.port),
155
                allow_agent=False,
156
                look_for_keys=False)
157
            self._stdin, self._stdout, self._stderr = self._socket.exec_command(
0 ignored issues
show
Coding Style introduced by
The attribute _stderr was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
Coding Style introduced by
The attribute _stdin was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
Coding Style introduced by
The attribute _stdout was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
158
                "", get_pty=False)
159
160
        except (paramiko.BadHostKeyException,
0 ignored issues
show
Coding Style Naming introduced by
The name e does not conform to the variable naming conventions ((([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
161
                paramiko.AuthenticationException,
162
                paramiko.SSHException, OSError) as e:
163
            logger.debug('SSH Connection failed: %s', e)
164
            raise
165
166
    def read(self):
167
        response = ''
168
169
        self._start_xml()
170
171
        while True:
172
            data = self._stdout.channel.recv(BUF_SIZE)
173
            # Connection was closed by server
174
            if not data:
175
                break
176
177
            self._feed_xml(data)
178
179
            response += data.decode('utf-8')
180
181
            if self._is_end_xml():
182
                break
183
184
        return response
185
186
    def send(self, data):
187
        logger.debug('SSH:send(): %s', data)
188
        if len(data) > MAX_SSH_DATA_LENGTH:
189
            sent_bytes = self._send_in_chunks(data, MAX_SSH_DATA_LENGTH)
190
            logger.debug("SSH: %s bytes sent.", sent_bytes)
191
        else:
192
            self._stdin.channel.send(data)
193
194
195
class TLSConnection(GmpConnection):
0 ignored issues
show
Unused Code introduced by
The variable __class__ seems to be unused.
Loading history...
196
    """
197
    TLS class to connect, read and write from gvmd via tls secured socket
198
    """
199
200
    def __init__(self, certfile=None, cafile=None, keyfile=None,
0 ignored issues
show
best-practice introduced by
Too many arguments (7/5)
Loading history...
201
                 hostname='127.0.0.1', port=DEFAULT_GVM_PORT,
202
                 timeout=DEFAULT_TIMEOUT):
203
        if certfile and cafile and keyfile:
204
            context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH,
205
                                                 cafile=cafile)
206
            context.check_hostname = False
207
            context.load_cert_chain(certfile=certfile, keyfile=keyfile)
208
            new_socket = socketlib.socket(socketlib.AF_INET,
209
                                          socketlib.SOCK_STREAM)
210
            sock = context.wrap_socket(new_socket, server_side=False)
211
        else:
212
            context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
213
            sock = context.wrap_socket(socketlib.socket(socketlib.AF_INET))
214
215
        super().__init__(sock, timeout=timeout)
216
217
        self.hostname = hostname
218
        self.port = port
219
220
    def connect(self):
221
        self._socket.settimeout(self._timeout)
222
        self._socket.connect((self.hostname, int(self.port)))
223
224
    def read(self):
225
        response = ''
226
227
        while True:
228
            data = self._socket.read(BUF_SIZE)
229
230
            response += data.decode(errors='ignore')
231
            if len(data) < BUF_SIZE:
232
                break
233
234
        return response
235
236
237
class UnixSocketConnection(GmpConnection, XmlReader):
0 ignored issues
show
Unused Code introduced by
The variable __class__ seems to be unused.
Loading history...
238
    """
239
    UNIX-Socket class to connect, read, write from gsad via direct
240
    communicating UNIX-Socket
241
    """
242
243
    def __init__(self, path=DEFAULT_UNIX_SOCKET_PATH, timeout=DEFAULT_TIMEOUT,
244
                 read_timeout=DEFAULT_READ_TIMEOUT):
245
        socket = socketlib.socket(socketlib.AF_UNIX, socketlib.SOCK_STREAM)
246
247
        super().__init__(socket, timeout=timeout)
248
249
        self.read_timeout = read_timeout
250
        self.path = path
251
252
    def connect(self):
253
        """Connect to the UNIX socket
254
        """
255
        self._socket.settimeout(self._timeout)
256
        self._socket.connect(self.path)
257
258
    def read(self):
259
        """Read from the UNIX socket
260
        """
261
        response = ''
262
263
        break_timeout = time.time() + self.read_timeout
264
        old_timeout = self._socket.gettimeout()
265
        self._socket.settimeout(5)  # in seconds
266
267
        self._start_xml()
268
269
        while time.time() < break_timeout:
270
            data = b''
271
272
            try:
273
                data = self._socket.recv(BUF_SIZE)
274
            except (socketlib.timeout) as exception:
275
                logger.debug('Warning: No data recieved '
276
                             'from server: %s', exception)
277
                continue
278
279
            self._feed_xml(data)
280
281
            response += data.decode('utf-8')
282
283
            if len(data) < BUF_SIZE:
284
                if self._is_end_xml():
285
                    break
286
287
        self._socket.settimeout(old_timeout)
288
        return response
289