/**
   * If the target is not connected to the local SOCKS5 proxy an exception should be thrown.
   *
   * @throws Exception should not happen
   */
  @Test
  public void shouldFailIfTargetIsNotConnectedToLocalSocks5Proxy() throws Exception {

    // start a local SOCKS5 proxy
    Socks5Proxy.setLocalSocks5ProxyPort(proxyPort);
    Socks5Proxy socks5Proxy = Socks5Proxy.getSocks5Proxy();
    socks5Proxy.start();

    // build stream host information for local SOCKS5 proxy
    StreamHost streamHost =
        new StreamHost(connection.getUser(), socks5Proxy.getLocalAddresses().get(0));
    streamHost.setPort(socks5Proxy.getPort());

    // create digest to get the socket opened by target
    String digest = Socks5Utils.createDigest(sessionID, initiatorJID, targetJID);

    Socks5ClientForInitiator socks5Client =
        new Socks5ClientForInitiator(streamHost, digest, connection, sessionID, targetJID);

    try {
      socks5Client.getSocket(10000);

      fail("exception should be thrown");
    } catch (SmackException e) {
      assertTrue(e.getMessage().contains("target is not connected to SOCKS5 proxy"));
      protocol.verifyAll(); // assert no XMPP messages were sent
    }

    socks5Proxy.stop();
  }
  /**
   * Target and initiator should successfully connect to a "remote" SOCKS5 proxy and the initiator
   * activates the bytestream.
   *
   * @throws Exception should not happen
   */
  @Test
  public void shouldSuccessfullyEstablishConnectionAndActivateSocks5Proxy() throws Exception {

    // build activation confirmation response
    IQ activationResponse =
        new IQ() {

          @Override
          public String getChildElementXML() {
            return null;
          }
        };
    activationResponse.setFrom(proxyJID);
    activationResponse.setTo(initiatorJID);
    activationResponse.setType(IQ.Type.RESULT);

    protocol.addResponse(
        activationResponse,
        Verification.correspondingSenderReceiver,
        Verification.requestTypeSET,
        new Verification<Bytestream, IQ>() {

          public void verify(Bytestream request, IQ response) {
            // verify that the correct stream should be activated
            assertNotNull(request.getToActivate());
            assertEquals(targetJID, request.getToActivate().getTarget());
          }
        });

    // start a local SOCKS5 proxy
    Socks5TestProxy socks5Proxy = Socks5TestProxy.getProxy(proxyPort);
    socks5Proxy.start();

    StreamHost streamHost = new StreamHost(proxyJID, socks5Proxy.getAddress());
    streamHost.setPort(socks5Proxy.getPort());

    // create digest to get the socket opened by target
    String digest = Socks5Utils.createDigest(sessionID, initiatorJID, targetJID);

    Socks5ClientForInitiator socks5Client =
        new Socks5ClientForInitiator(streamHost, digest, connection, sessionID, targetJID);

    Socket initiatorSocket = socks5Client.getSocket(10000);
    InputStream in = initiatorSocket.getInputStream();

    Socket targetSocket = socks5Proxy.getSocket(digest);
    OutputStream out = targetSocket.getOutputStream();

    // verify test data
    for (int i = 0; i < 10; i++) {
      out.write(i);
      assertEquals(i, in.read());
    }

    protocol.verifyAll();

    initiatorSocket.close();
    targetSocket.close();
    socks5Proxy.stop();
  }
  /**
   * Initiator and target should successfully connect to the local SOCKS5 proxy.
   *
   * @throws Exception should not happen
   */
  @Test
  public void shouldSuccessfullyConnectThroughLocalSocks5Proxy() throws Exception {

    // start a local SOCKS5 proxy
    Socks5Proxy.setLocalSocks5ProxyPort(proxyPort);
    Socks5Proxy socks5Proxy = Socks5Proxy.getSocks5Proxy();
    socks5Proxy.start();

    // test data
    final byte[] data = new byte[] {1, 2, 3};

    // create digest
    final String digest = Socks5Utils.createDigest(sessionID, initiatorJID, targetJID);

    // allow connection of target with this digest
    socks5Proxy.addTransfer(digest);

    // build stream host information
    final StreamHost streamHost =
        new StreamHost(connection.getUser(), socks5Proxy.getLocalAddresses().get(0));
    streamHost.setPort(socks5Proxy.getPort());

    // target connects to local SOCKS5 proxy
    Thread targetThread =
        new Thread() {

          @Override
          public void run() {
            try {
              Socks5Client targetClient = new Socks5Client(streamHost, digest);
              Socket socket = targetClient.getSocket(10000);
              socket.getOutputStream().write(data);
            } catch (Exception e) {
              fail(e.getMessage());
            }
          }
        };
    targetThread.start();

    Thread.sleep(200);

    // initiator connects
    Socks5ClientForInitiator socks5Client =
        new Socks5ClientForInitiator(streamHost, digest, connection, sessionID, targetJID);

    Socket socket = socks5Client.getSocket(10000);

    // verify test data
    InputStream in = socket.getInputStream();
    for (int i = 0; i < data.length; i++) {
      assertEquals(data[i], in.read());
    }

    targetThread.join();

    protocol.verifyAll(); // assert no XMPP messages were sent

    socks5Proxy.removeTransfer(digest);
    socks5Proxy.stop();
  }
 /**
  * Returns the response to the SOCKS5 Bytestream request containing the SOCKS5 proxy used.
  *
  * @param selectedHost the used SOCKS5 proxy
  * @return the response to the SOCKS5 Bytestream request
  */
 private Bytestream createUsedHostResponse(StreamHost selectedHost) {
   Bytestream response = new Bytestream(this.bytestreamRequest.getSessionID());
   response.setTo(this.bytestreamRequest.getFrom());
   response.setType(IQ.Type.RESULT);
   response.setPacketID(this.bytestreamRequest.getPacketID());
   response.setUsedHost(selectedHost.getJID());
   return response;
 }
  /**
   * If the initiator can connect to a SOCKS5 proxy but activating the stream fails an exception
   * should be thrown.
   *
   * @throws Exception should not happen
   */
  @Test
  public void shouldFailIfActivateSocks5ProxyFails() throws Exception {

    // build error response as reply to the stream activation
    XMPPError xmppError = new XMPPError(XMPPError.Condition.internal_server_error);
    IQ error =
        new IQ() {

          public String getChildElementXML() {
            return null;
          }
        };
    error.setType(Type.ERROR);
    error.setFrom(proxyJID);
    error.setTo(initiatorJID);
    error.setError(xmppError);

    protocol.addResponse(
        error, Verification.correspondingSenderReceiver, Verification.requestTypeSET);

    // start a local SOCKS5 proxy
    Socks5TestProxy socks5Proxy = Socks5TestProxy.getProxy(proxyPort);
    socks5Proxy.start();

    StreamHost streamHost = new StreamHost(proxyJID, socks5Proxy.getAddress());
    streamHost.setPort(socks5Proxy.getPort());

    // create digest to get the socket opened by target
    String digest = Socks5Utils.createDigest(sessionID, initiatorJID, targetJID);

    Socks5ClientForInitiator socks5Client =
        new Socks5ClientForInitiator(streamHost, digest, connection, sessionID, targetJID);

    try {

      socks5Client.getSocket(10000);

      fail("exception should be thrown");
    } catch (XMPPErrorException e) {
      assertTrue(XMPPError.Condition.internal_server_error.equals(e.getXMPPError().getCondition()));
      protocol.verifyAll();
    }

    socks5Proxy.stop();
  }
  /**
   * Accepts the SOCKS5 Bytestream initialization request and returns the socket to send/receive
   * data.
   *
   * <p>Before accepting the SOCKS5 Bytestream request you can set timeouts by invoking {@link
   * #setTotalConnectTimeout(int)} and {@link #setMinimumConnectTimeout(int)}.
   *
   * @return the socket to send/receive data
   * @throws XMPPException if connection to all SOCKS5 proxies failed or if stream is invalid.
   * @throws InterruptedException if the current thread was interrupted while waiting
   */
  public Socks5BytestreamSession accept() throws XMPPException, InterruptedException {
    Collection<StreamHost> streamHosts = this.bytestreamRequest.getStreamHosts();

    // throw exceptions if request contains no stream hosts
    if (streamHosts.size() == 0) {
      cancelRequest();
    }

    StreamHost selectedHost = null;
    Socket socket = null;

    String digest =
        Socks5Utils.createDigest(
            this.bytestreamRequest.getSessionID(),
            this.bytestreamRequest.getFrom(),
            this.manager.getConnection().getUser());

    /*
     * determine timeout for each connection attempt; each SOCKS5 proxy has the same amount of
     * time so that the first does not consume the whole timeout
     */
    int timeout =
        Math.max(getTotalConnectTimeout() / streamHosts.size(), getMinimumConnectTimeout());

    for (StreamHost streamHost : streamHosts) {
      String address = streamHost.getAddress() + ":" + streamHost.getPort();

      // check to see if this address has been blacklisted
      int failures = getConnectionFailures(address);
      if (CONNECTION_FAILURE_THRESHOLD > 0 && failures >= CONNECTION_FAILURE_THRESHOLD) {
        continue;
      }

      // establish socket
      try {

        // build SOCKS5 client
        final Socks5Client socks5Client = new Socks5Client(streamHost, digest);

        // connect to SOCKS5 proxy with a timeout
        socket = socks5Client.getSocket(timeout);

        // set selected host
        selectedHost = streamHost;
        break;

      } catch (TimeoutException e) {
        incrementConnectionFailures(address);
      } catch (IOException e) {
        incrementConnectionFailures(address);
      } catch (XMPPException e) {
        incrementConnectionFailures(address);
      }
    }

    // throw exception if connecting to all SOCKS5 proxies failed
    if (selectedHost == null || socket == null) {
      cancelRequest();
    }

    // send used-host confirmation
    Bytestream response = createUsedHostResponse(selectedHost);
    this.manager.getConnection().sendPacket(response);

    return new Socks5BytestreamSession(
        socket, selectedHost.getJID().equals(this.bytestreamRequest.getFrom()));
  }