content: /*
   * Copyright (C) 2015 The Android Open Source Project
   *
   * 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 android.security.net.config;
  
  import android.util.Pair;
  import java.util.HashSet;
  import java.util.Locale;
  import java.util.Set;
  import javax.net.ssl.X509TrustManager;
  
  /**
   * An application's network security configuration.
   *
   * <p>{@link #getConfigForHostname(String)} provides a means to obtain network security
   * configuration to be used for communicating with a specific hostname.</p>
   *
   * @hide
   */
  public final class ApplicationConfig {
      private static ApplicationConfig sInstance;
      private static Object sLock = new Object();
  
      private Set<Pair<Domain, NetworkSecurityConfig>> mConfigs;
      private NetworkSecurityConfig mDefaultConfig;
      private X509TrustManager mTrustManager;
  
      private ConfigSource mConfigSource;
      private boolean mInitialized;
      private final Object mLock = new Object();
  
      public ApplicationConfig(ConfigSource configSource) {
          mConfigSource = configSource;
          mInitialized = false;
      }
  
      /**
       * @hide
       */
      public boolean hasPerDomainConfigs() {
          ensureInitialized();
          return mConfigs != null && !mConfigs.isEmpty();
      }
  
      /**
       * Get the {@link NetworkSecurityConfig} corresponding to the provided hostname.
       * When matching the most specific matching domain rule will be used, if no match exists
       * then the default configuration will be returned.
       *
       * {@code NetworkSecurityConfig} objects returned by this method can be safely cached for
       * {@code hostname}. Subsequent calls with the same hostname will always return the same
       * {@code NetworkSecurityConfig}.
       *
       * @return {@link NetworkSecurityConfig} to be used to determine
       * the network security configuration for connections to {@code hostname}.
       */
      public NetworkSecurityConfig getConfigForHostname(String hostname) {
          ensureInitialized();
          if (hostname == null || hostname.isEmpty() || mConfigs == null) {
              return mDefaultConfig;
          }
          if (hostname.charAt(0) ==  '.') {
              throw new IllegalArgumentException("hostname must not begin with a .");
          }
          // Domains are case insensitive.
          hostname = hostname.toLowerCase(Locale.US);
          // Normalize hostname by removing trailing . if present, all Domain hostnames are
          // absolute.
          if (hostname.charAt(hostname.length() - 1) == '.') {
              hostname = hostname.substring(0, hostname.length() - 1);
          }
          // Find the Domain -> NetworkSecurityConfig entry with the most specific matching
          // Domain entry for hostname.
          // TODO: Use a smarter data structure for the lookup.
          Pair<Domain, NetworkSecurityConfig> bestMatch = null;
          for (Pair<Domain, NetworkSecurityConfig> entry : mConfigs) {
              Domain domain = entry.first;
              NetworkSecurityConfig config = entry.second;
              // Check for an exact match.
              if (domain.hostname.equals(hostname)) {
                  return config;
              }
              // Otherwise check if the Domain includes sub-domains and that the hostname is a
              // sub-domain of the Domain.
              if (domain.subdomainsIncluded
                      && hostname.endsWith(domain.hostname)
                      && hostname.charAt(hostname.length() - domain.hostname.length() - 1) == '.') {
                  if (bestMatch == null) {
                      bestMatch = entry;
                  } else if (domain.hostname.length() > bestMatch.first.hostname.length()) {
                      bestMatch = entry;
                  }
              }
          }
          if (bestMatch != null) {
              return bestMatch.second;
          }
          // If no match was found use the default configuration.
          return mDefaultConfig;
      }
  
      /**
       * Returns the {@link X509TrustManager} that implements the checking of trust anchors and
       * certificate pinning based on this configuration.
       */
      public X509TrustManager getTrustManager() {
          ensureInitialized();
          return mTrustManager;
      }
  
      /**
       * Returns {@code true} if cleartext traffic is permitted for this application, which is the
       * case only if all configurations permit cleartext traffic. For finer-grained policy use
       * {@link #isCleartextTrafficPermitted(String)}.
       */
      public boolean isCleartextTrafficPermitted() {
          ensureInitialized();
          if (mConfigs != null) {
              for (Pair<Domain, NetworkSecurityConfig> entry : mConfigs) {
                  if (!entry.second.isCleartextTrafficPermitted()) {
                      return false;
                  }
              }
          }
  
          return mDefaultConfig.isCleartextTrafficPermitted();
      }
  
      /**
       * Returns {@code true} if cleartext traffic is permitted for this application when connecting
       * to {@code hostname}.
       */
      public boolean isCleartextTrafficPermitted(String hostname) {
          return getConfigForHostname(hostname).isCleartextTrafficPermitted();
      }
  
      public void handleTrustStorageUpdate() {
          synchronized(mLock) {
              // If the config is uninitialized then there is no work to be done to handle an update,
              // avoid needlessly parsing configs.
              if (!mInitialized) {
                  return;
              }
              mDefaultConfig.handleTrustStorageUpdate();
              if (mConfigs != null) {
                  Set<NetworkSecurityConfig> updatedConfigs =
                          new HashSet<NetworkSecurityConfig>(mConfigs.size());
                  for (Pair<Domain, NetworkSecurityConfig> entry : mConfigs) {
                      if (updatedConfigs.add(entry.second)) {
                          entry.second.handleTrustStorageUpdate();
                      }
                  }
              }
          }
      }
  
      private void ensureInitialized() {
          synchronized(mLock) {
              if (mInitialized) {
                  return;
              }
              mConfigs = mConfigSource.getPerDomainConfigs();
              mDefaultConfig = mConfigSource.getDefaultConfig();
              mConfigSource = null;
              mTrustManager = new RootTrustManager(this);
              mInitialized = true;
          }
      }
  
      public static void setDefaultInstance(ApplicationConfig config) {
          synchronized (sLock) {
              sInstance = config;
          }
      }
  
      public static ApplicationConfig getDefaultInstance() {
          synchronized (sLock) {
              return sInstance;
          }
      }
  }
  