Mercurial > libervia-backend
comparison sat/plugins/plugin_xep_0363.py @ 2562:26edcf3a30eb
core, setup: huge cleaning:
- moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention
- move twisted directory to root
- removed all hacks from setup.py, and added missing dependencies, it is now clean
- use https URL for website in setup.py
- removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed
- renamed sat.sh to sat and fixed its installation
- added python_requires to specify Python version needed
- replaced glib2reactor which use deprecated code by gtk3reactor
sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 02 Apr 2018 19:44:50 +0200 |
parents | src/plugins/plugin_xep_0363.py@7ad5f2c4e34a |
children | 56f94936df1e |
comparison
equal
deleted
inserted
replaced
2561:bd30dc3ffe5a | 2562:26edcf3a30eb |
---|---|
1 #!/usr/bin/env python2 | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # SAT plugin for HTTP File Upload (XEP-0363) | |
5 # Copyright (C) 2009-2018 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 C.PI_NAME: "HTTP File Upload", | |
46 C.PI_IMPORT_NAME: "XEP-0363", | |
47 C.PI_TYPE: "XEP", | |
48 C.PI_PROTOCOLS: ["XEP-0363"], | |
49 C.PI_DEPENDENCIES: ["FILE", "UPLOAD"], | |
50 C.PI_MAIN: "XEP_0363", | |
51 C.PI_HANDLER: "yes", | |
52 C.PI_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, client): | |
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(client, (NS_HTTP_UPLOAD,)) | |
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 progress_id_d, dummy = self.fileHTTPUpload(filepath, filename or None, jid.JID(upload_jid) if upload_jid else None, {'ignore_tls_errors': ignore_tls_errors}, profile) | |
127 return progress_id_d | |
128 | |
129 def fileHTTPUpload(self, filepath, filename=None, upload_jid=None, options=None, profile=C.PROF_KEY_NONE): | |
130 """upload a file through HTTP | |
131 | |
132 @param filepath(str): absolute path of the file | |
133 @param filename(None, unicode): name to use for the upload | |
134 None to use basename of the path | |
135 @param upload_jid(jid.JID, None): upload capable entity jid, | |
136 or None to use autodetected, if possible | |
137 @param options(dict): options where key can be: | |
138 - ignore_tls_errors(bool): if True, SSL certificate will not be checked | |
139 @param profile: %(doc_profile)s | |
140 @return (D(tuple[D(unicode), D(unicode)])): progress id and Deferred which fire download URL | |
141 """ | |
142 if options is None: | |
143 options = {} | |
144 ignore_tls_errors = options.get('ignore_tls_errors', False) | |
145 client = self.host.getClient(profile) | |
146 filename = filename or os.path.basename(filepath) | |
147 size = os.path.getsize(filepath) | |
148 progress_id_d = defer.Deferred() | |
149 download_d = defer.Deferred() | |
150 d = self.getSlot(client, filename, size, upload_jid=upload_jid) | |
151 d.addCallbacks(self._getSlotCb, self._getSlotEb, (client, progress_id_d, download_d, filepath, size, ignore_tls_errors), None, (client, progress_id_d, download_d)) | |
152 return progress_id_d, download_d | |
153 | |
154 def _getSlotEb(self, fail, client, progress_id_d, download_d): | |
155 """an error happened while trying to get slot""" | |
156 log.warning(u"Can't get upload slot: {reason}".format(reason=fail.value)) | |
157 progress_id_d.errback(fail) | |
158 download_d.errback(fail) | |
159 | |
160 def _getSlotCb(self, slot, client, progress_id_d, download_d, path, size, ignore_tls_errors=False): | |
161 """Called when slot is received, try to do the upload | |
162 | |
163 @param slot(Slot): slot instance with the get and put urls | |
164 @param progress_id_d(defer.Deferred): Deferred to call when progress_id is known | |
165 @param progress_id_d(defer.Deferred): Deferred to call with URL when upload is done | |
166 @param path(str): path to the file to upload | |
167 @param size(int): size of the file to upload | |
168 @param ignore_tls_errors(bool): ignore TLS certificate is True | |
169 @return (tuple | |
170 """ | |
171 log.debug(u"Got upload slot: {}".format(slot)) | |
172 sat_file = self.host.plugins['FILE'].File(self.host, client, path, size=size, auto_end_signals=False) | |
173 progress_id_d.callback(sat_file.uid) | |
174 file_producer = http_client.FileBodyProducer(sat_file) | |
175 if ignore_tls_errors: | |
176 agent = http_client.Agent(reactor, NoCheckContextFactory()) | |
177 else: | |
178 agent = http_client.Agent(reactor) | |
179 d = agent.request('PUT', slot.put.encode('utf-8'), http_headers.Headers({'User-Agent': [C.APP_NAME.encode('utf-8')]}), file_producer) | |
180 d.addCallbacks(self._uploadCb, self._uploadEb, (sat_file, slot, download_d), None, (sat_file, download_d)) | |
181 return d | |
182 | |
183 def _uploadCb(self, dummy, sat_file, slot, download_d): | |
184 """Called once file is successfully uploaded | |
185 | |
186 @param sat_file(SatFile): file used for the upload | |
187 should be closed, be is needed to send the progressFinished signal | |
188 @param slot(Slot): put/get urls | |
189 """ | |
190 log.info(u"HTTP upload finished") | |
191 sat_file.progressFinished({'url': slot.get}) | |
192 download_d.callback(slot.get) | |
193 | |
194 def _uploadEb(self, fail, sat_file, download_d): | |
195 """Called on unsuccessful upload | |
196 | |
197 @param sat_file(SatFile): file used for the upload | |
198 should be closed, be is needed to send the progressError signal | |
199 """ | |
200 download_d.errback(fail) | |
201 try: | |
202 wrapped_fail = fail.value.reasons[0] | |
203 except (AttributeError, IndexError): | |
204 sat_file.progressError(unicode(fail)) | |
205 raise fail | |
206 else: | |
207 if wrapped_fail.check(SSL.Error): | |
208 msg = u"TLS validation error, can't connect to HTTPS server" | |
209 log.warning(msg + ": " + unicode(wrapped_fail.value)) | |
210 sat_file.progressError(msg) | |
211 | |
212 def _gotSlot(self, iq_elt, client): | |
213 """Slot have been received | |
214 | |
215 This method convert the iq_elt result to a Slot instance | |
216 @param iq_elt(domish.Element): <IQ/> result as specified in XEP-0363 | |
217 """ | |
218 try: | |
219 slot_elt = iq_elt.elements(NS_HTTP_UPLOAD, 'slot').next() | |
220 put_url = unicode(slot_elt.elements(NS_HTTP_UPLOAD, 'put').next()) | |
221 get_url = unicode(slot_elt.elements(NS_HTTP_UPLOAD, 'get').next()) | |
222 except StopIteration: | |
223 raise exceptions.DataError(u"Incorrect stanza received from server") | |
224 slot = Slot(put=put_url, get=get_url) | |
225 return slot | |
226 | |
227 def _getSlot(self, filename, size, content_type, upload_jid, profile_key=C.PROF_KEY_NONE): | |
228 """Get a upload slot | |
229 | |
230 This method can be used when uploading is done by the frontend | |
231 @param filename(unicode): name of the file to upload | |
232 @param size(int): size of the file (must be non null) | |
233 @param upload_jid(jid.JID(), None, ''): HTTP upload capable entity | |
234 @param content_type(unicode, None): MIME type of the content | |
235 empty string or None to guess automatically | |
236 """ | |
237 filename = filename.replace('/', '_') | |
238 client = self.host.getClient(profile_key) | |
239 return self.getSlot(client, filename, size, content_type or None, upload_jid or None) | |
240 | |
241 def getSlot(self, client, filename, size, content_type=None, upload_jid=None): | |
242 """Get a slot (i.e. download/upload links) | |
243 | |
244 @param filename(unicode): name to use for the upload | |
245 @param size(int): size of the file to upload (must be >0) | |
246 @param content_type(None, unicode): MIME type of the content | |
247 None to autodetect | |
248 @param upload_jid(jid.JID, None): HTTP upload capable upload_jid | |
249 or None to use the server component (if any) | |
250 @param client: %(doc_client)s | |
251 @return (Slot): the upload (put) and download (get) URLs | |
252 @raise exceptions.NotFound: no HTTP upload capable upload_jid has been found | |
253 """ | |
254 assert filename and size | |
255 if content_type is None: | |
256 # TODO: manage python magic for file guessing (in a dedicated plugin ?) | |
257 content_type = mimetypes.guess_type(filename, strict=False)[0] | |
258 | |
259 if upload_jid is None: | |
260 try: | |
261 upload_jid = client.http_upload_service | |
262 except AttributeError: | |
263 d = self.getHTTPUploadEntity(profile=client.profile) | |
264 d.addCallback(lambda found_entity: self.getSlot(client, filename, size, content_type, found_entity)) | |
265 return d | |
266 else: | |
267 if upload_jid is None: | |
268 raise failure.Failure(exceptions.NotFound(u'No HTTP upload entity found')) | |
269 | |
270 iq_elt = client.IQ('get') | |
271 iq_elt['to'] = upload_jid.full() | |
272 request_elt = iq_elt.addElement((NS_HTTP_UPLOAD, 'request')) | |
273 request_elt.addElement('filename', content=filename) | |
274 request_elt.addElement('size', content=unicode(size)) | |
275 if content_type is not None: | |
276 request_elt.addElement('content-type', content=content_type) | |
277 | |
278 d = iq_elt.send() | |
279 d.addCallback(self._gotSlot, client) | |
280 | |
281 return d | |
282 | |
283 | |
284 class XEP_0363_handler(XMPPHandler): | |
285 implements(iwokkel.IDisco) | |
286 | |
287 def getDiscoInfo(self, requestor, target, nodeIdentifier=''): | |
288 return [disco.DiscoFeature(NS_HTTP_UPLOAD)] | |
289 | |
290 def getDiscoItems(self, requestor, target, nodeIdentifier=''): | |
291 return [] |