Skip to content

Commit 4c63194

Browse files
authored
Merge pull request #1525 from marklogic/feature/cloud-reverse-proxy
Enhancements for connecting to ML Cloud
2 parents bc724e2 + ddcd6fc commit 4c63194

File tree

15 files changed

+313
-53
lines changed

15 files changed

+313
-53
lines changed

marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientBuilder.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,17 @@ public DatabaseClientBuilder withDigestAuth(String username, String password) {
143143
}
144144

145145
public DatabaseClientBuilder withMarkLogicCloudAuth(String apiKey, String basePath) {
146-
return withSecurityContextType(SECURITY_CONTEXT_TYPE_MARKLOGIC_CLOUD)
146+
withSecurityContextType(SECURITY_CONTEXT_TYPE_MARKLOGIC_CLOUD)
147147
.withCloudApiKey(apiKey)
148148
.withBasePath(basePath);
149+
150+
// Assume sensible defaults for establishing an SSL connection. In the scenario where the user's JVM's
151+
// truststore has a certificate matching that of the MarkLogic Cloud instance, this saves the user from having
152+
// to configure anything except the API key and base path.
153+
if (null == props.get(PREFIX + "sslProtocol") && null == props.get(PREFIX + "sslContext")) {
154+
withSSLProtocol("default");
155+
}
156+
return this;
149157
}
150158

