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