From 84c9f3bfb4bdfde0e771d6ee291689b0a691e623 Mon Sep 17 00:00:00 2001
From: Brian Warner <warner@lothar.com>
Date: Mon, 23 Apr 2012 18:02:22 -0400
Subject: [PATCH] Fix introweb display for mixed V1/V2 clients. Closes #1721.

This significantly cleans up the IntroducerServer web-status renderers.
Instead of poking around in the introducer's internals, now the web-status
renderers get clean AnnouncementDescriptor and SubscriberDescriptor
objects. They are still somewhat foolscap-centric, but will provide a clean
abstraction boundary for future improvements.

The specific #1721 bug was that old (V1) subscribers were handled by
wrapping their RemoteReference in a special WrapV1SubscriberInV2Interface
object, but the web-status display was trying to peek inside the object to
learn what host+port it was associated with, and the wrapper did not proxy
those extra attributes.

A test was added to test_introducer to make sure the introweb page renders
properly and at least contains the nicknames of both the V1 and V2 clients.
---
 src/allmydata/introducer/common.py    |  69 +++++++++++-
 src/allmydata/introducer/old.py       |  35 +++++-
 src/allmydata/introducer/server.py    |  46 ++++++--
 src/allmydata/test/test_introducer.py | 147 +++++++++++++-------------
 src/allmydata/util/rrefutil.py        |  37 ++++++-
 src/allmydata/web/introweb.py         | 125 ++++++----------------
 6 files changed, 276 insertions(+), 183 deletions(-)

diff --git a/src/allmydata/introducer/common.py b/src/allmydata/introducer/common.py
index 2f6e9c89..c7347971 100644
--- a/src/allmydata/introducer/common.py
+++ b/src/allmydata/introducer/common.py
@@ -1,6 +1,7 @@
 
 import re, simplejson
