Add auth command, support authentication (#1724)

pull/1234/merge
hengyunabc 4 years ago committed by GitHub
parent 985963b9d1
commit e520b10319
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -145,6 +145,12 @@ STAT_URL=
# app name
APP_NAME=
# username
USERNAME=
# password
PASSWORD=
############ Command Arguments ############
# if arguments contains -c/--command or -f/--batch-file, BATCH_MODE will be true
@ -400,6 +406,7 @@ Usage:
[--http-port <value>] [--session-timeout <value>] [--arthas-home <value>]
[--tunnel-server <value>] [--agent-id <value>] [--stat-url <value>]
[--app-name <value>]
[--username <value>] [--password <value>]
[--use-version <value>] [--repo-mirror <value>] [--versions] [--use-http]
[--attach-only] [-c <value>] [-f <value>] [-v] [pid]
@ -420,6 +427,8 @@ Options and Arguments:
--tunnel-server Remote tunnel server url
--agent-id Special agent id
--app-name Special app name
--username Special username
--password Special password
--select select target process by classname or JARfilename
-c,--command <value> Command to execute, multiple commands separated
by ;
@ -433,6 +442,7 @@ EXAMPLES:
./as.sh <pid>
./as.sh --target-ip 0.0.0.0
./as.sh --telnet-port 9999 --http-port -1
./as.sh --username admin --password <password>
./as.sh --tunnel-server 'ws://192.168.10.11:7777/ws' --app-name demoapp
./as.sh --tunnel-server 'ws://192.168.10.11:7777/ws' --agent-id bvDOe8XbTM2pQWjF4cfw
./as.sh --stat-url 'http://192.168.10.11:8080/api/stat'
@ -607,6 +617,16 @@ parse_arguments()
shift # past argument
shift # past value
;;
--username)
USERNAME="$2"
shift # past argument
shift # past value
;;
--password)
PASSWORD="$2"
shift # past argument
shift # past value
;;
--use-http)
USE_HTTP=true
shift # past argument
@ -806,6 +826,16 @@ attach_jvm()
tempArgs+=("${APP_NAME}")
fi
if [ "${USERNAME}" ]; then
tempArgs+=("-username")
tempArgs+=("${USERNAME}")
fi
if [ "${PASSWORD}" ]; then
tempArgs+=("-password")
tempArgs+=("${PASSWORD}")
fi
if [ "${TARGET_IP}" ]; then
tempArgs+=("-target-ip")
tempArgs+=("${TARGET_IP}")

