/*
* Copyright (c) 2016 Network New Technologies Inc.
*
* 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 com.networknt.server;
import com.networknt.config.Config;
import com.networknt.handler.MiddlewareHandler;
import com.networknt.registry.Registry;
import com.networknt.registry.URL;
import com.networknt.registry.URLImpl;
import com.networknt.service.SingletonServiceFactory;
import com.networknt.switcher.SwitcherUtil;
import com.networknt.utility.Constants;
import com.networknt.utility.Util;
import io.undertow.Handlers;
import io.undertow.Undertow;
import io.undertow.UndertowOptions;
import io.undertow.server.HttpHandler;
import io.undertow.util.Headers;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.xnio.Options;
import javax.net.ssl.*;
import java.io.BufferedInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.nio.file.*;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.util.Enumeration;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public class Server {
static final Logger logger = LoggerFactory.getLogger(Server.class);
public static final String CONFIG_NAME = "server";
public static final String CONFIG_SECRET = "secret";
static final String DEFAULT_ENV = "dev";
static final String LIGHT_ENV = "light-env";
static final String LIGHT_CONFIG_SERVER_URI = "light-config-server-uri";
public static ServerConfig config = (ServerConfig) Config.getInstance().getJsonObjectConfig(CONFIG_NAME, ServerConfig.class);
public static Map<String, Object> secret = Config.getInstance().getJsonMapConfig(CONFIG_SECRET);
public final static TrustManager[] TRUST_ALL_CERTS = new X509TrustManager[] { new DummyTrustManager() };
static protected boolean shutdownRequested = false;
static Undertow server = null;
static URL serviceHttpUrl;
static URL serviceHttpsUrl;
static Registry registry;
static SSLContext sslContext;
public static void main(final String[] args) {
logger.info("server starts");
// setup system property to redirect undertow logs to slf4j/logback.
System.setProperty("org.jboss.logging.provider", "slf4j");
// load config files from light-config-server if possible.
loadConfig();
start();
}
static public void start() {
// add shutdown hook here.
addDaemonShutdownHook();
// add startup hooks here.
final ServiceLoader<StartupHookProvider> startupLoaders = ServiceLoader.load(StartupHookProvider.class);
for (final StartupHookProvider provider : startupLoaders) {
provider.onStartup();
}
// application level service registry. only be used without docker container.
if(config.enableRegistry) {
// assuming that registry is defined in service.json, otherwise won't start server.
registry = (Registry) SingletonServiceFactory.getBean(Registry.class);
if(registry == null) throw new RuntimeException("Could not find registry instance in service map");
InetAddress inetAddress = Util.getInetAddress();
String ipAddress = inetAddress.getHostAddress();
if(config.enableHttp) {
serviceHttpUrl = new URLImpl("light", ipAddress, config.getHttpPort(), config.getServiceId());
registry.register(serviceHttpUrl);
if(logger.isInfoEnabled()) logger.info("register serviceHttpUrl " + serviceHttpUrl);
}
if(config.enableHttps) {
serviceHttpsUrl = new URLImpl("light", ipAddress, config.getHttpsPort(), config.getServiceId());
registry.register(serviceHttpsUrl);
if(logger.isInfoEnabled()) logger.info("register serviceHttpsUrl " + serviceHttpsUrl);
}
}
HttpHandler handler = null;
// API routing handler or others handler implemented by application developer.
final ServiceLoader<HandlerProvider> handlerLoaders = ServiceLoader.load(HandlerProvider.class);
for (final HandlerProvider provider : handlerLoaders) {
if (provider.getHandler() != null) {
handler = provider.getHandler();
break;
}
}
if (handler == null) {
logger.error("Unable to start the server - no route handler provider available in the classpath");
return;
}
// Middleware Handlers plugged into the handler chain.
final ServiceLoader<MiddlewareHandler> middlewareLoaders = ServiceLoader.load(MiddlewareHandler.class);
logger.debug("found middlewareLoaders", middlewareLoaders);
for (final MiddlewareHandler middlewareHandler : middlewareLoaders) {
logger.info("Plugin: " + middlewareHandler.getClass().getName());
if(middlewareHandler.isEnabled()) {
handler = middlewareHandler.setNext(handler);
middlewareHandler.register();
}
}
Undertow.Builder builder = Undertow.builder();
if(config.enableHttp2) {
sslContext = createSSLContext();
builder.addHttpsListener(config.getHttpsPort(), config.getIp(), sslContext);
builder.setServerOption(UndertowOptions.ENABLE_HTTP2, true);
} else {
if(config.enableHttp) {
builder.addHttpListener(config.getHttpPort(), config.getIp());
}
if(config.enableHttps) {
sslContext = createSSLContext();
builder.addHttpsListener(config.getHttpsPort(), config.getIp(), sslContext);
}
}
server = builder
.setBufferSize(1024 * 16)
.setIoThreads(Runtime.getRuntime().availableProcessors() * 2) //this seems slightly faster in some configurations
.setSocketOption(Options.BACKLOG, 10000)
.setServerOption(UndertowOptions.ALWAYS_SET_KEEP_ALIVE, false) //don't send a keep-alive header for HTTP/1.1 requests, as it is not required
.setServerOption(UndertowOptions.ALWAYS_SET_DATE, true)
.setServerOption(UndertowOptions.RECORD_REQUEST_START_TIME, false)
.setHandler(Handlers.header(handler, Headers.SERVER_STRING, "L"))
.setWorkerThreads(200)
.build();
server.start();
if(logger.isInfoEnabled()) {
if(config.enableHttp) {
logger.info("Http Server started on ip:" + config.getIp() + " Port:" + config.getHttpPort());
}
if(config.enableHttps) {
logger.info("Https Server started on ip:" + config.getIp() + " Port:" + config.getHttpsPort());
}
}
if(config.enableRegistry) {
// start heart beat if registry is enabled
SwitcherUtil.setSwitcherValue(Constants.REGISTRY_HEARTBEAT_SWITCHER, true);
if(logger.isInfoEnabled()) logger.info("Registry heart beat switcher is on");
}
}
static public void stop() {
if (server != null) server.stop();
}
// implement shutdown hook here.
static public void shutdown() {
// need to unregister the service
if(config.enableRegistry && registry != null && config.enableHttp) {
registry.unregister(serviceHttpUrl);
if(logger.isInfoEnabled()) logger.info("unregister serviceHttpUrl " + serviceHttpUrl);
}
if(config.enableRegistry && registry != null && config.enableHttps) {
registry.unregister(serviceHttpsUrl);
if(logger.isInfoEnabled()) logger.info("unregister serviceHttpsUrl " + serviceHttpsUrl);
}
final ServiceLoader<ShutdownHookProvider> shutdownLoaders = ServiceLoader.load(ShutdownHookProvider.class);
for (final ShutdownHookProvider provider : shutdownLoaders) {
provider.onShutdown();
}
stop();
logger.info("Cleaning up before server shutdown");
}
static protected void addDaemonShutdownHook() {
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
Server.shutdown();
}
});
}
private static KeyStore loadKeyStore() {
String name = config.getKeystoreName();
try (InputStream stream = Config.getInstance().getInputStreamFromFile(name)) {
KeyStore loadedKeystore = KeyStore.getInstance("JKS");
loadedKeystore.load(stream, ((String)secret.get("keystorePass")).toCharArray());
return loadedKeystore;
} catch (Exception e) {
logger.error("Unable to load keystore " + name, e);
throw new RuntimeException("Unable to load keystore " + name, e);
}
}
protected static KeyStore loadTrustStore() {
String name = config.getTruststoreName();
try (InputStream stream = Config.getInstance().getInputStreamFromFile(name)) {
KeyStore loadedKeystore = KeyStore.getInstance("JKS");
loadedKeystore.load(stream, ((String)secret.get("truststorePass")).toCharArray());
return loadedKeystore;
} catch (Exception e) {
logger.error("Unable to load truststore " + name, e);
throw new RuntimeException("Unable to load truststore " + name, e);
}
}
private static TrustManager[] buildTrustManagers(final KeyStore trustStore) {
TrustManager[] trustManagers = null;
if (trustStore == null) {
try {
TrustManagerFactory trustManagerFactory = TrustManagerFactory
.getInstance(KeyManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(trustStore);
trustManagers = trustManagerFactory.getTrustManagers();
}
catch (NoSuchAlgorithmException | KeyStoreException e) {
logger.error("Unable to initialise TrustManager[]", e);
throw new RuntimeException("Unable to initialise TrustManager[]", e);
}
}
else {
trustManagers = TRUST_ALL_CERTS;
}
return trustManagers;
}
private static KeyManager[] buildKeyManagers(final KeyStore keyStore, char[] keyPass) {
KeyManager[] keyManagers;
try {
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory
.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, keyPass);
keyManagers = keyManagerFactory.getKeyManagers();
}
catch (NoSuchAlgorithmException | UnrecoverableKeyException | KeyStoreException e) {
logger.error("Unable to initialise KeyManager[]", e);
throw new RuntimeException("Unable to initialise KeyManager[]", e);
}
return keyManagers;
}
private static SSLContext createSSLContext() throws RuntimeException {
try {
KeyManager[] keyManagers = buildKeyManagers(loadKeyStore(), ((String)secret.get("keyPass")).toCharArray());
TrustManager[] trustManagers;
if(config.isEnableTwoWayTls()) {
trustManagers = buildTrustManagers(loadTrustStore());
} else {
trustManagers = buildTrustManagers(null);
}
SSLContext sslContext;
sslContext = SSLContext.getInstance("TLSv1");
sslContext.init(keyManagers, trustManagers, null);
return sslContext;
} catch (Exception e) {
logger.error("Unable to create SSLContext", e);
throw new RuntimeException("Unable to create SSLContext", e);
}
}
private static void loadConfig() {
// if it is necessary to load config files from config server
// Here we expect at least env(dev/sit/uat/prod) and optional config server url
String env = System.getProperty(LIGHT_ENV);
if(env == null) {
logger.warn("Warning! No light-env has been passed in from command line. Default to dev");
env = DEFAULT_ENV;
}
String configUri = System.getProperty(LIGHT_CONFIG_SERVER_URI);
if(configUri != null) {
// try to get config files from the server.
String targetMergeDirectory = System.getProperty(Config.LIGHT_4J_CONFIG_DIR);
if(targetMergeDirectory == null) {
logger.warn("Warning! No light-4j-config-dir has been passed in from command line.");
return;
}
String version = Util.getJarVersion();
String service = config.getServiceId();
configUri = configUri + "/v1/config/" + version + "/" + env + "/" + service;
String tempDir = System.getProperty("java.io.tmpdir");
String zipFile = tempDir + "/config.zip";
// /v1/config/1.2.4/dev/com.networknt.petstore-1.0.0
HttpClient httpClient = HttpClientBuilder.create().build();
HttpGet httpGet = new HttpGet(configUri);
try {
HttpResponse response = httpClient.execute(httpGet);
HttpEntity entity = response.getEntity();
if (entity != null) {
FileOutputStream fos = new FileOutputStream(zipFile);
entity.writeTo(fos);
fos.close();
}
// unzip config.zip and merge files to externalized config folder.
unzipFile(zipFile, targetMergeDirectory);
} catch (IOException e) {
logger.error("IOException", e);
}
} else {
logger.info("light-config-server-uri is missing in the command line. Use local config files");
}
}
private static void mergeConfigFiles(String source, String target) {
}
private static void unzipFile(String path, String target) {
//Open the file
try(ZipFile file = new ZipFile(path))
{
FileSystem fileSystem = FileSystems.getDefault();
//Get file entries
Enumeration<? extends ZipEntry> entries = file.entries();
//We will unzip files in this folder
Files.createDirectory(fileSystem.getPath(target));
//Iterate over entries
while (entries.hasMoreElements())
{
ZipEntry entry = entries.nextElement();
//If directory then create a new directory in uncompressed folder
if (entry.isDirectory())
{
System.out.println("Creating Directory:" + target + entry.getName());
Files.createDirectories(fileSystem.getPath(target + entry.getName()));
}
//Else create the file
else
{
InputStream is = file.getInputStream(entry);
BufferedInputStream bis = new BufferedInputStream(is);
String uncompressedFileName = target + entry.getName();
Path uncompressedFilePath = fileSystem.getPath(uncompressedFileName);
Files.createFile(uncompressedFilePath);
FileOutputStream fileOutput = new FileOutputStream(uncompressedFileName);
while (bis.available() > 0)
{
fileOutput.write(bis.read());
}
fileOutput.close();
System.out.println("Written :" + entry.getName());
}
}
}
catch(IOException e) {
logger.error("IOException", e);
}
}
}