Shuyi Chen | d7955ce | 2013-05-22 14:51:55 -0700 | [diff] [blame] | 1 | /**
|
| 2 | * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
|
| 3 | * you may not use this file except in compliance with the License.
|
| 4 | * You may obtain a copy of the License at
|
| 5 | *
|
| 6 | * http://www.apache.org/licenses/LICENSE-2.0
|
| 7 | *
|
| 8 | * Unless required by applicable law or agreed to in writing, software
|
| 9 | * distributed under the License is distributed on an "AS IS" BASIS,
|
| 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 11 | * See the License for the specific language governing permissions and
|
| 12 | * limitations under the License.
|
| 13 | */
|
| 14 | package org.jivesoftware.smackx.bytestreams.socks5;
|
| 15 |
|
| 16 | import java.io.IOException;
|
| 17 | import java.lang.ref.WeakReference;
|
| 18 | import java.net.Socket;
|
| 19 | import java.util.ArrayList;
|
| 20 | import java.util.Collections;
|
| 21 | import java.util.Iterator;
|
| 22 | import java.util.LinkedList;
|
| 23 | import java.util.List;
|
| 24 | import java.util.Map;
|
| 25 | import java.util.Random;
|
| 26 | import java.util.WeakHashMap;
|
| 27 | import java.util.concurrent.ConcurrentHashMap;
|
| 28 | import java.util.concurrent.TimeoutException;
|
| 29 |
|
| 30 | import org.jivesoftware.smack.AbstractConnectionListener;
|
| 31 | import org.jivesoftware.smack.Connection;
|
| 32 | import org.jivesoftware.smack.ConnectionCreationListener;
|
| 33 | import org.jivesoftware.smack.XMPPException;
|
| 34 | import org.jivesoftware.smack.packet.IQ;
|
| 35 | import org.jivesoftware.smack.packet.Packet;
|
| 36 | import org.jivesoftware.smack.packet.XMPPError;
|
| 37 | import org.jivesoftware.smack.util.SyncPacketSend;
|
| 38 | import org.jivesoftware.smackx.ServiceDiscoveryManager;
|
| 39 | import org.jivesoftware.smackx.bytestreams.BytestreamListener;
|
| 40 | import org.jivesoftware.smackx.bytestreams.BytestreamManager;
|
| 41 | import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
|
| 42 | import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;
|
| 43 | import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHostUsed;
|
| 44 | import org.jivesoftware.smackx.filetransfer.FileTransferManager;
|
| 45 | import org.jivesoftware.smackx.packet.DiscoverInfo;
|
| 46 | import org.jivesoftware.smackx.packet.DiscoverItems;
|
| 47 | import org.jivesoftware.smackx.packet.DiscoverInfo.Identity;
|
| 48 | import org.jivesoftware.smackx.packet.DiscoverItems.Item;
|
| 49 |
|
| 50 | /**
|
| 51 | * The Socks5BytestreamManager class handles establishing SOCKS5 Bytestreams as specified in the <a
|
| 52 | * href="http://xmpp.org/extensions/xep-0065.html">XEP-0065</a>.
|
| 53 | * <p>
|
| 54 | * A SOCKS5 Bytestream is negotiated partly over the XMPP XML stream and partly over a separate
|
| 55 | * socket. The actual transfer though takes place over a separately created socket.
|
| 56 | * <p>
|
| 57 | * A SOCKS5 Bytestream generally has three parties, the initiator, the target, and the stream host.
|
| 58 | * The stream host is a specialized SOCKS5 proxy setup on a server, or, the initiator can act as the
|
| 59 | * stream host.
|
| 60 | * <p>
|
| 61 | * To establish a SOCKS5 Bytestream invoke the {@link #establishSession(String)} method. This will
|
| 62 | * negotiate a SOCKS5 Bytestream with the given target JID and return a socket.
|
| 63 | * <p>
|
| 64 | * If a session ID for the SOCKS5 Bytestream was already negotiated (e.g. while negotiating a file
|
| 65 | * transfer) invoke {@link #establishSession(String, String)}.
|
| 66 | * <p>
|
| 67 | * To handle incoming SOCKS5 Bytestream requests add an {@link Socks5BytestreamListener} to the
|
| 68 | * manager. There are two ways to add this listener. If you want to be informed about incoming
|
| 69 | * SOCKS5 Bytestreams from a specific user add the listener by invoking
|
| 70 | * {@link #addIncomingBytestreamListener(BytestreamListener, String)}. If the listener should
|
| 71 | * respond to all SOCKS5 Bytestream requests invoke
|
| 72 | * {@link #addIncomingBytestreamListener(BytestreamListener)}.
|
| 73 | * <p>
|
| 74 | * Note that the registered {@link Socks5BytestreamListener} will NOT be notified on incoming Socks5
|
| 75 | * bytestream requests sent in the context of <a
|
| 76 | * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
|
| 77 | * {@link FileTransferManager})
|
| 78 | * <p>
|
| 79 | * If no {@link Socks5BytestreamListener}s are registered, all incoming SOCKS5 Bytestream requests
|
| 80 | * will be rejected by returning a <not-acceptable/> error to the initiator.
|
| 81 | *
|
| 82 | * @author Henning Staib
|
| 83 | */
|
| 84 | public final class Socks5BytestreamManager implements BytestreamManager {
|
| 85 |
|
| 86 | /*
|
| 87 | * create a new Socks5BytestreamManager and register a shutdown listener on every established
|
| 88 | * connection
|
| 89 | */
|
| 90 | static {
|
| 91 | Connection.addConnectionCreationListener(new ConnectionCreationListener() {
|
| 92 |
|
| 93 | public void connectionCreated(final Connection connection) {
|
| 94 | final Socks5BytestreamManager manager;
|
| 95 | manager = Socks5BytestreamManager.getBytestreamManager(connection);
|
| 96 |
|
| 97 | // register shutdown listener
|
| 98 | connection.addConnectionListener(new AbstractConnectionListener() {
|
| 99 |
|
| 100 | public void connectionClosed() {
|
| 101 | manager.disableService();
|
| 102 | }
|
| 103 |
|
| 104 | public void connectionClosedOnError(Exception e) {
|
| 105 | manager.disableService();
|
| 106 | }
|
| 107 |
|
| 108 | public void reconnectionSuccessful() {
|
| 109 | managers.put(connection, manager);
|
| 110 | }
|
| 111 |
|
| 112 | });
|
| 113 | }
|
| 114 |
|
| 115 | });
|
| 116 | }
|
| 117 |
|
| 118 | /**
|
| 119 | * The XMPP namespace of the SOCKS5 Bytestream
|
| 120 | */
|
| 121 | public static final String NAMESPACE = "http://jabber.org/protocol/bytestreams";
|
| 122 |
|
| 123 | /* prefix used to generate session IDs */
|
| 124 | private static final String SESSION_ID_PREFIX = "js5_";
|
| 125 |
|
| 126 | /* random generator to create session IDs */
|
| 127 | private final static Random randomGenerator = new Random();
|
| 128 |
|
| 129 | /* stores one Socks5BytestreamManager for each XMPP connection */
|
| 130 | private final static Map<Connection, Socks5BytestreamManager> managers = new WeakHashMap<Connection, Socks5BytestreamManager>();
|
| 131 |
|
| 132 | /* XMPP connection */
|
| 133 | private final Connection connection;
|
| 134 |
|
| 135 | /*
|
| 136 | * assigns a user to a listener that is informed if a bytestream request for this user is
|
| 137 | * received
|
| 138 | */
|
| 139 | private final Map<String, BytestreamListener> userListeners = new ConcurrentHashMap<String, BytestreamListener>();
|
| 140 |
|
| 141 | /*
|
| 142 | * list of listeners that respond to all bytestream requests if there are not user specific
|
| 143 | * listeners for that request
|
| 144 | */
|
| 145 | private final List<BytestreamListener> allRequestListeners = Collections.synchronizedList(new LinkedList<BytestreamListener>());
|
| 146 |
|
| 147 | /* listener that handles all incoming bytestream requests */
|
| 148 | private final InitiationListener initiationListener;
|
| 149 |
|
| 150 | /* timeout to wait for the response to the SOCKS5 Bytestream initialization request */
|
| 151 | private int targetResponseTimeout = 10000;
|
| 152 |
|
| 153 | /* timeout for connecting to the SOCKS5 proxy selected by the target */
|
| 154 | private int proxyConnectionTimeout = 10000;
|
| 155 |
|
| 156 | /* blacklist of errornous SOCKS5 proxies */
|
| 157 | private final List<String> proxyBlacklist = Collections.synchronizedList(new LinkedList<String>());
|
| 158 |
|
| 159 | /* remember the last proxy that worked to prioritize it */
|
| 160 | private String lastWorkingProxy = null;
|
| 161 |
|
| 162 | /* flag to enable/disable prioritization of last working proxy */
|
| 163 | private boolean proxyPrioritizationEnabled = true;
|
| 164 |
|
| 165 | /*
|
| 166 | * list containing session IDs of SOCKS5 Bytestream initialization packets that should be
|
| 167 | * ignored by the InitiationListener
|
| 168 | */
|
| 169 | private List<String> ignoredBytestreamRequests = Collections.synchronizedList(new LinkedList<String>());
|
| 170 |
|
| 171 | /**
|
| 172 | * Returns the Socks5BytestreamManager to handle SOCKS5 Bytestreams for a given
|
| 173 | * {@link Connection}.
|
| 174 | * <p>
|
| 175 | * If no manager exists a new is created and initialized.
|
| 176 | *
|
| 177 | * @param connection the XMPP connection or <code>null</code> if given connection is
|
| 178 | * <code>null</code>
|
| 179 | * @return the Socks5BytestreamManager for the given XMPP connection
|
| 180 | */
|
| 181 | public static synchronized Socks5BytestreamManager getBytestreamManager(Connection connection) {
|
| 182 | if (connection == null) {
|
| 183 | return null;
|
| 184 | }
|
| 185 | Socks5BytestreamManager manager = managers.get(connection);
|
| 186 | if (manager == null) {
|
| 187 | manager = new Socks5BytestreamManager(connection);
|
| 188 | managers.put(connection, manager);
|
| 189 | manager.activate();
|
| 190 | }
|
| 191 | return manager;
|
| 192 | }
|
| 193 |
|
| 194 | /**
|
| 195 | * Private constructor.
|
| 196 | *
|
| 197 | * @param connection the XMPP connection
|
| 198 | */
|
| 199 | private Socks5BytestreamManager(Connection connection) {
|
| 200 | this.connection = connection;
|
| 201 | this.initiationListener = new InitiationListener(this);
|
| 202 | }
|
| 203 |
|
| 204 | /**
|
| 205 | * Adds BytestreamListener that is called for every incoming SOCKS5 Bytestream request unless
|
| 206 | * there is a user specific BytestreamListener registered.
|
| 207 | * <p>
|
| 208 | * If no listeners are registered all SOCKS5 Bytestream request are rejected with a
|
| 209 | * <not-acceptable/> error.
|
| 210 | * <p>
|
| 211 | * Note that the registered {@link BytestreamListener} will NOT be notified on incoming Socks5
|
| 212 | * bytestream requests sent in the context of <a
|
| 213 | * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
|
| 214 | * {@link FileTransferManager})
|
| 215 | *
|
| 216 | * @param listener the listener to register
|
| 217 | */
|
| 218 | public void addIncomingBytestreamListener(BytestreamListener listener) {
|
| 219 | this.allRequestListeners.add(listener);
|
| 220 | }
|
| 221 |
|
| 222 | /**
|
| 223 | * Removes the given listener from the list of listeners for all incoming SOCKS5 Bytestream
|
| 224 | * requests.
|
| 225 | *
|
| 226 | * @param listener the listener to remove
|
| 227 | */
|
| 228 | public void removeIncomingBytestreamListener(BytestreamListener listener) {
|
| 229 | this.allRequestListeners.remove(listener);
|
| 230 | }
|
| 231 |
|
| 232 | /**
|
| 233 | * Adds BytestreamListener that is called for every incoming SOCKS5 Bytestream request from the
|
| 234 | * given user.
|
| 235 | * <p>
|
| 236 | * Use this method if you are awaiting an incoming SOCKS5 Bytestream request from a specific
|
| 237 | * user.
|
| 238 | * <p>
|
| 239 | * If no listeners are registered all SOCKS5 Bytestream request are rejected with a
|
| 240 | * <not-acceptable/> error.
|
| 241 | * <p>
|
| 242 | * Note that the registered {@link BytestreamListener} will NOT be notified on incoming Socks5
|
| 243 | * bytestream requests sent in the context of <a
|
| 244 | * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
|
| 245 | * {@link FileTransferManager})
|
| 246 | *
|
| 247 | * @param listener the listener to register
|
| 248 | * @param initiatorJID the JID of the user that wants to establish a SOCKS5 Bytestream
|
| 249 | */
|
| 250 | public void addIncomingBytestreamListener(BytestreamListener listener, String initiatorJID) {
|
| 251 | this.userListeners.put(initiatorJID, listener);
|
| 252 | }
|
| 253 |
|
| 254 | /**
|
| 255 | * Removes the listener for the given user.
|
| 256 | *
|
| 257 | * @param initiatorJID the JID of the user the listener should be removed
|
| 258 | */
|
| 259 | public void removeIncomingBytestreamListener(String initiatorJID) {
|
| 260 | this.userListeners.remove(initiatorJID);
|
| 261 | }
|
| 262 |
|
| 263 | /**
|
| 264 | * Use this method to ignore the next incoming SOCKS5 Bytestream request containing the given
|
| 265 | * session ID. No listeners will be notified for this request and and no error will be returned
|
| 266 | * to the initiator.
|
| 267 | * <p>
|
| 268 | * This method should be used if you are awaiting a SOCKS5 Bytestream request as a reply to
|
| 269 | * another packet (e.g. file transfer).
|
| 270 | *
|
| 271 | * @param sessionID to be ignored
|
| 272 | */
|
| 273 | public void ignoreBytestreamRequestOnce(String sessionID) {
|
| 274 | this.ignoredBytestreamRequests.add(sessionID);
|
| 275 | }
|
| 276 |
|
| 277 | /**
|
| 278 | * Disables the SOCKS5 Bytestream manager by removing the SOCKS5 Bytestream feature from the
|
| 279 | * service discovery, disabling the listener for SOCKS5 Bytestream initiation requests and
|
| 280 | * resetting its internal state.
|
| 281 | * <p>
|
| 282 | * To re-enable the SOCKS5 Bytestream feature invoke {@link #getBytestreamManager(Connection)}.
|
| 283 | * Using the file transfer API will automatically re-enable the SOCKS5 Bytestream feature.
|
| 284 | */
|
| 285 | public synchronized void disableService() {
|
| 286 |
|
| 287 | // remove initiation packet listener
|
| 288 | this.connection.removePacketListener(this.initiationListener);
|
| 289 |
|
| 290 | // shutdown threads
|
| 291 | this.initiationListener.shutdown();
|
| 292 |
|
| 293 | // clear listeners
|
| 294 | this.allRequestListeners.clear();
|
| 295 | this.userListeners.clear();
|
| 296 |
|
| 297 | // reset internal state
|
| 298 | this.lastWorkingProxy = null;
|
| 299 | this.proxyBlacklist.clear();
|
| 300 | this.ignoredBytestreamRequests.clear();
|
| 301 |
|
| 302 | // remove manager from static managers map
|
| 303 | managers.remove(this.connection);
|
| 304 |
|
| 305 | // shutdown local SOCKS5 proxy if there are no more managers for other connections
|
| 306 | if (managers.size() == 0) {
|
| 307 | Socks5Proxy.getSocks5Proxy().stop();
|
| 308 | }
|
| 309 |
|
| 310 | // remove feature from service discovery
|
| 311 | ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(this.connection);
|
| 312 |
|
| 313 | // check if service discovery is not already disposed by connection shutdown
|
| 314 | if (serviceDiscoveryManager != null) {
|
| 315 | serviceDiscoveryManager.removeFeature(NAMESPACE);
|
| 316 | }
|
| 317 |
|
| 318 | }
|
| 319 |
|
| 320 | /**
|
| 321 | * Returns the timeout to wait for the response to the SOCKS5 Bytestream initialization request.
|
| 322 | * Default is 10000ms.
|
| 323 | *
|
| 324 | * @return the timeout to wait for the response to the SOCKS5 Bytestream initialization request
|
| 325 | */
|
| 326 | public int getTargetResponseTimeout() {
|
| 327 | if (this.targetResponseTimeout <= 0) {
|
| 328 | this.targetResponseTimeout = 10000;
|
| 329 | }
|
| 330 | return targetResponseTimeout;
|
| 331 | }
|
| 332 |
|
| 333 | /**
|
| 334 | * Sets the timeout to wait for the response to the SOCKS5 Bytestream initialization request.
|
| 335 | * Default is 10000ms.
|
| 336 | *
|
| 337 | * @param targetResponseTimeout the timeout to set
|
| 338 | */
|
| 339 | public void setTargetResponseTimeout(int targetResponseTimeout) {
|
| 340 | this.targetResponseTimeout = targetResponseTimeout;
|
| 341 | }
|
| 342 |
|
| 343 | /**
|
| 344 | * Returns the timeout for connecting to the SOCKS5 proxy selected by the target. Default is
|
| 345 | * 10000ms.
|
| 346 | *
|
| 347 | * @return the timeout for connecting to the SOCKS5 proxy selected by the target
|
| 348 | */
|
| 349 | public int getProxyConnectionTimeout() {
|
| 350 | if (this.proxyConnectionTimeout <= 0) {
|
| 351 | this.proxyConnectionTimeout = 10000;
|
| 352 | }
|
| 353 | return proxyConnectionTimeout;
|
| 354 | }
|
| 355 |
|
| 356 | /**
|
| 357 | * Sets the timeout for connecting to the SOCKS5 proxy selected by the target. Default is
|
| 358 | * 10000ms.
|
| 359 | *
|
| 360 | * @param proxyConnectionTimeout the timeout to set
|
| 361 | */
|
| 362 | public void setProxyConnectionTimeout(int proxyConnectionTimeout) {
|
| 363 | this.proxyConnectionTimeout = proxyConnectionTimeout;
|
| 364 | }
|
| 365 |
|
| 366 | /**
|
| 367 | * Returns if the prioritization of the last working SOCKS5 proxy on successive SOCKS5
|
| 368 | * Bytestream connections is enabled. Default is <code>true</code>.
|
| 369 | *
|
| 370 | * @return <code>true</code> if prioritization is enabled, <code>false</code> otherwise
|
| 371 | */
|
| 372 | public boolean isProxyPrioritizationEnabled() {
|
| 373 | return proxyPrioritizationEnabled;
|
| 374 | }
|
| 375 |
|
| 376 | /**
|
| 377 | * Enable/disable the prioritization of the last working SOCKS5 proxy on successive SOCKS5
|
| 378 | * Bytestream connections.
|
| 379 | *
|
| 380 | * @param proxyPrioritizationEnabled enable/disable the prioritization of the last working
|
| 381 | * SOCKS5 proxy
|
| 382 | */
|
| 383 | public void setProxyPrioritizationEnabled(boolean proxyPrioritizationEnabled) {
|
| 384 | this.proxyPrioritizationEnabled = proxyPrioritizationEnabled;
|
| 385 | }
|
| 386 |
|
| 387 | /**
|
| 388 | * Establishes a SOCKS5 Bytestream with the given user and returns the Socket to send/receive
|
| 389 | * data to/from the user.
|
| 390 | * <p>
|
| 391 | * Use this method to establish SOCKS5 Bytestreams to users accepting all incoming Socks5
|
| 392 | * bytestream requests since this method doesn't provide a way to tell the user something about
|
| 393 | * the data to be sent.
|
| 394 | * <p>
|
| 395 | * To establish a SOCKS5 Bytestream after negotiation the kind of data to be sent (e.g. file
|
| 396 | * transfer) use {@link #establishSession(String, String)}.
|
| 397 | *
|
| 398 | * @param targetJID the JID of the user a SOCKS5 Bytestream should be established
|
| 399 | * @return the Socket to send/receive data to/from the user
|
| 400 | * @throws XMPPException if the user doesn't support or accept SOCKS5 Bytestreams, if no Socks5
|
| 401 | * Proxy could be found, if the user couldn't connect to any of the SOCKS5 Proxies
|
| 402 | * @throws IOException if the bytestream could not be established
|
| 403 | * @throws InterruptedException if the current thread was interrupted while waiting
|
| 404 | */
|
| 405 | public Socks5BytestreamSession establishSession(String targetJID) throws XMPPException,
|
| 406 | IOException, InterruptedException {
|
| 407 | String sessionID = getNextSessionID();
|
| 408 | return establishSession(targetJID, sessionID);
|
| 409 | }
|
| 410 |
|
| 411 | /**
|
| 412 | * Establishes a SOCKS5 Bytestream with the given user using the given session ID and returns
|
| 413 | * the Socket to send/receive data to/from the user.
|
| 414 | *
|
| 415 | * @param targetJID the JID of the user a SOCKS5 Bytestream should be established
|
| 416 | * @param sessionID the session ID for the SOCKS5 Bytestream request
|
| 417 | * @return the Socket to send/receive data to/from the user
|
| 418 | * @throws XMPPException if the user doesn't support or accept SOCKS5 Bytestreams, if no Socks5
|
| 419 | * Proxy could be found, if the user couldn't connect to any of the SOCKS5 Proxies
|
| 420 | * @throws IOException if the bytestream could not be established
|
| 421 | * @throws InterruptedException if the current thread was interrupted while waiting
|
| 422 | */
|
| 423 | public Socks5BytestreamSession establishSession(String targetJID, String sessionID)
|
| 424 | throws XMPPException, IOException, InterruptedException {
|
| 425 |
|
| 426 | XMPPException discoveryException = null;
|
| 427 | // check if target supports SOCKS5 Bytestream
|
| 428 | if (!supportsSocks5(targetJID)) {
|
| 429 | throw new XMPPException(targetJID + " doesn't support SOCKS5 Bytestream");
|
| 430 | }
|
| 431 |
|
| 432 | List<String> proxies = new ArrayList<String>();
|
| 433 | // determine SOCKS5 proxies from XMPP-server
|
| 434 | try {
|
| 435 | proxies.addAll(determineProxies());
|
| 436 | } catch (XMPPException e) {
|
| 437 | // don't abort here, just remember the exception thrown by determineProxies()
|
| 438 | // determineStreamHostInfos() will at least add the local Socks5 proxy (if enabled)
|
| 439 | discoveryException = e;
|
| 440 | }
|
| 441 |
|
| 442 | // determine address and port of each proxy
|
| 443 | List<StreamHost> streamHosts = determineStreamHostInfos(proxies);
|
| 444 |
|
| 445 | if (streamHosts.isEmpty()) {
|
| 446 | throw discoveryException != null ? discoveryException : new XMPPException("no SOCKS5 proxies available");
|
| 447 | }
|
| 448 |
|
| 449 | // compute digest
|
| 450 | String digest = Socks5Utils.createDigest(sessionID, this.connection.getUser(), targetJID);
|
| 451 |
|
| 452 | // prioritize last working SOCKS5 proxy if exists
|
| 453 | if (this.proxyPrioritizationEnabled && this.lastWorkingProxy != null) {
|
| 454 | StreamHost selectedStreamHost = null;
|
| 455 | for (StreamHost streamHost : streamHosts) {
|
| 456 | if (streamHost.getJID().equals(this.lastWorkingProxy)) {
|
| 457 | selectedStreamHost = streamHost;
|
| 458 | break;
|
| 459 | }
|
| 460 | }
|
| 461 | if (selectedStreamHost != null) {
|
| 462 | streamHosts.remove(selectedStreamHost);
|
| 463 | streamHosts.add(0, selectedStreamHost);
|
| 464 | }
|
| 465 |
|
| 466 | }
|
| 467 |
|
| 468 | Socks5Proxy socks5Proxy = Socks5Proxy.getSocks5Proxy();
|
| 469 | try {
|
| 470 |
|
| 471 | // add transfer digest to local proxy to make transfer valid
|
| 472 | socks5Proxy.addTransfer(digest);
|
| 473 |
|
| 474 | // create initiation packet
|
| 475 | Bytestream initiation = createBytestreamInitiation(sessionID, targetJID, streamHosts);
|
| 476 |
|
| 477 | // send initiation packet
|
| 478 | Packet response = SyncPacketSend.getReply(this.connection, initiation,
|
| 479 | getTargetResponseTimeout());
|
| 480 |
|
| 481 | // extract used stream host from response
|
| 482 | StreamHostUsed streamHostUsed = ((Bytestream) response).getUsedHost();
|
| 483 | StreamHost usedStreamHost = initiation.getStreamHost(streamHostUsed.getJID());
|
| 484 |
|
| 485 | if (usedStreamHost == null) {
|
| 486 | throw new XMPPException("Remote user responded with unknown host");
|
| 487 | }
|
| 488 |
|
| 489 | // build SOCKS5 client
|
| 490 | Socks5Client socks5Client = new Socks5ClientForInitiator(usedStreamHost, digest,
|
| 491 | this.connection, sessionID, targetJID);
|
| 492 |
|
| 493 | // establish connection to proxy
|
| 494 | Socket socket = socks5Client.getSocket(getProxyConnectionTimeout());
|
| 495 |
|
| 496 | // remember last working SOCKS5 proxy to prioritize it for next request
|
| 497 | this.lastWorkingProxy = usedStreamHost.getJID();
|
| 498 |
|
| 499 | // negotiation successful, return the output stream
|
| 500 | return new Socks5BytestreamSession(socket, usedStreamHost.getJID().equals(
|
| 501 | this.connection.getUser()));
|
| 502 |
|
| 503 | }
|
| 504 | catch (TimeoutException e) {
|
| 505 | throw new IOException("Timeout while connecting to SOCKS5 proxy");
|
| 506 | }
|
| 507 | finally {
|
| 508 |
|
| 509 | // remove transfer digest if output stream is returned or an exception
|
| 510 | // occurred
|
| 511 | socks5Proxy.removeTransfer(digest);
|
| 512 |
|
| 513 | }
|
| 514 | }
|
| 515 |
|
| 516 | /**
|
| 517 | * Returns <code>true</code> if the given target JID supports feature SOCKS5 Bytestream.
|
| 518 | *
|
| 519 | * @param targetJID the target JID
|
| 520 | * @return <code>true</code> if the given target JID supports feature SOCKS5 Bytestream
|
| 521 | * otherwise <code>false</code>
|
| 522 | * @throws XMPPException if there was an error querying target for supported features
|
| 523 | */
|
| 524 | private boolean supportsSocks5(String targetJID) throws XMPPException {
|
| 525 | ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(this.connection);
|
| 526 | DiscoverInfo discoverInfo = serviceDiscoveryManager.discoverInfo(targetJID);
|
| 527 | return discoverInfo.containsFeature(NAMESPACE);
|
| 528 | }
|
| 529 |
|
| 530 | /**
|
| 531 | * Returns a list of JIDs of SOCKS5 proxies by querying the XMPP server. The SOCKS5 proxies are
|
| 532 | * in the same order as returned by the XMPP server.
|
| 533 | *
|
| 534 | * @return list of JIDs of SOCKS5 proxies
|
| 535 | * @throws XMPPException if there was an error querying the XMPP server for SOCKS5 proxies
|
| 536 | */
|
| 537 | private List<String> determineProxies() throws XMPPException {
|
| 538 | ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(this.connection);
|
| 539 |
|
| 540 | List<String> proxies = new ArrayList<String>();
|
| 541 |
|
| 542 | // get all items form XMPP server
|
| 543 | DiscoverItems discoverItems = serviceDiscoveryManager.discoverItems(this.connection.getServiceName());
|
| 544 | Iterator<Item> itemIterator = discoverItems.getItems();
|
| 545 |
|
| 546 | // query all items if they are SOCKS5 proxies
|
| 547 | while (itemIterator.hasNext()) {
|
| 548 | Item item = itemIterator.next();
|
| 549 |
|
| 550 | // skip blacklisted servers
|
| 551 | if (this.proxyBlacklist.contains(item.getEntityID())) {
|
| 552 | continue;
|
| 553 | }
|
| 554 |
|
| 555 | try {
|
| 556 | DiscoverInfo proxyInfo;
|
| 557 | proxyInfo = serviceDiscoveryManager.discoverInfo(item.getEntityID());
|
| 558 | Iterator<Identity> identities = proxyInfo.getIdentities();
|
| 559 |
|
| 560 | // item must have category "proxy" and type "bytestream"
|
| 561 | while (identities.hasNext()) {
|
| 562 | Identity identity = identities.next();
|
| 563 |
|
| 564 | if ("proxy".equalsIgnoreCase(identity.getCategory())
|
| 565 | && "bytestreams".equalsIgnoreCase(identity.getType())) {
|
| 566 | proxies.add(item.getEntityID());
|
| 567 | break;
|
| 568 | }
|
| 569 |
|
| 570 | /*
|
| 571 | * server is not a SOCKS5 proxy, blacklist server to skip next time a Socks5
|
| 572 | * bytestream should be established
|
| 573 | */
|
| 574 | this.proxyBlacklist.add(item.getEntityID());
|
| 575 |
|
| 576 | }
|
| 577 | }
|
| 578 | catch (XMPPException e) {
|
| 579 | // blacklist errornous server
|
| 580 | this.proxyBlacklist.add(item.getEntityID());
|
| 581 | }
|
| 582 | }
|
| 583 |
|
| 584 | return proxies;
|
| 585 | }
|
| 586 |
|
| 587 | /**
|
| 588 | * Returns a list of stream hosts containing the IP address an the port for the given list of
|
| 589 | * SOCKS5 proxy JIDs. The order of the returned list is the same as the given list of JIDs
|
| 590 | * excluding all SOCKS5 proxies who's network settings could not be determined. If a local
|
| 591 | * SOCKS5 proxy is running it will be the first item in the list returned.
|
| 592 | *
|
| 593 | * @param proxies a list of SOCKS5 proxy JIDs
|
| 594 | * @return a list of stream hosts containing the IP address an the port
|
| 595 | */
|
| 596 | private List<StreamHost> determineStreamHostInfos(List<String> proxies) {
|
| 597 | List<StreamHost> streamHosts = new ArrayList<StreamHost>();
|
| 598 |
|
| 599 | // add local proxy on first position if exists
|
| 600 | List<StreamHost> localProxies = getLocalStreamHost();
|
| 601 | if (localProxies != null) {
|
| 602 | streamHosts.addAll(localProxies);
|
| 603 | }
|
| 604 |
|
| 605 | // query SOCKS5 proxies for network settings
|
| 606 | for (String proxy : proxies) {
|
| 607 | Bytestream streamHostRequest = createStreamHostRequest(proxy);
|
| 608 | try {
|
| 609 | Bytestream response = (Bytestream) SyncPacketSend.getReply(this.connection,
|
| 610 | streamHostRequest);
|
| 611 | streamHosts.addAll(response.getStreamHosts());
|
| 612 | }
|
| 613 | catch (XMPPException e) {
|
| 614 | // blacklist errornous proxies
|
| 615 | this.proxyBlacklist.add(proxy);
|
| 616 | }
|
| 617 | }
|
| 618 |
|
| 619 | return streamHosts;
|
| 620 | }
|
| 621 |
|
| 622 | /**
|
| 623 | * Returns a IQ packet to query a SOCKS5 proxy its network settings.
|
| 624 | *
|
| 625 | * @param proxy the proxy to query
|
| 626 | * @return IQ packet to query a SOCKS5 proxy its network settings
|
| 627 | */
|
| 628 | private Bytestream createStreamHostRequest(String proxy) {
|
| 629 | Bytestream request = new Bytestream();
|
| 630 | request.setType(IQ.Type.GET);
|
| 631 | request.setTo(proxy);
|
| 632 | return request;
|
| 633 | }
|
| 634 |
|
| 635 | /**
|
| 636 | * Returns the stream host information of the local SOCKS5 proxy containing the IP address and
|
| 637 | * the port or null if local SOCKS5 proxy is not running.
|
| 638 | *
|
| 639 | * @return the stream host information of the local SOCKS5 proxy or null if local SOCKS5 proxy
|
| 640 | * is not running
|
| 641 | */
|
| 642 | private List<StreamHost> getLocalStreamHost() {
|
| 643 |
|
| 644 | // get local proxy singleton
|
| 645 | Socks5Proxy socks5Server = Socks5Proxy.getSocks5Proxy();
|
| 646 |
|
| 647 | if (socks5Server.isRunning()) {
|
| 648 | List<String> addresses = socks5Server.getLocalAddresses();
|
| 649 | int port = socks5Server.getPort();
|
| 650 |
|
| 651 | if (addresses.size() >= 1) {
|
| 652 | List<StreamHost> streamHosts = new ArrayList<StreamHost>();
|
| 653 | for (String address : addresses) {
|
| 654 | StreamHost streamHost = new StreamHost(this.connection.getUser(), address);
|
| 655 | streamHost.setPort(port);
|
| 656 | streamHosts.add(streamHost);
|
| 657 | }
|
| 658 | return streamHosts;
|
| 659 | }
|
| 660 |
|
| 661 | }
|
| 662 |
|
| 663 | // server is not running or local address could not be determined
|
| 664 | return null;
|
| 665 | }
|
| 666 |
|
| 667 | /**
|
| 668 | * Returns a SOCKS5 Bytestream initialization request packet with the given session ID
|
| 669 | * containing the given stream hosts for the given target JID.
|
| 670 | *
|
| 671 | * @param sessionID the session ID for the SOCKS5 Bytestream
|
| 672 | * @param targetJID the target JID of SOCKS5 Bytestream request
|
| 673 | * @param streamHosts a list of SOCKS5 proxies the target should connect to
|
| 674 | * @return a SOCKS5 Bytestream initialization request packet
|
| 675 | */
|
| 676 | private Bytestream createBytestreamInitiation(String sessionID, String targetJID,
|
| 677 | List<StreamHost> streamHosts) {
|
| 678 | Bytestream initiation = new Bytestream(sessionID);
|
| 679 |
|
| 680 | // add all stream hosts
|
| 681 | for (StreamHost streamHost : streamHosts) {
|
| 682 | initiation.addStreamHost(streamHost);
|
| 683 | }
|
| 684 |
|
| 685 | initiation.setType(IQ.Type.SET);
|
| 686 | initiation.setTo(targetJID);
|
| 687 |
|
| 688 | return initiation;
|
| 689 | }
|
| 690 |
|
| 691 | /**
|
| 692 | * Responses to the given packet's sender with a XMPP error that a SOCKS5 Bytestream is not
|
| 693 | * accepted.
|
| 694 | *
|
| 695 | * @param packet Packet that should be answered with a not-acceptable error
|
| 696 | */
|
| 697 | protected void replyRejectPacket(IQ packet) {
|
| 698 | XMPPError xmppError = new XMPPError(XMPPError.Condition.no_acceptable);
|
| 699 | IQ errorIQ = IQ.createErrorResponse(packet, xmppError);
|
| 700 | this.connection.sendPacket(errorIQ);
|
| 701 | }
|
| 702 |
|
| 703 | /**
|
| 704 | * Activates the Socks5BytestreamManager by registering the SOCKS5 Bytestream initialization
|
| 705 | * listener and enabling the SOCKS5 Bytestream feature.
|
| 706 | */
|
| 707 | private void activate() {
|
| 708 | // register bytestream initiation packet listener
|
| 709 | this.connection.addPacketListener(this.initiationListener,
|
| 710 | this.initiationListener.getFilter());
|
| 711 |
|
| 712 | // enable SOCKS5 feature
|
| 713 | enableService();
|
| 714 | }
|
| 715 |
|
| 716 | /**
|
| 717 | * Adds the SOCKS5 Bytestream feature to the service discovery.
|
| 718 | */
|
| 719 | private void enableService() {
|
| 720 | ServiceDiscoveryManager manager = ServiceDiscoveryManager.getInstanceFor(this.connection);
|
| 721 | if (!manager.includesFeature(NAMESPACE)) {
|
| 722 | manager.addFeature(NAMESPACE);
|
| 723 | }
|
| 724 | }
|
| 725 |
|
| 726 | /**
|
| 727 | * Returns a new unique session ID.
|
| 728 | *
|
| 729 | * @return a new unique session ID
|
| 730 | */
|
| 731 | private String getNextSessionID() {
|
| 732 | StringBuilder buffer = new StringBuilder();
|
| 733 | buffer.append(SESSION_ID_PREFIX);
|
| 734 | buffer.append(Math.abs(randomGenerator.nextLong()));
|
| 735 | return buffer.toString();
|
| 736 | }
|
| 737 |
|
| 738 | /**
|
| 739 | * Returns the XMPP connection.
|
| 740 | *
|
| 741 | * @return the XMPP connection
|
| 742 | */
|
| 743 | protected Connection getConnection() {
|
| 744 | return this.connection;
|
| 745 | }
|
| 746 |
|
| 747 | /**
|
| 748 | * Returns the {@link BytestreamListener} that should be informed if a SOCKS5 Bytestream request
|
| 749 | * from the given initiator JID is received.
|
| 750 | *
|
| 751 | * @param initiator the initiator's JID
|
| 752 | * @return the listener
|
| 753 | */
|
| 754 | protected BytestreamListener getUserListener(String initiator) {
|
| 755 | return this.userListeners.get(initiator);
|
| 756 | }
|
| 757 |
|
| 758 | /**
|
| 759 | * Returns a list of {@link BytestreamListener} that are informed if there are no listeners for
|
| 760 | * a specific initiator.
|
| 761 | *
|
| 762 | * @return list of listeners
|
| 763 | */
|
| 764 | protected List<BytestreamListener> getAllRequestListeners() {
|
| 765 | return this.allRequestListeners;
|
| 766 | }
|
| 767 |
|
| 768 | /**
|
| 769 | * Returns the list of session IDs that should be ignored by the InitialtionListener
|
| 770 | *
|
| 771 | * @return list of session IDs
|
| 772 | */
|
| 773 | protected List<String> getIgnoredBytestreamRequests() {
|
| 774 | return ignoredBytestreamRequests;
|
| 775 | }
|
| 776 |
|
| 777 | }
|