1 /** 2 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); 3 * you may not use this file except in compliance with the License. 4 * You may obtain a copy of the License at 5 * 6 * http://www.apache.org/licenses/LICENSE-2.0 7 * 8 * Unless required by applicable law or agreed to in writing, software 9 * distributed under the License is distributed on an "AS IS" BASIS, 10 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 * See the License for the specific language governing permissions and 12 * limitations under the License. 13 */ 14 package org.jivesoftware.smackx.bytestreams.ibb; 15 16 import java.util.Collections; 17 import java.util.HashMap; 18 import java.util.LinkedList; 19 import java.util.List; 20 import java.util.Map; 21 import java.util.Random; 22 import java.util.concurrent.ConcurrentHashMap; 23 24 import org.jivesoftware.smack.AbstractConnectionListener; 25 import org.jivesoftware.smack.Connection; 26 import org.jivesoftware.smack.ConnectionCreationListener; 27 import org.jivesoftware.smack.XMPPException; 28 import org.jivesoftware.smack.packet.IQ; 29 import org.jivesoftware.smack.packet.XMPPError; 30 import org.jivesoftware.smack.util.SyncPacketSend; 31 import org.jivesoftware.smackx.bytestreams.BytestreamListener; 32 import org.jivesoftware.smackx.bytestreams.BytestreamManager; 33 import org.jivesoftware.smackx.bytestreams.ibb.packet.Open; 34 import org.jivesoftware.smackx.filetransfer.FileTransferManager; 35 36 /** 37 * The InBandBytestreamManager class handles establishing In-Band Bytestreams as specified in the <a 38 * href="http://xmpp.org/extensions/xep-0047.html">XEP-0047</a>. 39 * <p> 40 * The In-Band Bytestreams (IBB) enables two entities to establish a virtual bytestream over which 41 * they can exchange Base64-encoded chunks of data over XMPP itself. It is the fall-back mechanism 42 * in case the Socks5 bytestream method of transferring data is not available. 43 * <p> 44 * There are two ways to send data over an In-Band Bytestream. It could either use IQ stanzas to 45 * send data packets or message stanzas. If IQ stanzas are used every data packet is acknowledged by 46 * the receiver. This is the recommended way to avoid possible rate-limiting penalties. Message 47 * stanzas are not acknowledged because most XMPP server implementation don't support stanza 48 * flow-control method like <a href="http://xmpp.org/extensions/xep-0079.html">Advanced Message 49 * Processing</a>. To set the stanza that should be used invoke {@link #setStanza(StanzaType)}. 50 * <p> 51 * To establish an In-Band Bytestream invoke the {@link #establishSession(String)} method. This will 52 * negotiate an in-band bytestream with the given target JID and return a session. 53 * <p> 54 * If a session ID for the In-Band Bytestream was already negotiated (e.g. while negotiating a file 55 * transfer) invoke {@link #establishSession(String, String)}. 56 * <p> 57 * To handle incoming In-Band Bytestream requests add an {@link InBandBytestreamListener} to the 58 * manager. There are two ways to add this listener. If you want to be informed about incoming 59 * In-Band Bytestreams from a specific user add the listener by invoking 60 * {@link #addIncomingBytestreamListener(BytestreamListener, String)}. If the listener should 61 * respond to all In-Band Bytestream requests invoke 62 * {@link #addIncomingBytestreamListener(BytestreamListener)}. 63 * <p> 64 * Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming 65 * In-Band bytestream requests sent in the context of <a 66 * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See 67 * {@link FileTransferManager}) 68 * <p> 69 * If no {@link InBandBytestreamListener}s are registered, all incoming In-Band bytestream requests 70 * will be rejected by returning a <not-acceptable/> error to the initiator. 71 * 72 * @author Henning Staib 73 */ 74 public class InBandBytestreamManager implements BytestreamManager { 75 76 /** 77 * Stanzas that can be used to encapsulate In-Band Bytestream data packets. 78 */ 79 public enum StanzaType { 80 81 /** 82 * IQ stanza. 83 */ 84 IQ, 85 86 /** 87 * Message stanza. 88 */ 89 MESSAGE 90 } 91 92 /* 93 * create a new InBandBytestreamManager and register its shutdown listener on every established 94 * connection 95 */ 96 static { Connection.addConnectionCreationListener(new ConnectionCreationListener() { public void connectionCreated(Connection connection) { final InBandBytestreamManager manager; manager = InBandBytestreamManager.getByteStreamManager(connection); connection.addConnectionListener(new AbstractConnectionListener() { public void connectionClosed() { manager.disableService(); } }); } })97 Connection.addConnectionCreationListener(new ConnectionCreationListener() { 98 public void connectionCreated(Connection connection) { 99 final InBandBytestreamManager manager; 100 manager = InBandBytestreamManager.getByteStreamManager(connection); 101 102 // register shutdown listener 103 connection.addConnectionListener(new AbstractConnectionListener() { 104 105 public void connectionClosed() { 106 manager.disableService(); 107 } 108 109 }); 110 111 } 112 }); 113 } 114 115 /** 116 * The XMPP namespace of the In-Band Bytestream 117 */ 118 public static final String NAMESPACE = "http://jabber.org/protocol/ibb"; 119 120 /** 121 * Maximum block size that is allowed for In-Band Bytestreams 122 */ 123 public static final int MAXIMUM_BLOCK_SIZE = 65535; 124 125 /* prefix used to generate session IDs */ 126 private static final String SESSION_ID_PREFIX = "jibb_"; 127 128 /* random generator to create session IDs */ 129 private final static Random randomGenerator = new Random(); 130 131 /* stores one InBandBytestreamManager for each XMPP connection */ 132 private final static Map<Connection, InBandBytestreamManager> managers = new HashMap<Connection, InBandBytestreamManager>(); 133 134 /* XMPP connection */ 135 private final Connection connection; 136 137 /* 138 * assigns a user to a listener that is informed if an In-Band Bytestream request for this user 139 * is received 140 */ 141 private final Map<String, BytestreamListener> userListeners = new ConcurrentHashMap<String, BytestreamListener>(); 142 143 /* 144 * list of listeners that respond to all In-Band Bytestream requests if there are no user 145 * specific listeners for that request 146 */ 147 private final List<BytestreamListener> allRequestListeners = Collections.synchronizedList(new LinkedList<BytestreamListener>()); 148 149 /* listener that handles all incoming In-Band Bytestream requests */ 150 private final InitiationListener initiationListener; 151 152 /* listener that handles all incoming In-Band Bytestream IQ data packets */ 153 private final DataListener dataListener; 154 155 /* listener that handles all incoming In-Band Bytestream close requests */ 156 private final CloseListener closeListener; 157 158 /* assigns a session ID to the In-Band Bytestream session */ 159 private final Map<String, InBandBytestreamSession> sessions = new ConcurrentHashMap<String, InBandBytestreamSession>(); 160 161 /* block size used for new In-Band Bytestreams */ 162 private int defaultBlockSize = 4096; 163 164 /* maximum block size allowed for this connection */ 165 private int maximumBlockSize = MAXIMUM_BLOCK_SIZE; 166 167 /* the stanza used to send data packets */ 168 private StanzaType stanza = StanzaType.IQ; 169 170 /* 171 * list containing session IDs of In-Band Bytestream open packets that should be ignored by the 172 * InitiationListener 173 */ 174 private List<String> ignoredBytestreamRequests = Collections.synchronizedList(new LinkedList<String>()); 175 176 /** 177 * Returns the InBandBytestreamManager to handle In-Band Bytestreams for a given 178 * {@link Connection}. 179 * 180 * @param connection the XMPP connection 181 * @return the InBandBytestreamManager for the given XMPP connection 182 */ getByteStreamManager(Connection connection)183 public static synchronized InBandBytestreamManager getByteStreamManager(Connection connection) { 184 if (connection == null) 185 return null; 186 InBandBytestreamManager manager = managers.get(connection); 187 if (manager == null) { 188 manager = new InBandBytestreamManager(connection); 189 managers.put(connection, manager); 190 } 191 return manager; 192 } 193 194 /** 195 * Constructor. 196 * 197 * @param connection the XMPP connection 198 */ InBandBytestreamManager(Connection connection)199 private InBandBytestreamManager(Connection connection) { 200 this.connection = connection; 201 202 // register bytestream open packet listener 203 this.initiationListener = new InitiationListener(this); 204 this.connection.addPacketListener(this.initiationListener, 205 this.initiationListener.getFilter()); 206 207 // register bytestream data packet listener 208 this.dataListener = new DataListener(this); 209 this.connection.addPacketListener(this.dataListener, this.dataListener.getFilter()); 210 211 // register bytestream close packet listener 212 this.closeListener = new CloseListener(this); 213 this.connection.addPacketListener(this.closeListener, this.closeListener.getFilter()); 214 215 } 216 217 /** 218 * Adds InBandBytestreamListener that is called for every incoming in-band bytestream request 219 * unless there is a user specific InBandBytestreamListener registered. 220 * <p> 221 * If no listeners are registered all In-Band Bytestream request are rejected with a 222 * <not-acceptable/> error. 223 * <p> 224 * Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming 225 * Socks5 bytestream requests sent in the context of <a 226 * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See 227 * {@link FileTransferManager}) 228 * 229 * @param listener the listener to register 230 */ addIncomingBytestreamListener(BytestreamListener listener)231 public void addIncomingBytestreamListener(BytestreamListener listener) { 232 this.allRequestListeners.add(listener); 233 } 234 235 /** 236 * Removes the given listener from the list of listeners for all incoming In-Band Bytestream 237 * requests. 238 * 239 * @param listener the listener to remove 240 */ removeIncomingBytestreamListener(BytestreamListener listener)241 public void removeIncomingBytestreamListener(BytestreamListener listener) { 242 this.allRequestListeners.remove(listener); 243 } 244 245 /** 246 * Adds InBandBytestreamListener that is called for every incoming in-band bytestream request 247 * from the given user. 248 * <p> 249 * Use this method if you are awaiting an incoming Socks5 bytestream request from a specific 250 * user. 251 * <p> 252 * If no listeners are registered all In-Band Bytestream request are rejected with a 253 * <not-acceptable/> error. 254 * <p> 255 * Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming 256 * Socks5 bytestream requests sent in the context of <a 257 * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See 258 * {@link FileTransferManager}) 259 * 260 * @param listener the listener to register 261 * @param initiatorJID the JID of the user that wants to establish an In-Band Bytestream 262 */ addIncomingBytestreamListener(BytestreamListener listener, String initiatorJID)263 public void addIncomingBytestreamListener(BytestreamListener listener, String initiatorJID) { 264 this.userListeners.put(initiatorJID, listener); 265 } 266 267 /** 268 * Removes the listener for the given user. 269 * 270 * @param initiatorJID the JID of the user the listener should be removed 271 */ removeIncomingBytestreamListener(String initiatorJID)272 public void removeIncomingBytestreamListener(String initiatorJID) { 273 this.userListeners.remove(initiatorJID); 274 } 275 276 /** 277 * Use this method to ignore the next incoming In-Band Bytestream request containing the given 278 * session ID. No listeners will be notified for this request and and no error will be returned 279 * to the initiator. 280 * <p> 281 * This method should be used if you are awaiting an In-Band Bytestream request as a reply to 282 * another packet (e.g. file transfer). 283 * 284 * @param sessionID to be ignored 285 */ ignoreBytestreamRequestOnce(String sessionID)286 public void ignoreBytestreamRequestOnce(String sessionID) { 287 this.ignoredBytestreamRequests.add(sessionID); 288 } 289 290 /** 291 * Returns the default block size that is used for all outgoing in-band bytestreams for this 292 * connection. 293 * <p> 294 * The recommended default block size is 4096 bytes. See <a 295 * href="http://xmpp.org/extensions/xep-0047.html#usage">XEP-0047</a> Section 5. 296 * 297 * @return the default block size 298 */ getDefaultBlockSize()299 public int getDefaultBlockSize() { 300 return defaultBlockSize; 301 } 302 303 /** 304 * Sets the default block size that is used for all outgoing in-band bytestreams for this 305 * connection. 306 * <p> 307 * The default block size must be between 1 and 65535 bytes. The recommended default block size 308 * is 4096 bytes. See <a href="http://xmpp.org/extensions/xep-0047.html#usage">XEP-0047</a> 309 * Section 5. 310 * 311 * @param defaultBlockSize the default block size to set 312 */ setDefaultBlockSize(int defaultBlockSize)313 public void setDefaultBlockSize(int defaultBlockSize) { 314 if (defaultBlockSize <= 0 || defaultBlockSize > MAXIMUM_BLOCK_SIZE) { 315 throw new IllegalArgumentException("Default block size must be between 1 and " 316 + MAXIMUM_BLOCK_SIZE); 317 } 318 this.defaultBlockSize = defaultBlockSize; 319 } 320 321 /** 322 * Returns the maximum block size that is allowed for In-Band Bytestreams for this connection. 323 * <p> 324 * Incoming In-Band Bytestream open request will be rejected with an 325 * <resource-constraint/> error if the block size is greater then the maximum allowed 326 * block size. 327 * <p> 328 * The default maximum block size is 65535 bytes. 329 * 330 * @return the maximum block size 331 */ getMaximumBlockSize()332 public int getMaximumBlockSize() { 333 return maximumBlockSize; 334 } 335 336 /** 337 * Sets the maximum block size that is allowed for In-Band Bytestreams for this connection. 338 * <p> 339 * The maximum block size must be between 1 and 65535 bytes. 340 * <p> 341 * Incoming In-Band Bytestream open request will be rejected with an 342 * <resource-constraint/> error if the block size is greater then the maximum allowed 343 * block size. 344 * 345 * @param maximumBlockSize the maximum block size to set 346 */ setMaximumBlockSize(int maximumBlockSize)347 public void setMaximumBlockSize(int maximumBlockSize) { 348 if (maximumBlockSize <= 0 || maximumBlockSize > MAXIMUM_BLOCK_SIZE) { 349 throw new IllegalArgumentException("Maximum block size must be between 1 and " 350 + MAXIMUM_BLOCK_SIZE); 351 } 352 this.maximumBlockSize = maximumBlockSize; 353 } 354 355 /** 356 * Returns the stanza used to send data packets. 357 * <p> 358 * Default is {@link StanzaType#IQ}. See <a 359 * href="http://xmpp.org/extensions/xep-0047.html#message">XEP-0047</a> Section 4. 360 * 361 * @return the stanza used to send data packets 362 */ getStanza()363 public StanzaType getStanza() { 364 return stanza; 365 } 366 367 /** 368 * Sets the stanza used to send data packets. 369 * <p> 370 * The use of {@link StanzaType#IQ} is recommended. See <a 371 * href="http://xmpp.org/extensions/xep-0047.html#message">XEP-0047</a> Section 4. 372 * 373 * @param stanza the stanza to set 374 */ setStanza(StanzaType stanza)375 public void setStanza(StanzaType stanza) { 376 this.stanza = stanza; 377 } 378 379 /** 380 * Establishes an In-Band Bytestream with the given user and returns the session to send/receive 381 * data to/from the user. 382 * <p> 383 * Use this method to establish In-Band Bytestreams to users accepting all incoming In-Band 384 * Bytestream requests since this method doesn't provide a way to tell the user something about 385 * the data to be sent. 386 * <p> 387 * To establish an In-Band Bytestream after negotiation the kind of data to be sent (e.g. file 388 * transfer) use {@link #establishSession(String, String)}. 389 * 390 * @param targetJID the JID of the user an In-Band Bytestream should be established 391 * @return the session to send/receive data to/from the user 392 * @throws XMPPException if the user doesn't support or accept in-band bytestreams, or if the 393 * user prefers smaller block sizes 394 */ establishSession(String targetJID)395 public InBandBytestreamSession establishSession(String targetJID) throws XMPPException { 396 String sessionID = getNextSessionID(); 397 return establishSession(targetJID, sessionID); 398 } 399 400 /** 401 * Establishes an In-Band Bytestream with the given user using the given session ID and returns 402 * the session to send/receive data to/from the user. 403 * 404 * @param targetJID the JID of the user an In-Band Bytestream should be established 405 * @param sessionID the session ID for the In-Band Bytestream request 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 */ establishSession(String targetJID, String sessionID)410 public InBandBytestreamSession establishSession(String targetJID, String sessionID) 411 throws XMPPException { 412 Open byteStreamRequest = new Open(sessionID, this.defaultBlockSize, this.stanza); 413 byteStreamRequest.setTo(targetJID); 414 415 // sending packet will throw exception on timeout or error reply 416 SyncPacketSend.getReply(this.connection, byteStreamRequest); 417 418 InBandBytestreamSession inBandBytestreamSession = new InBandBytestreamSession( 419 this.connection, byteStreamRequest, targetJID); 420 this.sessions.put(sessionID, inBandBytestreamSession); 421 422 return inBandBytestreamSession; 423 } 424 425 /** 426 * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream is 427 * not accepted. 428 * 429 * @param request IQ packet that should be answered with a not-acceptable error 430 */ replyRejectPacket(IQ request)431 protected void replyRejectPacket(IQ request) { 432 XMPPError xmppError = new XMPPError(XMPPError.Condition.no_acceptable); 433 IQ error = IQ.createErrorResponse(request, xmppError); 434 this.connection.sendPacket(error); 435 } 436 437 /** 438 * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream open 439 * request is rejected because its block size is greater than the maximum allowed block size. 440 * 441 * @param request IQ packet that should be answered with a resource-constraint error 442 */ replyResourceConstraintPacket(IQ request)443 protected void replyResourceConstraintPacket(IQ request) { 444 XMPPError xmppError = new XMPPError(XMPPError.Condition.resource_constraint); 445 IQ error = IQ.createErrorResponse(request, xmppError); 446 this.connection.sendPacket(error); 447 } 448 449 /** 450 * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream 451 * session could not be found. 452 * 453 * @param request IQ packet that should be answered with a item-not-found error 454 */ replyItemNotFoundPacket(IQ request)455 protected void replyItemNotFoundPacket(IQ request) { 456 XMPPError xmppError = new XMPPError(XMPPError.Condition.item_not_found); 457 IQ error = IQ.createErrorResponse(request, xmppError); 458 this.connection.sendPacket(error); 459 } 460 461 /** 462 * Returns a new unique session ID. 463 * 464 * @return a new unique session ID 465 */ getNextSessionID()466 private String getNextSessionID() { 467 StringBuilder buffer = new StringBuilder(); 468 buffer.append(SESSION_ID_PREFIX); 469 buffer.append(Math.abs(randomGenerator.nextLong())); 470 return buffer.toString(); 471 } 472 473 /** 474 * Returns the XMPP connection. 475 * 476 * @return the XMPP connection 477 */ getConnection()478 protected Connection getConnection() { 479 return this.connection; 480 } 481 482 /** 483 * Returns the {@link InBandBytestreamListener} that should be informed if a In-Band Bytestream 484 * request from the given initiator JID is received. 485 * 486 * @param initiator the initiator's JID 487 * @return the listener 488 */ getUserListener(String initiator)489 protected BytestreamListener getUserListener(String initiator) { 490 return this.userListeners.get(initiator); 491 } 492 493 /** 494 * Returns a list of {@link InBandBytestreamListener} that are informed if there are no 495 * listeners for a specific initiator. 496 * 497 * @return list of listeners 498 */ getAllRequestListeners()499 protected List<BytestreamListener> getAllRequestListeners() { 500 return this.allRequestListeners; 501 } 502 503 /** 504 * Returns the sessions map. 505 * 506 * @return the sessions map 507 */ getSessions()508 protected Map<String, InBandBytestreamSession> getSessions() { 509 return sessions; 510 } 511 512 /** 513 * Returns the list of session IDs that should be ignored by the InitialtionListener 514 * 515 * @return list of session IDs 516 */ getIgnoredBytestreamRequests()517 protected List<String> getIgnoredBytestreamRequests() { 518 return ignoredBytestreamRequests; 519 } 520 521 /** 522 * Disables the InBandBytestreamManager by removing its packet listeners and resetting its 523 * internal status. 524 */ disableService()525 private void disableService() { 526 527 // remove manager from static managers map 528 managers.remove(connection); 529 530 // remove all listeners registered by this manager 531 this.connection.removePacketListener(this.initiationListener); 532 this.connection.removePacketListener(this.dataListener); 533 this.connection.removePacketListener(this.closeListener); 534 535 // shutdown threads 536 this.initiationListener.shutdown(); 537 538 // reset internal status 539 this.userListeners.clear(); 540 this.allRequestListeners.clear(); 541 this.sessions.clear(); 542 this.ignoredBytestreamRequests.clear(); 543 544 } 545 546 } 547