From f130ce29c0a61bab5274c898f3093b4c5374fdd8 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Thu, 3 Nov 2005 14:46:56 +0000 Subject: [PATCH] we now stringprep jid and resource everywhere: when we recieve a jid from server when we enter a jid or resource in GUI --- src/common/connection.py | 27 ++-- src/common/helpers.py | 124 ++++++++++++++--- src/common/xmpp_stringprep.py | 246 ++++++++++++++++++++++++++++++++++ src/config.py | 64 +++++---- src/dialogs.py | 17 +-- 5 files changed, 406 insertions(+), 72 deletions(-) create mode 100644 src/common/xmpp_stringprep.py diff --git a/src/common/connection.py b/src/common/connection.py index 2e3e65079..433705077 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -177,25 +177,14 @@ class Connection: self.retrycount = 0 # END __init__ - def strip_jid(self, jid): - '''look into gajim.contacts if we already have this jid CASE INSENSITIVE - and returns it - the function accetps jid and fjid''' - ji, resource = gajim.get_room_and_nick_from_fjid(jid) - for rjid in gajim.contacts[self.name]: - if ji.lower() == rjid.lower(): # we found the jid - ji = rjid - if resource: - return ji + '/' + resource - return ji - def get_full_jid(self, iq_obj): '''return the full jid (with resource) from an iq as unicode''' - return unicode(self.strip_jid(str(iq_obj.getFrom()))) + return helpers.parse_jid(str(iq_obj.getFrom())) def get_jid(self, iq_obj): '''return the jid (without resource) from an iq as unicode''' - return unicode(self.strip_jid(iq_obj.getFrom().getStripped())) + jid = self.get_full_jid(iq_obj) + return gajim.get_jid_without_resource(jid) def put_event(self, ev): if gajim.events_for_ui.has_key(self.name): @@ -1096,7 +1085,7 @@ class Connection: def _rosterSetCB(self, con, iq_obj): gajim.log.debug('rosterSetCB') for item in iq_obj.getTag('query').getChildren(): - jid = item.getAttr('jid') + jid = helprs.parse_jid(item.getAttr('jid')) name = item.getAttr('name') sub = item.getAttr('subscription') ask = item.getAttr('ask') @@ -1266,9 +1255,11 @@ class Connection: def _getRosterCB(self, con, iq_obj): if not self.connection: return - roster = self.connection.getRoster().getRaw().copy() - if not roster: - roster = {} + r = self.connection.getRoster().getRaw() + + roster = {} + for jid in r: + roster[helpers.parse_jid(jid)] = r[jid] jid = gajim.get_jid_from_account(self.name) diff --git a/src/common/helpers.py b/src/common/helpers.py index 8a096855c..78cb0b0dd 100644 --- a/src/common/helpers.py +++ b/src/common/helpers.py @@ -26,6 +26,7 @@ import stat import gajim from common import i18n +from common.xmpp_stringprep import nodeprep, resourceprep, nameprep try: import winsound # windows-only built-in module for playing wav @@ -35,15 +36,104 @@ except: _ = i18n._ Q_ = i18n.Q_ +class InvalidFormat(Exception): + pass + +def parse_jid(jidstring): + '''Perform stringprep on all JID fragments from a string + and return the full jid''' + # This function comes from http://svn.twistedmatrix.com/cvs/trunk/twisted/words/protocols/jabber/jid.py + + user = None + server = None + resource = None + + # Search for delimiters + user_sep = jidstring.find("@") + res_sep = jidstring.find("/") + + if user_sep == -1: + if res_sep == -1: + # host + server = jidstring + else: + # host/resource + server = jidstring[0:res_sep] + resource = jidstring[res_sep + 1:] or None + else: + if res_sep == -1: + # user@host + user = jidstring[0:user_sep] or None + server = jidstring[user_sep + 1:] + else: + if user_sep < res_sep: + # user@host/resource + user = jidstring[0:user_sep] or None + server = jidstring[user_sep + 1:user_sep + (res_sep - user_sep)] + resource = jidstring[res_sep + 1:] or None + else: + # server/resource (with an @ in resource) + server = jidstring[0:res_sep] + resource = jidstring[res_sep + 1:] or None + + return prep(user, server, resource) + +def parse_resource(resource): + '''Perform stringprep on resource and return it''' + if resource: + try: + return resourceprep.prepare(unicode(resource)) + except UnicodeError: + raise InvalidFormat, "Invalid character in resource" + +def prep(user, server, resource): + '''Perform stringprep on all JID fragments and return the full jid''' + # This function comes from http://svn.twistedmatrix.com/cvs/trunk/twisted/words/protocols/jabber/jid.py + + if user: + try: + user = nodeprep.prepare(unicode(user)) + except UnicodeError: + raise InvalidFormat, "Invalid character in username" + else: + user = None + + if not server: + raise InvalidFormat, "Server address required." + else: + try: + server = nameprep.prepare(unicode(server)) + except UnicodeError: + raise InvalidFormat, "Invalid character in hostname" + + if resource: + try: + resource = resourceprep.prepare(unicode(resource)) + except UnicodeError: + raise InvalidFormat, "Invalid character in resource" + else: + resource = None + + if user: + if resource: + return "%s@%s/%s" % (user, server, resource) + else: + return "%s@%s" % (user, server) + else: + if resource: + return "%s/%s" % (server, resource) + else: + return server + def temp_failure_retry(func, *args, **kwargs): - while True: - try: - return func(*args, **kwargs) - except (os.error, IOError), ex: - if ex.errno == errno.EINTR: - continue - else: - raise + while True: + try: + return func(*args, **kwargs) + except (os.error, IOError), ex: + if ex.errno == errno.EINTR: + continue + else: + raise def check_paths(): LOGPATH = gajim.LOGPATH @@ -422,13 +512,13 @@ def one_account_connected(): return one_connected def get_output_of_command(command): - try: - child_stdin, child_stdout = os.popen2(command) - except ValueError: - return None + try: + child_stdin, child_stdout = os.popen2(command) + except ValueError: + return None - output = child_stdout.readlines() - child_stdout.close() - child_stdin.close() - - return output + output = child_stdout.readlines() + child_stdout.close() + child_stdin.close() + + return output diff --git a/src/common/xmpp_stringprep.py b/src/common/xmpp_stringprep.py new file mode 100644 index 000000000..9dbbbfc00 --- /dev/null +++ b/src/common/xmpp_stringprep.py @@ -0,0 +1,246 @@ +## common/xmpp_stringprep.py +## +## Gajim Team: +## - Yann Le Boulanger +## - Vincent Hanquez +## - Nikos Kouremenos +## +## Copyright (C) 2001-2005 Twisted Matrix Laboratories. +## Copyright (C) 2005 Gajim Team +## +## 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; version 2 only. +## +## 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 sys, warnings + +if sys.version_info < (2,3,2): + import re + + class IDNA: + dots = re.compile(u"[\u002E\u3002\uFF0E\uFF61]") + def nameprep(self, label): + return label.lower() + + idna = IDNA() + + crippled = True + + warnings.warn("Accented and non-Western Jabber IDs will not be properly " + "case-folded with this version of Python, resulting in " + "incorrect protocol-level behavior. It is strongly " + "recommended you upgrade to Python 2.3.2 or newer if you " + "intend to use Twisted's Jabber support.") + +else: + import stringprep + import unicodedata + from encodings import idna + + crippled = False + +del sys, warnings + +class ILookupTable: + """ Interface for character lookup classes. """ + + def lookup(self, c): + """ Return whether character is in this table. """ + +class IMappingTable: + """ Interface for character mapping classes. """ + + def map(self, c): + """ Return mapping for character. """ + +class LookupTableFromFunction: + + __implements__ = ILookupTable + + def __init__(self, in_table_function): + self.lookup = in_table_function + +class LookupTable: + + __implements__ = ILookupTable + + def __init__(self, table): + self._table = table + + def lookup(self, c): + return c in self._table + +class MappingTableFromFunction: + + __implements__ = IMappingTable + + def __init__(self, map_table_function): + self.map = map_table_function + +class EmptyMappingTable: + + __implements__ = IMappingTable + + def __init__(self, in_table_function): + self._in_table_function = in_table_function + + def map(self, c): + if self._in_table_function(c): + return None + else: + return c + +class Profile: + def __init__(self, mappings=[], normalize=True, prohibiteds=[], + check_unassigneds=True, check_bidi=True): + self.mappings = mappings + self.normalize = normalize + self.prohibiteds = prohibiteds + self.do_check_unassigneds = check_unassigneds + self.do_check_bidi = check_bidi + + def prepare(self, string): + result = self.map(string) + if self.normalize: + result = unicodedata.normalize("NFKC", result) + self.check_prohibiteds(result) + if self.do_check_unassigneds: + self.check_unassigneds(result) + if self.do_check_bidi: + self.check_bidirectionals(result) + return result + + def map(self, string): + result = [] + + for c in string: + result_c = c + + for mapping in self.mappings: + result_c = mapping.map(c) + if result_c != c: + break + + if result_c is not None: + result.append(result_c) + + return u"".join(result) + + def check_prohibiteds(self, string): + for c in string: + for table in self.prohibiteds: + if table.lookup(c): + raise UnicodeError, "Invalid character %s" % repr(c) + + def check_unassigneds(self, string): + for c in string: + if stringprep.in_table_a1(c): + raise UnicodeError, "Unassigned code point %s" % repr(c) + + def check_bidirectionals(self, string): + found_LCat = False + found_RandALCat = False + + for c in string: + if stringprep.in_table_d1(c): + found_RandALCat = True + if stringprep.in_table_d2(c): + found_LCat = True + + if found_LCat and found_RandALCat: + raise UnicodeError, "Violation of BIDI Requirement 2" + + if found_RandALCat and not (stringprep.in_table_d1(string[0]) and + stringprep.in_table_d1(string[-1])): + raise UnicodeError, "Violation of BIDI Requirement 3" + + +class NamePrep: + """ Implements nameprep on international domain names. + + STD3ASCIIRules is assumed true in this implementation. + """ + + # Prohibited characters. + prohibiteds = [unichr(n) for n in range(0x00, 0x2c + 1) + + range(0x2e, 0x2f + 1) + + range(0x3a, 0x40 + 1) + + range(0x5b, 0x60 + 1) + + range(0x7b, 0x7f + 1) ] + + def prepare(self, string): + result = [] + + labels = idna.dots.split(string) + + if labels and len(labels[-1]) == 0: + trailing_dot = '.' + del labels[-1] + else: + trailing_dot = '' + + for label in labels: + result.append(self.nameprep(label)) + + return ".".join(result)+trailing_dot + + def check_prohibiteds(self, string): + for c in string: + if c in self.prohibiteds: + raise UnicodeError, "Invalid character %s" % repr(c) + + def nameprep(self, label): + label = idna.nameprep(label) + self.check_prohibiteds(label) + if label[0] == '-': + raise UnicodeError, "Invalid leading hyphen-minus" + if label[-1] == '-': + raise UnicodeError, "Invalid trailing hyphen-minus" + return label + +if crippled: + case_map = MappingTableFromFunction(lambda c: c.lower()) + nodeprep = Profile(mappings=[case_map], + normalize=False, + prohibiteds=[LookupTable([u' ', u'"', u'&', u"'", u'/', + u':', u'<', u'>', u'@'])], + check_unassigneds=False, + check_bidi=False) + + resourceprep = Profile(normalize=False, + check_unassigneds=False, + check_bidi=False) + +else: + C_11 = LookupTableFromFunction(stringprep.in_table_c11) + C_12 = LookupTableFromFunction(stringprep.in_table_c12) + C_21 = LookupTableFromFunction(stringprep.in_table_c21) + C_22 = LookupTableFromFunction(stringprep.in_table_c22) + C_3 = LookupTableFromFunction(stringprep.in_table_c3) + C_4 = LookupTableFromFunction(stringprep.in_table_c4) + C_5 = LookupTableFromFunction(stringprep.in_table_c5) + C_6 = LookupTableFromFunction(stringprep.in_table_c6) + C_7 = LookupTableFromFunction(stringprep.in_table_c7) + C_8 = LookupTableFromFunction(stringprep.in_table_c8) + C_9 = LookupTableFromFunction(stringprep.in_table_c9) + + B_1 = EmptyMappingTable(stringprep.in_table_b1) + B_2 = MappingTableFromFunction(stringprep.map_table_b2) + + nodeprep = Profile(mappings=[B_1, B_2], + prohibiteds=[C_11, C_12, C_21, C_22, + C_3, C_4, C_5, C_6, C_7, C_8, C_9, + LookupTable([u'"', u'&', u"'", u'/', + u':', u'<', u'>', u'@'])]) + + resourceprep = Profile(mappings=[B_1,], + prohibiteds=[C_12, C_21, C_22, + C_3, C_4, C_5, C_6, C_7, C_8, C_9]) + +nameprep = NamePrep() diff --git a/src/config.py b/src/config.py index ba4bd1082..ed2c663ac 100644 --- a/src/config.py +++ b/src/config.py @@ -1169,16 +1169,28 @@ class AccountModificationWindow: _('Account name cannot contain spaces.')).get_response() return jid = self.xml.get_widget('jid_entry').get_text().decode('utf-8') - if jid == '' or jid.count('@') != 1: - dialogs.ErrorDialog(_('Invalid Jabber ID'), - _('A Jabber ID must be in the form "user@servername".')).get_response() + + # check if jid is conform to RFC and stringprep it + try: + jid = helpers.parse_jid(jid) + except helpers.InvalidFormat, s: + pritext = _('Invalid User ID') + dialogs.ErrorDialog(pritext, s).get_response() return + + resource = self.xml.get_widget('resource_entry').get_text().decode('utf-8') + try: + resource = helpers.parse_resource(resource) + except helpers.InvalidFormat, s: + pritext = _('Invalid User ID') + dialogs.ErrorDialog(pritext, s).get_response() + return + config['savepass'] = self.xml.get_widget( 'save_password_checkbutton').get_active() config['password'] = self.xml.get_widget('password_entry').get_text().\ decode('utf-8') - config['resource'] = self.xml.get_widget('resource_entry').get_text().\ - decode('utf-8') + config['resource'] = resource config['priority'] = self.xml.get_widget('priority_spinbutton').\ get_value_as_int() config['autoconnect'] = self.xml.get_widget('autoconnect_checkbutton').\ @@ -2516,16 +2528,26 @@ class AccountCreationWizardWindow: server = widgets['server_comboboxentry'].child.get_text() savepass = widgets['save_password_checkbutton'].get_active() password = widgets['pass_entry'].get_text() - if self.check_data(username, server): - self.save_account(self.account, username, server, savepass, password, - register_new) - self.finish_label.set_text(finish_text) - self.xml.get_widget('cancel_button').hide() - self.back_button.hide() - self.xml.get_widget('forward_button').hide() - self.finish_button.set_sensitive(True) - self.advanced_button.show() - self.notebook.set_current_page(3) + + jid = username + '@' + server + # check if jid is conform to RFC and stringprep it + try: + jid = helpers.parse_jid(jid) + except helpers.InvalidFormat, s: + pritext = _('Invalid User ID') + dialogs.ErrorDialog(pritext, s).get_response() + return + + username, server = gajim.get_room_name_and_server_from_room_jid(jid) + self.save_account(self.account, username, server, savepass, password, + register_new) + self.finish_label.set_text(finish_text) + self.xml.get_widget('cancel_button').hide() + self.back_button.hide() + self.xml.get_widget('forward_button').hide() + self.finish_button.set_sensitive(True) + self.advanced_button.show() + self.notebook.set_current_page(3) def on_advanced_button_clicked(self, widget): gajim.interface.windows[self.account]['account_modification'] = \ @@ -2535,18 +2557,6 @@ class AccountCreationWizardWindow: def on_finish_button_clicked(self, widget): self.window.destroy() - def check_data(self, username, server): - if len(username) == 0: - dialogs.ErrorDialog(_('Username is missing'), - _('You need to enter a username to continue.')).get_response() - return False - elif len(server) == 0: - dialogs.ErrorDialog(_('Server address is missing'), -_('You need to enter a valid server address to continue.')).get_response() - return False - else: - return True - def on_nick_entry_changed(self, widget): self.update_jid(widget) diff --git a/src/dialogs.py b/src/dialogs.py index 8f06a656c..da3a67ee1 100644 --- a/src/dialogs.py +++ b/src/dialogs.py @@ -355,19 +355,16 @@ _('Please fill in the data of the contact you want to add in account %s') %accou if not jid: return - if jid.find('@') < 0: + # check if jid is conform to RFC and stringprep it + try: + jid = helpers.parse_jid(jid) + except helpers.InvalidFormat, s: pritext = _('Invalid User ID') - sectext = _('Jabber ID must be of the form "user@servername".') - ErrorDialog(pritext, sectext).get_response() + ErrorDialog(pritext, s).get_response() return - # check if contact is already in roster (user@server == UsEr@server) - already_in = False - for rjid in gajim.contacts[self.account]: - if jid.lower() == rjid.lower(): - already_in = True - break - if already_in: + # Check if jid is already in roster + if jid in gajim.contacts[self.account]: ErrorDialog(_('Contact already in roster'), _('The contact is already listed in your roster.')).get_response() return