All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.lastbamboo.common.stun.client.UdpStunClient Maven / Gradle / Ivy

The newest version!
package org.lastbamboo.common.stun.client;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.commons.id.uuid.UUID;
import org.littleshoot.dnssec4j.DNSSECException;
import org.littleshoot.dnssec4j.DnsSec;
import org.littleshoot.mina.common.ByteBuffer;
import org.littleshoot.mina.common.ConnectFuture;
import org.littleshoot.mina.common.ExecutorThreadModel;
import org.littleshoot.mina.common.IoConnector;
import org.littleshoot.mina.common.IoHandler;
import org.littleshoot.mina.common.IoServiceListener;
import org.littleshoot.mina.common.IoSession;
import org.littleshoot.mina.common.SimpleByteBufferAllocator;
import org.littleshoot.mina.filter.codec.ProtocolCodecFactory;
import org.littleshoot.mina.filter.codec.ProtocolCodecFilter;
import org.littleshoot.mina.transport.socket.nio.DatagramConnector;
import org.littleshoot.mina.transport.socket.nio.DatagramConnectorConfig;
import org.littleshoot.stun.stack.StunIoHandler;
import org.littleshoot.stun.stack.StunProtocolCodecFactory;
import org.littleshoot.stun.stack.message.BindingErrorResponse;
import org.littleshoot.stun.stack.message.BindingRequest;
import org.littleshoot.stun.stack.message.BindingSuccessResponse;
import org.littleshoot.stun.stack.message.ConnectErrorStunMessage;
import org.littleshoot.stun.stack.message.NullStunMessage;
import org.littleshoot.stun.stack.message.StunMessage;
import org.littleshoot.stun.stack.message.StunMessageVisitor;
import org.littleshoot.stun.stack.message.StunMessageVisitorAdapter;
import org.littleshoot.stun.stack.message.StunMessageVisitorFactory;
import org.littleshoot.stun.stack.transaction.StunTransactionListener;
import org.littleshoot.stun.stack.transaction.StunTransactionTracker;
import org.littleshoot.stun.stack.transaction.StunTransactionTrackerImpl;
import org.littleshoot.util.CandidateProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Abstract STUN client.  Subclasses typically define transports.
 */
public class UdpStunClient implements StunClient, StunTransactionListener {

    private static final Logger LOG = 
        LoggerFactory.getLogger(UdpStunClient.class);
    
    private final Collection m_ioServiceListeners =
        new ArrayList();

    private RankedStunServer m_stunServer;
    
    private final IoHandler m_ioHandler;

    private final Map m_idsToResponses =
        new ConcurrentHashMap();

    private InetSocketAddress m_localAddress;

    /**
     * Just keeps track of the current connection 5-tuple so we don't try
     * to connect to the host we're already connected to.
     */
    private IoSession m_currentIoSession;

    private final StunTransactionTracker m_transactionTracker;

    private final InetSocketAddress m_originalLocalAddress;

    private final Collection m_sessions = new ArrayList();

    private final Queue m_stunServers = 
        new PriorityQueue();

    /**
     * Creates a new STUN client for ICE processing.  This client is capable
     * of obtaining "server reflexive" and "host" candidates.  We don't use
     * relaying for UDP, so this does not currently support generating
     * "relayed" candidates.
     * 
     * @param transactionTracker The transaction tracker to use.
     * @param ioHandler The {@link IoHandler} to use.
     * @param stunServerCandidateProvider Provider for STUN server addresses.
     * @throws IOException If we can't get a STUN server address.
     */
    public UdpStunClient(
        final StunTransactionTracker transactionTracker,
        final IoHandler ioHandler, 
        final CandidateProvider stunServerCandidateProvider) 
            throws IOException {
        this(null, stunServerCandidateProvider.getCandidates(),
                transactionTracker, ioHandler);
    }
    
