/*
* Copyright (c) Members of the EGEE Collaboration. 2006-2010.
* See http://www.eu-egee.org/partners/ for details on the copyright holders.
*
* 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.glite.authz.pep.server;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.security.Security;
import java.util.List;
import java.util.Timer;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Status;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.glite.authz.common.config.ConfigurationException;
import org.glite.authz.common.http.JettyAdminService;
import org.glite.authz.common.http.JettyRunThread;
import org.glite.authz.common.http.JettyShutdownTask;
import org.glite.authz.common.http.JettySslSelectChannelConnector;
import org.glite.authz.common.http.ServiceMetricsServlet;
import org.glite.authz.common.http.StatusCommand;
import org.glite.authz.common.http.SystemExitTask;
import org.glite.authz.common.http.TimerShutdownTask;
import org.glite.authz.common.logging.AccessLoggingFilter;
import org.glite.authz.common.logging.LoggingReloadTask;
import org.glite.authz.common.util.Files;
import org.glite.authz.pep.pip.PolicyInformationPoint;
import org.glite.authz.pep.server.config.PEPDaemonConfiguration;
import org.glite.authz.pep.server.config.PEPDaemonIniConfigurationParser;
import org.mortbay.jetty.Connector;
import org.mortbay.jetty.Server;
import org.mortbay.jetty.nio.SelectChannelConnector;
import org.mortbay.jetty.servlet.Context;
import org.mortbay.jetty.servlet.FilterHolder;
import org.mortbay.jetty.servlet.ServletHolder;
import org.mortbay.thread.concurrent.ThreadPool;
import org.opensaml.DefaultBootstrap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The daemon component for the PEP.
*
* The daemon listens for either HTTP GET or POST requests. When receiving an
* HTTP GET request it expects request to be a Base64 encoded value bound to the
* 'request' URL parameter. When receiving an HTTP POST request it expects the
* request to be the body of the message in a Base64 encoded form. In both cases
* the message is the Hessian2 serialized form of a
* {@link org.glite.authz.pep.model.Request} object and the response is a
* Hessian2 serialized {@link org.glite.authz.pep.model.Response}.
*/
public final class PEPDaemon {
/** System property name PEPD_HOME path is bound to. */
public static final String PEP_HOME_PROP= "org.glite.authz.pep.home";
/** System property name PEPD_CONFDIR path is bound to. */
public static final String PEP_CONFDIR_PROP= "org.glite.authz.pep.confdir";
/** System property name PEPD_LOGDIR path is bound to. */
public static final String PEP_LOGDIR_PROP= "org.glite.authz.pep.logdir";
/** System property name PEP_GRACEFUL to set to force a graceful shutdown */
public static final String PEP_GRACEFUL_PROP= "org.glite.authz.pep.server.graceful";
/** Default admin port: {@value} */
public static int DEFAULT_ADMIN_PORT= 8155;
/** Default admin host: {@value} */
public static String DEFAULT_ADMIN_HOST= "localhost";
/** Default service port: {@value} */
public static int DEFAULT_SERVICE_PORT= 8154;
/** Default logging configuration refresh period: {@value} ms */
public static final int DEFAULT_LOGGING_CONFIG_REFRESH_PERIOD= 5 * 60 * 1000;
/** Class logger. */
private static final Logger LOG= LoggerFactory.getLogger(PEPDaemon.class);
/** Constructor. */
private PEPDaemon() {
}
/**
* Entry point for starting the daemon.
*
* @param args
* command line arguments
*
* @throws Exception
* thrown if there is a problem starting the daemon
*/
public static void main(String[] args) throws Exception {
if (args.length < 1 || args.length > 1) {
errorAndExit("Missing configuration file argument", null);
}
String confDir= System.getProperty(PEP_CONFDIR_PROP);
if (confDir == null) {
errorAndExit("System property " + PEP_CONFDIR_PROP + " is not set",
null);
}
final Timer backgroundTaskTimer= new Timer(true);
String loggingConfigFilePath= confDir + "/logging.xml";
initializeLogging(loggingConfigFilePath, backgroundTaskTimer);
Security.addProvider(new BouncyCastleProvider());
DefaultBootstrap.bootstrap();
final PEPDaemonConfiguration daemonConfig= parseConfiguration(args[0]);
List<PolicyInformationPoint> pips= daemonConfig.getPolicyInformationPoints();
if (pips != null && !pips.isEmpty()) {
for (PolicyInformationPoint pip : daemonConfig.getPolicyInformationPoints()) {
if (pip != null) {
LOG.debug("Starting PIP {}", pip.getId());
pip.start();
}
}
}
Server pepDaemonService= createPEPDaemonService(daemonConfig);
JettyRunThread pepDaemonServiceThread= new JettyRunThread(pepDaemonService);
pepDaemonServiceThread.setName("PEP Server Service");
pepDaemonServiceThread.start();
JettyAdminService adminService= createAdminService(daemonConfig,
backgroundTaskTimer,
pepDaemonService);
LOG.debug("Starting admin service");
adminService.start();
LOG.info(Version.getServiceIdentifier() + " started");
}
/**
* Creates the PEP daemon service to run.
*
* @param daemonConfig
* the configuration for the service
*
* @return a configured PEP daemon server
*/
private static Server createPEPDaemonService(
PEPDaemonConfiguration daemonConfig) {
Server httpServer= new Server();
httpServer.setSendServerVersion(false);
httpServer.setSendDateHeader(false);
if (System.getProperty(PEP_GRACEFUL_PROP)!=null) {
LOG.info("Graceful shutdown enabled: " + PEP_GRACEFUL_PROP );
httpServer.setGracefulShutdown(1000); // 1 sec
}
httpServer.setStopAtShutdown(true);
BlockingQueue<Runnable> requestQueue;
if (daemonConfig.getMaxRequestQueueSize() < 1) {
requestQueue= new LinkedBlockingQueue<Runnable>();
}
else {
requestQueue= new ArrayBlockingQueue<Runnable>(daemonConfig.getMaxRequestQueueSize());
}
ThreadPool threadPool= new ThreadPool(5,
daemonConfig.getMaxRequests(),
1,
TimeUnit.SECONDS,
requestQueue);
httpServer.setThreadPool(threadPool);
Connector connector= createServiceConnector(daemonConfig);
httpServer.setConnectors(new Connector[] { connector });
Context servletContext= new Context(httpServer, "/", false, false);
servletContext.setDisplayName("PEP Server");
servletContext.setAttribute(PEPDaemonConfiguration.BINDING_NAME,
daemonConfig);
FilterHolder accessLoggingFilter= new FilterHolder(new AccessLoggingFilter());
servletContext.addFilter(accessLoggingFilter, "/*", Context.REQUEST);
ServletHolder authzRequestServlet= new ServletHolder(new PEPDaemonServlet());
authzRequestServlet.setName("Authorization Servlet");
servletContext.addServlet(authzRequestServlet, "/authz");
ServletHolder statusRequestServlet= new ServletHolder(new ServiceMetricsServlet(daemonConfig.getServiceMetrics()));
statusRequestServlet.setName("Status Servlet");
servletContext.addServlet(statusRequestServlet, "/status");
return httpServer;
}
/**
* Builds an admin service for the PEP daemon. This admin service has the
* following commands registered with it:
*
* <ul>
* <li><em>shutdown</em> - shuts down the PDP daemon service and the admin
* service</li>
* <li><em>status</em> - prints out a status page w/ metrics</li>
* <li><em>expungeResponseCache</em> - expunges all the current entries in
* the PDP response cache</li>
* </ul>
*
* In addition, a shutdown task that will shutdown all caches is also
* registered.
*
* @param daemonConfig
* PEP daemon configuration
* @param backgroundTimer
* timer used for background tasks
* @param daemonService
* the PEP daemon service
*
* @return the admin service
*/
private static JettyAdminService createAdminService(
PEPDaemonConfiguration daemonConfig, Timer backgroundTimer,
Server daemonService) {
String adminHost= daemonConfig.getAdminHost();
if (adminHost == null) {
adminHost= DEFAULT_ADMIN_HOST;
}
int adminPort= daemonConfig.getAdminPort();
if (adminPort < 1) {
adminPort= DEFAULT_ADMIN_PORT;
}
JettyAdminService adminService= new JettyAdminService(adminHost,
adminPort,
daemonConfig.getAdminPassword());
adminService.registerAdminCommand(new StatusCommand(daemonConfig.getServiceMetrics()));
adminService.registerAdminCommand(new ClearResponseCacheCommand());
// first shutdown task will force a System.exit(0) after 60 sec.
adminService.registerShutdownTask(new SystemExitTask(60000));
adminService.registerShutdownTask(new TimerShutdownTask(backgroundTimer));
adminService.registerShutdownTask(new JettyShutdownTask(daemonService));
adminService.registerShutdownTask(new Runnable() {
public void run() {
CacheManager cacheMgr= CacheManager.getInstance();
if (cacheMgr != null
&& cacheMgr.getStatus() == Status.STATUS_ALIVE) {
cacheMgr.shutdown();
}
}
});
return adminService;
}
/**
* Creates the HTTP connector used to receive authorization requests.
*
* @param daemonConfig
* the daemon configuration
*
* @return the created connector
*/
private static Connector createServiceConnector(
PEPDaemonConfiguration daemonConfig) {
Connector connector;
if (!daemonConfig.isSslEnabled()) {
connector= new SelectChannelConnector();
}
else {
if (daemonConfig.getKeyManager() == null) {
LOG.error("Service port was meant to be SSL enabled but no service key/certificate was specified in the configuration file");
}
if (daemonConfig.getTrustManager() == null) {
LOG.error("Service port was meant to be SSL enabled but no trust information directory was specified in the configuration file");
}
connector= new JettySslSelectChannelConnector(daemonConfig.getKeyManager(),
daemonConfig.getTrustManager());
if (daemonConfig.isClientCertAuthRequired()) {
((JettySslSelectChannelConnector) connector).setNeedClientAuth(true);
}
}
connector.setHost(daemonConfig.getHostname());
if (daemonConfig.getPort() == 0) {
connector.setPort(DEFAULT_SERVICE_PORT);
}
else {
connector.setPort(daemonConfig.getPort());
}
connector.setMaxIdleTime(daemonConfig.getConnectionTimeout());
connector.setRequestBufferSize(daemonConfig.getReceiveBufferSize());
connector.setResponseBufferSize(daemonConfig.getSendBufferSize());
return connector;
}
/**
* Reads the configuration file and creates a configuration from it.
*
* @param configFilePath
* path to configuration file
*
* @return configuration file and creates a configuration from it
*/
private static PEPDaemonConfiguration parseConfiguration(
String configFilePath) {
File configFile= null;
try {
LOG.info("PEP Daemon configuration file: {}", configFilePath);
configFile= Files.getReadableFile(configFilePath);
} catch (IOException e) {
errorAndExit(e.getMessage(), null);
}
try {
PEPDaemonIniConfigurationParser configParser= new PEPDaemonIniConfigurationParser();
return configParser.parse(new FileReader(configFile));
} catch (IOException e) {
LOG.error("Unable to read configuration file", e);
errorAndExit("Unable to read configuration file " + configFilePath,
e);
} catch (ConfigurationException e) {
LOG.error("Unable to load configuration file", e);
errorAndExit("Error parsing configuration file " + configFilePath,
e);
}
return null;
}
/**
* Logs, as an error, the error message and exits the program.
*
* @param errorMessage
* error message
* @param e
* exception that caused it
*/
private static void errorAndExit(String errorMessage, Exception e) {
System.err.println(errorMessage);
if (e != null) {
System.err.println("This error was caused by the exception:");
e.printStackTrace(System.err);
}
System.out.flush();
System.exit(1);
}
/**
* Initializes the logging system and starts the process to watch for config
* file changes (5 min).
* <p>
* The function uses {@link #errorAndExit(String, Exception)} if the
* loggingConfigFilePath is a directory, does not exist, or can not be read
*
* @param loggingConfigFilePath
* path to the logging configuration file
* @param reloadTasks
* timer controlling the reloading of tasks
*/
private static void initializeLogging(String loggingConfigFilePath,
Timer reloadTasks) {
LoggingReloadTask reloadTask= null;
try {
reloadTask= new LoggingReloadTask(loggingConfigFilePath);
} catch (IOException e) {
errorAndExit("Invalid logging configuration file: "
+ loggingConfigFilePath, e);
}
// check/reload every 5 minutes
reloadTask.run();
reloadTasks.scheduleAtFixedRate(reloadTask,
DEFAULT_LOGGING_CONFIG_REFRESH_PERIOD,
DEFAULT_LOGGING_CONFIG_REFRESH_PERIOD);
}
}