OkHttpClientBuilder.java
/*
* onedrive-java-sdk - A Java SDK to access OneDrive drives and files.
* Copyright © 2023-2025 Andy Miles (andy.miles@amilesend.com)
*
* This program 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.amilesend.onedrive.connection.http;
import com.amilesend.client.connection.ConnectionException;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import okhttp3.Authenticator;
import okhttp3.ConnectionSpec;
import okhttp3.Credentials;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.Route;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.Nullable;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.net.Proxy;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.time.Duration;
import java.util.List;
/**
* Utility to configure and build a {@link OkHttpClient} with the option of default configuration for this SDK.
* This supports configuring a client that:
* <ul>
* <li>Customizes the SSL trust manager and hostname verifier</li>
* <li>Configures a proxy with username and password</li>
* <li>Configures support to follow redirects</li>
* <li>Configures connection and read/write timeouts (default is disabled)</li>
* </ul>
*/
public class OkHttpClientBuilder {
private X509TrustManager trustManager;
private HostnameVerifier hostnameVerifier;
private Proxy proxy;
private String proxyUsername;
private String proxyPassword;
private Authenticator proxyAuthenticator;
private boolean isRedirectsAllowed = true;
private boolean isForTest;
private Duration connectTimeout = Duration.ofMillis(10000L);
private Duration readTimeout = Duration.ofMillis(10000L);
private Duration writeTimeout = Duration.ofMillis(10000L);
/**
* Sets the SSL/TLS trust manager to use with the HTTP client.
*
* @param trustManager the trust manager
* @return the builder instance
* @see X509TrustManager
*/
public OkHttpClientBuilder trustManager(final X509TrustManager trustManager) {
this.trustManager = trustManager;
return this;
}
/**
* Sets the hostname verifier to use with the HTTP client.
*
* @param hostnameVerifier the trust manager
* @return the builder instance
* @see HostnameVerifier
*/
public OkHttpClientBuilder hostnameVerifier(final HostnameVerifier hostnameVerifier) {
this.hostnameVerifier = hostnameVerifier;
return this;
}
/**
* Sets the proxy and associated username plus password to use with the HTTP client. Note: if {@code username} and
* {@code password} is undefined, then no credential-based authentication will be configured.
*
* @param proxy the proxy settings
* @param username the username
* @param password the password
* @return the builder instance
*/
public OkHttpClientBuilder proxy(final Proxy proxy, final String username, final String password) {
this.proxy = proxy;
this.proxyUsername = username;
this.proxyPassword = password;
return this;
}
/**
* Sets the proxy and associated authenticator to use with the HTTP client.
*
* @param proxy the proxy settings
* @param proxyAuthenticator the proxy authenticator
* @return the builder instance
*/
public OkHttpClientBuilder proxy(final Proxy proxy, final Authenticator proxyAuthenticator) {
this.proxy = proxy;
this.proxyAuthenticator = proxyAuthenticator;
return this;
}
/**
* Sets the connection timeout for the HTTP client.
*
* @param connectTimeout the connection timeout
* @return the builder instance
*/
public OkHttpClientBuilder connectTimeout(final Duration connectTimeout) {
this.connectTimeout = connectTimeout;
return this;
}
/**
* Sets the read timeout for the HTTP client.
*
* @param readTimeout the read timeout
* @return the builder instance
*/
public OkHttpClientBuilder readTimeout(final Duration readTimeout) {
this.readTimeout = readTimeout;
return this;
}
/**
* Sets the write timeout for the HTTP client.
*
* @param writeTimeout the write timeout.
* @return the builder instance
*/
public OkHttpClientBuilder writeTimeout(final Duration writeTimeout) {
this.writeTimeout = writeTimeout;
return this;
}
/**
* Sets the flag to allow for automatic URL redirects when responses return 300-based HTTP responses.
*
* @param isRedirectsAllowed If {@code true}, then the client will automatically invoke the redirected URL;
* else, {@code false}
* @return the builder instance
*/
public OkHttpClientBuilder isRedirectsAllowed(final boolean isRedirectsAllowed) {
this.isRedirectsAllowed = isRedirectsAllowed;
return this;
}
/**
* Sets the flag to allow for non-SSL/TLS based requests used for testing. Note: Should not be set for normal
* use with real calls to the Graph API.
*
* @param isForTest If {@code true}, then the client will allow HTTP-based invocations; else, {@code false}.
* @return the builder instance
*/
public OkHttpClientBuilder isForTest(final boolean isForTest) {
this.isForTest = isForTest;
return this;
}
/**
* Builds a new {@code OkHttpClient} instance.
*
* @return the configured HTTP client
*/
public OkHttpClient build() {
return configureProxy(
configureSsl(new OkHttpClient.Builder()
.followSslRedirects(isRedirectsAllowed)
.followRedirects(isRedirectsAllowed)
.connectTimeout(connectTimeout)
.readTimeout(readTimeout)
.writeTimeout(writeTimeout)
.connectionSpecs(getConnectionSpecs())))
.build();
}
private List<ConnectionSpec> getConnectionSpecs() {
return isForTest
? List.of(ConnectionSpec.CLEARTEXT, ConnectionSpec.MODERN_TLS)
: List.of(ConnectionSpec.MODERN_TLS);
}
private OkHttpClient.Builder configureSsl(OkHttpClient.Builder builder) {
if (trustManager == null) {
return builder;
}
try {
final SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[]{ trustManager }, new SecureRandom());
builder = builder.sslSocketFactory(sslContext.getSocketFactory(), trustManager);
} catch (final KeyManagementException | NoSuchAlgorithmException ex) {
throw new ConnectionException("Unable to create HttpClient: " + ex.getMessage(), ex);
}
if (hostnameVerifier == null) {
return builder;
}
return builder.hostnameVerifier(hostnameVerifier);
}
private OkHttpClient.Builder configureProxy(OkHttpClient.Builder builder) {
if (proxy == null) {
return builder;
}
builder = builder.proxy(proxy);
final Authenticator authenticatorToSet;
if (proxyAuthenticator != null) {
authenticatorToSet = proxyAuthenticator;
} else if (StringUtils.isNotBlank(proxyUsername) || StringUtils.isNotBlank(proxyPassword)) {
authenticatorToSet = new CredentialAuthenticator(proxyUsername, proxyPassword);
} else {
authenticatorToSet = null;
}
return authenticatorToSet != null ? builder.proxyAuthenticator(authenticatorToSet) : builder;
}
@RequiredArgsConstructor
private static class CredentialAuthenticator implements Authenticator {
private final String username;
private final String password;
@Nullable
@Override
public Request authenticate(final Route route, @NonNull final Response response) {
final String credential = Credentials.basic(username, password);
return response.request()
.newBuilder()
.header("Proxy-Authorization", credential)
.build();
}
}
}