    /**
     * Creates a new STUN client that connects to the specified STUN servers.
     * @param stunServerCandidateProvider Class that provides STUN servers to
     * use.
     * @throws IOException If we can't get a STUN server address. 
     */
    public UdpStunClient(
        final CandidateProvider stunServerCandidateProvider) 
            throws IOException {
        this(null, stunServerCandidateProvider.getCandidates(), null, null);
    }
    
    /**
     * Creates a new STUN client that connects to the specified STUN servers.
     * @param stunServerCandidateProvider Class that provides STUN servers to
     * use.
     * @throws IOException If we can't get a STUN server address. 
     */
    public UdpStunClient(final InetSocketAddress... stunServers) 
        throws IOException {
        this(null, Arrays.asList(stunServers), null, null);
    }
    
    /**
     * Creates a new STUN client that connects to the specified STUN servers.
     * @param stunServerCandidateProvider Class that provides STUN servers to
     * use.
     * @throws IOException If we can't get a STUN server address. 
     */
    public UdpStunClient(final Collection stunServers) 
        throws IOException {
        this(null, stunServers, null, null);
    }

    
    /**
     * Creates a new STUN client that connects to the specified STUN server.
     * 
     * @param stunServerAddress The address of the STUN server to connect to.
     * @param connectTimeout The timeout to wait for connections.
     * @throws IOException 
     */
    private UdpStunClient(final InetSocketAddress localAddress,
            final Collection stunServers,
            final StunTransactionTracker transactionTracker,
            final IoHandler ioHandler) throws IOException {
        if (stunServers == null) {
            LOG.error("Null STUN server provider");
            throw new NullPointerException("Null STUN server provider");
        }
        LOG.info("Creating UDP STUN CLIENT");
        for (final InetSocketAddress isa : stunServers) {
            try {
                this.m_stunServers.add(new RankedStunServer(isa));
            } catch (final DNSSECException e) {
                LOG.warn("DNSSEC verification error!!", e);
            }
        }
        ByteBuffer.setUseDirectBuffers(false);
        ByteBuffer.setAllocator(new SimpleByteBufferAllocator());
        m_originalLocalAddress = localAddress;
        if (transactionTracker == null) {
            this.m_transactionTracker = new StunTransactionTrackerImpl();
        } else {
            this.m_transactionTracker = transactionTracker;
        }

        m_stunServer = pickStunServerInetAddress();

        if (ioHandler == null) {
            final StunMessageVisitorFactory messageVisitorFactoryToUse = 
                new StunClientMessageVisitorFactory(
                    this.m_transactionTracker);
            m_ioHandler = new StunIoHandler(messageVisitorFactoryToUse);
        } else {
            m_ioHandler = ioHandler;
        }
    }
   
    @Override
    public void connect() throws IOException {
        IoSession session;
        try {
            session = connect(m_originalLocalAddress, m_stunServer.isa);
        } catch (final IOException e) {
            onFailure(m_stunServer);
            throw e;
        }

        // We set the local address here because the original could be null
        // to bind to an ephemeral port.
        this.m_localAddress = (InetSocketAddress) session.getLocalAddress();
    }

    private void onFailure(final RankedStunServer rss) throws IOException {
        // It needs to get placed again in the ranking.
        if (m_stunServer.failures < 5) {
            m_stunServer.failures++;
            m_stunServers.remove(rss);
            m_stunServers.add(rss);
        }
        this.m_stunServer = pickStunServerInetAddress();
    }
    
    private void onSuccess(final RankedStunServer m_stunServer2) {
        if (m_stunServer.successes < 5) {
            this.m_stunServer.successes++;
        }
    }

