/** * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jivesoftware.smackx.bytestreams.socks5; import java.io.IOException; import java.net.Socket; import java.util.Collection; import java.util.concurrent.TimeoutException; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.XMPPError; import org.jivesoftware.smack.util.Cache; import org.jivesoftware.smackx.bytestreams.BytestreamRequest; import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream; import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost; /** * Socks5BytestreamRequest class handles incoming SOCKS5 Bytestream requests. * * @author Henning Staib */ public class Socks5BytestreamRequest implements BytestreamRequest { /* lifetime of an Item in the blacklist */ private static final long BLACKLIST_LIFETIME = 60 * 1000 * 120; /* size of the blacklist */ private static final int BLACKLIST_MAX_SIZE = 100; /* blacklist of addresses of SOCKS5 proxies */ private static final Cache<String, Integer> ADDRESS_BLACKLIST = new Cache<String, Integer>( BLACKLIST_MAX_SIZE, BLACKLIST_LIFETIME); /* * The number of connection failures it takes for a particular SOCKS5 proxy * to be blacklisted. When a proxy is blacklisted no more connection * attempts will be made to it for a period of 2 hours. */ private static int CONNECTION_FAILURE_THRESHOLD = 2; /** * Returns the number of connection failures it takes for a particular * SOCKS5 proxy to be blacklisted. When a proxy is blacklisted no more * connection attempts will be made to it for a period of 2 hours. Default * is 2. * * @return the number of connection failures it takes for a particular * SOCKS5 proxy to be blacklisted */ public static int getConnectFailureThreshold() { return CONNECTION_FAILURE_THRESHOLD; } /** * Sets the number of connection failures it takes for a particular SOCKS5 * proxy to be blacklisted. When a proxy is blacklisted no more connection * attempts will be made to it for a period of 2 hours. Default is 2. * <p> * Setting the connection failure threshold to zero disables the * blacklisting. * * @param connectFailureThreshold * the number of connection failures it takes for a particular * SOCKS5 proxy to be blacklisted */ public static void setConnectFailureThreshold(int connectFailureThreshold) { CONNECTION_FAILURE_THRESHOLD = connectFailureThreshold; } /* the bytestream initialization request */ private final Bytestream bytestreamRequest; /* * SOCKS5 Bytestream manager containing the XMPP connection and helper * methods */ private final Socks5BytestreamManager manager; /* timeout to connect to all SOCKS5 proxies */ private int totalConnectTimeout = 10000; /* minimum timeout to connect to one SOCKS5 proxy */ private int minimumConnectTimeout = 2000; /** * Creates a new Socks5BytestreamRequest. * * @param manager * the SOCKS5 Bytestream manager * @param bytestreamRequest * the SOCKS5 Bytestream initialization packet */ protected Socks5BytestreamRequest(Socks5BytestreamManager manager, Bytestream bytestreamRequest) { this.manager = manager; this.bytestreamRequest = bytestreamRequest; } /** * Accepts the SOCKS5 Bytestream initialization request and returns the * socket to send/receive data. * <p> * Before accepting the SOCKS5 Bytestream request you can set timeouts by * invoking {@link #setTotalConnectTimeout(int)} and * {@link #setMinimumConnectTimeout(int)}. * * @return the socket to send/receive data * @throws XMPPException * if connection to all SOCKS5 proxies failed or if stream is * invalid. * @throws InterruptedException * if the current thread was interrupted while waiting */ @Override public Socks5BytestreamSession accept() throws XMPPException, InterruptedException { final Collection<StreamHost> streamHosts = bytestreamRequest .getStreamHosts(); // throw exceptions if request contains no stream hosts if (streamHosts.size() == 0) { cancelRequest(); } StreamHost selectedHost = null; Socket socket = null; final String digest = Socks5Utils.createDigest( bytestreamRequest.getSessionID(), bytestreamRequest.getFrom(), manager.getConnection().getUser()); /* * determine timeout for each connection attempt; each SOCKS5 proxy has * the same amount of time so that the first does not consume the whole * timeout */ final int timeout = Math.max( getTotalConnectTimeout() / streamHosts.size(), getMinimumConnectTimeout()); for (final StreamHost streamHost : streamHosts) { final String address = streamHost.getAddress() + ":" + streamHost.getPort(); // check to see if this address has been blacklisted final int failures = getConnectionFailures(address); if (CONNECTION_FAILURE_THRESHOLD > 0 && failures >= CONNECTION_FAILURE_THRESHOLD) { continue; } // establish socket try { // build SOCKS5 client final Socks5Client socks5Client = new Socks5Client(streamHost, digest); // connect to SOCKS5 proxy with a timeout socket = socks5Client.getSocket(timeout); // set selected host selectedHost = streamHost; break; } catch (final TimeoutException e) { incrementConnectionFailures(address); } catch (final IOException e) { incrementConnectionFailures(address); } catch (final XMPPException e) { incrementConnectionFailures(address); } } // throw exception if connecting to all SOCKS5 proxies failed if (selectedHost == null || socket == null) { cancelRequest(); } // send used-host confirmation final Bytestream response = createUsedHostResponse(selectedHost); manager.getConnection().sendPacket(response); return new Socks5BytestreamSession(socket, selectedHost.getJID() .equals(bytestreamRequest.getFrom())); } /** * Cancels the SOCKS5 Bytestream request by sending an error to the * initiator and building a XMPP exception. * * @throws XMPPException * XMPP exception containing the XMPP error */ private void cancelRequest() throws XMPPException { final String errorMessage = "Could not establish socket with any provided host"; final XMPPError error = new XMPPError( XMPPError.Condition.item_not_found, errorMessage); final IQ errorIQ = IQ.createErrorResponse(bytestreamRequest, error); manager.getConnection().sendPacket(errorIQ); throw new XMPPException(errorMessage, error); } /** * Returns the response to the SOCKS5 Bytestream request containing the * SOCKS5 proxy used. * * @param selectedHost * the used SOCKS5 proxy * @return the response to the SOCKS5 Bytestream request */ private Bytestream createUsedHostResponse(StreamHost selectedHost) { final Bytestream response = new Bytestream( bytestreamRequest.getSessionID()); response.setTo(bytestreamRequest.getFrom()); response.setType(IQ.Type.RESULT); response.setPacketID(bytestreamRequest.getPacketID()); response.setUsedHost(selectedHost.getJID()); return response; } /** * Returns how often the connection to the given address failed. * * @param address * the address * @return number of connection failures */ private int getConnectionFailures(String address) { final Integer count = ADDRESS_BLACKLIST.get(address); return count != null ? count : 0; } /** * Returns the sender of the SOCKS5 Bytestream initialization request. * * @return the sender of the SOCKS5 Bytestream initialization request. */ @Override public String getFrom() { return bytestreamRequest.getFrom(); } /** * Returns the timeout to connect to one SOCKS5 proxy while accepting the * SOCKS5 Bytestream request. Default is 2000ms. * * @return the timeout to connect to one SOCKS5 proxy */ public int getMinimumConnectTimeout() { if (minimumConnectTimeout <= 0) { return 2000; } return minimumConnectTimeout; } /** * Returns the session ID of the SOCKS5 Bytestream initialization request. * * @return the session ID of the SOCKS5 Bytestream initialization request. */ @Override public String getSessionID() { return bytestreamRequest.getSessionID(); } /** * Returns the maximum timeout to connect to SOCKS5 proxies. Default is * 10000ms. * <p> * When accepting a SOCKS5 Bytestream request Smack tries to connect to all * SOCKS5 proxies given by the initiator until a connection is established. * This timeout divided by the number of SOCKS5 proxies determines the * timeout for every connection attempt. * <p> * You can set the minimum timeout for establishing a connection to one * SOCKS5 proxy by invoking {@link #setMinimumConnectTimeout(int)}. * * @return the maximum timeout to connect to SOCKS5 proxies */ public int getTotalConnectTimeout() { if (totalConnectTimeout <= 0) { return 10000; } return totalConnectTimeout; } /** * Increments the connection failure counter by one for the given address. * * @param address * the address the connection failure counter should be increased */ private void incrementConnectionFailures(String address) { final Integer count = ADDRESS_BLACKLIST.get(address); ADDRESS_BLACKLIST.put(address, count == null ? 1 : count + 1); } /** * Rejects the SOCKS5 Bytestream request by sending a reject error to the * initiator. */ @Override public void reject() { manager.replyRejectPacket(bytestreamRequest); } /** * Sets the timeout to connect to one SOCKS5 proxy while accepting the * SOCKS5 Bytestream request. Default is 2000ms. * * @param minimumConnectTimeout * the timeout to connect to one SOCKS5 proxy */ public void setMinimumConnectTimeout(int minimumConnectTimeout) { this.minimumConnectTimeout = minimumConnectTimeout; } /** * Sets the maximum timeout to connect to SOCKS5 proxies. Default is * 10000ms. * <p> * When accepting a SOCKS5 Bytestream request Smack tries to connect to all * SOCKS5 proxies given by the initiator until a connection is established. * This timeout divided by the number of SOCKS5 proxies determines the * timeout for every connection attempt. * <p> * You can set the minimum timeout for establishing a connection to one * SOCKS5 proxy by invoking {@link #setMinimumConnectTimeout(int)}. * * @param totalConnectTimeout * the maximum timeout to connect to SOCKS5 proxies */ public void setTotalConnectTimeout(int totalConnectTimeout) { this.totalConnectTimeout = totalConnectTimeout; } }