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