1 /** 2 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); 3 * you may not use this file except in compliance with the License. 4 * You may obtain a copy of the License at 5 * 6 * http://www.apache.org/licenses/LICENSE-2.0 7 * 8 * Unless required by applicable law or agreed to in writing, software 9 * distributed under the License is distributed on an "AS IS" BASIS, 10 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 * See the License for the specific language governing permissions and 12 * limitations under the License. 13 */ 14 package org.jivesoftware.smackx.bytestreams.socks5; 15 16 import java.io.IOException; 17 import java.net.Socket; 18 import java.util.Collection; 19 import java.util.concurrent.TimeoutException; 20 21 import org.jivesoftware.smack.XMPPException; 22 import org.jivesoftware.smack.packet.IQ; 23 import org.jivesoftware.smack.packet.XMPPError; 24 import org.jivesoftware.smack.util.Cache; 25 import org.jivesoftware.smackx.bytestreams.BytestreamRequest; 26 import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream; 27 import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost; 28 29 /** 30 * Socks5BytestreamRequest class handles incoming SOCKS5 Bytestream requests. 31 * 32 * @author Henning Staib 33 */ 34 public class Socks5BytestreamRequest implements BytestreamRequest { 35 36 /* lifetime of an Item in the blacklist */ 37 private static final long BLACKLIST_LIFETIME = 60 * 1000 * 120; 38 39 /* size of the blacklist */ 40 private static final int BLACKLIST_MAX_SIZE = 100; 41 42 /* blacklist of addresses of SOCKS5 proxies */ 43 private static final Cache<String, Integer> ADDRESS_BLACKLIST = new Cache<String, Integer>( 44 BLACKLIST_MAX_SIZE, BLACKLIST_LIFETIME); 45 46 /* 47 * The number of connection failures it takes for a particular SOCKS5 proxy to be blacklisted. 48 * When a proxy is blacklisted no more connection attempts will be made to it for a period of 2 49 * hours. 50 */ 51 private static int CONNECTION_FAILURE_THRESHOLD = 2; 52 53 /* the bytestream initialization request */ 54 private Bytestream bytestreamRequest; 55 56 /* SOCKS5 Bytestream manager containing the XMPP connection and helper methods */ 57 private Socks5BytestreamManager manager; 58 59 /* timeout to connect to all SOCKS5 proxies */ 60 private int totalConnectTimeout = 10000; 61 62 /* minimum timeout to connect to one SOCKS5 proxy */ 63 private int minimumConnectTimeout = 2000; 64 65 /** 66 * Returns the number of connection failures it takes for a particular SOCKS5 proxy to be 67 * blacklisted. When a proxy is blacklisted no more connection attempts will be made to it for a 68 * period of 2 hours. Default is 2. 69 * 70 * @return the number of connection failures it takes for a particular SOCKS5 proxy to be 71 * blacklisted 72 */ 73 public static int getConnectFailureThreshold() { 74 return CONNECTION_FAILURE_THRESHOLD; 75 } 76 77 /** 78 * Sets the number of connection failures it takes for a particular SOCKS5 proxy to be 79 * blacklisted. When a proxy is blacklisted no more connection attempts will be made to it for a 80 * period of 2 hours. Default is 2. 81 * <p> 82 * Setting the connection failure threshold to zero disables the blacklisting. 83 * 84 * @param connectFailureThreshold the number of connection failures it takes for a particular 85 * SOCKS5 proxy to be blacklisted 86 */ 87 public static void setConnectFailureThreshold(int connectFailureThreshold) { 88 CONNECTION_FAILURE_THRESHOLD = connectFailureThreshold; 89 } 90 91 /** 92 * Creates a new Socks5BytestreamRequest. 93 * 94 * @param manager the SOCKS5 Bytestream manager 95 * @param bytestreamRequest the SOCKS5 Bytestream initialization packet 96 */ 97 protected Socks5BytestreamRequest(Socks5BytestreamManager manager, Bytestream bytestreamRequest) { 98 this.manager = manager; 99 this.bytestreamRequest = bytestreamRequest; 100 } 101 102 /** 103 * Returns the maximum timeout to connect to SOCKS5 proxies. Default is 10000ms. 104 * <p> 105 * When accepting a SOCKS5 Bytestream request Smack tries to connect to all SOCKS5 proxies given 106 * by the initiator until a connection is established. This timeout divided by the number of 107 * SOCKS5 proxies determines the timeout for every connection attempt. 108 * <p> 109 * You can set the minimum timeout for establishing a connection to one SOCKS5 proxy by invoking 110 * {@link #setMinimumConnectTimeout(int)}. 111 * 112 * @return the maximum timeout to connect to SOCKS5 proxies 113 */ 114 public int getTotalConnectTimeout() { 115 if (this.totalConnectTimeout <= 0) { 116 return 10000; 117 } 118 return this.totalConnectTimeout; 119 } 120 121 /** 122 * Sets the maximum timeout to connect to SOCKS5 proxies. Default is 10000ms. 123 * <p> 124 * When accepting a SOCKS5 Bytestream request Smack tries to connect to all SOCKS5 proxies given 125 * by the initiator until a connection is established. This timeout divided by the number of 126 * SOCKS5 proxies determines the timeout for every connection attempt. 127 * <p> 128 * You can set the minimum timeout for establishing a connection to one SOCKS5 proxy by invoking 129 * {@link #setMinimumConnectTimeout(int)}. 130 * 131 * @param totalConnectTimeout the maximum timeout to connect to SOCKS5 proxies 132 */ 133 public void setTotalConnectTimeout(int totalConnectTimeout) { 134 this.totalConnectTimeout = totalConnectTimeout; 135 } 136 137 /** 138 * Returns the timeout to connect to one SOCKS5 proxy while accepting the SOCKS5 Bytestream 139 * request. Default is 2000ms. 140 * 141 * @return the timeout to connect to one SOCKS5 proxy 142 */ 143 public int getMinimumConnectTimeout() { 144 if (this.minimumConnectTimeout <= 0) { 145 return 2000; 146 } 147 return this.minimumConnectTimeout; 148 } 149 150 /** 151 * Sets the timeout to connect to one SOCKS5 proxy while accepting the SOCKS5 Bytestream 152 * request. Default is 2000ms. 153 * 154 * @param minimumConnectTimeout the timeout to connect to one SOCKS5 proxy 155 */ 156 public void setMinimumConnectTimeout(int minimumConnectTimeout) { 157 this.minimumConnectTimeout = minimumConnectTimeout; 158 } 159 160 /** 161 * Returns the sender of the SOCKS5 Bytestream initialization request. 162 * 163 * @return the sender of the SOCKS5 Bytestream initialization request. 164 */ 165 public String getFrom() { 166 return this.bytestreamRequest.getFrom(); 167 } 168 169 /** 170 * Returns the session ID of the SOCKS5 Bytestream initialization request. 171 * 172 * @return the session ID of the SOCKS5 Bytestream initialization request. 173 */ 174 public String getSessionID() { 175 return this.bytestreamRequest.getSessionID(); 176 } 177 178 /** 179 * Accepts the SOCKS5 Bytestream initialization request and returns the socket to send/receive 180 * data. 181 * <p> 182 * Before accepting the SOCKS5 Bytestream request you can set timeouts by invoking 183 * {@link #setTotalConnectTimeout(int)} and {@link #setMinimumConnectTimeout(int)}. 184 * 185 * @return the socket to send/receive data 186 * @throws XMPPException if connection to all SOCKS5 proxies failed or if stream is invalid. 187 * @throws InterruptedException if the current thread was interrupted while waiting 188 */ 189 public Socks5BytestreamSession accept() throws XMPPException, InterruptedException { 190 Collection<StreamHost> streamHosts = this.bytestreamRequest.getStreamHosts(); 191 192 // throw exceptions if request contains no stream hosts 193 if (streamHosts.size() == 0) { 194 cancelRequest(); 195 } 196 197 StreamHost selectedHost = null; 198 Socket socket = null; 199 200 String digest = Socks5Utils.createDigest(this.bytestreamRequest.getSessionID(), 201 this.bytestreamRequest.getFrom(), this.manager.getConnection().getUser()); 202 203 /* 204 * determine timeout for each connection attempt; each SOCKS5 proxy has the same amount of 205 * time so that the first does not consume the whole timeout 206 */ 207 int timeout = Math.max(getTotalConnectTimeout() / streamHosts.size(), 208 getMinimumConnectTimeout()); 209 210 for (StreamHost streamHost : streamHosts) { 211 String address = streamHost.getAddress() + ":" + streamHost.getPort(); 212 213 // check to see if this address has been blacklisted 214 int failures = getConnectionFailures(address); 215 if (CONNECTION_FAILURE_THRESHOLD > 0 && failures >= CONNECTION_FAILURE_THRESHOLD) { 216 continue; 217 } 218 219 // establish socket 220 try { 221 222 // build SOCKS5 client 223 final Socks5Client socks5Client = new Socks5Client(streamHost, digest); 224 225 // connect to SOCKS5 proxy with a timeout 226 socket = socks5Client.getSocket(timeout); 227 228 // set selected host 229 selectedHost = streamHost; 230 break; 231 232 } 233 catch (TimeoutException e) { 234 incrementConnectionFailures(address); 235 } 236 catch (IOException e) { 237 incrementConnectionFailures(address); 238 } 239 catch (XMPPException e) { 240 incrementConnectionFailures(address); 241 } 242 243 } 244 245 // throw exception if connecting to all SOCKS5 proxies failed 246 if (selectedHost == null || socket == null) { 247 cancelRequest(); 248 } 249 250 // send used-host confirmation 251 Bytestream response = createUsedHostResponse(selectedHost); 252 this.manager.getConnection().sendPacket(response); 253 254 return new Socks5BytestreamSession(socket, selectedHost.getJID().equals( 255 this.bytestreamRequest.getFrom())); 256 257 } 258 259 /** 260 * Rejects the SOCKS5 Bytestream request by sending a reject error to the initiator. 261 */ 262 public void reject() { 263 this.manager.replyRejectPacket(this.bytestreamRequest); 264 } 265 266 /** 267 * Cancels the SOCKS5 Bytestream request by sending an error to the initiator and building a 268 * XMPP exception. 269 * 270 * @throws XMPPException XMPP exception containing the XMPP error 271 */ 272 private void cancelRequest() throws XMPPException { 273 String errorMessage = "Could not establish socket with any provided host"; 274 XMPPError error = new XMPPError(XMPPError.Condition.item_not_found, errorMessage); 275 IQ errorIQ = IQ.createErrorResponse(this.bytestreamRequest, error); 276 this.manager.getConnection().sendPacket(errorIQ); 277 throw new XMPPException(errorMessage, error); 278 } 279 280 /** 281 * Returns the response to the SOCKS5 Bytestream request containing the SOCKS5 proxy used. 282 * 283 * @param selectedHost the used SOCKS5 proxy 284 * @return the response to the SOCKS5 Bytestream request 285 */ 286 private Bytestream createUsedHostResponse(StreamHost selectedHost) { 287 Bytestream response = new Bytestream(this.bytestreamRequest.getSessionID()); 288 response.setTo(this.bytestreamRequest.getFrom()); 289 response.setType(IQ.Type.RESULT); 290 response.setPacketID(this.bytestreamRequest.getPacketID()); 291 response.setUsedHost(selectedHost.getJID()); 292 return response; 293 } 294 295 /** 296 * Increments the connection failure counter by one for the given address. 297 * 298 * @param address the address the connection failure counter should be increased 299 */ 300 private void incrementConnectionFailures(String address) { 301 Integer count = ADDRESS_BLACKLIST.get(address); 302 ADDRESS_BLACKLIST.put(address, count == null ? 1 : count + 1); 303 } 304 305 /** 306 * Returns how often the connection to the given address failed. 307 * 308 * @param address the address 309 * @return number of connection failures 310 */ 311 private int getConnectionFailures(String address) { 312 Integer count = ADDRESS_BLACKLIST.get(address); 313 return count != null ? count : 0; 314 } 315 316 } 317