diff --git a/config/checkstyle.xml b/config/checkstyle.xml index 041e31ffb..d925f4909 100644 --- a/config/checkstyle.xml +++ b/config/checkstyle.xml @@ -6,6 +6,11 @@ + + + + + @@ -61,6 +66,14 @@ + + + + + diff --git a/documentation/extensions/index.md b/documentation/extensions/index.md index 05eb8e311..44739287b 100644 --- a/documentation/extensions/index.md +++ b/documentation/extensions/index.md @@ -103,6 +103,7 @@ Experimental Smack Extensions and currently supported XEPs of smack-experimental | [OMEMO Multi End Message and Object Encryption](omemo.md) | [XEP-0384](https://xmpp.org/extensions/xep-0384.html) | n/a | Encrypt messages using OMEMO encryption (currently only with smack-omemo-signal -> GPLv3). | | [Consistent Color Generation](consistent_colors.md) | [XEP-0392](https://xmpp.org/extensions/xep-0392.html) | 0.4.0 | Generate consistent colors for identifiers like usernames to provide a consistent user experience. | | [Message Markup](messagemarkup.md) | [XEP-0394](https://xmpp.org/extensions/xep-0394.html) | 0.1.0 | Style message bodies while keeping body and markup information separated. | +| DNS Queries over XMPP (DoX) | [XEP-0418](https://xmpp.org/extensions/xep-0418.html) | 0.1.0 | Send DNS queries and responses over XMPP. | Unofficial XMPP Extensions -------------------------- diff --git a/documentation/processing.md b/documentation/processing.md index 9a16917cf..10d6a66ae 100644 --- a/documentation/processing.md +++ b/documentation/processing.md @@ -20,7 +20,7 @@ and a stanza listener: ``` // Create a stanza filter to listen for new messages from a particular // user. We use an AndFilter to combine two other filters._ -StanzaFilter filter = new AndFilter(StanzaTypeFilter.Message, FromMatchesFilter.create("mary@jivesoftware.com")); +StanzaFilter filter = new AndFilter(StanzaTypeFilter.MESSAGE, FromMatchesFilter.create("mary@jivesoftware.com")); // Assume we've created an XMPPConnection named "connection". // First, register a stanza collector using the filter we created. diff --git a/settings.gradle b/settings.gradle index 53b0bd554..6188d60b6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -12,6 +12,7 @@ include 'smack-core', 'smack-debug-slf4j', 'smack-resolver-dnsjava', 'smack-resolver-minidns', + 'smack-resolver-minidns-dox', 'smack-resolver-javax', 'smack-sasl-javax', 'smack-sasl-provided', diff --git a/smack-android/src/main/java/org/jivesoftware/smack/util/stringencoder/android/AndroidBase64Encoder.java b/smack-android/src/main/java/org/jivesoftware/smack/util/stringencoder/android/AndroidBase64Encoder.java index 5d143737d..8e4d47d6c 100644 --- a/smack-android/src/main/java/org/jivesoftware/smack/util/stringencoder/android/AndroidBase64Encoder.java +++ b/smack-android/src/main/java/org/jivesoftware/smack/util/stringencoder/android/AndroidBase64Encoder.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2014 Florian Schmaus + * Copyright © 2014-2019 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,18 +42,17 @@ public final class AndroidBase64Encoder implements org.jivesoftware.smack.util.s } @Override - public byte[] decode(byte[] input, int offset, int len) { - return Base64.decode(input, offset, len, 0); + public String encodeToString(byte[] input) { + return Base64.encodeToString(input, BASE64_ENCODER_FLAGS); } @Override - public String encodeToString(byte[] input, int offset, int len) { - return Base64.encodeToString(input, offset, len, BASE64_ENCODER_FLAGS); + public String encodeToStringWithoutPadding(byte[] input) { + return Base64.encodeToString(input, BASE64_ENCODER_FLAGS | Base64.NO_PADDING); } @Override - public byte[] encode(byte[] input, int offset, int len) { - return Base64.encode(input, offset, len, BASE64_ENCODER_FLAGS); + public byte[] encode(byte[] input) { + return Base64.encode(input, BASE64_ENCODER_FLAGS); } - } diff --git a/smack-core/build.gradle b/smack-core/build.gradle index 4fcf904ce..69ee5d468 100644 --- a/smack-core/build.gradle +++ b/smack-core/build.gradle @@ -20,7 +20,6 @@ dependencies { testCompile "org.powermock:powermock-module-junit4-rule:$powerMockVersion" testCompile "org.powermock:powermock-api-mockito2:$powerMockVersion" testCompile 'com.jamesmurty.utils:java-xmlbuilder:1.2' - testCompile 'net.iharder:base64:2.3.8' } class CreateFileTask extends DefaultTask { diff --git a/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java b/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java index 49b1e516f..e82ad15aa 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java @@ -41,6 +41,7 @@ import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Queue; import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CopyOnWriteArraySet; @@ -343,6 +344,17 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { protected final AsyncButOrdered inOrderListeners = new AsyncButOrdered<>(); + /** + * An executor which uses {@link #asyncGoLimited(Runnable)} to limit the number of asynchronously processed runnables + * per connection. + */ + private final Executor limitedExcutor = new Executor() { + @Override + public void execute(Runnable runnable) { + asyncGoLimited(runnable); + } + }; + /** * The used host to establish the connection to */ @@ -1336,7 +1348,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { executorService = ASYNC_BUT_ORDERED.asExecutorFor(this); break; case async: - executorService = CACHED_EXECUTOR_SERVICE; + executorService = limitedExcutor; break; } final IQRequestHandler finalIqRequestHandler = iqRequestHandler; @@ -1353,7 +1365,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { return; } - assert (response.getType() == IQ.Type.result || response.getType() == IQ.Type.error); + assert response.isResponseIQ(); response.setTo(iqRequest.getFrom()); response.setStanzaId(iqRequest.getStanzaId()); @@ -1379,7 +1391,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { final Collection listenersToNotify = new LinkedList<>(); extractMatchingListeners(packet, asyncRecvListeners, listenersToNotify); for (final StanzaListener listener : listenersToNotify) { - asyncGo(new Runnable() { + asyncGoLimited(new Runnable() { @Override public void run() { try { @@ -1875,6 +1887,75 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { return getClass().getSimpleName() + '[' + localEndpointString + "] (" + getConnectionCounter() + ')'; } + /** + * A queue of deferred runnables that where not executed immediately because {@link #currentAsyncRunnables} reached + * {@link #maxAsyncRunnables}. Note that we use a {@code LinkedList} in order to avoid space blowups in case the + * list ever becomes very big and shrinks again. + */ + private final Queue deferredAsyncRunnables = new LinkedList<>(); + + private int deferredAsyncRunnablesCount; + + private int deferredAsyncRunnablesCountPrevious; + + private int maxAsyncRunnables = SmackConfiguration.getDefaultConcurrencyLevelLimit(); + + private int currentAsyncRunnables; + + protected void asyncGoLimited(final Runnable runnable) { + Runnable wrappedRunnable = new Runnable() { + @Override + public void run() { + runnable.run(); + + synchronized (deferredAsyncRunnables) { + Runnable defferredRunnable = deferredAsyncRunnables.poll(); + if (defferredRunnable == null) { + currentAsyncRunnables--; + } else { + deferredAsyncRunnablesCount--; + asyncGo(defferredRunnable); + } + } + } + }; + + synchronized (deferredAsyncRunnables) { + if (currentAsyncRunnables < maxAsyncRunnables) { + currentAsyncRunnables++; + asyncGo(wrappedRunnable); + } else { + deferredAsyncRunnablesCount++; + deferredAsyncRunnables.add(wrappedRunnable); + } + + final int HIGH_WATERMARK = 100; + final int INFORM_WATERMARK = 20; + + final int deferredAsyncRunnablesCount = this.deferredAsyncRunnablesCount; + + if (deferredAsyncRunnablesCount >= HIGH_WATERMARK + && deferredAsyncRunnablesCountPrevious < HIGH_WATERMARK) { + LOGGER.log(Level.WARNING, "High watermark of " + HIGH_WATERMARK + " simultaneous executing runnables reached"); + } else if (deferredAsyncRunnablesCount >= INFORM_WATERMARK + && deferredAsyncRunnablesCountPrevious < INFORM_WATERMARK) { + LOGGER.log(Level.INFO, INFORM_WATERMARK + " simultaneous executing runnables reached"); + } + + deferredAsyncRunnablesCountPrevious = deferredAsyncRunnablesCount; + } + } + + public void setMaxAsyncOperations(int maxAsyncOperations) { + if (maxAsyncOperations < 1) { + throw new IllegalArgumentException("Max async operations must be greater than 0"); + } + + synchronized (deferredAsyncRunnables) { + maxAsyncRunnables = maxAsyncOperations; + } + } + protected static void asyncGo(Runnable runnable) { CACHED_EXECUTOR_SERVICE.execute(runnable); } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/AsyncButOrdered.java b/smack-core/src/main/java/org/jivesoftware/smack/AsyncButOrdered.java index 00fb94549..6fb6244ab 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/AsyncButOrdered.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/AsyncButOrdered.java @@ -55,6 +55,16 @@ public class AsyncButOrdered { private final Map threadActiveMap = new WeakHashMap<>(); + private final Executor executor; + + public AsyncButOrdered() { + this(null); + } + + public AsyncButOrdered(Executor executor) { + this.executor = executor; + } + /** * Invoke the given {@link Runnable} asynchronous but ordered in respect to the given key. * @@ -86,7 +96,11 @@ public class AsyncButOrdered { if (newHandler) { Handler handler = new Handler(keyQueue, key); threadActiveMap.put(key, true); - AbstractXMPPConnection.asyncGo(handler); + if (executor == null) { + AbstractXMPPConnection.asyncGo(handler); + } else { + executor.execute(handler); + } } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/ConnectionConfiguration.java b/smack-core/src/main/java/org/jivesoftware/smack/ConnectionConfiguration.java index 28c7ea782..b87f2ccd2 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/ConnectionConfiguration.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/ConnectionConfiguration.java @@ -1,6 +1,6 @@ /** * - * Copyright 2003-2007 Jive Software, 2017-2018 Florian Schmaus. + * Copyright 2003-2007 Jive Software, 2017-2019 Florian Schmaus. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,9 +49,29 @@ import org.minidns.dnsname.DnsName; import org.minidns.util.InetAddressUtil; /** - * Configuration to use while establishing the connection to the server. + * The connection configuration used for XMPP client-to-server connections. A well configured XMPP service will + * typically only require you to provide two parameters: The XMPP address, also known as the JID, of the user and the + * password. All other configuration parameters could ideally be determined automatically by Smack. Hence it is often + * enough to call {@link Builder#setXmppAddressAndPassword(CharSequence, String)}. + *

+ * Technically there are typically at least two parameters required: Some kind of credentials for authentication. And + * the XMPP service domain. The credentials often consists of a username and password use for the SASL authentication. + * But there are also other authentication mechanisms, like client side certificates, which do not require a particular + * username and password. + *

+ *

+ * There are some misconceptions about XMPP client-to-server parameters: The first is that the username used for + * authentication will be equal to the localpart of the bound XMPP address after authentication. While this is usually + * true, it is not required. Technically the username used for authentication and the resulting XMPP address are + * completely independent from each other. The second common misconception steers from the terms "XMPP host" and "XMPP + * service domain": An XMPP service host is a system which hosts one or multiple XMPP domains. The "XMPP service domain" + * will be usually the domainpart of the bound JID. This domain is used to verify the remote endpoint, typically using + * TLS. This third misconception is that the XMPP service domain is required to become the domainpart of the bound JID. + * Again, while this is very common to be true, it is not strictly required. + *

* * @author Gaston Dombiak + * @author Florian Schmaus */ public abstract class ConnectionConfiguration { @@ -534,6 +554,39 @@ public abstract class ConnectionConfiguration { } } + /** + * Convenience method to configure the username, password and XMPP service domain. + * + * @param jid the XMPP address of the user. + * @param password the password of the user. + * @return a reference to this builder. + * @throws XmppStringprepException in case the XMPP address is not valid. + * @see #setXmppAddressAndPassword(EntityBareJid, String) + * @since 4.4.0 + */ + public B setXmppAddressAndPassword(CharSequence jid, String password) throws XmppStringprepException { + return setXmppAddressAndPassword(JidCreate.entityBareFrom(jid), password); + } + + /** + * Convenience method to configure the username, password and XMPP service domain. The localpart of the provided + * JID is used as username and the domanipart is used as XMPP service domain. + *

+ * Please note that this does and can not configure the client XMPP address. XMPP services are not required to + * assign bound JIDs where the localpart matches the username and the domainpart matches the verified domainpart. + * Although most services will follow that pattern. + *

+ * + * @param jid + * @param password + * @return a reference to this builder. + * @since 4.4.0 + */ + public B setXmppAddressAndPassword(EntityBareJid jid, String password) { + setUsernameAndPassword(jid.getLocalpart(), password); + return setXmppDomain(jid.asDomainBareJid()); + } + /** * Set the XMPP entities username and password. *

diff --git a/smack-core/src/main/java/org/jivesoftware/smack/SmackConfiguration.java b/smack-core/src/main/java/org/jivesoftware/smack/SmackConfiguration.java index 01f2995a4..10695df47 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/SmackConfiguration.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/SmackConfiguration.java @@ -365,4 +365,19 @@ public final class SmackConfiguration { public static void setUnknownIqRequestReplyMode(UnknownIqRequestReplyMode unknownIqRequestReplyMode) { SmackConfiguration.unknownIqRequestReplyMode = Objects.requireNonNull(unknownIqRequestReplyMode, "Must set mode"); } + + private static final int defaultConcurrencyLevelLimit; + + static { + int availableProcessors = Runtime.getRuntime().availableProcessors(); + if (availableProcessors < 8) { + defaultConcurrencyLevelLimit = 8; + } else { + defaultConcurrencyLevelLimit = (int) (availableProcessors * 1.1); + } + } + + public static int getDefaultConcurrencyLevelLimit() { + return defaultConcurrencyLevelLimit; + } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/XMPPConnection.java b/smack-core/src/main/java/org/jivesoftware/smack/XMPPConnection.java index c6c2db78f..cd7c6cf7e 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/XMPPConnection.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/XMPPConnection.java @@ -70,6 +70,27 @@ import org.jxmpp.jid.EntityFullJid; * disconnected and then connected again. Listeners of the XMPPConnection will be retained across * connections. *

+ *

Incoming Stanza Listeners

+ * Most callbacks (listeners, handlers, …) than you can add to a connection come in three different variants: + *
    + *
  • standard
  • + *
  • async (asynchronous)
  • + *
  • sync (synchronous)
  • + *
+ *

+ * Standard callbacks are invoked concurrently, but it is ensured that the same callback is never run concurrently. + * The callback's identity is used as key for that. The events delivered to the callback preserve the order of the + * causing events of the connection. + *

+ *

+ * Asynchronous callbacks are run decoupled from the connections main event loop. Hence a callback triggered by + * stanza B may (appear to) invoked before a callback triggered by stanza A, even though stanza A arrived before B. + *

+ *

+ * Synchronous callbacks are run synchronous to the main event loop of a connection. Hence they are invoked in the + * exact order of how events happen there, most importantly the arrival order of incoming stanzas. You should only + * use synchronous callbacks in rare situations. + *

* * @author Matt Tucker * @author Guenther Niess diff --git a/smack-core/src/main/java/org/jivesoftware/smack/debugger/ConsoleDebugger.java b/smack-core/src/main/java/org/jivesoftware/smack/debugger/ConsoleDebugger.java index fb5d80a69..def9b310e 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/debugger/ConsoleDebugger.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/debugger/ConsoleDebugger.java @@ -16,12 +16,11 @@ */ package org.jivesoftware.smack.debugger; -import java.io.PrintWriter; -import java.io.StringWriter; import java.text.SimpleDateFormat; import java.util.Date; import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.util.ExceptionUtil; /** * Very simple debugger that prints to the console (stdout) the sent and received stanzas. Use @@ -55,12 +54,8 @@ public class ConsoleDebugger extends AbstractDebugger { @Override protected void log(String logMessage, Throwable throwable) { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - // CHECKSTYLE:OFF - throwable.printStackTrace(pw); - // CHECKSTYLE:ON - log(logMessage + sw); + String stacktrace = ExceptionUtil.getStackTrace(throwable); + log(logMessage + '\n' + stacktrace); } public static final class Factory implements SmackDebuggerFactory { diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/AbstractError.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/AbstractError.java index ece30af7b..e75e7a0e9 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/AbstractError.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/AbstractError.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import org.jivesoftware.smack.util.ExceptionUtil; import org.jivesoftware.smack.util.Objects; import org.jivesoftware.smack.util.PacketUtil; import org.jivesoftware.smack.util.XmlStringBuilder; @@ -150,6 +151,17 @@ public class AbstractError { return getThis(); } + public B setDescriptiveEnText(String descriptiveEnText, Exception exception) { + StringBuilder sb = new StringBuilder(512); + sb.append(descriptiveEnText) + .append('\n'); + + String stacktrace = ExceptionUtil.getStackTrace(exception); + sb.append(stacktrace); + + return setDescriptiveEnText(sb.toString()); + } + public B setTextNamespace(String textNamespace) { this.textNamespace = textNamespace; return getThis(); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/IQ.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/IQ.java index 3626b9b9b..9b15c9c95 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/IQ.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/IQ.java @@ -105,6 +105,16 @@ public abstract class IQ extends Stanza { } } + /** + * Return true if this IQ is a request, i.e. an IQ of type {@link Type#result} or {@link Type#error}. + * + * @return true if IQ type is 'result' or 'error', false otherwise. + * @since 4.4 + */ + public boolean isResponseIQ() { + return !isRequestIQ(); + } + public final String getChildElementName() { return childElementName; } @@ -287,7 +297,7 @@ public abstract class IQ extends Stanza { * @return a new {@link Type#error IQ.Type.error} IQ based on the originating IQ. */ public static ErrorIQ createErrorResponse(final IQ request, final StanzaError.Builder error) { - if (!(request.getType() == Type.get || request.getType() == Type.set)) { + if (!request.isRequestIQ()) { throw new IllegalArgumentException( "IQ must be of type 'set' or 'get'. Original IQ: " + request.toXML()); } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/ExceptionUtil.java b/smack-core/src/main/java/org/jivesoftware/smack/util/ExceptionUtil.java new file mode 100644 index 000000000..1c282381a --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/ExceptionUtil.java @@ -0,0 +1,38 @@ +/** + * + * Copyright 2019 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.smack.util; + +import java.io.PrintWriter; +import java.io.StringWriter; + +public class ExceptionUtil { + + public static String getStackTrace(Throwable throwable) { + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter); + + // CHECKSTYLE:OFF + throwable.printStackTrace(printWriter); + // CHECKSTYLE:ON + + printWriter.flush(); + + StringBuffer stringBuffer = stringWriter.getBuffer(); + return stringBuffer.toString(); + } + +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/RandomUtil.java b/smack-core/src/main/java/org/jivesoftware/smack/util/RandomUtil.java new file mode 100644 index 000000000..857557869 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/RandomUtil.java @@ -0,0 +1,50 @@ +/** + * + * Copyright 2003-2007 Jive Software, 2016-2019 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.smack.util; + +import java.security.SecureRandom; +import java.util.Random; + +public class RandomUtil { + + static final ThreadLocal SECURE_RANDOM = new ThreadLocal() { + @Override + protected SecureRandom initialValue() { + return new SecureRandom(); + } + }; + + /** + * Pseudo-random number generator object for use with randomString(). + * The Random class is not considered to be cryptographically secure, so + * only use these random Strings for low to medium security applications. + */ + static final ThreadLocal RANDOM = new ThreadLocal() { + @Override + protected Random initialValue() { + return new Random(); + } + }; + + public static int nextSecureRandomInt(int bound) { + return SECURE_RANDOM.get().nextInt(bound); + } + + public static int nextSecureRandomInt() { + return SECURE_RANDOM.get().nextInt(); + } +} 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 affe6a047..a3d7ad096 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-2018 Florian Schmaus. + * Copyright 2003-2007 Jive Software, 2016-2019 Florian Schmaus. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ package org.jivesoftware.smack.util; import java.io.UnsupportedEncodingException; -import java.security.SecureRandom; import java.util.Collection; import java.util.Iterator; import java.util.Random; @@ -253,18 +252,6 @@ public class StringUtils { } } - /** - * Pseudo-random number generator object for use with randomString(). - * 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 ThreadLocal randGen = new ThreadLocal() { - @Override - protected Random initialValue() { - return new Random(); - } - }; - /** * Array of numbers and letters of mixed case. Numbers appear in the list * twice so that there is a more equal chance that a number will be picked. @@ -288,23 +275,16 @@ public class StringUtils { * @return a random String of numbers and letters of the specified length. */ public static String insecureRandomString(int length) { - return randomString(length, randGen.get()); + return randomString(length, RandomUtil.RANDOM.get()); } - private static final ThreadLocal SECURE_RANDOM = new ThreadLocal() { - @Override - protected SecureRandom initialValue() { - return new SecureRandom(); - } - }; - public static String randomString(final int length) { - return randomString(length, SECURE_RANDOM.get()); + return randomString(length, RandomUtil.SECURE_RANDOM.get()); } public static String randomString(final int length, Random random) { - if (length < 1) { - return null; + if (length == 0) { + return ""; } byte[] randomBytes = new byte[length]; diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/stringencoder/Base64.java b/smack-core/src/main/java/org/jivesoftware/smack/util/stringencoder/Base64.java index 80b6928d6..fb6ae327b 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/stringencoder/Base64.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/stringencoder/Base64.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2014-2015 Florian Schmaus + * Copyright © 2014-2019 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,29 +39,19 @@ public class Base64 { } public static final String encodeToString(byte[] input) { - byte[] bytes = encode(input); - try { - return new String(bytes, StringUtils.USASCII); - } catch (UnsupportedEncodingException e) { - throw new AssertionError(e); - } + return base64encoder.encodeToString(input); } public static final String encodeToString(byte[] input, int offset, int len) { - byte[] bytes = encode(input, offset, len); - try { - return new String(bytes, StringUtils.USASCII); - } catch (UnsupportedEncodingException e) { - throw new AssertionError(e); - } + return encodeToString(slice(input, offset, len)); + } + + public static final String encodeToStringWithoutPadding(byte[] input) { + return base64encoder.encodeToStringWithoutPadding(input); } public static final byte[] encode(byte[] input) { - return encode(input, 0, input.length); - } - - public static final byte[] encode(byte[] input, int offset, int len) { - return base64encoder.encode(input, offset, len); + return base64encoder.encode(input); } public static final String decodeToString(String string) { @@ -73,34 +63,43 @@ public class Base64 { } } - public static final String decodeToString(byte[] input, int offset, int len) { - byte[] bytes = decode(input, offset, len); + // TODO: We really should not mask the IllegalArgumentException. But some unit test depend on this behavior, like + // ibb.packet.DataPacketExtension.shouldReturnNullIfDataIsInvalid(). + public static final byte[] decode(String string) { try { - return new String(bytes, StringUtils.UTF8); - } catch (UnsupportedEncodingException e) { - throw new IllegalStateException("UTF-8 not supported", e); + return base64encoder.decode(string); + } catch (IllegalArgumentException e) { + return null; } } - public static final byte[] decode(String string) { - return base64encoder.decode(string); - } - public static final byte[] decode(byte[] input) { - return base64encoder.decode(input, 0, input.length); + String string; + try { + string = new String(input, StringUtils.USASCII); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(e); + } + return decode(string); } - public static final byte[] decode(byte[] input, int offset, int len) { - return base64encoder.decode(input, offset, len); + private static byte[] slice(byte[] input, int offset, int len) { + if (offset == 0 && len == input.length) { + return input; + } + + byte[] res = new byte[len]; + System.arraycopy(input, offset, res, 0, len); + return res; } public interface Encoder { byte[] decode(String string); - byte[] decode(byte[] input, int offset, int len); + String encodeToString(byte[] input); - String encodeToString(byte[] input, int offset, int len); + String encodeToStringWithoutPadding(byte[] input); - byte[] encode(byte[] input, int offset, int len); + byte[] encode(byte[] input); } } diff --git a/smack-core/src/test/java/org/jivesoftware/smack/DummyConnection.java b/smack-core/src/test/java/org/jivesoftware/smack/DummyConnection.java index 81c1f9ead..3dc30cc76 100644 --- a/smack-core/src/test/java/org/jivesoftware/smack/DummyConnection.java +++ b/smack-core/src/test/java/org/jivesoftware/smack/DummyConnection.java @@ -64,6 +64,11 @@ public class DummyConnection extends AbstractXMPPConnection { this(getDummyConfigurationBuilder().build()); } + public DummyConnection(CharSequence username, String password, String serviceName) throws XmppStringprepException { + this(getDummyConfigurationBuilder().setUsernameAndPassword(username, password).setXmppDomain( + JidCreate.domainBareFrom(serviceName)).build()); + } + private EntityFullJid getUserJid() { try { return JidCreate.entityFullFrom(config.getUsername() diff --git a/smack-core/src/test/java/org/jivesoftware/smack/test/util/SmackTestSuite.java b/smack-core/src/test/java/org/jivesoftware/smack/test/util/SmackTestSuite.java index 20aeedefb..ae60c4543 100644 --- a/smack-core/src/test/java/org/jivesoftware/smack/test/util/SmackTestSuite.java +++ b/smack-core/src/test/java/org/jivesoftware/smack/test/util/SmackTestSuite.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2014 Florian Schmaus + * Copyright © 2014-2019 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,14 +16,10 @@ */ package org.jivesoftware.smack.test.util; -import java.io.IOException; -import java.io.UnsupportedEncodingException; +import java.util.Base64; -import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.stringencoder.Base64.Encoder; -import net.iharder.Base64; - /** * The SmackTestSuite takes care of initializing Smack for the unit tests. For example the Base64 * encoder is configured. @@ -35,53 +31,24 @@ public class SmackTestSuite { @Override public byte[] decode(String string) { - try { - return Base64.decode(string); - } - catch (IllegalArgumentException e) { - // Expected by e.g. the unit test. - // " Base64-encoded string must have at least four characters, but length specified was 1", - // should not cause an exception, but instead null should be returned. Maybe - // this should be changed in a later Smack release, so that the actual exception - // is handled. - return null; - } - catch (IOException e) { - throw new IllegalStateException(e); - } + return Base64.getDecoder().decode(string); } @Override - public byte[] decode(byte[] input, int offset, int len) { - try { - return Base64.decode(input, offset, len, 0); - } - catch (IllegalArgumentException e) { - // Expected by e.g. the unit test. - // " Base64-encoded string must have at least four characters, but length specified was 1", - // should not cause an exception, but instead null should be returned. Maybe - // this should be changed in a later Smack release, so that the actual exception - // is handled. - return null; - } catch (IOException e) { - throw new IllegalStateException(e); - } + public String encodeToString(byte[] input) { + return Base64.getEncoder().encodeToString(input); } @Override - public String encodeToString(byte[] input, int offset, int len) { - return Base64.encodeBytes(input, offset, len); + public String encodeToStringWithoutPadding(byte[] input) { + return Base64.getEncoder().withoutPadding().encodeToString(input); } @Override - public byte[] encode(byte[] input, int offset, int len) { - String string = encodeToString(input, offset, len); - try { - return string.getBytes(StringUtils.USASCII); - } catch (UnsupportedEncodingException e) { - throw new AssertionError(e); - } + public byte[] encode(byte[] input) { + return Base64.getEncoder().encode(input); } + }); } } diff --git a/smack-core/src/test/java/org/jivesoftware/smack/util/MemoryLeakTestUtil.java b/smack-core/src/test/java/org/jivesoftware/smack/util/MemoryLeakTestUtil.java new file mode 100644 index 000000000..e014caca5 --- /dev/null +++ b/smack-core/src/test/java/org/jivesoftware/smack/util/MemoryLeakTestUtil.java @@ -0,0 +1,140 @@ +/** + * + * Copyright 2019 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.smack.util; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import java.lang.ref.PhantomReference; +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.logging.Logger; + +import org.jivesoftware.smack.DummyConnection; +import org.jivesoftware.smack.Manager; + +import org.jxmpp.stringprep.XmppStringprepException; + +/** + * Utility class to test for memory leaks caused by Smack. + *

+ * Note that this test is based on the assumption that it is possible to trigger a full garbage collection run, which is + * not the case. See also this + * stackoverflow + * question. Hence the {@link #triggerGarbageCollection()} method defined in this class is not portable and depends + * on implementation depended Java Virtual Machine behavior. + *

+ * + * @see SMACK-383 Jira Issue + */ +public class MemoryLeakTestUtil { + + private static final Logger LOGGER = Logger.getLogger(MemoryLeakTestUtil.class.getName()); + + public static void noResourceLeakTest(Function managerSupplier) + throws XmppStringprepException, IllegalArgumentException, InterruptedException { + final int numConnections = 10; + + ReferenceQueue connectionsReferenceQueue = new ReferenceQueue<>(); + ReferenceQueue managerReferenceQueue = new ReferenceQueue<>(); + + // Those two sets ensure that we hold a strong reference to the created PhantomReferences until the end of the + // test. + @SuppressWarnings("ModifiedButNotUsed") + Set> connectionsPhantomReferences = new HashSet<>(); + @SuppressWarnings("ModifiedButNotUsed") + Set> managersPhantomReferences = new HashSet<>(); + + List connections = new ArrayList<>(numConnections); + for (int i = 0; i < numConnections; i++) { + DummyConnection connection = new DummyConnection("foo" + i, "bar", "baz"); + + PhantomReference connectionPhantomReference = new PhantomReference<>(connection, connectionsReferenceQueue); + connectionsPhantomReferences.add(connectionPhantomReference); + + Manager manager = managerSupplier.apply(connection); + PhantomReference managerPhantomReference = new PhantomReference(manager, managerReferenceQueue); + managersPhantomReferences.add(managerPhantomReference); + + connections.add(connection); + } + + // Clear the only references to the created connections. + connections = null; + + triggerGarbageCollection(); + + // Now the connections should have been gc'ed, but not managers not yet. + assertReferencesQueueSize(connectionsReferenceQueue, numConnections); + assertReferencesQueueIsEmpty(managerReferenceQueue); + + // We new create another connection and explicitly a new Manager. This will trigger the cleanup mechanism in the + // WeakHashMaps used by the Manager's iNSTANCE field. This should clean up all references to the Managers. + DummyConnection connection = new DummyConnection("last", "bar", "baz"); + @SuppressWarnings("unused") + Manager manager = managerSupplier.apply(connection); + + // The previous Managers should now be reclaimable by the garbage collector. First trigger a GC run. + triggerGarbageCollection(); + + // Now the Managers should have been freed and this means we should see their phantom references in the + // reference queue. + assertReferencesQueueSize(managerReferenceQueue, numConnections); + } + + private static void assertReferencesQueueSize(ReferenceQueue referenceQueue, int expectedSize) throws IllegalArgumentException, InterruptedException { + final int timeout = 60000; + for (int itemsRemoved = 0; itemsRemoved < expectedSize; ++itemsRemoved) { + Reference reference = referenceQueue.remove(timeout); + assertNotNull("No reference found after " + timeout + "ms", reference); + reference.clear(); + } + + Reference reference = referenceQueue.poll(); + assertNull("Reference queue is not empty when it should be", reference); + } + + private static void assertReferencesQueueIsEmpty(ReferenceQueue referenceQueue) { + Reference reference = referenceQueue.poll(); + assertNull(reference); + } + + private static void triggerGarbageCollection() { + Object object = new Object(); + WeakReference weakReference = new WeakReference<>(object); + object = null; + + int gcCalls = 0; + do { + if (gcCalls > 1000) { + throw new AssertionError("No observed gargabe collection after " + gcCalls + " calls of System.gc()"); + } + System.gc(); + gcCalls++; + } while (weakReference.get() != null); + + // Note that this is no guarantee that a *full* garbage collection run has been made, which is what we actually + // need here in order to prevent false negatives. + LOGGER.finer("Observed garbage collection after " + gcCalls + " calls of System.gc()"); + } +} diff --git a/smack-core/src/test/java/org/jivesoftware/smack/util/StringUtilsTest.java b/smack-core/src/test/java/org/jivesoftware/smack/util/StringUtilsTest.java index 0cff5a730..7679a118e 100644 --- a/smack-core/src/test/java/org/jivesoftware/smack/util/StringUtilsTest.java +++ b/smack-core/src/test/java/org/jivesoftware/smack/util/StringUtilsTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2003-2007 Jive Software. + * Copyright 2003-2007 Jive Software, 2019 Florian Schmaus. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -88,13 +88,7 @@ public class StringUtilsTest { @Test public void testRandomString() { - // Boundary test - String result = StringUtils.randomString(-1); - assertNull(result); - - // Zero length string test - result = StringUtils.randomString(0); - assertNull(result); + String result; // Test various lengths - make sure the same length is returned result = StringUtils.randomString(4); @@ -104,4 +98,17 @@ public class StringUtilsTest { result = StringUtils.randomString(128); assertTrue(result.length() == 128); } + + @Test(expected = NegativeArraySizeException.class) + public void testNegativeArraySizeException() { + // Boundary test + StringUtils.randomString(-1); + } + + @Test + public void testZeroLengthRandomString() { + // Zero length string test + String result = StringUtils.randomString(0); + assertEquals("", result); + } } diff --git a/smack-experimental/build.gradle b/smack-experimental/build.gradle index 2f4d4e0d5..0e902139c 100644 --- a/smack-experimental/build.gradle +++ b/smack-experimental/build.gradle @@ -11,5 +11,5 @@ dependencies { testCompile project(path: ":smack-core", configuration: "archives") testCompile project(path: ":smack-extensions", configuration: "testRuntime") - compile "org.bouncycastle:bcprov-jdk15on:1.57" + compile "org.bouncycastle:bcprov-jdk15on:$bouncyCastleVersion" } diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/dox/DnsOverXmppManager.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/dox/DnsOverXmppManager.java new file mode 100644 index 000000000..634eab400 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/dox/DnsOverXmppManager.java @@ -0,0 +1,169 @@ +/** + * + * Copyright 2019 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.dox; + +import java.io.IOException; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.logging.Logger; + +import org.jivesoftware.smack.Manager; +import org.jivesoftware.smack.SmackException.NoResponseException; +import org.jivesoftware.smack.SmackException.NotConnectedException; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException.XMPPErrorException; +import org.jivesoftware.smack.iqrequest.AbstractIqRequestHandler; +import org.jivesoftware.smack.iqrequest.IQRequestHandler.Mode; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.StanzaError; +import org.jivesoftware.smack.packet.StanzaError.Condition; +import org.jivesoftware.smack.packet.StanzaError.Type; +import org.jivesoftware.smack.util.RandomUtil; +import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; +import org.jivesoftware.smackx.dox.element.DnsIq; + +import org.jxmpp.jid.Jid; +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsmessage.Question; + +public final class DnsOverXmppManager extends Manager { + + private static final Logger LOGGER = Logger.getLogger(DnsOverXmppManager.class.getName()); + + private static final Map INSTANCES = new WeakHashMap<>(); + + public static synchronized DnsOverXmppManager getInstanceFor(XMPPConnection connection) { + DnsOverXmppManager manager = INSTANCES.get(connection); + if (manager == null) { + manager = new DnsOverXmppManager(connection); + INSTANCES.put(connection, manager); + } + return manager; + } + + private static final String NAMESPACE = DnsIq.NAMESPACE; + + private static DnsOverXmppResolver defaultResolver; + + public void setDefaultDnsOverXmppResolver(DnsOverXmppResolver resolver) { + defaultResolver = resolver; + } + + private final ServiceDiscoveryManager serviceDiscoveryManager; + + private DnsOverXmppResolver resolver = defaultResolver; + + private boolean enabled; + + private final AbstractIqRequestHandler dnsIqRequestHandler = new AbstractIqRequestHandler( + DnsIq.ELEMENT, DnsIq.NAMESPACE, IQ.Type.get, Mode.async) { + + @Override + public IQ handleIQRequest(IQ iqRequest) { + DnsOverXmppResolver resolver = DnsOverXmppManager.this.resolver; + if (resolver == null) { + LOGGER.info("Resolver was null while attempting to handle " + iqRequest); + return null; + } + + DnsIq dnsIqRequest = (DnsIq) iqRequest; + DnsMessage query = dnsIqRequest.getDnsMessage(); + + DnsMessage response; + try { + response = resolver.resolve(query); + } catch (IOException exception) { + StanzaError.Builder errorBuilder = StanzaError.getBuilder() + .setType(Type.CANCEL) + .setCondition(Condition.internal_server_error) + .setDescriptiveEnText("Exception while resolving your DNS query", exception) + ; + + IQ errorResponse = IQ.createErrorResponse(iqRequest, errorBuilder); + return errorResponse; + } + + if (query.id != response.id) { + // The ID may not match because the resolver returned a cached result. + response = response.asBuilder().setId(query.id).build(); + } + + DnsIq dnsIqResult = new DnsIq(response); + dnsIqResult.setType(IQ.Type.result); + return dnsIqResult; + } + }; + + private DnsOverXmppManager(XMPPConnection connection) { + super(connection); + this.serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection); + } + + public synchronized void setDnsOverXmppResolver(DnsOverXmppResolver resolver) { + this.resolver = resolver; + if (resolver == null) { + disable(); + } + } + + public synchronized void enable() { + if (enabled) return; + + if (resolver == null) { + throw new IllegalStateException("No DnsOverXmppResolver configured"); + } + + XMPPConnection connection = connection(); + if (connection == null) return; + + connection.registerIQRequestHandler(dnsIqRequestHandler); + serviceDiscoveryManager.addFeature(NAMESPACE); + } + + public synchronized void disable() { + if (!enabled) return; + + XMPPConnection connection = connection(); + if (connection == null) return; + + serviceDiscoveryManager.removeFeature(NAMESPACE); + connection.unregisterIQRequestHandler(dnsIqRequestHandler); + } + + public boolean isSupported(Jid jid) + throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { + return serviceDiscoveryManager.supportsFeature(jid, NAMESPACE); + } + + public DnsMessage query(Jid jid, Question question) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { + DnsMessage queryMessage = DnsMessage.builder() + .addQuestion(question) + .setId(RandomUtil.nextSecureRandomInt()) + .setRecursionDesired(true) + .build(); + return query(jid, queryMessage); + } + + public DnsMessage query(Jid jid, DnsMessage query) + throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { + DnsIq queryIq = new DnsIq(query, jid); + + DnsIq responseIq = connection().sendIqRequestAndWaitForResponse(queryIq); + + return responseIq.getDnsMessage(); + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/dox/DnsOverXmppResolver.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/dox/DnsOverXmppResolver.java new file mode 100644 index 000000000..26f6cfa5e --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/dox/DnsOverXmppResolver.java @@ -0,0 +1,27 @@ +/** + * + * Copyright 2019 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.dox; + +import java.io.IOException; + +import org.minidns.dnsmessage.DnsMessage; + +public interface DnsOverXmppResolver { + + DnsMessage resolve(DnsMessage query) throws IOException; + +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/dox/element/DnsIq.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/dox/element/DnsIq.java new file mode 100644 index 000000000..77c626eae --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/dox/element/DnsIq.java @@ -0,0 +1,80 @@ +/** + * + * Copyright 2019 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.dox.element; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.util.stringencoder.Base64; + +import org.jxmpp.jid.Jid; +import org.minidns.dnsmessage.DnsMessage; + +public class DnsIq extends IQ { + + public static final String ELEMENT = "dns"; + public static final String NAMESPACE = "urn:xmpp:dox:0"; + + private final DnsMessage dnsMessage; + + private String base64DnsMessage; + + public DnsIq(String base64DnsMessage) throws IOException { + this(Base64.decode(base64DnsMessage)); + this.base64DnsMessage = base64DnsMessage; + } + + public DnsIq(byte[] dnsMessage) throws IOException { + this(new DnsMessage(dnsMessage)); + } + + public DnsIq(DnsMessage dnsQuery, Jid to) { + this(dnsQuery); + setTo(to); + setType(Type.get); + } + + public DnsIq(DnsMessage dnsMessage) { + super(ELEMENT, NAMESPACE); + this.dnsMessage = dnsMessage; + } + + public DnsMessage getDnsMessage() { + return dnsMessage; + } + + @SuppressWarnings("ByteBufferBackingArray") + public String getDnsMessageBase64Encoded() { + if (base64DnsMessage == null) { + ByteBuffer byteBuffer = dnsMessage.getInByteBuffer(); + byte[] bytes = byteBuffer.array(); + base64DnsMessage = Base64.encodeToStringWithoutPadding(bytes); + } + return base64DnsMessage; + } + + @Override + protected IQChildElementXmlStringBuilder getIQChildElementBuilder(IQChildElementXmlStringBuilder xml) { + xml.rightAngleBracket(); + + xml.escape(getDnsMessageBase64Encoded()); + + return xml; + } + +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/dox/element/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/dox/element/package-info.java new file mode 100644 index 000000000..163c14fb8 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/dox/element/package-info.java @@ -0,0 +1,21 @@ +/** + * + * Copyright 2019 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. + */ + +/** + * XEP-0418: DNS Queries over XMPP (DoX) XML providers. + */ +package org.jivesoftware.smackx.dox.element; diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/dox/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/dox/package-info.java new file mode 100644 index 000000000..0aa8e5137 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/dox/package-info.java @@ -0,0 +1,21 @@ +/** + * + * Copyright 2019 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. + */ + +/** + * Smack's API for XEP-0418: DNS Queries over XMPP (Dox). + */ +package org.jivesoftware.smackx.dox; diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/dox/provider/DnsIqProvider.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/dox/provider/DnsIqProvider.java new file mode 100644 index 000000000..3800060eb --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/dox/provider/DnsIqProvider.java @@ -0,0 +1,38 @@ +/** + * + * Copyright 2019 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.dox.provider; + +import java.io.IOException; + +import org.jivesoftware.smack.packet.XmlEnvironment; +import org.jivesoftware.smack.parsing.SmackParsingException; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smackx.dox.element.DnsIq; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +public class DnsIqProvider extends IQProvider { + + @Override + public DnsIq parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment) + throws XmlPullParserException, IOException, SmackParsingException { + String base64DnsMessage = parser.nextText(); + return new DnsIq(base64DnsMessage); + } + +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/dox/provider/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/dox/provider/package-info.java new file mode 100644 index 000000000..c0c8ce8fe --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/dox/provider/package-info.java @@ -0,0 +1,21 @@ +/** + * + * Copyright 2019 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. + */ + +/** + * XEP-0418: DNS Queries over XMPP (DoX) XML providers. + */ +package org.jivesoftware.smackx.dox.provider; diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/hashes/HashManager.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/hashes/HashManager.java index e4607ad4d..a872b376c 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/hashes/HashManager.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/hashes/HashManager.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2017 Paul Schaub + * Copyright © 2017 Paul Schaub, 2019 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,6 @@ import static org.jivesoftware.smackx.hashes.HashManager.ALGORITHM.SHA_512; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; import java.security.Security; import java.util.Arrays; import java.util.Collections; @@ -61,7 +60,6 @@ public final class HashManager extends Manager { static { Security.addProvider(new BouncyCastleProvider()); } - public static final String PROVIDER = "BC"; public static final String PREFIX_NS_ALGO = "urn:xmpp:hash-function-text-names:"; @@ -224,52 +222,52 @@ public final class HashManager extends Manager { try { switch (algorithm) { case MD5: - md = MessageDigest.getInstance("MD5", PROVIDER); + md = MessageDigest.getInstance("MD5"); break; case SHA_1: - md = MessageDigest.getInstance("SHA-1", PROVIDER); + md = MessageDigest.getInstance("SHA-1"); break; case SHA_224: - md = MessageDigest.getInstance("SHA-224", PROVIDER); + md = MessageDigest.getInstance("SHA-224"); break; case SHA_256: - md = MessageDigest.getInstance("SHA-256", PROVIDER); + md = MessageDigest.getInstance("SHA-256"); break; case SHA_384: - md = MessageDigest.getInstance("SHA-384", PROVIDER); + md = MessageDigest.getInstance("SHA-384"); break; case SHA_512: - md = MessageDigest.getInstance("SHA-512", PROVIDER); + md = MessageDigest.getInstance("SHA-512"); break; case SHA3_224: - md = MessageDigest.getInstance("SHA3-224", PROVIDER); + md = MessageDigest.getInstance("SHA3-224"); break; case SHA3_256: - md = MessageDigest.getInstance("SHA3-256", PROVIDER); + md = MessageDigest.getInstance("SHA3-256"); break; case SHA3_384: - md = MessageDigest.getInstance("SHA3-384", PROVIDER); + md = MessageDigest.getInstance("SHA3-384"); break; case SHA3_512: - md = MessageDigest.getInstance("SHA3-512", PROVIDER); + md = MessageDigest.getInstance("SHA3-512"); break; case BLAKE2B160: - md = MessageDigest.getInstance("BLAKE2b-160", PROVIDER); + md = MessageDigest.getInstance("BLAKE2b-160"); break; case BLAKE2B256: - md = MessageDigest.getInstance("BLAKE2b-256", PROVIDER); + md = MessageDigest.getInstance("BLAKE2b-256"); break; case BLAKE2B384: - md = MessageDigest.getInstance("BLAKE2b-384", PROVIDER); + md = MessageDigest.getInstance("BLAKE2b-384"); break; case BLAKE2B512: - md = MessageDigest.getInstance("BLAKE2b-512", PROVIDER); + md = MessageDigest.getInstance("BLAKE2b-512"); break; default: throw new AssertionError("Invalid enum value: " + algorithm); } return md; - } catch (NoSuchAlgorithmException | NoSuchProviderException e) { + } catch (NoSuchAlgorithmException e) { throw new AssertionError(e); } } diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/jingle_filetransfer/JingleFileTransferManager.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/jingle_filetransfer/JingleFileTransferManager.java index 7b96cdbd4..a50ea0519 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/jingle_filetransfer/JingleFileTransferManager.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/jingle_filetransfer/JingleFileTransferManager.java @@ -32,7 +32,7 @@ public final class JingleFileTransferManager extends Manager { super(connection); } - public static JingleFileTransferManager getInstanceFor(XMPPConnection connection) { + public static synchronized JingleFileTransferManager getInstanceFor(XMPPConnection connection) { JingleFileTransferManager manager = INSTANCES.get(connection); if (manager == null) { manager = new JingleFileTransferManager(connection); 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 3e3e52a22..dd7db3426 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 @@ -178,7 +178,9 @@ public final class MamManager extends Manager { * @param connection the XMPP connection to get the archive for. * @return the instance of MamManager. */ + // CHECKSTYLE:OFF:RegexpSingleline public static MamManager getInstanceFor(XMPPConnection connection) { + // CHECKSTYLE:ON:RegexpSingleline return getInstanceFor(connection, (Jid) null); } diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/reference/ReferenceManager.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/reference/ReferenceManager.java index d3bdfce12..b588aa5e3 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/reference/ReferenceManager.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/reference/ReferenceManager.java @@ -51,7 +51,7 @@ public final class ReferenceManager extends Manager { * @param connection xmpp connection * @return reference manager instance */ - public static ReferenceManager getInstanceFor(XMPPConnection connection) { + public static synchronized ReferenceManager getInstanceFor(XMPPConnection connection) { ReferenceManager manager = INSTANCES.get(connection); if (manager == null) { manager = new ReferenceManager(connection); diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/sid/StableUniqueStanzaIdManager.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/sid/StableUniqueStanzaIdManager.java index 99ec03f5d..a9fd4fcfa 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/sid/StableUniqueStanzaIdManager.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/sid/StableUniqueStanzaIdManager.java @@ -78,7 +78,7 @@ public final class StableUniqueStanzaIdManager extends Manager { * @param connection xmpp-connection * @return manager instance for the connection */ - public static StableUniqueStanzaIdManager getInstanceFor(XMPPConnection connection) { + public static synchronized StableUniqueStanzaIdManager getInstanceFor(XMPPConnection connection) { StableUniqueStanzaIdManager manager = INSTANCES.get(connection); if (manager == null) { manager = new StableUniqueStanzaIdManager(connection); diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/SpoilerManager.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/SpoilerManager.java index f3bb80c85..fa5c8680b 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/SpoilerManager.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/SpoilerManager.java @@ -61,7 +61,7 @@ public final class SpoilerManager extends Manager { * @param connection xmpp connection * @return SpoilerManager */ - public static SpoilerManager getInstanceFor(XMPPConnection connection) { + public static synchronized SpoilerManager getInstanceFor(XMPPConnection connection) { SpoilerManager manager = INSTANCES.get(connection); if (manager == null) { manager = new SpoilerManager(connection); 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 a47159e88..974c149f2 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 @@ -317,4 +317,11 @@ org.jivesoftware.smackx.message_markup.provider.MarkupElementProvider + + + dns + urn:xmpp:dox:0 + org.jivesoftware.smackx.dox.provider.DnsIqProvider + + diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/ServiceDiscoveryManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/ServiceDiscoveryManager.java index 674e5ca9a..38b965577 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/ServiceDiscoveryManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/ServiceDiscoveryManager.java @@ -1,6 +1,6 @@ /** * - * Copyright 2003-2007 Jive Software, 2018 Florian Schmaus. + * Copyright 2003-2007 Jive Software, 2018-2019 Florian Schmaus. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -227,7 +227,7 @@ public final class ServiceDiscoveryManager extends Manager { /** * Returns the type of client that will be returned when asked for the client identity in a * disco request. The valid types are defined by the category client. Follow this link to learn - * the possible types: Jabber::Registrar. + * the possible types: XMPP Registry for Service Discovery Identities * * @return the type of client that will be returned when asked for the client identity in a * disco request. @@ -271,8 +271,8 @@ public final class ServiceDiscoveryManager extends Manager { */ public Set getIdentities() { Set res = new HashSet<>(identities); - // Add the default identity that must exist - res.add(defaultIdentity); + // Add the main identity that must exist + res.add(identity); return Collections.unmodifiableSet(res); } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/packet/DiscoverInfo.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/packet/DiscoverInfo.java index 1f02a0edb..c31a45aad 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/packet/DiscoverInfo.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/packet/DiscoverInfo.java @@ -263,7 +263,7 @@ public class DiscoverInfo extends IQ implements TypedCloneable { * Represents the identity of a given XMPP entity. An entity may have many identities but all * the identities SHOULD have the same name.

* - * Refer to Jabber::Registrar + * Refer to XMPP Registry for Service Discovery Identities * in order to get the official registry of values for the category and type * attributes. * @@ -327,7 +327,7 @@ public class DiscoverInfo extends IQ implements TypedCloneable { /** * Returns the entity's category. To get the official registry of values for the - * 'category' attribute refer to Jabber::Registrar + * 'category' attribute refer to XMPP Registry for Service Discovery Identities. * * @return the entity's category. */ @@ -346,7 +346,7 @@ public class DiscoverInfo extends IQ implements TypedCloneable { /** * Returns the entity's type. To get the official registry of values for the - * 'type' attribute refer to Jabber::Registrar + * 'type' attribute refer to XMPP Registry for Service Discovery Identities. * * @return the entity's type. */ diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/geoloc/GeoLocationManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/geoloc/GeoLocationManager.java index e6b75b46b..2afe22663 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/geoloc/GeoLocationManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/geoloc/GeoLocationManager.java @@ -124,7 +124,7 @@ public final class GeoLocationManager extends Manager { private LeafNode getNode() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotALeafNodeException { - return PubSubManager.getInstance(connection()).getOrCreateLeafNode(GeoLocation.NAMESPACE); + return PubSubManager.getInstanceFor(connection()).getOrCreateLeafNode(GeoLocation.NAMESPACE); } } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/JingleTransportMethodManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/JingleTransportMethodManager.java index 848dc1671..9a9941a07 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/JingleTransportMethodManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/JingleTransportMethodManager.java @@ -48,7 +48,7 @@ public final class JingleTransportMethodManager extends Manager { super(connection); } - public static JingleTransportMethodManager getInstanceFor(XMPPConnection connection) { + public static synchronized JingleTransportMethodManager getInstanceFor(XMPPConnection connection) { JingleTransportMethodManager manager = INSTANCES.get(connection); if (manager == null) { manager = new JingleTransportMethodManager(connection); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/transports/jingle_ibb/JingleIBBTransportManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/transports/jingle_ibb/JingleIBBTransportManager.java index 500a7b0a8..63dada1ae 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/transports/jingle_ibb/JingleIBBTransportManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/transports/jingle_ibb/JingleIBBTransportManager.java @@ -38,7 +38,7 @@ public final class JingleIBBTransportManager extends JingleTransportManager getJoinedRooms(EntityJid user) throws NoResponseException, XMPPErrorException, + public List getJoinedRooms(EntityFullJid user) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { // Send the disco packet to the user DiscoverItems result = serviceDiscoveryManager.discoverItems(user, DISCO_NODE); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pep/PepManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pep/PepManager.java index f581e9830..26eba67e9 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pep/PepManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pep/PepManager.java @@ -120,7 +120,7 @@ public final class PepManager extends Manager { // TODO Add filter to check if from supports PubSub as per xep163 2 2.4 connection.addSyncStanzaListener(packetListener, FROM_BARE_JID_WITH_EVENT_EXTENSION_FILTER); - pepPubSubManager = PubSubManager.getInstance(connection, null); + pepPubSubManager = PubSubManager.getInstanceFor(connection, null); } public PubSubManager getPepPubSubManager() { diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PubSubManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PubSubManager.java index 77895035f..2904e0392 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PubSubManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PubSubManager.java @@ -88,7 +88,9 @@ public final class PubSubManager extends Manager { * @param connection * @return the default PubSub manager. */ - public static PubSubManager getInstance(XMPPConnection connection) { + // CHECKSTYLE:OFF:RegexpSingleline + public static PubSubManager getInstanceFor(XMPPConnection connection) { + // CHECKSTYLE:ON:RegexpSingleline DomainBareJid pubSubService = null; if (connection.isAuthenticated()) { try { @@ -110,7 +112,7 @@ public final class PubSubManager extends Manager { throw new RuntimeException(e); } } - return getInstance(connection, pubSubService); + return getInstanceFor(connection, pubSubService); } /** @@ -121,7 +123,9 @@ public final class PubSubManager extends Manager { * @param pubSubService the PubSub service, may be null. * @return a PubSub manager for the connection and service. */ - public static PubSubManager getInstance(XMPPConnection connection, BareJid pubSubService) { + // CHECKSTYLE:OFF:RegexpSingleline + public static PubSubManager getInstanceFor(XMPPConnection connection, BareJid pubSubService) { + // CHECKSTYLE:ON:RegexpSingleline if (pubSubService != null && connection.isAuthenticated() && connection.getUser().asBareJid().equals(pubSubService)) { // PEP service. pubSubService = null; @@ -147,6 +151,28 @@ public final class PubSubManager extends Manager { return pubSubManager; } + /** + * Deprecated. + * + * @deprecated use {@link #getInstanceFor(XMPPConnection)} instead. + */ + @Deprecated + // TODO: Remove in Smack 4.5. + public static PubSubManager getInstance(XMPPConnection connection) { + return getInstanceFor(connection); + } + + /** + * Deprecated. + * + * @deprecated use {@link #getInstanceFor(XMPPConnection, BareJid)} instead. + */ + @Deprecated + // TODO: Remove in Smack 4.5. + public static PubSubManager getInstance(XMPPConnection connection, BareJid pubSubService) { + return getInstanceFor(connection, pubSubService); + } + /** * Create a pubsub manager associated to the specified connection where * the pubsub requests require a specific to address for packets. diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/muc/MucMemoryLeakTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/muc/MucMemoryLeakTest.java new file mode 100644 index 000000000..2f6fa7634 --- /dev/null +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/muc/MucMemoryLeakTest.java @@ -0,0 +1,32 @@ +/** + * + * Copyright 2019 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.muc; + +import org.jivesoftware.smack.test.util.SmackTestSuite; +import org.jivesoftware.smack.util.MemoryLeakTestUtil; + +import org.junit.Test; +import org.jxmpp.stringprep.XmppStringprepException; + +public class MucMemoryLeakTest extends SmackTestSuite { + + @Test + public void mucMemoryLeakTest() throws XmppStringprepException, IllegalArgumentException, InterruptedException { + MemoryLeakTestUtil.noResourceLeakTest((c) -> MultiUserChatManager.getInstanceFor(c)); + } + +} 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 e9652e652..04cf9cb89 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 @@ -560,7 +560,8 @@ public final class Roster extends Manager { } /** - * Add a roster loaded listener. + * Add a roster loaded listener. Roster loaded listeners are invoked once the {@link Roster} + * was successfully loaded. * * @param rosterLoadedListener the listener to add. * @return true if the listener was not already added. @@ -587,6 +588,20 @@ public final class Roster extends Manager { } } + /** + * Add a {@link PresenceEventListener}. Such a listener will be fired whenever certain + * presence events happen.

+ * Among those events are: + *

    + *
  • 'available' presence received + *
  • 'unavailable' presence received + *
  • 'error' presence received + *
  • 'subscribed' presence received + *
  • 'unsubscribed' presence received + *
+ * @param presenceEventListener listener to add. + * @return true if the listener was not already added. + */ public boolean addPresenceEventListener(PresenceEventListener presenceEventListener) { return presenceEventListeners.add(presenceEventListener); } @@ -1516,7 +1531,29 @@ public final class Roster extends Manager { final Presence presence = (Presence) packet; final Jid from = presence.getFrom(); - final BareJid key = from != null ? from.asBareJid() : null; + final BareJid key; + if (from != null) { + key = from.asBareJid(); + } else { + XMPPConnection connection = connection(); + if (connection == null) { + LOGGER.finest("Connection was null while trying to handle exotic presence stanza: " + presence); + return; + } + // Assume the presence come "from the users account on the server" since no from was set (RFC 6120 § + // 8.1.2.1 4.). Note that getUser() may return null, but should never return null in this case as where + // connected. + EntityFullJid myJid = connection.getUser(); + if (myJid == null) { + LOGGER.info( + "Connection had no local address in Roster's presence listener." + + " Possibly we received a presence without from before being authenticated." + + " Presence: " + presence); + return; + } + LOGGER.info("Exotic presence stanza without from received: " + presence); + key = myJid.asBareJid(); + } asyncButOrdered.performAsyncButOrdered(key, new Runnable() { @Override diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/XmppConnectionStressTest.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/XmppConnectionStressTest.java index acb62b869..aa0965306 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/XmppConnectionStressTest.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/XmppConnectionStressTest.java @@ -151,18 +151,37 @@ public class XmppConnectionStressTest { // Sanity check: All markers before must be true, all markers including the messageNumber marker must be false. for (int i = 0; i < fromMarkers.length; i++) { - if ((i < messageNumber && !fromMarkers[i]) - || (i >= messageNumber && fromMarkers[i])) { - // TODO: Better exception. - Exception exception = new Exception("out of order"); - receiveExceptions.put(connection, exception); - // TODO: Current Smack design does not guarantee that the listener won't be invoked again. - // This is because the decission to invoke a sync listeners is done at a different place - // then invoking the listener. - connection.removeSyncStanzaListener(this); - receivedSemaphore.release(); - return; + final String inOrderViolation; + if (i < messageNumber && !fromMarkers[i]) { + // A previous message was missing. + inOrderViolation = "not yet message #"; + } else if (i >= messageNumber && fromMarkers[i]) { + // We already received a new message. + // TODO: Can it ever happen that this is taken? Wouldn't we prior run into the "a previous + // message is missing" case? + inOrderViolation = "we already received a later (or the same) message #"; + } else { + continue; } + + + StringBuilder exceptionMessage = new StringBuilder(); + exceptionMessage.append("We received message #").append(messageNumber).append(" but "); + exceptionMessage.append(inOrderViolation); + exceptionMessage.append(i); + exceptionMessage.append("\nMessage with id ").append(stanza.getStanzaId()) + .append(" from ").append(from) + .append(" to ").append(stanza.getTo()); + + Exception exception = new Exception(exceptionMessage.toString()); + receiveExceptions.put(connection, exception); + // TODO: Current Smack design does not guarantee that the listener won't be invoked again. + // This is because the decission to invoke a sync listeners is done at a different place + // then invoking the listener. + connection.removeSyncStanzaListener(this); + receivedSemaphore.release(); + // TODO: Do not return here? + return; } fromMarkers[messageNumber] = true; diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionManager.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionManager.java index ad076ccf2..4ea5531c3 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionManager.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionManager.java @@ -240,6 +240,10 @@ public class XmppConnectionManager { } connections.clear(); + + if (accountRegistrationConnection != null) { + accountRegistrationConnection.disconnect(); + } } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoManagerSetupHelper.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoManagerSetupHelper.java index 0c2e7fa34..c4cf718d3 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoManagerSetupHelper.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoManagerSetupHelper.java @@ -125,7 +125,7 @@ public class OmemoManagerSetupHelper { } public static void cleanUpPubSub(OmemoManager omemoManager) { - PubSubManager pm = PubSubManager.getInstance(omemoManager.getConnection(),omemoManager.getOwnJid()); + PubSubManager pm = PubSubManager.getInstanceFor(omemoManager.getConnection(),omemoManager.getOwnJid()); try { omemoManager.requestDeviceListUpdateFor(omemoManager.getOwnJid()); } catch (SmackException.NotConnectedException | InterruptedException | SmackException.NoResponseException | PubSubException.NotALeafNodeException | XMPPException.XMPPErrorException e) { diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/pubsub/PubSubIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/pubsub/PubSubIntegrationTest.java index e13351c91..92ca0c643 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/pubsub/PubSubIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/pubsub/PubSubIntegrationTest.java @@ -17,12 +17,16 @@ package org.jivesoftware.smackx.pubsub; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import java.util.List; import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.XMPPException.XMPPErrorException; +import org.jivesoftware.smack.packet.StanzaError; import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTest; @@ -42,33 +46,70 @@ public class PubSubIntegrationTest extends AbstractSmackIntegrationTest { if (pubSubService == null) { throw new TestNotPossibleException("No PubSub service found"); } - pubSubManagerOne = PubSubManager.getInstance(conOne, pubSubService); + pubSubManagerOne = PubSubManager.getInstanceFor(conOne, pubSubService); if (!pubSubManagerOne.canCreateNodesAndPublishItems()) { throw new TestNotPossibleException("PubSub service does not allow node creation"); } } + /** + * Asserts that an event notification (publication without item) can be published to + * a node that is both 'notification-only' as well as 'transient'. + */ @SmackIntegrationTest - public void simplePubSubNodeTest() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { - final String nodename = "sinttest-simple-nodename-" + testRunId; - final String itemId = "sintest-simple-itemid-" + testRunId; + public void transientNotificationOnlyNodeWithoutItemTest() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { + final String nodename = "sinttest-transient-notificationonly-withoutitem-nodename-" + testRunId; ConfigureForm defaultConfiguration = pubSubManagerOne.getDefaultConfiguration(); ConfigureForm config = new ConfigureForm(defaultConfiguration.createAnswerForm()); - // Configure the node as "Notification-Only Node", which in turn means that - // items do not need payload, to prevent payload-required error responses when - // publishing the item. + // Configure the node as "Notification-Only Node". config.setDeliverPayloads(false); - // Set persistent_items to 'false' (was previously 'true') as workaround for ejabberd issue #2799 - // (https://github.com/processone/ejabberd/issues/2799). + // Configure the node as "transient" (set persistent_items to 'false') + config.setPersistentItems(false); + Node node = pubSubManagerOne.createNode(nodename, config); + try { + LeafNode leafNode = (LeafNode) node; + leafNode.publish(); + List items = leafNode.getItems(); + assertTrue(items.isEmpty()); + } + finally { + pubSubManagerOne.deleteNode(nodename); + } + } + + /** + * Asserts that an error is returned when a publish request to a node that is both + * 'notification-only' as well as 'transient' contains an item element. + * + *

From XEP-0060 § 7.1.3.6:

+ *
+ * If the event type is notification + transient and the publisher provides an item, + * the service MUST bounce the publication request with a <bad-request/> error + * and a pubsub-specific error condition of <item-forbidden/>. + *
+ * + * @see + * 7.1.3.6 Request Does Not Match Configuration + */ + @SmackIntegrationTest + public void transientNotificationOnlyNodeWithItemTest() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { + final String nodename = "sinttest-transient-notificationonly-withitem-nodename-" + testRunId; + final String itemId = "sinttest-transient-notificationonly-withitem-itemid-" + testRunId; + ConfigureForm defaultConfiguration = pubSubManagerOne.getDefaultConfiguration(); + ConfigureForm config = new ConfigureForm(defaultConfiguration.createAnswerForm()); + // Configure the node as "Notification-Only Node". + config.setDeliverPayloads(false); + // Configure the node as "transient" (set persistent_items to 'false') config.setPersistentItems(false); Node node = pubSubManagerOne.createNode(nodename, config); try { LeafNode leafNode = (LeafNode) node; leafNode.publish(new Item(itemId)); - List items = leafNode.getItems(); - assertEquals(1, items.size()); - Item item = items.get(0); - assertEquals(itemId, item.getId()); + fail("An exception should have been thrown."); + } + catch (XMPPErrorException e) { + assertEquals(StanzaError.Type.MODIFY, e.getStanzaError().getType()); + assertNotNull(e.getStanzaError().getExtension("item-forbidden", "http://jabber.org/protocol/pubsub#errors")); } finally { pubSubManagerOne.deleteNode(nodename); diff --git a/smack-java7/src/main/java/org/jivesoftware/smack/util/stringencoder/java7/Base64.java b/smack-java7/src/main/java/org/jivesoftware/smack/util/stringencoder/java7/Base64.java deleted file mode 100644 index 058dc67cf..000000000 --- a/smack-java7/src/main/java/org/jivesoftware/smack/util/stringencoder/java7/Base64.java +++ /dev/null @@ -1,1695 +0,0 @@ -/** - * - * Copyright the original author or authors - * - * 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.smack.util.stringencoder.java7; - -import java.nio.charset.Charset; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Encodes and decodes to and from Base64 notation. - *

- * This code was obtained from http://iharder.net/base64 - *

- * - * @author Robert Harder - * @author rob@iharder.net - * @version 2.2.1 - */ -// CHECKSTYLE:OFF -public final class Base64 -{ - private static final Logger LOGGER = Logger.getLogger(Base64.class.getName()); - - /* ******** P U B L I C F I E L D S ******** */ - - /** No options specified. Value is zero. */ - public static final int NO_OPTIONS = 0; - - /** Specify encoding. */ - public static final int ENCODE = 1; - - - /** Specify decoding. */ - public static final int DECODE = 0; - - - /** Specify that data should be gzip-compressed. */ - public static final int GZIP = 2; - - - /** Don't break lines when encoding (violates strict Base64 specification). */ - public static final int DONT_BREAK_LINES = 8; - - /** - * Encode using Base64-like encoding that is URL- and Filename-safe as described - * in Section 4 of RFC3548: - * http://www.faqs.org/rfcs/rfc3548.html. - * It is important to note that data encoded this way is not officially valid Base64, - * or at the very least should not be called Base64 without also specifying that is - * was encoded using the URL- and Filename-safe dialect. - */ - public static final int URL_SAFE = 16; - - - /** - * Encode using the special "ordered" dialect of Base64 described here: - * http://www.faqs.org/qa/rfcc-1940.html. - */ - public static final int ORDERED = 32; - - -/* ******** P R I V A T E F I E L D S ******** */ - - - /** Maximum line length (76) of Base64 output. */ - private static final int MAX_LINE_LENGTH = 76; - - - /** The equals sign (=) as a byte. */ - private static final byte EQUALS_SIGN = (byte)'='; - - - /** The new line character (\n) as a byte. */ - private static final byte NEW_LINE = (byte)'\n'; - - - /** Preferred encoding. */ - private static final String PREFERRED_ENCODING = "UTF-8"; - - - // I think I end up not using the BAD_ENCODING indicator. - // private static final byte BAD_ENCODING = -9; // Indicates error in encoding - private static final byte WHITE_SPACE_ENC = -5; // Indicates white space in encoding - private static final byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in encoding - - -/* ******** S T A N D A R D B A S E 6 4 A L P H A B E T ******** */ - - /** The 64 valid Base64 values. */ - // private static final byte[] ALPHABET; - /* Host platform me be something funny like EBCDIC, so we hardcode these values. */ - private static final byte[] _STANDARD_ALPHABET = - { - (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G', - (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N', - (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', - (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', - (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g', - (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n', - (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', - (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z', - (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', - (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'+', (byte)'/' - }; - - - /** - * Translates a Base64 value to either its 6-bit reconstruction value - * or a negative number indicating some other meaning. - **/ - private static final byte[] _STANDARD_DECODABET = - { - -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8 - -5,-5, // Whitespace: Tab and Linefeed - -9,-9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26 - -9,-9,-9,-9,-9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42 - 62, // Plus sign at decimal 43 - -9,-9,-9, // Decimal 44 - 46 - 63, // Slash at decimal 47 - 52,53,54,55,56,57,58,59,60,61, // Numbers zero through nine - -9,-9,-9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9,-9,-9, // Decimal 62 - 64 - 0,1,2,3,4,5,6,7,8,9,10,11,12,13, // Letters 'A' through 'N' - 14,15,16,17,18,19,20,21,22,23,24,25, // Letters 'O' through 'Z' - -9,-9,-9,-9,-9,-9, // Decimal 91 - 96 - 26,27,28,29,30,31,32,33,34,35,36,37,38, // Letters 'a' through 'm' - 39,40,41,42,43,44,45,46,47,48,49,50,51, // Letters 'n' through 'z' - -9,-9,-9,-9 // Decimal 123 - 126 - /*,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ - }; - - -/* ******** U R L S A F E B A S E 6 4 A L P H A B E T ******** */ - - /** - * Used in the URL- and Filename-safe dialect described in Section 4 of RFC3548: - * http://www.faqs.org/rfcs/rfc3548.html. - * Notice that the last two bytes become "hyphen" and "underscore" instead of "plus" and "slash." - */ - private static final byte[] _URL_SAFE_ALPHABET = - { - (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G', - (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N', - (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', - (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', - (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g', - (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n', - (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', - (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z', - (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', - (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'-', (byte)'_' - }; - - /** - * Used in decoding URL- and Filename-safe dialects of Base64. - */ - private static final byte[] _URL_SAFE_DECODABET = - { - -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8 - -5,-5, // Whitespace: Tab and Linefeed - -9,-9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26 - -9,-9,-9,-9,-9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42 - -9, // Plus sign at decimal 43 - -9, // Decimal 44 - 62, // Minus sign at decimal 45 - -9, // Decimal 46 - -9, // Slash at decimal 47 - 52,53,54,55,56,57,58,59,60,61, // Numbers zero through nine - -9,-9,-9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9,-9,-9, // Decimal 62 - 64 - 0,1,2,3,4,5,6,7,8,9,10,11,12,13, // Letters 'A' through 'N' - 14,15,16,17,18,19,20,21,22,23,24,25, // Letters 'O' through 'Z' - -9,-9,-9,-9, // Decimal 91 - 94 - 63, // Underscore at decimal 95 - -9, // Decimal 96 - 26,27,28,29,30,31,32,33,34,35,36,37,38, // Letters 'a' through 'm' - 39,40,41,42,43,44,45,46,47,48,49,50,51, // Letters 'n' through 'z' - -9,-9,-9,-9 // Decimal 123 - 126 - /*,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ - }; - - - -/* ******** O R D E R E D B A S E 6 4 A L P H A B E T ******** */ - - /** - * I don't get the point of this technique, but it is described here: - * http://www.faqs.org/qa/rfcc-1940.html. - */ - private static final byte[] _ORDERED_ALPHABET = - { - (byte)'-', - (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', - (byte)'5', (byte)'6', (byte)'7', (byte)'8', (byte)'9', - (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G', - (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N', - (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', - (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', - (byte)'_', - (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g', - (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n', - (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', - (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z' - }; - - /** - * Used in decoding the "ordered" dialect of Base64. - */ - private static final byte[] _ORDERED_DECODABET = - { - -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8 - -5,-5, // Whitespace: Tab and Linefeed - -9,-9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26 - -9,-9,-9,-9,-9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42 - -9, // Plus sign at decimal 43 - -9, // Decimal 44 - 0, // Minus sign at decimal 45 - -9, // Decimal 46 - -9, // Slash at decimal 47 - 1,2,3,4,5,6,7,8,9,10, // Numbers zero through nine - -9,-9,-9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9,-9,-9, // Decimal 62 - 64 - 11,12,13,14,15,16,17,18,19,20,21,22,23, // Letters 'A' through 'M' - 24,25,26,27,28,29,30,31,32,33,34,35,36, // Letters 'N' through 'Z' - -9,-9,-9,-9, // Decimal 91 - 94 - 37, // Underscore at decimal 95 - -9, // Decimal 96 - 38,39,40,41,42,43,44,45,46,47,48,49,50, // Letters 'a' through 'm' - 51,52,53,54,55,56,57,58,59,60,61,62,63, // Letters 'n' through 'z' - -9,-9,-9,-9 // Decimal 123 - 126 - /*,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ - }; - - -/* ******** D E T E R M I N E W H I C H A L H A B E T ******** */ - - - /** - * Returns one of the _SOMETHING_ALPHABET byte arrays depending on - * the options specified. - * It's possible, though silly, to specify ORDERED and URLSAFE - * in which case one of them will be picked, though there is - * no guarantee as to which one will be picked. - */ - private static byte[] getAlphabet( int options ) - { - if( (options & URL_SAFE) == URL_SAFE ) return _URL_SAFE_ALPHABET; - else if( (options & ORDERED) == ORDERED ) return _ORDERED_ALPHABET; - else return _STANDARD_ALPHABET; - - } // end getAlphabet - - - /** - * Returns one of the _SOMETHING_DECODABET byte arrays depending on - * the options specified. - * It's possible, though silly, to specify ORDERED and URL_SAFE - * in which case one of them will be picked, though there is - * no guarantee as to which one will be picked. - */ - private static byte[] getDecodabet( int options ) - { - if( (options & URL_SAFE) == URL_SAFE ) return _URL_SAFE_DECODABET; - else if( (options & ORDERED) == ORDERED ) return _ORDERED_DECODABET; - else return _STANDARD_DECODABET; - - } // end getAlphabet - - - - /** Defeats instantiation. */ - private Base64(){} - -/* ******** E N C O D I N G M E T H O D S ******** */ - - - /** - * Encodes up to the first three bytes of array threeBytes - * and returns a four-byte array in Base64 notation. - * The actual number of significant bytes in your array is - * given by numSigBytes. - * The array threeBytes needs only be as big as - * numSigBytes. - * Code can reuse a byte array by passing a four-byte array as b4. - * - * @param b4 A reusable byte array to reduce array instantiation - * @param threeBytes the array to convert - * @param numSigBytes the number of significant bytes in your array - * @return four byte array in Base64 notation. - * @since 1.5.1 - */ - private static byte[] encode3to4( byte[] b4, byte[] threeBytes, int numSigBytes, int options ) - { - encode3to4( threeBytes, 0, numSigBytes, b4, 0, options ); - return b4; - } // end encode3to4 - - - /** - *

Encodes up to three bytes of the array source - * and writes the resulting four Base64 bytes to destination. - * The source and destination arrays can be manipulated - * anywhere along their length by specifying - * srcOffset and destOffset. - * This method does not check to make sure your arrays - * are large enough to accommodate srcOffset + 3 for - * the source array or destOffset + 4 for - * the destination array. - * The actual number of significant bytes in your array is - * given by numSigBytes.

- *

This is the lowest level of the encoding methods with - * all possible parameters.

- * - * @param source the array to convert - * @param srcOffset the index where conversion begins - * @param numSigBytes the number of significant bytes in your array - * @param destination the array to hold the conversion - * @param destOffset the index where output will be put - * @return the destination array - * @since 1.3 - */ - @SuppressWarnings("UnnecessaryParentheses") - private static byte[] encode3to4( - byte[] source, int srcOffset, int numSigBytes, - byte[] destination, int destOffset, int options ) - { - byte[] ALPHABET = getAlphabet( options ); - - // 1 2 3 - // 01234567890123456789012345678901 Bit position - // --------000000001111111122222222 Array position from threeBytes - // --------| || || || | Six bit groups to index ALPHABET - // >>18 >>12 >> 6 >> 0 Right shift necessary - // 0x3f 0x3f 0x3f Additional AND - - // Create buffer with zero-padding if there are only one or two - // significant bytes passed in the array. - // We have to shift left 24 in order to flush out the 1's that appear - // when Java treats a value as negative that is cast from a byte to an int. - int inBuff = ( numSigBytes > 0 ? ((source[ srcOffset ] << 24) >>> 8) : 0 ) - | ( numSigBytes > 1 ? ((source[ srcOffset + 1 ] << 24) >>> 16) : 0 ) - | ( numSigBytes > 2 ? ((source[ srcOffset + 2 ] << 24) >>> 24) : 0 ); - - switch( numSigBytes ) - { - case 3: - destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ]; - destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ]; - destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>> 6) & 0x3f ]; - destination[ destOffset + 3 ] = ALPHABET[ (inBuff ) & 0x3f ]; - return destination; - - case 2: - destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ]; - destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ]; - destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>> 6) & 0x3f ]; - destination[ destOffset + 3 ] = EQUALS_SIGN; - return destination; - - case 1: - destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ]; - destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ]; - destination[ destOffset + 2 ] = EQUALS_SIGN; - destination[ destOffset + 3 ] = EQUALS_SIGN; - return destination; - - default: - return destination; - } // end switch - } // end encode3to4 - - - - /** - * Serializes an object and returns the Base64-encoded - * version of that serialized object. If the object - * cannot be serialized or there is another error, - * the method will return null. - * The object is not GZip-compressed before being encoded. - * - * @param serializableObject The object to encode - * @return The Base64-encoded object - * @since 1.4 - */ - public static String encodeObject( java.io.Serializable serializableObject ) - { - return encodeObject( serializableObject, NO_OPTIONS ); - } // end encodeObject - - - - /** - * Serializes an object and returns the Base64-encoded - * version of that serialized object. If the object - * cannot be serialized or there is another error, - * the method will return null. - *

- * Valid options:

-     *   GZIP: gzip-compresses object before encoding it.
-     *   DONT_BREAK_LINES: don't break lines at 76 characters
-     *     Note: Technically, this makes your encoding non-compliant.
-     * 
- *

- * Example: encodeObject( myObj, Base64.GZIP ) or - *

- * Example: encodeObject( myObj, Base64.GZIP | Base64.DONT_BREAK_LINES ) - * - * @param serializableObject The object to encode - * @param options Specified options - * @return The Base64-encoded object - * @see Base64#GZIP - * @see Base64#DONT_BREAK_LINES - * @since 2.0 - */ - public static String encodeObject( java.io.Serializable serializableObject, int options ) - { - // Streams - java.io.ByteArrayOutputStream baos = null; - java.io.OutputStream b64os = null; - java.io.ObjectOutputStream oos = null; - java.util.zip.GZIPOutputStream gzos = null; - - // Isolate options - int gzip = (options & GZIP); - - try - { - // ObjectOutputStream -> (GZIP) -> Base64 -> ByteArrayOutputStream - baos = new java.io.ByteArrayOutputStream(); - b64os = new Base64.OutputStream( baos, ENCODE | options ); - - // GZip? - if( gzip == GZIP ) - { - gzos = new java.util.zip.GZIPOutputStream( b64os ); - oos = new java.io.ObjectOutputStream( gzos ); - } // end if: gzip - else - oos = new java.io.ObjectOutputStream( b64os ); - - oos.writeObject( serializableObject ); - } // end try - catch( java.io.IOException e ) - { - LOGGER.log(Level.SEVERE, "Error encoding object", e); - return null; - } // end catch - finally - { - try{ oos.close(); } catch( Exception e ){} - try{ gzos.close(); } catch( Exception e ){} - try{ b64os.close(); } catch( Exception e ){} - try{ baos.close(); } catch( Exception e ){} - } // end finally - - // Return value according to relevant encoding. - try - { - return new String( baos.toByteArray(), PREFERRED_ENCODING ); - } // end try - catch (java.io.UnsupportedEncodingException uue) - { - return new String( baos.toByteArray(), Charset.defaultCharset() ); - } // end catch - - } // end encode - - - - /** - * Encodes a byte array into Base64 notation. - * Does not GZip-compress data. - * - * @param source The data to convert - * @since 1.4 - */ - public static String encodeBytes( byte[] source ) - { - return encodeBytes( source, 0, source.length, NO_OPTIONS ); - } // end encodeBytes - - - - /** - * Encodes a byte array into Base64 notation. - *

- * Valid options:

-     *   GZIP: gzip-compresses object before encoding it.
-     *   DONT_BREAK_LINES: don't break lines at 76 characters
-     *     Note: Technically, this makes your encoding non-compliant.
-     * 
- *

- * Example: encodeBytes( myData, Base64.GZIP ) or - *

- * Example: encodeBytes( myData, Base64.GZIP | Base64.DONT_BREAK_LINES ) - * - * - * @param source The data to convert - * @param options Specified options - * @see Base64#GZIP - * @see Base64#DONT_BREAK_LINES - * @since 2.0 - */ - public static String encodeBytes( byte[] source, int options ) - { - return encodeBytes( source, 0, source.length, options ); - } // end encodeBytes - - - /** - * Encodes a byte array into Base64 notation. - * Does not GZip-compress data. - * - * @param source The data to convert - * @param off Offset in array where conversion should begin - * @param len Length of data to convert - * @since 1.4 - */ - public static String encodeBytes( byte[] source, int off, int len ) - { - return encodeBytes( source, off, len, NO_OPTIONS ); - } // end encodeBytes - - - - /** - * Encodes a byte array into Base64 notation. - *

- * Valid options:

-     *   GZIP: gzip-compresses object before encoding it.
-     *   DONT_BREAK_LINES: don't break lines at 76 characters
-     *     Note: Technically, this makes your encoding non-compliant.
-     * 
- *

- * Example: encodeBytes( myData, Base64.GZIP ) or - *

- * Example: encodeBytes( myData, Base64.GZIP | Base64.DONT_BREAK_LINES ) - * - * - * @param source The data to convert - * @param off Offset in array where conversion should begin - * @param len Length of data to convert - * @param options Specified options; alphabet type is pulled from this (standard, url-safe, ordered) - * @see Base64#GZIP - * @see Base64#DONT_BREAK_LINES - * @since 2.0 - */ - @SuppressWarnings("UnnecessaryParentheses") - public static String encodeBytes( byte[] source, int off, int len, int options ) - { - // Isolate options - int dontBreakLines = ( options & DONT_BREAK_LINES ); - int gzip = ( options & GZIP ); - - // Compress? - if( gzip == GZIP ) - { - java.io.ByteArrayOutputStream baos = null; - java.util.zip.GZIPOutputStream gzos = null; - Base64.OutputStream b64os = null; - - - try - { - // GZip -> Base64 -> ByteArray - baos = new java.io.ByteArrayOutputStream(); - b64os = new Base64.OutputStream( baos, ENCODE | options ); - gzos = new java.util.zip.GZIPOutputStream( b64os ); - - gzos.write( source, off, len ); - gzos.close(); - } // end try - catch( java.io.IOException e ) - { - LOGGER.log(Level.SEVERE, "Error encoding bytes", e); - return null; - } // end catch - finally - { - try{ gzos.close(); } catch( Exception e ){} - try{ b64os.close(); } catch( Exception e ){} - try{ baos.close(); } catch( Exception e ){} - } // end finally - - // Return value according to relevant encoding. - try - { - return new String( baos.toByteArray(), PREFERRED_ENCODING ); - } // end try - catch (java.io.UnsupportedEncodingException uue) - { - return new String( baos.toByteArray(), Charset.defaultCharset() ); - } // end catch - } // end if: compress - - // Else, don't compress. Better not to use streams at all then. - else - { - // Convert option to boolean in way that code likes it. - boolean breakLines = dontBreakLines == 0; - - int len43 = len * 4 / 3; - byte[] outBuff = new byte[ ( len43 ) // Main 4:3 - + ( (len % 3) > 0 ? 4 : 0 ) // Account for padding - + (breakLines ? ( len43 / MAX_LINE_LENGTH ) : 0) ]; // New lines - int d = 0; - int e = 0; - int len2 = len - 2; - int lineLength = 0; - for( ; d < len2; d+=3, e+=4 ) - { - encode3to4( source, d+off, 3, outBuff, e, options ); - - lineLength += 4; - if( breakLines && lineLength == MAX_LINE_LENGTH ) - { - outBuff[e+4] = NEW_LINE; - e++; - lineLength = 0; - } // end if: end of line - } // en dfor: each piece of array - - if( d < len ) - { - encode3to4( source, d+off, len - d, outBuff, e, options ); - e += 4; - } // end if: some padding needed - - - // Return value according to relevant encoding. - try - { - return new String( outBuff, 0, e, PREFERRED_ENCODING ); - } // end try - catch (java.io.UnsupportedEncodingException uue) - { - return new String( outBuff, 0, e , Charset.defaultCharset()); - } // end catch - - } // end else: don't compress - - } // end encodeBytes - - - - - -/* ******** D E C O D I N G M E T H O D S ******** */ - - - /** - * Decodes four bytes from array source - * and writes the resulting bytes (up to three of them) - * to destination. - * The source and destination arrays can be manipulated - * anywhere along their length by specifying - * srcOffset and destOffset. - * This method does not check to make sure your arrays - * are large enough to accommodate srcOffset + 4 for - * the source array or destOffset + 3 for - * the destination array. - * This method returns the actual number of bytes that - * were converted from the Base64 encoding. - *

This is the lowest level of the decoding methods with - * all possible parameters.

- * - * - * @param source the array to convert - * @param srcOffset the index where conversion begins - * @param destination the array to hold the conversion - * @param destOffset the index where output will be put - * @param options alphabet type is pulled from this (standard, url-safe, ordered) - * @return the number of decoded bytes converted - * @since 1.3 - */ - @SuppressWarnings("UnnecessaryParentheses") - private static int decode4to3( byte[] source, int srcOffset, byte[] destination, int destOffset, int options ) - { - byte[] DECODABET = getDecodabet( options ); - - // Example: Dk== - if( source[ srcOffset + 2] == EQUALS_SIGN ) - { - // Two ways to do the same thing. Don't know which way I like best. - // int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) - // | ( ( DECODABET[ source[ srcOffset + 1] ] << 24 ) >>> 12 ); - int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 ) - | ( ( DECODABET[ source[ srcOffset + 1] ] & 0xFF ) << 12 ); - - destination[ destOffset ] = (byte)( outBuff >>> 16 ); - return 1; - } - - // Example: DkL= - else if( source[ srcOffset + 3 ] == EQUALS_SIGN ) - { - // Two ways to do the same thing. Don't know which way I like best. - // int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) - // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) - // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ); - int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 ) - | ( ( DECODABET[ source[ srcOffset + 1 ] ] & 0xFF ) << 12 ) - | ( ( DECODABET[ source[ srcOffset + 2 ] ] & 0xFF ) << 6 ); - - destination[ destOffset ] = (byte)( outBuff >>> 16 ); - destination[ destOffset + 1 ] = (byte)( outBuff >>> 8 ); - return 2; - } - - // Example: DkLE - else - { - try{ - // Two ways to do the same thing. Don't know which way I like best. - // int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) - // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) - // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ) - // | ( ( DECODABET[ source[ srcOffset + 3 ] ] << 24 ) >>> 24 ); - int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 ) - | ( ( DECODABET[ source[ srcOffset + 1 ] ] & 0xFF ) << 12 ) - | ( ( DECODABET[ source[ srcOffset + 2 ] ] & 0xFF ) << 6) - | ( ( DECODABET[ source[ srcOffset + 3 ] ] & 0xFF ) ); - - - destination[ destOffset ] = (byte)( outBuff >> 16 ); - destination[ destOffset + 1 ] = (byte)( outBuff >> 8 ); - destination[ destOffset + 2 ] = (byte)( outBuff ); - - return 3; - }catch( Exception e){ - LOGGER.log(Level.SEVERE, e.getMessage(), e); - LOGGER.severe(""+source[srcOffset]+ ": " + ( DECODABET[ source[ srcOffset ] ] ) ); - LOGGER.severe(""+source[srcOffset+1]+ ": " + ( DECODABET[ source[ srcOffset + 1 ] ] ) ); - LOGGER.severe(""+source[srcOffset+2]+ ": " + ( DECODABET[ source[ srcOffset + 2 ] ] ) ); - LOGGER.severe(""+source[srcOffset+3]+ ": " + ( DECODABET[ source[ srcOffset + 3 ] ] ) ); - return -1; - } // end catch - } - } // end decodeToBytes - - - - - /** - * Very low-level access to decoding ASCII characters in - * the form of a byte array. Does not support automatically - * gunzipping or any other "fancy" features. - * - * @param source The Base64 encoded data - * @param off The offset of where to begin decoding - * @param len The length of characters to decode - * @return decoded data - * @since 1.3 - */ - public static byte[] decode( byte[] source, int off, int len, int options ) - { - byte[] DECODABET = getDecodabet( options ); - - int len34 = len * 3 / 4; - byte[] outBuff = new byte[ len34 ]; // Upper limit on size of output - int outBuffPosn = 0; - - byte[] b4 = new byte[4]; - int b4Posn = 0; - int i = 0; - byte sbiCrop = 0; - byte sbiDecode = 0; - for( i = off; i < off+len; i++ ) - { - sbiCrop = (byte)(source[i] & 0x7f); // Only the low seven bits - sbiDecode = DECODABET[ sbiCrop ]; - - if( sbiDecode >= WHITE_SPACE_ENC ) // White space, Equals sign or better - { - if( sbiDecode >= EQUALS_SIGN_ENC ) - { - b4[ b4Posn++ ] = sbiCrop; - if( b4Posn > 3 ) - { - outBuffPosn += decode4to3( b4, 0, outBuff, outBuffPosn, options ); - b4Posn = 0; - - // If that was the equals sign, break out of 'for' loop - if( sbiCrop == EQUALS_SIGN ) - break; - } // end if: quartet built - - } // end if: equals sign or better - - } // end if: white space, equals sign or better - else - { - LOGGER.warning("Bad Base64 input character at " + i + ": " + source[i] + "(decimal)"); - return null; - } // end else: - } // each input character - - byte[] out = new byte[ outBuffPosn ]; - System.arraycopy( outBuff, 0, out, 0, outBuffPosn ); - return out; - } // end decode - - - - - /** - * Decodes data from Base64 notation, automatically - * detecting gzip-compressed data and decompressing it. - * - * @param s the string to decode - * @return the decoded data - * @since 1.4 - */ - public static byte[] decode( String s ) - { - return decode( s, NO_OPTIONS ); - } - - - /** - * Decodes data from Base64 notation, automatically - * detecting gzip-compressed data and decompressing it. - * - * @param s the string to decode - * @param options encode options such as URL_SAFE - * @return the decoded data - * @since 1.4 - */ - public static byte[] decode( String s, int options ) - { - byte[] bytes; - try - { - bytes = s.getBytes( PREFERRED_ENCODING ); - } // end try - catch( java.io.UnsupportedEncodingException uee ) - { - bytes = s.getBytes( Charset.defaultCharset() ); - } // end catch - // - - // Decode - bytes = decode( bytes, 0, bytes.length, options ); - - - // Check to see if it's gzip-compressed - // GZIP Magic Two-Byte Number: 0x8b1f (35615) - if( bytes != null && bytes.length >= 4 ) - { - - int head = (bytes[0] & 0xff) | ((bytes[1] << 8) & 0xff00); - if( java.util.zip.GZIPInputStream.GZIP_MAGIC == head ) - { - java.io.ByteArrayInputStream bais = null; - java.util.zip.GZIPInputStream gzis = null; - java.io.ByteArrayOutputStream baos = null; - byte[] buffer = new byte[2048]; - int length = 0; - - try - { - baos = new java.io.ByteArrayOutputStream(); - bais = new java.io.ByteArrayInputStream( bytes ); - gzis = new java.util.zip.GZIPInputStream( bais ); - - while( ( length = gzis.read( buffer ) ) >= 0 ) - { - baos.write(buffer,0,length); - } // end while: reading input - - // No error? Get new bytes. - bytes = baos.toByteArray(); - - } // end try - catch( java.io.IOException e ) - { - // Just return originally-decoded bytes - } // end catch - finally - { - try{ baos.close(); } catch( Exception e ){} - try{ gzis.close(); } catch( Exception e ){} - try{ bais.close(); } catch( Exception e ){} - } // end finally - - } // end if: gzipped - } // end if: bytes.length >= 2 - - return bytes; - } // end decode - - - - - /** - * Attempts to decode Base64 data and deserialize a Java - * Object within. Returns null if there was an error. - * - * @param encodedObject The Base64 data to decode - * @return The decoded and deserialized object - * @since 1.5 - */ - public static Object decodeToObject( String encodedObject ) - { - // Decode and gunzip if necessary - byte[] objBytes = decode( encodedObject ); - - java.io.ByteArrayInputStream bais = null; - java.io.ObjectInputStream ois = null; - Object obj = null; - - try - { - bais = new java.io.ByteArrayInputStream( objBytes ); - ois = new java.io.ObjectInputStream( bais ); - - obj = ois.readObject(); - } // end try - catch( java.io.IOException e ) - { - LOGGER.log(Level.SEVERE, "Error reading object", e); - } // end catch - catch( java.lang.ClassNotFoundException e ) - { - LOGGER.log(Level.SEVERE, "Class not found for encoded object", e); - } // end catch - finally - { - try{ bais.close(); } catch( Exception e ){} - try{ ois.close(); } catch( Exception e ){} - } // end finally - - return obj; - } // end decodeObject - - - - /** - * Convenience method for encoding data to a file. - * - * @param dataToEncode byte array of data to encode in base64 form - * @param filename Filename for saving encoded data - * @return true if successful, false otherwise - * - * @since 2.1 - */ - public static boolean encodeToFile( byte[] dataToEncode, String filename ) - { - boolean success = false; - Base64.OutputStream bos = null; - try - { - bos = new Base64.OutputStream( - new java.io.FileOutputStream( filename ), Base64.ENCODE ); - bos.write( dataToEncode ); - success = true; - } // end try - catch( java.io.IOException e ) - { - - success = false; - } // end catch: IOException - finally - { - try{ bos.close(); } catch( Exception e ){} - } // end finally - - return success; - } // end encodeToFile - - - /** - * Convenience method for decoding data to a file. - * - * @param dataToDecode Base64-encoded data as a string - * @param filename Filename for saving decoded data - * @return true if successful, false otherwise - * - * @since 2.1 - */ - public static boolean decodeToFile( String dataToDecode, String filename ) - { - boolean success = false; - Base64.OutputStream bos = null; - try - { - bos = new Base64.OutputStream( - new java.io.FileOutputStream( filename ), Base64.DECODE ); - bos.write( dataToDecode.getBytes( PREFERRED_ENCODING ) ); - success = true; - } // end try - catch( java.io.IOException e ) - { - success = false; - } // end catch: IOException - finally - { - try{ bos.close(); } catch( Exception e ){} - } // end finally - - return success; - } // end decodeToFile - - - - - /** - * Convenience method for reading a base64-encoded - * file and decoding it. - * - * @param filename Filename for reading encoded data - * @return decoded byte array or null if unsuccessful - * - * @since 2.1 - */ - public static byte[] decodeFromFile( String filename ) - { - byte[] decodedData = null; - Base64.InputStream bis = null; - try - { - // Set up some useful variables - java.io.File file = new java.io.File( filename ); - byte[] buffer = null; - int length = 0; - int numBytes = 0; - - // Check for size of file - if( file.length() > Integer.MAX_VALUE ) - { - LOGGER.warning("File is too big for this convenience method (" + file.length() + " bytes)."); - return null; - } // end if: file too big for int index - buffer = new byte[ (int)file.length() ]; - - // Open a stream - bis = new Base64.InputStream( - new java.io.BufferedInputStream( - new java.io.FileInputStream( file ) ), Base64.DECODE ); - - // Read until done - while( ( numBytes = bis.read( buffer, length, 4096 ) ) >= 0 ) - length += numBytes; - - // Save in a variable to return - decodedData = new byte[ length ]; - System.arraycopy( buffer, 0, decodedData, 0, length ); - - } // end try - catch( java.io.IOException e ) - { - LOGGER.log(Level.SEVERE, "Error decoding from file " + filename, e); - } // end catch: IOException - finally - { - try{ bis.close(); } catch( Exception e) {} - } // end finally - - return decodedData; - } // end decodeFromFile - - - - /** - * Convenience method for reading a binary file - * and base64-encoding it. - * - * @param filename Filename for reading binary data - * @return base64-encoded string or null if unsuccessful - * - * @since 2.1 - */ - public static String encodeFromFile( String filename ) - { - String encodedData = null; - Base64.InputStream bis = null; - try - { - // Set up some useful variables - java.io.File file = new java.io.File( filename ); - byte[] buffer = new byte[ Math.max((int)(file.length() * 1.4),40) ]; // Need max() for math on small files (v2.2.1) - int length = 0; - int numBytes = 0; - - // Open a stream - bis = new Base64.InputStream( - new java.io.BufferedInputStream( - new java.io.FileInputStream( file ) ), Base64.ENCODE ); - - // Read until done - while( ( numBytes = bis.read( buffer, length, 4096 ) ) >= 0 ) - length += numBytes; - - // Save in a variable to return - encodedData = new String( buffer, 0, length, Base64.PREFERRED_ENCODING ); - - } // end try - catch( java.io.IOException e ) - { - LOGGER.log(Level.SEVERE, "Error encoding from file " + filename, e); - } // end catch: IOException - finally - { - try{ bis.close(); } catch( Exception e) {} - } // end finally - - return encodedData; - } // end encodeFromFile - - /** - * Reads infile and encodes it to outfile. - * - * @param infile Input file - * @param outfile Output file - * @since 2.2 - */ - public static void encodeFileToFile( String infile, String outfile ) - { - String encoded = Base64.encodeFromFile( infile ); - java.io.OutputStream out = null; - try{ - out = new java.io.BufferedOutputStream( - new java.io.FileOutputStream( outfile ) ); - out.write( encoded.getBytes("US-ASCII") ); // Strict, 7-bit output. - } // end try - catch( java.io.IOException ex ) { - LOGGER.log(Level.SEVERE, "Error encoding file " + infile, ex); - } // end catch - finally { - try { out.close(); } - catch( Exception ex ){} - } // end finally - } // end encodeFileToFile - - - /** - * Reads infile and decodes it to outfile. - * - * @param infile Input file - * @param outfile Output file - * @since 2.2 - */ - public static void decodeFileToFile( String infile, String outfile ) - { - byte[] decoded = Base64.decodeFromFile( infile ); - java.io.OutputStream out = null; - try{ - out = new java.io.BufferedOutputStream( - new java.io.FileOutputStream( outfile ) ); - out.write( decoded ); - } // end try - catch( java.io.IOException ex ) { - LOGGER.log(Level.SEVERE, "Error decoding file " + infile, ex); - } // end catch - finally { - try { out.close(); } - catch( Exception ex ){} - } // end finally - } // end decodeFileToFile - - - /* ******** I N N E R C L A S S I N P U T S T R E A M ******** */ - - - - /** - * A {@link Base64.InputStream} will read data from another - * java.io.InputStream, given in the constructor, - * and encode/decode to/from Base64 notation on the fly. - * - * @see Base64 - * @since 1.3 - */ - public static class InputStream extends java.io.FilterInputStream - { - private boolean encode; // Encoding or decoding - private int position; // Current position in the buffer - private byte[] buffer; // Small buffer holding converted data - private int bufferLength; // Length of buffer (3 or 4) - private int numSigBytes; // Number of meaningful bytes in the buffer - private int lineLength; - private boolean breakLines; // Break lines at less than 80 characters - private int options; // Record options used to create the stream. - private byte[] decodabet; // Local copies to avoid extra method calls - - - /** - * Constructs a {@link Base64.InputStream} in DECODE mode. - * - * @param in the java.io.InputStream from which to read data. - * @since 1.3 - */ - public InputStream( java.io.InputStream in ) - { - this( in, DECODE ); - } // end constructor - - - /** - * Constructs a {@link Base64.InputStream} in - * either ENCODE or DECODE mode. - *

- * Valid options:

-         *   ENCODE or DECODE: Encode or Decode as data is read.
-         *   DONT_BREAK_LINES: don't break lines at 76 characters
-         *     (only meaningful when encoding)
-         *     Note: Technically, this makes your encoding non-compliant.
-         * 
- *

- * Example: new Base64.InputStream( in, Base64.DECODE ) - * - * - * @param in the java.io.InputStream from which to read data. - * @param options Specified options - * @see Base64#ENCODE - * @see Base64#DECODE - * @see Base64#DONT_BREAK_LINES - * @since 2.0 - */ - public InputStream( java.io.InputStream in, int options ) - { - super( in ); - this.breakLines = (options & DONT_BREAK_LINES) != DONT_BREAK_LINES; - this.encode = (options & ENCODE) == ENCODE; - this.bufferLength = encode ? 4 : 3; - this.buffer = new byte[ bufferLength ]; - this.position = -1; - this.lineLength = 0; - this.options = options; // Record for later, mostly to determine which alphabet to use - this.decodabet = getDecodabet(options); - } // end constructor - - /** - * Reads enough of the input stream to convert - * to/from Base64 and returns the next byte. - * - * @return next byte - * @since 1.3 - */ - @Override - public int read() throws java.io.IOException - { - // Do we need to get data? - if( position < 0 ) - { - if( encode ) - { - byte[] b3 = new byte[3]; - int numBinaryBytes = 0; - for( int i = 0; i < 3; i++ ) - { - try - { - int b = in.read(); - - // If end of stream, b is -1. - if( b >= 0 ) - { - b3[i] = (byte)b; - numBinaryBytes++; - } // end if: not end of stream - - } // end try: read - catch( java.io.IOException e ) - { - // Only a problem if we got no data at all. - if( i == 0 ) - throw e; - - } // end catch - } // end for: each needed input byte - - if( numBinaryBytes > 0 ) - { - encode3to4( b3, 0, numBinaryBytes, buffer, 0, options ); - position = 0; - numSigBytes = 4; - } // end if: got data - else - { - return -1; - } // end else - } // end if: encoding - - // Else decoding - else - { - byte[] b4 = new byte[4]; - int i = 0; - for( i = 0; i < 4; i++ ) - { - // Read four "meaningful" bytes: - int b = 0; - do{ b = in.read(); } - while( b >= 0 && decodabet[ b & 0x7f ] <= WHITE_SPACE_ENC ); - - if( b < 0 ) - break; // Reads a -1 if end of stream - - b4[i] = (byte)b; - } // end for: each needed input byte - - if( i == 4 ) - { - numSigBytes = decode4to3( b4, 0, buffer, 0, options ); - position = 0; - } // end if: got four characters - else if( i == 0 ){ - return -1; - } // end else if: also padded correctly - else - { - // Must have broken out from above. - throw new java.io.IOException( "Improperly padded Base64 input." ); - } // end - - } // end else: decode - } // end else: get data - - // Got data? - if( position >= 0 ) - { - // End of relevant data? - if( /*!encode &&*/ position >= numSigBytes ) - return -1; - - if( encode && breakLines && lineLength >= MAX_LINE_LENGTH ) - { - lineLength = 0; - return '\n'; - } // end if - else - { - lineLength++; // This isn't important when decoding - // but throwing an extra "if" seems - // just as wasteful. - - int b = buffer[ position++ ]; - - if( position >= bufferLength ) - position = -1; - - return b & 0xFF; // This is how you "cast" a byte that's - // intended to be unsigned. - } // end else - } // end if: position >= 0 - - // Else error - else - { - // When JDK1.4 is more accepted, use an assertion here. - throw new java.io.IOException( "Error in Base64 code reading stream." ); - } // end else - } // end read - - - /** - * Calls {@link #read()} repeatedly until the end of stream - * is reached or len bytes are read. - * Returns number of bytes read into array or -1 if - * end of stream is encountered. - * - * @param dest array to hold values - * @param off offset for array - * @param len max number of bytes to read into array - * @return bytes read into array or -1 if end of stream is encountered. - * @since 1.3 - */ - @Override - public int read( byte[] dest, int off, int len ) throws java.io.IOException - { - int i; - int b; - for( i = 0; i < len; i++ ) - { - b = read(); - - // if( b < 0 && i == 0 ) - // return -1; - - if( b >= 0 ) - dest[off + i] = (byte)b; - else if( i == 0 ) - return -1; - else - break; // Out of 'for' loop - } // end for: each byte read - return i; - } // end read - - } // end inner class InputStream - - - - - - - /* ******** I N N E R C L A S S O U T P U T S T R E A M ******** */ - - - - /** - * A {@link Base64.OutputStream} will write data to another - * java.io.OutputStream, given in the constructor, - * and encode/decode to/from Base64 notation on the fly. - * - * @see Base64 - * @since 1.3 - */ - public static class OutputStream extends java.io.FilterOutputStream - { - private boolean encode; - private int position; - private byte[] buffer; - private int bufferLength; - private int lineLength; - private boolean breakLines; - private byte[] b4; // Scratch used in a few places - private boolean suspendEncoding; - private int options; // Record for later - private byte[] decodabet; // Local copies to avoid extra method calls - - /** - * Constructs a {@link Base64.OutputStream} in ENCODE mode. - * - * @param out the java.io.OutputStream to which data will be written. - * @since 1.3 - */ - public OutputStream( java.io.OutputStream out ) - { - this( out, ENCODE ); - } // end constructor - - - /** - * Constructs a {@link Base64.OutputStream} in - * either ENCODE or DECODE mode. - *

- * Valid options:

-         *   ENCODE or DECODE: Encode or Decode as data is read.
-         *   DONT_BREAK_LINES: don't break lines at 76 characters
-         *     (only meaningful when encoding)
-         *     Note: Technically, this makes your encoding non-compliant.
-         * 
- *

- * Example: new Base64.OutputStream( out, Base64.ENCODE ) - * - * @param out the java.io.OutputStream to which data will be written. - * @param options Specified options. - * @see Base64#ENCODE - * @see Base64#DECODE - * @see Base64#DONT_BREAK_LINES - * @since 1.3 - */ - public OutputStream( java.io.OutputStream out, int options ) - { - super( out ); - this.breakLines = (options & DONT_BREAK_LINES) != DONT_BREAK_LINES; - this.encode = (options & ENCODE) == ENCODE; - this.bufferLength = encode ? 3 : 4; - this.buffer = new byte[ bufferLength ]; - this.position = 0; - this.lineLength = 0; - this.suspendEncoding = false; - this.b4 = new byte[4]; - this.options = options; - this.decodabet = getDecodabet(options); - } // end constructor - - - /** - * Writes the byte to the output stream after - * converting to/from Base64 notation. - * When encoding, bytes are buffered three - * at a time before the output stream actually - * gets a write() call. - * When decoding, bytes are buffered four - * at a time. - * - * @param theByte the byte to write - * @since 1.3 - */ - @Override - public void write(int theByte) throws java.io.IOException - { - // Encoding suspended? - if( suspendEncoding ) - { - super.out.write( theByte ); - return; - } // end if: supsended - - // Encode? - if( encode ) - { - buffer[ position++ ] = (byte)theByte; - if( position >= bufferLength ) // Enough to encode. - { - out.write( encode3to4( b4, buffer, bufferLength, options ) ); - - lineLength += 4; - if( breakLines && lineLength >= MAX_LINE_LENGTH ) - { - out.write( NEW_LINE ); - lineLength = 0; - } // end if: end of line - - position = 0; - } // end if: enough to output - } // end if: encoding - - // Else, Decoding - else - { - // Meaningful Base64 character? - if( decodabet[ theByte & 0x7f ] > WHITE_SPACE_ENC ) - { - buffer[ position++ ] = (byte)theByte; - if( position >= bufferLength ) // Enough to output. - { - int len = Base64.decode4to3( buffer, 0, b4, 0, options ); - out.write( b4, 0, len ); - // out.write( Base64.decode4to3( buffer ) ); - position = 0; - } // end if: enough to output - } // end if: meaningful base64 character - else if( decodabet[ theByte & 0x7f ] != WHITE_SPACE_ENC ) - { - throw new java.io.IOException( "Invalid character in Base64 data." ); - } // end else: not white space either - } // end else: decoding - } // end write - - - - /** - * Calls {@link #write(int)} repeatedly until len - * bytes are written. - * - * @param theBytes array from which to read bytes - * @param off offset for array - * @param len max number of bytes to read into array - * @since 1.3 - */ - @Override - public void write( byte[] theBytes, int off, int len ) throws java.io.IOException - { - // Encoding suspended? - if( suspendEncoding ) - { - super.out.write( theBytes, off, len ); - return; - } // end if: supsended - - for( int i = 0; i < len; i++ ) - { - write( theBytes[ off + i ] ); - } // end for: each byte written - - } // end write - - - - /** - * Method added by PHIL. [Thanks, PHIL. -Rob] - * This pads the buffer without closing the stream. - */ - public void flushBase64() throws java.io.IOException - { - if( position > 0 ) - { - if( encode ) - { - out.write( encode3to4( b4, buffer, position, options ) ); - position = 0; - } // end if: encoding - else - { - throw new java.io.IOException( "Base64 input not properly padded." ); - } // end else: decoding - } // end if: buffer partially full - - } // end flush - - - /** - * Flushes and closes (I think, in the superclass) the stream. - * - * @since 1.3 - */ - @Override - public void close() throws java.io.IOException - { - // 1. Ensure that pending characters are written - flushBase64(); - - // 2. Actually close the stream - // Base class both flushes and closes. - super.close(); - - buffer = null; - out = null; - } // end close - - - - /** - * Suspends encoding of the stream. - * May be helpful if you need to embed a piece of - * base640-encoded data in a stream. - * - * @since 1.5.1 - */ - public void suspendEncoding() throws java.io.IOException - { - flushBase64(); - this.suspendEncoding = true; - } // end suspendEncoding - - - /** - * Resumes encoding of the stream. - * May be helpful if you need to embed a piece of - * base640-encoded data in a stream. - * - * @since 1.5.1 - */ - public void resumeEncoding() - { - this.suspendEncoding = false; - } // end resumeEncoding - - - - } // end inner class OutputStream - - -} // end class Base64 -// CHECKSTYLE:ON diff --git a/smack-java7/src/main/java/org/jivesoftware/smack/util/stringencoder/java7/Java7Base64Encoder.java b/smack-java7/src/main/java/org/jivesoftware/smack/util/stringencoder/java7/Java7Base64Encoder.java index da7ba78a6..6b8d07252 100644 --- a/smack-java7/src/main/java/org/jivesoftware/smack/util/stringencoder/java7/Java7Base64Encoder.java +++ b/smack-java7/src/main/java/org/jivesoftware/smack/util/stringencoder/java7/Java7Base64Encoder.java @@ -16,9 +16,7 @@ */ package org.jivesoftware.smack.util.stringencoder.java7; -import java.io.UnsupportedEncodingException; - -import org.jivesoftware.smack.util.StringUtils; +import java.util.Base64; /** * A Base 64 encoding implementation. @@ -28,10 +26,15 @@ public final class Java7Base64Encoder implements org.jivesoftware.smack.util.str private static final Java7Base64Encoder instance = new Java7Base64Encoder(); - private static final int BASE64_ENCODER_FLAGS = Base64.DONT_BREAK_LINES; + private final Base64.Encoder encoder; + private final Base64.Encoder encoderWithoutPadding; + + private final Base64.Decoder decoder; private Java7Base64Encoder() { - // Use getInstance() + encoder = Base64.getEncoder(); + encoderWithoutPadding = encoder.withoutPadding(); + decoder = Base64.getDecoder(); } public static Java7Base64Encoder getInstance() { @@ -40,27 +43,21 @@ public final class Java7Base64Encoder implements org.jivesoftware.smack.util.str @Override public byte[] decode(String string) { - return Base64.decode(string); + return decoder.decode(string); } @Override - public byte[] decode(byte[] input, int offset, int len) { - return Base64.decode(input, offset, len, 0); + public String encodeToString(byte[] input) { + return encoder.encodeToString(input); } @Override - public String encodeToString(byte[] input, int offset, int len) { - return Base64.encodeBytes(input, offset, len, BASE64_ENCODER_FLAGS); + public String encodeToStringWithoutPadding(byte[] input) { + return encoderWithoutPadding.encodeToString(input); } @Override - public byte[] encode(byte[] input, int offset, int len) { - String string = encodeToString(input, offset, len); - try { - return string.getBytes(StringUtils.USASCII); - } catch (UnsupportedEncodingException e) { - throw new AssertionError(e); - } + public byte[] encode(byte[] input) { + return encoder.encode(input); } - } diff --git a/smack-java7/src/main/java/org/jivesoftware/smack/util/stringencoder/java7/Java7Base64UrlSafeEncoder.java b/smack-java7/src/main/java/org/jivesoftware/smack/util/stringencoder/java7/Java7Base64UrlSafeEncoder.java index 5b724501a..1d2d9a530 100644 --- a/smack-java7/src/main/java/org/jivesoftware/smack/util/stringencoder/java7/Java7Base64UrlSafeEncoder.java +++ b/smack-java7/src/main/java/org/jivesoftware/smack/util/stringencoder/java7/Java7Base64UrlSafeEncoder.java @@ -17,6 +17,7 @@ package org.jivesoftware.smack.util.stringencoder.java7; import java.io.UnsupportedEncodingException; +import java.util.Base64; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.stringencoder.StringEncoder; @@ -37,10 +38,12 @@ public final class Java7Base64UrlSafeEncoder implements StringEncoder { private static final Java7Base64UrlSafeEncoder instance = new Java7Base64UrlSafeEncoder(); - private static final int BASE64_ENCODER_FLAGS = Base64.URL_SAFE | Base64.DONT_BREAK_LINES; + private final Base64.Encoder encoder; + private final Base64.Decoder decoder; private Java7Base64UrlSafeEncoder() { - // Use getInstance() + encoder = Base64.getUrlEncoder(); + decoder = Base64.getUrlDecoder(); } public static Java7Base64UrlSafeEncoder getInstance() { @@ -56,13 +59,14 @@ public final class Java7Base64UrlSafeEncoder implements StringEncoder { catch (UnsupportedEncodingException e) { throw new AssertionError(e); } - return Base64.encodeBytes(bytes, BASE64_ENCODER_FLAGS); + return encoder.encodeToString(bytes); } @Override public String decode(String s) { + byte[] bytes = decoder.decode(s); try { - return new String(Base64.decode(s, BASE64_ENCODER_FLAGS), StringUtils.UTF8); + return new String(bytes, StringUtils.UTF8); } catch (UnsupportedEncodingException e) { throw new AssertionError(e); diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/JingleSession.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/JingleSession.java index 84f679c1e..296630f26 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/JingleSession.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/JingleSession.java @@ -620,7 +620,7 @@ public final class JingleSession extends JingleNegotiator implements MediaReceiv * A XMPP connection * @return a Jingle session */ - public static JingleSession getInstanceFor(XMPPConnection con) { + public static synchronized JingleSession getInstanceFor(XMPPConnection con) { if (con == null) { throw new IllegalArgumentException("XMPPConnection cannot be null"); } diff --git a/smack-omemo/build.gradle b/smack-omemo/build.gradle index e4279f74a..354c3db20 100644 --- a/smack-omemo/build.gradle +++ b/smack-omemo/build.gradle @@ -3,7 +3,7 @@ dependencies { compile project(":smack-extensions") compile project(":smack-experimental") - compile "org.bouncycastle:bcprov-jdk15on:1.60" + compile "org.bouncycastle:bcprov-jdk15on:$bouncyCastleVersion" testCompile project(path: ":smack-core", configuration: "testRuntime") } diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoService.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoService.java index 27f775f3e..e4c93a0d1 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoService.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoService.java @@ -1,6 +1,6 @@ /** * - * Copyright 2017 Paul Schaub + * Copyright 2017 Paul Schaub, 2019 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,6 @@ import java.io.UnsupportedEncodingException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; import java.security.Security; import java.util.ArrayList; import java.util.Collection; @@ -308,7 +307,7 @@ public abstract class OmemoService(userDevice, gullibleTrustCallback, getOmemoRatchet(manager), messageKey, iv, null); - } catch (InvalidKeyException | InvalidAlgorithmParameterException | NoSuchPaddingException | BadPaddingException | UnsupportedEncodingException | NoSuchProviderException | IllegalBlockSizeException e) { + } catch (InvalidKeyException | InvalidAlgorithmParameterException | NoSuchPaddingException | BadPaddingException | UnsupportedEncodingException | IllegalBlockSizeException e) { throw new CryptoFailedException(e); } @@ -370,7 +369,7 @@ public abstract class OmemoService( userDevice, manager.getTrustCallback(), getOmemoRatchet(managerGuard.get()), messageKey, iv, message); - } catch (UnsupportedEncodingException | BadPaddingException | IllegalBlockSizeException | NoSuchProviderException | + } catch (UnsupportedEncodingException | BadPaddingException | IllegalBlockSizeException | NoSuchPaddingException | InvalidAlgorithmParameterException | InvalidKeyException | NoSuchAlgorithmException e) { throw new CryptoFailedException(e); } @@ -557,7 +556,7 @@ public abstract class OmemoService(bundle)); } @@ -607,7 +606,7 @@ public abstract class OmemoService(deviceList)); } @@ -786,33 +785,6 @@ public abstract class OmemoService buildMissingSessionsWithContact(XMPPConnection connection, - OmemoDevice userDevice, - BareJid contact) - throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { - - OmemoCachedDeviceList contactsDeviceIds = getOmemoStoreBackend().loadCachedDeviceList(userDevice, contact); - Set contactsDevices = new HashSet<>(); - for (int deviceId : contactsDeviceIds.getActiveDevices()) { - contactsDevices.add(new OmemoDevice(contact, deviceId)); - } - - return buildMissingSessionsWithDevices(connection, userDevice, contactsDevices); - } - /** * Build sessions with all devices from the set, we don't have a session with yet. * Return the set of all devices we have a session with afterwards. @@ -854,34 +826,6 @@ public abstract class OmemoService buildMissingSessionsWithContacts(XMPPConnection connection, - OmemoDevice userDevice, - Set contacts) - throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { - - Set devicesWithSessions = new HashSet<>(); - - for (BareJid contact : contacts) { - Set devices = buildMissingSessionsWithContact(connection, userDevice, contact); - devicesWithSessions.addAll(devices); - } - - return devicesWithSessions; - } - /** * Return a set of all devices from the provided set, which trust level is undecided. * A device is also considered undecided, if its fingerprint cannot be loaded. @@ -914,37 +858,6 @@ public abstract class OmemoService getUntrustedDeviced(OmemoDevice userDevice, OmemoTrustCallback trustCallback, Set devices) { - Set untrustedDevices = new HashSet<>(); - - for (OmemoDevice device : devices) { - - OmemoFingerprint fingerprint; - try { - fingerprint = getOmemoStoreBackend().getFingerprint(userDevice, device); - } catch (CorruptedOmemoKeyException | NoIdentityKeyException e) { - // TODO: Best solution? - untrustedDevices.add(device); - continue; - } - - if (trustCallback.getTrust(device, fingerprint) == TrustState.untrusted) { - untrustedDevices.add(device); - } - } - - return untrustedDevices; - } - /** * Return true, if the OmemoManager of userDevice has a session with the contactsDevice. * diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/CipherAndAuthTag.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/CipherAndAuthTag.java index 964a33910..3f03f2f72 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/CipherAndAuthTag.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/internal/CipherAndAuthTag.java @@ -1,6 +1,6 @@ /** * - * Copyright 2017 Paul Schaub + * Copyright 2017 Paul Schaub, 2019 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,11 +18,9 @@ package org.jivesoftware.smackx.omemo.internal; import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.CIPHERMODE; import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.KEYTYPE; -import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.PROVIDER; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.IvParameterSpec; @@ -50,13 +48,13 @@ public class CipherAndAuthTag { Cipher cipher; try { - cipher = Cipher.getInstance(CIPHERMODE, PROVIDER); + cipher = Cipher.getInstance(CIPHERMODE); SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE); IvParameterSpec ivSpec = new IvParameterSpec(iv); cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); } catch (NoSuchAlgorithmException | java.security.InvalidKeyException | InvalidAlgorithmParameterException | - NoSuchPaddingException | NoSuchProviderException e) { + NoSuchPaddingException e) { throw new CryptoFailedException(e); } diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoConstants.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoConstants.java index c362145c6..885260568 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoConstants.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoConstants.java @@ -58,6 +58,5 @@ public final class OmemoConstants { public static final String KEYTYPE = "AES"; public static final int KEYLENGTH = 128; public static final String CIPHERMODE = "AES/GCM/NoPadding"; - public static final String PROVIDER = "BC"; } } diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoMessageBuilder.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoMessageBuilder.java index 8d254f44c..7a625ee15 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoMessageBuilder.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/util/OmemoMessageBuilder.java @@ -1,6 +1,6 @@ /** * - * Copyright 2017 Paul Schaub + * Copyright 2017 Paul Schaub, 2019 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,7 +19,6 @@ package org.jivesoftware.smackx.omemo.util; import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.CIPHERMODE; import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.KEYLENGTH; import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.KEYTYPE; -import static org.jivesoftware.smackx.omemo.util.OmemoConstants.Crypto.PROVIDER; import java.io.UnsupportedEncodingException; import java.security.InvalidAlgorithmParameterException; @@ -96,7 +95,6 @@ public class OmemoMessageBuilder ratchet, String message) throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, - UnsupportedEncodingException, NoSuchProviderException, InvalidAlgorithmParameterException { + UnsupportedEncodingException, InvalidAlgorithmParameterException { this(userDevice, callback, ratchet, generateKey(KEYTYPE, KEYLENGTH), generateIv(), message); } @@ -150,7 +147,6 @@ public class OmemoMessageBuilder to, String rpad, Date timestamp, List payload) { + public CryptElement(Set to, String rpad, Date timestamp, List payload) { super(to, rpad, timestamp, payload); } - public CryptElement(Set to, List payload) { + public CryptElement(Set to, List payload) { super(to, payload); } diff --git a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/element/EncryptedOpenPgpContentElement.java b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/element/EncryptedOpenPgpContentElement.java index 951dc90f8..a79dc8997 100644 --- a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/element/EncryptedOpenPgpContentElement.java +++ b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/element/EncryptedOpenPgpContentElement.java @@ -1,6 +1,6 @@ /** * - * Copyright 2017 Florian Schmaus, 2018 Paul Schaub. + * Copyright 2017-2019 Florian Schmaus, 2018 Paul Schaub. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,13 @@ */ package org.jivesoftware.smackx.ox.element; -import java.security.SecureRandom; import java.util.Date; import java.util.List; import java.util.Set; import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.util.Objects; +import org.jivesoftware.smack.util.RandomUtil; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.XmlStringBuilder; @@ -38,14 +38,14 @@ public abstract class EncryptedOpenPgpContentElement extends OpenPgpContentEleme private final String rpad; - protected EncryptedOpenPgpContentElement(Set to, String rpad, Date timestamp, List payload) { + protected EncryptedOpenPgpContentElement(Set to, String rpad, Date timestamp, List payload) { super(Objects.requireNonNullNorEmpty( to, "Encrypted OpenPGP content elements must have at least one 'to' attribute."), timestamp, payload); this.rpad = Objects.requireNonNull(rpad); } - protected EncryptedOpenPgpContentElement(Set to, List payload) { + protected EncryptedOpenPgpContentElement(Set to, List payload) { super(Objects.requireNonNullNorEmpty( to, "Encrypted OpenPGP content elements must have at least one 'to' attribute."), new Date(), payload); @@ -53,8 +53,7 @@ public abstract class EncryptedOpenPgpContentElement extends OpenPgpContentEleme } private static String createRandomPadding() { - SecureRandom secRan = new SecureRandom(); - int len = secRan.nextInt(256); + int len = RandomUtil.nextSecureRandomInt(256); return StringUtils.randomString(len); } diff --git a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/element/OpenPgpContentElement.java b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/element/OpenPgpContentElement.java index 2b61db669..db7014eef 100644 --- a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/element/OpenPgpContentElement.java +++ b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/element/OpenPgpContentElement.java @@ -1,6 +1,6 @@ /** * - * Copyright 2017 Florian Schmaus, 2018 Paul Schaub. + * Copyright 2017-2019 Florian Schmaus, 2018 Paul Schaub. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,13 +48,13 @@ public abstract class OpenPgpContentElement implements ExtensionElement { public static final String ATTR_STAMP = "stamp"; public static final String ELEM_PAYLOAD = "payload"; - private final Set to; + private final Set to; private final Date timestamp; private final MultiMap payload; private String timestampString; - protected OpenPgpContentElement(Set to, Date timestamp, List payload) { + protected OpenPgpContentElement(Set to, Date timestamp, List payload) { this.to = to; this.timestamp = Objects.requireNonNull(timestamp); this.payload = new MultiMap<>(); @@ -68,7 +68,7 @@ public abstract class OpenPgpContentElement implements ExtensionElement { * * @return recipients. */ - public final Set getTo() { + public final Set getTo() { return to; } diff --git a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/element/SigncryptElement.java b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/element/SigncryptElement.java index d0d165a86..c92f02f0f 100644 --- a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/element/SigncryptElement.java +++ b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/element/SigncryptElement.java @@ -1,6 +1,6 @@ /** * - * Copyright 2017 Florian Schmaus, 2018 Paul Schaub. + * Copyright 2017-2019 Florian Schmaus, 2018 Paul Schaub. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,11 +35,11 @@ public class SigncryptElement extends EncryptedOpenPgpContentElement { public static final String ELEMENT = "signcrypt"; - public SigncryptElement(Set to, String rpad, Date timestamp, List payload) { + public SigncryptElement(Set to, String rpad, Date timestamp, List payload) { super(to, rpad, timestamp, payload); } - public SigncryptElement(Set to, List payload) { + public SigncryptElement(Set to, List payload) { super(to, payload); } diff --git a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/util/OpenPgpPubSubUtil.java b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/util/OpenPgpPubSubUtil.java index 088c23e8f..9880facf1 100644 --- a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/util/OpenPgpPubSubUtil.java +++ b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/util/OpenPgpPubSubUtil.java @@ -204,7 +204,7 @@ public class OpenPgpPubSubUtil { public static PublicKeysListElement fetchPubkeysList(XMPPConnection connection, BareJid contact) throws InterruptedException, XMPPException.XMPPErrorException, SmackException.NoResponseException, PubSubException.NotALeafNodeException, SmackException.NotConnectedException, PubSubException.NotAPubSubNodeException { - PubSubManager pm = PubSubManager.getInstance(connection, contact); + PubSubManager pm = PubSubManager.getInstanceFor(connection, contact); LeafNode node = getLeafNode(pm, PEP_NODE_PUBLIC_KEYS); List> list = node.getItems(1); @@ -274,7 +274,7 @@ public class OpenPgpPubSubUtil { public static PubkeyElement fetchPubkey(XMPPConnection connection, BareJid contact, OpenPgpV4Fingerprint v4_fingerprint) throws InterruptedException, XMPPException.XMPPErrorException, PubSubException.NotAPubSubNodeException, PubSubException.NotALeafNodeException, SmackException.NotConnectedException, SmackException.NoResponseException { - PubSubManager pm = PubSubManager.getInstance(connection, contact); + PubSubManager pm = PubSubManager.getInstanceFor(connection, contact); String nodeName = PEP_NODE_PUBLIC_KEY(v4_fingerprint); LeafNode node = getLeafNode(pm, nodeName); diff --git a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox_im/OXInstantMessagingManager.java b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox_im/OXInstantMessagingManager.java index 0056a121c..591ead494 100644 --- a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox_im/OXInstantMessagingManager.java +++ b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox_im/OXInstantMessagingManager.java @@ -135,7 +135,7 @@ public final class OXInstantMessagingManager extends Manager { * @param connection XMPP connection * @return manager instance */ - public static OXInstantMessagingManager getInstanceFor(XMPPConnection connection) { + public static synchronized OXInstantMessagingManager getInstanceFor(XMPPConnection connection) { OXInstantMessagingManager manager = INSTANCES.get(connection); if (manager == null) { diff --git a/smack-repl/build.gradle b/smack-repl/build.gradle index aac8253e9..2b36f85ef 100644 --- a/smack-repl/build.gradle +++ b/smack-repl/build.gradle @@ -14,6 +14,7 @@ dependencies { compile project(':smack-bosh') compile project(':smack-java7') compile project(':smack-resolver-minidns') + compile project(':smack-resolver-minidns-dox') compile project(':smack-extensions') compile project(':smack-experimental') compile project(':smack-legacy') diff --git a/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/DoX.java b/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/DoX.java new file mode 100644 index 000000000..ddab89d4d --- /dev/null +++ b/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/DoX.java @@ -0,0 +1,78 @@ +/** + * + * Copyright 2019 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.igniterealtime.smack.smackrepl; + +import java.io.IOException; + +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.debugger.ConsoleDebugger; +import org.jivesoftware.smack.tcp.XMPPTCPConnection; +import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration; +import org.jivesoftware.smackx.dox.DnsOverXmppManager; +import org.jivesoftware.smackx.dox.resolver.minidns.DnsOverXmppMiniDnsResolver; + +import org.jxmpp.jid.Jid; +import org.jxmpp.jid.impl.JidCreate; +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsmessage.Question; +import org.minidns.record.Record; + +public class DoX { + + public static void main(String[] args) throws XMPPException, SmackException, IOException, InterruptedException { + SmackConfiguration.DEBUG = true; + + XMPPTCPConnection connection = new XMPPTCPConnection(args[0], args[1]); + connection.setReplyTimeout(60000); + + connection.connect().login(); + + DnsOverXmppManager dox = DnsOverXmppManager.getInstanceFor(connection); + + Jid target = JidCreate.from("dns@moparisthebest.com/listener"); + Question question = new Question("geekplace.eu", Record.TYPE.A); + + DnsMessage response = dox.query(target, question); + + // CHECKSTYLE:OFF + System.out.println(response); + // CHECKSTYLE:ON + + connection.disconnect(); + } + + public static XMPPTCPConnection runDoxResolver(String jid, String password) + throws XMPPException, SmackException, IOException, InterruptedException { + XMPPTCPConnectionConfiguration config = XMPPTCPConnectionConfiguration.builder() + .setXmppAddressAndPassword(jid, password) + .setResource("dns") + .setDebuggerFactory(ConsoleDebugger.Factory.INSTANCE) + .build(); + XMPPTCPConnection connection = new XMPPTCPConnection(config); + + connection.connect().login(); + + DnsOverXmppManager dox = DnsOverXmppManager.getInstanceFor(connection); + dox.setDnsOverXmppResolver(DnsOverXmppMiniDnsResolver.INSTANCE); + + dox.enable(); + + return connection; + } +} diff --git a/smack-resolver-minidns-dox/build.gradle b/smack-resolver-minidns-dox/build.gradle new file mode 100644 index 000000000..3d070c5bd --- /dev/null +++ b/smack-resolver-minidns-dox/build.gradle @@ -0,0 +1,8 @@ +description = """\ +DNS over XMPP (DoX) support using MiniDNS.""" + +dependencies { + compile project(path: ':smack-resolver-minidns') + compile project(path: ':smack-experimental') + testCompile project(path: ":smack-core", configuration: "testRuntime") +} diff --git a/smack-resolver-minidns-dox/src/main/java/org/jivesoftware/smackx/dox/resolver/minidns/DnsOverXmppMiniDnsResolver.java b/smack-resolver-minidns-dox/src/main/java/org/jivesoftware/smackx/dox/resolver/minidns/DnsOverXmppMiniDnsResolver.java new file mode 100644 index 000000000..adf8e202b --- /dev/null +++ b/smack-resolver-minidns-dox/src/main/java/org/jivesoftware/smackx/dox/resolver/minidns/DnsOverXmppMiniDnsResolver.java @@ -0,0 +1,57 @@ +/** + * + * Copyright 2019 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.dox.resolver.minidns; + +import java.io.IOException; + +import org.jivesoftware.smackx.dox.DnsOverXmppResolver; + +import org.minidns.DnsClient; +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsmessage.Question; +import org.minidns.dnsqueryresult.DnsQueryResult; +import org.minidns.dnssec.DnssecClient; +import org.minidns.dnssec.DnssecQueryResult; + +public final class DnsOverXmppMiniDnsResolver implements DnsOverXmppResolver { + + public static final DnsOverXmppMiniDnsResolver INSTANCE = new DnsOverXmppMiniDnsResolver(new DnsClient(), new DnssecClient()); + + private final DnsClient dnsClient; + private final DnssecClient dnssecClient; + + DnsOverXmppMiniDnsResolver(DnsClient dnsClient, DnssecClient dnssecClient) { + this.dnsClient = dnsClient; + this.dnssecClient = dnssecClient; + } + + @Override + public DnsMessage resolve(DnsMessage query) throws IOException { + Question question = query.getQuestion(); + + final DnsQueryResult dnsQueryResult; + if (query.isDnssecOk()) { + DnssecQueryResult dnssecQueryResult = dnssecClient.queryDnssec(question); + dnsQueryResult = dnssecQueryResult.dnsQueryResult; + } else { + dnsQueryResult = dnsClient.query(question); + } + + return dnsQueryResult.response; + } + +} diff --git a/smack-resolver-minidns-dox/src/main/java/org/jivesoftware/smackx/dox/resolver/minidns/package-info.java b/smack-resolver-minidns-dox/src/main/java/org/jivesoftware/smackx/dox/resolver/minidns/package-info.java new file mode 100644 index 000000000..e101e6f20 --- /dev/null +++ b/smack-resolver-minidns-dox/src/main/java/org/jivesoftware/smackx/dox/resolver/minidns/package-info.java @@ -0,0 +1,21 @@ +/** + * + * Copyright 2019 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. + */ + +/** + * XEP-0418: DNS Queries over XMPP (Dox) using MiniDNS. + */ +package org.jivesoftware.smackx.dox.resolver.minidns; diff --git a/smack-resolver-minidns-dox/src/test/java/org/jivesoftware/smackx/dox/resolver/minidns/DnsOverXmppMiniDnsResolverTest.java b/smack-resolver-minidns-dox/src/test/java/org/jivesoftware/smackx/dox/resolver/minidns/DnsOverXmppMiniDnsResolverTest.java new file mode 100644 index 000000000..e9d91f6ef --- /dev/null +++ b/smack-resolver-minidns-dox/src/test/java/org/jivesoftware/smackx/dox/resolver/minidns/DnsOverXmppMiniDnsResolverTest.java @@ -0,0 +1,167 @@ +/** + * + * Copyright 2019 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.dox.resolver.minidns; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.net.InetAddress; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; +import org.minidns.DnsCache; +import org.minidns.DnsClient; +import org.minidns.MiniDnsFuture; +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsmessage.DnsMessage.RESPONSE_CODE; +import org.minidns.dnsmessage.Question; +import org.minidns.dnsname.DnsName; +import org.minidns.dnsqueryresult.CachedDnsQueryResult; +import org.minidns.dnsqueryresult.DnsQueryResult; +import org.minidns.dnssec.DnssecClient; +import org.minidns.dnssec.DnssecValidationFailedException; +import org.minidns.record.Record.TYPE; +import org.minidns.source.DnsDataSource; + +public final class DnsOverXmppMiniDnsResolverTest { + + @Test + public void dnsOverXmppMiniDnsResolverTest() throws IOException { + TestDnsDataSource dnsSource = new TestDnsDataSource(); + TestDnsDataSource dnssecSource = new TestDnsDataSource(); + + DnsClient dnsClient = new DnsClient(NoopDnsCache.INSTANCE); + dnsClient.setDataSource(dnsSource); + + DnssecClient dnssecClient = new DnssecClient(NoopDnsCache.INSTANCE); + dnssecClient.setDataSource(dnssecSource); + + DnsOverXmppMiniDnsResolver doxResolver = new DnsOverXmppMiniDnsResolver(dnsClient, dnssecClient); + + Question question = new Question("example.org", TYPE.A); + + { + DnsMessage nondnssecQuery = question.asQueryMessage(); + + doxResolver.resolve(nondnssecQuery); + + assertTrue(dnsSource.getAndResetWasQueried()); + assertFalse(dnssecSource.getAndResetWasQueried()); + } + + { + DnsMessage.Builder dnssecQueryBuilder = question.asMessageBuilder(); + dnssecQueryBuilder.getEdnsBuilder().setDnssecOk(); + DnsMessage dnssecQuery = dnssecQueryBuilder.build(); + + DnssecValidationFailedException dnssecValidationFailedException = null; + try { + doxResolver.resolve(dnssecQuery); + } catch (DnssecValidationFailedException e) { + dnssecValidationFailedException = e; + } + // This exception is expected since we don't have a realy DNS source. + assertNotNull(dnssecValidationFailedException); + + assertFalse(dnsSource.getAndResetWasQueried()); + assertTrue(dnssecSource.getAndResetWasQueried()); + } + } + + public static class TestDnsDataSource implements DnsDataSource { + + private final AtomicBoolean wasQueried = new AtomicBoolean(); + + public boolean getAndResetWasQueried() { + return wasQueried.getAndSet(false); + } + + private void setWasQueried() { + wasQueried.set(true); + } + + @Override + public DnsQueryResult query(DnsMessage query, InetAddress address, int port) throws IOException { + setWasQueried(); + return new TestDnsQueryResult(query); + } + + @Override + public MiniDnsFuture queryAsync(DnsMessage query, InetAddress address, int port, + OnResponseCallback onResponseCallback) { + setWasQueried(); + DnsQueryResult result = new TestDnsQueryResult(query); + return MiniDnsFuture.from(result); + } + + @Override + public int getUdpPayloadSize() { + return 0; + } + + @Override + public int getTimeout() { + return 0; + } + + @Override + public void setTimeout(int timeout) { + } + + private static class TestDnsQueryResult extends DnsQueryResult { + + protected TestDnsQueryResult(DnsMessage query) { + super(QueryMethod.testWorld, query, createNxDomainAnswerFor(query)); + } + + private static DnsMessage createNxDomainAnswerFor(DnsMessage query) { + Question question = query.getQuestion(); + + DnsMessage response = DnsMessage.builder() + .setQuestion(question) + .setRecursionAvailable(true) + .setResponseCode(RESPONSE_CODE.NX_DOMAIN) + .build(); + + return response; + } + } + } + + // TODO: Workaround for NPE-if-no-cache-set bug in MiniDNS. Remove we use a MiniDNS version where this is fixed, + // i.e. one that has 864fbb5 ("Fix NPE in AbstractDnsClient if cache is 'null'") + private static class NoopDnsCache extends DnsCache { + + private static final NoopDnsCache INSTANCE = new NoopDnsCache(); + + @Override + protected void putNormalized(DnsMessage normalizedQuery, DnsQueryResult result) { + } + + @Override + public void offer(DnsMessage query, DnsQueryResult result, DnsName authoritativeZone) { + } + + @Override + protected CachedDnsQueryResult getNormalized(DnsMessage normalizedQuery) { + return null; + } + + } +} diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java index 183275771..dd7b7a30a 100644 --- a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java @@ -121,7 +121,6 @@ import org.jivesoftware.smack.util.dns.HostAddress; import org.jxmpp.jid.impl.JidCreate; import org.jxmpp.jid.parts.Resourcepart; import org.jxmpp.stringprep.XmppStringprepException; -import org.jxmpp.util.XmppStringUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -319,7 +318,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { * @throws XmppStringprepException */ public XMPPTCPConnection(CharSequence jid, String password) throws XmppStringprepException { - this(XmppStringUtils.parseLocalpart(jid.toString()), password, XmppStringUtils.parseDomain(jid.toString())); + this(XMPPTCPConnectionConfiguration.builder().setXmppAddressAndPassword(jid, password).build()); } /** @@ -482,10 +481,6 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { } private void shutdown(boolean instant) { - if (disconnectedButResumeable) { - return; - } - // First shutdown the writer, this will result in a closing stream element getting send to // the server LOGGER.finer("PacketWriter shutdown()"); @@ -503,6 +498,15 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { CloseableUtil.maybeClose(socket, LOGGER); setWasAuthenticated(); + + // Wait for reader and writer threads to be terminated. + readerWriterSemaphore.acquireUninterruptibly(2); + readerWriterSemaphore.release(2); + + if (disconnectedButResumeable) { + return; + } + // If we are able to resume the stream, then don't set // connected/authenticated/usingTLS to false since we like behave like we are still // connected (e.g. sendStanza should not throw a NotConnectedException). @@ -523,10 +527,6 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { writer = null; initState(); - - // Wait for reader and writer threads to be terminated. - readerWriterSemaphore.acquireUninterruptibly(2); - readerWriterSemaphore.release(2); } @Override @@ -860,6 +860,8 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { protected class PacketReader { + private final String threadName = "Smack Reader (" + getConnectionCounter() + ')'; + XmlPullParser parser; private volatile boolean done; @@ -874,13 +876,15 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { Async.go(new Runnable() { @Override public void run() { + LOGGER.finer(threadName + " start"); try { parsePackets(); } finally { + LOGGER.finer(threadName + " exit"); XMPPTCPConnection.this.readerWriterSemaphore.release(); } } - }, "Smack Reader (" + getConnectionCounter() + ")"); + }, threadName); } /** @@ -1128,6 +1132,8 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { protected class PacketWriter { public static final int QUEUE_SIZE = XMPPTCPConnection.QUEUE_SIZE; + private final String threadName = "Smack Writer (" + getConnectionCounter() + ')'; + private final ArrayBlockingQueueWithShutdown queue = new ArrayBlockingQueueWithShutdown<>( QUEUE_SIZE, true); @@ -1173,13 +1179,15 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { Async.go(new Runnable() { @Override public void run() { + LOGGER.finer(threadName + " start"); try { writePackets(); } finally { + LOGGER.finer(threadName + " exit"); XMPPTCPConnection.this.readerWriterSemaphore.release(); } } - }, "Smack Writer (" + getConnectionCounter() + ")"); + }, threadName); } private boolean done() { diff --git a/version.gradle b/version.gradle index 28e90a9a4..2bb14c10d 100644 --- a/version.gradle +++ b/version.gradle @@ -10,6 +10,7 @@ allprojects { // - https://issues.igniterealtime.org/browse/SMACK-858 jxmppVersion = '0.7.0-alpha5' miniDnsVersion = '0.4.0-alpha3' + bouncyCastleVersion = '1.61' smackMinAndroidSdk = 19 } }