From 376777091e7cdc9d021595481f517ae182cae6c5 Mon Sep 17 00:00:00 2001
From: Jefry Lagrange <jefry.reyes@gmail.com>
Date: Sun, 15 Jan 2012 19:37:00 -0500
Subject: [PATCH] support for xep-300

---
 src/common/gajim.py          |  4 +-
 src/common/jingle.py         | 14 ++++++
 src/common/jingle_ft.py      | 39 +++++++++++++---
 src/common/jingle_session.py | 27 +++++++++--
 src/common/xmpp/protocol.py  | 91 ++++++++++++++++++++++++++++++++++--
 5 files changed, 161 insertions(+), 14 deletions(-)

diff --git a/src/common/gajim.py b/src/common/gajim.py
index 83ae590f74..6face0fbf5 100644
--- a/src/common/gajim.py
+++ b/src/common/gajim.py
@@ -208,7 +208,9 @@ gajim_common_features = [xmpp.NS_BYTESTREAM, xmpp.NS_SI, xmpp.NS_FILE,
         'jabber:iq:gateway', xmpp.NS_LAST, xmpp.NS_PRIVACY, xmpp.NS_PRIVATE,
         xmpp.NS_REGISTER, xmpp.NS_VERSION, xmpp.NS_DATA, xmpp.NS_ENCRYPTED, 'msglog',
         'sslc2s', 'stringprep', xmpp.NS_PING, xmpp.NS_TIME_REVISED, xmpp.NS_SSN,
-        xmpp.NS_MOOD, xmpp.NS_ACTIVITY, xmpp.NS_NICK, xmpp.NS_ROSTERX, xmpp.NS_SECLABEL]
+        xmpp.NS_MOOD, xmpp.NS_ACTIVITY, xmpp.NS_NICK, xmpp.NS_ROSTERX, xmpp.NS_SECLABEL,
+        xmpp.NS_HASHES, xmpp.NS_HASHES_MD5, xmpp.NS_HASHES_SHA1,
+        xmpp.NS_HASHES_SHA256, xmpp.NS_HASHES_SHA512]
 
 # Optional features gajim supports per account
 gajim_optional_features = {}
diff --git a/src/common/jingle.py b/src/common/jingle.py
index 6a7519e000..3f3c8f7262 100644
--- a/src/common/jingle.py
+++ b/src/common/jingle.py
@@ -151,10 +151,24 @@ class ConnectionJingle(object):
         file_props['sid'] = jingle.sid
         c = JingleFileTransfer(jingle, file_props=file_props,
                                use_security=use_security)
+        c.hash_algo = self.__hash_support(contact) 
         jingle.add_content('file' + helpers.get_random_string_16(), c)
         jingle.start_session()
         return c.transport.sid
 
+    def __hash_support(self, contact):
+        
+        if contact.supports(xmpp.NS_HASHES):
+            if contact.supports(xmpp.NS_HASHES_MD5):
+                return 'md5'
+            elif contact.supports(xmpp.NS_HASHES_SHA1):
+                return 'sha-1'
+            elif contact.supports(xmpp.NS_HASHES_SHA256):
+                return 'sha-256'
+            elif contact.supports(xmpp.NS_HASHES_SHA512):
+                return 'sha-512'
+            
+        return None
 
     def iter_jingle_sessions(self, jid, sid=None, media=None):
         if sid:
diff --git a/src/common/jingle_ft.py b/src/common/jingle_ft.py
index 13a31394b2..f2fa6893e7 100644
--- a/src/common/jingle_ft.py
+++ b/src/common/jingle_ft.py
@@ -26,7 +26,7 @@ from jingle_transport import JingleTransportICEUDP, JingleTransportSocks5, Jingl
 from common import helpers
 from common.socks5 import Socks5ReceiverClient, Socks5SenderClient
 from common.connection_handlers_events import FileRequestReceivedEvent
-
+import threading
 import logging
 log = logging.getLogger('gajim.c.jingle_ft')
 
@@ -56,9 +56,11 @@ class JingleFileTransfer(JingleContent):
 
         # events we might be interested in
         self.callbacks['session-initiate'] += [self.__on_session_initiate]
+        self.callbacks['session-initiate-sent'] += [self.__on_session_initiate_sent]
         self.callbacks['content-add'] += [self.__on_session_initiate]
         self.callbacks['session-accept'] += [self.__on_session_accept]
-        self.callbacks['session-terminate'] += [self.__on_session_terminate]
+        self.callbacks['session-terminate'] += [self.__on_session_terminate]        
+        self.callbacks['session-info'] += [self.__on_session_info]
         self.callbacks['transport-accept'] += [self.__on_transport_accept]
         self.callbacks['transport-replace'] += [self.__on_transport_replace]
         self.callbacks['session-accept-sent'] += [self.__transport_setup]
@@ -99,12 +101,36 @@ class JingleFileTransfer(JingleContent):
         self.session = session
         self.media = 'file'
         self.nominated_cand = {}
+        
+        # Hash algorithm that we are using to calculate the integrity of the 
+        # file. Could be 'md5', 'sha-1', etc...
+        self.hash_algo = None
 
     def __on_session_initiate(self, stanza, content, error, action):
         gajim.nec.push_incoming_event(FileRequestReceivedEvent(None,
             conn=self.session.connection, stanza=stanza, jingle_content=content,
             FT_content=self))
-
+    def __on_session_initiate_sent(self, stanza, content, error, action):
+        # Calculate file_hash in a new thread
+        self.hashThread = threading.Thread(target=self.__calcHash)
+        self.hashThread.start()
+        
+    def __calcHash(self):
+        if self.hash_algo == None:
+            return
+        try:
+            file = open(self.file_props['file-name'], 'r')
+        except:
+            return
+        h = xmpp.Hashes()
+        h.calculateHash(self.hash_algo, file)
+        checksum = xmpp.Node(tag='checksum',  
+                             payload=[xmpp.Node(tag='file', payload=[h])])
+        checksum.setNamespace(xmpp.NS_JINGLE_FILE_TRANSFER)
+        # Send hash in a session info
+        self.session.__session_info(checksum )
+    
+        
     def __on_session_accept(self, stanza, content, error, action):
         log.info("__on_session_accept")
         con = self.session.connection
@@ -152,6 +178,9 @@ class JingleFileTransfer(JingleContent):
     def __on_session_terminate(self, stanza, content, error, action):
         log.info("__on_session_terminate")
 
+    def __on_session_info(self, stanza, content, error, action):
+        pass
+        
     def __on_transport_accept(self, stanza, content, error, action):
         log.info("__on_transport_accept")
 
@@ -164,8 +193,6 @@ class JingleFileTransfer(JingleContent):
     def __on_transport_info(self, stanza, content, error, action):
         log.info("__on_transport_info")
 
-        #if not self.weinitiate: # proxy activated from initiator
-        #    return
         if content.getTag('transport').getTag('candidate-error'):
             self.nominated_cand['peer-cand'] = False
             if self.state == STATE_CAND_SENT_PENDING_REPLY:
@@ -392,7 +419,7 @@ class JingleFileTransfer(JingleContent):
 
 
     def start_transfer(self):
-        
+
         self.state = STATE_TRANSFERING
         
         # It tells wether we start the transfer as client or server
diff --git a/src/common/jingle_session.py b/src/common/jingle_session.py
index 4ade92a0a3..4d5a184b30 100644
--- a/src/common/jingle_session.py
+++ b/src/common/jingle_session.py
@@ -98,7 +98,7 @@ class JingleSession(object):
 
 
         self.accepted = True # is this session accepted by user
-
+        self.file_hash = None
         # callbacks to call on proper contents
         # use .prepend() to add new callbacks, especially when you're going
         # to send error instead of ack
@@ -126,6 +126,7 @@ class JingleSession(object):
                 'iq-result':            [self.__broadcast],
                 'iq-error':             [self.__on_error],
         }
+        
 
     def collect_iq_id(self, iq_id):
         if iq_id is not None:
@@ -416,9 +417,20 @@ class JingleSession(object):
     def __on_session_info(self, stanza, jingle, error, action):
         # TODO: ringing, active, (un)hold, (un)mute
         payload = jingle.getPayload()
-        if payload:
-            self.__send_error(stanza, 'feature-not-implemented', 'unsupported-info', type_='modify')
-            raise xmpp.NodeProcessed
+        for p in payload:
+            if p.getName() == 'checksum':
+                hashes = p.getTag('file').getTag(name='hashes', 
+                                        namespace=xmpp.NS_HASHES)
+                for hash in hashes.getChildren():
+                    algo = hash.getAttr('algo')
+                    if algo in xmpp.Hashes.supported:
+                        data = hash.getData()
+                        self.file_hash = data
+                        print data
+                        raise xmpp.NodeProcessed
+        self.__send_error(stanza, 'feature-not-implemented', 'unsupported-info', type_='modify')
+        raise xmpp.NodeProcessed
+        
 
     def __on_content_remove(self, stanza, jingle, error, action):
         for content in jingle.iterTags('content'):
@@ -704,6 +716,13 @@ class JingleSession(object):
         if payload:
             jingle.addChild(node=payload)
         self.connection.connection.send(stanza)
+        
+    def _JingleFileTransfer__session_info(self, p):
+        # For some strange reason when I call
+        # self.session.__session_info(h) from the jingleFileTransfer object
+        # within a thread, this method gets called instead. Even though, it
+        # isn't being called explicitly.
+        self.__session_info(p)
 
     def _session_terminate(self, reason=None):
         assert self.state != JingleStates.ended
diff --git a/src/common/xmpp/protocol.py b/src/common/xmpp/protocol.py
index 87bc44ec60..82b16a9c04 100644
--- a/src/common/xmpp/protocol.py
+++ b/src/common/xmpp/protocol.py
@@ -23,6 +23,7 @@ sub- stanzas) handling routines
 from simplexml import Node, NodeBuilder
 import time
 import string
+import hashlib
 
 def ascii_upper(s):
     trans_table = string.maketrans(string.ascii_lowercase,
@@ -89,7 +90,7 @@ NS_JINGLE_ERRORS  = 'urn:xmpp:jingle:errors:1'                        # XEP-0166
 NS_JINGLE_RTP     = 'urn:xmpp:jingle:apps:rtp:1'                      # XEP-0167
 NS_JINGLE_RTP_AUDIO = 'urn:xmpp:jingle:apps:rtp:audio'                # XEP-0167
 NS_JINGLE_RTP_VIDEO = 'urn:xmpp:jingle:apps:rtp:video'                # XEP-0167
-NS_JINGLE_FILE_TRANSFER='urn:xmpp:jingle:apps:file-transfer:1'        # XEP-0234
+NS_JINGLE_FILE_TRANSFER ='urn:xmpp:jingle:apps:file-transfer:1'        # XEP-0234
 NS_JINGLE_XTLS='urn:xmpp:jingle:security:xtls:0'                      # XTLS: EXPERIMENTAL security layer of jingle
 NS_JINGLE_RAW_UDP = 'urn:xmpp:jingle:transports:raw-udp:1'            # XEP-0177
 NS_JINGLE_ICE_UDP = 'urn:xmpp:jingle:transports:ice-udp:1'            # XEP-0176
@@ -160,7 +161,12 @@ NS_PUBKEY_PUBKEY  = 'urn:xmpp:pubkey:2'
 NS_PUBKEY_REVOKE  = 'urn:xmpp:revoke:2'
 NS_PUBKEY_ATTEST  = 'urn:xmpp:attest:2'
 NS_STREAM_MGMT    = 'urn:xmpp:sm:2'                                   # XEP-198
-
+NS_HASHES         = 'urn:xmpp:hashes:0'                               # XEP-300
+NS_HASHES_MD5     = 'urn:xmpp:hash-function-textual-names:md5'
+NS_HASHES_SHA1    = 'urn:xmpp:hash-function-textual-names:sha-1'
+NS_HASHES_SHA256  = 'urn:xmpp:hash-function-textual-names:sha-256'
+NS_HASHES_SHA512  = 'urn:xmpp:hash-function-textual-names:sha-512'
+                 
 xmpp_stream_error_conditions = '''
 bad-format --  --  -- The entity has sent XML that cannot be processed.
 bad-namespace-prefix --  --  -- The entity has sent a namespace prefix that is unsupported, or has sent no namespace prefix on an element that requires such a prefix.
@@ -1030,7 +1036,85 @@ class Iq(Protocol):
             attrs={'id': self.getID()})
         iq.setQuery(self.getQuery().getName()).setNamespace(self.getQueryNS())
         return iq
-
+    
+class Hashes(Node): 
+    """
+    Hash elements for various XEPs as defined in XEP-300
+    """
+    
+    """
+    RECOMENDED HASH USE:
+    Algorithm     Support
+    MD2           MUST NOT
+    MD4           MUST NOT
+    MD5           MAY
+    SHA-1         MUST
+    SHA-256       MUST
+    SHA-512       SHOULD
+    """
+    
+    supported = ('md5', 'sha-1', 'sha-256', 'sha-512')
+    
+    def __init__(self, nsp=NS_HASHES):
+        Node.__init__(self, None, {}, [], None, None,False, None)
+        self.setNamespace(nsp)
+        self.setName('hashes')
+    
+    def calculateHash(self, algo, file_string):
+        """
+        Calculate the hash and add it. It is preferable doing it here
+        instead of doing it all over the place in Gajim.
+        """
+        hl = None
+        hash = None
+        
+        # file_string can be a string or a file
+        if type(file_string) == str: # if it is a string
+            if algo == 'md5':
+                hl = hashlib.md5()
+            elif algo == 'sha-1':
+                hl = hashlib.sha1()
+            elif algo == 'sha-256':
+                hl = hashlib.sha256()
+            elif algo == 'sha-512':
+                hl = hashlib.sha512()
+                
+            if hl == None:
+                # Raise exception
+                raise Exception('Hash algorithm not supported')
+            else:
+                hl.update(file_string)
+                hash = hl.hexdigest()
+        else: # if it is a file
+                
+            if algo == 'md5':
+                hl = hashlib.md5()
+            elif algo == 'sha-1':
+                hl = hashlib.sha1()
+            elif algo == 'sha-256':
+                hl = hashlib.sha256()
+            elif algo == 'sha-512':
+                hl = hashlib.sha512()
+                
+            if hl == None:
+                # Raise exception
+                raise Exception('Hash algorithm not supported')
+            else:
+                for line in file_string:
+                    hl.update(line)
+                hash = hl.hexdigest()
+                
+        self.addHash(hash, algo)
+            
+    def addHash(self, hash, algo):
+        """
+        More than one hash can be added. Although it is permitted, it should
+        not be done for big files because it could slow down Gajim.
+        """
+        attrs = {}
+        attrs['algo'] = algo 
+        self.addChild('hash', attrs, [hash])
+     
 class Acks(Node):
     """
     Acknowledgement elements for Stream Management
@@ -1413,3 +1497,4 @@ class DataForm(Node):
         Simple dictionary interface for setting datafields values by their names
         """
         return self.setField(name).setValue(val)
+
-- 
GitLab