Fix and improve the HTTP File Upload implementation

Fix a few resource leaks. Improve the API and add an integration
test. Also add compability layer for XEP-0363: HTTP File Upload 0.2.

SMACK-747
This commit is contained in:
Florian Schmaus 2017-03-09 21:35:29 +01:00
parent 72d4c8b611
commit 09b6608a3a
26 changed files with 691 additions and 177 deletions

View File

@ -235,4 +235,19 @@ public class ParserUtils {
return uri; return uri;
} }
public static String getRequiredAttribute(XmlPullParser parser, String name) throws IOException {
String value = parser.getAttributeValue("", name);
if (StringUtils.isNullOrEmpty(value)) {
throw new IOException("Attribute " + name + " is null or empty (" + value + ')');
}
return value;
}
public static String getRequiredNextText(XmlPullParser parser) throws XmlPullParserException, IOException {
String text = parser.nextText();
if (StringUtils.isNullOrEmpty(text)) {
throw new IOException("Next text is null or empty (" + text + ')');
}
return text;
}
} }

View File

@ -17,6 +17,7 @@
package org.jivesoftware.smackx.httpfileupload; package org.jivesoftware.smackx.httpfileupload;
import org.jivesoftware.smack.AbstractConnectionListener; import org.jivesoftware.smack.AbstractConnectionListener;
import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.ConnectionCreationListener; import org.jivesoftware.smack.ConnectionCreationListener;
import org.jivesoftware.smack.Manager; import org.jivesoftware.smack.Manager;
import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.SmackException;
@ -25,8 +26,10 @@ import org.jivesoftware.smack.XMPPConnectionRegistry;
import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
import org.jivesoftware.smackx.disco.packet.DiscoverInfo; import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
import org.jivesoftware.smackx.httpfileupload.UploadService.Version;
import org.jivesoftware.smackx.httpfileupload.element.Slot; import org.jivesoftware.smackx.httpfileupload.element.Slot;
import org.jivesoftware.smackx.httpfileupload.element.SlotRequest; import org.jivesoftware.smackx.httpfileupload.element.SlotRequest;
import org.jivesoftware.smackx.httpfileupload.element.SlotRequest_V0_2;
import org.jivesoftware.smackx.xdata.FormField; import org.jivesoftware.smackx.xdata.FormField;
import org.jivesoftware.smackx.xdata.packet.DataForm; import org.jivesoftware.smackx.xdata.packet.DataForm;
import org.jxmpp.jid.DomainBareJid; import org.jxmpp.jid.DomainBareJid;
@ -34,25 +37,34 @@ import org.jxmpp.jid.DomainBareJid;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry;
import java.util.WeakHashMap; import java.util.WeakHashMap;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
/** /**
* HTTP File Upload manager class. * A manager for XEP-0363: HTTP File Upload.
* XEP version 0.2.5
* *
* @author Grigory Fedorov * @author Grigory Fedorov
* @author Florian Schmaus
* @see <a href="http://xmpp.org/extensions/xep-0363.html">XEP-0363: HTTP File Upload</a> * @see <a href="http://xmpp.org/extensions/xep-0363.html">XEP-0363: HTTP File Upload</a>
*/ */
public final class HttpFileUploadManager extends Manager { public final class HttpFileUploadManager extends Manager {
public static final String NAMESPACE = "urn:xmpp:http:upload:0";
public static final String NAMESPACE_0_2 = "urn:xmpp:http:upload";
private static final Logger LOGGER = Logger.getLogger(HttpFileUploadManager.class.getName()); private static final Logger LOGGER = Logger.getLogger(HttpFileUploadManager.class.getName());
static { static {
@ -65,8 +77,10 @@ public final class HttpFileUploadManager extends Manager {
} }
private static final Map<XMPPConnection, HttpFileUploadManager> INSTANCES = new WeakHashMap<>(); private static final Map<XMPPConnection, HttpFileUploadManager> INSTANCES = new WeakHashMap<>();
private DomainBareJid defaultUploadService;
private Long maxFileSize; private UploadService defaultUploadService;
private SSLSocketFactory tlsSocketFactory;
/** /**
* Obtain the HttpFileUploadManager responsible for a connection. * Obtain the HttpFileUploadManager responsible for a connection.
@ -106,6 +120,40 @@ public final class HttpFileUploadManager extends Manager {
}); });
} }
private static UploadService uploadServiceFrom(DiscoverInfo discoverInfo) {
assert(containsHttpFileUploadNamespace(discoverInfo));
UploadService.Version version;
if (discoverInfo.containsFeature(NAMESPACE)) {
version = Version.v0_3;
} else if (discoverInfo.containsFeature(NAMESPACE_0_2)) {
version = Version.v0_2;
} else {
throw new AssertionError();
}
DomainBareJid address = discoverInfo.getFrom().asDomainBareJid();
DataForm dataForm = DataForm.from(discoverInfo);
if (dataForm == null) {
return new UploadService(address, version);
}
FormField field = dataForm.getField("max-file-size");
if (field == null) {
return new UploadService(address, version);
}
List<String> values = field.getValues();
if (values.isEmpty()) {
return new UploadService(address, version);
}
Long maxFileSize = Long.valueOf(values.get(0));
return new UploadService(address, version, maxFileSize);
}
/** /**
* Discover upload service. * Discover upload service.
* *
@ -122,39 +170,20 @@ public final class HttpFileUploadManager extends Manager {
*/ */
public boolean discoverUploadService() throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, public boolean discoverUploadService() throws XMPPException.XMPPErrorException, SmackException.NotConnectedException,
InterruptedException, SmackException.NoResponseException { InterruptedException, SmackException.NoResponseException {
defaultUploadService = null; ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection());
maxFileSize = null; List<DiscoverInfo> servicesDiscoverInfo = sdm
.findServicesDiscoverInfo(NAMESPACE, true, true);
List<DiscoverInfo> servicesDiscoverInfo = ServiceDiscoveryManager.getInstanceFor(connection())
.findServicesDiscoverInfo(SlotRequest.NAMESPACE, true, false);
if (servicesDiscoverInfo.isEmpty()) { if (servicesDiscoverInfo.isEmpty()) {
return false; servicesDiscoverInfo = sdm.findServicesDiscoverInfo(NAMESPACE_0_2, true, true);
if (servicesDiscoverInfo.isEmpty()) {
return false;
}
} }
DiscoverInfo discoverInfo = servicesDiscoverInfo.get(0); DiscoverInfo discoverInfo = servicesDiscoverInfo.get(0);
defaultUploadService = discoverInfo.getFrom().asDomainBareJid(); defaultUploadService = uploadServiceFrom(discoverInfo);
if (defaultUploadService == null) {
return false;
}
DataForm dataForm = DataForm.from(discoverInfo);
if (dataForm == null) {
return true;
}
FormField field = dataForm.getField("max-file-size");
if (field == null) {
return true;
}
List<String> values = field.getValues();
if (!values.isEmpty()) {
maxFileSize = Long.valueOf(values.get(0));
}
return true; return true;
} }
@ -172,19 +201,10 @@ public final class HttpFileUploadManager extends Manager {
* *
* @return upload service JID or null if not available * @return upload service JID or null if not available
*/ */
public DomainBareJid getDefaultUploadService() { public UploadService getDefaultUploadService() {
return defaultUploadService; return defaultUploadService;
} }
/**
* Get max file size allowed by upload service.
*
* @return max file size in bytes or null if not available
*/
public Long getMaxFileSize() {
return maxFileSize;
}
/** /**
* Request slot and uploaded file to HTTP file upload service. * Request slot and uploaded file to HTTP file upload service.
* *
@ -203,19 +223,6 @@ public final class HttpFileUploadManager extends Manager {
return uploadFile(file, null); return uploadFile(file, null);
} }
/**
* Callback interface to get upload progress.
*/
public interface UploadProgressListener {
/**
* Callback for displaying upload progress.
*
* @param uploadedBytes - number of bytes uploaded at the moment
* @param totalBytes - total number of bytes to be uploaded
*/
void onUploadProgress(long uploadedBytes, long totalBytes);
}
/** /**
* Request slot and uploaded file to HTTP file upload service with progress callback. * Request slot and uploaded file to HTTP file upload service with progress callback.
* *
@ -233,27 +240,26 @@ public final class HttpFileUploadManager extends Manager {
*/ */
public URL uploadFile(File file, UploadProgressListener listener) throws InterruptedException, public URL uploadFile(File file, UploadProgressListener listener) throws InterruptedException,
XMPPException.XMPPErrorException, SmackException, IOException { XMPPException.XMPPErrorException, SmackException, IOException {
if (!file.isFile()) {
throw new FileNotFoundException("The path " + file.getAbsolutePath() + " is not a file");
}
final Slot slot = requestSlot(file.getName(), file.length(), "application/octet-stream"); final Slot slot = requestSlot(file.getName(), file.length(), "application/octet-stream");
uploadFile(file, slot.getPutUrl(), listener); uploadFile(file, slot, listener);
return slot.getGetUrl(); return slot.getGetUrl();
} }
/** /**
* Request a new upload slot from default upload service (if discovered). * Request a new upload slot from default upload service (if discovered). When you get slot you should upload file
* * to PUT URL and share GET URL. Note that this is a synchronous call -- Smack must wait for the server response.
* When you get slot you should upload file to PUT URL and share GET URL.
* Note that this is a synchronous call -- Smack must wait for the server response.
* *
* @param filename name of file to be uploaded * @param filename name of file to be uploaded
* @param fileSize file size in bytes -- must be less or equal * @param fileSize file size in bytes.
* to {@link HttpFileUploadManager#getMaxFileSize()} (if available)
* @return file upload Slot in case of success * @return file upload Slot in case of success
* @throws IllegalArgumentException if fileSize is less than or equal to zero or greater than the maximum size
* @throws IllegalArgumentException if fileSize is less than or equal to zero * supported by the service.
* or greater than {@link HttpFileUploadManager#getMaxFileSize()}
* @throws InterruptedException * @throws InterruptedException
* @throws XMPPException.XMPPErrorException * @throws XMPPException.XMPPErrorException
* @throws SmackException.NotConnectedException * @throws SmackException.NotConnectedException
@ -271,13 +277,12 @@ public final class HttpFileUploadManager extends Manager {
* Note that this is a synchronous call -- Smack must wait for the server response. * Note that this is a synchronous call -- Smack must wait for the server response.
* *
* @param filename name of file to be uploaded * @param filename name of file to be uploaded
* @param fileSize file size in bytes -- must be less or equal * @param fileSize file size in bytes.
* to {@link HttpFileUploadManager#getMaxFileSize()} (if available)
* @param contentType file content-type or null * @param contentType file content-type or null
* @return file upload Slot in case of success * @return file upload Slot in case of success
* @throws IllegalArgumentException if fileSize is less than or equal to zero * @throws IllegalArgumentException if fileSize is less than or equal to zero or greater than the maximum size
* or greater than {@link HttpFileUploadManager#getMaxFileSize()} * supported by the service.
* @throws SmackException.NotConnectedException * @throws SmackException.NotConnectedException
* @throws InterruptedException * @throws InterruptedException
* @throws XMPPException.XMPPErrorException * @throws XMPPException.XMPPErrorException
@ -295,81 +300,164 @@ public final class HttpFileUploadManager extends Manager {
* Note that this is a synchronous call -- Smack must wait for the server response. * Note that this is a synchronous call -- Smack must wait for the server response.
* *
* @param filename name of file to be uploaded * @param filename name of file to be uploaded
* @param fileSize file size in bytes -- must be less or equal * @param fileSize file size in bytes.
* to {@link HttpFileUploadManager#getMaxFileSize()} (if available)
* @param contentType file content-type or null * @param contentType file content-type or null
* @param uploadService upload service to use or null for default one * @param uploadServiceAddress the address of the upload service to use or null for default one
* @return file upload Slot in case of success * @return file upload Slot in case of success
* @throws IllegalArgumentException if fileSize is less than or equal to zero * @throws IllegalArgumentException if fileSize is less than or equal to zero or greater than the maximum size
* or greater than {@link HttpFileUploadManager#getMaxFileSize()} * supported by the service.
* @throws SmackException * @throws SmackException
* @throws InterruptedException * @throws InterruptedException
* @throws XMPPException.XMPPErrorException * @throws XMPPException.XMPPErrorException
*/ */
public Slot requestSlot(String filename, long fileSize, String contentType, DomainBareJid uploadService) public Slot requestSlot(String filename, long fileSize, String contentType, DomainBareJid uploadServiceAddress)
throws SmackException, InterruptedException, XMPPException.XMPPErrorException { throws SmackException, InterruptedException, XMPPException.XMPPErrorException {
if (defaultUploadService == null && uploadService == null) { final XMPPConnection connection = connection();
throw new SmackException("No upload service specified or discovered."); final UploadService defaultUploadService = this.defaultUploadService;
}
if (uploadService == null && maxFileSize != null) { // The upload service we are going to use.
if (fileSize > maxFileSize) { UploadService uploadService;
throw new IllegalArgumentException("Requested file size " + fileSize
+ " is greater than max allowed size " + maxFileSize); if (uploadServiceAddress == null) {
uploadService = defaultUploadService;
} else {
if (defaultUploadService != null && defaultUploadService.getAddress().equals(uploadServiceAddress)) {
// Avoid performing a service discovery if we already know about the given service.
uploadService = defaultUploadService;
} else {
DiscoverInfo discoverInfo = ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(uploadServiceAddress);
if (!containsHttpFileUploadNamespace(discoverInfo)) {
throw new IllegalArgumentException("There is no HTTP upload service running at the given address '"
+ uploadServiceAddress + '\'');
}
uploadService = uploadServiceFrom(discoverInfo);
} }
} }
SlotRequest slotRequest = new SlotRequest(filename, fileSize, contentType); if (uploadService == null) {
if (uploadService != null) { throw new SmackException("No upload service specified and also none discovered.");
slotRequest.setTo(uploadService);
} else {
slotRequest.setTo(defaultUploadService);
} }
return connection().createStanzaCollectorAndSend(slotRequest).nextResultOrThrow(); if (!uploadService.acceptsFileOfSize(fileSize)) {
throw new IllegalArgumentException(
"Requested file size " + fileSize + " is greater than max allowed size " + uploadService.getMaxFileSize());
}
SlotRequest slotRequest;
switch (uploadService.getVersion()) {
case v0_3:
slotRequest = new SlotRequest(uploadService.getAddress(), filename, fileSize, contentType);
break;
case v0_2:
slotRequest = new SlotRequest_V0_2(uploadService.getAddress(), filename, fileSize, contentType);
break;
default:
throw new AssertionError();
}
return connection.createStanzaCollectorAndSend(slotRequest).nextResultOrThrow();
} }
private void uploadFile(File file, URL putUrl, UploadProgressListener listener) throws IOException { public void setTlsContext(SSLContext tlsContext) {
if (tlsContext == null) {
return;
}
this.tlsSocketFactory = tlsContext.getSocketFactory();
}
public void useTlsSettingsFrom(ConnectionConfiguration connectionConfiguration) {
SSLContext sslContext = connectionConfiguration.getCustomSSLContext();
setTlsContext(sslContext);
}
private void uploadFile(final File file, final Slot slot, UploadProgressListener listener) throws IOException {
final long fileSize = file.length();
// TODO Remove once Smack's minimum Android API level is 19 or higher. See also comment below.
if (fileSize >= Integer.MAX_VALUE) {
throw new IllegalArgumentException("File size " + fileSize + " must be less than " + Integer.MAX_VALUE);
}
final int fileSizeInt = (int) fileSize;
// Construct the FileInputStream first to make sure we can actually read the file.
final FileInputStream fis = new FileInputStream(file);
final URL putUrl = slot.getPutUrl();
final HttpURLConnection urlConnection = (HttpURLConnection) putUrl.openConnection(); final HttpURLConnection urlConnection = (HttpURLConnection) putUrl.openConnection();
urlConnection.setRequestMethod("PUT"); urlConnection.setRequestMethod("PUT");
urlConnection.setUseCaches(false); urlConnection.setUseCaches(false);
urlConnection.setDoOutput(true); urlConnection.setDoOutput(true);
// TODO Change to using fileSize once Smack's minimum Android API level is 19 or higher.
urlConnection.setFixedLengthStreamingMode(fileSizeInt);
urlConnection.setRequestProperty("Content-Type", "application/octet-stream;"); urlConnection.setRequestProperty("Content-Type", "application/octet-stream;");
OutputStream outputStream = urlConnection.getOutputStream(); for (Entry<String, String> header : slot.getHeaders().entrySet()) {
urlConnection.setRequestProperty(header.getKey(), header.getValue());
long bytesSend = 0;
long fileSize = file.length();
if (listener != null) {
listener.onUploadProgress(0, fileSize);
} }
BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(file)); final SSLSocketFactory tlsSocketFactory = this.tlsSocketFactory;
if (tlsSocketFactory != null && urlConnection instanceof HttpsURLConnection) {
HttpsURLConnection httpsUrlConnection = (HttpsURLConnection) urlConnection;
httpsUrlConnection.setSSLSocketFactory(tlsSocketFactory);
}
byte[] buffer = new byte[4096]; try {
int bytesRead; OutputStream outputStream = urlConnection.getOutputStream();
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead); long bytesSend = 0;
outputStream.flush();
bytesSend += bytesRead;
if (listener != null) { if (listener != null) {
listener.onUploadProgress(bytesSend, fileSize); listener.onUploadProgress(0, fileSize);
} }
BufferedInputStream inputStream = new BufferedInputStream(fis);
// TODO Factor in extra static method (and re-use e.g. in bytestream code).
byte[] buffer = new byte[4096];
int bytesRead;
try {
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
bytesSend += bytesRead;
if (listener != null) {
listener.onUploadProgress(bytesSend, fileSize);
}
}
}
finally {
try {
inputStream.close();
}
catch (IOException e) {
LOGGER.log(Level.WARNING, "Exception while closing input stream", e);
}
try {
outputStream.close();
}
catch (IOException e) {
LOGGER.log(Level.WARNING, "Exception while closing output stream", e);
}
}
int status = urlConnection.getResponseCode();
switch (status) {
case HttpURLConnection.HTTP_OK:
case HttpURLConnection.HTTP_CREATED:
case HttpURLConnection.HTTP_NO_CONTENT:
break;
default:
throw new IOException("Error response " + status + " from server during file upload: "
+ urlConnection.getResponseMessage() + ", file size: " + fileSize + ", put URL: "
+ putUrl);
}
} }
finally {
inputStream.close(); urlConnection.disconnect();
outputStream.close();
int status = urlConnection.getResponseCode();
if (status != HttpURLConnection.HTTP_CREATED) {
throw new IOException("Error response from server during file upload:"
+ " " + urlConnection.getResponseCode()
+ " " + urlConnection.getResponseMessage()
+ ", file size: " + fileSize
+ ", put URL: " + putUrl);
} }
} }
private static boolean containsHttpFileUploadNamespace(DiscoverInfo discoverInfo) {
return discoverInfo.containsFeature(NAMESPACE) || discoverInfo.containsFeature(NAMESPACE_0_2);
}
} }

