Ticket #153: avahi-find-hosts

File avahi-find-hosts, 10.1 KB (added by jwm, 5 years ago)
Line 
1#!/usr/bin/python
2# $Id$
3
4# This file is part of avahi.
5#
6# avahi is free software; you can redistribute it and/or modify it
7# under the terms of the GNU Lesser General Public License as
8# published by the Free Software Foundation; either version 2 of the
9# License, or (at your option) any later version.
10#
11# avahi is distributed in the hope that it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
14# License for more details.
15#
16# You should have received a copy of the GNU Lesser General Public
17# License along with avahi; if not, write to the Free Software
18# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
19# USA.
20
21"""
22avahi-find-hosts is a command line program intended to be used to fetch
23a list of machines on the local network that are advertising a particular
24service.
25
26It can return IP addresses, fully qualified domain names or, in the cases
27where the service supports it, URLs.
28"""
29
30__author__ = "John Morton"
31__version__ = "$Revision$"
32__date__ = "$Date$"
33__copyright__ = "Copyright (c) 2007 John Morton"
34__license__ = "LGPL"
35
36import sys
37import optparse
38import re
39
40import gobject
41import dbus
42import avahi
43import avahi.ServiceTypeDatabase
44
45# Automatically sets the default mainloop for dbus to a Glib provided one.
46# Needed to get signals to work.
47import dbus.glib
48
49debug = False
50
51# Maps simple protocol names to the DNS resource service type, and url scheme
52# name, if building URLs for that service makes sense.
53# In the absence of an explicit entry here, the the name will be mapped as
54# service->_service._tcp, which works in most cases.
55
56service_map = { "ftp":          dict(stype="_ftp._tcp",
57                                     url_scheme='ftp'),
58                "http":         dict(stype="_http._tcp",
59                                     url_scheme='http'),
60                "https":        dict(stype="_https._tcp",
61                                     url_scheme='https'),
62                "nfs":          dict(stype="_nfs._udp",
63                                     url_scheme='nfs'),               
64                "rsync":        dict(stype="_rsync._tcp",
65                                     url_scheme='rsync'),
66                "ssh":          dict(stype="_ssh._tcp",
67                                     url_scheme='ssh'),
68                "sftp":         dict(stype="_sftp_ssh._tcp",
69                                     url_scheme='sftp'),
70                "workstation":  dict(stype="_workstation._tcp",
71                                     ),
72               
73                }
74
75# An index for mapping the DNS service type directly to the URL scheme.
76stype_map = {}
77for s in service_map:
78    if service_map[s].has_key('url_scheme'):
79        stype_map[service_map[s]['stype']] = service_map[s]['url_scheme']
80del s
81
82
83parser = optparse.OptionParser(
84    usage="%prog [options] <service>" )
85
86parser.set_defaults(
87    debug=False,
88    use_host_names=True,
89    output="hosts",
90    domain="local",
91    timeout=3000
92    )
93                   
94parser.add_option("-t", "--timeout", dest="timeout",
95                  help="Timeout for service browsing.")
96parser.add_option("-d", "--domain",  dest="domain",
97                  help="Default domain for browsing")
98parser.add_option("-H", "--hostnames", dest="use_host_names",
99                  action="store_const", const=True,
100                  help="Resolve to host names")
101parser.add_option("-A", "--addresses", dest="use_host_names",
102                  action="store_const", const=False,
103                  help="Resolve to addresses")
104parser.add_option("-u", "--output-urls", dest="output",
105                  action="store_const", const="urls",
106                  help="Output URLs")
107parser.add_option("--debug", dest="debug", action="store_true", )
108
109
110class Service(object):
111    """A simple structure to store the date that Avahi will retrieve when
112    resolving a service."""
113
114    def __init__(self, interface, protocol, name, stype, domain,
115                 host, aprotocol, address, port, txt, flags):
116        self.interface = interface
117        self.protocol = protocol
118        self.name = name
119        self.type = stype
120        self.domain = domain
121        self.host = host
122        self.aprotocol = aprotocol
123        self.address = address
124        self.port = port
125        self.txt = avahi.txt_array_to_string_array(txt)
126        self.flags = flags
127       
128    def key(self):
129        """Provides a tuple of attributes suitable for using as a
130        lookup key."""
131        return (self.interface, self.protocol,
132                self.name, self.type, self.domain)
133       
134
135class AvahiServices(object):
136    """A class to encapsulate the business of talking to the avahi-daemon over
137    the D-bus."""
138   
139    services = {}
140
141    def __init__(self, domain, loop):
142        """Sets up the connection to avahi-daemon over D-bus."""
143       
144        # Connect to the system bus...
145        self.bus = dbus.SystemBus()
146        # Get a proxy to the object we want to talk to.
147        avahi_proxy = self.bus.get_object(avahi.DBUS_NAME,
148                                          avahi.DBUS_PATH_SERVER)
149        # Set the interface we want to use; server in this case.
150        self.server = dbus.Interface(avahi_proxy,
151                                     avahi.DBUS_INTERFACE_SERVER)
152       
153        self.version_string = self.server.GetVersionString()
154        self.domain = domain
155        self.loop = loop
156
157    def browse_service_type(self, stype):
158        """Sets up call back methods to browse for a specific service type"""
159        # Ask the server for a path to the browser object for the
160        # service we're interested in...
161        browser_path = self.server.ServiceBrowserNew(avahi.IF_UNSPEC,
162                                                     avahi.PROTO_UNSPEC,
163                                                     stype, self.domain,
164                                                     dbus.UInt32(0))
165        # Get it's proxy object...
166        browser_proxy = self.bus.get_object(avahi.DBUS_NAME, browser_path)
167        # And set the interface we want to use       
168        browser = dbus.Interface(browser_proxy,
169                                 avahi.DBUS_INTERFACE_SERVICE_BROWSER)
170
171        # Now connect the call backs to the relevant signals.
172        browser.connect_to_signal('ItemNew', self.new_service)
173        browser.connect_to_signal('ItemRemove', self.remove_service)
174        browser.connect_to_signal('AllForNow', self.all_for_now)
175
176   
177    def new_service(self, interface, protocol, name, stype, domain, flags):
178        """Callback method used to handle a new service has appearing, or
179        a known one being retrieved from avahi-daemon's cache.
180
181        Adds a Service object to our collection.  """
182       
183        service = Service(*self.server.ResolveService(
184            interface, protocol, name, stype, domain,
185            avahi.PROTO_UNSPEC, dbus.UInt32(0)))
186        self.services[service.key()] = service
187
188    def remove_service(self, interface, protocol, name, stype, domain):
189        """Callback method to handle a service has going away.
190
191        Removes the matching Service object if it exists.
192        """
193       
194        try:
195            del self.services[(interface, protocol, name, stype, domain)]
196        except KeyError:
197            pass
198
199    def all_for_now(self):
200        """A callback to handle the 'all for now' signal.
201
202        This signal tells us that we now have all the service entires
203        of the types we asked for, that avahi presently knows about.
204
205        Exits the mainloop, returning control back to where the loop was
206        first run.
207        """
208        self.loop.quit()
209
210
211def get_host(service, use_host_names):
212    """Returns the fully qualified host name, or IP address if the
213    host name looks suspicious, or is preferred."""
214    if (use_host_names and
215        re.match("^([0-9a-z][0-9a-z\-]*\.)*([0-9a-z][0-9a-z\-]*)\.?",
216                 service.host)):
217        return service.host       
218   
219    return service.address
220   
221def make_url(service, scheme, use_host_names):
222    """Generate a URL from service data."""
223    # FIXME -- this isn't really flexible enough to account for a lot
224    # of different URL types.
225   
226    host = get_host(service, use_host_names)
227
228    user = ""
229    password = ""
230   
231    for k in service.txt:
232        if k.startswith("user="):  user = k[5:]       
233    for k in service.txt:
234        if k.startswith("password="): password = k[9:]
235    if password:
236        user += ":" + password
237    if len(user): user = "@" + user
238
239    path = ""
240    for k in service.txt:
241        if k.startswith("path="): path = k[5:]
242    if not path.startswith("/"):
243        path = "/" + path
244
245    return "%s://%s%s%s" % (scheme, user, host, path)
246
247def main(argv=None):
248    global debug
249    if argv == None:
250        argv = sys.argv
251       
252    (options, args) = parser.parse_args()
253    debug = options.debug
254   
255    if len(args) != 1:
256        parser.print_help()
257        return 1
258
259    # Provide the Glib mainloop, so signals work.
260    loop = gobject.MainLoop()
261    gobject.timeout_add(options.timeout, loop.quit)
262
263    browser = AvahiServices(options.domain, loop)
264
265    # Set up browsing for the service type the user is interested in.
266    # We look up aliases, use the full _foo._prot form, or take a guess.
267    if service_map.has_key(args[0]):
268        stype = service_map[args[0]]['stype']
269    else:       
270        if args[0][0] == '_':
271            stype = args[0]
272        else:
273            stype = "_%s._tcp" % args[0]
274    browser.browse_service_type(stype)
275
276    try:
277        # Run the loop! This waits around until the avahi daemon sends
278        # us messages about the services found, and gives us the
279        # "all for now" message. Or the user gets bored and interupts the
280        # process with ctrl-c.
281        loop.run()
282    except KeyboardInterrupt:
283        pass
284
285    for s in browser.services:
286        if options.output == "urls" and stype_map.has_key(stype):
287            print make_url(browser.services[s],
288                           stype_map[stype], options.use_host_names)
289        else:
290            print get_host(browser.services[s], options.use_host_names)
291   
292    return 0
293
294
295if __name__ == "__main__":
296    sys.exit(main())
297