diff --git a/core/src/main/java/com/taobao/arthas/core/server/ArthasBootstrap.java b/core/src/main/java/com/taobao/arthas/core/server/ArthasBootstrap.java index cd34834c8..405b23ac5 100644 --- a/core/src/main/java/com/taobao/arthas/core/server/ArthasBootstrap.java +++ b/core/src/main/java/com/taobao/arthas/core/server/ArthasBootstrap.java @@ -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) { diff --git a/core/src/main/java/com/taobao/arthas/core/shell/term/impl/httptelnet/DirectoryBrowser.java b/core/src/main/java/com/taobao/arthas/core/shell/term/impl/httptelnet/DirectoryBrowser.java new file mode 100644 index 000000000..eae6e12b5 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/term/impl/httptelnet/DirectoryBrowser.java @@ -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 = "\n" + + "\n" + + "\n" + + "\n" + + " Arthas Resouces: %s\n" + + " \n" + + " \n" + + "\n" + + "\n" + + "\n" + + "
\n" + + "

%s

\n" + + "
\n" + + "
\n" + + "
\n" + + "
\n";
+
+    private static String pageFooter = "       
\n" + + "
\n" + + "
\n" + + "\n" + + "\n" + + ""; + //@formatter:on + + private static String linePart1Str = ""; + 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("../\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 + ""; + 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 + ""; + 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; + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/term/impl/httptelnet/HttpRequestHandler.java b/core/src/main/java/com/taobao/arthas/core/shell/term/impl/httptelnet/HttpRequestHandler.java new file mode 100644 index 000000000..12b598051 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/shell/term/impl/httptelnet/HttpRequestHandler.java @@ -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 Julien Viet + * @author hengyunabc 2019-11-06 + */ +public class HttpRequestHandler extends SimpleChannelInboundHandler { + + 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(); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/term/impl/httptelnet/ProtocolDetectHandler.java b/core/src/main/java/com/taobao/arthas/core/shell/term/impl/httptelnet/ProtocolDetectHandler.java index 6562cfaac..74d05a68c 100644 --- a/core/src/main/java/com/taobao/arthas/core/shell/term/impl/httptelnet/ProtocolDetectHandler.java +++ b/core/src/main/java/com/taobao/arthas/core/shell/term/impl/httptelnet/ProtocolDetectHandler.java @@ -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 ttyConnectionFactory; public ProtocolDetectHandler(ChannelGroup channelGroup, final Supplier handlerFactory, - Consumer ttyConnectionFactory) { + Consumer 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); } + }