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