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