/** * Copyright (C) 2007 Jive Software. All rights reserved. * * This software is published under the terms of the GNU Public License (GPL), * a copy of which is included in this distribution. */ package org.jivesoftware.openfire.stun; import de.javawi.jstun.test.demo.StunServer; import org.dom4j.Element; import org.jivesoftware.openfire.IQHandlerInfo; import org.jivesoftware.openfire.XMPPServer; import org.jivesoftware.openfire.auth.UnauthorizedException; import org.jivesoftware.openfire.container.BasicModule; import org.jivesoftware.openfire.handler.IQHandler; import org.jivesoftware.util.JiveGlobals; import org.jivesoftware.util.Log; import org.jivesoftware.util.PropertyEventDispatcher; import org.jivesoftware.util.PropertyEventListener; import org.xmpp.packet.IQ; import org.xmpp.packet.PacketError; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; import java.util.Map; /** * STUN server and service module. It provides address discovery for p2p sessions to be * used for media transmission and receiving of UDP packets. It's especially useful for * clients behind NAT devices. * * @author Thiago Camargo */ public class STUNService extends BasicModule { private static final String ELEMENT_NAME = "stun"; private static final String NAMESPACE = "google:jingleinfo"; private static final String DEFAULT_EXTERNAL_ADDRESSES = "stun.xten.net:3478;" + "jivesoftware.com:3478;" + "igniterealtime.org:3478;" + "stun.fwdnet.net:3478"; private IQHandler stunIQHandler; private StunServer stunServer = null; private boolean enabled = false; private boolean localEnabled = false; private String primaryAddress = null; private String secondaryAddress = null; private int primaryPort; private int secondaryPort; private List<StunServerAddress> externalServers = null; /** * Constructs a new STUN Service */ public STUNService() { super("STUN Service"); } public void destroy() { super.destroy(); stunServer = null; } public void initialize(XMPPServer server) { super.initialize(server); this.enabled = JiveGlobals.getBooleanProperty("stun.enabled", true); primaryAddress = JiveGlobals.getProperty("stun.address.primary"); secondaryAddress = JiveGlobals.getProperty("stun.address.secondary"); String addresses = JiveGlobals.getProperty("stun.external.addresses"); // If no custom external addresses are defined, use the defaults. if (addresses == null) { addresses = DEFAULT_EXTERNAL_ADDRESSES; } externalServers = getStunServerAddresses(addresses); primaryPort = JiveGlobals.getIntProperty("stun.port.primary", 3478); secondaryPort = JiveGlobals.getIntProperty("stun.port.secondary", 3479); this.localEnabled = JiveGlobals.getBooleanProperty("stun.local.enabled", false); // If the local server is supposed to be enabled, ensure that primary and secondary // addresses are defined. if (localEnabled) { if (primaryAddress == null || secondaryAddress == null) { Log.warn("STUN server cannot be enabled. Primary and secondary addresses must be defined."); localEnabled = false; } } // Add listeners for STUN service being enabled and disabled via manual property changes. PropertyEventDispatcher.addListener(new PropertyEventListener() { public void propertySet(String property, Map<String, Object> params) { if (property.equals("stun.enabled")) { boolean oldValue = enabled; enabled = JiveGlobals.getBooleanProperty("stun.enabled", true); // if (enabled && !oldValue) { startSTUNService(); } else if (!enabled && oldValue) { stop(); } } else if (property.equals("stun.local.enabled")) { localEnabled = JiveGlobals.getBooleanProperty("stun.local.enabled", false); } } public void propertyDeleted(String property, Map<String, Object> params) { if (property.equals("stun.enabled")) { enabled = true; } else if (property.equals("stun.local.enabled")) { localEnabled = false; } } public void xmlPropertySet(String property, Map<String, Object> params) { // Ignore. } public void xmlPropertyDeleted(String property, Map<String, Object> params) { // Ignore. } }); } public void start() { if (isEnabled()) { startSTUNService(); if (isLocalEnabled()) { startLocalServer(); } } } private void startLocalServer() { try { InetAddress primary = InetAddress.getByName(primaryAddress); InetAddress secondary = InetAddress.getByName(secondaryAddress); if (primary != null && secondary != null) { stunServer = new StunServer(primaryPort, primary, secondaryPort, secondary); stunServer.start(); } else { setLocalEnabled(false); } } catch (SocketException e) { Log.error("Disabling STUN server", e); setLocalEnabled(false); } catch (UnknownHostException e) { Log.error("Disabling STUN server", e); setLocalEnabled(false); } } private void startSTUNService() { XMPPServer server = XMPPServer.getInstance(); // Register the STUN feature in disco. server.getIQDiscoInfoHandler().addServerFeature(NAMESPACE); // Add an IQ handler. stunIQHandler = new STUNIQHandler(); server.getIQRouter().addHandler(stunIQHandler); } private void stopSTUNService() { XMPPServer server = XMPPServer.getInstance(); server.getIQDiscoInfoHandler().removeServerFeature(NAMESPACE); if (stunIQHandler != null) { server.getIQRouter().removeHandler(stunIQHandler); stunIQHandler = null; } } public void stop() { super.stop(); this.enabled = false; stopSTUNService(); stopLocal(); } private void stopLocal() { if (stunServer != null) { stunServer.stop(); } stunServer = null; } public String getName() { return "stun"; } public List<StunServerAddress> getExternalServers() { return externalServers; } public void addExternalServer(String server, String port) { externalServers.add(new StunServerAddress(server, port)); String property = ""; for (StunServerAddress stunServerAddress : externalServers) { if (!property.equals("")) { property += ";"; } property += stunServerAddress.getServer() + ":" + stunServerAddress.getPort(); } JiveGlobals.setProperty("stun.external.addresses", property); } public void removeExternalServer(int index) { externalServers.remove(index); String property = ""; for (StunServerAddress stunServerAddress : externalServers) { if (!property.equals("")) { property += ";"; } property += stunServerAddress.getServer() + ":" + stunServerAddress.getPort(); } JiveGlobals.setProperty("stun.external.addresses", property); } /** * Returns true if the service is enabled. * * @return enabled */ public boolean isEnabled() { return enabled; } /** * Returns true if the local STUN server is enabled. * * @return enabled */ public boolean isLocalEnabled() { return localEnabled; } /** * Set the service enable status. * * @param enabled boolean to enable or disable * @param localEnabled local Server enable or disable */ public void setEnabled(boolean enabled, boolean localEnabled) { if (enabled && !this.enabled) { startSTUNService(); if (isLocalEnabled()) { startLocalServer(); } } else { stopSTUNService(); } this.enabled = enabled; this.localEnabled = localEnabled; } /** * Set the Local STUN Server enable status. * * @param enabled boolean to enable or disable */ public void setLocalEnabled(boolean enabled) { this.localEnabled = enabled; if (isLocalEnabled()) { startLocalServer(); } else { stopLocal(); } } /** * Get the secondary Port used by the STUN server * * @return secondary Port used by the STUN server */ public int getSecondaryPort() { return secondaryPort; } /** * Get the primary Port used by the STUN server * * @return primary Port used by the STUN server */ public int getPrimaryPort() { return primaryPort; } /** * Get the secondary Address used by the STUN server * * @return secondary Address used by the STUN server */ public String getSecondaryAddress() { return secondaryAddress; } /** * Get the primary Address used by the STUN server * * @return primary Address used by the STUN server */ public String getPrimaryAddress() { return primaryAddress; } public List<InetAddress> getAddresses() { List<InetAddress> list = new ArrayList<InetAddress>(); try { Enumeration<NetworkInterface> ifaces = NetworkInterface.getNetworkInterfaces(); while (ifaces.hasMoreElements()) { NetworkInterface iface = ifaces.nextElement(); Enumeration<InetAddress> iaddresses = iface.getInetAddresses(); while (iaddresses.hasMoreElements()) { InetAddress iaddress = iaddresses.nextElement(); if (!iaddress.isLoopbackAddress() && !iaddress.isLinkLocalAddress()) { list.add(iaddress); } } } } catch (Exception e) { // Do Nothing } return list; } /** * Abstraction method used to convert a String into a STUN Server Address List * * @param addresses the String representation of server addresses with their * respective ports (server1:port1;server2:port2). * @return STUN server addresses list. */ private List<StunServerAddress> getStunServerAddresses(String addresses) { List<StunServerAddress> list = new ArrayList<StunServerAddress>(); if (addresses.equals("")) { return list; } String servers[] = addresses.split(";"); for (String server : servers) { String address[] = server.split(":"); StunServerAddress aux = new StunServerAddress(address[0], address[1]); if (!list.contains(aux)) { list.add(aux); } } return list; } /** * An IQ handler for STUN requests. */ private class STUNIQHandler extends IQHandler { public STUNIQHandler() { super("stun"); } public IQ handleIQ(IQ iq) throws UnauthorizedException { IQ reply = IQ.createResultIQ(iq); Element childElement = iq.getChildElement(); String namespace = childElement.getNamespaceURI(); Element childElementCopy = iq.getChildElement().createCopy(); reply.setChildElement(childElementCopy); if (NAMESPACE.equals(namespace)) { if (isEnabled()) { Element stun = childElementCopy.addElement("stun"); // If the local server is configured as a STUN server, send it as the first item. if (isLocalEnabled()) { StunServerAddress local; local = new StunServerAddress(primaryAddress, String.valueOf(primaryPort)); if (!externalServers.contains(local)) { Element server = stun.addElement("server"); server.addAttribute("host", local.getServer()); server.addAttribute("udp", local.getPort()); } } // Add any external STUN servers that are specified. for (StunServerAddress stunServerAddress : externalServers) { Element server = stun.addElement("server"); server.addAttribute("host", stunServerAddress.getServer()); server.addAttribute("udp", stunServerAddress.getPort()); } try { String ip = sessionManager.getSession(iq.getFrom()).getHostAddress(); if (ip != null) { Element publicIp = childElementCopy.addElement("publicip"); publicIp.addAttribute("ip", ip); } } catch (UnknownHostException e) { e.printStackTrace(); } } } else { // Answer an error since the server can't handle the requested namespace reply.setError(PacketError.Condition.service_unavailable); } try { Log.debug("RETURNED:" + reply.toXML()); } catch (Exception e) { Log.error(e); } return reply; } public IQHandlerInfo getInfo() { return new IQHandlerInfo(ELEMENT_NAME, NAMESPACE); } } }