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