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.LinkedList; 021import java.util.List; 022import java.util.Map; 023import java.util.Random; 024import java.util.WeakHashMap; 025import java.util.concurrent.ConcurrentHashMap; 026 027import org.jivesoftware.smack.AbstractConnectionClosedListener; 028import org.jivesoftware.smack.ConnectionCreationListener; 029import org.jivesoftware.smack.Manager; 030import org.jivesoftware.smack.SmackException; 031import org.jivesoftware.smack.SmackException.NoResponseException; 032import org.jivesoftware.smack.SmackException.NotConnectedException; 033import org.jivesoftware.smack.XMPPConnection; 034import org.jivesoftware.smack.XMPPConnectionRegistry; 035import org.jivesoftware.smack.XMPPException; 036import org.jivesoftware.smack.XMPPException.XMPPErrorException; 037import org.jivesoftware.smack.packet.IQ; 038import org.jivesoftware.smack.packet.XMPPError; 039 040import org.jivesoftware.smackx.bytestreams.BytestreamListener; 041import org.jivesoftware.smackx.bytestreams.BytestreamManager; 042import org.jivesoftware.smackx.bytestreams.ibb.packet.Open; 043import org.jivesoftware.smackx.filetransfer.FileTransferManager; 044 045import org.jxmpp.jid.Jid; 046 047/** 048 * The InBandBytestreamManager class handles establishing In-Band Bytestreams as specified in the <a 049 * href="http://xmpp.org/extensions/xep-0047.html">XEP-0047</a>. 050 * <p> 051 * The In-Band Bytestreams (IBB) enables two entities to establish a virtual bytestream over which 052 * they can exchange Base64-encoded chunks of data over XMPP itself. It is the fall-back mechanism 053 * in case the Socks5 bytestream method of transferring data is not available. 054 * <p> 055 * There are two ways to send data over an In-Band Bytestream. It could either use IQ stanzas to 056 * send data packets or message stanzas. If IQ stanzas are used every data stanza(/packet) is acknowledged by 057 * the receiver. This is the recommended way to avoid possible rate-limiting penalties. Message 058 * stanzas are not acknowledged because most XMPP server implementation don't support stanza 059 * flow-control method like <a href="http://xmpp.org/extensions/xep-0079.html">Advanced Message 060 * Processing</a>. To set the stanza that should be used invoke {@link #setStanza(StanzaType)}. 061 * <p> 062 * To establish an In-Band Bytestream invoke the {@link #establishSession(Jid)} method. This will 063 * negotiate an in-band bytestream with the given target JID and return a session. 064 * <p> 065 * If a session ID for the In-Band Bytestream was already negotiated (e.g. while negotiating a file 066 * transfer) invoke {@link #establishSession(Jid, String)}. 067 * <p> 068 * To handle incoming In-Band Bytestream requests add an {@link InBandBytestreamListener} to the 069 * manager. There are two ways to add this listener. If you want to be informed about incoming 070 * In-Band Bytestreams from a specific user add the listener by invoking 071 * {@link #addIncomingBytestreamListener(BytestreamListener, Jid)}. If the listener should 072 * respond to all In-Band Bytestream requests invoke 073 * {@link #addIncomingBytestreamListener(BytestreamListener)}. 074 * <p> 075 * Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming 076 * In-Band bytestream requests sent in the context of <a 077 * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See 078 * {@link FileTransferManager}) 079 * <p> 080 * If no {@link InBandBytestreamListener}s are registered, all incoming In-Band bytestream requests 081 * will be rejected by returning a <not-acceptable/> error to the initiator. 082 * 083 * @author Henning Staib 084 */ 085public final class InBandBytestreamManager extends Manager implements BytestreamManager { 086 087 /** 088 * Stanzas that can be used to encapsulate In-Band Bytestream data packets. 089 */ 090 public enum StanzaType { 091 092 /** 093 * IQ stanza. 094 */ 095 IQ, 096 097 /** 098 * Message stanza. 099 */ 100 MESSAGE 101 } 102 103 /* 104 * create a new InBandBytestreamManager and register its shutdown listener on every established 105 * connection 106 */ 107 static { 108 XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() { 109 @Override 110 public void connectionCreated(final XMPPConnection connection) { 111 // create the manager for this connection 112 InBandBytestreamManager.getByteStreamManager(connection); 113 114 // register shutdown listener 115 connection.addConnectionListener(new AbstractConnectionClosedListener() { 116 117 @Override 118 public void connectionTerminated() { 119 InBandBytestreamManager.getByteStreamManager(connection).disableService(); 120 } 121 122 @Override 123 public void reconnectionSuccessful() { 124 // re-create the manager for this connection 125 InBandBytestreamManager.getByteStreamManager(connection); 126 } 127 128 }); 129 130 } 131 }); 132 } 133 134 /** 135 * Maximum block size that is allowed for In-Band Bytestreams. 136 */ 137 public static final int MAXIMUM_BLOCK_SIZE = 65535; 138 139 /* prefix used to generate session IDs */ 140 private static final String SESSION_ID_PREFIX = "jibb_"; 141 142 /* random generator to create session IDs */ 143 private final static Random randomGenerator = new Random(); 144 145 /* stores one InBandBytestreamManager for each XMPP connection */ 146 private final static Map<XMPPConnection, InBandBytestreamManager> managers = new WeakHashMap<>(); 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 super(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 final XMPPConnection connection = connection(); 438 439 // sending packet will throw exception on timeout or error reply 440 connection.createStanzaCollectorAndSend(byteStreamRequest).nextResultOrThrow(); 441 442 InBandBytestreamSession inBandBytestreamSession = new InBandBytestreamSession( 443 connection, byteStreamRequest, targetJID); 444 this.sessions.put(sessionID, inBandBytestreamSession); 445 446 return inBandBytestreamSession; 447 } 448 449 /** 450 * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream is 451 * not accepted. 452 * 453 * @param request IQ stanza(/packet) that should be answered with a not-acceptable error 454 * @throws NotConnectedException 455 * @throws InterruptedException 456 */ 457 protected void replyRejectPacket(IQ request) throws NotConnectedException, InterruptedException { 458 IQ error = IQ.createErrorResponse(request, XMPPError.Condition.not_acceptable); 459 connection().sendStanza(error); 460 } 461 462 /** 463 * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream open 464 * request is rejected because its block size is greater than the maximum allowed block size. 465 * 466 * @param request IQ stanza(/packet) that should be answered with a resource-constraint error 467 * @throws NotConnectedException 468 * @throws InterruptedException 469 */ 470 protected void replyResourceConstraintPacket(IQ request) throws NotConnectedException, InterruptedException { 471 IQ error = IQ.createErrorResponse(request, XMPPError.Condition.resource_constraint); 472 connection().sendStanza(error); 473 } 474 475 /** 476 * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream 477 * session could not be found. 478 * 479 * @param request IQ stanza(/packet) that should be answered with a item-not-found error 480 * @throws NotConnectedException 481 * @throws InterruptedException 482 */ 483 protected void replyItemNotFoundPacket(IQ request) throws NotConnectedException, InterruptedException { 484 IQ error = IQ.createErrorResponse(request, XMPPError.Condition.item_not_found); 485 connection().sendStanza(error); 486 } 487 488 /** 489 * Returns a new unique session ID. 490 * 491 * @return a new unique session ID 492 */ 493 private static String getNextSessionID() { 494 StringBuilder buffer = new StringBuilder(); 495 buffer.append(SESSION_ID_PREFIX); 496 buffer.append(Math.abs(randomGenerator.nextLong())); 497 return buffer.toString(); 498 } 499 500 /** 501 * Returns the XMPP connection. 502 * 503 * @return the XMPP connection 504 */ 505 protected XMPPConnection getConnection() { 506 return connection(); 507 } 508 509 /** 510 * Returns the {@link InBandBytestreamListener} that should be informed if a In-Band Bytestream 511 * request from the given initiator JID is received. 512 * 513 * @param initiator the initiator's JID 514 * @return the listener 515 */ 516 protected BytestreamListener getUserListener(Jid initiator) { 517 return this.userListeners.get(initiator); 518 } 519 520 /** 521 * Returns a list of {@link InBandBytestreamListener} that are informed if there are no 522 * listeners for a specific initiator. 523 * 524 * @return list of listeners 525 */ 526 protected List<BytestreamListener> getAllRequestListeners() { 527 return this.allRequestListeners; 528 } 529 530 /** 531 * Returns the sessions map. 532 * 533 * @return the sessions map 534 */ 535 protected Map<String, InBandBytestreamSession> getSessions() { 536 return sessions; 537 } 538 539 /** 540 * Returns the list of session IDs that should be ignored by the InitialtionListener 541 * 542 * @return list of session IDs 543 */ 544 protected List<String> getIgnoredBytestreamRequests() { 545 return ignoredBytestreamRequests; 546 } 547 548 /** 549 * Disables the InBandBytestreamManager by removing its stanza(/packet) listeners and resetting its 550 * internal status, which includes removing this instance from the managers map. 551 */ 552 private void disableService() { 553 final XMPPConnection connection = connection(); 554 555 // remove manager from static managers map 556 managers.remove(connection); 557 558 // remove all listeners registered by this manager 559 connection.unregisterIQRequestHandler(initiationListener); 560 connection.unregisterIQRequestHandler(dataListener); 561 connection.unregisterIQRequestHandler(closeListener); 562 563 // shutdown threads 564 this.initiationListener.shutdown(); 565 566 // reset internal status 567 this.userListeners.clear(); 568 this.allRequestListeners.clear(); 569 this.sessions.clear(); 570 this.ignoredBytestreamRequests.clear(); 571 572 } 573 574}