    private final IoSession connect(final InetSocketAddress localAddress,
            final InetSocketAddress stunServer) throws IOException {
        // We can't connect twice to the same 5-tuple, so check to verify we're
        // not reconnecting to the remote host we're already connected to.
        if (this.m_currentIoSession != null
                && this.m_currentIoSession.getRemoteAddress().equals(
                        stunServer)) {
            return this.m_currentIoSession;
        }

        final ProtocolCodecFactory codecFactory = new StunProtocolCodecFactory();
        final ProtocolCodecFilter stunFilter = new ProtocolCodecFilter(
                codecFactory);

        final IoConnector connector = createConnector();
        connector.getFilterChain().addLast("stunFilter", stunFilter);

        if (this.m_ioServiceListeners.isEmpty()) {
            LOG.debug("No service listeners for: {}", getClass()
                    .getSimpleName());
        }
        synchronized (this.m_ioServiceListeners) {
            for (final IoServiceListener sl : this.m_ioServiceListeners) {
                connector.addListener(sl);
            }
        }
        LOG.debug("Connecting to: {}", stunServer);
        final ConnectFuture cf = connector.connect(stunServer,
            localAddress, m_ioHandler);
        LOG.debug("About to join");
        cf.join();
        LOG.debug("Connected to: {}", stunServer);
        final IoSession session = cf.getSession();
        if (session == null) {
            throw new IOException("Could not get session with: "
                    + stunServer);
        }
        this.m_sessions.add(session);
        this.m_currentIoSession = session;
        return session;
    }

    public InetSocketAddress getHostAddress() {
        return m_localAddress;
    }

    public InetAddress getStunServerAddress() {
        return this.m_stunServer.isa.getAddress();
    }

    protected void waitIfNoResponse(final StunMessage request,
            final long waitTime) {
        LOG.debug("Waiting " + waitTime + " milliseconds...");
        if (waitTime == 0L)
            return;
        if (!m_idsToResponses.containsKey(request.getTransactionId())) {
            try {
                LOG.debug("Actually waiting...");
                request.wait(waitTime);
            } catch (final InterruptedException e) {
                // This can happen if multiple STUN clients are started in
                // a thread pool, for example.
                LOG.info("Interrupt", e);
            }
        }
    }

    public Object onTransactionFailed(final StunMessage request,
            final StunMessage response) {
        return notifyWaiters(request, response);
    }

    public Object onTransactionSucceeded(final StunMessage request,
            final StunMessage response) {
        return notifyWaiters(request, response);
    }

    private Object notifyWaiters(final StunMessage request, 
        final StunMessage response) {
        synchronized (request) {
            this.m_idsToResponses.put(request.getTransactionId(), response);
            request.notify();
        }
        return null;
    }

    public final void addIoServiceListener(
            final IoServiceListener serviceListener) {
        LOG.debug("Adding service listener for: {}", this);
        this.m_ioServiceListeners.add(serviceListener);
    }

    public void close() {
        LOG.info("Closing sessions...");
        synchronized (m_sessions) {
            for (final IoSession session : m_sessions) {
                LOG.info("Closing: {}", session);
                session.close();
            }
        }
    }

    private IoConnector createConnector() {
        final DatagramConnector connector = new DatagramConnector();
        final DatagramConnectorConfig cfg = connector.getDefaultConfig();
        cfg.getSessionConfig().setReuseAddress(true);
        cfg.setThreadModel(ExecutorThreadModel.getInstance(getClass()
                .getSimpleName()));
        return connector;
    }

    public InetSocketAddress getServerReflexiveAddress() throws IOException {
        for (int i = 0; i < this.m_stunServers.size(); i++) {
            LOG.info("Getting server reflexive address from: {}",
                    this.m_stunServer);
            final BindingRequest br = new BindingRequest();
            final StunMessage message = write(br, this.m_stunServer.isa);
            final StunMessageVisitor visitor = 
                new StunMessageVisitorAdapter() {
                @Override
                public InetSocketAddress visitBindingSuccessResponse(
                        final BindingSuccessResponse response) {
                    return response.getMappedAddress();
                }

                @Override
                public InetSocketAddress visitBindingErrorResponse(
                        final BindingErrorResponse response) {
                    LOG.warn("Received Binding Error Response: " + response);
                    return null;
                }

                @Override
                public InetSocketAddress visitConnectErrorMesssage(
                        final ConnectErrorStunMessage error) {
                    LOG.warn("Received ICMP error: {}", error);
                    return null;
                }
            };

            final InetSocketAddress isa = message.accept(visitor);
            if (isa == null) {
                onFailure(m_stunServer);
                continue;
            }
            onSuccess(this.m_stunServer);
            
            // Always keep rotating.
            this.m_stunServer = pickStunServerInetAddress();
            return isa;
        }

        // If we get here, all our attempts failed. Maybe the client's offline?
        throw new IOException("Could not get server reflexive address!");
    }

