/** * Handles a specific <tt>IOException</tt> which was thrown during the execution of {@link * #runInConnectThread(DTLSProtocol, TlsPeer, DatagramTransport)} while trying to establish a DTLS * connection * * @param ioe the <tt>IOException</tt> to handle * @param msg the human-readable message to log about the specified <tt>ioe</tt> * @param i the number of tries remaining after the current one * @return <tt>true</tt> if the specified <tt>ioe</tt> was successfully handled; <tt>false</tt>, * otherwise */ private boolean handleRunInConnectThreadException(IOException ioe, String msg, int i) { // SrtpControl.start(MediaType) starts its associated TransformEngine. // We will use that mediaType to signal the normal stop then as well // i.e. we will ignore exception after the procedure to stop this // PacketTransformer has begun. if (mediaType == null) return false; if (ioe instanceof TlsFatalAlert) { TlsFatalAlert tfa = (TlsFatalAlert) ioe; short alertDescription = tfa.getAlertDescription(); if (alertDescription == AlertDescription.unexpected_message) { msg += " Received fatal unexpected message."; if (i == 0 || !Thread.currentThread().equals(connectThread) || connector == null || mediaType == null) { msg += " Giving up after " + (CONNECT_TRIES - i) + " retries."; } else { msg += " Will retry."; logger.error(msg, ioe); return true; } } else { msg += " Received fatal alert " + alertDescription + "."; } } logger.error(msg, ioe); 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); } }
/** * 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); } } } } } } }
/** * 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; }
/** Stops this <tt>PacketTransformer</tt>. */ private synchronized void stop() { if (connectThread != null) connectThread = null; try { // The dtlsTransport and _srtpTransformer SHOULD be closed, of // course. The datagramTransport MUST be closed. if (dtlsTransport != null) { try { dtlsTransport.close(); } catch (IOException ioe) { logger.error("Failed to (properly) close " + dtlsTransport.getClass(), ioe); } dtlsTransport = null; } if (_srtpTransformer != null) { _srtpTransformer.close(); _srtpTransformer = null; } } finally { try { closeDatagramTransport(); } finally { notifyAll(); } } }
/** * Closes {@link #datagramTransport} if it is non-<tt>null</tt> and logs and swallows any * <tt>IOException</tt>. */ private void closeDatagramTransport() { if (datagramTransport != null) { try { datagramTransport.close(); } catch (IOException ioe) { // DatagramTransportImpl has no reason to fail because it is // merely an adapter of #connector and this PacketTransformer to // the terms of the Bouncy Castle Crypto API. logger.error("Failed to (properly) close " + datagramTransport.getClass(), ioe); } datagramTransport = null; } }
/** * 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; }
/** * 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 the data contained in a specific byte array as application data through the DTLS * connection of this <tt>DtlsPacketTransformer</tt>. * * @param buf the byte array containing data to send. * @param off the offset in <tt>buf</tt> where the data begins. * @param len the length of data to send. */ public void sendApplicationData(byte[] buf, int off, int len) { DTLSTransport dtlsTransport = this.dtlsTransport; Throwable throwable = null; if (dtlsTransport != null) { try { dtlsTransport.send(buf, off, len); } catch (IOException ioe) { throwable = ioe; } } else { throwable = new NullPointerException("dtlsTransport"); } if (throwable != null) { // SrtpControl.start(MediaType) starts its associated // TransformEngine. We will use that mediaType to signal the normal // stop then as well i.e. we will ignore exception after the // procedure to stop this PacketTransformer has begun. if (mediaType != null && !tlsPeerHasRaisedCloseNotifyWarning) { logger.error("Failed to send application data over DTLS transport: ", throwable); } } }
/** * 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; }
/** * 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; } }
/** Starts this <tt>PacketTransformer</tt>. */ private synchronized void start() { if (this.datagramTransport != null) { if (this.connectThread == null && dtlsTransport == null) { logger.warn( getClass().getName() + " has been started but has failed to establish" + " the DTLS connection!"); } return; } if (rtcpmux && Component.RTCP == componentID) { // In the case of rtcp-mux, the RTCP transformer does not create // a DTLS session. The SRTP context (_srtpTransformer) will be // initialized on demand using initializeSRTCPTransformerFromRtp(). return; } AbstractRTPConnector connector = this.connector; if (connector == null) throw new NullPointerException("connector"); DtlsControl.Setup setup = this.setup; SecureRandom secureRandom = DtlsControlImpl.createSecureRandom(); final DTLSProtocol dtlsProtocolObj; final TlsPeer tlsPeer; if (DtlsControl.Setup.ACTIVE.equals(setup)) { dtlsProtocolObj = new DTLSClientProtocol(secureRandom); tlsPeer = new TlsClientImpl(this); } else { dtlsProtocolObj = new DTLSServerProtocol(secureRandom); tlsPeer = new TlsServerImpl(this); } tlsPeerHasRaisedCloseNotifyWarning = false; final DatagramTransportImpl datagramTransport = new DatagramTransportImpl(componentID); datagramTransport.setConnector(connector); Thread connectThread = new Thread() { @Override public void run() { try { runInConnectThread(dtlsProtocolObj, tlsPeer, datagramTransport); } finally { if (Thread.currentThread().equals(DtlsPacketTransformer.this.connectThread)) { DtlsPacketTransformer.this.connectThread = null; } } } }; connectThread.setDaemon(true); connectThread.setName(DtlsPacketTransformer.class.getName() + ".connectThread"); this.connectThread = connectThread; this.datagramTransport = datagramTransport; boolean started = false; try { connectThread.start(); started = true; } finally { if (!started) { if (connectThread.equals(this.connectThread)) this.connectThread = null; if (datagramTransport.equals(this.datagramTransport)) this.datagramTransport = null; } } notifyAll(); }
/** * 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(); } } } }
/** * Implements {@link PacketTransformer} for DTLS-SRTP. It's capable of working in pure DTLS mode if * appropriate flag was set in <tt>DtlsControlImpl</tt>. * * @author Lyubomir Marinov */ public class DtlsPacketTransformer extends SinglePacketTransformer { private static final long CONNECT_RETRY_INTERVAL = 500; /** * The maximum number of times that {@link #runInConnectThread(DTLSProtocol, TlsPeer, * DatagramTransport)} is to retry the invocations of {@link DTLSClientProtocol#connect(TlsClient, * DatagramTransport)} and {@link DTLSServerProtocol#accept(TlsServer, DatagramTransport)} in * anticipation of a successful connection. */ private static final int CONNECT_TRIES = 3; /** * The indicator which determines whether unencrypted packets sent or received through * <tt>DtlsPacketTransformer</tt> are to be dropped. The default value is <tt>false</tt>. * * @see #DROP_UNENCRYPTED_PKTS_PNAME */ private static final boolean DROP_UNENCRYPTED_PKTS; /** * The name of the <tt>ConfigurationService</tt> and/or <tt>System</tt> property which indicates * whether unencrypted packets sent or received through <tt>DtlsPacketTransformer</tt> are to be * dropped. The default value is <tt>false</tt>. */ private static final String DROP_UNENCRYPTED_PKTS_PNAME = DtlsPacketTransformer.class.getName() + ".dropUnencryptedPkts"; /** The length of the header of a DTLS record. */ static final int DTLS_RECORD_HEADER_LENGTH = 13; /** * The number of milliseconds a <tt>DtlsPacketTransform</tt> is to wait on its {@link * #dtlsTransport} in order to receive a packet. */ private static final int DTLS_TRANSPORT_RECEIVE_WAITMILLIS = -1; /** * The <tt>Logger</tt> used by the <tt>DtlsPacketTransformer</tt> class and its instances to print * debug information. */ private static final Logger logger = Logger.getLogger(DtlsPacketTransformer.class); static { ConfigurationService cfg = LibJitsi.getConfigurationService(); boolean dropUnencryptedPkts = false; if (cfg == null) { String s = System.getProperty(DROP_UNENCRYPTED_PKTS_PNAME); if (s != null) dropUnencryptedPkts = Boolean.parseBoolean(s); } else { dropUnencryptedPkts = cfg.getBoolean(DROP_UNENCRYPTED_PKTS_PNAME, dropUnencryptedPkts); } DROP_UNENCRYPTED_PKTS = dropUnencryptedPkts; } /** * Determines whether a specific array of <tt>byte</tt>s appears to contain a DTLS record. * * @param buf the array of <tt>byte</tt>s to be analyzed * @param off the offset within <tt>buf</tt> at which the analysis is to start * @param len the number of bytes within <tt>buf</tt> starting at <tt>off</tt> to be analyzed * @return <tt>true</tt> if the specified <tt>buf</tt> appears to contain a DTLS record */ public static boolean isDtlsRecord(byte[] buf, int off, int len) { boolean b = false; if (len >= DTLS_RECORD_HEADER_LENGTH) { short type = TlsUtils.readUint8(buf, off); switch (type) { case ContentType.alert: case ContentType.application_data: case ContentType.change_cipher_spec: case ContentType.handshake: int major = buf[off + 1] & 0xff; int minor = buf[off + 2] & 0xff; ProtocolVersion version = null; if ((major == ProtocolVersion.DTLSv10.getMajorVersion()) && (minor == ProtocolVersion.DTLSv10.getMinorVersion())) { version = ProtocolVersion.DTLSv10; } if ((version == null) && (major == ProtocolVersion.DTLSv12.getMajorVersion()) && (minor == ProtocolVersion.DTLSv12.getMinorVersion())) { version = ProtocolVersion.DTLSv12; } if (version != null) { int length = TlsUtils.readUint16(buf, off + 11); if (DTLS_RECORD_HEADER_LENGTH + length <= len) b = true; } break; default: // Unless a new ContentType has been defined by the Bouncy // Castle Crypto APIs, the specified buf does not represent a // DTLS record. break; } } return b; } /** The ID of the component which this instance works for/is associated with. */ private final int componentID; /** The <tt>RTPConnector</tt> which uses this <tt>PacketTransformer</tt>. */ private AbstractRTPConnector connector; /** The background <tt>Thread</tt> which initializes {@link #dtlsTransport}. */ private Thread connectThread; /** * The <tt>DatagramTransport</tt> implementation which adapts {@link #connector} and this * <tt>PacketTransformer</tt> to the terms of the Bouncy Castle Crypto APIs. */ private DatagramTransportImpl datagramTransport; /** * The <tt>DTLSTransport</tt> through which the actual packet transformations are being performed * by this instance. */ private DTLSTransport dtlsTransport; /** The <tt>MediaType</tt> of the stream which this instance works for/is associated with. */ private MediaType mediaType; /** * Whether rtcp-mux is in use. * * <p>If enabled, and this is the transformer for RTCP, it will not establish a DTLS session on * its own, but rather wait for the RTP transformer to do so, and reuse it to initialize the SRTP * transformer. */ private boolean rtcpmux = false; /** * The value of the <tt>setup</tt> SDP attribute defined by RFC 4145 "TCP-Based Media * Transport in the Session Description Protocol (SDP)" which determines whether this * instance acts as a DTLS client or a DTLS server. */ private DtlsControl.Setup setup; /** The {@code SRTPTransformer} (to be) used by this instance. */ private SinglePacketTransformer _srtpTransformer; /** * The indicator which determines whether the <tt>TlsPeer</tt> employed by this * <tt>PacketTransformer</tt> has raised an <tt>AlertDescription.close_notify</tt> * <tt>AlertLevel.warning</tt> i.e. the remote DTLS peer has closed the write side of the * connection. */ private boolean tlsPeerHasRaisedCloseNotifyWarning; /** The <tt>TransformEngine</tt> which has initialized this instance. */ private final DtlsTransformEngine transformEngine; /** * Initializes a new <tt>DtlsPacketTransformer</tt> instance. * * @param transformEngine the <tt>TransformEngine</tt> which is initializing the new instance * @param componentID the ID of the component for which the new instance is to work */ public DtlsPacketTransformer(DtlsTransformEngine transformEngine, int componentID) { this.transformEngine = transformEngine; this.componentID = componentID; } /** {@inheritDoc} */ @Override public synchronized void close() { // SrtpControl.start(MediaType) starts its associated TransformEngine. // We will use that mediaType to signal the normal stop then as well // i.e. we will call setMediaType(null) first. setMediaType(null); setConnector(null); } /** * Closes {@link #datagramTransport} if it is non-<tt>null</tt> and logs and swallows any * <tt>IOException</tt>. */ private void closeDatagramTransport() { if (datagramTransport != null) { try { datagramTransport.close(); } catch (IOException ioe) { // DatagramTransportImpl has no reason to fail because it is // merely an adapter of #connector and this PacketTransformer to // the terms of the Bouncy Castle Crypto API. logger.error("Failed to (properly) close " + datagramTransport.getClass(), ioe); } datagramTransport = null; } } /** * Determines whether {@link #runInConnectThread(DTLSProtocol, TlsPeer, DatagramTransport)} is to * try to establish a DTLS connection. * * @param i the number of tries remaining after the current one * @param datagramTransport * @return <tt>true</tt> to try to establish a DTLS connection; otherwise, <tt>false</tt> */ private boolean enterRunInConnectThreadLoop(int i, DatagramTransport datagramTransport) { if (i < 0 || i > CONNECT_TRIES) { return false; } else { Thread currentThread = Thread.currentThread(); synchronized (this) { if (i > 0 && i < CONNECT_TRIES - 1) { boolean interrupted = false; try { wait(CONNECT_RETRY_INTERVAL); } catch (InterruptedException ie) { interrupted = true; } if (interrupted) currentThread.interrupt(); } return currentThread.equals(this.connectThread) && datagramTransport.equals(this.datagramTransport); } } } /** * Gets the <tt>DtlsControl</tt> implementation associated with this instance. * * @return the <tt>DtlsControl</tt> implementation associated with this instance */ DtlsControlImpl getDtlsControl() { return getTransformEngine().getDtlsControl(); } /** * Gets the <tt>TransformEngine</tt> which has initialized this instance. * * @return the <tt>TransformEngine</tt> which has initialized this instance */ DtlsTransformEngine getTransformEngine() { return transformEngine; } /** * Handles a specific <tt>IOException</tt> which was thrown during the execution of {@link * #runInConnectThread(DTLSProtocol, TlsPeer, DatagramTransport)} while trying to establish a DTLS * connection * * @param ioe the <tt>IOException</tt> to handle * @param msg the human-readable message to log about the specified <tt>ioe</tt> * @param i the number of tries remaining after the current one * @return <tt>true</tt> if the specified <tt>ioe</tt> was successfully handled; <tt>false</tt>, * otherwise */ private boolean handleRunInConnectThreadException(IOException ioe, String msg, int i) { // SrtpControl.start(MediaType) starts its associated TransformEngine. // We will use that mediaType to signal the normal stop then as well // i.e. we will ignore exception after the procedure to stop this // PacketTransformer has begun. if (mediaType == null) return false; if (ioe instanceof TlsFatalAlert) { TlsFatalAlert tfa = (TlsFatalAlert) ioe; short alertDescription = tfa.getAlertDescription(); if (alertDescription == AlertDescription.unexpected_message) { msg += " Received fatal unexpected message."; if (i == 0 || !Thread.currentThread().equals(connectThread) || connector == null || mediaType == null) { msg += " Giving up after " + (CONNECT_TRIES - i) + " retries."; } else { msg += " Will retry."; logger.error(msg, ioe); return true; } } else { msg += " Received fatal alert " + alertDescription + "."; } } logger.error(msg, ioe); return false; } /** * Tries to initialize {@link #_srtpTransformer} by using the <tt>DtlsPacketTransformer</tt> for * RTP. * * @return the (possibly updated) value of {@link #_srtpTransformer}. */ private SinglePacketTransformer initializeSRTCPTransformerFromRtp() { DtlsPacketTransformer rtpTransformer = (DtlsPacketTransformer) getTransformEngine().getRTPTransformer(); // Prevent recursion (that is pretty much impossible to ever happen). if (rtpTransformer != this) { PacketTransformer srtpTransformer = rtpTransformer.waitInitializeAndGetSRTPTransformer(); if (srtpTransformer != null && srtpTransformer instanceof SRTPTransformer) { synchronized (this) { if (_srtpTransformer == null) { _srtpTransformer = new SRTCPTransformer((SRTPTransformer) srtpTransformer); // For the sake of completeness, we notify whenever we // assign to _srtpTransformer. notifyAll(); } } } } return _srtpTransformer; } /** * Initializes a new <tt>SRTPTransformer</tt> instance with a specific (negotiated) * <tt>SRTPProtectionProfile</tt> and the keying material specified by a specific * <tt>TlsContext</tt>. * * @param srtpProtectionProfile the (negotiated) <tt>SRTPProtectionProfile</tt> to initialize the * new instance with * @param tlsContext the <tt>TlsContext</tt> which represents the keying material * @return a new <tt>SRTPTransformer</tt> instance initialized with <tt>srtpProtectionProfile</tt> * and <tt>tlsContext</tt> */ private SinglePacketTransformer initializeSRTPTransformer( int srtpProtectionProfile, TlsContext tlsContext) { boolean rtcp; switch (componentID) { case Component.RTCP: rtcp = true; break; case Component.RTP: rtcp = false; break; default: throw new IllegalStateException("componentID"); } int cipher_key_length; int cipher_salt_length; int cipher; int auth_function; int auth_key_length; int RTCP_auth_tag_length, RTP_auth_tag_length; switch (srtpProtectionProfile) { case SRTPProtectionProfile.SRTP_AES128_CM_HMAC_SHA1_32: cipher_key_length = 128 / 8; cipher_salt_length = 112 / 8; cipher = SRTPPolicy.AESCM_ENCRYPTION; auth_function = SRTPPolicy.HMACSHA1_AUTHENTICATION; auth_key_length = 160 / 8; RTCP_auth_tag_length = 80 / 8; RTP_auth_tag_length = 32 / 8; break; case SRTPProtectionProfile.SRTP_AES128_CM_HMAC_SHA1_80: cipher_key_length = 128 / 8; cipher_salt_length = 112 / 8; cipher = SRTPPolicy.AESCM_ENCRYPTION; auth_function = SRTPPolicy.HMACSHA1_AUTHENTICATION; auth_key_length = 160 / 8; RTCP_auth_tag_length = RTP_auth_tag_length = 80 / 8; break; case SRTPProtectionProfile.SRTP_NULL_HMAC_SHA1_32: cipher_key_length = 0; cipher_salt_length = 0; cipher = SRTPPolicy.NULL_ENCRYPTION; auth_function = SRTPPolicy.HMACSHA1_AUTHENTICATION; auth_key_length = 160 / 8; RTCP_auth_tag_length = 80 / 8; RTP_auth_tag_length = 32 / 8; break; case SRTPProtectionProfile.SRTP_NULL_HMAC_SHA1_80: cipher_key_length = 0; cipher_salt_length = 0; cipher = SRTPPolicy.NULL_ENCRYPTION; auth_function = SRTPPolicy.HMACSHA1_AUTHENTICATION; auth_key_length = 160 / 8; RTCP_auth_tag_length = RTP_auth_tag_length = 80 / 8; break; default: throw new IllegalArgumentException("srtpProtectionProfile"); } byte[] keyingMaterial = tlsContext.exportKeyingMaterial( ExporterLabel.dtls_srtp, null, 2 * (cipher_key_length + cipher_salt_length)); byte[] client_write_SRTP_master_key = new byte[cipher_key_length]; byte[] server_write_SRTP_master_key = new byte[cipher_key_length]; byte[] client_write_SRTP_master_salt = new byte[cipher_salt_length]; byte[] server_write_SRTP_master_salt = new byte[cipher_salt_length]; byte[][] keyingMaterialValues = { client_write_SRTP_master_key, server_write_SRTP_master_key, client_write_SRTP_master_salt, server_write_SRTP_master_salt }; for (int i = 0, keyingMaterialOffset = 0; i < keyingMaterialValues.length; i++) { byte[] keyingMaterialValue = keyingMaterialValues[i]; System.arraycopy( keyingMaterial, keyingMaterialOffset, keyingMaterialValue, 0, keyingMaterialValue.length); keyingMaterialOffset += keyingMaterialValue.length; } SRTPPolicy srtcpPolicy = new SRTPPolicy( cipher, cipher_key_length, auth_function, auth_key_length, RTCP_auth_tag_length, cipher_salt_length); SRTPPolicy srtpPolicy = new SRTPPolicy( cipher, cipher_key_length, auth_function, auth_key_length, RTP_auth_tag_length, cipher_salt_length); SRTPContextFactory clientSRTPContextFactory = new SRTPContextFactory( /* sender */ tlsContext instanceof TlsClientContext, client_write_SRTP_master_key, client_write_SRTP_master_salt, srtpPolicy, srtcpPolicy); SRTPContextFactory serverSRTPContextFactory = new SRTPContextFactory( /* sender */ tlsContext instanceof TlsServerContext, server_write_SRTP_master_key, server_write_SRTP_master_salt, srtpPolicy, srtcpPolicy); SRTPContextFactory forwardSRTPContextFactory; SRTPContextFactory reverseSRTPContextFactory; if (tlsContext instanceof TlsClientContext) { forwardSRTPContextFactory = clientSRTPContextFactory; reverseSRTPContextFactory = serverSRTPContextFactory; } else if (tlsContext instanceof TlsServerContext) { forwardSRTPContextFactory = serverSRTPContextFactory; reverseSRTPContextFactory = clientSRTPContextFactory; } else { throw new IllegalArgumentException("tlsContext"); } SinglePacketTransformer srtpTransformer; if (rtcp) { srtpTransformer = new SRTCPTransformer(forwardSRTPContextFactory, reverseSRTPContextFactory); } else { srtpTransformer = new SRTPTransformer(forwardSRTPContextFactory, reverseSRTPContextFactory); } return srtpTransformer; } /** * Notifies this instance that the DTLS record layer associated with a specific <tt>TlsPeer</tt> * has raised an alert. * * @param tlsPeer the <tt>TlsPeer</tt> whose associated DTLS record layer has raised an alert * @param alertLevel {@link AlertLevel} * @param alertDescription {@link AlertDescription} * @param message a human-readable message explaining what caused the alert. May be <tt>null</tt>. * @param cause the exception that caused the alert to be raised. May be <tt>null</tt>. */ void notifyAlertRaised( TlsPeer tlsPeer, short alertLevel, short alertDescription, String message, Exception cause) { if (AlertLevel.warning == alertLevel && AlertDescription.close_notify == alertDescription) { tlsPeerHasRaisedCloseNotifyWarning = true; } } /** {@inheritDoc} */ @Override public RawPacket reverseTransform(RawPacket pkt) { byte[] buf = pkt.getBuffer(); int off = pkt.getOffset(); int len = pkt.getLength(); if (isDtlsRecord(buf, off, len)) { if (rtcpmux && Component.RTCP == componentID) { // This should never happen. logger.warn( "Dropping a DTLS record, because it was received on the" + " RTCP channel while rtcpmux is in use."); return null; } boolean receive; synchronized (this) { if (datagramTransport == null) { receive = false; } else { datagramTransport.queueReceive(buf, off, len); receive = true; } } if (receive) { DTLSTransport dtlsTransport = this.dtlsTransport; if (dtlsTransport == null) { // The specified pkt looks like a DTLS record and it has // been consumed for the purposes of the secure channel // represented by this PacketTransformer. pkt = null; } else { try { int receiveLimit = dtlsTransport.getReceiveLimit(); int delta = receiveLimit - len; if (delta > 0) { pkt.grow(delta); buf = pkt.getBuffer(); off = pkt.getOffset(); len = pkt.getLength(); } else if (delta < 0) { pkt.shrink(-delta); buf = pkt.getBuffer(); off = pkt.getOffset(); len = pkt.getLength(); } int received = dtlsTransport.receive(buf, off, len, DTLS_TRANSPORT_RECEIVE_WAITMILLIS); if (received <= 0) { // No application data was decoded. pkt = null; } else { delta = len - received; if (delta > 0) pkt.shrink(delta); } } catch (IOException ioe) { pkt = null; // SrtpControl.start(MediaType) starts its associated // TransformEngine. We will use that mediaType to signal // the normal stop then as well i.e. we will ignore // exception after the procedure to stop this // PacketTransformer has begun. if (mediaType != null && !tlsPeerHasRaisedCloseNotifyWarning) { logger.error("Failed to decode a DTLS record!", ioe); } } } } else { // The specified pkt looks like a DTLS record but it is // unexpected in the current state of the secure channel // represented by this PacketTransformer. This PacketTransformer // has not been started (successfully) or has been closed. pkt = null; } } else if (transformEngine.isSrtpDisabled()) { // In pure DTLS mode only DTLS records pass through. pkt = null; } else { // DTLS-SRTP has not been initialized yet or has failed to // initialize. SinglePacketTransformer srtpTransformer = waitInitializeAndGetSRTPTransformer(); if (srtpTransformer != null) pkt = srtpTransformer.reverseTransform(pkt); else if (DROP_UNENCRYPTED_PKTS) pkt = null; // XXX Else, it is our explicit policy to let the received packet // pass through and rely on the SrtpListener to notify the user that // the session is not secured. } return pkt; } /** * Runs in {@link #connectThread} to initialize {@link #dtlsTransport}. * * @param dtlsProtocol * @param tlsPeer * @param datagramTransport */ private void runInConnectThread( DTLSProtocol dtlsProtocol, TlsPeer tlsPeer, DatagramTransport datagramTransport) { DTLSTransport dtlsTransport = null; final boolean srtp = !transformEngine.isSrtpDisabled(); int srtpProtectionProfile = 0; TlsContext tlsContext = null; // DTLS client if (dtlsProtocol instanceof DTLSClientProtocol) { DTLSClientProtocol dtlsClientProtocol = (DTLSClientProtocol) dtlsProtocol; TlsClientImpl tlsClient = (TlsClientImpl) tlsPeer; for (int i = CONNECT_TRIES - 1; i >= 0; i--) { if (!enterRunInConnectThreadLoop(i, datagramTransport)) break; try { dtlsTransport = dtlsClientProtocol.connect(tlsClient, datagramTransport); break; } catch (IOException ioe) { if (!handleRunInConnectThreadException( ioe, "Failed to connect this DTLS client to a DTLS" + " server!", i)) { break; } } } if (dtlsTransport != null && srtp) { srtpProtectionProfile = tlsClient.getChosenProtectionProfile(); tlsContext = tlsClient.getContext(); } } // DTLS server else if (dtlsProtocol instanceof DTLSServerProtocol) { DTLSServerProtocol dtlsServerProtocol = (DTLSServerProtocol) dtlsProtocol; TlsServerImpl tlsServer = (TlsServerImpl) tlsPeer; for (int i = CONNECT_TRIES - 1; i >= 0; i--) { if (!enterRunInConnectThreadLoop(i, datagramTransport)) break; try { dtlsTransport = dtlsServerProtocol.accept(tlsServer, datagramTransport); break; } catch (IOException ioe) { if (!handleRunInConnectThreadException( ioe, "Failed to accept a connection from a DTLS client!", i)) { break; } } } if (dtlsTransport != null && srtp) { srtpProtectionProfile = tlsServer.getChosenProtectionProfile(); tlsContext = tlsServer.getContext(); } } else { // It MUST be either a DTLS client or a DTLS server. throw new IllegalStateException("dtlsProtocol"); } SinglePacketTransformer srtpTransformer = (dtlsTransport == null || !srtp) ? null : initializeSRTPTransformer(srtpProtectionProfile, tlsContext); boolean closeSRTPTransformer; synchronized (this) { if (Thread.currentThread().equals(this.connectThread) && datagramTransport.equals(this.datagramTransport)) { this.dtlsTransport = dtlsTransport; _srtpTransformer = srtpTransformer; notifyAll(); } closeSRTPTransformer = (_srtpTransformer != srtpTransformer); } if (closeSRTPTransformer && srtpTransformer != null) srtpTransformer.close(); } /** * Sends the data contained in a specific byte array as application data through the DTLS * connection of this <tt>DtlsPacketTransformer</tt>. * * @param buf the byte array containing data to send. * @param off the offset in <tt>buf</tt> where the data begins. * @param len the length of data to send. */ public void sendApplicationData(byte[] buf, int off, int len) { DTLSTransport dtlsTransport = this.dtlsTransport; Throwable throwable = null; if (dtlsTransport != null) { try { dtlsTransport.send(buf, off, len); } catch (IOException ioe) { throwable = ioe; } } else { throwable = new NullPointerException("dtlsTransport"); } if (throwable != null) { // SrtpControl.start(MediaType) starts its associated // TransformEngine. We will use that mediaType to signal the normal // stop then as well i.e. we will ignore exception after the // procedure to stop this PacketTransformer has begun. if (mediaType != null && !tlsPeerHasRaisedCloseNotifyWarning) { logger.error("Failed to send application data over DTLS transport: ", throwable); } } } /** * Sets the <tt>RTPConnector</tt> which is to use or uses this <tt>PacketTransformer</tt>. * * @param connector the <tt>RTPConnector</tt> which is to use or uses this * <tt>PacketTransformer</tt> */ void setConnector(AbstractRTPConnector connector) { if (this.connector != connector) { this.connector = connector; DatagramTransportImpl datagramTransport = this.datagramTransport; if (datagramTransport != null) datagramTransport.setConnector(connector); } } /** * Sets the <tt>MediaType</tt> of the stream which this instance is to work for/be associated * with. * * @param mediaType the <tt>MediaType</tt> of the stream which this instance is to work for/be * associated with */ synchronized void setMediaType(MediaType mediaType) { if (this.mediaType != mediaType) { MediaType oldValue = this.mediaType; this.mediaType = mediaType; if (oldValue != null) stop(); if (this.mediaType != null) start(); } } /** * Enables/disables rtcp-mux. * * @param rtcpmux whether to enable or disable. */ void setRtcpmux(boolean rtcpmux) { this.rtcpmux = rtcpmux; } /** * Sets the DTLS protocol according to which this <tt>DtlsPacketTransformer</tt> is to act either * as a DTLS server or a DTLS client. * * @param setup the value of the <tt>setup</tt> SDP attribute to set on this instance in order to * determine whether this instance is to act as a DTLS client or a DTLS server */ void setSetup(DtlsControl.Setup setup) { if (this.setup != setup) this.setup = setup; } /** Starts this <tt>PacketTransformer</tt>. */ private synchronized void start() { if (this.datagramTransport != null) { if (this.connectThread == null && dtlsTransport == null) { logger.warn( getClass().getName() + " has been started but has failed to establish" + " the DTLS connection!"); } return; } if (rtcpmux && Component.RTCP == componentID) { // In the case of rtcp-mux, the RTCP transformer does not create // a DTLS session. The SRTP context (_srtpTransformer) will be // initialized on demand using initializeSRTCPTransformerFromRtp(). return; } AbstractRTPConnector connector = this.connector; if (connector == null) throw new NullPointerException("connector"); DtlsControl.Setup setup = this.setup; SecureRandom secureRandom = DtlsControlImpl.createSecureRandom(); final DTLSProtocol dtlsProtocolObj; final TlsPeer tlsPeer; if (DtlsControl.Setup.ACTIVE.equals(setup)) { dtlsProtocolObj = new DTLSClientProtocol(secureRandom); tlsPeer = new TlsClientImpl(this); } else { dtlsProtocolObj = new DTLSServerProtocol(secureRandom); tlsPeer = new TlsServerImpl(this); } tlsPeerHasRaisedCloseNotifyWarning = false; final DatagramTransportImpl datagramTransport = new DatagramTransportImpl(componentID); datagramTransport.setConnector(connector); Thread connectThread = new Thread() { @Override public void run() { try { runInConnectThread(dtlsProtocolObj, tlsPeer, datagramTransport); } finally { if (Thread.currentThread().equals(DtlsPacketTransformer.this.connectThread)) { DtlsPacketTransformer.this.connectThread = null; } } } }; connectThread.setDaemon(true); connectThread.setName(DtlsPacketTransformer.class.getName() + ".connectThread"); this.connectThread = connectThread; this.datagramTransport = datagramTransport; boolean started = false; try { connectThread.start(); started = true; } finally { if (!started) { if (connectThread.equals(this.connectThread)) this.connectThread = null; if (datagramTransport.equals(this.datagramTransport)) this.datagramTransport = null; } } notifyAll(); } /** Stops this <tt>PacketTransformer</tt>. */ private synchronized void stop() { if (connectThread != null) connectThread = null; try { // The dtlsTransport and _srtpTransformer SHOULD be closed, of // course. The datagramTransport MUST be closed. if (dtlsTransport != null) { try { dtlsTransport.close(); } catch (IOException ioe) { logger.error("Failed to (properly) close " + dtlsTransport.getClass(), ioe); } dtlsTransport = null; } if (_srtpTransformer != null) { _srtpTransformer.close(); _srtpTransformer = null; } } finally { try { closeDatagramTransport(); } finally { notifyAll(); } } } /** {@inheritDoc} */ @Override public RawPacket transform(RawPacket pkt) { byte[] buf = pkt.getBuffer(); int off = pkt.getOffset(); int len = pkt.getLength(); // If the specified pkt represents a DTLS record, then it should pass // through this PacketTransformer (e.g. it has been sent through // DatagramTransportImpl). if (isDtlsRecord(buf, off, len)) return pkt; // SRTP if (!transformEngine.isSrtpDisabled()) { // DTLS-SRTP has not been initialized yet or has failed to // initialize. SinglePacketTransformer srtpTransformer = waitInitializeAndGetSRTPTransformer(); if (srtpTransformer != null) pkt = srtpTransformer.transform(pkt); else if (DROP_UNENCRYPTED_PKTS) pkt = null; // XXX Else, it is our explicit policy to let the received packet // pass through and rely on the SrtpListener to notify the user that // the session is not secured. } // Pure/non-SRTP DTLS else { // The specified pkt will pass through this PacketTransformer only // if it gets transformed into a DTLS record. pkt = null; sendApplicationData(buf, off, len); } return pkt; } /** * Gets the {@code SRTPTransformer} used by this instance. If {@link #_srtpTransformer} does not * exist (yet) and the state of this instance indicates that its initialization is in progess, * then blocks until {@code _srtpTransformer} is initialized and returns it. * * @return the {@code SRTPTransformer} used by this instance */ private SinglePacketTransformer waitInitializeAndGetSRTPTransformer() { SinglePacketTransformer srtpTransformer = _srtpTransformer; if (srtpTransformer != null) return srtpTransformer; if (rtcpmux && Component.RTCP == componentID) return initializeSRTCPTransformerFromRtp(); // XXX It is our explicit policy to rely on the SrtpListener to notify // the user that the session is not secure. Unfortunately, (1) the // SrtpListener is not supported by this DTLS SrtpControl implementation // and (2) encrypted packets may arrive soon enough to be let through // while _srtpTransformer is still initializing. Consequently, we will // block and wait for _srtpTransformer to initialize. boolean interrupted = false; try { synchronized (this) { do { srtpTransformer = _srtpTransformer; if (srtpTransformer != null) break; // _srtpTransformer is initialized if (connectThread == null) { // Though _srtpTransformer is NOT initialized, there is // no point in waiting because there is no one to // initialize it. break; } try { // It does not really matter (enough) how much we wait // here because we wait in a loop. long timeout = CONNECT_TRIES * CONNECT_RETRY_INTERVAL; wait(timeout); } catch (InterruptedException ie) { interrupted = true; } } while (true); } } finally { if (interrupted) Thread.currentThread().interrupt(); } return srtpTransformer; } }
/** * 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); } }
/** * 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(); } }
/** * 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; }
/** {@inheritDoc} */ @Override public RawPacket reverseTransform(RawPacket pkt) { byte[] buf = pkt.getBuffer(); int off = pkt.getOffset(); int len = pkt.getLength(); if (isDtlsRecord(buf, off, len)) { if (rtcpmux && Component.RTCP == componentID) { // This should never happen. logger.warn( "Dropping a DTLS record, because it was received on the" + " RTCP channel while rtcpmux is in use."); return null; } boolean receive; synchronized (this) { if (datagramTransport == null) { receive = false; } else { datagramTransport.queueReceive(buf, off, len); receive = true; } } if (receive) { DTLSTransport dtlsTransport = this.dtlsTransport; if (dtlsTransport == null) { // The specified pkt looks like a DTLS record and it has // been consumed for the purposes of the secure channel // represented by this PacketTransformer. pkt = null; } else { try { int receiveLimit = dtlsTransport.getReceiveLimit(); int delta = receiveLimit - len; if (delta > 0) { pkt.grow(delta); buf = pkt.getBuffer(); off = pkt.getOffset(); len = pkt.getLength(); } else if (delta < 0) { pkt.shrink(-delta); buf = pkt.getBuffer(); off = pkt.getOffset(); len = pkt.getLength(); } int received = dtlsTransport.receive(buf, off, len, DTLS_TRANSPORT_RECEIVE_WAITMILLIS); if (received <= 0) { // No application data was decoded. pkt = null; } else { delta = len - received; if (delta > 0) pkt.shrink(delta); } } catch (IOException ioe) { pkt = null; // SrtpControl.start(MediaType) starts its associated // TransformEngine. We will use that mediaType to signal // the normal stop then as well i.e. we will ignore // exception after the procedure to stop this // PacketTransformer has begun. if (mediaType != null && !tlsPeerHasRaisedCloseNotifyWarning) { logger.error("Failed to decode a DTLS record!", ioe); } } } } else { // The specified pkt looks like a DTLS record but it is // unexpected in the current state of the secure channel // represented by this PacketTransformer. This PacketTransformer // has not been started (successfully) or has been closed. pkt = null; } } else if (transformEngine.isSrtpDisabled()) { // In pure DTLS mode only DTLS records pass through. pkt = null; } else { // DTLS-SRTP has not been initialized yet or has failed to // initialize. SinglePacketTransformer srtpTransformer = waitInitializeAndGetSRTPTransformer(); if (srtpTransformer != null) pkt = srtpTransformer.reverseTransform(pkt); else if (DROP_UNENCRYPTED_PKTS) pkt = null; // XXX Else, it is our explicit policy to let the received packet // pass through and rely on the SrtpListener to notify the user that // the session is not secured. } return pkt; }