예제 #1
0
  /**
   * 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);
    }
  }
예제 #2
0
  /**
   * 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);
              }
            }
          }
        }
      }
    }
  }
예제 #3
0
  /**
   * 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;
  }
예제 #4
0
  /**
   * 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;
  }
예제 #5
0
  /**
   * 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;
  }
예제 #6
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;
  }
예제 #7
0
/**
 * 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();
  }
}
예제 #8
0
  /**
   * 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;
  }
예제 #9
0
/**
 * 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;
  }
}
예제 #10
0
  /** The listening thread's run method. */
  @Override
  public void run() {
    DatagramPacket packet = null;

    while (this.running) {
      try {
        IceSocketWrapper localSock;

        synchronized (sockLock) {
          if (!running) return;

          localSock = this.sock;
        }

        /*
         * Make sure localSock's receiveBufferSize is taken into
         * account including after it gets changed.
         */
        int receiveBufferSize = 1500;
        /*
        if(localSock.getTCPSocket() != null)
        {
            receiveBufferSize = localSock.getTCPSocket().
                getReceiveBufferSize();
        }
        else if(localSock.getUDPSocket() != null)
        {
            receiveBufferSize = localSock.getUDPSocket().
                getReceiveBufferSize();
        }
        */

        if (packet == null) {
          packet = new DatagramPacket(new byte[receiveBufferSize], receiveBufferSize);
        } else {
          byte[] packetData = packet.getData();

          if ((packetData == null) || (packetData.length < receiveBufferSize)) {
            packet.setData(new byte[receiveBufferSize], 0, receiveBufferSize);
          } else {
            /*
             * XXX Tell the packet it is large enough because the
             * socket will not look at the length of the data array
             * property and will just respect the length property.
             */
            packet.setLength(receiveBufferSize);
          }
        }

        localSock.receive(packet);

        // get lost if we are no longer running.
        if (!running) return;

        logger.finest("received datagram");

        RawMessage rawMessage =
            new RawMessage(
                packet.getData(),
                packet.getLength(),
                new TransportAddress(
                    packet.getAddress(), packet.getPort(), listenAddress.getTransport()),
                listenAddress);

        messageQueue.add(rawMessage);
      } catch (SocketException ex) {
        if (running) {
          logger.log(
              Level.WARNING, "Connector died: " + listenAddress + " -> " + remoteAddress, ex);

          stop();
          // Something wrong has happened
          errorHandler.handleFatalError(
              this, "A socket exception was thrown" + " while trying to receive a message.", ex);
        } else {
          // The exception was most probably caused by calling
          // this.stop().
        }
      } catch (ClosedChannelException cce) {
        logger.log(Level.WARNING, "A net access point has gone useless:", cce);

        stop();
        errorHandler.handleFatalError(
            this, "ClosedChannelException occurred while listening" + " for messages!", cce);
      } catch (IOException ex) {
        logger.log(Level.WARNING, "A net access point has gone useless:", ex);

        errorHandler.handleError(ex.getMessage(), ex);
        // do not stop the thread;
      } catch (Throwable ex) {
        logger.log(Level.WARNING, "A net access point has gone useless:", ex);

        stop();
        errorHandler.handleFatalError(
            this, "Unknown error occurred while listening for messages!", ex);
      }
    }
  }
예제 #11
0
/**
 * The Network Access Point is the most outward part of the stack. It is constructed around a
 * datagram socket and takes care of forwarding incoming messages to the MessageProcessor as well as
 * sending datagrams to the STUN server specified by the original NetAccessPointDescriptor.
 *
 * @author Emil Ivov
 */
class Connector implements Runnable {
  /** Our class logger. */
  private static final Logger logger = Logger.getLogger(Connector.class.getName());

  /** The message queue is where incoming messages are added. */
  private final MessageQueue messageQueue;

  /** The socket object that used by this access point to access the network. */
  private IceSocketWrapper sock;

  /** The object that we use to lock socket operations (since the socket itself is often null) */
  private final Object sockLock = new Object();

  /** A flag that is set to false to exit the message processor. */
  private boolean running;

  /** The instance to be notified if errors occur in the network listening thread. */
  private final ErrorHandler errorHandler;

  /** The address that we are listening to. */
  private final TransportAddress listenAddress;

  /**
   * The remote address of the socket of this <tt>Connector</tt> if it is a TCP socket, or
   * <tt>null</tt> if it is UDP.
   */
  private final TransportAddress remoteAddress;

  /**
   * Creates a network access point.
   *
   * @param socket the socket that this access point is supposed to use for communication.
   * @param messageQueue the FIFO list where incoming messages should be queued
   * @param errorHandler the instance to notify when errors occur.
   */
  protected Connector(
      IceSocketWrapper socket, MessageQueue messageQueue, ErrorHandler errorHandler) {
    this.sock = socket;
    this.messageQueue = messageQueue;
    this.errorHandler = errorHandler;

    Transport transport = socket.getUDPSocket() != null ? Transport.UDP : Transport.TCP;

    listenAddress =
        new TransportAddress(socket.getLocalAddress(), socket.getLocalPort(), transport);
    if (transport == Transport.UDP) {
      remoteAddress = null;
    } else {
      Socket tcpSocket = socket.getTCPSocket();

      remoteAddress =
          new TransportAddress(tcpSocket.getInetAddress(), tcpSocket.getPort(), transport);
    }
  }

