Ticket #153: avahi-find-hosts

File avahi-find-hosts, 10.1 kB (added by jwm, 1 year 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 """
22 avahi-find-hosts is a command line program intended to be used to fetch
23 a list of machines on the local network that are advertising a particular
24 service.
25
26 It can return IP addresses, fully qualified domain names or, in the cases
27 where 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
36 import sys
37 import optparse
38 import re
39
40 import gobject
41 import dbus
42 import avahi
43 import avahi.ServiceTypeDatabase
44
45 # Automatically sets the default mainloop for dbus to a Glib provided one.
46 # Needed to get signals to work.
47 import dbus.glib
48
49 debug = 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
56 service_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.
76 stype_map = {}
77 for 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']
80 del s
81
82
83 parser = optparse.OptionParser(
84     usage="%prog [options] <service>" )
85
86 parser.set_defaults(
87     debug=False,
88     use_host_names=True,
89     output="hosts",
90     domain="local",
91     timeout=3000
92     )
93                    
94 parser.add_option("-t", "--timeout", dest="timeout",
95                   help="Timeout for service browsing.")
96 parser.add_option("-d", "--domain",  dest="domain",
97                   help="Default domain for browsing")
98 parser.add_option("-H", "--hostnames", dest="use_host_names",
99                   action="store_const", const=True,
100                   help="Resolve to host names")
101 parser.add_option("-A", "--addresses", dest="use_host_names",
102                   action="store_const", const=False,
103                   help="Resolve to addresses")
104 parser.add_option("-u", "--output-urls", dest="output",
105                   action="store_const", const="urls",
106                   help="Output URLs")
107 parser.add_option("--debug", dest="debug", action="store_true", )
108
109
110 class 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
135 class 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
211 def 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    
221 def 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
247 def 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
295 if __name__ == "__main__":
296     sys.exit(main())
297
298