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

org.lastbamboo.common.ice.IceStunClientCandidateProcessor Maven / Gradle / Ivy

package org.lastbamboo.common.ice;

import java.net.InetAddress;
import java.net.InetSocketAddress;

import org.lastbamboo.common.ice.candidate.IceCandidate;
import org.lastbamboo.common.ice.candidate.IceCandidatePair;
import org.lastbamboo.common.ice.candidate.IceCandidatePairState;
import org.lastbamboo.common.ice.candidate.IceCandidateType;
import org.lastbamboo.common.ice.candidate.IceCandidateVisitor;
import org.lastbamboo.common.ice.candidate.IceCandidateVisitorAdapter;
import org.lastbamboo.common.ice.candidate.IceTcpHostPassiveCandidate;
import org.lastbamboo.common.ice.candidate.IceTcpPeerReflexiveCandidate;
import org.lastbamboo.common.ice.candidate.IceTcpRelayPassiveCandidate;
import org.lastbamboo.common.ice.candidate.IceTcpServerReflexiveSoCandidate;
import org.lastbamboo.common.ice.candidate.IceUdpHostCandidate;
import org.lastbamboo.common.ice.candidate.IceUdpPeerReflexiveCandidate;
import org.lastbamboo.common.ice.candidate.IceUdpRelayCandidate;
import org.lastbamboo.common.ice.candidate.IceUdpServerReflexiveCandidate;
import org.littleshoot.stun.stack.message.BindingErrorResponse;
import org.littleshoot.stun.stack.message.BindingRequest;
import org.littleshoot.stun.stack.message.BindingSuccessResponse;
import org.littleshoot.stun.stack.message.CanceledStunMessage;
import org.littleshoot.stun.stack.message.ConnectErrorStunMessage;
import org.littleshoot.stun.stack.message.NullStunMessage;
import org.littleshoot.stun.stack.message.StunMessage;
import org.littleshoot.stun.stack.message.StunMessageVisitor;
import org.littleshoot.stun.stack.message.StunMessageVisitorAdapter;
import org.littleshoot.stun.stack.message.attributes.StunAttribute;
import org.littleshoot.stun.stack.message.attributes.ice.IceControlledAttribute;
import org.littleshoot.stun.stack.message.attributes.ice.IceControllingAttribute;
import org.littleshoot.stun.stack.message.attributes.ice.IcePriorityAttribute;
import org.littleshoot.stun.stack.message.attributes.ice.IceUseCandidateAttribute;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Class that performs connectivity checks for a single pair. This 
 * implements ICE section 7.1 from:

