From 376777091e7cdc9d021595481f517ae182cae6c5 Mon Sep 17 00:00:00 2001 From: Jefry Lagrange 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 83ae590f7..6face0fbf 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 6a7519e00..3f3c8f726 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 13a31394b..f2fa6893e 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 4ade92a0a..4d5a184b3 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 87bc44ec6..82b16a9c0 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) +