-from allmydata.util import keyutil, base32
+from foolscap.api import SturdyRef
+from allmydata.util import keyutil, base32, rrefutil
 
 def make_index(ann, key_s):
     """Return something that can be used as an index (e.g. a tuple of
@@ -34,7 +35,7 @@ def convert_announcement_v1_to_v2(ann_t):
     assert type(ver) is str
     assert type(oldest) is str
     ann = {"version": 0,
-           "nickname": nickname.decode("utf-8"),
+           "nickname": nickname.decode("utf-8", "replace"),
            "app-versions": {},
            "my-version": ver,
            "oldest-supported": oldest,
@@ -90,3 +91,67 @@ def unsign_from_foolscap(ann_t):
         key_vs = claimed_key_vs
     ann = simplejson.loads(msg.decode("utf-8"))
     return (ann, key_vs)
+
+class SubscriberDescriptor:
+    """This describes a subscriber, for status display purposes. It contains
+    the following attributes:
+
+    .service_name: what they subscribed to (string)
+    .when: time when they subscribed (seconds since epoch)
+    .nickname: their self-provided nickname, or "?" (unicode)
+    .version: their self-provided version (string)
+    .app_versions: versions of each library they use (dict str->str)
+    .advertised_addresses: what hosts they listen on (list of strings)
+    .remote_address: the external address from which they connected (string)
+    .tubid: for subscribers connecting with Foolscap, their tubid (string)
+    """
+
+    def __init__(self, service_name, when,
+                 nickname, version, app_versions,
+                 advertised_addresses, remote_address, tubid):
+        self.service_name = service_name
+        self.when = when
+        self.nickname = nickname
+        self.version = version
+        self.app_versions = app_versions
+        self.advertised_addresses = advertised_addresses
+        self.remote_address = remote_address
+        self.tubid = tubid
+
+class AnnouncementDescriptor:
+    """This describes an announcement, for status display purposes. It
+    contains the following attributes, which will be empty ("" for
+    strings) if the client did not provide them:
+
+     .when: time the announcement was first received (seconds since epoch)
+     .index: the announcements 'index', a tuple of (string-or-None).
+             The server remembers one announcement per index.
+     .canary: a Referenceable on the announcer, so the server can learn
+              when they disconnect (for the status display)
+     .announcement: raw dictionary of announcement data
+     .service_name: which service they are announcing (string)
+     .version: 'my-version' portion of announcement (string)
+     .nickname: their self-provided nickname, or "" (unicode)
+
+    The following attributes will be empty ([] for lists, "" for strings)
+    unless the announcement included an 'anonymous-storage-FURL'.
+
+     .advertised_addresses: which hosts they listen on (list of strings)
+     .tubid: their tubid (string)
+    """
+
+    def __init__(self, when, index, canary, ann_d):
+        self.when = when
+        self.index = index
+        self.canary = canary
+        self.announcement = ann_d
+        self.service_name = ann_d["service-name"]
+        self.version = ann_d.get("my-version", "")
+        self.nickname = ann_d.get("nickname", u"")
+        furl = ann_d.get("anonymous-storage-FURL")
+        if furl:
+            self.advertised_addresses = rrefutil.hosts_for_furl(furl)
+            self.tubid = SturdyRef(furl).tubID
+        else:
+            self.advertised_addresses = []
+            self.tubid = ""
diff --git a/src/allmydata/introducer/old.py b/src/allmydata/introducer/old.py
index e0bdacf7..36a9af76 100644
--- a/src/allmydata/introducer/old.py
+++ b/src/allmydata/introducer/old.py
@@ -8,6 +8,8 @@ from allmydata.interfaces import InsufficientVersionError
 from allmydata.util import log, idlib, rrefutil
 from foolscap.api import StringConstraint, TupleOf, SetOf, DictOf, Any, \
     RemoteInterface, Referenceable, eventually, SturdyRef
+from allmydata.introducer.common import SubscriberDescriptor, \
+     AnnouncementDescriptor
 FURL = StringConstraint(1000)
 
 # We keep a copy of the old introducer (both client and server) here to
@@ -362,7 +364,7 @@ class IntroducerService_v1(service.MultiService, Referenceable):
         self.introducer_url = None
         # 'index' is (service_name, tubid)
         self._announcements = {} # dict of index -> (announcement, timestamp)
-        self._subscribers = {} # dict of (rref->timestamp) dicts
+        self._subscribers = {} # [service_name]->[rref]->timestamp
         self._debug_counts = {"inbound_message": 0,
                               "inbound_duplicate": 0,
                               "inbound_update": 0,
@@ -380,10 +382,35 @@ class IntroducerService_v1(service.MultiService, Referenceable):
             kwargs["facility"] = "tahoe.introducer"
         return log.msg(*args, **kwargs)
 
-    def get_announcements(self):
-        return self._announcements
+    def get_announcements(self, include_stub_clients=True):
+        announcements = []
+        for index, (ann_t, when) in self._announcements.items():
+            (furl, service_name, ri_name, nickname, ver, oldest) = ann_t
+            if service_name == "stub_client" and not include_stub_clients:
+                continue
+            ann_d = {"nickname": nickname.decode("utf-8", "replace"),
+                     "my-version": ver,
+                     "service-name": service_name,
+                     "anonymous-storage-FURL": furl,
+                     }
+            ad = AnnouncementDescriptor(when, index, None, ann_d)
+            announcements.append(ad)
+        return announcements
+
     def get_subscribers(self):
-        return self._subscribers
+        s = []
+        for service_name, subscribers in self._subscribers.items():
+            for rref, when in subscribers.items():
+                tubid = rref.getRemoteTubID() or "?"
+                advertised_addresses = rrefutil.hosts_for_rref(rref)
+                remote_address = rrefutil.stringify_remote_address(rref)
+                nickname, version, app_versions = u"?", u"?", {}
+                sd = SubscriberDescriptor(service_name, when,
+                                          nickname, version, app_versions,
+                                          advertised_addresses, remote_address,
+                                          tubid)
+                s.append(sd)
+        return s
 
     def remote_get_version(self):
         return self.VERSION
diff --git a/src/allmydata/introducer/server.py b/src/allmydata/introducer/server.py
index 41f92b23..394fdf00 100644
--- a/src/allmydata/introducer/server.py
+++ b/src/allmydata/introducer/server.py
@@ -5,12 +5,12 @@ from twisted.application import service
 from foolscap.api import Referenceable
 import allmydata
 from allmydata import node
-from allmydata.util import log
+from allmydata.util import log, rrefutil
 from allmydata.introducer.interfaces import \
      RIIntroducerPublisherAndSubscriberService_v2
 from allmydata.introducer.common import convert_announcement_v1_to_v2, \
      convert_announcement_v2_to_v1, unsign_from_foolscap, make_index, \
-     get_tubid_string_from_ann
+     get_tubid_string_from_ann, SubscriberDescriptor, AnnouncementDescriptor
 
 class IntroducerNode(node.Node):
     PORTNUMFILE = "introducer.port"
@@ -56,7 +56,7 @@ class WrapV1SubscriberInV2Interface: # for_v1
     """
 
     def __init__(self, original):
-        self.original = original
+        self.original = original # also used for tests
     def __eq__(self, them):
         return self.original == them
     def __ne__(self, them):
