Mercurial > libervia-backend
comparison plugins/plugin_xep_0065.py @ 0:c4bc297b82f0
sat:
- first public release, initial commit
author | goffi@necton2 |
---|---|
date | Sat, 29 Aug 2009 13:34:59 +0200 |
parents | |
children | 4b05308d45f9 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:c4bc297b82f0 |
---|---|
1 #!/usr/bin/python | |
2 #-*- coding: utf-8 -*- | |
3 """ | |
4 SAT plugin for managing xep-0065 | |
5 | |
6 Copyright (C) | |
7 2002-2004 Dave Smith (dizzyd@jabber.org) | |
8 2007-2008 Fabio Forno (xmpp:ff@jabber.bluendo.com) | |
9 2009 Jérôme Poisson (goffi@goffi.org) | |
10 | |
11 This program is free software: you can redistribute it and/or modify | |
12 it under the terms of the GNU General Public License as published by | |
13 the Free Software Foundation, either version 3 of the License, or | |
14 (at your option) any later version. | |
15 | |
16 This program is distributed in the hope that it will be useful, | |
17 but WITHOUT ANY WARRANTY; without even the implied warranty of | |
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
19 GNU General Public License for more details. | |
20 | |
21 You should have received a copy of the GNU General Public License | |
22 along with this program. If not, see <http://www.gnu.org/licenses/>. | |
23 | |
24 -- | |
25 | |
26 This program is based on proxy65 (http://code.google.com/p/proxy65), | |
27 originaly written by David Smith and modified by Fabio Forno. | |
28 It is sublicensed under GPL v3 (or any later version) as allowed by the original | |
29 license. | |
30 | |
31 -- | |
32 | |
33 Here is a copy of the original license: | |
34 | |
35 Copyright (C) | |
36 2002-2004 Dave Smith (dizzyd@jabber.org) | |
37 2007-2008 Fabio Forno (xmpp:ff@jabber.bluendo.com) | |
38 | |
39 Permission is hereby granted, free of charge, to any person obtaining a copy | |
40 of this software and associated documentation files (the "Software"), to deal | |
41 in the Software without restriction, including without limitation the rights | |
42 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
43 copies of the Software, and to permit persons to whom the Software is | |
44 furnished to do so, subject to the following conditions: | |
45 | |
46 The above copyright notice and this permission notice shall be included in | |
47 all copies or substantial portions of the Software. | |
48 | |
49 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
50 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
51 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
52 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
53 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
54 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
55 THE SOFTWARE. | |
56 """ | |
57 | |
58 from logging import debug, info, error | |
59 from twisted.internet import protocol, reactor | |
60 from twisted.protocols.basic import FileSender | |
61 from twisted.words.xish import domish | |
62 import struct | |
63 from binascii import hexlify | |
64 import hashlib, pdb | |
65 | |
66 | |
67 PLUGIN_INFO = { | |
68 "name": "XEP 0065 Plugin", | |
69 "import_name": "XEP_0065", | |
70 "type": "XEP", | |
71 "main": "XEP_0065", | |
72 "description": """Implementation of SI File Transfert""" | |
73 } | |
74 | |
75 STATE_INITIAL = 0 | |
76 STATE_AUTH = 1 | |
77 STATE_REQUEST = 2 | |
78 STATE_READY = 3 | |
79 STATE_AUTH_USERPASS = 4 | |
80 STATE_TARGET_INITIAL = 5 | |
81 STATE_TARGET_AUTH = 6 | |
82 STATE_TARGET_REQUEST = 7 | |
83 STATE_TARGET_READY = 8 | |
84 STATE_LAST = 9 | |
85 | |
86 STATE_CONNECT_PENDING = STATE_LAST + 1 | |
87 | |
88 SOCKS5_VER = 0x05 | |
89 | |
90 ADDR_IPV4 = 0x01 | |
91 ADDR_DOMAINNAME = 0x03 | |
92 ADDR_IPV6 = 0x04 | |
93 | |
94 CMD_CONNECT = 0x01 | |
95 CMD_BIND = 0x02 | |
96 CMD_UDPASSOC = 0x03 | |
97 | |
98 AUTHMECH_ANON = 0x00 | |
99 AUTHMECH_USERPASS = 0x02 | |
100 AUTHMECH_INVALID = 0xFF | |
101 | |
102 REPLY_SUCCESS = 0x00 | |
103 REPLY_GENERAL_FAILUR = 0x01 | |
104 REPLY_CONN_NOT_ALLOWED = 0x02 | |
105 REPLY_NETWORK_UNREACHABLE = 0x03 | |
106 REPLY_HOST_UNREACHABLE = 0x04 | |
107 REPLY_CONN_REFUSED = 0x05 | |
108 REPLY_TTL_EXPIRED = 0x06 | |
109 REPLY_CMD_NOT_SUPPORTED = 0x07 | |
110 REPLY_ADDR_NOT_SUPPORTED = 0x08 | |
111 | |
112 | |
113 | |
114 | |
115 | |
116 class SOCKSv5(protocol.Protocol, FileSender): | |
117 def __init__(self): | |
118 debug("Protocol init") | |
119 self.state = STATE_INITIAL | |
120 self.buf = "" | |
121 self.supportedAuthMechs = [ AUTHMECH_ANON ] | |
122 self.supportedAddrs = [ ADDR_DOMAINNAME ] | |
123 self.enabledCommands = [ CMD_CONNECT ] | |
124 self.peersock = None | |
125 self.addressType = 0 | |
126 self.requestType = 0 | |
127 self.activeConns = {} | |
128 self.pendingConns = {} | |
129 self.transfered = 0 #nb of bytes already copied | |
130 | |
131 def _startNegotiation(self): | |
132 debug("_startNegotiation") | |
133 self.state = STATE_TARGET_AUTH | |
134 self.transport.write(struct.pack('!3B', SOCKS5_VER, 1, AUTHMECH_ANON)) | |
135 | |
136 def _parseNegotiation(self): | |
137 debug("_parseNegotiation") | |
138 try: | |
139 # Parse out data | |
140 ver, nmethod = struct.unpack('!BB', self.buf[:2]) | |
141 methods = struct.unpack('%dB' % nmethod, self.buf[2:nmethod+2]) | |
142 | |
143 # Ensure version is correct | |
144 if ver != 5: | |
145 self.transport.write(struct.pack('!BB', SOCKS5_VER, AUTHMECH_INVALID)) | |
146 self.transport.loseConnection() | |
147 return | |
148 | |
149 # Trim off front of the buffer | |
150 self.buf = self.buf[nmethod+2:] | |
151 | |
152 # Check for supported auth mechs | |
153 for m in self.supportedAuthMechs: | |
154 if m in methods: | |
155 # Update internal state, according to selected method | |
156 if m == AUTHMECH_ANON: | |
157 self.state = STATE_REQUEST | |
158 elif m == AUTHMECH_USERPASS: | |
159 self.state = STATE_AUTH_USERPASS | |
160 # Complete negotiation w/ this method | |
161 self.transport.write(struct.pack('!BB', SOCKS5_VER, m)) | |
162 return | |
163 | |
164 # No supported mechs found, notify client and close the connection | |
165 self.transport.write(struct.pack('!BB', SOCKS5_VER, AUTHMECH_INVALID)) | |
166 self.transport.loseConnection() | |
167 except struct.error: | |
168 pass | |
169 | |
170 def _parseUserPass(self): | |
171 debug("_parseUserPass") | |
172 try: | |
173 # Parse out data | |
174 ver, ulen = struct.unpack('BB', self.buf[:2]) | |
175 uname, = struct.unpack('%ds' % ulen, self.buf[2:ulen + 2]) | |
176 plen, = struct.unpack('B', self.buf[ulen + 2]) | |
177 password, = struct.unpack('%ds' % plen, self.buf[ulen + 3:ulen + 3 + plen]) | |
178 # Trim off fron of the buffer | |
179 self.buf = self.buf[3 + ulen + plen:] | |
180 # Fire event to authenticate user | |
181 if self.authenticateUserPass(uname, password): | |
182 # Signal success | |
183 self.state = STATE_REQUEST | |
184 self.transport.write(struct.pack('!BB', SOCKS5_VER, 0x00)) | |
185 else: | |
186 # Signal failure | |
187 self.transport.write(struct.pack('!BB', SOCKS5_VER, 0x01)) | |
188 self.transport.loseConnection() | |
189 except struct.error: | |
190 pass | |
191 | |
192 def sendErrorReply(self, errorcode): | |
193 debug("sendErrorReply") | |
194 # Any other address types are not supported | |
195 result = struct.pack('!BBBBIH', SOCKS5_VER, errorcode, 0, 1, 0, 0) | |
196 self.transport.write(result) | |
197 self.transport.loseConnection() | |
198 | |
199 def addConnection(self, address, connection): | |
200 info("Adding connection: %s, %s", address, connection) | |
201 olist = self.pendingConns.get(address, []) | |
202 if len(olist) <= 1: | |
203 olist.append(connection) | |
204 self.pendingConns[address] = olist | |
205 return True | |
206 else: | |
207 return False | |
208 | |
209 def removePendingConnection(self, address, connection): | |
210 olist = self.pendingConns[address] | |
211 if len(olist) == 1: | |
212 del self.pendingConns[address] | |
213 else: | |
214 olist.remove(connection) | |
215 self.pendingConns[address] = olist | |
216 | |
217 def removeActiveConnection(self, address): | |
218 del self.activeConns[address] | |
219 | |
220 def _parseRequest(self): | |
221 debug("_parseRequest") | |
222 try: | |
223 # Parse out data and trim buffer accordingly | |
224 ver, cmd, rsvd, self.addressType = struct.unpack('!BBBB', self.buf[:4]) | |
225 | |
226 # Ensure we actually support the requested address type | |
227 if self.addressType not in self.supportedAddrs: | |
228 self.sendErrorReply(REPLY_ADDR_NOT_SUPPORTED) | |
229 return | |
230 | |
231 # Deal with addresses | |
232 if self.addressType == ADDR_IPV4: | |
233 addr, port = struct.unpack('!IH', self.buf[4:10]) | |
234 self.buf = self.buf[10:] | |
235 elif self.addressType == ADDR_DOMAINNAME: | |
236 nlen = ord(self.buf[4]) | |
237 addr, port = struct.unpack('!%dsH' % nlen, self.buf[5:]) | |
238 self.buf = self.buf[7 + len(addr):] | |
239 else: | |
240 # Any other address types are not supported | |
241 self.sendErrorReply(REPLY_ADDR_NOT_SUPPORTED) | |
242 return | |
243 | |
244 # Ensure command is supported | |
245 if cmd not in self.enabledCommands: | |
246 # Send a not supported error | |
247 self.sendErrorReply(REPLY_CMD_NOT_SUPPORTED) | |
248 return | |
249 | |
250 # Process the command | |
251 if cmd == CMD_CONNECT: | |
252 self.connectRequested(addr, port) | |
253 elif cmd == CMD_BIND: | |
254 self.bindRequested(addr, port) | |
255 else: | |
256 # Any other command is not supported | |
257 self.sendErrorReply(REPLY_CMD_NOT_SUPPORTED) | |
258 | |
259 except struct.error, why: | |
260 return None | |
261 | |
262 def _makeRequest(self): | |
263 debug("_makeRequest") | |
264 self.state = STATE_TARGET_REQUEST | |
265 sha1 = hashlib.sha1(self.sid + self.initiator_jid + self.target_jid).hexdigest() | |
266 request = struct.pack('!5B%dsH' % len(sha1), SOCKS5_VER, CMD_CONNECT, 0, ADDR_DOMAINNAME, len(sha1), sha1, 0) | |
267 self.transport.write(request) | |
268 | |
269 def _parseRequestReply(self): | |
270 debug("_parseRequestReply") | |
271 try: | |
272 ver, rep, rsvd, self.addressType = struct.unpack('!BBBB', self.buf[:4]) | |
273 # Ensure we actually support the requested address type | |
274 if self.addressType not in self.supportedAddrs: | |
275 self.sendErrorReply(REPLY_ADDR_NOT_SUPPORTED) | |
276 return | |
277 | |
278 # Deal with addresses | |
279 if self.addressType == ADDR_IPV4: | |
280 addr, port = struct.unpack('!IH', self.buf[4:10]) | |
281 self.buf = self.buf[10:] | |
282 elif self.addressType == ADDR_DOMAINNAME: | |
283 nlen = ord(self.buf[4]) | |
284 addr, port = struct.unpack('!%dsH' % nlen, self.buf[5:]) | |
285 self.buf = self.buf[7 + len(addr):] | |
286 else: | |
287 # Any other address types are not supported | |
288 self.sendErrorReply(REPLY_ADDR_NOT_SUPPORTED) | |
289 return | |
290 | |
291 # Ensure reply is OK | |
292 if rep != REPLY_SUCCESS: | |
293 self.loseConnection() | |
294 return | |
295 | |
296 debug("Saving file in %s.", self.data["dest_path"]) | |
297 self.dest_file = open(self.data["dest_path"], 'w') | |
298 self.state = STATE_TARGET_READY | |
299 self.activateCB(self.target_jid, self.initiator_jid, self.sid, self.IQ_id) | |
300 | |
301 | |
302 except struct.error, why: | |
303 return None | |
304 | |
305 def connectionMade(self): | |
306 debug("connectionMade (mode = %s)" % self.mode) | |
307 self.host.registerProgressCB(self.transfert_id, self.getProgress) | |
308 | |
309 if self.mode == "target": | |
310 self.state = STATE_TARGET_INITIAL | |
311 self._startNegotiation() | |
312 | |
313 def connectRequested(self, addr, port): | |
314 debug(("connectRequested")) | |
315 # Check for special connect to the namespace -- this signifies that the client | |
316 # is just checking to ensure it can connect to the streamhost | |
317 if addr == "http://jabber.org/protocol/bytestreams": | |
318 self.connectCompleted(addr, 0) | |
319 self.transport.loseConnection() | |
320 return | |
321 | |
322 # Save addr, for cleanup | |
323 self.addr = addr | |
324 | |
325 # Check to see if the requested address is already | |
326 # activated -- send an error if so | |
327 if addr in self.activeConns: | |
328 self.sendErrorReply(socks5.REPLY_CONN_NOT_ALLOWED) | |
329 return | |
330 | |
331 # Add this address to the pending connections | |
332 if self.addConnection(addr, self): | |
333 self.connectCompleted(addr, 0) | |
334 self.transport.stopReading() | |
335 else: | |
336 self.sendErrorReply(socks5.REPLY_CONN_REFUSED) | |
337 | |
338 def getProgress(self, data): | |
339 """Fill data with position of current transfert""" | |
340 data["size"] = self.filesize | |
341 try: | |
342 data["position"] = str(self.dest_file.tell()) | |
343 except (ValueError, AttributeError): | |
344 data["position"] = "" | |
345 | |
346 def fileTransfered(self, d): | |
347 info("File transfer completed, closing connection") | |
348 self.transport.loseConnection() | |
349 | |
350 def updateTransfered(self, data): | |
351 self.transfered+=len(data) | |
352 return data | |
353 | |
354 def connectCompleted(self, remotehost, remoteport): | |
355 debug("connectCompleted") | |
356 if self.addressType == ADDR_IPV4: | |
357 result = struct.pack('!BBBBIH', SOCKS5_VER, REPLY_SUCCESS, 0, 1, remotehost, remoteport) | |
358 elif self.addressType == ADDR_DOMAINNAME: | |
359 result = struct.pack('!BBBBB%dsH' % len(remotehost), SOCKS5_VER, REPLY_SUCCESS, 0, | |
360 ADDR_DOMAINNAME, len(remotehost), remotehost, remoteport) | |
361 self.transport.write(result) | |
362 self.state = STATE_READY | |
363 self.dest_file=open(self.filepath) | |
364 d=self.beginFileTransfer(self.dest_file, self.transport, self.updateTransfered) | |
365 d.addCallback(self.fileTransfered) | |
366 | |
367 def bindRequested(self, addr, port): | |
368 pass | |
369 | |
370 def authenticateUserPass(self, user, passwd): | |
371 debug("User/pass: %s/%s", user, passwd) | |
372 return True | |
373 | |
374 def dataReceived(self, buf): | |
375 if self.state == STATE_TARGET_READY: | |
376 self.dest_file.write(buf) | |
377 self.transfered+=len(buf) | |
378 return | |
379 | |
380 self.buf = self.buf + buf | |
381 if self.state == STATE_INITIAL: | |
382 self._parseNegotiation() | |
383 if self.state == STATE_AUTH_USERPASS: | |
384 self._parseUserPass() | |
385 if self.state == STATE_REQUEST: | |
386 self._parseRequest() | |
387 if self.state == STATE_TARGET_AUTH: | |
388 ver, method = struct.unpack('!BB', buf) | |
389 self.buf = self.buf[2:] | |
390 if ver!=SOCKS5_VER or method!=AUTHMECH_ANON: | |
391 self.transport.loseConnection() | |
392 else: | |
393 self._makeRequest() | |
394 if self.state == STATE_TARGET_REQUEST: | |
395 self._parseRequestReply() | |
396 | |
397 | |
398 def clientConnectionLost(self, reason): | |
399 debug("clientConnectionLost") | |
400 self.transport.loseConnection() | |
401 | |
402 def connectionLost(self, reason): | |
403 debug("connectionLost") | |
404 self.host.removeProgressCB(self.transfert_id) | |
405 if self.state == STATE_CONNECT_PENDING: | |
406 self.removePendingConnection(self.addr, self) | |
407 else: | |
408 self.transport.unregisterProducer() | |
409 if self.peersock != None: | |
410 self.peersock.peersock = None | |
411 self.peersock.transport.unregisterProducer() | |
412 self.peersock = None | |
413 self.removeActiveConnection(self.addr) | |
414 | |
415 class Socks5ServerFactory(protocol.ServerFactory): | |
416 protocol = SOCKSv5 | |
417 protocol.mode = "initiator" #FIXME: Q&D way, fix it | |
418 | |
419 | |
420 def startedConnecting(self, connector): | |
421 debug ("Socks 5 server connection started") | |
422 | |
423 def clientConnectionLost(self, connector, reason): | |
424 debug ("Socks 5 server connection lost (reason: %s)", reason) | |
425 | |
426 class Socks5ClientFactory(protocol.ClientFactory): | |
427 protocol = SOCKSv5 | |
428 protocol.mode = "target" #FIXME: Q&D way, fix it | |
429 | |
430 def startedConnecting(self, connector): | |
431 debug ("Socks 5 client connection started") | |
432 | |
433 def clientConnectionLost(self, connector, reason): | |
434 debug ("Socks 5 client connection lost (reason: %s)", reason) | |
435 | |
436 | |
437 class XEP_0065(): | |
438 def __init__(self, host): | |
439 info("Plugin XEP_0065 initialization") | |
440 self.host = host | |
441 debug("registering") | |
442 self.server_factory = Socks5ServerFactory() | |
443 self.server_factory.protocol.host = self.host #needed for progress CB | |
444 self.client_factory = Socks5ClientFactory() | |
445 host.add_IQ_cb("http://jabber.org/protocol/bytestreams", self.getFile) | |
446 port = int(self.host.memory.getParamV("Port", "File Transfert")) | |
447 info("Launching Socks5 Stream server on port %d", port) | |
448 reactor.listenTCP(port, self.server_factory) | |
449 | |
450 def setData(self, data, id): | |
451 self.data = data | |
452 self.transfert_id = id | |
453 | |
454 def sendFile(self, id, filepath, size): | |
455 #lauching socks5 initiator | |
456 self.server_factory.protocol.mode = "initiator" | |
457 self.server_factory.protocol.filepath = filepath | |
458 self.server_factory.protocol.filesize = size | |
459 self.server_factory.protocol.transfert_id = id | |
460 | |
461 def getFile(self, stanza): | |
462 """Get file using byte stream""" | |
463 SI_elem = stanza.firstChildElement() | |
464 IQ_id = stanza['id'] | |
465 for element in SI_elem.elements(): | |
466 if element.name == "streamhost": | |
467 info ("Stream proposed: host=[%s] port=[%s]", element['host'], element['port']) | |
468 factory = self.client_factory | |
469 self.server_factory.protocol.mode = "target" | |
470 factory.protocol.host = self.host #needed for progress CB | |
471 factory.protocol.data = self.data | |
472 factory.protocol.transfert_id = self.transfert_id | |
473 factory.protocol.filesize = self.data["size"] | |
474 factory.protocol.sid = SI_elem['sid'] | |
475 factory.protocol.initiator_jid = element['jid'] | |
476 factory.protocol.target_jid = self.host.me.full() | |
477 factory.protocol.IQ_id = IQ_id | |
478 factory.protocol.activateCB = self.activateStream | |
479 reactor.connectTCP(element['host'], int(element['port']), factory) | |
480 | |
481 def activateStream(self, from_jid, to_jid, sid, IQ_id): | |
482 debug("activating stream") | |
483 result = domish.Element(('', 'iq')) | |
484 result['type'] = 'result' | |
485 result['id'] = IQ_id | |
486 result['from'] = from_jid | |
487 result['to'] = to_jid | |
488 query = result.addElement('query', 'http://jabber.org/protocol/bytestreams') | |
489 query['sid'] = sid | |
490 streamhost = query.addElement('streamhost-used') | |
491 streamhost['jid'] = to_jid #FIXME: use real streamhost | |
492 self.host.xmlstream.send(result) | |
493 |