/** * 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; }
/** * 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; }
/** * 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); } }
/** * 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(); } } } }