@@ -69,6 +69,8 @@ class WrapV1SubscriberInV2Interface: # for_v1
         return self.original.getSturdyRef()
     def getPeer(self):
         return self.original.getPeer()
+    def getLocationHints(self):
+        return self.original.getLocationHints()
     def callRemote(self, methname, *args, **kwargs):
         m = getattr(self, "wrap_" + methname)
         return m(*args, **kwargs)
@@ -133,16 +135,42 @@ class IntroducerService(service.MultiService, Referenceable):
             kwargs["facility"] = "tahoe.introducer.server"
         return log.msg(*args, **kwargs)
 
-    def get_announcements(self):
-        return self._announcements
+    def get_announcements(self, include_stub_clients=True):
+        """Return a list of AnnouncementDescriptor for all announcements"""
+        announcements = []
+        for (index, (_, canary, ann, when)) in self._announcements.items():
+            if ann["service-name"] == "stub_client":
+                if not include_stub_clients:
+                    continue
+            ad = AnnouncementDescriptor(when, index, canary, ann)
+            announcements.append(ad)
+        return announcements
+
     def get_subscribers(self):
-        """Return a list of (service_name, when, subscriber_info, rref) for
-        all subscribers. subscriber_info is a dict with the following keys:
-        version, nickname, app-versions, my-version, oldest-supported"""
+        """Return a list of SubscriberDescriptor objects for all subscribers"""
         s = []
         for service_name, subscriptions in self._subscribers.items():
             for rref,(subscriber_info,when) in subscriptions.items():
-                s.append( (service_name, when, subscriber_info, rref) )
+                # note that if the subscriber didn't do Tub.setLocation,
+                # tubid will be None. Also, subscribers do not tell us which
+                # pubkey they use; only publishers do that.
+                tubid = rref.getRemoteTubID() or "?"
+                advertised_addresses = rrefutil.hosts_for_rref(rref)
+                remote_address = rrefutil.stringify_remote_address(rref)
+                # these three assume subscriber_info["version"]==0, but
+                # should tolerate other versions
+                if not subscriber_info:
+                     # V1 clients that haven't yet sent their stub_info data
+                    subscriber_info = {}
+                nickname = subscriber_info.get("nickname", u"?")
+                version = subscriber_info.get("my-version", u"?")
+                app_versions = subscriber_info.get("app-versions", {})
+                # 'when' is the time they subscribed
+                sd = SubscriberDescriptor(service_name, when,
+                                          nickname, version, app_versions,
+                                          advertised_addresses, remote_address,
+                                          tubid)
+                s.append(sd)
         return s
 
     def remote_get_version(self):
diff --git a/src/allmydata/test/test_introducer.py b/src/allmydata/test/test_introducer.py
index 841771ef..1c61ad71 100644
--- a/src/allmydata/test/test_introducer.py
+++ b/src/allmydata/test/test_introducer.py
@@ -4,7 +4,7 @@ from base64 import b32decode
 import simplejson
 
 from twisted.trial import unittest
-from twisted.internet import defer
+from twisted.internet import defer, address
 from twisted.python import log
 
 from foolscap.api import Tub, Referenceable, fireEventually, flushEventualQueue
@@ -19,6 +19,7 @@ from allmydata.introducer.common import get_tubid_string_from_ann, \
 from allmydata.introducer import old
 # test compatibility with old introducer .tac files
 from allmydata.introducer import IntroducerNode
+from allmydata.web import introweb
 from allmydata.util import pollmixin, keyutil
 import allmydata.test.common_util as testutil
 
@@ -95,17 +96,19 @@ class Introducer(ServiceMixin, unittest.TestCase, pollmixin.PollMixin):
         announcements = i.get_announcements()
         self.failUnlessEqual(len(announcements), 1)
         key1 = ("storage", "v0-"+keyid, None)
-        self.failUnless(key1 in announcements)
-        (ign, ign, ann1_out, ign) = announcements[key1]
+        self.failUnlessEqual(announcements[0].index, key1)
+        ann1_out = announcements[0].announcement
         self.failUnlessEqual(ann1_out["anonymous-storage-FURL"], furl1)
 
         furl2 = "pb://%s@127.0.0.1:36106/swissnum" % keyid
         ann2 = (furl2, "storage", "RIStorage", "nick1", "ver23", "ver0")
         i.remote_publish(ann2)
+        announcements = i.get_announcements()
         self.failUnlessEqual(len(announcements), 2)
         key2 = ("storage", None, keyid)
