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.HashMap;
  22. import java.util.Map;
  23. import java.util.concurrent.TimeoutException;

  24. import org.jivesoftware.smack.SmackException;
  25. import org.jivesoftware.smack.SmackException.NotConnectedException;
  26. import org.jivesoftware.smack.XMPPException;
  27. import org.jivesoftware.smack.XMPPException.XMPPErrorException;
  28. import org.jivesoftware.smack.packet.IQ;
  29. import org.jivesoftware.smack.packet.StanzaError;

  30. import org.jivesoftware.smackx.bytestreams.BytestreamRequest;
  31. import org.jivesoftware.smackx.bytestreams.socks5.Socks5Exception.CouldNotConnectToAnyProvidedSocks5Host;
  32. import org.jivesoftware.smackx.bytestreams.socks5.Socks5Exception.NoSocks5StreamHostsProvided;
  33. import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
  34. import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;

  35. import org.jxmpp.jid.Jid;
  36. import org.jxmpp.util.cache.Cache;
  37. import org.jxmpp.util.cache.ExpirationCache;

  38. /**
  39.  * Socks5BytestreamRequest class handles incoming SOCKS5 Bytestream requests.
  40.  *
  41.  * @author Henning Staib
  42.  */
  43. public class Socks5BytestreamRequest implements BytestreamRequest {

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

  46.     /* size of the blacklist */
  47.     private static final int BLACKLIST_MAX_SIZE = 100;

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

  51.     private static int DEFAULT_CONNECTION_FAILURE_THRESHOLD = 2;

  52.     /*
  53.      * The number of connection failures it takes for a particular SOCKS5 proxy to be blacklisted.
  54.      * When a proxy is blacklisted no more connection attempts will be made to it for a period of 2
  55.      * hours.
  56.      */
  57.     private int connectionFailureThreshold = DEFAULT_CONNECTION_FAILURE_THRESHOLD;

  58.     /* the bytestream initialization request */
  59.     private Bytestream bytestreamRequest;

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

  62.     /* timeout to connect to all SOCKS5 proxies */
  63.     private int totalConnectTimeout = 10000;

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

  66.     /**
  67.      * Returns the default connection failure threshold.
  68.      *
  69.      * @return the default connection failure threshold.
  70.      * @see #setConnectFailureThreshold(int)
  71.      * @since 4.4.0
  72.      */
  73.     public static int getDefaultConnectFailureThreshold() {
  74.         return DEFAULT_CONNECTION_FAILURE_THRESHOLD;
  75.     }

  76.     /**
  77.      * Sets the default connection failure threshold.
  78.      *
  79.      * @param defaultConnectFailureThreshold the default connection failure threshold.
  80.      * @see #setConnectFailureThreshold(int)
  81.      * @since 4.4.0
  82.      */
  83.     public static void setDefaultConnectFailureThreshold(int defaultConnectFailureThreshold) {
  84.         DEFAULT_CONNECTION_FAILURE_THRESHOLD = defaultConnectFailureThreshold;
  85.     }

  86.     /**
  87.      * Returns the number of connection failures it takes for a particular SOCKS5 proxy to be
  88.      * blacklisted. When a proxy is blacklisted no more connection attempts will be made to it for a
  89.      * period of 2 hours. Default is 2.
  90.      *
  91.      * @return the number of connection failures it takes for a particular SOCKS5 proxy to be
  92.      *         blacklisted
  93.      */
  94.     public int getConnectFailureThreshold() {
  95.         return connectionFailureThreshold;
  96.     }

  97.     /**
  98.      * Sets the number of connection failures it takes for a particular SOCKS5 proxy to be
  99.      * blacklisted. When a proxy is blacklisted no more connection attempts will be made to it for a
  100.      * period of 2 hours. Default is 2.
  101.      * <p>
  102.      * Setting the connection failure threshold to zero disables the blacklisting.
  103.      *
  104.      * @param connectFailureThreshold the number of connection failures it takes for a particular
  105.      *        SOCKS5 proxy to be blacklisted
  106.      */
  107.     public void setConnectFailureThreshold(int connectFailureThreshold) {
  108.         connectionFailureThreshold = connectFailureThreshold;
  109.     }

  110.     /**
  111.      * Creates a new Socks5BytestreamRequest.
  112.      *
  113.      * @param manager the SOCKS5 Bytestream manager
  114.      * @param bytestreamRequest the SOCKS5 Bytestream initialization packet
  115.      */
  116.     protected Socks5BytestreamRequest(Socks5BytestreamManager manager, Bytestream bytestreamRequest) {
  117.         this.manager = manager;
  118.         this.bytestreamRequest = bytestreamRequest;
  119.     }

  120.     /**
  121.      * Returns the maximum timeout to connect to SOCKS5 proxies. Default is 10000ms.
  122.      * <p>
  123.      * When accepting a SOCKS5 Bytestream request Smack tries to connect to all SOCKS5 proxies given
  124.      * by the initiator until a connection is established. This timeout divided by the number of
  125.      * SOCKS5 proxies determines the timeout for every connection attempt.
  126.      * <p>
  127.      * You can set the minimum timeout for establishing a connection to one SOCKS5 proxy by invoking
  128.      * {@link #setMinimumConnectTimeout(int)}.
  129.      *
  130.      * @return the maximum timeout to connect to SOCKS5 proxies
  131.      */
  132.     public int getTotalConnectTimeout() {
  133.         if (this.totalConnectTimeout <= 0) {
  134.             return 10000;
  135.         }
  136.         return this.totalConnectTimeout;
  137.     }

  138.     /**
  139.      * Sets the maximum timeout to connect to SOCKS5 proxies. Default is 10000ms.
  140.      * <p>
  141.      * When accepting a SOCKS5 Bytestream request Smack tries to connect to all SOCKS5 proxies given
  142.      * by the initiator until a connection is established. This timeout divided by the number of
  143.      * SOCKS5 proxies determines the timeout for every connection attempt.
  144.      * <p>
  145.      * You can set the minimum timeout for establishing a connection to one SOCKS5 proxy by invoking
  146.      * {@link #setMinimumConnectTimeout(int)}.
  147.      *
  148.      * @param totalConnectTimeout the maximum timeout to connect to SOCKS5 proxies
  149.      */
  150.     public void setTotalConnectTimeout(int totalConnectTimeout) {
  151.         this.totalConnectTimeout = totalConnectTimeout;
  152.     }

  153.     /**
  154.      * Returns the timeout to connect to one SOCKS5 proxy while accepting the SOCKS5 Bytestream
  155.      * request. Default is 2000ms.
  156.      *
  157.      * @return the timeout to connect to one SOCKS5 proxy
  158.      */
  159.     public int getMinimumConnectTimeout() {
  160.         if (this.minimumConnectTimeout <= 0) {
  161.             return 2000;
  162.         }
  163.         return this.minimumConnectTimeout;
  164.     }

  165.     /**
  166.      * Sets the timeout to connect to one SOCKS5 proxy while accepting the SOCKS5 Bytestream
  167.      * request. Default is 2000ms.
  168.      *
  169.      * @param minimumConnectTimeout the timeout to connect to one SOCKS5 proxy
  170.      */
  171.     public void setMinimumConnectTimeout(int minimumConnectTimeout) {
  172.         this.minimumConnectTimeout = minimumConnectTimeout;
  173.     }

  174.     /**
  175.      * Returns the sender of the SOCKS5 Bytestream initialization request.
  176.      *
  177.      * @return the sender of the SOCKS5 Bytestream initialization request.
  178.      */
  179.     @Override
  180.     public Jid getFrom() {
  181.         return this.bytestreamRequest.getFrom();
  182.     }

  183.     /**
  184.      * Returns the session ID of the SOCKS5 Bytestream initialization request.
  185.      *
  186.      * @return the session ID of the SOCKS5 Bytestream initialization request.
  187.      */
  188.     @Override
  189.     public String getSessionID() {
  190.         return this.bytestreamRequest.getSessionID();
  191.     }

  192.     /**
  193.      * Accepts the SOCKS5 Bytestream initialization request and returns the socket to send/receive
  194.      * data.
  195.      * <p>
  196.      * Before accepting the SOCKS5 Bytestream request you can set timeouts by invoking
  197.      * {@link #setTotalConnectTimeout(int)} and {@link #setMinimumConnectTimeout(int)}.
  198.      *
  199.      * @return the socket to send/receive data
  200.      * @throws InterruptedException if the current thread was interrupted while waiting
  201.      * @throws XMPPErrorException if there was an XMPP error returned.
  202.      * @throws NotConnectedException if the XMPP connection is not connected.
  203.      * @throws CouldNotConnectToAnyProvidedSocks5Host if no connection to any provided stream host could be established
  204.      * @throws NoSocks5StreamHostsProvided if no stream host was provided.
  205.      */
  206.     @Override
  207.     public Socks5BytestreamSession accept() throws InterruptedException, XMPPErrorException,
  208.                     CouldNotConnectToAnyProvidedSocks5Host, NotConnectedException, NoSocks5StreamHostsProvided {
  209.         Collection<StreamHost> streamHosts = this.bytestreamRequest.getStreamHosts();

  210.         Map<StreamHost, Exception> streamHostsExceptions = new HashMap<>();
  211.         // throw exceptions if request contains no stream hosts
  212.         if (streamHosts.size() == 0) {
  213.             cancelRequest(streamHostsExceptions);
  214.         }

  215.         StreamHost selectedHost = null;
  216.         Socket socket = null;

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

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

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

  227.             // check to see if this address has been blacklisted
  228.             int failures = getConnectionFailures(address);
  229.             if (connectionFailureThreshold > 0 && failures >= connectionFailureThreshold) {
  230.                 continue;
  231.             }

  232.             // establish socket
  233.             try {

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

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

  238.                 // set selected host
  239.                 selectedHost = streamHost;
  240.                 break;

  241.             }
  242.             catch (TimeoutException | IOException | SmackException | XMPPException e) {
  243.                 streamHostsExceptions.put(streamHost, e);
  244.                 incrementConnectionFailures(address);
  245.             }

  246.         }

  247.         // throw exception if connecting to all SOCKS5 proxies failed
  248.         if (selectedHost == null || socket == null) {
  249.             cancelRequest(streamHostsExceptions);
  250.         }

  251.         // send used-host confirmation
  252.         Bytestream response = createUsedHostResponse(selectedHost);
  253.         this.manager.getConnection().sendStanza(response);

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

  256.     }

  257.     /**
  258.      * Rejects the SOCKS5 Bytestream request by sending a reject error to the initiator.
  259.      * @throws NotConnectedException if the XMPP connection is not connected.
  260.      * @throws InterruptedException if the calling thread was interrupted.
  261.      */
  262.     @Override
  263.     public void reject() throws NotConnectedException, InterruptedException {
  264.         this.manager.replyRejectPacket(this.bytestreamRequest);
  265.     }

  266.     /**
  267.      * Cancels the SOCKS5 Bytestream request by sending an error to the initiator and building a
  268.      * XMPP exception.
  269.      *
  270.      * @param streamHostsExceptions the stream hosts and their exceptions.
  271.      * @throws NotConnectedException if the XMPP connection is not connected.
  272.      * @throws InterruptedException if the calling thread was interrupted.
  273.      * @throws CouldNotConnectToAnyProvidedSocks5Host as expected result.
  274.      * @throws NoSocks5StreamHostsProvided if no stream host was provided.
  275.      */
  276.     private void cancelRequest(Map<StreamHost, Exception> streamHostsExceptions)
  277.                     throws NotConnectedException, InterruptedException, CouldNotConnectToAnyProvidedSocks5Host, NoSocks5StreamHostsProvided {
  278.         final Socks5Exception.NoSocks5StreamHostsProvided noHostsProvidedException;
  279.         final Socks5Exception.CouldNotConnectToAnyProvidedSocks5Host couldNotConnectException;
  280.         final String errorMessage;

  281.         if (streamHostsExceptions.isEmpty()) {
  282.             noHostsProvidedException = new Socks5Exception.NoSocks5StreamHostsProvided();
  283.             couldNotConnectException = null;
  284.             errorMessage = noHostsProvidedException.getMessage();
  285.         } else {
  286.             noHostsProvidedException = null;
  287.             couldNotConnectException = Socks5Exception.CouldNotConnectToAnyProvidedSocks5Host.construct(streamHostsExceptions);
  288.             errorMessage = couldNotConnectException.getMessage();
  289.         }

  290.         StanzaError error = StanzaError.from(StanzaError.Condition.item_not_found, errorMessage).build();
  291.         IQ errorIQ = IQ.createErrorResponse(this.bytestreamRequest, error);
  292.         this.manager.getConnection().sendStanza(errorIQ);

  293.         if (noHostsProvidedException != null) {
  294.             throw noHostsProvidedException;
  295.         } else {
  296.             throw couldNotConnectException;
  297.         }
  298.     }

  299.     /**
  300.      * Returns the response to the SOCKS5 Bytestream request containing the SOCKS5 proxy used.
  301.      *
  302.      * @param selectedHost the used SOCKS5 proxy
  303.      * @return the response to the SOCKS5 Bytestream request
  304.      */
  305.     private Bytestream createUsedHostResponse(StreamHost selectedHost) {
  306.         Bytestream response = new Bytestream(this.bytestreamRequest.getSessionID());
  307.         response.setTo(this.bytestreamRequest.getFrom());
  308.         response.setType(IQ.Type.result);
  309.         response.setStanzaId(this.bytestreamRequest.getStanzaId());
  310.         response.setUsedHost(selectedHost.getJID());
  311.         return response;
  312.     }

  313.     /**
  314.      * Increments the connection failure counter by one for the given address.
  315.      *
  316.      * @param address the address the connection failure counter should be increased
  317.      */
  318.     private static void incrementConnectionFailures(String address) {
  319.         Integer count = ADDRESS_BLACKLIST.lookup(address);
  320.         ADDRESS_BLACKLIST.put(address, count == null ? 1 : count + 1);
  321.     }

  322.     /**
  323.      * Returns how often the connection to the given address failed.
  324.      *
  325.      * @param address the address
  326.      * @return number of connection failures
  327.      */
  328.     private static int getConnectionFailures(String address) {
  329.         Integer count = ADDRESS_BLACKLIST.lookup(address);
  330.         return count != null ? count : 0;
  331.     }

  332. }