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.StanzaError; 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 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 static final Random randomGenerator = new Random(); 143 144 /* stores one InBandBytestreamManager for each XMPP connection */ 145 private static final 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 public void removeIncomingBytestreamListener(Jid initiatorJID) { 285 this.userListeners.remove(initiatorJID); 286 } 287 288 /** 289 * Use this method to ignore the next incoming In-Band Bytestream request containing the given 290 * session ID. No listeners will be notified for this request and and no error will be returned 291 * to the initiator. 292 * <p> 293 * This method should be used if you are awaiting an In-Band Bytestream request as a reply to 294 * another stanza (e.g. file transfer). 295 * 296 * @param sessionID to be ignored 297 */ 298 public void ignoreBytestreamRequestOnce(String sessionID) { 299 this.ignoredBytestreamRequests.add(sessionID); 300 } 301 302 /** 303 * Returns the default block size that is used for all outgoing in-band bytestreams for this 304 * connection. 305 * <p> 306 * The recommended default block size is 4096 bytes. See <a 307 * href="http://xmpp.org/extensions/xep-0047.html#usage">XEP-0047</a> Section 5. 308 * 309 * @return the default block size 310 */ 311 public int getDefaultBlockSize() { 312 return defaultBlockSize; 313 } 314 315 /** 316 * Sets the default block size that is used for all outgoing in-band bytestreams for this 317 * connection. 318 * <p> 319 * The default block size must be between 1 and 65535 bytes. The recommended default block size 320 * is 4096 bytes. See <a href="http://xmpp.org/extensions/xep-0047.html#usage">XEP-0047</a> 321 * Section 5. 322 * 323 * @param defaultBlockSize the default block size to set 324 */ 325 public void setDefaultBlockSize(int defaultBlockSize) { 326 if (defaultBlockSize <= 0 || defaultBlockSize > MAXIMUM_BLOCK_SIZE) { 327 throw new IllegalArgumentException("Default block size must be between 1 and " 328 + MAXIMUM_BLOCK_SIZE); 329 } 330 this.defaultBlockSize = defaultBlockSize; 331 } 332 333 /** 334 * Returns the maximum block size that is allowed for In-Band Bytestreams for this connection. 335 * <p> 336 * Incoming In-Band Bytestream open request will be rejected with an 337 * <resource-constraint/> error if the block size is greater then the maximum allowed 338 * block size. 339 * <p> 340 * The default maximum block size is 65535 bytes. 341 * 342 * @return the maximum block size 343 */ 344 public int getMaximumBlockSize() { 345 return maximumBlockSize; 346 } 347 348 /** 349 * Sets the maximum block size that is allowed for In-Band Bytestreams for this connection. 350 * <p> 351 * The maximum block size must be between 1 and 65535 bytes. 352 * <p> 353 * Incoming In-Band Bytestream open request will be rejected with an 354 * <resource-constraint/> error if the block size is greater then the maximum allowed 355 * block size. 356 * 357 * @param maximumBlockSize the maximum block size to set 358 */ 359 public void setMaximumBlockSize(int maximumBlockSize) { 360 if (maximumBlockSize <= 0 || maximumBlockSize > MAXIMUM_BLOCK_SIZE) { 361 throw new IllegalArgumentException("Maximum block size must be between 1 and " 362 + MAXIMUM_BLOCK_SIZE); 363 } 364 this.maximumBlockSize = maximumBlockSize; 365 } 366 367 /** 368 * Returns the stanza used to send data packets. 369 * <p> 370 * Default is {@link StanzaType#IQ}. See <a 371 * href="http://xmpp.org/extensions/xep-0047.html#message">XEP-0047</a> Section 4. 372 * 373 * @return the stanza used to send data packets 374 */ 375 public StanzaType getStanza() { 376 return stanza; 377 } 378 379 /** 380 * Sets the stanza used to send data packets. 381 * <p> 382 * The use of {@link StanzaType#IQ} is recommended. See <a 383 * href="http://xmpp.org/extensions/xep-0047.html#message">XEP-0047</a> Section 4. 384 * 385 * @param stanza the stanza to set 386 */ 387 public void setStanza(StanzaType stanza) { 388 this.stanza = stanza; 389 } 390 391 /** 392 * Establishes an In-Band Bytestream with the given user and returns the session to send/receive 393 * data to/from the user. 394 * <p> 395 * Use this method to establish In-Band Bytestreams to users accepting all incoming In-Band 396 * Bytestream requests since this method doesn't provide a way to tell the user something about 397 * the data to be sent. 398 * <p> 399 * To establish an In-Band Bytestream after negotiation the kind of data to be sent (e.g. file 400 * transfer) use {@link #establishSession(Jid, String)}. 401 * 402 * @param targetJID the JID of the user an In-Band Bytestream should be established 403 * @return the session to send/receive data to/from the user 404 * @throws XMPPException if the user doesn't support or accept in-band bytestreams, or if the 405 * user prefers smaller block sizes 406 * @throws SmackException if there was no response from the server. 407 * @throws InterruptedException 408 */ 409 @Override 410 public InBandBytestreamSession establishSession(Jid targetJID) throws XMPPException, SmackException, InterruptedException { 411 String sessionID = getNextSessionID(); 412 return establishSession(targetJID, sessionID); 413 } 414 415 /** 416 * Establishes an In-Band Bytestream with the given user using the given session ID and returns 417 * the session to send/receive data to/from the user. 418 * 419 * @param targetJID the JID of the user an In-Band Bytestream should be established 420 * @param sessionID the session ID for the In-Band Bytestream request 421 * @return the session to send/receive data to/from the user 422 * @throws XMPPErrorException if the user doesn't support or accept in-band bytestreams, or if the 423 * user prefers smaller block sizes 424 * @throws NoResponseException if there was no response from the server. 425 * @throws NotConnectedException 426 * @throws InterruptedException 427 */ 428 @Override 429 public InBandBytestreamSession establishSession(Jid targetJID, String sessionID) 430 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 431 Open byteStreamRequest = new Open(sessionID, this.defaultBlockSize, this.stanza); 432 byteStreamRequest.setTo(targetJID); 433 434 final XMPPConnection connection = connection(); 435 436 // sending packet will throw exception on timeout or error reply 437 connection.createStanzaCollectorAndSend(byteStreamRequest).nextResultOrThrow(); 438 439 InBandBytestreamSession inBandBytestreamSession = new InBandBytestreamSession( 440 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 stanza that should be answered with a not-acceptable error 451 * @throws NotConnectedException 452 * @throws InterruptedException 453 */ 454 protected void replyRejectPacket(IQ request) throws NotConnectedException, InterruptedException { 455 IQ error = IQ.createErrorResponse(request, StanzaError.Condition.not_acceptable); 456 connection().sendStanza(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 stanza that should be answered with a resource-constraint error 464 * @throws NotConnectedException 465 * @throws InterruptedException 466 */ 467 protected void replyResourceConstraintPacket(IQ request) throws NotConnectedException, InterruptedException { 468 IQ error = IQ.createErrorResponse(request, StanzaError.Condition.resource_constraint); 469 connection().sendStanza(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 stanza that should be answered with a item-not-found error 477 * @throws NotConnectedException 478 * @throws InterruptedException 479 */ 480 protected void replyItemNotFoundPacket(IQ request) throws NotConnectedException, InterruptedException { 481 IQ error = IQ.createErrorResponse(request, StanzaError.Condition.item_not_found); 482 connection().sendStanza(error); 483 } 484 485 /** 486 * Returns a new unique session ID. 487 * 488 * @return a new unique session ID 489 */ 490 private static 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 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(Jid 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 stanza listeners and resetting its 547 * internal status, which includes removing this instance from the managers map. 548 */ 549 private void disableService() { 550 final XMPPConnection connection = connection(); 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}