001/**
002 *
003 * Copyright the original author or authors
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smackx.bytestreams.socks5;
018
019import java.io.IOException;
020import java.net.Socket;
021import java.util.Collection;
022import java.util.concurrent.TimeoutException;
023
024import org.jivesoftware.smack.SmackException;
025import org.jivesoftware.smack.SmackException.NotConnectedException;
026import org.jivesoftware.smack.XMPPException;
027import org.jivesoftware.smack.XMPPException.XMPPErrorException;
028import org.jivesoftware.smack.packet.IQ;
029import org.jivesoftware.smack.packet.XMPPError;
030import org.jivesoftware.smackx.bytestreams.BytestreamRequest;
031import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
032import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;
033import org.jxmpp.util.cache.Cache;
034import org.jxmpp.util.cache.ExpirationCache;
035
036/**
037 * Socks5BytestreamRequest class handles incoming SOCKS5 Bytestream requests.
038 * 
039 * @author Henning Staib
040 */
041public class Socks5BytestreamRequest implements BytestreamRequest {
042
043    /* lifetime of an Item in the blacklist */
044    private static final long BLACKLIST_LIFETIME = 60 * 1000 * 120;
045
046    /* size of the blacklist */
047    private static final int BLACKLIST_MAX_SIZE = 100;
048
049    /* blacklist of addresses of SOCKS5 proxies */
050    private static final Cache<String, Integer> ADDRESS_BLACKLIST = new ExpirationCache<String, Integer>(
051                    BLACKLIST_MAX_SIZE, BLACKLIST_LIFETIME);
052
053    /*
054     * The number of connection failures it takes for a particular SOCKS5 proxy to be blacklisted.
055     * When a proxy is blacklisted no more connection attempts will be made to it for a period of 2
056     * hours.
057     */
058    private static int CONNECTION_FAILURE_THRESHOLD = 2;
059
060    /* the bytestream initialization request */
061    private Bytestream bytestreamRequest;
062
063    /* SOCKS5 Bytestream manager containing the XMPP connection and helper methods */
064    private Socks5BytestreamManager manager;
065
066    /* timeout to connect to all SOCKS5 proxies */
067    private int totalConnectTimeout = 10000;
068
069    /* minimum timeout to connect to one SOCKS5 proxy */
070    private int minimumConnectTimeout = 2000;
071
072    /**
073     * Returns the number of connection failures it takes for a particular SOCKS5 proxy to be
074     * blacklisted. When a proxy is blacklisted no more connection attempts will be made to it for a
075     * period of 2 hours. Default is 2.
076     * 
077     * @return the number of connection failures it takes for a particular SOCKS5 proxy to be
078     *         blacklisted
079     */
080    public static int getConnectFailureThreshold() {
081        return CONNECTION_FAILURE_THRESHOLD;
082    }
083
084    /**
085     * Sets the number of connection failures it takes for a particular SOCKS5 proxy to be
086     * blacklisted. When a proxy is blacklisted no more connection attempts will be made to it for a
087     * period of 2 hours. Default is 2.
088     * <p>
089     * Setting the connection failure threshold to zero disables the blacklisting.
090     * 
091     * @param connectFailureThreshold the number of connection failures it takes for a particular
092     *        SOCKS5 proxy to be blacklisted
093     */
094    public static void setConnectFailureThreshold(int connectFailureThreshold) {
095        CONNECTION_FAILURE_THRESHOLD = connectFailureThreshold;
096    }
097
098    /**
099     * Creates a new Socks5BytestreamRequest.
100     * 
101     * @param manager the SOCKS5 Bytestream manager
102     * @param bytestreamRequest the SOCKS5 Bytestream initialization packet
103     */
104    protected Socks5BytestreamRequest(Socks5BytestreamManager manager, Bytestream bytestreamRequest) {
105        this.manager = manager;
106        this.bytestreamRequest = bytestreamRequest;
107    }
108
109    /**
110     * Returns the maximum timeout to connect to SOCKS5 proxies. Default is 10000ms.
111     * <p>
112     * When accepting a SOCKS5 Bytestream request Smack tries to connect to all SOCKS5 proxies given
113     * by the initiator until a connection is established. This timeout divided by the number of
114     * SOCKS5 proxies determines the timeout for every connection attempt.
115     * <p>
116     * You can set the minimum timeout for establishing a connection to one SOCKS5 proxy by invoking
117     * {@link #setMinimumConnectTimeout(int)}.
118     * 
119     * @return the maximum timeout to connect to SOCKS5 proxies
120     */
121    public int getTotalConnectTimeout() {
122        if (this.totalConnectTimeout <= 0) {
123            return 10000;
124        }
125        return this.totalConnectTimeout;
126    }
127
128    /**
129     * Sets the maximum timeout to connect to SOCKS5 proxies. Default is 10000ms.
130     * <p>
131     * When accepting a SOCKS5 Bytestream request Smack tries to connect to all SOCKS5 proxies given
132     * by the initiator until a connection is established. This timeout divided by the number of
133     * SOCKS5 proxies determines the timeout for every connection attempt.
134     * <p>
135     * You can set the minimum timeout for establishing a connection to one SOCKS5 proxy by invoking
136     * {@link #setMinimumConnectTimeout(int)}.
137     * 
138     * @param totalConnectTimeout the maximum timeout to connect to SOCKS5 proxies
139     */
140    public void setTotalConnectTimeout(int totalConnectTimeout) {
141        this.totalConnectTimeout = totalConnectTimeout;
142    }
143
144    /**
145     * Returns the timeout to connect to one SOCKS5 proxy while accepting the SOCKS5 Bytestream
146     * request. Default is 2000ms.
147     * 
148     * @return the timeout to connect to one SOCKS5 proxy
149     */
150    public int getMinimumConnectTimeout() {
151        if (this.minimumConnectTimeout <= 0) {
152            return 2000;
153        }
154        return this.minimumConnectTimeout;
155    }
156
157    /**
158     * Sets the timeout to connect to one SOCKS5 proxy while accepting the SOCKS5 Bytestream
159     * request. Default is 2000ms.
160     * 
161     * @param minimumConnectTimeout the timeout to connect to one SOCKS5 proxy
162     */
163    public void setMinimumConnectTimeout(int minimumConnectTimeout) {
164        this.minimumConnectTimeout = minimumConnectTimeout;
165    }
166
167    /**
168     * Returns the sender of the SOCKS5 Bytestream initialization request.
169     * 
170     * @return the sender of the SOCKS5 Bytestream initialization request.
171     */
172    public String getFrom() {
173        return this.bytestreamRequest.getFrom();
174    }
175
176    /**
177     * Returns the session ID of the SOCKS5 Bytestream initialization request.
178     * 
179     * @return the session ID of the SOCKS5 Bytestream initialization request.
180     */
181    public String getSessionID() {
182        return this.bytestreamRequest.getSessionID();
183    }
184
185    /**
186     * Accepts the SOCKS5 Bytestream initialization request and returns the socket to send/receive
187     * data.
188     * <p>
189     * Before accepting the SOCKS5 Bytestream request you can set timeouts by invoking
190     * {@link #setTotalConnectTimeout(int)} and {@link #setMinimumConnectTimeout(int)}.
191     * 
192     * @return the socket to send/receive data
193     * @throws InterruptedException if the current thread was interrupted while waiting
194     * @throws XMPPErrorException 
195     * @throws SmackException 
196     */
197    public Socks5BytestreamSession accept() throws InterruptedException, XMPPErrorException, SmackException {
198        Collection<StreamHost> streamHosts = this.bytestreamRequest.getStreamHosts();
199
200        // throw exceptions if request contains no stream hosts
201        if (streamHosts.size() == 0) {
202            cancelRequest();
203        }
204
205        StreamHost selectedHost = null;
206        Socket socket = null;
207
208        String digest = Socks5Utils.createDigest(this.bytestreamRequest.getSessionID(),
209                        this.bytestreamRequest.getFrom(), this.manager.getConnection().getUser());
210
211        /*
212         * determine timeout for each connection attempt; each SOCKS5 proxy has the same amount of
213         * time so that the first does not consume the whole timeout
214         */
215        int timeout = Math.max(getTotalConnectTimeout() / streamHosts.size(),
216                        getMinimumConnectTimeout());
217
218        for (StreamHost streamHost : streamHosts) {
219            String address = streamHost.getAddress() + ":" + streamHost.getPort();
220
221            // check to see if this address has been blacklisted
222            int failures = getConnectionFailures(address);
223            if (CONNECTION_FAILURE_THRESHOLD > 0 && failures >= CONNECTION_FAILURE_THRESHOLD) {
224                continue;
225            }
226
227            // establish socket
228            try {
229
230                // build SOCKS5 client
231                final Socks5Client socks5Client = new Socks5Client(streamHost, digest);
232
233                // connect to SOCKS5 proxy with a timeout
234                socket = socks5Client.getSocket(timeout);
235
236                // set selected host
237                selectedHost = streamHost;
238                break;
239
240            }
241            catch (TimeoutException e) {
242                incrementConnectionFailures(address);
243            }
244            catch (IOException e) {
245                incrementConnectionFailures(address);
246            }
247            catch (XMPPException e) {
248                incrementConnectionFailures(address);
249            }
250
251        }
252
253        // throw exception if connecting to all SOCKS5 proxies failed
254        if (selectedHost == null || socket == null) {
255            cancelRequest();
256        }
257
258        // send used-host confirmation
259        Bytestream response = createUsedHostResponse(selectedHost);
260        this.manager.getConnection().sendStanza(response);
261
262        return new Socks5BytestreamSession(socket, selectedHost.getJID().equals(
263                        this.bytestreamRequest.getFrom()));
264
265    }
266
267    /**
268     * Rejects the SOCKS5 Bytestream request by sending a reject error to the initiator.
269     * @throws NotConnectedException 
270     */
271    public void reject() throws NotConnectedException {
272        this.manager.replyRejectPacket(this.bytestreamRequest);
273    }
274
275    /**
276     * Cancels the SOCKS5 Bytestream request by sending an error to the initiator and building a
277     * XMPP exception.
278     * @throws XMPPErrorException 
279     * @throws NotConnectedException 
280     */
281    private void cancelRequest() throws XMPPErrorException, NotConnectedException {
282        String errorMessage = "Could not establish socket with any provided host";
283        XMPPError error = XMPPError.from(XMPPError.Condition.item_not_found, errorMessage);
284        IQ errorIQ = IQ.createErrorResponse(this.bytestreamRequest, error);
285        this.manager.getConnection().sendStanza(errorIQ);
286        throw new XMPPErrorException(errorMessage, error);
287    }
288
289    /**
290     * Returns the response to the SOCKS5 Bytestream request containing the SOCKS5 proxy used.
291     * 
292     * @param selectedHost the used SOCKS5 proxy
293     * @return the response to the SOCKS5 Bytestream request
294     */
295    private Bytestream createUsedHostResponse(StreamHost selectedHost) {
296        Bytestream response = new Bytestream(this.bytestreamRequest.getSessionID());
297        response.setTo(this.bytestreamRequest.getFrom());
298        response.setType(IQ.Type.result);
299        response.setStanzaId(this.bytestreamRequest.getStanzaId());
300        response.setUsedHost(selectedHost.getJID());
301        return response;
302    }
303
304    /**
305     * Increments the connection failure counter by one for the given address.
306     * 
307     * @param address the address the connection failure counter should be increased
308     */
309    private void incrementConnectionFailures(String address) {
310        Integer count = ADDRESS_BLACKLIST.get(address);
311        ADDRESS_BLACKLIST.put(address, count == null ? 1 : count + 1);
312    }
313
314    /**
315     * Returns how often the connection to the given address failed.
316     * 
317     * @param address the address
318     * @return number of connection failures
319     */
320    private int getConnectionFailures(String address) {
321        Integer count = ADDRESS_BLACKLIST.get(address);
322        return count != null ? count : 0;
323    }
324
325}