Author: nextgens Date: 2007-03-04 15:23:01 +0000 (Sun, 04 Mar 2007) New Revision: 11954
Added: trunk/plugins/MDNSDiscovery/ trunk/plugins/MDNSDiscovery/MDNSDiscovery.java trunk/plugins/MDNSDiscovery/com/ trunk/plugins/MDNSDiscovery/com/strangeberry/ trunk/plugins/MDNSDiscovery/com/strangeberry/jmdns/ trunk/plugins/MDNSDiscovery/com/strangeberry/jmdns/tools/ trunk/plugins/MDNSDiscovery/com/strangeberry/jmdns/tools/Browser.java trunk/plugins/MDNSDiscovery/com/strangeberry/jmdns/tools/Main.java trunk/plugins/MDNSDiscovery/com/strangeberry/jmdns/tools/Responder.java trunk/plugins/MDNSDiscovery/gpl.txt trunk/plugins/MDNSDiscovery/javax/ trunk/plugins/MDNSDiscovery/javax/jmdns/ trunk/plugins/MDNSDiscovery/javax/jmdns/DNSCache.java trunk/plugins/MDNSDiscovery/javax/jmdns/DNSConstants.java trunk/plugins/MDNSDiscovery/javax/jmdns/DNSEntry.java trunk/plugins/MDNSDiscovery/javax/jmdns/DNSIncoming.java trunk/plugins/MDNSDiscovery/javax/jmdns/DNSListener.java trunk/plugins/MDNSDiscovery/javax/jmdns/DNSOutgoing.java trunk/plugins/MDNSDiscovery/javax/jmdns/DNSQuestion.java trunk/plugins/MDNSDiscovery/javax/jmdns/DNSRecord.java trunk/plugins/MDNSDiscovery/javax/jmdns/DNSState.java trunk/plugins/MDNSDiscovery/javax/jmdns/HostInfo.java trunk/plugins/MDNSDiscovery/javax/jmdns/JmDNS.java trunk/plugins/MDNSDiscovery/javax/jmdns/ServiceEvent.java trunk/plugins/MDNSDiscovery/javax/jmdns/ServiceInfo.java trunk/plugins/MDNSDiscovery/javax/jmdns/ServiceListener.java trunk/plugins/MDNSDiscovery/javax/jmdns/ServiceTypeListener.java Log: plugins: new plugin called MDNSDiscovery ; it implements Zeroconf (called Bonjour/RendezVous by apple) support on a freenet node. That will allow FCP clients to discover "automagicaly" FCP servers and might have other applications (we will discuss that on @darknet-tools soon) it works fine using latest trunk (r11953) no backward compatibility is planned. Added: trunk/plugins/MDNSDiscovery/MDNSDiscovery.java =================================================================== --- trunk/plugins/MDNSDiscovery/MDNSDiscovery.java (rev 0) +++ trunk/plugins/MDNSDiscovery/MDNSDiscovery.java 2007-03-04 15:23:01 UTC (rev 11954) @@ -0,0 +1,178 @@ +/* This code is part of Freenet. It is distributed under the GNU General + * Public License, version 2 (or at your option any later version). See + * http://www.gnu.org/ for further details of the GPL. */ + +package plugins.MDNSDiscovery; + +import java.io.IOException; + +import plugins.MDNSDiscovery.javax.jmdns.JmDNS; +import plugins.MDNSDiscovery.javax.jmdns.ServiceEvent; +import plugins.MDNSDiscovery.javax.jmdns.ServiceInfo; +import plugins.MDNSDiscovery.javax.jmdns.ServiceListener; +import freenet.clients.http.PageMaker; +import freenet.config.Config; +import freenet.pluginmanager.*; +import freenet.support.HTMLNode; +import freenet.support.api.HTTPRequest; + +/** + * This plugin implements Zeroconf (called Bonjour/RendezVous by apple) support on a freenet node. + * + * @author Florent Daignière <nextgens at freenetproject.org> + * + * @see http://www.dns-sd.org/ServiceTypes.html + * @see http://www.multicastdns.org/ + * @see http://jmdns.sourceforge.net/ + * + * TODO: We shouldn't start a thread at all ... but they are issues on startup (the configuration framework isn't available yet) + * TODO: We shouldn't advertise services if they aren't reachable (ie: bound to localhost) + * TODO: We will need to manage the list on our own insteed of requesting it for each http request + * TODO: Plug into config. callbacks to reflect changes + */ +public class MDNSDiscovery implements FredPlugin, FredPluginHTTP{ + public static String freenetServiceType = "_freenet._udp.local."; + private boolean goon = true; + private JmDNS jmdns; + private ServiceInfo fproxyInfo, tcmiInfo, fcpInfo, nodeInfo; + private Config nodeConfig; + private PageMaker pageMaker; + + /** + * Called upon plugin unloading : we unregister advertised services + */ + public void terminate() { + jmdns.unregisterAllServices(); + goon = false; + } + + public void runPlugin(PluginRespirator pr) { + // wait until the node is initialised. + do{ + try{ + Thread.sleep(1000); + }catch (InterruptedException e) {} + }while(pr.getNode() == null || !pr.getNode().isHasStarted()); + + nodeConfig = pr.getNode().config; + pageMaker = new PageMaker("clean"); + + try{ + // Create the multicast listener + jmdns = new JmDNS(); + final String address = "server on " + jmdns.getLocalHost() + " (" + pr.getNode().getMyName(); + + // Advertise Fproxy + if(nodeConfig.get("fproxy").getBoolean("enabled")){ + fproxyInfo = new ServiceInfo("_http._tcp.local.", "Freenet 0.7 Fproxy " + address, + nodeConfig.get("fproxy").getInt("port"), 0, 0, "path=/"); + jmdns.registerService(fproxyInfo); + } + + // Advertise FCP + if(nodeConfig.get("fcp").getBoolean("enabled")){ + fcpInfo = new ServiceInfo("_fcp._tcp.local.", "Freenet 0.7 FCP " + address, + nodeConfig.get("fcp").getInt("port"), 0, 0, ""); + jmdns.registerService(fcpInfo); + } + + // Advertise TCMI + if(nodeConfig.get("console").getBoolean("enabled")){ + tcmiInfo = new ServiceInfo("_telnet._tcp.local.", "Freenet 0.7 TCMI " + address, + nodeConfig.get("console").getInt("port"), 0, 0, ""); + jmdns.registerService(tcmiInfo); + } + + // Advertise the node + nodeInfo = new ServiceInfo(MDNSDiscovery.freenetServiceType, "Freenet 0.7 Node " + address, + nodeConfig.get("node").getInt("listenPort"), 0, 0, ""); + jmdns.registerService(nodeInfo); + + // Watch out for other nodes + jmdns.addServiceListener(MDNSDiscovery.freenetServiceType, new NodeMDNSListener(this)); + + } catch (IOException e) { + e.printStackTrace(); + } + + while(goon){ + synchronized (this) { + try{ + wait(300000); + }catch (InterruptedException e) {} + } + } + } + + static class NodeMDNSListener implements ServiceListener { + final MDNSDiscovery plugin; + + public NodeMDNSListener(MDNSDiscovery plugin) { + this.plugin = plugin; + } + + public synchronized void serviceAdded(ServiceEvent event) { + System.out.println("Service added : " + event.getName()+"."+event.getType()); + synchronized (plugin) { + plugin.notify(); + } + } + + public synchronized void serviceRemoved(ServiceEvent event) { + System.out.println("Service removed : " + event.getName()+"."+event.getType()); + synchronized (plugin) { + plugin.notify(); + } + } + + public synchronized void serviceResolved(ServiceEvent event) { + System.out.println("Service resolved: " + event.getInfo()); + synchronized (plugin) { + plugin.notify(); + } + } + } + + public String handleHTTPGet(HTTPRequest request) throws PluginHTTPException { + HTMLNode pageNode = pageMaker.getPageNode("MDNSDiscovery plugin configuration page", false); + HTMLNode contentNode = pageMaker.getContentNode(pageNode); + + ServiceInfo[] foundNodes = jmdns.list(MDNSDiscovery.freenetServiceType); + if(foundNodes != null && foundNodes.length > 0){ + HTMLNode peerTableInfobox = contentNode.addChild("div", "class", "infobox infobox-normal"); + HTMLNode peerTableInfoboxHeader = peerTableInfobox.addChild("div", "class", "infobox-header"); + peerTableInfoboxHeader.addChild("#", "The following nodes have been found on the local subnet :"); + HTMLNode peerTableInfoboxContent = peerTableInfobox.addChild("div", "class", "infobox-content"); + + HTMLNode peerTable = peerTableInfoboxContent.addChild("table", "class", "darknet_connections"); + HTMLNode peerTableHeaderRow = peerTable.addChild("tr"); + peerTableHeaderRow.addChild("th").addChild("span", new String[] { "title", "style" }, new String[] { "The node's name.", "border-bottom: 1px dotted; cursor: help;" }, "Name"); + peerTableHeaderRow.addChild("th").addChild("span", new String[] { "title", "style" }, new String[] { "The node's network address as IP:Port", "border-bottom: 1px dotted; cursor: help;" }, "Address"); + peerTableHeaderRow.addChild("th").addChild("span", new String[] { "title", "style" }, new String[] { "A description of the service.", "border-bottom: 1px dotted; cursor: help;" }, "Description"); + + HTMLNode peerRow = peerTable.addChild("tr"); + + for(int i=0; i<foundNodes.length; i++){ + peerRow.addChild("td", "class", "peer-name").addChild("#", foundNodes[i].getServer()); + peerRow.addChild("td", "class", "peer-address").addChild("#", foundNodes[i].getHostAddress()+':'+foundNodes[i].getPort()); + peerRow.addChild("td", "class", "peer-private-darknet-comment-note").addChild("#", foundNodes[i].getName()); + } + }else{ + HTMLNode peerTableInfobox = contentNode.addChild("div", "class", "infobox infobox-warning"); + HTMLNode peerTableInfoboxHeader = peerTableInfobox.addChild("div", "class", "infobox-header"); + peerTableInfoboxHeader.addChild("#", "Nothing found!"); + HTMLNode peerTableInfoboxContent = peerTableInfobox.addChild("div", "class", "infobox-content"); + peerTableInfoboxContent.addChild("#", "No freenet node found on the local subnet, sorry!"); + } + + return pageNode.generate(); + } + + public String handleHTTPPut(HTTPRequest request) throws PluginHTTPException { + throw new PluginHTTPException(); + } + + public String handleHTTPPost(HTTPRequest request) throws PluginHTTPException { + throw new PluginHTTPException(); + } +} Added: trunk/plugins/MDNSDiscovery/com/strangeberry/jmdns/tools/Browser.java =================================================================== --- trunk/plugins/MDNSDiscovery/com/strangeberry/jmdns/tools/Browser.java (rev 0) +++ trunk/plugins/MDNSDiscovery/com/strangeberry/jmdns/tools/Browser.java 2007-03-04 15:23:01 UTC (rev 11954) @@ -0,0 +1,272 @@ +// %Z%%M%, %I%, %G% +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package plugins.MDNSDiscovery.com.strangeberry.jmdns.tools; + +import java.io.*; +import java.net.*; +import java.util.*; +import java.awt.*; +import java.awt.event.*; +import javax.swing.*; +import javax.swing.table.*; +import javax.swing.border.*; +import javax.swing.event.*; +import plugins.MDNSDiscovery.javax.jmdns.*; + +/** + * User Interface for browsing JmDNS services. + * + * @author Arthur van Hoff, Werner Randelshofer + * @version %I%, %G% + */ +public class Browser extends JFrame implements ServiceListener, ServiceTypeListener, ListSelectionListener { + JmDNS jmdns; + Vector headers; + String type; + DefaultListModel types; + JList typeList; + DefaultListModel services; + JList serviceList; + JTextArea info; + + Browser(JmDNS jmdns) throws IOException { + super("JmDNS Browser"); + this.jmdns = jmdns; + + Color bg = new Color(230, 230, 230); + EmptyBorder border = new EmptyBorder(5, 5, 5, 5); + Container content = getContentPane(); + content.setLayout(new GridLayout(1, 3)); + + types = new DefaultListModel(); + typeList = new JList(types); + typeList.setBorder(border); + typeList.setBackground(bg); + typeList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + typeList.addListSelectionListener(this); + + JPanel typePanel = new JPanel(); + typePanel.setLayout(new BorderLayout()); + typePanel.add("North", new JLabel("Types")); + typePanel.add("Center", new JScrollPane(typeList, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED)); + content.add(typePanel); + + services = new DefaultListModel(); + serviceList = new JList(services); + serviceList.setBorder(border); + serviceList.setBackground(bg); + serviceList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + serviceList.addListSelectionListener(this); + + JPanel servicePanel = new JPanel(); + servicePanel.setLayout(new BorderLayout()); + servicePanel.add("North", new JLabel("Services")); + servicePanel.add("Center", new JScrollPane(serviceList, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED)); + content.add(servicePanel); + + info = new JTextArea(); + info.setBorder(border); + info.setBackground(bg); + info.setEditable(false); + info.setLineWrap(true); + + JPanel infoPanel = new JPanel(); + infoPanel.setLayout(new BorderLayout()); + infoPanel.add("North", new JLabel("Details")); + infoPanel.add("Center", new JScrollPane(info, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED)); + content.add(infoPanel); + + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setLocation(100, 100); + setSize(600, 400); + + jmdns.addServiceTypeListener(this); + + // register some well known types + String list[] = new String[] { + "_http._tcp.local.", + "_ftp._tcp.local.", + "_tftp._tcp.local.", + "_ssh._tcp.local.", + "_smb._tcp.local.", + "_printer._tcp.local.", + "_airport._tcp.local.", + "_afpovertcp._tcp.local.", + "_ichat._tcp.local.", + "_eppc._tcp.local.", + "_presence._tcp.local." + }; + + for (int i = 0 ; i < list.length ; i++) { + jmdns.registerServiceType(list[i]); + } + + show(); + } + + /** + * Add a service. + */ + public void serviceAdded(ServiceEvent event) { + final String name = event.getName(); + + System.out.println("ADD: " + name); + SwingUtilities.invokeLater(new Runnable() { + public void run() { insertSorted(services, name); } + }); + } + + /** + * Remove a service. + */ + public void serviceRemoved(ServiceEvent event) { + final String name = event.getName(); + + System.out.println("REMOVE: " + name); + SwingUtilities.invokeLater(new Runnable() { + public void run() { services.removeElement(name); } + }); + } + + /** + * A new service type was <discovered. + */ + public void serviceTypeAdded(ServiceEvent event) { + final String type = event.getType(); + + System.out.println("TYPE: " + type); + SwingUtilities.invokeLater(new Runnable() { + public void run() { insertSorted(types, type); } + }); + } + + + void insertSorted(DefaultListModel model, String value) { + for (int i = 0, n = model.getSize() ; i < n ; i++) { + if (value.compareToIgnoreCase((String)model.elementAt(i)) < 0) { + model.insertElementAt(value, i); + return; + } + } + model.addElement(value); + } + + /** + * Resolve a service. + */ + public void serviceResolved(ServiceEvent event) { + String name = event.getName(); + String type = event.getType(); + ServiceInfo info = event.getInfo(); + + if (name.equals(serviceList.getSelectedValue())) { + if (info == null) { + this.info.setText("service not found"); + } else { + + StringBuffer buf = new StringBuffer(); + buf.append(name); + buf.append('.'); + buf.append(type); + buf.append('\n'); + buf.append(info.getServer()); + buf.append(':'); + buf.append(info.getPort()); + buf.append('\n'); + buf.append(info.getAddress()); + buf.append(':'); + buf.append(info.getPort()); + buf.append('\n'); + for (Enumeration names = info.getPropertyNames() ; names.hasMoreElements() ; ) { + String prop = (String)names.nextElement(); + buf.append(prop); + buf.append('='); + buf.append(info.getPropertyString(prop)); + buf.append('\n'); + } + + this.info.setText(buf.toString()); + } + } + } + + /** + * List selection changed. + */ + public void valueChanged(ListSelectionEvent e) { + if (!e.getValueIsAdjusting()) { + if (e.getSource() == typeList) { + type = (String)typeList.getSelectedValue(); + jmdns.removeServiceListener(type, this); + services.setSize(0); + info.setText(""); + if (type != null) { + jmdns.addServiceListener(type, this); + } + } else if (e.getSource() == serviceList) { + String name = (String)serviceList.getSelectedValue(); + if (name == null) { + info.setText(""); + } else { + System.out.println(this+" valueChanged() type:"+type+" name:"+name); + System.out.flush(); + ServiceInfo service = jmdns.getServiceInfo(type, name); + if (service == null) { + info.setText("service not found"); + } else { + jmdns.requestServiceInfo(type, name); + } + } + } + } + } + + /** + * Table data. + */ + class ServiceTableModel extends AbstractTableModel { + public String getColumnName(int column) { + switch (column) { + case 0: return "service"; + case 1: return "address"; + case 2: return "port"; + case 3: return "text"; + } + return null; + } + public int getColumnCount() { + return 1; + } + public int getRowCount() { + return services.size(); + } + public Object getValueAt(int row, int col) { + return services.elementAt(row); + } + } + + public String toString() { + return "RVBROWSER"; + } + + /** + * Main program. + */ + public static void main(String argv[]) throws IOException { + new Browser(new JmDNS()); + } +} Added: trunk/plugins/MDNSDiscovery/com/strangeberry/jmdns/tools/Main.java =================================================================== --- trunk/plugins/MDNSDiscovery/com/strangeberry/jmdns/tools/Main.java (rev 0) +++ trunk/plugins/MDNSDiscovery/com/strangeberry/jmdns/tools/Main.java 2007-03-04 15:23:01 UTC (rev 11954) @@ -0,0 +1,115 @@ +// %Z%%M%, %I%, %G% +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package plugins.MDNSDiscovery.com.strangeberry.jmdns.tools; + +import java.io.*; +import java.net.*; +import java.util.*; +import plugins.MDNSDiscovery.javax.jmdns.*; + +/** + * Main sample program for JmDNS. + * + * @author Arthur van Hoff, Werner Randelshofer + * @version %I%, %G% + */ +public class Main { + static class SampleListener implements ServiceListener, ServiceTypeListener { + public void serviceAdded(ServiceEvent event) { + System.out.println("ADD: " + event.getDNS().getServiceInfo(event.getType(), event.getName(), 3*1000)); + } + public void serviceRemoved(ServiceEvent event) { + System.out.println("REMOVE: " + event.getName()); + } + public void serviceResolved(ServiceEvent event) { + System.out.println("RESOLVED: " + event.getInfo()); + } + public void serviceTypeAdded(ServiceEvent event) { + System.out.println("TYPE: " + event.getType()); + } + } + + public static void main(String argv[]) throws IOException { + int argc = argv.length; + boolean debug = false; + InetAddress intf = null; + + if ((argc > 0) && "-d".equals(argv[0])) { + System.arraycopy(argv, 1, argv, 0, --argc); + System.getProperties().put("jmdns.debug", "1"); + debug = true; + } + if ((argc > 1) && "-i".equals(argv[0])) { + intf = InetAddress.getByName(argv[1]); + System.arraycopy(argv, 2, argv, 0, argc -= 2); + } + if (intf == null) { + intf = InetAddress.getLocalHost(); + } + + JmDNS jmdns = new JmDNS(intf); + + if ((argc == 0) || ((argc >= 1) && "-browse".equals(argv[0]))) { + new Browser(jmdns); + for (int i = 2 ; i < argc ; i++) { + jmdns.registerServiceType(argv[i]); + } + } else if ((argc == 1) && "-bt".equals(argv[0])) { + jmdns.addServiceTypeListener(new SampleListener()); + } else if ((argc == 3) && "-bs".equals(argv[0])) { + jmdns.addServiceListener(argv[1] + "." + argv[2], new SampleListener()); + } else if ((argc > 4) && "-rs".equals(argv[0])) { + String type = argv[2] + "." + argv[3]; + String name = argv[1]; + Hashtable props = null; + for (int i = 5 ; i < argc ; i++) { + int j = argv[i].indexOf('='); + if (j < 0) { + throw new RuntimeException("not key=val: " + argv[i]); + } + if (props == null) { + props = new Hashtable(); + } + props.put(argv[i].substring(0, j), argv[i].substring(j+1)); + } + jmdns.registerService(new ServiceInfo(type, name, Integer.parseInt(argv[4]), 0, 0, props)); + + // This while loop keeps the main thread alive + while (true) { + try { + Thread.sleep(Integer.MAX_VALUE); + } catch (InterruptedException e) { + break; + } + } + } else if ((argc == 2) && "-f".equals(argv[0])) { + new Responder(jmdns, argv[1]); + } else if (!debug) { + System.out.println(); + System.out.println("jmdns:"); + System.out.println(" -d - output debugging info"); + System.out.println(" -i <addr> - specify the interface address"); + System.out.println(" -browse [<type>...] - GUI browser (default)"); + System.out.println(" -bt - browse service types"); + System.out.println(" -bs <type> <domain> - browse services by type"); + System.out.println(" -rs <name> <type> <domain> <port> <txt> - register service"); + System.out.println(" -f <file> - rendezvous responder"); + System.out.println(); + System.exit(1); + } + } +} Added: trunk/plugins/MDNSDiscovery/com/strangeberry/jmdns/tools/Responder.java =================================================================== --- trunk/plugins/MDNSDiscovery/com/strangeberry/jmdns/tools/Responder.java (rev 0) +++ trunk/plugins/MDNSDiscovery/com/strangeberry/jmdns/tools/Responder.java 2007-03-04 15:23:01 UTC (rev 11954) @@ -0,0 +1,79 @@ +// %Z%%M%, %I%, %G% +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package plugins.MDNSDiscovery.com.strangeberry.jmdns.tools; + +import java.io.*; +import plugins.MDNSDiscovery.javax.jmdns.*; + +/** + * A sample JmDNS responder that reads a set of rendezvous service + * definitions from a file and registers them with rendezvous. It uses + * the same file format as Apple's responder. Each record consists of + * 4 lines: name, type, text, port. Empty lines and lines starting with # + * between records are ignored. + * + * @author Arthur van Hoff + * @version %I%, %G% + */ +public class Responder +{ + /** + * Constructor. + */ + public Responder(JmDNS jmdns, String file) throws IOException + { + BufferedReader in = new BufferedReader(new FileReader(file)); + try { + while (true) { + String ln = in.readLine(); + while ((ln != null) && (ln.startsWith("#") || ln.trim().length() == 0)) { + ln = in.readLine(); + } + if (ln == null) { + break; + } + String name = ln; + String type = in.readLine(); + String text = in.readLine(); + int port = Integer.parseInt(in.readLine()); + + // make sure the type is fully qualified and in the local. domain + if (!type.endsWith(".")) { + type += "."; + } + if (!type.endsWith(".local.")) { + type += "local."; + } + + jmdns.registerService( + new ServiceInfo(type, name, port, text)); + } + } finally { + in.close(); + } + } + + /** + * Create a responder. + */ + public static void main(String argv[]) throws IOException + { + new Responder(new JmDNS(), (argv.length > 0) ? argv[0] : "services.txt"); + } +} + + Added: trunk/plugins/MDNSDiscovery/gpl.txt =================================================================== --- trunk/plugins/MDNSDiscovery/gpl.txt (rev 0) +++ trunk/plugins/MDNSDiscovery/gpl.txt 2007-03-04 15:23:01 UTC (rev 11954) @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + 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 + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. Added: trunk/plugins/MDNSDiscovery/javax/jmdns/DNSCache.java =================================================================== --- trunk/plugins/MDNSDiscovery/javax/jmdns/DNSCache.java (rev 0) +++ trunk/plugins/MDNSDiscovery/javax/jmdns/DNSCache.java 2007-03-04 15:23:01 UTC (rev 11954) @@ -0,0 +1,257 @@ +//Copyright 2003-2005 Arthur van Hoff Rick Blair +//Licensed under Apache License version 2.0 +//Original license LGPL + + +package plugins.MDNSDiscovery.javax.jmdns; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.logging.Logger; + +/** + * A table of DNS entries. This is a hash table which + * can handle multiple entries with the same name. + * <p/> + * Storing multiple entries with the same name is implemented using a + * linked list of <code>CacheNode</code>'s. + * <p/> + * The current implementation of the API of DNSCache does expose the + * cache nodes to clients. Clients must explicitly deal with the nodes + * when iterating over entries in the cache. Here's how to iterate over + * all entries in the cache: + * <pre> + * for (Iterator i=dnscache.iterator(); i.hasNext(); ) { + * for (DNSCache.CacheNode n = (DNSCache.CacheNode) i.next(); n != null; n.next()) { + * DNSEntry entry = n.getValue(); + * ...do something with entry... + * } + * } + * </pre> + * <p/> + * And here's how to iterate over all entries having a given name: + * <pre> + * for (DNSCache.CacheNode n = (DNSCache.CacheNode) dnscache.find(name); n != null; n.next()) { + * DNSEntry entry = n.getValue(); + * ...do something with entry... + * } + * </pre> + * + * @version %I%, %G% + * @author Arthur van Hoff, Werner Randelshofer, Rick Blair + */ +class DNSCache +{ + private static Logger logger = Logger.getLogger(DNSCache.class.toString()); + // Implementation note: + // We might completely hide the existence of CacheNode's in a future version + // of DNSCache. But this will require to implement two (inner) classes for + // the iterators that will be returned by method <code>iterator()</code> and + // method <code>find(name)</code>. + // Since DNSCache is not a public class, it does not seem worth the effort + // to clean its API up that much. + + // [PJYF Oct 15 2004] This should implements Collections that would be amuch cleaner implementation + + /** + * The number of DNSEntry's in the cache. + */ + private int size; + + /** + * The hashtable used internally to store the entries of the cache. + * Keys are instances of String. The String contains an unqualified service + * name. + * Values are linked lists of CacheNode instances. + */ + private HashMap hashtable; + + /** + * Cache nodes are used to implement storage of multiple DNSEntry's of the + * same name in the cache. + */ + public static class CacheNode + { + private static Logger logger = Logger.getLogger(CacheNode.class.toString()); + private DNSEntry value; + private CacheNode next; + + public CacheNode(DNSEntry value) + { + this.value = value; + } + + public CacheNode next() + { + return next; + } + + public DNSEntry getValue() + { + return value; + } + } + + + /** + * Create a table with a given initial size. + */ + public DNSCache(final int size) + { + hashtable = new HashMap(size); + } + + /** + * Clears the cache. + */ + public synchronized void clear() + { + hashtable.clear(); + size = 0; + } + + /** + * Adds an entry to the table. + */ + public synchronized void add(final DNSEntry entry) + { + //logger.log("DNSCache.add("+entry.getName()+")"); + CacheNode newValue = new CacheNode(entry); + CacheNode node = (CacheNode) hashtable.get(entry.getName()); + if (node == null) + { + hashtable.put(entry.getName(), newValue); + } + else + { + newValue.next = node.next; + node.next = newValue; + } + size++; + } + + /** + * Remove a specific entry from the table. Returns true if the + * entry was found. + */ + public synchronized boolean remove(DNSEntry entry) + { + CacheNode node = (CacheNode) hashtable.get(entry.getName()); + if (node != null) + { + if (node.value == entry) + { + if (node.next == null) + { + hashtable.remove(entry.getName()); + } + else + { + hashtable.put(entry.getName(), node.next); + } + size--; + return true; + } + + CacheNode previous = node; + node = node.next; + while (node != null) + { + if (node.value == entry) + { + previous.next = node.next; + size--; + return true; + } + previous = node; + node = node.next; + } + ; + } + return false; + } + + /** + * Get a matching DNS entry from the table (using equals). + * Returns the entry that was found. + */ + public synchronized DNSEntry get(DNSEntry entry) + { + for (CacheNode node = find(entry.getName()); node != null; node = node.next) + { + if (node.value.equals(entry)) + { + return node.value; + } + } + return null; + } + + /** + * Get a matching DNS entry from the table. + */ + public synchronized DNSEntry get(String name, int type, int clazz) + { + for (CacheNode node = find(name); node != null; node = node.next) + { + if (node.value.type == type && node.value.clazz == clazz) + { + return node.value; + } + } + return null; + } + + /** + * Iterates over all cache nodes. + * The iterator returns instances of DNSCache.CacheNode. + * Each instance returned is the first node of a linked list. + * To retrieve all entries, one must iterate over this linked list. See + * code snippets in the header of the class. + */ + public Iterator iterator() + { + return Collections.unmodifiableCollection(hashtable.values()).iterator(); + } + + /** + * Iterate only over items with matching name. + * Returns an instance of DNSCache.CacheNode or null. + * If an instance is returned, it is the first node of a linked list. + * To retrieve all entries, one must iterate over this linked list. + */ + public synchronized CacheNode find(String name) + { + return (CacheNode) hashtable.get(name); + } + + /** + * List all entries for debugging. + */ + public synchronized void print() + { + for (Iterator i = iterator(); i.hasNext();) + { + for (CacheNode n = (CacheNode) i.next(); n != null; n = n.next) + { + System.out.println(n.value); + } + } + } + + public synchronized String toString() + { + StringBuffer aLog = new StringBuffer(); + aLog.append("\t---- cache ----"); + for (Iterator i = iterator(); i.hasNext();) + { + for (CacheNode n = (CacheNode) i.next(); n != null; n = n.next) + { + aLog.append("\n\t\t" + n.value); + } + } + return aLog.toString(); + } + +} Added: trunk/plugins/MDNSDiscovery/javax/jmdns/DNSConstants.java =================================================================== --- trunk/plugins/MDNSDiscovery/javax/jmdns/DNSConstants.java (rev 0) +++ trunk/plugins/MDNSDiscovery/javax/jmdns/DNSConstants.java 2007-03-04 15:23:01 UTC (rev 11954) @@ -0,0 +1,125 @@ +//Copyright 2003-2005 Arthur van Hoff, Rick Blair +//Licensed under Apache License version 2.0 +//Original license LGPL + + +package plugins.MDNSDiscovery.javax.jmdns; + +/** + * DNS constants. + * + * @version %I%, %G% + * @author Arthur van Hoff, Jeff Sonstein, Werner Randelshofer, Pierre Frisch, Rick Blair + */ +final class DNSConstants +{ + + // changed to final class - jeffs + final static String MDNS_GROUP = "224.0.0.251"; + final static String MDNS_GROUP_IPV6 = "FF02::FB"; + final static int MDNS_PORT = 5353; + final static int DNS_PORT = 53; + final static int DNS_TTL = 60 * 60; // default one hour TTL + // final static int DNS_TTL = 120 * 60; // two hour TTL (draft-cheshire-dnsext-multicastdns.txt ch 13) + + final static int MAX_MSG_TYPICAL = 1460; + final static int MAX_MSG_ABSOLUTE = 8972; + + final static int FLAGS_QR_MASK = 0x8000; // Query response mask + final static int FLAGS_QR_QUERY = 0x0000; // Query + final static int FLAGS_QR_RESPONSE = 0x8000; // Response + + final static int FLAGS_AA = 0x0400; // Authorative answer + final static int FLAGS_TC = 0x0200; // Truncated + final static int FLAGS_RD = 0x0100; // Recursion desired + final static int FLAGS_RA = 0x8000; // Recursion available + + final static int FLAGS_Z = 0x0040; // Zero + final static int FLAGS_AD = 0x0020; // Authentic data + final static int FLAGS_CD = 0x0010; // Checking disabled + + final static int CLASS_IN = 1; // Final Static Internet + final static int CLASS_CS = 2; // CSNET + final static int CLASS_CH = 3; // CHAOS + final static int CLASS_HS = 4; // Hesiod + final static int CLASS_NONE = 254; // Used in DNS UPDATE [RFC 2136] + final static int CLASS_ANY = 255; // Not a DNS class, but a DNS query class, meaning "all classes" + final static int CLASS_MASK = 0x7FFF; // Multicast DNS uses the bottom 15 bits to identify the record class... + final static int CLASS_UNIQUE = 0x8000; // ... and the top bit indicates that all other cached records are now invalid + + final static int TYPE_IGNORE = 0; // This is a hack to stop further processing + final static int TYPE_A = 1; // Address + final static int TYPE_NS = 2; // Name Server + final static int TYPE_MD = 3; // Mail Destination + final static int TYPE_MF = 4; // Mail Forwarder + final static int TYPE_CNAME = 5; // Canonical Name + final static int TYPE_SOA = 6; // Start of Authority + final static int TYPE_MB = 7; // Mailbox + final static int TYPE_MG = 8; // Mail Group + final static int TYPE_MR = 9; // Mail Rename + final static int TYPE_NULL = 10; // NULL RR + final static int TYPE_WKS = 11; // Well-known-service + final static int TYPE_PTR = 12; // Domain Name pofinal static inter + final static int TYPE_HINFO = 13; // Host information + final static int TYPE_MINFO = 14; // Mailbox information + final static int TYPE_MX = 15; // Mail exchanger + final static int TYPE_TXT = 16; // Arbitrary text string + final static int TYPE_RP = 17; // for Responsible Person [RFC1183] + final static int TYPE_AFSDB = 18; // for AFS Data Base location [RFC1183] + final static int TYPE_X25 = 19; // for X.25 PSDN address [RFC1183] + final static int TYPE_ISDN = 20; // for ISDN address [RFC1183] + final static int TYPE_RT = 21; // for Route Through [RFC1183] + final static int TYPE_NSAP = 22; // for NSAP address, NSAP style A record [RFC1706] + final static int TYPE_NSAP_PTR = 23; // + final static int TYPE_SIG = 24; // for security signature [RFC2931] + final static int TYPE_KEY = 25; // for security key [RFC2535] + final static int TYPE_PX = 26; // X.400 mail mapping information [RFC2163] + final static int TYPE_GPOS = 27; // Geographical Position [RFC1712] + final static int TYPE_AAAA = 28; // IP6 Address [Thomson] + final static int TYPE_LOC = 29; // Location Information [Vixie] + final static int TYPE_NXT = 30; // Next Domain - OBSOLETE [RFC2535, RFC3755] + final static int TYPE_EID = 31; // Endpoint Identifier [Patton] + final static int TYPE_NIMLOC = 32; // Nimrod Locator [Patton] + final static int TYPE_SRV = 33; // Server Selection [RFC2782] + final static int TYPE_ATMA = 34; // ATM Address [Dobrowski] + final static int TYPE_NAPTR = 35; // Naming Authority Pointer [RFC2168, RFC2915] + final static int TYPE_KX = 36; // Key Exchanger [RFC2230] + final static int TYPE_CERT = 37; // CERT [RFC2538] + final static int TYPE_A6 = 38; // A6 [RFC2874] + final static int TYPE_DNAME = 39; // DNAME [RFC2672] + final static int TYPE_SINK = 40; // SINK [Eastlake] + final static int TYPE_OPT = 41; // OPT [RFC2671] + final static int TYPE_APL = 42; // APL [RFC3123] + final static int TYPE_DS = 43; // Delegation Signer [RFC3658] + final static int TYPE_SSHFP = 44; // SSH Key Fingerprint [RFC-ietf-secsh-dns-05.txt] + final static int TYPE_RRSIG = 46; // RRSIG [RFC3755] + final static int TYPE_NSEC = 47; // NSEC [RFC3755] + final static int TYPE_DNSKEY = 48; // DNSKEY [RFC3755] + final static int TYPE_UINFO = 100; // [IANA-Reserved] + final static int TYPE_UID = 101; // [IANA-Reserved] + final static int TYPE_GID = 102; // [IANA-Reserved] + final static int TYPE_UNSPEC = 103; // [IANA-Reserved] + final static int TYPE_TKEY = 249; // Transaction Key [RFC2930] + final static int TYPE_TSIG = 250; // Transaction Signature [RFC2845] + final static int TYPE_IXFR = 251; // Incremental transfer [RFC1995] + final static int TYPE_AXFR = 252; // Transfer of an entire zone [RFC1035] + final static int TYPE_MAILA = 253; // Mailbox-related records (MB, MG or MR) [RFC1035] + final static int TYPE_MAILB = 254; // Mail agent RRs (Obsolete - see MX) [RFC1035] + final static int TYPE_ANY = 255; // Request for all records [RFC1035] + + //Time Intervals for various functions + + final static int SHARED_QUERY_TIME = 20; //milliseconds before send shared query + final static int QUERY_WAIT_INTERVAL = 225; //milliseconds between query loops. + final static int PROBE_WAIT_INTERVAL = 250; //milliseconds between probe loops. + final static int RESPONSE_MIN_WAIT_INTERVAL = 20; //minimal wait interval for response. + final static int RESPONSE_MAX_WAIT_INTERVAL = 115; //maximal wait interval for response + final static int PROBE_CONFLICT_INTERVAL = 1000; //milliseconds to wait after conflict. + final static int PROBE_THROTTLE_COUNT = 10; //After x tries go 1 time a sec. on probes. + final static int PROBE_THROTTLE_COUNT_INTERVAL = 5000; //We only increment the throttle count, if + // the previous increment is inside this interval. + final static int ANNOUNCE_WAIT_INTERVAL = 1000; //milliseconds between Announce loops. + final static int RECORD_REAPER_INTERVAL = 10000; //milliseconds between cache cleanups. + final static int KNOWN_ANSWER_TTL = 120; + final static int ANNOUNCED_RENEWAL_TTL_INTERVAL = DNS_TTL * 500; // 50% of the TTL in milliseconds +} Added: trunk/plugins/MDNSDiscovery/javax/jmdns/DNSEntry.java =================================================================== --- trunk/plugins/MDNSDiscovery/javax/jmdns/DNSEntry.java (rev 0) +++ trunk/plugins/MDNSDiscovery/javax/jmdns/DNSEntry.java 2007-03-04 15:23:01 UTC (rev 11954) @@ -0,0 +1,148 @@ +//Copyright 2003-2005 Arthur van Hoff, Rick Blair +//Licensed under Apache License version 2.0 +//Original license LGPL + + +package plugins.MDNSDiscovery.javax.jmdns; + +import java.util.logging.Logger; + +/** + * DNS entry with a name, type, and class. This is the base + * class for questions and records. + * + * @version %I%, %G% + * @author Arthur van Hoff, Pierre Frisch, Rick Blair + */ +class DNSEntry +{ + private static Logger logger = Logger.getLogger(DNSEntry.class.toString()); + String key; + String name; + int type; + int clazz; + boolean unique; + + /** + * Create an entry. + */ + DNSEntry(String name, int type, int clazz) + { + this.key = name.toLowerCase(); + this.name = name; + this.type = type; + this.clazz = clazz & DNSConstants.CLASS_MASK; + this.unique = (clazz & DNSConstants.CLASS_UNIQUE) != 0; + } + + /** + * Check if two entries have exactly the same name, type, and class. + */ + public boolean equals(Object obj) + { + if (obj instanceof DNSEntry) + { + DNSEntry other = (DNSEntry) obj; + return name.equals(other.name) && type == other.type && clazz == other.clazz; + } + return false; + } + + public String getName() + { + return name; + } + + public int getType() + { + return type; + } + + /** + * Overriden, to return a value which is consistent with the value returned + * by equals(Object). + */ + public int hashCode() + { + return name.hashCode() + type + clazz; + } + + /** + * Get a string given a clazz. + */ + static String getClazz(int clazz) + { + switch (clazz & DNSConstants.CLASS_MASK) + { + case DNSConstants.CLASS_IN: + return "in"; + case DNSConstants.CLASS_CS: + return "cs"; + case DNSConstants.CLASS_CH: + return "ch"; + case DNSConstants.CLASS_HS: + return "hs"; + case DNSConstants.CLASS_NONE: + return "none"; + case DNSConstants.CLASS_ANY: + return "any"; + default: + return "?"; + } + } + + /** + * Get a string given a type. + */ + static String getType(int type) + { + switch (type) + { + case DNSConstants.TYPE_A: + return "a"; + case DNSConstants.TYPE_AAAA: + return "aaaa"; + case DNSConstants.TYPE_NS: + return "ns"; + case DNSConstants.TYPE_MD: + return "md"; + case DNSConstants.TYPE_MF: + return "mf"; + case DNSConstants.TYPE_CNAME: + return "cname"; + case DNSConstants.TYPE_SOA: + return "soa"; + case DNSConstants.TYPE_MB: + return "mb"; + case DNSConstants.TYPE_MG: + return "mg"; + case DNSConstants.TYPE_MR: + return "mr"; + case DNSConstants.TYPE_NULL: + return "null"; + case DNSConstants.TYPE_WKS: + return "wks"; + case DNSConstants.TYPE_PTR: + return "ptr"; + case DNSConstants.TYPE_HINFO: + return "hinfo"; + case DNSConstants.TYPE_MINFO: + return "minfo"; + case DNSConstants.TYPE_MX: + return "mx"; + case DNSConstants.TYPE_TXT: + return "txt"; + case DNSConstants.TYPE_SRV: + return "srv"; + case DNSConstants.TYPE_ANY: + return "any"; + default: + return "?"; + } + } + + public String toString(String hdr, String other) + { + return hdr + "[" + getType(type) + "," + getClazz(clazz) + (unique ? "-unique," : ",") + name + ((other != null) ? "," + other + "]" : "]"); + } +} Added: trunk/plugins/MDNSDiscovery/javax/jmdns/DNSIncoming.java =================================================================== --- trunk/plugins/MDNSDiscovery/javax/jmdns/DNSIncoming.java (rev 0) +++ trunk/plugins/MDNSDiscovery/javax/jmdns/DNSIncoming.java 2007-03-04 15:23:01 UTC (rev 11954) @@ -0,0 +1,466 @@ +///Copyright 2003-2005 Arthur van Hoff, Rick Blair +//Licensed under Apache License version 2.0 +//Original license LGPL + + + +package plugins.MDNSDiscovery.javax.jmdns; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Parse an incoming DNS message into its components. + * + * @version %I%, %G% + * @author Arthur van Hoff, Werner Randelshofer, Pierre Frisch + */ +final class DNSIncoming +{ + private static Logger logger = Logger.getLogger(DNSIncoming.class.toString()); + // Implementation note: This vector should be immutable. + // If a client of DNSIncoming changes the contents of this vector, + // we get undesired results. To fix this, we have to migrate to + // the Collections API of Java 1.2. i.e we replace Vector by List. + // final static Vector EMPTY = new Vector(); + + private DatagramPacket packet; + private int off; + private int len; + private byte data[]; + + int id; + private int flags; + private int numQuestions; + int numAnswers; + private int numAuthorities; + private int numAdditionals; + private long receivedTime; + + List questions; + List answers; + + /** + * Parse a message from a datagram packet. + */ + DNSIncoming(DatagramPacket packet) throws IOException + { + this.packet = packet; + this.data = packet.getData(); + this.len = packet.getLength(); + this.off = packet.getOffset(); + this.questions = Collections.EMPTY_LIST; + this.answers = Collections.EMPTY_LIST; + this.receivedTime = System.currentTimeMillis(); + + try + { + id = readUnsignedShort(); + flags = readUnsignedShort(); + numQuestions = readUnsignedShort(); + numAnswers = readUnsignedShort(); + numAuthorities = readUnsignedShort(); + numAdditionals = readUnsignedShort(); + + // parse questions + if (numQuestions > 0) + { + questions = Collections.synchronizedList(new ArrayList(numQuestions)); + for (int i = 0; i < numQuestions; i++) + { + DNSQuestion question = new DNSQuestion(readName(), readUnsignedShort(), readUnsignedShort()); + questions.add(question); + } + } + + // parse answers + int n = numAnswers + numAuthorities + numAdditionals; + if (n > 0) + { + answers = Collections.synchronizedList(new ArrayList(n)); + for (int i = 0; i < n; i++) + { + String domain = readName(); + int type = readUnsignedShort(); + int clazz = readUnsignedShort(); + int ttl = readInt(); + int len = readUnsignedShort(); + int end = off + len; + DNSRecord rec = null; + + switch (type) + { + case DNSConstants.TYPE_A: // IPv4 + case DNSConstants.TYPE_AAAA: // IPv6 FIXME [PJYF Oct 14 2004] This has not been tested + rec = new DNSRecord.Address(domain, type, clazz, ttl, readBytes(off, len)); + break; + case DNSConstants.TYPE_CNAME: + case DNSConstants.TYPE_PTR: + rec = new DNSRecord.Pointer(domain, type, clazz, ttl, readName()); + break; + case DNSConstants.TYPE_TXT: + rec = new DNSRecord.Text(domain, type, clazz, ttl, readBytes(off, len)); + break; + case DNSConstants.TYPE_SRV: + rec = new DNSRecord.Service(domain, type, clazz, ttl, + readUnsignedShort(), readUnsignedShort(), readUnsignedShort(), readName()); + break; + case DNSConstants.TYPE_HINFO: + // Maybe we should do something with those + break; + default : + logger.finer("DNSIncoming() unknown type:" + type); + break; + } + + if (rec != null) + { + // Add a record, if we were able to create one. + answers.add(rec); + } + else + { + // Addjust the numbers for the skipped record + if (answers.size() < numAnswers) + { + numAnswers--; + } + else + { + if (answers.size() < numAnswers + numAuthorities) + { + numAuthorities--; + } + else + { + if (answers.size() < numAnswers + numAuthorities + numAdditionals) + { + numAdditionals--; + } + } + } + } + off = end; + } + } + } + catch (IOException e) + { + logger.log(Level.WARNING, "DNSIncoming() dump " + print(true) + "\n exception ", e); + throw e; + } + } + + /** + * Check if the message is a query. + */ + boolean isQuery() + { + return (flags & DNSConstants.FLAGS_QR_MASK) == DNSConstants.FLAGS_QR_QUERY; + } + + /** + * Check if the message is truncated. + */ + boolean isTruncated() + { + return (flags & DNSConstants.FLAGS_TC) != 0; + } + + /** + * Check if the message is a response. + */ + boolean isResponse() + { + return (flags & DNSConstants.FLAGS_QR_MASK) == DNSConstants.FLAGS_QR_RESPONSE; + } + + private int get(int off) throws IOException + { + if ((off < 0) || (off >= len)) + { + throw new IOException("parser error: offset=" + off); + } + return data[off] & 0xFF; + } + + private int readUnsignedShort() throws IOException + { + return (get(off++) << 8) + get(off++); + } + + private int readInt() throws IOException + { + return (readUnsignedShort() << 16) + readUnsignedShort(); + } + + private byte[] readBytes(int off, int len) throws IOException + { + byte bytes[] = new byte[len]; + System.arraycopy(data, off, bytes, 0, len); + return bytes; + } + + private void readUTF(StringBuffer buf, int off, int len) throws IOException + { + for (int end = off + len; off < end;) + { + int ch = get(off++); + switch (ch >> 4) + { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + // 0xxxxxxx + break; + case 12: + case 13: + // 110x xxxx 10xx xxxx + ch = ((ch & 0x1F) << 6) | (get(off++) & 0x3F); + break; + case 14: + // 1110 xxxx 10xx xxxx 10xx xxxx + ch = ((ch & 0x0f) << 12) | ((get(off++) & 0x3F) << 6) | (get(off++) & 0x3F); + break; + default: + // 10xx xxxx, 1111 xxxx + ch = ((ch & 0x3F) << 4) | (get(off++) & 0x0f); + break; + } + buf.append((char) ch); + } + } + + private String readName() throws IOException + { + StringBuffer buf = new StringBuffer(); + int off = this.off; + int next = -1; + int first = off; + + while (true) + { + int len = get(off++); + if (len == 0) + { + break; + } + switch (len & 0xC0) + { + case 0x00: + //buf.append("[" + off + "]"); + readUTF(buf, off, len); + off += len; + buf.append('.'); + break; + case 0xC0: + //buf.append("<" + (off - 1) + ">"); + if (next < 0) + { + next = off + 1; + } + off = ((len & 0x3F) << 8) | get(off++); + if (off >= first) + { + throw new IOException("bad domain name: possible circular name detected"); + } + first = off; + break; + default: + throw new IOException("bad domain name: '" + buf + "' at " + off); + } + } + this.off = (next >= 0) ? next : off; + return buf.toString(); + } + + /** + * Debugging. + */ + String print(boolean dump) + { + StringBuffer buf = new StringBuffer(); + buf.append(toString() + "\n"); + for (Iterator iterator = questions.iterator(); iterator.hasNext();) + { + buf.append(" ques:" + iterator.next() + "\n"); + } + int count = 0; + for (Iterator iterator = answers.iterator(); iterator.hasNext(); count++) + { + if (count < numAnswers) + { + buf.append(" answ:"); + } + else + { + if (count < numAnswers + numAuthorities) + { + buf.append(" auth:"); + } + else + { + buf.append(" addi:"); + } + } + buf.append(iterator.next() + "\n"); + } + if (dump) + { + for (int off = 0, len = packet.getLength(); off < len; off += 32) + { + int n = Math.min(32, len - off); + if (off < 10) + { + buf.append(' '); + } + if (off < 100) + { + buf.append(' '); + } + buf.append(off); + buf.append(':'); + for (int i = 0; i < n; i++) + { + if ((i % 8) == 0) + { + buf.append(' '); + } + buf.append(Integer.toHexString((data[off + i] & 0xF0) >> 4)); + buf.append(Integer.toHexString((data[off + i] & 0x0F) >> 0)); + } + buf.append("\n"); + buf.append(" "); + for (int i = 0; i < n; i++) + { + if ((i % 8) == 0) + { + buf.append(' '); + } + buf.append(' '); + int ch = data[off + i] & 0xFF; + buf.append(((ch > ' ') && (ch < 127)) ? (char) ch : '.'); + } + buf.append("\n"); + + // limit message size + if (off + 32 >= 256) + { + buf.append("....\n"); + break; + } + } + } + return buf.toString(); + } + + public String toString() + { + StringBuffer buf = new StringBuffer(); + buf.append(isQuery() ? "dns[query," : "dns[response,"); + if (packet.getAddress() != null) + { + buf.append(packet.getAddress().getHostAddress()); + } + buf.append(':'); + buf.append(packet.getPort()); + buf.append(",len="); + buf.append(packet.getLength()); + buf.append(",id=0x"); + buf.append(Integer.toHexString(id)); + if (flags != 0) + { + buf.append(",flags=0x"); + buf.append(Integer.toHexString(flags)); + if ((flags & DNSConstants.FLAGS_QR_RESPONSE) != 0) + { + buf.append(":r"); + } + if ((flags & DNSConstants.FLAGS_AA) != 0) + { + buf.append(":aa"); + } + if ((flags & DNSConstants.FLAGS_TC) != 0) + { + buf.append(":tc"); + } + } + if (numQuestions > 0) + { + buf.append(",questions="); + buf.append(numQuestions); + } + if (numAnswers > 0) + { + buf.append(",answers="); + buf.append(numAnswers); + } + if (numAuthorities > 0) + { + buf.append(",authorities="); + buf.append(numAuthorities); + } + if (numAdditionals > 0) + { + buf.append(",additionals="); + buf.append(numAdditionals); + } + buf.append("]"); + return buf.toString(); + } + + /** + * Appends answers to this Incoming. + * + * @throws IllegalArgumentException If not a query or if Truncated. + */ + void append(DNSIncoming that) + { + if (this.isQuery() && this.isTruncated() && that.isQuery()) + { + this.questions.addAll(that.questions); + this.numQuestions += that.numQuestions; + + if (Collections.EMPTY_LIST.equals(answers)) + { + answers = Collections.synchronizedList(new ArrayList()); + } + + if (that.numAnswers > 0) + { + this.answers.addAll(this.numAnswers, that.answers.subList(0, that.numAnswers)); + this.numAnswers += that.numAnswers; + } + if (that.numAuthorities > 0) + { + this.answers.addAll(this.numAnswers + this.numAuthorities, that.answers.subList(that.numAnswers, that.numAnswers + that.numAuthorities)); + this.numAuthorities += that.numAuthorities; + } + if (that.numAdditionals > 0) + { + this.answers.addAll(that.answers.subList(that.numAnswers + that.numAuthorities, that.numAnswers + that.numAuthorities + that.numAdditionals)); + this.numAdditionals += that.numAdditionals; + } + } + else + { + throw new IllegalArgumentException(); + } + } + + int elapseSinceArrival() + { + return (int) (System.currentTimeMillis() - receivedTime); + } +} Added: trunk/plugins/MDNSDiscovery/javax/jmdns/DNSListener.java =================================================================== --- trunk/plugins/MDNSDiscovery/javax/jmdns/DNSListener.java (rev 0) +++ trunk/plugins/MDNSDiscovery/javax/jmdns/DNSListener.java 2007-03-04 15:23:01 UTC (rev 11954) @@ -0,0 +1,24 @@ +//Copyright 2003-2005 Arthur van Hoff, Rick Blair +//Licensed under Apache License version 2.0 +//Original license LGPL + + +package plugins.MDNSDiscovery.javax.jmdns; + +// REMIND: Listener should follow Java idiom for listener or have a different +// name. + +/** + * DNSListener. + * Listener for record updates. + * + * @author Werner Randelshofer, Rick Blair + * @version 1.0 May 22, 2004 Created. + */ +interface DNSListener +{ + /** + * Update a DNS record. + */ + void updateRecord(JmDNS jmdns, long now, DNSRecord record); +} Added: trunk/plugins/MDNSDiscovery/javax/jmdns/DNSOutgoing.java =================================================================== --- trunk/plugins/MDNSDiscovery/javax/jmdns/DNSOutgoing.java (rev 0) +++ trunk/plugins/MDNSDiscovery/javax/jmdns/DNSOutgoing.java 2007-03-04 15:23:01 UTC (rev 11954) @@ -0,0 +1,381 @@ +//Copyright 2003-2005 Arthur van Hoff, Rick Blair +//Licensed under Apache License version 2.0 +//Original license LGPL + + +package plugins.MDNSDiscovery.javax.jmdns; + +import java.io.IOException; +import java.util.Hashtable; +import java.util.LinkedList; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * An outgoing DNS message. + * + * @version %I%, %G% + * @author Arthur van Hoff, Rick Blair, Werner Randelshofer + */ +final class DNSOutgoing +{ + private static Logger logger = Logger.getLogger(DNSOutgoing.class.toString()); + int id; + int flags; + private boolean multicast; + private int numQuestions; + private int numAnswers; + private int numAuthorities; + private int numAdditionals; + private Hashtable names; + + byte data[]; + int off; + int len; + + /** + * Create an outgoing multicast query or response. + */ + DNSOutgoing(int flags) + { + this(flags, true); + } + + /** + * Create an outgoing query or response. + */ + DNSOutgoing(int flags, boolean multicast) + { + this.flags = flags; + this.multicast = multicast; + names = new Hashtable(); + data = new byte[DNSConstants.MAX_MSG_TYPICAL]; + off = 12; + } + + /** + * Add a question to the message. + */ + void addQuestion(DNSQuestion rec) throws IOException + { + if (numAnswers > 0 || numAuthorities > 0 || numAdditionals > 0) + { + throw new IllegalStateException("Questions must be added before answers"); + } + numQuestions++; + writeQuestion(rec); + } + + /** + * Add an answer if it is not suppressed. + */ + void addAnswer(DNSIncoming in, DNSRecord rec) throws IOException + { + if (numAuthorities > 0 || numAdditionals > 0) + { + throw new IllegalStateException("Answers must be added before authorities and additionals"); + } + if (!rec.suppressedBy(in)) + { + addAnswer(rec, 0); + } + } + + /** + * Add an additional answer to the record. Omit if there is no room. + */ + void addAdditionalAnswer(DNSIncoming in, DNSRecord rec) throws IOException + { + if ((off < DNSConstants.MAX_MSG_TYPICAL - 200) && !rec.suppressedBy(in)) + { + writeRecord(rec, 0); + numAdditionals++; + } + } + + /** + * Add an answer to the message. + */ + void addAnswer(DNSRecord rec, long now) throws IOException + { + if (numAuthorities > 0 || numAdditionals > 0) + { + throw new IllegalStateException("Questions must be added before answers"); + } + if (rec != null) + { + if ((now == 0) || !rec.isExpired(now)) + { + writeRecord(rec, now); + numAnswers++; + } + } + } + + private LinkedList authorativeAnswers = new LinkedList(); + + /** + * Add an authorative answer to the message. + */ + void addAuthorativeAnswer(DNSRecord rec) throws IOException + { + if (numAdditionals > 0) + { + throw new IllegalStateException("Authorative answers must be added before additional answers"); + } + authorativeAnswers.add(rec); + writeRecord(rec, 0); + numAuthorities++; + + // VERIFY: + + } + + void writeByte(int value) throws IOException + { + if (off >= data.length) + { + throw new IOException("buffer full"); + } + data[off++] = (byte) value; + } + + void writeBytes(String str, int off, int len) throws IOException + { + for (int i = 0; i < len; i++) + { + writeByte(str.charAt(off + i)); + } + } + + void writeBytes(byte data[]) throws IOException + { + if (data != null) + { + writeBytes(data, 0, data.length); + } + } + + void writeBytes(byte data[], int off, int len) throws IOException + { + for (int i = 0; i < len; i++) + { + writeByte(data[off + i]); + } + } + + void writeShort(int value) throws IOException + { + writeByte(value >> 8); + writeByte(value); + } + + void writeInt(int value) throws IOException + { + writeShort(value >> 16); + writeShort(value); + } + + void writeUTF(String str, int off, int len) throws IOException + { + // compute utf length + int utflen = 0; + for (int i = 0; i < len; i++) + { + int ch = str.charAt(off + i); + if ((ch >= 0x0001) && (ch <= 0x007F)) + { + utflen += 1; + } + else + { + if (ch > 0x07FF) + { + utflen += 3; + } + else + { + utflen += 2; + } + } + } + // write utf length + writeByte(utflen); + // write utf data + for (int i = 0; i < len; i++) + { + int ch = str.charAt(off + i); + if ((ch >= 0x0001) && (ch <= 0x007F)) + { + writeByte(ch); + } + else + { + if (ch > 0x07FF) + { + writeByte(0xE0 | ((ch >> 12) & 0x0F)); + writeByte(0x80 | ((ch >> 6) & 0x3F)); + writeByte(0x80 | ((ch >> 0) & 0x3F)); + } + else + { + writeByte(0xC0 | ((ch >> 6) & 0x1F)); + writeByte(0x80 | ((ch >> 0) & 0x3F)); + } + } + } + } + + void writeName(String name) throws IOException + { + while (true) + { + int n = name.indexOf('.'); + if (n < 0) + { + n = name.length(); + } + if (n <= 0) + { + writeByte(0); + return; + } + Integer offset = (Integer) names.get(name); + if (offset != null) + { + int val = offset.intValue(); + + if (val > off) + { + logger.log(Level.WARNING, "DNSOutgoing writeName failed val=" + val + " name=" + name); + } + + writeByte((val >> 8) | 0xC0); + writeByte(val); + return; + } + names.put(name, new Integer(off)); + writeUTF(name, 0, n); + name = name.substring(n); + if (name.startsWith(".")) + { + name = name.substring(1); + } + } + } + + void writeQuestion(DNSQuestion question) throws IOException + { + writeName(question.name); + writeShort(question.type); + writeShort(question.clazz); + } + + void writeRecord(DNSRecord rec, long now) throws IOException + { + int save = off; + try + { + writeName(rec.name); + writeShort(rec.type); + writeShort(rec.clazz | ((rec.unique && multicast) ? DNSConstants.CLASS_UNIQUE : 0)); + writeInt((now == 0) ? rec.ttl : rec.getRemainingTTL(now)); + writeShort(0); + int start = off; + rec.write(this); + int len = off - start; + data[start - 2] = (byte) (len >> 8); + data[start - 1] = (byte) (len & 0xFF); + } + catch (IOException e) + { + off = save; + throw e; + } + } + + /** + * Finish the message before sending it off. + */ + void finish() throws IOException + { + int save = off; + off = 0; + + writeShort(multicast ? 0 : id); + writeShort(flags); + writeShort(numQuestions); + writeShort(numAnswers); + writeShort(numAuthorities); + writeShort(numAdditionals); + off = save; + } + + boolean isQuery() + { + return (flags & DNSConstants.FLAGS_QR_MASK) == DNSConstants.FLAGS_QR_QUERY; + } + + public boolean isEmpty() + { + return numQuestions == 0 && numAuthorities == 0 + && numAdditionals == 0 && numAnswers == 0; + } + + + public String toString() + { + StringBuffer buf = new StringBuffer(); + buf.append(isQuery() ? "dns[query," : "dns[response,"); + //buf.append(packet.getAddress().getHostAddress()); + buf.append(':'); + //buf.append(packet.getPort()); + //buf.append(",len="); + //buf.append(packet.getLength()); + buf.append(",id=0x"); + buf.append(Integer.toHexString(id)); + if (flags != 0) + { + buf.append(",flags=0x"); + buf.append(Integer.toHexString(flags)); + if ((flags & DNSConstants.FLAGS_QR_RESPONSE) != 0) + { + buf.append(":r"); + } + if ((flags & DNSConstants.FLAGS_AA) != 0) + { + buf.append(":aa"); + } + if ((flags & DNSConstants.FLAGS_TC) != 0) + { + buf.append(":tc"); + } + } + if (numQuestions > 0) + { + buf.append(",questions="); + buf.append(numQuestions); + } + if (numAnswers > 0) + { + buf.append(",answers="); + buf.append(numAnswers); + } + if (numAuthorities > 0) + { + buf.append(",authorities="); + buf.append(numAuthorities); + } + if (numAdditionals > 0) + { + buf.append(",additionals="); + buf.append(numAdditionals); + } + buf.append(",\nnames=" + names); + buf.append(",\nauthorativeAnswers=" + authorativeAnswers); + + buf.append("]"); + return buf.toString(); + } + +} Added: trunk/plugins/MDNSDiscovery/javax/jmdns/DNSQuestion.java =================================================================== --- trunk/plugins/MDNSDiscovery/javax/jmdns/DNSQuestion.java (rev 0) +++ trunk/plugins/MDNSDiscovery/javax/jmdns/DNSQuestion.java 2007-03-04 15:23:01 UTC (rev 11954) @@ -0,0 +1,44 @@ +//Copyright 2003-2005 Arthur van Hoff, Rick Blair +//Licensed under Apache License version 2.0 +//Original license LGPL + + +package plugins.MDNSDiscovery.javax.jmdns; + +import java.util.logging.Logger; + +/** + * A DNS question. + * + * @version %I%, %G% + * @author Arthur van Hoff + */ +final class DNSQuestion extends DNSEntry +{ + private static Logger logger = Logger.getLogger(DNSQuestion.class.toString()); + + /** + * Create a question. + */ + DNSQuestion(String name, int type, int clazz) + { + super(name, type, clazz); + } + + /** + * Check if this question is answered by a given DNS record. + */ + boolean answeredBy(DNSRecord rec) + { + return (clazz == rec.clazz) && ((type == rec.type) || (type == DNSConstants.TYPE_ANY)) && + name.equals(rec.name); + } + + /** + * For debugging only. + */ + public String toString() + { + return toString("question", null); + } +} Added: trunk/plugins/MDNSDiscovery/javax/jmdns/DNSRecord.java =================================================================== --- trunk/plugins/MDNSDiscovery/javax/jmdns/DNSRecord.java (rev 0) +++ trunk/plugins/MDNSDiscovery/javax/jmdns/DNSRecord.java 2007-03-04 15:23:01 UTC (rev 11954) @@ -0,0 +1,673 @@ +//Copyright 2003-2005 Arthur van Hoff, Rick Blair +//Licensed under Apache License version 2.0 +//Original license LGPL + + +package plugins.MDNSDiscovery.javax.jmdns; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Iterator; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * DNS record + * + * @version %I%, %G% + * @author Arthur van Hoff, Rick Blair, Werner Randelshofer, Pierre Frisch + */ +abstract class DNSRecord extends DNSEntry +{ + private static Logger logger = Logger.getLogger(DNSRecord.class.toString()); + int ttl; + private long created; + + /** + * Create a DNSRecord with a name, type, clazz, and ttl. + */ + DNSRecord(String name, int type, int clazz, int ttl) + { + super(name, type, clazz); + this.ttl = ttl; + this.created = System.currentTimeMillis(); + } + + /** + * True if this record is the same as some other record. + */ + public boolean equals(Object other) + { + return (other instanceof DNSRecord) && sameAs((DNSRecord) other); + } + + /** + * True if this record is the same as some other record. + */ + boolean sameAs(DNSRecord other) + { + return super.equals(other) && sameValue((DNSRecord) other); + } + + /** + * True if this record has the same value as some other record. + */ + abstract boolean sameValue(DNSRecord other); + + /** + * True if this record has the same type as some other record. + */ + boolean sameType(DNSRecord other) + { + return type == other.type; + } + + /** + * Handles a query represented by this record. + * + * @return Returns true if a conflict with one of the services registered + * with JmDNS or with the hostname occured. + */ + abstract boolean handleQuery(JmDNS dns, long expirationTime); + + /** + * Handles a responserepresented by this record. + * + * @return Returns true if a conflict with one of the services registered + * with JmDNS or with the hostname occured. + */ + abstract boolean handleResponse(JmDNS dns); + + /** + * Adds this as an answer to the provided outgoing datagram. + */ + abstract DNSOutgoing addAnswer(JmDNS dns, DNSIncoming in, InetAddress addr, int port, DNSOutgoing out) throws IOException; + + /** + * True if this record is suppressed by the answers in a message. + */ + boolean suppressedBy(DNSIncoming msg) + { + try + { + for (int i = msg.numAnswers; i-- > 0;) + { + if (suppressedBy((DNSRecord) msg.answers.get(i))) + { + return true; + } + } + return false; + } + catch (ArrayIndexOutOfBoundsException e) + { + logger.log(Level.WARNING, "suppressedBy() message " + msg + " exception ", e); + // msg.print(true); + return false; + } + } + + /** + * True if this record would be supressed by an answer. + * This is the case if this record would not have a + * significantly longer TTL. + */ + boolean suppressedBy(DNSRecord other) + { + if (sameAs(other) && (other.ttl > ttl / 2)) + { + return true; + } + return false; + } + + /** + * Get the expiration time of this record. + */ + long getExpirationTime(int percent) + { + return created + (percent * ttl * 10L); + } + + /** + * Get the remaining TTL for this record. + */ + int getRemainingTTL(long now) + { + return (int) Math.max(0, (getExpirationTime(100) - now) / 1000); + } + + /** + * Check if the record is expired. + */ + boolean isExpired(long now) + { + return getExpirationTime(100) <= now; + } + + /** + * Check if the record is stale, ie it has outlived + * more than half of its TTL. + */ + boolean isStale(long now) + { + return getExpirationTime(50) <= now; + } + + /** + * Reset the TTL of a record. This avoids having to + * update the entire record in the cache. + */ + void resetTTL(DNSRecord other) + { + created = other.created; + ttl = other.ttl; + } + + /** + * Write this record into an outgoing message. + */ + abstract void write(DNSOutgoing out) throws IOException; + + /** + * Address record. + */ + static class Address extends DNSRecord + { + private static Logger logger = Logger.getLogger(Address.class.toString()); + InetAddress addr; + + Address(String name, int type, int clazz, int ttl, InetAddress addr) + { + super(name, type, clazz, ttl); + this.addr = addr; + } + + Address(String name, int type, int clazz, int ttl, byte[] rawAddress) + { + super(name, type, clazz, ttl); + try + { + this.addr = InetAddress.getByAddress(rawAddress); + } + catch (UnknownHostException exception) + { + logger.log(Level.WARNING, "Address() exception ", exception); + } + } + + void write(DNSOutgoing out) throws IOException + { + if (addr != null) + { + byte[] buffer = addr.getAddress(); + if (DNSConstants.TYPE_A == type) + { + // If we have a type A records we should answer with a IPv4 address + if (addr instanceof Inet4Address) + { + // All is good + } + else + { + // Get the last four bytes + byte[] tempbuffer = buffer; + buffer = new byte[4]; + System.arraycopy(tempbuffer, 12, buffer, 0, 4); + } + } + else + { + // If we have a type AAAA records we should answer with a IPv6 address + if (addr instanceof Inet4Address) + { + byte[] tempbuffer = buffer; + buffer = new byte[16]; + for (int i = 0; i < 16; i++) + { + if (i < 11) + { + buffer[i] = tempbuffer[i - 12]; + } + else + { + buffer[i] = 0; + } + } + } + } + int length = buffer.length; + out.writeBytes(buffer, 0, length); + } + } + + boolean same(DNSRecord other) + { + return ((sameName(other)) && ((sameValue(other)))); + } + + boolean sameName(DNSRecord other) + { + return name.equalsIgnoreCase(((Address) other).name); + } + + boolean sameValue(DNSRecord other) + { + return addr.equals(((Address) other).getAddress()); + } + + InetAddress getAddress() + { + return addr; + } + + /** + * Creates a byte array representation of this record. + * This is needed for tie-break tests according to + * draft-cheshire-dnsext-multicastdns-04.txt chapter 9.2. + */ + private byte[] toByteArray() + { + try + { + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + DataOutputStream dout = new DataOutputStream(bout); + dout.write(name.getBytes("UTF8")); + dout.writeShort(type); + dout.writeShort(clazz); + //dout.writeInt(len); + byte[] buffer = addr.getAddress(); + for (int i = 0; i < buffer.length; i++) + { + dout.writeByte(buffer[i]); + } + dout.close(); + return bout.toByteArray(); + } + catch (IOException e) + { + throw new InternalError(); + } + } + + /** + * Does a lexicographic comparison of the byte array representation + * of this record and that record. + * This is needed for tie-break tests according to + * draft-cheshire-dnsext-multicastdns-04.txt chapter 9.2. + */ + private int lexCompare(DNSRecord.Address that) + { + byte[] thisBytes = this.toByteArray(); + byte[] thatBytes = that.toByteArray(); + for (int i = 0, n = Math.min(thisBytes.length, thatBytes.length); i < n; i++) + { + if (thisBytes[i] > thatBytes[i]) + { + return 1; + } + else + { + if (thisBytes[i] < thatBytes[i]) + { + return -1; + } + } + } + return thisBytes.length - thatBytes.length; + } + + /** + * Does the necessary actions, when this as a query. + */ + boolean handleQuery(JmDNS dns, long expirationTime) + { + DNSRecord.Address dnsAddress = dns.getLocalHost().getDNSAddressRecord(this); + if (dnsAddress != null) + { + if (dnsAddress.sameType(this) && dnsAddress.sameName(this) && (!dnsAddress.sameValue(this))) + { + logger.finer("handleQuery() Conflicting probe detected. dns state " + dns.getState() + " lex compare " + lexCompare(dnsAddress)); + // Tie-breaker test + if (dns.getState().isProbing() && lexCompare(dnsAddress) >= 0) + { + // We lost the tie-break. We have to choose a different name. + dns.getLocalHost().incrementHostName(); + dns.getCache().clear(); + for (Iterator i = dns.services.values().iterator(); i.hasNext();) + { + ServiceInfo info = (ServiceInfo) i.next(); + info.revertState(); + } + } + dns.revertState(); + return true; + } + } + return false; + } + + /** + * Does the necessary actions, when this as a response. + */ + boolean handleResponse(JmDNS dns) + { + DNSRecord.Address dnsAddress = dns.getLocalHost().getDNSAddressRecord(this); + if (dnsAddress != null) + { + if (dnsAddress.sameType(this) && dnsAddress.sameName(this) && (!dnsAddress.sameValue(this))) + { + logger.finer("handleResponse() Denial detected"); + + if (dns.getState().isProbing()) + { + dns.getLocalHost().incrementHostName(); + dns.getCache().clear(); + for (Iterator i = dns.services.values().iterator(); i.hasNext();) + { + ServiceInfo info = (ServiceInfo) i.next(); + info.revertState(); + } + } + dns.revertState(); + return true; + } + } + return false; + } + + DNSOutgoing addAnswer(JmDNS dns, DNSIncoming in, InetAddress addr, int port, DNSOutgoing out) throws IOException + { + return out; + } + + public String toString() + { + return toString(" address '" + (addr != null ? addr.getHostAddress() : "null") + "'"); + } + + } + + /** + * Pointer record. + */ + static class Pointer extends DNSRecord + { + private static Logger logger = Logger.getLogger(Pointer.class.toString()); + String alias; + + Pointer(String name, int type, int clazz, int ttl, String alias) + { + super(name, type, clazz, ttl); + this.alias = alias; + } + + void write(DNSOutgoing out) throws IOException + { + out.writeName(alias); + } + + boolean sameValue(DNSRecord other) + { + return alias.equals(((Pointer) other).alias); + } + + boolean handleQuery(JmDNS dns, long expirationTime) + { + // Nothing to do (?) + // I think there is no possibility for conflicts for this record type? + return false; + } + + boolean handleResponse(JmDNS dns) + { + // Nothing to do (?) + // I think there is no possibility for conflicts for this record type? + return false; + } + + String getAlias() + { + return alias; + } + + DNSOutgoing addAnswer(JmDNS dns, DNSIncoming in, InetAddress addr, int port, DNSOutgoing out) throws IOException + { + return out; + } + + public String toString() + { + return toString(alias); + } + } + + static class Text extends DNSRecord + { + private static Logger logger = Logger.getLogger(Text.class.toString()); + byte text[]; + + Text(String name, int type, int clazz, int ttl, byte text[]) + { + super(name, type, clazz, ttl); + this.text = text; + } + + void write(DNSOutgoing out) throws IOException + { + out.writeBytes(text, 0, text.length); + } + + boolean sameValue(DNSRecord other) + { + Text txt = (Text) other; + if (txt.text.length != text.length) + { + return false; + } + for (int i = text.length; i-- > 0;) + { + if (txt.text[i] != text[i]) + { + return false; + } + } + return true; + } + + boolean handleQuery(JmDNS dns, long expirationTime) + { + // Nothing to do (?) + // I think there is no possibility for conflicts for this record type? + return false; + } + + boolean handleResponse(JmDNS dns) + { + // Nothing to do (?) + // Shouldn't we care if we get a conflict at this level? + /* + ServiceInfo info = (ServiceInfo) dns.services.get(name.toLowerCase()); + if (info != null) { + if (! Arrays.equals(text,info.text)) { + info.revertState(); + return true; + } + }*/ + return false; + } + + DNSOutgoing addAnswer(JmDNS dns, DNSIncoming in, InetAddress addr, int port, DNSOutgoing out) throws IOException + { + return out; + } + + public String toString() + { + return toString((text.length > 10) ? new String(text, 0, 7) + "..." : new String(text)); + } + } + + /** + * Service record. + */ + static class Service extends DNSRecord + { + private static Logger logger = Logger.getLogger(Service.class.toString()); + int priority; + int weight; + int port; + String server; + + Service(String name, int type, int clazz, int ttl, int priority, int weight, int port, String server) + { + super(name, type, clazz, ttl); + this.priority = priority; + this.weight = weight; + this.port = port; + this.server = server; + } + + void write(DNSOutgoing out) throws IOException + { + out.writeShort(priority); + out.writeShort(weight); + out.writeShort(port); + out.writeName(server); + } + + private byte[] toByteArray() + { + try + { + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + DataOutputStream dout = new DataOutputStream(bout); + dout.write(name.getBytes("UTF8")); + dout.writeShort(type); + dout.writeShort(clazz); + //dout.writeInt(len); + dout.writeShort(priority); + dout.writeShort(weight); + dout.writeShort(port); + dout.write(server.getBytes("UTF8")); + dout.close(); + return bout.toByteArray(); + } + catch (IOException e) + { + throw new InternalError(); + } + } + + private int lexCompare(DNSRecord.Service that) + { + byte[] thisBytes = this.toByteArray(); + byte[] thatBytes = that.toByteArray(); + for (int i = 0, n = Math.min(thisBytes.length, thatBytes.length); i < n; i++) + { + if (thisBytes[i] > thatBytes[i]) + { + return 1; + } + else + { + if (thisBytes[i] < thatBytes[i]) + { + return -1; + } + } + } + return thisBytes.length - thatBytes.length; + } + + boolean sameValue(DNSRecord other) + { + Service s = (Service) other; + return (priority == s.priority) && (weight == s.weight) && (port == s.port) && server.equals(s.server); + } + + boolean handleQuery(JmDNS dns, long expirationTime) + { + ServiceInfo info = (ServiceInfo) dns.services.get(name.toLowerCase()); + if (info != null + && (port != info.port || !server.equalsIgnoreCase(dns.getLocalHost().getName()))) + { + logger.finer("handleQuery() Conflicting probe detected"); + + // Tie breaker test + if (info.getState().isProbing() && lexCompare(new DNSRecord.Service(info.getQualifiedName(), DNSConstants.TYPE_SRV, + DNSConstants.CLASS_IN | DNSConstants.CLASS_UNIQUE, + DNSConstants.DNS_TTL, info.priority, + info.weight, info.port, dns.getLocalHost().getName())) >= 0) + { + // We lost the tie break + String oldName = info.getQualifiedName().toLowerCase(); + info.setName(dns.incrementName(info.getName())); + dns.services.remove(oldName); + dns.services.put(info.getQualifiedName().toLowerCase(), info); + logger.finer("handleQuery() Lost tie break: new unique name chosen:" + info.getName()); + + } + info.revertState(); + return true; + + } + return false; + } + + boolean handleResponse(JmDNS dns) + { + ServiceInfo info = (ServiceInfo) dns.services.get(name.toLowerCase()); + if (info != null + && (port != info.port || !server.equalsIgnoreCase(dns.getLocalHost().getName()))) + { + logger.finer("handleResponse() Denial detected"); + + if (info.getState().isProbing()) + { + String oldName = info.getQualifiedName().toLowerCase(); + info.setName(dns.incrementName(info.getName())); + dns.services.remove(oldName); + dns.services.put(info.getQualifiedName().toLowerCase(), info); + logger.finer("handleResponse() New unique name chose:" + info.getName()); + + } + info.revertState(); + return true; + } + return false; + } + + DNSOutgoing addAnswer(JmDNS dns, DNSIncoming in, InetAddress addr, int port, DNSOutgoing out) throws IOException + { + ServiceInfo info = (ServiceInfo) dns.services.get(name.toLowerCase()); + if (info != null) + { + if (this.port == info.port != server.equals(dns.getLocalHost().getName())) + { + return dns.addAnswer(in, addr, port, out, + new DNSRecord.Service(info.getQualifiedName(), DNSConstants.TYPE_SRV, + DNSConstants.CLASS_IN | DNSConstants.CLASS_UNIQUE, + DNSConstants.DNS_TTL, info.priority, + info.weight, info.port, dns.getLocalHost().getName())); + } + } + return out; + } + + public String toString() + { + return toString(server + ":" + port); + } + } + + public String toString(String other) + { + return toString("record", ttl + "/" + getRemainingTTL(System.currentTimeMillis()) + "," + other); + } +} + Added: trunk/plugins/MDNSDiscovery/javax/jmdns/DNSState.java =================================================================== --- trunk/plugins/MDNSDiscovery/javax/jmdns/DNSState.java (rev 0) +++ trunk/plugins/MDNSDiscovery/javax/jmdns/DNSState.java 2007-03-04 15:23:01 UTC (rev 11954) @@ -0,0 +1,111 @@ +//Copyright 2003-2005 Arthur van Hoff, Rick Blair +//Licensed under Apache License version 2.0 +//Original license LGPL + +package plugins.MDNSDiscovery.javax.jmdns; + +import java.util.ArrayList; +import java.util.logging.Logger; + +/** + * DNSState defines the possible states for services registered with JmDNS. + * + * @author Werner Randelshofer, Rick Blair + * @version 1.0 May 23, 2004 Created. + */ +public class DNSState implements Comparable +{ + private static Logger logger = Logger.getLogger(DNSState.class.toString()); + + private final String name; + + /** + * Ordinal of next state to be created. + */ + private static int nextOrdinal = 0; + /** + * Assign an ordinal to this state. + */ + private final int ordinal = nextOrdinal++; + /** + * Logical sequence of states. + * The sequence is consistent with the ordinal of a state. + * This is used for advancing through states. + */ + private final static ArrayList sequence = new ArrayList(); + + private DNSState(String name) + { + this.name = name; + sequence.add(this); + } + + public final String toString() + { + return name; + } + + public static final DNSState PROBING_1 = new DNSState("probing 1"); + public static final DNSState PROBING_2 = new DNSState("probing 2"); + public static final DNSState PROBING_3 = new DNSState("probing 3"); + public static final DNSState ANNOUNCING_1 = new DNSState("announcing 1"); + public static final DNSState ANNOUNCING_2 = new DNSState("announcing 2"); + public static final DNSState ANNOUNCED = new DNSState("announced"); + public static final DNSState CANCELED = new DNSState("canceled"); + + /** + * Returns the next advanced state. + * In general, this advances one step in the following sequence: PROBING_1, + * PROBING_2, PROBING_3, ANNOUNCING_1, ANNOUNCING_2, ANNOUNCED. + * Does not advance for ANNOUNCED and CANCELED state. + */ + public final DNSState advance() + { + return (isProbing() || isAnnouncing()) ? (DNSState) sequence.get(ordinal + 1) : this; + } + + /** + * Returns to the next reverted state. + * All states except CANCELED revert to PROBING_1. + * Status CANCELED does not revert. + */ + public final DNSState revert() + { + return (this == CANCELED) ? this : PROBING_1; + } + + /** + * Returns true, if this is a probing state. + */ + public boolean isProbing() + { + return compareTo(PROBING_1) >= 0 && compareTo(PROBING_3) <= 0; + } + + /** + * Returns true, if this is an announcing state. + */ + public boolean isAnnouncing() + { + return compareTo(ANNOUNCING_1) >= 0 && compareTo(ANNOUNCING_2) <= 0; + } + + /** + * Returns true, if this is an announced state. + */ + public boolean isAnnounced() + { + return compareTo(ANNOUNCED) == 0; + } + + /** + * Compares two states. + * The states compare as follows: + * PROBING_1 < PROBING_2 < PROBING_3 < ANNOUNCING_1 < + * ANNOUNCING_2 < RESPONDING < ANNOUNCED < CANCELED. + */ + public int compareTo(Object o) + { + return ordinal - ((DNSState) o).ordinal; + } +} \ No newline at end of file Added: trunk/plugins/MDNSDiscovery/javax/jmdns/HostInfo.java =================================================================== --- trunk/plugins/MDNSDiscovery/javax/jmdns/HostInfo.java (rev 0) +++ trunk/plugins/MDNSDiscovery/javax/jmdns/HostInfo.java 2007-03-04 15:23:01 UTC (rev 11954) @@ -0,0 +1,138 @@ +//Copyright 2003-2005 Arthur van Hoff, Rick Blair +//Licensed under Apache License version 2.0 +//Original license LGPL + + + +package plugins.MDNSDiscovery.javax.jmdns; + +import java.net.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * HostInfo information on the local host to be able to cope with change of addresses. + * + * @version %I%, %G% + * @author Pierre Frisch, Werner Randelshofer + */ +class HostInfo +{ + private static Logger logger = Logger.getLogger(HostInfo.class.toString()); + protected String name; + protected InetAddress address; + protected NetworkInterface interfaze; + /** + * This is used to create a unique name for the host name. + */ + private int hostNameCount; + + public HostInfo(InetAddress address, String name) + { + super(); + this.address = address; + this.name = name; + if (address != null) + { + try + { + interfaze = NetworkInterface.getByInetAddress(address); + } + catch (Exception exception) + { + // FIXME Shouldn't we take an action here? + logger.log(Level.WARNING, "LocalHostInfo() exception ", exception); + } + } + } + + public String getName() + { + return name; + } + + public InetAddress getAddress() + { + return address; + } + + public NetworkInterface getInterface() + { + return interfaze; + } + + synchronized String incrementHostName() + { + hostNameCount++; + int plocal = name.indexOf(".local."); + int punder = name.lastIndexOf("-"); + name = name.substring(0, (punder == -1 ? plocal : punder)) + "-" + hostNameCount + ".local."; + return name; + } + + boolean shouldIgnorePacket(DatagramPacket packet) + { + boolean result = false; + if (getAddress() != null) + { + InetAddress from = packet.getAddress(); + if (from != null) + { + if (from.isLinkLocalAddress() && (!getAddress().isLinkLocalAddress())) + { + // Ignore linklocal packets on regular interfaces, unless this is + // also a linklocal interface. This is to avoid duplicates. This is + // a terrible hack caused by the lack of an API to get the address + // of the interface on which the packet was received. + result = true; + } + if (from.isLoopbackAddress() && (!getAddress().isLoopbackAddress())) + { + // Ignore loopback packets on a regular interface unless this is + // also a loopback interface. + result = true; + } + } + } + return result; + } + + DNSRecord.Address getDNSAddressRecord(DNSRecord.Address address) + { + return (DNSConstants.TYPE_AAAA == address.type ? getDNS6AddressRecord() : getDNS4AddressRecord()); + } + + DNSRecord.Address getDNS4AddressRecord() + { + if ((getAddress() != null) && + ((getAddress() instanceof Inet4Address) || + ((getAddress() instanceof Inet6Address) && (((Inet6Address) getAddress()).isIPv4CompatibleAddress())))) + { + return new DNSRecord.Address(getName(), DNSConstants.TYPE_A, DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, getAddress()); + } + return null; + } + + DNSRecord.Address getDNS6AddressRecord() + { + if ((getAddress() != null) && (getAddress() instanceof Inet6Address)) + { + return new DNSRecord.Address(getName(), DNSConstants.TYPE_AAAA, DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, getAddress()); + } + return null; + } + + public String toString() + { + StringBuffer buf = new StringBuffer(); + buf.append("local host info["); + buf.append(getName() != null ? getName() : "no name"); + buf.append(", "); + buf.append(getInterface() != null ? getInterface().getDisplayName() : "???"); + buf.append(":"); + buf.append(getAddress() != null ? getAddress().getHostAddress() : "no address"); + buf.append("]"); + return buf.toString(); + } + +} Added: trunk/plugins/MDNSDiscovery/javax/jmdns/JmDNS.java =================================================================== --- trunk/plugins/MDNSDiscovery/javax/jmdns/JmDNS.java (rev 0) +++ trunk/plugins/MDNSDiscovery/javax/jmdns/JmDNS.java 2007-03-04 15:23:01 UTC (rev 11954) @@ -0,0 +1,2559 @@ +///Copyright 2003-2005 Arthur van Hoff, Rick Blair +//Licensed under Apache License version 2.0 +//Original license LGPL + + +package plugins.MDNSDiscovery.javax.jmdns; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.InetAddress; +import java.net.MulticastSocket; +import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +// REMIND: multiple IP addresses + +/** + * mDNS implementation in Java. + * + * @version %I%, %G% + * @author Arthur van Hoff, Rick Blair, Jeff Sonstein, + * Werner Randelshofer, Pierre Frisch, Scott Lewis + */ +public class JmDNS +{ + private static Logger logger = Logger.getLogger(JmDNS.class.toString()); + /** + * The version of JmDNS. + */ + public static String VERSION = "2.0"; + + /** + * This is the multicast group, we are listening to for multicast DNS messages. + */ + private InetAddress group; + /** + * This is our multicast socket. + */ + private MulticastSocket socket; + + /** + * Used to fix live lock problem on unregester. + */ + + protected boolean closed = false; + + /** + * Holds instances of JmDNS.DNSListener. + * Must by a synchronized collection, because it is updated from + * concurrent threads. + */ + private List listeners; + /** + * Holds instances of ServiceListener's. + * Keys are Strings holding a fully qualified service type. + * Values are LinkedList's of ServiceListener's. + */ + private Map serviceListeners; + /** + * Holds instances of ServiceTypeListener's. + */ + private List typeListeners; + + + /** + * Cache for DNSEntry's. + */ + private DNSCache cache; + + /** + * This hashtable holds the services that have been registered. + * Keys are instances of String which hold an all lower-case version of the + * fully qualified service name. + * Values are instances of ServiceInfo. + */ + Map services; + + /** + * This hashtable holds the service types that have been registered or + * that have been received in an incoming datagram. + * Keys are instances of String which hold an all lower-case version of the + * fully qualified service type. + * Values hold the fully qualified service type. + */ + Map serviceTypes; + /** + * This is the shutdown hook, we registered with the java runtime. + */ + private Thread shutdown; + + /** + * Handle on the local host + */ + HostInfo localHost; + + private Thread incomingListener = null; + + /** + * Throttle count. + * This is used to count the overall number of probes sent by JmDNS. + * When the last throttle increment happened . + */ + private int throttle; + /** + * Last throttle increment. + */ + private long lastThrottleIncrement; + + /** + * The timer is used to dispatch all outgoing messages of JmDNS. + * It is also used to dispatch maintenance tasks for the DNS cache. + */ + private Timer timer; + + /** + * The source for random values. + * This is used to introduce random delays in responses. This reduces the + * potential for collisions on the network. + */ + private final static Random random = new Random(); + + /** + * This lock is used to coordinate processing of incoming and outgoing + * messages. This is needed, because the Rendezvous Conformance Test + * does not forgive race conditions. + */ + private Object ioLock = new Object(); + + /** + * If an incoming package which needs an answer is truncated, we store it + * here. We add more incoming DNSRecords to it, until the JmDNS.Responder + * timer picks it up. + * Remind: This does not work well with multiple planned answers for packages + * that came in from different clients. + */ + private DNSIncoming plannedAnswer; + + // State machine + /** + * The state of JmDNS. + * <p/> + * For proper handling of concurrency, this variable must be + * changed only using methods advanceState(), revertState() and cancel(). + */ + private DNSState state = DNSState.PROBING_1; + + /** + * Timer task associated to the host name. + * This is used to prevent from having multiple tasks associated to the host + * name at the same time. + */ + TimerTask task; + + /** + * This hashtable is used to maintain a list of service types being collected + * by this JmDNS instance. + * The key of the hashtable is a service type name, the value is an instance + * of JmDNS.ServiceCollector. + * + * @see #list + */ + private HashMap serviceCollectors = new HashMap(); + + /** + * Create an instance of JmDNS. + */ + public JmDNS() throws IOException + { + logger.finer("JmDNS instance created"); + try + { + InetAddress addr = InetAddress.getLocalHost(); + init(addr.isLoopbackAddress() ? null : addr, addr.getHostName()); // [PJYF Oct 14 2004] Why do we disallow the loopback address? + } + catch (IOException e) + { + init(null, "computer"); + } + } + + /** + * Create an instance of JmDNS and bind it to a + * specific network interface given its IP-address. + */ + public JmDNS(InetAddress addr) throws IOException + { + try + { + init(addr, addr.getHostName()); + } + catch (IOException e) + { + init(null, "computer"); + } + } + + /** + * Initialize everything. + * + * @param address The interface to which JmDNS binds to. + * @param name The host name of the interface. + */ + private void init(InetAddress address, String name) throws IOException + { + // A host name with "." is illegal. so strip off everything and append .local. + int idx = name.indexOf("."); + if (idx > 0) + { + name = name.substring(0, idx); + } + name += ".local."; + // localHost to IP address binding + localHost = new HostInfo(address, name); + + cache = new DNSCache(100); + + listeners = Collections.synchronizedList(new ArrayList()); + serviceListeners = new HashMap(); + typeListeners = new ArrayList(); + + services = new Hashtable(20); + serviceTypes = new Hashtable(20); + + // REMIND: If I could pass in a name for the Timer thread, + // I would pass 'JmDNS.Timer'. + timer = new Timer(); + new RecordReaper().start(); + shutdown = new Thread(new Shutdown(), "JmDNS.Shutdown"); + Runtime.getRuntime().addShutdownHook(shutdown); + + incomingListener = new Thread(new SocketListener(), "JmDNS.SocketListener"); + + // Bind to multicast socket + openMulticastSocket(localHost); + start(services.values()); + } + + private void start(Collection serviceInfos) + { + state = DNSState.PROBING_1; + incomingListener.start(); + new Prober().start(); + for (Iterator iterator = serviceInfos.iterator(); iterator.hasNext();) + { + try + { + registerService(new ServiceInfo((ServiceInfo) iterator.next())); + } + catch (Exception exception) + { + logger.log(Level.WARNING, "start() Registration exception ", exception); + } + } + } + + private void openMulticastSocket(HostInfo hostInfo) throws IOException + { + if (group == null) + { + group = InetAddress.getByName(DNSConstants.MDNS_GROUP); + } + if (socket != null) + { + this.closeMulticastSocket(); + } + socket = new MulticastSocket(DNSConstants.MDNS_PORT); + if ((hostInfo != null) && (localHost.getInterface() != null)) + { + socket.setNetworkInterface(hostInfo.getInterface()); + } + socket.setTimeToLive(255); + socket.joinGroup(group); + } + + private void closeMulticastSocket() + { + logger.finer("closeMulticastSocket()"); + if (socket != null) + { + // close socket + try + { + socket.leaveGroup(group); + socket.close(); + if (incomingListener != null) + { + incomingListener.join(); + } + } + catch (Exception exception) + { + logger.log(Level.WARNING, "closeMulticastSocket() Close socket exception ", exception); + } + socket = null; + } + } + + // State machine + /** + * Sets the state and notifies all objects that wait on JmDNS. + */ + synchronized void advanceState() + { + state = state.advance(); + notifyAll(); + } + + /** + * Sets the state and notifies all objects that wait on JmDNS. + */ + synchronized void revertState() + { + state = state.revert(); + notifyAll(); + } + + /** + * Sets the state and notifies all objects that wait on JmDNS. + */ + synchronized void cancel() + { + state = DNSState.CANCELED; + notifyAll(); + } + + /** + * Returns the current state of this info. + */ + DNSState getState() + { + return state; + } + + + /** + * Return the DNSCache associated with the cache variable + */ + DNSCache getCache() + { + return cache; + } + + /** + * Return the HostName associated with this JmDNS instance. + * Note: May not be the same as what started. The host name is subject to + * negotiation. + */ + public String getHostName() + { + return localHost.getName(); + } + + public HostInfo getLocalHost() + { + return localHost; + } + + /** + * Return the address of the interface to which this instance of JmDNS is + * bound. + */ + public InetAddress getInterface() throws IOException + { + return socket.getInterface(); + } + + /** + * Get service information. If the information is not cached, the method + * will block until updated information is received. + * <p/> + * Usage note: Do not call this method from the AWT event dispatcher thread. + * You will make the user interface unresponsive. + * + * @param type fully qualified service type, such as <code>_http._tcp.local.</code> . + * @param name unqualified service name, such as <code>foobar</code> . + * @return null if the service information cannot be obtained + */ + public ServiceInfo getServiceInfo(String type, String name) + { + return getServiceInfo(type, name, 3 * 1000); + } + + /** + * Get service information. If the information is not cached, the method + * will block for the given timeout until updated information is received. + * <p/> + * Usage note: If you call this method from the AWT event dispatcher thread, + * use a small timeout, or you will make the user interface unresponsive. + * + * @param type full qualified service type, such as <code>_http._tcp.local.</code> . + * @param name unqualified service name, such as <code>foobar</code> . + * @param timeout timeout in milliseconds + * @return null if the service information cannot be obtained + */ + public ServiceInfo getServiceInfo(String type, String name, int timeout) + { + ServiceInfo info = new ServiceInfo(type, name); + new ServiceInfoResolver(info).start(); + + try + { + long end = System.currentTimeMillis() + timeout; + long delay; + synchronized (info) + { + while (!info.hasData() && (delay = end - System.currentTimeMillis()) > 0) + { + info.wait(delay); + } + } + } + catch (InterruptedException e) + { + // empty + } + + return (info.hasData()) ? info : null; + } + + /** + * Request service information. The information about the service is + * requested and the ServiceListener.resolveService method is called as soon + * as it is available. + * <p/> + * Usage note: Do not call this method from the AWT event dispatcher thread. + * You will make the user interface unresponsive. + * + * @param type full qualified service type, such as <code>_http._tcp.local.</code> . + * @param name unqualified service name, such as <code>foobar</code> . + */ + public void requestServiceInfo(String type, String name) + { + requestServiceInfo(type, name, 3 * 1000); + } + + /** + * Request service information. The information about the service is requested + * and the ServiceListener.resolveService method is called as soon as it is available. + * + * @param type full qualified service type, such as <code>_http._tcp.local.</code> . + * @param name unqualified service name, such as <code>foobar</code> . + * @param timeout timeout in milliseconds + */ + public void requestServiceInfo(String type, String name, int timeout) + { + registerServiceType(type); + ServiceInfo info = new ServiceInfo(type, name); + new ServiceInfoResolver(info).start(); + + try + { + long end = System.currentTimeMillis() + timeout; + long delay; + synchronized (info) + { + while (!info.hasData() && (delay = end - System.currentTimeMillis()) > 0) + { + info.wait(delay); + } + } + } + catch (InterruptedException e) + { + // empty + } + } + + void handleServiceResolved(ServiceInfo info) + { + List list = (List) serviceListeners.get(info.type.toLowerCase()); + if (list != null) + { + ServiceEvent event = new ServiceEvent(this, info.type, info.getName(), info); + // Iterate on a copy in case listeners will modify it + final ArrayList listCopy = new ArrayList(list); + for (Iterator iterator = listCopy.iterator(); iterator.hasNext();) + { + ((ServiceListener) iterator.next()).serviceResolved(event); + } + } + } + + /** + * Listen for service types. + * + * @param listener listener for service types + */ + public void addServiceTypeListener(ServiceTypeListener listener) throws IOException + { + synchronized (this) + { + typeListeners.remove(listener); + typeListeners.add(listener); + } + + // report cached service types + for (Iterator iterator = serviceTypes.values().iterator(); iterator.hasNext();) + { + listener.serviceTypeAdded(new ServiceEvent(this, (String) iterator.next(), null, null)); + } + + new TypeResolver().start(); + } + + /** + * Remove listener for service types. + * + * @param listener listener for service types + */ + public void removeServiceTypeListener(ServiceTypeListener listener) + { + synchronized (this) + { + typeListeners.remove(listener); + } + } + + /** + * Listen for services of a given type. The type has to be a fully qualified + * type name such as <code>_http._tcp.local.</code>. + * + * @param type full qualified service type, such as <code>_http._tcp.local.</code>. + * @param listener listener for service updates + */ + public void addServiceListener(String type, ServiceListener listener) + { + String lotype = type.toLowerCase(); + removeServiceListener(lotype, listener); + List list = null; + synchronized (this) + { + list = (List) serviceListeners.get(lotype); + if (list == null) + { + list = Collections.synchronizedList(new LinkedList()); + serviceListeners.put(lotype, list); + } + list.add(listener); + } + + // report cached service types + for (Iterator i = cache.iterator(); i.hasNext();) + { + for (DNSCache.CacheNode n = (DNSCache.CacheNode) i.next(); n != null; n = n.next()) + { + DNSRecord rec = (DNSRecord) n.getValue(); + if (rec.type == DNSConstants.TYPE_SRV) + { + if (rec.name.endsWith(type)) + { + listener.serviceAdded(new ServiceEvent(this, type, toUnqualifiedName(type, rec.name), null)); + } + } + } + } + new ServiceResolver(type).start(); + } + + /** + * Remove listener for services of a given type. + * + * @param listener listener for service updates + */ + public void removeServiceListener(String type, ServiceListener listener) + { + type = type.toLowerCase(); + List list = (List) serviceListeners.get(type); + if (list != null) + { + synchronized (this) + { + list.remove(listener); + if (list.size() == 0) + { + serviceListeners.remove(type); + } + } + } + } + + /** + * Register a service. The service is registered for access by other jmdns clients. + * The name of the service may be changed to make it unique. + */ + public void registerService(ServiceInfo info) throws IOException + { + registerServiceType(info.type); + + // bind the service to this address + info.server = localHost.getName(); + info.addr = localHost.getAddress(); + + synchronized (this) + { + makeServiceNameUnique(info); + services.put(info.getQualifiedName().toLowerCase(), info); + } + + new /*Service*/Prober().start(); + try + { + synchronized (info) + { + while (info.getState().compareTo(DNSState.ANNOUNCED) < 0) + { + info.wait(); + } + } + } + catch (InterruptedException e) + { + //empty + } + logger.fine("registerService() JmDNS registered service as " + info); + } + + /** + * Unregister a service. The service should have been registered. + */ + public void unregisterService(ServiceInfo info) + { + synchronized (this) + { + services.remove(info.getQualifiedName().toLowerCase()); + } + info.cancel(); + + // Note: We use this lock object to synchronize on it. + // Synchronizing on another object (e.g. the ServiceInfo) does + // not make sense, because the sole purpose of the lock is to + // wait until the canceler has finished. If we synchronized on + // the ServiceInfo or on the Canceler, we would block all + // accesses to synchronized methods on that object. This is not + // what we want! + Object lock = new Object(); + new Canceler(info, lock).start(); + + // Remind: We get a deadlock here, if the Canceler does not run! + try + { + synchronized (lock) + { + lock.wait(); + } + } + catch (InterruptedException e) + { + // empty + } + } + + /** + * Unregister all services. + */ + public void unregisterAllServices() + { + logger.finer("unregisterAllServices()"); + if (services.size() == 0) + { + return; + } + + Collection list; + synchronized (this) + { + list = new LinkedList(services.values()); + services.clear(); + } + for (Iterator iterator = list.iterator(); iterator.hasNext();) + { + ((ServiceInfo) iterator.next()).cancel(); + } + + + Object lock = new Object(); + new Canceler(list, lock).start(); + // Remind: We get a livelock here, if the Canceler does not run! + try { + synchronized (lock) { + if (!closed) { + lock.wait(); + } + } + } catch (InterruptedException e) { + // empty + } + + + } + + /** + * Register a service type. If this service type was not already known, + * all service listeners will be notified of the new service type. Service types + * are automatically registered as they are discovered. + */ + public void registerServiceType(String type) + { + String name = type.toLowerCase(); + if (serviceTypes.get(name) == null) + { + if ((type.indexOf("._mdns._udp.") < 0) && !type.endsWith(".in-addr.arpa.")) + { + Collection list; + synchronized (this) + { + serviceTypes.put(name, type); + list = new LinkedList(typeListeners); + } + for (Iterator iterator = list.iterator(); iterator.hasNext();) + { + ((ServiceTypeListener) iterator.next()).serviceTypeAdded(new ServiceEvent(this, type, null, null)); + } + } + } + } + + /** + * Generate a possibly unique name for a host using the information we + * have in the cache. + * + * @return returns true, if the name of the host had to be changed. + */ + private boolean makeHostNameUnique(DNSRecord.Address host) + { + String originalName = host.getName(); + long now = System.currentTimeMillis(); + + boolean collision; + do + { + collision = false; + + // Check for collision in cache + for (DNSCache.CacheNode j = cache.find(host.getName().toLowerCase()); j != null; j = j.next()) + { + DNSRecord a = (DNSRecord) j.getValue(); + if (false) + { + host.name = incrementName(host.getName()); + collision = true; + break; + } + } + } + while (collision); + + if (originalName.equals(host.getName())) + { + return false; + } + else + { + return true; + } + } + + /** + * Generate a possibly unique name for a service using the information we + * have in the cache. + * + * @return returns true, if the name of the service info had to be changed. + */ + private boolean makeServiceNameUnique(ServiceInfo info) + { + String originalQualifiedName = info.getQualifiedName(); + long now = System.currentTimeMillis(); + + boolean collision; + do + { + collision = false; + + // Check for collision in cache + for (DNSCache.CacheNode j = cache.find(info.getQualifiedName().toLowerCase()); j != null; j = j.next()) + { + DNSRecord a = (DNSRecord) j.getValue(); + if ((a.type == DNSConstants.TYPE_SRV) && !a.isExpired(now)) + { + DNSRecord.Service s = (DNSRecord.Service) a; + if (s.port != info.port || !s.server.equals(localHost.getName())) + { + logger.finer("makeServiceNameUnique() JmDNS.makeServiceNameUnique srv collision:" + a + " s.server=" + s.server + " " + localHost.getName() + " equals:" + (s.server.equals(localHost.getName()))); + info.setName(incrementName(info.getName())); + collision = true; + break; + } + } + } + + // Check for collision with other service infos published by JmDNS + Object selfService = services.get(info.getQualifiedName().toLowerCase()); + if (selfService != null && selfService != info) + { + info.setName(incrementName(info.getName())); + collision = true; + } + } + while (collision); + + return !(originalQualifiedName.equals(info.getQualifiedName())); + } + + String incrementName(String name) + { + try + { + int l = name.lastIndexOf('('); + int r = name.lastIndexOf(')'); + if ((l >= 0) && (l < r)) + { + name = name.substring(0, l) + "(" + (Integer.parseInt(name.substring(l + 1, r)) + 1) + ")"; + } + else + { + name += " (2)"; + } + } + catch (NumberFormatException e) + { + name += " (2)"; + } + return name; + } + + /** + * Add a listener for a question. The listener will receive updates + * of answers to the question as they arrive, or from the cache if they + * are already available. + */ + void addListener(DNSListener listener, DNSQuestion question) + { + long now = System.currentTimeMillis(); + + // add the new listener + synchronized (this) + { + listeners.add(listener); + } + + // report existing matched records + if (question != null) + { + for (DNSCache.CacheNode i = cache.find(question.name); i != null; i = i.next()) + { + DNSRecord c = (DNSRecord) i.getValue(); + if (question.answeredBy(c) && !c.isExpired(now)) + { + listener.updateRecord(this, now, c); + } + } + } + } + + /** + * Remove a listener from all outstanding questions. The listener will no longer + * receive any updates. + */ + void removeListener(DNSListener listener) + { + synchronized (this) + { + listeners.remove(listener); + } + } + + + // Remind: Method updateRecord should receive a better name. + /** + * Notify all listeners that a record was updated. + */ + void updateRecord(long now, DNSRecord rec) + { + // We do not want to block the entire DNS while we are updating the record for each listener (service info) + List listenerList = null; + synchronized (this) + { + listenerList = new ArrayList(listeners); + } + for (Iterator iterator = listenerList.iterator(); iterator.hasNext();) + { + DNSListener listener = (DNSListener) iterator.next(); + listener.updateRecord(this, now, rec); + } + if (rec.type == DNSConstants.TYPE_PTR || rec.type == DNSConstants.TYPE_SRV) + { + List serviceListenerList = null; + synchronized (this) + { + serviceListenerList = (List) serviceListeners.get(rec.name.toLowerCase()); + // Iterate on a copy in case listeners will modify it + if (serviceListenerList != null) + { + serviceListenerList = new ArrayList(serviceListenerList); + } + } + if (serviceListenerList != null) + { + boolean expired = rec.isExpired(now); + String type = rec.getName(); + String name = ((DNSRecord.Pointer) rec).getAlias(); + // DNSRecord old = (DNSRecord)services.get(name.toLowerCase()); + if (!expired) + { + // new record + ServiceEvent event = new ServiceEvent(this, type, toUnqualifiedName(type, name), null); + for (Iterator iterator = serviceListenerList.iterator(); iterator.hasNext();) + { + ((ServiceListener) iterator.next()).serviceAdded(event); + } + } + else + { + // expire record + ServiceEvent event = new ServiceEvent(this, type, toUnqualifiedName(type, name), null); + for (Iterator iterator = serviceListenerList.iterator(); iterator.hasNext();) + { + ((ServiceListener) iterator.next()).serviceRemoved(event); + } + } + } + } + } + + /** + * Handle an incoming response. Cache answers, and pass them on to + * the appropriate questions. + */ + private void handleResponse(DNSIncoming msg) throws IOException + { + long now = System.currentTimeMillis(); + + boolean hostConflictDetected = false; + boolean serviceConflictDetected = false; + + for (Iterator i = msg.answers.iterator(); i.hasNext();) + { + boolean isInformative = false; + DNSRecord rec = (DNSRecord) i.next(); + boolean expired = rec.isExpired(now); + + // update the cache + DNSRecord c = (DNSRecord) cache.get(rec); + if (c != null) + { + if (expired) + { + isInformative = true; + cache.remove(c); + } + else + { + c.resetTTL(rec); + rec = c; + } + } + else + { + if (!expired) + { + isInformative = true; + cache.add(rec); + } + } + switch (rec.type) + { + case DNSConstants.TYPE_PTR: + // handle _mdns._udp records + if (rec.getName().indexOf("._mdns._udp.") >= 0) + { + if (!expired && rec.name.startsWith("_services._mdns._udp.")) + { + isInformative = true; + registerServiceType(((DNSRecord.Pointer) rec).alias); + } + continue; + } + registerServiceType(rec.name); + break; + } + + if ((rec.getType() == DNSConstants.TYPE_A) || (rec.getType() == DNSConstants.TYPE_AAAA)) + { + hostConflictDetected |= rec.handleResponse(this); + } + else + { + serviceConflictDetected |= rec.handleResponse(this); + } + + // notify the listeners + if (isInformative) + { + updateRecord(now, rec); + } + } + + if (hostConflictDetected || serviceConflictDetected) + { + new Prober().start(); + } + } + + /** + * Handle an incoming query. See if we can answer any part of it + * given our service infos. + */ + private void handleQuery(DNSIncoming in, InetAddress addr, int port) throws IOException + { + // Track known answers + boolean hostConflictDetected = false; + boolean serviceConflictDetected = false; + long expirationTime = System.currentTimeMillis() + DNSConstants.KNOWN_ANSWER_TTL; + for (Iterator i = in.answers.iterator(); i.hasNext();) + { + DNSRecord answer = (DNSRecord) i.next(); + if ((answer.getType() == DNSConstants.TYPE_A) || (answer.getType() == DNSConstants.TYPE_AAAA)) + { + hostConflictDetected |= answer.handleQuery(this, expirationTime); + } + else + { + serviceConflictDetected |= answer.handleQuery(this, expirationTime); + } + } + + if (plannedAnswer != null) + { + plannedAnswer.append(in); + } + else + { + if (in.isTruncated()) + { + plannedAnswer = in; + } + + new Responder(in, addr, port).start(); + } + + if (hostConflictDetected || serviceConflictDetected) + { + new Prober().start(); + } + } + + /** + * Add an answer to a question. Deal with the case when the + * outgoing packet overflows + */ + DNSOutgoing addAnswer(DNSIncoming in, InetAddress addr, int port, DNSOutgoing out, DNSRecord rec) throws IOException + { + if (out == null) + { + out = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA); + } + try + { + out.addAnswer(in, rec); + } + catch (IOException e) + { + out.flags |= DNSConstants.FLAGS_TC; + out.id = in.id; + out.finish(); + send(out); + + out = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA); + out.addAnswer(in, rec); + } + return out; + } + + + /** + * Send an outgoing multicast DNS message. + */ + private void send(DNSOutgoing out) throws IOException + { + out.finish(); + if (!out.isEmpty()) + { + DatagramPacket packet = new DatagramPacket(out.data, out.off, group, DNSConstants.MDNS_PORT); + + try + { + DNSIncoming msg = new DNSIncoming(packet); + logger.finest("send() JmDNS out:" + msg.print(true)); + } + catch (IOException e) + { + logger.throwing(getClass().toString(), "send(DNSOutgoing) - JmDNS can not parse what it sends!!!", e); + } + socket.send(packet); + } + } + + /** + * Listen for multicast packets. + */ + class SocketListener implements Runnable + { + public void run() + { + try + { + byte buf[] = new byte[DNSConstants.MAX_MSG_ABSOLUTE]; + DatagramPacket packet = new DatagramPacket(buf, buf.length); + while (state != DNSState.CANCELED) + { + packet.setLength(buf.length); + socket.receive(packet); + if (state == DNSState.CANCELED) + { + break; + } + try + { + if (localHost.shouldIgnorePacket(packet)) + { + continue; + } + + DNSIncoming msg = new DNSIncoming(packet); + logger.finest("SocketListener.run() JmDNS in:" + msg.print(true)); + + synchronized (ioLock) + { + if (msg.isQuery()) + { + if (packet.getPort() != DNSConstants.MDNS_PORT) + { + handleQuery(msg, packet.getAddress(), packet.getPort()); + } + handleQuery(msg, group, DNSConstants.MDNS_PORT); + } + else + { + handleResponse(msg); + } + } + } + catch (IOException e) + { + logger.log(Level.WARNING, "run() exception ", e); + } + } + } + catch (IOException e) + { + if (state != DNSState.CANCELED) + { + logger.log(Level.WARNING, "run() exception ", e); + recover(); + } + } + } + } + + + /** + * Periodicaly removes expired entries from the cache. + */ + private class RecordReaper extends TimerTask + { + public void start() + { + timer.schedule(this, DNSConstants.RECORD_REAPER_INTERVAL, DNSConstants.RECORD_REAPER_INTERVAL); + } + + public void run() + { + synchronized (JmDNS.this) + { + if (state == DNSState.CANCELED) + { + return; + } + logger.finest("run() JmDNS reaping cache"); + + // Remove expired answers from the cache + // ------------------------------------- + // To prevent race conditions, we defensively copy all cache + // entries into a list. + List list = new ArrayList(); + synchronized (cache) + { + for (Iterator i = cache.iterator(); i.hasNext();) + { + for (DNSCache.CacheNode n = (DNSCache.CacheNode) i.next(); n != null; n = n.next()) + { + list.add(n.getValue()); + } + } + } + // Now, we remove them. + long now = System.currentTimeMillis(); + for (Iterator i = list.iterator(); i.hasNext();) + { + DNSRecord c = (DNSRecord) i.next(); + if (c.isExpired(now)) + { + updateRecord(now, c); + cache.remove(c); + } + } + } + } + } + + + /** + * The Prober sends three consecutive probes for all service infos + * that needs probing as well as for the host name. + * The state of each service info of the host name is advanced, when a probe has + * been sent for it. + * When the prober has run three times, it launches an Announcer. + * <p/> + * If a conflict during probes occurs, the affected service infos (and affected + * host name) are taken away from the prober. This eventually causes the prober + * tho cancel itself. + */ + private class Prober extends TimerTask + { + /** + * The state of the prober. + */ + DNSState taskState = DNSState.PROBING_1; + + public Prober() + { + // Associate the host name to this, if it needs probing + if (state == DNSState.PROBING_1) + { + task = this; + } + // Associate services to this, if they need probing + synchronized (JmDNS.this) + { + for (Iterator iterator = services.values().iterator(); iterator.hasNext();) + { + ServiceInfo info = (ServiceInfo) iterator.next(); + if (info.getState() == DNSState.PROBING_1) + { + info.task = this; + } + } + } + } + + + public void start() + { + long now = System.currentTimeMillis(); + if (now - lastThrottleIncrement < DNSConstants.PROBE_THROTTLE_COUNT_INTERVAL) + { + throttle++; + } + else + { + throttle = 1; + } + lastThrottleIncrement = now; + + if (state == DNSState.ANNOUNCED && throttle < DNSConstants.PROBE_THROTTLE_COUNT) + { + timer.schedule(this, random.nextInt(1 + DNSConstants.PROBE_WAIT_INTERVAL), DNSConstants.PROBE_WAIT_INTERVAL); + } + else + { + timer.schedule(this, DNSConstants.PROBE_CONFLICT_INTERVAL, DNSConstants.PROBE_CONFLICT_INTERVAL); + } + } + + public boolean cancel() + { + // Remove association from host name to this + if (task == this) + { + task = null; + } + + // Remove associations from services to this + synchronized (JmDNS.this) + { + for (Iterator i = services.values().iterator(); i.hasNext();) + { + ServiceInfo info = (ServiceInfo) i.next(); + if (info.task == this) + { + info.task = null; + } + } + } + + return super.cancel(); + } + + public void run() + { + synchronized (ioLock) + { + DNSOutgoing out = null; + try + { + // send probes for JmDNS itself + if (state == taskState && task == this) + { + if (out == null) + { + out = new DNSOutgoing(DNSConstants.FLAGS_QR_QUERY); + } + out.addQuestion(new DNSQuestion(localHost.getName(), DNSConstants.TYPE_ANY, DNSConstants.CLASS_IN)); + DNSRecord answer = localHost.getDNS4AddressRecord(); + if (answer != null) + { + out.addAuthorativeAnswer(answer); + } + answer = localHost.getDNS6AddressRecord(); + if (answer != null) + { + out.addAuthorativeAnswer(answer); + } + advanceState(); + } + // send probes for services + // Defensively copy the services into a local list, + // to prevent race conditions with methods registerService + // and unregisterService. + List list; + synchronized (JmDNS.this) + { + list = new LinkedList(services.values()); + } + for (Iterator i = list.iterator(); i.hasNext();) + { + ServiceInfo info = (ServiceInfo) i.next(); + + synchronized (info) + { + if (info.getState() == taskState && info.task == this) + { + info.advanceState(); + logger.fine("run() JmDNS probing " + info.getQualifiedName() + " state " + info.getState()); + if (out == null) + { + out = new DNSOutgoing(DNSConstants.FLAGS_QR_QUERY); + out.addQuestion(new DNSQuestion(info.getQualifiedName(), DNSConstants.TYPE_ANY, DNSConstants.CLASS_IN)); + } + out.addAuthorativeAnswer(new DNSRecord.Service(info.getQualifiedName(), DNSConstants.TYPE_SRV, DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, info.priority, info.weight, info.port, localHost.getName())); + } + } + } + if (out != null) + { + logger.finer("run() JmDNS probing #" + taskState); + send(out); + } + else + { + // If we have nothing to send, another timer taskState ahead + // of us has done the job for us. We can cancel. + cancel(); + return; + } + } + catch (Throwable e) + { + logger.log(Level.WARNING, "run() exception ", e); + recover(); + } + + taskState = taskState.advance(); + if (!taskState.isProbing()) + { + cancel(); + + new Announcer().start(); + } + } + } + + } + + /** + * The Announcer sends an accumulated query of all announces, and advances + * the state of all serviceInfos, for which it has sent an announce. + * The Announcer also sends announcements and advances the state of JmDNS itself. + * <p/> + * When the announcer has run two times, it finishes. + */ + private class Announcer extends TimerTask + { + /** + * The state of the announcer. + */ + DNSState taskState = DNSState.ANNOUNCING_1; + + public Announcer() + { + // Associate host to this, if it needs announcing + if (state == DNSState.ANNOUNCING_1) + { + task = this; + } + // Associate services to this, if they need announcing + synchronized (JmDNS.this) + { + for (Iterator s = services.values().iterator(); s.hasNext();) + { + ServiceInfo info = (ServiceInfo) s.next(); + if (info.getState() == DNSState.ANNOUNCING_1) + { + info.task = this; + } + } + } + } + + public void start() + { + timer.schedule(this, DNSConstants.ANNOUNCE_WAIT_INTERVAL, DNSConstants.ANNOUNCE_WAIT_INTERVAL); + } + + public boolean cancel() + { + // Remove association from host to this + if (task == this) + { + task = null; + } + + // Remove associations from services to this + synchronized (JmDNS.this) + { + for (Iterator i = services.values().iterator(); i.hasNext();) + { + ServiceInfo info = (ServiceInfo) i.next(); + if (info.task == this) + { + info.task = null; + } + } + } + + return super.cancel(); + } + + public void run() + { + DNSOutgoing out = null; + try + { + // send probes for JmDNS itself + if (state == taskState) + { + if (out == null) + { + out = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA); + } + DNSRecord answer = localHost.getDNS4AddressRecord(); + if (answer != null) + { + out.addAnswer(answer, 0); + } + answer = localHost.getDNS6AddressRecord(); + if (answer != null) + { + out.addAnswer(answer, 0); + } + advanceState(); + } + // send announces for services + // Defensively copy the services into a local list, + // to prevent race conditions with methods registerService + // and unregisterService. + List list; + synchronized (JmDNS.this) + { + list = new ArrayList(services.values()); + } + for (Iterator i = list.iterator(); i.hasNext();) + { + ServiceInfo info = (ServiceInfo) i.next(); + synchronized (info) + { + if (info.getState() == taskState && info.task == this) + { + info.advanceState(); + logger.finer("run() JmDNS announcing " + info.getQualifiedName() + " state " + info.getState()); + if (out == null) + { + out = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA); + } + out.addAnswer(new DNSRecord.Pointer(info.type, DNSConstants.TYPE_PTR, DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, info.getQualifiedName()), 0); + out.addAnswer(new DNSRecord.Service(info.getQualifiedName(), DNSConstants.TYPE_SRV, DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, info.priority, info.weight, info.port, localHost.getName()), 0); + out.addAnswer(new DNSRecord.Text(info.getQualifiedName(), DNSConstants.TYPE_TXT, DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, info.text), 0); + } + } + } + if (out != null) + { + logger.finer("run() JmDNS announcing #" + taskState); + send(out); + } + else + { + // If we have nothing to send, another timer taskState ahead + // of us has done the job for us. We can cancel. + cancel(); + } + } + catch (Throwable e) + { + logger.log(Level.WARNING, "run() exception ", e); + recover(); + } + + taskState = taskState.advance(); + if (!taskState.isAnnouncing()) + { + cancel(); + + new Renewer().start(); + } + } + } + + /** + * The Renewer is there to send renewal announcment when the record expire for ours infos. + */ + private class Renewer extends TimerTask + { + /** + * The state of the announcer. + */ + DNSState taskState = DNSState.ANNOUNCED; + + public Renewer() + { + // Associate host to this, if it needs renewal + if (state == DNSState.ANNOUNCED) + { + task = this; + } + // Associate services to this, if they need renewal + synchronized (JmDNS.this) + { + for (Iterator s = services.values().iterator(); s.hasNext();) + { + ServiceInfo info = (ServiceInfo) s.next(); + if (info.getState() == DNSState.ANNOUNCED) + { + info.task = this; + } + } + } + } + + public void start() + { + timer.schedule(this, DNSConstants.ANNOUNCED_RENEWAL_TTL_INTERVAL, DNSConstants.ANNOUNCED_RENEWAL_TTL_INTERVAL); + } + + public boolean cancel() + { + // Remove association from host to this + if (task == this) + { + task = null; + } + + // Remove associations from services to this + synchronized (JmDNS.this) + { + for (Iterator i = services.values().iterator(); i.hasNext();) + { + ServiceInfo info = (ServiceInfo) i.next(); + if (info.task == this) + { + info.task = null; + } + } + } + + return super.cancel(); + } + + public void run() + { + DNSOutgoing out = null; + try + { + // send probes for JmDNS itself + if (state == taskState) + { + if (out == null) + { + out = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA); + } + DNSRecord answer = localHost.getDNS4AddressRecord(); + if (answer != null) + { + out.addAnswer(answer, 0); + } + answer = localHost.getDNS6AddressRecord(); + if (answer != null) + { + out.addAnswer(answer, 0); + } + advanceState(); + } + // send announces for services + // Defensively copy the services into a local list, + // to prevent race conditions with methods registerService + // and unregisterService. + List list; + synchronized (JmDNS.this) + { + list = new ArrayList(services.values()); + } + for (Iterator i = list.iterator(); i.hasNext();) + { + ServiceInfo info = (ServiceInfo) i.next(); + synchronized (info) + { + if (info.getState() == taskState && info.task == this) + { + info.advanceState(); + logger.finer("run() JmDNS announced " + info.getQualifiedName() + " state " + info.getState()); + if (out == null) + { + out = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA); + } + out.addAnswer(new DNSRecord.Pointer(info.type, DNSConstants.TYPE_PTR, DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, info.getQualifiedName()), 0); + out.addAnswer(new DNSRecord.Service(info.getQualifiedName(), DNSConstants.TYPE_SRV, DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, info.priority, info.weight, info.port, localHost.getName()), 0); + out.addAnswer(new DNSRecord.Text(info.getQualifiedName(), DNSConstants.TYPE_TXT, DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, info.text), 0); + } + } + } + if (out != null) + { + logger.finer("run() JmDNS announced"); + send(out); + } + else + { + // If we have nothing to send, another timer taskState ahead + // of us has done the job for us. We can cancel. + cancel(); + } + } + catch (Throwable e) + { + logger.log(Level.WARNING, "run() exception ", e); + recover(); + } + + taskState = taskState.advance(); + if (!taskState.isAnnounced()) + { + cancel(); + + } + } + } + + /** + * The Responder sends a single answer for the specified service infos + * and for the host name. + */ + private class Responder extends TimerTask + { + private DNSIncoming in; + private InetAddress addr; + private int port; + + public Responder(DNSIncoming in, InetAddress addr, int port) + { + this.in = in; + this.addr = addr; + this.port = port; + } + + public void start() + { + // According to draft-cheshire-dnsext-multicastdns.txt + // chapter "8 Responding": + // We respond immediately if we know for sure, that we are + // the only one who can respond to the query. + // In all other cases, we respond within 20-120 ms. + // + // According to draft-cheshire-dnsext-multicastdns.txt + // chapter "7.2 Multi-Packet Known Answer Suppression": + // We respond after 20-120 ms if the query is truncated. + + boolean iAmTheOnlyOne = true; + for (Iterator i = in.questions.iterator(); i.hasNext();) + { + DNSEntry entry = (DNSEntry) i.next(); + if (entry instanceof DNSQuestion) + { + DNSQuestion q = (DNSQuestion) entry; + logger.finest("start() question=" + q); + iAmTheOnlyOne &= (q.type == DNSConstants.TYPE_SRV + || q.type == DNSConstants.TYPE_TXT + || q.type == DNSConstants.TYPE_A + || q.type == DNSConstants.TYPE_AAAA + || localHost.getName().equalsIgnoreCase(q.name) + || services.containsKey(q.name.toLowerCase())); + if (!iAmTheOnlyOne) + { + break; + } + } + } + int delay = (iAmTheOnlyOne && !in.isTruncated()) ? 0 : DNSConstants.RESPONSE_MIN_WAIT_INTERVAL + random.nextInt(DNSConstants.RESPONSE_MAX_WAIT_INTERVAL - DNSConstants.RESPONSE_MIN_WAIT_INTERVAL + 1) - in.elapseSinceArrival(); + if (delay < 0) + { + delay = 0; + } + logger.finest("start() Responder chosen delay=" + delay); + timer.schedule(this, delay); + } + + public void run() + { + synchronized (ioLock) + { + if (plannedAnswer == in) + { + plannedAnswer = null; + } + + // We use these sets to prevent duplicate records + // FIXME - This should be moved into DNSOutgoing + HashSet questions = new HashSet(); + HashSet answers = new HashSet(); + + + if (state == DNSState.ANNOUNCED) + { + try + { + long now = System.currentTimeMillis(); + long expirationTime = now + 1; //=now+DNSConstants.KNOWN_ANSWER_TTL; + boolean isUnicast = (port != DNSConstants.MDNS_PORT); + + + // Answer questions + for (Iterator iterator = in.questions.iterator(); iterator.hasNext();) + { + DNSEntry entry = (DNSEntry) iterator.next(); + if (entry instanceof DNSQuestion) + { + DNSQuestion q = (DNSQuestion) entry; + + // for unicast responses the question must be included + if (isUnicast) + { + //out.addQuestion(q); + questions.add(q); + } + + int type = q.type; + if (type == DNSConstants.TYPE_ANY || type == DNSConstants.TYPE_SRV) + { // I ama not sure of why there is a special case here [PJYF Oct 15 2004] + if (localHost.getName().equalsIgnoreCase(q.getName())) + { + // type = DNSConstants.TYPE_A; + DNSRecord answer = localHost.getDNS4AddressRecord(); + if (answer != null) + { + answers.add(answer); + } + answer = localHost.getDNS6AddressRecord(); + if (answer != null) + { + answers.add(answer); + } + type = DNSConstants.TYPE_IGNORE; + } + else + { + if (serviceTypes.containsKey(q.getName().toLowerCase())) + { + type = DNSConstants.TYPE_PTR; + } + } + } + + switch (type) + { + case DNSConstants.TYPE_A: + { + // Answer a query for a domain name + //out = addAnswer( in, addr, port, out, host ); + DNSRecord answer = localHost.getDNS4AddressRecord(); + if (answer != null) + { + answers.add(answer); + } + break; + } + case DNSConstants.TYPE_AAAA: + { + // Answer a query for a domain name + DNSRecord answer = localHost.getDNS6AddressRecord(); + if (answer != null) + { + answers.add(answer); + } + break; + } + case DNSConstants.TYPE_PTR: + { + // Answer a query for services of a given type + + // find matching services + for (Iterator serviceIterator = services.values().iterator(); serviceIterator.hasNext();) + { + ServiceInfo info = (ServiceInfo) serviceIterator.next(); + if (info.getState() == DNSState.ANNOUNCED) + { + if (q.name.equalsIgnoreCase(info.type)) + { + DNSRecord answer = localHost.getDNS4AddressRecord(); + if (answer != null) + { + answers.add(answer); + } + answer = localHost.getDNS6AddressRecord(); + if (answer != null) + { + answers.add(answer); + } + answers.add(new DNSRecord.Pointer(info.type, DNSConstants.TYPE_PTR, DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, info.getQualifiedName())); + answers.add(new DNSRecord.Service(info.getQualifiedName(), DNSConstants.TYPE_SRV, DNSConstants.CLASS_IN | DNSConstants.CLASS_UNIQUE, DNSConstants.DNS_TTL, info.priority, info.weight, info.port, localHost.getName())); + answers.add(new DNSRecord.Text(info.getQualifiedName(), DNSConstants.TYPE_TXT, DNSConstants.CLASS_IN | DNSConstants.CLASS_UNIQUE, DNSConstants.DNS_TTL, info.text)); + } + } + } + if (q.name.equalsIgnoreCase("_services._mdns._udp.local.")) + { + for (Iterator serviceTypeIterator = serviceTypes.values().iterator(); serviceTypeIterator.hasNext();) + { + answers.add(new DNSRecord.Pointer("_services._mdns._udp.local.", DNSConstants.TYPE_PTR, DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, (String) serviceTypeIterator.next())); + } + } + break; + } + case DNSConstants.TYPE_SRV: + case DNSConstants.TYPE_ANY: + case DNSConstants.TYPE_TXT: + { + ServiceInfo info = (ServiceInfo) services.get(q.name.toLowerCase()); + if (info != null && info.getState() == DNSState.ANNOUNCED) + { + DNSRecord answer = localHost.getDNS4AddressRecord(); + if (answer != null) + { + answers.add(answer); + } + answer = localHost.getDNS6AddressRecord(); + if (answer != null) + { + answers.add(answer); + } + answers.add(new DNSRecord.Pointer(info.type, DNSConstants.TYPE_PTR, DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, info.getQualifiedName())); + answers.add(new DNSRecord.Service(info.getQualifiedName(), DNSConstants.TYPE_SRV, DNSConstants.CLASS_IN | DNSConstants.CLASS_UNIQUE, DNSConstants.DNS_TTL, info.priority, info.weight, info.port, localHost.getName())); + answers.add(new DNSRecord.Text(info.getQualifiedName(), DNSConstants.TYPE_TXT, DNSConstants.CLASS_IN | DNSConstants.CLASS_UNIQUE, DNSConstants.DNS_TTL, info.text)); + } + break; + } + default : + { + //System.out.println("JmDNSResponder.unhandled query:"+q); + break; + } + } + } + } + + + // remove known answers, if the ttl is at least half of + // the correct value. (See Draft Cheshire chapter 7.1.). + for (Iterator i = in.answers.iterator(); i.hasNext();) + { + DNSRecord knownAnswer = (DNSRecord) i.next(); + if (knownAnswer.ttl > DNSConstants.DNS_TTL / 2 && answers.remove(knownAnswer)) + { + logger.log(Level.FINER, "JmDNS Responder Known Answer Removed"); + } + } + + + // responde if we have answers + if (answers.size() != 0) + { + logger.finer("run() JmDNS responding"); + DNSOutgoing out = null; + if (isUnicast) + { + out = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA, false); + } + + for (Iterator i = questions.iterator(); i.hasNext();) + { + out.addQuestion((DNSQuestion) i.next()); + } + for (Iterator i = answers.iterator(); i.hasNext();) + { + out = addAnswer(in, addr, port, out, (DNSRecord) i.next()); + } + send(out); + } + cancel(); + } + catch (Throwable e) + { + logger.log(Level.WARNING, "run() exception ", e); + close(); + } + } + } + } + } + + /** + * Helper class to resolve service types. + * <p/> + * The TypeResolver queries three times consecutively for service types, and then + * removes itself from the timer. + * <p/> + * The TypeResolver will run only if JmDNS is in state ANNOUNCED. + */ + private class TypeResolver extends TimerTask + { + public void start() + { + timer.schedule(this, DNSConstants.QUERY_WAIT_INTERVAL, DNSConstants.QUERY_WAIT_INTERVAL); + } + + /** + * Counts the number of queries that were sent. + */ + int count = 0; + + public void run() + { + try + { + if (state == DNSState.ANNOUNCED) + { + if (++count < 3) + { + logger.finer("run() JmDNS querying type"); + DNSOutgoing out = new DNSOutgoing(DNSConstants.FLAGS_QR_QUERY); + out.addQuestion(new DNSQuestion("_services._mdns._udp.local.", DNSConstants.TYPE_PTR, DNSConstants.CLASS_IN)); + for (Iterator iterator = serviceTypes.values().iterator(); iterator.hasNext();) + { + out.addAnswer(new DNSRecord.Pointer("_services._mdns._udp.local.", DNSConstants.TYPE_PTR, DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, (String) iterator.next()), 0); + } + send(out); + } + else + { + // After three queries, we can quit. + cancel(); + } + ; + } + else + { + if (state == DNSState.CANCELED) + { + cancel(); + } + } + } + catch (Throwable e) + { + logger.log(Level.WARNING, "run() exception ", e); + recover(); + } + } + } + + /** + * The ServiceResolver queries three times consecutively for services of + * a given type, and then removes itself from the timer. + * <p/> + * The ServiceResolver will run only if JmDNS is in state ANNOUNCED. + * REMIND: Prevent having multiple service resolvers for the same type in the + * timer queue. + */ + private class ServiceResolver extends TimerTask + { + /** + * Counts the number of queries being sent. + */ + int count = 0; + private String type; + + public ServiceResolver(String type) + { + this.type = type; + } + + public void start() + { + timer.schedule(this, DNSConstants.QUERY_WAIT_INTERVAL, DNSConstants.QUERY_WAIT_INTERVAL); + } + + public void run() + { + try + { + if (state == DNSState.ANNOUNCED) + { + if (count++ < 3) + { + logger.finer("run() JmDNS querying service"); + long now = System.currentTimeMillis(); + DNSOutgoing out = new DNSOutgoing(DNSConstants.FLAGS_QR_QUERY); + out.addQuestion(new DNSQuestion(type, DNSConstants.TYPE_PTR, DNSConstants.CLASS_IN)); + for (Iterator s = services.values().iterator(); s.hasNext();) + { + final ServiceInfo info = (ServiceInfo) s.next(); + try + { + out.addAnswer(new DNSRecord.Pointer(info.type, DNSConstants.TYPE_PTR, DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, info.getQualifiedName()), now); + } + catch (IOException ee) + { + break; + } + } + send(out); + } + else + { + // After three queries, we can quit. + cancel(); + } + ; + } + else + { + if (state == DNSState.CANCELED) + { + cancel(); + } + } + } + catch (Throwable e) + { + logger.log(Level.WARNING, "run() exception ", e); + recover(); + } + } + } + + /** + * The ServiceInfoResolver queries up to three times consecutively for + * a service info, and then removes itself from the timer. + * <p/> + * The ServiceInfoResolver will run only if JmDNS is in state ANNOUNCED. + * REMIND: Prevent having multiple service resolvers for the same info in the + * timer queue. + */ + private class ServiceInfoResolver extends TimerTask + { + /** + * Counts the number of queries being sent. + */ + int count = 0; + private ServiceInfo info; + + public ServiceInfoResolver(ServiceInfo info) + { + this.info = info; + info.dns = JmDNS.this; + addListener(info, new DNSQuestion(info.getQualifiedName(), DNSConstants.TYPE_ANY, DNSConstants.CLASS_IN)); + } + + public void start() + { + timer.schedule(this, DNSConstants.QUERY_WAIT_INTERVAL, DNSConstants.QUERY_WAIT_INTERVAL); + } + + public void run() + { + try + { + if (state == DNSState.ANNOUNCED) + { + if (count++ < 3 && !info.hasData()) + { + long now = System.currentTimeMillis(); + DNSOutgoing out = new DNSOutgoing(DNSConstants.FLAGS_QR_QUERY); + out.addQuestion(new DNSQuestion(info.getQualifiedName(), DNSConstants.TYPE_SRV, DNSConstants.CLASS_IN)); + out.addQuestion(new DNSQuestion(info.getQualifiedName(), DNSConstants.TYPE_TXT, DNSConstants.CLASS_IN)); + if (info.server != null) + { + out.addQuestion(new DNSQuestion(info.server, DNSConstants.TYPE_A, DNSConstants.CLASS_IN)); + } + out.addAnswer((DNSRecord) cache.get(info.getQualifiedName(), DNSConstants.TYPE_SRV, DNSConstants.CLASS_IN), now); + out.addAnswer((DNSRecord) cache.get(info.getQualifiedName(), DNSConstants.TYPE_TXT, DNSConstants.CLASS_IN), now); + if (info.server != null) + { + out.addAnswer((DNSRecord) cache.get(info.server, DNSConstants.TYPE_A, DNSConstants.CLASS_IN), now); + } + send(out); + } + else + { + // After three queries, we can quit. + cancel(); + removeListener(info); + } + ; + } + else + { + if (state == DNSState.CANCELED) + { + cancel(); + removeListener(info); + } + } + } + catch (Throwable e) + { + logger.log(Level.WARNING, "run() exception ", e); + recover(); + } + } + } + + /** + * The Canceler sends two announces with TTL=0 for the specified services. + */ + private class Canceler extends TimerTask + { + /** + * Counts the number of announces being sent. + */ + int count = 0; + /** + * The services that need cancelling. + * Note: We have to use a local variable here, because the services + * that are canceled, are removed immediately from variable JmDNS.services. + */ + private ServiceInfo[] infos; + /** + * We call notifyAll() on the lock object, when we have canceled the + * service infos. + * This is used by method JmDNS.unregisterService() and + * JmDNS.unregisterAllServices, to ensure that the JmDNS + * socket stays open until the Canceler has canceled all services. + * <p/> + * Note: We need this lock, because ServiceInfos do the transition from + * state ANNOUNCED to state CANCELED before we get here. We could get + * rid of this lock, if we added a state named CANCELLING to DNSState. + */ + private Object lock; + int ttl = 0; + + public Canceler(ServiceInfo info, Object lock) + { + this.infos = new ServiceInfo[]{info}; + this.lock = lock; + addListener(info, new DNSQuestion(info.getQualifiedName(), DNSConstants.TYPE_ANY, DNSConstants.CLASS_IN)); + } + + public Canceler(ServiceInfo[] infos, Object lock) + { + this.infos = infos; + this.lock = lock; + } + + public Canceler(Collection infos, Object lock) + { + this.infos = (ServiceInfo[]) infos.toArray(new ServiceInfo[infos.size()]); + this.lock = lock; + } + + public void start() + { + timer.schedule(this, 0, DNSConstants.ANNOUNCE_WAIT_INTERVAL); + } + + public void run() + { + try + { + if (++count < 3) + { + logger.finer("run() JmDNS canceling service"); + // announce the service + //long now = System.currentTimeMillis(); + DNSOutgoing out = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA); + for (int i = 0; i < infos.length; i++) + { + ServiceInfo info = infos[i]; + out.addAnswer(new DNSRecord.Pointer(info.type, DNSConstants.TYPE_PTR, DNSConstants.CLASS_IN, ttl, info.getQualifiedName()), 0); + out.addAnswer(new DNSRecord.Service(info.getQualifiedName(), DNSConstants.TYPE_SRV, DNSConstants.CLASS_IN, ttl, info.priority, info.weight, info.port, localHost.getName()), 0); + out.addAnswer(new DNSRecord.Text(info.getQualifiedName(), DNSConstants.TYPE_TXT, DNSConstants.CLASS_IN, ttl, info.text), 0); + DNSRecord answer = localHost.getDNS4AddressRecord(); + if (answer != null) + { + out.addAnswer(answer, 0); + } + answer = localHost.getDNS6AddressRecord(); + if (answer != null) + { + out.addAnswer(answer, 0); + } + } + send(out); + } + else + { + // After three successful announcements, we are finished. + synchronized (lock) + { + closed=true; + lock.notifyAll(); + } + cancel(); + } + } + catch (Throwable e) + { + logger.log(Level.WARNING, "run() exception ", e); + recover(); + } + } + } + + // REMIND: Why is this not an anonymous inner class? + /** + * Shutdown operations. + */ + private class Shutdown implements Runnable + { + public void run() + { + shutdown = null; + close(); + } + } + + /** + * Recover jmdns when there is an error. + */ + protected void recover() + { + logger.finer("recover()"); + // We have an IO error so lets try to recover if anything happens lets close it. + // This should cover the case of the IP address changing under our feet + if (DNSState.CANCELED != state) + { + synchronized (this) + { // Synchronize only if we are not already in process to prevent dead locks + // + logger.finer("recover() Cleanning up"); + // Stop JmDNS + state = DNSState.CANCELED; // This protects against recursive calls + + // We need to keep a copy for reregistration + Collection oldServiceInfos = new ArrayList(services.values()); + + // Cancel all services + unregisterAllServices(); + disposeServiceCollectors(); + // + // close multicast socket + closeMulticastSocket(); + // + cache.clear(); + logger.finer("recover() All is clean"); + // + // All is clear now start the services + // + try + { + openMulticastSocket(localHost); + start(oldServiceInfos); + } + catch (Exception exception) + { + logger.log(Level.WARNING, "recover() Start services exception ", exception); + } + logger.log(Level.WARNING, "recover() We are back!"); + } + } + } + + /** + * Close down jmdns. Release all resources and unregister all services. + */ + public void close() + { + if (state != DNSState.CANCELED) + { + synchronized (this) + { // Synchronize only if we are not already in process to prevent dead locks + // Stop JmDNS + state = DNSState.CANCELED; // This protects against recursive calls + + unregisterAllServices(); + disposeServiceCollectors(); + + // close socket + closeMulticastSocket(); + + // Stop the timer + timer.cancel(); + + // remove the shutdown hook + if (shutdown != null) + { + Runtime.getRuntime().removeShutdownHook(shutdown); + } + + } + } + } + + /** + * List cache entries, for debugging only. + */ + void print() + { + System.out.println("---- cache ----"); + cache.print(); + System.out.println(); + } + + /** + * List Services and serviceTypes. + * Debugging Only + */ + + public void printServices() + { + System.err.println(toString()); + } + + public String toString() + { + StringBuffer aLog = new StringBuffer(); + aLog.append("\t---- Services -----"); + if (services != null) + { + for (Iterator k = services.keySet().iterator(); k.hasNext();) + { + Object key = k.next(); + aLog.append("\n\t\tService: " + key + ": " + services.get(key)); + } + } + aLog.append("\n"); + aLog.append("\t---- Types ----"); + if (serviceTypes != null) + { + for (Iterator k = serviceTypes.keySet().iterator(); k.hasNext();) + { + Object key = k.next(); + aLog.append("\n\t\tType: " + key + ": " + serviceTypes.get(key)); + } + } + aLog.append("\n"); + aLog.append(cache.toString()); + aLog.append("\n"); + aLog.append("\t---- Service Collectors ----"); + if (serviceCollectors != null) + { + synchronized (serviceCollectors) + { + for (Iterator k = serviceCollectors.keySet().iterator(); k.hasNext();) + { + Object key = k.next(); + aLog.append("\n\t\tService Collector: " + key + ": " + serviceCollectors.get(key)); + } + serviceCollectors.clear(); + } + } + return aLog.toString(); + } + + /** + * Returns a list of service infos of the specified type. + * + * @param type Service type name, such as <code>_http._tcp.local.</code>. + * @return An array of service instance names. + */ + public ServiceInfo[] list(String type) + { + // Implementation note: The first time a list for a given type is + // requested, a ServiceCollector is created which collects service + // infos. This greatly speeds up the performance of subsequent calls + // to this method. The caveats are, that 1) the first call to this method + // for a given type is slow, and 2) we spawn a ServiceCollector + // instance for each service type which increases network traffic a + // little. + + ServiceCollector collector; + + boolean newCollectorCreated; + synchronized (serviceCollectors) + { + collector = (ServiceCollector) serviceCollectors.get(type); + if (collector == null) + { + collector = new ServiceCollector(type); + serviceCollectors.put(type, collector); + addServiceListener(type, collector); + newCollectorCreated = true; + } + else + { + newCollectorCreated = false; + } + } + + // After creating a new ServiceCollector, we collect service infos for + // 200 milliseconds. This should be enough time, to get some service + // infos from the network. + if (newCollectorCreated) + { + try + { + Thread.sleep(200); + } + catch (InterruptedException e) + { + } + } + + return collector.list(); + } + + /** + * This method disposes all ServiceCollector instances which have been + * created by calls to method <code>list(type)</code>. + * + * @see #list + */ + private void disposeServiceCollectors() + { + logger.finer("disposeServiceCollectors()"); + synchronized (serviceCollectors) + { + for (Iterator i = serviceCollectors.values().iterator(); i.hasNext();) + { + ServiceCollector collector = (ServiceCollector) i.next(); + removeServiceListener(collector.type, collector); + } + serviceCollectors.clear(); + } + } + + /** + * Instances of ServiceCollector are used internally to speed up the + * performance of method <code>list(type)</code>. + * + * @see #list + */ + private static class ServiceCollector implements ServiceListener + { + private static Logger logger = Logger.getLogger(ServiceCollector.class.toString()); + /** + * A set of collected service instance names. + */ + private Map infos = Collections.synchronizedMap(new HashMap()); + + public String type; + + public ServiceCollector(String type) + { + this.type = type; + } + + /** + * A service has been added. + */ + public void serviceAdded(ServiceEvent event) + { + synchronized (infos) + { + event.getDNS().requestServiceInfo(event.getType(), event.getName(), 0); + } + } + + /** + * A service has been removed. + */ + public void serviceRemoved(ServiceEvent event) + { + synchronized (infos) + { + infos.remove(event.getName()); + } + } + + /** + * A service hase been resolved. Its details are now available in the + * ServiceInfo record. + */ + public void serviceResolved(ServiceEvent event) + { + synchronized (infos) + { + infos.put(event.getName(), event.getInfo()); + } + } + + /** + * Returns an array of all service infos which have been collected by this + * ServiceCollector. + */ + public ServiceInfo[] list() + { + synchronized (infos) + { + return (ServiceInfo[]) infos.values().toArray(new ServiceInfo[infos.size()]); + } + } + + public String toString() + { + StringBuffer aLog = new StringBuffer(); + synchronized (infos) + { + for (Iterator k = infos.keySet().iterator(); k.hasNext();) + { + Object key = k.next(); + aLog.append("\n\t\tService: " + key + ": " + infos.get(key)); + } + } + return aLog.toString(); + } + }; + + private static String toUnqualifiedName(String type, String qualifiedName) + { + if (qualifiedName.endsWith(type)) + { + return qualifiedName.substring(0, qualifiedName.length() - type.length() - 1); + } + else + { + return qualifiedName; + } + } +} + Added: trunk/plugins/MDNSDiscovery/javax/jmdns/ServiceEvent.java =================================================================== --- trunk/plugins/MDNSDiscovery/javax/jmdns/ServiceEvent.java (rev 0) +++ trunk/plugins/MDNSDiscovery/javax/jmdns/ServiceEvent.java 2007-03-04 15:23:01 UTC (rev 11954) @@ -0,0 +1,99 @@ +///Copyright 2003-2005 Arthur van Hoff, Rick Blair +//Licensed under Apache License version 2.0 +//Original license LGPL + +package plugins.MDNSDiscovery.javax.jmdns; + +import java.util.EventObject; +import java.util.logging.Logger; + +/** + * ServiceEvent. + * + * @author Werner Randelshofer, Rick Blair + * @version %I%, %G% + */ +public class ServiceEvent extends EventObject +{ + private static Logger logger = Logger.getLogger(ServiceEvent.class.toString()); + /** + * The type name of the service. + */ + private String type; + /** + * The instance name of the service. Or null, if the event was + * fired to a service type listener. + */ + private String name; + /** + * The service info record, or null if the service could be be resolved. + * This is also null, if the event was fired to a service type listener. + */ + private ServiceInfo info; + + /** + * Creates a new instance. + * + * @param source the JmDNS instance which originated the event. + * @param type the type name of the service. + * @param name the instance name of the service. + * @param info the service info record, or null if the service could be be resolved. + */ + public ServiceEvent(JmDNS source, String type, String name, ServiceInfo info) + { + super(source); + this.type = type; + this.name = name; + this.info = info; + } + + /** + * Returns the JmDNS instance which originated the event. + */ + public JmDNS getDNS() + { + return (JmDNS) getSource(); + } + + /** + * Returns the fully qualified type of the service. + */ + public String getType() + { + return type; + } + + /** + * Returns the instance name of the service. + * Always returns null, if the event is sent to a service type listener. + */ + public String getName() + { + return name; + } + + /** + * Returns the service info record, or null if the service could not be + * resolved. + * Always returns null, if the event is sent to a service type listener. + */ + public ServiceInfo getInfo() + { + return info; + } + + public String toString() + { + StringBuffer buf = new StringBuffer(); + buf.append("<" + getClass().getName() + "> "); + buf.append(super.toString()); + buf.append(" name "); + buf.append(getName()); + buf.append(" type "); + buf.append(getType()); + buf.append(" info "); + buf.append(getInfo()); + return buf.toString(); + } + +} Added: trunk/plugins/MDNSDiscovery/javax/jmdns/ServiceInfo.java =================================================================== --- trunk/plugins/MDNSDiscovery/javax/jmdns/ServiceInfo.java (rev 0) +++ trunk/plugins/MDNSDiscovery/javax/jmdns/ServiceInfo.java 2007-03-04 15:23:01 UTC (rev 11954) @@ -0,0 +1,659 @@ +//Copyright 2003-2005 Arthur van Hoff, Rick Blair +//Licensed under Apache License version 2.0 +//Original license LGPL + +package plugins.MDNSDiscovery.javax.jmdns; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetAddress; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.TimerTask; +import java.util.Vector; +import java.util.logging.Logger; + +/** + * JmDNS service information. + * + * @version %I%, %G% + * @author Arthur van Hoff, Jeff Sonstein, Werner Randelshofer + */ +public class ServiceInfo implements DNSListener +{ + private static Logger logger = Logger.getLogger(ServiceInfo.class.toString()); + public final static byte[] NO_VALUE = new byte[0]; + JmDNS dns; + + // State machine + /** + * The state of this service info. + * This is used only for services announced by JmDNS. + * <p/> + * For proper handling of concurrency, this variable must be + * changed only using methods advanceState(), revertState() and cancel(). + */ + private DNSState state = DNSState.PROBING_1; + + /** + * Task associated to this service info. + * Possible tasks are JmDNS.Prober, JmDNS.Announcer, JmDNS.Responder, + * JmDNS.Canceler. + */ + TimerTask task; + + String type; + private String name; + String server; + int port; + int weight; + int priority; + byte text[]; + Hashtable props; + InetAddress addr; + + + /** + * Construct a service description for registrating with JmDNS. + * + * @param type fully qualified service type name, such as <code>_http._tcp.local.</code>. + * @param name unqualified service instance name, such as <code>foobar</code> + * @param port the local port on which the service runs + * @param text string describing the service + */ + public ServiceInfo(String type, String name, int port, String text) + { + this(type, name, port, 0, 0, text); + } + + /** + * Construct a service description for registrating with JmDNS. + * + * @param type fully qualified service type name, such as <code>_http._tcp.local.</code>. + * @param name unqualified service instance name, such as <code>foobar</code> + * @param port the local port on which the service runs + * @param weight weight of the service + * @param priority priority of the service + * @param text string describing the service + */ + public ServiceInfo(String type, String name, int port, int weight, int priority, String text) + { + this(type, name, port, weight, priority, (byte[]) null); + try + { + ByteArrayOutputStream out = new ByteArrayOutputStream(text.length()); + writeUTF(out, text); + this.text = out.toByteArray(); + } + catch (IOException e) + { + throw new RuntimeException("unexpected exception: " + e); + } + } + + /** + * Construct a service description for registrating with JmDNS. The properties hashtable must + * map property names to either Strings or byte arrays describing the property values. + * + * @param type fully qualified service type name, such as <code>_http._tcp.local.</code>. + * @param name unqualified service instance name, such as <code>foobar</code> + * @param port the local port on which the service runs + * @param weight weight of the service + * @param priority priority of the service + * @param props properties describing the service + */ + public ServiceInfo(String type, String name, int port, int weight, int priority, Hashtable props) + { + this(type, name, port, weight, priority, new byte[0]); + if (props != null) + { + try + { + ByteArrayOutputStream out = new ByteArrayOutputStream(256); + for (Enumeration e = props.keys(); e.hasMoreElements();) + { + String key = (String) e.nextElement(); + Object val = props.get(key); + ByteArrayOutputStream out2 = new ByteArrayOutputStream(100); + writeUTF(out2, key); + if (val instanceof String) + { + out2.write('='); + writeUTF(out2, (String) val); + } + else + { + if (val instanceof byte[]) + { + out2.write('='); + byte[] bval = (byte[]) val; + out2.write(bval, 0, bval.length); + } + else + { + if (val != NO_VALUE) + { + throw new IllegalArgumentException("invalid property value: " + val); + } + } + } + byte data[] = out2.toByteArray(); + out.write(data.length); + out.write(data, 0, data.length); + } + this.text = out.toByteArray(); + } + catch (IOException e) + { + throw new RuntimeException("unexpected exception: " + e); + } + } + } + + /** + * Construct a service description for registrating with JmDNS. + * + * @param type fully qualified service type name, such as <code>_http._tcp.local.</code>. + * @param name unqualified service instance name, such as <code>foobar</code> + * @param port the local port on which the service runs + * @param weight weight of the service + * @param priority priority of the service + * @param text bytes describing the service + */ + public ServiceInfo(String type, String name, int port, int weight, int priority, byte text[]) + { + this.type = type; + this.name = name; + this.port = port; + this.weight = weight; + this.priority = priority; + this.text = text; + } + + /** + * Construct a service record during service discovery. + */ + ServiceInfo(String type, String name) + { + if (!type.endsWith(".")) + { + throw new IllegalArgumentException("type must be fully qualified DNS name ending in '.': " + type); + } + + this.type = type; + this.name = name; + } + + /** + * During recovery we need to duplicate service info to reregister them + */ + public ServiceInfo(ServiceInfo info) + { + if (info != null) + { + this.type = info.type; + this.name = info.name; + this.port = info.port; + this.weight = info.weight; + this.priority = info.priority; + this.text = info.text; + } + } + + /** + * Fully qualified service type name, such as <code>_http._tcp.local.</code> . + */ + public String getType() + { + return type; + } + + /** + * Unqualified service instance name, such as <code>foobar</code> . + */ + public String getName() + { + return name; + } + + /** + * Sets the service instance name. + * + * @param name unqualified service instance name, such as <code>foobar</code> + */ + void setName(String name) + { + this.name = name; + } + + /** + * Fully qualified service name, such as <code>foobar._http._tcp.local.</code> . + */ + public String getQualifiedName() + { + return name + "." + type; + } + + /** + * Get the name of the server. + */ + public String getServer() + { + return server; + } + + /** + * Get the host address of the service (ie X.X.X.X). + */ + public String getHostAddress() + { + return (addr != null ? addr.getHostAddress() : ""); + } + + public InetAddress getAddress() + { + return addr; + } + + /** + * Get the InetAddress of the service. + */ + public InetAddress getInetAddress() + { + return addr; + } + + /** + * Get the port for the service. + */ + public int getPort() + { + return port; + } + + /** + * Get the priority of the service. + */ + public int getPriority() + { + return priority; + } + + /** + * Get the weight of the service. + */ + public int getWeight() + { + return weight; + } + + /** + * Get the text for the serivce as raw bytes. + */ + public byte[] getTextBytes() + { + return text; + } + + /** + * Get the text for the service. This will interpret the text bytes + * as a UTF8 encoded string. Will return null if the bytes are not + * a valid UTF8 encoded string. + */ + public String getTextString() + { + if ((text == null) || (text.length == 0) || ((text.length == 1) && (text[0] == 0))) + { + return null; + } + return readUTF(text, 0, text.length); + } + + /** + * Get the URL for this service. An http URL is created by + * combining the address, port, and path properties. + */ + public String getURL() + { + return getURL("http"); + } + + /** + * Get the URL for this service. An URL is created by + * combining the protocol, address, port, and path properties. + */ + public String getURL(String protocol) + { + String url = protocol + "://" + getAddress() + ":" + getPort(); + String path = getPropertyString("path"); + if (path != null) + { + if (path.indexOf("://") >= 0) + { + url = path; + } + else + { + url += path.startsWith("/") ? path : "/" + path; + } + } + return url; + } + + /** + * Get a property of the service. This involves decoding the + * text bytes into a property list. Returns null if the property + * is not found or the text data could not be decoded correctly. + */ + public synchronized byte[] getPropertyBytes(String name) + { + return (byte[]) getProperties().get(name); + } + + /** + * Get a property of the service. This involves decoding the + * text bytes into a property list. Returns null if the property + * is not found, the text data could not be decoded correctly, or + * the resulting bytes are not a valid UTF8 string. + */ + public synchronized String getPropertyString(String name) + { + byte data[] = (byte[]) getProperties().get(name); + if (data == null) + { + return null; + } + if (data == NO_VALUE) + { + return "true"; + } + return readUTF(data, 0, data.length); + } + + /** + * Enumeration of the property names. + */ + public Enumeration getPropertyNames() + { + Hashtable props = getProperties(); + return (props != null) ? props.keys() : new Vector().elements(); + } + + /** + * Write a UTF string with a length to a stream. + */ + void writeUTF(OutputStream out, String str) throws IOException + { + for (int i = 0, len = str.length(); i < len; i++) + { + int c = str.charAt(i); + if ((c >= 0x0001) && (c <= 0x007F)) + { + out.write(c); + } + else + { + if (c > 0x07FF) + { + out.write(0xE0 | ((c >> 12) & 0x0F)); + out.write(0x80 | ((c >> 6) & 0x3F)); + out.write(0x80 | ((c >> 0) & 0x3F)); + } + else + { + out.write(0xC0 | ((c >> 6) & 0x1F)); + out.write(0x80 | ((c >> 0) & 0x3F)); + } + } + } + } + + /** + * Read data bytes as a UTF stream. + */ + String readUTF(byte data[], int off, int len) + { + StringBuffer buf = new StringBuffer(); + for (int end = off + len; off < end;) + { + int ch = data[off++] & 0xFF; + switch (ch >> 4) + { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + // 0xxxxxxx + break; + case 12: + case 13: + if (off >= len) + { + return null; + } + // 110x xxxx 10xx xxxx + ch = ((ch & 0x1F) << 6) | (data[off++] & 0x3F); + break; + case 14: + if (off + 2 >= len) + { + return null; + } + // 1110 xxxx 10xx xxxx 10xx xxxx + ch = ((ch & 0x0f) << 12) | ((data[off++] & 0x3F) << 6) | (data[off++] & 0x3F); + break; + default: + if (off + 1 >= len) + { + return null; + } + // 10xx xxxx, 1111 xxxx + ch = ((ch & 0x3F) << 4) | (data[off++] & 0x0f); + break; + } + buf.append((char) ch); + } + return buf.toString(); + } + + synchronized Hashtable getProperties() + { + if ((props == null) && (text != null)) + { + Hashtable props = new Hashtable(); + int off = 0; + while (off < text.length) + { + // length of the next key value pair + int len = text[off++] & 0xFF; + if ((len == 0) || (off + len > text.length)) + { + props.clear(); + break; + } + // look for the '=' + int i = 0; + for (; (i < len) && (text[off + i] != '='); i++) + { + ; + } + + // get the property name + String name = readUTF(text, off, i); + if (name == null) + { + props.clear(); + break; + } + if (i == len) + { + props.put(name, NO_VALUE); + } + else + { + byte value[] = new byte[len - ++i]; + System.arraycopy(text, off + i, value, 0, len - i); + props.put(name, value); + off += len; + } + } + this.props = props; + } + return props; + } + + // REMIND: Oops, this shouldn't be public! + /** + * JmDNS callback to update a DNS record. + */ + public void updateRecord(JmDNS jmdns, long now, DNSRecord rec) + { + if ((rec != null) && !rec.isExpired(now)) + { + switch (rec.type) + { + case DNSConstants.TYPE_A: // IPv4 + case DNSConstants.TYPE_AAAA: // IPv6 FIXME [PJYF Oct 14 2004] This has not been tested + if (rec.name.equals(server)) + { + addr = ((DNSRecord.Address) rec).getAddress(); + + } + break; + case DNSConstants.TYPE_SRV: + if (rec.name.equals(getQualifiedName())) + { + DNSRecord.Service srv = (DNSRecord.Service) rec; + server = srv.server; + port = srv.port; + weight = srv.weight; + priority = srv.priority; + addr = null; + // changed to use getCache() instead - jeffs + // updateRecord(jmdns, now, (DNSRecord)jmdns.cache.get(server, TYPE_A, CLASS_IN)); + updateRecord(jmdns, now, (DNSRecord) jmdns.getCache().get(server, DNSConstants.TYPE_A, DNSConstants.CLASS_IN)); + } + break; + case DNSConstants.TYPE_TXT: + if (rec.name.equals(getQualifiedName())) + { + DNSRecord.Text txt = (DNSRecord.Text) rec; + text = txt.text; + } + break; + } + // Future Design Pattern + // This is done, to notify the wait loop in method + // JmDNS.getServiceInfo(type, name, timeout); + if (hasData() && dns != null) + { + dns.handleServiceResolved(this); + dns = null; + } + synchronized (this) + { + notifyAll(); + } + } + } + + /** + * Returns true if the service info is filled with data. + */ + boolean hasData() + { + return server != null && addr != null && text != null; + } + + + // State machine + /** + * Sets the state and notifies all objects that wait on the ServiceInfo. + */ + synchronized void advanceState() + { + state = state.advance(); + notifyAll(); + } + + /** + * Sets the state and notifies all objects that wait on the ServiceInfo. + */ + synchronized void revertState() + { + state = state.revert(); + notifyAll(); + } + + /** + * Sets the state and notifies all objects that wait on the ServiceInfo. + */ + synchronized void cancel() + { + state = DNSState.CANCELED; + notifyAll(); + } + + /** + * Returns the current state of this info. + */ + DNSState getState() + { + return state; + } + + + public int hashCode() + { + return getQualifiedName().hashCode(); + } + + public boolean equals(Object obj) + { + return (obj instanceof ServiceInfo) && getQualifiedName().equals(((ServiceInfo) obj).getQualifiedName()); + } + + public String getNiceTextString() + { + StringBuffer buf = new StringBuffer(); + for (int i = 0, len = text.length; i < len; i++) + { + if (i >= 20) + { + buf.append("..."); + break; + } + int ch = text[i] & 0xFF; + if ((ch < ' ') || (ch > 127)) + { + buf.append("\\0"); + buf.append(Integer.toString(ch, 8)); + } + else + { + buf.append((char) ch); + } + } + return buf.toString(); + } + + public String toString() + { + StringBuffer buf = new StringBuffer(); + buf.append("service["); + buf.append(getQualifiedName()); + buf.append(','); + buf.append(getAddress()); + buf.append(':'); + buf.append(port); + buf.append(','); + buf.append(getNiceTextString()); + buf.append(']'); + return buf.toString(); + } +} Added: trunk/plugins/MDNSDiscovery/javax/jmdns/ServiceListener.java =================================================================== --- trunk/plugins/MDNSDiscovery/javax/jmdns/ServiceListener.java (rev 0) +++ trunk/plugins/MDNSDiscovery/javax/jmdns/ServiceListener.java 2007-03-04 15:23:01 UTC (rev 11954) @@ -0,0 +1,42 @@ +//Copyright 2003-2005 Arthur van Hoff, Rick Blair +//Licensed under Apache License version 2.0 +//Original license LGPL + +package plugins.MDNSDiscovery.javax.jmdns; + +import java.util.EventListener; + +/** + * Listener for service updates. + * + * @version %I%, %G% + * @author Arthur van Hoff, Werner Randelshofer + */ +public interface ServiceListener extends EventListener +{ + /** + * A service has been added. + * + * @param event The ServiceEvent providing the name and fully qualified type + * of the service. + */ + void serviceAdded(ServiceEvent event); + + /** + * A service has been removed. + * + * @param event The ServiceEvent providing the name and fully qualified type + * of the service. + */ + void serviceRemoved(ServiceEvent event); + + /** + * A service has been resolved. Its details are now available in the + * ServiceInfo record. + * + * @param event The ServiceEvent providing the name, the fully qualified + * type of the service, and the service info record, or null if the service + * could not be resolved. + */ + void serviceResolved(ServiceEvent event); +} Added: trunk/plugins/MDNSDiscovery/javax/jmdns/ServiceTypeListener.java =================================================================== --- trunk/plugins/MDNSDiscovery/javax/jmdns/ServiceTypeListener.java (rev 0) +++ trunk/plugins/MDNSDiscovery/javax/jmdns/ServiceTypeListener.java 2007-03-04 15:23:01 UTC (rev 11954) @@ -0,0 +1,24 @@ +//Copyright 2003-2005 Arthur van Hoff, Rick Blair +//Licensed under Apache License version 2.0 +//Original license LGPL + +package plugins.MDNSDiscovery.javax.jmdns; + +import java.util.EventListener; + +/** + * Listener for service types. + * + * @version %I%, %G% + * @author Arthur van Hoff, Werner Randelshofer + */ +public interface ServiceTypeListener extends EventListener +{ + /** + * A new service type was discovered. + * + * @param event The service event providing the fully qualified type of + * the service. + */ + void serviceTypeAdded(ServiceEvent event); +}
