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.WeakHashMap; 028 029import org.jivesoftware.smack.Manager; 030import org.jivesoftware.smack.SmackException.NoResponseException; 031import org.jivesoftware.smack.SmackException.NotConnectedException; 032import org.jivesoftware.smack.XMPPConnection; 033import org.jivesoftware.smack.XMPPException.XMPPErrorException; 034import org.jivesoftware.smack.packet.IQ; 035import org.jivesoftware.smack.packet.Stanza; 036import org.jivesoftware.smack.packet.StanzaError; 037 038import org.jivesoftware.smackx.bytestreams.ibb.packet.DataPacketExtension; 039import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream; 040import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; 041import org.jivesoftware.smackx.filetransfer.FileTransferException.NoAcceptableTransferMechanisms; 042import org.jivesoftware.smackx.filetransfer.FileTransferException.NoStreamMethodsOfferedException; 043import org.jivesoftware.smackx.formtypes.FormFieldRegistry; 044import org.jivesoftware.smackx.si.packet.StreamInitiation; 045import org.jivesoftware.smackx.xdata.FormField; 046import org.jivesoftware.smackx.xdata.ListSingleFormField; 047import org.jivesoftware.smackx.xdata.packet.DataForm; 048 049import org.jxmpp.jid.Jid; 050 051/** 052 * Manages the negotiation of file transfers according to XEP-0096. If a file is 053 * being sent the remote user chooses the type of stream under which the file 054 * will be sent. 055 * 056 * @author Alexander Wenckus 057 * @see <a href="http://xmpp.org/extensions/xep-0096.html">XEP-0096: SI File Transfer</a> 058 */ 059public final class FileTransferNegotiator extends Manager { 060 061 public static final String SI_NAMESPACE = "http://jabber.org/protocol/si"; 062 public static final String SI_PROFILE_FILE_TRANSFER_NAMESPACE = "http://jabber.org/protocol/si/profile/file-transfer"; 063 private static final String[] NAMESPACE = { SI_NAMESPACE, SI_PROFILE_FILE_TRANSFER_NAMESPACE }; 064 065 private static final Map<XMPPConnection, FileTransferNegotiator> INSTANCES = new WeakHashMap<>(); 066 067 private static final String STREAM_INIT_PREFIX = "jsi_"; 068 069 protected static final String STREAM_DATA_FIELD_NAME = "stream-method"; 070 static { 071 FormFieldRegistry.addLookasideFieldRegistryEntry(STREAM_DATA_FIELD_NAME, FormField.Type.list_single); 072 } 073 074 private static final Random randomGenerator = new Random(); 075 076 /** 077 * A static variable to use only offer IBB for file transfer. It is generally recommend to only 078 * set this variable to true for testing purposes as IBB is the backup file transfer method 079 * and shouldn't be used as the only transfer method in production systems. 080 */ 081 public static boolean IBB_ONLY = System.getProperty("ibb") != null;//true; 082 083 /** 084 * Returns the file transfer negotiator related to a particular connection. 085 * When this class is requested on a particular connection the file transfer 086 * service is automatically enabled. 087 * 088 * @param connection The connection for which the transfer manager is desired 089 * @return The FileTransferNegotiator 090 */ 091 public static synchronized FileTransferNegotiator getInstanceFor( 092 final XMPPConnection connection) { 093 FileTransferNegotiator fileTransferNegotiator = INSTANCES.get(connection); 094 if (fileTransferNegotiator == null) { 095 fileTransferNegotiator = new FileTransferNegotiator(connection); 096 INSTANCES.put(connection, fileTransferNegotiator); 097 } 098 return fileTransferNegotiator; 099 } 100 101 /** 102 * Enable the Jabber services related to file transfer on the particular 103 * connection. 104 * 105 * @param connection The connection on which to enable or disable the services. 106 * @param isEnabled True to enable, false to disable. 107 */ 108 private static void setServiceEnabled(final XMPPConnection connection, 109 final boolean isEnabled) { 110 ServiceDiscoveryManager manager = ServiceDiscoveryManager 111 .getInstanceFor(connection); 112 113 List<String> namespaces = new ArrayList<>(); 114 namespaces.addAll(Arrays.asList(NAMESPACE)); 115 namespaces.add(DataPacketExtension.NAMESPACE); 116 if (!IBB_ONLY) { 117 namespaces.add(Bytestream.NAMESPACE); 118 } 119 120 for (String namespace : namespaces) { 121 if (isEnabled) { 122 manager.addFeature(namespace); 123 } else { 124 manager.removeFeature(namespace); 125 } 126 } 127 } 128 129 /** 130 * Checks to see if all file transfer related services are enabled on the 131 * connection. 132 * 133 * @param connection The connection to check 134 * @return True if all related services are enabled, false if they are not. 135 */ 136 public static boolean isServiceEnabled(final XMPPConnection connection) { 137 ServiceDiscoveryManager manager = ServiceDiscoveryManager 138 .getInstanceFor(connection); 139 140 List<String> namespaces = new ArrayList<>(); 141 namespaces.addAll(Arrays.asList(NAMESPACE)); 142 namespaces.add(DataPacketExtension.NAMESPACE); 143 if (!IBB_ONLY) { 144 namespaces.add(Bytestream.NAMESPACE); 145 } 146 147 for (String namespace : namespaces) { 148 if (!manager.includesFeature(namespace)) { 149 return false; 150 } 151 } 152 return true; 153 } 154 155 /** 156 * Returns a collection of the supported transfer protocols. 157 * 158 * @return Returns a collection of the supported transfer protocols. 159 */ 160 public static Collection<String> getSupportedProtocols() { 161 List<String> protocols = new ArrayList<>(); 162 protocols.add(DataPacketExtension.NAMESPACE); 163 if (!IBB_ONLY) { 164 protocols.add(Bytestream.NAMESPACE); 165 } 166 return Collections.unmodifiableList(protocols); 167 } 168 169 // non-static 170 171 private final StreamNegotiator byteStreamTransferManager; 172 173 private final StreamNegotiator inbandTransferManager; 174 175 private FileTransferNegotiator(final XMPPConnection connection) { 176 super(connection); 177 byteStreamTransferManager = new Socks5TransferNegotiator(connection); 178 inbandTransferManager = new IBBTransferNegotiator(connection); 179 180 setServiceEnabled(connection, true); 181 } 182 183 /** 184 * Selects an appropriate stream negotiator after examining the incoming file transfer request. 185 * 186 * @param request The related file transfer request. 187 * @return The file transfer object that handles the transfer 188 * @throws NoStreamMethodsOfferedException If there are either no stream methods contained in the packet, or 189 * there is not an appropriate stream method. 190 * @throws NotConnectedException if the XMPP connection is not connected. 191 * @throws NoAcceptableTransferMechanisms if no acceptable transfer mechanisms are available 192 * @throws InterruptedException if the calling thread was interrupted. 193 */ 194 public StreamNegotiator selectStreamNegotiator( 195 FileTransferRequest request) throws NotConnectedException, NoStreamMethodsOfferedException, NoAcceptableTransferMechanisms, InterruptedException { 196 StreamInitiation si = request.getStreamInitiation(); 197 ListSingleFormField streamMethodField = getStreamMethodField(si 198 .getFeatureNegotiationForm()); 199 200 if (streamMethodField == null) { 201 String errorMessage = "No stream methods contained in stanza."; 202 StanzaError error = StanzaError.from(StanzaError.Condition.bad_request, errorMessage).build(); 203 IQ iqPacket = IQ.createErrorResponse(si, error); 204 connection().sendStanza(iqPacket); 205 throw new FileTransferException.NoStreamMethodsOfferedException(); 206 } 207 208 // select the appropriate protocol 209 StreamNegotiator selectedStreamNegotiator; 210 try { 211 selectedStreamNegotiator = getNegotiator(streamMethodField); 212 } 213 catch (NoAcceptableTransferMechanisms e) { 214 IQ iqPacket = IQ.createErrorResponse(si, StanzaError.from(StanzaError.Condition.bad_request, "No acceptable transfer mechanism").build()); 215 connection().sendStanza(iqPacket); 216 throw e; 217 } 218 219 // return the appropriate negotiator 220 221 return selectedStreamNegotiator; 222 } 223 224 private static ListSingleFormField getStreamMethodField(DataForm form) { 225 return (ListSingleFormField) form.getField(STREAM_DATA_FIELD_NAME); 226 } 227 228 private StreamNegotiator getNegotiator(final ListSingleFormField field) 229 throws NoAcceptableTransferMechanisms { 230 String variable; 231 boolean isByteStream = false; 232 boolean isIBB = false; 233 for (FormField.Option option : field.getOptions()) { 234 variable = option.getValueString(); 235 if (variable.equals(Bytestream.NAMESPACE) && !IBB_ONLY) { 236 isByteStream = true; 237 } 238 else if (variable.equals(DataPacketExtension.NAMESPACE)) { 239 isIBB = true; 240 } 241 } 242 243 if (!isByteStream && !isIBB) { 244 throw new FileTransferException.NoAcceptableTransferMechanisms(); 245 } 246 247 if (isByteStream) { 248 return byteStreamTransferManager; 249 } 250 else { 251 return inbandTransferManager; 252 } 253 } 254 255 /** 256 * Returns a new, unique, stream ID to identify a file transfer. 257 * 258 * @return Returns a new, unique, stream ID to identify a file transfer. 259 */ 260 public static String getNextStreamID() { 261 StringBuilder buffer = new StringBuilder(); 262 buffer.append(STREAM_INIT_PREFIX); 263 buffer.append(randomGenerator.nextInt(Integer.MAX_VALUE) + randomGenerator.nextInt(Integer.MAX_VALUE)); 264 265 return buffer.toString(); 266 } 267 268 /** 269 * Send a request to another user to send them a file. The other user has 270 * the option of, accepting, rejecting, or not responding to a received file 271 * transfer request. 272 * <p> 273 * If they accept, the stanza will contain the other user's chosen stream 274 * type to send the file across. The two choices this implementation 275 * provides to the other user for file transfer are <a 276 * href="http://www.xmpp.org/extensions/jep-0065.html">SOCKS5 Bytestreams</a>, 277 * which is the preferred method of transfer, and <a 278 * href="http://www.xmpp.org/extensions/jep-0047.html">In-Band Bytestreams</a>, 279 * which is the fallback mechanism. 280 * </p> 281 * <p> 282 * The other user may choose to decline the file request if they do not 283 * desire the file, their client does not support XEP-0096, or if there are 284 * no acceptable means to transfer the file. 285 * </p> 286 * Finally, if the other user does not respond this method will return null 287 * after the specified timeout. 288 * 289 * @param userID The userID of the user to whom the file will be sent. 290 * @param streamID The unique identifier for this file transfer. 291 * @param fileName The name of this file. Preferably it should include an 292 * extension as it is used to determine what type of file it is. 293 * @param size The size, in bytes, of the file. 294 * @param desc A description of the file. 295 * @param responseTimeout The amount of time, in milliseconds, to wait for the remote 296 * user to respond. If they do not respond in time, this 297 * @return Returns the stream negotiator selected by the peer. 298 * @throws XMPPErrorException Thrown if there is an error negotiating the file transfer. 299 * @throws NotConnectedException if the XMPP connection is not connected. 300 * @throws NoResponseException if there was no response from the remote entity. 301 * @throws NoAcceptableTransferMechanisms if no acceptable transfer mechanisms are available 302 * @throws InterruptedException if the calling thread was interrupted. 303 */ 304 public StreamNegotiator negotiateOutgoingTransfer(final Jid userID, 305 final String streamID, final String fileName, final long size, 306 final String desc, int responseTimeout) throws XMPPErrorException, NotConnectedException, NoResponseException, NoAcceptableTransferMechanisms, InterruptedException { 307 StreamInitiation si = new StreamInitiation(); 308 si.setSessionID(streamID); 309 si.setMimeType(URLConnection.guessContentTypeFromName(fileName)); 310 311 StreamInitiation.File siFile = new StreamInitiation.File(fileName, size); 312 siFile.setDesc(desc); 313 si.setFile(siFile); 314 315 si.setFeatureNegotiationForm(createDefaultInitiationForm()); 316 317 si.setFrom(connection().getUser()); 318 si.setTo(userID); 319 si.setType(IQ.Type.set); 320 321 Stanza siResponse = connection().createStanzaCollectorAndSend(si).nextResultOrThrow( 322 responseTimeout); 323 324 if (siResponse instanceof IQ) { 325 IQ iqResponse = (IQ) siResponse; 326 if (iqResponse.getType().equals(IQ.Type.result)) { 327 StreamInitiation response = (StreamInitiation) siResponse; 328 return getOutgoingNegotiator(getStreamMethodField(response 329 .getFeatureNegotiationForm())); 330 331 } 332 else { 333 throw new XMPPErrorException(iqResponse, iqResponse.getError()); 334 } 335 } 336 else { 337 return null; 338 } 339 } 340 341 private StreamNegotiator getOutgoingNegotiator(final FormField field) throws NoAcceptableTransferMechanisms { 342 boolean isByteStream = false; 343 boolean isIBB = false; 344 for (CharSequence variable : field.getValues()) { 345 String variableString = variable.toString(); 346 if (variableString.equals(Bytestream.NAMESPACE) && !IBB_ONLY) { 347 isByteStream = true; 348 } 349 else if (variableString.equals(DataPacketExtension.NAMESPACE)) { 350 isIBB = true; 351 } 352 } 353 354 if (!isByteStream && !isIBB) { 355 throw new FileTransferException.NoAcceptableTransferMechanisms(); 356 } 357 358 if (isByteStream) { 359 return byteStreamTransferManager; 360 } 361 else { 362 return inbandTransferManager; 363 } 364 } 365 366 private static DataForm createDefaultInitiationForm() { 367 DataForm.Builder form = DataForm.builder(DataForm.Type.form); 368 ListSingleFormField.Builder fieldBuilder = FormField.listSingleBuilder(STREAM_DATA_FIELD_NAME); 369 370 if (!IBB_ONLY) { 371 fieldBuilder.addOption(Bytestream.NAMESPACE); 372 } 373 fieldBuilder.addOption(DataPacketExtension.NAMESPACE); 374 form.addField(fieldBuilder.build()); 375 return form.build(); 376 } 377}