/** * Implements a <tt>CandidateHarvester</tt> which gathers <tt>Candidate</tt>s for a specified {@link * Component} using UPnP. * * @author Sebastien Vincent */ public class UPNPHarvester extends AbstractCandidateHarvester { /** The logger. */ private static final Logger logger = Logger.getLogger(UPNPHarvester.class.getName()); /** Maximum port to try to allocate. */ private static final int MAX_RETRIES = 5; /** ST search field for WANIPConnection. */ private static final String stIP = "urn:schemas-upnp-org:service:WANIPConnection:1"; /** ST search field for WANPPPConnection. */ private static final String stPPP = "urn:schemas-upnp-org:service:WANPPPConnection:1"; /** Synchronization object. */ private final Object rootSync = new Object(); /** Gateway device. */ private GatewayDevice device = null; /** Number of UPnP discover threads that have finished. */ private int finishThreads = 0; /** * Gathers UPnP candidates for all host <tt>Candidate</tt>s that are already present in the * specified <tt>component</tt>. This method relies on the specified <tt>component</tt> to already * contain all its host candidates so that it would resolve them. * * @param component the {@link Component} that we'd like to gather candidate UPnP * <tt>Candidate</tt>s for * @return the <tt>LocalCandidate</tt>s gathered by this <tt>CandidateHarvester</tt> */ public synchronized Collection<LocalCandidate> harvest(Component component) { Collection<LocalCandidate> candidates = new HashSet<>(); int retries = 0; logger.fine("Begin UPnP harvesting"); try { if (device == null) { // do it only once if (finishThreads == 0) { try { UPNPThread wanIPThread = new UPNPThread(stIP); UPNPThread wanPPPThread = new UPNPThread(stPPP); wanIPThread.start(); wanPPPThread.start(); synchronized (rootSync) { while (finishThreads != 2) { rootSync.wait(); } } if (wanIPThread.getDevice() != null) { device = wanIPThread.getDevice(); } else if (wanPPPThread.getDevice() != null) { device = wanPPPThread.getDevice(); } } catch (Throwable e) { logger.info("UPnP discovery failed: " + e); } } if (device == null) return candidates; } InetAddress localAddress = device.getLocalAddress(); String externalIPAddress = device.getExternalIPAddress(); PortMappingEntry portMapping = new PortMappingEntry(); IceSocketWrapper socket = new IceUdpSocketWrapper(new MultiplexingDatagramSocket(0, localAddress)); int port = socket.getLocalPort(); int externalPort = socket.getLocalPort(); while (retries < MAX_RETRIES) { if (!device.getSpecificPortMappingEntry(port, "UDP", portMapping)) { if (device.addPortMapping( externalPort, port, localAddress.getHostAddress(), "UDP", "ice4j.org: " + port)) { List<LocalCandidate> cands = createUPNPCandidate(socket, externalIPAddress, externalPort, component, device); logger.info("Add UPnP port mapping: " + externalIPAddress + " " + externalPort); // we have to add the UPNPCandidate and also the base. // if we don't add the base, we won't be able to add // peer reflexive candidate if someone contact us on the // UPNPCandidate for (LocalCandidate cand : cands) { // try to add the candidate to the component and then // only add it to the harvest not redundant if (component.addLocalCandidate(cand)) { candidates.add(cand); } } break; } else { port++; } } else { port++; } retries++; } } catch (Throwable e) { logger.info("Exception while gathering UPnP candidates: " + e); } return candidates; } /** * Create a UPnP candidate. * * @param socket local socket * @param externalIP external IP address * @param port local port * @param component parent component * @param device the UPnP gateway device * @return a new <tt>UPNPCandidate</tt> instance which represents the specified * <tt>TransportAddress</tt> * @throws Exception if something goes wrong during candidate creation */ private List<LocalCandidate> createUPNPCandidate( IceSocketWrapper socket, String externalIP, int port, Component component, GatewayDevice device) throws Exception { List<LocalCandidate> ret = new ArrayList<>(); TransportAddress addr = new TransportAddress(externalIP, port, Transport.UDP); HostCandidate base = new HostCandidate(socket, component); UPNPCandidate candidate = new UPNPCandidate(addr, base, component, device); IceSocketWrapper stunSocket = candidate.getStunSocket(null); candidate.getStunStack().addSocket(stunSocket); component.getComponentSocket().add(candidate.getCandidateIceSocketWrapper()); ret.add(candidate); ret.add(base); return ret; } /** UPnP discover thread. */ private class UPNPThread extends Thread { /** Gateway device. */ private GatewayDevice device = null; /** ST search field. */ private final String st; /** * Constructor. * * @param st ST search field */ public UPNPThread(String st) { this.st = st; } /** * Returns gateway device. * * @return gateway device */ public GatewayDevice getDevice() { return device; } /** Thread Entry point. */ public void run() { try { GatewayDiscover gd = new GatewayDiscover(st); gd.discover(); if (gd.getValidGateway() != null) { device = gd.getValidGateway(); } } catch (Throwable e) { logger.info("Failed to harvest UPnP: " + e); /* * The Javadoc on ThreadDeath says: If ThreadDeath is caught by * a method, it is important that it be rethrown so that the * thread actually dies. */ if (e instanceof ThreadDeath) throw (ThreadDeath) e; } finally { synchronized (rootSync) { finishThreads++; rootSync.notify(); } } } } /** * Returns a <tt>String</tt> representation of this harvester containing its name. * * @return a <tt>String</tt> representation of this harvester containing its name. */ @Override public String toString() { return getClass().getSimpleName(); } }
/** * The Network Access Point is the most outward part of the stack. It is constructed around a * datagram socket and takes care of forwarding incoming messages to the MessageProcessor as well as * sending datagrams to the STUN server specified by the original NetAccessPointDescriptor. * * @author Emil Ivov */ class Connector implements Runnable { /** Our class logger. */ private static final Logger logger = Logger.getLogger(Connector.class.getName()); /** The message queue is where incoming messages are added. */ private final MessageQueue messageQueue; /** The socket object that used by this access point to access the network. */ private IceSocketWrapper sock; /** The object that we use to lock socket operations (since the socket itself is often null) */ private final Object sockLock = new Object(); /** A flag that is set to false to exit the message processor. */ private boolean running; /** The instance to be notified if errors occur in the network listening thread. */ private final ErrorHandler errorHandler; /** The address that we are listening to. */ private final TransportAddress listenAddress; /** * The remote address of the socket of this <tt>Connector</tt> if it is a TCP socket, or * <tt>null</tt> if it is UDP. */ private final TransportAddress remoteAddress; /** * Creates a network access point. * * @param socket the socket that this access point is supposed to use for communication. * @param messageQueue the FIFO list where incoming messages should be queued * @param errorHandler the instance to notify when errors occur. */ protected Connector( IceSocketWrapper socket, MessageQueue messageQueue, ErrorHandler errorHandler) { this.sock = socket; this.messageQueue = messageQueue; this.errorHandler = errorHandler; Transport transport = socket.getUDPSocket() != null ? Transport.UDP : Transport.TCP; listenAddress = new TransportAddress(socket.getLocalAddress(), socket.getLocalPort(), transport); if (transport == Transport.UDP) { remoteAddress = null; } else { Socket tcpSocket = socket.getTCPSocket(); remoteAddress = new TransportAddress(tcpSocket.getInetAddress(), tcpSocket.getPort(), transport); } } /** Start the network listening thread. */ void start() { this.running = true; Thread thread = new Thread(this, "IceConnector@" + hashCode()); thread.setDaemon(true); thread.start(); } /** * Returns the <tt>DatagramSocket</tt> that contains the port and address associated with this * access point. * * @return the <tt>DatagramSocket</tt> associated with this AP. */ protected IceSocketWrapper getSocket() { return sock; } /** The listening thread's run method. */ @Override public void run() { DatagramPacket packet = null; while (this.running) { try { IceSocketWrapper localSock; synchronized (sockLock) { if (!running) return; localSock = this.sock; } /* * Make sure localSock's receiveBufferSize is taken into * account including after it gets changed. */ int receiveBufferSize = 1500; /* if(localSock.getTCPSocket() != null) { receiveBufferSize = localSock.getTCPSocket(). getReceiveBufferSize(); } else if(localSock.getUDPSocket() != null) { receiveBufferSize = localSock.getUDPSocket(). getReceiveBufferSize(); } */ if (packet == null) { packet = new DatagramPacket(new byte[receiveBufferSize], receiveBufferSize); } else { byte[] packetData = packet.getData(); if ((packetData == null) || (packetData.length < receiveBufferSize)) { packet.setData(new byte[receiveBufferSize], 0, receiveBufferSize); } else { /* * XXX Tell the packet it is large enough because the * socket will not look at the length of the data array * property and will just respect the length property. */ packet.setLength(receiveBufferSize); } } localSock.receive(packet); // get lost if we are no longer running. if (!running) return; logger.finest("received datagram"); RawMessage rawMessage = new RawMessage( packet.getData(), packet.getLength(), new TransportAddress( packet.getAddress(), packet.getPort(), listenAddress.getTransport()), listenAddress); messageQueue.add(rawMessage); } catch (SocketException ex) { if (running) { logger.log( Level.WARNING, "Connector died: " + listenAddress + " -> " + remoteAddress, ex); stop(); // Something wrong has happened errorHandler.handleFatalError( this, "A socket exception was thrown" + " while trying to receive a message.", ex); } else { // The exception was most probably caused by calling // this.stop(). } } catch (ClosedChannelException cce) { logger.log(Level.WARNING, "A net access point has gone useless:", cce); stop(); errorHandler.handleFatalError( this, "ClosedChannelException occurred while listening" + " for messages!", cce); } catch (IOException ex) { logger.log(Level.WARNING, "A net access point has gone useless:", ex); errorHandler.handleError(ex.getMessage(), ex); // do not stop the thread; } catch (Throwable ex) { logger.log(Level.WARNING, "A net access point has gone useless:", ex); stop(); errorHandler.handleFatalError( this, "Unknown error occurred while listening for messages!", ex); } } } /** Makes the access point stop listening on its socket. */ protected void stop() { synchronized (sockLock) { this.running = false; if (this.sock != null) { this.sock.close(); this.sock = null; } } } /** * Sends message through this access point's socket. * * @param message the bytes to send. * @param address message destination. * @throws IOException if an exception occurs while sending the message. */ void sendMessage(byte[] message, TransportAddress address) throws IOException { DatagramPacket datagramPacket = new DatagramPacket(message, 0, message.length, address); sock.send(datagramPacket); } /** * Returns a String representation of the object. * * @return a String representation of the object. */ @Override public String toString() { return "ice4j.Connector@" + listenAddress + " status: " + (running ? "not" : "") + " running"; } /** * Returns the <tt>TransportAddress</tt> that this access point is bound on. * * @return the <tt>TransportAddress</tt> associated with this AP. */ TransportAddress getListenAddress() { return listenAddress; } /** * Returns the remote <tt>TransportAddress</tt> in case of TCP, or <tt>null</tt> in case of UDP. * * @return the remote <tt>TransportAddress</tt> in case of TCP, or <tt>null</tt> in case of UDP. */ TransportAddress getRemoteAddress() { return remoteAddress; } }
/** * Implements a <tt>CandidateHarvester</tt> which gathers <tt>Candidate</tt>s for a specified {@link * Component} using Jingle Nodes as defined in XEP 278 "Jingle Relay Nodes". * * @author Sebastien Vincent */ public class JingleNodesHarvester extends AbstractCandidateHarvester { /** * The <tt>Logger</tt> used by the <tt>JingleNodesHarvester</tt> class and its instances for * logging output. */ private static final Logger logger = Logger.getLogger(JingleNodesHarvester.class.getName()); /** XMPP connection. */ private SmackServiceNode serviceNode = null; /** * JingleNodes relay allocate two address/port couple for us. Due to the architecture of Ice4j * that harvest address for each component, we store the second address/port couple. */ private TransportAddress localAddressSecond = null; /** * JingleNodes relay allocate two address/port couple for us. Due to the architecture of Ice4j * that harvest address for each component, we store the second address/port couple. */ private TransportAddress relayedAddressSecond = null; /** * Constructor. * * @param serviceNode the <tt>SmackServiceNode</tt> */ public JingleNodesHarvester(SmackServiceNode serviceNode) { this.serviceNode = serviceNode; } /** * Gathers Jingle Nodes candidates for all host <tt>Candidate</tt>s that are already present in * the specified <tt>component</tt>. This method relies on the specified <tt>component</tt> to * already contain all its host candidates so that it would resolve them. * * @param component the {@link Component} that we'd like to gather candidate Jingle Nodes * <tt>Candidate</tt>s for * @return the <tt>LocalCandidate</tt>s gathered by this <tt>CandidateHarvester</tt> */ @Override public synchronized Collection<LocalCandidate> harvest(Component component) { logger.info("harvest Jingle Nodes"); Collection<LocalCandidate> candidates = new HashSet<LocalCandidate>(); String ip = null; int port = -1; /* if we have already a candidate (RTCP) allocated, get it */ if (localAddressSecond != null && relayedAddressSecond != null) { LocalCandidate candidate = createJingleNodesCandidate(relayedAddressSecond, component, localAddressSecond); // try to add the candidate to the component and then only add it to // the harvest not redundant (not sure how it could be red. but ...) if (component.addLocalCandidate(candidate)) { candidates.add(candidate); } localAddressSecond = null; relayedAddressSecond = null; return candidates; } XMPPConnection conn = serviceNode.getConnection(); JingleChannelIQ ciq = null; if (serviceNode != null) { final TrackerEntry preferred = serviceNode.getPreferedRelay(); if (preferred != null) { ciq = SmackServiceNode.getChannel(conn, preferred.getJid()); } } if (ciq != null) { ip = ciq.getHost(); port = ciq.getRemoteport(); if (logger.isInfoEnabled()) { logger.info( "JN relay: " + ip + " remote port:" + port + " local port: " + ciq.getLocalport()); } if (ip == null || ciq.getRemoteport() == 0) { logger.warn("JN relay ignored because ip was null or port 0"); return candidates; } // Drop the scope or interface name if the relay sends it // along in its IPv6 address. The scope/ifname is only valid on the // host that owns the IP and we don't need it here. int scopeIndex = ip.indexOf('%'); if (scopeIndex > 0) { logger.warn("Dropping scope from assumed IPv6 address " + ip); ip = ip.substring(0, scopeIndex); } /* RTP */ TransportAddress relayedAddress = new TransportAddress(ip, port, Transport.UDP); TransportAddress localAddress = new TransportAddress(ip, ciq.getLocalport(), Transport.UDP); LocalCandidate local = createJingleNodesCandidate(relayedAddress, component, localAddress); /* RTCP */ relayedAddressSecond = new TransportAddress(ip, port + 1, Transport.UDP); localAddressSecond = new TransportAddress(ip, ciq.getLocalport() + 1, Transport.UDP); // try to add the candidate to the component and then only add it to // the harvest not redundant (not sure how it could be red. but ...) if (component.addLocalCandidate(local)) { candidates.add(local); } } return candidates; } /** * Creates a new <tt>JingleNodesRelayedCandidate</tt> instance which is to represent a specific * <tt>TransportAddress</tt>. * * @param transportAddress the <tt>TransportAddress</tt> allocated by the relay * @param component the <tt>Component</tt> for which the candidate will be added * @param localEndPoint <tt>TransportAddress</tt> of the Jingle Nodes relay where we will send our * packet. * @return a new <tt>JingleNodesRelayedCandidate</tt> instance which represents the specified * <tt>TransportAddress</tt> */ protected JingleNodesCandidate createJingleNodesCandidate( TransportAddress transportAddress, Component component, TransportAddress localEndPoint) { JingleNodesCandidate cand = null; try { cand = new JingleNodesCandidate(transportAddress, component, localEndPoint); IceSocketWrapper stunSocket = cand.getStunSocket(null); cand.getStunStack().addSocket(stunSocket); } catch (Throwable e) { logger.debug("Exception occurred when creating JingleNodesCandidate: " + e); } return cand; } }
/** * Represents the harvesting of STUN <tt>Candidates</tt> for a specific <tt>HostCandidate</tt> * performed by a specific <tt>StunCandidateHarvester</tt>. * * @author Lyubomir Marinov */ public class StunCandidateHarvest extends AbstractResponseCollector { /** * The <tt>Logger</tt> used by the <tt>StunCandidateHarvest</tt> class and its instances for * logging output. */ private static final Logger logger = Logger.getLogger(StunCandidateHarvest.class.getName()); /** * The constant which defines an empty array with <tt>LocalCandidate</tt> element type. Explicitly * defined in order to reduce unnecessary allocations. */ private static final LocalCandidate[] NO_CANDIDATES = new LocalCandidate[0]; /** * The value of the <tt>sendKeepAliveMessage</tt> property of <tt>StunCandidateHarvest</tt> which * specifies that no sending of STUN keep-alive messages is to performed for the purposes of * keeping the <tt>Candidate</tt>s harvested by the <tt>StunCandidateHarvester</tt> in question * alive. */ protected static final long SEND_KEEP_ALIVE_MESSAGE_INTERVAL_NOT_SPECIFIED = 0; /** The list of <tt>Candidate</tt>s harvested for {@link #hostCandidate} by this harvest. */ private final List<LocalCandidate> candidates = new LinkedList<>(); /** * The indicator which determines whether this <tt>StunCandidateHarvest</tt> has completed the * harvesting of <tt>Candidate</tt>s for {@link #hostCandidate}. */ private boolean completedResolvingCandidate = false; /** * The <tt>StunCandidateHarvester</tt> performing the harvesting of STUN <tt>Candidate</tt>s for a * <tt>Component</tt> which this harvest is part of. */ public final StunCandidateHarvester harvester; /** The <tt>HostCandidate</tt> the STUN harvesting of which is represented by this instance. */ public final HostCandidate hostCandidate; /** The <tt>LongTermCredential</tt> used by this instance. */ private LongTermCredentialSession longTermCredentialSession; /** * The STUN <tt>Request</tt>s which have been sent by this instance, have not received a STUN * <tt>Response</tt> yet and have not timed out. Put in place to avoid a limitation of the * <tt>ResponseCollector</tt> and its use of <tt>StunMessageEvent</tt> which do not make the STUN * <tt>Request</tt> to which a STUN <tt>Response</tt> responds available though it is known in * <tt>StunClientTransaction</tt>. */ private final Map<TransactionID, Request> requests = new HashMap<>(); /** * The interval in milliseconds at which a new STUN keep-alive message is to be sent to the STUN * server associated with the <tt>StunCandidateHarvester</tt> of this instance in order to keep * one of the <tt>Candidate</tt>s harvested by this instance alive. */ private long sendKeepAliveMessageInterval = SEND_KEEP_ALIVE_MESSAGE_INTERVAL_NOT_SPECIFIED; /** * The <tt>Object</tt> used to synchronize access to the members related to the sending of STUN * keep-alive messages to the STUN server associated with the <tt>StunCandidateHarvester</tt> of * this instance. */ private final Object sendKeepAliveMessageSyncRoot = new Object(); /** * The <tt>Thread</tt> which sends the STUN keep-alive messages to the STUN server associated with * the <tt>StunCandidateHarvester</tt> of this instance in order to keep the <tt>Candidate</tt>s * harvested by this instance alive. */ private Thread sendKeepAliveMessageThread; /** * The time (stamp) in milliseconds of the last call to {@link #sendKeepAliveMessage()} which * completed without throwing an exception. <b>Note</b>: It doesn't mean that the keep-alive * message was a STUN <tt>Request</tt> and it received a success STUN <tt>Response</tt>. */ private long sendKeepAliveMessageTime = -1; /** * Initializes a new <tt>StunCandidateHarvest</tt> which is to represent the harvesting of STUN * <tt>Candidate</tt>s for a specific <tt>HostCandidate</tt> performed by a specific * <tt>StunCandidateHarvester</tt>. * * @param harvester the <tt>StunCandidateHarvester</tt> which is performing the STUN harvesting * @param hostCandidate the <tt>HostCandidate</tt> for which STUN <tt>Candidate</tt>s are to be * harvested */ public StunCandidateHarvest(StunCandidateHarvester harvester, HostCandidate hostCandidate) { this.harvester = harvester; this.hostCandidate = hostCandidate; } /** * Adds a specific <tt>LocalCandidate</tt> to the list of <tt>LocalCandidate</tt>s harvested for * {@link #hostCandidate} by this harvest. * * @param candidate the <tt>LocalCandidate</tt> to be added to the list of * <tt>LocalCandidate</tt>s harvested for {@link #hostCandidate} by this harvest * @return <tt>true</tt> if the list of <tt>LocalCandidate</tt>s changed as a result of the method * invocation; otherwise, <tt>false</tt> */ protected boolean addCandidate(LocalCandidate candidate) { boolean added; // try to add the candidate to the component and then only add it to the // harvest if it wasn't deemed redundant if (!candidates.contains(candidate) && hostCandidate.getParentComponent().addLocalCandidate(candidate)) { added = candidates.add(candidate); } else { added = false; } return added; } /** * Adds the <tt>Attribute</tt>s to a specific <tt>Request</tt> which support the STUN short-term * credential mechanism if the mechanism in question is utilized by this * <tt>StunCandidateHarvest</tt> (i.e. by the associated <tt>StunCandidateHarvester</tt>). * * @param request the <tt>Request</tt> to which to add the <tt>Attribute</tt>s supporting the STUN * short-term credential mechanism if the mechanism in question is utilized by this * <tt>StunCandidateHarvest</tt> * @return <tt>true</tt> if the STUN short-term credential mechanism is actually utilized by this * <tt>StunCandidateHarvest</tt> for the specified <tt>request</tt>; otherwise, <tt>false</tt> */ protected boolean addShortTermCredentialAttributes(Request request) { String shortTermCredentialUsername = harvester.getShortTermCredentialUsername(); if (shortTermCredentialUsername != null) { request.putAttribute(AttributeFactory.createUsernameAttribute(shortTermCredentialUsername)); request.putAttribute( AttributeFactory.createMessageIntegrityAttribute(shortTermCredentialUsername)); return true; } else return false; } /** * Completes the harvesting of <tt>Candidate</tt>s for {@link #hostCandidate}. Notifies {@link * #harvester} about the completion of the harvesting of <tt>Candidate</tt> for * <tt>hostCandidate</tt> performed by this <tt>StunCandidateHarvest</tt>. * * @param request the <tt>Request</tt> sent by this <tt>StunCandidateHarvest</tt> with which the * harvesting of <tt>Candidate</tt>s for <tt>hostCandidate</tt> has completed * @param response the <tt>Response</tt> received by this <tt>StunCandidateHarvest</tt>, if any, * with which the harvesting of <tt>Candidate</tt>s for <tt>hostCandidate</tt> has completed * @return <tt>true</tt> if the harvesting of <tt>Candidate</tt>s for <tt>hostCandidate</tt> * performed by this <tt>StunCandidateHarvest</tt> has completed; otherwise, <tt>false</tt> */ protected boolean completedResolvingCandidate(Request request, Response response) { if (!completedResolvingCandidate) { completedResolvingCandidate = true; try { if (((response == null) || !response.isSuccessResponse()) && (longTermCredentialSession != null)) { harvester .getStunStack() .getCredentialsManager() .unregisterAuthority(longTermCredentialSession); longTermCredentialSession = null; } } finally { harvester.completedResolvingCandidate(this); } } return completedResolvingCandidate; } /** * Determines whether a specific <tt>LocalCandidate</tt> is contained in the list of * <tt>LocalCandidate</tt>s harvested for {@link #hostCandidate} by this harvest. * * @param candidate the <tt>LocalCandidate</tt> to look for in the list of * <tt>LocalCandidate</tt>s harvested for {@link #hostCandidate} by this harvest * @return <tt>true</tt> if the list of <tt>LocalCandidate</tt>s contains the specified * <tt>candidate</tt>; otherwise, <tt>false</tt> */ protected boolean containsCandidate(LocalCandidate candidate) { if (candidate != null) { LocalCandidate[] candidates = getCandidates(); if ((candidates != null) && (candidates.length != 0)) { for (LocalCandidate c : candidates) { if (candidate.equals(c)) return true; } } } return false; } /** * Creates new <tt>Candidate</tt>s determined by a specific STUN <tt>Response</tt>. * * @param response the received STUN <tt>Response</tt> */ protected void createCandidates(Response response) { createServerReflexiveCandidate(response); } /** * Creates a new STUN <tt>Message</tt> to be sent to the STUN server associated with the * <tt>StunCandidateHarvester</tt> of this instance in order to keep a specific * <tt>LocalCandidate</tt> (harvested by this instance) alive. * * @param candidate the <tt>LocalCandidate</tt> (harvested by this instance) to create a new * keep-alive STUN message for * @return a new keep-alive STUN <tt>Message</tt> for the specified <tt>candidate</tt> or * <tt>null</tt> if no keep-alive sending is to occur * @throws StunException if anything goes wrong while creating the new keep-alive STUN * <tt>Message</tt> for the specified <tt>candidate</tt> or the candidate is of an unsupported * <tt>CandidateType</tt> */ protected Message createKeepAliveMessage(LocalCandidate candidate) throws StunException { /* * We'll not be keeping a STUN Binding alive for now. If we decide to in * the future, we'll have to create a Binding Indication and add support * for sending it. */ if (CandidateType.SERVER_REFLEXIVE_CANDIDATE.equals(candidate.getType())) return null; else { throw new StunException(StunException.ILLEGAL_ARGUMENT, "candidate"); } } /** * Creates a new <tt>Request</tt> instance which is to be sent by this * <tt>StunCandidateHarvest</tt> in order to retry a specific <tt>Request</tt>. For example, the * long-term credential mechanism dictates that a <tt>Request</tt> is first sent by the client * without any credential-related attributes, then it gets challenged by the server and the client * retries the original <tt>Request</tt> with the appropriate credential-related attributes in * response. * * @param request the <tt>Request</tt> which is to be retried by this * <tt>StunCandidateHarvest</tt> * @return the new <tt>Request</tt> instance which is to be sent by this * <tt>StunCandidateHarvest</tt> in order to retry the specified <tt>request</tt> */ protected Request createRequestToRetry(Request request) { switch (request.getMessageType()) { case Message.BINDING_REQUEST: return MessageFactory.createBindingRequest(); default: throw new IllegalArgumentException("request.messageType"); } } /** * Creates a new <tt>Request</tt> which is to be sent to {@link StunCandidateHarvester#stunServer} * in order to start resolving {@link #hostCandidate}. * * @return a new <tt>Request</tt> which is to be sent to {@link StunCandidateHarvester#stunServer} * in order to start resolving {@link #hostCandidate} */ protected Request createRequestToStartResolvingCandidate() { return MessageFactory.createBindingRequest(); } /** * Creates and starts the {@link #sendKeepAliveMessageThread} which is to send STUN keep-alive * <tt>Message</tt>s to the STUN server associated with the <tt>StunCandidateHarvester</tt> of * this instance in order to keep the <tt>Candidate</tt>s harvested by this instance alive. */ private void createSendKeepAliveMessageThread() { synchronized (sendKeepAliveMessageSyncRoot) { Thread t = new SendKeepAliveMessageThread(this); t.setDaemon(true); t.setName(getClass().getName() + ".sendKeepAliveMessageThread: " + hostCandidate); boolean started = false; sendKeepAliveMessageThread = t; try { t.start(); started = true; } finally { if (!started && (sendKeepAliveMessageThread == t)) sendKeepAliveMessageThread = null; } } } /** * Creates a <tt>ServerReflexiveCandidate</tt> using {@link #hostCandidate} as its base and the * <tt>XOR-MAPPED-ADDRESS</tt> attribute in <tt>response</tt> for the actual * <tt>TransportAddress</tt> of the new candidate. If the message is malformed and/or does not * contain the corresponding attribute, this method simply has no effect. * * @param response the STUN <tt>Response</tt> which is supposed to contain the address we should * use for the new candidate */ protected void createServerReflexiveCandidate(Response response) { TransportAddress addr = getMappedAddress(response); if (addr != null) { ServerReflexiveCandidate srvrRflxCand = createServerReflexiveCandidate(addr); if (srvrRflxCand != null) { try { addCandidate(srvrRflxCand); } finally { // Free srvrRflxCand if it has not been consumed. if (!containsCandidate(srvrRflxCand)) { try { srvrRflxCand.free(); } catch (Exception ex) { if (logger.isLoggable(Level.FINE)) { logger.log( Level.FINE, "Failed to free" + " ServerReflexiveCandidate: " + srvrRflxCand, ex); } } } } } } } /** * Creates a new <tt>ServerReflexiveCandidate</tt> instance which is to represent a specific * <tt>TransportAddress</tt> harvested through {@link #hostCandidate} and the STUN server * associated with {@link #harvester}. * * @param transportAddress the <tt>TransportAddress</tt> to be represented by the new * <tt>ServerReflexiveCandidate</tt> instance * @return a new <tt>ServerReflexiveCandidate</tt> instance which represents the specified * <tt>TransportAddress</tt> harvested through {@link #hostCandidate} and the STUN server * associated with {@link #harvester} */ protected ServerReflexiveCandidate createServerReflexiveCandidate( TransportAddress transportAddress) { return new ServerReflexiveCandidate( transportAddress, hostCandidate, harvester.stunServer, CandidateExtendedType.STUN_SERVER_REFLEXIVE_CANDIDATE); } /** * Runs in {@link #sendKeepAliveMessageThread} to notify this instance that * <tt>sendKeepAliveMessageThread</tt> is about to exit. */ private void exitSendKeepAliveMessageThread() { synchronized (sendKeepAliveMessageSyncRoot) { if (sendKeepAliveMessageThread == Thread.currentThread()) sendKeepAliveMessageThread = null; /* * Well, if the currentThread is finishing and this instance is * still to send keep-alive messages, we'd better start another * Thread for the purpose to continue the work that the * currentThread was supposed to carry out. */ if ((sendKeepAliveMessageThread == null) && (sendKeepAliveMessageInterval != SEND_KEEP_ALIVE_MESSAGE_INTERVAL_NOT_SPECIFIED)) { createSendKeepAliveMessageThread(); } } } /** * Gets the number of <tt>Candidate</tt>s harvested for {@link #hostCandidate} during this * harvest. * * @return the number of <tt>Candidate</tt>s harvested for {@link #hostCandidate} during this * harvest */ int getCandidateCount() { return candidates.size(); } /** * Gets the <tt>Candidate</tt>s harvested for {@link #hostCandidate} during this harvest. * * @return an array containing the <tt>Candidate</tt>s harvested for {@link #hostCandidate} during * this harvest */ LocalCandidate[] getCandidates() { return candidates.toArray(NO_CANDIDATES); } /** * Gets the <tt>TransportAddress</tt> specified in the XOR-MAPPED-ADDRESS attribute of a specific * <tt>Response</tt>. * * @param response the <tt>Response</tt> from which the XOR-MAPPED-ADDRESS attribute is to be * retrieved and its <tt>TransportAddress</tt> value is to be returned * @return the <tt>TransportAddress</tt> specified in the XOR-MAPPED-ADDRESS attribute of * <tt>response</tt> */ protected TransportAddress getMappedAddress(Response response) { Attribute attribute = response.getAttribute(Attribute.XOR_MAPPED_ADDRESS); if (attribute instanceof XorMappedAddressAttribute) { return ((XorMappedAddressAttribute) attribute).getAddress(response.getTransactionID()); } // old STUN servers (RFC3489) send MAPPED-ADDRESS address attribute = response.getAttribute(Attribute.MAPPED_ADDRESS); if (attribute instanceof MappedAddressAttribute) { return ((MappedAddressAttribute) attribute).getAddress(); } else return null; } /** * Notifies this <tt>StunCandidateHarvest</tt> that a specific STUN <tt>Request</tt> has been * challenged for a long-term credential (as the short-term credential mechanism does not utilize * challenging) in a specific <tt>realm</tt> and with a specific <tt>nonce</tt>. * * @param realm the realm in which the specified STUN <tt>Request</tt> has been challenged for a * long-term credential * @param nonce the nonce with which the specified STUN <tt>Request</tt> has been challenged for a * long-term credential * @param request the STUN <tt>Request</tt> which has been challenged for a long-term credential * @param requestTransactionID the <tt>TransactionID</tt> of <tt>request</tt> because * <tt>request</tt> only has it as a <tt>byte</tt> array and <tt>TransactionID</tt> is * required for the <tt>applicationData</tt> property value * @return <tt>true</tt> if the challenge has been processed and this * <tt>StunCandidateHarvest</tt> is to continue processing STUN <tt>Response</tt>s; otherwise, * <tt>false</tt> * @throws StunException if anything goes wrong while processing the challenge */ private boolean processChallenge( byte[] realm, byte[] nonce, Request request, TransactionID requestTransactionID) throws StunException { UsernameAttribute usernameAttribute = (UsernameAttribute) request.getAttribute(Attribute.USERNAME); if (usernameAttribute == null) { if (longTermCredentialSession == null) { LongTermCredential longTermCredential = harvester.createLongTermCredential(this, realm); if (longTermCredential == null) { // The long-term credential mechanism is not being utilized. return false; } else { longTermCredentialSession = new LongTermCredentialSession(longTermCredential, realm); harvester .getStunStack() .getCredentialsManager() .registerAuthority(longTermCredentialSession); } } else { /* * If we're going to use the long-term credential to retry the * request, the long-term credential should be for the request * in terms of realm. */ if (!longTermCredentialSession.realmEquals(realm)) return false; } } else { /* * If we sent a USERNAME in our request, then we had the long-term * credential at the time we sent the request in question. */ if (longTermCredentialSession == null) return false; else { /* * If we're going to use the long-term credential to retry the * request, the long-term credential should be for the request * in terms of username. */ if (!longTermCredentialSession.usernameEquals(usernameAttribute.getUsername())) return false; else { // And it terms of realm, of course. if (!longTermCredentialSession.realmEquals(realm)) return false; } } } /* * The nonce is either becoming known for the first time or being * updated after the old one has gone stale. */ longTermCredentialSession.setNonce(nonce); Request retryRequest = createRequestToRetry(request); TransactionID retryRequestTransactionID = null; if (retryRequest != null) { if (requestTransactionID != null) { Object applicationData = requestTransactionID.getApplicationData(); if (applicationData != null) { byte[] retryRequestTransactionIDAsBytes = retryRequest.getTransactionID(); retryRequestTransactionID = (retryRequestTransactionIDAsBytes == null) ? TransactionID.createNewTransactionID() : TransactionID.createTransactionID( harvester.getStunStack(), retryRequestTransactionIDAsBytes); retryRequestTransactionID.setApplicationData(applicationData); } } retryRequestTransactionID = sendRequest(retryRequest, false, retryRequestTransactionID); } return (retryRequestTransactionID != null); } /** * Notifies this <tt>StunCandidateHarvest</tt> that a specific STUN <tt>Response</tt> has been * received and it challenges a specific STUN <tt>Request</tt> for a long-term credential (as the * short-term credential mechanism does not utilize challenging). * * @param response the STUN <tt>Response</tt> which has been received * @param request the STUN <tt>Request</tt> to which <tt>response</tt> responds and which it * challenges for a long-term credential * @return <tt>true</tt> if the challenge has been processed and this * <tt>StunCandidateHarvest</tt> is to continue processing STUN <tt>Response</tt>s; otherwise, * <tt>false</tt> * @param transactionID the <tt>TransactionID</tt> of <tt>response</tt> and <tt>request</tt> * because <tt>response</tt> and <tt>request</tt> only have it as a <tt>byte</tt> array and * <tt>TransactionID</tt> is required for the <tt>applicationData</tt> property value * @throws StunException if anything goes wrong while processing the challenge */ private boolean processChallenge(Response response, Request request, TransactionID transactionID) throws StunException { boolean retried = false; if (response.getAttributeCount() > 0) { /* * The response SHOULD NOT contain a USERNAME or * MESSAGE-INTEGRITY attribute. */ char[] excludedResponseAttributeTypes = new char[] {Attribute.USERNAME, Attribute.MESSAGE_INTEGRITY}; boolean challenge = true; for (char excludedResponseAttributeType : excludedResponseAttributeTypes) { if (response.containsAttribute(excludedResponseAttributeType)) { challenge = false; break; } } if (challenge) { // This response MUST include a REALM value. RealmAttribute realmAttribute = (RealmAttribute) response.getAttribute(Attribute.REALM); if (realmAttribute == null) challenge = false; else { // The response MUST include a NONCE. NonceAttribute nonceAttribute = (NonceAttribute) response.getAttribute(Attribute.NONCE); if (nonceAttribute == null) challenge = false; else { retried = processChallenge( realmAttribute.getRealm(), nonceAttribute.getNonce(), request, transactionID); } } } } return retried; } /** * Notifies this <tt>StunCandidateHarvest</tt> that a specific <tt>Request</tt> has either * received an error <tt>Response</tt> or has failed to receive any <tt>Response</tt>. Allows * extenders to override and process unhandled error <tt>Response</tt>s or failures. The default * implementation does no processing. * * @param response the error <tt>Response</tt> which has been received for <tt>request</tt> * @param request the <tt>Request</tt> to which <tt>Response</tt> responds * @param transactionID the <tt>TransactionID</tt> of <tt>response</tt> and <tt>request</tt> * because <tt>response</tt> and <tt>request</tt> only have it as a <tt>byte</tt> array and * <tt>TransactionID</tt> is required for the <tt>applicationData</tt> property value * @return <tt>true</tt> if the error or failure condition has been processed and this instance * can continue its execution (e.g. the resolution of the candidate) as if it was expected; * otherwise, <tt>false</tt> */ protected boolean processErrorOrFailure( Response response, Request request, TransactionID transactionID) { return false; } /** * Notifies this <tt>ResponseCollector</tt> that a transaction described by the specified * <tt>BaseStunMessageEvent</tt> has failed. The possible reasons for the failure include * timeouts, unreachable destination, etc. * * @param event the <tt>BaseStunMessageEvent</tt> which describes the failed transaction and the * runtime type of which specifies the failure reason * @see AbstractResponseCollector#processFailure(BaseStunMessageEvent) */ @Override protected void processFailure(BaseStunMessageEvent event) { TransactionID transactionID = event.getTransactionID(); logger.finest("A transaction expired: tranid=" + transactionID); logger.finest("localAddr=" + hostCandidate); /* * Clean up for the purposes of the workaround which determines the STUN * Request to which a STUN Response responds. */ Request request; synchronized (requests) { request = requests.remove(transactionID); } if (request == null) { Message message = event.getMessage(); if (message instanceof Request) request = (Request) message; } boolean completedResolvingCandidate = true; try { if (processErrorOrFailure(null, request, transactionID)) completedResolvingCandidate = false; } finally { if (completedResolvingCandidate) completedResolvingCandidate(request, null); } } /** * Notifies this <tt>ResponseCollector</tt> that a STUN response described by the specified * <tt>StunResponseEvent</tt> has been received. * * @param event the <tt>StunResponseEvent</tt> which describes the received STUN response * @see ResponseCollector#processResponse(StunResponseEvent) */ @Override public void processResponse(StunResponseEvent event) { TransactionID transactionID = event.getTransactionID(); logger.finest("Received a message: tranid= " + transactionID); logger.finest("localCand= " + hostCandidate); /* * Clean up for the purposes of the workaround which determines the STUN * Request to which a STUN Response responds. */ synchronized (requests) { requests.remove(transactionID); } // At long last, do start handling the received STUN Response. Response response = event.getResponse(); Request request = event.getRequest(); boolean completedResolvingCandidate = true; try { if (response.isSuccessResponse()) { // Authentication and Message-Integrity Mechanisms if (request.containsAttribute(Attribute.MESSAGE_INTEGRITY)) { MessageIntegrityAttribute messageIntegrityAttribute = (MessageIntegrityAttribute) response.getAttribute(Attribute.MESSAGE_INTEGRITY); /* * RFC 5389: If MESSAGE-INTEGRITY was absent, the response * MUST be discarded, as if it was never received. */ if (messageIntegrityAttribute == null) return; UsernameAttribute usernameAttribute = (UsernameAttribute) request.getAttribute(Attribute.USERNAME); /* * For a request or indication message, the agent MUST * include the USERNAME and MESSAGE-INTEGRITY attributes in * the message. */ if (usernameAttribute == null) return; if (!harvester .getStunStack() .validateMessageIntegrity( messageIntegrityAttribute, LongTermCredential.toString(usernameAttribute.getUsername()), !request.containsAttribute(Attribute.REALM) && !request.containsAttribute(Attribute.NONCE), event.getRawMessage())) return; } processSuccess(response, request, transactionID); } else { ErrorCodeAttribute errorCodeAttr = (ErrorCodeAttribute) response.getAttribute(Attribute.ERROR_CODE); if ((errorCodeAttr != null) && (errorCodeAttr.getErrorClass() == 4)) { try { switch (errorCodeAttr.getErrorNumber()) { case 1: // 401 Unauthorized if (processUnauthorized(response, request, transactionID)) completedResolvingCandidate = false; break; case 38: // 438 Stale Nonce if (processStaleNonce(response, request, transactionID)) completedResolvingCandidate = false; break; } } catch (StunException sex) { completedResolvingCandidate = true; } } if (completedResolvingCandidate && processErrorOrFailure(response, request, transactionID)) completedResolvingCandidate = false; } } finally { if (completedResolvingCandidate) completedResolvingCandidate(request, response); } } /** * Handles a specific STUN error <tt>Response</tt> with error code "438 Stale Nonce" to a specific * STUN <tt>Request</tt>. * * @param response the received STUN error <tt>Response</tt> with error code "438 Stale Nonce" * which is to be handled * @param request the STUN <tt>Request</tt> to which <tt>response</tt> responds * @param transactionID the <tt>TransactionID</tt> of <tt>response</tt> and <tt>request</tt> * because <tt>response</tt> and <tt>request</tt> only have it as a <tt>byte</tt> array and * <tt>TransactionID</tt> is required for the <tt>applicationData</tt> property value * @return <tt>true</tt> if the specified STUN error <tt>response</tt> was successfully handled; * <tt>false</tt>, otherwise * @throws StunException if anything goes wrong while handling the specified "438 Stale Nonce" * error <tt>response</tt> */ private boolean processStaleNonce(Response response, Request request, TransactionID transactionID) throws StunException { /* * The request MUST contain USERNAME, REALM, NONCE and MESSAGE-INTEGRITY * attributes. */ boolean challenge; if (request.getAttributeCount() > 0) { char[] includedRequestAttributeTypes = new char[] { Attribute.USERNAME, Attribute.REALM, Attribute.NONCE, Attribute.MESSAGE_INTEGRITY }; challenge = true; for (char includedRequestAttributeType : includedRequestAttributeTypes) { if (!request.containsAttribute(includedRequestAttributeType)) { challenge = false; break; } } } else challenge = false; return (challenge && processChallenge(response, request, transactionID)); } /** * Handles a specific STUN success <tt>Response</tt> to a specific STUN <tt>Request</tt>. * * @param response the received STUN success <tt>Response</tt> which is to be handled * @param request the STUN <tt>Request</tt> to which <tt>response</tt> responds * @param transactionID the <tt>TransactionID</tt> of <tt>response</tt> and <tt>request</tt> * because <tt>response</tt> and <tt>request</tt> only have it as a <tt>byte</tt> array and * <tt>TransactionID</tt> is required for the <tt>applicationData</tt> property value */ protected void processSuccess(Response response, Request request, TransactionID transactionID) { if (!completedResolvingCandidate) createCandidates(response); } /** * Handles a specific STUN error <tt>Response</tt> with error code "401 Unauthorized" to a * specific STUN <tt>Request</tt>. * * @param response the received STUN error <tt>Response</tt> with error code "401 Unauthorized" * which is to be handled * @param request the STUN <tt>Request</tt> to which <tt>response</tt> responds * @param transactionID the <tt>TransactionID</tt> of <tt>response</tt> and <tt>request</tt> * because <tt>response</tt> and <tt>request</tt> only have it as a <tt>byte</tt> array and * <tt>TransactionID</tt> is required for the <tt>applicationData</tt> property value * @return <tt>true</tt> if the specified STUN error <tt>response</tt> was successfully handled; * <tt>false</tt>, otherwise * @throws StunException if anything goes wrong while handling the specified "401 Unauthorized" * error <tt>response</tt> */ private boolean processUnauthorized( Response response, Request request, TransactionID transactionID) throws StunException { /* * If the response is a challenge, retry the request with a new * transaction. */ boolean challenge = true; /* * The client SHOULD omit the USERNAME, MESSAGE-INTEGRITY, REALM, and * NONCE attributes from the "First Request". */ if (request.getAttributeCount() > 0) { char[] excludedRequestAttributeTypes = new char[] { Attribute.USERNAME, Attribute.MESSAGE_INTEGRITY, Attribute.REALM, Attribute.NONCE }; for (char excludedRequestAttributeType : excludedRequestAttributeTypes) { if (request.containsAttribute(excludedRequestAttributeType)) { challenge = false; break; } } } return (challenge && processChallenge(response, request, transactionID)); } /** * Runs in {@link #sendKeepAliveMessageThread} and sends STUN keep-alive <tt>Message</tt>s to the * STUN server associated with the <tt>StunCandidateHarvester</tt> of this instance. * * @return <tt>true</tt> if the method is to be invoked again; otherwise, <tt>false</tt> */ private boolean runInSendKeepAliveMessageThread() { synchronized (sendKeepAliveMessageSyncRoot) { // Since we're going to #wait, make sure we're not canceled yet. if (sendKeepAliveMessageThread != Thread.currentThread()) return false; if (sendKeepAliveMessageInterval == SEND_KEEP_ALIVE_MESSAGE_INTERVAL_NOT_SPECIFIED) { return false; } // Determine the amount of milliseconds that we'll have to #wait. long timeout; if (sendKeepAliveMessageTime == -1) { /* * If we're just starting, don't just go and send a new STUN * keep-alive message but rather wait for the whole interval. */ timeout = sendKeepAliveMessageInterval; } else { timeout = sendKeepAliveMessageTime + sendKeepAliveMessageInterval - System.currentTimeMillis(); } // At long last, #wait if necessary. if (timeout > 0) { try { sendKeepAliveMessageSyncRoot.wait(timeout); } catch (InterruptedException iex) { } /* * Apart from being the time to send the STUN keep-alive * message, it could be that we've experienced a spurious * wake-up or that we've been canceled. */ return true; } } sendKeepAliveMessageTime = System.currentTimeMillis(); try { sendKeepAliveMessage(); } catch (StunException sex) { logger.log(Level.INFO, "Failed to send STUN keep-alive message.", sex); } return true; } /** * Sends a new STUN <tt>Message</tt> to the STUN server associated with the * <tt>StunCandidateHarvester</tt> of this instance in order to keep a <tt>LocalCandidate</tt> * harvested by this instance alive. * * @throws StunException if anything goes wrong while sending a new keep-alive STUN * <tt>Message</tt> */ protected void sendKeepAliveMessage() throws StunException { for (LocalCandidate candidate : getCandidates()) if (sendKeepAliveMessage(candidate)) break; } /** * Sends a new STUN <tt>Message</tt> to the STUN server associated with the * <tt>StunCandidateHarvester</tt> of this instance in order to keep a specific * <tt>LocalCandidate</tt> alive. * * @param candidate the <tt>LocalCandidate</tt> to send a new keep-alive STUN <tt>Message</tt> for * @return <tt>true</tt> if a new STUN <tt>Message</tt> was sent to the STUN server associated * with the <tt>StunCandidateHarvester</tt> of this instance or <tt>false</tt> if the STUN * kee-alive functionality was not been used for the specified <tt>candidate</tt> * @throws StunException if anything goes wrong while sending the new keep-alive STUN * <tt>Message</tt> for the specified <tt>candidate</tt> */ protected boolean sendKeepAliveMessage(LocalCandidate candidate) throws StunException { Message keepAliveMessage = createKeepAliveMessage(candidate); /* * The #createKeepAliveMessage method javadoc says it returns null when * the STUN keep-alive functionality of this StunCandidateHarvest is to * not be utilized. */ if (keepAliveMessage == null) { return false; } else if (keepAliveMessage instanceof Request) { return (sendRequest((Request) keepAliveMessage, false, null) != null); } else { throw new StunException( StunException.ILLEGAL_ARGUMENT, "Failed to create keep-alive STUN message for candidate: " + candidate); } } /** * Sends a specific <tt>Request</tt> to the STUN server associated with this * <tt>StunCandidateHarvest</tt>. * * @param request the <tt>Request</tt> to send to the STUN server associated with this * <tt>StunCandidateHarvest</tt> * @param firstRequest <tt>true</tt> if the specified <tt>request</tt> should be sent as the first * request in the terms of STUN; otherwise, <tt>false</tt> * @return the <tt>TransactionID</tt> of the STUN client transaction through which the specified * <tt>Request</tt> has been sent to the STUN server associated with this * <tt>StunCandidateHarvest</tt> * @param transactionID the <tt>TransactionID</tt> of <tt>request</tt> because <tt>request</tt> * only has it as a <tt>byte</tt> array and <tt>TransactionID</tt> is required for the * <tt>applicationData</tt> property value * @throws StunException if anything goes wrong while sending the specified <tt>Request</tt> to * the STUN server associated with this <tt>StunCandidateHarvest</tt> */ protected TransactionID sendRequest( Request request, boolean firstRequest, TransactionID transactionID) throws StunException { if (!firstRequest && (longTermCredentialSession != null)) longTermCredentialSession.addAttributes(request); StunStack stunStack = harvester.getStunStack(); TransportAddress stunServer = harvester.stunServer; TransportAddress hostCandidateTransportAddress = hostCandidate.getTransportAddress(); if (transactionID == null) { byte[] transactionIDAsBytes = request.getTransactionID(); transactionID = (transactionIDAsBytes == null) ? TransactionID.createNewTransactionID() : TransactionID.createTransactionID(harvester.getStunStack(), transactionIDAsBytes); } synchronized (requests) { try { transactionID = stunStack.sendRequest( request, stunServer, hostCandidateTransportAddress, this, transactionID); } catch (IllegalArgumentException iaex) { if (logger.isLoggable(Level.INFO)) { logger.log( Level.INFO, "Failed to send " + request + " through " + hostCandidateTransportAddress + " to " + stunServer, iaex); } throw new StunException(StunException.ILLEGAL_ARGUMENT, iaex.getMessage(), iaex); } catch (IOException ioex) { if (logger.isLoggable(Level.INFO)) { logger.log( Level.INFO, "Failed to send " + request + " through " + hostCandidateTransportAddress + " to " + stunServer, ioex); } throw new StunException(StunException.NETWORK_ERROR, ioex.getMessage(), ioex); } requests.put(transactionID, request); } return transactionID; } /** * Sets the interval in milliseconds at which a new STUN keep-alive message is to be sent to the * STUN server associated with the <tt>StunCandidateHarvester</tt> of this instance in order to * keep one of the <tt>Candidate</tt>s harvested by this instance alive. * * @param sendKeepAliveMessageInterval the interval in milliseconds at which a new STUN keep-alive * message is to be sent to the STUN server associated with the * <tt>StunCandidateHarvester</tt> of this instance in order to keep one of the * <tt>Candidate</tt>s harvested by this instance alive or {@link * #SEND_KEEP_ALIVE_MESSAGE_INTERVAL_NOT_SPECIFIED} if the keep-alive functionality is to not * be utilized */ protected void setSendKeepAliveMessageInterval(long sendKeepAliveMessageInterval) { if ((sendKeepAliveMessageInterval != SEND_KEEP_ALIVE_MESSAGE_INTERVAL_NOT_SPECIFIED) && (sendKeepAliveMessageInterval < 1)) throw new IllegalArgumentException("sendKeepAliveMessageInterval"); synchronized (sendKeepAliveMessageSyncRoot) { this.sendKeepAliveMessageInterval = sendKeepAliveMessageInterval; if (sendKeepAliveMessageThread == null) { if (this.sendKeepAliveMessageInterval != SEND_KEEP_ALIVE_MESSAGE_INTERVAL_NOT_SPECIFIED) createSendKeepAliveMessageThread(); } else sendKeepAliveMessageSyncRoot.notify(); } } /** * Starts the harvesting of <tt>Candidate</tt>s to be performed for {@link #hostCandidate}. * * @return <tt>true</tt> if this <tt>StunCandidateHarvest</tt> has started the harvesting of * <tt>Candidate</tt>s for {@link #hostCandidate}; otherwise, <tt>false</tt> * @throws Exception if anything goes wrong while starting the harvesting of <tt>Candidate</tt>s * to be performed for {@link #hostCandidate} */ boolean startResolvingCandidate() throws Exception { Request requestToStartResolvingCandidate; if (!completedResolvingCandidate && ((requestToStartResolvingCandidate = createRequestToStartResolvingCandidate()) != null)) { // Short-Term Credential Mechanism addShortTermCredentialAttributes(requestToStartResolvingCandidate); sendRequest(requestToStartResolvingCandidate, true, null); return true; } else return false; } /** Close the harvest. */ public void close() { // stop keep alive thread setSendKeepAliveMessageInterval(SEND_KEEP_ALIVE_MESSAGE_INTERVAL_NOT_SPECIFIED); } /** * Sends STUN keep-alive <tt>Message</tt>s to the STUN server associated with the * <tt>StunCandidateHarvester</tt> of this instance. * * @author Lyubomir Marinov */ private static class SendKeepAliveMessageThread extends Thread { /** * The <tt>StunCandidateHarvest</tt> which has initialized this instance. The * <tt>StunCandidateHarvest</tt> is referenced by a <tt>WeakReference</tt> in an attempt to * reduce the risk that the <tt>Thread</tt> may live regardless of the fact that the specified * <tt>StunCandidateHarvest<tt> may no longer be reachable. */ private final WeakReference<StunCandidateHarvest> harvest; /** * Initializes a new <tt>SendKeepAliveMessageThread</tt> instance with a specific * <tt>StunCandidateHarvest</tt>. * * @param harvest the <tt>StunCandidateHarvest</tt> to initialize the new instance with */ public SendKeepAliveMessageThread(StunCandidateHarvest harvest) { this.harvest = new WeakReference<>(harvest); } @Override public void run() { try { do { StunCandidateHarvest harvest = this.harvest.get(); if ((harvest == null) || !harvest.runInSendKeepAliveMessageThread()) { break; } } while (true); } finally { StunCandidateHarvest harvest = this.harvest.get(); if (harvest != null) harvest.exitSendKeepAliveMessageThread(); } } } }