Socks5BytestreamRequest.java

  1. /**
  2.  *
  3.  * Copyright the original author or authors
  4.  *
  5.  * Licensed under the Apache License, Version 2.0 (the "License");
  6.  * you may not use this file except in compliance with the License.
  7.  * You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.jivesoftware.smackx.bytestreams.socks5;

  18. import java.io.IOException;
  19. import java.net.Socket;
  20. import java.util.Collection;
  21. import java.util.concurrent.TimeoutException;

  22. import org.jivesoftware.smack.SmackException;
  23. import org.jivesoftware.smack.SmackException.NotConnectedException;
  24. import org.jivesoftware.smack.XMPPException;
  25. import org.jivesoftware.smack.XMPPException.XMPPErrorException;
  26. import org.jivesoftware.smack.packet.IQ;
  27. import org.jivesoftware.smack.packet.XMPPError;
  28. import org.jivesoftware.smackx.bytestreams.BytestreamRequest;
  29. import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
  30. import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;
  31. import org.jxmpp.jid.Jid;
  32. import org.jxmpp.util.cache.Cache;
  33. import org.jxmpp.util.cache.ExpirationCache;

  34. /**
  35.  * Socks5BytestreamRequest class handles incoming SOCKS5 Bytestream requests.
  36.  *
  37.  * @author Henning Staib
  38.  */
  39. public class Socks5BytestreamRequest implements BytestreamRequest {

  40.     /* lifetime of an Item in the blacklist */
  41.     private static final long BLACKLIST_LIFETIME = 60 * 1000 * 120;

  42.     /* size of the blacklist */
  43.     private static final int BLACKLIST_MAX_SIZE = 100;

  44.     /* blacklist of addresses of SOCKS5 proxies */
  45.     private static final Cache<String, Integer> ADDRESS_BLACKLIST = new ExpirationCache<String, Integer>(
  46.                     BLACKLIST_MAX_SIZE, BLACKLIST_LIFETIME);

  47.     /*
  48.      * The number of connection failures it takes for a particular SOCKS5 proxy to be blacklisted.
  49.      * When a proxy is blacklisted no more connection attempts will be made to it for a period of 2
  50.      * hours.
  51.      */
  52.     private static int CONNECTION_FAILURE_THRESHOLD = 2;

  53.     /* the bytestream initialization request */
  54.     private Bytestream bytestreamRequest;

  55.     /* SOCKS5 Bytestream manager containing the XMPP connection and helper methods */
  56.     private Socks5BytestreamManager manager;

  57.     /* timeout to connect to all SOCKS5 proxies */
  58.     private int totalConnectTimeout = 10000;

  59.     /* minimum timeout to connect to one SOCKS5 proxy */
  60.     private int minimumConnectTimeout = 2000;

  61.     /**
  62.      * Returns the number of connection failures it takes for a particular SOCKS5 proxy to be
  63.      * blacklisted. When a proxy is blacklisted no more connection attempts will be made to it for a
  64.      * period of 2 hours. Default is 2.
  65.      *
  66.      * @return the number of connection failures it takes for a particular SOCKS5 proxy to be
  67.      *         blacklisted
  68.      */
  69.     public static int getConnectFailureThreshold() {
  70.         return CONNECTION_FAILURE_THRESHOLD;
  71.     }

  72.     /**
  73.      * Sets the number of connection failures it takes for a particular SOCKS5 proxy to be
  74.      * blacklisted. When a proxy is blacklisted no more connection attempts will be made to it for a
  75.      * period of 2 hours. Default is 2.
  76.      * <p>
  77.      * Setting the connection failure threshold to zero disables the blacklisting.
  78.      *
  79.      * @param connectFailureThreshold the number of connection failures it takes for a particular
  80.      *        SOCKS5 proxy to be blacklisted
  81.      */
  82.     public static void setConnectFailureThreshold(int connectFailureThreshold) {
  83.         CONNECTION_FAILURE_THRESHOLD = connectFailureThreshold;
  84.     }

  85.     /**
  86.      * Creates a new Socks5BytestreamRequest.
  87.      *
  88.      * @param manager the SOCKS5 Bytestream manager
  89.      * @param bytestreamRequest the SOCKS5 Bytestream initialization packet
  90.      */
  91.     protected Socks5BytestreamRequest(Socks5BytestreamManager manager, Bytestream bytestreamRequest) {
  92.         this.manager = manager;
  93.         this.bytestreamRequest = bytestreamRequest;
  94.     }

  95.     /**
  96.      * Returns the maximum timeout to connect to SOCKS5 proxies. Default is 10000ms.
  97.      * <p>
  98.      * When accepting a SOCKS5 Bytestream request Smack tries to connect to all SOCKS5 proxies given
  99.      * by the initiator until a connection is established. This timeout divided by the number of
  100.      * SOCKS5 proxies determines the timeout for every connection attempt.
  101.      * <p>
  102.      * You can set the minimum timeout for establishing a connection to one SOCKS5 proxy by invoking
  103.      * {@link #setMinimumConnectTimeout(int)}.
  104.      *
  105.      * @return the maximum timeout to connect to SOCKS5 proxies
  106.      */
  107.     public int getTotalConnectTimeout() {
  108.         if (this.totalConnectTimeout <= 0) {
  109.             return 10000;
  110.         }
  111.         return this.totalConnectTimeout;
  112.     }

  113.     /**
  114.      * Sets the maximum timeout to connect to SOCKS5 proxies. Default is 10000ms.
  115.      * <p>
  116.      * When accepting a SOCKS5 Bytestream request Smack tries to connect to all SOCKS5 proxies given
  117.      * by the initiator until a connection is established. This timeout divided by the number of
  118.      * SOCKS5 proxies determines the timeout for every connection attempt.
  119.      * <p>
  120.      * You can set the minimum timeout for establishing a connection to one SOCKS5 proxy by invoking
  121.      * {@link #setMinimumConnectTimeout(int)}.
  122.      *
  123.      * @param totalConnectTimeout the maximum timeout to connect to SOCKS5 proxies
  124.      */
  125.     public void setTotalConnectTimeout(int totalConnectTimeout) {
  126.         this.totalConnectTimeout = totalConnectTimeout;
  127.     }

  128.     /**
  129.      * Returns the timeout to connect to one SOCKS5 proxy while accepting the SOCKS5 Bytestream
  130.      * request. Default is 2000ms.
  131.      *
  132.      * @return the timeout to connect to one SOCKS5 proxy
  133.      */
  134.     public int getMinimumConnectTimeout() {
  135.         if (this.minimumConnectTimeout <= 0) {
  136.             return 2000;
  137.         }
  138.         return this.minimumConnectTimeout;
  139.     }

  140.     /**
  141.      * Sets the timeout to connect to one SOCKS5 proxy while accepting the SOCKS5 Bytestream
  142.      * request. Default is 2000ms.
  143.      *
  144.      * @param minimumConnectTimeout the timeout to connect to one SOCKS5 proxy
  145.      */
  146.     public void setMinimumConnectTimeout(int minimumConnectTimeout) {
  147.         this.minimumConnectTimeout = minimumConnectTimeout;
  148.     }

  149.     /**
  150.      * Returns the sender of the SOCKS5 Bytestream initialization request.
  151.      *
  152.      * @return the sender of the SOCKS5 Bytestream initialization request.
  153.      */
  154.     public Jid getFrom() {
  155.         return this.bytestreamRequest.getFrom();
  156.     }

  157.     /**
  158.      * Returns the session ID of the SOCKS5 Bytestream initialization request.
  159.      *
  160.      * @return the session ID of the SOCKS5 Bytestream initialization request.
  161.      */
  162.     public String getSessionID() {
  163.         return this.bytestreamRequest.getSessionID();
  164.     }

  165.     /**
  166.      * Accepts the SOCKS5 Bytestream initialization request and returns the socket to send/receive
  167.      * data.
  168.      * <p>
  169.      * Before accepting the SOCKS5 Bytestream request you can set timeouts by invoking
  170.      * {@link #setTotalConnectTimeout(int)} and {@link #setMinimumConnectTimeout(int)}.
  171.      *
  172.      * @return the socket to send/receive data
  173.      * @throws InterruptedException if the current thread was interrupted while waiting
  174.      * @throws XMPPErrorException
  175.      * @throws SmackException
  176.      */
  177.     public Socks5BytestreamSession accept() throws InterruptedException, XMPPErrorException, SmackException {
  178.         Collection<StreamHost> streamHosts = this.bytestreamRequest.getStreamHosts();

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

  183.         StreamHost selectedHost = null;
  184.         Socket socket = null;

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

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

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

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

  200.             // establish socket
  201.             try {

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

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

  206.                 // set selected host
  207.                 selectedHost = streamHost;
  208.                 break;

  209.             }
  210.             catch (TimeoutException e) {
  211.                 incrementConnectionFailures(address);
  212.             }
  213.             catch (IOException e) {
  214.                 incrementConnectionFailures(address);
  215.             }
  216.             catch (XMPPException e) {
  217.                 incrementConnectionFailures(address);
  218.             }

  219.         }

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

  224.         // send used-host confirmation
  225.         Bytestream response = createUsedHostResponse(selectedHost);
  226.         this.manager.getConnection().sendStanza(response);

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

  229.     }

  230.     /**
  231.      * Rejects the SOCKS5 Bytestream request by sending a reject error to the initiator.
  232.      * @throws NotConnectedException
  233.      * @throws InterruptedException
  234.      */
  235.     public void reject() throws NotConnectedException, InterruptedException {
  236.         this.manager.replyRejectPacket(this.bytestreamRequest);
  237.     }

  238.     /**
  239.      * Cancels the SOCKS5 Bytestream request by sending an error to the initiator and building a
  240.      * XMPP exception.
  241.      * @throws XMPPErrorException
  242.      * @throws NotConnectedException
  243.      * @throws InterruptedException
  244.      */
  245.     private void cancelRequest() throws XMPPErrorException, NotConnectedException, InterruptedException {
  246.         String errorMessage = "Could not establish socket with any provided host";
  247.         XMPPError error = XMPPError.from(XMPPError.Condition.item_not_found, errorMessage);
  248.         IQ errorIQ = IQ.createErrorResponse(this.bytestreamRequest, error);
  249.         this.manager.getConnection().sendStanza(errorIQ);
  250.         throw new XMPPErrorException(errorMessage, error);
  251.     }

  252.     /**
  253.      * Returns the response to the SOCKS5 Bytestream request containing the SOCKS5 proxy used.
  254.      *
  255.      * @param selectedHost the used SOCKS5 proxy
  256.      * @return the response to the SOCKS5 Bytestream request
  257.      */
  258.     private Bytestream createUsedHostResponse(StreamHost selectedHost) {
  259.         Bytestream response = new Bytestream(this.bytestreamRequest.getSessionID());
  260.         response.setTo(this.bytestreamRequest.getFrom());
  261.         response.setType(IQ.Type.result);
  262.         response.setStanzaId(this.bytestreamRequest.getStanzaId());
  263.         response.setUsedHost(selectedHost.getJID());
  264.         return response;
  265.     }

  266.     /**
  267.      * Increments the connection failure counter by one for the given address.
  268.      *
  269.      * @param address the address the connection failure counter should be increased
  270.      */
  271.     private void incrementConnectionFailures(String address) {
  272.         Integer count = ADDRESS_BLACKLIST.get(address);
  273.         ADDRESS_BLACKLIST.put(address, count == null ? 1 : count + 1);
  274.     }

  275.     /**
  276.      * Returns how often the connection to the given address failed.
  277.      *
  278.      * @param address the address
  279.      * @return number of connection failures
  280.      */
  281.     private int getConnectionFailures(String address) {
  282.         Integer count = ADDRESS_BLACKLIST.get(address);
  283.         return count != null ? count : 0;
  284.     }

  285. }