151159
public DatabaseClientBuilder withKerberosAuth(String principal) {

marklogic-client-api/src/main/java/com/marklogic/client/impl/DatabaseClientPropertySource.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import com.marklogic.client.DatabaseClient;
1919
import com.marklogic.client.DatabaseClientBuilder;
2020
import com.marklogic.client.DatabaseClientFactory;
21+
import org.slf4j.Logger;
22+
import org.slf4j.LoggerFactory;
2123

2224
import javax.net.ssl.SSLContext;
2325
import javax.net.ssl.X509TrustManager;
@@ -37,7 +39,8 @@
3739
*/
3840
public class DatabaseClientPropertySource {
3941

40-
private final static String PREFIX = DatabaseClientBuilder.PREFIX;
42+
private static final Logger logger = LoggerFactory.getLogger(DatabaseClientPropertySource.class);
43+
private static final String PREFIX = DatabaseClientBuilder.PREFIX;
4144

4245
private final Function<String, Object> propertySource;
4346

@@ -226,6 +229,18 @@ private X509TrustManager determineTrustManager() {
226229
throw new IllegalArgumentException(
227230
String.format("Trust manager must be an instance of %s", X509TrustManager.class.getName()));
228231
}
232+
// If the user chooses the "default" SSLContext, then it's already been initialized - but OkHttp still
233+
// needs a separate X509TrustManager, so use the JVM's default trust manager. The assumption is that the
234+
// default SSLContext was initialized with the JVM's default trust manager. A user can of course always override
235+
// this by simply providing their own trust manager.
236+
if ("default".equalsIgnoreCase((String) propertySource.apply(PREFIX + "sslProtocol"))) {
237+
X509TrustManager defaultTrustManager = SSLUtil.getDefaultTrustManager();
238+
if (logger.isDebugEnabled() && defaultTrustManager != null && defaultTrustManager.getAcceptedIssuers() != null) {
239+
logger.debug("Count of accepted issuers in default trust manager: {}",
240+
defaultTrustManager.getAcceptedIssuers().length);
241+
}
242+
return defaultTrustManager;
243+
}
229244
return null;
230245
}
231246

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.marklogic.client.impl;
2+
3+
import javax.net.ssl.TrustManager;
4+
import javax.net.ssl.TrustManagerFactory;
5+
import javax.net.ssl.X509TrustManager;
6+
import java.security.KeyStore;
7+
import java.security.KeyStoreException;
8+
import java.security.NoSuchAlgorithmException;
9+
10+
public interface SSLUtil {
11+
12+
static X509TrustManager getDefaultTrustManager() {
13+
return (X509TrustManager) getDefaultTrustManagers()[0];
14+
}
15+
16+
/**
17+
* @return a non-empty array of TrustManager instances based on the JVM's default trust manager algorithm, with the
18+
* first trust manager guaranteed to be an instance of X509TrustManager.
19+
*/
20+
static TrustManager[] getDefaultTrustManagers() {
21+
final String defaultAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
22+
TrustManagerFactory trustManagerFactory;
23+
try {
24+
trustManagerFactory = TrustManagerFactory.getInstance(defaultAlgorithm);
25+
} catch (NoSuchAlgorithmException e) {
26+
throw new RuntimeException("Unable to obtain trust manager factory using JVM's default trust manager algorithm: " + defaultAlgorithm, e);
27+
}
28+
29+
try {
30+
trustManagerFactory.init((KeyStore) null);
31+
} catch (KeyStoreException e) {
32+
throw new RuntimeException("Unable to initialize trust manager factory obtained using JVM's default trust manager algorithm: " + defaultAlgorithm
33+
+ "; cause: " + e.getMessage(), e);
34+
}
35+
36+
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
37+
if (trustManagers == null || trustManagers.length == 0) {
38+
throw new RuntimeException("No trust managers found using the JVM's default trust manager algorithm: " + defaultAlgorithm);
39+
}
40+
if (!(trustManagers[0] instanceof X509TrustManager)) {
41+
throw new RuntimeException("Default trust manager is not an X509TrustManager: " + trustManagers[0]);
42+
}
43+
return trustManagers;
44+
}
45+
}

marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/MarkLogicCloudAuthenticationConfigurer.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ private Response callTokenEndpoint(MarkLogicCloudAuthContext securityContext) {
6161
// Current assumption is that the SSL config provided for connecting to MarkLogic should also be applicable
6262
// for connecting to MarkLogic Cloud's "/token" endpoint.
6363
OkHttpUtil.configureSocketFactory(clientBuilder, securityContext.getSSLContext(), securityContext.getTrustManager());
64+
OkHttpUtil.configureHostnameVerifier(clientBuilder, securityContext.getSSLHostnameVerifier());
6465

6566
if (logger.isInfoEnabled()) {
6667
logger.info("Calling token endpoint at: " + tokenUrl);

marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/OkHttpUtil.java

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.marklogic.client.DatabaseClientFactory;
44
import com.marklogic.client.impl.HTTPKerberosAuthInterceptor;
55
import com.marklogic.client.impl.HTTPSamlAuthInterceptor;
6+
import com.marklogic.client.impl.SSLUtil;
67
import okhttp3.ConnectionPool;
78
import okhttp3.CookieJar;
89
import okhttp3.Dns;
@@ -13,15 +14,11 @@
1314
import javax.net.ssl.HostnameVerifier;
1415
import javax.net.ssl.SSLContext;
1516
import javax.net.ssl.TrustManager;
16-
import javax.net.ssl.TrustManagerFactory;
1717
import javax.net.ssl.X509TrustManager;
1818
import java.net.Inet4Address;
1919
import java.net.InetAddress;
2020
import java.net.UnknownHostException;
2121
import java.security.KeyManagementException;
22-
import java.security.KeyStore;
23-
import java.security.KeyStoreException;
24-
import java.security.NoSuchAlgorithmException;
2522
import java.util.ArrayList;
2623
import java.util.List;
2724
import java.util.Map;
@@ -123,7 +120,7 @@ private static void configureSAMLAuth(DatabaseClientFactory.SAMLAuthContext saml
123120
* @param clientBuilder
124121
* @param sslVerifier
125122
*/
126-
private static void configureHostnameVerifier(OkHttpClient.Builder clientBuilder, DatabaseClientFactory.SSLHostnameVerifier sslVerifier) {
123+
static void configureHostnameVerifier(OkHttpClient.Builder clientBuilder, DatabaseClientFactory.SSLHostnameVerifier sslVerifier) {
127124
HostnameVerifier hostnameVerifier = null;
128125
if (DatabaseClientFactory.SSLHostnameVerifier.ANY.equals(sslVerifier)) {
129126
hostnameVerifier = (hostname, session) -> true;
@@ -169,25 +166,16 @@ static void configureSocketFactory(OkHttpClient.Builder clientBuilder, SSLContex
169166
* @param sslContext
170167
*/
171168
private static void initializeSslContext(OkHttpClient.Builder clientBuilder, SSLContext sslContext) {
169+
TrustManager[] trustManagers = SSLUtil.getDefaultTrustManagers();
172170
try {
173-
TrustManagerFactory trustMgrFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
174-
trustMgrFactory.init((KeyStore) null);
175-
TrustManager[] trustMgrs = trustMgrFactory.getTrustManagers();
176-
if (trustMgrs == null || trustMgrs.length == 0) {
177-
throw new IllegalArgumentException("no trust manager and could not get default trust manager");
178-
}
179-
if (!(trustMgrs[0] instanceof X509TrustManager)) {
180-
throw new IllegalArgumentException("no trust manager and default is not an X509TrustManager");
181-
}
182-
sslContext.init(null, trustMgrs, null);
183-
clientBuilder.sslSocketFactory(new SSLSocketFactoryDelegator(sslContext.getSocketFactory()), (X509TrustManager) trustMgrs[0]);
184-
} catch (KeyStoreException e) {
185-
throw new IllegalArgumentException("no trust manager and cannot initialize factory for default", e);
186-
} catch (NoSuchAlgorithmException e) {
187-
throw new IllegalArgumentException("no trust manager and no algorithm for default manager", e);
171+
// In a future release, we may want to check if getSocketFactory() works already, implying that the
172+
// SSLContext has already been initialized. However, if that's the case, then it's not guaranteed that
173+
// the default trust manager is the appropriate one to pass to OkHttp.
174+
sslContext.init(null, trustManagers, null);
188175
} catch (KeyManagementException e) {
189-
throw new IllegalArgumentException("no trust manager and cannot initialize context with default", e);
176+
throw new RuntimeException("Unable to initialize SSLContext; cause: " + e.getMessage(), e);
190177
}
178+
clientBuilder.sslSocketFactory(new SSLSocketFactoryDelegator(sslContext.getSocketFactory()), (X509TrustManager) trustManagers[0]);
191179
}
192180

193181
static class DnsImpl implements Dns {

marklogic-client-api/src/test/java/com/marklogic/client/test/DatabaseClientBuilderTest.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,15 @@ void cloudWithBasePath() {
107107
(DatabaseClientFactory.MarkLogicCloudAuthContext) bean.getSecurityContext();
108108
assertEquals("my-key", context.getKey());
109109
assertEquals("/my/path", bean.getBasePath());
110+
111+
assertNotNull(context.getSSLContext(), "If no sslProtocol or sslContext is set, the JVM's default SSL " +
112+
"context should be used");
113+
114+
assertNotNull(context.getSSLContext().getSocketFactory(), "Since the JVM's default SSL context is expected " +
115+
"to be used, it should already be initialized, and thus able to return a socket factory");
116+
117+
assertNotNull(context.getTrustManager(), "Since the JVM's default SSL context is used, the JVM's default " +
118+
"trust manager should be used as well if the user doesn't provide their own");
110119
}
111120

112121
@Test
@@ -174,6 +183,27 @@ void sslProtocol() {
174183
"initialize the SSLContext before using it by using the JVM's default trust manager.");
175184
}
176185

186+
@Test
187+
void defaultSslProtocolAndNoTrustManager() {
188+
bean = Common.newClientBuilder()
189+
.withSSLProtocol("default")
190+
.buildBean();
191+
192+
DatabaseClientFactory.SecurityContext context = bean.getSecurityContext();
193+
assertNotNull(context);
194+
195+
SSLContext sslContext = context.getSSLContext();
196+
assertNotNull(sslContext);
197+
assertNotNull(sslContext.getSocketFactory(), "A protocol of 'default' should result in the JVM's default " +
198+
"SSLContext being used, which is expected to have been initialized already and can thus return a socket " +
199+
"factory");
200+
201+
assertNotNull(context.getTrustManager(), "If the user specifies a protocol of 'default' but does not " +
202+
"provide a trust manager, the assumption is that the JVM's default trust manager should be used, thus " +
203+
"saving the user from having to do the work of providing this themselves.");
204+
}
205+
206+
177207
@Test
178208
void invalidSslProtocol() {
179209
RuntimeException ex = assertThrows(RuntimeException.class, () -> Common.newClientBuilder()

marklogic-client-api/src/test/java/com/marklogic/client/test/MarkLogicCloudAuthenticationDebugger.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
import com.marklogic.client.DatabaseClientFactory;
66
import com.marklogic.client.io.JacksonHandle;
77

8-
import javax.net.ssl.SSLContext;
9-
import javax.net.ssl.TrustManager;
10-
import javax.net.ssl.X509TrustManager;
11-
128
/**
139
* We don't yet have a way to run tests against a MarkLogic Cloud instance. In the meantime, this program and its
1410
* related Gradle task can be used for easy manual testing.
11+
*
12+
* For local testing against the ReverseProxyServer in the test-app project, which emulates MarkLogic Cloud, use
13+
* "localhost" as the cloud host, "username:password" (often "admin:the admin password") as the apiKey, and
14+
* "local/manage" as the basePath.
1515
*/
1616
public class MarkLogicCloudAuthenticationDebugger {
1717

@@ -20,11 +20,12 @@ public static void main(String[] args) throws Exception {
2020
String apiKey = args[1];
2121
String basePath = args[2];
2222

23+
// Expected to default to the JVM's default SSL context and default trust manager
2324
DatabaseClient client = new DatabaseClientBuilder()
2425
.withHost(cloudHost)
2526
.withMarkLogicCloudAuth(apiKey, basePath)
26-
.withSSLContext(SSLContext.getDefault())
27-
.withTrustManager(Common.TRUST_ALL_MANAGER)
27+
// Have to use "ANY", as the default is "COMMON", which won't work for our selfsigned cert
28+
.withSSLHostnameVerifier(DatabaseClientFactory.SSLHostnameVerifier.ANY)
2829
.build();
2930

3031
DatabaseClient.ConnectionResult result = client.checkConnection();

test-app/build.gradle

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ dependencies {
88
implementation "io.undertow:undertow-core:2.2.21.Final"
99
implementation "io.undertow:undertow-servlet:2.2.21.Final"
1010
implementation "com.marklogic:ml-javaclient-util:4.4.0"
11+
implementation 'org.slf4j:slf4j-api:1.7.36'
12+
implementation 'ch.qos.logback:logback-classic:1.3.5'
13+
implementation "com.fasterxml.jackson.core:jackson-databind:2.14.1"
14+
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
1115
}
1216

1317
// See https://github.com/psxpaul/gradle-execfork-plugin for docs.
@@ -22,3 +26,13 @@ task runReverseProxyServer(type: com.github.psxpaul.task.JavaExecFork, dependsOn
2226
standardOutput = file("$buildDir/reverse-proxy.log")
2327
errorOutput = file("$buildDir/reverse-proxy-error.log")
2428
}
29+
30+
task runBlockingReverseProxyServer(type: JavaExec) {
31+
description = "Run the reverse proxy server so that it blocks and waits for requests; use ctrl-C to stop it. " +
32+
"This is intended for manual testing with the reverse proxy server. If you wish to enable an HTTPS port that is 443" +
33+
"or any value less than 1024, you will need to use sudo to run this - e.g. " +
34+
"sudo ./gradlew runBlockingReverseProxyServer -PrpsHttpsPort=443 ."
35+
classpath = sourceSets.main.runtimeClasspath
36+
main = "com.marklogic.client.test.ReverseProxyServer"
37+
args = [rpsMarkLogicServer, rpsProxyServer, rpsHttpPort, rpsHttpsPort]
38+
}

test-app/gradle.properties

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,8 @@ mlRestPort=8012
66
mlContentDatabaseName=java-unittest
77
mlTdeValidationEnabled=false
88
mlForestsPerHost=java-functest,2,java-unittest,3
9+
10+
rpsMarkLogicServer=localhost
11+
rpsProxyServer=localhost
12+
rpsHttpPort=8020
13+
rpsHttpsPort=0

0 commit comments

Comments
 (0)