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.ibb; 018 019import java.util.Collections; 020import java.util.HashMap; 021import java.util.LinkedList; 022import java.util.List; 023import java.util.Map; 024import java.util.Random; 025import java.util.concurrent.ConcurrentHashMap; 026 027import org.jivesoftware.smack.AbstractConnectionClosedListener; 028import org.jivesoftware.smack.ConnectionCreationListener; 029import org.jivesoftware.smack.SmackException; 030import org.jivesoftware.smack.SmackException.NoResponseException; 031import org.jivesoftware.smack.SmackException.NotConnectedException; 032import org.jivesoftware.smack.XMPPConnection; 033import org.jivesoftware.smack.XMPPConnectionRegistry; 034import org.jivesoftware.smack.XMPPException; 035import org.jivesoftware.smack.XMPPException.XMPPErrorException; 036import org.jivesoftware.smack.packet.IQ; 037import org.jivesoftware.smack.packet.XMPPError; 038import org.jivesoftware.smackx.bytestreams.BytestreamListener; 039import org.jivesoftware.smackx.bytestreams.BytestreamManager; 040import org.jivesoftware.smackx.bytestreams.ibb.packet.Open; 041import org.jivesoftware.smackx.filetransfer.FileTransferManager; 042 043/** 044 * The InBandBytestreamManager class handles establishing In-Band Bytestreams as specified in the <a 045 * href="http://xmpp.org/extensions/xep-0047.html">XEP-0047</a>. 046 * <p> 047 * The In-Band Bytestreams (IBB) enables two entities to establish a virtual bytestream over which 048 * they can exchange Base64-encoded chunks of data over XMPP itself. It is the fall-back mechanism 049 * in case the Socks5 bytestream method of transferring data is not available. 050 * <p> 051 * There are two ways to send data over an In-Band Bytestream. It could either use IQ stanzas to 052 * send data packets or message stanzas. If IQ stanzas are used every data stanza(/packet) is acknowledged by 053 * the receiver. This is the recommended way to avoid possible rate-limiting penalties. Message 054 * stanzas are not acknowledged because most XMPP server implementation don't support stanza 055 * flow-control method like <a href="http://xmpp.org/extensions/xep-0079.html">Advanced Message 056 * Processing</a>. To set the stanza that should be used invoke {@link #setStanza(StanzaType)}. 057 * <p> 058 * To establish an In-Band Bytestream invoke the {@link #establishSession(String)} method. This will 059 * negotiate an in-band bytestream with the given target JID and return a session. 060 * <p> 061 * If a session ID for the In-Band Bytestream was already negotiated (e.g. while negotiating a file 062 * transfer) invoke {@link #establishSession(String, String)}. 063 * <p> 064 * To handle incoming In-Band Bytestream requests add an {@link InBandBytestreamListener} to the 065 * manager. There are two ways to add this listener. If you want to be informed about incoming 066 * In-Band Bytestreams from a specific user add the listener by invoking 067 * {@link #addIncomingBytestreamListener(BytestreamListener, String)}. If the listener should 068 * respond to all In-Band Bytestream requests invoke 069 * {@link #addIncomingBytestreamListener(BytestreamListener)}. 070 * <p> 071 * Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming 072 * In-Band bytestream requests sent in the context of <a 073 * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See 074 * {@link FileTransferManager}) 075 * <p> 076 * If no {@link InBandBytestreamListener}s are registered, all incoming In-Band bytestream requests 077 * will be rejected by returning a <not-acceptable/> error to the initiator. 078 * 079 * @author Henning Staib 080 */ 081public class InBandBytestreamManager implements BytestreamManager { 082 083 /** 084 * Stanzas that can be used to encapsulate In-Band Bytestream data packets. 085 */ 086 public enum StanzaType { 087 088 /** 089 * IQ stanza. 090 */ 091 IQ, 092 093 /** 094 * Message stanza. 095 */ 096 MESSAGE 097 } 098 099 /* 100 * create a new InBandBytestreamManager and register its shutdown listener on every established 101 * connection 102 */ 103 static { 104 XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() { 105 public void connectionCreated(final XMPPConnection connection) { 106 // create the manager for this connection 107 InBandBytestreamManager.getByteStreamManager(connection); 108 109 // register shutdown listener 110 connection.addConnectionListener(new AbstractConnectionClosedListener() { 111 112 @Override 113 public void connectionTerminated() { 114 InBandBytestreamManager.getByteStreamManager(connection).disableService(); 115 } 116 117 @Override 118 public void reconnectionSuccessful() { 119 // re-create the manager for this connection 120 InBandBytestreamManager.getByteStreamManager(connection); 121 } 122 123 }); 124 125 } 126 }); 127 } 128 129 /** 130 * Maximum block size that is allowed for In-Band Bytestreams 131 */ 132 public static final int MAXIMUM_BLOCK_SIZE = 65535; 133 134 /* prefix used to generate session IDs */ 135 private static final String SESSION_ID_PREFIX = "jibb_"; 136 137 /* random generator to create session IDs */ 138 private final static Random randomGenerator = new Random(); 139 140 /* stores one InBandBytestreamManager for each XMPP connection */ 141 private final static Map<XMPPConnection, InBandBytestreamManager> managers = new HashMap<XMPPConnection, InBandBytestreamManager>(); 142 143 /* XMPP connection */ 144 private final XMPPConnection connection; 145 146 /* 147 * assigns a user to a listener that is informed if an In-Band Bytestream request for this user 148 * is received 149 */ 150 private final Map<String, BytestreamListener> userListeners = new ConcurrentHashMap<String, BytestreamListener>(); 151 152 /* 153 * list of listeners that respond to all In-Band Bytestream requests if there are no user 154 * specific listeners for that request 155 */ 156 private final List<BytestreamListener> allRequestListeners = Collections.synchronizedList(new LinkedList<BytestreamListener>()); 157 158 /* listener that handles all incoming In-Band Bytestream requests */ 159 private final InitiationListener initiationListener; 160 161 /* listener that handles all incoming In-Band Bytestream IQ data packets */ 162 private final DataListener dataListener; 163 164 /* listener that handles all incoming In-Band Bytestream close requests */ 165 private final CloseListener closeListener; 166 167 /* assigns a session ID to the In-Band Bytestream session */ 168 private final Map<String, InBandBytestreamSession> sessions = new ConcurrentHashMap<String, InBandBytestreamSession>(); 169 170 /* block size used for new In-Band Bytestreams */ 171 private int defaultBlockSize = 4096; 172 173 /* maximum block size allowed for this connection */ 174 private int maximumBlockSize = MAXIMUM_BLOCK_SIZE; 175 176 /* the stanza used to send data packets */ 177 private StanzaType stanza = StanzaType.IQ; 178 179 /* 180 * list containing session IDs of In-Band Bytestream open packets that should be ignored by the 181 * InitiationListener 182 */ 183 private List<String> ignoredBytestreamRequests = Collections.synchronizedList(new LinkedList<String>()); 184 185 /** 186 * Returns the InBandBytestreamManager to handle In-Band Bytestreams for a given 187 * {@link XMPPConnection}. 188 * 189 * @param connection the XMPP connection 190 * @return the InBandBytestreamManager for the given XMPP connection 191 */ 192 public static synchronized InBandBytestreamManager getByteStreamManager(XMPPConnection connection) { 193 if (connection == null) 194 return null; 195 InBandBytestreamManager manager = managers.get(connection); 196 if (manager == null) { 197 manager = new InBandBytestreamManager(connection); 198 managers.put(connection, manager); 199 } 200 return manager; 201 } 202 203 /** 204 * Constructor. 205 * 206 * @param connection the XMPP connection 207 */ 208 private InBandBytestreamManager(XMPPConnection connection) { 209 this.connection = connection; 210 211 // register bytestream open packet listener 212 this.initiationListener = new InitiationListener(this); 213 connection.registerIQRequestHandler(initiationListener); 214 215 // register bytestream data packet listener 216 this.dataListener = new DataListener(this); 217 connection.registerIQRequestHandler(dataListener); 218 219 // register bytestream close packet listener 220 this.closeListener = new CloseListener(this); 221 connection.registerIQRequestHandler(closeListener); 222 } 223 224 /** 225 * Adds InBandBytestreamListener that is called for every incoming in-band bytestream request 226 * unless there is a user specific InBandBytestreamListener registered. 227 * <p> 228 * If no listeners are registered all In-Band Bytestream request are rejected with a 229 * <not-acceptable/> error. 230 * <p> 231 * Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming 232 * Socks5 bytestream requests sent in the context of <a 233 * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See 234 * {@link FileTransferManager}) 235 * 236 * @param listener the listener to register 237 */ 238 public void addIncomingBytestreamListener(BytestreamListener listener) { 239 this.allRequestListeners.add(listener); 240 } 241 242 /** 243 * Removes the given listener from the list of listeners for all incoming In-Band Bytestream 244 * requests. 245 * 246 * @param listener the listener to remove 247 */ 248 public void removeIncomingBytestreamListener(BytestreamListener listener) { 249 this.allRequestListeners.remove(listener); 250 } 251 252 /** 253 * Adds InBandBytestreamListener that is called for every incoming in-band bytestream request 254 * from the given user. 255 * <p> 256 * Use this method if you are awaiting an incoming Socks5 bytestream request from a specific 257 * user. 258 * <p> 259 * If no listeners are registered all In-Band Bytestream request are rejected with a 260 * <not-acceptable/> error. 261 * <p> 262 * Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming 263 * Socks5 bytestream requests sent in the context of <a 264 * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See 265 * {@link FileTransferManager}) 266 * 267 * @param listener the listener to register 268 * @param initiatorJID the JID of the user that wants to establish an In-Band Bytestream 269 */ 270 public void addIncomingBytestreamListener(BytestreamListener listener, String initiatorJID) { 271 this.userListeners.put(initiatorJID, listener); 272 } 273 274 /** 275 * Removes the listener for the given user. 276 * 277 * @param initiatorJID the JID of the user the listener should be removed 278 */ 279 public void removeIncomingBytestreamListener(String initiatorJID) { 280 this.userListeners.remove(initiatorJID); 281 } 282 283 /** 284 * Use this method to ignore the next incoming In-Band Bytestream request containing the given 285 * session ID. No listeners will be notified for this request and and no error will be returned 286 * to the initiator. 287 * <p> 288 * This method should be used if you are awaiting an In-Band Bytestream request as a reply to 289 * another stanza(/packet) (e.g. file transfer). 290 * 291 * @param sessionID to be ignored 292 */ 293 public void ignoreBytestreamRequestOnce(String sessionID) { 294 this.ignoredBytestreamRequests.add(sessionID); 295 } 296 297 /** 298 * Returns the default block size that is used for all outgoing in-band bytestreams for this 299 * connection. 300 * <p> 301 * The recommended default block size is 4096 bytes. See <a 302 * href="http://xmpp.org/extensions/xep-0047.html#usage">XEP-0047</a> Section 5. 303 * 304 * @return the default block size 305 */ 306 public int getDefaultBlockSize() { 307 return defaultBlockSize; 308 } 309 310 /** 311 * Sets the default block size that is used for all outgoing in-band bytestreams for this 312 * connection. 313 * <p> 314 * The default block size must be between 1 and 65535 bytes. The recommended default block size 315 * is 4096 bytes. See <a href="http://xmpp.org/extensions/xep-0047.html#usage">XEP-0047</a> 316 * Section 5. 317 * 318 * @param defaultBlockSize the default block size to set 319 */ 320 public void setDefaultBlockSize(int defaultBlockSize) { 321 if (defaultBlockSize <= 0 || defaultBlockSize > MAXIMUM_BLOCK_SIZE) { 322 throw new IllegalArgumentException("Default block size must be between 1 and " 323 + MAXIMUM_BLOCK_SIZE); 324 } 325 this.defaultBlockSize = defaultBlockSize; 326 } 327 328 /** 329 * Returns the maximum block size that is allowed for In-Band Bytestreams for this connection. 330 * <p> 331 * Incoming In-Band Bytestream open request will be rejected with an 332 * <resource-constraint/> error if the block size is greater then the maximum allowed 333 * block size. 334 * <p> 335 * The default maximum block size is 65535 bytes. 336 * 337 * @return the maximum block size 338 */ 339 public int getMaximumBlockSize() { 340 return maximumBlockSize; 341 } 342 343 /** 344 * Sets the maximum block size that is allowed for In-Band Bytestreams for this connection. 345 * <p> 346 * The maximum block size must be between 1 and 65535 bytes. 347 * <p> 348 * Incoming In-Band Bytestream open request will be rejected with an 349 * <resource-constraint/> error if the block size is greater then the maximum allowed 350 * block size. 351 * 352 * @param maximumBlockSize the maximum block size to set 353 */ 354 public void setMaximumBlockSize(int maximumBlockSize) { 355 if (maximumBlockSize <= 0 || maximumBlockSize > MAXIMUM_BLOCK_SIZE) { 356 throw new IllegalArgumentException("Maximum block size must be between 1 and " 357 + MAXIMUM_BLOCK_SIZE); 358 } 359 this.maximumBlockSize = maximumBlockSize; 360 } 361 362 /** 363 * Returns the stanza used to send data packets. 364 * <p> 365 * Default is {@link StanzaType#IQ}. See <a 366 * href="http://xmpp.org/extensions/xep-0047.html#message">XEP-0047</a> Section 4. 367 * 368 * @return the stanza used to send data packets 369 */ 370 public StanzaType getStanza() { 371 return stanza; 372 } 373 374 /** 375 * Sets the stanza used to send data packets. 376 * <p> 377 * The use of {@link StanzaType#IQ} is recommended. See <a 378 * href="http://xmpp.org/extensions/xep-0047.html#message">XEP-0047</a> Section 4. 379 * 380 * @param stanza the stanza to set 381 */ 382 public void setStanza(StanzaType stanza) { 383 this.stanza = stanza; 384 } 385 386 /** 387 * Establishes an In-Band Bytestream with the given user and returns the session to send/receive 388 * data to/from the user. 389 * <p> 390 * Use this method to establish In-Band Bytestreams to users accepting all incoming In-Band 391 * Bytestream requests since this method doesn't provide a way to tell the user something about 392 * the data to be sent. 393 * <p> 394 * To establish an In-Band Bytestream after negotiation the kind of data to be sent (e.g. file 395 * transfer) use {@link #establishSession(String, String)}. 396 * 397 * @param targetJID the JID of the user an In-Band Bytestream should be established 398 * @return the session to send/receive data to/from the user 399 * @throws XMPPException if the user doesn't support or accept in-band bytestreams, or if the 400 * user prefers smaller block sizes 401 * @throws SmackException if there was no response from the server. 402 */ 403 public InBandBytestreamSession establishSession(String targetJID) throws XMPPException, SmackException { 404 String sessionID = getNextSessionID(); 405 return establishSession(targetJID, sessionID); 406 } 407 408 /** 409 * Establishes an In-Band Bytestream with the given user using the given session ID and returns 410 * the session to send/receive data to/from the user. 411 * 412 * @param targetJID the JID of the user an In-Band Bytestream should be established 413 * @param sessionID the session ID for the In-Band Bytestream request 414 * @return the session to send/receive data to/from the user 415 * @throws XMPPErrorException if the user doesn't support or accept in-band bytestreams, or if the 416 * user prefers smaller block sizes 417 * @throws NoResponseException if there was no response from the server. 418 * @throws NotConnectedException 419 */ 420 public InBandBytestreamSession establishSession(String targetJID, String sessionID) 421 throws NoResponseException, XMPPErrorException, NotConnectedException { 422 Open byteStreamRequest = new Open(sessionID, this.defaultBlockSize, this.stanza); 423 byteStreamRequest.setTo(targetJID); 424 425 // sending packet will throw exception on timeout or error reply 426 connection.createPacketCollectorAndSend(byteStreamRequest).nextResultOrThrow(); 427 428 InBandBytestreamSession inBandBytestreamSession = new InBandBytestreamSession( 429 this.connection, byteStreamRequest, targetJID); 430 this.sessions.put(sessionID, inBandBytestreamSession); 431 432 return inBandBytestreamSession; 433 } 434 435 /** 436 * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream is 437 * not accepted. 438 * 439 * @param request IQ stanza(/packet) that should be answered with a not-acceptable error 440 * @throws NotConnectedException 441 */ 442 protected void replyRejectPacket(IQ request) throws NotConnectedException { 443 XMPPError xmppError = new XMPPError(XMPPError.Condition.not_acceptable); 444 IQ error = IQ.createErrorResponse(request, xmppError); 445 this.connection.sendStanza(error); 446 } 447 448 /** 449 * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream open 450 * request is rejected because its block size is greater than the maximum allowed block size. 451 * 452 * @param request IQ stanza(/packet) that should be answered with a resource-constraint error 453 * @throws NotConnectedException 454 */ 455 protected void replyResourceConstraintPacket(IQ request) throws NotConnectedException { 456 XMPPError xmppError = new XMPPError(XMPPError.Condition.resource_constraint); 457 IQ error = IQ.createErrorResponse(request, xmppError); 458 this.connection.sendStanza(error); 459 } 460 461 /** 462 * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream 463 * session could not be found. 464 * 465 * @param request IQ stanza(/packet) that should be answered with a item-not-found error 466 * @throws NotConnectedException 467 */ 468 protected void replyItemNotFoundPacket(IQ request) throws NotConnectedException { 469 XMPPError xmppError = new XMPPError(XMPPError.Condition.item_not_found); 470 IQ error = IQ.createErrorResponse(request, xmppError); 471 this.connection.sendStanza(error); 472 } 473 474 /** 475 * Returns a new unique session ID. 476 * 477 * @return a new unique session ID 478 */ 479 private String getNextSessionID() { 480 StringBuilder buffer = new StringBuilder(); 481 buffer.append(SESSION_ID_PREFIX); 482 buffer.append(Math.abs(randomGenerator.nextLong())); 483 return buffer.toString(); 484 } 485 486 /** 487 * Returns the XMPP connection. 488 * 489 * @return the XMPP connection 490 */ 491 protected XMPPConnection getConnection() { 492 return this.connection; 493 } 494 495 /** 496 * Returns the {@link InBandBytestreamListener} that should be informed if a In-Band Bytestream 497 * request from the given initiator JID is received. 498 * 499 * @param initiator the initiator's JID 500 * @return the listener 501 */ 502 protected BytestreamListener getUserListener(String initiator) { 503 return this.userListeners.get(initiator); 504 } 505 506 /** 507 * Returns a list of {@link InBandBytestreamListener} that are informed if there are no 508 * listeners for a specific initiator. 509 * 510 * @return list of listeners 511 */ 512 protected List<BytestreamListener> getAllRequestListeners() { 513 return this.allRequestListeners; 514 } 515 516 /** 517 * Returns the sessions map. 518 * 519 * @return the sessions map 520 */ 521 protected Map<String, InBandBytestreamSession> getSessions() { 522 return sessions; 523 } 524 525 /** 526 * Returns the list of session IDs that should be ignored by the InitialtionListener 527 * 528 * @return list of session IDs 529 */ 530 protected List<String> getIgnoredBytestreamRequests() { 531 return ignoredBytestreamRequests; 532 } 533 534 /** 535 * Disables the InBandBytestreamManager by removing its stanza(/packet) listeners and resetting its 536 * internal status, which includes removing this instance from the managers map. 537 */ 538 private void disableService() { 539 540 // remove manager from static managers map 541 managers.remove(connection); 542 543 // remove all listeners registered by this manager 544 connection.unregisterIQRequestHandler(initiationListener); 545 connection.unregisterIQRequestHandler(dataListener); 546 connection.unregisterIQRequestHandler(closeListener); 547 548 // shutdown threads 549 this.initiationListener.shutdown(); 550 551 // reset internal status 552 this.userListeners.clear(); 553 this.allRequestListeners.clear(); 554 this.sessions.clear(); 555 this.ignoredBytestreamRequests.clear(); 556 557 } 558 559}