Commit 938c0f26 authored by Guus der Kinderen's avatar Guus der Kinderen

OF-946: Merge SSLConfig.Type with Purpose

parent 97f7cf3f
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -63,14 +63,14 @@ public class SSLProtocolSocketFactory implements SecureProtocolSocketFactory {
private SSLContext createSSLContext(String host) {
try {
final SSLContext context = SSLConfig.getSSLContext( SSLConfig.Type.ADMIN );
final SSLContext context = SSLConfig.getSSLContext( Purpose.ADMIN );
context.init(
null,
new TrustManager[] {
new ClearspaceX509TrustManager(
host,
manager.getProperties(),
SSLConfig.getStore( Purpose.ADMINISTRATIVE_TRUSTSTORE ) )
SSLConfig.getTrustStore( Purpose.ADMIN ) )
},
null);
return context;
......
/**
* $Revision: 3034 $
* $Date: 2005-11-04 21:02:33 -0300 (Fri, 04 Nov 2005) $
*
* Copyright (C) 2004-2008 Jive Software. 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.container;
import java.io.File;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import org.apache.tomcat.InstanceManager;
import org.apache.tomcat.SimpleInstanceManager;
import org.eclipse.jetty.apache.jsp.JettyJasperInitializer;
import org.eclipse.jetty.plus.annotation.ContainerInitializer;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.spdy.server.http.HTTPSPDYServerConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.webapp.WebAppContext;
import org.jivesoftware.openfire.JMXManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.keystore.IdentityStoreConfig;
import org.jivesoftware.openfire.keystore.Purpose;
import org.jivesoftware.openfire.keystore.CertificateStoreConfig;
import org.jivesoftware.openfire.net.SSLConfig;
import org.jivesoftware.util.CertificateEventListener;
import org.jivesoftware.util.CertificateManager;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The admin console plugin. It starts a Jetty instance on the configured
* port and loads the admin console web application.
*
* @author Matt Tucker
*/
public class AdminConsolePlugin implements Plugin {
private static final Logger Log = LoggerFactory.getLogger(AdminConsolePlugin.class);
/**
* Random secret used by JVM to allow SSO. Only other cluster nodes can use this secret
* as a way to integrate the admin consoles of each cluster node.
*/
public final static String secret = StringUtils.randomString(64);
private int adminPort;
private int adminSecurePort;
private Server adminServer;
private ContextHandlerCollection contexts;
private CertificateEventListener certificateListener;
private boolean restartNeeded = false;
private boolean sslEnabled = false;
private File pluginDir;
/**
* Create a Jetty module.
*/
public AdminConsolePlugin() {
contexts = new ContextHandlerCollection();
// JSP 2.0 uses commons-logging, so also override that implementation.
System.setProperty("org.apache.commons.logging.LogFactory", "org.jivesoftware.util.log.util.CommonsLogFactory");
}
/**
* Starts the Jetty instance.
*/
public void startup() {
restartNeeded = false;
// Add listener for certificate events
certificateListener = new CertificateListener();
CertificateManager.addListener(certificateListener);
// the number of threads allocated to each connector/port
int serverThreads = JiveGlobals.getXMLProperty("adminConsole.serverThreads", 2);
adminPort = JiveGlobals.getXMLProperty("adminConsole.port", 9090);
adminSecurePort = JiveGlobals.getXMLProperty("adminConsole.securePort", 9091);
final QueuedThreadPool tp = new QueuedThreadPool();
tp.setName("Jetty-QTP-AdminConsole");
adminServer = new Server(tp);
if (JMXManager.isEnabled()) {
JMXManager jmx = JMXManager.getInstance();
adminServer.addBean(jmx.getContainer());
}
// Create connector for http traffic if it's enabled.
if (adminPort > 0) {
final HttpConfiguration httpConfig = new HttpConfiguration();
// Do not send Jetty info in HTTP headers
httpConfig.setSendServerVersion( false );
final ServerConnector httpConnector = new ServerConnector(adminServer, null, null, null, -1, serverThreads, new HttpConnectionFactory(httpConfig));
// Listen on a specific network interface if it has been set.
String bindInterface = getBindInterface();
httpConnector.setHost(bindInterface);
httpConnector.setPort(adminPort);
adminServer.addConnector(httpConnector);
}
// Create a connector for https traffic if it's enabled.
sslEnabled = false;
try {
final IdentityStoreConfig identityStoreConfig = (IdentityStoreConfig) SSLConfig.getInstance().getStoreConfig( Purpose.WEBADMIN_IDENTITYSTORE );
if (adminSecurePort > 0 && identityStoreConfig.getStore().aliases().hasMoreElements() )
{
if ( !identityStoreConfig.containsDomainCertificate( "RSA" )) {
Log.warn("Admin console: Using RSA certificates but they are not valid for the hosted domain");
}
final CertificateStoreConfig trustStoreConfig = SSLConfig.getInstance().getStoreConfig( Purpose.WEBADMIN_TRUSTSTORE );
final SslContextFactory sslContextFactory = new SslContextFactory();
sslContextFactory.setTrustStorePath( trustStoreConfig.getCanonicalPath() );
sslContextFactory.setTrustStorePassword( trustStoreConfig.getPassword() );
sslContextFactory.setTrustStoreType( trustStoreConfig.getType() );
sslContextFactory.setKeyStorePath( identityStoreConfig.getCanonicalPath() );
sslContextFactory.setKeyStorePassword( identityStoreConfig.getPassword() );
sslContextFactory.setKeyStoreType( identityStoreConfig.getType() );
sslContextFactory.addExcludeProtocols( "SSLv3" );
sslContextFactory.setNeedClientAuth( false );
sslContextFactory.setWantClientAuth( false );
final ServerConnector httpsConnector;
if ("npn".equals(JiveGlobals.getXMLProperty("spdy.protocol", "")))
{
httpsConnector = new HTTPSPDYServerConnector(adminServer, sslContextFactory);
} else {
HttpConfiguration httpsConfig = new HttpConfiguration();
httpsConfig.setSendServerVersion( false );
httpsConfig.setSecureScheme("https");
httpsConfig.setSecurePort(adminSecurePort);
httpsConfig.addCustomizer(new SecureRequestCustomizer());
HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory(httpsConfig);
SslConnectionFactory sslConnectionFactory = new SslConnectionFactory(sslContextFactory, org.eclipse.jetty.http.HttpVersion.HTTP_1_1.toString());
httpsConnector = new ServerConnector(adminServer, null, null, null, -1, serverThreads,
sslConnectionFactory, httpConnectionFactory);
}
String bindInterface = getBindInterface();
httpsConnector.setHost(bindInterface);
httpsConnector.setPort(adminSecurePort);
adminServer.addConnector(httpsConnector);
sslEnabled = true;
}
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
// Make sure that at least one connector was registered.
if (adminServer.getConnectors() == null || adminServer.getConnectors().length == 0) {
adminServer = null;
// Log warning.
log(LocaleUtils.getLocalizedString("admin.console.warning"));
return;
}
HandlerCollection collection = new HandlerCollection();
adminServer.setHandler(collection);
collection.setHandlers(new Handler[] { contexts, new DefaultHandler() });
try {
adminServer.start();
}
catch (Exception e) {
Log.error("Could not start admin console server", e);
}
// Log the ports that the admin server is listening on.
logAdminConsolePorts();
}
/**
* Shuts down the Jetty server.
* */
public void shutdown() {
// Remove listener for certificate events
if (certificateListener != null) {
CertificateManager.removeListener(certificateListener);
}
//noinspection ConstantConditions
try {
if (adminServer != null && adminServer.isRunning()) {
adminServer.stop();
}
}
catch (Exception e) {
Log.error("Error stopping admin console server", e);
}
adminServer = null;
}
@Override
public void initializePlugin(PluginManager manager, File pluginDir) {
this.pluginDir = pluginDir;
createWebAppContext();
startup();
}
@Override
public void destroyPlugin() {
shutdown();
}
/**
* Returns true if the Jetty server needs to be restarted. This is usually required when
* certificates are added, deleted or modified or when server ports were modified.
*
* @return true if the Jetty server needs to be restarted.
*/
public boolean isRestartNeeded() {
return restartNeeded;
}
/**
* Returns <tt>null</tt> if the admin console will be available in all network interfaces of this machine
* or a String representing the only interface where the admin console will be available.
*
* @return String representing the only interface where the admin console will be available or null if it
* will be available in all interfaces.
*/
public String getBindInterface() {
String adminInterfaceName = JiveGlobals.getXMLProperty("adminConsole.interface");
String globalInterfaceName = JiveGlobals.getXMLProperty("network.interface");
String bindInterface = null;
if (adminInterfaceName != null && adminInterfaceName.trim().length() > 0) {
bindInterface = adminInterfaceName;
}
else if (globalInterfaceName != null && globalInterfaceName.trim().length() > 0) {
bindInterface = globalInterfaceName;
}
return bindInterface;
}
/**
* Returns the non-SSL port on which the admin console is currently operating.
*
* @return the non-SSL port on which the admin console is currently operating.
*/
public int getAdminUnsecurePort() {
return adminPort;
}
/**
* Returns the SSL port on which the admin console is current operating.
*
* @return the SSL port on which the admin console is current operating.
*/
public int getAdminSecurePort() {
if (!sslEnabled) {
return 0;
}
return adminSecurePort;
}
/**
* Returns the collection of Jetty contexts used in the admin console. A root context "/"
* is where the admin console lives. Additional contexts can be added dynamically for
* other web applications that should be run as part of the admin console server
* process. The following pseudo code demonstrates how to do this:
*
* <pre>
* ContextHandlerCollection contexts = ((AdminConsolePlugin)pluginManager.getPlugin("admin")).getContexts();
* context = new WebAppContext(SOME_DIRECTORY, "/CONTEXT_NAME");
* contexts.addHandler(context);
* context.setWelcomeFiles(new String[]{"index.jsp"});
* context.start();
* </pre>
*
* @return the Jetty handlers.
*/
public ContextHandlerCollection getContexts() {
return contexts;
}
public void restart() {
try {
adminServer.stop();
adminServer.start();
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
}
private void createWebAppContext() {
ServletContextHandler context;
// Add web-app. Check to see if we're in development mode. If so, we don't
// add the normal web-app location, but the web-app in the project directory.
if (Boolean.getBoolean("developmentMode")) {
System.out.println(LocaleUtils.getLocalizedString("admin.console.devmode"));
context = new WebAppContext(contexts, pluginDir.getParentFile().getParentFile().getParentFile().getParent() +
File.separator + "src" + File.separator + "web", "/");
}
else {
context = new WebAppContext(contexts, pluginDir.getAbsoluteFile() + File.separator + "webapp",
"/");
}
// Ensure the JSP engine is initialized correctly (in order to be able to cope with Tomcat/Jasper precompiled JSPs).
final List<ContainerInitializer> initializers = new ArrayList<>();
initializers.add(new ContainerInitializer(new JettyJasperInitializer(), null));
context.setAttribute("org.eclipse.jetty.containerInitializers", initializers);
context.setAttribute(InstanceManager.class.getName(), new SimpleInstanceManager());
context.setWelcomeFiles(new String[]{"index.jsp"});
}
private void log(String string) {
Log.info(string);
System.out.println(string);
}
private void logAdminConsolePorts() {
// Log what ports the admin console is running on.
String listening = LocaleUtils.getLocalizedString("admin.console.listening");
String hostname = getBindInterface() == null ?
XMPPServer.getInstance().getServerInfo().getXMPPDomain() :
getBindInterface();
boolean isPlainStarted = false;
boolean isSecureStarted = false;
boolean isSPDY = false;
for (Connector connector : adminServer.getConnectors()) {
if (((ServerConnector) connector).getPort() == adminPort) {
isPlainStarted = true;
}
else if (((ServerConnector) connector).getPort() == adminSecurePort) {
isSecureStarted = true;
}
if (connector instanceof HTTPSPDYServerConnector) {
isSPDY = true;
}
}
if (isPlainStarted && isSecureStarted) {
log(listening + ":" + System.getProperty("line.separator") +
" http://" + hostname + ":" +
adminPort + System.getProperty("line.separator") +
" https://" + hostname + ":" +
adminSecurePort + (isSPDY ? " (SPDY)" : ""));
}
else if (isSecureStarted) {
log(listening + " https://" + hostname + ":" + adminSecurePort + (isSPDY ? " (SPDY)" : ""));
}
else if (isPlainStarted) {
log(listening + " http://" + hostname + ":" + adminPort);
}
}
/**
* Listens for security certificates being created and destroyed so we can track when the
* admin console needs to be restarted.
*/
private class CertificateListener implements CertificateEventListener {
@Override
public void certificateCreated(KeyStore keyStore, String alias, X509Certificate cert) {
// If new certificate is RSA then (re)start the HTTPS service
if ("RSA".equals(cert.getPublicKey().getAlgorithm())) {
restartNeeded = true;
}
}
@Override
public void certificateDeleted(KeyStore keyStore, String alias) {
restartNeeded = true;
}
@Override
public void certificateSigned(KeyStore keyStore, String alias,
List<X509Certificate> certificates) {
// If new certificate is RSA then (re)start the HTTPS service
if ("RSA".equals(certificates.get(0).getPublicKey().getAlgorithm())) {
restartNeeded = true;
}
}
}
}
/**
* $Revision: 3034 $
* $Date: 2005-11-04 21:02:33 -0300 (Fri, 04 Nov 2005) $
*
* Copyright (C) 2004-2008 Jive Software. 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.container;
import java.io.File;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import org.apache.tomcat.InstanceManager;
import org.apache.tomcat.SimpleInstanceManager;
import org.eclipse.jetty.apache.jsp.JettyJasperInitializer;
import org.eclipse.jetty.plus.annotation.ContainerInitializer;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.spdy.server.http.HTTPSPDYServerConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.webapp.WebAppContext;
import org.jivesoftware.openfire.JMXManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.keystore.IdentityStoreConfig;
import org.jivesoftware.openfire.keystore.Purpose;
import org.jivesoftware.openfire.keystore.CertificateStoreConfig;
import org.jivesoftware.openfire.keystore.TrustStoreConfig;
import org.jivesoftware.openfire.net.SSLConfig;
import org.jivesoftware.util.CertificateEventListener;
import org.jivesoftware.util.CertificateManager;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The admin console plugin. It starts a Jetty instance on the configured
* port and loads the admin console web application.
*
* @author Matt Tucker
*/
public class AdminConsolePlugin implements Plugin {
private static final Logger Log = LoggerFactory.getLogger(AdminConsolePlugin.class);
/**
* Random secret used by JVM to allow SSO. Only other cluster nodes can use this secret
* as a way to integrate the admin consoles of each cluster node.
*/
public final static String secret = StringUtils.randomString(64);
private int adminPort;
private int adminSecurePort;
private Server adminServer;
private ContextHandlerCollection contexts;
private CertificateEventListener certificateListener;
private boolean restartNeeded = false;
private boolean sslEnabled = false;
private File pluginDir;
/**
* Create a Jetty module.
*/
public AdminConsolePlugin() {
contexts = new ContextHandlerCollection();
// JSP 2.0 uses commons-logging, so also override that implementation.
System.setProperty("org.apache.commons.logging.LogFactory", "org.jivesoftware.util.log.util.CommonsLogFactory");
}
/**
* Starts the Jetty instance.
*/
public void startup() {
restartNeeded = false;
// Add listener for certificate events
certificateListener = new CertificateListener();
CertificateManager.addListener(certificateListener);
// the number of threads allocated to each connector/port
int serverThreads = JiveGlobals.getXMLProperty("adminConsole.serverThreads", 2);
adminPort = JiveGlobals.getXMLProperty("adminConsole.port", 9090);
adminSecurePort = JiveGlobals.getXMLProperty("adminConsole.securePort", 9091);
final QueuedThreadPool tp = new QueuedThreadPool();
tp.setName("Jetty-QTP-AdminConsole");
adminServer = new Server(tp);
if (JMXManager.isEnabled()) {
JMXManager jmx = JMXManager.getInstance();
adminServer.addBean(jmx.getContainer());
}
// Create connector for http traffic if it's enabled.
if (adminPort > 0) {
final HttpConfiguration httpConfig = new HttpConfiguration();
// Do not send Jetty info in HTTP headers
httpConfig.setSendServerVersion( false );
final ServerConnector httpConnector = new ServerConnector(adminServer, null, null, null, -1, serverThreads, new HttpConnectionFactory(httpConfig));
// Listen on a specific network interface if it has been set.
String bindInterface = getBindInterface();
httpConnector.setHost(bindInterface);
httpConnector.setPort(adminPort);
adminServer.addConnector(httpConnector);
}
// Create a connector for https traffic if it's enabled.
sslEnabled = false;
try {
final IdentityStoreConfig identityStoreConfig = SSLConfig.getInstance().getIdentityStoreConfig( Purpose.WEBADMIN );
if (adminSecurePort > 0 && identityStoreConfig.getStore().aliases().hasMoreElements() )
{
if ( !identityStoreConfig.containsDomainCertificate( "RSA" )) {
Log.warn("Admin console: Using RSA certificates but they are not valid for the hosted domain");
}
final TrustStoreConfig trustStoreConfig = SSLConfig.getInstance().getTrustStoreConfig( Purpose.WEBADMIN );
final SslContextFactory sslContextFactory = new SslContextFactory();
sslContextFactory.setTrustStorePath( trustStoreConfig.getCanonicalPath() );
sslContextFactory.setTrustStorePassword( trustStoreConfig.getPassword() );
sslContextFactory.setTrustStoreType( trustStoreConfig.getType() );
sslContextFactory.setKeyStorePath( identityStoreConfig.getCanonicalPath() );
sslContextFactory.setKeyStorePassword( identityStoreConfig.getPassword() );
sslContextFactory.setKeyStoreType( identityStoreConfig.getType() );
sslContextFactory.addExcludeProtocols( "SSLv3" );
sslContextFactory.setNeedClientAuth( false );
sslContextFactory.setWantClientAuth( false );
final ServerConnector httpsConnector;
if ("npn".equals(JiveGlobals.getXMLProperty("spdy.protocol", "")))
{
httpsConnector = new HTTPSPDYServerConnector(adminServer, sslContextFactory);
} else {
HttpConfiguration httpsConfig = new HttpConfiguration();
httpsConfig.setSendServerVersion( false );
httpsConfig.setSecureScheme("https");
httpsConfig.setSecurePort(adminSecurePort);
httpsConfig.addCustomizer(new SecureRequestCustomizer());
HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory(httpsConfig);
SslConnectionFactory sslConnectionFactory = new SslConnectionFactory(sslContextFactory, org.eclipse.jetty.http.HttpVersion.HTTP_1_1.toString());
httpsConnector = new ServerConnector(adminServer, null, null, null, -1, serverThreads,
sslConnectionFactory, httpConnectionFactory);
}
String bindInterface = getBindInterface();
httpsConnector.setHost(bindInterface);
httpsConnector.setPort(adminSecurePort);
adminServer.addConnector(httpsConnector);
sslEnabled = true;
}
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
// Make sure that at least one connector was registered.
if (adminServer.getConnectors() == null || adminServer.getConnectors().length == 0) {
adminServer = null;
// Log warning.
log(LocaleUtils.getLocalizedString("admin.console.warning"));
return;
}
HandlerCollection collection = new HandlerCollection();
adminServer.setHandler(collection);
collection.setHandlers(new Handler[] { contexts, new DefaultHandler() });
try {
adminServer.start();
}
catch (Exception e) {
Log.error("Could not start admin console server", e);
}
// Log the ports that the admin server is listening on.
logAdminConsolePorts();
}
/**
* Shuts down the Jetty server.
* */
public void shutdown() {
// Remove listener for certificate events
if (certificateListener != null) {
CertificateManager.removeListener(certificateListener);
}
//noinspection ConstantConditions
try {
if (adminServer != null && adminServer.isRunning()) {
adminServer.stop();
}
}
catch (Exception e) {
Log.error("Error stopping admin console server", e);
}
adminServer = null;
}
@Override
public void initializePlugin(PluginManager manager, File pluginDir) {
this.pluginDir = pluginDir;
createWebAppContext();
startup();
}
@Override
public void destroyPlugin() {
shutdown();
}
/**
* Returns true if the Jetty server needs to be restarted. This is usually required when
* certificates are added, deleted or modified or when server ports were modified.
*
* @return true if the Jetty server needs to be restarted.
*/
public boolean isRestartNeeded() {
return restartNeeded;
}
/**
* Returns <tt>null</tt> if the admin console will be available in all network interfaces of this machine
* or a String representing the only interface where the admin console will be available.
*
* @return String representing the only interface where the admin console will be available or null if it
* will be available in all interfaces.
*/
public String getBindInterface() {
String adminInterfaceName = JiveGlobals.getXMLProperty("adminConsole.interface");
String globalInterfaceName = JiveGlobals.getXMLProperty("network.interface");
String bindInterface = null;
if (adminInterfaceName != null && adminInterfaceName.trim().length() > 0) {
bindInterface = adminInterfaceName;
}
else if (globalInterfaceName != null && globalInterfaceName.trim().length() > 0) {
bindInterface = globalInterfaceName;
}
return bindInterface;
}
/**
* Returns the non-SSL port on which the admin console is currently operating.
*
* @return the non-SSL port on which the admin console is currently operating.
*/
public int getAdminUnsecurePort() {
return adminPort;
}
/**
* Returns the SSL port on which the admin console is current operating.
*
* @return the SSL port on which the admin console is current operating.
*/
public int getAdminSecurePort() {
if (!sslEnabled) {
return 0;
}
return adminSecurePort;
}
/**
* Returns the collection of Jetty contexts used in the admin console. A root context "/"
* is where the admin console lives. Additional contexts can be added dynamically for
* other web applications that should be run as part of the admin console server
* process. The following pseudo code demonstrates how to do this:
*
* <pre>
* ContextHandlerCollection contexts = ((AdminConsolePlugin)pluginManager.getPlugin("admin")).getContexts();
* context = new WebAppContext(SOME_DIRECTORY, "/CONTEXT_NAME");
* contexts.addHandler(context);
* context.setWelcomeFiles(new String[]{"index.jsp"});
* context.start();
* </pre>
*
* @return the Jetty handlers.
*/
public ContextHandlerCollection getContexts() {
return contexts;
}
public void restart() {
try {
adminServer.stop();
adminServer.start();
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
}
private void createWebAppContext() {
ServletContextHandler context;
// Add web-app. Check to see if we're in development mode. If so, we don't
// add the normal web-app location, but the web-app in the project directory.
if (Boolean.getBoolean("developmentMode")) {
System.out.println(LocaleUtils.getLocalizedString("admin.console.devmode"));
context = new WebAppContext(contexts, pluginDir.getParentFile().getParentFile().getParentFile().getParent() +
File.separator + "src" + File.separator + "web", "/");
}
else {
context = new WebAppContext(contexts, pluginDir.getAbsoluteFile() + File.separator + "webapp",
"/");
}
context.setWelcomeFiles(new String[]{"index.jsp"});
}
private void log(String string) {
Log.info(string);
System.out.println(string);
}
private void logAdminConsolePorts() {
// Log what ports the admin console is running on.
String listening = LocaleUtils.getLocalizedString("admin.console.listening");
String hostname = getBindInterface() == null ?
XMPPServer.getInstance().getServerInfo().getXMPPDomain() :
getBindInterface();
boolean isPlainStarted = false;
boolean isSecureStarted = false;
boolean isSPDY = false;
for (Connector connector : adminServer.getConnectors()) {
if (((ServerConnector) connector).getPort() == adminPort) {
isPlainStarted = true;
}
else if (((ServerConnector) connector).getPort() == adminSecurePort) {
isSecureStarted = true;
}
if (connector instanceof HTTPSPDYServerConnector) {
isSPDY = true;
}
}
if (isPlainStarted && isSecureStarted) {
log(listening + ":" + System.getProperty("line.separator") +
" http://" + hostname + ":" +
adminPort + System.getProperty("line.separator") +
" https://" + hostname + ":" +
adminSecurePort + (isSPDY ? " (SPDY)" : ""));
}
else if (isSecureStarted) {
log(listening + " https://" + hostname + ":" + adminSecurePort + (isSPDY ? " (SPDY)" : ""));
}
else if (isPlainStarted) {
log(listening + " http://" + hostname + ":" + adminPort);
}
}
/**
* Listens for security certificates being created and destroyed so we can track when the
* admin console needs to be restarted.
*/
private class CertificateListener implements CertificateEventListener {
@Override
public void certificateCreated(KeyStore keyStore, String alias, X509Certificate cert) {
// If new certificate is RSA then (re)start the HTTPS service
if ("RSA".equals(cert.getPublicKey().getAlgorithm())) {
restartNeeded = true;
}
}
@Override
public void certificateDeleted(KeyStore keyStore, String alias) {
restartNeeded = true;
}
@Override
public void certificateSigned(KeyStore keyStore, String alias,
List<X509Certificate> certificates) {
// If new certificate is RSA then (re)start the HTTPS service
if ("RSA".equals(certificates.get(0).getPublicKey().getAlgorithm())) {
restartNeeded = true;
}
}
}
}
/**
* $RCSfile$
* $Revision: $
* $Date: $
*
* Copyright (C) 2005-2008 Jive Software. 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.http;
import java.io.File;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.*;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import org.apache.tomcat.InstanceManager;
import org.apache.tomcat.SimpleInstanceManager;
import org.eclipse.jetty.apache.jsp.JettyJasperInitializer;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.plus.annotation.ContainerInitializer;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.ForwardedRequestCustomizer;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.servlets.AsyncGzipFilter;
import org.eclipse.jetty.spdy.server.http.HTTPSPDYServerConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.webapp.WebAppContext;
import org.jivesoftware.openfire.Connection;
import org.jivesoftware.openfire.JMXManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.keystore.IdentityStoreConfig;
import org.jivesoftware.openfire.keystore.Purpose;
import org.jivesoftware.openfire.keystore.CertificateStoreConfig;
import org.jivesoftware.openfire.net.SSLConfig;
import org.jivesoftware.openfire.session.ConnectionSettings;
import org.jivesoftware.util.CertificateEventListener;
import org.jivesoftware.util.CertificateManager;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.PropertyEventDispatcher;
import org.jivesoftware.util.PropertyEventListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
*/
public final class HttpBindManager {
private static final Logger Log = LoggerFactory.getLogger(HttpBindManager.class);
public static final String HTTP_BIND_ENABLED = "httpbind.enabled";
public static final boolean HTTP_BIND_ENABLED_DEFAULT = true;
public static final String HTTP_BIND_PORT = "httpbind.port.plain";
public static final int HTTP_BIND_PORT_DEFAULT = 7070;
public static final String HTTP_BIND_SECURE_PORT = "httpbind.port.secure";
public static final int HTTP_BIND_SECURE_PORT_DEFAULT = 7443;
public static final String HTTP_BIND_THREADS = "httpbind.client.processing.threads";
public static final String HTTP_BIND_AUTH_PER_CLIENTCERT_POLICY = "httpbind.client.cert.policy";
public static final int HTTP_BIND_THREADS_DEFAULT = 8;
private static final String HTTP_BIND_FORWARDED = "httpbind.forwarded.enabled";
private static final String HTTP_BIND_FORWARDED_FOR = "httpbind.forwarded.for.header";
private static final String HTTP_BIND_FORWARDED_SERVER = "httpbind.forwarded.server.header";
private static final String HTTP_BIND_FORWARDED_HOST = "httpbind.forwarded.host.header";
private static final String HTTP_BIND_FORWARDED_HOST_NAME = "httpbind.forwarded.host.name";
// http binding CORS default properties
public static final String HTTP_BIND_CORS_ENABLED = "httpbind.CORS.enabled";
public static final boolean HTTP_BIND_CORS_ENABLED_DEFAULT = true;
public static final String HTTP_BIND_CORS_ALLOW_ORIGIN = "httpbind.CORS.domains";
public static final String HTTP_BIND_CORS_ALLOW_ORIGIN_DEFAULT = "*";
public static final String HTTP_BIND_CORS_ALLOW_METHODS_DEFAULT = "PROPFIND, PROPPATCH, COPY, MOVE, DELETE, MKCOL, LOCK, UNLOCK, PUT, GETLIB, VERSION-CONTROL, CHECKIN, CHECKOUT, UNCHECKOUT, REPORT, UPDATE, CANCELUPLOAD, HEAD, OPTIONS, GET, POST";
public static final String HTTP_BIND_CORS_ALLOW_HEADERS_DEFAULT = "Overwrite, Destination, Content-Type, Depth, User-Agent, X-File-Size, X-Requested-With, If-Modified-Since, X-File-Name, Cache-Control";
public static final String HTTP_BIND_CORS_MAX_AGE_DEFAULT = "86400";
public static final String HTTP_BIND_REQUEST_HEADER_SIZE = "httpbind.request.header.size";
public static final int HTTP_BIND_REQUEST_HEADER_SIZE_DEFAULT = 32768;
public static Map<String, Boolean> HTTP_BIND_ALLOWED_ORIGINS = new HashMap<>();
private static HttpBindManager instance = new HttpBindManager();
// Compression "optional" by default; use "disabled" to disable compression (restart required)
// When enabled, http response will be compressed if the http request includes an
// "Accept" header with a value of "gzip" and/or "deflate"
private static boolean isCompressionEnabled = !(JiveGlobals.getProperty(
ConnectionSettings.Client.COMPRESSION_SETTINGS, Connection.CompressionPolicy.optional.toString())
.equalsIgnoreCase(Connection.CompressionPolicy.disabled.toString()));
private Server httpBindServer;
private int bindPort;
private int bindSecurePort;
private Connector httpConnector;
private Connector httpsConnector;
private CertificateListener certificateListener;
private HttpSessionManager httpSessionManager;
private ContextHandlerCollection contexts;
// is all orgin allowed flag
private boolean allowAllOrigins;
public static HttpBindManager getInstance() {
return instance;
}
private HttpBindManager() {
// JSP 2.0 uses commons-logging, so also override that implementation.
System.setProperty("org.apache.commons.logging.LogFactory", "org.jivesoftware.util.log.util.CommonsLogFactory");
JiveGlobals.migrateProperty(HTTP_BIND_ENABLED);
JiveGlobals.migrateProperty(HTTP_BIND_PORT);
JiveGlobals.migrateProperty(HTTP_BIND_SECURE_PORT);
JiveGlobals.migrateProperty(HTTP_BIND_THREADS);
JiveGlobals.migrateProperty(HTTP_BIND_FORWARDED);
JiveGlobals.migrateProperty(HTTP_BIND_FORWARDED_FOR);
JiveGlobals.migrateProperty(HTTP_BIND_FORWARDED_SERVER);
JiveGlobals.migrateProperty(HTTP_BIND_FORWARDED_HOST);
JiveGlobals.migrateProperty(HTTP_BIND_FORWARDED_HOST_NAME);
JiveGlobals.migrateProperty(HTTP_BIND_CORS_ENABLED);
JiveGlobals.migrateProperty(HTTP_BIND_CORS_ALLOW_ORIGIN);
JiveGlobals.migrateProperty(HTTP_BIND_REQUEST_HEADER_SIZE);
PropertyEventDispatcher.addListener(new HttpServerPropertyListener());
this.httpSessionManager = new HttpSessionManager();
// we need to initialise contexts at constructor time in order for plugins to add their contexts before start()
contexts = new ContextHandlerCollection();
// setup the cache for the allowed origins
this.setupAllowedOriginsMap();
}
public void start() {
certificateListener = new CertificateListener();
CertificateManager.addListener(certificateListener);
if (!isHttpBindServiceEnabled()) {
return;
}
bindPort = getHttpBindUnsecurePort();
bindSecurePort = getHttpBindSecurePort();
configureHttpBindServer(bindPort, bindSecurePort);
try {
httpBindServer.start();
Log.info("HTTP bind service started");
}
catch (Exception e) {
Log.error("Error starting HTTP bind service", e);
}
}
public void stop() {
CertificateManager.removeListener(certificateListener);
if (httpBindServer != null) {
try {
httpBindServer.stop();
Log.info("HTTP bind service stopped");
}
catch (Exception e) {
Log.error("Error stopping HTTP bind service", e);
}
httpBindServer = null;
}
}
public HttpSessionManager getSessionManager() {
return httpSessionManager;
}
private boolean isHttpBindServiceEnabled() {
return JiveGlobals.getBooleanProperty(HTTP_BIND_ENABLED, HTTP_BIND_ENABLED_DEFAULT);
}
private void createConnector(int port, int bindThreads) {
httpConnector = null;
if (port > 0) {
HttpConfiguration httpConfig = new HttpConfiguration();
configureProxiedConnector(httpConfig);
ServerConnector connector = new ServerConnector(httpBindServer, null, null, null, -1, bindThreads,
new HttpConnectionFactory(httpConfig));
// Listen on a specific network interface if it has been set.
connector.setHost(getBindInterface());
connector.setPort(port);
httpConnector = connector;
}
}
private void createSSLConnector(int securePort, int bindThreads) {
httpsConnector = null;
try {
final IdentityStoreConfig identityStoreConfig = (IdentityStoreConfig) SSLConfig.getInstance().getStoreConfig( Purpose.BOSHBASED_IDENTITYSTORE );
final KeyStore keyStore = identityStoreConfig.getStore();
if (securePort > 0 && identityStoreConfig.getStore().aliases().hasMoreElements() ) {
if ( !identityStoreConfig.containsDomainCertificate( "RSA" ) ) {
Log.warn("HTTP binding: Using RSA certificates but they are not valid for " +
"the hosted domain");
}
final CertificateStoreConfig trustStoreConfig = SSLConfig.getInstance().getStoreConfig( Purpose.BOSHBASED_C2S_TRUSTSTORE );
final SslContextFactory sslContextFactory = new SslContextFactory();
sslContextFactory.setTrustStorePath( trustStoreConfig.getCanonicalPath() );
sslContextFactory.setTrustStorePassword( trustStoreConfig.getPassword() );
sslContextFactory.setTrustStoreType( trustStoreConfig.getType() );
sslContextFactory.setKeyStorePath( identityStoreConfig.getCanonicalPath() );
sslContextFactory.setKeyStorePassword( identityStoreConfig.getPassword() );
sslContextFactory.setKeyStoreType( identityStoreConfig.getType() );
sslContextFactory.addExcludeProtocols( "SSLv3" );
// Set policy for checking client certificates
String certPol = JiveGlobals.getProperty(HTTP_BIND_AUTH_PER_CLIENTCERT_POLICY, "disabled");
if(certPol.equals("needed")) {
sslContextFactory.setNeedClientAuth(true);
sslContextFactory.setWantClientAuth(true);
} else if(certPol.equals("wanted")) {
sslContextFactory.setNeedClientAuth(false);
sslContextFactory.setWantClientAuth(true);
} else {
sslContextFactory.setNeedClientAuth(false);
sslContextFactory.setWantClientAuth(false);
}
HttpConfiguration httpsConfig = new HttpConfiguration();
httpsConfig.setSecureScheme("https");
httpsConfig.setSecurePort(securePort);
configureProxiedConnector(httpsConfig);
httpsConfig.addCustomizer(new SecureRequestCustomizer());
ServerConnector sslConnector = null;
if ("npn".equals(JiveGlobals.getXMLProperty("spdy.protocol", "")))
{
sslConnector = new HTTPSPDYServerConnector(httpBindServer, sslContextFactory);
} else {
sslConnector = new ServerConnector(httpBindServer, null, null, null, -1, bindThreads,
new SslConnectionFactory(sslContextFactory, "http/1.1"), new HttpConnectionFactory(httpsConfig));
}
sslConnector.setHost(getBindInterface());
sslConnector.setPort(securePort);
httpsConnector = sslConnector;
}
}
catch (Exception e) {
Log.error("Error creating SSL connector for Http bind", e);
}
}
private void configureProxiedConnector(HttpConfiguration httpConfig) {
// Check to see if we are deployed behind a proxy
// Refer to http://eclipse.org/jetty/documentation/current/configuring-connectors.html
if (isXFFEnabled()) {
ForwardedRequestCustomizer customizer = new ForwardedRequestCustomizer();
// default: "X-Forwarded-For"
String forwardedForHeader = getXFFHeader();
if (forwardedForHeader != null) {
customizer.setForwardedForHeader(forwardedForHeader);
}
// default: "X-Forwarded-Server"
String forwardedServerHeader = getXFFServerHeader();
if (forwardedServerHeader != null) {
customizer.setForwardedServerHeader(forwardedServerHeader);
}
// default: "X-Forwarded-Host"
String forwardedHostHeader = getXFFHostHeader();
if (forwardedHostHeader != null) {
customizer.setForwardedHostHeader(forwardedHostHeader);
}
// default: none
String hostName = getXFFHostName();
if (hostName != null) {
customizer.setHostHeader(hostName);
}
httpConfig.addCustomizer(customizer);
}
httpConfig.setRequestHeaderSize(JiveGlobals.getIntProperty(HTTP_BIND_REQUEST_HEADER_SIZE, HTTP_BIND_REQUEST_HEADER_SIZE_DEFAULT));
}
private String getBindInterface() {
String interfaceName = JiveGlobals.getXMLProperty("network.interface");
String bindInterface = null;
if (interfaceName != null) {
if (interfaceName.trim().length() > 0) {
bindInterface = interfaceName;
}
}
return bindInterface;
}
/**
* Returns true if the HTTP binding server is currently enabled.
*
* @return true if the HTTP binding server is currently enabled.
*/
public boolean isHttpBindEnabled() {
return httpBindServer != null && httpBindServer.isRunning();
}
/**
* Returns true if a listener on the HTTP binding port is running.
*
* @return true if a listener on the HTTP binding port is running.
*/
public boolean isHttpBindActive() {
return httpConnector != null && httpConnector.isRunning();
}
/**
* Returns true if a listener on the HTTPS binding port is running.
*
* @return true if a listener on the HTTPS binding port is running.
*/
public boolean isHttpsBindActive() {
return httpsConnector != null && httpsConnector.isRunning();
}
public String getHttpBindUnsecureAddress() {
return "http://" + XMPPServer.getInstance().getServerInfo().getXMPPDomain() + ":" +
bindPort + "/http-bind/";
}
public String getHttpBindSecureAddress() {
return "https://" + XMPPServer.getInstance().getServerInfo().getXMPPDomain() + ":" +
bindSecurePort + "/http-bind/";
}
public String getJavaScriptUrl() {
return "http://" + XMPPServer.getInstance().getServerInfo().getXMPPDomain() + ":" +
bindPort + "/scripts/";
}
// http binding CORS support start
private void setupAllowedOriginsMap() {
String originString = getCORSAllowOrigin();
if (originString.equals(HTTP_BIND_CORS_ALLOW_ORIGIN_DEFAULT)) {
allowAllOrigins = true;
} else {
allowAllOrigins = false;
String[] origins = originString.split(",");
// reset the cache
HTTP_BIND_ALLOWED_ORIGINS.clear();
for (String str : origins) {
HTTP_BIND_ALLOWED_ORIGINS.put(str, true);
}
}
}
public boolean isCORSEnabled() {
return JiveGlobals.getBooleanProperty(HTTP_BIND_CORS_ENABLED, HTTP_BIND_CORS_ENABLED_DEFAULT);
}
public void setCORSEnabled(Boolean value) {
if (value != null)
JiveGlobals.setProperty(HTTP_BIND_CORS_ENABLED, String.valueOf(value));
}
public String getCORSAllowOrigin() {
return JiveGlobals.getProperty(HTTP_BIND_CORS_ALLOW_ORIGIN , HTTP_BIND_CORS_ALLOW_ORIGIN_DEFAULT);
}
public void setCORSAllowOrigin(String origins) {
if (origins == null || origins.trim().length() == 0)
origins = HTTP_BIND_CORS_ALLOW_ORIGIN_DEFAULT;
else {
origins = origins.replaceAll("\\s+", "");
}
JiveGlobals.setProperty(HTTP_BIND_CORS_ALLOW_ORIGIN, origins);
setupAllowedOriginsMap();
}
public boolean isAllOriginsAllowed() {
return allowAllOrigins;
}
public boolean isThisOriginAllowed(String origin) {
return HTTP_BIND_ALLOWED_ORIGINS.get(origin) != null;
}
// http binding CORS support end
public boolean isXFFEnabled() {
return JiveGlobals.getBooleanProperty(HTTP_BIND_FORWARDED, false);
}
public void setXFFEnabled(boolean enabled) {
JiveGlobals.setProperty(HTTP_BIND_FORWARDED, String.valueOf(enabled));
}
public String getXFFHeader() {
return JiveGlobals.getProperty(HTTP_BIND_FORWARDED_FOR);
}
public void setXFFHeader(String header) {
if (header == null || header.trim().length() == 0) {
JiveGlobals.deleteProperty(HTTP_BIND_FORWARDED_FOR);
} else {
JiveGlobals.setProperty(HTTP_BIND_FORWARDED_FOR, header);
}
}
public String getXFFServerHeader() {
return JiveGlobals.getProperty(HTTP_BIND_FORWARDED_SERVER);
}
public void setXFFServerHeader(String header) {
if (header == null || header.trim().length() == 0) {
JiveGlobals.deleteProperty(HTTP_BIND_FORWARDED_SERVER);
} else {
JiveGlobals.setProperty(HTTP_BIND_FORWARDED_SERVER, header);
}
}
public String getXFFHostHeader() {
return JiveGlobals.getProperty(HTTP_BIND_FORWARDED_HOST);
}
public void setXFFHostHeader(String header) {
if (header == null || header.trim().length() == 0) {
JiveGlobals.deleteProperty(HTTP_BIND_FORWARDED_HOST);
} else {
JiveGlobals.setProperty(HTTP_BIND_FORWARDED_HOST, header);
}
}
public String getXFFHostName() {
return JiveGlobals.getProperty(HTTP_BIND_FORWARDED_HOST_NAME);
}
public void setXFFHostName(String name) {
if (name == null || name.trim().length() == 0) {
JiveGlobals.deleteProperty(HTTP_BIND_FORWARDED_HOST_NAME);
} else {
JiveGlobals.setProperty(HTTP_BIND_FORWARDED_HOST_NAME, name);
}
}
public void setHttpBindEnabled(boolean isEnabled) {
JiveGlobals.setProperty(HTTP_BIND_ENABLED, String.valueOf(isEnabled));
}
/**
* Set the ports on which the HTTP binding service will be running.
*
* @param unsecurePort the unsecured connection port which clients can connect to.
* @param securePort the secured connection port which clients can connect to.
* @throws Exception when there is an error configuring the HTTP binding ports.
*/
public void setHttpBindPorts(int unsecurePort, int securePort) throws Exception {
if (unsecurePort != HTTP_BIND_PORT_DEFAULT) {
JiveGlobals.setProperty(HTTP_BIND_PORT, String.valueOf(unsecurePort));
}
else {
JiveGlobals.deleteProperty(HTTP_BIND_PORT);
}
if (securePort != HTTP_BIND_SECURE_PORT_DEFAULT) {
JiveGlobals.setProperty(HTTP_BIND_SECURE_PORT, String.valueOf(securePort));
}
else {
JiveGlobals.deleteProperty(HTTP_BIND_SECURE_PORT);
}
}
/**
* Starts an HTTP Bind server on the specified port and secure port.
*
* @param port the port to start the normal (unsecured) HTTP Bind service on.
* @param securePort the port to start the TLS (secure) HTTP Bind service on.
*/
private synchronized void configureHttpBindServer(int port, int securePort) {
// this is the number of threads allocated to each connector/port
int bindThreads = JiveGlobals.getIntProperty(HTTP_BIND_THREADS, HTTP_BIND_THREADS_DEFAULT);
final QueuedThreadPool tp = new QueuedThreadPool();
tp.setName("Jetty-QTP-BOSH");
httpBindServer = new Server(tp);
if (JMXManager.isEnabled()) {
JMXManager jmx = JMXManager.getInstance();
httpBindServer.addBean(jmx.getContainer());
}
createConnector(port, bindThreads);
createSSLConnector(securePort, bindThreads);
if (httpConnector == null && httpsConnector == null) {
httpBindServer = null;
return;
}
if (httpConnector != null) {
httpBindServer.addConnector(httpConnector);
}
if (httpsConnector != null) {
httpBindServer.addConnector(httpsConnector);
}
//contexts = new ContextHandlerCollection();
// TODO implement a way to get plugins to add their their web services to contexts
createBoshHandler(contexts, "/http-bind");
createCrossDomainHandler(contexts, "/crossdomain.xml");
loadStaticDirectory(contexts);
HandlerCollection collection = new HandlerCollection();
httpBindServer.setHandler(collection);
collection.setHandlers(new Handler[] { contexts, new DefaultHandler() });
}
private void createBoshHandler(ContextHandlerCollection contexts, String boshPath)
{
ServletContextHandler context = new ServletContextHandler(contexts, boshPath, ServletContextHandler.SESSIONS);
// Ensure the JSP engine is initialized correctly (in order to be able to cope with Tomcat/Jasper precompiled JSPs).
final List<ContainerInitializer> initializers = new ArrayList<>();
initializers.add(new ContainerInitializer(new JettyJasperInitializer(), null));
context.setAttribute("org.eclipse.jetty.containerInitializers", initializers);
context.setAttribute(InstanceManager.class.getName(), new SimpleInstanceManager());
context.addServlet(new ServletHolder(new HttpBindServlet()),"/*");
if (isHttpCompressionEnabled()) {
Filter gzipFilter = new AsyncGzipFilter() {
@Override
public void init(FilterConfig config) throws ServletException {
super.init(config);
_methods.add(HttpMethod.POST.asString());
Log.info("Installed response compression filter");
}
};
FilterHolder filterHolder = new FilterHolder();
filterHolder.setFilter(gzipFilter);
context.addFilter(filterHolder, "/*", EnumSet.of(DispatcherType.REQUEST));
}
}
// NOTE: enabled by default
private boolean isHttpCompressionEnabled() {
return isCompressionEnabled;
}
private void createCrossDomainHandler(ContextHandlerCollection contexts, String crossPath)
{
ServletContextHandler context = new ServletContextHandler(contexts, crossPath, ServletContextHandler.SESSIONS);
// Ensure the JSP engine is initialized correctly (in order to be able to cope with Tomcat/Jasper precompiled JSPs).
final List<ContainerInitializer> initializers = new ArrayList<>();
initializers.add(new ContainerInitializer(new JettyJasperInitializer(), null));
context.setAttribute("org.eclipse.jetty.containerInitializers", initializers);
context.setAttribute(InstanceManager.class.getName(), new SimpleInstanceManager());
context.addServlet(new ServletHolder(new FlashCrossDomainServlet()),"");
}
private void loadStaticDirectory(ContextHandlerCollection contexts) {
File spankDirectory = new File(JiveGlobals.getHomeDirectory() + File.separator
+ "resources" + File.separator + "spank");
if (spankDirectory.exists()) {
if (spankDirectory.canRead()) {
WebAppContext context = new WebAppContext(contexts, spankDirectory.getPath(), "/");
context.setWelcomeFiles(new String[]{"index.html"});
}
else {
Log.warn("Openfire cannot read the directory: " + spankDirectory);
}
}
}
public ContextHandlerCollection getContexts() {
return contexts;
}
private void doEnableHttpBind(boolean shouldEnable) {
if (shouldEnable && httpBindServer == null) {
start();
}
else if (!shouldEnable && httpBindServer != null) {
stop();
}
}
/**
* Returns the HTTP binding port which does not use SSL.
*
* @return the HTTP binding port which does not use SSL.
*/
public int getHttpBindUnsecurePort() {
return JiveGlobals.getIntProperty(HTTP_BIND_PORT, HTTP_BIND_PORT_DEFAULT);
}
/**
* Returns the HTTP binding port which uses SSL.
*
* @return the HTTP binding port which uses SSL.
*/
public int getHttpBindSecurePort() {
return JiveGlobals.getIntProperty(HTTP_BIND_SECURE_PORT, HTTP_BIND_SECURE_PORT_DEFAULT);
}
/**
* Returns true if script syntax is enabled. Script syntax allows BOSH to be used in
* environments where clients may be restricted to using a particular server. Instead of using
* standard HTTP Post requests to transmit data, HTTP Get requests are used.
*
* @return true if script syntax is enabled.
* @see <a href="http://www.xmpp.org/extensions/xep-0124.html#script">BOSH: Alternative Script
* Syntax</a>
*/
public boolean isScriptSyntaxEnabled() {
return JiveGlobals.getBooleanProperty("xmpp.httpbind.scriptSyntax.enabled", false);
}
/**
* Enables or disables script syntax.
*
* @param isEnabled true to enable script syntax and false to disable it.
* @see #isScriptSyntaxEnabled()
* @see <a href="http://www.xmpp.org/extensions/xep-0124.html#script">BOSH: Alternative Script
* Syntax</a>
*/
public void setScriptSyntaxEnabled(boolean isEnabled) {
final String property = "xmpp.httpbind.scriptSyntax.enabled";
if(!isEnabled) {
JiveGlobals.deleteProperty(property);
}
else {
JiveGlobals.setProperty(property, String.valueOf(isEnabled));
}
}
private void setUnsecureHttpBindPort(int value) {
if (value == bindPort) {
return;
}
restartServer();
}
private void setSecureHttpBindPort(int value) {
if (value == bindSecurePort) {
return;
}
restartServer();
}
private synchronized void restartServer() {
stop();
start();
}
/** Listens for changes to Jive properties that affect the HTTP server manager. */
private class HttpServerPropertyListener implements PropertyEventListener {
@Override
public void propertySet(String property, Map<String, Object> params) {
if (property.equalsIgnoreCase(HTTP_BIND_ENABLED)) {
doEnableHttpBind(Boolean.valueOf(params.get("value").toString()));
}
else if (property.equalsIgnoreCase(HTTP_BIND_PORT)) {
int value;
try {
value = Integer.valueOf(params.get("value").toString());
}
catch (NumberFormatException ne) {
JiveGlobals.deleteProperty(HTTP_BIND_PORT);
return;
}
setUnsecureHttpBindPort(value);
}
else if (property.equalsIgnoreCase(HTTP_BIND_SECURE_PORT)) {
int value;
try {
value = Integer.valueOf(params.get("value").toString());
}
catch (NumberFormatException ne) {
JiveGlobals.deleteProperty(HTTP_BIND_SECURE_PORT);
return;
}
setSecureHttpBindPort(value);
}
else if (HTTP_BIND_AUTH_PER_CLIENTCERT_POLICY.equalsIgnoreCase( property )) {
restartServer();
}
}
@Override
public void propertyDeleted(String property, Map<String, Object> params) {
if (property.equalsIgnoreCase(HTTP_BIND_ENABLED)) {
doEnableHttpBind(HTTP_BIND_ENABLED_DEFAULT);
}
else if (property.equalsIgnoreCase(HTTP_BIND_PORT)) {
setUnsecureHttpBindPort(HTTP_BIND_PORT_DEFAULT);
}
else if (property.equalsIgnoreCase(HTTP_BIND_SECURE_PORT)) {
setSecureHttpBindPort(HTTP_BIND_SECURE_PORT_DEFAULT);
}
else if (HTTP_BIND_AUTH_PER_CLIENTCERT_POLICY.equalsIgnoreCase( property )) {
restartServer();
}
}
@Override
public void xmlPropertySet(String property, Map<String, Object> params) {
}
@Override
public void xmlPropertyDeleted(String property, Map<String, Object> params) {
}
}
private class CertificateListener implements CertificateEventListener {
@Override
public void certificateCreated(KeyStore keyStore, String alias, X509Certificate cert) {
// If new certificate is RSA then (re)start the HTTPS service
if ("RSA".equals(cert.getPublicKey().getAlgorithm())) {
restartServer();
}
}
@Override
public void certificateDeleted(KeyStore keyStore, String alias) {
restartServer();
}
@Override
public void certificateSigned(KeyStore keyStore, String alias,
List<X509Certificate> certificates) {
// If new certificate is RSA then (re)start the HTTPS service
if ("RSA".equals(certificates.get(0).getPublicKey().getAlgorithm())) {
restartServer();
}
}
}
}
/**
* $RCSfile$
* $Revision: $
* $Date: $
*
* Copyright (C) 2005-2008 Jive Software. 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.http;
import java.io.File;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import org.apache.tomcat.InstanceManager;
import org.apache.tomcat.SimpleInstanceManager;
import org.eclipse.jetty.apache.jsp.JettyJasperInitializer;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.plus.annotation.ContainerInitializer;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.ForwardedRequestCustomizer;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.servlets.AsyncGzipFilter;
import org.eclipse.jetty.spdy.server.http.HTTPSPDYServerConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.webapp.WebAppContext;
import org.jivesoftware.openfire.Connection;
import org.jivesoftware.openfire.JMXManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.keystore.IdentityStoreConfig;
import org.jivesoftware.openfire.keystore.Purpose;
import org.jivesoftware.openfire.keystore.CertificateStoreConfig;
import org.jivesoftware.openfire.net.SSLConfig;
import org.jivesoftware.openfire.session.ConnectionSettings;
import org.jivesoftware.util.CertificateEventListener;
import org.jivesoftware.util.CertificateManager;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.PropertyEventDispatcher;
import org.jivesoftware.util.PropertyEventListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
*/
public final class HttpBindManager {
private static final Logger Log = LoggerFactory.getLogger(HttpBindManager.class);
public static final String HTTP_BIND_ENABLED = "httpbind.enabled";
public static final boolean HTTP_BIND_ENABLED_DEFAULT = true;
public static final String HTTP_BIND_PORT = "httpbind.port.plain";
public static final int HTTP_BIND_PORT_DEFAULT = 7070;
public static final String HTTP_BIND_SECURE_PORT = "httpbind.port.secure";
public static final int HTTP_BIND_SECURE_PORT_DEFAULT = 7443;
public static final String HTTP_BIND_THREADS = "httpbind.client.processing.threads";
public static final String HTTP_BIND_AUTH_PER_CLIENTCERT_POLICY = "httpbind.client.cert.policy";
public static final int HTTP_BIND_THREADS_DEFAULT = 8;
private static final String HTTP_BIND_FORWARDED = "httpbind.forwarded.enabled";
private static final String HTTP_BIND_FORWARDED_FOR = "httpbind.forwarded.for.header";
private static final String HTTP_BIND_FORWARDED_SERVER = "httpbind.forwarded.server.header";
private static final String HTTP_BIND_FORWARDED_HOST = "httpbind.forwarded.host.header";
private static final String HTTP_BIND_FORWARDED_HOST_NAME = "httpbind.forwarded.host.name";
// http binding CORS default properties
public static final String HTTP_BIND_CORS_ENABLED = "httpbind.CORS.enabled";
public static final boolean HTTP_BIND_CORS_ENABLED_DEFAULT = true;
public static final String HTTP_BIND_CORS_ALLOW_ORIGIN = "httpbind.CORS.domains";
public static final String HTTP_BIND_CORS_ALLOW_ORIGIN_DEFAULT = "*";
public static final String HTTP_BIND_CORS_ALLOW_METHODS_DEFAULT = "PROPFIND, PROPPATCH, COPY, MOVE, DELETE, MKCOL, LOCK, UNLOCK, PUT, GETLIB, VERSION-CONTROL, CHECKIN, CHECKOUT, UNCHECKOUT, REPORT, UPDATE, CANCELUPLOAD, HEAD, OPTIONS, GET, POST";
public static final String HTTP_BIND_CORS_ALLOW_HEADERS_DEFAULT = "Overwrite, Destination, Content-Type, Depth, User-Agent, X-File-Size, X-Requested-With, If-Modified-Since, X-File-Name, Cache-Control";
public static final String HTTP_BIND_CORS_MAX_AGE_DEFAULT = "86400";
public static final String HTTP_BIND_REQUEST_HEADER_SIZE = "httpbind.request.header.size";
public static final int HTTP_BIND_REQUEST_HEADER_SIZE_DEFAULT = 32768;
public static Map<String, Boolean> HTTP_BIND_ALLOWED_ORIGINS = new HashMap<>();
private static HttpBindManager instance = new HttpBindManager();
// Compression "optional" by default; use "disabled" to disable compression (restart required)
// When enabled, http response will be compressed if the http request includes an
// "Accept" header with a value of "gzip" and/or "deflate"
private static boolean isCompressionEnabled = !(JiveGlobals.getProperty(
ConnectionSettings.Client.COMPRESSION_SETTINGS, Connection.CompressionPolicy.optional.toString())
.equalsIgnoreCase(Connection.CompressionPolicy.disabled.toString()));
private Server httpBindServer;
private int bindPort;
private int bindSecurePort;
private Connector httpConnector;
private Connector httpsConnector;
private CertificateListener certificateListener;
private HttpSessionManager httpSessionManager;
private ContextHandlerCollection contexts;
// is all orgin allowed flag
private boolean allowAllOrigins;
public static HttpBindManager getInstance() {
return instance;
}
private HttpBindManager() {
// JSP 2.0 uses commons-logging, so also override that implementation.
System.setProperty("org.apache.commons.logging.LogFactory", "org.jivesoftware.util.log.util.CommonsLogFactory");
JiveGlobals.migrateProperty(HTTP_BIND_ENABLED);
JiveGlobals.migrateProperty(HTTP_BIND_PORT);
JiveGlobals.migrateProperty(HTTP_BIND_SECURE_PORT);
JiveGlobals.migrateProperty(HTTP_BIND_THREADS);
JiveGlobals.migrateProperty(HTTP_BIND_FORWARDED);
JiveGlobals.migrateProperty(HTTP_BIND_FORWARDED_FOR);
JiveGlobals.migrateProperty(HTTP_BIND_FORWARDED_SERVER);
JiveGlobals.migrateProperty(HTTP_BIND_FORWARDED_HOST);
JiveGlobals.migrateProperty(HTTP_BIND_FORWARDED_HOST_NAME);
JiveGlobals.migrateProperty(HTTP_BIND_CORS_ENABLED);
JiveGlobals.migrateProperty(HTTP_BIND_CORS_ALLOW_ORIGIN);
JiveGlobals.migrateProperty(HTTP_BIND_REQUEST_HEADER_SIZE);
PropertyEventDispatcher.addListener(new HttpServerPropertyListener());
this.httpSessionManager = new HttpSessionManager();
// we need to initialise contexts at constructor time in order for plugins to add their contexts before start()
contexts = new ContextHandlerCollection();
// setup the cache for the allowed origins
this.setupAllowedOriginsMap();
}
public void start() {
certificateListener = new CertificateListener();
CertificateManager.addListener(certificateListener);
if (!isHttpBindServiceEnabled()) {
return;
}
bindPort = getHttpBindUnsecurePort();
bindSecurePort = getHttpBindSecurePort();
configureHttpBindServer(bindPort, bindSecurePort);
try {
httpBindServer.start();
Log.info("HTTP bind service started");
}
catch (Exception e) {
Log.error("Error starting HTTP bind service", e);
}
}
public void stop() {
CertificateManager.removeListener(certificateListener);
if (httpBindServer != null) {
try {
httpBindServer.stop();
Log.info("HTTP bind service stopped");
}
catch (Exception e) {
Log.error("Error stopping HTTP bind service", e);
}
httpBindServer = null;
}
}
public HttpSessionManager getSessionManager() {
return httpSessionManager;
}
private boolean isHttpBindServiceEnabled() {
return JiveGlobals.getBooleanProperty(HTTP_BIND_ENABLED, HTTP_BIND_ENABLED_DEFAULT);
}
private void createConnector(int port, int bindThreads) {
httpConnector = null;
if (port > 0) {
HttpConfiguration httpConfig = new HttpConfiguration();
configureProxiedConnector(httpConfig);
ServerConnector connector = new ServerConnector(httpBindServer, null, null, null, -1, bindThreads,
new HttpConnectionFactory(httpConfig));
// Listen on a specific network interface if it has been set.
connector.setHost(getBindInterface());
connector.setPort(port);
httpConnector = connector;
}
}
private void createSSLConnector(int securePort, int bindThreads) {
httpsConnector = null;
try {
final IdentityStoreConfig identityStoreConfig = SSLConfig.getInstance().getIdentityStoreConfig( Purpose.BOSH_C2S );
final KeyStore keyStore = identityStoreConfig.getStore();
if (securePort > 0 && identityStoreConfig.getStore().aliases().hasMoreElements() ) {
if ( !identityStoreConfig.containsDomainCertificate( "RSA" ) ) {
Log.warn("HTTP binding: Using RSA certificates but they are not valid for " +
"the hosted domain");
}
final TrustStoreConfig trustStoreConfig = SSLConfig.getInstance().getTrustStoreConfig( Purpose.BOSH_C2S );
final SslContextFactory sslContextFactory = new SslContextFactory();
sslContextFactory.setTrustStorePath( trustStoreConfig.getCanonicalPath() );
sslContextFactory.setTrustStorePassword( trustStoreConfig.getPassword() );
sslContextFactory.setTrustStoreType( trustStoreConfig.getType() );
sslContextFactory.setKeyStorePath( identityStoreConfig.getCanonicalPath() );
sslContextFactory.setKeyStorePassword( identityStoreConfig.getPassword() );
sslContextFactory.setKeyStoreType( identityStoreConfig.getType() );
sslContextFactory.addExcludeProtocols( "SSLv3" );
// Set policy for checking client certificates
String certPol = JiveGlobals.getProperty(HTTP_BIND_AUTH_PER_CLIENTCERT_POLICY, "disabled");
if(certPol.equals("needed")) {
sslContextFactory.setNeedClientAuth(true);
sslContextFactory.setWantClientAuth(true);
} else if(certPol.equals("wanted")) {
sslContextFactory.setNeedClientAuth(false);
sslContextFactory.setWantClientAuth(true);
} else {
sslContextFactory.setNeedClientAuth(false);
sslContextFactory.setWantClientAuth(false);
}
HttpConfiguration httpsConfig = new HttpConfiguration();
httpsConfig.setSecureScheme("https");
httpsConfig.setSecurePort(securePort);
configureProxiedConnector(httpsConfig);
httpsConfig.addCustomizer(new SecureRequestCustomizer());
ServerConnector sslConnector = null;
if ("npn".equals(JiveGlobals.getXMLProperty("spdy.protocol", "")))
{
sslConnector = new HTTPSPDYServerConnector(httpBindServer, sslContextFactory);
} else {
sslConnector = new ServerConnector(httpBindServer, null, null, null, -1, bindThreads,
new SslConnectionFactory(sslContextFactory, "http/1.1"), new HttpConnectionFactory(httpsConfig));
}
sslConnector.setHost(getBindInterface());
sslConnector.setPort(securePort);
httpsConnector = sslConnector;
}
}
catch (Exception e) {
Log.error("Error creating SSL connector for Http bind", e);
}
}
private void configureProxiedConnector(HttpConfiguration httpConfig) {
// Check to see if we are deployed behind a proxy
// Refer to http://eclipse.org/jetty/documentation/current/configuring-connectors.html
if (isXFFEnabled()) {
ForwardedRequestCustomizer customizer = new ForwardedRequestCustomizer();
// default: "X-Forwarded-For"
String forwardedForHeader = getXFFHeader();
if (forwardedForHeader != null) {
customizer.setForwardedForHeader(forwardedForHeader);
}
// default: "X-Forwarded-Server"
String forwardedServerHeader = getXFFServerHeader();
if (forwardedServerHeader != null) {
customizer.setForwardedServerHeader(forwardedServerHeader);
}
// default: "X-Forwarded-Host"
String forwardedHostHeader = getXFFHostHeader();
if (forwardedHostHeader != null) {
customizer.setForwardedHostHeader(forwardedHostHeader);
}
// default: none
String hostName = getXFFHostName();
if (hostName != null) {
customizer.setHostHeader(hostName);
}
httpConfig.addCustomizer(customizer);
}
httpConfig.setRequestHeaderSize(JiveGlobals.getIntProperty(HTTP_BIND_REQUEST_HEADER_SIZE, HTTP_BIND_REQUEST_HEADER_SIZE_DEFAULT));
}
private String getBindInterface() {
String interfaceName = JiveGlobals.getXMLProperty("network.interface");
String bindInterface = null;
if (interfaceName != null) {
if (interfaceName.trim().length() > 0) {
bindInterface = interfaceName;
}
}
return bindInterface;
}
/**
* Returns true if the HTTP binding server is currently enabled.
*
* @return true if the HTTP binding server is currently enabled.
*/
public boolean isHttpBindEnabled() {
return httpBindServer != null && httpBindServer.isRunning();
}
/**
* Returns true if a listener on the HTTP binding port is running.
*
* @return true if a listener on the HTTP binding port is running.
*/
public boolean isHttpBindActive() {
return httpConnector != null && httpConnector.isRunning();
}
/**
* Returns true if a listener on the HTTPS binding port is running.
*
* @return true if a listener on the HTTPS binding port is running.
*/
public boolean isHttpsBindActive() {
return httpsConnector != null && httpsConnector.isRunning();
}
public String getHttpBindUnsecureAddress() {
return "http://" + XMPPServer.getInstance().getServerInfo().getXMPPDomain() + ":" +
bindPort + "/http-bind/";
}
public String getHttpBindSecureAddress() {
return "https://" + XMPPServer.getInstance().getServerInfo().getXMPPDomain() + ":" +
bindSecurePort + "/http-bind/";
}
public String getJavaScriptUrl() {
return "http://" + XMPPServer.getInstance().getServerInfo().getXMPPDomain() + ":" +
bindPort + "/scripts/";
}
// http binding CORS support start
private void setupAllowedOriginsMap() {
String originString = getCORSAllowOrigin();
if (originString.equals(HTTP_BIND_CORS_ALLOW_ORIGIN_DEFAULT)) {
allowAllOrigins = true;
} else {
allowAllOrigins = false;
String[] origins = originString.split(",");
// reset the cache
HTTP_BIND_ALLOWED_ORIGINS.clear();
for (String str : origins) {
HTTP_BIND_ALLOWED_ORIGINS.put(str, true);
}
}
}
public boolean isCORSEnabled() {
return JiveGlobals.getBooleanProperty(HTTP_BIND_CORS_ENABLED, HTTP_BIND_CORS_ENABLED_DEFAULT);
}
public void setCORSEnabled(Boolean value) {
if (value != null)
JiveGlobals.setProperty(HTTP_BIND_CORS_ENABLED, String.valueOf(value));
}
public String getCORSAllowOrigin() {
return JiveGlobals.getProperty(HTTP_BIND_CORS_ALLOW_ORIGIN , HTTP_BIND_CORS_ALLOW_ORIGIN_DEFAULT);
}
public void setCORSAllowOrigin(String origins) {
if (origins == null || origins.trim().length() == 0)
origins = HTTP_BIND_CORS_ALLOW_ORIGIN_DEFAULT;
else {
origins = origins.replaceAll("\\s+", "");
}
JiveGlobals.setProperty(HTTP_BIND_CORS_ALLOW_ORIGIN, origins);
setupAllowedOriginsMap();
}
public boolean isAllOriginsAllowed() {
return allowAllOrigins;
}
public boolean isThisOriginAllowed(String origin) {
return HTTP_BIND_ALLOWED_ORIGINS.get(origin) != null;
}
// http binding CORS support end
public boolean isXFFEnabled() {
return JiveGlobals.getBooleanProperty(HTTP_BIND_FORWARDED, false);
}
public void setXFFEnabled(boolean enabled) {
JiveGlobals.setProperty(HTTP_BIND_FORWARDED, String.valueOf(enabled));
}
public String getXFFHeader() {
return JiveGlobals.getProperty(HTTP_BIND_FORWARDED_FOR);
}
public void setXFFHeader(String header) {
if (header == null || header.trim().length() == 0) {
JiveGlobals.deleteProperty(HTTP_BIND_FORWARDED_FOR);
} else {
JiveGlobals.setProperty(HTTP_BIND_FORWARDED_FOR, header);
}
}
public String getXFFServerHeader() {
return JiveGlobals.getProperty(HTTP_BIND_FORWARDED_SERVER);
}
public void setXFFServerHeader(String header) {
if (header == null || header.trim().length() == 0) {
JiveGlobals.deleteProperty(HTTP_BIND_FORWARDED_SERVER);
} else {
JiveGlobals.setProperty(HTTP_BIND_FORWARDED_SERVER, header);
}
}
public String getXFFHostHeader() {
return JiveGlobals.getProperty(HTTP_BIND_FORWARDED_HOST);
}
public void setXFFHostHeader(String header) {
if (header == null || header.trim().length() == 0) {
JiveGlobals.deleteProperty(HTTP_BIND_FORWARDED_HOST);
} else {
JiveGlobals.setProperty(HTTP_BIND_FORWARDED_HOST, header);
}
}
public String getXFFHostName() {
return JiveGlobals.getProperty(HTTP_BIND_FORWARDED_HOST_NAME);
}
public void setXFFHostName(String name) {
if (name == null || name.trim().length() == 0) {
JiveGlobals.deleteProperty(HTTP_BIND_FORWARDED_HOST_NAME);
} else {
JiveGlobals.setProperty(HTTP_BIND_FORWARDED_HOST_NAME, name);
}
}
public void setHttpBindEnabled(boolean isEnabled) {
JiveGlobals.setProperty(HTTP_BIND_ENABLED, String.valueOf(isEnabled));
}
/**
* Set the ports on which the HTTP binding service will be running.
*
* @param unsecurePort the unsecured connection port which clients can connect to.
* @param securePort the secured connection port which clients can connect to.
* @throws Exception when there is an error configuring the HTTP binding ports.
*/
public void setHttpBindPorts(int unsecurePort, int securePort) throws Exception {
if (unsecurePort != HTTP_BIND_PORT_DEFAULT) {
JiveGlobals.setProperty(HTTP_BIND_PORT, String.valueOf(unsecurePort));
}
else {
JiveGlobals.deleteProperty(HTTP_BIND_PORT);
}
if (securePort != HTTP_BIND_SECURE_PORT_DEFAULT) {
JiveGlobals.setProperty(HTTP_BIND_SECURE_PORT, String.valueOf(securePort));
}
else {
JiveGlobals.deleteProperty(HTTP_BIND_SECURE_PORT);
}
}
/**
* Starts an HTTP Bind server on the specified port and secure port.
*
* @param port the port to start the normal (unsecured) HTTP Bind service on.
* @param securePort the port to start the TLS (secure) HTTP Bind service on.
*/
private synchronized void configureHttpBindServer(int port, int securePort) {
// this is the number of threads allocated to each connector/port
int bindThreads = JiveGlobals.getIntProperty(HTTP_BIND_THREADS, HTTP_BIND_THREADS_DEFAULT);
final QueuedThreadPool tp = new QueuedThreadPool();
tp.setName("Jetty-QTP-BOSH");
httpBindServer = new Server(tp);
if (JMXManager.isEnabled()) {
JMXManager jmx = JMXManager.getInstance();
httpBindServer.addBean(jmx.getContainer());
}
createConnector(port, bindThreads);
createSSLConnector(securePort, bindThreads);
if (httpConnector == null && httpsConnector == null) {
httpBindServer = null;
return;
}
if (httpConnector != null) {
httpBindServer.addConnector(httpConnector);
}
if (httpsConnector != null) {
httpBindServer.addConnector(httpsConnector);
}
//contexts = new ContextHandlerCollection();
// TODO implement a way to get plugins to add their their web services to contexts
createBoshHandler(contexts, "/http-bind");
createCrossDomainHandler(contexts, "/crossdomain.xml");
loadStaticDirectory(contexts);
HandlerCollection collection = new HandlerCollection();
httpBindServer.setHandler(collection);
collection.setHandlers(new Handler[] { contexts, new DefaultHandler() });
}
private void createBoshHandler(ContextHandlerCollection contexts, String boshPath)
{
ServletContextHandler context = new ServletContextHandler(contexts, boshPath, ServletContextHandler.SESSIONS);
// Ensure the JSP engine is initialized correctly (in order to be able to cope with Tomcat/Jasper precompiled JSPs).
final List<ContainerInitializer> initializers = new ArrayList<>();
initializers.add(new ContainerInitializer(new JettyJasperInitializer(), null));
context.setAttribute("org.eclipse.jetty.containerInitializers", initializers);
context.setAttribute(InstanceManager.class.getName(), new SimpleInstanceManager());
context.addServlet(new ServletHolder(new HttpBindServlet()),"/*");
if (isHttpCompressionEnabled()) {
Filter gzipFilter = new AsyncGzipFilter() {
@Override
public void init(FilterConfig config) throws ServletException {
super.init(config);
_methods.add(HttpMethod.POST.asString());
Log.info("Installed response compression filter");
}
};
FilterHolder filterHolder = new FilterHolder();
filterHolder.setFilter(gzipFilter);
context.addFilter(filterHolder, "/*", EnumSet.of(DispatcherType.REQUEST));
}
}
// NOTE: enabled by default
private boolean isHttpCompressionEnabled() {
return isCompressionEnabled;
}
private void createCrossDomainHandler(ContextHandlerCollection contexts, String crossPath)
{
ServletContextHandler context = new ServletContextHandler(contexts, crossPath, ServletContextHandler.SESSIONS);
// Ensure the JSP engine is initialized correctly (in order to be able to cope with Tomcat/Jasper precompiled JSPs).
final List<ContainerInitializer> initializers = new ArrayList<>();
initializers.add(new ContainerInitializer(new JettyJasperInitializer(), null));
context.setAttribute("org.eclipse.jetty.containerInitializers", initializers);
context.setAttribute(InstanceManager.class.getName(), new SimpleInstanceManager());
context.addServlet(new ServletHolder(new FlashCrossDomainServlet()),"");
}
private void loadStaticDirectory(ContextHandlerCollection contexts) {
File spankDirectory = new File(JiveGlobals.getHomeDirectory() + File.separator
+ "resources" + File.separator + "spank");
if (spankDirectory.exists()) {
if (spankDirectory.canRead()) {
WebAppContext context = new WebAppContext(contexts, spankDirectory.getPath(), "/");
context.setWelcomeFiles(new String[]{"index.html"});
}
else {
Log.warn("Openfire cannot read the directory: " + spankDirectory);
}
}
}
public ContextHandlerCollection getContexts() {
return contexts;
}
private void doEnableHttpBind(boolean shouldEnable) {
if (shouldEnable && httpBindServer == null) {
start();
}
else if (!shouldEnable && httpBindServer != null) {
stop();
}
}
/**
* Returns the HTTP binding port which does not use SSL.
*
* @return the HTTP binding port which does not use SSL.
*/
public int getHttpBindUnsecurePort() {
return JiveGlobals.getIntProperty(HTTP_BIND_PORT, HTTP_BIND_PORT_DEFAULT);
}
/**
* Returns the HTTP binding port which uses SSL.
*
* @return the HTTP binding port which uses SSL.
*/
public int getHttpBindSecurePort() {
return JiveGlobals.getIntProperty(HTTP_BIND_SECURE_PORT, HTTP_BIND_SECURE_PORT_DEFAULT);
}
/**
* Returns true if script syntax is enabled. Script syntax allows BOSH to be used in
* environments where clients may be restricted to using a particular server. Instead of using
* standard HTTP Post requests to transmit data, HTTP Get requests are used.
*
* @return true if script syntax is enabled.
* @see <a href="http://www.xmpp.org/extensions/xep-0124.html#script">BOSH: Alternative Script
* Syntax</a>
*/
public boolean isScriptSyntaxEnabled() {
return JiveGlobals.getBooleanProperty("xmpp.httpbind.scriptSyntax.enabled", false);
}
/**
* Enables or disables script syntax.
*
* @param isEnabled true to enable script syntax and false to disable it.
* @see #isScriptSyntaxEnabled()
* @see <a href="http://www.xmpp.org/extensions/xep-0124.html#script">BOSH: Alternative Script
* Syntax</a>
*/
public void setScriptSyntaxEnabled(boolean isEnabled) {
final String property = "xmpp.httpbind.scriptSyntax.enabled";
if(!isEnabled) {
JiveGlobals.deleteProperty(property);
}
else {
JiveGlobals.setProperty(property, String.valueOf(isEnabled));
}
}
private void setUnsecureHttpBindPort(int value) {
if (value == bindPort) {
return;
}
restartServer();
}
private void setSecureHttpBindPort(int value) {
if (value == bindSecurePort) {
return;
}
restartServer();
}
private synchronized void restartServer() {
stop();
start();
}
/** Listens for changes to Jive properties that affect the HTTP server manager. */
private class HttpServerPropertyListener implements PropertyEventListener {
@Override
public void propertySet(String property, Map<String, Object> params) {
if (property.equalsIgnoreCase(HTTP_BIND_ENABLED)) {
doEnableHttpBind(Boolean.valueOf(params.get("value").toString()));
}
else if (property.equalsIgnoreCase(HTTP_BIND_PORT)) {
int value;
try {
value = Integer.valueOf(params.get("value").toString());
}
catch (NumberFormatException ne) {
JiveGlobals.deleteProperty(HTTP_BIND_PORT);
return;
}
setUnsecureHttpBindPort(value);
}
else if (property.equalsIgnoreCase(HTTP_BIND_SECURE_PORT)) {
int value;
try {
value = Integer.valueOf(params.get("value").toString());
}
catch (NumberFormatException ne) {
JiveGlobals.deleteProperty(HTTP_BIND_SECURE_PORT);
return;
}
setSecureHttpBindPort(value);
}
else if (HTTP_BIND_AUTH_PER_CLIENTCERT_POLICY.equalsIgnoreCase( property )) {
restartServer();
}
}
@Override
public void propertyDeleted(String property, Map<String, Object> params) {
if (property.equalsIgnoreCase(HTTP_BIND_ENABLED)) {
doEnableHttpBind(HTTP_BIND_ENABLED_DEFAULT);
}
else if (property.equalsIgnoreCase(HTTP_BIND_PORT)) {
setUnsecureHttpBindPort(HTTP_BIND_PORT_DEFAULT);
}
else if (property.equalsIgnoreCase(HTTP_BIND_SECURE_PORT)) {
setSecureHttpBindPort(HTTP_BIND_SECURE_PORT_DEFAULT);
}
else if (HTTP_BIND_AUTH_PER_CLIENTCERT_POLICY.equalsIgnoreCase( property )) {
restartServer();
}
}
@Override
public void xmlPropertySet(String property, Map<String, Object> params) {
}
@Override
public void xmlPropertyDeleted(String property, Map<String, Object> params) {
}
}
private class CertificateListener implements CertificateEventListener {
@Override
public void certificateCreated(KeyStore keyStore, String alias, X509Certificate cert) {
// If new certificate is RSA then (re)start the HTTPS service
if ("RSA".equals(cert.getPublicKey().getAlgorithm())) {
restartServer();
}
}
@Override
public void certificateDeleted(KeyStore keyStore, String alias) {
restartServer();
}
@Override
public void certificateSigned(KeyStore keyStore, String alias,
List<X509Certificate> certificates) {
// If new certificate is RSA then (re)start the HTTPS service
if ("RSA".equals(certificates.get(0).getPublicKey().getAlgorithm())) {
restartServer();
}
}
}
}
......@@ -54,7 +54,7 @@ public abstract class CertificateStoreConfig
{
try
{
this.canonicalPath = SSLConfig.canonicalize( path );
this.canonicalPath = Purpose.canonicalize( path );
final File file = new File( canonicalPath );
if ( createIfAbsent && !file.exists() )
......
package org.jivesoftware.openfire.keystore;
import org.jivesoftware.util.JiveGlobals;
import java.io.File;
import java.io.IOException;
/**
* Potential intended usages for keystores
* Potential intended usages (for TLS connectivity).
*
* @author Guus der Kinderen, guus.der.kinderen@gmail.com
*/
public enum Purpose
{
/**
* Identification of this Openfire instance used by regular socket-based connections.
* Socket-based server-to-server (XMPP federation) connectivity.
*/
SOCKETBASED_IDENTITYSTORE( false ),
SOCKET_S2S( "xmpp.socket.ssl.", null ),
/**
* Identification of remote servers that you choose to trust, applies to server-to-server federation via regular socket-based connections.
* Socket-based client connectivity.
*/
SOCKETBASED_S2S_TRUSTSTORE( true ),
SOCKET_C2S( "xmpp.socket.ssl.client.", null ),
/**
* Identification of clients that you choose to trust, applies to mutual authentication via regular socket-based connections.
* BOSH (HTTP-bind) based client connectivity.
*/
SOCKETBASED_C2S_TRUSTSTORE( true ),
BOSH_C2S( "xmpp.bosh.ssl.client.", SOCKET_C2S),
/**
* Identification of this Openfire instance used by regular BOSH (HTTP-bind) connections.
* Generic administrative services (eg: user providers).
*/
BOSHBASED_IDENTITYSTORE( false ),
ADMIN( "admin.ssl.", SOCKET_S2S),
/**
* Identification of clients that you choose to trust, applies to mutual authentication via BOSH (HTTP-bind) connections.
* Openfire web-admin console.
*/
BOSHBASED_C2S_TRUSTSTORE( true ),
WEBADMIN( "admin.web.ssl.", ADMIN);
/**
* Identification of this Openfire instance used by connections to administrative services (eg: user providers).
*/
ADMINISTRATIVE_IDENTITYSTORE( false ),
String prefix;
Purpose fallback;
Purpose( String prefix, Purpose fallback) {
this.prefix = prefix;
this.fallback = fallback;
}
/**
* Identification of remote applications/servers that provide administrative functionality (eg: user providers).
*/
ADMINISTRATIVE_TRUSTSTORE( true ),
public String getPrefix()
{
return prefix;
}
/**
* Openfire web-admin console.
*/
WEBADMIN_IDENTITYSTORE( false ),
public Purpose getFallback()
{
return fallback;
}
/**
* Openfire web-admin console.
*/
WEBADMIN_TRUSTSTORE( true );
public String getIdentityStoreType()
{
final String propertyName = prefix + "storeType";
final String defaultValue = "jks";
private final boolean isTrustStore;
if ( fallback == null )
{
return JiveGlobals.getProperty( propertyName, defaultValue ).trim();
}
else
{
return JiveGlobals.getProperty( propertyName, fallback.getIdentityStoreType() ).trim();
}
}
Purpose( boolean isTrustStore )
public String getTrustStoreType()
{
this.isTrustStore = isTrustStore;
return getIdentityStoreType();
}
public boolean isIdentityStore()
public String getIdentityStorePassword()
{
return !isTrustStore;
final String propertyName = prefix + "keypass";
final String defaultValue = "changeit";
if ( fallback == null )
{
return JiveGlobals.getProperty( propertyName, defaultValue ).trim();
}
else
{
return JiveGlobals.getProperty( propertyName, fallback.getIdentityStorePassword() ).trim();
}
}
public boolean isTrustStore()
public String getTrustStorePassword()
{
return isTrustStore;
final String propertyName = prefix + "trustpass";
final String defaultValue = "changeit";
if ( fallback == null )
{
return JiveGlobals.getProperty( propertyName, defaultValue ).trim();
}
else
{
return JiveGlobals.getProperty( propertyName, fallback.getTrustStorePassword() ).trim();
}
}
public boolean acceptSelfSigned()
{
// TODO these are new properties! Deprecate (migrate?) all existing 'accept-selfsigned properties' (Eg: org.jivesoftware.openfire.session.ConnectionSettings.Server.TLS_ACCEPT_SELFSIGNED_CERTS )
final String propertyName = prefix + "certificate.accept-selfsigned";
final boolean defaultValue = false;
if ( fallback == null )
{
return JiveGlobals.getBooleanProperty( propertyName, defaultValue );
}
else
{
return JiveGlobals.getBooleanProperty( propertyName, fallback.acceptSelfSigned() );
}
}
public boolean verifyValidity()
{
// TODO these are new properties! Deprecate (migrate?) all existing 'verify / verify-validity properties' (Eg: org.jivesoftware.openfire.session.ConnectionSettings.Server.TLS_CERTIFICATE_VERIFY_VALIDITY )
final String propertyName = prefix + "certificate.verify.validity";
final boolean defaultValue = true;
if ( fallback == null )
{
return JiveGlobals.getBooleanProperty( propertyName, defaultValue );
}
else
{
return JiveGlobals.getBooleanProperty( propertyName, fallback.acceptSelfSigned() );
}
}
public String getIdentityStoreLocation() throws IOException
{
return canonicalize( getIdentityStoreLocation() );
}
public String getIdentityStoreLocationNonCanonicalized()
{
final String propertyName = prefix + "keystore";
final String defaultValue = "resources" + File.separator + "security" + File.separator + "keystore";
if ( fallback == null )
{
return JiveGlobals.getProperty( propertyName, defaultValue ).trim();
}
else
{
return JiveGlobals.getProperty( propertyName, fallback.getIdentityStoreLocationNonCanonicalized() ).trim();
}
}
public String getTrustStoreLocation() throws IOException
{
return canonicalize( getTrustStoreLocation() );
}
public String getTrustStoreLocationNonCanonicalized()
{
final String propertyName = prefix + "truststore";
final String defaultValue = "resources" + File.separator + "security" + File.separator + "truststore";
if ( fallback == null )
{
return JiveGlobals.getProperty( propertyName, defaultValue ).trim();
}
else
{
return JiveGlobals.getProperty( propertyName, fallback.getTrustStoreLocationNonCanonicalized() ).trim();
}
}
public static String canonicalize( String path ) throws IOException
{
File file = new File( path );
if (!file.isAbsolute()) {
file = new File( JiveGlobals.getHomeDirectory() + File.separator + path );
}
return file.getCanonicalPath();
}
}
/**
* $RCSfile$
* $Revision: $
* $Date: $
*
* Copyright (C) 2005-2008 Jive Software. 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.net;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.regex.Pattern;
import javax.security.sasl.Sasl;
import javax.security.sasl.SaslException;
import javax.security.sasl.SaslServer;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.Namespace;
import org.dom4j.QName;
import org.jivesoftware.openfire.Connection;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.auth.AuthFactory;
import org.jivesoftware.openfire.auth.AuthToken;
import org.jivesoftware.openfire.auth.AuthorizationManager;
import org.jivesoftware.openfire.keystore.Purpose;
import org.jivesoftware.openfire.lockout.LockOutManager;
import org.jivesoftware.openfire.session.ClientSession;
import org.jivesoftware.openfire.session.ConnectionSettings;
import org.jivesoftware.openfire.session.IncomingServerSession;
import org.jivesoftware.openfire.session.LocalClientSession;
import org.jivesoftware.openfire.session.LocalIncomingServerSession;
import org.jivesoftware.openfire.session.LocalSession;
import org.jivesoftware.openfire.session.Session;
import org.jivesoftware.util.CertificateManager;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* SASLAuthentication is responsible for returning the available SASL mechanisms to use and for
* actually performing the SASL authentication.<p>
*
* The list of available SASL mechanisms is determined by:
* <ol>
* <li>The type of {@link org.jivesoftware.openfire.user.UserProvider} being used since
* some SASL mechanisms require the server to be able to retrieve user passwords</li>
* <li>Whether anonymous logins are enabled or not.</li>
* <li>Whether shared secret authentication is enabled or not.</li>
* <li>Whether the underlying connection has been secured or not.</li>
* </ol>
*
* @author Hao Chen
* @author Gaston Dombiak
*/
public class SASLAuthentication {
private static final Logger Log = LoggerFactory.getLogger(SASLAuthentication.class);
// http://stackoverflow.com/questions/8571501/how-to-check-whether-the-string-is-base64-encoded-or-not
// plus an extra regex alternative to catch a single equals sign ('=', see RFC 6120 6.4.2)
private static final Pattern BASE64_ENCODED = Pattern.compile("^(=|([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==))$");
private static final String SASL_NAMESPACE = "xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"";
private static Map<String, ElementType> typeMap = new TreeMap<>();
private static Set<String> mechanisms = null;
static {
initMechanisms();
}
public enum ElementType {
ABORT("abort"), AUTH("auth"), RESPONSE("response"), CHALLENGE("challenge"), FAILURE("failure"), UNDEF("");
private String name = null;
@Override
public String toString() {
return name;
}
private ElementType(String name) {
this.name = name;
typeMap.put(this.name, this);
}
public static ElementType valueof(String name) {
if (name == null) {
return UNDEF;
}
ElementType e = typeMap.get(name);
return e != null ? e : UNDEF;
}
}
private enum Failure {
ABORTED("aborted"),
ACCOUNT_DISABLED("account-disabled"),
CREDENTIALS_EXPIRED("credentials-expired"),
ENCRYPTION_REQUIRED("encryption-required"),
INCORRECT_ENCODING("incorrect-encoding"),
INVALID_AUTHZID("invalid-authzid"),
INVALID_MECHANISM("invalid-mechanism"),
MALFORMED_REQUEST("malformed-request"),
MECHANISM_TOO_WEAK("mechanism-too-weak"),
NOT_AUTHORIZED("not-authorized"),
TEMPORARY_AUTH_FAILURE("temporary-auth-failure");
private String name = null;
private Failure(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}
public enum Status {
/**
* Entity needs to respond last challenge. Session is still negotiating
* SASL authentication.
*/
needResponse,
/**
* SASL negotiation has failed. The entity may retry a few times before the connection
* is closed.
*/
failed,
/**
* SASL negotiation has been successful.
*/
authenticated;
}
/**
* Returns a string with the valid SASL mechanisms available for the specified session. If
* the session's connection is not secured then only include the SASL mechanisms that don't
* require TLS.
*
* @param session The current session
*
* @return a string with the valid SASL mechanisms available for the specified session.
*/
public static String getSASLMechanisms(LocalSession session) {
if (!(session instanceof ClientSession) && !(session instanceof IncomingServerSession)) {
return "";
}
Element mechs = getSASLMechanismsElement(session);
return mechs.asXML();
}
public static Element getSASLMechanismsElement(Session session) {
if (!(session instanceof ClientSession) && !(session instanceof IncomingServerSession)) {
return null;
}
Element mechs = DocumentHelper.createElement( new QName( "mechanisms",
new Namespace( "", "urn:ietf:params:xml:ns:xmpp-sasl" ) ) );
if (session instanceof LocalIncomingServerSession) {
// Server connections don't follow the same rules as clients
if (session.isSecure()) {
LocalIncomingServerSession svr = (LocalIncomingServerSession)session;
final KeyStore keyStore = SSLConfig.getStore( Purpose.SOCKETBASED_IDENTITYSTORE );
final KeyStore trustStore = SSLConfig.getStore( Purpose.SOCKETBASED_S2S_TRUSTSTORE );
final X509Certificate trusted = CertificateManager.getEndEntityCertificate( svr.getConnection().getPeerCertificates(), keyStore, trustStore );
boolean haveTrustedCertificate = trusted != null;
if (trusted != null && svr.getDefaultIdentity() != null) {
haveTrustedCertificate = verifyCertificate(trusted, svr.getDefaultIdentity());
}
if (haveTrustedCertificate) {
// Offer SASL EXTERNAL only if TLS has already been negotiated and the peer has a trusted cert.
Element mechanism = mechs.addElement("mechanism");
mechanism.setText("EXTERNAL");
}
}
}
else {
for (String mech : getSupportedMechanisms()) {
Element mechanism = mechs.addElement("mechanism");
mechanism.setText(mech);
}
}
return mechs;
}
/**
* Handles the SASL authentication packet. The entity may be sending an initial
* authentication request or a response to a challenge made by the server. The returned
* value indicates whether the authentication has finished either successfully or not or
* if the entity is expected to send a response to a challenge.
*
* @param session the session that is authenticating with the server.
* @param doc the stanza sent by the authenticating entity.
* @return value that indicates whether the authentication has finished either successfully
* or not or if the entity is expected to send a response to a challenge.
*/
public static Status handle(LocalSession session, Element doc) {
Status status;
String mechanism;
if (doc.getNamespace().asXML().equals(SASL_NAMESPACE)) {
ElementType type = ElementType.valueof(doc.getName());
switch (type) {
case ABORT:
authenticationFailed(session, Failure.ABORTED);
status = Status.failed;
break;
case AUTH:
mechanism = doc.attributeValue("mechanism");
// http://xmpp.org/rfcs/rfc6120.html#sasl-errors-invalid-mechanism
// The initiating entity did not specify a mechanism
if (mechanism == null) {
authenticationFailed(session, Failure.INVALID_MECHANISM);
status = Status.failed;
break;
}
// Store the requested SASL mechanism by the client
session.setSessionData("SaslMechanism", mechanism);
//Log.debug("SASLAuthentication.doHandshake() AUTH entered: "+mechanism);
if (mechanism.equalsIgnoreCase("ANONYMOUS") &&
mechanisms.contains("ANONYMOUS")) {
status = doAnonymousAuthentication(session);
}
else if (mechanism.equalsIgnoreCase("EXTERNAL")) {
status = doExternalAuthentication(session, doc);
}
else if (mechanisms.contains(mechanism)) {
// The selected SASL mechanism requires the server to send a challenge
// to the client
try {
Map<String, String> props = new TreeMap<>();
props.put(Sasl.QOP, "auth");
if (mechanism.equals("GSSAPI")) {
props.put(Sasl.SERVER_AUTH, "TRUE");
}
SaslServer ss = Sasl.createSaslServer(mechanism, "xmpp",
session.getServerName(), props,
new XMPPCallbackHandler());
if (ss == null) {
authenticationFailed(session, Failure.INVALID_MECHANISM);
return Status.failed;
}
// evaluateResponse doesn't like null parameter
byte[] token = new byte[0];
String value = doc.getTextTrim();
if (value.length() > 0) {
if (!BASE64_ENCODED.matcher(value).matches()) {
authenticationFailed(session, Failure.INCORRECT_ENCODING);
return Status.failed;
}
// If auth request includes a value then validate it
token = StringUtils.decodeBase64(value);
if (token == null) {
token = new byte[0];
}
}
if (mechanism.equals("DIGEST-MD5")) {
// RFC2831 (DIGEST-MD5) says the client MAY provide an initial response on subsequent
// authentication. Java SASL does not (currently) support this and thows an exception
// if we try. This violates the RFC, so we just strip any initial token.
token = new byte[0];
}
byte[] challenge = ss.evaluateResponse(token);
if (ss.isComplete()) {
authenticationSuccessful(session, ss.getAuthorizationID(),
challenge);
status = Status.authenticated;
}
else {
// Send the challenge
sendChallenge(session, challenge);
status = Status.needResponse;
}
session.setSessionData("SaslServer", ss);
}
catch (SaslException e) {
Log.info("User Login Failed. " + e.getMessage());
authenticationFailed(session, Failure.NOT_AUTHORIZED);
status = Status.failed;
}
}
else {
Log.warn("Client wants to do a MECH we don't support: '" +
mechanism + "'");
authenticationFailed(session, Failure.INVALID_MECHANISM);
status = Status.failed;
}
break;
case RESPONSE:
// Store the requested SASL mechanism by the client
mechanism = (String) session.getSessionData("SaslMechanism");
if (mechanism.equalsIgnoreCase("EXTERNAL")) {
status = doExternalAuthentication(session, doc);
}
else if (mechanism.equalsIgnoreCase("JIVE-SHAREDSECRET")) {
status = doSharedSecretAuthentication(session, doc);
}
else if (mechanisms.contains(mechanism)) {
SaslServer ss = (SaslServer) session.getSessionData("SaslServer");
if (ss != null) {
boolean ssComplete = ss.isComplete();
String response = doc.getTextTrim();
if (response.length() > 0) {
if (!BASE64_ENCODED.matcher(response).matches()) {
authenticationFailed(session, Failure.INCORRECT_ENCODING);
return Status.failed;
}
}
try {
if (ssComplete) {
authenticationSuccessful(session, ss.getAuthorizationID(),
null);
status = Status.authenticated;
}
else {
byte[] data = StringUtils.decodeBase64(response);
if (data == null) {
data = new byte[0];
}
byte[] challenge = ss.evaluateResponse(data);
if (ss.isComplete()) {
authenticationSuccessful(session, ss.getAuthorizationID(),
challenge);
status = Status.authenticated;
}
else {
// Send the challenge
sendChallenge(session, challenge);
status = Status.needResponse;
}
}
}
catch (SaslException e) {
Log.debug("SASLAuthentication: SaslException", e);
authenticationFailed(session, Failure.NOT_AUTHORIZED);
status = Status.failed;
}
}
else {
Log.error("SaslServer is null, should be valid object instead.");
authenticationFailed(session, Failure.NOT_AUTHORIZED);
status = Status.failed;
}
}
else {
Log.warn(
"Client responded to a MECH we don't support: '" + mechanism + "'");
authenticationFailed(session, Failure.INVALID_MECHANISM);
status = Status.failed;
}
break;
default:
authenticationFailed(session, Failure.NOT_AUTHORIZED);
status = Status.failed;
// Ignore
break;
}
}
else {
Log.debug("SASLAuthentication: Unknown namespace sent in auth element: " + doc.asXML());
authenticationFailed(session, Failure.MALFORMED_REQUEST);
status = Status.failed;
}
// Check if SASL authentication has finished so we can clean up temp information
if (status == Status.failed || status == Status.authenticated) {
// Remove the SaslServer from the Session
session.removeSessionData("SaslServer");
// Remove the requested SASL mechanism by the client
session.removeSessionData("SaslMechanism");
}
return status;
}
/**
* Returns true if shared secret authentication is enabled. Shared secret
* authentication creates an anonymous session, but requires that the authenticating
* entity know a shared secret key. The client sends a digest of the secret key,
* which is compared against a digest of the local shared key.
*
* @return true if shared secret authentication is enabled.
*/
public static boolean isSharedSecretAllowed() {
return JiveGlobals.getBooleanProperty("xmpp.auth.sharedSecretEnabled");
}
/**
* Sets whether shared secret authentication is enabled. Shared secret
* authentication creates an anonymous session, but requires that the authenticating
* entity know a shared secret key. The client sends a digest of the secret key,
* which is compared against a digest of the local shared key.
*
* @param sharedSecretAllowed true if shared secret authentication should be enabled.
*/
public static void setSharedSecretAllowed(boolean sharedSecretAllowed) {
JiveGlobals.setProperty("xmpp.auth.sharedSecretEnabled", sharedSecretAllowed ? "true" : "false");
}
/**
* Returns the shared secret value, or <tt>null</tt> if shared secret authentication is
* disabled. If this is the first time the shared secret value has been requested (and
* shared secret auth is enabled), the key will be randomly generated and stored in the
* property <tt>xmpp.auth.sharedSecret</tt>.
*
* @return the shared secret value.
*/
public static String getSharedSecret() {
if (!isSharedSecretAllowed()) {
return null;
}
String sharedSecret = JiveGlobals.getProperty("xmpp.auth.sharedSecret");
if (sharedSecret == null) {
sharedSecret = StringUtils.randomString(8);
JiveGlobals.setProperty("xmpp.auth.sharedSecret", sharedSecret);
}
return sharedSecret;
}
/**
* Returns true if the supplied digest matches the shared secret value. The digest
* must be an MD5 hash of the secret key, encoded as hex. This value is supplied
* by clients attempting shared secret authentication.
*
* @param digest the MD5 hash of the secret key, encoded as hex.
* @return true if authentication succeeds.
*/
public static boolean authenticateSharedSecret(String digest) {
if (!isSharedSecretAllowed()) {
return false;
}
String sharedSecert = getSharedSecret();
return StringUtils.hash(sharedSecert).equals( digest );
}
private static Status doAnonymousAuthentication(LocalSession session) {
if (XMPPServer.getInstance().getIQAuthHandler().isAnonymousAllowed()) {
// Verify that client can connect from his IP address
boolean forbidAccess = false;
try {
String hostAddress = session.getConnection().getHostAddress();
if (!LocalClientSession.getAllowedAnonymIPs().isEmpty() &&
!LocalClientSession.getAllowedAnonymIPs().containsKey(hostAddress)) {
byte[] address = session.getConnection().getAddress();
String range1 = (address[0] & 0xff) + "." + (address[1] & 0xff) + "." +
(address[2] & 0xff) +
".*";
String range2 = (address[0] & 0xff) + "." + (address[1] & 0xff) + ".*.*";
String range3 = (address[0] & 0xff) + ".*.*.*";
if (!LocalClientSession.getAllowedAnonymIPs().containsKey(range1) &&
!LocalClientSession.getAllowedAnonymIPs().containsKey(range2) &&
!LocalClientSession.getAllowedAnonymIPs().containsKey(range3)) {
forbidAccess = true;
}
}
} catch (UnknownHostException e) {
forbidAccess = true;
}
if (forbidAccess) {
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}
// Just accept the authentication :)
authenticationSuccessful(session, null, null);
return Status.authenticated;
}
else {
// anonymous login is disabled so close the connection
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}
}
private static Status doExternalAuthentication(LocalSession session, Element doc) {
// At this point the connection has already been secured using TLS
if (session instanceof IncomingServerSession) {
String hostname = doc.getTextTrim();
if (hostname == null || hostname.length() == 0) {
// No hostname was provided so send a challenge to get it
sendChallenge(session, new byte[0]);
return Status.needResponse;
}
hostname = new String(StringUtils.decodeBase64(hostname), StandardCharsets.UTF_8);
if (hostname.length() == 0) {
hostname = null;
}
try {
LocalIncomingServerSession svr = (LocalIncomingServerSession)session;
String defHostname = svr.getDefaultIdentity();
if (hostname == null) {
hostname = defHostname;
} else if (!hostname.equals(defHostname)) {
// Mismatch; really odd.
Log.info("SASLAuthentication rejected from='{}' and authzid='{}'", hostname, defHostname);
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}
} catch(Exception e) {
// Erm. Nothing?
}
if (hostname == null) {
Log.info("No authzid supplied for anonymous session.");
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}
// Check if certificate validation is disabled for s2s
// Flag that indicates if certificates of the remote server should be validated.
// Disabling certificate validation is not recommended for production environments.
boolean verify =
JiveGlobals.getBooleanProperty(ConnectionSettings.Server.TLS_CERTIFICATE_VERIFY, true);
if (!verify) {
authenticationSuccessful(session, hostname, null);
return Status.authenticated;
} else if(verifyCertificates(session.getConnection().getPeerCertificates(), hostname, true)) {
authenticationSuccessful(session, hostname, null);
LocalIncomingServerSession s = (LocalIncomingServerSession)session;
if (s != null) {
s.tlsAuth();
}
return Status.authenticated;
}
}
else if (session instanceof LocalClientSession) {
// Client EXTERNAL login
Log.debug("SASLAuthentication: EXTERNAL authentication via SSL certs for c2s connection");
// This may be null, we will deal with that later
String username = new String(StringUtils.decodeBase64(doc.getTextTrim()), StandardCharsets.UTF_8);
String principal = "";
ArrayList<String> principals = new ArrayList<>();
Connection connection = session.getConnection();
if (connection.getPeerCertificates().length < 1) {
Log.debug("SASLAuthentication: EXTERNAL authentication requested, but no certificates found.");
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}
final KeyStore keyStore = SSLConfig.getStore( Purpose.SOCKETBASED_IDENTITYSTORE );
final KeyStore trustStore = SSLConfig.getStore( Purpose.SOCKETBASED_C2S_TRUSTSTORE );
final X509Certificate trusted = CertificateManager.getEndEntityCertificate( connection.getPeerCertificates(), keyStore, trustStore );
if (trusted == null) {
Log.debug("SASLAuthentication: EXTERNAL authentication requested, but EE cert untrusted.");
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}
principals.addAll(CertificateManager.getClientIdentities(trusted));
if(principals.size() == 1) {
principal = principals.get(0);
} else if(principals.size() > 1) {
Log.debug("SASLAuthentication: EXTERNAL authentication: more than one principal found, using first.");
principal = principals.get(0);
} else {
Log.debug("SASLAuthentication: EXTERNAL authentication: No principals found.");
}
if (username == null || username.length() == 0) {
// No username was provided, according to XEP-0178 we need to:
// * attempt to get it from the cert first
// * have the server assign one
// There shouldn't be more than a few principals in here. One ideally
// We set principal to the first one in the list to have a sane default
// If this list is empty, then the cert had no identity at all, which
// will cause an authorization failure
for(String princ : principals) {
String u = AuthorizationManager.map(princ);
if(!u.equals(princ)) {
username = u;
principal = princ;
break;
}
}
if (username == null || username.length() == 0) {
// Still no username. Punt.
username = principal;
}
Log.debug("SASLAuthentication: no username requested, using "+username);
}
//Its possible that either/both username and principal are null here
//The providers should not allow a null authorization
if (AuthorizationManager.authorize(username,principal)) {
Log.debug("SASLAuthentication: "+principal+" authorized to "+username);
authenticationSuccessful(session, username, null);
return Status.authenticated;
}
} else {
Log.debug("SASLAuthentication: unknown session type. Cannot perform EXTERNAL authentication");
}
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}
public static boolean verifyCertificate(X509Certificate trustedCert, String hostname) {
for (String identity : CertificateManager.getServerIdentities(trustedCert)) {
// Verify that either the identity is the same as the hostname, or for wildcarded
// identities that the hostname ends with .domainspecified or -is- domainspecified.
if ((identity.startsWith("*.")
&& (hostname.endsWith(identity.replace("*.", "."))
|| hostname.equals(identity.replace("*.", ""))))
|| hostname.equals(identity)) {
return true;
}
}
return false;
}
/**
* @deprecated Use {@link #verifyCertificates(Certificate[], String, boolean)} instead.
*/
@Deprecated
public static boolean verifyCertificates(Certificate[] chain, String hostname) {
return verifyCertificates( chain, hostname, true );
}
public static boolean verifyCertificates(Certificate[] chain, String hostname, boolean isS2S) {
final KeyStore keyStore = SSLConfig.getStore( Purpose.SOCKETBASED_IDENTITYSTORE );
final KeyStore trustStore = SSLConfig.getStore( isS2S ? Purpose.SOCKETBASED_S2S_TRUSTSTORE : Purpose.SOCKETBASED_C2S_TRUSTSTORE );
final X509Certificate trusted = CertificateManager.getEndEntityCertificate( chain, keyStore, trustStore );
if (trusted != null) {
return verifyCertificate(trusted, hostname);
}
return false;
}
private static Status doSharedSecretAuthentication(LocalSession session, Element doc) {
String secretDigest;
String response = doc.getTextTrim();
if (response == null || response.length() == 0) {
// No info was provided so send a challenge to get it
sendChallenge(session, new byte[0]);
return Status.needResponse;
}
// Parse data and obtain username & password
String data = new String(StringUtils.decodeBase64(response), StandardCharsets.UTF_8);
StringTokenizer tokens = new StringTokenizer(data, "\0");
tokens.nextToken();
secretDigest = tokens.nextToken();
if (authenticateSharedSecret(secretDigest)) {
authenticationSuccessful(session, null, null);
return Status.authenticated;
}
// Otherwise, authentication failed.
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}
private static void sendChallenge(Session session, byte[] challenge) {
StringBuilder reply = new StringBuilder(250);
if (challenge == null) {
challenge = new byte[0];
}
String challenge_b64 = StringUtils.encodeBase64(challenge).trim();
if ("".equals(challenge_b64)) {
challenge_b64 = "="; // Must be padded if null
}
reply.append(
"<challenge xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
reply.append(challenge_b64);
reply.append("</challenge>");
session.deliverRawText(reply.toString());
}
private static void authenticationSuccessful(LocalSession session, String username,
byte[] successData) {
if (username != null && LockOutManager.getInstance().isAccountDisabled(username)) {
// Interception! This person is locked out, fail instead!
LockOutManager.getInstance().recordFailedLogin(username);
authenticationFailed(session, Failure.ACCOUNT_DISABLED);
return;
}
StringBuilder reply = new StringBuilder(80);
reply.append("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"");
if (successData != null) {
String successData_b64 = StringUtils.encodeBase64(successData).trim();
reply.append('>').append(successData_b64).append("</success>");
}
else {
reply.append("/>");
}
session.deliverRawText( reply.toString() );
// We only support SASL for c2s
if (session instanceof ClientSession) {
((LocalClientSession) session).setAuthToken(new AuthToken(username));
}
else if (session instanceof IncomingServerSession) {
String hostname = username;
// Add the validated domain as a valid domain. The remote server can
// now send packets from this address
((LocalIncomingServerSession) session).addValidatedDomain(hostname);
Log.info("Inbound Server {} authenticated (via TLS)", username);
}
}
private static void authenticationFailed(LocalSession session, Failure failure) {
StringBuilder reply = new StringBuilder(80);
reply.append("<failure xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"><");
reply.append(failure.toString());
reply.append("/></failure>");
session.deliverRawText(reply.toString());
// Give a number of retries before closing the connection
Integer retries = (Integer) session.getSessionData("authRetries");
if (retries == null) {
retries = 1;
}
else {
retries = retries + 1;
}
session.setSessionData("authRetries", retries);
if (retries >= JiveGlobals.getIntProperty("xmpp.auth.retries", 3) ) {
// Close the connection
session.close();
}
}
/**
* Adds a new SASL mechanism to the list of supported SASL mechanisms by the server. The
* new mechanism will be offered to clients and connection managers as stream features.<p>
*
* Note: this method simply registers the SASL mechanism to be advertised as a supported
* mechanism by Openfire. Actual SASL handling is done by Java itself, so you must add
* the provider to Java.
*
* @param mechanism the new SASL mechanism.
*/
public static void addSupportedMechanism(String mechanism) {
mechanisms.add(mechanism);
}
/**
* Removes a SASL mechanism from the list of supported SASL mechanisms by the server.
*
* @param mechanism the SASL mechanism to remove.
*/
public static void removeSupportedMechanism(String mechanism) {
mechanisms.remove(mechanism);
}
/**
* Returns the list of supported SASL mechanisms by the server. Note that Java may have
* support for more mechanisms but some of them may not be returned since a special setup
* is required that might be missing. Use {@link #addSupportedMechanism(String)} to add
* new SASL mechanisms.
*
* @return the list of supported SASL mechanisms by the server.
*/
public static Set<String> getSupportedMechanisms() {
Set<String> answer = new HashSet<>(mechanisms);
// Clean up not-available mechanisms
for (Iterator<String> it=answer.iterator(); it.hasNext();) {
String mech = it.next();
if (mech.equals("CRAM-MD5") || mech.equals("DIGEST-MD5")) {
// Check if the user provider in use supports passwords retrieval. Accessing
// to the users passwords will be required by the CallbackHandler
if (!AuthFactory.supportsPasswordRetrieval()) {
it.remove();
}
}
else if (mech.equals("SCRAM-SHA-1")) {
if (!AuthFactory.supportsPasswordRetrieval() && !AuthFactory.supportsScram()) {
it.remove();
}
}
else if (mech.equals("ANONYMOUS")) {
// Check anonymous is supported
if (!XMPPServer.getInstance().getIQAuthHandler().isAnonymousAllowed()) {
it.remove();
}
}
else if (mech.equals("JIVE-SHAREDSECRET")) {
// Check shared secret is supported
if (!isSharedSecretAllowed()) {
it.remove();
}
}
}
return answer;
}
private static void initMechanisms() {
// Convert XML based provider setup to Database based
JiveGlobals.migrateProperty("sasl.mechs");
JiveGlobals.migrateProperty("sasl.gssapi.debug");
JiveGlobals.migrateProperty("sasl.gssapi.config");
JiveGlobals.migrateProperty("sasl.gssapi.useSubjectCredsOnly");
mechanisms = new HashSet<>();
String available = JiveGlobals.getProperty("sasl.mechs");
if (available == null) {
mechanisms.add("ANONYMOUS");
mechanisms.add("PLAIN");
mechanisms.add("DIGEST-MD5");
mechanisms.add("CRAM-MD5");
mechanisms.add("SCRAM-SHA-1");
mechanisms.add("JIVE-SHAREDSECRET");
}
else {
StringTokenizer st = new StringTokenizer(available, " ,\t\n\r\f");
while (st.hasMoreTokens()) {
String mech = st.nextToken().toUpperCase();
// Check that the mech is a supported mechansim. Maybe we shouldnt check this and allow any?
if (mech.equals("ANONYMOUS") ||
mech.equals("PLAIN") ||
mech.equals("DIGEST-MD5") ||
mech.equals("CRAM-MD5") ||
mech.equals("SCRAM-SHA-1") ||
mech.equals("GSSAPI") ||
mech.equals("EXTERNAL") ||
mech.equals("JIVE-SHAREDSECRET"))
{
Log.debug("SASLAuthentication: Added " + mech + " to mech list");
mechanisms.add(mech);
}
}
if (mechanisms.contains("GSSAPI")) {
if (JiveGlobals.getProperty("sasl.gssapi.config") != null) {
System.setProperty("java.security.krb5.debug",
JiveGlobals.getProperty("sasl.gssapi.debug", "false"));
System.setProperty("java.security.auth.login.config",
JiveGlobals.getProperty("sasl.gssapi.config"));
System.setProperty("javax.security.auth.useSubjectCredsOnly",
JiveGlobals.getProperty("sasl.gssapi.useSubjectCredsOnly", "false"));
}
else {
//Not configured, remove the option.
Log.debug("SASLAuthentication: Removed GSSAPI from mech list");
mechanisms.remove("GSSAPI");
}
}
}
//Add our providers to the Security class
Security.addProvider(new org.jivesoftware.openfire.sasl.SaslProvider());
}
}
/**
* $RCSfile$
* $Revision: $
* $Date: $
*
* Copyright (C) 2005-2008 Jive Software. 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.net;
import java.io.UnsupportedEncodingException;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.regex.Pattern;
import javax.security.sasl.Sasl;
import javax.security.sasl.SaslException;
import javax.security.sasl.SaslServer;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.Namespace;
import org.dom4j.QName;
import org.jivesoftware.openfire.Connection;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.auth.AuthFactory;
import org.jivesoftware.openfire.auth.AuthToken;
import org.jivesoftware.openfire.auth.AuthorizationManager;
import org.jivesoftware.openfire.keystore.Purpose;
import org.jivesoftware.openfire.lockout.LockOutManager;
import org.jivesoftware.openfire.session.ClientSession;
import org.jivesoftware.openfire.session.ConnectionSettings;
import org.jivesoftware.openfire.session.IncomingServerSession;
import org.jivesoftware.openfire.session.LocalClientSession;
import org.jivesoftware.openfire.session.LocalIncomingServerSession;
import org.jivesoftware.openfire.session.LocalSession;
import org.jivesoftware.openfire.session.Session;
import org.jivesoftware.util.CertificateManager;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* SASLAuthentication is responsible for returning the available SASL mechanisms to use and for
* actually performing the SASL authentication.<p>
*
* The list of available SASL mechanisms is determined by:
* <ol>
* <li>The type of {@link org.jivesoftware.openfire.user.UserProvider} being used since
* some SASL mechanisms require the server to be able to retrieve user passwords</li>
* <li>Whether anonymous logins are enabled or not.</li>
* <li>Whether shared secret authentication is enabled or not.</li>
* <li>Whether the underlying connection has been secured or not.</li>
* </ol>
*
* @author Hao Chen
* @author Gaston Dombiak
*/
public class SASLAuthentication {
private static final Logger Log = LoggerFactory.getLogger(SASLAuthentication.class);
// http://stackoverflow.com/questions/8571501/how-to-check-whether-the-string-is-base64-encoded-or-not
// plus an extra regex alternative to catch a single equals sign ('=', see RFC 6120 6.4.2)
private static final Pattern BASE64_ENCODED = Pattern.compile("^(=|([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==))$");
private static final String SASL_NAMESPACE = "xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"";
private static Map<String, ElementType> typeMap = new TreeMap<>();
private static Set<String> mechanisms = null;
static {
initMechanisms();
}
public enum ElementType {
ABORT("abort"), AUTH("auth"), RESPONSE("response"), CHALLENGE("challenge"), FAILURE("failure"), UNDEF("");
private String name = null;
@Override
public String toString() {
return name;
}
private ElementType(String name) {
this.name = name;
typeMap.put(this.name, this);
}
public static ElementType valueof(String name) {
if (name == null) {
return UNDEF;
}
ElementType e = typeMap.get(name);
return e != null ? e : UNDEF;
}
}
private enum Failure {
ABORTED("aborted"),
ACCOUNT_DISABLED("account-disabled"),
CREDENTIALS_EXPIRED("credentials-expired"),
ENCRYPTION_REQUIRED("encryption-required"),
INCORRECT_ENCODING("incorrect-encoding"),
INVALID_AUTHZID("invalid-authzid"),
INVALID_MECHANISM("invalid-mechanism"),
MALFORMED_REQUEST("malformed-request"),
MECHANISM_TOO_WEAK("mechanism-too-weak"),
NOT_AUTHORIZED("not-authorized"),
TEMPORARY_AUTH_FAILURE("temporary-auth-failure");
private String name = null;
private Failure(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}
public enum Status {
/**
* Entity needs to respond last challenge. Session is still negotiating
* SASL authentication.
*/
needResponse,
/**
* SASL negotiation has failed. The entity may retry a few times before the connection
* is closed.
*/
failed,
/**
* SASL negotiation has been successful.
*/
authenticated;
}
/**
* Returns a string with the valid SASL mechanisms available for the specified session. If
* the session's connection is not secured then only include the SASL mechanisms that don't
* require TLS.
*
* @param session The current session
*
* @return a string with the valid SASL mechanisms available for the specified session.
*/
public static String getSASLMechanisms(LocalSession session) {
if (!(session instanceof ClientSession) && !(session instanceof IncomingServerSession)) {
return "";
}
Element mechs = getSASLMechanismsElement(session);
return mechs.asXML();
}
public static Element getSASLMechanismsElement(Session session) {
if (!(session instanceof ClientSession) && !(session instanceof IncomingServerSession)) {
return null;
}
Element mechs = DocumentHelper.createElement( new QName( "mechanisms",
new Namespace( "", "urn:ietf:params:xml:ns:xmpp-sasl" ) ) );
if (session instanceof LocalIncomingServerSession) {
// Server connections don't follow the same rules as clients
if (session.isSecure()) {
LocalIncomingServerSession svr = (LocalIncomingServerSession)session;
final KeyStore keyStore = SSLConfig.getIdentityStore( Purpose.SOCKET_S2S );
final KeyStore trustStore = SSLConfig.getTrustStore( Purpose.SOCKET_S2S );
final X509Certificate trusted = CertificateManager.getEndEntityCertificate( svr.getConnection().getPeerCertificates(), keyStore, trustStore );
boolean haveTrustedCertificate = trusted != null;
if (trusted != null && svr.getDefaultIdentity() != null) {
haveTrustedCertificate = verifyCertificate(trusted, svr.getDefaultIdentity());
}
if (haveTrustedCertificate) {
// Offer SASL EXTERNAL only if TLS has already been negotiated and the peer has a trusted cert.
Element mechanism = mechs.addElement("mechanism");
mechanism.setText("EXTERNAL");
}
}
}
else {
for (String mech : getSupportedMechanisms()) {
Element mechanism = mechs.addElement("mechanism");
mechanism.setText(mech);
}
}
return mechs;
}
/**
* Handles the SASL authentication packet. The entity may be sending an initial
* authentication request or a response to a challenge made by the server. The returned
* value indicates whether the authentication has finished either successfully or not or
* if the entity is expected to send a response to a challenge.
*
* @param session the session that is authenticating with the server.
* @param doc the stanza sent by the authenticating entity.
* @return value that indicates whether the authentication has finished either successfully
* or not or if the entity is expected to send a response to a challenge.
*/
public static Status handle(LocalSession session, Element doc) {
Status status;
String mechanism;
if (doc.getNamespace().asXML().equals(SASL_NAMESPACE)) {
ElementType type = ElementType.valueof(doc.getName());
switch (type) {
case ABORT:
authenticationFailed(session, Failure.ABORTED);
status = Status.failed;
break;
case AUTH:
mechanism = doc.attributeValue("mechanism");
// http://xmpp.org/rfcs/rfc6120.html#sasl-errors-invalid-mechanism
// The initiating entity did not specify a mechanism
if (mechanism == null) {
authenticationFailed(session, Failure.INVALID_MECHANISM);
status = Status.failed;
break;
}
// Store the requested SASL mechanism by the client
session.setSessionData("SaslMechanism", mechanism);
//Log.debug("SASLAuthentication.doHandshake() AUTH entered: "+mechanism);
if (mechanism.equalsIgnoreCase("ANONYMOUS") &&
mechanisms.contains("ANONYMOUS")) {
status = doAnonymousAuthentication(session);
}
else if (mechanism.equalsIgnoreCase("EXTERNAL")) {
status = doExternalAuthentication(session, doc);
}
else if (mechanisms.contains(mechanism)) {
// The selected SASL mechanism requires the server to send a challenge
// to the client
try {
Map<String, String> props = new TreeMap<>();
props.put(Sasl.QOP, "auth");
if (mechanism.equals("GSSAPI")) {
props.put(Sasl.SERVER_AUTH, "TRUE");
}
SaslServer ss = Sasl.createSaslServer(mechanism, "xmpp",
session.getServerName(), props,
new XMPPCallbackHandler());
if (ss == null) {
authenticationFailed(session, Failure.INVALID_MECHANISM);
return Status.failed;
}
// evaluateResponse doesn't like null parameter
byte[] token = new byte[0];
String value = doc.getTextTrim();
if (value.length() > 0) {
if (!BASE64_ENCODED.matcher(value).matches()) {
authenticationFailed(session, Failure.INCORRECT_ENCODING);
return Status.failed;
}
// If auth request includes a value then validate it
token = StringUtils.decodeBase64(value);
if (token == null) {
token = new byte[0];
}
}
if (mechanism.equals("DIGEST-MD5")) {
// RFC2831 (DIGEST-MD5) says the client MAY provide an initial response on subsequent
// authentication. Java SASL does not (currently) support this and thows an exception
// if we try. This violates the RFC, so we just strip any initial token.
token = new byte[0];
}
byte[] challenge = ss.evaluateResponse(token);
if (ss.isComplete()) {
authenticationSuccessful(session, ss.getAuthorizationID(),
challenge);
status = Status.authenticated;
}
else {
// Send the challenge
sendChallenge(session, challenge);
status = Status.needResponse;
}
session.setSessionData("SaslServer", ss);
}
catch (SaslException e) {
Log.info("User Login Failed. " + e.getMessage());
authenticationFailed(session, Failure.NOT_AUTHORIZED);
status = Status.failed;
}
}
else {
Log.warn("Client wants to do a MECH we don't support: '" +
mechanism + "'");
authenticationFailed(session, Failure.INVALID_MECHANISM);
status = Status.failed;
}
break;
case RESPONSE:
// Store the requested SASL mechanism by the client
mechanism = (String) session.getSessionData("SaslMechanism");
if (mechanism.equalsIgnoreCase("EXTERNAL")) {
status = doExternalAuthentication(session, doc);
}
else if (mechanism.equalsIgnoreCase("JIVE-SHAREDSECRET")) {
status = doSharedSecretAuthentication(session, doc);
}
else if (mechanisms.contains(mechanism)) {
SaslServer ss = (SaslServer) session.getSessionData("SaslServer");
if (ss != null) {
boolean ssComplete = ss.isComplete();
String response = doc.getTextTrim();
if (response.length() > 0) {
if (!BASE64_ENCODED.matcher(response).matches()) {
authenticationFailed(session, Failure.INCORRECT_ENCODING);
return Status.failed;
}
}
try {
if (ssComplete) {
authenticationSuccessful(session, ss.getAuthorizationID(),
null);
status = Status.authenticated;
}
else {
byte[] data = StringUtils.decodeBase64(response);
if (data == null) {
data = new byte[0];
}
byte[] challenge = ss.evaluateResponse(data);
if (ss.isComplete()) {
authenticationSuccessful(session, ss.getAuthorizationID(),
challenge);
status = Status.authenticated;
}
else {
// Send the challenge
sendChallenge(session, challenge);
status = Status.needResponse;
}
}
}
catch (SaslException e) {
Log.debug("SASLAuthentication: SaslException", e);
authenticationFailed(session, Failure.NOT_AUTHORIZED);
status = Status.failed;
}
}
else {
Log.error("SaslServer is null, should be valid object instead.");
authenticationFailed(session, Failure.NOT_AUTHORIZED);
status = Status.failed;
}
}
else {
Log.warn(
"Client responded to a MECH we don't support: '" + mechanism + "'");
authenticationFailed(session, Failure.INVALID_MECHANISM);
status = Status.failed;
}
break;
default:
authenticationFailed(session, Failure.NOT_AUTHORIZED);
status = Status.failed;
// Ignore
break;
}
}
else {
Log.debug("SASLAuthentication: Unknown namespace sent in auth element: " + doc.asXML());
authenticationFailed(session, Failure.MALFORMED_REQUEST);
status = Status.failed;
}
// Check if SASL authentication has finished so we can clean up temp information
if (status == Status.failed || status == Status.authenticated) {
// Remove the SaslServer from the Session
session.removeSessionData("SaslServer");
// Remove the requested SASL mechanism by the client
session.removeSessionData("SaslMechanism");
}
return status;
}
/**
* Returns true if shared secret authentication is enabled. Shared secret
* authentication creates an anonymous session, but requires that the authenticating
* entity know a shared secret key. The client sends a digest of the secret key,
* which is compared against a digest of the local shared key.
*
* @return true if shared secret authentication is enabled.
*/
public static boolean isSharedSecretAllowed() {
return JiveGlobals.getBooleanProperty("xmpp.auth.sharedSecretEnabled");
}
/**
* Sets whether shared secret authentication is enabled. Shared secret
* authentication creates an anonymous session, but requires that the authenticating
* entity know a shared secret key. The client sends a digest of the secret key,
* which is compared against a digest of the local shared key.
*
* @param sharedSecretAllowed true if shared secret authentication should be enabled.
*/
public static void setSharedSecretAllowed(boolean sharedSecretAllowed) {
JiveGlobals.setProperty("xmpp.auth.sharedSecretEnabled", sharedSecretAllowed ? "true" : "false");
}
/**
* Returns the shared secret value, or <tt>null</tt> if shared secret authentication is
* disabled. If this is the first time the shared secret value has been requested (and
* shared secret auth is enabled), the key will be randomly generated and stored in the
* property <tt>xmpp.auth.sharedSecret</tt>.
*
* @return the shared secret value.
*/
public static String getSharedSecret() {
if (!isSharedSecretAllowed()) {
return null;
}
String sharedSecret = JiveGlobals.getProperty("xmpp.auth.sharedSecret");
if (sharedSecret == null) {
sharedSecret = StringUtils.randomString(8);
JiveGlobals.setProperty("xmpp.auth.sharedSecret", sharedSecret);
}
return sharedSecret;
}
/**
* Returns true if the supplied digest matches the shared secret value. The digest
* must be an MD5 hash of the secret key, encoded as hex. This value is supplied
* by clients attempting shared secret authentication.
*
* @param digest the MD5 hash of the secret key, encoded as hex.
* @return true if authentication succeeds.
*/
public static boolean authenticateSharedSecret(String digest) {
if (!isSharedSecretAllowed()) {
return false;
}
String sharedSecert = getSharedSecret();
return StringUtils.hash(sharedSecert).equals( digest );
}
private static Status doAnonymousAuthentication(LocalSession session) {
if (XMPPServer.getInstance().getIQAuthHandler().isAnonymousAllowed()) {
// Verify that client can connect from his IP address
boolean forbidAccess = false;
try {
String hostAddress = session.getConnection().getHostAddress();
if (!LocalClientSession.getAllowedAnonymIPs().isEmpty() &&
!LocalClientSession.getAllowedAnonymIPs().containsKey(hostAddress)) {
byte[] address = session.getConnection().getAddress();
String range1 = (address[0] & 0xff) + "." + (address[1] & 0xff) + "." +
(address[2] & 0xff) +
".*";
String range2 = (address[0] & 0xff) + "." + (address[1] & 0xff) + ".*.*";
String range3 = (address[0] & 0xff) + ".*.*.*";
if (!LocalClientSession.getAllowedAnonymIPs().containsKey(range1) &&
!LocalClientSession.getAllowedAnonymIPs().containsKey(range2) &&
!LocalClientSession.getAllowedAnonymIPs().containsKey(range3)) {
forbidAccess = true;
}
}
} catch (UnknownHostException e) {
forbidAccess = true;
}
if (forbidAccess) {
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}
// Just accept the authentication :)
authenticationSuccessful(session, null, null);
return Status.authenticated;
}
else {
// anonymous login is disabled so close the connection
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}
}
private static Status doExternalAuthentication(LocalSession session, Element doc) {
// At this point the connection has already been secured using TLS
if (session instanceof IncomingServerSession) {
String hostname = doc.getTextTrim();
if (hostname == null || hostname.length() == 0) {
// No hostname was provided so send a challenge to get it
sendChallenge(session, new byte[0]);
return Status.needResponse;
}
hostname = new String(StringUtils.decodeBase64(hostname), StandardCharsets.UTF_8);
if (hostname.length() == 0) {
hostname = null;
}
try {
LocalIncomingServerSession svr = (LocalIncomingServerSession)session;
String defHostname = svr.getDefaultIdentity();
if (hostname == null) {
hostname = defHostname;
} else if (!hostname.equals(defHostname)) {
// Mismatch; really odd.
Log.info("SASLAuthentication rejected from='{}' and authzid='{}'", hostname, defHostname);
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}
} catch(Exception e) {
// Erm. Nothing?
}
if (hostname == null) {
Log.info("No authzid supplied for anonymous session.");
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}
// Check if certificate validation is disabled for s2s
// Flag that indicates if certificates of the remote server should be validated.
// Disabling certificate validation is not recommended for production environments.
boolean verify =
JiveGlobals.getBooleanProperty(ConnectionSettings.Server.TLS_CERTIFICATE_VERIFY, true);
if (!verify) {
authenticationSuccessful(session, hostname, null);
return Status.authenticated;
} else if(verifyCertificates(session.getConnection().getPeerCertificates(), hostname, true)) {
authenticationSuccessful(session, hostname, null);
LocalIncomingServerSession s = (LocalIncomingServerSession)session;
if (s != null) {
s.tlsAuth();
}
return Status.authenticated;
}
}
else if (session instanceof LocalClientSession) {
// Client EXTERNAL login
Log.debug("SASLAuthentication: EXTERNAL authentication via SSL certs for c2s connection");
// This may be null, we will deal with that later
String username = new String(StringUtils.decodeBase64(doc.getTextTrim()), StandardCharsets.UTF_8);
String principal = "";
ArrayList<String> principals = new ArrayList<>();
Connection connection = session.getConnection();
if (connection.getPeerCertificates().length < 1) {
Log.debug("SASLAuthentication: EXTERNAL authentication requested, but no certificates found.");
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}
final KeyStore keyStore = SSLConfig.getIdentityStore( Purpose.SOCKET_C2S );
final KeyStore trustStore = SSLConfig.getTrustStore( Purpose.SOCKET_C2S );
final X509Certificate trusted = CertificateManager.getEndEntityCertificate( connection.getPeerCertificates(), keyStore, trustStore );
if (trusted == null) {
Log.debug("SASLAuthentication: EXTERNAL authentication requested, but EE cert untrusted.");
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}
principals.addAll(CertificateManager.getClientIdentities(trusted));
if(principals.size() == 1) {
principal = principals.get(0);
} else if(principals.size() > 1) {
Log.debug("SASLAuthentication: EXTERNAL authentication: more than one principal found, using first.");
principal = principals.get(0);
} else {
Log.debug("SASLAuthentication: EXTERNAL authentication: No principals found.");
}
if (username == null || username.length() == 0) {
// No username was provided, according to XEP-0178 we need to:
// * attempt to get it from the cert first
// * have the server assign one
// There shouldn't be more than a few principals in here. One ideally
// We set principal to the first one in the list to have a sane default
// If this list is empty, then the cert had no identity at all, which
// will cause an authorization failure
for(String princ : principals) {
String u = AuthorizationManager.map(princ);
if(!u.equals(princ)) {
username = u;
principal = princ;
break;
}
}
if (username == null || username.length() == 0) {
// Still no username. Punt.
username = principal;
}
Log.debug("SASLAuthentication: no username requested, using "+username);
}
//Its possible that either/both username and principal are null here
//The providers should not allow a null authorization
if (AuthorizationManager.authorize(username,principal)) {
Log.debug("SASLAuthentication: "+principal+" authorized to "+username);
authenticationSuccessful(session, username, null);
return Status.authenticated;
}
} else {
Log.debug("SASLAuthentication: unknown session type. Cannot perform EXTERNAL authentication");
}
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}
public static boolean verifyCertificate(X509Certificate trustedCert, String hostname) {
for (String identity : CertificateManager.getServerIdentities(trustedCert)) {
// Verify that either the identity is the same as the hostname, or for wildcarded
// identities that the hostname ends with .domainspecified or -is- domainspecified.
if ((identity.startsWith("*.")
&& (hostname.endsWith(identity.replace("*.", "."))
|| hostname.equals(identity.replace("*.", ""))))
|| hostname.equals(identity)) {
return true;
}
}
return false;
}
/**
* @deprecated Use {@link #verifyCertificates(Certificate[], String, boolean)} instead.
*/
@Deprecated
public static boolean verifyCertificates(Certificate[] chain, String hostname) {
return verifyCertificates( chain, hostname, true );
}
public static boolean verifyCertificates(Certificate[] chain, String hostname, boolean isS2S) {
final Purpose purpose = isS2S ? Purpose.SOCKET_S2S : Purpose.SOCKET_C2S;
final KeyStore keyStore = SSLConfig.getIdentityStore( purpose );
final KeyStore trustStore = SSLConfig.getTrustStore( purpose );
final X509Certificate trusted = CertificateManager.getEndEntityCertificate( chain, keyStore, trustStore );
if (trusted != null) {
return verifyCertificate(trusted, hostname);
}
return false;
}
private static Status doSharedSecretAuthentication(LocalSession session, Element doc) {
String secretDigest;
String response = doc.getTextTrim();
if (response == null || response.length() == 0) {
// No info was provided so send a challenge to get it
sendChallenge(session, new byte[0]);
return Status.needResponse;
}
// Parse data and obtain username & password
String data = new String(StringUtils.decodeBase64(response), StandardCharsets.UTF_8);
StringTokenizer tokens = new StringTokenizer(data, "\0");
tokens.nextToken();
secretDigest = tokens.nextToken();
if (authenticateSharedSecret(secretDigest)) {
authenticationSuccessful(session, null, null);
return Status.authenticated;
}
// Otherwise, authentication failed.
authenticationFailed(session, Failure.NOT_AUTHORIZED);
return Status.failed;
}
private static void sendChallenge(Session session, byte[] challenge) {
StringBuilder reply = new StringBuilder(250);
if (challenge == null) {
challenge = new byte[0];
}
String challenge_b64 = StringUtils.encodeBase64(challenge).trim();
if ("".equals(challenge_b64)) {
challenge_b64 = "="; // Must be padded if null
}
reply.append(
"<challenge xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
reply.append(challenge_b64);
reply.append("</challenge>");
session.deliverRawText(reply.toString());
}
private static void authenticationSuccessful(LocalSession session, String username,
byte[] successData) {
if (username != null && LockOutManager.getInstance().isAccountDisabled(username)) {
// Interception! This person is locked out, fail instead!
LockOutManager.getInstance().recordFailedLogin(username);
authenticationFailed(session, Failure.ACCOUNT_DISABLED);
return;
}
StringBuilder reply = new StringBuilder(80);
reply.append("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"");
if (successData != null) {
String successData_b64 = StringUtils.encodeBase64(successData).trim();
reply.append('>').append(successData_b64).append("</success>");
}
else {
reply.append("/>");
}
session.deliverRawText( reply.toString() );
// We only support SASL for c2s
if (session instanceof ClientSession) {
((LocalClientSession) session).setAuthToken(new AuthToken(username));
}
else if (session instanceof IncomingServerSession) {
String hostname = username;
// Add the validated domain as a valid domain. The remote server can
// now send packets from this address
((LocalIncomingServerSession) session).addValidatedDomain(hostname);
Log.info("Inbound Server {} authenticated (via TLS)", username);
}
}
private static void authenticationFailed(LocalSession session, Failure failure) {
StringBuilder reply = new StringBuilder(80);
reply.append("<failure xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"><");
reply.append(failure.toString());
reply.append("/></failure>");
session.deliverRawText(reply.toString());
// Give a number of retries before closing the connection
Integer retries = (Integer) session.getSessionData("authRetries");
if (retries == null) {
retries = 1;
}
else {
retries = retries + 1;
}
session.setSessionData("authRetries", retries);
if (retries >= JiveGlobals.getIntProperty("xmpp.auth.retries", 3) ) {
// Close the connection
session.close();
}
}
/**
* Adds a new SASL mechanism to the list of supported SASL mechanisms by the server. The
* new mechanism will be offered to clients and connection managers as stream features.<p>
*
* Note: this method simply registers the SASL mechanism to be advertised as a supported
* mechanism by Openfire. Actual SASL handling is done by Java itself, so you must add
* the provider to Java.
*
* @param mechanism the new SASL mechanism.
*/
public static void addSupportedMechanism(String mechanism) {
mechanisms.add(mechanism);
}
/**
* Removes a SASL mechanism from the list of supported SASL mechanisms by the server.
*
* @param mechanism the SASL mechanism to remove.
*/
public static void removeSupportedMechanism(String mechanism) {
mechanisms.remove(mechanism);
}
/**
* Returns the list of supported SASL mechanisms by the server. Note that Java may have
* support for more mechanisms but some of them may not be returned since a special setup
* is required that might be missing. Use {@link #addSupportedMechanism(String)} to add
* new SASL mechanisms.
*
* @return the list of supported SASL mechanisms by the server.
*/
public static Set<String> getSupportedMechanisms() {
Set<String> answer = new HashSet<>(mechanisms);
// Clean up not-available mechanisms
for (Iterator<String> it=answer.iterator(); it.hasNext();) {
String mech = it.next();
if (mech.equals("CRAM-MD5") || mech.equals("DIGEST-MD5")) {
// Check if the user provider in use supports passwords retrieval. Accessing
// to the users passwords will be required by the CallbackHandler
if (!AuthFactory.supportsPasswordRetrieval()) {
it.remove();
}
}
else if (mech.equals("SCRAM-SHA-1")) {
if (!AuthFactory.supportsPasswordRetrieval() && !AuthFactory.supportsScram()) {
it.remove();
}
}
else if (mech.equals("ANONYMOUS")) {
// Check anonymous is supported
if (!XMPPServer.getInstance().getIQAuthHandler().isAnonymousAllowed()) {
it.remove();
}
}
else if (mech.equals("JIVE-SHAREDSECRET")) {
// Check shared secret is supported
if (!isSharedSecretAllowed()) {
it.remove();
}
}
}
return answer;
}
private static void initMechanisms() {
// Convert XML based provider setup to Database based
JiveGlobals.migrateProperty("sasl.mechs");
JiveGlobals.migrateProperty("sasl.gssapi.debug");
JiveGlobals.migrateProperty("sasl.gssapi.config");
JiveGlobals.migrateProperty("sasl.gssapi.useSubjectCredsOnly");
mechanisms = new HashSet<>();
String available = JiveGlobals.getProperty("sasl.mechs");
if (available == null) {
mechanisms.add("ANONYMOUS");
mechanisms.add("PLAIN");
mechanisms.add("DIGEST-MD5");
mechanisms.add("CRAM-MD5");
mechanisms.add("SCRAM-SHA-1");
mechanisms.add("JIVE-SHAREDSECRET");
}
else {
StringTokenizer st = new StringTokenizer(available, " ,\t\n\r\f");
while (st.hasMoreTokens()) {
String mech = st.nextToken().toUpperCase();
// Check that the mech is a supported mechansim. Maybe we shouldnt check this and allow any?
if (mech.equals("ANONYMOUS") ||
mech.equals("PLAIN") ||
mech.equals("DIGEST-MD5") ||
mech.equals("CRAM-MD5") ||
mech.equals("SCRAM-SHA-1") ||
mech.equals("GSSAPI") ||
mech.equals("EXTERNAL") ||
mech.equals("JIVE-SHAREDSECRET"))
{
Log.debug("SASLAuthentication: Added " + mech + " to mech list");
mechanisms.add(mech);
}
}
if (mechanisms.contains("GSSAPI")) {
if (JiveGlobals.getProperty("sasl.gssapi.config") != null) {
System.setProperty("java.security.krb5.debug",
JiveGlobals.getProperty("sasl.gssapi.debug", "false"));
System.setProperty("java.security.auth.login.config",
JiveGlobals.getProperty("sasl.gssapi.config"));
System.setProperty("javax.security.auth.useSubjectCredsOnly",
JiveGlobals.getProperty("sasl.gssapi.useSubjectCredsOnly", "false"));
}
else {
//Not configured, remove the option.
Log.debug("SASLAuthentication: Removed GSSAPI from mech list");
mechanisms.remove("GSSAPI");
}
}
}
//Add our providers to the Security class
Security.addProvider(new org.jivesoftware.openfire.sasl.SaslProvider());
}
}
......@@ -51,7 +51,8 @@ public class SSLConfig
private static final Logger Log = LoggerFactory.getLogger( SSLConfig.class );
private final ConcurrentMap<Purpose, String> locationByPurpose = new ConcurrentHashMap<>();
private final ConcurrentMap<String, CertificateStoreConfig> storesByLocation = new ConcurrentHashMap<>();
private final ConcurrentMap<String, IdentityStoreConfig> identityStoresByLocation = new ConcurrentHashMap<>();
private final ConcurrentMap<String, TrustStoreConfig> trustStoresByLocation = new ConcurrentHashMap<>();
private static SSLConfig INSTANCE;
......@@ -73,201 +74,230 @@ public class SSLConfig
}
/**
* An utility method that is short-hand for getInstance().getStoreConfig(purpose).getStore();
* An utility method that is short-hand for getInstance().getIdentityStoreConfig(purpose).getStore();
* @param purpose The purpose for which to return a store.
* @return a store (never null).
*/
public static synchronized KeyStore getStore( Purpose purpose )
public static synchronized KeyStore getIdentityStore( Purpose purpose )
{
return getInstance().getStoreConfig( purpose ).getStore();
return getInstance().getIdentityStoreConfig( purpose ).getStore();
}
/**
* Openfire allows a store to be re-used for multiple purposes. This method will find the store used for the
* provided purpose, and based on that will return all <em>other</em> purposes for which the same store is used.
*
* @param purpose The purpose for which to find a store (cannot be null).
* @return all <em>other</em> purposes for which the store is used (never null, but possibly an empty collection).
* @throws IOException
* An utility method that is short-hand for getInstance().getTrustStoreConfig(purpose).getStore();
* @param purpose The purpose for which to return a store.
* @return a store (never null).
*/
public Set<Purpose> getOtherPurposesForSameStore( Purpose purpose ) throws IOException
public static synchronized KeyStore getTrustStore( Purpose purpose )
{
if ( purpose == null )
{
throw new IllegalArgumentException( "Argument 'purpose' cannot be null." );
}
final Set<Purpose> results = new HashSet<>();
final String location = getLocation( purpose );
for ( Map.Entry<Purpose, String> entry : locationByPurpose.entrySet() )
{
if ( entry.getValue().equalsIgnoreCase( location ) ) {
results.add( entry.getKey() );
}
}
return results;
return getInstance().getTrustStoreConfig( purpose ).getStore();
}
public static String getNonCanonicalizedLocation(Purpose purpose)
{
final String path;
switch ( purpose )
{
// Identity store for socket-based IO (this is the default identity store)
case SOCKETBASED_IDENTITYSTORE:
path = JiveGlobals.getProperty( "xmpp.socket.ssl.keystore", "resources" + File.separator + "security" + File.separator + "keystore" );
break;
// Identity store for BOSH-based IO (falls back to the default identity store when not configured)
case BOSHBASED_IDENTITYSTORE:
path = JiveGlobals.getProperty( "xmpp.bosh.ssl.keystore", getNonCanonicalizedLocation( Purpose.SOCKETBASED_IDENTITYSTORE ) );
break;
// Identity store for administrative IO (falls back to the default identity store when not configured)
case ADMINISTRATIVE_IDENTITYSTORE:
path = JiveGlobals.getProperty( "admin.ssl.keystore", getNonCanonicalizedLocation( Purpose.SOCKETBASED_IDENTITYSTORE ) );
break;
// Identity store for admin panel (falls back to the administrative identity store when not configured)
case WEBADMIN_IDENTITYSTORE:
path = JiveGlobals.getProperty( "admin.web.ssl.keystore", getNonCanonicalizedLocation( Purpose.ADMINISTRATIVE_IDENTITYSTORE ) );
break;
// server-to-server trust store (This is the only / default S2S trust store. S2S over BOSH is unsupported by Openfire).
case SOCKETBASED_S2S_TRUSTSTORE:
path = JiveGlobals.getProperty( "xmpp.socket.ssl.truststore", "resources" + File.separator + "security" + File.separator + "truststore" );
break;
// client-to-server trust store for socket-based IO (This is the default C2S trust store).
case SOCKETBASED_C2S_TRUSTSTORE:
path = JiveGlobals.getProperty( "xmpp.socket.ssl.client.truststore", "resources" + File.separator + "security" + File.separator + "client.truststore" );
break;
// /**
// * Openfire allows a store to be re-used for multiple purposes. This method will find the store used for the
// * provided purpose, and based on that will return all <em>other</em> purposes for which the same store is used.
// *
// * @param purpose The purpose for which to find a store (cannot be null).
// * @return all <em>other</em> purposes for which the store is used (never null, but possibly an empty collection).
// * @throws IOException
// */
// public Set<Purpose> getOtherPurposesForSameStore( Type type, boolean isTrustStore ) throws IOException
// {
// if ( type == null )
// {
// throw new IllegalArgumentException( "Argument 'type' cannot be null." );
// }
//
// final Set<Purpose> results = new HashSet<>();
//
// for ( Map.Entry<Purpose, String> entry : locationByPurpose.entrySet() )
// {
// if ( entry.getValue().equalsIgnoreCase( location ) )
// {
// results.add( entry.getKey() );
// }
// }
//
// return results;
// }
// client-to-server trust store for BOSH-based IO (falls back to the default C2S trust store when not configured).
case BOSHBASED_C2S_TRUSTSTORE:
path = JiveGlobals.getProperty( "xmpp.bosh.ssl.client.truststore", getNonCanonicalizedLocation( Purpose.SOCKETBASED_C2S_TRUSTSTORE ) );
break;
// public static String getNonCanonicalizedLocation(Purpose purpose)
// {
// final String path;
// switch ( purpose )
// {
// // Identity store for socket-based IO (this is the default identity store)
// case SOCKETBASED_IDENTITYSTORE:
// path = JiveGlobals.getProperty( "xmpp.socket.ssl.keystore", "resources" + File.separator + "security" + File.separator + "keystore" );
// break;
//
// // Identity store for BOSH-based IO (falls back to the default identity store when not configured)
// case BOSHBASED_IDENTITYSTORE:
// path = JiveGlobals.getProperty( "xmpp.bosh.ssl.keystore", getNonCanonicalizedLocation( Purpose.SOCKETBASED_IDENTITYSTORE ) );
// break;
//
// // Identity store for administrative IO (falls back to the default identity store when not configured)
// case ADMINISTRATIVE_IDENTITYSTORE:
// path = JiveGlobals.getProperty( "admin.ssl.keystore", getNonCanonicalizedLocation( Purpose.SOCKETBASED_IDENTITYSTORE ) );
// break;
//
// // Identity store for admin panel (falls back to the administrative identity store when not configured)
// case WEBADMIN_IDENTITYSTORE:
// path = JiveGlobals.getProperty( "admin.web.ssl.keystore", getNonCanonicalizedLocation( Purpose.ADMINISTRATIVE_IDENTITYSTORE ) );
// break;
//
// // server-to-server trust store (This is the only / default S2S trust store. S2S over BOSH is unsupported by Openfire).
// case SOCKETBASED_S2S_TRUSTSTORE:
// path = JiveGlobals.getProperty( "xmpp.socket.ssl.truststore", "resources" + File.separator + "security" + File.separator + "truststore" );
// break;
//
// // client-to-server trust store for socket-based IO (This is the default C2S trust store).
// case SOCKETBASED_C2S_TRUSTSTORE:
// path = JiveGlobals.getProperty( "xmpp.socket.ssl.client.truststore", "resources" + File.separator + "security" + File.separator + "client.truststore" );
// break;
//
// // client-to-server trust store for BOSH-based IO (falls back to the default C2S trust store when not configured).
// case BOSHBASED_C2S_TRUSTSTORE:
// path = JiveGlobals.getProperty( "xmpp.bosh.ssl.client.truststore", getNonCanonicalizedLocation( Purpose.SOCKETBASED_C2S_TRUSTSTORE ) );
// break;
//
// // Administrative trust store (falls back to the default trust store when not configured)
// case ADMINISTRATIVE_TRUSTSTORE:
// path = JiveGlobals.getProperty( "admin.ssl.truststore", getNonCanonicalizedLocation( Purpose.SOCKETBASED_S2S_TRUSTSTORE ) );
// break;
//
// // Trust store for admin panel (falls back to the administrative trust store when not configured)
// case WEBADMIN_TRUSTSTORE:
// path = JiveGlobals.getProperty( "admin.web.ssl.truststore", getNonCanonicalizedLocation( Purpose.ADMINISTRATIVE_TRUSTSTORE ) );
// break;
//
// default:
// throw new IllegalStateException( "Unrecognized purpose: " + purpose );
// }
// return path;
// }
// Administrative trust store (falls back to the default trust store when not configured)
case ADMINISTRATIVE_TRUSTSTORE:
path = JiveGlobals.getProperty( "admin.ssl.truststore", getNonCanonicalizedLocation( Purpose.SOCKETBASED_S2S_TRUSTSTORE ) );
break;
// public static String getLocation(Purpose purpose, boolean isTrustStore) throws IOException
// {
// final String location;
// if ( isTrustStore ) {
// location = purpose.getTrustStoreLocationNonCanonicalized();
// } else {
// location = purpose.getIdentityStoreLocationNonCanonicalized();
// }
// return canonicalize( location );
// }
// Trust store for admin panel (falls back to the administrative trust store when not configured)
case WEBADMIN_TRUSTSTORE:
path = JiveGlobals.getProperty( "admin.web.ssl.truststore", getNonCanonicalizedLocation( Purpose.ADMINISTRATIVE_TRUSTSTORE ) );
break;
// protected String getPassword( Type type, boolean isTrustStore )
// {
// switch ( purpose ) {
// case SOCKETBASED_IDENTITYSTORE:
// return JiveGlobals.getProperty( "xmpp.socket.ssl.keypass", "changeit" ).trim();
//
// case BOSHBASED_IDENTITYSTORE:
// return JiveGlobals.getProperty( "xmpp.bosh.ssl.keypass", getPassword( Purpose.SOCKETBASED_IDENTITYSTORE ) ).trim();
//
// case ADMINISTRATIVE_IDENTITYSTORE:
// return JiveGlobals.getProperty( "admin.ssl.keypass", getPassword( Purpose.SOCKETBASED_IDENTITYSTORE ) ).trim();
//
// case WEBADMIN_IDENTITYSTORE:
// return JiveGlobals.getProperty( "admin.web.ssl.keypass", getPassword( Purpose.ADMINISTRATIVE_IDENTITYSTORE ) ).trim();
//
// case SOCKETBASED_S2S_TRUSTSTORE:
// return JiveGlobals.getProperty( "xmpp.socket.ssl.trustpass", "changeit" ).trim();
//
// case SOCKETBASED_C2S_TRUSTSTORE:
// return JiveGlobals.getProperty( "xmpp.socket.ssl.client.trustpass", "changeit" ).trim();
//
// case BOSHBASED_C2S_TRUSTSTORE:
// return JiveGlobals.getProperty( "xmpp.bosh.ssl.client.trustpass", getPassword( Purpose.SOCKETBASED_C2S_TRUSTSTORE ) ).trim();
//
// case ADMINISTRATIVE_TRUSTSTORE:
// return JiveGlobals.getProperty( "admin.ssl.trustpass", getPassword( Purpose.SOCKETBASED_S2S_TRUSTSTORE ) ).trim();
//
// case WEBADMIN_TRUSTSTORE:
// return JiveGlobals.getProperty( "admin..web.ssl.trustpass", getPassword( Purpose.ADMINISTRATIVE_TRUSTSTORE ) ).trim();
//
// default:
// throw new IllegalStateException( "Unrecognized purpose: " + purpose );
// }
// }
default:
throw new IllegalStateException( "Unrecognized purpose: " + purpose );
}
return path;
}
// protected String getStoreType( Type type, boolean isTrustStore )
// {
// // FIXME All properties should be unique, instead of being re-used by different stores.
// switch ( purpose )
// {
// case SOCKETBASED_IDENTITYSTORE:
// return JiveGlobals.getProperty( "xmpp.socket.ssl.storeType", "jks" ).trim();
//
// case BOSHBASED_IDENTITYSTORE:
// return JiveGlobals.getProperty( "xmpp.bosh.ssl.storeType", getStoreType( Purpose.SOCKETBASED_IDENTITYSTORE ) ).trim();
//
// case ADMINISTRATIVE_IDENTITYSTORE:
// return JiveGlobals.getProperty( "admin.ssl.storeType", getStoreType( Purpose.SOCKETBASED_IDENTITYSTORE ) ).trim();
//
// case WEBADMIN_IDENTITYSTORE:
// return JiveGlobals.getProperty( "admin.web.ssl.storeType", getStoreType( Purpose.ADMINISTRATIVE_IDENTITYSTORE ) ).trim();
//
// case SOCKETBASED_S2S_TRUSTSTORE:
// return JiveGlobals.getProperty( "xmpp.socket.ssl.storeType", "jks" ).trim();
//
// case SOCKETBASED_C2S_TRUSTSTORE:
// return JiveGlobals.getProperty( "xmpp.socket.ssl.client.storeType", "jks" ).trim();
//
// case BOSHBASED_C2S_TRUSTSTORE:
// return JiveGlobals.getProperty( "xmpp.bosh.ssl.client.storeType", getStoreType( Purpose.SOCKETBASED_C2S_TRUSTSTORE ) ).trim();
//
// case ADMINISTRATIVE_TRUSTSTORE:
// return JiveGlobals.getProperty( "admin.ssl.storeType", getStoreType( Purpose.SOCKETBASED_S2S_TRUSTSTORE ) ).trim();
//
// case WEBADMIN_TRUSTSTORE:
// return JiveGlobals.getProperty( "admin.web.ssl.storeType", getStoreType( Purpose.ADMINISTRATIVE_TRUSTSTORE ) ).trim();
//
// default:
// throw new IllegalStateException( "Unrecognized purpose: " + purpose );
// }
// }
public static String getLocation(Purpose purpose) throws IOException
private SSLConfig() throws CertificateStoreConfigException, IOException, NoSuchAlgorithmException
{
return canonicalize( getNonCanonicalizedLocation( purpose ) );
}
protected String getPassword(Purpose purpose){
switch ( purpose ) {
case SOCKETBASED_IDENTITYSTORE:
return JiveGlobals.getProperty( "xmpp.socket.ssl.keypass", "changeit" ).trim();
case BOSHBASED_IDENTITYSTORE:
return JiveGlobals.getProperty( "xmpp.bosh.ssl.keypass", getPassword( Purpose.SOCKETBASED_IDENTITYSTORE ) ).trim();
case ADMINISTRATIVE_IDENTITYSTORE:
return JiveGlobals.getProperty( "admin.ssl.keypass", getPassword( Purpose.SOCKETBASED_IDENTITYSTORE ) ).trim();
case WEBADMIN_IDENTITYSTORE:
return JiveGlobals.getProperty( "admin.web.ssl.keypass", getPassword( Purpose.ADMINISTRATIVE_IDENTITYSTORE ) ).trim();
case SOCKETBASED_S2S_TRUSTSTORE:
return JiveGlobals.getProperty( "xmpp.socket.ssl.trustpass", "changeit" ).trim();
case SOCKETBASED_C2S_TRUSTSTORE:
return JiveGlobals.getProperty( "xmpp.socket.ssl.client.trustpass", "changeit" ).trim();
case BOSHBASED_C2S_TRUSTSTORE:
return JiveGlobals.getProperty( "xmpp.bosh.ssl.client.trustpass", getPassword( Purpose.SOCKETBASED_C2S_TRUSTSTORE ) ).trim();
case ADMINISTRATIVE_TRUSTSTORE:
return JiveGlobals.getProperty( "admin.ssl.trustpass", getPassword( Purpose.SOCKETBASED_S2S_TRUSTSTORE ) ).trim();
case WEBADMIN_TRUSTSTORE:
return JiveGlobals.getProperty( "admin..web.ssl.trustpass", getPassword( Purpose.ADMINISTRATIVE_TRUSTSTORE ) ).trim();
default:
throw new IllegalStateException( "Unrecognized purpose: " + purpose );
}
}
protected String getStoreType(Purpose purpose) {
// FIXME All properties should be unique, instead of being re-used by different stores.
switch ( purpose )
for ( final Purpose purpose : Purpose.values() )
{
case SOCKETBASED_IDENTITYSTORE:
return JiveGlobals.getProperty( "xmpp.socket.ssl.storeType", "jks" ).trim();
case BOSHBASED_IDENTITYSTORE:
return JiveGlobals.getProperty( "xmpp.bosh.ssl.storeType", getStoreType( Purpose.SOCKETBASED_IDENTITYSTORE ) ).trim();
case ADMINISTRATIVE_IDENTITYSTORE:
return JiveGlobals.getProperty( "admin.ssl.storeType", getStoreType( Purpose.SOCKETBASED_IDENTITYSTORE ) ).trim();
case WEBADMIN_IDENTITYSTORE:
return JiveGlobals.getProperty( "admin.web.ssl.storeType", getStoreType( Purpose.ADMINISTRATIVE_IDENTITYSTORE ) ).trim();
case SOCKETBASED_S2S_TRUSTSTORE:
return JiveGlobals.getProperty( "xmpp.socket.ssl.storeType", "jks" ).trim();
case SOCKETBASED_C2S_TRUSTSTORE:
return JiveGlobals.getProperty( "xmpp.socket.ssl.client.storeType", "jks" ).trim();
case BOSHBASED_C2S_TRUSTSTORE:
return JiveGlobals.getProperty( "xmpp.bosh.ssl.client.storeType", getStoreType( Purpose.SOCKETBASED_C2S_TRUSTSTORE ) ).trim();
case ADMINISTRATIVE_TRUSTSTORE:
return JiveGlobals.getProperty( "admin.ssl.storeType", getStoreType( Purpose.SOCKETBASED_S2S_TRUSTSTORE ) ).trim();
case WEBADMIN_TRUSTSTORE:
return JiveGlobals.getProperty( "admin.web.ssl.storeType", getStoreType( Purpose.ADMINISTRATIVE_TRUSTSTORE ) ).trim();
// Instantiate an identity store.
final String locationIdent = purpose.getIdentityStoreLocation();
locationByPurpose.put( purpose, locationIdent );
if ( !identityStoresByLocation.containsKey( locationIdent ) )
{
final IdentityStoreConfig storeConfig = new IdentityStoreConfig( purpose.getIdentityStoreLocation(), purpose.getIdentityStorePassword(), purpose.getIdentityStoreType(), false );
identityStoresByLocation.put( locationIdent, storeConfig );
}
default:
throw new IllegalStateException( "Unrecognized purpose: " + purpose );
// Instantiate trust store.
final String locationTrust = purpose.getTrustStoreLocation();
locationByPurpose.put( purpose, locationTrust );
if ( !trustStoresByLocation.containsKey( locationTrust ) )
{
final TrustStoreConfig storeConfig = new TrustStoreConfig( purpose.getTrustStoreLocation(), purpose.getTrustStorePassword(), purpose.getTrustStoreType(), false, purpose.acceptSelfSigned(), purpose.verifyValidity() );
trustStoresByLocation.put( locationTrust, storeConfig );
}
}
}
private SSLConfig() throws CertificateStoreConfigException, IOException, NoSuchAlgorithmException
public IdentityStoreConfig getIdentityStoreConfig( Purpose purpose )
{
for (Purpose purpose : Purpose.values()) {
final String location = getLocation( purpose );
if ( !storesByLocation.containsKey( location )) {
final CertificateStoreConfig storeConfig;
if (purpose.isTrustStore()) {
final boolean acceptSelfSigned = false; // TODO make configurable
final boolean checkValidity = true; ; // TODO make configurable
storeConfig = new TrustStoreConfig( getLocation( purpose ), getPassword( purpose ), getStoreType( purpose ), false, acceptSelfSigned, checkValidity );
} else {
storeConfig = new IdentityStoreConfig( getLocation( purpose ), getPassword( purpose ), getStoreType( purpose ), false );
}
storesByLocation.put( location, storeConfig );
}
locationByPurpose.put(purpose, location);
if ( purpose == null ) {
throw new IllegalArgumentException( "Argument 'purpose' cannot be null.");
}
return identityStoresByLocation.get( locationByPurpose.get( purpose ) );
}
public CertificateStoreConfig getStoreConfig( Purpose purpose ) {
public TrustStoreConfig getTrustStoreConfig( Purpose purpose )
{
if ( purpose == null ) {
throw new IllegalArgumentException( "Argument 'purpose' cannot be null.");
}
return storesByLocation.get( locationByPurpose.get( purpose ) );
return trustStoresByLocation.get( locationByPurpose.get( purpose ) );
}
// public void useStoreForPurpose( Purpose purpose, String location, String password, String storeType, boolean createIfAbsent ) throws IOException, CertificateStoreConfigException
......@@ -369,83 +399,12 @@ public class SSLConfig
// // TODO notify listeners
// }
public static String canonicalize( String path ) throws IOException
public static SSLContext getSSLContext( final Purpose purpose ) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException
{
File file = new File( path );
if (!file.isAbsolute()) {
file = new File( JiveGlobals.getHomeDirectory() + File.separator + path );
}
return file.getCanonicalPath();
}
// TODO merge this with Purpose!
public enum Type {
SOCKET_S2S( "xmpp.socket.ssl.", null ),
SOCKET_C2S( "xmpp.socket.ssl.client.", null ),
BOSH_C2S( "xmpp.bosh.ssl.client.", SOCKET_C2S),
ADMIN( "admin.ssl.", SOCKET_S2S),
WEBADMIN( "admin.web.ssl.", ADMIN);
String prefix;
Type fallback;
Type( String prefix, Type fallback) {
this.prefix = prefix;
this.fallback = fallback;
}
public String getPrefix()
{
return prefix;
}
public Type getFallback()
{
return fallback;
}
}
public static SSLContext getSSLContext( final Type type ) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException
{
// TODO: allow different algorithms for different connection types (eg client/server/bosh etc)
final String algorithm = JiveGlobals.getProperty( ConnectionSettings.Client.TLS_ALGORITHM, "TLS" );
final Purpose idPurpose;
final Purpose trustPurpose;
switch ( type ) {
case SOCKET_S2S:
idPurpose = Purpose.SOCKETBASED_IDENTITYSTORE;
trustPurpose = Purpose.SOCKETBASED_S2S_TRUSTSTORE;
break;
case SOCKET_C2S:
idPurpose = Purpose.SOCKETBASED_IDENTITYSTORE;
trustPurpose = Purpose.SOCKETBASED_C2S_TRUSTSTORE;
break;
case BOSH_C2S:
idPurpose = Purpose.BOSHBASED_IDENTITYSTORE;
trustPurpose = Purpose.BOSHBASED_C2S_TRUSTSTORE;
break;
case ADMIN:
idPurpose = Purpose.ADMINISTRATIVE_IDENTITYSTORE;
trustPurpose = Purpose.ADMINISTRATIVE_TRUSTSTORE;
break;
case WEBADMIN:
idPurpose = Purpose.WEBADMIN_IDENTITYSTORE;
trustPurpose = Purpose.WEBADMIN_TRUSTSTORE;
break;
default:
throw new IllegalStateException( "Unsupported type: " + type );
}
final KeyManager[] keyManagers = ((IdentityStoreConfig) getInstance().getStoreConfig( idPurpose )).getKeyManagers();
final TrustManager[] trustManagers = ((TrustStoreConfig) getInstance().getStoreConfig( trustPurpose )).getTrustManagers();
final KeyManager[] keyManagers = getInstance().getIdentityStoreConfig( purpose ).getKeyManagers();
final TrustManager[] trustManagers = getInstance().getTrustStoreConfig( purpose ).getTrustManagers();
final SSLContext sslContext = SSLContext.getInstance( algorithm );
final SSLContext sslContext = SSLContext.getInstance( purpose.getIdentityStoreType() );
sslContext.init( keyManagers, trustManagers, new SecureRandom() );
return sslContext;
......@@ -456,13 +415,13 @@ public class SSLConfig
*
* For Openfire, an engine is of this mode used for most purposes (as Openfire is a server by nature).
*
* @param type The type of connectivity for which to configure a new SSLEngine instance. Cannot be null.
* @param purpose The type of connectivity for which to configure a new SSLEngine instance. Cannot be null.
* @param clientAuth indication of the desired level of client-sided authentication (mutual authentication). Cannot be null.
* @return An initialized SSLEngine instance (never null).
*/
public static SSLEngine getServerModeSSLEngine( Type type, Connection.ClientAuth clientAuth ) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException
public static SSLEngine getServerModeSSLEngine( Purpose purpose, Connection.ClientAuth clientAuth ) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException
{
final SSLEngine sslEngine = getSSLEngine( type );
final SSLEngine sslEngine = getSSLEngine( purpose );
sslEngine.setUseClientMode( false );
switch ( clientAuth )
......@@ -488,12 +447,12 @@ public class SSLConfig
*
* For Openfire, an engine of this mode is typically used when the server tries to connect to another server.
*
* @param type The type of connectivity for which to configure a new SSLEngine instance. Cannot be null.
* @param purpose The type of connectivity for which to configure a new SSLEngine instance. Cannot be null.
* @return An initialized SSLEngine instance (never null).
*/
public static SSLEngine getClientModeSSLEngine( Type type ) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException
public static SSLEngine getClientModeSSLEngine( Purpose purpose ) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException
{
final SSLEngine sslEngine = getSSLEngine( type );
final SSLEngine sslEngine = getSSLEngine( purpose );
sslEngine.setUseClientMode( true );
return sslEngine;
......@@ -506,16 +465,16 @@ public class SSLConfig
* The returned value lacks further configuration. In most cases, developers will want to use getClientModeSSLEngine
* or getServerModeSSLEngine instead of this method.
*
* @param type The type of connectivity for which to pre-configure a new SSLEngine instance. Cannot be null.
* @param purpose The type of connectivity for which to pre-configure a new SSLEngine instance. Cannot be null.
* @return A pre-configured SSLEngine (never null).
*/
private static SSLEngine getSSLEngine( final Type type ) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException
private static SSLEngine getSSLEngine( final Purpose purpose ) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException
{
final SSLContext sslContext = getSSLContext( type );
final SSLContext sslContext = getSSLContext( purpose );
final SSLEngine sslEngine = sslContext.createSSLEngine();
configureProtocols( sslEngine, type );
configureCipherSuites( sslEngine, type );
configureProtocols( sslEngine, purpose );
configureCipherSuites( sslEngine, purpose );
return sslEngine;
}
......@@ -533,16 +492,16 @@ public class SSLConfig
* where the SSLEngine default gets filtered but not replaced.
*
* @param sslEngine The instance to configure. Cannot be null.
* @param type The type of configuration to use (used to select the relevent property). Cannot be null.
* @param purpose The type of configuration to use (used to select the relevent property). Cannot be null.
*/
private static void configureProtocols( SSLEngine sslEngine, Type type )
private static void configureProtocols( SSLEngine sslEngine, Purpose purpose )
{
// Find configuration, using fallback where applicable.
String enabledProtocols = JiveGlobals.getProperty( type.getPrefix() + "enabled.protocols" );
while (enabledProtocols == null && type.getFallback() != null)
String enabledProtocols = JiveGlobals.getProperty( purpose.getPrefix() + "enabled.protocols" );
while (enabledProtocols == null && purpose.getFallback() != null)
{
type = type.getFallback();
enabledProtocols = JiveGlobals.getProperty( type.getPrefix() + "enabled.protocols" );
purpose = purpose.getFallback();
enabledProtocols = JiveGlobals.getProperty( purpose.getPrefix() + "enabled.protocols" );
}
if (enabledProtocols != null )
......@@ -585,15 +544,15 @@ public class SSLConfig
* entire SSLEngine default gets replaced.
*
* @param sslEngine The instance to configure. Cannot be null.
* @param type The type of configuration to use (used to select the relevent property). Cannot be null.
* @param purpose The type of configuration to use (used to select the relevent property). Cannot be null.
*/
private static void configureCipherSuites( SSLEngine sslEngine, Type type )
private static void configureCipherSuites( SSLEngine sslEngine, Purpose purpose )
{
String enabledCipherSuites = JiveGlobals.getProperty( type.getPrefix() + "enabled.ciphersuites" );
while (enabledCipherSuites == null && type.getFallback() != null)
String enabledCipherSuites = JiveGlobals.getProperty( purpose.getPrefix() + "enabled.ciphersuites" );
while (enabledCipherSuites == null && purpose.getFallback() != null)
{
type = type.getFallback();
enabledCipherSuites = JiveGlobals.getProperty( type.getPrefix() + "enabled.ciphersuites" );
purpose = purpose.getFallback();
enabledCipherSuites = JiveGlobals.getProperty( purpose.getPrefix() + "enabled.ciphersuites" );
}
if (enabledCipherSuites != null )
......@@ -638,14 +597,14 @@ public class SSLConfig
* Instead of an SSLContext or SSLEngine, Apache MINA uses an SslFilter instance. It is generally not needed to
* create both SSLContext/SSLEngine as well as SslFilter instances.
*
* @param type Communication type (used to select the relevant property). Cannot be null.
* @param purpose Communication type (used to select the relevant property). Cannot be null.
* @param clientAuth indication of the desired level of client-sided authentication (mutual authentication). Cannot be null.
* @return An initialized SslFilter instance (never null)
*/
public static SslFilter getServerModeSslFilter( SSLConfig.Type type, Connection.ClientAuth clientAuth ) throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException
public static SslFilter getServerModeSslFilter( Purpose purpose, Connection.ClientAuth clientAuth ) throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException
{
final SSLContext sslContext = SSLConfig.getSSLContext( type );
final SSLEngine sslEngine = SSLConfig.getServerModeSSLEngine( type, clientAuth );
final SSLContext sslContext = SSLConfig.getSSLContext( purpose );
final SSLEngine sslEngine = SSLConfig.getServerModeSSLEngine( purpose, clientAuth );
return getSslFilter( sslContext, sslEngine );
}
......@@ -658,13 +617,13 @@ public class SSLConfig
* Instead of an SSLContext or SSLEngine, Apache MINA uses an SslFilter instance. It is generally not needed to
* create both SSLContext/SSLEngine as well as SslFilter instances.
*
* @param type Communication type (used to select the relevant property). Cannot be null.
* @param purpose Communication type (used to select the relevant property). Cannot be null.
* @return An initialized SslFilter instance (never null)
*/
public static SslFilter getClientModeSslFilter( SSLConfig.Type type ) throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException
public static SslFilter getClientModeSslFilter( Purpose purpose ) throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException
{
final SSLContext sslContext = SSLConfig.getSSLContext( type );
final SSLEngine sslEngine = SSLConfig.getClientModeSSLEngine( type );
final SSLContext sslContext = SSLConfig.getSSLContext( purpose );
final SSLEngine sslEngine = SSLConfig.getClientModeSSLEngine( purpose );
return getSslFilter( sslContext, sslEngine );
}
......
......@@ -31,6 +31,7 @@ import javax.net.ssl.SSLEngineResult.HandshakeStatus;
import javax.net.ssl.SSLEngineResult.Status;
import org.jivesoftware.openfire.Connection;
import org.jivesoftware.openfire.keystore.Purpose;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -77,12 +78,12 @@ public class TLSWrapper {
final SSLEngine sslEngine;
if ( clientMode )
{
sslEngine = SSLConfig.getClientModeSSLEngine( SSLConfig.Type.SOCKET_S2S );
sslEngine = SSLConfig.getClientModeSSLEngine( Purpose.SOCKET_S2S );
}
else
{
final SSLConfig.Type type = isPeerClient ? SSLConfig.Type.SOCKET_C2S : SSLConfig.Type.SOCKET_S2S;
sslEngine = SSLConfig.getServerModeSSLEngine( type, clientAuth );
final Purpose purpose = isPeerClient ? Purpose.SOCKET_C2S : Purpose.SOCKET_S2S;
sslEngine = SSLConfig.getServerModeSSLEngine( purpose, clientAuth );
}
final SSLSession sslSession = sslEngine.getSession();
......
......@@ -28,19 +28,13 @@ import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.*;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.filterchain.IoFilterChain;
......@@ -52,26 +46,20 @@ import org.jivesoftware.openfire.Connection;
import org.jivesoftware.openfire.ConnectionCloseListener;
import org.jivesoftware.openfire.PacketDeliverer;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.keystore.IdentityStoreConfig;
import org.jivesoftware.openfire.keystore.Purpose;
import org.jivesoftware.openfire.keystore.TrustStoreConfig;
import org.jivesoftware.openfire.keystore.*;
import org.jivesoftware.openfire.net.*;
import org.jivesoftware.openfire.session.ConnectionSettings;
import org.jivesoftware.openfire.session.LocalSession;
import org.jivesoftware.openfire.session.Session;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.XMLWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.Packet;
/**
* Implementation of {@link Connection} inteface specific for NIO connections when using
* the MINA framework.<p>
*
* MINA project can be found at <a href="http://mina.apache.org">here</a>.
* Implementation of {@link Connection} interface specific for NIO connections when using the Apache MINA framework.
*
* @author Gaston Dombiak
* @see <a href="http://mina.apache.org">Apache MINA</a>
*/
public class NIOConnection implements Connection {
......@@ -378,65 +366,28 @@ public class NIOConnection implements Connection {
@Deprecated
@Override
public void startTLS(boolean clientMode, String remoteServer, ClientAuth authentication) throws Exception {
final boolean isClientToServer = ( remoteServer == null );
startTLS( clientMode, isClientToServer, authentication );
final boolean isPeerClient = ( remoteServer == null );
startTLS( clientMode, isPeerClient, authentication );
}
public void startTLS(boolean clientMode, boolean isClientToServer, ClientAuth authentication) throws Exception {
Log.debug( "StartTLS: using {}", isClientToServer ? "c2s" : "s2s" );
public void startTLS(boolean clientMode, boolean isPeerClient, ClientAuth authentication) throws Exception {
final SSLConfig sslConfig = SSLConfig.getInstance();
final TrustStoreConfig storeConfig;
if (isClientToServer) {
storeConfig = (TrustStoreConfig) sslConfig.getStoreConfig( Purpose.SOCKETBASED_C2S_TRUSTSTORE );
} else {
storeConfig = (TrustStoreConfig) sslConfig.getStoreConfig( Purpose.SOCKETBASED_S2S_TRUSTSTORE );
final SslFilter filter;
if ( clientMode ) {
filter = SSLConfig.getClientModeSslFilter( Purpose.SOCKET_S2S );
}
final TrustManager[] tm;
if (clientMode || authentication == ClientAuth.needed || authentication == ClientAuth.wanted) {
// We might need to verify a certificate from our peer, so get different TrustManager[]'s
final KeyStore ksTrust = storeConfig.getStore();
if(isClientToServer) {
// Check if we can trust certificates presented by the client
tm = new TrustManager[]{new ClientTrustManager(ksTrust)};
} else {
// Check if we can trust certificates presented by the server
tm = new TrustManager[]{new ServerTrustManager(ksTrust)};
}
} else {
tm = storeConfig.getTrustManagers();
else
{
final Purpose purpose = isPeerClient ? Purpose.SOCKET_C2S : Purpose.SOCKET_S2S;
filter = SSLConfig.getServerModeSslFilter( purpose, authentication );
}
final SSLContext tlsContext = SSLConfig.getSSLContext();
final IdentityStoreConfig identityStoreConfig = (IdentityStoreConfig) sslConfig.getStoreConfig( Purpose.SOCKETBASED_IDENTITYSTORE );
tlsContext.init( identityStoreConfig.getKeyManagers(), tm, null);
SslFilter filter = new SslFilter(tlsContext);
filter.setUseClientMode(clientMode);
// Disable SSLv3 due to POODLE vulnerability.
if (clientMode) {
filter.setEnabledProtocols(new String[]{"TLSv1", "TLSv1.1", "TLSv1.2"});
} else {
// ... but accept a SSLv2 Hello when in server mode.
filter.setEnabledProtocols(new String[]{"SSLv2Hello", "TLSv1", "TLSv1.1", "TLSv1.2"});
}
if (authentication == ClientAuth.needed) {
filter.setNeedClientAuth(true);
}
else if (authentication == ClientAuth.wanted) {
// Just indicate that we would like to authenticate the client but if client
// certificates are self-signed or have no certificate chain then we are still
// good
filter.setWantClientAuth(true);
}
ioSession.getFilterChain().addBefore(EXECUTOR_FILTER_NAME, TLS_FILTER_NAME, filter);
ioSession.setAttribute(SslFilter.DISABLE_ENCRYPTION_ONCE, Boolean.TRUE);
if (!clientMode) {
if ( !clientMode ) {
// Indicate the client that the server is ready to negotiate TLS
deliverRawText("<proceed xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\"/>");
deliverRawText( "<proceed xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\"/>" );
}
}
......
/**
* $RCSfile$
* $Revision: 3187 $
* $Date: 2005-12-11 13:34:34 -0300 (Sun, 11 Dec 2005) $
*
* Copyright (C) 2005-2008 Jive Software. 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.session;
import java.net.UnknownHostException;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.StringTokenizer;
import org.jivesoftware.openfire.Connection;
import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.openfire.StreamID;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.auth.AuthToken;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.cluster.ClusterManager;
import org.jivesoftware.openfire.keystore.Purpose;
import org.jivesoftware.openfire.net.SASLAuthentication;
import org.jivesoftware.openfire.net.SSLConfig;
import org.jivesoftware.openfire.net.SocketConnection;
import org.jivesoftware.openfire.privacy.PrivacyList;
import org.jivesoftware.openfire.privacy.PrivacyListManager;
import org.jivesoftware.openfire.streammanagement.StreamManager;
import org.jivesoftware.openfire.user.PresenceEventDispatcher;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.cache.Cache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmpp.packet.JID;
import org.xmpp.packet.Packet;
import org.xmpp.packet.Presence;
import org.xmpp.packet.StreamError;
/**
* Represents a session between the server and a client.
*
* @author Gaston Dombiak
*/
public class LocalClientSession extends LocalSession implements ClientSession {
private static final Logger Log = LoggerFactory.getLogger(LocalClientSession.class);
private static final String ETHERX_NAMESPACE = "http://etherx.jabber.org/streams";
private static final String FLASH_NAMESPACE = "http://www.jabber.com/streams/flash";
/**
* Keep the list of IP address that are allowed to connect to the server. If the list is
* empty then anyone is allowed to connect to the server.<p>
*
* Note: Key = IP address or IP range; Value = empty string. A hash map is being used for
* performance reasons.
*/
private static Map<String,String> allowedIPs = new HashMap<>();
private static Map<String,String> allowedAnonymIPs = new HashMap<>();
private boolean messageCarbonsEnabled;
/**
* The authentication token for this session.
*/
protected AuthToken authToken;
/**
* Flag indicating if this session has been initialized yet (upon first available transition).
*/
private boolean initialized;
/**
* Flag that indicates if the session was available ever.
*/
private boolean wasAvailable = false;
/**
* Flag indicating if the user requested to not receive offline messages when sending
* an available presence. The user may send a disco request with node
* "http://jabber.org/protocol/offline" so that no offline messages are sent to the
* user when he becomes online. If the user is connected from many resources then
* if one of the sessions stopped the flooding then no session should flood the user.
*/
private boolean offlineFloodStopped = false;
private Presence presence = null;
private int conflictCount = 0;
/**
* Privacy list that overrides the default privacy list. This list affects only this
* session and only for the duration of the session.
*/
private String activeList;
/**
* Default privacy list used for the session's user. This list is processed if there
* is no active list set for the session.
*/
private String defaultList;
static {
// Fill out the allowedIPs with the system property
String allowed = JiveGlobals.getProperty(ConnectionSettings.Client.LOGIN_ALLOWED, "");
StringTokenizer tokens = new StringTokenizer(allowed, ", ");
while (tokens.hasMoreTokens()) {
String address = tokens.nextToken().trim();
allowedIPs.put(address, "");
}
String allowedAnonym = JiveGlobals.getProperty(ConnectionSettings.Client.LOGIN_ANONYM_ALLOWED, "");
tokens = new StringTokenizer(allowedAnonym, ", ");
while (tokens.hasMoreTokens()) {
String address = tokens.nextToken().trim();
allowedAnonymIPs.put(address, "");
}
}
/**
* Returns the list of IP address that are allowed to connect to the server. If the list is
* empty then anyone is allowed to connect to the server except for anonymous users that are
* subject to {@link #getAllowedAnonymIPs()}. This list is used for both anonymous and
* non-anonymous users.
*
* @return the list of IP address that are allowed to connect to the server.
*/
public static Map<String, String> getAllowedIPs() {
return allowedIPs;
}
/**
* Returns the list of IP address that are allowed to connect to the server for anonymous
* users. If the list is empty then anonymous will be only restricted by {@link #getAllowedIPs()}.
*
* @return the list of IP address that are allowed to connect to the server.
*/
public static Map<String, String> getAllowedAnonymIPs() {
return allowedAnonymIPs;
}
/**
* Returns a newly created session between the server and a client. The session will
* be created and returned only if correct name/prefix (i.e. 'stream' or 'flash')
* and namespace were provided by the client.
*
* @param serverName the name of the server where the session is connecting to.
* @param xpp the parser that is reading the provided XML through the connection.
* @param connection the connection with the client.
* @return a newly created session between the server and a client.
* @throws org.xmlpull.v1.XmlPullParserException if an error occurs while parsing incoming data.
*/
public static LocalClientSession createSession(String serverName, XmlPullParser xpp, Connection connection)
throws XmlPullParserException {
boolean isFlashClient = xpp.getPrefix().equals("flash");
connection.setFlashClient(isFlashClient);
// Conduct error checking, the opening tag should be 'stream'
// in the 'etherx' namespace
if (!xpp.getName().equals("stream") && !isFlashClient) {
throw new XmlPullParserException(
LocaleUtils.getLocalizedString("admin.error.bad-stream"));
}
if (!xpp.getNamespace(xpp.getPrefix()).equals(ETHERX_NAMESPACE) &&
!(isFlashClient && xpp.getNamespace(xpp.getPrefix()).equals(FLASH_NAMESPACE)))
{
throw new XmlPullParserException(LocaleUtils.getLocalizedString(
"admin.error.bad-namespace"));
}
if (!allowedIPs.isEmpty()) {
String hostAddress = "Unknown";
// The server is using a whitelist so check that the IP address of the client
// is authorized to connect to the server
try {
hostAddress = connection.getHostAddress();
} catch (UnknownHostException e) {
// Do nothing
}
if (!isAllowed(connection)) {
// Client cannot connect from this IP address so end the stream and
// TCP connection
Log.debug("LocalClientSession: Closed connection to client attempting to connect from: " + hostAddress);
// Include the not-authorized error in the response
StreamError error = new StreamError(StreamError.Condition.not_authorized);
connection.deliverRawText(error.toXML());
// Close the underlying connection
connection.close();
return null;
}
}
// Default language is English ("en").
String language = "en";
// Default to a version of "0.0". Clients written before the XMPP 1.0 spec may
// not report a version in which case "0.0" should be assumed (per rfc3920
// section 4.4.1).
int majorVersion = 0;
int minorVersion = 0;
for (int i = 0; i < xpp.getAttributeCount(); i++) {
if ("lang".equals(xpp.getAttributeName(i))) {
language = xpp.getAttributeValue(i);
}
if ("version".equals(xpp.getAttributeName(i))) {
try {
int[] version = decodeVersion(xpp.getAttributeValue(i));
majorVersion = version[0];
minorVersion = version[1];
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
}
}
// If the client supports a greater major version than the server,
// set the version to the highest one the server supports.
if (majorVersion > MAJOR_VERSION) {
majorVersion = MAJOR_VERSION;
minorVersion = MINOR_VERSION;
}
else if (majorVersion == MAJOR_VERSION) {
// If the client supports a greater minor version than the
// server, set the version to the highest one that the server
// supports.
if (minorVersion > MINOR_VERSION) {
minorVersion = MINOR_VERSION;
}
}
// Store language and version information in the connection.
connection.setLanaguage(language);
connection.setXMPPVersion(majorVersion, minorVersion);
// Indicate the TLS policy to use for this connection
if (!connection.isSecure()) {
boolean hasCertificates = false;
try {
hasCertificates = SSLConfig.getStore( Purpose.SOCKETBASED_IDENTITYSTORE ).size() > 0;
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
Connection.TLSPolicy tlsPolicy = getTLSPolicy();
if (Connection.TLSPolicy.required == tlsPolicy && !hasCertificates) {
Log.error("Client session rejected. TLS is required but no certificates " +
"were created.");
return null;
}
// Set default TLS policy
connection.setTlsPolicy(hasCertificates ? tlsPolicy : Connection.TLSPolicy.disabled);
} else {
// Set default TLS policy
connection.setTlsPolicy(Connection.TLSPolicy.disabled);
}
// Indicate the compression policy to use for this connection
connection.setCompressionPolicy(getCompressionPolicy());
// Create a ClientSession for this user.
LocalClientSession session = SessionManager.getInstance().createClientSession(connection);
// Build the start packet response
StringBuilder sb = new StringBuilder(200);
sb.append("<?xml version='1.0' encoding='");
sb.append(CHARSET);
sb.append("'?>");
if (isFlashClient) {
sb.append("<flash:stream xmlns:flash=\"http://www.jabber.com/streams/flash\" ");
}
else {
sb.append("<stream:stream ");
}
sb.append("xmlns:stream=\"http://etherx.jabber.org/streams\" xmlns=\"jabber:client\" from=\"");
sb.append(serverName);
sb.append("\" id=\"");
sb.append(session.getStreamID().toString());
sb.append("\" xml:lang=\"");
sb.append(language);
// Don't include version info if the version is 0.0.
if (majorVersion != 0) {
sb.append("\" version=\"");
sb.append(majorVersion).append('.').append(minorVersion);
}
sb.append("\">");
connection.deliverRawText(sb.toString());
// If this is a "Jabber" connection, the session is now initialized and we can
// return to allow normal packet parsing.
if (majorVersion == 0) {
return session;
}
// Otherwise, this is at least XMPP 1.0 so we need to announce stream features.
sb = new StringBuilder(490);
sb.append("<stream:features>");
if (connection.getTlsPolicy() != Connection.TLSPolicy.disabled) {
sb.append("<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\">");
if (connection.getTlsPolicy() == Connection.TLSPolicy.required) {
sb.append("<required/>");
}
sb.append("</starttls>");
}
// Include available SASL Mechanisms
sb.append(SASLAuthentication.getSASLMechanisms(session));
// Include Stream features
String specificFeatures = session.getAvailableStreamFeatures();
if (specificFeatures != null) {
sb.append(specificFeatures);
}
sb.append("</stream:features>");
connection.deliverRawText(sb.toString());
return session;
}
public static boolean isAllowed(Connection connection) {
if (!allowedIPs.isEmpty()) {
// The server is using a whitelist so check that the IP address of the client
// is authorized to connect to the server
boolean forbidAccess = false;
try {
if (!allowedIPs.containsKey(connection.getHostAddress())) {
byte[] address = connection.getAddress();
String range1 = (address[0] & 0xff) + "." + (address[1] & 0xff) + "." +
(address[2] & 0xff) +
".*";
String range2 = (address[0] & 0xff) + "." + (address[1] & 0xff) + ".*.*";
String range3 = (address[0] & 0xff) + ".*.*.*";
if (!allowedIPs.containsKey(range1) && !allowedIPs.containsKey(range2) &&
!allowedIPs.containsKey(range3)) {
forbidAccess = true;
}
}
} catch (UnknownHostException e) {
forbidAccess = true;
}
return !forbidAccess;
}
return true;
}
/**
* Sets the list of IP address that are allowed to connect to the server. If the list is
* empty then anyone is allowed to connect to the server except for anonymous users that are
* subject to {@link #getAllowedAnonymIPs()}. This list is used for both anonymous and
* non-anonymous users.
*
* @param allowed the list of IP address that are allowed to connect to the server.
*/
public static void setAllowedIPs(Map<String, String> allowed) {
allowedIPs = allowed;
if (allowedIPs.isEmpty()) {
JiveGlobals.deleteProperty(ConnectionSettings.Client.LOGIN_ALLOWED);
}
else {
// Iterate through the elements in the map.
StringBuilder buf = new StringBuilder();
Iterator<String> iter = allowedIPs.keySet().iterator();
if (iter.hasNext()) {
buf.append(iter.next());
}
while (iter.hasNext()) {
buf.append(", ").append(iter.next());
}
JiveGlobals.setProperty(ConnectionSettings.Client.LOGIN_ALLOWED, buf.toString());
}
}
/**
* Sets the list of IP address that are allowed to connect to the server for anonymous
* users. If the list is empty then anonymous will be only restricted by {@link #getAllowedIPs()}.
*
* @param allowed the list of IP address that are allowed to connect to the server.
*/
public static void setAllowedAnonymIPs(Map<String, String> allowed) {
allowedAnonymIPs = allowed;
if (allowedAnonymIPs.isEmpty()) {
JiveGlobals.deleteProperty(ConnectionSettings.Client.LOGIN_ANONYM_ALLOWED);
}
else {
// Iterate through the elements in the map.
StringBuilder buf = new StringBuilder();
Iterator<String> iter = allowedAnonymIPs.keySet().iterator();
if (iter.hasNext()) {
buf.append(iter.next());
}
while (iter.hasNext()) {
buf.append(", ").append(iter.next());
}
JiveGlobals.setProperty(ConnectionSettings.Client.LOGIN_ANONYM_ALLOWED, buf.toString());
}
}
/**
* Returns whether TLS is mandatory, optional or is disabled for clients. When TLS is
* mandatory clients are required to secure their connections or otherwise their connections
* will be closed. On the other hand, when TLS is disabled clients are not allowed to secure
* their connections using TLS. Their connections will be closed if they try to secure the
* connection. in this last case.
*
* @return whether TLS is mandatory, optional or is disabled.
*/
public static SocketConnection.TLSPolicy getTLSPolicy() {
// Set the TLS policy stored as a system property
String policyName = JiveGlobals.getProperty(ConnectionSettings.Client.TLS_POLICY, Connection.TLSPolicy.optional.toString());
SocketConnection.TLSPolicy tlsPolicy;
try {
tlsPolicy = Connection.TLSPolicy.valueOf(policyName);
} catch (IllegalArgumentException e) {
Log.error("Error parsing xmpp.client.tls.policy: " + policyName, e);
tlsPolicy = Connection.TLSPolicy.optional;
}
return tlsPolicy;
}
/**
* Sets whether TLS is mandatory, optional or is disabled for clients. When TLS is
* mandatory clients are required to secure their connections or otherwise their connections
* will be closed. On the other hand, when TLS is disabled clients are not allowed to secure
* their connections using TLS. Their connections will be closed if they try to secure the
* connection. in this last case.
*
* @param policy whether TLS is mandatory, optional or is disabled.
*/
public static void setTLSPolicy(SocketConnection.TLSPolicy policy) {
JiveGlobals.setProperty(ConnectionSettings.Client.TLS_POLICY, policy.toString());
}
/**
* Returns whether compression is optional or is disabled for clients.
*
* @return whether compression is optional or is disabled.
*/
public static SocketConnection.CompressionPolicy getCompressionPolicy() {
// Set the Compression policy stored as a system property
String policyName = JiveGlobals
.getProperty(ConnectionSettings.Client.COMPRESSION_SETTINGS, Connection.CompressionPolicy.optional.toString());
SocketConnection.CompressionPolicy compressionPolicy;
try {
compressionPolicy = Connection.CompressionPolicy.valueOf(policyName);
} catch (IllegalArgumentException e) {
Log.error("Error parsing xmpp.client.compression.policy: " + policyName, e);
compressionPolicy = Connection.CompressionPolicy.optional;
}
return compressionPolicy;
}
/**
* Sets whether compression is optional or is disabled for clients.
*
* @param policy whether compression is optional or is disabled.
*/
public static void setCompressionPolicy(SocketConnection.CompressionPolicy policy) {
JiveGlobals.setProperty(ConnectionSettings.Client.COMPRESSION_SETTINGS, policy.toString());
}
/**
* Returns the Privacy list that overrides the default privacy list. This list affects
* only this session and only for the duration of the session.
*
* @return the Privacy list that overrides the default privacy list.
*/
@Override
public PrivacyList getActiveList() {
if (activeList != null) {
try {
return PrivacyListManager.getInstance().getPrivacyList(getUsername(), activeList);
} catch (UserNotFoundException e) {
Log.error(e.getMessage(), e);
}
}
return null;
}
/**
* Sets the Privacy list that overrides the default privacy list. This list affects
* only this session and only for the duration of the session.
*
* @param activeList the Privacy list that overrides the default privacy list.
*/
@Override
public void setActiveList(PrivacyList activeList) {
this.activeList = activeList != null ? activeList.getName() : null;
if (ClusterManager.isClusteringStarted()) {
// Track information about the session and share it with other cluster nodes
Cache<String,ClientSessionInfo> cache = SessionManager.getInstance().getSessionInfoCache();
cache.put(getAddress().toString(), new ClientSessionInfo(this));
}
}
/**
* Returns the default Privacy list used for the session's user. This list is
* processed if there is no active list set for the session.
*
* @return the default Privacy list used for the session's user.
*/
@Override
public PrivacyList getDefaultList() {
if (defaultList != null) {
try {
return PrivacyListManager.getInstance().getPrivacyList(getUsername(), defaultList);
} catch (UserNotFoundException e) {
Log.error(e.getMessage(), e);
}
}
return null;
}
/**
* Sets the default Privacy list used for the session's user. This list is
* processed if there is no active list set for the session.
*
* @param defaultList the default Privacy list used for the session's user.
*/
@Override
public void setDefaultList(PrivacyList defaultList) {
// Do nothing if nothing has changed
if ((this.defaultList == null && defaultList == null) ||
(defaultList != null && defaultList.getName().equals(this.defaultList))) {
return;
}
this.defaultList = defaultList != null ? defaultList.getName() : null;
if (ClusterManager.isClusteringStarted()) {
// Track information about the session and share it with other cluster nodes
Cache<String,ClientSessionInfo> cache = SessionManager.getInstance().getSessionInfoCache();
cache.put(getAddress().toString(), new ClientSessionInfo(this));
}
}
/**
* Creates a session with an underlying connection and permission protection.
*
* @param serverName name of the server.
* @param connection The connection we are proxying.
* @param streamID unique identifier of this session.
*/
public LocalClientSession(String serverName, Connection connection, StreamID streamID) {
super(serverName, connection, streamID);
// Set an unavailable initial presence
presence = new Presence();
presence.setType(Presence.Type.unavailable);
}
/**
* Returns the username associated with this session. Use this information
* with the user manager to obtain the user based on username.
*
* @return the username associated with this session
* @throws org.jivesoftware.openfire.user.UserNotFoundException if a user is not associated with a session
* (the session has not authenticated yet)
*/
@Override
public String getUsername() throws UserNotFoundException {
if (authToken == null) {
throw new UserNotFoundException();
}
return getAddress().getNode();
}
/**
* Sets the new Authorization Token for this session. The session is not yet considered fully
* authenticated (i.e. active) since a resource has not been binded at this point. This
* message will be sent after SASL authentication was successful but yet resource binding
* is required.
*
* @param auth the authentication token obtained from SASL authentication.
*/
public void setAuthToken(AuthToken auth) {
authToken = auth;
}
/**
* Initialize the session with a valid authentication token and
* resource name. This automatically upgrades the session's
* status to authenticated and enables many features that are not
* available until authenticated (obtaining managers for example).
*
* @param auth the authentication token obtained from the AuthFactory.
* @param resource the resource this session authenticated under.
*/
public void setAuthToken(AuthToken auth, String resource) {
setAddress(new JID(auth.getUsername(), getServerName(), resource));
authToken = auth;
setStatus(Session.STATUS_AUTHENTICATED);
// Set default privacy list for this session
setDefaultList(PrivacyListManager.getInstance().getDefaultPrivacyList(auth.getUsername()));
// Add session to the session manager. The session will be added to the routing table as well
sessionManager.addSession(this);
}
/**
* Initialize the session as an anonymous login. This automatically upgrades the session's
* status to authenticated and enables many features that are not available until
* authenticated (obtaining managers for example).<p>
*/
public void setAnonymousAuth() {
// Anonymous users have a full JID. Use the random resource as the JID's node
String resource = getAddress().getResource();
setAddress(new JID(resource, getServerName(), resource, true));
setStatus(Session.STATUS_AUTHENTICATED);
if (authToken == null) {
authToken = new AuthToken(resource, true);
}
// Add session to the session manager. The session will be added to the routing table as well
sessionManager.addSession(this);
}
/**
* Returns the authentication token associated with this session.
*
* @return the authentication token associated with this session (can be null).
*/
public AuthToken getAuthToken() {
return authToken;
}
@Override
public boolean isAnonymousUser() {
return authToken == null || authToken.isAnonymous();
}
/**
* Flag indicating if this session has been initialized once coming
* online. Session initialization occurs after the session receives
* the first "available" presence update from the client. Initialization
* actions include pushing offline messages, presence subscription requests,
* and presence statuses to the client. Initialization occurs only once
* following the first available presence transition.
*
* @return True if the session has already been initializsed
*/
@Override
public boolean isInitialized() {
return initialized;
}
/**
* Sets the initialization state of the session.
*
* @param isInit True if the session has been initialized
* @see #isInitialized
*/
@Override
public void setInitialized(boolean isInit) {
initialized = isInit;
}
/**
* Returns true if the session was available ever.
*
* @return true if the session was available ever.
*/
public boolean wasAvailable() {
return wasAvailable;
}
/**
* Returns true if the offline messages of the user should be sent to the user when
* the user becomes online. If the user sent a disco request with node
* "http://jabber.org/protocol/offline" before the available presence then do not
* flood the user with the offline messages. If the user is connected from many resources
* then if one of the sessions stopped the flooding then no session should flood the user.
*
* @return true if the offline messages of the user should be sent to the user when the user
* becomes online.
* @see <a href="http://www.xmpp.org/extensions/xep-0160.html">XEP-0160: Best Practices for Handling Offline Messages</a>
*/
@Override
public boolean canFloodOfflineMessages() {
// XEP-0160: When the recipient next sends non-negative available presence to the server, the server delivers the message to the resource that has sent that presence.
if(offlineFloodStopped || presence.getPriority() < 0) {
return false;
}
String username = getAddress().getNode();
for (ClientSession session : sessionManager.getSessions(username)) {
if (session.isOfflineFloodStopped()) {
return false;
}
}
return true;
}
/**
* Returns true if the user requested to not receive offline messages when sending
* an available presence. The user may send a disco request with node
* "http://jabber.org/protocol/offline" so that no offline messages are sent to the
* user when he becomes online. If the user is connected from many resources then
* if one of the sessions stopped the flooding then no session should flood the user.
*
* @return true if the user requested to not receive offline messages when sending
* an available presence.
*/
@Override
public boolean isOfflineFloodStopped() {
return offlineFloodStopped;
}
/**
* Sets if the user requested to not receive offline messages when sending
* an available presence. The user may send a disco request with node
* "http://jabber.org/protocol/offline" so that no offline messages are sent to the
* user when he becomes online. If the user is connected from many resources then
* if one of the sessions stopped the flooding then no session should flood the user.
*
* @param offlineFloodStopped if the user requested to not receive offline messages when
* sending an available presence.
*/
public void setOfflineFloodStopped(boolean offlineFloodStopped) {
this.offlineFloodStopped = offlineFloodStopped;
if (ClusterManager.isClusteringStarted()) {
// Track information about the session and share it with other cluster nodes
Cache<String,ClientSessionInfo> cache = SessionManager.getInstance().getSessionInfoCache();
cache.put(getAddress().toString(), new ClientSessionInfo(this));
}
}
/**
* Obtain the presence of this session.
*
* @return The presence of this session or null if not authenticated
*/
@Override
public Presence getPresence() {
return presence;
}
/**
* Set the presence of this session
*
* @param presence The presence for the session
*/
@Override
public void setPresence(Presence presence) {
Presence oldPresence = this.presence;
this.presence = presence;
if (oldPresence.isAvailable() && !this.presence.isAvailable()) {
// The client is no longer available
sessionManager.sessionUnavailable(this);
// Mark that the session is no longer initialized. This means that if the user sends
// an available presence again the session will be initialized again thus receiving
// offline messages and offline presence subscription requests
setInitialized(false);
// Notify listeners that the session is no longer available
PresenceEventDispatcher.unavailableSession(this, presence);
}
else if (!oldPresence.isAvailable() && this.presence.isAvailable()) {
// The client is available
sessionManager.sessionAvailable(this, presence);
wasAvailable = true;
// Notify listeners that the session is now available
PresenceEventDispatcher.availableSession(this, presence);
}
else if (this.presence.isAvailable() && oldPresence.getPriority() != this.presence.getPriority())
{
// The client has changed the priority of his presence
sessionManager.changePriority(this, oldPresence.getPriority());
// Notify listeners that the priority of the session/resource has changed
PresenceEventDispatcher.presenceChanged(this, presence);
}
else if (this.presence.isAvailable()) {
// Notify listeners that the show or status value of the presence has changed
PresenceEventDispatcher.presenceChanged(this, presence);
}
if (ClusterManager.isClusteringStarted()) {
// Track information about the session and share it with other cluster nodes
Cache<String,ClientSessionInfo> cache = SessionManager.getInstance().getSessionInfoCache();
cache.put(getAddress().toString(), new ClientSessionInfo(this));
}
}
@Override
public String getAvailableStreamFeatures() {
// Offer authenticate and registration only if TLS was not required or if required
// then the connection is already secured
if (conn.getTlsPolicy() == Connection.TLSPolicy.required && !conn.isSecure()) {
return null;
}
StringBuilder sb = new StringBuilder(200);
// Include Stream Compression Mechanism
if (conn.getCompressionPolicy() != Connection.CompressionPolicy.disabled &&
!conn.isCompressed()) {
sb.append(
"<compression xmlns=\"http://jabber.org/features/compress\"><method>zlib</method></compression>");
}
if (getAuthToken() == null) {
// Advertise that the server supports Non-SASL Authentication
sb.append("<auth xmlns=\"http://jabber.org/features/iq-auth\"/>");
// Advertise that the server supports In-Band Registration
if (XMPPServer.getInstance().getIQRegisterHandler().isInbandRegEnabled()) {
sb.append("<register xmlns=\"http://jabber.org/features/iq-register\"/>");
}
}
else {
// If the session has been authenticated then offer resource binding,
// and session establishment
sb.append("<bind xmlns=\"urn:ietf:params:xml:ns:xmpp-bind\"/>");
sb.append("<session xmlns=\"urn:ietf:params:xml:ns:xmpp-session\"><optional/></session>");
// Offer XEP-0198 stream management capabilities if enabled.
if(JiveGlobals.getBooleanProperty("stream.management.active", true)) {
sb.append(String.format("<sm xmlns='%s'/>", StreamManager.NAMESPACE_V2));
sb.append(String.format("<sm xmlns='%s'/>", StreamManager.NAMESPACE_V3));
}
}
return sb.toString();
}
/**
* Increments the conflict by one.
*/
@Override
public int incrementConflictCount() {
conflictCount++;
return conflictCount;
}
@Override
public boolean isMessageCarbonsEnabled() {
return messageCarbonsEnabled;
}
@Override
public void setMessageCarbonsEnabled(boolean enabled) {
messageCarbonsEnabled = enabled;
}
/**
* Returns true if the specified packet must not be blocked based on the active or default
* privacy list rules. The active list will be tried first. If none was found then the
* default list is going to be used. If no default list was defined for this user then
* allow the packet to flow.
*
* @param packet the packet to analyze if it must be blocked.
* @return true if the specified packet must be blocked.
*/
@Override
public boolean canProcess(Packet packet) {
PrivacyList list = getActiveList();
if (list != null) {
// If a privacy list is active then make sure that the packet is not blocked
return !list.shouldBlockPacket(packet);
}
else {
list = getDefaultList();
// There is no active list so check if there exists a default list and make
// sure that the packet is not blocked
return list == null || !list.shouldBlockPacket(packet);
}
}
@Override
public void deliver(Packet packet) throws UnauthorizedException {
conn.deliver(packet);
if(streamManager.isEnabled()) {
streamManager.incrementServerSentStanzas();
// Temporarily store packet until delivery confirmed
streamManager.getUnacknowledgedServerStanzas().addLast(new StreamManager.UnackedPacket(new Date(), packet.createCopy()));
if(getNumServerPackets() % JiveGlobals.getLongProperty("stream.management.requestFrequency", 5) == 0) {
streamManager.sendServerRequest();
}
}
}
@Override
public String toString() {
return super.toString() + " presence: " + presence;
}
}
/**
* $RCSfile$
* $Revision: 3187 $
* $Date: 2005-12-11 13:34:34 -0300 (Sun, 11 Dec 2005) $
*
* Copyright (C) 2005-2008 Jive Software. 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.session;
import java.net.UnknownHostException;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.StringTokenizer;
import org.jivesoftware.openfire.Connection;
import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.openfire.StreamID;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.auth.AuthToken;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.cluster.ClusterManager;
import org.jivesoftware.openfire.keystore.Purpose;
import org.jivesoftware.openfire.net.SASLAuthentication;
import org.jivesoftware.openfire.net.SSLConfig;
import org.jivesoftware.openfire.net.SocketConnection;
import org.jivesoftware.openfire.privacy.PrivacyList;
import org.jivesoftware.openfire.privacy.PrivacyListManager;
import org.jivesoftware.openfire.streammanagement.StreamManager;
import org.jivesoftware.openfire.user.PresenceEventDispatcher;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.cache.Cache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmpp.packet.JID;
import org.xmpp.packet.Packet;
import org.xmpp.packet.Presence;
import org.xmpp.packet.StreamError;
/**
* Represents a session between the server and a client.
*
* @author Gaston Dombiak
*/
public class LocalClientSession extends LocalSession implements ClientSession {
private static final Logger Log = LoggerFactory.getLogger(LocalClientSession.class);
private static final String ETHERX_NAMESPACE = "http://etherx.jabber.org/streams";
private static final String FLASH_NAMESPACE = "http://www.jabber.com/streams/flash";
/**
* Keep the list of IP address that are allowed to connect to the server. If the list is
* empty then anyone is allowed to connect to the server.<p>
*
* Note: Key = IP address or IP range; Value = empty string. A hash map is being used for
* performance reasons.
*/
private static Map<String,String> allowedIPs = new HashMap<>();
private static Map<String,String> allowedAnonymIPs = new HashMap<>();
private boolean messageCarbonsEnabled;
/**
* The authentication token for this session.
*/
protected AuthToken authToken;
/**
* Flag indicating if this session has been initialized yet (upon first available transition).
*/
private boolean initialized;
/**
* Flag that indicates if the session was available ever.
*/
private boolean wasAvailable = false;
/**
* Flag indicating if the user requested to not receive offline messages when sending
* an available presence. The user may send a disco request with node
* "http://jabber.org/protocol/offline" so that no offline messages are sent to the
* user when he becomes online. If the user is connected from many resources then
* if one of the sessions stopped the flooding then no session should flood the user.
*/
private boolean offlineFloodStopped = false;
private Presence presence = null;
private int conflictCount = 0;
/**
* Privacy list that overrides the default privacy list. This list affects only this
* session and only for the duration of the session.
*/
private String activeList;
/**
* Default privacy list used for the session's user. This list is processed if there
* is no active list set for the session.
*/
private String defaultList;
static {
// Fill out the allowedIPs with the system property
String allowed = JiveGlobals.getProperty(ConnectionSettings.Client.LOGIN_ALLOWED, "");
StringTokenizer tokens = new StringTokenizer(allowed, ", ");
while (tokens.hasMoreTokens()) {
String address = tokens.nextToken().trim();
allowedIPs.put(address, "");
}
String allowedAnonym = JiveGlobals.getProperty(ConnectionSettings.Client.LOGIN_ANONYM_ALLOWED, "");
tokens = new StringTokenizer(allowedAnonym, ", ");
while (tokens.hasMoreTokens()) {
String address = tokens.nextToken().trim();
allowedAnonymIPs.put(address, "");
}
}
/**
* Returns the list of IP address that are allowed to connect to the server. If the list is
* empty then anyone is allowed to connect to the server except for anonymous users that are
* subject to {@link #getAllowedAnonymIPs()}. This list is used for both anonymous and
* non-anonymous users.
*
* @return the list of IP address that are allowed to connect to the server.
*/
public static Map<String, String> getAllowedIPs() {
return allowedIPs;
}
/**
* Returns the list of IP address that are allowed to connect to the server for anonymous
* users. If the list is empty then anonymous will be only restricted by {@link #getAllowedIPs()}.
*
* @return the list of IP address that are allowed to connect to the server.
*/
public static Map<String, String> getAllowedAnonymIPs() {
return allowedAnonymIPs;
}
/**
* Returns a newly created session between the server and a client. The session will
* be created and returned only if correct name/prefix (i.e. 'stream' or 'flash')
* and namespace were provided by the client.
*
* @param serverName the name of the server where the session is connecting to.
* @param xpp the parser that is reading the provided XML through the connection.
* @param connection the connection with the client.
* @return a newly created session between the server and a client.
* @throws org.xmlpull.v1.XmlPullParserException if an error occurs while parsing incoming data.
*/
public static LocalClientSession createSession(String serverName, XmlPullParser xpp, Connection connection)
throws XmlPullParserException {
boolean isFlashClient = xpp.getPrefix().equals("flash");
connection.setFlashClient(isFlashClient);
// Conduct error checking, the opening tag should be 'stream'
// in the 'etherx' namespace
if (!xpp.getName().equals("stream") && !isFlashClient) {
throw new XmlPullParserException(
LocaleUtils.getLocalizedString("admin.error.bad-stream"));
}
if (!xpp.getNamespace(xpp.getPrefix()).equals(ETHERX_NAMESPACE) &&
!(isFlashClient && xpp.getNamespace(xpp.getPrefix()).equals(FLASH_NAMESPACE)))
{
throw new XmlPullParserException(LocaleUtils.getLocalizedString(
"admin.error.bad-namespace"));
}
if (!allowedIPs.isEmpty()) {
String hostAddress = "Unknown";
// The server is using a whitelist so check that the IP address of the client
// is authorized to connect to the server
try {
hostAddress = connection.getHostAddress();
} catch (UnknownHostException e) {
// Do nothing
}
if (!isAllowed(connection)) {
// Client cannot connect from this IP address so end the stream and
// TCP connection
Log.debug("LocalClientSession: Closed connection to client attempting to connect from: " + hostAddress);
// Include the not-authorized error in the response
StreamError error = new StreamError(StreamError.Condition.not_authorized);
connection.deliverRawText(error.toXML());
// Close the underlying connection
connection.close();
return null;
}
}
// Default language is English ("en").
String language = "en";
// Default to a version of "0.0". Clients written before the XMPP 1.0 spec may
// not report a version in which case "0.0" should be assumed (per rfc3920
// section 4.4.1).
int majorVersion = 0;
int minorVersion = 0;
for (int i = 0; i < xpp.getAttributeCount(); i++) {
if ("lang".equals(xpp.getAttributeName(i))) {
language = xpp.getAttributeValue(i);
}
if ("version".equals(xpp.getAttributeName(i))) {
try {
int[] version = decodeVersion(xpp.getAttributeValue(i));
majorVersion = version[0];
minorVersion = version[1];
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
}
}
// If the client supports a greater major version than the server,
// set the version to the highest one the server supports.
if (majorVersion > MAJOR_VERSION) {
majorVersion = MAJOR_VERSION;
minorVersion = MINOR_VERSION;
}
else if (majorVersion == MAJOR_VERSION) {
// If the client supports a greater minor version than the
// server, set the version to the highest one that the server
// supports.
if (minorVersion > MINOR_VERSION) {
minorVersion = MINOR_VERSION;
}
}
// Store language and version information in the connection.
connection.setLanaguage(language);
connection.setXMPPVersion(majorVersion, minorVersion);
// Indicate the TLS policy to use for this connection
if (!connection.isSecure()) {
boolean hasCertificates = false;
try {
hasCertificates = SSLConfig.getIdentityStore( Purpose.SOCKET_C2S ).size() > 0;
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
Connection.TLSPolicy tlsPolicy = getTLSPolicy();
if (Connection.TLSPolicy.required == tlsPolicy && !hasCertificates) {
Log.error("Client session rejected. TLS is required but no certificates " +
"were created.");
return null;
}
// Set default TLS policy
connection.setTlsPolicy(hasCertificates ? tlsPolicy : Connection.TLSPolicy.disabled);
} else {
// Set default TLS policy
connection.setTlsPolicy(Connection.TLSPolicy.disabled);
}
// Indicate the compression policy to use for this connection
connection.setCompressionPolicy(getCompressionPolicy());
// Create a ClientSession for this user.
LocalClientSession session = SessionManager.getInstance().createClientSession(connection);
// Build the start packet response
StringBuilder sb = new StringBuilder(200);
sb.append("<?xml version='1.0' encoding='");
sb.append(CHARSET);
sb.append("'?>");
if (isFlashClient) {
sb.append("<flash:stream xmlns:flash=\"http://www.jabber.com/streams/flash\" ");
}
else {
sb.append("<stream:stream ");
}
sb.append("xmlns:stream=\"http://etherx.jabber.org/streams\" xmlns=\"jabber:client\" from=\"");
sb.append(serverName);
sb.append("\" id=\"");
sb.append(session.getStreamID().toString());
sb.append("\" xml:lang=\"");
sb.append(language);
// Don't include version info if the version is 0.0.
if (majorVersion != 0) {
sb.append("\" version=\"");
sb.append(majorVersion).append('.').append(minorVersion);
}
sb.append("\">");
connection.deliverRawText(sb.toString());
// If this is a "Jabber" connection, the session is now initialized and we can
// return to allow normal packet parsing.
if (majorVersion == 0) {
return session;
}
// Otherwise, this is at least XMPP 1.0 so we need to announce stream features.
sb = new StringBuilder(490);
sb.append("<stream:features>");
if (connection.getTlsPolicy() != Connection.TLSPolicy.disabled) {
sb.append("<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\">");
if (connection.getTlsPolicy() == Connection.TLSPolicy.required) {
sb.append("<required/>");
}
sb.append("</starttls>");
}
// Include available SASL Mechanisms
sb.append(SASLAuthentication.getSASLMechanisms(session));
// Include Stream features
String specificFeatures = session.getAvailableStreamFeatures();
if (specificFeatures != null) {
sb.append(specificFeatures);
}
sb.append("</stream:features>");
connection.deliverRawText(sb.toString());
return session;
}
public static boolean isAllowed(Connection connection) {
if (!allowedIPs.isEmpty()) {
// The server is using a whitelist so check that the IP address of the client
// is authorized to connect to the server
boolean forbidAccess = false;
try {
if (!allowedIPs.containsKey(connection.getHostAddress())) {
byte[] address = connection.getAddress();
String range1 = (address[0] & 0xff) + "." + (address[1] & 0xff) + "." +
(address[2] & 0xff) +
".*";
String range2 = (address[0] & 0xff) + "." + (address[1] & 0xff) + ".*.*";
String range3 = (address[0] & 0xff) + ".*.*.*";
if (!allowedIPs.containsKey(range1) && !allowedIPs.containsKey(range2) &&
!allowedIPs.containsKey(range3)) {
forbidAccess = true;
}
}
} catch (UnknownHostException e) {
forbidAccess = true;
}
return !forbidAccess;
}
return true;
}
/**
* Sets the list of IP address that are allowed to connect to the server. If the list is
* empty then anyone is allowed to connect to the server except for anonymous users that are
* subject to {@link #getAllowedAnonymIPs()}. This list is used for both anonymous and
* non-anonymous users.
*
* @param allowed the list of IP address that are allowed to connect to the server.
*/
public static void setAllowedIPs(Map<String, String> allowed) {
allowedIPs = allowed;
if (allowedIPs.isEmpty()) {
JiveGlobals.deleteProperty(ConnectionSettings.Client.LOGIN_ALLOWED);
}
else {
// Iterate through the elements in the map.
StringBuilder buf = new StringBuilder();
Iterator<String> iter = allowedIPs.keySet().iterator();
if (iter.hasNext()) {
buf.append(iter.next());
}
while (iter.hasNext()) {
buf.append(", ").append(iter.next());
}
JiveGlobals.setProperty(ConnectionSettings.Client.LOGIN_ALLOWED, buf.toString());
}
}
/**
* Sets the list of IP address that are allowed to connect to the server for anonymous
* users. If the list is empty then anonymous will be only restricted by {@link #getAllowedIPs()}.
*
* @param allowed the list of IP address that are allowed to connect to the server.
*/
public static void setAllowedAnonymIPs(Map<String, String> allowed) {
allowedAnonymIPs = allowed;
if (allowedAnonymIPs.isEmpty()) {
JiveGlobals.deleteProperty(ConnectionSettings.Client.LOGIN_ANONYM_ALLOWED);
}
else {
// Iterate through the elements in the map.
StringBuilder buf = new StringBuilder();
Iterator<String> iter = allowedAnonymIPs.keySet().iterator();
if (iter.hasNext()) {
buf.append(iter.next());
}
while (iter.hasNext()) {
buf.append(", ").append(iter.next());
}
JiveGlobals.setProperty(ConnectionSettings.Client.LOGIN_ANONYM_ALLOWED, buf.toString());
}
}
/**
* Returns whether TLS is mandatory, optional or is disabled for clients. When TLS is
* mandatory clients are required to secure their connections or otherwise their connections
* will be closed. On the other hand, when TLS is disabled clients are not allowed to secure
* their connections using TLS. Their connections will be closed if they try to secure the
* connection. in this last case.
*
* @return whether TLS is mandatory, optional or is disabled.
*/
public static SocketConnection.TLSPolicy getTLSPolicy() {
// Set the TLS policy stored as a system property
String policyName = JiveGlobals.getProperty(ConnectionSettings.Client.TLS_POLICY, Connection.TLSPolicy.optional.toString());
SocketConnection.TLSPolicy tlsPolicy;
try {
tlsPolicy = Connection.TLSPolicy.valueOf(policyName);
} catch (IllegalArgumentException e) {
Log.error("Error parsing xmpp.client.tls.policy: " + policyName, e);
tlsPolicy = Connection.TLSPolicy.optional;
}
return tlsPolicy;
}
/**
* Sets whether TLS is mandatory, optional or is disabled for clients. When TLS is
* mandatory clients are required to secure their connections or otherwise their connections
* will be closed. On the other hand, when TLS is disabled clients are not allowed to secure
* their connections using TLS. Their connections will be closed if they try to secure the
* connection. in this last case.
*
* @param policy whether TLS is mandatory, optional or is disabled.
*/
public static void setTLSPolicy(SocketConnection.TLSPolicy policy) {
JiveGlobals.setProperty(ConnectionSettings.Client.TLS_POLICY, policy.toString());
}
/**
* Returns whether compression is optional or is disabled for clients.
*
* @return whether compression is optional or is disabled.
*/
public static SocketConnection.CompressionPolicy getCompressionPolicy() {
// Set the Compression policy stored as a system property
String policyName = JiveGlobals
.getProperty(ConnectionSettings.Client.COMPRESSION_SETTINGS, Connection.CompressionPolicy.optional.toString());
SocketConnection.CompressionPolicy compressionPolicy;
try {
compressionPolicy = Connection.CompressionPolicy.valueOf(policyName);
} catch (IllegalArgumentException e) {
Log.error("Error parsing xmpp.client.compression.policy: " + policyName, e);
compressionPolicy = Connection.CompressionPolicy.optional;
}
return compressionPolicy;
}
/**
* Sets whether compression is optional or is disabled for clients.
*
* @param policy whether compression is optional or is disabled.
*/
public static void setCompressionPolicy(SocketConnection.CompressionPolicy policy) {
JiveGlobals.setProperty(ConnectionSettings.Client.COMPRESSION_SETTINGS, policy.toString());
}
/**
* Returns the Privacy list that overrides the default privacy list. This list affects
* only this session and only for the duration of the session.
*
* @return the Privacy list that overrides the default privacy list.
*/
@Override
public PrivacyList getActiveList() {
if (activeList != null) {
try {
return PrivacyListManager.getInstance().getPrivacyList(getUsername(), activeList);
} catch (UserNotFoundException e) {
Log.error(e.getMessage(), e);
}
}
return null;
}
/**
* Sets the Privacy list that overrides the default privacy list. This list affects
* only this session and only for the duration of the session.
*
* @param activeList the Privacy list that overrides the default privacy list.
*/
@Override
public void setActiveList(PrivacyList activeList) {
this.activeList = activeList != null ? activeList.getName() : null;
if (ClusterManager.isClusteringStarted()) {
// Track information about the session and share it with other cluster nodes
Cache<String,ClientSessionInfo> cache = SessionManager.getInstance().getSessionInfoCache();
cache.put(getAddress().toString(), new ClientSessionInfo(this));
}
}
/**
* Returns the default Privacy list used for the session's user. This list is
* processed if there is no active list set for the session.
*
* @return the default Privacy list used for the session's user.
*/
@Override
public PrivacyList getDefaultList() {
if (defaultList != null) {
try {
return PrivacyListManager.getInstance().getPrivacyList(getUsername(), defaultList);
} catch (UserNotFoundException e) {
Log.error(e.getMessage(), e);
}
}
return null;
}
/**
* Sets the default Privacy list used for the session's user. This list is
* processed if there is no active list set for the session.
*
* @param defaultList the default Privacy list used for the session's user.
*/
@Override
public void setDefaultList(PrivacyList defaultList) {
// Do nothing if nothing has changed
if ((this.defaultList == null && defaultList == null) ||
(defaultList != null && defaultList.getName().equals(this.defaultList))) {
return;
}
this.defaultList = defaultList != null ? defaultList.getName() : null;
if (ClusterManager.isClusteringStarted()) {
// Track information about the session and share it with other cluster nodes
Cache<String,ClientSessionInfo> cache = SessionManager.getInstance().getSessionInfoCache();
cache.put(getAddress().toString(), new ClientSessionInfo(this));
}
}
/**
* Creates a session with an underlying connection and permission protection.
*
* @param serverName name of the server.
* @param connection The connection we are proxying.
* @param streamID unique identifier of this session.
*/
public LocalClientSession(String serverName, Connection connection, StreamID streamID) {
super(serverName, connection, streamID);
// Set an unavailable initial presence
presence = new Presence();
presence.setType(Presence.Type.unavailable);
}
/**
* Returns the username associated with this session. Use this information
* with the user manager to obtain the user based on username.
*
* @return the username associated with this session
* @throws org.jivesoftware.openfire.user.UserNotFoundException if a user is not associated with a session
* (the session has not authenticated yet)
*/
@Override
public String getUsername() throws UserNotFoundException {
if (authToken == null) {
throw new UserNotFoundException();
}
return getAddress().getNode();
}
/**
* Sets the new Authorization Token for this session. The session is not yet considered fully
* authenticated (i.e. active) since a resource has not been binded at this point. This
* message will be sent after SASL authentication was successful but yet resource binding
* is required.
*
* @param auth the authentication token obtained from SASL authentication.
*/
public void setAuthToken(AuthToken auth) {
authToken = auth;
}
/**
* Initialize the session with a valid authentication token and
* resource name. This automatically upgrades the session's
* status to authenticated and enables many features that are not
* available until authenticated (obtaining managers for example).
*
* @param auth the authentication token obtained from the AuthFactory.
* @param resource the resource this session authenticated under.
*/
public void setAuthToken(AuthToken auth, String resource) {
setAddress(new JID(auth.getUsername(), getServerName(), resource));
authToken = auth;
setStatus(Session.STATUS_AUTHENTICATED);
// Set default privacy list for this session
setDefaultList(PrivacyListManager.getInstance().getDefaultPrivacyList(auth.getUsername()));
// Add session to the session manager. The session will be added to the routing table as well
sessionManager.addSession(this);
}
/**
* Initialize the session as an anonymous login. This automatically upgrades the session's
* status to authenticated and enables many features that are not available until
* authenticated (obtaining managers for example).<p>
*/
public void setAnonymousAuth() {
// Anonymous users have a full JID. Use the random resource as the JID's node
String resource = getAddress().getResource();
setAddress(new JID(resource, getServerName(), resource, true));
setStatus(Session.STATUS_AUTHENTICATED);
if (authToken == null) {
authToken = new AuthToken(resource, true);
}
// Add session to the session manager. The session will be added to the routing table as well
sessionManager.addSession(this);
}
/**
* Returns the authentication token associated with this session.
*
* @return the authentication token associated with this session (can be null).
*/
public AuthToken getAuthToken() {
return authToken;
}
@Override
public boolean isAnonymousUser() {
return authToken == null || authToken.isAnonymous();
}
/**
* Flag indicating if this session has been initialized once coming
* online. Session initialization occurs after the session receives
* the first "available" presence update from the client. Initialization
* actions include pushing offline messages, presence subscription requests,
* and presence statuses to the client. Initialization occurs only once
* following the first available presence transition.
*
* @return True if the session has already been initializsed
*/
@Override
public boolean isInitialized() {
return initialized;
}
/**
* Sets the initialization state of the session.
*
* @param isInit True if the session has been initialized
* @see #isInitialized
*/
@Override
public void setInitialized(boolean isInit) {
initialized = isInit;
}
/**
* Returns true if the session was available ever.
*
* @return true if the session was available ever.
*/
public boolean wasAvailable() {
return wasAvailable;
}
/**
* Returns true if the offline messages of the user should be sent to the user when
* the user becomes online. If the user sent a disco request with node
* "http://jabber.org/protocol/offline" before the available presence then do not
* flood the user with the offline messages. If the user is connected from many resources
* then if one of the sessions stopped the flooding then no session should flood the user.
*
* @return true if the offline messages of the user should be sent to the user when the user
* becomes online.
* @see <a href="http://www.xmpp.org/extensions/xep-0160.html">XEP-0160: Best Practices for Handling Offline Messages</a>
*/
@Override
public boolean canFloodOfflineMessages() {
// XEP-0160: When the recipient next sends non-negative available presence to the server, the server delivers the message to the resource that has sent that presence.
if(offlineFloodStopped || presence.getPriority() < 0) {
return false;
}
String username = getAddress().getNode();
for (ClientSession session : sessionManager.getSessions(username)) {
if (session.isOfflineFloodStopped()) {
return false;
}
}
return true;
}
/**
* Returns true if the user requested to not receive offline messages when sending
* an available presence. The user may send a disco request with node
* "http://jabber.org/protocol/offline" so that no offline messages are sent to the
* user when he becomes online. If the user is connected from many resources then
* if one of the sessions stopped the flooding then no session should flood the user.
*
* @return true if the user requested to not receive offline messages when sending
* an available presence.
*/
@Override
public boolean isOfflineFloodStopped() {
return offlineFloodStopped;
}
/**
* Sets if the user requested to not receive offline messages when sending
* an available presence. The user may send a disco request with node
* "http://jabber.org/protocol/offline" so that no offline messages are sent to the
* user when he becomes online. If the user is connected from many resources then
* if one of the sessions stopped the flooding then no session should flood the user.
*
* @param offlineFloodStopped if the user requested to not receive offline messages when
* sending an available presence.
*/
public void setOfflineFloodStopped(boolean offlineFloodStopped) {
this.offlineFloodStopped = offlineFloodStopped;
if (ClusterManager.isClusteringStarted()) {
// Track information about the session and share it with other cluster nodes
Cache<String,ClientSessionInfo> cache = SessionManager.getInstance().getSessionInfoCache();
cache.put(getAddress().toString(), new ClientSessionInfo(this));
}
}
/**
* Obtain the presence of this session.
*
* @return The presence of this session or null if not authenticated
*/
@Override
public Presence getPresence() {
return presence;
}
/**
* Set the presence of this session
*
* @param presence The presence for the session
*/
@Override
public void setPresence(Presence presence) {
Presence oldPresence = this.presence;
this.presence = presence;
if (oldPresence.isAvailable() && !this.presence.isAvailable()) {
// The client is no longer available
sessionManager.sessionUnavailable(this);
// Mark that the session is no longer initialized. This means that if the user sends
// an available presence again the session will be initialized again thus receiving
// offline messages and offline presence subscription requests
setInitialized(false);
// Notify listeners that the session is no longer available
PresenceEventDispatcher.unavailableSession(this, presence);
}
else if (!oldPresence.isAvailable() && this.presence.isAvailable()) {
// The client is available
sessionManager.sessionAvailable(this, presence);
wasAvailable = true;
// Notify listeners that the session is now available
PresenceEventDispatcher.availableSession(this, presence);
}
else if (this.presence.isAvailable() && oldPresence.getPriority() != this.presence.getPriority())
{
// The client has changed the priority of his presence
sessionManager.changePriority(this, oldPresence.getPriority());
// Notify listeners that the priority of the session/resource has changed
PresenceEventDispatcher.presenceChanged(this, presence);
}
else if (this.presence.isAvailable()) {
// Notify listeners that the show or status value of the presence has changed
PresenceEventDispatcher.presenceChanged(this, presence);
}
if (ClusterManager.isClusteringStarted()) {
// Track information about the session and share it with other cluster nodes
Cache<String,ClientSessionInfo> cache = SessionManager.getInstance().getSessionInfoCache();
cache.put(getAddress().toString(), new ClientSessionInfo(this));
}
}
@Override
public String getAvailableStreamFeatures() {
// Offer authenticate and registration only if TLS was not required or if required
// then the connection is already secured
if (conn.getTlsPolicy() == Connection.TLSPolicy.required && !conn.isSecure()) {
return null;
}
StringBuilder sb = new StringBuilder(200);
// Include Stream Compression Mechanism
if (conn.getCompressionPolicy() != Connection.CompressionPolicy.disabled &&
!conn.isCompressed()) {
sb.append(
"<compression xmlns=\"http://jabber.org/features/compress\"><method>zlib</method></compression>");
}
if (getAuthToken() == null) {
// Advertise that the server supports Non-SASL Authentication
sb.append("<auth xmlns=\"http://jabber.org/features/iq-auth\"/>");
// Advertise that the server supports In-Band Registration
if (XMPPServer.getInstance().getIQRegisterHandler().isInbandRegEnabled()) {
sb.append("<register xmlns=\"http://jabber.org/features/iq-register\"/>");
}
}
else {
// If the session has been authenticated then offer resource binding,
// and session establishment
sb.append("<bind xmlns=\"urn:ietf:params:xml:ns:xmpp-bind\"/>");
sb.append("<session xmlns=\"urn:ietf:params:xml:ns:xmpp-session\"><optional/></session>");
// Offer XEP-0198 stream management capabilities if enabled.
if(JiveGlobals.getBooleanProperty("stream.management.active", true)) {
sb.append(String.format("<sm xmlns='%s'/>", StreamManager.NAMESPACE_V2));
sb.append(String.format("<sm xmlns='%s'/>", StreamManager.NAMESPACE_V3));
}
}
return sb.toString();
}
/**
* Increments the conflict by one.
*/
@Override
public int incrementConflictCount() {
conflictCount++;
return conflictCount;
}
@Override
public boolean isMessageCarbonsEnabled() {
return messageCarbonsEnabled;
}
@Override
public void setMessageCarbonsEnabled(boolean enabled) {
messageCarbonsEnabled = enabled;
}
/**
* Returns true if the specified packet must not be blocked based on the active or default
* privacy list rules. The active list will be tried first. If none was found then the
* default list is going to be used. If no default list was defined for this user then
* allow the packet to flow.
*
* @param packet the packet to analyze if it must be blocked.
* @return true if the specified packet must be blocked.
*/
@Override
public boolean canProcess(Packet packet) {
PrivacyList list = getActiveList();
if (list != null) {
// If a privacy list is active then make sure that the packet is not blocked
return !list.shouldBlockPacket(packet);
}
else {
list = getDefaultList();
// There is no active list so check if there exists a default list and make
// sure that the packet is not blocked
return list == null || !list.shouldBlockPacket(packet);
}
}
@Override
public void deliver(Packet packet) throws UnauthorizedException {
conn.deliver(packet);
if(streamManager.isEnabled()) {
streamManager.incrementServerSentStanzas();
// Temporarily store packet until delivery confirmed
streamManager.getUnacknowledgedServerStanzas().addLast(new StreamManager.UnackedPacket(new Date(), packet.createCopy()));
if(getNumServerPackets() % JiveGlobals.getLongProperty("stream.management.requestFrequency", 5) == 0) {
streamManager.sendServerRequest();
}
}
}
@Override
public String toString() {
return super.toString() + " presence: " + presence;
}
}
/**
* $RCSfile: $
* $Revision: $
* $Date: $
*
* Copyright (C) 2005-2008 Jive Software. 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.session;
import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.dom4j.Element;
import org.dom4j.io.XMPPPacketReader;
import org.jivesoftware.openfire.Connection;
import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.openfire.StreamID;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.keystore.Purpose;
import org.jivesoftware.openfire.net.SASLAuthentication;
import org.jivesoftware.openfire.net.SSLConfig;
import org.jivesoftware.openfire.net.SocketConnection;
import org.jivesoftware.openfire.server.ServerDialback;
import org.jivesoftware.util.CertificateManager;
import org.jivesoftware.util.JiveGlobals;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmpp.packet.JID;
import org.xmpp.packet.Packet;
/**
* Server-to-server communication is done using two TCP connections between the servers. One
* connection is used for sending packets while the other connection is used for receiving packets.
* The <tt>IncomingServerSession</tt> represents the connection to a remote server that will only
* be used for receiving packets.<p>
*
* Currently only the Server Dialback method is being used for authenticating the remote server.
* Once the remote server has been authenticated incoming packets will be processed by this server.
* It is also possible for remote servers to authenticate more domains once the session has been
* established. For optimization reasons the existing connection is used between the servers.
* Therefore, the incoming server session holds the list of authenticated domains which are allowed
* to send packets to this server.<p>
*
* Using the Server Dialback method it is possible that this server may also act as the
* Authoritative Server. This implies that an incoming connection will be established with this
* server for authenticating a domain. This incoming connection will only last for a brief moment
* and after the domain has been authenticated the connection will be closed and no session will
* exist.
*
* @author Gaston Dombiak
*/
public class LocalIncomingServerSession extends LocalServerSession implements IncomingServerSession {
private static final Logger Log = LoggerFactory.getLogger(LocalIncomingServerSession.class);
/**
* List of domains, subdomains and virtual hostnames of the remote server that were
* validated with this server. The remote server is allowed to send packets to this
* server from any of the validated domains.
*/
private Set<String> validatedDomains = new HashSet<>();
/**
* Domains or subdomain of this server that was used by the remote server
* when validating the new connection. This information is useful to prevent
* many connections from the same remote server to the same local domain.
*/
private String localDomain = null;
/**
* Default domain, as supplied in stream header typically.
*/
private String fromDomain = null;
/**
* Creates a new session that will receive packets. The new session will be authenticated
* before being returned. If the authentication process fails then the answer will be
* <tt>null</tt>.<p>
*
* @param serverName hostname of this server.
* @param reader reader on the new established connection with the remote server.
* @param connection the new established connection with the remote server.
* @return a new session that will receive packets or null if a problem occured while
* authenticating the remote server or when acting as the Authoritative Server during
* a Server Dialback authentication process.
* @throws org.xmlpull.v1.XmlPullParserException if an error occurs while parsing the XML.
* @throws java.io.IOException if an input/output error occurs while using the connection.
*/
public static LocalIncomingServerSession createSession(String serverName, XMPPPacketReader reader,
SocketConnection connection) throws XmlPullParserException, IOException {
XmlPullParser xpp = reader.getXPPParser();
String version = xpp.getAttributeValue("", "version");
String fromDomain = xpp.getAttributeValue("", "from");
int[] serverVersion = version != null ? decodeVersion(version) : new int[] {0,0};
try {
// Get the stream ID for the new session
StreamID streamID = SessionManager.getInstance().nextStreamID();
// Create a server Session for the remote server
LocalIncomingServerSession session =
SessionManager.getInstance().createIncomingServerSession(connection, streamID, fromDomain);
// Send the stream header
StringBuilder openingStream = new StringBuilder();
openingStream.append("<stream:stream");
openingStream.append(" xmlns:db=\"jabber:server:dialback\"");
openingStream.append(" xmlns:stream=\"http://etherx.jabber.org/streams\"");
openingStream.append(" xmlns=\"jabber:server\"");
openingStream.append(" from=\"").append(serverName).append("\"");
if (fromDomain != null) {
openingStream.append(" to=\"").append(fromDomain).append("\"");
}
openingStream.append(" id=\"").append(streamID).append("\"");
// OF-443: Not responding with a 1.0 version in the stream header when federating with older
// implementations appears to reduce connection issues with those domains (patch by Marcin Cieślak).
if (serverVersion[0] >= 1) {
openingStream.append(" version=\"1.0\">");
} else {
openingStream.append('>');
}
connection.deliverRawText(openingStream.toString());
if (serverVersion[0] >= 1) {
// Remote server is XMPP 1.0 compliant so offer TLS and SASL to establish the connection (and server dialback)
// Indicate the TLS policy to use for this connection
Connection.TLSPolicy tlsPolicy =
ServerDialback.isEnabled() ? Connection.TLSPolicy.optional :
Connection.TLSPolicy.required;
boolean hasCertificates = false;
try {
hasCertificates = SSLConfig.getStore( Purpose.SOCKETBASED_IDENTITYSTORE ).size() > 0;
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
if (Connection.TLSPolicy.required == tlsPolicy && !hasCertificates) {
Log.error("Server session rejected. TLS is required but no certificates " +
"were created.");
return null;
}
connection.setTlsPolicy(hasCertificates ? tlsPolicy : Connection.TLSPolicy.disabled);
}
// Indicate the compression policy to use for this connection
String policyName = JiveGlobals.getProperty(ConnectionSettings.Server.COMPRESSION_SETTINGS,
Connection.CompressionPolicy.disabled.toString());
Connection.CompressionPolicy compressionPolicy =
Connection.CompressionPolicy.valueOf(policyName);
connection.setCompressionPolicy(compressionPolicy);
StringBuilder sb = new StringBuilder();
if (serverVersion[0] >= 1) {
// Remote server is XMPP 1.0 compliant so offer TLS and SASL to establish the connection (and server dialback)
// Don't offer stream-features to pre-1.0 servers, as it confuses them. Sending features to Openfire < 3.7.1 confuses it too - OF-443)
sb.append("<stream:features>");
if (JiveGlobals.getBooleanProperty(ConnectionSettings.Server.TLS_ENABLED, true)) {
sb.append("<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\">");
if (!ServerDialback.isEnabled()) {
// Server dialback is disabled so TLS is required
sb.append("<required/>");
}
sb.append("</starttls>");
}
// Include available SASL Mechanisms
sb.append(SASLAuthentication.getSASLMechanisms(session));
if (ServerDialback.isEnabled()) {
// Also offer server dialback (when TLS is not required). Server dialback may be offered
// after TLS has been negotiated and a self-signed certificate is being used
sb.append("<dialback xmlns=\"urn:xmpp:features:dialback\"><errors/></dialback>");
}
sb.append("</stream:features>");
}
connection.deliverRawText(sb.toString());
// Set the domain or subdomain of the local server targeted by the remote server
session.setLocalDomain(serverName);
return session;
}
catch (Exception e) {
Log.error("Error establishing connection from remote server:" + connection, e);
connection.close();
return null;
}
}
public LocalIncomingServerSession(String serverName, Connection connection, StreamID streamID, String fromDomain) {
super(serverName, connection, streamID);
this.fromDomain = fromDomain;
}
public String getDefaultIdentity() {
return this.fromDomain;
}
@Override
boolean canProcess(Packet packet) {
return true;
}
@Override
void deliver(Packet packet) throws UnauthorizedException {
// Do nothing
}
/**
* Returns true if the request of a new domain was valid. Sessions may receive subsequent
* domain validation request. If the validation of the new domain fails then the session and
* the underlying TCP connection will be closed.<p>
*
* For optimization reasons, the same session may be servicing several domains of a
* remote server.
*
* @param dbResult the DOM stanza requesting the domain validation.
* @return true if the requested domain was valid.
*/
public boolean validateSubsequentDomain(Element dbResult) {
ServerDialback method = new ServerDialback(getConnection(), getServerName());
if (method.validateRemoteDomain(dbResult, getStreamID())) {
// Add the validated domain as a valid domain
addValidatedDomain(dbResult.attributeValue("from"));
return true;
}
return false;
}
/**
* Returns true if the specified domain has been validated for this session. The remote
* server should send a "db:result" packet for registering new subdomains or even
* virtual hosts.<p>
*
* In the spirit of being flexible we allow remote servers to not register subdomains
* and even so consider subdomains that include the server domain in their domain part
* as valid domains.
*
* @param domain the domain to validate.
* @return true if the specified domain has been validated for this session.
*/
public boolean isValidDomain(String domain) {
// Check if the specified domain is contained in any of the validated domains
for (String validatedDomain : getValidatedDomains()) {
if (domain.equals(validatedDomain)) {
return true;
}
}
return false;
}
/**
* Returns a collection with all the domains, subdomains and virtual hosts that where
* validated. The remote server is allowed to send packets from any of these domains,
* subdomains and virtual hosts.
*
* @return domains, subdomains and virtual hosts that where validated.
*/
@Override
public Collection<String> getValidatedDomains() {
return Collections.unmodifiableCollection(validatedDomains);
}
/**
* Adds a new validated domain, subdomain or virtual host to the list of
* validated domains for the remote server.
*
* @param domain the new validated domain, subdomain or virtual host to add.
*/
public void addValidatedDomain(String domain) {
if (validatedDomains.add(domain)) {
// Set the first validated domain as the address of the session
if (validatedDomains.size() < 2) {
setAddress(new JID(null, domain, null));
}
// Register the new validated domain for this server session in SessionManager
SessionManager.getInstance().registerIncomingServerSession(domain, this);
}
}
/**
* Removes the previously validated domain from the list of validated domains. The remote
* server will no longer be able to send packets from the removed domain, subdomain or
* virtual host.
*
* @param domain the domain, subdomain or virtual host to remove from the list of
* validated domains.
*/
public void removeValidatedDomain(String domain) {
validatedDomains.remove(domain);
// Unregister the validated domain for this server session in SessionManager
SessionManager.getInstance().unregisterIncomingServerSession(domain, this);
}
/**
* Returns the domain or subdomain of the local server used by the remote server
* when validating the session. This information is only used to prevent many
* connections from the same remote server to the same domain or subdomain of
* the local server.
*
* @return the domain or subdomain of the local server used by the remote server
* when validating the session.
*/
@Override
public String getLocalDomain() {
return localDomain;
}
/**
* Sets the domain or subdomain of the local server used by the remote server when asking
* to validate the session. This information is only used to prevent many connections from
* the same remote server to the same domain or subdomain of the local server.
*
* @param domain the domain or subdomain of the local server used when validating the
* session.
*/
public void setLocalDomain(String domain) {
localDomain = domain;
}
/**
* Verifies the received key sent by the remote server. This server is trying to generate
* an outgoing connection to the remote server and the remote server is reusing an incoming
* connection for validating the key.
*
* @param doc the received Element that contains the key to verify.
*/
public void verifyReceivedKey(Element doc) {
ServerDialback.verifyReceivedKey(doc, getConnection());
}
@Override
public String getAvailableStreamFeatures() {
StringBuilder sb = new StringBuilder();
// Include Stream Compression Mechanism
if (conn.getCompressionPolicy() != Connection.CompressionPolicy.disabled &&
!conn.isCompressed()) {
sb.append("<compression xmlns=\"http://jabber.org/features/compress\"><method>zlib</method></compression>");
}
// Offer server dialback if using self-signed certificates and no authentication has been done yet
boolean usingSelfSigned;
final Certificate[] chain = conn.getLocalCertificates();
if (chain == null || chain.length == 0) {
usingSelfSigned = true;
} else {
try {
final KeyStore keyStore = SSLConfig.getStore( Purpose.SOCKETBASED_IDENTITYSTORE );
usingSelfSigned = CertificateManager.isSelfSignedCertificate(keyStore, (X509Certificate) chain[0]);
} catch (KeyStoreException ex) {
Log.warn("Exception occurred while trying to determine whether local certificate is self-signed. Proceeding as if it is.", ex);
usingSelfSigned = true;
}
}
if (usingSelfSigned && ServerDialback.isEnabledForSelfSigned() && validatedDomains.isEmpty()) {
sb.append("<dialback xmlns=\"urn:xmpp:features:dialback\"><errors/></dialback>");
}
return sb.toString();
}
public void tlsAuth() {
usingServerDialback = false;
}
}
/**
* $RCSfile: $
* $Revision: $
* $Date: $
*
* Copyright (C) 2005-2008 Jive Software. 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.session;
import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.dom4j.Element;
import org.dom4j.io.XMPPPacketReader;
import org.jivesoftware.openfire.Connection;
import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.openfire.StreamID;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.keystore.Purpose;
import org.jivesoftware.openfire.net.SASLAuthentication;
import org.jivesoftware.openfire.net.SSLConfig;
import org.jivesoftware.openfire.net.SocketConnection;
import org.jivesoftware.openfire.server.ServerDialback;
import org.jivesoftware.util.CertificateManager;
import org.jivesoftware.util.JiveGlobals;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmpp.packet.JID;
import org.xmpp.packet.Packet;
/**
* Server-to-server communication is done using two TCP connections between the servers. One
* connection is used for sending packets while the other connection is used for receiving packets.
* The <tt>IncomingServerSession</tt> represents the connection to a remote server that will only
* be used for receiving packets.<p>
*
* Currently only the Server Dialback method is being used for authenticating the remote server.
* Once the remote server has been authenticated incoming packets will be processed by this server.
* It is also possible for remote servers to authenticate more domains once the session has been
* established. For optimization reasons the existing connection is used between the servers.
* Therefore, the incoming server session holds the list of authenticated domains which are allowed
* to send packets to this server.<p>
*
* Using the Server Dialback method it is possible that this server may also act as the
* Authoritative Server. This implies that an incoming connection will be established with this
* server for authenticating a domain. This incoming connection will only last for a brief moment
* and after the domain has been authenticated the connection will be closed and no session will
* exist.
*
* @author Gaston Dombiak
*/
public class LocalIncomingServerSession extends LocalServerSession implements IncomingServerSession {
private static final Logger Log = LoggerFactory.getLogger(LocalIncomingServerSession.class);
/**
* List of domains, subdomains and virtual hostnames of the remote server that were
* validated with this server. The remote server is allowed to send packets to this
* server from any of the validated domains.
*/
private Set<String> validatedDomains = new HashSet<>();
/**
* Domains or subdomain of this server that was used by the remote server
* when validating the new connection. This information is useful to prevent
* many connections from the same remote server to the same local domain.
*/
private String localDomain = null;
/**
* Default domain, as supplied in stream header typically.
*/
private String fromDomain = null;
/**
* Creates a new session that will receive packets. The new session will be authenticated
* before being returned. If the authentication process fails then the answer will be
* <tt>null</tt>.<p>
*
* @param serverName hostname of this server.
* @param reader reader on the new established connection with the remote server.
* @param connection the new established connection with the remote server.
* @return a new session that will receive packets or null if a problem occured while
* authenticating the remote server or when acting as the Authoritative Server during
* a Server Dialback authentication process.
* @throws org.xmlpull.v1.XmlPullParserException if an error occurs while parsing the XML.
* @throws java.io.IOException if an input/output error occurs while using the connection.
*/
public static LocalIncomingServerSession createSession(String serverName, XMPPPacketReader reader,
SocketConnection connection) throws XmlPullParserException, IOException {
XmlPullParser xpp = reader.getXPPParser();
String version = xpp.getAttributeValue("", "version");
String fromDomain = xpp.getAttributeValue("", "from");
int[] serverVersion = version != null ? decodeVersion(version) : new int[] {0,0};
try {
// Get the stream ID for the new session
StreamID streamID = SessionManager.getInstance().nextStreamID();
// Create a server Session for the remote server
LocalIncomingServerSession session =
SessionManager.getInstance().createIncomingServerSession(connection, streamID, fromDomain);
// Send the stream header
StringBuilder openingStream = new StringBuilder();
openingStream.append("<stream:stream");
openingStream.append(" xmlns:db=\"jabber:server:dialback\"");
openingStream.append(" xmlns:stream=\"http://etherx.jabber.org/streams\"");
openingStream.append(" xmlns=\"jabber:server\"");
openingStream.append(" from=\"").append(serverName).append("\"");
if (fromDomain != null) {
openingStream.append(" to=\"").append(fromDomain).append("\"");
}
openingStream.append(" id=\"").append(streamID).append("\"");
// OF-443: Not responding with a 1.0 version in the stream header when federating with older
// implementations appears to reduce connection issues with those domains (patch by Marcin Cieślak).
if (serverVersion[0] >= 1) {
openingStream.append(" version=\"1.0\">");
} else {
openingStream.append('>');
}
connection.deliverRawText(openingStream.toString());
if (serverVersion[0] >= 1) {
// Remote server is XMPP 1.0 compliant so offer TLS and SASL to establish the connection (and server dialback)
// Indicate the TLS policy to use for this connection
Connection.TLSPolicy tlsPolicy =
ServerDialback.isEnabled() ? Connection.TLSPolicy.optional :
Connection.TLSPolicy.required;
boolean hasCertificates = false;
try {
hasCertificates = SSLConfig.getIdentityStore( Purpose.SOCKET_S2S ).size() > 0;
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
if (Connection.TLSPolicy.required == tlsPolicy && !hasCertificates) {
Log.error("Server session rejected. TLS is required but no certificates " +
"were created.");
return null;
}
connection.setTlsPolicy(hasCertificates ? tlsPolicy : Connection.TLSPolicy.disabled);
}
// Indicate the compression policy to use for this connection
String policyName = JiveGlobals.getProperty(ConnectionSettings.Server.COMPRESSION_SETTINGS,
Connection.CompressionPolicy.disabled.toString());
Connection.CompressionPolicy compressionPolicy =
Connection.CompressionPolicy.valueOf(policyName);
connection.setCompressionPolicy(compressionPolicy);
StringBuilder sb = new StringBuilder();
if (serverVersion[0] >= 1) {
// Remote server is XMPP 1.0 compliant so offer TLS and SASL to establish the connection (and server dialback)
// Don't offer stream-features to pre-1.0 servers, as it confuses them. Sending features to Openfire < 3.7.1 confuses it too - OF-443)
sb.append("<stream:features>");
if (JiveGlobals.getBooleanProperty(ConnectionSettings.Server.TLS_ENABLED, true)) {
sb.append("<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\">");
if (!ServerDialback.isEnabled()) {
// Server dialback is disabled so TLS is required
sb.append("<required/>");
}
sb.append("</starttls>");
}
// Include available SASL Mechanisms
sb.append(SASLAuthentication.getSASLMechanisms(session));
if (ServerDialback.isEnabled()) {
// Also offer server dialback (when TLS is not required). Server dialback may be offered
// after TLS has been negotiated and a self-signed certificate is being used
sb.append("<dialback xmlns=\"urn:xmpp:features:dialback\"><errors/></dialback>");
}
sb.append("</stream:features>");
}
connection.deliverRawText(sb.toString());
// Set the domain or subdomain of the local server targeted by the remote server
session.setLocalDomain(serverName);
return session;
}
catch (Exception e) {
Log.error("Error establishing connection from remote server:" + connection, e);
connection.close();
return null;
}
}
public LocalIncomingServerSession(String serverName, Connection connection, StreamID streamID, String fromDomain) {
super(serverName, connection, streamID);
this.fromDomain = fromDomain;
}
public String getDefaultIdentity() {
return this.fromDomain;
}
@Override
boolean canProcess(Packet packet) {
return true;
}
@Override
void deliver(Packet packet) throws UnauthorizedException {
// Do nothing
}
/**
* Returns true if the request of a new domain was valid. Sessions may receive subsequent
* domain validation request. If the validation of the new domain fails then the session and
* the underlying TCP connection will be closed.<p>
*
* For optimization reasons, the same session may be servicing several domains of a
* remote server.
*
* @param dbResult the DOM stanza requesting the domain validation.
* @return true if the requested domain was valid.
*/
public boolean validateSubsequentDomain(Element dbResult) {
ServerDialback method = new ServerDialback(getConnection(), getServerName());
if (method.validateRemoteDomain(dbResult, getStreamID())) {
// Add the validated domain as a valid domain
addValidatedDomain(dbResult.attributeValue("from"));
return true;
}
return false;
}
/**
* Returns true if the specified domain has been validated for this session. The remote
* server should send a "db:result" packet for registering new subdomains or even
* virtual hosts.<p>
*
* In the spirit of being flexible we allow remote servers to not register subdomains
* and even so consider subdomains that include the server domain in their domain part
* as valid domains.
*
* @param domain the domain to validate.
* @return true if the specified domain has been validated for this session.
*/
public boolean isValidDomain(String domain) {
// Check if the specified domain is contained in any of the validated domains
for (String validatedDomain : getValidatedDomains()) {
if (domain.equals(validatedDomain)) {
return true;
}
}
return false;
}
/**
* Returns a collection with all the domains, subdomains and virtual hosts that where
* validated. The remote server is allowed to send packets from any of these domains,
* subdomains and virtual hosts.
*
* @return domains, subdomains and virtual hosts that where validated.
*/
public Collection<String> getValidatedDomains() {
return Collections.unmodifiableCollection(validatedDomains);
}
/**
* Adds a new validated domain, subdomain or virtual host to the list of
* validated domains for the remote server.
*
* @param domain the new validated domain, subdomain or virtual host to add.
*/
public void addValidatedDomain(String domain) {
if (validatedDomains.add(domain)) {
// Set the first validated domain as the address of the session
if (validatedDomains.size() < 2) {
setAddress(new JID(null, domain, null));
}
// Register the new validated domain for this server session in SessionManager
SessionManager.getInstance().registerIncomingServerSession(domain, this);
}
}
/**
* Removes the previously validated domain from the list of validated domains. The remote
* server will no longer be able to send packets from the removed domain, subdomain or
* virtual host.
*
* @param domain the domain, subdomain or virtual host to remove from the list of
* validated domains.
*/
public void removeValidatedDomain(String domain) {
validatedDomains.remove(domain);
// Unregister the validated domain for this server session in SessionManager
SessionManager.getInstance().unregisterIncomingServerSession(domain, this);
}
/**
* Returns the domain or subdomain of the local server used by the remote server
* when validating the session. This information is only used to prevent many
* connections from the same remote server to the same domain or subdomain of
* the local server.
*
* @return the domain or subdomain of the local server used by the remote server
* when validating the session.
*/
@Override
public String getLocalDomain() {
return localDomain;
}
/**
* Sets the domain or subdomain of the local server used by the remote server when asking
* to validate the session. This information is only used to prevent many connections from
* the same remote server to the same domain or subdomain of the local server.
*
* @param domain the domain or subdomain of the local server used when validating the
* session.
*/
public void setLocalDomain(String domain) {
localDomain = domain;
}
/**
* Verifies the received key sent by the remote server. This server is trying to generate
* an outgoing connection to the remote server and the remote server is reusing an incoming
* connection for validating the key.
*
* @param doc the received Element that contains the key to verify.
*/
public void verifyReceivedKey(Element doc) {
ServerDialback.verifyReceivedKey(doc, getConnection());
}
@Override
public String getAvailableStreamFeatures() {
StringBuilder sb = new StringBuilder();
// Include Stream Compression Mechanism
if (conn.getCompressionPolicy() != Connection.CompressionPolicy.disabled &&
!conn.isCompressed()) {
sb.append("<compression xmlns=\"http://jabber.org/features/compress\"><method>zlib</method></compression>");
}
// Offer server dialback if using self-signed certificates and no authentication has been done yet
boolean usingSelfSigned;
final Certificate[] chain = conn.getLocalCertificates();
if (chain == null || chain.length == 0) {
usingSelfSigned = true;
} else {
try {
final KeyStore keyStore = SSLConfig.getIdentityStore( Purpose.SOCKET_S2S );
usingSelfSigned = CertificateManager.isSelfSignedCertificate(keyStore, (X509Certificate) chain[0]);
} catch (KeyStoreException ex) {
Log.warn("Exception occurred while trying to determine whether local certificate is self-signed. Proceeding as if it is.", ex);
usingSelfSigned = true;
}
}
if (usingSelfSigned && ServerDialback.isEnabledForSelfSigned() && validatedDomains.isEmpty()) {
sb.append("<dialback xmlns=\"urn:xmpp:features:dialback\"><errors/></dialback>");
}
return sb.toString();
}
public void tlsAuth() {
usingServerDialback = false;
}
}
......@@ -56,14 +56,7 @@ import org.apache.mina.integration.jmx.IoServiceMBean;
import org.apache.mina.integration.jmx.IoSessionMBean;
import org.apache.mina.transport.socket.SocketSessionConfig;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;
import org.jivesoftware.openfire.ConnectionManager;
import org.jivesoftware.openfire.JMXManager;
import org.jivesoftware.openfire.PacketDeliverer;
import org.jivesoftware.openfire.PacketRouter;
import org.jivesoftware.openfire.RoutingTable;
import org.jivesoftware.openfire.ServerPort;
import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.*;
import org.jivesoftware.openfire.container.BasicModule;
import org.jivesoftware.openfire.container.PluginManager;
import org.jivesoftware.openfire.container.PluginManagerListener;
......@@ -451,6 +444,7 @@ public class ConnectionManagerImpl extends BasicModule implements ConnectionMana
Log.debug("Throttling read buffer for connections from sslSocketAcceptor={} to max={} bytes",
sslSocketAcceptor, maxBufferSize);
// Add the SSL filter now since sockets are "borned" encrypted in the old ssl method
Connection.ClientAuth clientAuth;
try {
......@@ -459,9 +453,8 @@ public class ConnectionManagerImpl extends BasicModule implements ConnectionMana
clientAuth = Connection.ClientAuth.disabled;
}
final SslFilter sslFilter = SSLConfig.getServerModeSslFilter( SSLConfig.Type.SOCKET_C2S, clientAuth );
final SslFilter sslFilter = SSLConfig.getServerModeSslFilter( Purpose.SOCKET_C2S, clientAuth );
sslSocketAcceptor.getFilterChain().addAfter(EXECUTOR_FILTER_NAME, TLS_FILTER_NAME, sslFilter);
}
catch (Exception e) {
System.err.println("Error starting SSL XMPP listener on port " + port + ": " +
......@@ -615,7 +608,7 @@ public class ConnectionManagerImpl extends BasicModule implements ConnectionMana
@Override
public boolean isClientSSLListenerEnabled() {
try {
return JiveGlobals.getBooleanProperty(ConnectionSettings.Client.ENABLE_OLD_SSLPORT, false) && SSLConfig.getStore( Purpose.SOCKETBASED_IDENTITYSTORE ).size() > 0;
return JiveGlobals.getBooleanProperty(ConnectionSettings.Client.ENABLE_OLD_SSLPORT, false) && SSLConfig.getIdentityStore( Purpose.SOCKET_C2S ).size() > 0;
} catch (KeyStoreException e) {
return false;
}
......
......@@ -31,11 +31,6 @@
storePurpose = null;
}
if (! storePurpose.isIdentityStore() ) {
errors.put( "storePurpose", "shoud be an identity store (not a trust store)");
storePurpose = null;
}
pageContext.setAttribute( "storePurpose", storePurpose );
if (save) {
......@@ -47,7 +42,7 @@
}
if (errors.isEmpty()) {
try {
final IdentityStoreConfig identityStoreConfig = (IdentityStoreConfig) SSLConfig.getInstance().getStoreConfig( storePurpose );
final IdentityStoreConfig identityStoreConfig = SSLConfig.getInstance().getIdentityStoreConfig( storePurpose );
// Create an alias for the signed certificate
String domain = XMPPServer.getInstance().getServerInfo().getXMPPDomain();
......@@ -62,7 +57,7 @@
identityStoreConfig.installCertificate( alias, privateKey, passPhrase, certificate );
// Log the event
webManager.logEvent("imported SSL certificate in "+ storePurposeText, "alias = "+alias);
webManager.logEvent("imported SSL certificate in identity store "+ storePurposeText, "alias = "+alias);
response.sendRedirect("security-keystore.jsp?storePurpose="+storePurposeText);
return;
......@@ -77,8 +72,8 @@
<html>
<head>
<title><fmt:message key="ssl.import.certificate.keystore.${connectivityType}.title"/></title>
<meta name="pageID" content="security-keystore-${connectivityType}"/>
<title><fmt:message key="ssl.import.certificate.keystore.${storePurpose}.title"/></title>
<meta name="pageID" content="security-keystore-${storePurpose}"/>
</head>
<body>
......@@ -120,7 +115,7 @@
<!-- BEGIN 'Import Private Key and Certificate' -->
<form action="import-keystore-certificate.jsp" method="post" name="f">
<input type="hidden" name="connectivityType" value="${connectivityType}"/>
<input type="hidden" name="storePurpose" value="${storePurpose}"/>
<div class="jive-contentBoxHeader">
<fmt:message key="ssl.import.certificate.keystore.boxtitle" />
</div>
......
......@@ -16,7 +16,7 @@
<% final boolean save = ParamUtils.getParameter(request, "save") != null;
final String alias = ParamUtils.getParameter(request, "alias");
final String certificate = ParamUtils.getParameter(request, "certificate");
final String storePurposeText = ParamUtils.getParameter(request, "storePurpose");
final String storePurposeText = ParamUtils.getParameter(request, "storePurpose");
final Map<String, String> errors = new HashMap<String, String>();
......@@ -29,16 +29,11 @@
storePurpose = null;
}
if (! storePurpose.isTrustStore() ) {
errors.put( "storePurpose", "shoud be a trust store (not an identity store)");
storePurpose = null;
}
pageContext.setAttribute( "storePurpose", storePurpose );
if (save && errors.isEmpty())
{
final TrustStoreConfig trustStoreConfig = (TrustStoreConfig) SSLConfig.getInstance().getStoreConfig( storePurpose );
final TrustStoreConfig trustStoreConfig = SSLConfig.getInstance().getTrustStoreConfig( storePurpose );
if (alias == null || "".equals(alias))
{
......@@ -62,7 +57,7 @@
trustStoreConfig.installCertificate( alias, certificate );
// Log the event
webManager.logEvent("imported SSL certificate in "+ storePurposeText, "alias = "+alias);
webManager.logEvent("imported SSL certificate in trust store "+ storePurposeText, "alias = "+alias);
response.sendRedirect( "security-truststore.jsp?storePurpose=" + storePurposeText + "&importsuccess=true" );
return;
......@@ -79,9 +74,9 @@
<html>
<head>
<title>
<fmt:message key="ssl.import.certificate.keystore.${connectivityType}.title"/> - <fmt:message key="ssl.certificates.truststore.${param.type}-title"/>
<fmt:message key="ssl.import.certificate.keystore.${storePurpose}.title"/> - <fmt:message key="ssl.certificates.truststore.${param.type}-title"/>
</title>
<meta name="pageID" content="security-truststore-${connectivityType}-${param.type}"/>
<meta name="pageID" content="security-truststore-${storePurpose}-${param.type}"/>
</head>
<body>
......@@ -129,7 +124,7 @@
<!-- BEGIN 'Import Certificate' -->
<form action="import-truststore-certificate.jsp?type=${param.type}" method="post" name="f">
<input type="hidden" name="connectivityType" value="${connectivityType}"/>
<input type="hidden" name="connectivityType" value="${storePurpose}"/>
<div class="jive-contentBoxHeader">
<fmt:message key="ssl.import.certificate.keystore.boxtitle"/>
</div>
......
......@@ -253,7 +253,7 @@
<fmt:message key="index.server_name" />
</td>
<td class="c2">
<% final IdentityStoreConfig storeConfig = (IdentityStoreConfig) SSLConfig.getInstance().getStoreConfig( Purpose.SOCKETBASED_IDENTITYSTORE ); %>
<% final IdentityStoreConfig storeConfig = SSLConfig.getInstance().getIdentityStoreConfig( Purpose.SOCKET_C2S ); %>
<% try { %>
<% if (!storeConfig.containsDomainCertificate( "RSA" )) {%>
<img src="images/warning-16x16.gif" width="16" height="16" border="0" alt="<fmt:message key="index.certificate-warning" />" title="<fmt:message key="index.certificate-warning" />">&nbsp;
......
......@@ -9,6 +9,7 @@
<%@ page import="java.security.AlgorithmParameters" %>
<%@ page import="org.jivesoftware.openfire.keystore.Purpose" %>
<%@ page import="org.jivesoftware.openfire.keystore.CertificateStoreConfig" %>
<%@ page import="java.security.KeyStore" %>
<%@ taglib uri="admin" prefix="admin" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
......@@ -21,6 +22,7 @@
final String alias = ParamUtils.getParameter( request, "alias" );
final String storePurposeText = ParamUtils.getParameter( request, "storePurpose" );
final boolean isTrustStore = ParamUtils.getBooleanParameter( request, "isTrustStore" );
final Map<String, String> errors = new HashMap<String, String>();
......@@ -42,10 +44,15 @@
{
try
{
final CertificateStoreConfig certificateStoreConfig = SSLConfig.getInstance().getStoreConfig( storePurpose );
final KeyStore store;
if (isTrustStore) {
store = SSLConfig.getTrustStore( storePurpose );
} else {
store = SSLConfig.getIdentityStore( storePurpose );
}
// Get the certificate
final X509Certificate certificate = (X509Certificate) certificateStoreConfig.getStore().getCertificate( alias );
final X509Certificate certificate = (X509Certificate) store.getCertificate( alias );
if ( certificate == null ) {
errors.put( "alias", "alias" );
......@@ -62,7 +69,7 @@
// Handle a "go back" click:
if ( request.getParameter( "back" ) != null ) {
if ( storePurpose.isTrustStore() ) {
if ( isTrustStore ) {
response.sendRedirect( "security-truststore.jsp?storePurpose=" + storePurpose );
} else {
response.sendRedirect( "security-keystore.jsp?storePurpose=" + storePurpose );
......@@ -77,11 +84,11 @@
<head>
<title><fmt:message key="ssl.certificate.details.title"/></title>
<c:choose>
<c:when test="${storePurpose.identityStore}">
<meta name="pageID" content="security-keystore"/>
<c:when test="${isTrustStore}">
<meta name="pageID" content="security-truststore"/>
</c:when>
<c:otherwise>
<meta name="pageID" content="security-truststore"/>
<meta name="pageID" content="security-keystore"/>
</c:otherwise>
</c:choose>
</head>
......
......@@ -16,41 +16,12 @@
// Read parameters
final boolean save = request.getParameter("save") != null;
final String paramLocKeySocket = ParamUtils.getParameter(request, "loc-key-socket");
final String paramLocTrustSocketS2S = ParamUtils.getParameter(request, "loc-trust-socket-s2s");
final String paramLocTrustSocketC2S = ParamUtils.getParameter(request, "loc-trust-socket-c2s");
final String paramLocKeyBosh = ParamUtils.getParameter(request, "loc-key-bosh");
final String paramLocTrustBoshC2S = ParamUtils.getParameter(request, "loc-trust-bosh-c2s");
final String paramLocKeyWebadmin = ParamUtils.getParameter(request, "loc-key-webadmin");
final String paramLocTrustWebadmin = ParamUtils.getParameter(request, "loc-trust-webadmin");
final String paramLocKeyAdministrative = ParamUtils.getParameter( request, "loc-key-administrative" );
final String paramLocTrustAdministrative = ParamUtils.getParameter( request, "loc-trust-administrative" );
// TODO actually save something!
// Pre-update property values
final String locKeySocket = SSLConfig.getNonCanonicalizedLocation( Purpose.SOCKETBASED_IDENTITYSTORE );
final String locTrustSocketS2S = SSLConfig.getNonCanonicalizedLocation( Purpose.SOCKETBASED_S2S_TRUSTSTORE );
final String locTrustSocketC2S = SSLConfig.getNonCanonicalizedLocation( Purpose.SOCKETBASED_C2S_TRUSTSTORE );
final String locKeyBosh = SSLConfig.getNonCanonicalizedLocation( Purpose.BOSHBASED_IDENTITYSTORE );
final String locTrustBoshC2S = SSLConfig.getNonCanonicalizedLocation( Purpose.BOSHBASED_C2S_TRUSTSTORE );
final String locKeyWebadmin = SSLConfig.getNonCanonicalizedLocation( Purpose.WEBADMIN_IDENTITYSTORE );
final String locTrustWebadmin = SSLConfig.getNonCanonicalizedLocation( Purpose.WEBADMIN_TRUSTSTORE );
final String locKeyAdministrative = SSLConfig.getNonCanonicalizedLocation( Purpose.ADMINISTRATIVE_IDENTITYSTORE );
final String locTrustAdministrative = SSLConfig.getNonCanonicalizedLocation( Purpose.ADMINISTRATIVE_TRUSTSTORE );
final Map<String, String> errors = new HashMap<>();
pageContext.setAttribute( "errors", errors );
pageContext.setAttribute( "locKeySocket", locKeySocket );
pageContext.setAttribute( "locTrustSocketS2S",locTrustSocketS2S );
pageContext.setAttribute( "locTrustSocketC2S", locTrustSocketC2S );
pageContext.setAttribute( "locKeyBosh", locKeyBosh );
pageContext.setAttribute( "locTrustBoshC2S", locTrustBoshC2S );
pageContext.setAttribute( "locKeyWebadmin", locKeyWebadmin );
pageContext.setAttribute( "locTrustWebadmin", locTrustWebadmin );
pageContext.setAttribute( "locKeyAdministrative", locKeyAdministrative );
pageContext.setAttribute( "locTrustAdministrative", locTrustAdministrative );
%>
<html>
......
......@@ -36,18 +36,10 @@
try
{
storePurpose = Purpose.valueOf( storePurposeText );
if ( !storePurpose.isIdentityStore() )
{
errors.put( "storePurpose", "should be an identity store (not a trust store)");
}
else
storeConfig = SSLConfig.getInstance().getIdentityStoreConfig( storePurpose );
if ( storeConfig == null )
{
storeConfig = (IdentityStoreConfig) SSLConfig.getInstance().getStoreConfig( storePurpose );
if ( storeConfig == null )
{
errors.put( "storeConfig", "Unable to get an instance." );
}
errors.put( "storeConfig", "Unable to get an instance." );
}
}
catch (RuntimeException ex)
......@@ -60,7 +52,7 @@
pageContext.setAttribute( "storePurpose", storePurpose );
pageContext.setAttribute( "storeConfig", storeConfig );
final Set<Purpose> sameStorePurposes = SSLConfig.getInstance().getOtherPurposesForSameStore( storePurpose );
final Set<Purpose> sameStorePurposes = Collections.EMPTY_SET; // TODO FIXME: SSLConfig.getInstance().getOtherPurposesForSameStore( storePurpose );
pageContext.setAttribute( "sameStorePurposes", sameStorePurposes );
final Map<String, X509Certificate> certificates = storeConfig.getAllCertificates();
......
......@@ -7,6 +7,7 @@
<%@ page import="org.jivesoftware.openfire.keystore.Purpose" %>
<%@ page import="org.jivesoftware.openfire.keystore.TrustStoreConfig" %>
<%@ page import="java.util.Set" %>
<%@ page import="java.util.Collections" %>
<%@ taglib uri="admin" prefix="admin" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
......@@ -28,18 +29,10 @@
try
{
storePurpose = Purpose.valueOf( storePurposeText );
if ( !storePurpose.isTrustStore() )
{
errors.put( "storePurpose", "should be a trust store (not an identity store)");
}
else
storeConfig = SSLConfig.getInstance().getTrustStoreConfig( storePurpose );
if ( storeConfig == null )
{
storeConfig = (TrustStoreConfig) SSLConfig.getInstance().getStoreConfig( storePurpose );
if ( storeConfig == null )
{
errors.put( "storeConfig", "Unable to get an instance." );
}
errors.put( "storeConfig", "Unable to get an instance." );
}
}
catch (RuntimeException ex)
......@@ -52,7 +45,7 @@
pageContext.setAttribute( "storePurpose", storePurpose );
pageContext.setAttribute( "storeConfig", storeConfig );
final Set<Purpose> sameStorePurposes = SSLConfig.getInstance().getOtherPurposesForSameStore( storePurpose );
final Set<Purpose> sameStorePurposes = Collections.EMPTY_SET; // TODO FIXME: SSLConfig.getInstance().getOtherPurposesForSameStore( storePurpose );
pageContext.setAttribute( "sameStorePurposes", sameStorePurposes );
if ( delete )
......
......@@ -38,10 +38,6 @@
storePurpose = null;
}
if (! storePurpose.isIdentityStore() ) {
errors.put( "storePurpose", "shoud be an identity store (not a trust store)");
storePurpose = null;
}
pageContext.setAttribute( "storePurpose", storePurpose );
// if (save) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment