Java tutorial
package com.ociweb.pronghorn.adapter.netty.impl; /* * Copyright 2012 The Netty Project * * The Netty Project licenses this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import io.netty.buffer.UnpooledByteBufAllocator; import io.netty.buffer.UnpooledHeapByteBuf; import io.netty.buffer.WrappedByteBuf; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelProgressiveFuture; import io.netty.channel.ChannelProgressiveFutureListener; import io.netty.channel.DefaultFileRegion; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.DefaultHttpResponse; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpChunkedInput; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.LastHttpContent; import io.netty.handler.ssl.SslHandler; import io.netty.handler.stream.ChunkedFile; import io.netty.handler.stream.ChunkedInput; import io.netty.handler.stream.ChunkedNioFile; import io.netty.handler.stream.ChunkedStream; import io.netty.util.CharsetUtil; import io.netty.util.internal.SystemPropertyUtil; import javax.activation.MimetypesFileTypeMap; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.Locale; import java.util.TimeZone; import java.util.regex.Pattern; import static io.netty.handler.codec.http.HttpMethod.*; import static io.netty.handler.codec.http.HttpResponseStatus.*; import static io.netty.handler.codec.http.HttpVersion.*; /** * A simple handler that serves incoming HTTP requests to send their respective * HTTP responses. It also implements {@code 'If-Modified-Since'} header to * take advantage of browser cache, as described in * <a href="http://tools.ietf.org/html/rfc2616#section-14.25">RFC 2616</a>. * * <h3>How Browser Caching Works</h3> * * Web browser caching works with HTTP headers as illustrated by the following * sample: * <ol> * <li>Request #1 returns the content of {@code /file1.txt}.</li> * <li>Contents of {@code /file1.txt} is cached by the browser.</li> * <li>Request #2 for {@code /file1.txt} does return the contents of the * file again. Rather, a 304 Not Modified is returned. This tells the * browser to use the contents stored in its cache.</li> * <li>The server knows the file has not been modified because the * {@code If-Modified-Since} date is the same as the file's last * modified date.</li> * </ol> * * <pre> * Request #1 Headers * =================== * GET /file1.txt HTTP/1.1 * * Response #1 Headers * =================== * HTTP/1.1 200 OK * Date: Tue, 01 Mar 2011 22:44:26 GMT * Last-Modified: Wed, 30 Jun 2010 21:36:48 GMT * Expires: Tue, 01 Mar 2012 22:44:26 GMT * Cache-Control: private, max-age=31536000 * * Request #2 Headers * =================== * GET /file1.txt HTTP/1.1 * If-Modified-Since: Wed, 30 Jun 2010 21:36:48 GMT * * Response #2 Headers * =================== * HTTP/1.1 304 Not Modified * Date: Tue, 01 Mar 2011 22:44:28 GMT * * </pre> */ public class HttpStaticFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> { private static final String INDEX_HTML = "index.html"; 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; @Override public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { if (!request.decoderResult().isSuccess()) { sendError(ctx, BAD_REQUEST); return; } if (request.method() != GET) { sendError(ctx, METHOD_NOT_ALLOWED); return; } final String uri = request.uri(); final String path = sanitizeUri(uri, SystemPropertyUtil.get("user.dir")); if (path == null) { sendError(ctx, FORBIDDEN); return; } File file = new File(path); if (file.isHidden() || !file.exists()) { return; } if (file.isDirectory()) { if (uri.endsWith("/")) { sendListing(ctx, file); } else { sendRedirect(ctx, uri + '/'); } return; } sendFile(ctx, request, file); } public static void sendFile(ChannelHandlerContext ctx, FullHttpRequest request, File file) throws ParseException, IOException { long lastModified = file.lastModified(); String path = file.getPath(); // Cache Validation String ifModifiedSince = request.headers().get(HttpHeaderNames.IF_MODIFIED_SINCE); if (ifModifiedSince != null && !ifModifiedSince.isEmpty()) { 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 = lastModified / 1000; if (ifModifiedSinceDateSeconds == fileLastModifiedSeconds) { sendNotModified(ctx); return; } } RandomAccessFile raf; try { raf = new RandomAccessFile(file, "r"); } catch (FileNotFoundException ignore) { sendError(ctx, NOT_FOUND); return; } long fileLength = file.length(); beginHTTPResponse(ctx, request, lastModified, path, fileLength); // Write the content. ChannelFuture sendFileFuture; ChannelFuture lastContentFuture; if (ctx.pipeline().get(SslHandler.class) == null) { sendFileFuture = ctx.write(new DefaultFileRegion(raf.getChannel(), 0, fileLength), ctx.newProgressivePromise()); // Write the end marker. lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); } else { FileInputStream input = new FileInputStream(file); byte[] data = new byte[(int) fileLength]; //big hack to convert into byteBuf int pos = 0; int remaining = (int) fileLength; while (remaining > 0) { int len = input.read(data, pos, remaining); remaining -= len; pos += len; } sendFileFuture = ctx.write(Unpooled.wrappedBuffer(data), ctx.newProgressivePromise()); lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); } progressAndClose(request, sendFileFuture, lastContentFuture); } public static void beginHTTPResponse(ChannelHandlerContext ctx, FullHttpRequest request, long lastModified, String path, long fileLength) { HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK); HttpUtil.setContentLength(response, fileLength); setContentTypeHeader(response, path); response.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED); setDateAndCacheHeaders(response, lastModified); if (HttpUtil.isKeepAlive(request)) { response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); } // Write the initial line and the header. ctx.write(response); } public static void progressAndClose(FullHttpRequest request, ChannelFuture sendFileFuture, ChannelFuture lastContentFuture) { sendFileFuture.addListener(new ChannelProgressiveFutureListener() { @Override public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) { if (total < 0) { // total unknown System.err.println(future.channel() + " Transfer progress: " + progress); } else { System.err.println(future.channel() + " Transfer progress: " + progress + " / " + total); } } @Override public void operationComplete(ChannelProgressiveFuture future) { System.err.println(future.channel() + " Transfer complete."); } }); // Decide whether to close the connection or not. if (!HttpUtil.isKeepAlive(request)) { // Close the connection when the whole content is written out. lastContentFuture.addListener(ChannelFutureListener.CLOSE); } } private static final long STARTUP_TIME = System.currentTimeMillis(); public static void sendResource(ChannelHandlerContext ctx, FullHttpRequest request, String name) throws ParseException, IOException { InputStream resourceAsStream = HttpStaticFileServerHandler.class.getResourceAsStream(name); if (null == resourceAsStream) { throw new FileNotFoundException("Unable to find resource: " + name); } else { System.err.println("found resource: " + name); } int fileLength = resourceAsStream.available(); // String path = file.getPath(); long lastModified = STARTUP_TIME - 3000; String path = name; // Cache Validation String ifModifiedSince = request.headers().get(HttpHeaderNames.IF_MODIFIED_SINCE); if (ifModifiedSince != null && !ifModifiedSince.isEmpty()) { 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 = lastModified / 1000; if (ifModifiedSinceDateSeconds == fileLastModifiedSeconds) { sendNotModified(ctx); return; } } beginHTTPResponse(ctx, request, lastModified, path, fileLength); // Write the content. ChannelFuture sendFileFuture; ChannelFuture lastContentFuture; byte[] data = new byte[(int) fileLength]; //big hack to convert into byteBuf int pos = 0; int remaining = fileLength; while (remaining > 0) { int len = resourceAsStream.read(data, pos, remaining); remaining -= len; pos += len; } resourceAsStream.close(); sendFileFuture = ctx.write(Unpooled.wrappedBuffer(data), ctx.newProgressivePromise()); lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); progressAndClose(request, sendFileFuture, lastContentFuture); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); if (ctx.channel().isActive()) { sendError(ctx, INTERNAL_SERVER_ERROR); } } private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*"); public static String sanitizeUri(String uri, String root) { // Decode the path. try { uri = URLDecoder.decode(uri, "UTF-8"); } catch (UnsupportedEncodingException e) { throw new Error(e); } if (uri.isEmpty() || uri.charAt(0) != '/') { return null; } // Convert file separators. uri = uri.replace('/', File.separatorChar); // Simplistic dumb security check. // You will have to do something serious in the production environment. if (uri.contains(File.separator + '.') || uri.contains('.' + File.separator) || uri.charAt(0) == '.' || uri.charAt(uri.length() - 1) == '.' || INSECURE_URI.matcher(uri).matches()) { return null; } // // Convert to absolute path. String separatorAndURI = uri.startsWith(File.separator) ? uri : (File.separator + uri); if (null == root) { if ("/".equals(uri)) { return separatorAndURI + INDEX_HTML; } else { return separatorAndURI; } } String originalPath = root + separatorAndURI; if ("/".equals(uri)) { String indexPath = originalPath + INDEX_HTML; if (new File(indexPath).exists()) { return indexPath; } else { System.out.println("did not find " + indexPath); } } return originalPath; } private static final Pattern ALLOWED_FILE_NAME = Pattern.compile("[A-Za-z0-9][-_A-Za-z0-9\\.]*"); private static void sendListing(ChannelHandlerContext ctx, File dir) { FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK); response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8"); String dirPath = dir.getPath(); StringBuilder buf = new StringBuilder().append("<!DOCTYPE html>\r\n").append("<html><head><title>") .append("Listing of: ").append(dirPath).append("</title></head><body>\r\n") .append("<h3>Listing of: ").append(dirPath).append("</h3>\r\n") .append("<ul>").append("<li><a href=\"../\">..</a></li>\r\n"); for (File f : dir.listFiles()) { if (f.isHidden() || !f.canRead()) { continue; } String name = f.getName(); if (!ALLOWED_FILE_NAME.matcher(name).matches()) { continue; } buf.append("<li><a href=\"").append(name).append("\">").append(name).append("</a></li>\r\n"); } buf.append("</ul></body></html>\r\n"); ByteBuf buffer = Unpooled.copiedBuffer(buf, CharsetUtil.UTF_8); response.content().writeBytes(buffer); buffer.release(); // Close the connection as soon as the error message is sent. ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); } private static void sendRedirect(ChannelHandlerContext ctx, String newUri) { FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, FOUND); response.headers().set(HttpHeaderNames.LOCATION, newUri); // Close the connection as soon as the error message is sent. ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); } private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) { FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, status, Unpooled.copiedBuffer("Failure: " + status + "\r\n", CharsetUtil.UTF_8)); response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8"); // Close the connection as soon as the error message is sent. ctx.writeAndFlush(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) { FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, NOT_MODIFIED); setDateHeader(response); // Close the connection as soon as the error message is sent. ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); } /** * Sets the Date header for the HTTP response * * @param response * HTTP response */ private static void setDateHeader(FullHttpResponse response) { SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US); dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE)); Calendar time = new GregorianCalendar(); response.headers().set(HttpHeaderNames.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, long lastModified) { SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US); dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE)); // Date header Calendar time = new GregorianCalendar(); response.headers().set(HttpHeaderNames.DATE, dateFormatter.format(time.getTime())); // Add cache headers time.add(Calendar.SECOND, HTTP_CACHE_SECONDS); response.headers().set(HttpHeaderNames.EXPIRES, dateFormatter.format(time.getTime())); response.headers().set(HttpHeaderNames.CACHE_CONTROL, "private, max-age=" + HTTP_CACHE_SECONDS); response.headers().set(HttpHeaderNames.LAST_MODIFIED, dateFormatter.format(new Date(lastModified))); } /** * Sets the content type header for the HTTP Response * * @param response * HTTP response * @param file * file to extract content type */ private static void setContentTypeHeader(HttpResponse response, String path) { MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap(); //////////////////////////////////////// //NOTE: this does not work and behaves different between Java releases!!!!! /////////////////////////////////////// String contentType = mimeTypesMap.getContentType(path); //patching mistake from the above broken code. if (path.endsWith("css")) { contentType = "text/css"; } else if (path.endsWith("html")) { contentType = "text/html"; } else if (path.endsWith("jpg")) { contentType = "image/jpeg"; } response.headers().set(HttpHeaderNames.CONTENT_TYPE, contentType); } }