-        self.failUnless(key2 in announcements)
-        (ign, ign, ann2_out, ign) = announcements[key2]
+        wanted = [ad for ad in announcements if ad.index == key2]
+        self.failUnlessEqual(len(wanted), 1)
+        ann2_out = wanted[0].announcement
         self.failUnlessEqual(ann2_out["anonymous-storage-FURL"], furl2)
 
 
@@ -295,6 +298,7 @@ class Client(unittest.TestCase):
         d.addCallback(_then2)
         return d
 
+NICKNAME = u"n\u00EDickname-%s" # LATIN SMALL LETTER I WITH ACUTE
 
 class SystemTestMixin(ServiceMixin, pollmixin.PollMixin):
 
@@ -342,9 +346,9 @@ class Queue(SystemTestMixin, unittest.TestCase):
             return self.poll(_got_announcement)
         d.addCallback(_offline)
         def _done(ign):
-            v = list(introducer.get_announcements().values())[0]
-            (ign, ign, ann1_out, ign) = v
-            self.failUnlessEqual(ann1_out["anonymous-storage-FURL"], furl1)
+            v = introducer.get_announcements()[0]
+            furl = v.announcement["anonymous-storage-FURL"]
+            self.failUnlessEqual(furl, furl1)
         d.addCallback(_done)
 
         # now let the ack get back
@@ -404,11 +408,11 @@ class SystemTest(SystemTestMixin, unittest.TestCase):
             log.msg("creating client %d: %s" % (i, tub.getShortTubID()))
             if i == 0:
                 c = old.IntroducerClient_v1(tub, self.introducer_furl,
-                                            u"nickname-%d" % i,
+                                            NICKNAME % str(i),
                                             "version", "oldest")
             else:
                 c = IntroducerClient(tub, self.introducer_furl,
-                                     u"nickname-%d" % i,
+                                     NICKNAME % str(i),
                                      "version", "oldest",
                                      {"component": "component-v1"})
             received_announcements[c] = {}
@@ -541,7 +545,7 @@ class SystemTest(SystemTestMixin, unittest.TestCase):
                 ann = anns[nodeid0]
                 nick = ann["nickname"]
                 self.failUnlessEqual(type(nick), unicode)
-                self.failUnlessEqual(nick, u"nickname-0")
+                self.failUnlessEqual(nick, NICKNAME % "0")
             if server_version == V1:
                 for c in publishing_clients:
                     cdc = c._debug_counts
@@ -566,6 +570,12 @@ class SystemTest(SystemTestMixin, unittest.TestCase):
                              ]:
                         expected = 2
                     self.failUnlessEqual(cdc["outbound_message"], expected)
+            # now check the web status, make sure it renders without error
+            ir = introweb.IntroducerRoot(self.parent)
+            self.parent.nodeid = "NODEID"
+            text = ir.renderSynchronously().decode("utf-8")
+            self.failUnlessIn(NICKNAME % "0", text) # the v1 client
+            self.failUnlessIn(NICKNAME % "1", text) # a v2 client
             log.msg("_check1 done")
         d.addCallback(_check1)
 
@@ -699,13 +709,17 @@ class SystemTest(SystemTestMixin, unittest.TestCase):
 class FakeRemoteReference:
     def notifyOnDisconnect(self, *args, **kwargs): pass
     def getRemoteTubID(self): return "62ubehyunnyhzs7r6vdonnm2hpi52w6y"
+    def getLocationHints(self): return [("ipv4", "here.example.com", "1234"),
+                                        ("ipv4", "there.example.com", "2345")]
+    def getPeer(self): return address.IPv4Address("TCP", "remote.example.com",
+                                                  3456)
 
 class ClientInfo(unittest.TestCase):
     def test_client_v2(self):
         introducer = IntroducerService()
         tub = introducer_furl = None
         app_versions = {"whizzy": "fizzy"}
