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