From 927eb5e7d7732ae6b8230d3dd5965fb863fb24be Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 25 Mar 2019 11:57:27 +0100 Subject: [PATCH 01/37] Add MemoryLeakTest(Util) to check for the correct operation of what was implemented with SMACK-383. --- .../jivesoftware/smack/DummyConnection.java | 5 + .../smack/util/MemoryLeakTestUtil.java | 140 ++++++++++++++++++ .../smackx/muc/MucMemoryLeakTest.java | 32 ++++ 3 files changed, 177 insertions(+) create mode 100644 smack-core/src/test/java/org/jivesoftware/smack/util/MemoryLeakTestUtil.java create mode 100644 smack-extensions/src/test/java/org/jivesoftware/smackx/muc/MucMemoryLeakTest.java 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/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-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)); + } + +} From 14f288a763a5f386e62e4cd5f8f9a41b8e0427f0 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 25 Mar 2019 12:07:01 +0100 Subject: [PATCH 02/37] Introduce RandomUtil and use it in EncryptedOpenPgpContentElement --- .../jivesoftware/smack/util/RandomUtil.java | 46 +++++++++++++++++++ .../jivesoftware/smack/util/StringUtils.java | 26 ++--------- .../EncryptedOpenPgpContentElement.java | 7 ++- 3 files changed, 52 insertions(+), 27 deletions(-) create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/util/RandomUtil.java 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..6a0c617ba --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/RandomUtil.java @@ -0,0 +1,46 @@ +/** + * + * 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); + } +} 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..82e75f8b7 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,18 +275,11 @@ 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) { 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..0494e80d0 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; @@ -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); } From 89c0fa4b9933f225cf9deaf0713df5dc0c6a0d31 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 25 Mar 2019 12:11:59 +0100 Subject: [PATCH 03/37] Let StringUtils.(insecure)randomString() return empty string in case length is zero. Also do throw a NegativeArraySizeException if length is negative instead of returning null. This fixes the following sporadic test issue: org.jivesoftware.smackx.ox.PainlessOpenPgpProviderTest > encryptDecryptTest FAILED java.lang.AssertionError at org.jivesoftware.smack.util.XmlStringBuilder.escape(XmlStringBuilder.java:425) at org.jivesoftware.smackx.ox.element.EncryptedOpenPgpContentElement.addCommonXml(EncryptedOpenPgpContentElement.java:65) at org.jivesoftware.smackx.ox.element.CryptElement.toXML(CryptElement.java:51) at org.jivesoftware.smackx.ox.element.CryptElement.toXML(CryptElement.java:31) at org.jivesoftware.smack.packet.Element.toXML(Element.java:41) at org.jivesoftware.smackx.ox.element.OpenPgpContentElement.toInputStream(OpenPgpContentElement.java:186) at org.jivesoftware.smackx.ox.crypto.PainlessOpenPgpProvider.encrypt(PainlessOpenPgpProvider.java:136) at org.jivesoftware.smackx.ox.PainlessOpenPgpProviderTest.encryptDecryptTest(PainlessOpenPgpProviderTest.java:155) because EncryptedOpenPgpContentElement rpad field was sometimes 'null' in case the random function returned '0' as length. --- .../jivesoftware/smack/util/StringUtils.java | 4 ++-- .../smack/util/StringUtilsTest.java | 23 ++++++++++++------- 2 files changed, 17 insertions(+), 10 deletions(-) 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 82e75f8b7..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 @@ -283,8 +283,8 @@ public class StringUtils { } 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/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); + } } From ab7d81e7b58d39e1fbebad32a4d492165d4f1783 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 25 Mar 2019 12:29:40 +0100 Subject: [PATCH 04/37] Use type parameter bounds for the 'to' set in OpenPgpgContentElement --- .../org/jivesoftware/smackx/ox/element/CryptElement.java | 6 +++--- .../smackx/ox/element/EncryptedOpenPgpContentElement.java | 4 ++-- .../smackx/ox/element/OpenPgpContentElement.java | 8 ++++---- .../jivesoftware/smackx/ox/element/SigncryptElement.java | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/element/CryptElement.java b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/element/CryptElement.java index 1189d1607..7a9874f79 100644 --- a/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/element/CryptElement.java +++ b/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/element/CryptElement.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. @@ -32,11 +32,11 @@ public class CryptElement extends EncryptedOpenPgpContentElement { public static final String ELEMENT = "crypt"; - public CryptElement(Set 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 0494e80d0..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 @@ -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); 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); } From 5d46e281fcbd60f34bd96ccbe36faa8072d39354 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 25 Mar 2019 13:06:12 +0100 Subject: [PATCH 05/37] XMPPTCPConnection log when reader/writer threads start and exit --- .../jivesoftware/smack/tcp/XMPPTCPConnection.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) 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 97e0e2044..2441845c6 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 @@ -1063,6 +1063,8 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { protected class PacketReader { + private final String threadName = "Smack Reader (" + getConnectionCounter() + ')'; + XmlPullParser parser; private volatile boolean done; @@ -1077,13 +1079,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); } /** @@ -1336,6 +1340,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); @@ -1381,13 +1387,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() { From 7f542e403ff66c8c257fa2a8a2a455ab96da8b23 Mon Sep 17 00:00:00 2001 From: Alameyo Date: Wed, 27 Mar 2019 00:12:58 +0100 Subject: [PATCH 06/37] Adjust in documentation StanzaTypeFilter to uppercase as in code --- documentation/processing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. From 25b3f354216683e568a54b93be1c6a931cd79b26 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Thu, 28 Mar 2019 13:52:17 +0100 Subject: [PATCH 07/37] Ensure that shutdown() terminates reader/writer threads In case an exception happens in connect()/login() the 'disconnectedButResumable' boolean may (still) be set. Which causes only one of the reader and writer threads to exit, typically the reader thread, because shutdown() will bail out very early. This leaves a dangling (writer) thread causing memory leaks and deadlocks on a subsequent connect()/login(). --- .../smack/tcp/XMPPTCPConnection.java | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) 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 2441845c6..a08221ddb 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 @@ -508,10 +508,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()"); @@ -534,13 +530,25 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { packetReader.shutdown(); LOGGER.finer("PacketReader has been shut down"); - try { + final Socket socket = this.socket; + if (socket != null && socket.isConnected()) { + try { socket.close(); - } catch (Exception e) { + } catch (Exception e) { LOGGER.log(Level.WARNING, "shutdown", e); + } } 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). @@ -561,10 +569,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 From 9be498c440e2b10cae4d01b4d0037bfdb90c85cf Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 1 Apr 2019 22:02:36 +0200 Subject: [PATCH 08/37] Fix NPE in Roster's presence listeners if 'from' is not set The NPE is caused by an inbound presence stanza without the 'from' attribute set. The stacktrace of the NPE is: FATAL EXCEPTION: Smack Cached Executor Process: de.fhg.ivi.senetz.mobile.android.mbk.debug, PID: 13365 java.lang.NullPointerException: Attempt to invoke virtual method 'int java.lang.Object.hashCode()' on a null object reference at java.util.concurrent.ConcurrentHashMap.get(ConcurrentHashMap.java:944) at org.jivesoftware.smack.roster.Roster.getPresencesInternal(Roster.java:374) at org.jivesoftware.smack.roster.Roster.getOrCreatePresencesInternal(Roster.java:388) at org.jivesoftware.smack.roster.Roster.access$1100(Roster.java:94) at org.jivesoftware.smack.roster.Roster$PresencePacketListener$1.run(Roster.java:1519) at org.jivesoftware.smack.AsyncButOrdered$Handler.run(AsyncButOrdered.java:121) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1162) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:636) at java.lang.Thread.run(Thread.java:764) Thanks to Marcel Heckel for reporting this. Fixes SMACK-861. --- .../org/jivesoftware/smack/roster/Roster.java | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) 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 8e241151d..73486cd12 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 @@ -1471,7 +1471,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 From d6a90942a45bcae9cf0a54583cdc30b3ee896a6a Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Tue, 2 Apr 2019 15:53:47 +0200 Subject: [PATCH 09/37] Unify Bouncy Castle versions: Add bouncyCastleVersion variable --- smack-experimental/build.gradle | 2 +- smack-omemo/build.gradle | 2 +- version.gradle | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) 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-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/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 } } From 9f8d13b8cdb53f2e2373a92203c5aa32b09a23fa Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Tue, 2 Apr 2019 15:54:25 +0200 Subject: [PATCH 10/37] Do not explicitly select the Provider in HashManager Note that we still setup the BouncyCastleProvider so all requested MessageDigest instances should be avaialble. --- .../smackx/hashes/HashManager.java | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) 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); } } From 7d7fbe68286d77f60c8f390034ee39f849f42dfa Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Tue, 2 Apr 2019 15:55:31 +0200 Subject: [PATCH 11/37] Do not explicity select the (crypto) Provider in smack-omemo This makes the system select the "best" available provider. Also the 'BC' provider in newer Android version does not longer implement certain Ciphers, which causes an NoSuchAlgorithmException if the Cipher is requested explicitly by the 'BC' provider: E/XmppService: XmppServiceConnection - Error while sending pending messages org.jivesoftware.smackx.omemo.exceptions.CryptoFailedException: java.security.NoSuchAlgorithmException: The BC provider no longer provides an implementation for Cipher.AES/GCM/NoPadding. Please see https://android-developers.googleblog.com/2018/03/cryptography-changes-in-android-p.html for more details. at org.jivesoftware.smackx.omemo.OmemoService.encrypt(OmemoService.java:375) at org.jivesoftware.smackx.omemo.OmemoService.createOmemoMessage(OmemoService.java:537) at org.jivesoftware.smackx.omemo.OmemoManager.encrypt(OmemoManager.java:341) at org.jivesoftware.smackx.omemo.OmemoManager.encrypt(OmemoManager.java:314) at es.iecisa.xmppservice.XmppServiceConnection.lambda$sendMessage$0(XmppServiceConnection.java:516) at es.iecisa.xmppservice.-$$Lambda$XmppServiceConnection$aBU_80chagvypMTSd-aSm7pRQRY.run(Unknown Source:4) at java.lang.Thread.run(Thread.java:764) Caused by: java.security.NoSuchAlgorithmException: The BC provider no longer provides an implementation for Cipher.AES/GCM/NoPadding. Please see https://android-developers.googleblog.com/2018/03/cryptography-changes-in-android-p.html for more details. at sun.security.jca.Providers.checkBouncyCastleDeprecation(Providers.java:563) at sun.security.jca.Providers.checkBouncyCastleDeprecation(Providers.java:346) at javax.crypto.Cipher.createCipher(Cipher.java:722) at javax.crypto.Cipher.getInstance(Cipher.java:717) at javax.crypto.Cipher.getInstance(Cipher.java:674) at org.jivesoftware.smackx.omemo.util.OmemoMessageBuilder.setMessage(OmemoMessageBuilder.java:169) at org.jivesoftware.smackx.omemo.util.OmemoMessageBuilder.(OmemoMessageBuilder.java:116) at org.jivesoftware.smackx.omemo.OmemoService.encrypt(OmemoService.java:372) at org.jivesoftware.smackx.omemo.OmemoService.createOmemoMessage(OmemoService.java:537) at org.jivesoftware.smackx.omemo.OmemoManager.encrypt(OmemoManager.java:341) at org.jivesoftware.smackx.omemo.OmemoManager.encrypt(OmemoManager.java:314) at es.iecisa.xmppservice.XmppServiceConnection.lambda$sendMessage$0(XmppServiceConnection.java:516) at es.iecisa.xmppservice.-$$Lambda$XmppServiceConnection$aBU_80chagvypMTSd-aSm7pRQRY.run(Unknown Source:4) at java.lang.Thread.run(Thread.java:764) --- .../jivesoftware/smackx/omemo/OmemoService.java | 7 +++---- .../smackx/omemo/internal/CipherAndAuthTag.java | 8 +++----- .../smackx/omemo/util/OmemoConstants.java | 1 - .../smackx/omemo/util/OmemoMessageBuilder.java | 14 +++++--------- 4 files changed, 11 insertions(+), 19 deletions(-) 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..56d0c4d9f 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); } 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 Date: Tue, 2 Apr 2019 16:00:18 +0200 Subject: [PATCH 12/37] Remove dead code from OmemoService --- .../smackx/omemo/OmemoService.java | 86 ------------------- 1 file changed, 86 deletions(-) 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 56d0c4d9f..34f0c3dcc 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 @@ -785,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. @@ -853,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. @@ -913,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. * From af8822791914d9f00bb9acdffe9bb60962e1bf5d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 6 Apr 2019 02:36:32 +0200 Subject: [PATCH 13/37] Add missing Roster documentation --- .../org/jivesoftware/smack/roster/Roster.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) 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 8e241151d..08889fe51 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); } From 38384a1eed5ec4f7fcd9ca2e2998cfd6e2d02e07 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Sun, 7 Apr 2019 16:43:48 +0200 Subject: [PATCH 14/37] Improve javadoc of ConnectionConfiguration --- .../smack/ConnectionConfiguration.java | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) 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..566ede4ae 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 { From d6b6fdca171a889e93d4bd118e3c15cc0ad3ab7f Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Sun, 7 Apr 2019 16:44:04 +0200 Subject: [PATCH 15/37] Add ConnectionConfiguration.setXmppAddressAndPassword() --- .../smack/ConnectionConfiguration.java | 33 +++++++++++++++++++ .../smack/tcp/XMPPTCPConnection.java | 3 +- 2 files changed, 34 insertions(+), 2 deletions(-) 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 566ede4ae..b87f2ccd2 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/ConnectionConfiguration.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/ConnectionConfiguration.java @@ -554,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-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java index e680566d6..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()); } /** From 0d17f195b013916912c6a4fd4b2fe16c211eb46e Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Sun, 7 Apr 2019 22:13:38 +0200 Subject: [PATCH 16/37] Use EntityFullJid as JID type in MultiUserChat.getJoinedRooms() Got a user who put in a EntityBareJid and wondered why the returned list was empty. --- .../org/jivesoftware/smackx/muc/MultiUserChatManager.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatManager.java index 639e2f792..c5225f062 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatManager.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2014-2018 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. @@ -59,6 +59,7 @@ import org.jivesoftware.smackx.muc.packet.MUCUser; import org.jxmpp.jid.DomainBareJid; import org.jxmpp.jid.EntityBareJid; +import org.jxmpp.jid.EntityFullJid; import org.jxmpp.jid.EntityJid; import org.jxmpp.jid.Jid; import org.jxmpp.jid.parts.Resourcepart; @@ -304,7 +305,7 @@ public final class MultiUserChatManager extends Manager { * @throws NotConnectedException * @throws InterruptedException */ - public List 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); From a9673408ccae27ff3a30c4690cf7b17f04a1a91d Mon Sep 17 00:00:00 2001 From: Guus der Kinderen Date: Mon, 8 Apr 2019 16:10:29 +0200 Subject: [PATCH 17/37] Admin should be disconnected after tests. The Smack Integration tests can use an admin account to provision accounts that are used by the tests. This admin account uses an XMPP connection to interact with the server-under-test. When the tests are over, this account should be disconnected explicitly, to prevent stream management from keeping it alive longer than it needs to. --- .../igniterealtime/smack/inttest/XmppConnectionManager.java | 4 ++++ 1 file changed, 4 insertions(+) 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(); + } } From 7d5274dad1351c3fedf5a771568beb1a561349ab Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 8 Apr 2019 23:03:12 +0200 Subject: [PATCH 18/37] Use java.util.Base64 and remove build-in Base64 API --- .../android/AndroidBase64Encoder.java | 16 +- smack-core/build.gradle | 1 - .../smack/util/stringencoder/Base64.java | 52 +- .../smack/test/util/SmackTestSuite.java | 52 +- .../util/stringencoder/java7/Base64.java | 1695 ----------------- .../java7/Java7Base64Encoder.java | 31 +- .../java7/Java7Base64UrlSafeEncoder.java | 12 +- 7 files changed, 57 insertions(+), 1802 deletions(-) delete mode 100644 smack-java7/src/main/java/org/jivesoftware/smack/util/stringencoder/java7/Base64.java 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..42346274b 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,12 @@ 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 byte[] encode(byte[] input) { + return Base64.encode(input, BASE64_ENCODER_FLAGS); } - - @Override - public byte[] encode(byte[] input, int offset, int len) { - return Base64.encode(input, offset, len, 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/util/stringencoder/Base64.java b/smack-core/src/main/java/org/jivesoftware/smack/util/stringencoder/Base64.java index 80b6928d6..bf19067d9 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. @@ -48,20 +48,15 @@ public class Base64 { } 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 byte[] encode(byte[] input) { - return encode(input, 0, input.length); + return base64encoder.encode(input); } public static final byte[] encode(byte[] input, int offset, int len) { - return base64encoder.encode(input, offset, len); + return encode(slice(input, offset, len)); } public static final String decodeToString(String string) { @@ -73,34 +68,41 @@ 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); - - byte[] encode(byte[] input, int offset, int len); + byte[] encode(byte[] input); } } 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..889a32483 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,19 @@ 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 byte[] encode(byte[] input) { + return Base64.getEncoder().encode(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); - } - } }); } } 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..3ea688c92 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,12 @@ 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.Decoder decoder; private Java7Base64Encoder() { - // Use getInstance() + encoder = Base64.getEncoder(); + decoder = Base64.getDecoder(); } public static Java7Base64Encoder getInstance() { @@ -40,27 +40,16 @@ 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 byte[] encode(byte[] input) { + return encoder.encode(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); - } - } - } 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); From 87e0ac9ba128571a3eb5516595103175bd060f61 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 8 Apr 2019 23:04:25 +0200 Subject: [PATCH 19/37] Add IQ.isResponseIQ() --- .../main/java/org/jivesoftware/smack/packet/IQ.java | 10 ++++++++++ 1 file changed, 10 insertions(+) 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..591f94261 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; } From d1f2631771e96a5ef0f813217a99c8283f355e07 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 8 Apr 2019 23:04:46 +0200 Subject: [PATCH 20/37] Use IQ.isResponseIQ() in AbstractXMPPConnection --- .../java/org/jivesoftware/smack/AbstractXMPPConnection.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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..69026be48 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java @@ -1353,7 +1353,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()); From 307543071324331d97d09f3c0c5e9f01e8829419 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 8 Apr 2019 23:05:23 +0200 Subject: [PATCH 21/37] Use IQ.isRequestIQ() in IQ --- smack-core/src/main/java/org/jivesoftware/smack/packet/IQ.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 591f94261..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 @@ -297,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()); } From 1122bf394cd692cce1c75b9aa03cccbf87611240 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 8 Apr 2019 23:06:11 +0200 Subject: [PATCH 22/37] Add ExceptionUtil --- .../smack/debugger/ConsoleDebugger.java | 11 ++---- .../smack/util/ExceptionUtil.java | 38 +++++++++++++++++++ 2 files changed, 41 insertions(+), 8 deletions(-) create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/util/ExceptionUtil.java 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/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(); + } + +} From 75b1d8ce1326552bdbc421228b09c79e52e87100 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 8 Apr 2019 23:06:21 +0200 Subject: [PATCH 23/37] Add AbstractError.Builder.setDescriptiveEnText(String, Exception) --- .../org/jivesoftware/smack/packet/AbstractError.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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(); From 62fd897cf7c021b845ce7f4e66b5e04348204975 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 8 Apr 2019 23:09:12 +0200 Subject: [PATCH 24/37] Add support for XEP-0418: DNS Queries over XMPP (DoX) Fixes SMACK-862. --- documentation/extensions/index.md | 1 + settings.gradle | 1 + .../android/AndroidBase64Encoder.java | 5 + .../jivesoftware/smack/util/RandomUtil.java | 4 + .../smack/util/stringencoder/Base64.java | 13 +- .../smack/test/util/SmackTestSuite.java | 5 + .../smackx/dox/DnsOverXmppManager.java | 164 ++++++++++++++++++ .../smackx/dox/DnsOverXmppResolver.java | 27 +++ .../smackx/dox/element/DnsIq.java | 80 +++++++++ .../smackx/dox/element/package-info.java | 21 +++ .../jivesoftware/smackx/dox/package-info.java | 21 +++ .../smackx/dox/provider/DnsIqProvider.java | 38 ++++ .../smackx/dox/provider/package-info.java | 21 +++ .../experimental.providers | 7 + .../java7/Java7Base64Encoder.java | 8 + smack-repl/build.gradle | 1 + .../igniterealtime/smack/smackrepl/DoX.java | 78 +++++++++ smack-resolver-minidns-dox/build.gradle | 7 + .../minidns/DnsOverXmppMiniDnsResolver.java | 47 +++++ .../dox/resolver/minidns/package-info.java | 21 +++ 20 files changed, 564 insertions(+), 6 deletions(-) create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/dox/DnsOverXmppManager.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/dox/DnsOverXmppResolver.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/dox/element/DnsIq.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/dox/element/package-info.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/dox/package-info.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/dox/provider/DnsIqProvider.java create mode 100644 smack-experimental/src/main/java/org/jivesoftware/smackx/dox/provider/package-info.java create mode 100644 smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/DoX.java create mode 100644 smack-resolver-minidns-dox/build.gradle create mode 100644 smack-resolver-minidns-dox/src/main/java/org/jivesoftware/smackx/dox/resolver/minidns/DnsOverXmppMiniDnsResolver.java create mode 100644 smack-resolver-minidns-dox/src/main/java/org/jivesoftware/smackx/dox/resolver/minidns/package-info.java 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/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 42346274b..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 @@ -46,6 +46,11 @@ public final class AndroidBase64Encoder implements org.jivesoftware.smack.util.s return Base64.encodeToString(input, BASE64_ENCODER_FLAGS); } + @Override + public String encodeToStringWithoutPadding(byte[] input) { + return Base64.encodeToString(input, BASE64_ENCODER_FLAGS | Base64.NO_PADDING); + } + @Override public byte[] encode(byte[] input) { return Base64.encode(input, BASE64_ENCODER_FLAGS); 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 index 6a0c617ba..857557869 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/RandomUtil.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/RandomUtil.java @@ -43,4 +43,8 @@ public class RandomUtil { 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/stringencoder/Base64.java b/smack-core/src/main/java/org/jivesoftware/smack/util/stringencoder/Base64.java index bf19067d9..e98e84197 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 @@ -39,18 +39,17 @@ 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) { 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 base64encoder.encode(input); } @@ -103,6 +102,8 @@ public class Base64 { String encodeToString(byte[] input); + String encodeToStringWithoutPadding(byte[] input); + byte[] encode(byte[] input); } } 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 889a32483..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 @@ -39,6 +39,11 @@ public class SmackTestSuite { return Base64.getEncoder().encodeToString(input); } + @Override + public String encodeToStringWithoutPadding(byte[] input) { + return Base64.getEncoder().withoutPadding().encodeToString(input); + } + @Override public byte[] encode(byte[] input) { return Base64.getEncoder().encode(input); 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..092c1ce91 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/dox/DnsOverXmppManager.java @@ -0,0 +1,164 @@ +/** + * + * 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; + } + + 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/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-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 3ea688c92..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 @@ -27,10 +27,13 @@ public final class Java7Base64Encoder implements org.jivesoftware.smack.util.str private static final Java7Base64Encoder instance = new Java7Base64Encoder(); private final Base64.Encoder encoder; + private final Base64.Encoder encoderWithoutPadding; + private final Base64.Decoder decoder; private Java7Base64Encoder() { encoder = Base64.getEncoder(); + encoderWithoutPadding = encoder.withoutPadding(); decoder = Base64.getDecoder(); } @@ -48,6 +51,11 @@ public final class Java7Base64Encoder implements org.jivesoftware.smack.util.str return encoder.encodeToString(input); } + @Override + public String encodeToStringWithoutPadding(byte[] input) { + return encoderWithoutPadding.encodeToString(input); + } + @Override public byte[] encode(byte[] input) { return encoder.encode(input); 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..62c973774 --- /dev/null +++ b/smack-resolver-minidns-dox/build.gradle @@ -0,0 +1,7 @@ +description = """\ +DNS over XMPP (DoX) support using MiniDNS.""" + +dependencies { + compile project(path: ':smack-resolver-minidns') + compile project(path: ':smack-experimental') +} 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..e3a1f9d64 --- /dev/null +++ b/smack-resolver-minidns-dox/src/main/java/org/jivesoftware/smackx/dox/resolver/minidns/DnsOverXmppMiniDnsResolver.java @@ -0,0 +1,47 @@ +/** + * + * 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; + +public class DnsOverXmppMiniDnsResolver implements DnsOverXmppResolver { + + public static final DnsOverXmppMiniDnsResolver INSTANCE = new DnsOverXmppMiniDnsResolver(new DnsClient()); + + private final DnsClient dnsClient; + + public DnsOverXmppMiniDnsResolver(DnsClient dnsClient) { + this.dnsClient = dnsClient; + } + + @Override + public DnsMessage resolve(DnsMessage query) throws IOException { + Question question = query.getQuestion(); + + DnsQueryResult 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; From 474ea68d4abc5272a3809422047b924cd4feebc9 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Tue, 9 Apr 2019 09:33:06 +0200 Subject: [PATCH 25/37] Remove unused method in Smack's Base64 API --- .../org/jivesoftware/smack/util/stringencoder/Base64.java | 4 ---- 1 file changed, 4 deletions(-) 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 e98e84197..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 @@ -54,10 +54,6 @@ public class Base64 { return base64encoder.encode(input); } - public static final byte[] encode(byte[] input, int offset, int len) { - return encode(slice(input, offset, len)); - } - public static final String decodeToString(String string) { byte[] bytes = decode(string); try { From 9ad162af6ee656d933c4766a7bf8ec34f83dc6d8 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Tue, 9 Apr 2019 12:14:12 +0200 Subject: [PATCH 26/37] Use correct field in ServiceDiscoveryManager.getIdentities() Fixes SMACK-863. --- .../jivesoftware/smackx/disco/ServiceDiscoveryManager.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 eabc7773f..e2413b100 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. @@ -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); } From 0ec7e84cbc80576013bb2cd0b48ea6e4f8880d33 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Tue, 9 Apr 2019 14:26:15 +0200 Subject: [PATCH 27/37] Update link to XMPP Registry for Service Discovery Identities --- .../jivesoftware/smackx/disco/ServiceDiscoveryManager.java | 2 +- .../org/jivesoftware/smackx/disco/packet/DiscoverInfo.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) 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 e2413b100..06099413d 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 @@ -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. 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 2abda87fb..40cc92909 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. */ From fc45e1b9059df1fe7310390934797e218c3b1a6e Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Tue, 9 Apr 2019 17:02:56 +0200 Subject: [PATCH 28/37] DoX: Ensure that the response message ID matches the query ID --- .../java/org/jivesoftware/smackx/dox/DnsOverXmppManager.java | 5 +++++ 1 file changed, 5 insertions(+) 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 index 092c1ce91..634eab400 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/dox/DnsOverXmppManager.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/dox/DnsOverXmppManager.java @@ -97,6 +97,11 @@ public final class DnsOverXmppManager extends Manager { 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; From a1c88f1fad2475eebd9feb056ac0996a163ef769 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Tue, 9 Apr 2019 21:00:08 +0200 Subject: [PATCH 29/37] Add DNSSEC support to DnsOverXmppMiniDnsResolver --- .../minidns/DnsOverXmppMiniDnsResolver.java | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) 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 index e3a1f9d64..2d059b383 100644 --- 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 @@ -24,22 +24,30 @@ 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 class DnsOverXmppMiniDnsResolver implements DnsOverXmppResolver { +public final class DnsOverXmppMiniDnsResolver implements DnsOverXmppResolver { - public static final DnsOverXmppMiniDnsResolver INSTANCE = new DnsOverXmppMiniDnsResolver(new DnsClient()); + public static final DnsOverXmppMiniDnsResolver INSTANCE = new DnsOverXmppMiniDnsResolver(); - private final DnsClient dnsClient; + private final DnsClient dnsClient = new DnsClient(); + private final DnssecClient dnssecClient = new DnssecClient(); - public DnsOverXmppMiniDnsResolver(DnsClient dnsClient) { - this.dnsClient = dnsClient; + private DnsOverXmppMiniDnsResolver() { } @Override public DnsMessage resolve(DnsMessage query) throws IOException { Question question = query.getQuestion(); - DnsQueryResult dnsQueryResult = dnsClient.query(question); + final DnsQueryResult dnsQueryResult; + if (query.isDnssecOk()) { + DnssecQueryResult dnssecQueryResult = dnssecClient.queryDnssec(question); + dnsQueryResult = dnssecQueryResult.dnsQueryResult; + } else { + dnsQueryResult = dnsClient.query(question); + } return dnsQueryResult.response; } From 6076a9dfa54fdce20b82b8e55337341e148bb39c Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Wed, 10 Apr 2019 12:18:06 +0200 Subject: [PATCH 30/37] Introduce asyncGoLimited() which limits the number of threads created for asynchronous operations. Fixes SMACK-864. --- .../smack/AbstractXMPPConnection.java | 85 ++++++++++++++++++- .../jivesoftware/smack/AsyncButOrdered.java | 16 +++- .../smack/SmackConfiguration.java | 15 ++++ 3 files changed, 113 insertions(+), 3 deletions(-) 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 38e188ef9..fe82c07f6 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java @@ -26,6 +26,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; @@ -299,6 +300,17 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { protected static final AsyncButOrdered ASYNC_BUT_ORDERED = 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 */ @@ -1166,7 +1178,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; @@ -1216,7 +1228,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { } for (final StanzaListener listener : listenersToNotify) { - asyncGo(new Runnable() { + asyncGoLimited(new Runnable() { @Override public void run() { try { @@ -1773,6 +1785,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/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; + } } From 7fd0676ff4e9282d3bf7b76fbe9c9f306ef2d799 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Sun, 14 Apr 2019 21:29:20 +0200 Subject: [PATCH 31/37] Add unit test for smack-resolver-minidns-dox --- smack-resolver-minidns-dox/build.gradle | 1 + .../minidns/DnsOverXmppMiniDnsResolver.java | 10 +- .../DnsOverXmppMiniDnsResolverTest.java | 167 ++++++++++++++++++ 3 files changed, 174 insertions(+), 4 deletions(-) create mode 100644 smack-resolver-minidns-dox/src/test/java/org/jivesoftware/smackx/dox/resolver/minidns/DnsOverXmppMiniDnsResolverTest.java diff --git a/smack-resolver-minidns-dox/build.gradle b/smack-resolver-minidns-dox/build.gradle index 62c973774..3d070c5bd 100644 --- a/smack-resolver-minidns-dox/build.gradle +++ b/smack-resolver-minidns-dox/build.gradle @@ -4,4 +4,5 @@ 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 index 2d059b383..adf8e202b 100644 --- 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 @@ -29,12 +29,14 @@ import org.minidns.dnssec.DnssecQueryResult; public final class DnsOverXmppMiniDnsResolver implements DnsOverXmppResolver { - public static final DnsOverXmppMiniDnsResolver INSTANCE = new DnsOverXmppMiniDnsResolver(); + public static final DnsOverXmppMiniDnsResolver INSTANCE = new DnsOverXmppMiniDnsResolver(new DnsClient(), new DnssecClient()); - private final DnsClient dnsClient = new DnsClient(); - private final DnssecClient dnssecClient = new DnssecClient(); + private final DnsClient dnsClient; + private final DnssecClient dnssecClient; - private DnsOverXmppMiniDnsResolver() { + DnsOverXmppMiniDnsResolver(DnsClient dnsClient, DnssecClient dnssecClient) { + this.dnsClient = dnsClient; + this.dnssecClient = dnssecClient; } @Override 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; + } + + } +} From 488055948d2a67a2b576630d3e51dda107e620a3 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Sun, 14 Apr 2019 21:41:59 +0200 Subject: [PATCH 32/37] Add missing 'synchronized' keywords to Manager.getInstanceFor() Fixes SMACK-865. --- .../smackx/jingle_filetransfer/JingleFileTransferManager.java | 2 +- .../org/jivesoftware/smackx/reference/ReferenceManager.java | 2 +- .../jivesoftware/smackx/sid/StableUniqueStanzaIdManager.java | 2 +- .../java/org/jivesoftware/smackx/spoiler/SpoilerManager.java | 2 +- .../smackx/jingle/JingleTransportMethodManager.java | 2 +- .../jingle/transports/jingle_ibb/JingleIBBTransportManager.java | 2 +- .../jingle/transports/jingle_s5b/JingleS5BTransportManager.java | 2 +- .../java/org/jivesoftware/smackx/jingleold/JingleSession.java | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) 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/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-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 Date: Mon, 15 Apr 2019 09:46:43 +0200 Subject: [PATCH 33/37] Add checkstyle rule for 'synchronized' on Manager.getInstanceFor() --- config/checkstyle.xml | 13 +++++++++++++ .../org/jivesoftware/smackx/mam/MamManager.java | 2 ++ 2 files changed, 15 insertions(+) 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/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/MamManager.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/MamManager.java index ba3af51aa..2c72c8229 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); } From 99bf8316f509eba2c0758735e656da916ee257d0 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Tue, 16 Apr 2019 09:33:07 +0200 Subject: [PATCH 34/37] Add javadoc about callbacks to XMPPConnection --- .../jivesoftware/smack/XMPPConnection.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) 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 From d97fb126a1de2eb5df1560b3fc8e735cab32034e Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Tue, 16 Apr 2019 10:09:01 +0200 Subject: [PATCH 35/37] Add PubSubManager.getInstanceFor() just like all other Managers and deprecate PubSubManager.getInstance(). --- .../smackx/geoloc/GeoLocationManager.java | 2 +- .../jivesoftware/smackx/mood/MoodManager.java | 2 +- .../jivesoftware/smackx/pep/PepManager.java | 2 +- .../smackx/pubsub/PubSubManager.java | 32 +++++++++++++++++-- .../smackx/omemo/OmemoManagerSetupHelper.java | 2 +- .../smackx/pubsub/PubSubIntegrationTest.java | 2 +- .../smackx/omemo/OmemoService.java | 8 ++--- .../smackx/ox/util/OpenPgpPubSubUtil.java | 4 +-- 8 files changed, 40 insertions(+), 14 deletions(-) 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/mood/MoodManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/mood/MoodManager.java index 3c878a79b..d8cdd1678 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/mood/MoodManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/mood/MoodManager.java @@ -147,7 +147,7 @@ public final class MoodManager extends Manager { throws SmackException.NotLoggedInException, InterruptedException, PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException { if (pubSubManager == null) { - pubSubManager = PubSubManager.getInstance(getAuthenticatedConnectionOrThrow(), connection().getUser().asBareJid()); + pubSubManager = PubSubManager.getInstanceFor(getAuthenticatedConnectionOrThrow(), connection().getUser().asBareJid()); } LeafNode node = pubSubManager.getOrCreateLeafNode(MOOD_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-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..9226b3bb5 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 @@ -42,7 +42,7 @@ 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"); } 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 34f0c3dcc..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 @@ -556,7 +556,7 @@ public abstract class OmemoService(bundle)); } @@ -606,7 +606,7 @@ public abstract class OmemoService(deviceList)); } 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); From fc70484cf6f3f5a8dc95a6df92a139a106bdae67 Mon Sep 17 00:00:00 2001 From: Guus der Kinderen Date: Mon, 8 Apr 2019 14:40:37 +0200 Subject: [PATCH 36/37] More specific Pubsub integration tests. XEP-0060 prohibits publishing a request that contains an item to a node that is both 'notification-only' and 'transient' (section 7.1.3.6) In commit 8ed872ca639822aeff58eaaa673f953efb0ba6ee the existing pubsub publication test was modified (to resolve a different issue) to operate on a node that's both 'notification-only' and 'transient'. This resulted in a test that should return an error, even though the test implementation didn't expect one. This commit explicitly verifies that publishing an item to such a node causes an error to be returned. A new test is added that verifies that publishing an event notification does succeed. --- .../smackx/pubsub/PubSubIntegrationTest.java | 65 +++++++++++++++---- 1 file changed, 53 insertions(+), 12 deletions(-) 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 9226b3bb5..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; @@ -48,27 +52,64 @@ public class PubSubIntegrationTest extends AbstractSmackIntegrationTest { } } + /** + * 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); From 870756997faec1e1bfabfac0cd6c2395b04da873 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Wed, 24 Apr 2019 21:25:22 +0200 Subject: [PATCH 37/37] Improve exception message of XmppConnectionStressTest --- .../smack/XmppConnectionStressTest.java | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) 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;