-        client_v2 = IntroducerClient(tub, introducer_furl, u"nick-v2",
+        client_v2 = IntroducerClient(tub, introducer_furl, NICKNAME % u"v2",
                                      "my_version", "oldest", app_versions)
         #furl1 = "pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:0/swissnum"
         #ann_s = make_ann_t(client_v2, furl1, None)
@@ -713,16 +727,13 @@ class ClientInfo(unittest.TestCase):
         subscriber = FakeRemoteReference()
         introducer.remote_subscribe_v2(subscriber, "storage",
                                        client_v2._my_subscriber_info)
-        s = introducer.get_subscribers()
-        self.failUnlessEqual(len(s), 1)
-        sn, when, si, rref = s[0]
-        self.failUnlessIdentical(rref, subscriber)
-        self.failUnlessEqual(sn, "storage")
-        self.failUnlessEqual(si["version"], 0)
-        self.failUnlessEqual(si["oldest-supported"], "oldest")
-        self.failUnlessEqual(si["app-versions"], app_versions)
-        self.failUnlessEqual(si["nickname"], u"nick-v2")
-        self.failUnlessEqual(si["my-version"], "my_version")
+        subs = introducer.get_subscribers()
+        self.failUnlessEqual(len(subs), 1)
+        s0 = subs[0]
+        self.failUnlessEqual(s0.service_name, "storage")
+        self.failUnlessEqual(s0.app_versions, app_versions)
+        self.failUnlessEqual(s0.nickname, NICKNAME % u"v2")
+        self.failUnlessEqual(s0.version, "my_version")
 
     def test_client_v1(self):
         introducer = IntroducerService()
@@ -730,50 +741,39 @@ class ClientInfo(unittest.TestCase):
         introducer.remote_subscribe(subscriber, "storage")
         # the v1 subscribe interface had no subscriber_info: that was usually
         # sent in a separate stub_client pseudo-announcement
-        s = introducer.get_subscribers()
-        self.failUnlessEqual(len(s), 1)
-        sn, when, si, rref = s[0]
-        # rref will be a WrapV1SubscriberInV2Interface around the real
-        # subscriber
-        self.failUnlessIdentical(rref.original, subscriber)
-        self.failUnlessEqual(si, None) # not known yet
-        self.failUnlessEqual(sn, "storage")
+        subs = introducer.get_subscribers()
+        self.failUnlessEqual(len(subs), 1)
+        s0 = subs[0]
+        self.failUnlessEqual(s0.nickname, u"?") # not known yet
+        self.failUnlessEqual(s0.service_name, "storage")
 
         # now submit the stub_client announcement
         furl1 = "pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:0/swissnum"
         ann = (furl1, "stub_client", "RIStubClient",
-               u"nick-v1".encode("utf-8"), "my_version", "oldest")
+               (NICKNAME % u"v1").encode("utf-8"), "my_version", "oldest")
         introducer.remote_publish(ann)
         # the server should correlate the two
-        s = introducer.get_subscribers()
-        self.failUnlessEqual(len(s), 1)
-        sn, when, si, rref = s[0]
-        self.failUnlessIdentical(rref.original, subscriber)
-        self.failUnlessEqual(sn, "storage")
-
-        self.failUnlessEqual(si["version"], 0)
-        self.failUnlessEqual(si["oldest-supported"], "oldest")
+        subs = introducer.get_subscribers()
+        self.failUnlessEqual(len(subs), 1)
+        s0 = subs[0]
+        self.failUnlessEqual(s0.service_name, "storage")
         # v1 announcements do not contain app-versions
-        self.failUnlessEqual(si["app-versions"], {})
-        self.failUnlessEqual(si["nickname"], u"nick-v1")
-        self.failUnlessEqual(si["my-version"], "my_version")
+        self.failUnlessEqual(s0.app_versions, {})
+        self.failUnlessEqual(s0.nickname, NICKNAME % u"v1")
+        self.failUnlessEqual(s0.version, "my_version")
 
         # a subscription that arrives after the stub_client announcement
         # should be correlated too
         subscriber2 = FakeRemoteReference()
         introducer.remote_subscribe(subscriber2, "thing2")
 
-        s = introducer.get_subscribers()
-        subs = dict([(sn, (si,rref)) for sn, when, si, rref in s])
+        subs = introducer.get_subscribers()
         self.failUnlessEqual(len(subs), 2)
-        (si,rref) = subs["thing2"]
-        self.failUnlessIdentical(rref.original, subscriber2)
-        self.failUnlessEqual(si["version"], 0)
-        self.failUnlessEqual(si["oldest-supported"], "oldest")
+        s0 = [s for s in subs if s.service_name == "thing2"][0]
         # v1 announcements do not contain app-versions
-        self.failUnlessEqual(si["app-versions"], {})
-        self.failUnlessEqual(si["nickname"], u"nick-v1")
-        self.failUnlessEqual(si["my-version"], "my_version")
+        self.failUnlessEqual(s0.app_versions, {})
+        self.failUnlessEqual(s0.nickname, NICKNAME % u"v1")
+        self.failUnlessEqual(s0.version, "my_version")
 
 class Announcements(unittest.TestCase):
     def test_client_v2_unsigned(self):
@@ -789,14 +789,13 @@ class Announcements(unittest.TestCase):
         introducer.remote_publish_v2(ann_s0, canary0)
         a = introducer.get_announcements()
         self.failUnlessEqual(len(a), 1)
