/**
* Licensed to JumpMind Inc under one or more contributor
* license agreements. See the NOTICE file distributed
* with this work for additional information regarding
* copyright ownership. JumpMind Inc licenses this file
* to you under the GNU General Public License, version 3.0 (GPLv3)
* (the "License"); you may not use this file except in compliance
* with the License.
*
* You should have received a copy of the GNU General Public License,
* version 3.0 (GPLv3) along with this library; if not, see
* <http://www.gnu.org/licenses/>.
*
* 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.jumpmind.symmetric;
import static org.apache.commons.lang.StringUtils.isNotBlank;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.Enumeration;
import javax.management.Attribute;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import mx4j.tools.adaptor.http.HttpAdaptor;
import mx4j.tools.adaptor.http.XSLTProcessor;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.security.ConstraintMapping;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.security.HashLoginService;
import org.eclipse.jetty.security.authentication.BasicAuthenticator;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
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.session.AbstractSession;
import org.eclipse.jetty.server.session.HashSessionManager;
import org.eclipse.jetty.server.session.HashedSession;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.security.Password;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.webapp.WebAppContext;
import org.jumpmind.properties.TypedProperties;
import org.jumpmind.security.ISecurityService;
import org.jumpmind.security.SecurityConstants;
import org.jumpmind.security.SecurityServiceFactory;
import org.jumpmind.security.SecurityServiceFactory.SecurityServiceType;
import org.jumpmind.symmetric.common.ServerConstants;
import org.jumpmind.symmetric.common.SystemConstants;
import org.jumpmind.symmetric.transport.TransportManagerFactory;
import org.jumpmind.symmetric.web.ServletUtils;
import org.jumpmind.symmetric.web.SymmetricEngineHolder;
import org.jumpmind.symmetric.web.WebConstants;
import org.jumpmind.symmetric.web.rest.RestService;
import org.jumpmind.util.AppUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
/**
* Start up SymmetricDS through an embedded Jetty instance.
*
* @see SymmetricLauncher#main(String[])
*/
public class SymmetricWebServer {
protected static final Logger log = LoggerFactory.getLogger(SymmetricWebServer.class);
protected static final String DEFAULT_WEBAPP_DIR = System.getProperty(SystemConstants.SYSPROP_WEB_DIR, AppUtils.getSymHome() + "/web");
public static final String DEFAULT_HTTP_PORT = System.getProperty(SystemConstants.SYSPROP_DEFAULT_HTTP_PORT, "31415");
public static final String DEFAULT_JMX_PORT = System.getProperty(SystemConstants.SYSPROP_DEFAULT_JMX_PORT, "31416");
public static final String DEFAULT_HTTPS_PORT = System.getProperty(SystemConstants.SYSPROP_DEFAULT_HTTPS_PORT, "31417");
public static final int DEFAULT_MAX_IDLE_TIME = 7200000;
/**
* The type of HTTP connection to create for this SymmetricDS web server
*/
public enum Mode {
HTTP, HTTPS, MIXED;
}
private Server server;
private WebAppContext webapp;
protected boolean join = true;
protected String webHome = "/";
protected int maxIdleTime = DEFAULT_MAX_IDLE_TIME;
protected boolean httpEnabled = true;
protected int httpPort = Integer.parseInt(DEFAULT_HTTP_PORT);
protected boolean httpsEnabled = false;
protected int httpsPort = -1;
protected boolean jmxEnabled = true;
protected int jmxPort = Integer.parseInt(DEFAULT_JMX_PORT);
protected String basicAuthUsername = null;
protected String basicAuthPassword = null;
protected String propertiesFile = null;
protected String host = null;
protected boolean noNio = false;
protected boolean noDirectBuffer = false;
protected String webAppDir = DEFAULT_WEBAPP_DIR;
protected String name = "SymmetricDS";
protected String httpSslVerifiedServerNames = "all";
protected boolean allowSelfSignedCerts = true;
public SymmetricWebServer() {
this(null, DEFAULT_WEBAPP_DIR);
}
public SymmetricWebServer(String propertiesUrl) {
this(propertiesUrl, DEFAULT_WEBAPP_DIR);
}
public SymmetricWebServer(int maxIdleTime, String propertiesUrl) {
this(propertiesUrl, DEFAULT_WEBAPP_DIR);
this.maxIdleTime = maxIdleTime;
}
public SymmetricWebServer(String webDirectory, int maxIdleTime, String propertiesUrl, boolean join, boolean noNio, boolean noDirectBuffer) {
this(propertiesUrl, webDirectory);
this.maxIdleTime = maxIdleTime;
this.join = join;
this.noDirectBuffer = noDirectBuffer;
this.noNio = noNio;
}
public SymmetricWebServer(String propertiesUrl, String webappDir) {
this.propertiesFile = propertiesUrl;
this.webAppDir = webappDir;
initFromProperties();
}
protected void initFromProperties() {
try {
Class.forName(AbstractCommandLauncher.class.getName());
} catch (ClassNotFoundException e) {
}
TypedProperties serverProperties = new TypedProperties(System.getProperties());
httpEnabled = serverProperties.is(ServerConstants.HTTP_ENABLE,
Boolean.parseBoolean(System.getProperty(ServerConstants.HTTP_ENABLE, "true")));
httpsEnabled = serverProperties.is(ServerConstants.HTTPS_ENABLE,
Boolean.parseBoolean(System.getProperty(ServerConstants.HTTPS_ENABLE, "true")));
jmxEnabled = serverProperties.is(ServerConstants.JMX_HTTP_ENABLE,
Boolean.parseBoolean(System.getProperty(ServerConstants.JMX_HTTP_ENABLE, "true")));
httpPort = serverProperties.getInt(ServerConstants.HTTP_PORT,
Integer.parseInt(System.getProperty(ServerConstants.HTTP_PORT, "" + httpPort)));
httpsPort = serverProperties.getInt(ServerConstants.HTTPS_PORT,
Integer.parseInt(System.getProperty(ServerConstants.HTTPS_PORT, "" + httpsPort)));
jmxPort = serverProperties.getInt(ServerConstants.JMX_HTTP_PORT,
Integer.parseInt(System.getProperty(ServerConstants.JMX_HTTP_PORT, "" + jmxPort)));
host = serverProperties.get(ServerConstants.HOST_BIND_NAME, System.getProperty(ServerConstants.HOST_BIND_NAME, host));
httpSslVerifiedServerNames = serverProperties.get(ServerConstants.HTTPS_VERIFIED_SERVERS,
System.getProperty(ServerConstants.HTTPS_VERIFIED_SERVERS, httpSslVerifiedServerNames));
allowSelfSignedCerts = serverProperties.is(ServerConstants.HTTPS_ALLOW_SELF_SIGNED_CERTS,
Boolean.parseBoolean(System.getProperty(ServerConstants.HTTPS_ALLOW_SELF_SIGNED_CERTS, "" + allowSelfSignedCerts)));
}
public SymmetricWebServer start(int httpPort, int jmxPort, String propertiesUrl) throws Exception {
this.propertiesFile = propertiesUrl;
return start(httpPort, jmxPort);
}
public SymmetricWebServer start() throws Exception {
if (httpPort > 0 && httpsPort > 0 && httpEnabled && httpsEnabled) {
return startMixed(httpPort, httpsPort, jmxPort);
} else if (httpPort > 0 && httpEnabled) {
return start(httpPort, jmxPort);
} else if (httpsPort > 0 && httpsEnabled) {
return startSecure(httpsPort, jmxPort);
} else {
throw new IllegalStateException("Either an http or https port needs to be set before starting the server.");
}
}
public SymmetricWebServer start(int httpPort) throws Exception {
return start(httpPort, 0, httpPort + 1, Mode.HTTP);
}
public SymmetricWebServer start(int httpPort, int jmxPort) throws Exception {
return start(httpPort, 0, jmxPort, Mode.HTTP);
}
public SymmetricWebServer startSecure(int httpsPort, int jmxPort) throws Exception {
return start(0, httpsPort, jmxPort, Mode.HTTPS);
}
public SymmetricWebServer startMixed(int httpPort, int secureHttpPort, int jmxPort) throws Exception {
return start(httpPort, secureHttpPort, jmxPort, Mode.MIXED);
}
public SymmetricWebServer start(int httpPort, int securePort, int httpJmxPort, Mode mode) throws Exception {
TransportManagerFactory.initHttps(httpSslVerifiedServerNames, allowSelfSignedCerts);
// indicate to the app that we are in stand alone mode
System.setProperty(SystemConstants.SYSPROP_STANDALONE_WEB, "true");
server = new Server();
server.setConnectors(getConnectors(server, httpPort, securePort, mode));
setupBasicAuthIfNeeded(server);
webapp = new WebAppContext();
webapp.setParentLoaderPriority(true);
webapp.setConfigurationDiscovered(true);
webapp.setContextPath(webHome);
webapp.setWar(webAppDir);
webapp.setResourceBase(webAppDir);
// webapp.addServlet(DefaultServlet.class, "/*");
SessionManager sm = new SessionManager();
webapp.getSessionHandler().setSessionManager(sm);
webapp.getServletContext().getContextHandler()
.setMaxFormContentSize(Integer.parseInt(System.getProperty("org.eclipse.jetty.server.Request.maxFormContentSize", "800000")));
webapp.getServletContext().getContextHandler()
.setMaxFormKeys(Integer.parseInt(System.getProperty("org.eclipse.jetty.server.Request.maxFormKeys", "100000")));
if (propertiesFile != null) {
webapp.getServletContext().getContextHandler().setInitParameter(WebConstants.INIT_SINGLE_SERVER_PROPERTIES_FILE, propertiesFile);
webapp.getServletContext().getContextHandler()
.setInitParameter(WebConstants.INIT_PARAM_MULTI_SERVER_MODE, Boolean.toString(false));
} else {
webapp.getServletContext().getContextHandler()
.setInitParameter(WebConstants.INIT_PARAM_MULTI_SERVER_MODE, Boolean.toString(true));
}
server.setHandler(webapp);
server.start();
if (httpJmxPort > 0) {
registerHttpJmxAdaptor(httpJmxPort);
}
if (join) {
log.info("Joining the web server main thread");
server.join();
}
return this;
}
protected ServletContext getServletContext() {
return webapp != null ? webapp.getServletContext() : null;
}
public RestService getRestService() {
ServletContext servletContext = getServletContext();
WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(servletContext);
return rootContext.getBean(RestService.class);
}
public ISymmetricEngine getEngine() {
ISymmetricEngine engine = null;
ServletContext servletContext = getServletContext();
if (servletContext != null) {
SymmetricEngineHolder engineHolder = ServletUtils.getSymmetricEngineHolder(servletContext);
if (engineHolder != null) {
if (engineHolder.getEngines().size() == 1) {
return engineHolder.getEngines().values().iterator().next();
} else {
throw new IllegalStateException("Could not choose a single engine to return. There are "
+ engineHolder.getEngines().size() + " engines configured.");
}
}
}
return engine;
}
public void waitForEnginesToComeOnline(long maxWaitTimeInMs) throws InterruptedException {
long startTime = System.currentTimeMillis();
ServletContext servletContext = getServletContext();
if (servletContext != null) {
SymmetricEngineHolder engineHolder = ServletUtils.getSymmetricEngineHolder(servletContext);
while (engineHolder.areEnginesStarting()) {
AppUtils.sleep(500);
if ((System.currentTimeMillis() - startTime) > maxWaitTimeInMs) {
throw new InterruptedException("Timed out waiting for engines to start");
}
}
}
}
protected void setupBasicAuthIfNeeded(Server server) {
if (StringUtils.isNotBlank(basicAuthUsername)) {
ConstraintSecurityHandler sh = new ConstraintSecurityHandler();
Constraint constraint = new Constraint();
constraint.setName(Constraint.__BASIC_AUTH);
constraint.setRoles(new String[] { SecurityConstants.EMBEDDED_WEBSERVER_DEFAULT_ROLE });
constraint.setAuthenticate(true);
ConstraintMapping cm = new ConstraintMapping();
cm.setConstraint(constraint);
cm.setPathSpec("/*");
// sh.setConstraintMappings(new ConstraintMapping[] {cm});
sh.addConstraintMapping(cm);
sh.setAuthenticator(new BasicAuthenticator());
HashLoginService loginService = new HashLoginService();
loginService.putUser(basicAuthUsername, new Password(basicAuthPassword), null);
sh.setLoginService(loginService);
server.setHandler(sh);
}
}
protected Connector[] getConnectors(Server server, int port, int securePort, Mode mode) {
ArrayList<Connector> connectors = new ArrayList<Connector>();
String keyStoreFile = System.getProperty(SecurityConstants.SYSPROP_KEYSTORE);
String keyStoreType = System.getProperty(SecurityConstants.SYSPROP_KEYSTORE_TYPE, SecurityConstants.KEYSTORE_TYPE);
HttpConfiguration httpConfig = new HttpConfiguration();
if (mode.equals(Mode.HTTPS) || mode.equals(Mode.MIXED)) {
httpConfig.setSecureScheme("https");
httpConfig.setSecurePort(securePort);
}
httpConfig.setOutputBufferSize(32768);
if (mode.equals(Mode.HTTP) || mode.equals(Mode.MIXED)) {
ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(httpConfig));
http.setPort(port);
http.setHost(host);
http.setIdleTimeout(maxIdleTime);
connectors.add(http);
log.info(String.format("About to start %s web server on host:port %s:%s", name, host == null ? "default" : host, port));
}
if (mode.equals(Mode.HTTPS) || mode.equals(Mode.MIXED)) {
ISecurityService securityService = SecurityServiceFactory.create(SecurityServiceType.SERVER,
new TypedProperties(System.getProperties()));
securityService.installDefaultSslCert(host);
String keyStorePassword = System.getProperty(SecurityConstants.SYSPROP_KEYSTORE_PASSWORD);
keyStorePassword = (keyStorePassword != null) ? keyStorePassword : SecurityConstants.KEYSTORE_PASSWORD;
SslContextFactory sslConnectorFactory = new SslContextFactory();
sslConnectorFactory.setKeyStorePath(keyStoreFile);
sslConnectorFactory.setKeyManagerPassword(keyStorePassword);
/* Prevent POODLE attack */
sslConnectorFactory.addExcludeProtocols("SSLv3");
sslConnectorFactory.setCertAlias(System.getProperty(SecurityConstants.SYSPROP_KEYSTORE_CERT_ALIAS,
SecurityConstants.ALIAS_SYM_PRIVATE_KEY));
sslConnectorFactory.setKeyStoreType(keyStoreType);
HttpConfiguration httpsConfig = new HttpConfiguration(httpConfig);
httpsConfig.addCustomizer(new SecureRequestCustomizer());
ServerConnector https = new ServerConnector(server,
new SslConnectionFactory(sslConnectorFactory, HttpVersion.HTTP_1_1.asString()), new HttpConnectionFactory(httpsConfig));
https.setPort(securePort);
https.setIdleTimeout(maxIdleTime);
https.setHost(host);
connectors.add(https);
log.info(String.format("About to start %s web server on secure host:port %s:%s", name, host == null ? "default" : host,
securePort));
}
return connectors.toArray(new Connector[connectors.size()]);
}
protected void registerHttpJmxAdaptor(int jmxPort) throws Exception {
if (AppUtils.isSystemPropertySet(SystemConstants.SYSPROP_JMX_HTTP_CONSOLE_ENABLED, true) && jmxEnabled) {
log.info("Starting JMX HTTP console on port {}", jmxPort);
MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
ObjectName name = getHttpJmxAdaptorName();
mbeanServer.createMBean(HttpAdaptor.class.getName(), name);
if (!AppUtils.isSystemPropertySet(SystemConstants.SYSPROP_JMX_HTTP_CONSOLE_LOCALHOST_ENABLED, true)) {
mbeanServer.setAttribute(name, new Attribute("Host", "0.0.0.0"));
} else if (StringUtils.isNotBlank(host)) {
mbeanServer.setAttribute(name, new Attribute("Host", host));
}
mbeanServer.setAttribute(name, new Attribute("Port", new Integer(jmxPort)));
ObjectName processorName = getXslJmxAdaptorName();
mbeanServer.createMBean(XSLTProcessor.class.getName(), processorName);
mbeanServer.setAttribute(name, new Attribute("ProcessorName", processorName));
mbeanServer.invoke(name, "start", null, null);
}
}
protected ObjectName getHttpJmxAdaptorName() throws MalformedObjectNameException {
return new ObjectName("Server:name=HttpAdaptor");
}
protected ObjectName getXslJmxAdaptorName() throws MalformedObjectNameException {
return new ObjectName("Server:name=XSLTProcessor");
}
protected void removeHttpJmxAdaptor() {
if (AppUtils.isSystemPropertySet(SystemConstants.SYSPROP_JMX_HTTP_CONSOLE_ENABLED, true) && jmxEnabled) {
try {
MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
mbeanServer.unregisterMBean(getHttpJmxAdaptorName());
mbeanServer.unregisterMBean(getXslJmxAdaptorName());
} catch (Exception e) {
log.warn("Could not unregister the JMX HTTP Adaptor");
}
}
}
public void stop() throws Exception {
if (server != null) {
removeHttpJmxAdaptor();
server.stop();
}
}
public static void main(String[] args) throws Exception {
new SymmetricWebServer().start(8080, 8081);
}
public boolean isJoin() {
return join;
}
public void setJoin(boolean join) {
this.join = join;
}
public void setWebHome(String webHome) {
this.webHome = webHome;
}
public int getMaxIdleTime() {
return maxIdleTime;
}
public void setMaxIdleTime(int maxIdleTime) {
this.maxIdleTime = maxIdleTime;
}
public void setHttpPort(int httpPort) {
System.setProperty(ServerConstants.HTTP_PORT, Integer.toString(httpPort));
this.httpPort = httpPort;
}
public int getHttpPort() {
return httpPort;
}
public void setHttpsPort(int httpsPort) {
System.setProperty(ServerConstants.HTTPS_PORT, Integer.toString(httpsPort));
this.httpsPort = httpsPort;
}
public int getHttpsPort() {
return httpsPort;
}
public void setPropertiesFile(String propertiesFile) {
this.propertiesFile = propertiesFile;
}
public void setHost(String host) {
this.host = host;
}
public void setBasicAuthPassword(String basicAuthPassword) {
this.basicAuthPassword = basicAuthPassword;
}
public void setBasicAuthUsername(String basicAuthUsername) {
this.basicAuthUsername = basicAuthUsername;
}
public void setWebAppDir(String webAppDir) {
this.webAppDir = webAppDir;
}
public void setNoNio(boolean noNio) {
this.noNio = noNio;
}
public boolean isNoNio() {
return noNio;
}
public void setNoDirectBuffer(boolean noDirectBuffer) {
this.noDirectBuffer = noDirectBuffer;
}
public boolean isNoDirectBuffer() {
return noDirectBuffer;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public int getJmxPort() {
return jmxPort;
}
public void setJmxPort(int jmxPort) {
this.jmxPort = jmxPort;
}
public void setHttpEnabled(boolean httpEnabled) {
this.httpEnabled = httpEnabled;
}
public boolean isHttpEnabled() {
return httpEnabled;
}
public void setHttpsEnabled(boolean httpsEnabled) {
this.httpsEnabled = httpsEnabled;
}
public boolean isHttpsEnabled() {
return httpsEnabled;
}
public void setJmxEnabled(boolean jmxEnabled) {
this.jmxEnabled = jmxEnabled;
}
public boolean isJmxEnabled() {
return jmxEnabled;
}
class SessionManager extends HashSessionManager {
public SessionManager() {
setMaxInactiveInterval(10 * 60);
setLazyLoad(true);
setDeleteUnrestorableSessions(true);
setSessionCookie(getSessionCookie() + (httpPort > 0 ? httpPort
: httpsPort));
}
@Override
protected AbstractSession newSession(HttpServletRequest request) {
return new Session(this, request);
}
@Override
protected AbstractSession newSession(long created, long accessed, String clusterId) {
return new Session(this, created, accessed, clusterId);
}
@Override
protected synchronized HashedSession restoreSession(String idInCuster) {
if (isNotBlank(idInCuster)) {
return super.restoreSession(idInCuster);
} else {
return null;
}
}
public HashedSession restoreSession(InputStream is, HashedSession session) throws Exception {
DataInputStream di = new DataInputStream(is);
String clusterId = di.readUTF();
di.readUTF(); // nodeId
long created = di.readLong();
long accessed = di.readLong();
int requests = di.readInt();
if (session == null)
session = (HashedSession) newSession(created, accessed, clusterId);
session.setRequests(requests);
int size = di.readInt();
restoreSessionAttributes(di, size, session);
try {
int maxIdle = di.readInt();
session.setMaxInactiveInterval(maxIdle);
} catch (EOFException e) {
log.debug("No maxInactiveInterval persisted for session " + clusterId, e);
}
return session;
}
private void restoreSessionAttributes(InputStream is, int size, HashedSession session) throws Exception {
if (size > 0) {
ObjectInputStream ois = new ObjectInputStream(is);
for (int i = 0; i < size; i++) {
String key = ois.readUTF();
try {
Object value = ois.readObject();
session.setAttribute(key, value);
} catch (Exception ex) {
if (ex instanceof ClassCastException || ex instanceof ClassNotFoundException) {
log.warn("Could not restore the '" + key
+ "' session object. Code has probably changed. The error message was: " + ex.getMessage());
} else {
log.error("Could not restore the '" + key + "' session object.", ex);
}
}
}
}
}
}
class Session extends HashedSession {
protected Session(HashSessionManager hashSessionManager, HttpServletRequest request) {
super(hashSessionManager, request);
}
protected Session(HashSessionManager hashSessionManager, long created, long accessed, String clusterId) {
super(hashSessionManager, created, accessed, clusterId);
}
@Override
public synchronized void save(OutputStream os) throws IOException {
DataOutputStream out = new DataOutputStream(os);
out.writeUTF(getClusterId());
out.writeUTF(getNodeId());
out.writeLong(getCreationTime());
out.writeLong(getAccessed());
out.writeInt(getRequests());
Enumeration<String> e = getAttributeNames();
int count = 0;
while (e.hasMoreElements()) {
String key = e.nextElement();
Object obj = doGet(key);
if (obj instanceof Serializable) {
count++;
}
}
out.writeInt(count);
ObjectOutputStream oos = new ObjectOutputStream(out);
e = getAttributeNames();
while (e.hasMoreElements()) {
String key = e.nextElement();
Object obj = doGet(key);
if (obj instanceof Serializable) {
oos.writeUTF(key);
oos.writeObject(obj);
}
}
oos.flush();
out.writeInt(getMaxInactiveInterval());
}
}
}