__init__.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. #!/usr/bin/env python
  2. from copy import deepcopy
  3. import hashlib
  4. import hmac
  5. import json
  6. import os.path
  7. import signal
  8. import sys
  9. sys.path.insert(0, os.path.dirname(__file__)) # XXX for __import__ to work
  10. import time
  11. import threading
  12. import kopano
  13. from kopano import log_exc, Config
  14. from flask import Flask, request, abort
  15. CONFIG = {
  16. 'data_path': Config.string(default='/var/lib/kopano/presence/'),
  17. 'data_save_interval': Config.integer(default=5),
  18. 'plugins': Config.string(multiple=True, default=[]),
  19. 'run_as_user': Config.string(default="kopano"),
  20. 'run_as_group': Config.string(default="kopano"),
  21. 'server_bind': Config.string(),
  22. 'server_port': Config.integer(),
  23. 'server_auth_user': Config.string(),
  24. 'server_auth_password': Config.string(),
  25. 'server_secret_key': Config.string(),
  26. 'server_token_expire': Config.integer(),
  27. 'xmpp_jid': Config.string(),
  28. 'xmpp_password': Config.string(),
  29. 'xmpp_user_id_strip_domain': Config.boolean(),
  30. 'spreed_auto_unavailable': Config.integer(),
  31. }
  32. STATUSES = ['available', 'busy', 'away', 'unavailable'] # XXX check these?
  33. class Service(kopano.Service):
  34. def main(self):
  35. """ setup internal data, load plugins, setup signal handling, route GET/SET requests via Flask """
  36. self.data = {}
  37. self.lock = threading.Lock()
  38. self.plugins = {}
  39. for plugin in self.config['plugins']:
  40. self.plugins[plugin] = __import__('plugin_%s' % plugin).Plugin(self)
  41. for sig in (signal.SIGINT, signal.SIGTERM):
  42. signal.signal(sig, self.signal_handler)
  43. app = Flask('kopano_presence')
  44. app.add_url_rule('/', 'get', self.get, methods=['GET'])
  45. app.add_url_rule('/', 'put', self.put, methods=['PUT'])
  46. app.add_url_rule('/', 'post', self.post, methods=['POST'])
  47. app.run(host=self.config['server_bind'], port=self.config['server_port']) #, debug=True)
  48. def signal_handler(self, sig, frame):
  49. """ gracefully disconnect plugins on ctrl-c/kill signal """
  50. for plugin in self.plugins.values():
  51. plugin.disconnect()
  52. sys.exit(0)
  53. def check_auth(self):
  54. """ check shared-secret based authentication token """
  55. secret_key = str(self.config['server_secret_key'])
  56. t, userid, sha256 = str(request.json['AuthenticationToken']).split(':')
  57. if (sha256 != hmac.new(secret_key, '%s:%s' % (t, userid), hashlib.sha256).digest().encode('base64').strip().upper()) or \
  58. ((int(time.time()) - int(t)) > (self.config['server_token_expire'] * 60)):
  59. self.warning('unauthorized access; please check shared key settings in presence.cfg and client configuration.')
  60. abort(401)
  61. def get(self):
  62. """ return status for one or more users """
  63. self.check_auth()
  64. with log_exc(self.log):
  65. data = []
  66. for userstatus in request.json['UserStatus']:
  67. user_id = userstatus['user_id']
  68. userdata = self.data_get(user_id) or {}
  69. userdata['user_id'] = user_id
  70. data.append(userdata)
  71. return json.dumps({"Type": "UserStatus", "UserStatus": data}, indent=4)
  72. def put(self):
  73. """ update status for one or more users """
  74. self.check_auth()
  75. with log_exc(self.log):
  76. for userstatus in request.json['UserStatus']:
  77. user_id = userstatus['user_id']
  78. for plugin, plugin_data in userstatus.items():
  79. if plugin != 'user_id': # XXX
  80. plugin_data['user_id'] = user_id
  81. plugin_data['last_update'] = int(time.time())
  82. self.data_set(user_id, plugin, plugin_data.get('status'), plugin_data.get('message'))
  83. self.plugins[plugin].update(user_id, plugin_data)
  84. return ''
  85. def post(self):
  86. """ update status for one or more users and return status for one or more users """
  87. self.check_auth()
  88. with log_exc(self.log):
  89. data = []
  90. for userstatus in request.json['UserStatus']:
  91. user_id = userstatus['user_id']
  92. for plugin, plugin_data in userstatus.items():
  93. if plugin != 'user_id': # XXX
  94. plugin_data['user_id'] = user_id
  95. plugin_data['last_update'] = int(time.time())
  96. self.data_set(user_id, plugin, plugin_data.get('status'), plugin_data.get('message'))
  97. self.plugins[plugin].update(user_id, plugin_data)
  98. userdata = self.data_get(user_id) or {}
  99. userdata['user_id'] = user_id
  100. data.append(userdata)
  101. return json.dumps({"Type": "UserStatus", "UserStatus": data}, indent=4)
  102. def data_get(self, username):
  103. """ atomic get of user data """
  104. with self.lock:
  105. return deepcopy(self.data.get(username))
  106. def data_set(self, username, plugin, status, message):
  107. """ atomic update of user data """
  108. self.log.debug('%s: %s %s' % (plugin, username, status))
  109. with self.lock:
  110. userplugin = self.data.setdefault(username, {}).setdefault(plugin, {})
  111. userplugin['status'] = status
  112. userplugin['message'] = message
  113. def main():
  114. parser = kopano.parser('ckpsF')
  115. options, args = parser.parse_args()
  116. Service('presence', config=CONFIG, options=options).start()
  117. if __name__ == '__main__':
  118. main()