Completed
Push — master ( a1acfe...a11662 )
by Nicolas
01:53 queued 19s
created

GlancesXMLRPCServer.serve_forever()   A

Complexity

Conditions 2

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
dl 0
loc 5
rs 9.4285
c 1
b 0
f 0
1
# -*- coding: utf-8 -*-
2
#
3
# This file is part of Glances.
4
#
5
# Copyright (C) 2017 Nicolargo <[email protected]>
6
#
7
# Glances is free software; you can redistribute it and/or modify
8
# it under the terms of the GNU Lesser General Public License as published by
9
# the Free Software Foundation, either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Glances is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU Lesser General Public License for more details.
16
#
17
# You should have received a copy of the GNU Lesser General Public License
18
# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20
"""Manage the Glances server."""
21
22
import json
23
import socket
24
import sys
25
from base64 import b64decode
26
27
from glances import __version__
28
from glances.compat import SimpleXMLRPCRequestHandler, SimpleXMLRPCServer, Server
29
from glances.autodiscover import GlancesAutoDiscoverClient
30
from glances.logger import logger
31
from glances.stats_server import GlancesStatsServer
32
from glances.timer import Timer
33
34
35
class GlancesXMLRPCHandler(SimpleXMLRPCRequestHandler, object):
36
37
    """Main XML-RPC handler."""
38
39
    rpc_paths = ('/RPC2', )
40
41
    def end_headers(self):
42
        # Hack to add a specific header
43
        # Thk to: https://gist.github.com/rca/4063325
44
        self.send_my_headers()
45
        super(GlancesXMLRPCHandler, self).end_headers()
46
47
    def send_my_headers(self):
48
        # Specific header is here (solved the issue #227)
49
        self.send_header("Access-Control-Allow-Origin", "*")
50
51
    def authenticate(self, headers):
52
        # auth = headers.get('Authorization')
53
        try:
54
            (basic, _, encoded) = headers.get('Authorization').partition(' ')
55
        except Exception:
56
            # Client did not ask for authentidaction
57
            # If server need it then exit
58
            return not self.server.isAuth
59
        else:
60
            # Client authentication
61
            (basic, _, encoded) = headers.get('Authorization').partition(' ')
62
            assert basic == 'Basic', 'Only basic authentication supported'
63
            # Encoded portion of the header is a string
64
            # Need to convert to bytestring
65
            encoded_byte_string = encoded.encode()
66
            # Decode base64 byte string to a decoded byte string
67
            decoded_bytes = b64decode(encoded_byte_string)
68
            # Convert from byte string to a regular string
69
            decoded_string = decoded_bytes.decode()
70
            # Get the username and password from the string
71
            (username, _, password) = decoded_string.partition(':')
72
            # Check that username and password match internal global dictionary
73
            return self.check_user(username, password)
74
75
    def check_user(self, username, password):
76
        # Check username and password in the dictionary
77
        if username in self.server.user_dict:
78
            from glances.password import GlancesPassword
79
            pwd = GlancesPassword()
80
            return pwd.check_password(self.server.user_dict[username], password)
81
        else:
82
            return False
83
84
    def parse_request(self):
85
        if SimpleXMLRPCRequestHandler.parse_request(self):
86
            # Next we authenticate
87
            if self.authenticate(self.headers):
88
                return True
89
            else:
90
                # if authentication fails, tell the client
91
                self.send_error(401, 'Authentication failed')
92
        return False
93
94
    def log_message(self, log_format, *args):
95
        # No message displayed on the server side
96
        pass
97
98
99
class GlancesXMLRPCServer(SimpleXMLRPCServer, object):
100
101
    """Init a SimpleXMLRPCServer instance (IPv6-ready)."""
102
103
    finished = False
104
105
    def __init__(self, bind_address, bind_port=61209,
106
                 requestHandler=GlancesXMLRPCHandler):
107
108
        self.bind_address = bind_address
109
        self.bind_port = bind_port
110
        try:
111
            self.address_family = socket.getaddrinfo(bind_address, bind_port)[0][0]
112
        except socket.error as e:
113
            logger.error("Couldn't open socket: {}".format(e))
114
            sys.exit(1)
115
116
        super(GlancesXMLRPCServer, self).__init__((bind_address, bind_port), requestHandler)
