plugins XEP-0020, XEP-0065, XEP-0095, XEP-0096: fixed file copy with Stream Initiation:

/!\ range is not working yet
    /!\ pipe plugin is broken for now
This commit is contained in:
Goffi 2015-11-11 18:19:49 +01:00
commit dc0dbb149c

View file

@ -63,13 +63,12 @@ from sat.tools import sat_defer
from twisted.internet import protocol
from twisted.internet import reactor
from twisted.internet import error as internet_error
from twisted.words.protocols.jabber import jid, client as jabber_client
from twisted.words.protocols.jabber import error as jabber_error
from twisted.words.protocols.jabber import jid
from twisted.words.protocols.jabber import xmlstream
from twisted.protocols.basic import FileSender
from twisted.words.xish import domish
from twisted.internet import defer
from twisted.python import failure
from sat.core.exceptions import ProfileNotInCacheError
from collections import namedtuple
import struct
import hashlib
@ -432,7 +431,6 @@ class SOCKSv5(protocol.Protocol, FileSender):
return None
def _makeRequest(self):
# sha1 = getSessionHash(self.data["from"], self.data["to"], self.sid)
hash_ = self._session_hash
request = struct.pack('!5B%dsH' % len(hash_), SOCKS5_VER, CMD_CONNECT, 0, ADDR_DOMAINNAME, len(hash_), hash_, 0)
self.transport.write(request)
@ -464,13 +462,8 @@ class SOCKSv5(protocol.Protocol, FileSender):
self.loseConnection()
return
# if self.factory.proxy:
# self.state = STATE_READY
# self.factory.activateCb(self.sid, self.factory.iq_id, self.startTransfer, self.profile)
# else:
self.state = STATE_READY
self.connection.callback(None)
# self.factory.activateCb(self.sid, self.factory.iq_id, self.profile)
except struct.error:
# The buffer is probably not complete, we need to wait more
@ -489,9 +482,6 @@ class SOCKSv5(protocol.Protocol, FileSender):
.format(host=self.transport.getPeer().host))
return
self._session_hash = addr
# self.sid, self.profile = self.factory.hash_profiles_map[addr]
# client = self.factory.host.getClient(self.profile)
# client.xep_0065_current_stream[self.sid]["start_transfer_cb"] = self.startTransfer
self.connectCompleted(addr, 0)
def startTransfer(self):
@ -546,11 +536,6 @@ class SOCKSv5(protocol.Protocol, FileSender):
def connectionLost(self, reason):
log.debug(u"Socks5 connection lost: {}".format(reason.value))
# self.transport.unregisterProducer()
# if self.peersock is not None:
# self.peersock.peersock = None
# self.peersock.transport.unregisterProducer()
# self.peersock = None
if self.state != STATE_READY:
self.connection.errback(reason)
if self.server_mode :
@ -620,7 +605,6 @@ class Socks5ServerFactory(protocol.ServerFactory):
class Socks5ClientFactory(protocol.ClientFactory):
protocol = SOCKSv5
# def __init__(self, stream_data, sid, iq_id, activateCb, finishedCb, proxy=False, profile=C.PROF_KEY_NONE):
def __init__(self, parent, session_hash, profile):
"""Init the Client Factory
@ -635,13 +619,6 @@ class Socks5ClientFactory(protocol.ClientFactory):
self._protocol_instance = None
self.connector = None
self._discarded = False
# self.data = stream_data[sid]
# self.sid = sid
# self.iq_id = iq_id
# self.activateCb = activateCb
# self.finishedCb = finishedCb
# self.proxy = proxy
# self.profile = profile
def discard(self):
"""Disconnect the client
@ -671,7 +648,6 @@ class Socks5ClientFactory(protocol.ClientFactory):
self.getSession()[DEFER_KEY].callback(None)
else:
self.getSession()[DEFER_KEY].errback(reason)
# self.finishedCb(self.sid, reason.type == internet_error.ConnectionDone, self.profile) # TODO: really check if the state is actually successful
def buildProtocol(self, addr):
log.debug(("Socks 5 client connection started"))
@ -719,7 +695,7 @@ class XEP_0065(object):
def profileConnected(self, profile):
client = self.host.getClient(profile)
client.xep_0065_current_stream = {} # key: stream_id, value: session_data(dict)
client.xep_0065_sid_session = {} # key: stream_id, value: session_data(dict)
client._s5b_sessions = {}
def getSessionHash(self, from_jid, to_jid, sid):
@ -776,7 +752,7 @@ class XEP_0065(object):
notFound(server)
iq_elt = client.IQ('get')
iq_elt['to'] = proxy.full()
iq_elt.addElement('query', NS_BS)
iq_elt.addElement((NS_BS, 'query'))
try:
result_elt = yield iq_elt.send()
@ -914,6 +890,14 @@ class XEP_0065(object):
return defers_list
def getBestCandidate(self, candidates, session_hash, profile=C.PROF_KEY_NONE):
"""Get best candidate (according to priority) which can connect
@param candidates(iterable[Candidate]): candidates to test
@param session_hash(unicode): hash of the session
hash is the same as hostname computer in XEP-0065 § 5.3.2 #1
@param profile: %(doc_profile)s
@return (D(None, Candidate)): best candidate or None if none can connect
"""
defer_candidates = None
def connectionCb(candidate, profile):
@ -947,7 +931,7 @@ class XEP_0065(object):
def _timeOut(self, sid, client):
"""Delecte current_stream id, called after timeout
@param id: id of client.xep_0065_current_stream"""
@param id: id of client.xep_0065_sid_session"""
log.info(_("Socks5 Bytestream: TimeOut reached for id {sid} [{profile}]").format(
sid=sid, profile=client.profile))
self._killSession(sid, client, u"TIMEOUT")
@ -961,7 +945,7 @@ class XEP_0065(object):
else, will be used to call failure_cb
"""
try:
session = client.xep_0065_current_stream[sid]
session = client.xep_0065_sid_session[sid]
except KeyError:
log.warning(_("kill id called on a non existant id"))
return
@ -976,7 +960,7 @@ class XEP_0065(object):
if session['timer'].active():
session['timer'].cancel()
del client.xep_0065_current_stream[sid]
del client.xep_0065_sid_session[sid]
# FIXME: to check
try:
@ -1004,105 +988,73 @@ class XEP_0065(object):
@param successCb: method to call when stream successfuly finished
@param failureCb: method to call when something goes wrong
@param profile: %(doc_profile)s
@return (D): Deferred fired when session is finished
"""
client = self.host.getClient(profile)
session_data = self._createSession(file_obj, to_jid, sid, client.profile)
session_data = self._createSession(file_obj, to_jid, sid, True, client.profile)
session_data["to"] = to_jid
session_data["xmlstream"] = client.xmlstream
hash_ = session_data["hash"] = getSessionHash(client.jid, to_jid, sid)
session_data[client] = client
self.hash_profiles_map[hash_] = (sid, profile)
def gotCandidates(candidates):
session_data['candidates'] = candidates
iq_elt = client.IQ()
iq_elt["from"] = client.jid.full()
iq_elt["to"] = to_jid.full()
query_elt = iq_elt.addElement((NS_BS, 'query'))
query_elt['mode'] = 'tcp'
query_elt['sid'] = sid
iq_elt = jabber_client.IQ(client.xmlstream, 'set')
iq_elt["from"] = client.jid.full()
iq_elt["to"] = to_jid.full()
query_elt = iq_elt.addElement('query', NS_BS)
query_elt['mode'] = 'tcp'
query_elt['sid'] = sid
for candidate in candidates:
streamhost = query_elt.addElement('streamhost')
streamhost['host'] = candidate.host
streamhost['port'] = str(candidate.port)
streamhost['jid'] = candidate.jid.full()
#first streamhost: direct connection
streamhost = query_elt.addElement('streamhost')
streamhost['host'] = self.host.memory.getParamA("IP", "File Transfer")
streamhost['port'] = self.host.memory.getParamA("Port", "File Transfer")
streamhost['jid'] = client.jid.full()
d = iq_elt.send()
args = [session_data, client]
d.addCallbacks(self._IQNegotiationCb, self._IQNegotiationEb, args, None, args)
#second streamhost: mediated connection, using proxy
streamhost = query_elt.addElement('streamhost')
streamhost['host'] = self.host.memory.getParamA("Proxy host", "File Transfer", profile_key=profile)
streamhost['port'] = self.host.memory.getParamA("Proxy port", "File Transfer", profile_key=profile)
streamhost['jid'] = self.host.memory.getParamA("Proxy", "File Transfer", profile_key=profile)
iq_elt.addCallback(self._IQOpen, session_data, client)
iq_elt.send()
self.getCandidates(profile).addCallback(gotCandidates)
return session_data[DEFER_KEY]
def _IQOpen(self, session_data, client, iq_elt):
def _IQNegotiationCb(self, iq_elt, session_data, client):
"""Called when the result of open iq is received
@param session_data(dict): data of the session
@param client: %(doc_client)s
@param iq_elt(domish.Element): <iq> result
"""
sid = session_data['id']
if iq_elt["type"] == "error":
log.warning(_("Socks5 transfer failed"))
# FIXME: must clean session
return
try:
session_data = client.xep_0065_current_stream[sid]
file_obj = session_data["file_obj"]
timer = session_data["timer"]
except KeyError:
raise exceptions.InternalError
timer.reset(TIMEOUT)
query_elt = iq_elt.elements(NS_BS, 'query').next()
streamhost_elts = list(query_elt.elements(NS_BS, 'streamhost-used'))
if not streamhost_elts:
log.warning(_("No streamhost found in stream query"))
query_elt = iq_elt.elements(NS_BS, 'query').next()
streamhost_used_elt = query_elt.elements(NS_BS, 'streamhost-used').next()
except StopIteration:
log.warning(u"No streamhost found in stream query")
# FIXME: must clean session
return
# FIXME: must be cleaned !
streamhost_jid = streamhost_elts[0]['jid']
if streamhost_jid != client.jid.full():
log.debug(_("A proxy server is used"))
proxy_host = self.host.memory.getParamA("Proxy host", "File Transfer", profile_key=client.profile)
proxy_port = self.host.memory.getParamA("Proxy port", "File Transfer", profile_key=client.profile)
proxy_jid = self.host.memory.getParamA("Proxy", "File Transfer", profile_key=client.profile)
if proxy_jid != streamhost_jid:
log.warning(_("Proxy jid is not the same as in parameters, this should not happen"))
return
factory = Socks5ClientFactory(client.xep_0065_current_stream, sid, None, self.activateProxyStream, lambda sid, success, profile: self._killSession(sid, client), True, client.profile)
reactor.connectTCP(proxy_host, int(proxy_port), factory)
else:
session_data["start_transfer_cb"](file_obj) # We now activate the stream
def activateProxyStream(self, sid, iq_id, start_transfer_cb, profile):
log.debug(_("activating stream"))
client = self.host.getClient(profile)
session_data = client.xep_0065_current_stream[sid]
iq_elt = client.IQ(client.xmlstream, 'set')
iq_elt["from"] = client.jid.full()
iq_elt["to"] = self.host.memory.getParamA("Proxy", "File Transfer", profile_key=profile)
query_elt = iq_elt.addElement('query', NS_BS)
query_elt['sid'] = sid
query_elt.addElement('activate', content=session_data['to'].full())
iq_elt.addCallback(self.proxyResult, sid, start_transfer_cb, session_data['file_obj'])
iq_elt.send()
def proxyResult(self, sid, start_transfer_cb, file_obj, iq_elt):
if iq_elt['type'] == 'error':
log.warning(_("Can't activate the proxy stream"))
streamhost_jid = jid.JID(streamhost_used_elt['jid'])
try:
candidate = (c for c in session_data['candidates'] if c.jid == streamhost_jid).next()
except StopIteration:
log.warning(u"Candidate [{jid}] is unknown !".format(jid=streamhost_jid.full()))
return
if candidate.type == XEP_0065.TYPE_PROXY:
log.info(u"A Socks5 proxy is used")
d = self.connectCandidate(candidate, session_data['hash'], profile=client.profile)
d.addCallback(lambda dummy: candidate.activate(session_data['id'], session_data['peer_jid'], client))
d.addErrback(self._activationEb)
else:
start_transfer_cb(file_obj)
d = defer.succeed(None)
d.addCallback(lambda dummy: candidate.startTransfer(session_data['hash']))
def _activationEb(self, failure):
log.warning(u"Proxy activation error: {}".format(failure.value))
def _IQNegotiationEb(self, stanza_err, session_data, client):
log.warning(u"Socks5 transfer failed: {}".format(stanza_err.condition))
# FIXME: must clean session
def createSession(self, *args, **kwargs):
"""like [_createSession] but return the session deferred instead of the whole session
@ -1111,26 +1063,34 @@ class XEP_0065(object):
"""
return self._createSession(*args, **kwargs)[DEFER_KEY]
def _createSession(self, file_obj, to_jid, sid, profile):
def _createSession(self, file_obj, to_jid, sid, requester=False, profile=C.PROF_KEY_NONE):
"""Called when a bytestream is imminent
@param file_obj(file): File object where data will be written
@param to_jid(jid.JId): jid of the other peer
@param sid(unicode): session id
@param initiator(bool): if True, this session is create by initiator
@param profile: %(doc_profile)s
@return (dict): session data
"""
client = self.host.getClient(profile)
if sid in client.xep_0065_current_stream:
if sid in client.xep_0065_sid_session:
raise exceptions.ConflictError(u'A session with this id already exists !')
session_data = client.xep_0065_current_stream[sid] = \
if requester:
session_hash = getSessionHash(client.jid, to_jid, sid)
session_data = self._registerHash(session_hash, file_obj, profile)
else:
session_hash = getSessionHash(to_jid, client.jid, sid)
session_data = client._s5b_sessions[session_hash] = {
DEFER_KEY: defer.Deferred(),
}
client.xep_0065_sid_session[sid] = session_data
session_data.update(
{'id': sid,
DEFER_KEY: defer.Deferred(),
'to': to_jid,
'file_obj': file_obj,
'seq': -1, # FIXME: to check
'timer': reactor.callLater(TIMEOUT, self._timeOut, sid, client),
}
'peer_jid': to_jid,
'file': file_obj,
'hash': session_hash,
})
return session_data
@ -1156,7 +1116,7 @@ class XEP_0065(object):
return client._s5b_sessions[session_hash]
def registerHash(self, *args, **kwargs):
"""like [_registerHash] but resutrn the session deferred instead of the whole session
"""like [_registerHash] but resturn the session deferred instead of the whole session
session deferred is fired when transfer is finished
"""
return self._registerHash(*args, **kwargs)[DEFER_KEY]
@ -1195,91 +1155,62 @@ class XEP_0065(object):
return session_data
def streamQuery(self, iq_elt, profile):
"""Get file using byte stream"""
log.debug(_("BS stream query"))
log.debug(u"BS stream query")
client = self.host.getClient(profile)
if not client:
raise ProfileNotInCacheError
xmlstream = client.xmlstream
iq_elt.handled = True
query_elt = iq_elt.firstChildElement()
sid = query_elt.getAttribute("sid")
streamhost_elts = filter(lambda elt: elt.name == 'streamhost', query_elt.elements())
if not sid in client.xep_0065_current_stream:
log.warning(_(u"Ignoring unexpected BS transfer: %s" % sid))
self.sendNotAcceptableError(iq_elt['id'], iq_elt['from'], xmlstream)
return
client.xep_0065_current_stream[sid]['timer'].cancel()
client.xep_0065_current_stream[sid]["to"] = jid.JID(iq_elt["to"])
client.xep_0065_current_stream[sid]["xmlstream"] = xmlstream
query_elt = iq_elt.elements(NS_BS, 'query').next()
try:
sid = query_elt['sid']
except KeyError:
log.warning(u"Invalid bystreams request received")
return client.sendError(iq_elt, "bad-request")
streamhost_elts = list(query_elt.elements(NS_BS, 'streamhost'))
if not streamhost_elts:
log.warning(_(u"No streamhost found in stream query %s" % sid))
self.sendBadRequestError(iq_elt['id'], iq_elt['from'], xmlstream)
return
return client.sendError(iq_elt, "bad-request")
streamhost_elt = streamhost_elts[0] # TODO: manage several streamhost elements case
sh_host = streamhost_elt.getAttribute("host")
sh_port = streamhost_elt.getAttribute("port")
sh_jid = streamhost_elt.getAttribute("jid")
if not sh_host or not sh_port or not sh_jid:
log.warning(_("incomplete streamhost element"))
self.sendBadRequestError(iq_elt['id'], iq_elt['from'], xmlstream)
return
try:
session_data = client.xep_0065_sid_session[sid]
except KeyError:
log.warning(u"Ignoring unexpected BS transfer: {}".format(sid))
return client.sendError(iq_elt, 'not-acceptable')
client.xep_0065_current_stream[sid]["streamhost"] = (sh_host, sh_port, sh_jid)
peer_jid = session_data["peer_jid"] = jid.JID(iq_elt["from"])
log.info(_("Stream proposed: host=[%(host)s] port=[%(port)s]") % {'host': sh_host, 'port': sh_port})
factory = Socks5ClientFactory(client.xep_0065_current_stream, sid, iq_elt["id"], self.activateStream, lambda sid, success, profile: self._killSession(sid, client), profile=profile)
reactor.connectTCP(sh_host, int(sh_port), factory)
candidates = []
nb_sh = len(streamhost_elts)
for idx, sh_elt in enumerate(streamhost_elts):
try:
host, port, jid_ = sh_elt['host'], sh_elt['port'], jid.JID(sh_elt['jid'])
except KeyError:
log.warning(u"malformed streamhost element")
return client.sendError(iq_elt, "bad-request")
priority = nb_sh - idx
if jid_.userhostJID() != peer_jid.userhostJID():
type_ = XEP_0065.TYPE_PROXY
else:
type_ = XEP_0065.TYPE_DIRECT
candidates.append(Candidate(host, port, type_, priority, jid_))
def activateStream(self, sid, iq_id, profile):
client = self.host.getClient(profile)
log.debug(_("activating stream"))
result = domish.Element((None, 'iq'))
session_data = client.xep_0065_current_stream[sid]
result['type'] = 'result'
result['id'] = iq_id
result['from'] = session_data["to"].full()
result['to'] = session_data["from"].full()
query = result.addElement('query', NS_BS)
query['sid'] = sid
streamhost = query.addElement('streamhost-used')
streamhost['jid'] = session_data["streamhost"][2]
session_data["xmlstream"].send(result)
for candidate in candidates:
log.info(u"Candidate proposed: {}".format(candidate))
def sendNotAcceptableError(self, iq_id, to_jid, xmlstream):
"""Not acceptable error used when the stream is not expected or something is going wrong
@param iq_id: IQ id
@param to_jid: addressee
@param xmlstream: XML stream to use to send the error"""
result = domish.Element((None, 'iq'))
result['type'] = 'result'
result['id'] = iq_id
result['to'] = to_jid
error_el = result.addElement('error')
error_el['type'] = 'modify'
error_el.addElement(('urn:ietf:params:xml:ns:xmpp-stanzas', 'not-acceptable'))
xmlstream.send(result)
d = self.getBestCandidate(candidates, session_data['hash'], profile)
d.addCallback(self._ackStream, iq_elt, session_data, client)
def sendBadRequestError(self, iq_id, to_jid, xmlstream):
"""Not acceptable error used when the stream is not expected or something is going wrong
@param iq_id: IQ id
@param to_jid: addressee
@param xmlstream: XML stream to use to send the error"""
result = domish.Element((None, 'iq'))
result['type'] = 'result'
result['id'] = iq_id
result['to'] = to_jid
error_el = result.addElement('error')
error_el['type'] = 'cancel'
error_el.addElement(('urn:ietf:params:xml:ns:xmpp-stanzas', 'bad-request'))
xmlstream.send(result)
def _ackStream(self, candidate, iq_elt, session_data, client):
if candidate is None:
log.info("No streamhost candidate worked, we have to end negotiation")
return client.sendError(iq_elt, 'item-not-found')
log.debug(u"activating stream")
result_elt = xmlstream.toResponse(iq_elt, 'result')
query_elt = result_elt.addElement((NS_BS, 'query'))
query_elt['sid'] = session_data['id']
streamhost_used_elt = query_elt.addElement('streamhost-used')
streamhost_used_elt['jid'] = candidate.jid.full()
client.xmlstream.send(result_elt)
class XEP_0065_handler(XMPPHandler):