Mercurial > libervia-backend
diff 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 |
line wrap: on
line diff
--- a/sat/plugins/plugin_xep_0363.py Fri May 29 21:50:49 2020 +0200 +++ b/sat/plugins/plugin_xep_0363.py Fri May 29 21:55:45 2020 +0200 @@ -18,16 +18,19 @@ import os.path import mimetypes +from typing import NamedTuple, Callable, Optional from dataclasses import dataclass +from urllib import parse from wokkel import disco, iwokkel from zope.interface import implementer -from twisted.words.protocols.jabber import jid -from twisted.words.protocols.jabber.xmlstream import XMPPHandler +from twisted.words.protocols.jabber import jid, xmlstream, error +from twisted.words.xish import domish from twisted.internet import reactor from twisted.internet import defer from twisted.web import client as http_client from twisted.web import http_headers from sat.core.i18n import _ +from sat.core.xmpp import SatXMPPComponent from sat.core.constants import Const as C from sat.core.log import getLogger from sat.core import exceptions @@ -40,6 +43,7 @@ C.PI_NAME: "HTTP File Upload", C.PI_IMPORT_NAME: "XEP-0363", C.PI_TYPE: "XEP", + C.PI_MODES: C.PLUG_MODE_BOTH, C.PI_PROTOCOLS: ["XEP-0363"], C.PI_DEPENDENCIES: ["FILE", "UPLOAD"], C.PI_MAIN: "XEP_0363", @@ -48,6 +52,7 @@ } NS_HTTP_UPLOAD = "urn:xmpp:http:upload:0" +IQ_HTTP_UPLOAD_REQUEST = C.IQ_GET + '/request[@xmlns="' + NS_HTTP_UPLOAD + '"]' ALLOWED_HEADERS = ('authorization', 'cookie', 'expires') @@ -59,7 +64,21 @@ headers: list -class XEP_0363(object): +class UploadRequest(NamedTuple): + from_: jid.JID + filename: str + size: int + content_type: Optional[str] + + +class RequestHandler(NamedTuple): + callback: Callable[[SatXMPPComponent, UploadRequest], Optional[Slot]] + priority: int + + +class XEP_0363: + Slot=Slot + def __init__(self, host): log.info(_("plugin HTTP File Upload initialization")) self.host = host @@ -81,9 +100,26 @@ host.plugins["UPLOAD"].register( "HTTP Upload", self.getHTTPUploadEntity, self.fileHTTPUpload ) + # list of callbacks used when a request is done to a component + self.handlers = [] def getHandler(self, client): - return XEP_0363_handler() + return XEP_0363_handler(self) + + def registerHandler(self, callback, priority=0): + """Register a request handler + + @param callack: method to call when a request is done + the callback must return a Slot if the request is handled, + otherwise, other callbacks will be tried. + If the callback raises a StanzaError, its condition will be used if no other + callback can handle the request. + @param priority: handlers with higher priorities will be called first + """ + assert callback not in self.handlers + req_handler = RequestHandler(callback, priority) + self.handlers.append(req_handler) + self.handlers.sort(key=lambda handler: handler.priority, reverse=True) async def getHTTPUploadEntity(self, client, upload_jid=None): """Get HTTP upload capable entity @@ -313,9 +349,67 @@ return Slot(put=put_url, get=get_url, headers=headers) + # component + + def onComponentRequest(self, iq_elt, client): + iq_elt.handled=True + try: + request_elt = next(iq_elt.elements(NS_HTTP_UPLOAD, "request")) + request = UploadRequest( + from_=jid.JID(iq_elt['from']), + filename=parse.quote(request_elt['filename'].replace('/', '_'), safe=''), + size=int(request_elt['size']), + content_type=request_elt.getAttribute('content-type') + ) + except (StopIteration, KeyError, ValueError): + client.sendError(iq_elt, "bad-request") + return + + err = None + + for handler in self.handlers: + try: + slot = handler.callback(client, request) + except error.StanzaError as e: + log.warning(f"a stanza error has been raised while processing HTTP Upload of request: {e}") + if err is None: + # we keep the first error to return its condition later, + # if no other callback handle the request + err = e + if slot: + break + else: + log.warning( + _("no service can handle HTTP Upload request: {elt}") + .format(elt=iq_elt.toXml())) + if err is not None: + condition = err.condition + else: + condition = "feature-not-implemented" + client.sendError(iq_elt, condition) + return + + iq_result_elt = xmlstream.toResponse(iq_elt, "result") + slot_elt = iq_result_elt.addElement((NS_HTTP_UPLOAD, 'slot')) + put_elt = slot_elt.addElement('put') + put_elt['url'] = slot.put + get_elt = slot_elt.addElement('get') + get_elt['url'] = slot.get + client.send(iq_result_elt) + @implementer(iwokkel.IDisco) -class XEP_0363_handler(XMPPHandler): +class XEP_0363_handler(xmlstream.XMPPHandler): + + def __init__(self, plugin_parent): + self.plugin_parent = plugin_parent + + def connectionInitialized(self): + if self.parent.is_component: + self.xmlstream.addObserver( + IQ_HTTP_UPLOAD_REQUEST, self.plugin_parent.onComponentRequest, + client=self.parent + ) def getDiscoInfo(self, requestor, target, nodeIdentifier=""): return [disco.DiscoFeature(NS_HTTP_UPLOAD)]