Mercurial > libervia-pubsub
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])]) |