Skip to content

Commit 9e8547f

Browse files
committed
Intermediate Commit (2026-08-22 18:29)
1 parent b26ecf1 commit 9e8547f

9 files changed

Lines changed: 302 additions & 16 deletions

File tree

‎smack-core/src/main/java/org/jivesoftware/smack/bind2/Bind2Module.java‎

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import org.jivesoftware.smack.fsm.State;
2626
import org.jivesoftware.smack.fsm.StateDescriptor;
2727
import org.jivesoftware.smack.fsm.StateTransitionResult;
28-
import org.jivesoftware.smack.sasl.packet.Sasl2Feature;
2928
import org.jivesoftware.smack.sasl.sasl2.Sasl2Authentication.Sasl2AuthenticationResult;
3029
import org.jivesoftware.smack.sasl.sasl2.Sasl2Module;
3130
import org.jivesoftware.smack.sasl.sasl2.Sasl2Module.Sasl2StateDescriptor;
@@ -65,23 +64,28 @@ protected Bind2Module.Bind2State constructState(ModularXmppClientToServerConnect
6564

6665
private static final class Bind2State extends State {
6766

68-
private Sasl2Feature sasl2Feature;
69-
7067
private Bind2State(Bind2StateDescriptor bind2StateDescriptor,
7168
ModularXmppClientToServerConnectionInternal connectionInternal) {
7269
super(bind2StateDescriptor, connectionInternal);
7370
}
7471

7572
@Override
7673
public StateTransitionResult.TransitionImpossible isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) {
77-
// sasl2Feature must always be non-null, because we can only reach the bind2 state via sasl2
78-
sasl2Feature = connectionInternal.connection.getFeature(Sasl2Feature.class);
74+
Sasl2Module sasl2Module = connectionInternal.connection.getConnectionModuleFor(Sasl2ModuleDescriptor.class);
75+
if (sasl2Module == null) {
76+
return new StateTransitionResult.TransitionImpossibleReason("SASL 2 module not found on connection");
77+
}
78+
79+
Sasl2AuthenticationResult result = sasl2Module.getSasl2AuthenticationResult();
80+
if (result == null) {
81+
return new StateTransitionResult.TransitionImpossibleReason("SASL 2 authentication has not yielded a result");
82+
}
7983

80-
if (sasl2Feature != null && sasl2Feature.hasBind2()) {
81-
return null;
84+
if (!result.isResourceBound()) {
85+
return new StateTransitionResult.TransitionImpossibleReason("Bind 2 was not performed during SASL 2 authentication");
8286
}
8387

84-
return new StateTransitionResult.TransitionImpossibleReason("Bind 2 not announced by service");
88+
return null;
8589
}
8690

8791
@Override
@@ -112,7 +116,6 @@ public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext
112116

113117
@Override
114118
public void resetState() {
115-
sasl2Feature = null;
116119
Bind2Module bind2Module = connectionInternal.connection.getConnectionModuleFor(Bind2ModuleDescriptor.class);
117120
if (bind2Module != null) {
118121
bind2Module.bind2SuccessResult = null;

‎smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptorGraph.java‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -215,9 +215,17 @@ public static GraphVertex<StateDescriptor> constructStateDescriptorGraph(
215215
inferredForwardEdges.put(predecessor, backwardsEdge);
216216
}
217217
}
218-
// Ensure that the initial node has their successors inferred.
219-
for (Class<? extends StateDescriptor> inferredSuccessorOfInitialStateDescriptor : inferredForwardEdges.getAll(initialStatedescriptorClass)) {
220-
initialNode.getElement().addSuccessor(inferredSuccessorOfInitialStateDescriptor);
218+
// Ensure that all existing vertices (initial node and backward edge nodes) have their successors inferred.
219+
for (var predecessorClass : inferredForwardEdges.keySet()) {
220+
var predecessorVertex = graphVertexes.get(predecessorClass);
221+
// XXX: Null check required?
222+
if (predecessorVertex == null) {
223+
continue;
224+
}
225+
226+
for (var inferredSuccessor : inferredForwardEdges.getAll(predecessorClass)) {
227+
predecessorVertex.getElement().addSuccessor(inferredSuccessor);
228+
}
221229
}
222230

223231
HandleStateDescriptorGraphVertexContext context = new HandleStateDescriptorGraphVertexContext(graphVertexes, inferredForwardEdges);

‎smack-core/src/main/java/org/jivesoftware/smack/sasl/sasl2/Sasl2Module.java‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import org.jivesoftware.smack.SmackException;
2222
import org.jivesoftware.smack.XMPPException;
2323
import org.jivesoftware.smack.bind2.Bind2Module;
24-
import org.jivesoftware.smack.bind2.Bind2Module.Bind2StateDescriptor;
2524
import org.jivesoftware.smack.bind2.Bind2ModuleDescriptor;
2625
import org.jivesoftware.smack.bind2.element.Bind2Elements;
2726
import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection.AuthenticatedButUnboundStateDescriptor;
@@ -55,7 +54,6 @@ private Sasl2StateDescriptor() {
5554
super(Sasl2State.class, 388);
5655

5756
addPredeccessor(ConnectedButUnauthenticatedStateDescriptor.class);
58-
addSuccessor(Bind2StateDescriptor.class);
5957
addSuccessor(AuthenticatedButUnboundStateDescriptor.class);
6058
declarePrecedenceOver(SaslAuthenticationStateDescriptor.class);
6159
}

‎smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackSpecificLowLevelIntegrationTest.java‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
*
3-
* Copyright 2018-2021 Florian Schmaus
3+
* Copyright 2018-2026 Florian Schmaus
44
*
55
* Licensed under the Apache License, Version 2.0 (the "License");
66
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
1717
package org.igniterealtime.smack.inttest;
1818

1919
import java.util.ArrayList;
20+
import java.util.Collection;
2021
import java.util.List;
2122

2223
import org.jivesoftware.smack.AbstractXMPPConnection;
@@ -51,6 +52,18 @@ protected C getSpecificUnconnectedConnection() throws NoResponseException, XMPPE
5152
return environment.connectionManager.constructConnection(connectionDescriptor);
5253
}
5354

55+
protected C getSpecificUnconnectedConnection(ConnectionConfigurationBuilderApplier customConnectionConfigurationApplier)
56+
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
57+
var appliers = new ArrayList<>(List.of(customConnectionConfigurationApplier));
58+
return getSpecificUnconnectedConnection(appliers);
59+
}
60+
61+
protected C getSpecificUnconnectedConnection(
62+
Collection<ConnectionConfigurationBuilderApplier> customConnectionConfigurationAppliers)
63+
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
64+
return environment.connectionManager.constructConnection(connectionDescriptor, customConnectionConfigurationAppliers);
65+
}
66+
5467
protected List<C> getSpecificUnconnectedConnections(int count)
5568
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
5669
List<C> connections = new ArrayList<>(count);

‎smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionManager.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,7 @@ <C extends AbstractXMPPConnection> C constructConnection(
443443
return constructConnection(connectionDescriptor, null);
444444
}
445445

446-
private <C extends AbstractXMPPConnection> C constructConnection(
446+
<C extends AbstractXMPPConnection> C constructConnection(
447447
XmppConnectionDescriptor<C, ? extends ConnectionConfiguration, ? extends ConnectionConfiguration.Builder<?, ?>> connectionDescriptor,
448448
Collection<ConnectionConfigurationBuilderApplier> customConnectionConfigurationAppliers)
449449
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
*
3+
* Copyright 2026 Florian Schmaus
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.jivesoftware.smack.bind2;
18+
19+
import static org.junit.jupiter.api.Assertions.assertEquals;
20+
import static org.junit.jupiter.api.Assertions.assertNotNull;
21+
import static org.junit.jupiter.api.Assertions.assertNull;
22+
import static org.junit.jupiter.api.Assertions.assertTrue;
23+
24+
import org.jivesoftware.smack.bind2.Bind2Module.Bind2SuccessResult;
25+
import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection;
26+
import org.jivesoftware.smack.sasl.packet.Sasl2Feature;
27+
import org.jivesoftware.smack.util.StringUtils;
28+
29+
import org.igniterealtime.smack.inttest.AbstractSmackSpecificLowLevelIntegrationTest;
30+
import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment;
31+
import org.igniterealtime.smack.inttest.TestNotPossibleException;
32+
import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest;
33+
import org.igniterealtime.smack.inttest.annotations.SpecificationReference;
34+
35+
import org.jxmpp.jid.parts.Resourcepart;
36+
37+
@SpecificationReference(document = "XEP-0386", version = "1.1.0")
38+
public class Bind2IntegrationTest extends AbstractSmackSpecificLowLevelIntegrationTest<ModularXmppClientToServerConnection> {
39+
40+
@SuppressWarnings("this-escape")
41+
public Bind2IntegrationTest(SmackIntegrationTestEnvironment environment) throws Exception {
42+
super(environment, ModularXmppClientToServerConnection.class);
43+
ModularXmppClientToServerConnection connection = getSpecificUnconnectedConnection();
44+
try {
45+
connection.connect();
46+
var sasl2Feature = connection.getFeature(Sasl2Feature.class);
47+
if (sasl2Feature == null || !sasl2Feature.hasBind2()) {
48+
throw new TestNotPossibleException("XEP-0386: Bind 2 (Bind2) not supported by service");
49+
}
50+
} finally {
51+
connection.disconnect();
52+
}
53+
}
54+
55+
@SmackIntegrationTest
56+
public void testBind2ResourceBinding() throws Exception {
57+
ModularXmppClientToServerConnection connection = getSpecificUnconnectedConnection();
58+
try {
59+
connection.connect();
60+
Resourcepart requestedResource = Resourcepart.from("bind2-test-" + StringUtils.insecureRandomString(6));
61+
connection.login(connection.getConfiguration().getUsername(),
62+
connection.getConfiguration().getPassword(),
63+
requestedResource);
64+
65+
assertTrue(connection.isAuthenticated(), "Expected connection to be authenticated");
66+
assertNotNull(connection.getUser(), "Expected connection to have a bound user JID");
67+
assertNotNull(connection.getUser().getResourcepart(), "Expected connection user JID to have a resourcepart");
68+
69+
Bind2Module bind2Module = connection.getConnectionModuleFor(Bind2ModuleDescriptor.class);
70+
assertNotNull(bind2Module, "Bind2Module should be present on connection");
71+
72+
Bind2SuccessResult bind2SuccessResult = bind2Module.getBind2SuccessResult();
73+
assertNotNull(bind2SuccessResult, "Bind2SuccessResult should not be null");
74+
assertNotNull(bind2SuccessResult.getBound(), "Bound element in Bind2SuccessResult should not be null");
75+
assertEquals(connection.getUser().getResourcepart(), bind2SuccessResult.getBoundResource());
76+
assertEquals(requestedResource, bind2SuccessResult.getRequestedResource());
77+
} finally {
78+
connection.disconnect();
79+
}
80+
}
81+
82+
@SmackIntegrationTest
83+
public void testBind2ServerGeneratedResource() throws Exception {
84+
ModularXmppClientToServerConnection connection = getSpecificUnconnectedConnection();
85+
try {
86+
connection.connect();
87+
connection.login(connection.getConfiguration().getUsername(),
88+
connection.getConfiguration().getPassword(),
89+
null);
90+
91+
assertTrue(connection.isAuthenticated(), "Expected connection to be authenticated");
92+
assertNotNull(connection.getUser(), "Expected connection to have a bound user JID");
93+
assertNotNull(connection.getUser().getResourcepart(), "Expected bound resourcepart to be present");
94+
95+
Bind2Module bind2Module = connection.getConnectionModuleFor(Bind2ModuleDescriptor.class);
96+
assertNotNull(bind2Module, "Bind2Module should be present on connection");
97+
98+
Bind2SuccessResult bind2SuccessResult = bind2Module.getBind2SuccessResult();
99+
assertNotNull(bind2SuccessResult, "Bind2SuccessResult should not be null");
100+
assertNotNull(bind2SuccessResult.getBound(), "Bound element in Bind2SuccessResult should not be null");
101+
assertEquals(connection.getUser().getResourcepart(), bind2SuccessResult.getBoundResource());
102+
assertNull(bind2SuccessResult.getRequestedResource(), "Expected requestedResource to be null for server-generated resource");
103+
} finally {
104+
connection.disconnect();
105+
}
106+
}
107+
108+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
*
3+
* Copyright 2026 Florian Schmaus
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
/**
19+
* Integration tests for Bind 2 (Bind2, XEP-0386).
20+
*/
21+
package org.jivesoftware.smack.bind2;
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
*
3+
* Copyright 2026 Florian Schmaus
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.jivesoftware.smack.sasl.sasl2;
18+
19+
import static org.junit.jupiter.api.Assertions.assertEquals;
20+
import static org.junit.jupiter.api.Assertions.assertFalse;
21+
import static org.junit.jupiter.api.Assertions.assertNotNull;
22+
import static org.junit.jupiter.api.Assertions.assertThrows;
23+
import static org.junit.jupiter.api.Assertions.assertTrue;
24+
25+
import java.io.IOException;
26+
27+
import org.jivesoftware.smack.SmackException;
28+
import org.jivesoftware.smack.SmackException.NoResponseException;
29+
import org.jivesoftware.smack.SmackException.NotConnectedException;
30+
import org.jivesoftware.smack.XMPPException;
31+
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
32+
import org.jivesoftware.smack.bind2.Bind2ModuleDescriptor;
33+
import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection;
34+
import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionConfiguration;
35+
import org.jivesoftware.smack.sasl.SASLError;
36+
import org.jivesoftware.smack.sasl.SASLErrorException;
37+
import org.jivesoftware.smack.sasl.packet.Sasl2Feature;
38+
import org.jivesoftware.smack.sasl.sasl2.Sasl2Authentication.Sasl2AuthenticationResult;
39+
import org.jivesoftware.smack.util.StringUtils;
40+
41+
import org.igniterealtime.smack.inttest.AbstractSmackSpecificLowLevelIntegrationTest;
42+
import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment;
43+
import org.igniterealtime.smack.inttest.TestNotPossibleException;
44+
import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest;
45+
import org.igniterealtime.smack.inttest.annotations.SpecificationReference;
46+
47+
@SpecificationReference(document = "XEP-0388", version = "1.0.4")
48+
public class Sasl2IntegrationTest extends AbstractSmackSpecificLowLevelIntegrationTest<ModularXmppClientToServerConnection> {
49+
50+
@SuppressWarnings("this-escape")
51+
public Sasl2IntegrationTest(SmackIntegrationTestEnvironment environment) throws Exception {
52+
super(environment, ModularXmppClientToServerConnection.class);
53+
ModularXmppClientToServerConnection connection = getSpecificUnconnectedConnection();
54+
try {
55+
connection.connect();
56+
var sasl2Feature = connection.getFeature(Sasl2Feature.class);
57+
if (sasl2Feature == null) {
58+
throw new TestNotPossibleException("XEP-0388: Extensible SASL Profile (SASL2) not supported by service");
59+
}
60+
} finally {
61+
connection.disconnect();
62+
}
63+
}
64+
65+
private ModularXmppClientToServerConnection getUnconnectedSasl2OnlyConnection()
66+
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
67+
// TODO: Also remove RFC SASL.
68+
return getSpecificUnconnectedConnection(
69+
builder -> ((ModularXmppClientToServerConnectionConfiguration.Builder) builder).removeModule(
70+
Bind2ModuleDescriptor.class));
71+
}
72+
73+
@SmackIntegrationTest
74+
public void testSasl2Authentication() throws SmackException, IOException, XMPPException, InterruptedException {
75+
ModularXmppClientToServerConnection connection = getUnconnectedSasl2OnlyConnection();
76+
try {
77+
connection.connect().login();
78+
79+
assertTrue(connection.isAuthenticated(), "Expected connection to be authenticated");
80+
assertNotNull(connection.getUser(), "Expected connection to have a bound user JID");
81+
assertNotNull(connection.getUser().getResourcepart(), "Expected connection user JID to have a resourcepart");
82+
83+
Sasl2Module sasl2Module = connection.getConnectionModuleFor(Sasl2ModuleDescriptor.class);
84+
assertNotNull(sasl2Module, "Sasl2Module should be present on connection");
85+
86+
Sasl2AuthenticationResult result = sasl2Module.getSasl2AuthenticationResult();
87+
assertNotNull(result, "Sasl2AuthenticationResult should be set after successful SASL2 authentication");
88+
assertNotNull(result.getSuccessNonza(), "Sasl2 Success nonza should not be null");
89+
assertNotNull(result.getUsedSaslMechanism(), "Used SASL mechanism should not be null");
90+
assertFalse(result.isResourceBound(), "Result should not be resource bound via SASL2 when Bind2 is disabled");
91+
} finally {
92+
connection.disconnect();
93+
}
94+
}
95+
96+
@SmackIntegrationTest
97+
public void testSasl2AuthenticationFailure() throws SmackException, IOException, XMPPException, InterruptedException {
98+
ModularXmppClientToServerConnection connection = getUnconnectedSasl2OnlyConnection();
99+
try {
100+
connection.connect();
101+
var username = connection.getConfiguration().getUsername();
102+
var invalidPassword = "invalid-password-" + StringUtils.insecureRandomString(16);
103+
104+
SASLErrorException errorException = assertThrows(SASLErrorException.class,
105+
() -> connection.login(username, invalidPassword));
106+
107+
assertNotNull(errorException.getSasl2Failure(), "SASL2 Failure element should be present");
108+
assertEquals(SASLError.not_authorized, errorException.getSasl2Failure().getSASLError());
109+
assertFalse(connection.isAuthenticated(), "Connection should not be authenticated after failed login");
110+
} finally {
111+
connection.disconnect();
112+
}
113+
}
114+
}

0 commit comments

Comments
 (0)