From c449538c99e96ac01c4ef4654752ecd9e29044df Mon Sep 17 00:00:00 2001 From: jamesbunton Date: Mon, 2 Jan 2006 08:08:09 +0000 Subject: [PATCH] Fixed clientcaps. git-svn-id: http://delx.cjb.net/svn/pymsnt/trunk@82 55fbd22a-6204-0410-b2f0-b6c764c7e90a committer: jamesbunton --- src/config.py | 3 +- src/disco.py | 1 + src/ft.py | 180 +++++++++++++++++++++++++++++---- src/main.py | 4 +- src/tlib/msn/msnw.py | 2 +- src/tlib/socks5.py | 234 +++++++++++++++++++++++++++++++++++++++++++ src/utils.py | 5 + 7 files changed, 402 insertions(+), 27 deletions(-) create mode 100644 src/tlib/socks5.py diff --git a/src/config.py b/src/config.py index 655eb10..d49a61a 100644 --- a/src/config.py +++ b/src/config.py @@ -23,8 +23,7 @@ allowRegister = False getAllAvatars = False useXCP = False -ftLowPort = "6891" -ftHighPort = "6899" +ftJabberPort = "" ftOOBPort = "" ftOOBRoot = "http://" + ip + "/" diff --git a/src/disco.py b/src/disco.py index 58aeb14..0d4f110 100644 --- a/src/disco.py +++ b/src/disco.py @@ -20,6 +20,7 @@ CAPS = "http://jabber.org/protocol/caps" SUBSYNC = "http://jabber.org/protocol/roster-subsync" MUC = "http://jabber.org/protocol/muc" MUC_USER = MUC + "#user" +FEATURE_NEG = "http://jabber.org/protocol/feature-neg" SI = "http://jabber.org/protocol/si" FT = "http://jabber.org/protocol/si/profile/file-transfer" S5B = "http://jabber.org/protocol/bytestreams" diff --git a/src/ft.py b/src/ft.py index f1438ad..6b13706 100644 --- a/src/ft.py +++ b/src/ft.py @@ -34,21 +34,25 @@ class FTReceive: self.legacyftp = legacyftp LogEvent(INFO) self.checkSupport() - + def checkSupport(self): def discoDone(features): - c1 = features.count(disco.FT) - c2 = features.count(disco.S5) - c3 = features.count(disco.IBB) - c4 = features.count(disco.IQOOB) - #if c1 > 0 and c2 > 0: - # self.socksMode() - #elif c1 > 0 and c3 > 0: - # self.ibbMode() - if c4 > 0: + enabledS5 = hasattr(self.session.pytrans, "ftSOCKS5") + enabledOOB = hasattr(self.session.pytrans, "ftOOB") + hasFT = features.count(disco.FT) + hasS5 = features.count(disco.S5) + #hasIBB = features.count(disco.IBB) + hasOOB = features.count(disco.IQOOB) + if hasFT > 0 and hasS5 > 0 and enabledS5: + self.socksMode() + elif hasOOB > 0 and enabledOOB: self.oobMode() - else: + elif enabledOOB: self.messageOobMode() + else: + # No support + self.legacyftp.reject() + del self.legacyftp def discoFail(ignored=None): self.messageOobMode() @@ -58,11 +62,48 @@ class FTReceive: d.addErrback(discoFail) def socksMode(self): - raise NotImplementedError - - def ibbMode(self): - raise NotImplementedError + def ftReply(el): + if el.getAttribute("type") != "result": + ftDeclined() + return + # FIXME, some kind of mixin + self.session.pytrans.ftSOCKS5.addConnection(utils.socks5Hash(self.sid, self.senderJID, self.toJID), self.legacyftp) + self.legacyftp.accept(self) + + def ftDeclined(el): + self.legacyftp.reject() + del self.legacyftp + LogEvent(INFO, self.ident) + self.sid = random.randint(1000, sys.maxint) + iq = Element((None, "iq")) + iq.attributes["type"] = "set" + iq.attributes["to"] = self.toJID + iq.attributes["from"] = self.senderJID + si = iq.addElement("si") + si.attributes["xmlns"] = disco.SI + si.attributes["profile"] = disco.FT + si.attributes["id"] = self.sid + file = si.addElement("file") + file.attributes["profile"] = disco.FT + file.attributes["size"] = self.legacyftp.filesize + file.attributes["name"] = self.legacyftp.filename + # Feature negotiation + feature = si.addElement("feature") + feature.attributes["xmlns"] = disco.FEATURE_NEG + x = feature.addElement("x") + x.attributes["xmlns"] = "jabber:x:data" + x.attributes["type"] = "form" + field = x.addElement("field") + field.attributes["type"] = "list-single" + field.attributes["var"] = "stream-method" + option = field.addElement("option") + value = option.addElement("value") + value.addContent(disco.S5B) + d = self.pytrans.discovery.sendIq(iq, 60*3) + d.addCallback(ftReply) + d.addErrback(ftDeclined) + def oobMode(self): def cb(el): if el.getAttribute("type") != "result": @@ -75,8 +116,7 @@ class FTReceive: del self.legacyftp LogEvent(INFO, self.ident) - filename = self.legacyftp.filename - self.session.pytrans.ftOOB.putFile(self, filename) + filename = self.session.pytrans.ftOOB.putFile(self, self.legacyftp.filename) iq = Element((None, "iq")) iq.attributes["to"] = self.toJID iq.attributes["from"] = self.senderJID @@ -88,8 +128,7 @@ class FTReceive: def messageOobMode(self): LogEvent(INFO, self.ident) - filename = self.legacyftp.filename - self.session.pytrans.ftOOB.putFile(self, filename) + filename = self.session.pytrans.ftOOB.putFile(self, self.legacyftp.filename) m = Element((None, "message")) m.attributes["to"] = self.session.jabberID m.attributes["from"] = self.senderJID @@ -105,8 +144,101 @@ class FTReceive: +# SOCKS5 + +from tlib import socks5 + +class JEP65Connection(socks5.SOCKSv5): + def __init__(self, listener): + socks5.SOCKSv5.__init__(self) + self.listener = listener + self.supportedAuthMechs = [socks5.AUTHMECH_ANON] + self.supportedAddrs = [socks5.ADDR_DOMAINNAME] + self.enabledCommands = [socks5.CMD_CONNECT] + self.addr = "" + + def connectRequested(self, addr, port): + # Check for special connect to the namespace -- this signifies that + # the client is just checking that it can connect to the streamhost + if addr == disco.S5B: + self.connectCompleted(addr, 0) + self.transport.loseConnection() + return + + self.addr = addr + + if self.listener.isActive(addr): + self.sendErrorReply(socks5.REPLY_CONN_NOT_ALLOWED) + return + + if self.listener.addConnection(addr, self): + self.connectCompleted(addr, 0) + else: + self.sendErrorReply(socks5.REPLY_CONN_REFUSED) + + def connectionLost(self, reason): + if self.state == socks5.STATE_CONNECT_PENDING: + self.listener.removePendingConnection(self.addr, self) + else: + self.transport.unregisterProducer() + if self.peersock != None: + self.peersock.peersock = None + self.peersock.transport.unregisterProducer() + self.peersock = None + self.listener.removeActiveConnection(self.addr) + +class Proxy65(protocol.Factory): + def __init__(self, port): + internet.TCPServer(port, self) + self.pendingConns = {} + self.activeConns = {} + + def buildProtocol(self, addr): + return JEP65Connection(self) + + def isActive(self, address): + return address in self.activeConns + + def activateStream(self, address): + if address in self.pendingConns: + olist = self.pendingConns[address] + if len(olist) != 2: + LogEvent(WARN, '', "Not exactly two!") + return + + assert address not in self.activeConns + self.activeConns[address] = None + + olist[0].peersock = olist[1] + olist[1].peersock = olist[0] + olist[0].transport.registerProducer(olist[1], 0) + olist[1].transport.registerProducer(olist[0], 0) + else: + LogEvent(WARN, '', "No pending connection.") + + def addConnection(self, address, connection): + olist = self.pendingConns.get(address, []) + if len(olist) <= 1: + olist.append(connection) + self.pendingConns[address] = olist + if len(olist) == 2: + self.activateStream(address) + return True + else: + return False + + def removePendingConnection(self, address, connection): + olist = self.pendingConns[address] + if len(olist) == 1: + del self.pendingConns[address] + else: + olist.remove(connection) + + def removeActiveConnection(self, address): + del self.activeConns[address] + -# Put the files up for OOB download +# OOB download server from twisted.web import server, resource, error from twisted.internet import reactor @@ -129,14 +261,17 @@ class Connector: self.ftReceive.error() class FileTransferOOB(resource.Resource): - def __init__(self): + def __init__(self, port): self.isLeaf = True self.files = {} self.oobSite = server.Site(self) - reactor.listenTCP(int(config.ftOOBPort), self.oobSite) + reactor.listenTCP(port, self.oobSite) def putFile(self, file, filename): + path = str(random.randint(100000000, 999999999)) + filename = (path + "/" + filename).replace("//", "/") self.files[filename] = file + return filename def remFile(self, filename): if self.files.has_key(filename): @@ -146,6 +281,7 @@ class FileTransferOOB(resource.Resource): filename = request.path[1:] # Remove the leading / if self.files.has_key(filename): file = self.files[filename] + request.setHeader("Content-Length", str(self.legacyftp.filesize)) request.setHeader("Content-Disposition", "attachment; filename=\"%s\"" % file.legacyftp.filename.encode("utf-8")) Connector(file, request) del self.files[filename] diff --git a/src/main.py b/src/main.py index edebfeb..de1165b 100644 --- a/src/main.py +++ b/src/main.py @@ -60,7 +60,8 @@ class PyTransport(component.Service): self.vCardFactory = misciq.VCardFactory(self) self.iqAvatarFactor = misciq.IqAvatarFactory(self) self.connectUsers = misciq.ConnectUsers(self) - self.ftOOB = ft.FileTransferOOB() + if config.ftJabberPort: self.ftSOCKS5 = ft.Proxy65(int(config.ftJabberPort)) + if config.ftOOBPort: self.ftOOB = ft.FileTransferOOB(int(config.ftOOBPort)) self.statistics = misciq.Statistics(self) self.startTime = int(time.time()) @@ -256,7 +257,6 @@ class App: def SIGHUPstuff(*args): xmlconfig.reloadConfig() - debug.reopenFile() if os.name == "posix": import signal diff --git a/src/tlib/msn/msnw.py b/src/tlib/msn/msnw.py index 42314fb..ffd83c2 100644 --- a/src/tlib/msn/msnw.py +++ b/src/tlib/msn/msnw.py @@ -699,7 +699,7 @@ class OneSwitchboardSession(SwitchboardSessionBase): elif "text/x-clientcaps" == cTypes[0]: if message.hasHeader("JabberID"): jid = message.getHeader("JabberID") - self.switchboardSession.msncon.userMapping(message.userHandle, jid) + self.msncon.userMapping(message.userHandle, jid) else: LogEvent(INFO, self.ident, "Discarding unknown message type.") diff --git a/src/tlib/socks5.py b/src/tlib/socks5.py new file mode 100644 index 0000000..8639def --- /dev/null +++ b/src/tlib/socks5.py @@ -0,0 +1,234 @@ +##============================================================================ +## +## License: +## +## This library is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public +## License as published by the Free Software Foundation; either +## version 2 of the License, or (at your option) any later version. +## +## This library is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +## General Public License for more details. +## +## You should have received a copy of the GNU General Public +## License along with this library; if not, write to the Free Software +## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 021-1307 +## USA +## +## Copyright (C) 2002-2003 Dave Smith (dizzyd@jabber.org) +## Copyright (C) 2005 James Bunton (james@delx.cjb.net) +## +## $Id: socks5.py,v 1.1.1.1 2003/07/15 04:18:05 dizzyd Exp $ +##============================================================================ + +from twisted.internet import protocol, reactor +import struct + +STATE_INITIAL = 0 +STATE_AUTH = 1 +STATE_REQUEST = 2 +STATE_READY = 3 +STATE_AUTH_USERPASS = 4 +STATE_LAST = 5 + +STATE_CONNECT_PENDING = STATE_LAST + 1 + +SOCKS5_VER = 0x05 + +ADDR_IPV4 = 0x01 +ADDR_DOMAINNAME = 0x03 +ADDR_IPV6 = 0x04 + +CMD_CONNECT = 0x01 +CMD_BIND = 0x02 +CMD_UDPASSOC = 0x03 + +AUTHMECH_ANON = 0x00 +AUTHMECH_USERPASS = 0x02 +AUTHMECH_INVALID = 0xFF + +REPLY_SUCCESS = 0x00 +REPLY_GENERAL_FAILUR = 0x01 +REPLY_CONN_NOT_ALLOWED = 0x02 +REPLY_NETWORK_UNREACHABLE = 0x03 +REPLY_HOST_UNREACHABLE = 0x04 +REPLY_CONN_REFUSED = 0x05 +REPLY_TTL_EXPIRED = 0x06 +REPLY_CMD_NOT_SUPPORTED = 0x07 +REPLY_ADDR_NOT_SUPPORTED = 0x08 + +class SOCKSv5Outgoing(protocol.Protocol): + def __init__(self, peersock): + self.peersock = peersock + self.peersock.peersock = self + + def connectionMade(self): + _invalid_, hostname, port = self.transport.getPeer() + self.peersock.connectCompleted(hostname, port) + + def connectionLost(self, reason): + self.peersock.transport.loseConnection() + self.peersock.peersock = None + self.peersock = None + + def dataReceived(self, buf): + self.peersock.transport.write(buf) + +class SOCKSv5(protocol.Protocol): + def __init__(self): + self.state = STATE_INITIAL + self.buf = "" + self.supportedAuthMechs = [ AUTHMECH_USERPASS ] + self.supportedAddrs = [ ADDR_IPV4, ADDR_DOMAINNAME ] + self.enabledCommands = [ CMD_CONNECT, CMD_BIND ] + self.peersock = None + self.addressType = 0 + self.requestType = 0 + + def _parseNegotiation(self): + try: + # Parse out data + ver, nmethod = struct.unpack('!BB', self.buf[:2]) + methods = struct.unpack('%dB' % nmethod, self.buf[2:nmethod+2]) + + # Ensure version is correct + if ver != 5: + self.transport.write(struct.pack('!BB', SOCKS5_VER, AUTHMECH_INVALID)) + self.transport.loseConnection() + return + + # Trim off front of the buffer + self.buf = self.buf[nmethod+2:] + + # Check for supported auth mechs + for m in self.supportedAuthMechs: + if m in methods: + # Update internal state, according to selected method + if m == AUTHMECH_ANON: + self.state = STATE_REQUEST + elif m == AUTHMECH_USERPASS: + self.state = STATE_AUTH_USERPASS + # Complete negotiation w/ this method + self.transport.write(struct.pack('!BB', SOCKS5_VER, m)) + return + + # No supported mechs found, notify client and close the connection + self.transport.write(struct.pack('!BB', SOCKS5_VER, AUTHMECH_INVALID)) + self.transport.loseConnection() + except struct.error: + pass + + def _parseUserPass(self): + try: + # Parse out data + ver, ulen = struct.unpack('BB', self.buf[:2]) + uname, = struct.unpack('%ds' % ulen, self.buf[2:ulen + 2]) + plen, = struct.unpack('B', self.buf[ulen + 2]) + password, = struct.unpack('%ds' % plen, self.buf[ulen + 3:ulen + 3 + plen]) + # Trim off fron of the buffer + self.buf = self.buf[3 + ulen + plen:] + # Fire event to authenticate user + if self.authenticateUserPass(uname, password): + # Signal success + self.state = STATE_REQUEST + self.transport.write(struct.pack('!BB', SOCKS5_VER, 0x00)) + else: + # Signal failure + self.transport.write(struct.pack('!BB', SOCKS5_VER, 0x01)) + self.transport.loseConnection() + except struct.error: + pass + + def sendErrorReply(self, errorcode): + # Any other address types are not supported + result = struct.pack('!BBBBIH', SOCKS5_VER, errorcode, 0, 1, 0, 0) + self.transport.write(result) + self.transport.loseConnection() + + def _parseRequest(self): + try: + # Parse out data and trim buffer accordingly + ver, cmd, rsvd, self.addressType = struct.unpack('!BBBB', self.buf[:4]) + + # Ensure we actually support the requested address type + if self.addressType not in self.supportedAddrs: + self.sendErrorReply(REPLY_ADDR_NOT_SUPPORTED) + return + + # Deal with addresses + if self.addressType == ADDR_IPV4: + addr, port = struct.unpack('!IH', self.buf[4:10]) + self.buf = self.buf[10:] + elif self.addressType == ADDR_DOMAINNAME: + nlen = ord(self.buf[4]) + addr, port = struct.unpack('!%dsH' % nlen, self.buf[5:]) + self.buf = self.buf[7 + len(addr):] + else: + # Any other address types are not supported + self.sendErrorReply(REPLY_ADDR_NOT_SUPPORTED) + return + + # Ensure command is supported + if cmd not in self.enabledCommands: + # Send a not supported error + self.sendErrorReply(REPLY_CMD_NOT_SUPPORTED) + return + + # Process the command + if cmd == CMD_CONNECT: + self.connectRequested(addr, port) + elif cmd == CMD_BIND: + self.bindRequested(addr, port) + else: + # Any other command is not supported + self.sendErrorReply(REPLY_CMD_NOT_SUPPORTED) + + except struct.error, why: + return None + + + def connectRequested(self, addr, port): + self.transport.stopReading() + self.state = STATE_CONNECT_PENDING + protocol.ClientCreator(reactor, SOCKSv5Outgoing, self).connectTCP(addr, port) + + def connectCompleted(self, remotehost, remoteport): + if self.addressType == ADDR_IPV4: + result = struct.pack('!BBBBIH', SOCKS5_VER, REPLY_SUCCESS, 0, 1, remotehost, remoteport) + elif self.addressType == ADDR_DOMAINNAME: + result = struct.pack('!BBBBB%dsH' % len(remotehost), SOCKS5_VER, REPLY_SUCCESS, 0, + ADDR_DOMAINNAME, len(remotehost), remotehost, remoteport) + self.transport.write(result) + self.state = STATE_READY + self.transport.startReading() + + def bindRequested(self, addr, port): + pass + + def authenticateUserPass(self, user, passwd): + print "User/pass: ", user, passwd + return True + + def dataReceived(self, buf): + if self.state == STATE_READY: + self.peersock.transport.write(buf) + return + + self.buf = self.buf + buf + if self.state == STATE_INITIAL: + self._parseNegotiation() + if self.state == STATE_AUTH_USERPASS: + self._parseUserPass() + if self.state == STATE_REQUEST: + self._parseRequest() + + + +factory = protocol.Factory() +factory.protocol = SOCKSv5 + +if __name__ == "__main__": + reactor.listenTCP(8888, factory) + reactor.run() diff --git a/src/utils.py b/src/utils.py index 845c521..c27d536 100644 --- a/src/utils.py +++ b/src/utils.py @@ -7,6 +7,11 @@ def getLang(el): return el.getAttribute((u'http://www.w3.org/XML/1998/namespace', u'lang')) +import sha +def socks5Hash(sid, initiator, target): + return sha.new("%s%s%s" % (sid, initiator, target)).hexdigest() + + try: import Image import StringIO -- 2.39.2