/**
* Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2006-2016
*
* 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.italiangrid.voms.container;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.jar.Attributes;
import java.util.jar.Attributes.Name;
import java.util.jar.JarFile;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.eclipse.jetty.deploy.DeploymentManager;
import org.eclipse.jetty.jmx.MBeanContainer;
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
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.util.log.Log;
import org.italiangrid.utils.https.JettyRunThread;
import org.italiangrid.utils.https.SSLOptions;
import org.italiangrid.utils.https.ServerFactory;
import org.italiangrid.utils.https.impl.canl.CANLListener;
import org.italiangrid.voms.container.legacy.VOMSSslConnectorConfigurator;
import org.italiangrid.voms.container.lifecycle.ServerListener;
import org.italiangrid.voms.util.CertificateValidatorBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.core.joran.spi.JoranException;
import eu.emi.security.authn.x509.CrlCheckingMode;
import eu.emi.security.authn.x509.OCSPCheckingMode;
import eu.emi.security.authn.x509.X509CertChainValidatorExt;
public class Container {
public static final String LOGGER_CONTEXT_NAME = "voms-admin";
public static final Logger log = LoggerFactory.getLogger(Container.class);
private static final String TAGLIBS_JAR_NAME = "org.apache.taglibs.standard.glassfish";
public static final String CONF_FILE_NAME = "voms-admin-server.properties";
public static final String DEFAULT_WAR = "/usr/share/webapps/voms-admin.war";
public static final String DEFAULT_TMP_PREFIX = "/var/tmp";
public static final String DEFAULT_DEPLOY_DIR = "/var/lib/voms-admin/vo.d";
public static final String DEFAULT_WORK_DIR = "/var/lib/voms-admin/work";
public static final String HTTP_CONNECTOR_NAME = "voms-http";
public static final String HTTPS_CONNECTOR_NAME = "voms-https";
public static final String HTTP_CONNECTOR_PORT = "8088";
private static final String ARG_WAR = "war";
private static final String ARG_CONFDIR = "confdir";
private static final String ARG_DEPLOYDIR = "deploydir";
private static final String SERVICE_PROPERTIES_FILE = "service.properties";
private static final String SERVICE_PORT_KEY = "voms.aa.x509.additional_port";
private Options cliOptions;
private CommandLineParser parser = new GnuParser();
private Properties serverConfiguration;
private String war;
private String confDir;
private String deployDir;
private String workDir;
private String host;
private String port;
private String statusPort;
private String certFile;
private String keyFile;
private String trustDir;
private long trustDirRefreshIntervalInMsec;
private String bindAddress;
private String tlsExcludeProtocols;
private String tlsIncludeProtocols;
private String tlsExcludeCipherSuites;
private String tlsIncludeCipherSuites;
private boolean tlsRequireClientCert = true;
private Server server;
private DeploymentManager deploymentManager;
private HandlerCollection handlers = new HandlerCollection();
private ContextHandlerCollection contexts = new ContextHandlerCollection();
protected SSLOptions getSSLOptions() {
SSLOptions options = new SSLOptions();
options.setCertificateFile(certFile);
options.setKeyFile(keyFile);
options.setTrustStoreDirectory(trustDir);
options.setTrustStoreRefreshIntervalInMsec(trustDirRefreshIntervalInMsec);
options.setWantClientAuth(true);
options.setNeedClientAuth(tlsRequireClientCert);
if (tlsExcludeCipherSuites != null){
options.setExcludeCipherSuites(tlsExcludeCipherSuites.split(","));
}
if (tlsIncludeCipherSuites != null){
options.setIncludeCipherSuites(tlsIncludeCipherSuites.split(","));
}
if (tlsExcludeProtocols != null){
options.setExcludeProtocols(tlsExcludeProtocols.split(","));
}
if (tlsIncludeProtocols != null){
options.setIncludeProtocols(tlsIncludeProtocols.split(","));
}
return options;
}
protected void confDirSanityChecks() {
File confDirFile = new File(confDir);
if (!confDirFile.exists() || !confDirFile.isDirectory()) {
throw new IllegalArgumentException("VOMS Admin configuration directory "
+ "does not exists or is not a directory: "
+ confDirFile.getAbsolutePath());
}
}
protected Properties getVOServiceProperties(String voName) {
if (!getConfiguredVONames().contains(voName))
throw new IllegalArgumentException("VO " + voName + " is "
+ "not configured on this host.");
Properties voServiceProps = new Properties();
String propsPath = String.format("%s/%s/%s", confDir, voName,
SERVICE_PROPERTIES_FILE).replaceAll("/+", "/");
try {
voServiceProps.load(new FileInputStream(propsPath));
return voServiceProps;
} catch (IOException e) {
log.error(
"Error reading service properties configuration for VO {}: {}.",
new Object[] { voName, e.getMessage() }, e);
throw new RuntimeException(e.getMessage(), e);
}
}
protected Map<Integer, String> getConfiguredVOPortMappings() {
Map<Integer, String> voPorts = new HashMap<Integer, String>();
for (String vo : getConfiguredVONames()) {
Properties sp = getVOServiceProperties(vo);
if (sp.containsKey(SERVICE_PORT_KEY)) {
int port = Integer.parseInt(sp.getProperty(SERVICE_PORT_KEY));
if (port > 0)
voPorts.put(port, vo);
}
}
return voPorts;
}
protected List<String> getConfiguredVONames() {
confDirSanityChecks();
List<String> voNames = new ArrayList<String>();
File confDirFile = new File(confDir);
File[] voFiles = confDirFile.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.isDirectory();
}
});
for (File f : voFiles) {
voNames.add(f.getName());
}
return voNames;
}
protected void configureDeploymentManager() {
contexts.setServer(server);
deploymentManager = new DeploymentManager();
VOMSAppProvider provider = new VOMSAppProvider();
provider.setConfigurationDir(confDir);
provider.setDeploymentDir(deployDir);
provider.setHostname(host);
provider.setPort(port);
provider.setWarFile(war);
provider.setWorkDir(workDir);
deploymentManager.addAppProvider(provider);
deploymentManager.setContexts(contexts);
}
protected void addNameToHTTPSConnector() {
for (Connector c : server.getConnectors()) {
if (c.getPort() == Integer.parseInt(port)) {
SslSelectChannelConnector conn = (SslSelectChannelConnector) c;
conn.setName(HTTPS_CONNECTOR_NAME);
}
}
}
protected void configureLocalHTTPConnector() {
SelectChannelConnector conn = new SelectChannelConnector();
conn.setHost("localhost");
conn.setPort(Integer.parseInt(statusPort));
conn.setName(HTTP_CONNECTOR_NAME);
server.addConnector(conn);
}
protected void configureLegacyConnectors(String host,
X509CertChainValidatorExt validator, SSLOptions options,
int maxConnections, int maxRequestQueueSize) {
VOMSSslConnectorConfigurator configurator = new VOMSSslConnectorConfigurator(
validator);
Map<Integer, String> voMappings = getConfiguredVOPortMappings();
for (Map.Entry<Integer, String> e : voMappings.entrySet()) {
int voPort = e.getKey();
Connector c = configurator.configureConnector(host, voPort, options);
((SslSelectChannelConnector) c).setName("voms-" + e.getValue());
server.addConnector(c);
log.info("Configured VOMS legacy connector for VO {} on port {}.",
e.getValue(), voPort);
}
}
protected void configureJettyServer() {
SSLOptions options = getSSLOptions();
CANLListener l = new CANLListener();
CertificateValidatorBuilder builder = new CertificateValidatorBuilder();
X509CertChainValidatorExt validator = builder
.trustAnchorsDir(options.getTrustStoreDirectory())
.trustAnchorsUpdateInterval(trustDirRefreshIntervalInMsec)
.crlChecks(CrlCheckingMode.IF_VALID).lazyAnchorsLoading(false)
.ocspChecks(OCSPCheckingMode.IGNORE).storeUpdateListener(l)
.validationErrorListener(l)
.build();
int maxConnections = Integer
.parseInt(getConfigurationProperty(ConfigurationProperty.MAX_CONNECTIONS));
int maxRequestQueueSize = Integer
.parseInt(getConfigurationProperty(ConfigurationProperty.MAX_REQUEST_QUEUE_SIZE));
server = ServerFactory.newServer(bindAddress, Integer.parseInt(port),
getSSLOptions(), validator, maxConnections, maxRequestQueueSize);
MBeanContainer mbContainer = new MBeanContainer(ManagementFactory
.getPlatformMBeanServer());
// Enable JMX
server.addBean(mbContainer);
server.getContainer().addEventListener(mbContainer);
mbContainer.addBean(Log.getLog());
addNameToHTTPSConnector();
configureLocalHTTPConnector();
server.addLifeCycleListener(new ServerListener());
configureDeploymentManager();
configureLegacyConnectors(null, validator, options, maxConnections,
maxRequestQueueSize);
// Handlers contains VOMS webapp, status webapp, and as final
// choice the default handler to correctly handle 404 for non-handled
// contexts etc.
handlers.setHandlers(new Handler[] { contexts, new DefaultHandler() });
// The rewrite handler, will internally foward
// generate-ac request to legacy voms ports to the appropriate
// voms-admin webapp
RewriteHandler rh = new RewriteHandler();
rh.setRewritePathInfo(true);
rh.setRewriteRequestURI(true);
rh.addRule(new VOMSRewriteRule(getConfiguredVOPortMappings()));
// webapps are wrapped in the rewrite handler
// or VOMS port rewriting will not work
rh.setHandler(handlers);
server.setHandler(rh);
server.setDumpAfterStart(false);
server.setDumpBeforeStop(false);
server.setStopAtShutdown(true);
server.addBean(deploymentManager);
}
private void start() {
JettyRunThread vomsService = new JettyRunThread(server);
vomsService.start();
}
private void initOptions() {
cliOptions = new Options();
cliOptions.addOption(ARG_WAR, true, "The WAR used to start this server.");
cliOptions.addOption(ARG_DEPLOYDIR, true,
"The VOMS Admin deploy directory.");
cliOptions.addOption(ARG_CONFDIR, true,
"The configuration directory where " + "VOMS configuration is stored.");
}
private void failAndExit(String errorMessage, Throwable t) {
if (t != null) {
System.err.format("%s: %s\n", errorMessage, t.getMessage());
} else {
System.err.println(errorMessage);
}
System.exit(1);
}
private void parseCommandLineOptions(String[] args) {
try {
CommandLine cmdLine = parser.parse(cliOptions, args);
Properties sysconfigProperties = SysconfigUtil.loadSysconfig();
String installationPrefix = SysconfigUtil.getInstallationPrefix();
String defaultPrefixedWarPath = String.format("%s/%s",
installationPrefix, DEFAULT_WAR).replaceAll("/+", "/");
war = cmdLine.getOptionValue(ARG_WAR, defaultPrefixedWarPath);
confDir = cmdLine.getOptionValue(ARG_CONFDIR);
if (confDir == null){
confDir = sysconfigProperties
.getProperty(SysconfigUtil.SYSCONFIG_CONF_DIR);
}
deployDir = cmdLine.getOptionValue(ARG_DEPLOYDIR);
} catch (ParseException e) {
failAndExit("Error parsing command line arguments", e);
}
}
private String getConfigurationProperty(ConfigurationProperty prop) {
return serverConfiguration.getProperty(prop.getPropertyName(),
prop.getDefaultValue());
}
private void loadServerConfiguration(String configurationDir) {
try {
serverConfiguration = new Properties();
String configurationPath = String.format("%s/%s", configurationDir,
CONF_FILE_NAME).replaceAll("/+", "/");
serverConfiguration.load(new FileReader(configurationPath));
} catch (IOException e) {
throw new RuntimeException("Error loading configuration:"
+ e.getMessage(), e);
}
}
private void loadConfiguration() {
// FIXME: move this to standard server conf?
// Then it would be harder to source from the init
// script, which is the main (and only) client of
// the status handler
statusPort = SysconfigUtil.loadSysconfig().getProperty(
SysconfigUtil.SYSCONFIG_STATUS_PORT, HTTP_CONNECTOR_PORT);
loadServerConfiguration(confDir);
host = getConfigurationProperty(ConfigurationProperty.HOST);
port = getConfigurationProperty(ConfigurationProperty.PORT);
bindAddress = getConfigurationProperty(ConfigurationProperty.BIND_ADDRESS);
certFile = getConfigurationProperty(ConfigurationProperty.CERT);
keyFile = getConfigurationProperty(ConfigurationProperty.KEY);
trustDir = getConfigurationProperty(ConfigurationProperty.TRUST_ANCHORS_DIR);
tlsIncludeCipherSuites = getConfigurationProperty(
ConfigurationProperty.TLS_INCLUDE_CIPHER_SUITES);
tlsExcludeCipherSuites = getConfigurationProperty(
ConfigurationProperty.TLS_EXCLUDE_CIPHER_SUITES);
tlsExcludeProtocols = getConfigurationProperty(
ConfigurationProperty.TLS_EXCLUDE_PROTOCOLS);
tlsIncludeProtocols = getConfigurationProperty(
ConfigurationProperty.TLS_INCLUDE_PROTOCOLS);
tlsRequireClientCert = Boolean.parseBoolean(
getConfigurationProperty(ConfigurationProperty.TLS_REQUIRE_CLIENT_CERTIFICATE));
long refreshIntervalInSeconds = Long
.parseLong(getConfigurationProperty(ConfigurationProperty.TRUST_ANCHORS_REFRESH_PERIOD));
trustDirRefreshIntervalInMsec = TimeUnit.SECONDS
.toMillis(refreshIntervalInSeconds);
if (deployDir == null){
deployDir = String.format("%s/%s", SysconfigUtil.getInstallationPrefix(),
DEFAULT_DEPLOY_DIR).replaceAll("/+", "/");
}
workDir = String.format("%s/%s", SysconfigUtil.getInstallationPrefix(),
DEFAULT_WORK_DIR).replaceAll("/+", "/");
}
// Without this trick JSP page rendering on VOMS admin does not work
private void forceTaglibsLoading() {
if (System.getProperty("voms.disableTaglibsLoading")!=null){
log.warn("Taglibs loading disabled, as requested by voms.disableTaglibsLoading");
return;
}
try {
String classpath = java.lang.System.getProperty("java.class.path");
String entries[] = classpath.split(System.getProperty("path.separator"));
if (entries.length >= 1) {
JarFile f = new JarFile(entries[0]);
Attributes attrs = f.getManifest().getMainAttributes();
Name n = new Name("Class-Path");
String jarClasspath = attrs.getValue(n);
String jarEntries[] = jarClasspath.split(" ");
boolean taglibsFound = false;
for (String e : jarEntries) {
if (e.contains(TAGLIBS_JAR_NAME)) {
taglibsFound = true;
ClassLoader currentClassLoader = Thread.currentThread()
.getContextClassLoader();
File taglibsJar = new File(e);
URLClassLoader newClassLoader = new URLClassLoader(
new URL[] { taglibsJar.toURI().toURL() }, currentClassLoader);
Thread.currentThread().setContextClassLoader(newClassLoader);
}
}
f.close();
if (!taglibsFound) {
throw new RuntimeException("Error configuring taglibs classloading!");
}
}
} catch (IOException e) {
log.error(e.getMessage(), e);
System.exit(1);
}
}
public Container(String[] args) {
// Leave this here and first
forceTaglibsLoading();
try {
initOptions();
parseCommandLineOptions(args);
configureLogging();
} catch (Throwable t) {
// Here we print the error to standard error as the logging setup
// could be incomplete
System.err.println("Error starting voms-admin server: " + t.getMessage());
t.printStackTrace(System.err);
System.exit(1);
}
try {
loadConfiguration();
logStartupConfiguration();
configureJettyServer();
start();
} catch (Throwable t) {
log.error("Error starting voms-admin server: " + t.getMessage(), t);
System.exit(1);
}
}
private void logStartupConfiguration() {
log.info("VOMS Admin version {}.", Version.version());
log.info("Hostname: {}", host);
if (bindAddress != null) {
log.info("Binding on: {}:{}", bindAddress, port);
} else {
log.info("Binding on all interfaces on port: {}", port);
}
if (tlsIncludeProtocols != null){
log.info("TLS included protocols: {}", tlsIncludeProtocols);
}
if (tlsExcludeProtocols != null){
log.info("TLS excluded protocols: {}", tlsExcludeProtocols);
}
if (tlsIncludeCipherSuites != null){
log.info("TLS included cipher suites: {}", tlsIncludeCipherSuites);
}
if (tlsExcludeCipherSuites != null){
log.info("TLS excluded cipher suites: {}", tlsExcludeCipherSuites);
}
if (!tlsRequireClientCert){
log.warn("TLS require client certificate: {}.", tlsRequireClientCert);
}else {
log.info("TLS require client certificate: {}", tlsRequireClientCert);
}
log.info("HTTP status handler listening on: {}", statusPort);
log.info("Service credentials: {}, {}", certFile, keyFile);
log.info("Trust anchors directory: {}", trustDir);
log.info("Trust anchors directory refresh interval (in minutes): {}",
TimeUnit.MILLISECONDS.toMinutes(trustDirRefreshIntervalInMsec));
log.info("Web archive location: {}", war);
log.info("Configuration dir: {}", confDir);
log.info("Deployment dir: {}", deployDir);
log.info("Max # of concurrent connections: {}",
getConfigurationProperty(ConfigurationProperty.MAX_CONNECTIONS));
log.info("Max request queue size: {}",
getConfigurationProperty(ConfigurationProperty.MAX_REQUEST_QUEUE_SIZE));
}
private void configureLogging() {
String loggingConf = String.format("%s/%s", confDir,
"voms-admin-server.logback").replaceAll("/+", "/");
File f = new File(loggingConf);
if (!f.exists() || !f.canRead()) {
log.error("Error loading logging configuration: "
+ "{} does not exist or is not readable.");
return;
}
LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
lc.setName(LOGGER_CONTEXT_NAME);
JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(lc);
lc.reset();
try {
configurator.doConfigure(loggingConf);
} catch (JoranException e) {
failAndExit("Error setting up the logging system", e);
}
}
public static void main(String[] args) {
new Container(args);
}
}