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

org.lastbamboo.common.turn.client.TcpTurnClient Maven / Gradle / Ivy

package org.lastbamboo.common.turn.client;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Collection;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.commons.lang.SystemUtils;
import org.littleshoot.dnssec4j.DNSSECException;
import org.littleshoot.dnssec4j.DnsSec;
import org.littleshoot.mina.common.ByteBuffer;
import org.littleshoot.mina.common.CloseFuture;
import org.littleshoot.mina.common.ConnectFuture;
import org.littleshoot.mina.common.ExecutorThreadModel;
import org.littleshoot.mina.common.IoFilter;
import org.littleshoot.mina.common.IoFilterAdapter;
import org.littleshoot.mina.common.IoFuture;
import org.littleshoot.mina.common.IoFutureListener;
import org.littleshoot.mina.common.IoHandler;
import org.littleshoot.mina.common.IoService;
import org.littleshoot.mina.common.IoServiceConfig;
import org.littleshoot.mina.common.IoServiceListener;
import org.littleshoot.mina.common.IoSession;
import org.littleshoot.mina.common.RuntimeIOException;
import org.littleshoot.mina.common.SimpleByteBufferAllocator;
import org.littleshoot.mina.common.ThreadModel;
import org.littleshoot.mina.filter.codec.ProtocolCodecFactory;
import org.littleshoot.mina.filter.codec.ProtocolCodecFilter;
import org.littleshoot.mina.filter.codec.ProtocolDecoderOutput;
import org.littleshoot.mina.transport.socket.nio.SocketConnector;
import org.littleshoot.mina.transport.socket.nio.SocketConnectorConfig;
import org.littleshoot.stun.stack.StunMessageDecoder;
import org.littleshoot.stun.stack.message.BindingRequest;
import org.littleshoot.stun.stack.message.StunMessage;
import org.littleshoot.stun.stack.message.StunMessageVisitorAdapter;
import org.littleshoot.stun.stack.message.attributes.turn.ConnectionStatus;
import org.littleshoot.stun.stack.message.turn.AllocateErrorResponse;
import org.littleshoot.stun.stack.message.turn.AllocateRequest;
import org.littleshoot.stun.stack.message.turn.AllocateSuccessResponse;
import org.littleshoot.stun.stack.message.turn.ConnectRequest;
import org.littleshoot.stun.stack.message.turn.ConnectionStatusIndication;
import org.littleshoot.stun.stack.message.turn.DataIndication;
import org.littleshoot.util.CandidateProvider;
import org.littleshoot.util.RuntimeIoException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Class that handles all responsibilities of a TURN client.  It does this in
 * a couple of ways.  First, it opens a connection to the TURN server and
 * allocates a binding on the TURN server.  Second, it decodes 
 * Data Indication messages arriving from the TURN server. When it receives
 * a message, it creates sockets to the local HTTP server and forwards the
 * data (HTTP data) enclosed in the Data Indication to the local HTTP
 * server.

