/******************************************************************************* * Copyright (c) 2007, 2016 Wind River Systems, Inc. and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Wind River Systems - initial API and implementation *******************************************************************************/ package org.eclipse.tcf.internal.services.local; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.Inet4Address; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.InterfaceAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.net.UnknownHostException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import org.eclipse.tcf.internal.core.LocalPeer; import org.eclipse.tcf.internal.core.LoggingUtil; import org.eclipse.tcf.internal.core.RemotePeer; import org.eclipse.tcf.internal.core.ServiceManager; import org.eclipse.tcf.core.AbstractChannel; import org.eclipse.tcf.core.UserDefPeer; import org.eclipse.tcf.protocol.IChannel; import org.eclipse.tcf.protocol.IErrorReport; import org.eclipse.tcf.protocol.IPeer; import org.eclipse.tcf.protocol.IService; import org.eclipse.tcf.protocol.IServiceProvider; import org.eclipse.tcf.protocol.IToken; import org.eclipse.tcf.protocol.JSON; import org.eclipse.tcf.protocol.Protocol; import org.eclipse.tcf.services.ILocator; /** * Locator service uses transport layer to search * for peers and to collect and maintain up-to-date * data about peer's attributes. */ // TODO: research usage of DNS-SD (DNS Service Discovery) to discover TCF peers public class LocatorService implements ILocator { /** * Default discovery port */ private static final int DISCOVERY_PORT = 1534; /** * Max packet size */ private static final int MAX_PACKET_SIZE = 9000 - 40 - 8; /** * Preferred packet size */ private static final int PREF_PACKET_SIZE = 1500 - 40 - 8; private static LocatorService locator; private static LocalPeer local_peer; private static final Map<String,IPeer> peers = new HashMap<String,IPeer>(); private static final ArrayList<LocatorListener> listeners = new ArrayList<LocatorListener>(); private static final HashSet<String> error_log = new HashSet<String>(); private final HashSet<SubNet> subnets = new HashSet<SubNet>(); private final ArrayList<Slave> slaves = new ArrayList<Slave>(); private final byte[] inp_buf = new byte[MAX_PACKET_SIZE]; private final byte[] out_buf = new byte[MAX_PACKET_SIZE]; private InetAddress loopback_addr; /** * Flag indicating whether tracing of the discovery activity is enabled. */ private static boolean TRACE_DISCOVERY = System.getProperty("org.eclipse.tcf.core.tracing.discovery") != null; /** * Internal Subnetwork Representation */ private static class SubNet { final int prefix_length; final InetAddress address; final InetAddress broadcast; long last_slaves_req_time; boolean send_all_ok; SubNet(int prefix_length, InetAddress address, InetAddress broadcast) { this.prefix_length = prefix_length; this.address = address; this.broadcast = broadcast; } /** * Check whether the IP Address in part the subnetwork * @param addr IP Address * @return true if the IP Address is contained, otherwise false */ boolean contains(InetAddress addr) { if (addr == null || address == null) return false; byte[] a1 = addr.getAddress(); byte[] a2 = address.getAddress(); if (a1.length != a2.length) return false; int i = 0; int l = prefix_length <= a1.length * 8 ? prefix_length : a1.length * 8; while (i + 8 <= l) { int n = i / 8; if (a1[n] != a2[n]) return false; i += 8; } while (i < l) { int n = i / 8; int m = 1 << (7 - i % 8); if ((a1[n] & m) != (a2[n] & m)) return false; i++; } return true; } @Override public boolean equals(Object o) { if (!(o instanceof SubNet)) return false; SubNet x = (SubNet)o; return prefix_length == x.prefix_length && broadcast.equals(x.broadcast) && address.equals(x.address); } @Override public int hashCode() { return address.hashCode(); } @Override public String toString() { return address.getHostAddress() + "/" + prefix_length; } } /** Represents a peer which was discovered, that is a peer whose Locator was not the first Locator in the network. * A master is the peer which discovered a slave, that is a peer whose Locator was the first Locator in the network */ private static class Slave { final InetAddress address; final int port; /** Time of last packet receiver from this slave */ long last_packet_time; /** Time of last REQ_SLAVES packet received from this slave */ long last_req_slaves_time; Slave(InetAddress address, int port) { this.address = address; this.port = port; } @Override public String toString() { return address.getHostAddress() + "/" + port; } } /** * Represents a hostname/address item that is used when resolving IP addresses from hostnames. * It contains a timestamp of the last time the resolution of hostname -> ip was done. * As well as if it the mapping was used in a DATA_RETENTION_PERIOD * */ private static class AddressCacheItem { final String host; InetAddress address; long time_stamp; boolean used; AddressCacheItem(String host) { this.host = host; } } /** * Stores Hostname/IP addresses mappings that are used when resolving an IP Address from a hostname */ private static final HashMap<String,AddressCacheItem> addr_cache = new HashMap<String,AddressCacheItem>(); /** * Used to request an inmediate prune and update of addr_cache mappings */ private static boolean addr_request; /** * Socket used to send/receive the packets to/from remote peers */ private DatagramSocket socket; /** * The timestamp of the last received packet from a Locator master. * If there is no remote locator master, meaning that this is the locator master, then the value is 0. */ private long last_master_packet_time; /** * LocatorService's thread for refreshing subnet list of peers, as part of the discovery. * Its run method delegates the refresh of the subnet list of peers to the dispatch thread, using the {@code refresh_timer} method. * Thread's name is: TCF Locator Timer */ private final Thread timer_thread = new Thread() { public void run() { while (true) { try { final HashSet<SubNet> set = getSubNetList(); Protocol.invokeAndWait(new Runnable() { public void run() { refresh_timer(set); } }); sleep(DATA_RETENTION_PERIOD / 4); } catch (IllegalStateException x) { // TCF event dispatch is shut down return; } catch (Throwable x) { log("Unhandled exception in TCF discovery timer thread", x); } } } }; /** * LocatorService's thread for maintaining the hostname/ip mapping held in addr_cache as part of the discovery. * It runs while the <code>getInetAddress(String hostname)</code> is not running, to refresh ip resolution of hostsnames that getInetAddress has already added to addr_cache * Thread's name is: TCF Locator DNS Lookup */ private Thread dns_lookup_thread = new Thread() { public void run() { while (true) { try { long time; HashSet<AddressCacheItem> set = null; synchronized (addr_cache) { if (!addr_request) addr_cache.wait(DATA_RETENTION_PERIOD); time = System.currentTimeMillis(); for (Iterator<AddressCacheItem> i = addr_cache.values().iterator(); i.hasNext();) { AddressCacheItem a = i.next(); if (a.time_stamp + DATA_RETENTION_PERIOD * 10 < time) { /* * Remove entries that have not been used in between sucessive runs of this method */ if (a.used) { if (set == null) set = new HashSet<AddressCacheItem>(); set.add(a); } else { i.remove(); } } } /* * Once we have pruned the addr_cache we wait for the DATA_RETENTION_PERIOD unless the getInetAddress method sets addr_request true */ addr_request = false; } /* * Update the used AddressCacheItems of addr_cache */ if (set != null) { for (AddressCacheItem a : set) { InetAddress addr = null; try { addr = InetAddress.getByName(a.host); } catch (UnknownHostException x) { } synchronized (addr_cache) { a.address = addr; a.time_stamp = time; a.used = false; } } } } catch (Throwable x) { log("Unhandled exception in TCF discovery DNS lookup thread", x); } } } }; /** * Wrapper for final class DatagramPacket so its toString() can present * the value in the debugger in a readable fashion. */ private static class InputPacket { final DatagramPacket p; InputPacket(DatagramPacket p) { this.p = p; } DatagramPacket getPacket() { return p; } int getLength() { return p.getLength(); } byte[] getData() { return p.getData(); } int getPort() { return p.getPort(); } InetAddress getAddress() { return p.getAddress(); } @Override public String toString() { return "[address=" + p.getAddress().toString() + ",port=" + p.getPort() + ",data=\"" + new String(p.getData(), 0, p.getLength()) + "\"]"; } } /** * LocatorService's thread for handling packets received from other Peers, as part of the discovery. * Its run method delegates the handling of the packet to the dispatch thread, using the {@code handleDatagramPacket} method. * Thread's name is: TCF Locator Receiver */ private final Thread input_thread = new Thread() { public void run() { try { for (;;) { DatagramSocket socket = LocatorService.this.socket; try { final InputPacket p = new InputPacket(new DatagramPacket(inp_buf, inp_buf.length)); socket.receive(p.getPacket()); Protocol.invokeAndWait(new Runnable() { public void run() { handleDatagramPacket(p); } }); } catch (IllegalStateException x) { // TCF event dispatch is shut down return; } catch (Exception x) { if (socket != LocatorService.this.socket) continue; log("Cannot read from datagram socket at port " + socket.getLocalPort(), x); sleep(2000); } } } catch (Throwable x) { log("Unhandled exception in socket reading thread", x); } } }; static { ServiceManager.addServiceProvider(new IServiceProvider() { /* * LocatorService's ServiceProvider only implements getLocalService, as this service is implemented locally and not remotely */ public IService[] getLocalService(final IChannel channel) { channel.addCommandServer(locator, new IChannel.ICommandServer() { public void command(IToken token, String name, byte[] data) { locator.command((AbstractChannel)channel, token, name, data); } }); return new IService[]{ locator }; } public IService getServiceProxy(IChannel channel, String service_name) { return null; } }); // Bug #400659 (https://bugs.eclipse.org/bugs/show_bug.cgi?id=400659): // // To workaround Bug 388125 (Oracle bug 7179799) with Java 7, force // "java.net.preferIPv4Stack" to be set to "true" // // Applies on Windows platforms only. boolean isWin = System.getProperty("os.name", "").toLowerCase().startsWith("windows"); if (isWin && System.getProperty("java.net.preferIPv4Stack") == null) { //$NON-NLS-1$ System.setProperty("java.net.preferIPv4Stack", "true"); //$NON-NLS-1$ //$NON-NLS-2$ } } /** * Creates a socket which will behave either as a slave locator or master locator, depending on the given slave parameter * @param slave true if the socket is representing a slave locator, false otherwise * @return the newly created socket * @throws SocketException thrown when the socket could not be created on the discovery port (couldn't be a master locator) */ private static DatagramSocket createSocket(boolean slave) throws SocketException { DatagramSocket socket = null; if (slave) { socket = new DatagramSocket(); } else { socket = new DatagramSocket(null); socket.setReuseAddress(false); socket.bind(new InetSocketAddress(DISCOVERY_PORT)); } socket.setBroadcast(true); return socket; } /** * Creates a local instance of the LocatorService which involves creating: * 1. LocalPeer * 2. LocatorService itself */ public static void createLocalInstance() { local_peer = new LocalPeer(); locator = new LocatorService(); } /** * LocatorService constructor used in {@code createLocalInstace} */ public LocatorService() { try { loopback_addr = InetAddress.getByName(null); out_buf[0] = 'T'; out_buf[1] = 'C'; out_buf[2] = 'F'; out_buf[3] = CONF_VERSION; out_buf[4] = 0; out_buf[5] = 0; out_buf[6] = 0; out_buf[7] = 0; try { socket = createSocket(false); if (TRACE_DISCOVERY) { LoggingUtil.trace("Became the master agent (bound to port " + socket.getLocalPort() + ")"); } } catch (SocketException x) { socket = createSocket(true); if (TRACE_DISCOVERY) { LoggingUtil.trace("Became a slave agent (bound to port " + socket.getLocalPort() + ")"); } } input_thread.setName("TCF Locator Receiver"); timer_thread.setName("TCF Locator Timer"); dns_lookup_thread.setName("TCF Locator DNS Lookup"); input_thread.setDaemon(true); timer_thread.setDaemon(true); dns_lookup_thread.setDaemon(true); /* * All the locator thread's are run in the dispatch thread */ Protocol.invokeLater(new Runnable() { @Override public void run() { input_thread.start(); timer_thread.start(); dns_lookup_thread.start(); } }); listeners.add(new LocatorListener() { @Override public void peerAdded(IPeer peer) { sendPeerInfo(peer, null, 0); } @Override public void peerChanged(IPeer peer) { sendPeerInfo(peer, null, 0); } @Override public void peerHeartBeat(String id) { } @Override public void peerRemoved(String id) { } }); } catch (Exception x) { log("Cannot open UDP socket for TCF discovery protocol", x); } } /** * Get the LocalPeer instance * @return LocalPeer instance */ public static LocalPeer getLocalPeer() { return local_peer; } /** * Get the Locator Event listeners * @return array of locator listeners */ public static LocatorListener[] getListeners() { return listeners.toArray(new LocatorListener[listeners.size()]); } /** * Creates an error report using the given TCF error code, and msg parameters * @param code the error code, from the Standard TCF error codes * @param msg an error message * @return return a map representation of the error report */ private Map<String,Object> makeErrorReport(int code, String msg) { Map<String,Object> err = new HashMap<String,Object>(); err.put(IErrorReport.ERROR_TIME, Long.valueOf(System.currentTimeMillis())); err.put(IErrorReport.ERROR_CODE, Integer.valueOf(code)); err.put(IErrorReport.ERROR_FORMAT, msg); return err; } /** * Handles all command messages received through the channel * * @param channel the channel through which the commands are received/sent * @param token handle associated with the command * @param name command name * @param data command arguments in byte array format */ private void command(final AbstractChannel channel, final IToken token, String name, byte[] data) { try { /* * Redirect command */ if (name.equals("redirect")) { String peer_id = (String)JSON.parseSequence(data)[0]; IPeer peer = peers.get(peer_id); if (peer == null) { channel.sendResult(token, JSON.toJSONSequence(new Object[]{ makeErrorReport(IErrorReport.TCF_ERROR_UNKNOWN_PEER, "Unknown peer ID") })); return; } channel.sendResult(token, JSON.toJSONSequence(new Object[]{ null })); if (peer instanceof LocalPeer) { channel.sendEvent(Protocol.getLocator(), "Hello", JSON.toJSONSequence( new Object[]{ channel.getLocalServices() })); return; } new ChannelProxy(channel, peer.openChannel()); } /* * Sync command */ else if (name.equals("sync")) { channel.sendResult(token, null); } /* * getPeers command */ else if (name.equals("getPeers")) { int i = 0; Object[] arr = new Object[peers.size()]; for (IPeer p : peers.values()) arr[i++] = p.getAttributes(); channel.sendResult(token, JSON.toJSONSequence(new Object[]{ null, arr })); } /* * Unrecognized command */ else { channel.rejectCommand(token); } } catch (Throwable x) { channel.terminate(x); } } /** * Logs the given message and Exception/Error * @param msg message to be logged * @param x exception or error to be logged */ private void log(String msg, Throwable x) { // Don't report same error multiple times to avoid filling up the log file. synchronized (error_log) { if (error_log.contains(msg)) return; error_log.add(msg); } Protocol.log(msg, x); } /** * Gets the IP Address from a Hostname. * To do that it uses the addr_cache, to search for an entry with the given host. * If there's such an entry, it returns the address. * If there isn't such an entry it constructs an AddressCacheItem entry and adds it to the addr_cache. * Marks the given AddressCacheItem's used field as true. * * @param host Host in Hostname format * @return IP Address of the Host */ private InetAddress getInetAddress(String host) { if (host == null || host.length() == 0) return null; synchronized (addr_cache) { AddressCacheItem i = addr_cache.get(host); if (i == null) { i = new AddressCacheItem(host); char ch = host.charAt(0); if (ch == '[' || ch == ':' || ch >= '0' && ch <= '9') { try { i.address = InetAddress.getByName(host); } catch (UnknownHostException e) { } i.time_stamp = System.currentTimeMillis(); } else { /* InetAddress.getByName() can cause long delay - delegate to background thread */ addr_request = true; addr_cache.notify(); } addr_cache.put(host, i); } i.used = true; return i.address; } } /** * Refreshes the Subnetworks lists for slaves * @param nets Subnetwork lists */ private void refresh_timer(HashSet<SubNet> nets) { long time = System.currentTimeMillis(); /* Cleanup slave table */ if (slaves.size() > 0) { int i = 0; while (i < slaves.size()) { Slave s = slaves.get(i); /* Removes slave if there is no recent packet receveid from it */ if (s.last_packet_time + DATA_RETENTION_PERIOD < time) { slaves.remove(i); } else { i++; } } } /* Cleanup peers table */ ArrayList<RemotePeer> stale_peers = null; for (IPeer p : peers.values()) { if (p instanceof RemotePeer) { RemotePeer r = (RemotePeer)p; if (r.getLastUpdateTime() + DATA_RETENTION_PERIOD < time) { if (stale_peers == null) stale_peers = new ArrayList<RemotePeer>(); stale_peers.add(r); } } } if (stale_peers != null) { for (RemotePeer p : stale_peers) p.dispose(); } /* Try to become a master */ if (socket.getLocalPort() != DISCOVERY_PORT && last_master_packet_time + DATA_RETENTION_PERIOD / 2 <= time) { try { DatagramSocket s0 = socket; socket = createSocket(false); if (TRACE_DISCOVERY) { LoggingUtil.trace("Became the master agent (bound to port " + socket.getLocalPort() + ")"); } s0.close(); } catch (Throwable x) { } } if (refreshSubNetList(nets)) sendPeersRequest(null, 0); if (socket.getLocalPort() != DISCOVERY_PORT) { for (SubNet subnet : subnets) { addSlave(subnet.address, socket.getLocalPort(), time); } } sendAll(null, 0, null, time); } /** * Add a slaves with the given Address, port number and timestamp to the array of slaves, and return it * @param addr IP Address of the slave * @param port Port of the slave * @param timestamp timestamp at which the last packet was received * @return the slave which was created using the given parameters */ private Slave addSlave(InetAddress addr, int port, long timestamp) { /* * Check if there's an slave already in the list of known slaves, and if there is refresh the timestamp of the last packet received and return it */ for (Slave s : slaves) { if (s.port == port && s.address.equals(addr)) { if (s.last_packet_time < timestamp) s.last_packet_time = timestamp; return s; } } final Slave s = new Slave(addr, port); s.last_packet_time = timestamp; slaves.add(s); /* * Crea */ Protocol.invokeLater(new Runnable() { public void run() { long time_now = System.currentTimeMillis(); sendPeersRequest(s.address, s.port); sendAll(s.address, s.port, s, time_now); sendSlaveInfo(s, time_now); } }); return s; } private boolean refreshSubNetList(HashSet<SubNet> set) { if (set == null) return false; for (Iterator<SubNet> i = subnets.iterator(); i.hasNext();) { SubNet s = i.next(); if (set.contains(s)) continue; i.remove(); } boolean new_nets = false; for (Iterator<SubNet> i = set.iterator(); i.hasNext();) { SubNet s = i.next(); if (subnets.contains(s)) continue; subnets.add(s); new_nets = true; } if (TRACE_DISCOVERY) { StringBuilder str = new StringBuilder("Refreshed subnet list:"); for (SubNet subnet : subnets) { str.append("\n\t* address=" + subnet.address + ", broadcast=" + subnet.broadcast); } LoggingUtil.trace(str.toString()); } return new_nets; } /** * Finds all subnetworks, adds them to the {@code subnet} set and returns it. * @return set of SubNets */ private HashSet<SubNet> getSubNetList() { HashSet<SubNet> set = new HashSet<SubNet>(); try { String osname = System.getProperty("os.name", ""); if (osname.startsWith("Windows")) { /* * Workaround for JVM bug: * InterfaceAddress.getNetworkPrefixLength() does not conform to Javadoc * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6707289 * * The bug shows up only on Windows when IPv6 is enabled. * The bug is supposed to be fixed in Java 1.7. */ getWindowsSubNetList(set); } else { getSubNetList(set); } } catch (Exception x) { log("Cannot get list of network interfaces", x); return null; } return set; } /** * Find and adds all subnets to the given set * @param set set of subnetworks * @throws SocketException */ private void getSubNetList(HashSet<SubNet> set) throws SocketException { for (Enumeration<NetworkInterface> e = NetworkInterface.getNetworkInterfaces(); e.hasMoreElements();) { NetworkInterface f = e.nextElement(); for (InterfaceAddress ia : f.getInterfaceAddresses()) { int network_prefix_len = ia.getNetworkPrefixLength(); InetAddress address = ia.getAddress(); InetAddress broadcast = ia.getBroadcast(); if (address instanceof Inet4Address) { if (network_prefix_len == 0) { // Java 1.6.0 on Linux returns network prefix == 0 for loop-back interface byte[] buf = address.getAddress(); if (buf[0] == 127) network_prefix_len = 8; } if (broadcast == null) { // Java 1.7.0 on Linux returns broadcast == null for loop-back interface broadcast = address; } } if (network_prefix_len > 0 && address != null && broadcast != null) { set.add(new SubNet(network_prefix_len, address, broadcast)); } } } } /** * Finds and adds Subnetworks to the * @param set * @throws Exception */ private void getWindowsSubNetList(HashSet<SubNet> set) throws Exception { HashMap<String,InetAddress> map = new HashMap<String,InetAddress>(); for (Enumeration<NetworkInterface> e = NetworkInterface.getNetworkInterfaces(); e.hasMoreElements();) { NetworkInterface f = e.nextElement(); Enumeration<InetAddress> n = f.getInetAddresses(); while (n.hasMoreElements()) { InetAddress addr = n.nextElement(); if (addr instanceof Inet4Address) { String s = addr.getHostAddress(); if (s.startsWith("127.")) { byte[] buf = addr.getAddress(); buf[1] = buf[2] = buf[3] = (byte)255; set.add(new SubNet(8, addr, InetAddress.getByAddress(buf))); } else { map.put(s, addr); } } } } Process prs = Runtime.getRuntime().exec(new String[]{ "ipconfig", "/all" }, null); BufferedReader inp = new BufferedReader(new InputStreamReader(prs.getInputStream())); for (;;) { String s = inp.readLine(); if (s == null) break; int n = s.indexOf(" : "); if (n < 0) continue; n += 3; int m = n; while (m < s.length()) { char ch = s.charAt(m); if ((ch < '0' || ch > '9') && ch != '.') break; m++; } if (m == n) continue; InetAddress addr = map.get(s.substring(n, m)); if (addr == null) continue; do s = inp.readLine(); while (s != null && s.length() == 0); if (s == null) break; n = s.indexOf(" : "); if (n < 0) continue; s = s.substring(n + 3); int l = s.length(); int i_cnt = 0; int d_cnt = 0; for (int i = 0; i < l; i++) { char ch = s.charAt(i); if (ch == '.') d_cnt++; else if (ch < '0' || ch > '9') i_cnt++; } if (d_cnt != 3 || i_cnt != 0) continue; try { byte[] buf = InetAddress.getByName(s).getAddress(); int prefix_length = 0; for (int i = 0; i < 32; i++) { if ((buf[i / 8] & (1 << (7 - i % 8))) == 0) { prefix_length = i; break; } } if (prefix_length > 0) { buf = addr.getAddress(); for (int i = prefix_length; i < 32; i++) { buf[i / 8] |= 1 << (7 - i % 8); } set.add(new SubNet(prefix_length, addr, InetAddress.getByAddress(buf))); } } catch (Exception x) { } } try { prs.getErrorStream().close(); prs.getOutputStream().close(); inp.close(); } catch (IOException x) { } prs.waitFor(); } private byte[] getUTF8Bytes(String s) { try { return s.getBytes("UTF-8"); } catch (Exception x) { log("UTF-8 character encoder is not available", x); return s.getBytes(); } } /** Used for tracing */ private static String packetTypes[] = new String[] { null, "CONF_REQ_INFO", "CONF_PEER_INFO", "CONF_REQ_SLAVES", "CONF_SLAVES_INFO", "CONF_PEER_REMOVE" }; /** * Sends a Datagram packet of the given length to the Peer in the given subnet, with the given address and port * @param subnet the subnet on which the the peer address is located * @param size the datagram packet size * @param addr the peer's IP address * @param port the destination port number * @return true if the datagram was sent, false is the datagram was not sent */ private boolean sendDatagramPacket(SubNet subnet, int size, InetAddress addr, int port) { try { if (addr == null) { addr = subnet.address.equals(loopback_addr) ? loopback_addr : subnet.broadcast; port = DISCOVERY_PORT; for (Slave slave : slaves) { sendDatagramPacket(subnet, size, slave.address, slave.port); } } if (!subnet.contains(addr)) return false; if (port == socket.getLocalPort() && addr.equals(subnet.address)) return false; socket.send(new DatagramPacket(out_buf, size, addr, port)); if (TRACE_DISCOVERY) { Map<String,String> map = null; switch (out_buf[4]) { case CONF_PEER_INFO: map = parsePeerAtrributes(out_buf, size); break; case CONF_SLAVES_INFO: map = parseIDs(out_buf, size); break; case CONF_PEERS_REMOVED: map = parseIDs(out_buf, size); break; } traceDiscoveryPacket(false, packetTypes[out_buf[4]], map, addr, port); } } catch (Exception x) { log("Cannot send datagram packet to " + addr, x); return false; } return true; } /** * Parse peer attributes in CONF_PEER_INFO packet data. * * @param data - the packet data * @param size - the packet size * @return a map containing the attributes * @throws UnsupportedEncodingException */ private static Map<String,String> parsePeerAtrributes(byte[] data, int size) throws UnsupportedEncodingException { Map<String,String> map = new HashMap<String,String>(); String s = new String(data, 8, size - 8, "UTF-8"); int l = s.length(); int i = 0; while (i < l) { int i0 = i; while (i < l && s.charAt(i) != '=' && s.charAt(i) != 0) i++; int i1 = i; if (i < l && s.charAt(i) == '=') i++; int i2 = i; while (i < l && s.charAt(i) != 0) i++; int i3 = i; if (i < l && s.charAt(i) == 0) i++; String key = s.substring(i0, i1); String val = s.substring(i2, i3); map.put(key, val); } return map; } /** * Parse list of IDs in CONF_SLAVES_INFO and CONF_PEERS_REMOVED packet data. * * @param data - the packet data * @param size - the packet size * @return a map containing the IDs * @throws UnsupportedEncodingException */ private static Map<String,String> parseIDs(byte[] data, int size) throws UnsupportedEncodingException { int cnt = 0; Map<String,String> map = new HashMap<String,String>(); String s = new String(data, 8, size - 8, "UTF-8"); int l = s.length(); int i = 0; while (i < l) { int i0 = i; while (i < l && s.charAt(i) != 0) i++; if (i > i0) { String id = s.substring(i0, i); map.put(Integer.toString(cnt++), id); } while (i < l && s.charAt(i) == 0) i++; } return map; } /** * Sends a Peer Request to the given address and port. * If address is null, broadcast on all subnets. * @param addr - IP address to send the request, or null * @param port - IP port number to send the request */ private void sendPeersRequest(InetAddress addr, int port) { out_buf[4] = CONF_REQ_INFO; for (SubNet subnet : subnets) { sendDatagramPacket(subnet, 8, addr, port); } } /** * Send peer info to the given address and port. * If address is null, broadcast on all subnets. * @param peer - a peer info to send * @param addr - IP address to send the info, or null * @param port - IP port number to send the info */ private void sendPeerInfo(IPeer peer, InetAddress addr, int port) { if (peer instanceof UserDefPeer) return; Map<String,String> attrs = peer.getAttributes(); InetAddress peer_addr = getInetAddress(attrs.get(IPeer.ATTR_IP_HOST)); if (peer_addr == null) return; if (attrs.get(IPeer.ATTR_IP_PORT) == null) return; out_buf[4] = CONF_PEER_INFO; int i = 8; for (SubNet subnet : subnets) { if (peer instanceof RemotePeer) { if (socket.getLocalPort() != DISCOVERY_PORT) return; if (!subnet.address.equals(loopback_addr) && !subnet.address.equals(peer_addr)) continue; } if (!subnet.address.equals(loopback_addr)) { if (!subnet.contains(peer_addr)) continue; } if (i == 8) { StringBuffer sb = new StringBuffer(out_buf.length); for (String key : attrs.keySet()) { sb.append(key); sb.append('='); sb.append(attrs.get(key)); sb.append((char)0); } byte[] bt = getUTF8Bytes(sb.toString()); if (i + bt.length > out_buf.length) return; System.arraycopy(bt, 0, out_buf, i, bt.length); i += bt.length; } if (sendDatagramPacket(subnet, i, addr, port)) subnet.send_all_ok = true; } } /** * Sends a CONF_SLAVES_INFO empty packet to the given address/port * @param addr IP address of the peer * @param port port number of the peer */ private void sendEmptyPacket(InetAddress addr, int port) { out_buf[4] = CONF_SLAVES_INFO; for (SubNet subnet : subnets) { if (subnet.send_all_ok) continue; sendDatagramPacket(subnet, 8, addr, port); } } /** * Sends a packet to * @param addr IP address of the slave/peer * @param port port number of the slave/peer * @param sl slave to which the packet is going to be sent to * @param time */ private void sendAll(InetAddress addr, int port, Slave sl, long time) { for (SubNet subnet : subnets) subnet.send_all_ok = false; for (IPeer peer : peers.values()) sendPeerInfo(peer, addr, port); if (addr != null && sl != null && sl.last_req_slaves_time + DATA_RETENTION_PERIOD >= time) { sendSlavesInfo(addr, port, time); } sendEmptyPacket(addr, port); } /** * Sends a CONF_REQ_SLAVES packet to the given address and port number in the given subnet * @param subnet subnet in which the address belongs to * @param addr IP address of the slave * @param port port number of the slave */ private void sendSlavesRequest(SubNet subnet, InetAddress addr, int port) { out_buf[4] = CONF_REQ_SLAVES; sendDatagramPacket(subnet, 8, addr, port); } /** * Sends a CONF_SLAVES_INFO packet to the given slave with the given timestamp * @param x slave * @param time */ private void sendSlaveInfo(Slave x, long time) { int ttl = (int)(x.last_packet_time + DATA_RETENTION_PERIOD - time); if (ttl <= 0) return; out_buf[4] = CONF_SLAVES_INFO; for (SubNet subnet : subnets) { if (!subnet.contains(x.address)) continue; int i = 8; String s = ttl + ":" + x.port + ":" + x.address.getHostAddress(); byte[] bt = getUTF8Bytes(s); System.arraycopy(bt, 0, out_buf, i, bt.length); i += bt.length; out_buf[i++] = 0; for (Slave y : slaves) { if (!subnet.contains(y.address)) continue; if (y.last_req_slaves_time + DATA_RETENTION_PERIOD < time) continue; sendDatagramPacket(subnet, i, y.address, y.port); } } } /** * Sends a CONF_SLAVES_INFO packet to the given address, port number with the given timestamp * @param addr IP Address of slave * @param port Port number of slave * @param time timestamp of */ private void sendSlavesInfo(InetAddress addr, int port, long time) { out_buf[4] = CONF_SLAVES_INFO; for (SubNet subnet : subnets) { if (!subnet.contains(addr)) continue; int i = 8; for (Slave x : slaves) { int ttl = (int)(x.last_packet_time + DATA_RETENTION_PERIOD - time); if (ttl <= 0) continue; if (x.port == port && x.address.equals(addr)) continue; if (!subnet.address.equals(loopback_addr)) { if (!subnet.contains(x.address)) continue; } subnet.send_all_ok = true; String s = ttl + ":" + x.port + ":" + x.address.getHostAddress(); byte[] bt = getUTF8Bytes(s); if (i > 8 && i + bt.length >= PREF_PACKET_SIZE) { sendDatagramPacket(subnet, i, addr, port); i = 8; } System.arraycopy(bt, 0, out_buf, i, bt.length); i += bt.length; out_buf[i++] = 0; } if (i > 8) sendDatagramPacket(subnet, i, addr, port); } } /** * Checks if the the address and port are from a RemotePeer * @param address IP address of peer * @param port port number of peer * @return true is the address and port are not from a Remote Peer but instead is the Local Peer, else false */ private boolean isRemote(InetAddress address, int port) { if (port != socket.getLocalPort()) return true; for (SubNet s : subnets) { if (s.address.equals(address)) return false; } return true; } /** * Handles packets received during the auto discovery. * It verifies the packet is indeed a TCF packet, and handles it depending on the auto-configuration command * and responses codes e.g: CONF_PEER_INFO, * CONF_REQ_INFO, etc.. * * @param p packet received */ private void handleDatagramPacket(InputPacket p) { try { long time = System.currentTimeMillis(); byte[] buf = p.getData(); int len = p.getLength(); if (len < 8) return; if (buf[0] != 'T') return; if (buf[1] != 'C') return; if (buf[2] != 'F') return; if (buf[3] != CONF_VERSION) return; int remote_port = p.getPort(); InetAddress remote_address = p.getAddress(); if (isRemote(remote_address, remote_port)) { if (buf[4] == CONF_PEERS_REMOVED) { handlePeerRemovedPacket(p, remote_port == DISCOVERY_PORT && remote_address.isLoopbackAddress()); } else { Slave sl = null; if (remote_port != DISCOVERY_PORT) { sl = addSlave(remote_address, remote_port, time); } switch (buf[4]) { case CONF_PEER_INFO: handlePeerInfoPacket(p); break; case CONF_REQ_INFO: handleReqInfoPacket(p, sl, time); break; case CONF_SLAVES_INFO: handleSlavesInfoPacket(p, time); break; case CONF_REQ_SLAVES: handleReqSlavesPacket(p, sl, time); break; } for (SubNet subnet : subnets) { if (!subnet.contains(remote_address)) continue; long delay = DATA_RETENTION_PERIOD / 3; if (remote_port != DISCOVERY_PORT) delay = DATA_RETENTION_PERIOD / 3 * 2; else if (!subnet.address.equals(remote_address)) delay = DATA_RETENTION_PERIOD / 2; if (subnet.last_slaves_req_time + delay <= time) { sendSlavesRequest(subnet, remote_address, remote_port); subnet.last_slaves_req_time = time; } if (remote_port == DISCOVERY_PORT && subnet.address.equals(remote_address)) { last_master_packet_time = time; } } } } } catch (Throwable x) { log("Invalid datagram packet received from " + p.getAddress() + "/" + p.getPort(), x); } } /** * Handles a received CONF_PEER_INFO packet * @param p packet received */ private void handlePeerInfoPacket(InputPacket p) { try { Map<String,String> map = parsePeerAtrributes(p.getData(), p.getLength()); if (TRACE_DISCOVERY) traceDiscoveryPacket(true, "CONF_PEER_INFO", map, p); String id = map.get(IPeer.ATTR_ID); if (id == null) throw new Exception("Invalid peer info: no ID"); boolean ok = true; String host = map.get(IPeer.ATTR_IP_HOST); if (host != null) { ok = false; InetAddress peer_addr = getInetAddress(host); if (peer_addr != null) { for (SubNet subnet : subnets) { if (subnet.contains(peer_addr)) { ok = true; break; } } } } if (ok) { IPeer peer = peers.get(id); if (peer instanceof RemotePeer) { ((RemotePeer)peer).updateAttributes(map); } else if (peer == null) { new RemotePeer(map); } } } catch (Exception x) { log("Invalid datagram packet received from " + p.getAddress() + "/" + p.getPort(), x); } } /** * Handles a CONF_REQ_INFO packet received from the given slave, at the given time * It sendsAll * @param p packet received * @param sl slave * @param time timestamp at which the packet was received */ private void handleReqInfoPacket(InputPacket p, Slave sl, long time) { if (TRACE_DISCOVERY) traceDiscoveryPacket(true, "CONF_REQ_INFO", null, p); sendAll(p.getAddress(), p.getPort(), sl, time); } /** * Handles a received CONF_SLAVES_INFO * @param p packet received * @param time_now timestamp when the packet was received */ private void handleSlavesInfoPacket(InputPacket p, long time_now) { try { Map<String,String> map = parseIDs(p.getData(), p.getLength()); if (TRACE_DISCOVERY) traceDiscoveryPacket(true, "CONF_SLAVES_INFO", map, p); for (String s : map.values()) { int i = 0; int l = s.length(); int time0 = i; while (i < l&& s.charAt(i) != ':' && s.charAt(i) != 0) i++; int time1 = i; if (i < l && s.charAt(i) == ':') i++; int port0 = i; while (i < l&& s.charAt(i) != ':' && s.charAt(i) != 0) i++; int port1 = i; if (i < l && s.charAt(i) == ':') i++; int host0 = i; while (i < l && s.charAt(i) != 0) i++; int host1 = i; int port = Integer.parseInt(s.substring(port0, port1)); String timestamp = s.substring(time0, time1); String host = s.substring(host0, host1); if (port != DISCOVERY_PORT) { InetAddress addr = getInetAddress(host); if (addr != null) { long delta = 1000 * 60 * 30; // 30 minutes long time_val = timestamp.length() > 0 ? Long.parseLong(timestamp) : time_now; if (time_val < 3600000) { /* Time stamp is "time to live" in milliseconds */ time_val = time_now + time_val - DATA_RETENTION_PERIOD; } else if (time_val < time_now / 1000 + 50000000) { /* Time stamp is in seconds */ time_val *= 1000; } else { /* Time stamp is in milliseconds */ } if (time_val < time_now - delta || time_val > time_now + delta) { SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); String msg = "Invalid slave info timestamp: " + timestamp + " -> " + fmt.format(new Date(time_val)); log("Invalid datagram packet received from " + p.getAddress() + "/" + p.getPort(), new Exception(msg)); time_val = time_now - DATA_RETENTION_PERIOD / 2; } addSlave(addr, port, time_val); } } } } catch (Exception x) { log("Invalid datagram packet received from " + p.getAddress() + "/" + p.getPort(), x); } } /** * Handles CONF_REQ_SLAVES packet * SendsSlavesInfo * @param p packet received * @param sl slave from which the packet was received * @param time timestamp of when the packet was received */ private void handleReqSlavesPacket(InputPacket p, Slave sl, long time) { if (TRACE_DISCOVERY) traceDiscoveryPacket(true, "CONF_REQ_SLAVES", null, p); if (sl != null) sl.last_req_slaves_time = time; sendSlavesInfo(p.getAddress(), p.getPort(), time); } /** * Handles a received CONF_PEER_REMOVED packet * @param p packet received * @param master_exited true if the master locator was removed, false if it was a slave locator */ private void handlePeerRemovedPacket(InputPacket p, boolean master_exited) { try { Map<String,String> map = parseIDs(p.getData(), p.getLength()); if (TRACE_DISCOVERY) traceDiscoveryPacket(true, "CONF_PEERS_REMOVED", map, p); for (String id : map.values()) { IPeer peer = peers.get(id); if (peer instanceof RemotePeer) ((RemotePeer)peer).dispose(); } if (master_exited) { // Master locator has exited, let's try to get master port. Protocol.invokeLater(500, new Runnable() { public void run() { if (socket.getLocalPort() == DISCOVERY_PORT) return; try { DatagramSocket s0 = socket; socket = createSocket(false); if (TRACE_DISCOVERY) { LoggingUtil.trace("Became the master agent (bound to port " + socket.getLocalPort() + ")"); } s0.close(); } catch (Throwable x) { } } }); } } catch (Exception x) { log("Invalid datagram packet received from " + p.getAddress() + "/" + p.getPort(), x); } } /*----------------------------------------------------------------------------------*/ public static LocatorService getLocator() { return locator; } public String getName() { return NAME; } public Map<String,IPeer> getPeers() { assert Protocol.isDispatchThread(); return peers; } public IToken redirect(String peer_id, DoneRedirect done) { throw new Error("Channel redirect cannot be done on local peer"); } public IToken redirect(Map<String,String> peer, DoneRedirect done) { throw new Error("Channel redirect cannot be done on local peer"); } public IToken sync(DoneSync done) { throw new Error("Channel sync cannot be done on local peer"); } public IToken getAgentID(DoneGetAgentID done) { throw new Error("Channel get agent ID cannot be done on local peer"); } public void addListener(LocatorListener listener) { assert listener != null; assert Protocol.isDispatchThread(); listeners.add(listener); } public void removeListener(LocatorListener listener) { assert Protocol.isDispatchThread(); listeners.remove(listener); } /** * Log that a TCF Discovery packet has be sent or received. The trace is * sent to stdout. This should be called only if the tracing has been turned * on via java property definitions. * * @param received false if the packet was sent, otherwise it was received * @param type a string specifying the type of packet, e.g., "CONF_PEER_INFO" * @param attrs a set of attributes relevant to the type of packet (typically a peer's attributes) * @param addr the network address the packet is being sent to * @param port the port the packet is being sent to */ private static void traceDiscoveryPacket(boolean received, String type, Map<String,String> attrs, InetAddress addr, int port) { assert TRACE_DISCOVERY; StringBuilder str = new StringBuilder(type + (received ? " received from " : " sent to ") + addr + "/" + port); if (attrs != null) { Iterator<Entry<String, String>> iter = attrs.entrySet().iterator(); while (iter.hasNext()) { Entry<String, String> entry = iter.next(); str.append("\n\t" + entry.getKey() + '=' + entry.getValue()); } } LoggingUtil.trace(str.toString()); } /** * Convenience variant that takes a DatagramPacket for specifying * the target address and port. * * @param received false if the packet was sent, otherwise it was received * @param type a string specifying the type of packet, e.g., "CONF_PEER_INFO" * @param attrs a set of attributes relevant to the type of packet (typically a peer's attributes) * @param packet packet received */ private static void traceDiscoveryPacket(boolean received, String type, Map<String,String> attrs, InputPacket packet) { traceDiscoveryPacket(received, type, attrs, packet.getAddress(), packet.getPort()); } }