/*******************************************************************************
* Copyright (c) 2010, 2013 AGETO Service GmbH and others.
* All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html.
*
* Contributors:
* Mike Tschierschke - initial API and implementation
*******************************************************************************/
package org.eclipse.gyrex.admin.ui.internal;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.gyrex.admin.ui.internal.application.AdminApplicationConfiguration;
import org.eclipse.gyrex.admin.ui.internal.jetty.AdminServletHolder;
import org.eclipse.gyrex.admin.ui.internal.jetty.SimpleAdminLoginService;
import org.eclipse.gyrex.admin.ui.internal.servlets.AdminServletTracker;
import org.eclipse.gyrex.boot.internal.app.ServerApplication;
import org.eclipse.gyrex.common.runtime.BaseBundleActivator;
import org.eclipse.gyrex.monitoring.diagnostics.StatusTracker;
import org.eclipse.gyrex.server.Platform;
import org.eclipse.gyrex.server.settings.SystemSetting;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jetty.security.ConstraintMapping;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.security.SecurityHandler;
import org.eclipse.jetty.security.authentication.BasicAuthenticator;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.session.HashSessionManager;
import org.eclipse.jetty.server.session.SessionHandler;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.resource.ImageRegistry;
import org.eclipse.rap.rwt.RWT;
import org.eclipse.rap.rwt.application.ApplicationRunner;
import org.eclipse.rap.rwt.engine.RWTServlet;
import org.eclipse.swt.widgets.Display;
import org.osgi.framework.BundleContext;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The activator of the admin ui bundle. Serves also images.
*/
public class AdminUiActivator extends BaseBundleActivator {
public static final String SYMBOLIC_NAME = "org.eclipse.gyrex.admin.ui"; //$NON-NLS-1$
private static final String IMAGE_REGISTRY = SYMBOLIC_NAME + "#imageRegistry";
private static final int DEFAULT_ADMIN_PORT = 3110;
private static final Logger LOG = LoggerFactory.getLogger(AdminUiActivator.class);
private static volatile AdminUiActivator instance;
private static volatile Server server;
private static final SystemSetting<Boolean> useSslConnector = SystemSetting.newBooleanSetting("gyrex.admin.secure", "enables the Gyrex Admin UI to be deliviered via HTTPS instead of plain HTTP").usingDefault(Boolean.FALSE).create();
private static final SystemSetting<String> authenticationConfigString = SystemSetting.newStringSetting("gyrex.admin.auth", "authentication string containing username and password hash").create();
/**
* Returns an image descriptor for the image file at the given plug-in
* relative path
*
* @param path
* the path
* @return the image descriptor
*/
public static ImageDescriptor getImageDescriptor(final String path) {
return ImageDescriptor.createFromURL(FileLocator.find(instance.getBundle(), new Path(path), null));
}
/**
* Returns the instance.
*
* @return the instance
*/
public static AdminUiActivator getInstance() {
final AdminUiActivator activator = instance;
if (activator == null)
throw new IllegalStateException("inactive");
return activator;
}
private ApplicationRunner adminApplicationRunner;
private StatusTracker statusTracker;
private int adminPort;
private String adminHost;
/**
* The constructor
*/
public AdminUiActivator() {
super(SYMBOLIC_NAME);
}
private void addNonSslConnector(final Server server) {
final HttpConfiguration httpConfiguration = new HttpConfiguration();
httpConfiguration.setSendServerVersion(false);
httpConfiguration.setSendDateHeader(false);
final ServerConnector connector = new ServerConnector(server, new HttpConnectionFactory(httpConfiguration));
connector.setPort(adminPort);
if (null != adminHost) {
connector.setHost(adminHost);
}
connector.setIdleTimeout(60000);
// TODO: (Jetty9?) connector.setLowResourcesConnections(20000);
// TODO: (Jetty9?) connector.setLowResourcesMaxIdleTime(5000);
// TODO: (Jetty9?) connector.setForwarded(true);
// see https://bugs.eclipse.org/bugs/show_bug.cgi?id=356988 for an issue
// with configuring the connector
server.addConnector(connector);
}
private void addSslConnector(final Server server) {
try {
final File keystoreFile = Platform.getStateLocation(AdminUiActivator.getInstance().getBundle()).append("jettycerts").toFile();
if (!keystoreFile.isFile()) {
if (!keystoreFile.getParentFile().isDirectory() && !keystoreFile.getParentFile().mkdirs())
throw new IllegalStateException("Error creating directory for jetty ssl certificates");
final InputStream stream = getBundle().getEntry("cert/jettycerts.jks").openStream();
FileUtils.copyInputStreamToFile(stream, keystoreFile);
IOUtils.closeQuietly(stream);
}
final SslContextFactory sslContextFactory = new SslContextFactory(keystoreFile.getCanonicalPath());
sslContextFactory.setKeyStorePassword("changeit");
sslContextFactory.setKeyManagerPassword("changeit");
final HttpConfiguration httpConfiguration = new HttpConfiguration();
httpConfiguration.setSendServerVersion(false);
httpConfiguration.setSendDateHeader(false);
httpConfiguration.setSecurePort(adminPort);
final ServerConnector connector = new ServerConnector(server, sslContextFactory, new HttpConnectionFactory(httpConfiguration));
connector.setPort(adminPort);
if (null != adminHost) {
connector.setHost(adminHost);
}
connector.setIdleTimeout(60000);
// TODO: (Jetty9?) connector.setLowResourcesConnections(20000);
// TODO: (Jetty9?) connector.setLowResourcesMaxIdleTime(5000);
// TODO: (Jetty9?) connector.setForwarded(true);
server.addConnector(connector);
} catch (final Exception e) {
throw new IllegalStateException("Error configuring jetty ssl connector for admin ui.", e);
}
}
private void configureContextWithServletsAndResources(final ServletContextHandler contextHandler) throws MalformedURLException, IOException {
// configure context base directory (required for RAP/RWT resources)
final IPath contextBase = Platform.getStateLocation(getBundle()).append("context");
contextHandler.setBaseResource(Resource.newResource(contextBase.toFile()));
// configure defaults for resources served by Jetty's DefaultServlet
if (Platform.inDevelopmentMode()) {
contextHandler.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "true");
contextHandler.setInitParameter("org.eclipse.jetty.servlet.Default.useFileMappedBuffer", "false");
contextHandler.setInitParameter("org.eclipse.jetty.servlet.Default.maxCachedFiles", "0");
} else {
contextHandler.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "false");
contextHandler.setInitParameter("org.eclipse.jetty.servlet.Default.maxCacheSize", "2000000");
contextHandler.setInitParameter("org.eclipse.jetty.servlet.Default.maxCachedFileSize", "254000");
contextHandler.setInitParameter("org.eclipse.jetty.servlet.Default.maxCachedFiles", "1000");
contextHandler.setInitParameter("org.eclipse.jetty.servlet.Default.useFileMappedBuffer", "true");
}
// initialize and start RWT application
adminApplicationRunner = new ApplicationRunner(new AdminApplicationConfiguration(), contextHandler.getServletContext());
adminApplicationRunner.start();
// serve admin application directly
contextHandler.addServlet(new AdminServletHolder(new RWTServlet()), "/admin");
// register additional static resources references in body html
final ServletHolder staticResources = new AdminServletHolder(new DefaultServlet());
staticResources.setInitParameter("resourceBase", FileLocator.resolve(FileLocator.find(getBundle(), new Path("html"), null)).toExternalForm());
contextHandler.addServlet(staticResources, "/static/*");
// redirect to admin
contextHandler.addServlet(new AdminServletHolder(new HttpServlet() {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
resp.sendRedirect("/admin");
}
}), "");
// serve context resources (required for RAP/RWT resources)
contextHandler.addServlet(new AdminServletHolder(new DefaultServlet()), "/*");
// register Logback status servlet
try {
// note, we don't reference the class directly because the package import is optional
final Class<?> servletClass = AdminUiActivator.getInstance().getBundle().loadClass("ch.qos.logback.classic.ViewStatusMessagesServlet");
contextHandler.addServlet(new AdminServletHolder((Servlet) servletClass.newInstance()), "/logbackstatus");
} catch (final ClassNotFoundException | LinkageError e) {
LOG.warn("Logback status servlet not available. {}", e.getMessage(), e);
} catch (final Exception e) {
LOG.error("An error occurred while registering the Logback status servlet. {}", e.getMessage(), e);
}
// allow extension using custom servlets
final AdminServletTracker adminServletTracker = new AdminServletTracker(getBundle().getBundleContext(), contextHandler);
contextHandler.addBean(new AbstractLifeCycle() {
@Override
protected void doStart() throws Exception {
adminServletTracker.open();
}
@Override
protected void doStop() throws Exception {
adminServletTracker.close();
}
});
}
private SecurityHandler createSecurityHandler(final Handler baseHandler, final String username, final String password) {
final ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
final ConstraintMapping authenticationContraintMapping = new ConstraintMapping();
final Constraint constraint = new Constraint(Constraint.__BASIC_AUTH, AdminServletHolder.ADMIN_ROLE);
constraint.setAuthenticate(true);
authenticationContraintMapping.setConstraint(constraint);
authenticationContraintMapping.setPathSpec("/*");
securityHandler.addConstraintMapping(authenticationContraintMapping);
securityHandler.setAuthenticator(new BasicAuthenticator());
securityHandler.setHandler(baseHandler);
securityHandler.setLoginService(new SimpleAdminLoginService(username, password));
return securityHandler;
}
private HashSessionManager createSessionManager() {
final HashSessionManager sessionManager = new HashSessionManager();
sessionManager.setMaxInactiveInterval(1200);
sessionManager.setUsingCookies(false); // allows to use RAP in multiple tabs
return sessionManager;
}
@Override
protected void doStart(final BundleContext context) throws Exception {
instance = this;
try {
// set to default admin port if null or not a number
adminPort = Integer.valueOf(context.getProperty("gyrex.admin.http.port"));
} catch (final NumberFormatException nfe) {
adminPort = DEFAULT_ADMIN_PORT;
}
adminHost = context.getProperty("gyrex.admin.http.host");
statusTracker = new StatusTracker(context);
statusTracker.open();
// start the admin server asynchronously
final Job jettyStartJob = new Job("Start Jetty Admin Server") {
@Override
protected IStatus run(final IProgressMonitor monitor) {
try {
startServer();
} catch (final Exception e) {
LOG.error("Failed to start Jetty Admin server.", e);
ServerApplication.shutdown(new IllegalStateException("Unable to start Jetty admin server.", e));
return Status.CANCEL_STATUS;
}
return Status.OK_STATUS;
}
};
jettyStartJob.setSystem(true);
jettyStartJob.setPriority(Job.LONG);
jettyStartJob.schedule();
}
@Override
protected void doStop(final BundleContext context) throws Exception {
instance = null;
statusTracker.close();
statusTracker = null;
stopServer();
}
public ImageRegistry getImageRegistry() {
// ImageRegistry must be session scoped in RAP
ImageRegistry imageRegistry = (ImageRegistry) RWT.getUISession().getAttribute(IMAGE_REGISTRY);
if (imageRegistry == null) {
imageRegistry = new ImageRegistry(Display.getCurrent());
AdminUiImages.initializeImageRegistry(imageRegistry);
RWT.getUISession().setAttribute(IMAGE_REGISTRY, imageRegistry);
}
return imageRegistry;
}
public IStatus getSystemStatus() {
final StatusTracker tracker = statusTracker;
if (tracker == null)
throw createBundleInactiveException();
return tracker.getSystemStatus();
}
private void startServer() {
try {
server = new Server();
if (useSslConnector.isTrue()) {
addSslConnector(server);
} else {
addNonSslConnector(server);
}
// tweak server
server.setStopAtShutdown(true);
server.setStopTimeout(5000);
// set thread pool
// TODO: (Jetty9?) final QueuedThreadPool threadPool = new QueuedThreadPool(5);
// TODO: (Jetty9?) threadPool.setName("jetty-server-admin");
// TODO: (Jetty9?) server.setThreadPool(threadPool);
// create context
final ServletContextHandler contextHandler = new ServletContextHandler();
contextHandler.setSessionHandler(new SessionHandler(createSessionManager()));
configureContextWithServletsAndResources(contextHandler);
// enable authentication if configured
final String authenticationPhrase = authenticationConfigString.get();
if (useSslConnector.isTrue() && StringUtils.isNotBlank(authenticationPhrase)) {
final String[] segments = authenticationPhrase.split("/");
if (segments.length != 3)
throw new IllegalArgumentException("Illegal authentication configuration. Must be three string separated by '/'");
else if (!StringUtils.equals(segments[0], "BASIC"))
throw new IllegalArgumentException("Illegal authentication configuration. Only method 'BASIC' is supported. Found " + segments[0]);
server.setHandler(createSecurityHandler(contextHandler, segments[1], segments[2]));
} else {
server.setHandler(contextHandler);
}
server.start();
} catch (final Exception e) {
throw new IllegalStateException("Error starting jetty for admin ui", e);
}
}
private void stopServer() {
try {
adminApplicationRunner.stop();
adminApplicationRunner = null;
server.stop();
server = null;
} catch (final Exception e) {
throw new IllegalStateException("Error stopping jetty for admin ui", e);
}
}
}