1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.security.net.config;
18
19import android.util.Pair;
20import java.util.HashSet;
21import java.util.Locale;
22import java.util.Set;
23import javax.net.ssl.X509TrustManager;
24
25/**
26 * An application's network security configuration.
27 *
28 * <p>{@link #getConfigForHostname(String)} provides a means to obtain network security
29 * configuration to be used for communicating with a specific hostname.</p>
30 *
31 * @hide
32 */
33public final class ApplicationConfig {
34    private static ApplicationConfig sInstance;
35    private static Object sLock = new Object();
36
37    private Set<Pair<Domain, NetworkSecurityConfig>> mConfigs;
38    private NetworkSecurityConfig mDefaultConfig;
39    private X509TrustManager mTrustManager;
40
41    private ConfigSource mConfigSource;
42    private boolean mInitialized;
43    private final Object mLock = new Object();
44
45    public ApplicationConfig(ConfigSource configSource) {
46        mConfigSource = configSource;
47        mInitialized = false;
48    }
49
50    /**
51     * @hide
52     */
53    public boolean hasPerDomainConfigs() {
54        ensureInitialized();
55        return mConfigs != null && !mConfigs.isEmpty();
56    }
57
58    /**
59     * Get the {@link NetworkSecurityConfig} corresponding to the provided hostname.
60     * When matching the most specific matching domain rule will be used, if no match exists
61     * then the default configuration will be returned.
62     *
63     * {@code NetworkSecurityConfig} objects returned by this method can be safely cached for
64     * {@code hostname}. Subsequent calls with the same hostname will always return the same
65     * {@code NetworkSecurityConfig}.
66     *
67     * @return {@link NetworkSecurityConfig} to be used to determine
68     * the network security configuration for connections to {@code hostname}.
69     */
70    public NetworkSecurityConfig getConfigForHostname(String hostname) {
71        ensureInitialized();
72        if (hostname == null || hostname.isEmpty() || mConfigs == null) {
73            return mDefaultConfig;
74        }
75        if (hostname.charAt(0) ==  '.') {
76            throw new IllegalArgumentException("hostname must not begin with a .");
77        }
78        // Domains are case insensitive.
79        hostname = hostname.toLowerCase(Locale.US);
80        // Normalize hostname by removing trailing . if present, all Domain hostnames are
81        // absolute.
82        if (hostname.charAt(hostname.length() - 1) == '.') {
83            hostname = hostname.substring(0, hostname.length() - 1);
84        }
85        // Find the Domain -> NetworkSecurityConfig entry with the most specific matching
86        // Domain entry for hostname.
87        // TODO: Use a smarter data structure for the lookup.
88        Pair<Domain, NetworkSecurityConfig> bestMatch = null;
89        for (Pair<Domain, NetworkSecurityConfig> entry : mConfigs) {
90            Domain domain = entry.first;
91            NetworkSecurityConfig config = entry.second;
92            // Check for an exact match.
93            if (domain.hostname.equals(hostname)) {
94                return config;
95            }
96            // Otherwise check if the Domain includes sub-domains and that the hostname is a
97            // sub-domain of the Domain.
98            if (domain.subdomainsIncluded
99                    && hostname.endsWith(domain.hostname)
100                    && hostname.charAt(hostname.length() - domain.hostname.length() - 1) == '.') {
101                if (bestMatch == null) {
102                    bestMatch = entry;
103                } else if (domain.hostname.length() > bestMatch.first.hostname.length()) {
104                    bestMatch = entry;
105                }
106            }
107        }
108        if (bestMatch != null) {
109            return bestMatch.second;
110        }
111        // If no match was found use the default configuration.
112        return mDefaultConfig;
113    }
114
115    /**
116     * Returns the {@link X509TrustManager} that implements the checking of trust anchors and
117     * certificate pinning based on this configuration.
118     */
119    public X509TrustManager getTrustManager() {
120        ensureInitialized();
121        return mTrustManager;
122    }
123
124    /**
125     * Returns {@code true} if cleartext traffic is permitted for this application, which is the
126     * case only if all configurations permit cleartext traffic. For finer-grained policy use
127     * {@link #isCleartextTrafficPermitted(String)}.
128     */
129    public boolean isCleartextTrafficPermitted() {
130        ensureInitialized();
131        if (mConfigs != null) {
132            for (Pair<Domain, NetworkSecurityConfig> entry : mConfigs) {
133                if (!entry.second.isCleartextTrafficPermitted()) {
134                    return false;
135                }
136            }
137        }
138
139        return mDefaultConfig.isCleartextTrafficPermitted();
140    }
141
142    /**
143     * Returns {@code true} if cleartext traffic is permitted for this application when connecting
144     * to {@code hostname}.
145     */
146    public boolean isCleartextTrafficPermitted(String hostname) {
147        return getConfigForHostname(hostname).isCleartextTrafficPermitted();
148    }
149
150    public void handleTrustStorageUpdate() {
151        ensureInitialized();
152        mDefaultConfig.handleTrustStorageUpdate();
153        if (mConfigs != null) {
154            Set<NetworkSecurityConfig> updatedConfigs =
155                    new HashSet<NetworkSecurityConfig>(mConfigs.size());
156            for (Pair<Domain, NetworkSecurityConfig> entry : mConfigs) {
157                if (updatedConfigs.add(entry.second)) {
158                    entry.second.handleTrustStorageUpdate();
159                }
160            }
161        }
162    }
163
164    private void ensureInitialized() {
165        synchronized(mLock) {
166            if (mInitialized) {
167                return;
168            }
169            mConfigs = mConfigSource.getPerDomainConfigs();
170            mDefaultConfig = mConfigSource.getDefaultConfig();
171            mConfigSource = null;
172            mTrustManager = new RootTrustManager(this);
173            mInitialized = true;
174        }
175    }
176
177    public static void setDefaultInstance(ApplicationConfig config) {
178        synchronized (sLock) {
179            sInstance = config;
180        }
181    }
182
183    public static ApplicationConfig getDefaultInstance() {
184        synchronized (sLock) {
185            return sInstance;
186        }
187    }
188}
189