/* ************************************************************************
#
# 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.http;
import java.net.InetSocketAddress;
import divconq.bus.Message;
import divconq.bus.MessageUtil;
import divconq.bus.net.StreamMessage;
import divconq.hub.DomainInfo;
import divconq.hub.Hub;
import divconq.hub.HubState;
import divconq.lang.op.FuncResult;
import divconq.lang.op.OperationContext;
import divconq.lang.op.OperationContextBuilder;
import divconq.lang.op.OperationResult;
import divconq.log.Logger;
import divconq.net.NetUtil;
import divconq.net.ssl.SslHandler;
import divconq.session.DataStreamChannel;
import divconq.session.ISessionAdapter;
import divconq.session.IStreamDriver;
import divconq.session.Session;
import divconq.struct.CompositeParser;
import divconq.struct.CompositeStruct;
import divconq.struct.ListStruct;
import divconq.struct.RecordStruct;
import divconq.util.StringUtil;
import divconq.web.HttpBodyRequestDecoder;
import divconq.web.HttpContext;
import divconq.web.IContentDecoder;
import divconq.web.Request;
import divconq.web.Response;
import divconq.web.RpcHandler;
import divconq.web.WebContext;
import divconq.web.WebDomain;
import divconq.web.WebSite;
import divconq.web.WebSiteManager;
import divconq.xml.XElement;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.codec.http.cookie.DefaultCookie;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.DefaultHttpContent;
import io.netty.handler.codec.http.DefaultLastHttpContent;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpHeaders.Names;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PongWebSocketFrame;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;
/**
* Handles handshakes and messages
*/
public class ServerHandler extends SimpleChannelInboundHandler<Object> {
static protected final String RPC_PATH = "/rpc";
static protected final String BUS_PATH = "/bus";
static protected final String STATUS_PATH = "status";
static protected final String DOWNLOAD_PATH = "download";
static protected final String UPLOAD_PATH = "upload";
// this is the context used until we figure out if we have a session or not
static protected OperationContext defaultOpContext = OperationContext.useNewGuest();
protected HttpContext context = null;
protected WebSocketServerHandshaker handshaker = null; // TODO when handshaker completes then set context.session to a different session adaptor (direct messages to client instead of queue)
public ServerHandler(XElement config, WebSiteManager siteman) {
this.context = new HttpContext(config, siteman);
//System.out.println("new server handler!!");
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
Session s = this.context.getSession();
if (s != null)
Logger.info("Web Server connection inactive: " + s.getId());
if (Logger.isDebug())
Logger.debug("Connection inactive was " + ctx.channel().localAddress()
+ " from " + ctx.channel().remoteAddress()); // + " session " + this.context.getSession().getId());
this.context.closed();
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
//System.out.println("server got object: " + msg.getClass().getName());
OperationContext.set(ServerHandler.defaultOpContext);
if (this.context.getChannel() == null)
this.context.setChannel(ctx.channel());
if (msg instanceof HttpObject)
this.handleHttpRequest(ctx, (HttpObject) msg);
else if (msg instanceof WebSocketFrame)
this.handleWebSocketFrame(ctx, (WebSocketFrame) msg);
}
// TODO this may not be a real threat but review it anyway
// http://www.christian-schneider.net/CrossSiteWebSocketHijacking.html
// https://www.owasp.org/index.php/HTML5_Security_Cheat_Sheet
// https://www.owasp.org/index.php/Cross_Site_Scripting_Flaw
// https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet
// https://code.google.com/p/owasp-java-encoder/source/browse/trunk/core/src/main/java/org/owasp/encoder/HTMLEncoder.java
// http://kefirsf.org/kefirbb/
// http://codex.wordpress.org/Validating_Sanitizing_and_Escaping_User_Data
// http://excess-xss.com/
// http://en.wikipedia.org/wiki/HTTP_cookie
// If you wish to support both HTTP requests and websockets in the one server, refer to the io.netty.example.http.websocketx.server.WebSocketServer example. To know once a handshake was done you can intercept the ChannelInboundHandler.userEventTriggered(ChannelHandlerContext, Object) and check if the event was of type WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE.
// TODO CORS
// also review
// https://github.com/netty/netty/pull/2427/files
// http://www.html5rocks.com/en/tutorials/file/xhr2/
// http://www.html5rocks.com/en/tutorials/cors/
// http://enable-cors.org/server.html
// BREACH etc
// https://community.qualys.com/blogs/securitylabs/2013/08/07/defending-against-the-breach-attack
// https://en.wikipedia.org/wiki/BREACH_(security_exploit)
/*
GET http://229097002.log.optimizely.com/event?a=229097002&d=229097002&y=false&x761570292=750582396&s231842852=gc&s231947722=search&s232031415=false&n=http%3A%2F%2Fwww.telerik.com%2Fdownload%2Ffiddler%2Ffirst-run&u=oeu1393506471224r0.17277055932208896&wxhr=true&t=1398696975163&f=702401691,760731745,761570292,766240693,834650096 HTTP/1.1
Host: 229097002.log.optimizely.com
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36
Origin: http://www.telerik.com
Accept: * /*
Referer: http://www.telerik.com/download/fiddler/first-run
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Cookie: fixed_external_20728634_bucket_map=; fixed_external_9718688_bucket_map=; fixed_external_138031368_bucket_map=; end_user_id=oeu1393506471224r0.17277055932208896; bucket_map=761570292%3A750582396
HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: POST, GET
Access-Control-Allow-Origin: http://www.telerik.com
Content-Type: application/json
Date: Mon, 28 Apr 2014 14:56:18 GMT
P3P: CP="IDC DSP COR CURa ADMa OUR IND PHY ONL COM STA"
Server: nginx/1.2.7
Content-Length: 2
Connection: keep-alive
{}
Chrome Web Socket Request:
GET /rpc HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: v8MIKFOPlaVtRK2C1iOJ4Q==
Host: localhost:9443
Sec-WebSocket-Origin: http://localhost:9443
Sec-WebSocket-Version: 13
x-DivConq-Mode: Private
Java API with Session Id
POST /rpc HTTP/1.1
Host: localhost
User-Agent: DivConq HyperAPI Client 1.0
Connection: keep-alive
Content-Encoding: UTF-8
Content-Type: application/json; charset=utf-8
Cookie: SessionId=00700_fa2h199tkc2e8i2cs4e8s9ujhh_EetvVV9EocXc; $Path="/"
*
*/
public void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
if (!this.context.isWebSocket()) {
this.context.isWebSocket(true);
// upgrade how session handles responses
this.context.getSession().setAdatper(new ISessionAdapter() {
@Override
public void stop() {
ServerHandler.this.context.close();
}
@Override
public String getClientKey() {
return ServerHandler.this.context.getClientCert();
}
@Override
public ListStruct popMessages() {
return null;
}
@Override
public void deliver(Message msg) {
// clean up so it looks like an RPC message
msg.removeField("ToHub");
msg.removeField("Context");
msg.removeField("RespondTo");
msg.removeField("RespondTag");
msg.removeField("Version");
ServerHandler.this.context.getChannel().writeAndFlush(new TextWebSocketFrame(msg.toString()));
}
});
}
Channel ch = ctx.channel();
// Check for closing frame
if (frame instanceof CloseWebSocketFrame) {
Logger.info("Web Server received close");
ch.close();
return;
}
if (frame instanceof PingWebSocketFrame) {
Logger.info("Web Server received ping");
ctx.channel().writeAndFlush(new PongWebSocketFrame(frame.content().retain()));
return;
}
if (frame instanceof PongWebSocketFrame) {
Logger.info("Web Server received pong");
return;
}
if (frame instanceof TextWebSocketFrame) {
// use a clean (empty) context - session will add context if necessary
OperationContext.clear();
String data = ((TextWebSocketFrame) frame).text();
//System.out.println("dcBus " + this.session.getSessionMode() + " received message: " + data);
FuncResult<CompositeStruct> res = CompositeParser.parseJson(data);
if (res.hasErrors()) {
// TODO logging
Logger.warn("Web Server got a bad message: " + res.getMessage());
ch.close(); // don't stay with bad messages
return;
}
// TODO adapt ws message to bus message - see WsServer
//this.context.receiveMessage(this.context, ch, MessageUtil.fromRecord((RecordStruct)res.getResult()));
CompositeStruct croot = res.getResult();
if ((croot == null) || !(croot instanceof RecordStruct)) {
Logger.warn("Web Server got a bad message: " + res.getMessage());
ch.close(); // don't stay with bad messages
return;
}
RecordStruct mrec = (RecordStruct) croot;
// check that the request conforms to the schema for RpcMessage
OperationResult rootres = mrec.validate("RpcMessage");
if (rootres.hasErrors()) {
Logger.warn("Web Server got a bad message: " + res.getMessage());
ch.close(); // don't stay with bad messages
return;
}
// if so convert the Record into a Message for transport over our bus
Message msg = MessageUtil.fromRecord(mrec);
this.context.getSession().sendMessage(msg);
return;
}
// TODO unhandled frame type
// TODO logging
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
Logger.warn("Web server connection exception was " + cause);
if (Logger.isDebug())
Logger.debug("Web server connection exception was " + ctx.channel().localAddress()
+ " from " + ctx.channel().remoteAddress()); // + " session " + this.context.getSession().getId());
// TODO logging
//System.out.println("EC?");
//?cause.printStackTrace();
ctx.close();
}
public static String getWebSocketLocation(boolean secure, HttpRequest req) {
//boolean secure = "True".equals(this.config.getAttribute("Secure"));
return (secure ? "wss://" : "ws://") + req.headers().get(Names.HOST) + RPC_PATH;
}
// TODO improve to ignore large POSTs on most Paths
// TODO ip lockout
// TODO acl
// TODO debug level based in ip address
// TODO any where along the way, especially RPC, ping Remote Trust Center with down votes if doesn't work out
public void handleHttpRequest(ChannelHandlerContext ctx, HttpObject httpobj) throws Exception {
if (httpobj instanceof HttpContent) {
this.context.offerContent((HttpContent)httpobj);
return;
}
if (Logger.isDebug())
Logger.debug("Web server request " + httpobj.getClass().getName() + " " + ctx.channel().localAddress()
+ " from " + ctx.channel().remoteAddress()); // + " session " + this.context.getSession().getId());
// at the least don't allow web requests until running
// TODO later we may need to have a "Going Down" flag and filter new requests but allow existing
if (!Hub.instance.isRunning()) {
this.context.sendInternalError();
return;
}
if (!(httpobj instanceof HttpRequest)) {
this.context.sendRequestBad();
return;
}
HttpRequest httpreq = (HttpRequest) httpobj;
this.context.load(ctx, httpreq);
// Handle a bad request.
if (!httpreq.getDecoderResult().isSuccess()) {
this.context.sendRequestBad();
return;
}
Request req = this.context.getRequest();
Response resp = this.context.getResponse();
// to avoid lots of unused sessions
if (req.pathEquals("/favicon.ico")) {
this.context.sendNotFound();
return;
}
// make sure we don't have a leftover task context
OperationContext.clear();
String origin = "http:" + NetUtil.formatIpAddress((InetSocketAddress)ctx.channel().remoteAddress());
// TODO use X-Forwarded-For if available, maybe a plug in approach to getting client's IP?
DomainInfo dinfo = this.context.getSiteman().resolveDomainInfo(req.getHeader("Host"));
if (dinfo == null) {
if (Logger.isDebug())
Logger.debug("Domain not found for: " + req.getHeader("Host"));
this.context.sendForbidden();
return;
}
WebDomain wdomain = this.context.getSiteman().getDomain(dinfo.getId());
// check into url re-routing
String reroute = wdomain.route(req, (SslHandler)ctx.channel().pipeline().get("ssl"));
if (StringUtil.isNotEmpty(reroute)) {
if (Logger.isDebug())
Logger.debug("Routing the request to: " + reroute);
this.context.getResponse().setStatus(HttpResponseStatus.FOUND);
this.context.getResponse().setHeader("Location", reroute);
this.context.send();
return;
}
WebSite wsite = wdomain.site(req);
if (Logger.isTrace())
Logger.trace("Site: " + (wsite != null ? wsite.getAlias() : "[missing]"));
this.context.setSite(wsite);
Cookie sesscookie = req.getCookie("dcSessionId");
Session sess = null;
if (sesscookie != null) {
String v = sesscookie.value();
String sessionid = v.substring(0, v.lastIndexOf('_'));
String accesscode = v.substring(v.lastIndexOf('_') + 1);
sess = Hub.instance.getSessions().lookupAuth(sessionid, accesscode);
}
if (sess == null) {
sess = Hub.instance.getSessions().create(origin, dinfo.getId());
Logger.info("Started new session: " + sess.getId() + " on " + req.getPath() + " for " + origin);
// TODO if ssl set client key on user context
//req.getSecuritySession().getPeerCertificates();
sess.setAdatper(new ISessionAdapter() {
protected volatile ListStruct msgs = new ListStruct();
@Override
public void stop() {
if (Logger.isDebug())
Logger.debug("Web server session adapter got a STOP request.");
ServerHandler.this.context.close();
}
@Override
public String getClientKey() {
return ServerHandler.this.context.getClientCert();
}
@Override
public ListStruct popMessages() {
ListStruct ret = this.msgs;
this.msgs = new ListStruct();
return ret;
}
@Override
public void deliver(Message msg) {
// keep no more than 100 messages - this is not a "reliable" approach, just basic comm help
while (this.msgs.getSize() > 99)
this.msgs.removeItem(0);
this.msgs.addItem(msg);
}
});
Cookie sk = new DefaultCookie("dcSessionId", sess.getId() + "_" + sess.getKey());
sk.setPath("/");
sk.setHttpOnly(true);
// TODO configure, but make Secure by default if using https
if (ctx.channel().pipeline().get("ssl") != null)
sk.setSecure(true);
resp.setCookie(sk);
}
this.context.setSession(sess);
sess.touch();
OperationContextBuilder ctxb = sess.allocateContextBuilder()
.withOrigin(origin);
Cookie localek = wsite.resolveLocale(this.context, sess.getUser(), ctxb);
if (localek != null) {
localek.setPath("/");
localek.setHttpOnly(true);
// TODO configure, but make Secure by default if using https
if (ctx.channel().pipeline().get("ssl") != null)
localek.setSecure(true);
resp.setCookie(localek);
}
OperationContext tc = sess.useContext(ctxb);
tc.setLocaleResource(wsite);
if (!"rpc".equals(req.getPath().getName(0)))
tc.info("Web request for host: " + req.getHeader("Host") + " url: " + req.getPath() + " by: " + origin + " session: " + sess.getId());
// TODO if (Logger.isDebug()) {
// System.out.println("Operating locale " + tc.getWorkingLocale());
//}
/*
System.out.println("sess proto: " + ((SslHandler)ctx.channel().pipeline().get("ssl")).engine().getSession().getProtocol());
System.out.println("sess suite: " + ((SslHandler)ctx.channel().pipeline().get("ssl")).engine().getSession().getCipherSuite());
*/
try {
// --------------------------------------------
// bus request (deprecated in favor of http/2 sometime soon)
// --------------------------------------------
if (req.pathEquals(ServerHandler.BUS_PATH)) {
// Allow only GET methods.
if (req.getMethod() != HttpMethod.GET) {
this.context.sendForbidden();
return;
}
if (Logger.isDebug())
Logger.debug("Setup a bus (ws) handshake " + sess.getId());
// Handshake
WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
ServerHandler.getWebSocketLocation("True".equals(this.context.getConfig().getAttribute("Secure")), httpreq), null, false);
this.handshaker = wsFactory.newHandshaker(httpreq);
if (this.handshaker == null)
WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
else {
DefaultFullHttpRequest freq = new DefaultFullHttpRequest(httpreq.getProtocolVersion(), httpreq.getMethod(), httpreq.getUri());
freq.headers().add(httpreq.headers());
this.handshaker.handshake(ctx.channel(), freq);
return;
}
this.context.sendForbidden();
return;
}
// --------------------------------------------
// upload file request
// --------------------------------------------
// "upload" is it's own built-in extension.
if ((req.getPath().getNameCount() == 3) && req.getPath().getName(0).equals(ServerHandler.UPLOAD_PATH)) {
if (!Hub.instance.isRunning()) { // only allow uploads when running
this.context.sendRequestBad();
return;
}
// currently only supporting POST/PUT of pure binary - though support for form uploads can be restored, see below
// we cannot rely on content type being meaningful
//if (!"application/octet-stream".equals(req.getContentType().getPrimary())) {
// this.context.sendRequestBad();
// return;
//}
// TODO add CORS support if needed
if ((req.getMethod() != HttpMethod.PUT) && (req.getMethod() != HttpMethod.POST)) {
this.context.sendRequestBad();
return;
}
final String cid = req.getPath().getName(1);
final String op = req.getPath().getName(2);
if (Logger.isDebug())
Logger.debug("Initiating an upload block on " + cid + " for " + sess.getId());
final DataStreamChannel dsc = sess.getChannel(cid);
if (dsc == null) {
this.context.sendRequestBad();
return;
}
dsc.setDriver(new IStreamDriver() {
@Override
public void cancel() {
Logger.error("Transfer canceled on channel: " + cid);
dsc.complete();
ServerHandler.this.context.sendRequestBad(); // TODO headers?
}
@Override
public void nextChunk() {
Logger.debug("Continue on channel: " + cid);
ServerHandler.this.context.sendRequestOk();
}
@Override
public void message(StreamMessage msg) {
if (msg.isFinal()) {
Logger.debug("Final on channel: " + cid);
dsc.complete();
ServerHandler.this.context.sendRequestOk();
}
}
});
//if (req.getMethod() == HttpMethod.PUT) {
this.context.setDecoder(new IContentDecoder() {
protected boolean completed = false;
protected int seq = 0;
@Override
public void release() {
// trust that http connection is closing or what ever needs to happen, we just need to deal with datastream
Logger.debug("Releasing data stream: " + cid + " completed: " + completed);
// TODO this is not true, client may switch connections to continue upload, only a session timeout or datastream timeout count as true problems
// if not done with request then something went wrong, kill data channel
//if (!this.completed)
// dsc.abort();
}
@Override
public void offer(HttpContent chunk) {
boolean finalchunk = (chunk instanceof LastHttpContent);
//System.out.println("Chunk: " + finalchunk);
ByteBuf buffer = chunk.content();
if (!dsc.isClosed()) {
int size = buffer.readableBytes();
//System.out.println("Chunk size: " + size);
if (Logger.isDebug())
Logger.debug("Offered chunk on: " + cid + " size: " + size + " final: " + finalchunk);
dsc.touch(); // TODO try to set progress on dsc
// TODO set hint in netty as to where this buffer was handled and sent
if (size > 0) {
buffer.retain(); // we will be using a reference up during send
StreamMessage b = new StreamMessage("Block", buffer);
b.setField("Sequence", this.seq);
//System.out.println("Buffer ref cnt a: " + buffer.refCnt());
OperationResult or = dsc.send(b);
//System.out.println("Buffer ref cnt b: " + buffer.refCnt());
// indicate we have read the buffer?
buffer.readerIndex(buffer.writerIndex());
if (or.hasErrors()) {
dsc.close();
return;
}
this.seq++;
}
// if last buffer of last block then mark the upload as completed
if (finalchunk) {
if ("Final".equals(op))
dsc.send(MessageUtil.streamFinal());
else
dsc.getDriver().nextChunk();
}
}
else {
if (Logger.isDebug())
Logger.debug("Offered chunk on closed channel: " + cid);
}
// means this block is completed, not necessarily entire file uploaded
if (finalchunk)
this.completed = true;
}
});
//return;
//}
/* old approach that supported multipart posts TODO review/remove
if (req.getMethod() == HttpMethod.POST) {
StreamingDataFactory sdf = new StreamingDataFactory(dsc, op);
// TODO consider supporting non-multipart?
final HttpPostMultipartRequestDecoder prd = new HttpPostMultipartRequestDecoder(sdf, httpreq);
this.context.setDecoder(new IContentDecoder() {
@Override
public void release() {
// trust that http connection is closing or what ever needs to happen, we just need to deal with datastream
// if not done with request then something went wrong, kill data channel
if ((prd.getStatus() != MultiPartStatus.EPILOGUE) && (prd.getStatus() != MultiPartStatus.PREEPILOGUE))
dsc.kill();
}
@Override
public void offer(HttpContent chunk) {
//the only thing we care about is the file, the file will stream to dsc - the rest can disappear
prd.offer(chunk);
}
});
return;
}
*/
//this.context.sendRequestBad();
return;
}
// --------------------------------------------
// download file request
// --------------------------------------------
// "download" is it's own built-in extension.
if ((req.getPath().getNameCount() == 2) && req.getPath().getName(0).equals(ServerHandler.DOWNLOAD_PATH)) {
if (!Hub.instance.isRunning()) { // only allow downloads when running
this.context.sendRequestBad();
return;
}
if (req.getMethod() != HttpMethod.GET) {
this.context.sendRequestBad();
return;
}
String cid = req.getPath().getName(1);
if (Logger.isDebug())
Logger.debug("Initiating an download on " + cid + " for " + sess.getId());
final DataStreamChannel dsc = sess.getChannel(cid);
if (dsc == null) {
this.context.sendRequestBad();
return;
}
dsc.setDriver(new IStreamDriver() {
//protected long amt = 0;
protected long seq = 0;
@Override
public void cancel() {
Logger.debug("Transfer canceled on channel: " + cid);
dsc.complete();
ServerHandler.this.context.close();
}
@Override
public void nextChunk() {
// meaningless in download
}
@Override
public void message(StreamMessage msg) {
int seqnum = (int) msg.getFieldAsInteger("Sequence", 0);
if (seqnum != this.seq) {
this.error(1, "Bad sequence number: " + seqnum);
return;
}
if (msg.hasData()) {
if (Logger.isDebug())
Logger.debug("Transfer data: " + msg.getData().readableBytes());
//this.amt += msg.getData().readableBytes();
HttpContent b = new DefaultHttpContent(Unpooled.copiedBuffer(msg.getData())); // TODO not copied
ServerHandler.this.context.sendDownload(b);
}
this.seq++;
// TODO update progress
if (msg.isFinal()) {
if (Logger.isDebug())
Logger.debug("Transfer completed: " + msg.getData().readableBytes());
ServerHandler.this.context.sendDownload(new DefaultLastHttpContent());
ServerHandler.this.context.close();
dsc.complete();
}
}
public void error(int code, String msg) {
Logger.error("Transfer error - " + code + ": " + msg);
dsc.send(MessageUtil.streamError(code, msg));
ServerHandler.this.context.close();
}
});
// for some reason HyperSession is sending content.
this.context.setDecoder(new IContentDecoder() {
@Override
public void release() {
}
@Override
public void offer(HttpContent chunk) {
if (!(chunk instanceof LastHttpContent))
Logger.error("Unexplained and unwanted content during download: " + chunk);
}
});
// tell the client that chunked content is coming
this.context.sendDownloadHeaders(dsc.getPath() != null ? dsc.getPath().getFileName() : null, dsc.getMime());
// TODO for now disable compression on downloads - later determine if we should enable for some cases
this.context.getResponse().setHeader(HttpHeaders.Names.CONTENT_ENCODING, HttpHeaders.Values.IDENTITY);
if (Logger.isDebug())
Logger.debug("Singal Transfer Start - " + cid);
// get the data flowing
dsc.send(new StreamMessage("Start"));
return;
}
// --------------------------------------------
// status request
// --------------------------------------------
if ((req.getPath().getNameCount() == 1) && req.getPath().getName(0).equals(ServerHandler.STATUS_PATH)) {
if (Hub.instance.getState() == HubState.Running)
this.context.sendRequestOk();
else
this.context.sendRequestBad();
return;
}
// --------------------------------------------
// rpc request
// --------------------------------------------
// "rpc" is it's own built-in extension. all requests to rpc are routed through
// DivConq bus, if the request is valid
if (req.pathEquals(ServerHandler.RPC_PATH)) {
if (req.getMethod() != HttpMethod.POST) {
this.context.sendRequestBad();
return;
}
//System.out.println("looks like we have a rpc message");
// max 4MB of json? -- TODO is that max chunk size or max total? we don't need 4MB chunk...
this.context.setDecoder(new HttpBodyRequestDecoder(4096 * 1024, new RpcHandler(this.context)));
return;
}
// --------------------------------------------
// regular path/file request
// --------------------------------------------
if (Logger.isDebug())
Logger.debug("Request posted to web domain: " + sess.getId());
OperationResult res = new OperationResult();
WebContext wctx = new WebContext(this.context);
wdomain.execute(wctx);
// no errors starting page processing, return
if (res.hasErrors()) {
resp.setHeader("X-dcResultCode", res.getCode() + "");
resp.setHeader("X-dcResultMesage", res.getMessage());
this.context.sendNotFound();
}
}
catch (Exception x) {
Logger.error("Request triggered exception: " + sess.getId() + " - " + x);
this.context.sendInternalError();
}
}
}