Completed
Push — master ( 308939...624384 )
by Juan José
19s
created

gvm.connections.DebugConnection.send()   A

Complexity

Conditions 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nop 2
dl 0
loc 6
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.
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
146
class SSHConnection(GvmConnection):
147
    """
148
    SSH Class to connect, read and write from GVM via SSH
149
150
    Arguments:
151
        timeout (int, optional): Timeout in seconds for the connection.
152
        hostname (str, optional): DNS name or IP address of the remote server.
153
            Default is 127.0.0.1.
154
        port (int, optional): Port of the remote SSH server.
155
        username (str, optional): Username to use for SSH login.
156
        password (str, optional): Passwort to use for SSH login.
157
    """
158
159
    def __init__(self, timeout=DEFAULT_TIMEOUT, hostname='127.0.0.1', port=22,
160
                 username='gmp', password=''):
161
        super().__init__(timeout=timeout)
162
163
        self.hostname = hostname
164
        self.port = int(port)
165
        self.username = username
166
        self.password = password
167
168
    def _send_in_chunks(self, data, chunk_size):
169
        i_start = 0
170
        i_end = chunk_size
171
        sent_bytes = 0
172
        length = len(data)
173
174
        while sent_bytes < length:
175
            time.sleep(0.01)
176
177
            self._stdin.channel.send(data[i_start:i_end])
178
179
            i_start = i_end
180
            if i_end > length:
181
                i_end = length
182
            else:
183
                i_end = i_end + chunk_size
184
185
            sent_bytes += (i_end - i_start)
186
187
        return sent_bytes
188
189
    def connect(self):
190
        """
191
        Connect to the SSH server and authenticate to it
192
        """
193
        self._socket = paramiko.SSHClient()
194
        self._socket.set_missing_host_key_policy(paramiko.AutoAddPolicy())
195
196
        try:
197
            self._socket.connect(
198
                hostname=self.hostname,
199
                username=self.username,
200
                password=self.password,
201
                timeout=self._timeout,
202
                port=int(self.port),
203
                allow_agent=False,
204
                look_for_keys=False)
205
            self._stdin, self._stdout, self._stderr = self._socket.exec_command(
206
                "", get_pty=False)
207
208
        except (paramiko.BadHostKeyException,
209
                paramiko.AuthenticationException,
210
                paramiko.SSHException,
211
                ) as e:
212
            raise GvmError('SSH Connection failed', e)
213
214
    def _read(self):
215
        return self._stdout.channel.recv(BUF_SIZE)
216
217
    def send(self, data):
218
        if len(data) > MAX_SSH_DATA_LENGTH:
219
            self._send_in_chunks(data, MAX_SSH_DATA_LENGTH)
220
        else:
221
            self._stdin.channel.send(data)
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.5/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, timeout=DEFAULT_TIMEOUT,
296
                 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
313
    def __init__(self, connection):
314
        self._connection = connection
315
316
    def read(self):
317
        data = self._connection.read()
318
319
        logger.debug('Read %s characters. Data %s', len(data), data)
320
321
        self.last_read_data = data
322
        return data
323
324
    def send(self, data):
325
        self.last_send_data = data
326
327
        logger.debug('Sending %s characters. Data %s', len(data), data)
328
329
        return self._connection.send(data)
330
331
    def connect(self):
332
        logger.debug('Connecting')
333
334
        return self._connection.connect()
335
336
    def disconnect(self):
337
        logger.debug('Disconnecting')
338
339
        return self._connection.disconnect()
340