mirror of
https://gitee.com/arthas/arthas.git
synced 2024-11-30 03:07:37 +08:00
parent
d4e54cd2f2
commit
285b73d624
@ -3,17 +3,40 @@ package com.taobao.arthas.core.shell.term.impl.http;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import com.alibaba.arthas.deps.org.slf4j.Logger;
|
||||
import com.alibaba.arthas.deps.org.slf4j.LoggerFactory;
|
||||
import com.taobao.arthas.common.IOUtils;
|
||||
|
||||
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.handler.codec.http.DefaultFullHttpResponse;
|
||||
import io.netty.handler.codec.http.DefaultHttpResponse;
|
||||
import io.netty.handler.codec.http.FullHttpRequest;
|
||||
import io.netty.handler.codec.http.HttpChunkedInput;
|
||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||
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.HttpUtil;
|
||||
import io.netty.handler.codec.http.HttpVersion;
|
||||
import io.netty.handler.codec.http.LastHttpContent;
|
||||
import io.netty.handler.ssl.SslHandler;
|
||||
import io.netty.handler.stream.ChunkedFile;
|
||||
|
||||
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
|
||||
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
|
||||
|
||||
/**
|
||||
*
|
||||
@ -22,6 +45,10 @@ import io.netty.handler.codec.http.HttpVersion;
|
||||
*/
|
||||
public class DirectoryBrowser {
|
||||
|
||||
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 long MIN_NETTY_DIRECT_SEND_SIZE = 10 * 1024 * 1024;
|
||||
private static final Logger logger = LoggerFactory.getLogger(DirectoryBrowser.class);
|
||||
//@formatter:off
|
||||
private static String pageHeader = "<!DOCTYPE html>\n" +
|
||||
"<html>\n" +
|
||||
@ -108,14 +135,24 @@ public class DirectoryBrowser {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static DefaultFullHttpResponse view(File dir, String path, HttpVersion version) throws IOException {
|
||||
/**
|
||||
* write data here,still return not null just to know succeeded.
|
||||
* @param dir
|
||||
* @param path
|
||||
* @param request
|
||||
* @param ctx
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
public static DefaultFullHttpResponse directView(File dir, String path, FullHttpRequest request, ChannelHandlerContext ctx) throws IOException {
|
||||
if (path.startsWith("/")) {
|
||||
path = path.substring(1, path.length());
|
||||
}
|
||||
|
||||
// path maybe: arthas-output/20201225-203454.svg
|
||||
// 需要取 dir的parent来去掉前缀
|
||||
File file = new File(dir.getParent(), path);
|
||||
|
||||
HttpVersion version = request.protocolVersion();
|
||||
if (isSubFile(dir, file)) {
|
||||
DefaultFullHttpResponse fullResp = new DefaultFullHttpResponse(version, HttpResponseStatus.OK);
|
||||
|
||||
@ -127,22 +164,112 @@ public class DirectoryBrowser {
|
||||
String renderResult = renderDir(file);
|
||||
fullResp.content().writeBytes(renderResult.getBytes("utf-8"));
|
||||
fullResp.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=utf-8");
|
||||
ctx.write(fullResp);
|
||||
ChannelFuture future = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
|
||||
future.addListener(ChannelFutureListener.CLOSE);
|
||||
return fullResp;
|
||||
} else {
|
||||
FileInputStream fileInputStream = new FileInputStream(file);
|
||||
try {
|
||||
byte[] content = IOUtils.getBytes(fileInputStream);
|
||||
fullResp.content().writeBytes(content);
|
||||
HttpUtil.setContentLength(fullResp, fullResp.content().readableBytes());
|
||||
} finally {
|
||||
IOUtils.close(fileInputStream);
|
||||
logger.info("get file now. file:" + file.getPath());
|
||||
if (file.isHidden() || !file.exists() || file.isDirectory() || !file.isFile()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
RandomAccessFile raf;
|
||||
try {
|
||||
raf = new RandomAccessFile(file, "r");
|
||||
} catch (Exception ignore) {
|
||||
return null;
|
||||
}
|
||||
long fileLength = raf.length();
|
||||
if (fileLength < MIN_NETTY_DIRECT_SEND_SIZE){
|
||||
FileInputStream fileInputStream = new FileInputStream(file);
|
||||
try {
|
||||
byte[] content = IOUtils.getBytes(fileInputStream);
|
||||
fullResp.content().writeBytes(content);
|
||||
HttpUtil.setContentLength(fullResp, fullResp.content().readableBytes());
|
||||
} finally {
|
||||
IOUtils.close(fileInputStream);
|
||||
}
|
||||
ctx.write(fullResp);
|
||||
ChannelFuture future = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
|
||||
future.addListener(ChannelFutureListener.CLOSE);
|
||||
return fullResp;
|
||||
}
|
||||
logger.info("file {} size bigger than 10MB, send by future.",file.getName());
|
||||
HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);
|
||||
HttpUtil.setContentLength(response, fileLength);
|
||||
setContentTypeHeader(response, file);
|
||||
setDateAndCacheHeaders(response, file);
|
||||
if (HttpUtil.isKeepAlive(request)) {
|
||||
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
|
||||
}
|
||||
|
||||
// Write the initial line and the header.
|
||||
ctx.write(response);
|
||||
// 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(new ChannelProgressiveFutureListener() {
|
||||
@Override
|
||||
public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) {
|
||||
if (total < 0) { // total unknown
|
||||
logger.info(future.channel() + " Transfer progress: " + progress);
|
||||
} else {
|
||||
logger.info(future.channel() + " Transfer progress: " + progress + " / " + total);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void operationComplete(ChannelProgressiveFuture future) {
|
||||
logger.info(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);
|
||||
}
|
||||
return fullResp;
|
||||
}
|
||||
return fullResp;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
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.headers().set(HttpHeaderNames.DATE, dateFormatter.format(time.getTime()));
|
||||
|
||||
// Add cache headers
|
||||
time.add(Calendar.SECOND, 3600);
|
||||
response.headers().set(HttpHeaderNames.EXPIRES, dateFormatter.format(time.getTime()));
|
||||
response.headers().set(HttpHeaderNames.CACHE_CONTROL, "private, max-age=" + 3600);
|
||||
response.headers().set(
|
||||
HttpHeaderNames.LAST_MODIFIED, dateFormatter.format(new Date(fileToCache.lastModified())));
|
||||
}
|
||||
|
||||
private static void setContentTypeHeader(HttpResponse response, File file) {
|
||||
String contentType = "application/octet-stream";
|
||||
// 暂时hardcode 大文件的content-type
|
||||
response.headers().set(HttpHeaderNames.CONTENT_TYPE, contentType);
|
||||
}
|
||||
public static boolean isSubFile(File parent, File child) throws IOException {
|
||||
String parentPath = parent.getCanonicalPath();
|
||||
String childPath = child.getCanonicalPath();
|
||||
|
@ -1,18 +1,11 @@
|
||||
package com.taobao.arthas.core.shell.term.impl.http;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
|
||||
import com.alibaba.arthas.deps.org.slf4j.Logger;
|
||||
import com.alibaba.arthas.deps.org.slf4j.LoggerFactory;
|
||||
import com.taobao.arthas.common.IOUtils;
|
||||
import com.taobao.arthas.core.server.ArthasBootstrap;
|
||||
import com.taobao.arthas.core.shell.term.impl.http.api.HttpApiHandler;
|
||||
import com.taobao.arthas.core.shell.term.impl.httptelnet.HttpTelnetTermServer;
|
||||
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
@ -24,13 +17,19 @@ import io.netty.handler.codec.http.HttpHeaderNames;
|
||||
import io.netty.handler.codec.http.HttpResponse;
|
||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||
import io.netty.handler.codec.http.HttpUtil;
|
||||
import io.netty.handler.codec.http.HttpVersion;
|
||||
import io.netty.handler.codec.http.LastHttpContent;
|
||||
import io.termd.core.http.HttpTtyConnection;
|
||||
import io.termd.core.util.Logging;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
|
||||
import static com.taobao.arthas.core.util.HttpUtils.createRedirectResponse;
|
||||
import static com.taobao.arthas.core.util.HttpUtils.createResponse;
|
||||
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:julien@julienviet.com">Julien Viet</a>
|
||||
@ -46,6 +45,7 @@ public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequ
|
||||
|
||||
private HttpApiHandler httpApiHandler;
|
||||
|
||||
|
||||
public HttpRequestHandler(String wsUri) {
|
||||
this(wsUri, ArthasBootstrap.getInstance().getOutputPath());
|
||||
}
|
||||
@ -73,6 +73,7 @@ public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequ
|
||||
}
|
||||
|
||||
boolean isHttpApiResponse = false;
|
||||
boolean isFileResponseFinished = false;
|
||||
try {
|
||||
//handle http restful api
|
||||
if ("/api".equals(path)) {
|
||||
@ -95,7 +96,8 @@ public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequ
|
||||
|
||||
//try output dir later, avoid overlay classpath resources files
|
||||
if (response == null){
|
||||
response = DirectoryBrowser.view(dir, path, request.protocolVersion());
|
||||
response = DirectoryBrowser.directView(dir, path, request,ctx);
|
||||
isFileResponseFinished = (response == null) ? false : true;
|
||||
}
|
||||
|
||||
//not found
|
||||
@ -109,25 +111,26 @@ public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequ
|
||||
if (response == null){
|
||||
response = createResponse(request, HttpResponseStatus.INTERNAL_SERVER_ERROR, "Server error");
|
||||
}
|
||||
ctx.write(response);
|
||||
ChannelFuture future = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
|
||||
future.addListener(ChannelFutureListener.CLOSE);
|
||||
|
||||
//reuse http api response buf
|
||||
if (isHttpApiResponse && response instanceof DefaultFullHttpResponse) {
|
||||
final HttpResponse finalResponse = response;
|
||||
future.addListener(new ChannelFutureListener() {
|
||||
@Override
|
||||
public void operationComplete(ChannelFuture future) throws Exception {
|
||||
httpApiHandler.onCompleted((DefaultFullHttpResponse) finalResponse);
|
||||
}
|
||||
});
|
||||
if(!isFileResponseFinished) {
|
||||
ctx.write(response);
|
||||
ChannelFuture future = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
|
||||
future.addListener(ChannelFutureListener.CLOSE);
|
||||
//reuse http api response buf
|
||||
if (isHttpApiResponse && response instanceof DefaultFullHttpResponse) {
|
||||
final HttpResponse finalResponse = response;
|
||||
future.addListener(new ChannelFutureListener() {
|
||||
@Override
|
||||
public void operationComplete(ChannelFuture future) throws Exception {
|
||||
httpApiHandler.onCompleted((DefaultFullHttpResponse) finalResponse);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private FullHttpResponse readFileFromResource(FullHttpRequest request, String path) throws IOException {
|
||||
private HttpResponse readFileFromResource(FullHttpRequest request, String path) throws IOException {
|
||||
DefaultFullHttpResponse fullResp = null;
|
||||
InputStream in = null;
|
||||
try {
|
||||
@ -166,7 +169,7 @@ public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequ
|
||||
}
|
||||
|
||||
private static void send100Continue(ChannelHandlerContext ctx) {
|
||||
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
|
||||
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.CONTINUE);
|
||||
ctx.writeAndFlush(response);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user