All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.blade.server.netty.StaticFileHandler Maven / Gradle / Ivy

/**
 * Copyright (c) 2017, biezhi 王爵 nice ([email protected])
 * 

* Licensed 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. */ package com.blade.server.netty; import com.blade.Blade; import com.blade.exception.ForbiddenException; import com.blade.exception.NotFoundException; import com.blade.kit.*; import com.blade.mvc.Const; import com.blade.mvc.WebContext; import com.blade.mvc.handler.RequestHandler; import com.blade.mvc.http.Request; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; 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.*; import io.netty.handler.ssl.SslHandler; import io.netty.handler.stream.ChunkedFile; import io.netty.util.CharsetUtil; import lombok.extern.slf4j.Slf4j; import lombok.var; import java.io.*; import java.net.URLConnection; import java.net.URLDecoder; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.Date; import java.util.Locale; import java.util.Objects; import java.util.regex.Pattern; import static com.blade.kit.BladeKit.*; import static io.netty.handler.codec.http.HttpResponseStatus.*; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; /** * static file handler * * @author biezhi * 2017/5/31 */ @Slf4j public class StaticFileHandler implements RequestHandler { private static final String STYLE = "body{background:#fff;margin:0;padding:30px;-webkit-font-smoothing:antialiased;font-family:Menlo,Consolas,monospace}main{max-width:920px}header{display:flex;justify-content:space-between}#toggle{display:none;cursor:pointer}#toggle:before{display:inline-block;content:url(\"data:image/svg+xml; utf8, \")}#toggle.single-column:before{content:url(\"data:image/svg+xml; utf8, \")}a{color:#1A00F2;text-decoration:none}h1{font-size:18px;font-weight:500;margin-top:0;color:#000;font-family:-apple-system,Helvetica;display:flex}h1 a{color:inherit;font-weight:700;border-bottom:1px dashed transparent}h1 a::after{content:'/'}h1 a:hover{color:#7d7d7d}h1 i{font-style:normal}ul{margin:0;padding:20px 0 0 0}ul.single-column{flex-direction:column}ul li{list-style:none;padding:10px 0;font-size:14px;display:flex;justify-content:space-between}ul li i{color:#9B9B9B;font-size:11px;display:block;font-style:normal;white-space:nowrap;padding-left:15px}ul a{color:#1A00F2;white-space:nowrap;overflow:hidden;display:block;text-overflow:ellipsis}ul a::before{content:url(\"data:image/svg+xml; utf8, \");display:inline-block;vertical-align:middle;margin-right:10px}ul a:hover{color:#000}ul a[class=''] + i{display:none}ul a[class='']::before{content:url(\"data:image/svg+xml; utf8, \")}ul a[class='gif']::before,ul a[class='jpg']::before,ul a[class='png']::before,ul a[class='svg']::before{content:url(\"data:image/svg+xml; utf8, \");width:16px}@media (min-width:768px){#toggle{display:inline-block}ul{display:flex;flex-wrap:wrap}ul li{width:230px;padding-right:20px}ul.single-column li{width:auto}}@media (min-width:992px){body{padding:45px}h1{font-size:15px}ul li{font-size:13px;box-sizing:border-box;justify-content:flex-start}ul li:hover i{opacity:1}ul li i{font-size:10px;opacity:0;margin-left:10px;margin-top:3px;padding-left:0}}"; private boolean showFileList; /** * default cache 30 days. */ private final long HTTP_CACHE_SECONDS; public StaticFileHandler(Blade blade) { this.showFileList = blade.environment().getBoolean(Const.ENV_KEY_STATIC_LIST, false); this.HTTP_CACHE_SECONDS = blade.environment().getLong(Const.ENV_KEY_HTTP_CACHE_TIMEOUT, 86400 * 30); } /** * print static file to client * * @param webContext web context * @throws Exception */ @Override public void handle(WebContext webContext) throws Exception { Request request = webContext.getRequest(); ChannelHandlerContext ctx = webContext.getChannelHandlerContext(); if (!HttpConst.METHOD_GET.equals(request.method())) { sendError(ctx, METHOD_NOT_ALLOWED); return; } String uri = URLDecoder.decode(request.uri(), "UTF-8"); Instant start = Instant.now(); String cleanUri = PathKit.cleanPath(uri.replaceFirst(request.contextPath(), "/")); String method = StringKit.padRight(request.method(), 6); // webjars if (cleanUri.startsWith(Const.WEB_JARS)) { InputStream input = StaticFileHandler.class.getResourceAsStream("/META-INF/resources" + uri); if (null == input) { log404(log, method, uri); throw new NotFoundException(uri); } if (writeJarResource(ctx, request, cleanUri, input)) { log200AndCost(log, start, method, uri); } return; } // jar file if (BladeKit.isInJar()) { InputStream input = StaticFileHandler.class.getResourceAsStream(cleanUri); if (null == input) { log404(log, method, uri); throw new NotFoundException(uri); } if (writeJarResource(ctx, request, cleanUri, input)) { log200AndCost(log, start, method, uri); } return; } // disk file final String path = sanitizeUri(cleanUri); if (path == null) { log403(log, method, uri); throw new ForbiddenException(); } File file = new File(path); if (file.isHidden() || !file.exists()) { // gradle resources path File resourcesDirectory = new File(new File(Const.class.getResource("/").getPath()).getParent() + "/resources"); if (resourcesDirectory.isDirectory()) { file = new File(resourcesDirectory.getPath() + "/" + cleanUri.substring(1)); if (file.isHidden() || !file.exists()) { log404(log, method, uri); throw new NotFoundException(uri); } } else { log404(log, method, uri); throw new NotFoundException(uri); } } if (file.isDirectory() && showFileList) { sendListing(ctx, uri, file, cleanUri); return; } if (!file.isFile()) { sendError(ctx, FORBIDDEN); return; } // Cache Validation if (isHttp304(ctx, request, file.length(), file.lastModified())) { log304(log, method, uri); return; } HttpResponse httpResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); setContentTypeHeader(httpResponse, file); setDateAndCacheHeaders(httpResponse, file); if (request.useGZIP()) { File output = new File(file.getPath() + ".gz"); IOKit.compressGZIP(file, output); file = output; setGzip(httpResponse); } RandomAccessFile raf; try { raf = new RandomAccessFile(file, "r"); } catch (FileNotFoundException ignore) { sendError(ctx, NOT_FOUND); return; } long fileLength = raf.length(); httpResponse.headers().set(HttpConst.CONTENT_LENGTH, fileLength); if (request.keepAlive()) { httpResponse.headers().set(HttpConst.CONNECTION, HttpConst.KEEP_ALIVE); } // Write the initial line and the header. ctx.write(httpResponse); // 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 { sendFileFuture = ctx.writeAndFlush( new HttpChunkedInput(new ChunkedFile(raf, 0, fileLength, 8192)), ctx.newProgressivePromise()); // HttpChunkedInput will write the end marker (LastHttpContent) for us. lastContentFuture = sendFileFuture; } sendFileFuture.addListener(ProgressiveFutureListener.build(raf)); // Decide whether to close the connection or not. if (!request.keepAlive()) { lastContentFuture.addListener(ChannelFutureListener.CLOSE); } log200AndCost(log, start, method, uri); } private boolean writeJarResource(ChannelHandlerContext ctx, Request request, String uri, InputStream input) throws IOException { var staticInputStream = new StaticInputStream(input); int size = staticInputStream.size(); if (isHttp304(ctx, request, size, -1)) { log304(log, request.method(), uri); return false; } FullHttpResponse httpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, OK, staticInputStream.asByteBuf()); setDateAndCacheHeaders(httpResponse, null); String contentType = StringKit.mimeType(uri); if (null != contentType) { httpResponse.headers().set(HttpConst.CONTENT_TYPE, contentType); } httpResponse.headers().set(HttpConst.CONTENT_LENGTH, size); if (request.keepAlive()) { httpResponse.headers().set(HttpConst.CONNECTION, HttpConst.KEEP_ALIVE); } // Write the initial line and the header. ctx.writeAndFlush(httpResponse); return true; } private boolean isHttp304(ChannelHandlerContext ctx, Request request, long size, long lastModified) { String ifModifiedSince = request.header(HttpConst.IF_MODIFIED_SINCE); if (StringKit.isNotEmpty(ifModifiedSince) && HTTP_CACHE_SECONDS > 0) { Date ifModifiedSinceDate = format(ifModifiedSince, Const.HTTP_DATE_FORMAT); // Only compare up to the second because the datetime format we send to the client // does not have milliseconds long ifModifiedSinceDateSeconds = ifModifiedSinceDate.getTime() / 1000; if (ifModifiedSinceDateSeconds == lastModified / 1000) { FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, NOT_MODIFIED); String contentType = StringKit.mimeType(request.uri()); if (null != contentType) { response.headers().set(HttpConst.CONTENT_TYPE, contentType); } response.headers().set(HttpConst.DATE, DateKit.gmtDate()); response.headers().set(HttpConst.CONTENT_LENGTH, size); if (request.keepAlive()) { response.headers().set(HttpConst.CONNECTION, HttpConst.KEEP_ALIVE); } // Close the connection as soon as the error message is sent. ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); return true; } } return false; } public Date format(String date, String pattern) { DateTimeFormatter fmt = DateTimeFormatter.ofPattern(pattern, Locale.US); LocalDateTime formatted = LocalDateTime.parse(date, fmt); Instant instant = formatted.atZone(ZoneId.systemDefault()).toInstant(); return Date.from(instant); } /** * Sets the Date and Cache headers for the HTTP Response * * @param response HTTP response * @param fileToCache file to extract content type */ private void setDateAndCacheHeaders(HttpResponse response, File fileToCache) { response.headers().set(HttpConst.DATE, DateKit.gmtDate()); // Add cache headers if (HTTP_CACHE_SECONDS > 0) { response.headers().set(HttpConst.EXPIRES, DateKit.gmtDate(LocalDateTime.now().plusSeconds(HTTP_CACHE_SECONDS))); response.headers().set(HttpConst.CACHE_CONTROL, "private, max-age=" + HTTP_CACHE_SECONDS); if (null != fileToCache) { response.headers().set(HttpConst.LAST_MODIFIED, DateKit.gmtDate(new Date(fileToCache.lastModified()))); } else { response.headers().set(HttpConst.LAST_MODIFIED, DateKit.gmtDate(LocalDateTime.now().plusDays(-1))); } } } private static final Pattern ALLOWED_FILE_NAME = Pattern.compile("[^-._]?[^<>&\"]*"); private static void sendListing(ChannelHandlerContext ctx, String uri, File dir, String dirPath) { var response = new DefaultFullHttpResponse(HTTP_1_1, OK); response.headers().set(HttpConst.CONTENT_TYPE, "text/html; charset=UTF-8"); StringBuilder buf = new StringBuilder() .append("\r\n") .append("") .append("") .append("") .append("") .append("Files within: ") .append(dirPath) .append("") .append("

Index of "); String[] dirs = uri.split("/"); for (String s : dirs) { if (StringKit.isEmpty(s)) { continue; } String path = uri.substring(0, uri.indexOf(s) + s.length()); buf.append("" + s + ""); } buf.append("

"); buf.append(""); buf.append("
"); buf.append(""); buf.append("
"); buf.append(""); buf.append(""); 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 sendError(ChannelHandlerContext ctx, HttpResponseStatus status) { var response = new DefaultFullHttpResponse(HTTP_1_1, status, Unpooled.copiedBuffer("Failure: " + status + "\r\n", CharsetUtil.UTF_8)); response.headers().set(HttpConst.CONTENT_TYPE, Const.CONTENT_TYPE_TEXT); // Close the connection as soon as the error message is sent. ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); } private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*"); private static String sanitizeUri(String uri) { if (uri.isEmpty() || uri.charAt(0) != HttpConst.CHAR_SLASH) { return null; } // Convert file separators. uri = uri.replace(HttpConst.CHAR_SLASH, File.separatorChar); // Simplistic dumb security check. // You will have to do something serious in the production environment. if (uri.contains(File.separator + HttpConst.CHAR_POINT) || uri.contains('.' + File.separator) || uri.charAt(0) == '.' || uri.charAt(uri.length() - 1) == '.' || INSECURE_URI.matcher(uri).matches()) { return null; } // Maven resources path String path = Const.CLASSPATH + File.separator + uri.substring(1); return path.replace("//", "/"); } /** * 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, File file) { String contentType = StringKit.mimeType(file.getName()); if (null == contentType) { contentType = URLConnection.guessContentTypeFromName(file.getName()); } response.headers().set(HttpConst.CONTENT_TYPE, contentType); } private void setGzip(HttpResponse response) { response.headers().set(HttpConst.CONTENT_ENCODING, "gzip"); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy