/**
 * Copyright (C) 2008 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, or a commercial license
 * agreement with Jive.
 */

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) {
                        Log.error(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);
            }
            return reply;
        }

        public IQHandlerInfo getInfo() {
            return new IQHandlerInfo(ELEMENT_NAME, NAMESPACE);
        }
    }
}