Skip to content

Commit f6a6259

Browse files
authored
mirror node security port number in url (#2480)
Signed-off-by: emiliyank <e.kadiyski@gmail.com>
1 parent ff0e5b0 commit f6a6259

7 files changed

Lines changed: 181 additions & 27 deletions

File tree

sdk/src/main/java/com/hedera/hashgraph/sdk/Client.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,20 @@ public synchronized List<String> getMirrorNetwork() {
442442
return mirrorNetwork.getNetwork();
443443
}
444444

445+
/**
446+
* Build the REST base URL for the next healthy mirror node.
447+
* Returns a string like `https://host[:port]/api/v1`.
448+
* If the selected mirror node is a local host (localhost/127.0.0.1) returns `http://localhost:{5551|8545}/api/v1`.
449+
*/
450+
public String getMirrorRestBaseUrl() {
451+
try {
452+
return mirrorNetwork.getRestBaseUrl();
453+
} catch (InterruptedException e) {
454+
Thread.currentThread().interrupt();
455+
throw new IllegalStateException("Interrupted while retrieving mirror base URL", e);
456+
}
457+
}
458+
445459
/**
446460
* Set the mirror network nodes.
447461
*

sdk/src/main/java/com/hedera/hashgraph/sdk/EntityIdHelper.java

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import java.time.Duration;
1313
import java.util.ArrayList;
1414
import java.util.List;
15-
import java.util.Optional;
1615
import java.util.concurrent.CompletableFuture;
1716
import java.util.concurrent.CompletionException;
1817
import java.util.regex.Pattern;
@@ -264,7 +263,7 @@ static boolean isLongZeroAddress(byte[] address) {
264263
*/
265264
static CompletableFuture<Long> getAccountNumFromMirrorNodeAsync(Client client, String evmAddress) {
266265
String apiEndpoint = "/accounts/" + evmAddress;
267-
return performQueryToMirrorNodeAsync(client, apiEndpoint, null, false)
266+
return performQueryToMirrorNodeAsync(client, apiEndpoint, null)
268267
.thenApply(response -> parseNumFromMirrorNodeResponse(response, "account"));
269268
}
270269

@@ -279,7 +278,7 @@ static CompletableFuture<Long> getAccountNumFromMirrorNodeAsync(Client client, S
279278
*/
280279
public static CompletableFuture<EvmAddress> getEvmAddressFromMirrorNodeAsync(Client client, long num) {
281280
String apiEndpoint = "/accounts/" + num;
282-
return performQueryToMirrorNodeAsync(client, apiEndpoint, null, false)
281+
return performQueryToMirrorNodeAsync(client, apiEndpoint, null)
283282
.thenApply(response -> EvmAddress.fromString(parseStringMirrorNodeResponse(response, "evm_address")));
284283
}
285284

@@ -295,30 +294,18 @@ public static CompletableFuture<EvmAddress> getEvmAddressFromMirrorNodeAsync(Cli
295294
public static CompletableFuture<Long> getContractNumFromMirrorNodeAsync(Client client, String evmAddress) {
296295
String apiEndpoint = "/contracts/" + evmAddress;
297296

298-
CompletableFuture<String> responseFuture = performQueryToMirrorNodeAsync(client, apiEndpoint, null, false);
297+
CompletableFuture<String> responseFuture = performQueryToMirrorNodeAsync(client, apiEndpoint, null);
299298

300299
return responseFuture.thenApply(response -> parseNumFromMirrorNodeResponse(response, "contract_id"));
301300
}
302301

303-
static CompletableFuture<String> performQueryToMirrorNodeAsync(
304-
Client client, String apiEndpoint, String jsonBody, boolean isContractCall) {
305-
Optional<String> mirrorUrl = client.getMirrorNetwork().stream()
306-
.map(url -> url.substring(0, url.indexOf(":")))
307-
.findFirst();
308-
309-
if (mirrorUrl.isEmpty()) {
310-
return CompletableFuture.failedFuture(new IllegalArgumentException("Mirror URL not found"));
311-
}
312-
313-
String apiUrl = "https://" + mirrorUrl.get() + "/api/v1" + apiEndpoint;
302+
static CompletableFuture<String> performQueryToMirrorNodeAsync(Client client, String apiEndpoint, String jsonBody) {
303+
return performQueryToMirrorNodeAsync(client.getMirrorRestBaseUrl(), apiEndpoint, jsonBody);
304+
}
314305

315-
if (client.getLedgerId() == null) {
316-
if (isContractCall) {
317-
apiUrl = "http://" + mirrorUrl.get() + ":8545/api/v1" + apiEndpoint;
318-
} else {
319-
apiUrl = "http://" + mirrorUrl.get() + ":5551/api/v1" + apiEndpoint;
320-
}
321-
}
306+
static CompletableFuture<String> performQueryToMirrorNodeAsync(
307+
String baseUrl, String apiEndpoint, String jsonBody) {
308+
String apiUrl = baseUrl + apiEndpoint;
322309

323310
HttpClient httpClient = HttpClient.newHttpClient();
324311
var httpBuilder =

sdk/src/main/java/com/hedera/hashgraph/sdk/MirrorNetwork.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,4 +106,11 @@ protected MirrorNode createNodeFromNetworkEntry(Map.Entry<String, BaseNodeAddres
106106
synchronized MirrorNode getNextMirrorNode() throws InterruptedException {
107107
return getNumberOfMostHealthyNodes(1).get(0);
108108
}
109+
110+
/**
111+
* Convenience to get the REST base URL from the next healthy mirror node.
112+
*/
113+
String getRestBaseUrl() throws InterruptedException {
114+
return getNextMirrorNode().getRestBaseUrl();
115+
}
109116
}

sdk/src/main/java/com/hedera/hashgraph/sdk/MirrorNode.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,46 @@ protected String getAuthority() {
3636
BaseNodeAddress getKey() {
3737
return address;
3838
}
39+
40+
/**
41+
* Build the REST base URL for this mirror node.
42+
*
43+
* @return scheme://host[:port]/api/v1
44+
*/
45+
String getRestBaseUrl() {
46+
String host = address.getAddress();
47+
int port = address.getPort();
48+
49+
if (host == null) {
50+
throw new IllegalStateException("mirror node address is not set");
51+
}
52+
53+
if (isLocalHost(host)) {
54+
// For localhost, always use port 5551 for general REST calls
55+
return "http://" + host + ":5551/api/v1";
56+
}
57+
58+
String scheme = chooseScheme(port);
59+
60+
StringBuilder base = new StringBuilder();
61+
base.append(scheme).append("://").append(host);
62+
// Omit default ports
63+
if (!isDefaultPort(scheme, port)) {
64+
base.append(":").append(port);
65+
}
66+
base.append("/api/v1");
67+
return base.toString();
68+
}
69+
70+
private static boolean isLocalHost(String host) {
71+
return "localhost".equals(host) || "127.0.0.1".equals(host);
72+
}
73+
74+
private static String chooseScheme(int port) {
75+
return port == 80 ? "http" : "https";
76+
}
77+
78+
private static boolean isDefaultPort(String scheme, int port) {
79+
return ("http".equals(scheme) && port == 80) || ("https".equals(scheme) && port == 443);
80+
}
3981
}

sdk/src/main/java/com/hedera/hashgraph/sdk/MirrorNodeContractQuery.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -277,11 +277,22 @@ private CompletableFuture<String> executeMirrorNodeRequest(Client client, String
277277
blockNumber,
278278
estimate);
279279

280-
return performQueryToMirrorNodeAsync(client, apiEndpoint, jsonPayload, true)
281-
.exceptionally(ex -> {
282-
client.getLogger().error("Error while performing post request to Mirror Node: " + ex.getMessage());
283-
throw new CompletionException(ex);
284-
});
280+
String baseUrl = client.getMirrorRestBaseUrl();
281+
282+
// For localhost contract calls, override to use port 8545 unless system property overrides
283+
if (baseUrl.contains("localhost:5551") || baseUrl.contains("127.0.0.1:5551")) {
284+
String contractPort = System.getProperty("hedera.mirror.contract.port");
285+
if (contractPort != null && !contractPort.isEmpty()) {
286+
baseUrl = baseUrl.replace(":5551", ":" + contractPort);
287+
} else {
288+
baseUrl = baseUrl.replace(":5551", ":8545");
289+
}
290+
}
291+
292+
return performQueryToMirrorNodeAsync(baseUrl, apiEndpoint, jsonPayload).exceptionally(ex -> {
293+
client.getLogger().error("Error while performing post request to Mirror Node: " + ex.getMessage());
294+
throw new CompletionException(ex);
295+
});
285296
}
286297

287298
static String createJsonPayload(
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
package com.hedera.hashgraph.sdk;
3+
4+
import static org.assertj.core.api.Assertions.assertThat;
5+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
6+
7+
import java.util.HashMap;
8+
import java.util.List;
9+
import java.util.concurrent.ExecutorService;
10+
import java.util.concurrent.Executors;
11+
import org.junit.jupiter.api.AfterEach;
12+
import org.junit.jupiter.api.BeforeEach;
13+
import org.junit.jupiter.api.Test;
14+
15+
/**
16+
* Replacement for MirrorNodeUrlBuilderTest validating base URL generation via Client/MirrorNode.
17+
*/
18+
public class ClientMirrorBaseUrlTest {
19+
20+
private ExecutorService executor;
21+
22+
@BeforeEach
23+
void setUp() {
24+
executor = Executors.newSingleThreadExecutor();
25+
}
26+
27+
@AfterEach
28+
void tearDown() {
29+
if (executor != null) {
30+
executor.shutdown();
31+
}
32+
}
33+
34+
@Test
35+
void hostPort_customPort_preserved_https_whenLedgerSet() {
36+
var network = Network.forNetwork(executor, new HashMap<>());
37+
var mirrorNetwork = MirrorNetwork.forNetwork(executor, List.of("mirror.example.com:8080"));
38+
var client = new Client(executor, network, mirrorNetwork, null, true, null, 0, 0);
39+
client.setLedgerId(LedgerId.TESTNET);
40+
41+
String base = client.getMirrorRestBaseUrl();
42+
assertThat(base).isEqualTo("https://mirror.example.com:8080/api/v1");
43+
}
44+
45+
@Test
46+
void hostPort_defaultHttpsPort_omitted() {
47+
var network = Network.forNetwork(executor, new HashMap<>());
48+
var mirrorNetwork = MirrorNetwork.forNetwork(executor, List.of("mirror.example.com:443"));
49+
var client = new Client(executor, network, mirrorNetwork, null, true, null, 0, 0);
50+
client.setLedgerId(LedgerId.TESTNET);
51+
52+
String base = client.getMirrorRestBaseUrl();
53+
assertThat(base).isEqualTo("https://mirror.example.com/api/v1");
54+
}
55+
56+
@Test
57+
void localNetwork_regularQuery_uses5551_http() {
58+
var network = Network.forNetwork(executor, new HashMap<>());
59+
var mirrorNetwork = MirrorNetwork.forNetwork(executor, List.of("localhost:8080"));
60+
var client = new Client(executor, network, mirrorNetwork, null, true, null, 0, 0);
61+
// No ledger id -> local
62+
63+
String base = client.getMirrorRestBaseUrl();
64+
assertThat(base).isEqualTo("http://localhost:5551/api/v1");
65+
}
66+
67+
@Test
68+
void localNetwork_contractCall_uses5551_http() {
69+
var network = Network.forNetwork(executor, new HashMap<>());
70+
var mirrorNetwork = MirrorNetwork.forNetwork(executor, List.of("127.0.0.1:8080"));
71+
var client = new Client(executor, network, mirrorNetwork, null, true, null, 0, 0);
72+
// No ledger id -> local
73+
74+
String base = client.getMirrorRestBaseUrl();
75+
assertThat(base).isEqualTo("http://127.0.0.1:5551/api/v1");
76+
}
77+
78+
@Test
79+
void emptyMirrorNetwork_throws_whenAccessingBase() {
80+
var network = Network.forNetwork(executor, new HashMap<>());
81+
var mirrorNetwork = MirrorNetwork.forNetwork(executor, List.of());
82+
var client = new Client(executor, network, mirrorNetwork, null, true, null, 0, 0);
83+
84+
assertThatThrownBy(() -> client.getMirrorRestBaseUrl()).isInstanceOf(RuntimeException.class);
85+
}
86+
}

sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/MirrorNodeContractQueryIntegrationTest.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ class MirrorNodeContractQueryIntegrationTest {
3030
@Test
3131
@DisplayName("Can estimate and simulate transaction")
3232
void canSimulateTransaction() throws Exception {
33+
// Clear any system properties to ensure clean state
34+
System.clearProperty("hedera.mirror.contract.port");
3335
try (var testEnv = new IntegrationTestEnv(1)) {
3436
var response = new FileCreateTransaction()
3537
.setKeys(testEnv.operatorKey)
@@ -169,6 +171,8 @@ void failsWhenGasLimitIsLow() throws Exception {
169171
@Test
170172
@DisplayName("Fails when sender is not set")
171173
void failsWhenSenderIsNotSet() throws Exception {
174+
// Set system property to use port 5551 for contract calls in this test
175+
System.setProperty("hedera.mirror.contract.port", "5551");
172176
try (var testEnv = new IntegrationTestEnv(1)) {
173177
var response = new FileCreateTransaction()
174178
.setKeys(testEnv.operatorKey)
@@ -207,6 +211,9 @@ void failsWhenSenderIsNotSet() throws Exception {
207211
.execute(testEnv.client);
208212
})
209213
.withMessageContaining("Received non-200 response from Mirror Node");
214+
} finally {
215+
// Clear the system property after the test
216+
System.clearProperty("hedera.mirror.contract.port");
210217
}
211218
}
212219

0 commit comments

Comments
 (0)