/* ************************************************************************
#
# 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.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.DefaultHttpContent;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpHeaders.Names;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.stream.ChunkedInput;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.security.cert.X509Certificate;
import divconq.bus.Message;
import divconq.log.Logger;
import divconq.net.ssl.SslHandler;
import divconq.session.Session;
import divconq.struct.RecordStruct;
import divconq.util.KeyUtil;
import divconq.util.MimeUtil;
import divconq.xml.XElement;
// TODO combine with WebContext??
public class HttpContext implements IInnerContext {
protected Channel chan = null;
protected XElement config = null;
// used with HTTP only, not WS
protected IContentDecoder decoder = null;
protected Request request = null;
protected Response response = null;
protected RecordStruct altparams = null;
protected Session session = null;
protected WebSiteManager siteman = null;
protected WebSite site = null;
protected boolean isWebsocket = false;
public Request getRequest() {
return this.request;
}
public Response getResponse() {
return this.response;
}
public void setSession(Session v) {
this.session = v;
}
public Session getSession() {
return this.session;
}
@Override
public void setAltParams(RecordStruct v) {
this.altparams = v;
}
@Override
public RecordStruct getAltParams() {
return this.altparams;
}
@Override
public WebDomain getDomain() {
if (this.site != null)
return this.site.getDomain();
return null;
}
public void setSite(WebSite v) {
this.site = v;
}
@Override
public WebSite getSite() {
return this.site;
}
public WebSiteManager getSiteman() {
return this.siteman;
}
public void isWebSocket(boolean v) {
this.isWebsocket = v;
}
public boolean isWebSocket() {
return this.isWebsocket;
}
public void load(ChannelHandlerContext ctx, HttpRequest req) {
IContentDecoder d = this.decoder;
if (d != null) {
d.release();
this.decoder = null;
}
this.request = new Request();
this.request.load(ctx, req);
this.response = new Response();
this.response.load(ctx, req);
}
public XElement getConfig() {
return this.config;
}
public Channel getChannel() {
return this.chan;
}
public void setChannel(Channel chan) {
this.chan = chan;
}
public void setDecoder(IContentDecoder v) {
this.decoder = v;
}
public HttpContext(XElement config, WebSiteManager siteman) {
this.config = config;
this.siteman = siteman;
}
public void offerContent(HttpContent v) {
this.session.touch(); // TODO make sure we are in proper context when this is called - as well as any calls to getSession above - then remove this extra reference to current session
IContentDecoder d = this.decoder;
if (d != null)
d.offer(v);
// TODO in netty 5 alpha 1 this is getting called after each normal http get and is forcing us to close everytime
// we may want to review later
//else
// this.sendBadRequest();
}
public void close() {
try {
if (this.chan != null)
this.chan.close().await(2000);
}
catch (InterruptedException x) {
// ignore
}
}
public void sendNotFound() {
if (Logger.isDebug())
Logger.debug("Web server respond with Not Found");
if (this.response != null) {
this.response.setStatus(HttpResponseStatus.NOT_FOUND);
this.send();
}
}
public void sendForbidden() {
if (Logger.isDebug())
Logger.debug("Web server respond with Forbidden");
if (this.response != null) {
this.response.setStatus(HttpResponseStatus.FORBIDDEN);
this.response.setKeepAlive(false);
this.send();
}
}
public void sendInternalError() {
if (Logger.isDebug())
Logger.debug("Web server respond with Internal Server Error");
if (this.response != null) {
this.response.setStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR);
this.response.setKeepAlive(false);
this.send();
}
}
public void sendRequestBad() {
if (Logger.isDebug())
Logger.debug("Web server respond with Request Bad");
if (this.response != null) {
this.response.setStatus(HttpResponseStatus.BAD_REQUEST);
this.response.setKeepAlive(false);
this.send();
}
}
public void sendRequestOkClose() {
if (this.response != null) {
this.response.setStatus(HttpResponseStatus.OK);
this.response.setKeepAlive(false);
this.send();
}
}
public void sendRequestOk() {
if (this.response != null) {
this.response.setStatus(HttpResponseStatus.OK);
//this.response.setKeepAlive(true);
this.send();
}
}
public void send() {
//if ((this.chan != null) && this.chan.isWritable() && (this.response != null))
if ((this.chan != null) && (this.response != null))
this.response.write(this.chan);
}
public void sendStart(int contentLength) {
if ((this.chan != null) && (this.response != null))
this.response.writeStart(this.chan, contentLength);
}
public void send(ByteBuf content) {
if (this.chan != null)
this.chan.write(new DefaultHttpContent(content));
}
public void send(ChunkedInput<HttpContent> content) {
if (this.chan != null)
this.chan.write(content);
/* TODO we don't need this?
.addListener(new GenericFutureListener<Future<? super Void>>() {
@Override
public void operationComplete(Future<? super Void> future)
throws Exception {
//System.out.println("Sending an end");
//HttpContext.this.response.writeEnd(HttpContext.this.chan);
}
});
*/
}
public void sendEnd() {
if ((this.chan != null) && (this.response != null))
this.response.writeEnd(this.chan);
}
public void sendChunked() {
//if ((this.chan != null) && this.chan.isWritable() && (this.response != null))
if ((this.chan != null) && (this.response != null))
this.response.writeChunked(this.chan);
}
public void sendDownloadHeaders(String name, String mime) {
//if ((this.chan != null) && this.chan.isWritable() && (this.response != null))
if ((this.chan != null) && (this.response != null))
this.response.writeDownloadHeaders(this.chan, name, mime);
}
// should already have TaskContext if needed (SERVICES and HELLO do not need task context)
public void send(Message m) {
try {
//if ((this.chan != null) && this.chan.isWritable()) {
if (this.chan != null) {
if (this.isWebsocket)
this.chan.writeAndFlush(new TextWebSocketFrame(m.toString())); //.sync(); we do not need to sync - HTTP is one request, one response. we would not pile messages on this channel
else {
// include the version hash for the current deployed files
//m.setField("DeployVersion", this.siteman.getVersion());
// we are always using UTF 8, charset is required with any "text/*" mime type that is not ANSI text
this.response.setHeader(Names.CONTENT_TYPE, MimeUtil.getMimeType("json") + "; charset=utf-8");
// TODO enable CORS - http://www.html5rocks.com/en/tutorials/file/xhr2/
// TODO possibly config to be more secure for some users - see CORS handler in Netty
this.response.setHeader(Names.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
this.response.setBody(m);
this.response.write(this.chan);
}
}
}
catch (Exception x) {
}
}
public void send(HttpContent chunk) {
try {
if ((this.chan != null)) {
this.chan.writeAndFlush(chunk); // we do not need to sync - HTTP is one request, one response. we would not pile messages on this channel
}
}
catch (Exception x) {
}
}
public void sendDownload(HttpContent chunk) {
try {
if ((this.chan != null))
this.chan.writeAndFlush(chunk).sync(); // for downloads we do need sync so we don't overwhelm client
// TODO see if we can use something other than sync - http://normanmaurer.me/presentations/2014-facebook-eng-netty/slides.html#10.0
}
catch (Exception x) {
}
}
public void closed() {
IContentDecoder d = this.decoder;
if (d != null)
d.release();
}
@Override
public IWebMacro getMacro(String name) {
return this.siteman.getMacro(name);
}
// get the thumbprint of client cert, if available
public String getClientCert() {
SslHandler sslhandler = (SslHandler) this.chan.pipeline().get("ssl");
if (sslhandler != null) {
try {
X509Certificate[] list = sslhandler.engine().getSession().getPeerCertificateChain();
if (list.length > 0) {
String thumbprint = KeyUtil.getCertThumbprint(list[0]);
//System.out.println("got thumbprint: " + thumbprint);
return thumbprint;
}
}
catch (SSLPeerUnverifiedException x) {
// ignore, at this point we don't enforce peer certs
}
}
return null;
}
}