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 */ 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 */ 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 */ 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 */ 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 */ 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 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 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 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 */ 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 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 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 */ 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 */ 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 */ 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 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 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