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