-        (index, (ann_s, canary, ann, when)) = a.items()[0]
-        self.failUnlessIdentical(canary, canary0)
-        self.failUnlessEqual(index, ("storage", None, tubid))
-        self.failUnlessEqual(ann["app-versions"], app_versions)
-        self.failUnlessEqual(ann["nickname"], u"nick-v2")
-        self.failUnlessEqual(ann["service-name"], "storage")
-        self.failUnlessEqual(ann["my-version"], "my_version")
-        self.failUnlessEqual(ann["anonymous-storage-FURL"], furl1)
+        self.failUnlessIdentical(a[0].canary, canary0)
+        self.failUnlessEqual(a[0].index, ("storage", None, tubid))
+        self.failUnlessEqual(a[0].announcement["app-versions"], app_versions)
+        self.failUnlessEqual(a[0].nickname, u"nick-v2")
+        self.failUnlessEqual(a[0].service_name, "storage")
+        self.failUnlessEqual(a[0].version, "my_version")
+        self.failUnlessEqual(a[0].announcement["anonymous-storage-FURL"], furl1)
 
     def test_client_v2_signed(self):
         introducer = IntroducerService()
@@ -813,14 +812,13 @@ class Announcements(unittest.TestCase):
         introducer.remote_publish_v2(ann_t0, canary0)
         a = introducer.get_announcements()
         self.failUnlessEqual(len(a), 1)
-        (index, (ann_s, canary, ann, when)) = a.items()[0]
-        self.failUnlessIdentical(canary, canary0)
-        self.failUnlessEqual(index, ("storage", pks, None))
-        self.failUnlessEqual(ann["app-versions"], app_versions)
-        self.failUnlessEqual(ann["nickname"], u"nick-v2")
-        self.failUnlessEqual(ann["service-name"], "storage")
-        self.failUnlessEqual(ann["my-version"], "my_version")
-        self.failUnlessEqual(ann["anonymous-storage-FURL"], furl1)
+        self.failUnlessIdentical(a[0].canary, canary0)
+        self.failUnlessEqual(a[0].index, ("storage", pks, None))
+        self.failUnlessEqual(a[0].announcement["app-versions"], app_versions)
+        self.failUnlessEqual(a[0].nickname, u"nick-v2")
+        self.failUnlessEqual(a[0].service_name, "storage")
+        self.failUnlessEqual(a[0].version, "my_version")
+        self.failUnlessEqual(a[0].announcement["anonymous-storage-FURL"], furl1)
 
     def test_client_v1(self):
         introducer = IntroducerService()
@@ -833,14 +831,13 @@ class Announcements(unittest.TestCase):
 
         a = introducer.get_announcements()
         self.failUnlessEqual(len(a), 1)
-        (index, (ann_s, canary, ann, when)) = a.items()[0]
-        self.failUnlessEqual(canary, None)
-        self.failUnlessEqual(index, ("storage", None, tubid))
-        self.failUnlessEqual(ann["app-versions"], {})
-        self.failUnlessEqual(ann["nickname"], u"nick-v1".encode("utf-8"))
-        self.failUnlessEqual(ann["service-name"], "storage")
-        self.failUnlessEqual(ann["my-version"], "my_version")
-        self.failUnlessEqual(ann["anonymous-storage-FURL"], furl1)
+        self.failUnlessEqual(a[0].index, ("storage", None, tubid))
+        self.failUnlessEqual(a[0].canary, None)
+        self.failUnlessEqual(a[0].announcement["app-versions"], {})
+        self.failUnlessEqual(a[0].nickname, u"nick-v1".encode("utf-8"))
+        self.failUnlessEqual(a[0].service_name, "storage")
+        self.failUnlessEqual(a[0].version, "my_version")
+        self.failUnlessEqual(a[0].announcement["anonymous-storage-FURL"], furl1)
 
 
 class TooNewServer(IntroducerService):
diff --git a/src/allmydata/util/rrefutil.py b/src/allmydata/util/rrefutil.py
index fd2259b6..a14d15f1 100644
--- a/src/allmydata/util/rrefutil.py
+++ b/src/allmydata/util/rrefutil.py
@@ -1,5 +1,7 @@
 
