package net.java.sip.communicator.impl.netaddr;

import java.net.*;

import net.java.stun4j.*;
import net.java.stun4j.attribute.*;
import net.java.stun4j.message.*;
import net.java.stun4j.stack.*;
import net.java.sip.communicator.util.*;

/**
 * <p>
 * This class implements the STUN Discovery Process as described by section 10.1
 * of rfc 3489.
 * </p><p>
 * The flow makes use of three tests.  In test I, the client sends a
 * STUN Binding Request to a server, without any flags set in the
 * CHANGE-REQUEST attribute, and without the RESPONSE-ADDRESS attribute.
 * This causes the server to send the response back to the address and
 * port that the request came from.  In test II, the client sends a
 * Binding Request with both the "change IP" and "change port" flags
 * from the CHANGE-REQUEST attribute set.  In test III, the client sends
 * a Binding Request with only the "change port" flag set.
 * </p><p>
 * The client begins by initiating test I.  If this test yields no
 * response, the client knows right away that it is not capable of UDP
 * connectivity.  If the test produces a response, the client examines
 * the MAPPED-ADDRESS attribute.  If this address and port are the same
 * as the local IP address and port of the socket used to send the
 * request, the client knows that it is not natted.  It executes test
 * II.
 * </p><p>
 * If a response is received, the client knows that it has open access
 * to the Internet (or, at least, its behind a firewall that behaves
 * like a full-cone NAT, but without the translation).  If no response
 * is received, the client knows its behind a symmetric UDP firewall.
 * </p><p>
 * In the event that the IP address and port of the socket did not match
 * the MAPPED-ADDRESS attribute in the response to test I, the client
 * knows that it is behind a NAT.  It performs test II.  If a response
 * is received, the client knows that it is behind a full-cone NAT.  If
 * no response is received, it performs test I again, but this time,
 * does so to the address and port from the CHANGED-ADDRESS attribute
 * from the response to test I.  If the IP address and port returned in
 * the MAPPED-ADDRESS attribute are not the same as the ones from the
 * first test I, the client knows its behind a symmetric NAT.  If the
 * address and port are the same, the client is either behind a
 * restricted or port restricted NAT.  To make a determination about
 * which one it is behind, the client initiates test III.  If a response
 * is received, its behind a restricted NAT, and if no response is
 * received, its behind a port restricted NAT.
 * </p><p>
 * This procedure yields substantial information about the operating
 * condition of the client application.  In the event of multiple NATs
 * between the client and the Internet, the type that is discovered will
 * be the type of the most restrictive NAT between the client and the
 * Internet.  The types of NAT, in order of restrictiveness, from most
 * to least, are symmetric, port restricted cone, restricted cone, and
 * full cone.
 * </p><p>
 * Typically, a client will re-do this discovery process periodically to
 * detect changes, or look for inconsistent results.  It is important to
 * note that when the discovery process is redone, it should not
 * generally be done from the same local address and port used in the
 * previous discovery process.  If the same local address and port are
 * reused, bindings from the previous test may still be in existence,
 * and these will invalidate the results of the test.  Using a different
 * local address and port for subsequent tests resolves this problem.
 * An alternative is to wait sufficiently long to be confident that the
 * old bindings have expired (half an hour should more than suffice).
 * </p><p>
 * <p>Organisation: <p> Louis Pasteur University, Strasbourg, France</p>
 * <p>Network Research Team (http://www-r2.u-strasbg.fr)</p></p>
 * @author Emil Ivov
 * @version 0.1
 */

public class StunClient
{
    private static final Logger logger =
        Logger.getLogger(StunClient.class);
    /**
     * Indicates whether the underlying stack has been initialized and started
     * and that the discoverer is operational.
     */
    private boolean started = false;

    /**
     * The stack to use for STUN communication.
     */
    private StunStack stunStack = null;

    /**
     * The provider to send our messages through
     */
    private StunProvider stunProvider = null;

    /**
     * The point where we'll be listening.
     */
    private NetAccessPointDescriptor apDescriptor = null;

    /**
     * The address of the stun server
     */
    private StunAddress serverAddress = null;

    /**
     * A utility used to flatten the multithreaded architecture of the Stack
     * and execute the discovery process in a synchronized manner
     */
    private BlockingRequestSender requestSender = null;

    /**
     * Creates a StunAddressDiscoverer. In order to use it one must start the
     * discoverer.
     * @param localAddress the address where the stach should bind.
     */
    public StunClient(StunAddress localAddress)
    {
        apDescriptor = new NetAccessPointDescriptor(localAddress);
    }

    /**
     * Creates a StunAddressDiscoverer. In order to use it one must start the
     * discoverer.
     * @param apDescriptor the address where the stach should bind.
     * @param serverAddress the address of the server to interrogate.
     */
    public StunClient(NetAccessPointDescriptor apDescriptor,
                      StunAddress serverAddress)
    {
        this.apDescriptor = apDescriptor;
        this.serverAddress = serverAddress;

    }

    /**
     * Shuts down the underlying stack and prepares the object for garbage
     * collection.
     */
    public void shutDown()
    {
        try
        {
            stunStack.removeNetAccessPoint(apDescriptor);
        }
        catch (StunException ex)
        {
            logger.warn("Failed to remove NetAP: "+apDescriptor.toString(), ex);
        }
        stunStack = null;
        stunProvider = null;
        apDescriptor = null;
        requestSender = null;

        this.started = false;

    }

    /**
     * Puts the discoverer into an operational state.
     * @throws StunException if we fail to bind or some other error occurs.
     */
    public void start() throws StunException
    {
        stunStack = StunStack.getInstance();
        stunStack.start();

        stunStack.installNetAccessPoint(apDescriptor);

        stunProvider = stunStack.getProvider();

        requestSender = new BlockingRequestSender(stunProvider, apDescriptor);

        started = true;
    }

    /**
     * Sends a binding request to the specified server address. Both change IP
     * and change port flags are set to false.
     * @param serverAddress the address where to send the bindingRequest.
     * @return The returned message encapsulating event or null if no message
     * was received.
     * @throws StunException if an exception occurs while sending the messge
     */
    public StunMessageEvent doStunTestI(StunAddress serverAddress) throws
        StunException
    {
        Request request = MessageFactory.createBindingRequest();

        ChangeRequestAttribute changeRequest =
            (ChangeRequestAttribute) request.getAttribute(Attribute.
            CHANGE_REQUEST);
        changeRequest.setChangeIpFlag(false);
        changeRequest.setChangePortFlag(false);
        StunMessageEvent evt =
            requestSender.sendRequestAndWaitForResponse(request, serverAddress);
        if (evt != null)
            System.out.println("TEST I res=" + evt.getRemoteAddress().toString()
                               + " - " + evt.getRemoteAddress().getHostName());
        else
            System.out.println("NO RESPONSE received to TEST I.");
        return evt;
    }

    /**
     * Sends a binding request to the specified server address with both change
     * IP and change port flags are set to true.
     * @param serverAddress the address where to send the bindingRequest.
     * @return The returned message encapsulating event or null if no message
     * was received.
     * @throws StunException if an exception occurs while sending the messge
     */
    public StunMessageEvent doStunTestII(StunAddress serverAddress) throws
        StunException
    {
        Request request = MessageFactory.createBindingRequest();

        ChangeRequestAttribute changeRequest =
            (ChangeRequestAttribute) request.getAttribute(Attribute.
            CHANGE_REQUEST);
        changeRequest.setChangeIpFlag(true);
        changeRequest.setChangePortFlag(true);

        StunMessageEvent evt =
            requestSender.sendRequestAndWaitForResponse(request, serverAddress);
        if (evt != null)
            System.out.println("Test II res=" + evt.getRemoteAddress().toString()
                               + " - " + evt.getRemoteAddress().getHostName());
        else
            System.out.println("NO RESPONSE received to Test II.");

        return evt;
    }

    /**
     * Sends a binding request to the specified server address with only change
     * port flag set to true and change IP flag - to false.
     * @param serverAddress the address where to send the bindingRequest.
     * @return The returned message encapsulating event or null if no message
     * was received.
     * @throws StunException if an exception occurs while sending the messge
     */
    public StunMessageEvent doStunTestIII(StunAddress serverAddress) throws
        StunException
    {
        Request request = MessageFactory.createBindingRequest();

        ChangeRequestAttribute changeRequest = (ChangeRequestAttribute) request.
            getAttribute(Attribute.CHANGE_REQUEST);
        changeRequest.setChangeIpFlag(false);
        changeRequest.setChangePortFlag(true);

        StunMessageEvent evt =
            requestSender.sendRequestAndWaitForResponse(request, serverAddress);
        if (evt != null)
            System.out.println("Test III res=" +
                               evt.getRemoteAddress().toString()
                               + " - " + evt.getRemoteAddress().getHostName());
        else
            System.out.println("NO RESPONSE received to Test III.");

        return evt;
    }

    /**
     * Makes shure the discoverer is operational and throws an
     * StunException.ILLEGAL_STATE if that is not the case.
     * @throws StunException ILLEGAL_STATE if the discoverer is not operational.
     */
    private void checkStarted() throws StunException
    {
        if (!started)
            throw new StunException(StunException.ILLEGAL_STATE,
                                    "The Discoverer must be started before "
                                    + "launching the discovery process!");
    }
}
/**
 * Sample run results.
 *
 * TEST I res=/69.0.209.22:3478 - stun01bak.sipphone.com
 * mapped address is=193.108.24.226./193.108.24.226:5678,  name=193.108.24.226.
 * backup server address is=69.0.208.27./69.0.208.27:3478, name=69.0.208.27.
 * NO RESPONSE received to Test II.
 * TEST I res=/69.0.208.27:3478 - stun01.sipphone.com
 * NO RESPONSE received to Test III.
 * The detected network configuration is: Port Restricted Cone NAT
 * Your mapped public address is: 193.108.24.226./193.
 */
