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