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