comparison sat_pubsub/privilege.py @ 338:6d059f07c2d3

privilege: added presence and +notify initial support: presence privilege is now used and capabilities are cached. When an entity is connected, last items are sent according to +notify nodes in disco. This is initial support, XEP-0356 doesn't allow yet to get roster updated, or synchronises contacts on startup. Only "open" access model is supported for now. "presence" should be added soon as it is trivial to support now. Only last items sending is handled for now, notifications support for new items/deletions should follow. Capabilities hash is not checked yet, with the security concerns that this imply. Check should be added in the future.
author Goffi <goffi@goffi.org>
date Sat, 12 Aug 2017 18:29:32 +0200
parents c7fe09894952
children 28c9579901d3
comparison
equal deleted inserted replaced
337:57a3051ee435 338:6d059f07c2d3
22 # This module implements XEP-0356 (Privileged Entity) to manage rosters, messages and presences 22 # This module implements XEP-0356 (Privileged Entity) to manage rosters, messages and presences
23 23
24 from wokkel import xmppim 24 from wokkel import xmppim
25 from wokkel.compat import IQ 25 from wokkel.compat import IQ
26 from wokkel import pubsub 26 from wokkel import pubsub
27 from wokkel import disco
27 from wokkel.iwokkel import IPubSubService 28 from wokkel.iwokkel import IPubSubService
28 from wokkel.subprotocols import XMPPHandler
29 from twisted.python import log 29 from twisted.python import log
30 from twisted.python import failure 30 from twisted.python import failure
31 from twisted.internet import defer
31 from twisted.words.xish import domish 32 from twisted.words.xish import domish
32 from twisted.words.protocols.jabber import jid 33 from twisted.words.protocols.jabber import jid
34 import time
33 35
34 FORWARDED_NS = 'urn:xmpp:forward:0' 36 FORWARDED_NS = 'urn:xmpp:forward:0'
35 PRIV_ENT_NS = 'urn:xmpp:privilege:1' 37 PRIV_ENT_NS = 'urn:xmpp:privilege:1'
36 PRIV_ENT_ADV_XPATH = '/message/privilege[@xmlns="{}"]'.format(PRIV_ENT_NS) 38 PRIV_ENT_ADV_XPATH = '/message/privilege[@xmlns="{}"]'.format(PRIV_ENT_NS)
37 ROSTER_NS = 'jabber:iq:roster' 39 ROSTER_NS = 'jabber:iq:roster'
48 pass 50 pass
49 51
50 class NotAllowedError(Exception): 52 class NotAllowedError(Exception):
51 pass 53 pass
52 54
53 class PrivilegesHandler(XMPPHandler): 55 class PrivilegesHandler(disco.DiscoClientProtocol):
54 #FIXME: need to manage updates, and database sync 56 #FIXME: need to manage updates, and database sync
55 #TODO: cache 57 #TODO: cache
56 58
57 def __init__(self, service_jid): 59 def __init__(self, service_jid):
58 super(PrivilegesHandler, self).__init__() 60 super(PrivilegesHandler, self).__init__()
59 self._permissions = {PERM_ROSTER: 'none', 61 self._permissions = {PERM_ROSTER: 'none',
60 PERM_MESSAGE: 'none', 62 PERM_MESSAGE: 'none',
61 PERM_PRESENCE: 'none'} 63 PERM_PRESENCE: 'none'}
62 self._pubsub_service = None 64 self._pubsub_service = None
65 self._backend = None
63 # FIXME: we use a hack supposing that our privilege come from hostname 66 # FIXME: we use a hack supposing that our privilege come from hostname
64 # and we are a component named [name].hostname 67 # and we are a component named [name].hostname
65 # but we need to manage properly server 68 # but we need to manage properly server
66 # TODO: do proper server handling 69 # TODO: do proper server handling
67 self.server_jid = jid.JID(service_jid.host.split('.', 1)[1]) 70 self.server_jid = jid.JID(service_jid.host.split('.', 1)[1])
71 self.caps_map = {} # key: full jid, value: caps_hash
72 self.hash_map = {} # key: (hash,version), value: DiscoInfo instance
73 self.roster_cache = {} # key: jid, value: dict with "timestamp" and "roster"
74 self.presence_map = {} # inverted roster: key: jid, value: set of entities who has this jid in roster (with presence of "from" or "both")
75 self.server = None
68 76
69 @property 77 @property
70 def permissions(self): 78 def permissions(self):
71 return self._permissions 79 return self._permissions
72 80
73 def connectionInitialized(self): 81 def connectionInitialized(self):
74 for handler in self.parent.handlers: 82 for handler in self.parent.handlers:
75 if IPubSubService.providedBy(handler): 83 if IPubSubService.providedBy(handler):
76 self._pubsub_service = handler 84 self._pubsub_service = handler
77 break 85 break
86 self._backend = self.parent.parent.getServiceNamed('backend')
78 self.xmlstream.addObserver(PRIV_ENT_ADV_XPATH, self.onAdvertise) 87 self.xmlstream.addObserver(PRIV_ENT_ADV_XPATH, self.onAdvertise)
88 self.xmlstream.addObserver('/presence', self.onPresence)
79 89
80 def onAdvertise(self, message): 90 def onAdvertise(self, message):
81 """Managage the <message/> advertising privileges 91 """Managage the <message/> advertising privileges
82 92
83 self._permissions will be updated according to advertised privileged 93 self._permissions will be updated according to advertised privileged
117 log.msg("WARNING: permission not allowed to get roster") 127 log.msg("WARNING: permission not allowed to get roster")
118 raise failure.Failure(NotAllowedError('roster get is not allowed')) 128 raise failure.Failure(NotAllowedError('roster get is not allowed'))
119 129
120 def processRoster(result): 130 def processRoster(result):
121 roster = {} 131 roster = {}
122 for element in result.elements(ROSTER_NS, 'item'): 132 for element in result.query.elements(ROSTER_NS, 'item'):
123 item = xmppim.RosterItem.fromElement(element) 133 item = xmppim.RosterItem.fromElement(element)
124 roster[item.entity] = item 134 roster[item.entity] = item
125 135
126 return roster 136 return roster
127 137
188 # subscriber) 198 # subscriber)
189 # if redirectURI: 199 # if redirectURI:
190 # redirect = message.event.delete.addElement('redirect') 200 # redirect = message.event.delete.addElement('redirect')
191 # redirect['uri'] = redirectURI 201 # redirect['uri'] = redirectURI
192 # self.send(message) 202 # self.send(message)
203
204
205 ## presence ##
206
207 @defer.inlineCallbacks
208 def onPresence(self, presence_elt):
209 if self.server is None:
210 # FIXME: we use a hack supposing that our delegation come from hostname
211 # and we are a component named [name].hostname
212 # but we need to manage properly allowed servers
213 # TODO: do proper origin security check
214 _, self.server = presence_elt['to'].split('.', 1)
215 from_jid = jid.JID(presence_elt['from'])
216 from_jid_bare = from_jid.userhostJID()
217 if from_jid.host == self.server and from_jid_bare not in self.roster_cache:
218 roster = yield self.getRoster(from_jid_bare)
219 timestamp = time.time()
220 self.roster_cache[from_jid_bare] = {'timestamp': timestamp,
221 'roster': roster,
222 }
223 for roster_jid, roster_item in roster.iteritems():
224 if roster_item.subscriptionFrom:
225 self.presence_map.setdefault(roster_jid, set()).add(from_jid_bare)
226
227 presence_type = presence_elt.getAttribute('type')
228 if presence_type != "unavailable":
229 # new resource available, we check entity capabilities
230 try:
231 c_elt = next(presence_elt.elements('http://jabber.org/protocol/caps', 'c'))
232 hash_ = c_elt['hash']
233 ver = c_elt['ver']
234 except (StopIteration, KeyError):
235 # no capabilities, we don't go further
236 return
237
238 # FIXME: hash is not checked (cf. XEP-0115)
239 disco_tuple = (hash_, ver)
240 if from_jid not in self.caps_map:
241 self.caps_map[from_jid] = disco_tuple
242
243 if disco_tuple not in self.hash_map:
244 # first time we se this hash, what is behind it?
245 infos = yield self.requestInfo(from_jid)
246 self.hash_map[disco_tuple] = {
247 'notify': {f[:-7] for f in infos.features if f.endswith('+notify')},
248 'infos': infos
249 }
250
251 # nodes are the nodes subscribed with +notify
252 nodes = tuple(self.hash_map[disco_tuple]['notify'])
253 if not nodes:
254 return
255 # publishers are entities which have granted presence access to our user + user itself
256 publishers = self.presence_map.get(from_jid_bare, ()) + (from_jid_bare,)
257
258 last_items = yield self._backend.storage.getLastItems(publishers, nodes, ('open',), ('open',), True)
259 # we send message with last item, as required by https://xmpp.org/extensions/xep-0163.html#notify-last
260 for pep_jid, node, item, item_access_model in last_items:
261 self.notifyPublish(pep_jid, node, [(from_jid, None, [item])])