diff --git a/.travis.yml b/.travis.yml index 5b6c55b30..41f447858 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,6 @@ cache: - $HOME/.m2 before_install: - - export JAVA_OPTS="-XX:MaxPermSize=512M" - export GRADLE_VERSION=2.12 - wget https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-all.zip - unzip -q gradle-${GRADLE_VERSION}-all.zip diff --git a/README.md b/README.md index f4026b3e1..f9ec1f48b 100644 --- a/README.md +++ b/README.md @@ -3,20 +3,23 @@ Smack [![Build Status](https://travis-ci.org/igniterealtime/Smack.svg?branch=master)](https://travis-ci.org/igniterealtime/Smack) [![Coverage Status](https://coveralls.io/repos/igniterealtime/Smack/badge.svg)](https://coveralls.io/r/igniterealtime/Smack) [![Project Stats](https://www.openhub.net/p/smackxmpp/widgets/project_thin_badge.gif)](https://www.openhub.net/p/smackxmpp) [![Flattr this git repo](http://api.flattr.com/button/flattr-badge-large.png)](https://flattr.com/thing/3480125) [![Visit our IRC channel](https://kiwiirc.com/buttons/irc.freenode.net/smack.png)](https://kiwiirc.com/client/irc.freenode.net/smack) -Instructions how to use Smack in your Java or Android project are provided in the [Smack 4.1 Readme and Upgrade Guide](https://github.com/igniterealtime/Smack/wiki/Smack-4.1-Readme-and-Upgrade-Guide). - About ----- -[Smack] is an open source, highly modular, easy to use, XMPP client library written in Java for JVMs and Android. +[Smack] is an open source, highly modular, easy to use, XMPP client library written in Java for Java SE compatible JVMs and Android. -A pure Java library, it can be embedded into your applications to create anything from a full XMPP client to simple XMPP integrations such as sending notification messages and presence-enabling devices. +A pure Java library, it can be embedded into your applications to create anything from a full XMPP instant messaging client to simple XMPP integrations such as sending notification messages and presence-enabling devices. Smack and XMPP allows you to easily exchange data, in various ways e.g. fire-and-forget, publish-subscribe, between human and non-human endpoints (M2M, IoT, …). Confused? Have a look at the [Overview](documentation/overview.md). [Smack] - an [Ignite Realtime] community project. +Getting started +--------------- + +Instructions how to use Smack in your Java or Android project are provided in the [Smack 4.2 Readme and Upgrade Guide](https://github.com/igniterealtime/Smack/wiki/Smack-4.2-Readme-and-Upgrade-Guide). + Bug Reporting ------------- @@ -48,7 +51,7 @@ The guidelines also contain development quickstart instructions. Resources --------- -- Bug Tracker: http://issues.igniterealtime.org/browse/SMACK +- Bug Tracker: https://issues.igniterealtime.org/browse/SMACK - JaCoCo Coverage Reports: https://www.igniterealtime.org/builds/smack/dailybuilds/jacoco/html/ - Nightly Builds: http://www.igniterealtime.org/downloads/nightly_smack.jsp - Nightly Javadoc: http://www.igniterealtime.org/builds/smack/dailybuilds/javadoc/ @@ -57,6 +60,7 @@ Resources - Dev Forum: http://community.igniterealtime.org/community/developers/smack - Maven Releases: https://oss.sonatype.org/content/repositories/releases/org/igniterealtime/smack/ - Maven Snapshots: https://oss.sonatype.org/content/repositories/snapshots/org/igniterealtime/smack/ +- Nightly Unique Maven Snapshots: https://igniterealtime.org/repo/ Donate ------ diff --git a/build.gradle b/build.gradle index 4922ceff0..92a62e572 100644 --- a/build.gradle +++ b/build.gradle @@ -118,7 +118,7 @@ allprojects { // Some systems may not have set their platform default // converter to 'utf8', but we use unicode in our source // files. Therefore ensure that javac uses unicode - options.encoding = "utf8" + options.encoding = 'UTF-8' options.compilerArgs = [ '-Xlint:all', // Set '-options' because a non-java7 javac will emit a @@ -164,6 +164,7 @@ allprojects { } tasks.withType(Javadoc) { options.charSet = "UTF-8" + options.encoding = 'UTF-8' } // Pin the errorprone version to prevent "unsupported major.minor @@ -301,6 +302,8 @@ subprojects { archives sourcesJar archives javadocJar archives testJar + // See http://stackoverflow.com/a/21946676/194894 + testRuntime testJar } uploadArchives { diff --git a/documentation/extensions/index.md b/documentation/extensions/index.md index 481229840..325fcf30a 100644 --- a/documentation/extensions/index.md +++ b/documentation/extensions/index.md @@ -91,6 +91,7 @@ Experimental Smack Extensions and currently supported XEPs of smack-experimental | [Internet of Things - Discovery](iot.md) | [XEP-0347](http://xmpp.org/extensions/xep-0347.html) | Describes how Things can be installed and discovered by their owners. | | Client State Indication | [XEP-0352](http://xmpp.org/extensions/xep-0352.html) | A way for the client to indicate its active/inactive state. | | [Push Notifications](pushnotifications.md) | [XEP-0357](http://xmpp.org/extensions/xep-0357.html) | Defines a way to manage push notifications from an XMPP Server. | +| HTTP File Upload | [XEP-0363](http://xmpp.org/extensions/xep-0363.html) | Protocol to request permissions to upload a file to an HTTP server and get a shareable URL. | | [Multi-User Chat Light](muclight.md) | [XEP-xxxx](http://mongooseim.readthedocs.io/en/latest/open-extensions/xeps/xep-muc-light.html) | Multi-User Chats for mobile XMPP applications and specific enviroment. | | Google GCM JSON payload | n/a | Semantically the same as XEP-0335: JSON Containers | diff --git a/resources/releasedocs/changelog.html b/resources/releasedocs/changelog.html index cb513e360..b8909c036 100644 --- a/resources/releasedocs/changelog.html +++ b/resources/releasedocs/changelog.html @@ -141,6 +141,140 @@ hr {
+

4.2.0 -- 2017-03-10

+ +

Sub-task +

+ + +

Bug +

+ + +

New Feature +

+ + +

Task +

+ + +

Improvement +

+

4.1.9 -- 2016-11-19

diff --git a/smack-bosh/src/main/java/org/jivesoftware/smack/bosh/XMPPBOSHConnection.java b/smack-bosh/src/main/java/org/jivesoftware/smack/bosh/XMPPBOSHConnection.java index abf9105f6..b91bb5a40 100644 --- a/smack-bosh/src/main/java/org/jivesoftware/smack/bosh/XMPPBOSHConnection.java +++ b/smack-bosh/src/main/java/org/jivesoftware/smack/bosh/XMPPBOSHConnection.java @@ -38,6 +38,7 @@ import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.packet.Nonza; import org.jivesoftware.smack.packet.Presence; +import org.jivesoftware.smack.packet.XMPPError; import org.jivesoftware.smack.sasl.packet.SaslStreamElements.SASLFailure; import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Success; import org.jivesoftware.smack.util.PacketParserUtils; @@ -529,7 +530,13 @@ public class XMPPBOSHConnection extends AbstractXMPPConnection { } break; case "error": - throw new StreamErrorException(PacketParserUtils.parseStreamError(parser)); + //Some bosh error isn't stream error. + if ("urn:ietf:params:xml:ns:xmpp-streams".equals(parser.getNamespace(null))) { + throw new StreamErrorException(PacketParserUtils.parseStreamError(parser)); + } else { + XMPPError.Builder builder = PacketParserUtils.parseError(parser); + throw new XMPPException.XMPPErrorException(null, builder.build()); + } } break; } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/sasl/core/ScramMechanism.java b/smack-core/src/main/java/org/jivesoftware/smack/sasl/core/ScramMechanism.java index 08a1f6aab..edeedb665 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/sasl/core/ScramMechanism.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/sasl/core/ScramMechanism.java @@ -1,6 +1,6 @@ /** * - * Copyright 2014-2016 Florian Schmaus + * Copyright 2014-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. @@ -22,6 +22,7 @@ import java.security.SecureRandom; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Random; import javax.security.auth.callback.CallbackHandler; @@ -41,7 +42,12 @@ public abstract class ScramMechanism extends SASLMechanism { private static final byte[] SERVER_KEY_BYTES = toBytes("Server Key"); private static final byte[] ONE = new byte[] { 0, 0, 0, 1 }; - private static final SecureRandom RANDOM = new SecureRandom(); + private static final ThreadLocal SECURE_RANDOM = new ThreadLocal() { + @Override + protected SecureRandom initialValue() { + return new SecureRandom(); + } + }; private static final Cache CACHE = new LruCache(10); @@ -292,8 +298,9 @@ public abstract class ScramMechanism extends SASLMechanism { String getRandomAscii() { int count = 0; char[] randomAscii = new char[RANDOM_ASCII_BYTE_COUNT]; + final Random random = SECURE_RANDOM.get(); while (count < RANDOM_ASCII_BYTE_COUNT) { - int r = RANDOM.nextInt(128); + int r = random.nextInt(128); char c = (char) r; // RFC 5802 § 5.1 specifies 'r:' to exclude the ',' character and to be only printable ASCII characters if (!isPrintableNonCommaAsciiChar(c)) { 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-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java b/smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java index e61b15650..94e528099 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java @@ -1,6 +1,6 @@ /** * - * Copyright 2003-2007 Jive Software, 2016 Florian Schmaus. + * Copyright 2003-2007 Jive Software, 2016-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. @@ -271,7 +271,12 @@ public class StringUtils { * The Random class is not considered to be cryptographically secure, so * only use these random Strings for low to medium security applications. */ - private static final Random randGen = new Random(); + private static final ThreadLocal randGen = new ThreadLocal() { + @Override + protected Random initialValue() { + return new Random(); + } + }; /** * Array of numbers and letters of mixed case. Numbers appear in the list @@ -299,15 +304,22 @@ public class StringUtils { if (length < 1) { return null; } + + final Random random = randGen.get(); // Create a char buffer to put random letters and numbers in. char [] randBuffer = new char[length]; for (int i=0; i SECURE_RANDOM = new ThreadLocal() { + @Override + protected SecureRandom initialValue() { + return new SecureRandom(); + } + }; public static String randomString(final int length) { if (length < 1) { @@ -315,7 +327,7 @@ public class StringUtils { } byte[] randomBytes = new byte[length]; - SECURE_RANDOM.nextBytes(randomBytes); + SECURE_RANDOM.get().nextBytes(randomBytes); char[] randomChars = new char[length]; for (int i = 0; i < length; i++) { randomChars[i] = getPrintableChar(randomBytes[i]); diff --git a/smack-experimental/build.gradle b/smack-experimental/build.gradle index d6b8c2582..51315f23a 100644 --- a/smack-experimental/build.gradle +++ b/smack-experimental/build.gradle @@ -9,4 +9,5 @@ dependencies { compile project(':smack-extensions') testCompile project(path: ":smack-core", configuration: "testRuntime") testCompile project(path: ":smack-core", configuration: "archives") + testCompile project(path: ":smack-extensions", configuration: "testRuntime") } 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 new file mode 100644 index 000000000..ddf67f054 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadManager.java @@ -0,0 +1,463 @@ +/** + * + * 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; + +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; +import org.jivesoftware.smack.XMPPConnection; +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; + +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; + +/** + * 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 { + XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() { + @Override + public void connectionCreated(XMPPConnection connection) { + getInstanceFor(connection); + } + }); + } + + private static final Map INSTANCES = new WeakHashMap<>(); + + private UploadService defaultUploadService; + + private SSLSocketFactory tlsSocketFactory; + + /** + * Obtain the HttpFileUploadManager responsible for a connection. + * + * @param connection the connection object. + * @return a HttpFileUploadManager instance + */ + public static synchronized HttpFileUploadManager getInstanceFor(XMPPConnection connection) { + HttpFileUploadManager httpFileUploadManager = INSTANCES.get(connection); + + if (httpFileUploadManager == null) { + httpFileUploadManager = new HttpFileUploadManager(connection); + INSTANCES.put(connection, httpFileUploadManager); + } + + return httpFileUploadManager; + } + + private HttpFileUploadManager(XMPPConnection connection) { + super(connection); + + connection.addConnectionListener(new AbstractConnectionListener() { + @Override + public void authenticated(XMPPConnection connection, boolean resumed) { + // No need to reset the cache if the connection got resumed. + if (resumed) { + return; + } + + try { + discoverUploadService(); + } catch (XMPPException.XMPPErrorException | SmackException.NotConnectedException + | SmackException.NoResponseException | InterruptedException e) { + LOGGER.log(Level.WARNING, "Error during discovering HTTP File Upload service", e); + } + } + }); + } + + 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. + * + * Called automatically when connection is authenticated. + * + * Note that this is a synchronous call -- Smack must wait for the server response. + * + * @return true if upload service was discovered + + * @throws XMPPException.XMPPErrorException + * @throws SmackException.NotConnectedException + * @throws InterruptedException + * @throws SmackException.NoResponseException + */ + public boolean discoverUploadService() throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, + InterruptedException, SmackException.NoResponseException { + ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection()); + List servicesDiscoverInfo = sdm + .findServicesDiscoverInfo(NAMESPACE, true, true); + + if (servicesDiscoverInfo.isEmpty()) { + servicesDiscoverInfo = sdm.findServicesDiscoverInfo(NAMESPACE_0_2, true, true); + if (servicesDiscoverInfo.isEmpty()) { + return false; + } + } + + DiscoverInfo discoverInfo = servicesDiscoverInfo.get(0); + + defaultUploadService = uploadServiceFrom(discoverInfo); + return true; + } + + /** + * Check if upload service was discovered. + * + * @return true if upload service was discovered + */ + public boolean isUploadServiceDiscovered() { + return defaultUploadService != null; + } + + /** + * Get default upload service if it was discovered. + * + * @return upload service JID or null if not available + */ + public UploadService getDefaultUploadService() { + return defaultUploadService; + } + + /** + * Request slot and uploaded file to HTTP file upload service. + * + * You don't need to request slot and upload file separately, this method will do both. + * Note that this is a synchronous call -- Smack must wait for the server response. + * + * @param file file to be uploaded + * @return public URL for sharing uploaded file + * @throws InterruptedException + * @throws XMPPException.XMPPErrorException + * @throws SmackException + * @throws IOException in case of HTTP upload errors + */ + public URL uploadFile(File file) throws InterruptedException, XMPPException.XMPPErrorException, + SmackException, IOException { + return uploadFile(file, null); + } + + /** + * Request slot and uploaded file to HTTP file upload service with progress callback. + * + * You don't need to request slot and upload file separately, this method will do both. + * Note that this is a synchronous call -- Smack must wait for the server response. + * + * @param file file to be uploaded + * @param listener upload progress listener of null + * @return public URL for sharing uploaded file + * + * @throws InterruptedException + * @throws XMPPException.XMPPErrorException + * @throws SmackException + * @throws IOException + */ + 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, 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. + * + * @param filename name of file to be uploaded + * @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 the maximum size + * supported by the service. + * @throws InterruptedException + * @throws XMPPException.XMPPErrorException + * @throws SmackException.NotConnectedException + * @throws SmackException.NoResponseException + */ + public Slot requestSlot(String filename, long fileSize) throws InterruptedException, + XMPPException.XMPPErrorException, SmackException { + return requestSlot(filename, fileSize, null, null); + } + + /** + * Request a new upload slot with optional content type 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. + * @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 the maximum size + * supported by the service. + * @throws SmackException.NotConnectedException + * @throws InterruptedException + * @throws XMPPException.XMPPErrorException + * @throws SmackException.NoResponseException + */ + public Slot requestSlot(String filename, long fileSize, String contentType) throws SmackException, + InterruptedException, XMPPException.XMPPErrorException { + return requestSlot(filename, fileSize, contentType, null); + } + + /** + * Request a new upload slot with optional content type from custom upload service. + * + * 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. + * @param contentType file content-type or null + * @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 the maximum size + * supported by the service. + * @throws SmackException + * @throws InterruptedException + * @throws XMPPException.XMPPErrorException + */ + public Slot requestSlot(String filename, long fileSize, String contentType, DomainBareJid uploadServiceAddress) + throws SmackException, InterruptedException, XMPPException.XMPPErrorException { + final XMPPConnection connection = connection(); + final UploadService defaultUploadService = this.defaultUploadService; + + // 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); + } + } + + if (uploadService == null) { + throw new SmackException("No upload service specified and also none discovered."); + } + + 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(); + } + + 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;"); + for (Entry header : slot.getHeaders().entrySet()) { + urlConnection.setRequestProperty(header.getKey(), header.getValue()); + } + + final SSLSocketFactory tlsSocketFactory = this.tlsSocketFactory; + if (tlsSocketFactory != null && urlConnection instanceof HttpsURLConnection) { + HttpsURLConnection httpsUrlConnection = (HttpsURLConnection) urlConnection; + httpsUrlConnection.setSSLSocketFactory(tlsSocketFactory); + } + + try { + OutputStream outputStream = urlConnection.getOutputStream(); + + long bytesSend = 0; + + if (listener != null) { + 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 { + 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 new file mode 100644 index 000000000..a0fcb7d04 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/FileTooLargeError.java @@ -0,0 +1,76 @@ +/** + * + * 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.element; + +import org.jivesoftware.smack.packet.ExtensionElement; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smack.util.XmlStringBuilder; + +/** + * File Too Large error extension. + * + * @author Grigory Fedorov + * @see XEP-0363: HTTP File Upload + */ +public class FileTooLargeError implements ExtensionElement { + public static final String ELEMENT = "file-too-large"; + 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() { + return maxFileSize; + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public String getNamespace() { + return namespace; + } + + @Override + public XmlStringBuilder toXML() { + XmlStringBuilder xml = new XmlStringBuilder(this); + xml.rightAngleBracket(); + xml.element("max-file-size", String.valueOf(maxFileSize)); + xml.closeElement(this); + return xml; + } + + public static FileTooLargeError from(IQ iq) { + XMPPError error = iq.getError(); + if (error == null) { + return null; + } + return error.getExtension(ELEMENT, NAMESPACE); + } +} 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 new file mode 100644 index 000000000..e341bca46 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/Slot.java @@ -0,0 +1,84 @@ +/** + * + * 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.element; + +import org.jivesoftware.smack.packet.IQ; + +import java.net.URL; +import java.util.Collections; +import java.util.Map; + +/** + * Slot responded by upload service. + * + * @author Grigory Fedorov + * @see XEP-0363: HTTP File Upload + */ +public class Slot extends IQ { + + public static final String ELEMENT = "slot"; + public static final String NAMESPACE = SlotRequest.NAMESPACE; + + private final URL putUrl; + private final URL getUrl; + private final Map headers; + + public Slot(URL putUrl, URL getUrl) { + 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() { + return putUrl; + } + + public URL getGetUrl() { + return getUrl; + } + + public Map getHeaders() { + return headers; + } + + @Override + protected IQChildElementXmlStringBuilder getIQChildElementBuilder(IQChildElementXmlStringBuilder xml) { + xml.rightAngleBracket(); + + 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 new file mode 100644 index 000000000..a776ccf32 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/SlotRequest.java @@ -0,0 +1,89 @@ +/** + * + * 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.element; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smackx.httpfileupload.HttpFileUploadManager; +import org.jxmpp.jid.DomainBareJid; + +/** + * Upload slot request. + + * @author Grigory Fedorov + * @see XEP-0363: HTTP File Upload + */ +public class SlotRequest extends IQ { + public static final String ELEMENT = "request"; + 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. + * + * @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(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."); + } + + this.filename = filename; + this.size = size; + this.contentType = contentType; + + setType(Type.get); + setTo(uploadServiceAddress); + } + + public String getFilename() { + return filename; + } + + public long getSize() { + return size; + } + + public String getContentType() { + return contentType; + } + + @Override + protected IQChildElementXmlStringBuilder getIQChildElementBuilder(IQChildElementXmlStringBuilder xml) { + xml.rightAngleBracket(); + xml.element("filename", filename); + xml.element("size", String.valueOf(size)); + 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/element/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/package-info.java new file mode 100644 index 000000000..d20c3658b --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/element/package-info.java @@ -0,0 +1,24 @@ +/** + * + * 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. + */ + +/** + * IQ stanzas and extensions for XEP-0363: HTTP File Upload. + * + * @author Grigory Fedorov + * @see XEP-0363: HTTP File Upload + */ +package org.jivesoftware.smackx.httpfileupload.element; diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/package-info.java new file mode 100644 index 000000000..d61996ae9 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/package-info.java @@ -0,0 +1,24 @@ +/** + * + * 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. + */ + +/** + * Smack's API for XEP-0363: HTTP File Upload. + * + * @author Grigory Fedorov + * @see XEP-0363: HTTP File Upload + */ +package org.jivesoftware.smackx.httpfileupload; 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 new file mode 100644 index 000000000..77ea7fda4 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/provider/FileTooLargeErrorProvider.java @@ -0,0 +1,66 @@ +/** + * + * 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.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; + +/** + * Provider for File Too Large error extension. + * + * @author Grigory Fedorov + * @see XEP-0363: HTTP File Upload + */ +public class FileTooLargeErrorProvider extends ExtensionElementProvider { + + @Override + public FileTooLargeError parse(XmlPullParser parser, int initialDepth) throws Exception { + final String namespace = parser.getNamespace(); + Long maxFileSize = null; + + outerloop: while(true) { + int event = parser.next(); + + switch (event) { + case XmlPullParser.START_TAG: + String name = parser.getName(); + switch(name) { + case "max-file-size": + maxFileSize = Long.valueOf(parser.nextText()); + break; + } + break; + case XmlPullParser.END_TAG: + if (parser.getDepth() == initialDepth) { + break outerloop; + } + break; + } + } + + switch (namespace) { + case FileTooLargeError.NAMESPACE: + return new FileTooLargeError(maxFileSize); + case FileTooLargeError_V0_2.NAMESPACE: + return new FileTooLargeError_V0_2(maxFileSize); + default: + throw new AssertionError(); + } + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/provider/SlotProvider.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/provider/SlotProvider.java new file mode 100644 index 000000000..bd85ac5c2 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/provider/SlotProvider.java @@ -0,0 +1,87 @@ +/** + * + * 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.provider; + +import org.jivesoftware.smack.SmackException; +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_V0_2; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +/** + * Provider for Slot. + * + * @author Grigory Fedorov + * @see XEP-0363: HTTP File Upload + */ +public class SlotProvider extends IQProvider { + + @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(); + + switch (event) { + case XmlPullParser.START_TAG: + String name = parser.getName(); + switch(name) { + case "put": + putUrl = new URL(parser.nextText()); + break; + 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: + if (parser.getDepth() == initialDepth) { + break outerloop; + } + break; + } + } + + 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/java/org/jivesoftware/smackx/httpfileupload/provider/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/provider/package-info.java new file mode 100644 index 000000000..3894df689 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/httpfileupload/provider/package-info.java @@ -0,0 +1,24 @@ +/** + * + * 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. + */ + +/** + * Providers for XEP-0363: HTTP File Upload. + * + * @author Grigory Fedorov + * @see XEP-0363: HTTP File Upload + */ +package org.jivesoftware.smackx.httpfileupload.provider; diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/MamManager.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/MamManager.java index 18cc9ac9e..1a07554a4 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/MamManager.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/MamManager.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2016 Florian Schmaus and Fernando Ramirez + * Copyright © 2016-2017 Florian Schmaus, Fernando Ramirez * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ package org.jivesoftware.smackx.mam; import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; @@ -25,10 +26,10 @@ import java.util.WeakHashMap; import org.jivesoftware.smack.ConnectionCreationListener; import org.jivesoftware.smack.Manager; -import org.jivesoftware.smack.StanzaCollector; import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.SmackException.NotLoggedInException; +import org.jivesoftware.smack.StanzaCollector; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPConnectionRegistry; import org.jivesoftware.smack.XMPPException.XMPPErrorException; @@ -47,6 +48,8 @@ import org.jivesoftware.smackx.mam.filter.MamResultFilter; import org.jivesoftware.smackx.rsm.packet.RSMSet; import org.jivesoftware.smackx.xdata.FormField; import org.jivesoftware.smackx.xdata.packet.DataForm; +import org.jxmpp.jid.EntityBareJid; +import org.jxmpp.jid.EntityFullJid; import org.jxmpp.jid.Jid; import org.jxmpp.util.XmppDateTime; @@ -70,7 +73,7 @@ public final class MamManager extends Manager { }); } - private static final Map INSTANCES = new WeakHashMap<>(); + private static final Map> INSTANCES = new WeakHashMap<>(); /** * Get the singleton instance of MamManager. @@ -78,19 +81,29 @@ public final class MamManager extends Manager { * @param connection * @return the instance of MamManager */ - public static synchronized MamManager getInstanceFor(XMPPConnection connection) { - MamManager mamManager = INSTANCES.get(connection); + public static MamManager getInstanceFor(XMPPConnection connection) { + return getInstanceFor(connection, null); + } - if (mamManager == null) { - mamManager = new MamManager(connection); - INSTANCES.put(connection, mamManager); + public static synchronized MamManager getInstanceFor(XMPPConnection connection, Jid archiveAddress) { + Map managers = INSTANCES.get(connection); + if (managers == null) { + managers = new HashMap<>(); + INSTANCES.put(connection, managers); + } + MamManager mamManager = managers.get(archiveAddress); + if (mamManager == null) { + mamManager = new MamManager(connection, archiveAddress); + managers.put(archiveAddress, mamManager); } - return mamManager; } - private MamManager(XMPPConnection connection) { + private final Jid archiveAddress; + + private MamManager(XMPPConnection connection, Jid archiveAddress) { super(connection); + this.archiveAddress = archiveAddress; } /** @@ -106,7 +119,7 @@ public final class MamManager extends Manager { */ public MamQueryResult queryArchive(Integer max) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException { - return queryArchive(max, null, null, null, null); + return queryArchive(null, max, null, null, null, null); } /** @@ -122,7 +135,7 @@ public final class MamManager extends Manager { */ public MamQueryResult queryArchive(Jid withJid) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException { - return queryArchive(null, null, null, withJid, null); + return queryArchive(null, null, null, null, withJid, null); } /** @@ -142,7 +155,7 @@ public final class MamManager extends Manager { */ public MamQueryResult queryArchive(Date start, Date end) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException { - return queryArchive(null, start, end, null, null); + return queryArchive(null, null, start, end, null, null); } /** @@ -158,7 +171,7 @@ public final class MamManager extends Manager { */ public MamQueryResult queryArchive(List additionalFields) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException { - return queryArchive(null, null, null, null, additionalFields); + return queryArchive(null, null, null, null, null, additionalFields); } /** @@ -175,7 +188,7 @@ public final class MamManager extends Manager { */ public MamQueryResult queryArchiveWithStartDate(Date start) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException { - return queryArchive(null, start, null, null, null); + return queryArchive(null, null, start, null, null, null); } /** @@ -192,9 +205,10 @@ public final class MamManager extends Manager { */ public MamQueryResult queryArchiveWithEndDate(Date end) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException { - return queryArchive(null, null, end, null, null); + return queryArchive(null, null, null, end, null, null); } + /** * Query archive applying filters: max count, start date, end date, from/to * JID and with additional fields. @@ -214,6 +228,32 @@ public final class MamManager extends Manager { public MamQueryResult queryArchive(Integer max, Date start, Date end, Jid withJid, List additionalFields) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException { + return queryArchive(null, max, start, end, withJid, additionalFields); + } + + + /** + * Query an message archive like a MUC archive or a pubsub node archive, addressed by an archiveAddress, applying + * filters: max count, start date, end date, from/to JID and with additional fields. When archiveAddress is null the + * default, the server will be requested. + * + * @param node The Pubsub node name, can be null + * @param max + * @param start + * @param end + * @param withJid + * @param additionalFields + * @return the MAM query result + * @throws NoResponseException + * @throws XMPPErrorException + * @throws NotConnectedException + * @throws InterruptedException + * @throws NotLoggedInException + */ + public MamQueryResult queryArchive(String node, Integer max, Date start, Date end, Jid withJid, + List additionalFields) + throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, + NotLoggedInException { DataForm dataForm = null; String queryId = UUID.randomUUID().toString(); @@ -225,8 +265,9 @@ public final class MamManager extends Manager { addAdditionalFields(additionalFields, dataForm); } - MamQueryIQ mamQueryIQ = new MamQueryIQ(queryId, dataForm); + MamQueryIQ mamQueryIQ = new MamQueryIQ(queryId, node, dataForm); mamQueryIQ.setType(IQ.Type.set); + mamQueryIQ.setTo(archiveAddress); addResultsLimit(max, mamQueryIQ); return queryArchive(mamQueryIQ); @@ -290,9 +331,31 @@ public final class MamManager extends Manager { * @throws NotLoggedInException */ public MamQueryResult page(DataForm dataForm, RSMSet rsmSet) throws NoResponseException, XMPPErrorException, + NotConnectedException, InterruptedException, NotLoggedInException { + + return page(null, dataForm, rsmSet); + + } + + /** + * Returns a page of the archive. + * + * @param node The Pubsub node name, can be null + * @param dataForm + * @param rsmSet + * @return the MAM query result + * @throws NoResponseException + * @throws XMPPErrorException + * @throws NotConnectedException + * @throws InterruptedException + * @throws NotLoggedInException + */ + public MamQueryResult page(String node, DataForm dataForm, RSMSet rsmSet) + throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException { - MamQueryIQ mamQueryIQ = new MamQueryIQ(UUID.randomUUID().toString(), dataForm); + MamQueryIQ mamQueryIQ = new MamQueryIQ(UUID.randomUUID().toString(), node, dataForm); mamQueryIQ.setType(IQ.Type.set); + mamQueryIQ.setTo(archiveAddress); mamQueryIQ.addExtension(rsmSet); return queryArchive(mamQueryIQ); } @@ -315,7 +378,7 @@ public final class MamManager extends Manager { XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException { RSMSet previousResultRsmSet = mamQueryResult.mamFin.getRSMSet(); RSMSet requestRsmSet = new RSMSet(count, previousResultRsmSet.getLast(), RSMSet.PageDirection.after); - return page(mamQueryResult.form, requestRsmSet); + return page(mamQueryResult, requestRsmSet); } /** @@ -336,14 +399,24 @@ public final class MamManager extends Manager { XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException { RSMSet previousResultRsmSet = mamQueryResult.mamFin.getRSMSet(); RSMSet requestRsmSet = new RSMSet(count, previousResultRsmSet.getFirst(), RSMSet.PageDirection.before); - return page(mamQueryResult.form, requestRsmSet); + return page(mamQueryResult, requestRsmSet); + } + + private MamQueryResult page(MamQueryResult mamQueryResult, RSMSet requestRsmSet) throws NoResponseException, + XMPPErrorException, NotConnectedException, NotLoggedInException, InterruptedException { + ensureMamQueryResultMatchesThisManager(mamQueryResult); + + return page(mamQueryResult.node, mamQueryResult.form, requestRsmSet); } /** * Obtain page before the first message saved (specific chat). + *

+ * Note that the messageUid is the XEP-0313 UID and not the stanza ID of the message. + *

* * @param chatJid - * @param firstMessageId + * @param messageUid the UID of the message of which messages before should be received. * @param max * @return the MAM query result * @throws XMPPErrorException @@ -352,19 +425,22 @@ public final class MamManager extends Manager { * @throws InterruptedException * @throws NoResponseException */ - public MamQueryResult pageBefore(Jid chatJid, String firstMessageId, int max) throws XMPPErrorException, + public MamQueryResult pageBefore(Jid chatJid, String messageUid, int max) throws XMPPErrorException, NotLoggedInException, NotConnectedException, InterruptedException, NoResponseException { - RSMSet rsmSet = new RSMSet(null, firstMessageId, -1, -1, null, max, null, -1); + RSMSet rsmSet = new RSMSet(null, messageUid, -1, -1, null, max, null, -1); DataForm dataForm = getNewMamForm(); addWithJid(chatJid, dataForm); - return page(dataForm, rsmSet); + return page(null, dataForm, rsmSet); } /** * Obtain page after the last message saved (specific chat). + *

+ * Note that the messageUid is the XEP-0313 UID and not the stanza ID of the message. + *

* * @param chatJid - * @param lastMessageId + * @param messageUid the UID of the message of which messages after should be received. * @param max * @return the MAM query result * @throws XMPPErrorException @@ -373,12 +449,12 @@ public final class MamManager extends Manager { * @throws InterruptedException * @throws NoResponseException */ - public MamQueryResult pageAfter(Jid chatJid, String lastMessageId, int max) throws XMPPErrorException, + public MamQueryResult pageAfter(Jid chatJid, String messageUid, int max) throws XMPPErrorException, NotLoggedInException, NotConnectedException, InterruptedException, NoResponseException { - RSMSet rsmSet = new RSMSet(lastMessageId, null, -1, -1, null, max, null, -1); + RSMSet rsmSet = new RSMSet(messageUid, null, -1, -1, null, max, null, -1); DataForm dataForm = getNewMamForm(); addWithJid(chatJid, dataForm); - return page(dataForm, rsmSet); + return page(null, dataForm, rsmSet); } /** @@ -409,9 +485,27 @@ public final class MamManager extends Manager { * @throws NotLoggedInException */ public List retrieveFormFields() throws NoResponseException, XMPPErrorException, NotConnectedException, + InterruptedException, NotLoggedInException { + return retrieveFormFields(null); + } + + /** + * Get the form fields supported by the server. + * + * @param node The Pubsub node name, can be null + * @return the list of form fields. + * @throws NoResponseException + * @throws XMPPErrorException + * @throws NotConnectedException + * @throws InterruptedException + * @throws NotLoggedInException + */ + public List retrieveFormFields(String node) + throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException { String queryId = UUID.randomUUID().toString(); - MamQueryIQ mamQueryIq = new MamQueryIQ(queryId); + MamQueryIQ mamQueryIq = new MamQueryIQ(queryId, node, null); + mamQueryIq.setTo(archiveAddress); MamQueryIQ mamResponseQueryIq = connection().createStanzaCollectorAndSend(mamQueryIq).nextResultOrThrow(); @@ -445,7 +539,7 @@ public final class MamManager extends Manager { forwardedMessages.add(mamResultExtension.getForwarded()); } - return new MamQueryResult(forwardedMessages, mamFinIQ, DataForm.from(mamQueryIq)); + return new MamQueryResult(forwardedMessages, mamFinIQ, mamQueryIq.getNode(), DataForm.from(mamQueryIq)); } /** @@ -455,15 +549,41 @@ public final class MamManager extends Manager { public final static class MamQueryResult { public final List forwardedMessages; public final MamFinIQ mamFin; + private final String node; private final DataForm form; - private MamQueryResult(List forwardedMessages, MamFinIQ mamFin, DataForm form) { + private MamQueryResult(List forwardedMessages, MamFinIQ mamFin, String node, DataForm form) { this.forwardedMessages = forwardedMessages; this.mamFin = mamFin; + this.node = node; this.form = form; } } + private void ensureMamQueryResultMatchesThisManager(MamQueryResult mamQueryResult) { + EntityFullJid localAddress = connection().getUser(); + EntityBareJid localBareAddress = null; + if (localAddress != null) { + localBareAddress = localAddress.asEntityBareJid(); + } + boolean isLocalUserArchive = archiveAddress == null || archiveAddress.equals(localBareAddress); + + Jid finIqFrom = mamQueryResult.mamFin.getFrom(); + + if (finIqFrom != null) { + if (finIqFrom.equals(archiveAddress) || (isLocalUserArchive && finIqFrom.equals(localBareAddress))) { + return; + } + throw new IllegalArgumentException("The given MamQueryResult is from the MAM archive '" + finIqFrom + + "' whereas this MamManager is responsible for '" + archiveAddress + '\''); + } + else if (!isLocalUserArchive) { + throw new IllegalArgumentException( + "The given MamQueryResult is from the local entity (user) MAM archive, whereas this MamManager is responsible for '" + + archiveAddress + '\''); + } + } + /** * Returns true if Message Archive Management is supported by the server. * diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/element/MamQueryIQ.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/element/MamQueryIQ.java index 9a0c19045..3773985ba 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/element/MamQueryIQ.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/element/MamQueryIQ.java @@ -108,6 +108,15 @@ public class MamQueryIQ extends IQ { return queryId; } + /** + * Get the Node name. + * + * @return the node + */ + public String getNode() { + return node; + } + /** * Get the data form. * 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 55b89911d..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 @@ -232,5 +232,27 @@ urn:xmpp:chat-markers:0 org.jivesoftware.smackx.chat_markers.provider.AcknowledgedProvider - + + + + 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 + org.jivesoftware.smackx.httpfileupload.provider.SlotProvider + + + file-too-large + urn:xmpp:http:upload + org.jivesoftware.smackx.httpfileupload.provider.FileTooLargeErrorProvider + + diff --git a/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.xml b/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.xml index c621a0d43..05b819a27 100644 --- a/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.xml +++ b/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.xml @@ -4,5 +4,6 @@ org.jivesoftware.smackx.iot.data.IoTDataManager org.jivesoftware.smackx.iot.discovery.IoTDiscoveryManager org.jivesoftware.smackx.iot.provisioning.IoTProvisioningManager + org.jivesoftware.smackx.httpfileupload.HttpFileUploadManager 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 new file mode 100644 index 000000000..d2abf74f6 --- /dev/null +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/FileTooLargeErrorCreateTest.java @@ -0,0 +1,39 @@ +/** + * + * 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; + + +import org.jivesoftware.smackx.httpfileupload.element.FileTooLargeError; +import org.junit.Assert; +import org.junit.Test; + +public class FileTooLargeErrorCreateTest { + String fileTooLargeErrorExtensionExample + = "" + + "20000" + + ""; + + @Test + public void checkFileTooLargeErrorExtensionCreation() { + FileTooLargeError fileTooLargeError = new FileTooLargeError(20000); + + Assert.assertEquals(20000, fileTooLargeError.getMaxFileSize()); + Assert.assertEquals(fileTooLargeErrorExtensionExample, fileTooLargeError.toXML().toString()); + + } + +} 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 new file mode 100644 index 000000000..ebb5170cd --- /dev/null +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/SlotCreateTest.java @@ -0,0 +1,46 @@ +/** + * + * 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; + + +import org.jivesoftware.smackx.httpfileupload.element.Slot; +import org.junit.Assert; +import org.junit.Test; + +import java.net.MalformedURLException; +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" + + ""; + + @Test + public void checkSlotRequestCreation() throws MalformedURLException { + Slot slot = new Slot(new URL("https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my_juliet.png"), + new URL("https://download.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"), + slot.getPutUrl()); + Assert.assertEquals(new URL("https://download.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my_juliet.png"), + slot.getGetUrl()); + + Assert.assertEquals(testSlot, slot.getChildElementXML().toString()); + } +} 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 new file mode 100644 index 000000000..43c6fe80d --- /dev/null +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/SlotRequestCreateTest.java @@ -0,0 +1,72 @@ +/** + * + * 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; + +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(JidTestUtil.DOMAIN_BARE_JID_1, "my_juliet.png", 23456, "image/jpeg"); + + Assert.assertEquals("my_juliet.png", slotRequest.getFilename()); + Assert.assertEquals(23456, slotRequest.getSize()); + Assert.assertEquals("image/jpeg", slotRequest.getContentType()); + + Assert.assertEquals(testRequest, slotRequest.getChildElementXML().toString()); + } + + @Test + public void checkSlotRequestCreationWithoutContentType() throws XmppStringprepException { + 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()); + Assert.assertEquals(null, slotRequest.getContentType()); + + Assert.assertEquals(testRequestWithoutContentType, slotRequest.getChildElementXML().toString()); + } + + @Test(expected = IllegalArgumentException.class) + public void checkSlotRequestCreationNegativeSize() { + new SlotRequest(JidTestUtil.DOMAIN_BARE_JID_1, "my_juliet.png", -23456, "image/jpeg"); + } + + @Test(expected = IllegalArgumentException.class) + public void checkSlotRequestCreationZeroSize() { + 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/provider/FileTooLargeErrorProviderTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/provider/FileTooLargeErrorProviderTest.java new file mode 100644 index 000000000..564b142d6 --- /dev/null +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/provider/FileTooLargeErrorProviderTest.java @@ -0,0 +1,58 @@ +/** + * + * 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.provider; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.util.PacketParserUtils; +import org.jivesoftware.smackx.httpfileupload.element.FileTooLargeError; +import org.junit.Assert; +import org.junit.Test; + +public class FileTooLargeErrorProviderTest { + + /** + * Example 7. Alternative response by the upload service if the file size was too large + * @see XEP-0363: HTTP File Upload 5. Error conditions + */ + String slotErrorFileToLarge + = "" + + "" + + "my_juliet.png" + + "23456" + + "" + + "" + + "" + + "File too large. The maximum file size is 20000 bytes" + + "" + + "20000" + + "" + + "" + + ""; + + @Test + public void checkSlotErrorFileToLarge() throws Exception { + IQ fileTooLargeErrorIQ = PacketParserUtils.parseStanza(slotErrorFileToLarge); + + Assert.assertEquals(IQ.Type.error, fileTooLargeErrorIQ.getType()); + + FileTooLargeError fileTooLargeError = FileTooLargeError.from(fileTooLargeErrorIQ); + Assert.assertEquals(20000, fileTooLargeError.getMaxFileSize()); + } +} diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/provider/SlotProviderTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/provider/SlotProviderTest.java new file mode 100644 index 000000000..4cef3c550 --- /dev/null +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/provider/SlotProviderTest.java @@ -0,0 +1,55 @@ +/** + * + * 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.provider; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.util.PacketParserUtils; +import org.jivesoftware.smackx.httpfileupload.element.Slot; +import org.junit.Assert; +import org.junit.Test; + +import java.net.URL; + + +public class SlotProviderTest { + + /** + * Example 6. The upload service responds with a slot + * @see XEP-0363: HTTP File Upload 4. Requesting a slot + */ + String slotExample + = "" + + "" + + "https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my_juliet.png" + + "https://download.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my_juliet.png" + + "" + + ""; + + @Test + public void checkSlotProvider() throws Exception { + 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"), + slot.getPutUrl()); + Assert.assertEquals(new URL("https://download.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/my_juliet.png"), + slot.getGetUrl()); + } +} diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/MamTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/MamTest.java index 6d7c7c46e..3327f8384 100644 --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/MamTest.java +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/MamTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2016 Fernando Ramirez + * Copyright 2016-2017 Fernando Ramirez * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,17 +16,16 @@ */ package org.jivesoftware.smackx.mam; -import static org.mockito.Mockito.mock; - import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import org.jivesoftware.smack.DummyConnection; import org.jivesoftware.smack.XMPPConnection; -import org.jivesoftware.smackx.ExperimentalInitializerTest; +import org.jivesoftware.smackx.InitExtensions; import org.jivesoftware.smackx.xdata.packet.DataForm; import org.junit.Before; -public class MamTest extends ExperimentalInitializerTest { +public class MamTest extends InitExtensions { protected XMPPConnection connection; protected String queryId; @@ -35,7 +34,7 @@ public class MamTest extends ExperimentalInitializerTest { @Before public void setup() { // mock connection - connection = mock(XMPPConnection.class); + connection = new DummyConnection(); // test query id queryId = "testid"; diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java index 3d240756e..1bc1de725 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java @@ -43,7 +43,10 @@ import org.jivesoftware.smack.filter.FromMatchesFilter; import org.jivesoftware.smack.filter.MessageTypeFilter; import org.jivesoftware.smack.filter.MessageWithSubjectFilter; import org.jivesoftware.smack.filter.NotFilter; +import org.jivesoftware.smack.filter.OrFilter; +import org.jivesoftware.smack.filter.PresenceTypeFilter; import org.jivesoftware.smack.filter.StanzaFilter; +import org.jivesoftware.smack.filter.StanzaIdFilter; import org.jivesoftware.smack.filter.StanzaExtensionFilter; import org.jivesoftware.smack.filter.StanzaTypeFilter; import org.jivesoftware.smack.filter.ToMatchesFilter; @@ -327,9 +330,17 @@ public class MultiUserChat { messageCollector = connection.createStanzaCollector(fromRoomGroupchatFilter); // Wait for a presence packet back from the server. - // Use a bare JID filter, since the room may rewrite the nickname. - StanzaFilter responseFilter = new AndFilter(FromMatchesFilter.createBare(getRoom()), new StanzaTypeFilter( - Presence.class), MUCUserStatusCodeFilter.STATUS_110_PRESENCE_TO_SELF); + // @formatter:off + StanzaFilter responseFilter = new AndFilter(StanzaTypeFilter.PRESENCE, + new OrFilter( + // We use a bare JID filter for positive responses, since the MUC service/room may rewrite the nickname. + new AndFilter(FromMatchesFilter.createBare(getRoom()), MUCUserStatusCodeFilter.STATUS_110_PRESENCE_TO_SELF), + // In case there is an error reply, we match on an error presence with the same stanza id and from the full + // JID we send the join presence to. + new AndFilter(FromMatchesFilter.createFull(joinPresence.getTo()), new StanzaIdFilter(joinPresence), PresenceTypeFilter.ERROR) + ) + ); + // @formatter:on Presence presence; try { presence = connection.createStanzaCollectorAndSend(responseFilter, joinPresence).nextResultOrThrow(conf.getTimeout()); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/ping/PingManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/ping/PingManager.java index 11f9dcd0f..39b77e81d 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/ping/PingManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/ping/PingManager.java @@ -1,6 +1,6 @@ /** * - * Copyright 2012-2015 Florian Schmaus + * Copyright 2012-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,10 @@ */ package org.jivesoftware.smackx.ping; -import java.util.Collections; -import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; +import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; @@ -105,8 +104,7 @@ public final class PingManager extends Manager { defaultPingInterval = interval; } - private final Set pingFailedListeners = Collections - .synchronizedSet(new HashSet()); + private final Set pingFailedListeners = new CopyOnWriteArraySet<>(); private final ScheduledExecutorService executorService; diff --git a/smack-im/src/main/java/org/jivesoftware/smack/roster/Roster.java b/smack-im/src/main/java/org/jivesoftware/smack/roster/Roster.java index e7e6dbce2..286c968b7 100644 --- a/smack-im/src/main/java/org/jivesoftware/smack/roster/Roster.java +++ b/smack-im/src/main/java/org/jivesoftware/smack/roster/Roster.java @@ -66,6 +66,7 @@ import org.jivesoftware.smack.roster.rosterstore.RosterStore; import org.jivesoftware.smack.util.Objects; import org.jxmpp.jid.BareJid; import org.jxmpp.jid.EntityBareJid; +import org.jxmpp.jid.EntityFullJid; import org.jxmpp.jid.Jid; import org.jxmpp.jid.FullJid; import org.jxmpp.jid.impl.JidCreate; @@ -1674,9 +1675,16 @@ public final class Roster extends Manager { final XMPPConnection connection = connection(); RosterPacket rosterPacket = (RosterPacket) iqRequest; + EntityFullJid localAddress = connection.getUser(); + if (localAddress == null) { + LOGGER.warning("Ignoring roster push " + iqRequest + " while " + connection + + " has no bound resource. This may be a server bug."); + return null; + } + // Roster push (RFC 6121, 2.1.6) // A roster push with a non-empty from not matching our address MUST be ignored - EntityBareJid jid = connection.getUser().asEntityBareJid(); + EntityBareJid jid = localAddress.asEntityBareJid(); Jid from = rosterPacket.getFrom(); if (from != null && !from.equals(jid)) { LOGGER.warning("Ignoring roster push with a non matching 'from' ourJid='" + jid + "' from='" + from 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/filetransfer/package-info.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/filetransfer/package-info.java deleted file mode 100644 index 8d52af604..000000000 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/filetransfer/package-info.java +++ /dev/null @@ -1,21 +0,0 @@ -/** - * - * Copyright 2015 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. - */ - -/** - * TODO describe me. - */ -package org.jivesoftware.smackx.filetransfer; diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/filetransfer/package-info.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/filetransfer/package-info.java new file mode 120000 index 000000000..538c3207c --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/filetransfer/package-info.java @@ -0,0 +1 @@ +../../../../../../../../smack-extensions/src/main/java/org/jivesoftware/smackx/filetransfer/package-info.java \ No newline at end of file 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/main/java/org/jivesoftware/smackx/iot/package-info.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/package-info.java deleted file mode 100644 index 6e800e994..000000000 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/package-info.java +++ /dev/null @@ -1,21 +0,0 @@ -/** - * - * Copyright 2015 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. - */ - -/** - * TODO describe me. - */ -package org.jivesoftware.smackx.iot; diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/package-info.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/package-info.java new file mode 120000 index 000000000..3c51eae31 --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/package-info.java @@ -0,0 +1 @@ +../../../../../../../../smack-experimental/src/main/java/org/jivesoftware/smackx/iot/package-info.java \ No newline at end of file diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/iqversion/package-info.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/iqversion/package-info.java deleted file mode 100644 index a864122f0..000000000 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/iqversion/package-info.java +++ /dev/null @@ -1,21 +0,0 @@ -/** - * - * Copyright 2015 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. - */ - -/** - * TODO describe me. - */ -package org.jivesoftware.smackx.iqversion; diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/iqversion/package-info.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/iqversion/package-info.java new file mode 120000 index 000000000..f1ac066f1 --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/iqversion/package-info.java @@ -0,0 +1 @@ +../../../../../../../../smack-extensions/src/main/java/org/jivesoftware/smackx/iqversion/package-info.java \ No newline at end of file diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/package-info.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/package-info.java deleted file mode 100644 index 0ecde2c27..000000000 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/package-info.java +++ /dev/null @@ -1,21 +0,0 @@ -/** - * - * Copyright 2015 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. - */ - -/** - * TODO describe me. - */ -package org.jivesoftware.smackx.muc; diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/package-info.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/package-info.java new file mode 120000 index 000000000..d2e3650fc --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/package-info.java @@ -0,0 +1 @@ +../../../../../../../../smack-extensions/src/main/java/org/jivesoftware/smackx/muc/package-info.java \ No newline at end of file diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/package-info.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/package-info.java deleted file mode 100644 index 01e4ee051..000000000 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/package-info.java +++ /dev/null @@ -1,21 +0,0 @@ -/** - * - * Copyright 2015 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. - */ - -/** - * TODO describe me. - */ -package org.jivesoftware.smackx; diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/package-info.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/package-info.java new file mode 120000 index 000000000..f0646658f --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/package-info.java @@ -0,0 +1 @@ +../../../../../../../smack-extensions/src/main/java/org/jivesoftware/smackx/package-info.java \ No newline at end of file diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/ping/package-info.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/ping/package-info.java deleted file mode 100644 index 92e6a80e4..000000000 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/ping/package-info.java +++ /dev/null @@ -1,21 +0,0 @@ -/** - * - * Copyright 2015 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. - */ - -/** - * TODO describe me. - */ -package org.jivesoftware.smackx.ping; diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/ping/package-info.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/ping/package-info.java new file mode 120000 index 000000000..edea7d8fd --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/ping/package-info.java @@ -0,0 +1 @@ +../../../../../../../../smack-extensions/src/main/java/org/jivesoftware/smackx/ping/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)