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 83efb26121..3855972292 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java @@ -82,6 +82,7 @@ import org.jivesoftware.smack.packet.ErrorIQ; import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Limits; import org.jivesoftware.smack.packet.Mechanisms; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.MessageBuilder; @@ -1902,6 +1903,9 @@ protected final void parseFeatures(XmlPullParser parser) throws XmlPullParserExc case Compress.Feature.ELEMENT: streamFeature = PacketParserUtils.parseCompressionFeature(parser); break; + case Limits.ELEMENT: + streamFeature = PacketParserUtils.parseLimitsFeature(parser); + break; default: ExtensionElementProvider provider = ProviderManager.getStreamFeatureProvider(name, namespace); if (provider != null) { diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/Limits.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/Limits.java new file mode 100644 index 0000000000..20d7109d40 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/Limits.java @@ -0,0 +1,90 @@ +/* + * + * Copyright © 2014-2026 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.packet; + +import javax.xml.namespace.QName; + +import org.jivesoftware.smack.util.XmlStringBuilder; + +/** + * Limits feature. + * For any XMPP stream, there is an "initiating entity" (a client or server) and a "responding entity" that they are connecting to. + * The responding entity advertises its limits in the <stream:features/> element that it sends at the start of the stream. + * XEP-0478: Stream Limits Advertisement + */ +public class Limits implements ExtensionElement { + public static final String ELEMENT = "limits"; + public static final String NAMESPACE = "urn:xmpp:stream-limits:0"; + public static final QName QNAME = new QName(NAMESPACE, ELEMENT); + + /** + * The maximum size of any first-level stream elements (including stanzas), + * in bytes the announcing entity is willing to accept. + * Guidance on acceptable limits is provided in RFC 6120 section 13.12. + * If the responding entity is unable to determine its limits, this child can be absent. + * Element: <max-bytes/>. Type UnsignedInt. + */ + public final int maxBytes; + + /** + * The number of seconds without any traffic from the initiating entity after which + * the server may consider the stream idle, and either perform liveness checks + * (using e.g. Stream Management (XEP-0198) or XMPP Ping (XEP-0199)) or terminate the stream. + * Guidance on handling idle connections is provided in RFC 6120 section 4.6. + * If the responding entity is unable to determine its limits, this child can be absent. + * Element: <idle-seconds/>. Type UnsignedInt. + */ + public final int idleSeconds; + + public Limits(int maxBytes, int idleSeconds) { + this.maxBytes = maxBytes; + this.idleSeconds = idleSeconds; + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + public int getMaxBytes() { + return maxBytes; + } + + public int getIdleSeconds() { + return idleSeconds; + } + + @Override + public XmlStringBuilder toXML(XmlEnvironment enclosingNamespace) { + XmlStringBuilder xml = new XmlStringBuilder(this); + xml.rightAngleBracket(); + if (maxBytes != 0) { + xml.element("max-bytes", String.valueOf(maxBytes)); + } + if (idleSeconds != 0) { + xml.element("idle-seconds", String.valueOf(idleSeconds)); + } + xml.closeElement(this); + return xml; + } + +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/PacketParserUtils.java b/smack-core/src/main/java/org/jivesoftware/smack/util/PacketParserUtils.java index fceaff611d..11c0a1facc 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/PacketParserUtils.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/PacketParserUtils.java @@ -36,6 +36,7 @@ import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.IqData; +import org.jivesoftware.smack.packet.Limits; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.MessageBuilder; import org.jivesoftware.smack.packet.Presence; @@ -894,6 +895,57 @@ public static Session.Feature parseSessionFeature(XmlPullParser parser) throws X return new Session.Feature(optional); } + + /** + * Parse the stream limits from XEP-0478: Stream Limits Advertisement. + * + * @param parser the XML parser, positioned at the start of the "limits" stanza. + * @return a collection of Stings with the mechanisms included in the mechanisms stanza. + * @throws IOException if an I/O error occurred. + * @throws XmlPullParserException if an error in the XML parser occurred. + */ + public static Limits parseLimitsFeature(XmlPullParser parser) + throws XmlPullParserException, IOException { + ParserUtils.assertAtStartTag(parser); + final int initialDepth = parser.getDepth(); + int maxBytes = 0; + int idleSeconds = 0; + outerloop: while (true) { + XmlPullParser.Event event = parser.next(); + switch (event) { + case START_ELEMENT: + String name = parser.getName(); + switch (name) { + case "max-bytes": + try { + maxBytes = Integer.parseUnsignedInt(parser.nextText()); + } catch (NumberFormatException ignored) { + // ignore + } + break; + case "idle-seconds": + try { + idleSeconds = Integer.parseUnsignedInt(parser.nextText()); + } catch (NumberFormatException ignored) { + // ignore + } + break; + } + break; + case END_ELEMENT: + if (parser.getDepth() == initialDepth) { + break outerloop; + } + break; + default: + // Catch all for incomplete switch (MissingCasesInEnumSwitch) statement. + break; + } + } + + return new Limits(maxBytes, idleSeconds); + } + public static void addExtensionElement(StanzaBuilder stanzaBuilder, XmlPullParser parser, XmlEnvironment outerXmlEnvironment, JxmppContext jxmppContext) throws XmlPullParserException, IOException, SmackParsingException { ParserUtils.assertAtStartTag(parser); diff --git a/smack-core/src/test/java/org/jivesoftware/smack/AbstractXMPPConnectionTest.java b/smack-core/src/test/java/org/jivesoftware/smack/AbstractXMPPConnectionTest.java new file mode 100644 index 0000000000..21717e99b7 --- /dev/null +++ b/smack-core/src/test/java/org/jivesoftware/smack/AbstractXMPPConnectionTest.java @@ -0,0 +1,95 @@ +/* + * + * Copyright 2018-2026 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; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; +import static java.util.Arrays.asList; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import org.jivesoftware.smack.packet.Bind; +import org.jivesoftware.smack.packet.Limits; +import org.jivesoftware.smack.packet.Mechanisms; +import org.jivesoftware.smack.packet.Session; +import org.jivesoftware.smack.parsing.SmackParsingException; +import org.jivesoftware.smack.test.util.SmackTestSuite; +import org.jivesoftware.smack.util.PacketParserUtils; +import org.jivesoftware.smack.xml.XmlPullParser; +import org.jivesoftware.smack.xml.XmlPullParserException; + +import org.junit.Test; + +public class AbstractXMPPConnectionTest extends SmackTestSuite { + private final DummyConnection connection = new DummyConnection(); + + @Test + public void parseFeatures() throws XmlPullParserException, IOException, SmackParsingException { + String featureStr = "" + + "" + + " OAUTHBEARER" + + " SCRAM-SHA-1" + + " PLAIN" + + " SCRAM-SHA-1-PLUS" + + "" + + "" + + " " + + "" + + "" + + " " + + "" + + "" + + " " + + "" + + "" + + " 262144" + + " 840" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + InputStream input = new ByteArrayInputStream(featureStr.getBytes(StandardCharsets.UTF_8)); + XmlPullParser parser = PacketParserUtils.getParserFor(input); + connection.parseFeatures(parser); + + assertTrue(connection.hasFeature("mechanisms", "urn:ietf:params:xml:ns:xmpp-sasl")); + Mechanisms featMechanisms = connection.getFeature(Mechanisms.class); + assertEquals(asList("OAUTHBEARER", "SCRAM-SHA-1", "PLAIN", "SCRAM-SHA-1-PLUS"), featMechanisms.getMechanisms()); + + assertTrue(connection.hasFeature("bind", "urn:ietf:params:xml:ns:xmpp-bind")); + Bind.Feature featBind = connection.getFeature(Bind.Feature.class); + assertEquals(Bind.Feature.INSTANCE, featBind); + + assertTrue(connection.hasFeature("session", "urn:ietf:params:xml:ns:xmpp-session")); + Session.Feature featSession = connection.getFeature(Session.Feature.class); + assertTrue(featSession.isOptional()); + + assertTrue(connection.hasFeature("limits", "urn:xmpp:stream-limits:0")); + Limits featLimits = connection.getFeature(Limits.class); + assertEquals(262144, featLimits.getMaxBytes()); + assertEquals(840, featLimits.getIdleSeconds()); + } +}