You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

250 lines
8.5 KiB

4 years ago
  1. # -*- coding: utf-8 -*-
  2. #
  3. # This file is part of Glances.
  4. #
  5. # Copyright (C) 2018 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. """Manage the Glances server."""
  20. import json
  21. import socket
  22. import sys
  23. from base64 import b64decode
  24. from glances import __version__
  25. from glances.compat import SimpleXMLRPCRequestHandler, SimpleXMLRPCServer, Server
  26. from glances.autodiscover import GlancesAutoDiscoverClient
  27. from glances.logger import logger
  28. from glances.stats_server import GlancesStatsServer
  29. from glances.timer import Timer
  30. class GlancesXMLRPCHandler(SimpleXMLRPCRequestHandler, object):
  31. """Main XML-RPC handler."""
  32. rpc_paths = ('/RPC2', )
  33. def end_headers(self):
  34. # Hack to add a specific header
  35. # Thk to: https://gist.github.com/rca/4063325
  36. self.send_my_headers()
  37. super(GlancesXMLRPCHandler, self).end_headers()
  38. def send_my_headers(self):
  39. # Specific header is here (solved the issue #227)
  40. self.send_header("Access-Control-Allow-Origin", "*")
  41. def authenticate(self, headers):
  42. # auth = headers.get('Authorization')
  43. try:
  44. (basic, _, encoded) = headers.get('Authorization').partition(' ')
  45. except Exception:
  46. # Client did not ask for authentidaction
  47. # If server need it then exit
  48. return not self.server.isAuth
  49. else:
  50. # Client authentication
  51. (basic, _, encoded) = headers.get('Authorization').partition(' ')
  52. assert basic == 'Basic', 'Only basic authentication supported'
  53. # Encoded portion of the header is a string
  54. # Need to convert to bytestring
  55. encoded_byte_string = encoded.encode()
  56. # Decode base64 byte string to a decoded byte string
  57. decoded_bytes = b64decode(encoded_byte_string)
  58. # Convert from byte string to a regular string
  59. decoded_string = decoded_bytes.decode()
  60. # Get the username and password from the string
  61. (username, _, password) = decoded_string.partition(':')
  62. # Check that username and password match internal global dictionary
  63. return self.check_user(username, password)
  64. def check_user(self, username, password):
  65. # Check username and password in the dictionary
  66. if username in self.server.user_dict:
  67. from glances.password import GlancesPassword
  68. pwd = GlancesPassword()
  69. return pwd.check_password(self.server.user_dict[username], password)
  70. else:
  71. return False
  72. def parse_request(self):
  73. if SimpleXMLRPCRequestHandler.parse_request(self):
  74. # Next we authenticate
  75. if self.authenticate(self.headers):
  76. return True
  77. else:
  78. # if authentication fails, tell the client
  79. self.send_error(401, 'Authentication failed')
  80. return False
  81. def log_message(self, log_format, *args):
  82. # No message displayed on the server side
  83. pass
  84. class GlancesXMLRPCServer(SimpleXMLRPCServer, object):
  85. """Init a SimpleXMLRPCServer instance (IPv6-ready)."""
  86. finished = False
  87. def __init__(self, bind_address, bind_port=61209,
  88. requestHandler=GlancesXMLRPCHandler):
  89. self.bind_address = bind_address
  90. self.bind_port = bind_port
  91. try:
  92. self.address_family = socket.getaddrinfo(bind_address, bind_port)[0][0]
  93. except socket.error as e:
  94. logger.error("Couldn't open socket: {}".format(e))
  95. sys.exit(1)
  96. super(GlancesXMLRPCServer, self).__init__((bind_address, bind_port), requestHandler)
  97. def end(self):
  98. """Stop the server"""
  99. self.server_close()
  100. self.finished = True
  101. def serve_forever(self):
  102. """Main loop"""
  103. while not self.finished:
  104. self.handle_request()
  105. class GlancesInstance(object):
  106. """All the methods of this class are published as XML-RPC methods."""
  107. def __init__(self,
  108. config=None,
  109. args=None):
  110. # Init stats
  111. self.stats = GlancesStatsServer(config=config, args=args)
  112. # Initial update
  113. self.stats.update()
  114. # cached_time is the minimum time interval between stats updates
  115. # i.e. XML/RPC calls will not retrieve updated info until the time
  116. # since last update is passed (will retrieve old cached info instead)
  117. self.timer = Timer(0)
  118. self.cached_time = args.cached_time
  119. def __update__(self):
  120. # Never update more than 1 time per cached_time
  121. if self.timer.finished():
  122. self.stats.update()
  123. self.timer = Timer(self.cached_time)
  124. def init(self):
  125. # Return the Glances version
  126. return __version__
  127. def getAll(self):
  128. # Update and return all the stats
  129. self.__update__()
  130. return json.dumps(self.stats.getAll())
  131. def getAllPlugins(self):
  132. # Return the plugins list
  133. return json.dumps(self.stats.getPluginsList())
  134. def getAllLimits(self):
  135. # Return all the plugins limits
  136. return json.dumps(self.stats.getAllLimitsAsDict())
  137. def getAllViews(self):
  138. # Return all the plugins views
  139. return json.dumps(self.stats.getAllViewsAsDict())
  140. def __getattr__(self, item):
  141. """Overwrite the getattr method in case of attribute is not found.
  142. The goal is to dynamically generate the API get'Stats'() methods.
  143. """
  144. header = 'get'
  145. # Check if the attribute starts with 'get'
  146. if item.startswith(header):
  147. try:
  148. # Update the stat
  149. self.__update__()
  150. # Return the attribute
  151. return getattr(self.stats, item)
  152. except Exception:
  153. # The method is not found for the plugin
  154. raise AttributeError(item)
  155. else:
  156. # Default behavior
  157. raise AttributeError(item)
  158. class GlancesServer(object):
  159. """This class creates and manages the TCP server."""
  160. def __init__(self,
  161. requestHandler=GlancesXMLRPCHandler,
  162. config=None,
  163. args=None):
  164. # Args
  165. self.args = args
  166. # Init the XML RPC server
  167. try:
  168. self.server = GlancesXMLRPCServer(args.bind_address, args.port, requestHandler)
  169. except Exception as e:
  170. logger.critical("Cannot start Glances server: {}".format(e))
  171. sys.exit(2)
  172. else:
  173. print('Glances XML-RPC server is running on {}:{}'.format(args.bind_address, args.port))
  174. # The users dict
  175. # username / password couple
  176. # By default, no auth is needed
  177. self.server.user_dict = {}
  178. self.server.isAuth = False
  179. # Register functions
  180. self.server.register_introspection_functions()
  181. self.server.register_instance(GlancesInstance(config, args))
  182. if not self.args.disable_autodiscover:
  183. # Note: The Zeroconf service name will be based on the hostname
  184. # Correct issue: Zeroconf problem with zeroconf service name #889
  185. self.autodiscover_client = GlancesAutoDiscoverClient(socket.gethostname().split('.', 1)[0], args)
  186. else:
  187. logger.info("Glances autodiscover announce is disabled")
  188. def add_user(self, username, password):
  189. """Add an user to the dictionary."""
  190. self.server.user_dict[username] = password
  191. self.server.isAuth = True
  192. def serve_forever(self):
  193. """Call the main loop."""
  194. # Set the server login/password (if -P/--password tag)
  195. if self.args.password != "":
  196. self.add_user(self.args.username, self.args.password)
  197. # Serve forever
  198. self.server.serve_forever()
  199. def end(self):
  200. """End of the Glances server session."""
  201. if not self.args.disable_autodiscover:
  202. self.autodiscover_client.close()
  203. self.server.end()