services/presence/: identify Buddies by "key ID" (pubkey hash), not whole key.

This allows us to create Buddy objects as soon as we see a contact on the
server. For contacts not on trusted servers, or seen in anonymous MUCs, we
create a Buddy identified by JID instead (so we have some way to talk
about the anonymous contact within the Sugar API).

The concept of "trusted server" means a server which we trust to validate that
users with a keyID as the username part of their JID do in fact have that key.
Currently we just pretend that olpc.collabora.co.uk does this - in future, the
school servers will do this validation by using key rather than password
authentication.

Also create Buddy object paths based on the keyID or JID (for easier debugging).
This commit is contained in:
Simon McVittie 2007-05-28 17:25:52 +01:00
parent 5dacfdd365
commit a4a06206e3
4 changed files with 165 additions and 48 deletions

View File

@ -37,6 +37,7 @@ _PROP_CURACT = "current-activity"
_PROP_COLOR = "color"
_PROP_OWNER = "owner"
_PROP_VALID = "valid"
_PROP_OBJID = 'objid'
# Will go away soon
_PROP_IP4_ADDRESS = "ip4-address"
@ -90,15 +91,14 @@ class Buddy(ExportedGObject):
}
__gproperties__ = {
_PROP_KEY : (str, None, None, None,
gobject.PARAM_READWRITE |
gobject.PARAM_CONSTRUCT_ONLY),
_PROP_KEY : (str, None, None, None, gobject.PARAM_READWRITE),
_PROP_ICON : (object, None, None, gobject.PARAM_READWRITE),
_PROP_NICK : (str, None, None, None, gobject.PARAM_READWRITE),
_PROP_COLOR : (str, None, None, None, gobject.PARAM_READWRITE),
_PROP_CURACT : (str, None, None, None, gobject.PARAM_READWRITE),
_PROP_VALID : (bool, None, None, False, gobject.PARAM_READABLE),
_PROP_OWNER : (bool, None, None, False, gobject.PARAM_READABLE),
_PROP_OBJID : (str, None, None, None, gobject.PARAM_READABLE),
_PROP_IP4_ADDRESS : (str, None, None, None, gobject.PARAM_READWRITE)
}
@ -106,16 +106,16 @@ class Buddy(ExportedGObject):
"""Initialize the Buddy object
bus -- connection to the D-Bus session bus
object_id -- the activity's unique identifier
object_id -- the buddy's unique identifier, either based on their
key-ID or JID
kwargs -- used to initialize the object's properties
constructs a DBUS "object path" from the _BUDDY_PATH
and object_id
"""
if not object_id or not isinstance(object_id, int):
raise ValueError("object id must be a valid number")
self._object_path = _BUDDY_PATH + str(object_id)
self._object_id = object_id
self._object_path = dbus.ObjectPath(_BUDDY_PATH + object_id)
self._activities = {} # Activity ID -> Activity
self._activity_sigids = {}
@ -130,9 +130,6 @@ class Buddy(ExportedGObject):
self._color = None
self._ip4_address = None
if not kwargs.get(_PROP_KEY):
raise ValueError("key required")
_ALLOWED_INIT_PROPS = [_PROP_NICK, _PROP_KEY, _PROP_ICON,
_PROP_CURACT, _PROP_COLOR, _PROP_IP4_ADDRESS]
for (key, value) in kwargs.items():
@ -158,7 +155,9 @@ class Buddy(ExportedGObject):
pspec -- property specifier with a "name" attribute
"""
if pspec.name == _PROP_KEY:
if pspec.name == _PROP_OBJID:
return self._object_id
elif pspec.name == _PROP_KEY:
return self._key
elif pspec.name == _PROP_ICON:
return self._icon
@ -422,32 +421,40 @@ class Buddy(ExportedGObject):
"""
changed = False
changed_props = {}
if _PROP_NICK in properties.keys():
if _PROP_NICK in properties:
nick = properties[_PROP_NICK]
if nick != self._nick:
self._nick = nick
changed_props[_PROP_NICK] = nick
changed = True
if _PROP_COLOR in properties.keys():
if _PROP_COLOR in properties:
color = properties[_PROP_COLOR]
if color != self._color:
self._color = color
changed_props[_PROP_COLOR] = color
changed = True
if _PROP_CURACT in properties.keys():
if _PROP_CURACT in properties:
curact = properties[_PROP_CURACT]
if curact != self._current_activity:
self._current_activity = curact
changed_props[_PROP_CURACT] = curact
changed = True
if _PROP_IP4_ADDRESS in properties.keys():
if _PROP_IP4_ADDRESS in properties:
ip4addr = properties[_PROP_IP4_ADDRESS]
if ip4addr != self._ip4_address:
self._ip4_address = ip4addr
changed_props[_PROP_IP4_ADDRESS] = ip4addr
changed = True
if _PROP_KEY in properties:
# don't allow key to be set more than once
if self._key is None:
key = properties[_PROP_KEY]
if key is not None:
self._key = key
changed_props[_PROP_KEY] = key
changed = True
if not changed or not len(changed_props.keys()):
if not changed or not changed_props:
return
# Try emitting PropertyChanged before updating validity
@ -558,13 +565,11 @@ class ShellOwner(GenericOwner):
_SHELL_OWNER_INTERFACE = "org.laptop.Shell.Owner"
_SHELL_PATH = "/org/laptop/Shell"
def __init__(self, ps, bus, object_id, test=False):
def __init__(self, ps, bus):
"""Initialize the ShellOwner instance
ps -- presenceservice.PresenceService object
bus -- a connection to the D-Bus session bus
object_id -- the activity's unique identifier
test -- ignored
Retrieves initial property values from the profile
module. Loads the buddy icon from file as well.
@ -584,8 +589,8 @@ class ShellOwner(GenericOwner):
icon = f.read()
f.close()
GenericOwner.__init__(self, ps, bus, object_id, key=key,
nick=nick, color=color, icon=icon, server=server,
GenericOwner.__init__(self, ps, bus, psutils.pubkey_to_keyid(key),
key=key, nick=nick, color=color, icon=icon, server=server,
key_hash=key_hash, registered=registered)
# Ask to get notifications on Owner object property changes in the

View File

@ -1,4 +1,5 @@
# Copyright (C) 2007, Red Hat, Inc.
# Copyright (C) 2007 Collabora Ltd. <http://www.collabora.co.uk/>
#
# 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
@ -33,6 +34,7 @@ from sugar import util
from buddy import Buddy, ShellOwner
from activity import Activity
from psutils import pubkey_to_keyid
_PRESENCE_SERVICE = "org.laptop.Sugar.Presence"
_PRESENCE_INTERFACE = "org.laptop.Sugar.Presence"
@ -57,14 +59,16 @@ class PresenceService(ExportedGObject):
def _create_owner(self):
# Overridden by TestPresenceService
return ShellOwner(self, self._session_bus, self._get_next_object_id())
return ShellOwner(self, self._session_bus)
def __init__(self):
self._next_object_id = 0
self._connected = False
self._buddies = {} # key -> Buddy
self._buddies = {} # identifier -> Buddy
self._buddies_by_pubkey = {} # base64 public key -> Buddy
self._handles_buddies = {} # tp client -> (handle -> Buddy)
self._activities = {} # activity id -> Activity
self._session_bus = dbus.SessionBus()
@ -74,7 +78,10 @@ class PresenceService(ExportedGObject):
# Create the Owner object
self._owner = self._create_owner()
self._buddies[self._owner.props.key] = self._owner
key = self._owner.props.key
keyid = pubkey_to_keyid(key)
self._buddies['keyid/' + keyid] = self._owner
self._buddies_by_pubkey[key] = self._owner
self._registry = ManagerRegistry()
self._registry.LoadManagers()
@ -133,31 +140,35 @@ class PresenceService(ExportedGObject):
if self._connected != old_status:
self.emit('connection-status', self._connected)
def _contact_online(self, tp, handle, props):
new_buddy = False
key = props["key"]
buddy = self._buddies.get(key)
if not buddy:
def get_buddy(self, objid):
buddy = self._buddies.get(objid)
if buddy is None:
_logger.debug('Creating new buddy at .../%s', objid)
# we don't know yet this buddy
objid = self._get_next_object_id()
buddy = Buddy(self._session_bus, objid, key=key)
buddy = Buddy(self._session_bus, objid)
buddy.connect("validity-changed", self._buddy_validity_changed_cb)
buddy.connect("disappeared", self._buddy_disappeared_cb)
self._buddies[key] = buddy
self._buddies[objid] = buddy
return buddy
def _contact_online(self, tp, objid, handle, props):
_logger.debug('Handle %u, .../%s is now online', handle, objid)
buddy = self.get_buddy(objid)
self._handles_buddies[tp][handle] = buddy
# store the handle of the buddy for this CM
buddy.add_telepathy_handle(tp, handle)
buddy.set_properties(props)
def _buddy_validity_changed_cb(self, buddy, valid):
if valid:
self.BuddyAppeared(buddy.object_path())
self._buddies_by_pubkey[buddy.props.key] = buddy
_logger.debug("New Buddy: %s (%s)", buddy.props.nick,
buddy.props.color)
else:
self.BuddyDisappeared(buddy.object_path())
self._buddies_by_pubkey.pop(buddy.props.key, None)
_logger.debug("Buddy left: %s (%s)", buddy.props.nick,
buddy.props.color)
@ -166,16 +177,17 @@ class PresenceService(ExportedGObject):
self.BuddyDisappeared(buddy.object_path())
_logger.debug('Buddy left: %s (%s)', buddy.props.nick,
buddy.props.color)
self._buddies.pop(buddy.props.key)
self._buddies_by_pubkey.pop(buddy.props.key, None)
self._buddies.pop(buddy.props.objid, None)
def _contact_offline(self, tp, handle):
if not self._handles_buddies[tp].has_key(handle):
return
buddy = self._handles_buddies[tp].pop(handle)
key = buddy.props.key
# the handle of the buddy for this CM is not valid anymore
# (this might trigger _buddy_disappeared_cb if they are not visible
# via any CM)
buddy.remove_telepathy_handle(tp, handle)
def _get_next_object_id(self):
@ -326,8 +338,8 @@ class PresenceService(ExportedGObject):
in_signature="ay", out_signature="o",
byte_arrays=True)
def GetBuddyByPublicKey(self, key):
if self._buddies.has_key(key):
buddy = self._buddies[key]
buddy = self._buddies_by_pubkey.get(key)
if buddy is not None:
if buddy.props.valid:
return buddy.object_path()
raise NotFoundError("The buddy was not found.")

View File

@ -26,6 +26,7 @@ from sugar import env, util
from buddy import GenericOwner, _PROP_NICK, _PROP_CURACT, _PROP_COLOR
from presenceservice import PresenceService
from psutils import pubkey_to_keyid
_logger = logging.getLogger('s-p-s.pstest')
@ -37,7 +38,7 @@ class TestOwner(GenericOwner):
__gtype_name__ = "TestOwner"
def __init__(self, ps, bus, object_id, test_num, randomize):
def __init__(self, ps, bus, test_num, randomize):
self._cp = ConfigParser()
self._section = "Info"
self._test_activities = []
@ -62,8 +63,9 @@ class TestOwner(GenericOwner):
icon = _get_random_image()
_logger.debug("pubkey is %s" % pubkey)
GenericOwner.__init__(self, ps, bus, object_id, key=pubkey, nick=nick,
color=color, icon=icon, registered=registered, key_hash=privkey_hash)
GenericOwner.__init__(self, ps, bus, pubkey_to_keyid(pubkey),
key=pubkey, nick=nick, color=color, icon=icon,
registered=registered, key_hash=privkey_hash)
# Only do the random stuff if randomize is true
if randomize:
@ -169,7 +171,7 @@ class TestPresenceService(PresenceService):
PresenceService.__init__(self)
def _create_owner(self):
return TestOwner(self, self._session_bus, self._get_next_object_id(),
return TestOwner(self, self._session_bus,
self.__test_num, self.__randomize)
def internal_get_activity(self, actid):

View File

@ -20,6 +20,7 @@
import logging
import os
import sys
from string import hexdigits
try:
# Python >= 2.5
from hashlib import md5
@ -42,6 +43,7 @@ from telepathy.constants import (HANDLE_TYPE_CONTACT,
CONNECTION_STATUS_CONNECTING,
CONNECTION_STATUS_REASON_AUTHENTICATION_FAILED,
CONNECTION_STATUS_REASON_NONE_SPECIFIED,
CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES,
PROPERTY_FLAG_WRITE)
from sugar import util
@ -105,8 +107,11 @@ class ServerPlugin(gobject.GObject):
'contact-online':
# Contact has come online and we've discovered all their buddy
# properties.
# args: contact handle: int; dict {name: str => property: object}
(gobject.SIGNAL_RUN_FIRST, None, [object, object]),
# args:
# contact identification (based on key ID or JID): str
# contact handle: int or long
# dict {name: str => property: object}
(gobject.SIGNAL_RUN_FIRST, None, [str, object, object]),
'contact-offline':
# Contact has gone offline.
# args: contact handle
@ -263,7 +268,7 @@ class ServerPlugin(gobject.GObject):
account_info['server'] = self._owner.get_server()
khash = util.printable_hash(util._sha_data(self._owner.props.key))
khash = psutils.pubkey_to_keyid(self._owner.props.key)
account_info['account'] = "%s@%s" % (khash, account_info['server'])
account_info['password'] = self._owner.get_key_hash()
@ -770,10 +775,13 @@ class ServerPlugin(gobject.GObject):
return
props['nick'] = aliases[0]
jid = self._conn[CONN_INTERFACE].InspectHandles(HANDLE_TYPE_CONTACT,
[handle])[0]
self._online_contacts[handle] = jid
self.emit("contact-online", handle, props)
objid = self.identify_contacts(None, [handle])[handle]
self.emit("contact-online", objid, handle, props)
self._conn[CONN_INTERFACE_BUDDY_INFO].GetActivities(handle,
reply_handler=lambda *args: self._contact_online_activities_cb(
@ -841,7 +849,7 @@ class ServerPlugin(gobject.GObject):
handle not in self._subscribe_local_pending and
handle not in self._subscribe_remote_pending):
# it's probably a channel-specific handle - can't create a Buddy
# object
# object for those yet
return
self._online_contacts[handle] = None
@ -1063,3 +1071,93 @@ class ServerPlugin(gobject.GObject):
if room == act_handle:
self.emit("activity-properties-changed", act_id, properties)
return
def _server_is_trusted(self, hostname):
"""Return True if the server with the given hostname is trusted to
verify public-key ownership correctly, and only allows users to
register JIDs whose username part is either a public key fingerprint,
or of the wrong form to be a public key fingerprint (to allow for
ejabberd's admin@example.com address).
If we trust the server, we can skip verifying the key ourselves,
which leads to simplifications. In the current implementation we
never verify that people actually own the key they claim to, so
we will always give contacts on untrusted servers a JID- rather than
key-based identity.
For the moment we assume that the test server, olpc.collabora.co.uk,
does this verification.
"""
return (hostname == 'olpc.collabora.co.uk')
def identify_contacts(self, tp_chan, handles):
"""Work out the "best" unique identifier we can for the given handles,
in the context of the given channel (which may be None), using only
'fast' connection manager API (that does not involve network
round-trips).
For the XMPP server case, we proceed as follows:
* Find the owners of the given handles, if the channel has
channel-specific handles
* If the owner (globally-valid JID) is on a trusted server, return
'keyid/' plus the 'key fingerprint' (the user part of their JID,
currently implemented as the SHA-1 of the Base64 blob in
owner.key.pub)
* If the owner (globally-valid JID) cannot be found or is on an
untrusted server, return 'xmpp/' plus an escaped form of the JID
The idea is that we identify buddies by key-ID (i.e. by key, assuming
no collisions) if we can find it without making network round-trips,
but if that's not possible we just use their JIDs.
:Parameters:
`tp_chan` : telepathy.client.Channel or None
The channel in which the handles were found, or None if they
are known to be channel-specific handles
`handles` : iterable over (int or long)
The contacts' handles in that channel
:Returns:
A dict mapping the provided handles to the best available
unique identifier, which is a string that could be used as a
suffix to an object path
"""
# we need to be able to index into handles, so force them to
# be a sequence
if not isinstance(handles, (tuple, list)):
handles = tuple(handles)
owners = handles
if tp_chan is not None and CHANNEL_INTERFACE_GROUP in tp_chan:
group = tp_chan[CHANNEL_INTERFACE_GROUP]
if group.GetFlags() & CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES:
owners = group.GetHandleOwners(handles)
for i, owner in enumerate(owners):
if owner == 0:
owners[i] = handles[i]
jids = self._conn[CONN_INTERFACE].InspectHandles(HANDLE_TYPE_CONTACT,
owners)
ret = {}
for handle, jid in zip(handles, jids):
if '/' in jid:
# the contact is unidentifiable (in an anonymous MUC) - create
# a temporary identity for them, based on their room-JID
ret[handle] = 'xmpp/' + psutils.escape_identifier(jid)
else:
user, host = jid.split('@', 1)
if (self._server_is_trusted(host) and len(user) == 40 and
user.strip(hexdigits) == ''):
# they're on a trusted server and their username looks
# like a key-ID
ret[handle] = 'keyid/' + user.lower()
else:
# untrusted server, or not the right format to be a
# key-ID - identify the contact by their JID
ret[handle] = 'xmpp/' + psutils.escape_identifier(jid)
return ret