Projects/PicoRendezvous


As a companion to my Growl Python client, I decided to have a go at a minimalist Rendezvous client to go with it.

The class below was built after looking at pyzeroconf (which appears to be dead and has at least one serious threading bug) and mdnsd (which has an mquery utility that generates minimal PTR record queries).

Please note that this is not a general-purpose service locator: it doesn't even parse the replies (although that can be done in the request handling class with a bit more work), it merely keeps track of who replied - which is good enough for Growl, since Macs do not usually reply to queries for services they don't host themselves.

#!/usr/bin/env python

"""Minimalist Rendezvous Client for Python"""
__version__ = "0.2"
__author__ = "Rui Carmo (http://the.taoofmac.com)"
__copyright__ = "(C) 2004 Rui Carmo. Code under BSD License."

import string, os, socket, struct
from SocketServer import *

_MDNS_ADDR = '224.0.0.251'
_MDNS_PORT = 5353

class PicoRendezvous(UDPServer):
  allow_reuse_address = True
  replies = []

  def __init__(self):
    UDPServer.__init__(self, ("localhost", _MDNS_PORT), _ReplyHandler)
  # end def

  def query(self, proto):
    self.data = struct.pack( "!HHHHHH", 0, 0, 1, 0, 0, 0 )
    # pack query
    parts = proto.split('.')
    if parts[-1] == '':
      parts = parts[:-1]
    for part in parts:
      utf = part.encode('utf-8')
      l = len(utf)
      self.data += struct.pack("B", l)
      self.data += struct.pack('!' + str(l) + 's', utf)
    self.data += struct.pack("B", 0)
    # query for any PTR records
    self.data += struct.pack( "!BBH", 0, 12, 1 )
    try:
      self.socket.sendto(self.data,0,(_MDNS_ADDR,_MDNS_PORT))
    except:
      pass
    tenths = 0
    while tenths < 20:
      self.handle_request()
      tenths = tenths + 1
    return self.replies
  # end def

  def server_bind(self):
    self.group = ('', _MDNS_PORT)
    self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    try:
      self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
      self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
    except:
      pass
    self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, 255)
    self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1)
    try:
      self.socket.bind(self.group)
    except:
      pass
    self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_IF, socket.inet_aton(socket.gethostbyname(socket.gethostname())) + socket.inet_aton('0.0.0.0'))
    self.socket.setsockopt(socket.SOL_IP, socket.IP_ADD_MEMBERSHIP, socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'))
    self.socket.settimeout(0.1)
  # end def
# end class

class _ReplyHandler(DatagramRequestHandler):
  def handle(self):
    ip = self.client_address[0]
    if ip not in self.server.replies:
      self.server.replies.append(ip)
  # end def
# end class

if __name__ == '__main__':
  print "Starting Unit Test"
  print " - please make sure Growl is listening for network notifications"
  p = PicoRendezvous()
  print p.query('_growl._tcp.local.')