    public StunMessage write(final BindingRequest request,
        final InetSocketAddress remoteAddress) throws IOException {
        // Use an RTO of 100ms, as discussed in
        // draft-ietf-behave-rfc3489bis-06.txt section 7.1. Note we just
        // use this value and don't cache previously discovered values for
        // the RTO.
        final long rto = 100L;
        return write(request, remoteAddress, rto);
    }

    public StunMessage write(final BindingRequest request,
            final InetSocketAddress remoteAddress, final long rto)
            throws IOException {
        // Note we've typically already "connected" around creation time with
        // the connect method, but it's cheap with UDP.
        final IoSession session = connect(this.m_localAddress, remoteAddress);

        // This method will retransmit the same request multiple times because
        // it's being sent unreliably. All of these requests will be
        // identical, using the same transaction ID.
        final UUID id = request.getTransactionId();

        this.m_transactionTracker.addTransaction(request, this,
                this.m_localAddress, remoteAddress);

        int requests = 0;

        long waitTime = 0L;
        synchronized (request) {
            while (!m_idsToResponses.containsKey(id) && requests < 7) {
                waitIfNoResponse(request, waitTime);

                // See draft-ietf-behave-rfc3489bis-06.txt section 7.1. We
                // continually send the same request until we receive a
                // response, never sending more that 7 requests and using
                // an expanding interval between requests based on the
                // estimated round-trip-time to the server. This is because
                // some requests can be lost with UDP.
                session.write(request);

                // Wait a little longer with each send.
                waitTime = (2 * waitTime) + rto;

                requests++;
            }

            // Now we wait for 1.6 seconds after the last request was sent.
            // If we still don't receive a response, then the transaction
            // has failed.
            waitIfNoResponse(request, 1600);
        }

        if (m_idsToResponses.containsKey(id)) {
            final StunMessage response = this.m_idsToResponses.get(id);
            return response;
        }

        LOG.warn("Did not get response from: " + remoteAddress);
        return new NullStunMessage();
    }

    public InetSocketAddress getRelayAddress() {
        // We don't support UDP relays at this time.
        LOG.warn("Attempted to get a UDP relay!!");
        return null;
    }

    public boolean hostPortMapped() {
        // We don't map ports for clients (only for classes that also accept
        // incoming connections).
        return false;
    }

    private RankedStunServer pickStunServerInetAddress() throws IOException {
        return pickStunServerInetAddress(null);
    }

    private RankedStunServer pickStunServerInetAddress(
            final InetSocketAddress skipAddress) throws IOException {
        if (m_stunServers.isEmpty()) {
            LOG.warn("Could not get STuN addresses!!");
            throw new IOException("No STUN addresses returned!");
        }
        if (skipAddress != null) {
            m_stunServers.remove(skipAddress);
        }
        final RankedStunServer rss = m_stunServers.peek();
        return rss;
    }

    private class RankedStunServer implements Comparable{

        private InetSocketAddress isa;
        private int successes;
        private int failures;
        private RankedStunServer(final InetSocketAddress isa) 
            throws DNSSECException {
            if (isa.isUnresolved() && StunClientConfig.isUseDnsSec()) {
                try {
                    this.isa = DnsSec.verify(isa);
                } catch (IOException e) {
                    this.isa = isa;
                }
            } else {
                this.isa = isa;
            }
        }
        private int getScore() {
            return successes - failures;
        }
        

        @Override
        public String toString() {
            return "RankedStunServer [isa=" + isa + " score="+getScore()+"]";
        }
        
        @Override
        public int compareTo(final RankedStunServer rss) {
            final Integer score1 = rss.getScore();
            return score1.compareTo(getScore());
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy