Mercurial > libervia-backend
comparison libervia/backend/plugins/plugin_xep_0363.py @ 4270:0d7bb4df2343
Reformatted code base using black.
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 19 Jun 2024 18:44:57 +0200 |
parents | 72b95cdc3432 |
children |
comparison
equal
deleted
inserted
replaced
4269:64a85ce8be70 | 4270:0d7bb4df2343 |
---|---|
55 C.PI_DESCRIPTION: _("""Implementation of HTTP File Upload"""), | 55 C.PI_DESCRIPTION: _("""Implementation of HTTP File Upload"""), |
56 } | 56 } |
57 | 57 |
58 NS_HTTP_UPLOAD = "urn:xmpp:http:upload:0" | 58 NS_HTTP_UPLOAD = "urn:xmpp:http:upload:0" |
59 IQ_HTTP_UPLOAD_REQUEST = C.IQ_GET + '/request[@xmlns="' + NS_HTTP_UPLOAD + '"]' | 59 IQ_HTTP_UPLOAD_REQUEST = C.IQ_GET + '/request[@xmlns="' + NS_HTTP_UPLOAD + '"]' |
60 ALLOWED_HEADERS = ('authorization', 'cookie', 'expires') | 60 ALLOWED_HEADERS = ("authorization", "cookie", "expires") |
61 | 61 |
62 | 62 |
63 @dataclass | 63 @dataclass |
64 class Slot: | 64 class Slot: |
65 """Upload slot""" | 65 """Upload slot""" |
66 | |
66 put: str | 67 put: str |
67 get: str | 68 get: str |
68 headers: list | 69 headers: list |
69 | 70 |
70 | 71 |
79 callback: Callable[[SatXMPPComponent, UploadRequest], Optional[Slot]] | 80 callback: Callable[[SatXMPPComponent, UploadRequest], Optional[Slot]] |
80 priority: int | 81 priority: int |
81 | 82 |
82 | 83 |
83 class XEP_0363: | 84 class XEP_0363: |
84 Slot=Slot | 85 Slot = Slot |
85 | 86 |
86 def __init__(self, host): | 87 def __init__(self, host): |
87 log.info(_("plugin HTTP File Upload initialization")) | 88 log.info(_("plugin HTTP File Upload initialization")) |
88 self.host = host | 89 self.host = host |
89 host.bridge.add_method( | 90 host.bridge.add_method( |
134 return file_too_large_elt | 135 return file_too_large_elt |
135 | 136 |
136 async def get_http_upload_entity(self, client, upload_jid=None): | 137 async def get_http_upload_entity(self, client, upload_jid=None): |
137 """Get HTTP upload capable entity | 138 """Get HTTP upload capable entity |
138 | 139 |
139 upload_jid is checked, then its components | 140 upload_jid is checked, then its components |
140 @param upload_jid(None, jid.JID): entity to check | 141 @param upload_jid(None, jid.JID): entity to check |
141 @return(D(jid.JID)): first HTTP upload capable entity | 142 @return(D(jid.JID)): first HTTP upload capable entity |
142 @raise exceptions.NotFound: no entity found | 143 @raise exceptions.NotFound: no entity found |
143 """ | 144 """ |
144 try: | 145 try: |
145 entity = client.http_upload_service | 146 entity = client.http_upload_service |
146 except AttributeError: | 147 except AttributeError: |
147 found_entities = await self.host.find_features_set(client, (NS_HTTP_UPLOAD,)) | 148 found_entities = await self.host.find_features_set(client, (NS_HTTP_UPLOAD,)) |
148 try: | 149 try: |
153 if entity is None: | 154 if entity is None: |
154 raise exceptions.NotFound("No HTTP upload entity found") | 155 raise exceptions.NotFound("No HTTP upload entity found") |
155 | 156 |
156 return entity | 157 return entity |
157 | 158 |
158 def _file_http_upload(self, filepath, filename="", upload_jid="", | 159 def _file_http_upload( |
159 ignore_tls_errors=False, profile=C.PROF_KEY_NONE): | 160 self, |
161 filepath, | |
162 filename="", | |
163 upload_jid="", | |
164 ignore_tls_errors=False, | |
165 profile=C.PROF_KEY_NONE, | |
166 ): | |
160 assert os.path.isabs(filepath) and os.path.isfile(filepath) | 167 assert os.path.isabs(filepath) and os.path.isfile(filepath) |
161 client = self.host.get_client(profile) | 168 client = self.host.get_client(profile) |
162 return defer.ensureDeferred(self.file_http_upload( | 169 return defer.ensureDeferred( |
163 client, | 170 self.file_http_upload( |
164 filepath, | 171 client, |
165 filename or None, | 172 filepath, |
166 jid.JID(upload_jid) if upload_jid else None, | 173 filename or None, |
167 {"ignore_tls_errors": ignore_tls_errors}, | 174 jid.JID(upload_jid) if upload_jid else None, |
168 )) | 175 {"ignore_tls_errors": ignore_tls_errors}, |
176 ) | |
177 ) | |
169 | 178 |
170 async def file_http_upload( | 179 async def file_http_upload( |
171 self, | 180 self, |
172 client: SatXMPPEntity, | 181 client: SatXMPPEntity, |
173 filepath: Path, | 182 filepath: Path, |
174 filename: Optional[str] = None, | 183 filename: Optional[str] = None, |
175 upload_jid: Optional[jid.JID] = None, | 184 upload_jid: Optional[jid.JID] = None, |
176 extra: Optional[dict] = None | 185 extra: Optional[dict] = None, |
177 ) -> Tuple[str, defer.Deferred]: | 186 ) -> Tuple[str, defer.Deferred]: |
178 """Upload a file through HTTP | 187 """Upload a file through HTTP |
179 | 188 |
180 @param filepath: absolute path of the file | 189 @param filepath: absolute path of the file |
181 @param filename: name to use for the upload | 190 @param filename: name to use for the upload |
198 } | 207 } |
199 | 208 |
200 #: this trigger can be used to modify the filename or size requested when geting | 209 #: this trigger can be used to modify the filename or size requested when geting |
201 #: the slot, it is notably useful with encryption. | 210 #: the slot, it is notably useful with encryption. |
202 self.host.trigger.point( | 211 self.host.trigger.point( |
203 "XEP-0363_upload_pre_slot", client, extra, file_metadata, | 212 "XEP-0363_upload_pre_slot", |
204 triggers_no_cancel=True | 213 client, |
214 extra, | |
215 file_metadata, | |
216 triggers_no_cancel=True, | |
205 ) | 217 ) |
206 try: | 218 try: |
207 slot = await self.get_slot( | 219 slot = await self.get_slot( |
208 client, file_metadata["filename"], file_metadata["size"], | 220 client, |
209 upload_jid=upload_jid | 221 file_metadata["filename"], |
222 file_metadata["size"], | |
223 upload_jid=upload_jid, | |
210 ) | 224 ) |
211 except Exception as e: | 225 except Exception as e: |
212 log.warning(_("Can't get upload slot: {reason}").format(reason=e)) | 226 log.warning(_("Can't get upload slot: {reason}").format(reason=e)) |
213 raise e | 227 raise e |
214 else: | 228 else: |
215 log.debug(f"Got upload slot: {slot}") | 229 log.debug(f"Got upload slot: {slot}") |
216 sat_file = self.host.plugins["FILE"].File( | 230 sat_file = self.host.plugins["FILE"].File( |
217 self.host, client, filepath, uid=extra.get("progress_id"), | 231 self.host, |
232 client, | |
233 filepath, | |
234 uid=extra.get("progress_id"), | |
218 size=file_metadata["size"], | 235 size=file_metadata["size"], |
219 auto_end_signals=False | 236 auto_end_signals=False, |
220 ) | 237 ) |
221 progress_id = sat_file.uid | 238 progress_id = sat_file.uid |
222 | 239 |
223 file_producer = http_client.FileBodyProducer(sat_file) | 240 file_producer = http_client.FileBodyProducer(sat_file) |
224 | 241 |
228 agent = http_client.Agent(reactor) | 245 agent = http_client.Agent(reactor) |
229 | 246 |
230 headers = {"User-Agent": [C.APP_NAME.encode("utf-8")]} | 247 headers = {"User-Agent": [C.APP_NAME.encode("utf-8")]} |
231 | 248 |
232 for name, value in slot.headers: | 249 for name, value in slot.headers: |
233 name = name.encode('utf-8') | 250 name = name.encode("utf-8") |
234 value = value.encode('utf-8') | 251 value = value.encode("utf-8") |
235 headers[name] = [value] | 252 headers[name] = [value] |
236 | 253 |
237 | |
238 await self.host.trigger.async_point( | 254 await self.host.trigger.async_point( |
239 "XEP-0363_upload", client, extra, sat_file, file_producer, slot, | 255 "XEP-0363_upload", |
240 triggers_no_cancel=True) | 256 client, |
257 extra, | |
258 sat_file, | |
259 file_producer, | |
260 slot, | |
261 triggers_no_cancel=True, | |
262 ) | |
241 | 263 |
242 download_d = agent.request( | 264 download_d = agent.request( |
243 b"PUT", | 265 b"PUT", |
244 slot.put.encode("utf-8"), | 266 slot.put.encode("utf-8"), |
245 http_headers.Headers(headers), | 267 http_headers.Headers(headers), |
284 msg = "can't upload file" | 306 msg = "can't upload file" |
285 log.warning(msg + ": " + str(wrapped_fail.value)) | 307 log.warning(msg + ": " + str(wrapped_fail.value)) |
286 sat_file.progress_error(msg) | 308 sat_file.progress_error(msg) |
287 raise failure_ | 309 raise failure_ |
288 | 310 |
289 def _get_slot(self, filename, size, content_type, upload_jid, | 311 def _get_slot( |
290 profile_key=C.PROF_KEY_NONE): | 312 self, filename, size, content_type, upload_jid, profile_key=C.PROF_KEY_NONE |
313 ): | |
291 """Get an upload slot | 314 """Get an upload slot |
292 | 315 |
293 This method can be used when uploading is done by the frontend | 316 This method can be used when uploading is done by the frontend |
294 @param filename(unicode): name of the file to upload | 317 @param filename(unicode): name of the file to upload |
295 @param size(int): size of the file (must be non null) | 318 @param size(int): size of the file (must be non null) |
297 @param content_type(unicode, None): MIME type of the content | 320 @param content_type(unicode, None): MIME type of the content |
298 empty string or None to guess automatically | 321 empty string or None to guess automatically |
299 """ | 322 """ |
300 client = self.host.get_client(profile_key) | 323 client = self.host.get_client(profile_key) |
301 filename = filename.replace("/", "_") | 324 filename = filename.replace("/", "_") |
302 d = defer.ensureDeferred(self.get_slot( | 325 d = defer.ensureDeferred( |
303 client, filename, size, content_type or None, | 326 self.get_slot( |
304 jid.JID(upload_jid) if upload_jid else None | 327 client, |
305 )) | 328 filename, |
329 size, | |
330 content_type or None, | |
331 jid.JID(upload_jid) if upload_jid else None, | |
332 ) | |
333 ) | |
306 d.addCallback(lambda slot: (slot.get, slot.put, slot.headers)) | 334 d.addCallback(lambda slot: (slot.get, slot.put, slot.headers)) |
307 return d | 335 return d |
308 | 336 |
309 async def get_slot(self, client, filename, size, content_type=None, upload_jid=None): | 337 async def get_slot(self, client, filename, size, content_type=None, upload_jid=None): |
310 """Get a slot (i.e. download/upload links) | 338 """Get a slot (i.e. download/upload links) |
328 try: | 356 try: |
329 upload_jid = client.http_upload_service | 357 upload_jid = client.http_upload_service |
330 except AttributeError: | 358 except AttributeError: |
331 found_entity = await self.get_http_upload_entity(client) | 359 found_entity = await self.get_http_upload_entity(client) |
332 return await self.get_slot( | 360 return await self.get_slot( |
333 client, filename, size, content_type, found_entity) | 361 client, filename, size, content_type, found_entity |
362 ) | |
334 else: | 363 else: |
335 if upload_jid is None: | 364 if upload_jid is None: |
336 raise exceptions.NotFound("No HTTP upload entity found") | 365 raise exceptions.NotFound("No HTTP upload entity found") |
337 | 366 |
338 iq_elt = client.IQ("get") | 367 iq_elt = client.IQ("get") |
346 iq_result_elt = await iq_elt.send() | 375 iq_result_elt = await iq_elt.send() |
347 | 376 |
348 try: | 377 try: |
349 slot_elt = next(iq_result_elt.elements(NS_HTTP_UPLOAD, "slot")) | 378 slot_elt = next(iq_result_elt.elements(NS_HTTP_UPLOAD, "slot")) |
350 put_elt = next(slot_elt.elements(NS_HTTP_UPLOAD, "put")) | 379 put_elt = next(slot_elt.elements(NS_HTTP_UPLOAD, "put")) |
351 put_url = put_elt['url'] | 380 put_url = put_elt["url"] |
352 get_elt = next(slot_elt.elements(NS_HTTP_UPLOAD, "get")) | 381 get_elt = next(slot_elt.elements(NS_HTTP_UPLOAD, "get")) |
353 get_url = get_elt['url'] | 382 get_url = get_elt["url"] |
354 except (StopIteration, KeyError): | 383 except (StopIteration, KeyError): |
355 raise exceptions.DataError("Incorrect stanza received from server") | 384 raise exceptions.DataError("Incorrect stanza received from server") |
356 | 385 |
357 headers = [] | 386 headers = [] |
358 for header_elt in put_elt.elements(NS_HTTP_UPLOAD, "header"): | 387 for header_elt in put_elt.elements(NS_HTTP_UPLOAD, "header"): |
359 try: | 388 try: |
360 name = header_elt["name"] | 389 name = header_elt["name"] |
361 value = str(header_elt) | 390 value = str(header_elt) |
362 except KeyError: | 391 except KeyError: |
363 log.warning(_("Invalid header element: {xml}").format( | 392 log.warning( |
364 iq_result_elt.toXml())) | 393 _("Invalid header element: {xml}").format(iq_result_elt.toXml()) |
394 ) | |
365 continue | 395 continue |
366 name = name.replace('\n', '') | 396 name = name.replace("\n", "") |
367 value = value.replace('\n', '') | 397 value = value.replace("\n", "") |
368 if name.lower() not in ALLOWED_HEADERS: | 398 if name.lower() not in ALLOWED_HEADERS: |
369 log.warning(_('Ignoring unauthorised header "{name}": {xml}') | 399 log.warning( |
370 .format(name=name, xml = iq_result_elt.toXml())) | 400 _('Ignoring unauthorised header "{name}": {xml}').format( |
401 name=name, xml=iq_result_elt.toXml() | |
402 ) | |
403 ) | |
371 continue | 404 continue |
372 headers.append((name, value)) | 405 headers.append((name, value)) |
373 | 406 |
374 return Slot(put=put_url, get=get_url, headers=headers) | 407 return Slot(put=put_url, get=get_url, headers=headers) |
375 | 408 |
376 # component | 409 # component |
377 | 410 |
378 def on_component_request(self, iq_elt, client): | 411 def on_component_request(self, iq_elt, client): |
379 iq_elt.handled=True | 412 iq_elt.handled = True |
380 defer.ensureDeferred(self.handle_component_request(client, iq_elt)) | 413 defer.ensureDeferred(self.handle_component_request(client, iq_elt)) |
381 | 414 |
382 async def handle_component_request(self, client, iq_elt): | 415 async def handle_component_request(self, client, iq_elt): |
383 try: | 416 try: |
384 request_elt = next(iq_elt.elements(NS_HTTP_UPLOAD, "request")) | 417 request_elt = next(iq_elt.elements(NS_HTTP_UPLOAD, "request")) |
385 request = UploadRequest( | 418 request = UploadRequest( |
386 from_=jid.JID(iq_elt['from']), | 419 from_=jid.JID(iq_elt["from"]), |
387 filename=parse.quote(request_elt['filename'].replace('/', '_'), safe=''), | 420 filename=parse.quote(request_elt["filename"].replace("/", "_"), safe=""), |
388 size=int(request_elt['size']), | 421 size=int(request_elt["size"]), |
389 content_type=request_elt.getAttribute('content-type') | 422 content_type=request_elt.getAttribute("content-type"), |
390 ) | 423 ) |
391 except (StopIteration, KeyError, ValueError): | 424 except (StopIteration, KeyError, ValueError): |
392 client.sendError(iq_elt, "bad-request") | 425 client.sendError(iq_elt, "bad-request") |
393 return | 426 return |
394 | 427 |
409 else: | 442 else: |
410 if slot: | 443 if slot: |
411 break | 444 break |
412 else: | 445 else: |
413 log.warning( | 446 log.warning( |
414 _("no service can handle HTTP Upload request: {elt}") | 447 _("no service can handle HTTP Upload request: {elt}").format( |
415 .format(elt=iq_elt.toXml())) | 448 elt=iq_elt.toXml() |
449 ) | |
450 ) | |
416 if err is None: | 451 if err is None: |
417 err = error.StanzaError("feature-not-implemented") | 452 err = error.StanzaError("feature-not-implemented") |
418 client.send(err.toResponse(iq_elt)) | 453 client.send(err.toResponse(iq_elt)) |
419 return | 454 return |
420 | 455 |
421 iq_result_elt = xmlstream.toResponse(iq_elt, "result") | 456 iq_result_elt = xmlstream.toResponse(iq_elt, "result") |
422 slot_elt = iq_result_elt.addElement((NS_HTTP_UPLOAD, 'slot')) | 457 slot_elt = iq_result_elt.addElement((NS_HTTP_UPLOAD, "slot")) |
423 put_elt = slot_elt.addElement('put') | 458 put_elt = slot_elt.addElement("put") |
424 put_elt['url'] = slot.put | 459 put_elt["url"] = slot.put |
425 get_elt = slot_elt.addElement('get') | 460 get_elt = slot_elt.addElement("get") |
426 get_elt['url'] = slot.get | 461 get_elt["url"] = slot.get |
427 client.send(iq_result_elt) | 462 client.send(iq_result_elt) |
428 | 463 |
429 | 464 |
430 @implementer(iwokkel.IDisco) | 465 @implementer(iwokkel.IDisco) |
431 class XEP_0363_handler(xmlstream.XMPPHandler): | 466 class XEP_0363_handler(xmlstream.XMPPHandler): |
432 | 467 |
433 def __init__(self, plugin_parent): | 468 def __init__(self, plugin_parent): |
434 self.plugin_parent = plugin_parent | 469 self.plugin_parent = plugin_parent |
435 | 470 |
436 def connectionInitialized(self): | 471 def connectionInitialized(self): |
437 if ((self.parent.is_component | 472 if ( |
438 and PLUGIN_INFO[C.PI_IMPORT_NAME] in self.parent.enabled_features)): | 473 self.parent.is_component |
474 and PLUGIN_INFO[C.PI_IMPORT_NAME] in self.parent.enabled_features | |
475 ): | |
439 self.xmlstream.addObserver( | 476 self.xmlstream.addObserver( |
440 IQ_HTTP_UPLOAD_REQUEST, self.plugin_parent.on_component_request, | 477 IQ_HTTP_UPLOAD_REQUEST, |
441 client=self.parent | 478 self.plugin_parent.on_component_request, |
479 client=self.parent, | |
442 ) | 480 ) |
443 | 481 |
444 def getDiscoInfo(self, requestor, target, nodeIdentifier=""): | 482 def getDiscoInfo(self, requestor, target, nodeIdentifier=""): |
445 if ((self.parent.is_component | 483 if ( |
446 and not PLUGIN_INFO[C.PI_IMPORT_NAME] in self.parent.enabled_features)): | 484 self.parent.is_component |
485 and not PLUGIN_INFO[C.PI_IMPORT_NAME] in self.parent.enabled_features | |
486 ): | |
447 return [] | 487 return [] |
448 else: | 488 else: |
449 return [disco.DiscoFeature(NS_HTTP_UPLOAD)] | 489 return [disco.DiscoFeature(NS_HTTP_UPLOAD)] |
450 | 490 |
451 def getDiscoItems(self, requestor, target, nodeIdentifier=""): | 491 def getDiscoItems(self, requestor, target, nodeIdentifier=""): |