AdminConsolePlugin.java 13.8 KB
Newer Older
1 2 3 4
/**
 * $Revision: 3034 $
 * $Date: 2005-11-04 21:02:33 -0300 (Fri, 04 Nov 2005) $
 *
5
 * Copyright (C) 2004-2008 Jive Software. All rights reserved.
6
 *
7 8 9 10 11 12 13 14 15 16 17
 * 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.
18 19
 */

20
package org.jivesoftware.openfire.container;
21

22 23 24 25 26
import java.io.File;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.List;

27
import org.eclipse.jetty.http.ssl.SslContextFactory;
28 29 30 31 32 33 34 35 36
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server;
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.server.nio.SelectChannelConnector;
import org.eclipse.jetty.server.ssl.SslSelectChannelConnector;
import org.eclipse.jetty.servlet.ServletContextHandler;
37
import org.eclipse.jetty.util.thread.QueuedThreadPool;
38
import org.eclipse.jetty.webapp.WebAppContext;
39 40 41 42 43 44 45
import org.jivesoftware.openfire.XMPPServer;
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;
46 47
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
48 49 50 51 52 53 54 55 56

/**
 * 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 {

57
    private static final Logger Log = LoggerFactory.getLogger(AdminConsolePlugin.class);
58

59 60 61 62 63 64
    /**
     * 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);

65 66 67 68 69 70
    private int adminPort;
    private int adminSecurePort;
    private Server adminServer;
    private ContextHandlerCollection contexts;
    private CertificateEventListener certificateListener;
    private boolean restartNeeded = false;
71
    private boolean sslEnabled = false;
72

73 74 75
    private File pluginDir;

    /**
76
     * Create a Jetty module.
77 78
     */
    public AdminConsolePlugin() {
Matt Tucker's avatar
Matt Tucker committed
79 80
        contexts = new ContextHandlerCollection();
        
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
        // 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);

        adminPort = JiveGlobals.getXMLProperty("adminConsole.port", 9090);
        adminSecurePort = JiveGlobals.getXMLProperty("adminConsole.securePort", 9091);
        adminServer = new Server();
98 99 100 101
        final QueuedThreadPool tp = new QueuedThreadPool(254);
        tp.setName("Jetty-QTP-AdminConsole");
        adminServer.setThreadPool(tp);
        
102 103
        // Do not send Jetty info in HTTP headers
        adminServer.setSendServerVersion(false);
104 105 106 107 108

        // Create connector for http traffic if it's enabled.
        if (adminPort > 0) {
            Connector httpConnector = new SelectChannelConnector();
            // Listen on a specific network interface if it has been set.
109
            String bindInterface = getBindInterface();
110 111 112 113 114 115
            httpConnector.setHost(bindInterface);
            httpConnector.setPort(adminPort);
            adminServer.addConnector(httpConnector);
        }

        // Create a connector for https traffic if it's enabled.
116
        sslEnabled = false;
117
        try {
118
            if (adminSecurePort > 0 && CertificateManager.isRSACertificate(SSLConfig.getKeyStore(), "*"))
119
            {
120
                if (!CertificateManager.isRSACertificate(SSLConfig.getKeyStore(),
121
                        XMPPServer.getInstance().getServerInfo().getXMPPDomain())) {
122 123
                    Log.warn("Admin console: Using RSA certificates but they are not valid for the hosted domain");
                }
124 125 126 127 128 129 130 131 132 133 134
             
                final SslContextFactory sslContextFactory = new SslContextFactory(SSLConfig.getKeystoreLocation());
                sslContextFactory.setTrustStorePassword(SSLConfig.gets2sTrustPassword());
                sslContextFactory.setTrustStoreType(SSLConfig.getStoreType());
                sslContextFactory.setTrustStore(SSLConfig.gets2sTruststoreLocation());
                sslContextFactory.setNeedClientAuth(false);
                sslContextFactory.setWantClientAuth(false);
                sslContextFactory.setKeyStorePassword(SSLConfig.getKeyPassword());
                sslContextFactory.setKeyStoreType(SSLConfig.getStoreType());
                
                final SslSelectChannelConnector httpsConnector = new SslSelectChannelConnector(sslContextFactory);
135
                String bindInterface = getBindInterface();
136 137 138 139
                httpsConnector.setHost(bindInterface);
                httpsConnector.setPort(adminSecurePort);

                adminServer.addConnector(httpsConnector);
140 141

                sslEnabled = true;
142 143 144
            }
        }
        catch (Exception e) {
145
            Log.error(e.getMessage(), e);
146 147 148 149 150 151 152 153 154 155
        }

        // 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;
        }

