/*! ******************************************************************************
*
* Pentaho Data Integration
*
* Copyright (C) 2002-2016 by Pentaho : http://www.pentaho.com
*
*******************************************************************************
*
* 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.pentaho.di.www;
import com.sun.jersey.spi.container.servlet.ServletContainer;
import org.eclipse.jetty.plus.jaas.JAASLoginService;
import org.eclipse.jetty.security.ConstraintMapping;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.security.HashLoginService;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.UserIdentity;
import org.eclipse.jetty.server.bio.SocketConnector;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.server.handler.ResourceHandler;
import org.eclipse.jetty.server.ssl.SslSocketConnector;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.security.Credential;
import org.eclipse.jetty.util.security.Password;
import org.pentaho.di.cluster.SlaveServer;
import org.pentaho.di.core.Const;
import org.pentaho.di.core.util.Utils;
import org.pentaho.di.core.KettleEnvironment;
import org.pentaho.di.core.exception.KettleException;
import org.pentaho.di.core.extension.ExtensionPointHandler;
import org.pentaho.di.core.extension.KettleExtensionPoint;
import org.pentaho.di.core.logging.LogChannelInterface;
import org.pentaho.di.core.plugins.CartePluginType;
import org.pentaho.di.core.plugins.PluginInterface;
import org.pentaho.di.core.plugins.PluginRegistry;
import org.pentaho.di.i18n.BaseMessages;
import javax.servlet.Servlet;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
public class WebServer {
private static final int DEFAULT_DETECTION_TIMER = 20000;
private static Class<?> PKG = WebServer.class; // for i18n purposes, needed by Translator2!!
private LogChannelInterface log;
public static final int PORT = 80;
private Server server;
private TransformationMap transformationMap;
private JobMap jobMap;
private List<SlaveServerDetection> detections;
private SocketRepository socketRepository;
private String hostname;
private int port;
private Timer slaveMonitoringTimer;
private String passwordFile;
private WebServerShutdownHook webServerShutdownHook;
private IWebServerShutdownHandler webServerShutdownHandler = new DefaultWebServerShutdownHandler();
private SslConfiguration sslConfig;
public WebServer( LogChannelInterface log, TransformationMap transformationMap, JobMap jobMap,
SocketRepository socketRepository, List<SlaveServerDetection> detections, String hostname, int port, boolean join,
String passwordFile ) throws Exception {
this( log, transformationMap, jobMap, socketRepository, detections, hostname, port, join, passwordFile, null );
}
public WebServer( LogChannelInterface log, TransformationMap transformationMap, JobMap jobMap,
SocketRepository socketRepository, List<SlaveServerDetection> detections, String hostname, int port, boolean join,
String passwordFile, SslConfiguration sslConfig ) throws Exception {
this.log = log;
this.transformationMap = transformationMap;
this.jobMap = jobMap;
this.socketRepository = socketRepository;
this.detections = detections;
this.hostname = hostname;
this.port = port;
this.passwordFile = passwordFile;
this.sslConfig = sslConfig;
startServer();
// Start the monitoring of the registered slave servers...
//
startSlaveMonitoring();
webServerShutdownHook = new WebServerShutdownHook( this );
Runtime.getRuntime().addShutdownHook( webServerShutdownHook );
try {
ExtensionPointHandler.callExtensionPoint( log, KettleExtensionPoint.CarteStartup.id, this );
} catch ( KettleException e ) {
// Log error but continue regular operations to make sure Carte continues to run properly
//
log.logError( "Error calling extension point CarteStartup", e );
}
if ( join ) {
server.join();
}
}
public WebServer( LogChannelInterface log, TransformationMap transformationMap, JobMap jobMap,
SocketRepository socketRepository, List<SlaveServerDetection> slaveServers, String hostname, int port )
throws Exception {
this( log, transformationMap, jobMap, socketRepository, slaveServers, hostname, port, true );
}
public WebServer( LogChannelInterface log, TransformationMap transformationMap, JobMap jobMap,
SocketRepository socketRepository, List<SlaveServerDetection> detections, String hostname, int port,
boolean join ) throws Exception {
this( log, transformationMap, jobMap, socketRepository, detections, hostname, port, join, null, null );
}
public Server getServer() {
return server;
}
public void startServer() throws Exception {
server = new Server();
List<String> roles = new ArrayList<String>();
roles.add( Constraint.ANY_ROLE );
// Set up the security handler, optionally with JAAS
//
ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
if ( System.getProperty( "loginmodulename" ) != null
&& System.getProperty( "java.security.auth.login.config" ) != null ) {
JAASLoginService jaasLoginService = new JAASLoginService( "Kettle" );
jaasLoginService.setLoginModuleName( System.getProperty( "loginmodulename" ) );
securityHandler.setLoginService( jaasLoginService );
} else {
roles.add( "default" );
HashLoginService hashLoginService;
SlaveServer slaveServer = transformationMap.getSlaveServerConfig().getSlaveServer();
if ( !Utils.isEmpty( slaveServer.getPassword() ) ) {
hashLoginService = new HashLoginService( "Kettle" );
hashLoginService.putUser( slaveServer.getUsername(), new Password( slaveServer.getPassword() ),
new String[] { "default" } );
} else {
// See if there is a kettle.pwd file in the KETTLE_HOME directory:
if ( Utils.isEmpty( passwordFile ) ) {
File homePwdFile = new File( Const.getKettleCartePasswordFile() );
if ( homePwdFile.exists() ) {
passwordFile = Const.getKettleCartePasswordFile();
} else {
passwordFile = Const.getKettleLocalCartePasswordFile();
}
}
hashLoginService = new HashLoginService( "Kettle", passwordFile ) {
@Override public synchronized UserIdentity putUser( String userName, Credential credential, String[] roles ) {
List<String> newRoles = new ArrayList<String>();
newRoles.add( "default" );
Collections.addAll( newRoles, roles );
return super.putUser( userName, credential, newRoles.toArray( new String[newRoles.size()] ) );
}
};
}
securityHandler.setLoginService( hashLoginService );
}
Constraint constraint = new Constraint();
constraint.setName( Constraint.__BASIC_AUTH );
constraint.setRoles( roles.toArray( new String[roles.size()] ) );
constraint.setAuthenticate( true );
ConstraintMapping constraintMapping = new ConstraintMapping();
constraintMapping.setConstraint( constraint );
constraintMapping.setPathSpec( "/*" );
securityHandler.setConstraintMappings( new ConstraintMapping[] { constraintMapping } );
// Add all the servlets defined in kettle-servlets.xml ...
//
ContextHandlerCollection contexts = new ContextHandlerCollection();
// Root
//
ServletContextHandler
root =
new ServletContextHandler( contexts, GetRootServlet.CONTEXT_PATH, ServletContextHandler.SESSIONS );
GetRootServlet rootServlet = new GetRootServlet();
rootServlet.setJettyMode( true );
root.addServlet( new ServletHolder( rootServlet ), "/*" );
PluginRegistry pluginRegistry = PluginRegistry.getInstance();
List<PluginInterface> plugins = pluginRegistry.getPlugins( CartePluginType.class );
for ( PluginInterface plugin : plugins ) {
CartePluginInterface servlet = pluginRegistry.loadClass( plugin, CartePluginInterface.class );
servlet.setup( transformationMap, jobMap, socketRepository, detections );
servlet.setJettyMode( true );
ServletContextHandler servletContext =
new ServletContextHandler( contexts, getContextPath( servlet ), ServletContextHandler.SESSIONS );
ServletHolder servletHolder = new ServletHolder( (Servlet) servlet );
servletContext.addServlet( servletHolder, "/*" );
}
// setup jersey (REST)
ServletHolder jerseyServletHolder = new ServletHolder( ServletContainer.class );
jerseyServletHolder.setInitParameter( "com.sun.jersey.config.property.resourceConfigClass",
"com.sun.jersey.api.core.PackagesResourceConfig" );
jerseyServletHolder.setInitParameter( "com.sun.jersey.config.property.packages", "org.pentaho.di.www.jaxrs" );
root.addServlet( jerseyServletHolder, "/api/*" );
// setup static resource serving
// ResourceHandler mobileResourceHandler = new ResourceHandler();
// mobileResourceHandler.setWelcomeFiles(new String[]{"index.html"});
// mobileResourceHandler.setResourceBase(getClass().getClassLoader().
// getResource("org/pentaho/di/www/mobile").toExternalForm());
// Context mobileContext = new Context(contexts, "/mobile", Context.SESSIONS);
// mobileContext.setHandler(mobileResourceHandler);
// Allow png files to be shown for transformations and jobs...
//
ResourceHandler resourceHandler = new ResourceHandler();
resourceHandler.setResourceBase( "temp" );
// add all handlers/contexts to server
HandlerList handlers = new HandlerList();
handlers.setHandlers( new Handler[] { contexts, resourceHandler } );
securityHandler.setHandler( handlers );
server.setHandler( securityHandler );
// Start execution
createListeners();
server.start();
}
public String getContextPath( CartePluginInterface servlet ) {
String contextPath = servlet.getContextPath();
if ( !contextPath.startsWith( "/kettle" ) ) {
contextPath = "/kettle" + contextPath;
}
return contextPath;
}
public void join() throws InterruptedException {
server.join();
}
public void stopServer() {
webServerShutdownHook.setShuttingDown( true );
try {
ExtensionPointHandler.callExtensionPoint( log, KettleExtensionPoint.CarteShutdown.id, this );
} catch ( KettleException e ) {
// Log error but continue regular operations to make sure Carte can be shut down properly.
//
log.logError( "Error calling extension point CarteStartup", e );
}
try {
if ( server != null ) {
// Stop the monitoring timer
//
if ( slaveMonitoringTimer != null ) {
slaveMonitoringTimer.cancel();
slaveMonitoringTimer = null;
}
// Clean up all the server sockets...
//
socketRepository.closeAll();
// Stop the server...
//
server.stop();
KettleEnvironment.shutdown();
if ( webServerShutdownHandler != null ) {
webServerShutdownHandler.shutdownWebServer();
}
}
} catch ( Exception e ) {
log.logError( BaseMessages.getString( PKG, "WebServer.Error.FailedToStop.Title" ),
BaseMessages.getString( PKG, "WebServer.Error.FailedToStop.Msg", "" + e ) );
}
}
private void createListeners() {
SocketConnector connector = getConnector();
setupJettyOptions( connector );
connector.setPort( port );
connector.setHost( hostname );
connector.setName( BaseMessages.getString( PKG, "WebServer.Log.KettleHTTPListener", hostname ) );
log.logBasic( BaseMessages.getString( PKG, "WebServer.Log.CreateListener", hostname, "" + port ) );
server.setConnectors( new Connector[] { connector } );
}
private SocketConnector getConnector() {
if ( sslConfig != null ) {
log.logBasic( BaseMessages.getString( PKG, "WebServer.Log.SslModeUsing" ) );
SslSocketConnector connector = new SslSocketConnector();
connector.setKeystore( sslConfig.getKeyStore() );
connector.setPassword( sslConfig.getKeyStorePassword() );
connector.setKeyPassword( sslConfig.getKeyPassword() );
connector.setKeystoreType( sslConfig.getKeyStoreType() );
return connector;
} else {
return new SocketConnector();
}
}
/**
* Set up jetty options to the connector
*
* @param connector
*/
protected void setupJettyOptions( SocketConnector connector ) {
if ( validProperty( Const.KETTLE_CARTE_JETTY_ACCEPTORS ) ) {
connector.setAcceptors( Integer.parseInt( System.getProperty( Const.KETTLE_CARTE_JETTY_ACCEPTORS ) ) );
log.logBasic(
BaseMessages.getString( PKG, "WebServer.Log.ConfigOptions", "acceptors", connector.getAcceptors() ) );
}
if ( validProperty( Const.KETTLE_CARTE_JETTY_ACCEPT_QUEUE_SIZE ) ) {
connector
.setAcceptQueueSize( Integer.parseInt( System.getProperty( Const.KETTLE_CARTE_JETTY_ACCEPT_QUEUE_SIZE ) ) );
log.logBasic( BaseMessages
.getString( PKG, "WebServer.Log.ConfigOptions", "acceptQueueSize", connector.getAcceptQueueSize() ) );
}
if ( validProperty( Const.KETTLE_CARTE_JETTY_RES_MAX_IDLE_TIME ) ) {
connector.setLowResourceMaxIdleTime(
Integer.parseInt( System.getProperty( Const.KETTLE_CARTE_JETTY_RES_MAX_IDLE_TIME ) ) );
log.logBasic( BaseMessages.getString( PKG, "WebServer.Log.ConfigOptions", "lowResourcesMaxIdleTime",
connector.getLowResourceMaxIdleTime() ) );
}
}
/**
* Checks if the property is not null or not empty String that can be parseable as int and returns true if it is,
* otherwise false
*
* @param property the property to check
* @return true if the property is not null or not empty String that can be parseable as int, false otherwise
*/
private boolean validProperty( String property ) {
boolean isValid = false;
if ( System.getProperty( property ) != null && System.getProperty( property ).length() > 0 ) {
try {
Integer.parseInt( System.getProperty( property ) );
isValid = true;
} catch ( NumberFormatException nmbfExc ) {
log.logBasic( BaseMessages
.getString( PKG, "WebServer.Log.ConfigOptionsInvalid", property, System.getProperty( property ) ) );
}
}
return isValid;
}
/**
* @return the hostname
*/
public String getHostname() {
return hostname;
}
/**
* @param hostname the hostname to set
*/
public void setHostname( String hostname ) {
this.hostname = hostname;
}
/**
* @return the slave server detections
*/
public List<SlaveServerDetection> getDetections() {
return detections;
}
/**
* This method registers a timer to check up on all the registered slave servers every X seconds.<br>
*/
private void startSlaveMonitoring() {
slaveMonitoringTimer = new Timer( "WebServer Timer" );
TimerTask timerTask = new TimerTask() {
public void run() {
for ( SlaveServerDetection slaveServerDetection : detections ) {
SlaveServer slaveServer = slaveServerDetection.getSlaveServer();
// See if we can get a status...
//
try {
// TODO: consider making this lighter or retaining more information...
slaveServer.getStatus(); // throws the exception
slaveServerDetection.setActive( true );
slaveServerDetection.setLastActiveDate( new Date() );
} catch ( Exception e ) {
slaveServerDetection.setActive( false );
slaveServerDetection.setLastInactiveDate( new Date() );
// TODO: kick it out after a configurable period of time...
}
}
}
};
int detectionTime = defaultDetectionTimer();
slaveMonitoringTimer.schedule( timerTask, detectionTime, detectionTime );
}
/**
* @return the socketRepository
*/
public SocketRepository getSocketRepository() {
return socketRepository;
}
/**
* @param socketRepository the socketRepository to set
*/
public void setSocketRepository( SocketRepository socketRepository ) {
this.socketRepository = socketRepository;
}
public String getPasswordFile() {
return passwordFile;
}
public void setPasswordFile( String passwordFile ) {
this.passwordFile = passwordFile;
}
public LogChannelInterface getLog() {
return log;
}
public void setLog( LogChannelInterface log ) {
this.log = log;
}
public TransformationMap getTransformationMap() {
return transformationMap;
}
public void setTransformationMap( TransformationMap transformationMap ) {
this.transformationMap = transformationMap;
}
public JobMap getJobMap() {
return jobMap;
}
public void setJobMap( JobMap jobMap ) {
this.jobMap = jobMap;
}
public int getPort() {
return port;
}
public void setPort( int port ) {
this.port = port;
}
public Timer getSlaveMonitoringTimer() {
return slaveMonitoringTimer;
}
public void setSlaveMonitoringTimer( Timer slaveMonitoringTimer ) {
this.slaveMonitoringTimer = slaveMonitoringTimer;
}
public void setServer( Server server ) {
this.server = server;
}
public void setDetections( List<SlaveServerDetection> detections ) {
this.detections = detections;
}
/**
* Can be used to override the default shutdown behavior of performing a System.exit
*
* @param webServerShutdownHandler
*/
public void setWebServerShutdownHandler( IWebServerShutdownHandler webServerShutdownHandler ) {
this.webServerShutdownHandler = webServerShutdownHandler;
}
public int defaultDetectionTimer() {
String sDetectionTimer = System.getProperty( Const.KETTLE_SLAVE_DETECTION_TIMER );
if ( sDetectionTimer != null ) {
return Integer.parseInt( sDetectionTimer );
} else {
return DEFAULT_DETECTION_TIMER;
}
}
}