package org.caudexorigo.http.netty4;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.DefaultFileRegion;
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.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
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.LastHttpContent;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.stream.ChunkedFile;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.URI;
import java.nio.file.Files;
import java.util.Date;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.caudexorigo.text.UrlCodec;
public class StaticFileAction extends HttpAction
{
private final File rootDirectory;
private final String rootDirectoryPath;
private final long cacheDuration;
public StaticFileAction(URI rootPath)
{
this(rootPath, 0);
}
public StaticFileAction(URI rootPath, long cacheDuration)
{
super();
this.rootDirectory = new File(rootPath);
this.rootDirectoryPath = rootDirectory.getAbsolutePath();
this.cacheDuration = cacheDuration;
if (!rootDirectory.isDirectory() || !rootDirectory.canRead() || rootDirectory.isHidden())
{
throw new IllegalArgumentException("Not a valid root directory");
}
}
@Override
public void service(ChannelHandlerContext ctx, FullHttpRequest request, FullHttpResponse rsp)
{
validateRequest(request);
File file = getFile(request);
long clen = file.length();
HttpResponse response = new DefaultHttpResponse(rsp.getProtocolVersion(), rsp.getStatus(), false);
Set<String> previousHeaders = rsp.headers().names();
for (String hname : previousHeaders)
{
response.headers().add(hname, rsp.headers().get(hname));
}
CharSequence ctype = getMimeType(request, file);
if (StringUtils.isNotBlank(ctype))
{
response.headers().set(HttpHeaders.Names.CONTENT_TYPE, ctype);
}
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);
response.headers().set(HttpHeaders.Names.DATE, HttpDateFormat.getCurrentHttpDate());
if (cacheDuration > 0)
{
response.headers().set(HttpHeaders.Names.CACHE_CONTROL, String.format("max-age=%s", cacheDuration));
response.headers().set(HttpHeaders.Names.LAST_MODIFIED, HttpDateFormat.getHttpDate(new Date(file.lastModified())));
response.headers().set(HttpHeaders.Names.VARY, "Accept-Encoding");
}
String contentEncoding = getContentEncoding(request, file);
if (StringUtils.isNotBlank(contentEncoding))
{
response.headers().set(HttpHeaders.Names.CONTENT_ENCODING, contentEncoding);
}
boolean is_keep_alive = HttpHeaders.isKeepAlive(request);
try
{
if (request.getMethod() == HttpMethod.GET)
{
response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, Long.toString(clen));
rsp.headers().set(response.headers());
rsp.setStatus(rsp.getStatus());
ctx.write(response);
boolean useSsl = (ctx.pipeline().get(SslHandler.class) != null);
if (useSsl)
{
is_keep_alive = false;
ctx.write(new ChunkedFile(raf, 0, clen, 8192 * 2), ctx.voidPromise());
}
else
{
ctx.write(new DefaultFileRegion(raf.getChannel(), 0, clen), ctx.voidPromise());
}
}
else if (request.getMethod() == HttpMethod.HEAD)
{
response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, "0");
rsp.headers().set(response.headers());
rsp.setStatus(rsp.getStatus());
ctx.write(response);
}
}
catch (IOException e)
{
throw new RuntimeException(e);
}
// Write the end marker
ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
// Decide whether to close the connection or not.
if (!is_keep_alive)
{
// Close the connection when the whole content is written out.
lastContentFuture.addListener(ChannelFutureListener.CLOSE);
}
}
protected CharSequence getMimeType(HttpRequest request, File file)
{
try
{
// String req_path = StringUtils.substringBefore(request.getUri(),
// "?");
// String abs_path = getFileAbsolutePath(file);
String ctype = Files.probeContentType(file.toPath());
if (StringUtils.isNotBlank(ctype))
{
return ctype;
}
else
{
return MimeTable.getContentType(file.getPath());
}
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
// this method serves as optional overload hook
public String getContentEncoding(HttpRequest req, File file)
{
return null;
}
protected String getFileAbsolutePath(File file)
{
String abs_path = file.getAbsolutePath();
if (!file.getAbsolutePath().startsWith(rootDirectoryPath))
{
throw new WebException(new IllegalArgumentException("Forbidden"), HttpResponseStatus.FORBIDDEN.code());
}
return abs_path;
}
protected File getFile(FullHttpRequest request)
{
String req_path = StringUtils.substringBefore(request.getUri(), "?");
return getFile(req_path);
}
protected File getFile(String req_path)
{
String fs_path = sanitizePath(req_path);
if (fs_path == null)
{
throw new WebException(new IllegalArgumentException("Forbidden"), HttpResponseStatus.FORBIDDEN.code());
}
File file = new File(fs_path);
validateFile(file, req_path);
return file;
}
protected void validateFile(File file, String path)
{
if (file.isHidden() || !file.exists())
{
throw new WebException(new FileNotFoundException(String.format("File not found: '%s'", path)), HttpResponseStatus.NOT_FOUND.code());
}
if (!file.isFile())
{
throw new WebException(new IllegalArgumentException(), HttpResponseStatus.FORBIDDEN.code());
}
}
protected void validateRequest(FullHttpRequest request)
{
if (!((request.getMethod() == HttpMethod.GET) || (request.getMethod() == HttpMethod.HEAD)))
{
throw new WebException(new IllegalArgumentException("Method not allowed"), HttpResponseStatus.METHOD_NOT_ALLOWED.code());
}
if (HttpHeaders.isTransferEncodingChunked(request))
{
throw new WebException(new IllegalArgumentException("Bad request"), HttpResponseStatus.BAD_REQUEST.code());
}
}
private String sanitizePath(String req_path)
{
if (StringUtils.isBlank(req_path))
{
return null;
}
// Decode the path.
try
{
req_path = UrlCodec.decode(req_path, "ISO-8859-1");
}
catch (Throwable e)
{
req_path = UrlCodec.decode(req_path, "UTF-8");
}
// Convert file separators.
req_path = req_path.replace('/', File.separatorChar);
// Simplistic dumb security check.
// You will have to do something serious in the production environment.
if (req_path.contains(File.separator + ".") || req_path.contains("." + File.separator) || req_path.startsWith(".") || req_path.endsWith("."))
{
return null;
}
// Convert to absolute path.
return rootDirectoryPath + File.separator + req_path;
}
@Override
protected boolean isZeroCopy()
{
return true;
}
}