diff --git a/cli/central-client/src/main/java/org/ballerinalang/central/client/CentralAPIClient.java b/cli/central-client/src/main/java/org/ballerinalang/central/client/CentralAPIClient.java index c1a505cf5efa..1b3b9a15c3f0 100644 --- a/cli/central-client/src/main/java/org/ballerinalang/central/client/CentralAPIClient.java +++ b/cli/central-client/src/main/java/org/ballerinalang/central/client/CentralAPIClient.java @@ -51,12 +51,22 @@ import org.ballerinalang.central.client.model.ToolResolutionCentralResponse; import org.ballerinalang.central.client.model.ToolSearchResult; +import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; import java.io.PrintStream; import java.net.Proxy; import java.net.SocketTimeoutException; import java.net.UnknownHostException; import java.nio.file.Path; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -65,6 +75,9 @@ import java.util.concurrent.TimeUnit; import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; import static java.net.HttpURLConnection.HTTP_BAD_GATEWAY; import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; @@ -142,14 +155,18 @@ public class CentralAPIClient { private static final String ERR_PACKAGE_UN_DEPRECATE = "error: failed to undo deprecation of the package: "; private static final String ERR_PACKAGE_RESOLUTION = "error: while connecting to central: "; private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); - // System property name for enabling central verbose - public static final String SYS_PROP_CENTRAL_VERBOSE_ENABLED = "CENTRAL_VERBOSE_ENABLED"; + private static final MediaType JSON_CONTENT_TYPE = MediaType.parse("application/json"); + private static final int DEFAULT_CONNECT_TIMEOUT = 60; private static final int DEFAULT_READ_TIMEOUT = 60; private static final int DEFAULT_WRITE_TIMEOUT = 60; private static final int DEFAULT_CALL_TIMEOUT = 0; private static final int MAX_RETRY = 1; public static final String CONNECTION_RESET = "Connection reset"; + private static final String ENV_CENTRAL_VERBOSE_ENABLED = "CENTRAL_VERBOSE_ENABLED"; + private static final String ENV_TRUSTSTORE_PATH = "BALLERINA_CA_BUNDLE"; + private static final String ENV_TRUSTSTORE_PASSWORD = "BALLERINA_CA_PASSWORD"; + private static final String ENV_CERT_PATH = "BALLERINA_CA_CERT"; private final String baseUrl; private final Proxy proxy; @@ -163,13 +180,16 @@ public class CentralAPIClient { private final int writeTimeout; private final int callTimeout; private final int maxRetries; + private final String trustStorePath; + private final String trustStorePassword; + private final String singleCertPath; public CentralAPIClient(String baseUrl, Proxy proxy, String accessToken) { this.outStream = System.out; this.baseUrl = baseUrl; this.proxy = proxy; this.accessToken = accessToken; - this.verboseEnabled = Boolean.parseBoolean(System.getenv(SYS_PROP_CENTRAL_VERBOSE_ENABLED)); + this.verboseEnabled = Boolean.parseBoolean(System.getenv(ENV_CENTRAL_VERBOSE_ENABLED)); this.proxyUsername = ""; this.proxyPassword = ""; this.connectTimeout = DEFAULT_CONNECT_TIMEOUT; @@ -177,6 +197,9 @@ public CentralAPIClient(String baseUrl, Proxy proxy, String accessToken) { this.writeTimeout = DEFAULT_WRITE_TIMEOUT; this.callTimeout = DEFAULT_CALL_TIMEOUT; this.maxRetries = MAX_RETRY; + this.trustStorePath = System.getenv(ENV_TRUSTSTORE_PATH); + this.trustStorePassword = System.getenv(ENV_TRUSTSTORE_PASSWORD); + this.singleCertPath = System.getenv(ENV_CERT_PATH); } public CentralAPIClient(String baseUrl, Proxy proxy, String accessToken, boolean verboseEnabled, int maxRetries, @@ -193,6 +216,9 @@ public CentralAPIClient(String baseUrl, Proxy proxy, String accessToken, boolean this.writeTimeout = DEFAULT_WRITE_TIMEOUT; this.callTimeout = DEFAULT_CALL_TIMEOUT; this.maxRetries = maxRetries; + this.trustStorePath = System.getenv(ENV_TRUSTSTORE_PATH); + this.trustStorePassword = System.getenv(ENV_TRUSTSTORE_PASSWORD); + this.singleCertPath = System.getenv(ENV_CERT_PATH); } public CentralAPIClient(String baseUrl, Proxy proxy, String proxyUsername, String proxyPassword, @@ -202,7 +228,7 @@ public CentralAPIClient(String baseUrl, Proxy proxy, String proxyUsername, Strin this.baseUrl = baseUrl; this.proxy = proxy; this.accessToken = accessToken; - this.verboseEnabled = Boolean.parseBoolean(System.getenv(SYS_PROP_CENTRAL_VERBOSE_ENABLED)); + this.verboseEnabled = Boolean.parseBoolean(System.getenv(ENV_CENTRAL_VERBOSE_ENABLED)); this.proxyUsername = proxyUsername; this.proxyPassword = proxyPassword; this.connectTimeout = connectionTimeout; @@ -210,6 +236,9 @@ public CentralAPIClient(String baseUrl, Proxy proxy, String proxyUsername, Strin this.writeTimeout = writeTimeout; this.callTimeout = callTimeout; this.maxRetries = maxRetries; + this.trustStorePath = System.getenv(ENV_TRUSTSTORE_PATH); + this.trustStorePassword = System.getenv(ENV_TRUSTSTORE_PASSWORD); + this.singleCertPath = System.getenv(ENV_CERT_PATH); } /** @@ -1591,8 +1620,8 @@ public JsonObject getConnector(ConnectorInfo connector, String supportedPlatform * * @return the client */ - protected OkHttpClient getClient() { - OkHttpClient okHttpClient = new OkHttpClient.Builder() + protected OkHttpClient getClient() throws CentralClientException { + OkHttpClient.Builder builder = new OkHttpClient.Builder() .connectTimeout(connectTimeout, TimeUnit.SECONDS) .readTimeout(readTimeout, TimeUnit.SECONDS) .writeTimeout(writeTimeout, TimeUnit.SECONDS) @@ -1600,8 +1629,49 @@ protected OkHttpClient getClient() { .followRedirects(false) .retryOnConnectionFailure(true) .proxy(this.proxy) - .addInterceptor(new CustomRetryInterceptor(this.maxRetries)) - .build(); + .addInterceptor(new CustomRetryInterceptor(this.maxRetries)); + + // Load custom truststore if provided, otherwise use the default truststore + try { + KeyStore truststore; + if (this.trustStorePath != null && this.trustStorePassword != null) { + truststore = KeyStore.getInstance(KeyStore.getDefaultType()); + try (InputStream keys = new FileInputStream(trustStorePath)) { + truststore.load(keys, trustStorePassword.toCharArray()); + } + } else { + truststore = KeyStore.getInstance(KeyStore.getDefaultType()); + try (InputStream defaultKeys = new FileInputStream(System.getProperty("java.home") + + "/lib/security/cacerts")) { + truststore.load(defaultKeys, "changeit".toCharArray()); // Default password for cacerts + } + } + + // If there's a single certificate to add + if (this.singleCertPath != null) { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + try (InputStream certInputStream = new FileInputStream(singleCertPath)) { + Certificate certificate = certificateFactory.generateCertificate(certInputStream); + truststore.setCertificateEntry("bal-cert", certificate); + } + } + + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(truststore); + + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom()); + SSLContext.setDefault(sslContext); + + builder.sslSocketFactory(sslContext.getSocketFactory(), + (X509TrustManager) trustManagerFactory.getTrustManagers()[0]); + } catch (CertificateException | KeyStoreException | IOException | NoSuchAlgorithmException | + KeyManagementException e) { + throw new CentralClientException(e.getMessage()); + } + + OkHttpClient okHttpClient = builder.build(); if ((!(this.proxyUsername).isEmpty() && !(this.proxyPassword).isEmpty())) { Authenticator proxyAuthenticator = (route, response) -> { diff --git a/cli/central-client/src/test/java/org/ballerinalang/central/client/TestCustomRetryInterceptor.java b/cli/central-client/src/test/java/org/ballerinalang/central/client/TestCustomRetryInterceptor.java index a94489ff7694..f4d59c169c67 100644 --- a/cli/central-client/src/test/java/org/ballerinalang/central/client/TestCustomRetryInterceptor.java +++ b/cli/central-client/src/test/java/org/ballerinalang/central/client/TestCustomRetryInterceptor.java @@ -23,6 +23,7 @@ import okhttp3.Response; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; +import org.ballerinalang.central.client.exceptions.CentralClientException; import org.testng.Assert; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; @@ -49,7 +50,7 @@ public class TestCustomRetryInterceptor { @BeforeClass - public void setUp() { + public void setUp() throws CentralClientException { this.console = new ByteArrayOutputStream(); PrintStream outStream = new PrintStream(this.console); CentralAPIClient centralAPIClient = new CentralAPIClient("https://localhost:9090/registry",