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