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