Smack 4.2.0

-----BEGIN PGP SIGNATURE-----
 
 iQGTBAABCgB9FiEEl3UFnzoh3OFr5PuuIjmn6PWFIFIFAljDKBtfFIAAAAAALgAo
 aXNzdWVyLWZwckBub3RhdGlvbnMub3BlbnBncC5maWZ0aGhvcnNlbWFuLm5ldDk3
 NzUwNTlGM0EyMURDRTE2QkU0RkJBRTIyMzlBN0U4RjU4NTIwNTIACgkQIjmn6PWF
 IFJB7Qf6AlkwpzMqq1g18jzEBFVX/3Sk2QWivEY7t3EhGuSguan2VIfd1fL0P85Q
 vLBm6Pw93haIxHXKRUKc8DINwP9yuRMvUotCN2hYVgfqfByHGhDCJLTNZ9atncL5
 JToptfhdRy6kgljVZPtpXOMXKBvaO3QOuTuC5cmz8PlidsYw0yUnliPLQ36uPRWX
 eaEXXbgmkjJh35WjsaafD/uM86OCqZahfvEf3e8bkPzdAayd0OKU67+v0ArA9P2E
 CiRU5vfco/vt2Qo41aLLIEOjSFfVX6Xh/pXxfQvInMAxies0KRLi5vonOmfrWRmi
 uIblzcYRXCSaZSgVN2yF8KzmF4pzcw==
 =qETn
 -----END PGP SIGNATURE-----

Merge tag '4.2.0'

Smack 4.2.0
This commit is contained in:
Florian Schmaus 2017-03-10 23:35:06 +01:00
commit 1a93b448db
51 changed files with 1996 additions and 218 deletions

View File

@ -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

View File

@ -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
------

View File

@ -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 {

View File

@ -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 |

View File

@ -141,6 +141,140 @@ hr {
<div id="pageBody">
<h2>4.2.0 -- <span style="font-weight: normal;">2017-03-10</span></h2>
<h2> Sub-task
</h2>
<ul>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-639'>SMACK-639</a>] - Add support for pre-approved subscription requests (RFC 6121 § 3.4)
</li>
</ul>
<h2> Bug
</h2>
<ul>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-306'>SMACK-306</a>] - loadRosterOnLogin has non-trivial side effect on getRoster
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-416'>SMACK-416</a>] - Refactor PEP to make it use the existing pubsub API.
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-674'>SMACK-674</a>] - PubSub Affiliation extension element is missing &#39;jid&#39; attribute, and is using wrong element name &#39;subscription&#39;
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-682'>SMACK-682</a>] - Add support for &quot;XEP-0360: Nonzas (are not Stanzas)&quot;
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-683'>SMACK-683</a>] - Using a Proxy with XMPPTCPConnection failes with &quot;SocketException: Unconnected sockets not implemented&quot;
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-691'>SMACK-691</a>] - Add support for MUCItem&#39;s Actor &#39;nick&#39;
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-705'>SMACK-705</a>] - PubSub&#39;s Affiliation.getElementName() returns wrong name
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-722'>SMACK-722</a>] - SASL X-OAUTH2 implementation incorrectly performs Base64 encoding twice
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-723'>SMACK-723</a>] - Support &quot;Caps Optimizations&quot; (XEP-0115 § 8.4)
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-724'>SMACK-724</a>] - Do not re-use the Socket after connect() failed.
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-725'>SMACK-725</a>] - ReconnectionManager should handle AlreadyConnectedException and AlreadyLoggedInException not as failure
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-741'>SMACK-741</a>] - Ad-hoc command &#39;note&#39; element &#39;type&#39; attribute should be treated as optional
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-745'>SMACK-745</a>] - Memory leak in MultiUserChat
</li>
</ul>
<h2> New Feature
</h2>
<ul>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-366'>SMACK-366</a>] - Add support for DNSSEC.
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-610'>SMACK-610</a>] - Add support for XEP-0080: User Location
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-619'>SMACK-619</a>] - Add roomDestroyed to MUC UserStatusListener
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-625'>SMACK-625</a>] - Add support for XEP-313: Message Archive Management
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-675'>SMACK-675</a>] - Add support for PubSub affiliation actions as owner
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-677'>SMACK-677</a>] - Add support for SASL &#39;authzid&#39; (Authorization Identity)
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-690'>SMACK-690</a>] - Add support for DNS-Based Authentication of Named Entities (DANE, RFC 6698)
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-731'>SMACK-731</a>] - Add support for XEP-0191: Blocking Command
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-732'>SMACK-732</a>] - Smack should be able to handle &quot;single equals sign&quot; SASL responses
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-740'>SMACK-740</a>] - Add support for Multi-User Chat Light
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-742'>SMACK-742</a>] - Add support for XEP-0133: Service Administration
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-747'>SMACK-747</a>] - Add support for XEP-0363: HTTP File Upload
</li>
</ul>
<h2> Task
</h2>
<ul>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-638'>SMACK-638</a>] - Call connection creation listeners from within AbstractXMPPConnection&#39;s constructor
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-644'>SMACK-644</a>] - Throw exception if account creation or password change is performed over insecure connections
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-655'>SMACK-655</a>] - Enable StreamManagement by default
</li>
</ul>
<h2> Improvement
</h2>
<ul>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-372'>SMACK-372</a>] - Make package protected methods in PEPItem public
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-572'>SMACK-572</a>] - Rejoin MUC rooms after reconnect
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-628'>SMACK-628</a>] - Rework Roster handling with anonymous connections
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-629'>SMACK-629</a>] - Rework how Smack handles anonymous connections
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-631'>SMACK-631</a>] - Improve ParsingExceptionCallback, allow it to be a functional interface
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-632'>SMACK-632</a>] - Make Smack interruptible
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-633'>SMACK-633</a>] - Allow clean and graceful disconnects (stream closing)
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-634'>SMACK-634</a>] - Use jxmpp-jid, add Jid class to replace String&#39;s being used as JIDs
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-646'>SMACK-646</a>] - Add support for MUC roomnick rewrite
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-647'>SMACK-647</a>] - Don&#39;t automatically call login() on connect() if the connection was authenticated before
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-648'>SMACK-648</a>] - Improve MultiUserChat API
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-657'>SMACK-657</a>] - Rename RosterEntry.getStatus and RosterPacket.ItemStatus to ItemAskStatus
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-663'>SMACK-663</a>] - Roster should be fully loaded when Roster.getInstanceFor(XMPPConnection) is called with a authenticated connection
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-665'>SMACK-665</a>] - Rename &#39;serviceName&#39; to &#39;xmppServiceDomain&#39;
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-666'>SMACK-666</a>] - Typo in &#39;RosterEntries.rosterEntires()&#39;, change to &#39;RosterEntries.rosterEntries()&#39;
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-703'>SMACK-703</a>] - Limit the stored presences of entities not in Roster
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-704'>SMACK-704</a>] - Pass down Message stanza in ChatStateListener
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-711'>SMACK-711</a>] - Improve the logging of TCP connection attempts.
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-720'>SMACK-720</a>] - Improve support for Tor and Hidden Services.
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-721'>SMACK-721</a>] - Report illegal Stream Management states to avoid OOM Exception
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-727'>SMACK-727</a>] - Add partial support for the IoT XEPs (XEP-0323, -0324, -0325, -0347)
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-733'>SMACK-733</a>] - Handle outgoing &#39;unavailable&#39; Presences in Roster
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-736'>SMACK-736</a>] - Add support for Chat Markers (XEP-0333)
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-737'>SMACK-737</a>] - Add support for Bits of Binary (XEP-0231)
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-738'>SMACK-738</a>] - Add support for Push Notifications (XEP-0357)
</li>
</ul>
<h2>4.1.9 -- <span style="font-weight: normal;">2016-11-19</span></h2>

View File

@ -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;
}

View File

@ -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<SecureRandom> SECURE_RANDOM = new ThreadLocal<SecureRandom>() {
@Override
protected SecureRandom initialValue() {
return new SecureRandom();
}
};
private static final Cache<String, Keys> CACHE = new LruCache<String, Keys>(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)) {

View File

@ -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;
}
}

