/*
* 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.
*
*
{@link #getConfigForHostname(String)} provides a means to obtain network security
* configuration to be used for communicating with a specific hostname.
*
* @hide
*/
public final class ApplicationConfig {
private static ApplicationConfig sInstance;
private static Object sLock = new Object();
private Set> 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 bestMatch = null;
for (Pair 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 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() {
ensureInitialized();
mDefaultConfig.handleTrustStorageUpdate();
if (mConfigs != null) {
Set updatedConfigs =
new HashSet(mConfigs.size());
for (Pair 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;
}
}
}