View File

@ -0,0 +1,32 @@
/**
*
* Copyright © 2017 Grigory Fedorov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.httpfileupload;
/**
* Callback interface to get upload progress.
*/
public interface UploadProgressListener {
/**
* Callback for displaying upload progress.
*
* @param uploadedBytes the number of bytes uploaded at the moment
* @param totalBytes the total number of bytes to be uploaded
*/
void onUploadProgress(long uploadedBytes, long totalBytes);
}

View File

@ -0,0 +1,66 @@
/**
*
* Copyright © 2017 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.httpfileupload;
import org.jivesoftware.smack.util.Objects;
import org.jxmpp.jid.DomainBareJid;
public class UploadService {
enum Version {
v0_2,
v0_3,
};
private final DomainBareJid address;
private final Version version;
private final Long maxFileSize;
UploadService(DomainBareJid address, Version version) {
this(address, version, null);
}
UploadService(DomainBareJid address, Version version, Long maxFileSize) {
this.address = Objects.requireNonNull(address);
this.version = version;
this.maxFileSize = maxFileSize;
}
public DomainBareJid getAddress() {
return address;
}
public Version getVersion() {
return version;
}
public boolean hasMaxFileSizeLimit() {
return maxFileSize != null;
}
public Long getMaxFileSize() {
return maxFileSize;
}
public boolean acceptsFileOfSize(long size) {
if (!hasMaxFileSizeLimit()) {
return true;
}
return size <= maxFileSize;
}
}

View File

@ -32,9 +32,15 @@ public class FileTooLargeError implements ExtensionElement {
public static final String NAMESPACE = SlotRequest.NAMESPACE; public static final String NAMESPACE = SlotRequest.NAMESPACE;
private final long maxFileSize; private final long maxFileSize;
private final String namespace;
public FileTooLargeError(long maxFileSize) { public FileTooLargeError(long maxFileSize) {
this(maxFileSize, NAMESPACE);
}
protected FileTooLargeError(long maxFileSize, String namespace) {
this.maxFileSize = maxFileSize; this.maxFileSize = maxFileSize;
this.namespace = namespace;
} }
public long getMaxFileSize() { public long getMaxFileSize() {
@ -48,15 +54,15 @@ public class FileTooLargeError implements ExtensionElement {
@Override @Override
public String getNamespace() { public String getNamespace() {
return NAMESPACE; return namespace;
} }
@Override @Override
public CharSequence toXML() { public XmlStringBuilder toXML() {
XmlStringBuilder xml = new XmlStringBuilder(this); XmlStringBuilder xml = new XmlStringBuilder(this);
xml.rightAngleBracket(); xml.rightAngleBracket();
xml.element("max-file-size", String.valueOf(maxFileSize)); xml.element("max-file-size", String.valueOf(maxFileSize));
xml.closeElement(ELEMENT); xml.closeElement(this);
return xml; return xml;
} }

View File

@ -0,0 +1,30 @@
/**
*
* Copyright 2017 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.httpfileupload.element;
import org.jivesoftware.smackx.httpfileupload.HttpFileUploadManager;
public class FileTooLargeError_V0_2 extends FileTooLargeError {
public static final String NAMESPACE = HttpFileUploadManager.NAMESPACE_0_2;
public FileTooLargeError_V0_2(long maxFileSize) {
super(maxFileSize, NAMESPACE);
}
}

View File

@ -19,6 +19,8 @@ package org.jivesoftware.smackx.httpfileupload.element;
import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.IQ;
import java.net.URL; import java.net.URL;
import java.util.Collections;
import java.util.Map;
/** /**
* Slot responded by upload service. * Slot responded by upload service.
@ -33,12 +35,26 @@ public class Slot extends IQ {
private final URL putUrl; private final URL putUrl;
private final URL getUrl; private final URL getUrl;
private final Map<String, String> headers;
public Slot(URL putUrl, URL getUrl) { public Slot(URL putUrl, URL getUrl) {
super(ELEMENT, NAMESPACE); this(putUrl, getUrl, null);
}
public Slot(URL putUrl, URL getUrl, Map<String, String> headers) {
this(putUrl, getUrl, headers, NAMESPACE);
}
protected Slot(URL putUrl, URL getUrl, Map<String, String> headers, String namespace) {
super(ELEMENT, namespace);
setType(Type.result); setType(Type.result);
this.putUrl = putUrl; this.putUrl = putUrl;
this.getUrl = getUrl; this.getUrl = getUrl;
if (headers == null) {
this.headers = Collections.emptyMap();
} else {
this.headers = Collections.unmodifiableMap(headers);
}
} }
public URL getPutUrl() { public URL getPutUrl() {
@ -49,6 +65,9 @@ public class Slot extends IQ {
return getUrl; return getUrl;
} }
public Map<String, String> getHeaders() {
return headers;
}
@Override @Override
protected IQChildElementXmlStringBuilder getIQChildElementBuilder(IQChildElementXmlStringBuilder xml) { protected IQChildElementXmlStringBuilder getIQChildElementBuilder(IQChildElementXmlStringBuilder xml) {
@ -56,6 +75,9 @@ public class Slot extends IQ {
xml.element("put", putUrl.toString()); xml.element("put", putUrl.toString());
xml.element("get", getUrl.toString()); xml.element("get", getUrl.toString());
for (Map.Entry<String, String> entry : getHeaders().entrySet()) {
xml.openElement("header").attribute(entry.getKey(), entry.getValue());
}
return xml; return xml;
} }

View File

@ -17,6 +17,8 @@
package org.jivesoftware.smackx.httpfileupload.element; package org.jivesoftware.smackx.httpfileupload.element;
import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smackx.httpfileupload.HttpFileUploadManager;
import org.jxmpp.jid.DomainBareJid;
/** /**
* Upload slot request. * Upload slot request.
@ -26,23 +28,31 @@ import org.jivesoftware.smack.packet.IQ;
*/ */
public class SlotRequest extends IQ { public class SlotRequest extends IQ {
public static final String ELEMENT = "request"; public static final String ELEMENT = "request";
public static final String NAMESPACE = "urn:xmpp:http:upload"; public static final String NAMESPACE = HttpFileUploadManager.NAMESPACE;
private final String filename; private final String filename;
private final long size; private final long size;
private final String contentType; private final String contentType;
public SlotRequest(DomainBareJid uploadServiceAddress, String filename, long size) {
this(uploadServiceAddress, filename, size, null);
}
/** /**
* Create new slot request. * Create new slot request.
* *
* @throws IllegalArgumentException if size is less than or equal to zero * @param uploadServiceAddress the XMPP address of the service to request the slot from.
* @param filename name of file * @param filename name of file
* @param size size of file in bytes * @param size size of file in bytes
* @param contentType file content type or null * @param contentType file content type or null
* @throws IllegalArgumentException if size is less than or equal to zero
*/ */
public SlotRequest(String filename, long size, String contentType) { public SlotRequest(DomainBareJid uploadServiceAddress, String filename, long size, String contentType) {
super(ELEMENT, NAMESPACE); this(uploadServiceAddress, filename, size, contentType, NAMESPACE);
}
protected SlotRequest(DomainBareJid uploadServiceAddress, String filename, long size, String contentType, String namespace) {
super(ELEMENT, namespace);
if (size <= 0) { if (size <= 0) {
throw new IllegalArgumentException("File fileSize must be greater than zero."); throw new IllegalArgumentException("File fileSize must be greater than zero.");
@ -53,10 +63,7 @@ public class SlotRequest extends IQ {
this.contentType = contentType; this.contentType = contentType;
setType(Type.get); setType(Type.get);
} setTo(uploadServiceAddress);
public SlotRequest(String filename, long size) {
this(filename, size, null);
} }
public String getFilename() { public String getFilename() {
@ -76,9 +83,7 @@ public class SlotRequest extends IQ {
xml.rightAngleBracket(); xml.rightAngleBracket();
xml.element("filename", filename); xml.element("filename", filename);
xml.element("size", String.valueOf(size)); xml.element("size", String.valueOf(size));
if (contentType != null) { xml.optElement("content-type", contentType);
xml.element("content-type", contentType);
}
return xml; return xml;
} }
} }

View File

@ -0,0 +1,42 @@
/**
*
* Copyright 2017 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.httpfileupload.element;
import org.jivesoftware.smackx.httpfileupload.HttpFileUploadManager;
import org.jxmpp.jid.DomainBareJid;
public class SlotRequest_V0_2 extends SlotRequest {
public static final String NAMESPACE = HttpFileUploadManager.NAMESPACE_0_2;
public SlotRequest_V0_2(DomainBareJid uploadServiceAddress, String filename, long size) {
this(uploadServiceAddress, filename, size, null);
}
/**
* Create new slot request.
*
* @param uploadServiceAddress the XMPP address of the service to request the slot from.
* @param filename name of file
* @param size size of file in bytes
* @param contentType file content type or null
* @throws IllegalArgumentException if size is less than or equal to zero
*/
public SlotRequest_V0_2(DomainBareJid uploadServiceAddress, String filename, long size, String contentType) {
super(uploadServiceAddress, filename, size, contentType, NAMESPACE);
}
}

View File

@ -0,0 +1,31 @@
/**
*
* Copyright 2017 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.httpfileupload.element;
import java.net.URL;
import org.jivesoftware.smackx.httpfileupload.HttpFileUploadManager;
public class Slot_V0_2 extends Slot {
public static final String NAMESPACE = HttpFileUploadManager.NAMESPACE_0_2;
public Slot_V0_2(URL putUrl, URL getUrl) {
super(putUrl, getUrl, null, NAMESPACE);
}
}

View File

@ -18,6 +18,7 @@ package org.jivesoftware.smackx.httpfileupload.provider;
import org.jivesoftware.smack.provider.ExtensionElementProvider; import org.jivesoftware.smack.provider.ExtensionElementProvider;
import org.jivesoftware.smackx.httpfileupload.element.FileTooLargeError; import org.jivesoftware.smackx.httpfileupload.element.FileTooLargeError;
import org.jivesoftware.smackx.httpfileupload.element.FileTooLargeError_V0_2;
import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParser;
/** /**
@ -30,6 +31,7 @@ public class FileTooLargeErrorProvider extends ExtensionElementProvider<FileTooL
@Override @Override
public FileTooLargeError parse(XmlPullParser parser, int initialDepth) throws Exception { public FileTooLargeError parse(XmlPullParser parser, int initialDepth) throws Exception {
final String namespace = parser.getNamespace();
Long maxFileSize = null; Long maxFileSize = null;
outerloop: while(true) { outerloop: while(true) {
@ -52,6 +54,13 @@ public class FileTooLargeErrorProvider extends ExtensionElementProvider<FileTooL
} }
} }
return new FileTooLargeError(maxFileSize); switch (namespace) {
case FileTooLargeError.NAMESPACE:
return new FileTooLargeError(maxFileSize);
case FileTooLargeError_V0_2.NAMESPACE:
return new FileTooLargeError_V0_2(maxFileSize);
default:
throw new AssertionError();
}
} }
} }

View File

@ -18,12 +18,16 @@ package org.jivesoftware.smackx.httpfileupload.provider;
import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.provider.IQProvider; import org.jivesoftware.smack.provider.IQProvider;
import org.jivesoftware.smack.util.ParserUtils;
import org.jivesoftware.smackx.httpfileupload.element.Slot; import org.jivesoftware.smackx.httpfileupload.element.Slot;
import org.jivesoftware.smackx.httpfileupload.element.Slot_V0_2;
import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.util.HashMap;
import java.util.Map;
/** /**
* Provider for Slot. * Provider for Slot.
@ -35,8 +39,10 @@ public class SlotProvider extends IQProvider<Slot> {
@Override @Override
public Slot parse(XmlPullParser parser, int initialDepth) throws XmlPullParserException, IOException, SmackException { public Slot parse(XmlPullParser parser, int initialDepth) throws XmlPullParserException, IOException, SmackException {
final String namespace = parser.getNamespace();
URL putUrl = null; URL putUrl = null;
URL getUrl = null; URL getUrl = null;
Map<String, String> headers = null;
outerloop: while (true) { outerloop: while (true) {
int event = parser.next(); int event = parser.next();
@ -51,6 +57,14 @@ public class SlotProvider extends IQProvider<Slot> {
case "get": case "get":
getUrl = new URL(parser.nextText()); getUrl = new URL(parser.nextText());
break; break;
case "header":
String headerName = ParserUtils.getRequiredAttribute(parser, "name");
String headerValue = ParserUtils.getRequiredNextText(parser);
if (headers == null) {
headers = new HashMap<>();
}
headers.put(headerName, headerValue);
break;
} }
break; break;
case XmlPullParser.END_TAG: case XmlPullParser.END_TAG:
@ -61,6 +75,13 @@ public class SlotProvider extends IQProvider<Slot> {
} }
} }
return new Slot(putUrl, getUrl); switch (namespace) {
case Slot.NAMESPACE:
return new Slot(putUrl, getUrl, headers);
case Slot_V0_2.NAMESPACE:
return new Slot_V0_2(putUrl, getUrl);
default:
throw new AssertionError();
}
} }
} }

View File

@ -234,6 +234,16 @@
</extensionProvider> </extensionProvider>
<!-- XEP-0363: HTTP File Upload --> <!-- XEP-0363: HTTP File Upload -->
<iqProvider>
<elementName>slot</elementName>
<namespace>urn:xmpp:http:upload:0</namespace>
<className>org.jivesoftware.smackx.httpfileupload.provider.SlotProvider</className>
</iqProvider>
<extensionProvider>
<elementName>file-too-large</elementName>
<namespace>urn:xmpp:http:upload:0</namespace>
<className>org.jivesoftware.smackx.httpfileupload.provider.FileTooLargeErrorProvider</className>
</extensionProvider>
<iqProvider> <iqProvider>
<elementName>slot</elementName> <elementName>slot</elementName>
<namespace>urn:xmpp:http:upload</namespace> <namespace>urn:xmpp:http:upload</namespace>

View File

@ -23,7 +23,7 @@ import org.junit.Test;
public class FileTooLargeErrorCreateTest { public class FileTooLargeErrorCreateTest {
String fileTooLargeErrorExtensionExample String fileTooLargeErrorExtensionExample
= "<file-too-large xmlns='urn:xmpp:http:upload'>" = "<file-too-large xmlns='urn:xmpp:http:upload:0'>"
+ "<max-file-size>20000</max-file-size>" + "<max-file-size>20000</max-file-size>"
+ "</file-too-large>"; + "</file-too-large>";

View File

@ -26,7 +26,7 @@ import java.net.URL;
public class SlotCreateTest { public class SlotCreateTest {
String testSlot String testSlot
= "<slot xmlns='urn:xmpp:http:upload'>" = "<slot xmlns='urn:xmpp:http:upload:0'>"
+ "<put>https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my_juliet.png</put>" + "<put>https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my_juliet.png</put>"
+ "<get>https://download.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my_juliet.png</get>" + "<get>https://download.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my_juliet.png</get>"
+ "</slot>"; + "</slot>";

View File

@ -19,27 +19,28 @@ package org.jivesoftware.smackx.httpfileupload;
import org.jivesoftware.smackx.httpfileupload.element.SlotRequest; import org.jivesoftware.smackx.httpfileupload.element.SlotRequest;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.jxmpp.jid.JidTestUtil;
import org.jxmpp.stringprep.XmppStringprepException; import org.jxmpp.stringprep.XmppStringprepException;
public class SlotRequestCreateTest { public class SlotRequestCreateTest {
String testRequest String testRequest
= "<request xmlns='urn:xmpp:http:upload'>" = "<request xmlns='urn:xmpp:http:upload:0'>"
+ "<filename>my_juliet.png</filename>" + "<filename>my_juliet.png</filename>"
+ "<size>23456</size>" + "<size>23456</size>"
+ "<content-type>image/jpeg</content-type>" + "<content-type>image/jpeg</content-type>"
+ "</request>"; + "</request>";
String testRequestWithoutContentType String testRequestWithoutContentType
= "<request xmlns='urn:xmpp:http:upload'>" = "<request xmlns='urn:xmpp:http:upload:0'>"
+ "<filename>my_romeo.png</filename>" + "<filename>my_romeo.png</filename>"
+ "<size>52523</size>" + "<size>52523</size>"
+ "</request>"; + "</request>";
@Test @Test
public void checkSlotRequestCreation() throws XmppStringprepException { public void checkSlotRequestCreation() throws XmppStringprepException {
SlotRequest slotRequest = new SlotRequest("my_juliet.png", 23456, "image/jpeg"); SlotRequest slotRequest = new SlotRequest(JidTestUtil.DOMAIN_BARE_JID_1, "my_juliet.png", 23456, "image/jpeg");
Assert.assertEquals("my_juliet.png", slotRequest.getFilename()); Assert.assertEquals("my_juliet.png", slotRequest.getFilename());
Assert.assertEquals(23456, slotRequest.getSize()); Assert.assertEquals(23456, slotRequest.getSize());
@ -50,7 +51,7 @@ public class SlotRequestCreateTest {
@Test @Test
public void checkSlotRequestCreationWithoutContentType() throws XmppStringprepException { public void checkSlotRequestCreationWithoutContentType() throws XmppStringprepException {
SlotRequest slotRequest = new SlotRequest("my_romeo.png", 52523); SlotRequest slotRequest = new SlotRequest(JidTestUtil.DOMAIN_BARE_JID_1, "my_romeo.png", 52523);
Assert.assertEquals("my_romeo.png", slotRequest.getFilename()); Assert.assertEquals("my_romeo.png", slotRequest.getFilename());
Assert.assertEquals(52523, slotRequest.getSize()); Assert.assertEquals(52523, slotRequest.getSize());
@ -61,11 +62,11 @@ public class SlotRequestCreateTest {
@Test(expected = IllegalArgumentException.class) @Test(expected = IllegalArgumentException.class)
public void checkSlotRequestCreationNegativeSize() { public void checkSlotRequestCreationNegativeSize() {
new SlotRequest("my_juliet.png", -23456, "image/jpeg"); new SlotRequest(JidTestUtil.DOMAIN_BARE_JID_1, "my_juliet.png", -23456, "image/jpeg");
} }
@Test(expected = IllegalArgumentException.class) @Test(expected = IllegalArgumentException.class)
public void checkSlotRequestCreationZeroSize() { public void checkSlotRequestCreationZeroSize() {
new SlotRequest("my_juliet.png", 0, "image/jpeg"); new SlotRequest(JidTestUtil.DOMAIN_BARE_JID_1, "my_juliet.png", 0, "image/jpeg");
} }
} }

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.jivesoftware.smackx.httpfileupload; package org.jivesoftware.smackx.httpfileupload.provider;
import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.util.PacketParserUtils; import org.jivesoftware.smack.util.PacketParserUtils;
@ -33,14 +33,14 @@ public class FileTooLargeErrorProviderTest {
+ "id='step_03' " + "id='step_03' "
+ "to='romeo@montague.tld/garden' " + "to='romeo@montague.tld/garden' "
+ "type='error'>" + "type='error'>"
+ "<request xmlns='urn:xmpp:http:upload'>" + "<request xmlns='urn:xmpp:http:upload:0'>"
+ "<filename>my_juliet.png</filename>" + "<filename>my_juliet.png</filename>"
+ "<size>23456</size>" + "<size>23456</size>"
+ "</request>" + "</request>"
+ "<error type='modify'>" + "<error type='modify'>"
+ "<not-acceptable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas' />" + "<not-acceptable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas' />"
+ "<text xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'>File too large. The maximum file size is 20000 bytes</text>" + "<text xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'>File too large. The maximum file size is 20000 bytes</text>"
+ "<file-too-large xmlns='urn:xmpp:http:upload'>" + "<file-too-large xmlns='urn:xmpp:http:upload:0'>"
+ "<max-file-size>20000</max-file-size>" + "<max-file-size>20000</max-file-size>"
+ "</file-too-large>" + "</file-too-large>"
+ "</error>" + "</error>"

View File

@ -14,15 +14,13 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.jivesoftware.smackx.httpfileupload; package org.jivesoftware.smackx.httpfileupload.provider;
import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.util.PacketParserUtils; import org.jivesoftware.smack.util.PacketParserUtils;
import org.jivesoftware.smackx.httpfileupload.element.Slot; import org.jivesoftware.smackx.httpfileupload.element.Slot;
import org.jivesoftware.smackx.httpfileupload.provider.SlotProvider;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.xmlpull.v1.XmlPullParser;
import java.net.URL; import java.net.URL;
@ -38,7 +36,7 @@ public class SlotProviderTest {
+ "id='step_03' " + "id='step_03' "
+ "to='romeo@montague.tld/garden' " + "to='romeo@montague.tld/garden' "
+ "type='result'>" + "type='result'>"
+ "<slot xmlns='urn:xmpp:http:upload'>" + "<slot xmlns='urn:xmpp:http:upload:0'>"
+ "<put>https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my_juliet.png</put>" + "<put>https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my_juliet.png</put>"
+ "<get>https://download.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my_juliet.png</get>" + "<get>https://download.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my_juliet.png</get>"
+ "</slot>" + "</slot>"
@ -46,8 +44,7 @@ public class SlotProviderTest {
@Test @Test
public void checkSlotProvider() throws Exception { public void checkSlotProvider() throws Exception {
XmlPullParser parser = PacketParserUtils.getParserFor(slotExample); Slot slot = PacketParserUtils.parseStanza(slotExample);
Slot slot = new SlotProvider().parse(parser);
Assert.assertEquals(IQ.Type.result, slot.getType()); Assert.assertEquals(IQ.Type.result, slot.getType());
Assert.assertEquals(new URL("https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my_juliet.png"), Assert.assertEquals(new URL("https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my_juliet.png"),

View File

@ -16,10 +16,16 @@
*/ */
package org.igniterealtime.smack.inttest; package org.igniterealtime.smack.inttest;
import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Random; import java.util.Random;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.net.ssl.HttpsURLConnection;
import org.jivesoftware.smack.StanzaCollector; import org.jivesoftware.smack.StanzaCollector;
import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NoResponseException;
import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.SmackException.NotConnectedException;
@ -37,9 +43,12 @@ public abstract class AbstractSmackIntTest {
protected final long timeout; protected final long timeout;
protected AbstractSmackIntTest(String testRunId, long timeout) { protected final Configuration sinttestConfiguration;
protected AbstractSmackIntTest(String testRunId, Configuration configuration) {
this.testRunId = testRunId; this.testRunId = testRunId;
this.timeout = timeout; this.sinttestConfiguration = configuration;
this.timeout = configuration.replyTimeout;
} }
protected void performActionAndWaitUntilStanzaReceived(Runnable action, XMPPConnection connection, StanzaFilter filter) protected void performActionAndWaitUntilStanzaReceived(Runnable action, XMPPConnection connection, StanzaFilter filter)
@ -71,4 +80,19 @@ public abstract class AbstractSmackIntTest {
protected interface Condition { protected interface Condition {
boolean evaluate() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException; boolean evaluate() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException;
} }
protected File createNewTempFile() throws IOException {
File file = File.createTempFile("smack-integration-test-" + testRunId + "-temp-file", null);
file.deleteOnExit();
return file;
}
protected HttpURLConnection getHttpUrlConnectionFor(URL url) throws IOException {
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
if (sinttestConfiguration.tlsContext != null && urlConnection instanceof HttpsURLConnection) {
HttpsURLConnection httpsUrlConnection = (HttpsURLConnection) urlConnection;
httpsUrlConnection.setSSLSocketFactory(sinttestConfiguration.tlsContext.getSocketFactory());
}
return urlConnection;
}
} }

View File

@ -41,7 +41,7 @@ public abstract class AbstractSmackIntegrationTest extends AbstractSmackIntTest
protected final XMPPConnection connection; protected final XMPPConnection connection;
public AbstractSmackIntegrationTest(SmackIntegrationTestEnvironment environment) { public AbstractSmackIntegrationTest(SmackIntegrationTestEnvironment environment) {
super(environment.testRunId, environment.configuration.replyTimeout); super(environment.testRunId, environment.configuration);
this.connection = this.conOne = environment.conOne; this.connection = this.conOne = environment.conOne;
this.conTwo = environment.conTwo; this.conTwo = environment.conTwo;
this.conThree = environment.conThree; this.conThree = environment.conThree;

View File

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2015-2016 Florian Schmaus * Copyright 2015-2017 Florian Schmaus
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -19,14 +19,10 @@ package org.igniterealtime.smack.inttest;
import java.security.KeyManagementException; import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import javax.net.ssl.SSLContext;
import org.jivesoftware.smack.tcp.XMPPTCPConnection; import org.jivesoftware.smack.tcp.XMPPTCPConnection;
import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration; import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration;
import org.jxmpp.jid.DomainBareJid; import org.jxmpp.jid.DomainBareJid;
import eu.geekplace.javapinning.java7.Java7Pinning;
public abstract class AbstractSmackLowLevelIntegrationTest extends AbstractSmackIntTest { public abstract class AbstractSmackLowLevelIntegrationTest extends AbstractSmackIntTest {
private final SmackIntegrationTestEnvironment environment; private final SmackIntegrationTestEnvironment environment;
@ -39,7 +35,7 @@ public abstract class AbstractSmackLowLevelIntegrationTest extends AbstractSmack
protected final DomainBareJid service; protected final DomainBareJid service;
public AbstractSmackLowLevelIntegrationTest(SmackIntegrationTestEnvironment environment) { public AbstractSmackLowLevelIntegrationTest(SmackIntegrationTestEnvironment environment) {
super(environment.testRunId, environment.configuration.replyTimeout); super(environment.testRunId, environment.configuration);
this.environment = environment; this.environment = environment;
this.configuration = environment.configuration; this.configuration = environment.configuration;
this.service = configuration.service; this.service = configuration.service;
@ -47,9 +43,8 @@ public abstract class AbstractSmackLowLevelIntegrationTest extends AbstractSmack
public final XMPPTCPConnectionConfiguration.Builder getConnectionConfiguration() throws KeyManagementException, NoSuchAlgorithmException { public final XMPPTCPConnectionConfiguration.Builder getConnectionConfiguration() throws KeyManagementException, NoSuchAlgorithmException {
XMPPTCPConnectionConfiguration.Builder builder = XMPPTCPConnectionConfiguration.builder(); XMPPTCPConnectionConfiguration.Builder builder = XMPPTCPConnectionConfiguration.builder();
if (configuration.serviceTlsPin != null) { if (configuration.tlsContext != null) {
SSLContext sc = Java7Pinning.forPin(configuration.serviceTlsPin); builder.setCustomSSLContext(configuration.tlsContext);
builder.setCustomSSLContext(sc);
} }
builder.setSecurityMode(configuration.securityMode); builder.setSecurityMode(configuration.securityMode);
builder.setXmppDomain(service); builder.setXmppDomain(service);

View File

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2015-2016 Florian Schmaus * Copyright 2015-2017 Florian Schmaus
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -19,6 +19,8 @@ package org.igniterealtime.smack.inttest;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -26,6 +28,8 @@ import java.util.Map.Entry;
import java.util.Properties; import java.util.Properties;
import java.util.Set; import java.util.Set;
import javax.net.ssl.SSLContext;
import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode; import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode;
import org.jivesoftware.smack.util.Objects; import org.jivesoftware.smack.util.Objects;
import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.StringUtils;
@ -33,6 +37,8 @@ import org.jxmpp.jid.DomainBareJid;
import org.jxmpp.jid.impl.JidCreate; import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.stringprep.XmppStringprepException; import org.jxmpp.stringprep.XmppStringprepException;
import eu.geekplace.javapinning.java7.Java7Pinning;
public final class Configuration { public final class Configuration {
public enum AccountRegistration { public enum AccountRegistration {
@ -45,6 +51,8 @@ public final class Configuration {
public final String serviceTlsPin; public final String serviceTlsPin;
public final SSLContext tlsContext;
public final SecurityMode securityMode; public final SecurityMode securityMode;
public final int replyTimeout; public final int replyTimeout;
@ -78,10 +86,16 @@ public final class Configuration {
private Configuration(DomainBareJid service, String serviceTlsPin, SecurityMode securityMode, int replyTimeout, private Configuration(DomainBareJid service, String serviceTlsPin, SecurityMode securityMode, int replyTimeout,
boolean debug, String accountOneUsername, String accountOnePassword, String accountTwoUsername, boolean debug, String accountOneUsername, String accountOnePassword, String accountTwoUsername,
String accountTwoPassword, String accountThreeUsername, String accountThreePassword, Set<String> enabledTests, Set<String> disabledTests, String accountTwoPassword, String accountThreeUsername, String accountThreePassword, Set<String> enabledTests, Set<String> disabledTests,
Set<String> testPackages, String adminAccountUsername, String adminAccountPassword) { Set<String> testPackages, String adminAccountUsername, String adminAccountPassword)
throws KeyManagementException, NoSuchAlgorithmException {
this.service = Objects.requireNonNull(service, this.service = Objects.requireNonNull(service,
"'service' must be set. Either via 'properties' files or via system property 'sinttest.service'."); "'service' must be set. Either via 'properties' files or via system property 'sinttest.service'.");
this.serviceTlsPin = serviceTlsPin; this.serviceTlsPin = serviceTlsPin;
if (serviceTlsPin != null) {
tlsContext = Java7Pinning.forPin(serviceTlsPin);
} else {
tlsContext = null;
}
this.securityMode = securityMode; this.securityMode = securityMode;
if (replyTimeout > 0) { if (replyTimeout > 0) {
this.replyTimeout = replyTimeout; this.replyTimeout = replyTimeout;
@ -257,7 +271,7 @@ public final class Configuration {
return this; return this;
} }
public Configuration build() { public Configuration build() throws KeyManagementException, NoSuchAlgorithmException {
return new Configuration(service, serviceTlsPin, securityMode, replyTimeout, debug, accountOneUsername, return new Configuration(service, serviceTlsPin, securityMode, replyTimeout, debug, accountOneUsername,
accountOnePassword, accountTwoUsername, accountTwoPassword, accountThreeUsername, accountThreePassword, enabledTests, disabledTests, accountOnePassword, accountTwoUsername, accountTwoPassword, accountThreeUsername, accountThreePassword, enabledTests, disabledTests,
testPackages, adminAccountUsername, adminAccountPassword); testPackages, adminAccountUsername, adminAccountPassword);
@ -266,7 +280,8 @@ public final class Configuration {
private static final String SINTTEST = "sinttest."; private static final String SINTTEST = "sinttest.";
public static Configuration newConfiguration() throws IOException { public static Configuration newConfiguration()
throws IOException, KeyManagementException, NoSuchAlgorithmException {
Properties properties = new Properties(); Properties properties = new Properties();
File propertiesFile = findPropertiesFile(); File propertiesFile = findPropertiesFile();

View File

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2015-2016 Florian Schmaus * Copyright 2015-2017 Florian Schmaus
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -43,8 +43,6 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.net.ssl.SSLContext;
import org.igniterealtime.smack.inttest.IntTestUtil.UsernameAndPassword; import org.igniterealtime.smack.inttest.IntTestUtil.UsernameAndPassword;
import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode; import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode;
import org.jivesoftware.smack.SmackConfiguration; import org.jivesoftware.smack.SmackConfiguration;
@ -64,8 +62,6 @@ import org.reflections.scanners.MethodParameterScanner;
import org.reflections.scanners.SubTypesScanner; import org.reflections.scanners.SubTypesScanner;
import org.reflections.scanners.TypeAnnotationsScanner; import org.reflections.scanners.TypeAnnotationsScanner;
import eu.geekplace.javapinning.java7.Java7Pinning;
public class SmackIntegrationTestFramework { public class SmackIntegrationTestFramework {
private static final Logger LOGGER = Logger.getLogger(SmackIntegrationTestFramework.class.getName()); private static final Logger LOGGER = Logger.getLogger(SmackIntegrationTestFramework.class.getName());
@ -553,9 +549,8 @@ public class SmackIntegrationTestFramework {
.setResource(middlefix + '-' + testRunResult.testRunId) .setResource(middlefix + '-' + testRunResult.testRunId)
.setSecurityMode(config.securityMode); .setSecurityMode(config.securityMode);
// @formatter:on // @formatter:on
if (StringUtils.isNotEmpty(config.serviceTlsPin)) { if (config.tlsContext != null) {
SSLContext sc = Java7Pinning.forPin(config.serviceTlsPin); builder.setCustomSSLContext(config.tlsContext);
builder.setCustomSSLContext(sc);
} }
XMPPTCPConnection connection = new XMPPTCPConnection(builder.build()); XMPPTCPConnection connection = new XMPPTCPConnection(builder.build());
connection.connect(); connection.connect();
@ -581,9 +576,8 @@ public class SmackIntegrationTestFramework {
SmackException, IOException, XMPPException { SmackException, IOException, XMPPException {
Configuration config = environment.configuration; Configuration config = environment.configuration;
XMPPTCPConnectionConfiguration.Builder builder = XMPPTCPConnectionConfiguration.builder(); XMPPTCPConnectionConfiguration.Builder builder = XMPPTCPConnectionConfiguration.builder();
if (config.serviceTlsPin != null) { if (config.tlsContext != null) {
SSLContext sc = Java7Pinning.forPin(config.serviceTlsPin); builder.setCustomSSLContext(config.tlsContext);
builder.setCustomSSLContext(sc);
} }
builder.setSecurityMode(config.securityMode); builder.setSecurityMode(config.securityMode);
builder.setXmppDomain(config.service); builder.setXmppDomain(config.service);

View File

@ -0,0 +1,105 @@
/**
*
* Copyright 2017 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.httpfileupload;
import static org.junit.Assert.assertArrayEquals;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest;
import org.igniterealtime.smack.inttest.SmackIntegrationTest;
import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment;
import org.igniterealtime.smack.inttest.TestNotPossibleException;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.SmackException.NoResponseException;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
public class HttpFileUploadIntegrationTest extends AbstractSmackIntegrationTest {
private static final int FILE_SIZE = 1024*128;
private final HttpFileUploadManager hfumOne;
public HttpFileUploadIntegrationTest(SmackIntegrationTestEnvironment environment) throws XMPPErrorException,
NotConnectedException, NoResponseException, InterruptedException, TestNotPossibleException {
super(environment);
hfumOne = HttpFileUploadManager.getInstanceFor(conOne);
if (!hfumOne.discoverUploadService()) {
throw new TestNotPossibleException(
"HttpFileUploadManager was unable to discover a HTTP File Upload service");
}
UploadService uploadService = hfumOne.getDefaultUploadService();
if (!uploadService.acceptsFileOfSize(FILE_SIZE)) {
throw new TestNotPossibleException("The upload service at " + uploadService.getAddress()
+ " does not accept files of size " + FILE_SIZE
+ ". It only accepts files with a maximum size of " + uploadService.getMaxFileSize());
}
hfumOne.setTlsContext(environment.configuration.tlsContext);
}
@SmackIntegrationTest
public void httpFileUploadTest() throws FileNotFoundException, IOException, XMPPErrorException, InterruptedException, SmackException {
final int fileSize = FILE_SIZE;
File file = createNewTempFile();
FileOutputStream fos = new FileOutputStream(file.getCanonicalPath());
byte[] upBytes;
try {
upBytes = new byte[fileSize];
INSECURE_RANDOM.nextBytes(upBytes);
fos.write(upBytes);
}
finally {
fos.close();
}
URL getUrl = hfumOne.uploadFile(file, new UploadProgressListener() {
@Override
public void onUploadProgress(long uploadedBytes, long totalBytes) {
double progress = uploadedBytes / totalBytes;
LOGGER.fine("HTTP File Upload progress " + progress + "% (" + uploadedBytes + '/' + totalBytes + ')');
}
});
HttpURLConnection urlConnection = getHttpUrlConnectionFor(getUrl);
ByteArrayOutputStream baos = new ByteArrayOutputStream(fileSize);
byte[] buffer = new byte[4096];
int n;
try {
InputStream is = new BufferedInputStream(urlConnection.getInputStream());
while ((n = is.read(buffer)) != -1) {
baos.write(buffer, 0, n);
}
}
finally {
urlConnection.disconnect();
}
byte[] downBytes = baos.toByteArray();
assertArrayEquals(upBytes, downBytes);
}
}

View File

@ -0,0 +1 @@
../../../../../../../../smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/package-info.java

View File

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2015 Florian Schmaus * Copyright 2015-2017 Florian Schmaus
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,11 +16,16 @@
*/ */
package org.igniterealtime.smack.inttest; package org.igniterealtime.smack.inttest;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import org.jxmpp.jid.JidTestUtil; import org.jxmpp.jid.JidTestUtil;
public class SmackIntegrationTestUnitTestUtil { public class SmackIntegrationTestUnitTestUtil {
public static DummySmackIntegrationTestFramework getFrameworkForUnitTest(Class<? extends AbstractSmackIntTest> unitTest) { public static DummySmackIntegrationTestFramework getFrameworkForUnitTest(
Class<? extends AbstractSmackIntTest> unitTest)
throws KeyManagementException, NoSuchAlgorithmException {
// @formatter:off // @formatter:off
Configuration configuration = Configuration.builder() Configuration configuration = Configuration.builder()
.setService(JidTestUtil.DOMAIN_BARE_JID_1) .setService(JidTestUtil.DOMAIN_BARE_JID_1)