changeset 2866:8ce5748bfe97

plugin XEP-0363: updated to namespace "urn:xmpp:http:upload:0", handle headers
author Goffi <goffi@goffi.org>
date Fri, 22 Mar 2019 19:26:08 +0100
parents 9213c6dff48d
children 3cac3d050046
files sat/plugins/plugin_xep_0363.py sat/tools/stream.py
diffstat 2 files changed, 66 insertions(+), 54 deletions(-) [+]
line wrap: on
line diff
--- a/sat/plugins/plugin_xep_0363.py	Thu Mar 21 08:54:59 2019 +0100
+++ b/sat/plugins/plugin_xep_0363.py	Fri Mar 22 19:26:08 2019 +0100
@@ -50,13 +50,14 @@
     C.PI_DEPENDENCIES: ["FILE", "UPLOAD"],
     C.PI_MAIN: "XEP_0363",
     C.PI_HANDLER: "yes",
-    C.PI_DESCRIPTION: _("""Implementation of HTTP File Upload"""),
+    C.PI_DESCRIPTION: _(u"""Implementation of HTTP File Upload"""),
 }
 
-NS_HTTP_UPLOAD = "urn:xmpp:http:upload"
+NS_HTTP_UPLOAD = "urn:xmpp:http:upload:0"
+ALLOWED_HEADERS = ('authorization', 'cookie', 'expires')
 
 
-Slot = namedtuple("Slot", ["put", "get"])
+Slot = namedtuple("Slot", ["put", "get", "headers"])
 
 
 @implementer(IOpenSSLClientConnectionCreator)
@@ -76,7 +77,7 @@
     """Context factory which doesn't do TLS certificate check
 
     /!\\ it's obvisously a security flaw to use this class,
-    and it should be used only wiht explicite agreement from the end used
+    and it should be used only with explicite agreement from the end used
     """
 
     def creatorForNetloc(self, hostname, port):
@@ -139,14 +140,8 @@
 
         defer.returnValue(entity)
 
-    def _fileHTTPUpload(
-        self,
-        filepath,
-        filename="",
-        upload_jid="",
-        ignore_tls_errors=False,
-        profile=C.PROF_KEY_NONE,
-    ):
+    def _fileHTTPUpload(self, filepath, filename="", upload_jid="",
+                        ignore_tls_errors=False, profile=C.PROF_KEY_NONE):
         assert os.path.isabs(filepath) and os.path.isfile(filepath)
         progress_id_d, __ = self.fileHTTPUpload(
             filepath,
@@ -157,15 +152,9 @@
         )
         return progress_id_d
 
-    def fileHTTPUpload(
-        self,
-        filepath,
-        filename=None,
-        upload_jid=None,
-        options=None,
-        profile=C.PROF_KEY_NONE,
-    ):
-        """upload a file through HTTP
+    def fileHTTPUpload(self, filepath, filename=None, upload_jid=None, options=None,
+                       profile=C.PROF_KEY_NONE):
+        """Upload a file through HTTP
 
         @param filepath(str): absolute path of the file
         @param filename(None, unicode): name to use for the upload
@@ -175,7 +164,8 @@
         @param options(dict): options where key can be:
             - ignore_tls_errors(bool): if True, SSL certificate will not be checked
         @param profile: %(doc_profile)s
-        @return (D(tuple[D(unicode), D(unicode)])): progress id and Deferred which fire download URL
+        @return (D(tuple[D(unicode), D(unicode)])): progress id and Deferred which fire
+            download URL
         """
         if options is None:
             options = {}
@@ -201,14 +191,14 @@
         progress_id_d.errback(fail)
         download_d.errback(fail)
 
-    def _getSlotCb(
-        self, slot, client, progress_id_d, download_d, path, size, ignore_tls_errors=False
-    ):
+    def _getSlotCb(self, slot, client, progress_id_d, download_d, path, size,
+                   ignore_tls_errors=False):
         """Called when slot is received, try to do the upload
 
         @param slot(Slot): slot instance with the get and put urls
         @param progress_id_d(defer.Deferred): Deferred to call when progress_id is known
-        @param progress_id_d(defer.Deferred): Deferred to call with URL when upload is done
+        @param progress_id_d(defer.Deferred): Deferred to call with URL when upload is
+            done
         @param path(str): path to the file to upload
         @param size(int): size of the file to upload
         @param ignore_tls_errors(bool): ignore TLS certificate is True
@@ -224,10 +214,17 @@
             agent = http_client.Agent(reactor, NoCheckContextFactory())
         else:
             agent = http_client.Agent(reactor)
+
+        headers = {"User-Agent": [C.APP_NAME.encode("utf-8")]}
+        for name, value in slot.headers:
+            name = name.encode('utf-8')
+            value = value.encode('utf-8')
+            headers[name] = value
+
         d = agent.request(
             "PUT",
             slot.put.encode("utf-8"),
-            http_headers.Headers({"User-Agent": [C.APP_NAME.encode("utf-8")]}),
+            http_headers.Headers(headers),
             file_producer,
         )
         d.addCallbacks(
@@ -259,14 +256,17 @@
         download_d.errback(fail)
         try:
             wrapped_fail = fail.value.reasons[0]
-        except (AttributeError, IndexError):
+        except (AttributeError, IndexError) as e:
+            log.warning(_(u"upload failed: {reason}").format(reason=e))
             sat_file.progressError(unicode(fail))
             raise fail
         else:
             if wrapped_fail.check(SSL.Error):
                 msg = u"TLS validation error, can't connect to HTTPS server"
-                log.warning(msg + ": " + unicode(wrapped_fail.value))
-                sat_file.progressError(msg)
+            else:
+                msg = u"can't upload file"
+            log.warning(msg + ": " + unicode(wrapped_fail.value))
+            sat_file.progressError(msg)
 
     def _gotSlot(self, iq_elt, client):
         """Slot have been received
@@ -276,17 +276,35 @@
         """
         try:
             slot_elt = iq_elt.elements(NS_HTTP_UPLOAD, "slot").next()
-            put_url = unicode(slot_elt.elements(NS_HTTP_UPLOAD, "put").next())
-            get_url = unicode(slot_elt.elements(NS_HTTP_UPLOAD, "get").next())
-        except StopIteration:
+            put_elt = slot_elt.elements(NS_HTTP_UPLOAD, "put").next()
+            put_url = put_elt['url']
+            get_elt = slot_elt.elements(NS_HTTP_UPLOAD, "get").next()
+            get_url = get_elt['url']
+        except (StopIteration, KeyError):
             raise exceptions.DataError(u"Incorrect stanza received from server")
-        slot = Slot(put=put_url, get=get_url)
+        headers = []
+        for header_elt in put_elt.elements(NS_HTTP_UPLOAD, "header"):
+            try:
+                name = header_elt["name"]
+                value = unicode(header_elt)
+            except KeyError:
+                log.warning(_(u"Invalid header element: {xml}").format(
+                    iq_elt.toXml()))
+                continue
+            name = name.replace('\n', '')
+            value = value.replace('\n', '')
+            if name.lower() not in ALLOWED_HEADERS:
+                log.warning(_(u'Ignoring unauthorised header "{name}": {xml}')
+                    .format(name=name, xml = iq_elt.toXml()))
+                continue
+            headers.append((name, value))
+
+        slot = Slot(put=put_url, get=get_url, headers=tuple(headers))
         return slot
 
-    def _getSlot(
-        self, filename, size, content_type, upload_jid, profile_key=C.PROF_KEY_NONE
-    ):
-        """Get a upload slot
+    def _getSlot(self, filename, size, content_type, upload_jid,
+                 profile_key=C.PROF_KEY_NONE):
+        """Get an upload slot
 
         This method can be used when uploading is done by the frontend
         @param filename(unicode): name of the file to upload
@@ -339,10 +357,10 @@
         iq_elt = client.IQ("get")
         iq_elt["to"] = upload_jid.full()
         request_elt = iq_elt.addElement((NS_HTTP_UPLOAD, "request"))
-        request_elt.addElement("filename", content=filename)
-        request_elt.addElement("size", content=unicode(size))
+        request_elt["filename"] = filename
+        request_elt["size"] = unicode(size)
         if content_type is not None:
-            request_elt.addElement("content-type", content=content_type)
+            request_elt["content-type"] = content_type
 
         d = iq_elt.send()
         d.addCallback(self._gotSlot, client)
--- a/sat/tools/stream.py	Thu Mar 21 08:54:59 2019 +0100
+++ b/sat/tools/stream.py	Fri Mar 22 19:26:08 2019 +0100
@@ -37,6 +37,7 @@
 
         @return (D): deferred fired when stream is finished
         """
+        pass
 
 
 class SatFile(object):
@@ -44,17 +45,8 @@
 
     # TODO: manage "with" statement
 
-    def __init__(
-        self,
-        host,
-        client,
-        path,
-        mode="rb",
-        uid=None,
-        size=None,
-        data_cb=None,
-        auto_end_signals=True,
-    ):
+    def __init__(self, host, client, path, mode="rb", uid=None, size=None, data_cb=None,
+                 auto_end_signals=True):
         """
         @param host: %(doc_host)s
         @param path(str): path of the file to get
@@ -65,8 +57,10 @@
         @param size(None, int): size of the file (when known in advance)
         @param data_cb(None, callable): method to call on each data read/write
             mainly useful to do things like calculating hash
-        @param auto_end_signals(bool): if True, progressFinished and progressError signals are automatically sent
-            if False, you'll have to call self.progressFinished and self.progressError yourself
+        @param auto_end_signals(bool): if True, progressFinished and progressError signals
+            are automatically sent.
+            if False, you'll have to call self.progressFinished and self.progressError
+            yourself.
             progressStarted signal is always sent automatically
         """
         self.host = host