Issues (46)

glances/plugins/glances_ports.py (3 issues)

1
# -*- coding: utf-8 -*-
2
#
3
# This file is part of Glances.
4
#
5
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <[email protected]>
6
#
7
# SPDX-License-Identifier: LGPL-3.0-only
8
#
9
10
"""Ports scanner plugin."""
11
12
import os
13
import subprocess
14
import threading
15
import socket
16
import time
17
import numbers
18
19
from glances.globals import WINDOWS, MACOS, BSD
20
from glances.ports_list import GlancesPortsList
21
from glances.web_list import GlancesWebList
22
from glances.timer import Counter
23
from glances.compat import bool_type
24
from glances.logger import logger
25
from glances.plugins.glances_plugin import GlancesPlugin
26
27
try:
28
    import requests
29
30
    requests_tag = True
31
except ImportError as e:
32
    requests_tag = False
33
    logger.warning("Missing Python Lib ({}), Ports plugin is limited to port scanning".format(e))
34
35
36
class Plugin(GlancesPlugin):
37
    """Glances ports scanner plugin."""
38
39
    def __init__(self, args=None, config=None):
40
        """Init the plugin."""
41
        super(Plugin, self).__init__(args=args, config=config, stats_init_value=[])
42
        self.args = args
43
        self.config = config
44
45
        # We want to display the stat in the curse interface
46
        self.display_curse = True
47
48
        # Init stats
49
        self.stats = (
50
            GlancesPortsList(config=config, args=args).get_ports_list()
51
            + GlancesWebList(config=config, args=args).get_web_list()
52
        )
53
54
        # Global Thread running all the scans
55
        self._thread = None
56
57
    def exit(self):
58
        """Overwrite the exit method to close threads."""
59
        if self._thread is not None:
60
            self._thread.stop()
61
        # Call the father class
62
        super(Plugin, self).exit()
63
64
    @GlancesPlugin._check_decorator
65
    @GlancesPlugin._log_result_decorator
66
    def update(self):
67
        """Update the ports list."""
68
        if self.input_method == 'local':
69
            # Only refresh:
70
            # * if there is not other scanning thread
71
            # * every refresh seconds (define in the configuration file)
72
            if self._thread is None:
73
                thread_is_running = False
74
            else:
75
                thread_is_running = self._thread.is_alive()
76
            if not thread_is_running:
77
                # Run ports scanner
78
                self._thread = ThreadScanner(self.stats)
79
                self._thread.start()
80
                # # Restart timer
81
                # if len(self.stats) > 0:
82
                #     self.timer_ports = Timer(self.stats[0]['refresh'])
83
                # else:
84
                #     self.timer_ports = Timer(0)
85
        else:
86
            # Not available in SNMP mode
87
            pass
88
89
        return self.stats
90
91
    def get_key(self):
92
        """Return the key of the list."""
93
        return 'indice'
94
95 View Code Duplication
    def get_ports_alert(self, port, header="", log=False):
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
96
        """Return the alert status relative to the port scan return value."""
97
        ret = 'OK'
98
        if port['status'] is None:
99
            ret = 'CAREFUL'
100
        elif port['status'] == 0:
101
            ret = 'CRITICAL'
102
        elif (
103
            isinstance(port['status'], (float, int))
104
            and port['rtt_warning'] is not None
105
            and port['status'] > port['rtt_warning']
106
        ):
107
            ret = 'WARNING'
108
109
        # Get stat name
110
        stat_name = self.get_stat_name(header=header)
111
112
        # Manage threshold
113
        self.manage_threshold(stat_name, ret)
114
115
        # Manage action
116
        self.manage_action(stat_name, ret.lower(), header, port[self.get_key()])
117
118
        return ret
119
120 View Code Duplication
    def get_web_alert(self, web, header="", log=False):
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
121
        """Return the alert status relative to the web/url scan return value."""
122
        ret = 'OK'
123
        if web['status'] is None:
124
            ret = 'CAREFUL'