* * http://tools.ietf.org/html/draft-ietf-mmusic-ice-17#section-7.1 */ public class IceStunClientCandidateProcessor { private final Logger m_log = LoggerFactory.getLogger(getClass()); private final IceCandidatePair m_pair; private final IceAgent m_iceAgent; private final IceMediaStream m_mediaStream; private final ExistingSessionIceCandidatePairFactory m_existingSessionPairFactory; /** * Creates a new connectivity checker for a single UDP pair. * * @param iceAgent The top-level ICE agent. * @param iceMediaStream The media stream this check is trying to establish. * @param udpPair The candidate pair. * @param existingSessionPairFactory Pair factory to use when there's a * session that's already been created prior to construction of the pair. */ public IceStunClientCandidateProcessor(final IceAgent iceAgent, final IceMediaStream iceMediaStream, final IceCandidatePair udpPair, final ExistingSessionIceCandidatePairFactory existingSessionPairFactory) { m_iceAgent = iceAgent; m_mediaStream = iceMediaStream; m_pair = udpPair; m_existingSessionPairFactory = existingSessionPairFactory; } /** * Processes the specified local candidate. * * @param localCandidate The local candidate. */ public void processLocalCandidate(final IceCandidate localCandidate) { // See ICE section 7 "Performing Connectivity Checks". final IceCandidate remoteCandidate = this.m_pair.getRemoteCandidate(); // Now send a BindingRequest with PRIORITY, USE-CANDIDATE, // ICE-CONTROLLING etc. final long requestPriority = IcePriorityCalculator.calculatePriority( IceCandidateType.PEER_REFLEXIVE, IceTransportProtocol.UDP); final IcePriorityAttribute priorityAttribute = new IcePriorityAttribute(requestPriority); final StunAttribute controlling; // The agent uses the same tie-breaker throughout the session. final IceTieBreaker tieBreaker = m_iceAgent.getTieBreaker(); // We use a separate variable here because we need to know what // we sent in the case of error responses, and the data in the // ICE agent can change. final boolean isControlling = m_iceAgent.isControlling(); if (isControlling) { controlling = new IceControllingAttribute(tieBreaker.toByteArray()); } else { controlling = new IceControlledAttribute(tieBreaker.toByteArray()); } // TODO: Add CREDENTIALS attribute. final BindingRequest request; // This could be for either regular or aggressive nomination. final boolean includedUseCandidate; // We only include the USE-CANDIDATE attribute if it's set AND we're // the controlling agent. The controlled agent never sends // USE-CANDIDATE. if (this.m_pair.useCandidateSet() && isControlling) { m_log.info("Creating Binding Request with USE CANDIDATE"); request = new BindingRequest(priorityAttribute, controlling, new IceUseCandidateAttribute()); includedUseCandidate = true; } else { request = new BindingRequest(priorityAttribute, controlling); includedUseCandidate = false; } // TODO: Obtain RTO properly. final long rto = 40L; m_log.debug("Writing Binding Request: {}", request); final StunMessage response = this.m_pair.check(request, rto); final StunMessageVisitor visitor = new StunMessageVisitorAdapter() { @Override public IceCandidate visitBindingSuccessResponse( final BindingSuccessResponse bsr) { m_log.info("Visiting binding success response..."); if (m_pair.isNominateOnSuccess()) { /* As stated in section 7.2.1.5. Updating the Nominated Flag: "If the state of this pair is In-Progress, if its check produces a successful result, the resulting valid pair has its nominated flag set when the response arrives. This may end ICE processing for this media stream when it arrives; see Section 8." */ m_pair.nominate(); m_iceAgent.onNominatedPair(m_pair, m_mediaStream); return null; } // Now check the mapped address and see if it matches // any of the local candidates we know about. If it // does not, it's a new peer reflexive candidate. // // We use the mapped address of the response as the local // candidate address for the new pair, as specified in // 7.1.2.2.2. final InetSocketAddress mappedAddress = bsr.getMappedAddress(); final IceCandidate matchingCandidate = m_mediaStream.getLocalCandidate(mappedAddress, localCandidate.isUdp()); if (matchingCandidate != null) { // This will have the priority signaled in the original // SDP, as specified in 7.1.2.2.2. return matchingCandidate; } else { // This basically indicates the NAT bound to a new port for // the outgoing request to the new host, meaning the NAT // is using at least address-dependent mapping and possibly // address and port dependent mapping. // Note the base candidate here is the local candidate // from the pair, i.e. the candidate we're visiting. // We use the PRIORITY from the Binding Request, as // specified in section 7.1.2.2.1. and 7.1.2.2.2. final IceCandidate peerReflexiveLocal = new IceUdpPeerReflexiveCandidate(mappedAddress, localCandidate, m_iceAgent.isControlling(), requestPriority); m_mediaStream.addLocalCandidate(peerReflexiveLocal); return peerReflexiveLocal; } } @Override public IceCandidate visitBindingErrorResponse( final BindingErrorResponse bindingErrorResponse) { // This is likely a role-conflict error. We need to // handle it as specified in section 7.1.2.1. final int errorCode = bindingErrorResponse.getErrorCode(); if (errorCode != 487) { // It's an error response we don't know how to handle. m_log.warn("Unexpected error response: " + bindingErrorResponse.getAttributes()); m_pair.setState(IceCandidatePairState.FAILED); return null; } if (!isControlling) { m_iceAgent.setControlling(true); } else { m_iceAgent.setControlling(false); } // As stated in ICE 17 section 7.1.2.1. Failure Cases: // "the agent MUST enqueue the candidate pair whose check // generated the 487 into the triggered check queue. The // state of that pair is set to Waiting." // This has the effect of sending a new Binding Request to // the remote host reflecting the new role. // Note that we queue up a triggered check always here, // assuming that the role change was actually correct. m_pair.setState(IceCandidatePairState.WAITING); m_mediaStream.addTriggeredPair(m_pair); return null; } @Override public IceCandidate visitNullMessage(final NullStunMessage message) { m_log.debug("Visiting null message..."); // See section 7.1.2.1. Failure Cases // This means we never received any response to our request, // interpreted as a failure. m_pair.setState(IceCandidatePairState.FAILED); return null; } @Override public IceCandidate visitCanceledMessage( final CanceledStunMessage message) { m_log.debug("Transaction was canceled..."); // The outgoing message was canceled. This can happen when // we received a pair that generated a triggered check on // a STUN server. In this case, we don't treat it as a failure // but instead simply stop processing the pair. It will be // added to the triggered check queue and will be re-checked. return null; } @Override public IceCandidate visitConnectErrorMesssage( final ConnectErrorStunMessage message) { // See section 7.1.2.1. Failure Cases. m_log.debug("Got connect error -- setting pair state to " + "failed."); m_pair.setState(IceCandidatePairState.FAILED); return null; } }; if (response == null) { m_log.debug("No response -- could not connect?"); // See ICE 7.1.2.3. Check List and Timer State Updates -- // we need to update the check list and timer states in a // failure case as well. m_mediaStream.updateCheckListAndTimerStates(); return; } final IceCandidate newLocalCandidate = response.accept(visitor); if (newLocalCandidate == null) { m_log.debug("Check failed or was canceled -- should happen " + "quite often"); // See ICE 7.1.2.3. Check List and Timer State Updates -- // we need to update the check list and timer states in a // failure case as well. m_mediaStream.updateCheckListAndTimerStates(); return; } else { processSuccess(newLocalCandidate, remoteCandidate, includedUseCandidate, requestPriority); } } /** * Processes a successful response to a check. This is specified in * ICE section 7.1.2.2 at: * *

