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