diff src/plugins/plugin_xep_0065.py @ 1758:a66d34353f34

plugin XEP-0260, XEP-0065: fixed session hash handling: with XEP-0260, we need to manage 2 hashes (one with SHA1(SID + Initiator JID + Responder JID) and the other with SHA1(SID + Responder JID + Initiator JID)). This was not handled correclty, resulting in transfer failure in several cases.
author Goffi <goffi@goffi.org>
date Thu, 17 Dec 2015 22:37:59 +0100
parents abd6d6f89006
children 81923b3f8b14
line wrap: on
line diff
--- a/src/plugins/plugin_xep_0065.py	Thu Dec 17 22:37:58 2015 +0100
+++ b/src/plugins/plugin_xep_0065.py	Thu Dec 17 22:37:59 2015 +0100
@@ -283,15 +283,15 @@
         self.factory.startTransfer(session_hash, chunk_size=chunk_size)
 
 
-def getSessionHash(from_jid, to_jid, sid):
+def getSessionHash(requester_jid, target_jid, sid):
     """Calculate SHA1 Hash according to XEP-0065 §5.3.2
 
-    @param from_jid(jid.JID): jid of the requester
-    @param to_jid(jid.JID): jid of the target
+    @param requester_jid(jid.JID): jid of the requester (the one which activate the proxy)
+    @param target_jid(jid.JID): jid of the target
     @param sid(unicode): session id
     @return (str): hash
     """
-    return hashlib.sha1((sid + from_jid.full() + to_jid.full()).encode('utf-8')).hexdigest()
+    return hashlib.sha1((sid + requester_jid.full() + target_jid.full()).encode('utf-8')).hexdigest()
 
 
 class SOCKSv5(protocol.Protocol, FileSender):
@@ -617,14 +617,15 @@
 class Socks5ClientFactory(protocol.ClientFactory):
     protocol = SOCKSv5
 
-    def __init__(self, parent, session_hash, profile):
+    def __init__(self, parent, session, session_hash, profile):
         """Init the Client Factory
 
-        @param session_hash(unicode): hash of the session
-            hash is the same as hostname computer in XEP-0065 § 5.3.2 #1
+        @param session(dict): session data
+        @param session_hash(unicode): hash used for peer_connection
+            hash is the same as hostname computed in XEP-0065 § 5.3.2 #1
         @param profile(unciode): %(doc_profile)s
         """
-        self.session = parent.getSession(session_hash, profile)
+        self.session = session
         self.session_hash = session_hash
         self.profile = profile
         self.connection = defer.Deferred()
@@ -862,18 +863,28 @@
         candidate.factory.connector = connector
         return candidate.factory.connection
 
-    def connectCandidate(self, candidate, session_hash, delay=None, profile=C.PROF_KEY_NONE):
+    def connectCandidate(self, candidate, session_hash, peer_session_hash=None, delay=None, profile=C.PROF_KEY_NONE):
         """Connect to a candidate
 
         Connection will be done with a Socks5ClientFactory
         @param candidate(Candidate): candidate to connect to
         @param session_hash(unicode): hash of the session
-            hash is the same as hostname computer in XEP-0065 § 5.3.2 #1
+            hash is the same as hostname computed in XEP-0065 § 5.3.2 #1
+        @param peer_session_hash(unicode, None): hash used with the peer
+            None to use session_hash.
+            None must be used in 2 cases:
+                - when XEP-0065 is used with XEP-0096
+                - when a peer connect to a proxy *he proposed himself*
+            in practice, peer_session_hash is only used by tryCandidates
         @param delay(None, float): optional delay to wait before connection, in seconds
         @param profile: %(doc_profile)s
         @return (D): Deferred launched when TCP connection + Socks5 connection is done
         """
-        factory = Socks5ClientFactory(self, session_hash, profile)
+        if peer_session_hash is None:
+            # for XEP-0065, only one hash is needed
+            peer_session_hash = session_hash
+        session = self.getSession(session_hash, profile)
+        factory = Socks5ClientFactory(self, session, peer_session_hash, profile)
         candidate.factory = factory
         if delay is None:
             d = defer.succeed(candidate.host)
@@ -883,14 +894,14 @@
         d.addCallback(self._addConnector, candidate)
         return d
 
