Skip to content

Commit 90a04e7

Browse files
Enables EndpointValidation (Azure#47111)
* Enables EndpointValidation * Update Configs.java * Fixing test failures * Update TestSuiteBase.java * Create InvalidHostnameTest.java * Update InvalidHostnameTest.java * Updating changelog * Updated changelogs * Adding tests for direct mode * Fixing test flakiness due to Client leak * Fixed changelog * Iterating on test fixes * Adding more memory related logs * Iterating on test fixes * Revert unnecessary changes * Update TestSuiteBase.java * Update TestSuiteBase.java * Update TestSuiteBase.java * Update log4j2-test.properties * Update RxDocumentClientImpl.java * Update InvalidHostnameTest.java * Update CosmosDiagnosticsTest.java
1 parent 44d7ea8 commit 90a04e7

File tree

16 files changed

+383
-9
lines changed

16 files changed

+383
-9
lines changed

sdk/cosmos/azure-cosmos-encryption/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#### Bugs Fixed
1010

1111
#### Other Changes
12+
* Enabled hostname validation for RNTBD connections to backend - [PR 47111](https://github.com/Azure/azure-sdk-for-java/pull/47111)
1213

1314
### 2.24.0 (2025-10-21)
1415
#### Other Changes

sdk/cosmos/azure-cosmos-kafka-connect/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#### Bugs Fixed
1010

1111
#### Other Changes
12+
* Enabled hostname validation for RNTBD connections to backend - [PR 47111](https://github.com/Azure/azure-sdk-for-java/pull/47111)
1213

1314
### 2.6.1 (2025-11-18)
1415

sdk/cosmos/azure-cosmos-spark_3-3_2-12/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#### Bugs Fixed
1010

1111
#### Other Changes
12+
* Enabled hostname validation for RNTBD connections to backend - [PR 47111](https://github.com/Azure/azure-sdk-for-java/pull/47111)
1213

1314
### 4.41.0 (2025-10-21)
1415

sdk/cosmos/azure-cosmos-spark_3-4_2-12/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#### Bugs Fixed
1010

1111
#### Other Changes
12+
* Enabled hostname validation for RNTBD connections to backend - [PR 47111](https://github.com/Azure/azure-sdk-for-java/pull/47111)
1213

1314
### 4.41.0 (2025-10-21)
1415

sdk/cosmos/azure-cosmos-spark_3-5_2-12/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#### Bugs Fixed
1010

1111
#### Other Changes
12+
* Enabled hostname validation for RNTBD connections to backend - [PR 47111](https://github.com/Azure/azure-sdk-for-java/pull/47111)
1213

1314
### 4.41.0 (2025-10-21)
1415

sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/CosmosDiagnosticsTest.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -569,8 +569,16 @@ public void databaseAccountToClients() {
569569
String diagnostics = createResponse.getDiagnostics().toString();
570570

571571
// assert diagnostics shows the correct format for tracking client instances
572-
assertThat(diagnostics).contains(String.format("\"clientEndpoints\"" +
573-
":{\"%s\"", TestConfigurations.HOST));
572+
String prefix = "\"clientEndpoints\":{";
573+
int startIndex = diagnostics.indexOf(prefix);
574+
assertThat(startIndex).isGreaterThanOrEqualTo(0);
575+
startIndex += prefix.length();
576+
int endIndex = diagnostics.indexOf("}", startIndex);
577+
assertThat(endIndex).isGreaterThan(startIndex);
578+
int matchingIndex = diagnostics.indexOf(TestConfigurations.HOST, startIndex);
579+
assertThat(matchingIndex).isGreaterThanOrEqualTo(startIndex);
580+
assertThat(matchingIndex).isLessThanOrEqualTo(endIndex);
581+
574582
// track number of clients currently mapped to account
575583
int clientsIndex = diagnostics.indexOf("\"clientEndpoints\":");
576584
// we do end at +120 to ensure we grab the bracket even if the account is very long or if
@@ -592,8 +600,14 @@ public void databaseAccountToClients() {
592600
createResponse = cosmosContainer.createItem(internalObjectNode);
593601
diagnostics = createResponse.getDiagnostics().toString();
594602
// assert diagnostics shows the correct format for tracking client instances
595-
assertThat(diagnostics).contains(String.format("\"clientEndpoints\"" +
596-
":{\"%s\"", TestConfigurations.HOST));
603+
startIndex = diagnostics.indexOf(prefix);
604+
assertThat(startIndex).isGreaterThanOrEqualTo(0);
605+
startIndex += prefix.length();
606+
endIndex = diagnostics.indexOf("}", startIndex);
607+
assertThat(endIndex).isGreaterThan(startIndex);
608+
matchingIndex = diagnostics.indexOf(TestConfigurations.HOST, startIndex);
609+
assertThat(matchingIndex).isGreaterThanOrEqualTo(startIndex);
610+
assertThat(matchingIndex).isLessThanOrEqualTo(endIndex);
597611
// grab new value and assert one additional client is mapped to the same account used previously
598612
clientsIndex = diagnostics.indexOf("\"clientEndpoints\":");
599613
substrings = diagnostics.substring(clientsIndex, clientsIndex + 120)
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
/*
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License.
4+
*
5+
*/
6+
package com.azure.cosmos;
7+
8+
import com.azure.cosmos.implementation.Configs;
9+
import com.azure.cosmos.implementation.GlobalEndpointManager;
10+
import com.azure.cosmos.implementation.GoneException;
11+
import com.azure.cosmos.implementation.HttpConstants;
12+
import com.azure.cosmos.implementation.InternalServerErrorException;
13+
import com.azure.cosmos.implementation.RxDocumentServiceRequest;
14+
import com.azure.cosmos.implementation.Utils;
15+
import com.azure.cosmos.implementation.directconnectivity.ReflectionUtils;
16+
import com.azure.cosmos.implementation.directconnectivity.StoreResponse;
17+
import com.azure.cosmos.implementation.directconnectivity.TransportClient;
18+
import com.azure.cosmos.implementation.directconnectivity.Uri;
19+
import com.azure.cosmos.implementation.directconnectivity.rntbd.ProactiveOpenConnectionsProcessor;
20+
import com.azure.cosmos.implementation.faultinjection.IFaultInjectorProvider;
21+
import com.azure.cosmos.models.CosmosContainerIdentity;
22+
import com.azure.cosmos.models.ThroughputProperties;
23+
import com.azure.cosmos.rx.TestSuiteBase;
24+
import com.fasterxml.jackson.databind.node.ObjectNode;
25+
import io.netty.buffer.ByteBuf;
26+
import io.netty.buffer.ByteBufAllocator;
27+
import org.testng.SkipException;
28+
import org.testng.annotations.Factory;
29+
import org.testng.annotations.Test;
30+
import reactor.core.publisher.Mono;
31+
32+
import javax.net.ssl.SSLHandshakeException;
33+
import java.net.InetAddress;
34+
import java.net.URI;
35+
import java.net.URISyntaxException;
36+
import java.net.UnknownHostException;
37+
import java.util.List;
38+
import java.util.UUID;
39+
40+
import static org.assertj.core.api.Assertions.fail;
41+
import static org.assertj.core.api.Assertions.assertThat;
42+
43+
public class InvalidHostnameTest extends TestSuiteBase {
44+
@Factory(dataProvider = "clientBuildersWithSessionConsistency")
45+
public InvalidHostnameTest(CosmosClientBuilder clientBuilder) {
46+
super(clientBuilder);
47+
}
48+
49+
@Test(groups = { "fast", "fi-multi-master", "multi-region" }, timeOut = TIMEOUT)
50+
public void gatewayConnectionFailsWhenHostnameIsInvalid() throws Exception {
51+
gatewayConnectionFailsWhenHostnameIsInvalidCore(null);
52+
gatewayConnectionFailsWhenHostnameIsInvalidCore(false);
53+
}
54+
55+
@Test(groups = { "fast", "fi-multi-master", "multi-region" }, timeOut = TIMEOUT)
56+
public void gatewayConnectionFailsWhenHostnameIsInvalidEvenWhenHostnameValidationIsDisabled() throws Exception {
57+
gatewayConnectionFailsWhenHostnameIsInvalidCore(true);
58+
}
59+
60+
@Test(groups = { "fast", "fi-multi-master", "multi-region" }, timeOut = TIMEOUT)
61+
public void directConnectionSucceedsWhenHostnameIsInvalidAndHostnameValidationIsDisabled() throws Exception {
62+
directConnectionTestCore(true);
63+
}
64+
65+
@Test(groups = { "fast", "fi-multi-master", "multi-region" }, timeOut = TIMEOUT)
66+
public void directConnectionFailsWhenHostnameIsInvalidAndHostnameValidationIsNotSet() throws Exception {
67+
directConnectionFailsWhenHostnameIsInvalidCore(null);
68+
}
69+
70+
@Test(groups = { "fast", "fi-multi-master", "multi-region" }, timeOut = TIMEOUT)
71+
public void directConnectionFailsWhenHostnameIsInvalidAndHostnameValidationIsEnabled() throws Exception {
72+
directConnectionFailsWhenHostnameIsInvalidCore(false);
73+
}
74+
75+
private void directConnectionFailsWhenHostnameIsInvalidCore(Boolean disableHostnameValidation) throws Exception {
76+
try {
77+
directConnectionTestCore(disableHostnameValidation);
78+
fail("The test should have failed with invalid hostname when hostname "
79+
+ "validation is enabled or not set.");
80+
} catch (InternalServerErrorException cosmosException) {
81+
assertThat(cosmosException.getStatusCode()).isEqualTo(500);
82+
assertThat(cosmosException.getSubStatusCode())
83+
.isEqualTo(HttpConstants.SubStatusCodes.INVALID_RESULT);
84+
assertThat(cosmosException).hasCauseInstanceOf(RuntimeException.class);
85+
RuntimeException runtimeException = (RuntimeException)cosmosException.getCause();
86+
assertThat(runtimeException).hasCauseInstanceOf(GoneException.class);
87+
GoneException goneException = (GoneException)runtimeException.getCause();
88+
assertThat(goneException).hasCauseInstanceOf(SSLHandshakeException.class);
89+
logger.info("Expected exception was thrown", cosmosException);
90+
}
91+
}
92+
93+
private void directConnectionTestCore(Boolean disableHostnameValidation) throws Exception {
94+
CosmosDatabase createdDatabase = null;
95+
CosmosClient client = null;
96+
CosmosClientBuilder builder = getClientBuilder();
97+
98+
if (builder.getEndpoint().contains("localhost")) {
99+
throw new SkipException("This test is irrelevant for emulator");
100+
}
101+
102+
if (builder.getConnectionPolicy().getConnectionMode() != ConnectionMode.DIRECT) {
103+
throw new SkipException("This test is only relevant for direct mode");
104+
}
105+
106+
try {
107+
if (disableHostnameValidation != null) {
108+
System.setProperty("COSMOS.HOSTNAME_VALIDATION_DISABLED", disableHostnameValidation.toString());
109+
} else {
110+
System.clearProperty("COSMOS.HOSTNAME_VALIDATION_DISABLED");
111+
}
112+
Configs.resetIsHostnameValidationDisabledForTests();
113+
114+
client = builder.buildClient();
115+
116+
TransportClient originalTransportClient = ReflectionUtils.getTransportClient(client);
117+
118+
ReflectionUtils.setTransportClient(
119+
client,
120+
new HostnameInvalidationTransportClient(originalTransportClient));
121+
122+
String dbName = CosmosDatabaseForTest.generateId();
123+
createdDatabase = createSyncDatabase(client, dbName);
124+
createdDatabase.createContainer(
125+
"TestContainer",
126+
"/id",
127+
ThroughputProperties.createManualThroughput(400));
128+
CosmosContainer createdContainer = client.getDatabase(dbName).getContainer("TestContainer");
129+
ObjectNode newObject = Utils.getSimpleObjectMapper().createObjectNode();
130+
newObject.put("id", UUID.randomUUID().toString());
131+
createdContainer.upsertItem(newObject);
132+
}
133+
finally {
134+
if (createdDatabase != null) {
135+
safeDeleteSyncDatabase(createdDatabase);
136+
}
137+
138+
if (client != null) {
139+
safeCloseSyncClient(client);
140+
}
141+
142+
System.clearProperty("COSMOS.HOSTNAME_VALIDATION_DISABLED");
143+
Configs.resetIsHostnameValidationDisabledForTests();
144+
}
145+
}
146+
147+
private void gatewayConnectionFailsWhenHostnameIsInvalidCore(Boolean disableHostnameValidation) throws Exception {
148+
CosmosDatabase createdDatabase = null;
149+
CosmosClient client = null;
150+
CosmosClientBuilder builder = getClientBuilder();
151+
152+
if (builder.getEndpoint().contains("localhost")) {
153+
throw new SkipException("This test is irrelevant for emulator");
154+
}
155+
156+
try {
157+
if (disableHostnameValidation != null) {
158+
System.setProperty("COSMOS.HOSTNAME_VALIDATION_DISABLED", disableHostnameValidation.toString());
159+
} else {
160+
System.clearProperty("COSMOS.HOSTNAME_VALIDATION_DISABLED");
161+
}
162+
Configs.resetIsHostnameValidationDisabledForTests();
163+
164+
URI uri = URI.create(builder.getEndpoint());
165+
InetAddress address = InetAddress.getByName(uri.getHost());
166+
URI uriWithInvalidHostname = new URI(
167+
uri.getScheme(),
168+
uri.getUserInfo(),
169+
address.getHostAddress(), // Use the DNS-resolved IP-address as new hostname - this is invalid form TLS cert perspective
170+
uri.getPort(),
171+
uri.getPath(),
172+
uri.getQuery(),
173+
uri.getFragment()
174+
);
175+
builder.endpoint(uriWithInvalidHostname.toString());
176+
client = builder.buildClient();
177+
String dbName = CosmosDatabaseForTest.generateId();
178+
createdDatabase = createSyncDatabase(client, dbName);
179+
fail("The attempt to connect to the Gateway endpoint to create a database "
180+
+ "should have failed due to invalid hostname.");
181+
} catch (RuntimeException e) {
182+
assertThat(e).hasCauseInstanceOf(CosmosException.class);
183+
CosmosException cosmosException = (CosmosException)e.getCause();
184+
assertThat(cosmosException.getStatusCode()).isEqualTo(503);
185+
assertThat(cosmosException.getSubStatusCode())
186+
.isEqualTo(HttpConstants.SubStatusCodes.GATEWAY_ENDPOINT_UNAVAILABLE);
187+
assertThat(cosmosException).hasCauseInstanceOf(SSLHandshakeException.class);
188+
logger.info("Expected exception was thrown", cosmosException);
189+
}
190+
finally {
191+
if (createdDatabase != null) {
192+
safeDeleteSyncDatabase(createdDatabase);
193+
}
194+
195+
if (client != null) {
196+
safeCloseSyncClient(client);
197+
}
198+
199+
System.clearProperty("COSMOS.HOSTNAME_VALIDATION_DISABLED");
200+
Configs.resetIsHostnameValidationDisabledForTests();
201+
}
202+
}
203+
204+
private static class HostnameInvalidationTransportClient extends TransportClient {
205+
private final TransportClient inner;
206+
207+
public HostnameInvalidationTransportClient(TransportClient transportClient) {
208+
this.inner = transportClient;
209+
}
210+
211+
@Override
212+
public void close() throws Exception {
213+
this.inner.close();
214+
}
215+
216+
@Override
217+
public Mono<StoreResponse> invokeStoreAsync(Uri physicalAddress, RxDocumentServiceRequest request) {
218+
URI uri = physicalAddress.getURI();
219+
InetAddress address = null;
220+
try {
221+
address = InetAddress.getByName(uri.getHost());
222+
} catch (UnknownHostException e) {
223+
throw new RuntimeException(e);
224+
}
225+
String uriWithInvalidHostname;
226+
try {
227+
uriWithInvalidHostname = new URI(
228+
uri.getScheme(),
229+
uri.getUserInfo(),
230+
address.getHostAddress(), // Use the DNS-resolved IP-address as new hostname - this is invalid form TLS cert perspective
231+
uri.getPort(),
232+
uri.getPath(),
233+
uri.getQuery(),
234+
uri.getFragment()
235+
).toString();
236+
} catch (URISyntaxException e) {
237+
throw new RuntimeException(e);
238+
}
239+
240+
Uri ipBasedAddress = Uri.create(uriWithInvalidHostname);
241+
242+
logger.info("Changed physical address '{}' into '{}'.", physicalAddress, ipBasedAddress);
243+
244+
return this
245+
.inner
246+
.invokeStoreAsync(ipBasedAddress, request)
247+
.onErrorMap(t -> new RuntimeException(t));
248+
}
249+
250+
@Override
251+
public void configureFaultInjectorProvider(IFaultInjectorProvider injectorProvider) {
252+
this.inner.configureFaultInjectorProvider(injectorProvider);
253+
}
254+
255+
@Override
256+
public GlobalEndpointManager getGlobalEndpointManager() {
257+
return this.inner.getGlobalEndpointManager();
258+
}
259+
260+
@Override
261+
public ProactiveOpenConnectionsProcessor getProactiveOpenConnectionsProcessor() {
262+
return this.inner.getProactiveOpenConnectionsProcessor();
263+
}
264+
265+
@Override
266+
public void recordOpenConnectionsAndInitCachesCompleted(List<CosmosContainerIdentity> cosmosContainerIdentities) {
267+
this.inner.recordOpenConnectionsAndInitCachesCompleted(cosmosContainerIdentities);
268+
}
269+
270+
@Override
271+
public void recordOpenConnectionsAndInitCachesStarted(List<CosmosContainerIdentity> cosmosContainerIdentities) {
272+
this.inner.recordOpenConnectionsAndInitCachesStarted(cosmosContainerIdentities);
273+
}
274+
}
275+
}

sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/directconnectivity/RntbdTransportClientTest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1138,6 +1138,11 @@ public URI serviceEndpoint() {
11381138
return null;
11391139
}
11401140

1141+
@Override
1142+
public URI serverKeyUsedAsActualRemoteAddress() {
1143+
return this.remoteURI;
1144+
}
1145+
11411146
@Override
11421147
public void injectConnectionErrors(String ruleId, double threshold, Class<?> eventType) {
11431148
throw new NotImplementedException("injectConnectionErrors is not supported in FakeEndpoint");

sdk/cosmos/azure-cosmos/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* Fixed a possible memory leak (Netty buffers) in Gateway mode caused by a race condition when timeouts are happening. - [47228](https://github.com/Azure/azure-sdk-for-java/pull/47228) and [47251](https://github.com/Azure/azure-sdk-for-java/pull/47251)
1111

1212
#### Other Changes
13+
* Enabled hostname validation for RNTBD connections to backend - [PR 47111](https://github.com/Azure/azure-sdk-for-java/pull/47111)
1314
* Changed to use incremental change feed to get partition key ranges. - [46810](https://github.com/Azure/azure-sdk-for-java/pull/46810)
1415

1516
### 4.75.0 (2025-10-21)

0 commit comments

Comments
 (0)