aboutsummaryrefslogtreecommitdiff
path: root/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamRequest.java
blob: 0b2fdeb6a6bdced5f75bb9f705c37a3d7ccc0a1a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
/**
 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jivesoftware.smackx.bytestreams.socks5;

import java.io.IOException;
import java.net.Socket;
import java.util.Collection;
import java.util.concurrent.TimeoutException;

import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.XMPPError;
import org.jivesoftware.smack.util.Cache;
import org.jivesoftware.smackx.bytestreams.BytestreamRequest;
import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;

/**
 * Socks5BytestreamRequest class handles incoming SOCKS5 Bytestream requests.
 * 
 * @author Henning Staib
 */
public class Socks5BytestreamRequest implements BytestreamRequest {

    /* lifetime of an Item in the blacklist */
    private static final long BLACKLIST_LIFETIME = 60 * 1000 * 120;

    /* size of the blacklist */
    private static final int BLACKLIST_MAX_SIZE = 100;

    /* blacklist of addresses of SOCKS5 proxies */
    private static final Cache<String, Integer> ADDRESS_BLACKLIST = new Cache<String, Integer>(
                    BLACKLIST_MAX_SIZE, BLACKLIST_LIFETIME);

    /*
     * The number of connection failures it takes for a particular SOCKS5 proxy to be blacklisted.
     * When a proxy is blacklisted no more connection attempts will be made to it for a period of 2
     * hours.
     */
    private static int CONNECTION_FAILURE_THRESHOLD = 2;

    /* the bytestream initialization request */
    private Bytestream bytestreamRequest;

    /* SOCKS5 Bytestream manager containing the XMPP connection and helper methods */
    private Socks5BytestreamManager manager;

    /* timeout to connect to all SOCKS5 proxies */
    private int totalConnectTimeout = 10000;

    /* minimum timeout to connect to one SOCKS5 proxy */
    private int minimumConnectTimeout = 2000;

    /**
     * Returns the number of connection failures it takes for a particular SOCKS5 proxy to be
     * blacklisted. When a proxy is blacklisted no more connection attempts will be made to it for a
     * period of 2 hours. Default is 2.
     * 
     * @return the number of connection failures it takes for a particular SOCKS5 proxy to be
     *         blacklisted
     */
    public static int getConnectFailureThreshold() {
        return CONNECTION_FAILURE_THRESHOLD;
    }

    /**
     * Sets the number of connection failures it takes for a particular SOCKS5 proxy to be
     * blacklisted. When a proxy is blacklisted no more connection attempts will be made to it for a
     * period of 2 hours. Default is 2.
     * <p>
     * Setting the connection failure threshold to zero disables the blacklisting.
     * 
     * @param connectFailureThreshold the number of connection failures it takes for a particular
     *        SOCKS5 proxy to be blacklisted
     */
    public static void setConnectFailureThreshold(int connectFailureThreshold) {
        CONNECTION_FAILURE_THRESHOLD = connectFailureThreshold;
    }

    /**
     * Creates a new Socks5BytestreamRequest.
     * 
     * @param manager the SOCKS5 Bytestream manager
     * @param bytestreamRequest the SOCKS5 Bytestream initialization packet
     */
    protected Socks5BytestreamRequest(Socks5BytestreamManager manager, Bytestream bytestreamRequest) {
        this.manager = manager;
        this.bytestreamRequest = bytestreamRequest;
    }

    /**
     * Returns the maximum timeout to connect to SOCKS5 proxies. Default is 10000ms.
     * <p>
     * When accepting a SOCKS5 Bytestream request Smack tries to connect to all SOCKS5 proxies given
     * by the initiator until a connection is established. This timeout divided by the number of
     * SOCKS5 proxies determines the timeout for every connection attempt.
     * <p>
     * You can set the minimum timeout for establishing a connection to one SOCKS5 proxy by invoking
     * {@link #setMinimumConnectTimeout(int)}.
     * 
     * @return the maximum timeout to connect to SOCKS5 proxies
     */
    public int getTotalConnectTimeout() {
        if (this.totalConnectTimeout <= 0) {
            return 10000;
        }
        return this.totalConnectTimeout;
    }

    /**
     * Sets the maximum timeout to connect to SOCKS5 proxies. Default is 10000ms.
     * <p>
     * When accepting a SOCKS5 Bytestream request Smack tries to connect to all SOCKS5 proxies given
     * by the initiator until a connection is established. This timeout divided by the number of
     * SOCKS5 proxies determines the timeout for every connection attempt.
     * <p>
     * You can set the minimum timeout for establishing a connection to one SOCKS5 proxy by invoking
     * {@link #setMinimumConnectTimeout(int)}.
     * 
     * @param totalConnectTimeout the maximum timeout to connect to SOCKS5 proxies
     */
    public void setTotalConnectTimeout(int totalConnectTimeout) {
        this.totalConnectTimeout = totalConnectTimeout;
    }

    /**
     * Returns the timeout to connect to one SOCKS5 proxy while accepting the SOCKS5 Bytestream
     * request. Default is 2000ms.
     * 
     * @return the timeout to connect to one SOCKS5 proxy
     */
    public int getMinimumConnectTimeout() {
        if (this.minimumConnectTimeout <= 0) {
            return 2000;
        }
        return this.minimumConnectTimeout;
    }

