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