-from foolscap.api import Violation, RemoteException, DeadReferenceError
+from twisted.internet import address
+from foolscap.api import Violation, RemoteException, DeadReferenceError, \
+     SturdyRef
 
 def add_version_to_remote_reference(rref, default):
     """I try to add a .version attribute to the given RemoteReference. I call
@@ -23,3 +25,36 @@ def trap_and_discard(f, *errorTypes):
 
 def trap_deadref(f):
     return trap_and_discard(f, DeadReferenceError)
+
+
+def hosts_for_rref(rref, ignore_localhost=True):
+    # actually, this only returns hostnames
+    advertised = []
+    for hint in rref.getLocationHints():
+        # Foolscap-0.2.5 and earlier used strings in .locationHints, but we
+        # require a newer version that uses tuples of ("ipv4", host, port)
+        assert not isinstance(hint, str), hint
+        if hint[0] == "ipv4":
+            host = hint[1]
+            if ignore_localhost and host == "127.0.0.1":
+                continue
+            advertised.append(host)
+    return advertised
+
+def hosts_for_furl(furl, ignore_localhost=True):
+    advertised = []
+    for hint in SturdyRef(furl).locationHints:
+        assert not isinstance(hint, str), hint
+        if hint[0] == "ipv4":
+            host = hint[1]
+            if ignore_localhost and host == "127.0.0.1":
+                continue
+            advertised.append(host)
+    return advertised
+
+def stringify_remote_address(rref):
+    remote = rref.getPeer()
+    if isinstance(remote, address.IPv4Address):
+        return "%s:%d" % (remote.host, remote.port)
+    # loopback is a non-IPv4Address
+    return str(remote)
diff --git a/src/allmydata/web/introweb.py b/src/allmydata/web/introweb.py
index 783b2bee..fb804951 100644
--- a/src/allmydata/web/introweb.py
+++ b/src/allmydata/web/introweb.py
@@ -3,8 +3,6 @@ import time, os
 from nevow import rend, inevow
 from nevow.static import File as nevow_File
 from nevow.util import resource_filename
-from foolscap.api import SturdyRef
-from twisted.internet import address
 import allmydata
 import simplejson
 from allmydata import get_package_versions_string
@@ -36,18 +34,16 @@ class IntroducerRoot(rend.Page):
         res = {}
 
         counts = {}
-        subscribers = self.introducer_service.get_subscribers()
-        for (service_name, ign, ign, ign) in subscribers:
-            if service_name not in counts:
-                counts[service_name] = 0
-            counts[service_name] += 1
+        for s in self.introducer_service.get_subscribers():
+            if s.service_name not in counts:
+                counts[s.service_name] = 0
+            counts[s.service_name] += 1
         res["subscription_summary"] = counts
 
         announcement_summary = {}
         service_hosts = {}
-        for a in self.introducer_service.get_announcements().values():
-            (_, _, ann, when) = a
-            service_name = ann["service-name"]
+        for ad in self.introducer_service.get_announcements():
+            service_name = ad.service_name
             if service_name not in announcement_summary:
                 announcement_summary[service_name] = 0
             announcement_summary[service_name] += 1
@@ -60,12 +56,7 @@ class IntroducerRoot(rend.Page):
             # enough: when multiple services are run on a single host,
             # they're usually either configured with the same addresses,
             # or setLocationAutomatically picks up the same interfaces.
-            furl = ann["anonymous-storage-FURL"]
-            locations = SturdyRef(furl).getTubRef().getLocations()
-            # list of tuples, ("ipv4", host, port)
-            host = frozenset([hint[1]
-                              for hint in locations
-                              if hint[0] == "ipv4"])
+            host = frozenset(ad.advertised_addresses)
             service_hosts[service_name].add(host)
         res["announcement_summary"] = announcement_summary
         distinct_hosts = dict([(name, len(hosts))
@@ -85,12 +76,10 @@ class IntroducerRoot(rend.Page):
 
     def render_announcement_summary(self, ctx, data):
         services = {}
-        for a in self.introducer_service.get_announcements().values():
-            (_, _, ann, when) = a
-            service_name = ann["service-name"]
-            if service_name not in services:
-                services[service_name] = 0
-            services[service_name] += 1
+        for ad in self.introducer_service.get_announcements():
+            if ad.service_name not in services:
+                services[ad.service_name] = 0
+            services[ad.service_name] += 1
         service_names = services.keys()
         service_names.sort()
         return ", ".join(["%s: %d" % (service_name, services[service_name])
@@ -98,88 +87,40 @@ class IntroducerRoot(rend.Page):
 
     def render_client_summary(self, ctx, data):
         counts = {}
-        clients = self.introducer_service.get_subscribers()
-        for (service_name, ign, ign, ign) in clients:
-            if service_name not in counts:
-                counts[service_name] = 0
-            counts[service_name] += 1
+        for s in self.introducer_service.get_subscribers():
+            if s.service_name not in counts:
+                counts[s.service_name] = 0
+            counts[s.service_name] += 1
         return ", ".join([ "%s: %d" % (name, counts[name])
                            for name in sorted(counts.keys()) ] )
 
     def data_services(self, ctx, data):
-        introsvc = self.introducer_service
-        services = []
-        for a in introsvc.get_announcements().values():
-            (_, _, ann, when) = a
-            if ann["service-name"] == "stub_client":
-                continue
-            services.append( (when, ann) )
-        services.sort(key=lambda x: (x[1]["service-name"], x[1]["nickname"]))
-        # this used to be:
-        #services.sort(lambda a,b: cmp( (a[1][1], a), (b[1][1], b) ) )
-        # service_name was the primary key, then the whole tuple (starting
-        # with the furl) was the secondary key
+        services = self.introducer_service.get_announcements(False)
+        services.sort(key=lambda ad: (ad.service_name, ad.nickname))
         return services
 
-    def render_service_row(self, ctx, (since,ann)):
-        sr = SturdyRef(ann["anonymous-storage-FURL"])
-        nodeid = sr.tubID
-        advertised = self.show_location_hints(sr)
-        ctx.fillSlots("peerid", nodeid)
-        ctx.fillSlots("nickname", ann["nickname"])
-        ctx.fillSlots("advertised", " ".join(advertised))
+    def render_service_row(self, ctx, ad):
+        ctx.fillSlots("peerid", ad.tubid)
+        ctx.fillSlots("nickname", ad.nickname)
+        ctx.fillSlots("advertised", " ".join(ad.advertised_addresses))
         ctx.fillSlots("connected", "?")
-        TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
-        ctx.fillSlots("announced",
-                      time.strftime(TIME_FORMAT, time.localtime(since)))
-        ctx.fillSlots("version", ann["my-version"])
-        ctx.fillSlots("service_name", ann["service-name"])
+        when_s = time.strftime("%H:%M:%S %d-%b-%Y", time.localtime(ad.when))
+        ctx.fillSlots("announced", when_s)
+        ctx.fillSlots("version", ad.version)
+        ctx.fillSlots("service_name", ad.service_name)
         return ctx.tag
 
     def data_subscribers(self, ctx, data):
         return self.introducer_service.get_subscribers()
 
     def render_subscriber_row(self, ctx, s):
-        (service_name, since, info, rref) = s
-        nickname = info.get("nickname", "?")
-        version = info.get("my-version", "?")
-
-        sr = rref.getSturdyRef()
-        # if the subscriber didn't do Tub.setLocation, nodeid will be None
-        nodeid = sr.tubID or "?"
-        ctx.fillSlots("peerid", nodeid)
-        ctx.fillSlots("nickname", nickname)
-        advertised = self.show_location_hints(sr)
-        ctx.fillSlots("advertised", " ".join(advertised))
-        remote_host = rref.tracker.broker.transport.getPeer()
-        if isinstance(remote_host, address.IPv4Address):
-            remote_host_s = "%s:%d" % (remote_host.host, remote_host.port)
-        else:
-            # loopback is a non-IPv4Address
-            remote_host_s = str(remote_host)
-        ctx.fillSlots("connected", remote_host_s)
-        TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
-        ctx.fillSlots("since",
-                      time.strftime(TIME_FORMAT, time.localtime(since)))
-        ctx.fillSlots("version", version)
-        ctx.fillSlots("service_name", service_name)
+        ctx.fillSlots("nickname", s.nickname)
+        ctx.fillSlots("peerid", s.tubid)
+        ctx.fillSlots("advertised", " ".join(s.advertised_addresses))
+        ctx.fillSlots("connected", s.remote_address)
+        since_s = time.strftime("%H:%M:%S %d-%b-%Y", time.localtime(s.when))
+        ctx.fillSlots("since", since_s)
+        ctx.fillSlots("version", s.version)
+        ctx.fillSlots("service_name", s.service_name)
         return ctx.tag
 
-    def show_location_hints(self, sr, ignore_localhost=True):
-        advertised = []
-        for hint in sr.locationHints:
-            if isinstance(hint, str):
-                # Foolscap-0.2.5 and earlier used strings in .locationHints
-                if ignore_localhost and hint.startswith("127.0.0.1"):
-                    continue
-                advertised.append(hint.split(":")[0])
-            else:
-                # Foolscap-0.2.6 and later use tuples of ("ipv4", host, port)
-                if hint[0] == "ipv4":
-                    host = hint[1]
-                if ignore_localhost and host == "127.0.0.1":
-                    continue
-                advertised.append(hint[1])
-        return advertised
-
-
-- 
2.45.2