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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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 From 906fb18e7ee0583f18c02023c20337cfe2d3b921 Mon Sep 17 00:00:00 2001 From: 317787106 <317787106@qq.com> Date: Fri, 21 Aug 2026 22:19:45 +0800 Subject: [PATCH 16/20] rename node.admin.rpc to node.admin.http --- .../common/parameter/CommonParameter.java | 9 +++++---- .../org/tron/core/config/args/NodeConfig.java | 4 ++-- common/src/main/resources/reference.conf | 2 +- .../tron/core/config/args/NodeConfigTest.java | 20 +++++++++---------- .../java/org/tron/core/config/args/Args.java | 12 +++++------ .../admin/http/AdminRpcHttpService.java | 8 ++++---- .../services/admin/http/AdminRpcServlet.java | 3 ++- .../org/tron/core/config/args/ArgsTest.java | 18 ++++++++--------- 8 files changed, 39 insertions(+), 37 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 ab98c1be5f5..f5d0d483875 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -491,16 +491,17 @@ public class CommonParameter { public int jsonRpcMaxLogFilterNum = 20000; @Getter @Setter - public boolean adminRpcEnable = false; + public boolean adminHttpEnable = false; @Getter @Setter - public String adminListenAddress = Constant.LOCAL_HOST; + public String adminHttpListenAddress = Constant.LOCAL_HOST; @Getter @Setter - public int adminListenPort = 8575; + public int adminHttpListenPort = 8575; @Getter @Setter - public List adminVirtualHosts = new ArrayList<>(Collections.singletonList("localhost")); + public List adminHttpVirtualHosts = + new ArrayList<>(Collections.singletonList("localhost")); @Getter @Setter public boolean ipcEnable = false; 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 79a6f4de6f5..4971af55f8e 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 @@ -260,7 +260,7 @@ public static class JsonRpcConfig { public static class AdminConfig { private AdminIpcConfig ipc = new AdminIpcConfig(); - private AdminRpcConfig rpc = new AdminRpcConfig(); + private AdminHttpConfig http = new AdminHttpConfig(); } @Getter @@ -273,7 +273,7 @@ public static class AdminIpcConfig { @Getter @Setter - public static class AdminRpcConfig { + public static class AdminHttpConfig { private boolean enable = false; private String listenAddress = Constant.LOCAL_HOST; diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index 2ee42f4a803..38d4e7fb5d5 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -455,7 +455,7 @@ node { } # Administrative JSON-RPC HTTP API. - rpc { + http { # 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. 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 6ca43981fde..93db46e25f4 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 @@ -34,11 +34,11 @@ public void testDefaults() { assertFalse(nc.isOpenFullTcpDisconnect()); 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()); + assertFalse(nc.getAdmin().getHttp().isEnable()); + assertEquals("127.0.0.1", nc.getAdmin().getHttp().getListenAddress()); + assertEquals(8575, nc.getAdmin().getHttp().getPort()); assertEquals(Collections.singletonList("localhost"), - nc.getAdmin().getRpc().getVirtualHosts()); + nc.getAdmin().getHttp().getVirtualHosts()); // reference.conf matches code default: discovery disabled when not configured assertFalse(nc.isDiscoveryEnable()); assertFalse(nc.isDiscoveryPersist()); @@ -89,19 +89,19 @@ public void testRpcSubBean() { } @Test - public void testAdminRpcAndIpcBinding() { + public void testAdminHttpAndIpcBinding() { Config config = withRef( "node.admin { ipc { enable = true, socketDirectory = \"/tmp/tron-ipc\" }," - + " rpc { enable = true, listenAddress = \"127.0.0.2\", port = 18575," + + " http { 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()); + assertTrue(nc.getAdmin().getHttp().isEnable()); + assertEquals("127.0.0.2", nc.getAdmin().getHttp().getListenAddress()); + assertEquals(18575, nc.getAdmin().getHttp().getPort()); assertEquals(Arrays.asList("admin.example.com", "localhost"), - nc.getAdmin().getRpc().getVirtualHosts()); + nc.getAdmin().getHttp().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 d8cddff2109..9e36d68491e 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 @@ -612,13 +612,13 @@ private static void applyNodeConfig(NodeConfig nc) { PARAMETER.jsonRpcMaxLogFilterNum = jsonrpc.getMaxLogFilterNum(); PARAMETER.jsonRpcMaxMessageSize = jsonrpc.getMaxMessageSize(); - // ---- Admin RPC / IPC ---- + // ---- Admin HTTP / IPC ---- 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.adminVirtualHosts = new ArrayList<>(adminRpc.getVirtualHosts()); + NodeConfig.AdminHttpConfig adminHttp = nc.getAdmin().getHttp(); + PARAMETER.adminHttpEnable = adminHttp.isEnable(); + PARAMETER.adminHttpListenAddress = adminHttp.getListenAddress(); + PARAMETER.adminHttpListenPort = adminHttp.getPort(); + PARAMETER.adminHttpVirtualHosts = new ArrayList<>(adminHttp.getVirtualHosts()); PARAMETER.ipcEnable = adminIpc.isEnable(); PARAMETER.ipcSocketDirectory = adminIpc.getSocketDirectory(); 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 d4c6cc5f33e..b342b4423bb 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 @@ -23,16 +23,16 @@ public class AdminRpcHttpService extends HttpService { private AdminRpcServlet adminRpcServlet; public AdminRpcHttpService() { - enable = isFullNode() && Args.getInstance().isAdminRpcEnable(); - listenAddress = Args.getInstance().getAdminListenAddress(); - port = Args.getInstance().getAdminListenPort(); + enable = isFullNode() && Args.getInstance().isAdminHttpEnable(); + listenAddress = Args.getInstance().getAdminHttpListenAddress(); + port = Args.getInstance().getAdminHttpListenPort(); contextPath = "/"; } @Override public void innerStart() throws Exception { if (enable && !isLoopbackListenAddress(listenAddress)) { - logger.warn("Admin RPC is enabled on {} and may be accessible remotely. " + logger.warn("Admin HTTP is enabled on {} and may be accessible remotely. " + "Restrict access to trusted networks.", listenAddress); } super.innerStart(); 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 3d2b1ab10f0..e9459181e10 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 @@ -70,7 +70,8 @@ public Integer getJsonRpcCode(int httpStatusCode) { if (CommonParameter.getInstance().isMetricsPrometheusEnable()) { rpcServer.setInterceptorList(Collections.singletonList(interceptor)); } - virtualHosts = normalizeVirtualHosts(CommonParameter.getInstance().getAdminVirtualHosts()); + virtualHosts = normalizeVirtualHosts( + CommonParameter.getInstance().getAdminHttpVirtualHosts()); } @Override 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 0f9b076922e..46cdb55626f 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 @@ -358,15 +358,15 @@ public void testInitService() { } @Test - public void testAdminRpcAndIpcConfigBinding() { + public void testAdminHttpAndIpcConfigBinding() { 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", + override.put("node.admin.http.enable", "true"); + override.put("node.admin.http.listenAddress", "127.0.0.2"); + override.put("node.admin.http.port", "18575"); + override.put("node.admin.http.virtualHosts", Arrays.asList("admin.example.com", "localhost")); Config config = ConfigFactory.parseMap(override) .withFallback(ConfigFactory.defaultReference()); @@ -375,11 +375,11 @@ public void testAdminRpcAndIpcConfigBinding() { 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()); + Assert.assertTrue(Args.getInstance().isAdminHttpEnable()); + Assert.assertEquals("127.0.0.2", Args.getInstance().getAdminHttpListenAddress()); + Assert.assertEquals(18575, Args.getInstance().getAdminHttpListenPort()); Assert.assertEquals(Arrays.asList("admin.example.com", "localhost"), - Args.getInstance().getAdminVirtualHosts()); + Args.getInstance().getAdminHttpVirtualHosts()); } finally { Args.clearParam(); } From c56940266c287b7b0381315be59739f27f192a30 Mon Sep 17 00:00:00 2001 From: 317787106 <317787106@qq.com> Date: Fri, 21 Aug 2026 22:37:05 +0800 Subject: [PATCH 17/20] update some comment --- .../org/tron/core/services/admin/ipc/IpcService.java | 9 +++++++++ 1 file changed, 9 insertions(+) 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 310cc422939..d6153b43421 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 @@ -51,6 +51,11 @@ import org.tron.core.services.jsonrpc.JsonRpcErrorResolver; import org.tron.core.services.jsonrpc.JsonRpcMapper; +/** + * Provides the local Admin JSON-RPC endpoint over a Unix domain socket. It accepts and dispatches + * client requests, manages concurrent client connections, and owns the socket setup, permissions, + * and cleanup lifecycle. + */ @Component @Slf4j(topic = "API") public class IpcService extends AbstractService { @@ -342,6 +347,10 @@ private Exception cleanupFailedStart(Exception failure) { return runCleanup(failure, this::deleteSocketDirectory); } + /** + * Runs one cleanup action while retaining the first failure and suppressing later failures, so + * the remaining cleanup actions can still be attempted. + */ private Exception runCleanup(Exception failure, CleanupAction action) { try { action.run(); From 8bdfb71def08211dfb025d6289e4f5d4cb955f9a Mon Sep 17 00:00:00 2001 From: 317787106 <317787106@qq.com> Date: Fri, 21 Aug 2026 23:13:29 +0800 Subject: [PATCH 18/20] optimize shutdownExecutors --- .../core/services/admin/ipc/IpcService.java | 23 +++++++++--- .../services/admin/ipc/IpcServiceTest.java | 37 +++++++++++++++++++ 2 files changed, 54 insertions(+), 6 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 d6153b43421..7e41a1c5104 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 @@ -64,6 +64,7 @@ public class IpcService extends AbstractService { 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_IDLE_TIMEOUT_MILLIS = 10 * 60 * 1000; + private static final int EXECUTOR_SHUTDOWN_TIMEOUT_SECONDS = 1; private static final String IPC_DIRECTORY_NAME = ".ipc"; // macOS/Linux sun_path buffers are 104/108 bytes. Reserve one byte for the terminating null @@ -317,14 +318,24 @@ private void shutdownActiveClients() { } 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. + // Closing a junixsocket from another thread does not always wake a native read promptly. The + // workers are daemon threads, so interrupt them and use a short bounded wait instead of the + // shared executor shutdown helper's 60-second wait. acceptorExecutor.shutdownNow(); clientExecutor.shutdownNow(); - ExecutorServiceManager.shutdownAndAwaitTermination(acceptorExecutor, ACCEPTOR_EXECUTOR_NAME); - ExecutorServiceManager.shutdownAndAwaitTermination(clientExecutor, CLIENT_EXECUTOR_NAME); + awaitExecutorTermination(acceptorExecutor, ACCEPTOR_EXECUTOR_NAME); + awaitExecutorTermination(clientExecutor, CLIENT_EXECUTOR_NAME); + } + + private void awaitExecutorTermination(ExecutorService executor, String name) { + try { + if (!executor.awaitTermination(EXECUTOR_SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + logger.warn("Pool {} did not terminate within {} second", name, + EXECUTOR_SHUTDOWN_TIMEOUT_SECONDS); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } } private void deleteSocketFile() 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 385512bac3d..49b8eed0893 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 @@ -584,6 +584,43 @@ public void testStopClosesActiveClientSocket() throws Exception { } } + @Test(timeout = 5_000) + public void testStopDoesNotWaitForUnresponsiveClientWorker() throws Exception { + IpcService service = newIpcService(); + ExecutorService clientExecutor = getExecutorService(service, "clientExecutor"); + CountDownLatch workerStarted = new CountDownLatch(1); + CountDownLatch releaseWorker = new CountDownLatch(1); + clientExecutor.submit(() -> { + workerStarted.countDown(); + boolean interrupted = false; + while (true) { + try { + releaseWorker.await(); + break; + } catch (InterruptedException e) { + interrupted = true; + } + } + if (interrupted) { + Thread.currentThread().interrupt(); + } + }); + Assert.assertTrue("Expected the client worker to start", + workerStarted.await(2, TimeUnit.SECONDS)); + + try { + service.innerStop(); + + Assert.assertTrue(clientExecutor.isShutdown()); + Assert.assertFalse("The unresponsive worker should still be running", + clientExecutor.isTerminated()); + } finally { + releaseWorker.countDown(); + Assert.assertTrue("Expected the released client worker to terminate", + clientExecutor.awaitTermination(2, TimeUnit.SECONDS)); + } + } + @Test(timeout = 5_000) public void testInnerStopContinuesCleanupAfterServerCloseFailure() throws Exception { IpcService service = newIpcService(); From a219222bbd019d9787a90ce8791000e1c2b76117 Mon Sep 17 00:00:00 2001 From: halibobo1205 <82020050+halibobo1205@users.noreply.github.com> Date: Mon, 14 Sep 2026 14:41:49 +0800 Subject: [PATCH 19/20] chore(deps): upgrade grpc, jackson, logback, commons and drop joda-time (#6950) * chore(deps): upgrade grpc-java from 1.83.0 to 1.83.1 1. bump grpcVersion to 1.83.1 to pick up the upstream fix for grpc/grpc-java#12930 (PR grpc/grpc-java#12942), which enforces connection.remote().maxActiveStreams(maxStreams) at handler startup 2. drop GrpcNettyMaxConcurrentStreamsLimiter, the local protocol-negotiator shim that applied the same limit while 1.83.0 left the remote endpoint unbounded until the client acknowledged SETTINGS * chore(deps): upgrade jackson from 2.18.6 to 2.18.10 bump jackson-databind from 2.18.6 to 2.18.10 to pick up cumulative fixes from the 2.18.x line * chore(deps): upgrade logback to 1.3.16 and slf4j to 2.0.17 1. bump logback-classic from 1.2.13 to 1.3.16 and slf4j-api, jcl-over-slf4j, jul-to-slf4j from 1.7.36 to 2.0.17; logback 1.3 requires the slf4j 2.0 provider model, and 1.3.16 is the last 1.3.x release and the ceiling for the x86_64 JDK 8 build, since 1.5.x requires JDK 11 2. rename DelayingShutdownHook to DefaultShutdownHook in the toolkit logback.xml; logback 1.3 removed the old class and only auto-maps the legacy name with a startup warning 3. drop the CONSOLE appender from the toolkit logback.xml; no logger ever referenced it, so it never emitted output on 1.2 either, and logback 1.3 now flags it with an unreferenced-appender warning 4. accept one known 1.3.x behavior change: SizeAndTimeBasedRollingPolicy now throttles its maxFileSize comparison to once per 60s (SimpleInvocationGate) instead of the adaptive ~100-800ms gate of 1.2.13, so under sustained heavy logging a file can overshoot the 500MB cap by up to 60s of writes before the %i rollover fires; time-based rollover and totalSizeCap/maxHistory cleanup are ungated and unaffected 5. note for operators running a custom --log-config file: well-formed 1.2-era configs using standard elements keep working unchanged (jmxConfigurator degrades to an ignored-property warning, the legacy shutdown hook name is auto-mapped), and malformed XML still fails fast via TronError(LOG_LOAD) exactly as on 1.2; however, a config that references an uninstantiable class (e.g. a custom appender missing from the classpath) now aborts the whole appender-ref phase instead of losing just that one appender, so the node starts with no log output while the ERROR statuses are printed to stdout by LogService * chore(deps): upgrade commons-lang3/collections4 and drop commons-math 1. bump commons-lang3 from 3.4 to 3.20.0; the runtime classpath already resolved 3.18.0 through libp2p 2.2.9's transitive requirement, so align the declaration with what actually ships and move past the CVE-2025-48924 range that the nominal 3.4 still sits in 2. bump commons-collections4 from 4.1 to 4.6.0 3. remove commons-math 2.2; no source file imports org.apache.commons.math and nothing else in the dependency graph requests it * chore(deps): remove joda-time and use JDK time APIs 1. drop the joda-time 2.3 dependency. 2. replace the six new DateTime(millis) log-formatting call sites in DynamicPropertiesStore, DposTask and DposService with a new Time.getIsoTimeString helper backed by java.time; its formatter (yyyy-MM-dd'T'HH:mm:ss.SSSXXX in the system zone) reproduces joda's DateTime.toString() output byte for byte where the JDK and joda 2.3 time-zone databases agree (UTC nodes are unaffected); zones whose rules changed after joda's 2013-era tzdb, e.g. Europe/Moscow, now render the corrected offset for the same instant. 3. replace DateTime.now() day arithmetic in four test classes with the java.time equivalent, ZonedDateTime.now().minusDays(n)/plusDays(n) .toInstant().toEpochMilli(), keeping joda's calendar semantics one-to-one, and map plain DateTime.now().getMillis() to System.currentTimeMillis() --- build.gradle | 16 +- .../core/store/DynamicPropertiesStore.java | 6 +- common/build.gradle | 4 +- .../main/java/org/tron/common/utils/Time.java | 12 + .../org/tron/consensus/dpos/DposService.java | 6 +- .../org/tron/consensus/dpos/DposTask.java | 4 +- .../GrpcNettyMaxConcurrentStreamsLimiter.java | 79 ----- .../tron/common/application/RpcService.java | 3 +- ...cNettyMaxConcurrentStreamsLimiterTest.java | 108 ------ .../NettyHttp2HeaderSecurityTest.java | 53 +++ .../common/utils/RandomGeneratorTest.java | 3 +- .../org/tron/core/BandwidthProcessorTest.java | 18 +- .../test/java/org/tron/core/WalletTest.java | 30 +- .../ParticipateAssetIssueActuatorTest.java | 58 ++-- .../TransactionsMsgHandlerTest.java | 4 +- gradle/verification-metadata.xml | 323 ++++++++++-------- plugins/src/main/resources/logback.xml | 11 +- 17 files changed, 324 insertions(+), 414 deletions(-) delete mode 100644 framework/src/main/java/org/tron/common/application/GrpcNettyMaxConcurrentStreamsLimiter.java delete mode 100644 framework/src/test/java/org/tron/common/application/GrpcNettyMaxConcurrentStreamsLimiterTest.java create mode 100644 framework/src/test/java/org/tron/common/application/NettyHttp2HeaderSecurityTest.java diff --git a/build.gradle b/build.gradle index 65e72c0fb73..04dee79fbae 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ plugins { } ext { - grpcVersion = "1.83.0" + grpcVersion = "1.83.1" } allprojects { @@ -91,16 +91,14 @@ subprojects { } dependencies { - implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.36' - implementation group: 'org.slf4j', name: 'jcl-over-slf4j', version: '1.7.36' - implementation group: 'org.slf4j', name: 'jul-to-slf4j', version: '1.7.36' - implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.13' + implementation group: 'org.slf4j', name: 'slf4j-api', version: '2.0.17' + implementation group: 'org.slf4j', name: 'jcl-over-slf4j', version: '2.0.17' + implementation group: 'org.slf4j', name: 'jul-to-slf4j', version: '2.0.17' + implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.3.16' implementation "com.google.code.findbugs:jsr305:3.0.0" implementation group: 'org.springframework', name: 'spring-context', version: "${springVersion}" - implementation "org.apache.commons:commons-lang3:3.4" - implementation group: 'org.apache.commons', name: 'commons-math', version: '2.2' - implementation "org.apache.commons:commons-collections4:4.1" - implementation group: 'joda-time', name: 'joda-time', version: '2.3' + implementation "org.apache.commons:commons-lang3:3.20.0" + implementation "org.apache.commons:commons-collections4:4.6.0" implementation group: 'org.bouncycastle', name: 'bcprov-jdk18on', version: '1.84' compileOnly 'org.projectlombok:lombok:1.18.34' diff --git a/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java b/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java index 0f74f20d379..33bbaa4a362 100644 --- a/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java +++ b/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java @@ -12,13 +12,13 @@ import java.util.stream.IntStream; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.joda.time.DateTime; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.tron.common.parameter.CommonParameter; import org.tron.common.utils.ByteArray; import org.tron.common.utils.Sha256Hash; +import org.tron.common.utils.Time; import org.tron.core.capsule.BytesCapsule; import org.tron.core.config.Parameter.ChainConstant; import org.tron.core.db.TronStoreWithRevoking; @@ -2261,8 +2261,8 @@ public void updateNextMaintenanceTime(long blockTime) { logger.info( "Do update nextMaintenanceTime, currentMaintenanceTime: {}, blockTime: {}, " + "nextMaintenanceTime: {}.", - new DateTime(currentMaintenanceTime), new DateTime(blockTime), - new DateTime(nextMaintenanceTime) + Time.getIsoTimeString(currentMaintenanceTime), Time.getIsoTimeString(blockTime), + Time.getIsoTimeString(nextMaintenanceTime) ); } diff --git a/common/build.gradle b/common/build.gradle index 14d3eb4e637..4b36d067b70 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -8,7 +8,9 @@ sourceCompatibility = 1.8 dependencies { - api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.18.6' // https://github.com/FasterXML/jackson-databind/issues/3627 + // avoid x.y.z.w micro-patches, they may ship broken Gradle module metadata: + // https://github.com/FasterXML/jackson-databind/issues/3627 + api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.18.10' api "com.cedarsoftware:java-util:3.2.0" api group: 'org.apache.httpcomponents', name: 'httpasyncclient', version: '4.1.1' api group: 'commons-codec', name: 'commons-codec', version: '1.11' diff --git a/common/src/main/java/org/tron/common/utils/Time.java b/common/src/main/java/org/tron/common/utils/Time.java index fdbfcb5f283..15e9d3d4b55 100644 --- a/common/src/main/java/org/tron/common/utils/Time.java +++ b/common/src/main/java/org/tron/common/utils/Time.java @@ -1,9 +1,17 @@ package org.tron.common.utils; import java.sql.Timestamp; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; public class Time { + // Matches joda-time's DateTime.toString() output, byte for byte: fixed + // 3-digit millis, offset as +08:00, and Z when the system zone is UTC. + private static final DateTimeFormatter ISO_MILLIS_FORMAT = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"); + public static long getCurrentMillis() { return System.currentTimeMillis(); } @@ -11,4 +19,8 @@ public static long getCurrentMillis() { public static String getTimeString(long time) { return new Timestamp(time).toString(); } + + public static String getIsoTimeString(long time) { + return Instant.ofEpochMilli(time).atZone(ZoneId.systemDefault()).format(ISO_MILLIS_FORMAT); + } } diff --git a/consensus/src/main/java/org/tron/consensus/dpos/DposService.java b/consensus/src/main/java/org/tron/consensus/dpos/DposService.java index 397c9d0835c..0a40ec8e076 100644 --- a/consensus/src/main/java/org/tron/consensus/dpos/DposService.java +++ b/consensus/src/main/java/org/tron/consensus/dpos/DposService.java @@ -14,12 +14,12 @@ import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import org.joda.time.DateTime; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.tron.common.args.GenesisBlock; import org.tron.common.parameter.CommonParameter; import org.tron.common.utils.ByteArray; +import org.tron.common.utils.Time; import org.tron.consensus.ConsensusDelegate; import org.tron.consensus.base.BlockHandle; import org.tron.consensus.base.ConsensusInterface; @@ -134,14 +134,14 @@ public boolean validBlock(BlockCapsule blockCapsule) { if (slot == 0 && consensusDelegate.getDynamicPropertiesStore().allowConsensusLogicOptimization()) { logger.warn("ValidBlock failed: slot error, witness: {}, timeStamp: {}", - ByteArray.toHexString(witnessAddress.toByteArray()), new DateTime(timeStamp)); + ByteArray.toHexString(witnessAddress.toByteArray()), Time.getIsoTimeString(timeStamp)); return false; } final ByteString scheduledWitness = dposSlot.getScheduledWitness(slot); if (!scheduledWitness.equals(witnessAddress)) { logger.warn("ValidBlock failed: sWitness: {}, bWitness: {}, bTimeStamp: {}, slot: {}", ByteArray.toHexString(scheduledWitness.toByteArray()), - ByteArray.toHexString(witnessAddress.toByteArray()), new DateTime(timeStamp), slot); + ByteArray.toHexString(witnessAddress.toByteArray()), Time.getIsoTimeString(timeStamp), slot); return false; } diff --git a/consensus/src/main/java/org/tron/consensus/dpos/DposTask.java b/consensus/src/main/java/org/tron/consensus/dpos/DposTask.java index 9e42552c80f..38f5614e571 100644 --- a/consensus/src/main/java/org/tron/consensus/dpos/DposTask.java +++ b/consensus/src/main/java/org/tron/consensus/dpos/DposTask.java @@ -6,7 +6,6 @@ import java.util.concurrent.ExecutorService; import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import org.joda.time.DateTime; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.util.ObjectUtils; @@ -15,6 +14,7 @@ import org.tron.common.parameter.CommonParameter; import org.tron.common.utils.ByteArray; import org.tron.common.utils.Sha256Hash; +import org.tron.common.utils.Time; import org.tron.consensus.ConsensusDelegate; import org.tron.consensus.base.Param.Miner; import org.tron.consensus.base.State; @@ -123,7 +123,7 @@ private State produceBlock() { BlockHeader.raw raw = blockCapsule.getInstance().getBlockHeader().getRawData(); logger.info("Produce block successfully, num: {}, time: {}, witness: {}, ID:{}, parentID:{}", raw.getNumber(), - new DateTime(raw.getTimestamp()), + Time.getIsoTimeString(raw.getTimestamp()), ByteArray.toHexString(raw.getWitnessAddress().toByteArray()), new Sha256Hash(raw.getNumber(), Sha256Hash.of(CommonParameter .getInstance().isECKeyCryptoEngine(), raw.toByteArray())), diff --git a/framework/src/main/java/org/tron/common/application/GrpcNettyMaxConcurrentStreamsLimiter.java b/framework/src/main/java/org/tron/common/application/GrpcNettyMaxConcurrentStreamsLimiter.java deleted file mode 100644 index cdd71ffee3c..00000000000 --- a/framework/src/main/java/org/tron/common/application/GrpcNettyMaxConcurrentStreamsLimiter.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * java-tron is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * java-tron is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with java-tron. If not, see . - */ - -package org.tron.common.application; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; - -import io.grpc.netty.GrpcHttp2ConnectionHandler; -import io.grpc.netty.InternalProtocolNegotiator; -import io.grpc.netty.InternalProtocolNegotiators; -import io.grpc.netty.NettyServerBuilder; -import io.netty.channel.ChannelHandler; -import io.netty.util.AsciiString; - -/** Enforces the advertised HTTP/2 concurrent stream limit for grpc-netty servers. */ -final class GrpcNettyMaxConcurrentStreamsLimiter { - - private GrpcNettyMaxConcurrentStreamsLimiter() { - } - - static NettyServerBuilder configurePlaintext( - NettyServerBuilder builder, int maxConcurrentStreams) { - checkNotNull(builder, "builder"); - checkArgument(maxConcurrentStreams > 0, "maxConcurrentStreams must be positive"); - builder.maxConcurrentCallsPerConnection(maxConcurrentStreams); - // TODO: Remove this shim after https://github.com/grpc/grpc-java/issues/12930 is fixed. - return builder.protocolNegotiator(newPlaintextNegotiator(maxConcurrentStreams)); - } - - static InternalProtocolNegotiator.ProtocolNegotiator newPlaintextNegotiator( - int maxConcurrentStreams) { - checkArgument(maxConcurrentStreams > 0, "maxConcurrentStreams must be positive"); - return new EnforcingProtocolNegotiator( - InternalProtocolNegotiators.serverPlaintext(), maxConcurrentStreams); - } - - private static final class EnforcingProtocolNegotiator - implements InternalProtocolNegotiator.ProtocolNegotiator { - - private final InternalProtocolNegotiator.ProtocolNegotiator delegate; - private final int maxConcurrentStreams; - - private EnforcingProtocolNegotiator( - InternalProtocolNegotiator.ProtocolNegotiator delegate, int maxConcurrentStreams) { - this.delegate = checkNotNull(delegate, "delegate"); - this.maxConcurrentStreams = maxConcurrentStreams; - } - - @Override - public AsciiString scheme() { - return delegate.scheme(); - } - - @Override - public ChannelHandler newHandler(GrpcHttp2ConnectionHandler grpcHandler) { - // grpc-java builds the connection directly, bypassing Netty's builder-side enforcement. - grpcHandler.connection().remote().maxActiveStreams(maxConcurrentStreams); - return delegate.newHandler(grpcHandler); - } - - @Override - public void close() { - delegate.close(); - } - } -} diff --git a/framework/src/main/java/org/tron/common/application/RpcService.java b/framework/src/main/java/org/tron/common/application/RpcService.java index 27fcc479f4e..c398b71ae41 100644 --- a/framework/src/main/java/org/tron/common/application/RpcService.java +++ b/framework/src/main/java/org/tron/common/application/RpcService.java @@ -100,9 +100,8 @@ protected NettyServerBuilder initServerBuilder() { serverBuilder = serverBuilder.executor(this.executorService); } // Set configs from config.conf or default value - serverBuilder = GrpcNettyMaxConcurrentStreamsLimiter.configurePlaintext( - serverBuilder, parameter.getMaxConcurrentCallsPerConnection()); serverBuilder + .maxConcurrentCallsPerConnection(parameter.getMaxConcurrentCallsPerConnection()) .flowControlWindow(parameter.getFlowControlWindow()) .maxConnectionIdle(parameter.getMaxConnectionIdleInMillis(), TimeUnit.MILLISECONDS) .maxConnectionAge(parameter.getMaxConnectionAgeInMillis(), TimeUnit.MILLISECONDS) diff --git a/framework/src/test/java/org/tron/common/application/GrpcNettyMaxConcurrentStreamsLimiterTest.java b/framework/src/test/java/org/tron/common/application/GrpcNettyMaxConcurrentStreamsLimiterTest.java deleted file mode 100644 index fc578ca7947..00000000000 --- a/framework/src/test/java/org/tron/common/application/GrpcNettyMaxConcurrentStreamsLimiterTest.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * java-tron is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * java-tron is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with java-tron. If not, see . - */ - -package org.tron.common.application; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThrows; - -import io.grpc.ChannelLogger; -import io.grpc.ChannelLogger.ChannelLogLevel; -import io.grpc.netty.GrpcHttp2ConnectionHandler; -import io.grpc.netty.InternalProtocolNegotiator; -import io.netty.channel.ChannelHandler; -import io.netty.handler.codec.http2.DefaultHttp2Connection; -import io.netty.handler.codec.http2.DefaultHttp2ConnectionDecoder; -import io.netty.handler.codec.http2.DefaultHttp2ConnectionEncoder; -import io.netty.handler.codec.http2.DefaultHttp2FrameReader; -import io.netty.handler.codec.http2.DefaultHttp2FrameWriter; -import io.netty.handler.codec.http2.Http2Connection; -import io.netty.handler.codec.http2.Http2ConnectionDecoder; -import io.netty.handler.codec.http2.Http2ConnectionEncoder; -import io.netty.handler.codec.http2.Http2Error; -import io.netty.handler.codec.http2.Http2Exception; -import io.netty.handler.codec.http2.Http2FrameWriter; -import io.netty.handler.codec.http2.Http2Settings; -import org.junit.Test; - -public class GrpcNettyMaxConcurrentStreamsLimiterTest { - - private static final ChannelLogger NOOP_LOGGER = new ChannelLogger() { - @Override - public void log(ChannelLogLevel level, String message) { - } - - @Override - public void log(ChannelLogLevel level, String messageFormat, Object... args) { - } - }; - - @Test - public void shouldEnforceMaxStreamsBeforeSettingsAck() throws Exception { - Http2Connection connection = new DefaultHttp2Connection(true); - GrpcHttp2ConnectionHandler grpcHandler = newGrpcHandler(connection); - InternalProtocolNegotiator.ProtocolNegotiator negotiator = - GrpcNettyMaxConcurrentStreamsLimiter.newPlaintextNegotiator(2); - - ChannelHandler negotiationHandler = negotiator.newHandler(grpcHandler); - - assertNotNull(negotiationHandler); - assertEquals(2, connection.remote().maxActiveStreams()); - connection.remote().createStream(1, true); - connection.remote().createStream(3, true); - Http2Exception exception = assertThrows( - Http2Exception.class, () -> connection.remote().createStream(5, true)); - assertEquals(Http2Error.REFUSED_STREAM, exception.error()); - negotiator.close(); - } - - @Test - public void shouldIgnoreClientMaxHeaderListSizeOnServer() throws Exception { - Http2Connection connection = new DefaultHttp2Connection(true); - Http2FrameWriter frameWriter = new DefaultHttp2FrameWriter(); - Http2ConnectionEncoder encoder = - new DefaultHttp2ConnectionEncoder(connection, frameWriter); - long originalMaxHeaderListSize = - encoder.configuration().headersConfiguration().maxHeaderListSize(); - - encoder.remoteSettings(new Http2Settings().maxHeaderListSize(1)); - - assertEquals(originalMaxHeaderListSize, - encoder.configuration().headersConfiguration().maxHeaderListSize()); - encoder.close(); - } - - @Test - public void shouldRejectNonPositiveStreamLimit() { - IllegalArgumentException zeroLimitException = assertThrows(IllegalArgumentException.class, - () -> GrpcNettyMaxConcurrentStreamsLimiter.newPlaintextNegotiator(0)); - assertEquals("maxConcurrentStreams must be positive", zeroLimitException.getMessage()); - IllegalArgumentException negativeLimitException = assertThrows(IllegalArgumentException.class, - () -> GrpcNettyMaxConcurrentStreamsLimiter.newPlaintextNegotiator(-1)); - assertEquals("maxConcurrentStreams must be positive", negativeLimitException.getMessage()); - } - - private static GrpcHttp2ConnectionHandler newGrpcHandler(Http2Connection connection) { - Http2FrameWriter frameWriter = new DefaultHttp2FrameWriter(); - Http2ConnectionEncoder encoder = - new DefaultHttp2ConnectionEncoder(connection, frameWriter); - Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder( - connection, encoder, new DefaultHttp2FrameReader()); - return new GrpcHttp2ConnectionHandler( - null, decoder, encoder, new Http2Settings(), NOOP_LOGGER) { - }; - } -} diff --git a/framework/src/test/java/org/tron/common/application/NettyHttp2HeaderSecurityTest.java b/framework/src/test/java/org/tron/common/application/NettyHttp2HeaderSecurityTest.java new file mode 100644 index 00000000000..6a4f4330f04 --- /dev/null +++ b/framework/src/test/java/org/tron/common/application/NettyHttp2HeaderSecurityTest.java @@ -0,0 +1,53 @@ +/* + * java-tron is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * java-tron is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with java-tron. If not, see . + */ + +package org.tron.common.application; + +import static org.junit.Assert.assertEquals; + +import io.netty.handler.codec.http2.DefaultHttp2Connection; +import io.netty.handler.codec.http2.DefaultHttp2ConnectionEncoder; +import io.netty.handler.codec.http2.DefaultHttp2FrameWriter; +import io.netty.handler.codec.http2.Http2Connection; +import io.netty.handler.codec.http2.Http2ConnectionEncoder; +import io.netty.handler.codec.http2.Http2FrameWriter; +import io.netty.handler.codec.http2.Http2Settings; +import org.junit.Test; + +/** Guards the netty HTTP/2 header-size behaviour the gRPC server relies on. */ +public class NettyHttp2HeaderSecurityTest { + + /** + * CVE-2026-50560: SETTINGS_MAX_HEADER_LIST_SIZE tells the server what the client is willing to + * receive, so it must not shrink the server encoder's own limit. Otherwise a hostile client can + * advertise a tiny value and make every response-header write throw, which is a Rapid-Reset-like + * denial of service. Netty enforced the client value before 4.1.135.Final / 4.2.15.Final. + */ + @Test + public void shouldIgnoreClientMaxHeaderListSizeOnServer() throws Exception { + Http2Connection connection = new DefaultHttp2Connection(true); + Http2FrameWriter frameWriter = new DefaultHttp2FrameWriter(); + Http2ConnectionEncoder encoder = + new DefaultHttp2ConnectionEncoder(connection, frameWriter); + long originalMaxHeaderListSize = + encoder.configuration().headersConfiguration().maxHeaderListSize(); + + encoder.remoteSettings(new Http2Settings().maxHeaderListSize(1)); + + assertEquals(originalMaxHeaderListSize, + encoder.configuration().headersConfiguration().maxHeaderListSize()); + encoder.close(); + } +} diff --git a/framework/src/test/java/org/tron/common/utils/RandomGeneratorTest.java b/framework/src/test/java/org/tron/common/utils/RandomGeneratorTest.java index 4de441d940d..34c7536ebd0 100644 --- a/framework/src/test/java/org/tron/common/utils/RandomGeneratorTest.java +++ b/framework/src/test/java/org/tron/common/utils/RandomGeneratorTest.java @@ -9,7 +9,6 @@ import java.util.List; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; -import org.joda.time.DateTime; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; @@ -42,7 +41,7 @@ public void shuffle() { final List witnessCapsuleListBefore = this.getWitnessList(); logger.info("updateWitnessSchedule,before: " + getWitnessStringList(witnessCapsuleListBefore)); final List witnessCapsuleListAfter = new RandomGenerator() - .shuffle(witnessCapsuleListBefore, DateTime.now().getMillis()); + .shuffle(witnessCapsuleListBefore, System.currentTimeMillis()); logger.info("updateWitnessSchedule,after: " + getWitnessStringList(witnessCapsuleListAfter)); } diff --git a/framework/src/test/java/org/tron/core/BandwidthProcessorTest.java b/framework/src/test/java/org/tron/core/BandwidthProcessorTest.java index cf652af3650..622d20ae7d2 100755 --- a/framework/src/test/java/org/tron/core/BandwidthProcessorTest.java +++ b/framework/src/test/java/org/tron/core/BandwidthProcessorTest.java @@ -5,8 +5,8 @@ import com.google.protobuf.Any; import com.google.protobuf.ByteString; import java.nio.charset.StandardCharsets; +import java.time.ZonedDateTime; import lombok.extern.slf4j.Slf4j; -import org.joda.time.DateTime; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -64,8 +64,8 @@ public class BandwidthProcessorTest extends BaseTest { TO_ADDRESS = Wallet.getAddressPreFixString() + "abd4b9367799eaa3197fecb144eb71de1e049abc"; ASSET_ADDRESS = Wallet.getAddressPreFixString() + "548794500882809695a8a687866e76d4271a3456"; ASSET_ADDRESS_V2 = Wallet.getAddressPreFixString() + "548794500882809695a8a687866e76d4271a7890"; - START_TIME = DateTime.now().minusDays(1).getMillis(); - END_TIME = DateTime.now().getMillis(); + START_TIME = ZonedDateTime.now().minusDays(1).toInstant().toEpochMilli(); + END_TIME = System.currentTimeMillis(); } /** @@ -616,7 +616,7 @@ public void sameTokenNameCloseConsumeSuccess() { AccountType.Normal, chainBaseManager.getDynamicPropertiesStore().getAssetIssueFee()); ownerCapsule.setBalance(10_000_000L); - long expireTime = DateTime.now().getMillis() + 6 * 86_400_000; + long expireTime = System.currentTimeMillis() + 6 * 86_400_000; ownerCapsule.setFrozenForBandwidth(2_000_000L, expireTime); chainBaseManager.getAccountStore().put(ownerCapsule.getAddress().toByteArray(), ownerCapsule); @@ -627,7 +627,7 @@ public void sameTokenNameCloseConsumeSuccess() { AccountType.Normal, chainBaseManager.getDynamicPropertiesStore().getAssetIssueFee()); toAddressCapsule.setBalance(10_000_000L); - long expireTime2 = DateTime.now().getMillis() + 6 * 86_400_000; + long expireTime2 = System.currentTimeMillis() + 6 * 86_400_000; toAddressCapsule.setFrozenForBandwidth(2_000_000L, expireTime2); chainBaseManager.getAccountStore().put(toAddressCapsule.getAddress().toByteArray(), toAddressCapsule); @@ -731,7 +731,7 @@ public void sameTokenNameOpenConsumeSuccess() { AccountType.Normal, chainBaseManager.getDynamicPropertiesStore().getAssetIssueFee()); ownerCapsule.setBalance(10_000_000L); - long expireTime = DateTime.now().getMillis() + 6 * 86_400_000; + long expireTime = System.currentTimeMillis() + 6 * 86_400_000; ownerCapsule.setFrozenForBandwidth(2_000_000L, expireTime); chainBaseManager.getAccountStore().put(ownerCapsule.getAddress().toByteArray(), ownerCapsule); @@ -742,7 +742,7 @@ public void sameTokenNameOpenConsumeSuccess() { AccountType.Normal, chainBaseManager.getDynamicPropertiesStore().getAssetIssueFee()); toAddressCapsule.setBalance(10_000_000L); - long expireTime2 = DateTime.now().getMillis() + 6 * 86_400_000; + long expireTime2 = System.currentTimeMillis() + 6 * 86_400_000; toAddressCapsule.setFrozenForBandwidth(2_000_000L, expireTime2); chainBaseManager.getAccountStore().put(toAddressCapsule.getAddress().toByteArray(), toAddressCapsule); @@ -816,7 +816,7 @@ public void sameTokenNameCloseTransferToAccountNotExist() { AccountType.Normal, chainBaseManager.getDynamicPropertiesStore().getAssetIssueFee()); ownerCapsule.setBalance(10_000_000L); - long expireTime = DateTime.now().getMillis() + 6 * 86_400_000; + long expireTime = System.currentTimeMillis() + 6 * 86_400_000; ownerCapsule.setFrozenForBandwidth(2_000_000L, expireTime); chainBaseManager.getAccountStore().put(ownerCapsule.getAddress().toByteArray(), ownerCapsule); @@ -827,7 +827,7 @@ public void sameTokenNameCloseTransferToAccountNotExist() { AccountType.Normal, chainBaseManager.getDynamicPropertiesStore().getAssetIssueFee()); toAddressCapsule.setBalance(10_000_000L); - long expireTime2 = DateTime.now().getMillis() + 6 * 86_400_000; + long expireTime2 = System.currentTimeMillis() + 6 * 86_400_000; toAddressCapsule.setFrozenForBandwidth(2_000_000L, expireTime2); chainBaseManager.getAccountStore().delete(toAddressCapsule.getAddress().toByteArray()); diff --git a/framework/src/test/java/org/tron/core/WalletTest.java b/framework/src/test/java/org/tron/core/WalletTest.java index 9dbab338b67..7215a287912 100644 --- a/framework/src/test/java/org/tron/core/WalletTest.java +++ b/framework/src/test/java/org/tron/core/WalletTest.java @@ -30,12 +30,12 @@ import com.google.protobuf.Any; import com.google.protobuf.ByteString; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Arrays; import javax.annotation.Resource; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import org.joda.time.DateTime; import org.junit.Assert; import org.junit.Before; import org.junit.Ignore; @@ -113,21 +113,29 @@ public class WalletTest extends BaseTest { public static final long BLOCK_NUM_THREE = 3; public static final long BLOCK_NUM_FOUR = 4; public static final long BLOCK_NUM_FIVE = 5; - public static final long BLOCK_TIMESTAMP_ONE = DateTime.now().minusDays(4).getMillis(); - public static final long BLOCK_TIMESTAMP_TWO = DateTime.now().minusDays(3).getMillis(); - public static final long BLOCK_TIMESTAMP_THREE = DateTime.now().minusDays(2).getMillis(); - public static final long BLOCK_TIMESTAMP_FOUR = DateTime.now().minusDays(1).getMillis(); - public static final long BLOCK_TIMESTAMP_FIVE = DateTime.now().getMillis(); + public static final long BLOCK_TIMESTAMP_ONE = + ZonedDateTime.now().minusDays(4).toInstant().toEpochMilli(); + public static final long BLOCK_TIMESTAMP_TWO = + ZonedDateTime.now().minusDays(3).toInstant().toEpochMilli(); + public static final long BLOCK_TIMESTAMP_THREE = + ZonedDateTime.now().minusDays(2).toInstant().toEpochMilli(); + public static final long BLOCK_TIMESTAMP_FOUR = + ZonedDateTime.now().minusDays(1).toInstant().toEpochMilli(); + public static final long BLOCK_TIMESTAMP_FIVE = System.currentTimeMillis(); public static final long BLOCK_WITNESS_ONE = 12; public static final long BLOCK_WITNESS_TWO = 13; public static final long BLOCK_WITNESS_THREE = 14; public static final long BLOCK_WITNESS_FOUR = 15; public static final long BLOCK_WITNESS_FIVE = 16; - public static final long TRANSACTION_TIMESTAMP_ONE = DateTime.now().minusDays(4).getMillis(); - public static final long TRANSACTION_TIMESTAMP_TWO = DateTime.now().minusDays(3).getMillis(); - public static final long TRANSACTION_TIMESTAMP_THREE = DateTime.now().minusDays(2).getMillis(); - public static final long TRANSACTION_TIMESTAMP_FOUR = DateTime.now().minusDays(1).getMillis(); - public static final long TRANSACTION_TIMESTAMP_FIVE = DateTime.now().getMillis(); + public static final long TRANSACTION_TIMESTAMP_ONE = + ZonedDateTime.now().minusDays(4).toInstant().toEpochMilli(); + public static final long TRANSACTION_TIMESTAMP_TWO = + ZonedDateTime.now().minusDays(3).toInstant().toEpochMilli(); + public static final long TRANSACTION_TIMESTAMP_THREE = + ZonedDateTime.now().minusDays(2).toInstant().toEpochMilli(); + public static final long TRANSACTION_TIMESTAMP_FOUR = + ZonedDateTime.now().minusDays(1).toInstant().toEpochMilli(); + public static final long TRANSACTION_TIMESTAMP_FIVE = System.currentTimeMillis(); @Resource private Wallet wallet; private static Block block1; diff --git a/framework/src/test/java/org/tron/core/actuator/ParticipateAssetIssueActuatorTest.java b/framework/src/test/java/org/tron/core/actuator/ParticipateAssetIssueActuatorTest.java index 5c168f51bee..4af63285b1e 100755 --- a/framework/src/test/java/org/tron/core/actuator/ParticipateAssetIssueActuatorTest.java +++ b/framework/src/test/java/org/tron/core/actuator/ParticipateAssetIssueActuatorTest.java @@ -2,7 +2,7 @@ import com.google.protobuf.Any; import com.google.protobuf.ByteString; -import org.joda.time.DateTime; +import java.time.ZonedDateTime; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -403,8 +403,8 @@ public void sameTokenNameOpenRightAssetIssue() { */ @Test public void sameTokenNameCloseAssetIssueTimeRight() { - DateTime now = DateTime.now(); - initAssetIssue(now.minusDays(1).getMillis(), now.getMillis()); + ZonedDateTime now = ZonedDateTime.now(); + initAssetIssue(now.minusDays(1).toInstant().toEpochMilli(), now.toInstant().toEpochMilli()); ParticipateAssetIssueActuator actuator = new ParticipateAssetIssueActuator(); actuator.setChainBaseManager(chainBaseManager).setAny(getContract(1000L)); @@ -436,8 +436,8 @@ public void sameTokenNameCloseAssetIssueTimeRight() { @Test public void sameTokenNameOpenAssetIssueTimeRight() { chainBaseManager.getDynamicPropertiesStore().saveAllowSameTokenName(1); - DateTime now = DateTime.now(); - initAssetIssue(now.minusDays(1).getMillis(), now.getMillis()); + ZonedDateTime now = ZonedDateTime.now(); + initAssetIssue(now.minusDays(1).toInstant().toEpochMilli(), now.toInstant().toEpochMilli()); ParticipateAssetIssueActuator actuator = new ParticipateAssetIssueActuator(); actuator.setChainBaseManager(chainBaseManager).setAny(getContract(1000L)); @@ -470,8 +470,8 @@ public void sameTokenNameOpenAssetIssueTimeRight() { */ @Test public void sameTokenNameCloseAssetIssueTimeLeft() { - DateTime now = DateTime.now(); - initAssetIssue(now.minusDays(1).getMillis(), now.getMillis()); + ZonedDateTime now = ZonedDateTime.now(); + initAssetIssue(now.minusDays(1).toInstant().toEpochMilli(), now.toInstant().toEpochMilli()); ParticipateAssetIssueActuator actuator = new ParticipateAssetIssueActuator(); actuator.setChainBaseManager(chainBaseManager).setAny(getContract(1000L)); @@ -504,8 +504,8 @@ public void sameTokenNameCloseAssetIssueTimeLeft() { @Test public void sameTokenNameOpenAssetIssueTimeLeft() { chainBaseManager.getDynamicPropertiesStore().saveAllowSameTokenName(1); - DateTime now = DateTime.now(); - initAssetIssue(now.minusDays(1).getMillis(), now.getMillis()); + ZonedDateTime now = ZonedDateTime.now(); + initAssetIssue(now.minusDays(1).toInstant().toEpochMilli(), now.toInstant().toEpochMilli()); ParticipateAssetIssueActuator actuator = new ParticipateAssetIssueActuator(); actuator.setChainBaseManager(chainBaseManager).setAny(getContract(1000L)); @@ -605,8 +605,9 @@ public void sameTokenNameOpenExchangeDevisibleTest() { */ @Test public void sameTokenNameCloseNegativeAmountTest() { - DateTime now = DateTime.now(); - initAssetIssue(now.minusDays(1).getMillis(), now.plusDays(1).getMillis()); + ZonedDateTime now = ZonedDateTime.now(); + initAssetIssue(now.minusDays(1).toInstant().toEpochMilli(), + now.plusDays(1).toInstant().toEpochMilli()); ParticipateAssetIssueActuator actuator = new ParticipateAssetIssueActuator(); actuator.setChainBaseManager(chainBaseManager).setAny(getContract(-999L)); @@ -639,8 +640,9 @@ public void sameTokenNameCloseNegativeAmountTest() { @Test public void sameTokenNameOpenNegativeAmountTest() { chainBaseManager.getDynamicPropertiesStore().saveAllowSameTokenName(1); - DateTime now = DateTime.now(); - initAssetIssue(now.minusDays(1).getMillis(), now.plusDays(1).getMillis()); + ZonedDateTime now = ZonedDateTime.now(); + initAssetIssue(now.minusDays(1).toInstant().toEpochMilli(), + now.plusDays(1).toInstant().toEpochMilli()); ParticipateAssetIssueActuator actuator = new ParticipateAssetIssueActuator(); actuator.setChainBaseManager(chainBaseManager).setAny(getContract(-999L)); @@ -675,8 +677,9 @@ public void sameTokenNameOpenNegativeAmountTest() { */ @Test public void sameTokenNameCloseZeroAmountTest() { - DateTime now = DateTime.now(); - initAssetIssue(now.minusDays(1).getMillis(), now.plusDays(1).getMillis()); + ZonedDateTime now = ZonedDateTime.now(); + initAssetIssue(now.minusDays(1).toInstant().toEpochMilli(), + now.plusDays(1).toInstant().toEpochMilli()); ParticipateAssetIssueActuator actuator = new ParticipateAssetIssueActuator(); actuator.setChainBaseManager(chainBaseManager).setAny(getContract(0)); @@ -709,8 +712,9 @@ public void sameTokenNameCloseZeroAmountTest() { @Test public void sameTokenNameOpenZeroAmountTest() { chainBaseManager.getDynamicPropertiesStore().saveAllowSameTokenName(1); - DateTime now = DateTime.now(); - initAssetIssue(now.minusDays(1).getMillis(), now.plusDays(1).getMillis()); + ZonedDateTime now = ZonedDateTime.now(); + initAssetIssue(now.minusDays(1).toInstant().toEpochMilli(), + now.plusDays(1).toInstant().toEpochMilli()); ParticipateAssetIssueActuator actuator = new ParticipateAssetIssueActuator(); actuator.setChainBaseManager(chainBaseManager).setAny(getContract(0)); @@ -746,8 +750,9 @@ public void sameTokenNameOpenZeroAmountTest() { */ @Test public void sameTokenNameCloseNoExitOwnerTest() { - DateTime now = DateTime.now(); - initAssetIssue(now.minusDays(1).getMillis(), now.plusDays(1).getMillis()); + ZonedDateTime now = ZonedDateTime.now(); + initAssetIssue(now.minusDays(1).toInstant().toEpochMilli(), + now.plusDays(1).toInstant().toEpochMilli()); ParticipateAssetIssueActuator actuator = new ParticipateAssetIssueActuator(); actuator.setChainBaseManager(chainBaseManager) .setAny(getContractWithOwner(101, NOT_EXIT_ADDRESS)); @@ -782,8 +787,9 @@ public void sameTokenNameCloseNoExitOwnerTest() { @Test public void sameTokenNameOpenNoExitOwnerTest() { chainBaseManager.getDynamicPropertiesStore().saveAllowSameTokenName(1); - DateTime now = DateTime.now(); - initAssetIssue(now.minusDays(1).getMillis(), now.plusDays(1).getMillis()); + ZonedDateTime now = ZonedDateTime.now(); + initAssetIssue(now.minusDays(1).toInstant().toEpochMilli(), + now.plusDays(1).toInstant().toEpochMilli()); ParticipateAssetIssueActuator actuator = new ParticipateAssetIssueActuator(); actuator.setChainBaseManager(chainBaseManager) .setAny(getContractWithOwner(101, NOT_EXIT_ADDRESS)); @@ -1310,8 +1316,9 @@ public void sameTokenNameOpenNotEnoughAssetTest() { */ @Test public void sameTokenNameCloseNoneExistAssetTest() { - DateTime now = DateTime.now(); - initAssetIssue(now.minusDays(1).getMillis(), now.plusDays(1).getMillis()); + ZonedDateTime now = ZonedDateTime.now(); + initAssetIssue(now.minusDays(1).toInstant().toEpochMilli(), + now.plusDays(1).toInstant().toEpochMilli()); ParticipateAssetIssueActuator actuator = new ParticipateAssetIssueActuator(); actuator.setChainBaseManager(chainBaseManager) .setAny(getContract(1, "TTTTTTTTTTTT")); @@ -1346,8 +1353,9 @@ public void sameTokenNameCloseNoneExistAssetTest() { @Test public void sameTokenNameOpenNoneExistAssetTest() { chainBaseManager.getDynamicPropertiesStore().saveAllowSameTokenName(1); - DateTime now = DateTime.now(); - initAssetIssue(now.minusDays(1).getMillis(), now.plusDays(1).getMillis()); + ZonedDateTime now = ZonedDateTime.now(); + initAssetIssue(now.minusDays(1).toInstant().toEpochMilli(), + now.plusDays(1).toInstant().toEpochMilli()); ParticipateAssetIssueActuator actuator = new ParticipateAssetIssueActuator(); actuator.setChainBaseManager(chainBaseManager) .setAny(getContract(1, "TTTTTTTTTTTT")); diff --git a/framework/src/test/java/org/tron/core/net/messagehandler/TransactionsMsgHandlerTest.java b/framework/src/test/java/org/tron/core/net/messagehandler/TransactionsMsgHandlerTest.java index ed2121d360f..78af06e64bc 100644 --- a/framework/src/test/java/org/tron/core/net/messagehandler/TransactionsMsgHandlerTest.java +++ b/framework/src/test/java/org/tron/core/net/messagehandler/TransactionsMsgHandlerTest.java @@ -4,6 +4,7 @@ import com.google.protobuf.ByteString; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -14,7 +15,6 @@ import java.util.concurrent.RejectedExecutionException; import lombok.Getter; -import org.joda.time.DateTime; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; @@ -67,7 +67,7 @@ public void testProcessMessage() { .setOwnerAddress(ByteString.copyFrom(ByteArray.fromHexString("121212a9cf"))) .setToAddress(ByteString.copyFrom(ByteArray.fromHexString("232323a9cf"))).build(); - long transactionTimestamp = DateTime.now().minusDays(4).getMillis(); + long transactionTimestamp = ZonedDateTime.now().minusDays(4).toInstant().toEpochMilli(); Protocol.Transaction trx = Protocol.Transaction.newBuilder().setRawData( Protocol.Transaction.raw.newBuilder().setTimestamp(transactionTimestamp) .setRefBlockNum(1) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 6a3e641d5d6..2e30496116f 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -49,25 +49,25 @@ - - - + + + - - + + - - - + + + - - + + - - - + + + @@ -189,9 +189,9 @@ - - - + + + @@ -199,9 +199,9 @@ - - - + + + @@ -219,15 +219,15 @@ - - - + + + - - + + - - + + @@ -235,15 +235,15 @@ - - - + + + - - + + - - + + @@ -251,15 +251,15 @@ - - - + + + - - + + - - + + @@ -1171,76 +1171,76 @@ - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + @@ -1251,18 +1251,18 @@ - - - + + + - - + + - - + + - - + + @@ -1528,14 +1528,6 @@ - - - - - - - - @@ -1684,11 +1676,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1710,36 +1728,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + @@ -1792,6 +1783,11 @@ + + + + + @@ -2408,6 +2404,14 @@ + + + + + + + + @@ -2424,6 +2428,14 @@ + + + + + + + + @@ -2448,6 +2460,11 @@ + + + + + @@ -2612,20 +2629,20 @@ - - - + + + - - + + - - - + + + - - + + @@ -2647,16 +2664,26 @@ - - - - - - + + + + + + + + + + + + + + + + diff --git a/plugins/src/main/resources/logback.xml b/plugins/src/main/resources/logback.xml index fa557f1a412..3f5eff3a1e0 100644 --- a/plugins/src/main/resources/logback.xml +++ b/plugins/src/main/resources/logback.xml @@ -3,16 +3,7 @@ - - - - - %d{HH:mm:ss.SSS} %-5level [%t] [%c{1}]\(%F:%L\) %m%n - - - INFO - - + From 0d1948531818f78a6254d4bd6c11ba7043fae318 Mon Sep 17 00:00:00 2001 From: halibobo1205 <82020050+halibobo1205@users.noreply.github.com> Date: Mon, 14 Sep 2026 15:07:22 +0800 Subject: [PATCH 20/20] feat(api): sanitize HTTP API error responses (#6954) * feat(api): sanitize HTTP API error responses Standard HTTP error paths used to expose internal details to clients: Util.processError prefixed every message with the Java exception class name, several servlets printed raw Throwable.getMessage() directly, and the two solidity query endpoints returned bare-text error bodies. Centralize the client-facing text decision in Util.processError: * keep the raw non-blank message only for the exact runtime types JsonFormat.ParseException, ContractValidateException and MaintenanceUnavailableException; a null, empty or whitespace-only message falls back to "internal server error" * preserve the events-deprecation message only for the exact IllegalArgumentException type carrying EVENTS_DEPRECATED_MSG * write the fixed rate-limit and INVALID address messages, along with existing GetBlock validation messages, through the package-private writeAuditedError helper; these audited callers bypass exception classification, and printErrorMsg is private to the shared writer * return {"Error":"internal server error"} for every other exception, with no exception class name Client-visible changes: * all processError-based error bodies lose the "class : " prefix; unclassified raw messages become "internal server error" * the rate-limit rejection body becomes {"Error":"lack of computing resources"} on every endpoint extending RateLimiterServlet, including full-node, solidity and PBFT /jsonrpc * gettransactionbyid / gettransactioninfobyid on solidity return standard {"Error":...} JSON instead of bare text * validateaddress, getBrokerage and getReward replace leaked library messages in their failure branches with existing fixed texts; the "INVALID address" body is now written via writeAuditedError and loses the space after the colon * getblock keeps its exact error bodies (refactor only) Cover Solidity transaction and transaction-info GET/POST input errors, backend failures, successful lookups and missing records directly with mocked Wallet calls and in-memory requests and responses. Replace the transaction servlet tests that accidentally exercised POST in both cases, changed global stdout and used a shared temporary response file. Verify both endpoint and global rate-limit rejections across the three JSON-RPC servlet variants, including status, response body and the absence of business dispatch on rejection. HTTP status codes, success responses, request validation rules and gRPC behavior are unchanged. JSON-RPC behavior is unchanged except for the shared HTTP rate-limit response described above. Closes #6936 * fix(api): keep server-side failure logging at error level The previous commit routed four catch-all blocks through the shared processError entry point, which logs at debug. Those four catches cover server-side work only: getburntrx, getnodeinfo and getpendingsize read no request parameters, and in getreward malformed addresses are already handled by the preceding DecoderException | IllegalArgumentException catch. Their failures therefore left no trace under the default log configuration, where the API topic is INFO. Add a dedicated processServerError entry point that logs at error and then applies the same sanitization, and use it at those four call sites. Logging the exception once inside the helper keeps a single record at any log level, instead of pairing an error log in the servlet with the debug log in the shared path. The shared Exception entry point keeps debug on purpose: its callers also cover request parsing, so an unauthenticated client can fail it cheaply and repeatedly, and an unconditional stack trace per request would amplify that into log pressure. Distinguishing client from server faults on that path is the parameter/internal split tracked as follow-up in #6936. Client-facing responses are unchanged. --- .../core/services/http/GetBlockServlet.java | 4 +- .../services/http/GetBrokerageServlet.java | 8 +- .../core/services/http/GetBurnTrxServlet.java | 8 +- .../services/http/GetNodeInfoServlet.java | 8 +- .../services/http/GetPendingSizeServlet.java | 8 +- .../core/services/http/GetRewardServlet.java | 15 +- .../GetTransactionInfoByBlockNumServlet.java | 15 +- .../services/http/RateLimiterServlet.java | 3 +- .../org/tron/core/services/http/Util.java | 48 ++- .../services/http/ValidateAddressServlet.java | 2 +- .../GetTransactionByIdSolidityServlet.java | 14 +- ...GetTransactionInfoByIdSolidityServlet.java | 15 +- .../services/http/BroadcastServletTest.java | 2 +- .../http/JsonRpcRateLimiterServletTest.java | 129 ++++++++ .../services/http/UtilProcessErrorTest.java | 107 +++++++ ...GetTransactionByIdSolidityServletTest.java | 286 +++++++----------- ...ransactionInfoByIdSolidityServletTest.java | 135 +++++++++ 17 files changed, 545 insertions(+), 262 deletions(-) create mode 100644 framework/src/test/java/org/tron/core/services/http/JsonRpcRateLimiterServletTest.java create mode 100644 framework/src/test/java/org/tron/core/services/http/UtilProcessErrorTest.java create mode 100644 framework/src/test/java/org/tron/core/services/http/solidity/GetTransactionInfoByIdSolidityServletTest.java diff --git a/framework/src/main/java/org/tron/core/services/http/GetBlockServlet.java b/framework/src/main/java/org/tron/core/services/http/GetBlockServlet.java index 2320fc87c7d..a953ae11802 100644 --- a/framework/src/main/java/org/tron/core/services/http/GetBlockServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/GetBlockServlet.java @@ -77,9 +77,7 @@ private void fillResponse(boolean visible, BlockReq request, HttpServletResponse response.getWriter().println("{}"); } } catch (IllegalArgumentException e) { - JSONObject jsonObject = new JSONObject(); - jsonObject.put("Error", e.getMessage()); - response.getWriter().println(jsonObject.toJSONString()); + Util.writeAuditedError(e.getMessage(), response); } } diff --git a/framework/src/main/java/org/tron/core/services/http/GetBrokerageServlet.java b/framework/src/main/java/org/tron/core/services/http/GetBrokerageServlet.java index 1fbd94fe690..b735878d1e1 100644 --- a/framework/src/main/java/org/tron/core/services/http/GetBrokerageServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/GetBrokerageServlet.java @@ -1,6 +1,5 @@ package org.tron.core.services.http; -import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; @@ -27,12 +26,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) { } response.getWriter().println("{\"brokerage\": " + value + "}"); } catch (DecoderException | IllegalArgumentException e) { - try { - response.getWriter() - .println("{\"Error\": " + "\"INVALID address, " + e.getMessage() + "\"}"); - } catch (IOException ioe) { - logger.debug("IOException: {}", ioe.getMessage()); - } + Util.writeAuditedError(Util.INVALID_ADDRESS_MSG, response); } catch (Exception e) { Util.processError(e, response); } diff --git a/framework/src/main/java/org/tron/core/services/http/GetBurnTrxServlet.java b/framework/src/main/java/org/tron/core/services/http/GetBurnTrxServlet.java index ea066a6e98c..3a19825ba75 100644 --- a/framework/src/main/java/org/tron/core/services/http/GetBurnTrxServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/GetBurnTrxServlet.java @@ -1,6 +1,5 @@ package org.tron.core.services.http; -import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; @@ -24,12 +23,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) { : "{\"burnTrxAmount\": " + value + "}"; response.getWriter().println(out); } catch (Exception e) { - logger.error("", e); - try { - response.getWriter().println(Util.printErrorMsg(e)); - } catch (IOException ioe) { - logger.debug("IOException: {}", ioe.getMessage()); - } + Util.processServerError(e, response); } } diff --git a/framework/src/main/java/org/tron/core/services/http/GetNodeInfoServlet.java b/framework/src/main/java/org/tron/core/services/http/GetNodeInfoServlet.java index 0b8f7b9ce2b..c8b4aa39785 100644 --- a/framework/src/main/java/org/tron/core/services/http/GetNodeInfoServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/GetNodeInfoServlet.java @@ -1,6 +1,5 @@ package org.tron.core.services.http; -import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; @@ -24,12 +23,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) { response.getWriter().println(JSON.toJSONString(nodeInfo)); } catch (Exception e) { - logger.error("", e); - try { - response.getWriter().println(Util.printErrorMsg(e)); - } catch (IOException ioe) { - logger.debug("IOException: {}", ioe.getMessage()); - } + Util.processServerError(e, response); } } diff --git a/framework/src/main/java/org/tron/core/services/http/GetPendingSizeServlet.java b/framework/src/main/java/org/tron/core/services/http/GetPendingSizeServlet.java index 9788c926586..41a47c49001 100644 --- a/framework/src/main/java/org/tron/core/services/http/GetPendingSizeServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/GetPendingSizeServlet.java @@ -1,6 +1,5 @@ package org.tron.core.services.http; -import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; @@ -24,12 +23,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) { : "{\"pendingSize\": " + value + "}"; response.getWriter().println(out); } catch (Exception e) { - logger.error("", e); - try { - response.getWriter().println(Util.printErrorMsg(e)); - } catch (IOException ioe) { - logger.debug("IOException: {}", ioe.getMessage()); - } + Util.processServerError(e, response); } } diff --git a/framework/src/main/java/org/tron/core/services/http/GetRewardServlet.java b/framework/src/main/java/org/tron/core/services/http/GetRewardServlet.java index 61b88d1160f..780bab6ac94 100644 --- a/framework/src/main/java/org/tron/core/services/http/GetRewardServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/GetRewardServlet.java @@ -1,6 +1,5 @@ package org.tron.core.services.http; -import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; @@ -29,19 +28,9 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) { : "{\"reward\": " + value + "}"; response.getWriter().println(out); } catch (DecoderException | IllegalArgumentException e) { - try { - response.getWriter() - .println("{\"Error\": " + "\"INVALID address, " + e.getMessage() + "\"}"); - } catch (IOException ioe) { - logger.debug("IOException: {}", ioe.getMessage()); - } + Util.writeAuditedError(Util.INVALID_ADDRESS_MSG, response); } catch (Exception e) { - logger.error("", e); - try { - response.getWriter().println(Util.printErrorMsg(e)); - } catch (IOException ioe) { - logger.debug("IOException: {}", ioe.getMessage()); - } + Util.processServerError(e, response); } } diff --git a/framework/src/main/java/org/tron/core/services/http/GetTransactionInfoByBlockNumServlet.java b/framework/src/main/java/org/tron/core/services/http/GetTransactionInfoByBlockNumServlet.java index 5d0a09b1a68..25998c909b6 100644 --- a/framework/src/main/java/org/tron/core/services/http/GetTransactionInfoByBlockNumServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/GetTransactionInfoByBlockNumServlet.java @@ -1,6 +1,5 @@ package org.tron.core.services.http; -import java.io.IOException; import java.util.List; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -52,12 +51,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) { response.getWriter().println("{}"); } } catch (Exception e) { - logger.debug("Exception: {}", e.getMessage()); - try { - response.getWriter().println(Util.printErrorMsg(e)); - } catch (IOException ioe) { - logger.debug("IOException: {}", ioe.getMessage()); - } + Util.processError(e, response); } } @@ -75,12 +69,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) response.getWriter().println("{}"); } } catch (Exception e) { - logger.debug("Exception: {}", e.getMessage()); - try { - response.getWriter().println(Util.printErrorMsg(e)); - } catch (IOException ioe) { - logger.debug("IOException: {}", ioe.getMessage()); - } + Util.processError(e, response); } } } diff --git a/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java index b5ae7d58623..6f67aba3020 100644 --- a/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java @@ -131,8 +131,7 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) super.service(req, resp); Metrics.histogramObserve(requestTimer); } else { - resp.getWriter() - .println(Util.printErrorMsg(new IllegalAccessException("lack of computing resources"))); + Util.writeAuditedError(Util.RATE_LIMITER_ERROR_MSG, resp); } } catch (ServletException | IOException | BadMessageException e) { throw e; diff --git a/framework/src/main/java/org/tron/core/services/http/Util.java b/framework/src/main/java/org/tron/core/services/http/Util.java index 5be2495e1f7..ca20902c4d8 100644 --- a/framework/src/main/java/org/tron/core/services/http/Util.java +++ b/framework/src/main/java/org/tron/core/services/http/Util.java @@ -48,6 +48,8 @@ import org.tron.core.capsule.TransactionCapsule; import org.tron.core.config.args.Args; import org.tron.core.db.TransactionTrace; +import org.tron.core.exception.ContractValidateException; +import org.tron.core.exception.MaintenanceUnavailableException; import org.tron.core.services.http.JsonFormat.ParseException; import org.tron.json.JSON; import org.tron.json.JSONArray; @@ -65,6 +67,10 @@ @Slf4j(topic = "API") public class Util { + private static final String INTERNAL_SERVER_ERROR = "internal server error"; + public static final String RATE_LIMITER_ERROR_MSG = "lack of computing resources"; + static final String INVALID_ADDRESS_MSG = "INVALID address"; + public static final String EVENTS_DEPRECATED_MSG = "'events' field is deprecated and no longer supported"; @@ -114,12 +120,31 @@ public static String printTransactionFee(String transactionFee) { return jsonObject.toJSONString(); } - public static String printErrorMsg(Exception e) { + private static String printErrorMsg(String msg) { JSONObject jsonObject = new JSONObject(); - jsonObject.put("Error", e.getClass() + " : " + e.getMessage()); + jsonObject.put("Error", msg); return jsonObject.toJSONString(); } + private static String clientMessage(Exception e) { + if (e == null) { + return INTERNAL_SERVER_ERROR; + } + + Class type = e.getClass(); + if (type == IllegalArgumentException.class) { + return EVENTS_DEPRECATED_MSG.equals(e.getMessage()) + ? EVENTS_DEPRECATED_MSG : INTERNAL_SERVER_ERROR; + } + if (type == ParseException.class + || type == ContractValidateException.class + || type == MaintenanceUnavailableException.class) { + String message = e.getMessage(); + return StringUtils.isBlank(message) ? INTERNAL_SERVER_ERROR : message; + } + return INTERNAL_SERVER_ERROR; + } + public static String printBlockList(BlockList list, boolean selfType) { List blocks = list.getBlockList(); JSONObject jsonObject = new JSONObject(); @@ -526,11 +551,24 @@ public static String getMemo(byte[] memo) { } public static void processError(Exception e, HttpServletResponse response) { - logger.debug(e.getMessage(), e); + logger.debug("HTTP request failed", e); + writeAuditedError(clientMessage(e), response); + } + + // For catch blocks that cover server-side work only, so the failure stays visible at the + // default log level. The Exception entry point above keeps debug because its callers also + // cover request parsing, which an unauthenticated client can fail cheaply and repeatedly. + static void processServerError(Exception e, HttpServletResponse response) { + logger.error("HTTP request failed", e); + writeAuditedError(clientMessage(e), response); + } + + // Bypasses clientMessage: callers must pass audited fixed or pre-existing client texts only. + static void writeAuditedError(String msg, HttpServletResponse response) { try { - response.getWriter().println(Util.printErrorMsg(e)); + response.getWriter().println(Util.printErrorMsg(msg)); } catch (IOException ioe) { - logger.debug("IOException: {}", ioe.getMessage()); + logger.debug("Failed to write HTTP error response", ioe); } } diff --git a/framework/src/main/java/org/tron/core/services/http/ValidateAddressServlet.java b/framework/src/main/java/org/tron/core/services/http/ValidateAddressServlet.java index 07eecfc5466..3ef45b42a7e 100644 --- a/framework/src/main/java/org/tron/core/services/http/ValidateAddressServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/ValidateAddressServlet.java @@ -47,7 +47,7 @@ private String validAddress(String input) { } } catch (Exception e) { result = false; - msg = e.getMessage(); + msg = "Invalid address"; } JSONObject jsonAddress = new JSONObject(); diff --git a/framework/src/main/java/org/tron/core/services/http/solidity/GetTransactionByIdSolidityServlet.java b/framework/src/main/java/org/tron/core/services/http/solidity/GetTransactionByIdSolidityServlet.java index f98c7450afc..5998bc0850f 100644 --- a/framework/src/main/java/org/tron/core/services/http/solidity/GetTransactionByIdSolidityServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/solidity/GetTransactionByIdSolidityServlet.java @@ -30,12 +30,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) { String input = request.getParameter("value"); fillResponse(ByteString.copyFrom(ByteArray.fromHexString(input)), visible, response); } catch (Exception e) { - logger.debug("Exception: {}", e.getMessage()); - try { - response.getWriter().println(e.getMessage()); - } catch (IOException ioe) { - logger.debug("IOException: {}", ioe.getMessage()); - } + Util.processError(e, response); } } @@ -46,12 +41,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) JsonFormat.merge(params.getParams(), build, params.isVisible()); fillResponse(build.build().getValue(), params.isVisible(), response); } catch (Exception e) { - logger.debug("Exception: {}", e.getMessage()); - try { - response.getWriter().println(e.getMessage()); - } catch (IOException ioe) { - logger.debug("IOException: {}", ioe.getMessage()); - } + Util.processError(e, response); } } diff --git a/framework/src/main/java/org/tron/core/services/http/solidity/GetTransactionInfoByIdSolidityServlet.java b/framework/src/main/java/org/tron/core/services/http/solidity/GetTransactionInfoByIdSolidityServlet.java index 0408215f09d..197f5aaec0d 100644 --- a/framework/src/main/java/org/tron/core/services/http/solidity/GetTransactionInfoByIdSolidityServlet.java +++ b/framework/src/main/java/org/tron/core/services/http/solidity/GetTransactionInfoByIdSolidityServlet.java @@ -1,7 +1,6 @@ package org.tron.core.services.http.solidity; import com.google.protobuf.ByteString; -import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; @@ -37,12 +36,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) { response.getWriter().println(JsonFormat.printToString(transInfo, visible)); } } catch (Exception e) { - logger.debug("Exception: {}", e.getMessage()); - try { - response.getWriter().println(e.getMessage()); - } catch (IOException ioe) { - logger.debug("IOException: {}", ioe.getMessage()); - } + Util.processError(e, response); } } @@ -60,12 +54,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) response.getWriter().println(JsonFormat.printToString(transInfo, params.isVisible())); } } catch (Exception e) { - logger.debug("Exception: {}", e.getMessage()); - try { - response.getWriter().println(e.getMessage()); - } catch (IOException ioe) { - logger.debug("IOException: {}", ioe.getMessage()); - } + Util.processError(e, response); } } diff --git a/framework/src/test/java/org/tron/core/services/http/BroadcastServletTest.java b/framework/src/test/java/org/tron/core/services/http/BroadcastServletTest.java index d6bf3850f30..532ddcd5521 100644 --- a/framework/src/test/java/org/tron/core/services/http/BroadcastServletTest.java +++ b/framework/src/test/java/org/tron/core/services/http/BroadcastServletTest.java @@ -156,7 +156,7 @@ public void doPostTest() throws IOException { while ((text = bufferedReader.readLine()) != null) { sb.append(text); } - Assert.assertTrue(sb.toString().contains("null")); + Assert.assertTrue(sb.toString().contains("{\"Error\":\"internal server error\"}")); httpUrlConnection.disconnect(); } } \ No newline at end of file diff --git a/framework/src/test/java/org/tron/core/services/http/JsonRpcRateLimiterServletTest.java b/framework/src/test/java/org/tron/core/services/http/JsonRpcRateLimiterServletTest.java new file mode 100644 index 00000000000..52ff23a7d2d --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/http/JsonRpcRateLimiterServletTest.java @@ -0,0 +1,129 @@ +package org.tron.core.services.http; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import com.googlecode.jsonrpc4j.JsonRpcServer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collection; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.mockito.MockedStatic; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.util.ReflectionTestUtils; +import org.tron.common.TestConstants; +import org.tron.core.config.args.Args; +import org.tron.core.services.interfaceJsonRpcOnPBFT.JsonRpcOnPBFTServlet; +import org.tron.core.services.interfaceJsonRpcOnSolidity.JsonRpcOnSolidityServlet; +import org.tron.core.services.interfaceOnPBFT.WalletOnPBFT; +import org.tron.core.services.interfaceOnSolidity.WalletOnSolidity; +import org.tron.core.services.jsonrpc.JsonRpcServlet; +import org.tron.core.services.ratelimiter.GlobalRateLimiter; +import org.tron.core.services.ratelimiter.RateLimiterContainer; +import org.tron.core.services.ratelimiter.RuntimeData; +import org.tron.core.services.ratelimiter.adapter.IRateLimiter; + +@RunWith(Parameterized.class) +public class JsonRpcRateLimiterServletTest { + + private final Class servletClass; + private RateLimiterServlet servlet; + private IRateLimiter perEndpoint; + private Object dispatcher; + private MockHttpServletRequest request; + private MockHttpServletResponse response; + + public JsonRpcRateLimiterServletTest(Class servletClass) { + this.servletClass = servletClass; + } + + @Parameterized.Parameters(name = "{0}") + public static Collection servlets() { + return Arrays.asList(new Object[][] { + {JsonRpcServlet.class}, + {JsonRpcOnSolidityServlet.class}, + {JsonRpcOnPBFTServlet.class} + }); + } + + @Before + public void setUp() throws Exception { + // Initialize Args before GlobalRateLimiter's static QPS limiters are loaded. + Args.setParam(new String[0], TestConstants.TEST_CONF); + servlet = servletClass.getDeclaredConstructor().newInstance(); + RateLimiterContainer container = new RateLimiterContainer(); + perEndpoint = mock(IRateLimiter.class); + container.add("http_", servletClass.getSimpleName(), perEndpoint); + ReflectionTestUtils.setField(servlet, "container", container); + + if (servlet instanceof JsonRpcOnSolidityServlet) { + dispatcher = mock(WalletOnSolidity.class); + ReflectionTestUtils.setField(servlet, "walletOnSolidity", dispatcher); + } else if (servlet instanceof JsonRpcOnPBFTServlet) { + dispatcher = mock(WalletOnPBFT.class); + ReflectionTestUtils.setField(servlet, "walletOnPBFT", dispatcher); + } else { + dispatcher = mock(JsonRpcServer.class); + ReflectionTestUtils.setField(servlet, "rpcServer", dispatcher); + } + + request = new MockHttpServletRequest("POST", "/jsonrpc"); + request.setServletPath("/jsonrpc"); + request.setRemoteAddr("10.0.0.1"); + request.setContentType("application/json"); + request.setContent("{\"jsonrpc\":\"2.0\",\"method\":\"eth_blockNumber\",\"id\":1}" + .getBytes(StandardCharsets.UTF_8)); + response = new MockHttpServletResponse(); + } + + @After + public void tearDown() { + Args.clearParam(); + } + + @Test + public void testPerEndpointRejectionReturnsSanitizedHttpError() throws Exception { + when(perEndpoint.acquirePermit(any(RuntimeData.class))).thenReturn(false); + + try (MockedStatic global = mockStatic(GlobalRateLimiter.class)) { + servlet.service(request, response); + + global.verify(() -> GlobalRateLimiter.acquirePermit(any()), never()); + assertRateLimitResponse(); + } + } + + @Test + public void testGlobalRejectionReturnsSanitizedHttpError() throws Exception { + when(perEndpoint.acquirePermit(any(RuntimeData.class))).thenReturn(true); + + try (MockedStatic global = mockStatic(GlobalRateLimiter.class)) { + global.when(() -> GlobalRateLimiter.acquirePermit(any())).thenReturn(false); + + servlet.service(request, response); + + global.verify(() -> GlobalRateLimiter.acquirePermit(any())); + assertRateLimitResponse(); + } + } + + private void assertRateLimitResponse() throws Exception { + assertEquals(200, response.getStatus()); + assertEquals("application/json; charset=utf-8", response.getContentType()); + assertEquals("{\"Error\":\"lack of computing resources\"}", + response.getContentAsString().trim()); + verify(perEndpoint).acquirePermit(any(RuntimeData.class)); + verifyNoInteractions(dispatcher); + } +} diff --git a/framework/src/test/java/org/tron/core/services/http/UtilProcessErrorTest.java b/framework/src/test/java/org/tron/core/services/http/UtilProcessErrorTest.java new file mode 100644 index 00000000000..5d4baa34c6f --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/http/UtilProcessErrorTest.java @@ -0,0 +1,107 @@ +package org.tron.core.services.http; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import com.google.protobuf.InvalidProtocolBufferException; +import org.bouncycastle.util.encoders.DecoderException; +import org.bouncycastle.util.encoders.Hex; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletResponse; +import org.tron.core.exception.ContractValidateException; +import org.tron.core.exception.HeaderNotFound; +import org.tron.core.exception.MaintenanceUnavailableException; +import org.tron.core.exception.ZkProofValidateException; +import org.tron.json.JSONException; +import org.tron.json.JSONObject; + +public class UtilProcessErrorTest { + + private static final String INTERNAL_SERVER_ERROR = "internal server error"; + private static final String RATE_LIMITER_ERROR_MSG = "lack of computing resources"; + + @Test + public void exactCompatibilityTypesPreserveNonBlankMessage() throws Exception { + assertError(new JsonFormat.ParseException("1:2: invalid \"field\"\nvalue"), + "1:2: invalid \"field\"\nvalue"); + assertError(new ContractValidateException("balance is not sufficient"), + "balance is not sufficient"); + assertError(new MaintenanceUnavailableException("maintenance in progress"), + "maintenance in progress"); + } + + @Test + public void unclassifiedTypesFailClosed() throws Exception { + DecoderException decoder = assertThrows(DecoderException.class, () -> Hex.decode("zz")); + Exception[] errors = { + new NullPointerException("internal field name"), + new JSONException("server serialization detail"), + new InvalidProtocolBufferException("stored protobuf detail"), + decoder, + new HeaderNotFound("latest block not found"), + new IllegalArgumentException("No enum constant internal.Type.VALUE"), + new IllegalAccessException(RATE_LIMITER_ERROR_MSG), + new IllegalAccessException("other access failure"), + new ZkProofValidateException("wrapped validation detail", true) + }; + + for (Exception error : errors) { + assertError(error, INTERNAL_SERVER_ERROR); + } + } + + @Test + public void onlyExactFixedControlSignalsArePreserved() throws Exception { + assertError(new IllegalArgumentException(Util.EVENTS_DEPRECATED_MSG), + Util.EVENTS_DEPRECATED_MSG); + assertError(new IllegalArgumentException("other argument failure"), INTERNAL_SERVER_ERROR); + assertError(new NumberFormatException(Util.EVENTS_DEPRECATED_MSG), INTERNAL_SERVER_ERROR); + } + + @Test + public void nullBlankAndSubclassMessagesFailClosed() throws Exception { + assertError(null, INTERNAL_SERVER_ERROR); + assertError(new JsonFormat.ParseException(null), INTERNAL_SERVER_ERROR); + assertError(new JsonFormat.ParseException(""), INTERNAL_SERVER_ERROR); + assertError(new JsonFormat.ParseException(" "), INTERNAL_SERVER_ERROR); + assertError(new ContractValidateException("subclass message") { }, INTERNAL_SERVER_ERROR); + } + + @Test + public void auditedErrorWriterPreservesTextVerbatim() throws Exception { + for (String audited : new String[] {Util.INVALID_ADDRESS_MSG, Util.RATE_LIMITER_ERROR_MSG}) { + MockHttpServletResponse response = new MockHttpServletResponse(); + Util.writeAuditedError(audited, response); + JSONObject body = JSONObject.parseObject(response.getContentAsString()); + assertEquals(audited, body.getString("Error")); + } + } + + @Test + public void auditedErrorWriterWithNullMessageWritesEmptyObject() throws Exception { + MockHttpServletResponse response = new MockHttpServletResponse(); + Util.writeAuditedError(null, response); + assertEquals("{}", response.getContentAsString().trim()); + } + + @Test + public void serverErrorChannelSanitizesLikeTheSharedPath() throws Exception { + assertServerError(new NullPointerException("internal field name"), INTERNAL_SERVER_ERROR); + assertServerError(new ContractValidateException("balance is not sufficient"), + "balance is not sufficient"); + } + + private static void assertServerError(Exception error, String expected) throws Exception { + MockHttpServletResponse response = new MockHttpServletResponse(); + Util.processServerError(error, response); + JSONObject body = JSONObject.parseObject(response.getContentAsString()); + assertEquals(expected, body.getString("Error")); + } + + private static void assertError(Exception error, String expected) throws Exception { + MockHttpServletResponse response = new MockHttpServletResponse(); + Util.processError(error, response); + JSONObject body = JSONObject.parseObject(response.getContentAsString()); + assertEquals(expected, body.getString("Error")); + } +} diff --git a/framework/src/test/java/org/tron/core/services/http/solidity/GetTransactionByIdSolidityServletTest.java b/framework/src/test/java/org/tron/core/services/http/solidity/GetTransactionByIdSolidityServletTest.java index e1abb41d1e1..cacb904d9b9 100644 --- a/framework/src/test/java/org/tron/core/services/http/solidity/GetTransactionByIdSolidityServletTest.java +++ b/framework/src/test/java/org/tron/core/services/http/solidity/GetTransactionByIdSolidityServletTest.java @@ -1,202 +1,146 @@ package org.tron.core.services.http.solidity; -import static org.mockito.BDDMockito.given; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.io.PrintStream; -import java.io.PrintWriter; -import java.net.HttpURLConnection; -import java.net.URL; -import java.net.URLStreamHandlerFactory; -import java.nio.charset.StandardCharsets; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import lombok.extern.slf4j.Slf4j; +import com.google.protobuf.ByteString; +import java.util.Arrays; +import java.util.Collection; import org.junit.After; -import org.junit.Assert; import org.junit.Before; -import org.junit.BeforeClass; import org.junit.Test; -import org.tron.common.utils.FileUtil; -import org.tron.common.utils.PublicMethod; -import org.tron.core.services.http.solidity.mockito.HttpUrlStreamHandler; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.util.ReflectionTestUtils; +import org.tron.common.utils.ByteArray; +import org.tron.common.utils.Sha256Hash; +import org.tron.core.Wallet; +import org.tron.core.config.args.Args; +import org.tron.json.JSONObject; +import org.tron.protos.Protocol.Transaction; + +@RunWith(Parameterized.class) +public class GetTransactionByIdSolidityServletTest { + private static final String TRANSACTION_ID = + "309b6fa3d01353e46f57dd8a8f27611f98e392b50d035cef213f2c55225a8bd2"; + private static final ByteString TRANSACTION_ID_BYTES = + ByteString.copyFrom(ByteArray.fromHexString(TRANSACTION_ID)); -@Slf4j -public class GetTransactionByIdSolidityServletTest { + @Parameter + public String method; - private static HttpUrlStreamHandler httpUrlStreamHandler; - private GetTransactionByIdSolidityServlet getTransactionByIdSolidityServlet; - private HttpServletRequest request; - private HttpServletResponse response; - private HttpURLConnection httpUrlConnection; - private OutputStreamWriter outputStreamWriter; - private URL url; - - /** - * . - */ - @BeforeClass - public static void init() { - // Allows for mocking URL connections - URLStreamHandlerFactory urlStreamHandlerFactory = mock(URLStreamHandlerFactory.class); - try { - URL.setURLStreamHandlerFactory(urlStreamHandlerFactory); - } catch (Error e) { - logger.info("Ignore error: {}", e.getMessage()); - } + private GetTransactionByIdSolidityServlet servlet; + private Wallet wallet; + private long savedMaxMessageSize; - httpUrlStreamHandler = new HttpUrlStreamHandler(); - given(urlStreamHandlerFactory.createURLStreamHandler("http")).willReturn(httpUrlStreamHandler); + @Parameters(name = "{0}") + public static Collection methods() { + return Arrays.asList(new Object[][] {{"GET"}, {"POST"}}); } - /** - * Init. - */ - @Before public void setUp() { - getTransactionByIdSolidityServlet = new GetTransactionByIdSolidityServlet(); - this.request = mock(HttpServletRequest.class); - this.response = mock(HttpServletResponse.class); - this.httpUrlConnection = mock(HttpURLConnection.class); - this.outputStreamWriter = mock(OutputStreamWriter.class); - httpUrlStreamHandler.resetConnections(); + savedMaxMessageSize = Args.getInstance().getHttpMaxMessageSize(); + Args.getInstance().setHttpMaxMessageSize(1024); + servlet = new GetTransactionByIdSolidityServlet(); + wallet = mock(Wallet.class); + ReflectionTestUtils.setField(servlet, "wallet", wallet); } - /** - * Release Resource. - */ @After public void tearDown() { - if (FileUtil.deleteDir(new File("temp.txt"))) { - logger.info("Release resources successful."); + Args.getInstance().setHttpMaxMessageSize(savedMaxMessageSize); + } + + @Test + public void walletFailureReturnsSanitizedJson() throws Exception { + when(wallet.getTransactionById(TRANSACTION_ID_BYTES)) + .thenThrow(new NullPointerException("internal transaction store detail")); + + MockHttpServletResponse response = request(TRANSACTION_ID); + + assertEquals("internal server error", errorMessage(response)); + verify(wallet).getTransactionById(TRANSACTION_ID_BYTES); + } + + @Test + public void invalidHexReturnsJsonWithoutCallingWallet() throws Exception { + MockHttpServletResponse response = request("zz"); + + String message = errorMessage(response); + if ("GET".equals(method)) { + assertEquals("internal server error", message); } else { - logger.info("Release resources failure."); + assertTrue(message.matches("\\d+:\\d+: INVALID hex String")); } + verifyNoInteractions(wallet); } @Test - public void doPostTest() throws IOException { - - //send Post request - - final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); - System.setOut(new PrintStream(outContent)); - String href = "http://127.0.0.1:" - + PublicMethod.chooseRandomPort() + "/walletsolidity/gettransactioninfobyid"; - httpUrlStreamHandler.addConnection(new URL(href), httpUrlConnection); - httpUrlConnection.setRequestMethod("POST"); - httpUrlConnection.setRequestProperty("Content-Type", "application/json"); - httpUrlConnection.setRequestProperty("Connection", "Keep-Alive"); - httpUrlConnection.setUseCaches(false); - httpUrlConnection.setDoOutput(true); - String postData = "{\"value\": \"309b6fa3d01353e46f57dd8a8f27611f98e392b50d035cef21" - + "3f2c55225a8bd2\"}"; - httpUrlConnection.setRequestProperty("Content-Length", String.valueOf(postData.length())); - - when(httpUrlConnection.getOutputStream()).thenReturn(outContent); - OutputStreamWriter out = new OutputStreamWriter(httpUrlConnection.getOutputStream(), - StandardCharsets.UTF_8); - out.write(postData); - out.flush(); - out.close(); - PrintWriter writer = new PrintWriter("temp.txt"); - when(response.getWriter()).thenReturn(writer); - - getTransactionByIdSolidityServlet.doPost(request, response); - // Get Response Body - String line; - StringBuilder result = new StringBuilder(); - - byte[] buffer = new byte[1024]; - ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(buffer); - when(httpUrlConnection.getInputStream()).thenReturn(byteArrayInputStream); - BufferedReader in = new BufferedReader(new InputStreamReader(httpUrlConnection.getInputStream(), - StandardCharsets.UTF_8)); - - while ((line = in.readLine()) != null) { - result.append(line).append("\n"); - } - Assert.assertNotNull(result); - in.close(); - writer.flush(); - FileInputStream fileInputStream = new FileInputStream("temp.txt"); - InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream); - BufferedReader bufferedReader = new BufferedReader(inputStreamReader); - - StringBuilder sb = new StringBuilder(); - String text; - while ((text = bufferedReader.readLine()) != null) { - sb.append(text); - } - Assert.assertTrue(sb.toString().contains("null")); - httpUrlConnection.disconnect(); + public void missingTransactionKeepsEmptyObject() throws Exception { + MockHttpServletResponse response = request(TRANSACTION_ID); + + assertEquals(200, response.getStatus()); + assertEquals("{}", response.getContentAsString().trim()); + verify(wallet).getTransactionById(TRANSACTION_ID_BYTES); } @Test - public void doGetTest() throws IOException { - - final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); - System.setOut(new PrintStream(outContent)); - String href = "http://127.0.0.1:" - + PublicMethod.chooseRandomPort() + "/walletsolidity/gettransactioninfobyid"; - httpUrlStreamHandler.addConnection(new URL(href), httpUrlConnection); - httpUrlConnection.setRequestMethod("GET"); - httpUrlConnection.setRequestProperty("Content-Type", "application/json"); - httpUrlConnection.setRequestProperty("Connection", "Keep-Alive"); - httpUrlConnection.setUseCaches(false); - httpUrlConnection.setDoOutput(true); - String postData = "{\"value\": \"309b6fa3d01353e46f57dd8a8f27611f98e392b50d035cef21" - + "3f2c55225a8bd2\"}"; - httpUrlConnection.setRequestProperty("Content-Length", String.valueOf(postData.length())); - - when(httpUrlConnection.getOutputStream()).thenReturn(outContent); - OutputStreamWriter out = new OutputStreamWriter(httpUrlConnection.getOutputStream(), - StandardCharsets.UTF_8); - out.write(postData); - out.flush(); - out.close(); - PrintWriter writer = new PrintWriter("temp.txt"); - when(response.getWriter()).thenReturn(writer); - - getTransactionByIdSolidityServlet.doPost(request, response); - // Get Response Body - String line; - StringBuilder result = new StringBuilder(); - - byte[] buffer = new byte[1024]; - ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(buffer); - when(httpUrlConnection.getInputStream()).thenReturn(byteArrayInputStream); - BufferedReader in = new BufferedReader(new InputStreamReader(httpUrlConnection.getInputStream(), - StandardCharsets.UTF_8)); - - while ((line = in.readLine()) != null) { - result.append(line).append("\n"); - } - Assert.assertNotNull(result); - in.close(); - writer.flush(); - FileInputStream fileInputStream = new FileInputStream("temp.txt"); - InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream); - BufferedReader bufferedReader = new BufferedReader(inputStreamReader); - - StringBuilder sb = new StringBuilder(); - String text; - while ((text = bufferedReader.readLine()) != null) { - sb.append(text); + public void successfulLookupKeepsTransaction() throws Exception { + ByteString signature = ByteString.copyFromUtf8("transaction signature"); + Transaction transaction = Transaction.newBuilder() + .setRawData(Transaction.raw.newBuilder().setTimestamp(123).setExpiration(456)) + .addSignature(signature).build(); + when(wallet.getTransactionById(TRANSACTION_ID_BYTES)).thenReturn(transaction); + + MockHttpServletResponse response = request(TRANSACTION_ID); + + assertEquals(200, response.getStatus()); + JSONObject body = JSONObject.parseObject(response.getContentAsString()); + assertEquals(4, body.size()); + JSONObject rawData = body.getJSONObject("raw_data"); + assertEquals(123L, rawData.getLongValue("timestamp")); + assertEquals(456L, rawData.getLongValue("expiration")); + assertEquals(0, rawData.getJSONArray("contract").size()); + assertEquals(ByteArray.toHexString(transaction.getRawData().toByteArray()), + body.getString("raw_data_hex")); + assertEquals(Sha256Hash.of(Args.getInstance().isECKeyCryptoEngine(), + transaction.getRawData().toByteArray()).toString(), body.getString("txID")); + assertEquals(1, body.getJSONArray("signature").size()); + assertEquals(ByteArray.toHexString(signature.toByteArray()), + body.getJSONArray("signature").getString(0)); + verify(wallet).getTransactionById(TRANSACTION_ID_BYTES); + } + + private MockHttpServletResponse request(String value) throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(method, + "/walletsolidity/gettransactionbyid"); + MockHttpServletResponse response = new MockHttpServletResponse(); + if ("GET".equals(method)) { + request.setParameter("value", value); + servlet.doGet(request, response); + } else { + request.setContentType("application/json"); + request.setContent(("{\"value\":\"" + value + "\"}").getBytes(UTF_8)); + servlet.doPost(request, response); } - Assert.assertTrue(sb.toString().contains("null")); - httpUrlConnection.disconnect(); + return response; } -} + private static String errorMessage(MockHttpServletResponse response) throws Exception { + assertEquals(200, response.getStatus()); + JSONObject body = JSONObject.parseObject(response.getContentAsString()); + assertEquals(1, body.size()); + return body.getString("Error"); + } +} diff --git a/framework/src/test/java/org/tron/core/services/http/solidity/GetTransactionInfoByIdSolidityServletTest.java b/framework/src/test/java/org/tron/core/services/http/solidity/GetTransactionInfoByIdSolidityServletTest.java new file mode 100644 index 00000000000..a8810114f82 --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/http/solidity/GetTransactionInfoByIdSolidityServletTest.java @@ -0,0 +1,135 @@ +package org.tron.core.services.http.solidity; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import com.google.protobuf.ByteString; +import java.util.Arrays; +import java.util.Collection; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.util.ReflectionTestUtils; +import org.tron.common.utils.ByteArray; +import org.tron.core.Wallet; +import org.tron.core.config.args.Args; +import org.tron.json.JSONObject; +import org.tron.protos.Protocol.TransactionInfo; + +@RunWith(Parameterized.class) +public class GetTransactionInfoByIdSolidityServletTest { + + private static final String TRANSACTION_ID = + "309b6fa3d01353e46f57dd8a8f27611f98e392b50d035cef213f2c55225a8bd2"; + private static final ByteString TRANSACTION_ID_BYTES = + ByteString.copyFrom(ByteArray.fromHexString(TRANSACTION_ID)); + + @Parameter + public String method; + + private GetTransactionInfoByIdSolidityServlet servlet; + private Wallet wallet; + private long savedMaxMessageSize; + + @Parameters(name = "{0}") + public static Collection methods() { + return Arrays.asList(new Object[][] {{"GET"}, {"POST"}}); + } + + @Before + public void setUp() { + savedMaxMessageSize = Args.getInstance().getHttpMaxMessageSize(); + Args.getInstance().setHttpMaxMessageSize(1024); + servlet = new GetTransactionInfoByIdSolidityServlet(); + wallet = mock(Wallet.class); + ReflectionTestUtils.setField(servlet, "wallet", wallet); + } + + @After + public void tearDown() { + Args.getInstance().setHttpMaxMessageSize(savedMaxMessageSize); + } + + @Test + public void walletFailureReturnsSanitizedJson() throws Exception { + when(wallet.getTransactionInfoById(TRANSACTION_ID_BYTES)) + .thenThrow(new NullPointerException("internal transaction store detail")); + + MockHttpServletResponse response = request(TRANSACTION_ID); + + assertEquals("internal server error", errorMessage(response)); + verify(wallet).getTransactionInfoById(TRANSACTION_ID_BYTES); + } + + @Test + public void invalidHexReturnsJsonWithoutCallingWallet() throws Exception { + MockHttpServletResponse response = request("zz"); + + String message = errorMessage(response); + if ("GET".equals(method)) { + assertEquals("internal server error", message); + } else { + assertTrue(message.matches("\\d+:\\d+: INVALID hex String")); + } + verifyNoInteractions(wallet); + } + + @Test + public void missingTransactionKeepsEmptyObject() throws Exception { + MockHttpServletResponse response = request(TRANSACTION_ID); + + assertEquals(200, response.getStatus()); + assertEquals("{}", response.getContentAsString().trim()); + verify(wallet).getTransactionInfoById(TRANSACTION_ID_BYTES); + } + + @Test + public void successfulLookupKeepsTransactionInfo() throws Exception { + TransactionInfo info = TransactionInfo.newBuilder() + .setId(TRANSACTION_ID_BYTES).setFee(7).setBlockNumber(123).build(); + when(wallet.getTransactionInfoById(TRANSACTION_ID_BYTES)).thenReturn(info); + + MockHttpServletResponse response = request(TRANSACTION_ID); + + assertEquals(200, response.getStatus()); + JSONObject body = JSONObject.parseObject(response.getContentAsString()); + assertEquals(3, body.size()); + assertEquals(TRANSACTION_ID, body.getString("id")); + assertEquals(7L, body.getLongValue("fee")); + assertEquals(123L, body.getLongValue("blockNumber")); + verify(wallet).getTransactionInfoById(TRANSACTION_ID_BYTES); + } + + private MockHttpServletResponse request(String value) throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(method, + "/walletsolidity/gettransactioninfobyid"); + MockHttpServletResponse response = new MockHttpServletResponse(); + if ("GET".equals(method)) { + request.setParameter("value", value); + servlet.doGet(request, response); + } else { + request.setContentType("application/json"); + request.setContent(("{\"value\":\"" + value + "\"}").getBytes(UTF_8)); + servlet.doPost(request, response); + } + return response; + } + + private static String errorMessage(MockHttpServletResponse response) throws Exception { + assertEquals(200, response.getStatus()); + JSONObject body = JSONObject.parseObject(response.getContentAsString()); + assertEquals(1, body.size()); + return body.getString("Error"); + } +}