/*
HTTP stub server written in Java with embedded Jetty
Copyright (C) 2012 Alexander Zagniotov, Isa Goksu and Eric Mrak
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.github.azagniotov.stubby4j.server;
import io.github.azagniotov.stubby4j.cli.CommandLineInterpreter;
import io.github.azagniotov.stubby4j.handlers.AdminPortalHandler;
import io.github.azagniotov.stubby4j.handlers.AjaxEndpointStatsHandler;
import io.github.azagniotov.stubby4j.handlers.AjaxResourceContentHandler;
import io.github.azagniotov.stubby4j.handlers.FaviconHandler;
import io.github.azagniotov.stubby4j.handlers.JsonErrorHandler;
import io.github.azagniotov.stubby4j.handlers.StatusPageHandler;
import io.github.azagniotov.stubby4j.handlers.StubDataRefreshActionHandler;
import io.github.azagniotov.stubby4j.handlers.StubsPortalHandler;
import io.github.azagniotov.stubby4j.stubs.StubRepository;
import io.github.azagniotov.stubby4j.utils.ObjectUtils;
import io.github.azagniotov.stubby4j.utils.StringUtils;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler;
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.handler.AbstractHandler;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.ResourceHandler;
import org.eclipse.jetty.server.handler.gzip.GzipHandler;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
* @author Alexander Zagniotov
* @since 10/25/12, 5:17 PM
*/
@SuppressWarnings("serial")
public final class JettyFactory {
public static final int DEFAULT_ADMIN_PORT = 8889;
public static final int DEFAULT_STUBS_PORT = 8882;
public static final int DEFAULT_SSL_PORT = 7443;
public static final String DEFAULT_HOST = "localhost";
private static final int SERVER_CONNECTOR_IDLETIME_MILLIS = 45000;
private static final String PROTOCOL_HTTP_1_1 = "HTTP/1.1";
private static final String ADMIN_CONNECTOR_NAME = "AdminConnector";
private static final String STUBS_CONNECTOR_NAME = "StubsConnector";
private static final String SSL_CONNECTOR_NAME = "SslStubsConnector";
private static final String ROOT_PATH_INFO = "/";
private final Map<String, String> commandLineArgs;
private final StubRepository stubRepository;
private final List<String> statuses;
private String currentHost;
private int currentStubsPort;
private int currentAdminPort;
private int currentStubsSslPort;
JettyFactory(final Map<String, String> commandLineArgs, final StubRepository stubRepository) {
this.commandLineArgs = commandLineArgs;
this.stubRepository = stubRepository;
this.statuses = new LinkedList<>();
}
Server construct() throws IOException {
final Server server = new Server();
server.setDumpAfterStart(false);
server.setDumpBeforeStop(false);
server.setStopAtShutdown(true);
server.setConnectors(buildConnectors(server));
server.setHandler(constructHandlers());
return server;
}
private ContextHandlerCollection constructHandlers() {
final JettyContext jettyContext = new JettyContext(currentHost, currentStubsPort, currentStubsSslPort, currentAdminPort);
final ContextHandlerCollection handlers = new ContextHandlerCollection();
handlers.setHandlers(new Handler[]
{
constructHandler(STUBS_CONNECTOR_NAME, "/favicon.ico", gzipHandler(new FaviconHandler())),
constructHandler(STUBS_CONNECTOR_NAME, ROOT_PATH_INFO, gzipHandler(new StubsPortalHandler(stubRepository))),
constructHandler(SSL_CONNECTOR_NAME, "/favicon.ico", gzipHandler(new FaviconHandler())),
constructHandler(SSL_CONNECTOR_NAME, ROOT_PATH_INFO, gzipHandler(new StubsPortalHandler(stubRepository))),
constructHandler(ADMIN_CONNECTOR_NAME, "/status", gzipHandler(new StatusPageHandler(jettyContext, stubRepository))),
constructHandler(ADMIN_CONNECTOR_NAME, "/refresh", new StubDataRefreshActionHandler(jettyContext, stubRepository)),
constructHandler(ADMIN_CONNECTOR_NAME, "/js/highlight", gzipHandler(staticResourceHandler("ui/js/highlight/"))),
constructHandler(ADMIN_CONNECTOR_NAME, "/js/minified", gzipHandler(staticResourceHandler("ui/js/minified/"))),
constructHandler(ADMIN_CONNECTOR_NAME, "/js/d3", gzipHandler(staticResourceHandler("ui/js/d3/"))),
constructHandler(ADMIN_CONNECTOR_NAME, "/js", gzipHandler(staticResourceHandler("ui/js/"))),
constructHandler(ADMIN_CONNECTOR_NAME, "/css", gzipHandler(staticResourceHandler("ui/css/"))),
constructHandler(ADMIN_CONNECTOR_NAME, "/images", gzipHandler(staticResourceHandler("ui/images/"))),
constructHandler(ADMIN_CONNECTOR_NAME, "/ajax/resource", gzipHandler(new AjaxResourceContentHandler(stubRepository))),
constructHandler(ADMIN_CONNECTOR_NAME, "/ajax/stats", gzipHandler(new AjaxEndpointStatsHandler(stubRepository))),
constructHandler(ADMIN_CONNECTOR_NAME, "/favicon.ico", gzipHandler(new FaviconHandler())),
constructHandler(ADMIN_CONNECTOR_NAME, ROOT_PATH_INFO, gzipHandler(new AdminPortalHandler(stubRepository)))
}
);
return handlers;
}
private ResourceHandler staticResourceHandler(final String classPathResource) {
final ResourceHandler resourceHandler = new ResourceHandler();
resourceHandler.setDirectoriesListed(true);
resourceHandler.setBaseResource(Resource.newClassPathResource(classPathResource));
return resourceHandler;
}
private GzipHandler gzipHandler(final AbstractHandler abstractHandler) {
final GzipHandler gzipHandler = new GzipHandler();
gzipHandler.addIncludedMimeTypes(
"text/html,",
"text/plain,",
"text/xml,",
"application/xhtml,xml,",
"application/json,",
"text/css,",
"application/javascript,",
"application/x-javascript,",
"image/svg,xml,",
"image/x-icon,",
"image/gif,",
"image/jpg,",
"image/jpeg,",
"image/png");
gzipHandler.setHandler(abstractHandler);
return gzipHandler;
}
private ContextHandler constructHandler(final String connectorName, final String pathInfo, final Handler handler) {
final ContextHandler contextHandler = new ContextHandler();
contextHandler.setContextPath(pathInfo);
contextHandler.setAllowNullPathInfo(true);
// We prefix the name with an '@' because this is the way Jetty v9 finds a named connector
contextHandler.setVirtualHosts(new String[]{"@" + connectorName});
contextHandler.addLocaleEncoding(Locale.US.getDisplayName(), StringUtils.UTF_8);
contextHandler.setHandler(handler);
contextHandler.setErrorHandler(new JsonErrorHandler());
final MimeTypes mimeTypes = new MimeTypes();
mimeTypes.setMimeMap(new HashMap<>());
contextHandler.setMimeTypes(mimeTypes);
return contextHandler;
}
private Connector[] buildConnectors(final Server server) throws IOException {
final List<Connector> connectors = new ArrayList<>();
if (!commandLineArgs.containsKey(CommandLineInterpreter.OPTION_DISABLE_ADMIN)) {
connectors.add(buildAdminConnector(server));
}
connectors.add(buildStubsConnector(server));
if (!commandLineArgs.containsKey(CommandLineInterpreter.OPTION_DISABLE_SSL)) {
connectors.add(buildStubsSslConnector(server));
}
return connectors.toArray(new Connector[connectors.size()]);
}
private ServerConnector buildAdminConnector(final Server server) {
final HttpConfiguration httpConfiguration = constructHttpConfiguration();
final ServerConnector adminChannel = new ServerConnector(server, new HttpConnectionFactory(httpConfiguration));
adminChannel.setPort(getAdminPort(commandLineArgs));
adminChannel.setName(ADMIN_CONNECTOR_NAME);
adminChannel.setHost(DEFAULT_HOST);
adminChannel.setIdleTimeout(SERVER_CONNECTOR_IDLETIME_MILLIS);
if (commandLineArgs.containsKey(CommandLineInterpreter.OPTION_ADDRESS)) {
adminChannel.setHost(commandLineArgs.get(CommandLineInterpreter.OPTION_ADDRESS));
}
final String configured = String.format("Admin portal configured at http://%s:%s",
adminChannel.getHost(), adminChannel.getPort());
statuses.add(configured);
final String status = String.format("Admin portal status enabled at http://%s:%s/status",
adminChannel.getHost(), adminChannel.getPort());
statuses.add(status);
currentHost = adminChannel.getHost();
currentAdminPort = adminChannel.getPort();
return adminChannel;
}
private ServerConnector buildStubsConnector(final Server server) {
final HttpConfiguration httpConfiguration = constructHttpConfiguration();
final ServerConnector stubsChannel = new ServerConnector(server, new HttpConnectionFactory(httpConfiguration));
stubsChannel.setPort(getStubsPort(commandLineArgs));
stubsChannel.setName(STUBS_CONNECTOR_NAME);
stubsChannel.setHost(DEFAULT_HOST);
stubsChannel.setIdleTimeout(SERVER_CONNECTOR_IDLETIME_MILLIS);
if (commandLineArgs.containsKey(CommandLineInterpreter.OPTION_ADDRESS)) {
stubsChannel.setHost(commandLineArgs.get(CommandLineInterpreter.OPTION_ADDRESS));
}
final String status = String.format("Stubs portal configured at http://%s:%s",
stubsChannel.getHost(), stubsChannel.getPort());
statuses.add(status);
currentStubsPort = stubsChannel.getPort();
return stubsChannel;
}
private ServerConnector buildStubsSslConnector(final Server server) throws IOException {
String keystorePath = null;
String password = "password";
if (commandLineArgs.containsKey(CommandLineInterpreter.OPTION_KEYSTORE)
&& commandLineArgs.containsKey(CommandLineInterpreter.OPTION_KEYPASS)) {
password = commandLineArgs.get(CommandLineInterpreter.OPTION_KEYPASS);
keystorePath = commandLineArgs.get(CommandLineInterpreter.OPTION_KEYSTORE);
}
HttpConfiguration httpConfiguration = constructHttpConfiguration();
httpConfiguration.setSecureScheme(HttpScheme.HTTPS.asString());
httpConfiguration.setSecurePort(getStubsSslPort(commandLineArgs));
httpConfiguration.addCustomizer(new SecureRequestCustomizer());
final SslContextFactory sslContextFactory = constructSslContextFactory(password, keystorePath);
ServerConnector sslConnector = new ServerConnector(server,
new SslConnectionFactory(sslContextFactory, PROTOCOL_HTTP_1_1),
new HttpConnectionFactory(httpConfiguration));
sslConnector.setPort(getStubsSslPort(commandLineArgs));
sslConnector.setHost(DEFAULT_HOST);
sslConnector.setName(SSL_CONNECTOR_NAME);
sslConnector.setIdleTimeout(SERVER_CONNECTOR_IDLETIME_MILLIS);
if (commandLineArgs.containsKey(CommandLineInterpreter.OPTION_ADDRESS)) {
sslConnector.setHost(commandLineArgs.get(CommandLineInterpreter.OPTION_ADDRESS));
}
final String status = String.format("Stubs portal configured with TLS at https://%s:%s using %s keystore",
sslConnector.getHost(), sslConnector.getPort(), (ObjectUtils.isNull(keystorePath) ? "internal" : "provided " + keystorePath));
statuses.add(status);
currentStubsSslPort = sslConnector.getPort();
return sslConnector;
}
private SslContextFactory constructSslContextFactory(final String password, final String keystorePath) throws IOException {
final SslContextFactory sslFactory = new SslContextFactory();
sslFactory.setKeyStorePassword(password);
sslFactory.setKeyManagerPassword(password);
relaxSslTrustManager();
if (ObjectUtils.isNull(keystorePath)) {
final URL keyURL = this.getClass().getResource("/ssl/localhost.jks");
final Resource keyStoreResource = Resource.newResource(keyURL);
sslFactory.setKeyStoreResource(keyStoreResource);
return sslFactory;
}
sslFactory.setKeyStorePath(keystorePath);
return sslFactory;
}
private void relaxSslTrustManager() {
try {
new FakeX509TrustManager().allowAllSSL();
} catch (final Exception ex) {
throw new RuntimeException(ex.toString(), ex);
}
}
private HttpConfiguration constructHttpConfiguration() {
final HttpConfiguration httpConfiguration = new HttpConfiguration();
httpConfiguration.setSendServerVersion(true);
httpConfiguration.setSendXPoweredBy(true);
httpConfiguration.setOutputBufferSize(32768);
httpConfiguration.setRequestHeaderSize(8192);
httpConfiguration.setResponseHeaderSize(8192);
return httpConfiguration;
}
private int getStubsPort(final Map<String, String> commandLineArgs) {
if (commandLineArgs.containsKey(CommandLineInterpreter.OPTION_CLIENTPORT)) {
return Integer.parseInt(commandLineArgs.get(CommandLineInterpreter.OPTION_CLIENTPORT));
}
return DEFAULT_STUBS_PORT;
}
private int getStubsSslPort(final Map<String, String> commandLineArgs) {
if (commandLineArgs.containsKey(CommandLineInterpreter.OPTION_TLSPORT)) {
return Integer.parseInt(commandLineArgs.get(CommandLineInterpreter.OPTION_TLSPORT));
}
return DEFAULT_SSL_PORT;
}
private int getAdminPort(final Map<String, String> commandLineArgs) {
if (commandLineArgs.containsKey(CommandLineInterpreter.OPTION_ADMINPORT)) {
return Integer.parseInt(commandLineArgs.get(CommandLineInterpreter.OPTION_ADMINPORT));
}
return DEFAULT_ADMIN_PORT;
}
public List<String> statuses() {
return statuses;
}
}