comparison sat/plugins/plugin_xep_0363.py @ 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 003b8b4b56a7
children ab2696e34d29
comparison
equal deleted inserted replaced
2865:9213c6dff48d 2866:8ce5748bfe97
48 C.PI_TYPE: "XEP", 48 C.PI_TYPE: "XEP",
49 C.PI_PROTOCOLS: ["XEP-0363"], 49 C.PI_PROTOCOLS: ["XEP-0363"],
50 C.PI_DEPENDENCIES: ["FILE", "UPLOAD"], 50 C.PI_DEPENDENCIES: ["FILE", "UPLOAD"],
51 C.PI_MAIN: "XEP_0363", 51 C.PI_MAIN: "XEP_0363",
52 C.PI_HANDLER: "yes", 52 C.PI_HANDLER: "yes",
53 C.PI_DESCRIPTION: _("""Implementation of HTTP File Upload"""), 53 C.PI_DESCRIPTION: _(u"""Implementation of HTTP File Upload"""),
54 } 54 }
55 55
56 NS_HTTP_UPLOAD = "urn:xmpp:http:upload" 56 NS_HTTP_UPLOAD = "urn:xmpp:http:upload:0"
57 57 ALLOWED_HEADERS = ('authorization', 'cookie', 'expires')
58 58
59 Slot = namedtuple("Slot", ["put", "get"]) 59
60 Slot = namedtuple("Slot", ["put", "get", "headers"])
60 61
61 62
62 @implementer(IOpenSSLClientConnectionCreator) 63 @implementer(IOpenSSLClientConnectionCreator)
63 class NoCheckConnectionCreator(object): 64 class NoCheckConnectionCreator(object):
64 def __init__(self, hostname, ctx): 65 def __init__(self, hostname, ctx):
74 @implementer(iweb.IPolicyForHTTPS) 75 @implementer(iweb.IPolicyForHTTPS)
75 class NoCheckContextFactory(ssl.ClientContextFactory): 76 class NoCheckContextFactory(ssl.ClientContextFactory):
76 """Context factory which doesn't do TLS certificate check 77 """Context factory which doesn't do TLS certificate check
77 78
78 /!\\ it's obvisously a security flaw to use this class, 79 /!\\ 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 and it should be used only with explicite agreement from the end used
80 """ 81 """
81 82
82 def creatorForNetloc(self, hostname, port): 83 def creatorForNetloc(self, hostname, port):
83 log.warning( 84 log.warning(
84 u"TLS check disabled for {host} on port {port}".format( 85 u"TLS check disabled for {host} on port {port}".format(
137 if entity is None: 138 if entity is None:
138 raise failure.Failure(exceptions.NotFound(u"No HTTP upload entity found")) 139 raise failure.Failure(exceptions.NotFound(u"No HTTP upload entity found"))
139 140
140 defer.returnValue(entity) 141 defer.returnValue(entity)
141 142
142 def _fileHTTPUpload( 143 def _fileHTTPUpload(self, filepath, filename="", upload_jid="",
143 self, 144 ignore_tls_errors=False, profile=C.PROF_KEY_NONE):
144 filepath,
145 filename="",
146 upload_jid="",
147 ignore_tls_errors=False,
148 profile=C.PROF_KEY_NONE,
149 ):
150 assert os.path.isabs(filepath) and os.path.isfile(filepath) 145 assert os.path.isabs(filepath) and os.path.isfile(filepath)
151 progress_id_d, __ = self.fileHTTPUpload( 146 progress_id_d, __ = self.fileHTTPUpload(
152 filepath, 147 filepath,
153 filename or None, 148 filename or None,
154 jid.JID(upload_jid) if upload_jid else None, 149 jid.JID(upload_jid) if upload_jid else None,
155 {"ignore_tls_errors": ignore_tls_errors}, 150 {"ignore_tls_errors": ignore_tls_errors},
156 profile, 151 profile,
157 ) 152 )
158 return progress_id_d 153 return progress_id_d
159 154
160 def fileHTTPUpload( 155 def fileHTTPUpload(self, filepath, filename=None, upload_jid=None, options=None,
161 self, 156 profile=C.PROF_KEY_NONE):
162 filepath, 157 """Upload a file through HTTP
163 filename=None,
164 upload_jid=None,
165 options=None,
166 profile=C.PROF_KEY_NONE,
167 ):
168 """upload a file through HTTP
169 158
170 @param filepath(str): absolute path of the file 159 @param filepath(str): absolute path of the file
171 @param filename(None, unicode): name to use for the upload 160 @param filename(None, unicode): name to use for the upload
172 None to use basename of the path 161 None to use basename of the path
173 @param upload_jid(jid.JID, None): upload capable entity jid, 162 @param upload_jid(jid.JID, None): upload capable entity jid,
174 or None to use autodetected, if possible 163 or None to use autodetected, if possible
175 @param options(dict): options where key can be: 164 @param options(dict): options where key can be:
176 - ignore_tls_errors(bool): if True, SSL certificate will not be checked 165 - ignore_tls_errors(bool): if True, SSL certificate will not be checked
177 @param profile: %(doc_profile)s 166 @param profile: %(doc_profile)s
178 @return (D(tuple[D(unicode), D(unicode)])): progress id and Deferred which fire download URL 167 @return (D(tuple[D(unicode), D(unicode)])): progress id and Deferred which fire
168 download URL
179 """ 169 """
180 if options is None: 170 if options is None:
181 options = {} 171 options = {}
182 ignore_tls_errors = options.get("ignore_tls_errors", False) 172 ignore_tls_errors = options.get("ignore_tls_errors", False)
183 client = self.host.getClient(profile) 173 client = self.host.getClient(profile)
199 """an error happened while trying to get slot""" 189 """an error happened while trying to get slot"""
200 log.warning(u"Can't get upload slot: {reason}".format(reason=fail.value)) 190 log.warning(u"Can't get upload slot: {reason}".format(reason=fail.value))
201 progress_id_d.errback(fail) 191 progress_id_d.errback(fail)
202 download_d.errback(fail) 192 download_d.errback(fail)
203 193
204 def _getSlotCb( 194 def _getSlotCb(self, slot, client, progress_id_d, download_d, path, size,
205 self, slot, client, progress_id_d, download_d, path, size, ignore_tls_errors=False 195 ignore_tls_errors=False):
206 ):
207 """Called when slot is received, try to do the upload 196 """Called when slot is received, try to do the upload
208 197
209 @param slot(Slot): slot instance with the get and put urls 198 @param slot(Slot): slot instance with the get and put urls
210 @param progress_id_d(defer.Deferred): Deferred to call when progress_id is known 199 @param progress_id_d(defer.Deferred): Deferred to call when progress_id is known
211 @param progress_id_d(defer.Deferred): Deferred to call with URL when upload is done 200 @param progress_id_d(defer.Deferred): Deferred to call with URL when upload is
201 done
212 @param path(str): path to the file to upload 202 @param path(str): path to the file to upload
213 @param size(int): size of the file to upload 203 @param size(int): size of the file to upload
214 @param ignore_tls_errors(bool): ignore TLS certificate is True 204 @param ignore_tls_errors(bool): ignore TLS certificate is True
215 @return (tuple 205 @return (tuple
216 """ 206 """
222 file_producer = http_client.FileBodyProducer(sat_file) 212 file_producer = http_client.FileBodyProducer(sat_file)
223 if ignore_tls_errors: 213 if ignore_tls_errors:
224 agent = http_client.Agent(reactor, NoCheckContextFactory()) 214 agent = http_client.Agent(reactor, NoCheckContextFactory())
225 else: 215 else:
226 agent = http_client.Agent(reactor) 216 agent = http_client.Agent(reactor)
217
218 headers = {"User-Agent": [C.APP_NAME.encode("utf-8")]}
219 for name, value in slot.headers:
220 name = name.encode('utf-8')
221 value = value.encode('utf-8')
222 headers[name] = value
223
227 d = agent.request( 224 d = agent.request(
228 "PUT", 225 "PUT",
229 slot.put.encode("utf-8"), 226 slot.put.encode("utf-8"),
230 http_headers.Headers({"User-Agent": [C.APP_NAME.encode("utf-8")]}), 227 http_headers.Headers(headers),
231 file_producer, 228 file_producer,
232 ) 229 )
233 d.addCallbacks( 230 d.addCallbacks(
234 self._uploadCb, 231 self._uploadCb,
235 self._uploadEb, 232 self._uploadEb,
257 should be closed, be is needed to send the progressError signal 254 should be closed, be is needed to send the progressError signal
258 """ 255 """
259 download_d.errback(fail) 256 download_d.errback(fail)
260 try: 257 try:
261 wrapped_fail = fail.value.reasons[0] 258 wrapped_fail = fail.value.reasons[0]
262 except (AttributeError, IndexError): 259 except (AttributeError, IndexError) as e:
260 log.warning(_(u"upload failed: {reason}").format(reason=e))
263 sat_file.progressError(unicode(fail)) 261 sat_file.progressError(unicode(fail))
264 raise fail 262 raise fail
265 else: 263 else:
266 if wrapped_fail.check(SSL.Error): 264 if wrapped_fail.check(SSL.Error):
267 msg = u"TLS validation error, can't connect to HTTPS server" 265 msg = u"TLS validation error, can't connect to HTTPS server"
268 log.warning(msg + ": " + unicode(wrapped_fail.value)) 266 else:
269 sat_file.progressError(msg) 267 msg = u"can't upload file"
268 log.warning(msg + ": " + unicode(wrapped_fail.value))
269 sat_file.progressError(msg)
270 270
271 def _gotSlot(self, iq_elt, client): 271 def _gotSlot(self, iq_elt, client):
272 """Slot have been received 272 """Slot have been received
273 273
274 This method convert the iq_elt result to a Slot instance 274 This method convert the iq_elt result to a Slot instance
275 @param iq_elt(domish.Element): <IQ/> result as specified in XEP-0363 275 @param iq_elt(domish.Element): <IQ/> result as specified in XEP-0363
276 """ 276 """
277 try: 277 try:
278 slot_elt = iq_elt.elements(NS_HTTP_UPLOAD, "slot").next() 278 slot_elt = iq_elt.elements(NS_HTTP_UPLOAD, "slot").next()
279 put_url = unicode(slot_elt.elements(NS_HTTP_UPLOAD, "put").next()) 279 put_elt = slot_elt.elements(NS_HTTP_UPLOAD, "put").next()
280 get_url = unicode(slot_elt.elements(NS_HTTP_UPLOAD, "get").next()) 280 put_url = put_elt['url']
281 except StopIteration: 281 get_elt = slot_elt.elements(NS_HTTP_UPLOAD, "get").next()
282 get_url = get_elt['url']
283 except (StopIteration, KeyError):
282 raise exceptions.DataError(u"Incorrect stanza received from server") 284 raise exceptions.DataError(u"Incorrect stanza received from server")
283 slot = Slot(put=put_url, get=get_url) 285 headers = []
286 for header_elt in put_elt.elements(NS_HTTP_UPLOAD, "header"):
287 try:
288 name = header_elt["name"]
289 value = unicode(header_elt)
290 except KeyError:
291 log.warning(_(u"Invalid header element: {xml}").format(
292 iq_elt.toXml()))
293 continue
294 name = name.replace('\n', '')
295 value = value.replace('\n', '')
296 if name.lower() not in ALLOWED_HEADERS:
297 log.warning(_(u'Ignoring unauthorised header "{name}": {xml}')
298 .format(name=name, xml = iq_elt.toXml()))
299 continue
300 headers.append((name, value))
301
302 slot = Slot(put=put_url, get=get_url, headers=tuple(headers))
284 return slot 303 return slot
285 304
286 def _getSlot( 305 def _getSlot(self, filename, size, content_type, upload_jid,
287 self, filename, size, content_type, upload_jid, profile_key=C.PROF_KEY_NONE 306 profile_key=C.PROF_KEY_NONE):
288 ): 307 """Get an upload slot
289 """Get a upload slot
290 308
291 This method can be used when uploading is done by the frontend 309 This method can be used when uploading is done by the frontend
292 @param filename(unicode): name of the file to upload 310 @param filename(unicode): name of the file to upload
293 @param size(int): size of the file (must be non null) 311 @param size(int): size of the file (must be non null)
294 @param upload_jid(jid.JID(), None, ''): HTTP upload capable entity 312 @param upload_jid(jid.JID(), None, ''): HTTP upload capable entity
337 ) 355 )
338 356
339 iq_elt = client.IQ("get") 357 iq_elt = client.IQ("get")
340 iq_elt["to"] = upload_jid.full() 358 iq_elt["to"] = upload_jid.full()
341 request_elt = iq_elt.addElement((NS_HTTP_UPLOAD, "request")) 359 request_elt = iq_elt.addElement((NS_HTTP_UPLOAD, "request"))
342 request_elt.addElement("filename", content=filename) 360 request_elt["filename"] = filename
343 request_elt.addElement("size", content=unicode(size)) 361 request_elt["size"] = unicode(size)
344 if content_type is not None: 362 if content_type is not None:
345 request_elt.addElement("content-type", content=content_type) 363 request_elt["content-type"] = content_type
346 364
347 d = iq_elt.send() 365 d = iq_elt.send()
348 d.addCallback(self._gotSlot, client) 366 d.addCallback(self._gotSlot, client)
349 367
350 return d 368 return d