156 157 158
        HandlerCollection collection = new HandlerCollection();
        adminServer.setHandler(collection);
        collection.setHandlers(new Handler[] { contexts, new DefaultHandler() });
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188

        try {
            adminServer.start();
        }
        catch (Exception e) {
            Log.error("Could not start admin conosle 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;
189 190 191 192
    }

    public void initializePlugin(PluginManager manager, File pluginDir) {
        this.pluginDir = pluginDir;
193

Matt Tucker's avatar
Matt Tucker committed
194
        createWebAppContext();
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212

        startup();
    }

    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;
    }

213 214 215 216 217 218 219 220
    /**
     * 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() {
221 222
        String adminInterfaceName = JiveGlobals.getXMLProperty("adminConsole.interface");
        String globalInterfaceName = JiveGlobals.getXMLProperty("network.interface");
223
        String bindInterface = null;
224 225
        if (adminInterfaceName != null && adminInterfaceName.trim().length() > 0) {
            bindInterface = adminInterfaceName;
226
        }
227 228 229
        else if (globalInterfaceName != null && globalInterfaceName.trim().length() > 0) {
            bindInterface = globalInterfaceName;
         }
230 231 232
        return bindInterface;
    }

233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
    /**
     * 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() {
248 249 250
        if (!sslEnabled) {
            return 0;
        }
251 252 253
        return adminSecurePort;
    }

Matt Tucker's avatar
Matt Tucker committed
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269
    /**
     * 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.
     */
270 271 272 273 274
    public ContextHandlerCollection getContexts() {
        return contexts;
    }

    public void restart() {
275
        try {
276 277
            adminServer.stop();
            adminServer.start();
278 279
        }
        catch (Exception e) {
280
            Log.error(e.getMessage(), e);
281 282 283
        }
    }

Matt Tucker's avatar
Matt Tucker committed
284
    private void createWebAppContext() {
285
        ServletContextHandler context;
Alex Wenckus's avatar
Alex Wenckus committed
286 287 288 289
        // 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"));
290 291
            context = new WebAppContext(contexts, pluginDir.getParentFile().getParentFile().getParentFile().getParent() +
                     File.separator + "src" + File.separator + "web", "/");
292
        }
Alex Wenckus's avatar
Alex Wenckus committed
293
        else {
294
            context = new WebAppContext(contexts, pluginDir.getAbsoluteFile() + File.separator + "webapp",
Alex Wenckus's avatar
Alex Wenckus committed
295
                    "/");
296
        }
Alex Wenckus's avatar
Alex Wenckus committed
297
        context.setWelcomeFiles(new String[]{"index.jsp"});
298 299
    }

300 301 302 303 304 305 306 307
    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");
308 309 310
        String hostname = getBindInterface() == null ?
                XMPPServer.getInstance().getServerInfo().getXMPPDomain() :
                getBindInterface();
311 312 313 314 315 316 317 318 319 320 321 322 323
        boolean isPlainStarted = false;
        boolean isSecureStarted = false;
        for (Connector connector : adminServer.getConnectors()) {
            if (connector.getPort() == adminPort) {
                isPlainStarted = true;
            }
            else if (connector.getPort() == adminSecurePort) {
                isSecureStarted = true;
            }
        }

        if (isPlainStarted && isSecureStarted) {
            log(listening + ":" + System.getProperty("line.separator") +
324
                    "  http://" + hostname + ":" +
325
                    adminPort + System.getProperty("line.separator") +
326
                    "  https://" + hostname + ":" +
327 328 329
                    adminSecurePort);
        }
        else if (isSecureStarted) {
330
            log(listening + " https://" + hostname + ":" + adminSecurePort);
331 332
        }
        else if (isPlainStarted) {
333
            log(listening + " http://" + hostname + ":" + adminPort);
334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361
        }
    }

    /**
     * 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 {

        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;
            }
        }

        public void certificateDeleted(KeyStore keyStore, String alias) {
            restartNeeded = true;
        }

        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;
            }
        }
    }
362
}