1 /** 2 * $RCSfile$ 3 * $Revision$ 4 * $Date$ 5 * 6 * Copyright 2003-2006 Jive Software. 7 * 8 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); 9 * you may not use this file except in compliance with the License. 10 * You may obtain a copy of the License at 11 * 12 * http://www.apache.org/licenses/LICENSE-2.0 13 * 14 * Unless required by applicable law or agreed to in writing, software 15 * distributed under the License is distributed on an "AS IS" BASIS, 16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 * See the License for the specific language governing permissions and 18 * limitations under the License. 19 */ 20 package org.jivesoftware.smackx.filetransfer; 21 22 import java.net.URLConnection; 23 import java.util.ArrayList; 24 import java.util.Arrays; 25 import java.util.Collection; 26 import java.util.Collections; 27 import java.util.Iterator; 28 import java.util.List; 29 import java.util.Map; 30 import java.util.Random; 31 import java.util.concurrent.ConcurrentHashMap; 32 33 import org.jivesoftware.smack.Connection; 34 import org.jivesoftware.smack.ConnectionListener; 35 import org.jivesoftware.smack.PacketCollector; 36 import org.jivesoftware.smack.XMPPException; 37 import org.jivesoftware.smack.filter.PacketIDFilter; 38 import org.jivesoftware.smack.packet.IQ; 39 import org.jivesoftware.smack.packet.Packet; 40 import org.jivesoftware.smack.packet.XMPPError; 41 import org.jivesoftware.smackx.Form; 42 import org.jivesoftware.smackx.FormField; 43 import org.jivesoftware.smackx.ServiceDiscoveryManager; 44 import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager; 45 import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamManager; 46 import org.jivesoftware.smackx.packet.DataForm; 47 import org.jivesoftware.smackx.packet.StreamInitiation; 48 49 /** 50 * Manages the negotiation of file transfers according to JEP-0096. If a file is 51 * being sent the remote user chooses the type of stream under which the file 52 * will be sent. 53 * 54 * @author Alexander Wenckus 55 * @see <a href="http://xmpp.org/extensions/xep-0096.html">XEP-0096: SI File Transfer</a> 56 */ 57 public class FileTransferNegotiator { 58 59 // Static 60 61 private static final String[] NAMESPACE = { 62 "http://jabber.org/protocol/si/profile/file-transfer", 63 "http://jabber.org/protocol/si"}; 64 65 private static final Map<Connection, FileTransferNegotiator> transferObject = 66 new ConcurrentHashMap<Connection, FileTransferNegotiator>(); 67 68 private static final String STREAM_INIT_PREFIX = "jsi_"; 69 70 protected static final String STREAM_DATA_FIELD_NAME = "stream-method"; 71 72 private static final Random randomGenerator = new Random(); 73 74 /** 75 * A static variable to use only offer IBB for file transfer. It is generally recommend to only 76 * set this variable to true for testing purposes as IBB is the backup file transfer method 77 * and shouldn't be used as the only transfer method in production systems. 78 */ 79 public static boolean IBB_ONLY = (System.getProperty("ibb") != null);//true; 80 81 /** 82 * Returns the file transfer negotiator related to a particular connection. 83 * When this class is requested on a particular connection the file transfer 84 * service is automatically enabled. 85 * 86 * @param connection The connection for which the transfer manager is desired 87 * @return The IMFileTransferManager 88 */ getInstanceFor( final Connection connection)89 public static FileTransferNegotiator getInstanceFor( 90 final Connection connection) { 91 if (connection == null) { 92 throw new IllegalArgumentException("Connection cannot be null"); 93 } 94 if (!connection.isConnected()) { 95 return null; 96 } 97 98 if (transferObject.containsKey(connection)) { 99 return transferObject.get(connection); 100 } 101 else { 102 FileTransferNegotiator transfer = new FileTransferNegotiator( 103 connection); 104 setServiceEnabled(connection, true); 105 transferObject.put(connection, transfer); 106 return transfer; 107 } 108 } 109 110 /** 111 * Enable the Jabber services related to file transfer on the particular 112 * connection. 113 * 114 * @param connection The connection on which to enable or disable the services. 115 * @param isEnabled True to enable, false to disable. 116 */ setServiceEnabled(final Connection connection, final boolean isEnabled)117 public static void setServiceEnabled(final Connection connection, 118 final boolean isEnabled) { 119 ServiceDiscoveryManager manager = ServiceDiscoveryManager 120 .getInstanceFor(connection); 121 122 List<String> namespaces = new ArrayList<String>(); 123 namespaces.addAll(Arrays.asList(NAMESPACE)); 124 namespaces.add(InBandBytestreamManager.NAMESPACE); 125 if (!IBB_ONLY) { 126 namespaces.add(Socks5BytestreamManager.NAMESPACE); 127 } 128 129 for (String namespace : namespaces) { 130 if (isEnabled) { 131 if (!manager.includesFeature(namespace)) { 132 manager.addFeature(namespace); 133 } 134 } else { 135 manager.removeFeature(namespace); 136 } 137 } 138 139 } 140 141 /** 142 * Checks to see if all file transfer related services are enabled on the 143 * connection. 144 * 145 * @param connection The connection to check 146 * @return True if all related services are enabled, false if they are not. 147 */ isServiceEnabled(final Connection connection)148 public static boolean isServiceEnabled(final Connection connection) { 149 ServiceDiscoveryManager manager = ServiceDiscoveryManager 150 .getInstanceFor(connection); 151 152 List<String> namespaces = new ArrayList<String>(); 153 namespaces.addAll(Arrays.asList(NAMESPACE)); 154 namespaces.add(InBandBytestreamManager.NAMESPACE); 155 if (!IBB_ONLY) { 156 namespaces.add(Socks5BytestreamManager.NAMESPACE); 157 } 158 159 for (String namespace : namespaces) { 160 if (!manager.includesFeature(namespace)) { 161 return false; 162 } 163 } 164 return true; 165 } 166 167 /** 168 * A convenience method to create an IQ packet. 169 * 170 * @param ID The packet ID of the 171 * @param to To whom the packet is addressed. 172 * @param from From whom the packet is sent. 173 * @param type The IQ type of the packet. 174 * @return The created IQ packet. 175 */ createIQ(final String ID, final String to, final String from, final IQ.Type type)176 public static IQ createIQ(final String ID, final String to, 177 final String from, final IQ.Type type) { 178 IQ iqPacket = new IQ() { 179 public String getChildElementXML() { 180 return null; 181 } 182 }; 183 iqPacket.setPacketID(ID); 184 iqPacket.setTo(to); 185 iqPacket.setFrom(from); 186 iqPacket.setType(type); 187 188 return iqPacket; 189 } 190 191 /** 192 * Returns a collection of the supported transfer protocols. 193 * 194 * @return Returns a collection of the supported transfer protocols. 195 */ getSupportedProtocols()196 public static Collection<String> getSupportedProtocols() { 197 List<String> protocols = new ArrayList<String>(); 198 protocols.add(InBandBytestreamManager.NAMESPACE); 199 if (!IBB_ONLY) { 200 protocols.add(Socks5BytestreamManager.NAMESPACE); 201 } 202 return Collections.unmodifiableList(protocols); 203 } 204 205 // non-static 206 207 private final Connection connection; 208 209 private final StreamNegotiator byteStreamTransferManager; 210 211 private final StreamNegotiator inbandTransferManager; 212 FileTransferNegotiator(final Connection connection)213 private FileTransferNegotiator(final Connection connection) { 214 configureConnection(connection); 215 216 this.connection = connection; 217 byteStreamTransferManager = new Socks5TransferNegotiator(connection); 218 inbandTransferManager = new IBBTransferNegotiator(connection); 219 } 220 configureConnection(final Connection connection)221 private void configureConnection(final Connection connection) { 222 connection.addConnectionListener(new ConnectionListener() { 223 public void connectionClosed() { 224 cleanup(connection); 225 } 226 227 public void connectionClosedOnError(Exception e) { 228 cleanup(connection); 229 } 230 231 public void reconnectionFailed(Exception e) { 232 // ignore 233 } 234 235 public void reconnectionSuccessful() { 236 // ignore 237 } 238 239 public void reconnectingIn(int seconds) { 240 // ignore 241 } 242 }); 243 } 244 cleanup(final Connection connection)245 private void cleanup(final Connection connection) { 246 if (transferObject.remove(connection) != null) { 247 inbandTransferManager.cleanup(); 248 } 249 } 250 251 /** 252 * Selects an appropriate stream negotiator after examining the incoming file transfer request. 253 * 254 * @param request The related file transfer request. 255 * @return The file transfer object that handles the transfer 256 * @throws XMPPException If there are either no stream methods contained in the packet, or 257 * there is not an appropriate stream method. 258 */ selectStreamNegotiator( FileTransferRequest request)259 public StreamNegotiator selectStreamNegotiator( 260 FileTransferRequest request) throws XMPPException { 261 StreamInitiation si = request.getStreamInitiation(); 262 FormField streamMethodField = getStreamMethodField(si 263 .getFeatureNegotiationForm()); 264 265 if (streamMethodField == null) { 266 String errorMessage = "No stream methods contained in packet."; 267 XMPPError error = new XMPPError(XMPPError.Condition.bad_request, errorMessage); 268 IQ iqPacket = createIQ(si.getPacketID(), si.getFrom(), si.getTo(), 269 IQ.Type.ERROR); 270 iqPacket.setError(error); 271 connection.sendPacket(iqPacket); 272 throw new XMPPException(errorMessage, error); 273 } 274 275 // select the appropriate protocol 276 277 StreamNegotiator selectedStreamNegotiator; 278 try { 279 selectedStreamNegotiator = getNegotiator(streamMethodField); 280 } 281 catch (XMPPException e) { 282 IQ iqPacket = createIQ(si.getPacketID(), si.getFrom(), si.getTo(), 283 IQ.Type.ERROR); 284 iqPacket.setError(e.getXMPPError()); 285 connection.sendPacket(iqPacket); 286 throw e; 287 } 288 289 // return the appropriate negotiator 290 291 return selectedStreamNegotiator; 292 } 293 getStreamMethodField(DataForm form)294 private FormField getStreamMethodField(DataForm form) { 295 FormField field = null; 296 for (Iterator<FormField> it = form.getFields(); it.hasNext();) { 297 field = it.next(); 298 if (field.getVariable().equals(STREAM_DATA_FIELD_NAME)) { 299 break; 300 } 301 field = null; 302 } 303 return field; 304 } 305 getNegotiator(final FormField field)306 private StreamNegotiator getNegotiator(final FormField field) 307 throws XMPPException { 308 String variable; 309 boolean isByteStream = false; 310 boolean isIBB = false; 311 for (Iterator<FormField.Option> it = field.getOptions(); it.hasNext();) { 312 variable = it.next().getValue(); 313 if (variable.equals(Socks5BytestreamManager.NAMESPACE) && !IBB_ONLY) { 314 isByteStream = true; 315 } 316 else if (variable.equals(InBandBytestreamManager.NAMESPACE)) { 317 isIBB = true; 318 } 319 } 320 321 if (!isByteStream && !isIBB) { 322 XMPPError error = new XMPPError(XMPPError.Condition.bad_request, 323 "No acceptable transfer mechanism"); 324 throw new XMPPException(error.getMessage(), error); 325 } 326 327 //if (isByteStream && isIBB && field.getType().equals(FormField.TYPE_LIST_MULTI)) { 328 if (isByteStream && isIBB) { 329 return new FaultTolerantNegotiator(connection, 330 byteStreamTransferManager, 331 inbandTransferManager); 332 } 333 else if (isByteStream) { 334 return byteStreamTransferManager; 335 } 336 else { 337 return inbandTransferManager; 338 } 339 } 340 341 /** 342 * Reject a stream initiation request from a remote user. 343 * 344 * @param si The Stream Initiation request to reject. 345 */ rejectStream(final StreamInitiation si)346 public void rejectStream(final StreamInitiation si) { 347 XMPPError error = new XMPPError(XMPPError.Condition.forbidden, "Offer Declined"); 348 IQ iqPacket = createIQ(si.getPacketID(), si.getFrom(), si.getTo(), 349 IQ.Type.ERROR); 350 iqPacket.setError(error); 351 connection.sendPacket(iqPacket); 352 } 353 354 /** 355 * Returns a new, unique, stream ID to identify a file transfer. 356 * 357 * @return Returns a new, unique, stream ID to identify a file transfer. 358 */ getNextStreamID()359 public String getNextStreamID() { 360 StringBuilder buffer = new StringBuilder(); 361 buffer.append(STREAM_INIT_PREFIX); 362 buffer.append(Math.abs(randomGenerator.nextLong())); 363 364 return buffer.toString(); 365 } 366 367 /** 368 * Send a request to another user to send them a file. The other user has 369 * the option of, accepting, rejecting, or not responding to a received file 370 * transfer request. 371 * <p/> 372 * If they accept, the packet will contain the other user's chosen stream 373 * type to send the file across. The two choices this implementation 374 * provides to the other user for file transfer are <a 375 * href="http://www.jabber.org/jeps/jep-0065.html">SOCKS5 Bytestreams</a>, 376 * which is the preferred method of transfer, and <a 377 * href="http://www.jabber.org/jeps/jep-0047.html">In-Band Bytestreams</a>, 378 * which is the fallback mechanism. 379 * <p/> 380 * The other user may choose to decline the file request if they do not 381 * desire the file, their client does not support JEP-0096, or if there are 382 * no acceptable means to transfer the file. 383 * <p/> 384 * Finally, if the other user does not respond this method will return null 385 * after the specified timeout. 386 * 387 * @param userID The userID of the user to whom the file will be sent. 388 * @param streamID The unique identifier for this file transfer. 389 * @param fileName The name of this file. Preferably it should include an 390 * extension as it is used to determine what type of file it is. 391 * @param size The size, in bytes, of the file. 392 * @param desc A description of the file. 393 * @param responseTimeout The amount of time, in milliseconds, to wait for the remote 394 * user to respond. If they do not respond in time, this 395 * @return Returns the stream negotiator selected by the peer. 396 * @throws XMPPException Thrown if there is an error negotiating the file transfer. 397 */ negotiateOutgoingTransfer(final String userID, final String streamID, final String fileName, final long size, final String desc, int responseTimeout)398 public StreamNegotiator negotiateOutgoingTransfer(final String userID, 399 final String streamID, final String fileName, final long size, 400 final String desc, int responseTimeout) throws XMPPException { 401 StreamInitiation si = new StreamInitiation(); 402 si.setSesssionID(streamID); 403 si.setMimeType(URLConnection.guessContentTypeFromName(fileName)); 404 405 StreamInitiation.File siFile = new StreamInitiation.File(fileName, size); 406 siFile.setDesc(desc); 407 si.setFile(siFile); 408 409 si.setFeatureNegotiationForm(createDefaultInitiationForm()); 410 411 si.setFrom(connection.getUser()); 412 si.setTo(userID); 413 si.setType(IQ.Type.SET); 414 415 PacketCollector collector = connection 416 .createPacketCollector(new PacketIDFilter(si.getPacketID())); 417 connection.sendPacket(si); 418 Packet siResponse = collector.nextResult(responseTimeout); 419 collector.cancel(); 420 421 if (siResponse instanceof IQ) { 422 IQ iqResponse = (IQ) siResponse; 423 if (iqResponse.getType().equals(IQ.Type.RESULT)) { 424 StreamInitiation response = (StreamInitiation) siResponse; 425 return getOutgoingNegotiator(getStreamMethodField(response 426 .getFeatureNegotiationForm())); 427 428 } 429 else if (iqResponse.getType().equals(IQ.Type.ERROR)) { 430 throw new XMPPException(iqResponse.getError()); 431 } 432 else { 433 throw new XMPPException("File transfer response unreadable"); 434 } 435 } 436 else { 437 return null; 438 } 439 } 440 getOutgoingNegotiator(final FormField field)441 private StreamNegotiator getOutgoingNegotiator(final FormField field) 442 throws XMPPException { 443 String variable; 444 boolean isByteStream = false; 445 boolean isIBB = false; 446 for (Iterator<String> it = field.getValues(); it.hasNext();) { 447 variable = it.next(); 448 if (variable.equals(Socks5BytestreamManager.NAMESPACE) && !IBB_ONLY) { 449 isByteStream = true; 450 } 451 else if (variable.equals(InBandBytestreamManager.NAMESPACE)) { 452 isIBB = true; 453 } 454 } 455 456 if (!isByteStream && !isIBB) { 457 XMPPError error = new XMPPError(XMPPError.Condition.bad_request, 458 "No acceptable transfer mechanism"); 459 throw new XMPPException(error.getMessage(), error); 460 } 461 462 if (isByteStream && isIBB) { 463 return new FaultTolerantNegotiator(connection, 464 byteStreamTransferManager, inbandTransferManager); 465 } 466 else if (isByteStream) { 467 return byteStreamTransferManager; 468 } 469 else { 470 return inbandTransferManager; 471 } 472 } 473 createDefaultInitiationForm()474 private DataForm createDefaultInitiationForm() { 475 DataForm form = new DataForm(Form.TYPE_FORM); 476 FormField field = new FormField(STREAM_DATA_FIELD_NAME); 477 field.setType(FormField.TYPE_LIST_SINGLE); 478 if (!IBB_ONLY) { 479 field.addOption(new FormField.Option(Socks5BytestreamManager.NAMESPACE)); 480 } 481 field.addOption(new FormField.Option(InBandBytestreamManager.NAMESPACE)); 482 form.addField(field); 483 return form; 484 } 485 } 486