125
        elif web['status'] not in [200, 301, 302]:
126
            ret = 'CRITICAL'
127
        elif web['rtt_warning'] is not None and web['elapsed'] > web['rtt_warning']:
128
            ret = 'WARNING'
129
130
        # Get stat name
131
        stat_name = self.get_stat_name(header=header)
132
133
        # Manage threshold
134
        self.manage_threshold(stat_name, ret)
135
136
        # Manage action
137
        self.manage_action(stat_name, ret.lower(), header, web[self.get_key()])
138
139
        return ret
140
141
    def msg_curse(self, args=None, max_width=None):
142
        """Return the dict to display in the curse interface."""
143
        # Init the return message
144
        # Only process if stats exist and display plugin enable...
145
        ret = []
146
147
        if not self.stats or args.disable_ports:
148
            return ret
149
150
        # Max size for the interface name
151
        name_max_width = max_width - 7
152
153
        # Build the string message
154
        for p in self.stats:
155
            if 'host' in p:
156
                if p['host'] is None:
157
                    status = 'None'
158
                elif p['status'] is None:
159
                    status = 'Scanning'
160
                elif isinstance(p['status'], bool_type) and p['status'] is True:
161
                    status = 'Open'
162
                elif p['status'] == 0:
163
                    status = 'Timeout'
164
                else:
165
                    # Convert second to ms
166
                    status = '{0:.0f}ms'.format(p['status'] * 1000.0)
167
168
                msg = '{:{width}}'.format(p['description'][0:name_max_width], width=name_max_width)
169
                ret.append(self.curse_add_line(msg))
170
                msg = '{:>9}'.format(status)
171
                ret.append(self.curse_add_line(msg, self.get_ports_alert(p, header=p['indice'] + '_rtt')))
172
                ret.append(self.curse_new_line())
173
            elif 'url' in p:
174
                msg = '{:{width}}'.format(p['description'][0:name_max_width], width=name_max_width)
175
                ret.append(self.curse_add_line(msg))
176
                if isinstance(p['status'], numbers.Number):
177
                    status = 'Code {}'.format(p['status'])
178
                elif p['status'] is None:
179
                    status = 'Scanning'
180
                else:
181
                    status = p['status']
182
                msg = '{:>9}'.format(status)
183
                ret.append(self.curse_add_line(msg, self.get_web_alert(p, header=p['indice'] + '_rtt')))
184
                ret.append(self.curse_new_line())
185
186
        # Delete the last empty line
187
        try:
188
            ret.pop()
189
        except IndexError:
190
            pass
191
192
        return ret
193
194
195
class ThreadScanner(threading.Thread):
196
    """
197
    Specific thread for the port/web scanner.
198
199
    stats is a list of dict
200
    """
201
202
    def __init__(self, stats):
203
        """Init the class."""
204
        logger.debug("ports plugin - Create thread for scan list {}".format(stats))
205
        super(ThreadScanner, self).__init__()
206
        # Event needed to stop properly the thread
207
        self._stopper = threading.Event()
208
        # The class return the stats as a list of dict
209
        self._stats = stats
210
        # Is part of Ports plugin
211
        self.plugin_name = "ports"
212
213
    def run(self):
214
        """Grab the stats.
215
216
        Infinite loop, should be stopped by calling the stop() method.
217
        """
218
        for p in self._stats:
219
            # End of the thread has been asked
220
            if self.stopped():
221
                break
222
            # Scan a port (ICMP or TCP)
223
            if 'port' in p:
224
                self._port_scan(p)
225
                # Had to wait between two scans
226
                # If not, result are not ok
227
                time.sleep(1)
228
            # Scan an URL
229
            elif 'url' in p and requests_tag:
230
                self._web_scan(p)
231
232
    @property
233
    def stats(self):
234
        """Stats getter."""
235
        return self._stats
236
237
    @stats.setter
238
    def stats(self, value):
239
        """Stats setter."""
240
        self._stats = value
241
242
    def stop(self, timeout=None):
243
        """Stop the thread."""
