package com.govu.httpserver; import com.govu.Govu; import com.govu.application.WebApplication; import com.govu.engine.render.Renderer; import com.govu.engine.render.exception.ControllerNotFoundException; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; import java.net.HttpCookie; import java.net.URISyntaxException; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.Iterator; import java.util.Locale; import java.util.Set; import java.util.TimeZone; import java.util.logging.Level; import org.apache.http.NameValuePair; import org.apache.http.client.utils.URIBuilder; import org.apache.log4j.Logger; import org.jboss.netty.buffer.ChannelBuffers; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelFuture; import org.jboss.netty.channel.ChannelFutureListener; import org.jboss.netty.channel.ChannelFutureProgressListener; import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.channel.DefaultFileRegion; import org.jboss.netty.channel.ExceptionEvent; import org.jboss.netty.channel.FileRegion; import org.jboss.netty.channel.MessageEvent; import org.jboss.netty.channel.SimpleChannelHandler; import org.jboss.netty.handler.codec.http.Cookie; import org.jboss.netty.handler.codec.http.CookieDecoder; import org.jboss.netty.handler.codec.http.CookieEncoder; import org.jboss.netty.handler.codec.http.DefaultHttpResponse; import org.jboss.netty.handler.codec.http.HttpHeaders; import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.*; import org.jboss.netty.handler.codec.http.HttpMethod; import org.jboss.netty.handler.codec.http.HttpRequest; import org.jboss.netty.handler.codec.http.HttpResponse; import org.jboss.netty.handler.codec.http.HttpResponseStatus; import static org.jboss.netty.handler.codec.http.HttpResponseStatus.*; import static org.jboss.netty.handler.codec.http.HttpVersion.*; import org.jboss.netty.handler.codec.http.multipart.Attribute; import org.jboss.netty.handler.codec.http.multipart.DefaultHttpDataFactory; import org.jboss.netty.handler.codec.http.multipart.HttpPostRequestDecoder; import org.jboss.netty.handler.codec.http.multipart.InterfaceHttpData; import org.jboss.netty.util.CharsetUtil; import org.mozilla.javascript.EcmaError; import org.mozilla.javascript.EvaluatorException; import org.mozilla.javascript.JavaScriptException; import org.mozilla.javascript.NativeObject; public class HttpServerHandler extends SimpleChannelHandler { public static Logger logger; public HttpServerHandler() { logger = Logger.getLogger("accessLog"); } @Override public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) { HttpRequest request = (HttpRequest) e.getMessage(); logger.info(ctx.getChannel().getRemoteAddress().toString() + ": " + request.getUri()); String pathString = request.getUri(); if (pathString.toLowerCase().startsWith("/controller/") || pathString.toLowerCase().startsWith("/model/") || pathString.toLowerCase().startsWith("/view/")) { HttpResponse response = new DefaultHttpResponse(HTTP_1_1, FORBIDDEN); response.setHeader(CONTENT_TYPE, "text/html; charset=UTF-8"); response.setContent(ChannelBuffers.copiedBuffer("Forbidden", CharsetUtil.UTF_8)); ChannelFuture future = e.getChannel().write(response); future.addListener(ChannelFutureListener.CLOSE); } else { String host = request.getHeader("host"); WebApplication app = Govu.getWebApp(host, pathString); File file = new File(Govu.webRoot + "/" + pathString); if (app == null) { if (file.exists()) { writeFile(file, ctx, request, e); } else { sendError(ctx, NOT_FOUND, "1"); } } else { try { render(request, app, pathString, ctx, e); } catch (ControllerNotFoundException ex) { file = new File(app.getAbsolutePath() + app.getRelativePath(pathString)); if (file.exists()) { writeFile(file, ctx, request, e); } else { sendError(ctx, NOT_FOUND, "2"); } } } } } public void render(HttpRequest request, WebApplication app, String pathString, ChannelHandlerContext ctx, MessageEvent e) throws ControllerNotFoundException { String res = ""; String redirect = null; HttpResponse response; Renderer renderer = null; try { String relativePath = app.getRelativePath(pathString); if (relativePath.equals("/")) { relativePath = "/index"; } URIBuilder uri = new URIBuilder(relativePath); String[] path = uri.getPath().split("/"); if (path.length > 1) { String type = path[1]; String method = path.length > 2 ? path[2] : "index"; HashMap<String, String> query = new HashMap<>(); for (Iterator<NameValuePair> itr = uri.getQueryParams().iterator(); itr.hasNext();) { NameValuePair pair = itr.next(); query.put(pair.getName(), pair.getValue()); } if (request.getMethod() == HttpMethod.POST) { HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(new DefaultHttpDataFactory(false), request); try { while (decoder.hasNext()) { InterfaceHttpData data = decoder.next(); if (data.getHttpDataType() == InterfaceHttpData.HttpDataType.Attribute) { query.put(data.getName(), ((Attribute) data).getValue()); } } } catch (HttpPostRequestDecoder.EndOfDataDecoderException ex) { } } Set<Cookie> cookies; String value = request.getHeader(HttpHeaders.Names.COOKIE); if (value == null) { cookies = Collections.emptySet(); } else { CookieDecoder decoder = new CookieDecoder(); cookies = decoder.decode(value); } app.setCookies(cookies); renderer = new Renderer(app, type, method, query); renderer.render(); res = renderer.getResponse(); } else { throw new ControllerNotFoundException(pathString); } } catch (EcmaError ex) { res = ex.getErrorMessage(); } catch (JavaScriptException ex) { if (ex.getValue() != null) { NativeObject obj = (NativeObject) ex.getValue(); if (obj.get("error").equals("redirect")) { redirect = obj.get("path").toString(); } else { HttpResponseStatus httpRes = new HttpResponseStatus(500, obj.get("msg").toString()); sendError(ctx, httpRes, "0"); } } else { res = ex.getMessage(); } } catch (URISyntaxException ex) { Govu.logger.error("URISyntaxException", ex); res = ex.getMessage(); } catch (HttpPostRequestDecoder.ErrorDataDecoderException ex) { Govu.logger.error("ErrorDataDecoderException", ex); res = ex.getMessage(); } catch (HttpPostRequestDecoder.IncompatibleDataDecoderException ex) { Govu.logger.error("IncompatibleDataDecoderException", ex); res = ex.getMessage(); } catch (FileNotFoundException ex) { Govu.logger.error("FileNotFoundException", ex); res = "View not found: " + request.getUri(); } catch (EvaluatorException ex) { Govu.logger.error("EvaluatorException", ex); res = "Syntax error: " + ex.getMessage(); } catch (IOException ex) { Govu.logger.error("IOException", ex); res = ex.getMessage(); } if (redirect != null) { response = new DefaultHttpResponse(HTTP_1_1, FOUND); setCookies(renderer,response); response.addHeader("Location", redirect); } else { response = new DefaultHttpResponse(HTTP_1_1, OK); response.setHeader(CONTENT_TYPE, "text/html; charset=UTF-8"); response.setContent(ChannelBuffers.copiedBuffer(res, CharsetUtil.UTF_8)); setCookies(renderer,response); } ChannelFuture future = e.getChannel().write(response); future.addListener(ChannelFutureListener.CLOSE); } private void setCookies(Renderer renderer, HttpResponse response) { if (renderer != null) { for (Iterator<HttpCookie> it = renderer.getApp().getCookieEncoder().iterator(); it.hasNext();) { HttpCookie httpCookie = it.next(); CookieEncoder encoder = new CookieEncoder(false); encoder.addCookie(httpCookie.getName(), httpCookie.getValue()); response.addHeader("Set-Cookie", httpCookie.toString()+"; path=/"); } } } public static final String HTTP_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz"; public static final String HTTP_DATE_GMT_TIMEZONE = "GMT"; public static final int HTTP_CACHE_SECONDS = 60; public void writeFile(File file, ChannelHandlerContext ctx, HttpRequest request, MessageEvent e) { try { // Cache Validation String ifModifiedSince = request.getHeader(IF_MODIFIED_SINCE); if (ifModifiedSince != null && ifModifiedSince.length() != 0) { try { SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US); Date ifModifiedSinceDate = dateFormatter.parse(ifModifiedSince); // Only compare up to the second because the datetime format we send to the client does // not have milliseconds long ifModifiedSinceDateSeconds = ifModifiedSinceDate.getTime() / 1000; long fileLastModifiedSeconds = file.lastModified() / 1000; if (ifModifiedSinceDateSeconds == fileLastModifiedSeconds) { sendNotModified(ctx); return; } } catch (ParseException ex) { java.util.logging.Logger.getLogger(HttpServerHandler.class.getName()).log(Level.SEVERE, null, ex); } } RandomAccessFile raf; try { raf = new RandomAccessFile(file, "r"); } catch (FileNotFoundException fnfe) { sendError(ctx, NOT_FOUND, "f"); return; } long fileLength = raf.length(); HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK); response.setHeader(CONTENT_LENGTH, fileLength); //setContentTypeHeader(response, file); setDateAndCacheHeaders(response, file); Channel ch = e.getChannel(); // Write the initial line and the header. ch.write(response); // Write the content. ChannelFuture writeFuture; // No encryption - use zero-copy. final FileRegion region = new DefaultFileRegion(raf.getChannel(), 0, fileLength); writeFuture = ch.write(region); writeFuture.addListener(new ChannelFutureProgressListener() { public void operationComplete(ChannelFuture future) { region.releaseExternalResources(); } public void operationProgressed( ChannelFuture future, long amount, long current, long total) { } }); writeFuture.addListener(ChannelFutureListener.CLOSE); } catch (IOException ex) { logger.error(ex); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception { logger.error(e.getCause()); } private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status, String code) { HttpResponse response = new DefaultHttpResponse(HTTP_1_1, status); response.setHeader(CONTENT_TYPE, "text/plain; charset=UTF-8"); response.setContent(ChannelBuffers.copiedBuffer( "Failure: " + status.toString() + "." + code + "\r\n", CharsetUtil.UTF_8)); ctx.getChannel().write(response).addListener(ChannelFutureListener.CLOSE); } /** * When file timestamp is the same as what the browser is sending up, send a * "304 Not Modified" * * @param ctx Context */ private static void sendNotModified(ChannelHandlerContext ctx) { HttpResponse response = new DefaultHttpResponse(HTTP_1_1, NOT_MODIFIED); setDateHeader(response); // Close the connection as soon as the error message is sent. ctx.getChannel().write(response).addListener(ChannelFutureListener.CLOSE); } /** * Sets the Date header for the HTTP response * * @param response HTTP response */ private static void setDateHeader(HttpResponse response) { SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US); dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE)); Calendar time = new GregorianCalendar(); response.setHeader(DATE, dateFormatter.format(time.getTime())); } /** * Sets the Date and Cache headers for the HTTP Response * * @param response HTTP response * @param fileToCache file to extract content type */ private static void setDateAndCacheHeaders(HttpResponse response, File fileToCache) { SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US); dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE)); // Date header Calendar time = new GregorianCalendar(); response.setHeader(DATE, dateFormatter.format(time.getTime())); // Add cache headers time.add(Calendar.SECOND, HTTP_CACHE_SECONDS); response.setHeader(EXPIRES, dateFormatter.format(time.getTime())); response.setHeader(CACHE_CONTROL, "private, max-age=" + HTTP_CACHE_SECONDS); response.setHeader(LAST_MODIFIED, dateFormatter.format(new Date(fileToCache.lastModified()))); } }