-    def tryCandidates(self, candidates, session_hash, connection_cb=None, connection_eb=None, profile=C.PROF_KEY_NONE):
+    def tryCandidates(self, candidates, session_hash, peer_session_hash, connection_cb=None, connection_eb=None, profile=C.PROF_KEY_NONE):
         defers_list = []
 
         for candidate in candidates:
             delay = CANDIDATE_DELAY * len(defers_list)
             if candidate.type == XEP_0065.TYPE_PROXY:
                 delay += CANDIDATE_DELAY_PROXY
-            d = self.connectCandidate(candidate, session_hash, delay, profile)
+            d = self.connectCandidate(candidate, session_hash, peer_session_hash, delay, profile)
             if connection_cb is not None:
                 d.addCallback(lambda dummy, candidate=candidate, profile=profile: connection_cb(candidate, profile))
             if connection_eb is not None:
@@ -899,12 +910,14 @@
 
         return defers_list
 
-    def getBestCandidate(self, candidates, session_hash, profile=C.PROF_KEY_NONE):
+    def getBestCandidate(self, candidates, session_hash, peer_session_hash=None, 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
+            hash is the same as hostname computed in XEP-0065 § 5.3.2 #1
+        @param peer_session_hash(unicode, None): hash of the other peer
+            only useful for XEP-0260, must be None for XEP-0065 streamhost candidates
         @param profile: %(doc_profile)s
         @return (D(None, Candidate)): best candidate or None if none can connect
         """
@@ -934,7 +947,7 @@
             good_candidates = [c for c in candidates if c]
             return good_candidates[0] if good_candidates else None
 
-        defer_candidates = self.tryCandidates(candidates, session_hash, connectionCb, connectionEb, profile)
+        defer_candidates = self.tryCandidates(candidates, session_hash, peer_session_hash, connectionCb, connectionEb, profile)
         d_list = defer.DeferredList(defer_candidates)
         d_list.addCallback(allTested)
         return d_list
@@ -966,12 +979,8 @@
             ))
 
         try:
-            # XXX: we need to be sure that hash is removed from self.hash_profiles_map
-            #      ONLY if it's the profile requesting the session killing
-            #      otherwise, this will result in a missing hash when the 2 peers
-            #      are on the same instance
-            if self.hash_profiles_map[session_hash] == client.profile:
-                del self.hash_profiles_map[session_hash]
+            assert self.hash_profiles_map[session_hash] == client.profile
+            del self.hash_profiles_map[session_hash]
         except KeyError:
             pass
 
@@ -1121,7 +1130,7 @@
         """Return session data
 
         @param session_hash(unicode): hash of the session
-            hash is the same as hostname computer in XEP-0065 § 5.3.2 #1
+            hash is the same as hostname computed in XEP-0065 § 5.3.2 #1
         @param profile(None, unicode): profile of the peer
             None is used only if profile is unknown (this is only the case
             for incoming request received by Socks5ServerFactory). None must
@@ -1165,23 +1174,8 @@
         if file_obj is not None:
             session_data['file'] = file_obj
 
-        if session_hash in self.hash_profiles_map:
-            # The only case when 2 profiles want to register the same hash
-            # is when they are on the same instance
-            log.info(u"Both Socks5 peers are on the same instance")
-            # XXX:If both peers are on the same instance, they'll register the same
-            #     session_hash, so we'll have 2 profiles for the same hash. The first
-            #     one will be the responder (and so the second one the initiator).
-            #     As we'll keep the initiator choosed candidate (see XEP-0260 § 2.4 #4),
-            #     responder will handle the Socks5 server. Only the server will use
-            #     self.hash_profiles_map to get the profile, so we can ignore the second
-            #     one (the initiator profile).
-            #     There is no easy way to known if the incoming connection
-            #     to the Socks5Server is from initiator or responder, so this seams a
-            #     reasonable workaround.
-            #     NOTE: this workaround is only used with XEP-0260
-        else:
-            self.hash_profiles_map[session_hash] = profile
+        assert session_hash not in self.hash_profiles_map
+        self.hash_profiles_map[session_hash] = profile
 
         return session_data
 
@@ -1234,7 +1228,7 @@
         for candidate in candidates:
             log.info(u"Candidate proposed: {}".format(candidate))
 
-        d = self.getBestCandidate(candidates, session_data['hash'], profile)
+        d = self.getBestCandidate(candidates, session_data['hash'], profile=profile)
         d.addCallback(self._ackStream, iq_elt, session_data, client)
 
     def _ackStream(self, candidate, iq_elt, session_data, client):