-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
NettyOutboundConnectionHandler.java
212 lines (186 loc) · 10 KB
/
NettyOutboundConnectionHandler.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
/*
* Copyright (C) 2023 Ignite Realtime Foundation. All rights reserved.
*
* 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.openfire.nio;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.ssl.SslHandshakeCompletionEvent;
import org.dom4j.*;
import org.jivesoftware.openfire.Connection;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.net.RespondingServerStanzaHandler;
import org.jivesoftware.openfire.net.SASLAuthentication;
import org.jivesoftware.openfire.net.StanzaHandler;
import org.jivesoftware.openfire.server.ServerDialback;
import org.jivesoftware.openfire.session.ConnectionSettings;
import org.jivesoftware.openfire.session.DomainPair;
import org.jivesoftware.openfire.session.LocalOutgoingServerSession;
import org.jivesoftware.openfire.spi.ConnectionConfiguration;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
import java.security.cert.CertificateException;
import java.time.Duration;
/**
* Outbound (S2S) specific ConnectionHandler that knows which subclass of {@link StanzaHandler} should be created
* and how to build and configure a {@link NettyConnection}.
*
* @author Matthew Vivian
* @author Alex Gidman
*/
public class NettyOutboundConnectionHandler extends NettyConnectionHandler {
private static final Logger Log = LoggerFactory.getLogger(NettyOutboundConnectionHandler.class);
private final DomainPair domainPair;
private final int port;
public NettyOutboundConnectionHandler(ConnectionConfiguration configuration, DomainPair domainPair, int port) {
super(configuration);
this.domainPair = domainPair;
this.port = port;
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
if (sslInitDone) {
super.channelActive(ctx);
}
}
@Override
NettyConnection createNettyConnection(ChannelHandlerContext ctx) {
return new NettyConnection(ctx, null, configuration);
}
@Override
StanzaHandler createStanzaHandler(NettyConnection connection) {
return new RespondingServerStanzaHandler( XMPPServer.getInstance().getPacketRouter(), connection, domainPair );
}
private static boolean configRequiresStrictCertificateValidation() {
return JiveGlobals.getBooleanProperty(ConnectionSettings.Server.STRICT_CERTIFICATE_VALIDATION, true);
}
@Override
public Duration getMaxIdleTime() {
return ConnectionSettings.Server.IDLE_TIMEOUT_PROPERTY.getValue();
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
Log.trace("Adding NettyOutboundConnectionHandler");
super.handlerAdded(ctx);
}
/**
* Called when SSL Handshake has been completed.
*
* If successful, attempts authentication via SASL, or dialback dependent on configuration and certificate validity.
* If not successful, either attempts dialback on a plain un-encrypted connection, or throws an exception dependent
* on configuration.
*
* @param ctx ChannelHandlerContext for the Netty channel
* @param evt Event that has been triggered - this implementation specifically identifies SslHandshakeCompletionEvent
* @throws Exception
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (!sslInitDone && evt instanceof SslHandshakeCompletionEvent) {
SslHandshakeCompletionEvent event = (SslHandshakeCompletionEvent) evt;
RespondingServerStanzaHandler stanzaHandler = (RespondingServerStanzaHandler) ctx.channel().attr(NettyConnectionHandler.HANDLER).get();
if (event.isSuccess()) {
sslInitDone = true;
NettyConnection connection = ctx.channel().attr(NettyConnectionHandler.CONNECTION).get();
connection.setEncrypted(true);
Log.debug("TLS negotiation was successful. Connection encrypted. Proceeding with authentication...");
// If TLS cannot be used for authentication, it is permissible to use another authentication mechanism
// such as dialback. RFC 6120 does not explicitly allow this, as it does not take into account any other
// authentication mechanism other than TLS (it does mention dialback in an interoperability note. However,
// RFC 7590 Section 3.4 writes: "In particular for XMPP server-to-server interactions, it can be reasonable
// for XMPP server implementations to accept encrypted but unauthenticated connections when Server Dialback
// keys [XEP-0220] are used." In short: if Dialback is allowed, unauthenticated TLS is better than no TLS.
if (SASLAuthentication.verifyCertificates(connection.getPeerCertificates(), domainPair.getRemote(), true)) {
Log.debug("SASL authentication will be attempted");
Log.debug("Send the stream header and wait for response...");
sendNewStreamHeader(connection);
} else if (JiveGlobals.getBooleanProperty(ConnectionSettings.Server.STRICT_CERTIFICATE_VALIDATION, true)) {
Log.warn("Aborting attempt to create outgoing session as certificate verification failed, and strictCertificateValidation is enabled.");
abandonSession(stanzaHandler);
} else if (ServerDialback.isEnabled() || ServerDialback.isEnabledForSelfSigned()) {
Log.debug("Failed to verify certificates for SASL authentication. Will continue with dialback.");
sendNewStreamHeader(connection);
} else {
Log.warn("Unable to authenticate the connection: Failed to verify certificates for SASL authentication and dialback is not available.");
abandonSession(stanzaHandler);
}
ctx.fireChannelActive();
} else {
// SSL Handshake has failed
stanzaHandler.setSession(null);
if (isCertificateException(event)){
if (configRequiresStrictCertificateValidation()) {
Log.warn("Aborting attempt to create outgoing session to {} as TLS handshake failed, and strictCertificateValidation is enabled.", stanzaHandler.getRemoteDomain());
Log.debug("Error due to strictCertificateValidation", event.cause());
abandonSession(stanzaHandler);
} else {
Log.warn("TLS handshake failed due to a certificate validation error");
}
}
// fall back to dialback
if (ServerDialback.isEnabled() && connectionConfigDoesNotRequireTls()) {
Log.debug("Unable to create a new TLS session. Going to try connecting using server dialback as a fallback.");
// Use server dialback (pre XMPP 1.0) over a plain connection
final LocalOutgoingServerSession outgoingSession = new ServerDialback(domainPair).createOutgoingSession(port);
if (outgoingSession != null) {
Log.debug("Successfully created new session (using dialback as a fallback)!");
stanzaHandler.setSession(outgoingSession);
stanzaHandler.setSessionAuthenticated();
} else {
Log.warn("Unable to create a new session: Dialback (as a fallback) failed.");
}
} else {
Log.warn("Unable to create a new session: exhausted all options (not trying dialback as a fallback, as server dialback is disabled by configuration.");
}
stanzaHandler.setAttemptedAllAuthenticationMethods();
}
}
super.userEventTriggered(ctx, evt);
}
private static boolean isCertificateException(SslHandshakeCompletionEvent event) {
return event.cause().getCause() instanceof CertificateException;
}
private static void abandonSession(RespondingServerStanzaHandler stanzaHandler) {
stanzaHandler.setSession(null);
stanzaHandler.setAttemptedAllAuthenticationMethods();
}
private void sendNewStreamHeader(NettyConnection connection) {
final Element stream = DocumentHelper.createElement(QName.get("stream", "stream", "http://etherx.jabber.org/streams"));
final Document document = DocumentHelper.createDocument(stream);
document.setXMLEncoding(StandardCharsets.UTF_8.toString());
stream.add(Namespace.get("", "jabber:server"));
if (ServerDialback.isEnabled() || ServerDialback.isEnabledForSelfSigned()) {
stream.add(Namespace.get("db", "jabber:server:dialback"));
}
stream.addAttribute("from", domainPair.getLocal()); // OF-673
stream.addAttribute("to", domainPair.getRemote());
stream.addAttribute("version", "1.0");
connection.deliverRawText(StringUtils.asUnclosedStream(document));
}
private boolean connectionConfigDoesNotRequireTls() {
return this.configuration.getTlsPolicy() != Connection.TLSPolicy.required;
}
@Override
public String toString()
{
return "NettyOutboundConnectionHandler{" +
"domainPair=" + domainPair +
", port=" + port +
", sslInitDone=" + sslInitDone +
", configuration=" + configuration +
'}';
}
}