diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/ParserUtils.java b/smack-core/src/main/java/org/jivesoftware/smack/util/ParserUtils.java index 5a67c720e..9a5b68153 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/ParserUtils.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/ParserUtils.java @@ -235,4 +235,19 @@ public class ParserUtils { 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; + } } diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadManager.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadManager.java index dfcc1f790..ddf67f054 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadManager.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadManager.java @@ -17,6 +17,7 @@ package org.jivesoftware.smackx.httpfileupload; import org.jivesoftware.smack.AbstractConnectionListener; +import org.jivesoftware.smack.ConnectionConfiguration; import org.jivesoftware.smack.ConnectionCreationListener; import org.jivesoftware.smack.Manager; import org.jivesoftware.smack.SmackException; @@ -25,8 +26,10 @@ import org.jivesoftware.smack.XMPPConnectionRegistry; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; 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.SlotRequest; +import org.jivesoftware.smackx.httpfileupload.element.SlotRequest_V0_2; import org.jivesoftware.smackx.xdata.FormField; import org.jivesoftware.smackx.xdata.packet.DataForm; import org.jxmpp.jid.DomainBareJid; @@ -34,25 +37,34 @@ import org.jxmpp.jid.DomainBareJid; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.WeakHashMap; import java.util.logging.Level; 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. - * XEP version 0.2.5 + * A manager for XEP-0363: HTTP File Upload. * * @author Grigory Fedorov + * @author Florian Schmaus * @see XEP-0363: HTTP File Upload */ 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()); static { @@ -65,8 +77,10 @@ public final class HttpFileUploadManager extends Manager { } private static final Map INSTANCES = new WeakHashMap<>(); - private DomainBareJid defaultUploadService; - private Long maxFileSize; + + private UploadService defaultUploadService; + + private SSLSocketFactory tlsSocketFactory; /** * 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 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. * @@ -122,39 +170,20 @@ public final class HttpFileUploadManager extends Manager { */ public boolean discoverUploadService() throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { - defaultUploadService = null; - maxFileSize = null; - - List servicesDiscoverInfo = ServiceDiscoveryManager.getInstanceFor(connection()) - .findServicesDiscoverInfo(SlotRequest.NAMESPACE, true, false); + ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection()); + List servicesDiscoverInfo = sdm + .findServicesDiscoverInfo(NAMESPACE, true, true); if (servicesDiscoverInfo.isEmpty()) { - return false; + servicesDiscoverInfo = sdm.findServicesDiscoverInfo(NAMESPACE_0_2, true, true); + if (servicesDiscoverInfo.isEmpty()) { + return false; + } } DiscoverInfo discoverInfo = servicesDiscoverInfo.get(0); - defaultUploadService = discoverInfo.getFrom().asDomainBareJid(); - - 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 values = field.getValues(); - if (!values.isEmpty()) { - maxFileSize = Long.valueOf(values.get(0)); - } - + defaultUploadService = uploadServiceFrom(discoverInfo); return true; } @@ -172,19 +201,10 @@ public final class HttpFileUploadManager extends Manager { * * @return upload service JID or null if not available */ - public DomainBareJid getDefaultUploadService() { + public UploadService getDefaultUploadService() { 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. * @@ -203,19 +223,6 @@ public final class HttpFileUploadManager extends Manager { 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. * @@ -233,27 +240,26 @@ public final class HttpFileUploadManager extends Manager { */ public URL uploadFile(File file, UploadProgressListener listener) throws InterruptedException, 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"); - uploadFile(file, slot.getPutUrl(), listener); + uploadFile(file, slot, listener); return slot.getGetUrl(); } /** - * 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. + * 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. * * @param filename name of file to be uploaded - * @param fileSize file size in bytes -- must be less or equal - * to {@link HttpFileUploadManager#getMaxFileSize()} (if available) + * @param fileSize file size in bytes. * @return file upload Slot in case of success - - * @throws IllegalArgumentException if fileSize is less than or equal to zero - * or greater than {@link HttpFileUploadManager#getMaxFileSize()} + * @throws IllegalArgumentException if fileSize is less than or equal to zero or greater than the maximum size + * supported by the service. * @throws InterruptedException * @throws XMPPException.XMPPErrorException * @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. * * @param filename name of file to be uploaded - * @param fileSize file size in bytes -- must be less or equal - * to {@link HttpFileUploadManager#getMaxFileSize()} (if available) + * @param fileSize file size in bytes. * @param contentType file content-type or null * @return file upload Slot in case of success - * @throws IllegalArgumentException if fileSize is less than or equal to zero - * or greater than {@link HttpFileUploadManager#getMaxFileSize()} + * @throws IllegalArgumentException if fileSize is less than or equal to zero or greater than the maximum size + * supported by the service. * @throws SmackException.NotConnectedException * @throws InterruptedException * @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. * * @param filename name of file to be uploaded - * @param fileSize file size in bytes -- must be less or equal - * to {@link HttpFileUploadManager#getMaxFileSize()} (if available) + * @param fileSize file size in bytes. * @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 - * @throws IllegalArgumentException if fileSize is less than or equal to zero - * or greater than {@link HttpFileUploadManager#getMaxFileSize()} + * @throws IllegalArgumentException if fileSize is less than or equal to zero or greater than the maximum size + * supported by the service. * @throws SmackException * @throws InterruptedException * @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 { - if (defaultUploadService == null && uploadService == null) { - throw new SmackException("No upload service specified or discovered."); - } + final XMPPConnection connection = connection(); + final UploadService defaultUploadService = this.defaultUploadService; - if (uploadService == null && maxFileSize != null) { - if (fileSize > maxFileSize) { - throw new IllegalArgumentException("Requested file size " + fileSize - + " is greater than max allowed size " + maxFileSize); + // The upload service we are going to use. + UploadService uploadService; + + 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) { - slotRequest.setTo(uploadService); - } else { - slotRequest.setTo(defaultUploadService); + if (uploadService == null) { + throw new SmackException("No upload service specified and also none discovered."); } - 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(); + urlConnection.setRequestMethod("PUT"); urlConnection.setUseCaches(false); 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;"); - OutputStream outputStream = urlConnection.getOutputStream(); - - long bytesSend = 0; - - long fileSize = file.length(); - if (listener != null) { - listener.onUploadProgress(0, fileSize); + for (Entry header : slot.getHeaders().entrySet()) { + urlConnection.setRequestProperty(header.getKey(), header.getValue()); } - 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]; - int bytesRead; - while ((bytesRead = inputStream.read(buffer)) != -1) { - outputStream.write(buffer, 0, bytesRead); - outputStream.flush(); - bytesSend += bytesRead; + try { + OutputStream outputStream = urlConnection.getOutputStream(); + + long bytesSend = 0; 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); + } } - - inputStream.close(); - 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); + finally { + urlConnection.disconnect(); } } + private static boolean containsHttpFileUploadNamespace(DiscoverInfo discoverInfo) { + return discoverInfo.containsFeature(NAMESPACE) || discoverInfo.containsFeature(NAMESPACE_0_2); + } } diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/UploadProgressListener.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/UploadProgressListener.java new file mode 100644 index 000000000..085734795 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/UploadProgressListener.java @@ -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); + +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/UploadService.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/UploadService.java new file mode 100644 index 000000000..e9d81a447 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/UploadService.java @@ -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; + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/FileTooLargeError.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/FileTooLargeError.java index 57eec5d62..a0fcb7d04 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/FileTooLargeError.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/FileTooLargeError.java @@ -32,9 +32,15 @@ public class FileTooLargeError implements ExtensionElement { public static final String NAMESPACE = SlotRequest.NAMESPACE; private final long maxFileSize; + private final String namespace; public FileTooLargeError(long maxFileSize) { + this(maxFileSize, NAMESPACE); + } + + protected FileTooLargeError(long maxFileSize, String namespace) { this.maxFileSize = maxFileSize; + this.namespace = namespace; } public long getMaxFileSize() { @@ -48,15 +54,15 @@ public class FileTooLargeError implements ExtensionElement { @Override public String getNamespace() { - return NAMESPACE; + return namespace; } @Override - public CharSequence toXML() { + public XmlStringBuilder toXML() { XmlStringBuilder xml = new XmlStringBuilder(this); xml.rightAngleBracket(); xml.element("max-file-size", String.valueOf(maxFileSize)); - xml.closeElement(ELEMENT); + xml.closeElement(this); return xml; } diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/FileTooLargeError_V0_2.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/FileTooLargeError_V0_2.java new file mode 100644 index 000000000..fedf8d118 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/FileTooLargeError_V0_2.java @@ -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); + } + + +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/Slot.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/Slot.java index 470987dc4..e341bca46 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/Slot.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/Slot.java @@ -19,6 +19,8 @@ package org.jivesoftware.smackx.httpfileupload.element; import org.jivesoftware.smack.packet.IQ; import java.net.URL; +import java.util.Collections; +import java.util.Map; /** * Slot responded by upload service. @@ -33,12 +35,26 @@ public class Slot extends IQ { private final URL putUrl; private final URL getUrl; + private final Map headers; public Slot(URL putUrl, URL getUrl) { - super(ELEMENT, NAMESPACE); + this(putUrl, getUrl, null); + } + + public Slot(URL putUrl, URL getUrl, Map headers) { + this(putUrl, getUrl, headers, NAMESPACE); + } + + protected Slot(URL putUrl, URL getUrl, Map headers, String namespace) { + super(ELEMENT, namespace); setType(Type.result); this.putUrl = putUrl; this.getUrl = getUrl; + if (headers == null) { + this.headers = Collections.emptyMap(); + } else { + this.headers = Collections.unmodifiableMap(headers); + } } public URL getPutUrl() { @@ -49,6 +65,9 @@ public class Slot extends IQ { return getUrl; } + public Map getHeaders() { + return headers; + } @Override protected IQChildElementXmlStringBuilder getIQChildElementBuilder(IQChildElementXmlStringBuilder xml) { @@ -56,6 +75,9 @@ public class Slot extends IQ { xml.element("put", putUrl.toString()); xml.element("get", getUrl.toString()); + for (Map.Entry entry : getHeaders().entrySet()) { + xml.openElement("header").attribute(entry.getKey(), entry.getValue()); + } return xml; } diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/SlotRequest.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/SlotRequest.java index bcdb823e6..a776ccf32 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/SlotRequest.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/SlotRequest.java @@ -17,6 +17,8 @@ package org.jivesoftware.smackx.httpfileupload.element; import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smackx.httpfileupload.HttpFileUploadManager; +import org.jxmpp.jid.DomainBareJid; /** * Upload slot request. @@ -26,23 +28,31 @@ import org.jivesoftware.smack.packet.IQ; */ public class SlotRequest extends IQ { 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 long size; private final String contentType; + public SlotRequest(DomainBareJid uploadServiceAddress, String filename, long size) { + this(uploadServiceAddress, filename, size, null); + } /** * 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 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(String filename, long size, String contentType) { - super(ELEMENT, NAMESPACE); + public SlotRequest(DomainBareJid uploadServiceAddress, String filename, long size, String contentType) { + this(uploadServiceAddress, filename, size, contentType, NAMESPACE); + } + + protected SlotRequest(DomainBareJid uploadServiceAddress, String filename, long size, String contentType, String namespace) { + super(ELEMENT, namespace); if (size <= 0) { throw new IllegalArgumentException("File fileSize must be greater than zero."); @@ -53,10 +63,7 @@ public class SlotRequest extends IQ { this.contentType = contentType; setType(Type.get); - } - - public SlotRequest(String filename, long size) { - this(filename, size, null); + setTo(uploadServiceAddress); } public String getFilename() { @@ -76,9 +83,7 @@ public class SlotRequest extends IQ { xml.rightAngleBracket(); xml.element("filename", filename); xml.element("size", String.valueOf(size)); - if (contentType != null) { - xml.element("content-type", contentType); - } + xml.optElement("content-type", contentType); return xml; } } diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/SlotRequest_V0_2.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/SlotRequest_V0_2.java new file mode 100644 index 000000000..eb87bc923 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/SlotRequest_V0_2.java @@ -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); + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/Slot_V0_2.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/Slot_V0_2.java new file mode 100644 index 000000000..20645ac1a --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/Slot_V0_2.java @@ -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); + } + +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/provider/FileTooLargeErrorProvider.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/provider/FileTooLargeErrorProvider.java index 3f1accb7c..77ea7fda4 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/provider/FileTooLargeErrorProvider.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/provider/FileTooLargeErrorProvider.java @@ -18,6 +18,7 @@ package org.jivesoftware.smackx.httpfileupload.provider; import org.jivesoftware.smack.provider.ExtensionElementProvider; import org.jivesoftware.smackx.httpfileupload.element.FileTooLargeError; +import org.jivesoftware.smackx.httpfileupload.element.FileTooLargeError_V0_2; import org.xmlpull.v1.XmlPullParser; /** @@ -30,6 +31,7 @@ public class FileTooLargeErrorProvider extends ExtensionElementProvider { @Override public Slot parse(XmlPullParser parser, int initialDepth) throws XmlPullParserException, IOException, SmackException { + final String namespace = parser.getNamespace(); URL putUrl = null; URL getUrl = null; + Map headers = null; outerloop: while (true) { int event = parser.next(); @@ -51,6 +57,14 @@ public class SlotProvider extends IQProvider { case "get": getUrl = new URL(parser.nextText()); 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; case XmlPullParser.END_TAG: @@ -61,6 +75,13 @@ public class SlotProvider extends IQProvider { } } - 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(); + } } } diff --git a/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers b/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers index 0b9f7cf8a..20ed6cd25 100644 --- a/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers +++ b/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers @@ -234,6 +234,16 @@ + + slot + urn:xmpp:http:upload:0 + org.jivesoftware.smackx.httpfileupload.provider.SlotProvider + + + file-too-large + urn:xmpp:http:upload:0 + org.jivesoftware.smackx.httpfileupload.provider.FileTooLargeErrorProvider + slot urn:xmpp:http:upload diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/FileTooLargeErrorCreateTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/FileTooLargeErrorCreateTest.java index 40c79f335..d2abf74f6 100644 --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/FileTooLargeErrorCreateTest.java +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/FileTooLargeErrorCreateTest.java @@ -23,7 +23,7 @@ import org.junit.Test; public class FileTooLargeErrorCreateTest { String fileTooLargeErrorExtensionExample - = "" + = "" + "20000" + ""; diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/SlotCreateTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/SlotCreateTest.java index 838de9466..ebb5170cd 100644 --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/SlotCreateTest.java +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/SlotCreateTest.java @@ -26,7 +26,7 @@ import java.net.URL; public class SlotCreateTest { String testSlot - = "" + = "" + "https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my_juliet.png" + "https://download.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my_juliet.png" + ""; diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/SlotRequestCreateTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/SlotRequestCreateTest.java index e7ef98bb1..43c6fe80d 100644 --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/SlotRequestCreateTest.java +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/SlotRequestCreateTest.java @@ -19,27 +19,28 @@ package org.jivesoftware.smackx.httpfileupload; import org.jivesoftware.smackx.httpfileupload.element.SlotRequest; import org.junit.Assert; import org.junit.Test; +import org.jxmpp.jid.JidTestUtil; import org.jxmpp.stringprep.XmppStringprepException; public class SlotRequestCreateTest { String testRequest - = "" + = "" + "my_juliet.png" + "23456" + "image/jpeg" + ""; String testRequestWithoutContentType - = "" + = "" + "my_romeo.png" + "52523" + ""; @Test 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(23456, slotRequest.getSize()); @@ -50,7 +51,7 @@ public class SlotRequestCreateTest { @Test 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(52523, slotRequest.getSize()); @@ -61,11 +62,11 @@ public class SlotRequestCreateTest { @Test(expected = IllegalArgumentException.class) 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) public void checkSlotRequestCreationZeroSize() { - new SlotRequest("my_juliet.png", 0, "image/jpeg"); + new SlotRequest(JidTestUtil.DOMAIN_BARE_JID_1, "my_juliet.png", 0, "image/jpeg"); } } diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/FileTooLargeErrorProviderTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/provider/FileTooLargeErrorProviderTest.java similarity index 94% rename from smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/FileTooLargeErrorProviderTest.java rename to smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/provider/FileTooLargeErrorProviderTest.java index 10573d938..564b142d6 100644 --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/FileTooLargeErrorProviderTest.java +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/provider/FileTooLargeErrorProviderTest.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * 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.util.PacketParserUtils; @@ -33,14 +33,14 @@ public class FileTooLargeErrorProviderTest { + "id='step_03' " + "to='romeo@montague.tld/garden' " + "type='error'>" - + "" + + "" + "my_juliet.png" + "23456" + "" + "" + "" + "File too large. The maximum file size is 20000 bytes" - + "" + + "" + "20000" + "" + "" diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/SlotProviderTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/provider/SlotProviderTest.java similarity index 85% rename from smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/SlotProviderTest.java rename to smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/provider/SlotProviderTest.java index 13b830b67..4cef3c550 100644 --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/SlotProviderTest.java +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/provider/SlotProviderTest.java @@ -14,15 +14,13 @@ * See the License for the specific language governing permissions and * 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.util.PacketParserUtils; import org.jivesoftware.smackx.httpfileupload.element.Slot; -import org.jivesoftware.smackx.httpfileupload.provider.SlotProvider; import org.junit.Assert; import org.junit.Test; -import org.xmlpull.v1.XmlPullParser; import java.net.URL; @@ -38,7 +36,7 @@ public class SlotProviderTest { + "id='step_03' " + "to='romeo@montague.tld/garden' " + "type='result'>" - + "" + + "" + "https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my_juliet.png" + "https://download.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my_juliet.png" + "" @@ -46,8 +44,7 @@ public class SlotProviderTest { @Test public void checkSlotProvider() throws Exception { - XmlPullParser parser = PacketParserUtils.getParserFor(slotExample); - Slot slot = new SlotProvider().parse(parser); + Slot slot = PacketParserUtils.parseStanza(slotExample); Assert.assertEquals(IQ.Type.result, slot.getType()); Assert.assertEquals(new URL("https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my_juliet.png"), diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackIntTest.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackIntTest.java index 98c7e6400..e23f2c3ed 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackIntTest.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackIntTest.java @@ -16,10 +16,16 @@ */ 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.concurrent.TimeoutException; import java.util.logging.Logger; +import javax.net.ssl.HttpsURLConnection; + import org.jivesoftware.smack.StanzaCollector; import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; @@ -37,9 +43,12 @@ public abstract class AbstractSmackIntTest { protected final long timeout; - protected AbstractSmackIntTest(String testRunId, long timeout) { + protected final Configuration sinttestConfiguration; + + protected AbstractSmackIntTest(String testRunId, Configuration configuration) { this.testRunId = testRunId; - this.timeout = timeout; + this.sinttestConfiguration = configuration; + this.timeout = configuration.replyTimeout; } protected void performActionAndWaitUntilStanzaReceived(Runnable action, XMPPConnection connection, StanzaFilter filter) @@ -71,4 +80,19 @@ public abstract class AbstractSmackIntTest { protected interface Condition { 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; + } } diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackIntegrationTest.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackIntegrationTest.java index e6cac85ee..91ccba353 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackIntegrationTest.java @@ -41,7 +41,7 @@ public abstract class AbstractSmackIntegrationTest extends AbstractSmackIntTest protected final XMPPConnection connection; public AbstractSmackIntegrationTest(SmackIntegrationTestEnvironment environment) { - super(environment.testRunId, environment.configuration.replyTimeout); + super(environment.testRunId, environment.configuration); this.connection = this.conOne = environment.conOne; this.conTwo = environment.conTwo; this.conThree = environment.conThree; diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackLowLevelIntegrationTest.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackLowLevelIntegrationTest.java index 81ddb7b4b..6d1bcd7a2 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackLowLevelIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackLowLevelIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2016 Florian Schmaus + * Copyright 2015-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. @@ -19,14 +19,10 @@ package org.igniterealtime.smack.inttest; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; -import javax.net.ssl.SSLContext; - import org.jivesoftware.smack.tcp.XMPPTCPConnection; import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration; import org.jxmpp.jid.DomainBareJid; -import eu.geekplace.javapinning.java7.Java7Pinning; - public abstract class AbstractSmackLowLevelIntegrationTest extends AbstractSmackIntTest { private final SmackIntegrationTestEnvironment environment; @@ -39,7 +35,7 @@ public abstract class AbstractSmackLowLevelIntegrationTest extends AbstractSmack protected final DomainBareJid service; public AbstractSmackLowLevelIntegrationTest(SmackIntegrationTestEnvironment environment) { - super(environment.testRunId, environment.configuration.replyTimeout); + super(environment.testRunId, environment.configuration); this.environment = environment; this.configuration = environment.configuration; this.service = configuration.service; @@ -47,9 +43,8 @@ public abstract class AbstractSmackLowLevelIntegrationTest extends AbstractSmack public final XMPPTCPConnectionConfiguration.Builder getConnectionConfiguration() throws KeyManagementException, NoSuchAlgorithmException { XMPPTCPConnectionConfiguration.Builder builder = XMPPTCPConnectionConfiguration.builder(); - if (configuration.serviceTlsPin != null) { - SSLContext sc = Java7Pinning.forPin(configuration.serviceTlsPin); - builder.setCustomSSLContext(sc); + if (configuration.tlsContext != null) { + builder.setCustomSSLContext(configuration.tlsContext); } builder.setSecurityMode(configuration.securityMode); builder.setXmppDomain(service); diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/Configuration.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/Configuration.java index 814b1b4f2..523fc65d4 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/Configuration.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/Configuration.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2016 Florian Schmaus + * Copyright 2015-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. @@ -19,6 +19,8 @@ package org.igniterealtime.smack.inttest; import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; import java.util.HashSet; import java.util.LinkedList; import java.util.List; @@ -26,6 +28,8 @@ import java.util.Map.Entry; import java.util.Properties; import java.util.Set; +import javax.net.ssl.SSLContext; + import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode; import org.jivesoftware.smack.util.Objects; import org.jivesoftware.smack.util.StringUtils; @@ -33,6 +37,8 @@ import org.jxmpp.jid.DomainBareJid; import org.jxmpp.jid.impl.JidCreate; import org.jxmpp.stringprep.XmppStringprepException; +import eu.geekplace.javapinning.java7.Java7Pinning; + public final class Configuration { public enum AccountRegistration { @@ -45,6 +51,8 @@ public final class Configuration { public final String serviceTlsPin; + public final SSLContext tlsContext; + public final SecurityMode securityMode; public final int replyTimeout; @@ -78,10 +86,16 @@ public final class Configuration { private Configuration(DomainBareJid service, String serviceTlsPin, SecurityMode securityMode, int replyTimeout, boolean debug, String accountOneUsername, String accountOnePassword, String accountTwoUsername, String accountTwoPassword, String accountThreeUsername, String accountThreePassword, Set enabledTests, Set disabledTests, - Set testPackages, String adminAccountUsername, String adminAccountPassword) { + Set testPackages, String adminAccountUsername, String adminAccountPassword) + throws KeyManagementException, NoSuchAlgorithmException { this.service = Objects.requireNonNull(service, "'service' must be set. Either via 'properties' files or via system property 'sinttest.service'."); this.serviceTlsPin = serviceTlsPin; + if (serviceTlsPin != null) { + tlsContext = Java7Pinning.forPin(serviceTlsPin); + } else { + tlsContext = null; + } this.securityMode = securityMode; if (replyTimeout > 0) { this.replyTimeout = replyTimeout; @@ -257,7 +271,7 @@ public final class Configuration { return this; } - public Configuration build() { + public Configuration build() throws KeyManagementException, NoSuchAlgorithmException { return new Configuration(service, serviceTlsPin, securityMode, replyTimeout, debug, accountOneUsername, accountOnePassword, accountTwoUsername, accountTwoPassword, accountThreeUsername, accountThreePassword, enabledTests, disabledTests, testPackages, adminAccountUsername, adminAccountPassword); @@ -266,7 +280,8 @@ public final class Configuration { private static final String SINTTEST = "sinttest."; - public static Configuration newConfiguration() throws IOException { + public static Configuration newConfiguration() + throws IOException, KeyManagementException, NoSuchAlgorithmException { Properties properties = new Properties(); File propertiesFile = findPropertiesFile(); diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java index 35edeac47..c23c5a56e 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2016 Florian Schmaus + * Copyright 2015-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. @@ -43,8 +43,6 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; -import javax.net.ssl.SSLContext; - import org.igniterealtime.smack.inttest.IntTestUtil.UsernameAndPassword; import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode; import org.jivesoftware.smack.SmackConfiguration; @@ -64,8 +62,6 @@ import org.reflections.scanners.MethodParameterScanner; import org.reflections.scanners.SubTypesScanner; import org.reflections.scanners.TypeAnnotationsScanner; -import eu.geekplace.javapinning.java7.Java7Pinning; - public class SmackIntegrationTestFramework { private static final Logger LOGGER = Logger.getLogger(SmackIntegrationTestFramework.class.getName()); @@ -553,9 +549,8 @@ public class SmackIntegrationTestFramework { .setResource(middlefix + '-' + testRunResult.testRunId) .setSecurityMode(config.securityMode); // @formatter:on - if (StringUtils.isNotEmpty(config.serviceTlsPin)) { - SSLContext sc = Java7Pinning.forPin(config.serviceTlsPin); - builder.setCustomSSLContext(sc); + if (config.tlsContext != null) { + builder.setCustomSSLContext(config.tlsContext); } XMPPTCPConnection connection = new XMPPTCPConnection(builder.build()); connection.connect(); @@ -581,9 +576,8 @@ public class SmackIntegrationTestFramework { SmackException, IOException, XMPPException { Configuration config = environment.configuration; XMPPTCPConnectionConfiguration.Builder builder = XMPPTCPConnectionConfiguration.builder(); - if (config.serviceTlsPin != null) { - SSLContext sc = Java7Pinning.forPin(config.serviceTlsPin); - builder.setCustomSSLContext(sc); + if (config.tlsContext != null) { + builder.setCustomSSLContext(config.tlsContext); } builder.setSecurityMode(config.securityMode); builder.setXmppDomain(config.service); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadIntegrationTest.java new file mode 100644 index 000000000..c846aad07 --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadIntegrationTest.java @@ -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); + } +} diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/httpfileupload/package-info.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/httpfileupload/package-info.java new file mode 120000 index 000000000..1bde5bbc6 --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/httpfileupload/package-info.java @@ -0,0 +1 @@ +../../../../../../../../smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/package-info.java \ No newline at end of file diff --git a/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/SmackIntegrationTestUnitTestUtil.java b/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/SmackIntegrationTestUnitTestUtil.java index 7032c008e..ba02dd4f6 100644 --- a/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/SmackIntegrationTestUnitTestUtil.java +++ b/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/SmackIntegrationTestUnitTestUtil.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015 Florian Schmaus + * Copyright 2015-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. @@ -16,11 +16,16 @@ */ package org.igniterealtime.smack.inttest; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; + import org.jxmpp.jid.JidTestUtil; public class SmackIntegrationTestUnitTestUtil { - public static DummySmackIntegrationTestFramework getFrameworkForUnitTest(Class unitTest) { + public static DummySmackIntegrationTestFramework getFrameworkForUnitTest( + Class unitTest) + throws KeyManagementException, NoSuchAlgorithmException { // @formatter:off Configuration configuration = Configuration.builder() .setService(JidTestUtil.DOMAIN_BARE_JID_1)