1# -*- coding: utf-8 -*-
2#
3# This file is part of Glances.
4#
5# Copyright (C) 2019 Nicolargo <nicolas@nicolargo.com>
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
22import json
23import socket
24import sys
25from base64 import b64decode
26
27from glances import __version__
28from glances.compat import SimpleXMLRPCRequestHandler, SimpleXMLRPCServer, Server
29from glances.autodiscover import GlancesAutoDiscoverClient
30from glances.logger import logger
31from glances.stats_server import GlancesStatsServer
32from glances.timer import Timer
33
34
35class 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
99class 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
128
129class GlancesInstance(object):
130
131    """All the methods of this class are published as XML-RPC methods."""
132
133    def __init__(self,
134                 config=None,
135                 args=None):
136        # Init stats
137        self.stats = GlancesStatsServer(config=config, args=args)
138
139        # Initial update
140        self.stats.update()
141
142        # cached_time is the minimum time interval between stats updates
143        # i.e. XML/RPC calls will not retrieve updated info until the time
144        # since last update is passed (will retrieve old cached info instead)
145        self.timer = Timer(0)
146        self.cached_time = args.cached_time
147
148    def __update__(self):
149        # Never update more than 1 time per cached_time
150        if self.timer.finished():
151            self.stats.update()
152            self.timer = Timer(self.cached_time)
153
154    def init(self):
155        # Return the Glances version
156        return __version__
157
158    def getAll(self):
159        # Update and return all the stats
160        self.__update__()
161        return json.dumps(self.stats.getAll())
162
163    def getAllPlugins(self):
164        # Return the plugins list
165        return json.dumps(self.stats.getPluginsList())
166
167    def getAllLimits(self):
168        # Return all the plugins limits
169        return json.dumps(self.stats.getAllLimitsAsDict())
170
171    def getAllViews(self):
172        # Return all the plugins views
173        return json.dumps(self.stats.getAllViewsAsDict())
174
175    def __getattr__(self, item):
176        """Overwrite the getattr method in case of attribute is not found.
177
178        The goal is to dynamically generate the API get'Stats'() methods.
179        """
180        header = 'get'
181        # Check if the attribute starts with 'get'
182        if item.startswith(header):
183            try:
184                # Update the stat
185                self.__update__()
186                # Return the attribute
187                return getattr(self.stats, item)
188            except Exception:
189                # The method is not found for the plugin
190                raise AttributeError(item)
191        else:
192            # Default behavior
193            raise AttributeError(item)
194
195
196class GlancesServer(object):
197
198    """This class creates and manages the TCP server."""
199
200    def __init__(self,
201                 requestHandler=GlancesXMLRPCHandler,
202                 config=None,
203                 args=None):
204        # Args
205        self.args = args
206
207        # Init the XML RPC server
208        try:
209            self.server = GlancesXMLRPCServer(args.bind_address, args.port, requestHandler)
210        except Exception as e:
211            logger.critical("Cannot start Glances server: {}".format(e))
212            sys.exit(2)
213        else:
214            print('Glances XML-RPC server is running on {}:{}'.format(args.bind_address, args.port))
215
216        # The users dict
217        # username / password couple
218        # By default, no auth is needed
219        self.server.user_dict = {}
220        self.server.isAuth = False
221
222        # Register functions
223        self.server.register_introspection_functions()
224        self.server.register_instance(GlancesInstance(config, args))
225
226        if not self.args.disable_autodiscover:
227            # Note: The Zeroconf service name will be based on the hostname
228            # Correct issue: Zeroconf problem with zeroconf service name #889
229            self.autodiscover_client = GlancesAutoDiscoverClient(socket.gethostname().split('.', 1)[0], args)
230        else:
231            logger.info("Glances autodiscover announce is disabled")
232
233    def add_user(self, username, password):
234        """Add an user to the dictionary."""
235        self.server.user_dict[username] = password
236        self.server.isAuth = True
237
238    def serve_forever(self):
239        """Call the main loop."""
240        # Set the server login/password (if -P/--password tag)
241        if self.args.password != "":
242            self.add_user(self.args.username, self.args.password)
243        # Serve forever
244        self.server.serve_forever()
245
246    def end(self):
247        """End of the Glances server session."""
248        if not self.args.disable_autodiscover:
249            self.autodiscover_client.close()
250        self.server.end()
251