001/** 002 * 003 * Copyright 2003-2006 Jive Software. 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.filetransfer; 018 019import java.net.URLConnection; 020import java.util.ArrayList; 021import java.util.Arrays; 022import java.util.Collection; 023import java.util.Collections; 024import java.util.List; 025import java.util.Map; 026import java.util.Random; 027import java.util.concurrent.ConcurrentHashMap; 028 029import org.jivesoftware.smack.AbstractConnectionListener; 030import org.jivesoftware.smack.SmackException.NotConnectedException; 031import org.jivesoftware.smack.XMPPConnection; 032import org.jivesoftware.smack.PacketCollector; 033import org.jivesoftware.smack.XMPPException.XMPPErrorException; 034import org.jivesoftware.smack.packet.IQ; 035import org.jivesoftware.smack.packet.Packet; 036import org.jivesoftware.smack.packet.XMPPError; 037import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager; 038import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamManager; 039import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; 040import org.jivesoftware.smackx.si.packet.StreamInitiation; 041import org.jivesoftware.smackx.xdata.Form; 042import org.jivesoftware.smackx.xdata.FormField; 043import org.jivesoftware.smackx.xdata.packet.DataForm; 044 045/** 046 * Manages the negotiation of file transfers according to XEP-0096. If a file is 047 * being sent the remote user chooses the type of stream under which the file 048 * will be sent. 049 * 050 * @author Alexander Wenckus 051 * @see <a href="http://xmpp.org/extensions/xep-0096.html">XEP-0096: SI File Transfer</a> 052 */ 053public class FileTransferNegotiator { 054 055 // Static 056 057 private static final String[] NAMESPACE = { 058 "http://jabber.org/protocol/si/profile/file-transfer", 059 "http://jabber.org/protocol/si"}; 060 061 private static final Map<XMPPConnection, FileTransferNegotiator> transferObject = 062 new ConcurrentHashMap<XMPPConnection, FileTransferNegotiator>(); 063 064 private static final String STREAM_INIT_PREFIX = "jsi_"; 065 066 protected static final String STREAM_DATA_FIELD_NAME = "stream-method"; 067 068 private static final Random randomGenerator = new Random(); 069 070 /** 071 * A static variable to use only offer IBB for file transfer. It is generally recommend to only 072 * set this variable to true for testing purposes as IBB is the backup file transfer method 073 * and shouldn't be used as the only transfer method in production systems. 074 */ 075 public static boolean IBB_ONLY = (System.getProperty("ibb") != null);//true; 076 077 /** 078 * Returns the file transfer negotiator related to a particular connection. 079 * When this class is requested on a particular connection the file transfer 080 * service is automatically enabled. 081 * 082 * @param connection The connection for which the transfer manager is desired 083 * @return The IMFileTransferManager 084 */ 085 public static FileTransferNegotiator getInstanceFor( 086 final XMPPConnection connection) { 087 if (connection == null) { 088 throw new IllegalArgumentException("XMPPConnection cannot be null"); 089 } 090 if (!connection.isConnected()) { 091 return null; 092 } 093 094 if (transferObject.containsKey(connection)) { 095 return transferObject.get(connection); 096 } 097 else { 098 FileTransferNegotiator transfer = new FileTransferNegotiator( 099 connection); 100 setServiceEnabled(connection, true); 101 transferObject.put(connection, transfer); 102 return transfer; 103 } 104 } 105 106 /** 107 * Enable the Jabber services related to file transfer on the particular 108 * connection. 109 * 110 * @param connection The connection on which to enable or disable the services. 111 * @param isEnabled True to enable, false to disable. 112 */ 113 public static void setServiceEnabled(final XMPPConnection connection, 114 final boolean isEnabled) { 115 ServiceDiscoveryManager manager = ServiceDiscoveryManager 116 .getInstanceFor(connection); 117 118 List<String> namespaces = new ArrayList<String>(); 119 namespaces.addAll(Arrays.asList(NAMESPACE)); 120 namespaces.add(InBandBytestreamManager.NAMESPACE); 121 if (!IBB_ONLY) { 122 namespaces.add(Socks5BytestreamManager.NAMESPACE); 123 } 124 125 for (String namespace : namespaces) { 126 if (isEnabled) { 127 if (!manager.includesFeature(namespace)) { 128 manager.addFeature(namespace); 129 } 130 } else { 131 manager.removeFeature(namespace); 132 } 133 } 134 135 } 136 137 /** 138 * Checks to see if all file transfer related services are enabled on the 139 * connection. 140 * 141 * @param connection The connection to check 142 * @return True if all related services are enabled, false if they are not. 143 */ 144 public static boolean isServiceEnabled(final XMPPConnection connection) { 145 ServiceDiscoveryManager manager = ServiceDiscoveryManager 146 .getInstanceFor(connection); 147 148 List<String> namespaces = new ArrayList<String>(); 149 namespaces.addAll(Arrays.asList(NAMESPACE)); 150 namespaces.add(InBandBytestreamManager.NAMESPACE); 151 if (!IBB_ONLY) { 152 namespaces.add(Socks5BytestreamManager.NAMESPACE); 153 } 154 155 for (String namespace : namespaces) { 156 if (!manager.includesFeature(namespace)) { 157 return false; 158 } 159 } 160 return true; 161 } 162 163 /** 164 * A convenience method to create an IQ packet. 165 * 166 * @param ID The packet ID of the 167 * @param to To whom the packet is addressed. 168 * @param from From whom the packet is sent. 169 * @param type The IQ type of the packet. 170 * @return The created IQ packet. 171 */ 172 public static IQ createIQ(final String ID, final String to, 173 final String from, final IQ.Type type) { 174 IQ iqPacket = new IQ() { 175 public String getChildElementXML() { 176 return null; 177 } 178 }; 179 iqPacket.setPacketID(ID); 180 iqPacket.setTo(to); 181 iqPacket.setFrom(from); 182 iqPacket.setType(type); 183 184 return iqPacket; 185 } 186 187 /** 188 * Returns a collection of the supported transfer protocols. 189 * 190 * @return Returns a collection of the supported transfer protocols. 191 */ 192 public static Collection<String> getSupportedProtocols() { 193 List<String> protocols = new ArrayList<String>(); 194 protocols.add(InBandBytestreamManager.NAMESPACE); 195 if (!IBB_ONLY) { 196 protocols.add(Socks5BytestreamManager.NAMESPACE); 197 } 198 return Collections.unmodifiableList(protocols); 199 } 200 201 // non-static 202 203 private final XMPPConnection connection; 204 205 private final StreamNegotiator byteStreamTransferManager; 206 207 private final StreamNegotiator inbandTransferManager; 208 209 private FileTransferNegotiator(final XMPPConnection connection) { 210 configureConnection(connection); 211 212 this.connection = connection; 213 byteStreamTransferManager = new Socks5TransferNegotiator(connection); 214 inbandTransferManager = new IBBTransferNegotiator(connection); 215 } 216 217 private void configureConnection(final XMPPConnection connection) { 218 connection.addConnectionListener(new AbstractConnectionListener() { 219 @Override 220 public void connectionClosed() { 221 cleanup(connection); 222 } 223 224 @Override 225 public void connectionClosedOnError(Exception e) { 226 cleanup(connection); 227 } 228 }); 229 } 230 231 private void cleanup(final XMPPConnection connection) { 232 if (transferObject.remove(connection) != null) { 233 inbandTransferManager.cleanup(); 234 } 235 } 236 237 /** 238 * Selects an appropriate stream negotiator after examining the incoming file transfer request. 239 * 240 * @param request The related file transfer request. 241 * @return The file transfer object that handles the transfer 242 * @throws XMPPErrorException If there are either no stream methods contained in the packet, or 243 * there is not an appropriate stream method. 244 * @throws NotConnectedException 245 */ 246 public StreamNegotiator selectStreamNegotiator( 247 FileTransferRequest request) throws XMPPErrorException, NotConnectedException { 248 StreamInitiation si = request.getStreamInitiation(); 249 FormField streamMethodField = getStreamMethodField(si 250 .getFeatureNegotiationForm()); 251 252 if (streamMethodField == null) { 253 String errorMessage = "No stream methods contained in packet."; 254 XMPPError error = new XMPPError(XMPPError.Condition.bad_request, errorMessage); 255 IQ iqPacket = createIQ(si.getPacketID(), si.getFrom(), si.getTo(), 256 IQ.Type.ERROR); 257 iqPacket.setError(error); 258 connection.sendPacket(iqPacket); 259 throw new XMPPErrorException(errorMessage, error); 260 } 261 262 // select the appropriate protocol 263 264 StreamNegotiator selectedStreamNegotiator; 265 try { 266 selectedStreamNegotiator = getNegotiator(streamMethodField); 267 } 268 catch (XMPPErrorException e) { 269 IQ iqPacket = createIQ(si.getPacketID(), si.getFrom(), si.getTo(), 270 IQ.Type.ERROR); 271 iqPacket.setError(e.getXMPPError()); 272 connection.sendPacket(iqPacket); 273 throw e; 274 } 275 276 // return the appropriate negotiator 277 278 return selectedStreamNegotiator; 279 } 280 281 private FormField getStreamMethodField(DataForm form) { 282 for (FormField field : form.getFields()) { 283 if (field.getVariable().equals(STREAM_DATA_FIELD_NAME)) { 284 return field; 285 } 286 } 287 return null; 288 } 289 290 private StreamNegotiator getNegotiator(final FormField field) 291 throws XMPPErrorException { 292 String variable; 293 boolean isByteStream = false; 294 boolean isIBB = false; 295 for (FormField.Option option : field.getOptions()) { 296 variable = option.getValue(); 297 if (variable.equals(Socks5BytestreamManager.NAMESPACE) && !IBB_ONLY) { 298 isByteStream = true; 299 } 300 else if (variable.equals(InBandBytestreamManager.NAMESPACE)) { 301 isIBB = true; 302 } 303 } 304 305 if (!isByteStream && !isIBB) { 306 XMPPError error = new XMPPError(XMPPError.Condition.bad_request, 307 "No acceptable transfer mechanism"); 308 throw new XMPPErrorException(error); 309 } 310 311 //if (isByteStream && isIBB && field.getType().equals(FormField.TYPE_LIST_MULTI)) { 312 if (isByteStream && isIBB) { 313 return new FaultTolerantNegotiator(connection, 314 byteStreamTransferManager, 315 inbandTransferManager); 316 } 317 else if (isByteStream) { 318 return byteStreamTransferManager; 319 } 320 else { 321 return inbandTransferManager; 322 } 323 } 324 325 /** 326 * Returns a new, unique, stream ID to identify a file transfer. 327 * 328 * @return Returns a new, unique, stream ID to identify a file transfer. 329 */ 330 public String getNextStreamID() { 331 StringBuilder buffer = new StringBuilder(); 332 buffer.append(STREAM_INIT_PREFIX); 333 buffer.append(Math.abs(randomGenerator.nextLong())); 334 335 return buffer.toString(); 336 } 337 338 /** 339 * Send a request to another user to send them a file. The other user has 340 * the option of, accepting, rejecting, or not responding to a received file 341 * transfer request. 342 * <p/> 343 * If they accept, the packet will contain the other user's chosen stream 344 * type to send the file across. The two choices this implementation 345 * provides to the other user for file transfer are <a 346 * href="http://www.xmpp.org/extensions/jep-0065.html">SOCKS5 Bytestreams</a>, 347 * which is the preferred method of transfer, and <a 348 * href="http://www.xmpp.org/extensions/jep-0047.html">In-Band Bytestreams</a>, 349 * which is the fallback mechanism. 350 * <p/> 351 * The other user may choose to decline the file request if they do not 352 * desire the file, their client does not support XEP-0096, or if there are 353 * no acceptable means to transfer the file. 354 * <p/> 355 * Finally, if the other user does not respond this method will return null 356 * after the specified timeout. 357 * 358 * @param userID The userID of the user to whom the file will be sent. 359 * @param streamID The unique identifier for this file transfer. 360 * @param fileName The name of this file. Preferably it should include an 361 * extension as it is used to determine what type of file it is. 362 * @param size The size, in bytes, of the file. 363 * @param desc A description of the file. 364 * @param responseTimeout The amount of time, in milliseconds, to wait for the remote 365 * user to respond. If they do not respond in time, this 366 * @return Returns the stream negotiator selected by the peer. 367 * @throws XMPPErrorException Thrown if there is an error negotiating the file transfer. 368 * @throws NotConnectedException 369 */ 370 public StreamNegotiator negotiateOutgoingTransfer(final String userID, 371 final String streamID, final String fileName, final long size, 372 final String desc, int responseTimeout) throws XMPPErrorException, NotConnectedException { 373 StreamInitiation si = new StreamInitiation(); 374 si.setSessionID(streamID); 375 si.setMimeType(URLConnection.guessContentTypeFromName(fileName)); 376 377 StreamInitiation.File siFile = new StreamInitiation.File(fileName, size); 378 siFile.setDesc(desc); 379 si.setFile(siFile); 380 381 si.setFeatureNegotiationForm(createDefaultInitiationForm()); 382 383 si.setFrom(connection.getUser()); 384 si.setTo(userID); 385 si.setType(IQ.Type.SET); 386 387 PacketCollector collector = connection.createPacketCollectorAndSend(si); 388 Packet siResponse = collector.nextResult(responseTimeout); 389 collector.cancel(); 390 391 if (siResponse instanceof IQ) { 392 IQ iqResponse = (IQ) siResponse; 393 if (iqResponse.getType().equals(IQ.Type.RESULT)) { 394 StreamInitiation response = (StreamInitiation) siResponse; 395 return getOutgoingNegotiator(getStreamMethodField(response 396 .getFeatureNegotiationForm())); 397 398 } 399 else { 400 throw new XMPPErrorException(iqResponse.getError()); 401 } 402 } 403 else { 404 return null; 405 } 406 } 407 408 private StreamNegotiator getOutgoingNegotiator(final FormField field) 409 throws XMPPErrorException { 410 boolean isByteStream = false; 411 boolean isIBB = false; 412 for (String variable : field.getValues()) { 413 if (variable.equals(Socks5BytestreamManager.NAMESPACE) && !IBB_ONLY) { 414 isByteStream = true; 415 } 416 else if (variable.equals(InBandBytestreamManager.NAMESPACE)) { 417 isIBB = true; 418 } 419 } 420 421 if (!isByteStream && !isIBB) { 422 XMPPError error = new XMPPError(XMPPError.Condition.bad_request, 423 "No acceptable transfer mechanism"); 424 throw new XMPPErrorException(error); 425 } 426 427 if (isByteStream && isIBB) { 428 return new FaultTolerantNegotiator(connection, 429 byteStreamTransferManager, inbandTransferManager); 430 } 431 else if (isByteStream) { 432 return byteStreamTransferManager; 433 } 434 else { 435 return inbandTransferManager; 436 } 437 } 438 439 private DataForm createDefaultInitiationForm() { 440 DataForm form = new DataForm(Form.TYPE_FORM); 441 FormField field = new FormField(STREAM_DATA_FIELD_NAME); 442 field.setType(FormField.TYPE_LIST_SINGLE); 443 if (!IBB_ONLY) { 444 field.addOption(new FormField.Option(Socks5BytestreamManager.NAMESPACE)); 445 } 446 field.addOption(new FormField.Option(InBandBytestreamManager.NAMESPACE)); 447 form.addField(field); 448 return form; 449 } 450}