From ca41df1d35be0502d552c0cb621bacf882e0ca56 Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Thu, 19 Jan 2023 12:43:45 -0500 Subject: [PATCH] DEVEXP-169 Extracted method for building an OkHttpClient This is intended for ml-app-deployer and is in the "extra" package, which is pseudo-public - it's public, but with a caveat that you'll need to bring your own OkHttp dependency to compile against it and it may not work in a future version. --- .../OkHttpClientBuilderFactory.java | 34 ++++++++ .../marklogic/client/impl/OkHttpServices.java | 81 +++---------------- .../client/impl/okhttp/OkHttpUtil.java | 78 +++++++++++++++++- .../OkHttpClientBuilderFactoryTest.java | 25 ++++++ 4 files changed, 142 insertions(+), 76 deletions(-) create mode 100644 marklogic-client-api/src/main/java/com/marklogic/client/extra/okhttpclient/OkHttpClientBuilderFactory.java create mode 100644 marklogic-client-api/src/test/java/com/marklogic/client/extra/okhttpclient/OkHttpClientBuilderFactoryTest.java diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/extra/okhttpclient/OkHttpClientBuilderFactory.java b/marklogic-client-api/src/main/java/com/marklogic/client/extra/okhttpclient/OkHttpClientBuilderFactory.java new file mode 100644 index 000000000..0c37fd1fa --- /dev/null +++ b/marklogic-client-api/src/main/java/com/marklogic/client/extra/okhttpclient/OkHttpClientBuilderFactory.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022 MarkLogic Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.marklogic.client.extra.okhttpclient; + +import com.marklogic.client.DatabaseClientFactory; +import com.marklogic.client.impl.okhttp.OkHttpUtil; +import okhttp3.OkHttpClient; + +/** + * Exposes the mechanism for constructing an {@code OkHttpClient.Builder} in the same fashion as when a + * {@code DatabaseClient} is constructed. Primarily intended for reuse in the ml-app-deployer library. If the + * Java Client moves to a different HTTP client library, this will no longer work. + * + * @since 6.1.0 + */ +public interface OkHttpClientBuilderFactory { + + static OkHttpClient.Builder newOkHttpClientBuilder(String host, int port, DatabaseClientFactory.SecurityContext securityContext) { + return OkHttpUtil.newOkHttpClientBuilder(host, port, securityContext); + } +} diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/OkHttpServices.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/OkHttpServices.java index cc590c533..65c8e0985 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/OkHttpServices.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/OkHttpServices.java @@ -188,67 +188,24 @@ private FailedRequest extractErrorFields(Response response) { @Override public void connect(String host, int port, String basePath, String database, SecurityContext securityContext){ - if (host == null) throw new IllegalArgumentException("No host provided"); if (securityContext == null) throw new IllegalArgumentException("No security context provided"); - OkHttpClient.Builder clientBuilder = OkHttpUtil.newClientBuilder(); - AuthenticationConfigurer authenticationConfigurer = null; - - // As of 6.1.0, kerberos/saml/certificate are still coded within this class to avoid potential breaks from - // refactoring. Once the tests for these auth methods are running properly, the code for each can be - // safely refactored. - if (securityContext instanceof BasicAuthContext) { - authenticationConfigurer = new BasicAuthenticationConfigurer(); - checkFirstRequest = false; - } else if (securityContext instanceof DigestAuthContext) { - authenticationConfigurer = new DigestAuthenticationConfigurer(); - checkFirstRequest = true; - } else if (securityContext instanceof KerberosAuthContext) { - configureKerberosAuth((KerberosAuthContext) securityContext, host, clientBuilder); - checkFirstRequest = false; - } else if (securityContext instanceof CertificateAuthContext) { - checkFirstRequest = false; - } else if (securityContext instanceof SAMLAuthContext) { - configureSAMLAuth((SAMLAuthContext) securityContext, clientBuilder); - checkFirstRequest = false; - } else if (securityContext instanceof MarkLogicCloudAuthContext) { - authenticationConfigurer = new MarkLogicCloudAuthenticationConfigurer(host, port); - checkFirstRequest = false; - } - else { - throw new IllegalArgumentException("Unsupported security context: " + securityContext.getClass()); - } + this.checkFirstRequest = securityContext instanceof DigestAuthContext; + this.database = database; + this.baseUri = HttpUrlBuilder.newBaseUrl(host, port, basePath, securityContext.getSSLContext()); - if (authenticationConfigurer != null) { - authenticationConfigurer.configureAuthentication(clientBuilder, securityContext); - } - - SSLContext sslContext = securityContext.getSSLContext(); - X509TrustManager trustManager = securityContext.getTrustManager(); + OkHttpClient.Builder clientBuilder = OkHttpUtil.newOkHttpClientBuilder(host, port, securityContext); - SSLHostnameVerifier sslVerifier = null; - if (sslContext != null || securityContext instanceof CertificateAuthContext) { - sslVerifier = securityContext.getSSLHostnameVerifier() != null ? - securityContext.getSSLHostnameVerifier() : - SSLHostnameVerifier.COMMON; + Properties props = System.getProperties(); + if (props.containsKey(OKHTTP_LOGGINGINTERCEPTOR_LEVEL)) { + configureOkHttpLogging(clientBuilder, props); } + this.configureDelayAndRetry(props); - OkHttpUtil.configureSocketFactory(clientBuilder, sslContext, trustManager); - OkHttpUtil.configureHostnameVerifier(clientBuilder, sslVerifier); - - this.database = database; - this.baseUri = HttpUrlBuilder.newBaseUrl(host, port, basePath, sslContext); - - Properties props = System.getProperties(); - if (props.containsKey(OKHTTP_LOGGINGINTERCEPTOR_LEVEL)) { - configureOkHttpLogging(clientBuilder, props); - } - this.configureDelayAndRetry(props); - - this.client = clientBuilder.build(); + this.client = clientBuilder.build(); } /** @@ -297,26 +254,6 @@ private void configureDelayAndRetry(Properties props) { } } - private void configureKerberosAuth(KerberosAuthContext keberosAuthContext, String host, OkHttpClient.Builder clientBuilder) { - Map kerberosOptions = keberosAuthContext.getKrbOptions(); - Interceptor interceptor = new HTTPKerberosAuthInterceptor(host, kerberosOptions); - clientBuilder.addInterceptor(interceptor); - } - - private void configureSAMLAuth(SAMLAuthContext samlAuthContext, OkHttpClient.Builder clientBuilder) { - Interceptor interceptor; - String authorizationTokenValue = samlAuthContext.getToken(); - if(authorizationTokenValue != null && authorizationTokenValue.length() > 0) { - interceptor = new HTTPSamlAuthInterceptor(authorizationTokenValue); - } else if(samlAuthContext.getAuthorizer()!=null) { - interceptor = new HTTPSamlAuthInterceptor(samlAuthContext.getAuthorizer()); - } else if(samlAuthContext.getRenewer()!=null) { - interceptor = new HTTPSamlAuthInterceptor(samlAuthContext.getAuthorization(),samlAuthContext.getRenewer()); - } else - throw new IllegalArgumentException("Either a call back or renewer expected."); - clientBuilder.addInterceptor(interceptor); - } - @Override public DatabaseClient getDatabaseClient() { return databaseClient; diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/OkHttpUtil.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/OkHttpUtil.java index f66c56a2f..d8ca95e6f 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/OkHttpUtil.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/OkHttpUtil.java @@ -1,13 +1,20 @@ package com.marklogic.client.impl.okhttp; import com.marklogic.client.DatabaseClientFactory; +import com.marklogic.client.impl.HTTPKerberosAuthInterceptor; +import com.marklogic.client.impl.HTTPSamlAuthInterceptor; import okhttp3.ConnectionPool; import okhttp3.CookieJar; import okhttp3.Dns; +import okhttp3.Interceptor; import okhttp3.OkHttpClient; import javax.net.SocketFactory; -import javax.net.ssl.*; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; import java.net.Inet4Address; import java.net.InetAddress; import java.net.UnknownHostException; @@ -17,6 +24,7 @@ import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; /** @@ -28,11 +36,53 @@ public abstract class OkHttpUtil { final private static ConnectionPool connectionPool = new ConnectionPool(); + public static OkHttpClient.Builder newOkHttpClientBuilder(String host, int port, DatabaseClientFactory.SecurityContext securityContext) { + OkHttpClient.Builder clientBuilder = OkHttpUtil.newClientBuilder(); + AuthenticationConfigurer authenticationConfigurer = null; + + // As of 6.1.0, kerberos/saml/certificate are still coded within this class to avoid potential breaks from + // refactoring. Once the tests for these auth methods are running properly, the code for each can be + // safely refactored. + if (securityContext instanceof DatabaseClientFactory.BasicAuthContext) { + authenticationConfigurer = new BasicAuthenticationConfigurer(); + } else if (securityContext instanceof DatabaseClientFactory.DigestAuthContext) { + authenticationConfigurer = new DigestAuthenticationConfigurer(); + } else if (securityContext instanceof DatabaseClientFactory.KerberosAuthContext) { + configureKerberosAuth((DatabaseClientFactory.KerberosAuthContext) securityContext, host, clientBuilder); + } else if (securityContext instanceof DatabaseClientFactory.CertificateAuthContext) { + } else if (securityContext instanceof DatabaseClientFactory.SAMLAuthContext) { + configureSAMLAuth((DatabaseClientFactory.SAMLAuthContext) securityContext, clientBuilder); + } else if (securityContext instanceof DatabaseClientFactory.MarkLogicCloudAuthContext) { + authenticationConfigurer = new MarkLogicCloudAuthenticationConfigurer(host, port); + } else { + throw new IllegalArgumentException("Unsupported security context: " + securityContext.getClass()); + } + + if (authenticationConfigurer != null) { + authenticationConfigurer.configureAuthentication(clientBuilder, securityContext); + } + + SSLContext sslContext = securityContext.getSSLContext(); + X509TrustManager trustManager = securityContext.getTrustManager(); + + DatabaseClientFactory.SSLHostnameVerifier sslVerifier = null; + if (sslContext != null || securityContext instanceof DatabaseClientFactory.CertificateAuthContext) { + sslVerifier = securityContext.getSSLHostnameVerifier() != null ? + securityContext.getSSLHostnameVerifier() : + DatabaseClientFactory.SSLHostnameVerifier.COMMON; + } + + OkHttpUtil.configureSocketFactory(clientBuilder, sslContext, trustManager); + OkHttpUtil.configureHostnameVerifier(clientBuilder, sslVerifier); + + return clientBuilder; + } + /** * @return an OkHttpClient.Builder initialized with a sensible set of defaults that can then have authentication * configured */ - public static OkHttpClient.Builder newClientBuilder() { + static OkHttpClient.Builder newClientBuilder() { return new OkHttpClient.Builder() .followRedirects(false) .followSslRedirects(false) @@ -47,13 +97,33 @@ public static OkHttpClient.Builder newClientBuilder() { .dns(new DnsImpl()); } + private static void configureKerberosAuth(DatabaseClientFactory.KerberosAuthContext keberosAuthContext, String host, OkHttpClient.Builder clientBuilder) { + Map kerberosOptions = keberosAuthContext.getKrbOptions(); + Interceptor interceptor = new HTTPKerberosAuthInterceptor(host, kerberosOptions); + clientBuilder.addInterceptor(interceptor); + } + + private static void configureSAMLAuth(DatabaseClientFactory.SAMLAuthContext samlAuthContext, OkHttpClient.Builder clientBuilder) { + Interceptor interceptor; + String authorizationTokenValue = samlAuthContext.getToken(); + if (authorizationTokenValue != null && authorizationTokenValue.length() > 0) { + interceptor = new HTTPSamlAuthInterceptor(authorizationTokenValue); + } else if (samlAuthContext.getAuthorizer() != null) { + interceptor = new HTTPSamlAuthInterceptor(samlAuthContext.getAuthorizer()); + } else if (samlAuthContext.getRenewer() != null) { + interceptor = new HTTPSamlAuthInterceptor(samlAuthContext.getAuthorization(), samlAuthContext.getRenewer()); + } else + throw new IllegalArgumentException("Either a call back or renewer expected."); + clientBuilder.addInterceptor(interceptor); + } + /** * Configure the hostname verifier for the given OkHttpClient.Builder based on the given SSLHostnameVerifier. * * @param clientBuilder * @param sslVerifier */ - public static void configureHostnameVerifier(OkHttpClient.Builder clientBuilder, DatabaseClientFactory.SSLHostnameVerifier sslVerifier) { + private static void configureHostnameVerifier(OkHttpClient.Builder clientBuilder, DatabaseClientFactory.SSLHostnameVerifier sslVerifier) { HostnameVerifier hostnameVerifier = null; if (DatabaseClientFactory.SSLHostnameVerifier.ANY.equals(sslVerifier)) { hostnameVerifier = (hostname, session) -> true; @@ -75,7 +145,7 @@ public static void configureHostnameVerifier(OkHttpClient.Builder clientBuilder, * @param sslContext * @param trustManager */ - public static void configureSocketFactory(OkHttpClient.Builder clientBuilder, SSLContext sslContext, X509TrustManager trustManager) { + private static void configureSocketFactory(OkHttpClient.Builder clientBuilder, SSLContext sslContext, X509TrustManager trustManager) { /** * Per https://square.github.io/okhttp/3.x/okhttp/okhttp3/OkHttpClient.Builder.html#sslSocketFactory-javax.net.ssl.SSLSocketFactory- , * OkHttp requires a TrustManager to be specified so that it can build a clean certificate chain. If trustManager diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/extra/okhttpclient/OkHttpClientBuilderFactoryTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/extra/okhttpclient/OkHttpClientBuilderFactoryTest.java new file mode 100644 index 000000000..e5f201145 --- /dev/null +++ b/marklogic-client-api/src/test/java/com/marklogic/client/extra/okhttpclient/OkHttpClientBuilderFactoryTest.java @@ -0,0 +1,25 @@ +package com.marklogic.client.extra.okhttpclient; + +import com.marklogic.client.DatabaseClientFactory; +import com.marklogic.client.test.Common; +import okhttp3.OkHttpClient; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class OkHttpClientBuilderFactoryTest { + + @Test + void smokeTest() { + DatabaseClientFactory.Bean bean = Common.newClientBuilder().buildBean(); + OkHttpClient.Builder builder = OkHttpClientBuilderFactory.newOkHttpClientBuilder( + bean.getHost(), bean.getPort(), bean.getSecurityContext()); + assertNotNull(builder); + + OkHttpClient client = builder.build(); + assertNotNull(client, "This is simply verifying that the public/extra method doesn't throw an error. It's " + + "expected to reuse the same approach that constructing a DatabaseClient does. And it's expected that " + + "with ml-app-deployer 4.5.0 depending on this method for any calls to MarkLogic, tests will fail in that " + + "project if there's ever an issue with this method"); + } +}