* http://tools.ietf.org/html/draft-ietf-mmusic-ice-18#section-7.1.2.2 *

* * Note we always know the source IP and port and destination IP and port * are correct here because we're using a MINA UDP socket that's * "connected", meaning it only accepts data from that single IP address * and port. * * @param localCandidate The calculated local candidate. This can be * peer reflexive, but it also could just be the candidate of the original * pair we just issued a check for. * @param remoteCandidate The remote candidate from the original pair. * @param useCandidate Whether the Binding Request included the * USE-CANDIDATE attribute. * @param bindingRequestPriority The priority of the Binding Request. */ private void processSuccess(final IceCandidate localCandidate, final IceCandidate remoteCandidate, final boolean useCandidate, final long bindingRequestPriority) { m_log.debug("Processing success..."); if (this.m_iceAgent.isClosed()) { m_log.info("Agent already closed...ignoring success."); return; } final InetSocketAddress remoteAddress = remoteCandidate.getSocketAddress(); final InetSocketAddress newLocalAddress = localCandidate.getSocketAddress(); final IceCandidatePair pairToAddToValidList; if (equalsOriginalPair(this.m_pair, newLocalAddress, remoteAddress)) { // Just add the original pair; m_log.debug("Using original pair..."); pairToAddToValidList = this.m_pair; } else { m_log.debug("Original pair not equal"); m_log.debug("Original pair was: {}", m_pair); m_log.debug( "New pair addresses: {}"+newLocalAddress, remoteAddress); final IceCandidatePair existingPair = m_mediaStream.getPair(newLocalAddress, remoteAddress, localCandidate.isUdp()); if (existingPair != null) { m_log.debug("Using existing pair..."); pairToAddToValidList = existingPair; } else { // The pair is a completely new pair. // We've already calculated the priority of the local candidate, // but we still need the priority of the remote candidate. // Here's the description of calculating the remote priority: // // The priority of the remote candidate is taken from the // SDP of the peer. If the candidate does not appear there, // then the check must have been a triggered check to a new // remote candidate. In that case, the priority is taken as the // value of the PRIORITY attribute in the Binding Request which // triggered the check that just completed. final long remotePriority; if (this.m_mediaStream.hasRemoteCandidateInSdp(remoteAddress, remoteCandidate.isUdp())) { // The check was not a triggered check, so use the original // candidate's priority. remotePriority = this.m_mediaStream.getRemoteCandidate( remoteAddress, remoteCandidate.isUdp()).getPriority(); } else { // The check was a triggered check, so we use the priority // from the Binding Request we just sent, as specified in // section 7.1.2.2.2. remotePriority = bindingRequestPriority; } final IceCandidate newRemoteCandidate = createRemoteCandidate(remoteCandidate, remotePriority); // This is key here. Even if the local candidate is a // new peer reflexive candidate, its base is still the // local candidate for the pair that started this check. // If it's not peer reflexive, the local candidate *is* the // local candidate of the pair that started the check. // The remote candidate is the remote candidate from the pair // in any case. // // Either way, the point is that the underlying transport // connection is the same so we need to reuse it, so we // construct a new pair with the same existing transport. m_log.debug("Creating new pair..."); pairToAddToValidList = m_existingSessionPairFactory.newUdpPair( localCandidate, newRemoteCandidate, this.m_pair.getIoSession()); m_log.debug("Created pair:\n{}", pairToAddToValidList); } } m_log.debug("Adding valid pair..."); m_mediaStream.addValidPair(pairToAddToValidList); // 7.1.2.2.3. Updating Pair States // Tell the media stream to update pair states as a result of // a valid pair. m_log.debug("Updating pair states..."); this.m_mediaStream.updatePairStates(this.m_pair); this.m_iceAgent.checkValidPairsForAllComponents(m_mediaStream); // Tell the ICE agent to consider this valid pair if it was not just // nominated. Nominated pairs have already been considered as valid // pairs -- that's how they had their nominated flag set. if (!updateNominatedFlag(pairToAddToValidList, useCandidate)) { m_log.debug("Not Nominated. Notifying agent of valid pairs..."); this.m_iceAgent.onValidPairs(m_mediaStream); } // 7.1.2.3. Check List and Timer State Updates m_log.debug("Updating check list and timer states."); m_mediaStream.updateCheckListAndTimerStates(); } /** * This implements:

* * 7.1.2.2.4. Updating the Nominated Flag

* * and part of:

* * 7.2.1.5. Updating the Nominated Flag

* * @param validPair The valid pair. * @param sentUseCandidateInRequest Whether or not the USE-CANDIDATE * attribute appeared in the original Binding Request. * @return true if the valid pair was nominated, otherwise * false. */ private boolean updateNominatedFlag(final IceCandidatePair validPair, final boolean sentUseCandidateInRequest) { if (this.m_iceAgent.isControlling() && sentUseCandidateInRequest) { m_log.debug("Nominating pair on controlling agent."); validPair.nominate(); this.m_iceAgent.onNominatedPair(validPair, this.m_mediaStream); return true; } // If the check was the result of a triggered check from a request // that included USE-CANDIDATE (a request from the controlling agent), // we nominate. else if (!this.m_iceAgent.isControlling() && m_pair.useCandidateSet()) { m_log.debug("Nominating new pair on controlled agent!!"); // We just put the pair in the successful state, so we know // that's the state it's in (it has to be in the successful // state for us to nominate it). // NOTE: We nominate the *original pair* here, not the valid pair. // That was the pair the originally had USE-CANDIDATE set. m_pair.nominate(); this.m_iceAgent.onNominatedPair(m_pair, this.m_mediaStream); return true; } m_log.debug("Not nominating pair."); return false; } /** * Checks if the new pair equals the original pair that generated * the check. * * @param pair The original pair that generated the check. * @param newLocalAddress The new local candidate. * @param newRemoteAddress The new remote candidate. * @return true if the pairs match, otherwise * false. */ private boolean equalsOriginalPair(final IceCandidatePair pair, final InetSocketAddress newLocalAddress, final InetSocketAddress newRemoteAddress) { final InetSocketAddress oldLocalAddress = pair.getLocalCandidate().getSocketAddress(); final InetSocketAddress oldRemoteAddress = pair.getRemoteCandidate().getSocketAddress(); return newLocalAddress.equals(oldLocalAddress) && newRemoteAddress.equals(oldRemoteAddress); } /** * This creates a new remote candidate with all the same characteristics of * the original remote candidate, except with a different priority. This * is a pain, but we don't want to just set the priority of an existing * candidate. * * @param remoteCandidate The remote candidate to essentially copy. * @param remotePriority The new priority. * @return The new candidate. */ private static IceCandidate createRemoteCandidate( final IceCandidate remoteCandidate, final long remotePriority) { final InetSocketAddress remoteAddress = remoteCandidate.getSocketAddress(); final String foundation = remoteCandidate.getFoundation(); final boolean controlling = remoteCandidate.isControlling(); final int componentId = remoteCandidate.getComponentId(); final InetAddress relatedAddress = remoteCandidate.getRelatedAddress(); final int relatedPort = remoteCandidate.getRelatedPort(); final IceCandidateVisitor visitor = new IceCandidateVisitorAdapter() { @Override public IceCandidate visitTcpHostPassiveCandidate( final IceTcpHostPassiveCandidate candidate) { return new IceTcpHostPassiveCandidate(remoteAddress, foundation, controlling, remotePriority, componentId); } @Override public IceCandidate visitTcpPeerReflexiveCandidate( final IceTcpPeerReflexiveCandidate candidate) { return new IceTcpPeerReflexiveCandidate(remoteAddress, foundation, componentId, controlling, remotePriority); } @Override public IceCandidate visitTcpRelayPassiveCandidate( final IceTcpRelayPassiveCandidate candidate) { return new IceTcpRelayPassiveCandidate(remoteAddress, foundation, relatedAddress, relatedPort, controlling, remotePriority, componentId); } @Override public IceCandidate visitTcpServerReflexiveSoCandidate( final IceTcpServerReflexiveSoCandidate candidate) { return new IceTcpServerReflexiveSoCandidate(remoteAddress, foundation, relatedAddress, relatedPort, controlling, remotePriority, componentId); } @Override public IceCandidate visitUdpHostCandidate( final IceUdpHostCandidate candidate) { return new IceUdpHostCandidate(remoteAddress, foundation, remotePriority, controlling, componentId); } @Override public IceCandidate visitUdpPeerReflexiveCandidate( final IceUdpPeerReflexiveCandidate candidate) { return new IceUdpPeerReflexiveCandidate(remoteAddress, foundation, componentId, controlling, remotePriority); } @Override public IceCandidate visitUdpRelayCandidate( final IceUdpRelayCandidate candidate) { return new IceUdpRelayCandidate(remoteAddress, foundation, remotePriority, controlling, componentId, relatedAddress, relatedPort); } @Override public IceCandidate visitUdpServerReflexiveCandidate( final IceUdpServerReflexiveCandidate candidate) { return new IceUdpServerReflexiveCandidate(remoteAddress, foundation, relatedAddress, relatedPort, controlling, remotePriority, componentId); } }; return remoteCandidate.accept(visitor); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy