0
|
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", |
9
|
72 "description": """Implementation of SOCKS5 Bytestreams""" |
0
|
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 |
8
|
456 debug("Launching socks5 initiator") |
0
|
457 self.server_factory.protocol.mode = "initiator" |
|
458 self.server_factory.protocol.filepath = filepath |
|
459 self.server_factory.protocol.filesize = size |
|
460 self.server_factory.protocol.transfert_id = id |
|
461 |
|
462 def getFile(self, stanza): |
|
463 """Get file using byte stream""" |
|
464 SI_elem = stanza.firstChildElement() |
|
465 IQ_id = stanza['id'] |
|
466 for element in SI_elem.elements(): |
|
467 if element.name == "streamhost": |
|
468 info ("Stream proposed: host=[%s] port=[%s]", element['host'], element['port']) |
|
469 factory = self.client_factory |
|
470 self.server_factory.protocol.mode = "target" |
|
471 factory.protocol.host = self.host #needed for progress CB |
|
472 factory.protocol.data = self.data |
|
473 factory.protocol.transfert_id = self.transfert_id |
|
474 factory.protocol.filesize = self.data["size"] |
|
475 factory.protocol.sid = SI_elem['sid'] |
|
476 factory.protocol.initiator_jid = element['jid'] |
|
477 factory.protocol.target_jid = self.host.me.full() |
|
478 factory.protocol.IQ_id = IQ_id |
|
479 factory.protocol.activateCB = self.activateStream |
|
480 reactor.connectTCP(element['host'], int(element['port']), factory) |
|
481 |
|
482 def activateStream(self, from_jid, to_jid, sid, IQ_id): |
|
483 debug("activating stream") |
|
484 result = domish.Element(('', 'iq')) |
|
485 result['type'] = 'result' |
|
486 result['id'] = IQ_id |
|
487 result['from'] = from_jid |
|
488 result['to'] = to_jid |
|
489 query = result.addElement('query', 'http://jabber.org/protocol/bytestreams') |
|
490 query['sid'] = sid |
|
491 streamhost = query.addElement('streamhost-used') |
|
492 streamhost['jid'] = to_jid #FIXME: use real streamhost |
|
493 self.host.xmlstream.send(result) |
|
494 |