/* ************************************************************************
#
# 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.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpHeaders.Names;
import io.netty.handler.codec.http.HttpHeaders.Values;
import io.netty.handler.codec.http.cookie.DefaultCookie;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.codec.http.cookie.ServerCookieEncoder;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import divconq.bus.Message;
import divconq.hub.Hub;
import divconq.io.InputWrapper;
import divconq.io.OutputWrapper;
import divconq.lang.Memory;
import divconq.log.Logger;
import divconq.net.NetUtil;
import divconq.util.FileUtil;
import divconq.util.MimeUtil;
import divconq.util.StringUtil;
import divconq.www.http.parse.DateParser;
public class Response {
protected Map<String, Cookie> cookies = new HashMap<>();
protected boolean keepAlive = false;
protected Memory body = new Memory();
protected Map<CharSequence, String> headers = new HashMap<>();
protected HttpResponseStatus status = HttpResponseStatus.OK;
protected PrintStream stream = null;
public PrintStream getPrintStream() {
if (this.stream == null)
try {
this.stream = new PrintStream(new OutputWrapper(this.body), true, "UTF-8");
}
catch (UnsupportedEncodingException x) {
// ignore, utf8 is supported
}
return this.stream;
}
public void setCookie(Cookie v) {
this.cookies.put(v.name(), v);
}
public void setHeader(CharSequence name, String value) {
this.headers.put(name, value);
}
public void setDateHeader(CharSequence name, long value) {
DateParser parser = new DateParser();
this.headers.put(name, parser.convert(value));
}
public void setStatus(HttpResponseStatus v) {
this.status = v;
}
public void setKeepAlive(boolean v) {
this.keepAlive = v;
}
public void write(PrintStream out) {
this.body.setPosition(0);
this.body.copyToStream(out);
}
public void write(Channel ch) {
if ((this.status != HttpResponseStatus.OK) && (this.body.getLength() == 0))
this.body.write(this.status.toString());
// Build the response object.
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, this.status);
int clen = 0;
this.body.setPosition(0);
try {
clen = response.content().writeBytes(new InputWrapper(this.body), this.body.getLength());
}
catch (IOException e) {
}
response.headers().set(Names.CONTENT_TYPE, "text/plain; charset=UTF-8");
if (this.keepAlive) {
// Add 'Content-Length' header only for a keep-alive connection.
response.headers().set(Names.CONTENT_LENGTH, clen);
// Add keep alive header as per:
// - http://www.w3.org/Protocols/HTTP/1.1/draft-ietf-http-v11-spec-01.html#Connection
response.headers().set(Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
}
// Encode the cookies
for (Cookie c : this.cookies.values())
response.headers().add(Names.SET_COOKIE, ServerCookieEncoder.STRICT.encode(c));
for (Entry<CharSequence, String> h : this.headers.entrySet())
response.headers().set(h.getKey(), h.getValue());
Hub.instance.getSecurityPolicy().hardenHttpResponse(response);
if (Logger.isDebug()) {
Logger.debug("Web server responding to " + ch.remoteAddress());
for (Entry<String, String> ent : response.headers().entries()) {
Logger.debug("Response header: " + ent.getKey() + ": " + ent.getValue());
}
}
// Write the response.
ChannelFuture future = ch.writeAndFlush(response);
// Close the non-keep-alive connection after the write operation is done.
if (!this.keepAlive)
future.addListener(ChannelFutureListener.CLOSE);
/* we do not need to sync - HTTP is one request, one response. we would not pile messages on this channel
*
* furthermore, when doing an upload stream we can actually get locked up here because the "write" from our stream
* is locked on the write process of the data bus and the response to the session is locked on the write of the response
* here - but all the HTTP threads are busy with their respective uploads. If they all use the same data bus session
* then all HTTP threads can get blocked trying to stream upload if even one of those has called an "OK" to upload and
* is stuck here. so be sure not to use sync with HTTP responses. this won't be a problem under normal use.
*
try {
future.sync();
}
catch (InterruptedException x) {
// TODO should we close channel?
}
*/
}
public void writeStart(Channel ch, int contentLength) {
// Build the response object.
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, this.status);
response.headers().set(Names.CONTENT_TYPE, "text/plain; charset=UTF-8");
if (this.keepAlive) {
// Add 'Content-Length' header only for a keep-alive connection.
if (contentLength > 0)
response.headers().set(Names.CONTENT_LENGTH, contentLength);
// Add keep alive header as per:
// - http://www.w3.org/Protocols/HTTP/1.1/draft-ietf-http-v11-spec-01.html#Connection
response.headers().set(Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
}
if (contentLength == 0)
response.headers().set(Names.TRANSFER_ENCODING, HttpHeaders.Values.CHUNKED);
// Encode the cookies
for (Cookie c : this.cookies.values())
response.headers().add(Names.SET_COOKIE, ServerCookieEncoder.STRICT.encode(c));
for (Entry<CharSequence, String> h : this.headers.entrySet())
response.headers().set(h.getKey(), h.getValue());
Hub.instance.getSecurityPolicy().hardenHttpResponse(response);
if (Logger.isDebug()) {
Logger.debug("Web server responding to " + ch.remoteAddress());
for (Entry<String, String> ent : response.headers().entries()) {
Logger.debug("Response header: " + ent.getKey() + ": " + ent.getValue());
}
}
// Write the response.
ch.write(response);
}
public void writeEnd(Channel ch) {
// Write the response.
ChannelFuture future = ch.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
// Close the non-keep-alive connection after the write operation is done.
if (!this.keepAlive)
future.addListener(ChannelFutureListener.CLOSE);
/* we do not need to sync - HTTP is one request, one response. we would not pile messages on this channel
*
* furthermore, when doing an upload stream we can actually get locked up here because the "write" from our stream
* is locked on the write process of the data bus and the response to the session is locked on the write of the response
* here - but all the HTTP threads are busy with their respective uploads. If they all use the same data bus session
* then all HTTP threads can get blocked trying to stream upload if even one of those has called an "OK" to upload and
* is stuck here. so be sure not to use sync with HTTP responses. this won't be a problem under normal use.
*
try {
future.sync();
}
catch (InterruptedException x) {
// TODO should we close channel?
}
*/
}
public void writeChunked(Channel ch) {
// Build the response object.
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, this.status);
response.headers().set(Names.CONTENT_TYPE, "text/plain; charset=UTF-8");
if (this.keepAlive) {
// Add keep alive header as per:
// - http://www.w3.org/Protocols/HTTP/1.1/draft-ietf-http-v11-spec-01.html#Connection
response.headers().set(Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
}
// TODO add a customer header telling how many messages are in the session adaptor's queue - if > 0
// Encode the cookies
for (Cookie c : this.cookies.values())
response.headers().add(Names.SET_COOKIE, ServerCookieEncoder.STRICT.encode(c));
for (Entry<CharSequence, String> h : this.headers.entrySet())
response.headers().set(h.getKey(), h.getValue());
response.headers().set(Names.TRANSFER_ENCODING, Values.CHUNKED);
// Write the response.
ChannelFuture future = ch.writeAndFlush(response);
// Close the non-keep-alive connection after the write operation is done.
if (!this.keepAlive)
future.addListener(ChannelFutureListener.CLOSE);
/* we do not need to sync - HTTP is one request, one response. we would not pile messages on this channel
try {
future.sync();
}
catch (InterruptedException x) {
// TODO should we close channel?
}
*/
}
public void writeDownloadHeaders(Channel ch, String name, String mime) {
// Build the response object.
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, this.status);
response.headers().set(Names.CONTENT_TYPE, StringUtil.isNotEmpty(mime) ? mime : MimeUtil.getMimeTypeForFile(name));
if (StringUtil.isEmpty(name))
name = FileUtil.randomFilename("bin");
response.headers().set("Content-Disposition", "attachment; filename=\"" + NetUtil.urlEncodeUTF8(name) + "\"");
Cookie dl = new DefaultCookie("fileDownload", "true");
dl.setPath("/");
response.headers().add(Names.SET_COOKIE, ServerCookieEncoder.STRICT.encode(dl));
// Encode the cookies
for (Cookie c : this.cookies.values())
response.headers().add(Names.SET_COOKIE, ServerCookieEncoder.STRICT.encode(c));
for (Entry<CharSequence, String> h : this.headers.entrySet())
response.headers().set(h.getKey(), h.getValue());
response.headers().set(Names.TRANSFER_ENCODING, Values.CHUNKED);
// Write the response.
ch.writeAndFlush(response);
}
public void load(ChannelHandlerContext ctx, HttpRequest req) {
this.keepAlive = HttpHeaders.isKeepAlive(req);
}
public void loadVoid() {
}
public void setBody(Message m) {
// TODO make more efficient
// TODO cleanup the content of the message some for browser?
this.body.write(m.toString());
}
public void addBody(String v) {
this.body.write(v);
}
public void setBody(Memory v) {
this.body = v;
}
public Memory getBody() {
return this.body;
}
}