diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/Message.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/Message.java index a85f7c5b6..61e43552b 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/Message.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/Message.java @@ -590,7 +590,7 @@ public final class Message extends MessageOrPresence public XmlStringBuilder toXML(XmlEnvironment enclosingXmlEnvironment) { XmlStringBuilder xml = new XmlStringBuilder(this, enclosingXmlEnvironment); xml.rightAngleBracket(); - xml.escape(message); + xml.text(message); xml.closeElement(getElementName()); return xml; } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/StandardExtensionElement.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/StandardExtensionElement.java index fd7229b84..d78ce1845 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/StandardExtensionElement.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/StandardExtensionElement.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2019 Florian Schmaus. + * Copyright 2015-2020 Florian Schmaus. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -142,7 +142,9 @@ public final class StandardExtensionElement implements ExtensionElement { } xml.rightAngleBracket(); - xml.optEscape(text); + if (text != null) { + xml.text(text); + } if (elements != null) { for (Map.Entry entry : elements.entrySet()) { diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/ArrayBlockingQueueWithShutdown.java b/smack-core/src/main/java/org/jivesoftware/smack/util/ArrayBlockingQueueWithShutdown.java index 01938c43c..da61c4724 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/ArrayBlockingQueueWithShutdown.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/ArrayBlockingQueueWithShutdown.java @@ -293,6 +293,30 @@ public class ArrayBlockingQueueWithShutdown extends AbstractQueue implemen } } + /** + * Put if the queue has not been shutdown yet. + * + * @param e the element to put into the queue. + * @return true if the element has been put into the queue, false if the queue was shutdown. + * @throws InterruptedException if the calling thread was interrupted. + * @since 4.4 + */ + public boolean putIfNotShutdown(E e) throws InterruptedException { + checkNotNull(e); + lock.lockInterruptibly(); + + try { + if (isShutdown) { + return false; + } + + putInternal(e, true); + return true; + } finally { + lock.unlock(); + } + } + public void putAll(Collection elements) throws InterruptedException { checkNotNull(elements); lock.lockInterruptibly(); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/XmlStringBuilder.java b/smack-core/src/main/java/org/jivesoftware/smack/util/XmlStringBuilder.java index 50e27e072..1f0b24582 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/XmlStringBuilder.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/XmlStringBuilder.java @@ -456,6 +456,13 @@ public class XmlStringBuilder implements Appendable, CharSequence, Element { return this; } + public XmlStringBuilder text(CharSequence text) { + assert text != null; + CharSequence escapedText = StringUtils.escapeForXmlText(text); + sb.append(escapedText); + return this; + } + public XmlStringBuilder escape(String text) { assert text != null; sb.append(StringUtils.escapeForXml(text)); diff --git a/smack-core/src/test/java/org/jivesoftware/smack/packet/MessageTest.java b/smack-core/src/test/java/org/jivesoftware/smack/packet/MessageTest.java index 4c45be4fe..cba14e74f 100644 --- a/smack-core/src/test/java/org/jivesoftware/smack/packet/MessageTest.java +++ b/smack-core/src/test/java/org/jivesoftware/smack/packet/MessageTest.java @@ -206,4 +206,17 @@ public class MessageTest { assertXmlSimilar(control, message.toXML(StreamOpen.CLIENT_NAMESPACE).toString()); } + + /** + * Tests that only required characters are XML escaped in body. + * + * @see SMACK-892 + */ + @Test + public void escapeInBodyTest() { + String theFive = "\"'<>&"; + Message.Body body = new Message.Body(null, theFive); + + assertEquals("\"'<>&", body.toXML().toString()); + } } diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/json/packet/AbstractJsonPacketExtension.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/json/packet/AbstractJsonPacketExtension.java index 4d395debf..bcdc3f858 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/json/packet/AbstractJsonPacketExtension.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/json/packet/AbstractJsonPacketExtension.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2014 Florian Schmaus + * Copyright © 2014-2020 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,7 @@ public abstract class AbstractJsonPacketExtension implements ExtensionElement { public final XmlStringBuilder toXML(org.jivesoftware.smack.packet.XmlEnvironment enclosingNamespace) { XmlStringBuilder xml = new XmlStringBuilder(this); xml.rightAngleBracket(); - xml.append(json); + xml.text(json); xml.closeElement(this); return xml; } diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/chat_markers/MarkableExtensionTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/chat_markers/MarkableExtensionTest.java index 2b08e1c1b..ff4198006 100644 --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/chat_markers/MarkableExtensionTest.java +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/chat_markers/MarkableExtensionTest.java @@ -33,7 +33,7 @@ import org.jxmpp.jid.impl.JidCreate; public class MarkableExtensionTest { String markableMessageStanza = "" - + "My lord, dispatch; read o'er these articles." + + "My lord, dispatch; read o'er these articles." + "" + ""; String markableExtension = ""; diff --git a/smack-openpgp/build.gradle b/smack-openpgp/build.gradle index 23d15a366..e083a2df6 100644 --- a/smack-openpgp/build.gradle +++ b/smack-openpgp/build.gradle @@ -8,7 +8,7 @@ dependencies { compile project(':smack-extensions') compile project(':smack-experimental') - api 'org.pgpainless:pgpainless-core:0.0.1-alpha11' + api 'org.pgpainless:pgpainless-core:0.1.0' testImplementation "org.bouncycastle:bcprov-jdk15on:${bouncyCastleVersion}" 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 188e63186..8e3c8d282 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 @@ -1592,7 +1592,12 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { } private void sendSmAcknowledgementInternal() throws NotConnectedException, InterruptedException { - packetWriter.sendStreamElement(new AckAnswer(clientHandledStanzasCount)); + AckAnswer ackAnswer = new AckAnswer(clientHandledStanzasCount); + // Do net put an ack to the queue if it has already been shutdown. Some servers, like ejabberd, like to request + // an ack even after we have send a stream close (and hance the queue was shutdown). If we would not check here, + // then the ack would dangle around in the queue, and be send on the next re-connection attempt even before the + // stream open. + packetWriter.queue.putIfNotShutdown(ackAnswer); } /** diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppTcpTransportModule.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppTcpTransportModule.java index ca003ca8d..276784e84 100644 --- a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppTcpTransportModule.java +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppTcpTransportModule.java @@ -500,9 +500,9 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM pendingInputFilterData = false; } - // We have successfully read something. It is now possible that a filter is now also able to write - // additional data (for example SSLEngine). if (pendingWriteInterestAfterRead) { + // We have successfully read something and someone announced a write interest after a read. It is + // now possible that a filter is now also able to write additional data (for example SSLEngine). pendingWriteInterestAfterRead = false; newInterestedOps |= SelectionKey.OP_WRITE; } @@ -1048,12 +1048,15 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM } } + @SuppressWarnings("ReferenceEquality") @Override public ByteBuffer input(ByteBuffer inputData) throws SSLException { ByteBuffer accumulatedData; if (pendingInputData == null) { accumulatedData = inputData; } else { + assert pendingInputData != inputData; + int accumulatedDataBytes = pendingInputData.remaining() + inputData.remaining(); accumulatedData = ByteBuffer.allocate(accumulatedDataBytes); accumulatedData.put(pendingInputData) @@ -1084,18 +1087,25 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM SSLEngineResult.HandshakeStatus handshakeStatus = handleHandshakeStatus(result); switch (handshakeStatus) { case NEED_TASK: - // A delegated task is asynchronously running. Signal that there is pending input data and - // cycle again through the smack reactor. + // A delegated task is asynchronously running. Take care of the remaining accumulatedData. addAsPendingInputData(accumulatedData); - break; + // Return here, as the async task created by handleHandshakeStatus will continue calling the + // cannelSelectedCallback. + return null; case NEED_UNWRAP: continue; case NEED_WRAP: // NEED_WRAP means that the SSLEngine needs to send data, probably without consuming data. // We exploit here the fact that the channelSelectedCallback is single threaded and that the // input processing is after the output processing. + addAsPendingInputData(accumulatedData); + // Note that it is ok that we the provided argument for pending input filter data to channel + // selected callback is false, as setPendingInputFilterData() will have set the internal state + // boolean accordingly. connectionInternal.asyncGo(() -> callChannelSelectedCallback(false, true)); - break; + // Do not break here, but instead return and let the asynchronously invoked + // callChannelSelectedCallback() do its work. + return null; default: break; } @@ -1127,8 +1137,15 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM } private void addAsPendingInputData(ByteBuffer byteBuffer) { + // Note that we can not simply write + // pendingInputData = byteBuffer; + // we have to copy the provided byte buffer, because it is possible that this byteBuffer is re-used by some + // higher layer. That is, here 'byteBuffer' is typically 'incomingBuffer', which is a direct buffer only + // allocated once per connection for performance reasons and hence re-used for read() calls. pendingInputData = ByteBuffer.allocate(byteBuffer.remaining()); pendingInputData.put(byteBuffer).flip(); + + pendingInputFilterData = pendingInputData.hasRemaining(); } private SSLEngineResult.HandshakeStatus handleHandshakeStatus(SSLEngineResult sslEngineResult) {