supoort http visit arthas-output directory. #921

This commit is contained in:
hengyunabc
2019-11-10 00:20:53 +08:00
parent 7ccbf12bdb
commit b10bb1089b
4 changed files with 295 additions and 10 deletions
@@ -9,7 +9,6 @@ import com.taobao.arthas.core.shell.command.CommandResolver;
import com.taobao.arthas.core.shell.handlers.BindHandler;
import com.taobao.arthas.core.shell.impl.ShellServerImpl;
import com.taobao.arthas.core.shell.term.impl.HttpTermServer;
import com.taobao.arthas.core.shell.term.impl.TelnetTermServer;
import com.taobao.arthas.core.shell.term.impl.httptelnet.HttpTelnetTermServer;
import com.taobao.arthas.core.util.ArthasBanner;
import com.taobao.arthas.core.util.Constants;
@@ -19,6 +18,7 @@ import com.taobao.middleware.logger.Logger;
import io.netty.channel.ChannelFuture;
import java.io.File;
import java.io.IOException;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Method;
@@ -50,10 +50,16 @@ public class ArthasBootstrap {
private ExecutorService executorService;
private TunnelClient tunnelClient;
private File arthasOutputDir;
private ArthasBootstrap(int pid, Instrumentation instrumentation) {
this.pid = pid;
this.instrumentation = instrumentation;
String outputPath = System.getProperty("arthas.output.dir", "arthas-output");
arthasOutputDir = new File(outputPath);
arthasOutputDir.mkdirs();
executorService = Executors.newCachedThreadPool(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
@@ -0,0 +1,153 @@
package com.taobao.arthas.core.shell.term.impl.httptelnet;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import com.taobao.arthas.common.IOUtils;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
/**
*
* @author hengyunabc 2019-11-06
*
*/
public class DirectoryBrowser {
//@formatter:off
private static String pageHeader = "<!DOCTYPE html>\n" +
"<html>\n" +
"\n" +
"<head>\n" +
" <title>Arthas Resouces: %s</title>\n" +
" <meta charset=\"utf-8\" name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" +
" <style>\n" +
"body {\n" +
" background: #fff;\n" +
"}\n" +
" </style>\n" +
"</head>\n" +
"\n" +
"<body>\n" +
" <header>\n" +
" <h1>%s</h1>\n" +
" </header>\n" +
" <hr/>\n" +
" <main>\n" +
" <pre id=\"contents\">\n";
private static String pageFooter = " </pre>\n" +
" </main>\n" +
" <hr/>\n" +
"</body>\n" +
"\n" +
"</html>";
//@formatter:on
private static String linePart1Str = "<a href=\"%s\" title=\"%s\">";
private static String linePart2Str = "%-60s";
private static String renderDir(File dir) {
File[] listFiles = dir.listFiles();
StringBuilder sb = new StringBuilder(8192);
String dirName = dir.getName() + "/";
sb.append(String.format(pageHeader, dirName, dirName));
sb.append("<a href=\"../\" title=\"../\">../</a>\n");
if (listFiles != null) {
Arrays.sort(listFiles);
for (File f : listFiles) {
if (f.isDirectory()) {
String name = f.getName() + "/";
String part1Format = String.format(linePart1Str, name, name, name);
sb.append(part1Format);
String linePart2 = name + "</a>";
String part2Format = String.format(linePart2Str, linePart2);
sb.append(part2Format);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String modifyStr = simpleDateFormat.format(new Date(f.lastModified()));
sb.append(modifyStr);
sb.append(" - ").append("\r\n");
}
}
for (File f : listFiles) {
if (f.isFile()) {
String name = f.getName();
String part1Format = String.format(linePart1Str, name, name, name);
sb.append(part1Format);
String linePart2 = name + "</a>";
String part2Format = String.format(linePart2Str, linePart2);
sb.append(part2Format);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String modifyStr = simpleDateFormat.format(new Date(f.lastModified()));
sb.append(modifyStr);
String sizeStr = String.format("%10d ", f.length());
sb.append(sizeStr).append("\r\n");
}
}
}
sb.append(pageFooter);
return sb.toString();
}
public static DefaultFullHttpResponse view(File dir, String path, HttpVersion version) throws IOException {
if (path.startsWith("/")) {
path = path.substring(1, path.length());
}
File file = new File(path);
if (isSubFile(dir, file)) {
DefaultFullHttpResponse fullResp = new DefaultFullHttpResponse(version, HttpResponseStatus.OK);
if (file.isDirectory()) {
if (!path.endsWith("/")) {
fullResp.setStatus(HttpResponseStatus.FOUND).headers().set(HttpHeaderNames.LOCATION, "/" + path + "/");
}
String renderResult = renderDir(file);
fullResp.content().writeBytes(renderResult.getBytes("utf-8"));
fullResp.headers().set(HttpHeaderNames.CONTENT_TYPE, "content-type: text/html; charset=utf-8");
} 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);
}
}
return fullResp;
}
return null;
}
public static boolean isSubFile(File parent, File child) throws IOException {
String parentPath = parent.getCanonicalPath();
String childPath = child.getCanonicalPath();
if (parentPath.equals(childPath) || childPath.startsWith(parent.getCanonicalPath() + File.separator)) {
return true;
}
return false;
}
}
@@ -0,0 +1,122 @@
package com.taobao.arthas.core.shell.term.impl.httptelnet;
import java.io.File;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
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.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.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;
/**
* @author <a href="mailto:julien@julienviet.com">Julien Viet</a>
* @author hengyunabc 2019-11-06
*/
public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
private final String wsUri;
private File dir;
public HttpRequestHandler(String wsUri, File dir) {
this.wsUri = wsUri;
this.dir = dir;
dir.mkdirs();
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
if (wsUri.equalsIgnoreCase(request.uri())) {
ctx.fireChannelRead(request.retain());
} else {
if (HttpUtil.is100ContinueExpected(request)) {
send100Continue(ctx);
}
HttpResponse response = new DefaultHttpResponse(request.protocolVersion(),
HttpResponseStatus.INTERNAL_SERVER_ERROR);
String path = new URI(request.uri()).getPath();
if ("/".equals(path)) {
path = "/index.html";
}
InputStream in = null;
try {
DefaultFullHttpResponse fileViewResult = DirectoryBrowser.view(dir, path, request.protocolVersion());
if (fileViewResult != null) {
response = fileViewResult;
} else {
URL res = HttpTtyConnection.class.getResource("/io/termd/core/http" + path);
if (res != null) {
DefaultFullHttpResponse fullResp = new DefaultFullHttpResponse(request.protocolVersion(),
HttpResponseStatus.OK);
in = res.openStream();
byte[] tmp = new byte[256];
for (int l = 0; l != -1; l = in.read(tmp)) {
fullResp.content().writeBytes(tmp, 0, l);
}
int li = path.lastIndexOf('.');
if (li != -1 && li != path.length() - 1) {
String ext = path.substring(li + 1, path.length());
String contentType;
if ("html".equals(ext)) {
contentType = "text/html";
} else if ("js".equals(ext)) {
contentType = "application/javascript";
} else if ("css".equals(ext)) {
contentType = "text/css";
} else {
contentType = null;
}
if (contentType != null) {
fullResp.headers().set(HttpHeaderNames.CONTENT_TYPE, contentType);
}
}
response = fullResp;
} else {
response.setStatus(HttpResponseStatus.NOT_FOUND);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
ctx.write(response);
ChannelFuture future = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
future.addListener(ChannelFutureListener.CLOSE);
IOUtils.close(in);
}
}
}
private static void send100Continue(ChannelHandlerContext ctx) {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
ctx.writeAndFlush(response);
}
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
Logging.logReportedIoError(cause);
ctx.close();
}
}
@@ -1,5 +1,6 @@
package com.taobao.arthas.core.shell.term.impl.httptelnet;
import java.io.File;
import java.util.concurrent.TimeUnit;
import io.netty.buffer.ByteBuf;
@@ -14,7 +15,6 @@ import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.util.concurrent.ScheduledFuture;
import io.termd.core.function.Consumer;
import io.termd.core.function.Supplier;
import io.termd.core.http.netty.HttpRequestHandler;
import io.termd.core.http.netty.TtyWebSocketFrameHandler;
import io.termd.core.telnet.TelnetHandler;
import io.termd.core.telnet.netty.TelnetChannelHandler;
@@ -31,7 +31,7 @@ public class ProtocolDetectHandler extends ChannelInboundHandlerAdapter {
private Consumer<TtyConnection> ttyConnectionFactory;
public ProtocolDetectHandler(ChannelGroup channelGroup, final Supplier<TelnetHandler> handlerFactory,
Consumer<TtyConnection> ttyConnectionFactory) {
Consumer<TtyConnection> ttyConnectionFactory) {
this.channelGroup = channelGroup;
this.handlerFactory = handlerFactory;
this.ttyConnectionFactory = ttyConnectionFactory;
@@ -41,29 +41,32 @@ public class ProtocolDetectHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(final ChannelHandlerContext ctx) throws Exception {
detectTelnetFuture = ctx.executor().schedule(new Runnable() {
detectTelnetFuture = ctx.channel().eventLoop().schedule(new Runnable() {
@Override
public void run() {
channelGroup.add(ctx.channel());
TelnetChannelHandler handler = new TelnetChannelHandler(handlerFactory);
ChannelPipeline pipeline = ctx.pipeline();
pipeline.addLast(handler);
ctx.fireChannelActive();
pipeline.remove(ProtocolDetectHandler.this);
ctx.fireChannelActive(); // trigger TelnetChannelHandler init
}
}, 500, TimeUnit.MILLISECONDS);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf in = (ByteBuf) msg;
if (in.readableBytes() < 3) {
return;
}
detectTelnetFuture.cancel(false);
if (detectTelnetFuture != null && detectTelnetFuture.isCancellable()) {
detectTelnetFuture.cancel(false);
}
byte[] bytes = new byte[4];
byte[] bytes = new byte[3];
in.getBytes(0, bytes);
String httpHeader = new String(bytes);
@@ -77,11 +80,12 @@ public class ProtocolDetectHandler extends ChannelInboundHandlerAdapter {
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new ChunkedWriteHandler());
pipeline.addLast(new HttpObjectAggregator(64 * 1024));
pipeline.addLast(new HttpRequestHandler("/ws"));
pipeline.addLast(new HttpRequestHandler("/ws", new File("arthas-output")));
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
pipeline.addLast(new TtyWebSocketFrameHandler(channelGroup, ttyConnectionFactory));
}
ctx.fireChannelRead(msg);
ctx.fireChannelRead(in);
pipeline.remove(this);
}
}