blob: ef5253154ff42e7eb65d5da29551eed8688e7836 [file] [log] [blame]
Shuyi Chend7955ce2013-05-22 14:51:55 -07001/**
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 */
14package org.jivesoftware.smackx.bytestreams.ibb;
15
16import java.util.Collections;
17import java.util.HashMap;
18import java.util.LinkedList;
19import java.util.List;
20import java.util.Map;
21import java.util.Random;
22import java.util.concurrent.ConcurrentHashMap;
23
24import org.jivesoftware.smack.AbstractConnectionListener;
25import org.jivesoftware.smack.Connection;
26import org.jivesoftware.smack.ConnectionCreationListener;
27import org.jivesoftware.smack.XMPPException;
28import org.jivesoftware.smack.packet.IQ;
29import org.jivesoftware.smack.packet.XMPPError;
30import org.jivesoftware.smack.util.SyncPacketSend;
31import org.jivesoftware.smackx.bytestreams.BytestreamListener;
32import org.jivesoftware.smackx.bytestreams.BytestreamManager;
33import org.jivesoftware.smackx.bytestreams.ibb.packet.Open;
34import 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 &lt;not-acceptable/&gt; error to the initiator.
71 *
72 * @author Henning Staib
73 */
74public 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 {
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 */
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 */
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 * &lt;not-acceptable/&gt; 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 */
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 */
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 * &lt;not-acceptable/&gt; 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 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 */
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 */
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 */
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 */
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 * &lt;resource-constraint/&gt; 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 */
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 * &lt;resource-constraint/&gt; 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 */
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 */
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 */
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 */
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 */
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 */
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 */
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 */
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 */
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 */
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 */
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 */
499 protected List<BytestreamListener> getAllRequestListeners() {
500 return this.allRequestListeners;
501 }
502
503 /**
504 * Returns the sessions map.
505 *
506 * @return the sessions map
507 */
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 */
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 */
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}