View File

@ -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<Random> randGen = new ThreadLocal<Random>() {
@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<randBuffer.length; i++) {
randBuffer[i] = numbersAndLetters[randGen.nextInt(numbersAndLetters.length)];
randBuffer[i] = numbersAndLetters[random.nextInt(numbersAndLetters.length)];
}
return new String(randBuffer);
}
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
private static final ThreadLocal<SecureRandom> SECURE_RANDOM = new ThreadLocal<SecureRandom>() {
@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]);

View File

@ -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")
}

View File

@ -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 <a href="http://xmpp.org/extensions/xep-0363.html">XEP-0363: HTTP File Upload</a>
*/
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<XMPPConnection, HttpFileUploadManager> 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<String> values = field.getValues();
if (values.isEmpty()) {
return new UploadService(address, version);
}
Long maxFileSize = Long.valueOf(values.get(0));
return new UploadService(address, version, maxFileSize);
}
/**
* Discover upload service.
*
* 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<DiscoverInfo> 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<String, String> 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);
}
}

View File

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

View File

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

View File

@ -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 <a href="http://xmpp.org/extensions/xep-0363.html">XEP-0363: HTTP File Upload</a>
*/
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);
}
}

View File

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

View File

@ -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 <a href="http://xmpp.org/extensions/xep-0363.html">XEP-0363: HTTP File Upload</a>
*/
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<String, String> headers;
public Slot(URL putUrl, URL getUrl) {
this(putUrl, getUrl, null);
}
public Slot(URL putUrl, URL getUrl, Map<String, String> headers) {
this(putUrl, getUrl, headers, NAMESPACE);
}
protected Slot(URL putUrl, URL getUrl, Map<String, String> headers, String namespace) {
super(ELEMENT, namespace);
setType(Type.result);
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<String, String> getHeaders() {
return headers;
}
@Override
protected IQChildElementXmlStringBuilder getIQChildElementBuilder(IQChildElementXmlStringBuilder xml) {
xml.rightAngleBracket();
xml.element("put", putUrl.toString());
xml.element("get", getUrl.toString());
for (Map.Entry<String, String> entry : getHeaders().entrySet()) {
xml.openElement("header").attribute(entry.getKey(), entry.getValue());
}
return xml;
}
}

View File

@ -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 <a href="http://xmpp.org/extensions/xep-0363.html">XEP-0363: HTTP File Upload</a>
*/
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;
}
}