@ -47,6 +47,7 @@ import static com.taobao.arthas.boot.ProcessUtils.STATUS_EXEC_TIMEOUT;
@Summary("Bootstrap Arthas")
@Description("EXAMPLES:\n" + " java -jar arthas-boot.jar <pid>\n" + " java -jar arthas-boot.jar --target-ip 0.0.0.0\n"
+ " java -jar arthas-boot.jar --telnet-port 9999 --http-port -1\n"
+ " java -jar arthas-boot.jar --username admin --password <password>\n"
+ " java -jar arthas-boot.jar --tunnel-server 'ws://192.168.10.11:7777/ws' --app-name demoapp\n"
+ " java -jar arthas-boot.jar --tunnel-server 'ws://192.168.10.11:7777/ws' --agent-id bvDOe8XbTM2pQWjF4cfw\n"
+ " java -jar arthas-boot.jar --stat-url 'http://192.168.10.11:8080/api/stat'\n"
@ -120,6 +121,9 @@ public class Bootstrap {
private String appName;
private String username;
private String password;
private String statUrl;
private String select;
@ -266,6 +270,17 @@ public class Bootstrap {
this.appName = appName;
}
@Option(longName = "username")
@Description("The username")
public void setUsername(String username) {
this.username = username;
}
@Option(longName = "password")
@Description("The password")
public void setPassword(String password) {
this.password = password;
}
@Option(longName = "stat-url")
@Description("The report stat url")
public void setStatUrl(String statUrl) {
@ -505,6 +520,15 @@ public class Bootstrap {
attachArgs.add("" + bootstrap.getSessionTimeout());
}
if (bootstrap.getUsername() != null) {
attachArgs.add("-username");
attachArgs.add(bootstrap.getUsername());
}
if (bootstrap.getPassword() != null) {
attachArgs.add("-password");
attachArgs.add(bootstrap.getPassword());
}
if (bootstrap.getTunnelServer() != null) {
attachArgs.add("-tunnel-server");
attachArgs.add(bootstrap.getTunnelServer());
@ -812,4 +836,12 @@ public class Bootstrap {
public String getSelect() {
return select;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
}

@ -26,4 +26,13 @@ public class ArthasConstants {
public static final int TELNET_PORT = 3658;
public static final int WEBSOCKET_IDLE_SECONDS = 60;
/**
* HTTP cookie id
*/
public static final String ASESSION_KEY = "asession";
public static final String DEFAULT_USERNAME = "arthas";
public static final String SUBJECT_KEY = "subject";
public static final String AUTH = "auth";
}

@ -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>
* nettyhttp sessioncookie
* </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());
}
}

@ -8,6 +8,8 @@ $(function () {
if (ip != '' && ip != null) {
$('#ip').val(ip);
} else {
$('#ip').val(window.location.hostname);
}
if (port != '' && port != null) {
$('#port').val(port);

@ -0,0 +1,40 @@
package com.taobao.arthas.core.security;
import java.security.Principal;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginException;
import org.assertj.core.api.Assertions;
import org.junit.Test;
/**
*
* @author hengyunabc 2021-03-04
*
*/
public class SecurityAuthenticatorImplTest {
@Test
public void test1() throws LoginException {
String username = "test";
String password = "ppp";
SecurityAuthenticatorImpl auth = new SecurityAuthenticatorImpl(username, password);
Assertions.assertThat(auth.needLogin()).isTrue();
Principal principal = new BasicPrincipal(username, password);
Subject subject = auth.login(principal);
Assertions.assertThat(subject).isNotNull();
}
@Test
public void test2() {
String username = "test";
String password = null;
SecurityAuthenticatorImpl auth = new SecurityAuthenticatorImpl(username, password);
Assertions.assertThat(auth.needLogin()).isTrue();
}
}

@ -73,131 +73,8 @@
<module>packaging</module>
</modules>
<profiles>
<profile>
<id>jdk8</id>
<activation>
<jdk>[1.8,)</jdk>
</activation>
<modules>
<module>tunnel-server</module>
</modules>
<build>
<plugins>
<!-- git commit info -->
<plugin>
<groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId>
<version>4.0.1</version>
<executions>
<execution>
<goals>
<goal>revision</goal>
</goals>
</execution>
</executions>
<configuration>
<verbose>false</verbose>
<dateFormat>yyyy-MM-dd'T'HH:mm:ssZ</dateFormat>
<generateGitPropertiesFile>true</generateGitPropertiesFile>
<generateGitPropertiesFilename>${project.build.outputDirectory}/arthas-git.properties</generateGitPropertiesFilename>
<excludeProperties>
<excludeProperty>git.branch</excludeProperty>
<excludeProperty>git.build.host</excludeProperty>
<excludeProperty>git.build.time</excludeProperty>
<excludeProperty>git.build.user.email</excludeProperty>
<excludeProperty>git.build.user.name</excludeProperty>
<excludeProperty>git.remote.origin.url</excludeProperty>
<excludeProperty>git.total.commit.count</excludeProperty>
<excludeProperty>git.commit.time</excludeProperty>
<excludeProperty>git.local.branch.ahead</excludeProperty>
<excludeProperty>git.local.branch.behind</excludeProperty>
<excludeProperty>git.tags</excludeProperty>
</excludeProperties>
<injectAllReactorProjects>true</injectAllReactorProjects>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<!-- ci test -->
<id>jdk12</id>
<activation>
<jdk>[12,)</jdk>
<property>
<name>JAVA8_HOME</name>
</property>
</activation>
<dependencies>
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.6.0</version>
<scope>system</scope>
<systemPath>${JAVA8_HOME}/lib/tools.jar</systemPath>
</dependency>
</dependencies>
</profile>
<profile>
<id>full</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<doclint>none</doclint>
<source>1.8</source>
<failOnError>false</failOnError>
</configuration>
<executions>
<execution>
<id>release</id>
<phase>package</phase>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>release</id>
<activation>
<property>
<name>performRelease</name>
<value>true</value>
</property>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<properties>
<revision>3.4.8</revision>
<revision>3.4.9-SNAPSHOT</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.target>1.6</maven.compiler.target>
<maven.compiler.source>1.6</maven.compiler.source>
@ -342,6 +219,129 @@
</dependencies>
</dependencyManagement>
<profiles>
<profile>
<id>jdk8</id>
<activation>
<jdk>[1.8,)</jdk>
</activation>
<modules>
<module>tunnel-server</module>
</modules>
<build>
<plugins>
<!-- git commit info -->
<plugin>
<groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId>
<version>4.0.1</version>
<executions>
<execution>
<goals>
<goal>revision</goal>
</goals>
</execution>
</executions>
<configuration>
<verbose>false</verbose>
<dateFormat>yyyy-MM-dd'T'HH:mm:ssZ</dateFormat>
<generateGitPropertiesFile>true</generateGitPropertiesFile>
<generateGitPropertiesFilename>${project.build.outputDirectory}/arthas-git.properties</generateGitPropertiesFilename>
<excludeProperties>
<excludeProperty>git.branch</excludeProperty>
<excludeProperty>git.build.host</excludeProperty>
<excludeProperty>git.build.time</excludeProperty>
<excludeProperty>git.build.user.email</excludeProperty>
<excludeProperty>git.build.user.name</excludeProperty>
<excludeProperty>git.remote.origin.url</excludeProperty>
<excludeProperty>git.total.commit.count</excludeProperty>
<excludeProperty>git.commit.time</excludeProperty>
<excludeProperty>git.local.branch.ahead</excludeProperty>
<excludeProperty>git.local.branch.behind</excludeProperty>
<excludeProperty>git.tags</excludeProperty>
</excludeProperties>
<injectAllReactorProjects>true</injectAllReactorProjects>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<!-- ci test -->
<id>jdk12</id>
<activation>
<jdk>[12,)</jdk>
<property>
<name>JAVA8_HOME</name>
</property>
</activation>
<dependencies>
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.6.0</version>
<scope>system</scope>
<systemPath>${JAVA8_HOME}/lib/tools.jar</systemPath>
</dependency>
</dependencies>
</profile>
<profile>
<id>full</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<doclint>none</doclint>
<source>1.8</source>
<failOnError>false</failOnError>
</configuration>
<executions>
<execution>
<id>release</id>
<phase>package</phase>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>release</id>
<activation>
<property>
<name>performRelease</name>
<value>true</value>
</property>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<build>
<pluginManagement>
<plugins>

@ -64,6 +64,9 @@
* [profiler](profiler.md)--使用[async-profiler](https://github.com/jvm-profiling-tools/async-profiler)对应用采样,生成火焰图
## 鉴权
* [auth](auth.md)--鉴权
## options
* [options](options.md)——查看或设置Arthas全局开关

@ -0,0 +1,46 @@
auth
===
> 验证当前会话
### 配置用户名和密码
在attach时可以在命令行指定密码。比如
```
java -jar arthas-boot.jar --password ppp
```
* 可以通过 `--username` 选项来指定用户,默认值是`arthas`。
* 也可以在 arthas.properties 里中配置 username/password。命令行的优先级大于配置文件。
### 在telnet console里鉴权
连接到arthas后直接执行命令会提示需要鉴权
```bash
[arthas@37430]$ help
Error! command not permitted, try to use 'auth' command to authenticates.
```
使用`auth`命令来鉴权,成功之后可以执行其它命令。
```
[arthas@37430]$ auth ppp
Authentication result: true
```
* 可以通过 `--username` 选项来指定用户,默认值是`arthas`。
### web console密码验证
打开浏览器,会有弹窗提示需要输入 用户名 和 密码。
成功之后,则可以直接连接上 web console。
### http api验证
Arthas 采用的是 HTTP 标准的 Basic Authorization客户端请求时增加对应的header即可。
* 参考:[https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication)

@ -39,6 +39,7 @@
* [base64](base64.md)
* [tee](tee.md)
* [pwd](pwd.md)
* [auth](auth.md)
* [options](options.md)
### Arthas 基础命令

@ -55,6 +55,10 @@ Advanced Usage
* [stack](stack.md) - display the stack trace for the specified class and method
* [tt](tt.md) - time tunnel, record the arguments and returned value for the methods and replay
## authentication
* [auth](auth.md) - authentication
## options
* [options](options.md) - check/set Arthas global options

@ -0,0 +1,45 @@
auth
===
> Authenticates the current session
### Configure username and password
When attaching, you can specify a password on the command line. such as:
```
java -jar arthas-boot.jar --password ppp
```
* The user can be specified by the `--username` option, the default value is `arthas`.
* You can also configure username/password in arthas.properties. The priority of the command line is higher than that of the configuration file.
### Authenticate in the telnet console
After connecting to arthas, directly executing the command will prompt for authentication:
```bash
[arthas@37430]$ help
Error! command not permitted, try to use 'auth' command to authenticates.
```
Use the `auth` command to authenticate, and you can execute other commands after success.
```
[arthas@37430]$ auth ppp
Authentication result: true
```
* The user can be specified by the `--username` option, the default value is `arthas`.
### Web console Authentication
Open the browser, there will be a pop-up window prompting you to enter your username and password.
After success, you can directly connect to the web console.
### HTTP API Authentication
Arthas uses the HTTP standard Basic Authorization.
* Reference: [https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication)

@ -39,6 +39,7 @@ All Commands
* [base64](base64.md)
* [tee](tee.md)
* [pwd](pwd.md)
* [auth](auth.md)
* [options](options.md)

@ -34,7 +34,7 @@ public class TunnelSocketServerInitializer extends ChannelInitializer<SocketChan
pipeline.addLast(sslCtx.newHandler(ch.alloc()));
}
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(65536));
pipeline.addLast(new HttpObjectAggregator(ArthasConstants.MAX_HTTP_CONTENT_LENGTH));
pipeline.addLast(new WebSocketServerCompressionHandler());
pipeline.addLast(new WebSocketServerProtocolHandler(tunnelServer.getPath(), null, true, ArthasConstants.MAX_HTTP_CONTENT_LENGTH, false, true, 10000L));

Loading…
Cancel
Save