117
118
    def end(self):
119
        """Stop the server"""
120
        self.server_close()
121
        self.finished = True
122
123
    def serve_forever(self):
124
        """Main loop"""
125
        while not self.finished:
126
            self.handle_request()
127
            logger.info(self.finished)
128
129
130
class GlancesInstance(object):
131
132
    """All the methods of this class are published as XML-RPC methods."""
133
134
    def __init__(self,
135
                 config=None,
136
                 args=None):
137
        # Init stats
138
        self.stats = GlancesStatsServer(config=config, args=args)
139
140
        # Initial update
141
        self.stats.update()
142
143
        # cached_time is the minimum time interval between stats updates
144
        # i.e. XML/RPC calls will not retrieve updated info until the time
145
        # since last update is passed (will retrieve old cached info instead)
146
        self.timer = Timer(0)
147
        self.cached_time = args.cached_time
148
149
    def __update__(self):
150
        # Never update more than 1 time per cached_time
151
        if self.timer.finished():
152
            self.stats.update()
153
            self.timer = Timer(self.cached_time)
154
155
    def init(self):
156
        # Return the Glances version
157
        return __version__
158
159
    def getAll(self):
160
        # Update and return all the stats
161
        self.__update__()
162
        return json.dumps(self.stats.getAll())
163
164
    def getAllPlugins(self):
165
        # Return the plugins list
166
        return json.dumps(self.stats.getAllPlugins())
167
168
    def getAllLimits(self):
169
        # Return all the plugins limits
170
        return json.dumps(self.stats.getAllLimitsAsDict())
171
172
    def getAllViews(self):
173
        # Return all the plugins views
174
        return json.dumps(self.stats.getAllViewsAsDict())
175
176
    def __getattr__(self, item):
177
        """Overwrite the getattr method in case of attribute is not found.
178
179
        The goal is to dynamically generate the API get'Stats'() methods.
180
        """
181
        header = 'get'
182
        # Check if the attribute starts with 'get'
183
        if item.startswith(header):
184
            try:
185
                # Update the stat
186
                self.__update__()
187
                # Return the attribute
188
                return getattr(self.stats, item)
189
            except Exception:
190
                # The method is not found for the plugin
191
                raise AttributeError(item)
192
        else:
193
            # Default behavior
194
            raise AttributeError(item)
195
196
197
class GlancesServer(object):
198
199
    """This class creates and manages the TCP server."""
200
201
    def __init__(self,
202
                 requestHandler=GlancesXMLRPCHandler,
203
                 config=None,
204
                 args=None):
205
        # Args
206
        self.args = args
207
208
        # Init the XML RPC server
209
        try:
210
            self.server = GlancesXMLRPCServer(args.bind_address, args.port, requestHandler)
211
        except Exception as e:
212
            logger.critical("Cannot start Glances server: {}".format(e))
213
            sys.exit(2)
214
        else:
215
            print('Glances XML-RPC server is running on {}:{}'.format(args.bind_address, args.port))
216
217
        # The users dict
218
        # username / password couple
219
        # By default, no auth is needed
220
        self.server.user_dict = {}
221
        self.server.isAuth = False
222
223
        # Register functions
224
        self.server.register_introspection_functions()
225
        self.server.register_instance(GlancesInstance(config, args))
226
227
        if not self.args.disable_autodiscover:
228
            # Note: The Zeroconf service name will be based on the hostname
229
            # Correct issue: Zeroconf problem with zeroconf service name #889
230
            self.autodiscover_client = GlancesAutoDiscoverClient(socket.gethostname().split('.', 1)[0], args)
231
        else:
232
            logger.info("Glances autodiscover announce is disabled")
233
234
    def add_user(self, username, password):
235
        """Add an user to the dictionary."""
236
        self.server.user_dict[username] = password
237
        self.server.isAuth = True
238
239
    def serve_forever(self):
240
        """Call the main loop."""
241
        # Set the server login/password (if -P/--password tag)
242
        if self.args.password != "":
243
            self.add_user(self.args.username, self.args.password)
244
        # Serve forever
245
        self.server.serve_forever()
246
247
    def end(self):
248
        """End of the Glances server session."""
249
        if not self.args.disable_autodiscover:
250
            self.autodiscover_client.close()
251
        self.server.end()
252