You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hugegraph.apache.org by ji...@apache.org on 2026/10/17 20:42:06 UTC
(hugegraph) branch master updated: refactor: improve activation and local diagnosis tool (#2945)
This is an automated email from the ASF dual-hosted git repository.
imbajin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hugegraph.git
The following commit(s) were added to refs/heads/master by this push:
new d505f6f02 refactor: improve activation and local diagnosis tool (#2945)
d505f6f02 is described below
commit d505f6f02503d36ff4f6735686cede3500640a51
Author: Soyaazz <52...@qq.com>
AuthorDate: Sun Oct 18 04:42:01 2026 +0800
refactor: improve activation and local diagnosis tool (#2945)
Arthas could previously be activated through unprotected HTTP endpoints, with diagnostic ports bound to 0.0.0.0. Server now requires configured authentication and an authenticated administrator for PUT /arthas; Store's unauthenticated /v1/arthasstart returns HTTP 403.
---
.github/workflows/pd-store-ci.yml | 12 +-
.github/workflows/server-tests.yml | 7 +
docs/server-runtime.md | 137 ++++++-
hugegraph-server/Dockerfile | 3 +-
hugegraph-server/Dockerfile-hstore | 3 +-
.../org/apache/hugegraph/api/arthas/ArthasAPI.java | 75 +++-
.../org/apache/hugegraph/config/ServerOptions.java | 38 +-
hugegraph-server/hugegraph-dist/pom.xml | 27 ++
.../src/assembly/descriptor/assembly.xml | 4 +
.../src/assembly/static/bin/ArthasAttach.java | 96 +++++
.../src/assembly/static/bin/arthas.sh | 232 ++++++++++++
.../src/assembly/static/bin/gremlin-console.sh | 1 +
.../src/assembly/static/bin/hugegraph-server.sh | 1 +
.../src/assembly/static/bin/init-store.sh | 3 +-
.../assembly/static/conf/rest-server.properties | 12 +-
.../src/assembly/travis/test-arthas-cli.sh | 404 +++++++++++++++++++++
.../assembly/travis/test-check-jacoco-report.sh | 2 +
.../org/apache/hugegraph/api/ArthasApiTest.java | 33 +-
.../hugegraph/api/arthas/ArthasConfigTest.java | 244 +++++++++++++
.../org/apache/hugegraph/unit/UnitTestSuite.java | 2 +
hugegraph-server/pom.xml | 1 -
hugegraph-store/Dockerfile | 3 +-
hugegraph-store/README.md | 3 +
hugegraph-store/hg-store-dist/pom.xml | 33 ++
.../src/assembly/descriptor/server-assembly.xml | 15 +-
.../src/assembly/static/conf/application.yml | 3 +
hugegraph-store/hg-store-node/pom.xml | 11 -
.../org/apache/hugegraph/store/node/AppConfig.java | 4 +-
.../store/node/controller/PartitionAPI.java | 26 +-
.../src/main/resources/application.yml | 3 +
.../store/node/controller/ArthasSecurityTest.java | 85 +++++
install-dist/release-docs/LICENSE | 6 +-
.../scripts/dependency/known-dependencies.txt | 9 +-
pom.xml | 2 +
34 files changed, 1471 insertions(+), 69 deletions(-)
diff --git a/.github/workflows/pd-store-ci.yml b/.github/workflows/pd-store-ci.yml
index 8dcc74b8f..ceb1d1121 100644
--- a/.github/workflows/pd-store-ci.yml
+++ b/.github/workflows/pd-store-ci.yml
@@ -374,6 +374,12 @@ jobs:
run: |
mvn clean package -U -Dmaven.javadoc.skip=true -Dmaven.test.skip=true -ntp --fail-at-end
+ - name: Verify Store Arthas local CLI attachment
+ run: |
+ VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
+ STORE_DIR=hugegraph-store/apache-hugegraph-store-$VERSION
+ bash "$TRAVIS_DIR/test-arthas-cli.sh" "$STORE_DIR"
+
- name: Verify Surefire single-class routing
run: |
SUREFIRE_DIR=hugegraph-store/hg-store-test/target/surefire-reports
@@ -446,12 +452,12 @@ jobs:
echo "PD_DIR=hugegraph-pd/apache-hugegraph-pd-$VERSION" >> "$GITHUB_ENV"
echo "STORE_DIR=hugegraph-store/apache-hugegraph-store-$VERSION" >> "$GITHUB_ENV"
- - name: Run historical Store ingress tests
+ - name: Run Store ingress and Arthas security tests
run: |
set -o pipefail
RUNTIME_LOG=$(mktemp "$RUNNER_TEMP/java-runtime.XXXXXX")
mvn test -pl hugegraph-store/hg-store-node -am \
- -Dtest=StoredRowIngressTest -Dsurefire.failIfNoSpecifiedTests=false \
+ -Dtest=StoredRowIngressTest,ArthasSecurityTest -Dsurefire.failIfNoSpecifiedTests=false \
-Djacoco.sessionId=store-node-test \
-Djacoco.destFile="$PWD/hugegraph-store/hg-store-test/target/store-node-test.exec" 2>&1 | tee "$RUNTIME_LOG"
bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RUNTIME_LOG" "$PD_DIR/logs" "$STORE_DIR/logs"
@@ -526,6 +532,8 @@ jobs:
"$TEST_REPORT_DIR/TEST-org.apache.hugegraph.store.service.ServerSuiteTest.xml" \
--require-test-report \
"hugegraph-store/hg-store-node/target/surefire-reports/TEST-org.apache.hugegraph.store.business.StoredRowIngressTest.xml" \
+ --require-test-report \
+ "hugegraph-store/hg-store-node/target/surefire-reports/TEST-org.apache.hugegraph.store.node.controller.ArthasSecurityTest.xml" \
--require-covered-group hg-store-common \
--require-covered-group hg-store-client \
--require-covered-group hg-store-rocksdb \
diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml
index c503bacf9..1f707604e 100644
--- a/.github/workflows/server-tests.yml
+++ b/.github/workflows/server-tests.yml
@@ -104,6 +104,13 @@ jobs:
$TRAVIS_DIR/test-java-security-properties.sh \
"$SERVER_DIR" "$GITHUB_WORKSPACE"
+ - name: Check bundled Arthas local CLI
+ if: ${{ env.BACKEND == 'rocksdb' }}
+ run: |
+ VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
+ SERVER_DIR=hugegraph-server/apache-hugegraph-server-$VERSION
+ bash "$TRAVIS_DIR/test-arthas-cli.sh" "$SERVER_DIR"
+
- name: Verify Surefire single-class routing
if: ${{ env.BACKEND == 'rocksdb' }}
run: |
diff --git a/docs/server-runtime.md b/docs/server-runtime.md
index d3466cce1..ec997c0cd 100644
--- a/docs/server-runtime.md
+++ b/docs/server-runtime.md
@@ -79,11 +79,138 @@ existing precedence; see [container heap sizing](../docker/README.md#server-heap
### Arthas diagnostics
-Server bundles Arthas 4.1.5. Activate the agent with `PUT /arthas` using the Server's
-configured authentication. The existing `arthas.ip`, `arthas.httpPort`,
-`arthas.telnetPort` and `arthas.disabledCommands` options still configure the agent.
-HugeGraph disables the bundled agent's optional MCP endpoint and keeps the existing
-activation response and diagnostic HTTP/telnet interfaces.
+Server and Store bundle Arthas 4.1.5, managed by the root POM. PD does not embed
+an Arthas agent. Diagnostic ports bind to `127.0.0.1` by default. Administrators
+can explicitly configure another valid address, including wildcard `0.0.0.0` or
+`::`; `localhost` is normalized to `127.0.0.1`. Wildcard listeners can accept both
+IPv4 and IPv6 depending on the OS/JVM configuration; restrict both in firewall
+rules. Server's `PUT /arthas`
+requires authentication to be enabled and the caller to be an authenticated administrator.
+Without authentication, the Server endpoint returns HTTP 403, including requests
+from localhost or through a reverse proxy. Store has no equivalent administrator
+authentication, so `GET /v1/arthasstart` always returns HTTP 403.
+
+For local diagnosis of either running service, use its distribution's bundled
+helper as the same OS user that owns the JVM:
+
+```bash
+bin/arthas.sh
+```
+
+The distribution prepares the agent in `lib/arthas` at build time. The helper
+requires JDK 17 and uses `bin/pid`, without downloads, runtime extraction, a user
+cache, service restarts or configuration edits. A new local agent listens on
+`127.0.0.1:3658`, with HTTP, MCP and reverse tunnels disabled. Use `--pid` or
+`--telnet-port` for another running instance, and `--command version` for a one-time
+command. Interactive mode requires a terminal; for pipes, scripts or `docker exec`
+without `-it`, use `--command` instead. Run `bin/arthas.sh --help` for usage.
+When the helper exits, including after `--command`, `quit`, or INT/TERM/HUP, it
+stops the agent and all its diagnostic sessions; the application JVM keeps running.
+SIGKILL or a host failure cannot run this cleanup: stop any surviving agent manually.
+For remote diagnosis, log in to the service host over SSH and run the same command,
+or explicitly configure a reachable listener with appropriate authentication and
+network access controls as described below.
+Official Server (including HStore) and Store images include JDK 17, so the same
+helper works inside the container, for example
+`docker exec -it <store-container> /hugegraph-store/bin/arthas.sh`.
+A JRE-only installation needs JDK 17 for this helper.
+If the Server API has already activated Arthas, connect to its existing telnet
+port instead: `bin/arthas.sh --telnet-port 8562` with the default configuration.
+Use `--target-ip` to select another bind address, for example
+`bin/arthas.sh --target-ip 0.0.0.0` or
+`bin/arthas.sh --target-ip ::1 --telnet-port 8562 --command version`.
+The default is `127.0.0.1`; `localhost` maps to `127.0.0.1`. Wildcard addresses
+`0.0.0.0` and `::` may accept both IPv4 and IPv6; the helper connects and cleans
+up through `127.0.0.1` for either wildcard. Remote clients connect
+to the service host's actual reachable address, not the wildcard address.
+An already running agent retains its listener and authentication settings;
+helper options do not reconfigure it. To switch to the helper's
+HTTP-disabled defaults, run `bin/arthas.sh --telnet-port 8562 --command stop`, then
+`bin/arthas.sh`; this stops only Arthas, and leaves the service JVM running.
+Arthas can execute code inside the JVM. OS attach permissions control initial
+activation; HugeGraph administrator authentication protects only the activation
+API, not subsequent diagnostic connections. We recommend enabling Arthas native
+authentication, especially when binding beyond loopback. However, native defaults
+allow connections from `127.0.0.1` to skip authentication even with credentials;
+other OS users or containers in the same Pod can still connect this way. To disable
+this bypass, set `-Darthas.localConnectionNonAuth=false` on the service JVM and
+restart it. Restrict host/Pod and port-forward access to trusted operators, and
+limit firewall rules, port mappings and SSH forwarding accordingly. Authentication
+does not encrypt telnet
+or ordinary HTTP traffic; use a protected network or encrypted transport for
+remote access. Avoid concurrent helper sessions because exiting one stops the
+shared agent. A disabled-command list is optional and is not access control.
+
+Pass native Arthas credentials when starting or reusing an agent:
+
+```bash
+bin/arthas.sh --target-ip 0.0.0.0 \
+ --username diagnostics --password '<arthas-password>'
+```
+
+Use single-line credentials; printable ASCII avoids the bundled Arthas 4.1.5
+telnet client's Unicode limitation. Password-only use defaults to username `arthas`.
+Native JVM credentials also apply to helper-started agents: if the target JVM has
+`-Darthas.username` or `-Darthas.password`, supply the effective credentials with
+`--username` / `--password` for authenticated commands and cleanup, especially
+when `-Darthas.localConnectionNonAuth=false` is set. The helper does not clear
+native credentials when the corresponding option is omitted.
+These credentials are separate from HugeGraph accounts. Batch commands and exit
+cleanup use the supplied credentials automatically. For interactive diagnosis,
+if prompted, enter `auth --username diagnostics <arthas-password>` in the Arthas
+console. Existing agents require their existing credentials; passing new values
+does not change them. Password arguments may appear in shell history and process
+listings, so handle them on a trusted host. With `--username` alone, native Arthas
+generates a password only when none is already configured in the target JVM's
+native settings; otherwise it uses that inherited password. Supply `--password` for
+authenticated batch commands and automatic cleanup. If cleanup fails, the helper
+returns nonzero and the agent may remain running; use the configured or generated credentials with
+native `auth`, then `stop`, to close it without stopping the service JVM.
+
+[Arthas native authentication](https://arthas.aliyun.com/en/doc/auth.html) remains
+optional and follows upstream defaults: `arthas.localConnectionNonAuth=true`
+allows connections from `127.0.0.1` to skip authentication. The helper uses a private
+configuration, so editing `lib/arthas/arthas.properties` does not configure it.
+If required, the target JVM property `-Darthas.localConnectionNonAuth=false` is
+honored after restarting the service JVM. Arthas 4.1.5 generates a password when
+binding `0.0.0.0` or `::` without an effective native password and prints it in the target
+JVM output and Arthas log; explicitly supplying credentials is more convenient
+for remote diagnosis and authenticated cleanup.
+
+Authenticated Server administrators can continue using `PUT /arthas` without an
+additional enable switch or restart. Its HTTP/telnet interfaces use the configured
+bind address; HugeGraph disables the optional MCP endpoint. The activation response
+still contains the same four Arthas configuration keys. For API activation with
+native credentials, add `-Darthas.username=diagnostics` and
+`-Darthas.password=<arthas-password>` to the Server JVM startup options and restart
+Server before activation. These are native JVM properties, not new REST
+configuration keys, and also apply to agents started by the local helper as described
+above. They may expose secrets in process listings. Without native
+credential settings, a specific non-loopback bind address does not require login; wildcard
+binds use the generated password described above. API activation alone
+leaves the agent running until an operator sends `stop`; connecting through the
+helper applies the helper's exit cleanup to that existing agent too.
+
+Before reusing an API-started agent, leave `session` and `stop` out of
+`arthas.disabled_commands` (or its legacy alias). `session` verifies the target
+PID; `stop` closes Arthas without stopping the service JVM. Existing configurations
+remain accepted, but failed PID verification or listener cleanup makes the helper
+print an error and exit nonzero; the agent may remain running. To recover from an
+agent with `stop` disabled, remove that restriction and restart the service before
+activating Arthas again. Newly attached local agents keep both commands available.
+
+No commands are disabled by default. Administrators can optionally set
+`arthas.disabled_commands=jad,mc` to restrict selected commands for API activation;
+new local agents do not inherit this list.
+
+The existing `arthas.ip`, `arthas.httpPort`, `arthas.telnetPort` and
+`arthas.disabledCommands` options configure the Server API-activated agent.
+The distribution uses `arthas.telnet_port`, `arthas.http_port` and
+`arthas.disabled_commands`. These snake_case keys take precedence over the legacy
+camelCase aliases when both are present, including explicit default values.
+Conflicting values produce a warning naming both keys and the effective key when
+the agent is activated. Remove the unused alias to avoid ambiguity. Store's legacy
+configuration fields are retained; the local helper does not read them.
For a synchronous one-time command, POST `{"action":"exec","command":"version"}`
to the agent's `/api` endpoint without a `sessionId`. Check `state=SUCCEEDED`, the
diff --git a/hugegraph-server/Dockerfile b/hugegraph-server/Dockerfile
index 1ec3ce8bd..694acd6f2 100644
--- a/hugegraph-server/Dockerfile
+++ b/hugegraph-server/Dockerfile
@@ -35,7 +35,8 @@ RUN --mount=type=cache,id=hugegraph-maven-${SOURCE_REVISION},target=/root/.m2,sh
# 2nd stage: runtime env
# Note: ZGC (The Z Garbage Collector) is only supported on ARM-Mac with java > 13
-FROM eclipse-temurin:17-jre-jammy
+FROM eclipse-temurin:17-jdk-jammy
+# Local Arthas attach requires the JDK attach and compiler modules.
LABEL maintainer="HugeGraph Docker Maintainers <de...@hugegraph.apache.org>"
diff --git a/hugegraph-server/Dockerfile-hstore b/hugegraph-server/Dockerfile-hstore
index 1b2f0e531..351b116d6 100644
--- a/hugegraph-server/Dockerfile-hstore
+++ b/hugegraph-server/Dockerfile-hstore
@@ -35,7 +35,8 @@ RUN --mount=type=cache,id=hugegraph-maven-${SOURCE_REVISION},target=/root/.m2,sh
# 2nd stage: runtime env
# Note: ZGC (The Z Garbage Collector) is only supported on ARM-Mac with java > 13
-FROM eclipse-temurin:17-jre-jammy
+FROM eclipse-temurin:17-jdk-jammy
+# Local Arthas attach requires the JDK attach and compiler modules.
LABEL maintainer="HugeGraph Docker Maintainers <de...@hugegraph.apache.org>"
diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/arthas/ArthasAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/arthas/ArthasAPI.java
index 77e961d4d..f2dd9e980 100644
--- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/arthas/ArthasAPI.java
+++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/arthas/ArthasAPI.java
@@ -17,48 +17,109 @@
package org.apache.hugegraph.api.arthas;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
import java.util.HashMap;
import org.apache.hugegraph.api.API;
+import org.apache.hugegraph.auth.HugeAuthenticator;
+import org.apache.hugegraph.config.ConfigOption;
import org.apache.hugegraph.config.HugeConfig;
import org.apache.hugegraph.config.ServerOptions;
+import org.apache.hugegraph.core.GraphManager;
import org.apache.hugegraph.util.JsonUtil;
+import org.apache.hugegraph.util.Log;
+import org.slf4j.Logger;
import com.codahale.metrics.annotation.Timed;
import com.taobao.arthas.agent.attach.ArthasAgent;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Singleton;
+import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.SecurityContext;
@Path("arthas")
@Singleton
@Tag(name = "ArthasAPI")
public class ArthasAPI extends API {
+ private static final Logger LOG = Log.logger(ArthasAPI.class);
+
@Context
private jakarta.inject.Provider<HugeConfig> configProvider;
@PUT
@Timed
@Produces(APPLICATION_JSON_WITH_CHARSET)
+ @RolesAllowed("admin")
@Operation(summary = "start arthas agent")
- public Object startArthas() {
+ public Object startArthas(@Context GraphManager manager,
+ @Context SecurityContext context) {
+ checkAccess(manager, context);
HugeConfig config = this.configProvider.get();
- HashMap<String, String> configMap = new HashMap<>(4);
- configMap.put("arthas.telnetPort", config.get(ServerOptions.ARTHAS_TELNET_PORT));
- configMap.put("arthas.httpPort", config.get(ServerOptions.ARTHAS_HTTP_PORT));
- configMap.put("arthas.ip", config.get(ServerOptions.ARTHAS_IP));
- configMap.put("arthas.disabledCommands",
- config.get(ServerOptions.ARTHAS_DISABLED_COMMANDS));
+ HashMap<String, String> configMap = arthasConfig(config);
String response = JsonUtil.toJson(configMap);
// Keep the optional MCP endpoint disabled without extending the activation response.
configMap.put("arthas.mcpEndpoint", "");
+ // Do not let external Arthas properties override the configured listener.
+ configMap.put("arthas.config.overrideAll", "false");
+ configMap.put("arthas.tunnelServer", "");
ArthasAgent.attach(configMap);
return response;
}
+
+ static void checkAccess(GraphManager manager, SecurityContext context) {
+ // No-auth requests receive an anonymous admin role, so a role check alone is insufficient.
+ if (!manager.requireAuthentication()) {
+ throw new ForbiddenException("HTTP Arthas activation requires authentication; " +
+ "use bin/arthas.sh on the server host instead");
+ }
+ if (context == null || context.getUserPrincipal() == null ||
+ HugeAuthenticator.USER_ANONY.equals(context.getUserPrincipal().getName()) ||
+ !context.isUserInRole("admin")) {
+ throw new ForbiddenException("Arthas activation requires an authenticated administrator");
+ }
+ }
+
+ static HashMap<String, String> arthasConfig(HugeConfig config) {
+ HashMap<String, String> configMap = new HashMap<>(4);
+ configMap.put("arthas.telnetPort",
+ option(config, ServerOptions.ARTHAS_TELNET_PORT,
+ ServerOptions.ARTHAS_TELNET_PORT_LEGACY));
+ configMap.put("arthas.httpPort",
+ option(config, ServerOptions.ARTHAS_HTTP_PORT,
+ ServerOptions.ARTHAS_HTTP_PORT_LEGACY));
+ String ip = config.get(ServerOptions.ARTHAS_IP);
+ ip = "localhost".equalsIgnoreCase(ip) ? "127.0.0.1" : ip;
+ try {
+ InetAddress.getByName(ip);
+ } catch (UnknownHostException e) {
+ throw new IllegalArgumentException("Invalid or unresolvable Arthas bind address '" + ip + "'", e);
+ }
+ configMap.put("arthas.ip", ip);
+ configMap.put("arthas.disabledCommands",
+ option(config, ServerOptions.ARTHAS_DISABLED_COMMANDS,
+ ServerOptions.ARTHAS_DISABLED_COMMANDS_LEGACY));
+ return configMap;
+ }
+
+ private static String option(HugeConfig config, ConfigOption<String> canonical,
+ ConfigOption<String> legacy) {
+ if (!config.containsKey(canonical.name())) {
+ return config.get(legacy);
+ }
+ String value = config.get(canonical);
+ if (config.containsKey(legacy.name()) && !value.equals(config.get(legacy))) {
+ LOG.warn("Conflicting Arthas options '{}' and '{}': '{}' takes precedence",
+ canonical.name(), legacy.name(), canonical.name());
+ }
+ return value;
+ }
}
diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/config/ServerOptions.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/config/ServerOptions.java
index 285c4b23a..3441291e4 100644
--- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/config/ServerOptions.java
+++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/config/ServerOptions.java
@@ -433,34 +433,58 @@ public class ServerOptions extends OptionHolder {
public static final ConfigOption<String> ARTHAS_TELNET_PORT =
new ConfigOption<>(
- "arthas.telnetPort",
+ "arthas.telnet_port",
"arthas provides telnet ports to the outside",
disallowEmpty(),
"8562"
);
+ public static final ConfigOption<String> ARTHAS_TELNET_PORT_LEGACY =
+ new ConfigOption<>(
+ "arthas.telnetPort",
+ "Legacy alias: arthas provides telnet ports to the outside",
+ disallowEmpty(),
+ "8562"
+ );
+
public static final ConfigOption<String> ARTHAS_HTTP_PORT =
new ConfigOption<>(
- "arthas.httpPort",
+ "arthas.http_port",
"arthas provides http ports to the outside",
disallowEmpty(),
"8561"
);
+ public static final ConfigOption<String> ARTHAS_HTTP_PORT_LEGACY =
+ new ConfigOption<>(
+ "arthas.httpPort",
+ "Legacy alias: arthas provides http ports to the outside",
+ disallowEmpty(),
+ "8561"
+ );
+
public static final ConfigOption<String> ARTHAS_IP =
new ConfigOption<>(
"arthas.ip",
- "arthas bound ip",
+ "Arthas bind address; use loopback unless external access is intended",
disallowEmpty(),
- "0.0.0.0"
+ "127.0.0.1"
);
public static final ConfigOption<String> ARTHAS_DISABLED_COMMANDS =
+ new ConfigOption<>(
+ "arthas.disabled_commands",
+ "Optional comma-separated Arthas disabled commands, empty by default",
+ null,
+ ""
+ );
+
+ public static final ConfigOption<String> ARTHAS_DISABLED_COMMANDS_LEGACY =
new ConfigOption<>(
"arthas.disabledCommands",
- "arthas disabled commands",
- disallowEmpty(),
- "jad"
+ "Legacy alias: optional comma-separated Arthas disabled commands",
+ null,
+ ""
);
public static final ConfigOption<Boolean> ALLOW_TRACE =
diff --git a/hugegraph-server/hugegraph-dist/pom.xml b/hugegraph-server/hugegraph-dist/pom.xml
index c4575bc87..810bd281b 100644
--- a/hugegraph-server/hugegraph-dist/pom.xml
+++ b/hugegraph-server/hugegraph-dist/pom.xml
@@ -183,6 +183,33 @@
<artifactId>maven-antrun-plugin</artifactId>
<version>1.8</version>
<executions>
+ <execution>
+ <id>prepare-arthas</id>
+ <phase>prepare-package</phase>
+ <goals>
+ <goal>run</goal>
+ </goals>
+ <configuration>
+ <target>
+ <unzip src="${com.taobao.arthas:arthas-packaging:jar}"
+ dest="${project.build.directory}/arthas-packaging">
+ <patternset>
+ <include name="arthas-bin.zip"/>
+ </patternset>
+ </unzip>
+ <delete dir="${project.build.directory}/arthas"/>
+ <unzip src="${project.build.directory}/arthas-packaging/arthas-bin.zip"
+ dest="${project.build.directory}/arthas">
+ <patternset>
+ <include name="arthas-*.jar"/>
+ <include name="logback.xml"/>
+ <include name="async-profiler/**"/>
+ <include name="lib/**"/>
+ </patternset>
+ </unzip>
+ </target>
+ </configuration>
+ </execution>
<execution>
<id>download-swagger-ui</id>
<phase>prepare-package</phase>
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/descriptor/assembly.xml b/hugegraph-server/hugegraph-dist/src/assembly/descriptor/assembly.xml
index b7f9ee048..bf5db512f 100644
--- a/hugegraph-server/hugegraph-dist/src/assembly/descriptor/assembly.xml
+++ b/hugegraph-server/hugegraph-dist/src/assembly/descriptor/assembly.xml
@@ -23,6 +23,10 @@
</formats>
<fileSets>
+ <fileSet>
+ <directory>${project.build.directory}/arthas</directory>
+ <outputDirectory>lib/arthas</outputDirectory>
+ </fileSet>
<fileSet>
<directory>${assembly.static.dir}/bin</directory>
<outputDirectory>bin</outputDirectory>
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/ArthasAttach.java b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/ArthasAttach.java
new file mode 100644
index 000000000..521d3536f
--- /dev/null
+++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/ArthasAttach.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Properties;
+import java.util.regex.Pattern;
+
+import com.sun.tools.attach.VirtualMachine;
+import com.taobao.arthas.core.config.FeatureCodec;
+
+class ArthasAttach {
+ public static void main(String[] args) throws Exception {
+ String ip = args[5];
+ InetAddress address = InetAddress.getByName(ip);
+ // Prefer native IPv4 loopback access for wildcard listeners, including dual-stack ::.
+ String connectIp = address.isAnyLocalAddress() ? "127.0.0.1" : ip;
+ Files.writeString(Path.of(args[4]).resolveSibling("connect-ip"), connectIp);
+ Path home = Path.of(args[1]);
+ Path top = Path.of(args[3]);
+ VirtualMachine vm = VirtualMachine.attach(args[0]);
+ Path configFile = null;
+ try {
+ Properties target = vm.getSystemProperties();
+ String command = target.getProperty("sun.java.command", "");
+ String main = "org.apache.hugegraph.bootstrap.HugeGraphServerBootstrap";
+ boolean server = Files.isRegularFile(top.resolve("bin/start-hugegraph.sh")) &&
+ (command.equals(main) || command.startsWith(main + " "));
+ boolean store = Files.isRegularFile(top.resolve("bin/start-hugegraph-store.sh")) &&
+ (command.matches(Pattern.quote(top.resolve("lib/hg-store-node-").toString()) +
+ "[^/]+[.]jar(?: .*|$)") ||
+ (command.matches("org[.]springframework[.]boot[.]loader[.]JarLauncher(?: .*|$)") &&
+ "HugeGraphStore".equals(target.getProperty("name"))));
+ if (!server && !store) {
+ throw new IllegalArgumentException("PID does not identify a Server or Store in this distribution");
+ }
+ // Allow exit cleanup only after this distribution's target has been validated.
+ Files.createFile(Path.of(args[4]));
+ // FeatureCodec drops blank values; a properties file preserves them.
+ configFile = Files.createTempFile("hugegraph-arthas-", ".properties");
+ Properties settings = new Properties();
+ settings.setProperty("arthas.config.overrideAll", "true");
+ settings.setProperty("arthas.ip", ip);
+ settings.setProperty("arthas.telnetPort", args[2]);
+ settings.setProperty("arthas.httpPort", "-1");
+ settings.setProperty("arthas.mcpEndpoint", "");
+ settings.setProperty("arthas.tunnelServer", "");
+ settings.setProperty("arthas.disabledCommands", "");
+ settings.setProperty("arthas.sessionTimeout", "1800");
+ if (!args[6].isEmpty() || !args[7].isEmpty()) {
+ settings.setProperty("arthas.username", args[6].isEmpty() ? "arthas" : args[6]);
+ }
+ if (!args[7].isEmpty()) {
+ settings.setProperty("arthas.password", args[7]);
+ }
+ settings.setProperty("arthas.localConnectionNonAuth",
+ target.getProperty("arthas.localConnectionNonAuth", "true"));
+ try (OutputStream output = Files.newOutputStream(configFile)) {
+ settings.store(output, null);
+ }
+ Map<String, String> config = new LinkedHashMap<>();
+ config.put("config.overrideAll", "true");
+ config.put("config.location", configFile.toString());
+ config.put("home", home.toString());
+ String agentArgs = home.resolve("arthas-core.jar") + ";" +
+ FeatureCodec.DEFAULT_COMMANDLINE_CODEC.toString(config);
+ vm.loadAgent(home.resolve("arthas-agent.jar").toString(),
+ URLEncoder.encode(agentArgs, StandardCharsets.UTF_8));
+ } finally {
+ vm.detach();
+ if (configFile != null) {
+ Files.deleteIfExists(configFile);
+ }
+ }
+ }
+}
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/arthas.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/arthas.sh
new file mode 100755
index 000000000..3b20dcdc6
--- /dev/null
+++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/arthas.sh
@@ -0,0 +1,232 @@
+#!/bin/bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Offline attachment to a running Server or Store using the bundled agent.
+set -euo pipefail
+
+fail() { echo "Error: $*" >&2; exit 1; }
+usage() {
+ echo "Usage: $0 [--pid PID] [--telnet-port PORT] [--target-ip ADDRESS] [--command COMMAND]"
+ echo " [--username USER] [--password PASSWORD]"
+ echo "Run as the service OS user; defaults to bin/pid and loopback port 3658."
+ echo "New agents disable HTTP/MCP; use the existing telnet port for an active agent."
+ echo "Exiting this helper stops Arthas, including after --command or quit; the JVM keeps running."
+}
+
+BIN=$(cd -P "$(dirname "${BASH_SOURCE[0]}")" && pwd)
+TOP=$(cd -P "$BIN/.." && pwd)
+pid=""
+port=3658
+target_ip=127.0.0.1
+username=""
+password=""
+command=""
+boot_args=()
+while [ "$#" -gt 0 ]; do
+ case "$1" in
+ --pid|--telnet-port|--target-ip|--command|--username|--password)
+ [ "$#" -ge 2 ] || fail "Missing value for $1"
+ case "$1" in
+ --pid) pid=$2 ;;
+ --telnet-port) port=$2 ;;
+ --target-ip) target_ip=$2 ;;
+ --command) command=$2 ;;
+ --username) username=$2 ;;
+ --password) password=$2 ;;
+ esac
+ shift 2 ;;
+ -h|--help) usage; exit 0 ;;
+ *) usage >&2; fail "Unknown option: $1" ;;
+ esac
+done
+# Native batch files contain one command per line.
+for credential in "$username" "$password"; do
+ [[ "$credential" != *$'\n'* && "$credential" != *$'\r'* ]] || fail "Credentials must be single-line values"
+done
+if [ -z "$pid" ]; then
+ [ -r "$BIN/pid" ] || fail "No bin/pid; start the service or supply --pid PID"
+ pid=$(cat "$BIN/pid")
+fi
+[[ "$pid" =~ ^[1-9][0-9]{0,9}$ ]] || fail "PID must be a positive integer"
+[[ "$port" =~ ^[1-9][0-9]{0,4}$ ]] && [ "$port" -le 65535 ] || fail "Invalid telnet port"
+[ -t 0 ] || [ -n "$command" ] || fail "Interactive use needs a terminal; use --command COMMAND"
+case "$target_ip" in
+ [Ll][Oo][Cc][Aa][Ll][Hh][Oo][Ss][Tt]) target_ip=127.0.0.1 ;;
+esac
+JAVA=${JAVA_HOME:+$JAVA_HOME/bin/}java
+ARTHAS="$TOP/lib/arthas"
+[ -f "$ARTHAS/arthas-boot.jar" ] || fail "Bundled Arthas files are missing from lib/arthas"
+# Keep the listener within this diagnosis session. Verify its PID before stopping
+# it, since an occupied port must never stop a different application's agent.
+state_dir=$(mktemp -d)
+client_pid=""
+attaching=true
+connect_ip=$target_ip
+quote_argument() {
+ local value=${1//\'/\'\\\'\'}
+ printf "'%s'" "$value"
+}
+write_batch() {
+ local file=$1
+ local commands=$2
+ {
+ if [ -n "$password" ]; then
+ printf 'auth --username %s %s\n' "$(quote_argument "${username:-arthas}")" "$(quote_argument "$password")"
+ fi
+ # Match arthas-client --command's semicolon-separated command syntax.
+ printf '%s\n' "${commands//;/$'\n'}"
+ } > "$file"
+}
+redact_auth_echo() {
+ local line
+ while IFS= read -r line || [ -n "$line" ]; do
+ case "$line" in
+ *'$ auth --username '*) printf '%s$ auth [credentials omitted]\n' "${line%%'$ auth --username '*}" ;;
+ *) printf '%s\n' "$line" ;;
+ esac
+ done
+}
+# The native client's -t option does not bound its final response wait.
+bounded_client() {
+ write_batch "$state_dir/cleanup.commands" "$1"
+ "$JAVA" -jar "$ARTHAS/arthas-client.jar" -t 5000 -f "$state_dir/cleanup.commands" "$connect_ip" "$port" &
+ local command_pid=$!
+ (sleep 10; kill -KILL "$command_pid" 2>/dev/null) >/dev/null 2>&1 &
+ local timer_pid=$!
+ local status=0
+ wait "$command_pid" || status=$?
+ kill -KILL "$timer_pid" 2>/dev/null || true
+ wait "$timer_pid" 2>/dev/null || true
+ return "$status"
+}
+format_endpoint() {
+ case "$connect_ip" in
+ *:*) printf '[%s]:%s' "$connect_ip" "$port" ;;
+ *) printf '%s:%s' "$connect_ip" "$port" ;;
+ esac
+}
+stop_agent() {
+ local session attempt agent_pid="" line endpoint rows=0
+ connect_ip=$(cat "$state_dir/connect-ip")
+ endpoint=$(format_endpoint)
+ if ! session=$(bounded_client session 2>&1); then
+ if [[ "$session" == *"Connection refused"* ]]; then
+ echo "Arthas listener closed at $endpoint" >&2
+ return 0
+ fi
+ if [[ "$session" == *"not permitted"* ]]; then
+ echo "Error: Arthas credentials rejected at $endpoint; agent left running" >&2
+ else
+ echo "Error: could not verify Arthas cleanup at $endpoint; agent left running" >&2
+ fi
+ return 1
+ fi
+ # AuthCommand echoes the username. Trust only one complete session table row.
+ while IFS= read -r line; do
+ if [[ "$line" =~ ^[[:space:]]*JAVA_PID[[:space:]]+([0-9]+)[[:space:]]*$ ]]; then
+ agent_pid=${BASH_REMATCH[1]}
+ rows=$((rows + 1))
+ fi
+ done <<< "$session"
+ if [ "$rows" -ne 1 ]; then
+ if [ "$rows" -eq 0 ] && [[ "$session" == *"not permitted"* ]]; then
+ echo "Error: Arthas credentials rejected at $endpoint; agent left running" >&2
+ else
+ echo "Error: could not verify an unambiguous Arthas PID at $endpoint; agent left running" >&2
+ fi
+ return 1
+ fi
+ if [ "$agent_pid" != "$pid" ]; then
+ echo "Error: Arthas at $endpoint belongs to PID $agent_pid, expected $pid; agent left running" >&2
+ return 1
+ fi
+ bounded_client stop >/dev/null 2>&1 || true
+ # Command errors can return zero; verify closure after asynchronous shutdown.
+ for attempt in 1 2 3; do
+ if ! session=$(bounded_client session 2>&1) && [[ "$session" == *"Connection refused"* ]]; then
+ echo "Arthas listener closed at $endpoint" >&2
+ return 0
+ fi
+ sleep 0.2
+ done
+ echo "Error: could not stop Arthas at $endpoint; check session/stop settings before retrying" >&2
+ return 1
+}
+cleanup() {
+ local status=$?
+ trap '' INT TERM HUP
+ trap - EXIT
+ if [ -n "$client_pid" ]; then
+ # Let an in-flight loadAgent finish before checking/closing its listener.
+ if [ "$attaching" = false ]; then
+ kill "$client_pid" 2>/dev/null || true
+ fi
+ # A queued second signal can interrupt wait even after traps are ignored.
+ while kill -0 "$client_pid" 2>/dev/null; do
+ wait "$client_pid" 2>/dev/null || true
+ done
+ fi
+ if [ -f "$state_dir/validated" ]; then
+ stop_agent || { [ "$status" -ne 0 ] || status=1; }
+ fi
+ rm -f "$state_dir/validated" "$state_dir/connect-ip" "$state_dir/cleanup.commands" "$state_dir/run.commands"
+ rmdir "$state_dir"
+ exit "$status"
+}
+trap cleanup EXIT
+trap 'trap "" INT TERM HUP; exit 130' INT
+trap 'trap "" INT TERM HUP; exit 143' TERM
+trap 'trap "" INT TERM HUP; exit 129' HUP
+echo "Waiting for PID $pid to answer the attach request..." >&2
+(
+ # Group signals must not end the client before target-side loadAgent finishes.
+ trap '' INT TERM HUP
+ exec "$JAVA" --add-modules jdk.attach --class-path "$ARTHAS/arthas-core.jar" \
+ "$BIN/ArthasAttach.java" "$pid" "$ARTHAS" "$port" "$TOP" "$state_dir/validated" "$target_ip" \
+ "$username" "$password"
+) <&0 &
+client_pid=$!
+wait "$client_pid"
+client_pid=""
+attaching=false
+connect_ip=$(cat "$state_dir/connect-ip")
+if ! session=$(bounded_client session 2>&1) && [[ "$session" == *"Connection refused"* ]]; then
+ echo "Error: no Arthas listener at $(format_endpoint);" \
+ "an existing agent may use another port (API default 8562); retry with --telnet-port" >&2
+ rm -f "$state_dir/validated"
+ exit 1
+fi
+if [ -n "$command" ]; then
+ if [ -n "$password" ]; then
+ write_batch "$state_dir/run.commands" "$command"
+ boot_args+=(--batch-file "$state_dir/run.commands")
+ else
+ boot_args+=(--command "$command")
+ fi
+fi
+[ -z "$username" ] || boot_args+=(--username "$username")
+[ -z "$password" ] || boot_args+=(--password "$password")
+boot_args+=(--arthas-home "$ARTHAS" --target-ip "$connect_ip" --telnet-port "$port" --http-port -1 "$pid")
+if [ -n "$password" ] && [ -n "$command" ]; then
+ # The native batch client echoes commands, including its auth command.
+ "$JAVA" -jar "$ARTHAS/arthas-boot.jar" "${boot_args[@]}" <&0 > >(redact_auth_echo) &
+else
+ "$JAVA" -jar "$ARTHAS/arthas-boot.jar" "${boot_args[@]}" <&0 &
+fi
+client_pid=$!
+wait "$client_pid"
+client_pid=""
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/gremlin-console.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/gremlin-console.sh
index 40ec2ff7d..eeee6b07c 100755
--- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/gremlin-console.sh
+++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/gremlin-console.sh
@@ -52,6 +52,7 @@ CP="$CP":$(find -L $LIB -name 'slf4j-log4j12*.jar' | sort | tr '\n' ':')
CP="$CP":$(find -L $LIB -name 'hugegraph*.jar' | sort | tr '\n' ':')
# Add the remaining jars in $BIN/../lib.
CP="$CP":$(find -L $LIB -name '*.jar' \
+ \! -path "$LIB/arthas/*" \
\! -name 'hugegraph*' \
\! -name 'slf4j-log4j12*.jar' | sort | tr '\n' ':')
# Add the jars in $BIN/../ext (at any subdirectory depth)
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh
index cdea1f8ce..3e0276ec0 100644
--- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh
+++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh
@@ -87,6 +87,7 @@ CP=$(find -L $LIB -name 'log4j-slf4j-impl*.jar' | sort | tr '\n' ':')
CP="$CP":$(find -L $LIB -name 'hugegraph*.jar' | sort | tr '\n' ':')
# Add the remaining jars in lib.
CP="$CP":$(find -L $LIB -name '*.jar' \
+ \! -path "$LIB/arthas/*" \
\! -name 'hugegraph*' \
\! -name 'log4j-slf4j-impl*.jar' | sort | tr '\n' ':')
# Add the jars in ext (at any subdirectory depth)
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/init-store.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/init-store.sh
index b1eb3438f..754a74cd6 100755
--- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/init-store.sh
+++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/init-store.sh
@@ -54,7 +54,8 @@ echo "Initializing HugeGraph Store..."
# Build classpath with hugegraph*.jar first to avoid class loading conflicts
CP=$(find -L "${LIB}" -name 'hugegraph*.jar' | sort | tr '\n' ':')
-CP="$CP":$(find -L "${LIB}" -name '*.jar' \! -name 'hugegraph*' | sort | tr '\n' ':')
+CP="$CP":$(find -L "${LIB}" -name '*.jar' \
+ \! -path "${LIB}/arthas/*" \! -name 'hugegraph*' | sort | tr '\n' ':')
CP="$CP":$(find -L "${PLUGINS}" -name '*.jar' | sort | tr '\n' ':')
source "$BIN/preload-topling.sh" || exit 1
CP="${TOPLING_RUNTIME_CLASSPATH:+$TOPLING_RUNTIME_CLASSPATH:}$CP"
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/conf/rest-server.properties b/hugegraph-server/hugegraph-dist/src/assembly/static/conf/rest-server.properties
index daeb092b7..be7b82a3b 100644
--- a/hugegraph-server/hugegraph-dist/src/assembly/static/conf/rest-server.properties
+++ b/hugegraph-server/hugegraph-dist/src/assembly/static/conf/rest-server.properties
@@ -12,10 +12,20 @@ batch.max_write_ratio=80
batch.max_write_threads=0
# configuration of arthas
+# Legacy aliases: arthas.telnetPort, arthas.httpPort, arthas.disabledCommands.
+# The snake_case key takes precedence when both forms are configured.
+# Different values for both forms produce a warning when the agent is activated.
arthas.telnet_port=8562
arthas.http_port=8561
+# Defaults to local access. External binding (e.g. 0.0.0.0) exposes diagnostic commands;
+# enable Arthas native authentication and restrict network access before exposing it.
+# For API activation, set -Darthas.username=... -Darthas.password=... on the Server JVM.
+# These credentials also apply to bin/arthas.sh; pass matching --username/--password for authenticated cleanup.
+# Native loopback auth bypass is controlled by -Darthas.localConnectionNonAuth=false.
+# Restart to apply JVM options; passwords in JVM arguments may appear in process listings.
arthas.ip=127.0.0.1
-arthas.disabled_commands=jad
+# Optional comma-separated disabled commands, for example jad,mc; empty allows all commands.
+arthas.disabled_commands=
# authentication configs
#auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-arthas-cli.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-arthas-cli.sh
new file mode 100644
index 000000000..b4428ebff
--- /dev/null
+++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-arthas-cli.sh
@@ -0,0 +1,404 @@
+#!/bin/bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Usage: bash test-arthas-cli.sh <built-server-or-store-distribution>
+# Real native attachment; all processes and files below belong to this test.
+set -euo pipefail
+[ "$#" -eq 1 ] || { echo "Supply a built Server or Store distribution" >&2; exit 1; }
+python3 - "$1" <<'PY'
+import os
+import pathlib
+import pty
+import re
+import select
+import shlex
+import shutil
+import signal
+import socket
+import subprocess
+import sys
+import tempfile
+import time
+import zipfile
+
+built = pathlib.Path(sys.argv[1]).resolve()
+store = (built / "bin/start-hugegraph-store.sh").is_file()
+component = "Store" if store else "Server"
+startup = "start-hugegraph-store.sh" if store else "start-hugegraph.sh"
+other_startup = "start-hugegraph.sh" if store else "start-hugegraph-store.sh"
+assert (built / "bin" / startup).is_file(), f"Not a Server or Store distribution: {built}"
+java_home = os.environ.get("JAVA_HOME")
+java = str(pathlib.Path(java_home) / "bin/java") if java_home else "java"
+javac = str(pathlib.Path(java_home) / "bin/javac") if java_home else "javac"
+with tempfile.TemporaryDirectory(prefix="arthas-test-") as work:
+ root = (pathlib.Path(work) / "native fixture").resolve()
+ for name in ("bin", "lib", "classes", "client-temp"):
+ (root / name).mkdir(parents=True)
+ for name in ("arthas.sh", "ArthasAttach.java"):
+ shutil.copy(built / "bin" / name, root / "bin" / name)
+ (root / "lib/arthas").symlink_to(built / "lib/arthas", target_is_directory=True)
+ (root / "bin" / startup).touch()
+ main = ("org.apache.hugegraph.store.node.StoreNodeApplication" if store else
+ "org.apache.hugegraph.bootstrap.HugeGraphServerBootstrap")
+ package, name = main.rsplit(".", 1)
+ source = root / f"{name}.java"
+ source.write_text(f"package {package}; public class {name} {{ "
+ "public static void main(String[] args) throws Exception { "
+ "Thread.sleep(180000); }}")
+ subprocess.run([javac, "-d", str(root / "classes"), str(source)], check=True)
+ if store:
+ # Store starts from an executable jar; exercise that PID validation path
+ # using the built distribution's real bundled agent and attach helper.
+ fixture_jar = root / "lib/hg-store-node-fixture.jar"
+ with zipfile.ZipFile(fixture_jar, "w") as jar:
+ jar.writestr("META-INF/MANIFEST.MF", f"Manifest-Version: 1.0\nMain-Class: {main}\n\n")
+ for compiled in (root / "classes").rglob("*.class"):
+ jar.write(compiled, compiled.relative_to(root / "classes"))
+ launch = ["-jar", str(fixture_jar)]
+ else:
+ launch = ["-cp", str(root / "classes"), main]
+ reservations = [socket.socket() for _ in range(3)]
+ for reservation in reservations:
+ reservation.bind(("127.0.0.1", 0))
+ safe_port, unsafe_telnet, unsafe_http = [s.getsockname()[1] for s in reservations]
+ for reservation in reservations:
+ reservation.close()
+ tunnel = socket.socket()
+ tunnel.bind(("127.0.0.1", 0))
+ tunnel.listen()
+ tunnel.settimeout(0.5)
+ tunnel_url = f"ws://127.0.0.1:{tunnel.getsockname()[1]}/ws"
+ unsafe = root / "unsafe.properties"
+ unsafe.write_text(f"arthas.config.overrideAll=true\narthas.ip=0.0.0.0\n"
+ f"arthas.telnetPort={unsafe_telnet}\narthas.httpPort={unsafe_http}\n"
+ f"arthas.mcpEndpoint=/mcp\narthas.tunnelServer={tunnel_url}\n"
+ "arthas.disabledCommands=stop,session\n")
+ target_env = dict(os.environ, ARTHAS_CONFIG_LOCATION=str(unsafe),
+ ARTHAS_CONFIG_OVERRIDE_ALL="true", ARTHAS_CONFIG_OVERRIDEALL="true", ARTHAS_IP="0.0.0.0",
+ ARTHAS_TUNNELSERVER=tunnel_url, ARTHAS_TUNNEL_SERVER=tunnel_url,
+ ARTHAS_DISABLEDCOMMANDS="stop,session")
+ target_env["arthas.config.location"] = str(unsafe)
+ target_env["arthas.tunnelServer"] = tunnel_url
+ client_env = dict(os.environ, TMPDIR=str(root / "client-temp"), JAVA_TOOL_OPTIONS=f'-Djava.io.tmpdir="{root / "client-temp"}"')
+ script = ["bash", str(root / "bin/arthas.sh")]
+ client = script + ["--telnet-port", str(safe_port)]
+
+ def run(arguments, success=True):
+ result = subprocess.run(arguments, env=client_env, text=True,
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT, input="", timeout=45)
+ assert (result.returncode == 0) == success, result.stdout
+ return result.stdout
+
+ def can_connect(host, port):
+ try:
+ with socket.create_connection((host, port), timeout=1):
+ return True
+ except OSError:
+ return False
+
+ def wait_port(opened, host="127.0.0.1"):
+ deadline = time.monotonic() + 15
+ while can_connect(host, safe_port) != opened:
+ assert time.monotonic() < deadline, f"Listener did not become {'open' if opened else 'closed'}"
+ time.sleep(0.1)
+
+ class Terminal:
+ # fork gives the helper a real controlling terminal: closing its master
+ # reproduces terminal hangup, including the kernel's process-group HUP.
+ def __init__(self, shell=False, options=()):
+ self.pid, self.master = pty.fork()
+ if self.pid == 0:
+ arguments = ["bash", "--noprofile", "--norc", "-i"] if shell else client + list(options)
+ os.execvpe(arguments[0], arguments, client_env)
+ self.returncode = None
+ self.output = ""
+ if shell:
+ os.write(self.master, (shlex.join(client) + "\n").encode())
+
+ def poll(self):
+ if self.returncode is None:
+ child, status = os.waitpid(self.pid, os.WNOHANG)
+ if child:
+ self.returncode = os.waitstatus_to_exitcode(status)
+ return self.returncode
+
+ def drain(self):
+ if self.master is not None and select.select([self.master], [], [], 0)[0]:
+ try:
+ output = os.read(self.master, 65536).decode(errors="replace")
+ self.output += output
+ log.write(output)
+ log.flush()
+ except OSError:
+ pass # The slave closes when the helper exits.
+
+ def ready(self):
+ deadline = time.monotonic() + 15
+ while "[arthas@" not in self.output:
+ self.drain()
+ assert self.poll() is None and time.monotonic() < deadline, self.output
+ time.sleep(0.02)
+
+ def wait(self, timeout=30):
+ deadline = time.monotonic() + timeout
+ while self.poll() is None:
+ self.drain()
+ assert time.monotonic() < deadline, "Interactive helper did not exit"
+ time.sleep(0.02)
+ self.drain()
+ if self.master is not None:
+ os.close(self.master)
+ self.master = None
+ return self.returncode
+
+ interactive = None
+ with (root / "jvm.log").open("w") as log:
+ process = subprocess.Popen([java, "-Darthas.config.location=" + str(unsafe),
+ "-Darthas.config.overrideAll=true", "-Darthas.ip=0.0.0.0",
+ "-Darthas.tunnelServer=" + tunnel_url,
+ "-Darthas.disabledCommands=stop,session",
+ *launch, "fixture"], env=target_env, stdout=log, stderr=subprocess.STDOUT)
+ try:
+ (root / "bin/pid").write_text(str(process.pid))
+ time.sleep(1)
+ for options in (["--pid", "-1"], ["--pid", "1; unsafe"], ["--pid", "9999999999"],
+ ["--telnet-port", "65536"], ["--target-ip", "::not-an-ip"],
+ ["--username", "line\nbreak"],
+ ["--http-port", "8563"], ["--tunnel-server", tunnel_url]):
+ output = run(script + options + ["--command", "version"], success=False)
+ if options[0] == "--username":
+ assert "single-line values" in output and "Waiting for PID" not in output, output
+ assert not can_connect("127.0.0.1", safe_port)
+ # EOF and piped input must fail before attaching; batch callers need --command.
+ for input_text in ("", "version\n"):
+ result = subprocess.run(client, env=client_env, input=input_text, text=True,
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=10)
+ assert result.returncode != 0 and "use --command" in result.stdout, result.stdout
+ assert not can_connect("127.0.0.1", safe_port)
+ # A live PID from a different component must not receive the agent.
+ (root / "bin" / startup).unlink()
+ (root / "bin" / other_startup).touch()
+ run(client + ["--command", "version"], success=False)
+ (root / "bin" / other_startup).unlink()
+ (root / "bin" / startup).touch()
+ output = run(client + ["--pid", str(process.pid), "--command", "version"])
+ assert re.search(r"\n4\.1\.5\s*\n", output), output
+ assert (f"Process {process.pid} already using port {safe_port}" in output or
+ f"The target process already listen port {safe_port}, skip attach." in output), output
+ wait_port(False)
+ assert process.poll() is None, "One-time diagnosis stopped the application JVM"
+ # Keep a real interactive client open while inspecting the live agent.
+ interactive = Terminal()
+ wait_port(True)
+ assert not can_connect("127.0.0.1", unsafe_telnet)
+ assert not can_connect("127.0.0.1", unsafe_http)
+ try:
+ connection, address = tunnel.accept()
+ except socket.timeout:
+ pass
+ else:
+ connection.close()
+ raise AssertionError(f"Arthas contacted the overridden tunnel endpoint: {address}")
+ # Inspect actual bind addresses; VPNs may redirect connection probes.
+ if sys.platform.startswith("linux"):
+ inodes = set()
+ for descriptor in pathlib.Path(f"/proc/{process.pid}/fd").iterdir():
+ try:
+ link = os.readlink(descriptor)
+ except FileNotFoundError:
+ continue
+ if link.startswith("socket:["):
+ inodes.add(link[8:-1])
+ addresses = []
+ for table in ("tcp", "tcp6"):
+ for line in pathlib.Path(f"/proc/{process.pid}/net/{table}").read_text().splitlines()[1:]:
+ fields = line.split()
+ address, port = fields[1].split(":")
+ if fields[3] == "0A" and fields[9] in inodes:
+ addresses.append((address, int(port, 16)))
+ assert len(addresses) == 1 and addresses[0][1] == safe_port, addresses
+ assert addresses[0][0] in ("0100007F", "0000000000000000FFFF00000100007F"), addresses
+ else:
+ listeners = subprocess.check_output(["lsof", "-nP", "-a", "-p", str(process.pid),
+ "-iTCP", "-sTCP:LISTEN"], text=True).splitlines()[1:]
+ assert len(listeners) == 1 and f"127.0.0.1:{safe_port} " in listeners[0], listeners
+ os.write(interactive.master, b"quit\n")
+ assert interactive.wait(45) == 0, (root / "jvm.log").read_text()
+ wait_port(False)
+ # Interactive Bash propagates the terminal HUP to its foreground job.
+ # Closing a direct helper PTY alone does not reproduce that signal sequence.
+ interactive = Terminal(shell=True)
+ wait_port(True)
+ interactive.ready()
+ os.close(interactive.master)
+ interactive.master = None
+ status = interactive.wait()
+ assert status in (-signal.SIGHUP, 128 + signal.SIGHUP), status
+ wait_port(False)
+ assert process.poll() is None, "Terminal hangup stopped the application JVM"
+ # Signals sent only to the helper must also clean up its child/client.
+ for termination in (signal.SIGTERM, signal.SIGHUP, signal.SIGINT):
+ interactive = Terminal()
+ if termination == signal.SIGTERM:
+ # Terminate during source compilation/attachment, before a listener exists.
+ time.sleep(0.2)
+ else:
+ wait_port(True)
+ interactive.ready()
+ os.kill(interactive.pid, termination)
+ if termination != signal.SIGTERM:
+ # Repeated/mixed signals during cleanup must not interrupt stop_agent.
+ for repeat in (signal.SIGHUP, signal.SIGINT, signal.SIGTERM) * 5:
+ time.sleep(0.02)
+ if interactive.poll() is None:
+ os.kill(interactive.pid, repeat)
+ assert interactive.wait() == 128 + termination, (root / "jvm.log").read_text()
+ wait_port(False)
+ assert process.poll() is None, "Terminal/signal cleanup stopped the application JVM"
+ # Linux mktemp honors TMPDIR, exposing only this fixture's marker.
+ # macOS mktemp ignores it; the other terminal/signal cases run there.
+ if sys.platform.startswith("linux"):
+ # Pause after validation so loadAgent cannot finish before the
+ # signal reaches both the helper and its attach JVM.
+ for termination in (signal.SIGTERM, signal.SIGHUP):
+ interactive = Terminal()
+ deadline = time.monotonic() + 15
+ while not list((root / "client-temp").glob("tmp.*/validated")):
+ interactive.drain()
+ assert interactive.poll() is None and time.monotonic() < deadline, interactive.output
+ time.sleep(0.001)
+ os.kill(process.pid, signal.SIGSTOP)
+ try:
+ assert not can_connect("127.0.0.1", safe_port), "Agent started before the attach pause"
+ # Queue the attach request before signaling the group.
+ time.sleep(0.1)
+ os.killpg(interactive.pid, termination)
+ if termination == signal.SIGHUP:
+ # Kernel and interactive Bash can deliver HUP almost together.
+ time.sleep(0.00006)
+ os.killpg(interactive.pid, termination)
+ time.sleep(1)
+ assert interactive.poll() is None, "Group signal interrupted in-flight attachment"
+ finally:
+ os.kill(process.pid, signal.SIGCONT)
+ assert interactive.wait() == 128 + termination, interactive.output
+ wait_port(False)
+ assert process.poll() is None, "Group signal stopped the application JVM"
+ # macOS requires an explicit interface alias for 127.0.0.2; Linux
+ # routes all of 127/8 locally and exercises that address in CI.
+ hosts = ("127.0.0.2", "::1", "localhost") if sys.platform.startswith("linux") else ("::1", "localhost")
+ for host in (*hosts, "0.0.0.0", "::"):
+ output = run(client + ["--target-ip", host, "--command", "version"])
+ assert re.search(r"\n4\.1\.5\s*\n", output), output
+ connect_host = {"localhost": "127.0.0.1", "0.0.0.0": "127.0.0.1", "::": "127.0.0.1"}.get(host, host)
+ endpoint = f"[{connect_host}]:{safe_port}" if ":" in connect_host else f"{connect_host}:{safe_port}"
+ assert output.count(f"Arthas listener closed at {endpoint}") == 1, output
+ wait_port(False, connect_host)
+ run(client + ["--command", "stop"])
+ wait_port(False)
+ # Disable native local authentication bypass for the following
+ # attachments; earlier signal cases exercise native no-auth defaults.
+ run(client + ["--command", "sysprop arthas.localConnectionNonAuth false"])
+ wait_port(False)
+ password = "p a;ss'\"\\word"
+ auth = ["--username", "operator name", "--password", password]
+ for options in (auth, ["--password", password]):
+ output = run(client + options + ["--command", "version"])
+ assert re.search(r"\n4\.1\.5\s*\n", output), output
+ assert password not in output and "auth --username" not in output, output
+ wait_port(False)
+ assert process.poll() is None, "Authenticated command stopped the application JVM"
+ interactive = Terminal(options=auth)
+ wait_port(True)
+ interactive.ready()
+ # An independent unauthenticated loopback client must be rejected.
+ unauthenticated = run([java, "-jar", str(root / "lib/arthas/arthas-client.jar"),
+ "-c", "version", "127.0.0.1", str(safe_port)])
+ assert "command not permitted" in unauthenticated, unauthenticated
+ assert not re.search(r"\n4\.1\.5\s*\n", unauthenticated), unauthenticated
+ # A running agent retains its credentials; invalid credentials must
+ # fail diagnosis/cleanup without stopping the existing listener.
+ wrong = run(client + ["--username", "operator name", "--password", "incorrect ; secret",
+ "--command", "version"], success=False)
+ assert "command not permitted" in wrong and "incorrect ; secret" not in wrong, wrong
+ assert "credentials rejected" in wrong and "agent left running" in wrong, wrong
+ wait_port(True)
+ quoted_password = "'" + password.replace("'", "'\\''") + "'"
+ os.write(interactive.master,
+ f'auth --username "operator name" {quoted_password}\nversion\nquit\n'.encode())
+ assert interactive.wait(45) == 0, interactive.output
+ assert re.search(r"[\r\n]4\.1\.5\s*[\r\n]", interactive.output), interactive.output
+ wait_port(False)
+ assert process.poll() is None, "Authenticated quit stopped the application JVM"
+ # Authentication echoes must not let one JVM impersonate another
+ # agent's session row. Both JVMs are valid targets in this fixture.
+ other = subprocess.Popen([java, "-Darthas.localConnectionNonAuth=false", *launch, "other fixture"],
+ env=target_env, stdout=log, stderr=subprocess.STDOUT)
+ try:
+ time.sleep(1)
+ impersonating = ["--username", f"prefix JAVA_PID {process.pid} suffix",
+ "--password", "mismatch ; secret"]
+ interactive = Terminal(options=["--pid", str(other.pid), *impersonating])
+ wait_port(True)
+ interactive.ready()
+ missing = run(script + ["--pid", str(other.pid), "--telnet-port", str(unsafe_http),
+ *impersonating, "--command", "version"], success=False)
+ assert f"no Arthas listener at 127.0.0.1:{unsafe_http}" in missing, missing
+ assert "API default 8562" in missing and "retry with --telnet-port" in missing, missing
+ assert "Arthas listener closed at" not in missing, missing
+ assert can_connect("127.0.0.1", safe_port), "Missing-port diagnosis stopped the existing agent"
+ mismatch = run(client + ["--pid", str(process.pid), *impersonating,
+ "--command", "version"], success=False)
+ assert f"belongs to PID {other.pid}, expected {process.pid}" in mismatch, mismatch
+ assert can_connect("127.0.0.1", safe_port), "PID mismatch stopped another JVM's agent"
+ assert process.poll() is None and other.poll() is None
+ # This helper owns the other JVM. The misleading username must
+ # also not prevent its legitimate exit cleanup.
+ os.kill(interactive.pid, signal.SIGTERM)
+ assert interactive.wait() == 128 + signal.SIGTERM, interactive.output
+ wait_port(False)
+ assert "Arthas listener closed at" in interactive.output, interactive.output
+ assert other.poll() is None, "Agent cleanup stopped the other application JVM"
+ finally:
+ if interactive is not None and interactive.poll() is None:
+ os.kill(interactive.pid, signal.SIGKILL)
+ interactive.wait(10)
+ other.terminate()
+ try:
+ other.wait(timeout=10)
+ except subprocess.TimeoutExpired:
+ other.kill()
+ other.wait()
+ assert not can_connect("127.0.0.1", safe_port)
+ assert not list((root / "client-temp").glob("hugegraph-arthas-*.properties"))
+ assert not list((root / "client-temp").glob("tmp.*")), "Helper state was not removed"
+ print(f"PASS: {component} PID/argument boundaries, hostile config, default loopback bind, no tunnel, "
+ "command/quit/terminal/group/repeated-signal cleanup, explicit binds/authentication without restart")
+ finally:
+ if interactive is not None and interactive.poll() is None:
+ os.kill(interactive.pid, signal.SIGKILL)
+ interactive.wait(10)
+ tunnel.close()
+ if process.poll() is None:
+ os.kill(process.pid, signal.SIGCONT)
+ process.terminate()
+ try:
+ process.wait(timeout=10)
+ except subprocess.TimeoutExpired:
+ process.kill()
+ process.wait()
+PY
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-check-jacoco-report.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-check-jacoco-report.sh
index bf7fd6453..dffc44324 100755
--- a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-check-jacoco-report.sh
+++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-check-jacoco-report.sh
@@ -488,6 +488,7 @@ assert required_modules(pd_job) == {
assert_order(store_job, [
"mvn clean package",
"mvn editorconfig:check -pl hugegraph-store/hg-store-test -am -ntp",
+ "-Dtest=StoredRowIngressTest,ArthasSecurityTest",
"-P store-common-test -Djacoco.sessionId=store-common-test",
"-P store-client-test -Djacoco.sessionId=store-client-test",
"-P store-rocksdb-test -Djacoco.sessionId=store-rocksdb-test",
@@ -517,6 +518,7 @@ assert reports_for_option(store_job, "--require-test-report") == {
"TEST-org.apache.hugegraph.store.core.CoreSuiteTest.xml",
"TEST-org.apache.hugegraph.store.service.ServerSuiteTest.xml",
"TEST-org.apache.hugegraph.store.business.StoredRowIngressTest.xml",
+ "TEST-org.apache.hugegraph.store.node.controller.ArthasSecurityTest.xml",
}
assert not reports_for_option(store_job, "--require-suite-report")
assert values_for_option(store_job, "--require-session") == (selected_profiles(store_job, "store") | {"store-node-test"})
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/ArthasApiTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/ArthasApiTest.java
index ab7e49e67..89e83b7bb 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/ArthasApiTest.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/ArthasApiTest.java
@@ -19,10 +19,10 @@ package org.apache.hugegraph.api;
import java.util.List;
import java.util.Map;
+import java.util.UUID;
import org.apache.hugegraph.util.JsonUtil;
import org.junit.Assert;
-import org.junit.Before;
import org.junit.Test;
import com.google.common.collect.ImmutableMap;
@@ -35,7 +35,7 @@ public class ArthasApiTest extends BaseApiTest {
private static final String ARTHAS_API_BASE_URL = "http://127.0.0.1:8561";
private static final String ARTHAS_API_PATH = "/api";
- @Before
+ @Test
public void testArthasStart() {
Response r = client().put(ARTHAS_START_PATH, "", "", ImmutableMap.of());
String result = assertResponseStatus(200, r);
@@ -44,10 +44,39 @@ public class ArthasApiTest extends BaseApiTest {
assertJsonContains(result, "arthas.httpPort");
assertJsonContains(result, "arthas.ip");
assertJsonContains(result, "arthas.disabledCommands");
+ Assert.assertEquals("127.0.0.1", assertJsonContains(result, "arthas.ip"));
+ }
+
+ @Test
+ public void testAnonymousCannotActivate() {
+ RestClient anonymous = new RestClient(BASE_URL, false);
+ try {
+ assertResponseStatus(401, anonymous.put(ARTHAS_START_PATH, "", "", ImmutableMap.of()));
+ } finally {
+ anonymous.close();
+ }
+ }
+
+ @Test
+ public void testNonAdminCannotActivate() {
+ String username = "arthas_user_" + UUID.randomUUID().toString().replace("-", "");
+ String usersPath = "graphspaces/DEFAULT/auth/users";
+ String userBody = JsonUtil.toJson(ImmutableMap.of("user_name", username,
+ "user_password", "Arthas1234"));
+ String created = assertResponseStatus(201, client().post(usersPath, userBody));
+ String userId = assertJsonContains(created, "id");
+ RestClient user = new RestClient(BASE_URL, username, "Arthas1234");
+ try {
+ assertResponseStatus(403, user.put(ARTHAS_START_PATH, "", "", ImmutableMap.of()));
+ } finally {
+ user.close();
+ assertResponseStatus(204, client().delete(usersPath, userId));
+ }
}
@Test
public void testArthasApi() {
+ testArthasStart();
RestClient arthasApiClient = new RestClient(ARTHAS_API_BASE_URL, false);
try {
// A one-time command must succeed through its automatic session cleanup.
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/arthas/ArthasConfigTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/arthas/ArthasConfigTest.java
new file mode 100644
index 000000000..685086e84
--- /dev/null
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/arthas/ArthasConfigTest.java
@@ -0,0 +1,244 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hugegraph.api.arthas;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.hugegraph.api.filter.AuthenticationFilter.Authorizer;
+import org.apache.hugegraph.auth.HugeAuthenticator.User;
+import org.apache.hugegraph.auth.HugePermission;
+import org.apache.hugegraph.auth.RolePermission;
+import org.apache.hugegraph.config.ConfigException;
+import org.apache.hugegraph.config.HugeConfig;
+import org.apache.hugegraph.config.OptionSpace;
+import org.apache.hugegraph.config.ServerOptions;
+import org.apache.hugegraph.core.GraphManager;
+import org.apache.hugegraph.testutil.Assert;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.Appender;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.Logger;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import jakarta.ws.rs.ForbiddenException;
+import jakarta.ws.rs.core.MultivaluedHashMap;
+import jakarta.ws.rs.core.SecurityContext;
+import jakarta.ws.rs.core.UriInfo;
+
+public class ArthasConfigTest {
+
+ private final List<LogEvent> events = new ArrayList<>();
+ private Logger logger;
+ private Level previousLevel;
+ private Appender appender;
+
+ @Before
+ public void captureWarnings() {
+ this.appender = Mockito.mock(Appender.class);
+ Mockito.when(this.appender.getName()).thenReturn("ArthasConfigTestAppender");
+ Mockito.when(this.appender.isStarted()).thenReturn(true);
+ Mockito.doAnswer(invocation -> {
+ LogEvent event = invocation.getArgument(0);
+ this.events.add(event.toImmutable());
+ return null;
+ }).when(this.appender).append(Mockito.any(LogEvent.class));
+ this.logger = (Logger) LogManager.getLogger(ArthasAPI.class);
+ this.previousLevel = this.logger.getLevel();
+ this.logger.setLevel(Level.WARN);
+ this.logger.addAppender(this.appender);
+ }
+
+ @After
+ public void restoreLogger() {
+ this.logger.removeAppender(this.appender);
+ this.logger.setLevel(this.previousLevel);
+ }
+
+ @BeforeClass
+ public static void registerOptions() {
+ OptionSpace.register("server", ServerOptions.instance());
+ }
+
+ @Test
+ public void testDefaults() {
+ Map<String, String> config = ArthasAPI.arthasConfig(new HugeConfig(Collections.emptyMap()));
+ assertOptions(config, "8562", "8561", "");
+ Assert.assertEquals("127.0.0.1", config.get("arthas.ip"));
+ Assert.assertTrue(this.events.isEmpty());
+ }
+
+ @Test
+ public void testDistributionKeys() {
+ Map<String, Object> properties = properties("telnet_port", "http_port", "disabled_commands");
+ properties.put("arthas.ip", "127.0.0.1");
+ Map<String, String> config = ArthasAPI.arthasConfig(new HugeConfig(properties));
+ assertOptions(config, "18562", "18561", "jad,stop");
+ Assert.assertEquals("127.0.0.1", config.get("arthas.ip"));
+ Assert.assertTrue(this.events.isEmpty());
+ }
+
+ @Test
+ public void testLegacyKeys() {
+ Map<String, Object> properties = properties("telnetPort", "httpPort", "disabledCommands");
+ assertOptions(ArthasAPI.arthasConfig(new HugeConfig(properties)), "18562", "18561", "jad,stop");
+ Assert.assertTrue(this.events.isEmpty());
+ }
+
+ @Test
+ public void testCanonicalKeysTakePrecedence() {
+ Map<String, Object> properties = properties("telnet_port", "http_port", "disabled_commands");
+ properties.put("arthas.telnetPort", "28562");
+ properties.put("arthas.httpPort", "28561");
+ properties.put("arthas.disabledCommands", "stop");
+ assertOptions(ArthasAPI.arthasConfig(new HugeConfig(properties)), "18562", "18561", "jad,stop");
+ Assert.assertEquals(3, this.events.size());
+ assertConflictWarning(0, "telnet_port", "telnetPort");
+ assertConflictWarning(1, "http_port", "httpPort");
+ assertConflictWarning(2, "disabled_commands", "disabledCommands");
+ }
+
+ @Test
+ public void testExplicitCanonicalDefaultsTakePrecedence() {
+ Map<String, Object> properties = properties("telnetPort", "httpPort", "disabledCommands");
+ properties.put("arthas.telnet_port", "8562");
+ properties.put("arthas.http_port", "8561");
+ properties.put("arthas.disabled_commands", "");
+ assertOptions(ArthasAPI.arthasConfig(new HugeConfig(properties)), "8562", "8561", "");
+ Assert.assertEquals(3, this.events.size());
+ assertConflictWarning(2, "disabled_commands", "disabledCommands");
+ }
+
+ @Test
+ public void testMatchingCanonicalAndLegacyKeysDoNotWarn() {
+ Map<String, Object> properties = properties("telnet_port", "http_port", "disabled_commands");
+ properties.putAll(properties("telnetPort", "httpPort", "disabledCommands"));
+ assertOptions(ArthasAPI.arthasConfig(new HugeConfig(properties)), "18562", "18561", "jad,stop");
+ Assert.assertTrue(this.events.isEmpty());
+ }
+
+ @Test
+ public void testCanonicalAndLegacyPortsAndIpRejectEmptyValues() {
+ for (String key : new String[]{"telnet_port", "telnetPort", "http_port", "httpPort", "ip"}) {
+ Assert.assertThrows(ConfigException.class,
+ () -> new HugeConfig(Collections.singletonMap("arthas." + key, "")));
+ }
+ }
+
+ @Test
+ public void testCanonicalAndLegacyDisabledCommandsCanBeEmpty() {
+ for (String key : new String[]{"disabled_commands", "disabledCommands"}) {
+ Map<String, String> config = ArthasAPI.arthasConfig(
+ new HugeConfig(Collections.singletonMap("arthas." + key, "")));
+ assertOptions(config, "8562", "8561", "");
+ }
+ }
+
+ @Test
+ public void testExplicitBindAddresses() {
+ for (String ip : new String[]{"127.0.0.1", "127.0.0.2", "::1", "localhost",
+ "0.0.0.0", "::", "192.0.2.1", "2001:db8::1", "fe80::1%1"}) {
+ Map<String, String> config = ArthasAPI.arthasConfig(
+ new HugeConfig(Collections.singletonMap("arthas.ip", ip)));
+ Assert.assertEquals("localhost".equals(ip) ? "127.0.0.1" : ip, config.get("arthas.ip"));
+ }
+ }
+
+ @Test
+ public void testInvalidBindAddressesAreRejected() {
+ for (String ip : new String[]{"2001:db8::xyz", "2001:db8:::1", "[::1", "0.0.0.0:3658"}) {
+ HugeConfig config = new HugeConfig(Collections.singletonMap("arthas.ip", ip));
+ Assert.assertThrows(IllegalArgumentException.class, () -> ArthasAPI.arthasConfig(config));
+ }
+ }
+
+ @Test
+ public void testNoAuthCannotActivateWithAnonymousAdminRole() {
+ GraphManager manager = Mockito.mock(GraphManager.class);
+ SecurityContext context = authorizer(manager, User.ANONYMOUS);
+ Assert.assertTrue(context.isUserInRole("admin"));
+ Throwable failure = Assert.assertThrows(ForbiddenException.class,
+ () -> new ArthasAPI().startArthas(manager, context));
+ Assert.assertTrue(failure.getMessage().contains("bin/arthas.sh"));
+ }
+
+ @Test
+ public void testAuthenticatedNonAdminCannotActivate() {
+ GraphManager manager = Mockito.mock(GraphManager.class);
+ Mockito.when(manager.requireAuthentication()).thenReturn(true);
+ User user = new User("graph_reader", RolePermission.role("DEFAULT", "hugegraph", HugePermission.READ));
+ SecurityContext context = authorizer(manager, user);
+ Assert.assertThrows(ForbiddenException.class,
+ () -> new ArthasAPI().startArthas(manager, context));
+ }
+
+ @Test
+ public void testAuthenticatedAdminIsAllowed() {
+ GraphManager manager = Mockito.mock(GraphManager.class);
+ Mockito.when(manager.requireAuthentication()).thenReturn(true);
+ ArthasAPI.checkAccess(manager, authorizer(manager, User.ADMIN));
+ }
+
+ @Test
+ public void testAnonymousOrMissingPrincipalIsRejectedEvenInAuthMode() {
+ GraphManager manager = Mockito.mock(GraphManager.class);
+ Mockito.when(manager.requireAuthentication()).thenReturn(true);
+ Assert.assertThrows(ForbiddenException.class,
+ () -> new ArthasAPI().startArthas(manager, authorizer(manager, User.ANONYMOUS)));
+ Assert.assertThrows(ForbiddenException.class,
+ () -> new ArthasAPI().startArthas(manager, Mockito.mock(SecurityContext.class)));
+ }
+
+ private static SecurityContext authorizer(GraphManager manager, User user) {
+ UriInfo uri = Mockito.mock(UriInfo.class);
+ Mockito.when(uri.getPathParameters()).thenReturn(new MultivaluedHashMap<>());
+ return new Authorizer(manager, user, uri);
+ }
+
+ private void assertConflictWarning(int index, String canonical, String legacy) {
+ LogEvent event = this.events.get(index);
+ Assert.assertEquals(Level.WARN, event.getLevel());
+ Assert.assertEquals(String.format("Conflicting Arthas options 'arthas.%s' and 'arthas.%s': " +
+ "'arthas.%s' takes precedence", canonical, legacy, canonical),
+ event.getMessage().getFormattedMessage());
+ }
+
+ private static Map<String, Object> properties(String telnet, String http, String commands) {
+ Map<String, Object> properties = new HashMap<>();
+ properties.put("arthas." + telnet, "18562");
+ properties.put("arthas." + http, "18561");
+ properties.put("arthas." + commands, "jad,stop");
+ return properties;
+ }
+
+ private static void assertOptions(Map<String, String> config, String telnet, String http,
+ String commands) {
+ Assert.assertEquals(4, config.size());
+ Assert.assertEquals(telnet, config.get("arthas.telnetPort"));
+ Assert.assertEquals(http, config.get("arthas.httpPort"));
+ Assert.assertEquals(commands, config.get("arthas.disabledCommands"));
+ }
+}
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java
index 721937223..ee1c268dd 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java
@@ -22,6 +22,7 @@ import org.apache.hugegraph.unit.core.LoadingModeTest;
import org.apache.hugegraph.GraphDropPendingTest;
import org.apache.hugegraph.StandardTaskAdmissionTest;
import org.apache.hugegraph.DistributedTaskAdmissionTest;
+import org.apache.hugegraph.api.arthas.ArthasConfigTest;
import org.apache.hugegraph.api.auth.GraphSpaceAuthPayloadTest;
import org.apache.hugegraph.api.auth.GraphSpaceGroupAPITest;
import org.apache.hugegraph.api.cypher.CypherClientTest;
@@ -151,6 +152,7 @@ import org.junit.runners.Suite;
LoadDetectFilterTest.class,
LoginAPITest.class,
PathFilterTest.class,
+ ArthasConfigTest.class,
VertexAuthorizationTest.class,
/* api cypher */
diff --git a/hugegraph-server/pom.xml b/hugegraph-server/pom.xml
index 5e6da3324..7fff054fe 100644
--- a/hugegraph-server/pom.xml
+++ b/hugegraph-server/pom.xml
@@ -53,7 +53,6 @@
<checkstyle.version>8.45</checkstyle.version>
<protobuf.version>3.21.7</protobuf.version>
<jmh.version>1.36</jmh.version>
- <arthas.version>4.1.5</arthas.version>
<swagger.version>2.2.18</swagger.version>
</properties>
diff --git a/hugegraph-store/Dockerfile b/hugegraph-store/Dockerfile
index 474fe5984..9e6fbdf18 100644
--- a/hugegraph-store/Dockerfile
+++ b/hugegraph-store/Dockerfile
@@ -35,7 +35,8 @@ RUN --mount=type=cache,id=hugegraph-maven-${SOURCE_REVISION},target=/root/.m2,sh
# 2nd stage: runtime env
# Note: ZGC (The Z Garbage Collector) is only supported on ARM-Mac with java > 13
-FROM eclipse-temurin:17-jre-jammy
+FROM eclipse-temurin:17-jdk-jammy
+# Local Arthas attach requires the JDK attach and compiler modules.
LABEL maintainer="HugeGraph Docker Maintainers <de...@hugegraph.apache.org>"
diff --git a/hugegraph-store/README.md b/hugegraph-store/README.md
index e74239134..3615649ff 100644
--- a/hugegraph-store/README.md
+++ b/hugegraph-store/README.md
@@ -415,6 +415,9 @@ timeout handling, diagnostics and resource drain behavior.
## Documentation
+For offline diagnosis of a running node without restarting it, see
+[Arthas diagnostics](../docs/server-runtime.md#arthas-diagnostics).
+
See the [storage lifecycle guide](../docs/storage-lifecycle.md) for request ownership,
shutdown, recovery and provider configuration.
diff --git a/hugegraph-store/hg-store-dist/pom.xml b/hugegraph-store/hg-store-dist/pom.xml
index c75fc2ad6..ab53403f3 100644
--- a/hugegraph-store/hg-store-dist/pom.xml
+++ b/hugegraph-store/hg-store-dist/pom.xml
@@ -38,6 +38,12 @@
<executable.jar.name>hg-store-node</executable.jar.name>
</properties>
<dependencies>
+ <dependency>
+ <groupId>com.taobao.arthas</groupId>
+ <artifactId>arthas-packaging</artifactId>
+ <version>${arthas.version}</version>
+ <scope>provided</scope>
+ </dependency>
<dependency>
<groupId>org.apache.hugegraph</groupId>
<artifactId>hg-store-node</artifactId>
@@ -93,6 +99,33 @@
<artifactId>maven-antrun-plugin</artifactId>
<version>1.3</version>
<executions>
+ <execution>
+ <id>prepare-arthas</id>
+ <phase>prepare-package</phase>
+ <goals>
+ <goal>run</goal>
+ </goals>
+ <configuration>
+ <tasks>
+ <unzip src="${maven.dependency.com.taobao.arthas.arthas-packaging.jar.path}"
+ dest="${project.build.directory}/arthas-packaging">
+ <patternset>
+ <include name="arthas-bin.zip"/>
+ </patternset>
+ </unzip>
+ <delete dir="${project.build.directory}/arthas"/>
+ <unzip src="${project.build.directory}/arthas-packaging/arthas-bin.zip"
+ dest="${project.build.directory}/arthas">
+ <patternset>
+ <include name="arthas-*.jar"/>
+ <include name="logback.xml"/>
+ <include name="async-profiler/**"/>
+ <include name="lib/**"/>
+ </patternset>
+ </unzip>
+ </tasks>
+ </configuration>
+ </execution>
<execution>
<phase>package</phase>
<goals>
diff --git a/hugegraph-store/hg-store-dist/src/assembly/descriptor/server-assembly.xml b/hugegraph-store/hg-store-dist/src/assembly/descriptor/server-assembly.xml
index 47170a6c2..ee2b6af9b 100644
--- a/hugegraph-store/hg-store-dist/src/assembly/descriptor/server-assembly.xml
+++ b/hugegraph-store/hg-store-dist/src/assembly/descriptor/server-assembly.xml
@@ -25,7 +25,10 @@
<!-- <format>tar.gz</format>-->
<fileSets>
-
+ <fileSet>
+ <directory>${project.build.directory}/arthas</directory>
+ <outputDirectory>lib/arthas</outputDirectory>
+ </fileSet>
<fileSet>
<directory>${assembly.static.dir}/bin</directory>
<outputDirectory>bin</outputDirectory>
@@ -47,6 +50,16 @@
</fileSets>
<files>
+ <file>
+ <source>${project.basedir}/../../hugegraph-server/hugegraph-dist/src/assembly/static/bin/ArthasAttach.java</source>
+ <outputDirectory>bin</outputDirectory>
+ <fileMode>644</fileMode>
+ </file>
+ <file>
+ <source>${project.basedir}/../../hugegraph-server/hugegraph-dist/src/assembly/static/bin/arthas.sh</source>
+ <outputDirectory>bin</outputDirectory>
+ <fileMode>755</fileMode>
+ </file>
<file>
<source>${project.basedir}/../../hugegraph-server/hugegraph-dist/src/assembly/static/bin/prepare-topling.sh</source>
<outputDirectory>bin</outputDirectory>
diff --git a/hugegraph-store/hg-store-dist/src/assembly/static/conf/application.yml b/hugegraph-store/hg-store-dist/src/assembly/static/conf/application.yml
index 8e2b8d4f7..90a5cecbb 100644
--- a/hugegraph-store/hg-store-dist/src/assembly/static/conf/application.yml
+++ b/hugegraph-store/hg-store-dist/src/assembly/static/conf/application.yml
@@ -62,3 +62,6 @@ logging:
config: 'file:./conf/log4j2.xml'
level:
root: info
+
+
+# HTTP Arthas activation is disabled. Use bin/arthas.sh locally without restarting Store.
diff --git a/hugegraph-store/hg-store-node/pom.xml b/hugegraph-store/hg-store-node/pom.xml
index 9c952507a..2a2000850 100644
--- a/hugegraph-store/hg-store-node/pom.xml
+++ b/hugegraph-store/hg-store-node/pom.xml
@@ -121,17 +121,6 @@
<artifactId>hg-store-core</artifactId>
</dependency>
- <dependency>
- <groupId>com.taobao.arthas</groupId>
- <artifactId>arthas-agent-attach</artifactId>
- <version>3.6.4</version>
- </dependency>
- <dependency>
- <groupId>com.taobao.arthas</groupId>
- <artifactId>arthas-packaging</artifactId>
- <version>3.6.4</version>
- </dependency>
-
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/AppConfig.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/AppConfig.java
index c69f5fe89..0bc57d22d 100644
--- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/AppConfig.java
+++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/AppConfig.java
@@ -214,10 +214,10 @@ public class AppConfig {
@Value("${arthas.httpPort:8565}")
private String httpPort;
- @Value("${arthas.ip:0.0.0.0}")
+ @Value("${arthas.ip:127.0.0.1}")
private String arthasip;
- @Value("${arthas.disabledCommands:jad}")
+ @Value("${arthas.disabledCommands:}")
private String disCmd;
}
diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/controller/PartitionAPI.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/controller/PartitionAPI.java
index 39f500ad9..59cff949e 100644
--- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/controller/PartitionAPI.java
+++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/controller/PartitionAPI.java
@@ -19,6 +19,7 @@ package org.apache.hugegraph.store.node.controller;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -33,11 +34,12 @@ import org.apache.hugegraph.store.business.BusinessHandler;
import org.apache.hugegraph.store.business.InnerKeyCreator;
import org.apache.hugegraph.store.meta.Partition;
import org.apache.hugegraph.store.metric.HgStoreMetric;
-import org.apache.hugegraph.store.node.AppConfig;
import org.apache.hugegraph.store.node.grpc.HgStoreNodeService;
import org.apache.hugegraph.util.Bytes;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@@ -47,7 +49,6 @@ import org.springframework.web.bind.annotation.RestController;
import com.alipay.sofa.jraft.entity.PeerId;
import com.alipay.sofa.jraft.util.Endpoint;
-import com.taobao.arthas.agent.attach.ArthasAgent;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
@@ -60,9 +61,6 @@ public class PartitionAPI {
@Autowired
HgStoreNodeService nodeService;
- @Autowired
- AppConfig appConfig;
-
@GetMapping(value = "/partitions", produces = "application/json")
public Map<String, Object> getPartitions(
@RequestParam(required = false, defaultValue = "") String flags) {
@@ -198,19 +196,11 @@ public class PartitionAPI {
}
@GetMapping(value = "/arthasstart", produces = "application/json")
- public Map<String, Object> arthasstart(
- @RequestParam(required = false, defaultValue = "") String flags) {
- HashMap<String, String> configMap = new HashMap<>();
- configMap.put("arthas.telnetPort", appConfig.getArthasConfig().getTelnetPort());
- configMap.put("arthas.httpPort", appConfig.getArthasConfig().getHttpPort());
- configMap.put("arthas.ip", appConfig.getArthasConfig().getArthasip());
- configMap.put("arthas.disabledCommands", appConfig.getArthasConfig().getDisCmd());
- ArthasAgent.attach(configMap);
- // TODO: remove this commented-out line, retPose is never used
-// DashResponse retPose = new DashResponse();
- List<String> ret = new ArrayList<>();
- ret.add("Arthas started successfully");
- return okMap("arthasstart", ret);
+ public ResponseEntity<Map<String, String>> arthasstart() {
+ // Store has no administrator authentication. Reject all HTTP callers,
+ // including loopback proxies, and use OS permissions for local attach.
+ return ResponseEntity.status(HttpStatus.FORBIDDEN).body(Collections.singletonMap(
+ "message", "HTTP Arthas activation is disabled; run bin/arthas.sh on the Store host"));
}
@PostMapping("/compat")
diff --git a/hugegraph-store/hg-store-node/src/main/resources/application.yml b/hugegraph-store/hg-store-node/src/main/resources/application.yml
index 0b6527060..9b70aa2c6 100644
--- a/hugegraph-store/hg-store-node/src/main/resources/application.yml
+++ b/hugegraph-store/hg-store-node/src/main/resources/application.yml
@@ -49,3 +49,6 @@ logging:
config: classpath:log4j2-dev.xml
level:
root: info
+
+
+# HTTP Arthas activation is disabled. Use bin/arthas.sh locally without restarting Store.
diff --git a/hugegraph-store/hg-store-node/src/test/java/org/apache/hugegraph/store/node/controller/ArthasSecurityTest.java b/hugegraph-store/hg-store-node/src/test/java/org/apache/hugegraph/store/node/controller/ArthasSecurityTest.java
new file mode 100644
index 000000000..057ac1f70
--- /dev/null
+++ b/hugegraph-store/hg-store-node/src/test/java/org/apache/hugegraph/store/node/controller/ArthasSecurityTest.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hugegraph.store.node.controller;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.http.HttpHeaders;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+public class ArthasSecurityTest {
+
+ private MockMvc mvc;
+
+ @BeforeEach
+ public void setup() {
+ // No Store engine or Arthas configuration is needed to reject activation.
+ this.mvc = MockMvcBuilders.standaloneSetup(new PartitionAPI()).build();
+ }
+
+ @Test
+ public void testRemoteActivationIsForbidden() throws Exception {
+ assertForbidden(requestFrom("192.0.2.1"));
+ }
+
+ @Test
+ public void testLoopbackActivationIsForbidden() throws Exception {
+ assertForbidden(requestFrom("127.0.0.1"));
+ assertForbidden(requestFrom("::1"));
+ }
+
+ @Test
+ public void testSameHostProxyCannotActivateArthas() throws Exception {
+ assertForbidden(requestFrom("127.0.0.1")
+ .header("Forwarded", "for=192.0.2.1;host=store.example")
+ .header("X-Forwarded-For", "192.0.2.1"));
+ }
+
+ @Test
+ public void testSpoofedLoopbackHeadersCannotActivateArthas() throws Exception {
+ assertForbidden(requestFrom("192.0.2.1")
+ .header("Forwarded", "for=127.0.0.1")
+ .header("X-Forwarded-For", "127.0.0.1"));
+ }
+
+ @Test
+ public void testAuthorizationHeaderDoesNotEnableStoreActivation() throws Exception {
+ assertForbidden(requestFrom("127.0.0.1")
+ .header(HttpHeaders.AUTHORIZATION, "Bearer admin"));
+ }
+
+ private static MockHttpServletRequestBuilder requestFrom(String address) {
+ return get("/v1/arthasstart").with(request -> {
+ request.setRemoteAddr(address);
+ return request;
+ });
+ }
+
+ private void assertForbidden(MockHttpServletRequestBuilder request) throws Exception {
+ this.mvc.perform(request)
+ .andExpect(status().isForbidden())
+ .andExpect(jsonPath("$.message").value(
+ "HTTP Arthas activation is disabled; run bin/arthas.sh on the Store host"));
+ }
+}
diff --git a/install-dist/release-docs/LICENSE b/install-dist/release-docs/LICENSE
index 545b95664..6e27cb1e2 100644
--- a/install-dist/release-docs/LICENSE
+++ b/install-dist/release-docs/LICENSE
@@ -265,6 +265,8 @@ Arthas 4.1.5 includes shaded Jackson, FastDoubleParser, ASM 9.9, Logback 1.2.13
and RSyntaxTextArea 3.3.4.
Their additional MIT, BSD, Boost and EPL 1.0 license texts and source locations
are in licenses/LICENSE-arthas-packaging-4.1.5.txt.
+The same bundle supplies the agent, boot, client, core and spy JARs unpacked
+under lib/arthas for offline local diagnosis.
Paths referenced by upstream licenses are reproduced as labeled sections in
the corresponding per-component license file.
@@ -363,9 +365,7 @@ the corresponding per-component license file.
https://central.sonatype.com/artifact/com.squareup.okio/okio-jvm/3.0.0 -> Apache 2.0
https://central.sonatype.com/artifact/com.squareup.okio/okio/1.15.0 -> Apache 2.0
https://central.sonatype.com/artifact/com.squareup/javapoet/1.13.0 -> Apache 2.0
- https://central.sonatype.com/artifact/com.taobao.arthas/arthas-agent-attach/3.6.4 -> Apache 2.0
https://central.sonatype.com/artifact/com.taobao.arthas/arthas-agent-attach/4.1.5 -> Apache 2.0
- https://central.sonatype.com/artifact/com.taobao.arthas/arthas-packaging/3.6.4 -> Apache 2.0
https://central.sonatype.com/artifact/com.taobao.arthas/arthas-packaging/4.1.5 -> Apache 2.0
https://central.sonatype.com/artifact/com.vaadin.external.google/android-json/0.0.20131108.vaadin1 -> Apache 2.0
https://central.sonatype.com/artifact/commons-beanutils/commons-beanutils/1.9.4 -> Apache 2.0
@@ -494,7 +494,6 @@ the corresponding per-component license file.
https://central.sonatype.com/artifact/javax.validation/validation-api/1.1.0.Final -> Apache 2.0
https://central.sonatype.com/artifact/joda-time/joda-time/2.10.8 -> Apache 2.0
https://central.sonatype.com/artifact/net.bytebuddy/byte-buddy-agent/1.10.20 -> Apache 2.0
- https://central.sonatype.com/artifact/net.bytebuddy/byte-buddy-agent/1.11.6 -> Apache 2.0
https://central.sonatype.com/artifact/net.bytebuddy/byte-buddy-agent/1.18.3 -> Apache 2.0
https://central.sonatype.com/artifact/net.bytebuddy/byte-buddy-agent/1.17.6 -> Apache 2.0
https://central.sonatype.com/artifact/net.bytebuddy/byte-buddy/1.10.20 -> Apache 2.0
@@ -696,7 +695,6 @@ the corresponding per-component license file.
https://central.sonatype.com/artifact/org.springframework/spring-webmvc/5.3.27 -> Apache 2.0
https://central.sonatype.com/artifact/org.xmlunit/xmlunit-core/2.8.4 -> Apache 2.0
https://central.sonatype.com/artifact/org.yaml/snakeyaml/2.2 -> Apache 2.0
- https://central.sonatype.com/artifact/org.zeroturnaround/zt-zip/1.14 -> Apache 2.0
https://central.sonatype.com/artifact/org.zeroturnaround/zt-zip/1.16 -> Apache 2.0
https://central.sonatype.com/artifact/org.apache.commons/commons-pool2/2.0 -> Apache 2.0
https://central.sonatype.com/artifact/org.apache.lucene/lucene-analyzers-common/5.2.1 -> Apache 2.0
diff --git a/install-dist/scripts/dependency/known-dependencies.txt b/install-dist/scripts/dependency/known-dependencies.txt
index 88a0e0a92..17f97dc5b 100644
--- a/install-dist/scripts/dependency/known-dependencies.txt
+++ b/install-dist/scripts/dependency/known-dependencies.txt
@@ -14,10 +14,13 @@ antlr4-runtime-4.9.1.jar
aopalliance-repackaged-3.0.1.jar
apiguardian-api-1.1.0.jar
apiguardian-api-1.1.2.jar
-arthas-agent-attach-3.6.4.jar
arthas-agent-attach-4.1.5.jar
-arthas-packaging-3.6.4.jar
+arthas-agent.jar
+arthas-boot.jar
+arthas-client.jar
+arthas-core.jar
arthas-packaging-4.1.5.jar
+arthas-spy.jar
asm-6.0.jar
asm-commons-9.4.jar
asm-tree-9.4.jar
@@ -33,7 +36,6 @@ bolt-1.6.4.jar
byte-buddy-1.10.20.jar
byte-buddy-1.17.6.jar
byte-buddy-agent-1.10.20.jar
-byte-buddy-agent-1.11.6.jar
byte-buddy-agent-1.17.6.jar
byte-buddy-agent-1.18.3.jar
caffeine-2.3.1.jar
@@ -515,5 +517,4 @@ xmlunit-core-2.8.4.jar
zjsonpatch-0.3.0.jar
zookeeper-3.8.6.jar
zookeeper-jute-3.8.6.jar
-zt-zip-1.14.jar
zt-zip-1.16.jar
diff --git a/pom.xml b/pom.xml
index d5c382037..6e6134b9a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -86,6 +86,7 @@
</scm>
<properties>
+ <arthas.version>4.1.5</arthas.version>
<fabric8.version>5.6.0</fabric8.version>
<revision>1.8.0</revision>
<groovy.version>4.0.25</groovy.version>
@@ -281,6 +282,7 @@
<exclude>**/pd/grpc/**/*.java</exclude>
<exclude>**/store/grpc/**/*.java</exclude>
<exclude>**/target/**</exclude>
+ <exclude>**/apache-hugegraph-*/lib/arthas/logback.xml</exclude>
<exclude>dist/**/*</exclude>
<!-- Git & GitHub -->
<exclude>.github/**/*</exclude>