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