Mercurial > libervia-backend
comparison sat/plugins/plugin_xep_0363.py @ 3289:9057713ab124
plugin comp file sharing: files can now be uploaded/downloaded via HTTP:
plugin XEP-0363 can now be used by components, and file sharing uses it.
The new `public_id` file metadata is used to serve files.
Files uploaded are put in the `/uploads` path.
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 29 May 2020 21:55:45 +0200 |
parents | 163014f09bf4 |
children | f5a5aa9fa73a |
comparison
equal
deleted
inserted
replaced
3288:780fb8dd07ef | 3289:9057713ab124 |
---|---|
16 # You should have received a copy of the GNU Affero General Public License | 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/>. | 17 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
18 | 18 |
19 import os.path | 19 import os.path |
20 import mimetypes | 20 import mimetypes |
21 from typing import NamedTuple, Callable, Optional | |
21 from dataclasses import dataclass | 22 from dataclasses import dataclass |
23 from urllib import parse | |
22 from wokkel import disco, iwokkel | 24 from wokkel import disco, iwokkel |
23 from zope.interface import implementer | 25 from zope.interface import implementer |
24 from twisted.words.protocols.jabber import jid | 26 from twisted.words.protocols.jabber import jid, xmlstream, error |
25 from twisted.words.protocols.jabber.xmlstream import XMPPHandler | 27 from twisted.words.xish import domish |
26 from twisted.internet import reactor | 28 from twisted.internet import reactor |
27 from twisted.internet import defer | 29 from twisted.internet import defer |
28 from twisted.web import client as http_client | 30 from twisted.web import client as http_client |
29 from twisted.web import http_headers | 31 from twisted.web import http_headers |
30 from sat.core.i18n import _ | 32 from sat.core.i18n import _ |
33 from sat.core.xmpp import SatXMPPComponent | |
31 from sat.core.constants import Const as C | 34 from sat.core.constants import Const as C |
32 from sat.core.log import getLogger | 35 from sat.core.log import getLogger |
33 from sat.core import exceptions | 36 from sat.core import exceptions |
34 from sat.tools import web as sat_web | 37 from sat.tools import web as sat_web |
35 | 38 |
38 | 41 |
39 PLUGIN_INFO = { | 42 PLUGIN_INFO = { |
40 C.PI_NAME: "HTTP File Upload", | 43 C.PI_NAME: "HTTP File Upload", |
41 C.PI_IMPORT_NAME: "XEP-0363", | 44 C.PI_IMPORT_NAME: "XEP-0363", |
42 C.PI_TYPE: "XEP", | 45 C.PI_TYPE: "XEP", |
46 C.PI_MODES: C.PLUG_MODE_BOTH, | |
43 C.PI_PROTOCOLS: ["XEP-0363"], | 47 C.PI_PROTOCOLS: ["XEP-0363"], |
44 C.PI_DEPENDENCIES: ["FILE", "UPLOAD"], | 48 C.PI_DEPENDENCIES: ["FILE", "UPLOAD"], |
45 C.PI_MAIN: "XEP_0363", | 49 C.PI_MAIN: "XEP_0363", |
46 C.PI_HANDLER: "yes", | 50 C.PI_HANDLER: "yes", |
47 C.PI_DESCRIPTION: _("""Implementation of HTTP File Upload"""), | 51 C.PI_DESCRIPTION: _("""Implementation of HTTP File Upload"""), |
48 } | 52 } |
49 | 53 |
50 NS_HTTP_UPLOAD = "urn:xmpp:http:upload:0" | 54 NS_HTTP_UPLOAD = "urn:xmpp:http:upload:0" |
55 IQ_HTTP_UPLOAD_REQUEST = C.IQ_GET + '/request[@xmlns="' + NS_HTTP_UPLOAD + '"]' | |
51 ALLOWED_HEADERS = ('authorization', 'cookie', 'expires') | 56 ALLOWED_HEADERS = ('authorization', 'cookie', 'expires') |
52 | 57 |
53 | 58 |
54 @dataclass | 59 @dataclass |
55 class Slot: | 60 class Slot: |
57 put: str | 62 put: str |
58 get: str | 63 get: str |
59 headers: list | 64 headers: list |
60 | 65 |
61 | 66 |
62 class XEP_0363(object): | 67 class UploadRequest(NamedTuple): |
68 from_: jid.JID | |
69 filename: str | |
70 size: int | |
71 content_type: Optional[str] | |
72 | |
73 | |
74 class RequestHandler(NamedTuple): | |
75 callback: Callable[[SatXMPPComponent, UploadRequest], Optional[Slot]] | |
76 priority: int | |
77 | |
78 | |
79 class XEP_0363: | |
80 Slot=Slot | |
81 | |
63 def __init__(self, host): | 82 def __init__(self, host): |
64 log.info(_("plugin HTTP File Upload initialization")) | 83 log.info(_("plugin HTTP File Upload initialization")) |
65 self.host = host | 84 self.host = host |
66 host.bridge.addMethod( | 85 host.bridge.addMethod( |
67 "fileHTTPUpload", | 86 "fileHTTPUpload", |
79 async_=True, | 98 async_=True, |
80 ) | 99 ) |
81 host.plugins["UPLOAD"].register( | 100 host.plugins["UPLOAD"].register( |
82 "HTTP Upload", self.getHTTPUploadEntity, self.fileHTTPUpload | 101 "HTTP Upload", self.getHTTPUploadEntity, self.fileHTTPUpload |
83 ) | 102 ) |
103 # list of callbacks used when a request is done to a component | |
104 self.handlers = [] | |
84 | 105 |
85 def getHandler(self, client): | 106 def getHandler(self, client): |
86 return XEP_0363_handler() | 107 return XEP_0363_handler(self) |
108 | |
109 def registerHandler(self, callback, priority=0): | |
110 """Register a request handler | |
111 | |
112 @param callack: method to call when a request is done | |
113 the callback must return a Slot if the request is handled, | |
114 otherwise, other callbacks will be tried. | |
115 If the callback raises a StanzaError, its condition will be used if no other | |
116 callback can handle the request. | |
117 @param priority: handlers with higher priorities will be called first | |
118 """ | |
119 assert callback not in self.handlers | |
120 req_handler = RequestHandler(callback, priority) | |
121 self.handlers.append(req_handler) | |
122 self.handlers.sort(key=lambda handler: handler.priority, reverse=True) | |
87 | 123 |
88 async def getHTTPUploadEntity(self, client, upload_jid=None): | 124 async def getHTTPUploadEntity(self, client, upload_jid=None): |
89 """Get HTTP upload capable entity | 125 """Get HTTP upload capable entity |
90 | 126 |
91 upload_jid is checked, then its components | 127 upload_jid is checked, then its components |
311 continue | 347 continue |
312 headers.append((name, value)) | 348 headers.append((name, value)) |
313 | 349 |
314 return Slot(put=put_url, get=get_url, headers=headers) | 350 return Slot(put=put_url, get=get_url, headers=headers) |
315 | 351 |
352 # component | |
353 | |
354 def onComponentRequest(self, iq_elt, client): | |
355 iq_elt.handled=True | |
356 try: | |
357 request_elt = next(iq_elt.elements(NS_HTTP_UPLOAD, "request")) | |
358 request = UploadRequest( | |
359 from_=jid.JID(iq_elt['from']), | |
360 filename=parse.quote(request_elt['filename'].replace('/', '_'), safe=''), | |
361 size=int(request_elt['size']), | |
362 content_type=request_elt.getAttribute('content-type') | |
363 ) | |
364 except (StopIteration, KeyError, ValueError): | |
365 client.sendError(iq_elt, "bad-request") | |
366 return | |
367 | |
368 err = None | |
369 | |
370 for handler in self.handlers: | |
371 try: | |
372 slot = handler.callback(client, request) | |
373 except error.StanzaError as e: | |
374 log.warning(f"a stanza error has been raised while processing HTTP Upload of request: {e}") | |
375 if err is None: | |
376 # we keep the first error to return its condition later, | |
377 # if no other callback handle the request | |
378 err = e | |
379 if slot: | |
380 break | |
381 else: | |
382 log.warning( | |
383 _("no service can handle HTTP Upload request: {elt}") | |
384 .format(elt=iq_elt.toXml())) | |
385 if err is not None: | |
386 condition = err.condition | |
387 else: | |
388 condition = "feature-not-implemented" | |
389 client.sendError(iq_elt, condition) | |
390 return | |
391 | |
392 iq_result_elt = xmlstream.toResponse(iq_elt, "result") | |
393 slot_elt = iq_result_elt.addElement((NS_HTTP_UPLOAD, 'slot')) | |
394 put_elt = slot_elt.addElement('put') | |
395 put_elt['url'] = slot.put | |
396 get_elt = slot_elt.addElement('get') | |
397 get_elt['url'] = slot.get | |
398 client.send(iq_result_elt) | |
399 | |
316 | 400 |
317 @implementer(iwokkel.IDisco) | 401 @implementer(iwokkel.IDisco) |
318 class XEP_0363_handler(XMPPHandler): | 402 class XEP_0363_handler(xmlstream.XMPPHandler): |
403 | |
404 def __init__(self, plugin_parent): | |
405 self.plugin_parent = plugin_parent | |
406 | |
407 def connectionInitialized(self): | |
408 if self.parent.is_component: | |
409 self.xmlstream.addObserver( | |
410 IQ_HTTP_UPLOAD_REQUEST, self.plugin_parent.onComponentRequest, | |
411 client=self.parent | |
412 ) | |
319 | 413 |
320 def getDiscoInfo(self, requestor, target, nodeIdentifier=""): | 414 def getDiscoInfo(self, requestor, target, nodeIdentifier=""): |
321 return [disco.DiscoFeature(NS_HTTP_UPLOAD)] | 415 return [disco.DiscoFeature(NS_HTTP_UPLOAD)] |
322 | 416 |
323 def getDiscoItems(self, requestor, target, nodeIdentifier=""): | 417 def getDiscoItems(self, requestor, target, nodeIdentifier=""): |