From a068c8393415125aa7789c9dc23e69a34e2816a1 Mon Sep 17 00:00:00 2001 From: 317787106 <317787106@qq.com> Date: Tue, 4 Aug 2026 14:33:42 +0800 Subject: [PATCH 01/15] feat(framework): support admin RPC --- .../common/parameter/CommonParameter.java | 16 + .../org/tron/core/config/args/NodeConfig.java | 11 + common/src/main/resources/reference.conf | 13 + .../tron/core/config/args/NodeConfigTest.java | 16 + framework/build.gradle | 2 + .../common/application/AbstractService.java | 9 +- .../tron/common/application/HttpService.java | 7 +- .../java/org/tron/core/config/args/Args.java | 13 +- .../tron/core/config/args/CLIParameter.java | 4 + .../core/services/admin/AdminJsonRpc.java | 21 ++ .../core/services/admin/AdminJsonRpcImpl.java | 28 ++ .../admin/CommonParameterExporter.java | 102 +++++++ .../admin/http/AdminRpcHttpService.java | 44 +++ .../services/admin/http/AdminRpcServlet.java | 71 +++++ .../core/services/admin/ipc/IpcClient.java | 286 ++++++++++++++++++ .../admin/ipc/IpcCommandCompleter.java | 36 +++ .../core/services/admin/ipc/IpcService.java | 168 ++++++++++ .../main/java/org/tron/program/FullNode.java | 5 + .../org/tron/core/config/args/ArgsTest.java | 22 ++ .../admin/CommonParameterExporterTest.java | 105 +++++++ .../services/admin/ipc/IpcClientTest.java | 99 ++++++ .../services/admin/ipc/IpcServiceTest.java | 88 ++++++ .../org/tron/keystroe/CredentialsTest.java | 33 ++ gradle/verification-metadata.xml | 44 +++ 24 files changed, 1238 insertions(+), 5 deletions(-) create mode 100644 framework/src/main/java/org/tron/core/services/admin/AdminJsonRpc.java create mode 100644 framework/src/main/java/org/tron/core/services/admin/AdminJsonRpcImpl.java create mode 100644 framework/src/main/java/org/tron/core/services/admin/CommonParameterExporter.java create mode 100644 framework/src/main/java/org/tron/core/services/admin/http/AdminRpcHttpService.java create mode 100644 framework/src/main/java/org/tron/core/services/admin/http/AdminRpcServlet.java create mode 100644 framework/src/main/java/org/tron/core/services/admin/ipc/IpcClient.java create mode 100644 framework/src/main/java/org/tron/core/services/admin/ipc/IpcCommandCompleter.java create mode 100644 framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java create mode 100644 framework/src/test/java/org/tron/core/services/admin/CommonParameterExporterTest.java create mode 100644 framework/src/test/java/org/tron/core/services/admin/ipc/IpcClientTest.java create mode 100644 framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java create mode 100644 framework/src/test/java/org/tron/keystroe/CredentialsTest.java diff --git a/common/src/main/java/org/tron/common/parameter/CommonParameter.java b/common/src/main/java/org/tron/common/parameter/CommonParameter.java index eeb92fdbd60..21260eb8210 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -182,6 +182,10 @@ public class CommonParameter { @Setter public boolean keystoreFactory = false; + @Getter + @Setter + public String ipcSocketFile = ""; + // -- RPC / HTTP -- @Getter @Setter @@ -490,6 +494,18 @@ public class CommonParameter { public int jsonRpcMaxLogFilterNum = 20000; @Getter @Setter + public boolean adminRpcEnable = false; + @Getter + @Setter + public String adminListenAddress = Constant.LOCAL_HOST; + @Getter + @Setter + public int adminListenPort = 8575; + @Getter + @Setter + public boolean ipcEnable = false; + @Getter + @Setter public int maxTransactionPendingSize; @Getter @Setter diff --git a/common/src/main/java/org/tron/core/config/args/NodeConfig.java b/common/src/main/java/org/tron/core/config/args/NodeConfig.java index 2158f56d0ba..22f91238305 100644 --- a/common/src/main/java/org/tron/core/config/args/NodeConfig.java +++ b/common/src/main/java/org/tron/core/config/args/NodeConfig.java @@ -38,6 +38,7 @@ public class NodeConfig { private int minParticipationRate = 0; private boolean openPrintLog = true; private boolean openTransactionSort = false; + private boolean ipcEnable = false; private int maxTps = 1000; private int maxBlockInvPerSecond = 10; private boolean openFullTcpDisconnect = false; //rename key @@ -128,6 +129,7 @@ public int getValidContractProtoThreads() { private HttpConfig http = new HttpConfig(); private RpcConfig rpc = new RpcConfig(); private JsonRpcConfig jsonrpc = new JsonRpcConfig(); + private AdminRpcConfig adminRpc = new AdminRpcConfig(); private NodeBackupConfig backup = new NodeBackupConfig(); private DynamicConfigSection dynamicConfig = new DynamicConfigSection(); private DnsConfig dns = new DnsConfig(); @@ -249,6 +251,15 @@ public static class JsonRpcConfig { private long maxMessageSize = 4194304; } + @Getter + @Setter + public static class AdminRpcConfig { + + private boolean enable = false; + private String listenAddress = "127.0.0.1"; + private int port = 8575; + } + @Getter @Setter public static class NodeBackupConfig { diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index 25fc4832e55..9622f2c74df 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -443,6 +443,19 @@ node { maxMessageSize = 4194304 } + # Whether to enable the local Unix-domain socket admin API. + ipcEnable = false + + # Administrative JSON-RPC settings. Disabled by default and bound to loopback only. + adminRpc { + # Whether to enable the administrative JSON-RPC HTTP service. Default: false. + enable = false + # Address on which the service listens. Keep the default loopback address for security. + listenAddress = "127.0.0.1" + # TCP port on which the administrative JSON-RPC HTTP service listens. Default: 8575. + port = 8575 + } + # Disabled API list (works for http, rpc and pbft, not jsonrpc). Case insensitive. disabledApi = [ # "getaccount", diff --git a/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java b/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java index bbc2d2475ee..2f5d274a32b 100644 --- a/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java @@ -28,6 +28,10 @@ public void testDefaults() { assertEquals(8, nc.getMinConnections()); assertEquals(4, nc.getMaxFastForwardNum()); assertFalse(nc.isOpenFullTcpDisconnect()); + assertFalse(nc.isIpcEnable()); + assertFalse(nc.getAdminRpc().isEnable()); + assertEquals("127.0.0.1", nc.getAdminRpc().getListenAddress()); + assertEquals(8575, nc.getAdminRpc().getPort()); // reference.conf matches code default: discovery disabled when not configured assertFalse(nc.isDiscoveryEnable()); assertFalse(nc.isDiscoveryPersist()); @@ -77,6 +81,18 @@ public void testRpcSubBean() { assertEquals(60071, nc.getRpc().getPBFTPort()); } + @Test + public void testAdminRpcAndIpcBinding() { + Config config = withRef( + "node { ipcEnable = true, adminRpc { enable = true," + + " listenAddress = \"127.0.0.2\", port = 18575 } }"); + NodeConfig nc = NodeConfig.fromConfig(config); + assertTrue(nc.isIpcEnable()); + assertTrue(nc.getAdminRpc().isEnable()); + assertEquals("127.0.0.2", nc.getAdminRpc().getListenAddress()); + assertEquals(18575, nc.getAdminRpc().getPort()); + } + @Test public void testBackupSubBean() { Config config = withRef( diff --git a/framework/build.gradle b/framework/build.gradle index 0ce33f253cf..3b5ffa36627 100644 --- a/framework/build.gradle +++ b/framework/build.gradle @@ -58,6 +58,8 @@ dependencies { testImplementation group: 'org.springframework', name: 'spring-test', version: "${springVersion}" testImplementation group: 'javax.portlet', name: 'portlet-api', version: '3.0.1' implementation group: 'org.zeromq', name: 'jeromq', version: '0.5.3' + implementation group: 'com.kohlschutter.junixsocket', name: 'junixsocket-core', version: '2.10.1' + implementation group: 'org.jline', name: 'jline', version: '3.21.0' api project(":chainbase") api project(":protocol") api project(":actuator") diff --git a/framework/src/main/java/org/tron/common/application/AbstractService.java b/framework/src/main/java/org/tron/common/application/AbstractService.java index 79c25dc4944..4d0317c0eb8 100644 --- a/framework/src/main/java/org/tron/common/application/AbstractService.java +++ b/framework/src/main/java/org/tron/common/application/AbstractService.java @@ -10,6 +10,7 @@ @Slf4j(topic = "service") public abstract class AbstractService implements Service { + protected String listenAddress; protected int port; @Getter protected boolean enable; @@ -19,12 +20,16 @@ public abstract class AbstractService implements Service { @Override public CompletableFuture start() { - logger.info("{} starting on {}", name, port); + if (port > 0) { + logger.info("{} starting on {}", name, port); + } final CompletableFuture resultFuture = new CompletableFuture<>(); try { innerStart(); resultFuture.complete(true); - logger.info("{} started, listening on {}", name, port); + if (port > 0) { + logger.info("{} started, listening on {}", name, port); + } } catch (Exception e) { resultFuture.completeExceptionally(e); } diff --git a/framework/src/main/java/org/tron/common/application/HttpService.java b/framework/src/main/java/org/tron/common/application/HttpService.java index 1dea271ec69..bdc7db3c61b 100644 --- a/framework/src/main/java/org/tron/common/application/HttpService.java +++ b/framework/src/main/java/org/tron/common/application/HttpService.java @@ -17,6 +17,7 @@ import com.google.common.annotations.VisibleForTesting; import java.io.IOException; +import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.util.concurrent.CompletableFuture; import javax.servlet.RequestDispatcher; @@ -77,7 +78,11 @@ public CompletableFuture start() { } protected void initServer() { - this.apiServer = new Server(this.port); + if (this.listenAddress == null) { + this.apiServer = new Server(this.port); + } else { + this.apiServer = new Server(new InetSocketAddress(this.listenAddress, this.port)); + } int maxHttpConnectNumber = Args.getInstance().getMaxHttpConnectNumber(); if (maxHttpConnectNumber > 0) { this.apiServer.addBean(new ConnectionLimit(maxHttpConnectNumber, this.apiServer)); diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index 0bca242606e..65e3b6653b0 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -561,6 +561,13 @@ private static void applyNodeConfig(NodeConfig nc) { PARAMETER.jsonRpcMaxLogFilterNum = jsonrpc.getMaxLogFilterNum(); PARAMETER.jsonRpcMaxMessageSize = jsonrpc.getMaxMessageSize(); + // ---- Admin RPC / IPC ---- + NodeConfig.AdminRpcConfig adminRpc = nc.getAdminRpc(); + PARAMETER.adminRpcEnable = adminRpc.isEnable(); + PARAMETER.adminListenAddress = adminRpc.getListenAddress(); + PARAMETER.adminListenPort = adminRpc.getPort(); + PARAMETER.ipcEnable = nc.isIpcEnable(); + // ---- P2P sub-bean ---- PARAMETER.nodeP2pVersion = nc.getP2p().getVersion(); @@ -861,6 +868,9 @@ private static void applyCLIParams(CLIParameter cmd, JCommander jc) { if (assigned.contains("--keystore-factory")) { PARAMETER.keystoreFactory = cmd.keystoreFactory; } + if (assigned.contains("--attach")) { + PARAMETER.ipcSocketFile = cmd.ipcSocketFile; + } if (assigned.contains("--rpc-thread")) { PARAMETER.rpcThreadNum = cmd.rpcThreadNum; } @@ -1292,7 +1302,7 @@ private static String getCommitIdAbbrev() { private static Map getOptionGroup() { String[] tronOption = new String[] {"version", "help", "shellConfFileName", "logbackPath", - "eventSubscribe", "solidityNode", "keystoreFactory"}; + "eventSubscribe", "solidityNode", "keystoreFactory", "ipcSocketFile"}; String[] dbOption = new String[] {"outputDirectory"}; String[] witnessOption = new String[] {"witness", "privateKey"}; String[] vmOption = new String[] {"debug"}; @@ -1315,4 +1325,3 @@ private static Map getOptionGroup() { return optionGroupMap; } } - diff --git a/framework/src/main/java/org/tron/core/config/args/CLIParameter.java b/framework/src/main/java/org/tron/core/config/args/CLIParameter.java index 4f056a32e3a..d1f0bfec909 100644 --- a/framework/src/main/java/org/tron/core/config/args/CLIParameter.java +++ b/framework/src/main/java/org/tron/core/config/args/CLIParameter.java @@ -53,6 +53,10 @@ public class CLIParameter { @Parameter(names = {"--keystore-factory"}, description = "running KeystoreFactory") public boolean keystoreFactory; + @Parameter(names = {"--attach"}, + description = "running an IPC client to interact with FullNode") + public String ipcSocketFile; + @Deprecated @Parameter(names = {"--fast-forward"}) public boolean fastForward; diff --git a/framework/src/main/java/org/tron/core/services/admin/AdminJsonRpc.java b/framework/src/main/java/org/tron/core/services/admin/AdminJsonRpc.java new file mode 100644 index 00000000000..b49e7a93ffd --- /dev/null +++ b/framework/src/main/java/org/tron/core/services/admin/AdminJsonRpc.java @@ -0,0 +1,21 @@ +package org.tron.core.services.admin; + +import com.googlecode.jsonrpc4j.JsonRpcError; +import com.googlecode.jsonrpc4j.JsonRpcErrors; +import com.googlecode.jsonrpc4j.JsonRpcMethod; +import com.googlecode.jsonrpc4j.JsonRpcParam; +import java.util.Map; +import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; + +public interface AdminJsonRpc { + + @JsonRpcMethod("admin_example") + @JsonRpcErrors({ + @JsonRpcError(exception = JsonRpcInvalidParamsException.class, code = -32602, data = "{}"), + }) + String adminExample(@JsonRpcParam("param1") String param1, @JsonRpcParam("param2") String param2) + throws JsonRpcInvalidParamsException; + + @JsonRpcMethod("admin_getRuntimeParameters") + Map getRuntimeParameters(); +} diff --git a/framework/src/main/java/org/tron/core/services/admin/AdminJsonRpcImpl.java b/framework/src/main/java/org/tron/core/services/admin/AdminJsonRpcImpl.java new file mode 100644 index 00000000000..19e9511f847 --- /dev/null +++ b/framework/src/main/java/org/tron/core/services/admin/AdminJsonRpcImpl.java @@ -0,0 +1,28 @@ +package org.tron.core.services.admin; + +import java.util.Map; +import org.springframework.stereotype.Component; +import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; + +@Component +public class AdminJsonRpcImpl implements AdminJsonRpc { + + private final CommonParameterExporter commonParameterExporter; + + public AdminJsonRpcImpl(CommonParameterExporter commonParameterExporter) { + this.commonParameterExporter = commonParameterExporter; + } + + @Override + public String adminExample(String param1, String param2) throws JsonRpcInvalidParamsException { + if ("".equals(param1) || "".equals(param2)) { + throw new JsonRpcInvalidParamsException("param1 or param2 should not be empty"); + } + return param1 + ":" + param2; + } + + @Override + public Map getRuntimeParameters() { + return commonParameterExporter.export(); + } +} diff --git a/framework/src/main/java/org/tron/core/services/admin/CommonParameterExporter.java b/framework/src/main/java/org/tron/core/services/admin/CommonParameterExporter.java new file mode 100644 index 00000000000..3271e8f27d2 --- /dev/null +++ b/framework/src/main/java/org/tron/core/services/admin/CommonParameterExporter.java @@ -0,0 +1,102 @@ +package org.tron.core.services.admin; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.tron.common.parameter.CommonParameter; + +@Component +@Slf4j(topic = "API") +public class CommonParameterExporter { + + static final String REDACTED_VALUE = "[REDACTED]"; + private static final String UNAVAILABLE_VALUE = "[UNAVAILABLE]"; + private static final String[] SENSITIVE_NAME_PARTS = { + "private", "password", "passwd", "secret", "credential", "mnemonic", + "accesskey", "apikey", "localwitness", "seedphrase", "dbconfig", + "authorization", "authtoken" + }; + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + public Map export() { + return export(CommonParameter.getInstance()); + } + + Map export(CommonParameter parameter) { + ObjectNode snapshot = OBJECT_MAPPER.createObjectNode(); + Field[] fields = CommonParameter.class.getFields(); + Arrays.sort(fields, Comparator.comparing(Field::getName)); + for (Field field : fields) { + String fieldName = field.getName(); + if (isSensitiveName(fieldName)) { + snapshot.put(fieldName, REDACTED_VALUE); + continue; + } + + try { + Object target = Modifier.isStatic(field.getModifiers()) ? null : parameter; + JsonNode value = OBJECT_MAPPER.valueToTree(field.get(target)); + snapshot.set(fieldName, sanitize(value)); + } catch (IllegalAccessException | IllegalArgumentException e) { + logger.warn("Unable to export runtime parameter {}", fieldName); + snapshot.put(fieldName, UNAVAILABLE_VALUE); + } + } + return OBJECT_MAPPER.convertValue(snapshot, + new TypeReference>() { }); + } + + JsonNode sanitize(JsonNode value) { + if (value == null || value.isNull() || value.isValueNode()) { + return value; + } + if (value.isArray()) { + ArrayNode sanitized = OBJECT_MAPPER.createArrayNode(); + for (JsonNode element : value) { + sanitized.add(sanitize(element)); + } + return sanitized; + } + if (value.isObject()) { + ObjectNode sanitized = OBJECT_MAPPER.createObjectNode(); + Iterator> fields = value.fields(); + while (fields.hasNext()) { + Entry field = fields.next(); + if (isSensitiveName(field.getKey())) { + sanitized.put(field.getKey(), REDACTED_VALUE); + } else { + sanitized.set(field.getKey(), sanitize(field.getValue())); + } + } + return sanitized; + } + return value; + } + + private boolean isSensitiveName(String name) { + String normalized = name.toLowerCase(Locale.ROOT); + if ("pwd".equals(normalized) || "key".equals(normalized) + || "token".equals(normalized) || "auth".equals(normalized)) { + return true; + } + for (String part : SENSITIVE_NAME_PARTS) { + if (normalized.contains(part)) { + return true; + } + } + return false; + } +} diff --git a/framework/src/main/java/org/tron/core/services/admin/http/AdminRpcHttpService.java b/framework/src/main/java/org/tron/core/services/admin/http/AdminRpcHttpService.java new file mode 100644 index 00000000000..04c6db8fefc --- /dev/null +++ b/framework/src/main/java/org/tron/core/services/admin/http/AdminRpcHttpService.java @@ -0,0 +1,44 @@ +package org.tron.core.services.admin.http; + +import java.util.EnumSet; +import javax.servlet.DispatcherType; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jetty.servlet.FilterHolder; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.tron.common.application.HttpService; +import org.tron.core.config.args.Args; +import org.tron.core.services.filter.HttpInterceptor; + +@Component +@Slf4j(topic = "API") +public class AdminRpcHttpService extends HttpService { + + @Autowired + private AdminRpcServlet adminRpcServlet; + + public AdminRpcHttpService() { + enable = isFullNode() && Args.getInstance().isAdminRpcEnable(); + listenAddress = Args.getInstance().getAdminListenAddress(); + port = Args.getInstance().getAdminListenPort(); + contextPath = "/"; + } + + @Override + protected void addServlet(ServletContextHandler context) { + context.addServlet(new ServletHolder(adminRpcServlet), "/admin"); + } + + @Override + protected void addFilter(ServletContextHandler context) { + // filter + ServletHandler handler = new ServletHandler(); + FilterHolder fh = handler + .addFilterWithMapping(HttpInterceptor.class, "/*", + EnumSet.of(DispatcherType.REQUEST)); + context.addFilter(fh, "/*", EnumSet.of(DispatcherType.REQUEST)); + } +} diff --git a/framework/src/main/java/org/tron/core/services/admin/http/AdminRpcServlet.java b/framework/src/main/java/org/tron/core/services/admin/http/AdminRpcServlet.java new file mode 100644 index 00000000000..71ceb5a5a95 --- /dev/null +++ b/framework/src/main/java/org/tron/core/services/admin/http/AdminRpcServlet.java @@ -0,0 +1,71 @@ +package org.tron.core.services.admin.http; + +import com.googlecode.jsonrpc4j.HttpStatusCodeProvider; +import com.googlecode.jsonrpc4j.JsonRpcInterceptor; +import com.googlecode.jsonrpc4j.JsonRpcServer; +import com.googlecode.jsonrpc4j.ProxyUtil; +import java.io.IOException; +import java.util.Collections; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.tron.common.parameter.CommonParameter; +import org.tron.core.services.admin.AdminJsonRpc; +import org.tron.core.services.http.RateLimiterServlet; +import org.tron.core.services.jsonrpc.JsonRpcErrorResolver; + +@Component +@Slf4j(topic = "API") +public class AdminRpcServlet extends RateLimiterServlet { + + private static final long serialVersionUID = 0L; + + private JsonRpcServer rpcServer = null; + + @Autowired + private AdminJsonRpc adminJsonRpc; + + @Autowired + private JsonRpcInterceptor interceptor; + + @Override + public void init(ServletConfig config) throws ServletException { + super.init(config); + + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + Object compositeService = ProxyUtil.createCompositeServiceProxy(cl, + new Object[] {adminJsonRpc}, + new Class[] {AdminJsonRpc.class}, + true); + + rpcServer = new JsonRpcServer(compositeService); + rpcServer.setErrorResolver(JsonRpcErrorResolver.INSTANCE); + + HttpStatusCodeProvider httpStatusCodeProvider = new HttpStatusCodeProvider() { + @Override + public int getHttpStatusCode(int resultCode) { + return 200; + } + + @Override + public Integer getJsonRpcCode(int httpStatusCode) { + return null; + } + }; + rpcServer.setHttpStatusCodeProvider(httpStatusCodeProvider); + + rpcServer.setShouldLogInvocationErrors(false); + if (CommonParameter.getInstance().isMetricsPrometheusEnable()) { + rpcServer.setInterceptorList(Collections.singletonList(interceptor)); + } + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + rpcServer.handle(req, resp); + } +} diff --git a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcClient.java b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcClient.java new file mode 100644 index 00000000000..b16bd6cdfe0 --- /dev/null +++ b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcClient.java @@ -0,0 +1,286 @@ +package org.tron.core.services.admin.ipc; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.googlecode.jsonrpc4j.JsonRpcMethod; +import com.googlecode.jsonrpc4j.JsonRpcParam; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.jline.reader.Completer; +import org.jline.reader.EndOfFileException; +import org.jline.reader.LineReader; +import org.jline.reader.LineReaderBuilder; +import org.jline.reader.UserInterruptException; +import org.jline.reader.impl.DefaultParser; +import org.jline.reader.impl.completer.ArgumentCompleter; +import org.jline.reader.impl.completer.NullCompleter; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; +import org.newsclub.net.unix.AFUNIXSocket; +import org.newsclub.net.unix.AFUNIXSocketAddress; +import org.tron.core.services.admin.AdminJsonRpc; + +@Slf4j(topic = "API") +public class IpcClient { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private final String socketFilePath; + private final Map commandLowerMap; + private final Map> commandParameters; + private int requestId = 0; + + public IpcClient(String socketFilePath) { + this.socketFilePath = socketFilePath; + this.commandLowerMap = collectAdminCommands(); + this.commandParameters = collectAdminCommandParams(); + } + + public static void start(String socketFilePath) { + IpcClient ipcClient = new IpcClient(socketFilePath); + try { + ipcClient.run(); + } catch (IOException e) { + logger.error("", e); + } + } + + private Map collectAdminCommands() { + Map commandMap = new HashMap<>(); + Class rpcInterface = AdminJsonRpc.class; + for (Method method : rpcInterface.getDeclaredMethods()) { + JsonRpcMethod rpcMethod = method.getAnnotation(JsonRpcMethod.class); + if (rpcMethod != null && rpcMethod.value() != null) { + commandMap.put(rpcMethod.value().toLowerCase(Locale.ROOT), rpcMethod.value()); + } + } + return commandMap; + } + + private Map> collectAdminCommandParams() { + Map> commandParameters = new HashMap<>(); + Class rpcInterface = AdminJsonRpc.class; + + for (Method method : rpcInterface.getDeclaredMethods()) { + JsonRpcMethod rpcMethod = method.getAnnotation(JsonRpcMethod.class); + if (rpcMethod == null || rpcMethod.value() == null) { + continue; + } + + String methodName = rpcMethod.value().toLowerCase(Locale.ROOT); + List params = new ArrayList<>(); + Annotation[][] paramAnnotations = method.getParameterAnnotations(); + for (Annotation[] annotations : paramAnnotations) { + for (Annotation anno : annotations) { + if (anno instanceof JsonRpcParam) { + JsonRpcParam p = (JsonRpcParam) anno; + params.add(p.value()); + } + } + } + commandParameters.put(methodName, params); + } + return commandParameters; + } + + private void printHelp() { + System.out.println("Available commands:"); + for (String usage : buildHelpLines()) { + System.out.println(" " + usage); + } + } + + List buildHelpLines() { + List commands = new ArrayList<>(commandLowerMap.values()); + Collections.sort(commands); + List helpLines = new ArrayList<>(); + for (String command : commands) { + helpLines.add(formatUsage(command)); + } + helpLines.add("help [command]"); + helpLines.add("exit"); + helpLines.add("quit"); + return helpLines; + } + + private String formatUsage(String command) { + List parameters = commandParameters.get(command.toLowerCase(Locale.ROOT)); + if (parameters == null || parameters.isEmpty()) { + return command; + } + return command + " <" + StringUtils.join(parameters, "> <") + ">"; + } + + public void run() throws IOException { + File socketFile = new File(socketFilePath); + if (!socketFile.exists()) { + System.err.println("IPC socket file does not exist: " + socketFile.getName()); + return; + } + AFUNIXSocketAddress address = AFUNIXSocketAddress.of(socketFile); + try (Socket socket = AFUNIXSocket.newInstance()) { + socket.connect(address); + System.out.println("Connected to server: " + socketFile.getAbsolutePath()); + try (Terminal terminal = TerminalBuilder.builder().system(true).build()) { + LineReader reader = createLineReader(terminal); + runSession(socket, reader); + } + } + } + + void runSession(Socket socket, LineReader reader) throws IOException { + AtomicBoolean connected = new AtomicBoolean(true); + outputResponse(socket, reader, connected, Thread.currentThread()); + printHelp(); + try { + inputRequest(socket, reader, connected); + } finally { + connected.set(false); + } + } + + /** + * start a thread to receive response from IPC server + */ + private void outputResponse(final Socket socket, LineReader reader, AtomicBoolean connected, + Thread inputThread) throws IOException { + final BufferedReader serverReader = new BufferedReader( + new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8)); + + Thread readerThread = new Thread(() -> { + try { + String response; + while ((response = serverReader.readLine()) != null) { + reader.printAbove(response); + } + } catch (IOException e) { + logger.debug("IPC response stream closed: {}", e.getMessage()); + } finally { + notifyDisconnected(connected, reader); + inputThread.interrupt(); + } + }, "admin-ipc-client-reader"); + readerThread.setDaemon(true); + readerThread.start(); + } + + private LineReader createLineReader(Terminal terminal) { + Completer commandCompleter = + new IpcCommandCompleter(commandLowerMap.keySet().toArray(new String[0])); + ArgumentCompleter completer = new ArgumentCompleter( + commandCompleter, + NullCompleter.INSTANCE + ); + return LineReaderBuilder.builder() + .terminal(terminal) + .completer(completer) + .parser(new DefaultParser()) + .variable(LineReader.INDENTATION, 2) + .option(LineReader.Option.AUTO_FRESH_LINE, true) + .option(LineReader.Option.CASE_INSENSITIVE, true) + .build(); + } + + /** + * read from System.in and send command to IPC server + */ + private void inputRequest(final Socket socket, LineReader reader, AtomicBoolean connected) { + String prompt = "> "; + + try (BufferedWriter serverWriter = new BufferedWriter( + new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8))) { + while (connected.get()) { + try { + String cmdLine = reader.readLine(prompt).trim(); + String[] cmdArray = cmdLine.split("\\s+"); + // split on trim() string will always return at the minimum: [""] + String cmd = cmdArray[0]; + if ("".equals(cmd)) { + continue; + } + String cmdLowerCase = cmd.toLowerCase(Locale.ROOT); + + if ("help".equals(cmdLowerCase)) { + if (cmdArray.length == 2 + && commandLowerMap.containsKey(cmdArray[1].toLowerCase(Locale.ROOT))) { + String rpcMethod = cmdArray[1].toLowerCase(Locale.ROOT); + System.out.println("usage: " + formatUsage(commandLowerMap.get(rpcMethod))); + } else { + printHelp(); + } + continue; + } else if ("exit".equals(cmdLowerCase) || "quit".equals(cmdLowerCase)) { + break; + } else if (!commandLowerMap.containsKey(cmdLowerCase)) { + System.err.println("Invalid cmd: " + cmd); + printHelp(); + continue; + } else if (cmdArray.length - 1 != commandParameters.get(cmdLowerCase).size()) { + System.err.println("Invalid parameter, usage: " + + formatUsage(commandLowerMap.get(cmdLowerCase))); + continue; + } + + List values = + new ArrayList<>(Arrays.asList(cmdArray).subList(1, cmdArray.length)); + + String request = buildJsonWithParameter(commandLowerMap.get(cmdLowerCase), values); + System.out.println("Sending request: " + request); + serverWriter.write(request); + serverWriter.newLine(); + serverWriter.flush(); + } catch (UserInterruptException e) { + // Ctrl + C or server disconnected + break; + } catch (EndOfFileException e) { + // Ctrl + D + break; + } catch (JsonProcessingException e) { + logger.error("Failed to build IPC request", e); + } catch (IOException e) { + notifyDisconnected(connected, reader); + break; + } catch (Exception e) { + logger.error("Failed to process IPC command", e); + } + } + } catch (IOException e) { + notifyDisconnected(connected, reader); + } + } + + private void notifyDisconnected(AtomicBoolean connected, LineReader reader) { + if (connected.compareAndSet(true, false)) { + reader.printAbove("Disconnected from server."); + } + } + + private String buildJsonWithParameter(String cmd, List values) + throws JsonProcessingException { + Map params = new LinkedHashMap<>(); + params.put("jsonrpc", "2.0"); + params.put("method", cmd); + params.put("params", values); + params.put("id", ++requestId); + return OBJECT_MAPPER.writeValueAsString(params); + } +} diff --git a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcCommandCompleter.java b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcCommandCompleter.java new file mode 100644 index 00000000000..999b03a6906 --- /dev/null +++ b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcCommandCompleter.java @@ -0,0 +1,36 @@ +package org.tron.core.services.admin.ipc; + +import java.util.List; +import java.util.Locale; +import org.jline.reader.Candidate; +import org.jline.reader.Completer; +import org.jline.reader.LineReader; +import org.jline.reader.ParsedLine; + +public class IpcCommandCompleter implements Completer { + + private final String[] commands; + + public IpcCommandCompleter(String... commands) { + this.commands = commands; + } + + @Override + public void complete(LineReader reader, ParsedLine line, List candidates) { + String buffer = line.word().toLowerCase(Locale.ROOT); + + for (String cmd : commands) { + if (cmd.toLowerCase(Locale.ROOT).startsWith(buffer)) { + candidates.add(new Candidate( + cmd, + cmd, + null, + null, + null, + null, + true + )); + } + } + } +} diff --git a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java new file mode 100644 index 00000000000..54afe1883df --- /dev/null +++ b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java @@ -0,0 +1,168 @@ +package org.tron.core.services.admin.ipc; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.googlecode.jsonrpc4j.JsonRpcServer; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.lang.management.ManagementFactory; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.concurrent.ExecutorService; +import lombok.extern.slf4j.Slf4j; +import org.newsclub.net.unix.AFUNIXServerSocket; +import org.newsclub.net.unix.AFUNIXSocket; +import org.newsclub.net.unix.AFUNIXSocketAddress; +import org.springframework.stereotype.Component; +import org.tron.common.application.AbstractService; +import org.tron.common.es.ExecutorServiceManager; +import org.tron.common.exit.ExitManager; +import org.tron.common.parameter.CommonParameter; +import org.tron.core.config.args.Args; +import org.tron.core.services.admin.AdminJsonRpc; + +@Component +@Slf4j(topic = "API") +public class IpcService extends AbstractService { + + private final String esName = "admin-ipc-server"; + private final ExecutorService pool = ExecutorServiceManager.newSingleThreadExecutor(esName, true); + private volatile boolean isRunning = true; + private AFUNIXServerSocket unixServerSocket; + private volatile AFUNIXSocket activeClientSocket; + private Path socketFilePath; + private final JsonRpcServer jsonRpcServer; + + public IpcService(AdminJsonRpc adminJsonRpc) { + enable = isFullNode() && Args.getInstance().isIpcEnable(); + port = -1; //not used + jsonRpcServer = new JsonRpcServer(new ObjectMapper(), adminJsonRpc, AdminJsonRpc.class); + } + + @Override + public void innerStart() throws Exception { + socketFilePath = resolveSocketFilePath(Args.getInstance(), getPid()); + createParentDirectories(socketFilePath); + Files.deleteIfExists(socketFilePath); + + File socketFile = socketFilePath.toFile(); + socketFile.deleteOnExit(); + + AFUNIXSocketAddress address = AFUNIXSocketAddress.of(socketFile); + unixServerSocket = AFUNIXServerSocket.bindOn(address); + unixServerSocket.setShutdownOnClose(true); + + logger.info("IpcService started, listening on {}", socketFile.getAbsolutePath()); + Runnable runnable = () -> { + while (isRunning) { + AFUNIXSocket client = null; + try { + client = unixServerSocket.accept(); + activeClientSocket = client; + if (isRunning) { + handleClient(client); + } + } catch (Throwable throwable) { + if (isRunning) { + logger.error("Handle IPC request error", throwable); + } + ExitManager.findTronError(throwable).ifPresent(e -> { + throw e; + }); + } finally { + closeClientSocket(client); + activeClientSocket = null; + } + } + }; + ExecutorServiceManager.submit(pool, runnable); + } + + private void handleClient(AFUNIXSocket client) { + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(client.getInputStream(), StandardCharsets.UTF_8)); + BufferedWriter writer = new BufferedWriter( + new OutputStreamWriter(client.getOutputStream(), StandardCharsets.UTF_8))) { + + String line; + while ((line = reader.readLine()) != null) { + String cmd = line.trim(); + logger.info("Server received: {}", cmd); + writer.write(handleCommand(cmd)); + writer.newLine(); + writer.flush(); + } + } catch (IOException e) { + if (isRunning) { + logger.error("Client disconnected {}", client); + } + } + } + + private String handleCommand(String jsonRequest) { + ByteArrayInputStream input = + new ByteArrayInputStream(jsonRequest.getBytes(StandardCharsets.UTF_8)); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + + String response; + try { + jsonRpcServer.handleRequest(input, output); + response = output.toString(StandardCharsets.UTF_8.name()); + } catch (IOException e) { + response = e.getMessage(); + } + + logger.info("IPC response: {}", response); + return response; + } + + @Override + public void innerStop() throws Exception { + logger.info("Begin to stop IpcService ..."); + isRunning = false; + closeClientSocket(activeClientSocket); + if (unixServerSocket != null) { + unixServerSocket.close(); + } + ExecutorServiceManager.shutdownAndAwaitTermination(pool, esName); + if (socketFilePath != null) { + Files.deleteIfExists(socketFilePath); + } + logger.info("IpcService stopped"); + } + + private void closeClientSocket(AFUNIXSocket client) { + if (client == null) { + return; + } + try { + client.close(); + } catch (IOException e) { + logger.warn("Failed to close IPC client socket", e); + } + } + + static Path resolveSocketFilePath(CommonParameter parameter, String pid) { + return Paths.get(parameter.getOutputDirectory(), + "java-tron." + pid + ".sock"); + } + + static void createParentDirectories(Path socketFilePath) throws IOException { + Path parent = socketFilePath.getParent(); + if (parent != null) { + Files.createDirectories(parent); + } + } + + public static String getPid() { + String name = ManagementFactory.getRuntimeMXBean().getName(); + return name.split("@")[0]; + } +} diff --git a/framework/src/main/java/org/tron/program/FullNode.java b/framework/src/main/java/org/tron/program/FullNode.java index 96b9f73d577..fec66d2863e 100644 --- a/framework/src/main/java/org/tron/program/FullNode.java +++ b/framework/src/main/java/org/tron/program/FullNode.java @@ -14,6 +14,7 @@ import org.tron.core.config.DefaultConfig; import org.tron.core.config.args.Args; import org.tron.core.exception.TronError; +import org.tron.core.services.admin.ipc.IpcClient; @Slf4j(topic = "app") public class FullNode { @@ -33,6 +34,10 @@ public static void main(String[] args) { KeystoreFactory.start(); return; } + if (StringUtils.isNotEmpty(parameter.getIpcSocketFile())) { + IpcClient.start(parameter.getIpcSocketFile()); + return; + } if (parameter.isSolidityNode()) { logger.info("Solidity node is running."); if (StringUtils.isEmpty(parameter.getTrustNodeAddr())) { diff --git a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java index 076a8ab5387..b9e9230778f 100644 --- a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java +++ b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java @@ -280,6 +280,28 @@ public void testInitService() { Args.clearParam(); } + @Test + public void testAdminRpcAndIpcConfigBinding() { + Map override = new HashMap<>(); + override.put("storage.db.directory", "database"); + override.put("node.ipcEnable", "true"); + override.put("node.adminRpc.enable", "true"); + override.put("node.adminRpc.listenAddress", "127.0.0.2"); + override.put("node.adminRpc.port", "18575"); + Config config = ConfigFactory.parseMap(override) + .withFallback(ConfigFactory.defaultReference()); + + try { + Args.applyConfigParams(config); + Assert.assertTrue(Args.getInstance().isIpcEnable()); + Assert.assertTrue(Args.getInstance().isAdminRpcEnable()); + Assert.assertEquals("127.0.0.2", Args.getInstance().getAdminListenAddress()); + Assert.assertEquals(18575, Args.getInstance().getAdminListenPort()); + } finally { + Args.clearParam(); + } + } + /** * Verify that CLI storage parameters correctly override config file values. * diff --git a/framework/src/test/java/org/tron/core/services/admin/CommonParameterExporterTest.java b/framework/src/test/java/org/tron/core/services/admin/CommonParameterExporterTest.java new file mode 100644 index 00000000000..00c0efd1301 --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/admin/CommonParameterExporterTest.java @@ -0,0 +1,105 @@ +package org.tron.core.services.admin; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.junit.Assert; +import org.junit.Test; +import org.tron.common.logsfilter.EventPluginConfig; +import org.tron.common.parameter.CommonParameter; +import org.tron.p2p.dns.update.PublishConfig; + +public class CommonParameterExporterTest { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private final CommonParameterExporter exporter = new CommonParameterExporter(); + + @Test + public void testExportIncludesAllPublicFieldsAndLiveValues() { + CommonParameter parameter = new CommonParameter(); + parameter.rpcPort = 150051; + parameter.chainId = "runtime-chain"; + + Map snapshot = exporter.export(parameter); + + for (Field field : CommonParameter.class.getFields()) { + Assert.assertTrue("Missing runtime parameter: " + field.getName(), + snapshot.containsKey(field.getName())); + } + Assert.assertEquals(150051, snapshot.get("rpcPort")); + Assert.assertEquals("runtime-chain", snapshot.get("chainId")); + + parameter.rpcPort = 250051; + snapshot = exporter.export(parameter); + Assert.assertEquals(250051, snapshot.get("rpcPort")); + } + + @Test + public void testExportSortsTopLevelKeysByFieldName() { + Map snapshot = exporter.export(new CommonParameter()); + List actualKeys = new ArrayList<>(snapshot.keySet()); + List sortedKeys = new ArrayList<>(actualKeys); + Collections.sort(sortedKeys); + + Assert.assertEquals(sortedKeys, actualKeys); + } + + @Test + public void testSanitizeRedactsSensitiveValuesRecursively() { + ObjectNode source = OBJECT_MAPPER.createObjectNode(); + source.put("privateKey", "private-value"); + source.put("password", "password-value"); + source.put("zenTokenId", "000000"); + ObjectNode nested = source.putObject("dns"); + nested.put("accessKeyId", "access-key-value"); + nested.put("accessKeySecret", "secret-value"); + nested.put("dnsPrivate", "dns-private-value"); + nested.put("endpoint", "127.0.0.1"); + + JsonNode sanitized = exporter.sanitize(source); + + Assert.assertEquals(CommonParameterExporter.REDACTED_VALUE, + sanitized.get("privateKey").asText()); + Assert.assertEquals(CommonParameterExporter.REDACTED_VALUE, + sanitized.get("password").asText()); + Assert.assertEquals(CommonParameterExporter.REDACTED_VALUE, + sanitized.get("dns").get("accessKeyId").asText()); + Assert.assertEquals(CommonParameterExporter.REDACTED_VALUE, + sanitized.get("dns").get("accessKeySecret").asText()); + Assert.assertEquals(CommonParameterExporter.REDACTED_VALUE, + sanitized.get("dns").get("dnsPrivate").asText()); + Assert.assertEquals("000000", sanitized.get("zenTokenId").asText()); + Assert.assertEquals("127.0.0.1", sanitized.get("dns").get("endpoint").asText()); + } + + @Test + public void testExportRedactsConfiguredSecrets() { + CommonParameter parameter = new CommonParameter(); + parameter.dnsPublishConfig = new PublishConfig(); + parameter.dnsPublishConfig.setDnsPrivate("dns-private-value"); + parameter.dnsPublishConfig.setAccessKeyId("access-key-value"); + parameter.dnsPublishConfig.setAccessKeySecret("secret-value"); + parameter.dnsPublishConfig.setDnsDomain("nodes.example.org"); + parameter.eventPluginConfig = new EventPluginConfig(); + parameter.eventPluginConfig.setDbConfig("mongodb://user:password@localhost/events"); + + Map snapshot = exporter.export(parameter); + Map dnsConfig = (Map) snapshot.get("dnsPublishConfig"); + Map eventConfig = (Map) snapshot.get("eventPluginConfig"); + + Assert.assertEquals(CommonParameterExporter.REDACTED_VALUE, + dnsConfig.get("dnsPrivate")); + Assert.assertEquals(CommonParameterExporter.REDACTED_VALUE, + dnsConfig.get("accessKeyId")); + Assert.assertEquals(CommonParameterExporter.REDACTED_VALUE, + dnsConfig.get("accessKeySecret")); + Assert.assertEquals("nodes.example.org", dnsConfig.get("dnsDomain")); + Assert.assertEquals(CommonParameterExporter.REDACTED_VALUE, + eventConfig.get("dbConfig")); + } +} diff --git a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcClientTest.java b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcClientTest.java new file mode 100644 index 00000000000..2a2c6de417c --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcClientTest.java @@ -0,0 +1,99 @@ +package org.tron.core.services.admin.ipc; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import org.jline.reader.LineReader; +import org.jline.reader.UserInterruptException; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +public class IpcClientTest { + + @Test + public void testBuildHelpLinesIncludesSortedCommandParameters() { + IpcClient client = new IpcClient("unused"); + + Assert.assertEquals(Arrays.asList( + "admin_example ", + "admin_getRuntimeParameters", + "help [command]", + "exit", + "quit"), client.buildHelpLines()); + } + + @Test + public void testMissingSocketFilePrintsConsoleError() throws Exception { + Path temporaryDirectory = Files.createTempDirectory("ipc-client-test-"); + Path missingSocket = temporaryDirectory.resolve("missing.sock"); + PrintStream originalErr = System.err; + ByteArrayOutputStream errorOutput = new ByteArrayOutputStream(); + PrintStream capturedErr = new PrintStream(errorOutput, true, "UTF-8"); + try { + System.setErr(capturedErr); + new IpcClient(missingSocket.toString()).run(); + } finally { + System.setErr(originalErr); + capturedErr.close(); + Files.deleteIfExists(temporaryDirectory); + } + + Assert.assertEquals("IPC socket file does not exist: missing.sock" + System.lineSeparator(), + errorOutput.toString("UTF-8")); + } + + @Test(timeout = 10_000) + public void testSessionPrintsResponseAndExitsWhenServerDisconnects() throws Exception { + CountDownLatch inputStarted = new CountDownLatch(1); + CountDownLatch waitForInterrupt = new CountDownLatch(1); + LineReader reader = Mockito.mock(LineReader.class); + Mockito.when(reader.readLine("> ")).thenAnswer(invocation -> { + inputStarted.countDown(); + try { + waitForInterrupt.await(); + return ""; + } catch (InterruptedException e) { + throw new UserInterruptException(""); + } + }); + + try (ServerSocket serverSocket = new ServerSocket(0); + Socket clientSocket = new Socket("127.0.0.1", serverSocket.getLocalPort()); + Socket serverConnection = serverSocket.accept()) { + IpcClient client = new IpcClient("unused"); + AtomicReference failure = new AtomicReference<>(); + Thread sessionThread = new Thread(() -> { + try { + client.runSession(clientSocket, reader); + } catch (Throwable throwable) { + failure.set(throwable); + } + }, "ipc-client-test-session"); + sessionThread.setDaemon(true); + sessionThread.start(); + + Assert.assertTrue("IPC client did not start reading terminal input", + inputStarted.await(5, TimeUnit.SECONDS)); + serverConnection.getOutputStream().write("response\n".getBytes(StandardCharsets.UTF_8)); + serverConnection.getOutputStream().flush(); + Mockito.verify(reader, Mockito.timeout(5_000)).printAbove("response"); + + serverConnection.close(); + sessionThread.join(5_000); + + Assert.assertFalse("IPC client did not exit after server disconnected", + sessionThread.isAlive()); + Assert.assertNull("IPC client session failed", failure.get()); + Mockito.verify(reader).printAbove("Disconnected from server."); + } + } +} diff --git a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java new file mode 100644 index 00000000000..60405b28723 --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java @@ -0,0 +1,88 @@ +package org.tron.core.services.admin.ipc; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.Assert; +import org.junit.Test; +import org.newsclub.net.unix.AFUNIXSocket; +import org.newsclub.net.unix.AFUNIXSocketAddress; +import org.tron.common.parameter.CommonParameter; +import org.tron.core.config.args.Args; +import org.tron.core.services.admin.AdminJsonRpcImpl; +import org.tron.core.services.admin.CommonParameterExporter; + +public class IpcServiceTest { + + @Test + public void testResolveSocketFilePathUsesOutputDirectory() { + CommonParameter parameter = new CommonParameter(); + parameter.outputDirectory = "node-output"; + + Path socketFilePath = IpcService.resolveSocketFilePath(parameter, "1234"); + + Assert.assertEquals( + Paths.get("node-output", "java-tron.1234.sock"), + socketFilePath); + } + + @Test + public void testCreateParentDirectoriesWithNoParent() throws IOException { + Path socketFilePath = Paths.get("java-tron.1234.sock"); + Assert.assertNull(socketFilePath.getParent()); + + IpcService.createParentDirectories(socketFilePath); + } + + @Test(timeout = 10_000) + public void testStopClosesActiveClientSocket() throws Exception { + CommonParameter parameter = Args.getInstance(); + String originalOutputDirectory = parameter.outputDirectory; + Path outputDirectory = Files.createTempDirectory("ipc-test-"); + IpcService service = new IpcService( + new AdminJsonRpcImpl(new CommonParameterExporter())); + boolean started = false; + try { + parameter.outputDirectory = outputDirectory.toString(); + service.innerStart(); + started = true; + + File socketFile = IpcService.resolveSocketFilePath(parameter, IpcService.getPid()).toFile(); + AFUNIXSocketAddress address = AFUNIXSocketAddress.of(socketFile); + try (AFUNIXSocket client = AFUNIXSocket.newInstance()) { + client.connect(address); + client.setSoTimeout(5_000); + try (BufferedWriter writer = new BufferedWriter( + new OutputStreamWriter(client.getOutputStream(), StandardCharsets.UTF_8)); + BufferedReader reader = new BufferedReader( + new InputStreamReader(client.getInputStream(), StandardCharsets.UTF_8))) { + writer.write("{\"jsonrpc\":\"2.0\",\"method\":\"admin_example\"," + + "\"params\":[\"a\",\"b\"],\"id\":1}"); + writer.newLine(); + writer.flush(); + Assert.assertNotNull(reader.readLine()); + + long startNanos = System.nanoTime(); + service.innerStop(); + started = false; + long elapsedMillis = (System.nanoTime() - startNanos) / 1_000_000; + Assert.assertTrue("IPC service shutdown took " + elapsedMillis + " ms", + elapsedMillis < 5_000); + } + } + } finally { + if (started) { + service.innerStop(); + } + parameter.outputDirectory = originalOutputDirectory; + Files.deleteIfExists(outputDirectory); + } + } +} diff --git a/framework/src/test/java/org/tron/keystroe/CredentialsTest.java b/framework/src/test/java/org/tron/keystroe/CredentialsTest.java new file mode 100644 index 00000000000..2642129e00a --- /dev/null +++ b/framework/src/test/java/org/tron/keystroe/CredentialsTest.java @@ -0,0 +1,33 @@ +package org.tron.keystroe; + +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; +import org.tron.common.crypto.SignInterface; +import org.tron.keystore.Credentials; + +public class CredentialsTest { + + @Test + public void test_equality() { + Object aObject = new Object(); + SignInterface si = Mockito.mock(SignInterface.class); + SignInterface si2 = Mockito.mock(SignInterface.class); + SignInterface si3 = Mockito.mock(SignInterface.class); + byte[] address = "TQhZ7W1RudxFdzJMw6FvMnujPxrS6sFfmj".getBytes(); + byte[] address2 = "TNCmcTdyrYKMtmE1KU2itzeCX76jGm5Not".getBytes(); + Mockito.when(si.getAddress()).thenReturn(address); + Mockito.when(si2.getAddress()).thenReturn(address); + Mockito.when(si3.getAddress()).thenReturn(address2); + Credentials aCredential = Credentials.create(si); + Assert.assertFalse(aObject.equals(aCredential)); + Assert.assertFalse(aCredential.equals(aObject)); + Assert.assertFalse(aCredential.equals(null)); + Credentials anotherCredential = Credentials.create(si); + Assert.assertTrue(aCredential.equals(anotherCredential)); + Credentials aCredential2 = Credentials.create(si2); + Assert.assertTrue(aCredential.equals(anotherCredential)); + Credentials aCredential3 = Credentials.create(si3); + Assert.assertFalse(aCredential.equals(aCredential3)); + } +} diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 832d2728f0b..a3e1dc7d7b1 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -805,6 +805,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2292,6 +2328,14 @@ + + + + + + + + From 38fc2bdde915136fe77e44edabd74a226b4a477f Mon Sep 17 00:00:00 2001 From: 317787106 <317787106@qq.com> Date: Tue, 4 Aug 2026 17:02:28 +0800 Subject: [PATCH 02/15] support multi client --- .../java/org/tron/core/config/args/Args.java | 12 +- .../tron/core/config/args/CLIParameter.java | 4 + .../core/services/admin/ipc/IpcClient.java | 300 +++++++++++++++--- .../core/services/admin/ipc/IpcService.java | 96 ++++-- .../main/java/org/tron/program/FullNode.java | 2 +- .../org/tron/core/config/args/ArgsTest.java | 29 ++ .../services/admin/ipc/IpcClientTest.java | 137 +++++++- .../services/admin/ipc/IpcServiceTest.java | 86 +++++ 8 files changed, 605 insertions(+), 61 deletions(-) diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index 65e3b6653b0..2902d4924c0 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -9,6 +9,7 @@ import com.beust.jcommander.JCommander; import com.beust.jcommander.ParameterDescription; +import com.beust.jcommander.ParameterException; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.typesafe.config.Config; @@ -104,6 +105,9 @@ public class Args extends CommonParameter { @Getter private static String configFilePath = ""; + @Getter + private static String ipcExecCommand; + // Singleton config beans — populated at startup, read-only after init. // New code can read directly from these beans instead of CommonParameter. @Getter @@ -159,6 +163,10 @@ public static void setParam(final String[] args, final String confFileName) { Args.printHelp(jc); exit(0); } + ipcExecCommand = cmd.ipcExecCommand; + if (ipcExecCommand != null && StringUtils.isEmpty(cmd.ipcSocketFile)) { + throw new ParameterException("--exec requires --attach "); + } // Resolve config file path configFilePath = StringUtils.isNoneBlank(cmd.shellConfFileName) @@ -956,6 +964,7 @@ public static void clearParam() { rateLimiterConfig = null; metricsConfig = null; eventConfig = null; + ipcExecCommand = null; } // getProposalExpirationTime removed — logic moved to BlockConfig.fromConfig() @@ -1302,7 +1311,8 @@ private static String getCommitIdAbbrev() { private static Map getOptionGroup() { String[] tronOption = new String[] {"version", "help", "shellConfFileName", "logbackPath", - "eventSubscribe", "solidityNode", "keystoreFactory", "ipcSocketFile"}; + "eventSubscribe", "solidityNode", "keystoreFactory", "ipcSocketFile", + "ipcExecCommand"}; String[] dbOption = new String[] {"outputDirectory"}; String[] witnessOption = new String[] {"witness", "privateKey"}; String[] vmOption = new String[] {"debug"}; diff --git a/framework/src/main/java/org/tron/core/config/args/CLIParameter.java b/framework/src/main/java/org/tron/core/config/args/CLIParameter.java index d1f0bfec909..441248ef3b9 100644 --- a/framework/src/main/java/org/tron/core/config/args/CLIParameter.java +++ b/framework/src/main/java/org/tron/core/config/args/CLIParameter.java @@ -57,6 +57,10 @@ public class CLIParameter { description = "running an IPC client to interact with FullNode") public String ipcSocketFile; + @Parameter(names = {"--exec"}, + description = "execute one Admin IPC command and exit (requires --attach)") + public String ipcExecCommand; + @Deprecated @Parameter(names = {"--fast-forward"}) public boolean fastForward; diff --git a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcClient.java b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcClient.java index b16bd6cdfe0..a0e898ce9eb 100644 --- a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcClient.java +++ b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcClient.java @@ -1,6 +1,8 @@ package org.tron.core.services.admin.ipc; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.googlecode.jsonrpc4j.JsonRpcMethod; import com.googlecode.jsonrpc4j.JsonRpcParam; @@ -12,10 +14,10 @@ import java.io.OutputStreamWriter; import java.lang.annotation.Annotation; import java.lang.reflect.Method; +import java.lang.reflect.Type; import java.net.Socket; import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; @@ -29,6 +31,9 @@ import org.jline.reader.EndOfFileException; import org.jline.reader.LineReader; import org.jline.reader.LineReaderBuilder; +import org.jline.reader.ParsedLine; +import org.jline.reader.Parser; +import org.jline.reader.SyntaxError; import org.jline.reader.UserInterruptException; import org.jline.reader.impl.DefaultParser; import org.jline.reader.impl.completer.ArgumentCompleter; @@ -38,6 +43,7 @@ import org.newsclub.net.unix.AFUNIXSocket; import org.newsclub.net.unix.AFUNIXSocketAddress; import org.tron.core.services.admin.AdminJsonRpc; +import org.tron.program.Version; @Slf4j(topic = "API") public class IpcClient { @@ -47,18 +53,25 @@ public class IpcClient { private final String socketFilePath; private final Map commandLowerMap; private final Map> commandParameters; + private final Map> commandParameterTypes; + private final DefaultParser commandParser = new DefaultParser().eofOnUnclosedQuote(true); private int requestId = 0; public IpcClient(String socketFilePath) { this.socketFilePath = socketFilePath; this.commandLowerMap = collectAdminCommands(); this.commandParameters = collectAdminCommandParams(); + this.commandParameterTypes = collectAdminCommandParamTypes(); } public static void start(String socketFilePath) { + start(socketFilePath, null); + } + + public static void start(String socketFilePath, String execCommand) { IpcClient ipcClient = new IpcClient(socketFilePath); try { - ipcClient.run(); + ipcClient.run(execCommand); } catch (IOException e) { logger.error("", e); } @@ -102,6 +115,22 @@ private Map> collectAdminCommandParams() { return commandParameters; } + private Map> collectAdminCommandParamTypes() { + Map> parameterTypes = new HashMap<>(); + for (Method method : AdminJsonRpc.class.getDeclaredMethods()) { + JsonRpcMethod rpcMethod = method.getAnnotation(JsonRpcMethod.class); + if (rpcMethod == null || rpcMethod.value() == null) { + continue; + } + List types = new ArrayList<>(); + for (Type type : method.getGenericParameterTypes()) { + types.add(OBJECT_MAPPER.getTypeFactory().constructType(type)); + } + parameterTypes.put(rpcMethod.value().toLowerCase(Locale.ROOT), types); + } + return parameterTypes; + } + private void printHelp() { System.out.println("Available commands:"); for (String usage : buildHelpLines()) { @@ -123,14 +152,24 @@ List buildHelpLines() { } private String formatUsage(String command) { - List parameters = commandParameters.get(command.toLowerCase(Locale.ROOT)); + String commandLowerCase = command.toLowerCase(Locale.ROOT); + List parameters = commandParameters.get(commandLowerCase); if (parameters == null || parameters.isEmpty()) { return command; } - return command + " <" + StringUtils.join(parameters, "> <") + ">"; + List parameterTypes = commandParameterTypes.get(commandLowerCase); + List typedParameters = new ArrayList<>(); + for (int i = 0; i < parameters.size(); i++) { + typedParameters.add(parameters.get(i) + ":" + formatType(parameterTypes.get(i))); + } + return command + " <" + StringUtils.join(typedParameters, "> <") + ">"; } public void run() throws IOException { + run(null); + } + + void run(String execCommand) throws IOException { File socketFile = new File(socketFilePath); if (!socketFile.exists()) { System.err.println("IPC socket file does not exist: " + socketFile.getName()); @@ -139,7 +178,11 @@ public void run() throws IOException { AFUNIXSocketAddress address = AFUNIXSocketAddress.of(socketFile); try (Socket socket = AFUNIXSocket.newInstance()) { socket.connect(address); - System.out.println("Connected to server: " + socketFile.getAbsolutePath()); + if (execCommand != null) { + runExec(socket, execCommand); + return; + } + printWelcome(socketFile); try (Terminal terminal = TerminalBuilder.builder().system(true).build()) { LineReader reader = createLineReader(terminal); runSession(socket, reader); @@ -147,10 +190,56 @@ public void run() throws IOException { } } + void runExec(Socket socket, String commandLine) throws IOException { + List commandWords; + try { + commandWords = parseCommandLine(commandLine); + } catch (SyntaxError e) { + System.err.println("Invalid command syntax."); + return; + } + if (commandWords.isEmpty()) { + System.err.println("No command specified for --exec."); + return; + } + if (isExitCommand(commandWords.get(0))) { + return; + } + + String request; + try { + request = buildRequest(commandWords); + } catch (IllegalArgumentException e) { + System.err.println(e.getMessage()); + return; + } + if (request == null) { + return; + } + + try (BufferedWriter serverWriter = new BufferedWriter( + new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8)); + BufferedReader serverReader = new BufferedReader( + new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8))) { + serverWriter.write(request); + serverWriter.newLine(); + serverWriter.flush(); + + String response; + do { + response = serverReader.readLine(); + } while (response != null && response.trim().isEmpty()); + if (response == null) { + System.err.println("Disconnected from server before receiving a response."); + return; + } + System.out.println(formatResponse(response)); + } + } + void runSession(Socket socket, LineReader reader) throws IOException { AtomicBoolean connected = new AtomicBoolean(true); outputResponse(socket, reader, connected, Thread.currentThread()); - printHelp(); try { inputRequest(socket, reader, connected); } finally { @@ -170,7 +259,9 @@ private void outputResponse(final Socket socket, LineReader reader, AtomicBoolea try { String response; while ((response = serverReader.readLine()) != null) { - reader.printAbove(response); + if (!response.trim().isEmpty()) { + reader.printAbove(formatResponse(response)); + } } } catch (IOException e) { logger.debug("IPC response stream closed: {}", e.getMessage()); @@ -193,13 +284,22 @@ private LineReader createLineReader(Terminal terminal) { return LineReaderBuilder.builder() .terminal(terminal) .completer(completer) - .parser(new DefaultParser()) + .parser(commandParser) .variable(LineReader.INDENTATION, 2) .option(LineReader.Option.AUTO_FRESH_LINE, true) .option(LineReader.Option.CASE_INSENSITIVE, true) + .option(LineReader.Option.HISTORY_IGNORE_DUPS, true) + .option(LineReader.Option.HISTORY_REDUCE_BLANKS, true) .build(); } + void printWelcome(File socketFile) { + System.out.println("Welcome to the java-tron admin console."); + System.out.println("Client: java-tron/" + Version.getVersion()); + System.out.println("IPC endpoint: " + socketFile.getAbsolutePath()); + System.out.println("Type \"help\" for available commands; \"exit\" or Ctrl-D to quit."); + } + /** * read from System.in and send command to IPC server */ @@ -210,41 +310,18 @@ private void inputRequest(final Socket socket, LineReader reader, AtomicBoolean new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8))) { while (connected.get()) { try { - String cmdLine = reader.readLine(prompt).trim(); - String[] cmdArray = cmdLine.split("\\s+"); - // split on trim() string will always return at the minimum: [""] - String cmd = cmdArray[0]; - if ("".equals(cmd)) { + List commandWords = parseCommandLine(reader.readLine(prompt)); + if (commandWords.isEmpty()) { continue; } - String cmdLowerCase = cmd.toLowerCase(Locale.ROOT); - - if ("help".equals(cmdLowerCase)) { - if (cmdArray.length == 2 - && commandLowerMap.containsKey(cmdArray[1].toLowerCase(Locale.ROOT))) { - String rpcMethod = cmdArray[1].toLowerCase(Locale.ROOT); - System.out.println("usage: " + formatUsage(commandLowerMap.get(rpcMethod))); - } else { - printHelp(); - } - continue; - } else if ("exit".equals(cmdLowerCase) || "quit".equals(cmdLowerCase)) { + if (isExitCommand(commandWords.get(0))) { break; - } else if (!commandLowerMap.containsKey(cmdLowerCase)) { - System.err.println("Invalid cmd: " + cmd); - printHelp(); - continue; - } else if (cmdArray.length - 1 != commandParameters.get(cmdLowerCase).size()) { - System.err.println("Invalid parameter, usage: " - + formatUsage(commandLowerMap.get(cmdLowerCase))); - continue; } - List values = - new ArrayList<>(Arrays.asList(cmdArray).subList(1, cmdArray.length)); - - String request = buildJsonWithParameter(commandLowerMap.get(cmdLowerCase), values); - System.out.println("Sending request: " + request); + String request = buildRequest(commandWords); + if (request == null) { + continue; + } serverWriter.write(request); serverWriter.newLine(); serverWriter.flush(); @@ -256,6 +333,10 @@ private void inputRequest(final Socket socket, LineReader reader, AtomicBoolean break; } catch (JsonProcessingException e) { logger.error("Failed to build IPC request", e); + } catch (SyntaxError e) { + System.err.println("Invalid command syntax."); + } catch (IllegalArgumentException e) { + System.err.println(e.getMessage()); } catch (IOException e) { notifyDisconnected(connected, reader); break; @@ -274,7 +355,148 @@ private void notifyDisconnected(AtomicBoolean connected, LineReader reader) { } } - private String buildJsonWithParameter(String cmd, List values) + List parseCommandLine(String commandLine) { + if (commandLine == null || commandLine.trim().isEmpty()) { + return Collections.emptyList(); + } + ParsedLine parsedLine = commandParser.parse( + commandLine, commandLine.length(), Parser.ParseContext.ACCEPT_LINE); + return parsedLine.words(); + } + + private boolean isExitCommand(String command) { + return "exit".equalsIgnoreCase(command) || "quit".equalsIgnoreCase(command); + } + + private String buildRequest(List commandWords) throws JsonProcessingException { + String command = commandWords.get(0); + String commandLowerCase = command.toLowerCase(Locale.ROOT); + if ("help".equals(commandLowerCase)) { + if (commandWords.size() == 2 + && commandLowerMap.containsKey(commandWords.get(1).toLowerCase(Locale.ROOT))) { + String rpcMethod = commandWords.get(1).toLowerCase(Locale.ROOT); + System.out.println("usage: " + formatUsage(commandLowerMap.get(rpcMethod))); + } else { + printHelp(); + } + return null; + } + if (!commandLowerMap.containsKey(commandLowerCase)) { + System.err.println("Invalid cmd: " + command); + printHelp(); + return null; + } + if (commandWords.size() - 1 != commandParameters.get(commandLowerCase).size()) { + System.err.println("Invalid parameter, usage: " + + formatUsage(commandLowerMap.get(commandLowerCase))); + return null; + } + + List rawValues = new ArrayList<>( + commandWords.subList(1, commandWords.size())); + List values = convertArguments(commandLowerCase, rawValues); + return buildJsonWithParameter(commandLowerMap.get(commandLowerCase), values); + } + + private List convertArguments(String command, List values) { + List convertedValues = new ArrayList<>(); + List parameterTypes = commandParameterTypes.get(command); + List parameterNames = commandParameters.get(command); + for (int i = 0; i < values.size(); i++) { + convertedValues.add(convertArgument(values.get(i), parameterTypes.get(i), + parameterNames.get(i))); + } + return convertedValues; + } + + Object convertArgument(String value, JavaType targetType, String parameterName) { + Class rawClass = targetType.getRawClass(); + if (String.class.equals(rawClass) || CharSequence.class.equals(rawClass)) { + return value; + } + if (Character.class.equals(rawClass) || Character.TYPE.equals(rawClass)) { + if (value.length() == 1) { + return value.charAt(0); + } + throw invalidParameterType(parameterName, targetType, null); + } + if (rawClass.isPrimitive() && "null".equals(value.trim())) { + throw invalidParameterType(parameterName, targetType, null); + } + Object convertedValue; + try { + if (rawClass.isEnum()) { + convertedValue = OBJECT_MAPPER.convertValue(value, targetType); + } else { + convertedValue = OBJECT_MAPPER.readValue(value, targetType); + } + } catch (JsonProcessingException | IllegalArgumentException e) { + throw invalidParameterType(parameterName, targetType, e); + } + if (convertedValue == null && rawClass.isPrimitive()) { + throw invalidParameterType(parameterName, targetType, null); + } + return convertedValue; + } + + private String formatType(JavaType type) { + Class rawClass = type.getRawClass(); + if (String.class.equals(rawClass) || CharSequence.class.equals(rawClass)) { + return "string"; + } + if (Boolean.class.equals(rawClass) || Boolean.TYPE.equals(rawClass)) { + return "boolean"; + } + if (Number.class.isAssignableFrom(rawClass) || rawClass.isPrimitive()) { + return rawClass.getSimpleName().toLowerCase(Locale.ROOT); + } + if (rawClass.isArray() || java.util.Collection.class.isAssignableFrom(rawClass)) { + return "array"; + } + if (java.util.Map.class.isAssignableFrom(rawClass)) { + return "object"; + } + return rawClass.getSimpleName(); + } + + private IllegalArgumentException invalidParameterType(String parameterName, JavaType targetType, + Throwable cause) { + return new IllegalArgumentException( + "Invalid value for <" + parameterName + ">; expected " + targetType.toCanonical(), cause); + } + + String formatResponse(String response) { + try { + JsonNode root = OBJECT_MAPPER.readTree(response); + if (root == null || root.isMissingNode()) { + return response; + } + JsonNode error = root.get("error"); + if (error != null && !error.isNull()) { + String code = error.has("code") ? " " + error.get("code").asText() : ""; + String message = error.has("message") ? error.get("message").asText() : "Unknown error"; + return "Error" + code + ": " + message; + } + if (root.has("result")) { + return formatJsonValue(root.get("result")); + } + return formatJsonValue(root); + } catch (JsonProcessingException e) { + return response; + } + } + + private String formatJsonValue(JsonNode value) throws JsonProcessingException { + if (value == null || value.isNull()) { + return "null"; + } + if (value.isTextual()) { + return value.asText(); + } + return OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(value); + } + + private String buildJsonWithParameter(String cmd, List values) throws JsonProcessingException { Map params = new LinkedHashMap<>(); params.put("jsonrpc", "2.0"); diff --git a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java index 54afe1883df..1d18a20d36d 100644 --- a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java +++ b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java @@ -15,7 +15,14 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.attribute.PosixFilePermission; +import java.util.EnumSet; +import java.util.Set; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; import lombok.extern.slf4j.Slf4j; import org.newsclub.net.unix.AFUNIXServerSocket; import org.newsclub.net.unix.AFUNIXSocket; @@ -32,11 +39,20 @@ @Slf4j(topic = "API") public class IpcService extends AbstractService { - private final String esName = "admin-ipc-server"; - private final ExecutorService pool = ExecutorServiceManager.newSingleThreadExecutor(esName, true); + private static final String ACCEPTOR_EXECUTOR_NAME = "admin-ipc-acceptor"; + private static final String CLIENT_EXECUTOR_NAME = "admin-ipc-client"; + private static final int CLIENT_HANDLER_THREADS = 4; + private static final int MAX_PENDING_CLIENTS = 16; + + private final ExecutorService acceptorExecutor = + ExecutorServiceManager.newSingleThreadExecutor(ACCEPTOR_EXECUTOR_NAME, true); + private final ExecutorService clientExecutor = + ExecutorServiceManager.newThreadPoolExecutor( + CLIENT_HANDLER_THREADS, CLIENT_HANDLER_THREADS, 0L, TimeUnit.MILLISECONDS, + new ArrayBlockingQueue<>(MAX_PENDING_CLIENTS), CLIENT_EXECUTOR_NAME, true); + private final Set activeClientSockets = ConcurrentHashMap.newKeySet(); private volatile boolean isRunning = true; private AFUNIXServerSocket unixServerSocket; - private volatile AFUNIXSocket activeClientSocket; private Path socketFilePath; private final JsonRpcServer jsonRpcServer; @@ -57,18 +73,28 @@ public void innerStart() throws Exception { AFUNIXSocketAddress address = AFUNIXSocketAddress.of(socketFile); unixServerSocket = AFUNIXServerSocket.bindOn(address); + try { + setOwnerOnlyPermissions(socketFilePath); + } catch (IOException | RuntimeException e) { + try { + unixServerSocket.close(); + } catch (IOException closeException) { + e.addSuppressed(closeException); + } + try { + Files.deleteIfExists(socketFilePath); + } catch (IOException deleteException) { + e.addSuppressed(deleteException); + } + throw e; + } unixServerSocket.setShutdownOnClose(true); logger.info("IpcService started, listening on {}", socketFile.getAbsolutePath()); Runnable runnable = () -> { while (isRunning) { - AFUNIXSocket client = null; try { - client = unixServerSocket.accept(); - activeClientSocket = client; - if (isRunning) { - handleClient(client); - } + registerClient(unixServerSocket.accept()); } catch (Throwable throwable) { if (isRunning) { logger.error("Handle IPC request error", throwable); @@ -76,13 +102,35 @@ public void innerStart() throws Exception { ExitManager.findTronError(throwable).ifPresent(e -> { throw e; }); - } finally { - closeClientSocket(client); - activeClientSocket = null; } } }; - ExecutorServiceManager.submit(pool, runnable); + ExecutorServiceManager.submit(acceptorExecutor, runnable); + } + + private void registerClient(AFUNIXSocket client) { + activeClientSockets.add(client); + if (!isRunning) { + closeAndRemoveClient(client); + return; + } + try { + ExecutorServiceManager.submit(clientExecutor, () -> { + try { + handleClient(client); + } finally { + closeAndRemoveClient(client); + } + }); + } catch (RejectedExecutionException e) { + closeAndRemoveClient(client); + if (isRunning) { + logger.warn("Too many IPC clients; rejecting connection"); + } + } catch (RuntimeException e) { + closeAndRemoveClient(client); + throw e; + } } private void handleClient(AFUNIXSocket client) { @@ -94,9 +142,8 @@ private void handleClient(AFUNIXSocket client) { String line; while ((line = reader.readLine()) != null) { String cmd = line.trim(); - logger.info("Server received: {}", cmd); + logger.debug("Server received: {}", cmd); writer.write(handleCommand(cmd)); - writer.newLine(); writer.flush(); } } catch (IOException e) { @@ -119,7 +166,7 @@ private String handleCommand(String jsonRequest) { response = e.getMessage(); } - logger.info("IPC response: {}", response); + logger.debug("IPC response: {}", response); return response; } @@ -127,11 +174,14 @@ private String handleCommand(String jsonRequest) { public void innerStop() throws Exception { logger.info("Begin to stop IpcService ..."); isRunning = false; - closeClientSocket(activeClientSocket); if (unixServerSocket != null) { unixServerSocket.close(); } - ExecutorServiceManager.shutdownAndAwaitTermination(pool, esName); + for (AFUNIXSocket client : activeClientSockets) { + closeClientSocket(client); + } + ExecutorServiceManager.shutdownAndAwaitTermination(acceptorExecutor, ACCEPTOR_EXECUTOR_NAME); + ExecutorServiceManager.shutdownAndAwaitTermination(clientExecutor, CLIENT_EXECUTOR_NAME); if (socketFilePath != null) { Files.deleteIfExists(socketFilePath); } @@ -149,6 +199,11 @@ private void closeClientSocket(AFUNIXSocket client) { } } + private void closeAndRemoveClient(AFUNIXSocket client) { + closeClientSocket(client); + activeClientSockets.remove(client); + } + static Path resolveSocketFilePath(CommonParameter parameter, String pid) { return Paths.get(parameter.getOutputDirectory(), "java-tron." + pid + ".sock"); @@ -161,6 +216,11 @@ static void createParentDirectories(Path socketFilePath) throws IOException { } } + static void setOwnerOnlyPermissions(Path socketFilePath) throws IOException { + Files.setPosixFilePermissions(socketFilePath, + EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE)); + } + public static String getPid() { String name = ManagementFactory.getRuntimeMXBean().getName(); return name.split("@")[0]; diff --git a/framework/src/main/java/org/tron/program/FullNode.java b/framework/src/main/java/org/tron/program/FullNode.java index fec66d2863e..599d04bc9ce 100644 --- a/framework/src/main/java/org/tron/program/FullNode.java +++ b/framework/src/main/java/org/tron/program/FullNode.java @@ -35,7 +35,7 @@ public static void main(String[] args) { return; } if (StringUtils.isNotEmpty(parameter.getIpcSocketFile())) { - IpcClient.start(parameter.getIpcSocketFile()); + IpcClient.start(parameter.getIpcSocketFile(), Args.getIpcExecCommand()); return; } if (parameter.isSolidityNode()) { diff --git a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java index 0b2f28d4e88..db4028e6b5f 100644 --- a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java +++ b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java @@ -15,6 +15,7 @@ package org.tron.core.config.args; +import com.beust.jcommander.ParameterException; import com.google.common.collect.Lists; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; @@ -48,6 +49,34 @@ public class ArgsTest { @Rule public ExpectedException thrown = ExpectedException.none(); + @Test + public void testAttachWithExecParameters() { + try { + Args.setParam(new String[] { + "--attach", "/tmp/java-tron.sock", + "--exec", "admin_example one two" + }, TestConstants.TEST_CONF); + + Assert.assertEquals("/tmp/java-tron.sock", Args.getInstance().getIpcSocketFile()); + Assert.assertEquals("admin_example one two", Args.getIpcExecCommand()); + } finally { + Args.clearParam(); + } + } + + @Test + public void testExecRequiresAttach() { + try { + Args.setParam(new String[] {"--exec", "admin_getRuntimeParameters"}, + TestConstants.TEST_CONF); + Assert.fail("Expected --exec without --attach to fail"); + } catch (ParameterException e) { + Assert.assertEquals("--exec requires --attach ", e.getMessage()); + } finally { + Args.clearParam(); + } + } + @Test public void get() { Args.setParam(new String[] {"--keystore-factory"}, TestConstants.TEST_CONF); diff --git a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcClientTest.java b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcClientTest.java index 2a2c6de417c..d4ab63fbab9 100644 --- a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcClientTest.java +++ b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcClientTest.java @@ -1,6 +1,12 @@ package org.tron.core.services.admin.ipc; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.type.TypeFactory; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.PrintStream; import java.net.ServerSocket; import java.net.Socket; @@ -19,18 +25,143 @@ public class IpcClientTest { + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + @Test public void testBuildHelpLinesIncludesSortedCommandParameters() { IpcClient client = new IpcClient("unused"); Assert.assertEquals(Arrays.asList( - "admin_example ", + "admin_example ", "admin_getRuntimeParameters", "help [command]", "exit", "quit"), client.buildHelpLines()); } + @Test + public void testParseCommandLinePreservesQuotedArguments() { + IpcClient client = new IpcClient("unused"); + + Assert.assertEquals(Arrays.asList("admin_example", "hello world", "second value"), + client.parseCommandLine("admin_example \"hello world\" 'second value'")); + } + + @Test + public void testConvertTypedArguments() { + IpcClient client = new IpcClient("unused"); + TypeFactory typeFactory = TypeFactory.defaultInstance(); + + Assert.assertEquals(42, client.convertArgument("42", + typeFactory.constructType(Integer.TYPE), "number")); + Assert.assertEquals(true, client.convertArgument("true", + typeFactory.constructType(Boolean.TYPE), "enabled")); + JavaType listType = typeFactory.constructCollectionType(java.util.List.class, Integer.class); + Assert.assertEquals(Arrays.asList(1, 2), + client.convertArgument("[1,2]", listType, "numbers")); + + try { + client.convertArgument("null", typeFactory.constructType(Integer.TYPE), "number"); + Assert.fail("Expected null to be rejected for a primitive parameter"); + } catch (IllegalArgumentException e) { + Assert.assertEquals("Invalid value for ; expected int", e.getMessage()); + } + + try { + client.convertArgument("sensitive-value", typeFactory.constructType(Integer.TYPE), "number"); + Assert.fail("Expected an invalid typed parameter"); + } catch (IllegalArgumentException e) { + Assert.assertEquals("Invalid value for ; expected int", e.getMessage()); + Assert.assertFalse(e.getMessage().contains("sensitive-value")); + } + } + + @Test + public void testFormatResponseShowsResultOrStructuredError() { + IpcClient client = new IpcClient("unused"); + + Assert.assertEquals("done", client.formatResponse( + "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"done\"}")); + String formattedObject = client.formatResponse( + "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"height\":10,\"ready\":true}}"); + Assert.assertTrue(formattedObject, formattedObject.contains(System.lineSeparator())); + Assert.assertTrue(formattedObject, formattedObject.contains("\"height\" : 10")); + Assert.assertEquals("Error -32602: Invalid params", client.formatResponse( + "{\"jsonrpc\":\"2.0\",\"id\":1," + + "\"error\":{\"code\":-32602,\"message\":\"Invalid params\"}}")); + Assert.assertEquals("", client.formatResponse("")); + } + + @Test + public void testExecSendsCommandAndPrintsFormattedResult() throws Exception { + Socket socket = Mockito.mock(Socket.class); + ByteArrayOutputStream requestOutput = new ByteArrayOutputStream(); + Mockito.when(socket.getOutputStream()).thenReturn(requestOutput); + Mockito.when(socket.getInputStream()).thenReturn(new ByteArrayInputStream( + "\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"hello world:b\"}\n" + .getBytes(StandardCharsets.UTF_8))); + + PrintStream originalOut = System.out; + ByteArrayOutputStream consoleOutput = new ByteArrayOutputStream(); + PrintStream capturedOut = new PrintStream(consoleOutput, true, "UTF-8"); + try { + System.setOut(capturedOut); + new IpcClient("unused").runExec(socket, "admin_example \"hello world\" b"); + } finally { + System.setOut(originalOut); + capturedOut.close(); + } + + JsonNode request = OBJECT_MAPPER.readTree(requestOutput.toString("UTF-8")); + Assert.assertEquals("admin_example", request.get("method").asText()); + Assert.assertEquals("hello world", request.get("params").get(0).asText()); + Assert.assertEquals("b", request.get("params").get(1).asText()); + Assert.assertEquals("hello world:b" + System.lineSeparator(), + consoleOutput.toString("UTF-8")); + } + + @Test + public void testWelcomeShowsConnectionAndUsageHint() throws Exception { + Path temporaryDirectory = Files.createTempDirectory("ipc-welcome-test-"); + File socketFile = temporaryDirectory.resolve("java-tron.1234.sock").toFile(); + PrintStream originalOut = System.out; + ByteArrayOutputStream consoleOutput = new ByteArrayOutputStream(); + PrintStream capturedOut = new PrintStream(consoleOutput, true, "UTF-8"); + try { + System.setOut(capturedOut); + new IpcClient(socketFile.getPath()).printWelcome(socketFile); + } finally { + System.setOut(originalOut); + capturedOut.close(); + Files.deleteIfExists(temporaryDirectory); + } + + String welcome = consoleOutput.toString("UTF-8"); + Assert.assertTrue(welcome, welcome.contains("Welcome to the java-tron admin console.")); + Assert.assertTrue(welcome, welcome.contains("IPC endpoint: " + socketFile.getAbsolutePath())); + Assert.assertFalse(welcome, welcome.contains("History:")); + Assert.assertTrue(welcome, welcome.contains("Type \"help\" for available commands")); + } + + @Test + public void testExecWithInvalidSyntaxDoesNotExposeCommand() throws Exception { + PrintStream originalErr = System.err; + ByteArrayOutputStream errorOutput = new ByteArrayOutputStream(); + PrintStream capturedErr = new PrintStream(errorOutput, true, "UTF-8"); + try { + System.setErr(capturedErr); + new IpcClient("unused").runExec(Mockito.mock(Socket.class), + "admin_example \"sensitive-value"); + } finally { + System.setErr(originalErr); + capturedErr.close(); + } + + Assert.assertEquals("Invalid command syntax." + System.lineSeparator(), + errorOutput.toString("UTF-8")); + Assert.assertFalse(errorOutput.toString("UTF-8").contains("sensitive-value")); + } + @Test public void testMissingSocketFilePrintsConsoleError() throws Exception { Path temporaryDirectory = Files.createTempDirectory("ipc-client-test-"); @@ -83,7 +214,7 @@ public void testSessionPrintsResponseAndExitsWhenServerDisconnects() throws Exce Assert.assertTrue("IPC client did not start reading terminal input", inputStarted.await(5, TimeUnit.SECONDS)); - serverConnection.getOutputStream().write("response\n".getBytes(StandardCharsets.UTF_8)); + serverConnection.getOutputStream().write("\nresponse\n\n".getBytes(StandardCharsets.UTF_8)); serverConnection.getOutputStream().flush(); Mockito.verify(reader, Mockito.timeout(5_000)).printAbove("response"); @@ -93,6 +224,8 @@ public void testSessionPrintsResponseAndExitsWhenServerDisconnects() throws Exce Assert.assertFalse("IPC client did not exit after server disconnected", sessionThread.isAlive()); Assert.assertNull("IPC client session failed", failure.get()); + Mockito.verify(reader, Mockito.never()).printAbove("null"); + Mockito.verify(reader, Mockito.never()).printAbove(""); Mockito.verify(reader).printAbove("Disconnected from server."); } } diff --git a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java index 60405b28723..994dffff3a0 100644 --- a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java @@ -10,6 +10,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.attribute.PosixFilePermission; +import java.util.EnumSet; import org.junit.Assert; import org.junit.Test; import org.newsclub.net.unix.AFUNIXSocket; @@ -41,6 +43,75 @@ public void testCreateParentDirectoriesWithNoParent() throws IOException { IpcService.createParentDirectories(socketFilePath); } + @Test(timeout = 10_000) + public void testSocketFileUsesOwnerOnlyPermissions() throws Exception { + CommonParameter parameter = Args.getInstance(); + String originalOutputDirectory = parameter.outputDirectory; + Path outputDirectory = Files.createTempDirectory(Paths.get("/tmp"), "ipc-permission-test-"); + IpcService service = new IpcService( + new AdminJsonRpcImpl(new CommonParameterExporter())); + boolean started = false; + try { + parameter.outputDirectory = outputDirectory.toString(); + service.innerStart(); + started = true; + + Path socketFile = IpcService.resolveSocketFilePath(parameter, IpcService.getPid()); + Assert.assertEquals( + EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE), + Files.getPosixFilePermissions(socketFile)); + } finally { + if (started) { + service.innerStop(); + } + parameter.outputDirectory = originalOutputDirectory; + Files.deleteIfExists(outputDirectory); + } + } + + @Test(timeout = 10_000) + public void testHandlesMultipleClientsConcurrently() throws Exception { + CommonParameter parameter = Args.getInstance(); + String originalOutputDirectory = parameter.outputDirectory; + Path outputDirectory = Files.createTempDirectory(Paths.get("/tmp"), "ipc-multi-client-test-"); + IpcService service = new IpcService( + new AdminJsonRpcImpl(new CommonParameterExporter())); + boolean started = false; + try { + parameter.outputDirectory = outputDirectory.toString(); + service.innerStart(); + started = true; + + File socketFile = IpcService.resolveSocketFilePath(parameter, IpcService.getPid()).toFile(); + AFUNIXSocketAddress address = AFUNIXSocketAddress.of(socketFile); + try (AFUNIXSocket firstClient = AFUNIXSocket.newInstance(); + AFUNIXSocket secondClient = AFUNIXSocket.newInstance()) { + firstClient.connect(address); + firstClient.setSoTimeout(5_000); + BufferedWriter firstWriter = new BufferedWriter( + new OutputStreamWriter(firstClient.getOutputStream(), StandardCharsets.UTF_8)); + BufferedReader firstReader = new BufferedReader( + new InputStreamReader(firstClient.getInputStream(), StandardCharsets.UTF_8)); + assertSuccessfulResponse(sendRequest(firstWriter, firstReader, 1), 1); + assertSuccessfulResponse(sendRequest(firstWriter, firstReader, 2), 2); + + secondClient.connect(address); + secondClient.setSoTimeout(5_000); + BufferedWriter secondWriter = new BufferedWriter( + new OutputStreamWriter(secondClient.getOutputStream(), StandardCharsets.UTF_8)); + BufferedReader secondReader = new BufferedReader( + new InputStreamReader(secondClient.getInputStream(), StandardCharsets.UTF_8)); + assertSuccessfulResponse(sendRequest(secondWriter, secondReader, 3), 3); + } + } finally { + if (started) { + service.innerStop(); + } + parameter.outputDirectory = originalOutputDirectory; + Files.deleteIfExists(outputDirectory); + } + } + @Test(timeout = 10_000) public void testStopClosesActiveClientSocket() throws Exception { CommonParameter parameter = Args.getInstance(); @@ -85,4 +156,19 @@ public void testStopClosesActiveClientSocket() throws Exception { Files.deleteIfExists(outputDirectory); } } + + private String sendRequest(BufferedWriter writer, BufferedReader reader, int requestId) + throws IOException { + writer.write("{\"jsonrpc\":\"2.0\",\"method\":\"admin_example\"," + + "\"params\":[\"a\",\"b\"],\"id\":" + requestId + "}"); + writer.newLine(); + writer.flush(); + return reader.readLine(); + } + + private void assertSuccessfulResponse(String response, int requestId) { + Assert.assertNotNull(response); + Assert.assertTrue(response, response.contains("\"result\":\"a:b\"")); + Assert.assertTrue(response, response.contains("\"id\":" + requestId)); + } } From cf7307c4007df43ac00a3af501e4f6d2d6f1a6cf Mon Sep 17 00:00:00 2001 From: 317787106 <317787106@qq.com> Date: Thu, 6 Aug 2026 15:29:16 +0800 Subject: [PATCH 03/15] log warn if admin listen address not restirct on 127.0.0.1 --- .../java/org/tron/core/config/args/NodeConfig.java | 3 ++- .../core/services/admin/http/AdminRpcHttpService.java | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/org/tron/core/config/args/NodeConfig.java b/common/src/main/java/org/tron/core/config/args/NodeConfig.java index 023cd3afc47..89d6ef1d359 100644 --- a/common/src/main/java/org/tron/core/config/args/NodeConfig.java +++ b/common/src/main/java/org/tron/core/config/args/NodeConfig.java @@ -11,6 +11,7 @@ import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import org.tron.core.Constant; import org.tron.core.exception.TronError; // Node configuration bean for the "node" section of config.conf. @@ -259,7 +260,7 @@ public static class JsonRpcConfig { public static class AdminRpcConfig { private boolean enable = false; - private String listenAddress = "127.0.0.1"; + private String listenAddress = Constant.LOCAL_HOST; private int port = 8575; } diff --git a/framework/src/main/java/org/tron/core/services/admin/http/AdminRpcHttpService.java b/framework/src/main/java/org/tron/core/services/admin/http/AdminRpcHttpService.java index 04c6db8fefc..e9c52ad7be7 100644 --- a/framework/src/main/java/org/tron/core/services/admin/http/AdminRpcHttpService.java +++ b/framework/src/main/java/org/tron/core/services/admin/http/AdminRpcHttpService.java @@ -10,6 +10,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.tron.common.application.HttpService; +import org.tron.core.Constant; import org.tron.core.config.args.Args; import org.tron.core.services.filter.HttpInterceptor; @@ -27,6 +28,15 @@ public AdminRpcHttpService() { contextPath = "/"; } + @Override + public void innerStart() throws Exception { + if (enable && !Constant.LOCAL_HOST.equals(listenAddress)) { + logger.warn("Admin RPC is enabled on {} and may be accessible remotely. " + + "Restrict access to trusted networks.", listenAddress); + } + super.innerStart(); + } + @Override protected void addServlet(ServletContextHandler context) { context.addServlet(new ServletHolder(adminRpcServlet), "/admin"); From 667c7f43bb0b4ee33a73c31203fc96b85917ace0 Mon Sep 17 00:00:00 2001 From: 317787106 <317787106@qq.com> Date: Thu, 6 Aug 2026 22:14:51 +0800 Subject: [PATCH 04/15] extract annoation; add Exportable; remove committee from Exportable; return code when using --exec; use IPC frame; don't init args when using --attach; throw Tron_ERROR when output-directory is not exist --- .../common/parameter/CommonParameter.java | 159 +++++++++++++++- .../org/tron/common/parameter/Exportable.java | 18 ++ .../common/application/AbstractService.java | 9 +- .../tron/common/application/HttpService.java | 2 + .../java/org/tron/core/config/args/Args.java | 22 ++- .../admin/CommonParameterExporter.java | 17 +- .../core/services/admin/ipc/IpcClient.java | 178 +++++++++--------- .../core/services/admin/ipc/IpcService.java | 114 +++++++++-- .../main/java/org/tron/program/FullNode.java | 7 +- .../org/tron/core/config/args/ArgsTest.java | 35 +++- .../admin/CommonParameterExporterTest.java | 83 ++++++-- .../services/admin/ipc/IpcClientTest.java | 59 +++++- .../services/admin/ipc/IpcServiceTest.java | 120 +++++++++++- 13 files changed, 672 insertions(+), 151 deletions(-) create mode 100644 common/src/main/java/org/tron/common/parameter/Exportable.java diff --git a/common/src/main/java/org/tron/common/parameter/CommonParameter.java b/common/src/main/java/org/tron/common/parameter/CommonParameter.java index 21260eb8210..694deb0b2dd 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -40,34 +40,44 @@ public class CommonParameter { // when the energy-limit governance proposal is activated. // Legacy: should belong to VMConfig, not here. @Setter + @Exportable public static boolean ENERGY_LIMIT_HARD_FORK = false; // -- Startup parameters -- @Getter + @Exportable public String outputDirectory = "output-directory"; @Getter + @Exportable public String logbackPath = ""; // -- Flags (CLI + Config) -- @Getter @Setter + @Exportable public boolean witness = false; @Getter @Setter + @Exportable public boolean supportConstant = false; @Getter @Setter + @Exportable public long maxEnergyLimitForConstant = 100_000_000L; @Getter @Setter + @Exportable public int lruCacheSize = 500; @Getter @Setter + @Exportable public boolean debug = false; @Getter @Setter + @Exportable public double minTimeRatio = 0.0; @Getter @Setter + @Exportable public double maxTimeRatio = calcMaxTimeRatio(); /** * Max TVM execution time (ms) for constant calls — covers @@ -80,192 +90,245 @@ public class CommonParameter { */ @Getter @Setter + @Exportable public long constantCallTimeoutMs = 0L; @Getter @Setter + @Exportable public boolean saveInternalTx; @Getter @Setter + @Exportable public boolean saveFeaturedInternalTx; @Getter @Setter + @Exportable public boolean saveCancelAllUnfreezeV2Details; @Getter @Setter + @Exportable public int longRunningTime = 10; @Getter @Setter + @Exportable public int maxHttpConnectNumber = 50; @Getter public List seedNodes = new ArrayList<>(); @Getter + @Exportable public boolean fastForward = false; // -- Network / P2P -- @Getter @Setter + @Exportable public String chainId; @Getter @Setter + @Exportable public boolean needSyncCheck; @Getter @Setter + @Exportable public boolean nodeDiscoveryEnable; @Getter @Setter + @Exportable public boolean nodeDiscoveryPersist; @Getter @Setter + @Exportable public boolean nodeEffectiveCheckEnable; @Getter @Setter + @Exportable public int fetchBlockTimeout; @Getter @Setter + @Exportable public int maxConnections = 30; // from clearParam(), consistent with mainnet.conf @Getter @Setter + @Exportable public int minConnections = 8; // from clearParam(), consistent with mainnet.conf @Getter @Setter + @Exportable public int minActiveConnections = 3; // from clearParam(), consistent with mainnet.conf @Getter @Setter + @Exportable public int maxConnectionsWithSameIp = 2; // from clearParam(), consistent with mainnet.conf @Getter @Setter + @Exportable public int maxTps; // clearParam: 1000 @Getter @Setter + @Exportable public int maxBlockInvPerSecond = 10; // default: 10 block inv hashes/s per peer @Getter @Setter + @Exportable public int minParticipationRate; @Getter + @Exportable public P2pConfig p2pConfig; @Getter @Setter + @Exportable public int nodeListenPort; @Getter @Setter + @Exportable public String nodeLanIp; @Getter @Setter + @Exportable public String nodeExternalIp; @Getter @Setter + @Exportable public int nodeP2pVersion; @Getter @Setter + @Exportable public boolean nodeEnableIpv6 = false; @Getter @Setter + @Exportable public List dnsTreeUrls; // clearParam: new ArrayList<>() @Getter @Setter public PublishConfig dnsPublishConfig; @Getter @Setter + @Exportable public long syncFetchBatchNum; // clearParam: 2000 @Getter @Setter + @Exportable public int maxPendingBlockSize; // If you are running a solidity node for java tron, // this flag is set to true @Getter @Setter + @Exportable public boolean solidityNode = false; // If you are running KeystoreFactory, // this flag is set to true @Getter @Setter + @Exportable public boolean keystoreFactory = false; - @Getter - @Setter - public String ipcSocketFile = ""; - // -- RPC / HTTP -- @Getter @Setter + @Exportable public int rpcPort; @Getter @Setter + @Exportable public int rpcOnSolidityPort; @Getter @Setter + @Exportable public int fullNodeHttpPort; @Getter @Setter + @Exportable public int solidityHttpPort; @Getter @Setter + @Exportable public int jsonRpcHttpFullNodePort; @Getter @Setter + @Exportable public int jsonRpcHttpSolidityPort; @Getter @Setter + @Exportable public int jsonRpcHttpPBFTPort; @Getter @Setter + @Exportable public int rpcThreadNum; @Getter @Setter + @Exportable public int solidityThreads; @Getter @Setter + @Exportable public int maxConcurrentCallsPerConnection; @Getter @Setter + @Exportable public int flowControlWindow; @Getter @Setter + @Exportable public int rpcMaxRstStream; @Getter @Setter + @Exportable public int rpcSecondsPerWindow; @Getter @Setter + @Exportable public long maxConnectionIdleInMillis; @Getter @Setter + @Exportable public int blockProducedTimeOut; @Getter @Setter + @Exportable public long netMaxTrxPerSecond; @Getter @Setter + @Exportable public long maxConnectionAgeInMillis; // Refers to RPC (gRPC) max message size; see httpMaxMessageSize / jsonRpcMaxMessageSize // below for the HTTP / JSON-RPC counterparts. @Getter @Setter + @Exportable public int maxMessageSize; @Getter @Setter + @Exportable public long httpMaxMessageSize; @Getter @Setter + @Exportable public long jsonRpcMaxMessageSize; @Getter @Setter + @Exportable public int maxHeaderListSize; @Getter @Setter + @Exportable public boolean isRpcReflectionServiceEnable; @Getter @Setter + @Exportable public int validateSignThreadNum; @Getter @Setter + @Exportable public long maintenanceTimeInterval; @Getter @Setter + @Exportable public long proposalExpireTime; @Getter @Setter + @Exportable public int checkFrozenTime; // clearParam: 1 // -- Committee parameters -- @@ -296,54 +359,70 @@ public class CommonParameter { @Getter @Setter + @Exportable public String trustNodeAddr; // clearParam: "" @Getter @Setter + @Exportable public boolean walletExtensionApi; @Getter @Setter + @Exportable public boolean estimateEnergy; @Getter @Setter + @Exportable public int estimateEnergyMaxRetry = 3; // from clearParam(), consistent with mainnet.conf @Getter @Setter + @Exportable public int backupPriority; @Getter @Setter + @Exportable public int backupPort; @Getter @Setter + @Exportable public int keepAliveInterval; @Getter @Setter + @Exportable public List backupMembers; @Getter @Setter + @Exportable public boolean isOpenFullTcpDisconnect; @Getter @Setter + @Exportable public int inactiveThreshold = 600; // from clearParam(), consistent with mainnet.conf @Getter @Setter + @Exportable public boolean nodeDetectEnable; @Getter @Setter public int allowMultiSign; @Getter @Setter + @Exportable public boolean vmTrace; @Getter @Setter + @Exportable public boolean needToUpdateAsset; @Getter @Setter + @Exportable public String trxReferenceBlock; @Getter @Setter + @Exportable public int minEffectiveConnection; @Getter @Setter + @Exportable public boolean trxCacheEnable; @Getter @Setter @@ -360,20 +439,25 @@ public class CommonParameter { @Getter @Setter + @Exportable public boolean allowShieldedTransactionApi; // clearParam: false @Getter @Setter + @Exportable public long blockNumForEnergyLimit; @Getter @Setter + @Exportable public boolean eventSubscribe = false; @Getter @Setter + @Exportable public long trxExpirationTimeInMilliseconds; // -- Shielded / ZK -- @Getter @Setter + @Exportable public String zenTokenId; // clearParam: "000000" @Getter @Setter @@ -383,156 +467,207 @@ public class CommonParameter { public long allowAccountStateRoot; @Getter @Setter + @Exportable public int validContractProtoThreadNum = 1; @Getter @Setter + @Exportable public int shieldedTransInPendingMaxCounts; // clearParam: 10 @Getter @Setter public long changedDelegation; @Getter @Setter + @Exportable public RateLimiterInitialization rateLimiterInitialization; @Getter @Setter + @Exportable public int rateLimiterGlobalQps = 50000; // from clearParam(), consistent with mainnet.conf @Getter @Setter + @Exportable public int rateLimiterGlobalIpQps = 10000; // from clearParam(), consistent with mainnet.conf @Getter + @Exportable public int rateLimiterGlobalApiQps = 1000; // from clearParam(), consistent with mainnet.conf @Getter @Setter + @Exportable public double rateLimiterSyncBlockChain; // clearParam: 3.0 @Getter @Setter + @Exportable public double rateLimiterFetchInvData; // clearParam: 3.0 @Getter @Setter + @Exportable public double rateLimiterDisconnect; // clearParam: 1.0 @Getter @Setter + @Exportable public boolean rateLimiterApiNonBlocking = false; @Getter + @Exportable public RocksDbSettings rocksDBCustomSettings; @Getter + @Exportable public GenesisBlock genesisBlock; @Getter @Setter + @Exportable public boolean p2pDisable = false; @Getter @Setter // from clearParam(), consistent with mainnet.conf + @Exportable public List activeNodes = new ArrayList<>(); @Getter @Setter // from clearParam(), consistent with mainnet.conf + @Exportable public List passiveNodes = new ArrayList<>(); @Getter + @Exportable public List fastForwardNodes; // clearParam: new ArrayList<>() @Getter + @Exportable public int maxFastForwardNum; // clearParam: 4 @Getter + @Exportable public Storage storage; @Getter + @Exportable public SeedNode seedNode; @Getter + @Exportable public EventPluginConfig eventPluginConfig; @Getter + @Exportable public FilterQuery eventFilter; @Getter @Setter + @Exportable public String cryptoEngine = Constant.ECKey_ENGINE; @Getter @Setter + @Exportable public boolean rpcEnable = true; @Getter @Setter + @Exportable public boolean rpcSolidityEnable = true; @Getter @Setter + @Exportable public boolean rpcPBFTEnable = true; @Getter @Setter + @Exportable public boolean fullNodeHttpEnable = true; @Getter @Setter + @Exportable public boolean solidityNodeHttpEnable = true; @Getter @Setter + @Exportable public boolean pBFTHttpEnable = true; @Getter @Setter + @Exportable public boolean jsonRpcHttpFullNodeEnable = false; @Getter @Setter + @Exportable public boolean jsonRpcHttpSolidityNodeEnable = false; @Getter @Setter + @Exportable public boolean jsonRpcHttpPBFTNodeEnable = false; @Getter @Setter + @Exportable public int jsonRpcMaxBlockRange = 5000; @Getter @Setter + @Exportable public int jsonRpcMaxSubTopics = 1000; @Getter @Setter + @Exportable public int jsonRpcMaxBlockFilterNum = 50000; @Getter @Setter + @Exportable public int jsonRpcMaxBatchSize = 100; @Getter @Setter + @Exportable public int jsonRpcMaxResponseSize = 25 * 1024 * 1024; @Getter @Setter + @Exportable public int jsonRpcMaxAddressSize = 1000; @Getter @Setter + @Exportable public int jsonRpcMaxLogFilterNum = 20000; @Getter @Setter + @Exportable public boolean adminRpcEnable = false; @Getter @Setter + @Exportable public String adminListenAddress = Constant.LOCAL_HOST; @Getter @Setter + @Exportable public int adminListenPort = 8575; @Getter @Setter + @Exportable public boolean ipcEnable = false; @Getter @Setter + @Exportable public int maxTransactionPendingSize; @Getter @Setter + @Exportable public long pendingTransactionTimeout; @Getter @Setter + @Exportable public int maxTrxCacheSize; @Getter @Setter + @Exportable public boolean nodeMetricsEnable = false; @Getter @Setter + @Exportable public boolean metricsPrometheusEnable = false; @Getter @Setter + @Exportable public int metricsPrometheusPort; @Getter @Setter + @Exportable public int agreeNodeCount; @Getter @Setter public long allowPBFT; @Getter @Setter + @Exportable public int rpcOnPBFTPort; @Getter @Setter + @Exportable public int pBFTHttpPort; @Getter @@ -540,6 +675,7 @@ public class CommonParameter { public long pBFTExpireNum; // clearParam: 20 @Getter @Setter + @Exportable public long oldSolidityBlockNum = -1; @Getter @@ -565,15 +701,19 @@ public class CommonParameter { public long allowHigherLimitForMaxCpuTimeOfOneTx; @Getter @Setter + @Exportable public boolean openHistoryQueryWhenLiteFN = false; @Getter @Setter + @Exportable public boolean historyBalanceLookup = false; @Getter @Setter + @Exportable public boolean openPrintLog = true; @Getter @Setter + @Exportable public boolean openTransactionSort = false; @Getter @Setter @@ -583,18 +723,23 @@ public class CommonParameter { public long allowAssetOptimization; @Getter @Setter + @Exportable public List disabledApiList; // clearParam: Collections.emptyList() @Getter @Setter + @Exportable public CronExpression shutdownBlockTime = null; @Getter @Setter + @Exportable public long shutdownBlockHeight = -1; @Getter @Setter + @Exportable public long shutdownBlockCount = -1; @Getter @Setter + @Exportable public long blockCacheTimeout = 60; @Getter @Setter @@ -628,21 +773,26 @@ public class CommonParameter { public long dynamicEnergyMaxFactor = 0L; @Getter @Setter + @Exportable public boolean dynamicConfigEnable; @Getter @Setter + @Exportable public long dynamicConfigCheckInterval; // clearParam: 600 @Getter @Setter public long allowTvmShangHai; @Getter @Setter + @Exportable public long allowCancelAllUnfreezeV2; @Getter @Setter + @Exportable public boolean unsolidifiedBlockCheck; @Getter @Setter + @Exportable public int maxUnsolidifiedBlocks; // clearParam: 54 @Getter @Setter @@ -652,6 +802,7 @@ public class CommonParameter { public long allowEnergyAdjustment; @Getter @Setter + @Exportable public long maxCreateAccountTxSize = 1000L; @Getter @Setter diff --git a/common/src/main/java/org/tron/common/parameter/Exportable.java b/common/src/main/java/org/tron/common/parameter/Exportable.java new file mode 100644 index 00000000000..71272d394a1 --- /dev/null +++ b/common/src/main/java/org/tron/common/parameter/Exportable.java @@ -0,0 +1,18 @@ +package org.tron.common.parameter; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a {@link CommonParameter} field as safe to expose through the admin runtime-parameter + * API. Unmarked fields are excluded by default. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Exportable { + +} diff --git a/framework/src/main/java/org/tron/common/application/AbstractService.java b/framework/src/main/java/org/tron/common/application/AbstractService.java index 4d0317c0eb8..79c25dc4944 100644 --- a/framework/src/main/java/org/tron/common/application/AbstractService.java +++ b/framework/src/main/java/org/tron/common/application/AbstractService.java @@ -10,7 +10,6 @@ @Slf4j(topic = "service") public abstract class AbstractService implements Service { - protected String listenAddress; protected int port; @Getter protected boolean enable; @@ -20,16 +19,12 @@ public abstract class AbstractService implements Service { @Override public CompletableFuture start() { - if (port > 0) { - logger.info("{} starting on {}", name, port); - } + logger.info("{} starting on {}", name, port); final CompletableFuture resultFuture = new CompletableFuture<>(); try { innerStart(); resultFuture.complete(true); - if (port > 0) { - logger.info("{} started, listening on {}", name, port); - } + logger.info("{} started, listening on {}", name, port); } catch (Exception e) { resultFuture.completeExceptionally(e); } diff --git a/framework/src/main/java/org/tron/common/application/HttpService.java b/framework/src/main/java/org/tron/common/application/HttpService.java index bdc7db3c61b..8eaa23102e9 100644 --- a/framework/src/main/java/org/tron/common/application/HttpService.java +++ b/framework/src/main/java/org/tron/common/application/HttpService.java @@ -40,6 +40,8 @@ public abstract class HttpService extends AbstractService { protected Server apiServer; + protected String listenAddress; + protected String contextPath; protected long maxRequestSize = 4 * 1024 * 1024; // 4MB diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index 2902d4924c0..b2a5061f392 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -105,6 +105,9 @@ public class Args extends CommonParameter { @Getter private static String configFilePath = ""; + @Getter + private static String ipcSocketFile; + @Getter private static String ipcExecCommand; @@ -163,10 +166,13 @@ public static void setParam(final String[] args, final String confFileName) { Args.printHelp(jc); exit(0); } - ipcExecCommand = cmd.ipcExecCommand; - if (ipcExecCommand != null && StringUtils.isEmpty(cmd.ipcSocketFile)) { + if (cmd.ipcExecCommand != null && StringUtils.isEmpty(cmd.ipcSocketFile)) { throw new ParameterException("--exec requires --attach "); } + if (StringUtils.isNotEmpty(cmd.ipcSocketFile)) { + applyAttachParams(cmd); + return; + } // Resolve config file path configFilePath = StringUtils.isNoneBlank(cmd.shellConfFileName) @@ -189,6 +195,14 @@ public static void setParam(final String[] args, final String confFileName) { initLocalWitnesses(config, cmd); } + private static void applyAttachParams(CLIParameter cmd) { + ipcSocketFile = cmd.ipcSocketFile; + ipcExecCommand = cmd.ipcExecCommand; + if (StringUtils.isNotEmpty(cmd.logbackPath)) { + PARAMETER.logbackPath = cmd.logbackPath; + } + } + /** * Bridge VmConfig bean values to CommonParameter fields. * Temporary until Phase 2 moves fields into domain config objects. @@ -876,9 +890,6 @@ private static void applyCLIParams(CLIParameter cmd, JCommander jc) { if (assigned.contains("--keystore-factory")) { PARAMETER.keystoreFactory = cmd.keystoreFactory; } - if (assigned.contains("--attach")) { - PARAMETER.ipcSocketFile = cmd.ipcSocketFile; - } if (assigned.contains("--rpc-thread")) { PARAMETER.rpcThreadNum = cmd.rpcThreadNum; } @@ -964,6 +975,7 @@ public static void clearParam() { rateLimiterConfig = null; metricsConfig = null; eventConfig = null; + ipcSocketFile = null; ipcExecCommand = null; } diff --git a/framework/src/main/java/org/tron/core/services/admin/CommonParameterExporter.java b/framework/src/main/java/org/tron/core/services/admin/CommonParameterExporter.java index 3271e8f27d2..5329813c6c3 100644 --- a/framework/src/main/java/org/tron/core/services/admin/CommonParameterExporter.java +++ b/framework/src/main/java/org/tron/core/services/admin/CommonParameterExporter.java @@ -17,6 +17,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.tron.common.parameter.CommonParameter; +import org.tron.common.parameter.Exportable; @Component @Slf4j(topic = "API") @@ -26,7 +27,7 @@ public class CommonParameterExporter { private static final String UNAVAILABLE_VALUE = "[UNAVAILABLE]"; private static final String[] SENSITIVE_NAME_PARTS = { "private", "password", "passwd", "secret", "credential", "mnemonic", - "accesskey", "apikey", "localwitness", "seedphrase", "dbconfig", + "accesskey", "apikey", "localwitness", "seedphrase", "authorization", "authtoken" }; private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); @@ -40,7 +41,13 @@ Map export(CommonParameter parameter) { Field[] fields = CommonParameter.class.getFields(); Arrays.sort(fields, Comparator.comparing(Field::getName)); for (Field field : fields) { + if (!field.isAnnotationPresent(Exportable.class)) { + continue; + } String fieldName = field.getName(); + if (isOmittedName(fieldName)) { + continue; + } if (isSensitiveName(fieldName)) { snapshot.put(fieldName, REDACTED_VALUE); continue; @@ -75,7 +82,9 @@ JsonNode sanitize(JsonNode value) { Iterator> fields = value.fields(); while (fields.hasNext()) { Entry field = fields.next(); - if (isSensitiveName(field.getKey())) { + if (isOmittedName(field.getKey())) { + continue; + } else if (isSensitiveName(field.getKey())) { sanitized.put(field.getKey(), REDACTED_VALUE); } else { sanitized.set(field.getKey(), sanitize(field.getValue())); @@ -86,6 +95,10 @@ JsonNode sanitize(JsonNode value) { return value; } + private boolean isOmittedName(String name) { + return "dbconfig".equals(name.toLowerCase(Locale.ROOT)); + } + private boolean isSensitiveName(String name) { String normalized = name.toLowerCase(Locale.ROOT); if ("pwd".equals(normalized) || "key".equals(normalized) diff --git a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcClient.java b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcClient.java index a0e898ce9eb..2b16691e269 100644 --- a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcClient.java +++ b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcClient.java @@ -49,86 +49,61 @@ public class IpcClient { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + static final int EXIT_SUCCESS = 0; + static final int EXIT_FAILURE = 1; private final String socketFilePath; - private final Map commandLowerMap; - private final Map> commandParameters; - private final Map> commandParameterTypes; + private final Map adminCommands; private final DefaultParser commandParser = new DefaultParser().eofOnUnclosedQuote(true); private int requestId = 0; public IpcClient(String socketFilePath) { this.socketFilePath = socketFilePath; - this.commandLowerMap = collectAdminCommands(); - this.commandParameters = collectAdminCommandParams(); - this.commandParameterTypes = collectAdminCommandParamTypes(); + this.adminCommands = collectAdminCommands(); } - public static void start(String socketFilePath) { - start(socketFilePath, null); + public static int start(String socketFilePath) { + return start(socketFilePath, null); } - public static void start(String socketFilePath, String execCommand) { + public static int start(String socketFilePath, String execCommand) { IpcClient ipcClient = new IpcClient(socketFilePath); try { - ipcClient.run(execCommand); + return ipcClient.run(execCommand); } catch (IOException e) { - logger.error("", e); + System.err.println("Failed to communicate with IPC server."); + logger.debug("IPC client communication failed: {}", e.getClass().getSimpleName()); + return EXIT_FAILURE; } } - private Map collectAdminCommands() { - Map commandMap = new HashMap<>(); - Class rpcInterface = AdminJsonRpc.class; - for (Method method : rpcInterface.getDeclaredMethods()) { - JsonRpcMethod rpcMethod = method.getAnnotation(JsonRpcMethod.class); - if (rpcMethod != null && rpcMethod.value() != null) { - commandMap.put(rpcMethod.value().toLowerCase(Locale.ROOT), rpcMethod.value()); - } - } - return commandMap; - } - - private Map> collectAdminCommandParams() { - Map> commandParameters = new HashMap<>(); - Class rpcInterface = AdminJsonRpc.class; - - for (Method method : rpcInterface.getDeclaredMethods()) { + private Map collectAdminCommands() { + Map commands = new HashMap<>(); + for (Method method : AdminJsonRpc.class.getDeclaredMethods()) { JsonRpcMethod rpcMethod = method.getAnnotation(JsonRpcMethod.class); if (rpcMethod == null || rpcMethod.value() == null) { continue; } - String methodName = rpcMethod.value().toLowerCase(Locale.ROOT); - List params = new ArrayList<>(); + List parameterNames = new ArrayList<>(); Annotation[][] paramAnnotations = method.getParameterAnnotations(); for (Annotation[] annotations : paramAnnotations) { for (Annotation anno : annotations) { if (anno instanceof JsonRpcParam) { - JsonRpcParam p = (JsonRpcParam) anno; - params.add(p.value()); + parameterNames.add(((JsonRpcParam) anno).value()); } } } - commandParameters.put(methodName, params); - } - return commandParameters; - } - private Map> collectAdminCommandParamTypes() { - Map> parameterTypes = new HashMap<>(); - for (Method method : AdminJsonRpc.class.getDeclaredMethods()) { - JsonRpcMethod rpcMethod = method.getAnnotation(JsonRpcMethod.class); - if (rpcMethod == null || rpcMethod.value() == null) { - continue; - } - List types = new ArrayList<>(); + List parameterTypes = new ArrayList<>(); for (Type type : method.getGenericParameterTypes()) { - types.add(OBJECT_MAPPER.getTypeFactory().constructType(type)); + parameterTypes.add(OBJECT_MAPPER.getTypeFactory().constructType(type)); } - parameterTypes.put(rpcMethod.value().toLowerCase(Locale.ROOT), types); + + AdminCommand command = new AdminCommand(rpcMethod.value(), parameterNames, parameterTypes); + commands.put(rpcMethod.value().toLowerCase(Locale.ROOT), command); } - return parameterTypes; + return commands; } private void printHelp() { @@ -139,11 +114,14 @@ private void printHelp() { } List buildHelpLines() { - List commands = new ArrayList<>(commandLowerMap.values()); + List commands = new ArrayList<>(); + for (AdminCommand command : adminCommands.values()) { + commands.add(command.name); + } Collections.sort(commands); List helpLines = new ArrayList<>(); for (String command : commands) { - helpLines.add(formatUsage(command)); + helpLines.add(formatUsage(adminCommands.get(command.toLowerCase(Locale.ROOT)))); } helpLines.add("help [command]"); helpLines.add("exit"); @@ -151,59 +129,57 @@ List buildHelpLines() { return helpLines; } - private String formatUsage(String command) { - String commandLowerCase = command.toLowerCase(Locale.ROOT); - List parameters = commandParameters.get(commandLowerCase); - if (parameters == null || parameters.isEmpty()) { - return command; + private String formatUsage(AdminCommand command) { + if (command.parameterNames.isEmpty()) { + return command.name; } - List parameterTypes = commandParameterTypes.get(commandLowerCase); List typedParameters = new ArrayList<>(); - for (int i = 0; i < parameters.size(); i++) { - typedParameters.add(parameters.get(i) + ":" + formatType(parameterTypes.get(i))); + for (int i = 0; i < command.parameterNames.size(); i++) { + typedParameters.add(command.parameterNames.get(i) + ":" + + formatType(command.parameterTypes.get(i))); } - return command + " <" + StringUtils.join(typedParameters, "> <") + ">"; + return command.name + " <" + StringUtils.join(typedParameters, "> <") + ">"; } - public void run() throws IOException { - run(null); + public int run() throws IOException { + return run(null); } - void run(String execCommand) throws IOException { + int run(String execCommand) throws IOException { File socketFile = new File(socketFilePath); if (!socketFile.exists()) { System.err.println("IPC socket file does not exist: " + socketFile.getName()); - return; + return EXIT_FAILURE; } AFUNIXSocketAddress address = AFUNIXSocketAddress.of(socketFile); try (Socket socket = AFUNIXSocket.newInstance()) { socket.connect(address); if (execCommand != null) { - runExec(socket, execCommand); - return; + return runExec(socket, execCommand); } printWelcome(socketFile); try (Terminal terminal = TerminalBuilder.builder().system(true).build()) { LineReader reader = createLineReader(terminal); runSession(socket, reader); } + return EXIT_SUCCESS; } } - void runExec(Socket socket, String commandLine) throws IOException { + int runExec(Socket socket, String commandLine) throws IOException { List commandWords; try { commandWords = parseCommandLine(commandLine); } catch (SyntaxError e) { System.err.println("Invalid command syntax."); - return; + return EXIT_FAILURE; } if (commandWords.isEmpty()) { System.err.println("No command specified for --exec."); - return; + return EXIT_FAILURE; } if (isExitCommand(commandWords.get(0))) { - return; + return EXIT_SUCCESS; } String request; @@ -211,10 +187,10 @@ void runExec(Socket socket, String commandLine) throws IOException { request = buildRequest(commandWords); } catch (IllegalArgumentException e) { System.err.println(e.getMessage()); - return; + return EXIT_FAILURE; } if (request == null) { - return; + return "help".equalsIgnoreCase(commandWords.get(0)) ? EXIT_SUCCESS : EXIT_FAILURE; } try (BufferedWriter serverWriter = new BufferedWriter( @@ -231,9 +207,15 @@ void runExec(Socket socket, String commandLine) throws IOException { } while (response != null && response.trim().isEmpty()); if (response == null) { System.err.println("Disconnected from server before receiving a response."); - return; + return EXIT_FAILURE; + } + String formattedResponse = formatResponse(response); + if (isSuccessfulResponse(response)) { + System.out.println(formattedResponse); + return EXIT_SUCCESS; } - System.out.println(formatResponse(response)); + System.err.println(formattedResponse); + return EXIT_FAILURE; } } @@ -276,7 +258,7 @@ private void outputResponse(final Socket socket, LineReader reader, AtomicBoolea private LineReader createLineReader(Terminal terminal) { Completer commandCompleter = - new IpcCommandCompleter(commandLowerMap.keySet().toArray(new String[0])); + new IpcCommandCompleter(adminCommands.keySet().toArray(new String[0])); ArgumentCompleter completer = new ArgumentCompleter( commandCompleter, NullCompleter.INSTANCE @@ -373,38 +355,37 @@ private String buildRequest(List commandWords) throws JsonProcessingExce String commandLowerCase = command.toLowerCase(Locale.ROOT); if ("help".equals(commandLowerCase)) { if (commandWords.size() == 2 - && commandLowerMap.containsKey(commandWords.get(1).toLowerCase(Locale.ROOT))) { + && adminCommands.containsKey(commandWords.get(1).toLowerCase(Locale.ROOT))) { String rpcMethod = commandWords.get(1).toLowerCase(Locale.ROOT); - System.out.println("usage: " + formatUsage(commandLowerMap.get(rpcMethod))); + System.out.println("usage: " + formatUsage(adminCommands.get(rpcMethod))); } else { printHelp(); } return null; } - if (!commandLowerMap.containsKey(commandLowerCase)) { + AdminCommand adminCommand = adminCommands.get(commandLowerCase); + if (adminCommand == null) { System.err.println("Invalid cmd: " + command); printHelp(); return null; } - if (commandWords.size() - 1 != commandParameters.get(commandLowerCase).size()) { + if (commandWords.size() - 1 != adminCommand.parameterNames.size()) { System.err.println("Invalid parameter, usage: " - + formatUsage(commandLowerMap.get(commandLowerCase))); + + formatUsage(adminCommand)); return null; } List rawValues = new ArrayList<>( commandWords.subList(1, commandWords.size())); - List values = convertArguments(commandLowerCase, rawValues); - return buildJsonWithParameter(commandLowerMap.get(commandLowerCase), values); + List values = convertArguments(adminCommand, rawValues); + return buildJsonWithParameter(adminCommand.name, values); } - private List convertArguments(String command, List values) { + private List convertArguments(AdminCommand command, List values) { List convertedValues = new ArrayList<>(); - List parameterTypes = commandParameterTypes.get(command); - List parameterNames = commandParameters.get(command); for (int i = 0; i < values.size(); i++) { - convertedValues.add(convertArgument(values.get(i), parameterTypes.get(i), - parameterNames.get(i))); + convertedValues.add(convertArgument(values.get(i), command.parameterTypes.get(i), + command.parameterNames.get(i))); } return convertedValues; } @@ -486,6 +467,19 @@ String formatResponse(String response) { } } + boolean isSuccessfulResponse(String response) { + try { + JsonNode root = OBJECT_MAPPER.readTree(response); + if (root == null || !root.isObject()) { + return false; + } + JsonNode error = root.get("error"); + return (error == null || error.isNull()) && root.has("result"); + } catch (JsonProcessingException e) { + return false; + } + } + private String formatJsonValue(JsonNode value) throws JsonProcessingException { if (value == null || value.isNull()) { return "null"; @@ -505,4 +499,18 @@ private String buildJsonWithParameter(String cmd, List values) params.put("id", ++requestId); return OBJECT_MAPPER.writeValueAsString(params); } + + private static class AdminCommand { + + private final String name; + private final List parameterNames; + private final List parameterTypes; + + private AdminCommand(String name, List parameterNames, + List parameterTypes) { + this.name = name; + this.parameterNames = parameterNames; + this.parameterTypes = parameterTypes; + } + } } diff --git a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java index 1d18a20d36d..d8aa4452686 100644 --- a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java +++ b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java @@ -1,6 +1,9 @@ package org.tron.core.services.admin.ipc; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.NullNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.googlecode.jsonrpc4j.JsonRpcServer; import java.io.BufferedReader; import java.io.BufferedWriter; @@ -13,12 +16,16 @@ import java.lang.management.ManagementFactory; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.PosixFileAttributeView; import java.nio.file.attribute.PosixFilePermission; import java.util.EnumSet; import java.util.Set; import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.RejectedExecutionException; @@ -33,12 +40,15 @@ import org.tron.common.exit.ExitManager; import org.tron.common.parameter.CommonParameter; import org.tron.core.config.args.Args; +import org.tron.core.exception.TronError; +import org.tron.core.exception.TronError.ErrCode; import org.tron.core.services.admin.AdminJsonRpc; @Component @Slf4j(topic = "API") public class IpcService extends AbstractService { + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private static final String ACCEPTOR_EXECUTOR_NAME = "admin-ipc-acceptor"; private static final String CLIENT_EXECUTOR_NAME = "admin-ipc-client"; private static final int CLIENT_HANDLER_THREADS = 4; @@ -58,19 +68,29 @@ public class IpcService extends AbstractService { public IpcService(AdminJsonRpc adminJsonRpc) { enable = isFullNode() && Args.getInstance().isIpcEnable(); - port = -1; //not used - jsonRpcServer = new JsonRpcServer(new ObjectMapper(), adminJsonRpc, AdminJsonRpc.class); + jsonRpcServer = new JsonRpcServer(OBJECT_MAPPER, adminJsonRpc, AdminJsonRpc.class); + } + + @Override + public CompletableFuture start() { + CompletableFuture resultFuture = new CompletableFuture<>(); + try { + innerStart(); + resultFuture.complete(true); + } catch (Exception e) { + resultFuture.completeExceptionally(e); + } + return resultFuture; } @Override public void innerStart() throws Exception { socketFilePath = resolveSocketFilePath(Args.getInstance(), getPid()); - createParentDirectories(socketFilePath); - Files.deleteIfExists(socketFilePath); + Path outputDirectory = socketFilePath.getParent(); + validateOutputDirectory(outputDirectory); + deleteStaleSocketFile(socketFilePath); File socketFile = socketFilePath.toFile(); - socketFile.deleteOnExit(); - AFUNIXSocketAddress address = AFUNIXSocketAddress.of(socketFile); unixServerSocket = AFUNIXServerSocket.bindOn(address); try { @@ -142,9 +162,14 @@ private void handleClient(AFUNIXSocket client) { String line; while ((line = reader.readLine()) != null) { String cmd = line.trim(); - logger.debug("Server received: {}", cmd); - writer.write(handleCommand(cmd)); - writer.flush(); + logger.debug("Received IPC request"); + String response = handleCommand(cmd); + if (!response.isEmpty()) { + writer.write(response); + writer.newLine(); + writer.flush(); + logger.debug("Sent IPC response"); + } } } catch (IOException e) { if (isRunning) { @@ -153,21 +178,52 @@ private void handleClient(AFUNIXSocket client) { } } - private String handleCommand(String jsonRequest) { + String handleCommand(String jsonRequest) { ByteArrayInputStream input = new ByteArrayInputStream(jsonRequest.getBytes(StandardCharsets.UTF_8)); ByteArrayOutputStream output = new ByteArrayOutputStream(); - String response; try { - jsonRpcServer.handleRequest(input, output); - response = output.toString(StandardCharsets.UTF_8.name()); + dispatchRequest(input, output); + if (output.size() == 0) { + return ""; + } + JsonNode response = OBJECT_MAPPER.readTree(output.toByteArray()); + return response == null ? "" : OBJECT_MAPPER.writeValueAsString(response); + } catch (Exception e) { + logger.debug("Failed to dispatch IPC request"); + return buildInternalErrorResponse(jsonRequest); + } + } + + void dispatchRequest(ByteArrayInputStream input, ByteArrayOutputStream output) + throws IOException { + jsonRpcServer.handleRequest(input, output); + } + + private String buildInternalErrorResponse(String jsonRequest) { + JsonNode requestId = NullNode.getInstance(); + try { + JsonNode request = OBJECT_MAPPER.readTree(jsonRequest); + if (request != null && request.has("id")) { + requestId = request.get("id"); + } } catch (IOException e) { - response = e.getMessage(); + logger.debug("Unable to read request id from invalid IPC request"); } - logger.debug("IPC response: {}", response); - return response; + ObjectNode error = OBJECT_MAPPER.createObjectNode(); + error.put("code", -32603); + error.put("message", "Internal error"); + ObjectNode response = OBJECT_MAPPER.createObjectNode(); + response.put("jsonrpc", "2.0"); + response.set("error", error); + response.set("id", requestId); + try { + return OBJECT_MAPPER.writeValueAsString(response); + } catch (IOException e) { + throw new IllegalStateException("Failed to serialize IPC error response", e); + } } @Override @@ -209,11 +265,29 @@ static Path resolveSocketFilePath(CommonParameter parameter, String pid) { "java-tron." + pid + ".sock"); } - static void createParentDirectories(Path socketFilePath) throws IOException { - Path parent = socketFilePath.getParent(); - if (parent != null) { - Files.createDirectories(parent); + static void validateOutputDirectory(Path outputDirectory) throws IOException { + if (outputDirectory == null || !Files.isDirectory(outputDirectory)) { + throw new TronError("IPC output directory does not exist or is not a directory", + ErrCode.API_SERVER_INIT); + } + if (!Files.getFileStore(outputDirectory) + .supportsFileAttributeView(PosixFileAttributeView.class)) { + throw new TronError("IPC requires a POSIX-compatible output directory", + ErrCode.API_SERVER_INIT); + } + } + + static void deleteStaleSocketFile(Path socketFilePath) throws IOException { + if (!Files.exists(socketFilePath, LinkOption.NOFOLLOW_LINKS)) { + return; + } + BasicFileAttributes attributes = Files.readAttributes(socketFilePath, + BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + if (attributes.isSymbolicLink() || !attributes.isOther()) { + throw new TronError("Refusing to replace a non-socket IPC endpoint", + ErrCode.API_SERVER_INIT); } + Files.delete(socketFilePath); } static void setOwnerOnlyPermissions(Path socketFilePath) throws IOException { diff --git a/framework/src/main/java/org/tron/program/FullNode.java b/framework/src/main/java/org/tron/program/FullNode.java index 599d04bc9ce..e0a5b4d8b2f 100644 --- a/framework/src/main/java/org/tron/program/FullNode.java +++ b/framework/src/main/java/org/tron/program/FullNode.java @@ -34,8 +34,11 @@ public static void main(String[] args) { KeystoreFactory.start(); return; } - if (StringUtils.isNotEmpty(parameter.getIpcSocketFile())) { - IpcClient.start(parameter.getIpcSocketFile(), Args.getIpcExecCommand()); + if (StringUtils.isNotEmpty(Args.getIpcSocketFile())) { + int exitCode = IpcClient.start(Args.getIpcSocketFile(), Args.getIpcExecCommand()); + if (exitCode != 0) { + System.exit(exitCode); + } return; } if (parameter.isSolidityNode()) { diff --git a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java index db4028e6b5f..9bdf63aa485 100644 --- a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java +++ b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java @@ -24,6 +24,9 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.InetAddress; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -51,21 +54,49 @@ public class ArgsTest { @Test public void testAttachWithExecParameters() { + Args.clearParam(); try { Args.setParam(new String[] { "--attach", "/tmp/java-tron.sock", - "--exec", "admin_example one two" + "--exec", "admin_example one two", + "--log-config", "attach-logback.xml" }, TestConstants.TEST_CONF); - Assert.assertEquals("/tmp/java-tron.sock", Args.getInstance().getIpcSocketFile()); + Assert.assertEquals("/tmp/java-tron.sock", Args.getIpcSocketFile()); Assert.assertEquals("admin_example one two", Args.getIpcExecCommand()); + Assert.assertEquals("attach-logback.xml", Args.getInstance().getLogbackPath()); + Assert.assertNull(Args.getNodeConfig()); + Assert.assertNull(Args.getLocalWitnesses()); + } finally { + Args.clearParam(); + } + Assert.assertNull(Args.getIpcSocketFile()); + Assert.assertNull(Args.getIpcExecCommand()); + } + + @Test + public void testAttachSkipsInvalidNodeConfig() throws Exception { + Args.clearParam(); + Path invalidConfig = Files.createTempFile("attach-invalid-config-", ".conf"); + Files.write(invalidConfig, Arrays.asList("node {"), StandardCharsets.UTF_8); + try { + Args.setParam(new String[] { + "--attach", "/tmp/java-tron.sock", + "--config", invalidConfig.toString() + }, TestConstants.TEST_CONF); + + Assert.assertEquals("/tmp/java-tron.sock", Args.getIpcSocketFile()); + Assert.assertNull(Args.getNodeConfig()); + Assert.assertEquals("", Args.getConfigFilePath()); } finally { Args.clearParam(); + Files.deleteIfExists(invalidConfig); } } @Test public void testExecRequiresAttach() { + Args.clearParam(); try { Args.setParam(new String[] {"--exec", "admin_getRuntimeParameters"}, TestConstants.TEST_CONF); diff --git a/framework/src/test/java/org/tron/core/services/admin/CommonParameterExporterTest.java b/framework/src/test/java/org/tron/core/services/admin/CommonParameterExporterTest.java index 00c0efd1301..dfa82fbd5f2 100644 --- a/framework/src/test/java/org/tron/core/services/admin/CommonParameterExporterTest.java +++ b/framework/src/test/java/org/tron/core/services/admin/CommonParameterExporterTest.java @@ -5,13 +5,18 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import java.lang.reflect.Field; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import org.junit.Assert; import org.junit.Test; +import org.tron.common.args.GenesisBlock; import org.tron.common.logsfilter.EventPluginConfig; import org.tron.common.parameter.CommonParameter; +import org.tron.common.parameter.Exportable; +import org.tron.core.config.args.Storage; +import org.tron.p2p.P2pConfig; import org.tron.p2p.dns.update.PublishConfig; public class CommonParameterExporterTest { @@ -20,7 +25,7 @@ public class CommonParameterExporterTest { private final CommonParameterExporter exporter = new CommonParameterExporter(); @Test - public void testExportIncludesAllPublicFieldsAndLiveValues() { + public void testExportIncludesOnlyAnnotatedFieldsAndLiveValues() { CommonParameter parameter = new CommonParameter(); parameter.rpcPort = 150051; parameter.chainId = "runtime-chain"; @@ -28,8 +33,13 @@ public void testExportIncludesAllPublicFieldsAndLiveValues() { Map snapshot = exporter.export(parameter); for (Field field : CommonParameter.class.getFields()) { - Assert.assertTrue("Missing runtime parameter: " + field.getName(), - snapshot.containsKey(field.getName())); + if (field.isAnnotationPresent(Exportable.class)) { + Assert.assertTrue("Missing exportable runtime parameter: " + field.getName(), + snapshot.containsKey(field.getName())); + } else { + Assert.assertFalse("Unexpected runtime parameter: " + field.getName(), + snapshot.containsKey(field.getName())); + } } Assert.assertEquals(150051, snapshot.get("rpcPort")); Assert.assertEquals("runtime-chain", snapshot.get("chainId")); @@ -49,6 +59,32 @@ public void testExportSortsTopLevelKeysByFieldName() { Assert.assertEquals(sortedKeys, actualKeys); } + @Test + public void testExportExcludesCommitteeParameters() { + Map snapshot = exporter.export(new CommonParameter()); + List committeeParameters = Arrays.asList( + "allowCreationOfContracts", "allowMultiSign", "allowAdaptiveEnergy", + "allowDelegateResource", "allowSameTokenName", "allowTvmTransferTrc10", + "allowTvmConstantinople", "allowTvmSolidity059", "forbidTransferToContract", + "allowShieldedTRC20Transaction", "allowMarketTransaction", + "allowTransactionFeePool", "allowBlackHoleOptimization", "allowNewResourceModel", + "allowTvmIstanbul", "allowProtoFilterNum", "allowAccountStateRoot", + "changedDelegation", "allowPBFT", "pBFTExpireNum", "allowTvmFreeze", + "allowTvmVote", "allowTvmLondon", "allowTvmCompatibleEvm", + "allowHigherLimitForMaxCpuTimeOfOneTx", "allowNewRewardAlgorithm", + "allowOptimizedReturnValueOfChainId", "allowTvmShangHai", "allowOldRewardOpt", + "allowEnergyAdjustment", "allowStrictMath", "consensusLogicOptimization", + "allowTvmCancun", "allowTvmBlob", "unfreezeDelayDays", + "allowAccountAssetOptimization", "allowAssetOptimization", "allowNewReward", + "memoFee", "allowDelegateOptimization", "allowDynamicEnergy", + "dynamicEnergyThreshold", "dynamicEnergyIncreaseFactor", "dynamicEnergyMaxFactor"); + + for (String fieldName : committeeParameters) { + Assert.assertFalse("Committee parameter must not be exported: " + fieldName, + snapshot.containsKey(fieldName)); + } + } + @Test public void testSanitizeRedactsSensitiveValuesRecursively() { ObjectNode source = OBJECT_MAPPER.createObjectNode(); @@ -59,6 +95,7 @@ public void testSanitizeRedactsSensitiveValuesRecursively() { nested.put("accessKeyId", "access-key-value"); nested.put("accessKeySecret", "secret-value"); nested.put("dnsPrivate", "dns-private-value"); + nested.put("dbConfig", "database|username|password"); nested.put("endpoint", "127.0.0.1"); JsonNode sanitized = exporter.sanitize(source); @@ -73,12 +110,13 @@ public void testSanitizeRedactsSensitiveValuesRecursively() { sanitized.get("dns").get("accessKeySecret").asText()); Assert.assertEquals(CommonParameterExporter.REDACTED_VALUE, sanitized.get("dns").get("dnsPrivate").asText()); + Assert.assertFalse(sanitized.get("dns").has("dbConfig")); Assert.assertEquals("000000", sanitized.get("zenTokenId").asText()); Assert.assertEquals("127.0.0.1", sanitized.get("dns").get("endpoint").asText()); } @Test - public void testExportRedactsConfiguredSecrets() { + public void testExportIncludesApprovedConfigurationsAndExcludesSecrets() { CommonParameter parameter = new CommonParameter(); parameter.dnsPublishConfig = new PublishConfig(); parameter.dnsPublishConfig.setDnsPrivate("dns-private-value"); @@ -87,19 +125,36 @@ public void testExportRedactsConfiguredSecrets() { parameter.dnsPublishConfig.setDnsDomain("nodes.example.org"); parameter.eventPluginConfig = new EventPluginConfig(); parameter.eventPluginConfig.setDbConfig("mongodb://user:password@localhost/events"); + parameter.eventPluginConfig.setServerAddress("127.0.0.1:5555"); + parameter.outputDirectory = "node-output"; + parameter.logbackPath = "logback.xml"; + parameter.storage = new Storage(); + parameter.storage.setDbDirectory("database"); + parameter.genesisBlock = GenesisBlock.getDefault(); + parameter.p2pConfig = new P2pConfig(); + parameter.p2pConfig.setIp("127.0.0.1"); + parameter.p2pConfig.setPublishConfig(parameter.dnsPublishConfig); Map snapshot = exporter.export(parameter); - Map dnsConfig = (Map) snapshot.get("dnsPublishConfig"); - Map eventConfig = (Map) snapshot.get("eventPluginConfig"); - - Assert.assertEquals(CommonParameterExporter.REDACTED_VALUE, - dnsConfig.get("dnsPrivate")); - Assert.assertEquals(CommonParameterExporter.REDACTED_VALUE, - dnsConfig.get("accessKeyId")); + Assert.assertFalse(snapshot.containsKey("dnsPublishConfig")); + Map eventPluginConfig = (Map) snapshot.get("eventPluginConfig"); + Assert.assertFalse(eventPluginConfig.containsKey("dbConfig")); + Assert.assertEquals("127.0.0.1:5555", eventPluginConfig.get("serverAddress")); + Assert.assertEquals("node-output", snapshot.get("outputDirectory")); + Assert.assertEquals("logback.xml", snapshot.get("logbackPath")); + Assert.assertEquals("database", ((Map) snapshot.get("storage")).get("dbDirectory")); + Assert.assertEquals("0", ((Map) snapshot.get("genesisBlock")).get("number")); + Map p2pConfig = (Map) snapshot.get("p2pConfig"); + Assert.assertEquals("127.0.0.1", p2pConfig.get("ip")); + Map publishConfig = (Map) p2pConfig.get("publishConfig"); Assert.assertEquals(CommonParameterExporter.REDACTED_VALUE, - dnsConfig.get("accessKeySecret")); - Assert.assertEquals("nodes.example.org", dnsConfig.get("dnsDomain")); + publishConfig.get("accessKeyId")); Assert.assertEquals(CommonParameterExporter.REDACTED_VALUE, - eventConfig.get("dbConfig")); + publishConfig.get("accessKeySecret")); + Assert.assertTrue(snapshot.containsKey("rateLimiterInitialization")); + Assert.assertTrue(snapshot.containsKey("rocksDBCustomSettings")); + Assert.assertTrue(snapshot.containsKey("seedNode")); + Assert.assertTrue(snapshot.containsKey("eventFilter")); + Assert.assertTrue(snapshot.containsKey("shutdownBlockTime")); } } diff --git a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcClientTest.java b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcClientTest.java index d4ab63fbab9..f75180c3c5c 100644 --- a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcClientTest.java +++ b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcClientTest.java @@ -106,7 +106,8 @@ public void testExecSendsCommandAndPrintsFormattedResult() throws Exception { PrintStream capturedOut = new PrintStream(consoleOutput, true, "UTF-8"); try { System.setOut(capturedOut); - new IpcClient("unused").runExec(socket, "admin_example \"hello world\" b"); + Assert.assertEquals(IpcClient.EXIT_SUCCESS, + new IpcClient("unused").runExec(socket, "admin_example \"hello world\" b")); } finally { System.setOut(originalOut); capturedOut.close(); @@ -150,8 +151,9 @@ public void testExecWithInvalidSyntaxDoesNotExposeCommand() throws Exception { PrintStream capturedErr = new PrintStream(errorOutput, true, "UTF-8"); try { System.setErr(capturedErr); - new IpcClient("unused").runExec(Mockito.mock(Socket.class), - "admin_example \"sensitive-value"); + Assert.assertEquals(IpcClient.EXIT_FAILURE, + new IpcClient("unused").runExec(Mockito.mock(Socket.class), + "admin_example \"sensitive-value")); } finally { System.setErr(originalErr); capturedErr.close(); @@ -171,7 +173,8 @@ public void testMissingSocketFilePrintsConsoleError() throws Exception { PrintStream capturedErr = new PrintStream(errorOutput, true, "UTF-8"); try { System.setErr(capturedErr); - new IpcClient(missingSocket.toString()).run(); + Assert.assertEquals(IpcClient.EXIT_FAILURE, + new IpcClient(missingSocket.toString()).run()); } finally { System.setErr(originalErr); capturedErr.close(); @@ -182,6 +185,54 @@ public void testMissingSocketFilePrintsConsoleError() throws Exception { errorOutput.toString("UTF-8")); } + @Test + public void testExecReturnsFailureForRpcError() throws Exception { + Socket socket = Mockito.mock(Socket.class); + Mockito.when(socket.getOutputStream()).thenReturn(new ByteArrayOutputStream()); + Mockito.when(socket.getInputStream()).thenReturn(new ByteArrayInputStream( + ("{\"jsonrpc\":\"2.0\",\"id\":1," + + "\"error\":{\"code\":-32603,\"message\":\"Internal error\"}}\n") + .getBytes(StandardCharsets.UTF_8))); + + PrintStream originalErr = System.err; + ByteArrayOutputStream errorOutput = new ByteArrayOutputStream(); + PrintStream capturedErr = new PrintStream(errorOutput, true, "UTF-8"); + try { + System.setErr(capturedErr); + Assert.assertEquals(IpcClient.EXIT_FAILURE, + new IpcClient("unused").runExec(socket, "admin_example a b")); + } finally { + System.setErr(originalErr); + capturedErr.close(); + } + + Assert.assertEquals("Error -32603: Internal error" + System.lineSeparator(), + errorOutput.toString("UTF-8")); + } + + @Test + public void testExecReturnsFailureWhenServerDisconnects() throws Exception { + Socket socket = Mockito.mock(Socket.class); + Mockito.when(socket.getOutputStream()).thenReturn(new ByteArrayOutputStream()); + Mockito.when(socket.getInputStream()).thenReturn(new ByteArrayInputStream(new byte[0])); + + PrintStream originalErr = System.err; + ByteArrayOutputStream errorOutput = new ByteArrayOutputStream(); + PrintStream capturedErr = new PrintStream(errorOutput, true, "UTF-8"); + try { + System.setErr(capturedErr); + Assert.assertEquals(IpcClient.EXIT_FAILURE, + new IpcClient("unused").runExec(socket, "admin_example a b")); + } finally { + System.setErr(originalErr); + capturedErr.close(); + } + + Assert.assertEquals( + "Disconnected from server before receiving a response." + System.lineSeparator(), + errorOutput.toString("UTF-8")); + } + @Test(timeout = 10_000) public void testSessionPrintsResponseAndExitsWhenServerDisconnects() throws Exception { CountDownLatch inputStarted = new CountDownLatch(1); diff --git a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java index 994dffff3a0..669a56e93bf 100644 --- a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java @@ -1,23 +1,32 @@ package org.tron.core.services.admin.ipc; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import java.io.BufferedReader; import java.io.BufferedWriter; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; import java.nio.file.Files; +import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.PosixFilePermission; import java.util.EnumSet; import org.junit.Assert; +import org.junit.Assume; import org.junit.Test; +import org.mockito.Mockito; import org.newsclub.net.unix.AFUNIXSocket; import org.newsclub.net.unix.AFUNIXSocketAddress; import org.tron.common.parameter.CommonParameter; import org.tron.core.config.args.Args; +import org.tron.core.exception.TronError; import org.tron.core.services.admin.AdminJsonRpcImpl; import org.tron.core.services.admin.CommonParameterExporter; @@ -36,15 +45,107 @@ public void testResolveSocketFilePathUsesOutputDirectory() { } @Test - public void testCreateParentDirectoriesWithNoParent() throws IOException { - Path socketFilePath = Paths.get("java-tron.1234.sock"); - Assert.assertNull(socketFilePath.getParent()); + public void testValidateOutputDirectoryRejectsMissingDirectory() throws IOException { + Path outputDirectory = Files.createTempDirectory("ipc-missing-output-test-"); + Files.delete(outputDirectory); - IpcService.createParentDirectories(socketFilePath); + try { + IpcService.validateOutputDirectory(outputDirectory); + Assert.fail("Expected a missing output directory to be rejected"); + } catch (TronError e) { + Assert.assertEquals("IPC output directory does not exist or is not a directory", + e.getMessage()); + } + } + + @Test + public void testDeleteStaleSocketFileRejectsRegularFile() throws IOException { + Path outputDirectory = Files.createTempDirectory("ipc-regular-file-test-"); + Path socketFile = outputDirectory.resolve("java-tron.1234.sock"); + Files.createFile(socketFile); + try { + IpcService.deleteStaleSocketFile(socketFile); + Assert.fail("Expected a regular file to be preserved"); + } catch (TronError e) { + Assert.assertEquals("Refusing to replace a non-socket IPC endpoint", e.getMessage()); + Assert.assertTrue(Files.isRegularFile(socketFile, LinkOption.NOFOLLOW_LINKS)); + } finally { + Files.deleteIfExists(socketFile); + Files.deleteIfExists(outputDirectory); + } + } + + @Test + public void testDeleteStaleSocketFileRejectsSymbolicLink() throws IOException { + assumePosixFileSystem(); + Path outputDirectory = Files.createTempDirectory("ipc-symbolic-link-test-"); + Path targetFile = outputDirectory.resolve("target"); + Path socketFile = outputDirectory.resolve("java-tron.1234.sock"); + Files.createFile(targetFile); + Files.createSymbolicLink(socketFile, targetFile.getFileName()); + try { + IpcService.deleteStaleSocketFile(socketFile); + Assert.fail("Expected a symbolic link to be preserved"); + } catch (TronError e) { + Assert.assertEquals("Refusing to replace a non-socket IPC endpoint", e.getMessage()); + Assert.assertTrue(Files.isSymbolicLink(socketFile)); + Assert.assertTrue(Files.exists(targetFile)); + } finally { + Files.deleteIfExists(socketFile); + Files.deleteIfExists(targetFile); + Files.deleteIfExists(outputDirectory); + } + } + + @Test + public void testValidateOutputDirectorySupportsPosixPermissions() throws IOException { + assumePosixFileSystem(); + Path outputDirectory = Files.createTempDirectory("ipc-posix-output-test-"); + try { + IpcService.validateOutputDirectory(outputDirectory); + } finally { + Files.deleteIfExists(outputDirectory); + } + } + + @Test + public void testHandleCommandReturnsSingleLineJsonResponse() throws Exception { + IpcService service = new IpcService( + new AdminJsonRpcImpl(new CommonParameterExporter())); + + String response = service.handleCommand( + "{\"jsonrpc\":\"2.0\",\"method\":\"admin_example\"," + + "\"params\":[\"a\",\"b\"],\"id\":7}"); + + Assert.assertFalse(response, response.contains("\n")); + Assert.assertFalse(response, response.contains("\r")); + Assert.assertEquals("a:b", new ObjectMapper().readTree(response).get("result").asText()); + } + + @Test + public void testHandleCommandReturnsJsonRpcErrorOnDispatcherFailure() throws Exception { + IpcService service = Mockito.spy(new IpcService( + new AdminJsonRpcImpl(new CommonParameterExporter()))); + Mockito.doThrow(new IOException("sensitive-detail")) + .when(service).dispatchRequest(Mockito.any(ByteArrayInputStream.class), + Mockito.any(ByteArrayOutputStream.class)); + + String response = service.handleCommand( + "{\"jsonrpc\":\"2.0\",\"method\":\"admin_example\"," + + "\"params\":[\"a\",\"b\"],\"id\":9}"); + JsonNode responseNode = new ObjectMapper().readTree(response); + + Assert.assertEquals("2.0", responseNode.get("jsonrpc").asText()); + Assert.assertEquals(-32603, responseNode.get("error").get("code").asInt()); + Assert.assertEquals("Internal error", responseNode.get("error").get("message").asText()); + Assert.assertEquals(9, responseNode.get("id").asInt()); + Assert.assertFalse(response, response.contains("sensitive-detail")); + Assert.assertFalse(response, response.contains("\n")); } @Test(timeout = 10_000) public void testSocketFileUsesOwnerOnlyPermissions() throws Exception { + assumePosixFileSystem(); CommonParameter parameter = Args.getInstance(); String originalOutputDirectory = parameter.outputDirectory; Path outputDirectory = Files.createTempDirectory(Paths.get("/tmp"), "ipc-permission-test-"); @@ -53,7 +154,7 @@ public void testSocketFileUsesOwnerOnlyPermissions() throws Exception { boolean started = false; try { parameter.outputDirectory = outputDirectory.toString(); - service.innerStart(); + Assert.assertTrue(service.start().get()); started = true; Path socketFile = IpcService.resolveSocketFilePath(parameter, IpcService.getPid()); @@ -62,7 +163,7 @@ public void testSocketFileUsesOwnerOnlyPermissions() throws Exception { Files.getPosixFilePermissions(socketFile)); } finally { if (started) { - service.innerStop(); + Assert.assertTrue(service.stop().get()); } parameter.outputDirectory = originalOutputDirectory; Files.deleteIfExists(outputDirectory); @@ -71,6 +172,7 @@ public void testSocketFileUsesOwnerOnlyPermissions() throws Exception { @Test(timeout = 10_000) public void testHandlesMultipleClientsConcurrently() throws Exception { + assumePosixFileSystem(); CommonParameter parameter = Args.getInstance(); String originalOutputDirectory = parameter.outputDirectory; Path outputDirectory = Files.createTempDirectory(Paths.get("/tmp"), "ipc-multi-client-test-"); @@ -114,6 +216,7 @@ public void testHandlesMultipleClientsConcurrently() throws Exception { @Test(timeout = 10_000) public void testStopClosesActiveClientSocket() throws Exception { + assumePosixFileSystem(); CommonParameter parameter = Args.getInstance(); String originalOutputDirectory = parameter.outputDirectory; Path outputDirectory = Files.createTempDirectory("ipc-test-"); @@ -171,4 +274,9 @@ private void assertSuccessfulResponse(String response, int requestId) { Assert.assertTrue(response, response.contains("\"result\":\"a:b\"")); Assert.assertTrue(response, response.contains("\"id\":" + requestId)); } + + private void assumePosixFileSystem() { + Assume.assumeTrue("IPC requires POSIX file permissions", + FileSystems.getDefault().supportedFileAttributeViews().contains("posix")); + } } From 95828dba3990b802b4263cd36e7289b756303046 Mon Sep 17 00:00:00 2001 From: 317787106 <317787106@qq.com> Date: Fri, 7 Aug 2026 12:32:11 +0800 Subject: [PATCH 05/15] fix bug from claude --- .../tron/common/application/HttpService.java | 12 +- .../java/org/tron/core/config/args/Args.java | 64 +++++++-- .../services/admin/http/AdminRpcServlet.java | 9 +- .../core/services/admin/ipc/IpcService.java | 74 ++++++++-- .../core/services/jsonrpc/JsonRpcMapper.java | 22 +++ .../services/jsonrpc/JsonRpcMediaType.java | 26 ++++ .../core/services/jsonrpc/JsonRpcServlet.java | 15 +- .../common/application/HttpServiceTest.java | 78 +++++++++++ .../org/tron/core/config/args/ArgsTest.java | 43 ++++-- .../admin/http/AdminRpcServletTest.java | 119 ++++++++++++++++ .../services/admin/ipc/IpcServiceTest.java | 130 ++++++++++++++++-- .../org/tron/keystroe/CredentialsTest.java | 33 ----- 12 files changed, 529 insertions(+), 96 deletions(-) create mode 100644 framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcMapper.java create mode 100644 framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcMediaType.java create mode 100644 framework/src/test/java/org/tron/common/application/HttpServiceTest.java create mode 100644 framework/src/test/java/org/tron/core/services/admin/http/AdminRpcServletTest.java delete mode 100644 framework/src/test/java/org/tron/keystroe/CredentialsTest.java diff --git a/framework/src/main/java/org/tron/common/application/HttpService.java b/framework/src/main/java/org/tron/common/application/HttpService.java index 8eaa23102e9..82ce0aff622 100644 --- a/framework/src/main/java/org/tron/common/application/HttpService.java +++ b/framework/src/main/java/org/tron/common/application/HttpService.java @@ -17,7 +17,6 @@ import com.google.common.annotations.VisibleForTesting; import java.io.IOException; -import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.util.concurrent.CompletableFuture; import javax.servlet.RequestDispatcher; @@ -29,6 +28,7 @@ import org.eclipse.jetty.server.ConnectionLimit; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.handler.ErrorHandler; import org.eclipse.jetty.server.handler.SizeLimitHandler; import org.eclipse.jetty.servlet.ServletContextHandler; @@ -80,11 +80,13 @@ public CompletableFuture start() { } protected void initServer() { - if (this.listenAddress == null) { - this.apiServer = new Server(this.port); - } else { - this.apiServer = new Server(new InetSocketAddress(this.listenAddress, this.port)); + this.apiServer = new Server(); + ServerConnector connector = new ServerConnector(this.apiServer); + connector.setPort(this.port); + if (this.listenAddress != null) { + connector.setHost(this.listenAddress); } + this.apiServer.addConnector(connector); int maxHttpConnectNumber = Args.getInstance().getMaxHttpConnectNumber(); if (maxHttpConnectNumber > 0) { this.apiServer.addBean(new ConnectionLimit(maxHttpConnectNumber, this.apiServer)); diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index b2a5061f392..a6846f808a7 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -166,11 +166,8 @@ public static void setParam(final String[] args, final String confFileName) { Args.printHelp(jc); exit(0); } - if (cmd.ipcExecCommand != null && StringUtils.isEmpty(cmd.ipcSocketFile)) { - throw new ParameterException("--exec requires --attach "); - } - if (StringUtils.isNotEmpty(cmd.ipcSocketFile)) { - applyAttachParams(cmd); + List assignedParameters = getAssignedParameters(jc); + if (tryApplyAttachParams(cmd, assignedParameters)) { return; } @@ -183,7 +180,7 @@ public static void setParam(final String[] args, final String confFileName) { applyConfigParams(config); // 3. CLI overrides Config (highest priority, including --es → eventSubscribe) - applyCLIParams(cmd, jc); + applyCLIParams(cmd, assignedParameters); // 4. Apply event config after CLI applyEventConfig(eventConfig); @@ -195,12 +192,56 @@ public static void setParam(final String[] args, final String confFileName) { initLocalWitnesses(config, cmd); } - private static void applyAttachParams(CLIParameter cmd) { + private static List getAssignedParameters(JCommander jc) { + return jc.getParameters().stream() + .filter(ParameterDescription::isAssigned) + .collect(Collectors.toList()); + } + + private static boolean tryApplyAttachParams(CLIParameter cmd, + List assignedParameters) { + boolean attachAssigned = isParameterAssigned(assignedParameters, "ipcSocketFile"); + if (!attachAssigned) { + if (isParameterAssigned(assignedParameters, "ipcExecCommand")) { + throw new ParameterException("--exec requires --attach "); + } + return false; + } + if (StringUtils.isBlank(cmd.ipcSocketFile)) { + throw new ParameterException("--attach requires a non-empty "); + } + + List unsupportedOptions = assignedParameters.stream() + .filter(pd -> !isAttachParameter(pd)) + .map(ParameterDescription::getLongestName) + .collect(Collectors.toList()); + if (!cmd.seedNodes.isEmpty()) { + unsupportedOptions.add("seedNode"); + } + if (!unsupportedOptions.isEmpty()) { + Collections.sort(unsupportedOptions); + throw new ParameterException("--attach cannot be combined with: " + + String.join(", ", unsupportedOptions)); + } ipcSocketFile = cmd.ipcSocketFile; ipcExecCommand = cmd.ipcExecCommand; if (StringUtils.isNotEmpty(cmd.logbackPath)) { PARAMETER.logbackPath = cmd.logbackPath; } + return true; + } + + private static boolean isParameterAssigned(List assignedParameters, + String fieldName) { + return assignedParameters.stream() + .anyMatch(pd -> fieldName.equals(pd.getParameterized().getName())); + } + + private static boolean isAttachParameter(ParameterDescription parameter) { + String fieldName = parameter.getParameterized().getName(); + return "ipcSocketFile".equals(fieldName) + || "ipcExecCommand".equals(fieldName) + || "logbackPath".equals(fieldName); } /** @@ -798,14 +839,13 @@ public static void applyConfigParams( * Apply CLI parameters that were explicitly passed. * Only assigned parameters override Config values. */ - private static void applyCLIParams(CLIParameter cmd, JCommander jc) { - Set assigned = jc.getParameters().stream() - .filter(ParameterDescription::isAssigned) + private static void applyCLIParams(CLIParameter cmd, + List assignedParameters) { + Set assigned = assignedParameters.stream() .map(ParameterDescription::getLongestName) .collect(Collectors.toSet()); - jc.getParameters().stream() - .filter(ParameterDescription::isAssigned) + assignedParameters.stream() .filter(pd -> { try { return CLIParameter.class.getDeclaredField(pd.getParameterized().getName()) diff --git a/framework/src/main/java/org/tron/core/services/admin/http/AdminRpcServlet.java b/framework/src/main/java/org/tron/core/services/admin/http/AdminRpcServlet.java index 71ceb5a5a95..77f1a604288 100644 --- a/framework/src/main/java/org/tron/core/services/admin/http/AdminRpcServlet.java +++ b/framework/src/main/java/org/tron/core/services/admin/http/AdminRpcServlet.java @@ -17,6 +17,8 @@ import org.tron.core.services.admin.AdminJsonRpc; import org.tron.core.services.http.RateLimiterServlet; import org.tron.core.services.jsonrpc.JsonRpcErrorResolver; +import org.tron.core.services.jsonrpc.JsonRpcMapper; +import org.tron.core.services.jsonrpc.JsonRpcMediaType; @Component @Slf4j(topic = "API") @@ -42,7 +44,7 @@ public void init(ServletConfig config) throws ServletException { new Class[] {AdminJsonRpc.class}, true); - rpcServer = new JsonRpcServer(compositeService); + rpcServer = new JsonRpcServer(JsonRpcMapper.create(), compositeService); rpcServer.setErrorResolver(JsonRpcErrorResolver.INSTANCE); HttpStatusCodeProvider httpStatusCodeProvider = new HttpStatusCodeProvider() { @@ -66,6 +68,11 @@ public Integer getJsonRpcCode(int httpStatusCode) { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + if (!JsonRpcMediaType.isSupported(req.getContentType())) { + resp.setStatus(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE); + resp.setContentLength(0); + return; + } rpcServer.handle(req, resp); } } diff --git a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java index d8aa4452686..3c40fdfface 100644 --- a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java +++ b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java @@ -5,15 +5,16 @@ import com.fasterxml.jackson.databind.node.NullNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.googlecode.jsonrpc4j.JsonRpcServer; -import java.io.BufferedReader; +import java.io.BufferedInputStream; import java.io.BufferedWriter; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; -import java.io.InputStreamReader; +import java.io.InputStream; import java.io.OutputStreamWriter; import java.lang.management.ManagementFactory; +import java.net.SocketTimeoutException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.LinkOption; @@ -34,6 +35,7 @@ import org.newsclub.net.unix.AFUNIXServerSocket; import org.newsclub.net.unix.AFUNIXSocket; import org.newsclub.net.unix.AFUNIXSocketAddress; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.tron.common.application.AbstractService; import org.tron.common.es.ExecutorServiceManager; @@ -53,6 +55,8 @@ public class IpcService extends AbstractService { private static final String CLIENT_EXECUTOR_NAME = "admin-ipc-client"; private static final int CLIENT_HANDLER_THREADS = 4; private static final int MAX_PENDING_CLIENTS = 16; + private static final int MAX_REQUEST_SIZE = 4 * 1024 * 1024; + private static final int CLIENT_IDLE_TIMEOUT_MILLIS = 10 * 60 * 1000; private final ExecutorService acceptorExecutor = ExecutorServiceManager.newSingleThreadExecutor(ACCEPTOR_EXECUTOR_NAME, true); @@ -65,10 +69,20 @@ public class IpcService extends AbstractService { private AFUNIXServerSocket unixServerSocket; private Path socketFilePath; private final JsonRpcServer jsonRpcServer; + private final int clientIdleTimeoutMillis; + @Autowired public IpcService(AdminJsonRpc adminJsonRpc) { + this(adminJsonRpc, CLIENT_IDLE_TIMEOUT_MILLIS); + } + + IpcService(AdminJsonRpc adminJsonRpc, int clientIdleTimeoutMillis) { + if (clientIdleTimeoutMillis <= 0) { + throw new IllegalArgumentException("IPC client idle timeout must be positive"); + } enable = isFullNode() && Args.getInstance().isIpcEnable(); jsonRpcServer = new JsonRpcServer(OBJECT_MAPPER, adminJsonRpc, AdminJsonRpc.class); + this.clientIdleTimeoutMillis = clientIdleTimeoutMillis; } @Override @@ -129,6 +143,15 @@ public void innerStart() throws Exception { } private void registerClient(AFUNIXSocket client) { + try { + client.setSoTimeout(clientIdleTimeoutMillis); + } catch (IOException e) { + closeClientSocket(client); + if (isRunning) { + logger.warn("Failed to configure IPC client idle timeout"); + } + return; + } activeClientSockets.add(client); if (!isRunning) { closeAndRemoveClient(client); @@ -154,13 +177,12 @@ private void registerClient(AFUNIXSocket client) { } private void handleClient(AFUNIXSocket client) { - try (BufferedReader reader = new BufferedReader( - new InputStreamReader(client.getInputStream(), StandardCharsets.UTF_8)); + try (BufferedInputStream input = new BufferedInputStream(client.getInputStream()); BufferedWriter writer = new BufferedWriter( new OutputStreamWriter(client.getOutputStream(), StandardCharsets.UTF_8))) { String line; - while ((line = reader.readLine()) != null) { + while ((line = readRequest(input, MAX_REQUEST_SIZE)) != null) { String cmd = line.trim(); logger.debug("Received IPC request"); String response = handleCommand(cmd); @@ -171,6 +193,10 @@ private void handleClient(AFUNIXSocket client) { logger.debug("Sent IPC response"); } } + } catch (SocketTimeoutException e) { + logger.debug("Closing IPC client after {} ms without input", clientIdleTimeoutMillis); + } catch (RequestTooLargeException e) { + logger.warn("IPC request exceeds maximum size of {} bytes", MAX_REQUEST_SIZE); } catch (IOException e) { if (isRunning) { logger.error("Client disconnected {}", client); @@ -178,6 +204,29 @@ private void handleClient(AFUNIXSocket client) { } } + private String readRequest(InputStream input, int maxRequestSize) throws IOException { + ByteArrayOutputStream request = new ByteArrayOutputStream(); + int value; + while ((value = input.read()) != -1) { + if (value == '\n') { + break; + } + if (request.size() >= maxRequestSize) { + throw new RequestTooLargeException(); + } + request.write(value); + } + if (value == -1 && request.size() == 0) { + return null; + } + byte[] bytes = request.toByteArray(); + int length = bytes.length; + if (length > 0 && bytes[length - 1] == '\r') { + length--; + } + return new String(bytes, 0, length, StandardCharsets.UTF_8); + } + String handleCommand(String jsonRequest) { ByteArrayInputStream input = new ByteArrayInputStream(jsonRequest.getBytes(StandardCharsets.UTF_8)); @@ -260,12 +309,12 @@ private void closeAndRemoveClient(AFUNIXSocket client) { activeClientSockets.remove(client); } - static Path resolveSocketFilePath(CommonParameter parameter, String pid) { + private Path resolveSocketFilePath(CommonParameter parameter, String pid) { return Paths.get(parameter.getOutputDirectory(), "java-tron." + pid + ".sock"); } - static void validateOutputDirectory(Path outputDirectory) throws IOException { + private void validateOutputDirectory(Path outputDirectory) throws IOException { if (outputDirectory == null || !Files.isDirectory(outputDirectory)) { throw new TronError("IPC output directory does not exist or is not a directory", ErrCode.API_SERVER_INIT); @@ -277,7 +326,7 @@ static void validateOutputDirectory(Path outputDirectory) throws IOException { } } - static void deleteStaleSocketFile(Path socketFilePath) throws IOException { + private void deleteStaleSocketFile(Path socketFilePath) throws IOException { if (!Files.exists(socketFilePath, LinkOption.NOFOLLOW_LINKS)) { return; } @@ -290,13 +339,18 @@ static void deleteStaleSocketFile(Path socketFilePath) throws IOException { Files.delete(socketFilePath); } - static void setOwnerOnlyPermissions(Path socketFilePath) throws IOException { + private void setOwnerOnlyPermissions(Path socketFilePath) throws IOException { Files.setPosixFilePermissions(socketFilePath, EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE)); } - public static String getPid() { + private String getPid() { String name = ManagementFactory.getRuntimeMXBean().getName(); return name.split("@")[0]; } + + private static final class RequestTooLargeException extends IOException { + + private static final long serialVersionUID = 1L; + } } diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcMapper.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcMapper.java new file mode 100644 index 00000000000..b5bcc8fd4d9 --- /dev/null +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcMapper.java @@ -0,0 +1,22 @@ +package org.tron.core.services.jsonrpc; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.StreamReadConstraints; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.tron.core.Constant; + +public final class JsonRpcMapper { + + private JsonRpcMapper() { + } + + public static ObjectMapper create() { + JsonFactory factory = JsonFactory.builder() + .streamReadConstraints(StreamReadConstraints.builder() + .maxNestingDepth(Constant.MAX_NESTING_DEPTH) + .maxTokenCount(Constant.MAX_TOKEN_COUNT) + .build()) + .build(); + return new ObjectMapper(factory); + } +} diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcMediaType.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcMediaType.java new file mode 100644 index 00000000000..780a08d2f48 --- /dev/null +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcMediaType.java @@ -0,0 +1,26 @@ +package org.tron.core.services.jsonrpc; + +import java.util.Locale; + +public final class JsonRpcMediaType { + + private static final String APPLICATION_JSON = "application/json"; + private static final String APPLICATION_JSON_RPC = "application/json-rpc"; + + private JsonRpcMediaType() { + } + + public static boolean isSupported(String contentType) { + if (contentType == null) { + return false; + } + int parameterSeparator = contentType.indexOf(';'); + String mediaType = (parameterSeparator < 0 + ? contentType : contentType.substring(0, parameterSeparator)) + .trim() + .toLowerCase(Locale.ROOT); + return APPLICATION_JSON.equals(mediaType) + || APPLICATION_JSON_RPC.equals(mediaType) + || mediaType.startsWith("application/") && mediaType.endsWith("+json"); + } +} diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java index ca249da4e5d..249e5372765 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/JsonRpcServlet.java @@ -1,8 +1,6 @@ package org.tron.core.services.jsonrpc; -import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.StreamReadConstraints; import com.fasterxml.jackson.core.exc.StreamConstraintsException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -25,7 +23,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.tron.common.parameter.CommonParameter; -import org.tron.core.Constant; import org.tron.core.services.filter.BufferedResponseWrapper; import org.tron.core.services.filter.CachedBodyRequestWrapper; import org.tron.core.services.http.RateLimiterServlet; @@ -34,17 +31,7 @@ @Slf4j(topic = "API") public class JsonRpcServlet extends RateLimiterServlet { - private static final ObjectMapper MAPPER = buildMapper(); - - private static ObjectMapper buildMapper() { - JsonFactory factory = JsonFactory.builder() - .streamReadConstraints(StreamReadConstraints.builder() - .maxNestingDepth(Constant.MAX_NESTING_DEPTH) - .maxTokenCount(Constant.MAX_TOKEN_COUNT) - .build()) - .build(); - return new ObjectMapper(factory); - } + private static final ObjectMapper MAPPER = JsonRpcMapper.create(); private enum JsonRpcError { PARSE_ERROR(-32700), diff --git a/framework/src/test/java/org/tron/common/application/HttpServiceTest.java b/framework/src/test/java/org/tron/common/application/HttpServiceTest.java new file mode 100644 index 00000000000..7dd6b77613c --- /dev/null +++ b/framework/src/test/java/org/tron/common/application/HttpServiceTest.java @@ -0,0 +1,78 @@ +package org.tron.common.application; + +import java.net.Socket; +import java.util.concurrent.TimeUnit; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.junit.Assert; +import org.junit.Test; + +public class HttpServiceTest { + + @Test + public void testInitServerPreservesLiteralListenAddress() { + TestHttpService service = new TestHttpService("127.0.0.1", 0); + try { + service.initializeServer(); + + Assert.assertEquals("127.0.0.1", service.getConnector().getHost()); + } finally { + service.destroyServer(); + } + } + + @Test + public void testInitServerLeavesHostUnsetForWildcardBinding() { + TestHttpService service = new TestHttpService(null, 0); + try { + service.initializeServer(); + + Assert.assertNull(service.getConnector().getHost()); + } finally { + service.destroyServer(); + } + } + + @Test(timeout = 10_000) + public void testServerBindsConfiguredIpv4Address() throws Exception { + TestHttpService service = new TestHttpService("127.0.0.1", 0); + try { + service.start().get(10, TimeUnit.SECONDS); + + int localPort = service.getConnector().getLocalPort(); + Assert.assertTrue(localPort > 0); + try (Socket socket = new Socket("127.0.0.1", localPort)) { + Assert.assertTrue(socket.isConnected()); + } + } finally { + service.stop().get(10, TimeUnit.SECONDS); + } + } + + private static class TestHttpService extends HttpService { + + TestHttpService(String listenAddress, int port) { + this.listenAddress = listenAddress; + this.port = port; + this.contextPath = "/"; + } + + void initializeServer() { + initServer(); + } + + ServerConnector getConnector() { + return (ServerConnector) apiServer.getConnectors()[0]; + } + + void destroyServer() { + if (apiServer != null) { + apiServer.destroy(); + } + } + + @Override + protected void addServlet(ServletContextHandler context) { + } + } +} diff --git a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java index 9bdf63aa485..f04fdcbd6c9 100644 --- a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java +++ b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java @@ -75,22 +75,49 @@ public void testAttachWithExecParameters() { } @Test - public void testAttachSkipsInvalidNodeConfig() throws Exception { + public void testAttachRejectsNodeConfigOption() { Args.clearParam(); - Path invalidConfig = Files.createTempFile("attach-invalid-config-", ".conf"); - Files.write(invalidConfig, Arrays.asList("node {"), StandardCharsets.UTF_8); try { Args.setParam(new String[] { "--attach", "/tmp/java-tron.sock", - "--config", invalidConfig.toString() + "--config", "config.conf" }, TestConstants.TEST_CONF); + Assert.fail("Expected a node configuration option to be rejected"); + } catch (ParameterException e) { + Assert.assertEquals("--attach cannot be combined with: --config", e.getMessage()); + } finally { + Args.clearParam(); + } + } - Assert.assertEquals("/tmp/java-tron.sock", Args.getIpcSocketFile()); - Assert.assertNull(Args.getNodeConfig()); - Assert.assertEquals("", Args.getConfigFilePath()); + @Test + public void testAttachRejectsEmptySocketPath() { + Args.clearParam(); + try { + Args.setParam(new String[] {"--attach", ""}, TestConstants.TEST_CONF); + Assert.fail("Expected an empty socket path to be rejected"); + } catch (ParameterException e) { + Assert.assertEquals("--attach requires a non-empty ", e.getMessage()); + } finally { + Args.clearParam(); + } + } + + @Test + public void testAttachRejectsOtherNodeOptions() { + Args.clearParam(); + try { + Args.setParam(new String[] { + "--attach", "/tmp/java-tron.sock", + "--keystore-factory", + "seed.example.org:18888" + }, TestConstants.TEST_CONF); + Assert.fail("Expected node startup options to be rejected"); + } catch (ParameterException e) { + Assert.assertEquals( + "--attach cannot be combined with: --keystore-factory, seedNode", e.getMessage()); } finally { Args.clearParam(); - Files.deleteIfExists(invalidConfig); } } diff --git a/framework/src/test/java/org/tron/core/services/admin/http/AdminRpcServletTest.java b/framework/src/test/java/org/tron/core/services/admin/http/AdminRpcServletTest.java new file mode 100644 index 00000000000..73402946619 --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/admin/http/AdminRpcServletTest.java @@ -0,0 +1,119 @@ +package org.tron.core.services.admin.http; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; + +import com.googlecode.jsonrpc4j.JsonRpcInterceptor; +import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.junit.Before; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockServletConfig; +import org.tron.core.Constant; +import org.tron.core.services.admin.AdminJsonRpc; + +public class AdminRpcServletTest { + + private TestableServlet servlet; + + @Before + public void setUp() throws Exception { + servlet = new TestableServlet(); + setField("adminJsonRpc", mock(AdminJsonRpc.class)); + setField("interceptor", mock(JsonRpcInterceptor.class)); + servlet.init(new MockServletConfig()); + } + + @Test + public void excessivelyNestedRequestIsRejected() throws Exception { + StringBuilder request = new StringBuilder(); + for (int i = 0; i <= Constant.MAX_NESTING_DEPTH; i++) { + request.append('['); + } + request.append('0'); + for (int i = 0; i <= Constant.MAX_NESTING_DEPTH; i++) { + request.append(']'); + } + + MockHttpServletResponse response = doPost(request.toString()); + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + assertEquals(0, response.getContentAsByteArray().length); + } + + @Test + public void requestWithTooManyTokensIsRejected() throws Exception { + StringBuilder request = new StringBuilder("{\"params\":["); + for (int i = 0; i < Constant.MAX_TOKEN_COUNT; i++) { + if (i > 0) { + request.append(','); + } + request.append('0'); + } + request.append("]}"); + + MockHttpServletResponse response = doPost(request.toString()); + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + assertEquals(0, response.getContentAsByteArray().length); + } + + @Test + public void nonJsonContentTypeIsRejected() throws Exception { + MockHttpServletResponse response = doPost( + "{\"jsonrpc\":\"2.0\",\"method\":\"admin_example\",\"id\":1}", "text/plain"); + + assertEquals(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE, response.getStatus()); + assertEquals(0, response.getContentAsByteArray().length); + } + + @Test + public void missingContentTypeIsRejected() throws Exception { + MockHttpServletResponse response = doPost( + "{\"jsonrpc\":\"2.0\",\"method\":\"admin_example\",\"id\":1}", null); + + assertEquals(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE, response.getStatus()); + assertEquals(0, response.getContentAsByteArray().length); + } + + @Test + public void jsonContentTypesAreAccepted() throws Exception { + String body = "{\"jsonrpc\":\"2.0\",\"method\":\"admin_example\",\"id\":1}"; + + assertEquals(HttpServletResponse.SC_OK, + doPost(body, "application/json; charset=UTF-8").getStatus()); + assertEquals(HttpServletResponse.SC_OK, + doPost(body, "application/json-rpc").getStatus()); + assertEquals(HttpServletResponse.SC_OK, + doPost(body, "application/vnd.tron+json").getStatus()); + } + + private void setField(String name, Object value) throws Exception { + Field field = AdminRpcServlet.class.getDeclaredField(name); + field.setAccessible(true); + field.set(servlet, value); + } + + private MockHttpServletResponse doPost(String body) throws Exception { + return doPost(body, "application/json"); + } + + private MockHttpServletResponse doPost(String body, String contentType) throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/admin"); + request.setContentType(contentType); + request.setContent(body.getBytes(StandardCharsets.UTF_8)); + MockHttpServletResponse response = new MockHttpServletResponse(); + servlet.callDoPost(request, response); + return response; + } + + private static class TestableServlet extends AdminRpcServlet { + + void callDoPost(HttpServletRequest request, HttpServletResponse response) throws IOException { + doPost(request, response); + } + } +} diff --git a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java index 669a56e93bf..d975d84e406 100644 --- a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java @@ -8,8 +8,11 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; import java.nio.file.FileSystems; import java.nio.file.Files; @@ -33,11 +36,12 @@ public class IpcServiceTest { @Test - public void testResolveSocketFilePathUsesOutputDirectory() { + public void testResolveSocketFilePathUsesOutputDirectory() throws Exception { + IpcService service = newIpcService(); CommonParameter parameter = new CommonParameter(); parameter.outputDirectory = "node-output"; - Path socketFilePath = IpcService.resolveSocketFilePath(parameter, "1234"); + Path socketFilePath = resolveSocketFilePath(service, parameter, "1234"); Assert.assertEquals( Paths.get("node-output", "java-tron.1234.sock"), @@ -45,12 +49,13 @@ public void testResolveSocketFilePathUsesOutputDirectory() { } @Test - public void testValidateOutputDirectoryRejectsMissingDirectory() throws IOException { + public void testValidateOutputDirectoryRejectsMissingDirectory() throws Exception { + IpcService service = newIpcService(); Path outputDirectory = Files.createTempDirectory("ipc-missing-output-test-"); Files.delete(outputDirectory); try { - IpcService.validateOutputDirectory(outputDirectory); + validateOutputDirectory(service, outputDirectory); Assert.fail("Expected a missing output directory to be rejected"); } catch (TronError e) { Assert.assertEquals("IPC output directory does not exist or is not a directory", @@ -59,12 +64,13 @@ public void testValidateOutputDirectoryRejectsMissingDirectory() throws IOExcept } @Test - public void testDeleteStaleSocketFileRejectsRegularFile() throws IOException { + public void testDeleteStaleSocketFileRejectsRegularFile() throws Exception { + IpcService service = newIpcService(); Path outputDirectory = Files.createTempDirectory("ipc-regular-file-test-"); Path socketFile = outputDirectory.resolve("java-tron.1234.sock"); Files.createFile(socketFile); try { - IpcService.deleteStaleSocketFile(socketFile); + deleteStaleSocketFile(service, socketFile); Assert.fail("Expected a regular file to be preserved"); } catch (TronError e) { Assert.assertEquals("Refusing to replace a non-socket IPC endpoint", e.getMessage()); @@ -76,15 +82,16 @@ public void testDeleteStaleSocketFileRejectsRegularFile() throws IOException { } @Test - public void testDeleteStaleSocketFileRejectsSymbolicLink() throws IOException { + public void testDeleteStaleSocketFileRejectsSymbolicLink() throws Exception { assumePosixFileSystem(); + IpcService service = newIpcService(); Path outputDirectory = Files.createTempDirectory("ipc-symbolic-link-test-"); Path targetFile = outputDirectory.resolve("target"); Path socketFile = outputDirectory.resolve("java-tron.1234.sock"); Files.createFile(targetFile); Files.createSymbolicLink(socketFile, targetFile.getFileName()); try { - IpcService.deleteStaleSocketFile(socketFile); + deleteStaleSocketFile(service, socketFile); Assert.fail("Expected a symbolic link to be preserved"); } catch (TronError e) { Assert.assertEquals("Refusing to replace a non-socket IPC endpoint", e.getMessage()); @@ -98,11 +105,12 @@ public void testDeleteStaleSocketFileRejectsSymbolicLink() throws IOException { } @Test - public void testValidateOutputDirectorySupportsPosixPermissions() throws IOException { + public void testValidateOutputDirectorySupportsPosixPermissions() throws Exception { assumePosixFileSystem(); + IpcService service = newIpcService(); Path outputDirectory = Files.createTempDirectory("ipc-posix-output-test-"); try { - IpcService.validateOutputDirectory(outputDirectory); + validateOutputDirectory(service, outputDirectory); } finally { Files.deleteIfExists(outputDirectory); } @@ -143,6 +151,23 @@ public void testHandleCommandReturnsJsonRpcErrorOnDispatcherFailure() throws Exc Assert.assertFalse(response, response.contains("\n")); } + @Test + public void testReadRequestAcceptsMaximumSize() throws Exception { + IpcService service = newIpcService(); + ByteArrayInputStream input = + new ByteArrayInputStream("1234\n".getBytes(StandardCharsets.UTF_8)); + + Assert.assertEquals("1234", readRequest(service, input, 4)); + } + + @Test(expected = IOException.class) + public void testReadRequestRejectsOversizedInputWithoutNewline() throws Exception { + IpcService service = newIpcService(); + ByteArrayInputStream input = new ByteArrayInputStream("12345".getBytes(StandardCharsets.UTF_8)); + + readRequest(service, input, 4); + } + @Test(timeout = 10_000) public void testSocketFileUsesOwnerOnlyPermissions() throws Exception { assumePosixFileSystem(); @@ -157,7 +182,7 @@ public void testSocketFileUsesOwnerOnlyPermissions() throws Exception { Assert.assertTrue(service.start().get()); started = true; - Path socketFile = IpcService.resolveSocketFilePath(parameter, IpcService.getPid()); + Path socketFile = resolveSocketFilePath(service, parameter, getPid(service)); Assert.assertEquals( EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE), Files.getPosixFilePermissions(socketFile)); @@ -184,7 +209,7 @@ public void testHandlesMultipleClientsConcurrently() throws Exception { service.innerStart(); started = true; - File socketFile = IpcService.resolveSocketFilePath(parameter, IpcService.getPid()).toFile(); + File socketFile = resolveSocketFilePath(service, parameter, getPid(service)).toFile(); AFUNIXSocketAddress address = AFUNIXSocketAddress.of(socketFile); try (AFUNIXSocket firstClient = AFUNIXSocket.newInstance(); AFUNIXSocket secondClient = AFUNIXSocket.newInstance()) { @@ -214,6 +239,38 @@ public void testHandlesMultipleClientsConcurrently() throws Exception { } } + @Test(timeout = 10_000) + public void testIdleClientIsDisconnected() throws Exception { + assumePosixFileSystem(); + CommonParameter parameter = Args.getInstance(); + String originalOutputDirectory = parameter.outputDirectory; + Path outputDirectory = Files.createTempDirectory(Paths.get("/tmp"), "ipc-idle-test-"); + IpcService service = new IpcService( + new AdminJsonRpcImpl(new CommonParameterExporter()), 200); + boolean started = false; + try { + parameter.outputDirectory = outputDirectory.toString(); + service.innerStart(); + started = true; + + File socketFile = resolveSocketFilePath(service, parameter, getPid(service)).toFile(); + AFUNIXSocketAddress address = AFUNIXSocketAddress.of(socketFile); + try (AFUNIXSocket client = AFUNIXSocket.newInstance()) { + client.connect(address); + client.setSoTimeout(5_000); + BufferedReader reader = new BufferedReader( + new InputStreamReader(client.getInputStream(), StandardCharsets.UTF_8)); + Assert.assertNull(reader.readLine()); + } + } finally { + if (started) { + service.innerStop(); + } + parameter.outputDirectory = originalOutputDirectory; + Files.deleteIfExists(outputDirectory); + } + } + @Test(timeout = 10_000) public void testStopClosesActiveClientSocket() throws Exception { assumePosixFileSystem(); @@ -228,7 +285,7 @@ public void testStopClosesActiveClientSocket() throws Exception { service.innerStart(); started = true; - File socketFile = IpcService.resolveSocketFilePath(parameter, IpcService.getPid()).toFile(); + File socketFile = resolveSocketFilePath(service, parameter, getPid(service)).toFile(); AFUNIXSocketAddress address = AFUNIXSocketAddress.of(socketFile); try (AFUNIXSocket client = AFUNIXSocket.newInstance()) { client.connect(address); @@ -260,6 +317,53 @@ public void testStopClosesActiveClientSocket() throws Exception { } } + private IpcService newIpcService() { + return new IpcService(new AdminJsonRpcImpl(new CommonParameterExporter())); + } + + private Path resolveSocketFilePath(IpcService service, CommonParameter parameter, String pid) + throws Exception { + return (Path) invokePrivate(service, "resolveSocketFilePath", + new Class[] {CommonParameter.class, String.class}, parameter, pid); + } + + private void validateOutputDirectory(IpcService service, Path outputDirectory) throws Exception { + invokePrivate(service, "validateOutputDirectory", new Class[] {Path.class}, + outputDirectory); + } + + private void deleteStaleSocketFile(IpcService service, Path socketFile) throws Exception { + invokePrivate(service, "deleteStaleSocketFile", new Class[] {Path.class}, socketFile); + } + + private String readRequest(IpcService service, InputStream input, int maxRequestSize) + throws Exception { + return (String) invokePrivate(service, "readRequest", + new Class[] {InputStream.class, int.class}, input, maxRequestSize); + } + + private String getPid(IpcService service) throws Exception { + return (String) invokePrivate(service, "getPid", new Class[0]); + } + + private Object invokePrivate(IpcService service, String methodName, Class[] parameterTypes, + Object... arguments) throws Exception { + Method method = IpcService.class.getDeclaredMethod(methodName, parameterTypes); + method.setAccessible(true); + try { + return method.invoke(service, arguments); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof Exception) { + throw (Exception) cause; + } + if (cause instanceof Error) { + throw (Error) cause; + } + throw new IllegalStateException(cause); + } + } + private String sendRequest(BufferedWriter writer, BufferedReader reader, int requestId) throws IOException { writer.write("{\"jsonrpc\":\"2.0\",\"method\":\"admin_example\"," diff --git a/framework/src/test/java/org/tron/keystroe/CredentialsTest.java b/framework/src/test/java/org/tron/keystroe/CredentialsTest.java deleted file mode 100644 index 2642129e00a..00000000000 --- a/framework/src/test/java/org/tron/keystroe/CredentialsTest.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.tron.keystroe; - -import org.junit.Assert; -import org.junit.Test; -import org.mockito.Mockito; -import org.tron.common.crypto.SignInterface; -import org.tron.keystore.Credentials; - -public class CredentialsTest { - - @Test - public void test_equality() { - Object aObject = new Object(); - SignInterface si = Mockito.mock(SignInterface.class); - SignInterface si2 = Mockito.mock(SignInterface.class); - SignInterface si3 = Mockito.mock(SignInterface.class); - byte[] address = "TQhZ7W1RudxFdzJMw6FvMnujPxrS6sFfmj".getBytes(); - byte[] address2 = "TNCmcTdyrYKMtmE1KU2itzeCX76jGm5Not".getBytes(); - Mockito.when(si.getAddress()).thenReturn(address); - Mockito.when(si2.getAddress()).thenReturn(address); - Mockito.when(si3.getAddress()).thenReturn(address2); - Credentials aCredential = Credentials.create(si); - Assert.assertFalse(aObject.equals(aCredential)); - Assert.assertFalse(aCredential.equals(aObject)); - Assert.assertFalse(aCredential.equals(null)); - Credentials anotherCredential = Credentials.create(si); - Assert.assertTrue(aCredential.equals(anotherCredential)); - Credentials aCredential2 = Credentials.create(si2); - Assert.assertTrue(aCredential.equals(anotherCredential)); - Credentials aCredential3 = Credentials.create(si3); - Assert.assertFalse(aCredential.equals(aCredential3)); - } -} From 95437ebb571a5437dc500ca015c6cc4db815f32f Mon Sep 17 00:00:00 2001 From: 317787106 <317787106@qq.com> Date: Fri, 7 Aug 2026 14:17:42 +0800 Subject: [PATCH 06/15] use isLoopbackListenAddress instead of 127.0.0.1; add sleep for ipcservice; add setErrorResolver for admin jsonrpc --- .../admin/CommonParameterExporter.java | 2 +- .../admin/http/AdminRpcHttpService.java | 13 ++++- .../core/services/admin/ipc/IpcClient.java | 51 +++++++++++------ .../core/services/admin/ipc/IpcService.java | 39 +++++++------ .../admin/CommonParameterExporterTest.java | 32 ++++++----- .../admin/http/AdminRpcHttpServiceTest.java | 21 +++++++ .../services/admin/ipc/IpcClientTest.java | 35 ++++++++++-- .../services/admin/ipc/IpcServiceTest.java | 56 ++++++++++--------- 8 files changed, 165 insertions(+), 84 deletions(-) create mode 100644 framework/src/test/java/org/tron/core/services/admin/http/AdminRpcHttpServiceTest.java diff --git a/framework/src/main/java/org/tron/core/services/admin/CommonParameterExporter.java b/framework/src/main/java/org/tron/core/services/admin/CommonParameterExporter.java index 5329813c6c3..1aa877f77e3 100644 --- a/framework/src/main/java/org/tron/core/services/admin/CommonParameterExporter.java +++ b/framework/src/main/java/org/tron/core/services/admin/CommonParameterExporter.java @@ -38,7 +38,7 @@ public Map export() { Map export(CommonParameter parameter) { ObjectNode snapshot = OBJECT_MAPPER.createObjectNode(); - Field[] fields = CommonParameter.class.getFields(); + Field[] fields = parameter.getClass().getFields(); Arrays.sort(fields, Comparator.comparing(Field::getName)); for (Field field : fields) { if (!field.isAnnotationPresent(Exportable.class)) { diff --git a/framework/src/main/java/org/tron/core/services/admin/http/AdminRpcHttpService.java b/framework/src/main/java/org/tron/core/services/admin/http/AdminRpcHttpService.java index e9c52ad7be7..d4c6cc5f33e 100644 --- a/framework/src/main/java/org/tron/core/services/admin/http/AdminRpcHttpService.java +++ b/framework/src/main/java/org/tron/core/services/admin/http/AdminRpcHttpService.java @@ -1,5 +1,6 @@ package org.tron.core.services.admin.http; +import java.net.InetAddress; import java.util.EnumSet; import javax.servlet.DispatcherType; import lombok.extern.slf4j.Slf4j; @@ -10,8 +11,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.tron.common.application.HttpService; -import org.tron.core.Constant; import org.tron.core.config.args.Args; +import org.tron.core.config.args.InetUtil; import org.tron.core.services.filter.HttpInterceptor; @Component @@ -30,13 +31,21 @@ public AdminRpcHttpService() { @Override public void innerStart() throws Exception { - if (enable && !Constant.LOCAL_HOST.equals(listenAddress)) { + if (enable && !isLoopbackListenAddress(listenAddress)) { logger.warn("Admin RPC is enabled on {} and may be accessible remotely. " + "Restrict access to trusted networks.", listenAddress); } super.innerStart(); } + static boolean isLoopbackListenAddress(String listenAddress) { + if (listenAddress == null) { + return false; + } + InetAddress address = InetUtil.resolveInetAddress(listenAddress); + return address != null && address.isLoopbackAddress(); + } + @Override protected void addServlet(ServletContextHandler context) { context.addServlet(new ServletHolder(adminRpcServlet), "/admin"); diff --git a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcClient.java b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcClient.java index 2b16691e269..e05e73b58b6 100644 --- a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcClient.java +++ b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcClient.java @@ -58,8 +58,12 @@ public class IpcClient { private int requestId = 0; public IpcClient(String socketFilePath) { + this(socketFilePath, AdminJsonRpc.class); + } + + IpcClient(String socketFilePath, Class adminApi) { this.socketFilePath = socketFilePath; - this.adminCommands = collectAdminCommands(); + this.adminCommands = collectAdminCommands(adminApi); } public static int start(String socketFilePath) { @@ -77,27 +81,32 @@ public static int start(String socketFilePath, String execCommand) { } } - private Map collectAdminCommands() { + private Map collectAdminCommands(Class adminApi) { Map commands = new HashMap<>(); - for (Method method : AdminJsonRpc.class.getDeclaredMethods()) { + for (Method method : adminApi.getDeclaredMethods()) { JsonRpcMethod rpcMethod = method.getAnnotation(JsonRpcMethod.class); if (rpcMethod == null || rpcMethod.value() == null) { continue; } List parameterNames = new ArrayList<>(); + List parameterTypes = new ArrayList<>(); Annotation[][] paramAnnotations = method.getParameterAnnotations(); - for (Annotation[] annotations : paramAnnotations) { - for (Annotation anno : annotations) { + Type[] genericParameterTypes = method.getGenericParameterTypes(); + for (int i = 0; i < paramAnnotations.length; i++) { + String parameterName = null; + for (Annotation anno : paramAnnotations[i]) { if (anno instanceof JsonRpcParam) { - parameterNames.add(((JsonRpcParam) anno).value()); + parameterName = ((JsonRpcParam) anno).value(); + break; } } - } - - List parameterTypes = new ArrayList<>(); - for (Type type : method.getGenericParameterTypes()) { - parameterTypes.add(OBJECT_MAPPER.getTypeFactory().constructType(type)); + if (StringUtils.isEmpty(parameterName)) { + throw new IllegalStateException("Missing @JsonRpcParam on " + method.getName() + + " parameter " + i); + } + parameterNames.add(parameterName); + parameterTypes.add(OBJECT_MAPPER.getTypeFactory().constructType(genericParameterTypes[i])); } AdminCommand command = new AdminCommand(rpcMethod.value(), parameterNames, parameterTypes); @@ -124,8 +133,7 @@ List buildHelpLines() { helpLines.add(formatUsage(adminCommands.get(command.toLowerCase(Locale.ROOT)))); } helpLines.add("help [command]"); - helpLines.add("exit"); - helpLines.add("quit"); + helpLines.add("exit/quit"); return helpLines; } @@ -258,7 +266,7 @@ private void outputResponse(final Socket socket, LineReader reader, AtomicBoolea private LineReader createLineReader(Terminal terminal) { Completer commandCompleter = - new IpcCommandCompleter(adminCommands.keySet().toArray(new String[0])); + new IpcCommandCompleter(getCompletionCommandNames()); ArgumentCompleter completer = new ArgumentCompleter( commandCompleter, NullCompleter.INSTANCE @@ -275,6 +283,13 @@ private LineReader createLineReader(Terminal terminal) { .build(); } + String[] getCompletionCommandNames() { + return adminCommands.values().stream() + .map(command -> command.name) + .sorted() + .toArray(String[]::new); + } + void printWelcome(File socketFile) { System.out.println("Welcome to the java-tron admin console."); System.out.println("Client: java-tron/" + Version.getVersion()); @@ -338,11 +353,15 @@ private void notifyDisconnected(AtomicBoolean connected, LineReader reader) { } List parseCommandLine(String commandLine) { - if (commandLine == null || commandLine.trim().isEmpty()) { + if (commandLine == null) { + return Collections.emptyList(); + } + String normalizedCommandLine = commandLine.trim(); + if (normalizedCommandLine.isEmpty()) { return Collections.emptyList(); } ParsedLine parsedLine = commandParser.parse( - commandLine, commandLine.length(), Parser.ParseContext.ACCEPT_LINE); + normalizedCommandLine, normalizedCommandLine.length(), Parser.ParseContext.ACCEPT_LINE); return parsedLine.words(); } diff --git a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java index 3c40fdfface..9640be17939 100644 --- a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java +++ b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java @@ -45,6 +45,7 @@ import org.tron.core.exception.TronError; import org.tron.core.exception.TronError.ErrCode; import org.tron.core.services.admin.AdminJsonRpc; +import org.tron.core.services.jsonrpc.JsonRpcErrorResolver; @Component @Slf4j(topic = "API") @@ -53,36 +54,28 @@ public class IpcService extends AbstractService { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private static final String ACCEPTOR_EXECUTOR_NAME = "admin-ipc-acceptor"; private static final String CLIENT_EXECUTOR_NAME = "admin-ipc-client"; - private static final int CLIENT_HANDLER_THREADS = 4; - private static final int MAX_PENDING_CLIENTS = 16; private static final int MAX_REQUEST_SIZE = 4 * 1024 * 1024; private static final int CLIENT_IDLE_TIMEOUT_MILLIS = 10 * 60 * 1000; private final ExecutorService acceptorExecutor = ExecutorServiceManager.newSingleThreadExecutor(ACCEPTOR_EXECUTOR_NAME, true); private final ExecutorService clientExecutor = - ExecutorServiceManager.newThreadPoolExecutor( - CLIENT_HANDLER_THREADS, CLIENT_HANDLER_THREADS, 0L, TimeUnit.MILLISECONDS, - new ArrayBlockingQueue<>(MAX_PENDING_CLIENTS), CLIENT_EXECUTOR_NAME, true); - private final Set activeClientSockets = ConcurrentHashMap.newKeySet(); + ExecutorServiceManager.newThreadPoolExecutor(4, 16, 0L, TimeUnit.MILLISECONDS, + new ArrayBlockingQueue<>(16), CLIENT_EXECUTOR_NAME, true); + private volatile boolean isRunning = true; private AFUNIXServerSocket unixServerSocket; private Path socketFilePath; private final JsonRpcServer jsonRpcServer; - private final int clientIdleTimeoutMillis; + + private final Set activeClientSockets = ConcurrentHashMap.newKeySet(); @Autowired public IpcService(AdminJsonRpc adminJsonRpc) { - this(adminJsonRpc, CLIENT_IDLE_TIMEOUT_MILLIS); - } - - IpcService(AdminJsonRpc adminJsonRpc, int clientIdleTimeoutMillis) { - if (clientIdleTimeoutMillis <= 0) { - throw new IllegalArgumentException("IPC client idle timeout must be positive"); - } enable = isFullNode() && Args.getInstance().isIpcEnable(); jsonRpcServer = new JsonRpcServer(OBJECT_MAPPER, adminJsonRpc, AdminJsonRpc.class); - this.clientIdleTimeoutMillis = clientIdleTimeoutMillis; + jsonRpcServer.setErrorResolver(JsonRpcErrorResolver.INSTANCE); + jsonRpcServer.setShouldLogInvocationErrors(false); } @Override @@ -130,12 +123,18 @@ public void innerStart() throws Exception { try { registerClient(unixServerSocket.accept()); } catch (Throwable throwable) { - if (isRunning) { - logger.error("Handle IPC request error", throwable); - } ExitManager.findTronError(throwable).ifPresent(e -> { throw e; }); + if (isRunning) { + logger.error("Handle IPC request error", throwable); + try { + TimeUnit.MILLISECONDS.sleep(1_000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } } } }; @@ -144,7 +143,7 @@ public void innerStart() throws Exception { private void registerClient(AFUNIXSocket client) { try { - client.setSoTimeout(clientIdleTimeoutMillis); + client.setSoTimeout(CLIENT_IDLE_TIMEOUT_MILLIS); } catch (IOException e) { closeClientSocket(client); if (isRunning) { @@ -194,7 +193,7 @@ private void handleClient(AFUNIXSocket client) { } } } catch (SocketTimeoutException e) { - logger.debug("Closing IPC client after {} ms without input", clientIdleTimeoutMillis); + logger.debug("Closing IPC client after {} ms without input", CLIENT_IDLE_TIMEOUT_MILLIS); } catch (RequestTooLargeException e) { logger.warn("IPC request exceeds maximum size of {} bytes", MAX_REQUEST_SIZE); } catch (IOException e) { diff --git a/framework/src/test/java/org/tron/core/services/admin/CommonParameterExporterTest.java b/framework/src/test/java/org/tron/core/services/admin/CommonParameterExporterTest.java index dfa82fbd5f2..d91edc4afe5 100644 --- a/framework/src/test/java/org/tron/core/services/admin/CommonParameterExporterTest.java +++ b/framework/src/test/java/org/tron/core/services/admin/CommonParameterExporterTest.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; -import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -29,26 +28,28 @@ public void testExportIncludesOnlyAnnotatedFieldsAndLiveValues() { CommonParameter parameter = new CommonParameter(); parameter.rpcPort = 150051; parameter.chainId = "runtime-chain"; + parameter.allowCreationOfContracts = 1L; Map snapshot = exporter.export(parameter); - for (Field field : CommonParameter.class.getFields()) { - if (field.isAnnotationPresent(Exportable.class)) { - Assert.assertTrue("Missing exportable runtime parameter: " + field.getName(), - snapshot.containsKey(field.getName())); - } else { - Assert.assertFalse("Unexpected runtime parameter: " + field.getName(), - snapshot.containsKey(field.getName())); - } - } Assert.assertEquals(150051, snapshot.get("rpcPort")); Assert.assertEquals("runtime-chain", snapshot.get("chainId")); + Assert.assertFalse(snapshot.containsKey("allowCreationOfContracts")); parameter.rpcPort = 250051; snapshot = exporter.export(parameter); Assert.assertEquals(250051, snapshot.get("rpcPort")); } + @Test + public void testExportUsesRuntimeParameterType() { + ExtendedCommonParameter parameter = new ExtendedCommonParameter(); + + Map snapshot = exporter.export(parameter); + + Assert.assertEquals("runtime-value", snapshot.get("runtimeOnly")); + } + @Test public void testExportSortsTopLevelKeysByFieldName() { Map snapshot = exporter.export(new CommonParameter()); @@ -151,10 +152,11 @@ public void testExportIncludesApprovedConfigurationsAndExcludesSecrets() { publishConfig.get("accessKeyId")); Assert.assertEquals(CommonParameterExporter.REDACTED_VALUE, publishConfig.get("accessKeySecret")); - Assert.assertTrue(snapshot.containsKey("rateLimiterInitialization")); - Assert.assertTrue(snapshot.containsKey("rocksDBCustomSettings")); - Assert.assertTrue(snapshot.containsKey("seedNode")); - Assert.assertTrue(snapshot.containsKey("eventFilter")); - Assert.assertTrue(snapshot.containsKey("shutdownBlockTime")); + } + + private static class ExtendedCommonParameter extends CommonParameter { + + @Exportable + public String runtimeOnly = "runtime-value"; } } diff --git a/framework/src/test/java/org/tron/core/services/admin/http/AdminRpcHttpServiceTest.java b/framework/src/test/java/org/tron/core/services/admin/http/AdminRpcHttpServiceTest.java new file mode 100644 index 00000000000..34cdab6539d --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/admin/http/AdminRpcHttpServiceTest.java @@ -0,0 +1,21 @@ +package org.tron.core.services.admin.http; + +import org.junit.Assert; +import org.junit.Test; + +public class AdminRpcHttpServiceTest { + + @Test + public void testLoopbackListenAddressesAreRecognized() { + Assert.assertTrue(AdminRpcHttpService.isLoopbackListenAddress("127.0.0.1")); + Assert.assertTrue(AdminRpcHttpService.isLoopbackListenAddress("::1")); + Assert.assertTrue(AdminRpcHttpService.isLoopbackListenAddress("localhost")); + } + + @Test + public void testNonLoopbackListenAddressesAreRejected() { + Assert.assertFalse(AdminRpcHttpService.isLoopbackListenAddress(null)); + Assert.assertFalse(AdminRpcHttpService.isLoopbackListenAddress("0.0.0.0")); + Assert.assertFalse(AdminRpcHttpService.isLoopbackListenAddress("192.0.2.1")); + } +} diff --git a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcClientTest.java b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcClientTest.java index f75180c3c5c..d65530d2963 100644 --- a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcClientTest.java +++ b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcClientTest.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.type.TypeFactory; +import com.googlecode.jsonrpc4j.JsonRpcMethod; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; @@ -35,16 +36,34 @@ public void testBuildHelpLinesIncludesSortedCommandParameters() { "admin_example ", "admin_getRuntimeParameters", "help [command]", - "exit", - "quit"), client.buildHelpLines()); + "exit/quit"), client.buildHelpLines()); + } + + @Test + public void testCompletionUsesCanonicalMethodNames() { + IpcClient client = new IpcClient("unused"); + + Assert.assertArrayEquals(new String[] { + "admin_example", "admin_getRuntimeParameters" + }, client.getCompletionCommandNames()); + } + + @Test + public void testMissingJsonRpcParameterAnnotationIsRejected() { + try { + new IpcClient("unused", MissingParameterAnnotationApi.class); + Assert.fail("Expected an unannotated JSON-RPC parameter to be rejected"); + } catch (IllegalStateException e) { + Assert.assertEquals("Missing @JsonRpcParam on invalid parameter 0", e.getMessage()); + } } @Test public void testParseCommandLinePreservesQuotedArguments() { IpcClient client = new IpcClient("unused"); - Assert.assertEquals(Arrays.asList("admin_example", "hello world", "second value"), - client.parseCommandLine("admin_example \"hello world\" 'second value'")); + Assert.assertEquals(Arrays.asList("admin_example", " hello world ", "second value"), + client.parseCommandLine(" \tadmin_example \" hello world \" 'second value' ")); } @Test @@ -107,7 +126,7 @@ public void testExecSendsCommandAndPrintsFormattedResult() throws Exception { try { System.setOut(capturedOut); Assert.assertEquals(IpcClient.EXIT_SUCCESS, - new IpcClient("unused").runExec(socket, "admin_example \"hello world\" b")); + new IpcClient("unused").runExec(socket, " admin_example \"hello world\" b \t")); } finally { System.setOut(originalOut); capturedOut.close(); @@ -280,4 +299,10 @@ public void testSessionPrintsResponseAndExitsWhenServerDisconnects() throws Exce Mockito.verify(reader).printAbove("Disconnected from server."); } } + + private interface MissingParameterAnnotationApi { + + @JsonRpcMethod("admin_invalid") + String invalid(String value); + } } diff --git a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java index d975d84e406..45dda293734 100644 --- a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java @@ -30,6 +30,8 @@ import org.tron.common.parameter.CommonParameter; import org.tron.core.config.args.Args; import org.tron.core.exception.TronError; +import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; +import org.tron.core.services.admin.AdminJsonRpc; import org.tron.core.services.admin.AdminJsonRpcImpl; import org.tron.core.services.admin.CommonParameterExporter; @@ -151,6 +153,24 @@ public void testHandleCommandReturnsJsonRpcErrorOnDispatcherFailure() throws Exc Assert.assertFalse(response, response.contains("\n")); } + @Test + public void testHandleCommandUsesAnnotatedErrorResolver() throws Exception { + AdminJsonRpc adminJsonRpc = Mockito.mock(AdminJsonRpc.class); + Mockito.when(adminJsonRpc.adminExample("a", "b")) + .thenThrow(new JsonRpcInvalidParamsException("Invalid admin parameters")); + IpcService service = new IpcService(adminJsonRpc); + + String response = service.handleCommand( + "{\"jsonrpc\":\"2.0\",\"method\":\"admin_example\"," + + "\"params\":[\"a\",\"b\"],\"id\":10}"); + JsonNode responseNode = new ObjectMapper().readTree(response); + + Assert.assertEquals(-32602, responseNode.get("error").get("code").asInt()); + Assert.assertEquals("Invalid admin parameters", + responseNode.get("error").get("message").asText()); + Assert.assertEquals(10, responseNode.get("id").asInt()); + } + @Test public void testReadRequestAcceptsMaximumSize() throws Exception { IpcService service = newIpcService(); @@ -240,34 +260,16 @@ public void testHandlesMultipleClientsConcurrently() throws Exception { } @Test(timeout = 10_000) - public void testIdleClientIsDisconnected() throws Exception { - assumePosixFileSystem(); - CommonParameter parameter = Args.getInstance(); - String originalOutputDirectory = parameter.outputDirectory; - Path outputDirectory = Files.createTempDirectory(Paths.get("/tmp"), "ipc-idle-test-"); - IpcService service = new IpcService( - new AdminJsonRpcImpl(new CommonParameterExporter()), 200); - boolean started = false; + public void testRegisterClientUsesDefaultIdleTimeout() throws Exception { + IpcService service = newIpcService(); + AFUNIXSocket client = Mockito.mock(AFUNIXSocket.class); + Mockito.doThrow(new IOException("closed")).when(client).getInputStream(); try { - parameter.outputDirectory = outputDirectory.toString(); - service.innerStart(); - started = true; + registerClient(service, client); - File socketFile = resolveSocketFilePath(service, parameter, getPid(service)).toFile(); - AFUNIXSocketAddress address = AFUNIXSocketAddress.of(socketFile); - try (AFUNIXSocket client = AFUNIXSocket.newInstance()) { - client.connect(address); - client.setSoTimeout(5_000); - BufferedReader reader = new BufferedReader( - new InputStreamReader(client.getInputStream(), StandardCharsets.UTF_8)); - Assert.assertNull(reader.readLine()); - } + Mockito.verify(client).setSoTimeout(10 * 60 * 1000); } finally { - if (started) { - service.innerStop(); - } - parameter.outputDirectory = originalOutputDirectory; - Files.deleteIfExists(outputDirectory); + service.innerStop(); } } @@ -342,6 +344,10 @@ private String readRequest(IpcService service, InputStream input, int maxRequest new Class[] {InputStream.class, int.class}, input, maxRequestSize); } + private void registerClient(IpcService service, AFUNIXSocket client) throws Exception { + invokePrivate(service, "registerClient", new Class[] {AFUNIXSocket.class}, client); + } + private String getPid(IpcService service) throws Exception { return (String) invokePrivate(service, "getPid", new Class[0]); } From 75b4061e1af05b6ffe207900eace4731aef5df9a Mon Sep 17 00:00:00 2001 From: 317787106 <317787106@qq.com> Date: Fri, 7 Aug 2026 16:00:31 +0800 Subject: [PATCH 07/15] move IpcClient.start ahead of LogService.load; write sock file in output-directory, else write in /tmp if path is too long; IPC client exist will not interpurt input --- .../core/services/admin/ipc/IpcClient.java | 14 +- .../core/services/admin/ipc/IpcService.java | 21 ++- .../main/java/org/tron/program/FullNode.java | 16 +-- .../services/admin/ipc/IpcClientTest.java | 31 +++++ .../services/admin/ipc/IpcServiceTest.java | 122 +++++++++++++++--- .../java/org/tron/program/FullNodeTest.java | 43 ++++++ 6 files changed, 211 insertions(+), 36 deletions(-) create mode 100644 framework/src/test/java/org/tron/program/FullNodeTest.java diff --git a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcClient.java b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcClient.java index e05e73b58b6..6a7f561ce7f 100644 --- a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcClient.java +++ b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcClient.java @@ -256,8 +256,9 @@ private void outputResponse(final Socket socket, LineReader reader, AtomicBoolea } catch (IOException e) { logger.debug("IPC response stream closed: {}", e.getMessage()); } finally { - notifyDisconnected(connected, reader); - inputThread.interrupt(); + if (notifyDisconnected(connected, reader)) { + inputThread.interrupt(); + } } }, "admin-ipc-client-reader"); readerThread.setDaemon(true); @@ -303,8 +304,9 @@ void printWelcome(File socketFile) { private void inputRequest(final Socket socket, LineReader reader, AtomicBoolean connected) { String prompt = "> "; - try (BufferedWriter serverWriter = new BufferedWriter( - new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8))) { + try { + BufferedWriter serverWriter = new BufferedWriter( + new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8)); while (connected.get()) { try { List commandWords = parseCommandLine(reader.readLine(prompt)); @@ -346,10 +348,12 @@ private void inputRequest(final Socket socket, LineReader reader, AtomicBoolean } } - private void notifyDisconnected(AtomicBoolean connected, LineReader reader) { + private boolean notifyDisconnected(AtomicBoolean connected, LineReader reader) { if (connected.compareAndSet(true, false)) { reader.printAbove("Disconnected from server."); + return true; } + return false; } List parseCommandLine(String commandLine) { diff --git a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java index 9640be17939..3d5a047d83f 100644 --- a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java +++ b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java @@ -57,6 +57,11 @@ public class IpcService extends AbstractService { private static final int MAX_REQUEST_SIZE = 4 * 1024 * 1024; private static final int CLIENT_IDLE_TIMEOUT_MILLIS = 10 * 60 * 1000; + // macOS/Linux sun_path buffers are 104/108 bytes. Reserve one byte for the terminating null + // and three bytes of portability margin below the smaller macOS limit. + private static final int MAX_SOCKET_PATH_BYTES = 100; + private static final Path FALLBACK_SOCKET_DIRECTORY = Paths.get("/tmp"); + private final ExecutorService acceptorExecutor = ExecutorServiceManager.newSingleThreadExecutor(ACCEPTOR_EXECUTOR_NAME, true); private final ExecutorService clientExecutor = @@ -309,8 +314,20 @@ private void closeAndRemoveClient(AFUNIXSocket client) { } private Path resolveSocketFilePath(CommonParameter parameter, String pid) { - return Paths.get(parameter.getOutputDirectory(), - "java-tron." + pid + ".sock"); + String socketFileName = "java-tron." + pid + ".sock"; + Path outputSocketFile = Paths.get(parameter.getOutputDirectory(), socketFileName) + .toAbsolutePath().normalize(); + if (getSocketPathLength(outputSocketFile) <= MAX_SOCKET_PATH_BYTES) { + return outputSocketFile; + } + + logger.warn("IPC socket path under output directory exceeds {} bytes; using /tmp instead", + MAX_SOCKET_PATH_BYTES); + return FALLBACK_SOCKET_DIRECTORY.resolve(socketFileName).toAbsolutePath().normalize(); + } + + private int getSocketPathLength(Path socketFile) { + return socketFile.toString().getBytes(AFUNIXSocketAddress.addressCharset()).length; } private void validateOutputDirectory(Path outputDirectory) throws IOException { diff --git a/framework/src/main/java/org/tron/program/FullNode.java b/framework/src/main/java/org/tron/program/FullNode.java index e0a5b4d8b2f..b5a0d17979a 100644 --- a/framework/src/main/java/org/tron/program/FullNode.java +++ b/framework/src/main/java/org/tron/program/FullNode.java @@ -26,14 +26,6 @@ public static void main(String[] args) { ExitManager.initExceptionHandler(); checkJdkVersion(); Args.setParam(args, "config.conf"); - CommonParameter parameter = Args.getInstance(); - - LogService.load(parameter.getLogbackPath()); - - if (parameter.isKeystoreFactory()) { - KeystoreFactory.start(); - return; - } if (StringUtils.isNotEmpty(Args.getIpcSocketFile())) { int exitCode = IpcClient.start(Args.getIpcSocketFile(), Args.getIpcExecCommand()); if (exitCode != 0) { @@ -41,6 +33,14 @@ public static void main(String[] args) { } return; } + + CommonParameter parameter = Args.getInstance(); + LogService.load(parameter.getLogbackPath()); + + if (parameter.isKeystoreFactory()) { + KeystoreFactory.start(); + return; + } if (parameter.isSolidityNode()) { logger.info("Solidity node is running."); if (StringUtils.isEmpty(parameter.getTrustNodeAddr())) { diff --git a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcClientTest.java b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcClientTest.java index d65530d2963..83ea41dd3a3 100644 --- a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcClientTest.java +++ b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcClientTest.java @@ -17,6 +17,7 @@ import java.util.Arrays; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import org.jline.reader.LineReader; import org.jline.reader.UserInterruptException; @@ -300,6 +301,36 @@ public void testSessionPrintsResponseAndExitsWhenServerDisconnects() throws Exce } } + @Test(timeout = 10_000) + public void testSessionExitDoesNotInterruptInputThread() throws Exception { + LineReader reader = Mockito.mock(LineReader.class); + Mockito.when(reader.readLine("> ")).thenReturn("exit"); + + try (ServerSocket serverSocket = new ServerSocket(0); + Socket clientSocket = new Socket("127.0.0.1", serverSocket.getLocalPort()); + Socket serverConnection = serverSocket.accept()) { + IpcClient client = new IpcClient("unused"); + AtomicReference failure = new AtomicReference<>(); + AtomicBoolean interrupted = new AtomicBoolean(true); + Thread sessionThread = new Thread(() -> { + try { + client.runSession(clientSocket, reader); + interrupted.set(Thread.currentThread().isInterrupted()); + } catch (Throwable throwable) { + failure.set(throwable); + } + }, "ipc-client-clean-exit-test-session"); + sessionThread.setDaemon(true); + sessionThread.start(); + sessionThread.join(5_000); + + Assert.assertFalse("IPC client did not exit after the exit command", sessionThread.isAlive()); + Assert.assertNull("IPC client session failed", failure.get()); + Assert.assertFalse("Clean IPC client exit left the thread interrupted", interrupted.get()); + Mockito.verify(reader, Mockito.never()).printAbove("Disconnected from server."); + } + } + private interface MissingParameterAnnotationApi { @JsonRpcMethod("admin_invalid") diff --git a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java index 45dda293734..99902f60957 100644 --- a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java @@ -6,7 +6,6 @@ import java.io.BufferedWriter; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -46,10 +45,38 @@ public void testResolveSocketFilePathUsesOutputDirectory() throws Exception { Path socketFilePath = resolveSocketFilePath(service, parameter, "1234"); Assert.assertEquals( - Paths.get("node-output", "java-tron.1234.sock"), + Paths.get("node-output", "java-tron.1234.sock").toAbsolutePath().normalize(), socketFilePath); } + @Test + public void testResolveSocketFilePathFallsBackToTmpForLongOutputPath() throws Exception { + IpcService service = newIpcService(); + CommonParameter parameter = new CommonParameter(); + parameter.outputDirectory = Paths.get("/tmp", + "a-very-long-output-directory-name-that-makes-the-resulting-unix-domain-socket-path-" + + "exceed-the-portable-limit").toString(); + + Path socketFilePath = resolveSocketFilePath(service, parameter, "1234"); + + Assert.assertEquals(Paths.get("/tmp", "java-tron.1234.sock"), socketFilePath); + } + + @Test + public void testResolveSocketFilePathCountsEncodedBytes() throws Exception { + IpcService service = newIpcService(); + CommonParameter parameter = new CommonParameter(); + StringBuilder outputDirectory = new StringBuilder("/tmp/"); + for (int i = 0; i < 40; i++) { + outputDirectory.append("目"); + } + parameter.outputDirectory = outputDirectory.toString(); + + Path socketFilePath = resolveSocketFilePath(service, parameter, "1234"); + + Assert.assertEquals(Paths.get("/tmp", "java-tron.1234.sock"), socketFilePath); + } + @Test public void testValidateOutputDirectoryRejectsMissingDirectory() throws Exception { IpcService service = newIpcService(); @@ -197,21 +224,19 @@ public void testSocketFileUsesOwnerOnlyPermissions() throws Exception { IpcService service = new IpcService( new AdminJsonRpcImpl(new CommonParameterExporter())); boolean started = false; + Path socketFile = null; try { parameter.outputDirectory = outputDirectory.toString(); Assert.assertTrue(service.start().get()); started = true; - Path socketFile = resolveSocketFilePath(service, parameter, getPid(service)); + socketFile = resolveSocketFilePath(service, parameter, getPid(service)); Assert.assertEquals( EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE), Files.getPosixFilePermissions(socketFile)); } finally { - if (started) { - Assert.assertTrue(service.stop().get()); - } - parameter.outputDirectory = originalOutputDirectory; - Files.deleteIfExists(outputDirectory); + cleanupIpcService(service, started, parameter, originalOutputDirectory, socketFile, + outputDirectory); } } @@ -224,13 +249,14 @@ public void testHandlesMultipleClientsConcurrently() throws Exception { IpcService service = new IpcService( new AdminJsonRpcImpl(new CommonParameterExporter())); boolean started = false; + Path socketFile = null; try { parameter.outputDirectory = outputDirectory.toString(); service.innerStart(); started = true; - File socketFile = resolveSocketFilePath(service, parameter, getPid(service)).toFile(); - AFUNIXSocketAddress address = AFUNIXSocketAddress.of(socketFile); + socketFile = resolveSocketFilePath(service, parameter, getPid(service)); + AFUNIXSocketAddress address = AFUNIXSocketAddress.of(socketFile.toFile()); try (AFUNIXSocket firstClient = AFUNIXSocket.newInstance(); AFUNIXSocket secondClient = AFUNIXSocket.newInstance()) { firstClient.connect(address); @@ -251,11 +277,8 @@ public void testHandlesMultipleClientsConcurrently() throws Exception { assertSuccessfulResponse(sendRequest(secondWriter, secondReader, 3), 3); } } finally { - if (started) { - service.innerStop(); - } - parameter.outputDirectory = originalOutputDirectory; - Files.deleteIfExists(outputDirectory); + cleanupIpcService(service, started, parameter, originalOutputDirectory, socketFile, + outputDirectory); } } @@ -282,13 +305,14 @@ public void testStopClosesActiveClientSocket() throws Exception { IpcService service = new IpcService( new AdminJsonRpcImpl(new CommonParameterExporter())); boolean started = false; + Path socketFile = null; try { parameter.outputDirectory = outputDirectory.toString(); service.innerStart(); started = true; - File socketFile = resolveSocketFilePath(service, parameter, getPid(service)).toFile(); - AFUNIXSocketAddress address = AFUNIXSocketAddress.of(socketFile); + socketFile = resolveSocketFilePath(service, parameter, getPid(service)); + AFUNIXSocketAddress address = AFUNIXSocketAddress.of(socketFile.toFile()); try (AFUNIXSocket client = AFUNIXSocket.newInstance()) { client.connect(address); client.setSoTimeout(5_000); @@ -311,16 +335,72 @@ public void testStopClosesActiveClientSocket() throws Exception { } } } finally { - if (started) { + cleanupIpcService(service, started, parameter, originalOutputDirectory, socketFile, + outputDirectory); + } + } + + @Test + public void testCleanupRestoresOutputDirectoryWhenStopFails() throws Exception { + CommonParameter parameter = new CommonParameter(); + String originalOutputDirectory = parameter.outputDirectory; + Path outputDirectory = Files.createTempDirectory("ipc-cleanup-test-"); + Path socketFile = Files.createFile(outputDirectory.resolve("java-tron.1234.sock")); + parameter.outputDirectory = outputDirectory.toString(); + IpcService service = Mockito.mock(IpcService.class); + Mockito.doThrow(new IOException("stop failed")).when(service).innerStop(); + + try { + cleanupIpcService(service, true, parameter, originalOutputDirectory, socketFile, + outputDirectory); + Assert.fail("Expected the stop failure to be preserved"); + } catch (IOException e) { + Assert.assertEquals("stop failed", e.getMessage()); + } + + Assert.assertEquals(originalOutputDirectory, parameter.outputDirectory); + Assert.assertFalse(Files.exists(socketFile)); + Assert.assertFalse(Files.exists(outputDirectory)); + } + + private IpcService newIpcService() { + return new IpcService(new AdminJsonRpcImpl(new CommonParameterExporter())); + } + + private void cleanupIpcService(IpcService service, boolean started, CommonParameter parameter, + String originalOutputDirectory, Path socketFile, Path outputDirectory) throws Exception { + parameter.outputDirectory = originalOutputDirectory; + Exception failure = null; + if (started) { + try { service.innerStop(); + } catch (Exception e) { + failure = e; + } + } + try { + if (socketFile != null) { + Files.deleteIfExists(socketFile); } - parameter.outputDirectory = originalOutputDirectory; + } catch (IOException e) { + failure = mergeCleanupFailure(failure, e); + } + try { Files.deleteIfExists(outputDirectory); + } catch (IOException e) { + failure = mergeCleanupFailure(failure, e); + } + if (failure != null) { + throw failure; } } - private IpcService newIpcService() { - return new IpcService(new AdminJsonRpcImpl(new CommonParameterExporter())); + private Exception mergeCleanupFailure(Exception failure, IOException cleanupFailure) { + if (failure == null) { + return cleanupFailure; + } + failure.addSuppressed(cleanupFailure); + return failure; } private Path resolveSocketFilePath(IpcService service, CommonParameter parameter, String pid) diff --git a/framework/src/test/java/org/tron/program/FullNodeTest.java b/framework/src/test/java/org/tron/program/FullNodeTest.java new file mode 100644 index 00000000000..1f364bb4a99 --- /dev/null +++ b/framework/src/test/java/org/tron/program/FullNodeTest.java @@ -0,0 +1,43 @@ +package org.tron.program; + +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.tron.common.arch.Arch; +import org.tron.common.exit.ExitManager; +import org.tron.common.log.LogService; +import org.tron.core.config.args.Args; +import org.tron.core.services.admin.ipc.IpcClient; + +public class FullNodeTest { + + @Test + public void testAttachStartsBeforeLogService() { + AtomicBoolean logServiceLoaded = new AtomicBoolean(false); + try (MockedStatic exitManager = Mockito.mockStatic(ExitManager.class); + MockedStatic arch = Mockito.mockStatic(Arch.class); + MockedStatic args = Mockito.mockStatic(Args.class); + MockedStatic logService = Mockito.mockStatic(LogService.class); + MockedStatic ipcClient = Mockito.mockStatic(IpcClient.class)) { + args.when(Args::getIpcSocketFile).thenReturn("/tmp/java-tron.sock"); + args.when(Args::getIpcExecCommand).thenReturn(null); + logService.when(() -> LogService.load(Mockito.anyString())) + .thenAnswer(invocation -> { + logServiceLoaded.set(true); + return null; + }); + ipcClient.when(() -> IpcClient.start("/tmp/java-tron.sock", null)) + .thenAnswer(invocation -> { + Assert.assertFalse("Attach initialized node logging", logServiceLoaded.get()); + return 0; + }); + + FullNode.main(new String[] {"--attach", "/tmp/java-tron.sock"}); + + ipcClient.verify(() -> IpcClient.start("/tmp/java-tron.sock", null)); + logService.verifyNoInteractions(); + } + } +} From 96964b5c792f1817abcc8e43269fa21b4fe7dc90 Mon Sep 17 00:00:00 2001 From: 317787106 <317787106@qq.com> Date: Fri, 7 Aug 2026 17:17:54 +0800 Subject: [PATCH 08/15] fix the UNAVAILABLE of p2pConfig when export it --- .../tron/core/config/args/DynamicArgs.java | 34 ++++--- .../org/tron/core/net/TronNetService.java | 16 ++-- .../admin/CommonParameterExporter.java | 88 ++++++++++++++++++- .../core/config/args/DynamicArgsTest.java | 40 +++++++++ .../admin/CommonParameterExporterTest.java | 59 +++++++++++++ 5 files changed, 216 insertions(+), 21 deletions(-) diff --git a/framework/src/main/java/org/tron/core/config/args/DynamicArgs.java b/framework/src/main/java/org/tron/core/config/args/DynamicArgs.java index 5a9923b16c9..bdd6431e5c5 100644 --- a/framework/src/main/java/org/tron/core/config/args/DynamicArgs.java +++ b/framework/src/main/java/org/tron/core/config/args/DynamicArgs.java @@ -4,7 +4,9 @@ import java.io.File; import java.net.InetAddress; import java.net.InetSocketAddress; +import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import javax.annotation.PostConstruct; @@ -68,30 +70,36 @@ public void reload() { updateTrustNodes(nodeConfig); } + /** + * Builds the complete active-node list before replacing the shared reference atomically. Using + * {@code clear()} followed by {@code addAll()} would let concurrent readers observe a transient + * empty or partially updated list. + */ private void updateActiveNodes(NodeConfig nodeConfig) { List newActiveNodes = Args.filterInetSocketAddress(nodeConfig.getActive(), true); parameter.setActiveNodes(newActiveNodes); - List activeNodes = TronNetService.getP2pConfig().getActiveNodes(); - activeNodes.clear(); - activeNodes.addAll(newActiveNodes); - logger.debug("p2p active nodes : {}", - TronNetService.getP2pConfig().getActiveNodes().toString()); + List activeNodes = new CopyOnWriteArrayList<>(newActiveNodes); + TronNetService.getP2pConfig().setActiveNodes(activeNodes); + logger.debug("p2p active nodes : {}", activeNodes); } + /** + * Builds the complete trust-node list before replacing the shared reference atomically, so + * concurrent configuration exports and network readers see either the old or the new snapshot. + */ private void updateTrustNodes(NodeConfig nodeConfig) { - List newPassiveNodes = new java.util.ArrayList<>(); + List newPassiveNodes = new ArrayList<>(); for (InetSocketAddress sa : Args.filterInetSocketAddress(nodeConfig.getPassive(), false)) { newPassiveNodes.add(sa.getAddress()); } parameter.setPassiveNodes(newPassiveNodes); - List trustNodes = TronNetService.getP2pConfig().getTrustNodes(); - trustNodes.clear(); - trustNodes.addAll(newPassiveNodes); - parameter.getActiveNodes().forEach(n -> trustNodes.add(n.getAddress())); - parameter.getFastForwardNodes().forEach(f -> trustNodes.add(f.getAddress())); - logger.debug("p2p trust nodes : {}", - TronNetService.getP2pConfig().getTrustNodes().toString()); + List newTrustNodes = new ArrayList<>(newPassiveNodes); + parameter.getActiveNodes().forEach(n -> newTrustNodes.add(n.getAddress())); + parameter.getFastForwardNodes().forEach(f -> newTrustNodes.add(f.getAddress())); + List trustNodes = new CopyOnWriteArrayList<>(newTrustNodes); + TronNetService.getP2pConfig().setTrustNodes(trustNodes); + logger.debug("p2p trust nodes : {}", trustNodes); } @PreDestroy diff --git a/framework/src/main/java/org/tron/core/net/TronNetService.java b/framework/src/main/java/org/tron/core/net/TronNetService.java index 8b97c8d9f4d..b0551f17c4a 100644 --- a/framework/src/main/java/org/tron/core/net/TronNetService.java +++ b/framework/src/main/java/org/tron/core/net/TronNetService.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -164,10 +165,15 @@ private P2pConfig updateConfig(P2pConfig config) { seeds.addAll(nodePersistService.dbRead()); logger.debug("Seed InetSocketAddress: {}", seeds); config.getSeedNodes().addAll(seeds); - config.getActiveNodes().addAll(parameter.getActiveNodes()); - config.getTrustNodes().addAll(parameter.getPassiveNodes()); - config.getActiveNodes().forEach(n -> config.getTrustNodes().add(n.getAddress())); - parameter.getFastForwardNodes().forEach(f -> config.getTrustNodes().add(f.getAddress())); + // These lists are read by configuration exporters and network services while dynamic reloads + // and relay tasks may replace or modify them. Snapshot-based copy-on-write lists prevent + // transient empty/partial views and ConcurrentModificationException during iteration. + List activeNodes = new CopyOnWriteArrayList<>(parameter.getActiveNodes()); + config.setActiveNodes(activeNodes); + List trustNodes = new CopyOnWriteArrayList<>(parameter.getPassiveNodes()); + activeNodes.forEach(n -> trustNodes.add(n.getAddress())); + parameter.getFastForwardNodes().forEach(f -> trustNodes.add(f.getAddress())); + config.setTrustNodes(trustNodes); int maxConnections = parameter.getMaxConnections(); int minConnections = parameter.getMinConnections(); int minActiveConnections = parameter.getMinActiveConnections(); @@ -204,4 +210,4 @@ private P2pConfig updateConfig(P2pConfig config) { } return config; } -} \ No newline at end of file +} diff --git a/framework/src/main/java/org/tron/core/services/admin/CommonParameterExporter.java b/framework/src/main/java/org/tron/core/services/admin/CommonParameterExporter.java index 1aa877f77e3..8c900dddb8e 100644 --- a/framework/src/main/java/org/tron/core/services/admin/CommonParameterExporter.java +++ b/framework/src/main/java/org/tron/core/services/admin/CommonParameterExporter.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import java.lang.reflect.Field; import java.lang.reflect.Modifier; +import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.Iterator; @@ -14,10 +15,14 @@ import java.util.Locale; import java.util.Map; import java.util.Map.Entry; +import java.util.TreeMap; import lombok.extern.slf4j.Slf4j; +import org.iq80.leveldb.Options; import org.springframework.stereotype.Component; import org.tron.common.parameter.CommonParameter; import org.tron.common.parameter.Exportable; +import org.tron.common.utils.Property; +import org.tron.core.config.args.Storage; @Component @Slf4j(topic = "API") @@ -55,10 +60,10 @@ Map export(CommonParameter parameter) { try { Object target = Modifier.isStatic(field.getModifiers()) ? null : parameter; - JsonNode value = OBJECT_MAPPER.valueToTree(field.get(target)); + JsonNode value = snapshotValue(field.get(target)); snapshot.set(fieldName, sanitize(value)); - } catch (IllegalAccessException | IllegalArgumentException e) { - logger.warn("Unable to export runtime parameter {}", fieldName); + } catch (IllegalAccessException | RuntimeException e) { + logExportFailure(fieldName, e); snapshot.put(fieldName, UNAVAILABLE_VALUE); } } @@ -66,6 +71,83 @@ Map export(CommonParameter parameter) { new TypeReference>() { }); } + private JsonNode snapshotValue(Object value) { + if (value instanceof Storage) { + return snapshotStorage((Storage) value); + } + return OBJECT_MAPPER.valueToTree(value); + } + + /** + * Builds an explicit snapshot because {@link Storage} contains runtime collaborators such as + * LevelDB {@link Options} that are not regular Jackson beans. Serializing the live object can + * fail and would also expose newly added third-party fields without an explicit review. + */ + private ObjectNode snapshotStorage(Storage storage) { + ObjectNode snapshot = OBJECT_MAPPER.createObjectNode(); + snapshot.put("dbDirectory", storage.getDbDirectory()); + snapshot.put("dbEngine", storage.getDbEngine()); + snapshot.put("dbSync", storage.isDbSync()); + snapshot.put("maxFlushCount", storage.getMaxFlushCount()); + snapshot.put("contractParseSwitch", storage.isContractParseSwitch()); + snapshot.put("transactionHistorySwitch", storage.getTransactionHistorySwitch()); + snapshot.put("checkpointVersion", storage.getCheckpointVersion()); + snapshot.put("checkpointSync", storage.isCheckpointSync()); + snapshot.put("estimatedBlockTransactions", storage.getEstimatedBlockTransactions()); + snapshot.put("txCacheInitOptimization", storage.isTxCacheInitOptimization()); + snapshot.set("cacheDbs", OBJECT_MAPPER.valueToTree( + new ArrayList<>(storage.getCacheDbs()))); + + Map propertyMap = storage.getPropertyMap(); + if (propertyMap == null) { + snapshot.putNull("propertyMap"); + return snapshot; + } + ObjectNode properties = snapshot.putObject("propertyMap"); + for (Entry entry : new TreeMap<>(propertyMap).entrySet()) { + try { + properties.set(entry.getKey(), snapshotStorageProperty(entry.getValue())); + } catch (RuntimeException e) { + logExportFailure("storage.propertyMap entry", e); + properties.put(entry.getKey(), UNAVAILABLE_VALUE); + } + } + return snapshot; + } + + /** + * Copies only stable property and database-option values so one unsupported runtime object does + * not make the entire storage section unavailable or expand the exported surface implicitly. + */ + private ObjectNode snapshotStorageProperty(Property property) { + ObjectNode snapshot = OBJECT_MAPPER.createObjectNode(); + snapshot.put("name", property.getName()); + snapshot.put("path", property.getPath()); + Options options = property.getDbOptions(); + if (options == null) { + snapshot.putNull("dbOptions"); + return snapshot; + } + ObjectNode optionSnapshot = snapshot.putObject("dbOptions"); + optionSnapshot.put("createIfMissing", options.createIfMissing()); + optionSnapshot.put("errorIfExists", options.errorIfExists()); + optionSnapshot.put("writeBufferSize", options.writeBufferSize()); + optionSnapshot.put("maxOpenFiles", options.maxOpenFiles()); + optionSnapshot.put("blockRestartInterval", options.blockRestartInterval()); + optionSnapshot.put("blockSize", options.blockSize()); + optionSnapshot.put("compressionType", options.compressionType().name()); + optionSnapshot.put("verifyChecksums", options.verifyChecksums()); + optionSnapshot.put("cacheSize", options.cacheSize()); + optionSnapshot.put("paranoidChecks", options.paranoidChecks()); + return snapshot; + } + + private void logExportFailure(String fieldName, Exception exception) { + logger.warn("Unable to export runtime parameter {}: {}", fieldName, + exception.getClass().getSimpleName()); + logger.debug("Runtime parameter export failure for " + fieldName, exception); + } + JsonNode sanitize(JsonNode value) { if (value == null || value.isNull() || value.isValueNode()) { return value; diff --git a/framework/src/test/java/org/tron/core/config/args/DynamicArgsTest.java b/framework/src/test/java/org/tron/core/config/args/DynamicArgsTest.java index 733c862e6a4..1e3a9ffe48b 100644 --- a/framework/src/test/java/org/tron/core/config/args/DynamicArgsTest.java +++ b/framework/src/test/java/org/tron/core/config/args/DynamicArgsTest.java @@ -1,6 +1,13 @@ package org.tron.core.config.args; import java.io.File; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import org.junit.Assert; import org.junit.Test; import org.tron.common.BaseMethodTest; @@ -55,4 +62,37 @@ public void start() { dynamicArgs.close(); } + + @Test + public void testReloadReplacesP2pNodeListsAtomically() { + CommonParameter parameter = Args.getInstance(); + List originalActiveNodes = parameter.getActiveNodes(); + List originalPassiveNodes = parameter.getPassiveNodes(); + List originalFastForwardNodes = parameter.getFastForwardNodes(); + TronNetService tronNetService = context.getBean(TronNetService.class); + P2pConfig originalP2pConfig = TronNetService.getP2pConfig(); + P2pConfig p2pConfig = new P2pConfig(); + ReflectUtils.setFieldValue(tronNetService, "p2pConfig", p2pConfig); + parameter.fastForwardNodes = new ArrayList<>(); + + NodeConfig nodeConfig = new NodeConfig(); + nodeConfig.setActive(Collections.singletonList("192.0.2.1:18889")); + nodeConfig.setPassive(Arrays.asList("127.0.0.2:18888", "127.0.0.3:18888")); + try { + ReflectUtils.invokeMethod(dynamicArgs, "updateActiveNodes", + new Class[] {NodeConfig.class}, nodeConfig); + ReflectUtils.invokeMethod(dynamicArgs, "updateTrustNodes", + new Class[] {NodeConfig.class}, nodeConfig); + + Assert.assertTrue(p2pConfig.getActiveNodes() instanceof CopyOnWriteArrayList); + Assert.assertTrue(p2pConfig.getTrustNodes() instanceof CopyOnWriteArrayList); + Assert.assertEquals(1, p2pConfig.getActiveNodes().size()); + Assert.assertEquals(3, p2pConfig.getTrustNodes().size()); + } finally { + parameter.setActiveNodes(originalActiveNodes); + parameter.setPassiveNodes(originalPassiveNodes); + parameter.fastForwardNodes = originalFastForwardNodes; + ReflectUtils.setFieldValue(tronNetService, "p2pConfig", originalP2pConfig); + } + } } diff --git a/framework/src/test/java/org/tron/core/services/admin/CommonParameterExporterTest.java b/framework/src/test/java/org/tron/core/services/admin/CommonParameterExporterTest.java index d91edc4afe5..0f338351d02 100644 --- a/framework/src/test/java/org/tron/core/services/admin/CommonParameterExporterTest.java +++ b/framework/src/test/java/org/tron/core/services/admin/CommonParameterExporterTest.java @@ -8,12 +8,15 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import org.iq80.leveldb.Options; import org.junit.Assert; import org.junit.Test; import org.tron.common.args.GenesisBlock; import org.tron.common.logsfilter.EventPluginConfig; import org.tron.common.parameter.CommonParameter; import org.tron.common.parameter.Exportable; +import org.tron.common.utils.Property; +import org.tron.common.utils.ReflectUtils; import org.tron.core.config.args.Storage; import org.tron.p2p.P2pConfig; import org.tron.p2p.dns.update.PublishConfig; @@ -154,6 +157,62 @@ public void testExportIncludesApprovedConfigurationsAndExcludesSecrets() { publishConfig.get("accessKeySecret")); } + @Test + public void testExportStorageWithDatabaseOptions() { + CommonParameter parameter = new CommonParameter(); + Storage storage = new Storage(); + storage.setDbDirectory("database"); + storage.setDbEngine("LEVELDB"); + Property property = new Property(); + property.setName("account"); + property.setPath("account-data"); + property.setDbOptions(new Options() + .createIfMissing(true) + .cacheSize(4_096L) + .writeBufferSize(8_192) + .maxOpenFiles(128) + .blockSize(1_024)); + ReflectUtils.setFieldValue(storage, "propertyMap", + Collections.singletonMap("account", property)); + parameter.storage = storage; + + Map snapshot = exporter.export(parameter); + + Map storageSnapshot = (Map) snapshot.get("storage"); + Assert.assertEquals("database", storageSnapshot.get("dbDirectory")); + Map propertyMap = (Map) storageSnapshot.get("propertyMap"); + Map propertySnapshot = (Map) propertyMap.get("account"); + Assert.assertEquals("account-data", propertySnapshot.get("path")); + Map optionSnapshot = (Map) propertySnapshot.get("dbOptions"); + Assert.assertEquals(4_096L, optionSnapshot.get("cacheSize")); + Assert.assertEquals(8_192, optionSnapshot.get("writeBufferSize")); + Assert.assertEquals(128, optionSnapshot.get("maxOpenFiles")); + Assert.assertEquals(1_024, optionSnapshot.get("blockSize")); + } + + @Test + public void testStoragePropertyFailureDoesNotHideStorage() { + CommonParameter parameter = new CommonParameter(); + Storage storage = new Storage(); + storage.setDbDirectory("database"); + Property property = new Property() { + @Override + public Options getDbOptions() { + throw new IllegalStateException("unavailable options"); + } + }; + ReflectUtils.setFieldValue(storage, "propertyMap", + Collections.singletonMap("broken", property)); + parameter.storage = storage; + + Map snapshot = exporter.export(parameter); + + Map storageSnapshot = (Map) snapshot.get("storage"); + Assert.assertEquals("database", storageSnapshot.get("dbDirectory")); + Map propertyMap = (Map) storageSnapshot.get("propertyMap"); + Assert.assertEquals("[UNAVAILABLE]", propertyMap.get("broken")); + } + private static class ExtendedCommonParameter extends CommonParameter { @Exportable From 573160916b315e143a01d59eadc77cbc436f7adc Mon Sep 17 00:00:00 2001 From: 317787106 <317787106@qq.com> Date: Fri, 7 Aug 2026 17:29:35 +0800 Subject: [PATCH 09/15] simply IpcService --- .../java/org/tron/core/services/admin/ipc/IpcService.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java index 3d5a047d83f..7aee3fcbec2 100644 --- a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java +++ b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java @@ -54,7 +54,7 @@ public class IpcService extends AbstractService { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private static final String ACCEPTOR_EXECUTOR_NAME = "admin-ipc-acceptor"; private static final String CLIENT_EXECUTOR_NAME = "admin-ipc-client"; - private static final int MAX_REQUEST_SIZE = 4 * 1024 * 1024; + private static final int MAX_REQUEST_SIZE = 4 * 1024 * 1024; //same as HttpService.maxRequestSize private static final int CLIENT_IDLE_TIMEOUT_MILLIS = 10 * 60 * 1000; // macOS/Linux sun_path buffers are 104/108 bytes. Reserve one byte for the terminating null @@ -186,7 +186,7 @@ private void handleClient(AFUNIXSocket client) { new OutputStreamWriter(client.getOutputStream(), StandardCharsets.UTF_8))) { String line; - while ((line = readRequest(input, MAX_REQUEST_SIZE)) != null) { + while ((line = readRequest(input)) != null) { String cmd = line.trim(); logger.debug("Received IPC request"); String response = handleCommand(cmd); @@ -208,14 +208,14 @@ private void handleClient(AFUNIXSocket client) { } } - private String readRequest(InputStream input, int maxRequestSize) throws IOException { + private String readRequest(InputStream input) throws IOException { ByteArrayOutputStream request = new ByteArrayOutputStream(); int value; while ((value = input.read()) != -1) { if (value == '\n') { break; } - if (request.size() >= maxRequestSize) { + if (request.size() >= MAX_REQUEST_SIZE) { throw new RequestTooLargeException(); } request.write(value); From a709dd6a3388780b71333841a84ab177f483c6f6 Mon Sep 17 00:00:00 2001 From: 317787106 <317787106@qq.com> Date: Fri, 7 Aug 2026 17:43:46 +0800 Subject: [PATCH 10/15] refactor(api): split runtime parameters into follow-up --- .../common/parameter/CommonParameter.java | 155 ------------ .../org/tron/common/parameter/Exportable.java | 18 -- .../tron/core/config/args/DynamicArgs.java | 34 ++- .../org/tron/core/net/TronNetService.java | 16 +- .../core/services/admin/AdminJsonRpc.java | 4 - .../core/services/admin/AdminJsonRpcImpl.java | 13 -- .../admin/CommonParameterExporter.java | 197 ---------------- .../org/tron/core/config/args/ArgsTest.java | 2 +- .../core/config/args/DynamicArgsTest.java | 40 ---- .../admin/CommonParameterExporterTest.java | 221 ------------------ .../services/admin/ipc/IpcClientTest.java | 3 +- .../services/admin/ipc/IpcServiceTest.java | 40 ++-- 12 files changed, 45 insertions(+), 698 deletions(-) delete mode 100644 common/src/main/java/org/tron/common/parameter/Exportable.java delete mode 100644 framework/src/main/java/org/tron/core/services/admin/CommonParameterExporter.java delete mode 100644 framework/src/test/java/org/tron/core/services/admin/CommonParameterExporterTest.java diff --git a/common/src/main/java/org/tron/common/parameter/CommonParameter.java b/common/src/main/java/org/tron/common/parameter/CommonParameter.java index 694deb0b2dd..f307ffe96f9 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -40,44 +40,34 @@ public class CommonParameter { // when the energy-limit governance proposal is activated. // Legacy: should belong to VMConfig, not here. @Setter - @Exportable public static boolean ENERGY_LIMIT_HARD_FORK = false; // -- Startup parameters -- @Getter - @Exportable public String outputDirectory = "output-directory"; @Getter - @Exportable public String logbackPath = ""; // -- Flags (CLI + Config) -- @Getter @Setter - @Exportable public boolean witness = false; @Getter @Setter - @Exportable public boolean supportConstant = false; @Getter @Setter - @Exportable public long maxEnergyLimitForConstant = 100_000_000L; @Getter @Setter - @Exportable public int lruCacheSize = 500; @Getter @Setter - @Exportable public boolean debug = false; @Getter @Setter - @Exportable public double minTimeRatio = 0.0; @Getter @Setter - @Exportable public double maxTimeRatio = calcMaxTimeRatio(); /** * Max TVM execution time (ms) for constant calls — covers @@ -90,245 +80,188 @@ public class CommonParameter { */ @Getter @Setter - @Exportable public long constantCallTimeoutMs = 0L; @Getter @Setter - @Exportable public boolean saveInternalTx; @Getter @Setter - @Exportable public boolean saveFeaturedInternalTx; @Getter @Setter - @Exportable public boolean saveCancelAllUnfreezeV2Details; @Getter @Setter - @Exportable public int longRunningTime = 10; @Getter @Setter - @Exportable public int maxHttpConnectNumber = 50; @Getter public List seedNodes = new ArrayList<>(); @Getter - @Exportable public boolean fastForward = false; // -- Network / P2P -- @Getter @Setter - @Exportable public String chainId; @Getter @Setter - @Exportable public boolean needSyncCheck; @Getter @Setter - @Exportable public boolean nodeDiscoveryEnable; @Getter @Setter - @Exportable public boolean nodeDiscoveryPersist; @Getter @Setter - @Exportable public boolean nodeEffectiveCheckEnable; @Getter @Setter - @Exportable public int fetchBlockTimeout; @Getter @Setter - @Exportable public int maxConnections = 30; // from clearParam(), consistent with mainnet.conf @Getter @Setter - @Exportable public int minConnections = 8; // from clearParam(), consistent with mainnet.conf @Getter @Setter - @Exportable public int minActiveConnections = 3; // from clearParam(), consistent with mainnet.conf @Getter @Setter - @Exportable public int maxConnectionsWithSameIp = 2; // from clearParam(), consistent with mainnet.conf @Getter @Setter - @Exportable public int maxTps; // clearParam: 1000 @Getter @Setter - @Exportable public int maxBlockInvPerSecond = 10; // default: 10 block inv hashes/s per peer @Getter @Setter - @Exportable public int minParticipationRate; @Getter - @Exportable public P2pConfig p2pConfig; @Getter @Setter - @Exportable public int nodeListenPort; @Getter @Setter - @Exportable public String nodeLanIp; @Getter @Setter - @Exportable public String nodeExternalIp; @Getter @Setter - @Exportable public int nodeP2pVersion; @Getter @Setter - @Exportable public boolean nodeEnableIpv6 = false; @Getter @Setter - @Exportable public List dnsTreeUrls; // clearParam: new ArrayList<>() @Getter @Setter public PublishConfig dnsPublishConfig; @Getter @Setter - @Exportable public long syncFetchBatchNum; // clearParam: 2000 @Getter @Setter - @Exportable public int maxPendingBlockSize; // If you are running a solidity node for java tron, // this flag is set to true @Getter @Setter - @Exportable public boolean solidityNode = false; // If you are running KeystoreFactory, // this flag is set to true @Getter @Setter - @Exportable public boolean keystoreFactory = false; // -- RPC / HTTP -- @Getter @Setter - @Exportable public int rpcPort; @Getter @Setter - @Exportable public int rpcOnSolidityPort; @Getter @Setter - @Exportable public int fullNodeHttpPort; @Getter @Setter - @Exportable public int solidityHttpPort; @Getter @Setter - @Exportable public int jsonRpcHttpFullNodePort; @Getter @Setter - @Exportable public int jsonRpcHttpSolidityPort; @Getter @Setter - @Exportable public int jsonRpcHttpPBFTPort; @Getter @Setter - @Exportable public int rpcThreadNum; @Getter @Setter - @Exportable public int solidityThreads; @Getter @Setter - @Exportable public int maxConcurrentCallsPerConnection; @Getter @Setter - @Exportable public int flowControlWindow; @Getter @Setter - @Exportable public int rpcMaxRstStream; @Getter @Setter - @Exportable public int rpcSecondsPerWindow; @Getter @Setter - @Exportable public long maxConnectionIdleInMillis; @Getter @Setter - @Exportable public int blockProducedTimeOut; @Getter @Setter - @Exportable public long netMaxTrxPerSecond; @Getter @Setter - @Exportable public long maxConnectionAgeInMillis; // Refers to RPC (gRPC) max message size; see httpMaxMessageSize / jsonRpcMaxMessageSize // below for the HTTP / JSON-RPC counterparts. @Getter @Setter - @Exportable public int maxMessageSize; @Getter @Setter - @Exportable public long httpMaxMessageSize; @Getter @Setter - @Exportable public long jsonRpcMaxMessageSize; @Getter @Setter - @Exportable public int maxHeaderListSize; @Getter @Setter - @Exportable public boolean isRpcReflectionServiceEnable; @Getter @Setter - @Exportable public int validateSignThreadNum; @Getter @Setter - @Exportable public long maintenanceTimeInterval; @Getter @Setter - @Exportable public long proposalExpireTime; @Getter @Setter - @Exportable public int checkFrozenTime; // clearParam: 1 // -- Committee parameters -- @@ -359,70 +292,54 @@ public class CommonParameter { @Getter @Setter - @Exportable public String trustNodeAddr; // clearParam: "" @Getter @Setter - @Exportable public boolean walletExtensionApi; @Getter @Setter - @Exportable public boolean estimateEnergy; @Getter @Setter - @Exportable public int estimateEnergyMaxRetry = 3; // from clearParam(), consistent with mainnet.conf @Getter @Setter - @Exportable public int backupPriority; @Getter @Setter - @Exportable public int backupPort; @Getter @Setter - @Exportable public int keepAliveInterval; @Getter @Setter - @Exportable public List backupMembers; @Getter @Setter - @Exportable public boolean isOpenFullTcpDisconnect; @Getter @Setter - @Exportable public int inactiveThreshold = 600; // from clearParam(), consistent with mainnet.conf @Getter @Setter - @Exportable public boolean nodeDetectEnable; @Getter @Setter public int allowMultiSign; @Getter @Setter - @Exportable public boolean vmTrace; @Getter @Setter - @Exportable public boolean needToUpdateAsset; @Getter @Setter - @Exportable public String trxReferenceBlock; @Getter @Setter - @Exportable public int minEffectiveConnection; @Getter @Setter - @Exportable public boolean trxCacheEnable; @Getter @Setter @@ -439,25 +356,20 @@ public class CommonParameter { @Getter @Setter - @Exportable public boolean allowShieldedTransactionApi; // clearParam: false @Getter @Setter - @Exportable public long blockNumForEnergyLimit; @Getter @Setter - @Exportable public boolean eventSubscribe = false; @Getter @Setter - @Exportable public long trxExpirationTimeInMilliseconds; // -- Shielded / ZK -- @Getter @Setter - @Exportable public String zenTokenId; // clearParam: "000000" @Getter @Setter @@ -467,207 +379,156 @@ public class CommonParameter { public long allowAccountStateRoot; @Getter @Setter - @Exportable public int validContractProtoThreadNum = 1; @Getter @Setter - @Exportable public int shieldedTransInPendingMaxCounts; // clearParam: 10 @Getter @Setter public long changedDelegation; @Getter @Setter - @Exportable public RateLimiterInitialization rateLimiterInitialization; @Getter @Setter - @Exportable public int rateLimiterGlobalQps = 50000; // from clearParam(), consistent with mainnet.conf @Getter @Setter - @Exportable public int rateLimiterGlobalIpQps = 10000; // from clearParam(), consistent with mainnet.conf @Getter - @Exportable public int rateLimiterGlobalApiQps = 1000; // from clearParam(), consistent with mainnet.conf @Getter @Setter - @Exportable public double rateLimiterSyncBlockChain; // clearParam: 3.0 @Getter @Setter - @Exportable public double rateLimiterFetchInvData; // clearParam: 3.0 @Getter @Setter - @Exportable public double rateLimiterDisconnect; // clearParam: 1.0 @Getter @Setter - @Exportable public boolean rateLimiterApiNonBlocking = false; @Getter - @Exportable public RocksDbSettings rocksDBCustomSettings; @Getter - @Exportable public GenesisBlock genesisBlock; @Getter @Setter - @Exportable public boolean p2pDisable = false; @Getter @Setter // from clearParam(), consistent with mainnet.conf - @Exportable public List activeNodes = new ArrayList<>(); @Getter @Setter // from clearParam(), consistent with mainnet.conf - @Exportable public List passiveNodes = new ArrayList<>(); @Getter - @Exportable public List fastForwardNodes; // clearParam: new ArrayList<>() @Getter - @Exportable public int maxFastForwardNum; // clearParam: 4 @Getter - @Exportable public Storage storage; @Getter - @Exportable public SeedNode seedNode; @Getter - @Exportable public EventPluginConfig eventPluginConfig; @Getter - @Exportable public FilterQuery eventFilter; @Getter @Setter - @Exportable public String cryptoEngine = Constant.ECKey_ENGINE; @Getter @Setter - @Exportable public boolean rpcEnable = true; @Getter @Setter - @Exportable public boolean rpcSolidityEnable = true; @Getter @Setter - @Exportable public boolean rpcPBFTEnable = true; @Getter @Setter - @Exportable public boolean fullNodeHttpEnable = true; @Getter @Setter - @Exportable public boolean solidityNodeHttpEnable = true; @Getter @Setter - @Exportable public boolean pBFTHttpEnable = true; @Getter @Setter - @Exportable public boolean jsonRpcHttpFullNodeEnable = false; @Getter @Setter - @Exportable public boolean jsonRpcHttpSolidityNodeEnable = false; @Getter @Setter - @Exportable public boolean jsonRpcHttpPBFTNodeEnable = false; @Getter @Setter - @Exportable public int jsonRpcMaxBlockRange = 5000; @Getter @Setter - @Exportable public int jsonRpcMaxSubTopics = 1000; @Getter @Setter - @Exportable public int jsonRpcMaxBlockFilterNum = 50000; @Getter @Setter - @Exportable public int jsonRpcMaxBatchSize = 100; @Getter @Setter - @Exportable public int jsonRpcMaxResponseSize = 25 * 1024 * 1024; @Getter @Setter - @Exportable public int jsonRpcMaxAddressSize = 1000; @Getter @Setter - @Exportable public int jsonRpcMaxLogFilterNum = 20000; @Getter @Setter - @Exportable public boolean adminRpcEnable = false; @Getter @Setter - @Exportable public String adminListenAddress = Constant.LOCAL_HOST; @Getter @Setter - @Exportable public int adminListenPort = 8575; @Getter @Setter - @Exportable public boolean ipcEnable = false; @Getter @Setter - @Exportable public int maxTransactionPendingSize; @Getter @Setter - @Exportable public long pendingTransactionTimeout; @Getter @Setter - @Exportable public int maxTrxCacheSize; @Getter @Setter - @Exportable public boolean nodeMetricsEnable = false; @Getter @Setter - @Exportable public boolean metricsPrometheusEnable = false; @Getter @Setter - @Exportable public int metricsPrometheusPort; @Getter @Setter - @Exportable public int agreeNodeCount; @Getter @Setter public long allowPBFT; @Getter @Setter - @Exportable public int rpcOnPBFTPort; @Getter @Setter - @Exportable public int pBFTHttpPort; @Getter @@ -675,7 +536,6 @@ public class CommonParameter { public long pBFTExpireNum; // clearParam: 20 @Getter @Setter - @Exportable public long oldSolidityBlockNum = -1; @Getter @@ -701,19 +561,15 @@ public class CommonParameter { public long allowHigherLimitForMaxCpuTimeOfOneTx; @Getter @Setter - @Exportable public boolean openHistoryQueryWhenLiteFN = false; @Getter @Setter - @Exportable public boolean historyBalanceLookup = false; @Getter @Setter - @Exportable public boolean openPrintLog = true; @Getter @Setter - @Exportable public boolean openTransactionSort = false; @Getter @Setter @@ -723,23 +579,18 @@ public class CommonParameter { public long allowAssetOptimization; @Getter @Setter - @Exportable public List disabledApiList; // clearParam: Collections.emptyList() @Getter @Setter - @Exportable public CronExpression shutdownBlockTime = null; @Getter @Setter - @Exportable public long shutdownBlockHeight = -1; @Getter @Setter - @Exportable public long shutdownBlockCount = -1; @Getter @Setter - @Exportable public long blockCacheTimeout = 60; @Getter @Setter @@ -773,26 +624,21 @@ public class CommonParameter { public long dynamicEnergyMaxFactor = 0L; @Getter @Setter - @Exportable public boolean dynamicConfigEnable; @Getter @Setter - @Exportable public long dynamicConfigCheckInterval; // clearParam: 600 @Getter @Setter public long allowTvmShangHai; @Getter @Setter - @Exportable public long allowCancelAllUnfreezeV2; @Getter @Setter - @Exportable public boolean unsolidifiedBlockCheck; @Getter @Setter - @Exportable public int maxUnsolidifiedBlocks; // clearParam: 54 @Getter @Setter @@ -802,7 +648,6 @@ public class CommonParameter { public long allowEnergyAdjustment; @Getter @Setter - @Exportable public long maxCreateAccountTxSize = 1000L; @Getter @Setter diff --git a/common/src/main/java/org/tron/common/parameter/Exportable.java b/common/src/main/java/org/tron/common/parameter/Exportable.java deleted file mode 100644 index 71272d394a1..00000000000 --- a/common/src/main/java/org/tron/common/parameter/Exportable.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.tron.common.parameter; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Marks a {@link CommonParameter} field as safe to expose through the admin runtime-parameter - * API. Unmarked fields are excluded by default. - */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) -public @interface Exportable { - -} diff --git a/framework/src/main/java/org/tron/core/config/args/DynamicArgs.java b/framework/src/main/java/org/tron/core/config/args/DynamicArgs.java index bdd6431e5c5..5a9923b16c9 100644 --- a/framework/src/main/java/org/tron/core/config/args/DynamicArgs.java +++ b/framework/src/main/java/org/tron/core/config/args/DynamicArgs.java @@ -4,9 +4,7 @@ import java.io.File; import java.net.InetAddress; import java.net.InetSocketAddress; -import java.util.ArrayList; import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import javax.annotation.PostConstruct; @@ -70,36 +68,30 @@ public void reload() { updateTrustNodes(nodeConfig); } - /** - * Builds the complete active-node list before replacing the shared reference atomically. Using - * {@code clear()} followed by {@code addAll()} would let concurrent readers observe a transient - * empty or partially updated list. - */ private void updateActiveNodes(NodeConfig nodeConfig) { List newActiveNodes = Args.filterInetSocketAddress(nodeConfig.getActive(), true); parameter.setActiveNodes(newActiveNodes); - List activeNodes = new CopyOnWriteArrayList<>(newActiveNodes); - TronNetService.getP2pConfig().setActiveNodes(activeNodes); - logger.debug("p2p active nodes : {}", activeNodes); + List activeNodes = TronNetService.getP2pConfig().getActiveNodes(); + activeNodes.clear(); + activeNodes.addAll(newActiveNodes); + logger.debug("p2p active nodes : {}", + TronNetService.getP2pConfig().getActiveNodes().toString()); } - /** - * Builds the complete trust-node list before replacing the shared reference atomically, so - * concurrent configuration exports and network readers see either the old or the new snapshot. - */ private void updateTrustNodes(NodeConfig nodeConfig) { - List newPassiveNodes = new ArrayList<>(); + List newPassiveNodes = new java.util.ArrayList<>(); for (InetSocketAddress sa : Args.filterInetSocketAddress(nodeConfig.getPassive(), false)) { newPassiveNodes.add(sa.getAddress()); } parameter.setPassiveNodes(newPassiveNodes); - List newTrustNodes = new ArrayList<>(newPassiveNodes); - parameter.getActiveNodes().forEach(n -> newTrustNodes.add(n.getAddress())); - parameter.getFastForwardNodes().forEach(f -> newTrustNodes.add(f.getAddress())); - List trustNodes = new CopyOnWriteArrayList<>(newTrustNodes); - TronNetService.getP2pConfig().setTrustNodes(trustNodes); - logger.debug("p2p trust nodes : {}", trustNodes); + List trustNodes = TronNetService.getP2pConfig().getTrustNodes(); + trustNodes.clear(); + trustNodes.addAll(newPassiveNodes); + parameter.getActiveNodes().forEach(n -> trustNodes.add(n.getAddress())); + parameter.getFastForwardNodes().forEach(f -> trustNodes.add(f.getAddress())); + logger.debug("p2p trust nodes : {}", + TronNetService.getP2pConfig().getTrustNodes().toString()); } @PreDestroy diff --git a/framework/src/main/java/org/tron/core/net/TronNetService.java b/framework/src/main/java/org/tron/core/net/TronNetService.java index b0551f17c4a..8b97c8d9f4d 100644 --- a/framework/src/main/java/org/tron/core/net/TronNetService.java +++ b/framework/src/main/java/org/tron/core/net/TronNetService.java @@ -7,7 +7,6 @@ import java.util.List; import java.util.Objects; import java.util.Set; -import java.util.concurrent.CopyOnWriteArrayList; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -165,15 +164,10 @@ private P2pConfig updateConfig(P2pConfig config) { seeds.addAll(nodePersistService.dbRead()); logger.debug("Seed InetSocketAddress: {}", seeds); config.getSeedNodes().addAll(seeds); - // These lists are read by configuration exporters and network services while dynamic reloads - // and relay tasks may replace or modify them. Snapshot-based copy-on-write lists prevent - // transient empty/partial views and ConcurrentModificationException during iteration. - List activeNodes = new CopyOnWriteArrayList<>(parameter.getActiveNodes()); - config.setActiveNodes(activeNodes); - List trustNodes = new CopyOnWriteArrayList<>(parameter.getPassiveNodes()); - activeNodes.forEach(n -> trustNodes.add(n.getAddress())); - parameter.getFastForwardNodes().forEach(f -> trustNodes.add(f.getAddress())); - config.setTrustNodes(trustNodes); + config.getActiveNodes().addAll(parameter.getActiveNodes()); + config.getTrustNodes().addAll(parameter.getPassiveNodes()); + config.getActiveNodes().forEach(n -> config.getTrustNodes().add(n.getAddress())); + parameter.getFastForwardNodes().forEach(f -> config.getTrustNodes().add(f.getAddress())); int maxConnections = parameter.getMaxConnections(); int minConnections = parameter.getMinConnections(); int minActiveConnections = parameter.getMinActiveConnections(); @@ -210,4 +204,4 @@ private P2pConfig updateConfig(P2pConfig config) { } return config; } -} +} \ No newline at end of file diff --git a/framework/src/main/java/org/tron/core/services/admin/AdminJsonRpc.java b/framework/src/main/java/org/tron/core/services/admin/AdminJsonRpc.java index b49e7a93ffd..73a43f35c53 100644 --- a/framework/src/main/java/org/tron/core/services/admin/AdminJsonRpc.java +++ b/framework/src/main/java/org/tron/core/services/admin/AdminJsonRpc.java @@ -4,7 +4,6 @@ import com.googlecode.jsonrpc4j.JsonRpcErrors; import com.googlecode.jsonrpc4j.JsonRpcMethod; import com.googlecode.jsonrpc4j.JsonRpcParam; -import java.util.Map; import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; public interface AdminJsonRpc { @@ -15,7 +14,4 @@ public interface AdminJsonRpc { }) String adminExample(@JsonRpcParam("param1") String param1, @JsonRpcParam("param2") String param2) throws JsonRpcInvalidParamsException; - - @JsonRpcMethod("admin_getRuntimeParameters") - Map getRuntimeParameters(); } diff --git a/framework/src/main/java/org/tron/core/services/admin/AdminJsonRpcImpl.java b/framework/src/main/java/org/tron/core/services/admin/AdminJsonRpcImpl.java index 19e9511f847..dba646bce07 100644 --- a/framework/src/main/java/org/tron/core/services/admin/AdminJsonRpcImpl.java +++ b/framework/src/main/java/org/tron/core/services/admin/AdminJsonRpcImpl.java @@ -1,18 +1,10 @@ package org.tron.core.services.admin; -import java.util.Map; import org.springframework.stereotype.Component; import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; @Component public class AdminJsonRpcImpl implements AdminJsonRpc { - - private final CommonParameterExporter commonParameterExporter; - - public AdminJsonRpcImpl(CommonParameterExporter commonParameterExporter) { - this.commonParameterExporter = commonParameterExporter; - } - @Override public String adminExample(String param1, String param2) throws JsonRpcInvalidParamsException { if ("".equals(param1) || "".equals(param2)) { @@ -20,9 +12,4 @@ public String adminExample(String param1, String param2) throws JsonRpcInvalidPa } return param1 + ":" + param2; } - - @Override - public Map getRuntimeParameters() { - return commonParameterExporter.export(); - } } diff --git a/framework/src/main/java/org/tron/core/services/admin/CommonParameterExporter.java b/framework/src/main/java/org/tron/core/services/admin/CommonParameterExporter.java deleted file mode 100644 index 8c900dddb8e..00000000000 --- a/framework/src/main/java/org/tron/core/services/admin/CommonParameterExporter.java +++ /dev/null @@ -1,197 +0,0 @@ -package org.tron.core.services.admin; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.Locale; -import java.util.Map; -import java.util.Map.Entry; -import java.util.TreeMap; -import lombok.extern.slf4j.Slf4j; -import org.iq80.leveldb.Options; -import org.springframework.stereotype.Component; -import org.tron.common.parameter.CommonParameter; -import org.tron.common.parameter.Exportable; -import org.tron.common.utils.Property; -import org.tron.core.config.args.Storage; - -@Component -@Slf4j(topic = "API") -public class CommonParameterExporter { - - static final String REDACTED_VALUE = "[REDACTED]"; - private static final String UNAVAILABLE_VALUE = "[UNAVAILABLE]"; - private static final String[] SENSITIVE_NAME_PARTS = { - "private", "password", "passwd", "secret", "credential", "mnemonic", - "accesskey", "apikey", "localwitness", "seedphrase", - "authorization", "authtoken" - }; - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - - public Map export() { - return export(CommonParameter.getInstance()); - } - - Map export(CommonParameter parameter) { - ObjectNode snapshot = OBJECT_MAPPER.createObjectNode(); - Field[] fields = parameter.getClass().getFields(); - Arrays.sort(fields, Comparator.comparing(Field::getName)); - for (Field field : fields) { - if (!field.isAnnotationPresent(Exportable.class)) { - continue; - } - String fieldName = field.getName(); - if (isOmittedName(fieldName)) { - continue; - } - if (isSensitiveName(fieldName)) { - snapshot.put(fieldName, REDACTED_VALUE); - continue; - } - - try { - Object target = Modifier.isStatic(field.getModifiers()) ? null : parameter; - JsonNode value = snapshotValue(field.get(target)); - snapshot.set(fieldName, sanitize(value)); - } catch (IllegalAccessException | RuntimeException e) { - logExportFailure(fieldName, e); - snapshot.put(fieldName, UNAVAILABLE_VALUE); - } - } - return OBJECT_MAPPER.convertValue(snapshot, - new TypeReference>() { }); - } - - private JsonNode snapshotValue(Object value) { - if (value instanceof Storage) { - return snapshotStorage((Storage) value); - } - return OBJECT_MAPPER.valueToTree(value); - } - - /** - * Builds an explicit snapshot because {@link Storage} contains runtime collaborators such as - * LevelDB {@link Options} that are not regular Jackson beans. Serializing the live object can - * fail and would also expose newly added third-party fields without an explicit review. - */ - private ObjectNode snapshotStorage(Storage storage) { - ObjectNode snapshot = OBJECT_MAPPER.createObjectNode(); - snapshot.put("dbDirectory", storage.getDbDirectory()); - snapshot.put("dbEngine", storage.getDbEngine()); - snapshot.put("dbSync", storage.isDbSync()); - snapshot.put("maxFlushCount", storage.getMaxFlushCount()); - snapshot.put("contractParseSwitch", storage.isContractParseSwitch()); - snapshot.put("transactionHistorySwitch", storage.getTransactionHistorySwitch()); - snapshot.put("checkpointVersion", storage.getCheckpointVersion()); - snapshot.put("checkpointSync", storage.isCheckpointSync()); - snapshot.put("estimatedBlockTransactions", storage.getEstimatedBlockTransactions()); - snapshot.put("txCacheInitOptimization", storage.isTxCacheInitOptimization()); - snapshot.set("cacheDbs", OBJECT_MAPPER.valueToTree( - new ArrayList<>(storage.getCacheDbs()))); - - Map propertyMap = storage.getPropertyMap(); - if (propertyMap == null) { - snapshot.putNull("propertyMap"); - return snapshot; - } - ObjectNode properties = snapshot.putObject("propertyMap"); - for (Entry entry : new TreeMap<>(propertyMap).entrySet()) { - try { - properties.set(entry.getKey(), snapshotStorageProperty(entry.getValue())); - } catch (RuntimeException e) { - logExportFailure("storage.propertyMap entry", e); - properties.put(entry.getKey(), UNAVAILABLE_VALUE); - } - } - return snapshot; - } - - /** - * Copies only stable property and database-option values so one unsupported runtime object does - * not make the entire storage section unavailable or expand the exported surface implicitly. - */ - private ObjectNode snapshotStorageProperty(Property property) { - ObjectNode snapshot = OBJECT_MAPPER.createObjectNode(); - snapshot.put("name", property.getName()); - snapshot.put("path", property.getPath()); - Options options = property.getDbOptions(); - if (options == null) { - snapshot.putNull("dbOptions"); - return snapshot; - } - ObjectNode optionSnapshot = snapshot.putObject("dbOptions"); - optionSnapshot.put("createIfMissing", options.createIfMissing()); - optionSnapshot.put("errorIfExists", options.errorIfExists()); - optionSnapshot.put("writeBufferSize", options.writeBufferSize()); - optionSnapshot.put("maxOpenFiles", options.maxOpenFiles()); - optionSnapshot.put("blockRestartInterval", options.blockRestartInterval()); - optionSnapshot.put("blockSize", options.blockSize()); - optionSnapshot.put("compressionType", options.compressionType().name()); - optionSnapshot.put("verifyChecksums", options.verifyChecksums()); - optionSnapshot.put("cacheSize", options.cacheSize()); - optionSnapshot.put("paranoidChecks", options.paranoidChecks()); - return snapshot; - } - - private void logExportFailure(String fieldName, Exception exception) { - logger.warn("Unable to export runtime parameter {}: {}", fieldName, - exception.getClass().getSimpleName()); - logger.debug("Runtime parameter export failure for " + fieldName, exception); - } - - JsonNode sanitize(JsonNode value) { - if (value == null || value.isNull() || value.isValueNode()) { - return value; - } - if (value.isArray()) { - ArrayNode sanitized = OBJECT_MAPPER.createArrayNode(); - for (JsonNode element : value) { - sanitized.add(sanitize(element)); - } - return sanitized; - } - if (value.isObject()) { - ObjectNode sanitized = OBJECT_MAPPER.createObjectNode(); - Iterator> fields = value.fields(); - while (fields.hasNext()) { - Entry field = fields.next(); - if (isOmittedName(field.getKey())) { - continue; - } else if (isSensitiveName(field.getKey())) { - sanitized.put(field.getKey(), REDACTED_VALUE); - } else { - sanitized.set(field.getKey(), sanitize(field.getValue())); - } - } - return sanitized; - } - return value; - } - - private boolean isOmittedName(String name) { - return "dbconfig".equals(name.toLowerCase(Locale.ROOT)); - } - - private boolean isSensitiveName(String name) { - String normalized = name.toLowerCase(Locale.ROOT); - if ("pwd".equals(normalized) || "key".equals(normalized) - || "token".equals(normalized) || "auth".equals(normalized)) { - return true; - } - for (String part : SENSITIVE_NAME_PARTS) { - if (normalized.contains(part)) { - return true; - } - } - return false; - } -} diff --git a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java index f04fdcbd6c9..f8bb4e1dc10 100644 --- a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java +++ b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java @@ -125,7 +125,7 @@ public void testAttachRejectsOtherNodeOptions() { public void testExecRequiresAttach() { Args.clearParam(); try { - Args.setParam(new String[] {"--exec", "admin_getRuntimeParameters"}, + Args.setParam(new String[] {"--exec", "admin_example"}, TestConstants.TEST_CONF); Assert.fail("Expected --exec without --attach to fail"); } catch (ParameterException e) { diff --git a/framework/src/test/java/org/tron/core/config/args/DynamicArgsTest.java b/framework/src/test/java/org/tron/core/config/args/DynamicArgsTest.java index 1e3a9ffe48b..733c862e6a4 100644 --- a/framework/src/test/java/org/tron/core/config/args/DynamicArgsTest.java +++ b/framework/src/test/java/org/tron/core/config/args/DynamicArgsTest.java @@ -1,13 +1,6 @@ package org.tron.core.config.args; import java.io.File; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; import org.junit.Assert; import org.junit.Test; import org.tron.common.BaseMethodTest; @@ -62,37 +55,4 @@ public void start() { dynamicArgs.close(); } - - @Test - public void testReloadReplacesP2pNodeListsAtomically() { - CommonParameter parameter = Args.getInstance(); - List originalActiveNodes = parameter.getActiveNodes(); - List originalPassiveNodes = parameter.getPassiveNodes(); - List originalFastForwardNodes = parameter.getFastForwardNodes(); - TronNetService tronNetService = context.getBean(TronNetService.class); - P2pConfig originalP2pConfig = TronNetService.getP2pConfig(); - P2pConfig p2pConfig = new P2pConfig(); - ReflectUtils.setFieldValue(tronNetService, "p2pConfig", p2pConfig); - parameter.fastForwardNodes = new ArrayList<>(); - - NodeConfig nodeConfig = new NodeConfig(); - nodeConfig.setActive(Collections.singletonList("192.0.2.1:18889")); - nodeConfig.setPassive(Arrays.asList("127.0.0.2:18888", "127.0.0.3:18888")); - try { - ReflectUtils.invokeMethod(dynamicArgs, "updateActiveNodes", - new Class[] {NodeConfig.class}, nodeConfig); - ReflectUtils.invokeMethod(dynamicArgs, "updateTrustNodes", - new Class[] {NodeConfig.class}, nodeConfig); - - Assert.assertTrue(p2pConfig.getActiveNodes() instanceof CopyOnWriteArrayList); - Assert.assertTrue(p2pConfig.getTrustNodes() instanceof CopyOnWriteArrayList); - Assert.assertEquals(1, p2pConfig.getActiveNodes().size()); - Assert.assertEquals(3, p2pConfig.getTrustNodes().size()); - } finally { - parameter.setActiveNodes(originalActiveNodes); - parameter.setPassiveNodes(originalPassiveNodes); - parameter.fastForwardNodes = originalFastForwardNodes; - ReflectUtils.setFieldValue(tronNetService, "p2pConfig", originalP2pConfig); - } - } } diff --git a/framework/src/test/java/org/tron/core/services/admin/CommonParameterExporterTest.java b/framework/src/test/java/org/tron/core/services/admin/CommonParameterExporterTest.java deleted file mode 100644 index 0f338351d02..00000000000 --- a/framework/src/test/java/org/tron/core/services/admin/CommonParameterExporterTest.java +++ /dev/null @@ -1,221 +0,0 @@ -package org.tron.core.services.admin; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import org.iq80.leveldb.Options; -import org.junit.Assert; -import org.junit.Test; -import org.tron.common.args.GenesisBlock; -import org.tron.common.logsfilter.EventPluginConfig; -import org.tron.common.parameter.CommonParameter; -import org.tron.common.parameter.Exportable; -import org.tron.common.utils.Property; -import org.tron.common.utils.ReflectUtils; -import org.tron.core.config.args.Storage; -import org.tron.p2p.P2pConfig; -import org.tron.p2p.dns.update.PublishConfig; - -public class CommonParameterExporterTest { - - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - private final CommonParameterExporter exporter = new CommonParameterExporter(); - - @Test - public void testExportIncludesOnlyAnnotatedFieldsAndLiveValues() { - CommonParameter parameter = new CommonParameter(); - parameter.rpcPort = 150051; - parameter.chainId = "runtime-chain"; - parameter.allowCreationOfContracts = 1L; - - Map snapshot = exporter.export(parameter); - - Assert.assertEquals(150051, snapshot.get("rpcPort")); - Assert.assertEquals("runtime-chain", snapshot.get("chainId")); - Assert.assertFalse(snapshot.containsKey("allowCreationOfContracts")); - - parameter.rpcPort = 250051; - snapshot = exporter.export(parameter); - Assert.assertEquals(250051, snapshot.get("rpcPort")); - } - - @Test - public void testExportUsesRuntimeParameterType() { - ExtendedCommonParameter parameter = new ExtendedCommonParameter(); - - Map snapshot = exporter.export(parameter); - - Assert.assertEquals("runtime-value", snapshot.get("runtimeOnly")); - } - - @Test - public void testExportSortsTopLevelKeysByFieldName() { - Map snapshot = exporter.export(new CommonParameter()); - List actualKeys = new ArrayList<>(snapshot.keySet()); - List sortedKeys = new ArrayList<>(actualKeys); - Collections.sort(sortedKeys); - - Assert.assertEquals(sortedKeys, actualKeys); - } - - @Test - public void testExportExcludesCommitteeParameters() { - Map snapshot = exporter.export(new CommonParameter()); - List committeeParameters = Arrays.asList( - "allowCreationOfContracts", "allowMultiSign", "allowAdaptiveEnergy", - "allowDelegateResource", "allowSameTokenName", "allowTvmTransferTrc10", - "allowTvmConstantinople", "allowTvmSolidity059", "forbidTransferToContract", - "allowShieldedTRC20Transaction", "allowMarketTransaction", - "allowTransactionFeePool", "allowBlackHoleOptimization", "allowNewResourceModel", - "allowTvmIstanbul", "allowProtoFilterNum", "allowAccountStateRoot", - "changedDelegation", "allowPBFT", "pBFTExpireNum", "allowTvmFreeze", - "allowTvmVote", "allowTvmLondon", "allowTvmCompatibleEvm", - "allowHigherLimitForMaxCpuTimeOfOneTx", "allowNewRewardAlgorithm", - "allowOptimizedReturnValueOfChainId", "allowTvmShangHai", "allowOldRewardOpt", - "allowEnergyAdjustment", "allowStrictMath", "consensusLogicOptimization", - "allowTvmCancun", "allowTvmBlob", "unfreezeDelayDays", - "allowAccountAssetOptimization", "allowAssetOptimization", "allowNewReward", - "memoFee", "allowDelegateOptimization", "allowDynamicEnergy", - "dynamicEnergyThreshold", "dynamicEnergyIncreaseFactor", "dynamicEnergyMaxFactor"); - - for (String fieldName : committeeParameters) { - Assert.assertFalse("Committee parameter must not be exported: " + fieldName, - snapshot.containsKey(fieldName)); - } - } - - @Test - public void testSanitizeRedactsSensitiveValuesRecursively() { - ObjectNode source = OBJECT_MAPPER.createObjectNode(); - source.put("privateKey", "private-value"); - source.put("password", "password-value"); - source.put("zenTokenId", "000000"); - ObjectNode nested = source.putObject("dns"); - nested.put("accessKeyId", "access-key-value"); - nested.put("accessKeySecret", "secret-value"); - nested.put("dnsPrivate", "dns-private-value"); - nested.put("dbConfig", "database|username|password"); - nested.put("endpoint", "127.0.0.1"); - - JsonNode sanitized = exporter.sanitize(source); - - Assert.assertEquals(CommonParameterExporter.REDACTED_VALUE, - sanitized.get("privateKey").asText()); - Assert.assertEquals(CommonParameterExporter.REDACTED_VALUE, - sanitized.get("password").asText()); - Assert.assertEquals(CommonParameterExporter.REDACTED_VALUE, - sanitized.get("dns").get("accessKeyId").asText()); - Assert.assertEquals(CommonParameterExporter.REDACTED_VALUE, - sanitized.get("dns").get("accessKeySecret").asText()); - Assert.assertEquals(CommonParameterExporter.REDACTED_VALUE, - sanitized.get("dns").get("dnsPrivate").asText()); - Assert.assertFalse(sanitized.get("dns").has("dbConfig")); - Assert.assertEquals("000000", sanitized.get("zenTokenId").asText()); - Assert.assertEquals("127.0.0.1", sanitized.get("dns").get("endpoint").asText()); - } - - @Test - public void testExportIncludesApprovedConfigurationsAndExcludesSecrets() { - CommonParameter parameter = new CommonParameter(); - parameter.dnsPublishConfig = new PublishConfig(); - parameter.dnsPublishConfig.setDnsPrivate("dns-private-value"); - parameter.dnsPublishConfig.setAccessKeyId("access-key-value"); - parameter.dnsPublishConfig.setAccessKeySecret("secret-value"); - parameter.dnsPublishConfig.setDnsDomain("nodes.example.org"); - parameter.eventPluginConfig = new EventPluginConfig(); - parameter.eventPluginConfig.setDbConfig("mongodb://user:password@localhost/events"); - parameter.eventPluginConfig.setServerAddress("127.0.0.1:5555"); - parameter.outputDirectory = "node-output"; - parameter.logbackPath = "logback.xml"; - parameter.storage = new Storage(); - parameter.storage.setDbDirectory("database"); - parameter.genesisBlock = GenesisBlock.getDefault(); - parameter.p2pConfig = new P2pConfig(); - parameter.p2pConfig.setIp("127.0.0.1"); - parameter.p2pConfig.setPublishConfig(parameter.dnsPublishConfig); - - Map snapshot = exporter.export(parameter); - Assert.assertFalse(snapshot.containsKey("dnsPublishConfig")); - Map eventPluginConfig = (Map) snapshot.get("eventPluginConfig"); - Assert.assertFalse(eventPluginConfig.containsKey("dbConfig")); - Assert.assertEquals("127.0.0.1:5555", eventPluginConfig.get("serverAddress")); - Assert.assertEquals("node-output", snapshot.get("outputDirectory")); - Assert.assertEquals("logback.xml", snapshot.get("logbackPath")); - Assert.assertEquals("database", ((Map) snapshot.get("storage")).get("dbDirectory")); - Assert.assertEquals("0", ((Map) snapshot.get("genesisBlock")).get("number")); - Map p2pConfig = (Map) snapshot.get("p2pConfig"); - Assert.assertEquals("127.0.0.1", p2pConfig.get("ip")); - Map publishConfig = (Map) p2pConfig.get("publishConfig"); - Assert.assertEquals(CommonParameterExporter.REDACTED_VALUE, - publishConfig.get("accessKeyId")); - Assert.assertEquals(CommonParameterExporter.REDACTED_VALUE, - publishConfig.get("accessKeySecret")); - } - - @Test - public void testExportStorageWithDatabaseOptions() { - CommonParameter parameter = new CommonParameter(); - Storage storage = new Storage(); - storage.setDbDirectory("database"); - storage.setDbEngine("LEVELDB"); - Property property = new Property(); - property.setName("account"); - property.setPath("account-data"); - property.setDbOptions(new Options() - .createIfMissing(true) - .cacheSize(4_096L) - .writeBufferSize(8_192) - .maxOpenFiles(128) - .blockSize(1_024)); - ReflectUtils.setFieldValue(storage, "propertyMap", - Collections.singletonMap("account", property)); - parameter.storage = storage; - - Map snapshot = exporter.export(parameter); - - Map storageSnapshot = (Map) snapshot.get("storage"); - Assert.assertEquals("database", storageSnapshot.get("dbDirectory")); - Map propertyMap = (Map) storageSnapshot.get("propertyMap"); - Map propertySnapshot = (Map) propertyMap.get("account"); - Assert.assertEquals("account-data", propertySnapshot.get("path")); - Map optionSnapshot = (Map) propertySnapshot.get("dbOptions"); - Assert.assertEquals(4_096L, optionSnapshot.get("cacheSize")); - Assert.assertEquals(8_192, optionSnapshot.get("writeBufferSize")); - Assert.assertEquals(128, optionSnapshot.get("maxOpenFiles")); - Assert.assertEquals(1_024, optionSnapshot.get("blockSize")); - } - - @Test - public void testStoragePropertyFailureDoesNotHideStorage() { - CommonParameter parameter = new CommonParameter(); - Storage storage = new Storage(); - storage.setDbDirectory("database"); - Property property = new Property() { - @Override - public Options getDbOptions() { - throw new IllegalStateException("unavailable options"); - } - }; - ReflectUtils.setFieldValue(storage, "propertyMap", - Collections.singletonMap("broken", property)); - parameter.storage = storage; - - Map snapshot = exporter.export(parameter); - - Map storageSnapshot = (Map) snapshot.get("storage"); - Assert.assertEquals("database", storageSnapshot.get("dbDirectory")); - Map propertyMap = (Map) storageSnapshot.get("propertyMap"); - Assert.assertEquals("[UNAVAILABLE]", propertyMap.get("broken")); - } - - private static class ExtendedCommonParameter extends CommonParameter { - - @Exportable - public String runtimeOnly = "runtime-value"; - } -} diff --git a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcClientTest.java b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcClientTest.java index 83ea41dd3a3..8579b24960e 100644 --- a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcClientTest.java +++ b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcClientTest.java @@ -35,7 +35,6 @@ public void testBuildHelpLinesIncludesSortedCommandParameters() { Assert.assertEquals(Arrays.asList( "admin_example ", - "admin_getRuntimeParameters", "help [command]", "exit/quit"), client.buildHelpLines()); } @@ -45,7 +44,7 @@ public void testCompletionUsesCanonicalMethodNames() { IpcClient client = new IpcClient("unused"); Assert.assertArrayEquals(new String[] { - "admin_example", "admin_getRuntimeParameters" + "admin_example" }, client.getCompletionCommandNames()); } diff --git a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java index 99902f60957..3f191eee8cc 100644 --- a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java @@ -10,6 +10,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; +import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; @@ -19,6 +20,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.PosixFilePermission; +import java.util.Arrays; import java.util.EnumSet; import org.junit.Assert; import org.junit.Assume; @@ -32,7 +34,6 @@ import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; import org.tron.core.services.admin.AdminJsonRpc; import org.tron.core.services.admin.AdminJsonRpcImpl; -import org.tron.core.services.admin.CommonParameterExporter; public class IpcServiceTest { @@ -148,7 +149,7 @@ public void testValidateOutputDirectorySupportsPosixPermissions() throws Excepti @Test public void testHandleCommandReturnsSingleLineJsonResponse() throws Exception { IpcService service = new IpcService( - new AdminJsonRpcImpl(new CommonParameterExporter())); + new AdminJsonRpcImpl()); String response = service.handleCommand( "{\"jsonrpc\":\"2.0\",\"method\":\"admin_example\"," @@ -162,7 +163,7 @@ public void testHandleCommandReturnsSingleLineJsonResponse() throws Exception { @Test public void testHandleCommandReturnsJsonRpcErrorOnDispatcherFailure() throws Exception { IpcService service = Mockito.spy(new IpcService( - new AdminJsonRpcImpl(new CommonParameterExporter()))); + new AdminJsonRpcImpl())); Mockito.doThrow(new IOException("sensitive-detail")) .when(service).dispatchRequest(Mockito.any(ByteArrayInputStream.class), Mockito.any(ByteArrayOutputStream.class)); @@ -201,18 +202,22 @@ public void testHandleCommandUsesAnnotatedErrorResolver() throws Exception { @Test public void testReadRequestAcceptsMaximumSize() throws Exception { IpcService service = newIpcService(); - ByteArrayInputStream input = - new ByteArrayInputStream("1234\n".getBytes(StandardCharsets.UTF_8)); + int maxRequestSize = getStaticIntField("MAX_REQUEST_SIZE"); + byte[] request = new byte[maxRequestSize + 1]; + Arrays.fill(request, 0, maxRequestSize, (byte) '1'); + request[maxRequestSize] = '\n'; - Assert.assertEquals("1234", readRequest(service, input, 4)); + Assert.assertEquals(maxRequestSize, + readRequest(service, new ByteArrayInputStream(request)).length()); } @Test(expected = IOException.class) public void testReadRequestRejectsOversizedInputWithoutNewline() throws Exception { IpcService service = newIpcService(); - ByteArrayInputStream input = new ByteArrayInputStream("12345".getBytes(StandardCharsets.UTF_8)); + int maxRequestSize = getStaticIntField("MAX_REQUEST_SIZE"); + ByteArrayInputStream input = new ByteArrayInputStream(new byte[maxRequestSize + 1]); - readRequest(service, input, 4); + readRequest(service, input); } @Test(timeout = 10_000) @@ -222,7 +227,7 @@ public void testSocketFileUsesOwnerOnlyPermissions() throws Exception { String originalOutputDirectory = parameter.outputDirectory; Path outputDirectory = Files.createTempDirectory(Paths.get("/tmp"), "ipc-permission-test-"); IpcService service = new IpcService( - new AdminJsonRpcImpl(new CommonParameterExporter())); + new AdminJsonRpcImpl()); boolean started = false; Path socketFile = null; try { @@ -247,7 +252,7 @@ public void testHandlesMultipleClientsConcurrently() throws Exception { String originalOutputDirectory = parameter.outputDirectory; Path outputDirectory = Files.createTempDirectory(Paths.get("/tmp"), "ipc-multi-client-test-"); IpcService service = new IpcService( - new AdminJsonRpcImpl(new CommonParameterExporter())); + new AdminJsonRpcImpl()); boolean started = false; Path socketFile = null; try { @@ -303,7 +308,7 @@ public void testStopClosesActiveClientSocket() throws Exception { String originalOutputDirectory = parameter.outputDirectory; Path outputDirectory = Files.createTempDirectory("ipc-test-"); IpcService service = new IpcService( - new AdminJsonRpcImpl(new CommonParameterExporter())); + new AdminJsonRpcImpl()); boolean started = false; Path socketFile = null; try { @@ -364,7 +369,7 @@ public void testCleanupRestoresOutputDirectoryWhenStopFails() throws Exception { } private IpcService newIpcService() { - return new IpcService(new AdminJsonRpcImpl(new CommonParameterExporter())); + return new IpcService(new AdminJsonRpcImpl()); } private void cleanupIpcService(IpcService service, boolean started, CommonParameter parameter, @@ -418,10 +423,15 @@ private void deleteStaleSocketFile(IpcService service, Path socketFile) throws E invokePrivate(service, "deleteStaleSocketFile", new Class[] {Path.class}, socketFile); } - private String readRequest(IpcService service, InputStream input, int maxRequestSize) - throws Exception { + private String readRequest(IpcService service, InputStream input) throws Exception { return (String) invokePrivate(service, "readRequest", - new Class[] {InputStream.class, int.class}, input, maxRequestSize); + new Class[] {InputStream.class}, input); + } + + private int getStaticIntField(String fieldName) throws Exception { + Field field = IpcService.class.getDeclaredField(fieldName); + field.setAccessible(true); + return field.getInt(null); } private void registerClient(IpcService service, AFUNIXSocket client) throws Exception { From 50b41c29c5b8adf4e23725a9a8f716f994fbae83 Mon Sep 17 00:00:00 2001 From: 317787106 <317787106@qq.com> Date: Wed, 12 Aug 2026 23:16:47 +0800 Subject: [PATCH 11/15] create directory for sock file; reorg admin config, add ipc.socketDirectory --- .../common/parameter/CommonParameter.java | 3 + .../org/tron/core/config/args/NodeConfig.java | 19 +- .../tron/core/config/args/NodeConfigTest.java | 22 +- .../java/org/tron/core/config/args/Args.java | 6 +- .../core/services/admin/ipc/IpcService.java | 189 ++++++++--- .../org/tron/core/config/args/ArgsTest.java | 10 +- .../services/admin/ipc/IpcServiceTest.java | 302 +++++++++++++++--- 7 files changed, 449 insertions(+), 102 deletions(-) diff --git a/common/src/main/java/org/tron/common/parameter/CommonParameter.java b/common/src/main/java/org/tron/common/parameter/CommonParameter.java index f307ffe96f9..9945ce08df6 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -502,6 +502,9 @@ public class CommonParameter { public boolean ipcEnable = false; @Getter @Setter + public String ipcSocketDirectory = ""; + @Getter + @Setter public int maxTransactionPendingSize; @Getter @Setter diff --git a/common/src/main/java/org/tron/core/config/args/NodeConfig.java b/common/src/main/java/org/tron/core/config/args/NodeConfig.java index 89d6ef1d359..d6bcafa8291 100644 --- a/common/src/main/java/org/tron/core/config/args/NodeConfig.java +++ b/common/src/main/java/org/tron/core/config/args/NodeConfig.java @@ -39,7 +39,6 @@ public class NodeConfig { private int minParticipationRate = 0; private boolean openPrintLog = true; private boolean openTransactionSort = false; - private boolean ipcEnable = false; private int maxTps = 1000; private int maxBlockInvPerSecond = 10; private boolean openFullTcpDisconnect = false; //rename key @@ -130,7 +129,7 @@ public int getValidContractProtoThreads() { private HttpConfig http = new HttpConfig(); private RpcConfig rpc = new RpcConfig(); private JsonRpcConfig jsonrpc = new JsonRpcConfig(); - private AdminRpcConfig adminRpc = new AdminRpcConfig(); + private AdminConfig admin = new AdminConfig(); private NodeBackupConfig backup = new NodeBackupConfig(); private DynamicConfigSection dynamicConfig = new DynamicConfigSection(); private DnsConfig dns = new DnsConfig(); @@ -255,6 +254,22 @@ public static class JsonRpcConfig { private long maxMessageSize = 4194304; } + @Getter + @Setter + public static class AdminConfig { + + private AdminIpcConfig ipc = new AdminIpcConfig(); + private AdminRpcConfig rpc = new AdminRpcConfig(); + } + + @Getter + @Setter + public static class AdminIpcConfig { + + private boolean enable = false; + private String socketDirectory = ""; + } + @Getter @Setter public static class AdminRpcConfig { diff --git a/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java b/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java index 86915fc0735..8c4a767c54a 100644 --- a/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java @@ -30,10 +30,11 @@ public void testDefaults() { assertEquals(8, nc.getMinConnections()); assertEquals(4, nc.getMaxFastForwardNum()); assertFalse(nc.isOpenFullTcpDisconnect()); - assertFalse(nc.isIpcEnable()); - assertFalse(nc.getAdminRpc().isEnable()); - assertEquals("127.0.0.1", nc.getAdminRpc().getListenAddress()); - assertEquals(8575, nc.getAdminRpc().getPort()); + assertFalse(nc.getAdmin().getIpc().isEnable()); + assertEquals("", nc.getAdmin().getIpc().getSocketDirectory()); + assertFalse(nc.getAdmin().getRpc().isEnable()); + assertEquals("127.0.0.1", nc.getAdmin().getRpc().getListenAddress()); + assertEquals(8575, nc.getAdmin().getRpc().getPort()); // reference.conf matches code default: discovery disabled when not configured assertFalse(nc.isDiscoveryEnable()); assertFalse(nc.isDiscoveryPersist()); @@ -86,13 +87,14 @@ public void testRpcSubBean() { @Test public void testAdminRpcAndIpcBinding() { Config config = withRef( - "node { ipcEnable = true, adminRpc { enable = true," - + " listenAddress = \"127.0.0.2\", port = 18575 } }"); + "node.admin { ipc { enable = true, socketDirectory = \"/tmp/tron-ipc\" }," + + " rpc { enable = true, listenAddress = \"127.0.0.2\", port = 18575 } }"); NodeConfig nc = NodeConfig.fromConfig(config); - assertTrue(nc.isIpcEnable()); - assertTrue(nc.getAdminRpc().isEnable()); - assertEquals("127.0.0.2", nc.getAdminRpc().getListenAddress()); - assertEquals(18575, nc.getAdminRpc().getPort()); + assertTrue(nc.getAdmin().getIpc().isEnable()); + assertEquals("/tmp/tron-ipc", nc.getAdmin().getIpc().getSocketDirectory()); + assertTrue(nc.getAdmin().getRpc().isEnable()); + assertEquals("127.0.0.2", nc.getAdmin().getRpc().getListenAddress()); + assertEquals(18575, nc.getAdmin().getRpc().getPort()); } @Test diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index a6846f808a7..c7c371e0dac 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -625,11 +625,13 @@ private static void applyNodeConfig(NodeConfig nc) { PARAMETER.jsonRpcMaxMessageSize = jsonrpc.getMaxMessageSize(); // ---- Admin RPC / IPC ---- - NodeConfig.AdminRpcConfig adminRpc = nc.getAdminRpc(); + NodeConfig.AdminIpcConfig adminIpc = nc.getAdmin().getIpc(); + NodeConfig.AdminRpcConfig adminRpc = nc.getAdmin().getRpc(); PARAMETER.adminRpcEnable = adminRpc.isEnable(); PARAMETER.adminListenAddress = adminRpc.getListenAddress(); PARAMETER.adminListenPort = adminRpc.getPort(); - PARAMETER.ipcEnable = nc.isIpcEnable(); + PARAMETER.ipcEnable = adminIpc.isEnable(); + PARAMETER.ipcSocketDirectory = adminIpc.getSocketDirectory(); // ---- P2P sub-bean ---- PARAMETER.nodeP2pVersion = nc.getP2p().getVersion(); diff --git a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java index 7aee3fcbec2..1a5ef527f03 100644 --- a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java +++ b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java @@ -16,6 +16,7 @@ import java.lang.management.ManagementFactory; import java.net.SocketTimeoutException; import java.nio.charset.StandardCharsets; +import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; @@ -23,6 +24,7 @@ import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.PosixFileAttributeView; import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; import java.util.EnumSet; import java.util.Set; import java.util.concurrent.ArrayBlockingQueue; @@ -46,22 +48,22 @@ import org.tron.core.exception.TronError.ErrCode; import org.tron.core.services.admin.AdminJsonRpc; import org.tron.core.services.jsonrpc.JsonRpcErrorResolver; +import org.tron.core.services.jsonrpc.JsonRpcMapper; @Component @Slf4j(topic = "API") public class IpcService extends AbstractService { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = JsonRpcMapper.create(); private static final String ACCEPTOR_EXECUTOR_NAME = "admin-ipc-acceptor"; private static final String CLIENT_EXECUTOR_NAME = "admin-ipc-client"; private static final int MAX_REQUEST_SIZE = 4 * 1024 * 1024; //same as HttpService.maxRequestSize private static final int CLIENT_IDLE_TIMEOUT_MILLIS = 10 * 60 * 1000; + private static final String IPC_DIRECTORY_NAME = ".ipc"; // macOS/Linux sun_path buffers are 104/108 bytes. Reserve one byte for the terminating null // and three bytes of portability margin below the smaller macOS limit. private static final int MAX_SOCKET_PATH_BYTES = 100; - private static final Path FALLBACK_SOCKET_DIRECTORY = Paths.get("/tmp"); - private final ExecutorService acceptorExecutor = ExecutorServiceManager.newSingleThreadExecutor(ACCEPTOR_EXECUTOR_NAME, true); private final ExecutorService clientExecutor = @@ -98,31 +100,20 @@ public CompletableFuture start() { @Override public void innerStart() throws Exception { socketFilePath = resolveSocketFilePath(Args.getInstance(), getPid()); - Path outputDirectory = socketFilePath.getParent(); - validateOutputDirectory(outputDirectory); - deleteStaleSocketFile(socketFilePath); - - File socketFile = socketFilePath.toFile(); - AFUNIXSocketAddress address = AFUNIXSocketAddress.of(socketFile); - unixServerSocket = AFUNIXServerSocket.bindOn(address); + Path socketDirectory = socketFilePath.getParent(); + validateSocketRootDirectory(socketDirectory.getParent()); try { + recreateSocketDirectory(socketDirectory); + File socketFile = socketFilePath.toFile(); + AFUNIXSocketAddress address = AFUNIXSocketAddress.of(socketFile); + unixServerSocket = AFUNIXServerSocket.bindOn(address); setOwnerOnlyPermissions(socketFilePath); + unixServerSocket.setShutdownOnClose(true); + + logger.info("IpcService started, listening on {}", socketFile.getAbsolutePath()); } catch (IOException | RuntimeException e) { - try { - unixServerSocket.close(); - } catch (IOException closeException) { - e.addSuppressed(closeException); - } - try { - Files.deleteIfExists(socketFilePath); - } catch (IOException deleteException) { - e.addSuppressed(deleteException); - } - throw e; + throw cleanupFailedStart(e); } - unixServerSocket.setShutdownOnClose(true); - - logger.info("IpcService started, listening on {}", socketFile.getAbsolutePath()); Runnable runnable = () -> { while (isRunning) { try { @@ -283,18 +274,74 @@ private String buildInternalErrorResponse(String jsonRequest) { public void innerStop() throws Exception { logger.info("Begin to stop IpcService ..."); isRunning = false; + + Exception failure = null; + failure = runCleanup(failure, this::closeServerSocket); + failure = runCleanup(failure, this::shutdownActiveClients); + failure = runCleanup(failure, this::shutdownExecutors); + activeClientSockets.clear(); + failure = runCleanup(failure, this::deleteSocketFile); + failure = runCleanup(failure, this::deleteSocketDirectory); + + if (failure != null) { + throw failure; + } + logger.info("IpcService stopped"); + } + + private void closeServerSocket() throws IOException { if (unixServerSocket != null) { unixServerSocket.close(); } + } + + private void shutdownActiveClients() { for (AFUNIXSocket client : activeClientSockets) { - closeClientSocket(client); + shutdownClientSocket(client); } + } + + private void shutdownExecutors() { + // The accept and client workers can be blocked in native socket reads. Closing a junixsocket + // from another thread does not always wake those reads promptly, while the shared shutdown + // helper waits 60 seconds before interrupting them. Interrupt first so IPC shutdown remains + // bounded instead of intermittently stalling until that fallback timeout. + acceptorExecutor.shutdownNow(); + clientExecutor.shutdownNow(); ExecutorServiceManager.shutdownAndAwaitTermination(acceptorExecutor, ACCEPTOR_EXECUTOR_NAME); ExecutorServiceManager.shutdownAndAwaitTermination(clientExecutor, CLIENT_EXECUTOR_NAME); + } + + private void deleteSocketFile() throws IOException { if (socketFilePath != null) { Files.deleteIfExists(socketFilePath); } - logger.info("IpcService stopped"); + } + + private void deleteSocketDirectory() throws IOException { + if (socketFilePath != null) { + Files.deleteIfExists(socketFilePath.getParent()); + } + } + + private Exception cleanupFailedStart(Exception failure) { + if (unixServerSocket != null) { + failure = runCleanup(failure, this::closeServerSocket); + failure = runCleanup(failure, this::deleteSocketFile); + } + return runCleanup(failure, this::deleteSocketDirectory); + } + + private Exception runCleanup(Exception failure, CleanupAction action) { + try { + action.run(); + } catch (Exception cleanupFailure) { + if (failure == null) { + return cleanupFailure; + } + failure.addSuppressed(cleanupFailure); + } + return failure; } private void closeClientSocket(AFUNIXSocket client) { @@ -308,51 +355,91 @@ private void closeClientSocket(AFUNIXSocket client) { } } + private void shutdownClientSocket(AFUNIXSocket client) { + if (client == null) { + return; + } + try { + client.shutdownInput(); + } catch (IOException e) { + logger.debug("Failed to shut down IPC client input"); + } + try { + client.shutdownOutput(); + } catch (IOException e) { + logger.debug("Failed to shut down IPC client output"); + } + closeClientSocket(client); + } + private void closeAndRemoveClient(AFUNIXSocket client) { closeClientSocket(client); activeClientSockets.remove(client); } private Path resolveSocketFilePath(CommonParameter parameter, String pid) { - String socketFileName = "java-tron." + pid + ".sock"; - Path outputSocketFile = Paths.get(parameter.getOutputDirectory(), socketFileName) - .toAbsolutePath().normalize(); - if (getSocketPathLength(outputSocketFile) <= MAX_SOCKET_PATH_BYTES) { - return outputSocketFile; + String configuredDirectory = parameter.getIpcSocketDirectory(); + Path socketRootDirectory; + if (configuredDirectory == null || configuredDirectory.trim().isEmpty()) { + socketRootDirectory = Paths.get(parameter.getOutputDirectory()); + } else { + socketRootDirectory = Paths.get(configuredDirectory); + if (!socketRootDirectory.isAbsolute()) { + throw new TronError("node.admin.ipc.socketDirectory must be an absolute path", + ErrCode.API_SERVER_INIT); + } } - logger.warn("IPC socket path under output directory exceeds {} bytes; using /tmp instead", - MAX_SOCKET_PATH_BYTES); - return FALLBACK_SOCKET_DIRECTORY.resolve(socketFileName).toAbsolutePath().normalize(); + Path socketFile = socketRootDirectory.resolve(IPC_DIRECTORY_NAME) + .resolve(pid + ".sock").toAbsolutePath().normalize(); + int socketPathLength = getSocketPathLength(socketFile); + if (socketPathLength > MAX_SOCKET_PATH_BYTES) { + throw new TronError("IPC socket path " + socketFile + " is " + socketPathLength + + " bytes, exceeding the portable limit of " + MAX_SOCKET_PATH_BYTES + + " bytes. Configure node.admin.ipc.socketDirectory to a shorter absolute directory", + ErrCode.API_SERVER_INIT); + } + return socketFile; } private int getSocketPathLength(Path socketFile) { return socketFile.toString().getBytes(AFUNIXSocketAddress.addressCharset()).length; } - private void validateOutputDirectory(Path outputDirectory) throws IOException { - if (outputDirectory == null || !Files.isDirectory(outputDirectory)) { - throw new TronError("IPC output directory does not exist or is not a directory", + private void validateSocketRootDirectory(Path socketRootDirectory) throws IOException { + if (socketRootDirectory == null || !Files.isDirectory(socketRootDirectory)) { + throw new TronError("IPC socket root directory does not exist or is not a directory", ErrCode.API_SERVER_INIT); } - if (!Files.getFileStore(outputDirectory) + if (!Files.getFileStore(socketRootDirectory) .supportsFileAttributeView(PosixFileAttributeView.class)) { - throw new TronError("IPC requires a POSIX-compatible output directory", + throw new TronError("IPC requires a POSIX-compatible socket root directory", ErrCode.API_SERVER_INIT); } } - private void deleteStaleSocketFile(Path socketFilePath) throws IOException { - if (!Files.exists(socketFilePath, LinkOption.NOFOLLOW_LINKS)) { - return; + private void recreateSocketDirectory(Path socketDirectory) throws IOException { + if (Files.exists(socketDirectory, LinkOption.NOFOLLOW_LINKS)) { + BasicFileAttributes attributes = Files.readAttributes(socketDirectory, + BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + if (attributes.isSymbolicLink() || !attributes.isDirectory()) { + throw new TronError("Refusing to replace a non-directory IPC path", + ErrCode.API_SERVER_INIT); + } + deleteDirectoryWithDirectEntries(socketDirectory); } - BasicFileAttributes attributes = Files.readAttributes(socketFilePath, - BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); - if (attributes.isSymbolicLink() || !attributes.isOther()) { - throw new TronError("Refusing to replace a non-socket IPC endpoint", - ErrCode.API_SERVER_INIT); + Files.createDirectory(socketDirectory, PosixFilePermissions.asFileAttribute( + EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, + PosixFilePermission.OWNER_EXECUTE))); + } + + private void deleteDirectoryWithDirectEntries(Path directory) throws IOException { + try (DirectoryStream entries = Files.newDirectoryStream(directory)) { + for (Path entry : entries) { + Files.delete(entry); + } } - Files.delete(socketFilePath); + Files.delete(directory); } private void setOwnerOnlyPermissions(Path socketFilePath) throws IOException { @@ -365,6 +452,12 @@ private String getPid() { return name.split("@")[0]; } + @FunctionalInterface + private interface CleanupAction { + + void run() throws Exception; + } + private static final class RequestTooLargeException extends IOException { private static final long serialVersionUID = 1L; diff --git a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java index f8bb4e1dc10..136f3a93947 100644 --- a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java +++ b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java @@ -371,16 +371,18 @@ public void testInitService() { public void testAdminRpcAndIpcConfigBinding() { Map override = new HashMap<>(); override.put("storage.db.directory", "database"); - override.put("node.ipcEnable", "true"); - override.put("node.adminRpc.enable", "true"); - override.put("node.adminRpc.listenAddress", "127.0.0.2"); - override.put("node.adminRpc.port", "18575"); + override.put("node.admin.ipc.enable", "true"); + override.put("node.admin.ipc.socketDirectory", "/tmp/tron-ipc"); + override.put("node.admin.rpc.enable", "true"); + override.put("node.admin.rpc.listenAddress", "127.0.0.2"); + override.put("node.admin.rpc.port", "18575"); Config config = ConfigFactory.parseMap(override) .withFallback(ConfigFactory.defaultReference()); try { Args.applyConfigParams(config); Assert.assertTrue(Args.getInstance().isIpcEnable()); + Assert.assertEquals("/tmp/tron-ipc", Args.getInstance().getIpcSocketDirectory()); Assert.assertTrue(Args.getInstance().isAdminRpcEnable()); Assert.assertEquals("127.0.0.2", Args.getInstance().getAdminListenAddress()); Assert.assertEquals(18575, Args.getInstance().getAdminListenPort()); diff --git a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java index 3f191eee8cc..ef75b43c1d5 100644 --- a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java @@ -22,13 +22,17 @@ import java.nio.file.attribute.PosixFilePermission; import java.util.Arrays; import java.util.EnumSet; +import java.util.Set; import org.junit.Assert; import org.junit.Assume; import org.junit.Test; +import org.mockito.MockedStatic; import org.mockito.Mockito; +import org.newsclub.net.unix.AFUNIXServerSocket; import org.newsclub.net.unix.AFUNIXSocket; import org.newsclub.net.unix.AFUNIXSocketAddress; import org.tron.common.parameter.CommonParameter; +import org.tron.core.Constant; import org.tron.core.config.args.Args; import org.tron.core.exception.TronError; import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; @@ -41,30 +45,37 @@ public class IpcServiceTest { public void testResolveSocketFilePathUsesOutputDirectory() throws Exception { IpcService service = newIpcService(); CommonParameter parameter = new CommonParameter(); - parameter.outputDirectory = "node-output"; + parameter.outputDirectory = "/tmp/node-output"; Path socketFilePath = resolveSocketFilePath(service, parameter, "1234"); Assert.assertEquals( - Paths.get("node-output", "java-tron.1234.sock").toAbsolutePath().normalize(), + Paths.get("/tmp/node-output", ".ipc", "1234.sock"), socketFilePath); } @Test - public void testResolveSocketFilePathFallsBackToTmpForLongOutputPath() throws Exception { + public void testResolveSocketFilePathRejectsLongOutputPath() throws Exception { IpcService service = newIpcService(); CommonParameter parameter = new CommonParameter(); parameter.outputDirectory = Paths.get("/tmp", "a-very-long-output-directory-name-that-makes-the-resulting-unix-domain-socket-path-" + "exceed-the-portable-limit").toString(); - Path socketFilePath = resolveSocketFilePath(service, parameter, "1234"); - - Assert.assertEquals(Paths.get("/tmp", "java-tron.1234.sock"), socketFilePath); + try { + resolveSocketFilePath(service, parameter, "1234"); + Assert.fail("Expected an overlong IPC socket path to be rejected"); + } catch (TronError e) { + Path expectedSocketFile = Paths.get(parameter.outputDirectory, ".ipc", "1234.sock") + .toAbsolutePath().normalize(); + Assert.assertTrue(e.getMessage().contains("exceeding the portable limit of 100 bytes")); + Assert.assertTrue(e.getMessage().contains("node.admin.ipc.socketDirectory")); + Assert.assertTrue(e.getMessage().contains(expectedSocketFile.toString())); + } } @Test - public void testResolveSocketFilePathCountsEncodedBytes() throws Exception { + public void testResolveSocketFilePathRejectsEncodedPathOverLimit() throws Exception { IpcService service = newIpcService(); CommonParameter parameter = new CommonParameter(); StringBuilder outputDirectory = new StringBuilder("/tmp/"); @@ -73,74 +84,149 @@ public void testResolveSocketFilePathCountsEncodedBytes() throws Exception { } parameter.outputDirectory = outputDirectory.toString(); + try { + resolveSocketFilePath(service, parameter, "1234"); + Assert.fail("Expected the encoded IPC socket path length to be checked"); + } catch (TronError e) { + Assert.assertTrue(e.getMessage().contains("exceeding the portable limit of 100 bytes")); + } + } + + @Test + public void testResolveSocketFilePathUsesConfiguredDirectory() throws Exception { + IpcService service = newIpcService(); + CommonParameter parameter = new CommonParameter(); + parameter.outputDirectory = "node-output"; + parameter.ipcSocketDirectory = "/tmp/tron-ipc"; + Path socketFilePath = resolveSocketFilePath(service, parameter, "1234"); - Assert.assertEquals(Paths.get("/tmp", "java-tron.1234.sock"), socketFilePath); + Assert.assertEquals(Paths.get("/tmp/tron-ipc/.ipc/1234.sock"), + socketFilePath); + } + + @Test + public void testResolveSocketFilePathRejectsRelativeConfiguredDirectory() throws Exception { + IpcService service = newIpcService(); + CommonParameter parameter = new CommonParameter(); + parameter.ipcSocketDirectory = "relative-ipc"; + + try { + resolveSocketFilePath(service, parameter, "1234"); + Assert.fail("Expected a relative IPC socket directory to be rejected"); + } catch (TronError e) { + Assert.assertEquals("node.admin.ipc.socketDirectory must be an absolute path", + e.getMessage()); + } + } + + @Test + public void testResolveSocketFilePathRejectsLongConfiguredDirectory() throws Exception { + IpcService service = newIpcService(); + CommonParameter parameter = new CommonParameter(); + parameter.outputDirectory = "/tmp"; + parameter.ipcSocketDirectory = Paths.get("/tmp", + "a-very-long-explicit-ipc-directory-that-makes-the-resulting-unix-domain-socket-path-" + + "exceed-the-portable-limit").toString(); + + try { + resolveSocketFilePath(service, parameter, "1234"); + Assert.fail("Expected an overlong configured IPC socket path to be rejected"); + } catch (TronError e) { + Path expectedSocketFile = Paths.get(parameter.ipcSocketDirectory, ".ipc", "1234.sock") + .toAbsolutePath().normalize(); + Assert.assertTrue(e.getMessage().contains("exceeding the portable limit of 100 bytes")); + Assert.assertTrue(e.getMessage().contains("node.admin.ipc.socketDirectory")); + Assert.assertTrue(e.getMessage().contains(expectedSocketFile.toString())); + } } @Test - public void testValidateOutputDirectoryRejectsMissingDirectory() throws Exception { + public void testValidateSocketRootDirectoryRejectsMissingDirectory() throws Exception { IpcService service = newIpcService(); Path outputDirectory = Files.createTempDirectory("ipc-missing-output-test-"); Files.delete(outputDirectory); try { - validateOutputDirectory(service, outputDirectory); + validateSocketRootDirectory(service, outputDirectory); Assert.fail("Expected a missing output directory to be rejected"); } catch (TronError e) { - Assert.assertEquals("IPC output directory does not exist or is not a directory", + Assert.assertEquals("IPC socket root directory does not exist or is not a directory", e.getMessage()); } } @Test - public void testDeleteStaleSocketFileRejectsRegularFile() throws Exception { + public void testRecreateSocketDirectoryRejectsRegularFile() throws Exception { IpcService service = newIpcService(); Path outputDirectory = Files.createTempDirectory("ipc-regular-file-test-"); - Path socketFile = outputDirectory.resolve("java-tron.1234.sock"); - Files.createFile(socketFile); + Path socketDirectory = outputDirectory.resolve(".ipc"); + Files.createFile(socketDirectory); try { - deleteStaleSocketFile(service, socketFile); - Assert.fail("Expected a regular file to be preserved"); + recreateSocketDirectory(service, socketDirectory); + Assert.fail("Expected a regular file at the reserved directory path to be preserved"); } catch (TronError e) { - Assert.assertEquals("Refusing to replace a non-socket IPC endpoint", e.getMessage()); - Assert.assertTrue(Files.isRegularFile(socketFile, LinkOption.NOFOLLOW_LINKS)); + Assert.assertEquals("Refusing to replace a non-directory IPC path", e.getMessage()); + Assert.assertTrue(Files.isRegularFile(socketDirectory, LinkOption.NOFOLLOW_LINKS)); } finally { - Files.deleteIfExists(socketFile); + Files.deleteIfExists(socketDirectory); Files.deleteIfExists(outputDirectory); } } @Test - public void testDeleteStaleSocketFileRejectsSymbolicLink() throws Exception { + public void testRecreateSocketDirectoryRejectsSymbolicLink() throws Exception { assumePosixFileSystem(); IpcService service = newIpcService(); Path outputDirectory = Files.createTempDirectory("ipc-symbolic-link-test-"); Path targetFile = outputDirectory.resolve("target"); - Path socketFile = outputDirectory.resolve("java-tron.1234.sock"); + Path socketDirectory = outputDirectory.resolve(".ipc"); Files.createFile(targetFile); - Files.createSymbolicLink(socketFile, targetFile.getFileName()); + Files.createSymbolicLink(socketDirectory, targetFile.getFileName()); try { - deleteStaleSocketFile(service, socketFile); + recreateSocketDirectory(service, socketDirectory); Assert.fail("Expected a symbolic link to be preserved"); } catch (TronError e) { - Assert.assertEquals("Refusing to replace a non-socket IPC endpoint", e.getMessage()); - Assert.assertTrue(Files.isSymbolicLink(socketFile)); + Assert.assertEquals("Refusing to replace a non-directory IPC path", e.getMessage()); + Assert.assertTrue(Files.isSymbolicLink(socketDirectory)); Assert.assertTrue(Files.exists(targetFile)); } finally { - Files.deleteIfExists(socketFile); + Files.deleteIfExists(socketDirectory); Files.deleteIfExists(targetFile); Files.deleteIfExists(outputDirectory); } } @Test - public void testValidateOutputDirectorySupportsPosixPermissions() throws Exception { + public void testRecreateSocketDirectoryRemovesStaleFilesAndUsesOwnerOnlyPermissions() + throws Exception { + assumePosixFileSystem(); + IpcService service = newIpcService(); + Path outputDirectory = Files.createTempDirectory("ipc-stale-directory-test-"); + Path socketDirectory = Files.createDirectory(outputDirectory.resolve(".ipc")); + Files.createFile(socketDirectory.resolve("1234.sock")); + try { + recreateSocketDirectory(service, socketDirectory); + + Assert.assertTrue(Files.isDirectory(socketDirectory)); + Assert.assertEquals( + EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, + PosixFilePermission.OWNER_EXECUTE), + Files.getPosixFilePermissions(socketDirectory)); + Assert.assertFalse(Files.exists(socketDirectory.resolve("1234.sock"))); + } finally { + Files.deleteIfExists(socketDirectory); + Files.deleteIfExists(outputDirectory); + } + } + + @Test + public void testValidateSocketRootDirectorySupportsPosixPermissions() throws Exception { assumePosixFileSystem(); IpcService service = newIpcService(); Path outputDirectory = Files.createTempDirectory("ipc-posix-output-test-"); try { - validateOutputDirectory(service, outputDirectory); + validateSocketRootDirectory(service, outputDirectory); } finally { Files.deleteIfExists(outputDirectory); } @@ -199,6 +285,26 @@ public void testHandleCommandUsesAnnotatedErrorResolver() throws Exception { Assert.assertEquals(10, responseNode.get("id").asInt()); } + @Test + public void testIpcMapperRejectsExcessiveNesting() throws Exception { + StringBuilder request = new StringBuilder(); + for (int i = 0; i <= Constant.MAX_NESTING_DEPTH; i++) { + request.append('['); + } + request.append('0'); + for (int i = 0; i <= Constant.MAX_NESTING_DEPTH; i++) { + request.append(']'); + } + + ObjectMapper mapper = getStaticObjectMapper("OBJECT_MAPPER"); + try { + mapper.readTree(request.toString()); + Assert.fail("Expected excessive IPC JSON nesting to be rejected"); + } catch (IOException e) { + Assert.assertTrue(e.getMessage().contains("nesting depth")); + } + } + @Test public void testReadRequestAcceptsMaximumSize() throws Exception { IpcService service = newIpcService(); @@ -245,6 +351,47 @@ public void testSocketFileUsesOwnerOnlyPermissions() throws Exception { } } + @Test(timeout = 10_000) + public void testInnerStartCleansSocketWhenPermissionUpdateFails() throws Exception { + assumePosixFileSystem(); + CommonParameter parameter = Args.getInstance(); + String originalOutputDirectory = parameter.outputDirectory; + String originalSocketDirectory = parameter.ipcSocketDirectory; + Path socketDirectory = Files.createTempDirectory(Paths.get("/tmp"), "ipc-perm-fail-"); + IpcService service = newIpcService(); + Path socketFile = null; + try { + parameter.outputDirectory = socketDirectory.toString(); + parameter.ipcSocketDirectory = ""; + socketFile = resolveSocketFilePath(service, parameter, getPid(service)); + Set permissions = EnumSet.of(PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE); + try (MockedStatic files = Mockito.mockStatic(Files.class, + Mockito.CALLS_REAL_METHODS)) { + Path expectedSocketFile = socketFile; + files.when(() -> Files.setPosixFilePermissions(expectedSocketFile, permissions)) + .thenThrow(new IOException("permission update failed")); + + try { + service.innerStart(); + Assert.fail("Expected the permission update failure to be preserved"); + } catch (IOException e) { + Assert.assertEquals("permission update failed", e.getMessage()); + } + } + + Assert.assertFalse(Files.exists(socketFile)); + } finally { + parameter.outputDirectory = originalOutputDirectory; + parameter.ipcSocketDirectory = originalSocketDirectory; + if (socketFile != null) { + Files.deleteIfExists(socketFile); + Files.deleteIfExists(socketFile.getParent()); + } + Files.deleteIfExists(socketDirectory); + } + } + @Test(timeout = 10_000) public void testHandlesMultipleClientsConcurrently() throws Exception { assumePosixFileSystem(); @@ -306,7 +453,7 @@ public void testStopClosesActiveClientSocket() throws Exception { assumePosixFileSystem(); CommonParameter parameter = Args.getInstance(); String originalOutputDirectory = parameter.outputDirectory; - Path outputDirectory = Files.createTempDirectory("ipc-test-"); + Path outputDirectory = Files.createTempDirectory(Paths.get("/tmp"), "ipc-test-"); IpcService service = new IpcService( new AdminJsonRpcImpl()); boolean started = false; @@ -345,12 +492,72 @@ public void testStopClosesActiveClientSocket() throws Exception { } } + @Test(timeout = 5_000) + public void testInnerStopContinuesCleanupAfterServerCloseFailure() throws Exception { + IpcService service = newIpcService(); + AFUNIXServerSocket serverSocket = Mockito.mock(AFUNIXServerSocket.class); + AFUNIXSocket clientSocket = Mockito.mock(AFUNIXSocket.class); + Path socketRootDirectory = Files.createTempDirectory("ipc-stop-failure-test-"); + Path socketDirectory = Files.createDirectory(socketRootDirectory.resolve(".ipc")); + Path socketFile = Files.createFile(socketDirectory.resolve("1234.sock")); + Mockito.doThrow(new IOException("server close failed")).when(serverSocket).close(); + setField(service, "unixServerSocket", serverSocket); + setField(service, "socketFilePath", socketFile); + getActiveClientSockets(service).add(clientSocket); + + try { + try { + service.innerStop(); + Assert.fail("Expected the server socket close failure to be preserved"); + } catch (IOException e) { + Assert.assertEquals("server close failed", e.getMessage()); + } + + Mockito.verify(clientSocket).shutdownInput(); + Mockito.verify(clientSocket).shutdownOutput(); + Mockito.verify(clientSocket).close(); + Assert.assertFalse(Files.exists(socketFile)); + Assert.assertFalse(Files.exists(socketDirectory)); + } finally { + Files.deleteIfExists(socketFile); + Files.deleteIfExists(socketDirectory); + Files.deleteIfExists(socketRootDirectory); + } + } + + @Test(timeout = 5_000) + public void testInnerStopSuppressesLaterCleanupFailure() throws Exception { + IpcService service = newIpcService(); + AFUNIXServerSocket serverSocket = Mockito.mock(AFUNIXServerSocket.class); + Path socketRootDirectory = Files.createTempDirectory("ipc-stop-suppressed-test-"); + Path socketDirectory = Files.createDirectory( + socketRootDirectory.resolve(".ipc")); + Path childFile = Files.createFile(socketDirectory.resolve("child")); + Mockito.doThrow(new IOException("server close failed")).when(serverSocket).close(); + setField(service, "unixServerSocket", serverSocket); + setField(service, "socketFilePath", socketDirectory.resolve("1234.sock")); + + try { + service.innerStop(); + Assert.fail("Expected cleanup failures to be preserved"); + } catch (IOException e) { + Assert.assertEquals("server close failed", e.getMessage()); + Assert.assertEquals(1, e.getSuppressed().length); + Assert.assertTrue(e.getSuppressed()[0] instanceof IOException); + } finally { + Files.deleteIfExists(childFile); + Files.deleteIfExists(socketDirectory); + Files.deleteIfExists(socketRootDirectory); + } + } + @Test public void testCleanupRestoresOutputDirectoryWhenStopFails() throws Exception { CommonParameter parameter = new CommonParameter(); String originalOutputDirectory = parameter.outputDirectory; Path outputDirectory = Files.createTempDirectory("ipc-cleanup-test-"); - Path socketFile = Files.createFile(outputDirectory.resolve("java-tron.1234.sock")); + Path socketDirectory = Files.createDirectory(outputDirectory.resolve(".ipc")); + Path socketFile = Files.createFile(socketDirectory.resolve("1234.sock")); parameter.outputDirectory = outputDirectory.toString(); IpcService service = Mockito.mock(IpcService.class); Mockito.doThrow(new IOException("stop failed")).when(service).innerStop(); @@ -365,6 +572,7 @@ public void testCleanupRestoresOutputDirectoryWhenStopFails() throws Exception { Assert.assertEquals(originalOutputDirectory, parameter.outputDirectory); Assert.assertFalse(Files.exists(socketFile)); + Assert.assertFalse(Files.exists(socketDirectory)); Assert.assertFalse(Files.exists(outputDirectory)); } @@ -386,6 +594,7 @@ private void cleanupIpcService(IpcService service, boolean started, CommonParame try { if (socketFile != null) { Files.deleteIfExists(socketFile); + Files.deleteIfExists(socketFile.getParent()); } } catch (IOException e) { failure = mergeCleanupFailure(failure, e); @@ -414,13 +623,15 @@ private Path resolveSocketFilePath(IpcService service, CommonParameter parameter new Class[] {CommonParameter.class, String.class}, parameter, pid); } - private void validateOutputDirectory(IpcService service, Path outputDirectory) throws Exception { - invokePrivate(service, "validateOutputDirectory", new Class[] {Path.class}, + private void validateSocketRootDirectory(IpcService service, Path outputDirectory) + throws Exception { + invokePrivate(service, "validateSocketRootDirectory", new Class[] {Path.class}, outputDirectory); } - private void deleteStaleSocketFile(IpcService service, Path socketFile) throws Exception { - invokePrivate(service, "deleteStaleSocketFile", new Class[] {Path.class}, socketFile); + private void recreateSocketDirectory(IpcService service, Path socketDirectory) throws Exception { + invokePrivate(service, "recreateSocketDirectory", new Class[] {Path.class}, + socketDirectory); } private String readRequest(IpcService service, InputStream input) throws Exception { @@ -434,14 +645,33 @@ private int getStaticIntField(String fieldName) throws Exception { return field.getInt(null); } - private void registerClient(IpcService service, AFUNIXSocket client) throws Exception { - invokePrivate(service, "registerClient", new Class[] {AFUNIXSocket.class}, client); + private ObjectMapper getStaticObjectMapper(String fieldName) throws Exception { + Field field = IpcService.class.getDeclaredField(fieldName); + field.setAccessible(true); + return (ObjectMapper) field.get(null); + } + + @SuppressWarnings("unchecked") + private Set getActiveClientSockets(IpcService service) throws Exception { + Field field = IpcService.class.getDeclaredField("activeClientSockets"); + field.setAccessible(true); + return (Set) field.get(service); + } + + private void setField(IpcService service, String fieldName, Object value) throws Exception { + Field field = IpcService.class.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(service, value); } private String getPid(IpcService service) throws Exception { return (String) invokePrivate(service, "getPid", new Class[0]); } + private void registerClient(IpcService service, AFUNIXSocket client) throws Exception { + invokePrivate(service, "registerClient", new Class[] {AFUNIXSocket.class}, client); + } + private Object invokePrivate(IpcService service, String methodName, Class[] parameterTypes, Object... arguments) throws Exception { Method method = IpcService.class.getDeclaredMethod(methodName, parameterTypes); From 66b60f41252064c932294643418269f1a7b33aeb Mon Sep 17 00:00:00 2001 From: 317787106 <317787106@qq.com> Date: Wed, 12 Aug 2026 23:17:15 +0800 Subject: [PATCH 12/15] reorg admin config, add ipc.socketDirectory --- common/src/main/resources/reference.conf | 32 ++++++++++++++++-------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index bc9c6d1e3fc..085322a16e1 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -442,17 +442,27 @@ node { maxMessageSize = 4194304 } - # Whether to enable the local Unix-domain socket admin API. - ipcEnable = false - - # Administrative JSON-RPC settings. Disabled by default and bound to loopback only. - adminRpc { - # Whether to enable the administrative JSON-RPC HTTP service. Default: false. - enable = false - # Address on which the service listens. Keep the default loopback address for security. - listenAddress = "127.0.0.1" - # TCP port on which the administrative JSON-RPC HTTP service listens. Default: 8575. - port = 8575 + # Administrative API settings. Disabled by default. + admin { + # Local Unix-domain socket administrative API. + ipc { + # Whether to enable the local Unix-domain socket admin API. Default: false. + enable = false + # Parent directory for the private .ipc directory. It must be an absolute path + # when set. Empty means output-directory. The node fails to start if the resulting socket + # path exceeds the portable Unix-domain socket path limit. + socketDirectory = "" + } + + # Administrative JSON-RPC HTTP API. + rpc { + # Whether to enable the administrative JSON-RPC HTTP service. Default: false. + enable = false + # Address on which the service listens. Keep the default loopback address for security. + listenAddress = "127.0.0.1" + # TCP port on which the administrative JSON-RPC HTTP service listens. Default: 8575. + port = 8575 + } } # Disabled API list (works for http, rpc and pbft, not jsonrpc). Case insensitive. From 300fd6e8d7e4a1cae4c85aadffcf794b710ac637 Mon Sep 17 00:00:00 2001 From: 317787106 <317787106@qq.com> Date: Thu, 13 Aug 2026 00:21:14 +0800 Subject: [PATCH 13/15] add admin.rpc.virtualHosts; output error message if --attach is invalid --- .../common/parameter/CommonParameter.java | 4 + .../org/tron/core/config/args/NodeConfig.java | 2 + common/src/main/resources/reference.conf | 3 + .../tron/core/config/args/NodeConfigTest.java | 9 ++- .../java/org/tron/core/config/args/Args.java | 32 +++----- .../services/admin/http/AdminRpcServlet.java | 81 +++++++++++++++++++ .../core/services/admin/ipc/IpcClient.java | 2 +- .../core/services/admin/ipc/IpcService.java | 8 +- .../org/tron/core/config/args/ArgsTest.java | 55 ++++++++----- .../admin/http/AdminRpcServletTest.java | 56 +++++++++++++ .../services/admin/ipc/IpcServiceTest.java | 35 ++++++++ 11 files changed, 236 insertions(+), 51 deletions(-) diff --git a/common/src/main/java/org/tron/common/parameter/CommonParameter.java b/common/src/main/java/org/tron/common/parameter/CommonParameter.java index 9945ce08df6..ab98c1be5f5 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -4,6 +4,7 @@ import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import lombok.Getter; import lombok.Setter; @@ -499,6 +500,9 @@ public class CommonParameter { public int adminListenPort = 8575; @Getter @Setter + public List adminVirtualHosts = new ArrayList<>(Collections.singletonList("localhost")); + @Getter + @Setter public boolean ipcEnable = false; @Getter @Setter diff --git a/common/src/main/java/org/tron/core/config/args/NodeConfig.java b/common/src/main/java/org/tron/core/config/args/NodeConfig.java index d6bcafa8291..79a6f4de6f5 100644 --- a/common/src/main/java/org/tron/core/config/args/NodeConfig.java +++ b/common/src/main/java/org/tron/core/config/args/NodeConfig.java @@ -7,6 +7,7 @@ import com.typesafe.config.ConfigBeanFactory; import com.typesafe.config.ConfigValueFactory; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import lombok.Getter; import lombok.Setter; @@ -277,6 +278,7 @@ public static class AdminRpcConfig { private boolean enable = false; private String listenAddress = Constant.LOCAL_HOST; private int port = 8575; + private List virtualHosts = new ArrayList<>(Collections.singletonList("localhost")); } @Getter diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index 085322a16e1..2ee42f4a803 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -462,6 +462,9 @@ node { listenAddress = "127.0.0.1" # TCP port on which the administrative JSON-RPC HTTP service listens. Default: 8575. port = 8575 + # Allowed HTTP Host header names. Matching is case-insensitive and ignores the port. + # IP address literals are always allowed. Use ["*"] only to explicitly allow any hostname. + virtualHosts = ["localhost"] } } diff --git a/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java b/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java index 8c4a767c54a..6ca43981fde 100644 --- a/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/NodeConfigTest.java @@ -7,6 +7,8 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; +import java.util.Arrays; +import java.util.Collections; import org.junit.Test; import org.tron.core.exception.TronError; @@ -35,6 +37,8 @@ public void testDefaults() { assertFalse(nc.getAdmin().getRpc().isEnable()); assertEquals("127.0.0.1", nc.getAdmin().getRpc().getListenAddress()); assertEquals(8575, nc.getAdmin().getRpc().getPort()); + assertEquals(Collections.singletonList("localhost"), + nc.getAdmin().getRpc().getVirtualHosts()); // reference.conf matches code default: discovery disabled when not configured assertFalse(nc.isDiscoveryEnable()); assertFalse(nc.isDiscoveryPersist()); @@ -88,13 +92,16 @@ public void testRpcSubBean() { public void testAdminRpcAndIpcBinding() { Config config = withRef( "node.admin { ipc { enable = true, socketDirectory = \"/tmp/tron-ipc\" }," - + " rpc { enable = true, listenAddress = \"127.0.0.2\", port = 18575 } }"); + + " rpc { enable = true, listenAddress = \"127.0.0.2\", port = 18575," + + " virtualHosts = [\"admin.example.com\", \"localhost\"] } }"); NodeConfig nc = NodeConfig.fromConfig(config); assertTrue(nc.getAdmin().getIpc().isEnable()); assertEquals("/tmp/tron-ipc", nc.getAdmin().getIpc().getSocketDirectory()); assertTrue(nc.getAdmin().getRpc().isEnable()); assertEquals("127.0.0.2", nc.getAdmin().getRpc().getListenAddress()); assertEquals(18575, nc.getAdmin().getRpc().getPort()); + assertEquals(Arrays.asList("admin.example.com", "localhost"), + nc.getAdmin().getRpc().getVirtualHosts()); } @Test diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index c7c371e0dac..d14542cc092 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -9,7 +9,6 @@ import com.beust.jcommander.JCommander; import com.beust.jcommander.ParameterDescription; -import com.beust.jcommander.ParameterException; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.typesafe.config.Config; @@ -203,26 +202,14 @@ private static boolean tryApplyAttachParams(CLIParameter cmd, boolean attachAssigned = isParameterAssigned(assignedParameters, "ipcSocketFile"); if (!attachAssigned) { if (isParameterAssigned(assignedParameters, "ipcExecCommand")) { - throw new ParameterException("--exec requires --attach "); + throwAttachParameterError("Error: --exec requires --attach "); } return false; } if (StringUtils.isBlank(cmd.ipcSocketFile)) { - throw new ParameterException("--attach requires a non-empty "); - } - - List unsupportedOptions = assignedParameters.stream() - .filter(pd -> !isAttachParameter(pd)) - .map(ParameterDescription::getLongestName) - .collect(Collectors.toList()); - if (!cmd.seedNodes.isEmpty()) { - unsupportedOptions.add("seedNode"); - } - if (!unsupportedOptions.isEmpty()) { - Collections.sort(unsupportedOptions); - throw new ParameterException("--attach cannot be combined with: " - + String.join(", ", unsupportedOptions)); + throwAttachParameterError("Error: --attach requires a non-empty "); } + //ignore seenodes from cmd ipcSocketFile = cmd.ipcSocketFile; ipcExecCommand = cmd.ipcExecCommand; if (StringUtils.isNotEmpty(cmd.logbackPath)) { @@ -231,19 +218,17 @@ private static boolean tryApplyAttachParams(CLIParameter cmd, return true; } + private static void throwAttachParameterError(String message) { + System.err.println(message); + throw new TronError(message, TronError.ErrCode.PARAMETER_INIT); + } + private static boolean isParameterAssigned(List assignedParameters, String fieldName) { return assignedParameters.stream() .anyMatch(pd -> fieldName.equals(pd.getParameterized().getName())); } - private static boolean isAttachParameter(ParameterDescription parameter) { - String fieldName = parameter.getParameterized().getName(); - return "ipcSocketFile".equals(fieldName) - || "ipcExecCommand".equals(fieldName) - || "logbackPath".equals(fieldName); - } - /** * Bridge VmConfig bean values to CommonParameter fields. * Temporary until Phase 2 moves fields into domain config objects. @@ -630,6 +615,7 @@ private static void applyNodeConfig(NodeConfig nc) { PARAMETER.adminRpcEnable = adminRpc.isEnable(); PARAMETER.adminListenAddress = adminRpc.getListenAddress(); PARAMETER.adminListenPort = adminRpc.getPort(); + PARAMETER.adminVirtualHosts = new ArrayList<>(adminRpc.getVirtualHosts()); PARAMETER.ipcEnable = adminIpc.isEnable(); PARAMETER.ipcSocketDirectory = adminIpc.getSocketDirectory(); diff --git a/framework/src/main/java/org/tron/core/services/admin/http/AdminRpcServlet.java b/framework/src/main/java/org/tron/core/services/admin/http/AdminRpcServlet.java index 77f1a604288..3d2b1ab10f0 100644 --- a/framework/src/main/java/org/tron/core/services/admin/http/AdminRpcServlet.java +++ b/framework/src/main/java/org/tron/core/services/admin/http/AdminRpcServlet.java @@ -1,11 +1,16 @@ package org.tron.core.services.admin.http; +import com.google.common.net.InetAddresses; import com.googlecode.jsonrpc4j.HttpStatusCodeProvider; import com.googlecode.jsonrpc4j.JsonRpcInterceptor; import com.googlecode.jsonrpc4j.JsonRpcServer; import com.googlecode.jsonrpc4j.ProxyUtil; import java.io.IOException; import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; @@ -27,6 +32,7 @@ public class AdminRpcServlet extends RateLimiterServlet { private static final long serialVersionUID = 0L; private JsonRpcServer rpcServer = null; + private Set virtualHosts = Collections.emptySet(); @Autowired private AdminJsonRpc adminJsonRpc; @@ -64,10 +70,15 @@ public Integer getJsonRpcCode(int httpStatusCode) { if (CommonParameter.getInstance().isMetricsPrometheusEnable()) { rpcServer.setInterceptorList(Collections.singletonList(interceptor)); } + virtualHosts = normalizeVirtualHosts(CommonParameter.getInstance().getAdminVirtualHosts()); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + if (!isAllowedHost(req.getHeader("Host"))) { + resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid Host header"); + return; + } if (!JsonRpcMediaType.isSupported(req.getContentType())) { resp.setStatus(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE); resp.setContentLength(0); @@ -75,4 +86,74 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I } rpcServer.handle(req, resp); } + + private boolean isAllowedHost(String hostHeader) { + if (hostHeader == null || hostHeader.isEmpty()) { + // A browser always sends Host. Preserve compatibility for non-browser HTTP/1.0 clients. + return true; + } + String host = extractHost(hostHeader); + if (host == null) { + return false; + } + if (InetAddresses.isInetAddress(host)) { + return true; + } + return virtualHosts.contains("*") + || virtualHosts.contains(host.toLowerCase(Locale.ROOT)); + } + + private String extractHost(String hostHeader) { + // IPv6 + if (hostHeader.startsWith("[")) { + int closingBracket = hostHeader.indexOf(']'); + if (closingBracket <= 1) { + return null; + } + String suffix = hostHeader.substring(closingBracket + 1); + if (!suffix.isEmpty() && !isPortSuffix(suffix)) { + return null; + } + return hostHeader.substring(1, closingBracket); + } + + // IPv4 + int firstColon = hostHeader.indexOf(':'); + if (firstColon < 0) { + return hostHeader; + } + if (firstColon != hostHeader.lastIndexOf(':')) { + return hostHeader; + } + String suffix = hostHeader.substring(firstColon); + if (!isPortSuffix(suffix)) { + return null; + } + return hostHeader.substring(0, firstColon); + } + + private boolean isPortSuffix(String suffix) { + if (suffix.length() <= 1 || suffix.charAt(0) != ':') { + return false; + } + for (int i = 1; i < suffix.length(); i++) { + if (!Character.isDigit(suffix.charAt(i))) { + return false; + } + } + return true; + } + + private Set normalizeVirtualHosts(List configuredHosts) { + Set normalizedHosts = new HashSet<>(); + if (configuredHosts == null) { + return normalizedHosts; + } + for (String configuredHost : configuredHosts) { + if (configuredHost != null && !configuredHost.trim().isEmpty()) { + normalizedHosts.add(configuredHost.trim().toLowerCase(Locale.ROOT)); + } + } + return normalizedHosts; + } } diff --git a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcClient.java b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcClient.java index 6a7f561ce7f..0d8cb8e4df2 100644 --- a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcClient.java +++ b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcClient.java @@ -156,7 +156,7 @@ public int run() throws IOException { int run(String execCommand) throws IOException { File socketFile = new File(socketFilePath); if (!socketFile.exists()) { - System.err.println("IPC socket file does not exist: " + socketFile.getName()); + System.err.println("Error: IPC socket file does not exist: " + socketFile.getName()); return EXIT_FAILURE; } AFUNIXSocketAddress address = AFUNIXSocketAddress.of(socketFile); diff --git a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java index 1a5ef527f03..dd93caf6a09 100644 --- a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java +++ b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java @@ -27,11 +27,11 @@ import java.nio.file.attribute.PosixFilePermissions; import java.util.EnumSet; import java.util.Set; -import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.SynchronousQueue; import java.util.concurrent.TimeUnit; import lombok.extern.slf4j.Slf4j; import org.newsclub.net.unix.AFUNIXServerSocket; @@ -67,8 +67,8 @@ public class IpcService extends AbstractService { private final ExecutorService acceptorExecutor = ExecutorServiceManager.newSingleThreadExecutor(ACCEPTOR_EXECUTOR_NAME, true); private final ExecutorService clientExecutor = - ExecutorServiceManager.newThreadPoolExecutor(4, 16, 0L, TimeUnit.MILLISECONDS, - new ArrayBlockingQueue<>(16), CLIENT_EXECUTOR_NAME, true); + ExecutorServiceManager.newThreadPoolExecutor(4, 16, 60L, TimeUnit.SECONDS, + new SynchronousQueue<>(), CLIENT_EXECUTOR_NAME, true); private volatile boolean isRunning = true; private AFUNIXServerSocket unixServerSocket; @@ -125,7 +125,7 @@ public void innerStart() throws Exception { if (isRunning) { logger.error("Handle IPC request error", throwable); try { - TimeUnit.MILLISECONDS.sleep(1_000); + TimeUnit.MILLISECONDS.sleep(5_000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; diff --git a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java index 136f3a93947..04228858031 100644 --- a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java +++ b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java @@ -15,12 +15,13 @@ package org.tron.core.config.args; -import com.beust.jcommander.ParameterException; import com.google.common.collect.Lists; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; import io.grpc.internal.GrpcUtil; import io.grpc.netty.NettyServerBuilder; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.InetAddress; @@ -78,13 +79,10 @@ public void testAttachWithExecParameters() { public void testAttachRejectsNodeConfigOption() { Args.clearParam(); try { - Args.setParam(new String[] { + assertAttachParameterError(new String[] { "--attach", "/tmp/java-tron.sock", "--config", "config.conf" - }, TestConstants.TEST_CONF); - Assert.fail("Expected a node configuration option to be rejected"); - } catch (ParameterException e) { - Assert.assertEquals("--attach cannot be combined with: --config", e.getMessage()); + }, "--attach cannot be combined with: --config"); } finally { Args.clearParam(); } @@ -94,10 +92,8 @@ public void testAttachRejectsNodeConfigOption() { public void testAttachRejectsEmptySocketPath() { Args.clearParam(); try { - Args.setParam(new String[] {"--attach", ""}, TestConstants.TEST_CONF); - Assert.fail("Expected an empty socket path to be rejected"); - } catch (ParameterException e) { - Assert.assertEquals("--attach requires a non-empty ", e.getMessage()); + assertAttachParameterError(new String[] {"--attach", ""}, + "--attach requires a non-empty "); } finally { Args.clearParam(); } @@ -107,15 +103,11 @@ public void testAttachRejectsEmptySocketPath() { public void testAttachRejectsOtherNodeOptions() { Args.clearParam(); try { - Args.setParam(new String[] { + assertAttachParameterError(new String[] { "--attach", "/tmp/java-tron.sock", "--keystore-factory", "seed.example.org:18888" - }, TestConstants.TEST_CONF); - Assert.fail("Expected node startup options to be rejected"); - } catch (ParameterException e) { - Assert.assertEquals( - "--attach cannot be combined with: --keystore-factory, seedNode", e.getMessage()); + }, "--attach cannot be combined with: --keystore-factory, seedNode"); } finally { Args.clearParam(); } @@ -125,16 +117,31 @@ public void testAttachRejectsOtherNodeOptions() { public void testExecRequiresAttach() { Args.clearParam(); try { - Args.setParam(new String[] {"--exec", "admin_example"}, - TestConstants.TEST_CONF); - Assert.fail("Expected --exec without --attach to fail"); - } catch (ParameterException e) { - Assert.assertEquals("--exec requires --attach ", e.getMessage()); + assertAttachParameterError(new String[] {"--exec", "admin_example"}, + "--exec requires --attach "); } finally { Args.clearParam(); } } + private void assertAttachParameterError(String[] args, String expectedMessage) { + PrintStream originalErr = System.err; + ByteArrayOutputStream errorOutput = new ByteArrayOutputStream(); + PrintStream capturedErr = new PrintStream(errorOutput); + try { + System.setErr(capturedErr); + Args.setParam(args, TestConstants.TEST_CONF); + Assert.fail("Expected invalid attach parameters to fail"); + } catch (TronError e) { + Assert.assertEquals(TronError.ErrCode.PARAMETER_INIT, e.getErrCode()); + Assert.assertEquals(expectedMessage, e.getMessage()); + Assert.assertEquals(expectedMessage + System.lineSeparator(), errorOutput.toString()); + } finally { + System.setErr(originalErr); + capturedErr.close(); + } + } + @Test public void get() { Args.setParam(new String[] {"--keystore-factory"}, TestConstants.TEST_CONF); @@ -369,13 +376,15 @@ public void testInitService() { @Test public void testAdminRpcAndIpcConfigBinding() { - Map override = new HashMap<>(); + Map override = new HashMap<>(); override.put("storage.db.directory", "database"); override.put("node.admin.ipc.enable", "true"); override.put("node.admin.ipc.socketDirectory", "/tmp/tron-ipc"); override.put("node.admin.rpc.enable", "true"); override.put("node.admin.rpc.listenAddress", "127.0.0.2"); override.put("node.admin.rpc.port", "18575"); + override.put("node.admin.rpc.virtualHosts", + Arrays.asList("admin.example.com", "localhost")); Config config = ConfigFactory.parseMap(override) .withFallback(ConfigFactory.defaultReference()); @@ -386,6 +395,8 @@ public void testAdminRpcAndIpcConfigBinding() { Assert.assertTrue(Args.getInstance().isAdminRpcEnable()); Assert.assertEquals("127.0.0.2", Args.getInstance().getAdminListenAddress()); Assert.assertEquals(18575, Args.getInstance().getAdminListenPort()); + Assert.assertEquals(Arrays.asList("admin.example.com", "localhost"), + Args.getInstance().getAdminVirtualHosts()); } finally { Args.clearParam(); } diff --git a/framework/src/test/java/org/tron/core/services/admin/http/AdminRpcServletTest.java b/framework/src/test/java/org/tron/core/services/admin/http/AdminRpcServletTest.java index 73402946619..a429ca8c0ed 100644 --- a/framework/src/test/java/org/tron/core/services/admin/http/AdminRpcServletTest.java +++ b/framework/src/test/java/org/tron/core/services/admin/http/AdminRpcServletTest.java @@ -7,6 +7,8 @@ import java.io.IOException; import java.lang.reflect.Field; import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashSet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.junit.Before; @@ -27,6 +29,7 @@ public void setUp() throws Exception { setField("adminJsonRpc", mock(AdminJsonRpc.class)); setField("interceptor", mock(JsonRpcInterceptor.class)); servlet.init(new MockServletConfig()); + setVirtualHosts("localhost"); } @Test @@ -91,6 +94,47 @@ public void jsonContentTypesAreAccepted() throws Exception { doPost(body, "application/vnd.tron+json").getStatus()); } + @Test + public void unlistedVirtualHostIsRejected() throws Exception { + MockHttpServletResponse response = doPost( + "{\"jsonrpc\":\"2.0\",\"method\":\"admin_example\",\"id\":1}", + "application/json", "evil.example:8575"); + + assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus()); + } + + @Test + public void listedVirtualHostIsAcceptedCaseInsensitivelyAndWithoutPort() throws Exception { + setVirtualHosts("admin.example.com"); + + MockHttpServletResponse response = doPost( + "{\"jsonrpc\":\"2.0\",\"method\":\"admin_example\",\"id\":1}", + "application/json", "ADMIN.EXAMPLE.COM:8575"); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + } + + @Test + public void ipLiteralHostsAreAccepted() throws Exception { + String body = "{\"jsonrpc\":\"2.0\",\"method\":\"admin_example\",\"id\":1}"; + + assertEquals(HttpServletResponse.SC_OK, + doPost(body, "application/json", "127.0.0.1:8575").getStatus()); + assertEquals(HttpServletResponse.SC_OK, + doPost(body, "application/json", "[::1]:8575").getStatus()); + } + + @Test + public void wildcardVirtualHostAcceptsAnyHostname() throws Exception { + setVirtualHosts("*"); + + MockHttpServletResponse response = doPost( + "{\"jsonrpc\":\"2.0\",\"method\":\"admin_example\",\"id\":1}", + "application/json", "any.example:8575"); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + } + private void setField(String name, Object value) throws Exception { Field field = AdminRpcServlet.class.getDeclaredField(name); field.setAccessible(true); @@ -102,14 +146,26 @@ private MockHttpServletResponse doPost(String body) throws Exception { } private MockHttpServletResponse doPost(String body, String contentType) throws Exception { + return doPost(body, contentType, null); + } + + private MockHttpServletResponse doPost(String body, String contentType, String host) + throws Exception { MockHttpServletRequest request = new MockHttpServletRequest("POST", "/admin"); request.setContentType(contentType); request.setContent(body.getBytes(StandardCharsets.UTF_8)); + if (host != null) { + request.addHeader("Host", host); + } MockHttpServletResponse response = new MockHttpServletResponse(); servlet.callDoPost(request, response); return response; } + private void setVirtualHosts(String... hosts) throws Exception { + setField("virtualHosts", new HashSet<>(Arrays.asList(hosts))); + } + private static class TestableServlet extends AdminRpcServlet { void callDoPost(HttpServletRequest request, HttpServletResponse response) throws IOException { diff --git a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java index ef75b43c1d5..c33b64e20ce 100644 --- a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java @@ -23,6 +23,8 @@ import java.util.Arrays; import java.util.EnumSet; import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import org.junit.Assert; import org.junit.Assume; import org.junit.Test; @@ -448,6 +450,39 @@ public void testRegisterClientUsesDefaultIdleTimeout() throws Exception { } } + @Test(timeout = 10_000) + public void testRejectsClientImmediatelyWhenAllHandlersAreBusy() throws Exception { + IpcService service = newIpcService(); + CountDownLatch handlersStarted = new CountDownLatch(16); + CountDownLatch releaseHandlers = new CountDownLatch(1); + try { + for (int i = 0; i < 16; i++) { + AFUNIXSocket client = Mockito.mock(AFUNIXSocket.class); + Mockito.when(client.getInputStream()).thenAnswer(invocation -> { + handlersStarted.countDown(); + try { + releaseHandlers.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + throw new IOException("closed"); + }); + registerClient(service, client); + } + Assert.assertTrue("Expected all IPC handlers to start without queueing", + handlersStarted.await(5, TimeUnit.SECONDS)); + + AFUNIXSocket rejectedClient = Mockito.mock(AFUNIXSocket.class); + registerClient(service, rejectedClient); + + Mockito.verify(rejectedClient).close(); + Assert.assertFalse(getActiveClientSockets(service).contains(rejectedClient)); + } finally { + releaseHandlers.countDown(); + service.innerStop(); + } + } + @Test(timeout = 10_000) public void testStopClosesActiveClientSocket() throws Exception { assumePosixFileSystem(); From f0a6a9172064f6efa8166f0bd3808c0e7dd2a6b6 Mon Sep 17 00:00:00 2001 From: 317787106 <317787106@qq.com> Date: Thu, 13 Aug 2026 11:24:57 +0800 Subject: [PATCH 14/15] add read timeout for client; set isRunning = false when inits; --- .../core/services/admin/ipc/IpcClient.java | 70 +++++++------- .../core/services/admin/ipc/IpcService.java | 23 +++-- .../common/application/HttpServiceTest.java | 4 +- .../org/tron/core/config/args/ArgsTest.java | 3 - .../services/admin/ipc/IpcClientTest.java | 49 ++++++++++ .../services/admin/ipc/IpcServiceTest.java | 93 +++++++++++++++++-- 6 files changed, 191 insertions(+), 51 deletions(-) diff --git a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcClient.java b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcClient.java index 0d8cb8e4df2..f65e8591271 100644 --- a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcClient.java +++ b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcClient.java @@ -16,6 +16,7 @@ import java.lang.reflect.Method; import java.lang.reflect.Type; import java.net.Socket; +import java.net.SocketTimeoutException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; @@ -49,6 +50,7 @@ public class IpcClient { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final int EXEC_RESPONSE_TIMEOUT_MILLIS = 30_000; static final int EXIT_SUCCESS = 0; static final int EXIT_FAILURE = 1; @@ -193,6 +195,9 @@ int runExec(Socket socket, String commandLine) throws IOException { String request; try { request = buildRequest(commandWords); + } catch (JsonProcessingException e) { + System.err.println("Failed to build IPC request."); + return EXIT_FAILURE; } catch (IllegalArgumentException e) { System.err.println(e.getMessage()); return EXIT_FAILURE; @@ -201,6 +206,7 @@ int runExec(Socket socket, String commandLine) throws IOException { return "help".equalsIgnoreCase(commandWords.get(0)) ? EXIT_SUCCESS : EXIT_FAILURE; } + socket.setSoTimeout(EXEC_RESPONSE_TIMEOUT_MILLIS); try (BufferedWriter serverWriter = new BufferedWriter( new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8)); BufferedReader serverReader = new BufferedReader( @@ -211,18 +217,23 @@ int runExec(Socket socket, String commandLine) throws IOException { String response; do { - response = serverReader.readLine(); + try { + response = serverReader.readLine(); + } catch (SocketTimeoutException e) { + System.err.println("Timed out waiting for IPC response."); + return EXIT_FAILURE; + } } while (response != null && response.trim().isEmpty()); if (response == null) { System.err.println("Disconnected from server before receiving a response."); return EXIT_FAILURE; } - String formattedResponse = formatResponse(response); - if (isSuccessfulResponse(response)) { - System.out.println(formattedResponse); + ParsedResponse parsedResponse = parseResponse(response); + if (parsedResponse.successful) { + System.out.println(parsedResponse.formatted); return EXIT_SUCCESS; } - System.err.println(formattedResponse); + System.err.println(parsedResponse.formatted); return EXIT_FAILURE; } } @@ -324,14 +335,11 @@ private void inputRequest(final Socket socket, LineReader reader, AtomicBoolean serverWriter.write(request); serverWriter.newLine(); serverWriter.flush(); - } catch (UserInterruptException e) { - // Ctrl + C or server disconnected - break; - } catch (EndOfFileException e) { - // Ctrl + D + } catch (UserInterruptException | EndOfFileException e) { + // Ctrl + C, Ctrl + D, or server disconnected break; } catch (JsonProcessingException e) { - logger.error("Failed to build IPC request", e); + System.err.println("Failed to build IPC request."); } catch (SyntaxError e) { System.err.println("Invalid command syntax."); } catch (IllegalArgumentException e) { @@ -339,8 +347,6 @@ private void inputRequest(final Socket socket, LineReader reader, AtomicBoolean } catch (IOException e) { notifyDisconnected(connected, reader); break; - } catch (Exception e) { - logger.error("Failed to process IPC command", e); } } } catch (IOException e) { @@ -470,36 +476,27 @@ private IllegalArgumentException invalidParameterType(String parameterName, Java } String formatResponse(String response) { + return parseResponse(response).formatted; + } + + private ParsedResponse parseResponse(String response) { try { JsonNode root = OBJECT_MAPPER.readTree(response); if (root == null || root.isMissingNode()) { - return response; + return new ParsedResponse(response, false); } JsonNode error = root.get("error"); if (error != null && !error.isNull()) { String code = error.has("code") ? " " + error.get("code").asText() : ""; String message = error.has("message") ? error.get("message").asText() : "Unknown error"; - return "Error" + code + ": " + message; + return new ParsedResponse("Error" + code + ": " + message, false); } if (root.has("result")) { - return formatJsonValue(root.get("result")); - } - return formatJsonValue(root); - } catch (JsonProcessingException e) { - return response; - } - } - - boolean isSuccessfulResponse(String response) { - try { - JsonNode root = OBJECT_MAPPER.readTree(response); - if (root == null || !root.isObject()) { - return false; + return new ParsedResponse(formatJsonValue(root.get("result")), true); } - JsonNode error = root.get("error"); - return (error == null || error.isNull()) && root.has("result"); + return new ParsedResponse(formatJsonValue(root), false); } catch (JsonProcessingException e) { - return false; + return new ParsedResponse(response, false); } } @@ -523,6 +520,17 @@ private String buildJsonWithParameter(String cmd, List values) return OBJECT_MAPPER.writeValueAsString(params); } + private static class ParsedResponse { + + private final String formatted; + private final boolean successful; + + private ParsedResponse(String formatted, boolean successful) { + this.formatted = formatted; + this.successful = successful; + } + } + private static class AdminCommand { private final String name; diff --git a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java index dd93caf6a09..e84ac9f1fbe 100644 --- a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java +++ b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java @@ -57,29 +57,32 @@ public class IpcService extends AbstractService { private static final ObjectMapper OBJECT_MAPPER = JsonRpcMapper.create(); private static final String ACCEPTOR_EXECUTOR_NAME = "admin-ipc-acceptor"; private static final String CLIENT_EXECUTOR_NAME = "admin-ipc-client"; - private static final int MAX_REQUEST_SIZE = 4 * 1024 * 1024; //same as HttpService.maxRequestSize private static final int CLIENT_IDLE_TIMEOUT_MILLIS = 10 * 60 * 1000; private static final String IPC_DIRECTORY_NAME = ".ipc"; // macOS/Linux sun_path buffers are 104/108 bytes. Reserve one byte for the terminating null // and three bytes of portability margin below the smaller macOS limit. private static final int MAX_SOCKET_PATH_BYTES = 100; + + private final JsonRpcServer jsonRpcServer; + private final int maxRequestSize; + private final ExecutorService acceptorExecutor = ExecutorServiceManager.newSingleThreadExecutor(ACCEPTOR_EXECUTOR_NAME, true); private final ExecutorService clientExecutor = ExecutorServiceManager.newThreadPoolExecutor(4, 16, 60L, TimeUnit.SECONDS, new SynchronousQueue<>(), CLIENT_EXECUTOR_NAME, true); - private volatile boolean isRunning = true; + private final Set activeClientSockets = ConcurrentHashMap.newKeySet(); private AFUNIXServerSocket unixServerSocket; private Path socketFilePath; - private final JsonRpcServer jsonRpcServer; - private final Set activeClientSockets = ConcurrentHashMap.newKeySet(); + private volatile boolean isRunning; @Autowired public IpcService(AdminJsonRpc adminJsonRpc) { enable = isFullNode() && Args.getInstance().isIpcEnable(); + maxRequestSize = Args.getInstance().maxMessageSize; jsonRpcServer = new JsonRpcServer(OBJECT_MAPPER, adminJsonRpc, AdminJsonRpc.class); jsonRpcServer.setErrorResolver(JsonRpcErrorResolver.INSTANCE); jsonRpcServer.setShouldLogInvocationErrors(false); @@ -134,7 +137,13 @@ public void innerStart() throws Exception { } } }; - ExecutorServiceManager.submit(acceptorExecutor, runnable); + isRunning = true; + try { + ExecutorServiceManager.submit(acceptorExecutor, runnable); + } catch (RuntimeException e) { + isRunning = false; + throw cleanupFailedStart(e); + } } private void registerClient(AFUNIXSocket client) { @@ -191,7 +200,7 @@ private void handleClient(AFUNIXSocket client) { } catch (SocketTimeoutException e) { logger.debug("Closing IPC client after {} ms without input", CLIENT_IDLE_TIMEOUT_MILLIS); } catch (RequestTooLargeException e) { - logger.warn("IPC request exceeds maximum size of {} bytes", MAX_REQUEST_SIZE); + logger.warn("IPC request exceeds maximum size of {} bytes", maxRequestSize); } catch (IOException e) { if (isRunning) { logger.error("Client disconnected {}", client); @@ -206,7 +215,7 @@ private String readRequest(InputStream input) throws IOException { if (value == '\n') { break; } - if (request.size() >= MAX_REQUEST_SIZE) { + if (request.size() >= maxRequestSize) { throw new RequestTooLargeException(); } request.write(value); diff --git a/framework/src/test/java/org/tron/common/application/HttpServiceTest.java b/framework/src/test/java/org/tron/common/application/HttpServiceTest.java index 7dd6b77613c..ace49654418 100644 --- a/framework/src/test/java/org/tron/common/application/HttpServiceTest.java +++ b/framework/src/test/java/org/tron/common/application/HttpServiceTest.java @@ -41,8 +41,8 @@ public void testServerBindsConfiguredIpv4Address() throws Exception { int localPort = service.getConnector().getLocalPort(); Assert.assertTrue(localPort > 0); - try (Socket socket = new Socket("127.0.0.1", localPort)) { - Assert.assertTrue(socket.isConnected()); + try (Socket ignored = new Socket("127.0.0.1", localPort)) { + // Successful construction proves that the configured address accepts connections. } } finally { service.stop().get(10, TimeUnit.SECONDS); diff --git a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java index 04228858031..144e732a352 100644 --- a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java +++ b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java @@ -25,9 +25,6 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.InetAddress; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.Arrays; import java.util.HashMap; import java.util.Map; diff --git a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcClientTest.java b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcClientTest.java index 8579b24960e..0cbffd63bed 100644 --- a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcClientTest.java +++ b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcClientTest.java @@ -8,9 +8,12 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.IOException; +import java.io.InputStream; import java.io.PrintStream; import java.net.ServerSocket; import java.net.Socket; +import java.net.SocketTimeoutException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -252,6 +255,34 @@ public void testExecReturnsFailureWhenServerDisconnects() throws Exception { errorOutput.toString("UTF-8")); } + @Test + public void testExecTimesOutWaitingForResponse() throws Exception { + Socket socket = Mockito.mock(Socket.class); + Mockito.when(socket.getOutputStream()).thenReturn(new ByteArrayOutputStream()); + Mockito.when(socket.getInputStream()).thenReturn(new InputStream() { + @Override + public int read() throws IOException { + throw new SocketTimeoutException("timed out"); + } + }); + + PrintStream originalErr = System.err; + ByteArrayOutputStream errorOutput = new ByteArrayOutputStream(); + PrintStream capturedErr = new PrintStream(errorOutput, true, "UTF-8"); + try { + System.setErr(capturedErr); + Assert.assertEquals(IpcClient.EXIT_FAILURE, + new IpcClient("unused").runExec(socket, "admin_example a b")); + } finally { + System.setErr(originalErr); + capturedErr.close(); + } + + Mockito.verify(socket).setSoTimeout(30_000); + Assert.assertEquals("Timed out waiting for IPC response." + System.lineSeparator(), + errorOutput.toString("UTF-8")); + } + @Test(timeout = 10_000) public void testSessionPrintsResponseAndExitsWhenServerDisconnects() throws Exception { CountDownLatch inputStarted = new CountDownLatch(1); @@ -330,6 +361,24 @@ public void testSessionExitDoesNotInterruptInputThread() throws Exception { } } + @Test(timeout = 10_000) + public void testSessionDoesNotSwallowUnexpectedRuntimeException() throws Exception { + LineReader reader = Mockito.mock(LineReader.class); + IllegalStateException expected = new IllegalStateException("unexpected failure"); + Mockito.when(reader.readLine("> ")).thenThrow(expected); + + try (ServerSocket serverSocket = new ServerSocket(0); + Socket clientSocket = new Socket("127.0.0.1", serverSocket.getLocalPort()); + Socket serverConnection = serverSocket.accept()) { + try { + new IpcClient("unused").runSession(clientSocket, reader); + Assert.fail("Expected the unexpected runtime exception to propagate"); + } catch (IllegalStateException e) { + Assert.assertSame(expected, e); + } + } + } + private interface MissingParameterAnnotationApi { @JsonRpcMethod("admin_invalid") diff --git a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java index c33b64e20ce..653623d183d 100644 --- a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java @@ -24,9 +24,13 @@ import java.util.EnumSet; import java.util.Set; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; +import org.junit.After; import org.junit.Assert; import org.junit.Assume; +import org.junit.Before; import org.junit.Test; import org.mockito.MockedStatic; import org.mockito.Mockito; @@ -43,6 +47,31 @@ public class IpcServiceTest { + private int originalMaxMessageSize; + + @Before + public void setUp() { + originalMaxMessageSize = Args.getInstance().maxMessageSize; + Args.getInstance().maxMessageSize = 4 * 1024 * 1024; + } + + @After + public void tearDown() { + Args.getInstance().maxMessageSize = originalMaxMessageSize; + } + + @Test + public void testServiceIsNotRunningBeforeStart() throws Exception { + Assert.assertFalse(isRunning(newIpcService())); + } + + @Test + public void testRequestSizePreservesConfiguredZero() throws Exception { + Args.getInstance().maxMessageSize = 0; + + Assert.assertEquals(0, getIntField(newIpcService(), "maxRequestSize")); + } + @Test public void testResolveSocketFilePathUsesOutputDirectory() throws Exception { IpcService service = newIpcService(); @@ -310,7 +339,7 @@ public void testIpcMapperRejectsExcessiveNesting() throws Exception { @Test public void testReadRequestAcceptsMaximumSize() throws Exception { IpcService service = newIpcService(); - int maxRequestSize = getStaticIntField("MAX_REQUEST_SIZE"); + int maxRequestSize = getIntField(service, "maxRequestSize"); byte[] request = new byte[maxRequestSize + 1]; Arrays.fill(request, 0, maxRequestSize, (byte) '1'); request[maxRequestSize] = '\n'; @@ -322,7 +351,7 @@ public void testReadRequestAcceptsMaximumSize() throws Exception { @Test(expected = IOException.class) public void testReadRequestRejectsOversizedInputWithoutNewline() throws Exception { IpcService service = newIpcService(); - int maxRequestSize = getStaticIntField("MAX_REQUEST_SIZE"); + int maxRequestSize = getIntField(service, "maxRequestSize"); ByteArrayInputStream input = new ByteArrayInputStream(new byte[maxRequestSize + 1]); readRequest(service, input); @@ -394,6 +423,43 @@ public void testInnerStartCleansSocketWhenPermissionUpdateFails() throws Excepti } } + @Test(timeout = 10_000) + public void testInnerStartRollsBackWhenAcceptorSubmissionFails() throws Exception { + assumePosixFileSystem(); + CommonParameter parameter = Args.getInstance(); + String originalOutputDirectory = parameter.outputDirectory; + String originalSocketDirectory = parameter.ipcSocketDirectory; + Path outputDirectory = Files.createTempDirectory(Paths.get("/tmp"), + "ipc-submit-fail-"); + IpcService service = newIpcService(); + Path socketFile = null; + try { + parameter.outputDirectory = outputDirectory.toString(); + parameter.ipcSocketDirectory = ""; + socketFile = resolveSocketFilePath(service, parameter, getPid(service)); + getExecutorService(service, "acceptorExecutor").shutdownNow(); + + try { + service.innerStart(); + Assert.fail("Expected acceptor submission to fail"); + } catch (RejectedExecutionException e) { + Assert.assertFalse(isRunning(service)); + } + + Assert.assertFalse(Files.exists(socketFile)); + Assert.assertFalse(Files.exists(socketFile.getParent())); + } finally { + parameter.outputDirectory = originalOutputDirectory; + parameter.ipcSocketDirectory = originalSocketDirectory; + service.innerStop(); + if (socketFile != null) { + Files.deleteIfExists(socketFile); + Files.deleteIfExists(socketFile.getParent()); + } + Files.deleteIfExists(outputDirectory); + } + } + @Test(timeout = 10_000) public void testHandlesMultipleClientsConcurrently() throws Exception { assumePosixFileSystem(); @@ -442,6 +508,7 @@ public void testRegisterClientUsesDefaultIdleTimeout() throws Exception { AFUNIXSocket client = Mockito.mock(AFUNIXSocket.class); Mockito.doThrow(new IOException("closed")).when(client).getInputStream(); try { + setField(service, "isRunning", true); registerClient(service, client); Mockito.verify(client).setSoTimeout(10 * 60 * 1000); @@ -456,6 +523,7 @@ public void testRejectsClientImmediatelyWhenAllHandlersAreBusy() throws Exceptio CountDownLatch handlersStarted = new CountDownLatch(16); CountDownLatch releaseHandlers = new CountDownLatch(1); try { + setField(service, "isRunning", true); for (int i = 0; i < 16; i++) { AFUNIXSocket client = Mockito.mock(AFUNIXSocket.class); Mockito.when(client.getInputStream()).thenAnswer(invocation -> { @@ -513,12 +581,8 @@ public void testStopClosesActiveClientSocket() throws Exception { writer.flush(); Assert.assertNotNull(reader.readLine()); - long startNanos = System.nanoTime(); service.innerStop(); started = false; - long elapsedMillis = (System.nanoTime() - startNanos) / 1_000_000; - Assert.assertTrue("IPC service shutdown took " + elapsedMillis + " ms", - elapsedMillis < 5_000); } } } finally { @@ -674,10 +738,10 @@ private String readRequest(IpcService service, InputStream input) throws Excepti new Class[] {InputStream.class}, input); } - private int getStaticIntField(String fieldName) throws Exception { + private int getIntField(IpcService service, String fieldName) throws Exception { Field field = IpcService.class.getDeclaredField(fieldName); field.setAccessible(true); - return field.getInt(null); + return field.getInt(service); } private ObjectMapper getStaticObjectMapper(String fieldName) throws Exception { @@ -686,6 +750,19 @@ private ObjectMapper getStaticObjectMapper(String fieldName) throws Exception { return (ObjectMapper) field.get(null); } + private boolean isRunning(IpcService service) throws Exception { + Field field = IpcService.class.getDeclaredField("isRunning"); + field.setAccessible(true); + return field.getBoolean(service); + } + + private ExecutorService getExecutorService(IpcService service, String fieldName) + throws Exception { + Field field = IpcService.class.getDeclaredField(fieldName); + field.setAccessible(true); + return (ExecutorService) field.get(service); + } + @SuppressWarnings("unchecked") private Set getActiveClientSockets(IpcService service) throws Exception { Field field = IpcService.class.getDeclaredField("activeClientSockets"); From ac1516b4586e33cb118630ecaa712c8eef7d8ecb Mon Sep 17 00:00:00 2001 From: 317787106 <317787106@qq.com> Date: Thu, 13 Aug 2026 13:02:33 +0800 Subject: [PATCH 15/15] optimize getSocketPathLength; IpcClient don't use Slf4j --- .../java/org/tron/core/config/args/Args.java | 5 ++++- .../core/services/admin/ipc/IpcClient.java | 12 +++++++---- .../core/services/admin/ipc/IpcService.java | 7 ++++++- .../org/tron/core/config/args/ArgsTest.java | 20 +++--------------- .../services/admin/ipc/IpcClientTest.java | 13 +++++++++++- .../services/admin/ipc/IpcServiceTest.java | 21 +++++++------------ 6 files changed, 40 insertions(+), 38 deletions(-) diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index d14542cc092..d8cddff2109 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -206,10 +206,13 @@ private static boolean tryApplyAttachParams(CLIParameter cmd, } return false; } + if (isParameterAssigned(assignedParameters, "shellConfFileName")) { + throwAttachParameterError("Error: --attach cannot be combined with: --config"); + } if (StringUtils.isBlank(cmd.ipcSocketFile)) { throwAttachParameterError("Error: --attach requires a non-empty "); } - //ignore seenodes from cmd + // Node-only CLI options are irrelevant to the standalone IPC client and are ignored. ipcSocketFile = cmd.ipcSocketFile; ipcExecCommand = cmd.ipcExecCommand; if (StringUtils.isNotEmpty(cmd.logbackPath)) { diff --git a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcClient.java b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcClient.java index f65e8591271..66c535c9197 100644 --- a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcClient.java +++ b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcClient.java @@ -26,7 +26,6 @@ import java.util.Locale; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; -import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.jline.reader.Completer; import org.jline.reader.EndOfFileException; @@ -46,7 +45,13 @@ import org.tron.core.services.admin.AdminJsonRpc; import org.tron.program.Version; -@Slf4j(topic = "API") +/** + * Standalone IPC console client. + * + *

Keep this class independent of SLF4J, including Lombok's {@code @Slf4j}. Client diagnostics + * must be written to the console through {@link System#out}, {@link System#err}, or JLine so the + * client does not initialize or write to the node's Logback appenders. + */ public class IpcClient { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); @@ -78,7 +83,6 @@ public static int start(String socketFilePath, String execCommand) { return ipcClient.run(execCommand); } catch (IOException e) { System.err.println("Failed to communicate with IPC server."); - logger.debug("IPC client communication failed: {}", e.getClass().getSimpleName()); return EXIT_FAILURE; } } @@ -265,7 +269,7 @@ private void outputResponse(final Socket socket, LineReader reader, AtomicBoolea } } } catch (IOException e) { - logger.debug("IPC response stream closed: {}", e.getMessage()); + // The socket closing is reported to the console by notifyDisconnected below. } finally { if (notifyDisconnected(connected, reader)) { inputThread.interrupt(); diff --git a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java index e84ac9f1fbe..310cc422939 100644 --- a/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java +++ b/framework/src/main/java/org/tron/core/services/admin/ipc/IpcService.java @@ -15,6 +15,7 @@ import java.io.OutputStreamWriter; import java.lang.management.ManagementFactory; import java.net.SocketTimeoutException; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.DirectoryStream; import java.nio.file.Files; @@ -412,7 +413,11 @@ private Path resolveSocketFilePath(CommonParameter parameter, String pid) { } private int getSocketPathLength(Path socketFile) { - return socketFile.toString().getBytes(AFUNIXSocketAddress.addressCharset()).length; + return getSocketPathLength(socketFile, AFUNIXSocketAddress.addressCharset()); + } + + static int getSocketPathLength(Path socketFile, Charset charset) { + return socketFile.toString().getBytes(charset).length; } private void validateSocketRootDirectory(Path socketRootDirectory) throws IOException { diff --git a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java index 144e732a352..0f9b076922e 100644 --- a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java +++ b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java @@ -79,7 +79,7 @@ public void testAttachRejectsNodeConfigOption() { assertAttachParameterError(new String[] { "--attach", "/tmp/java-tron.sock", "--config", "config.conf" - }, "--attach cannot be combined with: --config"); + }, "Error: --attach cannot be combined with: --config"); } finally { Args.clearParam(); } @@ -90,21 +90,7 @@ public void testAttachRejectsEmptySocketPath() { Args.clearParam(); try { assertAttachParameterError(new String[] {"--attach", ""}, - "--attach requires a non-empty "); - } finally { - Args.clearParam(); - } - } - - @Test - public void testAttachRejectsOtherNodeOptions() { - Args.clearParam(); - try { - assertAttachParameterError(new String[] { - "--attach", "/tmp/java-tron.sock", - "--keystore-factory", - "seed.example.org:18888" - }, "--attach cannot be combined with: --keystore-factory, seedNode"); + "Error: --attach requires a non-empty "); } finally { Args.clearParam(); } @@ -115,7 +101,7 @@ public void testExecRequiresAttach() { Args.clearParam(); try { assertAttachParameterError(new String[] {"--exec", "admin_example"}, - "--exec requires --attach "); + "Error: --exec requires --attach "); } finally { Args.clearParam(); } diff --git a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcClientTest.java b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcClientTest.java index 0cbffd63bed..1c9e4e0a9e3 100644 --- a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcClientTest.java +++ b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcClientTest.java @@ -32,6 +32,16 @@ public class IpcClientTest { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + @Test + public void testClientDoesNotDeclareLogger() { + try { + IpcClient.class.getDeclaredField("logger"); + Assert.fail("IPC client must not initialize the node logging system"); + } catch (NoSuchFieldException expected) { + // No logger field means loading IpcClient cannot initialize SLF4J through this class. + } + } + @Test public void testBuildHelpLinesIncludesSortedCommandParameters() { IpcClient client = new IpcClient("unused"); @@ -203,7 +213,8 @@ public void testMissingSocketFilePrintsConsoleError() throws Exception { Files.deleteIfExists(temporaryDirectory); } - Assert.assertEquals("IPC socket file does not exist: missing.sock" + System.lineSeparator(), + Assert.assertEquals("Error: IPC socket file does not exist: missing.sock" + + System.lineSeparator(), errorOutput.toString("UTF-8")); } diff --git a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java index 653623d183d..385512bac3d 100644 --- a/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java +++ b/framework/src/test/java/org/tron/core/services/admin/ipc/IpcServiceTest.java @@ -106,21 +106,14 @@ public void testResolveSocketFilePathRejectsLongOutputPath() throws Exception { } @Test - public void testResolveSocketFilePathRejectsEncodedPathOverLimit() throws Exception { - IpcService service = newIpcService(); - CommonParameter parameter = new CommonParameter(); - StringBuilder outputDirectory = new StringBuilder("/tmp/"); - for (int i = 0; i < 40; i++) { - outputDirectory.append("目"); - } - parameter.outputDirectory = outputDirectory.toString(); + public void testSocketPathLengthCountsUtf8Bytes() { + Path socketPath = Paths.get("/tmp/目录.sock"); - try { - resolveSocketFilePath(service, parameter, "1234"); - Assert.fail("Expected the encoded IPC socket path length to be checked"); - } catch (TronError e) { - Assert.assertTrue(e.getMessage().contains("exceeding the portable limit of 100 bytes")); - } + int encodedLength = IpcService.getSocketPathLength(socketPath, StandardCharsets.UTF_8); + + Assert.assertEquals(socketPath.toString().getBytes(StandardCharsets.UTF_8).length, + encodedLength); + Assert.assertTrue(encodedLength > socketPath.toString().length()); } @Test