package org.caudexorigo.http.netty; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; import java.io.UnsupportedEncodingException; import java.net.URI; import java.util.Date; import org.apache.commons.lang3.StringUtils; import org.caudexorigo.http.netty.reporting.ResponseFormatter; import org.caudexorigo.http.netty.reporting.StandardResponseFormatter; import org.caudexorigo.text.UrlCodec; import org.jboss.netty.buffer.ChannelBuffer; 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.ChannelHandlerContext; import org.jboss.netty.channel.DefaultFileRegion; import org.jboss.netty.channel.FileRegion; import org.jboss.netty.handler.codec.http.HttpHeaders; 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 org.jboss.netty.handler.ssl.SslHandler; public class StaticFileAction extends HttpAction { private final File rootDirectory; private final String rootDirectoryPath; private final long cacheAge; private ResponseFormatter rspFmt; public StaticFileAction(URI root_path) { this(root_path, new StandardResponseFormatter(false), 0); } public StaticFileAction(URI root_path, ResponseFormatter rspFmt) { this(root_path, rspFmt, 0); } public StaticFileAction(URI root_path, ResponseFormatter rspFmt, long cache_age) { rootDirectory = new File(root_path); rootDirectoryPath = rootDirectory.getAbsolutePath(); this.rspFmt = rspFmt; this.cacheAge = cache_age; if (!rootDirectory.isDirectory() || !rootDirectory.canRead() || rootDirectory.isHidden()) { throw new IllegalArgumentException("Not a valid root directory"); } } @Override public void service(ChannelHandlerContext ctx, HttpRequest request, HttpResponse response) { validateRequest(request, response); File file = getFile(request, response); validateFile(response, file, request.getUri()); String abs_path = getFileAbsolutePath(response, file); response.headers().set(HttpHeaders.Names.DATE, HttpDateFormat.getCurrentHttpDate()); response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, Long.toString(file.length())); String ctype = MimeTable.getContentType(abs_path); if (StringUtils.isNotBlank(ctype)) { response.headers().set(HttpHeaders.Names.CONTENT_TYPE, ctype); } if (StringUtils.isNotBlank(getContentEncoding())) { response.headers().set(HttpHeaders.Names.CONTENT_ENCODING, getContentEncoding()); } RandomAccessFile raf = null; try { raf = new RandomAccessFile(file, "r"); } catch (FileNotFoundException e1) { // this exception can be ignored because we already tested for it } response.setStatus(HttpResponseStatus.OK); if (cacheAge > 0) { response.headers().set(HttpHeaders.Names.CACHE_CONTROL, String.format("max-age=%s", cacheAge)); response.headers().set(HttpHeaders.Names.LAST_MODIFIED, HttpDateFormat.getHttpDate(new Date(file.lastModified()))); } Channel channel = ctx.getChannel(); boolean is_secure = channel.getPipeline().get(SslHandler.class) != null; boolean is_keep_alive = HttpHeaders.isKeepAlive(request); if (is_secure) { is_keep_alive = false; } if (is_keep_alive) { response.headers().set(HttpHeaders.Names.CONNECTION, "Keep-Alive"); } channel.write(response); // Write the content. ChannelFuture writeFuture; try { if (is_secure) { // Cannot use zero-copy with HTTPS // (http://www.jboss.org/file-access/default/members/netty/freezone/xref/3.2/org/jboss/netty/example/http/file/HttpStaticFileServerHandler.html) // writeFuture = channel.write(new ChunkedFile(raf, 0, raf.length(), 8192)); ChannelBuffer cb = ChannelBuffers.directBuffer((int) raf.length()); int b; while ((b = raf.read()) != -1) { cb.writeByte(b); } writeFuture = channel.write(cb); raf.close(); } else { final FileRegion region = new DefaultFileRegion(raf.getChannel(), 0, raf.length()); writeFuture = channel.write(region); writeFuture.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture cf) throws Exception { region.releaseExternalResources(); } }); } } catch (IOException e) { throw new RuntimeException(e); } if (!is_keep_alive) { writeFuture.addListener(ChannelFutureListener.CLOSE); } } @Override protected ResponseFormatter getResponseFormatter() { return this.rspFmt; } // this method serves as optional overload hook public String getContentEncoding() { return null; } protected String getFileAbsolutePath(HttpResponse response, File file) { String abs_path = file.getAbsolutePath(); if (!file.getAbsolutePath().startsWith(rootDirectoryPath)) { response.setStatus(HttpResponseStatus.FORBIDDEN); throw new IllegalArgumentException("Forbidden"); } return abs_path; } protected File getFile(HttpRequest request, HttpResponse response) { String path = sanitizePath(request.getUri()); if (path == null) { response.setStatus(HttpResponseStatus.FORBIDDEN); throw new IllegalArgumentException("Forbidden"); } File file = new File(path); return file; } protected void validateFile(HttpResponse response, File file, String path) { if (file.isHidden() || !file.exists()) { throw new WebException(new FileNotFoundException(String.format("File not found: '%s'", path)), HttpResponseStatus.NOT_FOUND.getCode()); } if (!file.isFile()) { throw new WebException(new IllegalArgumentException("Forbidden"), HttpResponseStatus.FORBIDDEN.getCode()); } } protected void validateRequest(HttpRequest request, HttpResponse response) { if (request.getMethod() != HttpMethod.GET) { throw new WebException(new IllegalArgumentException("Method not allowed"), HttpResponseStatus.METHOD_NOT_ALLOWED.getCode()); } if (request.isChunked()) { throw new WebException(new IllegalArgumentException("Bad request"), HttpResponseStatus.BAD_REQUEST.getCode()); } } private String sanitizePath(String spath) { String path = StringUtils.substringBefore(spath, "?"); if (StringUtils.isBlank(path)) { return null; } // Decode the path. try { path = UrlCodec.decode(path, "ISO-8859-1"); } catch (Throwable e) { path = UrlCodec.decode(path, "UTF-8"); } // Convert file separators. path = path.replace('/', File.separatorChar); // Simplistic dumb security check. // You will have to do something serious in the production environment. if (path.contains(File.separator + ".") || path.contains("." + File.separator) || path.startsWith(".") || path.endsWith(".")) { return null; } // Convert to absolute path. return rootDirectoryPath + File.separator + path; } }