  /** Start the network listening thread. */
  void start() {
    this.running = true;

    Thread thread = new Thread(this, "IceConnector@" + hashCode());

    thread.setDaemon(true);
    thread.start();
  }

  /**
   * Returns the <tt>DatagramSocket</tt> that contains the port and address associated with this
   * access point.
   *
   * @return the <tt>DatagramSocket</tt> associated with this AP.
   */
  protected IceSocketWrapper getSocket() {
    return sock;
  }

  /** The listening thread's run method. */
  @Override
  public void run() {
    DatagramPacket packet = null;

    while (this.running) {
      try {
        IceSocketWrapper localSock;

        synchronized (sockLock) {
          if (!running) return;

          localSock = this.sock;
        }

        /*
         * Make sure localSock's receiveBufferSize is taken into
         * account including after it gets changed.
         */
        int receiveBufferSize = 1500;
        /*
        if(localSock.getTCPSocket() != null)
        {
            receiveBufferSize = localSock.getTCPSocket().
                getReceiveBufferSize();
        }
        else if(localSock.getUDPSocket() != null)
        {
            receiveBufferSize = localSock.getUDPSocket().
                getReceiveBufferSize();
        }
        */

        if (packet == null) {
          packet = new DatagramPacket(new byte[receiveBufferSize], receiveBufferSize);
        } else {
          byte[] packetData = packet.getData();

          if ((packetData == null) || (packetData.length < receiveBufferSize)) {
            packet.setData(new byte[receiveBufferSize], 0, receiveBufferSize);
          } else {
            /*
             * XXX Tell the packet it is large enough because the
             * socket will not look at the length of the data array
             * property and will just respect the length property.
             */
            packet.setLength(receiveBufferSize);
          }
        }

        localSock.receive(packet);

        // get lost if we are no longer running.
        if (!running) return;

        logger.finest("received datagram");

        RawMessage rawMessage =
            new RawMessage(
                packet.getData(),
                packet.getLength(),
                new TransportAddress(
                    packet.getAddress(), packet.getPort(), listenAddress.getTransport()),
                listenAddress);

        messageQueue.add(rawMessage);
      } catch (SocketException ex) {
        if (running) {
          logger.log(
              Level.WARNING, "Connector died: " + listenAddress + " -> " + remoteAddress, ex);

          stop();
          // Something wrong has happened
          errorHandler.handleFatalError(
              this, "A socket exception was thrown" + " while trying to receive a message.", ex);
        } else {
          // The exception was most probably caused by calling
          // this.stop().
        }
      } catch (ClosedChannelException cce) {
        logger.log(Level.WARNING, "A net access point has gone useless:", cce);

        stop();
        errorHandler.handleFatalError(
            this, "ClosedChannelException occurred while listening" + " for messages!", cce);
      } catch (IOException ex) {
        logger.log(Level.WARNING, "A net access point has gone useless:", ex);

        errorHandler.handleError(ex.getMessage(), ex);
        // do not stop the thread;
      } catch (Throwable ex) {
        logger.log(Level.WARNING, "A net access point has gone useless:", ex);

        stop();
        errorHandler.handleFatalError(
            this, "Unknown error occurred while listening for messages!", ex);
      }
    }
  }

  /** Makes the access point stop listening on its socket. */
  protected void stop() {
    synchronized (sockLock) {
      this.running = false;
      if (this.sock != null) {
        this.sock.close();
        this.sock = null;
      }
    }
  }

  /**
   * Sends message through this access point's socket.
   *
   * @param message the bytes to send.
   * @param address message destination.
   * @throws IOException if an exception occurs while sending the message.
   */
  void sendMessage(byte[] message, TransportAddress address) throws IOException {
    DatagramPacket datagramPacket = new DatagramPacket(message, 0, message.length, address);

    sock.send(datagramPacket);
  }

  /**
   * Returns a String representation of the object.
   *
   * @return a String representation of the object.
   */
  @Override
  public String toString() {
    return "ice4j.Connector@" + listenAddress + " status: " + (running ? "not" : "") + " running";
  }

  /**
   * Returns the <tt>TransportAddress</tt> that this access point is bound on.
   *
   * @return the <tt>TransportAddress</tt> associated with this AP.
   */
  TransportAddress getListenAddress() {
    return listenAddress;
  }

  /**
   * Returns the remote <tt>TransportAddress</tt> in case of TCP, or <tt>null</tt> in case of UDP.
   *
   * @return the remote <tt>TransportAddress</tt> in case of TCP, or <tt>null</tt> in case of UDP.
   */
  TransportAddress getRemoteAddress() {
    return remoteAddress;
  }
}
예제 #12
0
  /**
   * 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);
    }
  }
예제 #13
0
/**
 * 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();
      }
    }
  }
}