From cb2d6295358b2dc1e47238599727ec8d41a86af3 Mon Sep 17 00:00:00 2001 From: tomk Date: Sat, 31 May 2008 16:51:40 +0000 Subject: [PATCH 01/20] added prototype of BOSHClient class and script for usage example, removed import of common.gajim from transports_nb --- src/common/xmpp/client_bosh.py | 194 ++++++++++++++++++++ src/common/xmpp/examples/run_client_bosh.py | 89 +++++++++ src/common/xmpp/transports_nb.py | 21 ++- 3 files changed, 298 insertions(+), 6 deletions(-) create mode 100644 src/common/xmpp/client_bosh.py create mode 100644 src/common/xmpp/examples/run_client_bosh.py diff --git a/src/common/xmpp/client_bosh.py b/src/common/xmpp/client_bosh.py new file mode 100644 index 000000000..c2003b375 --- /dev/null +++ b/src/common/xmpp/client_bosh.py @@ -0,0 +1,194 @@ +## client_bosh.py +## +## Copyright (C) 2008 Tomas Karasek +## +## 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 2, 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. + +import locale, random +import protocol +import simplexml +import debug +import dispatcher_nb +from client_nb import NBCommonClient + +DBG_BOSHCLIENT='boshclient' + +class BOSHClient(NBCommonClient): + ''' + BOSH (XMPP over HTTP) client implementation. It should provide the same + methods and functionality as NonBlockingClient. + ''' + + + def __init__(self, server, bosh_conn_mgr, port=5222, bosh_port=5280, + on_connect=None, on_connect_failure=None, on_proxy_failure=None, caller=None): + ''' + Class constuctor has the same parameters as NBCommonClient plus bosh_conn_mgr + and bosh_port - Connection manager address and port. bosh_conn_mgr should be + in form: 'http://httpcm.jabber.org/http-bind/' + Tcp connection will be opened to bosh_conn_mgr:bosh_port instead of + server:port. + ''' + self.bosh_protocol, self.bosh_host, self.bosh_uri = self.urisplit(bosh_conn_mgr) + if self.bosh_protocol is None: + self.bosh_protocol = 'http' + + self.bosh_port = bosh_port + + if self.bosh_uri == '': + bosh_uri = '/' + + self.xmpp_server = server + self.xmpp_port = port + + self.bosh_hold = 1 + self.bosh_wait=60 + self.bosh_rid=-1 + self.bosh_httpversion = 'HTTP/1.1' + + NBCommonClient.__init__(self, self.bosh_host, self.bosh_port, caller=caller, + on_connect=on_connect, on_connect_failure=on_connect_failure, + on_proxy_failure=on_proxy_failure) + + # Namespace and DBG are detected in NBCommonClient constructor + # with isinstance(). Since BOSHClient is descendant of NBCommonClient + # and client_bosh.py is NOT imported in client_nb.py, NB_COMPONENT_ACCEPT + # is put to namespace. This is not very nice, thus: + # TODO: refactor Namespace and DBG recognition in NBCommonClient or + # derived classes + self.Namespace, self.DBG = protocol.NS_HTTP_BIND, DBG_BOSHCLIENT + # pop of DBG_COMPONENT + self.debug_flags.pop() + self.debug_flags.append(self.DBG) + self.debug_flags.append(simplexml.DBG_NODEBUILDER) + + + def urisplit(self, uri): + ''' + Function for splitting URI string to tuple (protocol, host, path). + e.g. urisplit('http://httpcm.jabber.org/webclient') returns + ('http', 'httpcm.jabber.org', '/webclient') + ''' + import re + regex = '(([^:/]+)(://))?([^/]*)(/?.*)' + grouped = re.match(regex, uri).groups() + proto, host, path = grouped[1], grouped[3], grouped[4] + return proto, host, path + + + def _on_connected(self): + ''' + method called after socket starts connecting from NonBlockingTcp._do_connect + ''' + self.onreceive(self.on_bosh_session_init_response) + dispatcher_nb.Dispatcher().PlugIn(self) + + + def parse_http_message(self, message): + ''' + splits http message to tuple ( + statusline - list of e.g. ['HTTP/1.1', '200', 'OK'], + headers - dictionary of headers e.g. {'Content-Length': '604', + 'Content-Type': 'text/xml; charset=utf-8'}, + httpbody - string with http body + ) + ''' + message = message.replace('\r','') + (header, httpbody) = message.split('\n\n') + header = header.split('\n') + statusline = header[0].split(' ') + header = header[1:] + headers = {} + for dummy in header: + row = dummy.split(' ',1) + headers[row[0][:-1]] = row[1] + return (statusline, headers, httpbody) + + + def on_bosh_session_init_response(self, data): + ''' + Called on init response - should check relevant attributes from body tag + ''' + if data: + statusline, headers, httpbody = self.parse_http_message(data) + + if statusline[1] != '200': + self.DEBUG(self.DBG, "HTTP Error in received session init response: %s" + % statusline, 'error') + # error handling TBD! + + # ATM, whole tag is pass to ProcessNonBocking. + # Question is how to peel off the body tag from incoming stanzas and make + # use of ordinar xmpp traffic handling. + self.Dispatcher.ProcessNonBlocking(httpbody) + + + def _check_stream_start(self, ns, tag, attrs): + ''' + callback stub called from XML Parser when is discovered + ''' + self.DEBUG(self.DBG, 'CHECK_STREAM_START: ns: %s, tag: %s, attrs: %s' + % (ns, tag, attrs), 'info') + + + def StreamInit(self): + ''' + Initiation of BOSH session. Called instead of Dispatcher.StreamInit() + Initial body tag is created and sent to Conn Manager. + ''' + self.Dispatcher.Stream = simplexml.NodeBuilder() + self.Dispatcher.Stream._dispatch_depth = 2 + self.Dispatcher.Stream.dispatch = self.Dispatcher.dispatch + self.Dispatcher.Stream.stream_header_received = self._check_stream_start + self.debug_flags.append(simplexml.DBG_NODEBUILDER) + self.Dispatcher.Stream.DEBUG = self.DEBUG + self.Dispatcher.Stream.features = None + + initial_body_tag = simplexml.Node('body') + initial_body_tag.setNamespace(self.Namespace) + initial_body_tag.setAttr('content', 'text/xml; charset=utf-8') + initial_body_tag.setAttr('hold', str(self.bosh_hold)) + initial_body_tag.setAttr('to', self.xmpp_server) + initial_body_tag.setAttr('wait', str(self.bosh_wait)) + + r = random.Random() + r.seed() + # with 50-bit random initial rid, session would have to go up + # to 7881299347898368 messages to raise rid over 2**53 + # (see http://www.xmpp.org/extensions/xep-0124.html#rids) + self.bosh_rid = r.getrandbits(50) + initial_body_tag.setAttr('rid', str(self.bosh_rid)) + + if locale.getdefaultlocale()[0]: + initial_body_tag.setAttr('xml:lang', + locale.getdefaultlocale()[0].split('_')[0]) + initial_body_tag.setAttr('xmpp:version', '1.0') + initial_body_tag.setAttr('xmlns:xmpp', 'urn:xmpp:xbosh') + + self.send(self.build_bosh_message(initial_body_tag)) + + + def build_bosh_message(self, httpbody): + ''' + Builds bosh http message with given body. + Values for headers and status line fields are taken from class variables. + ) + ''' + headers = ['POST %s HTTP/1.1' % self.bosh_uri, + 'Host: %s' % self.bosh_host, + 'Content-Type: text/xml; charset=utf-8', + 'Content-Length: %s' % len(str(httpbody)), + '\r\n'] + headers = '\r\n'.join(headers) + return('%s%s\r\n' % (headers, httpbody)) + + + diff --git a/src/common/xmpp/examples/run_client_bosh.py b/src/common/xmpp/examples/run_client_bosh.py new file mode 100644 index 000000000..3ec215a25 --- /dev/null +++ b/src/common/xmpp/examples/run_client_bosh.py @@ -0,0 +1,89 @@ +# Example script for usage of BOSHClient class +# -------------------------------------------- +# run `python run_client_bosh.py` in gajim/src/common/xmpp/examples/ directory +# and quit with CTRL + c. +# Script will open TCP connection to Connection Manager, send BOSH initial +# request and receive initial response. Handling of init response is not +# done yet. + + +# imports gtk because of gobject.timeout_add() which is used for processing +# idlequeue +# TODO: rewrite to thread timer +import gtk +import gobject +import sys, os.path + +xmpppy_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), '..') +sys.path.append(xmpppy_dir) + +import idlequeue +from client_bosh import BOSHClient + + +class DummyConnection(): + ''' + Dummy class for test run of bosh_client. I use it because in Connection class + the gajim.py module is imported and stuff from it is read in there, so it + would be difficult to debug IMHO. On the other hand, I will have to test with + Connection class sooner or later somehow because that's the main place where + BOSHClient should be used. + DummyConnection class holds and processes IdleQueue for BOSHClient. + ''' + + def __init__(self, iq_interval_ms=1000): + self.classname = self.__class__.__name__ + self.iq_interval_ms = iq_interval_ms + self.idlequeue = idlequeue.IdleQueue() + self.timer=gobject.timeout_add(iq_interval_ms, self.process_idlequeue) + + + def process_idlequeue(self): + ''' + called each iq_interval_ms miliseconds. Checks for idlequeue timeouts. + ''' + self.idlequeue.process() + return True + + # callback stubs follows + def _event_dispatcher(self, realm, event, data): + print "\n>>> %s._event_dispatcher called:" % self.classname + print ">>> realm: %s, event: %s, data: %s\n" % (realm, event, data) + + + def onconsucc(self): + print '%s: CONNECTION SUCCEEDED' % self.classname + + + def onconfail(self, retry=None): + print '%s: CONNECTION FAILED.. retry?: %s' % (self.classname, retry) + + +if __name__ == "__main__": + dc = DummyConnection() + + # you can use my instalation of ejabberd2: + server = 'star.securitynet.cz' + bosh_conn_mgr = 'http://star.securitynet.cz/http-bind/' + + #server='jabbim.cz' + #bosh_conn_mgr='http://bind.jabbim.cz/' + + bc = BOSHClient( + server = server, + bosh_conn_mgr = bosh_conn_mgr, + bosh_port = 80, + on_connect = dc.onconsucc, + on_connect_failure = dc.onconfail, + caller = dc + ) + + bc.set_idlequeue(dc.idlequeue) + + bc.connect() + + try: + gtk.main() + except KeyboardInterrupt: + dc.process_idlequeue() + diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py index f7ab85a56..2c3364d45 100644 --- a/src/common/xmpp/transports_nb.py +++ b/src/common/xmpp/transports_nb.py @@ -33,7 +33,16 @@ import thread import logging log = logging.getLogger('gajim.c.x.transports_nb') -import common.gajim +# I don't need to load gajim.py just because of few TLS variables, so I changed +# :%s/common\.gajim\.DATA_DIR/\'\.\.\/data\'/c +# :%s/common\.gajim\.MY_CACERTS/\'\%s\/\.gajim\/cacerts\.pem\' \% os\.environ\[\'HOME\'\]/c + +# To change it back do: +# %s/\'\.\.\/data\'/common\.gajim\.DATA_DIR/c +# :%s/\'\%s\/\.gajim\/cacerts\.pem\' \% os\.environ\[\'HOME\'\]/common\.gajim\.MY_CACERTS/c + +# import common.gajim + USE_PYOPENSSL = False @@ -762,16 +771,16 @@ class NonBlockingTLS(PlugIn): #tcpsock._sslContext = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD) tcpsock.ssl_errnum = 0 tcpsock._sslContext.set_verify(OpenSSL.SSL.VERIFY_PEER, self._ssl_verify_callback) - cacerts = os.path.join(common.gajim.DATA_DIR, 'other', 'cacerts.pem') + cacerts = os.path.join('../data', 'other', 'cacerts.pem') try: tcpsock._sslContext.load_verify_locations(cacerts) except: log.warning('Unable to load SSL certificats from file %s' % \ os.path.abspath(cacerts)) # load users certs - if os.path.isfile(common.gajim.MY_CACERTS): + if os.path.isfile('%s/.gajim/cacerts.pem' % os.environ['HOME']): store = tcpsock._sslContext.get_cert_store() - f = open(common.gajim.MY_CACERTS) + f = open('%s/.gajim/cacerts.pem' % os.environ['HOME']) lines = f.readlines() i = 0 begin = -1 @@ -786,11 +795,11 @@ class NonBlockingTLS(PlugIn): store.add_cert(X509cert) except OpenSSL.crypto.Error, exception_obj: log.warning('Unable to load a certificate from file %s: %s' %\ - (common.gajim.MY_CACERTS, exception_obj.args[0][0][2])) + ('%s/.gajim/cacerts.pem' % os.environ['HOME'], exception_obj.args[0][0][2])) except: log.warning( 'Unknown error while loading certificate from file %s' % \ - common.gajim.MY_CACERTS) + '%s/.gajim/cacerts.pem' % os.environ['HOME']) begin = -1 i += 1 tcpsock._sslObj = OpenSSL.SSL.Connection(tcpsock._sslContext, tcpsock._sock) From 6a15c9b9c9b53989a7ebd9c38854c2fe4bb5b922 Mon Sep 17 00:00:00 2001 From: tomk Date: Mon, 9 Jun 2008 00:32:02 +0000 Subject: [PATCH 02/20] initial cleanup of xmpppy perfomed, see #3260 --- AUTHORS | 2 +- data/glade/chat_control_popup_menu.glade | 45 - data/glade/contact_otr_window.glade | 322 - data/glade/roster_contact_context_menu.glade | 7 - epydoc.conf | 29 + po/de.po | 8378 +++++++++--------- po/fr.po | 30 +- src/chat_control.py | 135 +- src/common/config.py | 5 +- src/common/connection.py | 40 +- src/common/connection_handlers.py | 174 +- src/common/contacts.py | 27 +- src/common/dbus_support.py | 7 +- src/common/gajim.py | 7 - src/common/helpers.py | 9 +- src/common/xmpp/__init__.py | 2 +- src/common/xmpp/auth.py | 306 - src/common/xmpp/auth_nb.py | 56 +- src/common/xmpp/browser.py | 216 - src/common/xmpp/client.py | 296 +- src/common/xmpp/client_bosh.py | 194 - src/common/xmpp/client_nb.py | 37 +- src/common/xmpp/commands.py | 330 - src/common/xmpp/dispatcher.py | 383 - src/common/xmpp/examples/run_client_bosh.py | 89 - src/common/xmpp/features.py | 184 - src/common/xmpp/features_nb.py | 6 +- src/common/xmpp/filetransfer.py | 199 - src/common/xmpp/roster.py | 195 - src/common/xmpp/roster_nb.py | 168 +- src/common/xmpp/session.py | 350 - src/common/xmpp/transports.py | 289 - src/common/xmpp/transports_nb.py | 22 +- src/config.py | 25 +- src/conversation_textview.py | 11 +- src/features_window.py | 9 - src/gajim.py | 326 +- src/groupchat_control.py | 12 +- src/history_window.py | 78 +- src/message_control.py | 65 +- src/message_window.py | 6 +- src/osx/__init__.py | 8 +- src/otr_windows.py | 357 - src/roster_window.py | 752 +- src/session.py | 5 +- src/statusicon.py | 10 +- src/systray.py | 1 - src/tictactoe.py | 427 - test/mocks.py | 68 + test/test_sessions.py | 69 +- 50 files changed, 5273 insertions(+), 9495 deletions(-) delete mode 100644 data/glade/contact_otr_window.glade create mode 100644 epydoc.conf delete mode 100644 src/common/xmpp/auth.py delete mode 100644 src/common/xmpp/browser.py delete mode 100644 src/common/xmpp/client_bosh.py delete mode 100644 src/common/xmpp/commands.py delete mode 100644 src/common/xmpp/dispatcher.py delete mode 100644 src/common/xmpp/examples/run_client_bosh.py delete mode 100644 src/common/xmpp/features.py delete mode 100644 src/common/xmpp/filetransfer.py delete mode 100644 src/common/xmpp/roster.py delete mode 100644 src/common/xmpp/session.py delete mode 100644 src/common/xmpp/transports.py delete mode 100644 src/otr_windows.py delete mode 100644 src/tictactoe.py create mode 100644 test/mocks.py diff --git a/AUTHORS b/AUTHORS index f1b6f72b3..0b64f2b68 100644 --- a/AUTHORS +++ b/AUTHORS @@ -6,10 +6,10 @@ Travis Shirk (travis AT pobox.com) Stephan Erb (steve-e AT h3c.de) Julien Pivotto (roidelapluie AT gmail.com) Jonathan Schleifer (js-gajim AT webkeks.org) +Jean-Marie Traissard (jim AT lapin.org) PAST DEVELOPERS: -Jean-Marie Traissard (jim AT lapin.org) Stefan Bethge (stefan AT lanpartei.de) Dimitur Kirov (dkirov AT gmail.com) Vincent Hanquez (tab AT snarc.org) diff --git a/data/glade/chat_control_popup_menu.glade b/data/glade/chat_control_popup_menu.glade index d53077869..1c923adbd 100644 --- a/data/glade/chat_control_popup_menu.glade +++ b/data/glade/chat_control_popup_menu.glade @@ -71,51 +71,6 @@ - - - True - False - Off-the-Record Encryption - True - - - - - True - OTR settings / fingerprint - True - - - - - - True - Authenticate contact - True - - - - - - True - Start / Refresh OTR - True - - - - - - True - False - End OTR - True - - - - - - - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK diff --git a/data/glade/contact_otr_window.glade b/data/glade/contact_otr_window.glade deleted file mode 100644 index e0f5b865c..000000000 --- a/data/glade/contact_otr_window.glade +++ /dev/null @@ -1,322 +0,0 @@ - - - - - - False - - - True - - - True - True - - - True - 5 - 5 - True - - - True - 0 - Your fingerprint: -<span weight="bold" face="monospace">01234567 89ABCDEF 01234567 89ABCDEF 01234567</span> - True - - - - - True - 0 - Purported fingerprint for asdfasdf@xyzxyzxyz.de: -<span weight="bold" face="monospace">01234567 89ABCDEF 01234567 89ABCDEF 01234567</span> - True - - - 1 - - - - - True - - - True - True - 1 - I have NOT -I have - - - False - - - - - True - 0.20000000298023224 - verified that the purported fingerprint is in fact the correct fingerprint for that contact. - True - - - 1 - - - - - 2 - - - - - - - True - Authentication - - - tab - False - - - - - True - 0 - GTK_SHADOW_NONE - - - True - 5 - 5 - True - - - True - True - OTR version 1 allowed - 0 - True - True - - - - - True - True - OTR version 2 allowed - 0 - True - True - - - 1 - - - - - True - True - Encryption required - 0 - True - - - 2 - - - - - True - True - Show others we understand OTR - 0 - True - True - - - 3 - - - - - True - True - Automatically initiate encryption if partner understands OTR - 0 - True - True - - - 4 - - - - - True - True - Automatically start OTR when an OTR error occured - 0 - True - - - 5 - - - - - - - True - True - Use the default settings - 0 - True - True - - - label_item - - - - - 1 - - - - - True - OTR Settings - - - tab - 1 - False - - - - - - - True - GTK_BUTTONBOX_END - - - True - True - True - gtk-cancel - True - 0 - - - - - True - True - True - True - True - gtk-ok - True - 0 - - - 1 - - - - - 1 - - - - - - - False - - - True - - - True - 5 - 5 - - - True - label - True - True - - - - - True - True - - - False - 1 - - - - - - - True - 5 - - - True - - - - False - - - - - True - GTK_BUTTONBOX_END - - - True - True - True - gtk-cancel - True - 0 - - - - - True - True - True - gtk-ok - True - 0 - - - 1 - - - - - 1 - - - - - False - 1 - - - - - - diff --git a/data/glade/roster_contact_context_menu.glade b/data/glade/roster_contact_context_menu.glade index 2a1955007..f873e7f77 100644 --- a/data/glade/roster_contact_context_menu.glade +++ b/data/glade/roster_contact_context_menu.glade @@ -91,13 +91,6 @@ - - - True - Play Tic Tac Toe - True - - True diff --git a/epydoc.conf b/epydoc.conf new file mode 100644 index 000000000..b2a1c0565 --- /dev/null +++ b/epydoc.conf @@ -0,0 +1,29 @@ +[epydoc] + +# Information about the project. +name: Gajim +url: http://gajim.org + +verbosity: 3 +imports: yes +redundant-details: yes +docformat: restructuredtext +# top: gajim + +# The list of modules to document. Modules can be named using +# dotted names, module filenames, or package directory names. +# This option may be repeated. +modules: src/* test/* + +# Write html output to the directory "apidocs" +#output: pdf +output: html +target: apidocs/ + +# Include all automatically generated graphs. These graphs are +# generated using Graphviz dot. +graph: all +dotpath: /usr/bin/dot +graph-font: Sans +graph-font-size: 10 + diff --git a/po/de.po b/po/de.po index 653485d1a..93fdff9c8 100644 --- a/po/de.po +++ b/po/de.po @@ -3,7 +3,7 @@ # This file is distributed under the same license as the gajim package. # Fridtjof Busse , 2005, 2006. # Benjamin Drung , 2007, 2008. -# Fabian Fingerle , 2007. +# Fabian Fingerle , 2007, 2008. # Sebastian Schäfer , 2007. # Nico Gulden , 2007, 2008. # @@ -15,7 +15,7 @@ msgid "" msgstr "" "Project-Id-Version: gajim 0.11\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2008-05-16 10:15+0200\n" +"POT-Creation-Date: 2008-06-03 07:50+0200\n" "PO-Revision-Date: 2008-03-24 20:40+0100\n" "Last-Translator: Nico Gulden \n" "Language-Team: German \n" @@ -103,8 +103,8 @@ msgid "" "Add this certificate to the list of trusted certificates.\n" "SHA1 fingerprint of the certificate:\n" msgstr "" -"Dieses Zertifikat der Liste von Vertrauenswürdigen Zertifikaten hinzufügen\n" -"SHA1 Fingerabdruck von dem Zertifikat:\n" +"Dieses Zertifikat der Liste vertrauenswürdiger Zertifikate hinzufügen\n" +"SHA1-Fingerprint dieses Zertifikates:\n" #: ../data/glade/account_creation_wizard_window.glade.h:10 msgid "Click to see features (like MSN, ICQ transports) of jabber servers" @@ -299,9 +299,9 @@ msgstr "Persönliche Informationen bearbeiten ..." #. No configured account #: ../data/glade/account_modification_window.glade.h:16 #: ../data/glade/accounts_window.glade.h:20 -#: ../data/glade/roster_window.glade.h:6 ../src/notify.py:498 -#: ../src/notify.py:529 ../src/notify.py:541 ../src/common/helpers.py:1083 -#: ../src/common/helpers.py:1095 +#: ../data/glade/roster_window.glade.h:6 ../src/common/helpers.py:1083 +#: ../src/common/helpers.py:1095 ../src/notify.py:498 ../src/notify.py:529 +#: ../src/notify.py:541 msgid "Gajim" msgstr "Gajim" @@ -311,15 +311,15 @@ msgstr "Gajim" #. General group cannot be changed #: ../data/glade/account_modification_window.glade.h:17 #: ../data/glade/accounts_window.glade.h:21 -#: ../data/glade/preferences_window.glade.h:50 -#: ../data/glade/zeroconf_properties_window.glade.h:7 ../src/dialogs.py:100 -#: ../src/dialogs.py:108 ../src/dialogs.py:162 ../src/roster_window.py:184 -#: ../src/roster_window.py:360 ../src/roster_window.py:641 -#: ../src/roster_window.py:685 ../src/roster_window.py:973 -#: ../src/roster_window.py:1193 ../src/roster_window.py:1351 -#: ../src/roster_window.py:2515 ../src/roster_window.py:4639 -#: ../src/roster_window.py:4766 ../src/roster_window.py:4935 -#: ../src/common/contacts.py:322 +#: ../data/glade/preferences_window.glade.h:47 +#: ../data/glade/zeroconf_properties_window.glade.h:7 +#: ../src/common/contacts.py:323 ../src/dialogs.py:100 ../src/dialogs.py:108 +#: ../src/dialogs.py:162 ../src/roster_window.py:184 +#: ../src/roster_window.py:361 ../src/roster_window.py:638 +#: ../src/roster_window.py:693 ../src/roster_window.py:958 +#: ../src/roster_window.py:1207 ../src/roster_window.py:1382 +#: ../src/roster_window.py:2569 ../src/roster_window.py:4718 +#: ../src/roster_window.py:4848 ../src/roster_window.py:5013 msgid "General" msgstr "Allgemein" @@ -384,21 +384,21 @@ msgstr "Informationen über Sie, wie sie auf dem Server gespeichert sind" #: ../data/glade/account_modification_window.glade.h:27 #: ../data/glade/accounts_window.glade.h:36 -#: ../data/glade/zeroconf_properties_window.glade.h:16 ../src/config.py:1611 -#: ../src/config.py:2098 ../src/config.py:3692 +#: ../data/glade/zeroconf_properties_window.glade.h:16 ../src/config.py:1523 +#: ../src/config.py:2017 ../src/config.py:3619 msgid "No key selected" msgstr "Kein Schlüssel gewählt" #. None means no proxy profile selected #. GPG Key #: ../data/glade/account_modification_window.glade.h:29 -#: ../data/glade/accounts_window.glade.h:38 ../src/config.py:1199 -#: ../src/config.py:1270 ../src/config.py:1519 ../src/config.py:1524 -#: ../src/config.py:2005 ../src/config.py:2083 ../src/config.py:2097 -#: ../src/config.py:3190 ../src/config.py:3258 ../src/config.py:3682 -#: ../src/config.py:3691 ../src/dialogs.py:304 ../src/dialogs.py:306 -#: ../src/roster_window.py:2597 ../src/roster_window.py:2604 -#: ../src/roster_window.py:2611 +#: ../data/glade/accounts_window.glade.h:38 ../src/config.py:1112 +#: ../src/config.py:1183 ../src/config.py:1431 ../src/config.py:1436 +#: ../src/config.py:1924 ../src/config.py:2002 ../src/config.py:2016 +#: ../src/config.py:3115 ../src/config.py:3183 ../src/config.py:3609 +#: ../src/config.py:3618 ../src/dialogs.py:304 ../src/dialogs.py:306 +#: ../src/roster_window.py:2645 ../src/roster_window.py:2652 +#: ../src/roster_window.py:2659 msgid "None" msgstr "Kein" @@ -485,6 +485,7 @@ msgstr "Konto-Status mit globalem _Status abgleichen" #: ../data/glade/account_modification_window.glade.h:43 #: ../data/glade/accounts_window.glade.h:51 +#: ../data/glade/synchronise_contacts_dialog.glade.h:2 #: ../data/glade/synchronise_select_account_dialog.glade.h:2 msgid "Synchronise contacts" msgstr "Kontakte synchronisieren" @@ -566,7 +567,7 @@ msgstr "" #: ../data/glade/accounts_window.glade.h:32 #: ../data/glade/zeroconf_information_window.glade.h:4 -#: ../data/glade/zeroconf_properties_window.glade.h:13 ../src/dialogs.py:614 +#: ../data/glade/zeroconf_properties_window.glade.h:13 ../src/dialogs.py:643 msgid "Jabber ID:" msgstr "Jabber-ID:" @@ -581,7 +582,7 @@ msgid "Mer_ge accounts" msgstr "Konten _zusammenführen" #. Rename -#: ../data/glade/accounts_window.glade.h:44 ../src/roster_window.py:4718 +#: ../data/glade/accounts_window.glade.h:44 ../src/roster_window.py:4800 msgid "Re_name" msgstr "_Umbenennen" @@ -783,7 +784,7 @@ msgid "Conditions" msgstr "Ereignisse" #: ../data/glade/advanced_notifications_window.glade.h:4 -#: ../data/glade/preferences_window.glade.h:16 +#: ../data/glade/preferences_window.glade.h:15 msgid "Sounds" msgstr "Klänge" @@ -955,6 +956,28 @@ msgstr "Sie haben einen neuen Eintrag erhalten:" msgid "Blocked Contacts" msgstr "blockierte Kontakte" +#: ../data/glade/change_activity_dialog.glade.h:1 +msgid "General:" +msgstr "Allgemein:" + +#: ../data/glade/change_activity_dialog.glade.h:2 +#: ../data/glade/change_mood_dialog.glade.h:1 +msgid "Message:" +msgstr "Nachricht" + +#: ../data/glade/change_activity_dialog.glade.h:3 +msgid "Set Activity" +msgstr "Aktivität setzen" + +#: ../data/glade/change_activity_dialog.glade.h:4 +msgid "Specific:" +msgstr "Detailliert:" + +#: ../data/glade/change_mood_dialog.glade.h:2 +#: ../data/glade/manage_proxies_window.glade.h:8 +msgid "Type:" +msgstr "Art:" + #: ../data/glade/change_password_dialog.glade.h:1 msgid "Change Password" msgstr "Passwort ändern" @@ -984,7 +1007,7 @@ msgid "Join _Group Chat" msgstr "_Gruppenchat beitreten" #: ../data/glade/chat_context_menu.glade.h:2 -#: ../data/glade/roster_contact_context_menu.glade.h:13 +#: ../data/glade/roster_contact_context_menu.glade.h:12 msgid "_Add to Roster..." msgstr "Zur Kont_aktliste hinzufügen ..." @@ -1016,56 +1039,51 @@ msgid "Authenticate contact" msgstr "Kontakt automatisch autorisieren" #: ../data/glade/chat_control_popup_menu.glade.h:2 -msgid "Click to see past conversations with this contact" -msgstr "Klicken, um die früheren Unterhaltungen mit diesem Kontakt zu sehen" +msgid "End OTR " +msgstr "OTR beenden " #: ../data/glade/chat_control_popup_menu.glade.h:3 -msgid "End OTR " -msgstr "" - -#: ../data/glade/chat_control_popup_menu.glade.h:4 msgid "Invite _Contacts" msgstr "_Kontakte einladen" -#: ../data/glade/chat_control_popup_menu.glade.h:5 +#: ../data/glade/chat_control_popup_menu.glade.h:4 msgid "OTR settings / fingerprint" -msgstr "" +msgstr "OTR Einstellungen / Fingerprint" + +#: ../data/glade/chat_control_popup_menu.glade.h:5 +msgid "Off-the-Record Encryption" +msgstr "OTR-Verschlüsselung" #: ../data/glade/chat_control_popup_menu.glade.h:6 -#, fuzzy -msgid "Off-the-Record Encryption" -msgstr "Open_PGP-Verschlüsselung" - -#: ../data/glade/chat_control_popup_menu.glade.h:7 #: ../data/glade/gc_occupants_menu.glade.h:3 #: ../data/glade/zeroconf_contact_context_menu.glade.h:4 msgid "Send _File" msgstr "_Datei senden" -#: ../data/glade/chat_control_popup_menu.glade.h:8 +#: ../data/glade/chat_control_popup_menu.glade.h:7 msgid "Start / Refresh OTR" msgstr "" -#: ../data/glade/chat_control_popup_menu.glade.h:9 +#: ../data/glade/chat_control_popup_menu.glade.h:8 msgid "Toggle End to End Encryption" -msgstr "Verschlüsselung (ESessions) aktivieren" +msgstr "Punkt-zu-Punkt-Verschlüsselung (ESessions) aktivieren" -#: ../data/glade/chat_control_popup_menu.glade.h:10 +#: ../data/glade/chat_control_popup_menu.glade.h:9 msgid "Toggle Open_PGP Encryption" msgstr "Open_PGP-Verschlüsselung aktivieren" -#: ../data/glade/chat_control_popup_menu.glade.h:11 +#: ../data/glade/chat_control_popup_menu.glade.h:10 #: ../data/glade/gc_occupants_menu.glade.h:4 msgid "_Add to Roster" msgstr "Zur Kont_aktliste hinzufügen" -#: ../data/glade/chat_control_popup_menu.glade.h:12 -#: ../data/glade/gc_control_popup_menu.glade.h:7 +#: ../data/glade/chat_control_popup_menu.glade.h:11 +#: ../data/glade/gc_control_popup_menu.glade.h:6 #: ../data/glade/gc_occupants_menu.glade.h:7 -#: ../data/glade/roster_contact_context_menu.glade.h:17 +#: ../data/glade/roster_contact_context_menu.glade.h:16 #: ../data/glade/roster_window.glade.h:20 #: ../data/glade/zeroconf_contact_context_menu.glade.h:6 -#: ../src/roster_window.py:5373 +#: ../src/roster_window.py:5456 msgid "_History" msgstr "_Verlauf" @@ -1273,26 +1291,22 @@ msgid "Change _Subject..." msgstr "_Thema ändern ..." #: ../data/glade/gc_control_popup_menu.glade.h:3 -msgid "Click to see past conversation in this room" -msgstr "Klicken, um die früheren Unterhaltungen in diesem Raum zu sehen" - -#: ../data/glade/gc_control_popup_menu.glade.h:4 msgid "Configure _Room..." msgstr "_Raum einrichten ..." -#: ../data/glade/gc_control_popup_menu.glade.h:5 +#: ../data/glade/gc_control_popup_menu.glade.h:4 msgid "_Bookmark" msgstr "Raum zu _Lesezeichen hinzufügen" -#: ../data/glade/gc_control_popup_menu.glade.h:6 +#: ../data/glade/gc_control_popup_menu.glade.h:5 msgid "_Destroy Room" msgstr "_Raum auflösen" -#: ../data/glade/gc_control_popup_menu.glade.h:8 +#: ../data/glade/gc_control_popup_menu.glade.h:7 msgid "_Manage Room" msgstr "Raum verwalten" -#: ../data/glade/gc_control_popup_menu.glade.h:9 +#: ../data/glade/gc_control_popup_menu.glade.h:8 msgid "_Minimize on close" msgstr "Beim Schließen _minimieren" @@ -1341,8 +1355,8 @@ msgid "From" msgstr "Von" #. holds subject -#: ../data/glade/groups_post_window.glade.h:3 ../src/history_manager.py:164 -#: ../src/history_manager.py:195 +#: ../data/glade/groups_post_window.glade.h:3 ../src/history_manager.py:165 +#: ../src/history_manager.py:196 msgid "Subject" msgstr "Betreff" @@ -1392,24 +1406,23 @@ msgstr "" msgid "_Search Database" msgstr "_Suche Datenbank" -#: ../data/glade/history_window.glade.h:1 ../src/history_window.py:300 +#: ../data/glade/history_window.glade.h:1 ../src/history_window.py:306 msgid "Conversation History" msgstr "Unterhaltungsverlauf" #: ../data/glade/history_window.glade.h:2 -#, fuzzy msgid "" "Enter JID or Contact name\n" "Groupchat Histories\n" "All Chat Histories" msgstr "" -"Aktueller Verlauf\n" +"JID oder Kontaktname eingeben\n" +"Gruppenchat-Verlauf\n" "Gesamter Chatverlauf" #: ../data/glade/history_window.glade.h:5 -#, fuzzy msgid "Search:" -msgstr "Suche" +msgstr "Suche:" #: ../data/glade/history_window.glade.h:6 #: ../data/glade/zeroconf_information_window.glade.h:10 @@ -1429,7 +1442,7 @@ msgstr "Ablehnen" msgid "Invitation Received" msgstr "Einladung empfangen" -#: ../data/glade/join_groupchat_window.glade.h:1 ../src/dialogs.py:1635 +#: ../data/glade/join_groupchat_window.glade.h:1 ../src/dialogs.py:1664 msgid "Join Group Chat" msgstr "Betrete Gruppenchat" @@ -1493,6 +1506,18 @@ msgstr "Server:" msgid "Title:" msgstr "Titel:" +#: ../data/glade/manage_pep_services_window.glade.h:1 +msgid "PEP Service Configuration" +msgstr "PEP-Dienst Konfiguration" + +#: ../data/glade/manage_pep_services_window.glade.h:2 +msgid "_Configure" +msgstr "_Einstellen" + +#: ../data/glade/manage_pep_services_window.glade.h:3 +msgid "gtk-delete" +msgstr "gtk-delete" + #: ../data/glade/manage_proxies_window.glade.h:1 msgid "Properties" msgstr "Eigenschaften" @@ -1521,10 +1546,6 @@ msgstr "Name:" msgid "Pass_word:" msgstr "Pass_wort:" -#: ../data/glade/manage_proxies_window.glade.h:8 -msgid "Type:" -msgstr "Art:" - #: ../data/glade/manage_proxies_window.glade.h:9 msgid "Use authentication" msgstr "Verwende Authentifizierung" @@ -1534,47 +1555,44 @@ msgid "_Host:" msgstr "_Server:" #: ../data/glade/message_window.glade.h:1 -#, fuzzy -msgid "Add this contact to roster (Ctrl-D)" -msgstr "Fügt Kontakt zum Roster hinzu" +msgid "Add this contact to roster (Ctrl+D)" +msgstr "Kontakt zum Roster hinzufügen (STRG+D)" #: ../data/glade/message_window.glade.h:2 -msgid "Bookmark this room (Ctrl-B)" +msgid "Bookmark this room (Ctrl+B)" msgstr "" #: ../data/glade/message_window.glade.h:3 -msgid "Browse the chat history (Ctrl-H)" +msgid "Browse the chat history (Ctrl+H)" msgstr "" #: ../data/glade/message_window.glade.h:4 -msgid "Change the room's subject (Ctrl-T)" +msgid "Change the room's subject (Ctrl+T)" msgstr "" #: ../data/glade/message_window.glade.h:5 -msgid "Change your nickname (Ctrl-N)" +msgid "Change your nickname (Ctrl+N)" msgstr "" #: ../data/glade/message_window.glade.h:6 -#, fuzzy -msgid "Invite contacts to the conversation (Ctrl-G)" -msgstr "beobachtet diese Unterhaltung" +msgid "Invite contacts to the conversation (Ctrl+G)" +msgstr "Kontakt zu Unterhaltung einladen (STRG+G)" #: ../data/glade/message_window.glade.h:7 -msgid "Send a file (Ctrl-F)" +msgid "Send a file (Ctrl+F)" msgstr "" #: ../data/glade/message_window.glade.h:8 -#, fuzzy -msgid "Show a list of emoticons (Alt-M)" -msgstr "Klicken Sie, um ein Emoticon einzufügen (Alt+M)" +msgid "Show a list of emoticons (Alt+M)" +msgstr "Smilie-Liste anzeigen (Alt+M)" #: ../data/glade/message_window.glade.h:9 -msgid "Show a menu of advanced functions (Alt-A)" -msgstr "" +msgid "Show a menu of advanced functions (Alt+A)" +msgstr "Liste erweiterter Funktionen anzeigen (Alt+A)" #: ../data/glade/message_window.glade.h:10 -msgid "Show the contact's profile (Ctrl-I)" -msgstr "" +msgid "Show the contact's profile (Ctrl+I)" +msgstr "Benutzerprofil anzeigen (STRG+I)" #. Make sure the character after "_" is not M/m (conflicts with Alt+M that is supposed to show the Emoticon Selector) #: ../data/glade/message_window.glade.h:12 @@ -1633,38 +1651,30 @@ msgid "Privacy" msgstr "Privatsphäre" #: ../data/glade/preferences_window.glade.h:14 -msgid "Publish and Subscribe" -msgstr "Veröffentlichen und Abonnieren" - -#: ../data/glade/preferences_window.glade.h:15 msgid "Roster Appearance" msgstr "Roster-Aussehen" -#: ../data/glade/preferences_window.glade.h:17 +#: ../data/glade/preferences_window.glade.h:16 msgid "Status Messages" msgstr "Status-Nachrichten" -#: ../data/glade/preferences_window.glade.h:18 +#: ../data/glade/preferences_window.glade.h:17 msgid "Themes" msgstr "Themen" -#: ../data/glade/preferences_window.glade.h:19 +#: ../data/glade/preferences_window.glade.h:18 msgid "Visual Notifications" msgstr "Visuelle Benachrichtigungen" -#: ../data/glade/preferences_window.glade.h:20 ../src/roster_window.py:4484 -msgid "Activity" -msgstr "Aktivität" - -#: ../data/glade/preferences_window.glade.h:21 +#: ../data/glade/preferences_window.glade.h:19 msgid "Advanced" msgstr "Erweitert" -#: ../data/glade/preferences_window.glade.h:22 +#: ../data/glade/preferences_window.glade.h:20 msgid "Advanced..." msgstr "Erweitert ..." -#: ../data/glade/preferences_window.glade.h:23 +#: ../data/glade/preferences_window.glade.h:21 msgid "" "All chat states\n" "Composing only\n" @@ -1674,25 +1684,21 @@ msgstr "" "Nur beim Schreiben\n" "Deaktiviert" -#: ../data/glade/preferences_window.glade.h:26 +#: ../data/glade/preferences_window.glade.h:24 msgid "Allow _OS information to be sent" msgstr "Sende _Betriebssystem-Information" -#: ../data/glade/preferences_window.glade.h:27 -msgid "Allow others to see your:" -msgstr "Erlaube anderen zu sehen:" - -#: ../data/glade/preferences_window.glade.h:28 +#: ../data/glade/preferences_window.glade.h:25 msgid "Allow popup/notifications when I'm _away/na/busy/invisible" msgstr "" "Popups/Benachrichtigungen _zulassen, wenn ich abwesend/NA/beschäftigt/" "unsichtbar bin" -#: ../data/glade/preferences_window.glade.h:29 +#: ../data/glade/preferences_window.glade.h:26 msgid "Ask status message when I:" msgstr "Erfrage Status-Nachricht beim: " -#: ../data/glade/preferences_window.glade.h:30 +#: ../data/glade/preferences_window.glade.h:27 msgid "" "Autodetect on every Gajim startup\n" "Always use GNOME default applications\n" @@ -1706,19 +1712,19 @@ msgstr "" "Immer XFCE4 Standard-Anwendungen verwenden\n" "Benutzerdefiniert" -#: ../data/glade/preferences_window.glade.h:35 +#: ../data/glade/preferences_window.glade.h:32 msgid "Chat message:" msgstr "Chat-Nachricht:" -#: ../data/glade/preferences_window.glade.h:36 +#: ../data/glade/preferences_window.glade.h:33 msgid "Check on startup if Gajim is the _default Jabber client" msgstr "Beim Starten überprüfen, ob Gajim der _Standard-Jabber-Client ist" -#: ../data/glade/preferences_window.glade.h:37 +#: ../data/glade/preferences_window.glade.h:34 msgid "Configure color and font of the interface" msgstr "Farbe und Schriftart für die Oberfläche konfigurieren" -#: ../data/glade/preferences_window.glade.h:38 +#: ../data/glade/preferences_window.glade.h:35 msgid "" "Detached roster with detached chats\n" "Detached roster with single chat\n" @@ -1732,23 +1738,23 @@ msgstr "" "seperate Kontaktliste gruppiert nach Benutzerkonto\n" "seperate Kontaktliste gruppiert nach Typ" -#: ../data/glade/preferences_window.glade.h:43 +#: ../data/glade/preferences_window.glade.h:40 msgid "Display _extra email details" msgstr "Zeige zusätzliche _E-Mail-Details" -#: ../data/glade/preferences_window.glade.h:44 +#: ../data/glade/preferences_window.glade.h:41 msgid "Display a_vatars of contacts in roster" msgstr "Zeige A_vatare von Kontakten im Roster" -#: ../data/glade/preferences_window.glade.h:45 +#: ../data/glade/preferences_window.glade.h:42 msgid "Display status _messages of contacts in roster" msgstr "Zeige die Status_nachrichten von Kontakten im Roster" -#: ../data/glade/preferences_window.glade.h:46 +#: ../data/glade/preferences_window.glade.h:43 msgid "Emoticons:" msgstr "Emoticons:" -#: ../data/glade/preferences_window.glade.h:47 +#: ../data/glade/preferences_window.glade.h:44 msgid "" "Gajim can send and receive meta-information related to a conversation you " "may have with a contact. Here you can specify which chatstates you want to " @@ -1758,7 +1764,7 @@ msgstr "" "betrifft, empfangen und versenden. Hier können Sie definieren, welche " "Chatstatus Sie in Ihrem Chatfenster anzeigen möchten." -#: ../data/glade/preferences_window.glade.h:48 +#: ../data/glade/preferences_window.glade.h:45 msgid "" "Gajim can send and receive meta-information related to a conversation you " "may have with a contact. Here you can specify which chatstates you want to " @@ -1768,7 +1774,7 @@ msgstr "" "betrifft, empfangen und versenden. Hier können Sie definieren, welche " "Chatstates Sie Ihrem Gegenüber senden möchten." -#: ../data/glade/preferences_window.glade.h:49 +#: ../data/glade/preferences_window.glade.h:46 msgid "" "Gajim will notify you via a popup window in the bottom right of the screen " "about contacts that just signed out" @@ -1776,25 +1782,18 @@ msgstr "" "Gajim wird Sie über ein Popup-Fenster in der rechten unteren Bildschirmecke " "über Kontakte informieren, die sich gerade abgemeldet haben" -#: ../data/glade/preferences_window.glade.h:51 +#: ../data/glade/preferences_window.glade.h:48 msgid "Hide all buttons in chat windows" msgstr "Versteckt alle Knöpfe im Chat-Fenster" -#: ../data/glade/preferences_window.glade.h:52 -msgid "" -"If checked, Gajim log GPG or E2E encrypted messages. However, when using E2E " -"encryption the remote site has to agree on logging. If the other side has " -"declined logging, your messages will NOT be logged." -msgstr "" - -#: ../data/glade/preferences_window.glade.h:53 +#: ../data/glade/preferences_window.glade.h:49 msgid "" "If checked, Gajim will allow others to detect the operation system you are " "using" msgstr "" "Wenn aktiviert, erlaubt Gajim anderen dein Betriebssystem festzustellen" -#: ../data/glade/preferences_window.glade.h:54 +#: ../data/glade/preferences_window.glade.h:50 msgid "" "If checked, Gajim will also include information about the sender of the new " "emails" @@ -1802,20 +1801,22 @@ msgstr "" "Wenn aktiviert, wird Gajim auch Informationen über den Absender von neuen E-" "Mails hinzufügen." -#: ../data/glade/preferences_window.glade.h:55 +#: ../data/glade/preferences_window.glade.h:51 msgid "" "If checked, Gajim will change status to Away when the computer is unused." msgstr "" "Wenn aktiviert, wird Gajim den Status in Abwesend ändern, wenn der Computer " "nicht genutzt wird." -#: ../data/glade/preferences_window.glade.h:56 +#: ../data/glade/preferences_window.glade.h:52 msgid "" "If checked, Gajim will change status to Not Available when the computer has " "not been used even longer" msgstr "" +"Wenn aktiviert, wird Gajim den Status in Nicht Verfügbar ändern, wenn der " +"Computer länger nicht genutzt wird." -#: ../data/glade/preferences_window.glade.h:57 +#: ../data/glade/preferences_window.glade.h:53 msgid "" "If checked, Gajim will display avatars of contacts in roster window and in " "group chats" @@ -1823,7 +1824,7 @@ msgstr "" "Wenn aktiviert, wird Gajim einen Avatar im Roster-Fenster und in " "Gruppenchats einblenden" -#: ../data/glade/preferences_window.glade.h:58 +#: ../data/glade/preferences_window.glade.h:54 msgid "" "If checked, Gajim will display status messages of contacts under the contact " "name in roster window and in group chats" @@ -1831,14 +1832,18 @@ msgstr "" "Wenn aktiviert, wird Gajim für jeden Kontakt unter dem Kontaktnamen im " "Roster und im Gruppenchat-Fenster die Statusnachricht anzeigen" -#: ../data/glade/preferences_window.glade.h:59 +#: ../data/glade/preferences_window.glade.h:55 msgid "" "If checked, Gajim will highlight spelling errors in input fields of chat " "windows. If no language is explicitly set via right click on the input " "field, the default language will be used for this contact or group chat." msgstr "" +"Wenn aktiviert, wird Gajim Rechtschreibfehler im Chat-Eingabefenster " +"hervorheben. Wenn keine Sprache durch setzen mittels Rechtsklick im " +"Eingabefeld definiert wurde, wird die Standardsprache für diesen Kontakt " +"oder Gruppenchat verwendet" -#: ../data/glade/preferences_window.glade.h:60 +#: ../data/glade/preferences_window.glade.h:56 msgid "" "If checked, Gajim will ignore incoming events from unauthorized contacts. " "Use with caution, because it blocks all messages from any contact that is " @@ -1848,7 +1853,18 @@ msgstr "" "Kontakten ignorieren. Mit Vorsicht benutzen, weil alle Nachrichten von " "Kontakten blockiert werden, die nicht in deiner Kontaktliste sind" -#: ../data/glade/preferences_window.glade.h:61 +#: ../data/glade/preferences_window.glade.h:57 +msgid "" +"If checked, Gajim will keep logs for encrypted messages. Please note that " +"when using E2E encryption the remote party has to agree on logging, else the " +"messages will not be logged." +msgstr "" +"Wenn aktiviert, wird Gajim Logs von verschlüsselten Nachrichten aufzeichnen. " +"Bitte bedenken Sie, dass bei der Benutzung von Punkt-zu-Punkt-" +"Verschlüsselung der Chatpartner zustimmen muss; ansonsten wird nicht " +"mitgeschnitten." + +#: ../data/glade/preferences_window.glade.h:58 msgid "" "If checked, Gajim will show a notification when a new e-mail is received via " "GMail" @@ -1856,7 +1872,7 @@ msgstr "" "Wenn aktiviert, wird Gajim eine Benachrichtigung anzeigen, wenn eine neue E-" "Mail über Googlemail empfangen wurden" -#: ../data/glade/preferences_window.glade.h:62 +#: ../data/glade/preferences_window.glade.h:59 msgid "" "If checked, Gajim will sort contacts in roster window and groupchats by " "their status and not by the shown name" @@ -1864,7 +1880,7 @@ msgstr "" "Wenn aktiviert, wird Gajim die Kontakte in der Kontaktliste und Gruppenchats " "nach ihrem Status und nicht nach dem angezeigten Namen sortieren" -#: ../data/glade/preferences_window.glade.h:63 +#: ../data/glade/preferences_window.glade.h:60 msgid "" "If checked, Gajim will use protocol-specific status icons. (eg. A contact " "from MSN will have the equivalent msn icon for status online, away, busy, " @@ -1874,13 +1890,15 @@ msgstr "" "verwenden (Ein Kontakt aus MSN wird z.B. das äquivalente MSN-Symbol für den " "Status \"Anwesend\", \"Abwesend\", \"Beschäftigt\", etc. haben)" -#: ../data/glade/preferences_window.glade.h:64 +#: ../data/glade/preferences_window.glade.h:61 msgid "" "If enabled, Gajim will not ask for a status message. The specified default " "message will be used instead." msgstr "" +"Wenn aktiviert, wird Gajim nicht nach einer Statusnachricht fragen. Die " +"Standardnachricht wird anstelle benutzt" -#: ../data/glade/preferences_window.glade.h:65 +#: ../data/glade/preferences_window.glade.h:62 msgid "" "If not disabled, Gajim will replace ascii smilies like ':)' with equivalent " "animated or static graphical emoticons" @@ -1888,53 +1906,43 @@ msgstr "" "Wenn nicht deaktivert, wird Gajim ASCII-Smilies wie ':)' mit äquivalenten " "graphischen Emoticons ersetzen" -#: ../data/glade/preferences_window.glade.h:66 +#: ../data/glade/preferences_window.glade.h:63 msgid "Ignore rich content in incoming messages" msgstr "Formatierten Inhalt in ankommenden Nachrichten ignorieren" -#: ../data/glade/preferences_window.glade.h:67 +#: ../data/glade/preferences_window.glade.h:64 msgid "Log _encrypted chat session" msgstr "_Verschlüsselte Sitzungen mitschneiden" -#: ../data/glade/preferences_window.glade.h:68 +#: ../data/glade/preferences_window.glade.h:65 msgid "Ma_nage..." msgstr "_Verwalten ..." -#: ../data/glade/preferences_window.glade.h:69 ../src/roster_window.py:4480 -msgid "Mood" -msgstr "" - -#. holds nickname -#: ../data/glade/preferences_window.glade.h:70 ../src/history_manager.py:149 -#: ../src/history_manager.py:201 -msgid "Nickname" -msgstr "Spitzname" - -#: ../data/glade/preferences_window.glade.h:71 +#: ../data/glade/preferences_window.glade.h:66 msgid "Notifications" msgstr "Benachrichtigungen" -#: ../data/glade/preferences_window.glade.h:72 +#: ../data/glade/preferences_window.glade.h:67 msgid "Notify me about contacts that sign _in" msgstr "Über Kontakte benachrichtigen, die sich einloggen" -#: ../data/glade/preferences_window.glade.h:73 +#: ../data/glade/preferences_window.glade.h:68 msgid "Notify me about contacts that sign _out" msgstr "Über Kontakte benachrichtigen, die sich abmelden" -#: ../data/glade/preferences_window.glade.h:74 +#: ../data/glade/preferences_window.glade.h:69 msgid "Notify on new _GMail email" msgstr "Über neue _Google Mail E-Mails benachrichtigen" -#: ../data/glade/preferences_window.glade.h:75 +#: ../data/glade/preferences_window.glade.h:70 msgid "Personal Events" msgstr "Persönliche Ereignisse" -#: ../data/glade/preferences_window.glade.h:76 +#: ../data/glade/preferences_window.glade.h:71 msgid "Play _sounds" msgstr "_Klänge abspielen" -#: ../data/glade/preferences_window.glade.h:77 +#: ../data/glade/preferences_window.glade.h:72 msgid "" "Pop it up\n" "Notify me about it\n" @@ -1944,23 +1952,19 @@ msgstr "" "Benachrichtige mich darüber\n" "Nur im Roster anzeigen" -#: ../data/glade/preferences_window.glade.h:80 +#: ../data/glade/preferences_window.glade.h:75 msgid "Preferences" msgstr "Einstellungen" -#: ../data/glade/preferences_window.glade.h:81 -msgid "Receive your contact's:" -msgstr "Empfang von Kontakten:" - -#: ../data/glade/preferences_window.glade.h:82 +#: ../data/glade/preferences_window.glade.h:76 msgid "Sign _in" msgstr "_anmelden" -#: ../data/glade/preferences_window.glade.h:83 +#: ../data/glade/preferences_window.glade.h:77 msgid "Sign _out" msgstr "a_bmelden" -#: ../data/glade/preferences_window.glade.h:84 +#: ../data/glade/preferences_window.glade.h:78 msgid "" "Some messages may include rich content (formatting, colors etc). If checked, " "Gajim will just display the raw message text." @@ -1969,127 +1973,127 @@ msgstr "" "Schriftarten etc.). Wenn aktiviert, wird Gajim nur den puren Nachrichtentext " "anzeigen." -#: ../data/glade/preferences_window.glade.h:85 +#: ../data/glade/preferences_window.glade.h:79 ../src/config.py:382 msgid "Status" msgstr "Status" -#: ../data/glade/preferences_window.glade.h:86 +#: ../data/glade/preferences_window.glade.h:80 msgid "Status _iconset:" msgstr "Status-S_ymbole:" -#: ../data/glade/preferences_window.glade.h:87 +#: ../data/glade/preferences_window.glade.h:81 msgid "Style" msgstr "Stil" -#: ../data/glade/preferences_window.glade.h:88 +#: ../data/glade/preferences_window.glade.h:82 msgid "T_heme:" msgstr "T_hema:" -#: ../data/glade/preferences_window.glade.h:89 +#: ../data/glade/preferences_window.glade.h:83 msgid "" "The auto away status message. If empty, Gajim will not change the current " "status message" msgstr "" +"Die automatische Away-Status-Nachricht. Wenn leer, wird Gajim die aktuelle " +"Statusnachricht belassen" -#: ../data/glade/preferences_window.glade.h:90 +#: ../data/glade/preferences_window.glade.h:84 msgid "" "The auto not available status message. If empty, Gajim will not change the " "current status message" msgstr "" +"Die automatische Nicht-Verfügbar-Status-Nachricht. Wenn leer, wird Gajim die " +"aktuelle Statusnachricht belassen" -#: ../data/glade/preferences_window.glade.h:91 -msgid "Tune" -msgstr "Musiktitel:" - -#: ../data/glade/preferences_window.glade.h:92 +#: ../data/glade/preferences_window.glade.h:85 msgid "Use _transports icons" msgstr "_Transport-Symbole verwenden" -#: ../data/glade/preferences_window.glade.h:93 +#: ../data/glade/preferences_window.glade.h:86 msgid "Use system _default" msgstr "Benutze System-_Voreinstellung" -#: ../data/glade/preferences_window.glade.h:94 +#: ../data/glade/preferences_window.glade.h:87 msgid "When new event is received:" msgstr "Wenn neue Nachricht empfangen wird" -#: ../data/glade/preferences_window.glade.h:95 +#: ../data/glade/preferences_window.glade.h:88 msgid "_Away after:" msgstr "Automatisch _abwesend nach:" -#: ../data/glade/preferences_window.glade.h:96 +#: ../data/glade/preferences_window.glade.h:89 msgid "_Browser:" msgstr "_Browser:" -#: ../data/glade/preferences_window.glade.h:97 +#: ../data/glade/preferences_window.glade.h:90 msgid "_Display chat state notifications:" msgstr "Zeige Benachrichtigung über Chatstatus:" -#: ../data/glade/preferences_window.glade.h:98 +#: ../data/glade/preferences_window.glade.h:91 msgid "_File manager:" msgstr "_Dateimanager:" -#: ../data/glade/preferences_window.glade.h:99 +#: ../data/glade/preferences_window.glade.h:92 msgid "_Highlight misspelled words" msgstr "Falsch geschriebene Wörter _hervorheben" -#: ../data/glade/preferences_window.glade.h:100 +#: ../data/glade/preferences_window.glade.h:93 msgid "_Ignore events from contacts not in the roster" msgstr "Ereignisse von Kontakten, die nicht in der Liste sind, _ignorieren" -#: ../data/glade/preferences_window.glade.h:101 +#: ../data/glade/preferences_window.glade.h:94 msgid "_Incoming message:" msgstr "_Eingehende Nachricht:" -#: ../data/glade/preferences_window.glade.h:102 +#: ../data/glade/preferences_window.glade.h:95 msgid "_Log status changes of contacts" msgstr "_Logge die Statusveränderungen von Kontakten" -#: ../data/glade/preferences_window.glade.h:103 +#: ../data/glade/preferences_window.glade.h:96 msgid "_Mail client:" msgstr "_Mail-Programm:" -#: ../data/glade/preferences_window.glade.h:104 +#: ../data/glade/preferences_window.glade.h:97 msgid "_Make message windows compact" msgstr "Nachrichtenfenster _kompakt anzeigen" -#: ../data/glade/preferences_window.glade.h:105 +#: ../data/glade/preferences_window.glade.h:98 msgid "_Not available after:" msgstr "Automatisch _nicht verfügbar nach:" -#: ../data/glade/preferences_window.glade.h:106 +#: ../data/glade/preferences_window.glade.h:99 msgid "_Open..." msgstr "Ö_ffnen ..." -#: ../data/glade/preferences_window.glade.h:107 +#: ../data/glade/preferences_window.glade.h:100 msgid "_Outgoing message:" msgstr "_Ausgehende Nachricht:" -#: ../data/glade/preferences_window.glade.h:108 +#: ../data/glade/preferences_window.glade.h:101 msgid "_Reset to Default Colors" msgstr "Auf Standard-Farben zu_rücksetzen" -#: ../data/glade/preferences_window.glade.h:109 +#: ../data/glade/preferences_window.glade.h:102 msgid "_Send chat state notifications:" msgstr "_Sende Benachrichtigung über Chatstatus:" -#: ../data/glade/preferences_window.glade.h:110 +#: ../data/glade/preferences_window.glade.h:103 msgid "_Sort contacts by status" msgstr "Kontakte nach Status _sortieren" -#: ../data/glade/preferences_window.glade.h:111 +#: ../data/glade/preferences_window.glade.h:104 msgid "_Status message:" msgstr "_Statusnachricht:" -#: ../data/glade/preferences_window.glade.h:112 +#: ../data/glade/preferences_window.glade.h:105 msgid "_URL highlight:" msgstr "" -#: ../data/glade/preferences_window.glade.h:113 +#: ../data/glade/preferences_window.glade.h:106 msgid "_Window behavior:" msgstr "Fensterverhalten" -#: ../data/glade/preferences_window.glade.h:114 +#: ../data/glade/preferences_window.glade.h:107 msgid "minutes" msgstr "Minuten" @@ -2133,7 +2137,7 @@ msgstr "Jabber-ID:" msgid "Order:" msgstr "Reihenfolge:" -#: ../data/glade/privacy_list_window.glade.h:11 ../src/dialogs.py:2500 +#: ../data/glade/privacy_list_window.glade.h:11 ../src/dialogs.py:2531 msgid "Privacy List" msgstr "Privatliste" @@ -2338,9 +2342,9 @@ msgstr "Konto _entfernen (in Gajim und auf dem Server)" #. Remove group #. Remove #: ../data/glade/remove_account_window.glade.h:4 -#: ../data/glade/roster_contact_context_menu.glade.h:19 -#: ../src/roster_window.py:4754 ../src/roster_window.py:5192 -#: ../src/roster_window.py:5320 +#: ../data/glade/roster_contact_context_menu.glade.h:18 +#: ../src/roster_window.py:4836 ../src/roster_window.py:5274 +#: ../src/roster_window.py:5403 msgid "_Remove" msgstr "_Entfernen" @@ -2362,74 +2366,70 @@ msgstr "_Gruppen bearbeiten ..." #. Execute Command #: ../data/glade/roster_contact_context_menu.glade.h:5 -#: ../src/roster_window.py:5263 +#: ../src/roster_window.py:5346 msgid "Execute Command..." msgstr "_Befehl ausführen ..." #. Invite to #. Invite to Groupchat #: ../data/glade/roster_contact_context_menu.glade.h:6 -#: ../src/roster_window.py:4675 ../src/roster_window.py:5145 +#: ../src/roster_window.py:4755 ../src/roster_window.py:5227 msgid "In_vite to" msgstr "_Einladen zu" -#: ../data/glade/roster_contact_context_menu.glade.h:7 -msgid "Play Tic Tac Toe" -msgstr "" - #. Send Custom Status -#: ../data/glade/roster_contact_context_menu.glade.h:8 -#: ../src/roster_window.py:4684 ../src/roster_window.py:5230 +#: ../data/glade/roster_contact_context_menu.glade.h:7 +#: ../src/roster_window.py:4765 ../src/roster_window.py:5312 msgid "Send Cus_tom Status" msgstr "Benutzerdefinierten Status senden" -#: ../data/glade/roster_contact_context_menu.glade.h:9 +#: ../data/glade/roster_contact_context_menu.glade.h:8 msgid "Send Single _Message..." msgstr "Einzelne _Nachricht senden ..." -#: ../data/glade/roster_contact_context_menu.glade.h:10 +#: ../data/glade/roster_contact_context_menu.glade.h:9 msgid "Send _File..." msgstr "_Datei senden ..." -#: ../data/glade/roster_contact_context_menu.glade.h:11 +#: ../data/glade/roster_contact_context_menu.glade.h:10 msgid "Set Custom _Avatar..." msgstr "Benutzerdefinierten Avatar wählen ..." -#: ../data/glade/roster_contact_context_menu.glade.h:12 +#: ../data/glade/roster_contact_context_menu.glade.h:11 #: ../data/glade/zeroconf_contact_context_menu.glade.h:5 msgid "Start _Chat" msgstr "_Chat starten" -#: ../data/glade/roster_contact_context_menu.glade.h:14 +#: ../data/glade/roster_contact_context_menu.glade.h:13 msgid "_Allow him/her to see my status" msgstr "_Erlaube Kontakt, meinen Status zu sehen" -#: ../data/glade/roster_contact_context_menu.glade.h:15 -#: ../src/roster_window.py:4745 ../src/roster_window.py:5182 -#: ../src/roster_window.py:5310 +#: ../data/glade/roster_contact_context_menu.glade.h:14 +#: ../src/roster_window.py:4827 ../src/roster_window.py:5264 +#: ../src/roster_window.py:5393 msgid "_Block" msgstr "_Blockieren" -#: ../data/glade/roster_contact_context_menu.glade.h:16 +#: ../data/glade/roster_contact_context_menu.glade.h:15 msgid "_Forbid him/her to see my status" msgstr "_Verbiete Kontakt, meinen Status zu sehen" -#: ../data/glade/roster_contact_context_menu.glade.h:18 +#: ../data/glade/roster_contact_context_menu.glade.h:17 #: ../data/glade/zeroconf_contact_context_menu.glade.h:7 msgid "_Manage Contact" msgstr "Kontakt verwalten" -#: ../data/glade/roster_contact_context_menu.glade.h:20 +#: ../data/glade/roster_contact_context_menu.glade.h:19 msgid "_Rename..." msgstr "_Umbenennen ..." -#: ../data/glade/roster_contact_context_menu.glade.h:21 +#: ../data/glade/roster_contact_context_menu.glade.h:20 msgid "_Subscription" msgstr "_Abonnement" -#: ../data/glade/roster_contact_context_menu.glade.h:22 -#: ../src/roster_window.py:4739 ../src/roster_window.py:5176 -#: ../src/roster_window.py:5307 +#: ../data/glade/roster_contact_context_menu.glade.h:21 +#: ../src/roster_window.py:4821 ../src/roster_window.py:5258 +#: ../src/roster_window.py:5390 msgid "_Unblock" msgstr "Entblocken" @@ -2524,7 +2524,7 @@ msgid "_Add contact" msgstr "_Kontakt hinzufügen" #. Information -#: ../data/glade/search_window.glade.h:4 ../src/roster_window.py:5332 +#: ../data/glade/search_window.glade.h:4 ../src/roster_window.py:5415 msgid "_Information" msgstr "Informationen" @@ -2616,6 +2616,10 @@ msgstr "Abonnementanfrage" msgid "_Deny" msgstr "_Ablehnen" +#: ../data/glade/synchronise_contacts_dialog.glade.h:1 +msgid "Select the account with which to synchronise" +msgstr "Wählen Sie das Benutzerkonto mit welchem synchronisiert werden soll" + #: ../data/glade/synchronise_select_account_dialog.glade.h:1 msgid "Select the account with which you want to synchronise" msgstr "Das Konto auswählen, mit dem Sie synchronisieren möchten" @@ -2743,13 +2747,13 @@ msgstr "Open_PGP-Schlüssel zuweisen" #. Edit Groups #: ../data/glade/zeroconf_contact_context_menu.glade.h:3 -#: ../src/roster_window.py:5165 +#: ../src/roster_window.py:5247 msgid "Edit _Groups" msgstr "_Gruppen bearbeiten" #. Rename #: ../data/glade/zeroconf_contact_context_menu.glade.h:8 -#: ../src/roster_window.py:5290 +#: ../src/roster_window.py:5373 msgid "_Rename" msgstr "_Umbenennen" @@ -2966,56 +2970,55 @@ msgstr "Ihre Nachricht kann erst gesendet werden, wenn Sie verbunden sind." #. Add to roster button #. add_to_roster_menuitem -#: ../src/chat_control.py:1065 ../src/chat_control.py:1797 -#: ../src/conversation_textview.py:724 ../src/dialogs.py:835 -#: ../src/gajim.py:1016 ../src/gajim.py:1017 ../src/gajim.py:1564 -#: ../src/gajim.py:1700 ../src/roster_window.py:758 -#: ../src/roster_window.py:768 ../src/roster_window.py:855 -#: ../src/roster_window.py:1386 ../src/roster_window.py:1388 -#: ../src/roster_window.py:1716 ../src/roster_window.py:2976 -#: ../src/roster_window.py:4857 ../src/roster_window.py:5031 +#: ../src/chat_control.py:1065 ../src/chat_control.py:1855 #: ../src/common/contacts.py:94 ../src/common/helpers.py:66 -#: ../src/common/helpers.py:280 +#: ../src/common/helpers.py:280 ../src/conversation_textview.py:724 +#: ../src/dialogs.py:864 ../src/gajim.py:1036 ../src/gajim.py:1037 +#: ../src/gajim.py:1706 ../src/roster_window.py:759 +#: ../src/roster_window.py:770 ../src/roster_window.py:857 +#: ../src/roster_window.py:1417 ../src/roster_window.py:1419 +#: ../src/roster_window.py:1772 ../src/roster_window.py:3026 +#: ../src/roster_window.py:4939 ../src/roster_window.py:5112 msgid "Not in Roster" msgstr "Nicht in der Kontaktliste" -#: ../src/chat_control.py:1126 ../src/chat_control.py:1373 +#: ../src/chat_control.py:1128 ../src/chat_control.py:1395 msgid "GPG encryption enabled" msgstr "Verschlüsselung aktiviert" -#: ../src/chat_control.py:1283 +#: ../src/chat_control.py:1305 #, python-format msgid "%(nickname)s from group chat %(room_name)s" msgstr "%(nickname)s aus Gruppenchat %(room_name)s" -#: ../src/chat_control.py:1364 +#: ../src/chat_control.py:1386 msgid "GPG encryption disabled" msgstr "Verschlüsselung deaktiviert" -#: ../src/chat_control.py:1379 ../src/chat_control.py:1596 +#: ../src/chat_control.py:1401 ../src/chat_control.py:1629 msgid "Session WILL NOT be logged" msgstr "Sitzung wird nicht aufgezeichnet" -#: ../src/chat_control.py:1381 ../src/chat_control.py:1594 +#: ../src/chat_control.py:1403 ../src/chat_control.py:1627 msgid "Session WILL be logged" msgstr "Sitzung wird aufgezeichnet" -#: ../src/chat_control.py:1445 ../src/groupchat_control.py:1492 +#: ../src/chat_control.py:1478 ../src/groupchat_control.py:1524 #, python-format msgid "Commands: %s" msgstr "Befehle: %s" -#: ../src/chat_control.py:1448 ../src/groupchat_control.py:1506 +#: ../src/chat_control.py:1481 ../src/groupchat_control.py:1538 #, python-format msgid "Usage: /%s, clears the text window." msgstr "Bedienung: /%s, leert das Textfenster." -#: ../src/chat_control.py:1451 ../src/groupchat_control.py:1511 +#: ../src/chat_control.py:1484 ../src/groupchat_control.py:1543 #, python-format msgid "Usage: /%s, hide the chat buttons." msgstr "Verwendung: /%s, versteckt die Chatbuttons." -#: ../src/chat_control.py:1454 ../src/groupchat_control.py:1527 +#: ../src/chat_control.py:1487 ../src/groupchat_control.py:1559 #, python-format msgid "" "Usage: /%s , sends action to the current group chat. Use third " @@ -3024,48 +3027,46 @@ msgstr "" "Bedienung: /%s , sendet die Aktion im aktuellen Gruppenchat. " "Verwende dritte Person, z.B. /%s explodiert)." -#: ../src/chat_control.py:1458 +#: ../src/chat_control.py:1491 #, python-format msgid "Usage: /%s, sends a ping to the contact" msgstr "Bedienung: /%s, sendet einen Ping zum Kontakt" -#: ../src/chat_control.py:1461 +#: ../src/chat_control.py:1494 #, python-format msgid "Usage: /%s, send the message to the contact" msgstr "Verwendung: /%s, sendet eine Nachricht zum Kontakt" -#: ../src/chat_control.py:1464 ../src/groupchat_control.py:1550 +#: ../src/chat_control.py:1497 ../src/groupchat_control.py:1582 #, python-format msgid "No help info for /%s" msgstr "Keine Hilfe vorhanden für /%s" -#: ../src/chat_control.py:1583 +#: ../src/chat_control.py:1616 msgid "Session negotiation cancelled" msgstr "Sitzungs-Aushandlung abgebrochen" -#: ../src/chat_control.py:1590 +#: ../src/chat_control.py:1623 msgid "E2E encryption enabled" msgstr "Verschlüsselung aktiviert" -#: ../src/chat_control.py:1600 +#: ../src/chat_control.py:1633 msgid "E2E encryption disabled" msgstr "Verschlüsselung deaktiviert" -#: ../src/chat_control.py:1626 ../src/chat_control.py:1637 -msgid "The following message was NOT encrypted" -msgstr "[Die folgende Nachricht wurde nicht verschlüsselt]" - -#: ../src/chat_control.py:1632 -msgid "The following message was encrypted" +#: ../src/chat_control.py:1661 ../src/chat_control.py:1670 +#: ../src/chat_control.py:1677 ../src/chat_control.py:1683 +#, fuzzy +msgid "The following message was " msgstr "[Die folgende Nachricht wurde verschlüsselt]" #. %s is being replaced in the code with JID -#: ../src/chat_control.py:1998 +#: ../src/chat_control.py:2057 #, python-format msgid "You just received a new message from \"%s\"" msgstr "Sie haben eine neue Nachricht von \"%s\"" -#: ../src/chat_control.py:1999 +#: ../src/chat_control.py:2058 msgid "" "If you close this tab and you have history disabled, this message will be " "lost." @@ -3073,3736 +3074,23 @@ msgstr "" "Wenn sie das Fenster schließen und der Verlauf abgeschaltet ist, geht die " "Nachricht verloren." -#: ../src/chat_control.py:2131 ../src/gajim.py:158 +#: ../src/chat_control.py:2190 ../src/gajim.py:159 msgid "Database Error" msgstr "Datenbankfehler" -#: ../src/chat_control.py:2132 ../src/gajim.py:159 +#: ../src/chat_control.py:2191 ../src/gajim.py:160 #, python-format msgid "" "The database file (%s) cannot be read. Try to repare it or remove it (all " "history will be lost)." msgstr "" +"Die Datenbankdatei (%s) konnte nicht gelesen werden. Versuchen Sie diese zu " +"reparieren oder zu löschen (dabei werden alle Verläufe verloren gehen)." -#: ../src/config.py:129 ../src/config.py:633 -msgid "Disabled" -msgstr "Deaktiviert" - -#: ../src/config.py:332 -msgid "Active" -msgstr "Aktiv" - -#: ../src/config.py:340 -msgid "Event" -msgstr "Ereignis" - -#: ../src/config.py:465 -msgid "Always use OS/X default applications" -msgstr "" - -#: ../src/config.py:466 -msgid "Custom" -msgstr "Benutzerdefiniert" - -#: ../src/config.py:702 ../src/dialogs.py:1148 -#, python-format -msgid "Dictionary for lang %s not available" -msgstr "Wörterburch für Sprache %s nicht verfügbar" - -#: ../src/config.py:703 -#, python-format -msgid "" -"You have to install %s dictionary to use spellchecking, or choose another " -"language by setting the speller_language option." -msgstr "" -"Sie müssen das Wörterbuch %s installieren oder eine andere Sprache wählen, " -"um die Rechtschreibprüfung zu nutzen." - -#: ../src/config.py:1068 -msgid "status message title" -msgstr "Statusbetreff" - -#: ../src/config.py:1068 -msgid "status message text" -msgstr "Statusnachricht" - -#: ../src/config.py:1105 -msgid "First Message Received" -msgstr "Erste empfangene Nachricht" - -#: ../src/config.py:1106 -msgid "Next Message Received Focused" -msgstr "Nächste empfangene Nachricht fixiert" - -#: ../src/config.py:1108 -msgid "Next Message Received Unfocused" -msgstr "Nächste empfangene Nachricht nicht fixiert" - -#: ../src/config.py:1109 -msgid "Contact Connected" -msgstr "Kontakt verbunden" - -#: ../src/config.py:1110 -msgid "Contact Disconnected" -msgstr "Kontakt nicht verbunden" - -#: ../src/config.py:1111 -msgid "Message Sent" -msgstr "Nachricht gesendet" - -#: ../src/config.py:1112 -msgid "Group Chat Message Highlight" -msgstr "Gruppenchat Nachrichten-Hervorhebung" - -#: ../src/config.py:1113 -msgid "Group Chat Message Received" -msgstr "Gruppenchat-Nachricht empfangen" - -#: ../src/config.py:1114 -msgid "GMail Email Received" -msgstr "E-Mail über Googlemail empfangen" - -#. Name column -#: ../src/config.py:1371 ../src/dialogs.py:1761 ../src/dialogs.py:1825 -#: ../src/disco.py:742 ../src/disco.py:1534 ../src/disco.py:1780 -#: ../src/history_window.py:92 -msgid "Name" -msgstr "Name" - -#: ../src/config.py:1450 ../src/common/config.py:408 -msgid "Be right back." -msgstr "Bin gleich zurück." - -#: ../src/config.py:1454 -msgid "Relogin now?" -msgstr "Jetzt neu einloggen?" - -#: ../src/config.py:1455 -msgid "If you want all the changes to apply instantly, you must relogin." -msgstr "" -"Wenn die Änderungen sofort übernommen werden sollen, müssen Sie sich neu " -"einloggen." - -#: ../src/config.py:1585 ../src/config.py:1684 -msgid "OpenPGP is not usable in this computer" -msgstr "OpenPGP kann auf diesem Computer nicht genutzt werden" - -#: ../src/config.py:1720 ../src/config.py:1761 -msgid "Unread events" -msgstr "Ungelesene Ereignisse" - -#: ../src/config.py:1721 -msgid "Read all pending events before removing this account." -msgstr "Alle ungelesenen Ereignisse lesen, bevor der Account entfernt wird." - -#: ../src/config.py:1747 -#, python-format -msgid "You have opened chat in account %s" -msgstr "Sie haben mit Account %s einen Chat geöffnet" - -#: ../src/config.py:1748 -msgid "All chat and groupchat windows will be closed. Do you want to continue?" -msgstr "Alle Chatfenster werden geschlossen. Fortfahren?" - -#: ../src/config.py:1757 -msgid "You are currently connected to the server" -msgstr "Sie sind mit dem Server verbunden" - -#: ../src/config.py:1758 -msgid "To change the account name, you must be disconnected." -msgstr "Verbindung muss beendet werden, um Kontonamen zu ändern." - -#: ../src/config.py:1762 -msgid "To change the account name, you must read all pending events." -msgstr "Um Kontonamen zu ändern, müssen Sie alle neunen Ereignisse lesen." - -#: ../src/config.py:1768 -msgid "Account Name Already Used" -msgstr "Kontoname wird bereits verwendet" - -#: ../src/config.py:1769 -msgid "" -"This name is already used by another of your accounts. Please choose another " -"name." -msgstr "" -"Dieser Name wird bereits für einen anderen Ihrer Accounts verwendet. Bitte " -"wählenSie einen anderen Namen." - -#: ../src/config.py:1773 ../src/config.py:1777 -msgid "Invalid account name" -msgstr "Ungültiger Kontoname" - -#: ../src/config.py:1774 -msgid "Account name cannot be empty." -msgstr "Kontoname darf nicht leer sein." - -#: ../src/config.py:1778 -msgid "Account name cannot contain spaces." -msgstr "Kontoname darf keine Leerzeichen enthalten." - -#: ../src/config.py:1842 -msgid "Rename Account" -msgstr "Konto umbenennen" - -#: ../src/config.py:1843 -#, python-format -msgid "Enter a new name for account %s" -msgstr "Geben Sie einen neuen Namen für das Konto %s ein" - -#: ../src/config.py:1861 ../src/config.py:1869 ../src/config.py:1911 -#: ../src/config.py:3128 ../src/dataforms_widget.py:535 -msgid "Invalid Jabber ID" -msgstr "Ungültige Jabber ID" - -#: ../src/config.py:1870 -msgid "A Jabber ID must be in the form \"user@servername\"." -msgstr "Jabber ID muss in der Form \"user@servername\" sein." - -#: ../src/config.py:2058 ../src/config.py:3200 -msgid "Invalid entry" -msgstr "Ungültiger Eintrag" - -#: ../src/config.py:2059 ../src/config.py:3201 -msgid "Custom port must be a port number." -msgstr "Proxy Port muss eine Portnummer sein." - -#: ../src/config.py:2080 ../src/config.py:3679 -msgid "Failed to get secret keys" -msgstr "Holen der geheimen Schlüssel fehlgeschlagen" - -#: ../src/config.py:2081 ../src/config.py:3680 -msgid "There was a problem retrieving your OpenPGP secret keys." -msgstr "Es gab ein Problem beim Holen ihres geheimen OpenPGP-Schlüssels." - -#: ../src/config.py:2084 ../src/config.py:3683 -msgid "OpenPGP Key Selection" -msgstr "OpenPGP Schlüssel-Auswahl" - -#: ../src/config.py:2085 ../src/config.py:3684 -msgid "Choose your OpenPGP key" -msgstr "Wählen Sie Ihren OpenPGP Schlüssel" - -#: ../src/config.py:2125 -msgid "No such account available" -msgstr "Account nicht verfügbar" - -#: ../src/config.py:2126 -msgid "You must create your account before editing your personal information." -msgstr "" -"Sie müssen ein Konto erstellen, bevor Sie die persönlichen Informationen " -"ändern können" - -#: ../src/config.py:2133 ../src/dialogs.py:1613 ../src/dialogs.py:1749 -#: ../src/dialogs.py:1929 ../src/disco.py:426 ../src/profile_window.py:318 -msgid "You are not connected to the server" -msgstr "Sie sind nicht mit dem Server verbunden" - -#: ../src/config.py:2134 -msgid "Without a connection, you can not edit your personal information." -msgstr "" -"Sie müssen angemeldet sein, um Ihre persönlichen Informationen zu bearbeiten" - -#: ../src/config.py:2138 -msgid "Your server doesn't support Vcard" -msgstr "Ihr Server unterstützt vCard nicht" - -#: ../src/config.py:2139 -msgid "Your server can't save your personal information." -msgstr "Ihr Server kann keine persönlichen Informationen speichern." - -#: ../src/config.py:2170 -msgid "Account Local already exists." -msgstr "Ein Konto mit dem Namen 'Local' ist bereits vorhanden" - -#: ../src/config.py:2171 -msgid "Please rename or remove it before enabling link-local messaging." -msgstr "" -"Bitte benennen Sie es um oder entfernen es, bevor Sie LAN-Kontakte " -"aktivieren." - -#: ../src/config.py:2350 -#, python-format -msgid "Edit %s" -msgstr "%s ändern" - -#: ../src/config.py:2352 -#, python-format -msgid "Register to %s" -msgstr "Auf %s registrieren" - -#. list at the beginning -#: ../src/config.py:2388 -msgid "Ban List" -msgstr "Sperrliste" - -#: ../src/config.py:2389 -msgid "Member List" -msgstr "Mitgliederliste" - -#: ../src/config.py:2390 -msgid "Owner List" -msgstr "Besitzerliste" - -#: ../src/config.py:2391 -msgid "Administrator List" -msgstr "Administratorliste" - -#. Address column -#. holds JID (who said this) -#: ../src/config.py:2440 ../src/disco.py:749 ../src/history_manager.py:177 -msgid "JID" -msgstr "JID" - -#: ../src/config.py:2448 -msgid "Reason" -msgstr "Grund" - -#: ../src/config.py:2453 -msgid "Nick" -msgstr "Spitzname" - -#: ../src/config.py:2457 -msgid "Role" -msgstr "Rolle" - -#: ../src/config.py:2482 -msgid "Banning..." -msgstr "Verbannen ..." - -#. You can move '\n' before user@domain if that line is TOO BIG -#: ../src/config.py:2484 -msgid "" -"Whom do you want to ban?\n" -"\n" -msgstr "" -"Wen möchten Sie verbannen?\n" -"\n" - -#: ../src/config.py:2486 -msgid "Adding Member..." -msgstr "Mitglied hinzufügen ..." - -#: ../src/config.py:2487 -msgid "" -"Whom do you want to make a member?\n" -"\n" -msgstr "" -"Wen möchten Sie zum Mitglied machen?\n" -"\n" - -#: ../src/config.py:2489 -msgid "Adding Owner..." -msgstr "Besitzer hinzufügen ..." - -#: ../src/config.py:2490 -msgid "" -"Whom do you want to make an owner?\n" -"\n" -msgstr "" -"Wen möchten Sie zum Besitzer machen?\n" -"\n" - -#: ../src/config.py:2492 -msgid "Adding Administrator..." -msgstr "Füge Administrator hinzu ..." - -#: ../src/config.py:2493 -msgid "" -"Whom do you want to make an administrator?\n" -"\n" -msgstr "" -"Wen möchten Sie zum Administrator machen?\n" -"\n" - -#: ../src/config.py:2494 -msgid "" -"Can be one of the following:\n" -"1. user@domain/resource (only that resource matches).\n" -"2. user@domain (any resource matches).\n" -"3. domain/resource (only that resource matches).\n" -"4. domain (the domain itself matches, as does any user@domain,\n" -"domain/resource, or address containing a subdomain." -msgstr "" -"Kann eines der folgenden sein:\"1. benutzer@domain/resource (nur die " -"Resource trifft zu).\n" -"2. benutzer@domain (jede Resource trifft zu).\n" -"3 domain (die Domain selbst trifft zu, als auch jeder benutzer@domain,\n" -"jede domain/resource oder Adresse, die eine Subdomain enthält." - -#: ../src/config.py:2600 -#, python-format -msgid "Removing %s account" -msgstr "Entferne Konto %s" - -#: ../src/config.py:2615 ../src/gajim.py:1462 ../src/roster_window.py:1754 -msgid "Password Required" -msgstr "Passwort benötigt" - -#: ../src/config.py:2616 ../src/roster_window.py:1749 -#, python-format -msgid "Enter your password for account %s" -msgstr "Geben Sie ihr Passwort für %s ein" - -#: ../src/config.py:2617 ../src/roster_window.py:1755 -msgid "Save password" -msgstr "Passwort speichern" - -#: ../src/config.py:2630 -#, python-format -msgid "Account \"%s\" is connected to the server" -msgstr "Konto \"%s\" ist mit Server verbunden" - -#: ../src/config.py:2631 -msgid "If you remove it, the connection will be lost." -msgstr "Wenn Sie es entfernen, wird die Verbindung beendet." - -#: ../src/config.py:2725 -msgid "Default" -msgstr "Standard" - -#: ../src/config.py:2725 -msgid "?print_status:All" -msgstr "?print_status:Alle" - -#: ../src/config.py:2726 -msgid "Enter and leave only" -msgstr "Nur betreten und verlassen" - -#: ../src/config.py:2727 -msgid "?print_status:None" -msgstr "?print_status:Nichts" - -#: ../src/config.py:2796 -msgid "New Group Chat" -msgstr "Neuer Gruppenchat" - -#: ../src/config.py:2829 -msgid "This bookmark has invalid data" -msgstr "Dieses Lesezeichen hat ungültige Daten" - -#: ../src/config.py:2830 -msgid "" -"Please be sure to fill out server and room fields or remove this bookmark." -msgstr "Bitte Serverfeld und Raumfeld ausfüllen oder Lesezeichen löschen." - -#: ../src/config.py:3111 -msgid "Invalid username" -msgstr "Ungültiger Benutzername" - -#: ../src/config.py:3113 -msgid "You must provide a username to configure this account." -msgstr "" -"Sie müssen einen Benutzernamen angeben, um diesen Account zu konfigurieren." - -#: ../src/config.py:3139 -msgid "Duplicate Jabber ID" -msgstr "Doppelte Jabber ID" - -#: ../src/config.py:3140 -msgid "This account is already configured in Gajim." -msgstr "Dieser Kontakt wurde in Gajim bereits konfiguriert." - -#: ../src/config.py:3157 -msgid "Account has been added successfully" -msgstr "Account wurde erfolgreich hinzugefügt" - -#: ../src/config.py:3158 ../src/config.py:3345 -msgid "" -"You can set advanced account options by pressing the Advanced button, or " -"later by choosing the Accounts menuitem under the Edit menu from the main " -"window." -msgstr "" -"Sie können die erweiterten Kontooptionen durch Klicken des Erweitert-Buttons " -"oder durch Klicken des Konto-Menüpunktes im Bearbeiten-Menü des " -"Hauptfensters erreichen." - -#: ../src/config.py:3176 -msgid "Invalid server" -msgstr "Ungültiger Server" - -#: ../src/config.py:3177 -msgid "Please provide a server on which you want to register." -msgstr "Bitte geben Sie an, bei dem Sie sich registrieren möchten." - -#: ../src/config.py:3228 ../src/gajim.py:2202 -msgid "Certificate Already in File" -msgstr "Zertifikat schon in der Datei" - -#: ../src/config.py:3229 ../src/gajim.py:2203 -#, python-format -msgid "This certificate is already in file %s, so it's not added again." -msgstr "" - -#: ../src/config.py:3297 -#, python-format -msgid "" -"Security Warning\n" -"\n" -"The authenticity of the %s SSL certificate could be invalid.\n" -"SSL Error: %s\n" -"Do you still want to connect to this server?" -msgstr "" - -#: ../src/config.py:3303 ../src/gajim.py:2226 -#, python-format -msgid "" -"Add this certificate to the list of trusted certificates.\n" -"SHA1 fingerprint of the certificate:\n" -"%s" -msgstr "" - -#: ../src/config.py:3324 ../src/config.py:3363 -msgid "An error occurred during account creation" -msgstr "Während der Konto-Erstellung ist ein Fehler aufgetreten" - -#: ../src/config.py:3344 -msgid "Your new account has been created successfully" -msgstr "Ihr neues Konto wurde erfolgreich erstellt" - -#: ../src/config.py:3447 -msgid "Account name is in use" -msgstr "Kontoname ist schon vergeben" - -#: ../src/config.py:3448 -msgid "You already have an account using this name." -msgstr "Sie haben bereits ein Konto mit diesem Namen." - -#: ../src/conversation_textview.py:468 -msgid "" -"Text below this line is what has been said since the last time you paid " -"attention to this group chat" -msgstr "" -"Text unterhalb dieser Linie stellt dar, was seit ihrem letzten Besuch in " -"diesem Gruppenchat gesagt wurde" - -#: ../src/conversation_textview.py:567 -#, python-format -msgid "_Actions for \"%s\"" -msgstr "_Aktionen für \"%s\"" - -#: ../src/conversation_textview.py:579 -msgid "Read _Wikipedia Article" -msgstr "_Wikipedia-Artikel lesen" - -#: ../src/conversation_textview.py:584 -msgid "Look it up in _Dictionary" -msgstr "Im Wörterbuch _suchen" - -#: ../src/conversation_textview.py:601 -#, python-format -msgid "Dictionary URL is missing an \"%s\" and it is not WIKTIONARY" -msgstr "In Wörterbuch URL fehlt ein \"%s\" und ist nicht Wiktionary" - -#. we must have %s in the url -#: ../src/conversation_textview.py:614 -#, python-format -msgid "Web Search URL is missing an \"%s\"" -msgstr "In Websuche URL fehlt ein \"%s\"" - -#: ../src/conversation_textview.py:617 -msgid "Web _Search for it" -msgstr "Im _Internet suchen" - -#: ../src/conversation_textview.py:623 -msgid "Open as _Link" -msgstr "Als _Link öffnen" - -#: ../src/conversation_textview.py:1111 -msgid "Yesterday" -msgstr "Gestern" - -#. the number is >= 2 -#. %i is day in year (1-365), %d (1-31) we want %i -#: ../src/conversation_textview.py:1115 -#, python-format -msgid "%i days ago" -msgstr "Vor %i Tagen" - -#. if we have subject, show it too! -#: ../src/conversation_textview.py:1149 -#, python-format -msgid "Subject: %s\n" -msgstr "Thema: %s\n" - -#: ../src/dataforms_widget.py:539 -msgid "Jabber ID already in list" -msgstr "Jabber ID bereits in der Liste" - -#: ../src/dataforms_widget.py:540 -msgid "The Jabber ID you entered is already in the list. Choose another one." -msgstr "Die eingegebene Jabber ID ist bereits in der Liste. Wähle eine andere." - -#. Default jid -#: ../src/dataforms_widget.py:551 -msgid "new@jabber.id" -msgstr "neu@jabber.id" - -#: ../src/dataforms_widget.py:554 ../src/dataforms_widget.py:556 -#, python-format -msgid "new%d@jabber.id" -msgstr "neu%d@jabber.id" - -#: ../src/dialogs.py:71 -#, python-format -msgid "Contact name: %s" -msgstr "Kontaktname: %s" - -#: ../src/dialogs.py:73 -#, python-format -msgid "Jabber ID: %s" -msgstr "Jabber ID: %s " - -#: ../src/dialogs.py:183 -msgid "Group" -msgstr "Gruppe" - -#: ../src/dialogs.py:190 -msgid "In the group" -msgstr "In der Gruppe" - -#: ../src/dialogs.py:290 -msgid "KeyID" -msgstr "Schlüssel-ID" - -#: ../src/dialogs.py:295 -msgid "Contact name" -msgstr "Name des Kontakts" - -#: ../src/dialogs.py:441 -#, fuzzy -msgid "Set Mood" -msgstr "Stimmung:" - -#: ../src/dialogs.py:484 -#, python-format -msgid "%s Status Message" -msgstr "%s Status-Nachricht" - -#: ../src/dialogs.py:486 -msgid "Status Message" -msgstr "Status-Nachricht" - -#: ../src/dialogs.py:586 -msgid "Save as Preset Status Message" -msgstr "Als derzeitige Statusnachricht speichern" - -#: ../src/dialogs.py:587 -msgid "Please type a name for this status message" -msgstr "Bitte geben Sie einen Namen für diese Statusnachricht ein:" - -#: ../src/dialogs.py:598 -msgid "Overwrite Status Message?" -msgstr "Status-Nachricht überschreiben" - -#: ../src/dialogs.py:599 -msgid "" -"This name is already used. Do you want to overwrite this status message?" -msgstr "" -"Dieser Name wird bereits verwendet. Möchten Sie die Status-Nachricht " -"überschreiben?" - -#: ../src/dialogs.py:615 -msgid "AIM Address:" -msgstr "AIM-Adresse" - -#: ../src/dialogs.py:616 -msgid "GG Number:" -msgstr "GG Nummer" - -#: ../src/dialogs.py:617 -msgid "ICQ Number:" -msgstr "ICQ-Nummer" - -#: ../src/dialogs.py:618 -msgid "MSN Address:" -msgstr "MSN-Adresse" - -#: ../src/dialogs.py:619 -msgid "Yahoo! Address:" -msgstr "Yahoo!-Adresse" - -#: ../src/dialogs.py:656 -#, python-format -msgid "Please fill in the data of the contact you want to add in account %s" -msgstr "" -"Bitte füllen Sie die Daten für den Kontakt aus, den Sie dem Konto %s " -"hinzufügen wollen" - -#: ../src/dialogs.py:658 -msgid "Please fill in the data of the contact you want to add" -msgstr "Bitte geben sie die Daten des neuen Kontakts ein" - -#: ../src/dialogs.py:815 ../src/dialogs.py:821 -msgid "Invalid User ID" -msgstr "Ungültige Benutzer ID" - -#: ../src/dialogs.py:822 -msgid "The user ID must not contain a resource." -msgstr "Die Benutzer-ID darf keine resource enthalten." - -#: ../src/dialogs.py:836 -msgid "Contact already in roster" -msgstr "Kontakt bereits im Roster" - -#: ../src/dialogs.py:837 -msgid "This contact is already listed in your roster." -msgstr "Der Kontakt befindet sich bereit in Ihrem Roster." - -#: ../src/dialogs.py:873 -msgid "User ID:" -msgstr "_Benutzer-ID:" - -#: ../src/dialogs.py:931 -msgid "A GTK+ jabber client" -msgstr "Ein GTK+ Jabber-Client" - -#: ../src/dialogs.py:932 -msgid "GTK+ Version:" -msgstr "GTK+-Version:" - -#: ../src/dialogs.py:933 -msgid "PyGTK Version:" -msgstr "PyGTK-Version" - -#: ../src/dialogs.py:943 -msgid "Current Developers:" -msgstr "Derzeitige Entwickler:" - -#: ../src/dialogs.py:945 -msgid "Past Developers:" -msgstr "Frühere Entwickler:" - -#: ../src/dialogs.py:951 -msgid "THANKS:" -msgstr "DANKE:" - -#. remove one english sentence -#. and add it manually as translatable -#: ../src/dialogs.py:957 -msgid "Last but not least, we would like to thank all the package maintainers." -msgstr "Zuletzt möchten wird gerne allen Paket-Verwaltern danken." - -#. here you write your name in the form Name FamilyName -#: ../src/dialogs.py:970 -msgid "translator-credits" -msgstr "" -"Fridtjof Busse\n" -"Benjamin Drung \n" -"Fabian Fingerle \n" -"Sebastian Schäfer \n" -"Nico Gulden" - -#: ../src/dialogs.py:1141 -#, python-format -msgid "Unable to bind to port %s." -msgstr "Konnte nicht an Port %s binden." - -#: ../src/dialogs.py:1142 -msgid "" -"Maybe you have another running instance of Gajim. File Transfer will be " -"cancelled." -msgstr "Möglicherweise läuft Gajim bereits. Dateitransfer wird abgebrochen." - -#: ../src/dialogs.py:1149 -#, python-format -msgid "" -"You have to install %s dictionary to use spellchecking, or choose another " -"language by setting the speller_language option.\n" -"\n" -"Highlighting misspelled words feature will not be used" -msgstr "" -"Sie müssen das Wörterbuch %s installieren oder eine andere Sprache wählen, " -"um die Rechtschreibprüfung zu nutzen.\n" -"\n" -"Das Hervorheben falsch geschriebener Worte wird nicht genutzt" - -#: ../src/dialogs.py:1542 -#, python-format -msgid "Subscription request for account %s from %s" -msgstr "Abonnement-Anforderung für Konto %s von %s" - -#: ../src/dialogs.py:1545 -#, python-format -msgid "Subscription request from %s" -msgstr "Abonnement-Anforderung von %s" - -#: ../src/dialogs.py:1606 ../src/gajim.py:2791 ../src/roster_window.py:1147 -#, python-format -msgid "You are already in group chat %s" -msgstr "Sie sind bereits im Gruppenchat %s" - -#: ../src/dialogs.py:1614 -msgid "You can not join a group chat unless you are connected." -msgstr "Sie können einem Gruppenchat erst beitreten, wenn Sie verbunden sind." - -#: ../src/dialogs.py:1633 -#, python-format -msgid "Join Group Chat with account %s" -msgstr "Betrete Gruppenchat mit Account %s" - -#: ../src/dialogs.py:1704 -#, fuzzy -msgid "Invalid Nickname" -msgstr "Ungültiger Benutzername" - -#: ../src/dialogs.py:1705 ../src/groupchat_control.py:1297 -#: ../src/groupchat_control.py:1569 -msgid "The nickname has not allowed characters." -msgstr "Die Jabber-ID für den Gruppenchat enthält nicht erlaubte Zeichen." - -#: ../src/dialogs.py:1709 ../src/dialogs.py:1715 -#: ../src/groupchat_control.py:1736 -msgid "Invalid group chat Jabber ID" -msgstr "Ungültige Jabber-ID für den Gruppenchat" - -#: ../src/dialogs.py:1710 ../src/dialogs.py:1716 -#: ../src/groupchat_control.py:1737 -msgid "The group chat Jabber ID has not allowed characters." -msgstr "Die Jabber-ID für den Gruppenchat enthält nicht erlaubte Zeichen." - -#: ../src/dialogs.py:1722 -msgid "This is not a group chat" -msgstr "Das ist kein Gruppenchat" - -#: ../src/dialogs.py:1723 -#, python-format -msgid "%s is not the name of a group chat." -msgstr "%s ist kein Name eines Gruppenchats." - -#: ../src/dialogs.py:1750 -msgid "Without a connection, you can not synchronise your contacts." -msgstr "Sie müssen verbunden sein, um Ihre Kontakte zu Synchronisieren." - -#: ../src/dialogs.py:1764 -msgid "Server" -msgstr "Server" - -#: ../src/dialogs.py:1797 -msgid "This account is not connected to the server" -msgstr "Konto \"%s\" ist nicht mit dem Server verbunden" - -#: ../src/dialogs.py:1798 -msgid "You cannot synchronize with an account unless it is connected." -msgstr "" -"Sie können nicht mit einem Konto Synchronisieren, solange es nicht verbunden " -"ist." - -#: ../src/dialogs.py:1822 -msgid "Synchronise" -msgstr "Synchronisieren" - -#: ../src/dialogs.py:1880 -#, python-format -msgid "Start Chat with account %s" -msgstr "Starte Chat mit Account %s" - -#: ../src/dialogs.py:1882 -msgid "Start Chat" -msgstr "Chat starten" - -#: ../src/dialogs.py:1883 -msgid "" -"Fill in the nickname or the Jabber ID of the contact you would like\n" -"to send a chat message to:" -msgstr "" -"Geben Sie die JID oder den Spitznamen des Kontaktes ein,\n" -"an den Sie eine Chat-Nachricht schicken wollen:" - -#. if offline or connecting -#: ../src/dialogs.py:1908 ../src/dialogs.py:2282 ../src/dialogs.py:2423 -msgid "Connection not available" -msgstr "Verbindung nicht verfügbar" - -#: ../src/dialogs.py:1909 ../src/dialogs.py:2283 ../src/dialogs.py:2424 -#, python-format -msgid "Please make sure you are connected with \"%s\"." -msgstr "Vergewissern Sie sich, dass Sie mit \"%s\" verbunden sind." - -#: ../src/dialogs.py:1918 ../src/dialogs.py:1921 -msgid "Invalid JID" -msgstr "Ungültige JID" - -#: ../src/dialogs.py:1921 -#, python-format -msgid "Unable to parse \"%s\"." -msgstr "Kann \"%s\" nicht parsen." - -#: ../src/dialogs.py:1930 -msgid "Without a connection, you can not change your password." -msgstr "Sie müssen verbunden sein, um Ihr Passwort zu ändern" - -#: ../src/dialogs.py:1948 -msgid "Invalid password" -msgstr "Ungültiges Passwort" - -#: ../src/dialogs.py:1949 -msgid "You must enter a password." -msgstr "Sie müssen ein Passwort eingeben." - -#: ../src/dialogs.py:1953 -msgid "Passwords do not match" -msgstr "Passwörter stimmen nicht überein" - -#: ../src/dialogs.py:1954 -msgid "The passwords typed in both fields must be identical." -msgstr "Die Passwörter in beiden Feldern müssen identisch sein." - -#. img to display -#. default value -#: ../src/dialogs.py:1996 ../src/notify.py:242 ../src/notify.py:456 -msgid "Contact Signed In" -msgstr "Kontakt hat sich angemeldet" - -#: ../src/dialogs.py:1998 ../src/notify.py:250 ../src/notify.py:458 -msgid "Contact Signed Out" -msgstr "Kontakt hat sich abgemeldet" - -#. chat message -#: ../src/dialogs.py:2000 ../src/notify.py:273 ../src/notify.py:460 -msgid "New Message" -msgstr "Neue Nachricht" - -#. single message -#: ../src/dialogs.py:2000 ../src/notify.py:254 ../src/notify.py:460 -msgid "New Single Message" -msgstr "Neue einzelne Nachricht" - -#. private message -#: ../src/dialogs.py:2001 ../src/notify.py:261 ../src/notify.py:461 -msgid "New Private Message" -msgstr "Neue private Nachricht" - -#: ../src/dialogs.py:2001 ../src/gajim.py:1655 ../src/notify.py:469 -msgid "New E-mail" -msgstr "Neue E-Mail" - -#: ../src/dialogs.py:2003 ../src/gajim.py:1720 ../src/notify.py:463 -msgid "File Transfer Request" -msgstr "Dateitransfer Anfrage" - -#: ../src/dialogs.py:2005 ../src/gajim.py:1627 ../src/gajim.py:1687 -#: ../src/notify.py:465 -msgid "File Transfer Error" -msgstr "Dateitransfer-Fehler" - -#: ../src/dialogs.py:2007 ../src/gajim.py:1759 ../src/gajim.py:1781 -#: ../src/gajim.py:1798 ../src/notify.py:467 -msgid "File Transfer Completed" -msgstr "Dateitransfer beendet" - -#: ../src/dialogs.py:2008 ../src/gajim.py:1762 ../src/notify.py:467 -msgid "File Transfer Stopped" -msgstr "Dateitransfer gestoppt" - -#: ../src/dialogs.py:2010 ../src/gajim.py:1483 ../src/notify.py:471 -msgid "Groupchat Invitation" -msgstr "Gruppenchat-Einladung" - -#: ../src/dialogs.py:2012 ../src/notify.py:234 ../src/notify.py:473 -msgid "Contact Changed Status" -msgstr "Kontakt hat Status verändert" - -#: ../src/dialogs.py:2201 -#, python-format -msgid "Single Message using account %s" -msgstr "Einzelne Nachricht mit Account %s" - -#: ../src/dialogs.py:2203 -#, python-format -msgid "Single Message in account %s" -msgstr "Einzelne Nachricht in Account %s" - -#: ../src/dialogs.py:2205 -msgid "Single Message" -msgstr "Einzelne Nachricht" - -#. prepare UI for Sending -#: ../src/dialogs.py:2208 -#, python-format -msgid "Send %s" -msgstr "Sende %s" - -#. prepare UI for Receiving -#: ../src/dialogs.py:2231 -#, python-format -msgid "Received %s" -msgstr "%s empfangen" - -#. prepare UI for Receiving -#: ../src/dialogs.py:2254 -#, python-format -msgid "Form %s" -msgstr "Von %s" - -#. we create a new blank window to send and we preset RE: and to jid -#: ../src/dialogs.py:2324 -#, python-format -msgid "RE: %s" -msgstr "RE: %s" - -#: ../src/dialogs.py:2325 -#, python-format -msgid "%s wrote:\n" -msgstr "%s schrieb:\n" - -#: ../src/dialogs.py:2369 -#, python-format -msgid "XML Console for %s" -msgstr "XML Konsole für %s" - -#: ../src/dialogs.py:2371 -msgid "XML Console" -msgstr "XML Konsole" - -#: ../src/dialogs.py:2494 -#, python-format -msgid "Privacy List %s" -msgstr "Privatliste %s" - -#: ../src/dialogs.py:2498 -#, python-format -msgid "Privacy List for %s" -msgstr "Privatsphären-Liste für %s" - -#: ../src/dialogs.py:2554 -#, python-format -msgid "Order: %s, action: %s, type: %s, value: %s" -msgstr "Sortierung: %s, Aktion: %s, Typ: %s, Wert: %s" - -#: ../src/dialogs.py:2557 -#, python-format -msgid "Order: %s, action: %s" -msgstr "Sortierung: %s, Aktion: %s" - -#: ../src/dialogs.py:2599 -msgid "Edit a rule" -msgstr "Eine Regel bearbeiten" - -#: ../src/dialogs.py:2686 -msgid "Add a rule" -msgstr "Eine Regel hinzufügen" - -#: ../src/dialogs.py:2782 -#, python-format -msgid "Privacy Lists for %s" -msgstr "Privatliste für %s" - -#: ../src/dialogs.py:2784 -msgid "Privacy Lists" -msgstr "Privatlisten" - -#: ../src/dialogs.py:2854 -msgid "Invalid List Name" -msgstr "Ungültiger Listenname" - -#: ../src/dialogs.py:2855 -msgid "You must enter a name to create a privacy list." -msgstr "" -"Sie müssen einen Namen eingeben um eine Privatsphären-Liste zu erstellen." - -#: ../src/dialogs.py:2892 -msgid "$Contact has invited you to join a discussion" -msgstr "$Contact hat Sie in den Gruppenchat %(room_jid)s eingeladen" - -#: ../src/dialogs.py:2894 -#, python-format -msgid "$Contact has invited you to group chat %(room_jid)s" -msgstr "$Contact hat Sie in den Gruppenchat %(room_jid)s eingeladen" - -#: ../src/dialogs.py:2907 -#, python-format -msgid "Comment: %s" -msgstr "Kommentar: %s" - -#: ../src/dialogs.py:2973 -msgid "Choose Sound" -msgstr "Sound wählen" - -#: ../src/dialogs.py:2983 ../src/dialogs.py:3034 -msgid "All files" -msgstr "Alle Dateien" - -#: ../src/dialogs.py:2988 -msgid "Wav Sounds" -msgstr "Wav Dateien" - -#: ../src/dialogs.py:3021 -msgid "Choose Image" -msgstr "Bild auswählen" - -#: ../src/dialogs.py:3039 -msgid "Images" -msgstr "Bilder" - -#: ../src/dialogs.py:3104 -#, python-format -msgid "When %s becomes:" -msgstr "Wenn %s wird:" - -#: ../src/dialogs.py:3106 -#, python-format -msgid "Adding Special Notification for %s" -msgstr "Füge speziellen Hinweis für %s hinzu" - -#. # means number -#: ../src/dialogs.py:3177 -msgid "#" -msgstr "Nr." - -#: ../src/dialogs.py:3183 -msgid "Condition" -msgstr "Bedingung" - -#: ../src/dialogs.py:3301 -msgid "when I am " -msgstr "wenn Ich bin " - -#: ../src/disco.py:110 -msgid "Others" -msgstr "Andere" - -#. When we redraw the group in remove_contact the -#. contact does still exist and so the group is still showing -#. the old numbers. -#: ../src/disco.py:111 ../src/disco.py:112 ../src/disco.py:1320 -#: ../src/gajim.py:893 ../src/roster_window.py:730 ../src/roster_window.py:745 -#: ../src/roster_window.py:1336 ../src/roster_window.py:1382 -#: ../src/roster_window.py:1384 ../src/roster_window.py:1524 -#: ../src/common/contacts.py:303 ../src/common/contacts.py:318 -#: ../src/common/helpers.py:66 -msgid "Transports" -msgstr "Transports" - -#. conference is a category for listing mostly groupchats in service discovery -#: ../src/disco.py:114 -msgid "Conference" -msgstr "Konferenz" - -#: ../src/disco.py:427 -msgid "Without a connection, you can not browse available services" -msgstr "Sie müssen angemeldet sein um Dienste zu durchsuchen" - -#: ../src/disco.py:501 -#, python-format -msgid "Service Discovery using account %s" -msgstr "Dienste durchsuchen für Konto %s" - -#: ../src/disco.py:503 -msgid "Service Discovery" -msgstr "Dienste durchsuchen" - -#: ../src/disco.py:643 -msgid "The service could not be found" -msgstr "Der Dienst konnte nicht gefunden werden" - -#: ../src/disco.py:644 -msgid "" -"There is no service at the address you entered, or it is not responding. " -"Check the address and try again." -msgstr "" -"Es existiert kein Dienst an der Adresse, die sie angegeben haben oder er " -"antwortet nicht. Überprüfen Sie die Adresse und versuchen Sie es erneut." - -#: ../src/disco.py:648 ../src/disco.py:929 -msgid "The service is not browsable" -msgstr "Der Dienst ist nicht durchsuchbar" - -#: ../src/disco.py:649 -msgid "This type of service does not contain any items to browse." -msgstr "" -"Dieser Art von Dienst enthält keine Objekte, di edurchsucht werden können." - -#: ../src/disco.py:729 -#, python-format -msgid "Browsing %s using account %s" -msgstr "Durchsuche %s mit Konto %s" - -#: ../src/disco.py:768 -msgid "_Browse" -msgstr "_Durchsuche" - -#: ../src/disco.py:930 -msgid "This service does not contain any items to browse." -msgstr "Dieser Dienst enthält keine keine durchsuchbaren Objekte." - -#: ../src/disco.py:1151 -msgid "_Execute Command" -msgstr "_Befehl ausführen" - -#: ../src/disco.py:1161 ../src/disco.py:1325 -msgid "Re_gister" -msgstr "Re_gistrieren" - -#: ../src/disco.py:1362 -#, python-format -msgid "Scanning %d / %d.." -msgstr "Durchsuche %d / %d.." - -#. Users column -#: ../src/disco.py:1544 -msgid "Users" -msgstr "Benutzer" - -#. Description column -#: ../src/disco.py:1552 -msgid "Description" -msgstr "Beschreibung" - -#. Id column -#: ../src/disco.py:1560 -msgid "Id" -msgstr "Id" - -#: ../src/disco.py:1789 -msgid "Subscribed" -msgstr "Abonniert" - -#: ../src/disco.py:1797 -msgid "Node" -msgstr "Node" - -#: ../src/disco.py:1854 -msgid "New post" -msgstr "Neue Nachricht" - -#: ../src/disco.py:1860 -msgid "_Subscribe" -msgstr "_Abonnieren" - -#: ../src/disco.py:1866 -msgid "_Unsubscribe" -msgstr "_Abbestellen" - -#: ../src/features_window.py:46 -msgid "PyOpenSSL" -msgstr "PyOpenSSL" - -#: ../src/features_window.py:47 -msgid "" -"A library used to validate server certificates to ensure a secure connection." -msgstr "" -"Eine Bibliothek um Server-Zertifikate zu validieren um eine sichere " -"Verbindung zu gewährleisten." - -#: ../src/features_window.py:48 ../src/features_window.py:49 -msgid "Requires python-pyopenssl." -msgstr "Erfordert python-pyopenssl." - -#: ../src/features_window.py:50 -msgid "Bonjour / Zeroconf" -msgstr "Bonjour / Zeroconf" - -#: ../src/features_window.py:51 -msgid "Serverless chatting with autodetected clients in a local network." -msgstr "" -"Serverloses Chatten mit automatisch erkannten Clients in einem lokalen Netz" - -#: ../src/features_window.py:52 -msgid "Requires python-avahi." -msgstr "Erfordert python-avahi." - -#: ../src/features_window.py:53 -msgid "Requires pybonjour (http://o2s.csail.mit.edu/o2s-wiki/pybonjour)." -msgstr "Erfordert pybonjour (http://o2s.csail.mit.edu/o2s-wiki/pybonjour)." - -#: ../src/features_window.py:54 -msgid "gajim-remote" -msgstr "gajim-remote" - -#: ../src/features_window.py:55 -msgid "A script to controle Gajim via commandline." -msgstr "Ein Skript, um Gajim über die Befehlszeile zu steuern." - -#: ../src/features_window.py:56 -msgid "Requires python-dbus." -msgstr "Erfordert python-dbus." - -#: ../src/features_window.py:57 ../src/features_window.py:61 -#: ../src/features_window.py:65 ../src/features_window.py:69 -#: ../src/features_window.py:73 ../src/features_window.py:81 -#: ../src/features_window.py:85 ../src/features_window.py:97 -msgid "Feature not available under Windows." -msgstr "Funktion unter Windows nicht verfügbar." - -#: ../src/features_window.py:58 -msgid "OpenGPG" -msgstr "OpenPGP: " - -#: ../src/features_window.py:59 -msgid "Encrypting chatmessages with gpg keys." -msgstr "Chat Nachrichten werden mit gpg Schlüssel verschlüsselt" - -#: ../src/features_window.py:60 -msgid "Requires gpg and python-GnuPGInterface." -msgstr "Erfordert gpg und python-GnuPGInterface." - -#: ../src/features_window.py:62 -msgid "network-manager" -msgstr "network-manager" - -#: ../src/features_window.py:63 -msgid "Autodetection of network status." -msgstr "Auto-Erkennung des Netzwerk-Status." - -#: ../src/features_window.py:64 -msgid "Requires gnome-network-manager and python-dbus." -msgstr "Erfordert gnome-network-manager und python-dbus." - -#: ../src/features_window.py:66 -msgid "Session Management" -msgstr "Sitzungsverwaltung" - -#: ../src/features_window.py:67 -msgid "Gajim session is stored on logout and restored on login." -msgstr "" -"Gajim-Sitzung wird beim Logout gespeichert und beim Login wiederhergestellt" - -#: ../src/features_window.py:68 -msgid "Requires python-gnome2." -msgstr "Erfordert python-gnome2." - -#: ../src/features_window.py:70 -msgid "gnome-keyring" -msgstr "gnome-keyring" - -#: ../src/features_window.py:71 -msgid "Passwords can be stored securely and not just in plaintext." -msgstr "Passwörter können sicher und nicht nur im Klartext gespeichert werden" - -#: ../src/features_window.py:72 -msgid "Requires gnome-keyring and python-gnome2-desktop." -msgstr "Erfordert gnome-keyring und python-gnome2-desktop." - -#: ../src/features_window.py:74 -msgid "SRV" -msgstr "SRV" - -#: ../src/features_window.py:75 -msgid "Ability to connect to servers which are using SRV records." -msgstr "Fähigkeit zu Servern die SRV-Einträge verwenden zu verbinden." - -#: ../src/features_window.py:76 -msgid "Requires dnsutils." -msgstr "Erfordert dnsutils." - -#: ../src/features_window.py:77 -msgid "Requires nslookup to use SRV records." -msgstr "Erfordert nslookup zur Nutzung von SRV-Einträgen." - -#: ../src/features_window.py:78 -msgid "Spell Checker" -msgstr "Rechtschreibprüfung" - -#: ../src/features_window.py:79 -msgid "Spellchecking of composed messages." -msgstr "Rechtschreibprüfung erstellter Nachrichten." - -#: ../src/features_window.py:80 -msgid "" -"Requires python-gnome2-extras or compilation of gtkspell module from Gajim " -"sources." -msgstr "" -"Erfordert python-gnome2-extras oder die Kompilation des gtkspell-Moduls aus " -"den Gajim-Quellen." - -#: ../src/features_window.py:82 -msgid "Notification-daemon" -msgstr "Notification-daemon" - -#: ../src/features_window.py:83 -msgid "Passive popups notifying for new events." -msgstr "Popups informieren über neue Ereignisse." - -#: ../src/features_window.py:84 -msgid "" -"Requires python-notify or instead python-dbus in conjunction with " -"notification-daemon." -msgstr "" -"Erforder python-notify oder stattdessen python-dbus in Verbindung mit " -"notification-daemon." - -#: ../src/features_window.py:86 -msgid "Trayicon" -msgstr "Tray-Symbol" - -#: ../src/features_window.py:87 -msgid "A icon in systemtray reflecting the current presence." -msgstr "Ein Icon im Systemtray, das den Status widerspiegelt." - -#: ../src/features_window.py:88 -msgid "" -"Requires python-gnome2-extras or compiled trayicon module from Gajim sources." -msgstr "" -"Erfordert python-gnome2-extras oder das kompilierte Trayicon-Modul aus den " -"Gajim-Quellen" - -#: ../src/features_window.py:89 -msgid "Requires PyGTK >= 2.10." -msgstr "Erforder PyGTK >= 2.10." - -#: ../src/features_window.py:90 -msgid "Idle" -msgstr "Idle" - -#: ../src/features_window.py:91 -msgid "Ability to measure idle time, in order to set auto status." -msgstr "Fähigkeit die Idle-Zeit zu messen um den Status automatisch zu setzen" - -#: ../src/features_window.py:92 ../src/features_window.py:93 -msgid "Requires compilation of the idle module from Gajim sources." -msgstr "Erfordert die Kompilation des Idle-Moduls aus den Gajim-Quellen." - -#: ../src/features_window.py:94 -msgid "LaTeX" -msgstr "LaTeX" - -#: ../src/features_window.py:95 -msgid "Transform LaTeX expressions between $$ $$." -msgstr "LaTeX-Ausdrücke zwischen $$ $$ umwandeln." - -#: ../src/features_window.py:96 -msgid "" -"Requires texlive-latex-base, dvips and imagemagick. You have to set " -"'use_latex' to True in the Advanced Configuration Editor." -msgstr "" -"Erfordert texlive-latex-base, dvips und imagemagick. Sie müssen 'use_latex' " -"im Advanced Configuration Editor auf 'true' setzen." - -#: ../src/features_window.py:98 -#, fuzzy -msgid "End to End Encryption" -msgstr "OpenPGP-Verschlüsselung" - -#: ../src/features_window.py:99 -msgid "Encrypting chatmessages." -msgstr "Chatnachrichten werden verschlüsselt." - -#: ../src/features_window.py:100 ../src/features_window.py:101 -msgid "Requires python-crypto." -msgstr "Erfordert python-crypto" - -#: ../src/features_window.py:102 -#, fuzzy -msgid "Off the Record Encryption" -msgstr "OpenPGP-Verschlüsselung" - -#: ../src/features_window.py:103 -#, fuzzy -msgid "Encrypting chatmessages in a way that even works through gateways." -msgstr "Chat Nachrichten werden mit gpg Schlüssel verschlüsselt" - -#: ../src/features_window.py:104 ../src/features_window.py:105 -#, fuzzy -msgid "Requires pyotr and libotr." -msgstr "Erfordert python-dbus." - -#: ../src/features_window.py:106 -msgid "RST Generator" -msgstr "RST Generator" - -#: ../src/features_window.py:107 -msgid "" -"Generate XHTML output from RST code (see http://docutils.sourceforge.net/" -"docs/ref/rst/restructuredtext.html)." -msgstr "" -"Erzeug aus RST-Code XHTML-Ausgaben (siehe http://docutils.sourceforge.net/" -"docs/ref/rst/restructuredtext.html)." - -#: ../src/features_window.py:108 ../src/features_window.py:109 -msgid "Requires python-docutils." -msgstr "Erfordert python-docutils." - -#: ../src/features_window.py:110 -msgid "libsexy" -msgstr "" - -#: ../src/features_window.py:111 -msgid "Ability to have clickable URLs in chat and groupchat window banners." -msgstr "Anklickbare URLs im Chat-Fenster." - -#: ../src/features_window.py:112 ../src/features_window.py:113 -msgid "Requires python-sexy." -msgstr "Erfordert python-sexy." - -#: ../src/features_window.py:120 ../src/common/helpers.py:261 -msgid "Available" -msgstr "Angemeldet" - -#: ../src/features_window.py:127 -msgid "Feature" -msgstr "Fähigkeit" - -#: ../src/filetransfers_window.py:77 -msgid "File" -msgstr "Datei" - -#: ../src/filetransfers_window.py:92 -msgid "Time" -msgstr "Zeit" - -#: ../src/filetransfers_window.py:104 -msgid "Progress" -msgstr "Fortschritt" - -#: ../src/filetransfers_window.py:164 ../src/filetransfers_window.py:218 -#, python-format -msgid "Filename: %s" -msgstr "Dateiname: %s" - -#: ../src/filetransfers_window.py:165 ../src/filetransfers_window.py:305 -#, python-format -msgid "Size: %s" -msgstr "Größe: %s" - -#. You is a reply of who sent a file -#. You is a reply of who received a file -#: ../src/filetransfers_window.py:174 ../src/filetransfers_window.py:184 -#: ../src/history_manager.py:485 -msgid "You" -msgstr "Sie" - -#: ../src/filetransfers_window.py:175 -#, python-format -msgid "Sender: %s" -msgstr "Gespeichert in: %s" - -#: ../src/filetransfers_window.py:176 ../src/filetransfers_window.py:595 -#: ../src/tooltips.py:649 -msgid "Recipient: " -msgstr "Empfänger: " - -#: ../src/filetransfers_window.py:187 -#, python-format -msgid "Saved in: %s" -msgstr "Absender: %s" - -#: ../src/filetransfers_window.py:189 -msgid "File transfer completed" -msgstr "Dateitransfer abgeschlossen" - -#: ../src/filetransfers_window.py:203 ../src/filetransfers_window.py:209 -msgid "File transfer cancelled" -msgstr "Dateitransfer abgebrochen" - -#: ../src/filetransfers_window.py:203 ../src/filetransfers_window.py:210 -msgid "Connection with peer cannot be established." -msgstr "Verbindung zum Teilnehmer kann nicht hergestellt werden" - -#: ../src/filetransfers_window.py:219 -#, python-format -msgid "Recipient: %s" -msgstr "Empfänger: %s" - -#: ../src/filetransfers_window.py:221 -#, python-format -msgid "Error message: %s" -msgstr "Fehlermeldung: %s" - -#: ../src/filetransfers_window.py:222 -msgid "File transfer stopped by the contact at the other end" -msgstr "Gegenseite hat Dateitransfer gestoppt" - -#: ../src/filetransfers_window.py:243 -msgid "Choose File to Send..." -msgstr "Datei auswählen ..." - -#: ../src/filetransfers_window.py:259 ../src/tooltips.py:689 -msgid "Description: " -msgstr "Beschreibung: " - -#: ../src/filetransfers_window.py:270 -msgid "Gajim cannot access this file" -msgstr "Gajim kann auf diese Datei nicht zugreifen" - -#: ../src/filetransfers_window.py:271 -msgid "This file is being used by another process." -msgstr "Diese Datei wird von einem anderen Prozess verwendet." - -#: ../src/filetransfers_window.py:303 -#, python-format -msgid "File: %s" -msgstr "Datei: %s" - -#: ../src/filetransfers_window.py:308 -#, python-format -msgid "Type: %s" -msgstr "Typ: %s" - -#: ../src/filetransfers_window.py:310 -#, python-format -msgid "Description: %s" -msgstr "Beschreibung: %s" - -#: ../src/filetransfers_window.py:311 -#, python-format -msgid "%s wants to send you a file:" -msgstr "%s möchte ihnen eine Datei senden:" - -#: ../src/filetransfers_window.py:324 ../src/gtkgui_helpers.py:778 -#, python-format -msgid "Cannot overwrite existing file \"%s\"" -msgstr "Kann existierende Datei \"%s\" nicht überschreiben" - -#: ../src/filetransfers_window.py:325 ../src/gtkgui_helpers.py:780 -msgid "" -"A file with this name already exists and you do not have permission to " -"overwrite it." -msgstr "" -"Eine Datei mit diesem Namen existiert bereits und Sie haben nicht die Rechte " -"sie zu überschreiben." - -#: ../src/filetransfers_window.py:332 ../src/gtkgui_helpers.py:784 -msgid "This file already exists" -msgstr "Diese Datei existiert bereits" - -#: ../src/filetransfers_window.py:332 ../src/gtkgui_helpers.py:784 -msgid "What do you want to do?" -msgstr "Was möchten Sie tun?" - -#. read-only bit is used to mark special folder under windows, -#. not to mark that a folder is read-only. See ticket #3587 -#: ../src/filetransfers_window.py:346 ../src/gtkgui_helpers.py:794 -#, python-format -msgid "Directory \"%s\" is not writable" -msgstr "Verzeichnis \"%s\" ist nicht schreibbar" - -#: ../src/filetransfers_window.py:346 ../src/gtkgui_helpers.py:795 -msgid "You do not have permission to create files in this directory." -msgstr "" -"Ihre Benutzerrechte erlauben es Ihnen nicht, in diesem Verzeichnis Dateien " -"anzulegen." - -#: ../src/filetransfers_window.py:356 -msgid "Save File as..." -msgstr "Datei speichern unter ..." - -#. Print remaining time in format 00:00:00 -#. You can change the places of (hours), (minutes), (seconds) - -#. they are not translatable. -#: ../src/filetransfers_window.py:436 -#, python-format -msgid "%(hours)02.d:%(minutes)02.d:%(seconds)02.d" -msgstr "%(hours)02.d:%(minutes)02.d:%(seconds)02.d" - -#. This should make the string Kb/s, -#. where 'Kb' part is taken from %s. -#. Only the 's' after / (which means second) should be translated. -#: ../src/filetransfers_window.py:525 -#, python-format -msgid "(%(filesize_unit)s/s)" -msgstr "(%(filesize_unit)s/s)" - -#: ../src/filetransfers_window.py:565 ../src/filetransfers_window.py:568 -msgid "Invalid File" -msgstr "Ungültige Datei" - -#: ../src/filetransfers_window.py:565 -msgid "File: " -msgstr "Datei: " - -#: ../src/filetransfers_window.py:569 -msgid "It is not possible to send empty files" -msgstr "Es nicht möglich, leere Dateien zu versenden" - -#: ../src/filetransfers_window.py:591 ../src/tooltips.py:639 -msgid "Name: " -msgstr "Name: " - -#: ../src/filetransfers_window.py:593 ../src/tooltips.py:643 -msgid "Sender: " -msgstr "Absender: " - -#: ../src/filetransfers_window.py:781 -msgid "Pause" -msgstr "Pause" - -#: ../src/gajim.py:71 -#, python-format -msgid "%s is not a valid loglevel" -msgstr "%s ist kein gültiger Loglevel." - -#: ../src/gajim.py:144 -msgid "Gajim needs X server to run. Quiting..." -msgstr "Gajim benötigt einen laufenden X-Server. Breche ab ..." - -#: ../src/gajim.py:174 -msgid "Gajim needs PyGTK 2.8 or above" -msgstr "Gajim benötigt PyGTK 2.6 oder höher" - -#: ../src/gajim.py:175 -msgid "Gajim needs PyGTK 2.8 or above to run. Quiting..." -msgstr "Gajim benötigt PyGTK 2.6 oder höher. Breche ab ..." - -#: ../src/gajim.py:177 -msgid "Gajim needs GTK 2.8 or above" -msgstr "Gajim benötigt GTK 2.6 oder höher" - -#: ../src/gajim.py:178 -msgid "Gajim needs GTK 2.8 or above to run. Quiting..." -msgstr "Gajim benötigt GTK 2.6+. Breche ab ..." - -#: ../src/gajim.py:183 -msgid "GTK+ runtime is missing libglade support" -msgstr "GTK+ runtine fehlt libglade-Unterstützung" - -#: ../src/gajim.py:185 -#, python-format -msgid "" -"Please remove your current GTK+ runtime and install the latest stable " -"version from %s" -msgstr "" -"Bitte entfernen Sie Ihre derzeitige GTK+ Laufzeitumgebung und installieren " -"Sie die aktuelle Version von %s" - -#: ../src/gajim.py:187 -msgid "" -"Please make sure that GTK+ and PyGTK have libglade support in your system." -msgstr "" -"Bitte stellen Sie sicher, dass GTK+ und PyGTK auf Ihrem System libglade-" -"Unterstützung besitzen." - -#: ../src/gajim.py:192 -msgid "Gajim needs PySQLite2 to run" -msgstr "Gajim benötigt PySQLite2 zum Starten" - -#: ../src/gajim.py:200 -msgid "Gajim needs pywin32 to run" -msgstr "Gajim benötigt pywin32 zum Laufen" - -#: ../src/gajim.py:201 -#, python-format -msgid "" -"Please make sure that Pywin32 is installed on your system. You can get it at " -"%s" -msgstr "" -"Bitte stellen Sie sicher das Pywin32 auf Ihrem System installiert ist. Sie " -"können es unter folgender URL herunterladen: %s " - -#: ../src/gajim.py:326 -#, fuzzy -msgid "Generating..." -msgstr "Essen" - -#: ../src/gajim.py:373 -msgid "" -"\n" -"This user has requested an Off-the-Record private conversation. However, " -"you do not have a plugin to support that.\n" -"See http://otr.cypherpunks.ca/ for more information." -msgstr "" - -#. set the icon to all newly opened wind -#: ../src/gajim.py:548 -msgid "Gajim is already running" -msgstr "Gajim läuft bereits" - -#: ../src/gajim.py:549 -msgid "" -"Another instance of Gajim seems to be running\n" -"Run anyway?" -msgstr "" -"Eine andere Instanz von Gajim schein bereits zu laufen\n" -"Trotzdem starten?" - -#: ../src/gajim.py:572 ../src/common/connection_handlers.py:904 -#: ../src/common/connection_handlers.py:1667 -#: ../src/common/connection_handlers.py:1715 -#: ../src/common/connection_handlers.py:1979 -#: ../src/common/connection_handlers.py:2091 ../src/common/connection.py:1155 -msgid "Disk Write Error" -msgstr "Fehler beim Schreiben auf Festplatte" - -#: ../src/gajim.py:676 -msgid "Do you accept this request?" -msgstr "Akzeptieren Sie diese Anfrage?" - -#: ../src/gajim.py:678 -#, python-format -msgid "Do you accept this request on account %s?" -msgstr "Akzeptieren Sie diese Anfrage vom %s Konto?" - -#: ../src/gajim.py:681 -#, python-format -msgid "HTTP (%s) Authorization for %s (id: %s)" -msgstr "HTTP (%s) Autorisierung für %s (id: %s)" - -#: ../src/gajim.py:729 ../src/notify.py:475 -msgid "Connection Failed" -msgstr "Verbindung fehlgeschlagen" - -#. ('MSGNOTSENT', account, (jid, ierror_msg, msg, time)) -#: ../src/gajim.py:987 ../src/gajim.py:999 -#, python-format -msgid "error while sending %s ( %s )" -msgstr "Fehler beim Senden von %s ( %s )" - -#: ../src/gajim.py:1032 -msgid "Authorization accepted" -msgstr "Autorisierung akzeptiert" - -#: ../src/gajim.py:1033 -#, python-format -msgid "The contact \"%s\" has authorized you to see his or her status." -msgstr "Kontakt \"%s\" hat Sie autorisiert seinen oder ihren Staus zu sehen." - -#: ../src/gajim.py:1052 -#, python-format -msgid "Contact \"%s\" removed subscription from you" -msgstr "Kontakt \"%s\" hat das Abonnement zurückgezogen" - -#: ../src/gajim.py:1053 -msgid "" -"You will always see him or her as offline.\n" -"Do you want to remove him or her from your contact list?" -msgstr "" -"Du wirst sie oder ihn immer als offline sehen.\n" -"Möchtest du sie oder ihn von deiner Kontaktliste entfernen?" - -#: ../src/gajim.py:1095 -#, python-format -msgid "Contact with \"%s\" cannot be established" -msgstr "Kontakt mit \"%s\" konnte nicht hergestellt werden" - -#: ../src/gajim.py:1096 ../src/common/connection.py:593 -msgid "Check your connection or try again later." -msgstr "" -"Überprüfen Sie die Verbindung oder versuchen Sie es später noch einmal." - -#: ../src/gajim.py:1275 ../src/groupchat_control.py:1036 -#, python-format -msgid "%s is now known as %s" -msgstr "%s heißt jetzt %s" - -#: ../src/gajim.py:1290 ../src/groupchat_control.py:1185 -#: ../src/roster_window.py:1860 -#, python-format -msgid "%s is now %s" -msgstr "%s ist jetzt %s" - -#. Can be a presence (see chg_contact_status in groupchat_control.py) -#. Can be a message (see handle_event_gc_config_change in gajim.py) -#: ../src/gajim.py:1410 ../src/groupchat_control.py:996 -msgid "Any occupant is allowed to see your full JID" -msgstr "Jeder Teilnehmer darf Ihre volle JID sehen" - -#: ../src/gajim.py:1413 -msgid "Room now shows unavailable member" -msgstr "Raum zeigt jetzt abwesende Teilnehmer" - -#: ../src/gajim.py:1415 -msgid "room now does not show unavailable members" -msgstr "Raum zeigt jetzt abwesende Teilnehmer nicht an" - -#: ../src/gajim.py:1418 -msgid "A non-privacy-related room configuration change has occurred" -msgstr "" -"Eine nicht Privatsphären-bezogene Raum-Konfigurations-Änderung ist " -"aufgetreten" - -#. Can be a presence (see chg_contact_status in groupchat_control.py) -#: ../src/gajim.py:1421 -msgid "Room logging is now enabled" -msgstr "Raum-Mitschnitt ist jetzt aktiviert" - -#: ../src/gajim.py:1423 -msgid "Room logging is now disabled" -msgstr "Raum-Mitschnitt ist jetzt deaktiviert" - -#: ../src/gajim.py:1425 -msgid "Room is now non-anonymous" -msgstr "Raum ist jetzt un-anonym" - -#: ../src/gajim.py:1428 -msgid "Room is now semi-anonymous" -msgstr "Raum ist jetzt semi-anonym" - -#: ../src/gajim.py:1431 -msgid "Room is now fully-anonymous" -msgstr "Raum ist jetzt voll anonym" - -#: ../src/gajim.py:1463 -#, python-format -msgid "A Password is required to join the room %s. Please type it." -msgstr "Für das Betreten des Gruppenchats-Raumes %s wird ein Passwort benötig." - -#: ../src/gajim.py:1497 -msgid "" -"You configured Gajim to use GPG agent, but there is no GPG agent running or " -"it returned a wrong passphrase.\n" -msgstr "" -"Du hast Gajim für die Verwendung des GPG-Agenten konfiguriert, aber es läuft " -"kein GPG-Agent oder er gab eine falsche Passphrase zurück.\n" - -#: ../src/gajim.py:1499 -msgid "You are currently connected without your OpenPGP key." -msgstr "Sie wurden ohne ihren GPG-Schlüssel verbunden" - -#: ../src/gajim.py:1502 -msgid "Your passphrase is incorrect" -msgstr "Ihre Passphrase ist falsch" - -#: ../src/gajim.py:1519 -msgid "Passphrase Required" -msgstr "Passphrase benötigt" - -#: ../src/gajim.py:1520 -#, python-format -msgid "Enter GPG key passphrase for account %s." -msgstr "Geben Sie die GPG-Passphrase für das Konto %s ein." - -#: ../src/gajim.py:1532 -msgid "Wrong Passphrase" -msgstr "Falsche Passphrase" - -#: ../src/gajim.py:1533 -msgid "Please retype your GPG passphrase or press Cancel." -msgstr "" -"Bitte geben Sie Ihre GPG-Passphrase erneut ein oder klicken Sie auf " -"Abbrechen." - -#: ../src/gajim.py:1638 -#, python-format -msgid "New mail on %(gmail_mail_address)s" -msgstr "Neue E-Mail auf %(gmail_mail_address)s" - -#: ../src/gajim.py:1640 -#, python-format -msgid "You have %d new mail conversation" -msgid_plural "You have %d new mail conversations" -msgstr[0] "Sie haben %d ungelesene E-Mail-Nachricht" -msgstr[1] "Sie haben %d ungelesene E-Mail-Nachrichten" - -#. FIXME: emulate Gtalk client popups. find out what they parse and how -#. they decide what to show -#. each message has a 'From', 'Subject' and 'Snippet' field -#: ../src/gajim.py:1649 -#, python-format -msgid "" -"\n" -"From: %(from_address)s" -msgstr "" -"\n" -"Von: %(from_address)s" - -#: ../src/gajim.py:1717 -#, python-format -msgid "%s wants to send you a file." -msgstr "%s möchte ihnen eine Datei senden." - -#: ../src/gajim.py:1782 -#, python-format -msgid "You successfully received %(filename)s from %(name)s." -msgstr "Sie haben %(filename)s erfolgreich von %(name)s erhalten." - -#. ft stopped -#: ../src/gajim.py:1786 -#, python-format -msgid "File transfer of %(filename)s from %(name)s stopped." -msgstr "Dateitransfer %(filename)s von %(name)s wurde gestoppt." - -#: ../src/gajim.py:1799 -#, python-format -msgid "You successfully sent %(filename)s to %(name)s." -msgstr "Sie haben%(filename)s erfolgreich an %(name)s gesendet." - -#. ft stopped -#: ../src/gajim.py:1803 -#, python-format -msgid "File transfer of %(filename)s to %(name)s stopped." -msgstr "Dateitransfer von %(filename)s an %(name)s wurde gestoppt." - -#: ../src/gajim.py:1930 ../src/gajim.py:1974 -msgid "Confirm these session options" -msgstr "Bestätigen Sie diese Sitzungs-Optionen" - -#: ../src/gajim.py:1931 -#, python-format -msgid "" -"The remote client wants to negotiate an session with these features:\n" -"\n" -"\t\t%s\n" -"\n" -"\t\tAre these options acceptable?" -msgstr "" -"Der entfernte Client will eine Sitzung mit diesen Features aushandeln:\n" -"\n" -"\t\t%s\n" -"\n" -"\t\tSind diese Einstellungen akzeptabel?" - -#: ../src/gajim.py:1975 -#, python-format -msgid "" -"The remote client selected these options:\n" -"\n" -"%s\n" -"\n" -"Continue with the session?" -msgstr "" -"Der entfernte Client hat diese Optionen ausgewählt:\n" -"\n" -"%s\n" -"\n" -"Mit der Sitzung fortfahren?" - -#: ../src/gajim.py:2112 -msgid "Username Conflict" -msgstr "Benutzernamenkonflikt" - -#: ../src/gajim.py:2113 -msgid "Please type a new username for your local account" -msgstr "Bitte geben Sie einen neuen Benutzernamen für Ihr lokales Konto ein" - -#: ../src/gajim.py:2128 ../src/gajim.py:2130 -msgid "Ping?" -msgstr "Ping?" - -#: ../src/gajim.py:2136 ../src/gajim.py:2138 -#, python-format -msgid "Pong! (%s s.)" -msgstr "Pong! (%s s.)" - -#: ../src/gajim.py:2142 ../src/gajim.py:2144 -msgid "Error." -msgstr "Fehler:" - -#: ../src/gajim.py:2169 -msgid "Resource Conflict" -msgstr "Resourcenkonflikt" - -#: ../src/gajim.py:2170 -msgid "" -"You are already connected to this account with the same resource. Please " -"type a new one" -msgstr "" -"Sie sind mit diesem Konto bereits mit derselben Ressource verbunden.Bitte " -"geben Sie eine neue ein" - -#: ../src/gajim.py:2223 -msgid "Error verifying SSL certificate" -msgstr "Fehler bei der Überprüfung des SSL-Zertifikats" - -#: ../src/gajim.py:2224 -#, python-format -msgid "" -"There was an error verifying the SSL certificate of your jabber server: %" -"(error)s\n" -"Do you still want to connect to this server?" -msgstr "" -"Es trat ein Fehler bei der Überprüfung des SSL-Zertifikats deines Jabber-" -"Servers auf: %(error)s\n" -"Möchtest du dich dennoch zum Server verbinden?" - -#: ../src/gajim.py:2229 -msgid "Ignore this error for this certificate." -msgstr "Ignoriere den Fehler für dieses Zertifikat." - -#: ../src/gajim.py:2244 -msgid "SSL certificate error" -msgstr "SSL-Zertifikat-Fehler" - -#: ../src/gajim.py:2245 -#, python-format -msgid "" -"It seems the SSL certificate has changed or your connection is being " -"hacked.\n" -"Old fingerprint: %s\n" -"New fingerprint: %s\n" -"\n" -"Do you still want to connect and update the fingerprint of the certificate?" -msgstr "" -"Das SSL-Zertifikat scheint sich geändert zu haben oder die Verbindung wurde " -"gehackt.\n" -"Alter Fingerprint: %s\n" -"Neuer Fingerprint: %s\n" -"\n" -"Möchten Sie noch verbinden und den Fingerprint des Zertifikats aktualisieren?" - -#: ../src/gajim.py:2264 -msgid "Insecure connection" -msgstr "Unsichere Verbindung" - -#: ../src/gajim.py:2265 -msgid "" -"You are about to send your password on an insecure connection. Are you sure " -"you want to do that?" -msgstr "" -"Sie sind dabei Ihr Passwort über eine unsichere Verbindung zu senden. Sind " -"Sie sicher, dass Sie das wollen?" - -#: ../src/gajim.py:2267 ../src/groupchat_control.py:1663 -#: ../src/roster_window.py:3602 -msgid "Do _not ask me again" -msgstr "_Nicht noch einmal fragen" - -#: ../src/gajim.py:2280 -msgid "PEP node was not removed" -msgstr "PEP Knoten wurde nicht entfernt" - -#: ../src/gajim.py:2281 -#, python-format -msgid "PEP node %s was not removed: %s" -msgstr "PEP Knoten %s wurde nicht entfernt: %s" - -#. theme doesn't exist, disable emoticons -#: ../src/gajim.py:2751 ../src/gajim.py:2772 -msgid "Emoticons disabled" -msgstr "Emoticons deaktiviert" - -#: ../src/gajim.py:2752 -msgid "" -"Your configured emoticons theme has not been found, so emoticons have been " -"disabled." -msgstr "" -"Das konfigurierte Emoticon-Thema wurde nicht gefunden. Emoticans sind " -"deaktiviert." - -#: ../src/gajim.py:2773 -msgid "" -"Your configured emoticons theme cannot been loaded. You maybe need to update " -"the format of emoticons.py file. See http://trac.gajim.org/wiki/Emoticons " -"for more details." -msgstr "" -"Dein konfiguriertes Emoticon-Thema kann nicht geladen werden. Du musst " -"vielleicht das Format der Datei emoticons.py aktualisieren. Gehe auf http://" -"trac.gajim.org/wiki/Emoticons für mehr Informationen." - -#: ../src/gajim.py:2799 ../src/roster_window.py:1155 -#: ../src/roster_window.py:3137 -msgid "You cannot join a group chat while you are invisible" -msgstr "" -"Sie können einem Gruppenchat nicht beitreten, wenn Sie unsichtbar sind." - -#. it is good to notify the user -#. in case he or she cannot see the output of the console -#: ../src/gajim.py:3074 -msgid "Could not save your settings and preferences" -msgstr "Konnte Einstellungen nicht speichern" - -#: ../src/gajim.py:3165 -msgid "Bookmark already set" -msgstr "Lesezeichen existiert schon" - -#: ../src/gajim.py:3166 -#, python-format -msgid "Group Chat \"%s\" is already in your bookmarks." -msgstr "Der Gruppenchat \"%s\" ist schon in den Lesezeichen." - -#: ../src/gajim.py:3179 -msgid "Bookmark has been added successfully" -msgstr "Lesezeichen wurde erfolgreich hinzugefügt" - -#: ../src/gajim.py:3180 -msgid "You can manage your bookmarks via Actions menu in your roster." -msgstr "" -"Sie können ihre Lesezeichen über das Aktionen-Menü in der Kontaktliste " -"bearbeiten" - -#. sorted alphanum -#: ../src/gajim.py:3275 ../src/common/config.py:102 -#: ../src/common/config.py:429 ../src/common/optparser.py:212 -#: ../src/common/optparser.py:430 ../src/common/optparser.py:464 -msgid "default" -msgstr "Standard" - -#: ../src/gajim.py:3361 -msgid "Network Manager support not available" -msgstr "Unterstützung für Network Manager nicht verfügbar" - -#: ../src/gajim.py:3474 -msgid "Session Management support not available (missing gnome.ui module)" -msgstr "Sitzungsmanagment-Unterstützung nicht verfügbar (Modul gnome.ui fehlt)" - -#: ../src/gajim-remote.py:74 -msgid "Shows a help on specific command" -msgstr "zeige die Hilfe für einen bestimmten Befehl" - -#. User gets help for the command, specified by this parameter -#: ../src/gajim-remote.py:77 -msgid "command" -msgstr "Befehl" - -#: ../src/gajim-remote.py:78 -msgid "show help on command" -msgstr "zeige Hilfe für Befehl" - -#: ../src/gajim-remote.py:82 -msgid "Shows or hides the roster window" -msgstr "Ein- oder Ausblenden des Hauptfensters" - -#: ../src/gajim-remote.py:86 -msgid "Pops up a window with the next pending event" -msgstr "Zeige Popup-Fenster mit dem nächsten ungelesenen Ereignis" - -#: ../src/gajim-remote.py:90 -msgid "" -"Prints a list of all contacts in the roster. Each contact appears on a " -"separate line" -msgstr "Gibt eine Liste aller Kontakte im Roster aus. Eine Zeile je Kontakt" - -#: ../src/gajim-remote.py:93 ../src/gajim-remote.py:108 -#: ../src/gajim-remote.py:118 ../src/gajim-remote.py:131 -#: ../src/gajim-remote.py:145 ../src/gajim-remote.py:154 -#: ../src/gajim-remote.py:175 ../src/gajim-remote.py:205 -#: ../src/gajim-remote.py:214 ../src/gajim-remote.py:221 -#: ../src/gajim-remote.py:228 ../src/gajim-remote.py:239 -#: ../src/gajim-remote.py:255 ../src/gajim-remote.py:264 -msgid "account" -msgstr "Konto" - -#: ../src/gajim-remote.py:93 -msgid "show only contacts of the given account" -msgstr "zeige nur Kontakte für das gegebene Konto" - -#: ../src/gajim-remote.py:99 -msgid "Prints a list of registered accounts" -msgstr "Gibt eine Liste registrierter Konten aus" - -#: ../src/gajim-remote.py:103 -msgid "Changes the status of account or accounts" -msgstr "Ändere den Status eines oder mehrerer Konten" - -#. offline, online, chat, away, xa, dnd, invisible should not be translated -#: ../src/gajim-remote.py:106 -msgid "status" -msgstr "Status" - -#: ../src/gajim-remote.py:106 -msgid "one of: offline, online, chat, away, xa, dnd, invisible " -msgstr "Eins von: offline, online, chat, away, xa, dnd, invisible " - -#: ../src/gajim-remote.py:107 ../src/gajim-remote.py:128 -#: ../src/gajim-remote.py:142 ../src/gajim-remote.py:153 -msgid "message" -msgstr "Nachricht" - -#: ../src/gajim-remote.py:107 -msgid "status message" -msgstr "Statusnachricht" - -#: ../src/gajim-remote.py:108 -msgid "" -"change status of account \"account\". If not specified, try to change status " -"of all accounts that have \"sync with global status\" option set" -msgstr "" -"Ändere Status vom Konto \"account\". Falls nicht angegeben, ändere Status " -"aller Konten für die \"Kontostatus mit globalem Status abgleichen\" " -"aktiviert ist" - -#: ../src/gajim-remote.py:114 -msgid "Shows the chat dialog so that you can send messages to a contact" -msgstr "" -"Zeige Chatfenster, so dass eine Nachricht an einen Kontakt gesendet werden " -"kann" - -#: ../src/gajim-remote.py:116 -msgid "JID of the contact that you want to chat with" -msgstr "JID des Kontakts mit Sie chatten möchten" - -#: ../src/gajim-remote.py:118 ../src/gajim-remote.py:205 -msgid "if specified, contact is taken from the contact list of this account" -msgstr "falls angegeben, wird der Kontakt von der Liste dieses Kontos gewählt" - -#: ../src/gajim-remote.py:123 -msgid "" -"Sends new chat message to a contact in the roster. Both OpenPGP key and " -"account are optional. If you want to set only 'account', without 'OpenPGP " -"key', just set 'OpenPGP key' to ''." -msgstr "" -"Sende neue Chat-Nachricht zu einem Kontakt im Roster. OpenPGP-Schlüssel und " -"Konto sind optional. Falls nur 'Konto' gesetzt werden soll, ohne OpenGPG-" -"Schlüssel, setzen Sie 'OpenGPG-Schlüssel' einfach auf ''." - -#: ../src/gajim-remote.py:127 ../src/gajim-remote.py:140 -msgid "JID of the contact that will receive the message" -msgstr "JID des Kontakts, der die Nachricht empfängt" - -#: ../src/gajim-remote.py:128 ../src/gajim-remote.py:142 -#: ../src/gajim-remote.py:153 -msgid "message contents" -msgstr "Nachrichteninhalt" - -#: ../src/gajim-remote.py:129 ../src/gajim-remote.py:143 -msgid "pgp key" -msgstr "PGP-Schlüssel" - -#: ../src/gajim-remote.py:129 ../src/gajim-remote.py:143 -msgid "if specified, the message will be encrypted using this public key" -msgstr "falls angegeben, wird die Nachricht damit verschlüsselt" - -#: ../src/gajim-remote.py:131 ../src/gajim-remote.py:145 -#: ../src/gajim-remote.py:154 -msgid "if specified, the message will be sent using this account" -msgstr "falls angegeben, wird die Nachricht über dieses Konto gesendet" - -#: ../src/gajim-remote.py:136 -msgid "" -"Sends new single message to a contact in the roster. Both OpenPGP key and " -"account are optional. If you want to set only 'account', without 'OpenPGP " -"key', just set 'OpenPGP key' to ''." -msgstr "" -"Sende neue einzelne Nachricht an einen Kontakt im Roster. OpenPGP-Schlüssel " -"und Konto sind optional. Falls nur 'Konto' gesetzt werden soll, ohne OpenGPG-" -"Schlüssel, lassen Sie 'OpenGPG-Schlüssel' einfach leer." - -#: ../src/gajim-remote.py:141 -msgid "subject" -msgstr "Betreff" - -#: ../src/gajim-remote.py:141 -msgid "message subject" -msgstr "Nachrichten-Betreff" - -#: ../src/gajim-remote.py:150 -msgid "Sends new message to a groupchat you've joined." -msgstr "" -"Sendet eine neue Nachricht an einen Gruppenchat, den Sie betreten haben." - -#: ../src/gajim-remote.py:152 -msgid "JID of the room that will receive the message" -msgstr "JID des Raums, der die Nachricht empfängt" - -#: ../src/gajim-remote.py:159 -msgid "Gets detailed info on a contact" -msgstr "Zeige detaillierte Informationen über Kontakt" - -#: ../src/gajim-remote.py:161 ../src/gajim-remote.py:174 -#: ../src/gajim-remote.py:204 ../src/gajim-remote.py:213 -msgid "JID of the contact" -msgstr "JID des Kontakts" - -#: ../src/gajim-remote.py:165 -msgid "Gets detailed info on a account" -msgstr "Zeige detaillierte Informationen über ein Konto" - -#: ../src/gajim-remote.py:167 -msgid "Name of the account" -msgstr "Name des Kontos" - -#: ../src/gajim-remote.py:171 -msgid "Sends file to a contact" -msgstr "Sendet dem Kontakt eine Datei" - -#: ../src/gajim-remote.py:173 -msgid "file" -msgstr "Datei" - -#: ../src/gajim-remote.py:173 -msgid "File path" -msgstr "Dateipfad" - -#: ../src/gajim-remote.py:175 -msgid "if specified, file will be sent using this account" -msgstr "Falls angegeben, wird die Nachricht über dieses Konto gesendet" - -#: ../src/gajim-remote.py:180 -msgid "Lists all preferences and their values" -msgstr "Zeige alle Einstellung und ihre Werte" - -#: ../src/gajim-remote.py:184 -msgid "Sets value of 'key' to 'value'." -msgstr "Setze Wert von 'Schlüssel' auf 'Wert'." - -#: ../src/gajim-remote.py:186 -msgid "key=value" -msgstr "key=value" - -#: ../src/gajim-remote.py:186 -msgid "'key' is the name of the preference, 'value' is the value to set it to" -msgstr "'key' ist der Name der Option, 'value' ist der der einzustellende Wert" - -#: ../src/gajim-remote.py:191 -msgid "Deletes a preference item" -msgstr "Lösche eine Einstellung" - -#: ../src/gajim-remote.py:193 -msgid "key" -msgstr "Schlüssel" - -#: ../src/gajim-remote.py:193 -msgid "name of the preference to be deleted" -msgstr "Name der zu löschenden Einstellung" - -#: ../src/gajim-remote.py:197 -msgid "Writes the current state of Gajim preferences to the .config file" -msgstr "Schreibe die aktuellen Gajim-Einstellung in die Konfigurationsdatei" - -#: ../src/gajim-remote.py:202 -msgid "Removes contact from roster" -msgstr "Entfernt den Kontakt aus dem Roster" - -#: ../src/gajim-remote.py:211 -msgid "Adds contact to roster" -msgstr "Fügt Kontakt zum Roster hinzu" - -#: ../src/gajim-remote.py:213 -msgid "jid" -msgstr "JID" - -#: ../src/gajim-remote.py:214 -msgid "Adds new contact to this account" -msgstr "Fügt neuen Kontakt zu diesem Konto hinzu" - -#: ../src/gajim-remote.py:219 -msgid "Returns current status (the global one unless account is specified)" -msgstr "" -"Gibt derzeitigen Status zurück (der globale Status, außer, es wird ein " -"Account spezifiziert)" - -#: ../src/gajim-remote.py:226 -msgid "" -"Returns current status message(the global one unless account is specified)" -msgstr "" -"Gibt derzeitige Statusnachricht zurück (der globale, außer, es wird ein " -"Account angegeben)" - -#: ../src/gajim-remote.py:233 -msgid "Returns number of unread messages" -msgstr "Gibt die Anzahl der ungelesenen Nachrichten zurück" - -#: ../src/gajim-remote.py:237 -msgid "Opens 'Start Chat' dialog" -msgstr "Öffnet den 'Chat starten' Dialog" - -#: ../src/gajim-remote.py:239 -msgid "Starts chat, using this account" -msgstr "Starte Chat mit diesem Account" - -#: ../src/gajim-remote.py:243 -msgid "Sends custom XML" -msgstr "Benutzerdefinierten XML-Code senden" - -#: ../src/gajim-remote.py:245 -msgid "XML to send" -msgstr "Zu sendender XML-Code" - -#: ../src/gajim-remote.py:246 -msgid "" -"Account in which the xml will be sent; if not specified, xml will be sent to " -"all accounts" -msgstr "" -"Konto, an welchen XML gesendet wird; wenn nicht spezifiziert, wird XML an " -"alle Konten gesendet" - -#: ../src/gajim-remote.py:252 -msgid "Handle a xmpp:/ uri" -msgstr "Behandle eine xmpp:/ URI" - -#: ../src/gajim-remote.py:254 -msgid "uri" -msgstr "URI" - -#: ../src/gajim-remote.py:259 -msgid "Join a MUC room" -msgstr "Neuen Raum betreten" - -#: ../src/gajim-remote.py:261 -msgid "room" -msgstr "Raum" - -#: ../src/gajim-remote.py:262 -msgid "nick" -msgstr "Spitzname" - -#: ../src/gajim-remote.py:263 -msgid "password" -msgstr "Passwort" - -#: ../src/gajim-remote.py:268 -msgid "Check if Gajim is running" -msgstr "Bitte überprüfen Sie, ob Gajim läuft" - -#: ../src/gajim-remote.py:272 ../src/gajim-remote.py:282 -#, fuzzy -msgid "Shows or hides the ipython window" -msgstr "Ein- oder Ausblenden des Hauptfensters" - -#: ../src/gajim-remote.py:306 -msgid "Missing argument \"contact_jid\"" -msgstr "Fehlendes Argument \"contact_jid\"" - -#: ../src/gajim-remote.py:325 -#, python-format -msgid "" -"'%s' is not in your roster.\n" -"Please specify account for sending the message." -msgstr "" -"'%s' ist nicht in ihrer Kontaktliste.\n" -"Bitte geben Sie ein Konto zum Senden der Nachricht an." - -#: ../src/gajim-remote.py:328 -msgid "You have no active account" -msgstr "Kein aktives Konto" - -#: ../src/gajim-remote.py:381 -msgid "It seems Gajim is not running. So you can't use gajim-remote." -msgstr "" -"Gajim scheint nicht gestartet zu sein. gajim-remote kann somit nicht genutzt " -"werden." - -#: ../src/gajim-remote.py:410 -#, python-format -msgid "" -"Usage: %s %s %s \n" -"\t %s" -msgstr "" -"Verwendung: %s %s %s \n" -"\t %s" - -#: ../src/gajim-remote.py:413 -msgid "Arguments:" -msgstr "Parameter:" - -#: ../src/gajim-remote.py:417 -#, python-format -msgid "%s not found" -msgstr "%s nicht gefunden" - -#: ../src/gajim-remote.py:421 -#, python-format -msgid "" -"Usage: %s command [arguments]\n" -"Command is one of:\n" -msgstr "" -"Verwendung: %s Befehl [Argumente]\n" -"Befehl ist einer von:\n" - -#: ../src/gajim-remote.py:494 -#, python-format -msgid "" -"Too many arguments. \n" -"Type \"%s help %s\" for more info" -msgstr "" -"Zu viele Argumente. \n" -"Geben Sie \"%s help %s\" ein für weitere Informationen" - -#: ../src/gajim-remote.py:498 -#, python-format -msgid "" -"Argument \"%s\" is not specified. \n" -"Type \"%s help %s\" for more info" -msgstr "" -"Argument \"%s\" wurde nicht angegeben. \n" -"Geben Sie \"%s help %s\" ein für Hilfe" - -#: ../src/gajim-remote.py:516 -msgid "Wrong uri" -msgstr "Falscher URI" - -#: ../src/gajim_themes_window.py:67 -msgid "Theme" -msgstr "Thema" - -#: ../src/gajim_themes_window.py:105 -msgid "You cannot make changes to the default theme" -msgstr "Sie können ihr derzeitiges Theme nicht ändern" - -#: ../src/gajim_themes_window.py:106 -msgid "Please create a clean new theme with your desired name." -msgstr "" -"Bitte erstellen Sie ein neues leeres Thema mit Ihrem gewünschten Namen." - -#. don't confuse translators -#: ../src/gajim_themes_window.py:180 -msgid "theme name" -msgstr "Name des Themas" - -#: ../src/gajim_themes_window.py:197 -msgid "You cannot delete your current theme" -msgstr "Sie können ihr derzeitiges Theme nicht löschen" - -#: ../src/gajim_themes_window.py:198 -msgid "Please first choose another for your current theme." -msgstr "Bitte wählen Sie zuerst einen anderen Namen für ihr derzeitiges Theme." - -#: ../src/groupchat_control.py:147 -msgid "Sending private message failed" -msgstr "Senden privater Nachricht fehlgeschlagen" - -#. in second %s code replaces with nickname -#: ../src/groupchat_control.py:149 -#, python-format -msgid "You are no longer in group chat \"%s\" or \"%s\" has left." -msgstr "" -"Sie sind nicht mehr im Gruppenchat \"%s\" oder \"%s\" hat den Gruppenchat " -"verlassen." - -#: ../src/groupchat_control.py:385 -msgid "Insert Nickname" -msgstr "Spitzname einfügen" - -#: ../src/groupchat_control.py:924 -msgid "Really send file?" -msgstr "Datei wirklich senden?" - -#: ../src/groupchat_control.py:925 -#, python-format -msgid "If you send a file to %s, he/she will know your real Jabber ID." -msgstr "" -"Wenn Sie eine Datei an %s senden wird er/sie Ihre echte Jabber ID kennen." - -#. Can be a message (see handle_event_gc_config_change in gajim.py) -#: ../src/groupchat_control.py:999 -msgid "Room logging is enabled" -msgstr "Raum-Mitschnitt ist aktiviert" - -#: ../src/groupchat_control.py:1001 -msgid "A new room has been created" -msgstr "Ein neuer Raum wurde erstellt" - -#: ../src/groupchat_control.py:1004 -msgid "The server has assigned or modified your roomnick" -msgstr "Der Server hat Ihren Raum-Nick zugewiesen oder verändert" - -#. do not print 'kicked by None' -#: ../src/groupchat_control.py:1010 -#, python-format -msgid "%(nick)s has been kicked: %(reason)s" -msgstr "%(nick)s wurde rausgeschmissen: %(reason)s" - -#: ../src/groupchat_control.py:1014 -#, python-format -msgid "%(nick)s has been kicked by %(who)s: %(reason)s" -msgstr "%(nick)s wurde von %(who)s rausgeschmissen: %(reason)s" - -#. do not print 'banned by None' -#: ../src/groupchat_control.py:1021 -#, python-format -msgid "%(nick)s has been banned: %(reason)s" -msgstr "%(nick)s wurde gebannt: %(reason)s" - -#: ../src/groupchat_control.py:1025 -#, python-format -msgid "%(nick)s has been banned by %(who)s: %(reason)s" -msgstr "%(nick)s wurde von %(who)s gebannt: %(reason)s" - -#: ../src/groupchat_control.py:1034 -#, python-format -msgid "You are now known as %s" -msgstr "Sie heißen nun %s" - -#: ../src/groupchat_control.py:1070 ../src/groupchat_control.py:1074 -#: ../src/groupchat_control.py:1079 -#, python-format -msgid "%(nick)s has been removed from the room (%(reason)s)" -msgstr "%(nick)s wurde von %(who)s rausgeschmissen: %(reason)s" - -#: ../src/groupchat_control.py:1071 -msgid "affiliation changed" -msgstr "Zugehörigkeit geändert: " - -#: ../src/groupchat_control.py:1076 -msgid "room configuration changed to members-only" -msgstr "Gruppenchatkonfiguration wurde auf ausschließlich Mitgleider geändert" - -#: ../src/groupchat_control.py:1081 -msgid "system shutdown" -msgstr "System wird heruntergefahren" - -#: ../src/groupchat_control.py:1178 -#, python-format -msgid "%s has left" -msgstr "%s ist gegangen" - -#: ../src/groupchat_control.py:1183 -#, python-format -msgid "%s has joined the group chat" -msgstr "%s hat den Gruppenchat betreten" - -#. Invalid Nickname -#. invalid char -#: ../src/groupchat_control.py:1296 ../src/groupchat_control.py:1568 -msgid "Invalid nickname" -msgstr "Ungültiger Benutzername" - -#: ../src/groupchat_control.py:1320 ../src/groupchat_control.py:1338 -#: ../src/groupchat_control.py:1422 ../src/groupchat_control.py:1439 -#, python-format -msgid "Nickname not found: %s" -msgstr "Spitzname nicht gefunden: %s" - -#: ../src/groupchat_control.py:1354 -msgid "This group chat has no subject" -msgstr "Dieser Gruppenchat hat kein Thema" - -#: ../src/groupchat_control.py:1365 -#, python-format -msgid "Invited %(contact_jid)s to %(room_jid)s." -msgstr "%(contact_jid)s in %(room_jid)s eingeladen" - -#: ../src/groupchat_control.py:1495 -#, python-format -msgid "" -"Usage: /%s [reason], bans the JID from the group chat. The " -"nickname of an occupant may be substituted, but not if it contains \"@\". If " -"the JID is currently in the group chat, he/she/it will also be kicked. Does " -"NOT support spaces in nickname." -msgstr "" -"Bedienung: /%s [Grund], bannt die JID aus dem Gruppenchat. " -"Der Spitzname eines Teilnehmers kann hinzugefügt werden, aber nicht, wenn es " -"ein \"@\" enthält. Wenn die JID derzeit im Gruppenchat ist, wird er/sie/es " -"ebenfalls gebannt. Unterstützt KEINE Leerzeichen im Spitznamen." - -#: ../src/groupchat_control.py:1502 -#, python-format -msgid "" -"Usage: /%s , opens a private chat window with the specified " -"occupant." -msgstr "" -"Verwendung: /%s , öffnet ein privates Nachrichtenfenster mit dem " -"Inhaber des angegebenen Spitznamens." - -#: ../src/groupchat_control.py:1508 -#, python-format -msgid "" -"Usage: /%s [reason], closes the current window or tab, displaying reason if " -"specified." -msgstr "" -"Bedienung: /%s [Grund], schließt das aktuelle Fenster oder Tab, mit Anzeige " -"eines Grundes, falls angegeben." - -#: ../src/groupchat_control.py:1514 -#, python-format -msgid "" -"Usage: /%s [reason], invites JID to the current group chat, optionally " -"providing a reason." -msgstr "" -"Bedienung: /%s [Grund], lädt die JID in den aktuellen Gruppenchat ein, " -"optional mit der Angabe eines Grundes." - -#: ../src/groupchat_control.py:1518 -#, python-format -msgid "" -"Usage: /%s @[/nickname], offers to join room@server optionally " -"using specified nickname." -msgstr "" -"Bedienung: /%s @ [/Spitzname], bietet den Beitritt zu " -"Raum@Server an, optional mit Spitznamen." - -#: ../src/groupchat_control.py:1522 -#, python-format -msgid "" -"Usage: /%s [reason], removes the occupant specified by nickname " -"from the group chat and optionally displays a reason. Does NOT support " -"spaces in nickname." -msgstr "" -"Bedienung: /%s [Grund], verweist den Inhaber des Spitznamen des " -"Gruppenchats und zeigt optional einen Grund. Unterstützt KEINE Leerzeichen " -"im Spitznamen." - -#: ../src/groupchat_control.py:1531 -#, python-format -msgid "" -"Usage: /%s [message], opens a private message window and sends " -"message to the occupant specified by nickname." -msgstr "" -"Verwendung: /%s [Nachricht], öffnet ein privates " -"Nachrichtenfenster und sendet die Nachricht zum Inhaber des angegebenen " -"Spitznamens." - -#: ../src/groupchat_control.py:1536 -#, python-format -msgid "Usage: /%s , changes your nickname in current group chat." -msgstr "" -"Bedienung: /%s , ändert den Spitznamen im aktuellen Gruppenchat." - -#: ../src/groupchat_control.py:1540 -#, python-format -msgid "Usage: /%s , display the names of group chat occupants." -msgstr "Benutzung: /%s , zeigt die Namen der Gruppenchatbesucher." - -#: ../src/groupchat_control.py:1544 -#, python-format -msgid "Usage: /%s [topic], displays or updates the current group chat topic." -msgstr "" -"Benutzung: /%s [topic], zeigt oder aktualisiert das derzeitige Gruppenchat-" -"Thema." - -#: ../src/groupchat_control.py:1547 -#, python-format -msgid "" -"Usage: /%s , sends a message without looking for other commands." -msgstr "" -"Benutzung: /%s , sendet eine Nachricht ohne andere Befehle zu " -"beachten." - -#: ../src/groupchat_control.py:1657 -#, python-format -msgid "Are you sure you want to leave group chat \"%s\"?" -msgstr "Möchten Sie den Gruppenchat \"%s\" wirklich verlassen?" - -#: ../src/groupchat_control.py:1659 -msgid "" -"If you close this window, you will be disconnected from this group chat." -msgstr "" -"Wenn Sie dieses Fenster schließen, wird die Verbindung zu diesem Gruppenchat " -"geschlossen." - -#: ../src/groupchat_control.py:1695 -msgid "Changing Subject" -msgstr "Thema ändern" - -#: ../src/groupchat_control.py:1696 -msgid "Please specify the new subject:" -msgstr "Bitte bestimmen Sie ein neues Thema" - -#: ../src/groupchat_control.py:1707 -msgid "Changing Nickname" -msgstr "Spitzname ändern" - -#: ../src/groupchat_control.py:1708 -msgid "Please specify the new nickname you want to use:" -msgstr "Bitte geben Sie an, welchen Spitznamen Sie verwenden möchten:" - -#. Ask for a reason -#: ../src/groupchat_control.py:1723 -#, python-format -msgid "Destroying %s" -msgstr "Zerstöre %s" - -#: ../src/groupchat_control.py:1724 -msgid "" -"You are going to definitively destroy this room.\n" -"You may specify a reason below:" -msgstr "" -"Sie werden den Raum endgültig zerstören.\n" -"Sie können hier einen Grund angeben:" - -#: ../src/groupchat_control.py:1726 -msgid "You may also enter an alternate venue:" -msgstr "Sie können auch einen alternativen Raum eintragen:" - -#. ask for reason -#: ../src/groupchat_control.py:1910 -#, python-format -msgid "Kicking %s" -msgstr "%s rausschmeißen" - -#: ../src/groupchat_control.py:1911 ../src/groupchat_control.py:2215 -msgid "You may specify a reason below:" -msgstr "Sie können eine Begründung angeben:" - -#. ask for reason -#: ../src/groupchat_control.py:2214 -#, python-format -msgid "Banning %s" -msgstr "%s verbannen" - -#: ../src/gtkexcepthook.py:46 -msgid "A programming error has been detected" -msgstr "Es wurde ein Programmfehler entdeckt" - -#: ../src/gtkexcepthook.py:47 -msgid "" -"It probably is not fatal, but should be reported to the developers " -"nonetheless." -msgstr "" -"Es ist vermutlich nicht fatal, aber der Fehler sollte trotzdem den " -"Entwicklern gemeldet werden" - -#: ../src/gtkexcepthook.py:53 -msgid "_Report Bug" -msgstr "Bug melden" - -#: ../src/gtkexcepthook.py:76 -msgid "Details" -msgstr "Details" - -#. we talk about file -#: ../src/gtkgui_helpers.py:162 ../src/gtkgui_helpers.py:177 -#, python-format -msgid "Error: cannot open %s for reading" -msgstr "Fehler: Kann %s kann nicht zum Lesen öffnen" - -#: ../src/gtkgui_helpers.py:352 -msgid "Error reading file:" -msgstr "Fehler beim Lesen der Datei:" - -#: ../src/gtkgui_helpers.py:355 -msgid "Error parsing file:" -msgstr "Fehler bei der Dateianalyse:" - -#. do not traceback (could be a permission problem) -#. we talk about a file here -#: ../src/gtkgui_helpers.py:392 -#, python-format -msgid "Could not write to %s. Session Management support will not work" -msgstr "" -"Konnte nicht an %s schreiben. Sitzungmanagment-Unterstützung wird nicht " -"funktionieren" - -#. xmpp: is currently handled by another program, so ask the user -#: ../src/gtkgui_helpers.py:724 -msgid "Gajim is not the default Jabber client" -msgstr "Gajim ist nicht Ihr Standard-Jabber-Client" - -#: ../src/gtkgui_helpers.py:725 -msgid "Would you like to make Gajim the default Jabber client?" -msgstr "Möchten Sie Gajim zu Ihrem Standard-Jabber-Client machen?" - -#: ../src/gtkgui_helpers.py:726 -msgid "Always check to see if Gajim is the default Jabber client on startup" -msgstr "" -"Immer beim Programmstart prüfen, ob Gajim der Standard-Jabber-Client ist" - -#: ../src/gtkgui_helpers.py:823 -msgid "Extension not supported" -msgstr "Erweiterung wird nicht unterstützt." - -#: ../src/gtkgui_helpers.py:824 -#, python-format -msgid "Image cannot be saved in %(type)s format. Save as %(new_filename)s?" -msgstr "" -"Bild kann nicht im %(type)s-Format gespeichert werden. Als %(new_filename)s " -"speichern?" - -#: ../src/gtkgui_helpers.py:833 -msgid "Save Image as..." -msgstr "Bild speichern unter ..." - -#: ../src/history_manager.py:90 -msgid "Cannot find history logs database" -msgstr "Kann Verlaufs-Datenkbank nicht finden" - -#. holds jid -#: ../src/history_manager.py:130 -msgid "Contacts" -msgstr "Kontakte" - -#. holds time -#: ../src/history_manager.py:143 ../src/history_manager.py:183 -#: ../src/history_window.py:100 -msgid "Date" -msgstr "Datum" - -#. holds message -#: ../src/history_manager.py:157 ../src/history_manager.py:189 -#: ../src/history_window.py:108 -msgid "Message" -msgstr "Nachricht" - -#: ../src/history_manager.py:209 -msgid "" -"Do you want to clean up the database? (STRONGLY NOT RECOMMENDED IF GAJIM IS " -"RUNNING)" -msgstr "" -"Möchten Sie die Datenbank aufräumen? (NICHT EMPFOHLEN, WENN GAJIM GERADE " -"LÄUFT)" - -#: ../src/history_manager.py:211 -msgid "" -"Normally allocated database size will not be freed, it will just become " -"reusable. If you really want to reduce database filesize, click YES, else " -"click NO.\n" -"\n" -"In case you click YES, please wait..." -msgstr "" -"Der reservierte Datenbankspeicherplatz wird nicht freigegeben, er wird nur " -"wiederverwendbar. Wenn Sie wirklich den Speicherplatz reduzieren möchten, " -"klicken Sie JA, ansonsten NEIN.\n" -"\n" -"Falls Sie JA klicken, warten Sie bitte einen Augenblick ..." - -#: ../src/history_manager.py:423 -msgid "Exporting History Logs..." -msgstr "Exportiere Verlauf ..." - -#: ../src/history_manager.py:498 -#, python-format -msgid "%(who)s on %(time)s said: %(message)s\n" -msgstr "%(who)s sagte um %(time)s: %(message)s\n" - -#: ../src/history_manager.py:535 -msgid "Do you really want to delete logs of the selected contact?" -msgid_plural "Do you really want to delete logs of the selected contacts?" -msgstr[0] "Möchten Sie wirklich alle Logs des ausgewählten Kontakts löschen?" -msgstr[1] "Möchten Sie wirklich alle Logs der ausgewählten Kontakte löschen?" - -#: ../src/history_manager.py:539 ../src/history_manager.py:574 -msgid "This is an irreversible operation." -msgstr "Dies ist ein unwiderruflicher Vorgang." - -#: ../src/history_manager.py:571 -msgid "Do you really want to delete the selected message?" -msgid_plural "Do you really want to delete the selected messages?" -msgstr[0] "Möchten Sie die ausgewählte Nachricht wirklich löschen?" -msgstr[1] "Möchten Sie die ausgewählten Nachrichten wirklich löschen?" - -#: ../src/history_window.py:282 -#, python-format -msgid "Conversation History with %s" -msgstr "Unterhaltungs-Verlauf mit %s" - -#: ../src/history_window.py:410 -#, python-format -msgid "%(nick)s is now %(status)s: %(status_msg)s" -msgstr "%(nick)s ist jezt %(status)s: %(status_msg)s" - -#: ../src/history_window.py:414 ../src/notify.py:229 -#, python-format -msgid "%(nick)s is now %(status)s" -msgstr "%(nick)s ist jetzt %(status)s" - -#: ../src/history_window.py:420 -#, python-format -msgid "Status is now: %(status)s: %(status_msg)s" -msgstr "Status ist jetzt: %(status)s: %(status_msg)s" - -#: ../src/history_window.py:423 -#, python-format -msgid "Status is now: %(status)s" -msgstr "Status ist jetzt: %(status)s" - -#: ../src/htmltextview.py:587 ../src/htmltextview.py:598 -msgid "Timeout loading image" -msgstr "Konnte Bild nicht laden" - -#: ../src/htmltextview.py:608 -msgid "Image is too big" -msgstr "Das Bild ist zu groß" - -#: ../src/message_window.py:394 -msgid "Chats" -msgstr "Chats" - -#: ../src/message_window.py:396 -msgid "Group Chats" -msgstr "Gruppenchat" - -#: ../src/message_window.py:398 -msgid "Private Chats" -msgstr "Private Chats" - -#: ../src/message_window.py:404 -msgid "Messages" -msgstr "Nachrichten" - -#: ../src/message_window.py:408 -#, python-format -msgid "%s - %s" -msgstr "%s - %s" - -#: ../src/negotiation.py:13 -msgid "- messages will be logged" -msgstr "- Nachrichten werden aufgezeichnet" - -#: ../src/negotiation.py:15 -msgid "- messages will not be logged" -msgstr "- Nachrichten werden nicht aufgezeichnet" - -#: ../src/negotiation.py:24 -msgid "OK to continue with negotiation?" -msgstr "In Ordnung mit der Verhandlung fortzufahren?" - -#: ../src/negotiation.py:25 -#, python-format -msgid "" -"You've begun an encrypted session with %s, but it can't be guaranteed that " -"you're talking directly to the person you think you are.\n" -"\n" -"You should speak with them directly (in person or on the phone) and confirm " -"that their Short Authentication String is identical to this one: %s\n" -"\n" -"Would you like to continue with the encrypted session?" -msgstr "" -"Sie haben eine verschlüsselte Sitzung mit %s begonnen, aber es kann nicht " -"garantiert werden, dass Sie tatsächlich mit der Person sprechen, die sie " -"vorgibt zu sein.\n" -"\n" -"Sie sollten mit der Person direkt (persönlich oder am Telefon) sprechen und " -"sich vergewissern, dass ihr kurzer Authentifizierungs-String mit diesem " -"übereinstimmt: %s\n" -"\n" -"Möchten Sie mit der verschlüsselten Sitzung fortfahren?" - -#: ../src/negotiation.py:31 -msgid "Yes, I verified the Short Authentication String" -msgstr "Ja, Ich habe den kurzen Authentifizierungs-String überprüft" - -#: ../src/notify.py:227 -#, python-format -msgid "%(nick)s Changed Status" -msgstr "%(nick)s änderte Status" - -#: ../src/notify.py:237 -#, python-format -msgid "%(nickname)s Signed In" -msgstr "%(nickname)s angemeldet" - -#: ../src/notify.py:245 -#, python-format -msgid "%(nickname)s Signed Out" -msgstr "%(nickname)s abgemeldet" - -#: ../src/notify.py:257 -#, python-format -msgid "New Single Message from %(nickname)s" -msgstr "Neue einzelne Nachricht von %(nickname)s" - -#: ../src/notify.py:265 -#, python-format -msgid "New Private Message from group chat %s" -msgstr "Neue private Nachricht von Gruppenchat %s" - -#: ../src/notify.py:267 -#, python-format -msgid "%(nickname)s: %(message)s" -msgstr "%(nickname)s: %(message)s" - -#: ../src/notify.py:270 -#, python-format -msgid "Messaged by %(nickname)s" -msgstr "Neue Nachricht von %(nickname)s" - -#: ../src/notify.py:276 -#, python-format -msgid "New Message from %(nickname)s" -msgstr "Neue Nachricht von %(nickname)s" - -#: ../src/profile_window.py:54 -msgid "Retrieving profile..." -msgstr "Empfange Profil ..." - -#: ../src/profile_window.py:107 ../src/roster_window.py:2638 -msgid "File is empty" -msgstr "Datei ist leer" - -#: ../src/profile_window.py:110 ../src/roster_window.py:2641 -msgid "File does not exist" -msgstr "Datei existiert nicht" - -#. keep identation -#. unknown format -#: ../src/profile_window.py:124 ../src/profile_window.py:140 -#: ../src/roster_window.py:2643 ../src/roster_window.py:2654 -msgid "Could not load image" -msgstr "Konnte Bild nicht laden" - -#: ../src/profile_window.py:250 -msgid "Information received" -msgstr "Informationen empfangen" - -#: ../src/profile_window.py:319 -msgid "Without a connection you can not publish your contact information." -msgstr "" -"Sie müssen angemeldet sein, um Kontakt-Informationen zu veröffentlichen" - -#: ../src/profile_window.py:333 -msgid "Sending profile..." -msgstr "Sende Profil ..." - -#: ../src/profile_window.py:348 -msgid "Information NOT published" -msgstr "Informationen NICHT veröffentlicht" - -#: ../src/profile_window.py:355 -msgid "vCard publication failed" -msgstr "vCard Veröffentlichung fehlgeschlagen" - -#: ../src/profile_window.py:356 -msgid "" -"There was an error while publishing your personal information, try again " -"later." -msgstr "" -"Bei der Veröffentlichung Ihrer persönlichen Informationen ist ein Fehler " -"aufgetreten, versuchen Sie es später nochmals." - -#: ../src/roster_window.py:265 ../src/roster_window.py:884 -msgid "Merged accounts" -msgstr "Alle Konten" - -#: ../src/roster_window.py:357 ../src/roster_window.py:638 -#: ../src/roster_window.py:683 ../src/roster_window.py:971 -#: ../src/roster_window.py:1191 ../src/roster_window.py:4933 -#: ../src/common/contacts.py:320 ../src/common/helpers.py:66 -msgid "Observers" -msgstr "Beobachter" - -#. When we redraw the group in remove_contact the -#. contact does still exist and so the group is still showing -#. the old numbers. -#. Make special context menu if group is Groupchats -#: ../src/roster_window.py:701 ../src/roster_window.py:719 -#: ../src/roster_window.py:1390 ../src/roster_window.py:1392 -#: ../src/roster_window.py:2689 ../src/roster_window.py:4646 -#: ../src/common/commands.py:199 ../src/common/contacts.py:107 -#: ../src/common/helpers.py:66 -msgid "Groupchats" -msgstr "Gruppenchat" - -#: ../src/roster_window.py:1696 -msgid "Authorization has been sent" -msgstr "Autorisierung wurde erneut gesendet" - -#: ../src/roster_window.py:1697 -#, python-format -msgid "Now \"%s\" will know your status." -msgstr "\"%s\" kennt jetzt ihren Status." - -#: ../src/roster_window.py:1717 -msgid "Subscription request has been sent" -msgstr "Abonnement-Anforderung wurde gesendet" - -#: ../src/roster_window.py:1718 +#: ../src/chat_control.py:2419 #, python-format -msgid "If \"%s\" accepts this request you will know his or her status." -msgstr "Wenn \"%s\" diese Anfrage akzeptiert, erfahren Sie dessen Status." - -#: ../src/roster_window.py:1730 -msgid "Authorization has been removed" -msgstr "Autorisierung wurde entfernt" - -#: ../src/roster_window.py:1731 -#, python-format -msgid "Now \"%s\" will always see you as offline." -msgstr "\"%s\" wird Sie nun immer als offline sehen." - -#: ../src/roster_window.py:1752 -msgid "" -"Gnome Keyring is installed but not correctly started\t\t\t\t\t\t\t\t" -"(environment variable probably not correctly set)" -msgstr "" -"Gnome Keyring ist installiert, aber nicht korrekt gestartet\t\t\t\t\t\t\t\t" -"(Umgebungsvariablen wahrscheinlich falsch gesetzt)" - -#: ../src/roster_window.py:1772 -msgid "GPG is not usable" -msgstr "GPG ist nicht benutzbar" - -#. %s is the account name here -#: ../src/roster_window.py:1773 ../src/common/connection_handlers.py:2217 -#: ../src/common/zeroconf/connection_zeroconf.py:174 -#, python-format -msgid "You will be connected to %s without OpenPGP." -msgstr "Sie werden ohne OpenPGP mit %s verbunden." - -#: ../src/roster_window.py:1933 ../src/roster_window.py:3087 -msgid "You are participating in one or more group chats" -msgstr "Sie nehmen an einem oder mehreren Gruppenchats teil" - -#: ../src/roster_window.py:1934 ../src/roster_window.py:3088 -msgid "" -"Changing your status to invisible will result in disconnection from those " -"group chats. Are you sure you want to go invisible?" -msgstr "" -"Wenn Sie Ihren Status auf unsichtbar setzen, werden sie von diesen " -"Gruppenchats getrennt. Sind Sie sicher, dass sie unsichtbar werden möchten?" - -#: ../src/roster_window.py:1960 -msgid "desync'ed" -msgstr "" - -#: ../src/roster_window.py:2083 ../src/roster_window.py:2331 -msgid "You have unread messages" -msgstr "Sie haben ungelesene Nachrichten" - -#: ../src/roster_window.py:2084 -msgid "" -"Messages will only be available for reading them later if you have history " -"enabled and contact is in your roster." -msgstr "" -"Nachrichten werden nur für das spätere Lesen verfügbar sein, wenn der " -"Verlauf aktiviert wurde und der Kontakt sich in der Kontaktliste befindet." - -#: ../src/roster_window.py:2332 -msgid "You must read them before removing this transport." -msgstr "Sie müssen sie alle lesen, bevor der Account entfernt wird." - -#: ../src/roster_window.py:2335 -#, python-format -msgid "Transport \"%s\" will be removed" -msgstr "Transport \"%s\" wird entfernt" - -#: ../src/roster_window.py:2336 -msgid "" -"You will no longer be able to send and receive messages from contacts using " -"this transport." -msgstr "" -"Sie können nun keine Nachrichten mehr mit Kontakten von diesem Transport " -"austauschen." - -#: ../src/roster_window.py:2339 -msgid "Transports will be removed" -msgstr "Transporte werden entfernt" - -#: ../src/roster_window.py:2344 -#, python-format -msgid "" -"You will no longer be able to send and receive messages to contacts from " -"these transports: %s" -msgstr "" -"Sie können nun keine Nachrichten mehr mit Kontakten von diesen Transporten " -"austauschen: %s" - -#. it's jid -#: ../src/roster_window.py:2510 -msgid "Rename Contact" -msgstr "Kontakt umbenennen" - -#: ../src/roster_window.py:2511 -#, python-format -msgid "Enter a new nickname for contact %s" -msgstr "Geben Sie einen Spitznamen für den Kontakt %s ein" - -#: ../src/roster_window.py:2518 -msgid "Rename Group" -msgstr "Gruppe umbenennen" - -#: ../src/roster_window.py:2519 -#, python-format -msgid "Enter a new name for group %s" -msgstr "Geben Sie einen neuen Namen für die Gruppe %s ein" - -#: ../src/roster_window.py:2572 -msgid "Remove Group" -msgstr "Gruppe entfernen" - -#: ../src/roster_window.py:2573 -#, python-format -msgid "Do you want to remove group %s from the roster?" -msgstr "Möchten Sie wirklich die Gruppe %s von Ihrer Kontaktliste entfernen?" - -#: ../src/roster_window.py:2574 -msgid "Remove also all contacts in this group from your roster" -msgstr "Auch alle Kontakte dieser Gruppe von Ihrer Kontaktliste entfernen" - -#: ../src/roster_window.py:2605 -msgid "Assign OpenPGP Key" -msgstr "OpenPGP-Schlüssel Zuweisen" - -#: ../src/roster_window.py:2606 -msgid "Select a key to apply to the contact" -msgstr "Weisen Sie dem Kontakt einen Schüssel zu" - -#: ../src/roster_window.py:2986 -#, python-format -msgid "Contact \"%s\" will be removed from your roster" -msgstr "Kontakt \"%s\" wird von ihrer Kontaktliste entfernt" - -#: ../src/roster_window.py:2990 -msgid "" -"By removing this contact you also remove authorization resulting in him or " -"her always seeing you as offline." -msgstr "" -"Durch das Entfernen dieses Kontaktes entziehen Sie ihm auch die Berechtigung " -"Ihren Status zu sehen, wodurch der Kontakt Sie nur noch als offline sehen " -"wird." - -#: ../src/roster_window.py:2995 -msgid "" -"By removing this contact you also by default remove authorization resulting " -"in him or her always seeing you as offline." -msgstr "" -"Durch das Entfernen dieses Kontaktes entziehen Sie ihm auch standardmäßig " -"die Berechtigung Ihren Status zu sehen, wodurch der Kontakt Sie nur noch als " -"offline sehen wird." - -#: ../src/roster_window.py:2998 -msgid "I want this contact to know my status after removal" -msgstr "" -"Ich möchte, dass dieser Kontakt meinen Status auch nach dem Entfernen sieht" - -#. several contact to remove at the same time -#: ../src/roster_window.py:3002 -msgid "Contacts will be removed from your roster" -msgstr "Kontakte werden von Ihrer Kontaktliste entfernt" - -#: ../src/roster_window.py:3006 -#, python-format -msgid "" -"By removing these contacts:%s\n" -"you also remove authorization resulting in them always seeing you as offline." -msgstr "" -"Durch das Entfernen dieser Kontakte:%s\n" -"entziehen Sie ihnen auch die Berechtigung Ihren Status zu sehen, wodurch die " -"Kontakte Sie nur noch als offline sehen werden." - -#: ../src/roster_window.py:3044 -msgid "No account available" -msgstr "Kein Konto vorhanden" - -#: ../src/roster_window.py:3045 -msgid "You must create an account before you can chat with other contacts." -msgstr "" -"Sie müssen ein Konto erstellen, bevor Sie sich zum Jabber-Netzwerk verbinden " -"können." - -#: ../src/roster_window.py:3519 -msgid "Metacontacts storage not supported by your server" -msgstr "" -"Das Speichern von Metakontakten wird von Ihrem Server nicht unterstützt" - -#: ../src/roster_window.py:3521 -msgid "" -"Your server does not support storing metacontacts information. So those " -"information will not be saved on next reconnection." -msgstr "" -"Ihr Server unterstützt leider nicht das Speichern von Metakontakt-" -"Informationen. Aus diesem Grund werden diese Informationen bei der nächsten " -"Neuverbindung nicht gespeichert werden." - -#: ../src/roster_window.py:3596 -msgid "" -"You are about to create a metacontact. Are you sure you want to continue?" -msgstr "" -"Sie sind dabei einen Metakontakt zu erstellen. Wollen Sie wirklich " -"fortfahren?" - -#: ../src/roster_window.py:3598 -msgid "" -"Metacontacts are a way to regroup several contacts in one line. Generally it " -"is used when the same person has several Jabber accounts or transport " -"accounts." -msgstr "" -"Metakontakte sind eine Möglichkeit, mehrere Kontakte in einer Zeile zu " -"gruppieren. Normalerweise benutzt man Sie, wenn eine Person mehrere Jabber- " -"oder Transport-Konten hat." - -#: ../src/roster_window.py:3705 -msgid "Invalid file URI:" -msgstr "Ungültige Datei URI:" - -#: ../src/roster_window.py:3716 -#, python-format -msgid "Do you want to send this file to %s:" -msgid_plural "Do you want to send those files to %s:" -msgstr[0] "Wollen Sie die Datei an %s senden:" -msgstr[1] "Wollen Sie die Dateien an %s senden:" - -#. new chat -#. single message -#. for chat_with -#. for single message -#. join gc -#: ../src/roster_window.py:4226 ../src/roster_window.py:4236 -#: ../src/roster_window.py:4245 ../src/systray.py:212 ../src/systray.py:217 -#: ../src/systray.py:223 -#, python-format -msgid "using account %s" -msgstr "mit Konto %s" - -#. add -#: ../src/roster_window.py:4252 -#, python-format -msgid "to %s account" -msgstr "an Konto %s" - -#. disco -#: ../src/roster_window.py:4257 -#, python-format -msgid "using %s account" -msgstr "mit Konto %s" - -#: ../src/roster_window.py:4338 -msgid "_Manage Bookmarks..." -msgstr "Lesezeichen _verwalten ..." - -#. profile, avatar -#: ../src/roster_window.py:4357 -#, python-format -msgid "of account %s" -msgstr "von Konto %s" - -#: ../src/roster_window.py:4397 -#, python-format -msgid "for account %s" -msgstr "für Konto %s" - -#: ../src/roster_window.py:4451 ../src/roster_window.py:4558 -msgid "_Change Status Message" -msgstr "Ändere _Statusnachricht" - -#: ../src/roster_window.py:4489 -#, fuzzy -msgid "Configure Services..." -msgstr "Konfigurieren ..." - -#: ../src/roster_window.py:4647 -msgid "_Maximize All" -msgstr "Alle _maximieren" - -#. Send Group Message -#: ../src/roster_window.py:4655 ../src/roster_window.py:5137 -msgid "Send Group M_essage" -msgstr "_Sende Nachricht an Gruppe" - -#: ../src/roster_window.py:4663 -msgid "To all users" -msgstr "An alle Benutzern" - -#: ../src/roster_window.py:4666 -msgid "To all online users" -msgstr "An alle angemeldeten Benutzer" - -#: ../src/roster_window.py:5054 -msgid "I would like to add you to my roster" -msgstr "Ich würde dich gerne in meine Liste aufnehmen" - -#. Manage Transport submenu -#: ../src/roster_window.py:5157 -msgid "_Manage Contacts" -msgstr "Kontakte verwalten" - -#. Send single message -#: ../src/roster_window.py:5218 -msgid "Send Single Message" -msgstr "Sende _einzelne Nachricht" - -#. Manage Transport submenu -#: ../src/roster_window.py:5273 -msgid "_Manage Transport" -msgstr "Transports" - -#. Modify Transport -#: ../src/roster_window.py:5281 -msgid "_Modify Transport" -msgstr "Trans_port ändern" - -#: ../src/roster_window.py:5356 -msgid "_Maximize" -msgstr "_Maximieren" - -#: ../src/roster_window.py:5363 -msgid "_Disconnect" -msgstr "_Verbindung trennen" - -#: ../src/roster_window.py:5440 -msgid "_New Group Chat" -msgstr "Neuer Gruppenchat" - -#. History manager -#: ../src/roster_window.py:5540 -msgid "History Manager" -msgstr "_Verlaufsmanager" - -#: ../src/roster_window.py:5549 -msgid "_Join New Group Chat" -msgstr "_Gruppenchat betreten" - -#: ../src/roster_window.py:5768 -msgid "Change Status Message..." -msgstr "Ändere Statusnachricht ..." - -#: ../src/search_window.py:91 -msgid "Waiting for results" -msgstr "Warte auf Ergebnisse" - -#: ../src/search_window.py:131 ../src/search_window.py:209 -msgid "Error in received dataform" -msgstr "Fehler den empfangenen Daten" - -#. No result -#: ../src/search_window.py:165 ../src/search_window.py:201 -msgid "No result" -msgstr "Kein Ergebnis" - -#: ../src/systray.py:169 -msgid "_Change Status Message..." -msgstr "Ändere _Statusnachricht ..." - -#: ../src/systray.py:254 -msgid "Hide this menu" -msgstr "Versteckt dieses Menü" - -#: ../src/tooltips.py:317 ../src/tooltips.py:566 -msgid "Jabber ID: " -msgstr "Jabber-ID:" - -#: ../src/tooltips.py:320 ../src/tooltips.py:570 -msgid "Resource: " -msgstr "Ressource: " - -#: ../src/tooltips.py:325 -#, python-format -msgid "%(owner_or_admin_or_member)s of this group chat" -msgstr "%(owner_or_admin_or_member)s dieses Gruppenchats" - -#: ../src/tooltips.py:423 -msgid " [blocked]" -msgstr " [blockiert]" - -#: ../src/tooltips.py:427 -msgid " [minimized]" -msgstr " [minimiert]" - -#: ../src/tooltips.py:442 ../src/tooltips.py:686 -msgid "Status: " -msgstr "Status: " - -#: ../src/tooltips.py:476 -#, python-format -msgid "Last status: %s" -msgstr "Letzter Status: %s" - -#: ../src/tooltips.py:478 -#, python-format -msgid " since %s" -msgstr " seit %s" - -#: ../src/tooltips.py:496 -msgid "Connected" -msgstr "Verbunden" - -#: ../src/tooltips.py:498 -msgid "Disconnected" -msgstr "Nicht verbunden" - -#: ../src/tooltips.py:507 -msgid "Mood:" -msgstr "Stimmung:" - -#: ../src/tooltips.py:519 -msgid "Activity:" -msgstr "Aktivität:" - -#: ../src/tooltips.py:539 -msgid "Unknown Artist" -msgstr "Unbekannter Künstler" - -#: ../src/tooltips.py:544 -msgid "Unknown Title" -msgstr "Unbekannter Titel" - -#: ../src/tooltips.py:549 -msgid "Unknown Source" -msgstr "Unbekannte Quelle" - -#: ../src/tooltips.py:550 -#, fuzzy -msgid "Tune:" -msgstr "Musiktitel:" - -#: ../src/tooltips.py:550 -#, python-format -msgid "" -"\"%(title)s\" by %(artist)s\n" -"from %(source)s" -msgstr "" -"\"%(title)s\" von %(artist)s\n" -"von %/source)s" - -#. ('both' is the normal sub so we don't show it) -#: ../src/tooltips.py:577 -msgid "Subscription: " -msgstr "Abonnement: " - -#: ../src/tooltips.py:587 -msgid "OpenPGP: " -msgstr "OpenPGP: " - -#: ../src/tooltips.py:642 -msgid "Download" -msgstr "Download" - -#: ../src/tooltips.py:648 -msgid "Upload" -msgstr "Upload" - -#: ../src/tooltips.py:655 -msgid "Type: " -msgstr "Typ: " - -#: ../src/tooltips.py:661 -msgid "Transferred: " -msgstr "Übertragen: " - -#: ../src/tooltips.py:664 ../src/tooltips.py:685 -msgid "Not started" -msgstr "Nicht gestartet" - -#: ../src/tooltips.py:668 -msgid "Stopped" -msgstr "Angehalten" - -#: ../src/tooltips.py:670 ../src/tooltips.py:673 -msgid "Completed" -msgstr "Abgeschlossen" - -#: ../src/tooltips.py:677 -msgid "?transfer status:Paused" -msgstr "?Transferstatus:Pausiert" - -#. stalled is not paused. it is like 'frozen' it stopped alone -#: ../src/tooltips.py:681 -msgid "Stalled" -msgstr "Steht still" - -#: ../src/tooltips.py:683 -msgid "Transferring" -msgstr "Übertrage" - -#: ../src/tooltips.py:719 -msgid "This service has not yet responded with detailed information" -msgstr "Dieser Dienst hat nicht mit detaillierten Informationen geantwortet" - -#: ../src/tooltips.py:722 -msgid "" -"This service could not respond with detailed information.\n" -"It is most likely legacy or broken" -msgstr "" -"Dieser Dienst konnte nicht mit detaillierten Informationen antworten\n" -"Er ist wahrscheinlich verwaltet oder defekt" - -#: ../src/vcard.py:243 -msgid "?Client:Unknown" -msgstr "Unbekannt" - -#: ../src/vcard.py:245 -msgid "?OS:Unknown" -msgstr "Unbekannt" - -#: ../src/vcard.py:270 ../src/vcard.py:280 ../src/vcard.py:479 -#, python-format -msgid "since %s" -msgstr "seit %s" - -#: ../src/vcard.py:309 -#, fuzzy -msgid "Affiliation:" -msgstr "Anwendungen" - -#: ../src/vcard.py:317 -msgid "" -"This contact is interested in your presence information, but you are not " -"interested in his/her presence" -msgstr "" -"Dieser Kontakt ist an Ihren Anwesenheitsinformationen interessiert, aber Sie " -"sind nicht an seiner/ihrer Anwesenheit interessiert" - -#: ../src/vcard.py:319 -msgid "" -"You are interested in the contact's presence information, but he/she is not " -"interested in yours" -msgstr "" -"Sie sind an den Anwesenheitsinformationen des Kontakts interessiert, aber er/" -"sie ist nicht an ihren interessiert" - -#: ../src/vcard.py:321 -msgid "You and the contact are interested in each other's presence information" -msgstr "" -"Sie und der Kontakt sind an den Anwesenheitsinformationen des Anderen " -"interessiert" - -#. None -#: ../src/vcard.py:323 -msgid "" -"You are not interested in the contact's presence, and neither he/she is " -"interested in yours" -msgstr "" -"Sie sind nicht an der Anwesenheit des Kontakts interessiert, und er/sie ist " -"nicht interessiert an ihrer" - -#: ../src/vcard.py:330 -msgid "You are waiting contact's answer about your subscription request" -msgstr "Sie warten auf die Antwort des Kontaktes auf ihre Abonnement-Anfrage" - -#: ../src/vcard.py:332 -msgid "There is no pending subscription request." -msgstr "" - -#: ../src/vcard.py:337 ../src/vcard.py:374 ../src/vcard.py:504 -msgid " resource with priority " -msgstr " resource mit Priorität " +msgid "Private conversation with %s lost." +msgstr "Private Unterhaltung mit %s unterbrochen." #: ../src/common/check_paths.py:38 msgid "creating logs database" @@ -6886,6 +3174,14 @@ msgstr "Sie haben keinen Gruppenchat betreten." msgid "Choose the groupchats you want to leave" msgstr "Wählen Sie den Gruppenchat, den Sie verlassen möchten" +#. Make special context menu if group is Groupchats +#: ../src/common/commands.py:199 ../src/common/contacts.py:107 +#: ../src/common/helpers.py:66 ../src/roster_window.py:714 +#: ../src/roster_window.py:1421 ../src/roster_window.py:1423 +#: ../src/roster_window.py:2738 ../src/roster_window.py:4725 +msgid "Groupchats" +msgstr "Gruppenchat" + #: ../src/common/commands.py:235 msgid "You left the following groupchats:" msgstr "Sie haben folgende Gruppenchats verlassen:" @@ -6938,6 +3234,13 @@ msgstr "" "Liste (leerzeichengeteilt) von Reihen (Kontos und Gruppen), die eingeklappt " "sind." +#. sorted alphanum +#: ../src/common/config.py:102 ../src/common/config.py:430 +#: ../src/common/optparser.py:212 ../src/common/optparser.py:430 +#: ../src/common/optparser.py:464 ../src/gajim.py:3237 +msgid "default" +msgstr "Standard" + #: ../src/common/config.py:105 msgid "Enable link-local/zeroconf messaging" msgstr "Aktiviere Anzeige von Kontakten im LAN über Zeroconf." @@ -7445,7 +3748,7 @@ msgstr "" "Maximale Anzahl Zeilen, die in einem Gespräch angezeigt werden. Die ältesten " "Zeilen werden gelöscht." -#: ../src/common/config.py:259 +#: ../src/common/config.py:251 msgid "" "If True, notification windows from notification-daemon will be attached to " "systray icon." @@ -7453,11 +3756,17 @@ msgstr "" "Wenn wahr, werden Benachrichtigungsfenster vom Benachrichtigungsdienst an " "das Systray-Icon angehängt." -#: ../src/common/config.py:260 +#: ../src/common/config.py:252 msgid "Choose interval between 2 checks of idleness." msgstr "" -#: ../src/common/config.py:271 +#: ../src/common/config.py:253 +msgid "" +"Change the value to change the size of latex formulas displayed. The higher " +"is larger." +msgstr "" + +#: ../src/common/config.py:264 msgid "" "Priority will change automatically according to your status. Priorities are " "defined in autopriority_* options." @@ -7465,20 +3774,20 @@ msgstr "" "Die Priorität wird automatisch dem Status entsprechend geändert. Prioritäten " "sind in den autopriority_* Optionen definiert." -#: ../src/common/config.py:279 +#: ../src/common/config.py:272 #, fuzzy msgid "" "Status used to autoconnect as. Can be online, chat, away, xa, dnd, invisible." msgstr "Eins von: offline, online, chat, away, xa, dnd, invisible " -#: ../src/common/config.py:284 +#: ../src/common/config.py:277 msgid "" "If disabled, don't sign presences with GPG key, even if GPG is configured." msgstr "" "Wenn deaktiviert, wird die Anwesenheit nicht mit einem GPG-Schlüssel " "signiert, wenn GPG konfiguriert ist." -#: ../src/common/config.py:286 +#: ../src/common/config.py:279 msgid "" "Ordered list (space separated) of connection type to try. Can contain tls, " "ssl or plain" @@ -7486,29 +3795,29 @@ msgstr "" "Geordnete Liste (mit Leerzeichen getrennt) zu versuchender Verbindungstypen. " "Kann tls, ssl oder plain enthalten" -#: ../src/common/config.py:287 +#: ../src/common/config.py:280 msgid "" "Show a warning dialog before sending password on an insecure connection." msgstr "" "Zeige einen Warndialog bevor das Passwort über eine unsichere Verbindung " "geschickt wird." -#: ../src/common/config.py:289 +#: ../src/common/config.py:282 msgid "Space separated list of ssl errors to ignore." msgstr "Leerzeichen getrennte Liste von SSL-Fehler, die zu ignorieren sind." -#: ../src/common/config.py:301 +#: ../src/common/config.py:294 msgid "" "How many seconds to wait for the answer of keepalive packet before we try to " "reconnect." msgstr "" #. yes, no, ask -#: ../src/common/config.py:305 +#: ../src/common/config.py:298 msgid "Jabberd2 workaround" msgstr "Jabberd2 Workaround" -#: ../src/common/config.py:309 +#: ../src/common/config.py:302 msgid "" "If checked, Gajim will use your IP and proxies defined in " "file_transfer_proxies option for file transfer." @@ -7516,101 +3825,105 @@ msgstr "" "Wenn aktiviert, wird Gajim Ihre IP und die in der file_transfer_proxies " "Option definierten Proxies für den Datentransfer verwenden." -#: ../src/common/config.py:370 +#: ../src/common/config.py:371 msgid "Is OpenPGP enabled for this contact?" msgstr "Ist OpenPGP für diesen Kontakt aktiviert?" -#: ../src/common/config.py:371 ../src/common/config.py:375 +#: ../src/common/config.py:372 ../src/common/config.py:376 msgid "Language for which we want to check misspelled words" msgstr "Sprache für, die nach falsch geschriebenen Wörtern gesucht werden soll" -#: ../src/common/config.py:381 +#: ../src/common/config.py:382 msgid "all or space separated status" msgstr "alle oder Leerzeichen-getrennter Status" -#: ../src/common/config.py:382 +#: ../src/common/config.py:383 msgid "'yes', 'no', or 'both'" msgstr "'ja', 'nein' oder 'beide'" -#: ../src/common/config.py:383 ../src/common/config.py:385 -#: ../src/common/config.py:386 ../src/common/config.py:389 -#: ../src/common/config.py:390 +#: ../src/common/config.py:384 ../src/common/config.py:386 +#: ../src/common/config.py:387 ../src/common/config.py:390 +#: ../src/common/config.py:391 msgid "'yes', 'no' or ''" msgstr "'ja, 'nein' oder ''" -#: ../src/common/config.py:396 +#: ../src/common/config.py:397 msgid "Sleeping" msgstr "Schlafen" -#: ../src/common/config.py:397 +#: ../src/common/config.py:398 msgid "Back soon" msgstr "Bin gleich wieder da" -#: ../src/common/config.py:397 +#: ../src/common/config.py:398 msgid "Back in some minutes." msgstr "Bin in ein paar Minuten zurück." -#: ../src/common/config.py:398 +#: ../src/common/config.py:399 msgid "Eating" msgstr "Essen" -#: ../src/common/config.py:398 +#: ../src/common/config.py:399 msgid "I'm eating, so leave me a message." msgstr "Ich esse gerade." -#: ../src/common/config.py:399 +#: ../src/common/config.py:400 msgid "Movie" msgstr "Film" -#: ../src/common/config.py:399 +#: ../src/common/config.py:400 msgid "I'm watching a movie." msgstr "Ich sehe mir einen Film an." -#: ../src/common/config.py:400 +#: ../src/common/config.py:401 msgid "Working" msgstr "Arbeiten" -#: ../src/common/config.py:400 +#: ../src/common/config.py:401 msgid "I'm working." msgstr "Ich arbeite." -#: ../src/common/config.py:401 +#: ../src/common/config.py:402 msgid "Phone" msgstr "Telefon" -#: ../src/common/config.py:401 +#: ../src/common/config.py:402 msgid "I'm on the phone." msgstr "Ich telefoniere." -#: ../src/common/config.py:402 +#: ../src/common/config.py:403 msgid "Out" msgstr "Draußen" -#: ../src/common/config.py:402 +#: ../src/common/config.py:403 msgid "I'm out enjoying life." msgstr "Ich bin draußen und genieße das Leben." -#: ../src/common/config.py:406 +#: ../src/common/config.py:407 msgid "I'm available." msgstr "Ich bin angemeldet." -#: ../src/common/config.py:407 +#: ../src/common/config.py:408 msgid "I'm free for chat." msgstr "Ich bin frei zum Chatten." -#: ../src/common/config.py:409 +#: ../src/common/config.py:409 ../src/config.py:1363 +msgid "Be right back." +msgstr "Bin gleich zurück." + +#: ../src/common/config.py:410 msgid "I'm not available." msgstr "Ich bin nicht verfügbar." -#: ../src/common/config.py:410 +#: ../src/common/config.py:411 msgid "Do not disturb." msgstr "Bitte nicht stören." -#: ../src/common/config.py:411 ../src/common/config.py:412 +#: ../src/common/config.py:412 ../src/common/config.py:413 msgid "Bye!" msgstr "Auf Wiedersehen!" -#: ../src/common/config.py:422 +#: ../src/common/config.py:423 msgid "" "Sound to play when a group chat message contains one of the words in " "muc_highlight_words, or when a group chat message contains your nickname." @@ -7618,88 +3931,103 @@ msgstr "" "Abzuspielender Ton, falls eine Gruppenchat-Nachricht eines der Wörter aus " "der muc_highlights_works-Liste oder Ihren Spitznamen enthält." -#: ../src/common/config.py:423 +#: ../src/common/config.py:424 msgid "Sound to play when any MUC message arrives." msgstr "" "Ton der beim empfangen einer Gruppenchat-Nachricht abgespielt werden soll" -#: ../src/common/config.py:432 ../src/common/optparser.py:226 +#: ../src/common/config.py:433 ../src/common/optparser.py:226 msgid "green" msgstr "grün" -#: ../src/common/config.py:436 ../src/common/optparser.py:212 +#: ../src/common/config.py:437 ../src/common/optparser.py:212 msgid "grocery" msgstr "gemüse" -#: ../src/common/config.py:440 +#: ../src/common/config.py:441 msgid "human" msgstr "menschlich" -#: ../src/common/config.py:444 +#: ../src/common/config.py:445 msgid "marine" msgstr "Marine" -#: ../src/common/connection_handlers.py:68 -#: ../src/common/zeroconf/connection_handlers_zeroconf.py:48 +#: ../src/common/connection_handlers.py:69 +#: ../src/common/zeroconf/connection_handlers_zeroconf.py:49 msgid "Unable to load idle module" msgstr "Konnte Idle-Modus nicht laden" -#: ../src/common/connection_handlers.py:226 -#: ../src/common/zeroconf/connection_handlers_zeroconf.py:242 +#: ../src/common/connection_handlers.py:227 +#: ../src/common/zeroconf/connection_handlers_zeroconf.py:93 msgid "Wrong host" msgstr "Falscher Host" -#: ../src/common/connection_handlers.py:227 +#: ../src/common/connection_handlers.py:228 msgid "Invalid local address? :-O" msgstr "Ungültige Lokale Adresse? :-O" -#: ../src/common/connection_handlers.py:628 +#: ../src/common/connection_handlers.py:629 #, python-format msgid "Registration information for transport %s has not arrived in time" msgstr "" "Registrierungsinformation für Transport %s sind nicht rechtzeitig angekommen" -#: ../src/common/connection_handlers.py:1866 +#: ../src/common/connection_handlers.py:906 +#: ../src/common/connection_handlers.py:1819 +#: ../src/common/connection_handlers.py:1869 +#: ../src/common/connection_handlers.py:2051 +#: ../src/common/connection_handlers.py:2163 ../src/common/connection.py:1155 +#: ../src/gajim.py:591 +msgid "Disk Write Error" +msgstr "Fehler beim Schreiben auf Festplatte" + +#: ../src/common/connection_handlers.py:1704 +#, python-format +msgid "" +"%s has ended his/her private conversation with you. You should do the same." +msgstr "" + +#: ../src/common/connection_handlers.py:1938 #, python-format msgid "Nickname not allowed: %s" msgstr "Spitzname nicht erlaubt: %s" #. we are banned #. group chat does not exist -#: ../src/common/connection_handlers.py:1936 -#: ../src/common/connection_handlers.py:1939 -#: ../src/common/connection_handlers.py:1942 -#: ../src/common/connection_handlers.py:1945 -#: ../src/common/connection_handlers.py:1949 -#: ../src/common/connection_handlers.py:1958 +#: ../src/common/connection_handlers.py:2008 +#: ../src/common/connection_handlers.py:2011 +#: ../src/common/connection_handlers.py:2014 +#: ../src/common/connection_handlers.py:2017 +#: ../src/common/connection_handlers.py:2021 +#: ../src/common/connection_handlers.py:2030 msgid "Unable to join group chat" msgstr "Fehler beim Betreten des Gruppenchats" -#: ../src/common/connection_handlers.py:1937 +#: ../src/common/connection_handlers.py:2009 #, python-format msgid "You are banned from group chat %s." msgstr "Sie sind von Gruppenchat %s gebannt." -#: ../src/common/connection_handlers.py:1940 +#: ../src/common/connection_handlers.py:2012 #, python-format msgid "Group chat %s does not exist." msgstr "Dieser Gruppenchat existiert nicht." -#: ../src/common/connection_handlers.py:1943 +#: ../src/common/connection_handlers.py:2015 msgid "Group chat creation is restricted." msgstr "Gruppenchaterstellung ist beschränkt." -#: ../src/common/connection_handlers.py:1946 +#: ../src/common/connection_handlers.py:2018 #, python-format msgid "Your registered nickname must be used in group chat %s." msgstr "Sie müssen Ihren registrierten Spitznamen verwenden" -#: ../src/common/connection_handlers.py:1950 +#: ../src/common/connection_handlers.py:2022 #, python-format msgid "You are not in the members list in groupchat %s." msgstr "Sie sind nicht in der Mitgliedliste" -#: ../src/common/connection_handlers.py:1959 +#: ../src/common/connection_handlers.py:2031 #, python-format msgid "" "Your desired nickname in group chat %s is in use or registered by another " @@ -7712,36 +4040,36 @@ msgstr "" #. Room has been destroyed. see #. http://www.xmpp.org/extensions/xep-0045.html#destroyroom -#: ../src/common/connection_handlers.py:1990 +#: ../src/common/connection_handlers.py:2062 msgid "Room has been destroyed" msgstr "Raum wurde zerstört" -#: ../src/common/connection_handlers.py:1997 +#: ../src/common/connection_handlers.py:2069 #, python-format msgid "You can join this room instead: %s" msgstr "Sie können stattdessen diesem Raum beitreten: %s" -#: ../src/common/connection_handlers.py:2024 +#: ../src/common/connection_handlers.py:2096 msgid "I would like to add you to my roster." msgstr "Ich würde dich gerne zu meiner Liste hinzufügen." #. BE CAREFUL: no con.updateRosterItem() in a callback -#: ../src/common/connection_handlers.py:2045 +#: ../src/common/connection_handlers.py:2117 #, python-format msgid "we are now subscribed to %s" msgstr "wir haben jetzt %s abonniert" -#: ../src/common/connection_handlers.py:2047 +#: ../src/common/connection_handlers.py:2119 #, python-format msgid "unsubscribe request from %s" msgstr "Anfrage zur Kündigung des Abonnements von %s" -#: ../src/common/connection_handlers.py:2049 +#: ../src/common/connection_handlers.py:2121 #, python-format msgid "we are now unsubscribed from %s" msgstr "%s hat das Abonnement beendet" -#: ../src/common/connection_handlers.py:2190 +#: ../src/common/connection_handlers.py:2262 #, python-format msgid "" "JID %s is not RFC compliant. It will not be added to your roster. Use roster " @@ -7752,11 +4080,19 @@ msgstr "" "um ihn zu entfernen" #. We didn't set a passphrase -#: ../src/common/connection_handlers.py:2215 +#: ../src/common/connection_handlers.py:2287 #: ../src/common/zeroconf/connection_zeroconf.py:172 msgid "OpenPGP passphrase was not given" msgstr "Keine OpenPGP-Passphrase gewählt" +#. %s is the account name here +#: ../src/common/connection_handlers.py:2289 +#: ../src/common/zeroconf/connection_zeroconf.py:174 +#: ../src/roster_window.py:1830 +#, python-format +msgid "You will be connected to %s without OpenPGP." +msgstr "Sie werden ohne OpenPGP mit %s verbunden." + #: ../src/common/connection.py:60 msgid "Unable to get issuer certificate" msgstr "Kann Zertifikat des Ausstellers nicht holen" @@ -7885,59 +4221,64 @@ msgstr "" msgid "Application verification failure" msgstr "" -#: ../src/common/connection.py:256 +#: ../src/common/connection.py:254 #: ../src/common/zeroconf/connection_zeroconf.py:214 #, python-format msgid "Connection with account \"%s\" has been lost" msgstr "Verbindung mit Konto \"%s\" abgebrochen" -#: ../src/common/connection.py:257 +#: ../src/common/connection.py:255 msgid "Reconnect manually." msgstr "Manuelle Neuverbindung." -#: ../src/common/connection.py:268 +#: ../src/common/connection.py:266 #, python-format msgid "Server %s answered wrongly to register request: %s" msgstr "Server %s beantwortete Registrierungsanfrage nicht korrekt: %s" -#: ../src/common/connection.py:302 +#: ../src/common/connection.py:300 #, python-format msgid "Server %s provided a different registration form" msgstr "Server %s bot ein anderes Registrierungsformular" -#: ../src/common/connection.py:318 +#: ../src/common/connection.py:316 #, python-format msgid "Unknown SSL error: %d" msgstr "Unbekannter SSL-Fehler: %d" #. wrong answer -#: ../src/common/connection.py:333 +#: ../src/common/connection.py:331 msgid "Invalid answer" msgstr "Ungültige Antwort" -#: ../src/common/connection.py:334 +#: ../src/common/connection.py:332 #, python-format msgid "Transport %s answered wrongly to register request: %s" msgstr "" "Transport %s beantwortete unsere Registrierungsanfrage nicht korrekt: %s" -#: ../src/common/connection.py:515 +#: ../src/common/connection.py:513 msgid "Connection to proxy failed" msgstr "Verbindung mit Proxy fehlgeschlagen" -#: ../src/common/connection.py:592 ../src/common/connection.py:672 +#: ../src/common/connection.py:590 ../src/common/connection.py:670 #: ../src/common/connection.py:1279 #: ../src/common/zeroconf/connection_zeroconf.py:248 #, python-format msgid "Could not connect to \"%s\"" msgstr "Konnte nicht mit %s verbinden" -#: ../src/common/connection.py:629 +#: ../src/common/connection.py:591 ../src/gajim.py:1116 +msgid "Check your connection or try again later." +msgstr "" +"Überprüfen Sie die Verbindung oder versuchen Sie es später noch einmal." + +#: ../src/common/connection.py:627 #, python-format msgid "The authenticity of the %s certificate could be invalid." msgstr "Die Authentizität des %s Zertifikats könnte ungültig sein." -#: ../src/common/connection.py:632 +#: ../src/common/connection.py:630 #, python-format msgid "" "\n" @@ -7946,7 +4287,7 @@ msgstr "" "\n" "SSL-Fehler: %s" -#: ../src/common/connection.py:634 +#: ../src/common/connection.py:632 #, python-format msgid "" "\n" @@ -7955,24 +4296,24 @@ msgstr "" "\n" "Unbekannter SSL-Fehler: %d" -#: ../src/common/connection.py:673 +#: ../src/common/connection.py:671 msgid "Check your connection or try again later" msgstr "Überprüfen Sie die Verbindung oder versuchen Sie es später noch einmal" -#: ../src/common/connection.py:698 +#: ../src/common/connection.py:696 #, python-format msgid "Authentication failed with \"%s\"" msgstr "Authentifizierung mit \"%s\" fehlgeschlagen" -#: ../src/common/connection.py:700 +#: ../src/common/connection.py:698 msgid "Please check your login and password for correctness." msgstr "Bitte überprüfen Sie ihren Benutzernamen und Passwort." -#: ../src/common/connection.py:761 +#: ../src/common/connection.py:759 msgid "Error while removing privacy list" msgstr "Fehler beim Entfernen der Privatliste" -#: ../src/common/connection.py:762 +#: ../src/common/connection.py:760 #, python-format msgid "" "Privacy list %s has not been removed. It is maybe active in one of your " @@ -8000,7 +4341,6 @@ msgstr "" #. we're not english #. one in locale and one en #: ../src/common/connection.py:1069 -#, fuzzy msgid "[This message is *encrypted* (See :XEP:`27`]" msgstr "[Diese Nachricht ist *verschlüsselt* (Siehe: JEP:`27`)]" @@ -8018,10 +4358,25 @@ msgstr "" msgid "Not fetched because of invisible status" msgstr "Nicht abgeholt aufgrund eines Unsichtbar-Status" -#: ../src/common/contacts.py:307 +#: ../src/common/contacts.py:304 ../src/common/contacts.py:319 +#: ../src/common/helpers.py:66 ../src/disco.py:111 ../src/disco.py:112 +#: ../src/disco.py:1320 ../src/gajim.py:913 ../src/roster_window.py:738 +#: ../src/roster_window.py:1367 ../src/roster_window.py:1413 +#: ../src/roster_window.py:1415 ../src/roster_window.py:1559 +msgid "Transports" +msgstr "Transports" + +#: ../src/common/contacts.py:308 msgid "Not in roster" msgstr "Nicht in der Kontaktliste" +#: ../src/common/contacts.py:321 ../src/common/helpers.py:66 +#: ../src/roster_window.py:358 ../src/roster_window.py:635 +#: ../src/roster_window.py:691 ../src/roster_window.py:956 +#: ../src/roster_window.py:1205 ../src/roster_window.py:5011 +msgid "Observers" +msgstr "Beobachter" + #. only say that to non Windows users #: ../src/common/dbus_support.py:41 msgid "D-Bus python bindings are missing in this computer" @@ -8290,6 +4645,10 @@ msgstr "Frei zum Chatten" msgid "_Available" msgstr "_Angemeldet" +#: ../src/common/helpers.py:261 ../src/features_window.py:120 +msgid "Available" +msgstr "Angemeldet" + #: ../src/common/helpers.py:263 msgid "Connecting" msgstr "Verbinde" @@ -8511,12 +4870,10 @@ msgid "impressed" msgstr "Beeindruckt" #: ../src/common/helpers.py:360 -#, fuzzy msgid "in awe" -msgstr "Ehrfürchtig" +msgstr "Bewundernd" #: ../src/common/helpers.py:360 -#, fuzzy msgid "in love" msgstr "Verliebt" @@ -8650,7 +5007,6 @@ msgid "talking" msgstr "Im Gespräch" #: ../src/common/helpers.py:382 -#, fuzzy msgid "doing chores" msgstr "Mache den Haushalt" @@ -8663,7 +5019,6 @@ msgid "traveling" msgstr "Auf Reisen" #: ../src/common/helpers.py:383 -#, fuzzy msgid "having an appointment" msgstr "Habe einen Termin" @@ -8677,7 +5032,6 @@ msgstr "In der Kneipe" #. Subactivites #: ../src/common/helpers.py:386 -#, fuzzy msgid "on the phone" msgstr "Am Telefon" @@ -8690,7 +5044,6 @@ msgid "hiking" msgstr "Wandern" #: ../src/common/helpers.py:387 -#, fuzzy msgid "on vacation" msgstr "Im Urlaub" @@ -8711,12 +5064,10 @@ msgid "sleeping" msgstr "Schlafen" #: ../src/common/helpers.py:389 -#, fuzzy msgid "brushing teeth" msgstr "Zähne putzen" #: ../src/common/helpers.py:389 -#, fuzzy msgid "playing sports" msgstr "Beim Sport" @@ -8725,22 +5076,18 @@ msgid "skiing" msgstr "Ski fahren" #: ../src/common/helpers.py:390 -#, fuzzy msgid "having breakfast" msgstr "Frühstücken" #: ../src/common/helpers.py:391 -#, fuzzy msgid "watching TV" msgstr "Fernsehen" #: ../src/common/helpers.py:391 -#, fuzzy msgid "doing the dishes" -msgstr "Geschirr spülen" +msgstr "Geschirr abspülen" #: ../src/common/helpers.py:392 -#, fuzzy msgid "at the spa" msgstr "Im Bad" @@ -8749,7 +5096,6 @@ msgid "cycling" msgstr "Rad fahren" #: ../src/common/helpers.py:393 -#, fuzzy msgid "hanging out" msgstr "Rumhängen" @@ -8766,7 +5112,6 @@ msgid "cooking" msgstr "Kochen" #: ../src/common/helpers.py:394 -#, fuzzy msgid "walking the dog" msgstr "Hund ausführen" @@ -8775,27 +5120,22 @@ msgid "writing" msgstr "Schreiben" #: ../src/common/helpers.py:395 -#, fuzzy msgid "on a trip" msgstr "Auf Reisen" #: ../src/common/helpers.py:395 -#, fuzzy msgid "day off" -msgstr "Ruhetag" +msgstr "Freier Tag" #: ../src/common/helpers.py:396 -#, fuzzy msgid "having tea" msgstr "Tee" #: ../src/common/helpers.py:396 -#, fuzzy msgid "on a bus" msgstr "Im Bus" #: ../src/common/helpers.py:397 -#, fuzzy msgid "having a beer" msgstr "Bier trinken" @@ -8804,16 +5144,14 @@ msgid "reading" msgstr "Lesen" #: ../src/common/helpers.py:398 -#, fuzzy msgid "buying groceries" -msgstr "Einkaufen" +msgstr "Lebensmittel einkaufen" #: ../src/common/helpers.py:398 msgid "shaving" msgstr "Beim Rasieren" #: ../src/common/helpers.py:399 -#, fuzzy msgid "getting a haircut" msgstr "Beim Frisör" @@ -8822,32 +5160,26 @@ msgid "gaming" msgstr "Spielen" #: ../src/common/helpers.py:400 -#, fuzzy msgid "having dinner" msgstr "Abendessen" #: ../src/common/helpers.py:400 -#, fuzzy msgid "doing maintenance" msgstr "Wartungsarbeiten" #: ../src/common/helpers.py:401 -#, fuzzy msgid "doing the laundry" msgstr "Wäsche waschen" #: ../src/common/helpers.py:401 -#, fuzzy msgid "on video phone" -msgstr "Ich telefoniere." +msgstr "Bei einem Videotelefonat." #: ../src/common/helpers.py:402 -#, fuzzy msgid "scheduled holiday" -msgstr "Regulärer Feiertag" +msgstr "Urlaub" #: ../src/common/helpers.py:402 -#, fuzzy msgid "going out" msgstr "Ausgehen" @@ -8856,28 +5188,24 @@ msgid "partying" msgstr "Feiern" #: ../src/common/helpers.py:403 -#, fuzzy msgid "having a snack" msgstr "Beim Imbiss" #: ../src/common/helpers.py:404 -#, fuzzy msgid "having lunch" msgstr "Mittagessen" #: ../src/common/helpers.py:404 -#, fuzzy msgid "working out" -msgstr "Berechnen" +msgstr "Ausarbeiten" #: ../src/common/helpers.py:405 msgid "cleaning" msgstr "Putzen" #: ../src/common/helpers.py:405 -#, fuzzy msgid "watching a movie" -msgstr "Film anschauen" +msgstr "Film schauen" #: ../src/common/helpers.py:406 msgid "sunbathing" @@ -8888,22 +5216,18 @@ msgid "socializing" msgstr "Kontakte knüpfen" #: ../src/common/helpers.py:407 -#, fuzzy msgid "running an errand" msgstr "Besorgungen machen" #: ../src/common/helpers.py:407 -#, fuzzy msgid "taking a bath" msgstr "Ein Bad nehmen" #: ../src/common/helpers.py:408 -#, fuzzy msgid "in real life" msgstr "Im wahren Leben" #: ../src/common/helpers.py:408 -#, fuzzy msgid "on a plane" msgstr "Im Flugzeug" @@ -8912,7 +5236,6 @@ msgid "shopping" msgstr "Beim Einkaufen" #: ../src/common/helpers.py:409 -#, fuzzy msgid "on a train" msgstr "Im Zug" @@ -8921,21 +5244,18 @@ msgid "running" msgstr "Laufen" #: ../src/common/helpers.py:410 -#, fuzzy msgid "taking a shower" -msgstr "Duschen" +msgstr "Unter der Dusche" #: ../src/common/helpers.py:410 msgid "jogging" msgstr "Beim Joggen" #: ../src/common/helpers.py:411 -#, fuzzy msgid "in a meeting" msgstr "In einer Besprechung" #: ../src/common/helpers.py:411 -#, fuzzy msgid "in a car" msgstr "Im Auto" @@ -8948,7 +5268,6 @@ msgid "swimming" msgstr "Schwimmen" #: ../src/common/helpers.py:413 -#, fuzzy msgid "having coffee" msgstr "Kaffee trinken" @@ -9033,7 +5352,7 @@ msgstr "" "Verbindung zum Host konnte nicht hergestellt werden: Zeitüberschreitung beim " "Senden von Daten." -#: ../src/common/zeroconf/connection_handlers_zeroconf.py:242 +#: ../src/common/zeroconf/connection_handlers_zeroconf.py:93 #, python-format msgid "" "The host %s you configured as the ft_add_hosts_to_send advanced option is " @@ -9062,9 +5381,8 @@ msgstr "" "Lokaler Nachrichtenversand funktioniert eventuell nicht richtig." #: ../src/common/zeroconf/connection_zeroconf.py:249 -#, fuzzy msgid "Please check if Avahi or Bonjour is installed." -msgstr "Bitte überprüfen Sie, ob Avahi installiert ist." +msgstr "Bitte überprüfen Sie, ob Avahi oder Bonjour installiert ist." #: ../src/common/zeroconf/connection_zeroconf.py:258 #: ../src/common/zeroconf/connection_zeroconf.py:262 @@ -9114,6 +5432,3769 @@ msgstr "Kontakt ist offline. Ihre Nachricht konnte nicht versendet werden." msgid "Error while adding service. %s" msgstr "Fehler beim Hinzufügen des Dienstes. %s" +#: ../src/config.py:124 ../src/config.py:546 +msgid "Disabled" +msgstr "Deaktiviert" + +#: ../src/config.py:303 +msgid "Active" +msgstr "Aktiv" + +#: ../src/config.py:311 +msgid "Event" +msgstr "Ereignis" + +#: ../src/config.py:387 +#, fuzzy +msgid "Default Message" +msgstr "Vorgegebene Status-Nachrichten" + +#: ../src/config.py:394 +msgid "Enabled" +msgstr "Aktiviert" + +#: ../src/config.py:436 +msgid "Always use OS/X default applications" +msgstr "" + +#: ../src/config.py:437 +msgid "Custom" +msgstr "Benutzerdefiniert" + +#: ../src/config.py:615 ../src/dialogs.py:1177 +#, python-format +msgid "Dictionary for lang %s not available" +msgstr "Wörterburch für Sprache %s nicht verfügbar" + +#: ../src/config.py:616 +#, python-format +msgid "" +"You have to install %s dictionary to use spellchecking, or choose another " +"language by setting the speller_language option." +msgstr "" +"Sie müssen das Wörterbuch %s installieren oder eine andere Sprache wählen, " +"um die Rechtschreibprüfung zu nutzen." + +#: ../src/config.py:981 +msgid "status message title" +msgstr "Statusbetreff" + +#: ../src/config.py:981 +msgid "status message text" +msgstr "Statusnachricht" + +#: ../src/config.py:1018 +msgid "First Message Received" +msgstr "Erste empfangene Nachricht" + +#: ../src/config.py:1019 +msgid "Next Message Received Focused" +msgstr "Nächste empfangene Nachricht fixiert" + +#: ../src/config.py:1021 +msgid "Next Message Received Unfocused" +msgstr "Nächste empfangene Nachricht nicht fixiert" + +#: ../src/config.py:1022 +msgid "Contact Connected" +msgstr "Kontakt verbunden" + +#: ../src/config.py:1023 +msgid "Contact Disconnected" +msgstr "Kontakt nicht verbunden" + +#: ../src/config.py:1024 +msgid "Message Sent" +msgstr "Nachricht gesendet" + +#: ../src/config.py:1025 +msgid "Group Chat Message Highlight" +msgstr "Gruppenchat Nachrichten-Hervorhebung" + +#: ../src/config.py:1026 +msgid "Group Chat Message Received" +msgstr "Gruppenchat-Nachricht empfangen" + +#: ../src/config.py:1027 +msgid "GMail Email Received" +msgstr "E-Mail über Googlemail empfangen" + +#. Name column +#: ../src/config.py:1284 ../src/dialogs.py:1790 ../src/dialogs.py:1854 +#: ../src/disco.py:742 ../src/disco.py:1534 ../src/disco.py:1780 +#: ../src/history_window.py:90 +msgid "Name" +msgstr "Name" + +#: ../src/config.py:1367 +msgid "Relogin now?" +msgstr "Jetzt neu einloggen?" + +#: ../src/config.py:1368 +msgid "If you want all the changes to apply instantly, you must relogin." +msgstr "" +"Wenn die Änderungen sofort übernommen werden sollen, müssen Sie sich neu " +"einloggen." + +#: ../src/config.py:1497 ../src/config.py:1596 +msgid "OpenPGP is not usable in this computer" +msgstr "OpenPGP kann auf diesem Computer nicht genutzt werden" + +#: ../src/config.py:1632 ../src/config.py:1673 +msgid "Unread events" +msgstr "Ungelesene Ereignisse" + +#: ../src/config.py:1633 +msgid "Read all pending events before removing this account." +msgstr "Alle ungelesenen Ereignisse lesen, bevor der Account entfernt wird." + +#: ../src/config.py:1659 +#, python-format +msgid "You have opened chat in account %s" +msgstr "Sie haben mit Account %s einen Chat geöffnet" + +#: ../src/config.py:1660 +msgid "All chat and groupchat windows will be closed. Do you want to continue?" +msgstr "Alle Chatfenster werden geschlossen. Fortfahren?" + +#: ../src/config.py:1669 +msgid "You are currently connected to the server" +msgstr "Sie sind mit dem Server verbunden" + +#: ../src/config.py:1670 +msgid "To change the account name, you must be disconnected." +msgstr "Verbindung muss beendet werden, um Kontonamen zu ändern." + +#: ../src/config.py:1674 +msgid "To change the account name, you must read all pending events." +msgstr "Um Kontonamen zu ändern, müssen Sie alle neunen Ereignisse lesen." + +#: ../src/config.py:1680 +msgid "Account Name Already Used" +msgstr "Kontoname wird bereits verwendet" + +#: ../src/config.py:1681 +msgid "" +"This name is already used by another of your accounts. Please choose another " +"name." +msgstr "" +"Dieser Name wird bereits für einen anderen Ihrer Accounts verwendet. Bitte " +"wählenSie einen anderen Namen." + +#: ../src/config.py:1685 ../src/config.py:1689 +msgid "Invalid account name" +msgstr "Ungültiger Kontoname" + +#: ../src/config.py:1686 +msgid "Account name cannot be empty." +msgstr "Kontoname darf nicht leer sein." + +#: ../src/config.py:1690 +msgid "Account name cannot contain spaces." +msgstr "Kontoname darf keine Leerzeichen enthalten." + +#: ../src/config.py:1761 +msgid "Rename Account" +msgstr "Konto umbenennen" + +#: ../src/config.py:1762 +#, python-format +msgid "Enter a new name for account %s" +msgstr "Geben Sie einen neuen Namen für das Konto %s ein" + +#: ../src/config.py:1780 ../src/config.py:1788 ../src/config.py:1830 +#: ../src/config.py:3053 ../src/dataforms_widget.py:535 +msgid "Invalid Jabber ID" +msgstr "Ungültige Jabber ID" + +#: ../src/config.py:1789 +msgid "A Jabber ID must be in the form \"user@servername\"." +msgstr "Jabber ID muss in der Form \"user@servername\" sein." + +#: ../src/config.py:1977 ../src/config.py:3125 +msgid "Invalid entry" +msgstr "Ungültiger Eintrag" + +#: ../src/config.py:1978 ../src/config.py:3126 +msgid "Custom port must be a port number." +msgstr "Proxy Port muss eine Portnummer sein." + +#: ../src/config.py:1999 ../src/config.py:3606 +msgid "Failed to get secret keys" +msgstr "Holen der geheimen Schlüssel fehlgeschlagen" + +#: ../src/config.py:2000 ../src/config.py:3607 +msgid "There was a problem retrieving your OpenPGP secret keys." +msgstr "Es gab ein Problem beim Holen ihres geheimen OpenPGP-Schlüssels." + +#: ../src/config.py:2003 ../src/config.py:3610 +msgid "OpenPGP Key Selection" +msgstr "OpenPGP Schlüssel-Auswahl" + +#: ../src/config.py:2004 ../src/config.py:3611 +msgid "Choose your OpenPGP key" +msgstr "Wählen Sie Ihren OpenPGP Schlüssel" + +#: ../src/config.py:2044 +msgid "No such account available" +msgstr "Account nicht verfügbar" + +#: ../src/config.py:2045 +msgid "You must create your account before editing your personal information." +msgstr "" +"Sie müssen ein Konto erstellen, bevor Sie die persönlichen Informationen " +"ändern können" + +#: ../src/config.py:2052 ../src/dialogs.py:1642 ../src/dialogs.py:1778 +#: ../src/dialogs.py:1958 ../src/disco.py:426 ../src/profile_window.py:318 +msgid "You are not connected to the server" +msgstr "Sie sind nicht mit dem Server verbunden" + +#: ../src/config.py:2053 +msgid "Without a connection, you can not edit your personal information." +msgstr "" +"Sie müssen angemeldet sein, um Ihre persönlichen Informationen zu bearbeiten" + +#: ../src/config.py:2057 +msgid "Your server doesn't support Vcard" +msgstr "Ihr Server unterstützt vCard nicht" + +#: ../src/config.py:2058 +msgid "Your server can't save your personal information." +msgstr "Ihr Server kann keine persönlichen Informationen speichern." + +#: ../src/config.py:2089 +msgid "Account Local already exists." +msgstr "Ein Konto mit dem Namen 'Local' ist bereits vorhanden" + +#: ../src/config.py:2090 +msgid "Please rename or remove it before enabling link-local messaging." +msgstr "" +"Bitte benennen Sie es um oder entfernen es, bevor Sie LAN-Kontakte " +"aktivieren." + +#: ../src/config.py:2273 +#, python-format +msgid "Edit %s" +msgstr "%s ändern" + +#: ../src/config.py:2275 +#, python-format +msgid "Register to %s" +msgstr "Auf %s registrieren" + +#. list at the beginning +#: ../src/config.py:2311 +msgid "Ban List" +msgstr "Sperrliste" + +#: ../src/config.py:2312 +msgid "Member List" +msgstr "Mitgliederliste" + +#: ../src/config.py:2313 +msgid "Owner List" +msgstr "Besitzerliste" + +#: ../src/config.py:2314 +msgid "Administrator List" +msgstr "Administratorliste" + +#. Address column +#. holds JID (who said this) +#: ../src/config.py:2363 ../src/disco.py:749 ../src/history_manager.py:178 +msgid "JID" +msgstr "JID" + +#: ../src/config.py:2371 +msgid "Reason" +msgstr "Grund" + +#: ../src/config.py:2376 +msgid "Nick" +msgstr "Spitzname" + +#: ../src/config.py:2380 +msgid "Role" +msgstr "Rolle" + +#: ../src/config.py:2405 +msgid "Banning..." +msgstr "Verbannen ..." + +#. You can move '\n' before user@domain if that line is TOO BIG +#: ../src/config.py:2407 +msgid "" +"Whom do you want to ban?\n" +"\n" +msgstr "" +"Wen möchten Sie verbannen?\n" +"\n" + +#: ../src/config.py:2409 +msgid "Adding Member..." +msgstr "Mitglied hinzufügen ..." + +#: ../src/config.py:2410 +msgid "" +"Whom do you want to make a member?\n" +"\n" +msgstr "" +"Wen möchten Sie zum Mitglied machen?\n" +"\n" + +#: ../src/config.py:2412 +msgid "Adding Owner..." +msgstr "Besitzer hinzufügen ..." + +#: ../src/config.py:2413 +msgid "" +"Whom do you want to make an owner?\n" +"\n" +msgstr "" +"Wen möchten Sie zum Besitzer machen?\n" +"\n" + +#: ../src/config.py:2415 +msgid "Adding Administrator..." +msgstr "Füge Administrator hinzu ..." + +#: ../src/config.py:2416 +msgid "" +"Whom do you want to make an administrator?\n" +"\n" +msgstr "" +"Wen möchten Sie zum Administrator machen?\n" +"\n" + +#: ../src/config.py:2417 +msgid "" +"Can be one of the following:\n" +"1. user@domain/resource (only that resource matches).\n" +"2. user@domain (any resource matches).\n" +"3. domain/resource (only that resource matches).\n" +"4. domain (the domain itself matches, as does any user@domain,\n" +"domain/resource, or address containing a subdomain." +msgstr "" +"Kann eines der folgenden sein:\"1. benutzer@domain/resource (nur die " +"Resource trifft zu).\n" +"2. benutzer@domain (jede Resource trifft zu).\n" +"3 domain (die Domain selbst trifft zu, als auch jeder benutzer@domain,\n" +"jede domain/resource oder Adresse, die eine Subdomain enthält." + +#: ../src/config.py:2523 +#, python-format +msgid "Removing %s account" +msgstr "Entferne Konto %s" + +#: ../src/config.py:2538 ../src/gajim.py:1484 ../src/roster_window.py:1811 +msgid "Password Required" +msgstr "Passwort benötigt" + +#: ../src/config.py:2539 ../src/roster_window.py:1805 +#, python-format +msgid "Enter your password for account %s" +msgstr "Geben Sie ihr Passwort für %s ein" + +#: ../src/config.py:2540 ../src/roster_window.py:1812 +msgid "Save password" +msgstr "Passwort speichern" + +#: ../src/config.py:2553 +#, python-format +msgid "Account \"%s\" is connected to the server" +msgstr "Konto \"%s\" ist mit Server verbunden" + +#: ../src/config.py:2554 +msgid "If you remove it, the connection will be lost." +msgstr "Wenn Sie es entfernen, wird die Verbindung beendet." + +#: ../src/config.py:2650 +msgid "Default" +msgstr "Standard" + +#: ../src/config.py:2650 +msgid "?print_status:All" +msgstr "?print_status:Alle" + +#: ../src/config.py:2651 +msgid "Enter and leave only" +msgstr "Nur betreten und verlassen" + +#: ../src/config.py:2652 +msgid "?print_status:None" +msgstr "?print_status:Nichts" + +#: ../src/config.py:2721 +msgid "New Group Chat" +msgstr "Neuer Gruppenchat" + +#: ../src/config.py:2754 +msgid "This bookmark has invalid data" +msgstr "Dieses Lesezeichen hat ungültige Daten" + +#: ../src/config.py:2755 +msgid "" +"Please be sure to fill out server and room fields or remove this bookmark." +msgstr "Bitte Serverfeld und Raumfeld ausfüllen oder Lesezeichen löschen." + +#: ../src/config.py:3036 +msgid "Invalid username" +msgstr "Ungültiger Benutzername" + +#: ../src/config.py:3038 +msgid "You must provide a username to configure this account." +msgstr "" +"Sie müssen einen Benutzernamen angeben, um diesen Account zu konfigurieren." + +#: ../src/config.py:3064 +msgid "Duplicate Jabber ID" +msgstr "Doppelte Jabber ID" + +#: ../src/config.py:3065 +msgid "This account is already configured in Gajim." +msgstr "Dieser Kontakt wurde in Gajim bereits konfiguriert." + +#: ../src/config.py:3082 +msgid "Account has been added successfully" +msgstr "Account wurde erfolgreich hinzugefügt" + +#: ../src/config.py:3083 ../src/config.py:3270 +msgid "" +"You can set advanced account options by pressing the Advanced button, or " +"later by choosing the Accounts menuitem under the Edit menu from the main " +"window." +msgstr "" +"Sie können die erweiterten Kontooptionen durch Klicken des Erweitert-Buttons " +"oder durch Klicken des Konto-Menüpunktes im Bearbeiten-Menü des " +"Hauptfensters erreichen." + +#: ../src/config.py:3101 +msgid "Invalid server" +msgstr "Ungültiger Server" + +#: ../src/config.py:3102 +msgid "Please provide a server on which you want to register." +msgstr "Bitte geben Sie an, bei dem Sie sich registrieren möchten." + +#: ../src/config.py:3153 ../src/gajim.py:2190 +msgid "Certificate Already in File" +msgstr "Zertifikat schon in der Datei" + +#: ../src/config.py:3154 ../src/gajim.py:2191 +#, python-format +msgid "This certificate is already in file %s, so it's not added again." +msgstr "" + +#: ../src/config.py:3222 +#, python-format +msgid "" +"Security Warning\n" +"\n" +"The authenticity of the %s SSL certificate could be invalid.\n" +"SSL Error: %s\n" +"Do you still want to connect to this server?" +msgstr "" + +#: ../src/config.py:3228 ../src/gajim.py:2214 +#, python-format +msgid "" +"Add this certificate to the list of trusted certificates.\n" +"SHA1 fingerprint of the certificate:\n" +"%s" +msgstr "" +"Dieses Zertifikat der Liste vertrauenswürdiger Zertifikate hinzufügen.\n" +"SHA1-Fingerprint dieses Zertifikates:\n" +"%s" + +#: ../src/config.py:3249 ../src/config.py:3288 +msgid "An error occurred during account creation" +msgstr "Während der Konto-Erstellung ist ein Fehler aufgetreten" + +#: ../src/config.py:3269 +msgid "Your new account has been created successfully" +msgstr "Ihr neues Konto wurde erfolgreich erstellt" + +#: ../src/config.py:3372 +msgid "Account name is in use" +msgstr "Kontoname ist schon vergeben" + +#: ../src/config.py:3373 +msgid "You already have an account using this name." +msgstr "Sie haben bereits ein Konto mit diesem Namen." + +#: ../src/conversation_textview.py:468 +msgid "" +"Text below this line is what has been said since the last time you paid " +"attention to this group chat" +msgstr "" +"Text unterhalb dieser Linie stellt dar, was seit ihrem letzten Besuch in " +"diesem Gruppenchat gesagt wurde" + +#: ../src/conversation_textview.py:567 +#, python-format +msgid "_Actions for \"%s\"" +msgstr "_Aktionen für \"%s\"" + +#: ../src/conversation_textview.py:579 +msgid "Read _Wikipedia Article" +msgstr "_Wikipedia-Artikel lesen" + +#: ../src/conversation_textview.py:584 +msgid "Look it up in _Dictionary" +msgstr "Im Wörterbuch _suchen" + +#: ../src/conversation_textview.py:601 +#, python-format +msgid "Dictionary URL is missing an \"%s\" and it is not WIKTIONARY" +msgstr "In Wörterbuch URL fehlt ein \"%s\" und ist nicht Wiktionary" + +#. we must have %s in the url +#: ../src/conversation_textview.py:614 +#, python-format +msgid "Web Search URL is missing an \"%s\"" +msgstr "In Websuche URL fehlt ein \"%s\"" + +#: ../src/conversation_textview.py:617 +msgid "Web _Search for it" +msgstr "Im _Internet suchen" + +#: ../src/conversation_textview.py:623 +msgid "Open as _Link" +msgstr "Als _Link öffnen" + +#: ../src/conversation_textview.py:1104 +msgid "Yesterday" +msgstr "Gestern" + +#. the number is >= 2 +#. %i is day in year (1-365), %d (1-31) we want %i +#: ../src/conversation_textview.py:1108 +#, python-format +msgid "%i days ago" +msgstr "Vor %i Tagen" + +#. if we have subject, show it too! +#: ../src/conversation_textview.py:1142 +#, python-format +msgid "Subject: %s\n" +msgstr "Thema: %s\n" + +#: ../src/dataforms_widget.py:539 +msgid "Jabber ID already in list" +msgstr "Jabber ID bereits in der Liste" + +#: ../src/dataforms_widget.py:540 +msgid "The Jabber ID you entered is already in the list. Choose another one." +msgstr "Die eingegebene Jabber ID ist bereits in der Liste. Wähle eine andere." + +#. Default jid +#: ../src/dataforms_widget.py:551 +msgid "new@jabber.id" +msgstr "neu@jabber.id" + +#: ../src/dataforms_widget.py:554 ../src/dataforms_widget.py:556 +#, python-format +msgid "new%d@jabber.id" +msgstr "neu%d@jabber.id" + +#: ../src/dialogs.py:71 +#, python-format +msgid "Contact name: %s" +msgstr "Kontaktname: %s" + +#: ../src/dialogs.py:73 +#, python-format +msgid "Jabber ID: %s" +msgstr "Jabber ID: %s " + +#: ../src/dialogs.py:183 +msgid "Group" +msgstr "Gruppe" + +#: ../src/dialogs.py:190 +msgid "In the group" +msgstr "In der Gruppe" + +#: ../src/dialogs.py:290 +msgid "KeyID" +msgstr "Schlüssel-ID" + +#: ../src/dialogs.py:295 +msgid "Contact name" +msgstr "Name des Kontakts" + +#: ../src/dialogs.py:461 +msgid "Set Mood" +msgstr "Stimmung setzen:" + +#: ../src/dialogs.py:513 +#, python-format +msgid "%s Status Message" +msgstr "%s Status-Nachricht" + +#: ../src/dialogs.py:515 +msgid "Status Message" +msgstr "Status-Nachricht" + +#: ../src/dialogs.py:615 +msgid "Save as Preset Status Message" +msgstr "Als derzeitige Statusnachricht speichern" + +#: ../src/dialogs.py:616 +msgid "Please type a name for this status message" +msgstr "Bitte geben Sie einen Namen für diese Statusnachricht ein:" + +#: ../src/dialogs.py:627 +msgid "Overwrite Status Message?" +msgstr "Status-Nachricht überschreiben" + +#: ../src/dialogs.py:628 +msgid "" +"This name is already used. Do you want to overwrite this status message?" +msgstr "" +"Dieser Name wird bereits verwendet. Möchten Sie die Status-Nachricht " +"überschreiben?" + +#: ../src/dialogs.py:644 +msgid "AIM Address:" +msgstr "AIM-Adresse" + +#: ../src/dialogs.py:645 +msgid "GG Number:" +msgstr "GG Nummer" + +#: ../src/dialogs.py:646 +msgid "ICQ Number:" +msgstr "ICQ-Nummer" + +#: ../src/dialogs.py:647 +msgid "MSN Address:" +msgstr "MSN-Adresse" + +#: ../src/dialogs.py:648 +msgid "Yahoo! Address:" +msgstr "Yahoo!-Adresse" + +#: ../src/dialogs.py:685 +#, python-format +msgid "Please fill in the data of the contact you want to add in account %s" +msgstr "" +"Bitte füllen Sie die Daten für den Kontakt aus, den Sie dem Konto %s " +"hinzufügen wollen" + +#: ../src/dialogs.py:687 +msgid "Please fill in the data of the contact you want to add" +msgstr "Bitte geben sie die Daten des neuen Kontakts ein" + +#: ../src/dialogs.py:844 ../src/dialogs.py:850 +msgid "Invalid User ID" +msgstr "Ungültige Benutzer ID" + +#: ../src/dialogs.py:851 +msgid "The user ID must not contain a resource." +msgstr "Die Benutzer-ID darf keine resource enthalten." + +#: ../src/dialogs.py:865 +msgid "Contact already in roster" +msgstr "Kontakt bereits im Roster" + +#: ../src/dialogs.py:866 +msgid "This contact is already listed in your roster." +msgstr "Der Kontakt befindet sich bereit in Ihrem Roster." + +#: ../src/dialogs.py:902 +msgid "User ID:" +msgstr "_Benutzer-ID:" + +#: ../src/dialogs.py:960 +msgid "A GTK+ jabber client" +msgstr "Ein GTK+ Jabber-Client" + +#: ../src/dialogs.py:961 +msgid "GTK+ Version:" +msgstr "GTK+-Version:" + +#: ../src/dialogs.py:962 +msgid "PyGTK Version:" +msgstr "PyGTK-Version" + +#: ../src/dialogs.py:972 +msgid "Current Developers:" +msgstr "Derzeitige Entwickler:" + +#: ../src/dialogs.py:974 +msgid "Past Developers:" +msgstr "Frühere Entwickler:" + +#: ../src/dialogs.py:980 +msgid "THANKS:" +msgstr "DANKE:" + +#. remove one english sentence +#. and add it manually as translatable +#: ../src/dialogs.py:986 +msgid "Last but not least, we would like to thank all the package maintainers." +msgstr "Zuletzt möchten wird gerne allen Paket-Verwaltern danken." + +#. here you write your name in the form Name FamilyName +#: ../src/dialogs.py:999 +msgid "translator-credits" +msgstr "" +"Fridtjof Busse\n" +"Benjamin Drung \n" +"Fabian Fingerle \n" +"Sebastian Schäfer \n" +"Nico Gulden" + +#: ../src/dialogs.py:1170 +#, python-format +msgid "Unable to bind to port %s." +msgstr "Konnte nicht an Port %s binden." + +#: ../src/dialogs.py:1171 +msgid "" +"Maybe you have another running instance of Gajim. File Transfer will be " +"cancelled." +msgstr "Möglicherweise läuft Gajim bereits. Dateitransfer wird abgebrochen." + +#: ../src/dialogs.py:1178 +#, python-format +msgid "" +"You have to install %s dictionary to use spellchecking, or choose another " +"language by setting the speller_language option.\n" +"\n" +"Highlighting misspelled words feature will not be used" +msgstr "" +"Sie müssen das Wörterbuch %s installieren oder eine andere Sprache wählen, " +"um die Rechtschreibprüfung zu nutzen.\n" +"\n" +"Das Hervorheben falsch geschriebener Worte wird nicht genutzt" + +#: ../src/dialogs.py:1571 +#, python-format +msgid "Subscription request for account %s from %s" +msgstr "Abonnement-Anforderung für Konto %s von %s" + +#: ../src/dialogs.py:1574 +#, python-format +msgid "Subscription request from %s" +msgstr "Abonnement-Anforderung von %s" + +#: ../src/dialogs.py:1635 ../src/gajim.py:2761 ../src/roster_window.py:1161 +#, python-format +msgid "You are already in group chat %s" +msgstr "Sie sind bereits im Gruppenchat %s" + +#: ../src/dialogs.py:1643 +msgid "You can not join a group chat unless you are connected." +msgstr "Sie können einem Gruppenchat erst beitreten, wenn Sie verbunden sind." + +#: ../src/dialogs.py:1662 +#, python-format +msgid "Join Group Chat with account %s" +msgstr "Betrete Gruppenchat mit Account %s" + +#: ../src/dialogs.py:1733 +msgid "Invalid Nickname" +msgstr "Ungültiger Benutzername" + +#: ../src/dialogs.py:1734 ../src/groupchat_control.py:1329 +#: ../src/groupchat_control.py:1601 +msgid "The nickname has not allowed characters." +msgstr "Die Jabber-ID für den Gruppenchat enthält nicht erlaubte Zeichen." + +#: ../src/dialogs.py:1738 ../src/dialogs.py:1744 +#: ../src/groupchat_control.py:1768 +msgid "Invalid group chat Jabber ID" +msgstr "Ungültige Jabber-ID für den Gruppenchat" + +#: ../src/dialogs.py:1739 ../src/dialogs.py:1745 +#: ../src/groupchat_control.py:1769 +msgid "The group chat Jabber ID has not allowed characters." +msgstr "Die Jabber-ID für den Gruppenchat enthält nicht erlaubte Zeichen." + +#: ../src/dialogs.py:1751 +msgid "This is not a group chat" +msgstr "Das ist kein Gruppenchat" + +#: ../src/dialogs.py:1752 +#, python-format +msgid "%s is not the name of a group chat." +msgstr "%s ist kein Name eines Gruppenchats." + +#: ../src/dialogs.py:1779 +msgid "Without a connection, you can not synchronise your contacts." +msgstr "Sie müssen verbunden sein, um Ihre Kontakte zu Synchronisieren." + +#: ../src/dialogs.py:1793 +msgid "Server" +msgstr "Server" + +#: ../src/dialogs.py:1826 +msgid "This account is not connected to the server" +msgstr "Konto \"%s\" ist nicht mit dem Server verbunden" + +#: ../src/dialogs.py:1827 +msgid "You cannot synchronize with an account unless it is connected." +msgstr "" +"Sie können nicht mit einem Konto Synchronisieren, solange es nicht verbunden " +"ist." + +#: ../src/dialogs.py:1851 +msgid "Synchronise" +msgstr "Synchronisieren" + +#: ../src/dialogs.py:1909 +#, python-format +msgid "Start Chat with account %s" +msgstr "Starte Chat mit Account %s" + +#: ../src/dialogs.py:1911 +msgid "Start Chat" +msgstr "Chat starten" + +#: ../src/dialogs.py:1912 +msgid "" +"Fill in the nickname or the Jabber ID of the contact you would like\n" +"to send a chat message to:" +msgstr "" +"Geben Sie die JID oder den Spitznamen des Kontaktes ein,\n" +"an den Sie eine Chat-Nachricht schicken wollen:" + +#. if offline or connecting +#: ../src/dialogs.py:1937 ../src/dialogs.py:2313 ../src/dialogs.py:2454 +msgid "Connection not available" +msgstr "Verbindung nicht verfügbar" + +#: ../src/dialogs.py:1938 ../src/dialogs.py:2314 ../src/dialogs.py:2455 +#, python-format +msgid "Please make sure you are connected with \"%s\"." +msgstr "Vergewissern Sie sich, dass Sie mit \"%s\" verbunden sind." + +#: ../src/dialogs.py:1947 ../src/dialogs.py:1950 +msgid "Invalid JID" +msgstr "Ungültige JID" + +#: ../src/dialogs.py:1950 +#, python-format +msgid "Unable to parse \"%s\"." +msgstr "Kann \"%s\" nicht parsen." + +#: ../src/dialogs.py:1959 +msgid "Without a connection, you can not change your password." +msgstr "Sie müssen verbunden sein, um Ihr Passwort zu ändern" + +#: ../src/dialogs.py:1977 +msgid "Invalid password" +msgstr "Ungültiges Passwort" + +#: ../src/dialogs.py:1978 +msgid "You must enter a password." +msgstr "Sie müssen ein Passwort eingeben." + +#: ../src/dialogs.py:1982 +msgid "Passwords do not match" +msgstr "Passwörter stimmen nicht überein" + +#: ../src/dialogs.py:1983 +msgid "The passwords typed in both fields must be identical." +msgstr "Die Passwörter in beiden Feldern müssen identisch sein." + +#. img to display +#. default value +#: ../src/dialogs.py:2027 ../src/notify.py:242 ../src/notify.py:456 +#: ../src/osx/growler.py:12 +msgid "Contact Signed In" +msgstr "Kontakt hat sich angemeldet" + +#: ../src/dialogs.py:2029 ../src/notify.py:250 ../src/notify.py:458 +#: ../src/osx/growler.py:12 +msgid "Contact Signed Out" +msgstr "Kontakt hat sich abgemeldet" + +#. chat message +#: ../src/dialogs.py:2031 ../src/notify.py:273 ../src/notify.py:460 +#: ../src/osx/growler.py:12 +msgid "New Message" +msgstr "Neue Nachricht" + +#. single message +#: ../src/dialogs.py:2031 ../src/notify.py:254 ../src/notify.py:460 +#: ../src/osx/growler.py:13 +msgid "New Single Message" +msgstr "Neue einzelne Nachricht" + +#. private message +#: ../src/dialogs.py:2032 ../src/notify.py:261 ../src/notify.py:461 +#: ../src/osx/growler.py:13 +msgid "New Private Message" +msgstr "Neue private Nachricht" + +#: ../src/dialogs.py:2032 ../src/gajim.py:1661 ../src/notify.py:469 +#: ../src/osx/growler.py:13 +msgid "New E-mail" +msgstr "Neue E-Mail" + +#: ../src/dialogs.py:2034 ../src/gajim.py:1726 ../src/notify.py:463 +#: ../src/osx/growler.py:14 +msgid "File Transfer Request" +msgstr "Dateitransfer Anfrage" + +#: ../src/dialogs.py:2036 ../src/gajim.py:1633 ../src/gajim.py:1693 +#: ../src/notify.py:465 ../src/osx/growler.py:14 +msgid "File Transfer Error" +msgstr "Dateitransfer-Fehler" + +#: ../src/dialogs.py:2038 ../src/gajim.py:1765 ../src/gajim.py:1787 +#: ../src/gajim.py:1804 ../src/notify.py:467 ../src/osx/growler.py:15 +msgid "File Transfer Completed" +msgstr "Dateitransfer beendet" + +#: ../src/dialogs.py:2039 ../src/gajim.py:1768 ../src/notify.py:467 +#: ../src/osx/growler.py:15 +msgid "File Transfer Stopped" +msgstr "Dateitransfer gestoppt" + +#: ../src/dialogs.py:2041 ../src/gajim.py:1505 ../src/notify.py:471 +#: ../src/osx/growler.py:16 +msgid "Groupchat Invitation" +msgstr "Gruppenchat-Einladung" + +#: ../src/dialogs.py:2043 ../src/notify.py:234 ../src/notify.py:473 +#: ../src/osx/growler.py:16 +msgid "Contact Changed Status" +msgstr "Kontakt hat Status verändert" + +#: ../src/dialogs.py:2232 +#, python-format +msgid "Single Message using account %s" +msgstr "Einzelne Nachricht mit Account %s" + +#: ../src/dialogs.py:2234 +#, python-format +msgid "Single Message in account %s" +msgstr "Einzelne Nachricht in Account %s" + +#: ../src/dialogs.py:2236 +msgid "Single Message" +msgstr "Einzelne Nachricht" + +#. prepare UI for Sending +#: ../src/dialogs.py:2239 +#, python-format +msgid "Send %s" +msgstr "Sende %s" + +#. prepare UI for Receiving +#: ../src/dialogs.py:2262 +#, python-format +msgid "Received %s" +msgstr "%s empfangen" + +#. prepare UI for Receiving +#: ../src/dialogs.py:2285 +#, python-format +msgid "Form %s" +msgstr "Von %s" + +#. we create a new blank window to send and we preset RE: and to jid +#: ../src/dialogs.py:2355 +#, python-format +msgid "RE: %s" +msgstr "RE: %s" + +#: ../src/dialogs.py:2356 +#, python-format +msgid "%s wrote:\n" +msgstr "%s schrieb:\n" + +#: ../src/dialogs.py:2400 +#, python-format +msgid "XML Console for %s" +msgstr "XML Konsole für %s" + +#: ../src/dialogs.py:2402 +msgid "XML Console" +msgstr "XML Konsole" + +#: ../src/dialogs.py:2525 +#, python-format +msgid "Privacy List %s" +msgstr "Privatliste %s" + +#: ../src/dialogs.py:2529 +#, python-format +msgid "Privacy List for %s" +msgstr "Privatsphären-Liste für %s" + +#: ../src/dialogs.py:2585 +#, python-format +msgid "Order: %s, action: %s, type: %s, value: %s" +msgstr "Sortierung: %s, Aktion: %s, Typ: %s, Wert: %s" + +#: ../src/dialogs.py:2588 +#, python-format +msgid "Order: %s, action: %s" +msgstr "Sortierung: %s, Aktion: %s" + +#: ../src/dialogs.py:2630 +msgid "Edit a rule" +msgstr "Eine Regel bearbeiten" + +#: ../src/dialogs.py:2717 +msgid "Add a rule" +msgstr "Eine Regel hinzufügen" + +#: ../src/dialogs.py:2813 +#, python-format +msgid "Privacy Lists for %s" +msgstr "Privatliste für %s" + +#: ../src/dialogs.py:2815 +msgid "Privacy Lists" +msgstr "Privatlisten" + +#: ../src/dialogs.py:2885 +msgid "Invalid List Name" +msgstr "Ungültiger Listenname" + +#: ../src/dialogs.py:2886 +msgid "You must enter a name to create a privacy list." +msgstr "" +"Sie müssen einen Namen eingeben um eine Privatsphären-Liste zu erstellen." + +#: ../src/dialogs.py:2923 +msgid "$Contact has invited you to join a discussion" +msgstr "$Contact hat Sie in den Gruppenchat %(room_jid)s eingeladen" + +#: ../src/dialogs.py:2925 +#, python-format +msgid "$Contact has invited you to group chat %(room_jid)s" +msgstr "$Contact hat Sie in den Gruppenchat %(room_jid)s eingeladen" + +#: ../src/dialogs.py:2938 +#, python-format +msgid "Comment: %s" +msgstr "Kommentar: %s" + +#: ../src/dialogs.py:3004 +msgid "Choose Sound" +msgstr "Sound wählen" + +#: ../src/dialogs.py:3014 ../src/dialogs.py:3065 +msgid "All files" +msgstr "Alle Dateien" + +#: ../src/dialogs.py:3019 +msgid "Wav Sounds" +msgstr "Wav Dateien" + +#: ../src/dialogs.py:3052 +msgid "Choose Image" +msgstr "Bild auswählen" + +#: ../src/dialogs.py:3070 +msgid "Images" +msgstr "Bilder" + +#: ../src/dialogs.py:3135 +#, python-format +msgid "When %s becomes:" +msgstr "Wenn %s wird:" + +#: ../src/dialogs.py:3137 +#, python-format +msgid "Adding Special Notification for %s" +msgstr "Füge speziellen Hinweis für %s hinzu" + +#. # means number +#: ../src/dialogs.py:3208 +msgid "#" +msgstr "Nr." + +#: ../src/dialogs.py:3214 +msgid "Condition" +msgstr "Bedingung" + +#: ../src/dialogs.py:3332 +msgid "when I am " +msgstr "wenn Ich bin " + +#: ../src/disco.py:110 +msgid "Others" +msgstr "Andere" + +#. conference is a category for listing mostly groupchats in service discovery +#: ../src/disco.py:114 +msgid "Conference" +msgstr "Konferenz" + +#: ../src/disco.py:427 +msgid "Without a connection, you can not browse available services" +msgstr "Sie müssen angemeldet sein um Dienste zu durchsuchen" + +#: ../src/disco.py:501 +#, python-format +msgid "Service Discovery using account %s" +msgstr "Dienste durchsuchen für Konto %s" + +#: ../src/disco.py:503 +msgid "Service Discovery" +msgstr "Dienste durchsuchen" + +#: ../src/disco.py:643 +msgid "The service could not be found" +msgstr "Der Dienst konnte nicht gefunden werden" + +#: ../src/disco.py:644 +msgid "" +"There is no service at the address you entered, or it is not responding. " +"Check the address and try again." +msgstr "" +"Es existiert kein Dienst an der Adresse, die sie angegeben haben oder er " +"antwortet nicht. Überprüfen Sie die Adresse und versuchen Sie es erneut." + +#: ../src/disco.py:648 ../src/disco.py:929 +msgid "The service is not browsable" +msgstr "Der Dienst ist nicht durchsuchbar" + +#: ../src/disco.py:649 +msgid "This type of service does not contain any items to browse." +msgstr "" +"Dieser Art von Dienst enthält keine Objekte, di edurchsucht werden können." + +#: ../src/disco.py:729 +#, python-format +msgid "Browsing %s using account %s" +msgstr "Durchsuche %s mit Konto %s" + +#: ../src/disco.py:768 +msgid "_Browse" +msgstr "_Durchsuche" + +#: ../src/disco.py:930 +msgid "This service does not contain any items to browse." +msgstr "Dieser Dienst enthält keine keine durchsuchbaren Objekte." + +#: ../src/disco.py:1151 +msgid "_Execute Command" +msgstr "_Befehl ausführen" + +#: ../src/disco.py:1161 ../src/disco.py:1325 +msgid "Re_gister" +msgstr "Re_gistrieren" + +#: ../src/disco.py:1362 +#, python-format +msgid "Scanning %d / %d.." +msgstr "Durchsuche %d / %d.." + +#. Users column +#: ../src/disco.py:1544 +msgid "Users" +msgstr "Benutzer" + +#. Description column +#: ../src/disco.py:1552 +msgid "Description" +msgstr "Beschreibung" + +#. Id column +#: ../src/disco.py:1560 +msgid "Id" +msgstr "Id" + +#: ../src/disco.py:1789 +msgid "Subscribed" +msgstr "Abonniert" + +#: ../src/disco.py:1797 +msgid "Node" +msgstr "Node" + +#: ../src/disco.py:1854 +msgid "New post" +msgstr "Neue Nachricht" + +#: ../src/disco.py:1860 +msgid "_Subscribe" +msgstr "_Abonnieren" + +#: ../src/disco.py:1866 +msgid "_Unsubscribe" +msgstr "_Abbestellen" + +#: ../src/features_window.py:46 +msgid "PyOpenSSL" +msgstr "PyOpenSSL" + +#: ../src/features_window.py:47 +msgid "" +"A library used to validate server certificates to ensure a secure connection." +msgstr "" +"Eine Bibliothek um Server-Zertifikate zu validieren um eine sichere " +"Verbindung zu gewährleisten." + +#: ../src/features_window.py:48 ../src/features_window.py:49 +msgid "Requires python-pyopenssl." +msgstr "Erfordert python-pyopenssl." + +#: ../src/features_window.py:50 +msgid "Bonjour / Zeroconf" +msgstr "Bonjour / Zeroconf" + +#: ../src/features_window.py:51 +msgid "Serverless chatting with autodetected clients in a local network." +msgstr "" +"Serverloses Chatten mit automatisch erkannten Clients in einem lokalen Netz" + +#: ../src/features_window.py:52 +msgid "Requires python-avahi." +msgstr "Erfordert python-avahi." + +#: ../src/features_window.py:53 +msgid "Requires pybonjour (http://o2s.csail.mit.edu/o2s-wiki/pybonjour)." +msgstr "Erfordert pybonjour (http://o2s.csail.mit.edu/o2s-wiki/pybonjour)." + +#: ../src/features_window.py:54 +msgid "gajim-remote" +msgstr "gajim-remote" + +#: ../src/features_window.py:55 +msgid "A script to controle Gajim via commandline." +msgstr "Ein Skript, um Gajim über die Befehlszeile zu steuern." + +#: ../src/features_window.py:56 +msgid "Requires python-dbus." +msgstr "Erfordert python-dbus." + +#: ../src/features_window.py:57 ../src/features_window.py:61 +#: ../src/features_window.py:65 ../src/features_window.py:69 +#: ../src/features_window.py:73 ../src/features_window.py:81 +#: ../src/features_window.py:85 ../src/features_window.py:97 +msgid "Feature not available under Windows." +msgstr "Funktion unter Windows nicht verfügbar." + +#: ../src/features_window.py:58 +msgid "OpenGPG" +msgstr "OpenPGP: " + +#: ../src/features_window.py:59 +msgid "Encrypting chatmessages with gpg keys." +msgstr "Chat Nachrichten werden mit gpg Schlüssel verschlüsselt" + +#: ../src/features_window.py:60 +msgid "Requires gpg and python-GnuPGInterface." +msgstr "Erfordert gpg und python-GnuPGInterface." + +#: ../src/features_window.py:62 +msgid "network-manager" +msgstr "network-manager" + +#: ../src/features_window.py:63 +msgid "Autodetection of network status." +msgstr "Auto-Erkennung des Netzwerk-Status." + +#: ../src/features_window.py:64 +msgid "Requires gnome-network-manager and python-dbus." +msgstr "Erfordert gnome-network-manager und python-dbus." + +#: ../src/features_window.py:66 +msgid "Session Management" +msgstr "Sitzungsverwaltung" + +#: ../src/features_window.py:67 +msgid "Gajim session is stored on logout and restored on login." +msgstr "" +"Gajim-Sitzung wird beim Logout gespeichert und beim Login wiederhergestellt" + +#: ../src/features_window.py:68 +msgid "Requires python-gnome2." +msgstr "Erfordert python-gnome2." + +#: ../src/features_window.py:70 +msgid "gnome-keyring" +msgstr "gnome-keyring" + +#: ../src/features_window.py:71 +msgid "Passwords can be stored securely and not just in plaintext." +msgstr "Passwörter können sicher und nicht nur im Klartext gespeichert werden" + +#: ../src/features_window.py:72 +msgid "Requires gnome-keyring and python-gnome2-desktop." +msgstr "Erfordert gnome-keyring und python-gnome2-desktop." + +#: ../src/features_window.py:74 +msgid "SRV" +msgstr "SRV" + +#: ../src/features_window.py:75 +msgid "Ability to connect to servers which are using SRV records." +msgstr "Fähigkeit zu Servern die SRV-Einträge verwenden zu verbinden." + +#: ../src/features_window.py:76 +msgid "Requires dnsutils." +msgstr "Erfordert dnsutils." + +#: ../src/features_window.py:77 +msgid "Requires nslookup to use SRV records." +msgstr "Erfordert nslookup zur Nutzung von SRV-Einträgen." + +#: ../src/features_window.py:78 +msgid "Spell Checker" +msgstr "Rechtschreibprüfung" + +#: ../src/features_window.py:79 +msgid "Spellchecking of composed messages." +msgstr "Rechtschreibprüfung erstellter Nachrichten." + +#: ../src/features_window.py:80 +msgid "" +"Requires python-gnome2-extras or compilation of gtkspell module from Gajim " +"sources." +msgstr "" +"Erfordert python-gnome2-extras oder die Kompilation des gtkspell-Moduls aus " +"den Gajim-Quellen." + +#: ../src/features_window.py:82 +msgid "Notification-daemon" +msgstr "Notification-daemon" + +#: ../src/features_window.py:83 +msgid "Passive popups notifying for new events." +msgstr "Popups informieren über neue Ereignisse." + +#: ../src/features_window.py:84 +msgid "" +"Requires python-notify or instead python-dbus in conjunction with " +"notification-daemon." +msgstr "" +"Erforder python-notify oder stattdessen python-dbus in Verbindung mit " +"notification-daemon." + +#: ../src/features_window.py:86 +msgid "Trayicon" +msgstr "Tray-Symbol" + +#: ../src/features_window.py:87 +msgid "A icon in systemtray reflecting the current presence." +msgstr "Ein Icon im Systemtray, das den Status widerspiegelt." + +#: ../src/features_window.py:88 +msgid "" +"Requires python-gnome2-extras or compiled trayicon module from Gajim sources." +msgstr "" +"Erfordert python-gnome2-extras oder das kompilierte Trayicon-Modul aus den " +"Gajim-Quellen" + +#: ../src/features_window.py:89 +msgid "Requires PyGTK >= 2.10." +msgstr "Erforder PyGTK >= 2.10." + +#: ../src/features_window.py:90 +msgid "Idle" +msgstr "Idle" + +#: ../src/features_window.py:91 +msgid "Ability to measure idle time, in order to set auto status." +msgstr "Fähigkeit die Idle-Zeit zu messen um den Status automatisch zu setzen" + +#: ../src/features_window.py:92 ../src/features_window.py:93 +msgid "Requires compilation of the idle module from Gajim sources." +msgstr "Erfordert die Kompilation des Idle-Moduls aus den Gajim-Quellen." + +#: ../src/features_window.py:94 +msgid "LaTeX" +msgstr "LaTeX" + +#: ../src/features_window.py:95 +msgid "Transform LaTeX expressions between $$ $$." +msgstr "LaTeX-Ausdrücke zwischen $$ $$ umwandeln." + +#: ../src/features_window.py:96 +msgid "" +"Requires texlive-latex-base, dvips and imagemagick. You have to set " +"'use_latex' to True in the Advanced Configuration Editor." +msgstr "" +"Erfordert texlive-latex-base, dvips und imagemagick. Sie müssen 'use_latex' " +"im Advanced Configuration Editor auf 'true' setzen." + +#: ../src/features_window.py:98 +msgid "End to End Encryption" +msgstr "Punkt-zu-Punkt-Verschlüsselung" + +#: ../src/features_window.py:99 +msgid "Encrypting chatmessages." +msgstr "Chatnachrichten werden verschlüsselt." + +#: ../src/features_window.py:100 ../src/features_window.py:101 +msgid "Requires python-crypto." +msgstr "Erfordert python-crypto" + +#: ../src/features_window.py:102 +msgid "Off the Record Encryption" +msgstr "OTR-Verschlüsselung" + +#: ../src/features_window.py:103 +msgid "Encrypting chatmessages in a way that even works through gateways." +msgstr "Verschlüssltes Kommunizieren, welches auch über Transporte möglich ist" + +#: ../src/features_window.py:104 ../src/features_window.py:105 +msgid "Requires pyotr and libotr (see http://trac.gajim.org/wiki/OTR)." +msgstr "Erfordert pyotr und libotr (http://trac.gajim.org/wiki/OTR)." + +#: ../src/features_window.py:106 +msgid "RST Generator" +msgstr "RST Generator" + +#: ../src/features_window.py:107 +msgid "" +"Generate XHTML output from RST code (see http://docutils.sourceforge.net/" +"docs/ref/rst/restructuredtext.html)." +msgstr "" +"Erzeug aus RST-Code XHTML-Ausgaben (siehe http://docutils.sourceforge.net/" +"docs/ref/rst/restructuredtext.html)." + +#: ../src/features_window.py:108 ../src/features_window.py:109 +msgid "Requires python-docutils." +msgstr "Erfordert python-docutils." + +#: ../src/features_window.py:110 +msgid "libsexy" +msgstr "" + +#: ../src/features_window.py:111 +msgid "Ability to have clickable URLs in chat and groupchat window banners." +msgstr "Anklickbare URLs im Chat-Fenster." + +#: ../src/features_window.py:112 ../src/features_window.py:113 +msgid "Requires python-sexy." +msgstr "Erfordert python-sexy." + +#: ../src/features_window.py:127 +msgid "Feature" +msgstr "Fähigkeit" + +#: ../src/filetransfers_window.py:77 +msgid "File" +msgstr "Datei" + +#: ../src/filetransfers_window.py:92 +msgid "Time" +msgstr "Zeit" + +#: ../src/filetransfers_window.py:104 +msgid "Progress" +msgstr "Fortschritt" + +#: ../src/filetransfers_window.py:164 ../src/filetransfers_window.py:218 +#, python-format +msgid "Filename: %s" +msgstr "Dateiname: %s" + +#: ../src/filetransfers_window.py:165 ../src/filetransfers_window.py:305 +#, python-format +msgid "Size: %s" +msgstr "Größe: %s" + +#. You is a reply of who sent a file +#. You is a reply of who received a file +#: ../src/filetransfers_window.py:174 ../src/filetransfers_window.py:184 +#: ../src/history_manager.py:486 +msgid "You" +msgstr "Sie" + +#: ../src/filetransfers_window.py:175 +#, python-format +msgid "Sender: %s" +msgstr "Gespeichert in: %s" + +#: ../src/filetransfers_window.py:176 ../src/filetransfers_window.py:595 +#: ../src/tooltips.py:646 +msgid "Recipient: " +msgstr "Empfänger: " + +#: ../src/filetransfers_window.py:187 +#, python-format +msgid "Saved in: %s" +msgstr "Absender: %s" + +#: ../src/filetransfers_window.py:189 +msgid "File transfer completed" +msgstr "Dateitransfer abgeschlossen" + +#: ../src/filetransfers_window.py:203 ../src/filetransfers_window.py:209 +msgid "File transfer cancelled" +msgstr "Dateitransfer abgebrochen" + +#: ../src/filetransfers_window.py:203 ../src/filetransfers_window.py:210 +msgid "Connection with peer cannot be established." +msgstr "Verbindung zum Teilnehmer kann nicht hergestellt werden" + +#: ../src/filetransfers_window.py:219 +#, python-format +msgid "Recipient: %s" +msgstr "Empfänger: %s" + +#: ../src/filetransfers_window.py:221 +#, python-format +msgid "Error message: %s" +msgstr "Fehlermeldung: %s" + +#: ../src/filetransfers_window.py:222 +msgid "File transfer stopped by the contact at the other end" +msgstr "Gegenseite hat Dateitransfer gestoppt" + +#: ../src/filetransfers_window.py:243 +msgid "Choose File to Send..." +msgstr "Datei auswählen ..." + +#: ../src/filetransfers_window.py:259 ../src/tooltips.py:686 +msgid "Description: " +msgstr "Beschreibung: " + +#: ../src/filetransfers_window.py:270 +msgid "Gajim cannot access this file" +msgstr "Gajim kann auf diese Datei nicht zugreifen" + +#: ../src/filetransfers_window.py:271 +msgid "This file is being used by another process." +msgstr "Diese Datei wird von einem anderen Prozess verwendet." + +#: ../src/filetransfers_window.py:303 +#, python-format +msgid "File: %s" +msgstr "Datei: %s" + +#: ../src/filetransfers_window.py:308 +#, python-format +msgid "Type: %s" +msgstr "Typ: %s" + +#: ../src/filetransfers_window.py:310 +#, python-format +msgid "Description: %s" +msgstr "Beschreibung: %s" + +#: ../src/filetransfers_window.py:311 +#, python-format +msgid "%s wants to send you a file:" +msgstr "%s möchte ihnen eine Datei senden:" + +#: ../src/filetransfers_window.py:324 ../src/gtkgui_helpers.py:778 +#, python-format +msgid "Cannot overwrite existing file \"%s\"" +msgstr "Kann existierende Datei \"%s\" nicht überschreiben" + +#: ../src/filetransfers_window.py:325 ../src/gtkgui_helpers.py:780 +msgid "" +"A file with this name already exists and you do not have permission to " +"overwrite it." +msgstr "" +"Eine Datei mit diesem Namen existiert bereits und Sie haben nicht die Rechte " +"sie zu überschreiben." + +#: ../src/filetransfers_window.py:332 ../src/gtkgui_helpers.py:784 +msgid "This file already exists" +msgstr "Diese Datei existiert bereits" + +#: ../src/filetransfers_window.py:332 ../src/gtkgui_helpers.py:784 +msgid "What do you want to do?" +msgstr "Was möchten Sie tun?" + +#. read-only bit is used to mark special folder under windows, +#. not to mark that a folder is read-only. See ticket #3587 +#: ../src/filetransfers_window.py:346 ../src/gtkgui_helpers.py:794 +#, python-format +msgid "Directory \"%s\" is not writable" +msgstr "Verzeichnis \"%s\" ist nicht schreibbar" + +#: ../src/filetransfers_window.py:346 ../src/gtkgui_helpers.py:795 +msgid "You do not have permission to create files in this directory." +msgstr "" +"Ihre Benutzerrechte erlauben es Ihnen nicht, in diesem Verzeichnis Dateien " +"anzulegen." + +#: ../src/filetransfers_window.py:356 +msgid "Save File as..." +msgstr "Datei speichern unter ..." + +#. Print remaining time in format 00:00:00 +#. You can change the places of (hours), (minutes), (seconds) - +#. they are not translatable. +#: ../src/filetransfers_window.py:436 +#, python-format +msgid "%(hours)02.d:%(minutes)02.d:%(seconds)02.d" +msgstr "%(hours)02.d:%(minutes)02.d:%(seconds)02.d" + +#. This should make the string Kb/s, +#. where 'Kb' part is taken from %s. +#. Only the 's' after / (which means second) should be translated. +#: ../src/filetransfers_window.py:525 +#, python-format +msgid "(%(filesize_unit)s/s)" +msgstr "(%(filesize_unit)s/s)" + +#: ../src/filetransfers_window.py:565 ../src/filetransfers_window.py:568 +msgid "Invalid File" +msgstr "Ungültige Datei" + +#: ../src/filetransfers_window.py:565 +msgid "File: " +msgstr "Datei: " + +#: ../src/filetransfers_window.py:569 +msgid "It is not possible to send empty files" +msgstr "Es nicht möglich, leere Dateien zu versenden" + +#: ../src/filetransfers_window.py:591 ../src/tooltips.py:636 +msgid "Name: " +msgstr "Name: " + +#: ../src/filetransfers_window.py:593 ../src/tooltips.py:640 +msgid "Sender: " +msgstr "Absender: " + +#: ../src/filetransfers_window.py:781 +msgid "Pause" +msgstr "Pause" + +#: ../src/gajim.py:72 +#, python-format +msgid "%s is not a valid loglevel" +msgstr "%s ist kein gültiger Loglevel." + +#: ../src/gajim.py:145 +msgid "Gajim needs X server to run. Quiting..." +msgstr "Gajim benötigt einen laufenden X-Server. Breche ab ..." + +#: ../src/gajim.py:175 +msgid "Gajim needs PyGTK 2.8 or above" +msgstr "Gajim benötigt PyGTK 2.6 oder höher" + +#: ../src/gajim.py:176 +msgid "Gajim needs PyGTK 2.8 or above to run. Quiting..." +msgstr "Gajim benötigt PyGTK 2.6 oder höher. Breche ab ..." + +#: ../src/gajim.py:178 +msgid "Gajim needs GTK 2.8 or above" +msgstr "Gajim benötigt GTK 2.6 oder höher" + +#: ../src/gajim.py:179 +msgid "Gajim needs GTK 2.8 or above to run. Quiting..." +msgstr "Gajim benötigt GTK 2.6+. Breche ab ..." + +#: ../src/gajim.py:184 +msgid "GTK+ runtime is missing libglade support" +msgstr "GTK+ runtine fehlt libglade-Unterstützung" + +#: ../src/gajim.py:186 +#, python-format +msgid "" +"Please remove your current GTK+ runtime and install the latest stable " +"version from %s" +msgstr "" +"Bitte entfernen Sie Ihre derzeitige GTK+ Laufzeitumgebung und installieren " +"Sie die aktuelle Version von %s" + +#: ../src/gajim.py:188 +msgid "" +"Please make sure that GTK+ and PyGTK have libglade support in your system." +msgstr "" +"Bitte stellen Sie sicher, dass GTK+ und PyGTK auf Ihrem System libglade-" +"Unterstützung besitzen." + +#: ../src/gajim.py:193 +msgid "Gajim needs PySQLite2 to run" +msgstr "Gajim benötigt PySQLite2 zum Starten" + +#: ../src/gajim.py:201 +msgid "Gajim needs pywin32 to run" +msgstr "Gajim benötigt pywin32 zum Laufen" + +#: ../src/gajim.py:202 +#, python-format +msgid "" +"Please make sure that Pywin32 is installed on your system. You can get it at " +"%s" +msgstr "" +"Bitte stellen Sie sicher das Pywin32 auf Ihrem System installiert ist. Sie " +"können es unter folgender URL herunterladen: %s " + +#: ../src/gajim.py:328 +msgid "Generating..." +msgstr "Generiere ..." + +#: ../src/gajim.py:332 +#, python-format +msgid "Generating a private key for %s..." +msgstr "" + +#: ../src/gajim.py:354 +#, python-format +msgid "" +"Generating a private key for %s...\n" +"done." +msgstr "" + +#: ../src/gajim.py:385 +msgid "" +"\n" +"This user has requested an Off-the-Record private conversation. However, you " +"do not have a plugin to support that.\n" +"See http://otr.cypherpunks.ca/ for more information." +msgstr "" + +#. set the icon to all newly opened wind +#: ../src/gajim.py:567 +msgid "Gajim is already running" +msgstr "Gajim läuft bereits" + +#: ../src/gajim.py:568 +msgid "" +"Another instance of Gajim seems to be running\n" +"Run anyway?" +msgstr "" +"Eine andere Instanz von Gajim schein bereits zu laufen\n" +"Trotzdem starten?" + +#: ../src/gajim.py:697 +msgid "Do you accept this request?" +msgstr "Akzeptieren Sie diese Anfrage?" + +#: ../src/gajim.py:699 +#, python-format +msgid "Do you accept this request on account %s?" +msgstr "Akzeptieren Sie diese Anfrage vom %s Konto?" + +#: ../src/gajim.py:702 +#, python-format +msgid "HTTP (%s) Authorization for %s (id: %s)" +msgstr "HTTP (%s) Autorisierung für %s (id: %s)" + +#: ../src/gajim.py:750 ../src/notify.py:475 ../src/osx/growler.py:17 +msgid "Connection Failed" +msgstr "Verbindung fehlgeschlagen" + +#. ('MSGNOTSENT', account, (jid, ierror_msg, msg, time, session)) +#: ../src/gajim.py:1007 ../src/gajim.py:1019 +#, python-format +msgid "error while sending %s ( %s )" +msgstr "Fehler beim Senden von %s ( %s )" + +#: ../src/gajim.py:1052 +msgid "Authorization accepted" +msgstr "Autorisierung akzeptiert" + +#: ../src/gajim.py:1053 +#, python-format +msgid "The contact \"%s\" has authorized you to see his or her status." +msgstr "Kontakt \"%s\" hat Sie autorisiert seinen oder ihren Staus zu sehen." + +#: ../src/gajim.py:1072 +#, python-format +msgid "Contact \"%s\" removed subscription from you" +msgstr "Kontakt \"%s\" hat das Abonnement zurückgezogen" + +#: ../src/gajim.py:1073 +msgid "" +"You will always see him or her as offline.\n" +"Do you want to remove him or her from your contact list?" +msgstr "" +"Du wirst sie oder ihn immer als offline sehen.\n" +"Möchtest du sie oder ihn von deiner Kontaktliste entfernen?" + +#: ../src/gajim.py:1115 +#, python-format +msgid "Contact with \"%s\" cannot be established" +msgstr "Kontakt mit \"%s\" konnte nicht hergestellt werden" + +#: ../src/gajim.py:1295 ../src/groupchat_control.py:1068 +#, python-format +msgid "%s is now known as %s" +msgstr "%s heißt jetzt %s" + +#: ../src/gajim.py:1310 ../src/groupchat_control.py:1217 +#: ../src/roster_window.py:1918 +#, python-format +msgid "%s is now %s" +msgstr "%s ist jetzt %s" + +#. Can be a presence (see chg_contact_status in groupchat_control.py) +#. Can be a message (see handle_event_gc_config_change in gajim.py) +#: ../src/gajim.py:1432 ../src/groupchat_control.py:1028 +msgid "Any occupant is allowed to see your full JID" +msgstr "Jeder Teilnehmer darf Ihre volle JID sehen" + +#: ../src/gajim.py:1435 +msgid "Room now shows unavailable member" +msgstr "Raum zeigt jetzt abwesende Teilnehmer" + +#: ../src/gajim.py:1437 +msgid "room now does not show unavailable members" +msgstr "Raum zeigt jetzt abwesende Teilnehmer nicht an" + +#: ../src/gajim.py:1440 +msgid "A non-privacy-related room configuration change has occurred" +msgstr "" +"Eine nicht Privatsphären-bezogene Raum-Konfigurations-Änderung ist " +"aufgetreten" + +#. Can be a presence (see chg_contact_status in groupchat_control.py) +#: ../src/gajim.py:1443 +msgid "Room logging is now enabled" +msgstr "Raum-Mitschnitt ist jetzt aktiviert" + +#: ../src/gajim.py:1445 +msgid "Room logging is now disabled" +msgstr "Raum-Mitschnitt ist jetzt deaktiviert" + +#: ../src/gajim.py:1447 +msgid "Room is now non-anonymous" +msgstr "Raum ist jetzt un-anonym" + +#: ../src/gajim.py:1450 +msgid "Room is now semi-anonymous" +msgstr "Raum ist jetzt semi-anonym" + +#: ../src/gajim.py:1453 +msgid "Room is now fully-anonymous" +msgstr "Raum ist jetzt voll anonym" + +#: ../src/gajim.py:1485 +#, python-format +msgid "A Password is required to join the room %s. Please type it." +msgstr "Für das Betreten des Gruppenchats-Raumes %s wird ein Passwort benötig." + +#: ../src/gajim.py:1519 +msgid "" +"You configured Gajim to use GPG agent, but there is no GPG agent running or " +"it returned a wrong passphrase.\n" +msgstr "" +"Du hast Gajim für die Verwendung des GPG-Agenten konfiguriert, aber es läuft " +"kein GPG-Agent oder er gab eine falsche Passphrase zurück.\n" + +#: ../src/gajim.py:1521 +msgid "You are currently connected without your OpenPGP key." +msgstr "Sie wurden ohne ihren GPG-Schlüssel verbunden" + +#: ../src/gajim.py:1524 +msgid "Your passphrase is incorrect" +msgstr "Ihre Passphrase ist falsch" + +#: ../src/gajim.py:1541 ../src/secrets.py:44 +msgid "Passphrase Required" +msgstr "Passphrase benötigt" + +#: ../src/gajim.py:1542 +#, python-format +msgid "Enter GPG key passphrase for account %s." +msgstr "Geben Sie die GPG-Passphrase für das Konto %s ein." + +#: ../src/gajim.py:1554 +msgid "Wrong Passphrase" +msgstr "Falsche Passphrase" + +#: ../src/gajim.py:1555 +msgid "Please retype your GPG passphrase or press Cancel." +msgstr "" +"Bitte geben Sie Ihre GPG-Passphrase erneut ein oder klicken Sie auf " +"Abbrechen." + +#: ../src/gajim.py:1644 +#, python-format +msgid "New mail on %(gmail_mail_address)s" +msgstr "Neue E-Mail auf %(gmail_mail_address)s" + +#: ../src/gajim.py:1646 +#, python-format +msgid "You have %d new mail conversation" +msgid_plural "You have %d new mail conversations" +msgstr[0] "Sie haben %d ungelesene E-Mail-Nachricht" +msgstr[1] "Sie haben %d ungelesene E-Mail-Nachrichten" + +#. FIXME: emulate Gtalk client popups. find out what they parse and how +#. they decide what to show +#. each message has a 'From', 'Subject' and 'Snippet' field +#: ../src/gajim.py:1655 +#, python-format +msgid "" +"\n" +"From: %(from_address)s" +msgstr "" +"\n" +"Von: %(from_address)s" + +#: ../src/gajim.py:1723 +#, python-format +msgid "%s wants to send you a file." +msgstr "%s möchte ihnen eine Datei senden." + +#: ../src/gajim.py:1788 +#, python-format +msgid "You successfully received %(filename)s from %(name)s." +msgstr "Sie haben %(filename)s erfolgreich von %(name)s erhalten." + +#. ft stopped +#: ../src/gajim.py:1792 +#, python-format +msgid "File transfer of %(filename)s from %(name)s stopped." +msgstr "Dateitransfer %(filename)s von %(name)s wurde gestoppt." + +#: ../src/gajim.py:1805 +#, python-format +msgid "You successfully sent %(filename)s to %(name)s." +msgstr "Sie haben%(filename)s erfolgreich an %(name)s gesendet." + +#. ft stopped +#: ../src/gajim.py:1809 +#, python-format +msgid "File transfer of %(filename)s to %(name)s stopped." +msgstr "Dateitransfer von %(filename)s an %(name)s wurde gestoppt." + +#: ../src/gajim.py:1936 ../src/gajim.py:1980 +msgid "Confirm these session options" +msgstr "Bestätigen Sie diese Sitzungs-Optionen" + +#: ../src/gajim.py:1937 +#, python-format +msgid "" +"The remote client wants to negotiate an session with these features:\n" +"\n" +"\t\t%s\n" +"\n" +"\t\tAre these options acceptable?" +msgstr "" +"Der entfernte Client will eine Sitzung mit diesen Features aushandeln:\n" +"\n" +"\t\t%s\n" +"\n" +"\t\tSind diese Einstellungen akzeptabel?" + +#: ../src/gajim.py:1981 +#, python-format +msgid "" +"The remote client selected these options:\n" +"\n" +"%s\n" +"\n" +"Continue with the session?" +msgstr "" +"Der entfernte Client hat diese Optionen ausgewählt:\n" +"\n" +"%s\n" +"\n" +"Mit der Sitzung fortfahren?" + +#: ../src/gajim.py:2100 +msgid "Username Conflict" +msgstr "Benutzernamenkonflikt" + +#: ../src/gajim.py:2101 +msgid "Please type a new username for your local account" +msgstr "Bitte geben Sie einen neuen Benutzernamen für Ihr lokales Konto ein" + +#: ../src/gajim.py:2116 ../src/gajim.py:2118 +msgid "Ping?" +msgstr "Ping?" + +#: ../src/gajim.py:2124 ../src/gajim.py:2126 +#, python-format +msgid "Pong! (%s s.)" +msgstr "Pong! (%s s.)" + +#: ../src/gajim.py:2130 ../src/gajim.py:2132 +msgid "Error." +msgstr "Fehler:" + +#: ../src/gajim.py:2157 +msgid "Resource Conflict" +msgstr "Resourcenkonflikt" + +#: ../src/gajim.py:2158 +msgid "" +"You are already connected to this account with the same resource. Please " +"type a new one" +msgstr "" +"Sie sind mit diesem Konto bereits mit derselben Ressource verbunden.Bitte " +"geben Sie eine neue ein" + +#: ../src/gajim.py:2211 +msgid "Error verifying SSL certificate" +msgstr "Fehler bei der Überprüfung des SSL-Zertifikats" + +#: ../src/gajim.py:2212 +#, python-format +msgid "" +"There was an error verifying the SSL certificate of your jabber server: %" +"(error)s\n" +"Do you still want to connect to this server?" +msgstr "" +"Es trat ein Fehler bei der Überprüfung des SSL-Zertifikats deines Jabber-" +"Servers auf: %(error)s\n" +"Möchtest du dich dennoch zum Server verbinden?" + +#: ../src/gajim.py:2217 +msgid "Ignore this error for this certificate." +msgstr "Ignoriere den Fehler für dieses Zertifikat." + +#: ../src/gajim.py:2232 +msgid "SSL certificate error" +msgstr "SSL-Zertifikat-Fehler" + +#: ../src/gajim.py:2233 +#, python-format +msgid "" +"It seems the SSL certificate has changed or your connection is being " +"hacked.\n" +"Old fingerprint: %s\n" +"New fingerprint: %s\n" +"\n" +"Do you still want to connect and update the fingerprint of the certificate?" +msgstr "" +"Das SSL-Zertifikat scheint sich geändert zu haben oder die Verbindung wurde " +"gehackt.\n" +"Alter Fingerprint: %s\n" +"Neuer Fingerprint: %s\n" +"\n" +"Möchten Sie noch verbinden und den Fingerprint des Zertifikats aktualisieren?" + +#: ../src/gajim.py:2252 +msgid "Insecure connection" +msgstr "Unsichere Verbindung" + +#: ../src/gajim.py:2253 +msgid "" +"You are about to send your password on an insecure connection. Are you sure " +"you want to do that?" +msgstr "" +"Sie sind dabei Ihr Passwort über eine unsichere Verbindung zu senden. Sind " +"Sie sicher, dass Sie das wollen?" + +#: ../src/gajim.py:2255 ../src/groupchat_control.py:1695 +#: ../src/roster_window.py:3665 +msgid "Do _not ask me again" +msgstr "_Nicht noch einmal fragen" + +#: ../src/gajim.py:2268 +msgid "PEP node was not removed" +msgstr "PEP Knoten wurde nicht entfernt" + +#: ../src/gajim.py:2269 +#, python-format +msgid "PEP node %s was not removed: %s" +msgstr "PEP Knoten %s wurde nicht entfernt: %s" + +#. theme doesn't exist, disable emoticons +#: ../src/gajim.py:2719 ../src/gajim.py:2740 +msgid "Emoticons disabled" +msgstr "Emoticons deaktiviert" + +#: ../src/gajim.py:2720 +msgid "" +"Your configured emoticons theme has not been found, so emoticons have been " +"disabled." +msgstr "" +"Das konfigurierte Emoticon-Thema wurde nicht gefunden. Emoticans sind " +"deaktiviert." + +#: ../src/gajim.py:2741 +msgid "" +"Your configured emoticons theme cannot been loaded. You maybe need to update " +"the format of emoticons.py file. See http://trac.gajim.org/wiki/Emoticons " +"for more details." +msgstr "" +"Dein konfiguriertes Emoticon-Thema kann nicht geladen werden. Du musst " +"vielleicht das Format der Datei emoticons.py aktualisieren. Gehe auf http://" +"trac.gajim.org/wiki/Emoticons für mehr Informationen." + +#: ../src/gajim.py:2769 ../src/roster_window.py:1169 +#: ../src/roster_window.py:3215 +msgid "You cannot join a group chat while you are invisible" +msgstr "" +"Sie können einem Gruppenchat nicht beitreten, wenn Sie unsichtbar sind." + +#. it is good to notify the user +#. in case he or she cannot see the output of the console +#: ../src/gajim.py:3031 +msgid "Could not save your settings and preferences" +msgstr "Konnte Einstellungen nicht speichern" + +#: ../src/gajim.py:3123 +msgid "Bookmark already set" +msgstr "Lesezeichen existiert schon" + +#: ../src/gajim.py:3124 +#, python-format +msgid "Group Chat \"%s\" is already in your bookmarks." +msgstr "Der Gruppenchat \"%s\" ist schon in den Lesezeichen." + +#: ../src/gajim.py:3137 +msgid "Bookmark has been added successfully" +msgstr "Lesezeichen wurde erfolgreich hinzugefügt" + +#: ../src/gajim.py:3138 +msgid "You can manage your bookmarks via Actions menu in your roster." +msgstr "" +"Sie können ihre Lesezeichen über das Aktionen-Menü in der Kontaktliste " +"bearbeiten" + +#: ../src/gajim.py:3325 +msgid "Network Manager support not available" +msgstr "Unterstützung für Network Manager nicht verfügbar" + +#: ../src/gajim.py:3438 +msgid "Session Management support not available (missing gnome.ui module)" +msgstr "Sitzungsmanagment-Unterstützung nicht verfügbar (Modul gnome.ui fehlt)" + +#: ../src/gajim-remote.py:74 +msgid "Shows a help on specific command" +msgstr "zeige die Hilfe für einen bestimmten Befehl" + +#. User gets help for the command, specified by this parameter +#: ../src/gajim-remote.py:77 +msgid "command" +msgstr "Befehl" + +#: ../src/gajim-remote.py:78 +msgid "show help on command" +msgstr "zeige Hilfe für Befehl" + +#: ../src/gajim-remote.py:82 +msgid "Shows or hides the roster window" +msgstr "Ein- oder Ausblenden des Hauptfensters" + +#: ../src/gajim-remote.py:86 +msgid "Pops up a window with the next pending event" +msgstr "Zeige Popup-Fenster mit dem nächsten ungelesenen Ereignis" + +#: ../src/gajim-remote.py:90 +msgid "" +"Prints a list of all contacts in the roster. Each contact appears on a " +"separate line" +msgstr "Gibt eine Liste aller Kontakte im Roster aus. Eine Zeile je Kontakt" + +#: ../src/gajim-remote.py:93 ../src/gajim-remote.py:108 +#: ../src/gajim-remote.py:118 ../src/gajim-remote.py:131 +#: ../src/gajim-remote.py:145 ../src/gajim-remote.py:154 +#: ../src/gajim-remote.py:175 ../src/gajim-remote.py:205 +#: ../src/gajim-remote.py:214 ../src/gajim-remote.py:221 +#: ../src/gajim-remote.py:228 ../src/gajim-remote.py:239 +#: ../src/gajim-remote.py:255 ../src/gajim-remote.py:264 +msgid "account" +msgstr "Konto" + +#: ../src/gajim-remote.py:93 +msgid "show only contacts of the given account" +msgstr "zeige nur Kontakte für das gegebene Konto" + +#: ../src/gajim-remote.py:99 +msgid "Prints a list of registered accounts" +msgstr "Gibt eine Liste registrierter Konten aus" + +#: ../src/gajim-remote.py:103 +msgid "Changes the status of account or accounts" +msgstr "Ändere den Status eines oder mehrerer Konten" + +#. offline, online, chat, away, xa, dnd, invisible should not be translated +#: ../src/gajim-remote.py:106 +msgid "status" +msgstr "Status" + +#: ../src/gajim-remote.py:106 +msgid "one of: offline, online, chat, away, xa, dnd, invisible " +msgstr "Eins von: offline, online, chat, away, xa, dnd, invisible " + +#: ../src/gajim-remote.py:107 ../src/gajim-remote.py:128 +#: ../src/gajim-remote.py:142 ../src/gajim-remote.py:153 +msgid "message" +msgstr "Nachricht" + +#: ../src/gajim-remote.py:107 +msgid "status message" +msgstr "Statusnachricht" + +#: ../src/gajim-remote.py:108 +msgid "" +"change status of account \"account\". If not specified, try to change status " +"of all accounts that have \"sync with global status\" option set" +msgstr "" +"Ändere Status vom Konto \"account\". Falls nicht angegeben, ändere Status " +"aller Konten für die \"Kontostatus mit globalem Status abgleichen\" " +"aktiviert ist" + +#: ../src/gajim-remote.py:114 +msgid "Shows the chat dialog so that you can send messages to a contact" +msgstr "" +"Zeige Chatfenster, so dass eine Nachricht an einen Kontakt gesendet werden " +"kann" + +#: ../src/gajim-remote.py:116 +msgid "JID of the contact that you want to chat with" +msgstr "JID des Kontakts mit Sie chatten möchten" + +#: ../src/gajim-remote.py:118 ../src/gajim-remote.py:205 +msgid "if specified, contact is taken from the contact list of this account" +msgstr "falls angegeben, wird der Kontakt von der Liste dieses Kontos gewählt" + +#: ../src/gajim-remote.py:123 +msgid "" +"Sends new chat message to a contact in the roster. Both OpenPGP key and " +"account are optional. If you want to set only 'account', without 'OpenPGP " +"key', just set 'OpenPGP key' to ''." +msgstr "" +"Sende neue Chat-Nachricht zu einem Kontakt im Roster. OpenPGP-Schlüssel und " +"Konto sind optional. Falls nur 'Konto' gesetzt werden soll, ohne OpenGPG-" +"Schlüssel, setzen Sie 'OpenGPG-Schlüssel' einfach auf ''." + +#: ../src/gajim-remote.py:127 ../src/gajim-remote.py:140 +msgid "JID of the contact that will receive the message" +msgstr "JID des Kontakts, der die Nachricht empfängt" + +#: ../src/gajim-remote.py:128 ../src/gajim-remote.py:142 +#: ../src/gajim-remote.py:153 +msgid "message contents" +msgstr "Nachrichteninhalt" + +#: ../src/gajim-remote.py:129 ../src/gajim-remote.py:143 +msgid "pgp key" +msgstr "PGP-Schlüssel" + +#: ../src/gajim-remote.py:129 ../src/gajim-remote.py:143 +msgid "if specified, the message will be encrypted using this public key" +msgstr "falls angegeben, wird die Nachricht damit verschlüsselt" + +#: ../src/gajim-remote.py:131 ../src/gajim-remote.py:145 +#: ../src/gajim-remote.py:154 +msgid "if specified, the message will be sent using this account" +msgstr "falls angegeben, wird die Nachricht über dieses Konto gesendet" + +#: ../src/gajim-remote.py:136 +msgid "" +"Sends new single message to a contact in the roster. Both OpenPGP key and " +"account are optional. If you want to set only 'account', without 'OpenPGP " +"key', just set 'OpenPGP key' to ''." +msgstr "" +"Sende neue einzelne Nachricht an einen Kontakt im Roster. OpenPGP-Schlüssel " +"und Konto sind optional. Falls nur 'Konto' gesetzt werden soll, ohne OpenGPG-" +"Schlüssel, lassen Sie 'OpenGPG-Schlüssel' einfach leer." + +#: ../src/gajim-remote.py:141 +msgid "subject" +msgstr "Betreff" + +#: ../src/gajim-remote.py:141 +msgid "message subject" +msgstr "Nachrichten-Betreff" + +#: ../src/gajim-remote.py:150 +msgid "Sends new message to a groupchat you've joined." +msgstr "" +"Sendet eine neue Nachricht an einen Gruppenchat, den Sie betreten haben." + +#: ../src/gajim-remote.py:152 +msgid "JID of the room that will receive the message" +msgstr "JID des Raums, der die Nachricht empfängt" + +#: ../src/gajim-remote.py:159 +msgid "Gets detailed info on a contact" +msgstr "Zeige detaillierte Informationen über Kontakt" + +#: ../src/gajim-remote.py:161 ../src/gajim-remote.py:174 +#: ../src/gajim-remote.py:204 ../src/gajim-remote.py:213 +msgid "JID of the contact" +msgstr "JID des Kontakts" + +#: ../src/gajim-remote.py:165 +msgid "Gets detailed info on a account" +msgstr "Zeige detaillierte Informationen über ein Konto" + +#: ../src/gajim-remote.py:167 +msgid "Name of the account" +msgstr "Name des Kontos" + +#: ../src/gajim-remote.py:171 +msgid "Sends file to a contact" +msgstr "Sendet dem Kontakt eine Datei" + +#: ../src/gajim-remote.py:173 +msgid "file" +msgstr "Datei" + +#: ../src/gajim-remote.py:173 +msgid "File path" +msgstr "Dateipfad" + +#: ../src/gajim-remote.py:175 +msgid "if specified, file will be sent using this account" +msgstr "Falls angegeben, wird die Nachricht über dieses Konto gesendet" + +#: ../src/gajim-remote.py:180 +msgid "Lists all preferences and their values" +msgstr "Zeige alle Einstellung und ihre Werte" + +#: ../src/gajim-remote.py:184 +msgid "Sets value of 'key' to 'value'." +msgstr "Setze Wert von 'Schlüssel' auf 'Wert'." + +#: ../src/gajim-remote.py:186 +msgid "key=value" +msgstr "key=value" + +#: ../src/gajim-remote.py:186 +msgid "'key' is the name of the preference, 'value' is the value to set it to" +msgstr "'key' ist der Name der Option, 'value' ist der der einzustellende Wert" + +#: ../src/gajim-remote.py:191 +msgid "Deletes a preference item" +msgstr "Lösche eine Einstellung" + +#: ../src/gajim-remote.py:193 +msgid "key" +msgstr "Schlüssel" + +#: ../src/gajim-remote.py:193 +msgid "name of the preference to be deleted" +msgstr "Name der zu löschenden Einstellung" + +#: ../src/gajim-remote.py:197 +msgid "Writes the current state of Gajim preferences to the .config file" +msgstr "Schreibe die aktuellen Gajim-Einstellung in die Konfigurationsdatei" + +#: ../src/gajim-remote.py:202 +msgid "Removes contact from roster" +msgstr "Entfernt den Kontakt aus dem Roster" + +#: ../src/gajim-remote.py:211 +msgid "Adds contact to roster" +msgstr "Fügt Kontakt zum Roster hinzu" + +#: ../src/gajim-remote.py:213 +msgid "jid" +msgstr "JID" + +#: ../src/gajim-remote.py:214 +msgid "Adds new contact to this account" +msgstr "Fügt neuen Kontakt zu diesem Konto hinzu" + +#: ../src/gajim-remote.py:219 +msgid "Returns current status (the global one unless account is specified)" +msgstr "" +"Gibt derzeitigen Status zurück (der globale Status, außer, es wird ein " +"Account spezifiziert)" + +#: ../src/gajim-remote.py:226 +msgid "" +"Returns current status message(the global one unless account is specified)" +msgstr "" +"Gibt derzeitige Statusnachricht zurück (der globale, außer, es wird ein " +"Account angegeben)" + +#: ../src/gajim-remote.py:233 +msgid "Returns number of unread messages" +msgstr "Gibt die Anzahl der ungelesenen Nachrichten zurück" + +#: ../src/gajim-remote.py:237 +msgid "Opens 'Start Chat' dialog" +msgstr "Öffnet den 'Chat starten' Dialog" + +#: ../src/gajim-remote.py:239 +msgid "Starts chat, using this account" +msgstr "Starte Chat mit diesem Account" + +#: ../src/gajim-remote.py:243 +msgid "Sends custom XML" +msgstr "Benutzerdefinierten XML-Code senden" + +#: ../src/gajim-remote.py:245 +msgid "XML to send" +msgstr "Zu sendender XML-Code" + +#: ../src/gajim-remote.py:246 +msgid "" +"Account in which the xml will be sent; if not specified, xml will be sent to " +"all accounts" +msgstr "" +"Konto, an welchen XML gesendet wird; wenn nicht spezifiziert, wird XML an " +"alle Konten gesendet" + +#: ../src/gajim-remote.py:252 +msgid "Handle a xmpp:/ uri" +msgstr "Behandle eine xmpp:/ URI" + +#: ../src/gajim-remote.py:254 +msgid "uri" +msgstr "URI" + +#: ../src/gajim-remote.py:259 +msgid "Join a MUC room" +msgstr "Neuen Raum betreten" + +#: ../src/gajim-remote.py:261 +msgid "room" +msgstr "Raum" + +#: ../src/gajim-remote.py:262 +msgid "nick" +msgstr "Spitzname" + +#: ../src/gajim-remote.py:263 +msgid "password" +msgstr "Passwort" + +#: ../src/gajim-remote.py:268 +msgid "Check if Gajim is running" +msgstr "Bitte überprüfen Sie, ob Gajim läuft" + +#: ../src/gajim-remote.py:272 ../src/gajim-remote.py:282 +msgid "Shows or hides the ipython window" +msgstr "Ein- oder Ausblenden des Hauptfensters" + +#: ../src/gajim-remote.py:306 +msgid "Missing argument \"contact_jid\"" +msgstr "Fehlendes Argument \"contact_jid\"" + +#: ../src/gajim-remote.py:325 +#, python-format +msgid "" +"'%s' is not in your roster.\n" +"Please specify account for sending the message." +msgstr "" +"'%s' ist nicht in ihrer Kontaktliste.\n" +"Bitte geben Sie ein Konto zum Senden der Nachricht an." + +#: ../src/gajim-remote.py:328 +msgid "You have no active account" +msgstr "Kein aktives Konto" + +#: ../src/gajim-remote.py:381 +msgid "It seems Gajim is not running. So you can't use gajim-remote." +msgstr "" +"Gajim scheint nicht gestartet zu sein. gajim-remote kann somit nicht genutzt " +"werden." + +#: ../src/gajim-remote.py:410 +#, python-format +msgid "" +"Usage: %s %s %s \n" +"\t %s" +msgstr "" +"Verwendung: %s %s %s \n" +"\t %s" + +#: ../src/gajim-remote.py:413 +msgid "Arguments:" +msgstr "Parameter:" + +#: ../src/gajim-remote.py:417 +#, python-format +msgid "%s not found" +msgstr "%s nicht gefunden" + +#: ../src/gajim-remote.py:421 +#, python-format +msgid "" +"Usage: %s command [arguments]\n" +"Command is one of:\n" +msgstr "" +"Verwendung: %s Befehl [Argumente]\n" +"Befehl ist einer von:\n" + +#: ../src/gajim-remote.py:494 +#, python-format +msgid "" +"Too many arguments. \n" +"Type \"%s help %s\" for more info" +msgstr "" +"Zu viele Argumente. \n" +"Geben Sie \"%s help %s\" ein für weitere Informationen" + +#: ../src/gajim-remote.py:498 +#, python-format +msgid "" +"Argument \"%s\" is not specified. \n" +"Type \"%s help %s\" for more info" +msgstr "" +"Argument \"%s\" wurde nicht angegeben. \n" +"Geben Sie \"%s help %s\" ein für Hilfe" + +#: ../src/gajim-remote.py:516 +msgid "Wrong uri" +msgstr "Falscher URI" + +#: ../src/gajim_themes_window.py:67 +msgid "Theme" +msgstr "Thema" + +#: ../src/gajim_themes_window.py:105 +msgid "You cannot make changes to the default theme" +msgstr "Sie können ihr derzeitiges Theme nicht ändern" + +#: ../src/gajim_themes_window.py:106 +msgid "Please create a clean new theme with your desired name." +msgstr "" +"Bitte erstellen Sie ein neues leeres Thema mit Ihrem gewünschten Namen." + +#. don't confuse translators +#: ../src/gajim_themes_window.py:180 +msgid "theme name" +msgstr "Name des Themas" + +#: ../src/gajim_themes_window.py:197 +msgid "You cannot delete your current theme" +msgstr "Sie können ihr derzeitiges Theme nicht löschen" + +#: ../src/gajim_themes_window.py:198 +msgid "Please first choose another for your current theme." +msgstr "Bitte wählen Sie zuerst einen anderen Namen für ihr derzeitiges Theme." + +#: ../src/groupchat_control.py:147 +msgid "Sending private message failed" +msgstr "Senden privater Nachricht fehlgeschlagen" + +#. in second %s code replaces with nickname +#: ../src/groupchat_control.py:149 +#, python-format +msgid "You are no longer in group chat \"%s\" or \"%s\" has left." +msgstr "" +"Sie sind nicht mehr im Gruppenchat \"%s\" oder \"%s\" hat den Gruppenchat " +"verlassen." + +#: ../src/groupchat_control.py:388 +msgid "Insert Nickname" +msgstr "Spitzname einfügen" + +#: ../src/groupchat_control.py:540 +#, fuzzy +msgid "Conversation with " +msgstr "Unterhaltungsverlauf" + +#: ../src/groupchat_control.py:542 +#, fuzzy +msgid "Continued conversation" +msgstr "Bestätigung abbrechen" + +#: ../src/groupchat_control.py:956 +msgid "Really send file?" +msgstr "Datei wirklich senden?" + +#: ../src/groupchat_control.py:957 +#, python-format +msgid "If you send a file to %s, he/she will know your real Jabber ID." +msgstr "" +"Wenn Sie eine Datei an %s senden wird er/sie Ihre echte Jabber ID kennen." + +#. Can be a message (see handle_event_gc_config_change in gajim.py) +#: ../src/groupchat_control.py:1031 +msgid "Room logging is enabled" +msgstr "Raum-Mitschnitt ist aktiviert" + +#: ../src/groupchat_control.py:1033 +msgid "A new room has been created" +msgstr "Ein neuer Raum wurde erstellt" + +#: ../src/groupchat_control.py:1036 +msgid "The server has assigned or modified your roomnick" +msgstr "Der Server hat Ihren Raum-Nick zugewiesen oder verändert" + +#. do not print 'kicked by None' +#: ../src/groupchat_control.py:1042 +#, python-format +msgid "%(nick)s has been kicked: %(reason)s" +msgstr "%(nick)s wurde rausgeschmissen: %(reason)s" + +#: ../src/groupchat_control.py:1046 +#, python-format +msgid "%(nick)s has been kicked by %(who)s: %(reason)s" +msgstr "%(nick)s wurde von %(who)s rausgeschmissen: %(reason)s" + +#. do not print 'banned by None' +#: ../src/groupchat_control.py:1053 +#, python-format +msgid "%(nick)s has been banned: %(reason)s" +msgstr "%(nick)s wurde gebannt: %(reason)s" + +#: ../src/groupchat_control.py:1057 +#, python-format +msgid "%(nick)s has been banned by %(who)s: %(reason)s" +msgstr "%(nick)s wurde von %(who)s gebannt: %(reason)s" + +#: ../src/groupchat_control.py:1066 +#, python-format +msgid "You are now known as %s" +msgstr "Sie heißen nun %s" + +#: ../src/groupchat_control.py:1102 ../src/groupchat_control.py:1106 +#: ../src/groupchat_control.py:1111 +#, python-format +msgid "%(nick)s has been removed from the room (%(reason)s)" +msgstr "%(nick)s wurde von %(who)s rausgeschmissen: %(reason)s" + +#: ../src/groupchat_control.py:1103 +msgid "affiliation changed" +msgstr "Zugehörigkeit geändert: " + +#: ../src/groupchat_control.py:1108 +msgid "room configuration changed to members-only" +msgstr "Gruppenchatkonfiguration wurde auf ausschließlich Mitgleider geändert" + +#: ../src/groupchat_control.py:1113 +msgid "system shutdown" +msgstr "System wird heruntergefahren" + +#: ../src/groupchat_control.py:1210 +#, python-format +msgid "%s has left" +msgstr "%s ist gegangen" + +#: ../src/groupchat_control.py:1215 +#, python-format +msgid "%s has joined the group chat" +msgstr "%s hat den Gruppenchat betreten" + +#. Invalid Nickname +#. invalid char +#: ../src/groupchat_control.py:1328 ../src/groupchat_control.py:1600 +msgid "Invalid nickname" +msgstr "Ungültiger Benutzername" + +#: ../src/groupchat_control.py:1352 ../src/groupchat_control.py:1370 +#: ../src/groupchat_control.py:1454 ../src/groupchat_control.py:1471 +#, python-format +msgid "Nickname not found: %s" +msgstr "Spitzname nicht gefunden: %s" + +#: ../src/groupchat_control.py:1386 +msgid "This group chat has no subject" +msgstr "Dieser Gruppenchat hat kein Thema" + +#: ../src/groupchat_control.py:1397 +#, python-format +msgid "Invited %(contact_jid)s to %(room_jid)s." +msgstr "%(contact_jid)s in %(room_jid)s eingeladen" + +#: ../src/groupchat_control.py:1527 +#, python-format +msgid "" +"Usage: /%s [reason], bans the JID from the group chat. The " +"nickname of an occupant may be substituted, but not if it contains \"@\". If " +"the JID is currently in the group chat, he/she/it will also be kicked. Does " +"NOT support spaces in nickname." +msgstr "" +"Bedienung: /%s [Grund], bannt die JID aus dem Gruppenchat. " +"Der Spitzname eines Teilnehmers kann hinzugefügt werden, aber nicht, wenn es " +"ein \"@\" enthält. Wenn die JID derzeit im Gruppenchat ist, wird er/sie/es " +"ebenfalls gebannt. Unterstützt KEINE Leerzeichen im Spitznamen." + +#: ../src/groupchat_control.py:1534 +#, python-format +msgid "" +"Usage: /%s , opens a private chat window with the specified " +"occupant." +msgstr "" +"Verwendung: /%s , öffnet ein privates Nachrichtenfenster mit dem " +"Inhaber des angegebenen Spitznamens." + +#: ../src/groupchat_control.py:1540 +#, python-format +msgid "" +"Usage: /%s [reason], closes the current window or tab, displaying reason if " +"specified." +msgstr "" +"Bedienung: /%s [Grund], schließt das aktuelle Fenster oder Tab, mit Anzeige " +"eines Grundes, falls angegeben." + +#: ../src/groupchat_control.py:1546 +#, python-format +msgid "" +"Usage: /%s [reason], invites JID to the current group chat, optionally " +"providing a reason." +msgstr "" +"Bedienung: /%s [Grund], lädt die JID in den aktuellen Gruppenchat ein, " +"optional mit der Angabe eines Grundes." + +#: ../src/groupchat_control.py:1550 +#, python-format +msgid "" +"Usage: /%s @[/nickname], offers to join room@server optionally " +"using specified nickname." +msgstr "" +"Bedienung: /%s @ [/Spitzname], bietet den Beitritt zu " +"Raum@Server an, optional mit Spitznamen." + +#: ../src/groupchat_control.py:1554 +#, python-format +msgid "" +"Usage: /%s [reason], removes the occupant specified by nickname " +"from the group chat and optionally displays a reason. Does NOT support " +"spaces in nickname." +msgstr "" +"Bedienung: /%s [Grund], verweist den Inhaber des Spitznamen des " +"Gruppenchats und zeigt optional einen Grund. Unterstützt KEINE Leerzeichen " +"im Spitznamen." + +#: ../src/groupchat_control.py:1563 +#, python-format +msgid "" +"Usage: /%s [message], opens a private message window and sends " +"message to the occupant specified by nickname." +msgstr "" +"Verwendung: /%s [Nachricht], öffnet ein privates " +"Nachrichtenfenster und sendet die Nachricht zum Inhaber des angegebenen " +"Spitznamens." + +#: ../src/groupchat_control.py:1568 +#, python-format +msgid "Usage: /%s , changes your nickname in current group chat." +msgstr "" +"Bedienung: /%s , ändert den Spitznamen im aktuellen Gruppenchat." + +#: ../src/groupchat_control.py:1572 +#, python-format +msgid "Usage: /%s , display the names of group chat occupants." +msgstr "Benutzung: /%s , zeigt die Namen der Gruppenchatbesucher." + +#: ../src/groupchat_control.py:1576 +#, python-format +msgid "Usage: /%s [topic], displays or updates the current group chat topic." +msgstr "" +"Benutzung: /%s [topic], zeigt oder aktualisiert das derzeitige Gruppenchat-" +"Thema." + +#: ../src/groupchat_control.py:1579 +#, python-format +msgid "" +"Usage: /%s , sends a message without looking for other commands." +msgstr "" +"Benutzung: /%s , sendet eine Nachricht ohne andere Befehle zu " +"beachten." + +#: ../src/groupchat_control.py:1689 +#, python-format +msgid "Are you sure you want to leave group chat \"%s\"?" +msgstr "Möchten Sie den Gruppenchat \"%s\" wirklich verlassen?" + +#: ../src/groupchat_control.py:1691 +msgid "" +"If you close this window, you will be disconnected from this group chat." +msgstr "" +"Wenn Sie dieses Fenster schließen, wird die Verbindung zu diesem Gruppenchat " +"geschlossen." + +#: ../src/groupchat_control.py:1727 +msgid "Changing Subject" +msgstr "Thema ändern" + +#: ../src/groupchat_control.py:1728 +msgid "Please specify the new subject:" +msgstr "Bitte bestimmen Sie ein neues Thema" + +#: ../src/groupchat_control.py:1739 +msgid "Changing Nickname" +msgstr "Spitzname ändern" + +#: ../src/groupchat_control.py:1740 +msgid "Please specify the new nickname you want to use:" +msgstr "Bitte geben Sie an, welchen Spitznamen Sie verwenden möchten:" + +#. Ask for a reason +#: ../src/groupchat_control.py:1755 +#, python-format +msgid "Destroying %s" +msgstr "Zerstöre %s" + +#: ../src/groupchat_control.py:1756 +msgid "" +"You are going to definitively destroy this room.\n" +"You may specify a reason below:" +msgstr "" +"Sie werden den Raum endgültig zerstören.\n" +"Sie können hier einen Grund angeben:" + +#: ../src/groupchat_control.py:1758 +msgid "You may also enter an alternate venue:" +msgstr "Sie können auch einen alternativen Raum eintragen:" + +#. ask for reason +#: ../src/groupchat_control.py:1942 +#, python-format +msgid "Kicking %s" +msgstr "%s rausschmeißen" + +#: ../src/groupchat_control.py:1943 ../src/groupchat_control.py:2247 +msgid "You may specify a reason below:" +msgstr "Sie können eine Begründung angeben:" + +#. ask for reason +#: ../src/groupchat_control.py:2246 +#, python-format +msgid "Banning %s" +msgstr "%s verbannen" + +#: ../src/gtkexcepthook.py:46 +msgid "A programming error has been detected" +msgstr "Es wurde ein Programmfehler entdeckt" + +#: ../src/gtkexcepthook.py:47 +msgid "" +"It probably is not fatal, but should be reported to the developers " +"nonetheless." +msgstr "" +"Es ist vermutlich nicht fatal, aber der Fehler sollte trotzdem den " +"Entwicklern gemeldet werden" + +#: ../src/gtkexcepthook.py:53 +msgid "_Report Bug" +msgstr "Bug melden" + +#: ../src/gtkexcepthook.py:76 +msgid "Details" +msgstr "Details" + +#. we talk about file +#: ../src/gtkgui_helpers.py:162 ../src/gtkgui_helpers.py:177 +#, python-format +msgid "Error: cannot open %s for reading" +msgstr "Fehler: Kann %s kann nicht zum Lesen öffnen" + +#: ../src/gtkgui_helpers.py:352 +msgid "Error reading file:" +msgstr "Fehler beim Lesen der Datei:" + +#: ../src/gtkgui_helpers.py:355 +msgid "Error parsing file:" +msgstr "Fehler bei der Dateianalyse:" + +#. do not traceback (could be a permission problem) +#. we talk about a file here +#: ../src/gtkgui_helpers.py:392 +#, python-format +msgid "Could not write to %s. Session Management support will not work" +msgstr "" +"Konnte nicht an %s schreiben. Sitzungmanagment-Unterstützung wird nicht " +"funktionieren" + +#. xmpp: is currently handled by another program, so ask the user +#: ../src/gtkgui_helpers.py:724 +msgid "Gajim is not the default Jabber client" +msgstr "Gajim ist nicht Ihr Standard-Jabber-Client" + +#: ../src/gtkgui_helpers.py:725 +msgid "Would you like to make Gajim the default Jabber client?" +msgstr "Möchten Sie Gajim zu Ihrem Standard-Jabber-Client machen?" + +#: ../src/gtkgui_helpers.py:726 +msgid "Always check to see if Gajim is the default Jabber client on startup" +msgstr "" +"Immer beim Programmstart prüfen, ob Gajim der Standard-Jabber-Client ist" + +#: ../src/gtkgui_helpers.py:823 +msgid "Extension not supported" +msgstr "Erweiterung wird nicht unterstützt." + +#: ../src/gtkgui_helpers.py:824 +#, python-format +msgid "Image cannot be saved in %(type)s format. Save as %(new_filename)s?" +msgstr "" +"Bild kann nicht im %(type)s-Format gespeichert werden. Als %(new_filename)s " +"speichern?" + +#: ../src/gtkgui_helpers.py:833 +msgid "Save Image as..." +msgstr "Bild speichern unter ..." + +#: ../src/history_manager.py:91 +msgid "Cannot find history logs database" +msgstr "Kann Verlaufs-Datenkbank nicht finden" + +#. holds jid +#: ../src/history_manager.py:131 +msgid "Contacts" +msgstr "Kontakte" + +#. holds time +#: ../src/history_manager.py:144 ../src/history_manager.py:184 +#: ../src/history_window.py:98 +msgid "Date" +msgstr "Datum" + +#. holds nickname +#: ../src/history_manager.py:150 ../src/history_manager.py:202 +msgid "Nickname" +msgstr "Spitzname" + +#. holds message +#: ../src/history_manager.py:158 ../src/history_manager.py:190 +#: ../src/history_window.py:106 +msgid "Message" +msgstr "Nachricht" + +#: ../src/history_manager.py:210 +msgid "" +"Do you want to clean up the database? (STRONGLY NOT RECOMMENDED IF GAJIM IS " +"RUNNING)" +msgstr "" +"Möchten Sie die Datenbank aufräumen? (NICHT EMPFOHLEN, WENN GAJIM GERADE " +"LÄUFT)" + +#: ../src/history_manager.py:212 +msgid "" +"Normally allocated database size will not be freed, it will just become " +"reusable. If you really want to reduce database filesize, click YES, else " +"click NO.\n" +"\n" +"In case you click YES, please wait..." +msgstr "" +"Der reservierte Datenbankspeicherplatz wird nicht freigegeben, er wird nur " +"wiederverwendbar. Wenn Sie wirklich den Speicherplatz reduzieren möchten, " +"klicken Sie JA, ansonsten NEIN.\n" +"\n" +"Falls Sie JA klicken, warten Sie bitte einen Augenblick ..." + +#: ../src/history_manager.py:424 +msgid "Exporting History Logs..." +msgstr "Exportiere Verlauf ..." + +#: ../src/history_manager.py:499 +#, python-format +msgid "%(who)s on %(time)s said: %(message)s\n" +msgstr "%(who)s sagte um %(time)s: %(message)s\n" + +#: ../src/history_manager.py:536 +msgid "Do you really want to delete logs of the selected contact?" +msgid_plural "Do you really want to delete logs of the selected contacts?" +msgstr[0] "Möchten Sie wirklich alle Logs des ausgewählten Kontakts löschen?" +msgstr[1] "Möchten Sie wirklich alle Logs der ausgewählten Kontakte löschen?" + +#: ../src/history_manager.py:540 ../src/history_manager.py:575 +msgid "This is an irreversible operation." +msgstr "Dies ist ein unwiderruflicher Vorgang." + +#: ../src/history_manager.py:572 +msgid "Do you really want to delete the selected message?" +msgid_plural "Do you really want to delete the selected messages?" +msgstr[0] "Möchten Sie die ausgewählte Nachricht wirklich löschen?" +msgstr[1] "Möchten Sie die ausgewählten Nachrichten wirklich löschen?" + +#: ../src/history_window.py:288 +#, python-format +msgid "Conversation History with %s" +msgstr "Unterhaltungs-Verlauf mit %s" + +#: ../src/history_window.py:406 +#, python-format +msgid "%(nick)s is now %(status)s: %(status_msg)s" +msgstr "%(nick)s ist jezt %(status)s: %(status_msg)s" + +#: ../src/history_window.py:410 ../src/notify.py:229 +#, python-format +msgid "%(nick)s is now %(status)s" +msgstr "%(nick)s ist jetzt %(status)s" + +#: ../src/history_window.py:416 +#, python-format +msgid "Status is now: %(status)s: %(status_msg)s" +msgstr "Status ist jetzt: %(status)s: %(status_msg)s" + +#: ../src/history_window.py:419 +#, python-format +msgid "Status is now: %(status)s" +msgstr "Status ist jetzt: %(status)s" + +#: ../src/htmltextview.py:587 ../src/htmltextview.py:598 +msgid "Timeout loading image" +msgstr "Konnte Bild nicht laden" + +#: ../src/htmltextview.py:608 +msgid "Image is too big" +msgstr "Das Bild ist zu groß" + +#: ../src/message_window.py:422 +msgid "Chats" +msgstr "Chats" + +#: ../src/message_window.py:424 +msgid "Group Chats" +msgstr "Gruppenchat" + +#: ../src/message_window.py:426 +msgid "Private Chats" +msgstr "Private Chats" + +#: ../src/message_window.py:432 +msgid "Messages" +msgstr "Nachrichten" + +#: ../src/message_window.py:436 +#, python-format +msgid "%s - %s" +msgstr "%s - %s" + +#: ../src/negotiation.py:13 +msgid "- messages will be logged" +msgstr "- Nachrichten werden aufgezeichnet" + +#: ../src/negotiation.py:15 +msgid "- messages will not be logged" +msgstr "- Nachrichten werden nicht aufgezeichnet" + +#: ../src/negotiation.py:24 +msgid "OK to continue with negotiation?" +msgstr "In Ordnung mit der Verhandlung fortzufahren?" + +#: ../src/negotiation.py:25 +#, python-format +msgid "" +"You've begun an encrypted session with %s, but it can't be guaranteed that " +"you're talking directly to the person you think you are.\n" +"\n" +"You should speak with them directly (in person or on the phone) and confirm " +"that their Short Authentication String is identical to this one: %s\n" +"\n" +"Would you like to continue with the encrypted session?" +msgstr "" +"Sie haben eine verschlüsselte Sitzung mit %s begonnen, aber es kann nicht " +"garantiert werden, dass Sie tatsächlich mit der Person sprechen, die sie " +"vorgibt zu sein.\n" +"\n" +"Sie sollten mit der Person direkt (persönlich oder am Telefon) sprechen und " +"sich vergewissern, dass ihr kurzer Authentifizierungs-String mit diesem " +"übereinstimmt: %s\n" +"\n" +"Möchten Sie mit der verschlüsselten Sitzung fortfahren?" + +#: ../src/negotiation.py:31 +msgid "Yes, I verified the Short Authentication String" +msgstr "Ja, Ich habe den kurzen Authentifizierungs-String überprüft" + +#: ../src/notify.py:227 +#, python-format +msgid "%(nick)s Changed Status" +msgstr "%(nick)s änderte Status" + +#: ../src/notify.py:237 +#, python-format +msgid "%(nickname)s Signed In" +msgstr "%(nickname)s angemeldet" + +#: ../src/notify.py:245 +#, python-format +msgid "%(nickname)s Signed Out" +msgstr "%(nickname)s abgemeldet" + +#: ../src/notify.py:257 +#, python-format +msgid "New Single Message from %(nickname)s" +msgstr "Neue einzelne Nachricht von %(nickname)s" + +#: ../src/notify.py:265 +#, python-format +msgid "New Private Message from group chat %s" +msgstr "Neue private Nachricht von Gruppenchat %s" + +#: ../src/notify.py:267 +#, python-format +msgid "%(nickname)s: %(message)s" +msgstr "%(nickname)s: %(message)s" + +#: ../src/notify.py:270 +#, python-format +msgid "Messaged by %(nickname)s" +msgstr "Neue Nachricht von %(nickname)s" + +#: ../src/notify.py:276 +#, python-format +msgid "New Message from %(nickname)s" +msgstr "Neue Nachricht von %(nickname)s" + +#: ../src/osx/growler.py:10 +#, fuzzy +msgid "Generic" +msgstr "Allgemein" + +#: ../src/profile_window.py:54 +msgid "Retrieving profile..." +msgstr "Empfange Profil ..." + +#: ../src/profile_window.py:107 ../src/roster_window.py:2687 +msgid "File is empty" +msgstr "Datei ist leer" + +#: ../src/profile_window.py:110 ../src/roster_window.py:2690 +msgid "File does not exist" +msgstr "Datei existiert nicht" + +#. keep identation +#. unknown format +#: ../src/profile_window.py:124 ../src/profile_window.py:140 +#: ../src/roster_window.py:2692 ../src/roster_window.py:2703 +msgid "Could not load image" +msgstr "Konnte Bild nicht laden" + +#: ../src/profile_window.py:250 +msgid "Information received" +msgstr "Informationen empfangen" + +#: ../src/profile_window.py:319 +msgid "Without a connection you can not publish your contact information." +msgstr "" +"Sie müssen angemeldet sein, um Kontakt-Informationen zu veröffentlichen" + +#: ../src/profile_window.py:333 +msgid "Sending profile..." +msgstr "Sende Profil ..." + +#: ../src/profile_window.py:348 +msgid "Information NOT published" +msgstr "Informationen NICHT veröffentlicht" + +#: ../src/profile_window.py:355 +msgid "vCard publication failed" +msgstr "vCard Veröffentlichung fehlgeschlagen" + +#: ../src/profile_window.py:356 +msgid "" +"There was an error while publishing your personal information, try again " +"later." +msgstr "" +"Bei der Veröffentlichung Ihrer persönlichen Informationen ist ein Fehler " +"aufgetreten, versuchen Sie es später nochmals." + +#: ../src/roster_window.py:265 ../src/roster_window.py:886 +msgid "Merged accounts" +msgstr "Alle Konten" + +#: ../src/roster_window.py:1752 +msgid "Authorization has been sent" +msgstr "Autorisierung wurde erneut gesendet" + +#: ../src/roster_window.py:1753 +#, python-format +msgid "Now \"%s\" will know your status." +msgstr "\"%s\" kennt jetzt ihren Status." + +#: ../src/roster_window.py:1773 +msgid "Subscription request has been sent" +msgstr "Abonnement-Anforderung wurde gesendet" + +#: ../src/roster_window.py:1774 +#, python-format +msgid "If \"%s\" accepts this request you will know his or her status." +msgstr "Wenn \"%s\" diese Anfrage akzeptiert, erfahren Sie dessen Status." + +#: ../src/roster_window.py:1786 +msgid "Authorization has been removed" +msgstr "Autorisierung wurde entfernt" + +#: ../src/roster_window.py:1787 +#, python-format +msgid "Now \"%s\" will always see you as offline." +msgstr "\"%s\" wird Sie nun immer als offline sehen." + +#: ../src/roster_window.py:1808 +#, fuzzy +msgid "" +"Gnome Keyring is installed but not \t\t\t\t\t\t\tcorrectly started " +"(environment variable probably not \t\t\t\t\t\t\tcorrectly set)" +msgstr "" +"Gnome Keyring ist installiert, aber nicht korrekt gestartet\t\t\t\t\t\t\t\t" +"(Umgebungsvariablen wahrscheinlich falsch gesetzt)" + +#: ../src/roster_window.py:1829 +msgid "GPG is not usable" +msgstr "GPG ist nicht benutzbar" + +#: ../src/roster_window.py:1991 ../src/roster_window.py:3138 +msgid "You are participating in one or more group chats" +msgstr "Sie nehmen an einem oder mehreren Gruppenchats teil" + +#: ../src/roster_window.py:1992 ../src/roster_window.py:3139 +msgid "" +"Changing your status to invisible will result in disconnection from those " +"group chats. Are you sure you want to go invisible?" +msgstr "" +"Wenn Sie Ihren Status auf unsichtbar setzen, werden sie von diesen " +"Gruppenchats getrennt. Sind Sie sicher, dass sie unsichtbar werden möchten?" + +#: ../src/roster_window.py:2018 +msgid "desync'ed" +msgstr "" + +#: ../src/roster_window.py:2141 ../src/roster_window.py:2385 +msgid "You have unread messages" +msgstr "Sie haben ungelesene Nachrichten" + +#: ../src/roster_window.py:2142 +msgid "" +"Messages will only be available for reading them later if you have history " +"enabled and contact is in your roster." +msgstr "" +"Nachrichten werden nur für das spätere Lesen verfügbar sein, wenn der " +"Verlauf aktiviert wurde und der Kontakt sich in der Kontaktliste befindet." + +#: ../src/roster_window.py:2386 +msgid "You must read them before removing this transport." +msgstr "Sie müssen sie alle lesen, bevor der Account entfernt wird." + +#: ../src/roster_window.py:2389 +#, python-format +msgid "Transport \"%s\" will be removed" +msgstr "Transport \"%s\" wird entfernt" + +#: ../src/roster_window.py:2390 +msgid "" +"You will no longer be able to send and receive messages from contacts using " +"this transport." +msgstr "" +"Sie können nun keine Nachrichten mehr mit Kontakten von diesem Transport " +"austauschen." + +#: ../src/roster_window.py:2393 +msgid "Transports will be removed" +msgstr "Transporte werden entfernt" + +#: ../src/roster_window.py:2398 +#, python-format +msgid "" +"You will no longer be able to send and receive messages to contacts from " +"these transports: %s" +msgstr "" +"Sie können nun keine Nachrichten mehr mit Kontakten von diesen Transporten " +"austauschen: %s" + +#. it's jid +#: ../src/roster_window.py:2564 +msgid "Rename Contact" +msgstr "Kontakt umbenennen" + +#: ../src/roster_window.py:2565 +#, python-format +msgid "Enter a new nickname for contact %s" +msgstr "Geben Sie einen Spitznamen für den Kontakt %s ein" + +#: ../src/roster_window.py:2572 +msgid "Rename Group" +msgstr "Gruppe umbenennen" + +#: ../src/roster_window.py:2573 +#, python-format +msgid "Enter a new name for group %s" +msgstr "Geben Sie einen neuen Namen für die Gruppe %s ein" + +#: ../src/roster_window.py:2620 +msgid "Remove Group" +msgstr "Gruppe entfernen" + +#: ../src/roster_window.py:2621 +#, python-format +msgid "Do you want to remove group %s from the roster?" +msgstr "Möchten Sie wirklich die Gruppe %s von Ihrer Kontaktliste entfernen?" + +#: ../src/roster_window.py:2622 +msgid "Remove also all contacts in this group from your roster" +msgstr "Auch alle Kontakte dieser Gruppe von Ihrer Kontaktliste entfernen" + +#: ../src/roster_window.py:2653 +msgid "Assign OpenPGP Key" +msgstr "OpenPGP-Schlüssel Zuweisen" + +#: ../src/roster_window.py:2654 +msgid "Select a key to apply to the contact" +msgstr "Weisen Sie dem Kontakt einen Schüssel zu" + +#: ../src/roster_window.py:3036 +#, python-format +msgid "Contact \"%s\" will be removed from your roster" +msgstr "Kontakt \"%s\" wird von ihrer Kontaktliste entfernt" + +#: ../src/roster_window.py:3040 +msgid "" +"By removing this contact you also remove authorization resulting in him or " +"her always seeing you as offline." +msgstr "" +"Durch das Entfernen dieses Kontaktes entziehen Sie ihm auch die Berechtigung " +"Ihren Status zu sehen, wodurch der Kontakt Sie nur noch als offline sehen " +"wird." + +#: ../src/roster_window.py:3045 +msgid "" +"By removing this contact you also by default remove authorization resulting " +"in him or her always seeing you as offline." +msgstr "" +"Durch das Entfernen dieses Kontaktes entziehen Sie ihm auch standardmäßig " +"die Berechtigung Ihren Status zu sehen, wodurch der Kontakt Sie nur noch als " +"offline sehen wird." + +#: ../src/roster_window.py:3048 +msgid "I want this contact to know my status after removal" +msgstr "" +"Ich möchte, dass dieser Kontakt meinen Status auch nach dem Entfernen sieht" + +#. several contact to remove at the same time +#: ../src/roster_window.py:3052 +msgid "Contacts will be removed from your roster" +msgstr "Kontakte werden von Ihrer Kontaktliste entfernt" + +#: ../src/roster_window.py:3056 +#, python-format +msgid "" +"By removing these contacts:%s\n" +"you also remove authorization resulting in them always seeing you as offline." +msgstr "" +"Durch das Entfernen dieser Kontakte:%s\n" +"entziehen Sie ihnen auch die Berechtigung Ihren Status zu sehen, wodurch die " +"Kontakte Sie nur noch als offline sehen werden." + +#: ../src/roster_window.py:3094 +msgid "No account available" +msgstr "Kein Konto vorhanden" + +#: ../src/roster_window.py:3095 +msgid "You must create an account before you can chat with other contacts." +msgstr "" +"Sie müssen ein Konto erstellen, bevor Sie sich zum Jabber-Netzwerk verbinden " +"können." + +#: ../src/roster_window.py:3582 +msgid "Metacontacts storage not supported by your server" +msgstr "" +"Das Speichern von Metakontakten wird von Ihrem Server nicht unterstützt" + +#: ../src/roster_window.py:3584 +msgid "" +"Your server does not support storing metacontacts information. So those " +"information will not be saved on next reconnection." +msgstr "" +"Ihr Server unterstützt leider nicht das Speichern von Metakontakt-" +"Informationen. Aus diesem Grund werden diese Informationen bei der nächsten " +"Neuverbindung nicht gespeichert werden." + +#: ../src/roster_window.py:3659 +msgid "" +"You are about to create a metacontact. Are you sure you want to continue?" +msgstr "" +"Sie sind dabei einen Metakontakt zu erstellen. Wollen Sie wirklich " +"fortfahren?" + +#: ../src/roster_window.py:3661 +msgid "" +"Metacontacts are a way to regroup several contacts in one line. Generally it " +"is used when the same person has several Jabber accounts or transport " +"accounts." +msgstr "" +"Metakontakte sind eine Möglichkeit, mehrere Kontakte in einer Zeile zu " +"gruppieren. Normalerweise benutzt man Sie, wenn eine Person mehrere Jabber- " +"oder Transport-Konten hat." + +#: ../src/roster_window.py:3774 +msgid "Invalid file URI:" +msgstr "Ungültige Datei URI:" + +#: ../src/roster_window.py:3785 +#, python-format +msgid "Do you want to send this file to %s:" +msgid_plural "Do you want to send those files to %s:" +msgstr[0] "Wollen Sie die Datei an %s senden:" +msgstr[1] "Wollen Sie die Dateien an %s senden:" + +#. new chat +#. single message +#. for chat_with +#. for single message +#. join gc +#: ../src/roster_window.py:4298 ../src/roster_window.py:4308 +#: ../src/roster_window.py:4317 ../src/systray.py:212 ../src/systray.py:217 +#: ../src/systray.py:223 +#, python-format +msgid "using account %s" +msgstr "mit Konto %s" + +#. add +#: ../src/roster_window.py:4324 +#, python-format +msgid "to %s account" +msgstr "an Konto %s" + +#. disco +#: ../src/roster_window.py:4329 +#, python-format +msgid "using %s account" +msgstr "mit Konto %s" + +#: ../src/roster_window.py:4410 +msgid "_Manage Bookmarks..." +msgstr "Lesezeichen _verwalten ..." + +#. profile, avatar +#: ../src/roster_window.py:4429 +#, python-format +msgid "of account %s" +msgstr "von Konto %s" + +#: ../src/roster_window.py:4469 +#, python-format +msgid "for account %s" +msgstr "für Konto %s" + +#: ../src/roster_window.py:4523 ../src/roster_window.py:4637 +msgid "_Change Status Message" +msgstr "Ändere _Statusnachricht" + +#: ../src/roster_window.py:4554 +#, fuzzy +msgid "Publish Tune" +msgstr "_Veröffentliche Musiktitel" + +#: ../src/roster_window.py:4559 +msgid "Mood" +msgstr "" + +#: ../src/roster_window.py:4563 +msgid "Activity" +msgstr "Aktivität" + +#: ../src/roster_window.py:4568 +msgid "Configure Services..." +msgstr "Dienste konfigurieren ..." + +#: ../src/roster_window.py:4726 +msgid "_Maximize All" +msgstr "Alle _maximieren" + +#. Send Group Message +#: ../src/roster_window.py:4734 ../src/roster_window.py:5219 +msgid "Send Group M_essage" +msgstr "_Sende Nachricht an Gruppe" + +#: ../src/roster_window.py:4742 +msgid "To all users" +msgstr "An alle Benutzern" + +#: ../src/roster_window.py:4746 +msgid "To all online users" +msgstr "An alle angemeldeten Benutzer" + +#: ../src/roster_window.py:5135 +msgid "I would like to add you to my roster" +msgstr "Ich würde dich gerne in meine Liste aufnehmen" + +#. Manage Transport submenu +#: ../src/roster_window.py:5239 +msgid "_Manage Contacts" +msgstr "Kontakte verwalten" + +#. Send single message +#: ../src/roster_window.py:5300 +msgid "Send Single Message" +msgstr "Sende _einzelne Nachricht" + +#. Manage Transport submenu +#: ../src/roster_window.py:5356 +msgid "_Manage Transport" +msgstr "Transports" + +#. Modify Transport +#: ../src/roster_window.py:5364 +msgid "_Modify Transport" +msgstr "Trans_port ändern" + +#: ../src/roster_window.py:5439 +msgid "_Maximize" +msgstr "_Maximieren" + +#: ../src/roster_window.py:5446 +msgid "_Disconnect" +msgstr "_Verbindung trennen" + +#: ../src/roster_window.py:5523 +msgid "_New Group Chat" +msgstr "Neuer Gruppenchat" + +#. History manager +#: ../src/roster_window.py:5623 +msgid "History Manager" +msgstr "_Verlaufsmanager" + +#: ../src/roster_window.py:5632 +msgid "_Join New Group Chat" +msgstr "_Gruppenchat betreten" + +#: ../src/roster_window.py:5840 +msgid "Change Status Message..." +msgstr "Ändere Statusnachricht ..." + +#: ../src/search_window.py:91 +msgid "Waiting for results" +msgstr "Warte auf Ergebnisse" + +#: ../src/search_window.py:131 ../src/search_window.py:209 +msgid "Error in received dataform" +msgstr "Fehler den empfangenen Daten" + +#. No result +#: ../src/search_window.py:165 ../src/search_window.py:201 +msgid "No result" +msgstr "Kein Ergebnis" + +#: ../src/secrets.py:45 +msgid "" +"To continue, Gajim needs to access your stored secrets. Enter your passphrase" +msgstr "" +"Um fortzufahren benötigt Gajim Zugriff auf Ihre gespeicherten Passwörter. " +"Geben Sie Ihre Passphrase ein" + +#: ../src/secrets.py:89 +msgid "Confirm Passphrase" +msgstr "Passphrase bestätigen" + +#: ../src/secrets.py:90 +msgid "Enter your new passphrase again for confirmation" +msgstr "Passphrase zur Bestätigung erneut eingeben" + +#: ../src/secrets.py:95 ../src/secrets.py:107 +msgid "Create Passphrase" +msgstr "Passphrase erstellen" + +#: ../src/secrets.py:96 +msgid "Passphrases did not match.\n" +msgstr "Passphrasen stimmen nicht überein.\n" + +#: ../src/secrets.py:97 ../src/secrets.py:108 +msgid "Gajim needs you to create a passphrase to encrypt stored secrets" +msgstr "" +"Sie müssen eine Passphrase angeben um Ihre gespeicherten Passwörter zu " +"verschlüsseln" + +#: ../src/systray.py:169 +msgid "_Change Status Message..." +msgstr "Ändere _Statusnachricht ..." + +#: ../src/systray.py:254 +msgid "Hide this menu" +msgstr "Versteckt dieses Menü" + +#: ../src/tooltips.py:317 ../src/tooltips.py:512 +msgid "Jabber ID: " +msgstr "Jabber-ID:" + +#: ../src/tooltips.py:320 ../src/tooltips.py:516 +msgid "Resource: " +msgstr "Ressource: " + +#: ../src/tooltips.py:325 +#, python-format +msgid "%(owner_or_admin_or_member)s of this group chat" +msgstr "%(owner_or_admin_or_member)s dieses Gruppenchats" + +#: ../src/tooltips.py:422 +msgid " [blocked]" +msgstr " [blockiert]" + +#: ../src/tooltips.py:426 +msgid " [minimized]" +msgstr " [minimiert]" + +#: ../src/tooltips.py:441 ../src/tooltips.py:683 +msgid "Status: " +msgstr "Status: " + +#: ../src/tooltips.py:472 +#, python-format +msgid "Last status: %s" +msgstr "Letzter Status: %s" + +#: ../src/tooltips.py:474 +#, python-format +msgid " since %s" +msgstr " seit %s" + +#: ../src/tooltips.py:492 +msgid "Connected" +msgstr "Verbunden" + +#: ../src/tooltips.py:494 +msgid "Disconnected" +msgstr "Nicht verbunden" + +#. ('both' is the normal sub so we don't show it) +#: ../src/tooltips.py:523 +msgid "Subscription: " +msgstr "Abonnement: " + +#: ../src/tooltips.py:533 +msgid "OpenPGP: " +msgstr "OpenPGP: " + +#: ../src/tooltips.py:576 +msgid "Mood:" +msgstr "Stimmung:" + +#: ../src/tooltips.py:586 +msgid "Activity:" +msgstr "Aktivität:" + +#: ../src/tooltips.py:604 +msgid "Unknown Artist" +msgstr "Unbekannter Künstler" + +#: ../src/tooltips.py:609 +msgid "Unknown Title" +msgstr "Unbekannter Titel" + +#: ../src/tooltips.py:614 +msgid "Unknown Source" +msgstr "Unbekannte Quelle" + +#: ../src/tooltips.py:615 +#, fuzzy +msgid "Tune:" +msgstr "Musiktitel:" + +#: ../src/tooltips.py:615 +#, python-format +msgid "" +"\"%(title)s\" by %(artist)s\n" +"from %(source)s" +msgstr "" +"\"%(title)s\" von %(artist)s\n" +"von %/source)s" + +#: ../src/tooltips.py:639 +msgid "Download" +msgstr "Download" + +#: ../src/tooltips.py:645 +msgid "Upload" +msgstr "Upload" + +#: ../src/tooltips.py:652 +msgid "Type: " +msgstr "Typ: " + +#: ../src/tooltips.py:658 +msgid "Transferred: " +msgstr "Übertragen: " + +#: ../src/tooltips.py:661 ../src/tooltips.py:682 +msgid "Not started" +msgstr "Nicht gestartet" + +#: ../src/tooltips.py:665 +msgid "Stopped" +msgstr "Angehalten" + +#: ../src/tooltips.py:667 ../src/tooltips.py:670 +msgid "Completed" +msgstr "Abgeschlossen" + +#: ../src/tooltips.py:674 +msgid "?transfer status:Paused" +msgstr "?Transferstatus:Pausiert" + +#. stalled is not paused. it is like 'frozen' it stopped alone +#: ../src/tooltips.py:678 +msgid "Stalled" +msgstr "Steht still" + +#: ../src/tooltips.py:680 +msgid "Transferring" +msgstr "Übertrage" + +#: ../src/tooltips.py:716 +msgid "This service has not yet responded with detailed information" +msgstr "Dieser Dienst hat nicht mit detaillierten Informationen geantwortet" + +#: ../src/tooltips.py:719 +msgid "" +"This service could not respond with detailed information.\n" +"It is most likely legacy or broken" +msgstr "" +"Dieser Dienst konnte nicht mit detaillierten Informationen antworten\n" +"Er ist wahrscheinlich verwaltet oder defekt" + +#: ../src/vcard.py:243 +msgid "?Client:Unknown" +msgstr "Unbekannt" + +#: ../src/vcard.py:245 +msgid "?OS:Unknown" +msgstr "Unbekannt" + +#: ../src/vcard.py:270 ../src/vcard.py:280 ../src/vcard.py:479 +#, python-format +msgid "since %s" +msgstr "seit %s" + +#: ../src/vcard.py:309 +#, fuzzy +msgid "Affiliation:" +msgstr "Anwendungen" + +#: ../src/vcard.py:317 +msgid "" +"This contact is interested in your presence information, but you are not " +"interested in his/her presence" +msgstr "" +"Dieser Kontakt ist an Ihren Anwesenheitsinformationen interessiert, aber Sie " +"sind nicht an seiner/ihrer Anwesenheit interessiert" + +#: ../src/vcard.py:319 +msgid "" +"You are interested in the contact's presence information, but he/she is not " +"interested in yours" +msgstr "" +"Sie sind an den Anwesenheitsinformationen des Kontakts interessiert, aber er/" +"sie ist nicht an ihren interessiert" + +#: ../src/vcard.py:321 +msgid "You and the contact are interested in each other's presence information" +msgstr "" +"Sie und der Kontakt sind an den Anwesenheitsinformationen des Anderen " +"interessiert" + +#. None +#: ../src/vcard.py:323 +msgid "" +"You are not interested in the contact's presence, and neither he/she is " +"interested in yours" +msgstr "" +"Sie sind nicht an der Anwesenheit des Kontakts interessiert, und er/sie ist " +"nicht interessiert an ihrer" + +#: ../src/vcard.py:330 +msgid "You are waiting contact's answer about your subscription request" +msgstr "Sie warten auf die Antwort des Kontaktes auf ihre Abonnement-Anfrage" + +#: ../src/vcard.py:332 +msgid "There is no pending subscription request." +msgstr "" + +#: ../src/vcard.py:337 ../src/vcard.py:374 ../src/vcard.py:504 +msgid " resource with priority " +msgstr " resource mit Priorität " + +#~ msgid "Click to see past conversation in this room" +#~ msgstr "Klicken, um die früheren Unterhaltungen in diesem Raum zu sehen" + +#~ msgid "Publish and Subscribe" +#~ msgstr "Veröffentlichen und Abonnieren" + +#~ msgid "Allow others to see your:" +#~ msgstr "Erlaube anderen zu sehen:" + +#~ msgid "Receive your contact's:" +#~ msgstr "Empfang von Kontakten:" + +#~ msgid "Tune" +#~ msgstr "Musiktitel:" + +#~ msgid "The following message was NOT encrypted" +#~ msgstr "[Die folgende Nachricht wurde nicht verschlüsselt]" + +#~ msgid "Requires pyotr and libotr." +#~ msgstr "Erfordert pyotr und libotr." + #~ msgid "History Viewer" #~ msgstr "Verlauf ansehen" @@ -9158,43 +9239,6 @@ msgstr "Fehler beim Hinzufügen des Dienstes. %s" #~ "Wenn aktiviert, kann Gajim regelmäßig ein Last.FM Konto abfragen und " #~ "kürzlich gespielte Songs über PEP senden." -#~ msgid "_Configure" -#~ msgstr "_Einstellen" - -#~ msgid "gtk-delete" -#~ msgstr "gtk-delete" - -#~ msgid "" -#~ "To continue, Gajim needs to access your stored secrets. Enter your " -#~ "passphrase" -#~ msgstr "" -#~ "Um fortzufahren benötigt Gajim Zugriff auf Ihre gespeicherten Passwörter. " -#~ "Geben Sie Ihre Passphrase ein" - -#~ msgid "Confirm Passphrase" -#~ msgstr "Passphrase bestätigen" - -#~ msgid "Enter your new passphrase again for confirmation" -#~ msgstr "Passphrase zur Bestätigung erneut eingeben" - -#~ msgid "Create Passphrase" -#~ msgstr "Passphrase erstellen" - -#~ msgid "Passphrases did not match.\n" -#~ msgstr "Passphrasen stimmen nicht überein.\n" - -#~ msgid "Gajim needs you to create a passphrase to encrypt stored secrets" -#~ msgstr "" -#~ "Sie müssen eine Passphrase angeben um Ihre gespeicherten Passwörter zu " -#~ "verschlüsseln" - -#, fuzzy -#~ msgid "Generic" -#~ msgstr "Allgemein" - -#~ msgid "Select the account with which to synchronise" -#~ msgstr "Wählen Sie das Benutzerkonto mit welchem synchronisiert werden soll" - #~ msgid "%s has not broadcast an OpenPGP key, nor has one been assigned" #~ msgstr "" #~ "%s hat keinen OpenPGP-Schlüssel verbreitet und es wurde keiner zugewiesen" @@ -9230,9 +9274,6 @@ msgstr "Fehler beim Hinzufügen des Dienstes. %s" #~ "fragen, wenn Sie Ihren Status zu Abwesend ändern; es wird die hier " #~ "definierte Nachricht verwendet." -#~ msgid "Default Status Messages" -#~ msgstr "Vorgegebene Status-Nachrichten" - #~ msgid "" #~ "Determined by sender\n" #~ "Chat message\n" @@ -9254,9 +9295,6 @@ msgstr "Fehler beim Hinzufügen des Dienstes. %s" #~ msgid "Publish _Mood" #~ msgstr "_Veröffentliche Stimmung" -#~ msgid "Publish _Tune" -#~ msgstr "_Veröffentliche Musiktitel" - #~ msgid "Set status message to reflect currently playing _music track" #~ msgstr "Setze Status-Nachricht auf den aktuell spielenden _Musiktitel" @@ -9611,7 +9649,7 @@ msgstr "Fehler beim Hinzufügen des Dienstes. %s" #~ msgid "Unable to check fingerprint for %s. Connection could be insecure." #~ msgstr "" -#~ "Konnte Fingerabdruck von %s nicht überprüfen. Die Verbindung ist " +#~ "Konnte Fingerprint von %s nicht überprüfen. Die Verbindung ist " #~ "möglicherweise unsicher." #~ msgid "Missing fingerprint in SSL connection to %s" diff --git a/po/fr.po b/po/fr.po index 84af56af7..f59491067 100644 --- a/po/fr.po +++ b/po/fr.po @@ -1297,16 +1297,15 @@ msgstr "_Gérer le Salon" #: ../data/glade/gc_control_popup_menu.glade.h:9 msgid "_Minimize on close" -msgstr "_Minimiser à la fermeteure" +msgstr "_Minimiser à la fermeture" #: ../data/glade/gc_occupants_menu.glade.h:1 msgid "Mo_derator" msgstr "Mo_dérer" #: ../data/glade/gc_occupants_menu.glade.h:2 -#, fuzzy msgid "Occupant Actions" -msgstr "Actions des occupants" +msgstr "Actions des Occupants" #: ../data/glade/gc_occupants_menu.glade.h:5 msgid "_Admin" @@ -1538,52 +1537,44 @@ msgid "_Host:" msgstr "_Serveur :" #: ../data/glade/message_window.glade.h:1 -#, fuzzy msgid "Add this contact to roster (Ctrl-D)" -msgstr "Ajouter le contact à la liste" +msgstr "Ajouter le contact à la liste (Ctrl-D)" #: ../data/glade/message_window.glade.h:2 -#, fuzzy msgid "Bookmark this room (Ctrl-B)" -msgstr "Enregistrer ce salon dans les marque-pages" +msgstr "Enregistrer ce salon dans les marque-pages (Ctrl-B)" #: ../data/glade/message_window.glade.h:3 msgid "Browse the chat history (Ctrl-H)" msgstr "Parcourir l'historique des conversations (Ctrl+H)" #: ../data/glade/message_window.glade.h:4 -#, fuzzy msgid "Change the room's subject (Ctrl-T)" -msgstr "Changer le sujet du salon" +msgstr "Changer le sujet du salon (Ctrl-T)" #: ../data/glade/message_window.glade.h:5 -#, fuzzy msgid "Change your nickname (Ctrl-N)" -msgstr "Changer votre surnom" +msgstr "Changer votre surnom (Ctrl-N)" #: ../data/glade/message_window.glade.h:6 -#, fuzzy msgid "Invite contacts to the conversation (Ctrl-G)" -msgstr "Inviter des contacts dans la conversation" +msgstr "Inviter des contacts dans la conversation (Ctrl-G)" #: ../data/glade/message_window.glade.h:7 -#, fuzzy msgid "Send a file (Ctrl-F)" -msgstr "Envoyer un fichier" +msgstr "Envoyer un fichier (Ctrl-F)" #: ../data/glade/message_window.glade.h:8 msgid "Show a list of emoticons (Alt-M)" msgstr "Afficher une liste des émoticônes (Alt+M)" #: ../data/glade/message_window.glade.h:9 -#, fuzzy msgid "Show a menu of advanced functions (Alt-A)" -msgstr "Affiche un menu de fonctions avancées" +msgstr "Affiche un menu de fonctions avancées (Alt-A)" #: ../data/glade/message_window.glade.h:10 -#, fuzzy msgid "Show the contact's profile (Ctrl-I)" -msgstr "Afficher le profil du contact (Ctrl+I)" +msgstr "Afficher le profil du contact (Ctrl-I)" #. Make sure the character after "_" is not M/m (conflicts with Alt+M that is supposed to show the Emoticon Selector) #: ../data/glade/message_window.glade.h:12 @@ -4509,7 +4500,6 @@ msgstr "" "« use_latex » à True dans l'éditeur de configuration avancée." #: ../src/features_window.py:98 -#, fuzzy msgid "End to End Encryption" msgstr "Chiffrement de bout en bout" diff --git a/src/chat_control.py b/src/chat_control.py index 9f6b9093d..127e3dc95 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -619,15 +619,16 @@ class ChatControlBase(MessageControl): '''Send the given message to the active tab. Doesn't return None if error ''' if not message or message == '\n': - return 1 + return None + + ret = None if not process_command or not self._process_command(message): ret = MessageControl.send_message(self, message, keyID, type = type, chatstate = chatstate, msg_id = msg_id, composing_xep = composing_xep, resource = resource, user_nick = self.user_nick) - if ret: - return ret + # Record message history self.save_sent_message(message) @@ -638,6 +639,8 @@ class ChatControlBase(MessageControl): message_buffer = self.msg_textview.get_buffer() message_buffer.set_text('') # clear message buffer (and tv of course) + return ret + def save_sent_message(self, message): # save the message, so user can scroll though the list with key up/down size = len(self.sent_history) @@ -1112,7 +1115,9 @@ class ChatControl(ChatControlBase): self.on_avatar_eventbox_button_press_event) self.handlers[id] = widget - self.set_session(session) + self.session = session + if session: + session.control = self # Enable ecryption if needed e2e_is_active = hasattr(self, 'session') and self.session and self.session.enable_encryption @@ -1133,9 +1138,6 @@ class ChatControl(ChatControlBase): self.status_tooltip = gtk.Tooltips() - if gajim.otr_module: - self.update_otr(True) - self.update_ui() # restore previous conversation self.restore_conversation() @@ -1205,52 +1207,6 @@ class ChatControl(ChatControlBase): # The name banner is drawn here ChatControlBase.update_ui(self) - def get_otr_status(self): - if not self.session: - return 0 - - ctx = gajim.otr_module.otrl_context_find( - self.session.conn.otr_userstates, - self.contact.get_full_jid().encode(), - gajim.get_jid_from_account(self.account).encode(), - gajim.OTR_PROTO, 1, (gajim.otr_add_appdata, - self.account))[0] - - if ctx.msgstate == gajim.otr_module.OTRL_MSGSTATE_ENCRYPTED: - if ctx.active_fingerprint.trust: - return 2 - else: - return 1 - elif ctx.msgstate == gajim.otr_module.OTRL_MSGSTATE_FINISHED: - return 3 - return 0 - - def update_otr(self, print_status=False): - otr_status_text = '' - otr_status = self.get_otr_status() - authenticated = False - - if otr_status > 0: - enc_status = True - else: - enc_status = False - - if otr_status == 1: - otr_status_text = u'*unauthenticated* secure OTR ' + \ - u'connection' - elif otr_status == 2: - otr_status_text = u'authenticated secure OTR ' + \ - u'connection' - authenticated = True - elif otr_status == 3: - otr_status_text = u'finished OTR connection' - - self._show_lock_image(enc_status, u'OTR', enc_status, True, - authenticated) - if print_status and otr_status_text != '': - self.print_conversation_line(u'[OTR] %s' % \ - otr_status_text, 'status', '', None) - def _update_banner_state_image(self): contact = gajim.contacts.get_contact_with_highest_priority(self.account, self.contact.jid) @@ -1497,7 +1453,7 @@ class ChatControl(ChatControlBase): def send_message(self, message, keyID = '', chatstate = None): '''Send a message to contact''' if message in ('', None, '\n') or self._process_command(message): - return + return None # Do we need to process command for the message ? process_command = True @@ -1540,7 +1496,7 @@ class ChatControl(ChatControlBase): # if peer supports jep85 and we are not 'ask', send 'active' # NOTE: first active and 'ask' is set in gajim.py elif composing_xep is not False: - #send active chatstate on every message (as JEP says) + # send active chatstate on every message (as XEP says) chatstate_to_send = 'active' contact.our_chatstate = 'active' @@ -1548,8 +1504,9 @@ class ChatControl(ChatControlBase): gobject.source_remove(self.possible_inactive_timeout_id) self._schedule_activity_timers() - if not ChatControlBase.send_message(self, message, keyID, type = 'chat', - chatstate = chatstate_to_send, composing_xep = composing_xep, + if ChatControlBase.send_message(self, message, keyID, + type = 'chat', chatstate = chatstate_to_send, + composing_xep = composing_xep, process_command = process_command): self.print_conversation(message, self.contact.jid, encrypted = encrypted) @@ -1660,15 +1617,6 @@ class ChatControl(ChatControlBase): 'NOT encrypted') ChatControlBase.print_conversation_line( self, msg, 'status', '', tim) - elif gajim.otr_module and self.get_otr_status() > 0: - # OTR - # TODO: This is not shown when the window - # isn't open - needs fixing! - if not encrypted and frm == '': - msg = _('The following message was ' + \ - 'NOT encrypted') - ChatControlBase.print_conversation_line( - self, msg, 'status', '', tim) else: # GPG encryption if encrypted and not self.gpg_is_active: @@ -1794,11 +1742,6 @@ class ChatControl(ChatControlBase): history_menuitem = xml.get_widget('history_menuitem') toggle_gpg_menuitem = xml.get_widget('toggle_gpg_menuitem') toggle_e2e_menuitem = xml.get_widget('toggle_e2e_menuitem') - otr_submenu = xml.get_widget('otr_submenu') - otr_settings_menuitem = xml.get_widget('otr_settings_menuitem') - smp_otr_menuitem = xml.get_widget('smp_otr_menuitem') - start_otr_menuitem = xml.get_widget('start_otr_menuitem') - end_otr_menuitem = xml.get_widget('end_otr_menuitem') send_file_menuitem = xml.get_widget('send_file_menuitem') information_menuitem = xml.get_widget('information_menuitem') convert_to_gc_menuitem = xml.get_widget('convert_to_groupchat') @@ -1891,32 +1834,6 @@ class ChatControl(ChatControlBase): self._on_convert_to_gc_menuitem_activate) self.handlers[id] = convert_to_gc_menuitem - if gajim.otr_module: - otr_submenu.set_sensitive(True) - id = otr_settings_menuitem.connect('activate', - self._on_otr_settings_menuitem_activate) - self.handlers[id] = otr_settings_menuitem - id = start_otr_menuitem.connect('activate', - self._on_start_otr_menuitem_activate) - self.handlers[id] = start_otr_menuitem - id = end_otr_menuitem.connect('activate', - self._on_end_otr_menuitem_activate) - self.handlers[id] = end_otr_menuitem - id = smp_otr_menuitem.connect('activate', - self._on_smp_otr_menuitem_activate) - self.handlers[id] = smp_otr_menuitem - - ctx = gajim.otr_module.otrl_context_find(gajim.connections[self.account].otr_userstates, - self.contact.get_full_jid().encode(), - gajim.get_jid_from_account(self.account).encode(), gajim.OTR_PROTO, 1, - (gajim.otr_add_appdata, self.account))[0] - # can end only when PLAINTEXT - end_otr_menuitem.set_sensitive(ctx.msgstate != - gajim.otr_module.OTRL_MSGSTATE_PLAINTEXT) - # can SMP only when ENCRYPTED - smp_otr_menuitem.set_sensitive(ctx.msgstate == - gajim.otr_module.OTRL_MSGSTATE_ENCRYPTED) - menu.connect('selection-done', self.destroy_menu, send_file_menuitem, convert_to_gc_menuitem, information_menuitem, history_menuitem) @@ -2037,7 +1954,7 @@ class ChatControl(ChatControlBase): # Clean events gajim.events.remove_events(self.account, self.get_full_jid(), types = ['printed_' + self.type_id, self.type_id]) - # remove all register handlers on wigets, created by self.xml + # remove all register handlers on widgets, created by self.xml # to prevent circular references among objects for i in self.handlers.keys(): if self.handlers[i].handler_is_connected(i): @@ -2404,28 +2321,6 @@ class ChatControl(ChatControlBase): # XXX decide whether to use 4 or 3 message negotiation self.session.negotiate_e2e(False) - def _on_start_otr_menuitem_activate(self, widget): - # ?OTR? gets replaced with a better message internally in otrl_message_sending - MessageControl.send_message(self, u'?OTR?', type='chat') - def _on_end_otr_menuitem_activate(self, widget): - fjid = self.contact.get_full_jid() - gajim.otr_module.otrl_message_disconnect( - self.session.conn.otr_userstates, (gajim.otr_ui_ops, - {'account': self.account, 'urgent': True}), - gajim.get_jid_from_account(self.account).encode(), - gajim.OTR_PROTO, fjid.encode()) - gajim.otr_ui_ops.gajim_log(_('Private conversation with ' \ - '%s lost.') % fjid, self.account, fjid.encode()) - self.update_otr() - def _on_otr_settings_menuitem_activate(self, widget): - gajim.otr_windows.ContactOtrWindow(self.contact, self.account, self) - def _on_smp_otr_menuitem_activate(self, widget): - ctx = gajim.otr_module.otrl_context_find(gajim.connections[self.account].otr_userstates, - self.contact.get_full_jid().encode(), - gajim.get_jid_from_account(self.account).encode(), gajim.OTR_PROTO, 1, - (gajim.otr_add_appdata, self.account))[0] - ctx.app_data.show(False) - def got_connected(self): ChatControlBase.got_connected(self) # Refreshing contact diff --git a/src/common/config.py b/src/common/config.py index 50f9e91e2..9b950705a 100644 --- a/src/common/config.py +++ b/src/common/config.py @@ -250,6 +250,7 @@ class Config: 'max_conversation_lines': [opt_int, 500, _('Maximum number of lines that are printed in conversations. Oldest lines are cleared.')], 'attach_notifications_to_systray': [opt_bool, False, _('If True, notification windows from notification-daemon will be attached to systray icon.')], 'check_idle_every_foo_seconds': [opt_int, 2, _('Choose interval between 2 checks of idleness.')], + 'latex_png_dpi': [opt_str, '108',_('Change the value to change the size of latex formulas displayed. The higher is larger.') ], } __options_per_key = { @@ -310,7 +311,8 @@ class Config: 'zeroconf_jabber_id': [ opt_str, '', '', True ], 'zeroconf_email': [ opt_str, '', '', True ], 'use_env_http_proxy' : [opt_bool, False], - 'otr_flags': [opt_int, 58 ], + 'answer_receipt' : [opt_bool, True, _('Answer to receipt requests')], + 'request_receipt' : [opt_bool, True, _('Sent receipt requests')], 'publish_mood': [opt_bool, True], 'publish_activity': [opt_bool, True], 'publish_tune': [opt_bool, False], @@ -369,7 +371,6 @@ class Config: 'contacts': ({ 'gpg_enabled': [ opt_bool, False, _('Is OpenPGP enabled for this contact?')], 'speller_language': [ opt_str, '', _('Language for which we want to check misspelled words')], - 'otr_flags': [opt_int, -1 ], }, {}), 'rooms': ({ 'speller_language': [ opt_str, '', _('Language for which we want to check misspelled words')], diff --git a/src/common/connection.py b/src/common/connection.py index e9ffba607..493140fe0 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -364,9 +364,9 @@ class Connection(ConnectionHandlers): # data is (dict) self.dispatch('PRIVACY_LISTS_ACTIVE_DEFAULT', (data)) elif realm == '': - if event == common.xmpp.transports.DATA_RECEIVED: + if event == common.xmpp.transports_nb.DATA_RECEIVED: self.dispatch('STANZA_ARRIVED', unicode(data, errors = 'ignore')) - elif event == common.xmpp.transports.DATA_SENT: + elif event == common.xmpp.transports_nb.DATA_SENT: self.dispatch('STANZA_SENT', unicode(data)) def select_next_host(self, hosts): @@ -896,20 +896,6 @@ class Connection(ConnectionHandlers): self.on_connect_auth = self._init_roster self.connect_and_auth() - if gajim.otr_module: - try: - gajim.otr_module.otrl_privkey_read(self.otr_userstates, - os.path.join(gajim.gajimpaths.root, - '%s.key' % self.name).encode()) - gajim.otr_module.otrl_privkey_read_fingerprints( - self.otr_userstates, os.path.join( - gajim.gajimpaths.root, '%s.fpr' % - self.name).encode(), - (gajim.otr_add_appdata, self.name)) - except Exception, e: - if not hasattr(e, 'os_errno') or e.os_errno != 2: - raise - def _init_roster(self, con): self.connection = con if not self.connection: @@ -1123,6 +1109,14 @@ class Connection(ConnectionHandlers): namespace=common.xmpp.NS_ADDRESS) addresses.addChild('address', attrs = {'type': 'ofrom', 'jid': forward_from}) + + # TODO: We should also check if the other end supports it + # as XEP 0184 says checking is a SHOULD. Maybe we should + # implement section 6 of the XEP as well? + if msgtxt and gajim.config.get_per('accounts', self.name, + 'request_receipt'): + msg_iq.setTag('request', namespace='urn:xmpp:receipts') + if session: # XEP-0201 session.last_send = time.time() @@ -1132,8 +1126,7 @@ class Connection(ConnectionHandlers): if session.enable_encryption: msg_iq = session.encrypt_stanza(msg_iq) - - self.connection.send(msg_iq) + msg_id = self.connection.send(msg_iq) if not forward_from and session and session.is_loggable(): no_log_for = gajim.config.get_per('accounts', self.name, 'no_log_for')\ .split() @@ -1155,6 +1148,8 @@ class Connection(ConnectionHandlers): self.dispatch('ERROR', (_('Disk Write Error'), str(e))) self.dispatch('MSGSENT', (jid, msg, keyID)) + return msg_id + def send_stanza(self, stanza): ''' send a stanza untouched ''' if not self.connection: @@ -1532,6 +1527,15 @@ class Connection(ConnectionHandlers): # disconnect from jabber server self.connection.send(p) + def gc_got_disconnected(self, room_jid): + ''' A groupchat got disconnected. This can be or purpose or not. + Save the time we quit to avoid duplicate logs AND be faster than get that + date from DB. Save it in mem AND in a small table (with fast access) + ''' + log_time = time_time() + self.last_history_time[room_jid] = log_time + gajim.logger.set_room_last_message_time(room_jid, log_time) + def gc_set_role(self, room_jid, nick, role, reason = ''): '''role is for all the life of the room so it's based on nick''' if not self.connection: diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index 816577f4e..25a771e05 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -50,7 +50,8 @@ if dbus_support.supported: from music_track_listener import MusicTrackListener from session import ChatControlSession -import tictactoe + +gajim.default_session_type = ChatControlSession STATUS_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd', 'invisible', 'error'] @@ -1217,9 +1218,6 @@ class ConnectionHandlersBase: # keep track of sessions this connection has with other JIDs self.sessions = {} - if gajim.otr_module: - self.otr_userstates = gajim.otr_module.otrl_userstate_create() - def _FeatureNegCB(self, con, stanza, session): gajim.log.debug('FeatureNegCB') feature = stanza.getTag(name='feature', namespace=common.xmpp.NS_FEATURE) @@ -1301,8 +1299,9 @@ sent a message to.''' # sessions that we haven't received a thread ID in idless = filter(lambda s: not s.received_thread_id, sessions) - # filter out everything exceptthe default session type - chat_sessions = filter(lambda s: isinstance(s, ChatControlSession), idless) + # filter out everything except the default session type + p = lambda s: isinstance(s, gajim.default_session_type) + chat_sessions = filter(p, idless) if chat_sessions: # return the session that we last sent a message in @@ -1311,10 +1310,25 @@ sent a message to.''' else: return None - # if deferred is true, the thread ID we're generating is tem + def find_controlless_session(self, jid): + '''find an active session that doesn't have a control attached''' + + try: + sessions = self.sessions[jid].values() + + # filter out everything except the default session type + p = lambda s: isinstance(s, gajim.default_session_type) + chat_sessions = filter(p, sessions) + + orphaned = filter(lambda s: not s.control, chat_sessions) + + return orphaned[0] + except KeyError: + return None + def make_new_session(self, jid, thread_id=None, type='chat', cls=None): if not cls: - cls = ChatControlSession + cls = gajim.default_session_type # determine if this session is a pm session # if not, discard the resource @@ -1354,6 +1368,9 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, except: HAS_IDLE = False + self.gmail_last_tid = None + self.gmail_last_time = None + def build_http_auth_answer(self, iq_obj, answer): if answer == 'yes': self.connection.send(iq_obj.buildReply('result')) @@ -1565,9 +1582,14 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, jid = gajim.get_jid_from_account(self.name) gajim.log.debug('Got notification of new gmail e-mail on %s. Asking the server for more info.' % jid) iq = common.xmpp.Iq(typ = 'get') - iq.setAttr('id', '13') + iq.setID(self.connection.getAnID()) query = iq.setTag('query') query.setNamespace(common.xmpp.NS_GMAILNOTIFY) + # we want only be notified about newer mails + if self.gmail_last_tid: + query.setAttr('newer-than-tid', self.gmail_last_tid) + if self.gmail_last_time: + query.setAttr('newer-than-time', self.gmail_last_time) self.connection.send(iq) raise common.xmpp.NodeProcessed @@ -1584,16 +1606,34 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, if gm.getTag('mailbox').getTag('mail-thread-info'): gmail_messages = gm.getTag('mailbox').getTags('mail-thread-info') for gmessage in gmail_messages: - sender = gmessage.getTag('senders').getTag('sender') - if not sender: + unread_senders = [] + for sender in gmessage.getTag('senders').getTags('sender'): + if sender.getAttr('unread') != '1': + continue + if sender.getAttr('name'): + unread_senders.append(sender.getAttr('name') + '< ' + \ + sender.getAttr('address') + '>') + else: + unread_senders.append(sender.getAttr('address')) + + if not unread_senders: continue - gmail_from = sender.getAttr('address') gmail_subject = gmessage.getTag('subject').getData() gmail_snippet = gmessage.getTag('snippet').getData() + tid = int(gmessage.getAttr('tid')) + if not self.gmail_last_tid or tid > self.gmail_last_tid: + self.gmail_last_tid = tid gmail_messages_list.append({ \ - 'From': gmail_from, \ + 'From': unread_senders, \ 'Subject': gmail_subject, \ - 'Snippet': gmail_snippet}) + 'Snippet': gmail_snippet, \ + 'url': gmessage.getAttr('url'), \ + 'participation': gmessage.getAttr('participation'), \ + 'messages': gmessage.getAttr('messages'), \ + 'date': gmessage.getAttr('date')}) + self.gmail_last_time = int(gm.getTag('mailbox').getAttr( + 'result-time')) + jid = gajim.get_jid_from_account(self.name) gajim.log.debug(('You have %s new gmail e-mails on %s.') % (newmsgs, jid)) self.dispatch('GMAIL_NOTIFY', (jid, newmsgs, gmail_messages_list)) @@ -1650,96 +1690,21 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, encrypted = False xep_200_encrypted = msg.getTag('c', namespace=common.xmpp.NS_STANZA_CRYPTO) + + # Receipt requested + # TODO: We shouldn't answer if we're invisible! + if msg.getTag('request', namespace='urn:xmpp:receipts') and \ + gajim.config.get_per('accounts', self.name, 'answer_receipt') \ + and gajim.contacts.get_contact_from_full_jid(self.name, frm). \ + sub not in (u'to', u'none'): + receipt = common.xmpp.Message(to = jid, typ = 'chat') + receipt.setID(msg.getID()) + receipt.setTag('received', + namespace='urn:xmpp:receipts') + receipt.setThread(thread_id) + con.send(receipt) - # We don't trust libotr, that's why we only pass the message - # to it if necessary. otrl_proto_message_type does this check. - if gajim.otr_module and not xep_200_encrypted \ - and isinstance(msgtxt, unicode) and \ - gajim.otr_module.otrl_proto_message_type(msgtxt.encode()) != \ - gajim.otr_module.OTRL_MSGTYPE_NOTOTR: - # set to encrypted if it's really encrypted. - if gajim.otr_module.otrl_proto_message_type( - msgtxt.encode()) != \ - gajim.otr_module.OTRL_MSGTYPE_TAGGEDPLAINTEXT: - encrypted = True - - # TODO: Do we really need .encode()? - # yes we do. OTR can't handle unicode. - otr_msg_tuple = \ - gajim.otr_module.otrl_message_receiving( - self.otr_userstates, - (gajim.otr_ui_ops, {'account': self.name}), - gajim.get_jid_from_account(self.name).encode(), - gajim.OTR_PROTO, - frm.encode(), - msgtxt.encode(), - (gajim.otr_add_appdata, self.name)) - msgtxt = unicode(otr_msg_tuple[1]) - - html_node = msg.getTag('html') - if html_node: - msg.delChild(html_node) - msg.setBody(msgtxt) - - if gajim.otr_module.otrl_tlv_find( - otr_msg_tuple[2], - gajim.otr_module.OTRL_TLV_DISCONNECTED) != None: - gajim.otr_ui_ops.gajim_log(_('%s ' \ - 'has ended his/her private ' \ - 'conversation with you. You should ' \ - 'do the same.') % frm, - self.name, - frm.encode()) - - ctrls = gajim.interface.msg_win_mgr.get_chat_controls(jid, self.name) - for ctrl in ctrls: - ctrl.update_otr() - - ctx = gajim.otr_module. \ - otrl_context_find( - self.otr_userstates, - frm.encode(), - gajim.get_jid_from_account( - self.name).encode(), - gajim.OTR_PROTO, 1, - (gajim.otr_add_appdata, - self.name))[0] - tlvs = otr_msg_tuple[2] - ctx.app_data.handle_tlv(tlvs) - - if msgtxt == '': - return - elif msgtxt != None and msgtxt != '': - gajim.otr_dont_append_tag[frm] = True - - # We're also here if we just don't - # support OTR. Thus, we should strip - # the tags from plaintext messages - # since they look ugly. - msgtxt = msgtxt.replace('\x20\x09\x20' \ - '\x20\x09\x09\x09\x09\x20\x09' \ - '\x20\x09\x20\x09\x20\x20', '') - msgtxt = msgtxt.replace('\x20\x09\x20' \ - '\x09\x20\x20\x09\x20', '') - msgtxt = msgtxt.replace('\x20\x20\x09' \ - '\x09\x20\x20\x09\x20', '') - - game_invite = msg.getTag('invite', namespace='http://jabber.org/protocol/games') - if game_invite: - game = game_invite.getTag('game') - - if game.getAttr('var') == \ - 'http://jabber.org/protocol/games/tictactoe': - cls = tictactoe.TicTacToeSession - - # this assumes that the invitation came with a thread_id we haven't - # seen - session = self.make_new_session(frm, thread_id, cls=cls) - - session.invited(msg) - - return - elif mtype != 'groupchat': + if mtype != 'groupchat': session = self.get_or_create_session(frm, thread_id) if thread_id and not session.received_thread_id: @@ -1851,7 +1816,6 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, no_log_for = '' no_log_for = no_log_for.split() - tim_int = int(float(mktime(tim))) if self.name not in no_log_for and jid not in no_log_for and not \ @@ -1861,8 +1825,6 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, # so don't store it in logs try: gajim.logger.write('gc_msg', frm, msgtxt, tim=tim) - # save the time we log to avoid duplicate logs - self.last_history_time[jid] = tim_int except exceptions.PysqliteOperationalError, e: self.dispatch('ERROR', (_('Disk Write Error'), str(e))) diff --git a/src/common/contacts.py b/src/common/contacts.py index 63724c238..2fa74f750 100644 --- a/src/common/contacts.py +++ b/src/common/contacts.py @@ -32,7 +32,7 @@ class Contact: self.jid = jid self.name = name self.contact_name = '' # nick choosen by contact - self.groups = groups + self.groups = groups # See below for what we do if it's empty self.show = show self.status = status self.sub = sub @@ -63,7 +63,11 @@ class Contact: # this is contact's chatstate self.chatstate = chatstate self.last_status_time = last_status_time - + if not self.groups: + if self.is_observer(): + self.groups = [_('Observers')] + else: + self.groups = [_('General')] self.mood = mood.copy() self.tune = tune.copy() self.activity = activity.copy() @@ -246,9 +250,10 @@ class Contacts: return None def iter_contacts(self, account): - for jid in self._contacts[account]: - for contact in self._contacts[account][jid]: - yield contact + if account in self._contacts: + for jid in self._contacts[account]: + for contact in self._contacts[account][jid]: + yield contact def get_contact_from_full_jid(self, account, fjid): ''' Get Contact object for specific resource of given jid''' @@ -310,18 +315,8 @@ class Contacts: if groups == []: in_groups = True else: - contact_groups = contact.groups - if not contact_groups: - # Contact is not in a group, so count it in General or - # Transports group - if common.gajim.jid_is_transport(jid): - contact_groups = [_('Transports')] - if contact.is_observer(): - contact_groups = [_('Observers')] - else: - contact_groups = [_('General')] for group in groups: - if group in contact_groups: + if group in contact.groups: in_groups = True break diff --git a/src/common/dbus_support.py b/src/common/dbus_support.py index 71695036a..b5998d2f4 100644 --- a/src/common/dbus_support.py +++ b/src/common/dbus_support.py @@ -29,8 +29,11 @@ _GAJIM_ERROR_IFACE = 'org.gajim.dbus.Error' try: if sys.platform == 'darwin': - import osx.dbus - osx.dbus.load(True) + try: + import osx.dbus + osx.dbus.load(True) + except ImportError: + pass import dbus import dbus.service import dbus.glib diff --git a/src/common/gajim.py b/src/common/gajim.py index eaa84ec4f..110e14008 100644 --- a/src/common/gajim.py +++ b/src/common/gajim.py @@ -165,13 +165,6 @@ else: if system('gpg -h >/dev/null 2>&1'): HAVE_GPG = False -OTR_PROTO = "xmpp" -otr_userstates = {} -otr_policy = {} - -# list of (full) jids not to attempt OTR with -otr_dont_append_tag = {} - gajim_identity = {'type': 'pc', 'category': 'client', 'name': 'Gajim'} gajim_common_features = [xmpp.NS_BYTESTREAM, xmpp.NS_SI, xmpp.NS_FILE, xmpp.NS_MUC, xmpp.NS_MUC_USER, diff --git a/src/common/helpers.py b/src/common/helpers.py index 1f9ca45f0..d99639e4c 100644 --- a/src/common/helpers.py +++ b/src/common/helpers.py @@ -53,8 +53,10 @@ except ImportError: hash_md5 = md5.new hash_sha1 = sha.new -if sys.platform == 'darwin': +try: from osx import nsapp +except ImportError: + pass try: import winsound # windows-only built-in module for playing wav @@ -570,7 +572,10 @@ def play_sound_file(path_to_soundfile): if path_to_soundfile is None or not os.path.exists(path_to_soundfile): return if sys.platform == 'darwin': - nsapp.playFile(path_to_soundfile) + try: + nsapp.playFile(path_to_soundfile) + except NameError: + pass elif os.name == 'nt': try: winsound.PlaySound(path_to_soundfile, diff --git a/src/common/xmpp/__init__.py b/src/common/xmpp/__init__.py index 4645ceda2..90f852598 100644 --- a/src/common/xmpp/__init__.py +++ b/src/common/xmpp/__init__.py @@ -26,7 +26,7 @@ and use only methods for access all values you should not have any problems. """ -import simplexml,protocol,debug,auth_nb,auth,transports,transports_nb,roster_nb,roster,dispatcher_nb,features_nb,features,browser,filetransfer,commands, idlequeue +import simplexml,protocol,debug,auth_nb,transports_nb,roster_nb,dispatcher_nb,features_nb,idlequeue from client_nb import * from client import * from protocol import * diff --git a/src/common/xmpp/auth.py b/src/common/xmpp/auth.py deleted file mode 100644 index 3091f5542..000000000 --- a/src/common/xmpp/auth.py +++ /dev/null @@ -1,306 +0,0 @@ -## auth.py -## -## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov -## -## 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 2, 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. - -# $Id: auth.py,v 1.35 2006/01/18 19:26:43 normanr Exp $ - -""" -Provides library with all Non-SASL and SASL authentication mechanisms. -Can be used both for client and transport authentication. -""" - -from protocol import * -from client import PlugIn -import sha,base64,random,dispatcher - -import md5 -def HH(some): return md5.new(some).hexdigest() -def H(some): return md5.new(some).digest() -def C(some): return ':'.join(some) - -class NonSASL(PlugIn): - """ Implements old Non-SASL (JEP-0078) authentication used in jabberd1.4 and transport authentication.""" - def __init__(self,user,password,resource): - """ Caches username, password and resource for auth. """ - PlugIn.__init__(self) - self.DBG_LINE='gen_auth' - self.user=user - self.password=password - self.resource=resource - - def plugin(self,owner): - """ Determine the best auth method (digest/0k/plain) and use it for auth. - Returns used method name on success. Used internally. """ - if not self.resource: return self.authComponent(owner) - self.DEBUG('Querying server about possible auth methods','start') - resp=owner.Dispatcher.SendAndWaitForResponse(Iq('get',NS_AUTH,payload=[Node('username',payload=[self.user])])) - if not isResultNode(resp): - self.DEBUG('No result node arrived! Aborting...','error') - return - iq=Iq(typ='set',node=resp) - query=iq.getTag('query') - query.setTagData('username',self.user) - query.setTagData('resource',self.resource) - - if query.getTag('digest'): - self.DEBUG("Performing digest authentication",'ok') - query.setTagData('digest',sha.new(owner.Dispatcher.Stream._document_attrs['id']+self.password).hexdigest()) - if query.getTag('password'): query.delChild('password') - method='digest' - elif query.getTag('token'): - token=query.getTagData('token') - seq=query.getTagData('sequence') - self.DEBUG("Performing zero-k authentication",'ok') - hash = sha.new(sha.new(self.password).hexdigest()+token).hexdigest() - for foo in xrange(int(seq)): hash = sha.new(hash).hexdigest() - query.setTagData('hash',hash) - method='0k' - else: - self.DEBUG("Sequre methods unsupported, performing plain text authentication",'warn') - query.setTagData('password',self.password) - method='plain' - resp=owner.Dispatcher.SendAndWaitForResponse(iq) - if isResultNode(resp): - self.DEBUG('Sucessfully authenticated with remove host.','ok') - owner.User=self.user - owner.Resource=self.resource - owner._registered_name=owner.User+'@'+owner.Server+'/'+owner.Resource - return method - self.DEBUG('Authentication failed!','error') - - def authComponent(self,owner): - """ Authenticate component. Send handshake stanza and wait for result. Returns "ok" on success. """ - self.handshake=0 - owner.send(Node(NS_COMPONENT_ACCEPT+' handshake',payload=[sha.new(owner.Dispatcher.Stream._document_attrs['id']+self.password).hexdigest()])) - owner.RegisterHandler('handshake',self.handshakeHandler,xmlns=NS_COMPONENT_ACCEPT) - while not self.handshake: - self.DEBUG("waiting on handshake",'notify') - owner.Process(1) - owner._registered_name=self.user - if self.handshake+1: return 'ok' - - def handshakeHandler(self,disp,stanza): - """ Handler for registering in dispatcher for accepting transport authentication. """ - if stanza.getName()=='handshake': self.handshake=1 - else: self.handshake=-1 - -class SASL(PlugIn): - """ Implements SASL authentication. """ - def __init__(self,username,password): - PlugIn.__init__(self) - self.username=username - self.password=password - - def plugin(self,owner): - if not self._owner.Dispatcher.Stream._document_attrs.has_key('version'): self.startsasl='not-supported' - elif self._owner.Dispatcher.Stream.features: - try: self.FeaturesHandler(self._owner.Dispatcher,self._owner.Dispatcher.Stream.features) - except NodeProcessed: pass - else: self.startsasl=None - - def auth(self): - """ Start authentication. Result can be obtained via "SASL.startsasl" attribute and will be - either "success" or "failure". Note that successfull auth will take at least - two Dispatcher.Process() calls. """ - if self.startsasl: pass - elif self._owner.Dispatcher.Stream.features: - try: self.FeaturesHandler(self._owner.Dispatcher,self._owner.Dispatcher.Stream.features) - except NodeProcessed: pass - else: self._owner.RegisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS) - - def plugout(self): - """ Remove SASL handlers from owner's dispatcher. Used internally. """ - self._owner.UnregisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS) - self._owner.UnregisterHandler('challenge',self.SASLHandler,xmlns=NS_SASL) - self._owner.UnregisterHandler('failure',self.SASLHandler,xmlns=NS_SASL) - self._owner.UnregisterHandler('success',self.SASLHandler,xmlns=NS_SASL) - - def FeaturesHandler(self,conn,feats): - """ Used to determine if server supports SASL auth. Used internally. """ - if not feats.getTag('mechanisms',namespace=NS_SASL): - self.startsasl='not-supported' - self.DEBUG('SASL not supported by server','error') - return - mecs=[] - for mec in feats.getTag('mechanisms',namespace=NS_SASL).getTags('mechanism'): - mecs.append(mec.getData()) - self._owner.RegisterHandler('challenge',self.SASLHandler,xmlns=NS_SASL) - self._owner.RegisterHandler('failure',self.SASLHandler,xmlns=NS_SASL) - self._owner.RegisterHandler('success',self.SASLHandler,xmlns=NS_SASL) - if "DIGEST-MD5" in mecs: - node=Node('auth',attrs={'xmlns':NS_SASL,'mechanism':'DIGEST-MD5'}) - elif "PLAIN" in mecs: - sasl_data='%s\x00%s\x00%s'%(self.username+'@'+self._owner.Server,self.username,self.password) - node=Node('auth',attrs={'xmlns':NS_SASL,'mechanism':'PLAIN'},payload=[base64.encodestring(sasl_data)]) - else: - self.startsasl='failure' - self.DEBUG('I can only use DIGEST-MD5 and PLAIN mecanisms.','error') - return - self.startsasl='in-process' - self._owner.send(node.__str__()) - raise NodeProcessed - - def SASLHandler(self,conn,challenge): - """ Perform next SASL auth step. Used internally. """ - if challenge.getNamespace()<>NS_SASL: return - if challenge.getName()=='failure': - self.startsasl='failure' - try: reason=challenge.getChildren()[0] - except: reason=challenge - self.DEBUG('Failed SASL authentification: %s'%reason,'error') - raise NodeProcessed - elif challenge.getName()=='success': - self.startsasl='success' - self.DEBUG('Successfully authenticated with remote server.','ok') - handlers=self._owner.Dispatcher.dumpHandlers() - self._owner.Dispatcher.PlugOut() - dispatcher.Dispatcher().PlugIn(self._owner) - self._owner.Dispatcher.restoreHandlers(handlers) - self._owner.User=self.username - raise NodeProcessed -########################################3333 - incoming_data=challenge.getData() - chal={} - data=base64.decodestring(incoming_data) - self.DEBUG('Got challenge:'+data,'ok') - for pair in data.split(','): - key,value=pair.split('=', 1) - if value[:1]=='"' and value[-1:]=='"': value=value[1:-1] - chal[key]=value - if chal.has_key('qop') and chal['qop']=='auth': - resp={} - resp['username']=self.username - resp['realm']=self._owner.Server - resp['nonce']=chal['nonce'] - cnonce='' - for i in range(7): - cnonce+=hex(int(random.random()*65536*4096))[2:] - resp['cnonce']=cnonce - resp['nc']=('00000001') - resp['qop']='auth' - resp['digest-uri']='xmpp/'+self._owner.Server - A1=C([H(C([resp['username'],resp['realm'],self.password])),resp['nonce'],resp['cnonce']]) - A2=C(['AUTHENTICATE',resp['digest-uri']]) - response= HH(C([HH(A1),resp['nonce'],resp['nc'],resp['cnonce'],resp['qop'],HH(A2)])) - resp['response']=response - resp['charset']='utf-8' - sasl_data='' - for key in ['charset','username','realm','nonce','nc','cnonce','digest-uri','response','qop']: - if key in ['nc','qop','response','charset']: sasl_data+="%s=%s,"%(key,resp[key]) - else: sasl_data+='%s="%s",'%(key,resp[key]) -########################################3333 - node=Node('response',attrs={'xmlns':NS_SASL},payload=[base64.encodestring(sasl_data[:-1]).replace('\r','').replace('\n','')]) - self._owner.send(node.__str__()) - elif chal.has_key('rspauth'): self._owner.send(Node('response',attrs={'xmlns':NS_SASL}).__str__()) - else: - self.startsasl='failure' - self.DEBUG('Failed SASL authentification: unknown challenge','error') - raise NodeProcessed - -class Bind(PlugIn): - """ Bind some JID to the current connection to allow router know of our location.""" - def __init__(self): - PlugIn.__init__(self) - self.DBG_LINE='bind' - self.bound=None - - def plugin(self,owner): - """ Start resource binding, if allowed at this time. Used internally. """ - if self._owner.Dispatcher.Stream.features: - try: self.FeaturesHandler(self._owner.Dispatcher,self._owner.Dispatcher.Stream.features) - except NodeProcessed: pass - else: self._owner.RegisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS) - - def plugout(self): - """ Remove Bind handler from owner's dispatcher. Used internally. """ - self._owner.UnregisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS) - - def FeaturesHandler(self,conn,feats): - """ Determine if server supports resource binding and set some internal attributes accordingly. """ - if not feats.getTag('bind',namespace=NS_BIND): - self.bound='failure' - self.DEBUG('Server does not requested binding.','error') - return - if feats.getTag('session',namespace=NS_SESSION): self.session=1 - else: self.session=-1 - self.bound=[] - - def Bind(self,resource=None): - """ Perform binding. Use provided resource name or random (if not provided). """ - while self.bound is None and self._owner.Process(1): pass - if resource: resource=[Node('resource',payload=[resource])] - else: resource=[] - resp=self._owner.SendAndWaitForResponse(Protocol('iq',typ='set',payload=[Node('bind',attrs={'xmlns':NS_BIND},payload=resource)])) - if isResultNode(resp): - self.bound.append(resp.getTag('bind').getTagData('jid')) - self.DEBUG('Successfully bound %s.'%self.bound[-1],'ok') - jid=JID(resp.getTag('bind').getTagData('jid')) - self._owner.User=jid.getNode() - self._owner.Resource=jid.getResource() - resp=self._owner.SendAndWaitForResponse(Protocol('iq',typ='set',payload=[Node('session',attrs={'xmlns':NS_SESSION})])) - if isResultNode(resp): - self.DEBUG('Successfully opened session.','ok') - self.session=1 - return 'ok' - else: - self.DEBUG('Session open failed.','error') - self.session=0 - elif resp: self.DEBUG('Binding failed: %s.'%resp.getTag('error'),'error') - else: - self.DEBUG('Binding failed: timeout expired.','error') - return '' - -class ComponentBind(PlugIn): - """ ComponentBind some JID to the current connection to allow router know of our location.""" - def __init__(self): - PlugIn.__init__(self) - self.DBG_LINE='bind' - self.bound=None - self.needsUnregister=None - - def plugin(self,owner): - """ Start resource binding, if allowed at this time. Used internally. """ - if self._owner.Dispatcher.Stream.features: - try: self.FeaturesHandler(self._owner.Dispatcher,self._owner.Dispatcher.Stream.features) - except NodeProcessed: pass - else: - self._owner.RegisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS) - self.needsUnregister=1 - - def plugout(self): - """ Remove ComponentBind handler from owner's dispatcher. Used internally. """ - if self.needsUnregister: - self._owner.UnregisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS) - - def FeaturesHandler(self,conn,feats): - """ Determine if server supports resource binding and set some internal attributes accordingly. """ - if not feats.getTag('bind',namespace=NS_BIND): - self.bound='failure' - self.DEBUG('Server does not requested binding.','error') - return - if feats.getTag('session',namespace=NS_SESSION): self.session=1 - else: self.session=-1 - self.bound=[] - - def Bind(self,domain=None): - """ Perform binding. Use provided domain name (if not provided). """ - while self.bound is None and self._owner.Process(1): pass - resp=self._owner.SendAndWaitForResponse(Protocol('bind',attrs={'name':domain},xmlns=NS_COMPONENT_1)) - if resp and resp.getAttr('error'): - self.DEBUG('Binding failed: %s.'%resp.getAttr('error'),'error') - elif resp: - self.DEBUG('Successfully bound.','ok') - return 'ok' - else: - self.DEBUG('Binding failed: timeout expired.','error') - return '' diff --git a/src/common/xmpp/auth_nb.py b/src/common/xmpp/auth_nb.py index fa6d40ad3..b2624043f 100644 --- a/src/common/xmpp/auth_nb.py +++ b/src/common/xmpp/auth_nb.py @@ -19,10 +19,14 @@ Can be used both for client and transport authentication. ''' import sys from protocol import * -from auth import * from client import PlugIn import sha,base64,random,dispatcher_nb +import md5 +def HH(some): return md5.new(some).hexdigest() +def H(some): return md5.new(some).digest() +def C(some): return ':'.join(some) + def challenge_splitter(data): ''' Helper function that creates a dict from challenge string. Sample chalenge string: @@ -235,6 +239,7 @@ class NonBlockingNonSASL(PlugIn): self.resource = resource self.on_auth = on_auth + def plugin(self, owner): ''' Determine the best auth method (digest/0k/plain) and use it for auth. Returns used method name on success. Used internally. ''' @@ -316,8 +321,24 @@ class NonBlockingNonSASL(PlugIn): else: self.handshake=-1 -class NonBlockingBind(Bind): +class NonBlockingBind(PlugIn): ''' Bind some JID to the current connection to allow router know of our location.''' + + def __init__(self): + PlugIn.__init__(self) + self.DBG_LINE='bind' + self.bound=None + + def FeaturesHandler(self,conn,feats): + """ Determine if server supports resource binding and set some internal attributes accordingly. """ + if not feats.getTag('bind',namespace=NS_BIND): + self.bound='failure' + self.DEBUG('Server does not requested binding.','error') + return + if feats.getTag('session',namespace=NS_SESSION): self.session=1 + else: self.session=-1 + self.bound=[] + def plugin(self, owner): ''' Start resource binding, if allowed at this time. Used internally. ''' if self._owner.Dispatcher.Stream.features: @@ -381,10 +402,16 @@ class NonBlockingBind(Bind): self.session = 0 self.on_bound(None) -class NBComponentBind(ComponentBind): +class NBComponentBind(PlugIn): ''' ComponentBind some JID to the current connection to allow router know of our location. ''' + def __init__(self): + PlugIn.__init__(self) + self.DBG_LINE='bind' + self.bound=None + self.needsUnregister=None + def plugin(self,owner): ''' Start resource binding, if allowed at this time. Used internally. ''' if self._owner.Dispatcher.Stream.features: @@ -427,3 +454,26 @@ class NBComponentBind(ComponentBind): self.DEBUG('Binding failed: timeout expired.', 'error') if self.on_bind: self.on_bind(None) + + def FeaturesHandler(self,conn,feats): + """ Determine if server supports resource binding and set some internal attributes accordingly. """ + if not feats.getTag('bind',namespace=NS_BIND): + self.bound='failure' + self.DEBUG('Server does not requested binding.','error') + return + if feats.getTag('session',namespace=NS_SESSION): self.session=1 + else: self.session=-1 + self.bound=[] + + def Bind(self,domain=None): + """ Perform binding. Use provided domain name (if not provided). """ + while self.bound is None and self._owner.Process(1): pass + resp=self._owner.SendAndWaitForResponse(Protocol('bind',attrs={'name':domain},xmlns=NS_COMPONENT_1)) + if resp and resp.getAttr('error'): + self.DEBUG('Binding failed: %s.'%resp.getAttr('error'),'error') + elif resp: + self.DEBUG('Successfully bound.','ok') + return 'ok' + else: + self.DEBUG('Binding failed: timeout expired.','error') + return '' diff --git a/src/common/xmpp/browser.py b/src/common/xmpp/browser.py deleted file mode 100644 index d696606a8..000000000 --- a/src/common/xmpp/browser.py +++ /dev/null @@ -1,216 +0,0 @@ -## browser.py -## -## Copyright (C) 2004 Alexey "Snake" Nezhdanov -## -## 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 2, 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. - -# $Id: browser.py,v 1.11 2005/10/07 23:17:09 normanr Exp $ - -"""Browser module provides DISCO server framework for your application. -This functionality can be used for very different purposes - from publishing -software version and supported features to building of "jabber site" that users -can navigate with their disco browsers and interact with active content. - -Such functionality is achieved via registering "DISCO handlers" that are -automatically called when user requests some node of your disco tree. -""" - -from dispatcher import * -from client import PlugIn - -class Browser(PlugIn): - """ WARNING! This class is for components only. It will not work in client mode! - - Standart xmpppy class that is ancestor of PlugIn and can be attached - to your application. - All processing will be performed in the handlers registered in the browser - instance. You can register any number of handlers ensuring that for each - node/jid combination only one (or none) handler registered. - You can register static information or the fully-blown function that will - calculate the answer dynamically. - Example of static info (see JEP-0030, examples 13-14): - # cl - your xmpppy connection instance. - b=xmpp.browser.Browser() - b.PlugIn(cl) - items=[] - item={} - item['jid']='catalog.shakespeare.lit' - item['node']='books' - item['name']='Books by and about Shakespeare' - items.append(item) - item={} - item['jid']='catalog.shakespeare.lit' - item['node']='clothing' - item['name']='Wear your literary taste with pride' - items.append(item) - item={} - item['jid']='catalog.shakespeare.lit' - item['node']='music' - item['name']='Music from the time of Shakespeare' - items.append(item) - info={'ids':[], 'features':[]} - b.setDiscoHandler({'items':items,'info':info}) - - items should be a list of item elements. - every item element can have any of these four keys: 'jid', 'node', 'name', 'action' - info should be a dicionary and must have keys 'ids' and 'features'. - Both of them should be lists: - ids is a list of dictionaries and features is a list of text strings. - Example (see JEP-0030, examples 1-2) - # cl - your xmpppy connection instance. - b=xmpp.browser.Browser() - b.PlugIn(cl) - items=[] - ids=[] - ids.append({'category':'conference','type':'text','name':'Play-Specific Chatrooms'}) - ids.append({'category':'directory','type':'chatroom','name':'Play-Specific Chatrooms'}) - features=[NS_DISCO_INFO,NS_DISCO_ITEMS,NS_MUC,NS_REGISTER,NS_SEARCH,NS_TIME,NS_VERSION] - info={'ids':ids,'features':features} - # info['xdata']=xmpp.protocol.DataForm() # JEP-0128 - b.setDiscoHandler({'items':[],'info':info}) - """ - def __init__(self): - """Initialises internal variables. Used internally.""" - PlugIn.__init__(self) - DBG_LINE='browser' - self._exported_methods=[] - self._handlers={'':{}} - - def plugin(self, owner): - """ Registers it's own iq handlers in your application dispatcher instance. - Used internally.""" - owner.RegisterHandler('iq',self._DiscoveryHandler,typ='get',ns=NS_DISCO_INFO) - owner.RegisterHandler('iq',self._DiscoveryHandler,typ='get',ns=NS_DISCO_ITEMS) - - def plugout(self): - """ Unregisters browser's iq handlers from your application dispatcher instance. - Used internally.""" - self._owner.UnregisterHandler('iq',self._DiscoveryHandler,typ='get',ns=NS_DISCO_INFO) - self._owner.UnregisterHandler('iq',self._DiscoveryHandler,typ='get',ns=NS_DISCO_ITEMS) - - def _traversePath(self,node,jid,set=0): - """ Returns dictionary and key or None,None - None - root node (w/o "node" attribute) - /a/b/c - node - /a/b/ - branch - Set returns '' or None as the key - get returns '' or None as the key or None as the dict. - Used internally.""" - if self._handlers.has_key(jid): cur=self._handlers[jid] - elif set: - self._handlers[jid]={} - cur=self._handlers[jid] - else: cur=self._handlers[''] - if node is None: node=[None] - else: node=node.replace('/',' /').split('/') - for i in node: - if i<>'' and cur.has_key(i): cur=cur[i] - elif set and i<>'': cur[i]={dict:cur,str:i}; cur=cur[i] - elif set or cur.has_key(''): return cur,'' - else: return None,None - if cur.has_key(1) or set: return cur,1 - raise "Corrupted data" - - def setDiscoHandler(self,handler,node='',jid=''): - """ This is the main method that you will use in this class. - It is used to register supplied DISCO handler (or dictionary with static info) - as handler of some disco tree branch. - If you do not specify the node this handler will be used for all queried nodes. - If you do not specify the jid this handler will be used for all queried JIDs. - - Usage: - cl.Browser.setDiscoHandler(someDict,node,jid) - or - cl.Browser.setDiscoHandler(someDISCOHandler,node,jid) - where - - someDict={ - 'items':[ - {'jid':'jid1','action':'action1','node':'node1','name':'name1'}, - {'jid':'jid2','action':'action2','node':'node2','name':'name2'}, - {'jid':'jid3','node':'node3','name':'name3'}, - {'jid':'jid4','node':'node4'} - ], - 'info' :{ - 'ids':[ - {'category':'category1','type':'type1','name':'name1'}, - {'category':'category2','type':'type2','name':'name2'}, - {'category':'category3','type':'type3','name':'name3'}, - ], - 'features':['feature1','feature2','feature3','feature4'], - 'xdata':DataForm - } - } - - and/or - - def someDISCOHandler(session,request,TYR): - # if TYR=='items': # returns items list of the same format as shown above - # elif TYR=='info': # returns info dictionary of the same format as shown above - # else: # this case is impossible for now. - """ - self.DEBUG('Registering handler %s for "%s" node->%s'%(handler,jid,node), 'info') - node,key=self._traversePath(node,jid,1) - node[key]=handler - - def getDiscoHandler(self,node='',jid=''): - """ Returns the previously registered DISCO handler - that is resonsible for this node/jid combination. - Used internally.""" - node,key=self._traversePath(node,jid) - if node: return node[key] - - def delDiscoHandler(self,node='',jid=''): - """ Unregisters DISCO handler that is resonsible for this - node/jid combination. When handler is unregistered the branch - is handled in the same way that it's parent branch from this moment. - """ - node,key=self._traversePath(node,jid) - if node: - handler=node[key] - del node[dict][node[str]] - return handler - - def _DiscoveryHandler(self,conn,request): - """ Servers DISCO iq request from the remote client. - Automatically determines the best handler to use and calls it - to handle the request. Used internally. - """ - handler=self.getDiscoHandler(request.getQuerynode(),request.getTo()) - if not handler: - self.DEBUG("No Handler for request with jid->%s node->%s ns->%s"%(request.getTo(),request.getQuerynode(),request.getQueryNS()),'error') - conn.send(Error(request,ERR_ITEM_NOT_FOUND)) - raise NodeProcessed - self.DEBUG("Handling request with jid->%s node->%s ns->%s"%(request.getTo(),request.getQuerynode(),request.getQueryNS()),'ok') - rep=request.buildReply('result') - if request.getQuerynode(): rep.setQuerynode(request.getQuerynode()) - q=rep.getTag('query') - if request.getQueryNS()==NS_DISCO_ITEMS: - # handler must return list: [{jid,action,node,name}] - if type(handler)==dict: lst=handler['items'] - else: lst=handler(conn,request,'items') - if lst==None: - conn.send(Error(request,ERR_ITEM_NOT_FOUND)) - raise NodeProcessed - for item in lst: q.addChild('item',item) - elif request.getQueryNS()==NS_DISCO_INFO: - if type(handler)==dict: dt=handler['info'] - else: dt=handler(conn,request,'info') - if dt==None: - conn.send(Error(request,ERR_ITEM_NOT_FOUND)) - raise NodeProcessed - # handler must return dictionary: - # {'ids':[{},{},{},{}], 'features':[fe,at,ur,es], 'xdata':DataForm} - for id in dt['ids']: q.addChild('identity',id) - for feature in dt['features']: q.addChild('feature',{'var':feature}) - if dt.has_key('xdata'): q.addChild(node=dt['xdata']) - conn.send(rep) - raise NodeProcessed diff --git a/src/common/xmpp/client.py b/src/common/xmpp/client.py index bc2335eff..aba778780 100644 --- a/src/common/xmpp/client.py +++ b/src/common/xmpp/client.py @@ -51,272 +51,38 @@ DBG_CLIENT='client' DBG_COMPONENT='component' class PlugIn: - """ Common xmpppy plugins infrastructure: plugging in/out, debugging. """ - def __init__(self): - self._exported_methods=[] - self.DBG_LINE=self.__class__.__name__.lower() + """ Common xmpppy plugins infrastructure: plugging in/out, debugging. """ + def __init__(self): + self._exported_methods=[] + self.DBG_LINE=self.__class__.__name__.lower() - def PlugIn(self,owner): - """ Attach to main instance and register ourself and all our staff in it. """ - self._owner=owner - if self.DBG_LINE not in owner.debug_flags: - owner.debug_flags.append(self.DBG_LINE) - self.DEBUG('Plugging %s into %s'%(self,self._owner),'start') - if owner.__dict__.has_key(self.__class__.__name__): - return self.DEBUG('Plugging ignored: another instance already plugged.','error') - self._old_owners_methods=[] - for method in self._exported_methods: - if owner.__dict__.has_key(method.__name__): - self._old_owners_methods.append(owner.__dict__[method.__name__]) - owner.__dict__[method.__name__]=method - owner.__dict__[self.__class__.__name__]=self - if self.__class__.__dict__.has_key('plugin'): return self.plugin(owner) + def PlugIn(self,owner): + """ Attach to main instance and register ourself and all our staff in it. """ + self._owner=owner + if self.DBG_LINE not in owner.debug_flags: + owner.debug_flags.append(self.DBG_LINE) + self.DEBUG('Plugging %s into %s'%(self,self._owner),'start') + if owner.__dict__.has_key(self.__class__.__name__): + return self.DEBUG('Plugging ignored: another instance already plugged.','error') + self._old_owners_methods=[] + for method in self._exported_methods: + if owner.__dict__.has_key(method.__name__): + self._old_owners_methods.append(owner.__dict__[method.__name__]) + owner.__dict__[method.__name__]=method + owner.__dict__[self.__class__.__name__]=self + if self.__class__.__dict__.has_key('plugin'): return self.plugin(owner) - def PlugOut(self): - """ Unregister all our staff from main instance and detach from it. """ - self.DEBUG('Plugging %s out of %s.'%(self,self._owner),'stop') - self._owner.debug_flags.remove(self.DBG_LINE) - for method in self._exported_methods: del self._owner.__dict__[method.__name__] - for method in self._old_owners_methods: self._owner.__dict__[method.__name__]=method - del self._owner.__dict__[self.__class__.__name__] - if self.__class__.__dict__.has_key('plugout'): return self.plugout() - del self._owner + def PlugOut(self): + """ Unregister all our staff from main instance and detach from it. """ + self.DEBUG('Plugging %s out of %s.'%(self,self._owner),'stop') + self._owner.debug_flags.remove(self.DBG_LINE) + for method in self._exported_methods: del self._owner.__dict__[method.__name__] + for method in self._old_owners_methods: self._owner.__dict__[method.__name__]=method + del self._owner.__dict__[self.__class__.__name__] + if self.__class__.__dict__.has_key('plugout'): return self.plugout() + del self._owner - def DEBUG(self,text,severity='info'): - """ Feed a provided debug line to main instance's debug facility along with our ID string. """ - self._owner.DEBUG(self.DBG_LINE,text,severity) + def DEBUG(self,text,severity='info'): + """ Feed a provided debug line to main instance's debug facility along with our ID string. """ + self._owner.DEBUG(self.DBG_LINE,text,severity) -import transports,dispatcher,auth,roster -class CommonClient: - """ Base for Client and Component classes.""" - def __init__(self,server,port=5222,debug=['always', 'nodebuilder'],caller=None): - """ Caches server name and (optionally) port to connect to. "debug" parameter specifies - the debug IDs that will go into debug output. You can either specifiy an "include" - or "exclude" list. The latter is done via adding "always" pseudo-ID to the list. - Full list: ['nodebuilder', 'dispatcher', 'gen_auth', 'SASL_auth', 'bind', 'socket', - 'CONNECTproxy', 'TLS', 'roster', 'browser', 'ibb'] . """ - if self.__class__.__name__=='Client': self.Namespace,self.DBG='jabber:client',DBG_CLIENT - elif self.__class__.__name__=='Component': self.Namespace,self.DBG=dispatcher.NS_COMPONENT_ACCEPT,DBG_COMPONENT - self.defaultNamespace=self.Namespace - self.disconnect_handlers=[] - self.Server=server - self.Port=port - # Who initiated this client - # Used to register the EventDispatcher - self._caller=caller - if debug and type(debug)<>list: debug=['always', 'nodebuilder'] - self._DEBUG=Debug.Debug(debug) - self.DEBUG=self._DEBUG.Show - self.debug_flags=self._DEBUG.debug_flags - self.debug_flags.append(self.DBG) - self._owner=self - self._registered_name=None - self.RegisterDisconnectHandler(self.DisconnectHandler) - self.connected='' - self._component=0 - - def RegisterDisconnectHandler(self,handler): - """ Register handler that will be called on disconnect.""" - self.disconnect_handlers.append(handler) - - def UnregisterDisconnectHandler(self,handler): - """ Unregister handler that is called on disconnect.""" - self.disconnect_handlers.remove(handler) - - def disconnected(self): - """ Called on disconnection. Calls disconnect handlers and cleans things up. """ - self.connected='' - self.DEBUG(self.DBG,'Disconnect detected','stop') - self.disconnect_handlers.reverse() - for i in self.disconnect_handlers: i() - self.disconnect_handlers.reverse() - if self.__dict__.has_key('TLS'): self.TLS.PlugOut() - - def DisconnectHandler(self): - """ Default disconnect handler. Just raises an IOError. - If you choosed to use this class in your production client, - override this method or at least unregister it. """ - raise IOError('Disconnected from server.') - - def event(self,eventName,args={}): - """ Default event handler. To be overriden. """ - print "Event: ",(eventName,args) - - def isConnected(self): - """ Returns connection state. F.e.: None / 'tls' / 'tcp+non_sasl' . """ - return self.connected - - def reconnectAndReauth(self): - """ Example of reconnection method. In fact, it can be used to batch connection and auth as well. """ - handlerssave=self.Dispatcher.dumpHandlers() - self.Dispatcher.PlugOut() - if self.__dict__.has_key('NonSASL'): self.NonSASL.PlugOut() - if self.__dict__.has_key('SASL'): self.SASL.PlugOut() - if self.__dict__.has_key('TLS'): self.TLS.PlugOut() - if self.__dict__.has_key('HTTPPROXYsocket'): self.HTTPPROXYsocket.PlugOut() - if self.__dict__.has_key('TCPsocket'): self.TCPsocket.PlugOut() - if not self.connect(server=self._Server,proxy=self._Proxy): return - if not self.auth(self._User,self._Password,self._Resource): return - self.Dispatcher.restoreHandlers(handlerssave) - return self.connected - - def get_peerhost(self): - ''' get the ip address of the account, from which is made connection - to the server , (e.g. me). - We will create listening socket on the same ip ''' - if hasattr(self, 'Connection'): - return self.Connection._sock.getsockname() - - def connect(self,server=None,proxy=None,ssl=None,use_srv=None): - """ Make a tcp/ip connection, protect it with tls/ssl if possible and start XMPP stream. - Returns None or 'tcp' or 'tls', depending on the result.""" - if not server: server=(self.Server,self.Port) - if proxy: socket=transports.HTTPPROXYsocket(proxy,server,use_srv) - else: socket=transports.TCPsocket(server,use_srv) - connected=socket.PlugIn(self) - if not connected: - socket.PlugOut() - return - self._Server,self._Proxy=server,proxy - self.connected='tcp' - if (ssl is None and self.Connection.getPort() in (5223, 443)) or ssl: - try: # FIXME. This should be done in transports.py - transports.TLS().PlugIn(self,now=1) - self.connected='ssl' - except socket.sslerror: - return - dispatcher.Dispatcher().PlugIn(self) - while self.Dispatcher.Stream._document_attrs is None: - if not self.Process(1): return - if self.Dispatcher.Stream._document_attrs.has_key('version') and self.Dispatcher.Stream._document_attrs['version']=='1.0': - while not self.Dispatcher.Stream.features and self.Process(): pass # If we get version 1.0 stream the features tag MUST BE presented - return self.connected - -class Client(CommonClient): - """ Example client class, based on CommonClient. """ - def connect(self,server=None,proxy=None,secure=None,use_srv=True): - """ Connect to jabber server. If you want to specify different ip/port to connect to you can - pass it as tuple as first parameter. If there is HTTP proxy between you and server - specify it's address and credentials (if needed) in the second argument. - If you want ssl/tls support to be discovered and enable automatically - leave third argument as None. (ssl will be autodetected only if port is 5223 or 443) - If you want to force SSL start (i.e. if port 5223 or 443 is remapped to some non-standard port) then set it to 1. - If you want to disable tls/ssl support completely, set it to 0. - Example: connect(('192.168.5.5',5222),{'host':'proxy.my.net','port':8080,'user':'me','password':'secret'}) - Returns '' or 'tcp' or 'tls', depending on the result.""" - if not CommonClient.connect(self,server,proxy,secure,use_srv) or secure<>None and not secure: return self.connected - transports.TLS().PlugIn(self) - if not self.Dispatcher.Stream._document_attrs.has_key('version') or not self.Dispatcher.Stream._document_attrs['version']=='1.0': return self.connected - while not self.Dispatcher.Stream.features and self.Process(): pass # If we get version 1.0 stream the features tag MUST BE presented - if not self.Dispatcher.Stream.features.getTag('starttls'): return self.connected # TLS not supported by server - while not self.TLS.starttls and self.Process(): pass - if not hasattr(self, 'TLS') or self.TLS.starttls!='success': self.event('tls_failed'); return self.connected - self.connected='tls' - return self.connected - - def auth(self,user,password,resource='',sasl=1): - """ Authenticate connnection and bind resource. If resource is not provided - random one or library name used. """ - self._User,self._Password,self._Resource=user,password,resource - while not self.Dispatcher.Stream._document_attrs and self.Process(): pass - if self.Dispatcher.Stream._document_attrs.has_key('version') and self.Dispatcher.Stream._document_attrs['version']=='1.0': - while not self.Dispatcher.Stream.features and self.Process(): pass # If we get version 1.0 stream the features tag MUST BE presented - if sasl: auth.SASL(user,password).PlugIn(self) - if not sasl or self.SASL.startsasl=='not-supported': - if not resource: resource='xmpppy' - if auth.NonSASL(user,password,resource).PlugIn(self): - self.connected+='+old_auth' - return 'old_auth' - return - self.SASL.auth() - while self.SASL.startsasl=='in-process' and self.Process(): pass - if self.SASL.startsasl=='success': - auth.Bind().PlugIn(self) - while self.Bind.bound is None and self.Process(): pass - if self.Bind.Bind(resource): - self.connected+='+sasl' - return 'sasl' - - def initRoster(self): - """ Plug in the roster. """ - if not self.__dict__.has_key('Roster'): roster.Roster().PlugIn(self) - - def getRoster(self): - """ Return the Roster instance, previously plugging it in and - requesting roster from server if needed. """ - self.initRoster() - return self.Roster.getRoster() - - def sendInitPresence(self,requestRoster=1): - """ Send roster request and initial . - You can disable the first by setting requestRoster argument to 0. """ - self.sendPresence(requestRoster=requestRoster) - - def sendPresence(self,jid=None,typ=None,requestRoster=0): - """ Send some specific presence state. - Can also request roster from server if according agrument is set.""" - if requestRoster: roster.Roster().PlugIn(self) - self.send(dispatcher.Presence(to=jid, typ=typ)) - -class Component(CommonClient): - """ Component class. The only difference from CommonClient is ability to perform component authentication. """ - def __init__(self,server,port=5347,typ=None,debug=['always', 'nodebuilder'],domains=None,component=0): - """ Init function for Components. - As components use a different auth mechanism which includes the namespace of the component. - Jabberd1.4 and Ejabberd use the default namespace then for all client messages. - Jabberd2 uses jabber:client. - 'server' argument is a server name that you are connecting to (f.e. "localhost"). - 'port' can be specified if 'server' resolves to correct IP. If it is not then you'll need to specify IP - and port while calling "connect()".""" - CommonClient.__init__(self,server,port=port,debug=debug) - self.typ=typ - self.component=component - if domains: - self.domains=domains - else: - self.domains=[server] - - def connect(self,server=None,proxy=None): - """ This will connect to the server, and if the features tag is found then set - the namespace to be jabber:client as that is required for jabberd2. - 'server' and 'proxy' arguments have the same meaning as in xmpp.Client.connect() """ - if self.component: - self.Namespace=auth.NS_COMPONENT_1 - self.Server=server[0] - CommonClient.connect(self,server=server,proxy=proxy) - if self.connected and (self.typ=='jabberd2' or not self.typ and self.Dispatcher.Stream.features is not None): - self.defaultNamespace=auth.NS_CLIENT - self.Dispatcher.RegisterNamespace(self.defaultNamespace) - self.Dispatcher.RegisterProtocol('iq',dispatcher.Iq) - self.Dispatcher.RegisterProtocol('message',dispatcher.Message) - self.Dispatcher.RegisterProtocol('presence',dispatcher.Presence) - return self.connected - - def auth(self,name,password,dup=None,sasl=0): - """ Authenticate component "name" with password "password".""" - self._User,self._Password,self._Resource=name,password,'' - try: - if self.component: sasl=1 - if sasl: auth.SASL(name,password).PlugIn(self) - if not sasl or self.SASL.startsasl=='not-supported': - if auth.NonSASL(name,password,'').PlugIn(self): - self.connected+='+old_auth' - return 'old_auth' - return - self.SASL.auth() - while self.SASL.startsasl=='in-process' and self.Process(): pass - if self.SASL.startsasl=='success': - if self.component: - self._component=self.component - for domain in self.domains: - auth.ComponentBind().PlugIn(self) - while self.ComponentBind.bound is None: self.Process() - if (not self.ComponentBind.Bind(domain)): - self.ComponentBind.PlugOut() - return - self.ComponentBind.PlugOut() - self.connected+='+sasl' - return 'sasl' - else: - raise auth.NotAuthorized(self.SASL.startsasl) - except: - self.DEBUG(self.DBG,"Failed to authenticate %s"%name,'error') diff --git a/src/common/xmpp/client_bosh.py b/src/common/xmpp/client_bosh.py deleted file mode 100644 index c2003b375..000000000 --- a/src/common/xmpp/client_bosh.py +++ /dev/null @@ -1,194 +0,0 @@ -## client_bosh.py -## -## Copyright (C) 2008 Tomas Karasek -## -## 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 2, 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. - -import locale, random -import protocol -import simplexml -import debug -import dispatcher_nb -from client_nb import NBCommonClient - -DBG_BOSHCLIENT='boshclient' - -class BOSHClient(NBCommonClient): - ''' - BOSH (XMPP over HTTP) client implementation. It should provide the same - methods and functionality as NonBlockingClient. - ''' - - - def __init__(self, server, bosh_conn_mgr, port=5222, bosh_port=5280, - on_connect=None, on_connect_failure=None, on_proxy_failure=None, caller=None): - ''' - Class constuctor has the same parameters as NBCommonClient plus bosh_conn_mgr - and bosh_port - Connection manager address and port. bosh_conn_mgr should be - in form: 'http://httpcm.jabber.org/http-bind/' - Tcp connection will be opened to bosh_conn_mgr:bosh_port instead of - server:port. - ''' - self.bosh_protocol, self.bosh_host, self.bosh_uri = self.urisplit(bosh_conn_mgr) - if self.bosh_protocol is None: - self.bosh_protocol = 'http' - - self.bosh_port = bosh_port - - if self.bosh_uri == '': - bosh_uri = '/' - - self.xmpp_server = server - self.xmpp_port = port - - self.bosh_hold = 1 - self.bosh_wait=60 - self.bosh_rid=-1 - self.bosh_httpversion = 'HTTP/1.1' - - NBCommonClient.__init__(self, self.bosh_host, self.bosh_port, caller=caller, - on_connect=on_connect, on_connect_failure=on_connect_failure, - on_proxy_failure=on_proxy_failure) - - # Namespace and DBG are detected in NBCommonClient constructor - # with isinstance(). Since BOSHClient is descendant of NBCommonClient - # and client_bosh.py is NOT imported in client_nb.py, NB_COMPONENT_ACCEPT - # is put to namespace. This is not very nice, thus: - # TODO: refactor Namespace and DBG recognition in NBCommonClient or - # derived classes - self.Namespace, self.DBG = protocol.NS_HTTP_BIND, DBG_BOSHCLIENT - # pop of DBG_COMPONENT - self.debug_flags.pop() - self.debug_flags.append(self.DBG) - self.debug_flags.append(simplexml.DBG_NODEBUILDER) - - - def urisplit(self, uri): - ''' - Function for splitting URI string to tuple (protocol, host, path). - e.g. urisplit('http://httpcm.jabber.org/webclient') returns - ('http', 'httpcm.jabber.org', '/webclient') - ''' - import re - regex = '(([^:/]+)(://))?([^/]*)(/?.*)' - grouped = re.match(regex, uri).groups() - proto, host, path = grouped[1], grouped[3], grouped[4] - return proto, host, path - - - def _on_connected(self): - ''' - method called after socket starts connecting from NonBlockingTcp._do_connect - ''' - self.onreceive(self.on_bosh_session_init_response) - dispatcher_nb.Dispatcher().PlugIn(self) - - - def parse_http_message(self, message): - ''' - splits http message to tuple ( - statusline - list of e.g. ['HTTP/1.1', '200', 'OK'], - headers - dictionary of headers e.g. {'Content-Length': '604', - 'Content-Type': 'text/xml; charset=utf-8'}, - httpbody - string with http body - ) - ''' - message = message.replace('\r','') - (header, httpbody) = message.split('\n\n') - header = header.split('\n') - statusline = header[0].split(' ') - header = header[1:] - headers = {} - for dummy in header: - row = dummy.split(' ',1) - headers[row[0][:-1]] = row[1] - return (statusline, headers, httpbody) - - - def on_bosh_session_init_response(self, data): - ''' - Called on init response - should check relevant attributes from body tag - ''' - if data: - statusline, headers, httpbody = self.parse_http_message(data) - - if statusline[1] != '200': - self.DEBUG(self.DBG, "HTTP Error in received session init response: %s" - % statusline, 'error') - # error handling TBD! - - # ATM, whole tag is pass to ProcessNonBocking. - # Question is how to peel off the body tag from incoming stanzas and make - # use of ordinar xmpp traffic handling. - self.Dispatcher.ProcessNonBlocking(httpbody) - - - def _check_stream_start(self, ns, tag, attrs): - ''' - callback stub called from XML Parser when is discovered - ''' - self.DEBUG(self.DBG, 'CHECK_STREAM_START: ns: %s, tag: %s, attrs: %s' - % (ns, tag, attrs), 'info') - - - def StreamInit(self): - ''' - Initiation of BOSH session. Called instead of Dispatcher.StreamInit() - Initial body tag is created and sent to Conn Manager. - ''' - self.Dispatcher.Stream = simplexml.NodeBuilder() - self.Dispatcher.Stream._dispatch_depth = 2 - self.Dispatcher.Stream.dispatch = self.Dispatcher.dispatch - self.Dispatcher.Stream.stream_header_received = self._check_stream_start - self.debug_flags.append(simplexml.DBG_NODEBUILDER) - self.Dispatcher.Stream.DEBUG = self.DEBUG - self.Dispatcher.Stream.features = None - - initial_body_tag = simplexml.Node('body') - initial_body_tag.setNamespace(self.Namespace) - initial_body_tag.setAttr('content', 'text/xml; charset=utf-8') - initial_body_tag.setAttr('hold', str(self.bosh_hold)) - initial_body_tag.setAttr('to', self.xmpp_server) - initial_body_tag.setAttr('wait', str(self.bosh_wait)) - - r = random.Random() - r.seed() - # with 50-bit random initial rid, session would have to go up - # to 7881299347898368 messages to raise rid over 2**53 - # (see http://www.xmpp.org/extensions/xep-0124.html#rids) - self.bosh_rid = r.getrandbits(50) - initial_body_tag.setAttr('rid', str(self.bosh_rid)) - - if locale.getdefaultlocale()[0]: - initial_body_tag.setAttr('xml:lang', - locale.getdefaultlocale()[0].split('_')[0]) - initial_body_tag.setAttr('xmpp:version', '1.0') - initial_body_tag.setAttr('xmlns:xmpp', 'urn:xmpp:xbosh') - - self.send(self.build_bosh_message(initial_body_tag)) - - - def build_bosh_message(self, httpbody): - ''' - Builds bosh http message with given body. - Values for headers and status line fields are taken from class variables. - ) - ''' - headers = ['POST %s HTTP/1.1' % self.bosh_uri, - 'Host: %s' % self.bosh_host, - 'Content-Type: text/xml; charset=utf-8', - 'Content-Length: %s' % len(str(httpbody)), - '\r\n'] - headers = '\r\n'.join(headers) - return('%s%s\r\n' % (headers, httpbody)) - - - diff --git a/src/common/xmpp/client_nb.py b/src/common/xmpp/client_nb.py index ef8cfbf9e..d82fb9e94 100644 --- a/src/common/xmpp/client_nb.py +++ b/src/common/xmpp/client_nb.py @@ -1,8 +1,8 @@ ## client_nb.py -## based on client.py +## based on client.py ## ## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov -## modified by Dimitur Kirov +## modified by Dimitur Kirov ## ## 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 @@ -29,7 +29,7 @@ import debug import transports_nb, dispatcher_nb, auth_nb, roster_nb from client import * -class NBCommonClient(CommonClient): +class NBCommonClient: ''' Base for Client and Component classes.''' def __init__(self, server, port=5222, debug=['always', 'nodebuilder'], caller=None, on_connect=None, on_proxy_failure=None, on_connect_failure=None): @@ -185,6 +185,37 @@ class NBCommonClient(CommonClient): self.on_stream_start = None return True + # moved from client.CommonClient: + def RegisterDisconnectHandler(self,handler): + """ Register handler that will be called on disconnect.""" + self.disconnect_handlers.append(handler) + + def UnregisterDisconnectHandler(self,handler): + """ Unregister handler that is called on disconnect.""" + self.disconnect_handlers.remove(handler) + + def DisconnectHandler(self): + """ Default disconnect handler. Just raises an IOError. + If you choosed to use this class in your production client, + override this method or at least unregister it. """ + raise IOError('Disconnected from server.') + + def event(self,eventName,args={}): + """ Default event handler. To be overriden. """ + print "Event: ",(eventName,args) + + def isConnected(self): + """ Returns connection state. F.e.: None / 'tls' / 'tcp+non_sasl' . """ + return self.connected + + def get_peerhost(self): + ''' get the ip address of the account, from which is made connection + to the server , (e.g. me). + We will create listening socket on the same ip ''' + # moved from client.CommonClient + if hasattr(self, 'Connection'): + return self.Connection._sock.getsockname() + class NonBlockingClient(NBCommonClient): ''' Example client class, based on CommonClient. ''' def connect(self,server=None,proxy=None,secure=None,use_srv=True): diff --git a/src/common/xmpp/commands.py b/src/common/xmpp/commands.py deleted file mode 100644 index 433faee86..000000000 --- a/src/common/xmpp/commands.py +++ /dev/null @@ -1,330 +0,0 @@ -## $Id: commands.py,v 1.11 2005/11/30 17:03:11 normanr Exp $ - -## Ad-Hoc Command manager -## Mike Albon (c) 5th January 2005 - -## 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 2, 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. - - -"""This module is a ad-hoc command processor for xmpppy. It uses the plug-in mechanism like most of the core library. It depends on a DISCO browser manager. - -There are 3 classes here, a command processor Commands like the Browser, and a command template plugin Command, and an example command. - -To use this module: - - Instansiate the module with the parent transport and disco browser manager as parameters. - 'Plug in' commands using the command template. - The command feature must be added to existing disco replies where neccessary. - -What it supplies: - - Automatic command registration with the disco browser manager. - Automatic listing of commands in the public command list. - A means of handling requests, by redirection though the command manager. -""" - -from protocol import * -from client import PlugIn - -class Commands(PlugIn): - """Commands is an ancestor of PlugIn and can be attached to any session. - - The commands class provides a lookup and browse mechnism. It follows the same priciple of the Browser class, for Service Discovery to provide the list of commands, it adds the 'list' disco type to your existing disco handler function. - - How it works: - The commands are added into the existing Browser on the correct nodes. When the command list is built the supplied discovery handler function needs to have a 'list' option in type. This then gets enumerated, all results returned as None are ignored. - The command executed is then called using it's Execute method. All session management is handled by the command itself. - """ - def __init__(self, browser): - """Initialises class and sets up local variables""" - PlugIn.__init__(self) - DBG_LINE='commands' - self._exported_methods=[] - self._handlers={'':{}} - self._browser = browser - - def plugin(self, owner): - """Makes handlers within the session""" - # Plug into the session and the disco manager - # We only need get and set, results are not needed by a service provider, only a service user. - owner.RegisterHandler('iq',self._CommandHandler,typ='set',ns=NS_COMMANDS) - owner.RegisterHandler('iq',self._CommandHandler,typ='get',ns=NS_COMMANDS) - self._browser.setDiscoHandler(self._DiscoHandler,node=NS_COMMANDS,jid='') - - def plugout(self): - """Removes handlers from the session""" - # unPlug from the session and the disco manager - self._owner.UnregisterHandler('iq',self_CommandHandler,ns=NS_COMMANDS) - for jid in self._handlers: - self._browser.delDiscoHandler(self._DiscoHandler,node=NS_COMMANDS) - - def _CommandHandler(self,conn,request): - """The internal method to process the routing of command execution requests""" - # This is the command handler itself. - # We must: - # Pass on command execution to command handler - # (Do we need to keep session details here, or can that be done in the command?) - jid = str(request.getTo()) - try: - node = request.getTagAttr('command','node') - except: - conn.send(Error(request,ERR_BAD_REQUEST)) - raise NodeProcessed - if self._handlers.has_key(jid): - if self._handlers[jid].has_key(node): - self._handlers[jid][node]['execute'](conn,request) - else: - conn.send(Error(request,ERR_ITEM_NOT_FOUND)) - raise NodeProcessed - elif self._handlers[''].has_key(node): - self._handlers[''][node]['execute'](conn,request) - else: - conn.send(Error(request,ERR_ITEM_NOT_FOUND)) - raise NodeProcessed - - def _DiscoHandler(self,conn,request,typ): - """The internal method to process service discovery requests""" - # This is the disco manager handler. - if typ == 'items': - # We must: - # Generate a list of commands and return the list - # * This handler does not handle individual commands disco requests. - # Pseudo: - # Enumerate the 'item' disco of each command for the specified jid - # Build responce and send - # To make this code easy to write we add an 'list' disco type, it returns a tuple or 'none' if not advertised - list = [] - items = [] - jid = str(request.getTo()) - # Get specific jid based results - if self._handlers.has_key(jid): - for each in self._handlers[jid].keys(): - items.append((jid,each)) - else: - # Get generic results - for each in self._handlers[''].keys(): - items.append(('',each)) - if items != []: - for each in items: - i = self._handlers[each[0]][each[1]]['disco'](conn,request,'list') - if i is not None: - list.append(Node(tag='item',attrs={'jid':i[0],'node':i[1],'name':i[2]})) - iq = request.buildReply('result') - if request.getQuerynode(): iq.setQuerynode(request.getQuerynode()) - iq.setQueryPayload(list) - conn.send(iq) - else: - conn.send(Error(request,ERR_ITEM_NOT_FOUND)) - raise NodeProcessed - elif typ == 'info': - return {'ids':[{'category':'automation','type':'command-list'}],'features':[]} - - def addCommand(self,name,cmddisco,cmdexecute,jid=''): - """The method to call if adding a new command to the session, the requred parameters of cmddisco and cmdexecute are the methods to enable that command to be executed""" - # This command takes a command object and the name of the command for registration - # We must: - # Add item into disco - # Add item into command list - if not self._handlers.has_key(jid): - self._handlers[jid]={} - self._browser.setDiscoHandler(self._DiscoHandler,node=NS_COMMANDS,jid=jid) - if self._handlers[jid].has_key(name): - raise NameError,'Command Exists' - else: - self._handlers[jid][name]={'disco':cmddisco,'execute':cmdexecute} - # Need to add disco stuff here - self._browser.setDiscoHandler(cmddisco,node=name,jid=jid) - - def delCommand(self,name,jid=''): - """Removed command from the session""" - # This command takes a command object and the name used for registration - # We must: - # Remove item from disco - # Remove item from command list - if not self._handlers.has_key(jid): - raise NameError,'Jid not found' - if not self._handlers[jid].has_key(name): - raise NameError, 'Command not found' - else: - #Do disco removal here - command = self.getCommand(name,jid)['disco'] - del self._handlers[jid][name] - self._browser.delDiscoHandler(command,node=name,jid=jid) - - def getCommand(self,name,jid=''): - """Returns the command tuple""" - # This gets the command object with name - # We must: - # Return item that matches this name - if not self._handlers.has_key(jid): - raise NameError,'Jid not found' - elif not self._handlers[jid].has_key(name): - raise NameError,'Command not found' - else: - return self._handlers[jid][name] - -class Command_Handler_Prototype(PlugIn): - """This is a prototype command handler, as each command uses a disco method - and execute method you can implement it any way you like, however this is - my first attempt at making a generic handler that you can hang process - stages on too. There is an example command below. - - The parameters are as follows: - name : the name of the command within the jabber environment - description : the natural language description - discofeatures : the features supported by the command - initial : the initial command in the from of {'execute':commandname} - - All stages set the 'actions' dictionary for each session to represent the possible options available. - """ - name = 'examplecommand' - count = 0 - description = 'an example command' - discofeatures = [NS_COMMANDS,NS_DATA] - # This is the command template - def __init__(self,jid=''): - """Set up the class""" - PlugIn.__init__(self) - DBG_LINE='command' - self.sessioncount = 0 - self.sessions = {} - # Disco information for command list pre-formatted as a tuple - self.discoinfo = {'ids':[{'category':'automation','type':'command-node','name':self.description}],'features': self.discofeatures} - self._jid = jid - - def plugin(self,owner): - """Plug command into the commands class""" - # The owner in this instance is the Command Processor - self._commands = owner - self._owner = owner._owner - self._commands.addCommand(self.name,self._DiscoHandler,self.Execute,jid=self._jid) - - def plugout(self): - """Remove command from the commands class""" - self._commands.delCommand(name,self._jid) - - def getSessionID(self): - """Returns an id for the command session""" - self.count = self.count+1 - return 'cmd-%s-%d'%(self.name,self.count) - - def Execute(self,conn,request): - """The method that handles all the commands, and routes them to the correct method for that stage.""" - # New request or old? - try: - session = request.getTagAttr('command','sessionid') - except: - session = None - try: - action = request.getTagAttr('command','action') - except: - action = None - if action is None: action = 'execute' - # Check session is in session list - if self.sessions.has_key(session): - if self.sessions[session]['jid']==request.getFrom(): - # Check action is vaild - if self.sessions[session]['actions'].has_key(action): - # Execute next action - self.sessions[session]['actions'][action](conn,request) - else: - # Stage not presented as an option - self._owner.send(Error(request,ERR_BAD_REQUEST)) - raise NodeProcessed - else: - # Jid and session don't match. Go away imposter - self._owner.send(Error(request,ERR_BAD_REQUEST)) - raise NodeProcessed - elif session is not None: - # Not on this sessionid you won't. - self._owner.send(Error(request,ERR_BAD_REQUEST)) - raise NodeProcessed - else: - # New session - self.initial[action](conn,request) - - def _DiscoHandler(self,conn,request,type): - """The handler for discovery events""" - if type == 'list': - return (request.getTo(),self.name,self.description) - elif type == 'items': - return [] - elif type == 'info': - return self.discoinfo - -class TestCommand(Command_Handler_Prototype): - """ Example class. You should read source if you wish to understate how it works. - Generally, it presents a "master" that giudes user through to calculate something. - """ - name = 'testcommand' - description = 'a noddy example command' - def __init__(self,jid=''): - """ Init internal constants. """ - Command_Handler_Prototype.__init__(self,jid) - self.pi = 3.14 - self.initial = {'execute':self.cmdFirstStage} - - def cmdFirstStage(self,conn,request): - """ Determine """ - # This is the only place this should be repeated as all other stages should have SessionIDs - try: - session = request.getTagAttr('command','sessionid') - except: - session = None - if session is None: - session = self.getSessionID() - sessions[session]={'jid':request.getFrom(),'actions':{'cancel':self.cmdCancel,'next':self.cmdSecondStage},'data':{'type':None}} - # As this is the first stage we only send a form - reply = request.buildReply('result') - form = DataForm(title='Select type of operation',data=['Use the combobox to select the type of calculation you would like to do, then click Next',DataField(name='calctype',label='Calculation Type',value=sessions[session]['data']['type'],options=[['circlediameter','Calculate the Diameter of a circle'],['circlearea','Calculate the area of a circle']],typ='list-single',required=1)]) - replypayload = [Node('actions',attrs={'execute':'next'},payload=[Node('next')]),form] - reply.addChild(name='command',attrs={'xmlns':NS_COMMAND,'node':request.getTagAttr('command','node'),'sessionid':session,'status':'executing'},payload=replypayload) - self._owner.send(reply) - raise NodeProcessed - - def cmdSecondStage(self,conn,request): - form = DataForm(node = result.getTag(name='command').getTag(name='x',namespace=NS_DATA)) - sessions[request.getTagAttr('command','sessionid')]['data']['type']=form.getField('calctype') - sessions[request.getTagAttr('command','sessionid')]['actions']={'cancel':self.cmdCancel,None:self.cmdThirdStage,'previous':cmdFirstStage} - # The form generation is split out to another method as it may be called by cmdThirdStage - self.cmdSecondStageReply(conn,request) - - def cmdSecondStageReply(self,conn,request): - reply = request.buildReply('result') - form = DataForm(title = 'Enter the radius', data=['Enter the radius of the circle (numbers only)',DataField(label='Radius',name='radius',typ='text-single')]) - replypayload = [Node('actions',attrs={'execute':'complete'},payload=[Node('complete'),Node('prev')]),form] - reply.addChild(name='command',attrs={'xmlns':NS_COMMAND,'node':request.getTagAttr('command','node'),'sessionid':request.getTagAttr('command','sessionid'),'status':'executing'},payload=replypayload) - self._owner.send(reply) - raise NodeProcessed - - def cmdThirdStage(self,conn,request): - form = DataForm(node = result.getTag(name='command').getTag(name='x',namespace=NS_DATA)) - try: - num = float(form.getField('radius')) - except: - self.cmdSecondStageReply(conn,request) - if sessions[request.getTagAttr('command','sessionid')]['data']['type'] == 'circlearea': - result = num*(pi**2) - else: - result = num*2*pi - reply = result.buildReply(request) - form = DataForm(typ='result',data=[DataField(label='result',name='result',value=result)]) - reply.addChild(name='command',attrs={'xmlns':NS_COMMAND,'node':request.getTagAttr('command','node'),'sessionid':request.getTagAttr('command','sessionid'),'status':'completed'},payload=form) - self._owner.send(reply) - raise NodeProcessed - - def cmdCancel(self,conn,request): - reply = request.buildReply('result') - reply.addChild(name='command',attrs={'xmlns':NS_COMMAND,'node':request.getTagAttr('command','node'),'sessionid':request.getTagAttr('command','sessionid'),'status':'cancelled'}) - self._owner.send(reply) - del sessions[request.getTagAttr('command','sessionid')] - - diff --git a/src/common/xmpp/dispatcher.py b/src/common/xmpp/dispatcher.py deleted file mode 100644 index 95e0f00b0..000000000 --- a/src/common/xmpp/dispatcher.py +++ /dev/null @@ -1,383 +0,0 @@ -## transports.py -## -## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov -## -## 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 2, 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. - -# $Id: dispatcher.py,v 1.40 2006/01/18 19:26:43 normanr Exp $ - -""" -Main xmpppy mechanism. Provides library with methods to assign different handlers -to different XMPP stanzas. -Contains one tunable attribute: DefaultTimeout (25 seconds by default). It defines time that -Dispatcher.SendAndWaitForResponce method will wait for reply stanza before giving up. -""" - -import simplexml,time,sys -from protocol import * -from client import PlugIn - -DefaultTimeout=25 -ID=0 - -class Dispatcher(PlugIn): - """ Ancestor of PlugIn class. Handles XMPP stream, i.e. aware of stream headers. - Can be plugged out/in to restart these headers (used for SASL f.e.). """ - def __init__(self): - PlugIn.__init__(self) - DBG_LINE='dispatcher' - self.handlers={} - self._expected={} - self._defaultHandler=None - self._pendingExceptions=[] - self._eventHandler=None - self._cycleHandlers=[] - self._exported_methods=[self.Process,self.RegisterHandler,self.RegisterDefaultHandler,\ - self.RegisterEventHandler,self.UnregisterCycleHandler,self.RegisterCycleHandler,\ - self.RegisterHandlerOnce,self.UnregisterHandler,self.RegisterProtocol,\ - self.WaitForResponse,self.SendAndWaitForResponse,self.send,self.disconnect,\ - self.SendAndCallForResponse, self.getAnID, ] - - def getAnID(self): - global ID - ID+=1 - return `ID` - - def dumpHandlers(self): - """ Return set of user-registered callbacks in it's internal format. - Used within the library to carry user handlers set over Dispatcher replugins. """ - return self.handlers - def restoreHandlers(self,handlers): - """ Restores user-registered callbacks structure from dump previously obtained via dumpHandlers. - Used within the library to carry user handlers set over Dispatcher replugins. """ - self.handlers=handlers - - def _init(self): - """ Registers default namespaces/protocols/handlers. Used internally. """ - self.RegisterNamespace('unknown') - self.RegisterNamespace(NS_STREAMS) - self.RegisterNamespace(self._owner.defaultNamespace) - self.RegisterProtocol('iq',Iq) - self.RegisterProtocol('presence',Presence) - self.RegisterProtocol('message',Message) - self.RegisterDefaultHandler(self.returnStanzaHandler) - # Register Gajim's event handler as soon as dispatcher begins - self.RegisterEventHandler(self._owner._caller._event_dispatcher) -# self.RegisterHandler('error',self.streamErrorHandler,xmlns=NS_STREAMS) - - def plugin(self, owner): - """ Plug the Dispatcher instance into Client class instance and send initial stream header. Used internally.""" - self._init() - for method in self._old_owners_methods: - if method.__name__=='send': self._owner_send=method; break - self._owner.lastErrNode=None - self._owner.lastErr=None - self._owner.lastErrCode=None - self.StreamInit() - - def plugout(self): - """ Prepares instance to be destructed. """ - self.Stream.dispatch=None - self.Stream.DEBUG=None - self.Stream.features=None - self.Stream.destroy() - - def StreamInit(self): - """ Send an initial stream header. """ - self.Stream=simplexml.NodeBuilder() - self.Stream._dispatch_depth=2 - self.Stream.dispatch=self.dispatch - self.Stream.stream_header_received=self._check_stream_start - self._owner.debug_flags.append(simplexml.DBG_NODEBUILDER) - self.Stream.DEBUG=self._owner.DEBUG - self.Stream.features=None - self._metastream=Node('stream:stream') - self._metastream.setNamespace(self._owner.Namespace) - self._metastream.setAttr('version','1.0') - self._metastream.setAttr('xmlns:stream',NS_STREAMS) - self._metastream.setAttr('to',self._owner.Server) - self._owner.send("%s>"%str(self._metastream)[:-2]) - - def _check_stream_start(self,ns,tag,attrs): - if ns<>NS_STREAMS or tag<>'stream': - raise ValueError('Incorrect stream start: (%s,%s). Terminating.'%(tag,ns)) - - def Process(self, timeout=0): - """ Check incoming stream for data waiting. If "timeout" is positive - block for as max. this time. - Returns: - 1) length of processed data if some data were processed; - 2) '0' string if no data were processed but link is alive; - 3) 0 (zero) if underlying connection is closed. - Take note that in case of disconnection detect during Process() call - disconnect handlers are called automatically. - """ - for handler in self._cycleHandlers: handler(self) - if len(self._pendingExceptions) > 0: - _pendingException = self._pendingExceptions.pop() - raise _pendingException[0], _pendingException[1], _pendingException[2] - if self._owner.Connection.pending_data(timeout): - try: data=self._owner.Connection.receive() - except IOError: return - self.Stream.Parse(data) - if len(self._pendingExceptions) > 0: - _pendingException = self._pendingExceptions.pop() - raise _pendingException[0], _pendingException[1], _pendingException[2] - return len(data) - return '0' # It means that nothing is received but link is alive. - - def RegisterNamespace(self,xmlns,order='info'): - """ Creates internal structures for newly registered namespace. - You can register handlers for this namespace afterwards. By default one namespace - already registered (jabber:client or jabber:component:accept depending on context. """ - self.DEBUG('Registering namespace "%s"'%xmlns,order) - self.handlers[xmlns]={} - self.RegisterProtocol('unknown',Protocol,xmlns=xmlns) - self.RegisterProtocol('default',Protocol,xmlns=xmlns) - - def RegisterProtocol(self,tag_name,Proto,xmlns=None,order='info'): - """ Used to declare some top-level stanza name to dispatcher. - Needed to start registering handlers for such stanzas. - Iq, message and presence protocols are registered by default. """ - if not xmlns: xmlns=self._owner.defaultNamespace - self.DEBUG('Registering protocol "%s" as %s(%s)'%(tag_name,Proto,xmlns), order) - self.handlers[xmlns][tag_name]={type:Proto, 'default':[]} - - def RegisterNamespaceHandler(self,xmlns,handler,typ='',ns='', makefirst=0, system=0): - """ Register handler for processing all stanzas for specified namespace. """ - self.RegisterHandler('default', handler, typ, ns, xmlns, makefirst, system) - - def RegisterHandler(self,name,handler,typ='',ns='',xmlns=None, makefirst=0, system=0): - """Register user callback as stanzas handler of declared type. Callback must take - (if chained, see later) arguments: dispatcher instance (for replying), incomed - return of previous handlers. - The callback must raise xmpp.NodeProcessed just before return if it want preven - callbacks to be called with the same stanza as argument _and_, more importantly - library from returning stanza to sender with error set (to be enabled in 0.2 ve - Arguments: - "name" - name of stanza. F.e. "iq". - "handler" - user callback. - "typ" - value of stanza's "type" attribute. If not specified any value match - "ns" - namespace of child that stanza must contain. - "chained" - chain together output of several handlers. - "makefirst" - insert handler in the beginning of handlers list instead of - adding it to the end. Note that more common handlers (i.e. w/o "typ" and " - will be called first nevertheless. - "system" - call handler even if NodeProcessed Exception were raised already. - """ - if not xmlns: xmlns=self._owner.defaultNamespace - self.DEBUG('Registering handler %s for "%s" type->%s ns->%s(%s)'%(handler,name,typ,ns,xmlns), 'info') - if not typ and not ns: typ='default' - if not self.handlers.has_key(xmlns): self.RegisterNamespace(xmlns,'warn') - if not self.handlers[xmlns].has_key(name): self.RegisterProtocol(name,Protocol,xmlns,'warn') - if not self.handlers[xmlns][name].has_key(typ+ns): self.handlers[xmlns][name][typ+ns]=[] - if makefirst: self.handlers[xmlns][name][typ+ns].insert(0,{'func':handler,'system':system}) - else: self.handlers[xmlns][name][typ+ns].append({'func':handler,'system':system}) - - def RegisterHandlerOnce(self,name,handler,typ='',ns='',xmlns=None,makefirst=0, system=0): - """ Unregister handler after first call (not implemented yet). """ - if not xmlns: xmlns=self._owner.defaultNamespace - self.RegisterHandler(name, handler, typ, ns, xmlns, makefirst, system) - - def UnregisterHandler(self,name,handler,typ='',ns='',xmlns=None): - """ Unregister handler. "typ" and "ns" must be specified exactly the same as with registering.""" - if not xmlns: xmlns=self._owner.defaultNamespace - if not typ and not ns: typ='default' - if not self.handlers[xmlns].has_key(name): return - if not self.handlers[xmlns][name].has_key(typ+ns): return - for pack in self.handlers[xmlns][name][typ+ns]: - if handler==pack['func']: break - else: pack=None - try: self.handlers[xmlns][name][typ+ns].remove(pack) - except ValueError: pass - - def RegisterDefaultHandler(self,handler): - """ Specify the handler that will be used if no NodeProcessed exception were raised. - This is returnStanzaHandler by default. """ - self._defaultHandler=handler - - def RegisterEventHandler(self,handler): - """ Register handler that will process events. F.e. "FILERECEIVED" event. """ - self._eventHandler=handler - - def returnStanzaHandler(self,conn,stanza): - """ Return stanza back to the sender with error set. """ - if stanza.getType() in ['get','set']: - conn.send(Error(stanza,ERR_FEATURE_NOT_IMPLEMENTED)) - - def streamErrorHandler(self,conn,error): - name,text='error',error.getData() - for tag in error.getChildren(): - if tag.getNamespace()==NS_XMPP_STREAMS: - if tag.getName()=='text': text=tag.getData() - else: name=tag.getName() - if name in stream_exceptions.keys(): exc=stream_exceptions[name] - else: exc=StreamError - raise exc((name,text)) - - def RegisterCycleHandler(self,handler): - """ Register handler that will be called on every Dispatcher.Process() call. """ - if handler not in self._cycleHandlers: self._cycleHandlers.append(handler) - - def UnregisterCycleHandler(self,handler): - """ Unregister handler that will is called on every Dispatcher.Process() call.""" - if handler in self._cycleHandlers: self._cycleHandlers.remove(handler) - - def Event(self,realm,event,data): - """ Raise some event. Takes three arguments: - 1) "realm" - scope of event. Usually a namespace. - 2) "event" - the event itself. F.e. "SUCESSFULL SEND". - 3) data that comes along with event. Depends on event.""" - if self._eventHandler: self._eventHandler(realm,event,data) - - def dispatch(self,stanza,session=None,direct=0): - """ Main procedure that performs XMPP stanza recognition and calling apppropriate handlers for it. - Called internally. """ - if not session: session=self - session.Stream._mini_dom=None - name=stanza.getName() - - if not direct and self._owner._component: - if name == 'route': - if stanza.getAttr('error') is None: - if len(stanza.getChildren()) == 1: - stanza = stanza.getChildren()[0] - name=stanza.getName() - else: - for each in stanza.getChildren(): - self.dispatch(each,session,direct=1) - return - elif name == 'presence': - return - elif name in ('features','bind'): - pass - else: - raise UnsupportedStanzaType(name) - - if name=='features': session.Stream.features=stanza - - xmlns=stanza.getNamespace() - if not self.handlers.has_key(xmlns): - self.DEBUG("Unknown namespace: " + xmlns,'warn') - xmlns='unknown' - if not self.handlers[xmlns].has_key(name): - self.DEBUG("Unknown stanza: " + name,'warn') - name='unknown' - else: - self.DEBUG("Got %s/%s stanza"%(xmlns,name), 'ok') - - if stanza.__class__.__name__=='Node': stanza=self.handlers[xmlns][name][type](node=stanza) - - typ=stanza.getType() - if not typ: typ='' - stanza.props=stanza.getProperties() - ID=stanza.getID() - - session.DEBUG("Dispatching %s stanza with type->%s props->%s id->%s"%(name,typ,stanza.props,ID),'ok') - - list=['default'] # we will use all handlers: - if self.handlers[xmlns][name].has_key(typ): list.append(typ) # from very common... - for prop in stanza.props: - if self.handlers[xmlns][name].has_key(prop): list.append(prop) - if typ and self.handlers[xmlns][name].has_key(typ+prop): list.append(typ+prop) # ...to very particular - - chain=self.handlers[xmlns]['default']['default'] - for key in list: - if key: chain = chain + self.handlers[xmlns][name][key] - - output='' - if session._expected.has_key(ID): - user=0 - if type(session._expected[ID])==type(()): - cb,args=session._expected[ID] - session.DEBUG("Expected stanza arrived. Callback %s(%s) found!"%(cb,args),'ok') - try: cb(session,stanza,**args) - except Exception, typ: - if typ.__class__.__name__<>'NodeProcessed': raise - else: - session.DEBUG("Expected stanza arrived!",'ok') - session._expected[ID]=stanza - else: user=1 - for handler in chain: - if user or handler['system']: - try: - handler['func'](session,stanza) - except Exception, typ: - if typ.__class__.__name__<>'NodeProcessed': - self._pendingExceptions.insert(0, sys.exc_info()) - return - user=0 - if user and self._defaultHandler: self._defaultHandler(session,stanza) - - def WaitForResponse(self, ID, timeout=None): - """ Block and wait until stanza with specific "id" attribute will come. - If no such stanza is arrived within timeout, return None. - If operation failed for some reason then owner's attributes - lastErrNode, lastErr and lastErrCode are set accordingly. """ - if timeout is None: timeout=DefaultTimeout - self._expected[ID]=None - has_timed_out=0 - abort_time=time.time() + timeout - self.DEBUG("Waiting for ID:%s with timeout %s..." % (ID,timeout),'wait') - while not self._expected[ID]: - if not self.Process(0.04): - self._owner.lastErr="Disconnect" - return None - if time.time() > abort_time: - self._owner.lastErr="Timeout" - return None - response=self._expected[ID] - del self._expected[ID] - if response.getErrorCode(): - self._owner.lastErrNode=response - self._owner.lastErr=response.getError() - self._owner.lastErrCode=response.getErrorCode() - return response - - def SendAndWaitForResponse(self, stanza, timeout=None): - """ Put stanza on the wire and wait for recipient's response to it. """ - if timeout is None: timeout=DefaultTimeout - return self.WaitForResponse(self.send(stanza),timeout) - - def SendAndCallForResponse(self, stanza, func, args={}): - """ Put stanza on the wire and call back when recipient replies. - Additional callback arguments can be specified in args. """ - self._expected[self.send(stanza)]=(func,args) - - def send(self,stanza): - """ Serialise stanza and put it on the wire. Assign an unique ID to it before send. - Returns assigned ID.""" - if type(stanza) in [type(''), type(u'')]: return self._owner_send(stanza) - if not isinstance(stanza,Protocol): _ID=None - elif not stanza.getID(): - global ID - ID+=1 - _ID=`ID` - stanza.setID(_ID) - else: _ID=stanza.getID() - if self._owner._registered_name and not stanza.getAttr('from'): stanza.setAttr('from',self._owner._registered_name) - if self._owner._component and stanza.getName()!='bind': - to=self._owner.Server - if stanza.getTo() and stanza.getTo().getDomain(): - to=stanza.getTo().getDomain() - frm=stanza.getFrom() - if frm.getDomain(): - frm=frm.getDomain() - route=Protocol('route',to=to,frm=frm,payload=[stanza]) - stanza=route - stanza.setNamespace(self._owner.Namespace) - stanza.setParent(self._metastream) - self._owner_send(stanza) - return _ID - - def disconnect(self): - """ Send a stream terminator and and handle all incoming stanzas before stream closure. """ - self._owner_send('') - while self.Process(1): pass diff --git a/src/common/xmpp/examples/run_client_bosh.py b/src/common/xmpp/examples/run_client_bosh.py deleted file mode 100644 index 3ec215a25..000000000 --- a/src/common/xmpp/examples/run_client_bosh.py +++ /dev/null @@ -1,89 +0,0 @@ -# Example script for usage of BOSHClient class -# -------------------------------------------- -# run `python run_client_bosh.py` in gajim/src/common/xmpp/examples/ directory -# and quit with CTRL + c. -# Script will open TCP connection to Connection Manager, send BOSH initial -# request and receive initial response. Handling of init response is not -# done yet. - - -# imports gtk because of gobject.timeout_add() which is used for processing -# idlequeue -# TODO: rewrite to thread timer -import gtk -import gobject -import sys, os.path - -xmpppy_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), '..') -sys.path.append(xmpppy_dir) - -import idlequeue -from client_bosh import BOSHClient - - -class DummyConnection(): - ''' - Dummy class for test run of bosh_client. I use it because in Connection class - the gajim.py module is imported and stuff from it is read in there, so it - would be difficult to debug IMHO. On the other hand, I will have to test with - Connection class sooner or later somehow because that's the main place where - BOSHClient should be used. - DummyConnection class holds and processes IdleQueue for BOSHClient. - ''' - - def __init__(self, iq_interval_ms=1000): - self.classname = self.__class__.__name__ - self.iq_interval_ms = iq_interval_ms - self.idlequeue = idlequeue.IdleQueue() - self.timer=gobject.timeout_add(iq_interval_ms, self.process_idlequeue) - - - def process_idlequeue(self): - ''' - called each iq_interval_ms miliseconds. Checks for idlequeue timeouts. - ''' - self.idlequeue.process() - return True - - # callback stubs follows - def _event_dispatcher(self, realm, event, data): - print "\n>>> %s._event_dispatcher called:" % self.classname - print ">>> realm: %s, event: %s, data: %s\n" % (realm, event, data) - - - def onconsucc(self): - print '%s: CONNECTION SUCCEEDED' % self.classname - - - def onconfail(self, retry=None): - print '%s: CONNECTION FAILED.. retry?: %s' % (self.classname, retry) - - -if __name__ == "__main__": - dc = DummyConnection() - - # you can use my instalation of ejabberd2: - server = 'star.securitynet.cz' - bosh_conn_mgr = 'http://star.securitynet.cz/http-bind/' - - #server='jabbim.cz' - #bosh_conn_mgr='http://bind.jabbim.cz/' - - bc = BOSHClient( - server = server, - bosh_conn_mgr = bosh_conn_mgr, - bosh_port = 80, - on_connect = dc.onconsucc, - on_connect_failure = dc.onconfail, - caller = dc - ) - - bc.set_idlequeue(dc.idlequeue) - - bc.connect() - - try: - gtk.main() - except KeyboardInterrupt: - dc.process_idlequeue() - diff --git a/src/common/xmpp/features.py b/src/common/xmpp/features.py deleted file mode 100644 index 5b4b6fea6..000000000 --- a/src/common/xmpp/features.py +++ /dev/null @@ -1,184 +0,0 @@ -## features.py -## -## Copyright (C) 2003-2004 Alexey "Snake" Nezhdanov -## -## 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 2, 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. - -# $Id: features.py,v 1.22 2005/09/30 20:13:04 mikealbon Exp $ - -""" -This module contains variable stuff that is not worth splitting into separate modules. -Here is: - DISCO client and agents-to-DISCO and browse-to-DISCO emulators. - IBR and password manager. - jabber:iq:privacy methods -All these methods takes 'disp' first argument that should be already connected -(and in most cases already authorised) dispatcher instance. -""" - -from protocol import * - -REGISTER_DATA_RECEIVED='REGISTER DATA RECEIVED' -PRIVACY_LISTS_RECEIVED='PRIVACY LISTS RECEIVED' -PRIVACY_LIST_RECEIVED='PRIVACY LIST RECEIVED' -PRIVACY_LISTS_ACTIVE_DEFAULT='PRIVACY LISTS ACTIVE DEFAULT' - -### DISCO ### http://jabber.org/protocol/disco ### JEP-0030 #################### -### Browse ### jabber:iq:browse ### JEP-0030 ################################### -### Agents ### jabber:iq:agents ### JEP-0030 ################################### -def _discover(disp,ns,jid,node=None,fb2b=0,fb2a=1): - """ Try to obtain info from the remote object. - If remote object doesn't support disco fall back to browse (if fb2b is true) - and if it doesnt support browse (or fb2b is not true) fall back to agents protocol - (if gb2a is true). Returns obtained info. Used internally. """ - iq=Iq(to=jid,typ='get',queryNS=ns) - if node: iq.setQuerynode(node) - rep=disp.SendAndWaitForResponse(iq) - if fb2b and not isResultNode(rep): rep=disp.SendAndWaitForResponse(Iq(to=jid,typ='get',queryNS=NS_BROWSE)) # Fallback to browse - if fb2a and not isResultNode(rep): rep=disp.SendAndWaitForResponse(Iq(to=jid,typ='get',queryNS=NS_AGENTS)) # Fallback to agents - if isResultNode(rep): return rep.getQueryPayload() - return [] - -def discoverItems(disp,jid,node=None): - """ Query remote object about any items that it contains. Return items list. """ - """ According to JEP-0030: - query MAY have node attribute - item: MUST HAVE jid attribute and MAY HAVE name, node, action attributes. - action attribute of item can be either of remove or update value.""" - ret=[] - for i in _discover(disp,NS_DISCO_ITEMS,jid,node): - if i.getName()=='agent' and i.getTag('name'): i.setAttr('name',i.getTagData('name')) - ret.append(i.attrs) - return ret - -def discoverInfo(disp,jid,node=None): - """ Query remote object about info that it publishes. Returns identities and features lists.""" - """ According to JEP-0030: - query MAY have node attribute - identity: MUST HAVE category and name attributes and MAY HAVE type attribute. - feature: MUST HAVE var attribute""" - identities , features = [] , [] - for i in _discover(disp,NS_DISCO_INFO,jid,node): - if i.getName()=='identity': identities.append(i.attrs) - elif i.getName()=='feature': features.append(i.getAttr('var')) - elif i.getName()=='agent': - if i.getTag('name'): i.setAttr('name',i.getTagData('name')) - if i.getTag('description'): i.setAttr('name',i.getTagData('description')) - identities.append(i.attrs) - if i.getTag('groupchat'): features.append(NS_GROUPCHAT) - if i.getTag('register'): features.append(NS_REGISTER) - if i.getTag('search'): features.append(NS_SEARCH) - return identities , features - -### Registration ### jabber:iq:register ### JEP-0077 ########################### -def getRegInfo(disp,host,info={},sync=True): - """ Gets registration form from remote host. - You can pre-fill the info dictionary. - F.e. if you are requesting info on registering user joey than specify - info as {'username':'joey'}. See JEP-0077 for details. - 'disp' must be connected dispatcher instance.""" - iq=Iq('get',NS_REGISTER,to=host) - for i in info.keys(): iq.setTagData(i,info[i]) - if sync: - resp=disp.SendAndWaitForResponse(iq) - _ReceivedRegInfo(disp.Dispatcher,resp, host) - else: disp.SendAndCallForResponse(iq,_ReceivedRegInfo, {'agent': host}) - -def _ReceivedRegInfo(con, resp, agent): - iq=Iq('get',NS_REGISTER,to=agent) - if not isResultNode(resp): return - df=resp.getTag('query',namespace=NS_REGISTER).getTag('x',namespace=NS_DATA) - if df: - con.Event(NS_REGISTER,REGISTER_DATA_RECEIVED,(agent,DataForm(node=df),True)) - return - df=DataForm(typ='form') - for i in resp.getQueryPayload(): - if type(i)<>type(iq): pass - elif i.getName()=='instructions': df.addInstructions(i.getData()) - else: df.setField(i.getName()).setValue(i.getData()) - con.Event(NS_REGISTER,REGISTER_DATA_RECEIVED,(agent,df,False)) - -def register(disp,host,info): - """ Perform registration on remote server with provided info. - disp must be connected dispatcher instance. - Returns true or false depending on registration result. - If registration fails you can get additional info from the dispatcher's owner - attributes lastErrNode, lastErr and lastErrCode. - """ - iq=Iq('set',NS_REGISTER,to=host) - if type(info)<>type({}): info=info.asDict() - for i in info.keys(): iq.setTag('query').setTagData(i,info[i]) - resp=disp.SendAndWaitForResponse(iq) - if isResultNode(resp): return 1 - -def unregister(disp,host): - """ Unregisters with host (permanently removes account). - disp must be connected and authorized dispatcher instance. - Returns true on success.""" - resp=disp.SendAndWaitForResponse(Iq('set',NS_REGISTER,to=host,payload=[Node('remove')])) - if isResultNode(resp): return 1 - -def changePasswordTo(disp,newpassword,host=None): - """ Changes password on specified or current (if not specified) server. - disp must be connected and authorized dispatcher instance. - Returns true on success.""" - if not host: host=disp._owner.Server - resp=disp.SendAndWaitForResponse(Iq('set',NS_REGISTER,to=host,payload=[Node('username',payload=[disp._owner.Server]),Node('password',payload=[newpassword])])) - if isResultNode(resp): return 1 - -### Privacy ### jabber:iq:privacy ### draft-ietf-xmpp-im-19 #################### -#type=[jid|group|subscription] -#action=[allow|deny] - -def getPrivacyLists(disp): - """ Requests privacy lists from connected server. - Returns dictionary of existing lists on success.""" - try: - dict={'lists':[]} - resp=disp.SendAndWaitForResponse(Iq('get',NS_PRIVACY)) - if not isResultNode(resp): return - for list in resp.getQueryPayload(): - if list.getName()=='list': dict['lists'].append(list.getAttr('name')) - else: dict[list.getName()]=list.getAttr('name') - return dict - except: pass - -def getPrivacyList(disp,listname): - """ Requests specific privacy list listname. Returns list of XML nodes (rules) - taken from the server responce.""" - try: - resp=disp.SendAndWaitForResponse(Iq('get',NS_PRIVACY,payload=[Node('list',{'name':listname})])) - if isResultNode(resp): return resp.getQueryPayload()[0] - except: pass - -def setActivePrivacyList(disp,listname=None,typ='active'): - """ Switches privacy list 'listname' to specified type. - By default the type is 'active'. Returns true on success.""" - if listname: attrs={'name':listname} - else: attrs={} - resp=disp.SendAndWaitForResponse(Iq('set',NS_PRIVACY,payload=[Node(typ,attrs)])) - if isResultNode(resp): return 1 - -def setDefaultPrivacyList(disp,listname=None): - """ Sets the default privacy list as 'listname'. Returns true on success.""" - return setActivePrivacyList(disp,listname,'default') - -def setPrivacyList(disp,list): - """ Set the ruleset. 'list' should be the simpleXML node formatted - according to RFC 3921 (XMPP-IM) (I.e. Node('list',{'name':listname},payload=[...]) ) - Returns true on success.""" - resp=disp.SendAndWaitForResponse(Iq('set',NS_PRIVACY,payload=[list])) - if isResultNode(resp): return 1 - -def delPrivacyList(disp,listname): - """ Deletes privacy list 'listname'. Returns true on success.""" - resp=disp.SendAndWaitForResponse(Iq('set',NS_PRIVACY,payload=[Node('list',{'name':listname})])) - if isResultNode(resp): return 1 diff --git a/src/common/xmpp/features_nb.py b/src/common/xmpp/features_nb.py index 786c9f6bd..dd46c7e4e 100644 --- a/src/common/xmpp/features_nb.py +++ b/src/common/xmpp/features_nb.py @@ -15,9 +15,13 @@ # $Id: features.py,v 1.22 2005/09/30 20:13:04 mikealbon Exp $ -from features import REGISTER_DATA_RECEIVED, PRIVACY_LISTS_RECEIVED, PRIVACY_LIST_RECEIVED, PRIVACY_LISTS_ACTIVE_DEFAULT from protocol import * +REGISTER_DATA_RECEIVED='REGISTER DATA RECEIVED' +PRIVACY_LISTS_RECEIVED='PRIVACY LISTS RECEIVED' +PRIVACY_LIST_RECEIVED='PRIVACY LIST RECEIVED' +PRIVACY_LISTS_ACTIVE_DEFAULT='PRIVACY LISTS ACTIVE DEFAULT' + def _on_default_response(disp, iq, cb): def _on_response(resp): if isResultNode(resp): diff --git a/src/common/xmpp/filetransfer.py b/src/common/xmpp/filetransfer.py deleted file mode 100644 index 87ddc2196..000000000 --- a/src/common/xmpp/filetransfer.py +++ /dev/null @@ -1,199 +0,0 @@ -## filetransfer.py -## -## Copyright (C) 2004 Alexey "Snake" Nezhdanov -## -## 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 2, 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. - -# $Id: filetransfer.py,v 1.6 2004/12/25 20:06:59 snakeru Exp $ - -""" -This module contains IBB class that is the simple implementation of JEP-0047. -Note that this is just a transport for data. You have to negotiate data transfer before -(via StreamInitiation most probably). Unfortunately SI is not implemented yet. -""" - -from protocol import * -from dispatcher import PlugIn -import base64 - -class IBB(PlugIn): - """ IBB used to transfer small-sized data chunk over estabilished xmpp connection. - Data is split into small blocks (by default 3000 bytes each), encoded as base 64 - and sent to another entity that compiles these blocks back into the data chunk. - This is very inefficiend but should work under any circumstances. Note that - using IBB normally should be the last resort. - """ - def __init__(self): - """ Initialise internal variables. """ - PlugIn.__init__(self) - self.DBG_LINE='ibb' - self._exported_methods=[self.OpenStream] - self._streams={} - self._ampnode=Node(NS_AMP+' amp',payload=[Node('rule',{'condition':'deliver-at','value':'stored','action':'error'}),Node('rule',{'condition':'match-resource','value':'exact','action':'error'})]) - - def plugin(self,owner): - """ Register handlers for receiving incoming datastreams. Used internally. """ - self._owner.RegisterHandlerOnce('iq',self.StreamOpenReplyHandler) # Move to StreamOpen and specify stanza id - self._owner.RegisterHandler('iq',self.IqHandler,ns=NS_IBB) - self._owner.RegisterHandler('message',self.ReceiveHandler,ns=NS_IBB) - - def IqHandler(self,conn,stanza): - """ Handles streams state change. Used internally. """ - typ=stanza.getType() - self.DEBUG('IqHandler called typ->%s'%typ,'info') - if typ=='set' and stanza.getTag('open',namespace=NS_IBB): self.StreamOpenHandler(conn,stanza) - elif typ=='set' and stanza.getTag('close',namespace=NS_IBB): self.StreamCloseHandler(conn,stanza) - elif typ=='result': self.StreamCommitHandler(conn,stanza) - elif typ=='error': self.StreamOpenReplyHandler(conn,stanza) - else: conn.send(Error(stanza,ERR_BAD_REQUEST)) - raise NodeProcessed - - def StreamOpenHandler(self,conn,stanza): - """ Handles opening of new incoming stream. Used internally. """ - """ - - - -""" - err=None - sid,blocksize=stanza.getTagAttr('open','sid'),stanza.getTagAttr('open','block-size') - self.DEBUG('StreamOpenHandler called sid->%s blocksize->%s'%(sid,blocksize),'info') - try: blocksize=int(blocksize) - except: err=ERR_BAD_REQUEST - if not sid or not blocksize: err=ERR_BAD_REQUEST - elif sid in self._streams.keys(): err=ERR_UNEXPECTED_REQUEST - if err: rep=Error(stanza,err) - else: - self.DEBUG("Opening stream: id %s, block-size %s"%(sid,blocksize),'info') - rep=Protocol('iq',stanza.getFrom(),'result',stanza.getTo(),{'id':stanza.getID()}) - self._streams[sid]={'direction':'<'+str(stanza.getFrom()),'block-size':blocksize,'fp':open('/tmp/xmpp_file_'+sid,'w'),'seq':0,'syn_id':stanza.getID()} - conn.send(rep) - - def OpenStream(self,sid,to,fp,blocksize=3000): - """ Start new stream. You should provide stream id 'sid', the endpoind jid 'to', - the file object containing info for send 'fp'. Also the desired blocksize can be specified. - Take into account that recommended stanza size is 4k and IBB uses base64 encoding - that increases size of data by 1/3.""" - if sid in self._streams.keys(): return - if not JID(to).getResource(): return - self._streams[sid]={'direction':'|>'+to,'block-size':blocksize,'fp':fp,'seq':0} - self._owner.RegisterCycleHandler(self.SendHandler) - syn=Protocol('iq',to,'set',payload=[Node(NS_IBB+' open',{'sid':sid,'block-size':blocksize})]) - self._owner.send(syn) - self._streams[sid]['syn_id']=syn.getID() - return self._streams[sid] - - def SendHandler(self,conn): - """ Send next portion of data if it is time to do it. Used internally. """ - self.DEBUG('SendHandler called','info') - for sid in self._streams.keys(): - stream=self._streams[sid] - if stream['direction'][:2]=='|>': cont=1 - elif stream['direction'][0]=='>': - chunk=stream['fp'].read(stream['block-size']) - if chunk: - datanode=Node(NS_IBB+' data',{'sid':sid,'seq':stream['seq']},base64.encodestring(chunk)) - stream['seq']+=1 - if stream['seq']==65536: stream['seq']=0 - conn.send(Protocol('message',stream['direction'][1:],payload=[datanode,self._ampnode])) - else: - """ notify the other side about stream closing - notify the local user about sucessfull send - delete the local stream""" - conn.send(Protocol('iq',stream['direction'][1:],'set',payload=[Node(NS_IBB+' close',{'sid':sid})])) - conn.Event(self.DBG_LINE,'SUCCESSFULL SEND',stream) - del self._streams[sid] - self._owner.UnregisterCycleHandler(self.SendHandler) - - """ - - - qANQR1DBwU4DX7jmYZnncmUQB/9KuKBddzQH+tZ1ZywKK0yHKnq57kWq+RFtQdCJ - WpdWpR0uQsuJe7+vh3NWn59/gTc5MDlX8dS9p0ovStmNcyLhxVgmqS8ZKhsblVeu - IpQ0JgavABqibJolc3BKrVtVV1igKiX/N7Pi8RtY1K18toaMDhdEfhBRzO/XB0+P - AQhYlRjNacGcslkhXqNjK5Va4tuOAPy2n1Q8UUrHbUd0g+xJ9Bm0G0LZXyvCWyKH - kuNEHFQiLuCY6Iv0myq6iX6tjuHehZlFSh80b5BVV9tNLwNR5Eqz1klxMhoghJOA - - - - - - -""" - - def ReceiveHandler(self,conn,stanza): - """ Receive next portion of incoming datastream and store it write - it to temporary file. Used internally. - """ - sid,seq,data=stanza.getTagAttr('data','sid'),stanza.getTagAttr('data','seq'),stanza.getTagData('data') - self.DEBUG('ReceiveHandler called sid->%s seq->%s'%(sid,seq),'info') - try: seq=int(seq); data=base64.decodestring(data) - except: seq=''; data='' - err=None - if not sid in self._streams.keys(): err=ERR_ITEM_NOT_FOUND - else: - stream=self._streams[sid] - if not data: err=ERR_BAD_REQUEST - elif seq<>stream['seq']: err=ERR_UNEXPECTED_REQUEST - else: - self.DEBUG('Successfull receive sid->%s %s+%s bytes'%(sid,stream['fp'].tell(),len(data)),'ok') - stream['seq']+=1 - stream['fp'].write(data) - if err: - self.DEBUG('Error on receive: %s'%err,'error') - conn.send(Error(Iq(to=stanza.getFrom(),frm=stanza.getTo(),payload=[Node(NS_IBB+' close')]),err,reply=0)) - - def StreamCloseHandler(self,conn,stanza): - """ Handle stream closure due to all data transmitted. - Raise xmpppy event specifying successfull data receive. """ - sid=stanza.getTagAttr('close','sid') - self.DEBUG('StreamCloseHandler called sid->%s'%sid,'info') - if sid in self._streams.keys(): - conn.send(stanza.buildReply('result')) - conn.Event(self.DBG_LINE,'SUCCESSFULL RECEIVE',self._streams[sid]) - del self._streams[sid] - else: conn.send(Error(stanza,ERR_ITEM_NOT_FOUND)) - - def StreamBrokenHandler(self,conn,stanza): - """ Handle stream closure due to all some error while receiving data. - Raise xmpppy event specifying unsuccessfull data receive. """ - syn_id=stanza.getID() - self.DEBUG('StreamBrokenHandler called syn_id->%s'%syn_id,'info') - for sid in self._streams.keys(): - stream=self._streams[sid] - if stream['syn_id']==syn_id: - if stream['direction'][0]=='<': conn.Event(self.DBG_LINE,'ERROR ON RECEIVE',stream) - else: conn.Event(self.DBG_LINE,'ERROR ON SEND',stream) - del self._streams[sid] - - def StreamOpenReplyHandler(self,conn,stanza): - """ Handle remote side reply about is it agree or not to receive our datastream. - Used internally. Raises xmpppy event specfiying if the data transfer - is agreed upon.""" - syn_id=stanza.getID() - self.DEBUG('StreamOpenReplyHandler called syn_id->%s'%syn_id,'info') - for sid in self._streams.keys(): - stream=self._streams[sid] - if stream['syn_id']==syn_id: - if stanza.getType()=='error': - if stream['direction'][0]=='<': conn.Event(self.DBG_LINE,'ERROR ON RECEIVE',stream) - else: conn.Event(self.DBG_LINE,'ERROR ON SEND',stream) - del self._streams[sid] - elif stanza.getType()=='result': - if stream['direction'][0]=='|': - stream['direction']=stream['direction'][1:] - conn.Event(self.DBG_LINE,'STREAM COMMITTED',stream) - else: conn.send(Error(stanza,ERR_UNEXPECTED_REQUEST)) diff --git a/src/common/xmpp/roster.py b/src/common/xmpp/roster.py deleted file mode 100644 index ff64aca5f..000000000 --- a/src/common/xmpp/roster.py +++ /dev/null @@ -1,195 +0,0 @@ -## roster.py -## -## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov -## -## 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 2, 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. - -# $Id: roster.py,v 1.17 2005/05/02 08:38:49 snakeru Exp $ - -""" -Simple roster implementation. Can be used though for different tasks like -mass-renaming of contacts. -""" - -from protocol import * -from client import PlugIn - -class Roster(PlugIn): - """ Defines a plenty of methods that will allow you to manage roster. - Also automatically track presences from remote JIDs taking into - account that every JID can have multiple resources connected. Does not - currently support 'error' presences. - You can also use mapping interface for access to the internal representation of - contacts in roster. - """ - def __init__(self): - """ Init internal variables. """ - PlugIn.__init__(self) - self.DBG_LINE='roster' - self._data = {} - self.set=None - self._exported_methods=[self.getRoster] - - def plugin(self,owner,request=1): - """ Register presence and subscription trackers in the owner's dispatcher. - Also request roster from server if the 'request' argument is set. - Used internally.""" - self._owner.RegisterHandler('iq',self.RosterIqHandler,'result',NS_ROSTER,makefirst=1) - self._owner.RegisterHandler('iq',self.RosterIqHandler,'set',NS_ROSTER) - self._owner.RegisterHandler('presence',self.PresenceHandler) - if request: self.Request() - - def Request(self,force=0): - """ Request roster from server if it were not yet requested - (or if the 'force' argument is set). """ - if self.set is None: self.set=0 - elif not force: return - self._owner.send(Iq('get',NS_ROSTER)) - self.DEBUG('Roster requested from server','start') - - def getRoster(self): - """ Requests roster from server if neccessary and returns self.""" - if not self.set: self.Request() - while not self.set: self._owner.Process(10) - return self - - def RosterIqHandler(self,dis,stanza): - """ Subscription tracker. Used internally for setting items state in - internal roster representation. """ - sender = stanza.getAttr('from') - if not sender is None and not sender.bareMatch( - self._owner.User + '@' + self._owner.Server): - return - for item in stanza.getTag('query').getTags('item'): - jid=item.getAttr('jid') - if item.getAttr('subscription')=='remove': - if self._data.has_key(jid): del self._data[jid] - return - self.DEBUG('Setting roster item %s...'%jid,'ok') - if not self._data.has_key(jid): self._data[jid]={} - self._data[jid]['name']=item.getAttr('name') - self._data[jid]['ask']=item.getAttr('ask') - self._data[jid]['subscription']=item.getAttr('subscription') - self._data[jid]['groups']=[] - if not self._data[jid].has_key('resources'): self._data[jid]['resources']={} - for group in item.getTags('group'): self._data[jid]['groups'].append(group.getData()) - self._data[self._owner.User+'@'+self._owner.Server]={'resources':{},'name':None,'ask':None,'subscription':None,'groups':None,} - self.set=1 - - def PresenceHandler(self,dis,pres): - """ Presence tracker. Used internally for setting items' resources state in - internal roster representation. """ - jid=pres.getFrom() - if not jid: - # If no from attribue, it's from server - jid=self._owner.Server - jid=JID(jid) - if not self._data.has_key(jid.getStripped()): self._data[jid.getStripped()]={'name':None,'ask':None,'subscription':'none','groups':['Not in roster'],'resources':{}} - if type(self._data[jid.getStripped()]['resources'])!=type(dict()): - self._data[jid.getStripped()]['resources']={} - item=self._data[jid.getStripped()] - typ=pres.getType() - - if not typ: - self.DEBUG('Setting roster item %s for resource %s...'%(jid.getStripped(),jid.getResource()),'ok') - item['resources'][jid.getResource()]=res={'show':None,'status':None,'priority':'0','timestamp':None} - if pres.getTag('show'): res['show']=pres.getShow() - if pres.getTag('status'): res['status']=pres.getStatus() - if pres.getTag('priority'): res['priority']=pres.getPriority() - if not pres.getTimestamp(): pres.setTimestamp() - res['timestamp']=pres.getTimestamp() - elif typ=='unavailable' and item['resources'].has_key(jid.getResource()): del item['resources'][jid.getResource()] - # Need to handle type='error' also - - def _getItemData(self,jid,dataname): - """ Return specific jid's representation in internal format. Used internally. """ - jid=jid[:(jid+'/').find('/')] - return self._data[jid][dataname] - def _getResourceData(self,jid,dataname): - """ Return specific jid's resource representation in internal format. Used internally. """ - if jid.find('/')+1: - jid,resource=jid.split('/',1) - if self._data[jid]['resources'].has_key(resource): return self._data[jid]['resources'][resource][dataname] - elif self._data[jid]['resources'].keys(): - lastpri=-129 - for r in self._data[jid]['resources'].keys(): - if int(self._data[jid]['resources'][r]['priority'])>lastpri: resource,lastpri=r,int(self._data[jid]['resources'][r]['priority']) - return self._data[jid]['resources'][resource][dataname] - def delItem(self,jid): - """ Delete contact 'jid' from roster.""" - self._owner.send(Iq('set',NS_ROSTER,payload=[Node('item',{'jid':jid,'subscription':'remove'})])) - def getAsk(self,jid): - """ Returns 'ask' value of contact 'jid'.""" - return self._getItemData(jid,'ask') - def getGroups(self,jid): - """ Returns groups list that contact 'jid' belongs to.""" - return self._getItemData(jid,'groups') - def getName(self,jid): - """ Returns name of contact 'jid'.""" - return self._getItemData(jid,'name') - def getPriority(self,jid): - """ Returns priority of contact 'jid'. 'jid' should be a full (not bare) JID.""" - return self._getResourceData(jid,'priority') - def getRawRoster(self): - """ Returns roster representation in internal format. """ - return self._data - def getRawItem(self,jid): - """ Returns roster item 'jid' representation in internal format. """ - return self._data[jid[:(jid+'/').find('/')]] - def getShow(self, jid): - """ Returns 'show' value of contact 'jid'. 'jid' should be a full (not bare) JID.""" - return self._getResourceData(jid,'show') - def getStatus(self, jid): - """ Returns 'status' value of contact 'jid'. 'jid' should be a full (not bare) JID.""" - return self._getResourceData(jid,'status') - def getSubscription(self,jid): - """ Returns 'subscription' value of contact 'jid'.""" - return self._getItemData(jid,'subscription') - def getResources(self,jid): - """ Returns list of connected resources of contact 'jid'.""" - return self._data[jid[:(jid+'/').find('/')]]['resources'].keys() - def setItem(self,jid,name=None,groups=[]): - """ Renames contact 'jid' and sets the groups list that it now belongs to.""" - iq=Iq('set',NS_ROSTER) - query=iq.getTag('query') - attrs={'jid':jid} - if name: attrs['name']=name - item=query.setTag('item',attrs) - for group in groups: item.addChild(node=Node('group',payload=[group])) - self._owner.send(iq) - def getItems(self): - """ Return list of all [bare] JIDs that the roster is currently tracks.""" - return self._data.keys() - def keys(self): - """ Same as getItems. Provided for the sake of dictionary interface.""" - return self._data.keys() - def __getitem__(self,item): - """ Get the contact in the internal format. Raises KeyError if JID 'item' is not in roster.""" - return self._data[item] - def getItem(self,item): - """ Get the contact in the internal format (or None if JID 'item' is not in roster).""" - if self._data.has_key(item): return self._data[item] - def Subscribe(self,jid): - """ Send subscription request to JID 'jid'.""" - self._owner.send(Presence(jid,'subscribe')) - def Unsubscribe(self,jid): - """ Ask for removing our subscription for JID 'jid'.""" - self._owner.send(Presence(jid,'unsubscribe')) - def Authorize(self,jid): - """ Authorise JID 'jid'. Works only if these JID requested auth previously. """ - self._owner.send(Presence(jid,'subscribed')) - def Unauthorize(self,jid): - """ Unauthorise JID 'jid'. Use for declining authorisation request - or for removing existing authorization. """ - self._owner.send(Presence(jid,'unsubscribed')) - def getRaw(self): - """Returns the internal data representation of the roster.""" - return self._data diff --git a/src/common/xmpp/roster_nb.py b/src/common/xmpp/roster_nb.py index 307ffd9b1..b843525e7 100644 --- a/src/common/xmpp/roster_nb.py +++ b/src/common/xmpp/roster_nb.py @@ -1,8 +1,8 @@ ## roster_nb.py -## based on roster.py +## based on roster.py ## ## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov -## modified by Dimitur Kirov +## modified by Dimitur Kirov ## ## 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 @@ -21,10 +21,168 @@ Simple roster implementation. Can be used though for different tasks like mass-renaming of contacts. ''' -from roster import Roster -from protocol import NS_ROSTER +from protocol import * +from client import PlugIn + +class NonBlockingRoster(PlugIn): + """ Defines a plenty of methods that will allow you to manage roster. + Also automatically track presences from remote JIDs taking into + account that every JID can have multiple resources connected. Does not + currently support 'error' presences. + You can also use mapping interface for access to the internal representation of + contacts in roster. + """ + def __init__(self): + """ Init internal variables. """ + PlugIn.__init__(self) + self.DBG_LINE='roster' + self._data = {} + self.set=None + self._exported_methods=[self.getRoster] + + def Request(self,force=0): + """ Request roster from server if it were not yet requested + (or if the 'force' argument is set). """ + if self.set is None: self.set=0 + elif not force: return + self._owner.send(Iq('get',NS_ROSTER)) + self.DEBUG('Roster requested from server','start') + + def RosterIqHandler(self,dis,stanza): + """ Subscription tracker. Used internally for setting items state in + internal roster representation. """ + sender = stanza.getAttr('from') + if not sender is None and not sender.bareMatch( + self._owner.User + '@' + self._owner.Server): + return + for item in stanza.getTag('query').getTags('item'): + jid=item.getAttr('jid') + if item.getAttr('subscription')=='remove': + if self._data.has_key(jid): del self._data[jid] + return + self.DEBUG('Setting roster item %s...'%jid,'ok') + if not self._data.has_key(jid): self._data[jid]={} + self._data[jid]['name']=item.getAttr('name') + self._data[jid]['ask']=item.getAttr('ask') + self._data[jid]['subscription']=item.getAttr('subscription') + self._data[jid]['groups']=[] + if not self._data[jid].has_key('resources'): self._data[jid]['resources']={} + for group in item.getTags('group'): self._data[jid]['groups'].append(group.getData()) + self._data[self._owner.User+'@'+self._owner.Server]={'resources':{},'name':None,'ask':None,'subscription':None,'groups':None,} + self.set=1 + + def PresenceHandler(self,dis,pres): + """ Presence tracker. Used internally for setting items' resources state in + internal roster representation. """ + jid=pres.getFrom() + if not jid: + # If no from attribue, it's from server + jid=self._owner.Server + jid=JID(jid) + if not self._data.has_key(jid.getStripped()): self._data[jid.getStripped()]={'name':None,'ask':None,'subscription':'none','groups':['Not in roster'],'resources':{}} + if type(self._data[jid.getStripped()]['resources'])!=type(dict()): + self._data[jid.getStripped()]['resources']={} + item=self._data[jid.getStripped()] + typ=pres.getType() + + if not typ: + self.DEBUG('Setting roster item %s for resource %s...'%(jid.getStripped(),jid.getResource()),'ok') + item['resources'][jid.getResource()]=res={'show':None,'status':None,'priority':'0','timestamp':None} + if pres.getTag('show'): res['show']=pres.getShow() + if pres.getTag('status'): res['status']=pres.getStatus() + if pres.getTag('priority'): res['priority']=pres.getPriority() + if not pres.getTimestamp(): pres.setTimestamp() + res['timestamp']=pres.getTimestamp() + elif typ=='unavailable' and item['resources'].has_key(jid.getResource()): del item['resources'][jid.getResource()] + # Need to handle type='error' also + + def _getItemData(self,jid,dataname): + """ Return specific jid's representation in internal format. Used internally. """ + jid=jid[:(jid+'/').find('/')] + return self._data[jid][dataname] + def _getResourceData(self,jid,dataname): + """ Return specific jid's resource representation in internal format. Used internally. """ + if jid.find('/')+1: + jid,resource=jid.split('/',1) + if self._data[jid]['resources'].has_key(resource): return self._data[jid]['resources'][resource][dataname] + elif self._data[jid]['resources'].keys(): + lastpri=-129 + for r in self._data[jid]['resources'].keys(): + if int(self._data[jid]['resources'][r]['priority'])>lastpri: resource,lastpri=r,int(self._data[jid]['resources'][r]['priority']) + return self._data[jid]['resources'][resource][dataname] + def delItem(self,jid): + """ Delete contact 'jid' from roster.""" + self._owner.send(Iq('set',NS_ROSTER,payload=[Node('item',{'jid':jid,'subscription':'remove'})])) + def getAsk(self,jid): + """ Returns 'ask' value of contact 'jid'.""" + return self._getItemData(jid,'ask') + def getGroups(self,jid): + """ Returns groups list that contact 'jid' belongs to.""" + return self._getItemData(jid,'groups') + def getName(self,jid): + """ Returns name of contact 'jid'.""" + return self._getItemData(jid,'name') + def getPriority(self,jid): + """ Returns priority of contact 'jid'. 'jid' should be a full (not bare) JID.""" + return self._getResourceData(jid,'priority') + def getRawRoster(self): + """ Returns roster representation in internal format. """ + return self._data + def getRawItem(self,jid): + """ Returns roster item 'jid' representation in internal format. """ + return self._data[jid[:(jid+'/').find('/')]] + def getShow(self, jid): + """ Returns 'show' value of contact 'jid'. 'jid' should be a full (not bare) JID.""" + return self._getResourceData(jid,'show') + def getStatus(self, jid): + """ Returns 'status' value of contact 'jid'. 'jid' should be a full (not bare) JID.""" + return self._getResourceData(jid,'status') + def getSubscription(self,jid): + """ Returns 'subscription' value of contact 'jid'.""" + return self._getItemData(jid,'subscription') + def getResources(self,jid): + """ Returns list of connected resources of contact 'jid'.""" + return self._data[jid[:(jid+'/').find('/')]]['resources'].keys() + def setItem(self,jid,name=None,groups=[]): + """ Renames contact 'jid' and sets the groups list that it now belongs to.""" + iq=Iq('set',NS_ROSTER) + query=iq.getTag('query') + attrs={'jid':jid} + if name: attrs['name']=name + item=query.setTag('item',attrs) + for group in groups: item.addChild(node=Node('group',payload=[group])) + self._owner.send(iq) + def getItems(self): + """ Return list of all [bare] JIDs that the roster is currently tracks.""" + return self._data.keys() + def keys(self): + """ Same as getItems. Provided for the sake of dictionary interface.""" + return self._data.keys() + def __getitem__(self,item): + """ Get the contact in the internal format. Raises KeyError if JID 'item' is not in roster.""" + return self._data[item] + def getItem(self,item): + """ Get the contact in the internal format (or None if JID 'item' is not in roster).""" + if self._data.has_key(item): return self._data[item] + def Subscribe(self,jid): + """ Send subscription request to JID 'jid'.""" + self._owner.send(Presence(jid,'subscribe')) + def Unsubscribe(self,jid): + """ Ask for removing our subscription for JID 'jid'.""" + self._owner.send(Presence(jid,'unsubscribe')) + def Authorize(self,jid): + """ Authorise JID 'jid'. Works only if these JID requested auth previously. """ + self._owner.send(Presence(jid,'subscribed')) + def Unauthorize(self,jid): + """ Unauthorise JID 'jid'. Use for declining authorisation request + or for removing existing authorization. """ + self._owner.send(Presence(jid,'unsubscribed')) + def getRaw(self): + """Returns the internal data representation of the roster.""" + return self._data + # copypasted methods for roster.py from constructor to here + -class NonBlockingRoster(Roster): def plugin(self, owner, request=1): ''' Register presence and subscription trackers in the owner's dispatcher. Also request roster from server if the 'request' argument is set. diff --git a/src/common/xmpp/session.py b/src/common/xmpp/session.py deleted file mode 100644 index b61e4f6de..000000000 --- a/src/common/xmpp/session.py +++ /dev/null @@ -1,350 +0,0 @@ -## -## XMPP server -## -## Copyright (C) 2004 Alexey "Snake" Nezhdanov -## -## 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 2, 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. - -__version__="$Id" - -""" -When your handler is called it is getting the session instance as the first argument. -This is the difference from xmpppy 0.1 where you got the "Client" instance. -With Session class you can have "multi-session" client instead of having -one client for each connection. Is is specifically important when you are -writing the server. -""" - -from protocol import * - -# Transport-level flags -SOCKET_UNCONNECTED =0 -SOCKET_ALIVE =1 -SOCKET_DEAD =2 -# XML-level flags -STREAM__NOT_OPENED =1 -STREAM__OPENED =2 -STREAM__CLOSING =3 -STREAM__CLOSED =4 -# XMPP-session flags -SESSION_NOT_AUTHED =1 -SESSION_AUTHED =2 -SESSION_BOUND =3 -SESSION_OPENED =4 -SESSION_CLOSED =5 - -class Session: - """ - The Session class instance is used for storing all session-related info like - credentials, socket/xml stream/session state flags, roster items (in case of - client type connection) etc. - Session object have no means of discovering is any info is ready to be read. - Instead you should use poll() (recomended) or select() methods for this purpose. - Session can be one of two types: 'server' and 'client'. 'server' session handles - inbound connection and 'client' one used to create an outbound one. - Session instance have multitude of internal attributes. The most imporant is the 'peer' one. - It is set once the peer is authenticated (client). - """ - def __init__(self,socket,owner,xmlns=None,peer=None): - """ When the session is created it's type (client/server) is determined from the beginning. - socket argument is the pre-created socket-like object. - It must have the following methods: send, recv, fileno, close. - owner is the 'master' instance that have Dispatcher plugged into it and generally - will take care about all session events. - xmlns is the stream namespace that will be used. Client must set this argument - If server sets this argument than stream will be dropped if opened with some another namespace. - peer is the name of peer instance. This is the flag that differentiates client session from - server session. Client must set it to the name of the server that will be connected, server must - leave this argument alone. - """ - self.xmlns=xmlns - if peer: - self.TYP='client' - self.peer=peer - self._socket_state=SOCKET_UNCONNECTED - else: - self.TYP='server' - self.peer=None - self._socket_state=SOCKET_ALIVE - self._sock=socket - self._send=socket.send - self._recv=socket.recv - self.fileno=socket.fileno - self._registered=0 - - self.Dispatcher=owner.Dispatcher - self.DBG_LINE='session' - self.DEBUG=owner.Dispatcher.DEBUG - self._expected={} - self._owner=owner - if self.TYP=='server': self.ID=`random.random()`[2:] - else: self.ID=None - - self.sendbuffer='' - self._stream_pos_queued=None - self._stream_pos_sent=0 - self.deliver_key_queue=[] - self.deliver_queue_map={} - self.stanza_queue=[] - - self._session_state=SESSION_NOT_AUTHED - self.waiting_features=[] - for feature in [NS_TLS,NS_SASL,NS_BIND,NS_SESSION]: - if feature in owner.features: self.waiting_features.append(feature) - self.features=[] - self.feature_in_process=None - self.slave_session=None - self.StartStream() - - def StartStream(self): - """ This method is used to initialise the internal xml expat parser - and to send initial stream header (in case of client connection). - Should be used after initial connection and after every stream restart.""" - self._stream_state=STREAM__NOT_OPENED - self.Stream=simplexml.NodeBuilder() - self.Stream._dispatch_depth=2 - self.Stream.dispatch=self._dispatch - self.Parse=self.Stream.Parse - self.Stream.stream_footer_received=self._stream_close - if self.TYP=='client': - self.Stream.stream_header_received=self._catch_stream_id - self._stream_open() - else: - self.Stream.stream_header_received=self._stream_open - - def receive(self): - """ Reads all pending incoming data. - Raises IOError on disconnection. - Blocks until at least one byte is read.""" - try: received = self._recv(10240) - except: received = '' - - if len(received): # length of 0 means disconnect - self.DEBUG(`self.fileno()`+' '+received,'got') - else: - self.DEBUG('Socket error while receiving data','error') - self.set_socket_state(SOCKET_DEAD) - raise IOError("Peer disconnected") - return received - - def sendnow(self,chunk): - """ Put chunk into "immidiatedly send" queue. - Should only be used for auth/TLS stuff and like. - If you just want to shedule regular stanza for delivery use enqueue method. - """ - if isinstance(chunk,Node): chunk = chunk.__str__().encode('utf-8') - elif type(chunk)==type(u''): chunk = chunk.encode('utf-8') - self.enqueue(chunk) - - def enqueue(self,stanza): - """ Takes Protocol instance as argument. - Puts stanza into "send" fifo queue. Items into the send queue are hold until - stream authenticated. After that this method is effectively the same as "sendnow" method.""" - if isinstance(stanza,Protocol): - self.stanza_queue.append(stanza) - else: self.sendbuffer+=stanza - if self._socket_state>=SOCKET_ALIVE: self.push_queue() - - def push_queue(self,failreason=ERR_RECIPIENT_UNAVAILABLE): - """ If stream is authenticated than move items from "send" queue to "immidiatedly send" queue. - Else if the stream is failed then return all queued stanzas with error passed as argument. - Otherwise do nothing.""" - # If the stream authed - convert stanza_queue into sendbuffer and set the checkpoints - - if self._stream_state>=STREAM__CLOSED or self._socket_state>=SOCKET_DEAD: # the stream failed. Return all stanzas that are still waiting for delivery. - self._owner.deactivatesession(self) - for key in self.deliver_key_queue: # Not sure. May be I - self._dispatch(Error(self.deliver_queue_map[key],failreason),trusted=1) # should simply re-dispatch it? - for stanza in self.stanza_queue: # But such action can invoke - self._dispatch(Error(stanza,failreason),trusted=1) # Infinite loops in case of S2S connection... - self.deliver_queue_map,self.deliver_key_queue,self.stanza_queue={},[],[] - return - elif self._session_state>=SESSION_AUTHED: # FIXME! - . - #### LOCK_QUEUE - for stanza in self.stanza_queue: - txt=stanza.__str__().encode('utf-8') - self.sendbuffer+=txt - self._stream_pos_queued+=len(txt) # should be re-evaluated for SSL connection. - self.deliver_queue_map[self._stream_pos_queued]=stanza # position of the stream when stanza will be successfully and fully sent - self.deliver_key_queue.append(self._stream_pos_queued) - self.stanza_queue=[] - #### UNLOCK_QUEUE - - def flush_queue(self): - """ Put the "immidiatedly send" queue content on the wire. Blocks until at least one byte sent.""" - if self.sendbuffer: - try: - # LOCK_QUEUE - sent=self._send(self.sendbuffer) # blocking socket - except: - # UNLOCK_QUEUE - self.set_socket_state(SOCKET_DEAD) - self.DEBUG("Socket error while sending data",'error') - return self.terminate_stream() - self.DEBUG(`self.fileno()`+' '+self.sendbuffer[:sent],'sent') - self._stream_pos_sent+=sent - self.sendbuffer=self.sendbuffer[sent:] - self._stream_pos_delivered=self._stream_pos_sent # Should be acquired from socket somehow. Take SSL into account. - while self.deliver_key_queue and self._stream_pos_delivered>self.deliver_key_queue[0]: - del self.deliver_queue_map[self.deliver_key_queue[0]] - self.deliver_key_queue.remove(self.deliver_key_queue[0]) - # UNLOCK_QUEUE - - def _dispatch(self,stanza,trusted=0): - """ This is callback that is used to pass the received stanza forth to owner's dispatcher - _if_ the stream is authorised. Otherwise the stanza is just dropped. - The 'trusted' argument is used to emulate stanza receive. - This method is used internally. - """ - self._owner.packets+=1 - print self._owner.packets - if self._stream_state==STREAM__OPENED or trusted: # if the server really should reject all stanzas after he is closed stream (himeself)? - self.DEBUG(stanza.__str__(),'dispatch') - stanza.trusted=trusted - return self.Dispatcher.dispatch(stanza,self) - - def _catch_stream_id(self,ns=None,tag='stream',attrs={}): - """ This callback is used to detect the stream namespace of incoming stream. Used internally. """ - if not attrs.has_key('id') or not attrs['id']: - return self.terminate_stream(STREAM_INVALID_XML) - self.ID=attrs['id'] - if not attrs.has_key('version'): self._owner.Dialback(self) - - def _stream_open(self,ns=None,tag='stream',attrs={}): - """ This callback is used to handle opening stream tag of the incoming stream. - In the case of client session it just make some validation. - Server session also sends server headers and if the stream valid the features node. - Used internally. """ - text='\n') - self.set_stream_state(STREAM__OPENED) - if self.TYP=='client': return - if tag<>'stream': return self.terminate_stream(STREAM_INVALID_XML) - if ns<>NS_STREAMS: return self.terminate_stream(STREAM_INVALID_NAMESPACE) - if self.Stream.xmlns<>self.xmlns: return self.terminate_stream(STREAM_BAD_NAMESPACE_PREFIX) - if not attrs.has_key('to'): return self.terminate_stream(STREAM_IMPROPER_ADDRESSING) - if attrs['to'] not in self._owner.servernames: return self.terminate_stream(STREAM_HOST_UNKNOWN) - self.ourname=attrs['to'].lower() - if self.TYP=='server' and attrs.has_key('version'): - # send features - features=Node('stream:features') - if NS_TLS in self.waiting_features: - features.T.starttls.setNamespace(NS_TLS) - features.T.starttls.T.required - if NS_SASL in self.waiting_features: - features.T.mechanisms.setNamespace(NS_SASL) - for mec in self._owner.SASL.mechanisms: - features.T.mechanisms.NT.mechanism=mec - else: - if NS_BIND in self.waiting_features: features.T.bind.setNamespace(NS_BIND) - if NS_SESSION in self.waiting_features: features.T.session.setNamespace(NS_SESSION) - self.sendnow(features) - - def feature(self,feature): - """ Declare some stream feature as activated one. """ - if feature not in self.features: self.features.append(feature) - self.unfeature(feature) - - def unfeature(self,feature): - """ Declare some feature as illegal. Illegal features can not be used. - Example: BIND feature becomes illegal after Non-SASL auth. """ - if feature in self.waiting_features: self.waiting_features.remove(feature) - - def _stream_close(self,unregister=1): - """ Write the closing stream tag and destroy the underlaying socket. Used internally. """ - if self._stream_state>=STREAM__CLOSED: return - self.set_stream_state(STREAM__CLOSING) - self.sendnow('') - self.set_stream_state(STREAM__CLOSED) - self.push_queue() # decompose queue really since STREAM__CLOSED - self._owner.flush_queues() - if unregister: self._owner.unregistersession(self) - self._destroy_socket() - - def terminate_stream(self,error=None,unregister=1): - """ Notify the peer about stream closure. - Ensure that xmlstream is not brokes - i.e. if the stream isn't opened yet - - open it before closure. - If the error condition is specified than create a stream error and send it along with - closing stream tag. - Emulate receiving 'unavailable' type presence just before stream closure. - """ - if self._stream_state>=STREAM__CLOSING: return - if self._stream_statef: raise "Stopping feature %s instead of %s !"%(f,self.feature_in_process) - self.feature_in_process=None - - def set_socket_state(self,newstate): - """ Change the underlaying socket state. - Socket starts with SOCKET_UNCONNECTED state - and then proceeds (possibly) to SOCKET_ALIVE - and then to SOCKET_DEAD """ - if self._socket_state=SESSION_AUTHED: self._stream_pos_queued=self._stream_pos_sent - self._session_state=newstate - - def set_stream_state(self,newstate): - """ Change the underlaying XML stream state - Stream starts with STREAM__NOT_OPENED and then proceeds with - STREAM__OPENED, STREAM__CLOSING and STREAM__CLOSED states. - Note that some features (like TLS and SASL) - requires stream re-start so this state can have non-linear changes. """ - if self._stream_state 0: - errnum = ex.args[0] - else: - errnum = -1 - if errnum == errno.EINTR: - continue - else: - raise - -class error: - """An exception to be raised in case of low-level errors in methods of 'transports' module.""" - def __init__(self,comment): - """Cache the descriptive string""" - self._comment=comment - - def __str__(self): - """Serialise exception into pre-cached descriptive string.""" - return self._comment - -class TCPsocket(PlugIn): - """ This class defines direct TCP connection method. """ - def __init__(self, server=None, use_srv=True): - """ Cache connection point 'server'. 'server' is the tuple of (host, port) - absolutely the same as standard tcp socket uses. """ - PlugIn.__init__(self) - self.DBG_LINE='socket' - self._exported_methods=[self.send,self.disconnect] - - self._server = server - - def plugin(self, owner): - """ Fire up connection. Return non-empty string on success. - Also registers self.disconnected method in the owner's dispatcher. - Called internally. """ - if not self._server: self._server=(self._owner.Server,5222) - if not self.connect(self._server): return - self._owner.Connection=self - self._owner.RegisterDisconnectHandler(self.disconnected) - return 'ok' - - def getHost(self): - """ Return the 'host' value that is connection is [will be] made to.""" - return self._server[0] - def getPort(self): - """ Return the 'port' value that is connection is [will be] made to.""" - return self._server[1] - - def connect(self,server=None): - """ Try to connect. Returns non-empty string on success. """ - try: - if not server: - server=self._server - for ai in socket.getaddrinfo(server[0],server[1],socket.AF_UNSPEC,socket.SOCK_STREAM): - try: - self._sock=socket.socket(*ai[:3]) - self._sock.connect(ai[4]) - self._send=self._sock.sendall - self._recv=self._sock.recv - self.DEBUG("Successfully connected to remote host %s"%`server`,'start') - return 'ok' - except: continue - except: pass - - def plugout(self): - """ Disconnect from the remote server and unregister self.disconnected method from - the owner's dispatcher. """ - self._owner.DeregisterDisconnectHandler(self.disconnected) - self.shutdown() - del self._owner.Connection - - def receive(self): - """ Reads all pending incoming data. Calls owner's disconnected() method if appropriate.""" - try: received = self._recv(1024000) - except: received = '' - - while temp_failure_retry(select.select,[self._sock],[],[],0)[0]: - try: add = self._recv(1024000) - except: add='' - received +=add - if not add: break - - if len(received): # length of 0 means disconnect - self.DEBUG(received,'got') - if hasattr(self._owner, 'Dispatcher'): - self._owner.Dispatcher.Event('', DATA_RECEIVED, received) - else: - self.DEBUG('Socket error while receiving data','error') - self._owner.disconnected() - return received - - def send(self,raw_data): - """ Writes raw outgoing data. Blocks until done. - If supplied data is unicode string, encodes it to utf-8 before send.""" - if type(raw_data)==type(u''): raw_data = raw_data.encode('utf-8') - elif type(raw_data)<>type(''): raw_data = ustr(raw_data).encode('utf-8') - try: - self._send(raw_data) - # Avoid printing messages that are empty keepalive packets. - if raw_data.strip(): - self.DEBUG(raw_data,'sent') - self._owner.Dispatcher.Event('', DATA_SENT, raw_data) - except: - self.DEBUG("Socket error while sending data",'error') - self._owner.disconnected() - - def pending_data(self,timeout=0): - """ Returns true if there is a data ready to be read. """ - return temp_failure_retry(select.select,[self._sock],[],[],timeout)[0] - - def disconnect(self): - """ Closes the socket. """ - self.DEBUG("Closing socket",'stop') - self._sock.close() - - def disconnected(self): - """ Called when a Network Error or disconnection occurs. - Designed to be overidden. """ - self.DEBUG("Socket operation failed",'error') - -DBG_CONNECT_PROXY='CONNECTproxy' -class HTTPPROXYsocket(TCPsocket): - """ HTTP (CONNECT) proxy connection class. Uses TCPsocket as the base class - redefines only connect method. Allows to use HTTP proxies like squid with - (optionally) simple authentication (using login and password). """ - def __init__(self,proxy,server,use_srv=True): - """ Caches proxy and target addresses. - 'proxy' argument is a dictionary with mandatory keys 'host' and 'port' (proxy address) - and optional keys 'user' and 'password' to use for authentication. - 'server' argument is a tuple of host and port - just like TCPsocket uses. """ - TCPsocket.__init__(self,server,use_srv) - self.DBG_LINE=DBG_CONNECT_PROXY - self._proxy=proxy - - def plugin(self, owner): - """ Starts connection. Used interally. Returns non-empty string on success.""" - owner.debug_flags.append(DBG_CONNECT_PROXY) - return TCPsocket.plugin(self,owner) - - def connect(self,dupe=None): - """ Starts connection. Connects to proxy, supplies login and password to it - (if were specified while creating instance). Instructs proxy to make - connection to the target server. Returns non-empty sting on success. """ - if not TCPsocket.connect(self,(self._proxy['host'],self._proxy['port'])): return - self.DEBUG("Proxy server contacted, performing authentification",'start') - connector = ['CONNECT %s:%s HTTP/1.0'%self._server, - 'Proxy-Connection: Keep-Alive', - 'Pragma: no-cache', - 'Host: %s:%s'%self._server, - 'User-Agent: HTTPPROXYsocket/v0.1'] - if self._proxy.has_key('user') and self._proxy.has_key('password'): - credentials = '%s:%s'%(self._proxy['user'],self._proxy['password']) - credentials = base64.encodestring(credentials).strip() - connector.append('Proxy-Authorization: Basic '+credentials) - connector.append('\r\n') - self.send('\r\n'.join(connector)) - try: reply = self.receive().replace('\r','') - except IOError: - self.DEBUG('Proxy suddenly disconnected','error') - self._owner.disconnected() - return - try: proto,code,desc=reply.split('\n')[0].split(' ',2) - except: raise error('Invalid proxy reply') - if code<>'200': - self.DEBUG('Invalid proxy reply: %s %s %s'%(proto,code,desc),'error') - self._owner.disconnected() - return - while reply.find('\n\n') == -1: - try: reply += self.receive().replace('\r','') - except IOError: - self.DEBUG('Proxy suddenly disconnected','error') - self._owner.disconnected() - return - self.DEBUG("Authentification successfull. Jabber server contacted.",'ok') - return 'ok' - - def DEBUG(self,text,severity): - """Overwrites DEBUG tag to allow debug output be presented as "CONNECTproxy".""" - return self._owner.DEBUG(DBG_CONNECT_PROXY,text,severity) - -class TLS(PlugIn): - """ TLS connection used to encrypts already estabilished tcp connection.""" - def PlugIn(self,owner,now=0): - """ If the 'now' argument is true then starts using encryption immidiatedly. - If 'now' in false then starts encryption as soon as TLS feature is - declared by the server (if it were already declared - it is ok). - """ - if owner.__dict__.has_key('TLS'): return # Already enabled. - PlugIn.PlugIn(self,owner) - DBG_LINE='TLS' - if now: return self._startSSL() - if self._owner.Dispatcher.Stream.features: - try: self.FeaturesHandler(self._owner.Dispatcher,self._owner.Dispatcher.Stream.features) - except NodeProcessed: pass - else: self._owner.RegisterHandlerOnce('features',self.FeaturesHandler,xmlns=NS_STREAMS) - self.starttls=None - - def plugout(self,now=0): - """ Unregisters TLS handler's from owner's dispatcher. Take note that encription - can not be stopped once started. You can only break the connection and start over.""" - self._owner.UnregisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS) -# self._owner.UnregisterHandlerOnce('proceed',self.StartTLSHandler,xmlns=NS_TLS) -# self._owner.UnregisterHandlerOnce('failure',self.StartTLSHandler,xmlns=NS_TLS) - - def FeaturesHandler(self, conn, feats): - """ Used to analyse server tag for TLS support. - If TLS is supported starts the encryption negotiation. Used internally""" - if not feats.getTag('starttls',namespace=NS_TLS): - self.DEBUG("TLS unsupported by remote server.",'warn') - return - self.DEBUG("TLS supported by remote server. Requesting TLS start.",'ok') - self._owner.RegisterHandlerOnce('proceed',self.StartTLSHandler,xmlns=NS_TLS) - self._owner.RegisterHandlerOnce('failure',self.StartTLSHandler,xmlns=NS_TLS) - self._owner.Connection.send(''%NS_TLS) - raise NodeProcessed - - def pending_data(self,timeout=0): - """ Returns true if there possible is a data ready to be read. """ - return self._tcpsock._seen_data or select.select([self._tcpsock._sock],[],[],timeout)[0] - - def _startSSL(self): - """ Immidiatedly switch socket to TLS mode. Used internally.""" - """ Here we should switch pending_data to hint mode.""" - tcpsock=self._owner.Connection - tcpsock._sslObj = socket.ssl(tcpsock._sock, None, None) - tcpsock._sslIssuer = tcpsock._sslObj.issuer() - tcpsock._sslServer = tcpsock._sslObj.server() - tcpsock._recv = tcpsock._sslObj.read - tcpsock._send = tcpsock._sslObj.write - self.starttls='success' - - def StartTLSHandler(self, conn, starttls): - """ Handle server reply if TLS is allowed to process. Behaves accordingly. - Used internally.""" - if starttls.getNamespace()<>NS_TLS: return - self.starttls=starttls.getName() - if self.starttls=='failure': - self.DEBUG("Got starttls response: "+self.starttls,'error') - return - self.DEBUG("Got starttls proceed response. Switching to TLS/SSL...",'ok') - self._startSSL() - self._owner.Dispatcher.PlugOut() - dispatcher.Dispatcher().PlugIn(self._owner) diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py index 2c3364d45..0ae25c24e 100644 --- a/src/common/xmpp/transports_nb.py +++ b/src/common/xmpp/transports_nb.py @@ -20,7 +20,6 @@ from simplexml import ustr from client import PlugIn from idlequeue import IdleObject from protocol import * -from transports import * import sys import os @@ -34,16 +33,19 @@ import logging log = logging.getLogger('gajim.c.x.transports_nb') # I don't need to load gajim.py just because of few TLS variables, so I changed -# :%s/common\.gajim\.DATA_DIR/\'\.\.\/data\'/c -# :%s/common\.gajim\.MY_CACERTS/\'\%s\/\.gajim\/cacerts\.pem\' \% os\.environ\[\'HOME\'\]/c +# %s/common\.gajim\.DATA_DIR/\'\.\.\/data\'/c +# %s/common\.gajim\.MY_CACERTS/\'\%s\/\.gajim\/cacerts\.pem\' \% os\.environ\[\'HOME\'\]/c # To change it back do: # %s/\'\.\.\/data\'/common\.gajim\.DATA_DIR/c -# :%s/\'\%s\/\.gajim\/cacerts\.pem\' \% os\.environ\[\'HOME\'\]/common\.gajim\.MY_CACERTS/c +# %s/\'%s\/\.gajim\/cacerts\.pem\'\ %\ os\.environ\[\'HOME\'\]/common\.gajim\.MY_CACERTS/c -# import common.gajim +import common.gajim +DATA_RECEIVED='DATA RECEIVED' +DATA_SENT='DATA SENT' + USE_PYOPENSSL = False try: @@ -771,16 +773,16 @@ class NonBlockingTLS(PlugIn): #tcpsock._sslContext = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD) tcpsock.ssl_errnum = 0 tcpsock._sslContext.set_verify(OpenSSL.SSL.VERIFY_PEER, self._ssl_verify_callback) - cacerts = os.path.join('../data', 'other', 'cacerts.pem') + cacerts = os.path.join(common.gajim.DATA_DIR, 'other', 'cacerts.pem') try: tcpsock._sslContext.load_verify_locations(cacerts) except: log.warning('Unable to load SSL certificats from file %s' % \ os.path.abspath(cacerts)) # load users certs - if os.path.isfile('%s/.gajim/cacerts.pem' % os.environ['HOME']): + if os.path.isfile(common.gajim.MY_CACERTS): store = tcpsock._sslContext.get_cert_store() - f = open('%s/.gajim/cacerts.pem' % os.environ['HOME']) + f = open(common.gajim.MY_CACERTS) lines = f.readlines() i = 0 begin = -1 @@ -795,11 +797,11 @@ class NonBlockingTLS(PlugIn): store.add_cert(X509cert) except OpenSSL.crypto.Error, exception_obj: log.warning('Unable to load a certificate from file %s: %s' %\ - ('%s/.gajim/cacerts.pem' % os.environ['HOME'], exception_obj.args[0][0][2])) + (common.gajim.MY_CACERTS, exception_obj.args[0][0][2])) except: log.warning( 'Unknown error while loading certificate from file %s' % \ - '%s/.gajim/cacerts.pem' % os.environ['HOME']) + common.gajim.MY_CACERTS) begin = -1 i += 1 tcpsock._sslObj = OpenSSL.SSL.Connection(tcpsock._sslContext, tcpsock._sock) diff --git a/src/config.py b/src/config.py index fd37bca5f..4e4c49161 100644 --- a/src/config.py +++ b/src/config.py @@ -515,11 +515,11 @@ class PreferencesWindow: def on_sort_by_show_checkbutton_toggled(self, widget): self.on_checkbutton_toggled(widget, 'sort_by_show') - gajim.interface.roster.draw_roster() + gajim.interface.roster.setup_and_draw_roster() def on_show_avatars_in_roster_checkbutton_toggled(self, widget): self.on_checkbutton_toggled(widget, 'show_avatars_in_roster') - gajim.interface.roster.draw_roster() + gajim.interface.roster.setup_and_draw_roster() # Redraw connected groupchats (in an ugly way) for account in gajim.connections: if gajim.connections[account].connected: @@ -530,14 +530,14 @@ class PreferencesWindow: def on_show_status_msgs_in_roster_checkbutton_toggled(self, widget): self.on_checkbutton_toggled(widget, 'show_status_msgs_in_roster') - gajim.interface.roster.draw_roster() + gajim.interface.roster.setup_and_draw_roster() for ctl in gajim.interface.msg_win_mgr.controls(): if ctl.type_id == message_control.TYPE_GC: ctl.update_ui() def on_sort_by_show_checkbutton_toggled(self, widget): self.on_checkbutton_toggled(widget, 'sort_by_show') - gajim.interface.roster.draw_roster() + gajim.interface.roster.setup_and_draw_roster() def on_emoticons_combobox_changed(self, widget): active = widget.get_active() @@ -570,7 +570,7 @@ class PreferencesWindow: ctl.chat_buttons_set_visible(active) gajim.config.set('compact_view', active) gajim.interface.save_config() - gajim.interface.roster.draw_roster() + gajim.interface.roster.setup_and_draw_roster() def on_xhtml_checkbutton_toggled(self, widget): self.on_checkbutton_toggled(widget, 'ignore_incoming_xhtml') @@ -1417,7 +1417,6 @@ class AccountsWindow: self.current_account = account if account == gajim.ZEROCONF_ACC_NAME: self.remove_button.set_sensitive(False) - self.rename_button.set_sensitive(False) self.init_account() self.update_proxy_list() @@ -1752,8 +1751,10 @@ class AccountsWindow: gajim.config.del_per('accounts', old_name) if self.current_account == old_name: self.current_account = new_name + if old_name == gajim.ZEROCONF_ACC_NAME: + gajim.ZEROCONF_ACC_NAME = new_name # refresh roster - gajim.interface.roster.draw_roster() + gajim.interface.roster.setup_and_draw_roster() self.init_accounts() self.select_account(new_name) @@ -2077,7 +2078,7 @@ class AccountsWindow: gajim.interface.roster.regroup = gajim.config.get('mergeaccounts') else: gajim.interface.roster.regroup = False - gajim.interface.roster.draw_roster() + gajim.interface.roster.setup_and_draw_roster() def on_enable_zeroconf_checkbutton2_toggled(self, widget): # don't do anything if there is an account with the local name but is a @@ -2119,7 +2120,7 @@ class AccountsWindow: gajim.interface.roster.regroup = gajim.config.get('mergeaccounts') else: gajim.interface.roster.regroup = False - gajim.interface.roster.draw_roster() + gajim.interface.roster.setup_and_draw_roster() gajim.interface.roster.set_actions_menu_needs_rebuild() elif not gajim.config.get('enable_zeroconf') and widget.get_active(): @@ -2156,7 +2157,7 @@ class AccountsWindow: gajim.interface.roster.regroup = gajim.config.get('mergeaccounts') else: gajim.interface.roster.regroup = False - gajim.interface.roster.draw_roster() + gajim.interface.roster.setup_and_draw_roster() gajim.interface.roster.set_actions_menu_needs_rebuild() gajim.interface.save_config() @@ -2587,7 +2588,7 @@ class RemoveAccountWindow: gajim.interface.roster.regroup = gajim.config.get('mergeaccounts') else: gajim.interface.roster.regroup = False - gajim.interface.roster.draw_roster() + gajim.interface.roster.setup_and_draw_roster() gajim.interface.roster.set_actions_menu_needs_rebuild() if gajim.interface.instances.has_key('accounts'): gajim.interface.instances['accounts'].init_accounts() @@ -3421,7 +3422,7 @@ class AccountCreationWizardWindow: gajim.interface.roster.regroup = gajim.config.get('mergeaccounts') else: gajim.interface.roster.regroup = False - gajim.interface.roster.draw_roster() + gajim.interface.roster.setup_and_draw_roster() gajim.interface.roster.set_actions_menu_needs_rebuild() gajim.interface.save_config() diff --git a/src/conversation_textview.py b/src/conversation_textview.py index 9db4f4d1e..2b6cf7fcf 100644 --- a/src/conversation_textview.py +++ b/src/conversation_textview.py @@ -854,15 +854,8 @@ class ConversationTextview: exitcode = p.wait() if exitcode == 0: - convert_version = helpers.get_output_of_command( - 'convert -version')[0].split()[2] - convert_version = [int(n) for n in convert_version.split('.')] - if convert_version > [6, 3, 4]: - # -alpha option was added in 6.3.5 release - alpha = ['-alpha', 'off'] - else: - alpha = [] - p = Popen(['convert'] + alpha + [tmpfile + '.ps', tmpfile + '.png'], + latex_png_dpi = gajim.config.get('latex_png_dpi') + p = Popen(['convert', '-background', 'white', '-flatten', '-density', latex_png_dpi, tmpfile + '.ps', tmpfile + '.png'], cwd=gettempdir()) exitcode = p.wait() diff --git a/src/features_window.py b/src/features_window.py index 42acc823c..5c8f10290 100644 --- a/src/features_window.py +++ b/src/features_window.py @@ -99,10 +99,6 @@ class FeaturesWindow: _('Encrypting chatmessages.'), _('Requires python-crypto.'), _('Requires python-crypto.')), - _('Off the Record Encryption'): (self.otr_available, - _('Encrypting chatmessages in a way that even works through gateways.'), - _('Requires pyotr and libotr (see http://trac.gajim.org/wiki/OTR).'), - _('Requires pyotr and libotr (see http://trac.gajim.org/wiki/OTR).')), _('RST Generator'): (self.docutils_available, _('Generate XHTML output from RST code (see http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html).'), _('Requires python-docutils.'), @@ -312,11 +308,6 @@ class FeaturesWindow: from common import gajim return gajim.HAVE_PYCRYPTO - def otr_available(self): - if gajim.otr_module: - return True - return False - def docutils_available(self): try: import docutils diff --git a/src/gajim.py b/src/gajim.py index 234ab6214..47d90a8d2 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -142,8 +142,19 @@ try: import gtk except Warning, msg: if str(msg) == 'could not open display': - print >> sys.stderr, _('Gajim needs X server to run. Quiting...') - sys.exit() + if sys.platform == 'darwin': + # It seems there is no way to open X11 without also + # opening an xterm. Even Apple's open-x11 script + # opens the application AND an xterm. + os.system('/Applications/Utilities/X11.app/Contents/MacOS/X11 &') + try: + import gtk + except Warning, msg: + print >> sys.stderr, _('No X11 running and failed to start it! Quitting...') + sys.exit() + else: + print >> sys.stderr, _('Gajim needs X server to run. Quiting...') + sys.exit() warnings.resetwarnings() if os.name == 'nt': @@ -255,214 +266,6 @@ from common import helpers from common import optparser from common import dataforms -from common.xmpp import Message as XmppMessage - -try: - import otr, otr_windows - gajim.otr_module = otr - gajim.otr_windows = otr_windows -except ImportError: - gajim.otr_module = None - gajim.otr_windows = None - -def add_appdata(data, context): - account = data - context.app_data = otr_windows.ContactOtrSMPWindow( - unicode(context.username), account) - -gajim.otr_add_appdata = add_appdata - -def otr_dialog_destroy(widget, *args, **kwargs): - widget.destroy() - -class OtrlMessageAppOps: - def gajim_log(self, msg, account, fjid, no_print=False): - if not isinstance(fjid, unicode): - fjid = unicode(fjid) - if not isinstance(account, unicode): - account = unicode(account) - resource=gajim.get_resource_from_jid(fjid) - tim = time.localtime() - - if not no_print: - ctrl = self.get_control(fjid, account) - if ctrl: - ctrl.print_conversation_line(u'[OTR] %s' % \ - msg, 'status', '', None) - id = gajim.logger.write('chat_msg_recv', fjid, - message='[OTR: %s]' % msg, tim=tim) - # gajim.logger.write() only marks a message as unread - # (and so only returns an id) when fjid is a real contact - # (NOT if it's a GC private chat) - if id: - gajim.logger.set_read_messages([id]) - - def get_control(self, fjid, account): - # first try to get the window with the full jid - ctrls = gajim.interface.msg_win_mgr.get_chat_controls(fjid, account) - if ctrls: - # got one, be happy - return ctrls[0] - - # otherwise try without the resource - ctrls = gajim.interface.msg_win_mgr.get_chat_controls( - gajim.get_jid_without_resource(fjid), account) - # but only use it when it is not a GC window - if ctrls and ctrls[0].TYPE_ID == message_control.TYPE_CHAT: - return ctrls[0] - - def policy(self, opdata=None, context=None): - policy = gajim.config.get_per('contacts', context.username, - "otr_flags") - if policy <= 0: - policy = gajim.config.get_per('contacts', - gajim.get_jid_without_resource( - context.username), 'otr_flags') - if policy <= 0: - policy = gajim.config.get_per('accounts', - opdata['account'], 'otr_flags') - return policy - - def create_privkey(self, opdata='', accountname='', protocol=''): - dialog = gtk.Dialog( - title = _('Generating...'), - parent = gajim.interface.roster.window, - flags = gtk.DIALOG_MODAL, - buttons = (gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE)) - permlabel = gtk.Label(_('Generating a private key for %s...') \ - % accountname) - permlabel.set_padding(20, 20) - dialog.set_response_sensitive(gtk.RESPONSE_CLOSE, False) - dialog.connect('destroy', otr_dialog_destroy) - dialog.connect('response', otr_dialog_destroy) - dialog.vbox.pack_start(permlabel) - dialog.get_root_window().raise_() - dialog.show_all() - dialog.map() - for c in dialog.get_children(): - c.show_now() - c.map() - - while gtk.events_pending(): - gtk.main_iteration(block = False) - - otr.otrl_privkey_generate( - gajim.connections[opdata['account']].otr_userstates, - os.path.join(gajimpaths.root, - '%s.key' % opdata['account']).encode(), - accountname, gajim.OTR_PROTO) - permlabel.set_text(_('Generating a private key for %s...\n' \ - 'done.') % accountname) - dialog.set_response_sensitive(gtk.RESPONSE_CLOSE, True) - - def is_logged_in(self, opdata={}, accountname='', protocol='', - recipient=""): - contact = gajim.contacts.get_contact_from_full_jid( - opdata['account'], recipient) - if contact: - return contact.show \ - in ['dnd', 'xa', 'chat', 'online', 'away', - 'invisible'] - return 0 - - def inject_message(self, opdata=None, accountname='', protocol='', - recipient='', message=''): - msg_type = otr.otrl_proto_message_type(message) - - if 'kwargs' not in opdata or 'urgent' in opdata: - # don't use send_message here to have the message - # sent immediatly. This results in being able to - # disconnect from OTR sessions before quitting - stanza = XmppMessage(to = recipient, - body = message, typ='chat') - gajim.connections[opdata['account']].connection. \ - send(stanza, now = True) - return - - if msg_type == otr.OTRL_MSGTYPE_QUERY: - # split away XHTML-contaminated explanatory message - message = unicode(message.splitlines()[0]) - message += _(u'\nThis user has requested an ' \ - 'Off-the-Record private conversation. ' \ - 'However, you do not have a plugin to ' \ - 'support that.\n' \ - 'See http://otr.cypherpunks.ca/ for more ' \ - 'information.') - - gajim.connections[opdata['account']].connection.send( - common.xmpp.Message(to = recipient, - body = message, typ = 'chat')) - return - - gajim.connections[opdata['account']].send_message(recipient, - message, **opdata['kwargs']) - - def notify(sef, opdata=None, username='', **kwargs): - self.gajim_log('Notify: ' + str(kwargs), opdata['account'], - username) - - def display_otr_message(self, opdata=None, username="", msg="", **kwargs): - self.gajim_log('OTR Message: ' + msg, opdata['account'], - username) - return 0 - - def update_context_list(self, **kwargs): - # FIXME stub FIXME # - pass - - def protocol_name(self, opdata=None, protocol=""): - return 'XMPP' - - def new_fingerprint(self, opdata=None, username='', fingerprint='', - **kwargs): - self.gajim_log('New fingerprint for %s: %s' % (username, - otr.otrl_privkey_hash_to_human(fingerprint)), - opdata['account'], username) - - def write_fingerprints(self, opdata=''): - otr.otrl_privkey_write_fingerprints( - gajim.connections[opdata['account']].otr_userstates, - os.path.join(gajimpaths.root, '%s.fpr' % \ - opdata['account']).encode()) - - def gone_secure(self, opdata='', context=None): - trust = context.active_fingerprint.trust \ - and 'verified' or 'unverified' - self.gajim_log('%s secured OTR connection started' % trust, - opdata['account'], context.username, no_print = True) - - ctrl = self.get_control(context.username, opdata['account']) - if ctrl: - ctrl.update_otr(True) - - def gone_insecure(self, opdata='', context=None): - self.gajim_log('Private conversation with %s lost.', - opdata['account'], context.username) - - ctrl = self.get_control(context.username, opdata['account']) - if ctrl: - ctrl.update_otr(True) - - def still_secure(self, opdata=None, context=None, is_reply=0): - ctrl = self.get_control(context.username, opdata['account']) - if ctrl: - ctrl.update_otr(True) - - self.gajim_log('OTR connection was refreshed', - opdata['account'], context.username) - - def log_message(self, opdata=None, message=''): - gajim.log.debug(message) - - def max_message_size(self, **kwargs): - return 0 - - def account_name(self, opdata=None, account='', protocol=''): - return gajim.get_name_from_jid(opdata['account'], - unicode(account)) - -gajim.otr_ui_ops = OtrlMessageAppOps() - if verbose: gajim.verbose = True del verbose @@ -600,8 +403,11 @@ def on_exit(): if os.path.exists(pid_filename): os.remove(pid_filename) if sys.platform == 'darwin': - import osx - osx.shutdown() + try: + import osx + osx.shutdown() + except ImportError: + pass import atexit atexit.register(on_exit) @@ -858,7 +664,7 @@ class Interface: if resource == gajim.connections[account].server_resource: return contact1 = gajim.contacts.create_contact(jid = ji, - name = gajim.nicks[account], groups = [], + name = gajim.nicks[account], groups = ['self_contact'], show = array[1], status = status_message, sub = 'both', ask = 'none', priority = priority, keyID = keyID, resource = resource) @@ -1648,18 +1454,26 @@ class Interface: gmail_new_messages, gmail_new_messages) if gajim.config.get('notify_on_new_gmail_email_extra'): + cnt = 0 for gmessage in gmail_messages_list: - #FIXME: emulate Gtalk client popups. find out what they parse and how - #they decide what to show - # each message has a 'From', 'Subject' and 'Snippet' field - text += _('\nFrom: %(from_address)s') % \ - {'from_address': gmessage['From']} + #FIXME: emulate Gtalk client popups. find out what they parse and + # how they decide what to show each message has a 'From', + # 'Subject' and 'Snippet' field + if cnt >=5: + break + senders = reduce(lambda b, a: a + ',\n ' + b, + gmessage['From']) + text += _('\n\nFrom: %(from_address)s\nSubject: %(subject)s\n%(snippet)s') % \ + {'from_address': senders, 'subject': gmessage['Subject'], + 'snippet': gmessage['Snippet']} + cnt += 1 if gajim.config.get_per('soundevents', 'gmail_received', 'enabled'): helpers.play_sound('gmail_received') path = gtkgui_helpers.get_path_to_generic_or_avatar(img) notify.popup(_('New E-mail'), jid, account, 'gmail', - path_to_image = path, title = title, text = text) + path_to_image=path, title=title, + text=gobject.markup_escape_text(text)) if self.remote_ctrl: self.remote_ctrl.raise_signal('NewGmail', (account, array)) @@ -2041,7 +1855,7 @@ class Interface: connection = gajim.connections[account] contact = gajim.contacts.create_contact(jid = jid.getStripped(), resource = resource, show = connection.get_status()) - self.new_chat(session, contact, account, resource = resource) + self.new_chat(contact, account, resource = resource, session = session) negotiation.FeatureNegotiationWindow(account, jid, session, form) @@ -2446,7 +2260,7 @@ class Interface: if not session: session = gajim.connections[account].get_or_create_session(fjid, None) - self.new_chat(session, contact, account, resource = resource) + self.new_chat(contact, account, resource = resource, session = session) ctrl = session.control gajim.last_message_time[account][jid] = 0 # long time ago @@ -2514,7 +2328,6 @@ class Interface: self.roster.draw_contact(jid, account) if w: w.set_active_tab(ctrl) - w.window.present() w.window.window.focus() # Using isinstance here because we want to catch all derived types if isinstance(ctrl, ChatControlBase): @@ -2750,11 +2563,12 @@ class Interface: def join_gc_room(self, account, room_jid, nick, password, minimize=False, is_continued=False): '''joins the room immediately''' + if not nick: + nick = gajim.nicks[account] if self.msg_win_mgr.has_window(room_jid, account) and \ gajim.gc_connected[account][room_jid]: gc_ctrl = self.msg_win_mgr.get_gc_control(room_jid, account) win = gc_ctrl.parent_win - win.window.present() win.set_active_tab(gc_ctrl) dialogs.ErrorDialog(_('You are already in group chat %s') % room_jid) return @@ -2782,7 +2596,6 @@ class Interface: if not minimized_control_exists: gc_control = self.msg_win_mgr.get_gc_control(room_jid, account) gc_control.parent_win.set_active_tab(gc_control) - gc_control.parent_win.window.present() gajim.connections[account].join_gc(nick, room_jid, password) if password: gajim.gc_passwords[room_jid] = password @@ -2841,7 +2654,7 @@ class Interface: # We call this here to avoid race conditions with widget validation session.control.read_queue() - def new_chat(self, session, contact, account, resource = None): + def new_chat(self, contact, account, resource = None, session = None): # Get target window, create a control, and associate it with the window type_ = message_control.TYPE_CHAT @@ -2875,14 +2688,13 @@ class Interface: session = gajim.connections[account].get_or_create_session(fjid, None) if not self.msg_win_mgr.has_window(fjid, account): - session.control = self.new_chat(session, contact, account, - resource=resource) + session.control = self.new_chat(contact, account, + resource=resource, session=session) if len(gajim.events.get_events(account, fjid)): session.control.read_queue() mw = session.control.parent_win mw.set_active_tab(session.control) - mw.window.present() # For JEP-0172 if added_to_roster: session.control.user_nick = gajim.nicks[account] @@ -2896,46 +2708,31 @@ class Interface: if resource: fjid += '/' + resource - conn = gajim.connections[account] + ctrl = None - if not session and fjid in conn.sessions: - sessions = filter(lambda s: isinstance(s, ChatControlSession), - conn.sessions[fjid].values()) + if session: + ctrl = session.control + else: + win = self.msg_win_mgr.get_window(fjid, account) - # look for an existing session with a chat control - for s in sessions: - if s.control: - session = s - break - - if not session and not len(sessions) == 0: - # there are no sessions with chat controls, just take the first one - session = sessions[0] - - if not session: - # couldn't find an existing ChatControlSession, just make a new one - session = conn.make_new_session(fjid, None, 'chat') - - if not session.control: - # open a new chat control - session.control = self.new_chat(session, contact, account, - resource=resource) - - if len(gajim.events.get_events(account, fjid)): - session.control.read_queue() + if win: + ctrl = win.get_controls(fjid, account)[0] + if not ctrl: + ctrl = self.new_chat(contact, account, + resource = resource, session = session) # last message is long time ago - gajim.last_message_time[account][session.control.get_full_jid()] = 0 + gajim.last_message_time[account][ctrl.get_full_jid()] = 0 - win = session.control.parent_win - win.set_active_tab(session.control) + win = ctrl.parent_win - if conn.is_zeroconf and conn.status in ('offline', 'invisible'): + win.set_active_tab(ctrl) + + if gajim.connections[account].is_zeroconf and \ + gajim.connections[account].status in ('offline', 'invisible'): for ctrl in win.get_controls(fjid, account): ctrl.got_disconnected() - win.window.present() - ################################################################################ ### Other Methods ################################################################################ @@ -3222,6 +3019,10 @@ class Interface: if os.name != 'nt' and gajim.config.get('check_if_gajim_is_default'): gtkgui_helpers.possibly_set_gajim_as_xmpp_handler() + for account in gajim.config.get_per('accounts'): + if gajim.config.get_per('accounts', account, 'is_zeroconf'): + gajim.ZEROCONF_ACC_NAME = account + break # Is gnome configured to activate row on single click ? try: import gconf @@ -3466,8 +3267,11 @@ if __name__ == '__main__': check_paths.check_and_possibly_create_paths() if sys.platform == 'darwin': - import osx - osx.init() + try: + import osx + osx.init() + except ImportError: + pass Interface() diff --git a/src/groupchat_control.py b/src/groupchat_control.py index 84b31fa0a..cf8e3bc2d 100644 --- a/src/groupchat_control.py +++ b/src/groupchat_control.py @@ -916,6 +916,8 @@ class GroupchatControl(ChatControlBase): gajim.contacts.remove_gc_contact(self.account, gc_contact) gajim.gc_connected[self.account][self.room_jid] = False ChatControlBase.got_disconnected(self) + # Tell connection to note the date we disconnect to avoid duplicate logs + gajim.connections[self.account].gc_got_disconnected(self.room_jid) # We don't redraw the whole banner here, because only icon change self._update_banner_state_image() if self.parent_win: @@ -1638,12 +1640,13 @@ class GroupchatControl(ChatControlBase): def shutdown(self, status='offline'): # destroy banner tooltip - bug #pygtk for that! self.subject_tooltip.destroy() + if gajim.gc_connected[self.account][self.room_jid]: + # Tell connection to note the date we disconnect to avoid duplicate + # logs. We do it only when connected because if connection was lost + # there may be new messages since disconnection. + gajim.connections[self.account].gc_got_disconnected(self.room_jid) gajim.connections[self.account].send_gc_status(self.nick, self.room_jid, show='offline', status=status) - # save in fast table in DB at what time we had last message - last_history_time = \ - gajim.connections[self.account].last_history_time[self.room_jid] - gajim.logger.set_room_last_message_time(self.room_jid, last_history_time) nick_list = gajim.contacts.get_nick_list(self.account, self.room_jid) for nick in nick_list: # Update pm chat window @@ -2073,7 +2076,6 @@ class GroupchatControl(ChatControlBase): ctrl = win.get_controls(nick_jid, self.account)[0] win.set_active_tab(ctrl) - win.window.present() return ctrl diff --git a/src/history_window.py b/src/history_window.py index 062adba9d..6c87d0940 100644 --- a/src/history_window.py +++ b/src/history_window.py @@ -65,8 +65,6 @@ class HistoryWindow: '''Class for browsing logs of conversations with contacts''' def __init__(self, jid = None, account = None): - self.mark_days_idle_call_id = None - xml = gtkgui_helpers.get_glade('history_window.glade') self.window = xml.get_widget('history_window') self.jid_entry = xml.get_widget('jid_entry') @@ -117,22 +115,25 @@ class HistoryWindow: self.completion_dict = {} self.accounts_seen_online = [] # Update dict when new accounts connect self.jids_to_search = [] - self._fill_completion_dict() + + # This will load history too + gobject.idle_add(self._fill_completion_dict().next) if jid: self.jid_entry.set_text(jid) xml.signal_autoconnect(self) - self._load_history(jid, account) self.window.show_all() def _fill_completion_dict(self): - '''Fill completion_dict for key auto completion. + '''Fill completion_dict for key auto completion. Then load history for + current jid (by calling another function). Key will be either jid or full_completion_name (contact name or long description like "pm-contact from groupchat....") {key : (jid, account, nick_name, full_completion_name} + this is a generator and does pseudo-threading via idle_add() ''' liststore = gtkgui_helpers.get_completion_liststore(self.jid_entry) @@ -150,8 +151,14 @@ class HistoryWindow: muc_active_img = gtkgui_helpers.load_icon('muc_active') contact_img = gajim.interface.jabber_state_images['16']['online'] muc_active_pix = muc_active_img.get_pixbuf() - contact_pix = contact_img.get_pixbuf() + contact_pix = contact_img.get_pixbuf() + keys = self.completion_dict.keys() + # Move the actual jid at first so we load history faster + actual_jid = self.jid_entry.get_text().decode('utf-8') + if actual_jid in keys: + keys.remove(actual_jid) + keys.insert(0, actual_jid) # Map jid to info tuple # Warning : This for is time critical with big DB for key in keys: @@ -180,13 +187,17 @@ class HistoryWindow: info_name = nick else: pix = contact_pix - + liststore.append((pix, completed)) self.completion_dict[key] = (info_jid, info_acc, info_name, info_completion) self.completion_dict[completed] = (info_jid, info_acc, info_name, info_completion) + if key == actual_jid: + self._load_history(info_jid, info_acc) + yield True keys.sort() + yield False def _get_account_for_jid(self, jid): '''Return the corresponding account of the jid. @@ -202,10 +213,6 @@ class HistoryWindow: return account def on_history_window_destroy(self, widget): - if self.mark_days_idle_call_id: - # if user destroys the window, and we have a generator filling mark days - # stop him! - gobject.source_remove(self.mark_days_idle_call_id) self.history_textview.del_handlers() del gajim.interface.instances['logs'] @@ -265,11 +272,10 @@ class HistoryWindow: # select logs for last date we have logs with contact self.calendar.set_sensitive(True) - self.calendar.emit('month-changed') - lastlog = gajim.logger.get_last_date_that_has_logs(self.jid, self.account) + last_log = \ + gajim.logger.get_last_date_that_has_logs(self.jid, self.account) - tim = lastlog - date = time.localtime(tim) + date = time.localtime(last_log) y, m, d = date[0], date[1], date[2] gtk_month = gtkgui_helpers.make_python_month_gtk_month(m) @@ -305,31 +311,21 @@ class HistoryWindow: month = gtkgui_helpers.make_gtk_month_python_month(month) self._add_lines_for_date(year, month, day) - def do_possible_mark_for_days_in_this_month(self, widget, year, month): - '''this is a generator and does pseudo-threading via idle_add() - so it runs progressively! yea :) - asks for days in this month if they have logs it bolds them (marks them)''' + def on_calendar_month_changed(self, widget): + '''asks for days in this month if they have logs it bolds them (marks + them) + ''' + year, month, day = widget.get_date() # integers + # in gtk January is 1, in python January is 0, + # I want the second + # first day of month is 1 not 0 + widget.clear_marks() + month = gtkgui_helpers.make_gtk_month_python_month(month) weekday, days_in_this_month = calendar.monthrange(year, month) log_days = gajim.logger.get_days_with_logs(self.jid, year, month, days_in_this_month, self.account) for day in log_days: widget.mark_day(day) - yield True - yield False - - def on_calendar_month_changed(self, widget): - year, month, day = widget.get_date() # integers - # in gtk January is 1, in python January is 0, - # I want the second - # first day of month is 1 not 0 - if self.mark_days_idle_call_id: - # if user changed month, and we have a generator filling mark days - # stop him from marking dates for the previously selected month - gobject.source_remove(self.mark_days_idle_call_id) - widget.clear_marks() - month = gtkgui_helpers.make_gtk_month_python_month(month) - self.mark_days_idle_call_id = gobject.idle_add( - self.do_possible_mark_for_days_in_this_month(widget, year, month).next) def _get_string_show_from_constant_int(self, show): if show == constants.SHOW_ONLINE: @@ -480,7 +476,8 @@ class HistoryWindow: message = row[4] local_time = time.localtime(tim) date = time.strftime('%x', local_time) - # jid (to which log is assigned to), name, date, message, time (full unix time) + # jid (to which log is assigned to), name, date, message, + # time (full unix time) model.append((jid, contact_name, date, message, tim)) def on_query_combobox_changed(self, widget): @@ -573,9 +570,12 @@ class HistoryWindow: def open_history(self, jid, account): '''Load chat history of the specified jid''' + self.jid_entry.set_text(jid) if account and account not in self.accounts_seen_online: # Update dict to not only show bare jid - self._fill_completion_dict - self.jid_entry.set_text(jid) - self._load_history(jid, account) + gobject.idle_add(self._fill_completion_dict().next) + else: + # Only in that case because it's called by self._fill_completion_dict() + # otherwise + self._load_history(jid, account) self.results_window.set_property('visible', False) diff --git a/src/message_control.py b/src/message_control.py index ebff23368..d7092f76a 100644 --- a/src/message_control.py +++ b/src/message_control.py @@ -134,12 +134,17 @@ class MessageControl: new_key = session.thread_id if oldsession: - self.parent_win.change_thread_key( - self.contact.jid, self.account, + jid = self.contact.jid + if self.resource: + jid += '/' + self.resource + + self.parent_win.change_thread_key(jid, self.account, oldsession.thread_id, new_key) if oldsession.enable_encryption: self.print_esession_details() + else: + self.parent_win.move_from_sessionless(self) def send_message(self, message, keyID = '', type = 'chat', chatstate = None, msg_id = None, composing_xep = None, resource = None, @@ -148,56 +153,18 @@ class MessageControl: # Doesn't return None if error jid = self.contact.jid original_message = message + conn = gajim.connections[self.account] if not self.session: - sess = gajim.connections[self.account].make_new_session(jid) + sess = conn.find_controlless_session(jid) + + if not sess: + sess = conn.make_new_session(jid) + self.set_session(sess) - self.parent_win.move_from_sessionless(self) - - xep_200 = bool(self.session) and self.session.enable_encryption - - if gajim.otr_module and not xep_200 and (jid not in gajim.otr_dont_append_tag): - if type == 'chat' and isinstance(message, unicode): - d = {'kwargs': {'keyID': keyID, 'type': type, - 'chatstate': chatstate, - 'msg_id': msg_id, - 'composing_xep': composing_xep, - 'resource': self.resource, - 'user_nick': user_nick, - 'session': self.session, - 'original_message': original_message}, - 'account': self.account} - - new_msg = gajim.otr_module.otrl_message_sending( - self.session.conn.otr_userstates, - (gajim.otr_ui_ops, d), - gajim.get_jid_from_account( - self.account).encode(), - gajim.OTR_PROTO, - self.contact.get_full_jid().encode(), - message.encode(), None, - (gajim.otr_add_appdata, self.account)) - - ctx = gajim.otr_module.otrl_context_find( - self.session.conn.otr_userstates, - self.contact.get_full_jid().encode(), - gajim.get_jid_from_account( - self.account).encode(), - gajim.OTR_PROTO, 1, - (gajim.otr_add_appdata, - self.account))[0] - - # we send all because inject_message can filter - # on HTML stuff then - gajim.otr_module.otrl_message_fragment_and_send( - (gajim.otr_ui_ops, d), ctx, new_msg, - gajim.otr_module.OTRL_FRAGMENT_SEND_ALL) - return # Send and update history - return gajim.connections[self.account].send_message(jid, - message, keyID, type = type, chatstate = chatstate, - msg_id = msg_id, composing_xep = composing_xep, + return conn.send_message(jid, message, keyID, type = type, + chatstate = chatstate, msg_id = msg_id, composing_xep = composing_xep, resource = self.resource, user_nick = user_nick, - session = self.session, - original_message = original_message) + session = self.session, original_message = original_message) diff --git a/src/message_window.py b/src/message_window.py index 21c362af5..a457318d2 100644 --- a/src/message_window.py +++ b/src/message_window.py @@ -448,6 +448,7 @@ class MessageWindow(object): def set_active_tab(self, ctrl): ctrl_page = self.notebook.page_num(ctrl.widget) self.notebook.set_current_page(ctrl_page) + self.window.present() def remove_tab(self, ctrl, method, reason = None, force = False): '''reason is only for gc (offline status message) @@ -692,10 +693,13 @@ class MessageWindow(object): del self.sessionless_ctrls[acct][jid][idx] + if len(self.sessionless_ctrls[acct][jid]) == 0: + del self.sessionless_ctrls[acct][jid] + if not self._controls.has_key(acct): self._controls[acct] = {} - if not self.sessionless_ctrls[acct].has_key(jid): + if not self._controls[acct].has_key(jid): self._controls[acct][jid] = {} thread_id = ctrl.session.thread_id diff --git a/src/osx/__init__.py b/src/osx/__init__.py index ee21386cb..263633712 100644 --- a/src/osx/__init__.py +++ b/src/osx/__init__.py @@ -1,15 +1,12 @@ import sys, commands from network_manager_listener import device_now_active, device_no_longer_active -import nsapp -if sys.platform != "darwin": - raise ImportError("System platform is not OS/X") - +if sys.platform != 'darwin': + raise ImportError('System platform is not OS X') net_device_active = True - ### ### Utility functions ### @@ -21,6 +18,7 @@ def checkPID(pid, procname): return True return False +import nsapp def init(): nsapp.init() diff --git a/src/otr_windows.py b/src/otr_windows.py deleted file mode 100644 index 2b3c5ab18..000000000 --- a/src/otr_windows.py +++ /dev/null @@ -1,357 +0,0 @@ -#!/usr/bin/env python -## otr_windows.py -## -## -## Copyright (C) 2008 Kjell Braden -## -## This file is part of Gajim. -## -## Gajim 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; version 3 only. -## -## Gajim 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 Gajim. If not, see . -## - -import gtkgui_helpers -from common import gajim - -our_fp_text = _('Your fingerprint:\n' \ - '%s') -their_fp_text = _('Purported fingerprint for %s:\n' \ - '%s') - -class ContactOtrSMPWindow: - def gw(self, n): - # shorthand for self.xml.get_widget(n) - return self.xml.get_widget(n) - - def __init__(self, fjid, account): - self.fjid = fjid - self.account = account - - self.xml = gtkgui_helpers.get_glade('contact_otr_window.glade') - self.window = self.xml.get_widget('otr_smp_window') - - # the contact may be unknown to gajim if ContactOtrSMPWindow - # is created very early - self.contact = gajim.contacts.get_contact_from_full_jid( - account, fjid) - if self.contact: - self.window.set_title(_('OTR settings for %s') % \ - self.contact.get_full_jid()) - - self.ctx = gajim.otr_module.otrl_context_find( - gajim.connections[self.account].otr_userstates, - self.fjid.encode(), gajim.get_jid_from_account( - self.account).encode(), gajim.OTR_PROTO, 1, - (gajim.otr_add_appdata, self.account))[0] - - self.gw('smp_cancel_button').connect('clicked', - self._on_destroy) - self.gw('smp_ok_button').connect('clicked', self._apply) - - def show(self, response): - # re-initialize if contact was unknown when we - # initially initialized - if not self.contact: - self.__init__(self.fjid, self.account) - # the contact MUST be known when showing the dialog - assert(self.contact) - - self.smp_running = False - self.finished = False - - self.gw('smp_cancel_button').set_sensitive(True) - self.gw('smp_ok_button').set_sensitive(True) - self.gw('progressbar').set_fraction(0) - self.gw('secret_entry').set_text('') - - self.response = response - if response: - self.gw('desc_label').set_markup(_('%s is trying ' \ - 'to authenticate you using a secret only ' \ - 'known to him/her and you. Please enter ' \ - 'your secret below.') % \ - self.contact.get_full_jid()) - else: - self.gw('desc_label').set_markup(_('You are ' \ - 'trying to authenticate %s using a secret' \ - 'only known to him/her and yourself.' \ - 'Please enter your secret below.') % \ - self.contact.get_full_jid()) - - self.window.show_all() - - def _abort(self, text=None): - self.smp_running = False - gajim.otr_module.otrl_message_abort_smp( - gajim.connections[self.account].otr_userstates, - (gajim.otr_ui_ops, {'account': self.account}), self.ctx) - if text: - gajim.otr_ui_ops.gajim_log(text, self.account, - self.contact.get_full_jid()) - - def _finish(self, text): - self.smp_running = False - self.finished = True - self.gw('smp_cancel_button').set_sensitive(False) - self.gw('smp_ok_button').set_sensitive(True) - self.gw('progressbar').set_fraction(1) - gajim.otr_ui_ops.gajim_log(text, self.account, - self.contact.get_full_jid()) - self.gw('desc_label').set_markup(text) - for ctrl in gajim.interface.msg_win_mgr.get_chat_controls( - self.contact.jid, self.account): - ctrl.update_otr(True) - gajim.otr_ui_ops.write_fingerprints({'account': self.account}) - - def handle_tlv(self, tlvs): - if not self.contact: - self.__init__(self.fjid, self.account) - - if tlvs: - nextTLV = self.ctx.smstate.nextExpected; - tlv = gajim.otr_module.otrl_tlv_find(tlvs, - gajim.otr_module.OTRL_TLV_SMP1) - if tlv: - if nextTLV != \ - gajim.otr_module.OTRL_SMP_EXPECT1: - self._abort() - else: - self.show(True) - self.gw('progressbar'). \ - set_fraction(0.3) - tlv = gajim.otr_module.otrl_tlv_find(tlvs, - gajim.otr_module.OTRL_TLV_SMP2) - if tlv: - if nextTLV != gajim.otr_module.OTRL_SMP_EXPECT2: - self._abort() - else: - self.ctx.smstate.nextExpected = \ - gajim.otr_module. \ - OTRL_SMP_EXPECT4 - self.gw('progressbar').set_fraction(0.6) - tlv = gajim.otr_module.otrl_tlv_find(tlvs, - gajim.otr_module.OTRL_TLV_SMP3) - if tlv: - if nextTLV != gajim.otr_module.OTRL_SMP_EXPECT3: - self._abort() - else: - self.ctx.smstate.nextExpected = \ - gajim.otr_module. \ - OTRL_SMP_EXPECT1; - if self.ctx.active_fingerprint.trust: - self._finish(_('SMP ' \ - 'verifying succeeded')) - else: - self._finish(_('SMP ' \ - 'verifying failed')) - tlv = gajim.otr_module.otrl_tlv_find(tlvs, - gajim.otr_module.OTRL_TLV_SMP4) - if tlv: - if nextTLV != gajim.otr_module.OTRL_SMP_EXPECT4: - self._abort() - else: - self.ctx.smstate.nextExpected = \ - gajim.otr_module. \ - OTRL_SMP_EXPECT1; - if self.ctx.active_fingerprint.trust: - self._finish(_('SMP ' \ - 'verifying succeeded')) - else: - self._finish(_('SMP ' \ - 'verifying failed')) - tlv = gajim.otr_module.otrl_tlv_find(tlvs, - gajim.otr_module.OTRL_TLV_SMP_ABORT) - if tlv: - self._finish(_('SMP verifying aborted')) - - def _on_destroy(self, widget): - if self.smp_running: - self._abort(_('user aborted SMP authentication')) - self.window.hide_all() - - def _apply(self, widget): - if self.finished: - self.window.hide_all() - return - secret = self.gw('secret_entry').get_text() - if self.response: - gajim.otr_module.otrl_message_respond_smp( - gajim.connections[self.account].otr_userstates, - (gajim.otr_ui_ops, {'account': self.account}), - self.ctx, secret) - else: - gajim.otr_module.otrl_message_initiate_smp( - gajim.connections[self.account].otr_userstates, - (gajim.otr_ui_ops, {'account': self.account}), - self.ctx, secret) - self.gw('progressbar').set_fraction(0.3) - self.smp_running = True - widget.set_sensitive(False) - -class ContactOtrWindow: - def gw(self, n): - # shorthand for self.xml.get_widget(n) - return self.xml.get_widget(n) - - def __init__(self, contact, account, ctrl=None): - self.contact = contact - self.account = account - self.ctrl = ctrl - - self.ctx = gajim.otr_module.otrl_context_find( - gajim.connections[self.account].otr_userstates, - self.contact.get_full_jid().encode(), - gajim.get_jid_from_account(self.account).encode(), - gajim.OTR_PROTO, 1, (gajim.otr_add_appdata, - self.account))[0] - - self.xml = gtkgui_helpers.get_glade('contact_otr_window.glade') - self.window = self.xml.get_widget('otr_settings_window') - - self.gw('settings_cancel_button').connect('clicked', - self._on_destroy) - self.gw('settings_ok_button').connect('clicked', self._apply) - self.gw('otr_default_checkbutton').connect('toggled', - self._otr_default_checkbutton_toggled) - - self.window.set_title(_('OTR settings for %s') % \ - self.contact.get_full_jid()) - - # always set the label containing our fingerprint - self.gw('our_fp_label').set_markup(our_fp_text % \ - gajim.otr_module.otrl_privkey_fingerprint( - gajim.connections[self.account].otr_userstates, - gajim.get_jid_from_account(self.account).encode(), - gajim.OTR_PROTO)) - - if self.ctx.msgstate != \ - gajim.otr_module.OTRL_MSGSTATE_ENCRYPTED: - # make the fingerprint widgets insensitive - # when not encrypted - for widget in self.gw('otr_fp_vbox').get_children(): - widget.set_sensitive(False) - # show that the fingerprint is unknown - self.gw('their_fp_label').set_markup( - their_fp_text % (self.contact.get_full_jid(), - _('unknown'))) - self.gw('verified_combobox').set_active(-1) - else: - # make the fingerprint widgets sensitive when encrypted - for widget in self.gw('otr_fp_vbox').get_children(): - widget.set_sensitive(True) - # show their fingerprint - self.gw('their_fp_label').set_markup( - their_fp_text%(self.contact.get_full_jid(), - gajim.otr_module.otrl_privkey_hash_to_human( - self.ctx.active_fingerprint.fingerprint))) - # set the trust combobox - if self.ctx.active_fingerprint.trust: - self.gw('verified_combobox').set_active(1) - else: - self.gw('verified_combobox').set_active(0) - - otr_flags = gajim.config.get_per('contacts', self.contact.jid, - 'otr_flags') - - if otr_flags >= 0: - self.gw('otr_default_checkbutton').set_active(0) - for w in self.gw('otr_settings_vbox').get_children(): - w.set_sensitive(True) - else: - # per-user settings not available, - # using default settings - otr_flags = gajim.config.get_per('accounts', - self.account, 'otr_flags') - self.gw('otr_default_checkbutton').set_active(1) - for w in self.gw('otr_settings_vbox').get_children(): - w.set_sensitive(False) - - self.gw('otr_policy_allow_v1_checkbutton').set_active( - otr_flags & gajim.otr_module.OTRL_POLICY_ALLOW_V1) - self.gw('otr_policy_allow_v2_checkbutton').set_active( - otr_flags & gajim.otr_module.OTRL_POLICY_ALLOW_V2) - self.gw('otr_policy_require_checkbutton').set_active( - otr_flags & gajim.otr_module.OTRL_POLICY_REQUIRE_ENCRYPTION) - self.gw('otr_policy_send_tag_checkbutton').set_active( - otr_flags & \ - gajim.otr_module.OTRL_POLICY_SEND_WHITESPACE_TAG) - self.gw('otr_policy_start_on_tag_checkbutton').set_active( - otr_flags & \ - gajim.otr_module.OTRL_POLICY_WHITESPACE_START_AKE) - self.gw('otr_policy_start_on_error_checkbutton').set_active( - otr_flags & \ - gajim.otr_module.OTRL_POLICY_ERROR_START_AKE) - - self.window.show_all() - - def _on_destroy(self, widget): - self.window.destroy() - - def _apply(self, widget): - # -1 when nothing is selected - # (ie. the connection is not encrypted) - trust_state = self.gw('verified_combobox').get_active() - if trust_state == 1 and not self.ctx.active_fingerprint.trust: - gajim.otr_module.otrl_context_set_trust( - self.ctx.active_fingerprint, 'verified') - gajim.otr_ui_ops.write_fingerprints( - {'account': self.account}) - elif trust_state == 0: - gajim.otr_module.otrl_context_set_trust( - self.ctx.active_fingerprint, '') - gajim.otr_ui_ops.write_fingerprints( - {'account': self.account}) - - if not self.ctrl: - self.ctrl = gajim.interface.msg_win_mgr.get_control( - self.contact.jid, self.account) - if self.ctrl: - self.ctrl.update_otr(True) - - if self.gw('otr_default_checkbutton').get_active(): - # default is enabled, so remove any user-specific - # settings if available - gajim.config.set_per('contacts', - self.contact.jid, 'otr_flags', -1) - else: - # build the flags using the checkboxes - flags = 0 - flags += self.gw('otr_policy_allow_v1_checkbutton'). \ - get_active() and gajim.otr_module. \ - OTRL_POLICY_ALLOW_V1 - flags += self.gw('otr_policy_allow_v2_checkbutton'). \ - get_active() and gajim.otr_module. \ - OTRL_POLICY_ALLOW_V2 - flags += self.gw('otr_policy_require_checkbutton'). \ - get_active() and gajim.otr_module. \ - OTRL_POLICY_REQUIRE_ENCRYPTION - flags += self.gw('otr_policy_send_tag_checkbutton'). \ - get_active() and gajim.otr_module. \ - OTRL_POLICY_SEND_WHITESPACE_TAG - flags += self.gw( - 'otr_policy_start_on_tag_checkbutton'). \ - get_active() and gajim.otr_module. \ - OTRL_POLICY_WHITESPACE_START_AKE - flags += self.gw( - 'otr_policy_start_on_error_checkbutton'). \ - get_active() and gajim.otr_module. \ - OTRL_POLICY_ERROR_START_AKE - - gajim.config.add_per('contacts', self.contact.jid) - gajim.config.set_per('contacts', self.contact.jid, - 'otr_flags', flags) - - self._on_destroy(widget) - - def _otr_default_checkbutton_toggled(self, widget): - for w in self.gw('otr_settings_vbox').get_children(): - w.set_sensitive(not widget.get_active()) diff --git a/src/roster_window.py b/src/roster_window.py index bee28bd63..935067cbb 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -61,9 +61,11 @@ if dbus_support.supported: import dbus from lastfm_track_listener import LastFMTrackListener -if sys.platform == 'darwin': +try: from osx import syncmenu - +except ImportError: + pass + #(icon, name, type, jid, account, editable, second pixbuf) ( C_IMG, # image to show state (online, new message etc) @@ -84,7 +86,7 @@ class RosterWindow: Keyword arguments: name -- the account name model -- the data model (default TreeFilterModel) - + ''' if not model: model = self.modelfilter @@ -109,7 +111,7 @@ class RosterWindow: account -- the account name account_iter -- the iter of the account the model (default None) model -- the data model (default TreeFilterModel) - + ''' if not model: model = self.modelfilter @@ -123,7 +125,7 @@ class RosterWindow: break group_iter = model.iter_next(group_iter) return group_iter - + def _get_self_contact_iter(self, jid, account, model = None): ''' Return the gtk.TreeIter of SelfContact or None if not found. @@ -132,14 +134,14 @@ class RosterWindow: jid -- the jid of SelfContact account -- the account of SelfContact model -- the data model (default TreeFilterModel) - + ''' - + if not model: model = self.modelfilter iterAcct = self._get_account_iter(account, model) iterC = model.iter_children(iterAcct) - + # There might be several SelfContacts in merged account view while iterC: if model[iterC][C_TYPE] != 'self_contact': @@ -159,7 +161,7 @@ class RosterWindow: account -- the account contact -- the contact (default None) model -- the data model (default TreeFilterModel) - + ''' if not model: model = self.modelfilter @@ -179,12 +181,9 @@ class RosterWindow: # We don't know this contact return - groups = contact.groups - if not groups: - groups = [_('General')] acct = self._get_account_iter(account, model) found = [] # the contact iters. One per group - for group in groups: + for group in contact.groups: group_iter = self._get_group_iter(group, account, acct, model) contact_iter = model.iter_children(group_iter) @@ -199,7 +198,7 @@ class RosterWindow: elif model.iter_has_child(contact_iter): # it's a big brother and has children contact_iter = model.iter_children(contact_iter) - else: + else: # try to find next contact: # other contact in this group or brother contact next_contact_iter = model.iter_next(contact_iter) @@ -214,14 +213,14 @@ class RosterWindow: # we tested all contacts in this group contact_iter = None return found - - + + def _iter_is_separator(self, model, titer): ''' Return True if the given iter is a separator. - + Keyword arguments: model -- the data model - iter -- the gtk.TreeIter to test + iter -- the gtk.TreeIter to test ''' if model[titer][0] == 'SEPARATOR': return True @@ -230,7 +229,7 @@ class RosterWindow: def _iter_contact_rows(self, model = None): '''Iterate over all contact rows in given model. - + Keyword argument model -- the data model (default TreeFilterModel) ''' @@ -246,35 +245,36 @@ class RosterWindow: contact_iter = model.iter_next(contact_iter) group_iter = model.iter_next(group_iter) account_iter = model.iter_next(account_iter) - - -############################################################################# + + +############################################################################# ### Methods for adding and removing roster window items ############################################################################# - + def add_account(self, account): '''Add account to roster and draw it. Do nothing if it is already in.''' if self._get_account_iter(account): - # Will happen on reconnect or for merged accounts + # Will happen on reconnect or for merged accounts return - + if self.regroup: # Merged accounts view show = helpers.get_global_show() - self.model.append(None, [gajim.interface.jabber_state_images['16'][show], - _('Merged accounts'), 'account', '', 'all', None, None]) + self.model.append(None, [gajim.interface.jabber_state_images['16'][ + show], _('Merged accounts'), 'account', '', 'all', None, None]) else: show = gajim.SHOW_LIST[gajim.connections[account].connected] our_jid = gajim.get_jid_from_account(account) tls_pixbuf = None if gajim.account_is_securely_connected(account): - tls_pixbuf = self.window.render_icon(gtk.STOCK_DIALOG_AUTHENTICATION, - gtk.ICON_SIZE_MENU) # the only way to create a pixbuf from stock + # the only way to create a pixbuf from stock + tls_pixbuf = self.window.render_icon( + gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU) - self.model.append(None, [gajim.interface.jabber_state_images['16'][show], - gobject.markup_escape_text(account), - 'account', our_jid, account, None, tls_pixbuf]) + self.model.append(None, [gajim.interface.jabber_state_images['16'][ + show], gobject.markup_escape_text(account), 'account', our_jid, + account, None, tls_pixbuf]) self.draw_account(account) @@ -286,7 +286,7 @@ class RosterWindow: c1 = time.clock() self.starting = True jids = gajim.contacts.get_jid_list(account) - + self.tree.freeze_child_notify() c5 = time.clock() for jid in jids: @@ -306,26 +306,26 @@ class RosterWindow: self.draw_account(account) self.starting = False c10 = time.clock() - + if jids: c4 = time.clock() - + print "" print "--- Add account contacts of %s ---------" % account - print "Total Time", c4-c1 - print "Add contact without draw", c6-c5 + print "Total Time", c4-c1 + print "Add contact without draw", c6-c5 print "Draw groups and account", c10-c9 print "--- contacts added -----------------------------" print "" - + def _add_entity(self, contact, account, groups = None, big_brother_contact = None, big_brother_account = None): '''Add the given contact to roster data model. - + Contact is added regardless if he is already in roster or not. Return list of newly added iters. - + Keyword arguments: contact -- the contact to add account -- the contacts account @@ -340,9 +340,9 @@ class RosterWindow: parent_iters = self._get_contact_iter(big_brother_contact.jid, big_brother_account, big_brother_contact, self.model) assert len(parent_iters) > 0,\ - "Big brother is not yet in roster!" + 'Big brother is not yet in roster!' - # Do not confuse get_contact_iter + # Do not confuse get_contact_iter # Sync groups of family members contact.groups = big_brother_contact.groups[:] @@ -353,13 +353,9 @@ class RosterWindow: else: # We are a normal contact. Add us to our groups. if not groups: - if contact.is_observer(): - contact.groups = [_('Observers')] groups = contact.groups - if not groups: - groups = [_('General')] for group in groups: - child_iterG = self._get_group_iter(group, account, model = self.model) + child_iterG = self._get_group_iter(group, account, model=self.model) if not child_iterG: # Group is not yet in roster, add it! child_iterA = self._get_account_iter(account, self.model) @@ -375,35 +371,30 @@ class RosterWindow: typestr = 'groupchat' else: typestr = 'contact' - + # we add some values here. see draw_contact for more i_ = self.model.append(child_iterG, (None, contact.get_shown_name(), typestr, contact.jid, account, None, None)) added_iters.append(i_) # Restore the group expand state - # FIXME path may be invalid at this point! - path = self.model.get_path(child_iterG) if account + group in self.collapsed_rows: is_expanded = False - self.tree.collapse_row(path) else: is_expanded = True - self.tree.expand_row(path, False) if group not in gajim.groups[account]: gajim.groups[account][group] = {'expand': is_expanded} - - assert len(added_iters), "%s has not been added to roster!" % contact.jid - return added_iters - - + + assert len(added_iters), '%s has not been added to roster!' % contact.jid + return added_iters + def _remove_entity(self, contact, account, groups = None): '''Remove the given contact from roster data model. - + Empty groups after contact removal are removed too. Return False if contact still has children and deletion was not performed. Return True on success. - + Keyword arguments: contact -- the contact to add account -- the contacts account @@ -411,7 +402,7 @@ class RosterWindow: ''' iters = self._get_contact_iter(contact.jid, account, contact, self.model) - assert iters, "%s shall be removed but is not in roster" % contact.jid + assert iters, '%s shall be removed but is not in roster' % contact.jid parent_iter = self.model.iter_parent(iters[0]) parent_type = self.model[parent_iter][C_TYPE] @@ -419,9 +410,10 @@ class RosterWindow: if groups: # Only remove from specified groups all_iters = iters[:] - group_iters = [self._get_group_iter(group, account) for group in groups] - iters = [titer for titer in all_iters - if self.model.iter_parent(titer) in group_iters] + group_iters = [self._get_group_iter(group, account) for group in \ + groups] + iters = [titer for titer in all_iters + if self.model.iter_parent(titer) in group_iters] iter_children = self.model.iter_children(iters[0]) @@ -432,24 +424,23 @@ class RosterWindow: # Remove us and empty groups from the model for i in iters: parent_i = self.model.iter_parent(i) - self.model.remove(i) - if parent_type == 'group' and \ - self.model.iter_n_children(parent_i) == 0: + self.model.iter_n_children(parent_i) == 1: group = self.model[parent_i][C_JID].decode('utf-8') if gajim.groups[account].has_key(group): del gajim.groups[account][group] self.model.remove(parent_i) + else: + self.model.remove(i) return True - def _add_metacontact_family(self, family, account): '''Add the give Metacontact family to roster data model. - - Add Big Brother to his groups and all others under him. + + Add Big Brother to his groups and all others under him. Return list of all added (contact, account) tuples with Big Brother as first element. - + Keyword arguments: family -- the family, see Contacts.get_metacontacts_family() ''' @@ -459,10 +450,11 @@ class RosterWindow: nearby_family = family else: # we want one nearby_family per account - nearby_family = [data for data in family + nearby_family = [data for data in family if account == data['account']] - big_brother_data = gajim.contacts.get_metacontacts_big_brother(nearby_family) + big_brother_data = gajim.contacts.get_metacontacts_big_brother( + nearby_family) big_brother_jid = big_brother_data['jid'] big_brother_account = big_brother_data['account'] big_brother_contact = gajim.contacts.get_first_contact_from_jid( @@ -470,9 +462,10 @@ class RosterWindow: assert len(self._get_contact_iter(big_brother_jid, big_brother_account, big_brother_contact, self.model)) == 0,\ - "Big brother %s already in roster \n Family: %s" % (big_brother_jid, family) + 'Big brother %s already in roster \n Family: %s' % (big_brother_jid, + family) self._add_entity(big_brother_contact, big_brother_account) - + brothers = [] # Filter family members for data in nearby_family: @@ -486,20 +479,19 @@ class RosterWindow: if not _contact: # Corresponding account is not connected continue - + assert len(self._get_contact_iter(_jid, _account, _contact, self.model) ) == 0, "%s already in roster. \n Family: " % (_jid, nearby_family) - self._add_entity(_contact, _account, big_brother_contact = big_brother_contact, - big_brother_account = big_brother_account) + self._add_entity(_contact, _account, big_brother_contact = \ + big_brother_contact, big_brother_account=big_brother_account) brothers.append((_contact, _account)) brothers.insert(0, (big_brother_contact, big_brother_account)) return brothers - def _remove_metacontact_family(self, family, account): '''Remove the given Metacontact family from roster data model. - + See Contacts.get_metacontacts_family() and RosterWindow._remove_entity() ''' if self.regroup: @@ -507,30 +499,30 @@ class RosterWindow: nearby_family = family else: # remove nearby_family per account - nearby_family = [data for data in family + nearby_family = [data for data in family if account == data['account']] - # Family might has changed (actual big brother not on top). + # Family might has changed (actual big brother not on top). # Remove childs first then big brother family_in_roster = False for data in nearby_family: _account = data['account'] _jid = data['jid'] _contact = gajim.contacts.get_first_contact_from_jid(_account, _jid) - + iters = self._get_contact_iter(_jid, _account, _contact, self.model) if not iters or not _contact: # Family might not be up to date. # Only try to remove what is actually in the roster continue - assert iters, "%s shall be removed but is not in roster \ - \n Family: %s" % (_jid, family) + assert iters, '%s shall be removed but is not in roster \ + \n Family: %s' % (_jid, family) family_in_roster = True parent_iter = self.model.iter_parent(iters[0]) parent_type = self.model[parent_iter][C_TYPE] - + if parent_type != 'contact': # The contact on top old_big_account = _account @@ -539,68 +531,64 @@ class RosterWindow: continue ok = self._remove_entity(_contact, _account) - assert ok, "%s was not removed" % _jid - assert len(self._get_contact_iter(_jid, _account, _contact, self.model)) == 0,\ - "%s is removed but still in roster" % _jid - + assert ok, '%s was not removed' % _jid + assert len(self._get_contact_iter(_jid, _account, _contact, + self.model)) == 0, '%s is removed but still in roster' % _jid + if not family_in_roster: return False - iters = self._get_contact_iter(old_big_jid, old_big_account, old_big_contact, - self.model) - assert len(iters) > 0, "Old Big Brother %s is not in roster anymore" % old_big_jid + iters = self._get_contact_iter(old_big_jid, old_big_account, + old_big_contact, self.model) + assert len(iters) > 0, 'Old Big Brother %s is not in roster anymore' % \ + old_big_jid assert not self.model.iter_children(iters[0]),\ - "Old Big Brother %s still has children" % old_big_jid - - # This one is strange but necessary: - # Refilter filtered model to not crash hard. It thinks it still has children. - self.refilter_shown_roster_items() - + 'Old Big Brother %s still has children' % old_big_jid + ok = self._remove_entity(old_big_contact, old_big_account) assert ok, "Old Big Brother %s not removed" % old_big_jid - assert len(self._get_contact_iter(old_big_jid, old_big_account, old_big_contact, - self.model)) == 0,\ - "Old Big Brother %s is removed but still in roster" % old_big_jid - + assert len(self._get_contact_iter(old_big_jid, old_big_account, + old_big_contact, self.model)) == 0,\ + 'Old Big Brother %s is removed but still in roster' % old_big_jid + return True - - + def _add_self_contact(self, account): '''Add account's SelfContact to roster and draw it and the account. - + Return the SelfContact contact instance ''' jid = gajim.get_jid_from_account(account) contact = gajim.contacts.get_first_contact_from_jid(account, jid) - - assert len(self._get_contact_iter(jid, account, contact, self.model)) == 0,\ - "Self contact %s already in roster" % jid + + assert len(self._get_contact_iter(jid, account, contact, self.model)) == \ + 0, 'Self contact %s already in roster' % jid child_iterA = self._get_account_iter(account, self.model) self.model.append(child_iterA, (None, gajim.nicks[account], 'self_contact', jid, account, None, None)) - + self.draw_contact(jid, account) self.draw_avatar(jid, account) self.draw_account(account) return contact - - + + def add_contact(self, jid, account): '''Add contact to roster and draw him. - + Add contact to all its group and redraw the groups, the contact and the account. If it's a Metacontact, add and draw the whole family. - Do nothing if the contact is already in roster. - - Return the added contact instance. If it is a Metacontact return + Do nothing if the contact is already in roster. + + Return the added contact instance. If it is a Metacontact return Big Brother. - + Keyword arguments: jid -- the contact's jid or SelfJid to add SelfContact account -- the corresponding account. - + ''' contact = gajim.contacts.get_contact_with_highest_priority(account, jid) if len(self._get_contact_iter(jid, account, contact, self.model)): @@ -611,14 +599,14 @@ class RosterWindow: if contact.resource != gajim.connections[account].server_resource: return self._add_self_contact(account) return - + is_observer = contact.is_observer() if is_observer: # if he has a tag, remove it tag = gajim.contacts.get_metacontacts_tag(account, jid) if tag: gajim.contacts.remove_metacontact(account, jid) - + # Add contact to roster family = gajim.contacts.get_metacontacts_family(account, jid) contacts = [] @@ -635,16 +623,11 @@ class RosterWindow: self._add_entity(contact, account) # Draw the contact and its groups contact - if is_observer: - contact.groups = [_('Observers')] - groups = contact.groups - if not groups: - groups = [_('General')] if not self.starting: for c, acc in contacts: self.draw_contact(c.jid, acc) self.draw_avatar(c.jid, acc) - for group in groups: + for group in contact.groups: self.draw_group(group, account) self.draw_account(account) @@ -653,18 +636,19 @@ class RosterWindow: def remove_contact(self, jid, account, force = False, backend = False): '''Remove contact from roster. - - Remove contact from all its group. Remove empty groups or redraw otherwise. + + Remove contact from all its group. Remove empty groups or redraw + otherwise. Draw the account. If it's a Metacontact, remove the whole family. - Do nothing if the contact is not in roster. - + Do nothing if the contact is not in roster. + Keyword arguments: jid -- the contact's jid or SelfJid to remove SelfContact account -- the corresponding account. force -- remove contact even it has pending evens (Default False) backend -- also remove contact instance (Default False) - + ''' contact = gajim.contacts.get_contact_with_highest_priority(account, jid) @@ -685,22 +669,17 @@ class RosterWindow: if family: # We have a family. So we are a metacontact. self._remove_metacontact_family(family, account) - else: + else: self._remove_entity(contact, account) # Draw all groups of the contact - groups = contact.groups - if contact.is_observer(): - contact.groups = [_('Observers')] - if not groups: - groups = [_('General')] - if backend: + if backend: # Remove contact before redrawing, otherwise the old # numbers will still be show gajim.contacts.remove_jid(account, jid) - for group in groups: + for group in contact.groups: self.draw_group(group, account) self.draw_account(account) @@ -719,31 +698,30 @@ class RosterWindow: gajim.contacts.add_contact(account, contact) self.add_contact(jid, account) else: - contact.show = 'online' - self.draw_contact(jid, account) + contact.show = 'online' + self.draw_contact(jid, account) return contact - + def remove_groupchat(self, jid, account): '''Remove groupchat from roster and redraw account and group.''' contact = gajim.contacts.get_contact_with_highest_priority(account, jid) self.remove_contact(jid, account, force = True, backend = True) return True - + # TODO: This function is yet unused! Port to new API def add_transport(self, jid, account): '''Add transport to roster and draw it. Return the added contact instance.''' contact = gajim.contacts.get_contact_with_highest_priority(account, jid) - if contact is None: + if contact is None: contact = gajim.contacts.create_contact(jid = jid, name = jid, - groups = [_('Transports')], show = 'offline', + groups = [_('Transports')], show = 'offline', status = 'offline', sub = 'from') gajim.contacts.add_contact(account, contact) self.add_contact(jid, account) return contact - def remove_transport(self, jid, account): '''Remove transport from roster and redraw account and group.''' @@ -751,7 +729,7 @@ class RosterWindow: self.remove_contact(jid, account, force = True, backend = True) return True - #FIXME: + #FIXME: # We need to define a generic way to keep contacts in roster # as long as they have pending events or as we # still chat with them @@ -764,7 +742,8 @@ class RosterWindow: # Close chat window msg_win = gajim.interface.msg_win_mgr.get_window(contact.jid, account) - for ctrl in gajim.interface.msg_win_mgr.get_chat_controls(contact.jid, account): + for ctrl in gajim.interface.msg_win_mgr.get_chat_controls( + contact.jid, account): msg_win.remove_tab(ctrl, msg_win.CLOSE_CLOSE_BUTTON) else: need_readd = True @@ -778,19 +757,19 @@ class RosterWindow: def add_contact_to_groups(self, jid, account, groups): - '''Add contact to given groups and redraw them. - + '''Add contact to given groups and redraw them. + Contact on server is updated too. When the contact has a family, the action will be performed for all members. - + Keyword Arguments: jid -- the jid account -- the corresponding account groups -- list of Groups to add the contact to. - + ''' self.remove_contact(jid, account, force = True) - + for contact in gajim.contacts.get_contacts(account, jid): for group in groups: if group not in contact.groups: @@ -806,16 +785,16 @@ class RosterWindow: # self._adjust_group_expand_collapse_state(group, account) def remove_contact_from_groups(self, jid, account, groups): - '''Remove contact from given groups and redraw them. - + '''Remove contact from given groups and redraw them. + Contact on server is updated too. When the contact has a family, the action will be performed for all members. - + Keyword Arguments: jid -- the jid account -- the corresponding account groups -- list of Groups to remove the contact from - + ''' self.remove_contact(jid, account, force = True) @@ -826,13 +805,13 @@ class RosterWindow: contact.groups.remove(group) gajim.connections[account].update_contact(jid, contact.name, contact.groups) - + self.add_contact(jid, account) for group in groups: self.draw_group(group, account) - - # FIXME: maybe move to gajim.py + + # FIXME: maybe move to gajim.py def remove_newly_added(self, jid, account): if jid in gajim.newly_added[account]: gajim.newly_added[account].remove(jid) @@ -848,7 +827,7 @@ class RosterWindow: if jid in gajim.to_be_removed[account]: gajim.to_be_removed[account].remove(jid) self.draw_contact(jid, account) - + #FIXME: integrate into add_contact() def add_to_not_in_the_roster(self, account, jid, nick = '', resource = ''): keyID = '' @@ -862,11 +841,11 @@ class RosterWindow: gajim.contacts.add_contact(account, contact) self.add_contact(contact.jid, account) return contact - - -################################################################################ + + +################################################################################ ### Methods for adding and removing roster window items -################################################################################ +################################################################################ def draw_account(self, account): child_iter = self._get_account_iter(account, self.model) @@ -876,7 +855,7 @@ class RosterWindow: num_of_accounts = gajim.get_number_of_connected_accounts() num_of_secured = gajim.get_number_of_securely_connected_accounts() - + if gajim.account_is_securely_connected(account) and not self.regroup or \ self.regroup and num_of_secured and num_of_secured == num_of_accounts: tls_pixbuf = self.window.render_icon(gtk.STOCK_DIALOG_AUTHENTICATION, @@ -906,7 +885,7 @@ class RosterWindow: self.model[child_iter][C_NAME] = account_name return False - + def draw_group(self, group, account): child_iter = self._get_group_iter(group, account, model = self.model) if not child_iter: @@ -924,10 +903,10 @@ class RosterWindow: nbr_on, nbr_total = gajim.contacts.get_nb_online_total_contacts( accounts = accounts, groups = [group]) text += ' (%s/%s)' % (repr(nbr_on), repr(nbr_total)) - + self.model[child_iter][C_NAME] = gobject.markup_escape_text(text) return False - + def draw_parent_contact(self, jid, account): child_iters = self._get_contact_iter(jid, account, self.model) if not child_iters: @@ -940,7 +919,7 @@ class RosterWindow: parent_account = self.model[parent_iter][C_ACCOUNT].decode('utf-8') self.draw_contact(parent_jid, parent_account) return False - + def draw_contact(self, jid, account, selected = False, focus = False): '''draw the correct state image, name BUT not avatar''' # focus is about if the roster window has toplevel-focus or not @@ -950,16 +929,10 @@ class RosterWindow: child_iters = self._get_contact_iter(jid, account, contact, self.model) if not child_iters: - return False + return False name = gobject.markup_escape_text(contact.get_shown_name()) - groups = contact.groups - if contact.is_observer(): - groups = [_('Observers')] - elif not groups: - groups = [_('General')] - # gets number of unread gc marked messages if jid in gajim.interface.minimized_controls[account]: nb_unread = len(gajim.events.get_events(account, jid, @@ -971,13 +944,13 @@ class RosterWindow: name = '%s *' % name elif nb_unread > 1: name = '%s [%s]' % (name, str(nb_unread)) - + # Strike name if blocked strike = False if jid in gajim.connections[account].blocked_contacts: strike = True else: - for group in groups: + for group in contact.groups: if group in gajim.connections[account].blocked_groups: strike = True break @@ -1025,7 +998,7 @@ class RosterWindow: name += \ '\n%s' \ % (colorstring, gobject.markup_escape_text(status)) - + # Check if our metacontacts family has changed brothers = [] family = gajim.contacts.get_metacontacts_family(account, jid) @@ -1036,10 +1009,11 @@ class RosterWindow: nearby_family = family else: # we want one nearby_family per account - nearby_family = [data for data in family + nearby_family = [data for data in family if account == data['account']] - big_brother_data = gajim.contacts.get_metacontacts_big_brother(nearby_family) + big_brother_data = gajim.contacts.get_metacontacts_big_brother( + nearby_family) big_brother_jid = big_brother_data['jid'] big_brother_account = big_brother_data['account'] @@ -1127,11 +1101,13 @@ class RosterWindow: self.draw_contact(c.jid, acc) self.draw_avatar(c.jid, acc) - for group in groups: + for group in contact.groups: # We need to make sure that _visible_func is called for # our groups otherwise we might not be shown iterG = self._get_group_iter(group, account, model = self.model) - self.model[iterG][C_JID] = self.model[iterG][C_JID] + if iterG: + # it's not self contact + self.model[iterG][C_JID] = self.model[iterG][C_JID] return False @@ -1158,7 +1134,6 @@ class RosterWindow: gajim.gc_connected[account][room_jid]: win = gajim.interface.msg_win_mgr.get_window(room_jid, account) ctrl = gajim.interface.msg_win_mgr.get_gc_control(room_jid, account) - win.window.present() win.set_active_tab(ctrl) dialogs.ErrorDialog(_('You are already in group chat %s') % room_jid) return @@ -1187,7 +1162,6 @@ class RosterWindow: gc_win = gajim.interface.msg_win_mgr.get_window(room_jid, account) gc_control = gc_win.get_gc_control(room_jid, account) gc_win.set_active_tab(gc_control) - gc_win.window.present() gajim.connections[account].join_gc(nick, room_jid, password) if password: gajim.gc_passwords[room_jid] = password @@ -1202,23 +1176,17 @@ class RosterWindow: ''' contact = gajim.contacts.get_first_contact_from_jid(account, jid) - groups = contact.groups - if contact.is_observer(): - contact.groups = [_('Observers')] - if not groups: - groups = [_('General')] - self.draw_contact(jid, account) self.draw_account(account) - for group in groups: + for group in contact.groups: self.draw_group(group, account) # FIXME: Is this needed, Jim? #self._adjust_group_expand_collapse_state(group, account) def _idle_draw_jids_of_account(self, jids, account): '''Draw given contacts and their avatars in a lazy fashion. - + Keyword arguments: jids -- a list of jids to draw account -- the corresponding account @@ -1232,23 +1200,32 @@ class RosterWindow: print "Draw contact and avatar", time.clock() - t print "-------------------------------" yield False - + t = time.clock() task = _draw_all_contacts(jids, account, t) gobject.idle_add(task.next) - - def draw_roster(self): - '''clear and draw roster''' - # clear the model, only if it is not empty - if self.model: - self.model.clear() + + def setup_and_draw_roster(self): + '''create new empty model and draw roster''' + #(icon, name, type, jid, account, editable, avatar_pixbuf, padlock_pixbuf) + self.model = gtk.TreeStore(gtk.Image, str, str, str, str, gtk.gdk.Pixbuf, + gtk.gdk.Pixbuf) + + self.model.set_sort_func(1, self._compareIters) + self.model.set_sort_column_id(1, gtk.SORT_ASCENDING) + self.modelfilter = self.model.filter_new() + self.modelfilter.set_visible_func(self._visible_func) + self.modelfilter.connect('row-has-child-toggled', + self.on_modelfilter_row_has_child_toggled) + self.tree.set_model(self.modelfilter) + for acct in gajim.connections: self.add_account(acct) self.add_account_contacts(acct) # Recalculate column width for ellipsizing self.tree.columns_autosize() - - + + def select_contact(self, jid, account): '''Select contact in roster. If contact is hidden but has events, show him.''' @@ -1297,21 +1274,21 @@ class RosterWindow: ############################################################################## ### Roster and Modelfilter handling -############################################################################## - +############################################################################## + def _search_roster_func(self, model, column, key, titer): if model[titer][C_NAME].decode('utf-8').lower().startswith( gobject.markup_escape_text(key.lower())): return False return True - + def refilter_shown_roster_items(self): self.filtering = True self.modelfilter.refilter() - self.filtering = False - + self.filtering = False + def contact_has_pending_roster_events(self, contact, account): - ''' Return True if the contact or one if it resources has pending events''' + '''Return True if the contact or one if it resources has pending events''' # jid has pending events if gajim.events.get_nb_roster_events(account, contact.jid) > 0: return True @@ -1335,10 +1312,10 @@ class RosterWindow: if contact.jid in gajim.to_be_removed[account]: return True return False - return True - + return True + def _visible_func(self, model, titer): - '''Determine whether iter should be visible in the treeview''' + '''Determine whether iter should be visible in the treeview''' type_ = model[titer][C_TYPE] if not type_: return False @@ -1371,9 +1348,9 @@ class RosterWindow: accounts = [account] for _acc in accounts: for contact in gajim.contacts.iter_contacts(_acc): - # Is this contact in this group ? - if group in contact.groups or (group == _('General') and not \ - contact.groups): + # Is this contact in this group ? (last part of if check if it's + # self contact) + if group in contact.groups: if self.contact_is_visible(contact, _acc): return True return False @@ -1488,7 +1465,7 @@ class RosterWindow: return 0 ################################################################################ -### FIXME: Methods that don't belong to roster window... +### FIXME: Methods that don't belong to roster window... ### ... atleast not in there current form ################################################################################ @@ -1510,8 +1487,8 @@ class RosterWindow: elif (time.time() - result[2]) > 2592000: # ok, here we see that we have a message in unread messages table - # that is older than a month. It is probably from someone not in our - # roster for accounts we usually launch, so we will delete this id + # that is older than a month. It is probably from someone not in our + # roster for accounts we usually launch, so we will delete this id # from unread message tables. gajim.logger.set_read_messages([result[0]]) @@ -1574,7 +1551,8 @@ class RosterWindow: # If we already have chat windows opened, update them with new contact # instance - for chat_control in gajim.interface.msg_win_mgr.get_chat_controls(ji, account): + for chat_control in gajim.interface.msg_win_mgr.get_chat_controls(ji, + account): chat_control.contact = contact1 def _change_awn_icon_status(self, status): @@ -1642,7 +1620,7 @@ class RosterWindow: def connected_rooms(self, account): if account in gajim.gc_connected[account].values(): return True - return False + return False def auto_join_bookmarks(self, account): '''autojoin bookmarks that have 'auto join' on for this account''' @@ -1655,14 +1633,15 @@ class RosterWindow: minimize = bm['minimize'] in ('1', 'true') gajim.interface.join_gc_room(account, jid, bm['nick'], bm['password'], minimize = minimize) - + def on_event_removed(self, event_list): - '''Remove contacts on last events removed. + '''Remove contacts on last events removed. Only performed if removal was requested before but the contact still had pending events ''' - contact_list = ((event.jid.split('/')[0], event.account) for event in event_list) + contact_list = ((event.jid.split('/')[0], event.account) for event in \ + event_list) for jid, account in contact_list: self.draw_contact(jid, account) @@ -1673,7 +1652,7 @@ class RosterWindow: # Remove contact will delay removal if there are more events pending self.remove_contact(jid, account, backend = True) self.show_title() - + def open_event(self, account, jid, event): '''If an event was handled, return True, else return False''' data = event.parameters @@ -1707,7 +1686,7 @@ class RosterWindow: dialogs.InvitationReceivedDialog(account, data[0], jid, data[2], data[1]) gajim.events.remove_events(account, jid, event) - return True + return True return False ################################################################################ @@ -1720,7 +1699,7 @@ class RosterWindow: else: self.xml.get_widget('roster_vbox2').hide() - + def show_tooltip(self, contact): pointer = self.tree.get_pointer() props = self.tree.get_path_at_pos(pointer[0], pointer[1]) @@ -1796,15 +1775,16 @@ class RosterWindow: text = _('Enter your password for account %s') % account if passwords.USER_HAS_GNOMEKEYRING and \ not passwords.USER_USES_GNOMEKEYRING: - text += '\n' + _('Gnome Keyring is installed but not correctly started\ - (environment variable probably not correctly set)') + text += '\n' + _('Gnome Keyring is installed but not \ + correctly started (environment variable probably not \ + correctly set)') w = dialogs.PassphraseDialog(_('Password Required'), text, _('Save password')) passphrase, save = w.run() if passphrase == -1: if child_iterA: - self.model[child_iterA][0] = gajim.interface.jabber_state_images[ - '16']['offline'] + self.model[child_iterA][0] = \ + gajim.interface.jabber_state_images['16']['offline'] if gajim.interface.systray_enabled: gajim.interface.systray.change_status('offline') self.update_status_combobox() @@ -1826,19 +1806,6 @@ class RosterWindow: elif gajim.sleeper_state[account] not in ('autoaway', 'autoxa'): gajim.sleeper_state[account] = 'off' - if gajim.otr_module: - # disconnect from ENCRYPTED OTR contexts when going - # offline/invisible - if status == 'offline' or status == 'invisible': - ctx = gajim.connections[account].otr_userstates.context_root - while ctx is not None: - if ctx.msgstate == gajim.otr_module.OTRL_MSGSTATE_ENCRYPTED: - disconnected = True - gajim.otr_module.otrl_message_disconnect(gajim.connections[account].otr_userstates, - (gajim.otr_ui_ops, - {'account':account,'urgent':True}), ctx.accountname, - ctx.protocol, ctx.username) - ctx = ctx.next if to: gajim.connections[account].send_custom_status(status, txt, to) else: @@ -1889,10 +1856,11 @@ class RosterWindow: win.redraw_tab(ctrl) gajim.contacts.remove_contact(account, contact) - elif contact.jid == gajim.get_jid_from_account(account) and show == 'offline': - # Our SelfContact went offline. Remove him + elif contact.jid == gajim.get_jid_from_account(account) and \ + show == 'offline': + # Our SelfContact went offline. Remove him from roster and contacts self.remove_contact(contact.jid, account) - + gajim.contacts.remove_contact(account, contact) # print status in chat window and update status/GPG image if gajim.interface.msg_win_mgr.has_window(contact.jid, account): win = gajim.interface.msg_win_mgr.get_window(contact.jid, account) @@ -1944,16 +1912,18 @@ class RosterWindow: for contact in [c for c in lcontact if (c.show != 'offline' or \ c.is_transport())]: self.chg_contact_status(contact, 'offline', '', account) - # Remove SelfContact from roster. It might be gone when we return - self.remove_contact(gajim.get_jid_from_account(account), account) - + # Remove SelfContact from roster and remove it. + # It might be gone when we return + self_jid = gajim.get_jid_from_account(account) + self.remove_contact(self_jid, account) + gajim.contacts.remove_jid(account, self_jid) self.actions_menu_needs_rebuild = True self.update_status_combobox() # Force the rebuild now since the on_activates on the menu itself does # not work with the os/x top level menubar if sys.platform == 'darwin': - self.make_menu(force = True) - + self.make_menu(force = True) + def get_status_message(self, show): if show in gajim.config.get_per('defaultstatusmsg'): if gajim.config.get_per('defaultstatusmsg', show, 'enabled'): @@ -1966,7 +1936,7 @@ class RosterWindow: dlg.window.present() # show it on current workspace message = dlg.run() return message - + def change_status(self, widget, account, status): def change(account, status): message = self.get_status_message(status) @@ -1983,7 +1953,7 @@ class RosterWindow: on_response_ok = (change, account, status)) else: change(account, status) - + def update_status_combobox(self): # table to change index in connection.connected to index in combobox table = {'offline':9, 'connecting':9, 'online':0, 'chat':1, 'away':2, @@ -2021,13 +1991,13 @@ class RosterWindow: prio = u.priority show = u.show return show - + def on_message_window_delete(self, win_mgr, msg_win): if gajim.config.get('one_message_window') == 'always_with_roster': self.show_roster_vbox(True) gtkgui_helpers.resize_window(self.window, gajim.config.get('roster_width'), - gajim.config.get('roster_height')) + gajim.config.get('roster_height')) def close_all_from_dict(self, dic): '''close all the windows in the given dictionary''' @@ -2200,7 +2170,7 @@ class RosterWindow: helpers.exec_command('python history_manager.py') else: # Unix user helpers.exec_command('python history_manager.py &') - + def on_info(self, widget, contact, account): '''Call vcard_information_window class to display contact's information''' if gajim.connections[account].is_zeroconf: @@ -2324,7 +2294,7 @@ class RosterWindow: if not show: show = 'online' contact = gajim.contacts.create_contact(jid = jid, - name = account, show = show, + name = account, groups = ['self_contact'], show = show, status = roster.getStatus(jid+'/'+resource), resource = resource, priority = roster.getPriority(jid+'/'+resource)) @@ -2585,23 +2555,17 @@ class RosterWindow: for g in helpers.special_groups: if g in (new_text, old_text): return - # get all contacts in that group - for jid in gajim.contacts.get_jid_list(account): - contact = gajim.contacts.get_contact_with_highest_priority( - account, jid) - if old_text in contact.groups: - # set them in the new one and remove it from the old - contact.groups.remove(old_text) - self.remove_contact(contact.jid, account) - if new_text not in contact.groups: - contact.groups.append(new_text) - self.add_contact(contact.jid, account) - gajim.connections[account].update_contact(contact.jid, - contact.name, contact.groups) - # If last removed iter was not visible, gajim.groups is not cleaned - if gajim.groups[account].has_key(old_text): - del gajim.groups[account][old_text] - self.draw_group(new_text, account) + # update all contacts in the given group + if self.regroup: + accounts = gajim.connections.keys() + else: + accounts = [account,] + for acc in accounts: + for jid in gajim.contacts.get_jid_list(acc): + contact = gajim.contacts.get_first_contact_from_jid(acc, jid) + if old_text in contact.groups: + self.remove_contact_from_groups(jid, acc, [old_text,]) + self.add_contact_to_groups(jid, acc, [new_text,]) def on_canceled(): if gajim.interface.instances.has_key('rename'): @@ -2659,7 +2623,8 @@ class RosterWindow: keyID = keyID[0] keys[contact.jid] = keyID - for ctrl in gajim.interface.msg_win_mgr.get_chat_controls(contact.jid, account): + for ctrl in gajim.interface.msg_win_mgr.get_chat_controls(contact.jid, + account): ctrl.update_ui() keys_str = '' for jid in keys: @@ -2791,7 +2756,7 @@ class RosterWindow: if resource: # we MUST have one contact only in list_ contact_jid += '/' + resource gajim.connections[room_account].send_invite(room_jid, contact_jid) - + def on_all_groupchat_maximized(self, widget, group_list): for (contact, account) in group_list: self.on_groupchat_maximized(widget, contact.jid, account) @@ -2799,14 +2764,14 @@ class RosterWindow: def on_groupchat_maximized(self, widget, jid, account): '''When a groupchat is maximised''' ctrl = gajim.interface.minimized_controls[account][jid] - mw = gajim.interface.msg_win_mgr.get_window(ctrl.contact.jid, ctrl.account) + mw = gajim.interface.msg_win_mgr.get_window(ctrl.contact.jid, + ctrl.account) if not mw: mw = gajim.interface.msg_win_mgr.create_window(ctrl.contact, ctrl.account, ctrl.type_id) ctrl.parent_win = mw mw.new_tab(ctrl) mw.set_active_tab(ctrl) - mw.window.present() del gajim.interface.minimized_controls[account][jid] self.remove_groupchat(jid, account) @@ -3051,7 +3016,7 @@ class RosterWindow: jids dialogs.ConfirmationDialog(pritext, sectext, on_response_ok = (on_ok2, list_)) - + def on_send_custom_status(self, widget, contact_list, show, group=None): '''send custom status''' dlg = dialogs.ChangeStatusMessageDialog(show) @@ -3090,7 +3055,8 @@ class RosterWindow: return status = model[active][2].decode('utf-8') statuses_unified = helpers.statuses_unified() # status "desync'ed" or not - if (active == 7 and statuses_unified) or (active == 9 and not statuses_unified): + if (active == 7 and statuses_unified) or (active == 9 and \ + not statuses_unified): # 'Change status message' selected: # do not change show, just show change status dialog status = model[self.previous_status_combobox_active][2].decode('utf-8') @@ -3263,18 +3229,6 @@ class RosterWindow: def on_profile_avatar_menuitem_activate(self, widget, account): gajim.interface.edit_own_details(account) - def play_tictactoe(self, widget, contact, account, resource=None): - jid = contact.jid - - if resource is not None: - jid = jid + u'/' + resource - - import tictactoe - - sess = gajim.connections[account].make_new_session(jid, - cls=tictactoe.TicTacToeSession) - sess.begin() - def on_execute_command(self, widget, contact, account, resource=None): '''Execute command. Full JID needed; if it is other contact, resource is necessary. Widget is unnecessary, only to be @@ -3416,7 +3370,7 @@ class RosterWindow: for group in gajim.groups[account]: if gajim.groups[account][group]['expand']: titer = self._get_group_iter(group, account) - if titer: + if titer: path = model.get_path(titer) self.tree.expand_row(path, False) elif type_ == 'contact': @@ -3426,7 +3380,7 @@ class RosterWindow: self.draw_contact(jid, account) self._toggeling_row = False - + def on_roster_treeview_row_collapsed(self, widget, titer, path): '''When a row is collapsed change the icon of the arrow''' self._toggeling_row = True @@ -3438,7 +3392,7 @@ class RosterWindow: accounts = gajim.connections.keys() else: accounts = [model[titer][C_ACCOUNT].decode('utf-8')] - + type_ = model[titer][C_TYPE] if type_ == 'group': child_model[child_iter][C_IMG] = gajim.interface.jabber_state_images[ @@ -3459,7 +3413,7 @@ class RosterWindow: jid = model[titer][C_JID].decode('utf-8') account = model[titer][C_ACCOUNT].decode('utf-8') self.draw_contact(jid, account) - + self._toggeling_row = False def on_modelfilter_row_has_child_toggled(self, model, path, titer): @@ -3470,14 +3424,14 @@ class RosterWindow: if self._toggeling_row: # Signal is emitted when we write to our model return - + type_ = model[titer][C_TYPE] account = model[titer][C_ACCOUNT] if not account: return account = account.decode('utf-8') - + if type_ == 'contact': child_iter = model.convert_iter_to_child_iter(titer) if self.model.iter_has_child(child_iter): @@ -3501,7 +3455,7 @@ class RosterWindow: ''' # Selection can change when the model is filtered # Only write to the model when filtering is finished! - + # FIXME: When we are filtering our custom colors are somehow lost model, list_of_paths = selection.get_selected_rows() @@ -3520,7 +3474,7 @@ class RosterWindow: jid = row[C_JID].decode('utf-8') account = row[C_ACCOUNT].decode('utf-8') self._last_selected_contact.append((jid, account)) - gobject.idle_add(self.draw_contact, jid, account, True) + gobject.idle_add(self.draw_contact, jid, account, True) def on_service_disco_menuitem_activate(self, widget, account): server_jid = gajim.config.get_per('accounts', account, 'hostname') @@ -3542,8 +3496,8 @@ class RosterWindow: if gajim.config.get('showoffline'): # We need to filter twice to show groups with no contacts inside # in the correct expand state - self.refilter_shown_roster_items() - + self.refilter_shown_roster_items() + def on_view_menu_activate(self, widget): # Hide the show roster menu if we are not in the right windowing mode. @@ -3556,8 +3510,8 @@ class RosterWindow: # when num controls is 0 this menuitem is hidden, but still need to # disable keybinding if self.hpaned.get_child2() is not None: - self.show_roster_vbox(widget.get_active()) - + self.show_roster_vbox(widget.get_active()) + ################################################################################ ### Drag and Drop handling ################################################################################ @@ -3594,7 +3548,7 @@ class RosterWindow: gajim.config.set('confirm_metacontacts', 'no') else: gajim.config.set('confirm_metacontacts', 'yes') - + # We might have dropped on a metacontact. # Remove it and readd later with updated family info dest_family = gajim.contacts.get_metacontacts_family(account_dest, @@ -3608,11 +3562,11 @@ class RosterWindow: c_source.jid) old_groups = c_source.groups - # Remove old source contact(s) + # Remove old source contact(s) if was_big_brother: # We have got little brothers. Readd them all self._remove_metacontact_family(old_family, account_source) - else: + else: # We are only a litle brother. Simply remove us from our big brother if self._get_contact_iter(c_source.jid, account_source): # When we have been in the group before. @@ -3624,7 +3578,7 @@ class RosterWindow: own_data['account'] = account_source # Don't touch the rest of the family old_family = [own_data] - + # Apply new tag and update contact for data in old_family: if account_source != data['account'] and not self.regroup: @@ -3644,7 +3598,7 @@ class RosterWindow: new_family = gajim.contacts.get_metacontacts_family(account_source, c_source.jid) brothers = self._add_metacontact_family(new_family, account_source) - + for c, acc in brothers: self.draw_contact(c.jid, acc) self.draw_avatar(c.jid, acc) @@ -3652,7 +3606,7 @@ class RosterWindow: old_groups.extend(c_dest.groups) for g in old_groups: self.draw_group(g, account_source) - + self.draw_account(account_source) context.finish(True, True, etime) @@ -3669,16 +3623,17 @@ class RosterWindow: _('Do _not ask me again'), on_response_ok = merge_contacts) if not confirm_metacontacts: # First time we see this window dlg.checkbutton.set_active(True) - - def on_drop_in_group(self, widget, account, c_source, grp_dest, is_big_brother, - context, etime, grp_source = None): + + def on_drop_in_group(self, widget, account, c_source, grp_dest, + is_big_brother, context, etime, grp_source = None): if is_big_brother: # add whole metacontact to new group self.add_contact_to_groups(c_source.jid, account, [grp_dest,]) - # remove afterwards so the contact is not moved to General in the meantime + # remove afterwards so the contact is not moved to General in the + # meantime if grp_dest != grp_source: - self.remove_contact_from_groups(c_source.jid, account, [grp_source,]) + self.remove_contact_from_groups(c_source.jid, account, [grp_source]) else: # Normal contact or little brother family = gajim.contacts.get_metacontacts_family(account, @@ -3703,13 +3658,15 @@ class RosterWindow: else: # Normal contact self.add_contact_to_groups(c_source.jid, account, [grp_dest,]) - # remove afterwards so the contact is not moved to General in the meantime + # remove afterwards so the contact is not moved to General in the + # meantime if grp_dest != grp_source: - self.remove_contact_from_groups(c_source.jid, account, [grp_source,]) + self.remove_contact_from_groups(c_source.jid, account, + [grp_source]) if context.action in (gtk.gdk.ACTION_MOVE, gtk.gdk.ACTION_COPY): context.finish(True, True, etime) - + def drag_drop(self, treeview, context, x, y, timestamp): target_list = treeview.drag_dest_get_target_list() @@ -3760,7 +3717,8 @@ class RosterWindow: uri = data.strip() uri_splitted = uri.split() # we may have more than one file dropped try: - uri_splitted.remove('\0') # This is always the last element in windows + # This is always the last element in windows + uri_splitted.remove('\0') except ValueError: pass nb_uri = len(uri_splitted) @@ -3831,7 +3789,8 @@ class RosterWindow: if grp_source in helpers.special_groups and \ grp_source not in ('Not in Roster', 'Observers'): # a transport or a minimized groupchat was dragged - # we can add it to other accounts but not move it to another group, see below + # we can add it to other accounts but not move it to another group, + # see below return jid_source = data.decode('utf-8') c_source = gajim.contacts.get_contact_with_highest_priority( @@ -3887,12 +3846,12 @@ class RosterWindow: self.on_drop_in_contact(treeview, account_source, c_source, account_dest, c_dest, is_big_brother, context, etime) return - + ################################################################################ ### Everything about images and icons.... ### Cleanup assigned to Jim++ :-) ################################################################################ - + def get_appropriate_state_images(self, jid, size = '16', icon_name = 'online'): '''check jid and return the appropriate state images dict for @@ -3909,7 +3868,6 @@ class RosterWindow: return self.transports_state_images[size][transport] return gajim.interface.jabber_state_images[size] - def make_transport_state_images(self, transport): '''initialise opened and closed 'transport' iconset dict''' if gajim.config.get('use_transports_iconsets'): @@ -3917,19 +3875,19 @@ class RosterWindow: '16x16') pixo, pixc = gtkgui_helpers.load_icons_meta() self.transports_state_images['opened'][transport] = \ - gtkgui_helpers.load_iconset(folder, pixo, transport = True) + gtkgui_helpers.load_iconset(folder, pixo, transport=True) self.transports_state_images['closed'][transport] = \ - gtkgui_helpers.load_iconset(folder, pixc, transport = True) + gtkgui_helpers.load_iconset(folder, pixc, transport=True) folder = os.path.join(helpers.get_transport_path(transport), '32x32') - self.transports_state_images['32'][transport] = gtkgui_helpers.load_iconset( - folder, transport = True) + self.transports_state_images['32'][transport] = \ + gtkgui_helpers.load_iconset(folder, transport=True) folder = os.path.join(helpers.get_transport_path(transport), '16x16') - self.transports_state_images['16'][transport] = gtkgui_helpers.load_iconset( - folder, transport = True) - + self.transports_state_images['16'][transport] = \ + gtkgui_helpers.load_iconset(folder, transport=True) + def update_jabber_state_images(self): # Update the roster - self.draw_roster() + self.setup_and_draw_roster() # Update the status combobox model = self.status_combobox.get_model() titer = model.get_iter_root() @@ -3937,7 +3895,8 @@ class RosterWindow: if model[titer][2] != '': # If it's not change status message iter # eg. if it has show parameter not '' - model[titer][1] = gajim.interface.jabber_state_images['16'][model[titer][2]] + model[titer][1] = gajim.interface.jabber_state_images['16'][model[ + titer][2]] titer = model.iter_next(titer) # Update the systray if gajim.interface.systray_enabled: @@ -3949,7 +3908,7 @@ class RosterWindow: win.redraw_tab(ctrl) self.update_status_combobox() - + def set_account_status_icon(self, account): status = gajim.connections[account].connected child_iterA = self._get_account_iter(account, self.model) @@ -3959,7 +3918,8 @@ class RosterWindow: show = gajim.SHOW_LIST[status] else: # accounts merged show = helpers.get_global_show() - self.model[child_iterA][C_IMG] = gajim.interface.jabber_state_images['16'][show] + self.model[child_iterA][C_IMG] = gajim.interface.jabber_state_images[ + '16'][show] ################################################################################ ### Style and theme related methods @@ -4030,7 +3990,7 @@ class RosterWindow: for contact in self._iter_contact_rows(): self.draw_contact(contact[C_JID].decode('utf-8'), contact[C_ACCOUNT].decode('utf-8')) - + def set_renderer_color(self, renderer, style, set_background = True): '''set style for treeview cell, using PRELIGHT system color''' if set_background: @@ -4039,7 +3999,7 @@ class RosterWindow: else: fgcolor = self.tree.style.fg[style] renderer.set_property('foreground-gdk', fgcolor) - + def _iconCellDataFunc(self, column, renderer, model, titer, data = None): '''When a row is added, set properties for icon renderer''' theme = gajim.config.get('roster_theme') @@ -4082,7 +4042,7 @@ class RosterWindow: else: renderer.set_property('xalign', 0.4) renderer.set_property('width', 26) - + def _nameCellDataFunc(self, column, renderer, model, titer, data = None): '''When a row is added, set properties for name renderer''' theme = gajim.config.get('roster_theme') @@ -4202,18 +4162,19 @@ class RosterWindow: renderer.set_property('xalign', 1) # align pixbuf to the right else: renderer.set_property('visible', False) - + ################################################################################ ### Everything about building menus ### FIXME: We really need to make it simpler! 1465 lines are a few to much.... -################################################################################ +################################################################################ def make_menu(self, force = False): '''create the main window\'s menus''' if not force and not self.actions_menu_needs_rebuild: return new_chat_menuitem = self.xml.get_widget('new_chat_menuitem') - single_message_menuitem = self.xml.get_widget('send_single_message_menuitem') + single_message_menuitem = self.xml.get_widget( + 'send_single_message_menuitem') join_gc_menuitem = self.xml.get_widget('join_gc_menuitem') muc_icon = gtkgui_helpers.load_icon('muc_active') if muc_icon: @@ -4476,10 +4437,13 @@ class RosterWindow: advanced_sub_menu.show_all() if sys.platform == 'darwin': - syncmenu.takeover_menu(self.xml.get_widget('menubar')) + try: + syncmenu.takeover_menu(self.xml.get_widget('menubar')) + except NameError: + pass + + self.actions_menu_needs_rebuild = False - self.actions_menu_needs_rebuild = False - def build_account_menu(self, account): # we have to create our own set of icons for the menu # using self.jabber_status_images is poopoo @@ -4570,8 +4534,8 @@ class RosterWindow: else: pep_config.set_sensitive(False) pep_submenu.append(pep_config) - pep_config.connect('activate', self.on_pep_services_menuitem_activate, - account) + pep_config.connect('activate', + self.on_pep_services_menuitem_activate, account) img = gtk.image_new_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU) pep_config.set_image(img) @@ -4696,7 +4660,7 @@ class RosterWindow: menu.connect('selection-done', gtkgui_helpers.destroy_widget) menu.show_all() menu.popup(None, None, None, event_button, event.time) - + def make_group_menu(self, event, titer): '''Make group's popup menu''' model = self.modelfilter @@ -4711,8 +4675,7 @@ class RosterWindow: for jid in gajim.contacts.get_jid_list(account): contact = gajim.contacts.get_contact_with_highest_priority(account, jid) - if group in contact.groups or (contact.groups == [] and group == \ - _('General')): + if group in contact.groups: if contact.show not in ('offline', 'error'): list_online.append((contact, account)) list_.append((contact, account)) @@ -4739,7 +4702,8 @@ class RosterWindow: group_message_to_all_item = gtk.MenuItem(_('To all users')) send_group_message_submenu.append(group_message_to_all_item) - group_message_to_all_online_item = gtk.MenuItem(_('To all online users')) + group_message_to_all_online_item = gtk.MenuItem( + _('To all online users')) send_group_message_submenu.append(group_message_to_all_online_item) group_message_to_all_online_item.connect('activate', @@ -4757,10 +4721,12 @@ class RosterWindow: menu.append(invite_menuitem) # Send Custom Status - send_custom_status_menuitem = gtk.ImageMenuItem(_('Send Cus_tom Status')) + send_custom_status_menuitem = gtk.ImageMenuItem( + _('Send Cus_tom Status')) # add a special img for this menuitem if group in gajim.connections[account].blocked_groups: - send_custom_status_menuitem.set_image(gtkgui_helpers.load_icon('offline')) + send_custom_status_menuitem.set_image(gtkgui_helpers.load_icon( + 'offline')) send_custom_status_menuitem.set_sensitive(False) else: icon = gtk.image_new_from_stock(gtk.STOCK_NETWORK, @@ -4774,8 +4740,8 @@ class RosterWindow: # icon MUST be different instance for every item state_images = gtkgui_helpers.load_iconset(path) status_menuitem = gtk.ImageMenuItem(helpers.get_uf_show(s)) - status_menuitem.connect('activate', self.on_send_custom_status, list_, - s, group) + status_menuitem.connect('activate', self.on_send_custom_status, + list_, s, group) icon = state_images[s] status_menuitem.set_image(icon) status_menuitems.append(status_menuitem) @@ -4849,7 +4815,7 @@ class RosterWindow: menu.attach_to_widget(self.tree, None) menu.connect('selection-done', gtkgui_helpers.destroy_widget) menu.show_all() - menu.popup(None, None, None, event_button, event.time) + menu.popup(None, None, None, event_button, event.time) def make_contact_menu(self, event, titer): '''Make contact\'s popup menu''' @@ -4995,25 +4961,19 @@ class RosterWindow: execute_command_menuitem = xml.get_widget( 'execute_command_menuitem') - tictactoe_menuitem = xml.get_widget('tictactoe_menuitem') - tictactoe_menuitem.connect('activate', self.play_tictactoe, contact, - account, contact.resource) - # send custom status icon blocked = False if jid in gajim.connections[account].blocked_contacts: blocked = True else: - groups = contact.groups - if contact.is_observer(): - groups = [_('Observers')] - elif not groups: - groups = [_('General')] - for group in groups: + for group in contact.groups: if group in gajim.connections[account].blocked_groups: blocked = True break - if blocked: + if gajim.get_transport_name_from_jid(jid, use_config_setting=False): + # Transport contact, send custom status unavailable + send_custom_status_menuitem.set_sensitive(False) + elif blocked: send_custom_status_menuitem.set_image( \ gtkgui_helpers.load_icon('offline')) send_custom_status_menuitem.set_sensitive(False) @@ -5129,7 +5089,8 @@ class RosterWindow: ask_auth_menuitem.connect('activate', self.req_sub, jid, _('I would like to add you to my roster'), account, contact.groups, contact.name) - if contact.sub in ('to', 'none'): + if contact.sub in ('to', 'none') or gajim.get_transport_name_from_jid( + jid, use_config_setting=False): revoke_auth_menuitem.set_sensitive(False) else: revoke_auth_menuitem.connect('activate', self.revoke_auth, jid, @@ -5182,8 +5143,8 @@ class RosterWindow: gtkgui_helpers.destroy_widget) roster_contact_context_menu.show_all() roster_contact_context_menu.popup(None, None, None, event_button, - event.time) - + event.time) + def make_multiple_contact_menu(self, event, iters): '''Make group's popup menu''' model = self.modelfilter @@ -5280,7 +5241,7 @@ class RosterWindow: menu.connect('selection-done', gtkgui_helpers.destroy_widget) menu.show_all() menu.popup(None, None, None, event_button, event.time) - + def make_transport_menu(self, event, titer): '''Make transport\'s popup menu''' model = self.modelfilter @@ -5306,7 +5267,8 @@ class RosterWindow: send_custom_status_menuitem = gtk.ImageMenuItem(_('Send Cus_tom Status')) # add a special img for this menuitem if blocked: - send_custom_status_menuitem.set_image(gtkgui_helpers.load_icon('offline')) + send_custom_status_menuitem.set_image(gtkgui_helpers.load_icon( + 'offline')) send_custom_status_menuitem.set_sensitive(False) else: if gajim.interface.status_sent_to_users.has_key(account) and \ @@ -5460,7 +5422,7 @@ class RosterWindow: menu.connect('selection-done', gtkgui_helpers.destroy_widget) menu.show_all() menu.popup(None, None, None, event_button, event.time) - + def build_resources_submenu(self, contacts, account, action, room_jid=None, room_account=None): ''' Build a submenu with contact's resources. @@ -5558,7 +5520,7 @@ class RosterWindow: menuitem.connect('activate', self.on_invite_to_room, list_, room_jid, account) invite_to_submenu.append(menuitem) - + def get_and_connect_advanced_menuitem_menu(self, account): '''adds FOR ACCOUNT options''' xml = gtkgui_helpers.get_glade('advanced_menuitem_menu.glade') @@ -5605,7 +5567,7 @@ class RosterWindow: advanced_menuitem_menu.show_all() return advanced_menuitem_menu - + def add_history_manager_menuitem(self, menu): '''adds a seperator and History Manager menuitem BELOW for account menuitems''' @@ -5619,7 +5581,7 @@ class RosterWindow: item.set_image(icon) menu.append(item) item.connect('activate', self.on_history_manager_menuitem_activate) - + def add_bookmarks_list(self, gc_sub_menu, account): '''Show join new group chat item and bookmarks list for an account''' item = gtk.ImageMenuItem(_('_Join New Group Chat')) @@ -5638,7 +5600,7 @@ class RosterWindow: item.connect('activate', self.on_bookmark_menuitem_activate, account, bookmark) gc_sub_menu.append(item) - + def set_actions_menu_needs_rebuild(self): self.actions_menu_needs_rebuild = True # Force the rebuild now since the on_activates on the menu itself does @@ -5646,7 +5608,7 @@ class RosterWindow: if sys.platform == 'darwin': self.make_menu(force = True) return - + def show_appropriate_context_menu(self, event, iters): # iters must be all of the same type model = self.modelfilter @@ -5686,8 +5648,10 @@ class RosterWindow: self.show_appropriate_context_menu(event, iters) return True - + def setup_for_osx(self): + # This is broken + return '''Massage the GTK menu so it will match up to the OS/X nib style menu when passed to sync-menu and merged''' main_menu = self.xml.get_widget('menubar') @@ -5732,11 +5696,11 @@ class RosterWindow: # Hide the GTK menubar itself and let the OS/X menubar do its thing #self.xml.get_widget('menubar').hide() return - + ################################################################################ -### +### ################################################################################ - + def __init__(self): self.filtering = False self.xml = gtkgui_helpers.get_glade('roster_window.glade') @@ -5784,20 +5748,6 @@ class RosterWindow: self.popups_notification_height = 0 self.popup_notification_windows = [] - #(icon, name, type, jid, account, editable, avatar_pixbuf, padlock_pixbuf) - self.model = gtk.TreeStore(gtk.Image, str, str, str, str, gtk.gdk.Pixbuf, - gtk.gdk.Pixbuf) - - self.model.set_sort_func(1, self._compareIters) - self.model.set_sort_column_id(1, gtk.SORT_ASCENDING) - self.modelfilter = self.model.filter_new() - self.modelfilter.set_visible_func(self._visible_func) - self.modelfilter.connect('row-has-child-toggled', - self.on_modelfilter_row_has_child_toggled) - self.tree.set_model(self.modelfilter) - # Workaroung: For strange reasons signal is behaving like row-changed - self._toggeling_row = False - # Remove contact from roster when last event opened # { (contact, account): { backend: boolean } self.contacts_to_be_removed = {} @@ -5834,8 +5784,8 @@ class RosterWindow: for show in ('online', 'chat', 'away', 'xa', 'dnd', 'invisible'): uf_show = helpers.get_uf_show(show) - liststore.append([uf_show, gajim.interface.jabber_state_images['16'][show], - show, True]) + liststore.append([uf_show, gajim.interface.jabber_state_images['16'][ + show], show, True]) # Add a Separator (self._iter_is_separator() checks on string SEPARATOR) liststore.append(['SEPARATOR', None, '', True]) @@ -5849,8 +5799,8 @@ class RosterWindow: liststore.append(['SEPARATOR', None, '', True]) uf_show = helpers.get_uf_show('offline') - liststore.append([uf_show, gajim.interface.jabber_state_images['16']['offline'], - 'offline', True]) + liststore.append([uf_show, gajim.interface.jabber_state_images['16'][ + 'offline'], 'offline', True]) status_combobox_items = ['online', 'chat', 'away', 'xa', 'dnd', 'invisible', 'separator1', 'change_status_msg', 'separator2', @@ -5927,8 +5877,8 @@ class RosterWindow: # signals self.TARGET_TYPE_URI_LIST = 80 - TARGETS = [('MY_TREE_MODEL_ROW', gtk.TARGET_SAME_APP | gtk.TARGET_SAME_WIDGET, - 0)] + TARGETS = [('MY_TREE_MODEL_ROW', + gtk.TARGET_SAME_APP | gtk.TARGET_SAME_WIDGET, 0)] TARGETS2 = [('MY_TREE_MODEL_ROW', gtk.TARGET_SAME_WIDGET, 0), ('text/uri-list', 0, self.TARGET_TYPE_URI_LIST)] self.tree.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, TARGETS, @@ -5945,7 +5895,9 @@ class RosterWindow: self.collapsed_rows = gajim.config.get('collapsed_rows').split('\t') self.tooltip = tooltips.RosterTooltip() - self.draw_roster() + # Workaroung: For strange reasons signal is behaving like row-changed + self._toggeling_row = False + self.setup_and_draw_roster() for account in gajim.connections: if gajim.config.get_per('accounts', account, 'publish_tune'): diff --git a/src/session.py b/src/session.py index 74666c048..d2823c89b 100644 --- a/src/session.py +++ b/src/session.py @@ -186,7 +186,6 @@ class ChatControlSession(stanza_session.EncryptedStanzaSession): if ctrl: self.control = ctrl self.control.set_session(self) - self.control.parent_win.move_from_sessionless(self.control) first = False if pm: @@ -317,8 +316,8 @@ class ChatControlSession(stanza_session.EncryptedStanzaSession): if popup: if not self.control: - self.control = gajim.interface.new_chat(self, contact, - self.conn.name, resource=resource_for_chat) + self.control = gajim.interface.new_chat(contact, + self.conn.name, resource=resource_for_chat, session=self) if len(gajim.events.get_events(self.conn.name, fjid)): self.control.read_queue() diff --git a/src/statusicon.py b/src/statusicon.py index 8dfe843e3..54d5aec3e 100644 --- a/src/statusicon.py +++ b/src/statusicon.py @@ -27,7 +27,10 @@ from common import gajim from common import helpers if sys.platform == 'darwin': - import osx + try: + import osx + except ImportError: + pass class StatusIcon(systray.Systray): '''Class for the notification area icon''' @@ -69,7 +72,10 @@ class StatusIcon(systray.Systray): self.status_icon.set_tooltip(text) if gajim.events.get_nb_systray_events(): if sys.platform == 'darwin': - osx.nsapp.requestUserAttention() + try: + osx.nsapp.requestUserAttention() + except NameError: + pass state = 'event' self.status_icon.set_blinking(True) else: diff --git a/src/systray.py b/src/systray.py index b9df2c287..a2f0c10fb 100644 --- a/src/systray.py +++ b/src/systray.py @@ -109,7 +109,6 @@ class Systray: if gajim.interface.msg_win_mgr.has_window(jid, account): gajim.interface.msg_win_mgr.get_window(jid, account).set_active_tab( jid, account) - gajim.interface.msg_win_mgr.get_window(jid, account).window.present() elif contact: gajim.interface.new_chat(contact, account) gajim.interface.msg_win_mgr.get_window(jid, account).set_active_tab( diff --git a/src/tictactoe.py b/src/tictactoe.py deleted file mode 100644 index 47db0ebf4..000000000 --- a/src/tictactoe.py +++ /dev/null @@ -1,427 +0,0 @@ -from common import stanza_session -from common import xmpp - -import pygtk -pygtk.require('2.0') -import gtk -from gtk import gdk -import cairo - -# implements - -games_ns = 'http://jabber.org/protocol/games' - -class InvalidMove(Exception): - pass - -class TicTacToeSession(stanza_session.StanzaSession): - # initiate a session - def begin(self, rows = 3, cols = 3, role_s = 'x'): - self.rows = rows - self.cols = cols - - self.role_s = role_s - - self.strike = 3 - - if self.role_s == 'x': - self.role_o = 'o' - else: - self.role_o = 'x' - - self.send_invitation() - - self.next_move_id = 1 - self.received = self.wait_for_invite_response - - def send_invitation(self): - msg = xmpp.Message() - - invite = msg.NT.invite - invite.setNamespace(games_ns) - - game = invite.NT.game - game.setAttr('var', games_ns + '/tictactoe') - - x = xmpp.DataForm(typ='submit') - - game.addChild(node=x) - - self.send(msg) - - def read_invitation(self, msg): - invite = msg.getTag('invite', namespace=games_ns) - game = invite.getTag('game') - x = game.getTag('x', namespace='jabber:x:data') - - form = xmpp.DataForm(node=x) - - if form.getField('role'): - self.role_o = form.getField('role').getValues()[0] - else: - self.role_o = 'x' - - if form.getField('rows'): - self.rows = int(form.getField('rows').getValues()[0]) - else: - self.rows = 3 - - if form.getField('cols'): - self.cols = int(form.getField('cols').getValues()[0]) - else: - self.cols = 3 - - # number in a row needed to win - if form.getField('strike'): - self.strike = int(form.getField('strike').getValues()[0]) - else: - self.strike = 3 - - # received an invitation - def invited(self, msg): - self.read_invitation(msg) - - # XXX prompt user - # "accept, reject, ignore" - - # the number of the move about to be made - self.next_move_id = 1 - - # display the board - self.board = TicTacToeBoard(self, self.rows, self.cols) - - # accept the invitation, join the game - response = xmpp.Message() - - join = response.NT.join - join.setNamespace(games_ns) - - self.send(response) - - if self.role_o == 'x': - self.role_s = 'o' - - self.their_turn() - else: - self.role_s = 'x' - self.role_o = 'o' - - self.our_turn() - - # just sent an invitation, expecting a reply - def wait_for_invite_response(self, msg): - if msg.getTag('join', namespace=games_ns): - self.board = TicTacToeBoard(self, self.rows, self.cols) - - if self.role_s == 'x': - self.our_turn() - else: - self.their_turn() - - elif msg.getTag('decline', namespace=games_ns): - # XXX notify the user - - # XXX end session - pass - - # silently ignores any received messages - def ignore(self, msg): - pass - - def game_over(self, msg): - invite = msg.getTag('invite', namespace=games_ns) - - # ignore messages unless they're renewing the game - if invite and invite.getAttr('type') == 'renew': - self.invited(msg) - - def wait_for_move(self, msg): - turn = msg.getTag('turn', namespace=games_ns) - move = turn.getTag('move', namespace='http://jabber.org/protocol/games/tictactoe') - - row = int(move.getAttr('row')) - col = int(move.getAttr('col')) - id = int(move.getAttr('id')) - - if id != self.next_move_id: - print 'unexpected move id, lost a move somewhere?' - return - - try: - self.board.mark(row, col, self.role_o) - except InvalidMove, e: - # received an invalid move, end the game. - - # XXX notify the user - self.end_game('cheating') - return - - # check win conditions - if self.board.check_for_strike(self.role_o, row, col, self.strike): - self.lost() - elif self.board.full(): - self.drawn() - else: - self.next_move_id += 1 - - self.our_turn() - - def is_my_turn(self): - # XXX not great semantics - return self.received == self.ignore - - def our_turn(self): - # ignore messages until we've made our move - self.received = self.ignore - self.board.set_title('your turn') - - def their_turn(self): - self.received = self.wait_for_move - self.board.set_title('their turn') - - # called when the board receives input - def move(self, row, col): - try: - self.board.mark(row, col, self.role_s) - except InvalidMove, e: - print 'you made an invalid move' - return - - self.send_move(row, col) - - # check win conditions - if self.board.check_for_strike(self.role_s, row, col, self.strike): - self.won() - elif self.board.full(): - self.drawn() - else: - self.next_move_id += 1 - - self.their_turn() - - # sends a move message - def send_move(self, row, column): - msg = xmpp.Message() - msg.setType('chat') - - turn = msg.NT.turn - turn.setNamespace(games_ns) - - move = turn.NT.move - move.setNamespace(games_ns+'/tictactoe') - - move.setAttr('row', str(row)) - move.setAttr('col', str(column)) - move.setAttr('id', str(self.next_move_id)) - - self.send(msg) - - # sends a termination message and ends the game - def end_game(self, reason): - msg = xmpp.Message() - - terminate = msg.NT.terminate - terminate.setNamespace(games_ns) - terminate.setAttr('reason', reason) - - self.send(msg) - - self.received = self.game_over - - def won(self): - self.end_game('won') - self.board.won() - - def lost(self): - self.end_game('lost') - self.board.lost() - - def drawn(self): - self.end_game('draw') - self.board.drawn() - -class TicTacToeBoard: - def __init__(self, session, rows, cols): - self.session = session - - self.state = 'None' - - self.rows = rows - self.cols = cols - - self.board = [ [None] * self.cols for r in xrange(self.rows) ] - - self.setup_window() - - # check if the last move (at row r and column c) won the game - def check_for_strike(self, p, r, c, strike): - # number in a row: up and down, left and right - tallyI = 0 - tally_ = 0 - - # number in a row: diagonal - # (imagine L or F as two sides of a right triangle: L\ or F/) - tallyL = 0 - tallyF = 0 - - # convert real columns to internal columns - r -= 1 - c -= 1 - - for d in xrange(-strike, strike): - r_in_range = 0 <= r+d < self.rows - c_in_range = 0 <= c+d < self.cols - - # vertical check - if r_in_range: - tallyI = tallyI + 1 - if self.board[r+d][c] != p: - tallyI = 0 - - # horizontal check - if c_in_range: - tally_ = tally_ + 1 - if self.board[r][c+d] != p: - tally_ = 0 - - # diagonal checks - if r_in_range and c_in_range: - tallyL = tallyL + 1 - if self.board[r+d][c+d] != p: - tallyL = 0 - - if r_in_range and 0 <= c-d < self.cols: - tallyF = tallyF + 1 - if self.board[r+d][c-d] != p: - tallyF = 0 - - if any([t == strike for t in (tallyL, tallyF, tallyI, tally_)]): - return True - - return False - - # is the board full? - def full(self): - for r in xrange(self.rows): - for c in xrange(self.cols): - if self.board[r][c] == None: - return False - - return True - - def setup_window(self): - self.win = gtk.Window() - - self.title_prefix = 'tic-tac-toe with %s' % self.session.jid - self.set_title() - - self.win.set_app_paintable(True) - - self.win.add_events(gdk.BUTTON_PRESS_MASK) - self.win.connect('button-press-event', self.clicked) - self.win.connect('expose-event', self.expose) - - self.win.show_all() - - def clicked(self, widget, event): - if not self.session.is_my_turn(): - return - - (width, height) = widget.get_size() - - # convert click co-ordinates to row and column - - row_height = height // self.rows - col_width = width // self.cols - - row = int(event.y // row_height) + 1 - column = int(event.x // col_width) + 1 - - self.session.move(row, column) - - # this actually draws the board - def expose(self, widget, event): - win = widget.window - - cr = win.cairo_create() - - cr.set_source_rgb(1.0, 1.0, 1.0) - - cr.set_operator(cairo.OPERATOR_SOURCE) - cr.paint() - - (width, height) = widget.get_size() - - row_height = height // self.rows - col_width = width // self.cols - - for i in xrange(self.rows): - for j in xrange(self.cols): - if self.board[i][j] == 'x': - self.draw_x(cr, i, j, row_height, col_width) - elif self.board[i][j] == 'o': - self.draw_o(cr, i, j, row_height, col_width) - - # XXX draw 'won', 'lost', 'draw' - - def draw_x(self, cr, row, col, row_height, col_width): - cr.set_source_rgb(0, 0, 0) - - top = row_height * (row + 0.2) - bottom = row_height * (row + 0.8) - - left = col_width * (col + 0.2) - right = col_width * (col + 0.8) - - cr.set_line_width(row_height / 5) - - cr.move_to(left, top) - cr.line_to(right, bottom) - - cr.move_to(right, top) - cr.line_to(left, bottom) - - cr.stroke() - - def draw_o(self, cr, row, col, row_height, col_width): - cr.set_source_rgb(0, 0, 0) - - x = col_width * (col + 0.5) - y = row_height * (row + 0.5) - - cr.arc(x, y, row_height/4, 0, 2.0*3.2) # slightly further than 2*pi - - cr.set_line_width(row_height / 5) - cr.stroke() - - # mark a move on the board - def mark(self, row, column, player): - if self.board[row-1][column-1]: - raise InvalidMove - else: - self.board[row-1][column-1] = player - - self.win.queue_draw() - - def set_title(self, suffix = None): - str = self.title_prefix - - if suffix: - str += ': ' + suffix - - self.win.set_title(str) - - def won(self): - self.state = 'won' - self.set_title('you won!') - self.win.queue_draw() - - def lost(self): - self.state = 'lost' - self.set_title('you lost.') - self.win.queue_draw() - - def drawn(self): - self.state = 'drawn' - self.win.set_title(self.title_prefix + ': a draw.') - self.win.queue_draw() diff --git a/test/mocks.py b/test/mocks.py new file mode 100644 index 000000000..213f79ddf --- /dev/null +++ b/test/mocks.py @@ -0,0 +1,68 @@ +# gajim-specific mock objects +from mock import Mock + +from common import gajim + +class MockConnection(Mock): + def __init__(self, name, *args): + Mock.__init__(self, *args) + self.name = name + gajim.connections[name] = self + +class MockWindow(Mock): + def __init__(self, *args): + Mock.__init__(self, *args) + self.window = Mock() + +class MockChatControl(Mock): + def __init__(self, *args): + Mock.__init__(self, *args) + + self.parent_win = MockWindow({'get_active_control': self}) + self.session = None + + def set_session(self, sess): + self.session = sess + + def __nonzero__(self): + return True + + def __eq__(self, other): + return self is other + +class MockInterface(Mock): + def __init__(self, acct, *args): + Mock.__init__(self, *args) + self.msg_win_mgr = Mock() + self.roster = Mock() + + self.remote_ctrl = None + self.minimized_controls = { acct: {} } + +class MockLogger(Mock): + def __init__(self): + Mock.__init__(self, {'write': None}) + +class MockContact(Mock): + def __nonzero__(self): + return True + +import random + +class MockSession(Mock): + def __init__(self, conn, jid, thread_id, type): + Mock.__init__(self) + + self.conn = conn + self.jid = jid + self.type = type + self.thread_id = thread_id + + if not self.thread_id: + self.thread_id = '%0x' % random.randint(0, 10000) + + def __repr__(self): + print '' % self.thread_id + + def __nonzero__(self): + return True diff --git a/test/test_sessions.py b/test/test_sessions.py index e4276ff56..e47d3759a 100644 --- a/test/test_sessions.py +++ b/test/test_sessions.py @@ -10,10 +10,6 @@ sys.path.append(gajim_root + '/src') # a temporary version of ~/.gajim for testing configdir = gajim_root + '/test/tmp' -import time - -from mock import Mock - # define _ for i18n import __builtin__ __builtin__._ = lambda x: x @@ -30,10 +26,15 @@ import common.configpaths common.configpaths.gajimpaths.init(configdir) common.configpaths.gajimpaths.init_profile() +import time + # for some reason common.gajim needs to be imported before xmpppy? from common import gajim from common import xmpp +from mock import Mock, expectParams +from mocks import * + gajim.DATA_DIR = gajim_root + '/data' from common.stanza_session import StanzaSession @@ -41,16 +42,10 @@ from common.stanza_session import StanzaSession # name to use for the test account account_name = 'test' -class MockConnection(Mock): - def __init__(self, *args): - Mock.__init__(self, *args) - self.name = account_name - gajim.connections[self.name] = self - class TestStanzaSession(unittest.TestCase): def setUp(self): self.jid = 'test@example.org/Gajim' - self.conn = MockConnection({'send_stanza': None}) + self.conn = MockConnection(account_name, {'send_stanza': None}) self.sess = StanzaSession(self.conn, self.jid, None, 'chat') def test_generate_thread_id(self): @@ -77,7 +72,7 @@ class TestStanzaSession(unittest.TestCase): self.assertEqual(msg.getThread(), self.sess.thread_id) - def test_terminate_without_sendng(self): + def test_terminate_without_sending(self): # no termination is sent if no messages have been sent in the session self.sess.terminate() @@ -100,45 +95,7 @@ class TestStanzaSession(unittest.TestCase): from session import ChatControlSession -class MockWindow(Mock): - def __init__(self, *args): - Mock.__init__(self, *args) - self.window = Mock() - -class MockChatControl(Mock): - def __init__(self, *args): - Mock.__init__(self, *args) - - self.parent_win = MockWindow({'get_active_control': self}) - self.session = None - - def set_session(self, sess): - self.session = sess - - def __nonzero__(self): - return True - - def __eq__(self, other): - return self is other - -class MockInterface(Mock): - def __init__(self, *args): - Mock.__init__(self, *args) - self.msg_win_mgr = Mock() - self.roster = Mock() - - self.remote_ctrl = None - self.minimized_controls = { account_name: {} } - -class MockLogger(Mock): - def __init__(self): - Mock.__init__(self, {'write': None}) - -class MockContact(Mock): - def __nonzero__(self): - return True - -gajim.interface = MockInterface() +gajim.interface = MockInterface(account_name) gajim.contacts.add_account(account_name) import notify @@ -146,7 +103,7 @@ import notify class TestChatControlSession(unittest.TestCase): def setUp(self): self.jid = 'test@example.org/Gajim' - self.conn = MockConnection({'send_stanza': None}) + self.conn = MockConnection(account_name, {'send_stanza': None}) self.sess = ChatControlSession(self.conn, self.jid, None) gajim.logger = MockLogger() @@ -217,6 +174,7 @@ class TestChatControlSession(unittest.TestCase): def test_receive_already_has_control(self): '''test receiving a message with a session already attached to a control''' + jid = 'bct@necronomicorp.com/Gajim' msgtxt = 'testing one two three' @@ -240,13 +198,16 @@ class TestChatControlSession(unittest.TestCase): def test_received_orphaned_control(self): '''test receiving a message when a control that doesn't have a session attached exists''' - jid = 'bct@necronomicorp.com/Gajim' + jid = 'bct@necronomicorp.com' + fjid = jid + '/Gajim' msgtxt = 'testing one two three' ctrl = MockChatControl() gajim.interface.msg_win_mgr = Mock({'get_sessionless_ctrl': ctrl}) + gajim.interface.msg_win_mgr.mockSetExpectation('get_sessionless_ctrl', + expectParams(account_name, jid)) - self.receive_chat_msg(jid, msgtxt) + self.receive_chat_msg(fjid, msgtxt) # message was logged calls = gajim.logger.mockGetNamedCalls('write') From 16e274b9ecbbaa638124759e4f79ca03edfe3c56 Mon Sep 17 00:00:00 2001 From: tomk Date: Thu, 12 Jun 2008 23:54:46 +0000 Subject: [PATCH 03/20] added test script for NonBlockingClient (test/test_client_nb.py) --- src/common/xmpp/transports_nb.py | 12 +- test/test_client_nb.py | 280 +++++++++++++++++++++++++++++++ 2 files changed, 286 insertions(+), 6 deletions(-) create mode 100644 test/test_client_nb.py diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py index 0ae25c24e..4c056c09d 100644 --- a/src/common/xmpp/transports_nb.py +++ b/src/common/xmpp/transports_nb.py @@ -40,7 +40,7 @@ log = logging.getLogger('gajim.c.x.transports_nb') # %s/\'\.\.\/data\'/common\.gajim\.DATA_DIR/c # %s/\'%s\/\.gajim\/cacerts\.pem\'\ %\ os\.environ\[\'HOME\'\]/common\.gajim\.MY_CACERTS/c -import common.gajim +# import common.gajim DATA_RECEIVED='DATA RECEIVED' @@ -773,16 +773,16 @@ class NonBlockingTLS(PlugIn): #tcpsock._sslContext = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD) tcpsock.ssl_errnum = 0 tcpsock._sslContext.set_verify(OpenSSL.SSL.VERIFY_PEER, self._ssl_verify_callback) - cacerts = os.path.join(common.gajim.DATA_DIR, 'other', 'cacerts.pem') + cacerts = os.path.join('../data', 'other', 'cacerts.pem') try: tcpsock._sslContext.load_verify_locations(cacerts) except: log.warning('Unable to load SSL certificats from file %s' % \ os.path.abspath(cacerts)) # load users certs - if os.path.isfile(common.gajim.MY_CACERTS): + if os.path.isfile('%s/.gajim/cacerts.pem' % os.environ['HOME']): store = tcpsock._sslContext.get_cert_store() - f = open(common.gajim.MY_CACERTS) + f = open('%s/.gajim/cacerts.pem' % os.environ['HOME']) lines = f.readlines() i = 0 begin = -1 @@ -797,11 +797,11 @@ class NonBlockingTLS(PlugIn): store.add_cert(X509cert) except OpenSSL.crypto.Error, exception_obj: log.warning('Unable to load a certificate from file %s: %s' %\ - (common.gajim.MY_CACERTS, exception_obj.args[0][0][2])) + ('%s/.gajim/cacerts.pem' % os.environ['HOME'], exception_obj.args[0][0][2])) except: log.warning( 'Unknown error while loading certificate from file %s' % \ - common.gajim.MY_CACERTS) + '%s/.gajim/cacerts.pem' % os.environ['HOME']) begin = -1 i += 1 tcpsock._sslObj = OpenSSL.SSL.Connection(tcpsock._sslContext, tcpsock._sock) diff --git a/test/test_client_nb.py b/test/test_client_nb.py new file mode 100644 index 000000000..768c19096 --- /dev/null +++ b/test/test_client_nb.py @@ -0,0 +1,280 @@ +import unittest, threading +from mock import Mock + +import sys, time, os.path + +gajim_root = os.path.join(os.path.abspath(os.path.dirname(__file__)), '..') + +sys.path.append(gajim_root + '/src/common/xmpp') + +import client_nb, idlequeue + +''' +Testing script for NonBlockingClient class (src/common/xmpp/client_nb.py) +It actually connects to a xmpp server so the connection values have to be +changed before running. +''' + +idlequeue_interval = 0.2 +''' +IdleQueue polling interval. 200ms is used in Gajim as default +''' + +xmpp_server_port = ('xmpp.example.org',5222) +''' +2-tuple - (XMPP server hostname, c2s port) +Script will connect to the machine. +''' + +credentials = ['primus', 'l0v3', 'testclient'] +''' +[username, password, passphrase] +Script will autheticate itself with this credentials on above mentioned server. +''' + + +class MockConnectionClass(Mock): + ''' + Class simulating Connection class from src/common/connection.py + + It is derived from Mock in order to avoid defining all methods + from real Connection that are called from NBClient or Dispatcher + ( _event_dispatcher for example) + ''' + + def __init__(self, *args): + self.event = threading.Event() + ''' + is used for waiting on connect, auth and disconnect callbacks + ''' + + self.event.clear() + Mock.__init__(self, *args) + + def on_connect(self, *args): + ''' + Method called on succesful connecting - after receiving + from server (NOT after TLS stream restart). + ''' + + #print 'on_connect - args:' + #for i in args: + # print ' %s' % i + self.connect_failed = False + self.event.set() + + def on_connect_failure(self, *args): + ''' + Method called on failure while connecting - on everything from TCP error + to error during TLS handshake + ''' + + #print 'on_connect failure - args:' + #for i in args: + # print ' %s' % i + self.connect_failed = True + self.event.set() + + def on_auth(self, con, auth): + ''' + Method called after authentication is done regardless on the result. + + :Parameters: + con : NonBlockingClient + reference to authenticated object + auth : string + type of authetication in case of success ('old_auth', 'sasl') or + None in case of auth failure + ''' + + #print 'on_auth - args:' + #print ' con: %s' % con + #print ' auth: %s' % auth + self.auth_connection = con + self.auth = auth + self.event.set() + + def wait(self): + ''' + Waiting until some callback sets the event and clearing the event subsequently. + ''' + + self.event.wait() + self.event.clear() + + + + +class IdleQueueThread(threading.Thread): + ''' + Thread for regular processing of idlequeue. + ''' + def __init__(self): + self.iq = idlequeue.IdleQueue() + self.stop = threading.Event() + ''' + Event used to stopping the thread main loop. + ''' + + self.stop.clear() + threading.Thread.__init__(self) + + def run(self): + while not self.stop.isSet(): + self.iq.process() + time.sleep(idlequeue_interval) + self.iq.process() + + def stop_thread(self): + self.stop.set() + + + + +class TestNonBlockingClient(unittest.TestCase): + ''' + Test Cases class for NonBlockingClient. + ''' + + def setUp(self): + ''' + IdleQueue thread is run and dummy connection is created. + ''' + + self.idlequeue_thread = IdleQueueThread() + self.connection = MockConnectionClass() + + self.client = client_nb.NonBlockingClient( + server=xmpp_server_port[0], + port=xmpp_server_port[1], + on_connect=self.connection.on_connect, + on_connect_failure=self.connection.on_connect_failure, + caller=self.connection + ) + ''' + NonBlockingClient instance with parameters from global variables and with + callbacks from dummy connection. + ''' + + self.client.set_idlequeue(self.idlequeue_thread.iq) + self.idlequeue_thread.start() + + def tearDown(self): + ''' + IdleQueue thread is stopped. + ''' + self.idlequeue_thread.stop_thread() + self.idlequeue_thread.join() + + def open_stream(self, server_port): + ''' + Method opening the XMPP connection. It returns when + is received from server. + + :param server_port: tuple of (hostname, port) for where the client should + connect. + ''' + self.client.connect(server_port) + + print 'waiting for callback from client constructor' + self.connection.wait() + + # if on_connect was called, client has to be connected and vice versa + if self.connection.connect_failed: + self.assert_(not self.client.isConnected()) + else: + self.assert_(self.client.isConnected()) + + def client_auth(self, username, password, resource, sasl): + ''' + Method authenticating connected client with supplied credentials. Returns + when authentication is over. + :param sasl: whether to use sasl (sasl=1) or old (sasl=0) authentication + + :todo: to check and be more specific about when it returns (bind, session..) + ''' + self.client.auth(username, password, resource, sasl, + on_auth=self.connection.on_auth) + + print 'waiting for authentication...' + self.connection.wait() + + def do_disconnect(self): + ''' + Does disconnecting of connected client. Returns when TCP connection is closed. + ''' + self.client.start_disconnect(None, on_disconnect=self.connection.event.set) + + print 'waiting for disconnecting...' + self.connection.wait() + + def test_proper_connect_sasl(self): + ''' + The ideal testcase - client is connected, authenticated with SASL and + then disconnected. + ''' + self.open_stream(xmpp_server_port) + + # if client is not connected, lets raise the AssertionError + self.assert_(self.client.isConnected()) + # (client.disconnect() is already called from NBClient._on_connected_failure + # so there's need to call it in this case + + self.client_auth(credentials[0], credentials[1], credentials[2], sasl=1) + self.assert_(self.connection.con) + self.assert_(self.connection.auth=='sasl') + + self.do_disconnect() + + + def test_proper_connect_oldauth(self): + ''' + The ideal testcase - client is connected, authenticated with old auth and + then disconnected. + ''' + self.open_stream(xmpp_server_port) + self.assert_(self.client.isConnected()) + self.client_auth(credentials[0], credentials[1], credentials[2], sasl=0) + self.assert_(self.connection.con) + self.assert_(self.connection.auth=='old_auth') + self.do_disconnect() + + def test_connect_to_nonexisting_host(self): + ''' + Connect to nonexisting host. DNS request for A records should return nothing. + ''' + self.open_stream(('fdsfsdf.fdsf.fss', 5222)) + self.assert_(not self.client.isConnected()) + + def test_connect_to_wrong_port(self): + ''' + Connect to nonexisting host. DNS request for A records should return some IP + but there shouldn't be XMPP server running on specified port. + ''' + self.open_stream((xmpp_server_port[0], 31337)) + self.assert_(not self.client.isConnected()) + + def test_connect_with_wrong_creds(self): + ''' + Connecting with invalid password. + ''' + self.open_stream(xmpp_server_port) + self.assert_(self.client.isConnected()) + self.client_auth(credentials[0], "wrong pass", credentials[2], sasl=0) + self.assert_(self.connection.auth is None) + self.do_disconnect() + + + + + + +if __name__ == '__main__': + + suite = unittest.TestLoader().loadTestsFromTestCase(TestNonBlockingClient) + unittest.TextTestRunner(verbosity=2).run(suite) + + + + + From 65644ca13fb329de11f0cd2217e9734c8b5951d5 Mon Sep 17 00:00:00 2001 From: tomk Date: Wed, 18 Jun 2008 23:58:19 +0000 Subject: [PATCH 04/20] added stub for new transports module plus basic test for it, testing code reorganized --- src/common/xmpp/debug.py | 2 +- src/common/xmpp/idlequeue.py | 14 +- src/common/xmpp/transports_nb.py | 6 + src/common/xmpp/transports_new.py | 270 ++++++++++++++++++++++++++++++ test/test_client_nb.py | 132 ++------------- test/test_nonblockingtcp.py | 119 +++++++++++++ test/xmpp_mocks.py | 120 +++++++++++++ 7 files changed, 540 insertions(+), 123 deletions(-) create mode 100644 src/common/xmpp/transports_new.py create mode 100644 test/test_nonblockingtcp.py create mode 100644 test/xmpp_mocks.py diff --git a/src/common/xmpp/debug.py b/src/common/xmpp/debug.py index 29321c1ff..60480b66c 100644 --- a/src/common/xmpp/debug.py +++ b/src/common/xmpp/debug.py @@ -154,7 +154,7 @@ class Debug: # If you dont want to validate flags on each call to # show(), set this to 0 # - validate_flags = 1, + validate_flags = 0, # # If you dont want the welcome message, set to 0 # default is to show welcome if any flags are active diff --git a/src/common/xmpp/idlequeue.py b/src/common/xmpp/idlequeue.py index 5ee73c987..955af1be1 100644 --- a/src/common/xmpp/idlequeue.py +++ b/src/common/xmpp/idlequeue.py @@ -53,6 +53,7 @@ class IdleQueue: self.selector = select.poll() def remove_timeout(self, fd): + print 'read timeout removed for fd %s' % fd if self.read_timeouts.has_key(fd): del(self.read_timeouts[fd]) @@ -68,6 +69,7 @@ class IdleQueue: def set_read_timeout(self, fd, seconds): ''' set a new timeout, if it is not removed after 'seconds', then obj.read_timeout() will be called ''' + print 'read timeout set for fd %s on %s seconds' % (fd, seconds) timeout = self.current_time() + seconds self.read_timeouts[fd] = timeout @@ -204,14 +206,14 @@ class SelectIdleQueue(IdleQueue): except select.error, e: waiting_descriptors = ((),(),()) if e[0] != 4: # interrupt - raise - for fd in waiting_descriptors[0]: - q = self.queue.get(fd) - if q: + raise + for fd in waiting_descriptors[0]: + q = self.queue.get(fd) + if q: q.pollin() for fd in waiting_descriptors[1]: - q = self.queue.get(fd) - if q: + q = self.queue.get(fd) + if q: q.pollout() for fd in waiting_descriptors[2]: self.queue.get(fd).pollend() diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py index 4c056c09d..a4f847764 100644 --- a/src/common/xmpp/transports_nb.py +++ b/src/common/xmpp/transports_nb.py @@ -337,7 +337,9 @@ class NonBlockingTcp(PlugIn, IdleObject): self.on_connect_failure() def _plug_idle(self): + # readable if socket is connected or disconnecting readable = self.state != 0 + # writeable if sth to send if self.sendqueue or self.sendbuff: writable = True else: @@ -346,6 +348,7 @@ class NonBlockingTcp(PlugIn, IdleObject): self.idlequeue.plug_idle(self, writable, readable) def pollout(self): + print 'pollout called - send possible' if self.state == 0: self.connect_to_next_ip() return @@ -359,6 +362,7 @@ class NonBlockingTcp(PlugIn, IdleObject): self._owner = None def pollin(self): + print 'pollin called - receive possible' self._do_receive() def pollend(self, retry=False): @@ -583,11 +587,13 @@ class NonBlockingTcp(PlugIn, IdleObject): errnum = 0 try: + print "==============sock.connect called" self._sock.connect(self._server) self._sock.setblocking(False) except Exception, ee: (errnum, errstr) = ee # in progress, or would block + print "errnum: %s" % errnum if errnum in (errno.EINPROGRESS, errno.EALREADY, errno.EWOULDBLOCK): self.state = 1 return diff --git a/src/common/xmpp/transports_new.py b/src/common/xmpp/transports_new.py new file mode 100644 index 000000000..36992ba95 --- /dev/null +++ b/src/common/xmpp/transports_new.py @@ -0,0 +1,270 @@ +from idlequeue import IdleObject +from client import PlugIn +import threading, socket, errno + +import logging +log = logging.getLogger('gajim.c.x.transports_nb') +consoleloghandler = logging.StreamHandler() +consoleloghandler.setLevel(logging.DEBUG) +consoleloghandler.setFormatter( + logging.Formatter('%(levelname)s: %(message)s') +) +log.setLevel(logging.DEBUG) +log.addHandler(consoleloghandler) +log.propagate = False + +''' +this module will replace transports_nb.py +For now, it can be run from test/test_nonblockingtcp.py +* set credentials in the testing script +''' + + +class NBgetaddrinfo(threading.Thread): + ''' + Class for nonblocking call of getaddrinfo. Maybe unnecessary. + ''' + def __init__(self, server, on_success, on_failure, timeout_sec): + ''' + Call is started from constructor. It is not needed to hold reference on + created instance. + :param server: tuple (hostname, port) for DNS request + :param on_success: callback for successful DNS request + :param on_failure: called when DNS request couldn't be performed + :param timeout_sec: max seconds to wait for return from getaddrinfo. After + this time, on_failure is called with error message. + ''' + threading.Thread.__init__(self) + self.on_success = on_success + self.on_failure = on_failure + self.server = server + self.lock = threading.Lock() + self.already_called = False + self.timer = threading.Timer(timeout_sec, self.on_timeout) + self.timer.start() + self.start() + + def on_timeout(self): + ''' + Called by timer. Means that getaddrinfo takes too long and will be + interrupted. + ''' + self.do_call(False, 'NBgetaddrinfo timeout while looking up %s:%s' % self.server) + + def do_call(self, success, data): + ''' + Method called either on success and failure. In case of timeout it will be + called twice but only the first (failure) call will be performed. + :param success: True if getaddrinfo returned properly, False if there was an + error or on timeout. + :param data: error message if failure, list of address structures if success + ''' + log.debug('NBgetaddrinfo::do_call(): %s' % repr(data)) + self.timer.cancel() + self.lock.acquire() + if not self.already_called: + self.already_called = True + self.lock.release() + if success: + self.on_success(data) + else: + self.on_failure(data) + return + else: + self.lock.release() + return + + def run(self): + try: + ips = socket.getaddrinfo(self.server[0],self.server[1],socket.AF_UNSPEC, + socket.SOCK_STREAM) + except socket.gaierror, e: + self.do_call(False, 'Lookup failure for %s: %s %s' % + (repr(self.server), e[0], e[1])) + except Exception, e: + self.do_call(False, 'Exception while DNS lookup of %s: %s' % + (repr(e), repr(self.server))) + else: + self.do_call(True, ips) + + + +DISCONNECTED ='DISCONNECTED' +CONNECTING ='CONNECTING' +CONNECTED ='CONNECTED' +DISCONNECTING ='DISCONNECTING' + +CONNECT_TIMEOUT_SECONDS = 5 +'''timeout to connect to the server socket, it doesn't include auth''' + +DISCONNECT_TIMEOUT_SECONDS = 10 +'''how long to wait for a disconnect to complete''' + +class NonBlockingTcp(PlugIn, IdleObject): + def __init__(self, on_xmpp_connect=None, on_xmpp_failure=None): + ''' + Class constructor. All parameters can be reset in tcp_connect or xmpp_connect + calls. + + ''' + PlugIn.__init__(self) + IdleObject.__init__(self) + self.on_tcp_connect = None + self.on_tcp_failure = None + self.sock = None + self.idlequeue = None + self.DBG_LINE='socket' + self.state = DISCONNECTED + ''' + CONNECTING - after non-blocking socket.connect() until TCP connection is estabilished + CONNECTED - after TCP connection is estabilished + DISCONNECTING - + DISCONNECTED + ''' + self._exported_methods=[self.send, self.disconnect, self.onreceive, self.set_send_timeout, + self.start_disconnect, self.set_timeout, self.remove_timeout] + + + def connect(self, conn_5tuple, on_tcp_connect, on_tcp_failure, idlequeue): + ''' + Creates and connects socket to server and port defined in conn_5tupe which + should be list item returned from getaddrinfo. + :param conn_5tuple: 5-tuple returned from getaddrinfo + :param on_tcp_connect: callback called on successful tcp connection + :param on_tcp_failure: callback called on failure when estabilishing tcp + connection + :param idlequeue: idlequeue for socket + ''' + self.on_tcp_connect = on_tcp_connect + self.on_tcp_failure = on_tcp_failure + self.conn_5tuple = conn_5tuple + try: + self.sock = socket.socket(*conn_5tuple[:3]) + except socket.error, (errnum, errstr): + on_tcp_failure('NonBlockingTcp: Error while creating socket: %s %s' % (errnum, errstr)) + return + + self.idlequeue = idlequeue + self.fd = self.sock.fileno() + self.idlequeue.plug_idle(self, True, False) + + errnum = 0 + ''' variable for errno symbol that will be found from exception raised from connect() ''' + + # set timeout for TCP connecting - if nonblocking connect() fails, pollend + # is called. If if succeeds pollout is called. + self.idlequeue.set_read_timeout(self.fd, CONNECT_TIMEOUT_SECONDS) + + try: + self.sock.setblocking(False) + self.sock.connect(conn_5tuple[4]) + except Exception, (errnum, errstr): + pass + + if errnum in (errno.EINPROGRESS, errno.EALREADY, errno.EWOULDBLOCK): + # connecting in progress + self.state = CONNECTING + log.debug('After nonblocking connect. "%s" raised => CONNECTING' % errstr) + # on_tcp_connect/failure will be called from self.pollin/self.pollout + return + elif errnum in (0, 10056, errno.EISCONN): + # already connected - this branch is very unlikely, nonblocking connect() will + # return EINPROGRESS exception in most cases. Anyway, we don't need timeout + # on connected descriptor + log.debug('After nonblocking connect. "%s" raised => CONNECTED' % errstr) + self._on_tcp_connect(self) + return + + # if there was some other error, call failure callback and unplug transport + # which will also remove read_timeouts for descriptor + self._on_tcp_failure('Exception while connecting to %s: %s - %s' % + (conn_5tuple[4], errnum, errstr)) + + def _on_tcp_connect(self, data): + ''' This method preceeds actual call of on_tcp_connect callback + ''' + self.state = CONNECTED + self.idlequeue.remove_timeout(self.fd) + self.on_tcp_connect(data) + + + def _on_tcp_failure(self,err_msg): + ''' This method preceeds actual call of on_tcp_failure callback + ''' + self.state = DISCONNECTED + self.idlequeue.unplug_idle(self.fd) + self.on_tcp_failure(err_msg) + + def pollin(self): + '''called when receive on plugged socket is possible ''' + log.debug('pollin called, state == %s' % self.state) + + def pollout(self): + '''called when send to plugged socket is possible''' + log.debug('pollout called, state == %s' % self.state) + + if self.state==CONNECTING: + self._on_tcp_connect(self) + return + + def pollend(self): + '''called when remote site closed connection''' + log.debug('pollend called, state == %s' % self.state) + if self.state==CONNECTING: + self._on_tcp_failure('Error during connect to %s:%s' % self.conn_5tuple[4]) + + def read_timeout(self): + ''' + Implemntation of IdleObject function called on timeouts from IdleQueue. + ''' + log.debug('read_timeout called, state == %s' % self.state) + if self.state==CONNECTING: + # if read_timeout is called during connecting, connect() didn't end yet + # thus we have to close the socket + try: + self.sock.close() + except socket.error, (errnum, errmsg): + log.error('Error while closing socket on connection timeout: %s %s' + % (errnum, errmsg)) + self._on_tcp_failure('Error during connect to %s:%s' % self.conn_5tuple[4]) + + + + def disconnect(self, on_disconnect=None): + if self.state == DISCONNECTED: + return + self.idlequeue.unplug_idle(self.fd) + try: + self.sock.shutdown(socket.SHUT_RDWR) + except socket.error, (errnum, errstr): + log.error('Error while disconnecting: %s %s' % (errnum,errstr)) + + try: + self.sock.close() + except socket.error, (errnum, errmsg): + log.error('Error closing socket: %s %s' % (errnum,errstr)) + if on_disconnect: + on_disconnect() + + + + + + + def send(self, data, now=False): + pass + + def onreceive(self): + pass + + def set_send_timeout(self): + pass + + def set_timeout(self): + pass + + def remove_timeout(self): + pass + + def start_disconnect(self): + pass diff --git a/test/test_client_nb.py b/test/test_client_nb.py index 768c19096..3ddc75eef 100644 --- a/test/test_client_nb.py +++ b/test/test_client_nb.py @@ -1,13 +1,13 @@ -import unittest, threading -from mock import Mock +import unittest +from xmpp_mocks import * -import sys, time, os.path +import sys, os.path gajim_root = os.path.join(os.path.abspath(os.path.dirname(__file__)), '..') sys.path.append(gajim_root + '/src/common/xmpp') -import client_nb, idlequeue +import client_nb ''' Testing script for NonBlockingClient class (src/common/xmpp/client_nb.py) @@ -15,10 +15,6 @@ It actually connects to a xmpp server so the connection values have to be changed before running. ''' -idlequeue_interval = 0.2 -''' -IdleQueue polling interval. 200ms is used in Gajim as default -''' xmpp_server_port = ('xmpp.example.org',5222) ''' @@ -26,111 +22,12 @@ xmpp_server_port = ('xmpp.example.org',5222) Script will connect to the machine. ''' -credentials = ['primus', 'l0v3', 'testclient'] +credentials = ['login', 'pass', 'testclient'] ''' [username, password, passphrase] Script will autheticate itself with this credentials on above mentioned server. ''' - -class MockConnectionClass(Mock): - ''' - Class simulating Connection class from src/common/connection.py - - It is derived from Mock in order to avoid defining all methods - from real Connection that are called from NBClient or Dispatcher - ( _event_dispatcher for example) - ''' - - def __init__(self, *args): - self.event = threading.Event() - ''' - is used for waiting on connect, auth and disconnect callbacks - ''' - - self.event.clear() - Mock.__init__(self, *args) - - def on_connect(self, *args): - ''' - Method called on succesful connecting - after receiving - from server (NOT after TLS stream restart). - ''' - - #print 'on_connect - args:' - #for i in args: - # print ' %s' % i - self.connect_failed = False - self.event.set() - - def on_connect_failure(self, *args): - ''' - Method called on failure while connecting - on everything from TCP error - to error during TLS handshake - ''' - - #print 'on_connect failure - args:' - #for i in args: - # print ' %s' % i - self.connect_failed = True - self.event.set() - - def on_auth(self, con, auth): - ''' - Method called after authentication is done regardless on the result. - - :Parameters: - con : NonBlockingClient - reference to authenticated object - auth : string - type of authetication in case of success ('old_auth', 'sasl') or - None in case of auth failure - ''' - - #print 'on_auth - args:' - #print ' con: %s' % con - #print ' auth: %s' % auth - self.auth_connection = con - self.auth = auth - self.event.set() - - def wait(self): - ''' - Waiting until some callback sets the event and clearing the event subsequently. - ''' - - self.event.wait() - self.event.clear() - - - - -class IdleQueueThread(threading.Thread): - ''' - Thread for regular processing of idlequeue. - ''' - def __init__(self): - self.iq = idlequeue.IdleQueue() - self.stop = threading.Event() - ''' - Event used to stopping the thread main loop. - ''' - - self.stop.clear() - threading.Thread.__init__(self) - - def run(self): - while not self.stop.isSet(): - self.iq.process() - time.sleep(idlequeue_interval) - self.iq.process() - - def stop_thread(self): - self.stop.set() - - - - class TestNonBlockingClient(unittest.TestCase): ''' Test Cases class for NonBlockingClient. @@ -147,8 +44,8 @@ class TestNonBlockingClient(unittest.TestCase): self.client = client_nb.NonBlockingClient( server=xmpp_server_port[0], port=xmpp_server_port[1], - on_connect=self.connection.on_connect, - on_connect_failure=self.connection.on_connect_failure, + on_connect=lambda *args: self.connection.on_connect(True, *args), + on_connect_failure=lambda *args: self.connection.on_connect(False, *args), caller=self.connection ) ''' @@ -180,10 +77,10 @@ class TestNonBlockingClient(unittest.TestCase): self.connection.wait() # if on_connect was called, client has to be connected and vice versa - if self.connection.connect_failed: - self.assert_(not self.client.isConnected()) - else: + if self.connection.connect_succeeded: self.assert_(self.client.isConnected()) + else: + self.assert_(not self.client.isConnected()) def client_auth(self, username, password, resource, sasl): ''' @@ -203,7 +100,7 @@ class TestNonBlockingClient(unittest.TestCase): ''' Does disconnecting of connected client. Returns when TCP connection is closed. ''' - self.client.start_disconnect(None, on_disconnect=self.connection.event.set) + self.client.start_disconnect(None, on_disconnect=self.connection.set_event) print 'waiting for disconnecting...' self.connection.wait() @@ -260,7 +157,7 @@ class TestNonBlockingClient(unittest.TestCase): ''' self.open_stream(xmpp_server_port) self.assert_(self.client.isConnected()) - self.client_auth(credentials[0], "wrong pass", credentials[2], sasl=0) + self.client_auth(credentials[0], "wrong pass", credentials[2], sasl=1) self.assert_(self.connection.auth is None) self.do_disconnect() @@ -271,7 +168,10 @@ class TestNonBlockingClient(unittest.TestCase): if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(TestNonBlockingClient) + #suite = unittest.TestLoader().loadTestsFromTestCase(TestNonBlockingClient) + suite = unittest.TestSuite() + suite.addTest(TestNonBlockingClient('test_proper_connect_sasl')) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/test/test_nonblockingtcp.py b/test/test_nonblockingtcp.py new file mode 100644 index 000000000..cf6d31a2d --- /dev/null +++ b/test/test_nonblockingtcp.py @@ -0,0 +1,119 @@ +''' +Unit test for NonBlockingTcp tranport. +''' + +import unittest +from xmpp_mocks import * + +import threading, sys, os.path, time + +gajim_root = os.path.join(os.path.abspath(os.path.dirname(__file__)), '..') + +sys.path.append(gajim_root + '/src/common/xmpp') +sys.path.append(gajim_root + '/src/common') + +import transports_new, debug +from client import * + +xmpp_server = ('xmpp.example.org',5222) +''' +2-tuple - (XMPP server hostname, c2s port) +Script will connect to the machine. +''' + +dns_timeout = 10 +''' +timeout for DNS A-request (for getaddrinfo() call) +''' + +class MockClient(IdleMock): + def __init__(self, server, port): + self.debug_flags=['all', 'nodebuilder'] + self._DEBUG = debug.Debug(['socket']) + self.DEBUG = self._DEBUG.Show + self.server = server + self.port = port + IdleMock.__init__(self) + self.tcp_connected = False + self.ip_addresses = [] + self.socket = None + + def do_dns_request(self): + transports_new.NBgetaddrinfo( + server=(self.server, self.port), + on_success=lambda *args:self.on_success('DNSrequest', *args), + on_failure=self.on_failure, + timeout_sec=dns_timeout + ) + self.wait() + + + def try_next_ip(self, err_message=None): + if err_message: + print err_message + if self.ip_addresses == []: + self.on_failure('Run out of hosts') + return + current_ip = self.ip_addresses.pop(0) + self.NonBlockingTcp.connect( + conn_5tuple=current_ip, + on_tcp_connect=lambda *args: self.on_success('TCPconnect',*args), + on_tcp_failure=self.try_next_ip, + idlequeue=self.idlequeue + ) + self.wait() + + + def set_idlequeue(self, idlequeue): + self.idlequeue=idlequeue + + def on_failure(self, data): + print 'Error: %s' % data + self.set_event() + + def on_success(self, mode, data): + if mode == "DNSrequest": + self.ip_addresses = data + elif mode == "TCPconnect": + pass + self.set_event() + + + + + + + + +class TestNonBlockingTcp(unittest.TestCase): + def setUp(self): + self.nbtcp = transports_new.NonBlockingTcp() + self.client = MockClient(*xmpp_server) + self.idlequeue_thread = IdleQueueThread() + self.idlequeue_thread.start() + self.client.set_idlequeue(self.idlequeue_thread.iq) + self.nbtcp.PlugIn(self.client) + + def tearDown(self): + self.idlequeue_thread.stop_thread() + self.idlequeue_thread.join() + + + def testSth(self): + self.client.do_dns_request() + if self.client.ip_addresses == []: + print 'No IP found for given hostname: %s' % self.client.server + return + else: + self.client.try_next_ip() + + + + + + +if __name__ == '__main__': + + suite = unittest.TestLoader().loadTestsFromTestCase(TestNonBlockingTcp) + unittest.TextTestRunner(verbosity=2).run(suite) + diff --git a/test/xmpp_mocks.py b/test/xmpp_mocks.py new file mode 100644 index 000000000..47d5b0c9f --- /dev/null +++ b/test/xmpp_mocks.py @@ -0,0 +1,120 @@ +''' +Module with dummy classes for unit testing of xmpp code (src/common/xmpp/*). +''' + +import threading, time, os.path, sys + +from mock import Mock + +gajim_root = os.path.join(os.path.abspath(os.path.dirname(__file__)), '..') + +sys.path.append(gajim_root + '/src/common/xmpp') + +''' +Module with classes usable for XMPP related testing. +''' + +import idlequeue +from client import PlugIn + +idlequeue_interval = 0.2 +''' +IdleQueue polling interval. 200ms is used in Gajim as default +''' + + +class IdleQueueThread(threading.Thread): + ''' + Thread for regular processing of idlequeue. + ''' + def __init__(self): + self.iq = idlequeue.IdleQueue() + self.stop = threading.Event() + ''' + Event used to stopping the thread main loop. + ''' + + self.stop.clear() + threading.Thread.__init__(self) + + def run(self): + while not self.stop.isSet(): + self.iq.process() + time.sleep(idlequeue_interval) + + def stop_thread(self): + self.stop.set() + + + + +class IdleMock: + ''' + Serves as template for testing objects that are normally controlled by GUI. + Allows to wait for asynchronous callbacks with wait() method. + ''' + def __init__(self): + self.event = threading.Event() + ''' + Event is used for waiting on callbacks. + ''' + self.event.clear() + + + def wait(self): + ''' + Waiting until some callback sets the event and clearing the event subsequently. + ''' + self.event.wait() + self.event.clear() + + def set_event(self): + self.event.set() + + +class MockConnectionClass(IdleMock,Mock): + ''' + Class simulating Connection class from src/common/connection.py + + It is derived from Mock in order to avoid defining all methods + from real Connection that are called from NBClient or Dispatcher + ( _event_dispatcher for example) + ''' + + def __init__(self, *args): + self.connect_succeeded = True + IdleMock.__init__(self) + Mock.__init__(self, *args) + + def on_connect(self, success, *args): + ''' + Method called after connecting - after receiving + from server (NOT after TLS stream restart) or connect failure + ''' + + print 'on_connect - args:' + print ' success - %s' % success + for i in args: + print ' %s' % i + self.connect_succeeded = success + self.set_event() + + def on_auth(self, con, auth): + ''' + Method called after authentication is done regardless on the result. + + :Parameters: + con : NonBlockingClient + reference to authenticated object + auth : string + type of authetication in case of success ('old_auth', 'sasl') or + None in case of auth failure + ''' + + #print 'on_auth - args:' + #print ' con: %s' % con + #print ' auth: %s' % auth + self.auth_connection = con + self.auth = auth + self.set_event() + From f3820706fb6e0f9057bf35443ff1b8cf12922ab8 Mon Sep 17 00:00:00 2001 From: tomk Date: Mon, 30 Jun 2008 00:02:32 +0000 Subject: [PATCH 05/20] - Refactored non-blocking transport and client classes - getaddrinfo is called in Client now - Added NonBlockingHttpBOSH transport (to tranports_nb) and BOSHClient (to client_nb) - Extended possible proxy types in configuration by "BOSH" proxy - Rewrote NonBlockingTLS to invoke success callback only after successful TLS handshake is over (formerly, the TLS Plugin returned right after sending ) --- data/glade/manage_proxies_window.glade | 3 +- src/common/connection.py | 67 +- src/common/xmpp/auth_nb.py | 3 + src/common/xmpp/client_nb.py | 595 +++++++------ src/common/xmpp/debug.py | 1 + src/common/xmpp/dispatcher_nb.py | 39 +- src/common/xmpp/idlequeue.py | 2 +- src/common/xmpp/protocol.py | 1107 ++++++++++++------------ src/common/xmpp/transports_nb.py | 1046 +++++++++++----------- src/common/xmpp/transports_new.py | 270 ------ src/config.py | 4 +- test/test_client_nb.py | 59 +- test/test_nonblockingtcp.py | 84 +- 13 files changed, 1555 insertions(+), 1725 deletions(-) delete mode 100644 src/common/xmpp/transports_new.py diff --git a/data/glade/manage_proxies_window.glade b/data/glade/manage_proxies_window.glade index 758cfcce7..b28beef97 100644 --- a/data/glade/manage_proxies_window.glade +++ b/data/glade/manage_proxies_window.glade @@ -211,7 +211,8 @@ True HTTP Connect -SOCKS5 +SOCKS5 +BOSH False True diff --git a/src/common/connection.py b/src/common/connection.py index 493140fe0..edb788aa3 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -55,6 +55,7 @@ from common.rst_xhtml_generator import create_xhtml from string import Template import logging log = logging.getLogger('gajim.c.connection') +log.setLevel(logging.DEBUG) ssl_error = { 2: _("Unable to get issuer certificate"), @@ -207,7 +208,7 @@ class Connection(ConnectionHandlers): def _disconnectedReconnCB(self): '''Called when we are disconnected''' - log.debug('disconnectedReconnCB') + log.error('disconnectedReconnCB') if gajim.account_is_connected(self.name): # we cannot change our status to offline or connecting # after we auth to server @@ -467,7 +468,6 @@ class Connection(ConnectionHandlers): proxy = None else: proxy = None - h = hostname p = 5222 ssl_p = 5223 @@ -504,7 +504,7 @@ class Connection(ConnectionHandlers): self.connect_to_next_host() def on_proxy_failure(self, reason): - log.debug('Connection to proxy failed') + log.error('Connection to proxy failed: %s' % reason) self.time_to_reconnect = None self.on_connect_failure = None self.disconnect(on_purpose = True) @@ -519,23 +519,6 @@ class Connection(ConnectionHandlers): self.last_connection.socket.disconnect() self.last_connection = None self.connection = None - if gajim.verbose: - con = common.xmpp.NonBlockingClient(self._hostname, caller = self, - on_connect = self.on_connect_success, - on_proxy_failure = self.on_proxy_failure, - on_connect_failure = self.connect_to_next_type) - else: - con = common.xmpp.NonBlockingClient(self._hostname, debug = [], - caller = self, on_connect = self.on_connect_success, - on_proxy_failure = self.on_proxy_failure, - on_connect_failure = self.connect_to_next_type) - self.last_connection = con - # increase default timeout for server responses - common.xmpp.dispatcher_nb.DEFAULT_TIMEOUT_SECONDS = self.try_connecting_for_foo_secs - con.set_idlequeue(gajim.idlequeue) - # FIXME: this is a hack; need a better way - if self.on_connect_success == self._on_new_account: - con.RegisterDisconnectHandler(self._on_new_account) if self._current_type == 'ssl': port = self._current_host['ssl_port'] @@ -546,9 +529,40 @@ class Connection(ConnectionHandlers): secur = 0 else: secur = None + + if self._proxy and self._proxy['type'] == 'bosh': + clientClass = common.xmpp.BOSHClient + else: + clientClass = common.xmpp.NonBlockingClient + + if gajim.verbose: + con = common.xmpp.NonBlockingClient( + hostname=self._current_host['host'], + port=port, + caller=self, + idlequeue=gajim.idlequeue) + else: + con = common.xmpp.NonBlockingClient( + hostname=self._current_host['host'], + debug=[], + port=port, + caller=self, + idlequeue=gajim.idlequeue) + + self.last_connection = con + # increase default timeout for server responses + common.xmpp.dispatcher_nb.DEFAULT_TIMEOUT_SECONDS = self.try_connecting_for_foo_secs + # FIXME: this is a hack; need a better way + if self.on_connect_success == self._on_new_account: + con.RegisterDisconnectHandler(self._on_new_account) + log.info('Connecting to %s: [%s:%d]', self.name, self._current_host['host'], port) - con.connect((self._current_host['host'], port), proxy=self._proxy, + con.connect( + on_connect=self.on_connect_success, + on_proxy_failure=self.on_proxy_failure, + on_connect_failure=self.connect_to_next_type, + proxy=self._proxy, secure = secur) else: self.connect_to_next_host(retry) @@ -561,6 +575,9 @@ class Connection(ConnectionHandlers): 'connection_types').split() else: self._connection_types = ['tls', 'ssl', 'plain'] + + # FIXME: remove after tls and ssl will be degubbed + #self._connection_types = ['plain'] host = self.select_next_host(self._hosts) self._current_host = host self._hosts.remove(host) @@ -975,7 +992,11 @@ class Connection(ConnectionHandlers): p.setStatus(msg) self.remove_all_transfers() self.time_to_reconnect = None - self.connection.start_disconnect(p, self._on_disconnected) + + self.connection.RegisterDisconnectHandler(self._on_disconnected) + self.connection.send(p) + self.connection.StreamTerminate() + #self.connection.start_disconnect(p, self._on_disconnected) else: self.time_to_reconnect = None self._on_disconnected() @@ -1010,7 +1031,7 @@ class Connection(ConnectionHandlers): def _on_disconnected(self): ''' called when a disconnect request has completed successfully''' self.dispatch('STATUS', 'offline') - self.disconnect() + self.disconnect(on_purpose=True) def get_status(self): return STATUS_LIST[self.connected] diff --git a/src/common/xmpp/auth_nb.py b/src/common/xmpp/auth_nb.py index b2624043f..662b3f60f 100644 --- a/src/common/xmpp/auth_nb.py +++ b/src/common/xmpp/auth_nb.py @@ -169,6 +169,9 @@ class SASL(PlugIn): self.startsasl='success' self.DEBUG('Successfully authenticated with remote server.', 'ok') handlers=self._owner.Dispatcher.dumpHandlers() + print '6' * 79 + print handlers + print '6' * 79 self._owner.Dispatcher.PlugOut() dispatcher_nb.Dispatcher().PlugIn(self._owner) self._owner.Dispatcher.restoreHandlers(handlers) diff --git a/src/common/xmpp/client_nb.py b/src/common/xmpp/client_nb.py index d82fb9e94..f362d02a9 100644 --- a/src/common/xmpp/client_nb.py +++ b/src/common/xmpp/client_nb.py @@ -17,40 +17,61 @@ # $Id: client.py,v 1.52 2006/01/02 19:40:55 normanr Exp $ ''' -Provides PlugIn class functionality to develop extentions for xmpppy. -Also provides Client and Component classes implementations as the -examples of xmpppy structures usage. +Provides Client classes implementations as examples of xmpppy structures usage. These classes can be used for simple applications "AS IS" though. ''' import socket import debug +import random -import transports_nb, dispatcher_nb, auth_nb, roster_nb +import transports_nb, dispatcher_nb, auth_nb, roster_nb, protocol from client import * +import logging +log = logging.getLogger('gajim.c.x.client_nb') + +consoleloghandler = logging.StreamHandler() +consoleloghandler.setLevel(logging.DEBUG) +consoleloghandler.setFormatter( + logging.Formatter('%(levelname)s: %(message)s') +) +log.setLevel(logging.DEBUG) +log.addHandler(consoleloghandler) +log.propagate = False + + class NBCommonClient: ''' Base for Client and Component classes.''' - def __init__(self, server, port=5222, debug=['always', 'nodebuilder'], caller=None, - on_connect=None, on_proxy_failure=None, on_connect_failure=None): - ''' Caches server name and (optionally) port to connect to. "debug" parameter specifies - the debug IDs that will go into debug output. You can either specifiy an "include" - or "exclude" list. The latter is done via adding "always" pseudo-ID to the list. - Full list: ['nodebuilder', 'dispatcher', 'gen_auth', 'SASL_auth', 'bind', 'socket', - 'CONNECTproxy', 'TLS', 'roster', 'browser', 'ibb'] . ''' + def __init__(self, hostname, idlequeue, port=5222, debug=['always', 'nodebuilder'], caller=None): - if isinstance(self, NonBlockingClient): - self.Namespace, self.DBG = 'jabber:client', DBG_CLIENT - elif isinstance(self, NBCommonClient): - self.Namespace, self.DBG = dispatcher_nb.NS_COMPONENT_ACCEPT, DBG_COMPONENT + ''' Caches connection data: + :param hostname: hostname of machine where the XMPP server is running (from Account + of from SRV request) and port to connect to. + :param idlequeue: processing idlequeue + :param port: port of listening XMPP server + :param debug: specifies the debug IDs that will go into debug output. You can either + specifiy an "include" or "exclude" list. The latter is done via adding "always" + pseudo-ID to the list. Full list: ['nodebuilder', 'dispatcher', 'gen_auth', + 'SASL_auth', 'bind', 'socket', 'CONNECTproxy', 'TLS', 'roster', 'browser', 'ibb']. + TODO: get rid of debug.py using + :param caller: calling object - it has to implement certain methods (necessary?) + + ''' + self.DBG = DBG_CLIENT + + self.Namespace = protocol.NS_CLIENT + + self.idlequeue = idlequeue self.defaultNamespace = self.Namespace self.disconnect_handlers = [] - self.Server = server + + # XMPP server and port from account or SRV + self.Server = hostname self.Port = port - # Who initiated this client - # Used to register the EventDispatcher + # caller is who initiated this client, it is sed to register the EventDispatcher self._caller = caller if debug and type(debug) != list: debug = ['always', 'nodebuilder'] @@ -62,20 +83,24 @@ class NBCommonClient: self._registered_name = None self.connected = '' self._component=0 - self.idlequeue = None self.socket = None - self.on_connect = on_connect - self.on_proxy_failure = on_proxy_failure - self.on_connect_failure = on_connect_failure + self.on_connect = None + self.on_proxy_failure = None + self.on_connect_failure = None + self.proxy = None - def set_idlequeue(self, idlequeue): - self.idlequeue = idlequeue - def disconnected(self): - ''' Called on disconnection. Calls disconnect handlers and cleans things up. ''' + def on_disconnect(self): + ''' + Called on disconnection - when connect failure occurs on running connection + (after stream is successfully opened). + Calls disconnect handlers and cleans things up. + ''' + self.connected='' self.DEBUG(self.DBG,'Disconnect detected','stop') for i in reversed(self.disconnect_handlers): + self.DEBUG(self.DBG, 'Calling disc handler %s' % i, 'stop') i() if self.__dict__.has_key('NonBlockingRoster'): self.NonBlockingRoster.PlugOut() @@ -94,96 +119,201 @@ class NBCommonClient: if self.__dict__.has_key('NonBlockingTcp'): self.NonBlockingTcp.PlugOut() - def reconnectAndReauth(self): - ''' Just disconnect. We do reconnecting in connection.py ''' - self.disconnect() - return '' - def connect(self,server=None,proxy=None, ssl=None, on_stream_start = None): - ''' Make a tcp/ip connection, protect it with tls/ssl if possible and start XMPP stream. ''' - if not server: - server = (self.Server, self.Port) - self._Server, self._Proxy, self._Ssl = server , proxy, ssl - self.on_stream_start = on_stream_start + def send(self, stanza, is_message = False, now = False): + ''' interface for putting stanzas on wire. Puts ID to stanza if needed and + sends it via socket wrapper''' + (id, stanza_to_send) = self.Dispatcher.assign_id(stanza) + + if is_message: + # somehow zeroconf-specific + self.Connection.send(stanza_to_send, True, now = now) + else: + self.Connection.send(stanza_to_send, now = now) + return id + + + + def connect(self, on_connect, on_connect_failure, on_proxy_failure=None, proxy=None, secure=None): + ''' + Open XMPP connection (open streams in both directions). + :param on_connect: called after stream is successfully opened + :param on_connect_failure: called when error occures during connection + :param on_proxy_failure: called if error occurres during TCP connection to + proxy server or during connection to the proxy + :param proxy: dictionary with proxy data. It should contain at least values + for keys 'host' and 'port' - connection details for proxy server and + optionally keys 'user' and 'pass' as proxy credentials + :param secure: + ''' + + self.on_connect = on_connect + self.on_connect_failure=on_connect_failure + self.on_proxy_failure = on_proxy_failure + self._secure = secure + self.Connection = None + if proxy: + # with proxies, client connects to proxy instead of directly to + # XMPP server from __init__. + # tcp_server is hostname used for socket connecting + tcp_server=proxy['host'] + tcp_port=proxy['port'] + self._on_tcp_failure = self.on_proxy_failure if proxy.has_key('type'): + if proxy.has_key('user') and proxy.has_key('pass'): + proxy_creds=(proxy['user'],proxy['pass']) + else: + proxy_creds=(None, None) + type_ = proxy['type'] if type_ == 'socks5': - self.socket = transports_nb.NBSOCKS5PROXYsocket( - self._on_connected, self._on_proxy_failure, - self._on_connected_failure, proxy, server) + self.socket = transports_nb.NBSOCKS5ProxySocket( + on_disconnect=self.on_disconnect, + proxy_creds=proxy_creds, + xmpp_server=(self.Server, self.Port)) elif type_ == 'http': - self.socket = transports_nb.NBHTTPPROXYsocket(self._on_connected, - self._on_proxy_failure, self._on_connected_failure, proxy, - server) + self.socket = transports_nb.NBHTTPProxySocket( + on_disconnect=self.on_disconnect, + proxy_creds=proxy_creds, + xmpp_server=(self.Server, self.Port)) + elif type_ == 'bosh': + tcp_server = transports_nb.urisplit(tcp_server)[1] + self.socket = transports_nb.NonBlockingHttpBOSH( + on_disconnect=self.on_disconnect, + bosh_uri = proxy['host'], + bosh_port = tcp_port) else: - self.socket = transports_nb.NBHTTPPROXYsocket(self._on_connected, - self._on_proxy_failure, self._on_connected_failure, proxy, - server) + self.socket = transports_nb.NBHTTPProxySocket( + on_disconnect=self.on_disconnect, + proxy_creds=(None, None), + xmpp_server=(self.Server, self.Port)) else: - self.connected = 'tcp' - self.socket = transports_nb.NonBlockingTcp(self._on_connected, - self._on_connected_failure, server) + self._on_tcp_failure = self._on_connect_failure + tcp_server=self.Server + tcp_port=self.Port + self.socket = transports_nb.NonBlockingTcp(on_disconnect = self.on_disconnect) + self.socket.PlugIn(self) - return True + + self._resolve_hostname( + hostname=tcp_server, + port=tcp_port, + on_success=self._try_next_ip, + on_failure=self._on_tcp_failure) + + + + def _resolve_hostname(self, hostname, port, on_success, on_failure): + ''' wrapper of getaddinfo call. FIXME: getaddinfo blocks''' + try: + self.ip_addresses = socket.getaddrinfo(hostname,port, + socket.AF_UNSPEC,socket.SOCK_STREAM) + except socket.gaierror, (errnum, errstr): + on_failure(err_message='Lookup failure for %s:%s - %s %s' % + (self.Server, self.Port, errnum, errstr)) + else: + on_success() + + - def get_attrs(self, on_stream_start): - self.on_stream_start = on_stream_start - self.onreceive(self._on_receive_document_attrs) + def _try_next_ip(self, err_message=None): + '''iterates over IP addresses from getaddinfo''' + if err_message: + self.DEBUG(self.DBG,err_message,'connect') + if self.ip_addresses == []: + self._on_tcp_failure(err_message='Run out of hosts for name %s:%s' % + (self.Server, self.Port)) + else: + self.current_ip = self.ip_addresses.pop(0) + self.socket.connect( + conn_5tuple=self.current_ip, + on_connect=lambda: self._xmpp_connect(socket_type='tcp'), + on_connect_failure=self._try_next_ip) - def _on_proxy_failure(self, reason): - if self.on_proxy_failure: - self.on_proxy_failure(reason) - def _on_connected_failure(self, retry = None): + def incoming_stream_version(self): + ''' gets version of xml stream''' + if self.Dispatcher.Stream._document_attrs.has_key('version'): + return self.Dispatcher.Stream._document_attrs['version'] + else: + return None + + def _xmpp_connect(self, socket_type): + self.connected = socket_type + self._xmpp_connect_machine() + + + def _xmpp_connect_machine(self, mode=None, data=None): + ''' + Finite automaton called after TCP connecting. Takes care of stream opening + and features tag handling. Calls _on_stream_start when stream is + started, and _on_connect_failure on failure. + ''' + #FIXME: use RegisterHandlerOnce instead of onreceive + log.info('=============xmpp_connect_machine() >> mode: %s, data: %s' % (mode,data)) + + def on_next_receive(mode): + if mode is None: + self.onreceive(None) + else: + self.onreceive(lambda data:self._xmpp_connect_machine(mode, data)) + + if not mode: + dispatcher_nb.Dispatcher().PlugIn(self) + on_next_receive('RECEIVE_DOCUMENT_ATTRIBUTES') + + elif mode == 'FAILURE': + self._on_connect_failure(err_message='During XMPP connect: %s' % data) + + elif mode == 'RECEIVE_DOCUMENT_ATTRIBUTES': + if data: + self.Dispatcher.ProcessNonBlocking(data) + if not hasattr(self, 'Dispatcher') or \ + self.Dispatcher.Stream._document_attrs is None: + self._xmpp_connect_machine( + mode='FAILURE', + data='Error on stream open') + if self.incoming_stream_version() == '1.0': + if not self.Dispatcher.Stream.features: + on_next_receive('RECEIVE_STREAM_FEATURES') + else: + self._xmpp_connect_machine(mode='STREAM_STARTED') + + else: + self._xmpp_connect_machine(mode='STREAM_STARTED') + + elif mode == 'RECEIVE_STREAM_FEATURES': + if data: + # sometimes are received together with document + # attributes and sometimes on next receive... + self.Dispatcher.ProcessNonBlocking(data) + if not self.Dispatcher.Stream.features: + self._xmpp_connect_machine( + mode='FAILURE', + data='Missing in 1.0 stream') + else: + self._xmpp_connect_machine(mode='STREAM_STARTED') + + elif mode == 'STREAM_STARTED': + self._on_stream_start() + + def _on_stream_start(self): + '''Called when stream is opened. To be overriden in derived classes.''' + + def _on_connect_failure(self, retry=None, err_message=None): + self.connected = None + if err_message: + self.DEBUG(self.DBG, err_message, 'connecting') if self.socket: self.socket.disconnect() - if self.on_connect_failure: - self.on_connect_failure(retry) + self.on_connect_failure(retry) + + def _on_connect(self): + self.onreceive(None) + self.on_connect(self, self.connected) - def _on_connected(self): - # FIXME: why was this needed? Please note that we're working - # in nonblocking mode, and this handler is actually called - # as soon as connection is initiated, NOT when connection - # succeeds, as the name suggests. - # # connect succeeded, so no need of this callback anymore - # self.on_connect_failure = None - self.connected = 'tcp' - if self._Ssl: - transports_nb.NonBlockingTLS().PlugIn(self, now=1) - if not self.Connection: # ssl error, stream is closed - return - self.connected = 'ssl' - self.onreceive(self._on_receive_document_attrs) - dispatcher_nb.Dispatcher().PlugIn(self) - def _on_receive_document_attrs(self, data): - if data: - self.Dispatcher.ProcessNonBlocking(data) - if not hasattr(self, 'Dispatcher') or \ - self.Dispatcher.Stream._document_attrs is None: - return - self.onreceive(None) - if self.Dispatcher.Stream._document_attrs.has_key('version') and \ - self.Dispatcher.Stream._document_attrs['version'] == '1.0': - self.onreceive(self._on_receive_stream_features) - return - if self.on_stream_start: - self.on_stream_start() - self.on_stream_start = None - return True - - def _on_receive_stream_features(self, data): - if data: - self.Dispatcher.ProcessNonBlocking(data) - if not self.Dispatcher.Stream.features: - return - # pass # If we get version 1.0 stream the features tag MUST BE presented - self.onreceive(None) - if self.on_stream_start: - self.on_stream_start() - self.on_stream_start = None - return True # moved from client.CommonClient: def RegisterDisconnectHandler(self,handler): @@ -200,11 +330,7 @@ class NBCommonClient: override this method or at least unregister it. """ raise IOError('Disconnected from server.') - def event(self,eventName,args={}): - """ Default event handler. To be overriden. """ - print "Event: ",(eventName,args) - - def isConnected(self): + def get_connect_type(self): """ Returns connection state. F.e.: None / 'tls' / 'tcp+non_sasl' . """ return self.connected @@ -212,74 +338,18 @@ class NBCommonClient: ''' get the ip address of the account, from which is made connection to the server , (e.g. me). We will create listening socket on the same ip ''' - # moved from client.CommonClient if hasattr(self, 'Connection'): return self.Connection._sock.getsockname() - -class NonBlockingClient(NBCommonClient): - ''' Example client class, based on CommonClient. ''' - def connect(self,server=None,proxy=None,secure=None,use_srv=True): - ''' Connect to jabber server. If you want to specify different ip/port to connect to you can - pass it as tuple as first parameter. If there is HTTP proxy between you and server - specify it's address and credentials (if needed) in the second argument. - If you want ssl/tls support to be discovered and enable automatically - leave third argument as None. (ssl will be autodetected only if port is 5223 or 443) - If you want to force SSL start (i.e. if port 5223 or 443 is remapped to some non-standard port) then set it to 1. - If you want to disable tls/ssl support completely, set it to 0. - Example: connect(('192.168.5.5',5222),{'host':'proxy.my.net','port':8080,'user':'me','password':'secret'}) - Returns '' or 'tcp' or 'tls', depending on the result.''' - self.__secure = secure - self.Connection = None - NBCommonClient.connect(self, server = server, proxy = proxy, ssl = secure, - on_stream_start = self._on_tcp_stream_start) - return self.connected - - - def _is_connected(self): - self.onreceive(None) - if self.on_connect: - self.on_connect(self, self.connected) - self.on_connect_failure = None - self.on_connect = None - - def _on_tcp_stream_start(self): - if not self.connected or self.__secure is not None and not self.__secure: - self._is_connected() - return True - self.isplugged = True - self.onreceive(None) - transports_nb.NonBlockingTLS().PlugIn(self) - if not self.Connection: # ssl error, stream is closed - return True - if not self.Dispatcher.Stream._document_attrs.has_key('version') or \ - not self.Dispatcher.Stream._document_attrs['version']=='1.0': - self._is_connected() - return - if not self.Dispatcher.Stream.features.getTag('starttls'): - self._is_connected() - return - self.onreceive(self._on_receive_starttls) - def _on_receive_starttls(self, data): - if data: - self.Dispatcher.ProcessNonBlocking(data) - if not self.NonBlockingTLS.starttls: - return - self.onreceive(None) - if not hasattr(self, 'NonBlockingTLS') or self.NonBlockingTLS.starttls != 'success': - self.event('tls_failed') - self._is_connected() - return - self.connected = 'tls' - self.onreceive(None) - self._is_connected() - return True - + def auth(self, user, password, resource = '', sasl = 1, on_auth = None): + + print 'auth called' ''' Authenticate connnection and bind resource. If resource is not provided random one or library name used. ''' self._User, self._Password, self._Resource, self._sasl = user, password, resource, sasl self.on_auth = on_auth - self.get_attrs(self._on_doc_attrs) + self._on_doc_attrs() return def _on_old_auth(self, res): @@ -335,6 +405,40 @@ class NonBlockingClient(NBCommonClient): self.on_auth(self, 'sasl') else: self.on_auth(self, None) + + + + +class NonBlockingClient(NBCommonClient): + ''' Example client class, based on CommonClient. ''' + + + def _on_stream_start(self): + ''' + Called after XMPP stream is opened. + In pure XMPP client, TLS negotiation may follow after esabilishing a stream. + ''' + self.onreceive(None) + if self.connected == 'tcp': + if not self.connected or self._secure is not None and not self._secure: + # if we are disconnected or TLS/SSL is not desired, return + self._on_connect() + return + if not self.Dispatcher.Stream.features.getTag('starttls'): + # if server doesn't advertise TLS in init response + self._on_connect() + return + if self.incoming_stream_version() != '1.0': + self._on_connect() + return + # otherwise start TLS + transports_nb.NonBlockingTLS().PlugIn( + self, + on_tls_success=lambda: self._xmpp_connect(socket_type='tls'), + on_tls_failure=self._on_connect_failure) + elif self.connected == 'tls': + self._on_connect() + def initRoster(self): ''' Plug in the roster. ''' @@ -354,87 +458,84 @@ class NonBlockingClient(NBCommonClient): if requestRoster: roster_nb.NonBlockingRoster().PlugIn(self) self.send(dispatcher_nb.Presence(to=jid, typ=typ)) -class Component(NBCommonClient): - ''' Component class. The only difference from CommonClient is ability to perform component authentication. ''' - def __init__(self, server, port=5347, typ=None, debug=['always', 'nodebuilder'], - domains=None, component=0, on_connect = None, on_connect_failure = None): - ''' Init function for Components. - As components use a different auth mechanism which includes the namespace of the component. - Jabberd1.4 and Ejabberd use the default namespace then for all client messages. - Jabberd2 uses jabber:client. - 'server' argument is a server name that you are connecting to (f.e. "localhost"). - 'port' can be specified if 'server' resolves to correct IP. If it is not then you'll need to specify IP - and port while calling "connect()".''' - NBCommonClient.__init__(self, server, port=port, debug=debug) - self.typ = typ - self.component=component - if domains: - self.domains=domains - else: - self.domains=[server] - self.on_connect_component = on_connect - self.on_connect_failure = on_connect_failure - - def connect(self, server=None, proxy=None): - ''' This will connect to the server, and if the features tag is found then set - the namespace to be jabber:client as that is required for jabberd2. - 'server' and 'proxy' arguments have the same meaning as in xmpp.Client.connect() ''' - if self.component: - self.Namespace=auth.NS_COMPONENT_1 - self.Server=server[0] - NBCommonClient.connect(self, server=server, proxy=proxy, - on_connect = self._on_connect, on_connect_failure = self.on_connect_failure) - - def _on_connect(self): - if self.typ=='jabberd2' or not self.typ and self.Dispatcher.Stream.features is not None: - self.defaultNamespace=auth.NS_CLIENT - self.Dispatcher.RegisterNamespace(self.defaultNamespace) - self.Dispatcher.RegisterProtocol('iq',dispatcher.Iq) - self.Dispatcher.RegisterProtocol('message',dispatcher_nb.Message) - self.Dispatcher.RegisterProtocol('presence',dispatcher_nb.Presence) - self.on_connect(self.connected) - def auth(self, name, password, dup=None, sasl=0): - ''' Authenticate component "name" with password "password".''' - self._User, self._Password, self._Resource=name, password,'' - try: - if self.component: - sasl=1 - if sasl: - auth.SASL(name,password).PlugIn(self) - if not sasl or self.SASL.startsasl=='not-supported': - if auth.NonSASL(name,password,'').PlugIn(self): - self.connected+='+old_auth' - return 'old_auth' - return - self.SASL.auth() - self.onreceive(self._on_auth_component) - except: - self.DEBUG(self.DBG,"Failed to authenticate %s" % name,'error') - - def _on_auth_component(self, data): - if data: - self.Dispatcher.ProcessNonBlocking(data) - if self.SASL.startsasl == 'in-process': - return - if self.SASL.startsasl =='success': - if self.component: - self._component = self.component - auth.NBComponentBind().PlugIn(self) - self.onreceive(_on_component_bind) - self.connected += '+sasl' - else: - raise auth.NotAuthorized(self.SASL.startsasl) - - def _on_component_bind(self, data): - if data: - self.Dispatcher.ProcessNonBlocking(data) - if self.NBComponentBind.bound is None: - return - - for domain in self.domains: - self.NBComponentBind.Bind(domain, _on_component_bound) - - def _on_component_bound(self, resp): - self.NBComponentBind.PlugOut() +class BOSHClient(NBCommonClient): + ''' + Client class implementing BOSH. + ''' + def __init__(self, *args, **kw): + '''Preceeds constructor of NBCommonClient and sets some of values that will + be used as attributes in tag''' + self.Namespace = NS_HTTP_BIND + # BOSH parameters should be given via Advanced Configuration Editor + self.bosh_hold = 1 + self.bosh_wait=60 + self.bosh_rid=-1 + self.bosh_httpversion = 'HTTP/1.1' + NBCommonClient.__init__(self, *args, **kw) + + + def connect(self, *args, **kw): + proxy = kw['proxy'] + self.bosh_protocol, self.bosh_host, self.bosh_uri = self.urisplit(proxy['host']) + self.bosh_port = proxy['port'] + NBCommonClient.connect(*args, **kw) + + def _on_stream_start(self): + ''' + Called after XMPP stream is opened. In BOSH TLS is negotiated on tranport layer + so success callback can be invoked. + (authentication is started from auth() method) + ''' + self.onreceive(None) + if self.connected == 'tcp': + self._on_connect() + + + + + + def bosh_raise_event(self, realm, event, data): + # should to extract stanza from body + self.DEBUG(self.DBG,'realm: %s, event: %s, data: %s' % (realm, event, data), + 'BOSH EventHandler') + self._caller._event_dispatcher(realm, event, data) + + + def StreamInit(self): + ''' + Init of BOSH session. Called instead of Dispatcher.StreamInit() + Initial body tag is created and sent. + ''' + #self.Dispatcher.RegisterEventHandler(self.bosh_event_handler) + self.Dispatcher.Stream = simplexml.NodeBuilder() + self.Dispatcher.Stream._dispatch_depth = 2 + self.Dispatcher.Stream.dispatch = self.Dispatcher.dispatch + self.Dispatcher.Stream.stream_header_received = self._check_stream_start + self.Dispatcher.Stream.features = None + + r = random.Random() + r.seed() + # with 50-bit random initial rid, session would have to go up + # to 7881299347898368 messages to raise rid over 2**53 + # (see http://www.xmpp.org/extensions/xep-0124.html#rids) + self.bosh_rid = r.getrandbits(50) + + initial_body_tag = BOSHBody( + attrs={'content': 'text/xml; charset=utf-8', + 'hold': str(self.bosh_hold), + # "to" should be domain, not hostname of machine + 'to': self.Server, + 'wait': str(self.bosh_wait), + 'rid': str(self.bosh_rid), + 'xmpp:version': '1.0', + 'xmlns:xmpp': 'urn:xmpp:xbosh'} + ) + + if locale.getdefaultlocale()[0]: + initial_body_tag.setAttr('xml:lang', + locale.getdefaultlocale()[0].split('_')[0]) + initial_body_tag.setAttr('xmpp:version', '1.0') + initial_body_tag.setAttr('xmlns:xmpp', 'urn:xmpp:xbosh') + self.send(initial_body_tag) diff --git a/src/common/xmpp/debug.py b/src/common/xmpp/debug.py index 60480b66c..80ab6be9a 100644 --- a/src/common/xmpp/debug.py +++ b/src/common/xmpp/debug.py @@ -393,6 +393,7 @@ class Debug: colors={} def Show(self, flag, msg, prefix=''): + msg=str(msg) msg=msg.replace('\r','\\r').replace('\n','\\n').replace('><','>\n <') if not colors_enabled: pass elif self.colors.has_key(prefix): msg=self.colors[prefix]+msg+color_none diff --git a/src/common/xmpp/dispatcher_nb.py b/src/common/xmpp/dispatcher_nb.py index 515a09d10..4633db28b 100644 --- a/src/common/xmpp/dispatcher_nb.py +++ b/src/common/xmpp/dispatcher_nb.py @@ -47,7 +47,7 @@ class Dispatcher(PlugIn): self._exported_methods=[self.RegisterHandler, self.RegisterDefaultHandler, \ self.RegisterEventHandler, self.UnregisterCycleHandler, self.RegisterCycleHandler, \ self.RegisterHandlerOnce, self.UnregisterHandler, self.RegisterProtocol, \ - self.SendAndWaitForResponse, self.send,self.disconnect, \ + self.SendAndWaitForResponse, self.assign_id, self.StreamTerminate, \ self.SendAndCallForResponse, self.getAnID, self.Event] def getAnID(self): @@ -79,6 +79,8 @@ class Dispatcher(PlugIn): def plugin(self, owner): ''' Plug the Dispatcher instance into Client class instance and send initial stream header. Used internally.''' + self.DEBUG('Dispatcher plugin', 'PlugIn') + self._init() self._owner.lastErrNode = None self._owner.lastErr = None @@ -116,6 +118,10 @@ class Dispatcher(PlugIn): locale.getdefaultlocale()[0].split('_')[0]) self._owner.send("%s>" % str(self._metastream)[:-2]) + def StreamTerminate(self): + ''' Send a stream terminator. ''' + self._owner.send('') + def _check_stream_start(self, ns, tag, attrs): if ns<>NS_STREAMS or tag<>'stream': raise ValueError('Incorrect stream start: (%s,%s). Terminating.' % (tag, ns)) @@ -139,7 +145,7 @@ class Dispatcher(PlugIn): return 0 except ExpatError: self.DEBUG('Invalid XML received from server. Forcing disconnect.', 'error') - self._owner.Connection.pollend() + self._owner.Connection.disconnect() return 0 if len(self._pendingExceptions) > 0: _pendingException = self._pendingExceptions.pop() @@ -244,7 +250,7 @@ class Dispatcher(PlugIn): def returnStanzaHandler(self,conn,stanza): ''' Return stanza back to the sender with error set. ''' if stanza.getType() in ['get','set']: - conn.send(Error(stanza,ERR_FEATURE_NOT_IMPLEMENTED)) + conn._owner.send(Error(stanza,ERR_FEATURE_NOT_IMPLEMENTED)) def streamErrorHandler(self,conn,error): name,text='error',error.getData() @@ -387,7 +393,7 @@ class Dispatcher(PlugIn): ''' Put stanza on the wire and wait for recipient's response to it. ''' if timeout is None: timeout = DEFAULT_TIMEOUT_SECONDS - self._witid = self.send(stanza) + self._witid = self._owner.send(stanza) if func: self.on_responses[self._witid] = (func, args) if timeout: @@ -401,11 +407,10 @@ class Dispatcher(PlugIn): Additional callback arguments can be specified in args. ''' self.SendAndWaitForResponse(stanza, 0, func, args) - def send(self, stanza, is_message = False, now = False): - ''' Serialise stanza and put it on the wire. Assign an unique ID to it before send. - Returns assigned ID.''' + def assign_id(self, stanza): + ''' Assign an unique ID to stanza and return assigned ID.''' if type(stanza) in [type(''), type(u'')]: - return self._owner.Connection.send(stanza, now = now) + return (None, stanza) if not isinstance(stanza, Protocol): _ID=None elif not stanza.getID(): @@ -417,23 +422,7 @@ class Dispatcher(PlugIn): _ID=stanza.getID() if self._owner._registered_name and not stanza.getAttr('from'): stanza.setAttr('from', self._owner._registered_name) - if self._owner._component and stanza.getName() != 'bind': - to=self._owner.Server - if stanza.getTo() and stanza.getTo().getDomain(): - to=stanza.getTo().getDomain() - frm=stanza.getFrom() - if frm.getDomain(): - frm=frm.getDomain() - route=Protocol('route', to=to, frm=frm, payload=[stanza]) - stanza=route stanza.setNamespace(self._owner.Namespace) stanza.setParent(self._metastream) - if is_message: - self._owner.Connection.send(stanza, True, now = now) - else: - self._owner.Connection.send(stanza, now = now) - return _ID + return (_ID, stanza) - def disconnect(self): - ''' Send a stream terminator. ''' - self._owner.Connection.send('') diff --git a/src/common/xmpp/idlequeue.py b/src/common/xmpp/idlequeue.py index 955af1be1..e9c4f0fde 100644 --- a/src/common/xmpp/idlequeue.py +++ b/src/common/xmpp/idlequeue.py @@ -33,7 +33,7 @@ class IdleObject: ''' called on new write event (connect in sockets is a pollout) ''' pass - def read_timeout(self, fd): + def read_timeout(self): ''' called when timeout has happend ''' pass diff --git a/src/common/xmpp/protocol.py b/src/common/xmpp/protocol.py index a9140c8f9..797a8e9af 100644 --- a/src/common/xmpp/protocol.py +++ b/src/common/xmpp/protocol.py @@ -171,27 +171,27 @@ temporary-auth-failure -- -- -- The authentication failed because of a tempora ERRORS,_errorcodes={},{} for ns,errname,errpool in [(NS_XMPP_STREAMS,'STREAM',xmpp_stream_error_conditions), - (NS_STANZAS ,'ERR' ,xmpp_stanza_error_conditions), - (NS_SASL ,'SASL' ,sasl_error_conditions)]: - for err in errpool.split('\n')[1:]: - cond,code,typ,text=err.split(' -- ') - name=errname+'_'+cond.upper().replace('-','_') - locals()[name]=ns+' '+cond - ERRORS[ns+' '+cond]=[code,typ,text] - if code: _errorcodes[code]=cond + (NS_STANZAS ,'ERR' ,xmpp_stanza_error_conditions), + (NS_SASL ,'SASL' ,sasl_error_conditions)]: + for err in errpool.split('\n')[1:]: + cond,code,typ,text=err.split(' -- ') + name=errname+'_'+cond.upper().replace('-','_') + locals()[name]=ns+' '+cond + ERRORS[ns+' '+cond]=[code,typ,text] + if code: _errorcodes[code]=cond del ns,errname,errpool,err,cond,code,typ,text def isResultNode(node): - """ Returns true if the node is a positive reply. """ - return node and node.getType()=='result' + """ Returns true if the node is a positive reply. """ + return node and node.getType()=='result' def isErrorNode(node): - """ Returns true if the node is a negative reply. """ - return node and node.getType()=='error' + """ Returns true if the node is a negative reply. """ + return node and node.getType()=='error' class NodeProcessed(Exception): - """ Exception that should be raised by handler when the handling should be stopped. """ + """ Exception that should be raised by handler when the handling should be stopped. """ class StreamError(Exception): - """ Base exception class for stream errors.""" + """ Base exception class for stream errors.""" class BadFormat(StreamError): pass class BadNamespacePrefix(StreamError): pass class Conflict(StreamError): pass @@ -218,559 +218,570 @@ class UnsupportedVersion(StreamError): pass class XMLNotWellFormed(StreamError): pass stream_exceptions = {'bad-format': BadFormat, - 'bad-namespace-prefix': BadNamespacePrefix, - 'conflict': Conflict, - 'connection-timeout': ConnectionTimeout, - 'host-gone': HostGone, - 'host-unknown': HostUnknown, - 'improper-addressing': ImproperAddressing, - 'internal-server-error': InternalServerError, - 'invalid-from': InvalidFrom, - 'invalid-id': InvalidID, - 'invalid-namespace': InvalidNamespace, - 'invalid-xml': InvalidXML, - 'not-authorized': NotAuthorized, - 'policy-violation': PolicyViolation, - 'remote-connection-failed': RemoteConnectionFailed, - 'resource-constraint': ResourceConstraint, - 'restricted-xml': RestrictedXML, - 'see-other-host': SeeOtherHost, - 'system-shutdown': SystemShutdown, - 'undefined-condition': UndefinedCondition, - 'unsupported-encoding': UnsupportedEncoding, - 'unsupported-stanza-type': UnsupportedStanzaType, - 'unsupported-version': UnsupportedVersion, - 'xml-not-well-formed': XMLNotWellFormed} + 'bad-namespace-prefix': BadNamespacePrefix, + 'conflict': Conflict, + 'connection-timeout': ConnectionTimeout, + 'host-gone': HostGone, + 'host-unknown': HostUnknown, + 'improper-addressing': ImproperAddressing, + 'internal-server-error': InternalServerError, + 'invalid-from': InvalidFrom, + 'invalid-id': InvalidID, + 'invalid-namespace': InvalidNamespace, + 'invalid-xml': InvalidXML, + 'not-authorized': NotAuthorized, + 'policy-violation': PolicyViolation, + 'remote-connection-failed': RemoteConnectionFailed, + 'resource-constraint': ResourceConstraint, + 'restricted-xml': RestrictedXML, + 'see-other-host': SeeOtherHost, + 'system-shutdown': SystemShutdown, + 'undefined-condition': UndefinedCondition, + 'unsupported-encoding': UnsupportedEncoding, + 'unsupported-stanza-type': UnsupportedStanzaType, + 'unsupported-version': UnsupportedVersion, + 'xml-not-well-formed': XMLNotWellFormed} class JID: - """ JID object. JID can be built from string, modified, compared, serialised into string. """ - def __init__(self, jid=None, node='', domain='', resource=''): - """ Constructor. JID can be specified as string (jid argument) or as separate parts. - Examples: - JID('node@domain/resource') - JID(node='node',domain='domain.org') - """ - if not jid and not domain: raise ValueError('JID must contain at least domain name') - elif type(jid)==type(self): self.node,self.domain,self.resource=jid.node,jid.domain,jid.resource - elif domain: self.node,self.domain,self.resource=node,domain,resource - else: - if jid.find('@')+1: self.node,jid=jid.split('@',1) - else: self.node='' - if jid.find('/')+1: self.domain,self.resource=jid.split('/',1) - else: self.domain,self.resource=jid,'' - def getNode(self): - """ Return the node part of the JID """ - return self.node - def setNode(self,node): - """ Set the node part of the JID to new value. Specify None to remove the node part.""" - self.node=node.lower() - def getDomain(self): - """ Return the domain part of the JID """ - return self.domain - def setDomain(self,domain): - """ Set the domain part of the JID to new value.""" - self.domain=domain.lower() - def getResource(self): - """ Return the resource part of the JID """ - return self.resource - def setResource(self,resource): - """ Set the resource part of the JID to new value. Specify None to remove the resource part.""" - self.resource=resource - def getStripped(self): - """ Return the bare representation of JID. I.e. string value w/o resource. """ - return self.__str__(0) - def __eq__(self, other): - """ Compare the JID to another instance or to string for equality. """ - try: other=JID(other) - except ValueError: return 0 - return self.resource==other.resource and self.__str__(0) == other.__str__(0) - def __ne__(self, other): - """ Compare the JID to another instance or to string for non-equality. """ - return not self.__eq__(other) - def bareMatch(self, other): - """ Compare the node and domain parts of the JID's for equality. """ - return self.__str__(0) == JID(other).__str__(0) - def __str__(self,wresource=1): - """ Serialise JID into string. """ - if self.node: jid=self.node+'@'+self.domain - else: jid=self.domain - if wresource and self.resource: return jid+'/'+self.resource - return jid - def __hash__(self): - """ Produce hash of the JID, Allows to use JID objects as keys of the dictionary. """ - return hash(self.__str__()) + """ JID object. JID can be built from string, modified, compared, serialised into string. """ + def __init__(self, jid=None, node='', domain='', resource=''): + """ Constructor. JID can be specified as string (jid argument) or as separate parts. + Examples: + JID('node@domain/resource') + JID(node='node',domain='domain.org') + """ + if not jid and not domain: raise ValueError('JID must contain at least domain name') + elif type(jid)==type(self): self.node,self.domain,self.resource=jid.node,jid.domain,jid.resource + elif domain: self.node,self.domain,self.resource=node,domain,resource + else: + if jid.find('@')+1: self.node,jid=jid.split('@',1) + else: self.node='' + if jid.find('/')+1: self.domain,self.resource=jid.split('/',1) + else: self.domain,self.resource=jid,'' + def getNode(self): + """ Return the node part of the JID """ + return self.node + def setNode(self,node): + """ Set the node part of the JID to new value. Specify None to remove the node part.""" + self.node=node.lower() + def getDomain(self): + """ Return the domain part of the JID """ + return self.domain + def setDomain(self,domain): + """ Set the domain part of the JID to new value.""" + self.domain=domain.lower() + def getResource(self): + """ Return the resource part of the JID """ + return self.resource + def setResource(self,resource): + """ Set the resource part of the JID to new value. Specify None to remove the resource part.""" + self.resource=resource + def getStripped(self): + """ Return the bare representation of JID. I.e. string value w/o resource. """ + return self.__str__(0) + def __eq__(self, other): + """ Compare the JID to another instance or to string for equality. """ + try: other=JID(other) + except ValueError: return 0 + return self.resource==other.resource and self.__str__(0) == other.__str__(0) + def __ne__(self, other): + """ Compare the JID to another instance or to string for non-equality. """ + return not self.__eq__(other) + def bareMatch(self, other): + """ Compare the node and domain parts of the JID's for equality. """ + return self.__str__(0) == JID(other).__str__(0) + def __str__(self,wresource=1): + """ Serialise JID into string. """ + if self.node: jid=self.node+'@'+self.domain + else: jid=self.domain + if wresource and self.resource: return jid+'/'+self.resource + return jid + def __hash__(self): + """ Produce hash of the JID, Allows to use JID objects as keys of the dictionary. """ + return hash(self.__str__()) + + class Protocol(Node): - """ A "stanza" object class. Contains methods that are common for presences, iqs and messages. """ - def __init__(self, name=None, to=None, typ=None, frm=None, attrs={}, payload=[], timestamp=None, xmlns=None, node=None): - """ Constructor, name is the name of the stanza i.e. 'message' or 'presence' or 'iq'. - to is the value of 'to' attribure, 'typ' - 'type' attribute - frn - from attribure, attrs - other attributes mapping, payload - same meaning as for simplexml payload definition - timestamp - the time value that needs to be stamped over stanza - xmlns - namespace of top stanza node - node - parsed or unparsed stana to be taken as prototype. - """ - if not attrs: attrs={} - if to: attrs['to']=to - if frm: attrs['from']=frm - if typ: attrs['type']=typ - Node.__init__(self, tag=name, attrs=attrs, payload=payload, node=node) - if not node and xmlns: self.setNamespace(xmlns) - if self['to']: self.setTo(self['to']) - if self['from']: self.setFrom(self['from']) - if node and type(self)==type(node) and self.__class__==node.__class__ and self.attrs.has_key('id'): del self.attrs['id'] - self.timestamp=None - for x in self.getTags('x',namespace=NS_DELAY): - try: - if x.getAttr('stamp')'text': return tag.getName() - return errtag.getData() - def getErrorMsg(self): - """ Return the textual description of the error (if present) or the error condition """ - errtag=self.getTag('error') - if errtag: - for tag in errtag.getChildren(): - if tag.getName()=='text': return tag.getData() - return self.getError() - def getErrorCode(self): - """ Return the error code. Obsolete. """ - return self.getTagAttr('error','code') - def setError(self,error,code=None): - """ Set the error code. Obsolete. Use error-conditions instead. """ - if code: - if str(code) in _errorcodes.keys(): error=ErrorNode(_errorcodes[str(code)],text=error) - else: error=ErrorNode(ERR_UNDEFINED_CONDITION,code=code,typ='cancel',text=error) - elif type(error) in [type(''),type(u'')]: error=ErrorNode(error) - self.setType('error') - self.addChild(node=error) - def setTimestamp(self,val=None): - """Set the timestamp. timestamp should be the yyyymmddThhmmss string.""" - if not val: val=time.strftime('%Y%m%dT%H:%M:%S', time.gmtime()) - self.timestamp=val - self.setTag('x',{'stamp':self.timestamp},namespace=NS_DELAY) - def getProperties(self): - """ Return the list of namespaces to which belongs the direct childs of element""" - props=[] - for child in self.getChildren(): - prop=child.getNamespace() - if prop not in props: props.append(prop) - return props - def __setitem__(self,item,val): - """ Set the item 'item' to the value 'val'.""" - if item in ['to','from']: val=JID(val) - return self.setAttr(item,val) + """ A "stanza" object class. Contains methods that are common for presences, iqs and messages. """ + def __init__(self, name=None, to=None, typ=None, frm=None, attrs={}, payload=[], timestamp=None, xmlns=None, node=None): + """ Constructor, name is the name of the stanza i.e. 'message' or 'presence' or 'iq'. + to is the value of 'to' attribure, 'typ' - 'type' attribute + frn - from attribure, attrs - other attributes mapping, + payload - same meaning as for simplexml payload definition + timestamp - the time value that needs to be stamped over stanza + xmlns - namespace of top stanza node + node - parsed or unparsed stana to be taken as prototype. + """ + if not attrs: attrs={} + if to: attrs['to']=to + if frm: attrs['from']=frm + if typ: attrs['type']=typ + Node.__init__(self, tag=name, attrs=attrs, payload=payload, node=node) + if not node and xmlns: self.setNamespace(xmlns) + if self['to']: self.setTo(self['to']) + if self['from']: self.setFrom(self['from']) + if node and type(self)==type(node) and self.__class__==node.__class__ and self.attrs.has_key('id'): del self.attrs['id'] + self.timestamp=None + for x in self.getTags('x',namespace=NS_DELAY): + try: + if x.getAttr('stamp')'text': return tag.getName() + return errtag.getData() + def getErrorMsg(self): + """ Return the textual description of the error (if present) or the error condition """ + errtag=self.getTag('error') + if errtag: + for tag in errtag.getChildren(): + if tag.getName()=='text': return tag.getData() + return self.getError() + def getErrorCode(self): + """ Return the error code. Obsolete. """ + return self.getTagAttr('error','code') + def setError(self,error,code=None): + """ Set the error code. Obsolete. Use error-conditions instead. """ + if code: + if str(code) in _errorcodes.keys(): error=ErrorNode(_errorcodes[str(code)],text=error) + else: error=ErrorNode(ERR_UNDEFINED_CONDITION,code=code,typ='cancel',text=error) + elif type(error) in [type(''),type(u'')]: error=ErrorNode(error) + self.setType('error') + self.addChild(node=error) + def setTimestamp(self,val=None): + """Set the timestamp. timestamp should be the yyyymmddThhmmss string.""" + if not val: val=time.strftime('%Y%m%dT%H:%M:%S', time.gmtime()) + self.timestamp=val + self.setTag('x',{'stamp':self.timestamp},namespace=NS_DELAY) + def getProperties(self): + """ Return the list of namespaces to which belongs the direct childs of element""" + props=[] + for child in self.getChildren(): + prop=child.getNamespace() + if prop not in props: props.append(prop) + return props + def __setitem__(self,item,val): + """ Set the item 'item' to the value 'val'.""" + if item in ['to','from']: val=JID(val) + return self.setAttr(item,val) + +class BOSHBody(Protocol): + ''' + tag that wraps usual XMPP stanzas in BOSH + ''' + def __init__(self, to=None, frm=None, attrs={}, payload=[], node=None): + Protocol.__init__(self, name='body', to=to, frm=frm, attrs=attrs, + payload=payload, xmlns=NS_HTTP_BIND, node=node) class Message(Protocol): - """ XMPP Message stanza - "push" mechanism.""" - def __init__(self, to=None, body=None, xhtml=None, typ=None, subject=None, attrs={}, frm=None, payload=[], timestamp=None, xmlns=NS_CLIENT, node=None): - """ Create message object. You can specify recipient, text of message, type of message - any additional attributes, sender of the message, any additional payload (f.e. jabber:x:delay element) and namespace in one go. - Alternatively you can pass in the other XML object as the 'node' parameted to replicate it as message. """ - Protocol.__init__(self, 'message', to=to, typ=typ, attrs=attrs, frm=frm, payload=payload, timestamp=timestamp, xmlns=xmlns, node=node) - if body: self.setBody(body) - if xhtml: self.setXHTML(xhtml) - if subject is not None: self.setSubject(subject) - def getBody(self): - """ Returns text of the message. """ - return self.getTagData('body') - def getXHTML(self, xmllang=None): - """ Returns serialized xhtml-im element text of the message. + """ XMPP Message stanza - "push" mechanism.""" + def __init__(self, to=None, body=None, xhtml=None, typ=None, subject=None, attrs={}, frm=None, payload=[], timestamp=None, xmlns=NS_CLIENT, node=None): + """ Create message object. You can specify recipient, text of message, type of message + any additional attributes, sender of the message, any additional payload (f.e. jabber:x:delay element) and namespace in one go. + Alternatively you can pass in the other XML object as the 'node' parameted to replicate it as message. """ + Protocol.__init__(self, 'message', to=to, typ=typ, attrs=attrs, frm=frm, payload=payload, timestamp=timestamp, xmlns=xmlns, node=node) + if body: self.setBody(body) + if xhtml: self.setXHTML(xhtml) + if subject is not None: self.setSubject(subject) + def getBody(self): + """ Returns text of the message. """ + return self.getTagData('body') + def getXHTML(self, xmllang=None): + """ Returns serialized xhtml-im element text of the message. - TODO: Returning a DOM could make rendering faster.""" - xhtml = self.getTag('html') - if xhtml: - if xmllang: - body = xhtml.getTag('body', attrs={'xml:lang':xmllang}) - else: - body = xhtml.getTag('body') - return str(body) - return None - def getSubject(self): - """ Returns subject of the message. """ - return self.getTagData('subject') - def getThread(self): - """ Returns thread of the message. """ - return self.getTagData('thread') - def setBody(self,val): - """ Sets the text of the message. """ - self.setTagData('body',val) + TODO: Returning a DOM could make rendering faster.""" + xhtml = self.getTag('html') + if xhtml: + if xmllang: + body = xhtml.getTag('body', attrs={'xml:lang':xmllang}) + else: + body = xhtml.getTag('body') + return str(body) + return None + def getSubject(self): + """ Returns subject of the message. """ + return self.getTagData('subject') + def getThread(self): + """ Returns thread of the message. """ + return self.getTagData('thread') + def setBody(self,val): + """ Sets the text of the message. """ + self.setTagData('body',val) - def setXHTML(self,val,xmllang=None): - """ Sets the xhtml text of the message (XEP-0071). - The parameter is the "inner html" to the body.""" - try: - if xmllang: - dom = NodeBuilder('' + val + '').getDom() - else: - dom = NodeBuilder(''+val+'',0).getDom() - if self.getTag('html'): - self.getTag('html').addChild(node=dom) - else: - self.setTag('html',namespace=NS_XHTML_IM).addChild(node=dom) - except Exception, e: - print "Error", e - pass #FIXME: log. we could not set xhtml (parse error, whatever) - def setSubject(self,val): - """ Sets the subject of the message. """ - self.setTagData('subject',val) - def setThread(self,val): - """ Sets the thread of the message. """ - self.setTagData('thread',val) - def buildReply(self,text=None): - """ Builds and returns another message object with specified text. - The to, from and thread properties of new message are pre-set as reply to this message. """ - m=Message(to=self.getFrom(),frm=self.getTo(),body=text,node=self) - th=self.getThread() - if th: m.setThread(th) - return m - def getStatusCode(self): - """Returns the status code of the message (for groupchat config - change)""" - attrs = [] - for xtag in self.getTags('x'): - for child in xtag.getTags('status'): - attrs.append(child.getAttr('code')) - return attrs + def setXHTML(self,val,xmllang=None): + """ Sets the xhtml text of the message (XEP-0071). + The parameter is the "inner html" to the body.""" + try: + if xmllang: + dom = NodeBuilder('' + val + '').getDom() + else: + dom = NodeBuilder(''+val+'',0).getDom() + if self.getTag('html'): + self.getTag('html').addChild(node=dom) + else: + self.setTag('html',namespace=NS_XHTML_IM).addChild(node=dom) + except Exception, e: + print "Error", e + pass #FIXME: log. we could not set xhtml (parse error, whatever) + def setSubject(self,val): + """ Sets the subject of the message. """ + self.setTagData('subject',val) + def setThread(self,val): + """ Sets the thread of the message. """ + self.setTagData('thread',val) + def buildReply(self,text=None): + """ Builds and returns another message object with specified text. + The to, from and thread properties of new message are pre-set as reply to this message. """ + m=Message(to=self.getFrom(),frm=self.getTo(),body=text,node=self) + th=self.getThread() + if th: m.setThread(th) + return m + def getStatusCode(self): + """Returns the status code of the message (for groupchat config + change)""" + attrs = [] + for xtag in self.getTags('x'): + for child in xtag.getTags('status'): + attrs.append(child.getAttr('code')) + return attrs class Presence(Protocol): - """ XMPP Presence object.""" - def __init__(self, to=None, typ=None, priority=None, show=None, status=None, attrs={}, frm=None, timestamp=None, payload=[], xmlns=NS_CLIENT, node=None): - """ Create presence object. You can specify recipient, type of message, priority, show and status values - any additional attributes, sender of the presence, timestamp, any additional payload (f.e. jabber:x:delay element) and namespace in one go. - Alternatively you can pass in the other XML object as the 'node' parameted to replicate it as presence. """ - Protocol.__init__(self, 'presence', to=to, typ=typ, attrs=attrs, frm=frm, payload=payload, timestamp=timestamp, xmlns=xmlns, node=node) - if priority: self.setPriority(priority) - if show: self.setShow(show) - if status: self.setStatus(status) - def getPriority(self): - """ Returns the priority of the message. """ - return self.getTagData('priority') - def getShow(self): - """ Returns the show value of the message. """ - return self.getTagData('show') - def getStatus(self): - """ Returns the status string of the message. """ - return self.getTagData('status') - def setPriority(self,val): - """ Sets the priority of the message. """ - self.setTagData('priority',val) - def setShow(self,val): - """ Sets the show value of the message. """ - self.setTagData('show',val) - def setStatus(self,val): - """ Sets the status string of the message. """ - self.setTagData('status',val) + """ XMPP Presence object.""" + def __init__(self, to=None, typ=None, priority=None, show=None, status=None, attrs={}, frm=None, timestamp=None, payload=[], xmlns=NS_CLIENT, node=None): + """ Create presence object. You can specify recipient, type of message, priority, show and status values + any additional attributes, sender of the presence, timestamp, any additional payload (f.e. jabber:x:delay element) and namespace in one go. + Alternatively you can pass in the other XML object as the 'node' parameted to replicate it as presence. """ + Protocol.__init__(self, 'presence', to=to, typ=typ, attrs=attrs, frm=frm, payload=payload, timestamp=timestamp, xmlns=xmlns, node=node) + if priority: self.setPriority(priority) + if show: self.setShow(show) + if status: self.setStatus(status) + def getPriority(self): + """ Returns the priority of the message. """ + return self.getTagData('priority') + def getShow(self): + """ Returns the show value of the message. """ + return self.getTagData('show') + def getStatus(self): + """ Returns the status string of the message. """ + return self.getTagData('status') + def setPriority(self,val): + """ Sets the priority of the message. """ + self.setTagData('priority',val) + def setShow(self,val): + """ Sets the show value of the message. """ + self.setTagData('show',val) + def setStatus(self,val): + """ Sets the status string of the message. """ + self.setTagData('status',val) - def _muc_getItemAttr(self,tag,attr): - for xtag in self.getTags('x'): - for child in xtag.getTags(tag): - return child.getAttr(attr) - def _muc_getSubTagDataAttr(self,tag,attr): - for xtag in self.getTags('x'): - for child in xtag.getTags('item'): - for cchild in child.getTags(tag): - return cchild.getData(),cchild.getAttr(attr) - return None,None - def getRole(self): - """Returns the presence role (for groupchat)""" - return self._muc_getItemAttr('item','role') - def getAffiliation(self): - """Returns the presence affiliation (for groupchat)""" - return self._muc_getItemAttr('item','affiliation') - def getNewNick(self): - """Returns the status code of the presence (for groupchat)""" - return self._muc_getItemAttr('item','nick') - def getJid(self): - """Returns the presence jid (for groupchat)""" - return self._muc_getItemAttr('item','jid') - def getReason(self): - """Returns the reason of the presence (for groupchat)""" - return self._muc_getSubTagDataAttr('reason','')[0] - def getActor(self): - """Returns the reason of the presence (for groupchat)""" - return self._muc_getSubTagDataAttr('actor','jid')[1] - def getStatusCode(self): - """Returns the status code of the presence (for groupchat)""" - attrs = [] - for xtag in self.getTags('x'): - for child in xtag.getTags('status'): - attrs.append(child.getAttr('code')) - return attrs + def _muc_getItemAttr(self,tag,attr): + for xtag in self.getTags('x'): + for child in xtag.getTags(tag): + return child.getAttr(attr) + def _muc_getSubTagDataAttr(self,tag,attr): + for xtag in self.getTags('x'): + for child in xtag.getTags('item'): + for cchild in child.getTags(tag): + return cchild.getData(),cchild.getAttr(attr) + return None,None + def getRole(self): + """Returns the presence role (for groupchat)""" + return self._muc_getItemAttr('item','role') + def getAffiliation(self): + """Returns the presence affiliation (for groupchat)""" + return self._muc_getItemAttr('item','affiliation') + def getNewNick(self): + """Returns the status code of the presence (for groupchat)""" + return self._muc_getItemAttr('item','nick') + def getJid(self): + """Returns the presence jid (for groupchat)""" + return self._muc_getItemAttr('item','jid') + def getReason(self): + """Returns the reason of the presence (for groupchat)""" + return self._muc_getSubTagDataAttr('reason','')[0] + def getActor(self): + """Returns the reason of the presence (for groupchat)""" + return self._muc_getSubTagDataAttr('actor','jid')[1] + def getStatusCode(self): + """Returns the status code of the presence (for groupchat)""" + attrs = [] + for xtag in self.getTags('x'): + for child in xtag.getTags('status'): + attrs.append(child.getAttr('code')) + return attrs class Iq(Protocol): - """ XMPP Iq object - get/set dialog mechanism. """ - def __init__(self, typ=None, queryNS=None, attrs={}, to=None, frm=None, payload=[], xmlns=NS_CLIENT, node=None): - """ Create Iq object. You can specify type, query namespace - any additional attributes, recipient of the iq, sender of the iq, any additional payload (f.e. jabber:x:data node) and namespace in one go. - Alternatively you can pass in the other XML object as the 'node' parameted to replicate it as an iq. """ - Protocol.__init__(self, 'iq', to=to, typ=typ, attrs=attrs, frm=frm, xmlns=xmlns, node=node) - if payload: self.setQueryPayload(payload) - if queryNS: self.setQueryNS(queryNS) - def getQueryNS(self): - """ Return the namespace of the 'query' child element.""" - tag=self.getTag('query') - if tag: return tag.getNamespace() - def getQuerynode(self): - """ Return the 'node' attribute value of the 'query' child element.""" - return self.getTagAttr('query','node') - def getQueryPayload(self): - """ Return the 'query' child element payload.""" - tag=self.getTag('query') - if tag: return tag.getPayload() - def getQueryChildren(self): - """ Return the 'query' child element child nodes.""" - tag=self.getTag('query') - if tag: return tag.getChildren() - def setQueryNS(self,namespace): - """ Set the namespace of the 'query' child element.""" - self.setTag('query').setNamespace(namespace) - def setQueryPayload(self,payload): - """ Set the 'query' child element payload.""" - self.setTag('query').setPayload(payload) - def setQuerynode(self,node): - """ Set the 'node' attribute value of the 'query' child element.""" - self.setTagAttr('query','node',node) - def buildReply(self,typ): - """ Builds and returns another Iq object of specified type. - The to, from and query child node of new Iq are pre-set as reply to this Iq. """ - iq=Iq(typ,to=self.getFrom(),frm=self.getTo(),attrs={'id':self.getID()}) - if self.getTag('query'): iq.setQueryNS(self.getQueryNS()) - return iq + """ XMPP Iq object - get/set dialog mechanism. """ + def __init__(self, typ=None, queryNS=None, attrs={}, to=None, frm=None, payload=[], xmlns=NS_CLIENT, node=None): + """ Create Iq object. You can specify type, query namespace + any additional attributes, recipient of the iq, sender of the iq, any additional payload (f.e. jabber:x:data node) and namespace in one go. + Alternatively you can pass in the other XML object as the 'node' parameted to replicate it as an iq. """ + Protocol.__init__(self, 'iq', to=to, typ=typ, attrs=attrs, frm=frm, xmlns=xmlns, node=node) + if payload: self.setQueryPayload(payload) + if queryNS: self.setQueryNS(queryNS) + def getQueryNS(self): + """ Return the namespace of the 'query' child element.""" + tag=self.getTag('query') + if tag: return tag.getNamespace() + def getQuerynode(self): + """ Return the 'node' attribute value of the 'query' child element.""" + return self.getTagAttr('query','node') + def getQueryPayload(self): + """ Return the 'query' child element payload.""" + tag=self.getTag('query') + if tag: return tag.getPayload() + def getQueryChildren(self): + """ Return the 'query' child element child nodes.""" + tag=self.getTag('query') + if tag: return tag.getChildren() + def setQueryNS(self,namespace): + """ Set the namespace of the 'query' child element.""" + self.setTag('query').setNamespace(namespace) + def setQueryPayload(self,payload): + """ Set the 'query' child element payload.""" + self.setTag('query').setPayload(payload) + def setQuerynode(self,node): + """ Set the 'node' attribute value of the 'query' child element.""" + self.setTagAttr('query','node',node) + def buildReply(self,typ): + """ Builds and returns another Iq object of specified type. + The to, from and query child node of new Iq are pre-set as reply to this Iq. """ + iq=Iq(typ,to=self.getFrom(),frm=self.getTo(),attrs={'id':self.getID()}) + if self.getTag('query'): iq.setQueryNS(self.getQueryNS()) + return iq class ErrorNode(Node): - """ XMPP-style error element. - In the case of stanza error should be attached to XMPP stanza. - In the case of stream-level errors should be used separately. """ - def __init__(self,name,code=None,typ=None,text=None): - """ Create new error node object. - Mandatory parameter: name - name of error condition. - Optional parameters: code, typ, text. Used for backwards compartibility with older jabber protocol.""" - if ERRORS.has_key(name): - cod,type,txt=ERRORS[name] - ns=name.split()[0] - else: cod,ns,type,txt='500',NS_STANZAS,'cancel','' - if typ: type=typ - if code: cod=code - if text: txt=text - Node.__init__(self,'error',{},[Node(name)]) - if type: self.setAttr('type',type) - if not cod: self.setName('stream:error') - if txt: self.addChild(node=Node(ns+' text',{},[txt])) - if cod: self.setAttr('code',cod) + """ XMPP-style error element. + In the case of stanza error should be attached to XMPP stanza. + In the case of stream-level errors should be used separately. """ + def __init__(self,name,code=None,typ=None,text=None): + """ Create new error node object. + Mandatory parameter: name - name of error condition. + Optional parameters: code, typ, text. Used for backwards compartibility with older jabber protocol.""" + if ERRORS.has_key(name): + cod,type,txt=ERRORS[name] + ns=name.split()[0] + else: cod,ns,type,txt='500',NS_STANZAS,'cancel','' + if typ: type=typ + if code: cod=code + if text: txt=text + Node.__init__(self,'error',{},[Node(name)]) + if type: self.setAttr('type',type) + if not cod: self.setName('stream:error') + if txt: self.addChild(node=Node(ns+' text',{},[txt])) + if cod: self.setAttr('code',cod) class Error(Protocol): - """ Used to quickly transform received stanza into error reply.""" - def __init__(self,node,error,reply=1): - """ Create error reply basing on the received 'node' stanza and the 'error' error condition. - If the 'node' is not the received stanza but locally created ('to' and 'from' fields needs not swapping) - specify the 'reply' argument as false.""" - if reply: Protocol.__init__(self,to=node.getFrom(),frm=node.getTo(),node=node) - else: Protocol.__init__(self,node=node) - self.setError(error) - if node.getType()=='error': self.__str__=self.__dupstr__ - def __dupstr__(self,dup1=None,dup2=None): - """ Dummy function used as preventor of creating error node in reply to error node. - I.e. you will not be able to serialise "double" error into string. - """ - return '' + """ Used to quickly transform received stanza into error reply.""" + def __init__(self,node,error,reply=1): + """ Create error reply basing on the received 'node' stanza and the 'error' error condition. + If the 'node' is not the received stanza but locally created ('to' and 'from' fields needs not swapping) + specify the 'reply' argument as false.""" + if reply: Protocol.__init__(self,to=node.getFrom(),frm=node.getTo(),node=node) + else: Protocol.__init__(self,node=node) + self.setError(error) + if node.getType()=='error': self.__str__=self.__dupstr__ + def __dupstr__(self,dup1=None,dup2=None): + """ Dummy function used as preventor of creating error node in reply to error node. + I.e. you will not be able to serialise "double" error into string. + """ + return '' class DataField(Node): - """ This class is used in the DataForm class to describe the single data item. - If you are working with jabber:x:data (XEP-0004, XEP-0068, XEP-0122) - then you will need to work with instances of this class. """ - def __init__(self,name=None,value=None,typ=None,required=0,desc=None,options=[],node=None): - """ Create new data field of specified name,value and type. - Also 'required','desc' and 'options' fields can be set. - Alternatively other XML object can be passed in as the 'node' parameted to replicate it as a new datafiled. - """ - Node.__init__(self,'field',node=node) - if name: self.setVar(name) - if type(value) in [list,tuple]: self.setValues(value) - elif value: self.setValue(value) - if typ: self.setType(typ) - elif not typ and not node: self.setType('text-single') - if required: self.setRequired(required) - if desc: self.setDesc(desc) - if options: self.setOptions(options) - def setRequired(self,req=1): - """ Change the state of the 'required' flag. """ - if req: self.setTag('required') - else: - try: self.delChild('required') - except ValueError: return - def isRequired(self): - """ Returns in this field a required one. """ - return self.getTag('required') - def setDesc(self,desc): - """ Set the description of this field. """ - self.setTagData('desc',desc) - def getDesc(self): - """ Return the description of this field. """ - return self.getTagData('desc') - def setValue(self,val): - """ Set the value of this field. """ - self.setTagData('value',val) - def getValue(self): - return self.getTagData('value') - def setValues(self,lst): - """ Set the values of this field as values-list. - Replaces all previous filed values! If you need to just add a value - use addValue method.""" - while self.getTag('value'): self.delChild('value') - for val in lst: self.addValue(val) - def addValue(self,val): - """ Add one more value to this field. Used in 'get' iq's or such.""" - self.addChild('value',{},[val]) - def getValues(self): - """ Return the list of values associated with this field.""" - ret=[] - for tag in self.getTags('value'): ret.append(tag.getData()) - return ret - def getOptions(self): - """ Return label-option pairs list associated with this field.""" - ret=[] - for tag in self.getTags('option'): ret.append([tag.getAttr('label'),tag.getTagData('value')]) - return ret - def setOptions(self,lst): - """ Set label-option pairs list associated with this field.""" - while self.getTag('option'): self.delChild('option') - for opt in lst: self.addOption(opt) - def addOption(self,opt): - """ Add one more label-option pair to this field.""" - if type(opt) in [str,unicode]: self.addChild('option').setTagData('value',opt) - else: self.addChild('option',{'label':opt[0]}).setTagData('value',opt[1]) - def getType(self): - """ Get type of this field. """ - return self.getAttr('type') - def setType(self,val): - """ Set type of this field. """ - return self.setAttr('type',val) - def getVar(self): - """ Get 'var' attribute value of this field. """ - return self.getAttr('var') - def setVar(self,val): - """ Set 'var' attribute value of this field. """ - return self.setAttr('var',val) + """ This class is used in the DataForm class to describe the single data item. + If you are working with jabber:x:data (XEP-0004, XEP-0068, XEP-0122) + then you will need to work with instances of this class. """ + def __init__(self,name=None,value=None,typ=None,required=0,desc=None,options=[],node=None): + """ Create new data field of specified name,value and type. + Also 'required','desc' and 'options' fields can be set. + Alternatively other XML object can be passed in as the 'node' parameted to replicate it as a new datafiled. + """ + Node.__init__(self,'field',node=node) + if name: self.setVar(name) + if type(value) in [list,tuple]: self.setValues(value) + elif value: self.setValue(value) + if typ: self.setType(typ) + elif not typ and not node: self.setType('text-single') + if required: self.setRequired(required) + if desc: self.setDesc(desc) + if options: self.setOptions(options) + def setRequired(self,req=1): + """ Change the state of the 'required' flag. """ + if req: self.setTag('required') + else: + try: self.delChild('required') + except ValueError: return + def isRequired(self): + """ Returns in this field a required one. """ + return self.getTag('required') + def setDesc(self,desc): + """ Set the description of this field. """ + self.setTagData('desc',desc) + def getDesc(self): + """ Return the description of this field. """ + return self.getTagData('desc') + def setValue(self,val): + """ Set the value of this field. """ + self.setTagData('value',val) + def getValue(self): + return self.getTagData('value') + def setValues(self,lst): + """ Set the values of this field as values-list. + Replaces all previous filed values! If you need to just add a value - use addValue method.""" + while self.getTag('value'): self.delChild('value') + for val in lst: self.addValue(val) + def addValue(self,val): + """ Add one more value to this field. Used in 'get' iq's or such.""" + self.addChild('value',{},[val]) + def getValues(self): + """ Return the list of values associated with this field.""" + ret=[] + for tag in self.getTags('value'): ret.append(tag.getData()) + return ret + def getOptions(self): + """ Return label-option pairs list associated with this field.""" + ret=[] + for tag in self.getTags('option'): ret.append([tag.getAttr('label'),tag.getTagData('value')]) + return ret + def setOptions(self,lst): + """ Set label-option pairs list associated with this field.""" + while self.getTag('option'): self.delChild('option') + for opt in lst: self.addOption(opt) + def addOption(self,opt): + """ Add one more label-option pair to this field.""" + if type(opt) in [str,unicode]: self.addChild('option').setTagData('value',opt) + else: self.addChild('option',{'label':opt[0]}).setTagData('value',opt[1]) + def getType(self): + """ Get type of this field. """ + return self.getAttr('type') + def setType(self,val): + """ Set type of this field. """ + return self.setAttr('type',val) + def getVar(self): + """ Get 'var' attribute value of this field. """ + return self.getAttr('var') + def setVar(self,val): + """ Set 'var' attribute value of this field. """ + return self.setAttr('var',val) class DataForm(Node): - """ DataForm class. Used for manipulating dataforms in XMPP. - Relevant XEPs: 0004, 0068, 0122. - Can be used in disco, pub-sub and many other applications.""" - def __init__(self, typ=None, data=[], title=None, node=None): - """ - Create new dataform of type 'typ'. 'data' is the list of DataField - instances that this dataform contains, 'title' - the title string. - You can specify the 'node' argument as the other node to be used as - base for constructing this dataform. + """ DataForm class. Used for manipulating dataforms in XMPP. + Relevant XEPs: 0004, 0068, 0122. + Can be used in disco, pub-sub and many other applications.""" + def __init__(self, typ=None, data=[], title=None, node=None): + """ + Create new dataform of type 'typ'. 'data' is the list of DataField + instances that this dataform contains, 'title' - the title string. + You can specify the 'node' argument as the other node to be used as + base for constructing this dataform. - title and instructions is optional and SHOULD NOT contain newlines. - Several instructions MAY be present. - 'typ' can be one of ('form' | 'submit' | 'cancel' | 'result' ) - 'typ' of reply iq can be ( 'result' | 'set' | 'set' | 'result' ) respectively. - 'cancel' form can not contain any fields. All other forms contains AT LEAST one field. - 'title' MAY be included in forms of type "form" and "result" - """ - Node.__init__(self,'x',node=node) - if node: - newkids=[] - for n in self.getChildren(): - if n.getName()=='field': newkids.append(DataField(node=n)) - else: newkids.append(n) - self.kids=newkids - if typ: self.setType(typ) - self.setNamespace(NS_DATA) - if title: self.setTitle(title) - if type(data)==type({}): - newdata=[] - for name in data.keys(): newdata.append(DataField(name,data[name])) - data=newdata - for child in data: - if type(child) in [type(''),type(u'')]: self.addInstructions(child) - elif child.__class__.__name__=='DataField': self.kids.append(child) - else: self.kids.append(DataField(node=child)) - def getType(self): - """ Return the type of dataform. """ - return self.getAttr('type') - def setType(self,typ): - """ Set the type of dataform. """ - self.setAttr('type',typ) - def getTitle(self): - """ Return the title of dataform. """ - return self.getTagData('title') - def setTitle(self,text): - """ Set the title of dataform. """ - self.setTagData('title',text) - def getInstructions(self): - """ Return the instructions of dataform. """ - return self.getTagData('instructions') - def setInstructions(self,text): - """ Set the instructions of dataform. """ - self.setTagData('instructions',text) - def addInstructions(self,text): - """ Add one more instruction to the dataform. """ - self.addChild('instructions',{},[text]) - def getField(self,name): - """ Return the datafield object with name 'name' (if exists). """ - return self.getTag('field',attrs={'var':name}) - def setField(self,name): - """ Create if nessessary or get the existing datafield object with name 'name' and return it. """ - f=self.getField(name) - if f: return f - return self.addChild(node=DataField(name)) - def asDict(self): - """ Represent dataform as simple dictionary mapping of datafield names to their values.""" - ret={} - for field in self.getTags('field'): - name=field.getAttr('var') - typ=field.getType() - if type(typ) in [type(''),type(u'')] and typ[-6:]=='-multi': - val=[] - for i in field.getTags('value'): val.append(i.getData()) - else: val=field.getTagData('value') - ret[name]=val - if self.getTag('instructions'): ret['instructions']=self.getInstructions() - return ret - def __getitem__(self,name): - """ Simple dictionary interface for getting datafields values by their names.""" - item=self.getField(name) - if item: return item.getValue() - raise IndexError('No such field') - def __setitem__(self,name,val): - """ Simple dictionary interface for setting datafields values by their names.""" - return self.setField(name).setValue(val) + title and instructions is optional and SHOULD NOT contain newlines. + Several instructions MAY be present. + 'typ' can be one of ('form' | 'submit' | 'cancel' | 'result' ) + 'typ' of reply iq can be ( 'result' | 'set' | 'set' | 'result' ) respectively. + 'cancel' form can not contain any fields. All other forms contains AT LEAST one field. + 'title' MAY be included in forms of type "form" and "result" + """ + Node.__init__(self,'x',node=node) + if node: + newkids=[] + for n in self.getChildren(): + if n.getName()=='field': newkids.append(DataField(node=n)) + else: newkids.append(n) + self.kids=newkids + if typ: self.setType(typ) + self.setNamespace(NS_DATA) + if title: self.setTitle(title) + if type(data)==type({}): + newdata=[] + for name in data.keys(): newdata.append(DataField(name,data[name])) + data=newdata + for child in data: + if type(child) in [type(''),type(u'')]: self.addInstructions(child) + elif child.__class__.__name__=='DataField': self.kids.append(child) + else: self.kids.append(DataField(node=child)) + def getType(self): + """ Return the type of dataform. """ + return self.getAttr('type') + def setType(self,typ): + """ Set the type of dataform. """ + self.setAttr('type',typ) + def getTitle(self): + """ Return the title of dataform. """ + return self.getTagData('title') + def setTitle(self,text): + """ Set the title of dataform. """ + self.setTagData('title',text) + def getInstructions(self): + """ Return the instructions of dataform. """ + return self.getTagData('instructions') + def setInstructions(self,text): + """ Set the instructions of dataform. """ + self.setTagData('instructions',text) + def addInstructions(self,text): + """ Add one more instruction to the dataform. """ + self.addChild('instructions',{},[text]) + def getField(self,name): + """ Return the datafield object with name 'name' (if exists). """ + return self.getTag('field',attrs={'var':name}) + def setField(self,name): + """ Create if nessessary or get the existing datafield object with name 'name' and return it. """ + f=self.getField(name) + if f: return f + return self.addChild(node=DataField(name)) + def asDict(self): + """ Represent dataform as simple dictionary mapping of datafield names to their values.""" + ret={} + for field in self.getTags('field'): + name=field.getAttr('var') + typ=field.getType() + if type(typ) in [type(''),type(u'')] and typ[-6:]=='-multi': + val=[] + for i in field.getTags('value'): val.append(i.getData()) + else: val=field.getTagData('value') + ret[name]=val + if self.getTag('instructions'): ret['instructions']=self.getInstructions() + return ret + def __getitem__(self,name): + """ Simple dictionary interface for getting datafields values by their names.""" + item=self.getField(name) + if item: return item.getValue() + raise IndexError('No such field') + def __setitem__(self,name,val): + """ Simple dictionary interface for setting datafields values by their names.""" + return self.setField(name).setValue(val) diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py index a4f847764..4ca3c51aa 100644 --- a/src/common/xmpp/transports_nb.py +++ b/src/common/xmpp/transports_nb.py @@ -27,10 +27,34 @@ import errno import time import traceback -import thread +import threading import logging log = logging.getLogger('gajim.c.x.transports_nb') +consoleloghandler = logging.StreamHandler() +consoleloghandler.setLevel(logging.DEBUG) +consoleloghandler.setFormatter( + logging.Formatter('%(levelname)s: %(message)s') +) +log.setLevel(logging.DEBUG) +log.addHandler(consoleloghandler) +log.propagate = False + + +def urisplit(self, uri): + ''' + Function for splitting URI string to tuple (protocol, host, path). + e.g. urisplit('http://httpcm.jabber.org/webclient') returns + ('http', 'httpcm.jabber.org', '/webclient') + ''' + import re + regex = '(([^:/]+)(://))?([^/]*)(/?.*)' + grouped = re.match(regex, uri).groups() + proto, host, path = grouped[1], grouped[3], grouped[4] + return proto, host, path + + + # I don't need to load gajim.py just because of few TLS variables, so I changed # %s/common\.gajim\.DATA_DIR/\'\.\.\/data\'/c @@ -39,13 +63,438 @@ log = logging.getLogger('gajim.c.x.transports_nb') # To change it back do: # %s/\'\.\.\/data\'/common\.gajim\.DATA_DIR/c # %s/\'%s\/\.gajim\/cacerts\.pem\'\ %\ os\.environ\[\'HOME\'\]/common\.gajim\.MY_CACERTS/c +# TODO: make the paths configurable - as constructor parameters or sth + # import common.gajim +# timeout to connect to the server socket, it doesn't include auth +CONNECT_TIMEOUT_SECONDS = 30 + +# how long to wait for a disconnect to complete +DISCONNECT_TIMEOUT_SECONDS = 10 + +# size of the buffer which reads data from server +# if lower, more stanzas will be fragmented and processed twice +RECV_BUFSIZE = 32768 # 2x maximum size of ssl packet, should be plenty +#RECV_BUFSIZE = 16 # FIXME: (#2634) gajim breaks with this setting: it's inefficient but should work. DATA_RECEIVED='DATA RECEIVED' DATA_SENT='DATA SENT' + +DISCONNECTED ='DISCONNECTED' +CONNECTING ='CONNECTING' +CONNECTED ='CONNECTED' +DISCONNECTING ='DISCONNECTING' + +class NonBlockingTcp(PlugIn, IdleObject): + ''' + Non-blocking TCP socket wrapper + ''' + def __init__(self, on_disconnect): + ''' + Class constructor. + ''' + + PlugIn.__init__(self) + IdleObject.__init__(self) + + self.on_disconnect = on_disconnect + + self.on_connect = None + self.on_connect_failure = None + self.sock = None + self.idlequeue = None + self.on_receive = None + self.DBG_LINE='socket' + self.state = DISCONNECTED + + # writable, readable - keep state of the last pluged flags + # This prevents replug of same object with the same flags + self.writable = True + self.readable = False + + # queue with messages to be send + self.sendqueue = [] + + # time to wait for SOME stanza to come and then send keepalive + self.sendtimeout = 0 + + # in case we want to something different than sending keepalives + self.on_timeout = None + + # bytes remained from the last send message + self.sendbuff = '' + self._exported_methods=[self.disconnect, self.onreceive, self.set_send_timeout, + self.set_timeout, self.remove_timeout] + + def plugin(self, owner): + owner.Connection=self + print 'plugin called' + self.idlequeue = owner.idlequeue + + def plugout(self): + self._owner.Connection = None + self._owner = None + + + def get_fd(self): + try: + tmp = self._sock.fileno() + return tmp + except: + return 0 + + def connect(self, conn_5tuple, on_connect, on_connect_failure): + ''' + Creates and connects socket to server and port defined in conn_5tupe which + should be list item returned from getaddrinfo. + :param conn_5tuple: 5-tuple returned from getaddrinfo + :param on_connect: callback called on successful tcp connection + :param on_connect_failure: callback called on failure when estabilishing tcp + connection + ''' + self.on_connect = on_connect + self.on_connect_failure = on_connect_failure + (self.server, self.port) = conn_5tuple[4] + log.debug('NonBlocking Connect :: About tot connect to %s:%s' % conn_5tuple[4]) + try: + self._sock = socket.socket(*conn_5tuple[:3]) + except socket.error, (errnum, errstr): + on_connect_failure('NonBlockingTcp: Error while creating socket: %s %s' % (errnum, errstr)) + return + + self._send = self._sock.send + self._recv = self._sock.recv + self.fd = self._sock.fileno() + self.idlequeue.plug_idle(self, True, False) + + errnum = 0 + ''' variable for errno symbol that will be found from exception raised from connect() ''' + + # set timeout for TCP connecting - if nonblocking connect() fails, pollend + # is called. If if succeeds pollout is called. + self.idlequeue.set_read_timeout(self.get_fd(), CONNECT_TIMEOUT_SECONDS) + + try: + self._sock.setblocking(False) + self._sock.connect((self.server,self.port)) + except Exception, (errnum, errstr): + pass + + if errnum in (errno.EINPROGRESS, errno.EALREADY, errno.EWOULDBLOCK): + # connecting in progress + self.set_state(CONNECTING) + log.debug('After connect. "%s" raised => CONNECTING' % errstr) + # on_connect/failure will be called from self.pollin/self.pollout + return + elif errnum in (0, 10056, errno.EISCONN): + # already connected - this branch is very unlikely, nonblocking connect() will + # return EINPROGRESS exception in most cases. When here, we don't need timeout + # on connected descriptor and success callback can be called. + log.debug('After connect. "%s" raised => CONNECTED' % errstr) + self._on_connect(self) + return + + # if there was some other error, call failure callback and unplug transport + # which will also remove read_timeouts for descriptor + self._on_connect_failure('Exception while connecting to %s:%s - %s %s' % + (self.server, self.port, errnum, errstr)) + + def _on_connect(self, data): + ''' preceeds call of on_connect callback ''' + self.set_state(CONNECTED) + self.idlequeue.remove_timeout(self.get_fd()) + self.on_connect() + + + def set_state(self, newstate): + assert(newstate in [DISCONNECTED, CONNECTING, CONNECTED, DISCONNECTING]) + if (self.state, newstate) in [(CONNECTING, DISCONNECTING), (DISCONNECTED, DISCONNECTING)]: + log.info('strange move: %s -> %s' % (self.state, newstate)) + self.state = newstate + + + def _on_connect_failure(self,err_message): + ''' preceeds call of on_connect_failure callback ''' + # In case of error while connecting we need to close socket + # but we don't want to call DisconnectHandlers from client, + # thus the do_callback=False + self.disconnect(do_callback=False) + self.on_connect_failure(err_message=err_message) + + + + def pollin(self): + '''called when receive on plugged socket is possible ''' + log.debug('pollin called, state == %s' % self.state) + self._do_receive() + + def pollout(self): + '''called when send to plugged socket is possible''' + log.debug('pollout called, state == %s' % self.state) + + if self.state==CONNECTING: + self._on_connect(self) + return + self._do_send() + + def pollend(self): + log.debug('pollend called, state == %s' % self.state) + + if self.state==CONNECTING: + self._on_connect_failure('Error during connect to %s:%s' % + (self.server, self.port)) + else : + self.disconnect() + + def disconnect(self, do_callback=True): + if self.state == DISCONNECTED: + return + self.idlequeue.unplug_idle(self.get_fd()) + try: + self._sock.shutdown(socket.SHUT_RDWR) + self._sock.close() + except socket.error, (errnum, errstr): + log.error('Error disconnecting a socket: %s %s' % (errnum,errstr)) + self.set_state(DISCONNECTED) + if do_callback: + # invoke callback given in __init__ + self.on_disconnect() + + def read_timeout(self): + ''' + Implemntation of IdleObject function called on timeouts from IdleQueue. + ''' + log.debug('read_timeout called, state == %s' % self.state) + if self.state==CONNECTING: + # if read_timeout is called during connecting, connect() didn't end yet + # thus we have to call the tcp failure callback + self._on_connect_failure('Error during connect to %s:%s' % + (self.server, self.port)) + else: + if self.on_timeout: + self.on_timeout() + self.renew_send_timeout() + + def renew_send_timeout(self): + if self.on_timeout and self.sendtimeout > 0: + self.set_timeout(self.sendtimeout) + else: + self.remove_timeout() + + def set_send_timeout(self, timeout, on_timeout): + self.sendtimeout = timeout + if self.sendtimeout > 0: + self.on_timeout = on_timeout + else: + self.on_timeout = None + + def set_timeout(self, timeout): + if self.state in [CONNECTING, CONNECTED] and self.get_fd() > 0: + self.idlequeue.set_read_timeout(self.get_fd(), timeout) + + def remove_timeout(self): + if self.get_fd(): + self.idlequeue.remove_timeout(self.get_fd()) + + def send(self, raw_data, now=False): + '''Append raw_data to the queue of messages to be send. + If supplied data is unicode string, encode it to utf-8. + ''' + + if self.state not in [CONNECTED, DISCONNECTING]: + log.error('Trying to send %s when transport is %s.' % + (raw_data, self.state)) + return + r = raw_data + if isinstance(r, unicode): + r = r.encode('utf-8') + elif not isinstance(r, str): + r = ustr(r).encode('utf-8') + if now: + self.sendqueue.insert(0, r) + self._do_send() + else: + self.sendqueue.append(r) + self._plug_idle() + + + + def _plug_idle(self): + # readable if socket is connected or disconnecting + readable = self.state != DISCONNECTED + # writeable if sth to send + if self.sendqueue or self.sendbuff: + writable = True + else: + writable = False + print 'About to plug fd %d, W:%s, R:%s' % (self.get_fd(), writable, readable) + if self.writable != writable or self.readable != readable: + print 'Really plugging fd %d, W:%s, R:%s' % (self.get_fd(), writable, readable) + self.idlequeue.plug_idle(self, writable, readable) + else: + print 'Not plugging - is already plugged' + + + + def _do_send(self): + if not self.sendbuff: + if not self.sendqueue: + return None # nothing to send + self.sendbuff = self.sendqueue.pop(0) + try: + send_count = self._send(self.sendbuff) + if send_count: + sent_data = self.sendbuff[:send_count] + self.sendbuff = self.sendbuff[send_count:] + self._plug_idle() + self._raise_event(DATA_SENT, sent_data) + + except socket.error, e: + log.error('_do_send:', exc_info=True) + traceback.print_exc() + self.disconnect() + + def _raise_event(self, event_type, data): + if data and data.strip(): + log.debug('raising event from transport: %s %s' % (event_type,data)) + if hasattr(self._owner, 'Dispatcher'): + self._owner.Dispatcher.Event('', event_type, data) + + def onreceive(self, recv_handler): + ''' Sets the on_receive callback. Do not confuse it with + on_receive() method, which is the callback itself.''' + if not recv_handler: + if hasattr(self._owner, 'Dispatcher'): + self.on_receive = self._owner.Dispatcher.ProcessNonBlocking + else: + self.on_receive = None + return + self.on_receive = recv_handler + + + def _do_receive(self): + ''' Reads all pending incoming data. Calls owner's disconnected() method if appropriate.''' + ERR_DISCONN = -2 # Misc error signifying that we got disconnected + received = None + errnum = 0 + errstr = 'No Error Set' + + try: + # get as many bites, as possible, but not more than RECV_BUFSIZE + received = self._recv(RECV_BUFSIZE) + except (socket.error, socket.herror, socket.gaierror), (errnum, errstr): + # save exception number and message to errnum, errstr + log.debug("_do_receive: got %s:" % received , exc_info=True) + + if received == '': + errnum = ERR_DISCONN + errstr = "Connection closed unexpectedly" + + if errnum in (ERR_DISCONN, errno.ECONNRESET, errno.ENOTCONN, errno.ESHUTDOWN): + # ECONNRESET - connection you are trying to access has been reset by the peer + # ENOTCONN - Transport endpoint is not connected + # ESHUTDOWN - shutdown(2) has been called on a socket to close down the + # sending end of the transmision, and then data was attempted to be sent + log.error("Connection to %s lost: %s %s" % ( self.server, errnum, errstr)) + self.disconnect() + return + + if received is None: + # in case of some other exception + # FIXME: is this needed?? + if errnum != 0: + self.DEBUG(self.DBG, errstr, 'error') + log.error("CConnection to %s lost: %s %s" % (self.server, errnum, errstr)) + if not errors_only and self.state in [CONNECTING, CONNECTED]: + self.pollend(retry=True) + return + received = '' + + # we have received some bytes, stop the timeout! + self.renew_send_timeout() + if self.on_receive: + self._raise_event(DATA_RECEIVED, received) + self._on_receive(received) + else: + # This should never happen, so we need the debug + log.error('SOCKET Unhandled data received: %s' % received) + self.disconnect() + + def _on_receive(self, data): + # Overriding this method allows modifying received data before it is passed + # to callback. + self.on_receive(data) + + +class NonBlockingHttpBOSH(NonBlockingTcp): + ''' + Socket wrapper that makes HTTP message out of send data and peels-off + HTTP headers from incoming messages + ''' + + def __init__(self, bosh_uri, bosh_port, on_disconnect): + self.bosh_protocol, self.bosh_host, self.bosh_path = self.urisplit(bosh_uri) + if self.bosh_protocol is None: + self.bosh_protocol = 'http' + if self.bosh_path == '': + bosh_path = '/' + self.bosh_port = bosh_port + + def send(self, raw_data, now=False): + + NonBlockingTcp.send( + self, + self.build_http_message(raw_data), + now) + + def _on_receive(self,data): + '''Preceeds pass of received data to Client class. Gets rid of HTTP headers + and checks them.''' + statusline, headers, httpbody = self.parse_http_message(data) + if statusline[1] != '200': + log.error('HTTP Error: %s %s' % (statusline[1], statusline[2])) + self.disconnect() + self.on_receive(httpbody) + + + def build_http_message(self, httpbody): + ''' + Builds bosh http message with given body. + Values for headers and status line fields are taken from class variables. + ) + ''' + headers = ['POST %s HTTP/1.1' % self.bosh_path, + 'Host: %s:%s' % (self.bosh_host, self.bosh_port), + 'Content-Type: text/xml; charset=utf-8', + 'Content-Length: %s' % len(str(httpbody)), + '\r\n'] + headers = '\r\n'.join(headers) + return('%s%s\r\n' % (headers, httpbody)) + + def parse_http_message(self, message): + ''' + splits http message to tuple ( + statusline - list of e.g. ['HTTP/1.1', '200', 'OK'], + headers - dictionary of headers e.g. {'Content-Length': '604', + 'Content-Type': 'text/xml; charset=utf-8'}, + httpbody - string with http body + ) + ''' + message = message.replace('\r','') + (header, httpbody) = message.split('\n\n') + header = header.split('\n') + statusline = header[0].split(' ') + header = header[1:] + headers = {} + for dummy in header: + row = dummy.split(' ',1) + headers[row[0][:-1]] = row[1] + return (statusline, headers, httpbody) + + USE_PYOPENSSL = False try: @@ -62,16 +511,6 @@ except ImportError: print >> sys.stderr, "PyOpenSSL not found, falling back to Python builtin SSL objects (insecure)." print >> sys.stderr, "=" * 79 -# timeout to connect to the server socket, it doesn't include auth -CONNECT_TIMEOUT_SECONDS = 30 - -# how long to wait for a disconnect to complete -DISCONNECT_TIMEOUT_SECONDS = 10 - -# size of the buffer which reads data from server -# if lower, more stanzas will be fragmented and processed twice -RECV_BUFSIZE = 32768 # 2x maximum size of ssl packet, should be plenty -#RECV_BUFSIZE = 16 # FIXME: (#2634) gajim breaks with this setting: it's inefficient but should work. def torf(cond, tv, fv): if cond: return tv @@ -243,439 +682,6 @@ class StdlibSSLWrapper(SSLWrapper): raise SSLWrapper.Error(self.sock or self.sslobj, e) return 0 -class NonBlockingTcp(PlugIn, IdleObject): - ''' This class can be used instead of transports.Tcp in threadless implementations ''' - def __init__(self, on_connect = None, on_connect_failure = None, server=None, use_srv = True): - ''' Cache connection point 'server'. 'server' is the tuple of (host, port) - absolutely the same as standard tcp socket uses. - on_connect - called when we connect to the socket - on_connect_failure - called if there was error connecting to socket - ''' - IdleObject.__init__(self) - PlugIn.__init__(self) - self.DBG_LINE='socket' - self._exported_methods=[self.send, self.disconnect, self.onreceive, self.set_send_timeout, - self.start_disconnect, self.set_timeout, self.remove_timeout] - self._server = server - self.on_connect = on_connect - self.on_connect_failure = on_connect_failure - self.on_receive = None - self.on_disconnect = None - self.printed_error = False - - # 0 - not connected - # 1 - connected - # -1 - about to disconnect (when we wait for final events to complete) - # -2 - disconnected - self.state = 0 - - # queue with messages to be send - self.sendqueue = [] - - # bytes remained from the last send message - self.sendbuff = '' - - # time to wait for SOME stanza to come and then send keepalive - self.sendtimeout = 0 - - # in case we want to something different than sending keepalives - self.on_timeout = None - - # writable, readable - keep state of the last pluged flags - # This prevents replug of same object with the same flags - self.writable = True - self.readable = False - self.ais = None - - def plugin(self, owner): - ''' Fire up connection. Return non-empty string on success. - Also registers self.disconnected method in the owner's dispatcher. - Called internally. ''' - self.idlequeue = owner.idlequeue - self.printed_error = False - if not self._server: - self._server=(self._owner.Server,5222) - if self.connect(self._server) is False: - return False - return True - - def read_timeout(self): - if self.state == 0: - self.idlequeue.unplug_idle(self.fd) - if self.on_connect_failure: - self.on_connect_failure() - else: - if self.on_timeout: - self.on_timeout() - self.renew_send_timeout() - - def connect(self,server=None, proxy = None, secure = None): - ''' Try to establish connection. ''' - if not server: - server=self._server - else: - self._server = server - self.printed_error = False - self.state = 0 - try: - self.set_timeout(CONNECT_TIMEOUT_SECONDS) - if len(server) == 2 and type(server[0]) in (str, unicode) and not \ - self.ais: - # FIXME: blocks here - self.ais = socket.getaddrinfo(server[0],server[1],socket.AF_UNSPEC,socket.SOCK_STREAM) - log.info('Found IPs: %s', self.ais) - else: - self.ais = (server,) - self.connect_to_next_ip() - return - except socket.gaierror, e: - log.info('Lookup failure for %s: %s[%s]', self.getName(), e[1], repr(e[0]), exc_info=True) - except: - log.error('Exception trying to connect to %s:', self.getName(), exc_info=True) - - if self.on_connect_failure: - self.on_connect_failure() - - def _plug_idle(self): - # readable if socket is connected or disconnecting - readable = self.state != 0 - # writeable if sth to send - if self.sendqueue or self.sendbuff: - writable = True - else: - writable = False - if self.writable != writable or self.readable != readable: - self.idlequeue.plug_idle(self, writable, readable) - - def pollout(self): - print 'pollout called - send possible' - if self.state == 0: - self.connect_to_next_ip() - return - self._do_send() - - def plugout(self): - ''' Disconnect from the remote server and unregister self.disconnected method from - the owner's dispatcher. ''' - self.disconnect() - self._owner.Connection = None - self._owner = None - - def pollin(self): - print 'pollin called - receive possible' - self._do_receive() - - def pollend(self, retry=False): - if not self.printed_error: - self.printed_error = True - try: self._do_receive(errors_only=True) - except: log.error("pollend: Got exception from _do_receive:", exc_info=True) - conn_failure_cb = self.on_connect_failure - self.disconnect() - if conn_failure_cb: - conn_failure_cb(retry) - - def disconnect(self): - if self.state == -2: # already disconnected - return - self.state = -2 - self.sendqueue = None - self.remove_timeout() - try: - self._owner.disconnected() - except: - pass - self.idlequeue.unplug_idle(self.fd) - sock = getattr(self, '_sock', None) - if sock: - try: - sock.shutdown(socket.SHUT_RDWR) - except socket.error, e: - if e[0] != errno.ENOTCONN: - log.error("Error shutting down socket for %s:", self.getName(), exc_info=True) - try: sock.close() - except: log.error("Error closing socket for %s:", self.getName(), exc_info=True) - # socket descriptor cannot be (un)plugged anymore - self.fd = -1 - if self.on_disconnect: - self.on_disconnect() - self.on_connect_failure = None - - def end_disconnect(self): - ''' force disconnect only if we are still trying to disconnect ''' - if self.state == -1: - self.disconnect() - - def start_disconnect(self, to_send, on_disconnect): - self.on_disconnect = on_disconnect - - # flush the sendqueue - while self.sendqueue: - self._do_send() - - self.sendqueue = [] - self.send(to_send) - self.send('') - self.state = -1 # about to disconnect - self.idlequeue.set_alarm(self.end_disconnect, DISCONNECT_TIMEOUT_SECONDS) - - def set_timeout(self, timeout): - if self.state >= 0 and self.fd > 0: - self.idlequeue.set_read_timeout(self.fd, timeout) - - def remove_timeout(self): - if self.fd: - self.idlequeue.remove_timeout(self.fd) - - def onreceive(self, recv_handler): - ''' Sets the on_receive callback. Do not confuse it with - on_receive() method, which is the callback itself. - - If recv_handler==None, it tries to set that callback assuming that - our owner also has a Dispatcher object plugged in, to its - ProcessNonBlocking method.''' - if not recv_handler: - if hasattr(self._owner, 'Dispatcher'): - self.on_receive = self._owner.Dispatcher.ProcessNonBlocking - else: - self.on_receive = None - return - _tmp = self.on_receive - # make sure this cb is not overriden by recursive calls - if not recv_handler(None) and _tmp == self.on_receive: - self.on_receive = recv_handler - - def _do_receive(self, errors_only=False): - ''' Reads all pending incoming data. Calls owner's disconnected() method if appropriate.''' - ERR_DISCONN = -2 # Misc error signifying that we got disconnected - ERR_OTHER = -1 # Other error - received = None - errnum = 0 - errtxt = 'No Error Set' - try: - # get as many bites, as possible, but not more than RECV_BUFSIZE - received = self._recv(RECV_BUFSIZE) - except (socket.error, socket.herror, socket.gaierror), e: - log.debug("_do_receive: got %s:", e.__class__, exc_info=True) - #traceback.print_exc() - #print "Current Stack:" - #traceback.print_stack() - errnum = e[0] - errtxt = str(errnum) + ':' + e[1] - except socket.sslerror, e: - log.error("_do_receive: got unknown %s:", e.__class__, exc_info=True) - #traceback.print_exc() - #print "Current Stack:" - #traceback.print_stack() - errnum = ERR_OTHER - errtxt = repr("socket.sslerror: " + e.args) - except SSLWrapper.Error, e: - log.debug("Caught: %s", str(e)) - errnum = gattr(e, 'errno', ERR_OTHER) - if not errnum: errnum = ERR_OTHER # unset, but we must put a status - errtxt = gattr(e, 'strerror') or repr(e.args) - - if received == '': - errnum = ERR_DISCONN - errtxt = "Connection closed unexpectedly" - - if errnum in (ERR_DISCONN, errno.ECONNRESET, errno.ENOTCONN, errno.ESHUTDOWN): - log.error("Connection to %s lost: %s [%d]", self.getName(), errtxt, errnum) - self.printed_error = True - if not errors_only: - self.pollend(retry=(errnum in (ERR_DISCONN, errno.ECONNRESET))) - # don't process result, because it will raise an error - return - - if received is None: - if errnum != 0: - self.DEBUG(errtxt, 'error') - log.error("Connection to %s lost: %s [%d]", self.getName(), errtxt, errnum) - self.printed_error = True - if not errors_only and self.state >= 0: - self.pollend(retry=True) - return - received = '' - - if errors_only or self.state < 0: - return - - # we have received some bites, stop the timeout! - self.renew_send_timeout() - if self.on_receive: - if received.strip(): - self.DEBUG(received, 'got') - if hasattr(self._owner, 'Dispatcher'): - self._owner.Dispatcher.Event('', DATA_RECEIVED, received) - self.on_receive(received) - else: - # This should never happed, so we need the debug - self.DEBUG('Unhandled data received: %s' % received,'got') - self.disconnect() - if self.on_connect_failure: - self.on_connect_failure() - return True - - def _do_send(self): - if not self.sendbuff: - if not self.sendqueue: - return None # nothing to send - self.sendbuff = self.sendqueue.pop(0) - self.sent_data = self.sendbuff - try: - send_count = self._send(self.sendbuff) - if send_count: - self.sendbuff = self.sendbuff[send_count:] - if not self.sendbuff and not self.sendqueue: - if self.state < 0: - self.idlequeue.unplug_idle(self.fd) - self._on_send() - self.disconnect() - return - # we are not waiting for write - self._plug_idle() - self._on_send() - except socket.error, e: - if e[0] == socket.SSL_ERROR_WANT_WRITE: - return True - log.error("_do_send:", exc_info=True) - #traceback.print_exc() - if self.state < 0: - self.disconnect() - return - if self._on_send_failure: - self._on_send_failure() - return - return True - - def connect_to_next_ip(self): - if self.state != 0: - return - if len(self.ais) == 0: - if self.on_connect_failure: - self.on_connect_failure() - return - ai = self.ais.pop(0) - log.info('Trying to connect to %s:%s', ai[4][0], ai[4][1]) - try: - self._sock = socket.socket(*ai[:3]) - self._server=ai[4] - except socket.error, e: - errnum, errstr = e - - # Ignore "Socket already connected". - # FIXME: This happens when we switch an already - # connected socket to SSL (STARTTLS). Instead of - # ignoring the error, the socket should only be - # connected to once. See #2846 and #3396. - workaround = (errno.EALREADY, 10056, 56) - - # 10035 - winsock equivalent of EINPROGRESS - if errnum not in (errno.EINPROGRESS, 10035) + workaround: - log.error('Could not connect to %s: %s [%s]', ai[4][0], errnum, - errstr, exc_info=True) - #traceback.print_exc() - self.connect_to_next_ip() - return - self.fd = self._sock.fileno() - self.idlequeue.plug_idle(self, True, False) - self._send = self._sock.send - self._recv = self._sock.recv - self._do_connect() - - def _do_connect(self): - errnum = 0 - - try: - print "==============sock.connect called" - self._sock.connect(self._server) - self._sock.setblocking(False) - except Exception, ee: - (errnum, errstr) = ee - # in progress, or would block - print "errnum: %s" % errnum - if errnum in (errno.EINPROGRESS, errno.EALREADY, errno.EWOULDBLOCK): - self.state = 1 - return - # 10056 - already connected, only on win32 - # code 'WS*' is not available on GNU, so we use its numeric value - elif errnum not in (0, 10056, errno.EISCONN): - log.error('Could not connect to %s: %s [%s]', self._server[0], errnum, - errstr) - self.connect_to_next_ip() - return - self.remove_timeout() - self._owner.Connection=self - self.state = 1 - - self._sock.setblocking(False) - self._plug_idle() - if self.on_connect: - self.on_connect() - self.on_connect = None - - def send(self, raw_data, now = False): - '''Append raw_data to the queue of messages to be send. - If supplied data is unicode string, encode it to utf-8. - ''' - - if self.state <= 0: - return - r = raw_data - if isinstance(r, unicode): - r = r.encode('utf-8') - elif not isinstance(r, str): - r = ustr(r).encode('utf-8') - if now: - self.sendqueue.insert(0, r) - self._do_send() - else: - self.sendqueue.append(r) - - self._plug_idle() - - def _on_send(self): - if self.sent_data and self.sent_data.strip(): - self.DEBUG(self.sent_data,'sent') - if hasattr(self._owner, 'Dispatcher'): - self._owner.Dispatcher.Event('', DATA_SENT, self.sent_data) - self.sent_data = None - - def _on_send_failure(self): - self.DEBUG("Socket error while sending data",'error') - self._owner.disconnected() - self.sent_data = None - - def set_send_timeout(self, timeout, on_timeout): - self.sendtimeout = timeout - if self.sendtimeout > 0: - self.on_timeout = on_timeout - else: - self.on_timeout = None - - def renew_send_timeout(self): - if self.on_timeout and self.sendtimeout > 0: - self.set_timeout(self.sendtimeout) - else: - self.remove_timeout() - - def getHost(self): - ''' Return the 'host' value that is connection is [will be] made to.''' - return self._server[0] - - def getName(self): - ''' Return the server's name, or 'getHost()' if not available.''' - retval = None - try: - retval = gattr(self._owner, 'name') - except: - pass - if retval: return retval - return self.getHost() - - def getPort(self): - ''' Return the 'port' value that is connection is [will be] made to.''' - return self._server[1] class NonBlockingTLS(PlugIn): ''' TLS connection used to encrypts already estabilished tcp connection.''' @@ -687,7 +693,7 @@ class NonBlockingTLS(PlugIn): "SSL_CB_ALERT": 0x4000, "SSL_CB_HANDSHAKE_START": 0x10, "SSL_CB_HANDSHAKE_DONE": 0x20} - def PlugIn(self, owner, now=0, on_tls_start = None): + def PlugIn(self, owner, on_tls_success, on_tls_failure, now=0): ''' If the 'now' argument is true then starts using encryption immidiatedly. If 'now' in false then starts encryption as soon as TLS feature is declared by the server (if it were already declared - it is ok). @@ -696,7 +702,8 @@ class NonBlockingTLS(PlugIn): return # Already enabled. PlugIn.PlugIn(self, owner) DBG_LINE='NonBlockingTLS' - self.on_tls_start = on_tls_start + self.on_tls_success = on_tls_success + self.on_tls_faliure = on_tls_failure if now: try: res = self._startSSL() @@ -705,7 +712,7 @@ class NonBlockingTLS(PlugIn): #traceback.print_exc() self._owner.socket.pollend() return - self.tls_start() + on_tls_success() return res if self._owner.Dispatcher.Stream.features: try: @@ -725,23 +732,17 @@ class NonBlockingTLS(PlugIn): self._owner.Dispatcher.PlugOut() self._owner = None - def tls_start(self): - if self.on_tls_start: - self.on_tls_start() - self.on_tls_start = None - def FeaturesHandler(self, conn, feats): ''' Used to analyse server tag for TLS support. If TLS is supported starts the encryption negotiation. Used internally ''' if not feats.getTag('starttls', namespace=NS_TLS): self.DEBUG("TLS unsupported by remote server.", 'warn') - self.tls_start() + self.on_tls_failure("TLS unsupported by remote server.") return self.DEBUG("TLS supported by remote server. Requesting TLS start.", 'ok') self._owner.RegisterHandlerOnce('proceed', self.StartTLSHandler, xmlns=NS_TLS) self._owner.RegisterHandlerOnce('failure', self.StartTLSHandler, xmlns=NS_TLS) self._owner.send('' % NS_TLS) - self.tls_start() raise NodeProcessed def _dumpX509(self, cert, stream=sys.stderr): @@ -824,9 +825,10 @@ class NonBlockingTLS(PlugIn): try: self.starttls='in progress' tcpsock._sslObj.do_handshake() - # Errors are handeled in _do_receive function except: - pass + log.error('Error while TLS handshake: ', exc_info=True) + self.on_tls_failure('Error while TLS Handshake') + return tcpsock._sslObj.setblocking(False) log.debug("Synchronous handshake completed") #log.debug("Async handshake started...") @@ -865,63 +867,73 @@ class NonBlockingTLS(PlugIn): ''' Handle server reply if TLS is allowed to process. Behaves accordingly. Used internally.''' if starttls.getNamespace() <> NS_TLS: + self.on_tls_failure('Unknown namespace: %s' % starttls.getNamespace()) return self.starttls = starttls.getName() if self.starttls == 'failure': - self.DEBUG('Got starttls response: ' + self.starttls,'error') + self.on_tls_failure('TLS received: %s' % self.starttls) return self.DEBUG('Got starttls proceed response. Switching to TLS/SSL...','ok') try: self._startSSL() except Exception, e: log.error("StartTLSHandler:", exc_info=True) + self.on_tls_failure('in StartTLSHandler') #traceback.print_exc() - self._owner.socket.pollend() return self._owner.Dispatcher.PlugOut() - dispatcher_nb.Dispatcher().PlugIn(self._owner) + self.on_tls_success() + #dispatcher_nb.Dispatcher().PlugIn(self._owner) - -class NBHTTPPROXYsocket(NonBlockingTcp): - ''' This class can be used instead of transports.HTTPPROXYsocket - HTTP (CONNECT) proxy connection class. Uses TCPsocket as the base class - redefines only connect method. Allows to use HTTP proxies like squid with - (optionally) simple authentication (using login and password). - +class NBProxySocket(NonBlockingTcp): ''' - def __init__(self, on_connect =None, on_proxy_failure=None, on_connect_failure = None,proxy = None,server = None,use_srv=True): - ''' Caches proxy and target addresses. - 'proxy' argument is a dictionary with mandatory keys 'host' and 'port' (proxy address) - and optional keys 'user' and 'password' to use for authentication. - 'server' argument is a tuple of host and port - just like TCPsocket uses. ''' - self.on_connect_proxy = on_connect - self.on_proxy_failure = on_proxy_failure - self.on_connect_failure = on_connect_failure - NonBlockingTcp.__init__(self, self._on_tcp_connect, on_connect_failure, server, use_srv) - self.DBG_LINE=DBG_CONNECT_PROXY - self.server = server - self.proxy=proxy + Interface for proxy socket wrappers - when tunnneling XMPP over proxies, + some connecting process usually has to be done before opening stream. + ''' + def __init__(self, on_disconnect, xmpp_server, proxy_creds=(None,None)): + self.proxy_user, self.proxy_pass = proxy_creds + self.xmpp_server = xmpp_server + NonBlockingTcp.__init__(self, on_disconnect) + - def plugin(self, owner): - ''' Starts connection. Used interally. Returns non-empty string on success.''' - owner.debug_flags.append(DBG_CONNECT_PROXY) - NonBlockingTcp.plugin(self,owner) + def connect(self, conn_5tuple, on_connect, on_connect_failure): + ''' + connect method is extended by proxy credentials and xmpp server hostname + and port because those are needed for + The idea is to insert Proxy-specific mechanism after TCP connect and + before XMPP stream opening (which is done from client). + ''' - def connect(self,dupe=None): + self.after_proxy_connect = on_connect + + NonBlockingTcp.connect(self, + conn_5tuple=conn_5tuple, + on_connect =self._on_tcp_connect, + on_connect_failure =on_connect_failure) + + def _on_tcp_connect(self): + pass + + + +class NBHTTPProxySocket(NBProxySocket): + ''' This class can be used instead of NonBlockingTcp + HTTP (CONNECT) proxy connection class. Allows to use HTTP proxies like squid with + (optionally) simple authentication (using login and password). + ''' + + def _on_tcp_connect(self): ''' Starts connection. Connects to proxy, supplies login and password to it (if were specified while creating instance). Instructs proxy to make connection to the target server. Returns non-empty sting on success. ''' - NonBlockingTcp.connect(self, (self.proxy['host'], self.proxy['port'])) - - def _on_tcp_connect(self): - self.DEBUG('Proxy server contacted, performing authentification','start') - connector = ['CONNECT %s:%s HTTP/1.0'%self.server, + log.debug('Proxy server contacted, performing authentification') + connector = ['CONNECT %s:%s HTTP/1.0' % self.xmpp_server, 'Proxy-Connection: Keep-Alive', 'Pragma: no-cache', - 'Host: %s:%s'%self.server, + 'Host: %s:%s' % self.xmpp_server, 'User-Agent: HTTPPROXYsocket/v0.1'] - if self.proxy.has_key('user') and self.proxy.has_key('password'): - credentials = '%s:%s' % ( self.proxy['user'], self.proxy['password']) + if self.proxy_user and self.proxy_pass: + credentials = '%s:%s' % (self.proxy_user, self.proxy_pass) credentials = base64.encodestring(credentials).strip() connector.append('Proxy-Authorization: Basic '+credentials) connector.append('\r\n') @@ -937,12 +949,11 @@ class NBHTTPPROXYsocket(NonBlockingTcp): except: log.error("_on_headers_sent:", exc_info=True) #traceback.print_exc() - self.on_proxy_failure('Invalid proxy reply') + self._on_connect_failure('Invalid proxy reply') return if code <> '200': - self.DEBUG('Invalid proxy reply: %s %s %s' % (proto, code, desc),'error') - self._owner.disconnected() - self.on_proxy_failure('Invalid proxy reply') + log.error('Invalid proxy reply: %s %s %s' % (proto, code, desc)) + self._on_connect_failure('Invalid proxy reply') return if len(reply) != 2: pass @@ -951,55 +962,24 @@ class NBHTTPPROXYsocket(NonBlockingTcp): def _on_proxy_auth(self, reply): if self.reply.find('\n\n') == -1: if reply is None: - self.on_proxy_failure('Proxy authentification failed') + self._on_connect_failure('Proxy authentification failed') return if reply.find('\n\n') == -1: self.reply += reply.replace('\r', '') - self.on_proxy_failure('Proxy authentification failed') + self._on_connect_failure('Proxy authentification failed') return - self.DEBUG('Authentification successfull. Jabber server contacted.','ok') - if self.on_connect_proxy: - self.on_connect_proxy() + log.debug('Authentification successfull. Jabber server contacted.') + self._on_connect(self) - def DEBUG(self, text, severity): - ''' Overwrites DEBUG tag to allow debug output be presented as "CONNECTproxy".''' - return self._owner.DEBUG(DBG_CONNECT_PROXY, text, severity) -class NBSOCKS5PROXYsocket(NonBlockingTcp): +class NBSOCKS5ProxySocket(NBProxySocket): '''SOCKS5 proxy connection class. Uses TCPsocket as the base class redefines only connect method. Allows to use SOCKS5 proxies with (optionally) simple authentication (only USERNAME/PASSWORD auth). ''' - def __init__(self, on_connect = None, on_proxy_failure = None, - on_connect_failure = None, proxy = None, server = None, use_srv = True): - ''' Caches proxy and target addresses. - 'proxy' argument is a dictionary with mandatory keys 'host' and 'port' - (proxy address) and optional keys 'user' and 'password' to use for - authentication. 'server' argument is a tuple of host and port - - just like TCPsocket uses. ''' - self.on_connect_proxy = on_connect - self.on_proxy_failure = on_proxy_failure - self.on_connect_failure = on_connect_failure - NonBlockingTcp.__init__(self, self._on_tcp_connect, on_connect_failure, - server, use_srv) - self.DBG_LINE=DBG_CONNECT_PROXY - self.server = server - self.proxy = proxy - self.ipaddr = None + # TODO: replace DEBUG with ordinrar logging, replace on_proxy_failure() with + # _on_connect_failure, at the end call _on_connect() - def plugin(self, owner): - ''' Starts connection. Used interally. Returns non-empty string on - success.''' - owner.debug_flags.append(DBG_CONNECT_PROXY) - NonBlockingTcp.plugin(self, owner) - - def connect(self, dupe = None): - ''' Starts connection. Connects to proxy, supplies login and password to - it (if were specified while creating instance). Instructs proxy to make - connection to the target server. Returns non-empty sting on success. - ''' - NonBlockingTcp.connect(self, (self.proxy['host'], self.proxy['port'])) - def _on_tcp_connect(self): self.DEBUG('Proxy server contacted, performing authentification', 'start') if self.proxy.has_key('user') and self.proxy.has_key('password'): diff --git a/src/common/xmpp/transports_new.py b/src/common/xmpp/transports_new.py deleted file mode 100644 index 36992ba95..000000000 --- a/src/common/xmpp/transports_new.py +++ /dev/null @@ -1,270 +0,0 @@ -from idlequeue import IdleObject -from client import PlugIn -import threading, socket, errno - -import logging -log = logging.getLogger('gajim.c.x.transports_nb') -consoleloghandler = logging.StreamHandler() -consoleloghandler.setLevel(logging.DEBUG) -consoleloghandler.setFormatter( - logging.Formatter('%(levelname)s: %(message)s') -) -log.setLevel(logging.DEBUG) -log.addHandler(consoleloghandler) -log.propagate = False - -''' -this module will replace transports_nb.py -For now, it can be run from test/test_nonblockingtcp.py -* set credentials in the testing script -''' - - -class NBgetaddrinfo(threading.Thread): - ''' - Class for nonblocking call of getaddrinfo. Maybe unnecessary. - ''' - def __init__(self, server, on_success, on_failure, timeout_sec): - ''' - Call is started from constructor. It is not needed to hold reference on - created instance. - :param server: tuple (hostname, port) for DNS request - :param on_success: callback for successful DNS request - :param on_failure: called when DNS request couldn't be performed - :param timeout_sec: max seconds to wait for return from getaddrinfo. After - this time, on_failure is called with error message. - ''' - threading.Thread.__init__(self) - self.on_success = on_success - self.on_failure = on_failure - self.server = server - self.lock = threading.Lock() - self.already_called = False - self.timer = threading.Timer(timeout_sec, self.on_timeout) - self.timer.start() - self.start() - - def on_timeout(self): - ''' - Called by timer. Means that getaddrinfo takes too long and will be - interrupted. - ''' - self.do_call(False, 'NBgetaddrinfo timeout while looking up %s:%s' % self.server) - - def do_call(self, success, data): - ''' - Method called either on success and failure. In case of timeout it will be - called twice but only the first (failure) call will be performed. - :param success: True if getaddrinfo returned properly, False if there was an - error or on timeout. - :param data: error message if failure, list of address structures if success - ''' - log.debug('NBgetaddrinfo::do_call(): %s' % repr(data)) - self.timer.cancel() - self.lock.acquire() - if not self.already_called: - self.already_called = True - self.lock.release() - if success: - self.on_success(data) - else: - self.on_failure(data) - return - else: - self.lock.release() - return - - def run(self): - try: - ips = socket.getaddrinfo(self.server[0],self.server[1],socket.AF_UNSPEC, - socket.SOCK_STREAM) - except socket.gaierror, e: - self.do_call(False, 'Lookup failure for %s: %s %s' % - (repr(self.server), e[0], e[1])) - except Exception, e: - self.do_call(False, 'Exception while DNS lookup of %s: %s' % - (repr(e), repr(self.server))) - else: - self.do_call(True, ips) - - - -DISCONNECTED ='DISCONNECTED' -CONNECTING ='CONNECTING' -CONNECTED ='CONNECTED' -DISCONNECTING ='DISCONNECTING' - -CONNECT_TIMEOUT_SECONDS = 5 -'''timeout to connect to the server socket, it doesn't include auth''' - -DISCONNECT_TIMEOUT_SECONDS = 10 -'''how long to wait for a disconnect to complete''' - -class NonBlockingTcp(PlugIn, IdleObject): - def __init__(self, on_xmpp_connect=None, on_xmpp_failure=None): - ''' - Class constructor. All parameters can be reset in tcp_connect or xmpp_connect - calls. - - ''' - PlugIn.__init__(self) - IdleObject.__init__(self) - self.on_tcp_connect = None - self.on_tcp_failure = None - self.sock = None - self.idlequeue = None - self.DBG_LINE='socket' - self.state = DISCONNECTED - ''' - CONNECTING - after non-blocking socket.connect() until TCP connection is estabilished - CONNECTED - after TCP connection is estabilished - DISCONNECTING - - DISCONNECTED - ''' - self._exported_methods=[self.send, self.disconnect, self.onreceive, self.set_send_timeout, - self.start_disconnect, self.set_timeout, self.remove_timeout] - - - def connect(self, conn_5tuple, on_tcp_connect, on_tcp_failure, idlequeue): - ''' - Creates and connects socket to server and port defined in conn_5tupe which - should be list item returned from getaddrinfo. - :param conn_5tuple: 5-tuple returned from getaddrinfo - :param on_tcp_connect: callback called on successful tcp connection - :param on_tcp_failure: callback called on failure when estabilishing tcp - connection - :param idlequeue: idlequeue for socket - ''' - self.on_tcp_connect = on_tcp_connect - self.on_tcp_failure = on_tcp_failure - self.conn_5tuple = conn_5tuple - try: - self.sock = socket.socket(*conn_5tuple[:3]) - except socket.error, (errnum, errstr): - on_tcp_failure('NonBlockingTcp: Error while creating socket: %s %s' % (errnum, errstr)) - return - - self.idlequeue = idlequeue - self.fd = self.sock.fileno() - self.idlequeue.plug_idle(self, True, False) - - errnum = 0 - ''' variable for errno symbol that will be found from exception raised from connect() ''' - - # set timeout for TCP connecting - if nonblocking connect() fails, pollend - # is called. If if succeeds pollout is called. - self.idlequeue.set_read_timeout(self.fd, CONNECT_TIMEOUT_SECONDS) - - try: - self.sock.setblocking(False) - self.sock.connect(conn_5tuple[4]) - except Exception, (errnum, errstr): - pass - - if errnum in (errno.EINPROGRESS, errno.EALREADY, errno.EWOULDBLOCK): - # connecting in progress - self.state = CONNECTING - log.debug('After nonblocking connect. "%s" raised => CONNECTING' % errstr) - # on_tcp_connect/failure will be called from self.pollin/self.pollout - return - elif errnum in (0, 10056, errno.EISCONN): - # already connected - this branch is very unlikely, nonblocking connect() will - # return EINPROGRESS exception in most cases. Anyway, we don't need timeout - # on connected descriptor - log.debug('After nonblocking connect. "%s" raised => CONNECTED' % errstr) - self._on_tcp_connect(self) - return - - # if there was some other error, call failure callback and unplug transport - # which will also remove read_timeouts for descriptor - self._on_tcp_failure('Exception while connecting to %s: %s - %s' % - (conn_5tuple[4], errnum, errstr)) - - def _on_tcp_connect(self, data): - ''' This method preceeds actual call of on_tcp_connect callback - ''' - self.state = CONNECTED - self.idlequeue.remove_timeout(self.fd) - self.on_tcp_connect(data) - - - def _on_tcp_failure(self,err_msg): - ''' This method preceeds actual call of on_tcp_failure callback - ''' - self.state = DISCONNECTED - self.idlequeue.unplug_idle(self.fd) - self.on_tcp_failure(err_msg) - - def pollin(self): - '''called when receive on plugged socket is possible ''' - log.debug('pollin called, state == %s' % self.state) - - def pollout(self): - '''called when send to plugged socket is possible''' - log.debug('pollout called, state == %s' % self.state) - - if self.state==CONNECTING: - self._on_tcp_connect(self) - return - - def pollend(self): - '''called when remote site closed connection''' - log.debug('pollend called, state == %s' % self.state) - if self.state==CONNECTING: - self._on_tcp_failure('Error during connect to %s:%s' % self.conn_5tuple[4]) - - def read_timeout(self): - ''' - Implemntation of IdleObject function called on timeouts from IdleQueue. - ''' - log.debug('read_timeout called, state == %s' % self.state) - if self.state==CONNECTING: - # if read_timeout is called during connecting, connect() didn't end yet - # thus we have to close the socket - try: - self.sock.close() - except socket.error, (errnum, errmsg): - log.error('Error while closing socket on connection timeout: %s %s' - % (errnum, errmsg)) - self._on_tcp_failure('Error during connect to %s:%s' % self.conn_5tuple[4]) - - - - def disconnect(self, on_disconnect=None): - if self.state == DISCONNECTED: - return - self.idlequeue.unplug_idle(self.fd) - try: - self.sock.shutdown(socket.SHUT_RDWR) - except socket.error, (errnum, errstr): - log.error('Error while disconnecting: %s %s' % (errnum,errstr)) - - try: - self.sock.close() - except socket.error, (errnum, errmsg): - log.error('Error closing socket: %s %s' % (errnum,errstr)) - if on_disconnect: - on_disconnect() - - - - - - - def send(self, data, now=False): - pass - - def onreceive(self): - pass - - def set_send_timeout(self): - pass - - def set_timeout(self): - pass - - def remove_timeout(self): - pass - - def start_disconnect(self): - pass diff --git a/src/config.py b/src/config.py index 4e4c49161..c678599e8 100644 --- a/src/config.py +++ b/src/config.py @@ -1199,7 +1199,7 @@ class ManageProxiesWindow: proxypass_entry.set_text(gajim.config.get_per('proxies', proxy, 'pass')) proxytype = gajim.config.get_per('proxies', proxy, 'type') - types = ['http', 'socks5'] + types = ['http', 'socks5', 'bosh'] self.proxytype_combobox.set_active(types.index(proxytype)) if gajim.config.get_per('proxies', proxy, 'user'): useauth_checkbutton.set_active(True) @@ -1227,7 +1227,7 @@ class ManageProxiesWindow: model.set_value(iter, 0, new_name) def on_proxytype_combobox_changed(self, widget): - types = ['http', 'socks5'] + types = ['http', 'socks5', 'bosh'] type_ = self.proxytype_combobox.get_active() proxy = self.proxyname_entry.get_text().decode('utf-8') gajim.config.set_per('proxies', proxy, 'type', types[type_]) diff --git a/test/test_client_nb.py b/test/test_client_nb.py index 3ddc75eef..b0aa88be8 100644 --- a/test/test_client_nb.py +++ b/test/test_client_nb.py @@ -22,7 +22,7 @@ xmpp_server_port = ('xmpp.example.org',5222) Script will connect to the machine. ''' -credentials = ['login', 'pass', 'testclient'] +credentials = ['loginn', 'passwo', 'testresour'] ''' [username, password, passphrase] Script will autheticate itself with this credentials on above mentioned server. @@ -41,19 +41,6 @@ class TestNonBlockingClient(unittest.TestCase): self.idlequeue_thread = IdleQueueThread() self.connection = MockConnectionClass() - self.client = client_nb.NonBlockingClient( - server=xmpp_server_port[0], - port=xmpp_server_port[1], - on_connect=lambda *args: self.connection.on_connect(True, *args), - on_connect_failure=lambda *args: self.connection.on_connect(False, *args), - caller=self.connection - ) - ''' - NonBlockingClient instance with parameters from global variables and with - callbacks from dummy connection. - ''' - - self.client.set_idlequeue(self.idlequeue_thread.iq) self.idlequeue_thread.start() def tearDown(self): @@ -70,17 +57,33 @@ class TestNonBlockingClient(unittest.TestCase): :param server_port: tuple of (hostname, port) for where the client should connect. + ''' - self.client.connect(server_port) + self.client = client_nb.NonBlockingClient( + hostname=server_port[0], + port=server_port[1], + caller=self.connection, + idlequeue=self.idlequeue_thread.iq, + ) + ''' + NonBlockingClient instance with parameters from global variables and with + callbacks from dummy connection. + ''' + + self.client.connect( + on_connect=lambda *args: self.connection.on_connect(True, *args), + on_connect_failure=lambda *args: self.connection.on_connect(False, *args), + secure=False + ) print 'waiting for callback from client constructor' self.connection.wait() # if on_connect was called, client has to be connected and vice versa if self.connection.connect_succeeded: - self.assert_(self.client.isConnected()) + self.assert_(self.client.get_connect_type()) else: - self.assert_(not self.client.isConnected()) + self.assert_(not self.client.get_connect_type()) def client_auth(self, username, password, resource, sasl): ''' @@ -100,7 +103,9 @@ class TestNonBlockingClient(unittest.TestCase): ''' Does disconnecting of connected client. Returns when TCP connection is closed. ''' - self.client.start_disconnect(None, on_disconnect=self.connection.set_event) + #self.client.start_disconnect(None, on_disconnect=self.connection.set_event) + self.client.RegisterDisconnectHandler(self.connection.set_event) + self.client.disconnect() print 'waiting for disconnecting...' self.connection.wait() @@ -113,7 +118,7 @@ class TestNonBlockingClient(unittest.TestCase): self.open_stream(xmpp_server_port) # if client is not connected, lets raise the AssertionError - self.assert_(self.client.isConnected()) + self.assert_(self.client.get_connect_type()) # (client.disconnect() is already called from NBClient._on_connected_failure # so there's need to call it in this case @@ -130,7 +135,7 @@ class TestNonBlockingClient(unittest.TestCase): then disconnected. ''' self.open_stream(xmpp_server_port) - self.assert_(self.client.isConnected()) + self.assert_(self.client.get_connect_type()) self.client_auth(credentials[0], credentials[1], credentials[2], sasl=0) self.assert_(self.connection.con) self.assert_(self.connection.auth=='old_auth') @@ -141,7 +146,8 @@ class TestNonBlockingClient(unittest.TestCase): Connect to nonexisting host. DNS request for A records should return nothing. ''' self.open_stream(('fdsfsdf.fdsf.fss', 5222)) - self.assert_(not self.client.isConnected()) + print 'nonexthost: %s' % self.client.get_connect_type() + self.assert_(not self.client.get_connect_type()) def test_connect_to_wrong_port(self): ''' @@ -149,14 +155,14 @@ class TestNonBlockingClient(unittest.TestCase): but there shouldn't be XMPP server running on specified port. ''' self.open_stream((xmpp_server_port[0], 31337)) - self.assert_(not self.client.isConnected()) + self.assert_(not self.client.get_connect_type()) def test_connect_with_wrong_creds(self): ''' Connecting with invalid password. ''' self.open_stream(xmpp_server_port) - self.assert_(self.client.isConnected()) + self.assert_(self.client.get_connect_type()) self.client_auth(credentials[0], "wrong pass", credentials[2], sasl=1) self.assert_(self.connection.auth is None) self.do_disconnect() @@ -168,9 +174,10 @@ class TestNonBlockingClient(unittest.TestCase): if __name__ == '__main__': - #suite = unittest.TestLoader().loadTestsFromTestCase(TestNonBlockingClient) - suite = unittest.TestSuite() - suite.addTest(TestNonBlockingClient('test_proper_connect_sasl')) + suite = unittest.TestLoader().loadTestsFromTestCase(TestNonBlockingClient) + #suite = unittest.TestSuite() + #suite.addTest(TestNonBlockingClient('test_proper_connect_oldauth')) + #suite.addTest(TestNonBlockingClient('test_connect_to_nonexisting_host')) unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/test/test_nonblockingtcp.py b/test/test_nonblockingtcp.py index cf6d31a2d..7987d3278 100644 --- a/test/test_nonblockingtcp.py +++ b/test/test_nonblockingtcp.py @@ -12,7 +12,7 @@ gajim_root = os.path.join(os.path.abspath(os.path.dirname(__file__)), '..') sys.path.append(gajim_root + '/src/common/xmpp') sys.path.append(gajim_root + '/src/common') -import transports_new, debug +import transports_nb from client import * xmpp_server = ('xmpp.example.org',5222) @@ -21,60 +21,48 @@ xmpp_server = ('xmpp.example.org',5222) Script will connect to the machine. ''' -dns_timeout = 10 -''' -timeout for DNS A-request (for getaddrinfo() call) -''' + +import socket +ips = socket.getaddrinfo(xmpp_server[0], xmpp_server[1], socket.AF_UNSPEC,socket.SOCK_STREAM) + +# change xmpp_server on real values +ip = ips[0] + class MockClient(IdleMock): - def __init__(self, server, port): + def __init__(self, idlequeue): + self.idlequeue=idlequeue self.debug_flags=['all', 'nodebuilder'] self._DEBUG = debug.Debug(['socket']) self.DEBUG = self._DEBUG.Show - self.server = server - self.port = port IdleMock.__init__(self) - self.tcp_connected = False - self.ip_addresses = [] - self.socket = None - def do_dns_request(self): - transports_new.NBgetaddrinfo( - server=(self.server, self.port), - on_success=lambda *args:self.on_success('DNSrequest', *args), - on_failure=self.on_failure, - timeout_sec=dns_timeout + def do_connect(self): + self.socket=transports_nb.NonBlockingTcp( + on_disconnect=lambda: self.on_success(mode='SocketDisconnect') + ) + + self.socket.PlugIn(self) + + self.socket.connect( + conn_5tuple=ip, + on_connect=lambda: self.on_success(mode='TCPconnect'), + on_connect_failure=self.on_failure ) self.wait() - - def try_next_ip(self, err_message=None): - if err_message: - print err_message - if self.ip_addresses == []: - self.on_failure('Run out of hosts') - return - current_ip = self.ip_addresses.pop(0) - self.NonBlockingTcp.connect( - conn_5tuple=current_ip, - on_tcp_connect=lambda *args: self.on_success('TCPconnect',*args), - on_tcp_failure=self.try_next_ip, - idlequeue=self.idlequeue - ) + def do_disconnect(self): + self.socket.disconnect() self.wait() - - def set_idlequeue(self, idlequeue): - self.idlequeue=idlequeue - def on_failure(self, data): print 'Error: %s' % data self.set_event() - def on_success(self, mode, data): - if mode == "DNSrequest": - self.ip_addresses = data - elif mode == "TCPconnect": + def on_success(self, mode, data=None): + if mode == "TCPconnect": + pass + if mode == "SocketDisconnect": pass self.set_event() @@ -87,12 +75,10 @@ class MockClient(IdleMock): class TestNonBlockingTcp(unittest.TestCase): def setUp(self): - self.nbtcp = transports_new.NonBlockingTcp() - self.client = MockClient(*xmpp_server) self.idlequeue_thread = IdleQueueThread() self.idlequeue_thread.start() - self.client.set_idlequeue(self.idlequeue_thread.iq) - self.nbtcp.PlugIn(self.client) + self.client = MockClient( + idlequeue=self.idlequeue_thread.iq) def tearDown(self): self.idlequeue_thread.stop_thread() @@ -100,12 +86,12 @@ class TestNonBlockingTcp(unittest.TestCase): def testSth(self): - self.client.do_dns_request() - if self.client.ip_addresses == []: - print 'No IP found for given hostname: %s' % self.client.server - return - else: - self.client.try_next_ip() + + self.client.do_connect() + self.assert_(self.client.socket.state == 'CONNECTED') + self.client.do_disconnect() + self.assert_(self.client.socket.state == 'DISCONNECTED') + From 937bb01a6908570533dfc28deda7c8e483526f59 Mon Sep 17 00:00:00 2001 From: tomk Date: Mon, 30 Jun 2008 23:02:12 +0000 Subject: [PATCH 06/20] moved TLS and SSL classes from transports_nb to new tls_nb module, fixed HTTP CONNECT proxy transport --- src/common/xmpp/client.py | 4 +- src/common/xmpp/client_nb.py | 13 +- src/common/xmpp/tls_nb.py | 437 ++++++++++++++++++++++++ src/common/xmpp/transports_nb.py | 561 +++++-------------------------- 4 files changed, 529 insertions(+), 486 deletions(-) create mode 100644 src/common/xmpp/tls_nb.py diff --git a/src/common/xmpp/client.py b/src/common/xmpp/client.py index aba778780..201da2a78 100644 --- a/src/common/xmpp/client.py +++ b/src/common/xmpp/client.py @@ -70,7 +70,9 @@ class PlugIn: self._old_owners_methods.append(owner.__dict__[method.__name__]) owner.__dict__[method.__name__]=method owner.__dict__[self.__class__.__name__]=self - if self.__class__.__dict__.has_key('plugin'): return self.plugin(owner) + # following will not work for classes inheriting plugin() + #if self.__class__.__dict__.has_key('plugin'): return self.plugin(owner) + if hasattr(self,'plugin'): return self.plugin(owner) def PlugOut(self): """ Unregister all our staff from main instance and detach from it. """ diff --git a/src/common/xmpp/client_nb.py b/src/common/xmpp/client_nb.py index f362d02a9..308300151 100644 --- a/src/common/xmpp/client_nb.py +++ b/src/common/xmpp/client_nb.py @@ -25,7 +25,7 @@ import socket import debug import random -import transports_nb, dispatcher_nb, auth_nb, roster_nb, protocol +import transports_nb, tls_nb, dispatcher_nb, auth_nb, roster_nb, protocol from client import * import logging @@ -184,10 +184,11 @@ class NBCommonClient: bosh_uri = proxy['host'], bosh_port = tcp_port) else: - self.socket = transports_nb.NBHTTPProxySocket( - on_disconnect=self.on_disconnect, - proxy_creds=(None, None), - xmpp_server=(self.Server, self.Port)) + # HTTP CONNECT to proxy from environment variables + self.socket = transports_nb.NBHTTPProxySocket( + on_disconnect=self.on_disconnect, + proxy_creds=(None, None), + xmpp_server=(self.Server, self.Port)) else: self._on_tcp_failure = self._on_connect_failure tcp_server=self.Server @@ -432,7 +433,7 @@ class NonBlockingClient(NBCommonClient): self._on_connect() return # otherwise start TLS - transports_nb.NonBlockingTLS().PlugIn( + tls_nb.NonBlockingTLS().PlugIn( self, on_tls_success=lambda: self._xmpp_connect(socket_type='tls'), on_tls_failure=self._on_connect_failure) diff --git a/src/common/xmpp/tls_nb.py b/src/common/xmpp/tls_nb.py new file mode 100644 index 000000000..e0b975de9 --- /dev/null +++ b/src/common/xmpp/tls_nb.py @@ -0,0 +1,437 @@ +## tls_nb.py +## based on transports_nb.py +## +## Copyright (C) 2003-2004 Alexey "Snake" Nezhdanov +## modified by Dimitur Kirov +## +## 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 2, 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. + +import socket +from client import PlugIn +from protocol import * + +import sys +import os +import errno +import time + +import traceback + +import logging + +log = logging.getLogger('gajim.c.x.tls_nb') +consoleloghandler = logging.StreamHandler() +consoleloghandler.setLevel(logging.DEBUG) +consoleloghandler.setFormatter( + logging.Formatter('%(levelname)s: %(message)s') +) +log.setLevel(logging.DEBUG) +log.addHandler(consoleloghandler) +log.propagate = False +# I don't need to load gajim.py just because of few TLS variables, so I changed +# %s/common\.gajim\.DATA_DIR/\'\.\.\/data\'/c +# %s/common\.gajim\.MY_CACERTS/\'\%s\/\.gajim\/cacerts\.pem\' \% os\.environ\[\'HOME\'\]/c + +# To change it back do: +# %s/\'\.\.\/data\'/common\.gajim\.DATA_DIR/c +# %s/\'%s\/\.gajim\/cacerts\.pem\'\ %\ os\.environ\[\'HOME\'\]/common\.gajim\.MY_CACERTS/c +# TODO: make the paths configurable - as constructor parameters or sth + +# import common.gajim + +USE_PYOPENSSL = False + +try: + #raise ImportError("Manually disabled PyOpenSSL") + import OpenSSL.SSL + import OpenSSL.crypto + USE_PYOPENSSL = True + log.info("PyOpenSSL loaded") +except ImportError: + log.debug("Import of PyOpenSSL failed:", exc_info=True) + + # FIXME: Remove these prints before release, replace with a warning dialog. + print >> sys.stderr, "=" * 79 + print >> sys.stderr, "PyOpenSSL not found, falling back to Python builtin SSL objects (insecure)." + print >> sys.stderr, "=" * 79 + + +def torf(cond, tv, fv): + if cond: return tv + return fv + +def gattr(obj, attr, default=None): + try: + return getattr(obj, attr) + except: + return default + +class SSLWrapper: + class Error(IOError): + def __init__(self, sock=None, exc=None, errno=None, strerror=None, peer=None): + self.parent = IOError + + errno = errno or gattr(exc, 'errno') + strerror = strerror or gattr(exc, 'strerror') or gattr(exc, 'args') + if not isinstance(strerror, basestring): strerror = repr(strerror) + + self.sock = sock + self.exc = exc + self.peer = peer + self.exc_name = None + self.exc_args = None + self.exc_str = None + self.exc_repr = None + + if self.exc is not None: + self.exc_name = str(self.exc.__class__) + self.exc_args = gattr(self.exc, 'args') + self.exc_str = str(self.exc) + self.exc_repr = repr(self.exc) + if not errno: + try: + if isinstance(exc, OpenSSL.SSL.SysCallError): + if self.exc_args[0] > 0: + errno = self.exc_args[0] + strerror = self.exc_args[1] + except: pass + + self.parent.__init__(self, errno, strerror) + + if self.peer is None and sock is not None: + try: + ppeer = self.sock.getpeername() + if len(ppeer) == 2 and isinstance(ppeer[0], basestring) \ + and isinstance(ppeer[1], int): + self.peer = ppeer + except: pass + + def __str__(self): + s = str(self.__class__) + if self.peer: s += " for %s:%d" % self.peer + if self.errno is not None: s += ": [Errno: %d]" % self.errno + if self.strerror: s += " (%s)" % self.strerror + if self.exc_name: + s += ", Caused by %s" % self.exc_name + if self.exc_str: + if self.strerror: s += "(%s)" % self.exc_str + else: s += "(%s)" % str(self.exc_args) + return s + + def __init__(self, sslobj, sock=None): + self.sslobj = sslobj + self.sock = sock + log.debug("%s.__init__ called with %s", self.__class__, sslobj) + + def recv(self, data, flags=None): + """ Receive wrapper for SSL object + + We can return None out of this function to signal that no data is + available right now. Better than an exception, which differs + depending on which SSL lib we're using. Unfortunately returning '' + can indicate that the socket has been closed, so to be sure, we avoid + this by returning None. """ + + raise NotImplementedException() + + def send(self, data, flags=None, now = False): + raise NotImplementedException() + +class PyOpenSSLWrapper(SSLWrapper): + '''Wrapper class for PyOpenSSL's recv() and send() methods''' + + def __init__(self, *args): + self.parent = SSLWrapper + self.parent.__init__(self, *args) + + def is_numtoolarge(self, e): + t = ('asn1 encoding routines', 'a2d_ASN1_OBJECT', 'first num too large') + return isinstance(e.args, (list, tuple)) and len(e.args) == 1 and \ + isinstance(e.args[0], (list, tuple)) and len(e.args[0]) == 2 and \ + e.args[0][0] == e.args[0][1] == t + + def recv(self, bufsize, flags=None): + retval = None + try: + if flags is None: retval = self.sslobj.recv(bufsize) + else: retval = self.sslobj.recv(bufsize, flags) + except (OpenSSL.SSL.WantReadError, OpenSSL.SSL.WantWriteError), e: + pass + # log.debug("Recv: " + repr(e)) + except OpenSSL.SSL.SysCallError, e: + log.debug("Recv: Got OpenSSL.SSL.SysCallError: " + repr(e), exc_info=True) + #traceback.print_exc() + raise SSLWrapper.Error(self.sock or self.sslobj, e) + except OpenSSL.SSL.Error, e: + if self.is_numtoolarge(e): + # warn, but ignore this exception + log.warning("Recv: OpenSSL: asn1enc: first num too large (ignored)") + else: + log.debug("Recv: Caught OpenSSL.SSL.Error:", exc_info=True) + #traceback.print_exc() + #print "Current Stack:" + #traceback.print_stack() + raise SSLWrapper.Error(self.sock or self.sslobj, e) + return retval + + def send(self, data, flags=None, now = False): + try: + if flags is None: return self.sslobj.send(data) + else: return self.sslobj.send(data, flags) + except (OpenSSL.SSL.WantReadError, OpenSSL.SSL.WantWriteError), e: + #log.debug("Send: " + repr(e)) + time.sleep(0.1) # prevent 100% CPU usage + except OpenSSL.SSL.SysCallError, e: + log.error("Send: Got OpenSSL.SSL.SysCallError: " + repr(e), exc_info=True) + #traceback.print_exc() + raise SSLWrapper.Error(self.sock or self.sslobj, e) + except OpenSSL.SSL.Error, e: + if self.is_numtoolarge(e): + # warn, but ignore this exception + log.warning("Send: OpenSSL: asn1enc: first num too large (ignored)") + else: + log.error("Send: Caught OpenSSL.SSL.Error:", exc_info=True) + #traceback.print_exc() + #print "Current Stack:" + #traceback.print_stack() + raise SSLWrapper.Error(self.sock or self.sslobj, e) + return 0 + +class StdlibSSLWrapper(SSLWrapper): + '''Wrapper class for Python's socket.ssl read() and write() methods''' + + def __init__(self, *args): + self.parent = SSLWrapper + self.parent.__init__(self, *args) + + def recv(self, bufsize, flags=None): + # we simply ignore flags since ssl object doesn't support it + try: + return self.sslobj.read(bufsize) + except socket.sslerror, e: + #log.debug("Recv: Caught socket.sslerror:", exc_info=True) + #traceback.print_exc() + if e.args[0] not in (socket.SSL_ERROR_WANT_READ, socket.SSL_ERROR_WANT_WRITE): + raise SSLWrapper.Error(self.sock or self.sslobj, e) + return None + + def send(self, data, flags=None, now = False): + # we simply ignore flags since ssl object doesn't support it + try: + return self.sslobj.write(data) + except socket.sslerror, e: + #log.debug("Send: Caught socket.sslerror:", exc_info=True) + #traceback.print_exc() + if e.args[0] not in (socket.SSL_ERROR_WANT_READ, socket.SSL_ERROR_WANT_WRITE): + raise SSLWrapper.Error(self.sock or self.sslobj, e) + return 0 + + +class NonBlockingTLS(PlugIn): + ''' TLS connection used to encrypts already estabilished tcp connection.''' + + # from ssl.h (partial extract) + ssl_h_bits = { "SSL_ST_CONNECT": 0x1000, "SSL_ST_ACCEPT": 0x2000, + "SSL_CB_LOOP": 0x01, "SSL_CB_EXIT": 0x02, + "SSL_CB_READ": 0x04, "SSL_CB_WRITE": 0x08, + "SSL_CB_ALERT": 0x4000, + "SSL_CB_HANDSHAKE_START": 0x10, "SSL_CB_HANDSHAKE_DONE": 0x20} + + def PlugIn(self, owner, on_tls_success, on_tls_failure, now=0): + ''' If the 'now' argument is true then starts using encryption immidiatedly. + If 'now' in false then starts encryption as soon as TLS feature is + declared by the server (if it were already declared - it is ok). + ''' + if owner.__dict__.has_key('NonBlockingTLS'): + return # Already enabled. + PlugIn.PlugIn(self, owner) + DBG_LINE='NonBlockingTLS' + self.on_tls_success = on_tls_success + self.on_tls_faliure = on_tls_failure + if now: + try: + res = self._startSSL() + except Exception, e: + log.error("PlugIn: while trying _startSSL():", exc_info=True) + #traceback.print_exc() + self._owner.socket.pollend() + return + on_tls_success() + return res + if self._owner.Dispatcher.Stream.features: + try: + self.FeaturesHandler(self._owner.Dispatcher, self._owner.Dispatcher.Stream.features) + except NodeProcessed: + pass + else: + self._owner.RegisterHandlerOnce('features',self.FeaturesHandler, xmlns=NS_STREAMS) + self.starttls = None + + def plugout(self,now=0): + ''' Unregisters TLS handler's from owner's dispatcher. Take note that encription + can not be stopped once started. You can only break the connection and start over.''' + # if dispatcher is not plugged we cannot (un)register handlers + if self._owner.__dict__.has_key('Dispatcher'): + self._owner.UnregisterHandler('features', self.FeaturesHandler,xmlns=NS_STREAMS) + self._owner.Dispatcher.PlugOut() + self._owner = None + + def FeaturesHandler(self, conn, feats): + ''' Used to analyse server tag for TLS support. + If TLS is supported starts the encryption negotiation. Used internally ''' + if not feats.getTag('starttls', namespace=NS_TLS): + self.DEBUG("TLS unsupported by remote server.", 'warn') + self.on_tls_failure("TLS unsupported by remote server.") + return + self.DEBUG("TLS supported by remote server. Requesting TLS start.", 'ok') + self._owner.RegisterHandlerOnce('proceed', self.StartTLSHandler, xmlns=NS_TLS) + self._owner.RegisterHandlerOnce('failure', self.StartTLSHandler, xmlns=NS_TLS) + self._owner.send('' % NS_TLS) + raise NodeProcessed + + def _dumpX509(self, cert, stream=sys.stderr): + print >> stream, "Digest (SHA-1):", cert.digest("sha1") + print >> stream, "Digest (MD5):", cert.digest("md5") + print >> stream, "Serial #:", cert.get_serial_number() + print >> stream, "Version:", cert.get_version() + print >> stream, "Expired:", torf(cert.has_expired(), "Yes", "No") + print >> stream, "Subject:" + self._dumpX509Name(cert.get_subject(), stream) + print >> stream, "Issuer:" + self._dumpX509Name(cert.get_issuer(), stream) + self._dumpPKey(cert.get_pubkey(), stream) + + def _dumpX509Name(self, name, stream=sys.stderr): + print >> stream, "X509Name:", str(name) + + def _dumpPKey(self, pkey, stream=sys.stderr): + typedict = {OpenSSL.crypto.TYPE_RSA: "RSA", OpenSSL.crypto.TYPE_DSA: "DSA"} + print >> stream, "PKey bits:", pkey.bits() + print >> stream, "PKey type: %s (%d)" % (typedict.get(pkey.type(), "Unknown"), pkey.type()) + + def _startSSL(self): + ''' Immidiatedly switch socket to TLS mode. Used internally.''' + log.debug("_startSSL called") + if USE_PYOPENSSL: return self._startSSL_pyOpenSSL() + return self._startSSL_stdlib() + + def _startSSL_pyOpenSSL(self): + #log.debug("_startSSL_pyOpenSSL called, thread id: %s", str(thread.get_ident())) + log.debug("_startSSL_pyOpenSSL called") + tcpsock = self._owner.Connection + # FIXME: should method be configurable? + tcpsock._sslContext = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_METHOD) + #tcpsock._sslContext = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD) + tcpsock.ssl_errnum = 0 + tcpsock._sslContext.set_verify(OpenSSL.SSL.VERIFY_PEER, self._ssl_verify_callback) + cacerts = os.path.join('../data', 'other', 'cacerts.pem') + try: + tcpsock._sslContext.load_verify_locations(cacerts) + except: + log.warning('Unable to load SSL certificats from file %s' % \ + os.path.abspath(cacerts)) + # load users certs + if os.path.isfile('%s/.gajim/cacerts.pem' % os.environ['HOME']): + store = tcpsock._sslContext.get_cert_store() + f = open('%s/.gajim/cacerts.pem' % os.environ['HOME']) + lines = f.readlines() + i = 0 + begin = -1 + for line in lines: + if 'BEGIN CERTIFICATE' in line: + begin = i + elif 'END CERTIFICATE' in line and begin > -1: + cert = ''.join(lines[begin:i+2]) + try: + X509cert = OpenSSL.crypto.load_certificate( + OpenSSL.crypto.FILETYPE_PEM, cert) + store.add_cert(X509cert) + except OpenSSL.crypto.Error, exception_obj: + log.warning('Unable to load a certificate from file %s: %s' %\ + ('%s/.gajim/cacerts.pem' % os.environ['HOME'], exception_obj.args[0][0][2])) + except: + log.warning( + 'Unknown error while loading certificate from file %s' % \ + '%s/.gajim/cacerts.pem' % os.environ['HOME']) + begin = -1 + i += 1 + tcpsock._sslObj = OpenSSL.SSL.Connection(tcpsock._sslContext, tcpsock._sock) + tcpsock._sslObj.set_connect_state() # set to client mode + + wrapper = PyOpenSSLWrapper(tcpsock._sslObj) + tcpsock._recv = wrapper.recv + tcpsock._send = wrapper.send + + log.debug("Initiating handshake...") + # FIXME: Figure out why _connect_success is called before the + # SSL handshake is completed in STARTTLS mode. See #2838. + tcpsock._sslObj.setblocking(True) + try: + self.starttls='in progress' + tcpsock._sslObj.do_handshake() + except: + log.error('Error while TLS handshake: ', exc_info=True) + self.on_tls_failure('Error while TLS Handshake') + return + tcpsock._sslObj.setblocking(False) + log.debug("Synchronous handshake completed") + #log.debug("Async handshake started...") + + # fake it, for now + self.starttls='success' + + def _startSSL_stdlib(self): + log.debug("_startSSL_stdlib called") + tcpsock=self._owner.Connection + tcpsock._sock.setblocking(True) + tcpsock._sslObj = socket.ssl(tcpsock._sock, None, None) + tcpsock._sock.setblocking(False) + tcpsock._sslIssuer = tcpsock._sslObj.issuer() + tcpsock._sslServer = tcpsock._sslObj.server() + wrapper = StdlibSSLWrapper(tcpsock._sslObj, tcpsock._sock) + tcpsock._recv = wrapper.recv + tcpsock._send = wrapper.send + self.starttls='success' + + def _ssl_verify_callback(self, sslconn, cert, errnum, depth, ok): + # Exceptions can't propagate up through this callback, so print them here. + try: + self._owner.Connection.ssl_fingerprint_sha1 = cert.digest('sha1') + if errnum == 0: + return True + self._owner.Connection.ssl_errnum = errnum + self._owner.Connection.ssl_cert_pem = OpenSSL.crypto.dump_certificate( + OpenSSL.crypto.FILETYPE_PEM, cert) + return True + except: + log.error("Exception caught in _ssl_info_callback:", exc_info=True) + traceback.print_exc() # Make sure something is printed, even if log is disabled. + + def StartTLSHandler(self, conn, starttls): + ''' Handle server reply if TLS is allowed to process. Behaves accordingly. + Used internally.''' + if starttls.getNamespace() <> NS_TLS: + self.on_tls_failure('Unknown namespace: %s' % starttls.getNamespace()) + return + self.starttls = starttls.getName() + if self.starttls == 'failure': + self.on_tls_failure('TLS received: %s' % self.starttls) + return + self.DEBUG('Got starttls proceed response. Switching to TLS/SSL...','ok') + try: + self._startSSL() + except Exception, e: + log.error("StartTLSHandler:", exc_info=True) + self.on_tls_failure('in StartTLSHandler') + #traceback.print_exc() + return + self._owner.Dispatcher.PlugOut() + self.on_tls_success() diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py index 4ca3c51aa..c69f33be8 100644 --- a/src/common/xmpp/transports_nb.py +++ b/src/common/xmpp/transports_nb.py @@ -14,8 +14,8 @@ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. -import socket,select,base64,dispatcher_nb -import struct +import socket,base64 + from simplexml import ustr from client import PlugIn from idlequeue import IdleObject @@ -27,7 +27,6 @@ import errno import time import traceback -import threading import logging log = logging.getLogger('gajim.c.x.transports_nb') @@ -52,21 +51,6 @@ def urisplit(self, uri): grouped = re.match(regex, uri).groups() proto, host, path = grouped[1], grouped[3], grouped[4] return proto, host, path - - - - -# I don't need to load gajim.py just because of few TLS variables, so I changed -# %s/common\.gajim\.DATA_DIR/\'\.\.\/data\'/c -# %s/common\.gajim\.MY_CACERTS/\'\%s\/\.gajim\/cacerts\.pem\' \% os\.environ\[\'HOME\'\]/c - -# To change it back do: -# %s/\'\.\.\/data\'/common\.gajim\.DATA_DIR/c -# %s/\'%s\/\.gajim\/cacerts\.pem\'\ %\ os\.environ\[\'HOME\'\]/common\.gajim\.MY_CACERTS/c -# TODO: make the paths configurable - as constructor parameters or sth - - -# import common.gajim # timeout to connect to the server socket, it doesn't include auth CONNECT_TIMEOUT_SECONDS = 30 @@ -130,8 +114,8 @@ class NonBlockingTcp(PlugIn, IdleObject): self.set_timeout, self.remove_timeout] def plugin(self, owner): - owner.Connection=self print 'plugin called' + owner.Connection=self self.idlequeue = owner.idlequeue def plugout(self): @@ -372,6 +356,7 @@ class NonBlockingTcp(PlugIn, IdleObject): else: self.on_receive = None return + log.debug('setting onreceive on %s' % recv_handler) self.on_receive = recv_handler @@ -406,484 +391,33 @@ class NonBlockingTcp(PlugIn, IdleObject): # in case of some other exception # FIXME: is this needed?? if errnum != 0: - self.DEBUG(self.DBG, errstr, 'error') log.error("CConnection to %s lost: %s %s" % (self.server, errnum, errstr)) - if not errors_only and self.state in [CONNECTING, CONNECTED]: - self.pollend(retry=True) + self.disconnect() return received = '' # we have received some bytes, stop the timeout! self.renew_send_timeout() + # pass received data to owner + #self. if self.on_receive: self._raise_event(DATA_RECEIVED, received) self._on_receive(received) else: - # This should never happen, so we need the debug + # This should never happen, so we need the debug. (If there is no handler + # on receive spacified, data are passed to Dispatcher.ProcessNonBlocking) log.error('SOCKET Unhandled data received: %s' % received) self.disconnect() def _on_receive(self, data): # Overriding this method allows modifying received data before it is passed - # to callback. + # to owner's callback. + log.debug('About to call on_receive which is %s' % self.on_receive) self.on_receive(data) -class NonBlockingHttpBOSH(NonBlockingTcp): - ''' - Socket wrapper that makes HTTP message out of send data and peels-off - HTTP headers from incoming messages - ''' - - def __init__(self, bosh_uri, bosh_port, on_disconnect): - self.bosh_protocol, self.bosh_host, self.bosh_path = self.urisplit(bosh_uri) - if self.bosh_protocol is None: - self.bosh_protocol = 'http' - if self.bosh_path == '': - bosh_path = '/' - self.bosh_port = bosh_port - - def send(self, raw_data, now=False): - - NonBlockingTcp.send( - self, - self.build_http_message(raw_data), - now) - - def _on_receive(self,data): - '''Preceeds pass of received data to Client class. Gets rid of HTTP headers - and checks them.''' - statusline, headers, httpbody = self.parse_http_message(data) - if statusline[1] != '200': - log.error('HTTP Error: %s %s' % (statusline[1], statusline[2])) - self.disconnect() - self.on_receive(httpbody) - - - def build_http_message(self, httpbody): - ''' - Builds bosh http message with given body. - Values for headers and status line fields are taken from class variables. - ) - ''' - headers = ['POST %s HTTP/1.1' % self.bosh_path, - 'Host: %s:%s' % (self.bosh_host, self.bosh_port), - 'Content-Type: text/xml; charset=utf-8', - 'Content-Length: %s' % len(str(httpbody)), - '\r\n'] - headers = '\r\n'.join(headers) - return('%s%s\r\n' % (headers, httpbody)) - - def parse_http_message(self, message): - ''' - splits http message to tuple ( - statusline - list of e.g. ['HTTP/1.1', '200', 'OK'], - headers - dictionary of headers e.g. {'Content-Length': '604', - 'Content-Type': 'text/xml; charset=utf-8'}, - httpbody - string with http body - ) - ''' - message = message.replace('\r','') - (header, httpbody) = message.split('\n\n') - header = header.split('\n') - statusline = header[0].split(' ') - header = header[1:] - headers = {} - for dummy in header: - row = dummy.split(' ',1) - headers[row[0][:-1]] = row[1] - return (statusline, headers, httpbody) -USE_PYOPENSSL = False - -try: - #raise ImportError("Manually disabled PyOpenSSL") - import OpenSSL.SSL - import OpenSSL.crypto - USE_PYOPENSSL = True - log.info("PyOpenSSL loaded") -except ImportError: - log.debug("Import of PyOpenSSL failed:", exc_info=True) - - # FIXME: Remove these prints before release, replace with a warning dialog. - print >> sys.stderr, "=" * 79 - print >> sys.stderr, "PyOpenSSL not found, falling back to Python builtin SSL objects (insecure)." - print >> sys.stderr, "=" * 79 - - -def torf(cond, tv, fv): - if cond: return tv - return fv - -def gattr(obj, attr, default=None): - try: - return getattr(obj, attr) - except: - return default - -class SSLWrapper: - class Error(IOError): - def __init__(self, sock=None, exc=None, errno=None, strerror=None, peer=None): - self.parent = IOError - - errno = errno or gattr(exc, 'errno') - strerror = strerror or gattr(exc, 'strerror') or gattr(exc, 'args') - if not isinstance(strerror, basestring): strerror = repr(strerror) - - self.sock = sock - self.exc = exc - self.peer = peer - self.exc_name = None - self.exc_args = None - self.exc_str = None - self.exc_repr = None - - if self.exc is not None: - self.exc_name = str(self.exc.__class__) - self.exc_args = gattr(self.exc, 'args') - self.exc_str = str(self.exc) - self.exc_repr = repr(self.exc) - if not errno: - try: - if isinstance(exc, OpenSSL.SSL.SysCallError): - if self.exc_args[0] > 0: - errno = self.exc_args[0] - strerror = self.exc_args[1] - except: pass - - self.parent.__init__(self, errno, strerror) - - if self.peer is None and sock is not None: - try: - ppeer = self.sock.getpeername() - if len(ppeer) == 2 and isinstance(ppeer[0], basestring) \ - and isinstance(ppeer[1], int): - self.peer = ppeer - except: pass - - def __str__(self): - s = str(self.__class__) - if self.peer: s += " for %s:%d" % self.peer - if self.errno is not None: s += ": [Errno: %d]" % self.errno - if self.strerror: s += " (%s)" % self.strerror - if self.exc_name: - s += ", Caused by %s" % self.exc_name - if self.exc_str: - if self.strerror: s += "(%s)" % self.exc_str - else: s += "(%s)" % str(self.exc_args) - return s - - def __init__(self, sslobj, sock=None): - self.sslobj = sslobj - self.sock = sock - log.debug("%s.__init__ called with %s", self.__class__, sslobj) - - def recv(self, data, flags=None): - """ Receive wrapper for SSL object - - We can return None out of this function to signal that no data is - available right now. Better than an exception, which differs - depending on which SSL lib we're using. Unfortunately returning '' - can indicate that the socket has been closed, so to be sure, we avoid - this by returning None. """ - - raise NotImplementedException() - - def send(self, data, flags=None, now = False): - raise NotImplementedException() - -class PyOpenSSLWrapper(SSLWrapper): - '''Wrapper class for PyOpenSSL's recv() and send() methods''' - - def __init__(self, *args): - self.parent = SSLWrapper - self.parent.__init__(self, *args) - - def is_numtoolarge(self, e): - t = ('asn1 encoding routines', 'a2d_ASN1_OBJECT', 'first num too large') - return isinstance(e.args, (list, tuple)) and len(e.args) == 1 and \ - isinstance(e.args[0], (list, tuple)) and len(e.args[0]) == 2 and \ - e.args[0][0] == e.args[0][1] == t - - def recv(self, bufsize, flags=None): - retval = None - try: - if flags is None: retval = self.sslobj.recv(bufsize) - else: retval = self.sslobj.recv(bufsize, flags) - except (OpenSSL.SSL.WantReadError, OpenSSL.SSL.WantWriteError), e: - pass - # log.debug("Recv: " + repr(e)) - except OpenSSL.SSL.SysCallError, e: - log.debug("Recv: Got OpenSSL.SSL.SysCallError: " + repr(e), exc_info=True) - #traceback.print_exc() - raise SSLWrapper.Error(self.sock or self.sslobj, e) - except OpenSSL.SSL.Error, e: - if self.is_numtoolarge(e): - # warn, but ignore this exception - log.warning("Recv: OpenSSL: asn1enc: first num too large (ignored)") - else: - log.debug("Recv: Caught OpenSSL.SSL.Error:", exc_info=True) - #traceback.print_exc() - #print "Current Stack:" - #traceback.print_stack() - raise SSLWrapper.Error(self.sock or self.sslobj, e) - return retval - - def send(self, data, flags=None, now = False): - try: - if flags is None: return self.sslobj.send(data) - else: return self.sslobj.send(data, flags) - except (OpenSSL.SSL.WantReadError, OpenSSL.SSL.WantWriteError), e: - #log.debug("Send: " + repr(e)) - time.sleep(0.1) # prevent 100% CPU usage - except OpenSSL.SSL.SysCallError, e: - log.error("Send: Got OpenSSL.SSL.SysCallError: " + repr(e), exc_info=True) - #traceback.print_exc() - raise SSLWrapper.Error(self.sock or self.sslobj, e) - except OpenSSL.SSL.Error, e: - if self.is_numtoolarge(e): - # warn, but ignore this exception - log.warning("Send: OpenSSL: asn1enc: first num too large (ignored)") - else: - log.error("Send: Caught OpenSSL.SSL.Error:", exc_info=True) - #traceback.print_exc() - #print "Current Stack:" - #traceback.print_stack() - raise SSLWrapper.Error(self.sock or self.sslobj, e) - return 0 - -class StdlibSSLWrapper(SSLWrapper): - '''Wrapper class for Python's socket.ssl read() and write() methods''' - - def __init__(self, *args): - self.parent = SSLWrapper - self.parent.__init__(self, *args) - - def recv(self, bufsize, flags=None): - # we simply ignore flags since ssl object doesn't support it - try: - return self.sslobj.read(bufsize) - except socket.sslerror, e: - #log.debug("Recv: Caught socket.sslerror:", exc_info=True) - #traceback.print_exc() - if e.args[0] not in (socket.SSL_ERROR_WANT_READ, socket.SSL_ERROR_WANT_WRITE): - raise SSLWrapper.Error(self.sock or self.sslobj, e) - return None - - def send(self, data, flags=None, now = False): - # we simply ignore flags since ssl object doesn't support it - try: - return self.sslobj.write(data) - except socket.sslerror, e: - #log.debug("Send: Caught socket.sslerror:", exc_info=True) - #traceback.print_exc() - if e.args[0] not in (socket.SSL_ERROR_WANT_READ, socket.SSL_ERROR_WANT_WRITE): - raise SSLWrapper.Error(self.sock or self.sslobj, e) - return 0 - - -class NonBlockingTLS(PlugIn): - ''' TLS connection used to encrypts already estabilished tcp connection.''' - - # from ssl.h (partial extract) - ssl_h_bits = { "SSL_ST_CONNECT": 0x1000, "SSL_ST_ACCEPT": 0x2000, - "SSL_CB_LOOP": 0x01, "SSL_CB_EXIT": 0x02, - "SSL_CB_READ": 0x04, "SSL_CB_WRITE": 0x08, - "SSL_CB_ALERT": 0x4000, - "SSL_CB_HANDSHAKE_START": 0x10, "SSL_CB_HANDSHAKE_DONE": 0x20} - - def PlugIn(self, owner, on_tls_success, on_tls_failure, now=0): - ''' If the 'now' argument is true then starts using encryption immidiatedly. - If 'now' in false then starts encryption as soon as TLS feature is - declared by the server (if it were already declared - it is ok). - ''' - if owner.__dict__.has_key('NonBlockingTLS'): - return # Already enabled. - PlugIn.PlugIn(self, owner) - DBG_LINE='NonBlockingTLS' - self.on_tls_success = on_tls_success - self.on_tls_faliure = on_tls_failure - if now: - try: - res = self._startSSL() - except Exception, e: - log.error("PlugIn: while trying _startSSL():", exc_info=True) - #traceback.print_exc() - self._owner.socket.pollend() - return - on_tls_success() - return res - if self._owner.Dispatcher.Stream.features: - try: - self.FeaturesHandler(self._owner.Dispatcher, self._owner.Dispatcher.Stream.features) - except NodeProcessed: - pass - else: - self._owner.RegisterHandlerOnce('features',self.FeaturesHandler, xmlns=NS_STREAMS) - self.starttls = None - - def plugout(self,now=0): - ''' Unregisters TLS handler's from owner's dispatcher. Take note that encription - can not be stopped once started. You can only break the connection and start over.''' - # if dispatcher is not plugged we cannot (un)register handlers - if self._owner.__dict__.has_key('Dispatcher'): - self._owner.UnregisterHandler('features', self.FeaturesHandler,xmlns=NS_STREAMS) - self._owner.Dispatcher.PlugOut() - self._owner = None - - def FeaturesHandler(self, conn, feats): - ''' Used to analyse server tag for TLS support. - If TLS is supported starts the encryption negotiation. Used internally ''' - if not feats.getTag('starttls', namespace=NS_TLS): - self.DEBUG("TLS unsupported by remote server.", 'warn') - self.on_tls_failure("TLS unsupported by remote server.") - return - self.DEBUG("TLS supported by remote server. Requesting TLS start.", 'ok') - self._owner.RegisterHandlerOnce('proceed', self.StartTLSHandler, xmlns=NS_TLS) - self._owner.RegisterHandlerOnce('failure', self.StartTLSHandler, xmlns=NS_TLS) - self._owner.send('' % NS_TLS) - raise NodeProcessed - - def _dumpX509(self, cert, stream=sys.stderr): - print >> stream, "Digest (SHA-1):", cert.digest("sha1") - print >> stream, "Digest (MD5):", cert.digest("md5") - print >> stream, "Serial #:", cert.get_serial_number() - print >> stream, "Version:", cert.get_version() - print >> stream, "Expired:", torf(cert.has_expired(), "Yes", "No") - print >> stream, "Subject:" - self._dumpX509Name(cert.get_subject(), stream) - print >> stream, "Issuer:" - self._dumpX509Name(cert.get_issuer(), stream) - self._dumpPKey(cert.get_pubkey(), stream) - - def _dumpX509Name(self, name, stream=sys.stderr): - print >> stream, "X509Name:", str(name) - - def _dumpPKey(self, pkey, stream=sys.stderr): - typedict = {OpenSSL.crypto.TYPE_RSA: "RSA", OpenSSL.crypto.TYPE_DSA: "DSA"} - print >> stream, "PKey bits:", pkey.bits() - print >> stream, "PKey type: %s (%d)" % (typedict.get(pkey.type(), "Unknown"), pkey.type()) - - def _startSSL(self): - ''' Immidiatedly switch socket to TLS mode. Used internally.''' - log.debug("_startSSL called") - if USE_PYOPENSSL: return self._startSSL_pyOpenSSL() - return self._startSSL_stdlib() - - def _startSSL_pyOpenSSL(self): - #log.debug("_startSSL_pyOpenSSL called, thread id: %s", str(thread.get_ident())) - log.debug("_startSSL_pyOpenSSL called") - tcpsock = self._owner.Connection - # FIXME: should method be configurable? - tcpsock._sslContext = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_METHOD) - #tcpsock._sslContext = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD) - tcpsock.ssl_errnum = 0 - tcpsock._sslContext.set_verify(OpenSSL.SSL.VERIFY_PEER, self._ssl_verify_callback) - cacerts = os.path.join('../data', 'other', 'cacerts.pem') - try: - tcpsock._sslContext.load_verify_locations(cacerts) - except: - log.warning('Unable to load SSL certificats from file %s' % \ - os.path.abspath(cacerts)) - # load users certs - if os.path.isfile('%s/.gajim/cacerts.pem' % os.environ['HOME']): - store = tcpsock._sslContext.get_cert_store() - f = open('%s/.gajim/cacerts.pem' % os.environ['HOME']) - lines = f.readlines() - i = 0 - begin = -1 - for line in lines: - if 'BEGIN CERTIFICATE' in line: - begin = i - elif 'END CERTIFICATE' in line and begin > -1: - cert = ''.join(lines[begin:i+2]) - try: - X509cert = OpenSSL.crypto.load_certificate( - OpenSSL.crypto.FILETYPE_PEM, cert) - store.add_cert(X509cert) - except OpenSSL.crypto.Error, exception_obj: - log.warning('Unable to load a certificate from file %s: %s' %\ - ('%s/.gajim/cacerts.pem' % os.environ['HOME'], exception_obj.args[0][0][2])) - except: - log.warning( - 'Unknown error while loading certificate from file %s' % \ - '%s/.gajim/cacerts.pem' % os.environ['HOME']) - begin = -1 - i += 1 - tcpsock._sslObj = OpenSSL.SSL.Connection(tcpsock._sslContext, tcpsock._sock) - tcpsock._sslObj.set_connect_state() # set to client mode - - wrapper = PyOpenSSLWrapper(tcpsock._sslObj) - tcpsock._recv = wrapper.recv - tcpsock._send = wrapper.send - - log.debug("Initiating handshake...") - # FIXME: Figure out why _connect_success is called before the - # SSL handshake is completed in STARTTLS mode. See #2838. - tcpsock._sslObj.setblocking(True) - try: - self.starttls='in progress' - tcpsock._sslObj.do_handshake() - except: - log.error('Error while TLS handshake: ', exc_info=True) - self.on_tls_failure('Error while TLS Handshake') - return - tcpsock._sslObj.setblocking(False) - log.debug("Synchronous handshake completed") - #log.debug("Async handshake started...") - - # fake it, for now - self.starttls='success' - - def _startSSL_stdlib(self): - log.debug("_startSSL_stdlib called") - tcpsock=self._owner.Connection - tcpsock._sock.setblocking(True) - tcpsock._sslObj = socket.ssl(tcpsock._sock, None, None) - tcpsock._sock.setblocking(False) - tcpsock._sslIssuer = tcpsock._sslObj.issuer() - tcpsock._sslServer = tcpsock._sslObj.server() - wrapper = StdlibSSLWrapper(tcpsock._sslObj, tcpsock._sock) - tcpsock._recv = wrapper.recv - tcpsock._send = wrapper.send - self.starttls='success' - - def _ssl_verify_callback(self, sslconn, cert, errnum, depth, ok): - # Exceptions can't propagate up through this callback, so print them here. - try: - self._owner.Connection.ssl_fingerprint_sha1 = cert.digest('sha1') - if errnum == 0: - return True - self._owner.Connection.ssl_errnum = errnum - self._owner.Connection.ssl_cert_pem = OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_PEM, cert) - return True - except: - log.error("Exception caught in _ssl_info_callback:", exc_info=True) - traceback.print_exc() # Make sure something is printed, even if log is disabled. - - def StartTLSHandler(self, conn, starttls): - ''' Handle server reply if TLS is allowed to process. Behaves accordingly. - Used internally.''' - if starttls.getNamespace() <> NS_TLS: - self.on_tls_failure('Unknown namespace: %s' % starttls.getNamespace()) - return - self.starttls = starttls.getName() - if self.starttls == 'failure': - self.on_tls_failure('TLS received: %s' % self.starttls) - return - self.DEBUG('Got starttls proceed response. Switching to TLS/SSL...','ok') - try: - self._startSSL() - except Exception, e: - log.error("StartTLSHandler:", exc_info=True) - self.on_tls_failure('in StartTLSHandler') - #traceback.print_exc() - return - self._owner.Dispatcher.PlugOut() - self.on_tls_success() - #dispatcher_nb.Dispatcher().PlugIn(self._owner) class NBProxySocket(NonBlockingTcp): ''' @@ -957,8 +491,10 @@ class NBHTTPProxySocket(NBProxySocket): return if len(reply) != 2: pass - self.onreceive(self._on_proxy_auth) - + self.after_proxy_connect() + #self.onreceive(self._on_proxy_auth) + + # FIXME: find out what it this method for def _on_proxy_auth(self, reply): if self.reply.find('\n\n') == -1: if reply is None: @@ -1109,3 +645,70 @@ class NBSOCKS5ProxySocket(NBProxySocket): def DEBUG(self, text, severity): ''' Overwrites DEBUG tag to allow debug output be presented as "CONNECTproxy".''' return self._owner.DEBUG(DBG_CONNECT_PROXY, text, severity) + + + +class NonBlockingHttpBOSH(NonBlockingTcp): + ''' + Socket wrapper that makes HTTP message out of send data and peels-off + HTTP headers from incoming messages + ''' + + def __init__(self, bosh_uri, bosh_port, on_disconnect): + self.bosh_protocol, self.bosh_host, self.bosh_path = self.urisplit(bosh_uri) + if self.bosh_protocol is None: + self.bosh_protocol = 'http' + if self.bosh_path == '': + bosh_path = '/' + self.bosh_port = bosh_port + + def send(self, raw_data, now=False): + + NonBlockingTcp.send( + self, + self.build_http_message(raw_data), + now) + + def _on_receive(self,data): + '''Preceeds passing received data to Client class. Gets rid of HTTP headers + and checks them.''' + statusline, headers, httpbody = self.parse_http_message(data) + if statusline[1] != '200': + log.error('HTTP Error: %s %s' % (statusline[1], statusline[2])) + self.disconnect() + self.on_receive(httpbody) + + + def build_http_message(self, httpbody): + ''' + Builds bosh http message with given body. + Values for headers and status line fields are taken from class variables. + ) + ''' + headers = ['POST %s HTTP/1.1' % self.bosh_path, + 'Host: %s:%s' % (self.bosh_host, self.bosh_port), + 'Content-Type: text/xml; charset=utf-8', + 'Content-Length: %s' % len(str(httpbody)), + '\r\n'] + headers = '\r\n'.join(headers) + return('%s%s\r\n' % (headers, httpbody)) + + def parse_http_message(self, message): + ''' + splits http message to tuple ( + statusline - list of e.g. ['HTTP/1.1', '200', 'OK'], + headers - dictionary of headers e.g. {'Content-Length': '604', + 'Content-Type': 'text/xml; charset=utf-8'}, + httpbody - string with http body + ) + ''' + message = message.replace('\r','') + (header, httpbody) = message.split('\n\n') + header = header.split('\n') + statusline = header[0].split(' ') + header = header[1:] + headers = {} + for dummy in header: + row = dummy.split(' ',1) + headers[row[0][:-1]] = row[1] + return (statusline, headers, httpbody) From 952e4a1569f8eb08e070b61e751cfad05ba71650 Mon Sep 17 00:00:00 2001 From: tomk Date: Wed, 2 Jul 2008 23:29:10 +0000 Subject: [PATCH 07/20] moved bosh code from client_nb.py to bosh.py, replaced debug logging with debug.py by logging in whole xmpppy (debug.py is now unused) --- src/common/connection.py | 33 +++--- src/common/xmpp/__init__.py | 3 +- src/common/xmpp/auth_nb.py | 65 ++++++----- src/common/xmpp/bosh.py | 112 ++++++++++++++++++ src/common/xmpp/client.py | 45 +------ src/common/xmpp/client_nb.py | 162 +++++-------------------- src/common/xmpp/dispatcher_nb.py | 39 ++++--- src/common/xmpp/idlequeue.py | 7 +- src/common/xmpp/roster_nb.py | 11 +- src/common/xmpp/simplexml.py | 15 +-- src/common/xmpp/tls_nb.py | 24 ++-- src/common/xmpp/transports_nb.py | 195 ++++++++++++++++--------------- src/gajim.py | 2 +- 13 files changed, 356 insertions(+), 357 deletions(-) create mode 100644 src/common/xmpp/bosh.py diff --git a/src/common/connection.py b/src/common/connection.py index edb788aa3..649baf178 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -208,7 +208,7 @@ class Connection(ConnectionHandlers): def _disconnectedReconnCB(self): '''Called when we are disconnected''' - log.error('disconnectedReconnCB') + log.info('disconnectedReconnCB called') if gajim.account_is_connected(self.name): # we cannot change our status to offline or connecting # after we auth to server @@ -531,23 +531,17 @@ class Connection(ConnectionHandlers): secur = None if self._proxy and self._proxy['type'] == 'bosh': - clientClass = common.xmpp.BOSHClient + clientClass = common.xmpp.bosh.BOSHClient else: clientClass = common.xmpp.NonBlockingClient - if gajim.verbose: - con = common.xmpp.NonBlockingClient( - hostname=self._current_host['host'], - port=port, - caller=self, - idlequeue=gajim.idlequeue) - else: - con = common.xmpp.NonBlockingClient( - hostname=self._current_host['host'], - debug=[], - port=port, - caller=self, - idlequeue=gajim.idlequeue) + # there was: + # "if gajim.verbose:" + # here + con = clientClass( + domain=self._hostname, + caller=self, + idlequeue=gajim.idlequeue) self.last_connection = con # increase default timeout for server responses @@ -555,10 +549,19 @@ class Connection(ConnectionHandlers): # FIXME: this is a hack; need a better way if self.on_connect_success == self._on_new_account: con.RegisterDisconnectHandler(self._on_new_account) + + # FIXME: BOSH properties should be in proxy dictionary - loaded from + # config + if self._proxy and self._proxy['type'] == 'bosh': + self._proxy['bosh_hold'] = '1' + self._proxy['bosh_wait'] = '60' + log.info('Connecting to %s: [%s:%d]', self.name, self._current_host['host'], port) con.connect( + hostname=self._current_host['host'], + port=port, on_connect=self.on_connect_success, on_proxy_failure=self.on_proxy_failure, on_connect_failure=self.connect_to_next_type, diff --git a/src/common/xmpp/__init__.py b/src/common/xmpp/__init__.py index 90f852598..109b9b4c0 100644 --- a/src/common/xmpp/__init__.py +++ b/src/common/xmpp/__init__.py @@ -26,7 +26,8 @@ and use only methods for access all values you should not have any problems. """ -import simplexml,protocol,debug,auth_nb,transports_nb,roster_nb,dispatcher_nb,features_nb,idlequeue +import simplexml, protocol, auth_nb, transports_nb, roster_nb +import dispatcher_nb, features_nb, idlequeue, bosh, tls_nb from client_nb import * from client import * from protocol import * diff --git a/src/common/xmpp/auth_nb.py b/src/common/xmpp/auth_nb.py index 662b3f60f..a05f82481 100644 --- a/src/common/xmpp/auth_nb.py +++ b/src/common/xmpp/auth_nb.py @@ -22,6 +22,10 @@ from protocol import * from client import PlugIn import sha,base64,random,dispatcher_nb +import logging +log = logging.getLogger('gajim.c.x.auth_nb') + + import md5 def HH(some): return md5.new(some).hexdigest() def H(some): return md5.new(some).digest() @@ -128,7 +132,7 @@ class SASL(PlugIn): ''' Used to determine if server supports SASL auth. Used internally. ''' if not feats.getTag('mechanisms', namespace=NS_SASL): self.startsasl='not-supported' - self.DEBUG('SASL not supported by server', 'error') + log.error('SASL not supported by server') return mecs=[] for mec in feats.getTag('mechanisms', namespace=NS_SASL).getTags('mechanism'): @@ -145,7 +149,7 @@ class SASL(PlugIn): payload=[base64.encodestring(sasl_data).replace('\n','')]) else: self.startsasl='failure' - self.DEBUG('I can only use DIGEST-MD5 and PLAIN mecanisms.', 'error') + log.error('I can only use DIGEST-MD5 and PLAIN mecanisms.') return self.startsasl='in-process' self._owner.send(node.__str__()) @@ -161,13 +165,13 @@ class SASL(PlugIn): reason = challenge.getChildren()[0] except: reason = challenge - self.DEBUG('Failed SASL authentification: %s' % reason, 'error') + log.error('Failed SASL authentification: %s' % reason) if self.on_sasl : self.on_sasl () raise NodeProcessed elif challenge.getName() == 'success': self.startsasl='success' - self.DEBUG('Successfully authenticated with remote server.', 'ok') + log.info('Successfully authenticated with remote server.') handlers=self._owner.Dispatcher.dumpHandlers() print '6' * 79 print handlers @@ -182,7 +186,7 @@ class SASL(PlugIn): ########################################3333 incoming_data = challenge.getData() data=base64.decodestring(incoming_data) - self.DEBUG('Got challenge:'+data,'ok') + log.info('Got challenge:'+data) chal = challenge_splitter(data) if not self.realm and chal.has_key('realm'): self.realm = chal['realm'] @@ -224,7 +228,7 @@ class SASL(PlugIn): self._owner.send(Node('response', attrs={'xmlns':NS_SASL}).__str__()) else: self.startsasl='failure' - self.DEBUG('Failed SASL authentification: unknown challenge', 'error') + log.error('Failed SASL authentification: unknown challenge') if self.on_sasl : self.on_sasl () raise NodeProcessed @@ -236,7 +240,6 @@ class NonBlockingNonSASL(PlugIn): def __init__(self, user, password, resource, on_auth): ''' Caches username, password and resource for auth. ''' PlugIn.__init__(self) - self.DBG_LINE ='gen_auth' self.user = user self.password= password self.resource = resource @@ -248,7 +251,7 @@ class NonBlockingNonSASL(PlugIn): Returns used method name on success. Used internally. ''' if not self.resource: return self.authComponent(owner) - self.DEBUG('Querying server about possible auth methods', 'start') + log.info('Querying server about possible auth methods') self.owner = owner resp = owner.Dispatcher.SendAndWaitForResponse( @@ -257,7 +260,7 @@ class NonBlockingNonSASL(PlugIn): def _on_username(self, resp): if not isResultNode(resp): - self.DEBUG('No result node arrived! Aborting...','error') + log.error('No result node arrived! Aborting...') return self.on_auth(None) iq=Iq(typ='set',node=resp) query=iq.getTag('query') @@ -265,7 +268,7 @@ class NonBlockingNonSASL(PlugIn): query.setTagData('resource',self.resource) if query.getTag('digest'): - self.DEBUG("Performing digest authentication",'ok') + log.info("Performing digest authentication") query.setTagData('digest', sha.new(self.owner.Dispatcher.Stream._document_attrs['id']+self.password).hexdigest()) if query.getTag('password'): @@ -274,26 +277,26 @@ class NonBlockingNonSASL(PlugIn): elif query.getTag('token'): token=query.getTagData('token') seq=query.getTagData('sequence') - self.DEBUG("Performing zero-k authentication",'ok') + log.info("Performing zero-k authentication") hash = sha.new(sha.new(self.password).hexdigest()+token).hexdigest() for foo in xrange(int(seq)): hash = sha.new(hash).hexdigest() query.setTagData('hash',hash) self._method='0k' else: - self.DEBUG("Sequre methods unsupported, performing plain text authentication",'warn') + log.warn("Sequre methods unsupported, performing plain text authentication") query.setTagData('password',self.password) self._method='plain' resp=self.owner.Dispatcher.SendAndWaitForResponse(iq, func=self._on_auth) def _on_auth(self, resp): if isResultNode(resp): - self.DEBUG('Sucessfully authenticated with remove host.','ok') + log.info('Sucessfully authenticated with remove host.') self.owner.User=self.user self.owner.Resource=self.resource self.owner._registered_name=self.owner.User+'@'+self.owner.Server+'/'+self.owner.Resource return self.on_auth(self._method) - self.DEBUG('Authentication failed!','error') + log.error('Authentication failed!') return self.on_auth(None) def authComponent(self,owner): @@ -309,7 +312,7 @@ class NonBlockingNonSASL(PlugIn): if data: self.Dispatcher.ProcessNonBlocking(data) if not self.handshake: - self.DEBUG('waiting on handshake', 'notify') + log.info('waiting on handshake') return self._owner.onreceive(None) owner._registered_name=self.user @@ -329,14 +332,13 @@ class NonBlockingBind(PlugIn): def __init__(self): PlugIn.__init__(self) - self.DBG_LINE='bind' self.bound=None def FeaturesHandler(self,conn,feats): """ Determine if server supports resource binding and set some internal attributes accordingly. """ if not feats.getTag('bind',namespace=NS_BIND): self.bound='failure' - self.DEBUG('Server does not requested binding.','error') + log.error('Server does not requested binding.') return if feats.getTag('session',namespace=NS_SESSION): self.session=1 else: self.session=-1 @@ -372,36 +374,36 @@ class NonBlockingBind(PlugIn): def _on_bound(self, resp): if isResultNode(resp): self.bound.append(resp.getTag('bind').getTagData('jid')) - self.DEBUG('Successfully bound %s.'%self.bound[-1],'ok') + log.info('Successfully bound %s.'%self.bound[-1]) jid=JID(resp.getTag('bind').getTagData('jid')) self._owner.User=jid.getNode() self._owner.Resource=jid.getResource() self._owner.SendAndWaitForResponse(Protocol('iq', typ='set', payload=[Node('session', attrs={'xmlns':NS_SESSION})]), func=self._on_session) elif resp: - self.DEBUG('Binding failed: %s.' % resp.getTag('error'),'error') + log.error('Binding failed: %s.' % resp.getTag('error')) self.on_bound(None) else: - self.DEBUG('Binding failed: timeout expired.', 'error') + log.error('Binding failed: timeout expired.') self.on_bound(None) def _on_session(self, resp): self._owner.onreceive(None) if isResultNode(resp): - self.DEBUG('Successfully opened session.', 'ok') + log.info('Successfully opened session.') self.session = 1 self.on_bound('ok') else: - self.DEBUG('Session open failed.', 'error') + log.error('Session open failed.') self.session = 0 self.on_bound(None) self._owner.onreceive(None) if isResultNode(resp): - self.DEBUG('Successfully opened session.', 'ok') + log.info('Successfully opened session.') self.session = 1 self.on_bound('ok') else: - self.DEBUG('Session open failed.', 'error') + log.error('Session open failed.') self.session = 0 self.on_bound(None) @@ -411,7 +413,6 @@ class NBComponentBind(PlugIn): ''' def __init__(self): PlugIn.__init__(self) - self.DBG_LINE='bind' self.bound=None self.needsUnregister=None @@ -448,13 +449,13 @@ class NBComponentBind(PlugIn): def _on_bind_reponse(self, res): if resp and resp.getAttr('error'): - self.DEBUG('Binding failed: %s.' % resp.getAttr('error'), 'error') + log.error('Binding failed: %s.' % resp.getAttr('error')) elif resp: - self.DEBUG('Successfully bound.', 'ok') + log.info('Successfully bound.') if self.on_bind: self.on_bind('ok') else: - self.DEBUG('Binding failed: timeout expired.', 'error') + log.error('Binding failed: timeout expired.') if self.on_bind: self.on_bind(None) @@ -462,7 +463,7 @@ class NBComponentBind(PlugIn): """ Determine if server supports resource binding and set some internal attributes accordingly. """ if not feats.getTag('bind',namespace=NS_BIND): self.bound='failure' - self.DEBUG('Server does not requested binding.','error') + log.error('Server does not requested binding.') return if feats.getTag('session',namespace=NS_SESSION): self.session=1 else: self.session=-1 @@ -473,10 +474,10 @@ class NBComponentBind(PlugIn): while self.bound is None and self._owner.Process(1): pass resp=self._owner.SendAndWaitForResponse(Protocol('bind',attrs={'name':domain},xmlns=NS_COMPONENT_1)) if resp and resp.getAttr('error'): - self.DEBUG('Binding failed: %s.'%resp.getAttr('error'),'error') + log.error('Binding failed: %s.'%resp.getAttr('error')) elif resp: - self.DEBUG('Successfully bound.','ok') + log.info('Successfully bound.') return 'ok' else: - self.DEBUG('Binding failed: timeout expired.','error') + log.error('Binding failed: timeout expired.') return '' diff --git a/src/common/xmpp/bosh.py b/src/common/xmpp/bosh.py new file mode 100644 index 000000000..e41322973 --- /dev/null +++ b/src/common/xmpp/bosh.py @@ -0,0 +1,112 @@ + +import protocol, simplexml, locale, random, dispatcher_nb +from client_nb import NBCommonClient +import logging +log = logging.getLogger('gajim.c.x.bosh') + + +class BOSHClient(NBCommonClient): + ''' + Client class implementing BOSH. + ''' + def __init__(self, *args, **kw): + '''Preceeds constructor of NBCommonClient and sets some of values that will + be used as attributes in tag''' + self.Namespace = protocol.NS_HTTP_BIND + # BOSH parameters should be given via Advanced Configuration Editor + self.bosh_xml_lang = None + self.bosh_hold = 1 + self.bosh_wait=60 + self.bosh_rid=None + self.bosh_sid=None + + self.bosh_httpversion = 'HTTP/1.1' + NBCommonClient.__init__(self, *args, **kw) + + + def connect(self, *args, **kw): + + + if locale.getdefaultlocale()[0]: + self.bosh_xml_lang = locale.getdefaultlocale()[0].split('_')[0] + + # with 50-bit random initial rid, session would have to go up + # to 7881299347898368 messages to raise rid over 2**53 + # (see http://www.xmpp.org/extensions/xep-0124.html#rids) + r = random.Random() + r.seed() + self.bosh_rid = r.getrandbits(50) + + proxy = kw['proxy'] + #self.bosh_protocol, self.bosh_host, self.bosh_uri = transports_nb.urisplit(proxy['host']) + self.bosh_port = proxy['port'] + self.bosh_wait = proxy['bosh_wait'] + self.bosh_hold = proxy['bosh_hold'] + self.bosh_to = proxy['to'] + #self.bosh_ack = proxy['bosh_ack'] + #self.bosh_secure = proxy['bosh_secure'] + NBCommonClient.connect(self, *args, **kw) + + def send(self, stanza, now = False): + (id, stanza_to_send) = self.Dispatcher.assign_id(stanza) + + self.Connection.send( + self.boshify_stanza(stanza_to_send), + now = now) + return id + + def get_bodytag(self): + # this should be called not until after session creation response so sid has + # to be initialized. + assert(self.sid is not None) + self.rid = self.rid+1 + return protocol.BOSHBody( + attrs={ 'rid': str(self.bosh_rid), + 'sid': self.bosh_sid}) + + + def get_initial_bodytag(self): + return protocol.BOSHBody( + attrs={'content': 'text/xml; charset=utf-8', + 'hold': str(self.bosh_hold), + 'to': self.bosh_to, + 'wait': str(self.bosh_wait), + 'rid': str(self.bosh_rid), + 'xmpp:version': '1.0', + 'xmlns:xmpp': 'urn:xmpp:xbosh'} + ) + + def get_closing_bodytag(self): + closing_bodytag = self.get_bodytag() + closing_bodytag.setAttr('type', 'terminate') + return closing_bodytag + + + def boshify_stanza(self, stanza): + ''' wraps stanza by body tag or modifies message entirely (in case of stream + opening and closing''' + log.info('boshify_staza - type is: %s' % type(stanza)) + if isinstance(stanza, simplexml.Node): + tag = self.get_bodytag() + return tag.setPayload(stanza) + else: + # only stream initialization and stream terminatoion are not Nodes + if stanza.startswith(dispatcher_nb.XML_DECLARATION): + # stream init + return self.get_initial_bodytag() + else: + # should be stream closing + assert(stanza == dispatcher_nb.STREAM_TERMINATOR) + return self.get_closing_bodytag() + + + + def _on_stream_start(self): + ''' + Called after XMPP stream is opened. In BOSH, TLS is negotiated elsewhere + so success callback can be invoked. + (authentication is started from auth() method) + ''' + self.onreceive(None) + if self.connected == 'tcp': + self._on_connect() diff --git a/src/common/xmpp/client.py b/src/common/xmpp/client.py index 201da2a78..c30905963 100644 --- a/src/common/xmpp/client.py +++ b/src/common/xmpp/client.py @@ -21,49 +21,21 @@ examples of xmpppy structures usage. These classes can be used for simple applications "AS IS" though. """ -import socket -import debug -Debug=debug -Debug.DEBUGGING_IS_ON=1 -Debug.Debug.colors['socket']=debug.color_dark_gray -Debug.Debug.colors['CONNECTproxy']=debug.color_dark_gray -Debug.Debug.colors['nodebuilder']=debug.color_brown -Debug.Debug.colors['client']=debug.color_cyan -Debug.Debug.colors['component']=debug.color_cyan -Debug.Debug.colors['dispatcher']=debug.color_green -Debug.Debug.colors['browser']=debug.color_blue -Debug.Debug.colors['auth']=debug.color_yellow -Debug.Debug.colors['roster']=debug.color_magenta -Debug.Debug.colors['ibb']=debug.color_yellow - -Debug.Debug.colors['down']=debug.color_brown -Debug.Debug.colors['up']=debug.color_brown -Debug.Debug.colors['data']=debug.color_brown -Debug.Debug.colors['ok']=debug.color_green -Debug.Debug.colors['warn']=debug.color_yellow -Debug.Debug.colors['error']=debug.color_red -Debug.Debug.colors['start']=debug.color_dark_gray -Debug.Debug.colors['stop']=debug.color_dark_gray -Debug.Debug.colors['sent']=debug.color_yellow -Debug.Debug.colors['got']=debug.color_bright_cyan - -DBG_CLIENT='client' -DBG_COMPONENT='component' +import logging +log = logging.getLogger('gajim.c.x.plugin') class PlugIn: """ Common xmpppy plugins infrastructure: plugging in/out, debugging. """ def __init__(self): self._exported_methods=[] - self.DBG_LINE=self.__class__.__name__.lower() def PlugIn(self,owner): """ Attach to main instance and register ourself and all our staff in it. """ self._owner=owner - if self.DBG_LINE not in owner.debug_flags: - owner.debug_flags.append(self.DBG_LINE) - self.DEBUG('Plugging %s into %s'%(self,self._owner),'start') + log.debug('Plugging %s into %s'%(self,self._owner)) if owner.__dict__.has_key(self.__class__.__name__): - return self.DEBUG('Plugging ignored: another instance already plugged.','error') + log.debug('Plugging ignored: another instance already plugged.') + return self._old_owners_methods=[] for method in self._exported_methods: if owner.__dict__.has_key(method.__name__): @@ -76,15 +48,10 @@ class PlugIn: def PlugOut(self): """ Unregister all our staff from main instance and detach from it. """ - self.DEBUG('Plugging %s out of %s.'%(self,self._owner),'stop') - self._owner.debug_flags.remove(self.DBG_LINE) + log.debug('Plugging %s out of %s.'%(self,self._owner)) for method in self._exported_methods: del self._owner.__dict__[method.__name__] for method in self._old_owners_methods: self._owner.__dict__[method.__name__]=method del self._owner.__dict__[self.__class__.__name__] if self.__class__.__dict__.has_key('plugout'): return self.plugout() del self._owner - def DEBUG(self,text,severity='info'): - """ Feed a provided debug line to main instance's debug facility along with our ID string. """ - self._owner.DEBUG(self.DBG_LINE,text,severity) - diff --git a/src/common/xmpp/client_nb.py b/src/common/xmpp/client_nb.py index 308300151..df6c4ca20 100644 --- a/src/common/xmpp/client_nb.py +++ b/src/common/xmpp/client_nb.py @@ -22,8 +22,6 @@ These classes can be used for simple applications "AS IS" though. ''' import socket -import debug -import random import transports_nb, tls_nb, dispatcher_nb, auth_nb, roster_nb, protocol from client import * @@ -31,54 +29,29 @@ from client import * import logging log = logging.getLogger('gajim.c.x.client_nb') -consoleloghandler = logging.StreamHandler() -consoleloghandler.setLevel(logging.DEBUG) -consoleloghandler.setFormatter( - logging.Formatter('%(levelname)s: %(message)s') -) -log.setLevel(logging.DEBUG) -log.addHandler(consoleloghandler) -log.propagate = False - class NBCommonClient: ''' Base for Client and Component classes.''' - def __init__(self, hostname, idlequeue, port=5222, debug=['always', 'nodebuilder'], caller=None): + def __init__(self, domain, idlequeue, caller=None): ''' Caches connection data: - :param hostname: hostname of machine where the XMPP server is running (from Account - of from SRV request) and port to connect to. + :param domain: domain - for to: attribute (from account info) :param idlequeue: processing idlequeue :param port: port of listening XMPP server - :param debug: specifies the debug IDs that will go into debug output. You can either - specifiy an "include" or "exclude" list. The latter is done via adding "always" - pseudo-ID to the list. Full list: ['nodebuilder', 'dispatcher', 'gen_auth', - 'SASL_auth', 'bind', 'socket', 'CONNECTproxy', 'TLS', 'roster', 'browser', 'ibb']. - TODO: get rid of debug.py using :param caller: calling object - it has to implement certain methods (necessary?) ''' - self.DBG = DBG_CLIENT - self.Namespace = protocol.NS_CLIENT self.idlequeue = idlequeue self.defaultNamespace = self.Namespace self.disconnect_handlers = [] - # XMPP server and port from account or SRV - self.Server = hostname - self.Port = port + self.Server = domain # caller is who initiated this client, it is sed to register the EventDispatcher self._caller = caller - if debug and type(debug) != list: - debug = ['always', 'nodebuilder'] - self._DEBUG = Debug.Debug(debug) - self.DEBUG = self._DEBUG.Show - self.debug_flags = self._DEBUG.debug_flags - self.debug_flags.append(self.DBG) self._owner = self self._registered_name = None self.connected = '' @@ -98,9 +71,9 @@ class NBCommonClient: ''' self.connected='' - self.DEBUG(self.DBG,'Disconnect detected','stop') + log.debug('Client disconnected..') for i in reversed(self.disconnect_handlers): - self.DEBUG(self.DBG, 'Calling disc handler %s' % i, 'stop') + log.debug('Calling disconnect handler %s' % i) i() if self.__dict__.has_key('NonBlockingRoster'): self.NonBlockingRoster.PlugOut() @@ -120,23 +93,22 @@ class NBCommonClient: self.NonBlockingTcp.PlugOut() - def send(self, stanza, is_message = False, now = False): + def send(self, stanza, now = False): ''' interface for putting stanzas on wire. Puts ID to stanza if needed and sends it via socket wrapper''' (id, stanza_to_send) = self.Dispatcher.assign_id(stanza) - if is_message: - # somehow zeroconf-specific - self.Connection.send(stanza_to_send, True, now = now) - else: - self.Connection.send(stanza_to_send, now = now) + self.Connection.send(stanza_to_send, now = now) return id - def connect(self, on_connect, on_connect_failure, on_proxy_failure=None, proxy=None, secure=None): + def connect(self, on_connect, on_connect_failure, hostname=None, port=5222, + on_proxy_failure=None, proxy=None, secure=None): ''' Open XMPP connection (open streams in both directions). + :param hostname: hostname of XMPP server from SRV request + :param port: port number of XMPP server :param on_connect: called after stream is successfully opened :param on_connect_failure: called when error occures during connection :param on_proxy_failure: called if error occurres during TCP connection to @@ -146,7 +118,12 @@ class NBCommonClient: optionally keys 'user' and 'pass' as proxy credentials :param secure: ''' - + self.Port = port + if hostname: + xmpp_hostname = hostname + else: + xmpp_hostname = self.Server + self.on_connect = on_connect self.on_connect_failure=on_connect_failure self.on_proxy_failure = on_proxy_failure @@ -155,8 +132,8 @@ class NBCommonClient: if proxy: # with proxies, client connects to proxy instead of directly to - # XMPP server from __init__. - # tcp_server is hostname used for socket connecting + # XMPP server ((hostname, port)) + # tcp_server is machine used for socket connection tcp_server=proxy['host'] tcp_port=proxy['port'] self._on_tcp_failure = self.on_proxy_failure @@ -168,30 +145,33 @@ class NBCommonClient: type_ = proxy['type'] if type_ == 'socks5': + # SOCKS5 proxy self.socket = transports_nb.NBSOCKS5ProxySocket( on_disconnect=self.on_disconnect, proxy_creds=proxy_creds, - xmpp_server=(self.Server, self.Port)) + xmpp_server=(xmpp_hostname, self.Port)) elif type_ == 'http': + # HTTP CONNECT to proxy self.socket = transports_nb.NBHTTPProxySocket( on_disconnect=self.on_disconnect, proxy_creds=proxy_creds, - xmpp_server=(self.Server, self.Port)) + xmpp_server=(xmpp_hostname, self.Port)) elif type_ == 'bosh': + # BOSH - XMPP over HTTP tcp_server = transports_nb.urisplit(tcp_server)[1] - self.socket = transports_nb.NonBlockingHttpBOSH( + self.socket = transports_nb.NonBlockingHTTP( on_disconnect=self.on_disconnect, - bosh_uri = proxy['host'], - bosh_port = tcp_port) + http_uri = proxy['host'], + http_port = tcp_port) else: # HTTP CONNECT to proxy from environment variables self.socket = transports_nb.NBHTTPProxySocket( on_disconnect=self.on_disconnect, proxy_creds=(None, None), - xmpp_server=(self.Server, self.Port)) + xmpp_server=(xmpp_hostname, self.Port)) else: self._on_tcp_failure = self._on_connect_failure - tcp_server=self.Server + tcp_server=xmpp_hostname tcp_port=self.Port self.socket = transports_nb.NonBlockingTcp(on_disconnect = self.on_disconnect) @@ -221,7 +201,7 @@ class NBCommonClient: def _try_next_ip(self, err_message=None): '''iterates over IP addresses from getaddinfo''' if err_message: - self.DEBUG(self.DBG,err_message,'connect') + log.debug('While looping over DNS A records: %s' % connect) if self.ip_addresses == []: self._on_tcp_failure(err_message='Run out of hosts for name %s:%s' % (self.Server, self.Port)) @@ -305,7 +285,7 @@ class NBCommonClient: def _on_connect_failure(self, retry=None, err_message=None): self.connected = None if err_message: - self.DEBUG(self.DBG, err_message, 'connecting') + log.debug('While connecting: %s' % err_message) if self.socket: self.socket.disconnect() self.on_connect_failure(retry) @@ -460,83 +440,3 @@ class NonBlockingClient(NBCommonClient): self.send(dispatcher_nb.Presence(to=jid, typ=typ)) -class BOSHClient(NBCommonClient): - ''' - Client class implementing BOSH. - ''' - def __init__(self, *args, **kw): - '''Preceeds constructor of NBCommonClient and sets some of values that will - be used as attributes in tag''' - self.Namespace = NS_HTTP_BIND - # BOSH parameters should be given via Advanced Configuration Editor - self.bosh_hold = 1 - self.bosh_wait=60 - self.bosh_rid=-1 - self.bosh_httpversion = 'HTTP/1.1' - NBCommonClient.__init__(self, *args, **kw) - - - def connect(self, *args, **kw): - proxy = kw['proxy'] - self.bosh_protocol, self.bosh_host, self.bosh_uri = self.urisplit(proxy['host']) - self.bosh_port = proxy['port'] - NBCommonClient.connect(*args, **kw) - - - def _on_stream_start(self): - ''' - Called after XMPP stream is opened. In BOSH TLS is negotiated on tranport layer - so success callback can be invoked. - (authentication is started from auth() method) - ''' - self.onreceive(None) - if self.connected == 'tcp': - self._on_connect() - - - - - - def bosh_raise_event(self, realm, event, data): - # should to extract stanza from body - self.DEBUG(self.DBG,'realm: %s, event: %s, data: %s' % (realm, event, data), - 'BOSH EventHandler') - self._caller._event_dispatcher(realm, event, data) - - - def StreamInit(self): - ''' - Init of BOSH session. Called instead of Dispatcher.StreamInit() - Initial body tag is created and sent. - ''' - #self.Dispatcher.RegisterEventHandler(self.bosh_event_handler) - self.Dispatcher.Stream = simplexml.NodeBuilder() - self.Dispatcher.Stream._dispatch_depth = 2 - self.Dispatcher.Stream.dispatch = self.Dispatcher.dispatch - self.Dispatcher.Stream.stream_header_received = self._check_stream_start - self.Dispatcher.Stream.features = None - - r = random.Random() - r.seed() - # with 50-bit random initial rid, session would have to go up - # to 7881299347898368 messages to raise rid over 2**53 - # (see http://www.xmpp.org/extensions/xep-0124.html#rids) - self.bosh_rid = r.getrandbits(50) - - initial_body_tag = BOSHBody( - attrs={'content': 'text/xml; charset=utf-8', - 'hold': str(self.bosh_hold), - # "to" should be domain, not hostname of machine - 'to': self.Server, - 'wait': str(self.bosh_wait), - 'rid': str(self.bosh_rid), - 'xmpp:version': '1.0', - 'xmlns:xmpp': 'urn:xmpp:xbosh'} - ) - - if locale.getdefaultlocale()[0]: - initial_body_tag.setAttr('xml:lang', - locale.getdefaultlocale()[0].split('_')[0]) - initial_body_tag.setAttr('xmpp:version', '1.0') - initial_body_tag.setAttr('xmlns:xmpp', 'urn:xmpp:xbosh') - self.send(initial_body_tag) diff --git a/src/common/xmpp/dispatcher_nb.py b/src/common/xmpp/dispatcher_nb.py index 4633db28b..3734d16a9 100644 --- a/src/common/xmpp/dispatcher_nb.py +++ b/src/common/xmpp/dispatcher_nb.py @@ -28,16 +28,21 @@ from xml.parsers.expat import ExpatError from protocol import * from client import PlugIn +import logging +log = logging.getLogger('gajim.c.x.dispatcher_nb') + # default timeout to wait for response for our id DEFAULT_TIMEOUT_SECONDS = 25 ID = 0 +STREAM_TERMINATOR = '' +XML_DECLARATION = '' + class Dispatcher(PlugIn): ''' Ancestor of PlugIn class. Handles XMPP stream, i.e. aware of stream headers. Can be plugged out/in to restart these headers (used for SASL f.e.). ''' def __init__(self): PlugIn.__init__(self) - DBG_LINE='dispatcher' self.handlers={} self._expected={} self._defaultHandler=None @@ -79,7 +84,7 @@ class Dispatcher(PlugIn): def plugin(self, owner): ''' Plug the Dispatcher instance into Client class instance and send initial stream header. Used internally.''' - self.DEBUG('Dispatcher plugin', 'PlugIn') + log.debug('Dispatcher plugin') self._init() self._owner.lastErrNode = None @@ -93,7 +98,6 @@ class Dispatcher(PlugIn): def plugout(self): ''' Prepares instance to be destructed. ''' self.Stream.dispatch = None - self.Stream.DEBUG = None self.Stream.features = None self.Stream.destroy() self._owner = None @@ -105,8 +109,6 @@ class Dispatcher(PlugIn): self.Stream._dispatch_depth = 2 self.Stream.dispatch = self.dispatch self.Stream.stream_header_received = self._check_stream_start - self._owner.debug_flags.append(simplexml.DBG_NODEBUILDER) - self.Stream.DEBUG = self._owner.DEBUG self.Stream.features = None self._metastream = Node('stream:stream') self._metastream.setNamespace(self._owner.Namespace) @@ -116,11 +118,11 @@ class Dispatcher(PlugIn): if locale.getdefaultlocale()[0]: self._metastream.setAttr('xml:lang', locale.getdefaultlocale()[0].split('_')[0]) - self._owner.send("%s>" % str(self._metastream)[:-2]) + self._owner.send("%s%s>" % (XML_DECLARATION,str(self._metastream)[:-2])) def StreamTerminate(self): ''' Send a stream terminator. ''' - self._owner.send('') + self._owner.send(STREAM_TERMINATOR) def _check_stream_start(self, ns, tag, attrs): if ns<>NS_STREAMS or tag<>'stream': @@ -144,7 +146,7 @@ class Dispatcher(PlugIn): self._owner.Connection.disconnect() return 0 except ExpatError: - self.DEBUG('Invalid XML received from server. Forcing disconnect.', 'error') + log.error('Invalid XML received from server. Forcing disconnect.') self._owner.Connection.disconnect() return 0 if len(self._pendingExceptions) > 0: @@ -157,7 +159,7 @@ class Dispatcher(PlugIn): ''' Creates internal structures for newly registered namespace. You can register handlers for this namespace afterwards. By default one namespace already registered (jabber:client or jabber:component:accept depending on context. ''' - self.DEBUG('Registering namespace "%s"' % xmlns, order) + log.info('Registering namespace "%s"' % xmlns) self.handlers[xmlns]={} self.RegisterProtocol('unknown', Protocol, xmlns=xmlns) self.RegisterProtocol('default', Protocol, xmlns=xmlns) @@ -167,8 +169,7 @@ class Dispatcher(PlugIn): Needed to start registering handlers for such stanzas. Iq, message and presence protocols are registered by default. ''' if not xmlns: xmlns=self._owner.defaultNamespace - self.DEBUG('Registering protocol "%s" as %s(%s)' % - (tag_name, Proto, xmlns), order) + log.info('Registering protocol "%s" as %s(%s)' %(tag_name, Proto, xmlns)) self.handlers[xmlns][tag_name]={type:Proto, 'default':[]} def RegisterNamespaceHandler(self, xmlns, handler, typ='', ns='', makefirst=0, system=0): @@ -195,8 +196,8 @@ class Dispatcher(PlugIn): ''' if not xmlns: xmlns=self._owner.defaultNamespace - self.DEBUG('Registering handler %s for "%s" type->%s ns->%s(%s)' % - (handler, name, typ, ns, xmlns), 'info') + log.info('Registering handler %s for "%s" type->%s ns->%s(%s)' % + (handler, name, typ, ns, xmlns)) if not typ and not ns: typ='default' if not self.handlers.has_key(xmlns): @@ -313,13 +314,13 @@ class Dispatcher(PlugIn): xmlns=stanza.getNamespace() if not self.handlers.has_key(xmlns): - self.DEBUG("Unknown namespace: " + xmlns, 'warn') + log.warn("Unknown namespace: " + xmlns) xmlns='unknown' if not self.handlers[xmlns].has_key(name): - self.DEBUG("Unknown stanza: " + name, 'warn') + log.warn("Unknown stanza: " + name) name='unknown' else: - self.DEBUG("Got %s/%s stanza" % (xmlns, name), 'ok') + log.debug("Got %s/%s stanza" % (xmlns, name)) if stanza.__class__.__name__=='Node': stanza=self.handlers[xmlns][name][type](node=stanza) @@ -329,7 +330,7 @@ class Dispatcher(PlugIn): stanza.props=stanza.getProperties() ID=stanza.getID() - session.DEBUG("Dispatching %s stanza with type->%s props->%s id->%s"%(name,typ,stanza.props,ID),'ok') + log.debug("Dispatching %s stanza with type->%s props->%s id->%s"%(name,typ,stanza.props,ID)) list=['default'] # we will use all handlers: if self.handlers[xmlns][name].has_key(typ): list.append(typ) # from very common... for prop in stanza.props: @@ -345,13 +346,13 @@ class Dispatcher(PlugIn): user=0 if type(session._expected[ID]) == type(()): cb,args = session._expected[ID] - session.DEBUG("Expected stanza arrived. Callback %s(%s) found!" % (cb, args), 'ok') + log.debug("Expected stanza arrived. Callback %s(%s) found!" % (cb, args)) try: cb(session,stanza,**args) except Exception, typ: if typ.__class__.__name__ <>'NodeProcessed': raise else: - session.DEBUG("Expected stanza arrived!",'ok') + log.debug("Expected stanza arrived!") session._expected[ID]=stanza else: user=1 diff --git a/src/common/xmpp/idlequeue.py b/src/common/xmpp/idlequeue.py index e9c4f0fde..2ca1b0bd3 100644 --- a/src/common/xmpp/idlequeue.py +++ b/src/common/xmpp/idlequeue.py @@ -13,6 +13,8 @@ ## GNU General Public License for more details. import select +import logging +log = logging.getLogger('gajim.c.x.idlequeue') class IdleObject: ''' base class for all idle listeners, these are the methods, which are called from IdleQueue @@ -53,7 +55,7 @@ class IdleQueue: self.selector = select.poll() def remove_timeout(self, fd): - print 'read timeout removed for fd %s' % fd + log.debug('read timeout removed for fd %s' % fd) if self.read_timeouts.has_key(fd): del(self.read_timeouts[fd]) @@ -69,7 +71,7 @@ class IdleQueue: def set_read_timeout(self, fd, seconds): ''' set a new timeout, if it is not removed after 'seconds', then obj.read_timeout() will be called ''' - print 'read timeout set for fd %s on %s seconds' % (fd, seconds) + log.debug('read timeout set for fd %s on %s seconds' % (fd, seconds)) timeout = self.current_time() + seconds self.read_timeouts[fd] = timeout @@ -79,6 +81,7 @@ class IdleQueue: if timeout > current_time: continue if self.queue.has_key(fd): + log.debug('Calling read_timeout for fd %s' % fd) self.queue[fd].read_timeout() else: self.remove_timeout(fd) diff --git a/src/common/xmpp/roster_nb.py b/src/common/xmpp/roster_nb.py index b843525e7..a269880ba 100644 --- a/src/common/xmpp/roster_nb.py +++ b/src/common/xmpp/roster_nb.py @@ -24,6 +24,10 @@ mass-renaming of contacts. from protocol import * from client import PlugIn +import logging +log = logging.getLogger('gajim.c.x.roster_nb') + + class NonBlockingRoster(PlugIn): """ Defines a plenty of methods that will allow you to manage roster. Also automatically track presences from remote JIDs taking into @@ -35,7 +39,6 @@ class NonBlockingRoster(PlugIn): def __init__(self): """ Init internal variables. """ PlugIn.__init__(self) - self.DBG_LINE='roster' self._data = {} self.set=None self._exported_methods=[self.getRoster] @@ -46,7 +49,7 @@ class NonBlockingRoster(PlugIn): if self.set is None: self.set=0 elif not force: return self._owner.send(Iq('get',NS_ROSTER)) - self.DEBUG('Roster requested from server','start') + log.info('Roster requested from server') def RosterIqHandler(self,dis,stanza): """ Subscription tracker. Used internally for setting items state in @@ -60,7 +63,7 @@ class NonBlockingRoster(PlugIn): if item.getAttr('subscription')=='remove': if self._data.has_key(jid): del self._data[jid] return - self.DEBUG('Setting roster item %s...'%jid,'ok') + log.info('Setting roster item %s...' % jid) if not self._data.has_key(jid): self._data[jid]={} self._data[jid]['name']=item.getAttr('name') self._data[jid]['ask']=item.getAttr('ask') @@ -86,7 +89,7 @@ class NonBlockingRoster(PlugIn): typ=pres.getType() if not typ: - self.DEBUG('Setting roster item %s for resource %s...'%(jid.getStripped(),jid.getResource()),'ok') + log.info('Setting roster item %s for resource %s...'%(jid.getStripped(),jid.getResource())) item['resources'][jid.getResource()]=res={'show':None,'status':None,'priority':'0','timestamp':None} if pres.getTag('show'): res['show']=pres.getShow() if pres.getTag('status'): res['status']=pres.getStatus() diff --git a/src/common/xmpp/simplexml.py b/src/common/xmpp/simplexml.py index 89a6cec59..f7561269b 100644 --- a/src/common/xmpp/simplexml.py +++ b/src/common/xmpp/simplexml.py @@ -18,6 +18,9 @@ I'm personally using it in many other separate projects. It is designed to be as standalone as possible.""" import xml.parsers.expat +import logging +log = logging.getLogger('gajim.c.x.simplexml') + def XMLescape(txt): """Returns provided string with symbols & < > " replaced by their respective XML entities.""" @@ -279,7 +282,6 @@ class NT(T): if isinstance(val,Node): self.node.addChild(attr,node=val) else: return self.node.addChild(attr,payload=[val]) -DBG_NODEBUILDER = 'nodebuilder' class NodeBuilder: """ Builds a Node class minidom from data parsed to it. This class used for two purposes: 1. Creation an XML Node from a textual representation. F.e. reading a config file. See an XML2Node method. @@ -293,7 +295,7 @@ class NodeBuilder: You can think about it as of "node upgrade". "data" (if provided) feeded to parser immidiatedly after instance init. """ - self.DEBUG(DBG_NODEBUILDER, "Preparing to handle incoming XML stream.", 'start') + log.debug("Preparing to handle incoming XML stream.") self._parser = xml.parsers.expat.ParserCreate(namespace_separator=' ') self._parser.StartElementHandler = self.starttag self._parser.EndElementHandler = self.endtag @@ -341,7 +343,7 @@ class NodeBuilder: attrs[self.namespaces[ns]+attr[sp+1:]]=attrs[attr] del attrs[attr] # self._inc_depth() - self.DEBUG(DBG_NODEBUILDER, "DEPTH -> %i , tag -> %s, attrs -> %s" % (self.__depth, tag, `attrs`), 'down') + log.info("DEPTH -> %i , tag -> %s, attrs -> %s" % (self.__depth, tag, `attrs`)) if self.__depth == self._dispatch_depth: if not self._mini_dom : self._mini_dom = Node(tag=tag, attrs=attrs) @@ -360,14 +362,14 @@ class NodeBuilder: self.last_is_data = 0 def endtag(self, tag ): """XML Parser callback. Used internally""" - self.DEBUG(DBG_NODEBUILDER, "DEPTH -> %i , tag -> %s" % (self.__depth, tag), 'up') + log.info("DEPTH -> %i , tag -> %s" % (self.__depth, tag)) self.check_data_buffer() if self.__depth == self._dispatch_depth: self.dispatch(self._mini_dom) elif self.__depth > self._dispatch_depth: self._ptr = self._ptr.parent else: - self.DEBUG(DBG_NODEBUILDER, "Got higher than dispatch level. Stream terminated?", 'stop') + log.info("Got higher than dispatch level. Stream terminated?") self._dec_depth() self.last_is_data = 0 if self.__depth == 0: self.stream_footer_received() @@ -385,8 +387,7 @@ class NodeBuilder: self.check_data_buffer() if prefix: self.namespaces[uri]=prefix+':' else: self.xmlns=uri - def DEBUG(self, level, text, comment=None): - """ Gets all NodeBuilder walking events. Can be used for debugging if redefined.""" + def getDom(self): """ Returns just built Node. """ self.check_data_buffer() diff --git a/src/common/xmpp/tls_nb.py b/src/common/xmpp/tls_nb.py index e0b975de9..148a8aac3 100644 --- a/src/common/xmpp/tls_nb.py +++ b/src/common/xmpp/tls_nb.py @@ -26,16 +26,8 @@ import time import traceback import logging - log = logging.getLogger('gajim.c.x.tls_nb') -consoleloghandler = logging.StreamHandler() -consoleloghandler.setLevel(logging.DEBUG) -consoleloghandler.setFormatter( - logging.Formatter('%(levelname)s: %(message)s') -) -log.setLevel(logging.DEBUG) -log.addHandler(consoleloghandler) -log.propagate = False + # I don't need to load gajim.py just because of few TLS variables, so I changed # %s/common\.gajim\.DATA_DIR/\'\.\.\/data\'/c # %s/common\.gajim\.MY_CACERTS/\'\%s\/\.gajim\/cacerts\.pem\' \% os\.environ\[\'HOME\'\]/c @@ -49,6 +41,11 @@ log.propagate = False USE_PYOPENSSL = False + +#TODO: add callback set from PlugIn for errors during runtime +# - sth like on_disconnect in socket wrappers + + try: #raise ImportError("Manually disabled PyOpenSSL") import OpenSSL.SSL @@ -164,8 +161,8 @@ class PyOpenSSLWrapper(SSLWrapper): if flags is None: retval = self.sslobj.recv(bufsize) else: retval = self.sslobj.recv(bufsize, flags) except (OpenSSL.SSL.WantReadError, OpenSSL.SSL.WantWriteError), e: + log.debug("Recv: Want-error: " + repr(e)) pass - # log.debug("Recv: " + repr(e)) except OpenSSL.SSL.SysCallError, e: log.debug("Recv: Got OpenSSL.SSL.SysCallError: " + repr(e), exc_info=True) #traceback.print_exc() @@ -253,7 +250,6 @@ class NonBlockingTLS(PlugIn): if owner.__dict__.has_key('NonBlockingTLS'): return # Already enabled. PlugIn.PlugIn(self, owner) - DBG_LINE='NonBlockingTLS' self.on_tls_success = on_tls_success self.on_tls_faliure = on_tls_failure if now: @@ -288,10 +284,10 @@ class NonBlockingTLS(PlugIn): ''' Used to analyse server tag for TLS support. If TLS is supported starts the encryption negotiation. Used internally ''' if not feats.getTag('starttls', namespace=NS_TLS): - self.DEBUG("TLS unsupported by remote server.", 'warn') + log.warn("TLS unsupported by remote server.") self.on_tls_failure("TLS unsupported by remote server.") return - self.DEBUG("TLS supported by remote server. Requesting TLS start.", 'ok') + log.debug("TLS supported by remote server. Requesting TLS start.") self._owner.RegisterHandlerOnce('proceed', self.StartTLSHandler, xmlns=NS_TLS) self._owner.RegisterHandlerOnce('failure', self.StartTLSHandler, xmlns=NS_TLS) self._owner.send('' % NS_TLS) @@ -425,7 +421,7 @@ class NonBlockingTLS(PlugIn): if self.starttls == 'failure': self.on_tls_failure('TLS received: %s' % self.starttls) return - self.DEBUG('Got starttls proceed response. Switching to TLS/SSL...','ok') + log.debug('Got starttls proceed response. Switching to TLS/SSL...') try: self._startSSL() except Exception, e: diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py index c69f33be8..613dbd693 100644 --- a/src/common/xmpp/transports_nb.py +++ b/src/common/xmpp/transports_nb.py @@ -30,17 +30,9 @@ import traceback import logging log = logging.getLogger('gajim.c.x.transports_nb') -consoleloghandler = logging.StreamHandler() -consoleloghandler.setLevel(logging.DEBUG) -consoleloghandler.setFormatter( - logging.Formatter('%(levelname)s: %(message)s') -) -log.setLevel(logging.DEBUG) -log.addHandler(consoleloghandler) -log.propagate = False -def urisplit(self, uri): +def urisplit(uri): ''' Function for splitting URI string to tuple (protocol, host, path). e.g. urisplit('http://httpcm.jabber.org/webclient') returns @@ -72,6 +64,23 @@ CONNECTING ='CONNECTING' CONNECTED ='CONNECTED' DISCONNECTING ='DISCONNECTING' + +class NonBlockingTransport(PlugIn): + def __init__(self, on_disconnect): + PlugIn.__init__(self) + self.on_disconnect = on_disconnect + + def plugin(self, owner): + owner.Connection=self + self.idlequeue = owner.idlequeue + + + def plugout(self): + self._owner.Connection = None + self._owner = None + + + class NonBlockingTcp(PlugIn, IdleObject): ''' Non-blocking TCP socket wrapper @@ -114,7 +123,6 @@ class NonBlockingTcp(PlugIn, IdleObject): self.set_timeout, self.remove_timeout] def plugin(self, owner): - print 'plugin called' owner.Connection=self self.idlequeue = owner.idlequeue @@ -142,7 +150,7 @@ class NonBlockingTcp(PlugIn, IdleObject): self.on_connect = on_connect self.on_connect_failure = on_connect_failure (self.server, self.port) = conn_5tuple[4] - log.debug('NonBlocking Connect :: About tot connect to %s:%s' % conn_5tuple[4]) + log.info('NonBlocking Connect :: About tot connect to %s:%s' % conn_5tuple[4]) try: self._sock = socket.socket(*conn_5tuple[:3]) except socket.error, (errnum, errstr): @@ -170,14 +178,14 @@ class NonBlockingTcp(PlugIn, IdleObject): if errnum in (errno.EINPROGRESS, errno.EALREADY, errno.EWOULDBLOCK): # connecting in progress self.set_state(CONNECTING) - log.debug('After connect. "%s" raised => CONNECTING' % errstr) + log.info('After connect. "%s" raised => CONNECTING' % errstr) # on_connect/failure will be called from self.pollin/self.pollout return elif errnum in (0, 10056, errno.EISCONN): # already connected - this branch is very unlikely, nonblocking connect() will # return EINPROGRESS exception in most cases. When here, we don't need timeout # on connected descriptor and success callback can be called. - log.debug('After connect. "%s" raised => CONNECTED' % errstr) + log.info('After connect. "%s" raised => CONNECTED' % errstr) self._on_connect(self) return @@ -212,12 +220,12 @@ class NonBlockingTcp(PlugIn, IdleObject): def pollin(self): '''called when receive on plugged socket is possible ''' - log.debug('pollin called, state == %s' % self.state) + log.info('pollin called, state == %s' % self.state) self._do_receive() def pollout(self): '''called when send to plugged socket is possible''' - log.debug('pollout called, state == %s' % self.state) + log.info('pollout called, state == %s' % self.state) if self.state==CONNECTING: self._on_connect(self) @@ -225,7 +233,7 @@ class NonBlockingTcp(PlugIn, IdleObject): self._do_send() def pollend(self): - log.debug('pollend called, state == %s' % self.state) + log.info('pollend called, state == %s' % self.state) if self.state==CONNECTING: self._on_connect_failure('Error during connect to %s:%s' % @@ -251,7 +259,7 @@ class NonBlockingTcp(PlugIn, IdleObject): ''' Implemntation of IdleObject function called on timeouts from IdleQueue. ''' - log.debug('read_timeout called, state == %s' % self.state) + log.warn('read_timeout called, state == %s' % self.state) if self.state==CONNECTING: # if read_timeout is called during connecting, connect() didn't end yet # thus we have to call the tcp failure callback @@ -309,17 +317,18 @@ class NonBlockingTcp(PlugIn, IdleObject): def _plug_idle(self): # readable if socket is connected or disconnecting readable = self.state != DISCONNECTED + fd = self.get_fd() # writeable if sth to send if self.sendqueue or self.sendbuff: writable = True else: writable = False - print 'About to plug fd %d, W:%s, R:%s' % (self.get_fd(), writable, readable) + log.debug('About to plug fd %d, W:%s, R:%s' % (fd, writable, readable)) if self.writable != writable or self.readable != readable: - print 'Really plugging fd %d, W:%s, R:%s' % (self.get_fd(), writable, readable) + log.debug('Really plugging fd %d, W:%s, R:%s' % (fd, writable, readable)) self.idlequeue.plug_idle(self, writable, readable) else: - print 'Not plugging - is already plugged' + log.debug('Not plugging fd %s because it\'s already plugged' % fd) @@ -343,7 +352,7 @@ class NonBlockingTcp(PlugIn, IdleObject): def _raise_event(self, event_type, data): if data and data.strip(): - log.debug('raising event from transport: %s %s' % (event_type,data)) + log.info('raising event from transport: %s %s' % (event_type,data)) if hasattr(self._owner, 'Dispatcher'): self._owner.Dispatcher.Event('', event_type, data) @@ -356,7 +365,7 @@ class NonBlockingTcp(PlugIn, IdleObject): else: self.on_receive = None return - log.debug('setting onreceive on %s' % recv_handler) + log.info('setting onreceive on %s' % recv_handler) self.on_receive = recv_handler @@ -372,7 +381,7 @@ class NonBlockingTcp(PlugIn, IdleObject): received = self._recv(RECV_BUFSIZE) except (socket.error, socket.herror, socket.gaierror), (errnum, errstr): # save exception number and message to errnum, errstr - log.debug("_do_receive: got %s:" % received , exc_info=True) + log.info("_do_receive: got %s:" % received , exc_info=True) if received == '': errnum = ERR_DISCONN @@ -412,10 +421,76 @@ class NonBlockingTcp(PlugIn, IdleObject): def _on_receive(self, data): # Overriding this method allows modifying received data before it is passed # to owner's callback. - log.debug('About to call on_receive which is %s' % self.on_receive) + log.info('About to call on_receive which is %s' % self.on_receive) self.on_receive(data) +class NonBlockingHTTP(NonBlockingTcp): + ''' + Socket wrapper that cretes HTTP message out of sent data and peels-off + HTTP headers from incoming messages + ''' + + def __init__(self, http_uri, http_port, on_disconnect): + self.http_protocol, self.http_host, self.http_path = urisplit(http_uri) + if self.http_protocol is None: + self.http_protocol = 'http' + if self.http_path == '': + http_path = '/' + self.http_port = http_port + NonBlockingTcp.__init__(self, on_disconnect) + + def send(self, raw_data, now=False): + + NonBlockingTcp.send( + self, + self.build_http_message(raw_data), + now) + + def _on_receive(self,data): + '''Preceeds passing received data to Client class. Gets rid of HTTP headers + and checks them.''' + statusline, headers, httpbody = self.parse_http_message(data) + if statusline[1] != '200': + log.error('HTTP Error: %s %s' % (statusline[1], statusline[2])) + self.disconnect() + return + self.on_receive(httpbody) + + + def build_http_message(self, httpbody): + ''' + Builds http message with given body. + Values for headers and status line fields are taken from class variables. + ) + ''' + headers = ['POST %s HTTP/1.1' % self.http_path, + 'Host: %s:%s' % (self.http_host, self.http_port), + 'Content-Type: text/xml; charset=utf-8', + 'Content-Length: %s' % len(str(httpbody)), + '\r\n'] + headers = '\r\n'.join(headers) + return('%s%s\r\n' % (headers, httpbody)) + + def parse_http_message(self, message): + ''' + splits http message to tuple ( + statusline - list of e.g. ['HTTP/1.1', '200', 'OK'], + headers - dictionary of headers e.g. {'Content-Length': '604', + 'Content-Type': 'text/xml; charset=utf-8'}, + httpbody - string with http body + ) + ''' + message = message.replace('\r','') + (header, httpbody) = message.split('\n\n') + header = header.split('\n') + statusline = header[0].split(' ') + header = header[1:] + headers = {} + for dummy in header: + row = dummy.split(' ',1) + headers[row[0][:-1]] = row[1] + return (statusline, headers, httpbody) @@ -460,7 +535,7 @@ class NBHTTPProxySocket(NBProxySocket): ''' Starts connection. Connects to proxy, supplies login and password to it (if were specified while creating instance). Instructs proxy to make connection to the target server. Returns non-empty sting on success. ''' - log.debug('Proxy server contacted, performing authentification') + log.info('Proxy server contacted, performing authentification') connector = ['CONNECT %s:%s HTTP/1.0' % self.xmpp_server, 'Proxy-Connection: Keep-Alive', 'Pragma: no-cache', @@ -504,7 +579,7 @@ class NBHTTPProxySocket(NBProxySocket): self.reply += reply.replace('\r', '') self._on_connect_failure('Proxy authentification failed') return - log.debug('Authentification successfull. Jabber server contacted.') + log.info('Authentification successfull. Jabber server contacted.') self._on_connect(self) @@ -517,7 +592,7 @@ class NBSOCKS5ProxySocket(NBProxySocket): # _on_connect_failure, at the end call _on_connect() def _on_tcp_connect(self): - self.DEBUG('Proxy server contacted, performing authentification', 'start') + log.info('Proxy server contacted, performing authentification') if self.proxy.has_key('user') and self.proxy.has_key('password'): to_send = '\x05\x02\x00\x02' else: @@ -532,7 +607,7 @@ class NBSOCKS5ProxySocket(NBProxySocket): self.on_proxy_failure('Invalid proxy reply') return if reply[0] != '\x05': - self.DEBUG('Invalid proxy reply', 'error') + log.info('Invalid proxy reply') self._owner.disconnected() self.on_proxy_failure('Invalid proxy reply') return @@ -648,67 +723,3 @@ class NBSOCKS5ProxySocket(NBProxySocket): -class NonBlockingHttpBOSH(NonBlockingTcp): - ''' - Socket wrapper that makes HTTP message out of send data and peels-off - HTTP headers from incoming messages - ''' - - def __init__(self, bosh_uri, bosh_port, on_disconnect): - self.bosh_protocol, self.bosh_host, self.bosh_path = self.urisplit(bosh_uri) - if self.bosh_protocol is None: - self.bosh_protocol = 'http' - if self.bosh_path == '': - bosh_path = '/' - self.bosh_port = bosh_port - - def send(self, raw_data, now=False): - - NonBlockingTcp.send( - self, - self.build_http_message(raw_data), - now) - - def _on_receive(self,data): - '''Preceeds passing received data to Client class. Gets rid of HTTP headers - and checks them.''' - statusline, headers, httpbody = self.parse_http_message(data) - if statusline[1] != '200': - log.error('HTTP Error: %s %s' % (statusline[1], statusline[2])) - self.disconnect() - self.on_receive(httpbody) - - - def build_http_message(self, httpbody): - ''' - Builds bosh http message with given body. - Values for headers and status line fields are taken from class variables. - ) - ''' - headers = ['POST %s HTTP/1.1' % self.bosh_path, - 'Host: %s:%s' % (self.bosh_host, self.bosh_port), - 'Content-Type: text/xml; charset=utf-8', - 'Content-Length: %s' % len(str(httpbody)), - '\r\n'] - headers = '\r\n'.join(headers) - return('%s%s\r\n' % (headers, httpbody)) - - def parse_http_message(self, message): - ''' - splits http message to tuple ( - statusline - list of e.g. ['HTTP/1.1', '200', 'OK'], - headers - dictionary of headers e.g. {'Content-Length': '604', - 'Content-Type': 'text/xml; charset=utf-8'}, - httpbody - string with http body - ) - ''' - message = message.replace('\r','') - (header, httpbody) = message.split('\n\n') - header = header.split('\n') - statusline = header[0].split(' ') - header = header[1:] - headers = {} - for dummy in header: - row = dummy.split(' ',1) - headers[row[0][:-1]] = row[1] - return (statusline, headers, httpbody) diff --git a/src/gajim.py b/src/gajim.py index 47d90a8d2..50b2d9f7e 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -50,7 +50,7 @@ import logging consoleloghandler = logging.StreamHandler() consoleloghandler.setLevel(1) consoleloghandler.setFormatter( -logging.Formatter('%(asctime)s %(name)s: %(levelname)s: %(message)s')) +logging.Formatter('%(name)s: %(levelname)s: %(message)s')) log = logging.getLogger('gajim') log.setLevel(logging.WARNING) log.addHandler(consoleloghandler) From f379d06d2c97257c165ad339680de38d4754ddb0 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Thu, 3 Jul 2008 06:26:39 +0000 Subject: [PATCH 08/20] fix connection for ipv6 --- src/common/xmpp/transports_nb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py index 613dbd693..3ee90241d 100644 --- a/src/common/xmpp/transports_nb.py +++ b/src/common/xmpp/transports_nb.py @@ -149,8 +149,8 @@ class NonBlockingTcp(PlugIn, IdleObject): ''' self.on_connect = on_connect self.on_connect_failure = on_connect_failure - (self.server, self.port) = conn_5tuple[4] - log.info('NonBlocking Connect :: About tot connect to %s:%s' % conn_5tuple[4]) + (self.server, self.port) = conn_5tuple[4][:2] + log.info('NonBlocking Connect :: About tot connect to %s:%s' % conn_5tuple[4][:2]) try: self._sock = socket.socket(*conn_5tuple[:3]) except socket.error, (errnum, errstr): From e1899f34dc808e3b08f90d11e1e66fce646a1f80 Mon Sep 17 00:00:00 2001 From: tomk Date: Mon, 7 Jul 2008 23:04:10 +0000 Subject: [PATCH 09/20] new BOSHDispatcher (in dispatcher_nb), improved BOSHClient class, minor changes in other xmpp modules --- src/common/xmpp/auth_nb.py | 5 +- src/common/xmpp/bosh.py | 247 +++++++++++++++++++++-------- src/common/xmpp/client.py | 20 ++- src/common/xmpp/client_nb.py | 220 ++++++++++++++------------ src/common/xmpp/dispatcher_nb.py | 112 +++++++++---- src/common/xmpp/simplexml.py | 9 +- src/common/xmpp/transports_nb.py | 259 +++++++++++++++++-------------- 7 files changed, 553 insertions(+), 319 deletions(-) diff --git a/src/common/xmpp/auth_nb.py b/src/common/xmpp/auth_nb.py index a05f82481..d8c045e0d 100644 --- a/src/common/xmpp/auth_nb.py +++ b/src/common/xmpp/auth_nb.py @@ -173,11 +173,8 @@ class SASL(PlugIn): self.startsasl='success' log.info('Successfully authenticated with remote server.') handlers=self._owner.Dispatcher.dumpHandlers() - print '6' * 79 - print handlers - print '6' * 79 self._owner.Dispatcher.PlugOut() - dispatcher_nb.Dispatcher().PlugIn(self._owner) + dispatcher_nb.Dispatcher().PlugIn(self._owner, after_SASL=True) self._owner.Dispatcher.restoreHandlers(handlers) self._owner.User = self.username if self.on_sasl : diff --git a/src/common/xmpp/bosh.py b/src/common/xmpp/bosh.py index e41322973..851d7cc4c 100644 --- a/src/common/xmpp/bosh.py +++ b/src/common/xmpp/bosh.py @@ -1,80 +1,208 @@ -import protocol, simplexml, locale, random, dispatcher_nb +import protocol, locale, random, dispatcher_nb from client_nb import NBCommonClient +import transports_nb import logging +from simplexml import Node log = logging.getLogger('gajim.c.x.bosh') class BOSHClient(NBCommonClient): ''' - Client class implementing BOSH. + Client class implementing BOSH. Extends common XMPP ''' - def __init__(self, *args, **kw): + def __init__(self, domain, idlequeue, caller=None): '''Preceeds constructor of NBCommonClient and sets some of values that will be used as attributes in tag''' - self.Namespace = protocol.NS_HTTP_BIND - # BOSH parameters should be given via Advanced Configuration Editor - self.bosh_xml_lang = None - self.bosh_hold = 1 - self.bosh_wait=60 - self.bosh_rid=None self.bosh_sid=None - self.bosh_httpversion = 'HTTP/1.1' - NBCommonClient.__init__(self, *args, **kw) - - - def connect(self, *args, **kw): - - - if locale.getdefaultlocale()[0]: - self.bosh_xml_lang = locale.getdefaultlocale()[0].split('_')[0] - # with 50-bit random initial rid, session would have to go up # to 7881299347898368 messages to raise rid over 2**53 # (see http://www.xmpp.org/extensions/xep-0124.html#rids) r = random.Random() r.seed() self.bosh_rid = r.getrandbits(50) + self.bosh_sid = None + + if locale.getdefaultlocale()[0]: + self.bosh_xml_lang = locale.getdefaultlocale()[0].split('_')[0] + else: + self.bosh_xml_lang = 'en' + + self.http_version = 'HTTP/1.1' + self.bosh_to = domain + + #self.Namespace = protocol.NS_HTTP_BIND + #self.defaultNamespace = self.Namespace + self.bosh_session_on = False + + NBCommonClient.__init__(self, domain, idlequeue, caller) + + + + def connect(self, on_connect, on_connect_failure, proxy, hostname=None, port=5222, + on_proxy_failure=None, secure=None): + ''' + Open XMPP connection (open XML streams in both directions). + :param hostname: hostname of XMPP server from SRV request + :param port: port number of XMPP server + :param on_connect: called after stream is successfully opened + :param on_connect_failure: called when error occures during connection + :param on_proxy_failure: called if error occurres during TCP connection to + proxy server or during connection to the proxy + :param proxy: dictionary with bosh-related paramters. It should contain at + least values for keys 'host' and 'port' - connection details for proxy + server and optionally keys 'user' and 'pass' as proxy credentials + :param secure: if + ''' + NBCommonClient.connect(self, on_connect, on_connect_failure, hostname, port, + on_proxy_failure, proxy, secure) + + if hostname: + self.route_host = hostname + else: + self.route_host = self.Server + + assert(proxy.has_key('type')) + assert(proxy['type']=='bosh') - proxy = kw['proxy'] - #self.bosh_protocol, self.bosh_host, self.bosh_uri = transports_nb.urisplit(proxy['host']) - self.bosh_port = proxy['port'] self.bosh_wait = proxy['bosh_wait'] self.bosh_hold = proxy['bosh_hold'] - self.bosh_to = proxy['to'] - #self.bosh_ack = proxy['bosh_ack'] - #self.bosh_secure = proxy['bosh_secure'] - NBCommonClient.connect(self, *args, **kw) + self.bosh_host = proxy['host'] + self.bosh_port = proxy['port'] + self.bosh_content = proxy['bosh_content'] + + # _on_tcp_failure is callback for errors which occur during name resolving or + # TCP connecting. + self._on_tcp_failure = self.on_proxy_failure + + + + # in BOSH, client connects to Connection Manager instead of directly to + # XMPP server ((hostname, port)). If HTTP Proxy is specified, client connects + # to HTTP proxy and Connection Manager is specified at URI and Host header + # in HTTP message + + # tcp_host, tcp_port is hostname and port for socket connection - Connection + # Manager or HTTP proxy + if proxy.has_key('proxy_host') and proxy['proxy_host'] and \ + proxy.has_key('proxy_port') and proxy['proxy_port']: + + tcp_host=proxy['proxy_host'] + tcp_port=proxy['proxy_port'] + + # user and password for HTTP proxy + if proxy.has_key('user') and proxy['user'] and \ + proxy.has_key('pass') and proxy['pass']: + + proxy_creds=(proxy['user'],proxy['pass']) + else: + proxy_creds=(None, None) + + else: + tcp_host = transports_nb.urisplit(proxy['host'])[1] + tcp_port=proxy['port'] + + if tcp_host is None: + self._on_connect_failure("Invalid BOSH URI") + return + + self.socket = self.get_socket() + + self._resolve_hostname( + hostname=tcp_host, + port=tcp_port, + on_success=self._try_next_ip, + on_failure=self._on_tcp_failure) + + def _on_stream_start(self): + ''' + Called after XMPP stream is opened. In BOSH, TLS is negotiated on socket + connect so success callback can be invoked after TCP connect. + (authentication is started from auth() method) + ''' + self.onreceive(None) + if self.connected == 'tcp': + self._on_connect() + + def get_socket(self): + tmp = transports_nb.NonBlockingHTTP( + raise_event=self.raise_event, + on_disconnect=self.on_http_disconnect, + http_uri = self.bosh_host, + http_port = self.bosh_port, + http_version = self.http_version + ) + tmp.PlugIn(self) + return tmp + + def on_http_disconnect(self): + log.info('HTTP socket disconnected') + #import traceback + #traceback.print_stack() + if self.bosh_session_on: + self.socket.connect( + conn_5tuple=self.current_ip, + on_connect=self.on_http_reconnect, + on_connect_failure=self.on_disconnect) + else: + self.on_disconnect() + + def on_http_reconnect(self): + self.socket._plug_idle() + log.info('Connected to BOSH CM again') + pass + + + def on_http_reconnect_fail(self): + log.error('Error when reconnecting to BOSH CM') + self.on_disconnect() def send(self, stanza, now = False): (id, stanza_to_send) = self.Dispatcher.assign_id(stanza) - self.Connection.send( + self.socket.send( self.boshify_stanza(stanza_to_send), now = now) return id + def get_rid(self): + # does this need a lock??" + self.bosh_rid = self.bosh_rid + 1 + return str(self.bosh_rid) + def get_bodytag(self): # this should be called not until after session creation response so sid has # to be initialized. - assert(self.sid is not None) - self.rid = self.rid+1 + assert(hasattr(self, 'bosh_sid')) return protocol.BOSHBody( - attrs={ 'rid': str(self.bosh_rid), + attrs={ 'rid': self.get_rid(), 'sid': self.bosh_sid}) - - def get_initial_bodytag(self): - return protocol.BOSHBody( - attrs={'content': 'text/xml; charset=utf-8', + def get_initial_bodytag(self, after_SASL=False): + tag = protocol.BOSHBody( + attrs={'content': self.bosh_content, 'hold': str(self.bosh_hold), + 'route': '%s:%s' % (self.route_host, self.Port), 'to': self.bosh_to, 'wait': str(self.bosh_wait), - 'rid': str(self.bosh_rid), + 'rid': self.get_rid(), + 'xml:lang': self.bosh_xml_lang, 'xmpp:version': '1.0', - 'xmlns:xmpp': 'urn:xmpp:xbosh'} - ) + 'ver': '1.6', + 'xmlns:xmpp': 'urn:xmpp:xbosh'}) + if after_SASL: + tag.delAttr('content') + tag.delAttr('hold') + tag.delAttr('route') + tag.delAttr('wait') + tag.delAttr('ver') + # xmpp:restart attribute is essential for stream restart request + tag.setAttr('xmpp:restart','true') + tag.setAttr('sid',self.bosh_sid) + + return tag + def get_closing_bodytag(self): closing_bodytag = self.get_bodytag() @@ -82,31 +210,26 @@ class BOSHClient(NBCommonClient): return closing_bodytag - def boshify_stanza(self, stanza): - ''' wraps stanza by body tag or modifies message entirely (in case of stream - opening and closing''' - log.info('boshify_staza - type is: %s' % type(stanza)) - if isinstance(stanza, simplexml.Node): - tag = self.get_bodytag() - return tag.setPayload(stanza) - else: - # only stream initialization and stream terminatoion are not Nodes - if stanza.startswith(dispatcher_nb.XML_DECLARATION): - # stream init - return self.get_initial_bodytag() - else: - # should be stream closing - assert(stanza == dispatcher_nb.STREAM_TERMINATOR) - return self.get_closing_bodytag() + def boshify_stanza(self, stanza=None, body_attrs=None): + ''' wraps stanza by body tag with rid and sid ''' + #log.info('boshify_staza - type is: %s, stanza is %s' % (type(stanza), stanza)) + tag = self.get_bodytag() + tag.setPayload([stanza]) + return tag + def on_bodytag_attrs(self, body_attrs): + #log.info('on_bodytag_attrs: %s' % body_attrs) + if body_attrs.has_key('type'): + if body_attrs['type']=='terminated': + # BOSH session terminated + self.bosh_session_on = False + elif body_attrs['type']=='error': + # recoverable error + pass + if not self.bosh_sid: + # initial response - when bosh_sid is set + self.bosh_session_on = True + self.bosh_sid = body_attrs['sid'] + self.Dispatcher.Stream._document_attrs['id']=body_attrs['authid'] - def _on_stream_start(self): - ''' - Called after XMPP stream is opened. In BOSH, TLS is negotiated elsewhere - so success callback can be invoked. - (authentication is started from auth() method) - ''' - self.onreceive(None) - if self.connected == 'tcp': - self._on_connect() diff --git a/src/common/xmpp/client.py b/src/common/xmpp/client.py index c30905963..f9f200dac 100644 --- a/src/common/xmpp/client.py +++ b/src/common/xmpp/client.py @@ -32,7 +32,7 @@ class PlugIn: def PlugIn(self,owner): """ Attach to main instance and register ourself and all our staff in it. """ self._owner=owner - log.debug('Plugging %s into %s'%(self,self._owner)) + log.info('Plugging %s __INTO__ %s' % (self,self._owner)) if owner.__dict__.has_key(self.__class__.__name__): log.debug('Plugging ignored: another instance already plugged.') return @@ -41,17 +41,27 @@ class PlugIn: if owner.__dict__.has_key(method.__name__): self._old_owners_methods.append(owner.__dict__[method.__name__]) owner.__dict__[method.__name__]=method - owner.__dict__[self.__class__.__name__]=self + if self.__class__.__name__.endswith('Dispatcher'): + # FIXME: I need BOSHDispatcher or XMPPDispatcher on .Dispatcher + # there must be a better way.. + owner.__dict__['Dispatcher']=self + else: + owner.__dict__[self.__class__.__name__]=self + # following will not work for classes inheriting plugin() #if self.__class__.__dict__.has_key('plugin'): return self.plugin(owner) if hasattr(self,'plugin'): return self.plugin(owner) def PlugOut(self): """ Unregister all our staff from main instance and detach from it. """ - log.debug('Plugging %s out of %s.'%(self,self._owner)) + log.info('Plugging %s __OUT__ of %s.' % (self,self._owner)) for method in self._exported_methods: del self._owner.__dict__[method.__name__] for method in self._old_owners_methods: self._owner.__dict__[method.__name__]=method - del self._owner.__dict__[self.__class__.__name__] - if self.__class__.__dict__.has_key('plugout'): return self.plugout() + if self.__class__.__name__.endswith('Dispatcher'): + del self._owner.__dict__['Dispatcher'] + else: + del self._owner.__dict__[self.__class__.__name__] + #if self.__class__.__dict__.has_key('plugout'): return self.plugout() + if hasattr(self,'plugout'): return self.plugout() del self._owner diff --git a/src/common/xmpp/client_nb.py b/src/common/xmpp/client_nb.py index df6c4ca20..c21437de5 100644 --- a/src/common/xmpp/client_nb.py +++ b/src/common/xmpp/client_nb.py @@ -41,11 +41,10 @@ class NBCommonClient: :param caller: calling object - it has to implement certain methods (necessary?) ''' - self.Namespace = protocol.NS_CLIENT - - self.idlequeue = idlequeue self.defaultNamespace = self.Namespace + + self.idlequeue = idlequeue self.disconnect_handlers = [] self.Server = domain @@ -85,12 +84,14 @@ class NBCommonClient: self.SASL.PlugOut() if self.__dict__.has_key('NonBlockingTLS'): self.NonBlockingTLS.PlugOut() - if self.__dict__.has_key('NBHTTPPROXYsocket'): + if self.__dict__.has_key('NBHTTPProxySocket'): self.NBHTTPPROXYsocket.PlugOut() - if self.__dict__.has_key('NBSOCKS5PROXYsocket'): + if self.__dict__.has_key('NBSOCKS5ProxySocket'): self.NBSOCKS5PROXYsocket.PlugOut() - if self.__dict__.has_key('NonBlockingTcp'): - self.NonBlockingTcp.PlugOut() + if self.__dict__.has_key('NonBlockingTCP'): + self.NonBlockingTCP.PlugOut() + if self.__dict__.has_key('NonBlockingHTTP'): + self.NonBlockingHTTP.PlugOut() def send(self, stanza, now = False): @@ -106,7 +107,7 @@ class NBCommonClient: def connect(self, on_connect, on_connect_failure, hostname=None, port=5222, on_proxy_failure=None, proxy=None, secure=None): ''' - Open XMPP connection (open streams in both directions). + Open XMPP connection (open XML streams in both directions). :param hostname: hostname of XMPP server from SRV request :param port: port number of XMPP server :param on_connect: called after stream is successfully opened @@ -118,70 +119,14 @@ class NBCommonClient: optionally keys 'user' and 'pass' as proxy credentials :param secure: ''' - self.Port = port - if hostname: - xmpp_hostname = hostname - else: - xmpp_hostname = self.Server - self.on_connect = on_connect self.on_connect_failure=on_connect_failure self.on_proxy_failure = on_proxy_failure self._secure = secure self.Connection = None + self.Port = port - if proxy: - # with proxies, client connects to proxy instead of directly to - # XMPP server ((hostname, port)) - # tcp_server is machine used for socket connection - tcp_server=proxy['host'] - tcp_port=proxy['port'] - self._on_tcp_failure = self.on_proxy_failure - if proxy.has_key('type'): - if proxy.has_key('user') and proxy.has_key('pass'): - proxy_creds=(proxy['user'],proxy['pass']) - else: - proxy_creds=(None, None) - - type_ = proxy['type'] - if type_ == 'socks5': - # SOCKS5 proxy - self.socket = transports_nb.NBSOCKS5ProxySocket( - on_disconnect=self.on_disconnect, - proxy_creds=proxy_creds, - xmpp_server=(xmpp_hostname, self.Port)) - elif type_ == 'http': - # HTTP CONNECT to proxy - self.socket = transports_nb.NBHTTPProxySocket( - on_disconnect=self.on_disconnect, - proxy_creds=proxy_creds, - xmpp_server=(xmpp_hostname, self.Port)) - elif type_ == 'bosh': - # BOSH - XMPP over HTTP - tcp_server = transports_nb.urisplit(tcp_server)[1] - self.socket = transports_nb.NonBlockingHTTP( - on_disconnect=self.on_disconnect, - http_uri = proxy['host'], - http_port = tcp_port) - else: - # HTTP CONNECT to proxy from environment variables - self.socket = transports_nb.NBHTTPProxySocket( - on_disconnect=self.on_disconnect, - proxy_creds=(None, None), - xmpp_server=(xmpp_hostname, self.Port)) - else: - self._on_tcp_failure = self._on_connect_failure - tcp_server=xmpp_hostname - tcp_port=self.Port - self.socket = transports_nb.NonBlockingTcp(on_disconnect = self.on_disconnect) - self.socket.PlugIn(self) - - self._resolve_hostname( - hostname=tcp_server, - port=tcp_port, - on_success=self._try_next_ip, - on_failure=self._on_tcp_failure) @@ -232,13 +177,14 @@ class NBCommonClient: started, and _on_connect_failure on failure. ''' #FIXME: use RegisterHandlerOnce instead of onreceive - log.info('=============xmpp_connect_machine() >> mode: %s, data: %s' % (mode,data)) + log.info('========xmpp_connect_machine() >> mode: %s, data: %s' % (mode,str(data)[:20] )) def on_next_receive(mode): + log.info('setting %s on next receive' % mode) if mode is None: self.onreceive(None) else: - self.onreceive(lambda data:self._xmpp_connect_machine(mode, data)) + self.onreceive(lambda _data:self._xmpp_connect_machine(mode, _data)) if not mode: dispatcher_nb.Dispatcher().PlugIn(self) @@ -259,9 +205,11 @@ class NBCommonClient: if not self.Dispatcher.Stream.features: on_next_receive('RECEIVE_STREAM_FEATURES') else: + log.info('got STREAM FEATURES in first read') self._xmpp_connect_machine(mode='STREAM_STARTED') else: + log.info('incoming stream version less than 1.0') self._xmpp_connect_machine(mode='STREAM_STARTED') elif mode == 'RECEIVE_STREAM_FEATURES': @@ -274,6 +222,7 @@ class NBCommonClient: mode='FAILURE', data='Missing in 1.0 stream') else: + log.info('got STREAM FEATURES in second read') self._xmpp_connect_machine(mode='STREAM_STARTED') elif mode == 'STREAM_STARTED': @@ -294,6 +243,10 @@ class NBCommonClient: self.onreceive(None) self.on_connect(self, self.connected) + def raise_event(self, event_type, data): + log.info('raising event from transport: %s %s' % (event_type,data)) + if hasattr(self, 'Dispatcher'): + self.Dispatcher.Event('', event_type, data) # moved from client.CommonClient: @@ -324,8 +277,6 @@ class NBCommonClient: def auth(self, user, password, resource = '', sasl = 1, on_auth = None): - - print 'auth called' ''' Authenticate connnection and bind resource. If resource is not provided random one or library name used. ''' self._User, self._Password, self._Resource, self._sasl = user, password, resource, sasl @@ -388,39 +339,6 @@ class NBCommonClient: self.on_auth(self, None) - - -class NonBlockingClient(NBCommonClient): - ''' Example client class, based on CommonClient. ''' - - - def _on_stream_start(self): - ''' - Called after XMPP stream is opened. - In pure XMPP client, TLS negotiation may follow after esabilishing a stream. - ''' - self.onreceive(None) - if self.connected == 'tcp': - if not self.connected or self._secure is not None and not self._secure: - # if we are disconnected or TLS/SSL is not desired, return - self._on_connect() - return - if not self.Dispatcher.Stream.features.getTag('starttls'): - # if server doesn't advertise TLS in init response - self._on_connect() - return - if self.incoming_stream_version() != '1.0': - self._on_connect() - return - # otherwise start TLS - tls_nb.NonBlockingTLS().PlugIn( - self, - on_tls_success=lambda: self._xmpp_connect(socket_type='tls'), - on_tls_failure=self._on_connect_failure) - elif self.connected == 'tls': - self._on_connect() - - def initRoster(self): ''' Plug in the roster. ''' if not self.__dict__.has_key('NonBlockingRoster'): @@ -440,3 +358,101 @@ class NonBlockingClient(NBCommonClient): self.send(dispatcher_nb.Presence(to=jid, typ=typ)) + +class NonBlockingClient(NBCommonClient): + ''' Example client class, based on CommonClient. ''' + + def __init__(self, domain, idlequeue, caller=None): + NBCommonClient.__init__(self, domain, idlequeue, caller) + + def connect(self, on_connect, on_connect_failure, hostname=None, port=5222, + on_proxy_failure=None, proxy=None, secure=None): + + NBCommonClient.connect(self, on_connect, on_connect_failure, hostname, port, + on_proxy_failure, proxy, secure) + + if hostname: + xmpp_hostname = hostname + else: + xmpp_hostname = self.Server + + if proxy: + # with proxies, client connects to proxy instead of directly to + # XMPP server ((hostname, port)) + # tcp_host is machine used for socket connection + tcp_host=proxy['host'] + tcp_port=proxy['port'] + self._on_tcp_failure = self.on_proxy_failure + if proxy.has_key('type'): + assert(proxy['type']!='bosh') + if proxy.has_key('user') and proxy.has_key('pass'): + proxy_creds=(proxy['user'],proxy['pass']) + else: + proxy_creds=(None, None) + + type_ = proxy['type'] + if type_ == 'socks5': + # SOCKS5 proxy + self.socket = transports_nb.NBSOCKS5ProxySocket( + on_disconnect=self.on_disconnect, + proxy_creds=proxy_creds, + xmpp_server=(xmpp_hostname, self.Port)) + elif type_ == 'http': + # HTTP CONNECT to proxy + self.socket = transports_nb.NBHTTPProxySocket( + on_disconnect=self.on_disconnect, + proxy_creds=proxy_creds, + xmpp_server=(xmpp_hostname, self.Port)) + else: + # HTTP CONNECT to proxy from environment variables + self.socket = transports_nb.NBHTTPProxySocket( + on_disconnect=self.on_disconnect, + proxy_creds=(None, None), + xmpp_server=(xmpp_hostname, self.Port)) + else: + self._on_tcp_failure = self._on_connect_failure + tcp_host=xmpp_hostname + tcp_port=self.Port + self.socket = transports_nb.NonBlockingTCP( + raise_event = self.raise_event, + on_disconnect = self.on_disconnect) + + self.socket.PlugIn(self) + + self._resolve_hostname( + hostname=tcp_host, + port=tcp_port, + on_success=self._try_next_ip, + on_failure=self._on_tcp_failure) + + + + + def _on_stream_start(self): + ''' + Called after XMPP stream is opened. + In pure XMPP client, TLS negotiation may follow after esabilishing a stream. + ''' + self.onreceive(None) + if self.connected == 'tcp': + if not self.connected or not self._secure: + # if we are disconnected or TLS/SSL is not desired, return + self._on_connect() + return + if not self.Dispatcher.Stream.features.getTag('starttls'): + # if server doesn't advertise TLS in init response + self._on_connect() + return + if self.incoming_stream_version() != '1.0': + self._on_connect() + return + # otherwise start TLS + tls_nb.NonBlockingTLS().PlugIn( + self, + on_tls_success=lambda: self._xmpp_connect(socket_type='tls'), + on_tls_failure=self._on_connect_failure) + elif self.connected == 'tls': + self._on_connect() + + + diff --git a/src/common/xmpp/dispatcher_nb.py b/src/common/xmpp/dispatcher_nb.py index 3734d16a9..1c6f04a8b 100644 --- a/src/common/xmpp/dispatcher_nb.py +++ b/src/common/xmpp/dispatcher_nb.py @@ -14,7 +14,6 @@ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. -# $Id: dispatcher.py,v 1.40 2006/01/18 19:26:43 normanr Exp $ ''' Main xmpppy mechanism. Provides library with methods to assign different handlers @@ -30,6 +29,7 @@ from client import PlugIn import logging log = logging.getLogger('gajim.c.x.dispatcher_nb') +log.setLevel(logging.INFO) # default timeout to wait for response for our id DEFAULT_TIMEOUT_SECONDS = 25 @@ -38,9 +38,33 @@ ID = 0 STREAM_TERMINATOR = '' XML_DECLARATION = '' -class Dispatcher(PlugIn): + + + +# FIXME: ugly +from client_nb import NonBlockingClient +from bosh import BOSHClient +class Dispatcher(): +# Why is this here - I needed to redefine Dispatcher for BOSH and easiest way +# was to inherit original Dispatcher (now renamed to XMPPDispatcher). Trouble +# is that reference used to access dispatcher instance is in Client attribute +# named by __class__.__name__ of the dispatcher instance .. long story short: +# I wrote following to avoid changing each client.Dispatcher.whatever() in xmpp/ + +# If having two kinds of dispatcher will go well, I will rewrite the + def PlugIn(self, client_obj, after_SASL=False): + if isinstance(client_obj, NonBlockingClient): + XMPPDispatcher().PlugIn(client_obj) + elif isinstance(client_obj, BOSHClient): + BOSHDispatcher().PlugIn(client_obj, after_SASL) + + + +class XMPPDispatcher(PlugIn): ''' Ancestor of PlugIn class. Handles XMPP stream, i.e. aware of stream headers. Can be plugged out/in to restart these headers (used for SASL f.e.). ''' + + def __init__(self): PlugIn.__init__(self) self.handlers={} @@ -84,8 +108,6 @@ class Dispatcher(PlugIn): def plugin(self, owner): ''' Plug the Dispatcher instance into Client class instance and send initial stream header. Used internally.''' - log.debug('Dispatcher plugin') - self._init() self._owner.lastErrNode = None self._owner.lastErr = None @@ -106,8 +128,8 @@ class Dispatcher(PlugIn): def StreamInit(self): ''' Send an initial stream header. ''' self.Stream = simplexml.NodeBuilder() - self.Stream._dispatch_depth = 2 self.Stream.dispatch = self.dispatch + self.Stream._dispatch_depth = 2 self.Stream.stream_header_received = self._check_stream_start self.Stream.features = None self._metastream = Node('stream:stream') @@ -159,7 +181,7 @@ class Dispatcher(PlugIn): ''' Creates internal structures for newly registered namespace. You can register handlers for this namespace afterwards. By default one namespace already registered (jabber:client or jabber:component:accept depending on context. ''' - log.info('Registering namespace "%s"' % xmlns) + log.debug('Registering namespace "%s"' % xmlns) self.handlers[xmlns]={} self.RegisterProtocol('unknown', Protocol, xmlns=xmlns) self.RegisterProtocol('default', Protocol, xmlns=xmlns) @@ -169,7 +191,7 @@ class Dispatcher(PlugIn): Needed to start registering handlers for such stanzas. Iq, message and presence protocols are registered by default. ''' if not xmlns: xmlns=self._owner.defaultNamespace - log.info('Registering protocol "%s" as %s(%s)' %(tag_name, Proto, xmlns)) + log.debug('Registering protocol "%s" as %s(%s)' %(tag_name, Proto, xmlns)) self.handlers[xmlns][tag_name]={type:Proto, 'default':[]} def RegisterNamespaceHandler(self, xmlns, handler, typ='', ns='', makefirst=0, system=0): @@ -196,7 +218,7 @@ class Dispatcher(PlugIn): ''' if not xmlns: xmlns=self._owner.defaultNamespace - log.info('Registering handler %s for "%s" type->%s ns->%s(%s)' % + log.debug('Registering handler %s for "%s" type->%s ns->%s(%s)' % (handler, name, typ, ns, xmlns)) if not typ and not ns: typ='default' @@ -287,32 +309,18 @@ class Dispatcher(PlugIn): def dispatch(self, stanza, session=None, direct=0): ''' Main procedure that performs XMPP stanza recognition and calling apppropriate handlers for it. Called internally. ''' + #log.info('dispatch called: stanza = %s, session = %s, direct= %s' % (stanza, session, direct)) if not session: session = self session.Stream._mini_dom = None name = stanza.getName() - if not direct and self._owner._component: - if name == 'route': - if stanza.getAttr('error') is None: - if len(stanza.getChildren()) == 1: - stanza = stanza.getChildren()[0] - name=stanza.getName() - else: - for each in stanza.getChildren(): - self.dispatch(each,session,direct=1) - return - elif name == 'presence': - return - elif name in ('features','bind'): - pass - else: - raise UnsupportedStanzaType(name) - if name=='features': session.Stream.features=stanza xmlns=stanza.getNamespace() + + #log.info('in dispatch, getting ns for %s, and the ns is %s' % (stanza, xmlns)) if not self.handlers.has_key(xmlns): log.warn("Unknown namespace: " + xmlns) xmlns='unknown' @@ -330,7 +338,6 @@ class Dispatcher(PlugIn): stanza.props=stanza.getProperties() ID=stanza.getID() - log.debug("Dispatching %s stanza with type->%s props->%s id->%s"%(name,typ,stanza.props,ID)) list=['default'] # we will use all handlers: if self.handlers[xmlns][name].has_key(typ): list.append(typ) # from very common... for prop in stanza.props: @@ -427,3 +434,56 @@ class Dispatcher(PlugIn): stanza.setParent(self._metastream) return (_ID, stanza) +class BOSHDispatcher(XMPPDispatcher): + + def PlugIn(self, owner, after_SASL=False): + self.after_SASL = after_SASL + XMPPDispatcher.PlugIn(self, owner) + + def StreamInit(self): + ''' Send an initial stream header. ''' + self.Stream = simplexml.NodeBuilder() + self.Stream.dispatch = self.dispatch + self.Stream._dispatch_depth = 2 + self.Stream.stream_header_received = self._check_stream_start + self.Stream.features = None + + self._metastream = Node('stream:stream') + self._metastream.setNamespace(self._owner.Namespace) + self._metastream.setAttr('version', '1.0') + self._metastream.setAttr('xmlns:stream', NS_STREAMS) + self._metastream.setAttr('to', self._owner.Server) + if locale.getdefaultlocale()[0]: + self._metastream.setAttr('xml:lang', + locale.getdefaultlocale()[0].split('_')[0]) + + self.restart = True + self._owner.Connection.send(self._owner.get_initial_bodytag(self.after_SASL)) + + + def StreamTerminate(self): + ''' Send a stream terminator. ''' + self._owner.Connection.send(self._owner.get_closing_bodytag()) + + def ProcessNonBlocking(self, data=None): + + if self.restart: + fromstream = self._metastream + fromstream.setAttr('from', fromstream.getAttr('to')) + fromstream.delAttr('to') + data = '%s%s>%s' % (XML_DECLARATION,str(fromstream)[:-2] ,data) + self.restart = False + + return XMPPDispatcher.ProcessNonBlocking(self, data) + + def dispatch(self, stanza, session=None, direct=0): + if stanza.getName()=='body' and stanza.getNamespace()==NS_HTTP_BIND: + self._owner.on_bodytag_attrs(stanza.getAttrs()) + #self._owner.send_empty_bodytag() + for child in stanza.getChildren(): + XMPPDispatcher.dispatch(self, child, session, direct) + else: + XMPPDispatcher.dispatch(self, stanza, session, direct) + + + diff --git a/src/common/xmpp/simplexml.py b/src/common/xmpp/simplexml.py index f7561269b..84d5165fa 100644 --- a/src/common/xmpp/simplexml.py +++ b/src/common/xmpp/simplexml.py @@ -20,7 +20,7 @@ I'm personally using it in many other separate projects. It is designed to be as import xml.parsers.expat import logging log = logging.getLogger('gajim.c.x.simplexml') - +#log.setLevel(logging.DEBUG) def XMLescape(txt): """Returns provided string with symbols & < > " replaced by their respective XML entities.""" @@ -99,7 +99,10 @@ class Node(object): for a in self.kids: if not fancy and (len(self.data)-1)>=cnt: s=s+XMLescape(self.data[cnt]) elif (len(self.data)-1)>=cnt: s=s+XMLescape(self.data[cnt].strip()) - s = s + a.__str__(fancy and fancy+1) + if isinstance(a, str) or isinstance(a, unicode): + s = s + a.__str__() + else: + s = s + a.__str__(fancy and fancy+1) cnt=cnt+1 if not fancy and (len(self.data)-1) >= cnt: s = s + XMLescape(self.data[cnt]) elif (len(self.data)-1) >= cnt: s = s + XMLescape(self.data[cnt].strip()) @@ -343,7 +346,7 @@ class NodeBuilder: attrs[self.namespaces[ns]+attr[sp+1:]]=attrs[attr] del attrs[attr] # self._inc_depth() - log.info("DEPTH -> %i , tag -> %s, attrs -> %s" % (self.__depth, tag, `attrs`)) + log.info("STARTTAG.. DEPTH -> %i , tag -> %s, attrs -> %s" % (self.__depth, tag, `attrs`)) if self.__depth == self._dispatch_depth: if not self._mini_dom : self._mini_dom = Node(tag=tag, attrs=attrs) diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py index 3ee90241d..a4e35656f 100644 --- a/src/common/xmpp/transports_nb.py +++ b/src/common/xmpp/transports_nb.py @@ -3,6 +3,7 @@ ## ## Copyright (C) 2003-2004 Alexey "Snake" Nezhdanov ## modified by Dimitur Kirov +## modified by Dimitur Kirov ## ## 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 @@ -65,10 +66,21 @@ CONNECTED ='CONNECTED' DISCONNECTING ='DISCONNECTING' + + class NonBlockingTransport(PlugIn): - def __init__(self, on_disconnect): + def __init__(self, raise_event, on_disconnect): PlugIn.__init__(self) + self.raise_event = raise_event self.on_disconnect = on_disconnect + self.on_connect = None + self.on_connect_failure = None + self.idlequeue = None + self.on_receive = None + self.server = None + self.port = None + self.state = DISCONNECTED + self._exported_methods=[self.disconnect, self.onreceive] def plugin(self, owner): owner.Connection=self @@ -79,30 +91,72 @@ class NonBlockingTransport(PlugIn): self._owner.Connection = None self._owner = None + def connect(self, conn_5tuple, on_connect, on_connect_failure): + self.on_connect = on_connect + self.on_connect_failure = on_connect_failure + (self.server, self.port) = conn_5tuple[4][:2] + log.info('NonBlocking Connect :: About tot connect to %s:%s' % (self.server, self.port)) -class NonBlockingTcp(PlugIn, IdleObject): + def set_state(self, newstate): + assert(newstate in [DISCONNECTED, CONNECTING, CONNECTED, DISCONNECTING]) + if (self.state, newstate) in [(CONNECTING, DISCONNECTING), (DISCONNECTED, DISCONNECTING)]: + log.info('strange move: %s -> %s' % (self.state, newstate)) + self.state = newstate + + def _on_connect(self, data): + ''' preceeds call of on_connect callback ''' + self.set_state(CONNECTED) + self.on_connect() + + def _on_connect_failure(self,err_message): + ''' preceeds call of on_connect_failure callback ''' + # In case of error while connecting we need to close socket + # but we don't want to call DisconnectHandlers from client, + # thus the do_callback=False + self.disconnect(do_callback=False) + self.on_connect_failure(err_message=err_message) + + def send(self, raw_data, now=False): + if self.state not in [CONNECTED, DISCONNECTING]: + # FIXME better handling needed + log.error('Trying to send %s when transport is %s.' % + (raw_data, self.state)) + return + + def disconnect(self, do_callback=True): + self.set_state(DISCONNECTED) + if do_callback: + # invoke callback given in __init__ + self.on_disconnect() + + def onreceive(self, recv_handler): + ''' Sets the on_receive callback. Do not confuse it with + on_receive() method, which is the callback itself.''' + if not recv_handler: + if hasattr(self._owner, 'Dispatcher'): + self.on_receive = self._owner.Dispatcher.ProcessNonBlocking + else: + self.on_receive = None + return + log.info('setting onreceive on %s' % recv_handler) + self.on_receive = recv_handler + + def tcp_connection_started(self): + self.set_state(CONNECTING) + # on_connect/on_conn_failure will be called from self.pollin/self.pollout + + + +class NonBlockingTCP(NonBlockingTransport, IdleObject): ''' Non-blocking TCP socket wrapper ''' - def __init__(self, on_disconnect): + def __init__(self, raise_event, on_disconnect): ''' Class constructor. ''' - - PlugIn.__init__(self) - IdleObject.__init__(self) - - self.on_disconnect = on_disconnect - - self.on_connect = None - self.on_connect_failure = None - self.sock = None - self.idlequeue = None - self.on_receive = None - self.DBG_LINE='socket' - self.state = DISCONNECTED - + NonBlockingTransport.__init__(self, raise_event, on_disconnect) # writable, readable - keep state of the last pluged flags # This prevents replug of same object with the same flags self.writable = True @@ -122,14 +176,6 @@ class NonBlockingTcp(PlugIn, IdleObject): self._exported_methods=[self.disconnect, self.onreceive, self.set_send_timeout, self.set_timeout, self.remove_timeout] - def plugin(self, owner): - owner.Connection=self - self.idlequeue = owner.idlequeue - - def plugout(self): - self._owner.Connection = None - self._owner = None - def get_fd(self): try: @@ -147,14 +193,12 @@ class NonBlockingTcp(PlugIn, IdleObject): :param on_connect_failure: callback called on failure when estabilishing tcp connection ''' - self.on_connect = on_connect - self.on_connect_failure = on_connect_failure - (self.server, self.port) = conn_5tuple[4][:2] - log.info('NonBlocking Connect :: About tot connect to %s:%s' % conn_5tuple[4][:2]) + NonBlockingTransport.connect(self, conn_5tuple, on_connect, on_connect_failure) + try: self._sock = socket.socket(*conn_5tuple[:3]) except socket.error, (errnum, errstr): - on_connect_failure('NonBlockingTcp: Error while creating socket: %s %s' % (errnum, errstr)) + self._on_connect_failure('NonBlockingTCP: Error while creating socket: %s %s' % (errnum, errstr)) return self._send = self._sock.send @@ -177,9 +221,8 @@ class NonBlockingTcp(PlugIn, IdleObject): if errnum in (errno.EINPROGRESS, errno.EALREADY, errno.EWOULDBLOCK): # connecting in progress - self.set_state(CONNECTING) log.info('After connect. "%s" raised => CONNECTING' % errstr) - # on_connect/failure will be called from self.pollin/self.pollout + self.tcp_connection_started() return elif errnum in (0, 10056, errno.EISCONN): # already connected - this branch is very unlikely, nonblocking connect() will @@ -195,27 +238,9 @@ class NonBlockingTcp(PlugIn, IdleObject): (self.server, self.port, errnum, errstr)) def _on_connect(self, data): - ''' preceeds call of on_connect callback ''' - self.set_state(CONNECTED) + ''' with TCP socket, we have to remove send-timeout ''' self.idlequeue.remove_timeout(self.get_fd()) - self.on_connect() - - - def set_state(self, newstate): - assert(newstate in [DISCONNECTED, CONNECTING, CONNECTED, DISCONNECTING]) - if (self.state, newstate) in [(CONNECTING, DISCONNECTING), (DISCONNECTED, DISCONNECTING)]: - log.info('strange move: %s -> %s' % (self.state, newstate)) - self.state = newstate - - - def _on_connect_failure(self,err_message): - ''' preceeds call of on_connect_failure callback ''' - # In case of error while connecting we need to close socket - # but we don't want to call DisconnectHandlers from client, - # thus the do_callback=False - self.disconnect(do_callback=False) - self.on_connect_failure(err_message=err_message) - + NonBlockingTransport._on_connect(self, data) def pollin(self): @@ -250,10 +275,7 @@ class NonBlockingTcp(PlugIn, IdleObject): self._sock.close() except socket.error, (errnum, errstr): log.error('Error disconnecting a socket: %s %s' % (errnum,errstr)) - self.set_state(DISCONNECTED) - if do_callback: - # invoke callback given in __init__ - self.on_disconnect() + NonBlockingTransport.disconnect(self, do_callback) def read_timeout(self): ''' @@ -295,11 +317,7 @@ class NonBlockingTcp(PlugIn, IdleObject): '''Append raw_data to the queue of messages to be send. If supplied data is unicode string, encode it to utf-8. ''' - - if self.state not in [CONNECTED, DISCONNECTING]: - log.error('Trying to send %s when transport is %s.' % - (raw_data, self.state)) - return + NonBlockingTransport.send(self, raw_data, now) r = raw_data if isinstance(r, unicode): r = r.encode('utf-8') @@ -343,31 +361,13 @@ class NonBlockingTcp(PlugIn, IdleObject): sent_data = self.sendbuff[:send_count] self.sendbuff = self.sendbuff[send_count:] self._plug_idle() - self._raise_event(DATA_SENT, sent_data) + self.raise_event(DATA_SENT, sent_data) except socket.error, e: log.error('_do_send:', exc_info=True) traceback.print_exc() self.disconnect() - def _raise_event(self, event_type, data): - if data and data.strip(): - log.info('raising event from transport: %s %s' % (event_type,data)) - if hasattr(self._owner, 'Dispatcher'): - self._owner.Dispatcher.Event('', event_type, data) - - def onreceive(self, recv_handler): - ''' Sets the on_receive callback. Do not confuse it with - on_receive() method, which is the callback itself.''' - if not recv_handler: - if hasattr(self._owner, 'Dispatcher'): - self.on_receive = self._owner.Dispatcher.ProcessNonBlocking - else: - self.on_receive = None - return - log.info('setting onreceive on %s' % recv_handler) - self.on_receive = recv_handler - def _do_receive(self): ''' Reads all pending incoming data. Calls owner's disconnected() method if appropriate.''' @@ -410,7 +410,7 @@ class NonBlockingTcp(PlugIn, IdleObject): # pass received data to owner #self. if self.on_receive: - self._raise_event(DATA_RECEIVED, received) + self.raise_event(DATA_RECEIVED, received) self._on_receive(received) else: # This should never happen, so we need the debug. (If there is no handler @@ -418,31 +418,37 @@ class NonBlockingTcp(PlugIn, IdleObject): log.error('SOCKET Unhandled data received: %s' % received) self.disconnect() - def _on_receive(self, data): - # Overriding this method allows modifying received data before it is passed - # to owner's callback. - log.info('About to call on_receive which is %s' % self.on_receive) + def _on_receive(self,data): + '''Preceeds passing received data to Client class. Gets rid of HTTP headers + and checks them.''' self.on_receive(data) -class NonBlockingHTTP(NonBlockingTcp): + +class NonBlockingHTTP(NonBlockingTCP): ''' Socket wrapper that cretes HTTP message out of sent data and peels-off HTTP headers from incoming messages ''' - def __init__(self, http_uri, http_port, on_disconnect): + def __init__(self, raise_event, on_disconnect, http_uri, http_port, http_version=None): self.http_protocol, self.http_host, self.http_path = urisplit(http_uri) if self.http_protocol is None: self.http_protocol = 'http' if self.http_path == '': http_path = '/' self.http_port = http_port - NonBlockingTcp.__init__(self, on_disconnect) + if http_version: + self.http_version = http_version + else: + self.http_version = 'HTTP/1.1' + # buffer for partial responses + self.recvbuff = '' + self.expected_length = 0 + NonBlockingTCP.__init__(self, raise_event, on_disconnect) def send(self, raw_data, now=False): - - NonBlockingTcp.send( + NonBlockingTCP.send( self, self.build_http_message(raw_data), now) @@ -450,21 +456,43 @@ class NonBlockingHTTP(NonBlockingTcp): def _on_receive(self,data): '''Preceeds passing received data to Client class. Gets rid of HTTP headers and checks them.''' - statusline, headers, httpbody = self.parse_http_message(data) - if statusline[1] != '200': - log.error('HTTP Error: %s %s' % (statusline[1], statusline[2])) - self.disconnect() + if not self.recvbuff: + # recvbuff empty - fresh HTTP message was received + statusline, headers, self.recvbuff = self.parse_http_message(data) + if statusline[1] != '200': + log.error('HTTP Error: %s %s' % (statusline[1], statusline[2])) + self.disconnect() + return + self.expected_length = int(headers['Content-Length']) + else: + #sth in recvbuff - append currently received data to HTTP mess in buffer + self.recvbuff = '%s%s' % (self.recvbuff, data) + + if self.expected_length > len(self.recvbuff): + # If we haven't received the whole HTTP mess yet, let's end the thread. + # It will be finnished from one of following poll calls on plugged socket. return + + # FIXME the reassembling doesn't work - Connection Manager on jabbim.cz + # closes TCP connection before sending announced bytes.. WTF + + # all was received, now call the on_receive callback + httpbody = self.recvbuff + + self.recvbuff='' + self.expected_length=0 self.on_receive(httpbody) - def build_http_message(self, httpbody): + def build_http_message(self, httpbody, method='POST'): ''' Builds http message with given body. Values for headers and status line fields are taken from class variables. ) ''' - headers = ['POST %s HTTP/1.1' % self.http_path, + absolute_uri = '%s://%s:%s%s' % (self.http_protocol, self.http_host, + self.http_port, self.http_path) + headers = ['%s %s %s' % (method, absolute_uri, self.http_version), 'Host: %s:%s' % (self.http_host, self.http_port), 'Content-Type: text/xml; charset=utf-8', 'Content-Length: %s' % len(str(httpbody)), @@ -482,7 +510,7 @@ class NonBlockingHTTP(NonBlockingTcp): ) ''' message = message.replace('\r','') - (header, httpbody) = message.split('\n\n') + (header, httpbody) = message.split('\n\n',1) header = header.split('\n') statusline = header[0].split(' ') header = header[1:] @@ -494,15 +522,15 @@ class NonBlockingHTTP(NonBlockingTcp): -class NBProxySocket(NonBlockingTcp): +class NBProxySocket(NonBlockingTCP): ''' Interface for proxy socket wrappers - when tunnneling XMPP over proxies, some connecting process usually has to be done before opening stream. ''' - def __init__(self, on_disconnect, xmpp_server, proxy_creds=(None,None)): + def __init__(self, raise_event, on_disconnect, xmpp_server, proxy_creds=(None,None)): self.proxy_user, self.proxy_pass = proxy_creds self.xmpp_server = xmpp_server - NonBlockingTcp.__init__(self, on_disconnect) + NonBlockingTCP.__init__(self, raise_event, on_disconnect) def connect(self, conn_5tuple, on_connect, on_connect_failure): @@ -515,7 +543,7 @@ class NBProxySocket(NonBlockingTcp): self.after_proxy_connect = on_connect - NonBlockingTcp.connect(self, + NonBlockingTCP.connect(self, conn_5tuple=conn_5tuple, on_connect =self._on_tcp_connect, on_connect_failure =on_connect_failure) @@ -526,7 +554,7 @@ class NBProxySocket(NonBlockingTcp): class NBHTTPProxySocket(NBProxySocket): - ''' This class can be used instead of NonBlockingTcp + ''' This class can be used instead of NonBlockingTCP HTTP (CONNECT) proxy connection class. Allows to use HTTP proxies like squid with (optionally) simple authentication (using login and password). ''' @@ -588,7 +616,7 @@ class NBSOCKS5ProxySocket(NBProxySocket): redefines only connect method. Allows to use SOCKS5 proxies with (optionally) simple authentication (only USERNAME/PASSWORD auth). ''' - # TODO: replace DEBUG with ordinrar logging, replace on_proxy_failure() with + # TODO: replace on_proxy_failure() with # _on_connect_failure, at the end call _on_connect() def _on_tcp_connect(self): @@ -620,13 +648,13 @@ class NBSOCKS5ProxySocket(NBProxySocket): self.send(to_send) else: if reply[1] == '\xff': - self.DEBUG('Authentification to proxy impossible: no acceptable ' - 'auth method', 'error') + log.error('Authentification to proxy impossible: no acceptable ' + 'auth method') self._owner.disconnected() self.on_proxy_failure('Authentification to proxy impossible: no ' 'acceptable authentification method') return - self.DEBUG('Invalid proxy reply', 'error') + log.error('Invalid proxy reply') self._owner.disconnected() self.on_proxy_failure('Invalid proxy reply') return @@ -635,21 +663,21 @@ class NBSOCKS5ProxySocket(NBProxySocket): if reply is None: return if len(reply) != 2: - self.DEBUG('Invalid proxy reply', 'error') + log.error('Invalid proxy reply') self._owner.disconnected() self.on_proxy_failure('Invalid proxy reply') return if reply[0] != '\x01': - self.DEBUG('Invalid proxy reply', 'error') + log.error('Invalid proxy reply') self._owner.disconnected() self.on_proxy_failure('Invalid proxy reply') return if reply[1] != '\x00': - self.DEBUG('Authentification to proxy failed', 'error') + log.error('Authentification to proxy failed') self._owner.disconnected() self.on_proxy_failure('Authentification to proxy failed') return - self.DEBUG('Authentification successfull. Jabber server contacted.','ok') + log.info('Authentification successfull. Jabber server contacted.') # Request connection req = "\x05\x01\x00" # If the given destination address is an IP address, we'll @@ -675,12 +703,12 @@ class NBSOCKS5ProxySocket(NBProxySocket): if reply is None: return if len(reply) < 10: - self.DEBUG('Invalid proxy reply', 'error') + log.error('Invalid proxy reply') self._owner.disconnected() self.on_proxy_failure('Invalid proxy reply') return if reply[0] != '\x05': - self.DEBUG('Invalid proxy reply', 'error') + log.error('Invalid proxy reply') self._owner.disconnected() self.on_proxy_failure('Invalid proxy reply') return @@ -700,7 +728,7 @@ class NBSOCKS5ProxySocket(NBProxySocket): txt = errors[ord(reply[1])-1] else: txt = 'Invalid proxy reply' - self.DEBUG(txt, 'error') + log.error(txt) self.on_proxy_failure(txt) return # Get the bound address/port @@ -709,7 +737,7 @@ class NBSOCKS5ProxySocket(NBProxySocket): elif reply[3] == "\x03": begin, end = 4, 4 + reply[4] else: - self.DEBUG('Invalid proxy reply', 'error') + log.error('Invalid proxy reply') self._owner.disconnected() self.on_proxy_failure('Invalid proxy reply') return @@ -717,9 +745,6 @@ class NBSOCKS5ProxySocket(NBProxySocket): if self.on_connect_proxy: self.on_connect_proxy() - def DEBUG(self, text, severity): - ''' Overwrites DEBUG tag to allow debug output be presented as "CONNECTproxy".''' - return self._owner.DEBUG(DBG_CONNECT_PROXY, text, severity) From 3d860f40a6dc99853684e7e9f1277b12207b40b9 Mon Sep 17 00:00:00 2001 From: tomk Date: Sun, 13 Jul 2008 22:22:58 +0000 Subject: [PATCH 10/20] BOSHClient transformed to NonBlockingBOSH transport - it's easier to maintain more connections from below, implemented handling of non-persistent HTTP connections - it runs with ejabberd, improved NonBlockingTransport interface, minor changes in BOSHDispatcher --- src/common/connection.py | 46 ++-- src/common/xmpp/bosh.py | 375 ++++++++++++++++--------------- src/common/xmpp/client.py | 2 +- src/common/xmpp/client_nb.py | 82 ++++--- src/common/xmpp/dispatcher_nb.py | 85 ++++--- src/common/xmpp/idlequeue.py | 11 +- src/common/xmpp/protocol.py | 14 +- src/common/xmpp/transports_nb.py | 181 +++++++++------ test/test_nonblockingtcp.py | 8 +- 9 files changed, 451 insertions(+), 353 deletions(-) diff --git a/src/common/connection.py b/src/common/connection.py index 649baf178..36e7719b6 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -521,24 +521,19 @@ class Connection(ConnectionHandlers): self.connection = None if self._current_type == 'ssl': + # SSL (force TLS on different port than plain) port = self._current_host['ssl_port'] - secur = 1 + secure = 'force' else: port = self._current_host['port'] if self._current_type == 'plain': - secur = 0 + # plain connection + secure = None else: - secur = None + # TLS (on the same port as plain) + secure = 'negotiate' - if self._proxy and self._proxy['type'] == 'bosh': - clientClass = common.xmpp.bosh.BOSHClient - else: - clientClass = common.xmpp.NonBlockingClient - - # there was: - # "if gajim.verbose:" - # here - con = clientClass( + con = common.xmpp.NonBlockingClient( domain=self._hostname, caller=self, idlequeue=gajim.idlequeue) @@ -550,11 +545,11 @@ class Connection(ConnectionHandlers): if self.on_connect_success == self._on_new_account: con.RegisterDisconnectHandler(self._on_new_account) - # FIXME: BOSH properties should be in proxy dictionary - loaded from - # config - if self._proxy and self._proxy['type'] == 'bosh': + # FIXME: BOSH properties should be loaded from config + if self._proxy and self._proxy['type'] == 'bosh': self._proxy['bosh_hold'] = '1' self._proxy['bosh_wait'] = '60' + self._proxy['bosh_content'] = 'text/xml; charset=utf-8' log.info('Connecting to %s: [%s:%d]', self.name, @@ -566,7 +561,7 @@ class Connection(ConnectionHandlers): on_proxy_failure=self.on_proxy_failure, on_connect_failure=self.connect_to_next_type, proxy=self._proxy, - secure = secur) + secure = secure) else: self.connect_to_next_host(retry) @@ -578,9 +573,11 @@ class Connection(ConnectionHandlers): 'connection_types').split() else: self._connection_types = ['tls', 'ssl', 'plain'] - + # FIXME: remove after tls and ssl will be degubbed - #self._connection_types = ['plain'] + self._connection_types = ['plain'] + + host = self.select_next_host(self._hosts) self._current_host = host self._hosts.remove(host) @@ -619,6 +616,8 @@ class Connection(ConnectionHandlers): if _con_type == 'tcp': _con_type = 'plain' if _con_type != self._current_type: + log.info('Connecting to next type beacuse desired is %s and returned is %s' + % (self._current_type, _con_type)) self.connect_to_next_type() return if _con_type == 'plain' and gajim.config.get_per('accounts', self.name, @@ -662,7 +661,12 @@ class Connection(ConnectionHandlers): (con.Connection.ssl_fingerprint_sha1,)) return True self._register_handlers(con, con_type) - con.auth(name, self.password, self.server_resource, 1, self.__on_auth) + con.auth( + user=name, + password=self.password, + resource=self.server_resource, + sasl=1, + on_auth=self.__on_auth) def ssl_certificate_accepted(self): name = gajim.config.get_per('accounts', self.name, 'name') @@ -997,7 +1001,7 @@ class Connection(ConnectionHandlers): self.time_to_reconnect = None self.connection.RegisterDisconnectHandler(self._on_disconnected) - self.connection.send(p) + self.connection.send(p, now=True) self.connection.StreamTerminate() #self.connection.start_disconnect(p, self._on_disconnected) else: @@ -1554,7 +1558,7 @@ class Connection(ConnectionHandlers): def gc_got_disconnected(self, room_jid): ''' A groupchat got disconnected. This can be or purpose or not. Save the time we quit to avoid duplicate logs AND be faster than get that - date from DB. Save it in mem AND in a small table (with fast access) + date from DB. Save it in mem AND in a small table (with fast access) ''' log_time = time_time() self.last_history_time[room_jid] = log_time diff --git a/src/common/xmpp/bosh.py b/src/common/xmpp/bosh.py index 851d7cc4c..3d53bdf40 100644 --- a/src/common/xmpp/bosh.py +++ b/src/common/xmpp/bosh.py @@ -1,235 +1,260 @@ -import protocol, locale, random, dispatcher_nb -from client_nb import NBCommonClient -import transports_nb -import logging +import locale, random +from transports_nb import NonBlockingTransport, NonBlockingHTTP, CONNECTED, CONNECTING, DISCONNECTED +from protocol import BOSHBody from simplexml import Node + +import logging log = logging.getLogger('gajim.c.x.bosh') -class BOSHClient(NBCommonClient): - ''' - Client class implementing BOSH. Extends common XMPP - ''' - def __init__(self, domain, idlequeue, caller=None): - '''Preceeds constructor of NBCommonClient and sets some of values that will - be used as attributes in tag''' - self.bosh_sid=None +FAKE_DESCRIPTOR = -1337 +'''Fake file descriptor - it's used for setting read_timeout in idlequeue for +BOSH Transport. Timeouts in queue are saved by socket descriptor. +In TCP-derived transports it is file descriptor of socket''' + + +class NonBlockingBOSH(NonBlockingTransport): + def __init__(self, raise_event, on_disconnect, idlequeue, xmpp_server, domain, + bosh_dict): + NonBlockingTransport.__init__(self, raise_event, on_disconnect, idlequeue) # with 50-bit random initial rid, session would have to go up # to 7881299347898368 messages to raise rid over 2**53 # (see http://www.xmpp.org/extensions/xep-0124.html#rids) r = random.Random() r.seed() + global FAKE_DESCRIPTOR + FAKE_DESCRIPTOR = FAKE_DESCRIPTOR - 1 + self.fake_fd = FAKE_DESCRIPTOR self.bosh_rid = r.getrandbits(50) self.bosh_sid = None - if locale.getdefaultlocale()[0]: self.bosh_xml_lang = locale.getdefaultlocale()[0].split('_')[0] else: self.bosh_xml_lang = 'en' self.http_version = 'HTTP/1.1' + self.http_persistent = False + self.http_pipelining = False self.bosh_to = domain - #self.Namespace = protocol.NS_HTTP_BIND - #self.defaultNamespace = self.Namespace - self.bosh_session_on = False + self.route_host, self.route_port = xmpp_server - NBCommonClient.__init__(self, domain, idlequeue, caller) + self.bosh_wait = bosh_dict['bosh_wait'] + self.bosh_hold = bosh_dict['bosh_hold'] + self.bosh_host = bosh_dict['host'] + self.bosh_port = bosh_dict['port'] + self.bosh_content = bosh_dict['bosh_content'] + + self.http_socks = [] + self.stanzas_to_send = [] + self.prio_bosh_stanza = None + self.current_recv_handler = None + + # if proxy_host .. do sth about HTTP proxy etc. + + + def connect(self, conn_5tuple, on_connect, on_connect_failure): + NonBlockingTransport.connect(self, conn_5tuple, on_connect, on_connect_failure) + self.http_socks.append(self.get_http_socket()) + self.tcp_connection_started() + + # this connect() is not needed because sockets can be connected on send but + # we need to know if host is reachable in order to invoke callback for + # failure when connecting (it's different than callback for errors + # occurring after connection is etabilished) + + self.http_socks[0].connect( + conn_5tuple = conn_5tuple, + on_connect = lambda: self._on_connect(self.http_socks[0]), + on_connect_failure = self._on_connect_failure) - def connect(self, on_connect, on_connect_failure, proxy, hostname=None, port=5222, - on_proxy_failure=None, secure=None): - ''' - Open XMPP connection (open XML streams in both directions). - :param hostname: hostname of XMPP server from SRV request - :param port: port number of XMPP server - :param on_connect: called after stream is successfully opened - :param on_connect_failure: called when error occures during connection - :param on_proxy_failure: called if error occurres during TCP connection to - proxy server or during connection to the proxy - :param proxy: dictionary with bosh-related paramters. It should contain at - least values for keys 'host' and 'port' - connection details for proxy - server and optionally keys 'user' and 'pass' as proxy credentials - :param secure: if + def get_fd(self): + return self.fake_fd + + def on_http_request_possible(self): ''' - NBCommonClient.connect(self, on_connect, on_connect_failure, hostname, port, - on_proxy_failure, proxy, secure) + Called after HTTP response is received - another request is possible. + There should be always one pending request on BOSH CM. + ''' + log.info('on_http_req possible, stanzas in list: %s, state:\n%s' % + (self.stanzas_to_send, self.get_current_state())) + # if one of sockets is connecting, sth is about to be sent - we don't have to + # send request from here + for s in self.http_socks: + if s.state==CONNECTING or s.pending_requests>0: return + self.flush_stanzas() - if hostname: - self.route_host = hostname + + def flush_stanzas(self): + # another to-be-locked candidate + log.info('flushing stanzas') + if self.prio_bosh_stanza: + tmp = self.prio_bosh_stanza + self.prio_bosh_stanza = None else: - self.route_host = self.Server - - assert(proxy.has_key('type')) - assert(proxy['type']=='bosh') - - self.bosh_wait = proxy['bosh_wait'] - self.bosh_hold = proxy['bosh_hold'] - self.bosh_host = proxy['host'] - self.bosh_port = proxy['port'] - self.bosh_content = proxy['bosh_content'] - - # _on_tcp_failure is callback for errors which occur during name resolving or - # TCP connecting. - self._on_tcp_failure = self.on_proxy_failure + tmp = self.stanzas_to_send + self.stanzas_to_send = [] + self.send_http(tmp) - - # in BOSH, client connects to Connection Manager instead of directly to - # XMPP server ((hostname, port)). If HTTP Proxy is specified, client connects - # to HTTP proxy and Connection Manager is specified at URI and Host header - # in HTTP message - - # tcp_host, tcp_port is hostname and port for socket connection - Connection - # Manager or HTTP proxy - if proxy.has_key('proxy_host') and proxy['proxy_host'] and \ - proxy.has_key('proxy_port') and proxy['proxy_port']: - - tcp_host=proxy['proxy_host'] - tcp_port=proxy['proxy_port'] + def send(self, stanza, now=False): + # body tags should be send only via send_http() + assert(not isinstance(stanza, BOSHBody)) + now = True + if now: + self.send_http([stanza]) + else: + self.stanzas_to_send.append(stanza) - # user and password for HTTP proxy - if proxy.has_key('user') and proxy['user'] and \ - proxy.has_key('pass') and proxy['pass']: - proxy_creds=(proxy['user'],proxy['pass']) + def send_http(self, payload): + # "Protocol" and string/unicode stanzas should be sent via send() + # (only initiating and terminating BOSH stanzas should be send via send_http) + assert(isinstance(payload, list) or isinstance(payload, BOSHBody)) + log.warn('send_http: stanzas: %s\n%s' % (payload, self.get_current_state())) + + if isinstance(payload, list): + bosh_stanza = self.boshify_stanzas(payload) + else: + # bodytag_payload is , we don't boshify, only add the rid + bosh_stanza = payload + picked_sock = self.pick_socket() + if picked_sock: + log.info('sending to socket %s' % id(picked_sock)) + bosh_stanza.setAttr('rid', self.get_rid()) + picked_sock.send(bosh_stanza) + else: + # no socket was picked but one is about to connect - save the stanza and + # return + if self.prio_bosh_stanza: + payload = self.merge_stanzas(payload, self.prio_bosh_stanza) + if payload is None: + log.error('Error in BOSH socket handling - unable to send %s because %s\ + is already about to be sent' % (payload, self.prio_bosh_stanza)) + self.disconnect() + self.prio_bosh_stanza = payload + + def merge_stanzas(self, s1, s2): + if isinstance(s1, BOSHBody): + if isinstance(s2, BOSHBody): + # both are boshbodies + return else: - proxy_creds=(None, None) - + s1.setPayload(s2, add=True) + return s1 + elif isinstance(s2, BOSHBody): + s2.setPayload(s1, add=True) + return s2 else: - tcp_host = transports_nb.urisplit(proxy['host'])[1] - tcp_port=proxy['port'] + #both are lists + s1.extend(s2) + return s1 - if tcp_host is None: - self._on_connect_failure("Invalid BOSH URI") + + def get_current_state(self): + t = '\t\tSOCKET_ID\tSOCKET_STATE\tPENDING_REQS\n' + for s in self.http_socks: + t = '%s\t\t%s\t%s\t%s\n' % (t,id(s), s.state, s.pending_requests) + return t + + + def pick_socket(self): + # try to pick connected socket with no pending reqs + for s in self.http_socks: + if s.state == CONNECTED and s.pending_requests == 0: + return s + + # try to connect some disconnected socket + for s in self.http_socks: + if s.state==DISCONNECTED: + self.connect_and_flush(s) return - self.socket = self.get_socket() - - self._resolve_hostname( - hostname=tcp_host, - port=tcp_port, - on_success=self._try_next_ip, - on_failure=self._on_tcp_failure) - - def _on_stream_start(self): - ''' - Called after XMPP stream is opened. In BOSH, TLS is negotiated on socket - connect so success callback can be invoked after TCP connect. - (authentication is started from auth() method) - ''' - self.onreceive(None) - if self.connected == 'tcp': - self._on_connect() - - def get_socket(self): - tmp = transports_nb.NonBlockingHTTP( - raise_event=self.raise_event, - on_disconnect=self.on_http_disconnect, - http_uri = self.bosh_host, - http_port = self.bosh_port, - http_version = self.http_version - ) - tmp.PlugIn(self) - return tmp - - def on_http_disconnect(self): - log.info('HTTP socket disconnected') - #import traceback - #traceback.print_stack() - if self.bosh_session_on: - self.socket.connect( - conn_5tuple=self.current_ip, - on_connect=self.on_http_reconnect, - on_connect_failure=self.on_disconnect) - else: - self.on_disconnect() - - def on_http_reconnect(self): - self.socket._plug_idle() - log.info('Connected to BOSH CM again') - pass + # if there is any just-connecting socket, it will send the data in its + # connect callback + for s in self.http_socks: + if s.state==CONNECTING: + return + # being here means there are only CONNECTED scokets with pending requests. + # Lets create and connect another one + s = self.get_http_socket() + self.http_socks.append(s) + self.connect_and_flush(s) + return - def on_http_reconnect_fail(self): - log.error('Error when reconnecting to BOSH CM') - self.on_disconnect() - - def send(self, stanza, now = False): - (id, stanza_to_send) = self.Dispatcher.assign_id(stanza) + def connect_and_flush(self, socket): + socket.connect( + conn_5tuple = self.conn_5tuple, + on_connect = self.flush_stanzas, + on_connect_failure = self.disconnect) - self.socket.send( - self.boshify_stanza(stanza_to_send), - now = now) - return id - def get_rid(self): - # does this need a lock??" - self.bosh_rid = self.bosh_rid + 1 - return str(self.bosh_rid) + def boshify_stanzas(self, stanzas=[], body_attrs=None): + ''' wraps zero to many stanzas by body tag with xmlns and sid ''' + log.debug('boshify_staza - type is: %s, stanza is %s' % (type(stanzas), stanzas)) + tag = BOSHBody(attrs={'sid': self.bosh_sid}) + tag.setPayload(stanzas) + return tag - def get_bodytag(self): - # this should be called not until after session creation response so sid has - # to be initialized. - assert(hasattr(self, 'bosh_sid')) - return protocol.BOSHBody( - attrs={ 'rid': self.get_rid(), - 'sid': self.bosh_sid}) def get_initial_bodytag(self, after_SASL=False): - tag = protocol.BOSHBody( + return BOSHBody( attrs={'content': self.bosh_content, 'hold': str(self.bosh_hold), - 'route': '%s:%s' % (self.route_host, self.Port), + 'route': '%s:%s' % (self.route_host, self.route_port), 'to': self.bosh_to, 'wait': str(self.bosh_wait), - 'rid': self.get_rid(), 'xml:lang': self.bosh_xml_lang, 'xmpp:version': '1.0', 'ver': '1.6', 'xmlns:xmpp': 'urn:xmpp:xbosh'}) - if after_SASL: - tag.delAttr('content') - tag.delAttr('hold') - tag.delAttr('route') - tag.delAttr('wait') - tag.delAttr('ver') - # xmpp:restart attribute is essential for stream restart request - tag.setAttr('xmpp:restart','true') - tag.setAttr('sid',self.bosh_sid) - - return tag + def get_after_SASL_bodytag(self): + return BOSHBody( + attrs={ 'to': self.bosh_to, + 'sid': self.bosh_sid, + 'xml:lang': self.bosh_xml_lang, + 'xmpp:version': '1.0', + 'xmpp:restart': 'true', + 'xmlns:xmpp': 'urn:xmpp:xbosh'}) def get_closing_bodytag(self): - closing_bodytag = self.get_bodytag() - closing_bodytag.setAttr('type', 'terminate') - return closing_bodytag + return BOSHBody(attrs={'sid': self.bosh_sid, 'type': 'terminate'}) + + def get_rid(self): + self.bosh_rid = self.bosh_rid + 1 + return str(self.bosh_rid) - def boshify_stanza(self, stanza=None, body_attrs=None): - ''' wraps stanza by body tag with rid and sid ''' - #log.info('boshify_staza - type is: %s, stanza is %s' % (type(stanza), stanza)) - tag = self.get_bodytag() - tag.setPayload([stanza]) - return tag + def get_http_socket(self): + s = NonBlockingHTTP( + raise_event=self.raise_event, + on_disconnect=self.disconnect, + idlequeue = self.idlequeue, + on_http_request_possible = self.on_http_request_possible, + http_uri = self.bosh_host, + http_port = self.bosh_port, + http_version = self.http_version) + if self.current_recv_handler: + s.onreceive(self.current_recv_handler) + return s + def onreceive(self, recv_handler): + if recv_handler is None: + recv_handler = self._owner.Dispatcher.ProcessNonBlocking + self.current_recv_handler = recv_handler + for s in self.http_socks: + s.onreceive(recv_handler) - def on_bodytag_attrs(self, body_attrs): - #log.info('on_bodytag_attrs: %s' % body_attrs) - if body_attrs.has_key('type'): - if body_attrs['type']=='terminated': - # BOSH session terminated - self.bosh_session_on = False - elif body_attrs['type']=='error': - # recoverable error - pass - if not self.bosh_sid: - # initial response - when bosh_sid is set - self.bosh_session_on = True - self.bosh_sid = body_attrs['sid'] - self.Dispatcher.Stream._document_attrs['id']=body_attrs['authid'] + def disconnect(self, do_callback=True): + if self.state == DISCONNECTED: return + + for s in self.http_socks: + s.disconnect(do_callback=False) + NonBlockingTransport.disconnect(self, do_callback) diff --git a/src/common/xmpp/client.py b/src/common/xmpp/client.py index f9f200dac..52c6bc2f2 100644 --- a/src/common/xmpp/client.py +++ b/src/common/xmpp/client.py @@ -48,7 +48,7 @@ class PlugIn: else: owner.__dict__[self.__class__.__name__]=self - # following will not work for classes inheriting plugin() + # following commented line will not work for classes inheriting plugin() #if self.__class__.__dict__.has_key('plugin'): return self.plugin(owner) if hasattr(self,'plugin'): return self.plugin(owner) diff --git a/src/common/xmpp/client_nb.py b/src/common/xmpp/client_nb.py index c21437de5..d22905580 100644 --- a/src/common/xmpp/client_nb.py +++ b/src/common/xmpp/client_nb.py @@ -23,7 +23,7 @@ These classes can be used for simple applications "AS IS" though. import socket -import transports_nb, tls_nb, dispatcher_nb, auth_nb, roster_nb, protocol +import transports_nb, tls_nb, dispatcher_nb, auth_nb, roster_nb, protocol, bosh from client import * import logging @@ -49,7 +49,7 @@ class NBCommonClient: self.Server = domain - # caller is who initiated this client, it is sed to register the EventDispatcher + # caller is who initiated this client, it is needed to register the EventDispatcher self._caller = caller self._owner = self self._registered_name = None @@ -92,16 +92,8 @@ class NBCommonClient: self.NonBlockingTCP.PlugOut() if self.__dict__.has_key('NonBlockingHTTP'): self.NonBlockingHTTP.PlugOut() - - - def send(self, stanza, now = False): - ''' interface for putting stanzas on wire. Puts ID to stanza if needed and - sends it via socket wrapper''' - (id, stanza_to_send) = self.Dispatcher.assign_id(stanza) - - self.Connection.send(stanza_to_send, now = now) - return id - + if self.__dict__.has_key('NonBlockingBOSH'): + self.NonBlockingBOSH.PlugOut() def connect(self, on_connect, on_connect_failure, hostname=None, port=5222, @@ -177,7 +169,7 @@ class NBCommonClient: started, and _on_connect_failure on failure. ''' #FIXME: use RegisterHandlerOnce instead of onreceive - log.info('========xmpp_connect_machine() >> mode: %s, data: %s' % (mode,str(data)[:20] )) + log.info('-------------xmpp_connect_machine() >> mode: %s, data: %s' % (mode,str(data)[:20] )) def on_next_receive(mode): log.info('setting %s on next receive' % mode) @@ -187,7 +179,8 @@ class NBCommonClient: self.onreceive(lambda _data:self._xmpp_connect_machine(mode, _data)) if not mode: - dispatcher_nb.Dispatcher().PlugIn(self) + # starting state + d=dispatcher_nb.Dispatcher().PlugIn(self) on_next_receive('RECEIVE_DOCUMENT_ATTRIBUTES') elif mode == 'FAILURE': @@ -205,7 +198,7 @@ class NBCommonClient: if not self.Dispatcher.Stream.features: on_next_receive('RECEIVE_STREAM_FEATURES') else: - log.info('got STREAM FEATURES in first read') + log.info('got STREAM FEATURES in first recv') self._xmpp_connect_machine(mode='STREAM_STARTED') else: @@ -222,7 +215,7 @@ class NBCommonClient: mode='FAILURE', data='Missing in 1.0 stream') else: - log.info('got STREAM FEATURES in second read') + log.info('got STREAM FEATURES in second recv') self._xmpp_connect_machine(mode='STREAM_STARTED') elif mode == 'STREAM_STARTED': @@ -244,7 +237,7 @@ class NBCommonClient: self.on_connect(self, self.connected) def raise_event(self, event_type, data): - log.info('raising event from transport: %s %s' % (event_type,data)) + log.info('raising event from transport: :::::%s::::\n_____________\n%s\n_____________\n' % (event_type,data)) if hasattr(self, 'Dispatcher'): self.Dispatcher.Event('', event_type, data) @@ -272,8 +265,9 @@ class NBCommonClient: ''' get the ip address of the account, from which is made connection to the server , (e.g. me). We will create listening socket on the same ip ''' - if hasattr(self, 'Connection'): - return self.Connection._sock.getsockname() + # FIXME: tuple (ip, port) is expected (and checked for) but port num is useless + if hasattr(self, 'socket'): + return self.socket.peerhost def auth(self, user, password, resource = '', sasl = 1, on_auth = None): @@ -364,6 +358,7 @@ class NonBlockingClient(NBCommonClient): def __init__(self, domain, idlequeue, caller=None): NBCommonClient.__init__(self, domain, idlequeue, caller) + self.protocol_type = 'XMPP' def connect(self, on_connect, on_connect_failure, hostname=None, port=5222, on_proxy_failure=None, proxy=None, secure=None): @@ -379,35 +374,33 @@ class NonBlockingClient(NBCommonClient): if proxy: # with proxies, client connects to proxy instead of directly to # XMPP server ((hostname, port)) - # tcp_host is machine used for socket connection - tcp_host=proxy['host'] - tcp_port=proxy['port'] + # tcp_host is hostname of machine used for socket connection + # (DNS request will be done for this hostname) + tcp_host, tcp_port, proxy_user, proxy_pass = \ + transports_nb.get_proxy_data_from_dict(proxy) + self._on_tcp_failure = self.on_proxy_failure - if proxy.has_key('type'): - assert(proxy['type']!='bosh') - if proxy.has_key('user') and proxy.has_key('pass'): - proxy_creds=(proxy['user'],proxy['pass']) - else: - proxy_creds=(None, None) - - type_ = proxy['type'] - if type_ == 'socks5': - # SOCKS5 proxy - self.socket = transports_nb.NBSOCKS5ProxySocket( + + if proxy['type'] == 'bosh': + self.socket = bosh.NonBlockingBOSH( on_disconnect=self.on_disconnect, - proxy_creds=proxy_creds, - xmpp_server=(xmpp_hostname, self.Port)) - elif type_ == 'http': - # HTTP CONNECT to proxy - self.socket = transports_nb.NBHTTPProxySocket( - on_disconnect=self.on_disconnect, - proxy_creds=proxy_creds, - xmpp_server=(xmpp_hostname, self.Port)) + raise_event = self.raise_event, + idlequeue = self.idlequeue, + xmpp_server=(xmpp_hostname, self.Port), + domain = self.Server, + bosh_dict = proxy) + self.protocol_type = 'BOSH' + else: - # HTTP CONNECT to proxy from environment variables - self.socket = transports_nb.NBHTTPProxySocket( + if proxy['type'] == 'socks5': + proxy_class = transports_nb.NBSOCKS5ProxySocket + elif proxy['type'] == 'http': + proxy_class = transports_nb.NBHTTPProxySocket + self.socket = proxy_class( on_disconnect=self.on_disconnect, - proxy_creds=(None, None), + raise_event = self.raise_event, + idlequeue = self.idlequeue, + proxy_creds=(proxy_user, proxy_pass), xmpp_server=(xmpp_hostname, self.Port)) else: self._on_tcp_failure = self._on_connect_failure @@ -415,6 +408,7 @@ class NonBlockingClient(NBCommonClient): tcp_port=self.Port self.socket = transports_nb.NonBlockingTCP( raise_event = self.raise_event, + idlequeue = self.idlequeue, on_disconnect = self.on_disconnect) self.socket.PlugIn(self) diff --git a/src/common/xmpp/dispatcher_nb.py b/src/common/xmpp/dispatcher_nb.py index 1c6f04a8b..818ae2790 100644 --- a/src/common/xmpp/dispatcher_nb.py +++ b/src/common/xmpp/dispatcher_nb.py @@ -42,8 +42,6 @@ XML_DECLARATION = '' # FIXME: ugly -from client_nb import NonBlockingClient -from bosh import BOSHClient class Dispatcher(): # Why is this here - I needed to redefine Dispatcher for BOSH and easiest way # was to inherit original Dispatcher (now renamed to XMPPDispatcher). Trouble @@ -53,9 +51,9 @@ class Dispatcher(): # If having two kinds of dispatcher will go well, I will rewrite the def PlugIn(self, client_obj, after_SASL=False): - if isinstance(client_obj, NonBlockingClient): + if client_obj.protocol_type == 'XMPP': XMPPDispatcher().PlugIn(client_obj) - elif isinstance(client_obj, BOSHClient): + elif client_obj.protocol_type == 'BOSH': BOSHDispatcher().PlugIn(client_obj, after_SASL) @@ -76,8 +74,8 @@ class XMPPDispatcher(PlugIn): self._exported_methods=[self.RegisterHandler, self.RegisterDefaultHandler, \ self.RegisterEventHandler, self.UnregisterCycleHandler, self.RegisterCycleHandler, \ self.RegisterHandlerOnce, self.UnregisterHandler, self.RegisterProtocol, \ - self.SendAndWaitForResponse, self.assign_id, self.StreamTerminate, \ - self.SendAndCallForResponse, self.getAnID, self.Event] + self.SendAndWaitForResponse, self.StreamTerminate, \ + self.SendAndCallForResponse, self.getAnID, self.Event, self.send] def getAnID(self): global ID @@ -112,10 +110,7 @@ class XMPPDispatcher(PlugIn): self._owner.lastErrNode = None self._owner.lastErr = None self._owner.lastErrCode = None - if hasattr(self._owner, 'StreamInit'): - self._owner.StreamInit() - else: - self.StreamInit() + self.StreamInit() def plugout(self): ''' Prepares instance to be destructed. ''' @@ -165,6 +160,7 @@ class XMPPDispatcher(PlugIn): self.Stream.Parse(data) # end stream:stream tag received if self.Stream and self.Stream.has_received_endtag(): + # FIXME call client method self._owner.Connection.disconnect() return 0 except ExpatError: @@ -414,25 +410,19 @@ class XMPPDispatcher(PlugIn): ''' Put stanza on the wire and call back when recipient replies. Additional callback arguments can be specified in args. ''' self.SendAndWaitForResponse(stanza, 0, func, args) - - def assign_id(self, stanza): - ''' Assign an unique ID to stanza and return assigned ID.''' - if type(stanza) in [type(''), type(u'')]: - return (None, stanza) - if not isinstance(stanza, Protocol): - _ID=None - elif not stanza.getID(): - global ID - ID+=1 - _ID=`ID` - stanza.setID(_ID) - else: - _ID=stanza.getID() - if self._owner._registered_name and not stanza.getAttr('from'): - stanza.setAttr('from', self._owner._registered_name) - stanza.setNamespace(self._owner.Namespace) - stanza.setParent(self._metastream) - return (_ID, stanza) + + def send(self, stanza, now=False): + id = None + if type(stanza) not in [type(''), type(u'')]: + if isinstance(stanza, Protocol): + id = stanza.getID() + if id is None: + stanza.setID(self.getAnID()) + id = stanza.getID() + if self._owner._registered_name and not stanza.getAttr('from'): + stanza.setAttr('from', self._owner._registered_name) + self._owner.Connection.send(stanza, now) + return id class BOSHDispatcher(XMPPDispatcher): @@ -458,12 +448,16 @@ class BOSHDispatcher(XMPPDispatcher): locale.getdefaultlocale()[0].split('_')[0]) self.restart = True - self._owner.Connection.send(self._owner.get_initial_bodytag(self.after_SASL)) + if self.after_SASL: + self._owner.Connection.send_http(self._owner.Connection.get_after_SASL_bodytag()) + else: + self._owner.Connection.send_http(self._owner.Connection.get_initial_bodytag()) + def StreamTerminate(self): ''' Send a stream terminator. ''' - self._owner.Connection.send(self._owner.get_closing_bodytag()) + self._owner.Connection.send_http(self._owner.Connection.get_closing_bodytag()) def ProcessNonBlocking(self, data=None): @@ -478,10 +472,31 @@ class BOSHDispatcher(XMPPDispatcher): def dispatch(self, stanza, session=None, direct=0): if stanza.getName()=='body' and stanza.getNamespace()==NS_HTTP_BIND: - self._owner.on_bodytag_attrs(stanza.getAttrs()) - #self._owner.send_empty_bodytag() - for child in stanza.getChildren(): - XMPPDispatcher.dispatch(self, child, session, direct) + + stanza_attrs = stanza.getAttrs() + + if stanza_attrs.has_key('authid'): + # should be only in init response + # auth module expects id of stream in document attributes + self.Stream._document_attrs['id'] = stanza_attrs['authid'] + + if stanza_attrs.has_key('sid'): + # session ID should be only in init response + self._owner.Connection.bosh_sid = stanza_attrs['sid'] + + if stanza_attrs.has_key('terminate'): + # staznas under body still should be passed to XMPP dispatcher + self._owner.on_disconnect() + + if stanza_attrs.has_key('error'): + # recoverable error + pass + + children = stanza.getChildren() + + if children: + for child in children: + XMPPDispatcher.dispatch(self, child, session, direct) else: XMPPDispatcher.dispatch(self, stanza, session, direct) diff --git a/src/common/xmpp/idlequeue.py b/src/common/xmpp/idlequeue.py index 2ca1b0bd3..66b40299f 100644 --- a/src/common/xmpp/idlequeue.py +++ b/src/common/xmpp/idlequeue.py @@ -15,6 +15,7 @@ import select import logging log = logging.getLogger('gajim.c.x.idlequeue') +log.setLevel(logging.DEBUG) class IdleObject: ''' base class for all idle listeners, these are the methods, which are called from IdleQueue @@ -36,7 +37,7 @@ class IdleObject: pass def read_timeout(self): - ''' called when timeout has happend ''' + ''' called when timeout happened ''' pass class IdleQueue: @@ -55,7 +56,8 @@ class IdleQueue: self.selector = select.poll() def remove_timeout(self, fd): - log.debug('read timeout removed for fd %s' % fd) + #log.debug('read timeout removed for fd %s' % fd) + print 'read timeout removed for fd %s' % fd if self.read_timeouts.has_key(fd): del(self.read_timeouts[fd]) @@ -71,11 +73,13 @@ class IdleQueue: def set_read_timeout(self, fd, seconds): ''' set a new timeout, if it is not removed after 'seconds', then obj.read_timeout() will be called ''' - log.debug('read timeout set for fd %s on %s seconds' % (fd, seconds)) + #log.debug('read timeout set for fd %s on %s seconds' % (fd, seconds)) + print 'read timeout set for fd %s on %s seconds' % (fd, seconds) timeout = self.current_time() + seconds self.read_timeouts[fd] = timeout def check_time_events(self): + print 'check time evs' current_time = self.current_time() for fd, timeout in self.read_timeouts.items(): if timeout > current_time: @@ -134,6 +138,7 @@ class IdleQueue: return False if flags & 3: # waiting read event + #print 'waiting read on %d, flags are %d' % (fd, flags) obj.pollin() return True diff --git a/src/common/xmpp/protocol.py b/src/common/xmpp/protocol.py index 797a8e9af..c80870a33 100644 --- a/src/common/xmpp/protocol.py +++ b/src/common/xmpp/protocol.py @@ -300,6 +300,13 @@ class JID: """ Produce hash of the JID, Allows to use JID objects as keys of the dictionary. """ return hash(self.__str__()) +class BOSHBody(Node): + ''' + tag that wraps usual XMPP stanzas in XMPP over BOSH + ''' + def __init__(self, attrs={}, payload=[], node=None): + Node.__init__(self, tag='body', attrs=attrs, payload=payload, node=node) + self.setNamespace(NS_HTTP_BIND) class Protocol(Node): @@ -400,13 +407,6 @@ class Protocol(Node): if item in ['to','from']: val=JID(val) return self.setAttr(item,val) -class BOSHBody(Protocol): - ''' - tag that wraps usual XMPP stanzas in BOSH - ''' - def __init__(self, to=None, frm=None, attrs={}, payload=[], node=None): - Protocol.__init__(self, name='body', to=to, frm=frm, attrs=attrs, - payload=payload, xmlns=NS_HTTP_BIND, node=node) class Message(Protocol): """ XMPP Message stanza - "push" mechanism.""" diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py index a4e35656f..e30a8aa90 100644 --- a/src/common/xmpp/transports_nb.py +++ b/src/common/xmpp/transports_nb.py @@ -3,7 +3,7 @@ ## ## Copyright (C) 2003-2004 Alexey "Snake" Nezhdanov ## modified by Dimitur Kirov -## modified by Dimitur Kirov +## modified by Tomas Karasek ## ## 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 @@ -45,6 +45,34 @@ def urisplit(uri): proto, host, path = grouped[1], grouped[3], grouped[4] return proto, host, path +def get_proxy_data_from_dict(proxy): + type = proxy['type'] + # with http-connect/socks5 proxy, we do tcp connecting to the proxy machine + tcp_host, tcp_port = proxy['host'], proxy['port'] + if type == 'bosh': + # in ['host'] is whole URI + tcp_host = urisplit(proxy['host'])[1] + # in BOSH, client connects to Connection Manager instead of directly to + # XMPP server ((hostname, port)). If HTTP Proxy is specified, client connects + # to HTTP proxy and Connection Manager is specified at URI and Host header + # in HTTP message + if proxy.has_key('proxy_host') and proxy.has_key('proxy_port'): + tcp_host, tcp_port = proxy['proxy_host'], proxy['proxy_port'] + + # user and pass for socks5/http_connect proxy. In case of BOSH, it's user and + # pass for http proxy - If there's no proxy_host they won't be used + if proxy.has_key('user'): + proxy_user = proxy['user'] + else: + proxy_user = None + if proxy.has_key('pass'): + proxy_pass = proxy['pass'] + else: + proxy_pass = None + return tcp_host, tcp_port, proxy_user, proxy_pass + + + # timeout to connect to the server socket, it doesn't include auth CONNECT_TIMEOUT_SECONDS = 30 @@ -63,62 +91,72 @@ DATA_SENT='DATA SENT' DISCONNECTED ='DISCONNECTED' CONNECTING ='CONNECTING' CONNECTED ='CONNECTED' -DISCONNECTING ='DISCONNECTING' - - +# transports have different constructor and same connect class NonBlockingTransport(PlugIn): - def __init__(self, raise_event, on_disconnect): + def __init__(self, raise_event, on_disconnect, idlequeue): PlugIn.__init__(self) self.raise_event = raise_event self.on_disconnect = on_disconnect self.on_connect = None self.on_connect_failure = None - self.idlequeue = None + self.idlequeue = idlequeue self.on_receive = None self.server = None self.port = None self.state = DISCONNECTED - self._exported_methods=[self.disconnect, self.onreceive] + self._exported_methods=[self.disconnect, self.onreceive, self.set_send_timeout, + self.set_timeout, self.remove_timeout] + + # time to wait for SOME stanza to come and then send keepalive + self.sendtimeout = 0 + + # in case we want to something different than sending keepalives + self.on_timeout = None def plugin(self, owner): owner.Connection=self - self.idlequeue = owner.idlequeue - def plugout(self): self._owner.Connection = None self._owner = None def connect(self, conn_5tuple, on_connect, on_connect_failure): + ''' + connect method should have the same declaration in all derived transports + + ''' + assert(self.state == DISCONNECTED) self.on_connect = on_connect self.on_connect_failure = on_connect_failure (self.server, self.port) = conn_5tuple[4][:2] - log.info('NonBlocking Connect :: About tot connect to %s:%s' % (self.server, self.port)) + self.conn_5tuple = conn_5tuple + log.info('NonBlocking Connect :: About to connect to %s:%s' % (self.server, self.port)) def set_state(self, newstate): - assert(newstate in [DISCONNECTED, CONNECTING, CONNECTED, DISCONNECTING]) - if (self.state, newstate) in [(CONNECTING, DISCONNECTING), (DISCONNECTED, DISCONNECTING)]: - log.info('strange move: %s -> %s' % (self.state, newstate)) + assert(newstate in [DISCONNECTED, CONNECTING, CONNECTED]) self.state = newstate def _on_connect(self, data): ''' preceeds call of on_connect callback ''' + # data is reference to socket wrapper instance. We don't need it in client + # because + self.peerhost = data._sock.getsockname() self.set_state(CONNECTED) self.on_connect() def _on_connect_failure(self,err_message): ''' preceeds call of on_connect_failure callback ''' - # In case of error while connecting we need to close socket + # In case of error while connecting we need to disconnect transport # but we don't want to call DisconnectHandlers from client, # thus the do_callback=False self.disconnect(do_callback=False) self.on_connect_failure(err_message=err_message) def send(self, raw_data, now=False): - if self.state not in [CONNECTED, DISCONNECTING]: + if self.state not in [CONNECTED]: # FIXME better handling needed log.error('Trying to send %s when transport is %s.' % (raw_data, self.state)) @@ -139,24 +177,49 @@ class NonBlockingTransport(PlugIn): else: self.on_receive = None return - log.info('setting onreceive on %s' % recv_handler) self.on_receive = recv_handler def tcp_connection_started(self): self.set_state(CONNECTING) # on_connect/on_conn_failure will be called from self.pollin/self.pollout + def read_timeout(self): + if self.on_timeout: + self.on_timeout() + self.renew_send_timeout() + + def renew_send_timeout(self): + if self.on_timeout and self.sendtimeout > 0: + self.set_timeout(self.sendtimeout) + else: + self.remove_timeout() + + def set_timeout(self, timeout): + self.idlequeue.set_read_timeout(self.get_fd(), timeout) + + def get_fd(self): + pass + + def remove_timeout(self): + self.idlequeue.remove_timeout(self.get_fd()) + + def set_send_timeout(self, timeout, on_timeout): + self.sendtimeout = timeout + if self.sendtimeout > 0: + self.on_timeout = on_timeout + else: + self.on_timeout = None class NonBlockingTCP(NonBlockingTransport, IdleObject): ''' Non-blocking TCP socket wrapper ''' - def __init__(self, raise_event, on_disconnect): + def __init__(self, raise_event, on_disconnect, idlequeue): ''' Class constructor. ''' - NonBlockingTransport.__init__(self, raise_event, on_disconnect) + NonBlockingTransport.__init__(self, raise_event, on_disconnect, idlequeue) # writable, readable - keep state of the last pluged flags # This prevents replug of same object with the same flags self.writable = True @@ -165,23 +228,16 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): # queue with messages to be send self.sendqueue = [] - # time to wait for SOME stanza to come and then send keepalive - self.sendtimeout = 0 - - # in case we want to something different than sending keepalives - self.on_timeout = None - # bytes remained from the last send message self.sendbuff = '' - self._exported_methods=[self.disconnect, self.onreceive, self.set_send_timeout, - self.set_timeout, self.remove_timeout] def get_fd(self): try: tmp = self._sock.fileno() return tmp - except: + except socket.error, (errnum, errstr): + log.error('Trying to get file descriptor of not-connected socket: %s' % errstr ) return 0 def connect(self, conn_5tuple, on_connect, on_connect_failure): @@ -205,6 +261,7 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): self._recv = self._sock.recv self.fd = self._sock.fileno() self.idlequeue.plug_idle(self, True, False) + self.peerhost = None errnum = 0 ''' variable for errno symbol that will be found from exception raised from connect() ''' @@ -221,11 +278,11 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): if errnum in (errno.EINPROGRESS, errno.EALREADY, errno.EWOULDBLOCK): # connecting in progress - log.info('After connect. "%s" raised => CONNECTING' % errstr) + log.info('After NB connect() of %s. "%s" raised => CONNECTING' % (id(self),errstr)) self.tcp_connection_started() return elif errnum in (0, 10056, errno.EISCONN): - # already connected - this branch is very unlikely, nonblocking connect() will + # already connected - this branch is probably useless, nonblocking connect() will # return EINPROGRESS exception in most cases. When here, we don't need timeout # on connected descriptor and success callback can be called. log.info('After connect. "%s" raised => CONNECTED' % errstr) @@ -240,6 +297,7 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): def _on_connect(self, data): ''' with TCP socket, we have to remove send-timeout ''' self.idlequeue.remove_timeout(self.get_fd()) + NonBlockingTransport._on_connect(self, data) @@ -253,6 +311,7 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): log.info('pollout called, state == %s' % self.state) if self.state==CONNECTING: + log.info('%s socket wrapper connected' % id(self)) self._on_connect(self) return self._do_send() @@ -288,30 +347,17 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): self._on_connect_failure('Error during connect to %s:%s' % (self.server, self.port)) else: - if self.on_timeout: - self.on_timeout() - self.renew_send_timeout() + NonBlockingTransport.read_timeout(self) - def renew_send_timeout(self): - if self.on_timeout and self.sendtimeout > 0: - self.set_timeout(self.sendtimeout) - else: - self.remove_timeout() - def set_send_timeout(self, timeout, on_timeout): - self.sendtimeout = timeout - if self.sendtimeout > 0: - self.on_timeout = on_timeout - else: - self.on_timeout = None def set_timeout(self, timeout): if self.state in [CONNECTING, CONNECTED] and self.get_fd() > 0: - self.idlequeue.set_read_timeout(self.get_fd(), timeout) + NonBlockingTransport.set_timeout(self, timeout) def remove_timeout(self): if self.get_fd(): - self.idlequeue.remove_timeout(self.get_fd()) + NonBlockingTransport.remove_timeout(self) def send(self, raw_data, now=False): '''Append raw_data to the queue of messages to be send. @@ -415,46 +461,50 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): else: # This should never happen, so we need the debug. (If there is no handler # on receive spacified, data are passed to Dispatcher.ProcessNonBlocking) - log.error('SOCKET Unhandled data received: %s' % received) + log.error('SOCKET %s Unhandled data received: %s' % (id(self), received)) + import traceback + traceback.print_stack() self.disconnect() def _on_receive(self,data): - '''Preceeds passing received data to Client class. Gets rid of HTTP headers - and checks them.''' + ''' preceeds on_receive callback. It peels off and checks HTTP headers in + class, in here it just calls the callback.''' self.on_receive(data) - class NonBlockingHTTP(NonBlockingTCP): ''' Socket wrapper that cretes HTTP message out of sent data and peels-off HTTP headers from incoming messages ''' - def __init__(self, raise_event, on_disconnect, http_uri, http_port, http_version=None): + def __init__(self, raise_event, on_disconnect, idlequeue, on_http_request_possible, + http_uri, http_port, http_version='HTTP/1.1'): + self.http_protocol, self.http_host, self.http_path = urisplit(http_uri) if self.http_protocol is None: self.http_protocol = 'http' if self.http_path == '': http_path = '/' self.http_port = http_port - if http_version: - self.http_version = http_version - else: - self.http_version = 'HTTP/1.1' + self.http_version = http_version # buffer for partial responses self.recvbuff = '' self.expected_length = 0 - NonBlockingTCP.__init__(self, raise_event, on_disconnect) + self.pending_requests = 0 + self.on_http_request_possible = on_http_request_possible + NonBlockingTCP.__init__(self, raise_event, on_disconnect, idlequeue) def send(self, raw_data, now=False): NonBlockingTCP.send( self, self.build_http_message(raw_data), now) + self.pending_requests += 1 + def _on_receive(self,data): - '''Preceeds passing received data to Client class. Gets rid of HTTP headers + '''Preceeds passing received data to owner class. Gets rid of HTTP headers and checks them.''' if not self.recvbuff: # recvbuff empty - fresh HTTP message was received @@ -470,7 +520,8 @@ class NonBlockingHTTP(NonBlockingTCP): if self.expected_length > len(self.recvbuff): # If we haven't received the whole HTTP mess yet, let's end the thread. - # It will be finnished from one of following poll calls on plugged socket. + # It will be finnished from one of following polls (io_watch) on plugged socket. + log.info('not enough bytes - %d expected, %d got' % (self.expected_length, len(self.recvbuff))) return # FIXME the reassembling doesn't work - Connection Manager on jabbim.cz @@ -481,8 +532,13 @@ class NonBlockingHTTP(NonBlockingTCP): self.recvbuff='' self.expected_length=0 + self.pending_requests -= 1 + assert(self.pending_requests >= 0) + # not-persistent connections + self.disconnect(do_callback = False) self.on_receive(httpbody) - + self.on_http_request_possible() + def build_http_message(self, httpbody, method='POST'): ''' @@ -512,7 +568,7 @@ class NonBlockingHTTP(NonBlockingTCP): message = message.replace('\r','') (header, httpbody) = message.split('\n\n',1) header = header.split('\n') - statusline = header[0].split(' ') + statusline = header[0].split(' ',2) header = header[1:] headers = {} for dummy in header: @@ -521,16 +577,16 @@ class NonBlockingHTTP(NonBlockingTCP): return (statusline, headers, httpbody) - class NBProxySocket(NonBlockingTCP): ''' Interface for proxy socket wrappers - when tunnneling XMPP over proxies, some connecting process usually has to be done before opening stream. ''' - def __init__(self, raise_event, on_disconnect, xmpp_server, proxy_creds=(None,None)): + def __init__(self, raise_event, on_disconnect, idlequeue, xmpp_server, + proxy_creds=(None,None)): self.proxy_user, self.proxy_pass = proxy_creds self.xmpp_server = xmpp_server - NonBlockingTCP.__init__(self, raise_event, on_disconnect) + NonBlockingTCP.__init__(self, raise_event, on_disconnect, idlequeue) def connect(self, conn_5tuple, on_connect, on_connect_failure): @@ -552,7 +608,6 @@ class NBProxySocket(NonBlockingTCP): pass - class NBHTTPProxySocket(NBProxySocket): ''' This class can be used instead of NonBlockingTCP HTTP (CONNECT) proxy connection class. Allows to use HTTP proxies like squid with diff --git a/test/test_nonblockingtcp.py b/test/test_nonblockingtcp.py index 7987d3278..cca0b0a26 100644 --- a/test/test_nonblockingtcp.py +++ b/test/test_nonblockingtcp.py @@ -1,5 +1,5 @@ ''' -Unit test for NonBlockingTcp tranport. +Unit test for NonBlockingTCP tranport. ''' import unittest @@ -38,7 +38,7 @@ class MockClient(IdleMock): IdleMock.__init__(self) def do_connect(self): - self.socket=transports_nb.NonBlockingTcp( + self.socket=transports_nb.NonBlockingTCP( on_disconnect=lambda: self.on_success(mode='SocketDisconnect') ) @@ -73,7 +73,7 @@ class MockClient(IdleMock): -class TestNonBlockingTcp(unittest.TestCase): +class TestNonBlockingTCP(unittest.TestCase): def setUp(self): self.idlequeue_thread = IdleQueueThread() self.idlequeue_thread.start() @@ -100,6 +100,6 @@ class TestNonBlockingTcp(unittest.TestCase): if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(TestNonBlockingTcp) + suite = unittest.TestLoader().loadTestsFromTestCase(TestNonBlockingTCP) unittest.TextTestRunner(verbosity=2).run(suite) From cecce21c53ea34240bf633ff2f00affba0e02189 Mon Sep 17 00:00:00 2001 From: tomk Date: Sun, 13 Jul 2008 23:10:11 +0000 Subject: [PATCH 11/20] added hack for Openfire that doesn\'t add xmlns to child iqs of sent body stanzas --- src/common/xmpp/dispatcher_nb.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/common/xmpp/dispatcher_nb.py b/src/common/xmpp/dispatcher_nb.py index 818ae2790..ee6c929ec 100644 --- a/src/common/xmpp/dispatcher_nb.py +++ b/src/common/xmpp/dispatcher_nb.py @@ -380,6 +380,7 @@ class XMPPDispatcher(PlugIn): if not res: return self._owner.remove_timeout() + print self._expected if self._expected[self._witid] is None: return if self.on_responses.has_key(self._witid): @@ -471,6 +472,7 @@ class BOSHDispatcher(XMPPDispatcher): return XMPPDispatcher.ProcessNonBlocking(self, data) def dispatch(self, stanza, session=None, direct=0): + if stanza.getName()=='body' and stanza.getNamespace()==NS_HTTP_BIND: stanza_attrs = stanza.getAttrs() @@ -496,6 +498,8 @@ class BOSHDispatcher(XMPPDispatcher): if children: for child in children: + if child.getNamespace() == NS_HTTP_BIND: + child.setNamespace(self._owner.defaultNamespace) XMPPDispatcher.dispatch(self, child, session, direct) else: XMPPDispatcher.dispatch(self, stanza, session, direct) From a58618c8430a4fd46d5ef78837c879915bbdd010 Mon Sep 17 00:00:00 2001 From: tomk Date: Fri, 18 Jul 2008 00:34:49 +0000 Subject: [PATCH 12/20] persistent HTTP connections in BOSH roughly implemented, added hack for openfire incapability of after-SASL-stream-restart-response in BOSH, changed doubles quotes to single --- src/common/connection.py | 1 + src/common/xmpp/auth_nb.py | 121 ++++---------- src/common/xmpp/bosh.py | 77 +++++---- src/common/xmpp/client.py | 10 +- src/common/xmpp/client_nb.py | 23 ++- src/common/xmpp/debug.py | 22 +-- src/common/xmpp/dispatcher_nb.py | 24 ++- src/common/xmpp/features_nb.py | 52 +++--- src/common/xmpp/idlequeue.py | 8 +- src/common/xmpp/protocol.py | 272 +++++++++++++++---------------- src/common/xmpp/roster_nb.py | 66 ++++---- src/common/xmpp/simplexml.py | 144 ++++++++-------- src/common/xmpp/tls_nb.py | 4 +- src/common/xmpp/transports_nb.py | 143 ++++++++-------- src/gajim.py | 3 +- 15 files changed, 471 insertions(+), 499 deletions(-) diff --git a/src/common/connection.py b/src/common/connection.py index 36e7719b6..08d7bcaae 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -550,6 +550,7 @@ class Connection(ConnectionHandlers): self._proxy['bosh_hold'] = '1' self._proxy['bosh_wait'] = '60' self._proxy['bosh_content'] = 'text/xml; charset=utf-8' + self._proxy['wait_for_restart_response'] = False log.info('Connecting to %s: [%s:%d]', self.name, diff --git a/src/common/xmpp/auth_nb.py b/src/common/xmpp/auth_nb.py index d8c045e0d..783bfbc24 100644 --- a/src/common/xmpp/auth_nb.py +++ b/src/common/xmpp/auth_nb.py @@ -143,10 +143,10 @@ class SASL(PlugIn): if "DIGEST-MD5" in mecs: node=Node('auth',attrs={'xmlns': NS_SASL, 'mechanism': 'DIGEST-MD5'}) elif "PLAIN" in mecs: - sasl_data='%s\x00%s\x00%s' % (self.username+'@' + self._owner.Server, - self.username, self.password) + sasl_data='%s\x00%s\x00%s' % (self.username+'@' + self._owner.Server, + self.username, self.password) node=Node('auth', attrs={'xmlns':NS_SASL,'mechanism':'PLAIN'}, - payload=[base64.encodestring(sasl_data).replace('\n','')]) + payload=[base64.encodestring(sasl_data).replace('\n','')]) else: self.startsasl='failure' log.error('I can only use DIGEST-MD5 and PLAIN mecanisms.') @@ -173,12 +173,19 @@ class SASL(PlugIn): self.startsasl='success' log.info('Successfully authenticated with remote server.') handlers=self._owner.Dispatcher.dumpHandlers() + + # save old features. They will be used in case we won't get response on + # stream restart after SASL auth (happens with XMPP over BOSH with Openfire) + old_features = self._owner.Dispatcher.Stream.features + self._owner.Dispatcher.PlugOut() - dispatcher_nb.Dispatcher().PlugIn(self._owner, after_SASL=True) + dispatcher_nb.Dispatcher().PlugIn(self._owner, after_SASL=True, + old_features=old_features) + self._owner.Dispatcher.restoreHandlers(handlers) self._owner.User = self.username if self.on_sasl : - self.on_sasl () + self.on_sasl() raise NodeProcessed ########################################3333 incoming_data = challenge.getData() @@ -232,7 +239,7 @@ class SASL(PlugIn): class NonBlockingNonSASL(PlugIn): ''' Implements old Non-SASL (JEP-0078) authentication used - in jabberd1.4 and transport authentication. + in jabberd1.4 and transport authentication. ''' def __init__(self, user, password, resource, on_auth): ''' Caches username, password and resource for auth. ''' @@ -331,16 +338,6 @@ class NonBlockingBind(PlugIn): PlugIn.__init__(self) self.bound=None - def FeaturesHandler(self,conn,feats): - """ Determine if server supports resource binding and set some internal attributes accordingly. """ - if not feats.getTag('bind',namespace=NS_BIND): - self.bound='failure' - log.error('Server does not requested binding.') - return - if feats.getTag('session',namespace=NS_SESSION): self.session=1 - else: self.session=-1 - self.bound=[] - def plugin(self, owner): ''' Start resource binding, if allowed at this time. Used internally. ''' if self._owner.Dispatcher.Stream.features: @@ -348,7 +345,23 @@ class NonBlockingBind(PlugIn): self.FeaturesHandler(self._owner.Dispatcher, self._owner.Dispatcher.Stream.features) except NodeProcessed: pass - else: self._owner.RegisterHandler('features', self.FeaturesHandler, xmlns=NS_STREAMS) + else: + self._owner.RegisterHandler('features', self.FeaturesHandler, xmlns=NS_STREAMS) + + def FeaturesHandler(self,conn,feats): + ''' Determine if server supports resource binding and set some internal attributes accordingly. ''' + if not feats.getTag('bind',namespace=NS_BIND): + log.error('Server does not requested binding.') + # we try to bind resource anyway + #self.bound='failure' + self.bound=[] + return + if feats.getTag('session',namespace=NS_SESSION): + self.session=1 + else: + self.session=-1 + self.bound=[] + def plugout(self): ''' Remove Bind handler from owner's dispatcher. Used internally. ''' @@ -404,77 +417,3 @@ class NonBlockingBind(PlugIn): self.session = 0 self.on_bound(None) -class NBComponentBind(PlugIn): - ''' ComponentBind some JID to the current connection to allow - router know of our location. - ''' - def __init__(self): - PlugIn.__init__(self) - self.bound=None - self.needsUnregister=None - - def plugin(self,owner): - ''' Start resource binding, if allowed at this time. Used internally. ''' - if self._owner.Dispatcher.Stream.features: - try: - self.FeaturesHandler(self._owner.Dispatcher, self._owner.Dispatcher.Stream.features) - except NodeProcessed: - pass - else: - self._owner.RegisterHandler('features', self.FeaturesHandler, xmlns=NS_STREAMS) - self.needsUnregister = 1 - - def plugout(self): - ''' Remove ComponentBind handler from owner's dispatcher. Used internally. ''' - if self.needsUnregister: - self._owner.UnregisterHandler('features', self.FeaturesHandler, xmlns=NS_STREAMS) - - def Bind(self, domain = None, on_bind = None): - ''' Perform binding. Use provided domain name (if not provided). ''' - self._owner.onreceive(self._on_bound) - self.on_bind = on_bind - - def _on_bound(self, resp): - if data: - self.Dispatcher.ProcessNonBlocking(data) - if self.bound is None: - return - self._owner.onreceive(None) - self._owner.SendAndWaitForResponse( - Protocol('bind', attrs={'name':domain}, xmlns=NS_COMPONENT_1), - func=self._on_bind_reponse) - - def _on_bind_reponse(self, res): - if resp and resp.getAttr('error'): - log.error('Binding failed: %s.' % resp.getAttr('error')) - elif resp: - log.info('Successfully bound.') - if self.on_bind: - self.on_bind('ok') - else: - log.error('Binding failed: timeout expired.') - if self.on_bind: - self.on_bind(None) - - def FeaturesHandler(self,conn,feats): - """ Determine if server supports resource binding and set some internal attributes accordingly. """ - if not feats.getTag('bind',namespace=NS_BIND): - self.bound='failure' - log.error('Server does not requested binding.') - return - if feats.getTag('session',namespace=NS_SESSION): self.session=1 - else: self.session=-1 - self.bound=[] - - def Bind(self,domain=None): - """ Perform binding. Use provided domain name (if not provided). """ - while self.bound is None and self._owner.Process(1): pass - resp=self._owner.SendAndWaitForResponse(Protocol('bind',attrs={'name':domain},xmlns=NS_COMPONENT_1)) - if resp and resp.getAttr('error'): - log.error('Binding failed: %s.'%resp.getAttr('error')) - elif resp: - log.info('Successfully bound.') - return 'ok' - else: - log.error('Binding failed: timeout expired.') - return '' diff --git a/src/common/xmpp/bosh.py b/src/common/xmpp/bosh.py index 3d53bdf40..4ce71cd68 100644 --- a/src/common/xmpp/bosh.py +++ b/src/common/xmpp/bosh.py @@ -24,9 +24,6 @@ class NonBlockingBOSH(NonBlockingTransport): # (see http://www.xmpp.org/extensions/xep-0124.html#rids) r = random.Random() r.seed() - global FAKE_DESCRIPTOR - FAKE_DESCRIPTOR = FAKE_DESCRIPTOR - 1 - self.fake_fd = FAKE_DESCRIPTOR self.bosh_rid = r.getrandbits(50) self.bosh_sid = None if locale.getdefaultlocale()[0]: @@ -35,7 +32,7 @@ class NonBlockingBOSH(NonBlockingTransport): self.bosh_xml_lang = 'en' self.http_version = 'HTTP/1.1' - self.http_persistent = False + self.http_persistent = True self.http_pipelining = False self.bosh_to = domain @@ -57,33 +54,38 @@ class NonBlockingBOSH(NonBlockingTransport): def connect(self, conn_5tuple, on_connect, on_connect_failure): NonBlockingTransport.connect(self, conn_5tuple, on_connect, on_connect_failure) + + global FAKE_DESCRIPTOR + FAKE_DESCRIPTOR = FAKE_DESCRIPTOR - 1 + self.fd = FAKE_DESCRIPTOR + + self.http_persistent = True self.http_socks.append(self.get_http_socket()) self.tcp_connection_started() # this connect() is not needed because sockets can be connected on send but # we need to know if host is reachable in order to invoke callback for - # failure when connecting (it's different than callback for errors + # connecting failurei eventually (it's different than callback for errors # occurring after connection is etabilished) - self.http_socks[0].connect( conn_5tuple = conn_5tuple, on_connect = lambda: self._on_connect(self.http_socks[0]), on_connect_failure = self._on_connect_failure) - - - def get_fd(self): - return self.fake_fd + def set_timeout(self, timeout): + if self.state in [CONNECTING, CONNECTED] and self.fd != -1: + NonBlockingTransport.set_timeout(self, timeout) + else: + log.warn('set_timeout: TIMEOUT NOT SET: state is %s, fd is %s' % (self.state, self.fd)) def on_http_request_possible(self): ''' Called after HTTP response is received - another request is possible. There should be always one pending request on BOSH CM. ''' - log.info('on_http_req possible, stanzas in list: %s, state:\n%s' % - (self.stanzas_to_send, self.get_current_state())) - # if one of sockets is connecting, sth is about to be sent - we don't have to - # send request from here + log.info('on_http_req possible state:\n%s' % self.get_current_state()) + # if one of sockets is connecting, sth is about to be sent + # if there is a pending request, we shouldn't send another one for s in self.http_socks: if s.state==CONNECTING or s.pending_requests>0: return self.flush_stanzas() @@ -96,19 +98,17 @@ class NonBlockingBOSH(NonBlockingTransport): tmp = self.prio_bosh_stanza self.prio_bosh_stanza = None else: - tmp = self.stanzas_to_send - self.stanzas_to_send = [] + if self.stanzas_to_send: + tmp = self.stanzas_to_send.pop(0) + else: + tmp = [] self.send_http(tmp) def send(self, stanza, now=False): # body tags should be send only via send_http() assert(not isinstance(stanza, BOSHBody)) - now = True - if now: - self.send_http([stanza]) - else: - self.stanzas_to_send.append(stanza) + self.send_http([stanza]) def send_http(self, payload): @@ -130,12 +130,16 @@ class NonBlockingBOSH(NonBlockingTransport): else: # no socket was picked but one is about to connect - save the stanza and # return + log.info('send_http: no free socket:\n%s' % self.get_current_state()) if self.prio_bosh_stanza: payload = self.merge_stanzas(payload, self.prio_bosh_stanza) if payload is None: - log.error('Error in BOSH socket handling - unable to send %s because %s\ - is already about to be sent' % (payload, self.prio_bosh_stanza)) - self.disconnect() + # if we cant merge the stanzas (both are BOSH ), add the current to + # queue to be sent later + self.stanzas_to_send.append(bosh_stanza) + log.warn('in BOSH send_http - unable to send %s because %s\ + is already about to be sent' % (str(payload), str(self.prio_bosh_stanza))) + return self.prio_bosh_stanza = payload def merge_stanzas(self, s1, s2): @@ -156,9 +160,11 @@ class NonBlockingBOSH(NonBlockingTransport): def get_current_state(self): - t = '\t\tSOCKET_ID\tSOCKET_STATE\tPENDING_REQS\n' + t = '------ SOCKET_ID\tSOCKET_STATE\tPENDING_REQS\n' for s in self.http_socks: - t = '%s\t\t%s\t%s\t%s\n' % (t,id(s), s.state, s.pending_requests) + t = '%s------ %s\t%s\t%s\n' % (t,id(s), s.state, s.pending_requests) + t = '%s------ prio stanza to send: %s, queued stanzas: %s' \ + % (t, self.prio_bosh_stanza, self.stanzas_to_send) return t @@ -181,9 +187,10 @@ class NonBlockingBOSH(NonBlockingTransport): return # being here means there are only CONNECTED scokets with pending requests. # Lets create and connect another one - s = self.get_http_socket() - self.http_socks.append(s) - self.connect_and_flush(s) + if len(self.http_socks) < 2: + s = self.get_http_socket() + self.http_socks.append(s) + self.connect_and_flush(s) return @@ -219,7 +226,6 @@ class NonBlockingBOSH(NonBlockingTransport): attrs={ 'to': self.bosh_to, 'sid': self.bosh_sid, 'xml:lang': self.bosh_xml_lang, - 'xmpp:version': '1.0', 'xmpp:restart': 'true', 'xmlns:xmpp': 'urn:xmpp:xbosh'}) @@ -239,7 +245,8 @@ class NonBlockingBOSH(NonBlockingTransport): on_http_request_possible = self.on_http_request_possible, http_uri = self.bosh_host, http_port = self.bosh_port, - http_version = self.http_version) + http_version = self.http_version, + http_persistent = self.http_persistent) if self.current_recv_handler: s.onreceive(self.current_recv_handler) return s @@ -251,9 +258,15 @@ class NonBlockingBOSH(NonBlockingTransport): for s in self.http_socks: s.onreceive(recv_handler) + def http_socket_disconnect(self, socket): + if self.http_persistent: + self.disconnect() + + + def disconnect(self, do_callback=True): if self.state == DISCONNECTED: return - + self.fd = -1 for s in self.http_socks: s.disconnect(do_callback=False) NonBlockingTransport.disconnect(self, do_callback) diff --git a/src/common/xmpp/client.py b/src/common/xmpp/client.py index 52c6bc2f2..5dd04b18e 100644 --- a/src/common/xmpp/client.py +++ b/src/common/xmpp/client.py @@ -14,23 +14,23 @@ # $Id: client.py,v 1.52 2006/01/02 19:40:55 normanr Exp $ -""" +''' Provides PlugIn class functionality to develop extentions for xmpppy. Also provides Client and Component classes implementations as the examples of xmpppy structures usage. These classes can be used for simple applications "AS IS" though. -""" +''' import logging log = logging.getLogger('gajim.c.x.plugin') class PlugIn: - """ Common xmpppy plugins infrastructure: plugging in/out, debugging. """ + ''' Common xmpppy plugins infrastructure: plugging in/out, debugging. ''' def __init__(self): self._exported_methods=[] def PlugIn(self,owner): - """ Attach to main instance and register ourself and all our staff in it. """ + ''' Attach to main instance and register ourself and all our staff in it. ''' self._owner=owner log.info('Plugging %s __INTO__ %s' % (self,self._owner)) if owner.__dict__.has_key(self.__class__.__name__): @@ -53,7 +53,7 @@ class PlugIn: if hasattr(self,'plugin'): return self.plugin(owner) def PlugOut(self): - """ Unregister all our staff from main instance and detach from it. """ + ''' Unregister all our staff from main instance and detach from it. ''' log.info('Plugging %s __OUT__ of %s.' % (self,self._owner)) for method in self._exported_methods: del self._owner.__dict__[method.__name__] for method in self._old_owners_methods: self._owner.__dict__[method.__name__]=method diff --git a/src/common/xmpp/client_nb.py b/src/common/xmpp/client_nb.py index d22905580..693f5e534 100644 --- a/src/common/xmpp/client_nb.py +++ b/src/common/xmpp/client_nb.py @@ -138,9 +138,9 @@ class NBCommonClient: def _try_next_ip(self, err_message=None): '''iterates over IP addresses from getaddinfo''' if err_message: - log.debug('While looping over DNS A records: %s' % connect) + log.debug('While looping over DNS A records: %s' % err_message) if self.ip_addresses == []: - self._on_tcp_failure(err_message='Run out of hosts for name %s:%s' % + self._on_tcp_failure('Run out of hosts for name %s:%s' % (self.Server, self.Port)) else: self.current_ip = self.ip_addresses.pop(0) @@ -237,7 +237,7 @@ class NBCommonClient: self.on_connect(self, self.connected) def raise_event(self, event_type, data): - log.info('raising event from transport: :::::%s::::\n_____________\n%s\n_____________\n' % (event_type,data)) + log.info('raising event from transport: >>>>>%s<<<<<\n_____________\n%s\n_____________\n' % (event_type,data)) if hasattr(self, 'Dispatcher'): self.Dispatcher.Event('', event_type, data) @@ -293,7 +293,7 @@ class NBCommonClient: self._Resource = 'xmpppy' auth_nb.NonBlockingNonSASL(self._User, self._Password, self._Resource, self._on_old_auth).PlugIn(self) return - self.onreceive(self._on_start_sasl) + #self.onreceive(self._on_start_sasl) self.SASL.auth() return True @@ -313,8 +313,18 @@ class NBCommonClient: self.SASL.PlugOut() elif self.SASL.startsasl == 'success': auth_nb.NonBlockingBind().PlugIn(self) - self.onreceive(self._on_auth_bind) - return True + if self.protocol_type == 'BOSH': + if self.wait_for_restart_response: + self.onreceive(self._on_auth_bind) + else: + self._on_auth_bind(None) + return + + elif self.protocol_type == 'XMPP': + auth_nb.NonBlockingBind().PlugIn(self) + self.onreceive(self._on_auth_bind) + return + return def _on_auth_bind(self, data): if data: @@ -390,6 +400,7 @@ class NonBlockingClient(NBCommonClient): domain = self.Server, bosh_dict = proxy) self.protocol_type = 'BOSH' + self.wait_for_restart_response = proxy['wait_for_restart_response'] else: if proxy['type'] == 'socks5': diff --git a/src/common/xmpp/debug.py b/src/common/xmpp/debug.py index 80ab6be9a..cd1ac595a 100644 --- a/src/common/xmpp/debug.py +++ b/src/common/xmpp/debug.py @@ -14,7 +14,7 @@ _version_ = '1.4.0' -"""\ +'''\ Generic debug class @@ -35,7 +35,7 @@ by the individual classes. For samples of usage, see samples subdir in distro source, and selftest in this code -""" +''' @@ -70,7 +70,7 @@ color_bright_cyan = chr(27) + "[36;1m" color_white = chr(27) + "[37;1m" -""" +''' Define your flags in yor modules like this: from debug import * @@ -99,7 +99,7 @@ DBG_MULTI = [ DBG_INIT, DBG_CONNECTION ] ------- To speed code up, typically for product releases or such use this class instead if you globaly want to disable debugging -""" +''' class NoDebug: @@ -214,7 +214,7 @@ class Debug: def show( self, msg, flag = None, prefix = None, sufix = None, lf = 0 ): - """ + ''' flag can be of folowing types: None - this msg will always be shown if any debugging is on flag - will be shown if flag is active @@ -225,7 +225,7 @@ class Debug: lf = -1 means strip linefeed if pressent lf = 1 means add linefeed if not pressent - """ + ''' if self.validate_flags: self._validate_flag( flag ) @@ -343,10 +343,10 @@ class Debug: def _as_one_list( self, items ): - """ init param might contain nested lists, typically from group flags. + ''' init param might contain nested lists, typically from group flags. This code organises lst and remves dupes - """ + ''' if type( items ) <> type( [] ) and type( items ) <> type( () ): return [ items ] r = [] @@ -363,7 +363,7 @@ class Debug: def _append_unique_str( self, lst, item ): - """filter out any dupes.""" + '''filter out any dupes.''' if type(item) <> type(''): msg2 = '%s' % item raise 'Invalid item type (should be string)',msg2 @@ -381,10 +381,10 @@ class Debug: raise 'Invalid debugflag given', msg2 def _remove_dupe_flags( self ): - """ + ''' if multiple instances of Debug is used in same app, some flags might be created multiple time, filter out dupes - """ + ''' unique_flags = [] for f in self.debug_flags: if f not in unique_flags: diff --git a/src/common/xmpp/dispatcher_nb.py b/src/common/xmpp/dispatcher_nb.py index ee6c929ec..09e6a2616 100644 --- a/src/common/xmpp/dispatcher_nb.py +++ b/src/common/xmpp/dispatcher_nb.py @@ -38,9 +38,6 @@ ID = 0 STREAM_TERMINATOR = '' XML_DECLARATION = '' - - - # FIXME: ugly class Dispatcher(): # Why is this here - I needed to redefine Dispatcher for BOSH and easiest way @@ -50,11 +47,11 @@ class Dispatcher(): # I wrote following to avoid changing each client.Dispatcher.whatever() in xmpp/ # If having two kinds of dispatcher will go well, I will rewrite the - def PlugIn(self, client_obj, after_SASL=False): + def PlugIn(self, client_obj, after_SASL=False, old_features=None): if client_obj.protocol_type == 'XMPP': XMPPDispatcher().PlugIn(client_obj) elif client_obj.protocol_type == 'BOSH': - BOSHDispatcher().PlugIn(client_obj, after_SASL) + BOSHDispatcher().PlugIn(client_obj, after_SASL, old_features) @@ -160,12 +157,11 @@ class XMPPDispatcher(PlugIn): self.Stream.Parse(data) # end stream:stream tag received if self.Stream and self.Stream.has_received_endtag(): - # FIXME call client method - self._owner.Connection.disconnect() + self._owner.disconnect() return 0 except ExpatError: log.error('Invalid XML received from server. Forcing disconnect.') - self._owner.Connection.disconnect() + self._owner.disconnect() return 0 if len(self._pendingExceptions) > 0: _pendingException = self._pendingExceptions.pop() @@ -380,7 +376,6 @@ class XMPPDispatcher(PlugIn): if not res: return self._owner.remove_timeout() - print self._expected if self._expected[self._witid] is None: return if self.on_responses.has_key(self._witid): @@ -427,7 +422,8 @@ class XMPPDispatcher(PlugIn): class BOSHDispatcher(XMPPDispatcher): - def PlugIn(self, owner, after_SASL=False): + def PlugIn(self, owner, after_SASL=False, old_features=None): + self.old_features = old_features self.after_SASL = after_SASL XMPPDispatcher.PlugIn(self, owner) @@ -437,7 +433,7 @@ class BOSHDispatcher(XMPPDispatcher): self.Stream.dispatch = self.dispatch self.Stream._dispatch_depth = 2 self.Stream.stream_header_received = self._check_stream_start - self.Stream.features = None + self.Stream.features = self.old_features self._metastream = Node('stream:stream') self._metastream.setNamespace(self._owner.Namespace) @@ -487,8 +483,7 @@ class BOSHDispatcher(XMPPDispatcher): self._owner.Connection.bosh_sid = stanza_attrs['sid'] if stanza_attrs.has_key('terminate'): - # staznas under body still should be passed to XMPP dispatcher - self._owner.on_disconnect() + self._owner.disconnect() if stanza_attrs.has_key('error'): # recoverable error @@ -498,6 +493,9 @@ class BOSHDispatcher(XMPPDispatcher): if children: for child in children: + # if child doesn't have any ns specified, simplexml (or expat) thinks it's + # of parent's (BOSH body) namespace, so we have to rewrite it to + # jabber:client if child.getNamespace() == NS_HTTP_BIND: child.setNamespace(self._owner.defaultNamespace) XMPPDispatcher.dispatch(self, child, session, direct) diff --git a/src/common/xmpp/features_nb.py b/src/common/xmpp/features_nb.py index dd46c7e4e..92f091abf 100644 --- a/src/common/xmpp/features_nb.py +++ b/src/common/xmpp/features_nb.py @@ -32,10 +32,10 @@ def _on_default_response(disp, iq, cb): disp.SendAndCallForResponse(iq, _on_response) def _discover(disp, ns, jid, node = None, fb2b=0, fb2a=1, cb=None): - """ Try to obtain info from the remote object. + ''' Try to obtain info from the remote object. If remote object doesn't support disco fall back to browse (if fb2b is true) and if it doesnt support browse (or fb2b is not true) fall back to agents protocol - (if gb2a is true). Returns obtained info. Used internally. """ + (if gb2a is true). Returns obtained info. Used internally. ''' iq=Iq(to=jid, typ='get', queryNS=ns) if node: iq.setQuerynode(node) @@ -61,11 +61,11 @@ def _discover(disp, ns, jid, node = None, fb2b=0, fb2a=1, cb=None): # this function is not used in gajim ??? def discoverItems(disp,jid,node=None, cb=None): - """ Query remote object about any items that it contains. Return items list. """ - """ According to JEP-0030: + ''' Query remote object about any items that it contains. Return items list. ''' + ''' According to JEP-0030: query MAY have node attribute item: MUST HAVE jid attribute and MAY HAVE name, node, action attributes. - action attribute of item can be either of remove or update value.""" + action attribute of item can be either of remove or update value.''' def _on_response(result_array): ret=[] for result in result_array: @@ -78,11 +78,11 @@ def discoverItems(disp,jid,node=None, cb=None): # this one is def discoverInfo(disp,jid,node=None, cb=None): - """ Query remote object about info that it publishes. Returns identities and features lists.""" - """ According to JEP-0030: + ''' Query remote object about info that it publishes. Returns identities and features lists.''' + ''' According to JEP-0030: query MAY have node attribute identity: MUST HAVE category and name attributes and MAY HAVE type attribute. - feature: MUST HAVE var attribute""" + feature: MUST HAVE var attribute''' def _on_response(result): identities , features = [] , [] for i in result: @@ -108,11 +108,11 @@ def discoverInfo(disp,jid,node=None, cb=None): ### Registration ### jabber:iq:register ### JEP-0077 ########################### def getRegInfo(disp, host, info={}, sync=True): - """ Gets registration form from remote host. + ''' Gets registration form from remote host. You can pre-fill the info dictionary. F.e. if you are requesting info on registering user joey than specify info as {'username':'joey'}. See JEP-0077 for details. - 'disp' must be connected dispatcher instance.""" + 'disp' must be connected dispatcher instance.''' iq=Iq('get',NS_REGISTER,to=host) for i in info.keys(): iq.setTagData(i,info[i]) @@ -144,11 +144,11 @@ def _ReceivedRegInfo(con, resp, agent): con.Event(NS_REGISTER, REGISTER_DATA_RECEIVED, (agent,df,False,'')) def register(disp, host, info, cb): - """ Perform registration on remote server with provided info. + ''' Perform registration on remote server with provided info. disp must be connected dispatcher instance. If registration fails you can get additional info from the dispatcher's owner attributes lastErrNode, lastErr and lastErrCode. - """ + ''' iq=Iq('set', NS_REGISTER, to=host) if not isinstance(info, dict): info=info.asDict() @@ -157,16 +157,16 @@ def register(disp, host, info, cb): disp.SendAndCallForResponse(iq, cb) def unregister(disp, host, cb): - """ Unregisters with host (permanently removes account). + ''' Unregisters with host (permanently removes account). disp must be connected and authorized dispatcher instance. - Returns true on success.""" + Returns true on success.''' iq = Iq('set', NS_REGISTER, to=host, payload=[Node('remove')]) _on_default_response(disp, iq, cb) def changePasswordTo(disp, newpassword, host=None, cb = None): - """ Changes password on specified or current (if not specified) server. + ''' Changes password on specified or current (if not specified) server. disp must be connected and authorized dispatcher instance. - Returns true on success.""" + Returns true on success.''' if not host: host=disp._owner.Server iq = Iq('set',NS_REGISTER,to=host, payload=[Node('username', payload=[disp._owner.Server]),Node('password',payload=[newpassword])]) @@ -177,8 +177,8 @@ def changePasswordTo(disp, newpassword, host=None, cb = None): #action=[allow|deny] def getPrivacyLists(disp): - """ Requests privacy lists from connected server. - Returns dictionary of existing lists on success.""" + ''' Requests privacy lists from connected server. + Returns dictionary of existing lists on success.''' iq = Iq('get', NS_PRIVACY) def _on_response(resp): dict = {'lists': []} @@ -209,8 +209,8 @@ def getActiveAndDefaultPrivacyLists(disp): disp.SendAndCallForResponse(iq, _on_response) def getPrivacyList(disp, listname): - """ Requests specific privacy list listname. Returns list of XML nodes (rules) - taken from the server responce.""" + ''' Requests specific privacy list listname. Returns list of XML nodes (rules) + taken from the server responce.''' def _on_response(resp): if not isResultNode(resp): disp.Event(NS_PRIVACY, PRIVACY_LIST_RECEIVED, (False)) @@ -220,8 +220,8 @@ def getPrivacyList(disp, listname): disp.SendAndCallForResponse(iq, _on_response) def setActivePrivacyList(disp, listname=None, typ='active', cb=None): - """ Switches privacy list 'listname' to specified type. - By default the type is 'active'. Returns true on success.""" + ''' Switches privacy list 'listname' to specified type. + By default the type is 'active'. Returns true on success.''' if listname: attrs={'name':listname} else: @@ -230,13 +230,13 @@ def setActivePrivacyList(disp, listname=None, typ='active', cb=None): _on_default_response(disp, iq, cb) def setDefaultPrivacyList(disp, listname=None): - """ Sets the default privacy list as 'listname'. Returns true on success.""" + ''' Sets the default privacy list as 'listname'. Returns true on success.''' return setActivePrivacyList(disp, listname,'default') def setPrivacyList(disp, listname, tags): - """ Set the ruleset. 'list' should be the simpleXML node formatted + ''' Set the ruleset. 'list' should be the simpleXML node formatted according to RFC 3921 (XMPP-IM) (I.e. Node('list',{'name':listname},payload=[...]) ) - Returns true on success.""" + Returns true on success.''' iq = Iq('set', NS_PRIVACY, xmlns = '') list_query = iq.getTag('query').setTag('list', {'name': listname}) for item in tags: @@ -252,6 +252,6 @@ def setPrivacyList(disp, listname, tags): _on_default_response(disp, iq, None) def delPrivacyList(disp,listname,cb=None): - """ Deletes privacy list 'listname'. Returns true on success.""" + ''' Deletes privacy list 'listname'. Returns true on success.''' iq = Iq('set',NS_PRIVACY,payload=[Node('list',{'name':listname})]) _on_default_response(disp, iq, cb) diff --git a/src/common/xmpp/idlequeue.py b/src/common/xmpp/idlequeue.py index 66b40299f..f358e45f5 100644 --- a/src/common/xmpp/idlequeue.py +++ b/src/common/xmpp/idlequeue.py @@ -56,8 +56,7 @@ class IdleQueue: self.selector = select.poll() def remove_timeout(self, fd): - #log.debug('read timeout removed for fd %s' % fd) - print 'read timeout removed for fd %s' % fd + log.info('read timeout removed for fd %s' % fd) if self.read_timeouts.has_key(fd): del(self.read_timeouts[fd]) @@ -73,13 +72,12 @@ class IdleQueue: def set_read_timeout(self, fd, seconds): ''' set a new timeout, if it is not removed after 'seconds', then obj.read_timeout() will be called ''' - #log.debug('read timeout set for fd %s on %s seconds' % (fd, seconds)) - print 'read timeout set for fd %s on %s seconds' % (fd, seconds) + log.info('read timeout set for fd %s on %s seconds' % (fd, seconds)) timeout = self.current_time() + seconds self.read_timeouts[fd] = timeout def check_time_events(self): - print 'check time evs' + log.info('check time evs') current_time = self.current_time() for fd, timeout in self.read_timeouts.items(): if timeout > current_time: diff --git a/src/common/xmpp/protocol.py b/src/common/xmpp/protocol.py index c80870a33..401c867d4 100644 --- a/src/common/xmpp/protocol.py +++ b/src/common/xmpp/protocol.py @@ -14,10 +14,10 @@ # $Id: protocol.py,v 1.52 2006/01/09 22:08:57 normanr Exp $ -""" +''' Protocol module contains tools that is needed for processing of xmpp-related data structures. -""" +''' from simplexml import Node,NodeBuilder,ustr import time @@ -112,7 +112,7 @@ NS_DATA_LAYOUT ='http://jabber.org/protocol/xdata-layout' # XEP-01 NS_DATA_VALIDATE='http://jabber.org/protocol/xdata-validate' # XEP-0122 NS_XMPP_STREAMS ='urn:ietf:params:xml:ns:xmpp-streams' -xmpp_stream_error_conditions=""" +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. conflict -- -- -- The server is closing the active stream for this entity because a new stream has been initiated that conflicts with the existing stream. @@ -136,8 +136,8 @@ undefined-condition -- -- -- The error condition is not one of those defined b unsupported-encoding -- -- -- The initiating entity has encoded the stream in an encoding that is not supported by the server. unsupported-stanza-type -- -- -- The initiating entity has sent a first-level child of the stream that is not supported by the server. unsupported-version -- -- -- The value of the 'version' attribute provided by the initiating entity in the stream header specifies a version of XMPP that is not supported by the server. -xml-not-well-formed -- -- -- The initiating entity has sent XML that is not well-formed.""" -xmpp_stanza_error_conditions=""" +xml-not-well-formed -- -- -- The initiating entity has sent XML that is not well-formed.''' +xmpp_stanza_error_conditions=''' bad-request -- 400 -- modify -- The sender has sent XML that is malformed or that cannot be processed. conflict -- 409 -- cancel -- Access cannot be granted because an existing resource or session exists with the same name or address. feature-not-implemented -- 501 -- cancel -- The feature requested is not implemented by the recipient or server and therefore cannot be processed. @@ -159,15 +159,15 @@ resource-constraint -- 500 -- wait -- The server or recipient lacks the system r service-unavailable -- 503 -- cancel -- The server or recipient does not currently provide the requested service. subscription-required -- 407 -- auth -- The requesting entity is not authorized to access the requested service because a subscription is required. undefined-condition -- 500 -- -- -unexpected-request -- 400 -- wait -- The recipient or server understood the request but was not expecting it at this time (e.g., the request was out of order).""" -sasl_error_conditions=""" +unexpected-request -- 400 -- wait -- The recipient or server understood the request but was not expecting it at this time (e.g., the request was out of order).''' +sasl_error_conditions=''' aborted -- -- -- The receiving entity acknowledges an element sent by the initiating entity; sent in reply to the element. incorrect-encoding -- -- -- The data provided by the initiating entity could not be processed because the [BASE64]Josefsson, S., The Base16, Base32, and Base64 Data Encodings, July 2003. encoding is incorrect (e.g., because the encoding does not adhere to the definition in Section 3 of [BASE64]Josefsson, S., The Base16, Base32, and Base64 Data Encodings, July 2003.); sent in reply to a element or an element with initial response data. invalid-authzid -- -- -- The authzid provided by the initiating entity is invalid, either because it is incorrectly formatted or because the initiating entity does not have permissions to authorize that ID; sent in reply to a element or an element with initial response data. invalid-mechanism -- -- -- The initiating entity did not provide a mechanism or requested a mechanism that is not supported by the receiving entity; sent in reply to an element. mechanism-too-weak -- -- -- The mechanism requested by the initiating entity is weaker than server policy permits for that initiating entity; sent in reply to a element or an element with initial response data. not-authorized -- -- -- The authentication failed because the initiating entity did not provide valid credentials (this includes but is not limited to the case of an unknown username); sent in reply to a element or an element with initial response data. -temporary-auth-failure -- -- -- The authentication failed because of a temporary error condition within the receiving entity; sent in reply to an element or element.""" +temporary-auth-failure -- -- -- The authentication failed because of a temporary error condition within the receiving entity; sent in reply to an element or element.''' ERRORS,_errorcodes={},{} for ns,errname,errpool in [(NS_XMPP_STREAMS,'STREAM',xmpp_stream_error_conditions), @@ -182,16 +182,16 @@ for ns,errname,errpool in [(NS_XMPP_STREAMS,'STREAM',xmpp_stream_error_condition del ns,errname,errpool,err,cond,code,typ,text def isResultNode(node): - """ Returns true if the node is a positive reply. """ + ''' Returns true if the node is a positive reply. ''' return node and node.getType()=='result' def isErrorNode(node): - """ Returns true if the node is a negative reply. """ + ''' Returns true if the node is a negative reply. ''' return node and node.getType()=='error' class NodeProcessed(Exception): - """ Exception that should be raised by handler when the handling should be stopped. """ + ''' Exception that should be raised by handler when the handling should be stopped. ''' class StreamError(Exception): - """ Base exception class for stream errors.""" + ''' Base exception class for stream errors.''' class BadFormat(StreamError): pass class BadNamespacePrefix(StreamError): pass class Conflict(StreamError): pass @@ -243,13 +243,13 @@ stream_exceptions = {'bad-format': BadFormat, 'xml-not-well-formed': XMLNotWellFormed} class JID: - """ JID object. JID can be built from string, modified, compared, serialised into string. """ + ''' JID object. JID can be built from string, modified, compared, serialised into string. ''' def __init__(self, jid=None, node='', domain='', resource=''): - """ Constructor. JID can be specified as string (jid argument) or as separate parts. + ''' Constructor. JID can be specified as string (jid argument) or as separate parts. Examples: JID('node@domain/resource') JID(node='node',domain='domain.org') - """ + ''' if not jid and not domain: raise ValueError('JID must contain at least domain name') elif type(jid)==type(self): self.node,self.domain,self.resource=jid.node,jid.domain,jid.resource elif domain: self.node,self.domain,self.resource=node,domain,resource @@ -259,45 +259,45 @@ class JID: if jid.find('/')+1: self.domain,self.resource=jid.split('/',1) else: self.domain,self.resource=jid,'' def getNode(self): - """ Return the node part of the JID """ + ''' Return the node part of the JID ''' return self.node def setNode(self,node): - """ Set the node part of the JID to new value. Specify None to remove the node part.""" + ''' Set the node part of the JID to new value. Specify None to remove the node part.''' self.node=node.lower() def getDomain(self): - """ Return the domain part of the JID """ + ''' Return the domain part of the JID ''' return self.domain def setDomain(self,domain): - """ Set the domain part of the JID to new value.""" + ''' Set the domain part of the JID to new value.''' self.domain=domain.lower() def getResource(self): - """ Return the resource part of the JID """ + ''' Return the resource part of the JID ''' return self.resource def setResource(self,resource): - """ Set the resource part of the JID to new value. Specify None to remove the resource part.""" + ''' Set the resource part of the JID to new value. Specify None to remove the resource part.''' self.resource=resource def getStripped(self): - """ Return the bare representation of JID. I.e. string value w/o resource. """ + ''' Return the bare representation of JID. I.e. string value w/o resource. ''' return self.__str__(0) def __eq__(self, other): - """ Compare the JID to another instance or to string for equality. """ + ''' Compare the JID to another instance or to string for equality. ''' try: other=JID(other) except ValueError: return 0 return self.resource==other.resource and self.__str__(0) == other.__str__(0) def __ne__(self, other): - """ Compare the JID to another instance or to string for non-equality. """ + ''' Compare the JID to another instance or to string for non-equality. ''' return not self.__eq__(other) def bareMatch(self, other): - """ Compare the node and domain parts of the JID's for equality. """ + ''' Compare the node and domain parts of the JID's for equality. ''' return self.__str__(0) == JID(other).__str__(0) def __str__(self,wresource=1): - """ Serialise JID into string. """ + ''' Serialise JID into string. ''' if self.node: jid=self.node+'@'+self.domain else: jid=self.domain if wresource and self.resource: return jid+'/'+self.resource return jid def __hash__(self): - """ Produce hash of the JID, Allows to use JID objects as keys of the dictionary. """ + ''' Produce hash of the JID, Allows to use JID objects as keys of the dictionary. ''' return hash(self.__str__()) class BOSHBody(Node): @@ -310,16 +310,16 @@ class BOSHBody(Node): class Protocol(Node): - """ A "stanza" object class. Contains methods that are common for presences, iqs and messages. """ + ''' A "stanza" object class. Contains methods that are common for presences, iqs and messages. ''' def __init__(self, name=None, to=None, typ=None, frm=None, attrs={}, payload=[], timestamp=None, xmlns=None, node=None): - """ Constructor, name is the name of the stanza i.e. 'message' or 'presence' or 'iq'. + ''' Constructor, name is the name of the stanza i.e. 'message' or 'presence' or 'iq'. to is the value of 'to' attribure, 'typ' - 'type' attribute frn - from attribure, attrs - other attributes mapping, payload - same meaning as for simplexml payload definition timestamp - the time value that needs to be stamped over stanza xmlns - namespace of top stanza node node - parsed or unparsed stana to be taken as prototype. - """ + ''' if not attrs: attrs={} if to: attrs['to']=to if frm: attrs['from']=frm @@ -336,54 +336,54 @@ class Protocol(Node): except: pass if timestamp is not None: self.setTimestamp(timestamp) # To auto-timestamp stanza just pass timestamp='' def getTo(self): - """ Return value of the 'to' attribute. """ + ''' Return value of the 'to' attribute. ''' try: return self['to'] except: return None def getFrom(self): - """ Return value of the 'from' attribute. """ + ''' Return value of the 'from' attribute. ''' try: return self['from'] except: return None def getTimestamp(self): - """ Return the timestamp in the 'yyyymmddThhmmss' format. """ + ''' Return the timestamp in the 'yyyymmddThhmmss' format. ''' if self.timestamp: return self.timestamp return time.strftime('%Y%m%dT%H:%M:%S', time.gmtime()) def getID(self): - """ Return the value of the 'id' attribute. """ + ''' Return the value of the 'id' attribute. ''' return self.getAttr('id') def setTo(self,val): - """ Set the value of the 'to' attribute. """ + ''' Set the value of the 'to' attribute. ''' self.setAttr('to', JID(val)) def getType(self): - """ Return the value of the 'type' attribute. """ + ''' Return the value of the 'type' attribute. ''' return self.getAttr('type') def setFrom(self,val): - """ Set the value of the 'from' attribute. """ + ''' Set the value of the 'from' attribute. ''' self.setAttr('from', JID(val)) def setType(self,val): - """ Set the value of the 'type' attribute. """ + ''' Set the value of the 'type' attribute. ''' self.setAttr('type', val) def setID(self,val): - """ Set the value of the 'id' attribute. """ + ''' Set the value of the 'id' attribute. ''' self.setAttr('id', val) def getError(self): - """ Return the error-condition (if present) or the textual description of the error (otherwise). """ + ''' Return the error-condition (if present) or the textual description of the error (otherwise). ''' errtag=self.getTag('error') if errtag: for tag in errtag.getChildren(): if tag.getName()<>'text': return tag.getName() return errtag.getData() def getErrorMsg(self): - """ Return the textual description of the error (if present) or the error condition """ + ''' Return the textual description of the error (if present) or the error condition ''' errtag=self.getTag('error') if errtag: for tag in errtag.getChildren(): if tag.getName()=='text': return tag.getData() return self.getError() def getErrorCode(self): - """ Return the error code. Obsolete. """ + ''' Return the error code. Obsolete. ''' return self.getTagAttr('error','code') def setError(self,error,code=None): - """ Set the error code. Obsolete. Use error-conditions instead. """ + ''' Set the error code. Obsolete. Use error-conditions instead. ''' if code: if str(code) in _errorcodes.keys(): error=ErrorNode(_errorcodes[str(code)],text=error) else: error=ErrorNode(ERR_UNDEFINED_CONDITION,code=code,typ='cancel',text=error) @@ -391,40 +391,40 @@ class Protocol(Node): self.setType('error') self.addChild(node=error) def setTimestamp(self,val=None): - """Set the timestamp. timestamp should be the yyyymmddThhmmss string.""" + '''Set the timestamp. timestamp should be the yyyymmddThhmmss string.''' if not val: val=time.strftime('%Y%m%dT%H:%M:%S', time.gmtime()) self.timestamp=val self.setTag('x',{'stamp':self.timestamp},namespace=NS_DELAY) def getProperties(self): - """ Return the list of namespaces to which belongs the direct childs of element""" + ''' Return the list of namespaces to which belongs the direct childs of element''' props=[] for child in self.getChildren(): prop=child.getNamespace() if prop not in props: props.append(prop) return props def __setitem__(self,item,val): - """ Set the item 'item' to the value 'val'.""" + ''' Set the item 'item' to the value 'val'.''' if item in ['to','from']: val=JID(val) return self.setAttr(item,val) class Message(Protocol): - """ XMPP Message stanza - "push" mechanism.""" + ''' XMPP Message stanza - "push" mechanism.''' def __init__(self, to=None, body=None, xhtml=None, typ=None, subject=None, attrs={}, frm=None, payload=[], timestamp=None, xmlns=NS_CLIENT, node=None): - """ Create message object. You can specify recipient, text of message, type of message + ''' Create message object. You can specify recipient, text of message, type of message any additional attributes, sender of the message, any additional payload (f.e. jabber:x:delay element) and namespace in one go. - Alternatively you can pass in the other XML object as the 'node' parameted to replicate it as message. """ + Alternatively you can pass in the other XML object as the 'node' parameted to replicate it as message. ''' Protocol.__init__(self, 'message', to=to, typ=typ, attrs=attrs, frm=frm, payload=payload, timestamp=timestamp, xmlns=xmlns, node=node) if body: self.setBody(body) if xhtml: self.setXHTML(xhtml) if subject is not None: self.setSubject(subject) def getBody(self): - """ Returns text of the message. """ + ''' Returns text of the message. ''' return self.getTagData('body') def getXHTML(self, xmllang=None): - """ Returns serialized xhtml-im element text of the message. + ''' Returns serialized xhtml-im element text of the message. - TODO: Returning a DOM could make rendering faster.""" + TODO: Returning a DOM could make rendering faster.''' xhtml = self.getTag('html') if xhtml: if xmllang: @@ -434,18 +434,18 @@ class Message(Protocol): return str(body) return None def getSubject(self): - """ Returns subject of the message. """ + ''' Returns subject of the message. ''' return self.getTagData('subject') def getThread(self): - """ Returns thread of the message. """ + ''' Returns thread of the message. ''' return self.getTagData('thread') def setBody(self,val): - """ Sets the text of the message. """ + ''' Sets the text of the message. ''' self.setTagData('body',val) def setXHTML(self,val,xmllang=None): - """ Sets the xhtml text of the message (XEP-0071). - The parameter is the "inner html" to the body.""" + ''' Sets the xhtml text of the message (XEP-0071). + The parameter is the "inner html" to the body.''' try: if xmllang: dom = NodeBuilder('' + val + '').getDom() @@ -459,21 +459,21 @@ class Message(Protocol): print "Error", e pass #FIXME: log. we could not set xhtml (parse error, whatever) def setSubject(self,val): - """ Sets the subject of the message. """ + ''' Sets the subject of the message. ''' self.setTagData('subject',val) def setThread(self,val): - """ Sets the thread of the message. """ + ''' Sets the thread of the message. ''' self.setTagData('thread',val) def buildReply(self,text=None): - """ Builds and returns another message object with specified text. - The to, from and thread properties of new message are pre-set as reply to this message. """ + ''' Builds and returns another message object with specified text. + The to, from and thread properties of new message are pre-set as reply to this message. ''' m=Message(to=self.getFrom(),frm=self.getTo(),body=text,node=self) th=self.getThread() if th: m.setThread(th) return m def getStatusCode(self): - """Returns the status code of the message (for groupchat config - change)""" + '''Returns the status code of the message (for groupchat config + change)''' attrs = [] for xtag in self.getTags('x'): for child in xtag.getTags('status'): @@ -481,32 +481,32 @@ class Message(Protocol): return attrs class Presence(Protocol): - """ XMPP Presence object.""" + ''' XMPP Presence object.''' def __init__(self, to=None, typ=None, priority=None, show=None, status=None, attrs={}, frm=None, timestamp=None, payload=[], xmlns=NS_CLIENT, node=None): - """ Create presence object. You can specify recipient, type of message, priority, show and status values + ''' Create presence object. You can specify recipient, type of message, priority, show and status values any additional attributes, sender of the presence, timestamp, any additional payload (f.e. jabber:x:delay element) and namespace in one go. - Alternatively you can pass in the other XML object as the 'node' parameted to replicate it as presence. """ + Alternatively you can pass in the other XML object as the 'node' parameted to replicate it as presence. ''' Protocol.__init__(self, 'presence', to=to, typ=typ, attrs=attrs, frm=frm, payload=payload, timestamp=timestamp, xmlns=xmlns, node=node) if priority: self.setPriority(priority) if show: self.setShow(show) if status: self.setStatus(status) def getPriority(self): - """ Returns the priority of the message. """ + ''' Returns the priority of the message. ''' return self.getTagData('priority') def getShow(self): - """ Returns the show value of the message. """ + ''' Returns the show value of the message. ''' return self.getTagData('show') def getStatus(self): - """ Returns the status string of the message. """ + ''' Returns the status string of the message. ''' return self.getTagData('status') def setPriority(self,val): - """ Sets the priority of the message. """ + ''' Sets the priority of the message. ''' self.setTagData('priority',val) def setShow(self,val): - """ Sets the show value of the message. """ + ''' Sets the show value of the message. ''' self.setTagData('show',val) def setStatus(self,val): - """ Sets the status string of the message. """ + ''' Sets the status string of the message. ''' self.setTagData('status',val) def _muc_getItemAttr(self,tag,attr): @@ -520,25 +520,25 @@ class Presence(Protocol): return cchild.getData(),cchild.getAttr(attr) return None,None def getRole(self): - """Returns the presence role (for groupchat)""" + '''Returns the presence role (for groupchat)''' return self._muc_getItemAttr('item','role') def getAffiliation(self): - """Returns the presence affiliation (for groupchat)""" + '''Returns the presence affiliation (for groupchat)''' return self._muc_getItemAttr('item','affiliation') def getNewNick(self): - """Returns the status code of the presence (for groupchat)""" + '''Returns the status code of the presence (for groupchat)''' return self._muc_getItemAttr('item','nick') def getJid(self): - """Returns the presence jid (for groupchat)""" + '''Returns the presence jid (for groupchat)''' return self._muc_getItemAttr('item','jid') def getReason(self): - """Returns the reason of the presence (for groupchat)""" + '''Returns the reason of the presence (for groupchat)''' return self._muc_getSubTagDataAttr('reason','')[0] def getActor(self): - """Returns the reason of the presence (for groupchat)""" + '''Returns the reason of the presence (for groupchat)''' return self._muc_getSubTagDataAttr('actor','jid')[1] def getStatusCode(self): - """Returns the status code of the presence (for groupchat)""" + '''Returns the status code of the presence (for groupchat)''' attrs = [] for xtag in self.getTags('x'): for child in xtag.getTags('status'): @@ -546,53 +546,53 @@ class Presence(Protocol): return attrs class Iq(Protocol): - """ XMPP Iq object - get/set dialog mechanism. """ + ''' XMPP Iq object - get/set dialog mechanism. ''' def __init__(self, typ=None, queryNS=None, attrs={}, to=None, frm=None, payload=[], xmlns=NS_CLIENT, node=None): - """ Create Iq object. You can specify type, query namespace + ''' Create Iq object. You can specify type, query namespace any additional attributes, recipient of the iq, sender of the iq, any additional payload (f.e. jabber:x:data node) and namespace in one go. - Alternatively you can pass in the other XML object as the 'node' parameted to replicate it as an iq. """ + Alternatively you can pass in the other XML object as the 'node' parameted to replicate it as an iq. ''' Protocol.__init__(self, 'iq', to=to, typ=typ, attrs=attrs, frm=frm, xmlns=xmlns, node=node) if payload: self.setQueryPayload(payload) if queryNS: self.setQueryNS(queryNS) def getQueryNS(self): - """ Return the namespace of the 'query' child element.""" + ''' Return the namespace of the 'query' child element.''' tag=self.getTag('query') if tag: return tag.getNamespace() def getQuerynode(self): - """ Return the 'node' attribute value of the 'query' child element.""" + ''' Return the 'node' attribute value of the 'query' child element.''' return self.getTagAttr('query','node') def getQueryPayload(self): - """ Return the 'query' child element payload.""" + ''' Return the 'query' child element payload.''' tag=self.getTag('query') if tag: return tag.getPayload() def getQueryChildren(self): - """ Return the 'query' child element child nodes.""" + ''' Return the 'query' child element child nodes.''' tag=self.getTag('query') if tag: return tag.getChildren() def setQueryNS(self,namespace): - """ Set the namespace of the 'query' child element.""" + ''' Set the namespace of the 'query' child element.''' self.setTag('query').setNamespace(namespace) def setQueryPayload(self,payload): - """ Set the 'query' child element payload.""" + ''' Set the 'query' child element payload.''' self.setTag('query').setPayload(payload) def setQuerynode(self,node): - """ Set the 'node' attribute value of the 'query' child element.""" + ''' Set the 'node' attribute value of the 'query' child element.''' self.setTagAttr('query','node',node) def buildReply(self,typ): - """ Builds and returns another Iq object of specified type. - The to, from and query child node of new Iq are pre-set as reply to this Iq. """ + ''' Builds and returns another Iq object of specified type. + The to, from and query child node of new Iq are pre-set as reply to this Iq. ''' iq=Iq(typ,to=self.getFrom(),frm=self.getTo(),attrs={'id':self.getID()}) if self.getTag('query'): iq.setQueryNS(self.getQueryNS()) return iq class ErrorNode(Node): - """ XMPP-style error element. + ''' XMPP-style error element. In the case of stanza error should be attached to XMPP stanza. - In the case of stream-level errors should be used separately. """ + In the case of stream-level errors should be used separately. ''' def __init__(self,name,code=None,typ=None,text=None): - """ Create new error node object. + ''' Create new error node object. Mandatory parameter: name - name of error condition. - Optional parameters: code, typ, text. Used for backwards compartibility with older jabber protocol.""" + Optional parameters: code, typ, text. Used for backwards compartibility with older jabber protocol.''' if ERRORS.has_key(name): cod,type,txt=ERRORS[name] ns=name.split()[0] @@ -607,30 +607,30 @@ class ErrorNode(Node): if cod: self.setAttr('code',cod) class Error(Protocol): - """ Used to quickly transform received stanza into error reply.""" + ''' Used to quickly transform received stanza into error reply.''' def __init__(self,node,error,reply=1): - """ Create error reply basing on the received 'node' stanza and the 'error' error condition. + ''' Create error reply basing on the received 'node' stanza and the 'error' error condition. If the 'node' is not the received stanza but locally created ('to' and 'from' fields needs not swapping) - specify the 'reply' argument as false.""" + specify the 'reply' argument as false.''' if reply: Protocol.__init__(self,to=node.getFrom(),frm=node.getTo(),node=node) else: Protocol.__init__(self,node=node) self.setError(error) if node.getType()=='error': self.__str__=self.__dupstr__ def __dupstr__(self,dup1=None,dup2=None): - """ Dummy function used as preventor of creating error node in reply to error node. + ''' Dummy function used as preventor of creating error node in reply to error node. I.e. you will not be able to serialise "double" error into string. - """ + ''' return '' class DataField(Node): - """ This class is used in the DataForm class to describe the single data item. + ''' This class is used in the DataForm class to describe the single data item. If you are working with jabber:x:data (XEP-0004, XEP-0068, XEP-0122) - then you will need to work with instances of this class. """ + then you will need to work with instances of this class. ''' def __init__(self,name=None,value=None,typ=None,required=0,desc=None,options=[],node=None): - """ Create new data field of specified name,value and type. + ''' Create new data field of specified name,value and type. Also 'required','desc' and 'options' fields can be set. Alternatively other XML object can be passed in as the 'node' parameted to replicate it as a new datafiled. - """ + ''' Node.__init__(self,'field',node=node) if name: self.setVar(name) if type(value) in [list,tuple]: self.setValues(value) @@ -641,70 +641,70 @@ class DataField(Node): if desc: self.setDesc(desc) if options: self.setOptions(options) def setRequired(self,req=1): - """ Change the state of the 'required' flag. """ + ''' Change the state of the 'required' flag. ''' if req: self.setTag('required') else: try: self.delChild('required') except ValueError: return def isRequired(self): - """ Returns in this field a required one. """ + ''' Returns in this field a required one. ''' return self.getTag('required') def setDesc(self,desc): - """ Set the description of this field. """ + ''' Set the description of this field. ''' self.setTagData('desc',desc) def getDesc(self): - """ Return the description of this field. """ + ''' Return the description of this field. ''' return self.getTagData('desc') def setValue(self,val): - """ Set the value of this field. """ + ''' Set the value of this field. ''' self.setTagData('value',val) def getValue(self): return self.getTagData('value') def setValues(self,lst): - """ Set the values of this field as values-list. - Replaces all previous filed values! If you need to just add a value - use addValue method.""" + ''' Set the values of this field as values-list. + Replaces all previous filed values! If you need to just add a value - use addValue method.''' while self.getTag('value'): self.delChild('value') for val in lst: self.addValue(val) def addValue(self,val): - """ Add one more value to this field. Used in 'get' iq's or such.""" + ''' Add one more value to this field. Used in 'get' iq's or such.''' self.addChild('value',{},[val]) def getValues(self): - """ Return the list of values associated with this field.""" + ''' Return the list of values associated with this field.''' ret=[] for tag in self.getTags('value'): ret.append(tag.getData()) return ret def getOptions(self): - """ Return label-option pairs list associated with this field.""" + ''' Return label-option pairs list associated with this field.''' ret=[] for tag in self.getTags('option'): ret.append([tag.getAttr('label'),tag.getTagData('value')]) return ret def setOptions(self,lst): - """ Set label-option pairs list associated with this field.""" + ''' Set label-option pairs list associated with this field.''' while self.getTag('option'): self.delChild('option') for opt in lst: self.addOption(opt) def addOption(self,opt): - """ Add one more label-option pair to this field.""" + ''' Add one more label-option pair to this field.''' if type(opt) in [str,unicode]: self.addChild('option').setTagData('value',opt) else: self.addChild('option',{'label':opt[0]}).setTagData('value',opt[1]) def getType(self): - """ Get type of this field. """ + ''' Get type of this field. ''' return self.getAttr('type') def setType(self,val): - """ Set type of this field. """ + ''' Set type of this field. ''' return self.setAttr('type',val) def getVar(self): - """ Get 'var' attribute value of this field. """ + ''' Get 'var' attribute value of this field. ''' return self.getAttr('var') def setVar(self,val): - """ Set 'var' attribute value of this field. """ + ''' Set 'var' attribute value of this field. ''' return self.setAttr('var',val) class DataForm(Node): - """ DataForm class. Used for manipulating dataforms in XMPP. + ''' DataForm class. Used for manipulating dataforms in XMPP. Relevant XEPs: 0004, 0068, 0122. - Can be used in disco, pub-sub and many other applications.""" + Can be used in disco, pub-sub and many other applications.''' def __init__(self, typ=None, data=[], title=None, node=None): - """ + ''' Create new dataform of type 'typ'. 'data' is the list of DataField instances that this dataform contains, 'title' - the title string. You can specify the 'node' argument as the other node to be used as @@ -716,7 +716,7 @@ class DataForm(Node): 'typ' of reply iq can be ( 'result' | 'set' | 'set' | 'result' ) respectively. 'cancel' form can not contain any fields. All other forms contains AT LEAST one field. 'title' MAY be included in forms of type "form" and "result" - """ + ''' Node.__init__(self,'x',node=node) if node: newkids=[] @@ -736,36 +736,36 @@ class DataForm(Node): elif child.__class__.__name__=='DataField': self.kids.append(child) else: self.kids.append(DataField(node=child)) def getType(self): - """ Return the type of dataform. """ + ''' Return the type of dataform. ''' return self.getAttr('type') def setType(self,typ): - """ Set the type of dataform. """ + ''' Set the type of dataform. ''' self.setAttr('type',typ) def getTitle(self): - """ Return the title of dataform. """ + ''' Return the title of dataform. ''' return self.getTagData('title') def setTitle(self,text): - """ Set the title of dataform. """ + ''' Set the title of dataform. ''' self.setTagData('title',text) def getInstructions(self): - """ Return the instructions of dataform. """ + ''' Return the instructions of dataform. ''' return self.getTagData('instructions') def setInstructions(self,text): - """ Set the instructions of dataform. """ + ''' Set the instructions of dataform. ''' self.setTagData('instructions',text) def addInstructions(self,text): - """ Add one more instruction to the dataform. """ + ''' Add one more instruction to the dataform. ''' self.addChild('instructions',{},[text]) def getField(self,name): - """ Return the datafield object with name 'name' (if exists). """ + ''' Return the datafield object with name 'name' (if exists). ''' return self.getTag('field',attrs={'var':name}) def setField(self,name): - """ Create if nessessary or get the existing datafield object with name 'name' and return it. """ + ''' Create if nessessary or get the existing datafield object with name 'name' and return it. ''' f=self.getField(name) if f: return f return self.addChild(node=DataField(name)) def asDict(self): - """ Represent dataform as simple dictionary mapping of datafield names to their values.""" + ''' Represent dataform as simple dictionary mapping of datafield names to their values.''' ret={} for field in self.getTags('field'): name=field.getAttr('var') @@ -778,10 +778,10 @@ class DataForm(Node): if self.getTag('instructions'): ret['instructions']=self.getInstructions() return ret def __getitem__(self,name): - """ Simple dictionary interface for getting datafields values by their names.""" + ''' Simple dictionary interface for getting datafields values by their names.''' item=self.getField(name) if item: return item.getValue() raise IndexError('No such field') def __setitem__(self,name,val): - """ Simple dictionary interface for setting datafields values by their names.""" + ''' Simple dictionary interface for setting datafields values by their names.''' return self.setField(name).setValue(val) diff --git a/src/common/xmpp/roster_nb.py b/src/common/xmpp/roster_nb.py index a269880ba..4b6b70ca5 100644 --- a/src/common/xmpp/roster_nb.py +++ b/src/common/xmpp/roster_nb.py @@ -29,31 +29,31 @@ log = logging.getLogger('gajim.c.x.roster_nb') class NonBlockingRoster(PlugIn): - """ Defines a plenty of methods that will allow you to manage roster. + ''' Defines a plenty of methods that will allow you to manage roster. Also automatically track presences from remote JIDs taking into account that every JID can have multiple resources connected. Does not currently support 'error' presences. You can also use mapping interface for access to the internal representation of contacts in roster. - """ + ''' def __init__(self): - """ Init internal variables. """ + ''' Init internal variables. ''' PlugIn.__init__(self) self._data = {} self.set=None self._exported_methods=[self.getRoster] def Request(self,force=0): - """ Request roster from server if it were not yet requested - (or if the 'force' argument is set). """ + ''' Request roster from server if it were not yet requested + (or if the 'force' argument is set). ''' if self.set is None: self.set=0 elif not force: return self._owner.send(Iq('get',NS_ROSTER)) log.info('Roster requested from server') def RosterIqHandler(self,dis,stanza): - """ Subscription tracker. Used internally for setting items state in - internal roster representation. """ + ''' Subscription tracker. Used internally for setting items state in + internal roster representation. ''' sender = stanza.getAttr('from') if not sender is None and not sender.bareMatch( self._owner.User + '@' + self._owner.Server): @@ -75,8 +75,8 @@ class NonBlockingRoster(PlugIn): self.set=1 def PresenceHandler(self,dis,pres): - """ Presence tracker. Used internally for setting items' resources state in - internal roster representation. """ + ''' Presence tracker. Used internally for setting items' resources state in + internal roster representation. ''' jid=pres.getFrom() if not jid: # If no from attribue, it's from server @@ -100,11 +100,11 @@ class NonBlockingRoster(PlugIn): # Need to handle type='error' also def _getItemData(self,jid,dataname): - """ Return specific jid's representation in internal format. Used internally. """ + ''' Return specific jid's representation in internal format. Used internally. ''' jid=jid[:(jid+'/').find('/')] return self._data[jid][dataname] def _getResourceData(self,jid,dataname): - """ Return specific jid's resource representation in internal format. Used internally. """ + ''' Return specific jid's resource representation in internal format. Used internally. ''' if jid.find('/')+1: jid,resource=jid.split('/',1) if self._data[jid]['resources'].has_key(resource): return self._data[jid]['resources'][resource][dataname] @@ -114,40 +114,40 @@ class NonBlockingRoster(PlugIn): if int(self._data[jid]['resources'][r]['priority'])>lastpri: resource,lastpri=r,int(self._data[jid]['resources'][r]['priority']) return self._data[jid]['resources'][resource][dataname] def delItem(self,jid): - """ Delete contact 'jid' from roster.""" + ''' Delete contact 'jid' from roster.''' self._owner.send(Iq('set',NS_ROSTER,payload=[Node('item',{'jid':jid,'subscription':'remove'})])) def getAsk(self,jid): - """ Returns 'ask' value of contact 'jid'.""" + ''' Returns 'ask' value of contact 'jid'.''' return self._getItemData(jid,'ask') def getGroups(self,jid): - """ Returns groups list that contact 'jid' belongs to.""" + ''' Returns groups list that contact 'jid' belongs to.''' return self._getItemData(jid,'groups') def getName(self,jid): - """ Returns name of contact 'jid'.""" + ''' Returns name of contact 'jid'.''' return self._getItemData(jid,'name') def getPriority(self,jid): - """ Returns priority of contact 'jid'. 'jid' should be a full (not bare) JID.""" + ''' Returns priority of contact 'jid'. 'jid' should be a full (not bare) JID.''' return self._getResourceData(jid,'priority') def getRawRoster(self): - """ Returns roster representation in internal format. """ + ''' Returns roster representation in internal format. ''' return self._data def getRawItem(self,jid): - """ Returns roster item 'jid' representation in internal format. """ + ''' Returns roster item 'jid' representation in internal format. ''' return self._data[jid[:(jid+'/').find('/')]] def getShow(self, jid): - """ Returns 'show' value of contact 'jid'. 'jid' should be a full (not bare) JID.""" + ''' Returns 'show' value of contact 'jid'. 'jid' should be a full (not bare) JID.''' return self._getResourceData(jid,'show') def getStatus(self, jid): - """ Returns 'status' value of contact 'jid'. 'jid' should be a full (not bare) JID.""" + ''' Returns 'status' value of contact 'jid'. 'jid' should be a full (not bare) JID.''' return self._getResourceData(jid,'status') def getSubscription(self,jid): - """ Returns 'subscription' value of contact 'jid'.""" + ''' Returns 'subscription' value of contact 'jid'.''' return self._getItemData(jid,'subscription') def getResources(self,jid): - """ Returns list of connected resources of contact 'jid'.""" + ''' Returns list of connected resources of contact 'jid'.''' return self._data[jid[:(jid+'/').find('/')]]['resources'].keys() def setItem(self,jid,name=None,groups=[]): - """ Renames contact 'jid' and sets the groups list that it now belongs to.""" + ''' Renames contact 'jid' and sets the groups list that it now belongs to.''' iq=Iq('set',NS_ROSTER) query=iq.getTag('query') attrs={'jid':jid} @@ -156,32 +156,32 @@ class NonBlockingRoster(PlugIn): for group in groups: item.addChild(node=Node('group',payload=[group])) self._owner.send(iq) def getItems(self): - """ Return list of all [bare] JIDs that the roster is currently tracks.""" + ''' Return list of all [bare] JIDs that the roster is currently tracks.''' return self._data.keys() def keys(self): - """ Same as getItems. Provided for the sake of dictionary interface.""" + ''' Same as getItems. Provided for the sake of dictionary interface.''' return self._data.keys() def __getitem__(self,item): - """ Get the contact in the internal format. Raises KeyError if JID 'item' is not in roster.""" + ''' Get the contact in the internal format. Raises KeyError if JID 'item' is not in roster.''' return self._data[item] def getItem(self,item): - """ Get the contact in the internal format (or None if JID 'item' is not in roster).""" + ''' Get the contact in the internal format (or None if JID 'item' is not in roster).''' if self._data.has_key(item): return self._data[item] def Subscribe(self,jid): - """ Send subscription request to JID 'jid'.""" + ''' Send subscription request to JID 'jid'.''' self._owner.send(Presence(jid,'subscribe')) def Unsubscribe(self,jid): - """ Ask for removing our subscription for JID 'jid'.""" + ''' Ask for removing our subscription for JID 'jid'.''' self._owner.send(Presence(jid,'unsubscribe')) def Authorize(self,jid): - """ Authorise JID 'jid'. Works only if these JID requested auth previously. """ + ''' Authorise JID 'jid'. Works only if these JID requested auth previously. ''' self._owner.send(Presence(jid,'subscribed')) def Unauthorize(self,jid): - """ Unauthorise JID 'jid'. Use for declining authorisation request - or for removing existing authorization. """ + ''' Unauthorise JID 'jid'. Use for declining authorisation request + or for removing existing authorization. ''' self._owner.send(Presence(jid,'unsubscribed')) def getRaw(self): - """Returns the internal data representation of the roster.""" + '''Returns the internal data representation of the roster.''' return self._data # copypasted methods for roster.py from constructor to here diff --git a/src/common/xmpp/simplexml.py b/src/common/xmpp/simplexml.py index 84d5165fa..a22e0c46f 100644 --- a/src/common/xmpp/simplexml.py +++ b/src/common/xmpp/simplexml.py @@ -14,8 +14,8 @@ # $Id: simplexml.py,v 1.27 2005/04/30 07:20:27 snakeru Exp $ -"""Simplexml module provides xmpppy library with all needed tools to handle XML nodes and XML streams. -I'm personally using it in many other separate projects. It is designed to be as standalone as possible.""" +'''Simplexml module provides xmpppy library with all needed tools to handle XML nodes and XML streams. +I'm personally using it in many other separate projects. It is designed to be as standalone as possible.''' import xml.parsers.expat import logging @@ -23,13 +23,13 @@ log = logging.getLogger('gajim.c.x.simplexml') #log.setLevel(logging.DEBUG) def XMLescape(txt): - """Returns provided string with symbols & < > " replaced by their respective XML entities.""" + '''Returns provided string with symbols & < > " replaced by their respective XML entities.''' # replace also FORM FEED and ESC, because they are not valid XML chars return txt.replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """).replace(u'\x0C', "").replace(u'\x1B', "") ENCODING='utf-8' def ustr(what): - """Converts object "what" to unicode string using it's own __str__ method if accessible or unicode method otherwise.""" + '''Converts object "what" to unicode string using it's own __str__ method if accessible or unicode method otherwise.''' if type(what) == type(u''): return what try: r=what.__str__() except AttributeError: r=str(what) @@ -37,7 +37,7 @@ def ustr(what): return r class Node(object): - """ Node class describes syntax of separate XML Node. It have a constructor that permits node creation + ''' Node class describes syntax of separate XML Node. It have a constructor that permits node creation from set of "namespace name", attributes and payload of text strings and other nodes. It does not natively support building node from text string and uses NodeBuilder class for that purpose. After creation node can be mangled in many ways so it can be completely changed. @@ -50,16 +50,16 @@ class Node(object): info with the "original" node that is changing the one node may influence the other. Though it is rarely needed (in xmpppy it is never needed at all since I'm usually never using original node after replication (and using replication only to move upwards on the classes tree). - """ + ''' FORCE_NODE_RECREATION=0 def __init__(self, tag=None, attrs={}, payload=[], parent=None, node=None): - """ Takes "tag" argument as the name of node (prepended by namespace, if needed and separated from it + ''' Takes "tag" argument as the name of node (prepended by namespace, if needed and separated from it by a space), attrs dictionary as the set of arguments, payload list as the set of textual strings and child nodes that this node carries within itself and "parent" argument that is another node that this one will be the child of. Also the __init__ can be provided with "node" argument that is either a text string containing exactly one node or another Node instance to begin with. If both "node" and other arguments is provided then the node initially created as replica of "node" - provided and then modified to be compliant with other arguments.""" + provided and then modified to be compliant with other arguments.''' if node: if self.FORCE_NODE_RECREATION and isinstance(node, Node): node=str(node) @@ -83,8 +83,8 @@ class Node(object): else: self.data.append(ustr(i)) def __str__(self,fancy=0): - """ Method used to dump node into textual representation. - if "fancy" argument is set to True produces indented output for readability.""" + ''' Method used to dump node into textual representation. + if "fancy" argument is set to True produces indented output for readability.''' s = (fancy-1) * 2 * ' ' + "<" + self.name if self.namespace: if not self.parent or self.parent.namespace!=self.namespace: @@ -115,8 +115,8 @@ class Node(object): if fancy: s = s + "\n" return s def addChild(self, name=None, attrs={}, payload=[], namespace=None, node=None): - """ If "node" argument is provided, adds it as child node. Else creates new node from - the other arguments' values and adds it as well.""" + ''' If "node" argument is provided, adds it as child node. Else creates new node from + the other arguments' values and adds it as well.''' if namespace: name=namespace+' '+name if node: newnode=node @@ -125,46 +125,46 @@ class Node(object): self.kids.append(newnode) return newnode def addData(self, data): - """ Adds some CDATA to node. """ + ''' Adds some CDATA to node. ''' self.data.append(ustr(data)) def clearData(self): - """ Removes all CDATA from the node. """ + ''' Removes all CDATA from the node. ''' self.data=[] def delAttr(self, key): - """ Deletes an attribute "key" """ + ''' Deletes an attribute "key" ''' del self.attrs[key] def delChild(self, node, attrs={}): - """ Deletes the "node" from the node's childs list, if "node" is an instance. - Else deletes the first node that have specified name and (optionally) attributes. """ + ''' Deletes the "node" from the node's childs list, if "node" is an instance. + Else deletes the first node that have specified name and (optionally) attributes. ''' if not isinstance(node, Node): node=self.getTag(node,attrs) self.kids.remove(node) return node def getAttrs(self): - """ Returns all node's attributes as dictionary. """ + ''' Returns all node's attributes as dictionary. ''' return self.attrs def getAttr(self, key): - """ Returns value of specified attribute. """ + ''' Returns value of specified attribute. ''' try: return self.attrs[key] except: return None def getChildren(self): - """ Returns all node's child nodes as list. """ + ''' Returns all node's child nodes as list. ''' return self.kids def getData(self): - """ Returns all node CDATA as string (concatenated). """ + ''' Returns all node CDATA as string (concatenated). ''' return ''.join(self.data) def getName(self): - """ Returns the name of node """ + ''' Returns the name of node ''' return self.name def getNamespace(self): - """ Returns the namespace of node """ + ''' Returns the namespace of node ''' return self.namespace def getParent(self): - """ Returns the parent of node (if present). """ + ''' Returns the parent of node (if present). ''' return self.parent def getPayload(self): - """ Return the payload of node i.e. list of child nodes and CDATA entries. + ''' Return the payload of node i.e. list of child nodes and CDATA entries. F.e. for "text1 text2" will be returned list: - ['text1', , , ' text2']. """ + ['text1', , , ' text2']. ''' ret=[] for i in range(len(self.kids)+len(self.data)+1): try: @@ -174,20 +174,20 @@ class Node(object): except IndexError: pass return ret def getTag(self, name, attrs={}, namespace=None): - """ Filters all child nodes using specified arguments as filter. - Returns the first found or None if not found. """ + ''' Filters all child nodes using specified arguments as filter. + Returns the first found or None if not found. ''' return self.getTags(name, attrs, namespace, one=1) def getTagAttr(self,tag,attr): - """ Returns attribute value of the child with specified name (or None if no such attribute).""" + ''' Returns attribute value of the child with specified name (or None if no such attribute).''' try: return self.getTag(tag).attrs[attr] except: return None def getTagData(self,tag): - """ Returns cocatenated CDATA of the child with specified name.""" + ''' Returns cocatenated CDATA of the child with specified name.''' try: return self.getTag(tag).getData() except: return None def getTags(self, name, attrs={}, namespace=None, one=0): - """ Filters all child nodes using specified arguments as filter. - Returns the list of nodes found. """ + ''' Filters all child nodes using specified arguments as filter. + Returns the list of nodes found. ''' nodes=[] for node in self.kids: if namespace and namespace<>node.getNamespace(): continue @@ -199,7 +199,7 @@ class Node(object): if not one: return nodes def iterTags(self, name, attrs={}, namespace=None): - """ Iterate over all children using specified arguments as filter. """ + ''' Iterate over all children using specified arguments as filter. ''' for node in self.kids: if namespace is not None and namespace!=node.getNamespace(): continue if node.getName() == name: @@ -210,57 +210,57 @@ class Node(object): yield node def setAttr(self, key, val): - """ Sets attribute "key" with the value "val". """ + ''' Sets attribute "key" with the value "val". ''' self.attrs[key]=val def setData(self, data): - """ Sets node's CDATA to provided string. Resets all previous CDATA!""" + ''' Sets node's CDATA to provided string. Resets all previous CDATA!''' self.data=[ustr(data)] def setName(self,val): - """ Changes the node name. """ + ''' Changes the node name. ''' self.name = val def setNamespace(self, namespace): - """ Changes the node namespace. """ + ''' Changes the node namespace. ''' self.namespace=namespace def setParent(self, node): - """ Sets node's parent to "node". WARNING: do not checks if the parent already present - and not removes the node from the list of childs of previous parent. """ + ''' Sets node's parent to "node". WARNING: do not checks if the parent already present + and not removes the node from the list of childs of previous parent. ''' self.parent = node def setPayload(self,payload,add=0): - """ Sets node payload according to the list specified. WARNING: completely replaces all node's - previous content. If you wish just to add child or CDATA - use addData or addChild methods. """ + ''' Sets node payload according to the list specified. WARNING: completely replaces all node's + previous content. If you wish just to add child or CDATA - use addData or addChild methods. ''' if type(payload) in (type(''),type(u'')): payload=[payload] if add: self.kids+=payload else: self.kids=payload def setTag(self, name, attrs={}, namespace=None): - """ Same as getTag but if the node with specified namespace/attributes not found, creates such - node and returns it. """ + ''' Same as getTag but if the node with specified namespace/attributes not found, creates such + node and returns it. ''' node=self.getTags(name, attrs, namespace=namespace, one=1) if node: return node else: return self.addChild(name, attrs, namespace=namespace) def setTagAttr(self,tag,attr,val): - """ Creates new node (if not already present) with name "tag" - and sets it's attribute "attr" to value "val". """ + ''' Creates new node (if not already present) with name "tag" + and sets it's attribute "attr" to value "val". ''' try: self.getTag(tag).attrs[attr]=val except: self.addChild(tag,attrs={attr:val}) def setTagData(self,tag,val,attrs={}): - """ Creates new node (if not already present) with name "tag" and (optionally) attributes "attrs" - and sets it's CDATA to string "val". """ + ''' Creates new node (if not already present) with name "tag" and (optionally) attributes "attrs" + and sets it's CDATA to string "val". ''' try: self.getTag(tag,attrs).setData(ustr(val)) except: self.addChild(tag,attrs,payload=[ustr(val)]) def has_attr(self,key): - """ Checks if node have attribute "key".""" + ''' Checks if node have attribute "key".''' return self.attrs.has_key(key) def __getitem__(self,item): - """ Returns node's attribute "item" value. """ + ''' Returns node's attribute "item" value. ''' return self.getAttr(item) def __setitem__(self,item,val): - """ Sets node's attribute "item" value. """ + ''' Sets node's attribute "item" value. ''' return self.setAttr(item,val) def __delitem__(self,item): - """ Deletes node's attribute "item". """ + ''' Deletes node's attribute "item". ''' return self.delAttr(item) def __getattr__(self,attr): - """ Reduce memory usage caused by T/NT classes - use memory only when needed. """ + ''' Reduce memory usage caused by T/NT classes - use memory only when needed. ''' if attr=='T': self.T=T(self) return self.T @@ -270,7 +270,7 @@ class Node(object): raise AttributeError class T: - """ Auxiliary class used to quick access to node's child nodes. """ + ''' Auxiliary class used to quick access to node's child nodes. ''' def __init__(self,node): self.__dict__['node']=node def __getattr__(self,attr): return self.node.setTag(attr) def __setattr__(self,attr,val): @@ -279,25 +279,25 @@ class T: def __delattr__(self,attr): return self.node.delChild(attr) class NT(T): - """ Auxiliary class used to quick create node's child nodes. """ + ''' Auxiliary class used to quick create node's child nodes. ''' def __getattr__(self,attr): return self.node.addChild(attr) def __setattr__(self,attr,val): if isinstance(val,Node): self.node.addChild(attr,node=val) else: return self.node.addChild(attr,payload=[val]) class NodeBuilder: - """ Builds a Node class minidom from data parsed to it. This class used for two purposes: + ''' Builds a Node class minidom from data parsed to it. This class used for two purposes: 1. Creation an XML Node from a textual representation. F.e. reading a config file. See an XML2Node method. 2. Handling an incoming XML stream. This is done by mangling the __dispatch_depth parameter and redefining the dispatch method. - You do not need to use this class directly if you do not designing your own XML handler.""" + You do not need to use this class directly if you do not designing your own XML handler.''' def __init__(self,data=None,initial_node=None): - """ Takes two optional parameters: "data" and "initial_node". + ''' Takes two optional parameters: "data" and "initial_node". By default class initialised with empty Node class instance. Though, if "initial_node" is provided it used as "starting point". You can think about it as of "node upgrade". "data" (if provided) feeded to parser immidiatedly after instance init. - """ + ''' log.debug("Preparing to handle incoming XML stream.") self._parser = xml.parsers.expat.ParserCreate(namespace_separator=' ') self._parser.StartElementHandler = self.starttag @@ -328,7 +328,7 @@ class NodeBuilder: self.data_buffer = None def destroy(self): - """ Method used to allow class instance to be garbage-collected. """ + ''' Method used to allow class instance to be garbage-collected. ''' self.check_data_buffer() self._parser.StartElementHandler = None self._parser.EndElementHandler = None @@ -336,7 +336,7 @@ class NodeBuilder: self._parser.StartNamespaceDeclHandler = None def starttag(self, tag, attrs): - """XML Parser callback. Used internally""" + '''XML Parser callback. Used internally''' self.check_data_buffer() attlist=attrs.keys() # for attr in attlist: # FIXME: Crude hack. And it also slows down the whole library considerably. @@ -364,7 +364,7 @@ class NodeBuilder: self._ptr.parent.data.append('') self.last_is_data = 0 def endtag(self, tag ): - """XML Parser callback. Used internally""" + '''XML Parser callback. Used internally''' log.info("DEPTH -> %i , tag -> %s" % (self.__depth, tag)) self.check_data_buffer() if self.__depth == self._dispatch_depth: @@ -386,27 +386,27 @@ class NodeBuilder: self.last_is_data = 1 def handle_namespace_start(self, prefix, uri): - """XML Parser callback. Used internally""" + '''XML Parser callback. Used internally''' self.check_data_buffer() if prefix: self.namespaces[uri]=prefix+':' else: self.xmlns=uri def getDom(self): - """ Returns just built Node. """ + ''' Returns just built Node. ''' self.check_data_buffer() return self._mini_dom def dispatch(self,stanza): - """ Gets called when the NodeBuilder reaches some level of depth on it's way up with the built - node as argument. Can be redefined to convert incoming XML stanzas to program events. """ + ''' Gets called when the NodeBuilder reaches some level of depth on it's way up with the built + node as argument. Can be redefined to convert incoming XML stanzas to program events. ''' def stream_header_received(self,ns,tag,attrs): - """ Method called when stream just opened. """ + ''' Method called when stream just opened. ''' self.check_data_buffer() def stream_footer_received(self): - """ Method called when stream just closed. """ + ''' Method called when stream just closed. ''' self.check_data_buffer() def has_received_endtag(self, level=0): - """ Return True if at least one end tag was seen (at level) """ + ''' Return True if at least one end tag was seen (at level) ''' return self.__depth <= level and self.__max_depth > level def _inc_depth(self): @@ -419,12 +419,12 @@ class NodeBuilder: self.__depth -= 1 def XML2Node(xml): - """ Converts supplied textual string into XML node. Handy f.e. for reading configuration file. - Raises xml.parser.expat.parsererror if provided string is not well-formed XML. """ + ''' Converts supplied textual string into XML node. Handy f.e. for reading configuration file. + Raises xml.parser.expat.parsererror if provided string is not well-formed XML. ''' return NodeBuilder(xml).getDom() def BadXML2Node(xml): - """ Converts supplied textual string into XML node. Survives if xml data is cutted half way round. + ''' Converts supplied textual string into XML node. Survives if xml data is cutted half way round. I.e. "some text
some more text". Will raise xml.parser.expat.parsererror on misplaced - tags though. F.e. "some text
some more text
" will not work.""" + tags though. F.e. "some text
some more text
" will not work.''' return NodeBuilder(xml).getDom() diff --git a/src/common/xmpp/tls_nb.py b/src/common/xmpp/tls_nb.py index 148a8aac3..bd2091817 100644 --- a/src/common/xmpp/tls_nb.py +++ b/src/common/xmpp/tls_nb.py @@ -129,13 +129,13 @@ class SSLWrapper: log.debug("%s.__init__ called with %s", self.__class__, sslobj) def recv(self, data, flags=None): - """ Receive wrapper for SSL object + ''' Receive wrapper for SSL object We can return None out of this function to signal that no data is available right now. Better than an exception, which differs depending on which SSL lib we're using. Unfortunately returning '' can indicate that the socket has been closed, so to be sure, we avoid - this by returning None. """ + this by returning None. ''' raise NotImplementedException() diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py index e30a8aa90..9a60cb9c5 100644 --- a/src/common/xmpp/transports_nb.py +++ b/src/common/xmpp/transports_nb.py @@ -61,14 +61,10 @@ def get_proxy_data_from_dict(proxy): # user and pass for socks5/http_connect proxy. In case of BOSH, it's user and # pass for http proxy - If there's no proxy_host they won't be used - if proxy.has_key('user'): - proxy_user = proxy['user'] - else: - proxy_user = None - if proxy.has_key('pass'): - proxy_pass = proxy['pass'] - else: - proxy_pass = None + if proxy.has_key('user'): proxy_user = proxy['user'] + else: proxy_user = None + if proxy.has_key('pass'): proxy_pass = proxy['pass'] + else: proxy_pass = None return tcp_host, tcp_port, proxy_user, proxy_pass @@ -89,6 +85,7 @@ DATA_SENT='DATA SENT' DISCONNECTED ='DISCONNECTED' +DISCONNECTING ='DISCONNECTING' CONNECTING ='CONNECTING' CONNECTED ='CONNECTED' @@ -132,11 +129,10 @@ class NonBlockingTransport(PlugIn): self.on_connect_failure = on_connect_failure (self.server, self.port) = conn_5tuple[4][:2] self.conn_5tuple = conn_5tuple - log.info('NonBlocking Connect :: About to connect to %s:%s' % (self.server, self.port)) def set_state(self, newstate): - assert(newstate in [DISCONNECTED, CONNECTING, CONNECTED]) + assert(newstate in [DISCONNECTED, CONNECTING, CONNECTED, DISCONNECTING]) self.state = newstate def _on_connect(self, data): @@ -156,9 +152,8 @@ class NonBlockingTransport(PlugIn): self.on_connect_failure(err_message=err_message) def send(self, raw_data, now=False): - if self.state not in [CONNECTED]: - # FIXME better handling needed - log.error('Trying to send %s when transport is %s.' % + if self.state != CONNECTED: + log.error('Trying to send %s when state is %s.' % (raw_data, self.state)) return @@ -195,13 +190,13 @@ class NonBlockingTransport(PlugIn): self.remove_timeout() def set_timeout(self, timeout): - self.idlequeue.set_read_timeout(self.get_fd(), timeout) + self.idlequeue.set_read_timeout(self.fd, timeout) def get_fd(self): pass def remove_timeout(self): - self.idlequeue.remove_timeout(self.get_fd()) + self.idlequeue.remove_timeout(self.fd) def set_send_timeout(self, timeout, on_timeout): self.sendtimeout = timeout @@ -220,26 +215,15 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): Class constructor. ''' NonBlockingTransport.__init__(self, raise_event, on_disconnect, idlequeue) - # writable, readable - keep state of the last pluged flags - # This prevents replug of same object with the same flags - self.writable = True - self.readable = False # queue with messages to be send self.sendqueue = [] # bytes remained from the last send message self.sendbuff = '' + - def get_fd(self): - try: - tmp = self._sock.fileno() - return tmp - except socket.error, (errnum, errstr): - log.error('Trying to get file descriptor of not-connected socket: %s' % errstr ) - return 0 - def connect(self, conn_5tuple, on_connect, on_connect_failure): ''' Creates and connects socket to server and port defined in conn_5tupe which @@ -250,17 +234,21 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): connection ''' NonBlockingTransport.connect(self, conn_5tuple, on_connect, on_connect_failure) + log.info('NonBlockingTCP Connect :: About to connect to %s:%s' % (self.server, self.port)) try: self._sock = socket.socket(*conn_5tuple[:3]) except socket.error, (errnum, errstr): - self._on_connect_failure('NonBlockingTCP: Error while creating socket: %s %s' % (errnum, errstr)) + self._on_connect_failure('NonBlockingTCP Connect: Error while creating socket:\ + %s %s' % (errnum, errstr)) return self._send = self._sock.send self._recv = self._sock.recv self.fd = self._sock.fileno() - self.idlequeue.plug_idle(self, True, False) + + # we want to be notified when send is possible to connected socket + self._plug_idle(writable=True, readable=False) self.peerhost = None errnum = 0 @@ -268,7 +256,7 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): # set timeout for TCP connecting - if nonblocking connect() fails, pollend # is called. If if succeeds pollout is called. - self.idlequeue.set_read_timeout(self.get_fd(), CONNECT_TIMEOUT_SECONDS) + self.idlequeue.set_read_timeout(self.fd, CONNECT_TIMEOUT_SECONDS) try: self._sock.setblocking(False) @@ -296,7 +284,7 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): def _on_connect(self, data): ''' with TCP socket, we have to remove send-timeout ''' - self.idlequeue.remove_timeout(self.get_fd()) + self.idlequeue.remove_timeout(self.fd) NonBlockingTransport._on_connect(self, data) @@ -328,12 +316,14 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): def disconnect(self, do_callback=True): if self.state == DISCONNECTED: return - self.idlequeue.unplug_idle(self.get_fd()) + self.set_state(DISCONNECTING) + self.idlequeue.unplug_idle(self.fd) try: self._sock.shutdown(socket.SHUT_RDWR) self._sock.close() except socket.error, (errnum, errstr): - log.error('Error disconnecting a socket: %s %s' % (errnum,errstr)) + log.error('Error while disconnecting a socket: %s %s' % (errnum,errstr)) + self.fd = -1 NonBlockingTransport.disconnect(self, do_callback) def read_timeout(self): @@ -352,12 +342,16 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): def set_timeout(self, timeout): - if self.state in [CONNECTING, CONNECTED] and self.get_fd() > 0: + if self.state in [CONNECTING, CONNECTED] and self.fd != -1: NonBlockingTransport.set_timeout(self, timeout) + else: + log.warn('set_timeout: TIMEOUT NOT SET: state is %s, fd is %s' % (self.state, self.fd)) def remove_timeout(self): - if self.get_fd(): + if self.fd: NonBlockingTransport.remove_timeout(self) + else: + log.warn('remove_timeout: no self.fd state is %s' % self.state) def send(self, raw_data, now=False): '''Append raw_data to the queue of messages to be send. @@ -374,39 +368,42 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): self._do_send() else: self.sendqueue.append(r) - self._plug_idle() + self._plug_idle(writable=True, readable=True) - def _plug_idle(self): - # readable if socket is connected or disconnecting - readable = self.state != DISCONNECTED - fd = self.get_fd() - # writeable if sth to send - if self.sendqueue or self.sendbuff: - writable = True - else: - writable = False - log.debug('About to plug fd %d, W:%s, R:%s' % (fd, writable, readable)) - if self.writable != writable or self.readable != readable: - log.debug('Really plugging fd %d, W:%s, R:%s' % (fd, writable, readable)) - self.idlequeue.plug_idle(self, writable, readable) - else: - log.debug('Not plugging fd %s because it\'s already plugged' % fd) + def _plug_idle(self, writable, readable): + ''' + Plugs file descriptor of socket to Idlequeue. Plugged socket + will be watched for "send possible" or/and "recv possible" events. pollin() + callback is invoked on "recv possible", pollout() on "send_possible". + Plugged socket will always be watched for "error" event - in that case, + pollend() is called. + ''' + # if we are connecting, we shouln't touch the socket until it's connected + assert(self.state!=CONNECTING) + self.idlequeue.plug_idle(self, writable, readable) + + log.info('Plugging fd %d, W:%s, R:%s' % (self.fd, writable, readable)) + self.idlequeue.plug_idle(self, writable, readable) + def _do_send(self): if not self.sendbuff: if not self.sendqueue: - return None # nothing to send + log.warn('calling send on empty buffer and queue') + return None self.sendbuff = self.sendqueue.pop(0) try: send_count = self._send(self.sendbuff) if send_count: sent_data = self.sendbuff[:send_count] self.sendbuff = self.sendbuff[send_count:] - self._plug_idle() + self._plug_idle( + writable=self.sendqueue or self.sendbuff, + readable=True) self.raise_event(DATA_SENT, sent_data) except socket.error, e: @@ -439,7 +436,10 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): # ESHUTDOWN - shutdown(2) has been called on a socket to close down the # sending end of the transmision, and then data was attempted to be sent log.error("Connection to %s lost: %s %s" % ( self.server, errnum, errstr)) - self.disconnect() + if self.on_remote_disconnect: + self.on_remote_disconnect() + else: + self.disconnect() return if received is None: @@ -454,15 +454,13 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): # we have received some bytes, stop the timeout! self.renew_send_timeout() # pass received data to owner - #self. if self.on_receive: self.raise_event(DATA_RECEIVED, received) self._on_receive(received) else: # This should never happen, so we need the debug. (If there is no handler - # on receive spacified, data are passed to Dispatcher.ProcessNonBlocking) + # on receive specified, data are passed to Dispatcher.ProcessNonBlocking) log.error('SOCKET %s Unhandled data received: %s' % (id(self), received)) - import traceback traceback.print_stack() self.disconnect() @@ -474,12 +472,14 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): class NonBlockingHTTP(NonBlockingTCP): ''' - Socket wrapper that cretes HTTP message out of sent data and peels-off + Socket wrapper that creates HTTP message out of sent data and peels-off HTTP headers from incoming messages ''' def __init__(self, raise_event, on_disconnect, idlequeue, on_http_request_possible, - http_uri, http_port, http_version='HTTP/1.1'): + http_uri, http_port, http_version='HTTP/1.1', http_persistent=False): + + NonBlockingTCP.__init__(self, raise_event, on_disconnect, idlequeue) self.http_protocol, self.http_host, self.http_path = urisplit(http_uri) if self.http_protocol is None: @@ -488,12 +488,12 @@ class NonBlockingHTTP(NonBlockingTCP): http_path = '/' self.http_port = http_port self.http_version = http_version + self.http_persistent = http_persistent # buffer for partial responses self.recvbuff = '' self.expected_length = 0 self.pending_requests = 0 self.on_http_request_possible = on_http_request_possible - NonBlockingTCP.__init__(self, raise_event, on_disconnect, idlequeue) def send(self, raw_data, now=False): NonBlockingTCP.send( @@ -503,6 +503,19 @@ class NonBlockingHTTP(NonBlockingTCP): self.pending_requests += 1 + def on_remote_disconnect(self): + if self.http_persistent: + self.http_persistent = False + self.disconnect(do_callback=False) + self.connect( + conn_5tuple = self.conn_5tuple, + on_connect = lambda: self._plug_idle(writable=True, readable=True), + on_connect_failure = self.disconnect) + + else: + self.disconnect() + return + def _on_receive(self,data): '''Preceeds passing received data to owner class. Gets rid of HTTP headers and checks them.''' @@ -524,9 +537,6 @@ class NonBlockingHTTP(NonBlockingTCP): log.info('not enough bytes - %d expected, %d got' % (self.expected_length, len(self.recvbuff))) return - # FIXME the reassembling doesn't work - Connection Manager on jabbim.cz - # closes TCP connection before sending announced bytes.. WTF - # all was received, now call the on_receive callback httpbody = self.recvbuff @@ -534,17 +544,18 @@ class NonBlockingHTTP(NonBlockingTCP): self.expected_length=0 self.pending_requests -= 1 assert(self.pending_requests >= 0) - # not-persistent connections - self.disconnect(do_callback = False) + if not self.http_persistent: + # not-persistent connections disconnect after response + self.disconnect(do_callback = False) self.on_receive(httpbody) self.on_http_request_possible() + def build_http_message(self, httpbody, method='POST'): ''' Builds http message with given body. Values for headers and status line fields are taken from class variables. - ) ''' absolute_uri = '%s://%s:%s%s' % (self.http_protocol, self.http_host, self.http_port, self.http_path) diff --git a/src/gajim.py b/src/gajim.py index 50b2d9f7e..fcb069016 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -50,7 +50,8 @@ import logging consoleloghandler = logging.StreamHandler() consoleloghandler.setLevel(1) consoleloghandler.setFormatter( -logging.Formatter('%(name)s: %(levelname)s: %(message)s')) + logging.Formatter('%(name)s: %(levelname)s: %(message)s') + ) log = logging.getLogger('gajim') log.setLevel(logging.WARNING) log.addHandler(consoleloghandler) From af3f1a9dd421b0520e247e621f9351e22a6a8bd8 Mon Sep 17 00:00:00 2001 From: tomk Date: Sat, 26 Jul 2008 22:42:40 +0000 Subject: [PATCH 13/20] - implemented BOSH key sequencing, acknowledgements - improved HTTP persistent connections - added alarm-unregister method to idlequeue - extended proxy managing dialog for BOSH proxy --- data/glade/manage_proxies_window.glade | 170 ++++++++- src/common/config.py | 9 + src/common/connection.py | 22 +- src/common/xmpp/bosh.py | 493 +++++++++++++++++-------- src/common/xmpp/client_nb.py | 8 +- src/common/xmpp/dispatcher_nb.py | 28 +- src/common/xmpp/idlequeue.py | 27 +- src/common/xmpp/transports_nb.py | 111 ++++-- src/config.py | 70 +++- src/gajim.py | 2 +- 10 files changed, 690 insertions(+), 250 deletions(-) diff --git a/data/glade/manage_proxies_window.glade b/data/glade/manage_proxies_window.glade index b28beef97..9580b12e3 100644 --- a/data/glade/manage_proxies_window.glade +++ b/data/glade/manage_proxies_window.glade @@ -235,6 +235,7 @@ BOSH 0 True + False
@@ -305,7 +306,7 @@ BOSH True False - 5 + 8 2 False 6 @@ -314,7 +315,7 @@ BOSH True - _Port: + Proxy _Port: True False GTK_JUSTIFY_LEFT @@ -333,8 +334,8 @@ BOSH 0 1 - 1 - 2 + 4 + 5 fill @@ -349,14 +350,15 @@ BOSH 0 True + False 1 2 - 0 - 1 + 3 + 4 @@ -370,14 +372,15 @@ BOSH 0 True + False 1 2 - 1 - 2 + 4 + 5
@@ -385,7 +388,7 @@ BOSH True - _Host: + Proxy _Host: True False GTK_JUSTIFY_LEFT @@ -404,8 +407,8 @@ BOSH 0 1 - 0 - 1 + 3 + 4 fill @@ -433,8 +436,8 @@ BOSH 0 1 - 4 - 5 + 7 + 8 fill @@ -462,8 +465,8 @@ BOSH 0 1 - 3 - 4 + 6 + 7 fill @@ -478,14 +481,15 @@ BOSH 0 True + False 1 2 - 4 - 5 + 7 + 8 @@ -499,14 +503,15 @@ BOSH 0 True + False 1 2 - 3 - 4 + 6 + 7 @@ -515,7 +520,7 @@ BOSH True True - Use authentication + Use proxy authentication True GTK_RELIEF_NORMAL True @@ -524,6 +529,80 @@ BOSH True + + 0 + 2 + 5 + 6 + fill + + + + + + + True + _BOSH URL: + True + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + proxyhost_entry + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 0 + 1 + fill + + + + + + + True + True + True + True + 0 + + True + + False + + + + 1 + 2 + 0 + 1 + + + + + + + True + True + Use HTTP proxy + True + GTK_RELIEF_NORMAL + True + False + False + True + + 0 2 @@ -533,6 +612,57 @@ BOSH + + + + True + B_OSH Port: + True + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + proxyhost_entry + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 1 + 2 + fill + + + + + + + True + True + True + True + 0 + + True + + False + + + + 1 + 2 + 1 + 2 + + + diff --git a/src/common/config.py b/src/common/config.py index 9b950705a..4a52f9eae 100644 --- a/src/common/config.py +++ b/src/common/config.py @@ -337,8 +337,17 @@ class Config: 'type': [ opt_str, 'http' ], 'host': [ opt_str, '' ], 'port': [ opt_int, 3128 ], + 'useauth': [ opt_bool, False ], 'user': [ opt_str, '' ], 'pass': [ opt_str, '' ], + 'bosh_uri': [ opt_str, '' ], + 'bosh_port': [ opt_int, 80 ], + 'bosh_useproxy': [ opt_bool, False ], + 'bosh_wait': [ opt_int, 30 ], + 'bosh_hold': [ opt_int, 2 ], + 'bosh_content': [ opt_str, 'text/xml; charset=utf-8' ], + 'bosh_http_pipelining': [ opt_bool, False ], + 'bosh_wait_for_restart_response': [ opt_bool, False ], }, {}), 'themes': ({ 'accounttextcolor': [ opt_color, 'black', '', True ], diff --git a/src/common/connection.py b/src/common/connection.py index 08d7bcaae..7df8b4a71 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -428,11 +428,11 @@ class Connection(ConnectionHandlers): # create connection if it doesn't already exist self.connected = 1 if p and p in gajim.config.get_per('proxies'): - proxy = {'host': gajim.config.get_per('proxies', p, 'host')} - proxy['port'] = gajim.config.get_per('proxies', p, 'port') - proxy['user'] = gajim.config.get_per('proxies', p, 'user') - proxy['password'] = gajim.config.get_per('proxies', p, 'pass') - proxy['type'] = gajim.config.get_per('proxies', p, 'type') + proxy = {} + proxyptr = gajim.config.get_per('proxies',p) + for key in proxyptr.keys(): proxy[key]=proxyptr[key][1] + print proxy + elif gajim.config.get_per('accounts', self.name, 'use_env_http_proxy'): try: try: @@ -546,11 +546,11 @@ class Connection(ConnectionHandlers): con.RegisterDisconnectHandler(self._on_new_account) # FIXME: BOSH properties should be loaded from config - if self._proxy and self._proxy['type'] == 'bosh': - self._proxy['bosh_hold'] = '1' - self._proxy['bosh_wait'] = '60' - self._proxy['bosh_content'] = 'text/xml; charset=utf-8' - self._proxy['wait_for_restart_response'] = False + #if self._proxy and self._proxy['type'] == 'bosh': + # self._proxy['bosh_hold'] = '2' + # self._proxy['bosh_wait'] = '10' + # self._proxy['bosh_content'] = 'text/xml; charset=utf-8' + # self._proxy['wait_for_restart_response'] = False log.info('Connecting to %s: [%s:%d]', self.name, @@ -1003,7 +1003,7 @@ class Connection(ConnectionHandlers): self.connection.RegisterDisconnectHandler(self._on_disconnected) self.connection.send(p, now=True) - self.connection.StreamTerminate() + self.connection.start_disconnect() #self.connection.start_disconnect(p, self._on_disconnected) else: self.time_to_reconnect = None diff --git a/src/common/xmpp/bosh.py b/src/common/xmpp/bosh.py index 4ce71cd68..c29fa7294 100644 --- a/src/common/xmpp/bosh.py +++ b/src/common/xmpp/bosh.py @@ -1,16 +1,20 @@ import locale, random -from transports_nb import NonBlockingTransport, NonBlockingHTTP, CONNECTED, CONNECTING, DISCONNECTED +from transports_nb import NonBlockingTransport, NonBlockingHTTPBOSH,\ + CONNECTED, CONNECTING, DISCONNECTED, DISCONNECTING,\ + urisplit from protocol import BOSHBody from simplexml import Node +import sha import logging log = logging.getLogger('gajim.c.x.bosh') +KEY_COUNT = 10 FAKE_DESCRIPTOR = -1337 '''Fake file descriptor - it's used for setting read_timeout in idlequeue for -BOSH Transport. Timeouts in queue are saved by socket descriptor. +BOSH Transport. In TCP-derived transports it is file descriptor of socket''' @@ -19,12 +23,6 @@ class NonBlockingBOSH(NonBlockingTransport): bosh_dict): NonBlockingTransport.__init__(self, raise_event, on_disconnect, idlequeue) - # with 50-bit random initial rid, session would have to go up - # to 7881299347898368 messages to raise rid over 2**53 - # (see http://www.xmpp.org/extensions/xep-0124.html#rids) - r = random.Random() - r.seed() - self.bosh_rid = r.getrandbits(50) self.bosh_sid = None if locale.getdefaultlocale()[0]: self.bosh_xml_lang = locale.getdefaultlocale()[0].split('_')[0] @@ -33,25 +31,30 @@ class NonBlockingBOSH(NonBlockingTransport): self.http_version = 'HTTP/1.1' self.http_persistent = True - self.http_pipelining = False + self.http_pipelining = bosh_dict['bosh_http_pipelining'] self.bosh_to = domain self.route_host, self.route_port = xmpp_server self.bosh_wait = bosh_dict['bosh_wait'] self.bosh_hold = bosh_dict['bosh_hold'] - self.bosh_host = bosh_dict['host'] - self.bosh_port = bosh_dict['port'] + self.bosh_requests = self.bosh_hold + self.bosh_uri = bosh_dict['bosh_uri'] + self.bosh_port = bosh_dict['bosh_port'] self.bosh_content = bosh_dict['bosh_content'] + self.wait_cb_time = None self.http_socks = [] - self.stanzas_to_send = [] - self.prio_bosh_stanza = None + self.stanza_buffer = [] + self.prio_bosh_stanzas = [] self.current_recv_handler = None + self.current_recv_socket = None + self.key_stack = None + self.ack_checker = None + self.after_init = False # if proxy_host .. do sth about HTTP proxy etc. - def connect(self, conn_5tuple, on_connect, on_connect_failure): NonBlockingTransport.connect(self, conn_5tuple, on_connect, on_connect_failure) @@ -59,14 +62,20 @@ class NonBlockingBOSH(NonBlockingTransport): FAKE_DESCRIPTOR = FAKE_DESCRIPTOR - 1 self.fd = FAKE_DESCRIPTOR - self.http_persistent = True - self.http_socks.append(self.get_http_socket()) + self.stanza_buffer = [] + self.prio_bosh_stanzas = [] + + self.key_stack = KeyStack(KEY_COUNT) + self.ack_checker = AckChecker() + self.after_init = True + + self.http_socks.append(self.get_new_http_socket()) self.tcp_connection_started() - # this connect() is not needed because sockets can be connected on send but - # we need to know if host is reachable in order to invoke callback for - # connecting failurei eventually (it's different than callback for errors - # occurring after connection is etabilished) + # following connect() is not necessary because sockets can be connected on + # send but we need to know if host is reachable in order to invoke callback + # for connecting failure eventually (the callback is different than callback + # for errors occurring after connection is etabilished) self.http_socks[0].connect( conn_5tuple = conn_5tuple, on_connect = lambda: self._on_connect(self.http_socks[0]), @@ -83,121 +92,209 @@ class NonBlockingBOSH(NonBlockingTransport): Called after HTTP response is received - another request is possible. There should be always one pending request on BOSH CM. ''' - log.info('on_http_req possible state:\n%s' % self.get_current_state()) - # if one of sockets is connecting, sth is about to be sent - # if there is a pending request, we shouldn't send another one + log.info('on_http_req possible, state:\n%s' % self.get_current_state()) + if self.state == DISCONNECTING: + self.disconnect() + return + self.send_BOSH(None) + + + def get_socket_in(self, state): for s in self.http_socks: - if s.state==CONNECTING or s.pending_requests>0: return - self.flush_stanzas() + if s.state==state: return s + return None - - def flush_stanzas(self): - # another to-be-locked candidate - log.info('flushing stanzas') - if self.prio_bosh_stanza: - tmp = self.prio_bosh_stanza - self.prio_bosh_stanza = None + def get_free_socket(self): + if self.http_pipelining: + assert( len(self.http_socks) == 1 ) + return self.get_socket_in(CONNECTED) else: - if self.stanzas_to_send: - tmp = self.stanzas_to_send.pop(0) - else: - tmp = [] - self.send_http(tmp) + last_recv_time, tmpsock = 0, None + for s in self.http_socks: + # we're interested only into CONNECTED socket with no req pending + if s.state==CONNECTED and s.pending_requests==0: + # if there's more of them, we want the one with less recent data receive + # (lowest last_recv_time) + if (last_recv_time==0) or (s.last_recv_time < last_recv_time): + last_recv_time = s.last_recv_time + tmpsock = s + if tmpsock: + return tmpsock + else: + return None + + + def send_BOSH(self, payload): + total_pending_reqs = sum([s.pending_requests for s in self.http_socks]) + + # when called after HTTP response when there are some pending requests and + # no data to send, we do nothing and disccard the payload + if payload is None and \ + total_pending_reqs > 0 and \ + self.stanza_buffer == [] and \ + self.prio_bosh_stanzas == [] or \ + self.state==DISCONNECTED: + return + + # now the payload is put to buffer and will be sent at some point + self.append_stanza(payload) + + # if we're about to make more requests than allowed, we don't send - stanzas will be + # sent after HTTP response from CM, exception is when we're disconnecting - then we + # send anyway + if total_pending_reqs >= self.bosh_requests and self.state!=DISCONNECTING: + log.warn('attemp to make more requests than allowed by Connection Manager:\n%s' % + self.get_current_state()) + return + + # when there's free CONNECTED socket, we flush the data + if self.get_free_socket(): + self.plug_socket() + return + + # if there is a connecting socket, we just wait for when it connects, + # payload will be sent in a sec when the socket connects + if self.get_socket_in(CONNECTING): return + + # being here means there are either DISCONNECTED sockets or all sockets are + # CONNECTED with too many pending requests + s = self.get_socket_in(DISCONNECTED) + + # if we have DISCONNECTED socket, lets connect it and ... + if s: + self.connect_and_flush(s) + else: + if len(self.http_socks) > 1: return + ss = self.get_new_http_socket() + self.http_socks.append(ss) + self.connect_and_flush(ss) + return + + def plug_socket(self): + stanza = None + s = self.get_free_socket() + if s: + s._plug_idle(writable=True, readable=True) + else: + log.error('=====!!!!!!!!====> Couldnt get free socket in plug_socket())') + + def build_stanza(self, socket): + if self.prio_bosh_stanzas: + stanza, add_payload = self.prio_bosh_stanzas.pop(0) + if add_payload: + stanza.setPayload(self.stanza_buffer) + self.stanza_buffer = [] + else: + stanza = self.boshify_stanzas(self.stanza_buffer) + self.stanza_buffer = [] + + stanza = self.ack_checker.backup_stanza(stanza, socket) + + key, newkey = self.key_stack.get() + if key: + stanza.setAttr('key', key) + if newkey: + stanza.setAttr('newkey', newkey) + + + log.info('sending msg with rid=%s to sock %s' % (stanza.getAttr('rid'), id(socket))) + socket.send(stanza) + self.renew_bosh_wait_timeout() + return stanza + + + def on_bosh_wait_timeout(self): + log.error('Connection Manager didn\'t respond within % seconds --> forcing \ + disconnect' % self.bosh_wait) + self.disconnect() + + + def renew_bosh_wait_timeout(self): + if self.wait_cb_time is not None: + self.remove_bosh_wait_timeout() + sched_time = self.idlequeue.set_alarm(self.on_bosh_wait_timeout, self.bosh_wait+10) + self.wait_cb_time = sched_time + + def remove_bosh_wait_timeout(self): + self.idlequeue.remove_alarm( + self.on_bosh_wait_timeout, + self.wait_cb_time) + + def on_persistent_fallback(self): + log.warn('Fallback to nonpersistent HTTP (no pipelining as well)') + self.http_persistent = False + self.http_pipelining = False + + + def handle_body_attrs(self, stanza_attrs): + self.remove_bosh_wait_timeout() + + if self.after_init: + self.after_init = False + if stanza_attrs.has_key('sid'): + # session ID should be only in init response + self.bosh_sid = stanza_attrs['sid'] + + if stanza_attrs.has_key('requests'): + #self.bosh_requests = int(stanza_attrs['requests']) + self.bosh_requests = int(stanza_attrs['wait']) + + if stanza_attrs.has_key('wait'): + self.bosh_wait = int(stanza_attrs['wait']) + + ack = None + if stanza_attrs.has_key('ack'): + ack = stanza_attrs['ack'] + self.ack_checker.process_incoming_ack(ack=ack, + socket=self.current_recv_socket) + + if stanza_attrs.has_key('type'): + if stanza_attrs['type'] in ['terminate', 'terminal']: + condition = 'n/a' + if stanza_attrs.has_key('condition'): + condition = stanza_attrs['condition'] + log.error('Received terminating stanza: %s - %s' % (condition, bosh_errors[condition])) + self.set_state(DISCONNECTING) + + if stanza_attrs['type'] == 'error': + # recoverable error + pass + return + + + def append_stanza(self, stanza): + if stanza: + if isinstance(stanza, tuple): + # tuple of BOSH stanza and True/False for whether to add payload + self.prio_bosh_stanzas.append(stanza) + else: + self.stanza_buffer.append(stanza) + def send(self, stanza, now=False): - # body tags should be send only via send_http() + # body tags should be send only via send_BOSH() assert(not isinstance(stanza, BOSHBody)) - self.send_http([stanza]) + self.send_BOSH(stanza) - def send_http(self, payload): - # "Protocol" and string/unicode stanzas should be sent via send() - # (only initiating and terminating BOSH stanzas should be send via send_http) - assert(isinstance(payload, list) or isinstance(payload, BOSHBody)) - log.warn('send_http: stanzas: %s\n%s' % (payload, self.get_current_state())) - - if isinstance(payload, list): - bosh_stanza = self.boshify_stanzas(payload) - else: - # bodytag_payload is , we don't boshify, only add the rid - bosh_stanza = payload - picked_sock = self.pick_socket() - if picked_sock: - log.info('sending to socket %s' % id(picked_sock)) - bosh_stanza.setAttr('rid', self.get_rid()) - picked_sock.send(bosh_stanza) - else: - # no socket was picked but one is about to connect - save the stanza and - # return - log.info('send_http: no free socket:\n%s' % self.get_current_state()) - if self.prio_bosh_stanza: - payload = self.merge_stanzas(payload, self.prio_bosh_stanza) - if payload is None: - # if we cant merge the stanzas (both are BOSH ), add the current to - # queue to be sent later - self.stanzas_to_send.append(bosh_stanza) - log.warn('in BOSH send_http - unable to send %s because %s\ - is already about to be sent' % (str(payload), str(self.prio_bosh_stanza))) - return - self.prio_bosh_stanza = payload - - def merge_stanzas(self, s1, s2): - if isinstance(s1, BOSHBody): - if isinstance(s2, BOSHBody): - # both are boshbodies - return - else: - s1.setPayload(s2, add=True) - return s1 - elif isinstance(s2, BOSHBody): - s2.setPayload(s1, add=True) - return s2 - else: - #both are lists - s1.extend(s2) - return s1 - def get_current_state(self): t = '------ SOCKET_ID\tSOCKET_STATE\tPENDING_REQS\n' for s in self.http_socks: t = '%s------ %s\t%s\t%s\n' % (t,id(s), s.state, s.pending_requests) - t = '%s------ prio stanza to send: %s, queued stanzas: %s' \ - % (t, self.prio_bosh_stanza, self.stanzas_to_send) + t = '%s------ prio stanzas: %s, queued XMPP stanzas: %s, not_acked stanzas: %s' \ + % (t, self.prio_bosh_stanzas, self.stanza_buffer, + self.ack_checker.get_not_acked_rids()) return t - def pick_socket(self): - # try to pick connected socket with no pending reqs - for s in self.http_socks: - if s.state == CONNECTED and s.pending_requests == 0: - return s - - # try to connect some disconnected socket - for s in self.http_socks: - if s.state==DISCONNECTED: - self.connect_and_flush(s) - return - - # if there is any just-connecting socket, it will send the data in its - # connect callback - for s in self.http_socks: - if s.state==CONNECTING: - return - # being here means there are only CONNECTED scokets with pending requests. - # Lets create and connect another one - if len(self.http_socks) < 2: - s = self.get_http_socket() - self.http_socks.append(s) - self.connect_and_flush(s) - return def connect_and_flush(self, socket): socket.connect( conn_5tuple = self.conn_5tuple, - on_connect = self.flush_stanzas, + on_connect = lambda :self.send_BOSH(None), on_connect_failure = self.disconnect) @@ -209,65 +306,163 @@ class NonBlockingBOSH(NonBlockingTransport): return tag - def get_initial_bodytag(self, after_SASL=False): - return BOSHBody( - attrs={'content': self.bosh_content, - 'hold': str(self.bosh_hold), - 'route': '%s:%s' % (self.route_host, self.route_port), - 'to': self.bosh_to, - 'wait': str(self.bosh_wait), - 'xml:lang': self.bosh_xml_lang, - 'xmpp:version': '1.0', - 'ver': '1.6', - 'xmlns:xmpp': 'urn:xmpp:xbosh'}) + def send_init(self, after_SASL=False): + if after_SASL: + t = BOSHBody( + attrs={ 'to': self.bosh_to, + 'sid': self.bosh_sid, + 'xml:lang': self.bosh_xml_lang, + 'xmpp:restart': 'true', + 'xmlns:xmpp': 'urn:xmpp:xbosh'}) + else: + t = BOSHBody( + attrs={ 'content': self.bosh_content, + 'hold': str(self.bosh_hold), + 'route': '%s:%s' % (self.route_host, self.route_port), + 'to': self.bosh_to, + 'wait': str(self.bosh_wait), + 'xml:lang': self.bosh_xml_lang, + 'xmpp:version': '1.0', + 'ver': '1.6', + 'xmlns:xmpp': 'urn:xmpp:xbosh'}) + self.send_BOSH((t,True)) - def get_after_SASL_bodytag(self): - return BOSHBody( - attrs={ 'to': self.bosh_to, - 'sid': self.bosh_sid, - 'xml:lang': self.bosh_xml_lang, - 'xmpp:restart': 'true', - 'xmlns:xmpp': 'urn:xmpp:xbosh'}) - - def get_closing_bodytag(self): - return BOSHBody(attrs={'sid': self.bosh_sid, 'type': 'terminate'}) - - def get_rid(self): - self.bosh_rid = self.bosh_rid + 1 - return str(self.bosh_rid) + def start_disconnect(self): + NonBlockingTransport.start_disconnect(self) + self.send_BOSH( + (BOSHBody(attrs={'sid': self.bosh_sid, 'type': 'terminate'}), True)) - def get_http_socket(self): - s = NonBlockingHTTP( + def get_new_http_socket(self): + s = NonBlockingHTTPBOSH( raise_event=self.raise_event, on_disconnect=self.disconnect, idlequeue = self.idlequeue, on_http_request_possible = self.on_http_request_possible, - http_uri = self.bosh_host, + http_uri = self.bosh_uri, http_port = self.bosh_port, http_version = self.http_version, - http_persistent = self.http_persistent) - if self.current_recv_handler: - s.onreceive(self.current_recv_handler) + http_persistent = self.http_persistent, + on_persistent_fallback = self.on_persistent_fallback) + s.onreceive(self.on_received_http) + s.set_stanza_build_cb(self.build_stanza) return s + def onreceive(self, recv_handler): if recv_handler is None: recv_handler = self._owner.Dispatcher.ProcessNonBlocking self.current_recv_handler = recv_handler - for s in self.http_socks: - s.onreceive(recv_handler) - def http_socket_disconnect(self, socket): - if self.http_persistent: - self.disconnect() + def on_received_http(self, data, socket): + self.current_recv_socket = socket + self.current_recv_handler(data) def disconnect(self, do_callback=True): + self.remove_bosh_wait_timeout() if self.state == DISCONNECTED: return self.fd = -1 for s in self.http_socks: s.disconnect(do_callback=False) NonBlockingTransport.disconnect(self, do_callback) + +def get_rand_number(): + # with 50-bit random initial rid, session would have to go up + # to 7881299347898368 messages to raise rid over 2**53 + # (see http://www.xmpp.org/extensions/xep-0124.html#rids) + # it's also used for sequence key initialization + r = random.Random() + r.seed() + return r.getrandbits(50) + + + +class AckChecker(): + def __init__(self): + self.rid = get_rand_number() + self.ack = 1 + self.last_rids = {} + self.not_acked = [] + + + def get_not_acked_rids(self): return [rid for rid, st in self.not_acked] + + def backup_stanza(self, stanza, socket): + socket.pending_requests += 1 + rid = self.get_rid() + self.not_acked.append((rid, stanza)) + stanza.setAttr('rid', str(rid)) + self.last_rids[socket]=rid + + if self.rid != self.ack + 1: + stanza.setAttr('ack', str(self.ack)) + return stanza + + def process_incoming_ack(self, socket, ack=None): + socket.pending_requests -= 1 + if ack: + ack = int(ack) + else: + ack = self.last_rids[socket] + + i = len([rid for rid, st in self.not_acked if ack >= rid]) + self.not_acked = self.not_acked[i:] + + self.ack = ack + + + def get_rid(self): + self.rid = self.rid + 1 + return self.rid + + + + + +class KeyStack(): + def __init__(self, count): + self.count = count + self.keys = [] + self.reset() + self.first_call = True + + def reset(self): + seed = str(get_rand_number()) + self.keys = [sha.new(seed).hexdigest()] + for i in range(self.count-1): + curr_seed = self.keys[i] + self.keys.append(sha.new(curr_seed).hexdigest()) + + def get(self): + if self.first_call: + self.first_call = False + return (None, self.keys.pop()) + + if len(self.keys)>1: + return (self.keys.pop(), None) + else: + last_key = self.keys.pop() + self.reset() + new_key = self.keys.pop() + return (last_key, new_key) + +# http://www.xmpp.org/extensions/xep-0124.html#errorstatus-terminal +bosh_errors = { + 'n/a': 'none or unknown condition in terminating body stanza', + 'bad-request': 'The format of an HTTP header or binding element received from the client is unacceptable (e.g., syntax error), or Script Syntax is not supported.', + 'host-gone': 'The target domain specified in the "to" attribute or the target host or port specified in the "route" attribute is no longer serviced by the connection manager.', + 'host-unknown': 'The target domain specified in the "to" attribute or the target host or port specified in the "route" attribute is unknown to the connection manager.', + 'improper-addressing': 'The initialization element lacks a "to" or "route" attribute (or the attribute has no value) but the connection manager requires one.', + 'internal-server-error': 'The connection manager has experienced an internal error that prevents it from servicing the request.', + 'item-not-found': '(1) "sid" is not valid, (2) "stream" is not valid, (3) "rid" is larger than the upper limit of the expected window, (4) connection manager is unable to resend response, (5) "key" sequence is invalid', + 'other-request': 'Another request being processed at the same time as this request caused the session to terminate.', + 'policy-violation': 'The client has broken the session rules (polling too frequently, requesting too frequently, too many simultaneous requests).', + 'remote-connection-failed': 'The connection manager was unable to connect to, or unable to connect securely to, or has lost its connection to, the server.', + 'remote-stream-error': 'Encapsulates an error in the protocol being transported.', + 'see-other-uri': 'The connection manager does not operate at this URI (e.g., the connection manager accepts only SSL or TLS connections at some https: URI rather than the http: URI requested by the client). The client may try POSTing to the URI in the content of the child element.', + 'system-shutdown': 'The connection manager is being shut down. All active HTTP sessions are being terminated. No new sessions can be created.', + 'undefined-condition': 'The error is not one of those defined herein; the connection manager SHOULD include application-specific information in the content of the wrapper.' +} diff --git a/src/common/xmpp/client_nb.py b/src/common/xmpp/client_nb.py index 693f5e534..eb44d4665 100644 --- a/src/common/xmpp/client_nb.py +++ b/src/common/xmpp/client_nb.py @@ -128,8 +128,8 @@ class NBCommonClient: self.ip_addresses = socket.getaddrinfo(hostname,port, socket.AF_UNSPEC,socket.SOCK_STREAM) except socket.gaierror, (errnum, errstr): - on_failure(err_message='Lookup failure for %s:%s - %s %s' % - (self.Server, self.Port, errnum, errstr)) + on_failure('Lookup failure for %s:%s, hostname: %s - %s' % + (self.Server, self.Port, hostname, errstr)) else: on_success() @@ -385,7 +385,7 @@ class NonBlockingClient(NBCommonClient): # with proxies, client connects to proxy instead of directly to # XMPP server ((hostname, port)) # tcp_host is hostname of machine used for socket connection - # (DNS request will be done for this hostname) + # (DNS request will be done for proxy or BOSH CM hostname) tcp_host, tcp_port, proxy_user, proxy_pass = \ transports_nb.get_proxy_data_from_dict(proxy) @@ -400,7 +400,7 @@ class NonBlockingClient(NBCommonClient): domain = self.Server, bosh_dict = proxy) self.protocol_type = 'BOSH' - self.wait_for_restart_response = proxy['wait_for_restart_response'] + self.wait_for_restart_response = proxy['bosh_wait_for_restart_response'] else: if proxy['type'] == 'socks5': diff --git a/src/common/xmpp/dispatcher_nb.py b/src/common/xmpp/dispatcher_nb.py index 09e6a2616..5b1eb0be4 100644 --- a/src/common/xmpp/dispatcher_nb.py +++ b/src/common/xmpp/dispatcher_nb.py @@ -35,7 +35,6 @@ log.setLevel(logging.INFO) DEFAULT_TIMEOUT_SECONDS = 25 ID = 0 -STREAM_TERMINATOR = '' XML_DECLARATION = '' # FIXME: ugly @@ -46,7 +45,8 @@ class Dispatcher(): # named by __class__.__name__ of the dispatcher instance .. long story short: # I wrote following to avoid changing each client.Dispatcher.whatever() in xmpp/ -# If having two kinds of dispatcher will go well, I will rewrite the +# If having two kinds of dispatcher will go well, I will rewrite the dispatcher +# references in other scripts def PlugIn(self, client_obj, after_SASL=False, old_features=None): if client_obj.protocol_type == 'XMPP': XMPPDispatcher().PlugIn(client_obj) @@ -71,7 +71,7 @@ class XMPPDispatcher(PlugIn): self._exported_methods=[self.RegisterHandler, self.RegisterDefaultHandler, \ self.RegisterEventHandler, self.UnregisterCycleHandler, self.RegisterCycleHandler, \ self.RegisterHandlerOnce, self.UnregisterHandler, self.RegisterProtocol, \ - self.SendAndWaitForResponse, self.StreamTerminate, \ + self.SendAndWaitForResponse, \ self.SendAndCallForResponse, self.getAnID, self.Event, self.send] def getAnID(self): @@ -134,9 +134,6 @@ class XMPPDispatcher(PlugIn): locale.getdefaultlocale()[0].split('_')[0]) self._owner.send("%s%s>" % (XML_DECLARATION,str(self._metastream)[:-2])) - def StreamTerminate(self): - ''' Send a stream terminator. ''' - self._owner.send(STREAM_TERMINATOR) def _check_stream_start(self, ns, tag, attrs): if ns<>NS_STREAMS or tag<>'stream': @@ -445,16 +442,12 @@ class BOSHDispatcher(XMPPDispatcher): locale.getdefaultlocale()[0].split('_')[0]) self.restart = True - if self.after_SASL: - self._owner.Connection.send_http(self._owner.Connection.get_after_SASL_bodytag()) - else: - self._owner.Connection.send_http(self._owner.Connection.get_initial_bodytag()) - + self._owner.Connection.send_init(after_SASL = self.after_SASL) def StreamTerminate(self): ''' Send a stream terminator. ''' - self._owner.Connection.send_http(self._owner.Connection.get_closing_bodytag()) + self._owner.Connection.send_terminator() def ProcessNonBlocking(self, data=None): @@ -472,22 +465,13 @@ class BOSHDispatcher(XMPPDispatcher): if stanza.getName()=='body' and stanza.getNamespace()==NS_HTTP_BIND: stanza_attrs = stanza.getAttrs() - if stanza_attrs.has_key('authid'): # should be only in init response # auth module expects id of stream in document attributes self.Stream._document_attrs['id'] = stanza_attrs['authid'] - if stanza_attrs.has_key('sid'): - # session ID should be only in init response - self._owner.Connection.bosh_sid = stanza_attrs['sid'] + self._owner.Connection.handle_body_attrs(stanza_attrs) - if stanza_attrs.has_key('terminate'): - self._owner.disconnect() - - if stanza_attrs.has_key('error'): - # recoverable error - pass children = stanza.getChildren() diff --git a/src/common/xmpp/idlequeue.py b/src/common/xmpp/idlequeue.py index f358e45f5..765063167 100644 --- a/src/common/xmpp/idlequeue.py +++ b/src/common/xmpp/idlequeue.py @@ -15,7 +15,6 @@ import select import logging log = logging.getLogger('gajim.c.x.idlequeue') -log.setLevel(logging.DEBUG) class IdleObject: ''' base class for all idle listeners, these are the methods, which are called from IdleQueue @@ -68,6 +67,25 @@ class IdleQueue: self.alarms[alarm_time].append(alarm_cb) else: self.alarms[alarm_time] = [alarm_cb] + return alarm_time + + + + def remove_alarm(self, alarm_cb, alarm_time): + ''' removes alarm callback alarm_cb scheduled on alarm_time''' + if not self.alarms.has_key(alarm_time): return False + i = -1 + for i in range(len(self.alarms[alarm_time])): + # let's not modify the list inside the loop + if self.alarms[alarm_time][i] is alarm_cb: break + if i != -1: + del self.alarms[alarm_time][i] + if self.alarms[alarm_time] == []: + del self.alarms[alarm_time] + return True + else: + return False + def set_read_timeout(self, fd, seconds): ''' set a new timeout, if it is not removed after 'seconds', @@ -91,9 +109,10 @@ class IdleQueue: for alarm_time in times: if alarm_time > current_time: break - for cb in self.alarms[alarm_time]: - cb() - del(self.alarms[alarm_time]) + if self.alarms.has_key(alarm_time): + for cb in self.alarms[alarm_time]: + cb() + del(self.alarms[alarm_time]) def plug_idle(self, obj, writable = True, readable = True): if obj.fd == -1: diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py index 9a60cb9c5..f110e4371 100644 --- a/src/common/xmpp/transports_nb.py +++ b/src/common/xmpp/transports_nb.py @@ -46,25 +46,17 @@ def urisplit(uri): return proto, host, path def get_proxy_data_from_dict(proxy): + tcp_host, tcp_port, proxy_user, proxy_pass = None, None, None, None type = proxy['type'] - # with http-connect/socks5 proxy, we do tcp connecting to the proxy machine - tcp_host, tcp_port = proxy['host'], proxy['port'] - if type == 'bosh': - # in ['host'] is whole URI - tcp_host = urisplit(proxy['host'])[1] - # in BOSH, client connects to Connection Manager instead of directly to - # XMPP server ((hostname, port)). If HTTP Proxy is specified, client connects - # to HTTP proxy and Connection Manager is specified at URI and Host header - # in HTTP message - if proxy.has_key('proxy_host') and proxy.has_key('proxy_port'): - tcp_host, tcp_port = proxy['proxy_host'], proxy['proxy_port'] - - # user and pass for socks5/http_connect proxy. In case of BOSH, it's user and - # pass for http proxy - If there's no proxy_host they won't be used - if proxy.has_key('user'): proxy_user = proxy['user'] - else: proxy_user = None - if proxy.has_key('pass'): proxy_pass = proxy['pass'] - else: proxy_pass = None + if type == 'bosh' and not proxy['bosh_useproxy']: + # with BOSH not over proxy we have to parse the hostname from BOSH URI + tcp_host, tcp_port = urisplit(proxy['bosh_uri'])[1], proxy['bosh_port'] + else: + # with proxy!=bosh or with bosh over HTTP proxy we're connecting to proxy + # machine + tcp_host, tcp_port = proxy['host'], proxy['port'] + if proxy['useauth']: + proxy_user, proxy_pass = proxy['user'], proxy['pass'] return tcp_host, tcp_port, proxy_user, proxy_pass @@ -104,7 +96,7 @@ class NonBlockingTransport(PlugIn): self.port = None self.state = DISCONNECTED self._exported_methods=[self.disconnect, self.onreceive, self.set_send_timeout, - self.set_timeout, self.remove_timeout] + self.set_timeout, self.remove_timeout, self.start_disconnect] # time to wait for SOME stanza to come and then send keepalive self.sendtimeout = 0 @@ -152,10 +144,10 @@ class NonBlockingTransport(PlugIn): self.on_connect_failure(err_message=err_message) def send(self, raw_data, now=False): - if self.state != CONNECTED: - log.error('Trying to send %s when state is %s.' % + if self.state not in [CONNECTED]: + log.error('Unable to send %s \n because state is %s.' % (raw_data, self.state)) - return + def disconnect(self, do_callback=True): self.set_state(DISCONNECTED) @@ -205,6 +197,10 @@ class NonBlockingTransport(PlugIn): else: self.on_timeout = None + def start_disconnect(self): + self.set_state(DISCONNECTING) + + class NonBlockingTCP(NonBlockingTransport, IdleObject): ''' @@ -221,8 +217,12 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): # bytes remained from the last send message self.sendbuff = '' - + self.terminator = '' + + def start_disconnect(self): + self.send('') + NonBlockingTransport.start_disconnect(self) def connect(self, conn_5tuple, on_connect, on_connect_failure): ''' @@ -316,7 +316,7 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): def disconnect(self, do_callback=True): if self.state == DISCONNECTED: return - self.set_state(DISCONNECTING) + self.set_state(DISCONNECTED) self.idlequeue.unplug_idle(self.fd) try: self._sock.shutdown(socket.SHUT_RDWR) @@ -342,7 +342,7 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): def set_timeout(self, timeout): - if self.state in [CONNECTING, CONNECTED] and self.fd != -1: + if self.state != DISCONNECTED and self.fd != -1: NonBlockingTransport.set_timeout(self, timeout) else: log.warn('set_timeout: TIMEOUT NOT SET: state is %s, fd is %s' % (self.state, self.fd)) @@ -394,6 +394,9 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): if not self.sendbuff: if not self.sendqueue: log.warn('calling send on empty buffer and queue') + self._plug_idle( + writable= ((self.sendqueue!=[]) or (self.sendbuff!='')), + readable=True) return None self.sendbuff = self.sendqueue.pop(0) try: @@ -402,7 +405,7 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): sent_data = self.sendbuff[:send_count] self.sendbuff = self.sendbuff[send_count:] self._plug_idle( - writable=self.sendqueue or self.sendbuff, + writable= ((self.sendqueue!=[]) or (self.sendbuff!='')), readable=True) self.raise_event(DATA_SENT, sent_data) @@ -477,7 +480,8 @@ class NonBlockingHTTP(NonBlockingTCP): ''' def __init__(self, raise_event, on_disconnect, idlequeue, on_http_request_possible, - http_uri, http_port, http_version='HTTP/1.1', http_persistent=False): + http_uri, http_port, on_persistent_fallback, http_version='HTTP/1.1', + http_persistent=False): NonBlockingTCP.__init__(self, raise_event, on_disconnect, idlequeue) @@ -493,22 +497,29 @@ class NonBlockingHTTP(NonBlockingTCP): self.recvbuff = '' self.expected_length = 0 self.pending_requests = 0 + self.on_persistent_fallback = on_persistent_fallback self.on_http_request_possible = on_http_request_possible + self.just_responed = False + self.last_recv_time = 0 def send(self, raw_data, now=False): NonBlockingTCP.send( self, self.build_http_message(raw_data), now) - self.pending_requests += 1 def on_remote_disconnect(self): + log.warn('on_remote_disconnect called, http_persistent = %s' % self.http_persistent) if self.http_persistent: self.http_persistent = False + self.on_persistent_fallback() self.disconnect(do_callback=False) self.connect( conn_5tuple = self.conn_5tuple, + # after connect, the socket will be plugged as writable - pollout will be + # called, and since there are still data in sendbuff, _do_send will be + # called and sendbuff will be flushed on_connect = lambda: self._plug_idle(writable=True, readable=True), on_connect_failure = self.disconnect) @@ -534,20 +545,21 @@ class NonBlockingHTTP(NonBlockingTCP): if self.expected_length > len(self.recvbuff): # If we haven't received the whole HTTP mess yet, let's end the thread. # It will be finnished from one of following polls (io_watch) on plugged socket. - log.info('not enough bytes - %d expected, %d got' % (self.expected_length, len(self.recvbuff))) + log.info('not enough bytes in HTTP response - %d expected, %d got' % + (self.expected_length, len(self.recvbuff))) return - # all was received, now call the on_receive callback + # everything was received httpbody = self.recvbuff self.recvbuff='' self.expected_length=0 - self.pending_requests -= 1 - assert(self.pending_requests >= 0) + if not self.http_persistent: # not-persistent connections disconnect after response self.disconnect(do_callback = False) - self.on_receive(httpbody) + self.last_recv_time = time.time() + self.on_receive(data=httpbody, socket=self) self.on_http_request_possible() @@ -563,6 +575,9 @@ class NonBlockingHTTP(NonBlockingTCP): 'Host: %s:%s' % (self.http_host, self.http_port), 'Content-Type: text/xml; charset=utf-8', 'Content-Length: %s' % len(str(httpbody)), + 'Proxy-Connection: keep-alive', + 'Pragma: no-cache', + 'Accept-Encoding: gzip, deflate', '\r\n'] headers = '\r\n'.join(headers) return('%s%s\r\n' % (headers, httpbody)) @@ -587,6 +602,35 @@ class NonBlockingHTTP(NonBlockingTCP): headers[row[0][:-1]] = row[1] return (statusline, headers, httpbody) +class NonBlockingHTTPBOSH(NonBlockingHTTP): + + + def set_stanza_build_cb(self, build_cb): + self.build_cb = build_cb + + def _do_send(self): + if not self.sendbuff: + stanza = self.build_cb(socket=self) + stanza = self.build_http_message(httpbody=stanza) + if isinstance(stanza, unicode): + stanza = stanza.encode('utf-8') + elif not isinstance(stanza, str): + stanza = ustr(stanza).encode('utf-8') + self.sendbuff = stanza + try: + send_count = self._send(self.sendbuff) + if send_count: + sent_data = self.sendbuff[:send_count] + self.sendbuff = self.sendbuff[send_count:] + self._plug_idle(writable = self.sendbuff != '', readable = True) + self.raise_event(DATA_SENT, sent_data) + + except socket.error, e: + log.error('_do_send:', exc_info=True) + traceback.print_exc() + self.disconnect() + + class NBProxySocket(NonBlockingTCP): ''' @@ -663,7 +707,6 @@ class NBHTTPProxySocket(NBProxySocket): self.after_proxy_connect() #self.onreceive(self._on_proxy_auth) - # FIXME: find out what it this method for def _on_proxy_auth(self, reply): if self.reply.find('\n\n') == -1: if reply is None: diff --git a/src/config.py b/src/config.py index c678599e8..98b24a0c2 100644 --- a/src/config.py +++ b/src/config.py @@ -1101,9 +1101,30 @@ class ManageProxiesWindow: self.proxies_treeview = self.xml.get_widget('proxies_treeview') self.proxyname_entry = self.xml.get_widget('proxyname_entry') self.proxytype_combobox = self.xml.get_widget('proxytype_combobox') + self.init_list() self.xml.signal_autoconnect(self) self.window.show_all() + # hide the BOSH fields by default + self.show_bosh_fields() + + def show_bosh_fields(self, show=True): + if show: + self.xml.get_widget('boshuri_entry').show() + self.xml.get_widget('boshport_entry').show() + self.xml.get_widget('boshuri_label').show() + self.xml.get_widget('boshport_label').show() + self.xml.get_widget('boshuseproxy_checkbutton').show() + else: + cb = self.xml.get_widget('boshuseproxy_checkbutton') + cb.hide() + cb.set_active(True) + self.on_boshuseproxy_checkbutton_toggled(cb) + self.xml.get_widget('boshuri_entry').hide() + self.xml.get_widget('boshport_entry').hide() + self.xml.get_widget('boshuri_label').hide() + self.xml.get_widget('boshport_label').hide() + def fill_proxies_treeview(self): model = self.proxies_treeview.get_model() @@ -1158,9 +1179,18 @@ class ManageProxiesWindow: def on_useauth_checkbutton_toggled(self, widget): act = widget.get_active() + proxy = self.proxyname_entry.get_text().decode('utf-8') + gajim.config.set_per('proxies', proxy, 'useauth', act) self.xml.get_widget('proxyuser_entry').set_sensitive(act) self.xml.get_widget('proxypass_entry').set_sensitive(act) + def on_boshuseproxy_checkbutton_toggled(self, widget): + act = widget.get_active() + proxy = self.proxyname_entry.get_text().decode('utf-8') + gajim.config.set_per('proxies', proxy, 'bosh_useproxy', act) + self.xml.get_widget('proxyhost_entry').set_sensitive(act) + self.xml.get_widget('proxyport_entry').set_sensitive(act) + def on_proxies_treeview_cursor_changed(self, widget): #FIXME: check if off proxy settings are correct (see # http://trac.gajim.org/changeset/1921#file2 line 1221 @@ -1173,19 +1203,33 @@ class ManageProxiesWindow: proxyport_entry = self.xml.get_widget('proxyport_entry') proxyuser_entry = self.xml.get_widget('proxyuser_entry') proxypass_entry = self.xml.get_widget('proxypass_entry') + boshuri_entry = self.xml.get_widget('boshuri_entry') + boshport_entry = self.xml.get_widget('boshport_entry') useauth_checkbutton = self.xml.get_widget('useauth_checkbutton') + boshuseproxy_checkbutton = self.xml.get_widget('boshuseproxy_checkbutton') proxyhost_entry.set_text('') proxyport_entry.set_text('') proxyuser_entry.set_text('') proxypass_entry.set_text('') - useauth_checkbutton.set_active(False) - self.on_useauth_checkbutton_toggled(useauth_checkbutton) + boshuri_entry.set_text('') + + #boshuseproxy_checkbutton.set_active(False) + #self.on_boshuseproxy_checkbutton_toggled(boshuseproxy_checkbutton) + + #useauth_checkbutton.set_active(False) + #self.on_useauth_checkbutton_toggled(useauth_checkbutton) + if proxy == _('None'): # special proxy None + self.show_bosh_fields(False) self.proxyname_entry.set_editable(False) self.xml.get_widget('remove_proxy_button').set_sensitive(False) self.xml.get_widget('proxytype_combobox').set_sensitive(False) self.xml.get_widget('proxy_table').set_sensitive(False) else: + proxytype = gajim.config.get_per('proxies', proxy, 'type') + + self.show_bosh_fields(proxytype=='bosh') + self.proxyname_entry.set_editable(True) self.xml.get_widget('remove_proxy_button').set_sensitive(True) self.xml.get_widget('proxytype_combobox').set_sensitive(True) @@ -1198,11 +1242,16 @@ class ManageProxiesWindow: 'user')) proxypass_entry.set_text(gajim.config.get_per('proxies', proxy, 'pass')) - proxytype = gajim.config.get_per('proxies', proxy, 'type') + boshuri_entry.set_text(gajim.config.get_per('proxies', proxy, + 'bosh_uri')) + boshport_entry.set_text(unicode(gajim.config.get_per('proxies', proxy, + 'bosh_port'))) types = ['http', 'socks5', 'bosh'] self.proxytype_combobox.set_active(types.index(proxytype)) - if gajim.config.get_per('proxies', proxy, 'user'): - useauth_checkbutton.set_active(True) + boshuseproxy_checkbutton.set_active( + gajim.config.get_per('proxies', proxy, 'bosh_useproxy')) + useauth_checkbutton.set_active( + gajim.config.get_per('proxies', proxy, 'useauth')) def on_proxies_treeview_key_press_event(self, widget, event): if event.keyval == gtk.keysyms.Delete: @@ -1229,6 +1278,7 @@ class ManageProxiesWindow: def on_proxytype_combobox_changed(self, widget): types = ['http', 'socks5', 'bosh'] type_ = self.proxytype_combobox.get_active() + self.show_bosh_fields(types[type_]=='bosh') proxy = self.proxyname_entry.get_text().decode('utf-8') gajim.config.set_per('proxies', proxy, 'type', types[type_]) @@ -1247,6 +1297,16 @@ class ManageProxiesWindow: proxy = self.proxyname_entry.get_text().decode('utf-8') gajim.config.set_per('proxies', proxy, 'user', value) + def on_boshuri_entry_changed(self, widget): + value = widget.get_text().decode('utf-8') + proxy = self.proxyname_entry.get_text().decode('utf-8') + gajim.config.set_per('proxies', proxy, 'bosh_uri', value) + + def on_boshport_entry_changed(self, widget): + value = widget.get_text().decode('utf-8') + proxy = self.proxyname_entry.get_text().decode('utf-8') + gajim.config.set_per('proxies', proxy, 'bosh_port', value) + def on_proxypass_entry_changed(self, widget): value = widget.get_text().decode('utf-8') proxy = self.proxyname_entry.get_text().decode('utf-8') diff --git a/src/gajim.py b/src/gajim.py index fcb069016..748810a3b 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -50,7 +50,7 @@ import logging consoleloghandler = logging.StreamHandler() consoleloghandler.setLevel(1) consoleloghandler.setFormatter( - logging.Formatter('%(name)s: %(levelname)s: %(message)s') + logging.Formatter('%(asctime)s %(name)s: %(levelname)s: %(message)s') ) log = logging.getLogger('gajim') log.setLevel(logging.WARNING) From 56e0ad7a96125f6895a1ce30323f93813fd43d6a Mon Sep 17 00:00:00 2001 From: tomk Date: Mon, 28 Jul 2008 18:53:00 +0000 Subject: [PATCH 14/20] basic proxy authentication for BOSH connections implemented --- src/common/connection.py | 1 - src/common/xmpp/bosh.py | 22 ++++++++++++-------- src/common/xmpp/client_nb.py | 16 ++++++++------- src/common/xmpp/transports_nb.py | 35 ++++++++++++++++++++------------ 4 files changed, 45 insertions(+), 29 deletions(-) diff --git a/src/common/connection.py b/src/common/connection.py index 7df8b4a71..fc6a71a56 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -431,7 +431,6 @@ class Connection(ConnectionHandlers): proxy = {} proxyptr = gajim.config.get_per('proxies',p) for key in proxyptr.keys(): proxy[key]=proxyptr[key][1] - print proxy elif gajim.config.get_per('accounts', self.name, 'use_env_http_proxy'): try: diff --git a/src/common/xmpp/bosh.py b/src/common/xmpp/bosh.py index c29fa7294..062084d1c 100644 --- a/src/common/xmpp/bosh.py +++ b/src/common/xmpp/bosh.py @@ -20,7 +20,7 @@ In TCP-derived transports it is file descriptor of socket''' class NonBlockingBOSH(NonBlockingTransport): def __init__(self, raise_event, on_disconnect, idlequeue, xmpp_server, domain, - bosh_dict): + bosh_dict, proxy_creds): NonBlockingTransport.__init__(self, raise_event, on_disconnect, idlequeue) self.bosh_sid = None @@ -42,7 +42,9 @@ class NonBlockingBOSH(NonBlockingTransport): self.bosh_uri = bosh_dict['bosh_uri'] self.bosh_port = bosh_dict['bosh_port'] self.bosh_content = bosh_dict['bosh_content'] - + self.over_proxy = bosh_dict['bosh_useproxy'] + self.use_proxy_auth = bosh_dict['useauth'] + self.proxy_creds = proxy_creds self.wait_cb_time = None self.http_socks = [] self.stanza_buffer = [] @@ -289,8 +291,6 @@ class NonBlockingBOSH(NonBlockingTransport): return t - - def connect_and_flush(self, socket): socket.connect( conn_5tuple = self.conn_5tuple, @@ -334,15 +334,21 @@ class NonBlockingBOSH(NonBlockingTransport): def get_new_http_socket(self): + http_dict = {'http_uri': self.bosh_uri, + 'http_port': self.bosh_port, + 'http_version': self.http_version, + 'http_persistent': self.http_persistent, + 'over_proxy': self.over_proxy} + if self.use_proxy_auth: + http_dict['proxy_user'], http_dict['proxy_pass'] = self.proxy_creds + + s = NonBlockingHTTPBOSH( raise_event=self.raise_event, on_disconnect=self.disconnect, idlequeue = self.idlequeue, on_http_request_possible = self.on_http_request_possible, - http_uri = self.bosh_uri, - http_port = self.bosh_port, - http_version = self.http_version, - http_persistent = self.http_persistent, + http_dict = http_dict, on_persistent_fallback = self.on_persistent_fallback) s.onreceive(self.on_received_http) s.set_stanza_build_cb(self.build_stanza) diff --git a/src/common/xmpp/client_nb.py b/src/common/xmpp/client_nb.py index eb44d4665..8e15f3672 100644 --- a/src/common/xmpp/client_nb.py +++ b/src/common/xmpp/client_nb.py @@ -71,6 +71,7 @@ class NBCommonClient: self.connected='' log.debug('Client disconnected..') + print 'ffffffffffffffffff' for i in reversed(self.disconnect_handlers): log.debug('Calling disconnect handler %s' % i) i() @@ -393,10 +394,11 @@ class NonBlockingClient(NBCommonClient): if proxy['type'] == 'bosh': self.socket = bosh.NonBlockingBOSH( - on_disconnect=self.on_disconnect, + on_disconnect = self.on_disconnect, raise_event = self.raise_event, idlequeue = self.idlequeue, - xmpp_server=(xmpp_hostname, self.Port), + proxy_creds = (proxy_user, proxy_pass), + xmpp_server = (xmpp_hostname, self.Port), domain = self.Server, bosh_dict = proxy) self.protocol_type = 'BOSH' @@ -408,19 +410,19 @@ class NonBlockingClient(NBCommonClient): elif proxy['type'] == 'http': proxy_class = transports_nb.NBHTTPProxySocket self.socket = proxy_class( - on_disconnect=self.on_disconnect, + on_disconnect = self.on_disconnect, raise_event = self.raise_event, idlequeue = self.idlequeue, - proxy_creds=(proxy_user, proxy_pass), - xmpp_server=(xmpp_hostname, self.Port)) + proxy_creds = (proxy_user, proxy_pass), + xmpp_server = (xmpp_hostname, self.Port)) else: self._on_tcp_failure = self._on_connect_failure tcp_host=xmpp_hostname tcp_port=self.Port self.socket = transports_nb.NonBlockingTCP( + on_disconnect = self.on_disconnect, raise_event = self.raise_event, - idlequeue = self.idlequeue, - on_disconnect = self.on_disconnect) + idlequeue = self.idlequeue) self.socket.PlugIn(self) diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py index f110e4371..29b95124f 100644 --- a/src/common/xmpp/transports_nb.py +++ b/src/common/xmpp/transports_nb.py @@ -480,19 +480,24 @@ class NonBlockingHTTP(NonBlockingTCP): ''' def __init__(self, raise_event, on_disconnect, idlequeue, on_http_request_possible, - http_uri, http_port, on_persistent_fallback, http_version='HTTP/1.1', - http_persistent=False): + on_persistent_fallback, http_dict): NonBlockingTCP.__init__(self, raise_event, on_disconnect, idlequeue) - self.http_protocol, self.http_host, self.http_path = urisplit(http_uri) + self.http_protocol, self.http_host, self.http_path = urisplit(http_dict['http_uri']) if self.http_protocol is None: self.http_protocol = 'http' if self.http_path == '': - http_path = '/' - self.http_port = http_port - self.http_version = http_version - self.http_persistent = http_persistent + self.http_path = '/' + self.http_port = http_dict['http_port'] + self.http_version = http_dict['http_version'] + self.http_persistent = http_dict['http_persistent'] + self.over_proxy = http_dict['over_proxy'] + if http_dict.has_key('proxy_user') and http_dict.has_key('proxy_pass'): + self.proxy_user, self.proxy_pass = http_dict['proxy_user'], http_dict['proxy_pass'] + else: + self.proxy_user, self.proxy_pass = None, None + # buffer for partial responses self.recvbuff = '' self.expected_length = 0 @@ -574,11 +579,15 @@ class NonBlockingHTTP(NonBlockingTCP): headers = ['%s %s %s' % (method, absolute_uri, self.http_version), 'Host: %s:%s' % (self.http_host, self.http_port), 'Content-Type: text/xml; charset=utf-8', - 'Content-Length: %s' % len(str(httpbody)), - 'Proxy-Connection: keep-alive', - 'Pragma: no-cache', - 'Accept-Encoding: gzip, deflate', - '\r\n'] + 'Content-Length: %s' % len(str(httpbody))] + if self.over_proxy: + headers.append('Proxy-Connection: keep-alive') + headers.append('Pragma: no-cache') + if self.proxy_user and self.proxy_pass: + credentials = '%s:%s' % (self.proxy_user, self.proxy_pass) + credentials = base64.encodestring(credentials).strip() + headers.append('Proxy-Authorization: Basic %s' % credentials) + headers.append('\r\n') headers = '\r\n'.join(headers) return('%s%s\r\n' % (headers, httpbody)) @@ -678,7 +687,7 @@ class NBHTTPProxySocket(NBProxySocket): 'Proxy-Connection: Keep-Alive', 'Pragma: no-cache', 'Host: %s:%s' % self.xmpp_server, - 'User-Agent: HTTPPROXYsocket/v0.1'] + 'User-Agent: Gajim'] if self.proxy_user and self.proxy_pass: credentials = '%s:%s' % (self.proxy_user, self.proxy_pass) credentials = base64.encodestring(credentials).strip() From cbfa9d97df478d912d3b96bf9cff914d13c5d97f Mon Sep 17 00:00:00 2001 From: tomk Date: Tue, 5 Aug 2008 23:52:35 +0000 Subject: [PATCH 15/20] - TLS classes refactored - NonBlockingTLS is now plugged to NonBlockingTCP and derived (was plugged to NonBlockingClient which made it unusable for BOSH) - Fixed HTTP CONNECT proxy socket - Implemented workaround for the bug with insecure-connection warning dialog (unfortunately, this is not over - I just forbid the transport to send BOSH empty bodies until auth module is plugged, which is wrong and will break if user will wait more than "inactivity" (usualy thirty) seconds before clicking the dialog. This workaround works with ejb and opf, and also breaks connection with both of them if delay is too long. - Implemented basic TLS over BOSH. It works only with OPF and poorly. --- src/common/connection.py | 142 ++++++++++++------------ src/common/connection_handlers.py | 2 +- src/common/xmpp/bosh.py | 76 ++++++++----- src/common/xmpp/client_nb.py | 103 +++++++++++------ src/common/xmpp/tls_nb.py | 177 ++++++++++-------------------- src/common/xmpp/transports_nb.py | 159 +++++++++++++++------------ src/gajim.py | 2 +- 7 files changed, 345 insertions(+), 316 deletions(-) diff --git a/src/common/connection.py b/src/common/connection.py index fc6a71a56..932a8c12a 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -397,7 +397,7 @@ class Connection(ConnectionHandlers): def connect(self, data = None): ''' Start a connection to the Jabber server. - Returns connection, and connection type ('tls', 'ssl', 'tcp', '') + Returns connection, and connection type ('tls', 'ssl', 'plain', '') data MUST contain hostname, usessl, proxy, use_custom_host, custom_host (if use_custom_host), custom_port (if use_custom_host)''' if self.connection: @@ -410,9 +410,11 @@ class Connection(ConnectionHandlers): p = data['proxy'] use_srv = True use_custom = data['use_custom_host'] + print 'use_custom = %s' % use_custom if use_custom: custom_h = data['custom_host'] custom_p = data['custom_port'] + print 'custom_port = %s' % custom_p else: hostname = gajim.config.get_per('accounts', self.name, 'hostname') usessl = gajim.config.get_per('accounts', self.name, 'usessl') @@ -422,8 +424,10 @@ class Connection(ConnectionHandlers): use_srv = gajim.config.get_per('accounts', self.name, 'use_srv') use_custom = gajim.config.get_per('accounts', self.name, 'use_custom_host') + print 'use_custom = %s' % use_custom custom_h = gajim.config.get_per('accounts', self.name, 'custom_host') custom_p = gajim.config.get_per('accounts', self.name, 'custom_port') + print 'custom_port = %s' % custom_p # create connection if it doesn't already exist self.connected = 1 @@ -502,68 +506,6 @@ class Connection(ConnectionHandlers): i['ssl_port'] = ssl_p self.connect_to_next_host() - def on_proxy_failure(self, reason): - log.error('Connection to proxy failed: %s' % reason) - self.time_to_reconnect = None - self.on_connect_failure = None - self.disconnect(on_purpose = True) - self.dispatch('STATUS', 'offline') - self.dispatch('CONNECTION_LOST', - (_('Connection to proxy failed'), reason)) - - def connect_to_next_type(self, retry=False): - if len(self._connection_types): - self._current_type = self._connection_types.pop(0) - if self.last_connection: - self.last_connection.socket.disconnect() - self.last_connection = None - self.connection = None - - if self._current_type == 'ssl': - # SSL (force TLS on different port than plain) - port = self._current_host['ssl_port'] - secure = 'force' - else: - port = self._current_host['port'] - if self._current_type == 'plain': - # plain connection - secure = None - else: - # TLS (on the same port as plain) - secure = 'negotiate' - - con = common.xmpp.NonBlockingClient( - domain=self._hostname, - caller=self, - idlequeue=gajim.idlequeue) - - self.last_connection = con - # increase default timeout for server responses - common.xmpp.dispatcher_nb.DEFAULT_TIMEOUT_SECONDS = self.try_connecting_for_foo_secs - # FIXME: this is a hack; need a better way - if self.on_connect_success == self._on_new_account: - con.RegisterDisconnectHandler(self._on_new_account) - - # FIXME: BOSH properties should be loaded from config - #if self._proxy and self._proxy['type'] == 'bosh': - # self._proxy['bosh_hold'] = '2' - # self._proxy['bosh_wait'] = '10' - # self._proxy['bosh_content'] = 'text/xml; charset=utf-8' - # self._proxy['wait_for_restart_response'] = False - - - log.info('Connecting to %s: [%s:%d]', self.name, - self._current_host['host'], port) - con.connect( - hostname=self._current_host['host'], - port=port, - on_connect=self.on_connect_success, - on_proxy_failure=self.on_proxy_failure, - on_connect_failure=self.connect_to_next_type, - proxy=self._proxy, - secure = secure) - else: - self.connect_to_next_host(retry) def connect_to_next_host(self, retry = False): if len(self._hosts): @@ -573,10 +515,14 @@ class Connection(ConnectionHandlers): 'connection_types').split() else: self._connection_types = ['tls', 'ssl', 'plain'] + #THEHACK + #self._connection_types = ['ssl', 'plain'] - # FIXME: remove after tls and ssl will be degubbed - self._connection_types = ['plain'] - + if self._proxy and self._proxy['type']=='bosh': + # with BOSH, we can't do TLS negotiation with , we do only "plain" + # connection and TLS with handshake right after TCP connecting ("ssl") + try: self._connection_types.remove('tls') + except ValueError: pass host = self.select_next_host(self._hosts) self._current_host = host @@ -597,6 +543,55 @@ class Connection(ConnectionHandlers): # try reconnect if connection has failed before auth to server self._disconnectedReconnCB() + def connect_to_next_type(self, retry=False): + if len(self._connection_types): + self._current_type = self._connection_types.pop(0) + if self.last_connection: + self.last_connection.socket.disconnect() + self.last_connection = None + self.connection = None + + if self._current_type == 'ssl': + # SSL (force TLS on different port than plain) + port = self._current_host['ssl_port'] + elif self._current_type == 'tls': + # TLS - negotiate tls after XMPP stream is estabilished + port = self._current_host['port'] + elif self._current_type == 'plain': + # plain connection on defined port + port = self._current_host['port'] + + cacerts = os.path.join(common.gajim.DATA_DIR, 'other', 'cacerts.pem') + mycerts = common.gajim.MY_CACERTS + secure_tuple = (self._current_type, cacerts, mycerts) + + con = common.xmpp.NonBlockingClient( + domain=self._hostname, + caller=self, + idlequeue=gajim.idlequeue) + + self.last_connection = con + # increase default timeout for server responses + common.xmpp.dispatcher_nb.DEFAULT_TIMEOUT_SECONDS = self.try_connecting_for_foo_secs + # FIXME: this is a hack; need a better way + if self.on_connect_success == self._on_new_account: + con.RegisterDisconnectHandler(self._on_new_account) + + log.info('Connecting to %s: [%s:%d]', self.name, + self._current_host['host'], port) + print secure_tuple + con.connect( + hostname=self._current_host['host'], + port=port, + on_connect=self.on_connect_success, + on_proxy_failure=self.on_proxy_failure, + on_connect_failure=self.connect_to_next_type, + proxy=self._proxy, + secure_tuple = secure_tuple) + else: + self.connect_to_next_host(retry) + + def _connect_failure(self, con_type = None): if not con_type: # we are not retrying, and not conecting @@ -607,14 +602,21 @@ class Connection(ConnectionHandlers): (_('Could not connect to "%s"') % self._hostname, _('Check your connection or try again later.'))) + + def on_proxy_failure(self, reason): + log.error('Connection to proxy failed: %s' % reason) + self.time_to_reconnect = None + self.on_connect_failure = None + self.disconnect(on_purpose = True) + self.dispatch('STATUS', 'offline') + self.dispatch('CONNECTION_LOST', + (_('Connection to proxy failed'), reason)) + def _connect_success(self, con, con_type): if not self.connected: # We went offline during connecting process # FIXME - not possible, maybe it was when we used threads return _con_type = con_type - # xmpp returns 'tcp', but we set 'plain' in connection_types in config - if _con_type == 'tcp': - _con_type = 'plain' if _con_type != self._current_type: log.info('Connecting to next type beacuse desired is %s and returned is %s' % (self._current_type, _con_type)) @@ -676,7 +678,7 @@ class Connection(ConnectionHandlers): def plain_connection_accepted(self): name = gajim.config.get_per('accounts', self.name, 'name') - self._register_handlers(self.connection, 'tcp') + self._register_handlers(self.connection, 'plain') self.connection.auth(name, self.password, self.server_resource, 1, self.__on_auth) diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index 25a771e05..687b7d9d1 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -207,7 +207,7 @@ class ConnectionBytestream: iq.setID(file_props['request-id']) query = iq.setTag('query') query.setNamespace(common.xmpp.NS_BYTESTREAM) - query.setAttr('mode', 'tcp') + query.setAttr('mode', 'plain') query.setAttr('sid', file_props['sid']) for ft_host in ft_add_hosts: # The streamhost, if set diff --git a/src/common/xmpp/bosh.py b/src/common/xmpp/bosh.py index 062084d1c..d89cd6a50 100644 --- a/src/common/xmpp/bosh.py +++ b/src/common/xmpp/bosh.py @@ -19,9 +19,10 @@ In TCP-derived transports it is file descriptor of socket''' class NonBlockingBOSH(NonBlockingTransport): - def __init__(self, raise_event, on_disconnect, idlequeue, xmpp_server, domain, - bosh_dict, proxy_creds): - NonBlockingTransport.__init__(self, raise_event, on_disconnect, idlequeue) + def __init__(self, raise_event, on_disconnect, idlequeue, estabilish_tls, certs, + xmpp_server, domain, bosh_dict, proxy_creds): + NonBlockingTransport.__init__(self, raise_event, on_disconnect, idlequeue, + estabilish_tls, certs) self.bosh_sid = None if locale.getdefaultlocale()[0]: @@ -37,12 +38,19 @@ class NonBlockingBOSH(NonBlockingTransport): self.route_host, self.route_port = xmpp_server self.bosh_wait = bosh_dict['bosh_wait'] - self.bosh_hold = bosh_dict['bosh_hold'] + if not self.http_pipelining: + self.bosh_hold = 1 + else: + self.bosh_hold = bosh_dict['bosh_hold'] self.bosh_requests = self.bosh_hold self.bosh_uri = bosh_dict['bosh_uri'] self.bosh_port = bosh_dict['bosh_port'] self.bosh_content = bosh_dict['bosh_content'] self.over_proxy = bosh_dict['bosh_useproxy'] + if estabilish_tls: + self.bosh_secure = 'true' + else: + self.bosh_secure = 'false' self.use_proxy_auth = bosh_dict['useauth'] self.proxy_creds = proxy_creds self.wait_cb_time = None @@ -55,7 +63,6 @@ class NonBlockingBOSH(NonBlockingTransport): self.ack_checker = None self.after_init = False - # if proxy_host .. do sth about HTTP proxy etc. def connect(self, conn_5tuple, on_connect, on_connect_failure): NonBlockingTransport.connect(self, conn_5tuple, on_connect, on_connect_failure) @@ -72,7 +79,7 @@ class NonBlockingBOSH(NonBlockingTransport): self.after_init = True self.http_socks.append(self.get_new_http_socket()) - self.tcp_connection_started() + self.tcp_connecting_started() # following connect() is not necessary because sockets can be connected on # send but we need to know if host is reachable in order to invoke callback @@ -80,14 +87,21 @@ class NonBlockingBOSH(NonBlockingTransport): # for errors occurring after connection is etabilished) self.http_socks[0].connect( conn_5tuple = conn_5tuple, - on_connect = lambda: self._on_connect(self.http_socks[0]), + on_connect = lambda: self._on_connect(), on_connect_failure = self._on_connect_failure) + def _on_connect(self): + self.peerhost = self.http_socks[0].peerhost + self.ssl_lib = self.http_socks[0].ssl_lib + NonBlockingTransport._on_connect(self) + + + def set_timeout(self, timeout): - if self.state in [CONNECTING, CONNECTED] and self.fd != -1: + if self.get_state() in [CONNECTING, CONNECTED] and self.fd != -1: NonBlockingTransport.set_timeout(self, timeout) else: - log.warn('set_timeout: TIMEOUT NOT SET: state is %s, fd is %s' % (self.state, self.fd)) + log.warn('set_timeout: TIMEOUT NOT SET: state is %s, fd is %s' % (self.get_state(), self.fd)) def on_http_request_possible(self): ''' @@ -95,17 +109,25 @@ class NonBlockingBOSH(NonBlockingTransport): There should be always one pending request on BOSH CM. ''' log.info('on_http_req possible, state:\n%s' % self.get_current_state()) - if self.state == DISCONNECTING: + if self.get_state() == DISCONNECTING: self.disconnect() return - self.send_BOSH(None) + + print 'SSSSSSSSSSEEEEEEEEEND' + if hasattr(self._owner, 'NonBlockingNonSASL') or hasattr(self._owner, 'SASL'): + #FIXME: Hack for making the non-secure warning dialog work + self.send_BOSH(None) + else: + self.http_socks[0]._plug_idle(writable=False, readable=True) + return def get_socket_in(self, state): for s in self.http_socks: - if s.state==state: return s + if s.get_state()==state: return s return None + def get_free_socket(self): if self.http_pipelining: assert( len(self.http_socks) == 1 ) @@ -114,8 +136,8 @@ class NonBlockingBOSH(NonBlockingTransport): last_recv_time, tmpsock = 0, None for s in self.http_socks: # we're interested only into CONNECTED socket with no req pending - if s.state==CONNECTED and s.pending_requests==0: - # if there's more of them, we want the one with less recent data receive + if s.get_state()==CONNECTED and s.pending_requests==0: + # if there's more of them, we want the one with the least recent data receive # (lowest last_recv_time) if (last_recv_time==0) or (s.last_recv_time < last_recv_time): last_recv_time = s.last_recv_time @@ -129,13 +151,14 @@ class NonBlockingBOSH(NonBlockingTransport): def send_BOSH(self, payload): total_pending_reqs = sum([s.pending_requests for s in self.http_socks]) - # when called after HTTP response when there are some pending requests and - # no data to send, we do nothing and disccard the payload + # when called after HTTP response (Payload=None) and when there are already + # some pending requests and no data to send, or when the socket is + # disconnected, we do nothing if payload is None and \ total_pending_reqs > 0 and \ self.stanza_buffer == [] and \ self.prio_bosh_stanzas == [] or \ - self.state==DISCONNECTED: + self.get_state()==DISCONNECTED: return # now the payload is put to buffer and will be sent at some point @@ -144,7 +167,7 @@ class NonBlockingBOSH(NonBlockingTransport): # if we're about to make more requests than allowed, we don't send - stanzas will be # sent after HTTP response from CM, exception is when we're disconnecting - then we # send anyway - if total_pending_reqs >= self.bosh_requests and self.state!=DISCONNECTING: + if total_pending_reqs >= self.bosh_requests and self.get_state()!=DISCONNECTING: log.warn('attemp to make more requests than allowed by Connection Manager:\n%s' % self.get_current_state()) return @@ -232,17 +255,16 @@ class NonBlockingBOSH(NonBlockingTransport): self.remove_bosh_wait_timeout() if self.after_init: - self.after_init = False if stanza_attrs.has_key('sid'): # session ID should be only in init response self.bosh_sid = stanza_attrs['sid'] if stanza_attrs.has_key('requests'): - #self.bosh_requests = int(stanza_attrs['requests']) - self.bosh_requests = int(stanza_attrs['wait']) + self.bosh_requests = int(stanza_attrs['requests']) if stanza_attrs.has_key('wait'): self.bosh_wait = int(stanza_attrs['wait']) + self.after_init = False ack = None if stanza_attrs.has_key('ack'): @@ -267,13 +289,12 @@ class NonBlockingBOSH(NonBlockingTransport): def append_stanza(self, stanza): if stanza: if isinstance(stanza, tuple): - # tuple of BOSH stanza and True/False for whether to add payload + # stanza is tuple of BOSH stanza and bool value for whether to add payload self.prio_bosh_stanzas.append(stanza) else: self.stanza_buffer.append(stanza) - def send(self, stanza, now=False): # body tags should be send only via send_BOSH() assert(not isinstance(stanza, BOSHBody)) @@ -284,7 +305,7 @@ class NonBlockingBOSH(NonBlockingTransport): def get_current_state(self): t = '------ SOCKET_ID\tSOCKET_STATE\tPENDING_REQS\n' for s in self.http_socks: - t = '%s------ %s\t%s\t%s\n' % (t,id(s), s.state, s.pending_requests) + t = '%s------ %s\t%s\t%s\n' % (t,id(s), s.get_state(), s.pending_requests) t = '%s------ prio stanzas: %s, queued XMPP stanzas: %s, not_acked stanzas: %s' \ % (t, self.prio_bosh_stanzas, self.stanza_buffer, self.ack_checker.get_not_acked_rids()) @@ -294,7 +315,7 @@ class NonBlockingBOSH(NonBlockingTransport): def connect_and_flush(self, socket): socket.connect( conn_5tuple = self.conn_5tuple, - on_connect = lambda :self.send_BOSH(None), + on_connect = self.on_http_request_possible, on_connect_failure = self.disconnect) @@ -313,6 +334,7 @@ class NonBlockingBOSH(NonBlockingTransport): 'sid': self.bosh_sid, 'xml:lang': self.bosh_xml_lang, 'xmpp:restart': 'true', + 'secure': self.bosh_secure, 'xmlns:xmpp': 'urn:xmpp:xbosh'}) else: t = BOSHBody( @@ -347,6 +369,8 @@ class NonBlockingBOSH(NonBlockingTransport): raise_event=self.raise_event, on_disconnect=self.disconnect, idlequeue = self.idlequeue, + estabilish_tls = self.estabilish_tls, + certs = self.certs, on_http_request_possible = self.on_http_request_possible, http_dict = http_dict, on_persistent_fallback = self.on_persistent_fallback) @@ -368,7 +392,7 @@ class NonBlockingBOSH(NonBlockingTransport): def disconnect(self, do_callback=True): self.remove_bosh_wait_timeout() - if self.state == DISCONNECTED: return + if self.get_state() == DISCONNECTED: return self.fd = -1 for s in self.http_socks: s.disconnect(do_callback=False) diff --git a/src/common/xmpp/client_nb.py b/src/common/xmpp/client_nb.py index 8e15f3672..7e9e347bf 100644 --- a/src/common/xmpp/client_nb.py +++ b/src/common/xmpp/client_nb.py @@ -26,6 +26,8 @@ import socket import transports_nb, tls_nb, dispatcher_nb, auth_nb, roster_nb, protocol, bosh from client import * +from protocol import NS_TLS + import logging log = logging.getLogger('gajim.c.x.client_nb') @@ -71,7 +73,6 @@ class NBCommonClient: self.connected='' log.debug('Client disconnected..') - print 'ffffffffffffffffff' for i in reversed(self.disconnect_handlers): log.debug('Calling disconnect handler %s' % i) i() @@ -86,9 +87,9 @@ class NBCommonClient: if self.__dict__.has_key('NonBlockingTLS'): self.NonBlockingTLS.PlugOut() if self.__dict__.has_key('NBHTTPProxySocket'): - self.NBHTTPPROXYsocket.PlugOut() + self.NBHTTPProxySocket.PlugOut() if self.__dict__.has_key('NBSOCKS5ProxySocket'): - self.NBSOCKS5PROXYsocket.PlugOut() + self.NBSOCKS5ProxySocket.PlugOut() if self.__dict__.has_key('NonBlockingTCP'): self.NonBlockingTCP.PlugOut() if self.__dict__.has_key('NonBlockingHTTP'): @@ -98,7 +99,7 @@ class NBCommonClient: def connect(self, on_connect, on_connect_failure, hostname=None, port=5222, - on_proxy_failure=None, proxy=None, secure=None): + on_proxy_failure=None, proxy=None, secure_tuple=None): ''' Open XMPP connection (open XML streams in both directions). :param hostname: hostname of XMPP server from SRV request @@ -110,17 +111,15 @@ class NBCommonClient: :param proxy: dictionary with proxy data. It should contain at least values for keys 'host' and 'port' - connection details for proxy server and optionally keys 'user' and 'pass' as proxy credentials - :param secure: + :param secure_tuple: ''' self.on_connect = on_connect self.on_connect_failure=on_connect_failure self.on_proxy_failure = on_proxy_failure - self._secure = secure + self.secure, self.cacerts, self.mycerts = secure_tuple self.Connection = None self.Port = port - - def _resolve_hostname(self, hostname, port, on_success, on_failure): @@ -147,7 +146,7 @@ class NBCommonClient: self.current_ip = self.ip_addresses.pop(0) self.socket.connect( conn_5tuple=self.current_ip, - on_connect=lambda: self._xmpp_connect(socket_type='tcp'), + on_connect=lambda: self._xmpp_connect(socket_type='plain'), on_connect_failure=self._try_next_ip) @@ -159,6 +158,8 @@ class NBCommonClient: return None def _xmpp_connect(self, socket_type): + if socket_type == 'plain' and self.Connection.ssl_lib: + socket_type = 'ssl' self.connected = socket_type self._xmpp_connect_machine() @@ -169,7 +170,6 @@ class NBCommonClient: and features tag handling. Calls _on_stream_start when stream is started, and _on_connect_failure on failure. ''' - #FIXME: use RegisterHandlerOnce instead of onreceive log.info('-------------xmpp_connect_machine() >> mode: %s, data: %s' % (mode,str(data)[:20] )) def on_next_receive(mode): @@ -181,6 +181,7 @@ class NBCommonClient: if not mode: # starting state + if self.__dict__.has_key('Dispatcher'): self.Dispatcher.PlugOut() d=dispatcher_nb.Dispatcher().PlugIn(self) on_next_receive('RECEIVE_DOCUMENT_ATTRIBUTES') @@ -222,11 +223,38 @@ class NBCommonClient: elif mode == 'STREAM_STARTED': self._on_stream_start() + + def _tls_negotiation_handler(self, con=None, tag=None): + log.info('-------------tls_negotiaton_handler() >> tag: %s' % tag) + if not con and not tag: + # starting state when we send the + self.RegisterHandlerOnce('proceed', self._tls_negotiation_handler, + xmlns=NS_TLS) + self.RegisterHandlerOnce('failure', self._tls_negotiation_handler, + xmlns=NS_TLS) + self.send('' % NS_TLS) + else: + if tag.getNamespace() <> NS_TLS: + self._on_connect_failure('Unknown namespace: %s' % tag.getNamespace()) + return + tagname = tag.getName() + if tagname == 'failure': + self._on_connect_failure('TLS received: %s' % tag) + return + log.info('Got starttls proceed response. Switching to TLS/SSL...') + # following call wouldn't work for BOSH transport but it doesn't matter + # because TLS negotiation with BOSH is forbidden + self.Connection.tls_init( + on_succ = lambda: self._xmpp_connect(socket_type='tls'), + on_fail = lambda: self._on_connect_failure('error while etabilishing TLS')) + + + def _on_stream_start(self): '''Called when stream is opened. To be overriden in derived classes.''' def _on_connect_failure(self, retry=None, err_message=None): - self.connected = None + self.connected = '' if err_message: log.debug('While connecting: %s' % err_message) if self.socket: @@ -234,6 +262,10 @@ class NBCommonClient: self.on_connect_failure(retry) def _on_connect(self): + if self.secure == 'tls': + self._on_connect_failure('uaaaaaa') + return + print 'self.secure = %s' % self.secure self.onreceive(None) self.on_connect(self, self.connected) @@ -243,7 +275,7 @@ class NBCommonClient: self.Dispatcher.Event('', event_type, data) - # moved from client.CommonClient: + # moved from client.CommonClient (blocking client from xmpppy): def RegisterDisconnectHandler(self,handler): """ Register handler that will be called on disconnect.""" self.disconnect_handlers.append(handler) @@ -259,7 +291,7 @@ class NBCommonClient: raise IOError('Disconnected from server.') def get_connect_type(self): - """ Returns connection state. F.e.: None / 'tls' / 'tcp+non_sasl' . """ + """ Returns connection state. F.e.: None / 'tls' / 'plain+non_sasl' . """ return self.connected def get_peerhost(self): @@ -267,8 +299,7 @@ class NBCommonClient: to the server , (e.g. me). We will create listening socket on the same ip ''' # FIXME: tuple (ip, port) is expected (and checked for) but port num is useless - if hasattr(self, 'socket'): - return self.socket.peerhost + return self.socket.peerhost def auth(self, user, password, resource = '', sasl = 1, on_auth = None): @@ -372,16 +403,21 @@ class NonBlockingClient(NBCommonClient): self.protocol_type = 'XMPP' def connect(self, on_connect, on_connect_failure, hostname=None, port=5222, - on_proxy_failure=None, proxy=None, secure=None): + on_proxy_failure=None, proxy=None, secure_tuple=None): NBCommonClient.connect(self, on_connect, on_connect_failure, hostname, port, - on_proxy_failure, proxy, secure) + on_proxy_failure, proxy, secure_tuple) if hostname: xmpp_hostname = hostname else: xmpp_hostname = self.Server + estabilish_tls = self.secure == 'ssl' + certs = (self.cacerts, self.mycerts) + + self._on_tcp_failure = self._on_connect_failure + if proxy: # with proxies, client connects to proxy instead of directly to # XMPP server ((hostname, port)) @@ -390,13 +426,14 @@ class NonBlockingClient(NBCommonClient): tcp_host, tcp_port, proxy_user, proxy_pass = \ transports_nb.get_proxy_data_from_dict(proxy) - self._on_tcp_failure = self.on_proxy_failure if proxy['type'] == 'bosh': self.socket = bosh.NonBlockingBOSH( on_disconnect = self.on_disconnect, raise_event = self.raise_event, idlequeue = self.idlequeue, + estabilish_tls = estabilish_tls, + certs = certs, proxy_creds = (proxy_user, proxy_pass), xmpp_server = (xmpp_hostname, self.Port), domain = self.Server, @@ -405,6 +442,7 @@ class NonBlockingClient(NBCommonClient): self.wait_for_restart_response = proxy['bosh_wait_for_restart_response'] else: + self._on_tcp_failure = self.on_proxy_failure if proxy['type'] == 'socks5': proxy_class = transports_nb.NBSOCKS5ProxySocket elif proxy['type'] == 'http': @@ -413,16 +451,19 @@ class NonBlockingClient(NBCommonClient): on_disconnect = self.on_disconnect, raise_event = self.raise_event, idlequeue = self.idlequeue, + estabilish_tls = estabilish_tls, + certs = certs, proxy_creds = (proxy_user, proxy_pass), xmpp_server = (xmpp_hostname, self.Port)) else: - self._on_tcp_failure = self._on_connect_failure tcp_host=xmpp_hostname tcp_port=self.Port self.socket = transports_nb.NonBlockingTCP( on_disconnect = self.on_disconnect, raise_event = self.raise_event, - idlequeue = self.idlequeue) + idlequeue = self.idlequeue, + estabilish_tls = estabilish_tls, + certs = certs) self.socket.PlugIn(self) @@ -434,31 +475,31 @@ class NonBlockingClient(NBCommonClient): - def _on_stream_start(self): ''' Called after XMPP stream is opened. In pure XMPP client, TLS negotiation may follow after esabilishing a stream. ''' self.onreceive(None) - if self.connected == 'tcp': - if not self.connected or not self._secure: - # if we are disconnected or TLS/SSL is not desired, return + if self.connected == 'plain': + if self.secure == 'plain': + # if we want plain connection, we're done now self._on_connect() return if not self.Dispatcher.Stream.features.getTag('starttls'): - # if server doesn't advertise TLS in init response + # if server doesn't advertise TLS in init response, we can't do more + log.warn('While connecting with type = "tls": TLS unsupported by remote server') self._on_connect() return if self.incoming_stream_version() != '1.0': + # if stream version is less than 1.0, we can't do more + log.warn('While connecting with type = "tls": stream version is less than 1.0') self._on_connect() return - # otherwise start TLS - tls_nb.NonBlockingTLS().PlugIn( - self, - on_tls_success=lambda: self._xmpp_connect(socket_type='tls'), - on_tls_failure=self._on_connect_failure) - elif self.connected == 'tls': + # otherwise start TLS + log.info("TLS supported by remote server. Requesting TLS start.") + self._tls_negotiation_handler() + elif self.connected in ['ssl', 'tls']: self._on_connect() diff --git a/src/common/xmpp/tls_nb.py b/src/common/xmpp/tls_nb.py index bd2091817..2f957f0bb 100644 --- a/src/common/xmpp/tls_nb.py +++ b/src/common/xmpp/tls_nb.py @@ -27,24 +27,12 @@ import traceback import logging log = logging.getLogger('gajim.c.x.tls_nb') - -# I don't need to load gajim.py just because of few TLS variables, so I changed -# %s/common\.gajim\.DATA_DIR/\'\.\.\/data\'/c -# %s/common\.gajim\.MY_CACERTS/\'\%s\/\.gajim\/cacerts\.pem\' \% os\.environ\[\'HOME\'\]/c - -# To change it back do: -# %s/\'\.\.\/data\'/common\.gajim\.DATA_DIR/c -# %s/\'%s\/\.gajim\/cacerts\.pem\'\ %\ os\.environ\[\'HOME\'\]/common\.gajim\.MY_CACERTS/c -# TODO: make the paths configurable - as constructor parameters or sth - -# import common.gajim +log.setLevel(logging.DEBUG) USE_PYOPENSSL = False - -#TODO: add callback set from PlugIn for errors during runtime -# - sth like on_disconnect in socket wrappers - +PYOPENSSL = 'PYOPENSSL' +PYSTDLIB = 'PYSTDLIB' try: #raise ImportError("Manually disabled PyOpenSSL") @@ -235,6 +223,11 @@ class StdlibSSLWrapper(SSLWrapper): class NonBlockingTLS(PlugIn): ''' TLS connection used to encrypts already estabilished tcp connection.''' + def __init__(self, cacerts, mycerts): + PlugIn.__init__(self) + self.cacerts = cacerts + self.mycerts = mycerts + # from ssl.h (partial extract) ssl_h_bits = { "SSL_ST_CONNECT": 0x1000, "SSL_ST_ACCEPT": 0x2000, "SSL_CB_LOOP": 0x01, "SSL_CB_EXIT": 0x02, @@ -242,56 +235,23 @@ class NonBlockingTLS(PlugIn): "SSL_CB_ALERT": 0x4000, "SSL_CB_HANDSHAKE_START": 0x10, "SSL_CB_HANDSHAKE_DONE": 0x20} - def PlugIn(self, owner, on_tls_success, on_tls_failure, now=0): - ''' If the 'now' argument is true then starts using encryption immidiatedly. - If 'now' in false then starts encryption as soon as TLS feature is - declared by the server (if it were already declared - it is ok). + def PlugIn(self, owner): ''' - if owner.__dict__.has_key('NonBlockingTLS'): - return # Already enabled. + start using encryption immediately + ''' + log.info('Starting TLS estabilishing') PlugIn.PlugIn(self, owner) - self.on_tls_success = on_tls_success - self.on_tls_faliure = on_tls_failure - if now: - try: - res = self._startSSL() - except Exception, e: - log.error("PlugIn: while trying _startSSL():", exc_info=True) - #traceback.print_exc() - self._owner.socket.pollend() - return - on_tls_success() - return res - if self._owner.Dispatcher.Stream.features: - try: - self.FeaturesHandler(self._owner.Dispatcher, self._owner.Dispatcher.Stream.features) - except NodeProcessed: - pass - else: - self._owner.RegisterHandlerOnce('features',self.FeaturesHandler, xmlns=NS_STREAMS) - self.starttls = None - - def plugout(self,now=0): - ''' Unregisters TLS handler's from owner's dispatcher. Take note that encription - can not be stopped once started. You can only break the connection and start over.''' - # if dispatcher is not plugged we cannot (un)register handlers - if self._owner.__dict__.has_key('Dispatcher'): - self._owner.UnregisterHandler('features', self.FeaturesHandler,xmlns=NS_STREAMS) - self._owner.Dispatcher.PlugOut() - self._owner = None + print 'inplugin' + try: + self._owner._plug_idle(writable=False, readable=False) + res = self._startSSL() + except Exception, e: + log.error("PlugIn: while trying _startSSL():", exc_info=True) + #traceback.print_exc() + return False + return res - def FeaturesHandler(self, conn, feats): - ''' Used to analyse server tag for TLS support. - If TLS is supported starts the encryption negotiation. Used internally ''' - if not feats.getTag('starttls', namespace=NS_TLS): - log.warn("TLS unsupported by remote server.") - self.on_tls_failure("TLS unsupported by remote server.") - return - log.debug("TLS supported by remote server. Requesting TLS start.") - self._owner.RegisterHandlerOnce('proceed', self.StartTLSHandler, xmlns=NS_TLS) - self._owner.RegisterHandlerOnce('failure', self.StartTLSHandler, xmlns=NS_TLS) - self._owner.send('' % NS_TLS) - raise NodeProcessed + def _dumpX509(self, cert, stream=sys.stderr): print >> stream, "Digest (SHA-1):", cert.digest("sha1") @@ -317,27 +277,26 @@ class NonBlockingTLS(PlugIn): ''' Immidiatedly switch socket to TLS mode. Used internally.''' log.debug("_startSSL called") if USE_PYOPENSSL: return self._startSSL_pyOpenSSL() - return self._startSSL_stdlib() + else: return self._startSSL_stdlib() def _startSSL_pyOpenSSL(self): #log.debug("_startSSL_pyOpenSSL called, thread id: %s", str(thread.get_ident())) log.debug("_startSSL_pyOpenSSL called") - tcpsock = self._owner.Connection + tcpsock = self._owner # FIXME: should method be configurable? - tcpsock._sslContext = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_METHOD) - #tcpsock._sslContext = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD) + #tcpsock._sslContext = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_METHOD) + tcpsock._sslContext = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD) tcpsock.ssl_errnum = 0 tcpsock._sslContext.set_verify(OpenSSL.SSL.VERIFY_PEER, self._ssl_verify_callback) - cacerts = os.path.join('../data', 'other', 'cacerts.pem') try: - tcpsock._sslContext.load_verify_locations(cacerts) + tcpsock._sslContext.load_verify_locations(self.cacerts) except: - log.warning('Unable to load SSL certificats from file %s' % \ - os.path.abspath(cacerts)) + log.warning('Unable to load SSL certificates from file %s' % \ + os.path.abspath(self.cacerts)) # load users certs - if os.path.isfile('%s/.gajim/cacerts.pem' % os.environ['HOME']): + if os.path.isfile(self.mycerts): store = tcpsock._sslContext.get_cert_store() - f = open('%s/.gajim/cacerts.pem' % os.environ['HOME']) + f = open(self.mycerts) lines = f.readlines() i = 0 begin = -1 @@ -352,11 +311,10 @@ class NonBlockingTLS(PlugIn): store.add_cert(X509cert) except OpenSSL.crypto.Error, exception_obj: log.warning('Unable to load a certificate from file %s: %s' %\ - ('%s/.gajim/cacerts.pem' % os.environ['HOME'], exception_obj.args[0][0][2])) + (self.mycerts, exception_obj.args[0][0][2])) except: - log.warning( - 'Unknown error while loading certificate from file %s' % \ - '%s/.gajim/cacerts.pem' % os.environ['HOME']) + log.warning('Unknown error while loading certificate from file %s' %\ + self.mycerts) begin = -1 i += 1 tcpsock._sslObj = OpenSSL.SSL.Connection(tcpsock._sslContext, tcpsock._sock) @@ -367,67 +325,52 @@ class NonBlockingTLS(PlugIn): tcpsock._send = wrapper.send log.debug("Initiating handshake...") - # FIXME: Figure out why _connect_success is called before the - # SSL handshake is completed in STARTTLS mode. See #2838. tcpsock._sslObj.setblocking(True) try: - self.starttls='in progress' tcpsock._sslObj.do_handshake() except: log.error('Error while TLS handshake: ', exc_info=True) - self.on_tls_failure('Error while TLS Handshake') - return + return False tcpsock._sslObj.setblocking(False) log.debug("Synchronous handshake completed") - #log.debug("Async handshake started...") + self._owner.ssl_lib = PYOPENSSL + return self._endSSL() - # fake it, for now - self.starttls='success' def _startSSL_stdlib(self): log.debug("_startSSL_stdlib called") - tcpsock=self._owner.Connection - tcpsock._sock.setblocking(True) - tcpsock._sslObj = socket.ssl(tcpsock._sock, None, None) - tcpsock._sock.setblocking(False) - tcpsock._sslIssuer = tcpsock._sslObj.issuer() - tcpsock._sslServer = tcpsock._sslObj.server() - wrapper = StdlibSSLWrapper(tcpsock._sslObj, tcpsock._sock) - tcpsock._recv = wrapper.recv - tcpsock._send = wrapper.send - self.starttls='success' + tcpsock=self._owner + try: + tcpsock._sock.setblocking(True) + tcpsock._sslObj = socket.ssl(tcpsock._sock, None, None) + tcpsock._sock.setblocking(False) + tcpsock._sslIssuer = tcpsock._sslObj.issuer() + tcpsock._sslServer = tcpsock._sslObj.server() + wrapper = StdlibSSLWrapper(tcpsock._sslObj, tcpsock._sock) + tcpsock._recv = wrapper.recv + tcpsock._send = wrapper.send + except: + log.error("Exception caught in _startSSL_stdlib:", exc_info=True) + return False + self._owner.ssl_lib = PYSTDLIB + return self._endSSL() + + def _endSSL(self): + self._owner._plug_idle(writable=True, readable=False) + return True def _ssl_verify_callback(self, sslconn, cert, errnum, depth, ok): # Exceptions can't propagate up through this callback, so print them here. try: - self._owner.Connection.ssl_fingerprint_sha1 = cert.digest('sha1') + print 'in ssl verify callback' + self._owner.ssl_fingerprint_sha1 = cert.digest('sha1') if errnum == 0: return True - self._owner.Connection.ssl_errnum = errnum - self._owner.Connection.ssl_cert_pem = OpenSSL.crypto.dump_certificate( + self._owner.ssl_errnum = errnum + self._owner.ssl_cert_pem = OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, cert) return True except: log.error("Exception caught in _ssl_info_callback:", exc_info=True) traceback.print_exc() # Make sure something is printed, even if log is disabled. - def StartTLSHandler(self, conn, starttls): - ''' Handle server reply if TLS is allowed to process. Behaves accordingly. - Used internally.''' - if starttls.getNamespace() <> NS_TLS: - self.on_tls_failure('Unknown namespace: %s' % starttls.getNamespace()) - return - self.starttls = starttls.getName() - if self.starttls == 'failure': - self.on_tls_failure('TLS received: %s' % self.starttls) - return - log.debug('Got starttls proceed response. Switching to TLS/SSL...') - try: - self._startSSL() - except Exception, e: - log.error("StartTLSHandler:", exc_info=True) - self.on_tls_failure('in StartTLSHandler') - #traceback.print_exc() - return - self._owner.Dispatcher.PlugOut() - self.on_tls_success() diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py index 29b95124f..d34ee3f7e 100644 --- a/src/common/xmpp/transports_nb.py +++ b/src/common/xmpp/transports_nb.py @@ -21,6 +21,7 @@ from simplexml import ustr from client import PlugIn from idlequeue import IdleObject from protocol import * +from tls_nb import NonBlockingTLS import sys import os @@ -76,15 +77,17 @@ DATA_RECEIVED='DATA RECEIVED' DATA_SENT='DATA SENT' -DISCONNECTED ='DISCONNECTED' -DISCONNECTING ='DISCONNECTING' -CONNECTING ='CONNECTING' -CONNECTED ='CONNECTED' - -# transports have different constructor and same connect +DISCONNECTED = 'DISCONNECTED' +DISCONNECTING = 'DISCONNECTING' +CONNECTING = 'CONNECTING' +PROXY_CONNECTING = 'PROXY_CONNECTING' +CONNECTED = 'CONNECTED' +STATES = [DISCONNECTED, DISCONNECTING, CONNECTING, PROXY_CONNECTING, CONNECTED] +# transports have different arguments in constructor and same in connect() +# method class NonBlockingTransport(PlugIn): - def __init__(self, raise_event, on_disconnect, idlequeue): + def __init__(self, raise_event, on_disconnect, idlequeue, estabilish_tls, certs): PlugIn.__init__(self) self.raise_event = raise_event self.on_disconnect = on_disconnect @@ -94,7 +97,11 @@ class NonBlockingTransport(PlugIn): self.on_receive = None self.server = None self.port = None - self.state = DISCONNECTED + self.set_state(DISCONNECTED) + self.estabilish_tls = estabilish_tls + self.certs = certs + # type of used ssl lib (if any) will be assigned to this member var + self.ssl_lib = None self._exported_methods=[self.disconnect, self.onreceive, self.set_send_timeout, self.set_timeout, self.remove_timeout, self.start_disconnect] @@ -114,9 +121,7 @@ class NonBlockingTransport(PlugIn): def connect(self, conn_5tuple, on_connect, on_connect_failure): ''' connect method should have the same declaration in all derived transports - ''' - assert(self.state == DISCONNECTED) self.on_connect = on_connect self.on_connect_failure = on_connect_failure (self.server, self.port) = conn_5tuple[4][:2] @@ -124,14 +129,16 @@ class NonBlockingTransport(PlugIn): def set_state(self, newstate): - assert(newstate in [DISCONNECTED, CONNECTING, CONNECTED, DISCONNECTING]) + assert(newstate in STATES) self.state = newstate - def _on_connect(self, data): + def get_state(self): + return self.state + + def _on_connect(self): ''' preceeds call of on_connect callback ''' # data is reference to socket wrapper instance. We don't need it in client # because - self.peerhost = data._sock.getsockname() self.set_state(CONNECTED) self.on_connect() @@ -144,9 +151,9 @@ class NonBlockingTransport(PlugIn): self.on_connect_failure(err_message=err_message) def send(self, raw_data, now=False): - if self.state not in [CONNECTED]: + if self.get_state() == DISCONNECTED: log.error('Unable to send %s \n because state is %s.' % - (raw_data, self.state)) + (raw_data, self.get_state())) def disconnect(self, do_callback=True): @@ -166,7 +173,7 @@ class NonBlockingTransport(PlugIn): return self.on_receive = recv_handler - def tcp_connection_started(self): + def tcp_connecting_started(self): self.set_state(CONNECTING) # on_connect/on_conn_failure will be called from self.pollin/self.pollout @@ -206,23 +213,23 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): ''' Non-blocking TCP socket wrapper ''' - def __init__(self, raise_event, on_disconnect, idlequeue): + def __init__(self, raise_event, on_disconnect, idlequeue, estabilish_tls, certs): ''' Class constructor. ''' - NonBlockingTransport.__init__(self, raise_event, on_disconnect, idlequeue) - + NonBlockingTransport.__init__(self, raise_event, on_disconnect, idlequeue, + estabilish_tls, certs) # queue with messages to be send self.sendqueue = [] # bytes remained from the last send message self.sendbuff = '' - self.terminator = '' def start_disconnect(self): - self.send('') NonBlockingTransport.start_disconnect(self) + self.send('', now=True) + self.disconnect() def connect(self, conn_5tuple, on_connect, on_connect_failure): ''' @@ -267,7 +274,7 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): if errnum in (errno.EINPROGRESS, errno.EALREADY, errno.EWOULDBLOCK): # connecting in progress log.info('After NB connect() of %s. "%s" raised => CONNECTING' % (id(self),errstr)) - self.tcp_connection_started() + self.tcp_connecting_started() return elif errnum in (0, 10056, errno.EISCONN): # already connected - this branch is probably useless, nonblocking connect() will @@ -282,39 +289,52 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): self._on_connect_failure('Exception while connecting to %s:%s - %s %s' % (self.server, self.port, errnum, errstr)) - def _on_connect(self, data): + def _on_connect(self): ''' with TCP socket, we have to remove send-timeout ''' self.idlequeue.remove_timeout(self.fd) + self.peerhost = self._sock.getsockname() + print self.estabilish_tls + if self.estabilish_tls: + self.tls_init( + on_succ = lambda: NonBlockingTransport._on_connect(self), + on_fail = lambda: self._on_connect_failure('error while estabilishing TLS')) + else: + NonBlockingTransport._on_connect(self) + + + def tls_init(self, on_succ, on_fail): + cacerts, mycerts = self.certs + result = NonBlockingTLS(cacerts, mycerts).PlugIn(self) + if result: on_succ() + else: on_fail() - NonBlockingTransport._on_connect(self, data) - def pollin(self): '''called when receive on plugged socket is possible ''' - log.info('pollin called, state == %s' % self.state) + log.info('pollin called, state == %s' % self.get_state()) self._do_receive() def pollout(self): '''called when send to plugged socket is possible''' - log.info('pollout called, state == %s' % self.state) + log.info('pollout called, state == %s' % self.get_state()) - if self.state==CONNECTING: + if self.get_state()==CONNECTING: log.info('%s socket wrapper connected' % id(self)) - self._on_connect(self) + self._on_connect() return self._do_send() def pollend(self): - log.info('pollend called, state == %s' % self.state) + log.info('pollend called, state == %s' % self.get_state()) - if self.state==CONNECTING: + if self.get_state()==CONNECTING: self._on_connect_failure('Error during connect to %s:%s' % (self.server, self.port)) else : self.disconnect() def disconnect(self, do_callback=True): - if self.state == DISCONNECTED: + if self.get_state() == DISCONNECTED: return self.set_state(DISCONNECTED) self.idlequeue.unplug_idle(self.fd) @@ -330,8 +350,8 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): ''' Implemntation of IdleObject function called on timeouts from IdleQueue. ''' - log.warn('read_timeout called, state == %s' % self.state) - if self.state==CONNECTING: + log.warn('read_timeout called, state == %s' % self.get_state()) + if self.get_state()==CONNECTING: # if read_timeout is called during connecting, connect() didn't end yet # thus we have to call the tcp failure callback self._on_connect_failure('Error during connect to %s:%s' % @@ -342,16 +362,16 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): def set_timeout(self, timeout): - if self.state != DISCONNECTED and self.fd != -1: + if self.get_state() != DISCONNECTED and self.fd != -1: NonBlockingTransport.set_timeout(self, timeout) else: - log.warn('set_timeout: TIMEOUT NOT SET: state is %s, fd is %s' % (self.state, self.fd)) + log.warn('set_timeout: TIMEOUT NOT SET: state is %s, fd is %s' % (self.get_state(), self.fd)) def remove_timeout(self): if self.fd: NonBlockingTransport.remove_timeout(self) else: - log.warn('remove_timeout: no self.fd state is %s' % self.state) + log.warn('remove_timeout: no self.fd state is %s' % self.get_state()) def send(self, raw_data, now=False): '''Append raw_data to the queue of messages to be send. @@ -368,6 +388,7 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): self._do_send() else: self.sendqueue.append(r) + self._plug_idle(writable=True, readable=True) @@ -380,8 +401,6 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): Plugged socket will always be watched for "error" event - in that case, pollend() is called. ''' - # if we are connecting, we shouln't touch the socket until it's connected - assert(self.state!=CONNECTING) self.idlequeue.plug_idle(self, writable, readable) log.info('Plugging fd %d, W:%s, R:%s' % (self.fd, writable, readable)) @@ -438,16 +457,15 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): # ENOTCONN - Transport endpoint is not connected # ESHUTDOWN - shutdown(2) has been called on a socket to close down the # sending end of the transmision, and then data was attempted to be sent - log.error("Connection to %s lost: %s %s" % ( self.server, errnum, errstr)) - if self.on_remote_disconnect: - self.on_remote_disconnect() - else: - self.disconnect() + log.error("Connection to %s lost: %s %s" % ( self.server, errnum, errstr), exc_info=True) + if hasattr(self, 'on_remote_disconnect'): self.on_remote_disconnect() + else: self.disconnect() return if received is None: - # in case of some other exception - # FIXME: is this needed?? + # in case of SSL error - because there are two types of TLS wrappers, the TLS + # pluging recv method returns None in case of error + print 'SSL ERROR' if errnum != 0: log.error("CConnection to %s lost: %s %s" % (self.server, errnum, errstr)) self.disconnect() @@ -456,6 +474,7 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): # we have received some bytes, stop the timeout! self.renew_send_timeout() + print '-->%s<--' % received # pass received data to owner if self.on_receive: self.raise_event(DATA_RECEIVED, received) @@ -479,10 +498,11 @@ class NonBlockingHTTP(NonBlockingTCP): HTTP headers from incoming messages ''' - def __init__(self, raise_event, on_disconnect, idlequeue, on_http_request_possible, - on_persistent_fallback, http_dict): + def __init__(self, raise_event, on_disconnect, idlequeue, estabilish_tls, certs, + on_http_request_possible, on_persistent_fallback, http_dict): - NonBlockingTCP.__init__(self, raise_event, on_disconnect, idlequeue) + NonBlockingTCP.__init__(self, raise_event, on_disconnect, idlequeue, + estabilish_tls, certs) self.http_protocol, self.http_host, self.http_path = urisplit(http_dict['http_uri']) if self.http_protocol is None: @@ -522,10 +542,7 @@ class NonBlockingHTTP(NonBlockingTCP): self.disconnect(do_callback=False) self.connect( conn_5tuple = self.conn_5tuple, - # after connect, the socket will be plugged as writable - pollout will be - # called, and since there are still data in sendbuff, _do_send will be - # called and sendbuff will be flushed - on_connect = lambda: self._plug_idle(writable=True, readable=True), + on_connect = self.on_http_request_possible, on_connect_failure = self.disconnect) else: @@ -549,7 +566,7 @@ class NonBlockingHTTP(NonBlockingTCP): if self.expected_length > len(self.recvbuff): # If we haven't received the whole HTTP mess yet, let's end the thread. - # It will be finnished from one of following polls (io_watch) on plugged socket. + # It will be finnished from one of following recvs on plugged socket. log.info('not enough bytes in HTTP response - %d expected, %d got' % (self.expected_length, len(self.recvbuff))) return @@ -587,6 +604,9 @@ class NonBlockingHTTP(NonBlockingTCP): credentials = '%s:%s' % (self.proxy_user, self.proxy_pass) credentials = base64.encodestring(credentials).strip() headers.append('Proxy-Authorization: Basic %s' % credentials) + else: + headers.append('Connection: Keep-Alive') + headers.append('\r\n') headers = '\r\n'.join(headers) return('%s%s\r\n' % (headers, httpbody)) @@ -611,6 +631,8 @@ class NonBlockingHTTP(NonBlockingTCP): headers[row[0][:-1]] = row[1] return (statusline, headers, httpbody) + + class NonBlockingHTTPBOSH(NonBlockingHTTP): @@ -646,29 +668,26 @@ class NBProxySocket(NonBlockingTCP): Interface for proxy socket wrappers - when tunnneling XMPP over proxies, some connecting process usually has to be done before opening stream. ''' - def __init__(self, raise_event, on_disconnect, idlequeue, xmpp_server, - proxy_creds=(None,None)): + def __init__(self, raise_event, on_disconnect, idlequeue, estabilish_tls, certs, + xmpp_server, proxy_creds=(None,None)): + self.proxy_user, self.proxy_pass = proxy_creds self.xmpp_server = xmpp_server - NonBlockingTCP.__init__(self, raise_event, on_disconnect, idlequeue) + NonBlockingTCP.__init__(self, raise_event, on_disconnect, idlequeue, + estabilish_tls, certs) - - def connect(self, conn_5tuple, on_connect, on_connect_failure): + def _on_connect(self): ''' - connect method is extended by proxy credentials and xmpp server hostname - and port because those are needed for - The idea is to insert Proxy-specific mechanism after TCP connect and - before XMPP stream opening (which is done from client). + We're redefining _on_connect method to insert proxy-specific mechanism before + invoking the ssl connection and then client callback. All the proxy connecting + is done before XML stream is opened. ''' + self.set_state(PROXY_CONNECTING) + self._on_tcp_connect() - self.after_proxy_connect = on_connect - - NonBlockingTCP.connect(self, - conn_5tuple=conn_5tuple, - on_connect =self._on_tcp_connect, - on_connect_failure =on_connect_failure) def _on_tcp_connect(self): + '''to be implemented in each proxy socket wrapper''' pass @@ -713,7 +732,7 @@ class NBHTTPProxySocket(NBProxySocket): return if len(reply) != 2: pass - self.after_proxy_connect() + NonBlockingT._on_connect(self) #self.onreceive(self._on_proxy_auth) def _on_proxy_auth(self, reply): diff --git a/src/gajim.py b/src/gajim.py index 748810a3b..be9eaab1a 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -545,7 +545,7 @@ class Interface: ctrl.print_conversation('Error %s: %s' % (array[2], array[1])) def handle_event_con_type(self, account, con_type): - # ('CON_TYPE', account, con_type) which can be 'ssl', 'tls', 'tcp' + # ('CON_TYPE', account, con_type) which can be 'ssl', 'tls', 'plain' gajim.con_types[account] = con_type self.roster.draw_account(account) From 45048610846418ded9497ebc48709e5c1bbbcc0b Mon Sep 17 00:00:00 2001 From: tomk Date: Sat, 9 Aug 2008 12:16:42 +0000 Subject: [PATCH 16/20] fixed handling of SSL errors --- src/common/connection.py | 12 ++++++---- src/common/xmpp/bosh.py | 39 ++++++++++++++++---------------- src/common/xmpp/client_nb.py | 3 ++- src/common/xmpp/idlequeue.py | 5 ++-- src/common/xmpp/simplexml.py | 1 - src/common/xmpp/tls_nb.py | 10 +++----- src/common/xmpp/transports_nb.py | 26 +++++++++++++-------- 7 files changed, 50 insertions(+), 46 deletions(-) diff --git a/src/common/connection.py b/src/common/connection.py index 932a8c12a..7154a56bc 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -515,13 +515,12 @@ class Connection(ConnectionHandlers): 'connection_types').split() else: self._connection_types = ['tls', 'ssl', 'plain'] - #THEHACK - #self._connection_types = ['ssl', 'plain'] if self._proxy and self._proxy['type']=='bosh': # with BOSH, we can't do TLS negotiation with , we do only "plain" # connection and TLS with handshake right after TCP connecting ("ssl") - try: self._connection_types.remove('tls') + try: + self._connection_types.remove('tls') except ValueError: pass host = self.select_next_host(self._hosts) @@ -553,7 +552,12 @@ class Connection(ConnectionHandlers): if self._current_type == 'ssl': # SSL (force TLS on different port than plain) - port = self._current_host['ssl_port'] + # If we do TLS over BOSH, port of XMPP server should be the standard one + # and TLS should be negotiated because immediate TLS on 5223 is deprecated + if self._proxy and self._proxy['type']=='bosh': + port = self._current_host['port'] + else: + port = self._current_host['ssl_port'] elif self._current_type == 'tls': # TLS - negotiate tls after XMPP stream is estabilished port = self._current_host['port'] diff --git a/src/common/xmpp/bosh.py b/src/common/xmpp/bosh.py index d89cd6a50..2c8ca9f4d 100644 --- a/src/common/xmpp/bosh.py +++ b/src/common/xmpp/bosh.py @@ -113,9 +113,8 @@ class NonBlockingBOSH(NonBlockingTransport): self.disconnect() return - print 'SSSSSSSSSSEEEEEEEEEND' + #Hack for making the non-secure warning dialog work if hasattr(self._owner, 'NonBlockingNonSASL') or hasattr(self._owner, 'SASL'): - #FIXME: Hack for making the non-secure warning dialog work self.send_BOSH(None) else: self.http_socks[0]._plug_idle(writable=False, readable=True) @@ -453,31 +452,31 @@ class AckChecker(): class KeyStack(): - def __init__(self, count): - self.count = count - self.keys = [] - self.reset() + def __init__(self, count): + self.count = count + self.keys = [] + self.reset() self.first_call = True - def reset(self): - seed = str(get_rand_number()) - self.keys = [sha.new(seed).hexdigest()] - for i in range(self.count-1): - curr_seed = self.keys[i] - self.keys.append(sha.new(curr_seed).hexdigest()) + def reset(self): + seed = str(get_rand_number()) + self.keys = [sha.new(seed).hexdigest()] + for i in range(self.count-1): + curr_seed = self.keys[i] + self.keys.append(sha.new(curr_seed).hexdigest()) - def get(self): + def get(self): if self.first_call: self.first_call = False return (None, self.keys.pop()) - if len(self.keys)>1: - return (self.keys.pop(), None) - else: - last_key = self.keys.pop() - self.reset() - new_key = self.keys.pop() - return (last_key, new_key) + if len(self.keys)>1: + return (self.keys.pop(), None) + else: + last_key = self.keys.pop() + self.reset() + new_key = self.keys.pop() + return (last_key, new_key) # http://www.xmpp.org/extensions/xep-0124.html#errorstatus-terminal bosh_errors = { diff --git a/src/common/xmpp/client_nb.py b/src/common/xmpp/client_nb.py index 7e9e347bf..9ecd46c83 100644 --- a/src/common/xmpp/client_nb.py +++ b/src/common/xmpp/client_nb.py @@ -23,7 +23,7 @@ These classes can be used for simple applications "AS IS" though. import socket -import transports_nb, tls_nb, dispatcher_nb, auth_nb, roster_nb, protocol, bosh +import transports_nb, dispatcher_nb, auth_nb, roster_nb, protocol, bosh from client import * from protocol import NS_TLS @@ -234,6 +234,7 @@ class NBCommonClient: xmlns=NS_TLS) self.send('' % NS_TLS) else: + # we got or if tag.getNamespace() <> NS_TLS: self._on_connect_failure('Unknown namespace: %s' % tag.getNamespace()) return diff --git a/src/common/xmpp/idlequeue.py b/src/common/xmpp/idlequeue.py index 765063167..832a6f46b 100644 --- a/src/common/xmpp/idlequeue.py +++ b/src/common/xmpp/idlequeue.py @@ -110,9 +110,8 @@ class IdleQueue: if alarm_time > current_time: break if self.alarms.has_key(alarm_time): - for cb in self.alarms[alarm_time]: - cb() - del(self.alarms[alarm_time]) + for cb in self.alarms[alarm_time]: cb() + if self.alarms.has_key(alarm_time): del(self.alarms[alarm_time]) def plug_idle(self, obj, writable = True, readable = True): if obj.fd == -1: diff --git a/src/common/xmpp/simplexml.py b/src/common/xmpp/simplexml.py index a22e0c46f..495239e9a 100644 --- a/src/common/xmpp/simplexml.py +++ b/src/common/xmpp/simplexml.py @@ -20,7 +20,6 @@ I'm personally using it in many other separate projects. It is designed to be as import xml.parsers.expat import logging log = logging.getLogger('gajim.c.x.simplexml') -#log.setLevel(logging.DEBUG) def XMLescape(txt): '''Returns provided string with symbols & < > " replaced by their respective XML entities.''' diff --git a/src/common/xmpp/tls_nb.py b/src/common/xmpp/tls_nb.py index 2f957f0bb..313edfc9b 100644 --- a/src/common/xmpp/tls_nb.py +++ b/src/common/xmpp/tls_nb.py @@ -150,7 +150,6 @@ class PyOpenSSLWrapper(SSLWrapper): else: retval = self.sslobj.recv(bufsize, flags) except (OpenSSL.SSL.WantReadError, OpenSSL.SSL.WantWriteError), e: log.debug("Recv: Want-error: " + repr(e)) - pass except OpenSSL.SSL.SysCallError, e: log.debug("Recv: Got OpenSSL.SSL.SysCallError: " + repr(e), exc_info=True) #traceback.print_exc() @@ -202,8 +201,7 @@ class StdlibSSLWrapper(SSLWrapper): try: return self.sslobj.read(bufsize) except socket.sslerror, e: - #log.debug("Recv: Caught socket.sslerror:", exc_info=True) - #traceback.print_exc() + log.debug("Recv: Caught socket.sslerror:", exc_info=True) if e.args[0] not in (socket.SSL_ERROR_WANT_READ, socket.SSL_ERROR_WANT_WRITE): raise SSLWrapper.Error(self.sock or self.sslobj, e) return None @@ -213,8 +211,7 @@ class StdlibSSLWrapper(SSLWrapper): try: return self.sslobj.write(data) except socket.sslerror, e: - #log.debug("Send: Caught socket.sslerror:", exc_info=True) - #traceback.print_exc() + log.debug("Send: Caught socket.sslerror:", exc_info=True) if e.args[0] not in (socket.SSL_ERROR_WANT_READ, socket.SSL_ERROR_WANT_WRITE): raise SSLWrapper.Error(self.sock or self.sslobj, e) return 0 @@ -274,13 +271,12 @@ class NonBlockingTLS(PlugIn): print >> stream, "PKey type: %s (%d)" % (typedict.get(pkey.type(), "Unknown"), pkey.type()) def _startSSL(self): - ''' Immidiatedly switch socket to TLS mode. Used internally.''' + ''' Immediatedly switch socket to TLS mode. Used internally.''' log.debug("_startSSL called") if USE_PYOPENSSL: return self._startSSL_pyOpenSSL() else: return self._startSSL_stdlib() def _startSSL_pyOpenSSL(self): - #log.debug("_startSSL_pyOpenSSL called, thread id: %s", str(thread.get_ident())) log.debug("_startSSL_pyOpenSSL called") tcpsock = self._owner # FIXME: should method be configurable? diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py index d34ee3f7e..e41db3a92 100644 --- a/src/common/xmpp/transports_nb.py +++ b/src/common/xmpp/transports_nb.py @@ -21,7 +21,7 @@ from simplexml import ustr from client import PlugIn from idlequeue import IdleObject from protocol import * -from tls_nb import NonBlockingTLS +import tls_nb import sys import os @@ -304,7 +304,7 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): def tls_init(self, on_succ, on_fail): cacerts, mycerts = self.certs - result = NonBlockingTLS(cacerts, mycerts).PlugIn(self) + result = tls_nb.NonBlockingTLS(cacerts, mycerts).PlugIn(self) if result: on_succ() else: on_fail() @@ -436,7 +436,10 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): def _do_receive(self): ''' Reads all pending incoming data. Calls owner's disconnected() method if appropriate.''' - ERR_DISCONN = -2 # Misc error signifying that we got disconnected + # Misc error signifying that we got disconnected + ERR_DISCONN = -2 + # code for unknown/other errors + ERR_OTHER = -3 received = None errnum = 0 errstr = 'No Error Set' @@ -444,9 +447,13 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): try: # get as many bites, as possible, but not more than RECV_BUFSIZE received = self._recv(RECV_BUFSIZE) - except (socket.error, socket.herror, socket.gaierror), (errnum, errstr): + except socket.error, (errnum, errstr): # save exception number and message to errnum, errstr log.info("_do_receive: got %s:" % received , exc_info=True) + except tls_nb.SSLWrapper.Error, e: + log.info("_do_receive, caugth SSL error: got %s:" % received , exc_info=True) + errnum = tls_nb.gattr(e, 'errno') or ERR_OTHER + errstr = tls_nb.gattr(e, 'exc_str') if received == '': errnum = ERR_DISCONN @@ -456,25 +463,24 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): # ECONNRESET - connection you are trying to access has been reset by the peer # ENOTCONN - Transport endpoint is not connected # ESHUTDOWN - shutdown(2) has been called on a socket to close down the - # sending end of the transmision, and then data was attempted to be sent + # sending end of the transmision, and then data was attempted to be sent log.error("Connection to %s lost: %s %s" % ( self.server, errnum, errstr), exc_info=True) if hasattr(self, 'on_remote_disconnect'): self.on_remote_disconnect() else: self.disconnect() return if received is None: - # in case of SSL error - because there are two types of TLS wrappers, the TLS - # pluging recv method returns None in case of error - print 'SSL ERROR' + # because there are two types of TLS wrappers, the TLS plugin recv method + # returns None in case of error if errnum != 0: log.error("CConnection to %s lost: %s %s" % (self.server, errnum, errstr)) self.disconnect() return received = '' + return # we have received some bytes, stop the timeout! self.renew_send_timeout() - print '-->%s<--' % received # pass received data to owner if self.on_receive: self.raise_event(DATA_RECEIVED, received) @@ -732,7 +738,7 @@ class NBHTTPProxySocket(NBProxySocket): return if len(reply) != 2: pass - NonBlockingT._on_connect(self) + NonBlockingTCP._on_connect(self) #self.onreceive(self._on_proxy_auth) def _on_proxy_auth(self, reply): From a76c173816c2692ef62f91e56a507bc7810f2d8f Mon Sep 17 00:00:00 2001 From: tomk Date: Thu, 14 Aug 2008 21:48:43 +0000 Subject: [PATCH 17/20] - improved SSL connections with BOSH - SSL over HTTP proxy is possible now, Gajim will do HTTP CONNECT on proxy to reach the BOSH Conn manager and try to estabilish TLS (same as what firefox do when approaching HTTPS server via proxy) - moved proxy-connecting code to xmpp/proxy_connectors.py - debugged SOCKS5 proxy code - tested with Tigase server --- src/common/connection.py | 14 +- src/common/xmpp/__init__.py | 2 +- src/common/xmpp/auth_nb.py | 29 --- src/common/xmpp/bosh.py | 99 +++++--- src/common/xmpp/client_nb.py | 49 ++-- src/common/xmpp/dispatcher_nb.py | 3 +- src/common/xmpp/proxy_connectors.py | 221 +++++++++++++++++ src/common/xmpp/tls_nb.py | 23 +- src/common/xmpp/transports_nb.py | 370 ++++++---------------------- 9 files changed, 405 insertions(+), 405 deletions(-) create mode 100644 src/common/xmpp/proxy_connectors.py diff --git a/src/common/connection.py b/src/common/connection.py index 7154a56bc..d191c8237 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -424,10 +424,8 @@ class Connection(ConnectionHandlers): use_srv = gajim.config.get_per('accounts', self.name, 'use_srv') use_custom = gajim.config.get_per('accounts', self.name, 'use_custom_host') - print 'use_custom = %s' % use_custom custom_h = gajim.config.get_per('accounts', self.name, 'custom_host') custom_p = gajim.config.get_per('accounts', self.name, 'custom_port') - print 'custom_port = %s' % custom_p # create connection if it doesn't already exist self.connected = 1 @@ -519,9 +517,11 @@ class Connection(ConnectionHandlers): if self._proxy and self._proxy['type']=='bosh': # with BOSH, we can't do TLS negotiation with , we do only "plain" # connection and TLS with handshake right after TCP connecting ("ssl") - try: - self._connection_types.remove('tls') - except ValueError: pass + scheme = common.xmpp.transports_nb.urisplit(self._proxy['bosh_uri'])[0] + if scheme=='https': + self._connection_types = ['ssl'] + else: + self._connection_types = ['plain'] host = self.select_next_host(self._hosts) self._current_host = host @@ -553,7 +553,7 @@ class Connection(ConnectionHandlers): if self._current_type == 'ssl': # SSL (force TLS on different port than plain) # If we do TLS over BOSH, port of XMPP server should be the standard one - # and TLS should be negotiated because immediate TLS on 5223 is deprecated + # and TLS should be negotiated because TLS on 5223 is deprecated if self._proxy and self._proxy['type']=='bosh': port = self._current_host['port'] else: @@ -583,7 +583,6 @@ class Connection(ConnectionHandlers): log.info('Connecting to %s: [%s:%d]', self.name, self._current_host['host'], port) - print secure_tuple con.connect( hostname=self._current_host['host'], port=port, @@ -1303,6 +1302,7 @@ class Connection(ConnectionHandlers): self.connect(config) def _on_new_account(self, con = None, con_type = None): + print 'on_new_acc- con: %s, con_type: %s' % (con, con_type) if not con_type: self.dispatch('NEW_ACC_NOT_CONNECTED', (_('Could not connect to "%s"') % self._hostname)) diff --git a/src/common/xmpp/__init__.py b/src/common/xmpp/__init__.py index 109b9b4c0..19c13bfa6 100644 --- a/src/common/xmpp/__init__.py +++ b/src/common/xmpp/__init__.py @@ -27,7 +27,7 @@ and use only methods for access all values you should not have any problems. """ import simplexml, protocol, auth_nb, transports_nb, roster_nb -import dispatcher_nb, features_nb, idlequeue, bosh, tls_nb +import dispatcher_nb, features_nb, idlequeue, bosh, tls_nb, proxy_connectors from client_nb import * from client import * from protocol import * diff --git a/src/common/xmpp/auth_nb.py b/src/common/xmpp/auth_nb.py index 783bfbc24..f4ca60058 100644 --- a/src/common/xmpp/auth_nb.py +++ b/src/common/xmpp/auth_nb.py @@ -253,8 +253,6 @@ class NonBlockingNonSASL(PlugIn): def plugin(self, owner): ''' Determine the best auth method (digest/0k/plain) and use it for auth. Returns used method name on success. Used internally. ''' - if not self.resource: - return self.authComponent(owner) log.info('Querying server about possible auth methods') self.owner = owner @@ -303,33 +301,6 @@ class NonBlockingNonSASL(PlugIn): log.error('Authentication failed!') return self.on_auth(None) - def authComponent(self,owner): - ''' Authenticate component. Send handshake stanza and wait for result. Returns "ok" on success. ''' - self.handshake=0 - owner.send(Node(NS_COMPONENT_ACCEPT+' handshake', - payload=[sha.new(owner.Dispatcher.Stream._document_attrs['id']+self.password).hexdigest()])) - owner.RegisterHandler('handshake', self.handshakeHandler, xmlns=NS_COMPONENT_ACCEPT) - self._owner.onreceive(self._on_auth_component) - - def _on_auth_component(self, data): - ''' called when we receive some response, after we send the handshake ''' - if data: - self.Dispatcher.ProcessNonBlocking(data) - if not self.handshake: - log.info('waiting on handshake') - return - self._owner.onreceive(None) - owner._registered_name=self.user - if self.handshake+1: - return self.on_auth('ok') - self.on_auth(None) - - def handshakeHandler(self,disp,stanza): - ''' Handler for registering in dispatcher for accepting transport authentication. ''' - if stanza.getName() == 'handshake': - self.handshake=1 - else: - self.handshake=-1 class NonBlockingBind(PlugIn): ''' Bind some JID to the current connection to allow router know of our location.''' diff --git a/src/common/xmpp/bosh.py b/src/common/xmpp/bosh.py index 2c8ca9f4d..5d1796a99 100644 --- a/src/common/xmpp/bosh.py +++ b/src/common/xmpp/bosh.py @@ -1,11 +1,29 @@ +## bosh.py +## +## +## Copyright (C) 2008 Tomas Karasek +## +## This file is part of Gajim. +## +## Gajim 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; version 3 only. +## +## Gajim 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 Gajim. If not, see . -import locale, random + +import locale, random, sha from transports_nb import NonBlockingTransport, NonBlockingHTTPBOSH,\ CONNECTED, CONNECTING, DISCONNECTED, DISCONNECTING,\ - urisplit + urisplit, DISCONNECT_TIMEOUT_SECONDS from protocol import BOSHBody from simplexml import Node -import sha import logging log = logging.getLogger('gajim.c.x.bosh') @@ -62,6 +80,13 @@ class NonBlockingBOSH(NonBlockingTransport): self.key_stack = None self.ack_checker = None self.after_init = False + self.proxy_dict = {} + if self.over_proxy and self.estabilish_tls: + self.proxy_dict['type'] = 'http' + # with SSL over proxy, we do HTTP CONNECT to proxy to open a channel to + # BOSH Connection Manager + self.proxy_dict['xmpp_server'] = (urisplit(self.bosh_uri)[1], self.bosh_port) + self.proxy_dict['credentials'] = self.proxy_creds def connect(self, conn_5tuple, on_connect, on_connect_failure): @@ -81,13 +106,9 @@ class NonBlockingBOSH(NonBlockingTransport): self.http_socks.append(self.get_new_http_socket()) self.tcp_connecting_started() - # following connect() is not necessary because sockets can be connected on - # send but we need to know if host is reachable in order to invoke callback - # for connecting failure eventually (the callback is different than callback - # for errors occurring after connection is etabilished) self.http_socks[0].connect( conn_5tuple = conn_5tuple, - on_connect = lambda: self._on_connect(), + on_connect = self._on_connect, on_connect_failure = self._on_connect_failure) def _on_connect(self): @@ -98,28 +119,29 @@ class NonBlockingBOSH(NonBlockingTransport): def set_timeout(self, timeout): - if self.get_state() in [CONNECTING, CONNECTED] and self.fd != -1: + if self.get_state() != DISCONNECTED and self.fd != -1: NonBlockingTransport.set_timeout(self, timeout) else: log.warn('set_timeout: TIMEOUT NOT SET: state is %s, fd is %s' % (self.get_state(), self.fd)) def on_http_request_possible(self): ''' - Called after HTTP response is received - another request is possible. - There should be always one pending request on BOSH CM. + Called after HTTP response is received - when another request is possible. + There should be always one pending request to BOSH CM. ''' log.info('on_http_req possible, state:\n%s' % self.get_current_state()) - if self.get_state() == DISCONNECTING: - self.disconnect() - return + if self.get_state()==DISCONNECTED: return #Hack for making the non-secure warning dialog work - if hasattr(self._owner, 'NonBlockingNonSASL') or hasattr(self._owner, 'SASL'): - self.send_BOSH(None) + if self._owner.got_features: + if (hasattr(self._owner, 'NonBlockingNonSASL') or hasattr(self._owner, 'SASL')): + self.send_BOSH(None) + else: + return else: - self.http_socks[0]._plug_idle(writable=False, readable=True) - return + self.send_BOSH(None) + def get_socket_in(self, state): for s in self.http_socks: @@ -129,7 +151,6 @@ class NonBlockingBOSH(NonBlockingTransport): def get_free_socket(self): if self.http_pipelining: - assert( len(self.http_socks) == 1 ) return self.get_socket_in(CONNECTED) else: last_recv_time, tmpsock = 0, None @@ -184,11 +205,12 @@ class NonBlockingBOSH(NonBlockingTransport): # CONNECTED with too many pending requests s = self.get_socket_in(DISCONNECTED) - # if we have DISCONNECTED socket, lets connect it and ... + # if we have DISCONNECTED socket, lets connect it and plug for send if s: self.connect_and_flush(s) else: - if len(self.http_socks) > 1: return + #if len(self.http_socks) > 1: return + print 'connecting sock' ss = self.get_new_http_socket() self.http_socks.append(ss) self.connect_and_flush(ss) @@ -200,7 +222,7 @@ class NonBlockingBOSH(NonBlockingTransport): if s: s._plug_idle(writable=True, readable=True) else: - log.error('=====!!!!!!!!====> Couldnt get free socket in plug_socket())') + log.error('=====!!!!!!!!====> Couldn\'t get free socket in plug_socket())') def build_stanza(self, socket): if self.prio_bosh_stanzas: @@ -222,21 +244,20 @@ class NonBlockingBOSH(NonBlockingTransport): log.info('sending msg with rid=%s to sock %s' % (stanza.getAttr('rid'), id(socket))) - socket.send(stanza) - self.renew_bosh_wait_timeout() + #socket.send(stanza) + self.renew_bosh_wait_timeout(self.bosh_wait + 3) return stanza def on_bosh_wait_timeout(self): - log.error('Connection Manager didn\'t respond within % seconds --> forcing \ - disconnect' % self.bosh_wait) + log.error('Connection Manager didn\'t respond within %s + 3 seconds --> forcing disconnect' % self.bosh_wait) self.disconnect() - def renew_bosh_wait_timeout(self): + def renew_bosh_wait_timeout(self, timeout): if self.wait_cb_time is not None: self.remove_bosh_wait_timeout() - sched_time = self.idlequeue.set_alarm(self.on_bosh_wait_timeout, self.bosh_wait+10) + sched_time = self.idlequeue.set_alarm(self.on_bosh_wait_timeout, timeout) self.wait_cb_time = sched_time def remove_bosh_wait_timeout(self): @@ -244,10 +265,17 @@ class NonBlockingBOSH(NonBlockingTransport): self.on_bosh_wait_timeout, self.wait_cb_time) - def on_persistent_fallback(self): + def on_persistent_fallback(self, socket): log.warn('Fallback to nonpersistent HTTP (no pipelining as well)') - self.http_persistent = False - self.http_pipelining = False + if socket.http_persistent: + socket.http_persistent = False + self.http_persistent = False + self.http_pipelining = False + socket.disconnect(do_callback=False) + self.connect_and_flush(socket) + else: + socket.disconnect() + def handle_body_attrs(self, stanza_attrs): @@ -277,7 +305,8 @@ class NonBlockingBOSH(NonBlockingTransport): if stanza_attrs.has_key('condition'): condition = stanza_attrs['condition'] log.error('Received terminating stanza: %s - %s' % (condition, bosh_errors[condition])) - self.set_state(DISCONNECTING) + self.disconnect() + return if stanza_attrs['type'] == 'error': # recoverable error @@ -295,8 +324,6 @@ class NonBlockingBOSH(NonBlockingTransport): def send(self, stanza, now=False): - # body tags should be send only via send_BOSH() - assert(not isinstance(stanza, BOSHBody)) self.send_BOSH(stanza) @@ -350,6 +377,7 @@ class NonBlockingBOSH(NonBlockingTransport): def start_disconnect(self): NonBlockingTransport.start_disconnect(self) + self.renew_bosh_wait_timeout(DISCONNECT_TIMEOUT_SECONDS) self.send_BOSH( (BOSHBody(attrs={'sid': self.bosh_sid, 'type': 'terminate'}), True)) @@ -359,7 +387,7 @@ class NonBlockingBOSH(NonBlockingTransport): 'http_port': self.bosh_port, 'http_version': self.http_version, 'http_persistent': self.http_persistent, - 'over_proxy': self.over_proxy} + 'add_proxy_headers': self.over_proxy and not self.estabilish_tls} if self.use_proxy_auth: http_dict['proxy_user'], http_dict['proxy_pass'] = self.proxy_creds @@ -372,6 +400,7 @@ class NonBlockingBOSH(NonBlockingTransport): certs = self.certs, on_http_request_possible = self.on_http_request_possible, http_dict = http_dict, + proxy_dict = self.proxy_dict, on_persistent_fallback = self.on_persistent_fallback) s.onreceive(self.on_received_http) s.set_stanza_build_cb(self.build_stanza) diff --git a/src/common/xmpp/client_nb.py b/src/common/xmpp/client_nb.py index 9ecd46c83..35d5079f4 100644 --- a/src/common/xmpp/client_nb.py +++ b/src/common/xmpp/client_nb.py @@ -56,12 +56,12 @@ class NBCommonClient: self._owner = self self._registered_name = None self.connected = '' - self._component=0 self.socket = None self.on_connect = None self.on_proxy_failure = None self.on_connect_failure = None self.proxy = None + self.got_features = False def on_disconnect(self): @@ -72,7 +72,6 @@ class NBCommonClient: ''' self.connected='' - log.debug('Client disconnected..') for i in reversed(self.disconnect_handlers): log.debug('Calling disconnect handler %s' % i) i() @@ -84,18 +83,13 @@ class NBCommonClient: self.NonBlockingNonSASL.PlugOut() if self.__dict__.has_key('SASL'): self.SASL.PlugOut() - if self.__dict__.has_key('NonBlockingTLS'): - self.NonBlockingTLS.PlugOut() - if self.__dict__.has_key('NBHTTPProxySocket'): - self.NBHTTPProxySocket.PlugOut() - if self.__dict__.has_key('NBSOCKS5ProxySocket'): - self.NBSOCKS5ProxySocket.PlugOut() if self.__dict__.has_key('NonBlockingTCP'): self.NonBlockingTCP.PlugOut() if self.__dict__.has_key('NonBlockingHTTP'): self.NonBlockingHTTP.PlugOut() if self.__dict__.has_key('NonBlockingBOSH'): self.NonBlockingBOSH.PlugOut() + log.debug('Client disconnected..') def connect(self, on_connect, on_connect_failure, hostname=None, port=5222, @@ -181,7 +175,9 @@ class NBCommonClient: if not mode: # starting state - if self.__dict__.has_key('Dispatcher'): self.Dispatcher.PlugOut() + if self.__dict__.has_key('Dispatcher'): + self.Dispatcher.PlugOut() + self.got_features = False d=dispatcher_nb.Dispatcher().PlugIn(self) on_next_receive('RECEIVE_DOCUMENT_ATTRIBUTES') @@ -197,7 +193,7 @@ class NBCommonClient: mode='FAILURE', data='Error on stream open') if self.incoming_stream_version() == '1.0': - if not self.Dispatcher.Stream.features: + if not self.got_features: on_next_receive('RECEIVE_STREAM_FEATURES') else: log.info('got STREAM FEATURES in first recv') @@ -212,7 +208,7 @@ class NBCommonClient: # sometimes are received together with document # attributes and sometimes on next receive... self.Dispatcher.ProcessNonBlocking(data) - if not self.Dispatcher.Stream.features: + if not self.got_features: self._xmpp_connect_machine( mode='FAILURE', data='Missing in 1.0 stream') @@ -263,10 +259,6 @@ class NBCommonClient: self.on_connect_failure(retry) def _on_connect(self): - if self.secure == 'tls': - self._on_connect_failure('uaaaaaa') - return - print 'self.secure = %s' % self.secure self.onreceive(None) self.on_connect(self, self.connected) @@ -343,7 +335,6 @@ class NBCommonClient: # wrong user/pass, stop auth self.connected = None self._on_sasl_auth(None) - self.SASL.PlugOut() elif self.SASL.startsasl == 'success': auth_nb.NonBlockingBind().PlugIn(self) if self.protocol_type == 'BOSH': @@ -418,6 +409,9 @@ class NonBlockingClient(NBCommonClient): certs = (self.cacerts, self.mycerts) self._on_tcp_failure = self._on_connect_failure + proxy_dict = {} + tcp_host=xmpp_hostname + tcp_port=self.Port if proxy: # with proxies, client connects to proxy instead of directly to @@ -444,27 +438,18 @@ class NonBlockingClient(NBCommonClient): else: self._on_tcp_failure = self.on_proxy_failure - if proxy['type'] == 'socks5': - proxy_class = transports_nb.NBSOCKS5ProxySocket - elif proxy['type'] == 'http': - proxy_class = transports_nb.NBHTTPProxySocket - self.socket = proxy_class( - on_disconnect = self.on_disconnect, - raise_event = self.raise_event, - idlequeue = self.idlequeue, - estabilish_tls = estabilish_tls, - certs = certs, - proxy_creds = (proxy_user, proxy_pass), - xmpp_server = (xmpp_hostname, self.Port)) - else: - tcp_host=xmpp_hostname - tcp_port=self.Port + proxy_dict['type'] = proxy['type'] + proxy_dict['xmpp_server'] = (xmpp_hostname, self.Port) + proxy_dict['credentials'] = (proxy_user, proxy_pass) + + if not proxy or proxy['type'] != 'bosh': self.socket = transports_nb.NonBlockingTCP( on_disconnect = self.on_disconnect, raise_event = self.raise_event, idlequeue = self.idlequeue, estabilish_tls = estabilish_tls, - certs = certs) + certs = certs, + proxy_dict = proxy_dict) self.socket.PlugIn(self) diff --git a/src/common/xmpp/dispatcher_nb.py b/src/common/xmpp/dispatcher_nb.py index 5b1eb0be4..430ff534d 100644 --- a/src/common/xmpp/dispatcher_nb.py +++ b/src/common/xmpp/dispatcher_nb.py @@ -305,6 +305,7 @@ class XMPPDispatcher(PlugIn): name = stanza.getName() if name=='features': + self._owner.got_features = True session.Stream.features=stanza xmlns=stanza.getNamespace() @@ -390,7 +391,7 @@ class XMPPDispatcher(PlugIn): ''' Put stanza on the wire and wait for recipient's response to it. ''' if timeout is None: timeout = DEFAULT_TIMEOUT_SECONDS - self._witid = self._owner.send(stanza) + self._witid = self.send(stanza) if func: self.on_responses[self._witid] = (func, args) if timeout: diff --git a/src/common/xmpp/proxy_connectors.py b/src/common/xmpp/proxy_connectors.py new file mode 100644 index 000000000..1b769288a --- /dev/null +++ b/src/common/xmpp/proxy_connectors.py @@ -0,0 +1,221 @@ +## proxy_connectors.py +## based on transports_nb.py +## +## Copyright (C) 2003-2004 Alexey "Snake" Nezhdanov +## modified by Dimitur Kirov +## modified by Tomas Karasek +## +## 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 2, 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. +import struct, socket, base64 + +''' +Module containing classes for proxy connecting. So far its HTTP CONNECT +and SOCKS5 proxy. +''' + +import logging +log = logging.getLogger('gajim.c.x.proxy_connectors') + +class ProxyConnector: + ''' + Interface for proxy-connecting object - when tunnneling XMPP over proxies, + some connecting process usually has to be done before opening stream. + Proxy connectors are used right after TCP connection is estabilished. + ''' + def __init__(self, send_method, onreceive, old_on_receive, on_success, + on_failure, xmpp_server, proxy_creds=(None,None)): + + self.send = send_method + self.onreceive = onreceive + self.old_on_receive = old_on_receive + self.on_success = on_success + self.on_failure = on_failure + self.xmpp_server = xmpp_server + self.proxy_user, self.proxy_pass = proxy_creds + self.old_on_receive = old_on_receive + + self.start_connecting() + + def start_connecting(self): + raise NotImplementedException() + + def connecting_over(self): + self.onreceive(self.old_on_receive) + self.on_success() + +class HTTPCONNECTConnector(ProxyConnector): + def start_connecting(self): + ''' + Connects to proxy, supplies login and password to it + (if were specified while creating instance). Instructs proxy to make + connection to the target server. + ''' + log.info('Proxy server contacted, performing authentification') + connector = ['CONNECT %s:%s HTTP/1.1' % self.xmpp_server, + 'Proxy-Connection: Keep-Alive', + 'Pragma: no-cache', + 'Host: %s:%s' % self.xmpp_server, + 'User-Agent: Gajim'] + if self.proxy_user and self.proxy_pass: + credentials = '%s:%s' % (self.proxy_user, self.proxy_pass) + credentials = base64.encodestring(credentials).strip() + connector.append('Proxy-Authorization: Basic '+credentials) + connector.append('\r\n') + self.onreceive(self._on_headers_sent) + self.send('\r\n'.join(connector)) + + def _on_headers_sent(self, reply): + if reply is None: + return + self.reply = reply.replace('\r', '') + try: + proto, code, desc = reply.split('\n')[0].split(' ', 2) + except: + log.error("_on_headers_sent:", exc_info=True) + #traceback.print_exc() + self.on_failure('Invalid proxy reply') + return + if code <> '200': + log.error('Invalid proxy reply: %s %s %s' % (proto, code, desc)) + self.on_failure('Invalid proxy reply') + return + if len(reply) != 2: + pass + self.connecting_over() + + + +class SOCKS5Connector(ProxyConnector): + ''' + SOCKS5 proxy connection class. Allows to use SOCKS5 proxies with + (optionally) simple authentication (only USERNAME/PASSWORD auth). + ''' + def start_connecting(self): + log.info('Proxy server contacted, performing authentification') + if self.proxy_user and self.proxy_pass: + to_send = '\x05\x02\x00\x02' + else: + to_send = '\x05\x01\x00' + self.onreceive(self._on_greeting_sent) + self.send(to_send) + + def _on_greeting_sent(self, reply): + if reply is None: + return + if len(reply) != 2: + self.on_failure('Invalid proxy reply') + return + if reply[0] != '\x05': + log.info('Invalid proxy reply') + self.on_failure('Invalid proxy reply') + return + if reply[1] == '\x00': + return self._on_proxy_auth('\x01\x00') + elif reply[1] == '\x02': + to_send = '\x01' + chr(len(self.proxy_user)) + self.proxy_user +\ + chr(len(self.proxy_pass)) + self.proxy_pass + self.onreceive(self._on_proxy_auth) + self.send(to_send) + else: + if reply[1] == '\xff': + log.error('Authentification to proxy impossible: no acceptable ' + 'auth method') + self.on_failure('Authentification to proxy impossible: no ' + 'acceptable authentification method') + return + log.error('Invalid proxy reply') + self.on_failure('Invalid proxy reply') + return + + def _on_proxy_auth(self, reply): + if reply is None: + return + if len(reply) != 2: + log.error('Invalid proxy reply') + self.on_failure('Invalid proxy reply') + return + if reply[0] != '\x01': + log.error('Invalid proxy reply') + self.on_failure('Invalid proxy reply') + return + if reply[1] != '\x00': + log.error('Authentification to proxy failed') + self.on_failure('Authentification to proxy failed') + return + log.info('Authentification successfull. Jabber server contacted.') + # Request connection + req = "\x05\x01\x00" + # If the given destination address is an IP address, we'll + # use the IPv4 address request even if remote resolving was specified. + try: + self.ipaddr = socket.inet_aton(self.xmpp_server[0]) + req = req + "\x01" + self.ipaddr + except socket.error: + # Well it's not an IP number, so it's probably a DNS name. +# if self.__proxy[3]==True: + # Resolve remotely + self.ipaddr = None + req = req + "\x03" + chr(len(self.xmpp_server[0])) + self.xmpp_server[0] +# else: +# # Resolve locally +# self.ipaddr = socket.inet_aton(socket.gethostbyname(self.xmpp_server[0])) +# req = req + "\x01" + ipaddr + req = req + struct.pack(">H",self.xmpp_server[1]) + self.onreceive(self._on_req_sent) + self.send(req) + + def _on_req_sent(self, reply): + if reply is None: + return + if len(reply) < 10: + log.error('Invalid proxy reply') + self.on_failure('Invalid proxy reply') + return + if reply[0] != '\x05': + log.error('Invalid proxy reply') + self.on_failure('Invalid proxy reply') + return + if reply[1] != "\x00": + # Connection failed + if ord(reply[1])<9: + errors = ['general SOCKS server failure', + 'connection not allowed by ruleset', + 'Network unreachable', + 'Host unreachable', + 'Connection refused', + 'TTL expired', + 'Command not supported', + 'Address type not supported' + ] + txt = errors[ord(reply[1])-1] + else: + txt = 'Invalid proxy reply' + log.error(txt) + self.on_failure(txt) + return + # Get the bound address/port + elif reply[3] == "\x01": + begin, end = 3, 7 + elif reply[3] == "\x03": + begin, end = 4, 4 + reply[4] + else: + log.error('Invalid proxy reply') + self.on_failure('Invalid proxy reply') + return + self.connecting_over() + + + + + + + + diff --git a/src/common/xmpp/tls_nb.py b/src/common/xmpp/tls_nb.py index 313edfc9b..72faab425 100644 --- a/src/common/xmpp/tls_nb.py +++ b/src/common/xmpp/tls_nb.py @@ -201,7 +201,7 @@ class StdlibSSLWrapper(SSLWrapper): try: return self.sslobj.read(bufsize) except socket.sslerror, e: - log.debug("Recv: Caught socket.sslerror:", exc_info=True) + log.debug("Recv: Caught socket.sslerror: " + repr(e), exc_info=True) if e.args[0] not in (socket.SSL_ERROR_WANT_READ, socket.SSL_ERROR_WANT_WRITE): raise SSLWrapper.Error(self.sock or self.sslobj, e) return None @@ -238,7 +238,6 @@ class NonBlockingTLS(PlugIn): ''' log.info('Starting TLS estabilishing') PlugIn.PlugIn(self, owner) - print 'inplugin' try: self._owner._plug_idle(writable=False, readable=False) res = self._startSSL() @@ -273,8 +272,17 @@ class NonBlockingTLS(PlugIn): def _startSSL(self): ''' Immediatedly switch socket to TLS mode. Used internally.''' log.debug("_startSSL called") - if USE_PYOPENSSL: return self._startSSL_pyOpenSSL() - else: return self._startSSL_stdlib() + + if USE_PYOPENSSL: result = self._startSSL_pyOpenSSL() + else: result = self._startSSL_stdlib() + + if result: + log.debug("Synchronous handshake completed") + self._owner._plug_idle(writable=True, readable=False) + return True + else: + return False + def _startSSL_pyOpenSSL(self): log.debug("_startSSL_pyOpenSSL called") @@ -328,9 +336,8 @@ class NonBlockingTLS(PlugIn): log.error('Error while TLS handshake: ', exc_info=True) return False tcpsock._sslObj.setblocking(False) - log.debug("Synchronous handshake completed") self._owner.ssl_lib = PYOPENSSL - return self._endSSL() + return True def _startSSL_stdlib(self): @@ -349,10 +356,6 @@ class NonBlockingTLS(PlugIn): log.error("Exception caught in _startSSL_stdlib:", exc_info=True) return False self._owner.ssl_lib = PYSTDLIB - return self._endSSL() - - def _endSSL(self): - self._owner._plug_idle(writable=True, readable=False) return True def _ssl_verify_callback(self, sslconn, cert, errnum, depth, ok): diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py index e41db3a92..166dd2956 100644 --- a/src/common/xmpp/transports_nb.py +++ b/src/common/xmpp/transports_nb.py @@ -15,20 +15,21 @@ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. -import socket,base64 from simplexml import ustr from client import PlugIn from idlequeue import IdleObject from protocol import * +import proxy_connectors import tls_nb +import socket import sys import os import errno import time - import traceback +import base64 import logging log = logging.getLogger('gajim.c.x.transports_nb') @@ -66,7 +67,7 @@ def get_proxy_data_from_dict(proxy): CONNECT_TIMEOUT_SECONDS = 30 # how long to wait for a disconnect to complete -DISCONNECT_TIMEOUT_SECONDS = 10 +DISCONNECT_TIMEOUT_SECONDS =5 # size of the buffer which reads data from server # if lower, more stanzas will be fragmented and processed twice @@ -82,7 +83,7 @@ DISCONNECTING = 'DISCONNECTING' CONNECTING = 'CONNECTING' PROXY_CONNECTING = 'PROXY_CONNECTING' CONNECTED = 'CONNECTED' -STATES = [DISCONNECTED, DISCONNECTING, CONNECTING, PROXY_CONNECTING, CONNECTED] +STATES = [DISCONNECTED, CONNECTING, PROXY_CONNECTING, CONNECTED, DISCONNECTING] # transports have different arguments in constructor and same in connect() # method @@ -124,7 +125,7 @@ class NonBlockingTransport(PlugIn): ''' self.on_connect = on_connect self.on_connect_failure = on_connect_failure - (self.server, self.port) = conn_5tuple[4][:2] + self.server, self.port = conn_5tuple[4][:2] self.conn_5tuple = conn_5tuple @@ -213,7 +214,8 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): ''' Non-blocking TCP socket wrapper ''' - def __init__(self, raise_event, on_disconnect, idlequeue, estabilish_tls, certs): + def __init__(self, raise_event, on_disconnect, idlequeue, estabilish_tls, certs, + proxy_dict=None): ''' Class constructor. ''' @@ -224,6 +226,8 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): # bytes remained from the last send message self.sendbuff = '' + self.proxy_dict = proxy_dict + self.on_remote_disconnect = self.disconnect() def start_disconnect(self): @@ -288,12 +292,29 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): # which will also remove read_timeouts for descriptor self._on_connect_failure('Exception while connecting to %s:%s - %s %s' % (self.server, self.port, errnum, errstr)) + + def _connect_to_proxy(self): + self.set_state(PROXY_CONNECTING) + if self.proxy_dict['type'] == 'socks5': + proxyclass = proxy_connectors.SOCKS5Connector + elif self.proxy_dict['type'] == 'http' : + proxyclass = proxy_connectors.HTTPCONNECTConnector + proxyclass( + send_method = self.send, + onreceive = self.onreceive, + old_on_receive = self.on_receive, + on_success = self._on_connect, + on_failure = self._on_connect_failure, + xmpp_server = self.proxy_dict['xmpp_server'], + proxy_creds = self.proxy_dict['credentials'] + ) + def _on_connect(self): - ''' with TCP socket, we have to remove send-timeout ''' - self.idlequeue.remove_timeout(self.fd) - self.peerhost = self._sock.getsockname() - print self.estabilish_tls + ''' + Preceeds invoking of on_connect callback. TCP connection is estabilished at + this time. + ''' if self.estabilish_tls: self.tls_init( on_succ = lambda: NonBlockingTransport._on_connect(self), @@ -320,7 +341,10 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): if self.get_state()==CONNECTING: log.info('%s socket wrapper connected' % id(self)) - self._on_connect() + self.idlequeue.remove_timeout(self.fd) + self.peerhost = self._sock.getsockname() + if self.proxy_dict: self._connect_to_proxy() + else: self._on_connect() return self._do_send() @@ -330,7 +354,7 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): if self.get_state()==CONNECTING: self._on_connect_failure('Error during connect to %s:%s' % (self.server, self.port)) - else : + else: self.disconnect() def disconnect(self, do_callback=True): @@ -338,6 +362,7 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): return self.set_state(DISCONNECTED) self.idlequeue.unplug_idle(self.fd) + if self.__dict__.has_key('NonBlockingTLS'): self.NonBlockingTLS.PlugOut() try: self._sock.shutdown(socket.SHUT_RDWR) self._sock.close() @@ -401,8 +426,6 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): Plugged socket will always be watched for "error" event - in that case, pollend() is called. ''' - self.idlequeue.plug_idle(self, writable, readable) - log.info('Plugging fd %d, W:%s, R:%s' % (self.fd, writable, readable)) self.idlequeue.plug_idle(self, writable, readable) @@ -436,10 +459,6 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): def _do_receive(self): ''' Reads all pending incoming data. Calls owner's disconnected() method if appropriate.''' - # Misc error signifying that we got disconnected - ERR_DISCONN = -2 - # code for unknown/other errors - ERR_OTHER = -3 received = None errnum = 0 errstr = 'No Error Set' @@ -448,35 +467,29 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): # get as many bites, as possible, but not more than RECV_BUFSIZE received = self._recv(RECV_BUFSIZE) except socket.error, (errnum, errstr): - # save exception number and message to errnum, errstr log.info("_do_receive: got %s:" % received , exc_info=True) except tls_nb.SSLWrapper.Error, e: - log.info("_do_receive, caugth SSL error: got %s:" % received , exc_info=True) - errnum = tls_nb.gattr(e, 'errno') or ERR_OTHER - errstr = tls_nb.gattr(e, 'exc_str') + log.info("_do_receive, caught SSL error, got %s:" % received , exc_info=True) + errnum, errstr = e.exc + + if (self.ssl_lib is None and received == '') or \ + (self.ssl_lib == tls_nb.PYSTDLIB and errnum == 8 ) or \ + (self.ssl_lib == tls_nb.PYOPENSSL and errnum == -1 ): + # 8 in stdlib: errstr == EOF occured in violation of protocol + # -1 in pyopenssl: errstr == Unexpected EOF + log.info("Disconnected by remote server: %s %s" % (errnum, errstr), exc_info=True) + self.on_remote_disconnect() + return - if received == '': - errnum = ERR_DISCONN - errstr = "Connection closed unexpectedly" - if errnum in (ERR_DISCONN, errno.ECONNRESET, errno.ENOTCONN, errno.ESHUTDOWN): - # ECONNRESET - connection you are trying to access has been reset by the peer - # ENOTCONN - Transport endpoint is not connected - # ESHUTDOWN - shutdown(2) has been called on a socket to close down the - # sending end of the transmision, and then data was attempted to be sent - log.error("Connection to %s lost: %s %s" % ( self.server, errnum, errstr), exc_info=True) - if hasattr(self, 'on_remote_disconnect'): self.on_remote_disconnect() - else: self.disconnect() + if errnum: + log.error("Connection to %s:%s lost: %s %s" % ( self.server, self.port, errnum, errstr), exc_info=True) + self.disconnect() return + # this branch is for case of non-fatal SSL errors - None is returned from + # recv() but no errnum is set if received is None: - # because there are two types of TLS wrappers, the TLS plugin recv method - # returns None in case of error - if errnum != 0: - log.error("CConnection to %s lost: %s %s" % (self.server, errnum, errstr)) - self.disconnect() - return - received = '' return # we have received some bytes, stop the timeout! @@ -505,10 +518,10 @@ class NonBlockingHTTP(NonBlockingTCP): ''' def __init__(self, raise_event, on_disconnect, idlequeue, estabilish_tls, certs, - on_http_request_possible, on_persistent_fallback, http_dict): + on_http_request_possible, on_persistent_fallback, http_dict, proxy_dict = None): NonBlockingTCP.__init__(self, raise_event, on_disconnect, idlequeue, - estabilish_tls, certs) + estabilish_tls, certs, proxy_dict) self.http_protocol, self.http_host, self.http_path = urisplit(http_dict['http_uri']) if self.http_protocol is None: @@ -518,7 +531,7 @@ class NonBlockingHTTP(NonBlockingTCP): self.http_port = http_dict['http_port'] self.http_version = http_dict['http_version'] self.http_persistent = http_dict['http_persistent'] - self.over_proxy = http_dict['over_proxy'] + self.add_proxy_headers = http_dict['add_proxy_headers'] if http_dict.has_key('proxy_user') and http_dict.has_key('proxy_pass'): self.proxy_user, self.proxy_pass = http_dict['proxy_user'], http_dict['proxy_pass'] else: @@ -528,46 +541,39 @@ class NonBlockingHTTP(NonBlockingTCP): self.recvbuff = '' self.expected_length = 0 self.pending_requests = 0 - self.on_persistent_fallback = on_persistent_fallback self.on_http_request_possible = on_http_request_possible - self.just_responed = False self.last_recv_time = 0 + self.close_current_connection = False + self.on_remote_disconnect = lambda: on_persistent_fallback(self) - def send(self, raw_data, now=False): - NonBlockingTCP.send( - self, - self.build_http_message(raw_data), - now) + def http_send(self, raw_data, now=False): + self.send(self.build_http_message(raw_data), now) - def on_remote_disconnect(self): - log.warn('on_remote_disconnect called, http_persistent = %s' % self.http_persistent) - if self.http_persistent: - self.http_persistent = False - self.on_persistent_fallback() - self.disconnect(do_callback=False) - self.connect( - conn_5tuple = self.conn_5tuple, - on_connect = self.on_http_request_possible, - on_connect_failure = self.disconnect) - - else: - self.disconnect() - return - def _on_receive(self,data): '''Preceeds passing received data to owner class. Gets rid of HTTP headers and checks them.''' + if self.get_state() == PROXY_CONNECTING: + NonBlockingTCP._on_receive(self, data) + return if not self.recvbuff: # recvbuff empty - fresh HTTP message was received - statusline, headers, self.recvbuff = self.parse_http_message(data) + try: + statusline, headers, self.recvbuff = self.parse_http_message(data) + except ValueError: + self.disconnect() + return + if statusline[1] != '200': log.error('HTTP Error: %s %s' % (statusline[1], statusline[2])) self.disconnect() return self.expected_length = int(headers['Content-Length']) + if headers.has_key('Connection') and headers['Connection'].strip()=='close': + self.close_current_connection = True + else: - #sth in recvbuff - append currently received data to HTTP mess in buffer + #sth in recvbuff - append currently received data to HTTP msg in buffer self.recvbuff = '%s%s' % (self.recvbuff, data) if self.expected_length > len(self.recvbuff): @@ -583,9 +589,10 @@ class NonBlockingHTTP(NonBlockingTCP): self.recvbuff='' self.expected_length=0 - if not self.http_persistent: + if not self.http_persistent or self.close_current_connection: # not-persistent connections disconnect after response self.disconnect(do_callback = False) + self.close_current_connection = False self.last_recv_time = time.time() self.on_receive(data=httpbody, socket=self) self.on_http_request_possible() @@ -601,9 +608,10 @@ class NonBlockingHTTP(NonBlockingTCP): self.http_port, self.http_path) headers = ['%s %s %s' % (method, absolute_uri, self.http_version), 'Host: %s:%s' % (self.http_host, self.http_port), + 'User-Agent: Gajim', 'Content-Type: text/xml; charset=utf-8', 'Content-Length: %s' % len(str(httpbody))] - if self.over_proxy: + if self.add_proxy_headers: headers.append('Proxy-Connection: keep-alive') headers.append('Pragma: no-cache') if self.proxy_user and self.proxy_pass: @@ -646,6 +654,9 @@ class NonBlockingHTTPBOSH(NonBlockingHTTP): self.build_cb = build_cb def _do_send(self): + if self.state == PROXY_CONNECTING: + NonBlockingTCP._do_send(self) + return if not self.sendbuff: stanza = self.build_cb(socket=self) stanza = self.build_http_message(httpbody=stanza) @@ -669,225 +680,4 @@ class NonBlockingHTTPBOSH(NonBlockingHTTP): -class NBProxySocket(NonBlockingTCP): - ''' - Interface for proxy socket wrappers - when tunnneling XMPP over proxies, - some connecting process usually has to be done before opening stream. - ''' - def __init__(self, raise_event, on_disconnect, idlequeue, estabilish_tls, certs, - xmpp_server, proxy_creds=(None,None)): - - self.proxy_user, self.proxy_pass = proxy_creds - self.xmpp_server = xmpp_server - NonBlockingTCP.__init__(self, raise_event, on_disconnect, idlequeue, - estabilish_tls, certs) - - def _on_connect(self): - ''' - We're redefining _on_connect method to insert proxy-specific mechanism before - invoking the ssl connection and then client callback. All the proxy connecting - is done before XML stream is opened. - ''' - self.set_state(PROXY_CONNECTING) - self._on_tcp_connect() - - - def _on_tcp_connect(self): - '''to be implemented in each proxy socket wrapper''' - pass - - -class NBHTTPProxySocket(NBProxySocket): - ''' This class can be used instead of NonBlockingTCP - HTTP (CONNECT) proxy connection class. Allows to use HTTP proxies like squid with - (optionally) simple authentication (using login and password). - ''' - - def _on_tcp_connect(self): - ''' Starts connection. Connects to proxy, supplies login and password to it - (if were specified while creating instance). Instructs proxy to make - connection to the target server. Returns non-empty sting on success. ''' - log.info('Proxy server contacted, performing authentification') - connector = ['CONNECT %s:%s HTTP/1.0' % self.xmpp_server, - 'Proxy-Connection: Keep-Alive', - 'Pragma: no-cache', - 'Host: %s:%s' % self.xmpp_server, - 'User-Agent: Gajim'] - if self.proxy_user and self.proxy_pass: - credentials = '%s:%s' % (self.proxy_user, self.proxy_pass) - credentials = base64.encodestring(credentials).strip() - connector.append('Proxy-Authorization: Basic '+credentials) - connector.append('\r\n') - self.onreceive(self._on_headers_sent) - self.send('\r\n'.join(connector)) - - def _on_headers_sent(self, reply): - if reply is None: - return - self.reply = reply.replace('\r', '') - try: - proto, code, desc = reply.split('\n')[0].split(' ', 2) - except: - log.error("_on_headers_sent:", exc_info=True) - #traceback.print_exc() - self._on_connect_failure('Invalid proxy reply') - return - if code <> '200': - log.error('Invalid proxy reply: %s %s %s' % (proto, code, desc)) - self._on_connect_failure('Invalid proxy reply') - return - if len(reply) != 2: - pass - NonBlockingTCP._on_connect(self) - #self.onreceive(self._on_proxy_auth) - - def _on_proxy_auth(self, reply): - if self.reply.find('\n\n') == -1: - if reply is None: - self._on_connect_failure('Proxy authentification failed') - return - if reply.find('\n\n') == -1: - self.reply += reply.replace('\r', '') - self._on_connect_failure('Proxy authentification failed') - return - log.info('Authentification successfull. Jabber server contacted.') - self._on_connect(self) - - -class NBSOCKS5ProxySocket(NBProxySocket): - '''SOCKS5 proxy connection class. Uses TCPsocket as the base class - redefines only connect method. Allows to use SOCKS5 proxies with - (optionally) simple authentication (only USERNAME/PASSWORD auth). - ''' - # TODO: replace on_proxy_failure() with - # _on_connect_failure, at the end call _on_connect() - - def _on_tcp_connect(self): - log.info('Proxy server contacted, performing authentification') - if self.proxy.has_key('user') and self.proxy.has_key('password'): - to_send = '\x05\x02\x00\x02' - else: - to_send = '\x05\x01\x00' - self.onreceive(self._on_greeting_sent) - self.send(to_send) - - def _on_greeting_sent(self, reply): - if reply is None: - return - if len(reply) != 2: - self.on_proxy_failure('Invalid proxy reply') - return - if reply[0] != '\x05': - log.info('Invalid proxy reply') - self._owner.disconnected() - self.on_proxy_failure('Invalid proxy reply') - return - if reply[1] == '\x00': - return self._on_proxy_auth('\x01\x00') - elif reply[1] == '\x02': - to_send = '\x01' + chr(len(self.proxy['user'])) + self.proxy['user'] +\ - chr(len(self.proxy['password'])) + self.proxy['password'] - self.onreceive(self._on_proxy_auth) - self.send(to_send) - else: - if reply[1] == '\xff': - log.error('Authentification to proxy impossible: no acceptable ' - 'auth method') - self._owner.disconnected() - self.on_proxy_failure('Authentification to proxy impossible: no ' - 'acceptable authentification method') - return - log.error('Invalid proxy reply') - self._owner.disconnected() - self.on_proxy_failure('Invalid proxy reply') - return - - def _on_proxy_auth(self, reply): - if reply is None: - return - if len(reply) != 2: - log.error('Invalid proxy reply') - self._owner.disconnected() - self.on_proxy_failure('Invalid proxy reply') - return - if reply[0] != '\x01': - log.error('Invalid proxy reply') - self._owner.disconnected() - self.on_proxy_failure('Invalid proxy reply') - return - if reply[1] != '\x00': - log.error('Authentification to proxy failed') - self._owner.disconnected() - self.on_proxy_failure('Authentification to proxy failed') - return - log.info('Authentification successfull. Jabber server contacted.') - # Request connection - req = "\x05\x01\x00" - # If the given destination address is an IP address, we'll - # use the IPv4 address request even if remote resolving was specified. - try: - self.ipaddr = socket.inet_aton(self.server[0]) - req = req + "\x01" + self.ipaddr - except socket.error: - # Well it's not an IP number, so it's probably a DNS name. -# if self.__proxy[3]==True: - # Resolve remotely - self.ipaddr = None - req = req + "\x03" + chr(len(self.server[0])) + self.server[0] -# else: -# # Resolve locally -# self.ipaddr = socket.inet_aton(socket.gethostbyname(self.server[0])) -# req = req + "\x01" + ipaddr - req = req + struct.pack(">H",self.server[1]) - self.onreceive(self._on_req_sent) - self.send(req) - - def _on_req_sent(self, reply): - if reply is None: - return - if len(reply) < 10: - log.error('Invalid proxy reply') - self._owner.disconnected() - self.on_proxy_failure('Invalid proxy reply') - return - if reply[0] != '\x05': - log.error('Invalid proxy reply') - self._owner.disconnected() - self.on_proxy_failure('Invalid proxy reply') - return - if reply[1] != "\x00": - # Connection failed - self._owner.disconnected() - if ord(reply[1])<9: - errors = ['general SOCKS server failure', - 'connection not allowed by ruleset', - 'Network unreachable', - 'Host unreachable', - 'Connection refused', - 'TTL expired', - 'Command not supported', - 'Address type not supported' - ] - txt = errors[ord(reply[1])-1] - else: - txt = 'Invalid proxy reply' - log.error(txt) - self.on_proxy_failure(txt) - return - # Get the bound address/port - elif reply[3] == "\x01": - begin, end = 3, 7 - elif reply[3] == "\x03": - begin, end = 4, 4 + reply[4] - else: - log.error('Invalid proxy reply') - self._owner.disconnected() - self.on_proxy_failure('Invalid proxy reply') - return - - if self.on_connect_proxy: - self.on_connect_proxy() - - - From acdf4ff1434c6f9f7b9f477d9d19887918b816e2 Mon Sep 17 00:00:00 2001 From: tomk Date: Sun, 17 Aug 2008 22:57:48 +0000 Subject: [PATCH 18/20] improved disconnect handling, added comments, fixed minor bugs --- src/common/connection.py | 17 +- src/common/xmpp/bosh.py | 58 +++- src/common/xmpp/client_nb.py | 395 +++++++++++++++------------- src/common/xmpp/proxy_connectors.py | 1 + src/common/xmpp/tls_nb.py | 11 +- src/common/xmpp/transports_nb.py | 156 ++++++----- 6 files changed, 369 insertions(+), 269 deletions(-) diff --git a/src/common/connection.py b/src/common/connection.py index d191c8237..cb09ee795 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -581,8 +581,8 @@ class Connection(ConnectionHandlers): if self.on_connect_success == self._on_new_account: con.RegisterDisconnectHandler(self._on_new_account) - log.info('Connecting to %s: [%s:%d]', self.name, - self._current_host['host'], port) + self.log_hosttype_info(port) + con.connect( hostname=self._current_host['host'], port=port, @@ -594,6 +594,19 @@ class Connection(ConnectionHandlers): else: self.connect_to_next_host(retry) + def log_hosttype_info(self, port): + msg = '>>>>>> Connecting to %s [%s:%d], type = %s' % (self.name, + self._current_host['host'], port, self._current_type) + log.info(msg) + if self._proxy: + msg = '>>>>>> ' + if self._proxy['type']=='bosh': + msg = '%s over BOSH %s:%s' % (msg, self._proxy['bosh_uri'], self._proxy['bosh_port']) + if self._proxy['type'] in ['http','socks5'] or self._proxy['bosh_useproxy']: + msg = '%s over proxy %s:%s' % (msg, self._proxy['host'], self._proxy['port']) + log.info(msg) + + def _connect_failure(self, con_type = None): if not con_type: diff --git a/src/common/xmpp/bosh.py b/src/common/xmpp/bosh.py index 5d1796a99..c6ae1beba 100644 --- a/src/common/xmpp/bosh.py +++ b/src/common/xmpp/bosh.py @@ -30,10 +30,9 @@ log = logging.getLogger('gajim.c.x.bosh') KEY_COUNT = 10 +# Fake file descriptor - it's used for setting read_timeout in idlequeue for +# BOSH Transport. In TCP-derived transports this is file descriptor of socket. FAKE_DESCRIPTOR = -1337 -'''Fake file descriptor - it's used for setting read_timeout in idlequeue for -BOSH Transport. -In TCP-derived transports it is file descriptor of socket''' class NonBlockingBOSH(NonBlockingTransport): @@ -126,10 +125,11 @@ class NonBlockingBOSH(NonBlockingTransport): def on_http_request_possible(self): ''' - Called after HTTP response is received - when another request is possible. + Called when HTTP request it's possible to send a HTTP request. It can be when + socket is connected or when HTTP response arrived. There should be always one pending request to BOSH CM. ''' - log.info('on_http_req possible, state:\n%s' % self.get_current_state()) + log.debug('on_http_req possible, state:\n%s' % self.get_current_state()) if self.get_state()==DISCONNECTED: return #Hack for making the non-secure warning dialog work @@ -137,6 +137,10 @@ class NonBlockingBOSH(NonBlockingTransport): if (hasattr(self._owner, 'NonBlockingNonSASL') or hasattr(self._owner, 'SASL')): self.send_BOSH(None) else: + # If we already got features and no auth module was plugged yet, we are + # probably waiting for confirmation of the "not-secure-connection" dialog. + # We don't send HTTP request in that case. + # see http://lists.jabber.ru/pipermail/ejabberd/2008-August/004027.html return else: self.send_BOSH(None) @@ -144,18 +148,20 @@ class NonBlockingBOSH(NonBlockingTransport): def get_socket_in(self, state): + ''' gets sockets in desired state ''' for s in self.http_socks: if s.get_state()==state: return s return None def get_free_socket(self): + ''' Selects and returns socket eligible for sending a data to.''' if self.http_pipelining: return self.get_socket_in(CONNECTED) else: last_recv_time, tmpsock = 0, None for s in self.http_socks: - # we're interested only into CONNECTED socket with no req pending + # we're interested only in CONNECTED socket with no requests pending if s.get_state()==CONNECTED and s.pending_requests==0: # if there's more of them, we want the one with the least recent data receive # (lowest last_recv_time) @@ -169,6 +175,10 @@ class NonBlockingBOSH(NonBlockingTransport): def send_BOSH(self, payload): + ''' + Tries to send a stanza in payload by appeding it to a buffer and plugging a + free socket for writing. + ''' total_pending_reqs = sum([s.pending_requests for s in self.http_socks]) # when called after HTTP response (Payload=None) and when there are already @@ -192,7 +202,8 @@ class NonBlockingBOSH(NonBlockingTransport): self.get_current_state()) return - # when there's free CONNECTED socket, we flush the data + # when there's free CONNECTED socket, we plug it for write and the data will + # be sent when write is possible if self.get_free_socket(): self.plug_socket() return @@ -209,8 +220,7 @@ class NonBlockingBOSH(NonBlockingTransport): if s: self.connect_and_flush(s) else: - #if len(self.http_socks) > 1: return - print 'connecting sock' + # otherwise create and connect a new one ss = self.get_new_http_socket() self.http_socks.append(ss) self.connect_and_flush(ss) @@ -225,6 +235,15 @@ class NonBlockingBOSH(NonBlockingTransport): log.error('=====!!!!!!!!====> Couldn\'t get free socket in plug_socket())') def build_stanza(self, socket): + ''' + Builds a BOSH body tag from data in buffers and adds key, rid and ack + attributes to it. + This method is called from _do_send() of underlying transport. This is to + ensure rid and keys will be processed in correct order. If I generate them + before plugging a socket for write (and did it for two sockets/HTTP + connections) in parallel, they might be sent in wrong order, which results + in violating the BOSH session and server-side disconnect. + ''' if self.prio_bosh_stanzas: stanza, add_payload = self.prio_bosh_stanzas.pop(0) if add_payload: @@ -244,7 +263,6 @@ class NonBlockingBOSH(NonBlockingTransport): log.info('sending msg with rid=%s to sock %s' % (stanza.getAttr('rid'), id(socket))) - #socket.send(stanza) self.renew_bosh_wait_timeout(self.bosh_wait + 3) return stanza @@ -266,8 +284,12 @@ class NonBlockingBOSH(NonBlockingTransport): self.wait_cb_time) def on_persistent_fallback(self, socket): - log.warn('Fallback to nonpersistent HTTP (no pipelining as well)') + ''' + Called from underlying transport when server closes TCP connection. + :param socket: disconnected transport object + ''' if socket.http_persistent: + log.warn('Fallback to nonpersistent HTTP (no pipelining as well)') socket.http_persistent = False self.http_persistent = False self.http_pipelining = False @@ -279,6 +301,9 @@ class NonBlockingBOSH(NonBlockingTransport): def handle_body_attrs(self, stanza_attrs): + ''' + Called for each incoming body stanza from dispatcher. Checks body attributes. + ''' self.remove_bosh_wait_timeout() if self.after_init: @@ -315,11 +340,13 @@ class NonBlockingBOSH(NonBlockingTransport): def append_stanza(self, stanza): + ''' appends stanza to a buffer to send ''' if stanza: if isinstance(stanza, tuple): # stanza is tuple of BOSH stanza and bool value for whether to add payload self.prio_bosh_stanzas.append(stanza) else: + # stanza is XMPP stanza. Will be boshified before send. self.stanza_buffer.append(stanza) @@ -391,7 +418,6 @@ class NonBlockingBOSH(NonBlockingTransport): if self.use_proxy_auth: http_dict['proxy_user'], http_dict['proxy_pass'] = self.proxy_creds - s = NonBlockingHTTPBOSH( raise_event=self.raise_event, on_disconnect=self.disconnect, @@ -402,6 +428,7 @@ class NonBlockingBOSH(NonBlockingTransport): http_dict = http_dict, proxy_dict = self.proxy_dict, on_persistent_fallback = self.on_persistent_fallback) + s.onreceive(self.on_received_http) s.set_stanza_build_cb(self.build_stanza) return s @@ -439,6 +466,10 @@ def get_rand_number(): class AckChecker(): + ''' + Class for generating rids and generating and checking acknowledgements in + BOSH messages. + ''' def __init__(self): self.rid = get_rand_number() self.ack = 1 @@ -481,6 +512,9 @@ class AckChecker(): class KeyStack(): + ''' + Class implementing key sequences for BOSH messages + ''' def __init__(self, count): self.count = count self.keys = [] diff --git a/src/common/xmpp/client_nb.py b/src/common/xmpp/client_nb.py index 35d5079f4..b7603cc08 100644 --- a/src/common/xmpp/client_nb.py +++ b/src/common/xmpp/client_nb.py @@ -16,11 +16,6 @@ # $Id: client.py,v 1.52 2006/01/02 19:40:55 normanr Exp $ -''' -Provides Client classes implementations as examples of xmpppy structures usage. -These classes can be used for simple applications "AS IS" though. -''' - import socket import transports_nb, dispatcher_nb, auth_nb, roster_nb, protocol, bosh @@ -32,16 +27,19 @@ import logging log = logging.getLogger('gajim.c.x.client_nb') -class NBCommonClient: - ''' Base for Client and Component classes.''' +class NonBlockingClient: + ''' + Client class is XMPP connection mountpoint. Objects for authentication, + network communication, roster, xml parsing ... are plugged to client object. + Client implements the abstract behavior - mostly negotioation and callbacks + handling, whereas underlying modules take care of feature-specific logic. + ''' def __init__(self, domain, idlequeue, caller=None): - - ''' Caches connection data: + ''' + Caches connection data: :param domain: domain - for to: attribute (from account info) :param idlequeue: processing idlequeue - :param port: port of listening XMPP server - :param caller: calling object - it has to implement certain methods (necessary?) - + :param caller: calling object - it has to implement method _event_dispatcher ''' self.Namespace = protocol.NS_CLIENT self.defaultNamespace = self.Namespace @@ -62,19 +60,22 @@ class NBCommonClient: self.on_connect_failure = None self.proxy = None self.got_features = False + self.stream_started = False + self.disconnecting = False + self.protocol_type = 'XMPP' - def on_disconnect(self): + def disconnect(self, message=''): ''' - Called on disconnection - when connect failure occurs on running connection - (after stream is successfully opened). - Calls disconnect handlers and cleans things up. + Called on disconnection - disconnect callback is picked based on state of the + client. ''' - - self.connected='' - for i in reversed(self.disconnect_handlers): - log.debug('Calling disconnect handler %s' % i) - i() + + # to avoid recursive calls + if self.disconnecting: return + + log.warn('Disconnecting NBClient: %s' % message) + if self.__dict__.has_key('NonBlockingRoster'): self.NonBlockingRoster.PlugOut() if self.__dict__.has_key('NonBlockingBind'): @@ -89,7 +90,41 @@ class NBCommonClient: self.NonBlockingHTTP.PlugOut() if self.__dict__.has_key('NonBlockingBOSH'): self.NonBlockingBOSH.PlugOut() + + connected = self.connected + stream_started = self.stream_started + + self.connected = '' + self.stream_started = False + + self.disconnecting = True + log.debug('Client disconnected..') + if connected == '': + # if we're disconnecting before connection to XMPP sever is opened, we don't + # call disconnect handlers but on_connect_failure callback + if self.proxy: + # with proxy, we have different failure callback + log.debug('calling on_proxy_failure cb') + self.on_proxy_failure(reason=message) + else: + log.debug('ccalling on_connect_failure cb') + self.on_connect_failure() + else: + # we are connected to XMPP server + if not stream_started: + # if error occur before XML stream was opened, e.g. no response on init + # request, we call the on_connect_failure callback because proper + # connection is not estabilished yet and it's not a proxy issue + log.debug('calling on_connect_failure cb') + self.on_connect_failure() + else: + # with open connection, we are calling the disconnect handlers + for i in reversed(self.disconnect_handlers): + log.debug('Calling disconnect handler %s' % i) + i() + self.disconnecting = False + def connect(self, on_connect, on_connect_failure, hostname=None, port=5222, @@ -101,11 +136,14 @@ class NBCommonClient: :param on_connect: called after stream is successfully opened :param on_connect_failure: called when error occures during connection :param on_proxy_failure: called if error occurres during TCP connection to - proxy server or during connection to the proxy + proxy server or during proxy connecting process :param proxy: dictionary with proxy data. It should contain at least values for keys 'host' and 'port' - connection details for proxy server and optionally keys 'user' and 'pass' as proxy credentials - :param secure_tuple: + :param secure_tuple: tuple of (desired connection type, cacerts and mycerts) + connection type can be 'ssl' - TLS estabilished after TCP connection, + 'tls' - TLS estabilished after negotiation with starttls, or 'plain'. + cacerts, mycerts - see tls_nb.NonBlockingTLS constructor for more details ''' self.on_connect = on_connect self.on_connect_failure=on_connect_failure @@ -113,16 +151,72 @@ class NBCommonClient: self.secure, self.cacerts, self.mycerts = secure_tuple self.Connection = None self.Port = port + self.proxy = proxy + if hostname: + xmpp_hostname = hostname + else: + xmpp_hostname = self.Server + + estabilish_tls = self.secure == 'ssl' + certs = (self.cacerts, self.mycerts) + + proxy_dict = {} + tcp_host=xmpp_hostname + tcp_port=self.Port + + if proxy: + # with proxies, client connects to proxy instead of directly to + # XMPP server ((hostname, port)) + # tcp_host is hostname of machine used for socket connection + # (DNS request will be done for proxy or BOSH CM hostname) + tcp_host, tcp_port, proxy_user, proxy_pass = \ + transports_nb.get_proxy_data_from_dict(proxy) + + + if proxy['type'] == 'bosh': + self.socket = bosh.NonBlockingBOSH( + on_disconnect = self.disconnect, + raise_event = self.raise_event, + idlequeue = self.idlequeue, + estabilish_tls = estabilish_tls, + certs = certs, + proxy_creds = (proxy_user, proxy_pass), + xmpp_server = (xmpp_hostname, self.Port), + domain = self.Server, + bosh_dict = proxy) + self.protocol_type = 'BOSH' + self.wait_for_restart_response = proxy['bosh_wait_for_restart_response'] + + else: + proxy_dict['type'] = proxy['type'] + proxy_dict['xmpp_server'] = (xmpp_hostname, self.Port) + proxy_dict['credentials'] = (proxy_user, proxy_pass) + + if not proxy or proxy['type'] != 'bosh': + self.socket = transports_nb.NonBlockingTCP( + on_disconnect = self.disconnect, + raise_event = self.raise_event, + idlequeue = self.idlequeue, + estabilish_tls = estabilish_tls, + certs = certs, + proxy_dict = proxy_dict) + + self.socket.PlugIn(self) + + self._resolve_hostname( + hostname=tcp_host, + port=tcp_port, + on_success=self._try_next_ip) - def _resolve_hostname(self, hostname, port, on_success, on_failure): - ''' wrapper of getaddinfo call. FIXME: getaddinfo blocks''' + def _resolve_hostname(self, hostname, port, on_success): + ''' wrapper for getaddinfo call. FIXME: getaddinfo blocks''' try: self.ip_addresses = socket.getaddrinfo(hostname,port, socket.AF_UNSPEC,socket.SOCK_STREAM) except socket.gaierror, (errnum, errstr): - on_failure('Lookup failure for %s:%s, hostname: %s - %s' % + self.disconnect(message= 'Lookup failure for %s:%s, hostname: %s - %s' % (self.Server, self.Port, hostname, errstr)) else: on_success() @@ -130,12 +224,13 @@ class NBCommonClient: def _try_next_ip(self, err_message=None): - '''iterates over IP addresses from getaddinfo''' + '''iterates over IP addresses from getaddrinfo''' if err_message: log.debug('While looping over DNS A records: %s' % err_message) if self.ip_addresses == []: - self._on_tcp_failure('Run out of hosts for name %s:%s' % - (self.Server, self.Port)) + msg = 'Run out of hosts for name %s:%s.' % (self.Server, self.Port) + msg = msg + ' Error for last IP: %s' % err_message + self.disconnect(msg) else: self.current_ip = self.ip_addresses.pop(0) self.socket.connect( @@ -152,19 +247,23 @@ class NBCommonClient: return None def _xmpp_connect(self, socket_type): - if socket_type == 'plain' and self.Connection.ssl_lib: - socket_type = 'ssl' + ''' + Starts XMPP connecting process - opens the XML stream. Is called after TCP + connection is estabilished or after switch to TLS when successfully + negotiated with . + ''' + if socket_type == 'plain' and self.Connection.ssl_lib: socket_type = 'ssl' self.connected = socket_type self._xmpp_connect_machine() def _xmpp_connect_machine(self, mode=None, data=None): ''' - Finite automaton called after TCP connecting. Takes care of stream opening - and features tag handling. Calls _on_stream_start when stream is - started, and _on_connect_failure on failure. + Finite automaton taking care of stream opening and features tag + handling. Calls _on_stream_start when stream is started, and disconnect() + on failure. ''' - log.info('-------------xmpp_connect_machine() >> mode: %s, data: %s' % (mode,str(data)[:20] )) + log.info('-------------xmpp_connect_machine() >> mode: %s, data: %s...' % (mode,str(data)[:20] )) def on_next_receive(mode): log.info('setting %s on next receive' % mode) @@ -182,7 +281,7 @@ class NBCommonClient: on_next_receive('RECEIVE_DOCUMENT_ATTRIBUTES') elif mode == 'FAILURE': - self._on_connect_failure(err_message='During XMPP connect: %s' % data) + self.disconnect('During XMPP connect: %s' % data) elif mode == 'RECEIVE_DOCUMENT_ATTRIBUTES': if data: @@ -219,8 +318,38 @@ class NBCommonClient: elif mode == 'STREAM_STARTED': self._on_stream_start() + def _on_stream_start(self): + ''' + Called after XMPP stream is opened. + TLS negotiation may follow after esabilishing a stream. + ''' + self.stream_started = True + self.onreceive(None) + if self.connected == 'plain': + if self.secure == 'plain': + # if we want plain connection, we're done now + self._on_connect() + return + if not self.Dispatcher.Stream.features.getTag('starttls'): + # if server doesn't advertise TLS in init response, we can't do more + log.warn('While connecting with type = "tls": TLS unsupported by remote server') + self._on_connect() + return + if self.incoming_stream_version() != '1.0': + # if stream version is less than 1.0, we can't do more + log.warn('While connecting with type = "tls": stream version is less than 1.0') + self._on_connect() + return + # otherwise start TLS negotioation + self.stream_started = False + log.info("TLS supported by remote server. Requesting TLS start.") + self._tls_negotiation_handler() + elif self.connected in ['ssl', 'tls']: + self._on_connect() + def _tls_negotiation_handler(self, con=None, tag=None): + ''' takes care of TLS negotioation with ''' log.info('-------------tls_negotiaton_handler() >> tag: %s' % tag) if not con and not tag: # starting state when we send the @@ -232,72 +361,44 @@ class NBCommonClient: else: # we got or if tag.getNamespace() <> NS_TLS: - self._on_connect_failure('Unknown namespace: %s' % tag.getNamespace()) + self.disconnect('Unknown namespace: %s' % tag.getNamespace()) return tagname = tag.getName() if tagname == 'failure': - self._on_connect_failure('TLS received: %s' % tag) + self.disconnect('TLS received: %s' % tag) return log.info('Got starttls proceed response. Switching to TLS/SSL...') # following call wouldn't work for BOSH transport but it doesn't matter - # because TLS negotiation with BOSH is forbidden + # because negotiation with BOSH is forbidden self.Connection.tls_init( on_succ = lambda: self._xmpp_connect(socket_type='tls'), - on_fail = lambda: self._on_connect_failure('error while etabilishing TLS')) + on_fail = lambda: self.disconnect('error while etabilishing TLS')) - def _on_stream_start(self): - '''Called when stream is opened. To be overriden in derived classes.''' - - def _on_connect_failure(self, retry=None, err_message=None): - self.connected = '' - if err_message: - log.debug('While connecting: %s' % err_message) - if self.socket: - self.socket.disconnect() - self.on_connect_failure(retry) - def _on_connect(self): + ''' preceeds call of on_connect callback ''' self.onreceive(None) self.on_connect(self, self.connected) def raise_event(self, event_type, data): + ''' + raises event to connection instance - DATA_SENT and DATA_RECIVED events are + used in XML console to show XMPP traffic + ''' log.info('raising event from transport: >>>>>%s<<<<<\n_____________\n%s\n_____________\n' % (event_type,data)) if hasattr(self, 'Dispatcher'): self.Dispatcher.Event('', event_type, data) - # moved from client.CommonClient (blocking client from xmpppy): - def RegisterDisconnectHandler(self,handler): - """ Register handler that will be called on disconnect.""" - self.disconnect_handlers.append(handler) - - def UnregisterDisconnectHandler(self,handler): - """ Unregister handler that is called on disconnect.""" - self.disconnect_handlers.remove(handler) - - def DisconnectHandler(self): - """ Default disconnect handler. Just raises an IOError. - If you choosed to use this class in your production client, - override this method or at least unregister it. """ - raise IOError('Disconnected from server.') - - def get_connect_type(self): - """ Returns connection state. F.e.: None / 'tls' / 'plain+non_sasl' . """ - return self.connected - - def get_peerhost(self): - ''' get the ip address of the account, from which is made connection - to the server , (e.g. me). - We will create listening socket on the same ip ''' - # FIXME: tuple (ip, port) is expected (and checked for) but port num is useless - return self.socket.peerhost - + # follows code for authentication, resource bind, session and roster download + # def auth(self, user, password, resource = '', sasl = 1, on_auth = None): - ''' Authenticate connnection and bind resource. If resource is not provided - random one or library name used. ''' + ''' + Authenticate connnection and bind resource. If resource is not provided + random one or library name used. + ''' self._User, self._Password, self._Resource, self._sasl = user, password, resource, sasl self.on_auth = on_auth self._on_doc_attrs() @@ -318,7 +419,6 @@ class NBCommonClient: self._Resource = 'xmpppy' auth_nb.NonBlockingNonSASL(self._User, self._Password, self._Resource, self._on_old_auth).PlugIn(self) return - #self.onreceive(self._on_start_sasl) self.SASL.auth() return True @@ -349,7 +449,8 @@ class NBCommonClient: self.onreceive(self._on_auth_bind) return return - + + def _on_auth_bind(self, data): if data: self.Dispatcher.ProcessNonBlocking(data) @@ -386,107 +487,35 @@ class NBCommonClient: self.send(dispatcher_nb.Presence(to=jid, typ=typ)) - -class NonBlockingClient(NBCommonClient): - ''' Example client class, based on CommonClient. ''' - - def __init__(self, domain, idlequeue, caller=None): - NBCommonClient.__init__(self, domain, idlequeue, caller) - self.protocol_type = 'XMPP' - - def connect(self, on_connect, on_connect_failure, hostname=None, port=5222, - on_proxy_failure=None, proxy=None, secure_tuple=None): - - NBCommonClient.connect(self, on_connect, on_connect_failure, hostname, port, - on_proxy_failure, proxy, secure_tuple) - - if hostname: - xmpp_hostname = hostname - else: - xmpp_hostname = self.Server - - estabilish_tls = self.secure == 'ssl' - certs = (self.cacerts, self.mycerts) - - self._on_tcp_failure = self._on_connect_failure - proxy_dict = {} - tcp_host=xmpp_hostname - tcp_port=self.Port - - if proxy: - # with proxies, client connects to proxy instead of directly to - # XMPP server ((hostname, port)) - # tcp_host is hostname of machine used for socket connection - # (DNS request will be done for proxy or BOSH CM hostname) - tcp_host, tcp_port, proxy_user, proxy_pass = \ - transports_nb.get_proxy_data_from_dict(proxy) - - - if proxy['type'] == 'bosh': - self.socket = bosh.NonBlockingBOSH( - on_disconnect = self.on_disconnect, - raise_event = self.raise_event, - idlequeue = self.idlequeue, - estabilish_tls = estabilish_tls, - certs = certs, - proxy_creds = (proxy_user, proxy_pass), - xmpp_server = (xmpp_hostname, self.Port), - domain = self.Server, - bosh_dict = proxy) - self.protocol_type = 'BOSH' - self.wait_for_restart_response = proxy['bosh_wait_for_restart_response'] - - else: - self._on_tcp_failure = self.on_proxy_failure - proxy_dict['type'] = proxy['type'] - proxy_dict['xmpp_server'] = (xmpp_hostname, self.Port) - proxy_dict['credentials'] = (proxy_user, proxy_pass) - - if not proxy or proxy['type'] != 'bosh': - self.socket = transports_nb.NonBlockingTCP( - on_disconnect = self.on_disconnect, - raise_event = self.raise_event, - idlequeue = self.idlequeue, - estabilish_tls = estabilish_tls, - certs = certs, - proxy_dict = proxy_dict) - - self.socket.PlugIn(self) - - self._resolve_hostname( - hostname=tcp_host, - port=tcp_port, - on_success=self._try_next_ip, - on_failure=self._on_tcp_failure) - - - - def _on_stream_start(self): - ''' - Called after XMPP stream is opened. - In pure XMPP client, TLS negotiation may follow after esabilishing a stream. - ''' - self.onreceive(None) - if self.connected == 'plain': - if self.secure == 'plain': - # if we want plain connection, we're done now - self._on_connect() - return - if not self.Dispatcher.Stream.features.getTag('starttls'): - # if server doesn't advertise TLS in init response, we can't do more - log.warn('While connecting with type = "tls": TLS unsupported by remote server') - self._on_connect() - return - if self.incoming_stream_version() != '1.0': - # if stream version is less than 1.0, we can't do more - log.warn('While connecting with type = "tls": stream version is less than 1.0') - self._on_connect() - return - # otherwise start TLS - log.info("TLS supported by remote server. Requesting TLS start.") - self._tls_negotiation_handler() - elif self.connected in ['ssl', 'tls']: - self._on_connect() - + # following methods are moved from blocking client class from xmpppy: + def RegisterDisconnectHandler(self,handler): + ''' Register handler that will be called on disconnect.''' + self.disconnect_handlers.append(handler) + + def UnregisterDisconnectHandler(self,handler): + ''' Unregister handler that is called on disconnect.''' + self.disconnect_handlers.remove(handler) + + def DisconnectHandler(self): + ''' + Default disconnect handler. Just raises an IOError. If you choosed to use + this class in your production client, override this method or at least + unregister it. + ''' + raise IOError('Disconnected from server.') + + def get_connect_type(self): + ''' Returns connection state. F.e.: None / 'tls' / 'plain+non_sasl'. ''' + return self.connected + + def get_peerhost(self): + ''' + Gets the ip address of the account, from which is made connection to the + server , (e.g. IP and port of gajim's socket. We will create listening socket + on the same ip + ''' + # FIXME: tuple (ip, port) is expected (and checked for) but port num is + # useless + return self.socket.peerhost diff --git a/src/common/xmpp/proxy_connectors.py b/src/common/xmpp/proxy_connectors.py index 1b769288a..1cb30d3b1 100644 --- a/src/common/xmpp/proxy_connectors.py +++ b/src/common/xmpp/proxy_connectors.py @@ -19,6 +19,7 @@ import struct, socket, base64 ''' Module containing classes for proxy connecting. So far its HTTP CONNECT and SOCKS5 proxy. +Authentication to NTLM (Microsoft implementation) proxies can be next. ''' import logging diff --git a/src/common/xmpp/tls_nb.py b/src/common/xmpp/tls_nb.py index 72faab425..08f00a7f9 100644 --- a/src/common/xmpp/tls_nb.py +++ b/src/common/xmpp/tls_nb.py @@ -3,6 +3,7 @@ ## ## Copyright (C) 2003-2004 Alexey "Snake" Nezhdanov ## modified by Dimitur Kirov +## modified by Tomas Karasek ## ## 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 @@ -190,7 +191,7 @@ class PyOpenSSLWrapper(SSLWrapper): return 0 class StdlibSSLWrapper(SSLWrapper): - '''Wrapper class for Python's socket.ssl read() and write() methods''' + '''Wrapper class for Python socket.ssl read() and write() methods''' def __init__(self, *args): self.parent = SSLWrapper @@ -221,6 +222,10 @@ class NonBlockingTLS(PlugIn): ''' TLS connection used to encrypts already estabilished tcp connection.''' def __init__(self, cacerts, mycerts): + ''' + :param cacerts: path to pem file with certificates of known XMPP servers + :param mycerts: path to pem file with certificates of user trusted servers + ''' PlugIn.__init__(self) self.cacerts = cacerts self.mycerts = mycerts @@ -239,11 +244,9 @@ class NonBlockingTLS(PlugIn): log.info('Starting TLS estabilishing') PlugIn.PlugIn(self, owner) try: - self._owner._plug_idle(writable=False, readable=False) res = self._startSSL() except Exception, e: log.error("PlugIn: while trying _startSSL():", exc_info=True) - #traceback.print_exc() return False return res @@ -278,7 +281,6 @@ class NonBlockingTLS(PlugIn): if result: log.debug("Synchronous handshake completed") - self._owner._plug_idle(writable=True, readable=False) return True else: return False @@ -361,7 +363,6 @@ class NonBlockingTLS(PlugIn): def _ssl_verify_callback(self, sslconn, cert, errnum, depth, ok): # Exceptions can't propagate up through this callback, so print them here. try: - print 'in ssl verify callback' self._owner.ssl_fingerprint_sha1 = cert.digest('sha1') if errnum == 0: return True diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py index 166dd2956..840adcbb7 100644 --- a/src/common/xmpp/transports_nb.py +++ b/src/common/xmpp/transports_nb.py @@ -84,11 +84,28 @@ CONNECTING = 'CONNECTING' PROXY_CONNECTING = 'PROXY_CONNECTING' CONNECTED = 'CONNECTED' STATES = [DISCONNECTED, CONNECTING, PROXY_CONNECTING, CONNECTED, DISCONNECTING] -# transports have different arguments in constructor and same in connect() -# method + +# Transports have different arguments in constructor and same in connect() +# method. class NonBlockingTransport(PlugIn): + ''' + Abstract class representing a trasport - object responsible for connecting to + XMPP server and putting stanzas on wire in desired form. + ''' def __init__(self, raise_event, on_disconnect, idlequeue, estabilish_tls, certs): + ''' + Each trasport class can have different constructor but it has to have at + least all the arguments of NonBlockingTransport constructor. + + :param raise_event: callback for monitoring of sent and received data + :param on_disconnect: callback called on disconnection during runtime + :param idlequeue: processing idlequeue + :param estabilish_tls: boolean whether to estabilish TLS connection after TCP + connection is done + :param certs: tuple of (cacerts, mycerts) see tls_nb.NonBlockingTLS + constructor for more details + ''' PlugIn.__init__(self) self.raise_event = raise_event self.on_disconnect = on_disconnect @@ -103,7 +120,7 @@ class NonBlockingTransport(PlugIn): self.certs = certs # type of used ssl lib (if any) will be assigned to this member var self.ssl_lib = None - self._exported_methods=[self.disconnect, self.onreceive, self.set_send_timeout, + self._exported_methods=[self.onreceive, self.set_send_timeout, self.set_timeout, self.remove_timeout, self.start_disconnect] # time to wait for SOME stanza to come and then send keepalive @@ -118,10 +135,15 @@ class NonBlockingTransport(PlugIn): def plugout(self): self._owner.Connection = None self._owner = None + self.disconnect(do_callback=False) def connect(self, conn_5tuple, on_connect, on_connect_failure): ''' - connect method should have the same declaration in all derived transports + Creates and connects transport to server and port defined in conn_5tupe which + should be item from list returned from getaddrinfo. + :param conn_5tuple: 5-tuple returned from getaddrinfo + :param on_connect: callback called on successful connect to the server + :param on_connect_failure: callback called on failure when connecting ''' self.on_connect = on_connect self.on_connect_failure = on_connect_failure @@ -164,8 +186,13 @@ class NonBlockingTransport(PlugIn): self.on_disconnect() def onreceive(self, recv_handler): - ''' Sets the on_receive callback. Do not confuse it with - on_receive() method, which is the callback itself.''' + ''' + Sets the on_receive callback. Do not confuse it with on_receive() method, + which is the callback itself. + onreceive(None) sets callback to Dispatcher.ProcessNonBlocking which is the + default one that will decide what to do with received stanza based on its + tag name and namespace. + ''' if not recv_handler: if hasattr(self._owner, 'Dispatcher'): self.on_receive = self._owner.Dispatcher.ProcessNonBlocking @@ -176,9 +203,9 @@ class NonBlockingTransport(PlugIn): def tcp_connecting_started(self): self.set_state(CONNECTING) - # on_connect/on_conn_failure will be called from self.pollin/self.pollout def read_timeout(self): + ''' called when there's no response from server in defined timeout ''' if self.on_timeout: self.on_timeout() self.renew_send_timeout() @@ -212,12 +239,13 @@ class NonBlockingTransport(PlugIn): class NonBlockingTCP(NonBlockingTransport, IdleObject): ''' - Non-blocking TCP socket wrapper + Non-blocking TCP socket wrapper. It is used for simple XMPP connection. Can be + connected via proxy and can estabilish TLS connection. ''' def __init__(self, raise_event, on_disconnect, idlequeue, estabilish_tls, certs, proxy_dict=None): ''' - Class constructor. + :param proxy_dict: dictionary with proxy data as loaded from config file ''' NonBlockingTransport.__init__(self, raise_event, on_disconnect, idlequeue, estabilish_tls, certs) @@ -227,7 +255,7 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): # bytes remained from the last send message self.sendbuff = '' self.proxy_dict = proxy_dict - self.on_remote_disconnect = self.disconnect() + self.on_remote_disconnect = self.disconnect def start_disconnect(self): @@ -236,14 +264,6 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): self.disconnect() def connect(self, conn_5tuple, on_connect, on_connect_failure): - ''' - Creates and connects socket to server and port defined in conn_5tupe which - should be list item returned from getaddrinfo. - :param conn_5tuple: 5-tuple returned from getaddrinfo - :param on_connect: callback called on successful tcp connection - :param on_connect_failure: callback called on failure when estabilishing tcp - connection - ''' NonBlockingTransport.connect(self, conn_5tuple, on_connect, on_connect_failure) log.info('NonBlockingTCP Connect :: About to connect to %s:%s' % (self.server, self.port)) @@ -258,12 +278,13 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): self._recv = self._sock.recv self.fd = self._sock.fileno() - # we want to be notified when send is possible to connected socket + # we want to be notified when send is possible to connected socket because + # it means the TCP connection is estabilished self._plug_idle(writable=True, readable=False) self.peerhost = None + #variable for errno symbol that will be found from exception raised from connect() errnum = 0 - ''' variable for errno symbol that will be found from exception raised from connect() ''' # set timeout for TCP connecting - if nonblocking connect() fails, pollend # is called. If if succeeds pollout is called. @@ -280,15 +301,8 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): log.info('After NB connect() of %s. "%s" raised => CONNECTING' % (id(self),errstr)) self.tcp_connecting_started() return - elif errnum in (0, 10056, errno.EISCONN): - # already connected - this branch is probably useless, nonblocking connect() will - # return EINPROGRESS exception in most cases. When here, we don't need timeout - # on connected descriptor and success callback can be called. - log.info('After connect. "%s" raised => CONNECTED' % errstr) - self._on_connect(self) - return - # if there was some other error, call failure callback and unplug transport + # if there was some other exception, call failure callback and unplug transport # which will also remove read_timeouts for descriptor self._on_connect_failure('Exception while connecting to %s:%s - %s %s' % (self.server, self.port, errnum, errstr)) @@ -312,8 +326,8 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): def _on_connect(self): ''' - Preceeds invoking of on_connect callback. TCP connection is estabilished at - this time. + Preceeds invoking of on_connect callback. TCP connection is already + estabilished by this this time. ''' if self.estabilish_tls: self.tls_init( @@ -324,6 +338,9 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): def tls_init(self, on_succ, on_fail): + ''' + Estabilishes a TLS/SSL on TCP connection by plugging a NonBlockingTLS module + ''' cacerts, mycerts = self.certs result = tls_nb.NonBlockingTLS(cacerts, mycerts).PlugIn(self) if result: on_succ() @@ -342,6 +359,7 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): if self.get_state()==CONNECTING: log.info('%s socket wrapper connected' % id(self)) self.idlequeue.remove_timeout(self.fd) + self._plug_idle(writable=False, readable=False) self.peerhost = self._sock.getsockname() if self.proxy_dict: self._connect_to_proxy() else: self._on_connect() @@ -349,6 +367,7 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): self._do_send() def pollend(self): + '''called on error on TCP connection''' log.info('pollend called, state == %s' % self.get_state()) if self.get_state()==CONNECTING: @@ -358,8 +377,7 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): self.disconnect() def disconnect(self, do_callback=True): - if self.get_state() == DISCONNECTED: - return + if self.get_state() == DISCONNECTED: return self.set_state(DISCONNECTED) self.idlequeue.unplug_idle(self.fd) if self.__dict__.has_key('NonBlockingTLS'): self.NonBlockingTLS.PlugOut() @@ -367,14 +385,12 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): self._sock.shutdown(socket.SHUT_RDWR) self._sock.close() except socket.error, (errnum, errstr): - log.error('Error while disconnecting a socket: %s %s' % (errnum,errstr)) + log.error('Error while disconnecting socket: %s' % errstr) self.fd = -1 NonBlockingTransport.disconnect(self, do_callback) def read_timeout(self): - ''' - Implemntation of IdleObject function called on timeouts from IdleQueue. - ''' + ''' method called when timeout passed ''' log.warn('read_timeout called, state == %s' % self.get_state()) if self.get_state()==CONNECTING: # if read_timeout is called during connecting, connect() didn't end yet @@ -403,11 +419,9 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): If supplied data is unicode string, encode it to utf-8. ''' NonBlockingTransport.send(self, raw_data, now) - r = raw_data - if isinstance(r, unicode): - r = r.encode('utf-8') - elif not isinstance(r, str): - r = ustr(r).encode('utf-8') + + r = self.encode_stanza(raw_data) + if now: self.sendqueue.insert(0, r) self._do_send() @@ -416,6 +430,12 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): self._plug_idle(writable=True, readable=True) + def encode_stanza(self, stanza): + if isinstance(stanza, unicode): + stanza = stanza.encode('utf-8') + elif not isinstance(stanza, str): + stanza = ustr(stanza).encode('utf-8') + return stanza def _plug_idle(self, writable, readable): @@ -433,12 +453,14 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): def _do_send(self): + ''' + Called when send() to connected socket is possible. First message from + sendqueue will be sent. + ''' if not self.sendbuff: if not self.sendqueue: log.warn('calling send on empty buffer and queue') - self._plug_idle( - writable= ((self.sendqueue!=[]) or (self.sendbuff!='')), - readable=True) + self._plug_idle(writable=False, readable=True) return None self.sendbuff = self.sendqueue.pop(0) try: @@ -471,13 +493,16 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): except tls_nb.SSLWrapper.Error, e: log.info("_do_receive, caught SSL error, got %s:" % received , exc_info=True) errnum, errstr = e.exc - + + if received == '': errstr = 'zero bytes on recv' + if (self.ssl_lib is None and received == '') or \ (self.ssl_lib == tls_nb.PYSTDLIB and errnum == 8 ) or \ (self.ssl_lib == tls_nb.PYOPENSSL and errnum == -1 ): # 8 in stdlib: errstr == EOF occured in violation of protocol # -1 in pyopenssl: errstr == Unexpected EOF - log.info("Disconnected by remote server: %s %s" % (errnum, errstr), exc_info=True) + log.info("Disconnected by remote server: #%s, %s" % (errnum, errstr)) + print self.on_remote_disconnect self.on_remote_disconnect() return @@ -489,8 +514,7 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): # this branch is for case of non-fatal SSL errors - None is returned from # recv() but no errnum is set - if received is None: - return + if received is None: return # we have received some bytes, stop the timeout! self.renew_send_timeout() @@ -519,6 +543,13 @@ class NonBlockingHTTP(NonBlockingTCP): def __init__(self, raise_event, on_disconnect, idlequeue, estabilish_tls, certs, on_http_request_possible, on_persistent_fallback, http_dict, proxy_dict = None): + ''' + :param on_http_request_possible: method to call when HTTP request to socket + owned by transport is possible. + :param on_persistent_fallback: callback called when server ends TCP + connection. It doesn't have to be fatal for HTTP session. + :param http_dict: dictionary with data for HTTP request and headers + ''' NonBlockingTCP.__init__(self, raise_event, on_disconnect, idlequeue, estabilish_tls, certs, proxy_dict) @@ -551,8 +582,10 @@ class NonBlockingHTTP(NonBlockingTCP): def _on_receive(self,data): - '''Preceeds passing received data to owner class. Gets rid of HTTP headers - and checks them.''' + ''' + Preceeds passing received data to owner class. Gets rid of HTTP headers and + checks them. + ''' if self.get_state() == PROXY_CONNECTING: NonBlockingTCP._on_receive(self, data) return @@ -648,7 +681,10 @@ class NonBlockingHTTP(NonBlockingTCP): class NonBlockingHTTPBOSH(NonBlockingHTTP): - + ''' + Class for BOSH HTTP connections. Slightly redefines HTTP transport by calling + bosh bodytag generating callback before putting data on wire. + ''' def set_stanza_build_cb(self, build_cb): self.build_cb = build_cb @@ -659,24 +695,10 @@ class NonBlockingHTTPBOSH(NonBlockingHTTP): return if not self.sendbuff: stanza = self.build_cb(socket=self) + stanza = self.encode_stanza(stanza) stanza = self.build_http_message(httpbody=stanza) - if isinstance(stanza, unicode): - stanza = stanza.encode('utf-8') - elif not isinstance(stanza, str): - stanza = ustr(stanza).encode('utf-8') self.sendbuff = stanza - try: - send_count = self._send(self.sendbuff) - if send_count: - sent_data = self.sendbuff[:send_count] - self.sendbuff = self.sendbuff[send_count:] - self._plug_idle(writable = self.sendbuff != '', readable = True) - self.raise_event(DATA_SENT, sent_data) - - except socket.error, e: - log.error('_do_send:', exc_info=True) - traceback.print_exc() - self.disconnect() + NonBlockingTCP._do_send(self) From ed7dd84cfe8ae8cf624a7894e0a9e9de40ebe6f8 Mon Sep 17 00:00:00 2001 From: tomk Date: Mon, 18 Aug 2008 14:34:09 +0000 Subject: [PATCH 19/20] fixed zeroconf to work with refactored dispatcher --- src/common/zeroconf/client_zeroconf.py | 34 +++++++------------ .../zeroconf/connection_handlers_zeroconf.py | 2 +- src/common/zeroconf/connection_zeroconf.py | 10 +++--- 3 files changed, 20 insertions(+), 26 deletions(-) diff --git a/src/common/zeroconf/client_zeroconf.py b/src/common/zeroconf/client_zeroconf.py index ba94f696d..342f58a68 100644 --- a/src/common/zeroconf/client_zeroconf.py +++ b/src/common/zeroconf/client_zeroconf.py @@ -23,6 +23,7 @@ from common.xmpp.idlequeue import IdleObject from common.xmpp import dispatcher_nb, simplexml from common.xmpp.client import * from common.xmpp.simplexml import ustr +from common.xmpp.transports_nb import DATA_RECEIVED, DATA_SENT from common.zeroconf import zeroconf from common.xmpp.protocol import * @@ -36,8 +37,6 @@ log = logging.getLogger('gajim.c.z.client_zeroconf') from common.zeroconf import roster_zeroconf MAX_BUFF_LEN = 65536 -DATA_RECEIVED = 'DATA RECEIVED' -DATA_SENT = 'DATA SENT' TYPE_SERVER, TYPE_CLIENT = range(2) # wait XX sec to establish a connection @@ -120,6 +119,7 @@ class P2PClient(IdleObject): on_ok=None, on_not_ok=None): self._owner = self self.Namespace = 'jabber:client' + self.protocol_type = 'XMPP' self.defaultNamespace = self.Namespace self._component = 0 self._registered_name = None @@ -130,16 +130,7 @@ class P2PClient(IdleObject): self.Server = host self.on_ok = on_ok self.on_not_ok = on_not_ok - self.DBG = 'client' self.Connection = None - if gajim.verbose: - debug = ['always', 'nodebuilder'] - else: - debug = [] - self._DEBUG = Debug.Debug(debug) - self.DEBUG = self._DEBUG.Show - self.debug_flags = self._DEBUG.debug_flags - self.debug_flags.append(self.DBG) self.sock_hash = None if _sock: self.sock_type = TYPE_SERVER @@ -202,8 +193,6 @@ class P2PClient(IdleObject): self.Dispatcher.Stream._dispatch_depth = 2 self.Dispatcher.Stream.dispatch = self.Dispatcher.dispatch self.Dispatcher.Stream.stream_header_received = self._check_stream_start - self.debug_flags.append(simplexml.DBG_NODEBUILDER) - self.Dispatcher.Stream.DEBUG = self.DEBUG self.Dispatcher.Stream.features = None if self.sock_type == TYPE_CLIENT: self.send_stream_header() @@ -221,8 +210,7 @@ class P2PClient(IdleObject): def _check_stream_start(self, ns, tag, attrs): if ns<>NS_STREAMS or tag<>'stream': - self.Connection.DEBUG('Incorrect stream start: (%s,%s).Terminating! ' \ - % (tag, ns), 'error') + log.error('Incorrect stream start: (%s,%s).Terminating!' % (tag, ns), 'error') self.Connection.disconnect() if self.on_not_ok: self.on_not_ok('Connection to host could not be established: Incorrect answer from server.') @@ -472,13 +460,13 @@ class P2PConnection(IdleObject, PlugIn): if self._owner.sock_type == TYPE_CLIENT: self.set_timeout(ACTIVITY_TIMEOUT_SECONDS) if received.strip(): - self.DEBUG(received, 'got') + log.debug('received: %s', received) if hasattr(self._owner, 'Dispatcher'): self._owner.Dispatcher.Event('', DATA_RECEIVED, received) self.on_receive(received) else: # This should never happed, so we need the debug - self.DEBUG('Unhandled data received: %s' % received,'error') + log.error('Unhandled data received: %s' % received) self.disconnect() return True @@ -553,7 +541,7 @@ class P2PConnection(IdleObject, PlugIn): def _on_send(self): if self.sent_data and self.sent_data.strip(): - self.DEBUG(self.sent_data,'sent') + log.debug('sent: %s' % self.sent_data) if hasattr(self._owner, 'Dispatcher'): self._owner.Dispatcher.Event('', DATA_SENT, self.sent_data) self.sent_data = None @@ -562,7 +550,7 @@ class P2PConnection(IdleObject, PlugIn): self.buff_is_message = False def _on_send_failure(self): - self.DEBUG("Socket error while sending data",'error') + log.error('Socket error while sending data') self._owner.disconnected() self.sent_data = None @@ -698,19 +686,23 @@ class ClientZeroconf: # look for hashed connections if to in self.recipient_to_hash: conn = self.connections[self.recipient_to_hash[to]] + id = conn.Dispatcher.getAnID() + stanza.setID(id) if conn.add_stanza(stanza, is_message): if on_ok: on_ok() - return 0 + return id if item['address'] in self.ip_to_hash: hash = self.ip_to_hash[item['address']] if self.hash_to_port[hash] == item['port']: conn = self.connections[hash] + id = conn.Dispatcher.getAnID() + stanza.setID(id) if conn.add_stanza(stanza, is_message): if on_ok: on_ok() - return 0 + return id # otherwise open new connection P2PClient(None, item['address'], item['port'], self, diff --git a/src/common/zeroconf/connection_handlers_zeroconf.py b/src/common/zeroconf/connection_handlers_zeroconf.py index 3befa52a3..ba43b684c 100644 --- a/src/common/zeroconf/connection_handlers_zeroconf.py +++ b/src/common/zeroconf/connection_handlers_zeroconf.py @@ -460,7 +460,7 @@ class ConnectionHandlersZeroconf(ConnectionVcard, ConnectionBytestream, connecti else: # XXX this shouldn't be hardcoded if isinstance(session, ChatControlSession): - session.received(frm, msgtxt, tim, encrypted, subject, msg) + session.received(frm, msgtxt, tim, encrypted, msg) else: session.received(msg) # END messageCB diff --git a/src/common/zeroconf/connection_zeroconf.py b/src/common/zeroconf/connection_zeroconf.py index 18f4250b6..b9d6a9641 100644 --- a/src/common/zeroconf/connection_zeroconf.py +++ b/src/common/zeroconf/connection_zeroconf.py @@ -138,6 +138,7 @@ class ConnectionZeroconf(ConnectionHandlersZeroconf): if gajim.handlers.has_key(event): gajim.handlers[event](self.name, data) + def _reconnect(self): # Do not try to reco while we are already trying self.time_to_reconnect = None @@ -449,18 +450,19 @@ class ConnectionZeroconf(ConnectionHandlersZeroconf): else: kind = 'single_msg_sent' gajim.logger.write(kind, jid, log_msg) - + self.dispatch('MSGSENT', (jid, msg, keyID)) def on_send_not_ok(reason): reason += ' ' + _('Your message could not be sent.') self.dispatch('MSGERROR', [jid, '-1', reason, None, None, session]) - ret = self.connection.send(msg_iq, msg != None, on_ok=on_send_ok, on_not_ok=on_send_not_ok) if ret == -1: # Contact Offline self.dispatch('MSGERROR', [jid, '-1', _('Contact is offline. Your message could not be sent.'), None, None, session]) + return ret + def send_stanza(self, stanza): # send a stanza untouched @@ -547,9 +549,9 @@ class ConnectionZeroconf(ConnectionHandlersZeroconf): def _event_dispatcher(self, realm, event, data): if realm == '': - if event == common.xmpp.transports.DATA_RECEIVED: + if event == common.xmpp.transports_nb.DATA_RECEIVED: self.dispatch('STANZA_ARRIVED', unicode(data, errors = 'ignore')) - elif event == common.xmpp.transports.DATA_SENT: + elif event == common.xmpp.transports_nb.DATA_SENT: self.dispatch('STANZA_SENT', unicode(data)) # END ConnectionZeroconf From a7c36048b93ec338e23687e29a96d2ac1d420217 Mon Sep 17 00:00:00 2001 From: tomk Date: Sun, 31 Aug 2008 23:40:06 +0000 Subject: [PATCH 20/20] - renamed src/common/nslookup.py to resolver.py - refactored resolver code and added asynchronous resolver based on patch by Damien Thebault[1] * Uses libasyncns-python[2]. If it's not available, old nslookup resolver is used) * works for SRV requests only at the moment [1] https://www.lagaule.org/pipermail/gajim-devel/2008-July/000460.html [2] https://code.launchpad.net/libasyncns-python --- src/common/{nslookup.py => resolver.py} | 173 +++++++++++++++++++----- src/common/xmpp/client_nb.py | 1 + src/gajim.py | 9 +- test/lib/__init__.py | 50 +++++++ test/lib/data.py | 77 +++++++++++ test/{ => lib}/mock.py | 2 + test/lib/mocks.py | 145 ++++++++++++++++++++ test/{ => lib}/notify.py | 2 + test/mocks.py | 68 ---------- test/test_resolver.py | 95 +++++++++++++ 10 files changed, 517 insertions(+), 105 deletions(-) rename src/common/{nslookup.py => resolver.py} (68%) create mode 100644 test/lib/__init__.py create mode 100755 test/lib/data.py rename test/{ => lib}/mock.py (99%) create mode 100644 test/lib/mocks.py rename test/{ => lib}/notify.py (96%) delete mode 100644 test/mocks.py create mode 100644 test/test_resolver.py diff --git a/src/common/nslookup.py b/src/common/resolver.py similarity index 68% rename from src/common/nslookup.py rename to src/common/resolver.py index ae535993e..52c297ff7 100644 --- a/src/common/nslookup.py +++ b/src/common/resolver.py @@ -1,4 +1,4 @@ -## common/nslookup.py +## common/resolver.py ## ## Copyright (C) 2006 Dimitur Kirov ## @@ -23,6 +23,7 @@ import re from xmpp.idlequeue import * +# needed for nslookup if os.name == 'nt': from subprocess import * # python24 only. we ask this for Windows elif os.name == 'posix': @@ -34,13 +35,138 @@ ns_type_pattern = re.compile('^[a-z]+$') # match srv host_name host_pattern = re.compile('^[a-z0-9\-._]*[a-z0-9]\.[a-z]{2,}$') -class Resolver: +USE_LIBASYNCNS = False + +try: + #raise ImportError("Manually disabled libasync") + import libasyncns + USE_LIBASYNCNS = True + log.info("libasyncns-python loaded") +except ImportError: + log.debug("Import of libasyncns-python failed, getaddrinfo will block", exc_info=True) + + # FIXME: Remove these prints before release, replace with a warning dialog. + print >> sys.stderr, "=" * 79 + print >> sys.stderr, "libasyncns-python not installed which means:" + print >> sys.stderr, " - nslookup will be used for SRV and TXT requests" + print >> sys.stderr, " - getaddrinfo will block" + print >> sys.stderr, "libasyncns-python can be found at https://launchpad.net/libasyncns-python" + print >> sys.stderr, "=" * 79 + + +def get_resolver(idlequeue): + if USE_LIBASYNCNS: + return LibAsyncNSResolver() + else: + return NSLookupResolver(idlequeue) + +class CommonResolver(): + def __init__(self): + # dict {"host+type" : list of records} + self.resolved_hosts = {} + # dict {"host+type" : list of callbacks} + self.handlers = {} + + def resolve(self, host, on_ready, type='srv'): + assert(type in ['srv', 'txt']) + if not host: + # empty host, return empty list of srv records + on_ready([]) + return + if self.resolved_hosts.has_key(host+type): + # host is already resolved, return cached values + on_ready(host, self.resolved_hosts[host+type]) + return + if self.handlers.has_key(host+type): + # host is about to be resolved by another connection, + # attach our callback + self.handlers[host+type].append(on_ready) + else: + # host has never been resolved, start now + self.handlers[host+type] = [on_ready] + self.start_resolve(host, type) + + def _on_ready(self, host, type, result_list): + # practically it is impossible to be the opposite, but who knows :) + if not self.resolved_hosts.has_key(host+type): + self.resolved_hosts[host+type] = result_list + if self.handlers.has_key(host+type): + for callback in self.handlers[host+type]: + callback(host, result_list) + del(self.handlers[host+type]) + + def start_resolve(self, host, type): + pass + + +class LibAsyncNSResolver(CommonResolver): + ''' + Asynchronous resolver using libasyncns-python. process() method has to be called + in order to proceed the pending requests. + Based on patch submitted by Damien Thebault. + ''' + def __init__(self): + self.asyncns = libasyncns.Asyncns() + CommonResolver.__init__(self) + + def start_resolve(self, host, type): + type = libasyncns.ns_t_srv + if type == 'txt': type = libasyncns.ns_t_txt + resq = self.asyncns.res_query(host, libasyncns.ns_c_in, type) + resq.userdata = {'host':host, 'type':type} + + # getaddrinfo to be done + #def resolve_name(self, dname, callback): + #resq = self.asyncns.getaddrinfo(dname) + #resq.userdata = {'callback':callback, 'dname':dname} + + def _on_ready(self, host, type, result_list): + if type == libasyncns.ns_t_srv: type = 'srv' + elif type == libasyncns.ns_t_txt: type = 'txt' + + CommonResolver._on_ready(self, host, type, result_list) + + + def process(self): + try: + self.asyncns.wait(False) + resq = self.asyncns.get_next() + except: + return True + if type(resq) == libasyncns.ResQuery: + # TXT or SRV result + while resq is not None: + try: + rl = resq.get_done() + except: + rl = [] + if rl: + for r in rl: + r['prio'] = r['pref'] + self._on_ready( + host = resq.userdata['host'], + type = resq.userdata['type'], + result_list = rl) + try: + resq = self.asyncns.get_next() + except: + resq = None + elif type(resq) == libasyncns.AddrInfoQuery: + # getaddrinfo result (A or AAAA) + rl = resq.get_done() + resq.userdata['callback'](resq.userdata['dname'], rl) + return True + +class NSLookupResolver(CommonResolver): + ''' + Asynchronous DNS resolver calling nslookup. Processing of pending requests + is invoked from idlequeue which is watching file descriptor of pipe of stdout + of nslookup process. + ''' def __init__(self, idlequeue): self.idlequeue = idlequeue - # dict {host : list of srv records} - self.resolved_hosts = {} - # dict {host : list of callbacks} - self.handlers = {} + self.process = False + CommonResolver.__init__(self) def parse_srv_result(self, fqdn, result): ''' parse the output of nslookup command and return list of @@ -133,42 +259,19 @@ class Resolver: 'prio': prio}) return hosts - def _on_ready(self, host, result): + def _on_ready(self, host, type, result): # nslookup finished, parse the result and call the handlers result_list = self.parse_srv_result(host, result) + CommonResolver._on_ready(self, host, type, result_list) - # practically it is impossible to be the opposite, but who knows :) - if not self.resolved_hosts.has_key(host): - self.resolved_hosts[host] = result_list - if self.handlers.has_key(host): - for callback in self.handlers[host]: - callback(host, result_list) - del(self.handlers[host]) - def start_resolve(self, host): + def start_resolve(self, host, type): ''' spawn new nslookup process and start waiting for results ''' - ns = NsLookup(self._on_ready, host) + ns = NsLookup(self._on_ready, host, type) ns.set_idlequeue(self.idlequeue) ns.commandtimeout = 10 ns.start() - def resolve(self, host, on_ready): - if not host: - # empty host, return empty list of srv records - on_ready([]) - return - if self.resolved_hosts.has_key(host): - # host is already resolved, return cached values - on_ready(host, self.resolved_hosts[host]) - return - if self.handlers.has_key(host): - # host is about to be resolved by another connection, - # attach our callback - self.handlers[host].append(on_ready) - else: - # host has never been resolved, start now - self.handlers[host] = [on_ready] - self.start_resolve(host) # TODO: move IdleCommand class in other file, maybe helpers ? class IdleCommand(IdleObject): @@ -268,7 +371,7 @@ class IdleCommand(IdleObject): self._return_result() class NsLookup(IdleCommand): - def __init__(self, on_result, host='_xmpp-client', type = 'srv'): + def __init__(self, on_result, host='_xmpp-client', type='srv'): IdleCommand.__init__(self, on_result) self.commandtimeout = 10 self.host = host.lower() @@ -288,7 +391,7 @@ class NsLookup(IdleCommand): def _return_result(self): if self.result_handler: - self.result_handler(self.host, self.result) + self.result_handler(self.host, self.type, self.result) self.result_handler = None # below lines is on how to use API and assist in testing diff --git a/src/common/xmpp/client_nb.py b/src/common/xmpp/client_nb.py index b7603cc08..088475b90 100644 --- a/src/common/xmpp/client_nb.py +++ b/src/common/xmpp/client_nb.py @@ -40,6 +40,7 @@ class NonBlockingClient: :param domain: domain - for to: attribute (from account info) :param idlequeue: processing idlequeue :param caller: calling object - it has to implement method _event_dispatcher + which is called from dispatcher instance ''' self.Namespace = protocol.NS_CLIENT self.defaultNamespace = self.Namespace diff --git a/src/gajim.py b/src/gajim.py index be9eaab1a..e5f0b3e03 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -260,7 +260,7 @@ import common.sleepy from common.xmpp import idlequeue from common.zeroconf import connection_zeroconf -from common import nslookup +from common import resolver from common import proxy65_manager from common import socks5 from common import helpers @@ -3077,7 +3077,7 @@ class Interface: # gajim.idlequeue.process() each foo miliseconds gajim.idlequeue = GlibIdleQueue() # resolve and keep current record of resolved hosts - gajim.resolver = nslookup.Resolver(gajim.idlequeue) + gajim.resolver = resolver.get_resolver(gajim.idlequeue) gajim.socks5queue = socks5.SocksQueue(gajim.idlequeue, self.handle_event_file_rcv_completed, self.handle_event_file_progress) @@ -3222,6 +3222,11 @@ class Interface: self.last_ftwindow_update = 0 gobject.timeout_add(100, self.autoconnect) + + # when using libasyncns we need to process resolver in regular intervals + if resolver.USE_LIBASYNCNS: + gobject.timeout_add(200, gajim.resolver.process) + if os.name == 'nt': gobject.timeout_add(200, self.process_connections) else: diff --git a/test/lib/__init__.py b/test/lib/__init__.py new file mode 100644 index 000000000..2b99a7e23 --- /dev/null +++ b/test/lib/__init__.py @@ -0,0 +1,50 @@ +import sys +import os.path +import getopt + +use_x = True +shortargs = 'hnv:' +longargs = 'help no-x verbose=' +opts, args = getopt.getopt(sys.argv[1:], shortargs, longargs.split()) +for o, a in opts: + if o in ('-n', '--no-x'): + use_x = False + +gajim_root = os.path.join(os.path.abspath(os.path.dirname(__file__)), '../..') + +# look for modules in the CWD, then gajim/test/lib, then gajim/src, then everywhere else +sys.path.insert(1, gajim_root + '/src') +sys.path.insert(1, gajim_root + '/test/lib') + +# a temporary version of ~/.gajim for testing +configdir = gajim_root + '/test/tmp' + +# define _ for i18n +import __builtin__ +__builtin__._ = lambda x: x + +import os + +def setup_env(): + # wipe config directory + if os.path.isdir(configdir): + import shutil + shutil.rmtree(configdir) + + os.mkdir(configdir) + + import common.configpaths + common.configpaths.gajimpaths.init(configdir) + common.configpaths.gajimpaths.init_profile() + + # for some reason common.gajim needs to be imported before xmpppy? + from common import gajim + + gajim.DATA_DIR = gajim_root + '/data' + gajim.use_x = use_x + + if use_x: + import gtkgui_helpers + gtkgui_helpers.GLADE_DIR = gajim_root + '/data/glade' + +# vim: se ts=3: diff --git a/test/lib/data.py b/test/lib/data.py new file mode 100755 index 000000000..074a7def8 --- /dev/null +++ b/test/lib/data.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +account1 = u'acc1' +account2 = u'Cool"chârßéµö' +account3 = u'dingdong.org' + +contacts = {} +contacts[account1] = { + u'myjid@'+account1: { + 'ask': None, 'groups': [], 'name': None, 'resources': {}, + 'subscription': u'both'}, + u'default1@gajim.org': { + 'ask': None, 'groups': [], 'name': None, 'resources': {}, + 'subscription': u'both'}, + u'default2@gajim.org': { + 'ask': None, 'groups': [u'GroupA',], 'name': None, 'resources': {}, + 'subscription': u'both'}, + u'Cool"chârßéµö@gajim.org': { + 'ask': None, 'groups': [u'' % self.thread_id + + def __nonzero__(self): + return True + + def __eq__(self, other): + return self is other + +# vim: se ts=3: diff --git a/test/notify.py b/test/lib/notify.py similarity index 96% rename from test/notify.py rename to test/lib/notify.py index 2e55e6959..f14100af3 100644 --- a/test/notify.py +++ b/test/lib/notify.py @@ -13,3 +13,5 @@ def get_show_in_roster(event, account, contact, session = None): def get_show_in_systray(event, account, contact, type_ = None): return True + +# vim: se ts=3: \ No newline at end of file diff --git a/test/mocks.py b/test/mocks.py deleted file mode 100644 index 213f79ddf..000000000 --- a/test/mocks.py +++ /dev/null @@ -1,68 +0,0 @@ -# gajim-specific mock objects -from mock import Mock - -from common import gajim - -class MockConnection(Mock): - def __init__(self, name, *args): - Mock.__init__(self, *args) - self.name = name - gajim.connections[name] = self - -class MockWindow(Mock): - def __init__(self, *args): - Mock.__init__(self, *args) - self.window = Mock() - -class MockChatControl(Mock): - def __init__(self, *args): - Mock.__init__(self, *args) - - self.parent_win = MockWindow({'get_active_control': self}) - self.session = None - - def set_session(self, sess): - self.session = sess - - def __nonzero__(self): - return True - - def __eq__(self, other): - return self is other - -class MockInterface(Mock): - def __init__(self, acct, *args): - Mock.__init__(self, *args) - self.msg_win_mgr = Mock() - self.roster = Mock() - - self.remote_ctrl = None - self.minimized_controls = { acct: {} } - -class MockLogger(Mock): - def __init__(self): - Mock.__init__(self, {'write': None}) - -class MockContact(Mock): - def __nonzero__(self): - return True - -import random - -class MockSession(Mock): - def __init__(self, conn, jid, thread_id, type): - Mock.__init__(self) - - self.conn = conn - self.jid = jid - self.type = type - self.thread_id = thread_id - - if not self.thread_id: - self.thread_id = '%0x' % random.randint(0, 10000) - - def __repr__(self): - print '' % self.thread_id - - def __nonzero__(self): - return True diff --git a/test/test_resolver.py b/test/test_resolver.py new file mode 100644 index 000000000..4cab122a0 --- /dev/null +++ b/test/test_resolver.py @@ -0,0 +1,95 @@ +import unittest + +import time + +import lib +lib.setup_env() + +from common import resolver +from gajim import GlibIdleQueue + +from mock import Mock, expectParams +from mocks import * + +import gtk + + +GMAIL_SRV_NAME = '_xmpp-client._tcp.gmail.com' +NONSENSE_NAME = 'sfsdfsdfsdf.sdfs.fsd' +JABBERCZ_TXT_NAME = '_xmppconnect.jabber.cz' +JABBERCZ_SRV_NAME = '_xmpp-client._tcp.jabber.cz' + +TEST_LIST = [(GMAIL_SRV_NAME, 'srv', True), + (NONSENSE_NAME, 'srv', False), + (JABBERCZ_SRV_NAME, 'srv', True)] + +class TestResolver(unittest.TestCase): + def setUp(self): + self.iq = GlibIdleQueue() + self.reset() + self.resolver = None + + def reset(self): + self.flag = False + self.expect_results = False + self.nslookup = False + self.resolver = None + + def testLibAsyncNSResolver(self): + self.reset() + if not resolver.USE_LIBASYNCNS: + print 'testLibAsyncResolver: libasyncns-python not installed' + return + self.resolver = resolver.LibAsyncNSResolver() + + for name, type, expect_results in TEST_LIST: + self.expect_results = expect_results + self.runLANSR(name, type) + self.flag = False + + def runLANSR(self, name, type): + self.resolver.resolve( + host = name, + type = type, + on_ready = self.myonready) + while not self.flag: + time.sleep(1) + self.resolver.process() + + + def myonready(self, name, result_set): + print 'on_ready called ...' + print 'hostname: %s' % name + print 'result set: %s' % result_set + print 'res.resolved_hosts: %s' % self.resolver.resolved_hosts + if self.expect_results: + self.assert_(len(result_set) > 0) + else: + self.assert_(result_set == []) + self.flag = True + if self.nslookup: self._testNSLR() + + + def testNSLookupResolver(self): + self.reset() + self.nslookup = True + self.resolver = resolver.NSLookupResolver(self.iq) + self.test_list = TEST_LIST + self._testNSLR() + try: + gtk.main() + except KeyboardInterrupt: + print 'KeyboardInterrupt caught' + + def _testNSLR(self): + if self.test_list == []: + gtk.main_quit() + return + name, type, self.expect_results = self.test_list.pop() + self.resolver.resolve( + host = name, + type = type, + on_ready = self.myonready) + +if __name__ == '__main__': + unittest.main()