244
        logger.debug("ports plugin - Close thread for scan list {}".format(self._stats))
245
        self._stopper.set()
246
247
    def stopped(self):
248
        """Return True is the thread is stopped."""
249
        return self._stopper.is_set()
250
251
    def _web_scan(self, web):
252
        """Scan the  Web/URL (dict) and update the status key."""
253
        try:
254
            req = requests.head(
255
                web['url'],
256
                allow_redirects=True,
257
                verify=web['ssl_verify'],
258
                proxies=web['proxies'],
259
                timeout=web['timeout'],
260
            )
261
        except Exception as e:
262
            logger.debug(e)
263
            web['status'] = 'Error'
264
            web['elapsed'] = 0
265
        else:
266
            web['status'] = req.status_code
267
            web['elapsed'] = req.elapsed.total_seconds()
268
        return web
269
270
    def _port_scan(self, port):
271
        """Scan the port structure (dict) and update the status key."""
272
        if int(port['port']) == 0:
273
            return self._port_scan_icmp(port)
274
        else:
275
            return self._port_scan_tcp(port)
276
277
    def _resolv_name(self, hostname):
278
        """Convert hostname to IP address."""
279
        ip = hostname
280
        try:
281
            ip = socket.gethostbyname(hostname)
282
        except Exception as e:
283
            logger.debug("{}: Cannot convert {} to IP address ({})".format(self.plugin_name, hostname, e))
284
        return ip
285
286
    def _port_scan_icmp(self, port):
287
        """Scan the (ICMP) port structure (dict) and update the status key."""
288
        ret = None
289
290
        # Create the ping command
291
        # Use the system ping command because it already have the sticky bit set
292
        # Python can not create ICMP packet with non root right
293
        if WINDOWS:
294
            timeout_opt = '-w'
295
            count_opt = '-n'
296
        elif MACOS or BSD:
297
            timeout_opt = '-t'
298
            count_opt = '-c'
299
        else:
300
            # Linux and co...
301
            timeout_opt = '-W'
302
            count_opt = '-c'
303
        # Build the command line
304
        # Note: Only string are allowed
305
        cmd = [
306
            'ping',
307
            count_opt,
308
            '1',
309
            timeout_opt,
310
            str(self._resolv_name(port['timeout'])),
311
            self._resolv_name(port['host']),
312
        ]
313
        fnull = open(os.devnull, 'w')
314
315
        try:
316
            counter = Counter()
317
            ret = subprocess.check_call(cmd, stdout=fnull, stderr=fnull, close_fds=True)
318
            if ret == 0:
319
                port['status'] = counter.get()
320
            else:
321
                port['status'] = False
322
        except subprocess.CalledProcessError:
323
            # Correct issue #1084: No Offline status for timed-out ports
324
            port['status'] = False
325
        except Exception as e:
326
            logger.debug("{}: Error while pinging host {} ({})".format(self.plugin_name, port['host'], e))
327
328
        fnull.close()
329
330
        return ret
331
332
    def _port_scan_tcp(self, port):
333
        """Scan the (TCP) port structure (dict) and update the status key."""
334
        ret = None
335
336
        # Create and configure the scanning socket
337
        try:
338
            socket.setdefaulttimeout(port['timeout'])
339
            _socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
340
        except Exception as e:
341
            logger.debug("{}: Error while creating scanning socket ({})".format(self.plugin_name, e))
342
343
        # Scan port
344
        ip = self._resolv_name(port['host'])
345
        counter = Counter()
346
        try:
347
            ret = _socket.connect_ex((ip, int(port['port'])))
0 ignored issues
show
The variable _socket does not seem to be defined for all execution paths.
Loading history...
348
        except Exception as e:
349
            logger.debug("{}: Error while scanning port {} ({})".format(self.plugin_name, port, e))
350
        else:
351
            if ret == 0:
352
                port['status'] = counter.get()
353
            else:
354
                port['status'] = False
355
        finally:
356
            _socket.close()
357
358
        return ret
359