org.lastbamboo.common.ice.TcpOfferAnswer Maven / Gradle / Ivy
package org.lastbamboo.common.ice;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicReference;
import javax.net.SocketFactory;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLSocket;
import org.lastbamboo.common.ice.candidate.IceCandidate;
import org.lastbamboo.common.ice.candidate.IceCandidateVisitor;
import org.lastbamboo.common.ice.candidate.IceCandidateVisitorAdapter;
import org.lastbamboo.common.ice.candidate.IceTcpHostPassiveCandidate;
import org.lastbamboo.common.ice.sdp.IceCandidateSdpDecoder;
import org.lastbamboo.common.ice.sdp.IceCandidateSdpDecoderImpl;
import org.lastbamboo.common.offer.answer.OfferAnswer;
import org.lastbamboo.common.offer.answer.OfferAnswerListener;
import org.lastbamboo.common.stun.client.PublicIpAddress;
import org.littleshoot.mina.common.ByteBuffer;
import org.littleshoot.stun.stack.StunAddressProvider;
import org.littleshoot.util.CandidateProvider;
import org.littleshoot.util.PublicIp;
import org.littleshoot.util.RuntimeIoException;
import org.littleshoot.util.ThreadUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link OfferAnswer} handler for TCP connections.
*/
public class TcpOfferAnswer implements IceOfferAnswer,
StunAddressProvider {
private final Logger log = LoggerFactory.getLogger(getClass());
private final AtomicReference socketRef =
new AtomicReference();
private final boolean controlling;
private final OfferAnswerListener offerAnswerListener;
private final MappedTcpOffererServerPool offererServer;
private PortMappedServerSocket portMappedServerSocket;
private final MappedServerSocket mappedServerSocket;
private final SocketFactory socketFactory;
private static final ExecutorService tcpIceServerThreadPool =
Executors.newCachedThreadPool(new ThreadFactory() {
private int count = 0;
@Override
public Thread newThread(Runnable r) {
final Thread t = new Thread(r,
"TCP-Ice-Server-Thread-" + hashCode() +"-"+count);
t.setDaemon(true);
count++;
return t;
}
});
/**
* Creates a new TCP {@link OfferAnswer} class for processing offers and
* answers for creating a TCP connection to a remote peer.
* @param publicAddress The public address for this host.
*
* @param offerAnswerListener The class to notify of sockets.
* @param controlling Whether or not we're the controlling side of the
* connection.
* @param answererServer The class that has a router-mapped port for
* the answering server socket.
* @param stunCandidateProvider Provider for STUN addresses.
*/
public TcpOfferAnswer(
final OfferAnswerListener offerAnswerListener,
final boolean controlling,
final MappedServerSocket answererServer,
final CandidateProvider stunCandidateProvider,
final MappedTcpOffererServerPool offererServer,
final SocketFactory socketFactory) {
this.offerAnswerListener = offerAnswerListener;
this.controlling = controlling;
this.offererServer = offererServer;
this.socketFactory = socketFactory;
// We only start another server socket on the controlling candidate
// because the non-controlled, answering agent simply uses the same
// server socket across all ICE sessions, forwarding their data to
// the HTTP server.
if (controlling || answererServer == null) {
log.info("Using pooled offerer server");
try {
this.portMappedServerSocket = offererServer.serverSocket();
this.mappedServerSocket = portMappedServerSocket;
// Accept incoming sockets on a listening thread.
listen();
log.info("Starting offer answer");
} catch (final IOException e) {
log.error("Could not bind server socket", e);
throw new RuntimeIoException("Could not bind server socket", e);
}
} else {
log.info("Using mapped server socket");
this.mappedServerSocket = answererServer;
}
}
public void close() {
log.info("Closing!!");
final Socket sock = socketRef.get();
if (sock != null) {
try {
sock.close();
} catch (final IOException e) {
log.info("Exception closing socket", e);
}
}
}
public void closeTcp() {
close();
}
public void closeUdp() {
// Ignored.
}
private void listen() {
final ServerSocket ss = this.portMappedServerSocket.getServerSocket();
final InetSocketAddress socketAddress =
(InetSocketAddress) ss.getLocalSocketAddress();
if (ss instanceof SSLServerSocket) {
log.info("Enabled cipher suites on SSL server socket: {}",
Arrays.asList(((SSLServerSocket)ss).getEnabledCipherSuites()));
}
final Runnable serverRunner = new Runnable() {
public void run() {
// We just accept the single socket on this port instead of
// the typical "while (true)".
try {
log.info("Waiting for incoming socket on: {}",
socketAddress);
final Socket sock = ss.accept();
// Just for debugging.
if (sock instanceof SSLSocket) {
final SSLSocket ssl = (SSLSocket) sock;
log.info("Enabled cipher suites on accepted " +
"server socket: {}",
Arrays.asList(ssl.getEnabledCipherSuites()));
}
log.info("GOT INCOMING SOCKET FROM "+
sock.getRemoteSocketAddress() +"!! Controlling: {}",
controlling);
sock.setKeepAlive(true);
onSocket(sock);
} catch (final IOException e) {
// This could also be a socket timeout because we limit
// the length of time allowed on accept calls.
log.info("Exception accepting socket. This will often " +
"happen when the client side connects first, and we " +
"simply return the socket back to the pool.", e);
} finally {
// Adding back server socket. Note this is fine to do no
// matter what actual processing happened with the socket
// just received, as that's just an independent Socket,
// while the ServerSocket is available for more connections.
offererServer.addServerSocket(portMappedServerSocket);
}
}
};
tcpIceServerThreadPool.execute(serverRunner);
}
public byte[] generateAnswer() {
// TODO: This is a little bit odd since the TCP side should
// theoretically generate the SDP for the TCP candidates.
final String msg =
"We fallback to the old code for gathering this for now.";
log.error("TCP implemenation can't generate offers or answers");
throw new UnsupportedOperationException(msg);
}
public byte[] generateOffer() {
// TODO: This is a little bit odd since the TCP side should
// theoretically generate the SDP for the TCP candidates.
final String msg =
"We fallback to the old code for gathering this for now.";
log.error("TCP implemenation can't generate offers or answers");
throw new UnsupportedOperationException(msg);
}
public void processOffer(final ByteBuffer offer) {
processRemoteCandidates(offer);
}
public void processAnswer(final ByteBuffer answer) {
// We don't need to do any processing if we've already got the socket.
if (this.socketRef.get() != null) {
log.info("Controlling side already has a socket -- "
+ "ignoring answer.");
return;
}
processRemoteCandidates(answer);
}
private void processRemoteCandidates(final ByteBuffer encodedCandidates) {
final IceCandidateSdpDecoder decoder = new IceCandidateSdpDecoderImpl();
final Collection remoteCandidates;
try {
// Note the second argument doesn't matter at all.
remoteCandidates = decoder.decode(encodedCandidates, controlling);
} catch (final IOException e) {
log.warn("Could not process remote candidates", e);
return;
}
// OK, we've got the candidates. We'll now parallelize connection
// attempts to all of them, taking the first to succeed. Note there's
// typically a single local network candidate that will only succeed
// if we're on same subnet and then a public candidate that's
// either there because the remote host is on the public Internet or
// because the public address was mapped using UPnP or NAT-PMP.
final IceCandidateVisitor © 2015 - 2025 Weber Informatics LLC | Privacy Policy