/**
* 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.jooby.internal.jetty;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.net.ssl.SSLContext;
import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory;
import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
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.ContextHandler;
import org.eclipse.jetty.util.DecoratedObjectFactory;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.websocket.api.WebSocketBehavior;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.server.WebSocketServerFactory;
import org.jooby.spi.HttpHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Throwables;
import com.google.common.primitives.Primitives;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigException;
import javaslang.control.Try;
public class JettyServer implements org.jooby.spi.Server {
private static final String H2 = "h2";
private static final String H2_17 = "h2-17";
private static final String HTTP_1_1 = "http/1.1";
private static final String JETTY_HTTP = "jetty.http";
private static final String CONNECTOR = "connector";
/** The logging system. */
private final Logger log = LoggerFactory.getLogger(org.jooby.spi.Server.class);
private Server server;
@Inject
public JettyServer(final HttpHandler handler, final Config conf,
final Provider<SSLContext> sslCtx) {
this.server = server(handler, conf, sslCtx);
}
private Server server(final HttpHandler handler, final Config conf,
final Provider<SSLContext> sslCtx) {
System.setProperty("org.eclipse.jetty.util.UrlEncoded.charset",
conf.getString("jetty.url.charset"));
System.setProperty("org.eclipse.jetty.server.Request.maxFormContentSize",
conf.getBytes("server.http.MaxRequestSize").toString());
QueuedThreadPool pool = conf(new QueuedThreadPool(), conf.getConfig("jetty.threads"),
"jetty.threads");
Server server = new Server(pool);
server.setStopAtShutdown(false);
// HTTP connector
boolean http2 = conf.getBoolean("server.http2.enabled");
ServerConnector http = http(server, conf.getConfig(JETTY_HTTP), JETTY_HTTP, http2);
http.setPort(conf.getInt("application.port"));
http.setHost(conf.getString("application.host"));
if (conf.hasPath("application.securePort")) {
ServerConnector https = https(server, conf.getConfig(JETTY_HTTP), JETTY_HTTP,
sslCtx.get(), http2);
https.setPort(conf.getInt("application.securePort"));
server.addConnector(https);
}
server.addConnector(http);
ContextHandler sch = new ContextHandler();
sch.setAttribute(DecoratedObjectFactory.ATTR, new DecoratedObjectFactory());
WebSocketPolicy wsConfig = conf(new WebSocketPolicy(WebSocketBehavior.SERVER),
conf.getConfig("jetty.ws"), "jetty.ws");
WebSocketServerFactory webSocketServerFactory = new WebSocketServerFactory(
sch.getServletContext(), wsConfig);
webSocketServerFactory.setCreator((req, rsp) -> {
JettyWebSocket ws = new JettyWebSocket();
req.getHttpServletRequest().setAttribute(JettyWebSocket.class.getName(), ws);
return ws;
});
// always '/' context path is internally handle by jooby
sch.setContextPath("/");
sch.setHandler(new JettyHandler(handler, webSocketServerFactory, conf
.getString("application.tmpdir"), conf.getBytes("jetty.FileSizeThreshold").intValue()));
server.setHandler(sch);
return server;
}
private ServerConnector http(final Server server, final Config conf, final String path,
final boolean http2) {
HttpConfiguration httpConfig = conf(new HttpConfiguration(), conf.withoutPath(CONNECTOR),
path);
ServerConnector connector;
if (http2) {
connector = new ServerConnector(server, new HttpConnectionFactory(httpConfig),
new HTTP2CServerConnectionFactory(httpConfig));
} else {
connector = new ServerConnector(server, new HttpConnectionFactory(httpConfig));
}
return conf(connector, conf.getConfig(CONNECTOR), path + "." + CONNECTOR);
}
private ServerConnector https(final Server server, final Config conf, final String path,
final SSLContext sslContext, final boolean http2) {
HttpConfiguration httpConf = conf(new HttpConfiguration(), conf.withoutPath(CONNECTOR),
path);
SslContextFactory sslContextFactory = new SslContextFactory();
sslContextFactory.setSslContext(sslContext);
sslContextFactory.setIncludeProtocols("TLSv1.2");
sslContextFactory.setIncludeCipherSuites("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256");
HttpConfiguration httpsConf = new HttpConfiguration(httpConf);
httpsConf.addCustomizer(new SecureRequestCustomizer());
HttpConnectionFactory https11 = new HttpConnectionFactory(httpsConf);
if (http2) {
ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory(H2, H2_17, HTTP_1_1);
alpn.setDefaultProtocol(HTTP_1_1);
HTTP2ServerConnectionFactory https2 = new HTTP2ServerConnectionFactory(httpsConf);
ServerConnector connector = new ServerConnector(server,
new SslConnectionFactory(sslContextFactory, "alpn"), alpn, https2, https11);
return conf(connector, conf.getConfig(CONNECTOR), path + ".connector");
} else {
ServerConnector connector = new ServerConnector(server,
new SslConnectionFactory(sslContextFactory, HTTP_1_1), https11);
return conf(connector, conf.getConfig(CONNECTOR), path + ".connector");
}
}
@Override
public void start() throws Exception {
server.start();
}
@Override
public void join() throws InterruptedException {
server.join();
}
@Override
public void stop() throws Exception {
server.stop();
}
@Override
public Optional<Executor> executor() {
return Optional.ofNullable(server.getThreadPool());
}
private void tryOption(final Object source, final Config config, final Method option) {
Try.run(() -> {
String optionName = option.getName().replace("set", "");
Object optionValue = config.getAnyRef(optionName);
Class<?> optionType = Primitives.wrap(option.getParameterTypes()[0]);
if (Number.class.isAssignableFrom(optionType) && optionValue instanceof String) {
// either a byte or time unit
try {
optionValue = config.getBytes(optionName);
} catch (ConfigException.BadValue ex) {
optionValue = config.getDuration(optionName, TimeUnit.MILLISECONDS);
}
if (optionType == Integer.class) {
// to int
optionValue = ((Number) optionValue).intValue();
}
}
log.debug("{}.{}({})", source.getClass().getSimpleName(), option.getName(), optionValue);
option.invoke(source, optionValue);
}).onFailure(x -> {
Throwable cause = x;
if (x instanceof InvocationTargetException) {
cause = ((InvocationTargetException) x).getTargetException();
}
Throwables.propagate(cause);
});
}
private <T> T conf(final T source, final Config config, final String path) {
Map<String, Method> methods = Arrays.stream(source.getClass().getMethods())
.filter(m -> m.getName().startsWith("set") && m.getParameterCount() == 1)
.collect(Collectors.toMap(Method::getName, Function.<Method> identity()));
config.entrySet().forEach(entry -> {
String key = "set" + entry.getKey();
Method method = methods.get(key);
if (method != null) {
tryOption(source, config, method);
} else {
log.error("Unknown option: {}.{} for: {}", path, key, source.getClass().getName());
}
});
return source;
}
}