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)]