blob: 196695e1fc6b3b36a0f41f3e3e9bbe0d9daa98f9 [file] [log] [blame]
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.apache.cloudstack;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.management.ManagementFactory;
import java.net.URL;
import java.util.Arrays;
import java.util.Properties;
import com.cloud.api.ApiServer;
import org.apache.commons.daemon.Daemon;
import org.apache.commons.daemon.DaemonContext;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jetty.jmx.MBeanContainer;
import org.eclipse.jetty.server.ForwardedRequestCustomizer;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.RequestLog;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.server.handler.MovedContextHandler;
import org.eclipse.jetty.server.handler.RequestLogHandler;
import org.eclipse.jetty.server.handler.gzip.GzipHandler;
import org.eclipse.jetty.server.session.SessionHandler;
import org.eclipse.jetty.util.ssl.KeyStoreScanner;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
import org.eclipse.jetty.webapp.WebAppContext;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import com.cloud.utils.Pair;
import com.cloud.utils.PropertiesUtil;
import com.cloud.utils.server.ServerProperties;
/***
* The ServerDaemon class implements the embedded server, it can be started either
* using JSVC or directly from the JAR along with additional jars not shaded in the uber-jar.
* Configuration parameters are read from server.properties file available on the classpath.
*/
public class ServerDaemon implements Daemon {
protected Logger logger = LogManager.getLogger(getClass());
private static final String WEB_XML = "META-INF/webapp/WEB-INF/web.xml";
/////////////////////////////////////////////////////
/////////////// Server Properties ///////////////////
/////////////////////////////////////////////////////
private static final String BIND_INTERFACE = "bind.interface";
private static final String CONTEXT_PATH = "context.path";
private static final String SESSION_TIMEOUT = "session.timeout";
private static final String HTTP_ENABLE = "http.enable";
private static final String HTTP_PORT = "http.port";
private static final String HTTPS_ENABLE = "https.enable";
private static final String HTTPS_PORT = "https.port";
private static final String KEYSTORE_FILE = "https.keystore";
private static final String KEYSTORE_PASSWORD = "https.keystore.password";
private static final String WEBAPP_DIR = "webapp.dir";
private static final String ACCESS_LOG = "access.log";
private static final String REQUEST_CONTENT_SIZE_KEY = "request.content.size";
private static final int DEFAULT_REQUEST_CONTENT_SIZE = 1048576;
private static final String REQUEST_MAX_FORM_KEYS_KEY = "request.max.form.keys";
private static final int DEFAULT_REQUEST_MAX_FORM_KEYS = 5000;
private static final String THREADS_MIN = "threads.min";
private static final String THREADS_MAX = "threads.max";
////////////////////////////////////////////////////////
/////////////// Server Configuration ///////////////////
////////////////////////////////////////////////////////
private Server server;
private boolean httpEnable = true;
private int httpPort = 8080;
private int httpsPort = 8443;
private int sessionTimeout = 30;
private int maxFormContentSize = DEFAULT_REQUEST_CONTENT_SIZE;
private int maxFormKeys = DEFAULT_REQUEST_MAX_FORM_KEYS;
private boolean httpsEnable = false;
private String accessLogFile = "access.log";
private String bindInterface = null;
private String contextPath = "/client";
private String keystoreFile;
private String keystorePassword;
private String webAppLocation;
private int minThreads;
private int maxThreads;
//////////////////////////////////////////////////
/////////////// Public methods ///////////////////
//////////////////////////////////////////////////
public static void main(final String... anArgs) throws Exception {
final ServerDaemon daemon = new ServerDaemon();
daemon.init(null);
daemon.start();
}
@Override
public void init(final DaemonContext context) {
final File confFile = PropertiesUtil.findConfigFile("server.properties");
if (confFile == null) {
logger.warn(String.format("Server configuration file not found. Initializing server daemon on %s, with http.enable=%s, http.port=%s, https.enable=%s, https.port=%s, context.path=%s",
bindInterface, httpEnable, httpPort, httpsEnable, httpsPort, contextPath));
return;
}
logger.info("Server configuration file found: " + confFile.getAbsolutePath());
try {
InputStream is = new FileInputStream(confFile);
final Properties properties = ServerProperties.getServerProperties(is);
if (properties == null) {
return;
}
setBindInterface(properties.getProperty(BIND_INTERFACE, null));
setContextPath(properties.getProperty(CONTEXT_PATH, "/client"));
setHttpEnable(Boolean.valueOf(properties.getProperty(HTTP_ENABLE, "true")));
setHttpPort(Integer.valueOf(properties.getProperty(HTTP_PORT, "8080")));
setHttpsEnable(Boolean.valueOf(properties.getProperty(HTTPS_ENABLE, "false")));
setHttpsPort(Integer.valueOf(properties.getProperty(HTTPS_PORT, "8443")));
setKeystoreFile(properties.getProperty(KEYSTORE_FILE));
setKeystorePassword(properties.getProperty(KEYSTORE_PASSWORD));
setWebAppLocation(properties.getProperty(WEBAPP_DIR));
setAccessLogFile(properties.getProperty(ACCESS_LOG, "access.log"));
setSessionTimeout(Integer.valueOf(properties.getProperty(SESSION_TIMEOUT, "30")));
setMaxFormContentSize(Integer.valueOf(properties.getProperty(REQUEST_CONTENT_SIZE_KEY, String.valueOf(DEFAULT_REQUEST_CONTENT_SIZE))));
setMaxFormKeys(Integer.valueOf(properties.getProperty(REQUEST_MAX_FORM_KEYS_KEY, String.valueOf(DEFAULT_REQUEST_MAX_FORM_KEYS))));
setMinThreads(Integer.valueOf(properties.getProperty(THREADS_MIN, "10")));
setMaxThreads(Integer.valueOf(properties.getProperty(THREADS_MAX, "500")));
} catch (final IOException e) {
logger.warn("Failed to read configuration from server.properties file", e);
} finally {
// make sure that at least HTTP is enabled if both of them are set to false (misconfiguration)
if (!httpEnable && !httpsEnable) {
setHttpEnable(true);
logger.warn("Server configuration malformed, neither http nor https is enabled, http will be enabled.");
}
}
logger.info(String.format("Initializing server daemon on %s, with http.enable=%s, http.port=%s, https.enable=%s, https.port=%s, context.path=%s",
bindInterface, httpEnable, httpPort, httpsEnable, httpsPort, contextPath));
}
@Override
public void start() throws Exception {
// Thread pool
final QueuedThreadPool threadPool = new QueuedThreadPool();
threadPool.setMinThreads(minThreads);
threadPool.setMaxThreads(maxThreads);
// Jetty Server
server = new Server(threadPool);
// Setup Scheduler
server.addBean(new ScheduledExecutorScheduler());
// Setup JMX
final MBeanContainer mbeanContainer = new MBeanContainer(ManagementFactory.getPlatformMBeanServer());
server.addBean(mbeanContainer);
// HTTP config
final HttpConfiguration httpConfig = new HttpConfiguration();
// it would be nice to make this dynamic but we take care of this ourselves for now: httpConfig.addCustomizer( new ForwardedRequestCustomizer() );
httpConfig.setSecureScheme("https");
httpConfig.setSecurePort(httpsPort);
httpConfig.setOutputBufferSize(32768);
httpConfig.setRequestHeaderSize(8192);
httpConfig.setResponseHeaderSize(8192);
httpConfig.setSendServerVersion(false);
httpConfig.setSendDateHeader(false);
addForwardingCustomiser(httpConfig);
// HTTP Connector
createHttpConnector(httpConfig);
// Setup handlers
Pair<SessionHandler,HandlerCollection> pair = createHandlers();
server.setHandler(pair.second());
// Extra config options
server.setStopAtShutdown(true);
server.setAttribute(ContextHandler.MAX_FORM_CONTENT_SIZE_KEY, maxFormContentSize);
server.setAttribute(ContextHandler.MAX_FORM_KEYS_KEY, maxFormKeys);
// HTTPS Connector
createHttpsConnector(httpConfig);
server.start();
// Must set the session timeout after the server has started
pair.first().setMaxInactiveInterval(sessionTimeout * 60);
server.join();
}
/**
* Adds a ForwardedRequestCustomizer to the HTTP configuration to handle forwarded headers.
* The header used for forwarding is determined by the ApiServer.listOfForwardHeaders property.
* Only non empty headers are considered and only the first of the comma-separated list is used.
* @param httpConfig the HTTP configuration to which the customizer will be added
*/
private static void addForwardingCustomiser(HttpConfiguration httpConfig) {
ForwardedRequestCustomizer customiser = new ForwardedRequestCustomizer();
String header = Arrays.stream(ApiServer.listOfForwardHeaders.value().split(",")).findFirst().orElse(null);
if (com.cloud.utils.StringUtils.isNotEmpty(header)) {
customiser.setForwardedForHeader(header);
}
httpConfig.addCustomizer(customiser);
}
@Override
public void stop() throws Exception {
server.stop();
}
@Override
public void destroy() {
server.destroy();
}
///////////////////////////////////////////////////
/////////////// Private methods ///////////////////
///////////////////////////////////////////////////
private void createHttpConnector(final HttpConfiguration httpConfig) {
if (httpEnable) {
final ServerConnector httpConnector = new ServerConnector(server, new HttpConnectionFactory(httpConfig));
httpConnector.setPort(httpPort);
httpConnector.setHost(bindInterface);
httpConnector.setIdleTimeout(30000);
server.addConnector(httpConnector);
}
}
private void createHttpsConnector(final HttpConfiguration httpConfig) {
// Configure SSL
if (httpsEnable && StringUtils.isNotEmpty(keystoreFile) && new File(keystoreFile).exists()) {
// SSL Context
final SslContextFactory sslContextFactory = new SslContextFactory.Server();
// Define keystore path and passwords
sslContextFactory.setKeyStorePath(keystoreFile);
sslContextFactory.setKeyStorePassword(keystorePassword);
sslContextFactory.setKeyManagerPassword(keystorePassword);
// HTTPS config
final HttpConfiguration httpsConfig = new HttpConfiguration(httpConfig);
httpsConfig.addCustomizer(new SecureRequestCustomizer());
// HTTPS Connector
final ServerConnector sslConnector = new ServerConnector(server,
new SslConnectionFactory(sslContextFactory, "http/1.1"),
new HttpConnectionFactory(httpsConfig));
sslConnector.setPort(httpsPort);
sslConnector.setHost(bindInterface);
server.addConnector(sslConnector);
// add scanner to auto-reload certs
try {
KeyStoreScanner scanner = new KeyStoreScanner(sslContextFactory);
server.addBean(scanner);
} catch (Exception ex) {
logger.error("failed to set up keystore scanner, manual refresh of certificates will be required", ex);
}
}
}
private Pair<SessionHandler,HandlerCollection> createHandlers() {
final WebAppContext webApp = new WebAppContext();
webApp.setContextPath(contextPath);
webApp.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "false");
webApp.setMaxFormContentSize(maxFormContentSize);
webApp.setMaxFormKeys(maxFormKeys);
// GZIP handler
final GzipHandler gzipHandler = new GzipHandler();
gzipHandler.addIncludedMimeTypes("text/html", "text/xml", "text/css", "text/plain", "text/javascript", "application/javascript", "application/json", "application/xml");
gzipHandler.setIncludedMethods("GET", "POST");
gzipHandler.setCompressionLevel(9);
gzipHandler.setHandler(webApp);
if (StringUtils.isEmpty(webAppLocation)) {
webApp.setWar(getShadedWarUrl());
} else {
webApp.setWar(webAppLocation);
}
// Request log handler
final RequestLogHandler log = new RequestLogHandler();
log.setRequestLog(createRequestLog());
// Redirect root context handler_war
MovedContextHandler rootRedirect = new MovedContextHandler();
rootRedirect.setContextPath("/");
rootRedirect.setNewContextURL(contextPath);
rootRedirect.setPermanent(true);
// Put rootRedirect at the end!
return new Pair<>(webApp.getSessionHandler(), new HandlerCollection(log, gzipHandler, rootRedirect));
}
private RequestLog createRequestLog() {
final ACSRequestLog log = new ACSRequestLog();
final File logPath = new File(accessLogFile);
final File parentFile = logPath.getParentFile();
if (parentFile != null) {
parentFile.mkdirs();
}
log.setFilename(logPath.getPath());
log.setAppend(true);
log.setLogTimeZone("GMT");
log.setLogLatency(true);
return log;
}
private URL getResource(String aResource) {
return Thread.currentThread().getContextClassLoader().getResource(aResource);
}
private String getShadedWarUrl() {
final String urlStr = getResource(WEB_XML).toString();
return urlStr.substring(0, urlStr.length() - 15);
}
///////////////////////////////////////////
/////////////// Setters ///////////////////
///////////////////////////////////////////
public void setBindInterface(String bindInterface) {
this.bindInterface = bindInterface;
}
public void setHttpPort(int httpPort) {
this.httpPort = httpPort;
}
public void setHttpEnable(boolean httpEnable) {
this.httpEnable = httpEnable;
}
public void setHttpsPort(int httpsPort) {
this.httpsPort = httpsPort;
}
public void setContextPath(String contextPath) {
this.contextPath = contextPath;
}
public void setHttpsEnable(boolean httpsEnable) {
this.httpsEnable = httpsEnable;
}
public void setKeystoreFile(String keystoreFile) {
this.keystoreFile = keystoreFile;
}
public void setKeystorePassword(String keystorePassword) {
this.keystorePassword = keystorePassword;
}
public void setAccessLogFile(String accessLogFile) {
this.accessLogFile = accessLogFile;
}
public void setWebAppLocation(String webAppLocation) {
this.webAppLocation = webAppLocation;
}
public void setSessionTimeout(int sessionTimeout) {
this.sessionTimeout = sessionTimeout;
}
public void setMaxFormContentSize(int maxFormContentSize) {
this.maxFormContentSize = maxFormContentSize;
}
public void setMaxFormKeys(int maxFormKeys) {
this.maxFormKeys = maxFormKeys;
}
public void setMinThreads(int minThreads) {
this.minThreads = minThreads;
}
public void setMaxThreads(int maxThreads) {
this.maxThreads = maxThreads;
}
}