/** * Transport resolver using the JSTUN library, to discover public IP and use it as a candidate. * * <p>The goal of this resolver is to take possible to establish and manage out-of-band connections * between two XMPP entities, even if they are behind Network Address Translators (NATs) or * firewalls. * * @author Thiago Camargo */ public class STUNResolver extends TransportResolver { /** STUN service definition. */ protected class STUNService { private String hostname; // The hostname of the service private int port; // The port number /** Default constructor, without name and port. */ public STUNService() { this(null, -1); } /** * Basic constructor, with the hostname and port * * @param hostname The hostname * @param port The port */ public STUNService(String hostname, int port) { super(); this.hostname = hostname; this.port = port; } /** * Check a binding with the STUN currentServer. * * <p>Note: this function blocks for some time, waiting for a response. * * @return true if the currentServer is usable. */ public boolean checkBinding() { final boolean result = false; try { final BindingLifetimeTest binding = new BindingLifetimeTest(hostname, port); binding.test(); while (true) { Thread.sleep(5000); if (binding.getLifetime() != -1) { if (binding.isCompleted()) { return true; } } else { break; } } } catch (final Exception e) { LOGGER.error(e.getMessage(), e); } return result; } /** * Get the host name of the STUN service. * * @return The host name */ public String getHostname() { return hostname; } /** * Get the port of the STUN service * * @return The port number where the STUN server is waiting. */ public int getPort() { return port; } /** * Basic format test: the service is not null. * * @return true if the hostname and port are null */ public boolean isNull() { if (hostname == null) { return true; } else if (hostname.length() == 0) { return true; } else if (port < 0) { return true; } else { return false; } } /** * Set the hostname of the STUN service. * * @param hostname The host name of the service. */ public void setHostname(String hostname) { this.hostname = hostname; } /** * Set the port number for the STUN service. * * @param port The port number. */ public void setPort(int port) { this.port = port; } } private static final SmackLogger LOGGER = SmackLogger.getLogger(STUNResolver.class); // The filename where the STUN servers are stored. public static final String STUNSERVERS_FILENAME = "META-INF/stun-config.xml"; // Current STUN server we are using protected STUNService currentServer; protected Thread resolverThread; protected int defaultPort; protected String resolvedPublicIP; protected String resolvedLocalIP; /** Constructor with default STUN server. */ public STUNResolver() { super(); defaultPort = 0; currentServer = new STUNService(); } /** * Constructor with a default port. * * @param defaultPort Port to use by default. */ public STUNResolver(int defaultPort) { this(); this.defaultPort = defaultPort; } /** * Get the best usable STUN server from a list. * * @return the best STUN server that can be used. */ private STUNService bestSTUNServer(ArrayList listServers) { if (listServers.isEmpty()) { return null; } else { // TODO: this should use some more advanced criteria... return (STUNService) listServers.get(0); } } /** * Cancel any operation. * * @see TransportResolver#cancel() */ @Override public synchronized void cancel() throws XMPPException { if (isResolving()) { resolverThread.interrupt(); setResolveEnd(); } } /** * Clear the list of candidates and start the resolution again. * * @see TransportResolver#clear() */ @Override public synchronized void clear() throws XMPPException { defaultPort = 0; super.clear(); } /** * Get the name of the current STUN server. * * @return the name of the STUN server */ public String getCurrentServerName() { if (!currentServer.isNull()) { return currentServer.getHostname(); } else { return null; } } /** * Get the port of the current STUN server. * * @return the port of the STUN server */ public int getCurrentServerPort() { if (!currentServer.isNull()) { return currentServer.getPort(); } else { return 0; } } /** * Initialize the resolver. * * @throws XMPPException */ @Override public void initialize() throws XMPPException { LOGGER.debug("Initialized"); if (!isResolving() && !isResolved()) { // Get the best STUN server available if (currentServer.isNull()) { loadSTUNServers(); } // We should have a valid STUN server by now... if (!currentServer.isNull()) { clearCandidates(); resolverThread = new Thread( new Runnable() { @Override public void run() { // Iterate through the list of interfaces, and ask // to the STUN server for our address. try { final Enumeration ifaces = NetworkInterface.getNetworkInterfaces(); String candAddress; int candPort; while (ifaces.hasMoreElements()) { final NetworkInterface iface = (NetworkInterface) ifaces.nextElement(); final Enumeration iaddresses = iface.getInetAddresses(); while (iaddresses.hasMoreElements()) { final InetAddress iaddress = (InetAddress) iaddresses.nextElement(); if (!iaddress.isLoopbackAddress() && !iaddress.isLinkLocalAddress()) { // Reset the candidate candAddress = null; candPort = -1; final DiscoveryTest test = new DiscoveryTest( iaddress, currentServer.getHostname(), currentServer.getPort()); try { // Run the tests and get the // discovery // information, where all the // info is stored... final DiscoveryInfo di = test.test(); candAddress = di.getPublicIP() != null ? di.getPublicIP().getHostAddress() : null; // Get a valid port if (defaultPort == 0) { candPort = getFreePort(); } else { candPort = defaultPort; } // If we have a valid candidate, // add it to the list. if (candAddress != null && candPort >= 0) { final TransportCandidate candidate = new TransportCandidate.Fixed(candAddress, candPort); candidate.setLocalIp( iaddress.getHostAddress() != null ? iaddress.getHostAddress() : iaddress.getHostName()); addCandidate(candidate); resolvedPublicIP = candidate.getIp(); resolvedLocalIP = candidate.getLocalIp(); return; } } catch (final Exception e) { LOGGER.error(e.getMessage(), e); } } } } } catch (final SocketException e) { LOGGER.error(e.getMessage(), e); } finally { setInitialized(); } } }, "Waiting for all the transport candidates checks..."); resolverThread.setName("STUN resolver"); resolverThread.start(); } else { throw new IllegalStateException("No valid STUN server found."); } } } /** * Return true if the service is working. * * @see TransportResolver#isResolving() */ @Override public boolean isResolving() { return super.isResolving() && resolverThread != null; } /** * Load a list of services: STUN servers and ports. Some public STUN servers are: * * <p> * * <pre> * iphone-stun.freenet.de:3478 * larry.gloo.net:3478 * stun.xten.net:3478 * stun.fwdnet.net * stun.fwd.org (no DNS SRV record) * stun01.sipphone.com (no DNS SRV record) * stun.softjoys.com (no DNS SRV record) * stun.voipbuster.com (no DNS SRV record) * stun.voxgratia.org (no DNS SRV record) * stun.noc.ams-ix.net * </pre> * * <p>This list should be contained in a file in the "META-INF" directory * * @return a list of services */ public ArrayList loadSTUNServers() { final ArrayList serversList = new ArrayList(); // Load the STUN configuration try { // Get an array of class loaders to try loading the config from. final ClassLoader[] classLoaders = new ClassLoader[2]; classLoaders[0] = new STUNResolver() {}.getClass().getClassLoader(); classLoaders[1] = Thread.currentThread().getContextClassLoader(); for (final ClassLoader classLoader : classLoaders) { final Enumeration stunConfigEnum = classLoader.getResources(STUNSERVERS_FILENAME); while (stunConfigEnum.hasMoreElements() && serversList.isEmpty()) { final URL url = (URL) stunConfigEnum.nextElement(); java.io.InputStream stunConfigStream = null; stunConfigStream = url.openStream(); serversList.addAll(loadSTUNServers(stunConfigStream)); stunConfigStream.close(); } } } catch (final Exception e) { LOGGER.error(e.getMessage(), e); } return serversList; } /** * Load the STUN configuration from a stream. * * @param stunConfigStream An InputStream with the configuration file. * @return A list of loaded servers */ public ArrayList loadSTUNServers(java.io.InputStream stunConfigStream) { final ArrayList serversList = new ArrayList(); String serverName; int serverPort; try { final XmlPullParser parser = new MXParser(); parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); parser.setInput(stunConfigStream, "UTF-8"); int eventType = parser.getEventType(); do { if (eventType == XmlPullParser.START_TAG) { // Parse a STUN server definition if (parser.getName().equals("stunServer")) { serverName = null; serverPort = -1; // Parse the hostname parser.next(); parser.next(); serverName = parser.nextText(); // Parse the port parser.next(); parser.next(); try { serverPort = Integer.parseInt(parser.nextText()); } catch (final Exception e) { } // If we have a valid hostname and port, add // it to the list. if (serverName != null && serverPort != -1) { final STUNService service = new STUNService(serverName, serverPort); serversList.add(service); } } } eventType = parser.next(); } while (eventType != XmlPullParser.END_DOCUMENT); } catch (final XmlPullParserException e) { LOGGER.error(e.getMessage(), e); } catch (final IOException e) { LOGGER.error(e.getMessage(), e); } currentServer = bestSTUNServer(serversList); return serversList; } /** Resolve the IP and obtain a valid transport method. */ @Override public synchronized void resolve(JingleSession session) throws XMPPException { setResolveInit(); clearCandidates(); final TransportCandidate candidate = new TransportCandidate.Fixed(resolvedPublicIP, getFreePort()); candidate.setLocalIp(resolvedLocalIP); LOGGER.debug("RESOLVING : " + resolvedPublicIP + ":" + candidate.getPort()); addCandidate(candidate); setResolveEnd(); } /** * Set the STUN server name and port * * @param ip the STUN server name * @param port the STUN server port */ public void setSTUNService(String ip, int port) { currentServer = new STUNService(ip, port); } }
/** * An image Quantizer based on the Octree algorithm. This is a very basic implementation at present * and could be much improved by picking the nodes to reduce more carefully (i.e. not completely at * random) when I get the time. */ public class OctTreeQuantizer implements Quantizer { private static final SmackLogger LOGGER = SmackLogger.getLogger(OctTreeQuantizer.class); /** The greatest depth the tree is allowed to reach */ static final int MAX_LEVEL = 5; /** An Octtree node. */ class OctTreeNode { int children; int level; OctTreeNode parent; OctTreeNode leaf[] = new OctTreeNode[8]; boolean isLeaf; int count; int totalRed; int totalGreen; int totalBlue; int index; /** A debugging method which prints the tree out. */ public void list(PrintStream s, int level) { String indentStr = ""; for (int i = 0; i < level; i++) indentStr += " "; if (count == 0) LOGGER.debug(indentStr + index + ": count=" + count); else LOGGER.debug( indentStr + index + ": count=" + count + " red=" + (totalRed / count) + " green=" + (totalGreen / count) + " blue=" + (totalBlue / count)); for (int i = 0; i < 8; i++) if (leaf[i] != null) leaf[i].list(s, level + 2); } } private int nodes = 0; private OctTreeNode root; private int reduceColors; private int maximumColors; private int colors = 0; private Vector[] colorList; public OctTreeQuantizer() { setup(256); colorList = new Vector[MAX_LEVEL + 1]; for (int i = 0; i < MAX_LEVEL + 1; i++) colorList[i] = new Vector(); root = new OctTreeNode(); } /** * Initialize the quantizer. This should be called before adding any pixels. * * @param numColors the number of colors we're quantizing to. */ public void setup(int numColors) { maximumColors = numColors; reduceColors = Math.max(512, numColors * 2); } /** * Add pixels to the quantizer. * * @param pixels the array of ARGB pixels * @param offset the offset into the array * @param count the count of pixels */ public void addPixels(int[] pixels, int offset, int count) { for (int i = 0; i < count; i++) { insertColor(pixels[i + offset]); if (colors > reduceColors) reduceTree(reduceColors); } } /** * Get the color table index for a color. * * @param rgb the color * @return the index */ public int getIndexForColor(int rgb) { int red = (rgb >> 16) & 0xff; int green = (rgb >> 8) & 0xff; int blue = rgb & 0xff; OctTreeNode node = root; for (int level = 0; level <= MAX_LEVEL; level++) { OctTreeNode child; int bit = 0x80 >> level; int index = 0; if ((red & bit) != 0) index += 4; if ((green & bit) != 0) index += 2; if ((blue & bit) != 0) index += 1; child = node.leaf[index]; if (child == null) return node.index; else if (child.isLeaf) return child.index; else node = child; } LOGGER.debug("getIndexForColor failed"); return 0; } private void insertColor(int rgb) { int red = (rgb >> 16) & 0xff; int green = (rgb >> 8) & 0xff; int blue = rgb & 0xff; OctTreeNode node = root; // LOGGER.debug("insertColor="+Integer.toHexString(rgb)); for (int level = 0; level <= MAX_LEVEL; level++) { OctTreeNode child; int bit = 0x80 >> level; int index = 0; if ((red & bit) != 0) index += 4; if ((green & bit) != 0) index += 2; if ((blue & bit) != 0) index += 1; child = node.leaf[index]; if (child == null) { node.children++; child = new OctTreeNode(); child.parent = node; node.leaf[index] = child; node.isLeaf = false; nodes++; colorList[level].addElement(child); if (level == MAX_LEVEL) { child.isLeaf = true; child.count = 1; child.totalRed = red; child.totalGreen = green; child.totalBlue = blue; child.level = level; colors++; return; } node = child; } else if (child.isLeaf) { child.count++; child.totalRed += red; child.totalGreen += green; child.totalBlue += blue; return; } else node = child; } LOGGER.debug("insertColor failed"); } private void reduceTree(int numColors) { for (int level = MAX_LEVEL - 1; level >= 0; level--) { Vector v = colorList[level]; if (v != null && v.size() > 0) { for (int j = 0; j < v.size(); j++) { OctTreeNode node = (OctTreeNode) v.elementAt(j); if (node.children > 0) { for (int i = 0; i < 8; i++) { OctTreeNode child = node.leaf[i]; if (child != null) { if (!child.isLeaf) LOGGER.debug("not a leaf!"); node.count += child.count; node.totalRed += child.totalRed; node.totalGreen += child.totalGreen; node.totalBlue += child.totalBlue; node.leaf[i] = null; node.children--; colors--; nodes--; colorList[level + 1].removeElement(child); } } node.isLeaf = true; colors++; if (colors <= numColors) return; } } } } LOGGER.debug("Unable to reduce the OctTree"); } /** * Build the color table. * * @return the color table */ public int[] buildColorTable() { int[] table = new int[colors]; buildColorTable(root, table, 0); return table; } /** * A quick way to use the quantizer. Just create a table the right size and pass in the pixels. * * @param inPixels the input colors * @param table the output color table */ public void buildColorTable(int[] inPixels, int[] table) { int count = inPixels.length; maximumColors = table.length; for (int i = 0; i < count; i++) { insertColor(inPixels[i]); if (colors > reduceColors) reduceTree(reduceColors); } if (colors > maximumColors) reduceTree(maximumColors); buildColorTable(root, table, 0); } private int buildColorTable(OctTreeNode node, int[] table, int index) { if (colors > maximumColors) reduceTree(maximumColors); if (node.isLeaf) { int count = node.count; table[index] = 0xff000000 | ((node.totalRed / count) << 16) | ((node.totalGreen / count) << 8) | node.totalBlue / count; node.index = index++; } else { for (int i = 0; i < 8; i++) { if (node.leaf[i] != null) { node.index = index; index = buildColorTable(node.leaf[i], table, index); } } } return index; } }