Mercurial > libervia-pubsub
annotate sat_pubsub/bookmark_compat.py @ 494:468b7cd6c344 default tip
bookmark compat: handle mapped errors:
This convert error on request (e.g. missing node) to appropriate stanza error.
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 13 Dec 2024 12:23:47 +0100 |
parents | 4e8e8788bc86 |
children |
rev | line source |
---|---|
491 | 1 #!/usr/bin/env python3 |
2 # | |
3 # Copyright (c) 2015-2024 Jérôme Poisson | |
4 | |
5 | |
6 # This program is free software: you can redistribute it and/or modify | |
7 # it under the terms of the GNU Affero General Public License as published by | |
8 # the Free Software Foundation, either version 3 of the License, or | |
9 # (at your option) any later version. | |
10 | |
11 # This program is distributed in the hope that it will be useful, | |
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 # GNU Affero General Public License for more details. | |
15 | |
16 # You should have received a copy of the GNU Affero General Public License | |
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
18 | |
19 "This module implements a compatibility layer for XEP-0048." | |
20 | |
21 | |
22 from twisted.internet import defer | |
23 from twisted.python import log | |
24 from twisted.words.protocols.jabber import error as jabber_error, jid, xmlstream | |
25 from twisted.words.xish import domish | |
26 from wokkel import disco, iwokkel, pubsub | |
27 from wokkel.iwokkel import IPubSubService | |
28 from zope.interface import implementer | |
29 | |
494
468b7cd6c344
bookmark compat: handle mapped errors:
Goffi <goffi@goffi.org>
parents:
491
diff
changeset
|
30 from sat_pubsub import backend |
491 | 31 from sat_pubsub import const |
32 | |
33 from . import error | |
34 | |
35 NS_IQ_PRIVATE = "jabber:iq:private" | |
36 NS_STORAGE_BOOKMARKS = "storage:bookmarks" | |
37 NS_BOOKMARKS2 = "urn:xmpp:bookmarks:1" | |
38 NS_BOOKMARKS_COMPAT = f"{NS_BOOKMARKS2}#compat" | |
39 IQ_PRIVATE_GET = f'/iq[@type="get"]/query[@xmlns="{NS_IQ_PRIVATE}"]' | |
40 IQ_PRIVATE_SET = f'/iq[@type="set"]/query[@xmlns="{NS_IQ_PRIVATE}"]' | |
41 | |
42 | |
43 @implementer(iwokkel.IDisco) | |
44 class BookmarkCompatHandler(disco.DiscoClientProtocol): | |
494
468b7cd6c344
bookmark compat: handle mapped errors:
Goffi <goffi@goffi.org>
parents:
491
diff
changeset
|
45 error_map = backend.PubSubResourceFromBackend.error_map |
491 | 46 |
47 def __init__(self, service_jid): | |
48 super().__init__() | |
49 self.backend = None | |
50 | |
51 def connectionInitialized(self): | |
52 for handler in self.parent.handlers: | |
53 if IPubSubService.providedBy(handler): | |
54 self._pubsub_service = handler | |
55 break | |
56 self.backend = self.parent.parent.getServiceNamed("backend") | |
57 self.xmlstream.addObserver(IQ_PRIVATE_GET, self._on_get) | |
58 self.xmlstream.addObserver(IQ_PRIVATE_SET, self._on_set) | |
59 | |
494
468b7cd6c344
bookmark compat: handle mapped errors:
Goffi <goffi@goffi.org>
parents:
491
diff
changeset
|
60 def handle_mapped_error(self, e: error.Error, iq_elt: domish.Element) -> None: |
468b7cd6c344
bookmark compat: handle mapped errors:
Goffi <goffi@goffi.org>
parents:
491
diff
changeset
|
61 condition, pubsub_condition, feature = self.error_map[e.__class__] # type: ignore |
468b7cd6c344
bookmark compat: handle mapped errors:
Goffi <goffi@goffi.org>
parents:
491
diff
changeset
|
62 msg = e.msg |
468b7cd6c344
bookmark compat: handle mapped errors:
Goffi <goffi@goffi.org>
parents:
491
diff
changeset
|
63 if pubsub_condition: |
468b7cd6c344
bookmark compat: handle mapped errors:
Goffi <goffi@goffi.org>
parents:
491
diff
changeset
|
64 exc = pubsub.PubSubError(condition, pubsub_condition, feature, msg) |
468b7cd6c344
bookmark compat: handle mapped errors:
Goffi <goffi@goffi.org>
parents:
491
diff
changeset
|
65 else: |
468b7cd6c344
bookmark compat: handle mapped errors:
Goffi <goffi@goffi.org>
parents:
491
diff
changeset
|
66 exc = jabber_error.StanzaError(condition, text=msg) |
468b7cd6c344
bookmark compat: handle mapped errors:
Goffi <goffi@goffi.org>
parents:
491
diff
changeset
|
67 error_elt = exc.toResponse(iq_elt) |
468b7cd6c344
bookmark compat: handle mapped errors:
Goffi <goffi@goffi.org>
parents:
491
diff
changeset
|
68 self.xmlstream.send(error_elt) |
468b7cd6c344
bookmark compat: handle mapped errors:
Goffi <goffi@goffi.org>
parents:
491
diff
changeset
|
69 return |
468b7cd6c344
bookmark compat: handle mapped errors:
Goffi <goffi@goffi.org>
parents:
491
diff
changeset
|
70 |
491 | 71 def _on_get(self, iq_elt: domish.Element) -> None: |
72 """Handle incoming legacy IQ GET requests for bookmarks. | |
73 | |
74 This method processes legacy XEP-0048 IQ GET requests. It does some checks and | |
75 then proceeds to return the bookmarks. | |
76 | |
77 @param iq_elt: The incoming IQ element. | |
78 """ | |
79 if not iq_elt.delegated: | |
80 return | |
81 assert self.xmlstream is not None | |
82 iq_elt.handled = True | |
83 from_jid = jid.JID(iq_elt["from"]) | |
84 to_jid = jid.JID(iq_elt["to"]) | |
85 if from_jid.userhostJID() != to_jid: | |
86 msg = ( | |
87 f"{from_jid.userhost()} is not allowed to access private storage of " | |
88 f"{to_jid.userhost()}!" | |
89 ) | |
90 log.msg(f"Hack attempt? {msg}") | |
91 error_elt = jabber_error.StanzaError("forbidden", text=msg).toResponse(iq_elt) | |
92 self.xmlstream.send(error_elt) | |
93 return | |
94 | |
95 query_elt = iq_elt.query | |
96 assert query_elt is not None | |
97 | |
98 storage_elt = query_elt.firstChildElement() | |
99 if ( | |
100 storage_elt is None | |
101 or storage_elt.name != "storage" | |
102 or storage_elt.uri != NS_STORAGE_BOOKMARKS | |
103 ): | |
104 error_elt = jabber_error.StanzaError( | |
105 "not-allowed", | |
106 text=( | |
107 f'"{NS_STORAGE_BOOKMARKS}" is the only private XML storage allowed ' | |
108 "on this server" | |
109 ), | |
110 ).toResponse(iq_elt) | |
111 self.xmlstream.send(error_elt) | |
112 return | |
113 | |
114 defer.ensureDeferred(self.return_bookmarks(iq_elt, to_jid)) | |
115 | |
116 async def return_bookmarks(self, iq_elt: domish.Element, requestor: jid.JID) -> None: | |
117 """Send IQ result for bookmark request on private XML. | |
118 | |
119 Retrieve bookmark from Bookmarks2 PEP node, convert them to private XML | |
120 XEP-0048 format, and send the IQ result. | |
121 @param iq_elt: The incoming IQ element. | |
122 @param requestor: The JID of the entity requesting the bookmarks. | |
123 """ | |
124 assert self.backend is not None | |
125 assert self.xmlstream is not None | |
494
468b7cd6c344
bookmark compat: handle mapped errors:
Goffi <goffi@goffi.org>
parents:
491
diff
changeset
|
126 try: |
468b7cd6c344
bookmark compat: handle mapped errors:
Goffi <goffi@goffi.org>
parents:
491
diff
changeset
|
127 items, __ = await self.backend.getItems( |
468b7cd6c344
bookmark compat: handle mapped errors:
Goffi <goffi@goffi.org>
parents:
491
diff
changeset
|
128 NS_BOOKMARKS2, requestor, requestor, ext_data={"pep": True} |
468b7cd6c344
bookmark compat: handle mapped errors:
Goffi <goffi@goffi.org>
parents:
491
diff
changeset
|
129 ) |
468b7cd6c344
bookmark compat: handle mapped errors:
Goffi <goffi@goffi.org>
parents:
491
diff
changeset
|
130 except tuple(self.error_map.keys()) as e: |
468b7cd6c344
bookmark compat: handle mapped errors:
Goffi <goffi@goffi.org>
parents:
491
diff
changeset
|
131 return self.handle_mapped_error(e, iq_elt) |
468b7cd6c344
bookmark compat: handle mapped errors:
Goffi <goffi@goffi.org>
parents:
491
diff
changeset
|
132 |
491 | 133 iq_result_elt = xmlstream.toResponse(iq_elt, "result") |
134 | |
135 query_elt = iq_result_elt.addElement((NS_IQ_PRIVATE, "query")) | |
136 storage_elt = query_elt.addElement((NS_STORAGE_BOOKMARKS, "storage")) | |
137 | |
138 # For simply add all bookmarks to get XEP-0048 private XML elements. | |
139 for item_elt in items: | |
140 conference_elt = next(item_elt.elements(NS_BOOKMARKS2, "conference"), None) | |
141 if conference_elt is not None: | |
142 # The namespace is not the same for XEP-0048 | |
143 conference_elt.uri = NS_STORAGE_BOOKMARKS | |
144 for elt in conference_elt.children: | |
145 elt.uri = NS_STORAGE_BOOKMARKS | |
146 conference_elt["jid"] = item_elt["id"] | |
147 storage_elt.addChild(conference_elt) | |
148 else: | |
149 log.msg( | |
150 "Warning: Unexpectedly missing conference element: " | |
151 f"{item_elt.toXml()}" | |
152 ) | |
153 | |
154 self.xmlstream.send(iq_result_elt) | |
155 | |
156 def _on_set(self, iq_elt: domish.Element) -> None: | |
157 if not iq_elt.delegated: | |
158 return | |
159 assert self.xmlstream is not None | |
160 iq_elt.handled = True | |
161 from_jid = jid.JID(iq_elt["from"]) | |
162 to_jid = jid.JID(iq_elt["to"]) | |
163 if from_jid.userhostJID() != to_jid: | |
164 msg = ( | |
165 f"{from_jid.userhost()} is not allowed to access private storage of " | |
166 f"{to_jid.userhost()}!" | |
167 ) | |
168 log.msg(f"Hack attempt? {msg}") | |
169 error_elt = jabber_error.StanzaError("forbidden", text=msg).toResponse(iq_elt) | |
170 self.xmlstream.send(error_elt) | |
171 return | |
172 query_elt = iq_elt.query | |
173 assert query_elt is not None | |
174 storage_elt = query_elt.firstChildElement() | |
175 if ( | |
176 storage_elt is None | |
177 or storage_elt.name != "storage" | |
178 or storage_elt.uri != NS_STORAGE_BOOKMARKS | |
179 ): | |
180 error_elt = jabber_error.StanzaError( | |
181 "not-allowed", | |
182 text=( | |
183 f'"{NS_STORAGE_BOOKMARKS}" is the only private XML storage allowed ' | |
184 "on this server" | |
185 ), | |
186 ).toResponse(iq_elt) | |
187 self.xmlstream.send(error_elt) | |
188 return | |
189 defer.ensureDeferred(self.on_set(iq_elt, from_jid)) | |
190 | |
191 async def publish_bookmarks( | |
192 self, iq_elt: domish.Element, items: list[domish.Element], requestor: jid.JID | |
193 ) -> None: | |
194 """Publish bookmarks on Bookmarks2 PEP node""" | |
195 assert self.backend is not None | |
196 assert self.xmlstream is not None | |
197 try: | |
198 await self.backend.publish( | |
199 NS_BOOKMARKS2, | |
200 items, | |
201 requestor, | |
202 options={const.OPT_ACCESS_MODEL: const.VAL_AMODEL_WHITELIST}, | |
203 pep=True, | |
204 recipient=requestor, | |
205 ) | |
206 except error.NodeNotFound: | |
207 # The node doesn't exist, we create it | |
208 await self.backend.createNode( | |
209 NS_BOOKMARKS2, | |
210 requestor, | |
211 options={const.OPT_ACCESS_MODEL: const.VAL_AMODEL_WHITELIST}, | |
212 pep=True, | |
213 recipient=requestor, | |
214 ) | |
215 await self.publish_bookmarks(iq_elt, items, requestor) | |
216 except Exception as e: | |
217 log.err(f"Error while publishing converted bookmarks: {e}") | |
218 error_elt = jabber_error.StanzaError( | |
219 "internal-server-error", | |
220 text=(f"Something went wrong while publishing the bookmark items: {e}"), | |
221 ).toResponse(iq_elt) | |
222 self.xmlstream.send(error_elt) | |
223 raise e | |
224 | |
225 async def on_set(self, iq_elt: domish.Element, requestor: jid.JID) -> None: | |
226 """Handle IQ set request for bookmarks on private XML. | |
227 | |
228 This method processes an IQ set request to update bookmarks stored in private XML. | |
229 It extracts conference elements from the request, transforms them into XEP-0402 | |
230 format, and publishes them to XEP-0402 PEP node. | |
231 | |
232 @param iq_elt: The incoming IQ element containing the set request. | |
233 @param requestor: The JID of the entity making the request. | |
234 """ | |
235 # TODO: We should check if items already exist in Bookmarks 2 and avoid updating | |
236 # them if there is no change. However, considering that XEP-0048 is deprecated and | |
237 # active implementations without XEP-0402 are rare, it might not be worth the | |
238 # trouble. | |
239 assert self.backend is not None | |
240 assert self.xmlstream is not None | |
241 query_elt = iq_elt.query | |
242 assert query_elt is not None | |
243 # <storage> presence has been checked in ``_on_set``, we know that we have it | |
244 # here. | |
245 storage_elt = next(query_elt.elements(NS_STORAGE_BOOKMARKS, "storage")) | |
246 | |
247 items = [] | |
248 for conference_elt in storage_elt.elements(NS_STORAGE_BOOKMARKS, "conference"): | |
249 item_elt = domish.Element((pubsub.NS_PUBSUB, "item")) | |
250 try: | |
251 item_elt["id"] = conference_elt["jid"] | |
252 except AttributeError: | |
253 log.msg( | |
254 "Warning: ignoring <conference> with missing jid: " | |
255 f"conference_elt.toXml()" | |
256 ) | |
257 continue | |
258 new_conference_elt = item_elt.addElement((NS_BOOKMARKS2, "conference")) | |
259 for attr, value in conference_elt.attributes.items(): | |
260 if attr == "jid": | |
261 continue | |
262 new_conference_elt[attr] = value | |
263 for child in conference_elt.children: | |
264 new_child = domish.Element((NS_BOOKMARKS2, child.name)) | |
265 new_child.addContent(str(child)) | |
266 new_conference_elt.addChild(new_child) | |
267 items.append(item_elt) | |
268 | |
494
468b7cd6c344
bookmark compat: handle mapped errors:
Goffi <goffi@goffi.org>
parents:
491
diff
changeset
|
269 try: |
468b7cd6c344
bookmark compat: handle mapped errors:
Goffi <goffi@goffi.org>
parents:
491
diff
changeset
|
270 await self.publish_bookmarks(iq_elt, items, requestor) |
468b7cd6c344
bookmark compat: handle mapped errors:
Goffi <goffi@goffi.org>
parents:
491
diff
changeset
|
271 except tuple(self.error_map.keys()) as e: |
468b7cd6c344
bookmark compat: handle mapped errors:
Goffi <goffi@goffi.org>
parents:
491
diff
changeset
|
272 return self.handle_mapped_error(e, iq_elt) |
491 | 273 |
274 iq_result_elt = xmlstream.toResponse(iq_elt, "result") | |
275 self.xmlstream.send(iq_result_elt) | |
276 | |
277 def getDiscoInfo(self, requestor, service, nodeIdentifier=""): | |
278 return [ | |
279 disco.DiscoFeature(NS_IQ_PRIVATE), | |
280 disco.DiscoFeature(NS_BOOKMARKS_COMPAT), | |
281 ] | |
282 | |
283 def getDiscoItems(self, requestor, service, nodeIdentifier=""): | |
284 return [] |