/**
* Copyright (C) 2005-2008 Jive Software. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.openfire.stun;
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;
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.PropertyEventDispatcher;
import org.jivesoftware.util.PropertyEventListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.IQ;
import org.xmpp.packet.PacketError;
import de.javawi.jstun.test.demo.StunServer;
/**
* 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 Logger Log = LoggerFactory.getLogger(STUNService.class);
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");
}
@Override
public void destroy() {
super.destroy();
stunServer = null;
}
@Override
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.
}
});
}
@Override
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;
}
}
@Override
public void stop() {
super.stop();
this.enabled = false;
stopSTUNService();
stopLocal();
}
private void stopLocal() {
if (stunServer != null) {
stunServer.stop();
}
stunServer = null;
}
@Override
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");
}
@Override
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) {
Log.error(e.getMessage(), e);
}
}
} else {
// Answer an error since the server can't handle the requested namespace
reply.setError(PacketError.Condition.service_unavailable);
}
try {
Log.debug("STUNService: RETURNED:" + reply.toXML());
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
return reply;
}
@Override
public IQHandlerInfo getInfo() {
return new IQHandlerInfo(ELEMENT_NAME, NAMESPACE);
}
}
}