sat:
- first public release, initial commit
This commit is contained in:
commit
d005c1b9a8
3 changed files with 4704 additions and 0 deletions
0
plugins/__init__.py
Normal file
0
plugins/__init__.py
Normal file
493
plugins/plugin_xep_0065.py
Executable file
493
plugins/plugin_xep_0065.py
Executable file
|
|
@ -0,0 +1,493 @@
|
|||
#!/usr/bin/python
|
||||
#-*- coding: utf-8 -*-
|
||||
"""
|
||||
SAT plugin for managing xep-0065
|
||||
|
||||
Copyright (C)
|
||||
2002-2004 Dave Smith (dizzyd@jabber.org)
|
||||
2007-2008 Fabio Forno (xmpp:ff@jabber.bluendo.com)
|
||||
2009 Jérôme Poisson (goffi@goffi.org)
|
||||
|
||||
This program 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 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program 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 program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
--
|
||||
|
||||
This program is based on proxy65 (http://code.google.com/p/proxy65),
|
||||
originaly written by David Smith and modified by Fabio Forno.
|
||||
It is sublicensed under GPL v3 (or any later version) as allowed by the original
|
||||
license.
|
||||
|
||||
--
|
||||
|
||||
Here is a copy of the original license:
|
||||
|
||||
Copyright (C)
|
||||
2002-2004 Dave Smith (dizzyd@jabber.org)
|
||||
2007-2008 Fabio Forno (xmpp:ff@jabber.bluendo.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from logging import debug, info, error
|
||||
from twisted.internet import protocol, reactor
|
||||
from twisted.protocols.basic import FileSender
|
||||
from twisted.words.xish import domish
|
||||
import struct
|
||||
from binascii import hexlify
|
||||
import hashlib, pdb
|
||||
|
||||
|
||||
PLUGIN_INFO = {
|
||||
"name": "XEP 0065 Plugin",
|
||||
"import_name": "XEP_0065",
|
||||
"type": "XEP",
|
||||
"main": "XEP_0065",
|
||||
"description": """Implementation of SI File Transfert"""
|
||||
}
|
||||
|
||||
STATE_INITIAL = 0
|
||||
STATE_AUTH = 1
|
||||
STATE_REQUEST = 2
|
||||
STATE_READY = 3
|
||||
STATE_AUTH_USERPASS = 4
|
||||
STATE_TARGET_INITIAL = 5
|
||||
STATE_TARGET_AUTH = 6
|
||||
STATE_TARGET_REQUEST = 7
|
||||
STATE_TARGET_READY = 8
|
||||
STATE_LAST = 9
|
||||
|
||||
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 SOCKSv5(protocol.Protocol, FileSender):
|
||||
def __init__(self):
|
||||
debug("Protocol init")
|
||||
self.state = STATE_INITIAL
|
||||
self.buf = ""
|
||||
self.supportedAuthMechs = [ AUTHMECH_ANON ]
|
||||
self.supportedAddrs = [ ADDR_DOMAINNAME ]
|
||||
self.enabledCommands = [ CMD_CONNECT ]
|
||||
self.peersock = None
|
||||
self.addressType = 0
|
||||
self.requestType = 0
|
||||
self.activeConns = {}
|
||||
self.pendingConns = {}
|
||||
self.transfered = 0 #nb of bytes already copied
|
||||
|
||||
def _startNegotiation(self):
|
||||
debug("_startNegotiation")
|
||||
self.state = STATE_TARGET_AUTH
|
||||
self.transport.write(struct.pack('!3B', SOCKS5_VER, 1, AUTHMECH_ANON))
|
||||
|
||||
def _parseNegotiation(self):
|
||||
debug("_parseNegotiation")
|
||||
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):
|
||||
debug("_parseUserPass")
|
||||
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):
|
||||
debug("sendErrorReply")
|
||||
# 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 addConnection(self, address, connection):
|
||||
info("Adding connection: %s, %s", address, connection)
|
||||
olist = self.pendingConns.get(address, [])
|
||||
if len(olist) <= 1:
|
||||
olist.append(connection)
|
||||
self.pendingConns[address] = olist
|
||||
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)
|
||||
self.pendingConns[address] = olist
|
||||
|
||||
def removeActiveConnection(self, address):
|
||||
del self.activeConns[address]
|
||||
|
||||
def _parseRequest(self):
|
||||
debug("_parseRequest")
|
||||
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 _makeRequest(self):
|
||||
debug("_makeRequest")
|
||||
self.state = STATE_TARGET_REQUEST
|
||||
sha1 = hashlib.sha1(self.sid + self.initiator_jid + self.target_jid).hexdigest()
|
||||
request = struct.pack('!5B%dsH' % len(sha1), SOCKS5_VER, CMD_CONNECT, 0, ADDR_DOMAINNAME, len(sha1), sha1, 0)
|
||||
self.transport.write(request)
|
||||
|
||||
def _parseRequestReply(self):
|
||||
debug("_parseRequestReply")
|
||||
try:
|
||||
ver, rep, 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 reply is OK
|
||||
if rep != REPLY_SUCCESS:
|
||||
self.loseConnection()
|
||||
return
|
||||
|
||||
debug("Saving file in %s.", self.data["dest_path"])
|
||||
self.dest_file = open(self.data["dest_path"], 'w')
|
||||
self.state = STATE_TARGET_READY
|
||||
self.activateCB(self.target_jid, self.initiator_jid, self.sid, self.IQ_id)
|
||||
|
||||
|
||||
except struct.error, why:
|
||||
return None
|
||||
|
||||
def connectionMade(self):
|
||||
debug("connectionMade (mode = %s)" % self.mode)
|
||||
self.host.registerProgressCB(self.transfert_id, self.getProgress)
|
||||
|
||||
if self.mode == "target":
|
||||
self.state = STATE_TARGET_INITIAL
|
||||
self._startNegotiation()
|
||||
|
||||
def connectRequested(self, addr, port):
|
||||
debug(("connectRequested"))
|
||||
# Check for special connect to the namespace -- this signifies that the client
|
||||
# is just checking to ensure it can connect to the streamhost
|
||||
if addr == "http://jabber.org/protocol/bytestreams":
|
||||
self.connectCompleted(addr, 0)
|
||||
self.transport.loseConnection()
|
||||
return
|
||||
|
||||
# Save addr, for cleanup
|
||||
self.addr = addr
|
||||
|
||||
# Check to see if the requested address is already
|
||||
# activated -- send an error if so
|
||||
if addr in self.activeConns:
|
||||
self.sendErrorReply(socks5.REPLY_CONN_NOT_ALLOWED)
|
||||
return
|
||||
|
||||
# Add this address to the pending connections
|
||||
if self.addConnection(addr, self):
|
||||
self.connectCompleted(addr, 0)
|
||||
self.transport.stopReading()
|
||||
else:
|
||||
self.sendErrorReply(socks5.REPLY_CONN_REFUSED)
|
||||
|
||||
def getProgress(self, data):
|
||||
"""Fill data with position of current transfert"""
|
||||
data["size"] = self.filesize
|
||||
try:
|
||||
data["position"] = str(self.dest_file.tell())
|
||||
except (ValueError, AttributeError):
|
||||
data["position"] = ""
|
||||
|
||||
def fileTransfered(self, d):
|
||||
info("File transfer completed, closing connection")
|
||||
self.transport.loseConnection()
|
||||
|
||||
def updateTransfered(self, data):
|
||||
self.transfered+=len(data)
|
||||
return data
|
||||
|
||||
def connectCompleted(self, remotehost, remoteport):
|
||||
debug("connectCompleted")
|
||||
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.dest_file=open(self.filepath)
|
||||
d=self.beginFileTransfer(self.dest_file, self.transport, self.updateTransfered)
|
||||
d.addCallback(self.fileTransfered)
|
||||
|
||||
def bindRequested(self, addr, port):
|
||||
pass
|
||||
|
||||
def authenticateUserPass(self, user, passwd):
|
||||
debug("User/pass: %s/%s", user, passwd)
|
||||
return True
|
||||
|
||||
def dataReceived(self, buf):
|
||||
if self.state == STATE_TARGET_READY:
|
||||
self.dest_file.write(buf)
|
||||
self.transfered+=len(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()
|
||||
if self.state == STATE_TARGET_AUTH:
|
||||
ver, method = struct.unpack('!BB', buf)
|
||||
self.buf = self.buf[2:]
|
||||
if ver!=SOCKS5_VER or method!=AUTHMECH_ANON:
|
||||
self.transport.loseConnection()
|
||||
else:
|
||||
self._makeRequest()
|
||||
if self.state == STATE_TARGET_REQUEST:
|
||||
self._parseRequestReply()
|
||||
|
||||
|
||||
def clientConnectionLost(self, reason):
|
||||
debug("clientConnectionLost")
|
||||
self.transport.loseConnection()
|
||||
|
||||
def connectionLost(self, reason):
|
||||
debug("connectionLost")
|
||||
self.host.removeProgressCB(self.transfert_id)
|
||||
if self.state == STATE_CONNECT_PENDING:
|
||||
self.removePendingConnection(self.addr, self)
|
||||
else:
|
||||
self.transport.unregisterProducer()
|
||||
if self.peersock != None:
|
||||
self.peersock.peersock = None
|
||||
self.peersock.transport.unregisterProducer()
|
||||
self.peersock = None
|
||||
self.removeActiveConnection(self.addr)
|
||||
|
||||
class Socks5ServerFactory(protocol.ServerFactory):
|
||||
protocol = SOCKSv5
|
||||
protocol.mode = "initiator" #FIXME: Q&D way, fix it
|
||||
|
||||
|
||||
def startedConnecting(self, connector):
|
||||
debug ("Socks 5 server connection started")
|
||||
|
||||
def clientConnectionLost(self, connector, reason):
|
||||
debug ("Socks 5 server connection lost (reason: %s)", reason)
|
||||
|
||||
class Socks5ClientFactory(protocol.ClientFactory):
|
||||
protocol = SOCKSv5
|
||||
protocol.mode = "target" #FIXME: Q&D way, fix it
|
||||
|
||||
def startedConnecting(self, connector):
|
||||
debug ("Socks 5 client connection started")
|
||||
|
||||
def clientConnectionLost(self, connector, reason):
|
||||
debug ("Socks 5 client connection lost (reason: %s)", reason)
|
||||
|
||||
|
||||
class XEP_0065():
|
||||
def __init__(self, host):
|
||||
info("Plugin XEP_0065 initialization")
|
||||
self.host = host
|
||||
debug("registering")
|
||||
self.server_factory = Socks5ServerFactory()
|
||||
self.server_factory.protocol.host = self.host #needed for progress CB
|
||||
self.client_factory = Socks5ClientFactory()
|
||||
host.add_IQ_cb("http://jabber.org/protocol/bytestreams", self.getFile)
|
||||
port = int(self.host.memory.getParamV("Port", "File Transfert"))
|
||||
info("Launching Socks5 Stream server on port %d", port)
|
||||
reactor.listenTCP(port, self.server_factory)
|
||||
|
||||
def setData(self, data, id):
|
||||
self.data = data
|
||||
self.transfert_id = id
|
||||
|
||||
def sendFile(self, id, filepath, size):
|
||||
#lauching socks5 initiator
|
||||
self.server_factory.protocol.mode = "initiator"
|
||||
self.server_factory.protocol.filepath = filepath
|
||||
self.server_factory.protocol.filesize = size
|
||||
self.server_factory.protocol.transfert_id = id
|
||||
|
||||
def getFile(self, stanza):
|
||||
"""Get file using byte stream"""
|
||||
SI_elem = stanza.firstChildElement()
|
||||
IQ_id = stanza['id']
|
||||
for element in SI_elem.elements():
|
||||
if element.name == "streamhost":
|
||||
info ("Stream proposed: host=[%s] port=[%s]", element['host'], element['port'])
|
||||
factory = self.client_factory
|
||||
self.server_factory.protocol.mode = "target"
|
||||
factory.protocol.host = self.host #needed for progress CB
|
||||
factory.protocol.data = self.data
|
||||
factory.protocol.transfert_id = self.transfert_id
|
||||
factory.protocol.filesize = self.data["size"]
|
||||
factory.protocol.sid = SI_elem['sid']
|
||||
factory.protocol.initiator_jid = element['jid']
|
||||
factory.protocol.target_jid = self.host.me.full()
|
||||
factory.protocol.IQ_id = IQ_id
|
||||
factory.protocol.activateCB = self.activateStream
|
||||
reactor.connectTCP(element['host'], int(element['port']), factory)
|
||||
|
||||
def activateStream(self, from_jid, to_jid, sid, IQ_id):
|
||||
debug("activating stream")
|
||||
result = domish.Element(('', 'iq'))
|
||||
result['type'] = 'result'
|
||||
result['id'] = IQ_id
|
||||
result['from'] = from_jid
|
||||
result['to'] = to_jid
|
||||
query = result.addElement('query', 'http://jabber.org/protocol/bytestreams')
|
||||
query['sid'] = sid
|
||||
streamhost = query.addElement('streamhost-used')
|
||||
streamhost['jid'] = to_jid #FIXME: use real streamhost
|
||||
self.host.xmlstream.send(result)
|
||||
|
||||
170
plugins/plugin_xep_0096.py
Executable file
170
plugins/plugin_xep_0096.py
Executable file
|
|
@ -0,0 +1,170 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
SAT plugin for managing xep-0096
|
||||
Copyright (C) 2009 Jérôme Poisson (goffi@goffi.org)
|
||||
|
||||
This program 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 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program 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 program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
from logging import debug, info, error
|
||||
from twisted.words.xish import domish
|
||||
from twisted.internet import protocol
|
||||
from twisted.words.protocols.jabber import client, jid, xmlstream, error
|
||||
import os.path
|
||||
from twisted.internet import reactor #FIXME best way ???
|
||||
|
||||
|
||||
PLUGIN_INFO = {
|
||||
"name": "XEP 0096 Plugin",
|
||||
"import_name": "XEP_0096",
|
||||
"type": "XEP",
|
||||
"dependencies": ["XEP_0065"],
|
||||
"main": "XEP_0096",
|
||||
"description": """Implementation of SI File Transfert"""
|
||||
}
|
||||
|
||||
class XEP_0096:
|
||||
def __init__(self, host):
|
||||
info("Plugin XEP_0096 initialization")
|
||||
self.host = host
|
||||
self._waiting_for_approval = {}
|
||||
host.add_IQ_cb("http://jabber.org/protocol/si", self.xep_96)
|
||||
host.bridge.register("sendFile", self.sendFile)
|
||||
|
||||
|
||||
def xep_96(self, IQ):
|
||||
info ("XEP-0096 management")
|
||||
SI_elem = IQ.firstChildElement()
|
||||
debug(SI_elem.toXml())
|
||||
filename = ""
|
||||
file_size = ""
|
||||
for element in SI_elem.elements():
|
||||
if element.name == "file":
|
||||
info ("File proposed: name=[%s] size=%s", element['name'], element['size'])
|
||||
filename = element["name"]
|
||||
file_size = element["size"]
|
||||
elif element.name == "feature":
|
||||
from_jid = IQ["from"]
|
||||
self._waiting_for_approval[IQ["id"]] = (element, from_jid, file_size)
|
||||
data={ "filename":filename, "from":from_jid, "size":file_size }
|
||||
self.host.askConfirmation(IQ["id"], "FILE_TRANSFERT", data, self.confirmationCB)
|
||||
|
||||
def confirmationCB(self, id, accepted, data):
|
||||
"""Called on confirmation answer"""
|
||||
if accepted:
|
||||
data['size'] = self._waiting_for_approval[id][2]
|
||||
self.host.plugins["XEP_0065"].setData(data, id)
|
||||
self.approved(id)
|
||||
else:
|
||||
debug ("Transfert [%s] refused", id)
|
||||
del(self._waiting_for_approval[id])
|
||||
|
||||
def approved(self, id):
|
||||
"""must be called when a file transfert has be accepted by client"""
|
||||
debug ("Transfert [%s] accepted", id)
|
||||
|
||||
if ( not self._waiting_for_approval.has_key(id) ):
|
||||
error ("Approved unknow id !")
|
||||
#TODO: manage this (maybe approved by several frontends)
|
||||
else:
|
||||
element, from_id, size = self._waiting_for_approval[id]
|
||||
del(self._waiting_for_approval[id])
|
||||
self.negociate(element, id, from_id)
|
||||
|
||||
def negociate(self, feat_elem, id, to_jid):
|
||||
#TODO: put this in a plugin
|
||||
#FIXME: over ultra mega ugly, need to be generic
|
||||
info ("Feature negociation")
|
||||
data = feat_elem.firstChildElement()
|
||||
field = data.firstChildElement()
|
||||
#FIXME: several options ! Q&D code for test only
|
||||
option = field.firstChildElement()
|
||||
value = option.firstChildElement()
|
||||
if unicode(value) == "http://jabber.org/protocol/bytestreams":
|
||||
#ugly, as usual, need to be entirely rewritten (just for test !)
|
||||
result = domish.Element(('', 'iq'))
|
||||
result['type'] = 'result'
|
||||
result['id'] = id
|
||||
result['to'] = to_jid
|
||||
si = result.addElement('si', 'http://jabber.org/protocol/si')
|
||||
file = si.addElement('file', 'http://jabber.org/protocol/si/profile/file-transfer')
|
||||
feature = si.addElement('feature', 'http://jabber.org/protocol/feature-neg')
|
||||
x = feature.addElement('x', 'jabber:x:data')
|
||||
x['type'] = 'submit'
|
||||
field = x.addElement('field')
|
||||
field['var'] = 'stream-method'
|
||||
value = field.addElement('value')
|
||||
value.addContent('http://jabber.org/protocol/bytestreams')
|
||||
self.host.xmlstream.send(result)
|
||||
|
||||
def fileCB(self, answer):
|
||||
if answer['type']=="result": #FIXME FIXME FIXME ugly ugly ugly ! and temp FIXME FIXME FIXME
|
||||
info("SENDING UGLY ANSWER")
|
||||
offer=client.IQ(self.host.xmlstream,'set')
|
||||
offer["from"]=self.host.me.full()
|
||||
offer["to"]=answer['from']
|
||||
query=offer.addElement('query', 'http://jabber.org/protocol/bytestreams')
|
||||
query['mode']='tcp'
|
||||
streamhost=query.addElement('streamhost')
|
||||
streamhost['host']=self.host.memory.getParamV("IP", "File Transfert")
|
||||
streamhost['port']=self.host.memory.getParamV("Port", "File Transfert")
|
||||
streamhost['jid']=self.host.me.full()
|
||||
offer.send()
|
||||
|
||||
|
||||
|
||||
|
||||
def sendFile(self, to, filepath):
|
||||
"""send a file using XEP-0096
|
||||
Return an unique id to identify the transfert
|
||||
"""
|
||||
debug ("sendfile (%s) to %s", filepath, to )
|
||||
print type(filepath), type(to)
|
||||
|
||||
statinfo = os.stat(filepath)
|
||||
|
||||
offer=client.IQ(self.host.xmlstream,'set')
|
||||
debug ("Transfert ID: %s", offer["id"])
|
||||
|
||||
self.host.plugins["XEP_0065"].sendFile(offer["id"], filepath, str(statinfo.st_size))
|
||||
|
||||
offer["from"]=self.host.me.full()
|
||||
offer["to"]=jid.JID(to).full()
|
||||
si=offer.addElement('si','http://jabber.org/protocol/si')
|
||||
si["mime-type"]='text/plain'
|
||||
si["profile"]='http://jabber.org/protocol/si/profile/file-transfer'
|
||||
file = si.addElement('file', 'http://jabber.org/protocol/si')
|
||||
file['name']=os.path.basename(filepath)
|
||||
file['size']=str(statinfo.st_size)
|
||||
|
||||
###
|
||||
# FIXME: Ugly temporary hard coded implementation of XEP-0020 & XEP-0004,
|
||||
# Need to be recoded elsewhere in a more generic way
|
||||
###
|
||||
|
||||
feature=si.addElement('feature', "http://jabber.org/protocol/feature-neg")
|
||||
x=feature.addElement('x', "jabber:x:data")
|
||||
x['type']='form'
|
||||
field=x.addElement('field')
|
||||
field['type']='list-single'
|
||||
field['var']='stream-method'
|
||||
option = field.addElement('option')
|
||||
value = option.addElement('value', content='http://jabber.org/protocol/bytestreams')
|
||||
|
||||
offer.addCallback(self.fileCB)
|
||||
offer.send()
|
||||
return offer["id"] #XXX: using IQ id as file transfert id seems OK as IQ id are required
|
||||
|
||||
Loading…
Reference in a new issue