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.ssl;
  
  import com.datastax.oss.driver.api.core.metadata.EndPoint;
  import com.datastax.oss.driver.api.core.ssl.SslEngineFactory;
  import com.datastax.oss.driver.internal.core.metadata.SniEndPoint;
  import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList;
  import edu.umd.cs.findbugs.annotations.NonNull;
  import java.net.InetSocketAddress;
  import java.util.concurrent.CopyOnWriteArrayList;
  import javax.net.ssl.SNIHostName;
  import javax.net.ssl.SSLContext;
  import javax.net.ssl.SSLEngine;
  import javax.net.ssl.SSLParameters;
  
  public class SniSslEngineFactory implements SslEngineFactory {
  
    // An offset that gets added to our "fake" ports (see below). We pick this value because it is the
    // start of the ephemeral port range.
    private static final int FAKE_PORT_OFFSET = 49152;
  
    private final SSLContext sslContext;
    private final CopyOnWriteArrayList<String> fakePorts = new CopyOnWriteArrayList<>();
    private final boolean allowDnsReverseLookupSan;
  
    public SniSslEngineFactory(SSLContext sslContext) {
      this(sslContext, true);
    }
  
    public SniSslEngineFactory(SSLContext sslContext, boolean allowDnsReverseLookupSan) {
      this.sslContext = sslContext;
      this.allowDnsReverseLookupSan = allowDnsReverseLookupSan;
    }
  
    @NonNull
    @Override
    public SSLEngine newSslEngine(@NonNull EndPoint remoteEndpoint) {
      if (!(remoteEndpoint instanceof SniEndPoint)) {
        throw new IllegalArgumentException(
            String.format(
                "Configuration error: can only use %s with SNI end points",
                this.getClass().getSimpleName()));
      }
      SniEndPoint sniEndPoint = (SniEndPoint) remoteEndpoint;
      InetSocketAddress address = sniEndPoint.resolve();
      String sniServerName = sniEndPoint.getServerName();
  
      // When hostname verification is enabled (with setEndpointIdentificationAlgorithm), the SSL
      // engine will try to match the server's certificate against the SNI host name; if that doesn't
      // work, it will fall back to the "advisory peer host" passed to createSSLEngine.
      //
      // In our case, the first check will never succeed because our SNI host name is not the DNS name
      // (we use the Cassandra host_id instead). So we *must* set the advisory peer information.
      //
      // However if we use the address as-is, this leads to another issue: the advisory peer
      // information is also used to cache SSL sessions internally. All of our nodes share the same
      // proxy address, so the JDK tries to reuse SSL sessions across nodes. But it doesn't update the
      // SNI host name every time, so it ends up opening connections to the wrong node.
      //
      // To avoid that, we create a unique "fake" port for every node. We still get session reuse for
      // a given node, but not across nodes. This is safe because the advisory port is only used for
      // session caching.
      String peerHost = allowDnsReverseLookupSan ? address.getHostName() : address.getHostString();
      SSLEngine engine = sslContext.createSSLEngine(peerHost, getFakePort(sniServerName));
      engine.setUseClientMode(true);
      SSLParameters parameters = engine.getSSLParameters();
      parameters.setServerNames(ImmutableList.of(new SNIHostName(sniServerName)));
      parameters.setEndpointIdentificationAlgorithm("HTTPS");
      engine.setSSLParameters(parameters);
      return engine;
    }
  
    private int getFakePort(String sniServerName) {
      fakePorts.addIfAbsent(sniServerName);
      return FAKE_PORT_OFFSET + fakePorts.indexOf(sniServerName);
    }
  
    @Override
    public void close() {
      // nothing to do
    }
  }
  