Passed
Pull Request — master (#55)
by
unknown
01:51
created

gvm.connections.SSHConnection.__init__()   A

Complexity

Conditions 1

Size

Total Lines 8
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 7
nop 7
dl 0
loc 8
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
    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_in_chunks(self, data, chunk_size):
175
        i_start = 0
176
        i_end = chunk_size
177
        sent_bytes = 0
178
        length = len(data)
179
180
        while sent_bytes < length:
181
            time.sleep(0.01)
182
183
            self._stdin.channel.send(data[i_start:i_end])
184
185
            i_start = i_end
186
            if i_end > length:
187
                i_end = length
188
            else:
189
                i_end = i_end + chunk_size
190
191
            sent_bytes += (i_end - i_start)
192
193
        return sent_bytes
194
195
    def connect(self):
196
        """
197
        Connect to the SSH server and authenticate to it
198
        """
199
        self._socket = paramiko.SSHClient()
200
        self._socket.set_missing_host_key_policy(paramiko.AutoAddPolicy())
201
202
        try:
203
            self._socket.connect(
204
                hostname=self.hostname,
205
                username=self.username,
206
                password=self.password,
207
                timeout=self._timeout,
208
                port=int(self.port),
209
                allow_agent=False,
210
                look_for_keys=False)
211
            self._stdin, self._stdout, self._stderr = self._socket.exec_command(
212
                "", get_pty=False)
213
214
        except (paramiko.BadHostKeyException,
215
                paramiko.AuthenticationException,
216
                paramiko.SSHException,
217
                ) as e:
218
            raise GvmError('SSH Connection failed', e)
219
220
    def _read(self):
221
        return self._stdout.channel.recv(BUF_SIZE)
222
223
    def send(self, data):
224
        if len(data) > MAX_SSH_DATA_LENGTH:
225
            self._send_in_chunks(data, MAX_SSH_DATA_LENGTH)
226
        else:
227
            self._stdin.channel.send(data)
228
229
    def finish_send(self):
230
        # shutdown socket for sending. only allow reading data afterwards
231
        self._stdout.channel.shutdown(socketlib.SHUT_WR)
232
233
234
class TLSConnection(GvmConnection):
235
    """
236
    TLS class to connect, read and write from a remote GVM daemon via TLS
237
    secured socket.
238
239
    Arguments:
240
        timeout (int, optional): Timeout in seconds for the connection.
241
        hostname (str, optional): DNS name or IP address of the remote TLS
242
            server.
243
        port (str, optional): Port for the TLS connection. Default is 9390.
244
        certfile (str, optional): Path to PEM encoded certificate file. See
245
            `python certificates`_ for details.
246
        cafile (str, optional): Path to PEM encoded CA file. See
247
            `python certificates`_ for details.
248
        keyfile (str, optional): Path to PEM encoded private key. See
249
            `python certificates`_ for details.
250
        password (str, optional): Password for the private key. If the password
251
            argument is not specified and a password is required it will be
252
            interactively prompt the user for a password.
253
254
    .. _python certificates:
255
        https://docs.python.org/3.5/library/ssl.html#certificates
256
    """
257
258
    def __init__(self, *, certfile=None, cafile=None, keyfile=None,
259
                 hostname='127.0.0.1', port=DEFAULT_GVM_PORT, password=None,
260
                 timeout=DEFAULT_TIMEOUT):
261
        super().__init__(timeout=timeout)
262
263
        self.hostname = hostname
264
        self.port = port
265
        self.certfile = certfile
266
        self.cafile = cafile
267
        self.keyfile = keyfile
268
        self.password = password
269
270
    def _new_socket(self):
271
        transport_socket = socketlib.socket(socketlib.AF_INET,
272
                                            socketlib.SOCK_STREAM)
273
274
        if self.certfile and self.cafile and self.keyfile:
275
            context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH,
276
                                                 cafile=self.cafile)
277
            context.check_hostname = False
278
            context.load_cert_chain(
279
                certfile=self.certfile, keyfile=self.keyfile,
280
                password=self.password)
281
            sock = context.wrap_socket(transport_socket, server_side=False)
282
        else:
283
            context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
284
            sock = context.wrap_socket(transport_socket)
285
286
        sock.settimeout(self._timeout)
287
288
        return sock
289
290
    def connect(self):
291
        self._socket = self._new_socket()
292
        self._socket.connect((self.hostname, int(self.port)))
293
294
295
class UnixSocketConnection(GvmConnection):
296
    """
297
    UNIX-Socket class to connect, read, write from a GVM server daemon via
298
    direct communicating UNIX-Socket
299
300
    Arguments:
301
        path (str, optional): Path to the socket.
302
        timeout (int, optional): Timeout in seconds for the connection.
303
    """
304
305
    def __init__(self, *, path=DEFAULT_UNIX_SOCKET_PATH,
306
                 timeout=DEFAULT_TIMEOUT, read_timeout=DEFAULT_READ_TIMEOUT):
307
        super().__init__(timeout=timeout)
308
309
        self.read_timeout = read_timeout
310
        self.path = path
311
312
    def connect(self):
313
        """Connect to the UNIX socket
314
        """
315
        self._socket = socketlib.socket(
316
            socketlib.AF_UNIX, socketlib.SOCK_STREAM)
317
        self._socket.settimeout(self._timeout)
318
        self._socket.connect(self.path)
319
320
321
class DebugConnection:
322
323
    def __init__(self, connection):
324
        self._connection = connection
325
326
    def read(self):
327
        data = self._connection.read()
328
329
        logger.debug('Read %s characters. Data %s', len(data), data)
330
331
        self.last_read_data = data
332
        return data
333
334
    def send(self, data):
335
        self.last_send_data = data
336
337
        logger.debug('Sending %s characters. Data %s', len(data), data)
338
339
        return self._connection.send(data)
340
341
    def connect(self):
342
        logger.debug('Connecting')
343
344
        return self._connection.connect()
345
346
    def disconnect(self):
347
        logger.debug('Disconnecting')
348
349
        return self._connection.disconnect()
350
351
    def finish_send(self):
352
        logger.debug('Finish send')
353
354
        self._connection.finish_send()
355