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