* * If this ever loses the connection to the TURN server, it notifies the * listener that maintains TURN connections. */ public class TcpTurnClient extends StunMessageVisitorAdapter implements TurnClient, IoServiceListener { private final Logger m_log = LoggerFactory.getLogger(getClass()); private InetSocketAddress m_stunServerAddress; private IoSession m_ioSession; private InetSocketAddress m_relayAddress; private InetSocketAddress m_mappedAddress; private boolean m_receivedAllocateResponse; private final TurnClientListener m_turnClientListener; private final ProtocolCodecFactory m_dataCodecFactory; private int m_totalReadDataBytes; private int m_totalReadRawDataBytes; private final AtomicBoolean m_connected = new AtomicBoolean(false); private final SocketConnector m_connector = new SocketConnector(); private final CandidateProvider m_candidateProvider; /** * Creates a new TURN client with the default provider for server addresses. * * @param clientListener The listener for TURN client events. * @param codecFactory The codec factory. */ /* public TcpTurnClient(final TurnClientListener clientListener, final ProtocolCodecFactory codecFactory) { this(clientListener, new TurnServerCandidateProvider(), codecFactory); } */ /** * Creates a new TCP TURN client. * * @param clientListener The listener for TURN client events. * @param candidateProvider The class that provides TURN candidate * servers. * @param codecFactory The codec factory. */ public TcpTurnClient(final TurnClientListener clientListener, final CandidateProvider candidateProvider, final ProtocolCodecFactory codecFactory) { m_turnClientListener = clientListener; m_candidateProvider = candidateProvider; m_dataCodecFactory = codecFactory; // Configure the MINA buffers for optimal performance. ByteBuffer.setUseDirectBuffers(false); ByteBuffer.setAllocator(new SimpleByteBufferAllocator()); } public void connect() throws IOException { if (this.m_connected.get()) { throw new IllegalArgumentException("Already connected..."); } final Collection candidates = this.m_candidateProvider .getCandidates(); m_log.info("Attempting connections to: {}", candidates); for (final InetSocketAddress serverAddress : candidates) { connect(serverAddress, null); synchronized (this.m_connected) { try { this.m_connected.wait(30 * 1000); } catch (final InterruptedException e) { m_log.error("Interrupted while waiting", e); } } if (isConnected()) { m_log.debug("Connected to: {}", serverAddress); break; } } if (!isConnected()) { m_log.error("Could not connect or did not get allocate response"); close(); throw new IOException("Could not connect to any of: " + candidates); } } private void connect(final InetSocketAddress unverifiedStunServerAddress, final InetSocketAddress localAddress) throws IOException { final InetSocketAddress stunServerAddress; if (TurnClientConfig.isUseDnsSec()) { try { stunServerAddress = DnsSec.verify(unverifiedStunServerAddress); } catch (final DNSSECException e) { throw new IOException("DNSSEC verification error", e); } } else { stunServerAddress = unverifiedStunServerAddress; } final StunMessageDecoder decoder = new StunMessageDecoder(); final IoFilter turnFilter = new IoFilterAdapter() { @Override public void filterWrite(final NextFilter nextFilter, final IoSession session, final WriteRequest writeRequest) throws Exception { // m_log.debug("Filtering write: "+writeRequest.getMessage()); nextFilter.filterWrite(session, writeRequest); } @Override public void messageReceived(final NextFilter nextFilter, final IoSession session, final Object message) throws Exception { final ByteBuffer in = (ByteBuffer) message; final ProtocolDecoderOutput out = new ProtocolDecoderOutput() { public void flush() { } public void write(final Object msg) { final StunMessage stunMessage = (StunMessage) msg; stunMessage.accept(TcpTurnClient.this); } }; decoder.decode(session, in, out); } }; // If TURN is used with ICE, this will be a demultiplexing filter // between STUN and the media stream data. final ProtocolCodecFilter dataFilter = new ProtocolCodecFilter( m_dataCodecFactory); m_connector.getFilterChain().addLast("stunFilter", turnFilter); // This is really only used for the encoding. m_connector.getFilterChain().addLast("dataFilter", dataFilter); m_connector.addListener(this); // m_connectionListener = listener; m_stunServerAddress = stunServerAddress; final SocketConnectorConfig config = new SocketConnectorConfig(); // Java has weird issues with the new networking stack in Windows Vista. if (SystemUtils.IS_OS_WINDOWS_VISTA) { config.getSessionConfig().setKeepAlive(false); } else { config.getSessionConfig().setKeepAlive(true); } config.getSessionConfig().setReuseAddress(true); final ThreadModel threadModel = ExecutorThreadModel .getInstance("TCP-TURN-Client-" + hashCode()); config.setThreadModel(threadModel); // config.setThreadModel(ThreadModel.MANUAL); m_log.info("Connection to STUN server here: {}", m_stunServerAddress); final IoHandler ioHandler = new TurnClientIoHandler(this); final ConnectFuture connectFuture; if (localAddress == null) { connectFuture = m_connector.connect(m_stunServerAddress, ioHandler, config); } else { connectFuture = m_connector.connect(m_stunServerAddress, localAddress, ioHandler, config); } final IoFutureListener futureListener = new IoFutureListener() { public void operationComplete(final IoFuture ioFuture) { if (!ioFuture.isReady()) { m_log.warn("Future not ready?"); return; } try { m_ioSession = ioFuture.getSession(); } catch (final RuntimeIOException e) { // This seems to get thrown when we can't connect at all. m_log.warn("Could not connect to TURN server at: " + stunServerAddress, e); // m_connectionListener.connectionFailed(); return; } if (m_ioSession == null || !m_ioSession.isConnected()) { m_log.error("Could not create session"); throw new RuntimeIoException("Could not get session"); } // TODO: We should not need this. final TurnStunMessageMapper mapper = new TurnStunMessageMapperImpl(); m_ioSession.setAttribute("REMOTE_ADDRESS_MAP", mapper); final AllocateRequest msg = new AllocateRequest(); m_log.debug("Sending allocate request to write handler..."); m_ioSession.write(msg); } }; connectFuture.addListener(futureListener); connectFuture.join(); } public void close() { m_log.debug("Closing TCP TURN client."); if (this.m_ioSession != null) { final CloseFuture closeFuture = this.m_ioSession.close(); closeFuture.join(); } } public void sendConnectRequest(final InetSocketAddress remoteAddress) { final ConnectRequest request = new ConnectRequest(remoteAddress); this.m_ioSession.write(request); } public InetSocketAddress getRelayAddress() { return this.m_relayAddress; } public InetSocketAddress getMappedAddress() { return this.m_mappedAddress; } @Override public StunMessage visitAllocateSuccessResponse( final AllocateSuccessResponse response) { // NOTE: This will get called many times for a single TURN session // between a client and a server because allocate requests are used // for keep-alives as well as the initial allocation. m_log.debug("Got successful allocate response: {}", response); // We need to set the relay address before notifying the // listener we're "connected". this.m_relayAddress = response.getRelayAddress(); this.m_mappedAddress = response.getMappedAddress(); this.m_receivedAllocateResponse = true; // this.m_connectionListener.connected(this.m_stunServerAddress); this.m_connected.set(true); synchronized (this.m_connected) { this.m_connected.notifyAll(); } return null; } @Override public StunMessage visitAllocateErrorResponse( final AllocateErrorResponse response) { m_log.warn("Received an Allocate Response error from the server: " + response.getAttributes()); // this.m_connectionListener.connectionFailed(); this.m_ioSession.close(); return null; } @Override public StunMessage visitConnectionStatusIndication( final ConnectionStatusIndication indication) { m_log.debug("Visiting connection status message: {}", indication); final ConnectionStatus status = indication.getConnectionStatus(); final InetSocketAddress remoteAddress = indication.getRemoteAddress(); switch (status) { case CLOSED: m_log.debug("Got connection closed from: " + remoteAddress); this.m_turnClientListener.onRemoteAddressClosed(remoteAddress); break; case ESTABLISHED: m_log.debug("Connection established from: " + remoteAddress); // Create a local connection for the newly established session. this.m_turnClientListener.onRemoteAddressOpened(remoteAddress, this.m_ioSession); break; case LISTEN: m_log.debug("Got server listening for incoming data from: " + remoteAddress); break; } return null; } @Override public StunMessage visitDataIndication(final DataIndication data) { m_log.debug("Visiting Data Indication message: {}", data); m_totalReadDataBytes += data.getTotalLength(); m_totalReadRawDataBytes += data.getData().length; final InetSocketAddress remoteAddress = data.getRemoteAddress(); try { m_turnClientListener.onData(remoteAddress, this.m_ioSession, data.getData()); } catch (final Exception e) { m_log.error("Could not process data: {}", data, e); } return null; } public void serviceActivated(final IoService service, final SocketAddress serviceAddress, final IoHandler handler, final IoServiceConfig config) { m_log.debug("Service activated..."); } public void serviceDeactivated(final IoService service, final SocketAddress serviceAddress, final IoHandler handler, final IoServiceConfig config) { m_log.debug("Service deactivated..."); } public void sessionCreated(final IoSession session) { m_log.debug("Session created..."); } public void sessionDestroyed(final IoSession session) { m_log.debug("Session destroyed..."); if (this.m_receivedAllocateResponse) { // We're disconnected, so set the allocate response flag to false // because the client's current connection, or lack thereof, has // not received a response. this.m_receivedAllocateResponse = false; this.m_connected.set(false); // this.m_connectionListener.disconnected(); } this.m_turnClientListener.close(); } public InetAddress getStunServerAddress() { return this.m_stunServerAddress.getAddress(); } public InetSocketAddress getHostAddress() { return (InetSocketAddress) this.m_ioSession.getLocalAddress(); } public InetSocketAddress getServerReflexiveAddress() { return getMappedAddress(); } public StunMessage write(final BindingRequest request, final InetSocketAddress remoteAddress) { // TODO We should just send the request to the server, // and we should combine the functionality of this class with the // functionality of TcpStunClient. // Or is this just handled by IceStunCheckers?? m_log.error("Unsupported!!!!!!!"); throw new IllegalStateException("Not implemented."); } public StunMessage write(final BindingRequest request, final InetSocketAddress remoteAddress, final long rto) { // See comment above. m_log.error("Unsupported!!!!!!!"); throw new IllegalStateException("Not implemented."); } public boolean isConnected() { return this.m_connected.get(); } public boolean hostPortMapped() { // We don't map ports for clients (only for classes that also accept // incoming connections). return false; } public void addIoServiceListener(final IoServiceListener serviceListener) { if (serviceListener == null) { throw new NullPointerException("Null listener"); } this.m_connector.addListener(serviceListener); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy