Add auth command, support authentication (#1724)

This commit is contained in:
hengyunabc
2021-03-04 22:47:58 +08:00
committed by GitHub
parent 985963b9d1
commit e520b10319
42 changed files with 1515 additions and 190 deletions
+4
View File
@@ -8,6 +8,10 @@ arthas.sessionTimeout=1800
arthas.enhanceLoaders=java.lang.ClassLoader
# https://arthas.aliyun.com/doc/en/auth
# arthas.username=arthas
# arthas.password=arthas
#arthas.appName=demoapp
#arthas.tunnelServer=ws://127.0.0.1:7777/ws
#arthas.agentId=mmmmmmyiddddd
@@ -38,6 +38,9 @@ public class Arthas {
Option sessionTimeout = new TypedOption<Integer>().setType(Integer.class)
.setShortName("session-timeout");
Option username = new TypedOption<String>().setType(String.class).setShortName("username");
Option password = new TypedOption<String>().setType(String.class).setShortName("password");
Option tunnelServer = new TypedOption<String>().setType(String.class).setShortName("tunnel-server");
Option agentId = new TypedOption<String>().setType(String.class).setShortName("agent-id");
Option appName = new TypedOption<String>().setType(String.class).setShortName(ArthasConstants.APP_NAME);
@@ -45,7 +48,9 @@ public class Arthas {
Option statUrl = new TypedOption<String>().setType(String.class).setShortName("stat-url");
CLI cli = CLIs.create("arthas").addOption(pid).addOption(core).addOption(agent).addOption(target)
.addOption(telnetPort).addOption(httpPort).addOption(sessionTimeout).addOption(tunnelServer).addOption(agentId).addOption(appName).addOption(statUrl);
.addOption(telnetPort).addOption(httpPort).addOption(sessionTimeout)
.addOption(username).addOption(password)
.addOption(tunnelServer).addOption(agentId).addOption(appName).addOption(statUrl);
CommandLine commandLine = cli.parse(Arrays.asList(args));
Configure configure = new Configure();
@@ -67,6 +72,9 @@ public class Arthas {
configure.setHttpPort((Integer) commandLine.getOptionValue("http-port"));
}
configure.setUsername((String) commandLine.getOptionValue("username"));
configure.setPassword((String) commandLine.getOptionValue("password"));
configure.setTunnelServer((String) commandLine.getOptionValue("tunnel-server"));
configure.setAgentId((String) commandLine.getOptionValue("agent-id"));
configure.setStatUrl((String) commandLine.getOptionValue("stat-url"));
@@ -9,6 +9,7 @@ import com.taobao.arthas.core.command.basic1000.HelpCommand;
import com.taobao.arthas.core.command.basic1000.HistoryCommand;
import com.taobao.arthas.core.command.basic1000.KeymapCommand;
import com.taobao.arthas.core.command.basic1000.OptionsCommand;
import com.taobao.arthas.core.command.basic1000.AuthCommand;
import com.taobao.arthas.core.command.basic1000.PwdCommand;
import com.taobao.arthas.core.command.basic1000.ResetCommand;
import com.taobao.arthas.core.command.basic1000.SessionCommand;
@@ -69,6 +70,7 @@ public class BuiltinCommandPack implements CommandResolver {
private static void initCommands() {
commands.add(Command.create(HelpCommand.class));
commands.add(Command.create(AuthCommand.class));
commands.add(Command.create(KeymapCommand.class));
commands.add(Command.create(SearchClassCommand.class));
commands.add(Command.create(SearchMethodCommand.class));
@@ -0,0 +1,109 @@
package com.taobao.arthas.core.command.basic1000;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginException;
import com.alibaba.arthas.deps.org.slf4j.Logger;
import com.alibaba.arthas.deps.org.slf4j.LoggerFactory;
import com.taobao.arthas.common.ArthasConstants;
import com.taobao.arthas.core.command.Constants;
import com.taobao.arthas.core.security.BasicPrincipal;
import com.taobao.arthas.core.security.SecurityAuthenticator;
import com.taobao.arthas.core.server.ArthasBootstrap;
import com.taobao.arthas.core.shell.cli.Completion;
import com.taobao.arthas.core.shell.cli.CompletionUtils;
import com.taobao.arthas.core.shell.command.AnnotatedCommand;
import com.taobao.arthas.core.shell.command.CommandProcess;
import com.taobao.arthas.core.shell.session.Session;
import com.taobao.middleware.cli.annotations.Argument;
import com.taobao.middleware.cli.annotations.DefaultValue;
import com.taobao.middleware.cli.annotations.Description;
import com.taobao.middleware.cli.annotations.Name;
import com.taobao.middleware.cli.annotations.Option;
import com.taobao.middleware.cli.annotations.Summary;
/**
* TODO 支持更多的鉴权方式。目前只支持 username/password的方式
*
* @author hengyunabc 2021-03-03
*
*/
// @formatter:off
@Name(ArthasConstants.AUTH)
@Summary("Authenticates the current session")
@Description(Constants.EXAMPLE +
" auth" +
" auth <password>\n" +
" auth --username <username> <password>\n"
+ Constants.WIKI + Constants.WIKI_HOME + ArthasConstants.AUTH)
//@formatter:on
public class AuthCommand extends AnnotatedCommand {
private static final Logger logger = LoggerFactory.getLogger(AuthCommand.class);
private String username;
private String password;
private SecurityAuthenticator authenticator = ArthasBootstrap.getInstance().getSecurityAuthenticator();
@Argument(argName = "password", index = 0, required = false)
@Description("password")
public void setPassword(String password) {
this.password = password;
}
@Option(shortName = "n", longName = "username")
@Description("username, default value 'arthas'")
@DefaultValue(ArthasConstants.DEFAULT_USERNAME)
public void setUsername(String username) {
this.username = username;
}
@Override
public void process(CommandProcess process) {
int status = 0;
String message = "";
try {
Session session = process.session();
if (username == null) {
status = 1;
message = "username can not be empty!";
return;
}
if (password == null) { // 没有传入passowrd参数时,打印当前结果
boolean authenticated = session.get(ArthasConstants.SUBJECT_KEY) != null;
boolean needLogin = this.authenticator.needLogin();
message = "Authentication result: " + authenticated + ", Need authentication: " + needLogin;
if (needLogin && !authenticated) {
status = 1;
}
return;
} else {
// 尝试进行鉴权
BasicPrincipal principal = new BasicPrincipal(username, password);
try {
Subject subject = authenticator.login(principal);
if (subject != null) {
// 把subject 保存到 session里,后续其它命令则可以正常执行
session.put(ArthasConstants.SUBJECT_KEY, subject);
message = "Authentication result: " + true + ", username: " + username;
} else {
status = 1;
message = "Authentication result: " + false + ", username: " + username;
}
} catch (LoginException e) {
logger.error("Authentication error, username: {}", username, e);
}
}
} finally {
process.end(status, message);
}
}
@Override
public void complete(Completion completion) {
if (!CompletionUtils.completeFilePath(completion)) {
super.complete(completion);
}
}
}
@@ -28,6 +28,9 @@ public class Configure {
private String tunnelServer;
private String agentId;
private String username;
private String password;
/**
* @see com.taobao.arthas.common.ArthasConstants#ARTHAS_OUTPUT
*/
@@ -161,6 +164,22 @@ public class Configure {
this.outputPath = outputPath;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
/**
* 序列化成字符串
*
@@ -0,0 +1,69 @@
package com.taobao.arthas.core.security;
import java.security.Principal;
/**
* Basic {@link Principal}.
*
* @author hengyunabc 2021-03-04
*/
public final class BasicPrincipal implements Principal {
private final String username;
private final String password;
public BasicPrincipal(String username, String password) {
this.username = username;
this.password = password;
}
@Override
public String getName() {
return username;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((password == null) ? 0 : password.hashCode());
result = prime * result + ((username == null) ? 0 : username.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
BasicPrincipal other = (BasicPrincipal) obj;
if (password == null) {
if (other.password != null)
return false;
} else if (!password.equals(other.password))
return false;
if (username == null) {
if (other.username != null)
return false;
} else if (!username.equals(other.username))
return false;
return true;
}
@Override
public String toString() {
// do not display the password
return "BasicPrincipal[" + username + "]";
}
}
@@ -0,0 +1,71 @@
package com.taobao.arthas.core.security;
import java.security.Principal;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginException;
/**
* A {@link SecurityAuthenticator} allows to plugin custom authenticators, such
* as JAAS based or custom implementations.
*/
public interface SecurityAuthenticator {
boolean needLogin();
/**
* Sets the name of the realm to use.
*/
void setName(String name);
/**
* Gets the name of the realm.
*/
String getName();
/**
* Sets the role class names (separated by comma)
* <p/>
* By default if no explicit role class names has been configured, then this
* implementation will assume the {@link Subject}
* {@link java.security.Principal}s is a role if the classname contains the word
* <tt>role</tt> (lower cased).
*
* @param names a list of FQN class names for role
* {@link java.security.Principal} implementations.
*/
void setRoleClassNames(String names);
/**
* Attempts to login the {@link java.security.Principal} on this realm.
* <p/>
* The login is a success if no Exception is thrown, and a {@link Subject} is
* returned.
*
* @param principal the principal
* @return the subject for the logged in principal, must <b>not</b> be
* <tt>null</tt>
* @throws LoginException is thrown if error logging in the
* {@link java.security.Principal}
*/
Subject login(Principal principal) throws LoginException;
/**
* Attempt to logout the subject.
*
* @param subject subject to logout
* @throws LoginException is thrown if error logging out subject
*/
void logout(Subject subject) throws LoginException;
/**
* Gets the user roles from the given {@link Subject}
*
* @param subject the subject
* @return <tt>null</tt> if no roles, otherwise a String with roles separated by
* comma.
*/
String getUserRoles(Subject subject);
}
@@ -0,0 +1,90 @@
package com.taobao.arthas.core.security;
import java.security.Principal;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginException;
import com.alibaba.arthas.deps.org.slf4j.Logger;
import com.alibaba.arthas.deps.org.slf4j.LoggerFactory;
import com.taobao.arthas.common.ArthasConstants;
import com.taobao.arthas.core.util.StringUtils;
/**
* TODO 支持不同角色不同权限,command按角色分类?
*
* @author hengyunabc 2021-03-03
*
*/
public class SecurityAuthenticatorImpl implements SecurityAuthenticator {
private static final Logger logger = LoggerFactory.getLogger(SecurityAuthenticatorImpl.class);
private String username;
private String password;
private Subject subject;
public SecurityAuthenticatorImpl(String username, String password) {
if (username != null && password == null) {
password = StringUtils.randomString(32);
logger.info("\nUsing generated security password: {}\n", password);
}
if (username == null && password != null) {
username = ArthasConstants.DEFAULT_USERNAME;
}
this.username = username;
this.password = password;
subject = new Subject();
}
@Override
public void setName(String name) {
// TODO Auto-generated method stub
}
@Override
public String getName() {
// TODO Auto-generated method stub
return null;
}
@Override
public void setRoleClassNames(String names) {
// TODO Auto-generated method stub
}
@Override
public Subject login(Principal principal) throws LoginException {
if (principal == null) {
return null;
}
if (principal instanceof BasicPrincipal) {
BasicPrincipal basicPrincipal = (BasicPrincipal) principal;
if (basicPrincipal.getName().equals(username) && basicPrincipal.getPassword().equals(this.password)) {
return subject;
}
}
return null;
}
@Override
public void logout(Subject subject) throws LoginException {
// TODO Auto-generated method stub
}
@Override
public String getUserRoles(Subject subject) {
// TODO Auto-generated method stub
return null;
}
@Override
public boolean needLogin() {
return username != null && password != null;
}
}
@@ -50,6 +50,8 @@ import com.taobao.arthas.core.env.ArthasEnvironment;
import com.taobao.arthas.core.env.MapPropertySource;
import com.taobao.arthas.core.env.PropertiesPropertySource;
import com.taobao.arthas.core.env.PropertySource;
import com.taobao.arthas.core.security.SecurityAuthenticator;
import com.taobao.arthas.core.security.SecurityAuthenticatorImpl;
import com.taobao.arthas.core.server.instrument.ClassLoader_Instrument;
import com.taobao.arthas.core.shell.ShellServer;
import com.taobao.arthas.core.shell.ShellServerOptions;
@@ -62,6 +64,7 @@ import com.taobao.arthas.core.shell.session.SessionManager;
import com.taobao.arthas.core.shell.session.impl.SessionManagerImpl;
import com.taobao.arthas.core.shell.term.impl.HttpTermServer;
import com.taobao.arthas.core.shell.term.impl.http.api.HttpApiHandler;
import com.taobao.arthas.core.shell.term.impl.http.session.HttpSessionManager;
import com.taobao.arthas.core.shell.term.impl.httptelnet.HttpTelnetTermServer;
import com.taobao.arthas.core.util.ArthasBanner;
import com.taobao.arthas.core.util.FileUtils;
@@ -119,6 +122,9 @@ public class ArthasBootstrap {
private HttpApiHandler httpApiHandler;
private HttpSessionManager httpSessionManager;
private SecurityAuthenticator securityAuthenticator;
private ArthasBootstrap(Instrumentation instrumentation, Map<String, String> args) throws Throwable {
this.instrumentation = instrumentation;
@@ -320,7 +326,7 @@ public class ArthasBootstrap {
overrideAll = Boolean.parseBoolean(properties.getProperty(CONFIG_OVERRIDE_ALL, "false"));
}
PropertySource propertySource = new PropertiesPropertySource(location, properties);
PropertySource<?> propertySource = new PropertiesPropertySource(location, properties);
if (overrideAll) {
arthasEnvironment.addFirst(propertySource);
} else {
@@ -383,6 +389,9 @@ public class ArthasBootstrap {
options.setSessionTimeout(configure.getSessionTimeout() * 1000);
}
this.httpSessionManager = new HttpSessionManager();
this.securityAuthenticator = new SecurityAuthenticatorImpl(configure.getUsername(), configure.getPassword());
shellServer = new ShellServerImpl(options);
BuiltinCommandPack builtinCommands = new BuiltinCommandPack();
List<CommandResolver> resolvers = new ArrayList<CommandResolver>();
@@ -394,18 +403,18 @@ public class ArthasBootstrap {
// TODO: discover user provided command resolver
if (configure.getTelnetPort() != null && configure.getTelnetPort() > 0) {
shellServer.registerTermServer(new HttpTelnetTermServer(configure.getIp(), configure.getTelnetPort(),
options.getConnectionTimeout(), workerGroup));
options.getConnectionTimeout(), workerGroup, httpSessionManager));
} else {
logger().info("telnet port is {}, skip bind telnet server.", configure.getTelnetPort());
}
if (configure.getHttpPort() != null && configure.getHttpPort() > 0) {
shellServer.registerTermServer(new HttpTermServer(configure.getIp(), configure.getHttpPort(),
options.getConnectionTimeout(), workerGroup));
options.getConnectionTimeout(), workerGroup, httpSessionManager));
} else {
// listen local address in VM communication
if (configure.getTunnelServer() != null) {
shellServer.registerTermServer(new HttpTermServer(configure.getIp(), configure.getHttpPort(),
options.getConnectionTimeout(), workerGroup));
options.getConnectionTimeout(), workerGroup, httpSessionManager));
}
logger().info("http port is {}, skip bind http server.", configure.getHttpPort());
}
@@ -480,6 +489,9 @@ public class ArthasBootstrap {
sessionManager.close();
sessionManager = null;
}
if (this.httpSessionManager != null) {
httpSessionManager.stop();
}
if (timer != null) {
timer.cancel();
}
@@ -633,4 +645,8 @@ public class ArthasBootstrap {
return outputPath;
}
public SecurityAuthenticator getSecurityAuthenticator() {
return securityAuthenticator;
}
}
@@ -18,13 +18,18 @@ import com.taobao.arthas.core.shell.system.impl.InternalCommandManager;
import com.taobao.arthas.core.shell.system.impl.JobControllerImpl;
import com.taobao.arthas.core.shell.term.Term;
import com.taobao.arthas.core.shell.term.impl.TermImpl;
import com.taobao.arthas.core.shell.term.impl.http.ExtHttpTtyConnection;
import com.taobao.arthas.core.util.Constants;
import com.taobao.arthas.core.util.FileUtils;
import io.termd.core.tty.TtyConnection;
import java.io.File;
import java.lang.instrument.Instrumentation;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
@@ -49,6 +54,18 @@ public class ShellImpl implements Shell {
public ShellImpl(ShellServer server, Term term, InternalCommandManager commandManager,
Instrumentation instrumentation, long pid, JobControllerImpl jobController) {
if (term instanceof TermImpl) {
TermImpl termImpl = (TermImpl) term;
TtyConnection conn = termImpl.getConn();
if (conn instanceof ExtHttpTtyConnection) {
// 传递http cookie 里的鉴权信息到新建立的session中
ExtHttpTtyConnection extConn = (ExtHttpTtyConnection) conn;
Map<String, Object> extSessions = extConn.extSessions();
for (Entry<String, Object> entry : extSessions.entrySet()) {
session.put(entry.getKey(), entry.getValue());
}
}
}
session.put(Session.COMMAND_MANAGER, commandManager);
session.put(Session.INSTRUMENTATION, instrumentation);
session.put(Session.PID, pid);
@@ -1,7 +1,9 @@
package com.taobao.arthas.core.shell.system.impl;
import com.taobao.arthas.common.ArthasConstants;
import com.taobao.arthas.core.GlobalOptions;
import com.taobao.arthas.core.distribution.ResultDistributor;
import com.taobao.arthas.core.server.ArthasBootstrap;
import com.taobao.arthas.core.shell.cli.CliToken;
import com.taobao.arthas.core.shell.command.Command;
import com.taobao.arthas.core.shell.command.internal.RedirectHandler;
@@ -60,15 +62,30 @@ public class JobControllerImpl implements JobController {
return jobs.remove(id) != null;
}
private void checkPermission(Session session, CliToken token) {
if (ArthasBootstrap.getInstance().getSecurityAuthenticator().needLogin()) {
// 检查session是否有 Subject
Object subject = session.get(ArthasConstants.SUBJECT_KEY);
if (subject == null) {
if (token != null && token.isText() && token.value().trim().equals(ArthasConstants.AUTH)) {
// 执行的是auth 命令
return;
}
throw new IllegalArgumentException("Error! command not permitted, try to use 'auth' command to authenticates.");
}
}
}
@Override
public Job createJob(InternalCommandManager commandManager, List<CliToken> tokens, Session session, JobListener jobHandler, Term term, ResultDistributor resultDistributor) {
checkPermission(session, tokens.get(0));
int jobId = idGenerator.incrementAndGet();
StringBuilder line = new StringBuilder();
for (CliToken arg : tokens) {
line.append(arg.raw());
}
boolean runInBackground = runInBackground(tokens);
Process process = createProcess(tokens, commandManager, jobId, term, resultDistributor);
Process process = createProcess(session, tokens, commandManager, jobId, term, resultDistributor);
process.setJobId(jobId);
JobImpl job = new JobImpl(jobId, this, process, line.toString(), runInBackground, session, jobHandler);
jobs.put(jobId, job);
@@ -126,12 +143,14 @@ public class JobControllerImpl implements JobController {
* @param resultDistributor
* @return the created process
*/
private Process createProcess(List<CliToken> line, InternalCommandManager commandManager, int jobId, Term term, ResultDistributor resultDistributor) {
private Process createProcess(Session session, List<CliToken> line, InternalCommandManager commandManager, int jobId, Term term, ResultDistributor resultDistributor) {
try {
ListIterator<CliToken> tokens = line.listIterator();
while (tokens.hasNext()) {
CliToken token = tokens.next();
if (token.isText()) {
// check before create process
checkPermission(session, token);
Command command = commandManager.getCommand(token.value());
if (command != null) {
return createCommandProcess(command, tokens, jobId, term, resultDistributor);
@@ -7,6 +7,8 @@ import com.taobao.arthas.core.shell.handlers.Handler;
import com.taobao.arthas.core.shell.term.Term;
import com.taobao.arthas.core.shell.term.TermServer;
import com.taobao.arthas.core.shell.term.impl.http.NettyWebsocketTtyBootstrap;
import com.taobao.arthas.core.shell.term.impl.http.session.HttpSessionManager;
import io.netty.util.concurrent.EventExecutorGroup;
import io.termd.core.function.Consumer;
import io.termd.core.tty.TtyConnection;
@@ -26,12 +28,14 @@ public class HttpTermServer extends TermServer {
private int port;
private long connectionTimeout;
private EventExecutorGroup workerGroup;
private HttpSessionManager httpSessionManager;
public HttpTermServer(String hostIp, int port, long connectionTimeout, EventExecutorGroup workerGroup) {
public HttpTermServer(String hostIp, int port, long connectionTimeout, EventExecutorGroup workerGroup, HttpSessionManager httpSessionManager) {
this.hostIp = hostIp;
this.port = port;
this.connectionTimeout = connectionTimeout;
this.workerGroup = workerGroup;
this.httpSessionManager = httpSessionManager;
}
@Override
@@ -43,7 +47,7 @@ public class HttpTermServer extends TermServer {
@Override
public TermServer listen(Handler<Future<TermServer>> listenHandler) {
// TODO: charset and inputrc from options
bootstrap = new NettyWebsocketTtyBootstrap(workerGroup).setHost(hostIp).setPort(port);
bootstrap = new NettyWebsocketTtyBootstrap(workerGroup, httpSessionManager).setHost(hostIp).setPort(port);
try {
bootstrap.start(new Consumer<TtyConnection>() {
@Override
@@ -221,6 +221,10 @@ public class TermImpl implements Term {
}
}
public TtyConnection getConn() {
return conn;
}
public void echo(int... codePoints) {
Consumer<int[]> out = conn.stdoutHandler();
for (int codePoint : codePoints) {
@@ -0,0 +1,138 @@
package com.taobao.arthas.core.shell.term.impl.http;
import java.nio.charset.Charset;
import javax.security.auth.Subject;
import com.alibaba.arthas.deps.org.slf4j.Logger;
import com.alibaba.arthas.deps.org.slf4j.LoggerFactory;
import com.taobao.arthas.common.ArthasConstants;
import com.taobao.arthas.core.security.BasicPrincipal;
import com.taobao.arthas.core.security.SecurityAuthenticator;
import com.taobao.arthas.core.server.ArthasBootstrap;
import com.taobao.arthas.core.shell.term.impl.http.session.HttpSession;
import com.taobao.arthas.core.shell.term.impl.http.session.HttpSessionManager;
import com.taobao.arthas.core.util.StringUtils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.base64.Base64;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.HttpHeaderNames;
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.HttpVersion;
import io.netty.util.Attribute;
/**
*
* @author hengyunabc 2021-03-03
*
*/
public final class BasicHttpAuthenticatorHandler extends ChannelDuplexHandler {
private static final Logger logger = LoggerFactory.getLogger(BasicHttpAuthenticatorHandler.class);
private HttpSessionManager httpSessionManager;
private SecurityAuthenticator securityAuthenticator = ArthasBootstrap.getInstance().getSecurityAuthenticator();
public BasicHttpAuthenticatorHandler(HttpSessionManager httpSessionManager) {
this.httpSessionManager = httpSessionManager;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (!securityAuthenticator.needLogin()) {
ctx.fireChannelRead(msg);
return;
}
boolean authed = false;
if (msg instanceof HttpRequest) {
HttpRequest httpRequest = (HttpRequest) msg;
// 判断session里是否有已登陆信息
HttpSession session = httpSessionManager.getOrCreateHttpSession(ctx, httpRequest);
if (session != null && session.getAttribute(ArthasConstants.SUBJECT_KEY) != null) {
authed = true;
}
// 判断请求header里是否带有 username/password
if (!authed) {
BasicPrincipal principal = extractBasicAuthSubject(httpRequest);
Subject subject = securityAuthenticator.login(principal);
if (subject != null) {
authed = true;
session.setAttribute(ArthasConstants.SUBJECT_KEY, subject);
}
}
if (!authed) {
// restricted resource, so send back 401 to require valid username/password
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.UNAUTHORIZED);
response.headers().set(HttpHeaderNames.WWW_AUTHENTICATE, "Basic realm=\"arthas webconsole\"");
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, 0);
ctx.writeAndFlush(response);
// close the channel
ctx.channel().close();
return;
}
}
ctx.fireChannelRead(msg);
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
if (msg instanceof HttpResponse) {
// write cookie
HttpResponse response = (HttpResponse) msg;
Attribute<HttpSession> attribute = ctx.channel().attr(HttpSessionManager.SESSION_KEY);
HttpSession session = attribute.get();
if (session != null) {
HttpSessionManager.setSessionCookie(response, session);
}
}
super.write(ctx, msg, promise);
}
/**
* Extracts the username and password details from the HTTP basic header
* Authorization.
* <p/>
* This requires that the <tt>Authorization</tt> HTTP header is provided, and
* its using Basic. Currently Digest is <b>not</b> supported.
*
* @return {@link HttpPrincipal} with username and password details, or
* <tt>null</tt> if not possible to extract
*/
protected static BasicPrincipal extractBasicAuthSubject(HttpRequest request) {
String auth = request.headers().get(HttpHeaderNames.AUTHORIZATION);
if (auth != null) {
String constraint = StringUtils.before(auth, " ");
if (constraint != null) {
if ("Basic".equalsIgnoreCase(constraint.trim())) {
String decoded = StringUtils.after(auth, " ");
// the decoded part is base64 encoded, so we need to decode that
ByteBuf buf = Unpooled.wrappedBuffer(decoded.getBytes());
ByteBuf out = Base64.decode(buf);
String userAndPw = out.toString(Charset.defaultCharset());
String username = StringUtils.before(userAndPw, ":");
String password = StringUtils.after(userAndPw, ":");
BasicPrincipal principal = new BasicPrincipal(username, password);
logger.debug("Extracted Basic Auth principal from HTTP header: {}", principal);
return principal;
}
}
}
return null;
}
}
@@ -0,0 +1,76 @@
package com.taobao.arthas.core.shell.term.impl.http;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import com.taobao.arthas.common.ArthasConstants;
import com.taobao.arthas.core.shell.term.impl.http.session.HttpSession;
import com.taobao.arthas.core.shell.term.impl.http.session.HttpSessionManager;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.termd.core.http.HttpTtyConnection;
/**
* 从http请求传递过来的 session 信息。解析websocket创建的 term 还需要登陆验证问题
*
* @author hengyunabc 2021-03-04
*
*/
public class ExtHttpTtyConnection extends HttpTtyConnection {
private ChannelHandlerContext context;
public ExtHttpTtyConnection(ChannelHandlerContext context) {
this.context = context;
}
@Override
protected void write(byte[] buffer) {
ByteBuf byteBuf = Unpooled.buffer();
byteBuf.writeBytes(buffer);
if (context != null) {
context.writeAndFlush(new TextWebSocketFrame(byteBuf));
}
}
@Override
public void schedule(Runnable task, long delay, TimeUnit unit) {
if (context != null) {
context.executor().schedule(task, delay, unit);
}
}
@Override
public void execute(Runnable task) {
if (context != null) {
context.executor().execute(task);
}
}
@Override
public void close() {
if (context != null) {
context.close();
}
}
public Map<String, Object> extSessions() {
if (context != null) {
HttpSession httpSession = HttpSessionManager.getHttpSessionFromContext(context);
if (httpSession != null) {
Object subject = httpSession.getAttribute(ArthasConstants.SUBJECT_KEY);
if (subject != null) {
Map<String, Object> result = new HashMap<String, Object>();
result.put(ArthasConstants.SUBJECT_KEY, subject);
return result;
}
}
}
return Collections.emptyMap();
}
}
@@ -77,7 +77,7 @@ public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequ
try {
//handle http restful api
if ("/api".equals(path)) {
response = httpApiHandler.handle(request);
response = httpApiHandler.handle(ctx, request);
isHttpApiResponse = true;
}
@@ -1,6 +1,7 @@
package com.taobao.arthas.core.shell.term.impl.http;
import com.taobao.arthas.common.ArthasConstants;
import com.taobao.arthas.core.shell.term.impl.http.session.HttpSessionManager;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
@@ -37,11 +38,13 @@ public class NettyWebsocketTtyBootstrap {
private EventLoopGroup group;
private Channel channel;
private EventExecutorGroup workerGroup;
private HttpSessionManager httpSessionManager;
public NettyWebsocketTtyBootstrap(EventExecutorGroup workerGroup) {
public NettyWebsocketTtyBootstrap(EventExecutorGroup workerGroup, HttpSessionManager httpSessionManager) {
this.workerGroup = workerGroup;
this.host = "localhost";
this.port = 8080;
this.httpSessionManager = httpSessionManager;
}
public String getHost() {
@@ -68,7 +71,7 @@ public class NettyWebsocketTtyBootstrap {
if (this.port > 0) {
ServerBootstrap b = new ServerBootstrap();
b.group(group).channel(NioServerSocketChannel.class).handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new TtyServerInitializer(channelGroup, handler, workerGroup));
.childHandler(new TtyServerInitializer(channelGroup, handler, workerGroup, httpSessionManager));
final ChannelFuture f = b.bind(host, port);
f.addListener(new GenericFutureListener<Future<? super Void>>() {
@@ -1,5 +1,8 @@
package com.taobao.arthas.core.shell.term.impl.http;
import com.taobao.arthas.common.ArthasConstants;
import com.taobao.arthas.core.shell.term.impl.http.session.HttpSessionManager;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.group.ChannelGroup;
@@ -13,10 +16,6 @@ import io.netty.util.concurrent.EventExecutorGroup;
import io.termd.core.function.Consumer;
import io.termd.core.tty.TtyConnection;
import java.io.File;
import com.taobao.arthas.common.ArthasConstants;
/**
* @author <a href="mailto:julien@julienviet.com">Julien Viet</a>
@@ -26,11 +25,13 @@ public class TtyServerInitializer extends ChannelInitializer<SocketChannel> {
private final ChannelGroup group;
private final Consumer<TtyConnection> handler;
private EventExecutorGroup workerGroup;
private HttpSessionManager httpSessionManager;
public TtyServerInitializer(ChannelGroup group, Consumer<TtyConnection> handler, EventExecutorGroup workerGroup) {
public TtyServerInitializer(ChannelGroup group, Consumer<TtyConnection> handler, EventExecutorGroup workerGroup, HttpSessionManager httpSessionManager) {
this.group = group;
this.handler = handler;
this.workerGroup = workerGroup;
this.workerGroup = workerGroup;
this.httpSessionManager = httpSessionManager;
}
@Override
@@ -40,6 +41,7 @@ public class TtyServerInitializer extends ChannelInitializer<SocketChannel> {
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new ChunkedWriteHandler());
pipeline.addLast(new HttpObjectAggregator(ArthasConstants.MAX_HTTP_CONTENT_LENGTH));
pipeline.addLast(new BasicHttpAuthenticatorHandler(httpSessionManager));
pipeline.addLast(workerGroup, "HttpRequestHandler", new HttpRequestHandler("/ws"));
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
pipeline.addLast(new IdleStateHandler(0, 0, ArthasConstants.WEBSOCKET_IDLE_SECONDS));
@@ -16,8 +16,6 @@
package com.taobao.arthas.core.shell.term.impl.http;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
@@ -29,7 +27,6 @@ import io.termd.core.function.Consumer;
import io.termd.core.http.HttpTtyConnection;
import io.termd.core.tty.TtyConnection;
import java.util.concurrent.TimeUnit;
/**
* @author <a href="mailto:julien@julienviet.com">Julien Viet</a>
@@ -57,37 +54,7 @@ public class TtyWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWe
if (evt == WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE) {
ctx.pipeline().remove(HttpRequestHandler.class);
group.add(ctx.channel());
conn = new HttpTtyConnection() {
@Override
protected void write(byte[] buffer) {
ByteBuf byteBuf = Unpooled.buffer();
byteBuf.writeBytes(buffer);
if (context != null) {
context.writeAndFlush(new TextWebSocketFrame(byteBuf));
}
}
@Override
public void schedule(Runnable task, long delay, TimeUnit unit) {
if (context != null) {
context.executor().schedule(task, delay, unit);
}
}
@Override
public void execute(Runnable task) {
if (context != null) {
context.executor().execute(task);
}
}
@Override
public void close() {
if (context != null) {
context.close();
}
}
};
conn = new ExtHttpTtyConnection(context);
handler.accept(conn);
} else if (evt instanceof IdleStateEvent) {
ctx.writeAndFlush(new PingWebSocketFrame());
@@ -3,6 +3,7 @@ package com.taobao.arthas.core.shell.term.impl.http.api;
import com.alibaba.arthas.deps.org.slf4j.Logger;
import com.alibaba.arthas.deps.org.slf4j.LoggerFactory;
import com.alibaba.fastjson.JSON;
import com.taobao.arthas.common.ArthasConstants;
import com.taobao.arthas.common.PidUtils;
import com.taobao.arthas.core.command.model.*;
import com.taobao.arthas.core.distribution.PackingResultDistributor;
@@ -25,6 +26,8 @@ import com.taobao.arthas.core.shell.system.JobListener;
import com.taobao.arthas.core.shell.system.impl.InternalCommandManager;
import com.taobao.arthas.core.shell.term.SignalHandler;
import com.taobao.arthas.core.shell.term.Term;
import com.taobao.arthas.core.shell.term.impl.http.session.HttpSession;
import com.taobao.arthas.core.shell.term.impl.http.session.HttpSessionManager;
import com.taobao.arthas.core.util.ArthasBanner;
import com.taobao.arthas.core.util.DateUtils;
import com.taobao.arthas.core.util.JsonUtils;
@@ -32,6 +35,7 @@ import com.taobao.arthas.core.util.StringUtils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.*;
import io.netty.util.CharsetUtil;
import io.termd.core.function.Function;
@@ -42,7 +46,6 @@ import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
@@ -55,7 +58,6 @@ public class HttpApiHandler {
private static final Logger logger = LoggerFactory.getLogger(HttpApiHandler.class);
public static final int DEFAULT_EXEC_TIMEOUT = 30000;
private final SessionManager sessionManager;
private static HttpApiHandler instance;
private final InternalCommandManager commandManager;
private final JobController jobController;
private final HistoryManager historyManager;
@@ -81,7 +83,7 @@ public class HttpApiHandler {
}
}
public HttpResponse handle(FullHttpRequest request) throws Exception {
public HttpResponse handle(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
ApiResponse result;
String requestBody = null;
@@ -92,7 +94,7 @@ public class HttpApiHandler {
requestBody = getBody(request);
ApiRequest apiRequest = parseRequest(requestBody);
requestId = apiRequest.getRequestId();
result = processRequest(apiRequest);
result = processRequest(ctx, apiRequest);
} else {
result = createResponse(ApiState.REFUSED, "Unsupported http method: " + method.name());
}
@@ -191,7 +193,7 @@ public class HttpApiHandler {
}
}
private ApiResponse processRequest(ApiRequest apiRequest) {
private ApiResponse processRequest(ChannelHandlerContext ctx, ApiRequest apiRequest) {
String actionStr = apiRequest.getAction();
try {
@@ -226,6 +228,16 @@ public class HttpApiHandler {
sessionManager.updateAccessTime(session);
}
// 请求到达这里,如果有需要鉴权,则已经在前面的handler里处理过了
// 如果有鉴权取到的 Subject,则传递到 arthas的session里
HttpSession httpSession = HttpSessionManager.getHttpSessionFromContext(ctx);
if (httpSession != null) {
Object subject = httpSession.getAttribute(ArthasConstants.SUBJECT_KEY);
if (subject != null) {
session.put(ArthasConstants.SUBJECT_KEY, subject);
}
}
//dispatch requests
ApiResponse response = dispatchRequest(action, apiRequest, session);
if (response != null) {
@@ -0,0 +1,156 @@
package com.taobao.arthas.core.shell.term.impl.http.session;
import java.util.Enumeration;
/**
*
* @author hengyunabc 2021-03-03
*
*/
public interface HttpSession {
/**
* Returns the time when this session was created, measured in milliseconds
* since midnight January 1, 1970 GMT.
*
* @return a <code>long</code> specifying when this session was created,
* expressed in milliseconds since 1/1/1970 GMT
* @exception IllegalStateException if this method is called on an invalidated
* session
*/
public long getCreationTime();
/**
* Returns a string containing the unique identifier assigned to this session.
* The identifier is assigned by the servlet container and is implementation
* dependent.
*
* @return a string specifying the identifier assigned to this session
* @exception IllegalStateException if this method is called on an invalidated
* session
*/
public String getId();
/**
* Returns the last time the client sent a request associated with this session,
* as the number of milliseconds since midnight January 1, 1970 GMT, and marked
* by the time the container received the request.
* <p>
* Actions that your application takes, such as getting or setting a value
* associated with the session, do not affect the access time.
*
* @return a <code>long</code> representing the last time the client sent a
* request associated with this session, expressed in milliseconds since
* 1/1/1970 GMT
* @exception IllegalStateException if this method is called on an invalidated
* session
*/
public long getLastAccessedTime();
/**
* Specifies the time, in seconds, between client requests before the servlet
* container will invalidate this session. A zero or negative time indicates
* that the session should never timeout.
*
* @param interval An integer specifying the number of seconds
*/
public void setMaxInactiveInterval(int interval);
/**
* Returns the maximum time interval, in seconds, that the servlet container
* will keep this session open between client accesses. After this interval, the
* servlet container will invalidate the session. The maximum time interval can
* be set with the <code>setMaxInactiveInterval</code> method. A zero or
* negative time indicates that the session should never timeout.
*
* @return an integer specifying the number of seconds this session remains open
* between client requests
* @see #setMaxInactiveInterval
*/
public int getMaxInactiveInterval();
/**
* Returns the object bound with the specified name in this session, or
* <code>null</code> if no object is bound under the name.
*
* @param name a string specifying the name of the object
* @return the object with the specified name
* @exception IllegalStateException if this method is called on an invalidated
* session
*/
public Object getAttribute(String name);
/**
* Returns an <code>Enumeration</code> of <code>String</code> objects containing
* the names of all the objects bound to this session.
*
* @return an <code>Enumeration</code> of <code>String</code> objects specifying
* the names of all the objects bound to this session
* @exception IllegalStateException if this method is called on an invalidated
* session
*/
public Enumeration<String> getAttributeNames();
/**
* Binds an object to this session, using the name specified. If an object of
* the same name is already bound to the session, the object is replaced.
* <p>
* After this method executes, and if the new object implements
* <code>HttpSessionBindingListener</code>, the container calls
* <code>HttpSessionBindingListener.valueBound</code>. The container then
* notifies any <code>HttpSessionAttributeListener</code>s in the web
* application.
* <p>
* If an object was already bound to this session of this name that implements
* <code>HttpSessionBindingListener</code>, its
* <code>HttpSessionBindingListener.valueUnbound</code> method is called.
* <p>
* If the value passed in is null, this has the same effect as calling
* <code>removeAttribute()</code>.
*
* @param name the name to which the object is bound; cannot be null
* @param value the object to be bound
* @exception IllegalStateException if this method is called on an invalidated
* session
*/
public void setAttribute(String name, Object value);
/**
* Removes the object bound with the specified name from this session. If the
* session does not have an object bound with the specified name, this method
* does nothing.
* <p>
* After this method executes, and if the object implements
* <code>HttpSessionBindingListener</code>, the container calls
* <code>HttpSessionBindingListener.valueUnbound</code>. The container then
* notifies any <code>HttpSessionAttributeListener</code>s in the web
* application.
*
* @param name the name of the object to remove from this session
* @exception IllegalStateException if this method is called on an invalidated
* session
*/
public void removeAttribute(String name);
/**
* Invalidates this session then unbinds any objects bound to it.
*
* @exception IllegalStateException if this method is called on an already
* invalidated session
*/
public void invalidate();
/**
* Returns <code>true</code> if the client does not yet know about the session
* or if the client chooses not to join the session. For example, if the server
* used only cookie-based sessions, and the client had disabled the use of
* cookies, then a session would be new on each request.
*
* @return <code>true</code> if the server has created a session, but the client
* has not yet joined
* @exception IllegalStateException if this method is called on an already
* invalidated session
*/
public boolean isNew();
}
@@ -0,0 +1,95 @@
package com.taobao.arthas.core.shell.term.impl.http.session;
import java.util.Collections;
import java.util.Set;
import com.taobao.arthas.common.ArthasConstants;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.codec.http.cookie.ServerCookieDecoder;
import io.netty.handler.codec.http.cookie.ServerCookieEncoder;
import io.netty.util.Attribute;
import io.netty.util.AttributeKey;
/**
* <pre>
* netty里的http session管理。因为同一域名的不同端口共享cookie,所以需要共用。
* </pre>
*
* @author hengyunabc 2021-03-03
*
*/
public class HttpSessionManager {
public static AttributeKey<HttpSession> SESSION_KEY = AttributeKey.valueOf("session");
private LRUCache<String, HttpSession> sessions = new LRUCache<String, HttpSession>(1024);
public HttpSessionManager() {
}
private HttpSession getSession(HttpRequest httpRequest) {
// TODO 增加从 url中获取 session id 功能?
Set<Cookie> cookies;
String value = httpRequest.headers().get(HttpHeaderNames.COOKIE);
if (value == null) {
cookies = Collections.emptySet();
} else {
cookies = ServerCookieDecoder.STRICT.decode(value);
}
for (Cookie cookie : cookies) {
if (ArthasConstants.ASESSION_KEY.equals(cookie.name())) {
String sessionId = cookie.value();
return sessions.get(sessionId);
}
}
return null;
}
public static HttpSession getHttpSessionFromContext(ChannelHandlerContext ctx) {
return ctx.channel().attr(SESSION_KEY).get();
}
public HttpSession getOrCreateHttpSession(ChannelHandlerContext ctx, HttpRequest httpRequest) {
// 尝试用 ctx 和从 cookie里读取出 session
Attribute<HttpSession> attribute = ctx.channel().attr(SESSION_KEY);
HttpSession httpSession = attribute.get();
if (httpSession != null) {
return httpSession;
}
httpSession = getSession(httpRequest);
if (httpSession != null) {
attribute.set(httpSession);
return httpSession;
}
// 创建session,并设置到ctx里
httpSession = newHttpSession();
attribute.set(httpSession);
return httpSession;
}
private HttpSession newHttpSession() {
SimpleHttpSession session = new SimpleHttpSession();
this.sessions.put(session.getId(), session);
return session;
}
public static void setSessionCookie(HttpResponse response, HttpSession session) {
response.headers().add(HttpHeaderNames.SET_COOKIE,
ServerCookieEncoder.STRICT.encode(ArthasConstants.ASESSION_KEY, session.getId()));
}
public void start() {
}
public void stop() {
sessions.clear();
}
}
@@ -0,0 +1,100 @@
package com.taobao.arthas.core.shell.term.impl.http.session;
import java.util.LinkedHashMap;
import java.util.Collection;
import java.util.Map;
import java.util.ArrayList;
/**
* An LRU cache, based on <code>LinkedHashMap</code>.
*
* <p>
* This cache has a fixed maximum number of elements (<code>cacheSize</code>).
* If the cache is full and another entry is added, the LRU (least recently
* used) entry is dropped.
*
* <p>
* This class is thread-safe. All methods of this class are synchronized.
*
* <p>
* Author: Christian d'Heureuse, Inventec Informatik AG, Zurich, Switzerland<br>
* Multi-licensed: EPL / LGPL / GPL / AL / BSD.
*/
public class LRUCache<K, V> {
private static final float hashTableLoadFactor = 0.75f;
private LinkedHashMap<K, V> map;
private int cacheSize;
/**
* Creates a new LRU cache.
*
* @param cacheSize the maximum number of entries that will be kept in this
* cache.
*/
public LRUCache(int cacheSize) {
this.cacheSize = cacheSize;
int hashTableCapacity = (int) Math.ceil(cacheSize / hashTableLoadFactor) + 1;
map = new LinkedHashMap<K, V>(hashTableCapacity, hashTableLoadFactor, true) {
// (an anonymous inner class)
private static final long serialVersionUID = 1;
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > LRUCache.this.cacheSize;
}
};
}
/**
* Retrieves an entry from the cache.<br>
* The retrieved entry becomes the MRU (most recently used) entry.
*
* @param key the key whose associated value is to be returned.
* @return the value associated to this key, or null if no value with this key
* exists in the cache.
*/
public synchronized V get(K key) {
return map.get(key);
}
/**
* Adds an entry to this cache. The new entry becomes the MRU (most recently
* used) entry. If an entry with the specified key already exists in the cache,
* it is replaced by the new entry. If the cache is full, the LRU (least
* recently used) entry is removed from the cache.
*
* @param key the key with which the specified value is to be associated.
* @param value a value to be associated with the specified key.
*/
public synchronized void put(K key, V value) {
map.put(key, value);
}
/**
* Clears the cache.
*/
public synchronized void clear() {
map.clear();
}
/**
* Returns the number of used entries in the cache.
*
* @return the number of entries currently in the cache.
*/
public synchronized int usedEntries() {
return map.size();
}
/**
* Returns a <code>Collection</code> that contains a copy of all cache entries.
*
* @return a <code>Collection</code> with a copy of the cache content.
*/
public synchronized Collection<Map.Entry<K, V>> getAll() {
return new ArrayList<Map.Entry<K, V>>(map.entrySet());
}
} // end class LRUCache
@@ -0,0 +1,79 @@
package com.taobao.arthas.core.shell.term.impl.http.session;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import com.taobao.arthas.core.util.StringUtils;
/**
*
* @author hengyunabc 2021-03-03
*
*/
public class SimpleHttpSession implements HttpSession {
private Map<String, Object> attributes = new ConcurrentHashMap<String, Object>();
private String id;
public SimpleHttpSession() {
id = StringUtils.randomString(32);
}
@Override
public long getCreationTime() {
return 0;
}
@Override
public String getId() {
return id;
}
@Override
public long getLastAccessedTime() {
return 0;
}
@Override
public void setMaxInactiveInterval(int interval) {
}
@Override
public int getMaxInactiveInterval() {
return 0;
}
@Override
public Object getAttribute(String name) {
return attributes.get(name);
}
@Override
public Enumeration<String> getAttributeNames() {
return Collections.enumeration(this.attributes.keySet());
}
@Override
public void setAttribute(String name, Object value) {
attributes.put(name, value);
}
@Override
public void removeAttribute(String name) {
attributes.remove(name);
}
@Override
public void invalidate() {
}
@Override
public boolean isNew() {
return false;
}
}
@@ -10,6 +10,7 @@ import com.taobao.arthas.core.shell.term.Term;
import com.taobao.arthas.core.shell.term.TermServer;
import com.taobao.arthas.core.shell.term.impl.Helper;
import com.taobao.arthas.core.shell.term.impl.TermImpl;
import com.taobao.arthas.core.shell.term.impl.http.session.HttpSessionManager;
import io.netty.util.concurrent.EventExecutorGroup;
import io.termd.core.function.Consumer;
@@ -31,12 +32,14 @@ public class HttpTelnetTermServer extends TermServer {
private int port;
private long connectionTimeout;
private EventExecutorGroup workerGroup;
private HttpSessionManager httpSessionManager;
public HttpTelnetTermServer(String hostIp, int port, long connectionTimeout, EventExecutorGroup workerGroup) {
public HttpTelnetTermServer(String hostIp, int port, long connectionTimeout, EventExecutorGroup workerGroup, HttpSessionManager httpSessionManager) {
this.hostIp = hostIp;
this.port = port;
this.connectionTimeout = connectionTimeout;
this.workerGroup = workerGroup;
this.httpSessionManager = httpSessionManager;
}
@Override
@@ -48,7 +51,7 @@ public class HttpTelnetTermServer extends TermServer {
@Override
public TermServer listen(Handler<Future<TermServer>> listenHandler) {
// TODO: charset and inputrc from options
bootstrap = new NettyHttpTelnetTtyBootstrap(workerGroup).setHost(hostIp).setPort(port);
bootstrap = new NettyHttpTelnetTtyBootstrap(workerGroup, httpSessionManager).setHost(hostIp).setPort(port);
try {
bootstrap.start(new Consumer<TtyConnection>() {
@Override
@@ -1,5 +1,7 @@
package com.taobao.arthas.core.shell.term.impl.httptelnet;
import com.taobao.arthas.core.shell.term.impl.http.session.HttpSessionManager;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
@@ -31,11 +33,13 @@ public class NettyHttpTelnetBootstrap extends TelnetBootstrap {
private EventLoopGroup group;
private ChannelGroup channelGroup;
private EventExecutorGroup workerGroup;
private HttpSessionManager httpSessionManager;
public NettyHttpTelnetBootstrap(EventExecutorGroup workerGroup) {
public NettyHttpTelnetBootstrap(EventExecutorGroup workerGroup, HttpSessionManager httpSessionManager) {
this.workerGroup = workerGroup;
this.group = new NioEventLoopGroup(new DefaultThreadFactory("arthas-NettyHttpTelnetBootstrap", true));
this.channelGroup = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
this.httpSessionManager = httpSessionManager;
}
public NettyHttpTelnetBootstrap setHost(String host) {
@@ -60,7 +64,7 @@ public class NettyHttpTelnetBootstrap extends TelnetBootstrap {
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ProtocolDetectHandler(channelGroup, handlerFactory, factory, workerGroup));
ch.pipeline().addLast(new ProtocolDetectHandler(channelGroup, handlerFactory, factory, workerGroup, httpSessionManager));
}
});
@@ -2,6 +2,8 @@ package com.taobao.arthas.core.shell.term.impl.httptelnet;
import java.nio.charset.Charset;
import com.taobao.arthas.core.shell.term.impl.http.session.HttpSessionManager;
import io.netty.util.concurrent.EventExecutorGroup;
import io.termd.core.function.Consumer;
import io.termd.core.function.Supplier;
@@ -22,8 +24,8 @@ public class NettyHttpTelnetTtyBootstrap {
private boolean inBinary;
private Charset charset = Charset.forName("UTF-8");
public NettyHttpTelnetTtyBootstrap(EventExecutorGroup workerGroup) {
this.httpTelnetTtyBootstrap = new NettyHttpTelnetBootstrap(workerGroup);
public NettyHttpTelnetTtyBootstrap(EventExecutorGroup workerGroup, HttpSessionManager httpSessionManager) {
this.httpTelnetTtyBootstrap = new NettyHttpTelnetBootstrap(workerGroup, httpSessionManager);
}
public String getHost() {
@@ -1,12 +1,14 @@
package com.taobao.arthas.core.shell.term.impl.httptelnet;
import java.io.File;
import java.util.concurrent.TimeUnit;
import com.taobao.arthas.common.ArthasConstants;
import com.taobao.arthas.core.shell.term.impl.http.BasicHttpAuthenticatorHandler;
import com.taobao.arthas.core.shell.term.impl.http.HttpRequestHandler;
import com.taobao.arthas.core.shell.term.impl.http.TtyWebSocketFrameHandler;
import com.taobao.arthas.core.shell.term.impl.http.session.HttpSessionManager;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
@@ -35,13 +37,16 @@ public class ProtocolDetectHandler extends ChannelInboundHandlerAdapter {
private Supplier<TelnetHandler> handlerFactory;
private Consumer<TtyConnection> ttyConnectionFactory;
private EventExecutorGroup workerGroup;
private HttpSessionManager httpSessionManager;
public ProtocolDetectHandler(ChannelGroup channelGroup, final Supplier<TelnetHandler> handlerFactory,
Consumer<TtyConnection> ttyConnectionFactory, EventExecutorGroup workerGroup) {
Consumer<TtyConnection> ttyConnectionFactory, EventExecutorGroup workerGroup,
HttpSessionManager httpSessionManager) {
this.channelGroup = channelGroup;
this.handlerFactory = handlerFactory;
this.ttyConnectionFactory = ttyConnectionFactory;
this.workerGroup = workerGroup;
this.httpSessionManager = httpSessionManager;
}
private ScheduledFuture<?> detectTelnetFuture;
@@ -87,6 +92,7 @@ public class ProtocolDetectHandler extends ChannelInboundHandlerAdapter {
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new ChunkedWriteHandler());
pipeline.addLast(new HttpObjectAggregator(ArthasConstants.MAX_HTTP_CONTENT_LENGTH));
pipeline.addLast(new BasicHttpAuthenticatorHandler(httpSessionManager));
pipeline.addLast(workerGroup, "HttpRequestHandler", new HttpRequestHandler("/ws"));
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
pipeline.addLast(new IdleStateHandler(0, 0, ArthasConstants.WEBSOCKET_IDLE_SECONDS));
@@ -13,6 +13,7 @@ import java.util.StringTokenizer;
import java.util.TreeSet;
public abstract class StringUtils {
private static final String AB = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
/**
* 获取异常的原因描述
@@ -912,4 +913,40 @@ public abstract class StringUtils {
}
return result;
}
public static String randomString(int length) {
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++)
sb.append(AB.charAt(ThreadLocalRandom.current().nextInt(AB.length())));
return sb.toString();
}
/**
* Returns the string before the given token
*
* @param text the text
* @param before the token
* @return the text before the token, or <tt>null</tt> if text does not contain
* the token
*/
public static String before(String text, String before) {
int pos = text.indexOf(before);
return pos == -1 ? null : text.substring(0, pos);
}
/**
* Returns the string after the given token
*
* @param text the text
* @param after the token
* @return the text after the token, or <tt>null</tt> if text does not contain
* the token
*/
public static String after(String text, String after) {
int pos = text.indexOf(after);
if (pos == -1) {
return null;
}
return text.substring(pos + after.length());
}
}