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