Mercurial > libervia-backend
comparison src/plugins/plugin_xep_0363.py @ 1640:d470affbe65c
plugin XEP-0363, upload: File upload (through HTTP upload only for now):
- HTTP upload implementation
- if the upload link is HTTPS, certificate is checked (can be disabled on demand)
- file can be uploaded directly, or a put/get slot can be requested without actual upload.
The later is mainly useful for distant frontends like Libervia
- upload plugin manage different upload methods, in a similar way as file plugin
- download url is sent in progressFinished metadata on successful upload
author | Goffi <goffi@goffi.org> |
---|---|
date | Sun, 22 Nov 2015 17:33:30 +0100 |
parents | |
children | d17772b0fe22 |
comparison
equal
deleted
inserted
replaced
1639:baac2e120600 | 1640:d470affbe65c |
---|---|
1 #!/usr/bin/python | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # SAT plugin for Jingle File Transfer (XEP-0363) | |
5 # Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015 Jérôme Poisson (goffi@goffi.org) | |
6 | |
7 # This program is free software: you can redistribute it and/or modify | |
8 # it under the terms of the GNU Affero General Public License as published by | |
9 # the Free Software Foundation, either version 3 of the License, or | |
10 # (at your option) any later version. | |
11 | |
12 # This program is distributed in the hope that it will be useful, | |
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 # GNU Affero General Public License for more details. | |
16 | |
17 # You should have received a copy of the GNU Affero General Public License | |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
19 | |
20 from sat.core.i18n import _ | |
21 from sat.core.constants import Const as C | |
22 from sat.core.log import getLogger | |
23 log = getLogger(__name__) | |
24 from sat.core import exceptions | |
25 from wokkel import disco, iwokkel | |
26 from zope.interface import implements | |
27 from twisted.words.protocols.jabber import jid | |
28 from twisted.words.protocols.jabber.xmlstream import XMPPHandler | |
29 from twisted.internet import reactor | |
30 from twisted.internet import defer | |
31 from twisted.internet import ssl | |
32 from twisted.internet.interfaces import IOpenSSLClientConnectionCreator | |
33 from twisted.web import client as http_client | |
34 from twisted.web import http_headers | |
35 from twisted.web import iweb | |
36 from twisted.python import failure | |
37 from collections import namedtuple | |
38 from zope.interface import implementer | |
39 from OpenSSL import SSL | |
40 import os.path | |
41 import mimetypes | |
42 | |
43 | |
44 PLUGIN_INFO = { | |
45 "name": "HTTP File Upload", | |
46 "import_name": "XEP-0363", | |
47 "type": "XEP", | |
48 "protocols": ["XEP-0363"], | |
49 "dependencies": ["FILE", "UPLOAD"], | |
50 "main": "XEP_0363", | |
51 "handler": "yes", | |
52 "description": _("""Implementation of HTTP File Upload""") | |
53 } | |
54 | |
55 NS_HTTP_UPLOAD = 'urn:xmpp:http:upload' | |
56 | |
57 | |
58 Slot = namedtuple('Slot', ['put', 'get']) | |
59 | |
60 | |
61 @implementer(IOpenSSLClientConnectionCreator) | |
62 class NoCheckConnectionCreator(object): | |
63 | |
64 def __init__(self, hostname, ctx): | |
65 self._ctx = ctx | |
66 | |
67 def clientConnectionForTLS(self, tlsProtocol): | |
68 context = self._ctx | |
69 connection = SSL.Connection(context, None) | |
70 connection.set_app_data(tlsProtocol) | |
71 return connection | |
72 | |
73 | |
74 @implementer(iweb.IPolicyForHTTPS) | |
75 class NoCheckContextFactory(ssl.ClientContextFactory): | |
76 """Context factory which doesn't do TLS certificate check | |
77 | |
78 /!\\ it's obvisously a security flaw to use this class, | |
79 and it should be used only wiht explicite agreement from the end used | |
80 """ | |
81 | |
82 def creatorForNetloc(self, hostname, port): | |
83 log.warning(u"TLS check disabled for {host} on port {port}".format(host=hostname, port=port)) | |
84 certificateOptions = ssl.CertificateOptions(trustRoot=None) | |
85 return NoCheckConnectionCreator(hostname, certificateOptions.getContext()) | |
86 | |
87 | |
88 class XEP_0363(object): | |
89 | |
90 def __init__(self, host): | |
91 log.info(_("plugin HTTP File Upload initialization")) | |
92 self.host = host | |
93 host.bridge.addMethod("fileHTTPUpload", ".plugin", in_sign='sssbs', out_sign='', method=self._fileHTTPUpload) | |
94 host.bridge.addMethod("fileHTTPUploadGetSlot", ".plugin", in_sign='sisss', out_sign='(ss)', method=self._getSlot, async=True) | |
95 host.plugins['UPLOAD'].register(u"HTTP Upload", self.getHTTPUploadEntity, self.fileHTTPUpload) | |
96 | |
97 def getHandler(self, profile): | |
98 return XEP_0363_handler() | |
99 | |
100 @defer.inlineCallbacks | |
101 def getHTTPUploadEntity(self, upload_jid=None, profile=C.PROF_KEY_NONE): | |
102 """Get HTTP upload capable entity | |
103 | |
104 upload_jid is checked, then its components | |
105 @param upload_jid(None, jid.JID): entity to check | |
106 @return(D(jid.JID)): first HTTP upload capable entity | |
107 @raise exceptions.NotFound: no entity found | |
108 """ | |
109 client = self.host.getClient(profile) | |
110 try: | |
111 entity = client.http_upload_service | |
112 except AttributeError: | |
113 found_entities = yield self.host.findFeaturesSet((NS_HTTP_UPLOAD,), profile=client.profile) | |
114 try: | |
115 entity = client.http_upload_service = iter(found_entities).next() | |
116 except StopIteration: | |
117 entity = client.http_upload_service = None | |
118 | |
119 if entity is None: | |
120 raise failure.Failure(exceptions.NotFound(u'No HTTP upload entity found')) | |
121 | |
122 defer.returnValue(entity) | |
123 | |
124 def _fileHTTPUpload(self, filepath, filename='', upload_jid='', ignore_tls_errors=False, profile=C.PROF_KEY_NONE): | |
125 assert os.path.isabs(filepath) and os.path.isfile(filepath) | |
126 return self.fileHTTPUpload(filepath, filename or None, jid.JID(upload_jid) if upload_jid else None, {'ignore-tls-errors': ignore_tls_errors}, profile) | |
127 | |
128 def fileHTTPUpload(self, filepath, filename=None, upload_jid=None, options=None, profile=C.PROF_KEY_NONE): | |
129 """upload a file through HTTP | |
130 | |
131 @param filepath(str): absolute path of the file | |
132 @param filename(None, unicode): name to use for the upload | |
133 None to use basename of the path | |
134 @param upload_jid(jid.JID, None): upload capable entity jid, | |
135 or None to use autodetected, if possible | |
136 @param options(dict): options where key can be: | |
137 - ignore_tls_errors(bool): if True, SSL certificate will not be checked | |
138 @param profile: %(doc_profile)s | |
139 @return (D(unicode)): progress id | |
140 """ | |
141 if options is None: | |
142 options = {} | |
143 ignore_tls_errors = options.get('ignore-tls-errors', False) | |
144 client = self.host.getClient(profile) | |
145 filename = filename or os.path.basename(filepath) | |
146 size = os.path.getsize(filepath) | |
147 progress_id_d = defer.Deferred() | |
148 d = self.getSlot(client, filename, size, upload_jid=upload_jid) | |
149 d.addCallbacks(self._getSlotCb, self._getSlotEb, (client, progress_id_d, filepath, size, ignore_tls_errors), None, (client, progress_id_d)) | |
150 return progress_id_d | |
151 | |
152 def _getSlotEb(self, fail, client, progress_id_d): | |
153 """an error happened while trying to get slot""" | |
154 log.warning(u"Can't get upload slot: {reason}".format(reason=fail.value)) | |
155 progress_id_d.errback(fail) | |
156 | |
157 def _getSlotCb(self, slot, client, progress_id_d, path, size, ignore_tls_errors=False): | |
158 """Called when slot is received, try to do the upload | |
159 | |
160 @param slot(Slot): slot instance with the get and put urls | |
161 @param progress_id_d(defer.Deferred): Deferred to call when progress_id is known | |
162 @param path(str): path to the file to upload | |
163 @param size(int): size of the file to upload | |
164 @param ignore_tls_errors(bool): ignore TLS certificate is True | |
165 @return (tuple | |
166 """ | |
167 log.debug(u"Got upload slot: {}".format(slot)) | |
168 sat_file = self.host.plugins['FILE'].File(self.host, path, size=size, auto_end_signals=False, profile=client.profile) | |
169 progress_id_d.callback(sat_file.uid) | |
170 file_producer = http_client.FileBodyProducer(sat_file) | |
171 if ignore_tls_errors: | |
172 agent = http_client.Agent(reactor, NoCheckContextFactory()) | |
173 else: | |
174 agent = http_client.Agent(reactor) | |
175 d = agent.request('PUT', slot.put.encode('utf-8'), http_headers.Headers({'User-Agent': [C.APP_NAME.encode('utf-8')]}), file_producer) | |
176 d.addCallbacks(self._uploadCb, self._uploadEb, (sat_file, slot), None, (sat_file,)) | |
177 return d | |
178 | |
179 def _uploadCb(self, dummy, sat_file, slot): | |
180 """Called once file is successfully uploaded | |
181 | |
182 @param sat_file(SatFile): file used for the upload | |
183 should be closed, be is needed to send the progressFinished signal | |
184 @param slot(Slot): put/get urls | |
185 """ | |
186 log.info(u"HTTP upload finished") | |
187 sat_file.progressFinished({'url': slot.get}) | |
188 | |
189 def _uploadEb(self, fail, sat_file): | |
190 """Called on unsuccessful upload | |
191 | |
192 @param sat_file(SatFile): file used for the upload | |
193 should be closed, be is needed to send the progressError signal | |
194 """ | |
195 try: | |
196 wrapped_fail = fail.value.reasons[0] | |
197 except (AttributeError, IndexError): | |
198 sat_file.progressError(unicode(fail)) | |
199 raise fail | |
200 else: | |
201 if wrapped_fail.check(SSL.Error): | |
202 msg = u"TLS validation error, can't connect to HTTPS server" | |
203 log.warning(msg + ": " + unicode(wrapped_fail.value)) | |
204 sat_file.progressError(msg) | |
205 | |
206 def _gotSlot(self, iq_elt, client): | |
207 """Slot have been received | |
208 | |
209 This method convert the iq_elt result to a Slot instance | |
210 @param iq_elt(domish.Element): <IQ/> result as specified in XEP-0363 | |
211 """ | |
212 try: | |
213 slot_elt = iq_elt.elements(NS_HTTP_UPLOAD, 'slot').next() | |
214 put_url = unicode(slot_elt.elements(NS_HTTP_UPLOAD, 'put').next()) | |
215 get_url = unicode(slot_elt.elements(NS_HTTP_UPLOAD, 'get').next()) | |
216 except StopIteration: | |
217 raise exceptions.DataError(u"Incorrect stanza received from server") | |
218 slot = Slot(put=put_url, get=get_url) | |
219 return slot | |
220 | |
221 def _getSlot(self, filename, size, content_type, upload_jid, profile_key=C.PROF_KEY_NONE): | |
222 """Get a upload slot | |
223 | |
224 This method can be used when uploading is done by the frontend | |
225 @param filename(unicode): name of the file to upload | |
226 @param size(int): size of the file (must be non null) | |
227 @param upload_jid(jid.JID(), None, ''): HTTP upload capable entity | |
228 @param content_type(unicode, None): MIME type of the content | |
229 empty string or None to guess automatically | |
230 """ | |
231 filename.replace('/', '_') | |
232 client = self.host.getClient(profile_key) | |
233 return self.getSlot(client, filename, size, content_type or None, upload_jid or None) | |
234 | |
235 def getSlot(self, client, filename, size, content_type=None, upload_jid=None): | |
236 """Get a slot (i.e. download/upload links) | |
237 | |
238 @param filename(unicode): name to use for the upload | |
239 @param size(int): size of the file to upload (must be >0) | |
240 @param content_type(None, unicode): MIME type of the content | |
241 None to autodetect | |
242 @param upload_jid(jid.JID, None): HTTP upload capable upload_jid | |
243 or None to use the server component (if any) | |
244 @param client: %(doc_client)s | |
245 @return (Slot): the upload (put) and download (get) URLs | |
246 @raise exceptions.NotFound: no HTTP upload capable upload_jid has been found | |
247 """ | |
248 assert filename and size | |
249 if content_type is None: | |
250 # TODO: manage python magic for file guessing (in a dedicated plugin ?) | |
251 content_type = mimetypes.guess_type(filename, strict=False)[0] | |
252 | |
253 if upload_jid is None: | |
254 try: | |
255 upload_jid = client.http_upload_service | |
256 except AttributeError: | |
257 d = self.getHTTPUploadEntity(profile=client.profile) | |
258 d.addCallback(lambda found_entity: self.getSlot(client, filename, size, content_type, found_entity)) | |
259 return d | |
260 else: | |
261 if upload_jid is None: | |
262 raise failure.Failure(exceptions.NotFound(u'No HTTP upload entity found')) | |
263 | |
264 iq_elt = client.IQ('get') | |
265 iq_elt['to'] = upload_jid.full() | |
266 request_elt = iq_elt.addElement((NS_HTTP_UPLOAD, 'request')) | |
267 request_elt.addElement('filename', content=filename) | |
268 request_elt.addElement('size', content=unicode(size)) | |
269 if content_type is not None: | |
270 request_elt.addElement('content-type', content=content_type) | |
271 | |
272 d = iq_elt.send() | |
273 d.addCallback(self._gotSlot, client) | |
274 | |
275 return d | |
276 | |
277 | |
278 class XEP_0363_handler(XMPPHandler): | |
279 implements(iwokkel.IDisco) | |
280 | |
281 def getDiscoInfo(self, requestor, target, nodeIdentifier=''): | |
282 return [disco.DiscoFeature(NS_HTTP_UPLOAD)] | |
283 | |
284 def getDiscoItems(self, requestor, target, nodeIdentifier=''): | |
285 return [] |