comparison src/plugins/plugin_xep_0065.py @ 223:86d249b6d9b7

Files reorganisation
author Goffi <goffi@goffi.org>
date Wed, 29 Dec 2010 01:06:29 +0100
parents plugins/plugin_xep_0065.py@bd24f2aed80c
children b1794cbb88e5
comparison
equal deleted inserted replaced
222:3198bfd66daa 223:86d249b6d9b7
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, 2010 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 from twisted.web.client import getPage
63 import struct
64 import urllib
65 import hashlib, pdb
66
67 from zope.interface import implements
68
69 try:
70 from twisted.words.protocols.xmlstream import XMPPHandler
71 except ImportError:
72 from wokkel.subprotocols import XMPPHandler
73
74 from wokkel import disco, iwokkel
75
76 IQ_SET = '/iq[@type="set"]'
77 NS_BS = 'http://jabber.org/protocol/bytestreams'
78 BS_REQUEST = IQ_SET + '/query[@xmlns="' + NS_BS + '"]'
79
80
81
82 PLUGIN_INFO = {
83 "name": "XEP 0065 Plugin",
84 "import_name": "XEP_0065",
85 "type": "XEP",
86 "protocols": ["XEP-0065"],
87 "main": "XEP_0065",
88 "handler": "yes",
89 "description": _("""Implementation of SOCKS5 Bytestreams""")
90 }
91
92 STATE_INITIAL = 0
93 STATE_AUTH = 1
94 STATE_REQUEST = 2
95 STATE_READY = 3
96 STATE_AUTH_USERPASS = 4
97 STATE_TARGET_INITIAL = 5
98 STATE_TARGET_AUTH = 6
99 STATE_TARGET_REQUEST = 7
100 STATE_TARGET_READY = 8
101 STATE_LAST = 9
102
103 STATE_CONNECT_PENDING = STATE_LAST + 1
104
105 SOCKS5_VER = 0x05
106
107 ADDR_IPV4 = 0x01
108 ADDR_DOMAINNAME = 0x03
109 ADDR_IPV6 = 0x04
110
111 CMD_CONNECT = 0x01
112 CMD_BIND = 0x02
113 CMD_UDPASSOC = 0x03
114
115 AUTHMECH_ANON = 0x00
116 AUTHMECH_USERPASS = 0x02
117 AUTHMECH_INVALID = 0xFF
118
119 REPLY_SUCCESS = 0x00
120 REPLY_GENERAL_FAILUR = 0x01
121 REPLY_CONN_NOT_ALLOWED = 0x02
122 REPLY_NETWORK_UNREACHABLE = 0x03
123 REPLY_HOST_UNREACHABLE = 0x04
124 REPLY_CONN_REFUSED = 0x05
125 REPLY_TTL_EXPIRED = 0x06
126 REPLY_CMD_NOT_SUPPORTED = 0x07
127 REPLY_ADDR_NOT_SUPPORTED = 0x08
128
129
130
131
132
133 class SOCKSv5(protocol.Protocol, FileSender):
134 def __init__(self):
135 debug(_("Protocol init"))
136 self.state = STATE_INITIAL
137 self.buf = ""
138 self.supportedAuthMechs = [ AUTHMECH_ANON ]
139 self.supportedAddrs = [ ADDR_DOMAINNAME ]
140 self.enabledCommands = [ CMD_CONNECT ]
141 self.peersock = None
142 self.addressType = 0
143 self.requestType = 0
144 self.activeConns = {}
145 self.pendingConns = {}
146 self.transfered = 0 #nb of bytes already copied
147
148 def _startNegotiation(self):
149 debug("_startNegotiation")
150 self.state = STATE_TARGET_AUTH
151 self.transport.write(struct.pack('!3B', SOCKS5_VER, 1, AUTHMECH_ANON))
152
153 def _parseNegotiation(self):
154 debug("_parseNegotiation")
155 try:
156 # Parse out data
157 ver, nmethod = struct.unpack('!BB', self.buf[:2])
158 methods = struct.unpack('%dB' % nmethod, self.buf[2:nmethod+2])
159
160 # Ensure version is correct
161 if ver != 5:
162 self.transport.write(struct.pack('!BB', SOCKS5_VER, AUTHMECH_INVALID))
163 self.transport.loseConnection()
164 return
165
166 # Trim off front of the buffer
167 self.buf = self.buf[nmethod+2:]
168
169 # Check for supported auth mechs
170 for m in self.supportedAuthMechs:
171 if m in methods:
172 # Update internal state, according to selected method
173 if m == AUTHMECH_ANON:
174 self.state = STATE_REQUEST
175 elif m == AUTHMECH_USERPASS:
176 self.state = STATE_AUTH_USERPASS
177 # Complete negotiation w/ this method
178 self.transport.write(struct.pack('!BB', SOCKS5_VER, m))
179 return
180
181 # No supported mechs found, notify client and close the connection
182 self.transport.write(struct.pack('!BB', SOCKS5_VER, AUTHMECH_INVALID))
183 self.transport.loseConnection()
184 except struct.error:
185 pass
186
187 def _parseUserPass(self):
188 debug("_parseUserPass")
189 try:
190 # Parse out data
191 ver, ulen = struct.unpack('BB', self.buf[:2])
192 uname, = struct.unpack('%ds' % ulen, self.buf[2:ulen + 2])
193 plen, = struct.unpack('B', self.buf[ulen + 2])
194 password, = struct.unpack('%ds' % plen, self.buf[ulen + 3:ulen + 3 + plen])
195 # Trim off fron of the buffer
196 self.buf = self.buf[3 + ulen + plen:]
197 # Fire event to authenticate user
198 if self.authenticateUserPass(uname, password):
199 # Signal success
200 self.state = STATE_REQUEST
201 self.transport.write(struct.pack('!BB', SOCKS5_VER, 0x00))
202 else:
203 # Signal failure
204 self.transport.write(struct.pack('!BB', SOCKS5_VER, 0x01))
205 self.transport.loseConnection()
206 except struct.error:
207 pass
208
209 def sendErrorReply(self, errorcode):
210 debug("sendErrorReply")
211 # Any other address types are not supported
212 result = struct.pack('!BBBBIH', SOCKS5_VER, errorcode, 0, 1, 0, 0)
213 self.transport.write(result)
214 self.transport.loseConnection()
215
216 def addConnection(self, address, connection):
217 info(_("Adding connection: %(address)s, %(connection)s") % {'address':address, 'connection':connection})
218 olist = self.pendingConns.get(address, [])
219 if len(olist) <= 1:
220 olist.append(connection)
221 self.pendingConns[address] = olist
222 return True
223 else:
224 return False
225
226 def removePendingConnection(self, address, connection):
227 olist = self.pendingConns[address]
228 if len(olist) == 1:
229 del self.pendingConns[address]
230 else:
231 olist.remove(connection)
232 self.pendingConns[address] = olist
233
234 def removeActiveConnection(self, address):
235 del self.activeConns[address]
236
237 def _parseRequest(self):
238 debug("_parseRequest")
239 try:
240 # Parse out data and trim buffer accordingly
241 ver, cmd, rsvd, self.addressType = struct.unpack('!BBBB', self.buf[:4])
242
243 # Ensure we actually support the requested address type
244 if self.addressType not in self.supportedAddrs:
245 self.sendErrorReply(REPLY_ADDR_NOT_SUPPORTED)
246 return
247
248 # Deal with addresses
249 if self.addressType == ADDR_IPV4:
250 addr, port = struct.unpack('!IH', self.buf[4:10])
251 self.buf = self.buf[10:]
252 elif self.addressType == ADDR_DOMAINNAME:
253 nlen = ord(self.buf[4])
254 addr, port = struct.unpack('!%dsH' % nlen, self.buf[5:])
255 self.buf = self.buf[7 + len(addr):]
256 else:
257 # Any other address types are not supported
258 self.sendErrorReply(REPLY_ADDR_NOT_SUPPORTED)
259 return
260
261 # Ensure command is supported
262 if cmd not in self.enabledCommands:
263 # Send a not supported error
264 self.sendErrorReply(REPLY_CMD_NOT_SUPPORTED)
265 return
266
267 # Process the command
268 if cmd == CMD_CONNECT:
269 self.connectRequested(addr, port)
270 elif cmd == CMD_BIND:
271 self.bindRequested(addr, port)
272 else:
273 # Any other command is not supported
274 self.sendErrorReply(REPLY_CMD_NOT_SUPPORTED)
275
276 except struct.error, why:
277 return None
278
279 def _makeRequest(self):
280 debug("_makeRequest")
281 self.state = STATE_TARGET_REQUEST
282 sha1 = hashlib.sha1(self.sid + self.initiator_jid + self.target_jid).hexdigest()
283 request = struct.pack('!5B%dsH' % len(sha1), SOCKS5_VER, CMD_CONNECT, 0, ADDR_DOMAINNAME, len(sha1), sha1, 0)
284 self.transport.write(request)
285
286 def _parseRequestReply(self):
287 debug("_parseRequestReply")
288 try:
289 ver, rep, rsvd, self.addressType = struct.unpack('!BBBB', self.buf[:4])
290 # Ensure we actually support the requested address type
291 if self.addressType not in self.supportedAddrs:
292 self.sendErrorReply(REPLY_ADDR_NOT_SUPPORTED)
293 return
294
295 # Deal with addresses
296 if self.addressType == ADDR_IPV4:
297 addr, port = struct.unpack('!IH', self.buf[4:10])
298 self.buf = self.buf[10:]
299 elif self.addressType == ADDR_DOMAINNAME:
300 nlen = ord(self.buf[4])
301 addr, port = struct.unpack('!%dsH' % nlen, self.buf[5:])
302 self.buf = self.buf[7 + len(addr):]
303 else:
304 # Any other address types are not supported
305 self.sendErrorReply(REPLY_ADDR_NOT_SUPPORTED)
306 return
307
308 # Ensure reply is OK
309 if rep != REPLY_SUCCESS:
310 self.loseConnection()
311 return
312
313 debug(_("Saving file in %s."), self.data["dest_path"])
314 self.dest_file = open(self.data["dest_path"], 'w')
315 self.state = STATE_TARGET_READY
316 self.activateCB(self.target_jid, self.initiator_jid, self.sid, self.IQ_id, self.xmlstream)
317
318
319 except struct.error, why:
320 return None
321
322 def connectionMade(self):
323 debug("connectionMade (mode = %s)" % self.mode)
324 self.host.registerProgressCB(self.transfert_id, self.getProgress)
325
326 if self.mode == "target":
327 self.state = STATE_TARGET_INITIAL
328 self._startNegotiation()
329
330 def connectRequested(self, addr, port):
331 debug("connectRequested")
332 # Check for special connect to the namespace -- this signifies that the client
333 # is just checking to ensure it can connect to the streamhost
334 if addr == "http://jabber.org/protocol/bytestreams":
335 self.connectCompleted(addr, 0)
336 self.transport.loseConnection()
337 return
338
339 # Save addr, for cleanup
340 self.addr = addr
341
342 # Check to see if the requested address is already
343 # activated -- send an error if so
344 if addr in self.activeConns:
345 self.sendErrorReply(socks5.REPLY_CONN_NOT_ALLOWED)
346 return
347
348 # Add this address to the pending connections
349 if self.addConnection(addr, self):
350 self.connectCompleted(addr, 0)
351 self.transport.stopReading()
352 else:
353 self.sendErrorReply(socks5.REPLY_CONN_REFUSED)
354
355 def getProgress(self, data):
356 """Fill data with position of current transfert"""
357 try:
358 data["position"] = str(self.dest_file.tell())
359 data["size"] = self.filesize
360 except (ValueError, AttributeError):
361 pass
362
363 def fileTransfered(self, d):
364 info(_("File transfer completed, closing connection"))
365 self.transport.loseConnection()
366 try:
367 self.dest_file.close()
368 except:
369 pass
370
371 def updateTransfered(self, data):
372 self.transfered+=len(data)
373 return data
374
375 def connectCompleted(self, remotehost, remoteport):
376 debug("connectCompleted")
377 if self.addressType == ADDR_IPV4:
378 result = struct.pack('!BBBBIH', SOCKS5_VER, REPLY_SUCCESS, 0, 1, remotehost, remoteport)
379 elif self.addressType == ADDR_DOMAINNAME:
380 result = struct.pack('!BBBBB%dsH' % len(remotehost), SOCKS5_VER, REPLY_SUCCESS, 0,
381 ADDR_DOMAINNAME, len(remotehost), remotehost, remoteport)
382 self.transport.write(result)
383 self.state = STATE_READY
384 self.dest_file=open(self.filepath)
385 d=self.beginFileTransfer(self.dest_file, self.transport, self.updateTransfered)
386 d.addCallback(self.fileTransfered)
387
388 def bindRequested(self, addr, port):
389 pass
390
391 def authenticateUserPass(self, user, passwd):
392 debug("User/pass: %s/%s", user, passwd)
393 return True
394
395 def dataReceived(self, buf):
396 if self.state == STATE_TARGET_READY:
397 self.dest_file.write(buf)
398 self.transfered+=len(buf)
399 return
400
401 self.buf = self.buf + buf
402 if self.state == STATE_INITIAL:
403 self._parseNegotiation()
404 if self.state == STATE_AUTH_USERPASS:
405 self._parseUserPass()
406 if self.state == STATE_REQUEST:
407 self._parseRequest()
408 if self.state == STATE_TARGET_AUTH:
409 ver, method = struct.unpack('!BB', buf)
410 self.buf = self.buf[2:]
411 if ver!=SOCKS5_VER or method!=AUTHMECH_ANON:
412 self.transport.loseConnection()
413 else:
414 self._makeRequest()
415 if self.state == STATE_TARGET_REQUEST:
416 self._parseRequestReply()
417
418
419 def clientConnectionLost(self, reason):
420 debug("clientConnectionLost")
421 self.transport.loseConnection()
422
423 def connectionLost(self, reason):
424 debug("connectionLost")
425 self.host.removeProgressCB(self.transfert_id)
426 if self.state == STATE_CONNECT_PENDING:
427 self.removePendingConnection(self.addr, self)
428 else:
429 self.transport.unregisterProducer()
430 if self.peersock != None:
431 self.peersock.peersock = None
432 self.peersock.transport.unregisterProducer()
433 self.peersock = None
434 self.removeActiveConnection(self.addr)
435
436 class Socks5ServerFactory(protocol.ServerFactory):
437 protocol = SOCKSv5
438 protocol.mode = "initiator" #FIXME: Q&D way, fix it
439
440
441 def startedConnecting(self, connector):
442 debug (_("Socks 5 server connection started"))
443
444 def clientConnectionLost(self, connector, reason):
445 debug (_("Socks 5 server connection lost (reason: %s)"), reason)
446
447 class Socks5ClientFactory(protocol.ClientFactory):
448 protocol = SOCKSv5
449 protocol.mode = "target" #FIXME: Q&D way, fix it
450
451 def startedConnecting(self, connector):
452 debug (_("Socks 5 client connection started"))
453
454 def clientConnectionLost(self, connector, reason):
455 debug (_("Socks 5 client connection lost (reason: %s)"), reason)
456
457
458 class XEP_0065():
459
460 params = """
461 <params>
462 <general>
463 <category name="File Transfert">
464 <param name="IP" value='0.0.0.0' default_cb='yes' type="string" />
465 <param name="Port" value="28915" type="string" />
466 </category>
467 </general>
468 </params>
469 """
470
471 def __init__(self, host):
472 info(_("Plugin XEP_0065 initialization"))
473 self.host = host
474 debug(_("registering"))
475 self.server_factory = Socks5ServerFactory()
476 self.server_factory.protocol.host = self.host #needed for progress CB
477 self.client_factory = Socks5ClientFactory()
478
479 #parameters
480 host.memory.importParams(XEP_0065.params)
481 host.memory.setDefault("IP", "File Transfert", self.getExternalIP)
482
483 port = int(self.host.memory.getParamA("Port", "File Transfert"))
484 info(_("Launching Socks5 Stream server on port %d"), port)
485 reactor.listenTCP(port, self.server_factory)
486
487 def getHandler(self, profile):
488 return XEP_0065_handler(self)
489
490 def getExternalIP(self):
491 """Return IP visible from outside, by asking to a website"""
492 return getPage("http://www.goffi.org/sat_tools/get_ip.php")
493
494 def setData(self, data, id):
495 self.data = data
496 self.transfert_id = id
497
498 def sendFile(self, id, filepath, size):
499 #lauching socks5 initiator
500 debug(_("Launching socks5 initiator"))
501 self.server_factory.protocol.mode = "initiator"
502 self.server_factory.protocol.filepath = filepath
503 self.server_factory.protocol.filesize = size
504 self.server_factory.protocol.transfert_id = id
505
506 def getFile(self, iq, profile_key='@DEFAULT@'):
507 """Get file using byte stream"""
508 client = self.host.getClient(profile_key)
509 assert(client)
510 iq.handled = True
511 SI_elem = iq.firstChildElement()
512 IQ_id = iq['id']
513 for element in SI_elem.elements():
514 if element.name == "streamhost":
515 info (_("Stream proposed: host=[%(host)s] port=[%(port)s]") % {'host':element['host'], 'port':element['port']})
516 factory = self.client_factory
517 self.server_factory.protocol.mode = "target"
518 factory.protocol.host = self.host #needed for progress CB
519 factory.protocol.xmlstream = client.xmlstream
520 factory.protocol.data = self.data
521 factory.protocol.transfert_id = self.transfert_id
522 factory.protocol.filesize = self.data["size"]
523 factory.protocol.sid = SI_elem['sid']
524 factory.protocol.initiator_jid = element['jid']
525 factory.protocol.target_jid = client.jid.full()
526 factory.protocol.IQ_id = IQ_id
527 factory.protocol.activateCB = self.activateStream
528 reactor.connectTCP(element['host'], int(element['port']), factory)
529
530 def activateStream(self, from_jid, to_jid, sid, IQ_id, xmlstream):
531 debug(_("activating stream"))
532 result = domish.Element(('', 'iq'))
533 result['type'] = 'result'
534 result['id'] = IQ_id
535 result['from'] = from_jid
536 result['to'] = to_jid
537 query = result.addElement('query', 'http://jabber.org/protocol/bytestreams')
538 query['sid'] = sid
539 streamhost = query.addElement('streamhost-used')
540 streamhost['jid'] = to_jid #FIXME: use real streamhost
541 xmlstream.send(result)
542
543 class XEP_0065_handler(XMPPHandler):
544 implements(iwokkel.IDisco)
545
546 def __init__(self, plugin_parent):
547 self.plugin_parent = plugin_parent
548 self.host = plugin_parent.host
549
550 def connectionInitialized(self):
551 self.xmlstream.addObserver(BS_REQUEST, self.plugin_parent.getFile)
552
553
554 def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
555 return [disco.DiscoFeature(NS_BS)]
556
557 def getDiscoItems(self, requestor, target, nodeIdentifier=''):
558 return []