/* ************************************************************************
#
# DivConq
#
# http://divconq.com/
#
# Copyright:
# Copyright 2014 eTimeline, LLC. All rights reserved.
#
# License:
# See the license.txt file in the project's top-level directory for details.
#
# Authors:
# * Andy White
#
************************************************************************ */
package divconq.web;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponseEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
import divconq.hub.Hub;
import divconq.hub.HubEvents;
import divconq.log.Logger;
import divconq.mod.ModuleBase;
import divconq.util.StringUtil;
import divconq.web.http.HttpContentCompressor;
import divconq.web.http.ServerHandler;
import divconq.web.http.SniHandler;
import divconq.xml.XElement;
//TODO integrate streaming - http://code.google.com/p/red5/
public class WebModule extends ModuleBase {
protected ConcurrentHashMap<Integer, Channel> activelisteners = new ConcurrentHashMap<>();
protected ReentrantLock listenlock = new ReentrantLock();
protected WebSiteManager siteman = new WebSiteManager();
public WebSiteManager getWebSiteManager() {
return this.siteman;
}
@Override
public void start() {
// prepare the web site manager from settings in module config
this.siteman.start(this, this.config);
// private hub then go online ASAP
//if (!Hub.instance.getResources().isGateway())
// this.goOnline();
Hub.instance.subscribeToEvent(HubEvents.Connected, e -> this.goOnline());
Hub.instance.subscribeToEvent(HubEvents.Booted, e -> this.goOffline());
}
public void goOnline() {
this.listenlock.lock();
try {
// don't try if already in online mode
if (this.activelisteners.size() > 0)
return;
// typically we should have an extension, unless we are supporting RPC only
// TODO if (WebSiteManager.instance.getDefaultExtension() == null)
// log.warn(0, "No default extension for web server");
boolean deflate = "True".equals(this.config.getAttribute("Deflate"));
for (XElement httpconfig : this.config.selectAll("HttpListener")) {
boolean secure = "True".equals(httpconfig.getAttribute("Secure"));
int httpport = (int) StringUtil.parseInt(httpconfig.getAttribute("Port"), secure ? 443 : 80);
// -------------------------------------------------
// message port
// -------------------------------------------------
ServerBootstrap b = new ServerBootstrap();
// TODO consider using shared EventLoopGroup
// http://normanmaurer.me/presentations/2014-facebook-eng-netty/slides.html#25.0
b.group(Hub.instance.getEventLoopGroup())
.channel(NioServerSocketChannel.class)
.option(ChannelOption.ALLOCATOR, Hub.instance.getBufferAllocator())
//.option(ChannelOption.SO_BACKLOG, 125) // this is probably not needed but serves as note to research
.option(ChannelOption.TCP_NODELAY, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
if (secure) {
SniHandler ssl = new SniHandler(WebModule.this.siteman);
//SslHandler ssl = new SslHandler(WebModule.this.siteman.findSslContextFactory("root").getServerEngine());
pipeline.addLast("ssl", ssl);
}
//pipeline.addLast("codec-http", new HttpServerCodec());
//pipeline.addLast("aggregator", new HttpObjectAggregator(65536));
pipeline.addLast("decoder", new HttpRequestDecoder(4096,8192,262144));
pipeline.addLast("encoder", new HttpResponseEncoder());
if (deflate)
pipeline.addLast("deflater", new HttpContentCompressor());
pipeline.addLast("chunkedWriter", new ChunkedWriteHandler());
pipeline.addLast("handler", new ServerHandler(httpconfig, WebModule.this.siteman));
if (Logger.isDebug())
Logger.debug("New web connection from " + ch.remoteAddress().getAddress().toString());
}
});
if (Logger.isDebug())
b.handler(new LoggingHandler("www", Logger.isTrace() ? LogLevel.TRACE : LogLevel.DEBUG));
try {
// must wait here, both to keep the activelisteners listeners up to date
// but also to make sure we don't release connectLock too soon
ChannelFuture bfuture = b.bind(httpport).sync();
if (bfuture.isSuccess()) {
Logger.info("Web Server listening - now listening for HTTP on TCP port " + httpport);
this.activelisteners.put(httpport, bfuture.channel());
}
else
Logger.error("Web Server unable to bind: " + bfuture.cause());
}
catch (InterruptedException x) {
Logger.error("Web Server interrupted while binding: " + x);
}
catch (Exception x) {
Logger.error("Web Server errored while binding: " + x);
}
}
}
finally {
this.listenlock.unlock();
}
this.siteman.online();
//for (IWebExtension ext : WebSiteManager.instance.extensions.values())
// ext.online();
}
public void goOffline() {
this.listenlock.lock();
try {
// we don't want to listen anymore
for (final Integer port : this.activelisteners.keySet()) {
// tear down message port
Channel ch = this.activelisteners.remove(port);
try {
// must wait here, both to keep the activelisteners listeners up to date
// but also to make sure we don't release connectLock too soon
ChannelFuture bfuture = ch.close().sync();
if (bfuture.isSuccess())
Logger.info("Web Server unbound");
else
Logger.error("Web Server unable to unbind: " + bfuture.cause());
}
catch (InterruptedException x) {
Logger.error("Web Server unable to unbind: " + x);
}
}
}
finally {
this.listenlock.unlock();
}
}
@Override
public void stop() {
this.goOffline();
}
}