Home | History | Annotate | Download | only in socks5
      1 /**
      2  * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
      3  * you may not use this file except in compliance with the License.
      4  * You may obtain a copy of the License at
      5  *
      6  *     http://www.apache.org/licenses/LICENSE-2.0
      7  *
      8  * Unless required by applicable law or agreed to in writing, software
      9  * distributed under the License is distributed on an "AS IS" BASIS,
     10  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     11  * See the License for the specific language governing permissions and
     12  * limitations under the License.
     13  */
     14 package org.jivesoftware.smackx.bytestreams.socks5;
     15 
     16 import java.io.IOException;
     17 import java.net.Socket;
     18 import java.util.Collection;
     19 import java.util.concurrent.TimeoutException;
     20 
     21 import org.jivesoftware.smack.XMPPException;
     22 import org.jivesoftware.smack.packet.IQ;
     23 import org.jivesoftware.smack.packet.XMPPError;
     24 import org.jivesoftware.smack.util.Cache;
     25 import org.jivesoftware.smackx.bytestreams.BytestreamRequest;
     26 import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
     27 import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;
     28 
     29 /**
     30  * Socks5BytestreamRequest class handles incoming SOCKS5 Bytestream requests.
     31  *
     32  * @author Henning Staib
     33  */
     34 public class Socks5BytestreamRequest implements BytestreamRequest {
     35 
     36     /* lifetime of an Item in the blacklist */
     37     private static final long BLACKLIST_LIFETIME = 60 * 1000 * 120;
     38 
     39     /* size of the blacklist */
     40     private static final int BLACKLIST_MAX_SIZE = 100;
     41 
     42     /* blacklist of addresses of SOCKS5 proxies */
     43     private static final Cache<String, Integer> ADDRESS_BLACKLIST = new Cache<String, Integer>(
     44                     BLACKLIST_MAX_SIZE, BLACKLIST_LIFETIME);
     45 
     46     /*
     47      * The number of connection failures it takes for a particular SOCKS5 proxy to be blacklisted.
     48      * When a proxy is blacklisted no more connection attempts will be made to it for a period of 2
     49      * hours.
     50      */
     51     private static int CONNECTION_FAILURE_THRESHOLD = 2;
     52 
     53     /* the bytestream initialization request */
     54     private Bytestream bytestreamRequest;
     55 
     56     /* SOCKS5 Bytestream manager containing the XMPP connection and helper methods */
     57     private Socks5BytestreamManager manager;
     58 
     59     /* timeout to connect to all SOCKS5 proxies */
     60     private int totalConnectTimeout = 10000;
     61 
     62     /* minimum timeout to connect to one SOCKS5 proxy */
     63     private int minimumConnectTimeout = 2000;
     64 
     65     /**
     66      * Returns the number of connection failures it takes for a particular SOCKS5 proxy to be
     67      * blacklisted. When a proxy is blacklisted no more connection attempts will be made to it for a
     68      * period of 2 hours. Default is 2.
     69      *
     70      * @return the number of connection failures it takes for a particular SOCKS5 proxy to be
     71      *         blacklisted
     72      */
     73     public static int getConnectFailureThreshold() {
     74         return CONNECTION_FAILURE_THRESHOLD;
     75     }
     76 
     77     /**
     78      * Sets the number of connection failures it takes for a particular SOCKS5 proxy to be
     79      * blacklisted. When a proxy is blacklisted no more connection attempts will be made to it for a
     80      * period of 2 hours. Default is 2.
     81      * <p>
     82      * Setting the connection failure threshold to zero disables the blacklisting.
     83      *
     84      * @param connectFailureThreshold the number of connection failures it takes for a particular
     85      *        SOCKS5 proxy to be blacklisted
     86      */
     87     public static void setConnectFailureThreshold(int connectFailureThreshold) {
     88         CONNECTION_FAILURE_THRESHOLD = connectFailureThreshold;
     89     }
     90 
     91     /**
     92      * Creates a new Socks5BytestreamRequest.
     93      *
     94      * @param manager the SOCKS5 Bytestream manager
     95      * @param bytestreamRequest the SOCKS5 Bytestream initialization packet
     96      */
     97     protected Socks5BytestreamRequest(Socks5BytestreamManager manager, Bytestream bytestreamRequest) {
     98         this.manager = manager;
     99         this.bytestreamRequest = bytestreamRequest;
    100     }
    101 
    102     /**
    103      * Returns the maximum timeout to connect to SOCKS5 proxies. Default is 10000ms.
    104      * <p>
    105      * When accepting a SOCKS5 Bytestream request Smack tries to connect to all SOCKS5 proxies given
    106      * by the initiator until a connection is established. This timeout divided by the number of
    107      * SOCKS5 proxies determines the timeout for every connection attempt.
    108      * <p>
    109      * You can set the minimum timeout for establishing a connection to one SOCKS5 proxy by invoking
    110      * {@link #setMinimumConnectTimeout(int)}.
    111      *
    112      * @return the maximum timeout to connect to SOCKS5 proxies
    113      */
    114     public int getTotalConnectTimeout() {
    115         if (this.totalConnectTimeout <= 0) {
    116             return 10000;
    117         }
    118         return this.totalConnectTimeout;
    119     }
    120 
    121     /**
    122      * Sets the maximum timeout to connect to SOCKS5 proxies. Default is 10000ms.
    123      * <p>
    124      * When accepting a SOCKS5 Bytestream request Smack tries to connect to all SOCKS5 proxies given
    125      * by the initiator until a connection is established. This timeout divided by the number of
    126      * SOCKS5 proxies determines the timeout for every connection attempt.
    127      * <p>
    128      * You can set the minimum timeout for establishing a connection to one SOCKS5 proxy by invoking
    129      * {@link #setMinimumConnectTimeout(int)}.
    130      *
    131      * @param totalConnectTimeout the maximum timeout to connect to SOCKS5 proxies
    132      */
    133     public void setTotalConnectTimeout(int totalConnectTimeout) {
    134         this.totalConnectTimeout = totalConnectTimeout;
    135     }
    136 
    137     /**
    138      * Returns the timeout to connect to one SOCKS5 proxy while accepting the SOCKS5 Bytestream
    139      * request. Default is 2000ms.
    140      *
    141      * @return the timeout to connect to one SOCKS5 proxy
    142      */
    143     public int getMinimumConnectTimeout() {
    144         if (this.minimumConnectTimeout <= 0) {
    145             return 2000;
    146         }
    147         return this.minimumConnectTimeout;
    148     }
    149 
    150     /**
    151      * Sets the timeout to connect to one SOCKS5 proxy while accepting the SOCKS5 Bytestream
    152      * request. Default is 2000ms.
    153      *
    154      * @param minimumConnectTimeout the timeout to connect to one SOCKS5 proxy
    155      */
    156     public void setMinimumConnectTimeout(int minimumConnectTimeout) {
    157         this.minimumConnectTimeout = minimumConnectTimeout;
    158     }
    159 
    160     /**
    161      * Returns the sender of the SOCKS5 Bytestream initialization request.
    162      *
    163      * @return the sender of the SOCKS5 Bytestream initialization request.
    164      */
    165     public String getFrom() {
    166         return this.bytestreamRequest.getFrom();
    167     }
    168 
    169     /**
    170      * Returns the session ID of the SOCKS5 Bytestream initialization request.
    171      *
    172      * @return the session ID of the SOCKS5 Bytestream initialization request.
    173      */
    174     public String getSessionID() {
    175         return this.bytestreamRequest.getSessionID();
    176     }
    177 
    178     /**
    179      * Accepts the SOCKS5 Bytestream initialization request and returns the socket to send/receive
    180      * data.
    181      * <p>
    182      * Before accepting the SOCKS5 Bytestream request you can set timeouts by invoking
    183      * {@link #setTotalConnectTimeout(int)} and {@link #setMinimumConnectTimeout(int)}.
    184      *
    185      * @return the socket to send/receive data
    186      * @throws XMPPException if connection to all SOCKS5 proxies failed or if stream is invalid.
    187      * @throws InterruptedException if the current thread was interrupted while waiting
    188      */
    189     public Socks5BytestreamSession accept() throws XMPPException, InterruptedException {
    190         Collection<StreamHost> streamHosts = this.bytestreamRequest.getStreamHosts();
    191 
    192         // throw exceptions if request contains no stream hosts
    193         if (streamHosts.size() == 0) {
    194             cancelRequest();
    195         }
    196 
    197         StreamHost selectedHost = null;
    198         Socket socket = null;
    199 
    200         String digest = Socks5Utils.createDigest(this.bytestreamRequest.getSessionID(),
    201                         this.bytestreamRequest.getFrom(), this.manager.getConnection().getUser());
    202 
    203         /*
    204          * determine timeout for each connection attempt; each SOCKS5 proxy has the same amount of
    205          * time so that the first does not consume the whole timeout
    206          */
    207         int timeout = Math.max(getTotalConnectTimeout() / streamHosts.size(),
    208                         getMinimumConnectTimeout());
    209 
    210         for (StreamHost streamHost : streamHosts) {
    211             String address = streamHost.getAddress() + ":" + streamHost.getPort();
    212 
    213             // check to see if this address has been blacklisted
    214             int failures = getConnectionFailures(address);
    215             if (CONNECTION_FAILURE_THRESHOLD > 0 && failures >= CONNECTION_FAILURE_THRESHOLD) {
    216                 continue;
    217             }
    218 
    219             // establish socket
    220             try {
    221 
    222                 // build SOCKS5 client
    223                 final Socks5Client socks5Client = new Socks5Client(streamHost, digest);
    224 
    225                 // connect to SOCKS5 proxy with a timeout
    226                 socket = socks5Client.getSocket(timeout);
    227 
    228                 // set selected host
    229                 selectedHost = streamHost;
    230                 break;
    231 
    232             }
    233             catch (TimeoutException e) {
    234                 incrementConnectionFailures(address);
    235             }
    236             catch (IOException e) {
    237                 incrementConnectionFailures(address);
    238             }
    239             catch (XMPPException e) {
    240                 incrementConnectionFailures(address);
    241             }
    242 
    243         }
    244 
    245         // throw exception if connecting to all SOCKS5 proxies failed
    246         if (selectedHost == null || socket == null) {
    247             cancelRequest();
    248         }
    249 
    250         // send used-host confirmation
    251         Bytestream response = createUsedHostResponse(selectedHost);
    252         this.manager.getConnection().sendPacket(response);
    253 
    254         return new Socks5BytestreamSession(socket, selectedHost.getJID().equals(
    255                         this.bytestreamRequest.getFrom()));
    256 
    257     }
    258 
    259     /**
    260      * Rejects the SOCKS5 Bytestream request by sending a reject error to the initiator.
    261      */
    262     public void reject() {
    263         this.manager.replyRejectPacket(this.bytestreamRequest);
    264     }
    265 
    266     /**
    267      * Cancels the SOCKS5 Bytestream request by sending an error to the initiator and building a
    268      * XMPP exception.
    269      *
    270      * @throws XMPPException XMPP exception containing the XMPP error
    271      */
    272     private void cancelRequest() throws XMPPException {
    273         String errorMessage = "Could not establish socket with any provided host";
    274         XMPPError error = new XMPPError(XMPPError.Condition.item_not_found, errorMessage);
    275         IQ errorIQ = IQ.createErrorResponse(this.bytestreamRequest, error);
    276         this.manager.getConnection().sendPacket(errorIQ);
    277         throw new XMPPException(errorMessage, error);
    278     }
    279 
    280     /**
    281      * Returns the response to the SOCKS5 Bytestream request containing the SOCKS5 proxy used.
    282      *
    283      * @param selectedHost the used SOCKS5 proxy
    284      * @return the response to the SOCKS5 Bytestream request
    285      */
    286     private Bytestream createUsedHostResponse(StreamHost selectedHost) {
    287         Bytestream response = new Bytestream(this.bytestreamRequest.getSessionID());
    288         response.setTo(this.bytestreamRequest.getFrom());
    289         response.setType(IQ.Type.RESULT);
    290         response.setPacketID(this.bytestreamRequest.getPacketID());
    291         response.setUsedHost(selectedHost.getJID());
    292         return response;
    293     }
    294 
    295     /**
    296      * Increments the connection failure counter by one for the given address.
    297      *
    298      * @param address the address the connection failure counter should be increased
    299      */
    300     private void incrementConnectionFailures(String address) {
    301         Integer count = ADDRESS_BLACKLIST.get(address);
    302         ADDRESS_BLACKLIST.put(address, count == null ? 1 : count + 1);
    303     }
    304 
    305     /**
    306      * Returns how often the connection to the given address failed.
    307      *
    308      * @param address the address
    309      * @return number of connection failures
    310      */
    311     private int getConnectionFailures(String address) {
    312         Integer count = ADDRESS_BLACKLIST.get(address);
    313         return count != null ? count : 0;
    314     }
    315 
    316 }
    317