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=""):