    /**
     * Sets the timeout to connect to one SOCKS5 proxy while accepting the SOCKS5 Bytestream
     * request. Default is 2000ms.
     * 
     * @param minimumConnectTimeout the timeout to connect to one SOCKS5 proxy
     */
    public void setMinimumConnectTimeout(int minimumConnectTimeout) {
        this.minimumConnectTimeout = minimumConnectTimeout;
    }

    /**
     * Returns the sender of the SOCKS5 Bytestream initialization request.
     * 
     * @return the sender of the SOCKS5 Bytestream initialization request.
     */
    public String getFrom() {
        return this.bytestreamRequest.getFrom();
    }

    /**
     * Returns the session ID of the SOCKS5 Bytestream initialization request.
     * 
     * @return the session ID of the SOCKS5 Bytestream initialization request.
     */
    public String getSessionID() {
        return this.bytestreamRequest.getSessionID();
    }

    /**
     * Accepts the SOCKS5 Bytestream initialization request and returns the socket to send/receive
     * data.
     * <p>
     * Before accepting the SOCKS5 Bytestream request you can set timeouts by invoking
     * {@link #setTotalConnectTimeout(int)} and {@link #setMinimumConnectTimeout(int)}.
     * 
     * @return the socket to send/receive data
     * @throws XMPPException if connection to all SOCKS5 proxies failed or if stream is invalid.
     * @throws InterruptedException if the current thread was interrupted while waiting
     */
    public Socks5BytestreamSession accept() throws XMPPException, InterruptedException {
        Collection<StreamHost> streamHosts = this.bytestreamRequest.getStreamHosts();

        // throw exceptions if request contains no stream hosts
        if (streamHosts.size() == 0) {
            cancelRequest();
        }

        StreamHost selectedHost = null;
        Socket socket = null;

        String digest = Socks5Utils.createDigest(this.bytestreamRequest.getSessionID(),
                        this.bytestreamRequest.getFrom(), this.manager.getConnection().getUser());

        /*
         * determine timeout for each connection attempt; each SOCKS5 proxy has the same amount of
         * time so that the first does not consume the whole timeout
         */
        int timeout = Math.max(getTotalConnectTimeout() / streamHosts.size(),
                        getMinimumConnectTimeout());

        for (StreamHost streamHost : streamHosts) {
            String address = streamHost.getAddress() + ":" + streamHost.getPort();

            // check to see if this address has been blacklisted
            int failures = getConnectionFailures(address);
            if (CONNECTION_FAILURE_THRESHOLD > 0 && failures >= CONNECTION_FAILURE_THRESHOLD) {
                continue;
            }

            // establish socket
            try {

                // build SOCKS5 client
                final Socks5Client socks5Client = new Socks5Client(streamHost, digest);

                // connect to SOCKS5 proxy with a timeout
                socket = socks5Client.getSocket(timeout);

                // set selected host
                selectedHost = streamHost;
                break;

            }
            catch (TimeoutException e) {
                incrementConnectionFailures(address);
            }
            catch (IOException e) {
                incrementConnectionFailures(address);
            }
            catch (XMPPException e) {
                incrementConnectionFailures(address);
            }

        }

        // throw exception if connecting to all SOCKS5 proxies failed
        if (selectedHost == null || socket == null) {
            cancelRequest();
        }

        // send used-host confirmation
        Bytestream response = createUsedHostResponse(selectedHost);
        this.manager.getConnection().sendPacket(response);

        return new Socks5BytestreamSession(socket, selectedHost.getJID().equals(
                        this.bytestreamRequest.getFrom()));

    }

    /**
     * Rejects the SOCKS5 Bytestream request by sending a reject error to the initiator.
     */
    public void reject() {
        this.manager.replyRejectPacket(this.bytestreamRequest);
    }

    /**
     * Cancels the SOCKS5 Bytestream request by sending an error to the initiator and building a
     * XMPP exception.
     * 
     * @throws XMPPException XMPP exception containing the XMPP error
     */
    private void cancelRequest() throws XMPPException {
        String errorMessage = "Could not establish socket with any provided host";
        XMPPError error = new XMPPError(XMPPError.Condition.item_not_found, errorMessage);
        IQ errorIQ = IQ.createErrorResponse(this.bytestreamRequest, error);
        this.manager.getConnection().sendPacket(errorIQ);
        throw new XMPPException(errorMessage, error);
    }

    /**
     * Returns the response to the SOCKS5 Bytestream request containing the SOCKS5 proxy used.
     * 
     * @param selectedHost the used SOCKS5 proxy
     * @return the response to the SOCKS5 Bytestream request
     */
    private Bytestream createUsedHostResponse(StreamHost selectedHost) {
        Bytestream response = new Bytestream(this.bytestreamRequest.getSessionID());
        response.setTo(this.bytestreamRequest.getFrom());
        response.setType(IQ.Type.RESULT);
        response.setPacketID(this.bytestreamRequest.getPacketID());
        response.setUsedHost(selectedHost.getJID());
        return response;
    }

    /**
     * Increments the connection failure counter by one for the given address.
     * 
     * @param address the address the connection failure counter should be increased
     */
    private void incrementConnectionFailures(String address) {
        Integer count = ADDRESS_BLACKLIST.get(address);
        ADDRESS_BLACKLIST.put(address, count == null ? 1 : count + 1);
    }

    /**
     * Returns how often the connection to the given address failed.
     * 
     * @param address the address
     * @return number of connection failures
     */
    private int getConnectionFailures(String address) {
        Integer count = ADDRESS_BLACKLIST.get(address);
        return count != null ? count : 0;
    }

}