content: /*
   * Licensed to the Apache Software Foundation (ASF) under one
   * or more contributor license agreements.  See the NOTICE file
   * distributed with this work for additional information
   * regarding copyright ownership.  The ASF licenses this file
   * to you 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.datastax.oss.driver.internal.core.config.cloud;
  
  import com.datastax.oss.driver.api.core.metadata.EndPoint;
  import com.datastax.oss.driver.internal.core.metadata.SniEndPoint;
  import com.datastax.oss.driver.internal.core.ssl.SniSslEngineFactory;
  import com.datastax.oss.driver.shaded.guava.common.io.ByteStreams;
  import com.datastax.oss.driver.shaded.guava.common.net.HostAndPort;
  import com.fasterxml.jackson.core.JsonParser;
  import com.fasterxml.jackson.databind.JsonNode;
  import com.fasterxml.jackson.databind.ObjectMapper;
  import edu.umd.cs.findbugs.annotations.NonNull;
  import java.io.BufferedReader;
  import java.io.ByteArrayInputStream;
  import java.io.ByteArrayOutputStream;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.InputStreamReader;
  import java.net.ConnectException;
  import java.net.InetSocketAddress;
  import java.net.MalformedURLException;
  import java.net.URL;
  import java.net.UnknownHostException;
  import java.nio.charset.StandardCharsets;
  import java.security.GeneralSecurityException;
  import java.security.KeyStore;
  import java.security.SecureRandom;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.List;
  import java.util.Objects;
  import java.util.zip.ZipEntry;
  import java.util.zip.ZipInputStream;
  import javax.net.ssl.HttpsURLConnection;
  import javax.net.ssl.KeyManagerFactory;
  import javax.net.ssl.SSLContext;
  import javax.net.ssl.TrustManagerFactory;
  import net.jcip.annotations.ThreadSafe;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  @ThreadSafe
  public class CloudConfigFactory {
    private static final Logger LOG = LoggerFactory.getLogger(CloudConfigFactory.class);
    /**
     * Creates a {@link CloudConfig} with information fetched from the specified Cloud configuration
     * URL.
     *
     * <p>The target URL must point to a valid secure connect bundle archive in ZIP format.
     *
     * @param cloudConfigUrl the URL to fetch the Cloud configuration from; cannot be null.
     * @throws IOException If the Cloud configuration cannot be read.
     * @throws GeneralSecurityException If the Cloud SSL context cannot be created.
     */
    @NonNull
    public CloudConfig createCloudConfig(@NonNull URL cloudConfigUrl)
        throws IOException, GeneralSecurityException {
      Objects.requireNonNull(cloudConfigUrl, "cloudConfigUrl cannot be null");
      return createCloudConfig(cloudConfigUrl.openStream());
    }
  
    /**
     * Creates a {@link CloudConfig} with information fetched from the specified {@link InputStream}.
     *
     * <p>The stream must contain a valid secure connect bundle archive in ZIP format. Note that the
     * stream will be closed after a call to that method and cannot be used anymore.
     *
     * @param cloudConfig the stream to read the Cloud configuration from; cannot be null.
     * @throws IOException If the Cloud configuration cannot be read.
     * @throws GeneralSecurityException If the Cloud SSL context cannot be created.
     */
    @NonNull
    public CloudConfig createCloudConfig(@NonNull InputStream cloudConfig)
        throws IOException, GeneralSecurityException {
      Objects.requireNonNull(cloudConfig, "cloudConfig cannot be null");
      JsonNode configJson = null;
      ByteArrayOutputStream keyStoreOutputStream = null;
      ByteArrayOutputStream trustStoreOutputStream = null;
      ObjectMapper mapper = new ObjectMapper().configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false);
      try (ZipInputStream zipInputStream = new ZipInputStream(cloudConfig)) {
        ZipEntry entry;
        while ((entry = zipInputStream.getNextEntry()) != null) {
          String fileName = entry.getName();
          switch (fileName) {
            case "config.json":
              configJson = mapper.readTree(zipInputStream);
              break;
            case "identity.jks":
              keyStoreOutputStream = new ByteArrayOutputStream();
              ByteStreams.copy(zipInputStream, keyStoreOutputStream);
              break;
            case "trustStore.jks":
              trustStoreOutputStream = new ByteArrayOutputStream();
              ByteStreams.copy(zipInputStream, trustStoreOutputStream);
              break;
          }
        }
      }
      if (configJson == null) {
        throw new IllegalStateException("Invalid bundle: missing file config.json");
      }
      if (keyStoreOutputStream == null) {
        throw new IllegalStateException("Invalid bundle: missing file identity.jks");
      }
      if (trustStoreOutputStream == null) {
        throw new IllegalStateException("Invalid bundle: missing file trustStore.jks");
      }
      char[] keyStorePassword = getKeyStorePassword(configJson);
      char[] trustStorePassword = getTrustStorePassword(configJson);
      ByteArrayInputStream keyStoreInputStream =
          new ByteArrayInputStream(keyStoreOutputStream.toByteArray());
      ByteArrayInputStream trustStoreInputStream =
          new ByteArrayInputStream(trustStoreOutputStream.toByteArray());
      SSLContext sslContext =
          createSslContext(
              keyStoreInputStream, keyStorePassword, trustStoreInputStream, trustStorePassword);
      URL metadataServiceUrl = getMetadataServiceUrl(configJson);
      JsonNode proxyMetadataJson;
      try (BufferedReader proxyMetadata = fetchProxyMetadata(metadataServiceUrl, sslContext)) {
        proxyMetadataJson = mapper.readTree(proxyMetadata);
      }
      InetSocketAddress sniProxyAddress = getSniProxyAddress(proxyMetadataJson);
      List<EndPoint> endPoints = getEndPoints(proxyMetadataJson, sniProxyAddress);
      String localDatacenter = getLocalDatacenter(proxyMetadataJson);
      SniSslEngineFactory sslEngineFactory = new SniSslEngineFactory(sslContext);
      validateIfBundleContainsUsernamePassword(configJson);
      return new CloudConfig(sniProxyAddress, endPoints, localDatacenter, sslEngineFactory);
    }
  
    @NonNull
    protected char[] getKeyStorePassword(JsonNode configFile) {
      if (configFile.has("keyStorePassword")) {
        return configFile.get("keyStorePassword").asText().toCharArray();
      } else {
        throw new IllegalStateException("Invalid config.json: missing field keyStorePassword");
      }
    }
  
    @NonNull
    protected char[] getTrustStorePassword(JsonNode configFile) {
      if (configFile.has("trustStorePassword")) {
        return configFile.get("trustStorePassword").asText().toCharArray();
      } else {
        throw new IllegalStateException("Invalid config.json: missing field trustStorePassword");
      }
    }
  
    @NonNull
    protected URL getMetadataServiceUrl(JsonNode configFile) throws MalformedURLException {
      if (configFile.has("host")) {
        String metadataServiceHost = configFile.get("host").asText();
        if (configFile.has("port")) {
          int metadataServicePort = configFile.get("port").asInt();
          return new URL("https", metadataServiceHost, metadataServicePort, "/metadata");
        } else {
          throw new IllegalStateException("Invalid config.json: missing field port");
        }
      } else {
        throw new IllegalStateException("Invalid config.json: missing field host");
      }
    }
  
    protected void validateIfBundleContainsUsernamePassword(JsonNode configFile) {
      if (configFile.has("username") || configFile.has("password")) {
        LOG.info(
            "The bundle contains config.json with username and/or password. Providing it in the bundle is deprecated and ignored.");
      }
    }
  
    @NonNull
    protected SSLContext createSslContext(
        @NonNull ByteArrayInputStream keyStoreInputStream,
        @NonNull char[] keyStorePassword,
        @NonNull ByteArrayInputStream trustStoreInputStream,
        @NonNull char[] trustStorePassword)
        throws IOException, GeneralSecurityException {
      KeyManagerFactory kmf = createKeyManagerFactory(keyStoreInputStream, keyStorePassword);
      TrustManagerFactory tmf = createTrustManagerFactory(trustStoreInputStream, trustStorePassword);
      SSLContext sslContext = SSLContext.getInstance("SSL");
      sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());
      return sslContext;
    }
  
    @NonNull
    protected KeyManagerFactory createKeyManagerFactory(
        @NonNull InputStream keyStoreInputStream, @NonNull char[] keyStorePassword)
        throws IOException, GeneralSecurityException {
      KeyStore ks = KeyStore.getInstance("JKS");
      ks.load(keyStoreInputStream, keyStorePassword);
      KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
      kmf.init(ks, keyStorePassword);
      Arrays.fill(keyStorePassword, (char) 0);
      return kmf;
    }
  
    @NonNull
    protected TrustManagerFactory createTrustManagerFactory(
        @NonNull InputStream trustStoreInputStream, @NonNull char[] trustStorePassword)
        throws IOException, GeneralSecurityException {
      KeyStore ts = KeyStore.getInstance("JKS");
      ts.load(trustStoreInputStream, trustStorePassword);
      TrustManagerFactory tmf =
          TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
      tmf.init(ts);
      Arrays.fill(trustStorePassword, (char) 0);
      return tmf;
    }
  
    @NonNull
    protected BufferedReader fetchProxyMetadata(
        @NonNull URL metadataServiceUrl, @NonNull SSLContext sslContext) throws IOException {
      try {
        HttpsURLConnection connection = (HttpsURLConnection) metadataServiceUrl.openConnection();
        connection.setSSLSocketFactory(sslContext.getSocketFactory());
        connection.setRequestMethod("GET");
        return new BufferedReader(
            new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8));
      } catch (ConnectException e) {
        throw new IllegalStateException(
            "Unable to connect to cloud metadata service. Please make sure your cluster is not parked or terminated",
            e);
      } catch (UnknownHostException e) {
        throw new IllegalStateException(
            "Unable to resolve host for cloud metadata service. Please make sure your cluster is not terminated",
            e);
      }
    }
  
    @NonNull
    protected String getLocalDatacenter(@NonNull JsonNode proxyMetadata) {
      JsonNode contactInfo = getContactInfo(proxyMetadata);
      if (contactInfo.has("local_dc")) {
        return contactInfo.get("local_dc").asText();
      } else {
        throw new IllegalStateException("Invalid proxy metadata: missing field local_dc");
      }
    }
  
    @NonNull
    protected InetSocketAddress getSniProxyAddress(@NonNull JsonNode proxyMetadata) {
      JsonNode contactInfo = getContactInfo(proxyMetadata);
      if (contactInfo.has("sni_proxy_address")) {
        HostAndPort sniProxyHostAndPort =
            HostAndPort.fromString(contactInfo.get("sni_proxy_address").asText());
        if (!sniProxyHostAndPort.hasPort()) {
          throw new IllegalStateException(
              "Invalid proxy metadata: missing port from field sni_proxy_address");
        }
        return InetSocketAddress.createUnresolved(
            sniProxyHostAndPort.getHost(), sniProxyHostAndPort.getPort());
      } else {
        throw new IllegalStateException("Invalid proxy metadata: missing field sni_proxy_address");
      }
    }
  
    @NonNull
    protected List<EndPoint> getEndPoints(
        @NonNull JsonNode proxyMetadata, @NonNull InetSocketAddress sniProxyAddress) {
      JsonNode contactInfo = getContactInfo(proxyMetadata);
      if (contactInfo.has("contact_points")) {
        List<EndPoint> endPoints = new ArrayList<>();
        JsonNode hostIdsJson = contactInfo.get("contact_points");
        for (int i = 0; i < hostIdsJson.size(); i++) {
          endPoints.add(new SniEndPoint(sniProxyAddress, hostIdsJson.get(i).asText()));
        }
        return endPoints;
      } else {
        throw new IllegalStateException("Invalid proxy metadata: missing field contact_points");
      }
    }
  
    @NonNull
    protected JsonNode getContactInfo(@NonNull JsonNode proxyMetadata) {
      if (proxyMetadata.has("contact_info")) {
        return proxyMetadata.get("contact_info");
      } else {
        throw new IllegalStateException("Invalid proxy metadata: missing field contact_info");
      }
    }
  }
  