Merge "Support nested domain-config elements"
am: 749501e88e

* commit '749501e88e5fa6bdc6594a9db5c232e31bdda867':
  Support nested domain-config elements
diff --git a/core/java/android/security/net/config/XmlConfigSource.java b/core/java/android/security/net/config/XmlConfigSource.java
index 77c9bb6..6abfb66 100644
--- a/core/java/android/security/net/config/XmlConfigSource.java
+++ b/core/java/android/security/net/config/XmlConfigSource.java
@@ -186,15 +186,21 @@
         return anchors;
     }
 
-    private Pair<NetworkSecurityConfig.Builder, Set<Domain>> parseConfigEntry(
-            XmlResourceParser parser, Set<String> seenDomains, boolean baseConfig)
+    private List<Pair<NetworkSecurityConfig.Builder, Set<Domain>>> parseConfigEntry(
+            XmlResourceParser parser, Set<String> seenDomains,
+            NetworkSecurityConfig.Builder parentBuilder, boolean baseConfig)
             throws IOException, XmlPullParserException, ParserException {
+        List<Pair<NetworkSecurityConfig.Builder, Set<Domain>>> builders = new ArrayList<>();
         NetworkSecurityConfig.Builder builder = new NetworkSecurityConfig.Builder();
+        builder.setParent(parentBuilder);
         Set<Domain> domains = new ArraySet<>();
         boolean seenPinSet = false;
         boolean seenTrustAnchors = false;
         String configName = parser.getName();
         int outerDepth = parser.getDepth();
+        // Add this builder now so that this builder occurs before any of its children. This
+        // makes the final build pass easier.
+        builders.add(new Pair<>(builder, domains));
         // Parse config attributes. Only set values that are present, config inheritence will
         // handle the rest.
         for (int i = 0; i < parser.getAttributeCount(); i++) {
@@ -212,7 +218,6 @@
         // Parse the config elements.
         while (XmlUtils.nextElementWithin(parser, outerDepth)) {
             String tagName = parser.getName();
-            // TODO: Support nested domain-config entries.
             if ("domain".equals(tagName)) {
                 if (baseConfig) {
                     throw new ParserException(parser, "domain element not allowed in base-config");
@@ -236,6 +241,12 @@
                 }
                 builder.setPinSet(parsePinSet(parser));
                 seenPinSet = true;
+            } else if ("domain-config".equals(tagName)) {
+                if (baseConfig) {
+                    throw new ParserException(parser,
+                            "Nested domain-config not allowed in base-config");
+                }
+                builders.addAll(parseConfigEntry(parser, seenDomains, builder, false));
             } else {
                 XmlUtils.skipCurrentTag(parser);
             }
@@ -243,7 +254,7 @@
         if (!baseConfig && domains.isEmpty()) {
             throw new ParserException(parser, "No domain elements in domain-config");
         }
-        return new Pair<>(builder, domains);
+        return builders;
     }
 
     private void parseNetworkSecurityConfig(XmlResourceParser parser)
@@ -263,9 +274,9 @@
                     throw new ParserException(parser, "Only one base-config allowed");
                 }
                 seenBaseConfig = true;
-                baseConfigBuilder = parseConfigEntry(parser, seenDomains, true).first;
+                baseConfigBuilder = parseConfigEntry(parser, seenDomains, null, true).get(0).first;
             } else if ("domain-config".equals(parser.getName())) {
-                builders.add(parseConfigEntry(parser, seenDomains, false));
+                builders.addAll(parseConfigEntry(parser, seenDomains, baseConfigBuilder, false));
             } else {
                 XmlUtils.skipCurrentTag(parser);
             }
@@ -286,8 +297,15 @@
         for (Pair<NetworkSecurityConfig.Builder, Set<Domain>> entry : builders) {
             NetworkSecurityConfig.Builder builder = entry.first;
             Set<Domain> domains = entry.second;
-            // Use the base-config for inheriting any unset values in the domain-config entry.
-            builder.setParent(baseConfigBuilder);
+            // Set the parent of configs that do not have a parent to the base-config. This can
+            // happen if the base-config comes after a domain-config in the file.
+            // Note that this is safe with regards to children because of the order that
+            // parseConfigEntry returns builders, the parent is always before the children. The
+            // children builders will not have build called until _after_ their parents have their
+            // parent set so everything is consistent.
+            if (builder.getParent() == null) {
+                builder.setParent(baseConfigBuilder);
+            }
             NetworkSecurityConfig config = builder.build();
             for (Domain domain : domains) {
                 configs.add(new Pair<>(domain, config));
diff --git a/tests/NetworkSecurityConfigTest/res/xml/nested_domains.xml b/tests/NetworkSecurityConfigTest/res/xml/nested_domains.xml
new file mode 100644
index 0000000..d45fd77
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/nested_domains.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+    <domain-config>
+      <domain includeSubdomains="true">android.com</domain>
+      <trust-anchors>
+          <certificates src="system" />
+      </trust-anchors>
+      <!-- nested config that adds pins -->
+      <domain-config>
+          <domain>developer.android.com</domain>
+          <pin-set>
+              <pin digest="SHA-256">7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>
+          </pin-set>
+      </domain-config>
+    </domain-config>
+    <base-config cleartextTrafficPermitted="false">
+    </base-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/nested_domains_override.xml b/tests/NetworkSecurityConfigTest/res/xml/nested_domains_override.xml
new file mode 100644
index 0000000..84e06e3
--- /dev/null
+++ b/tests/NetworkSecurityConfigTest/res/xml/nested_domains_override.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+    <base-config cleartextTrafficPermitted="false">
+    </base-config>
+    <!-- Nested config that overrides parent -->
+    <domain-config cleartextTrafficPermitted="true">
+        <domain includeSubdomains="true">android.com</domain>
+        <domain-config cleartextTrafficPermitted="false">
+            <domain>developer.android.com</domain>
+        </domain-config>
+    </domain-config>
+</network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/src/android/security/net/config/XmlConfigTests.java b/tests/NetworkSecurityConfigTest/src/android/security/net/config/XmlConfigTests.java
index 4914d06..f52a279 100644
--- a/tests/NetworkSecurityConfigTest/src/android/security/net/config/XmlConfigTests.java
+++ b/tests/NetworkSecurityConfigTest/src/android/security/net/config/XmlConfigTests.java
@@ -245,6 +245,35 @@
         TestUtils.assertUrlConnectionSucceeds(context, "android.com", 443);
     }
 
+    public void testNestedDomainConfigs() throws Exception {
+        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.nested_domains);
+        ApplicationConfig appConfig = new ApplicationConfig(source);
+        assertTrue(appConfig.hasPerDomainConfigs());
+        NetworkSecurityConfig parent = appConfig.getConfigForHostname("android.com");
+        NetworkSecurityConfig child = appConfig.getConfigForHostname("developer.android.com");
+        MoreAsserts.assertNotEqual(parent, child);
+        MoreAsserts.assertEmpty(parent.getPins().pins);
+        MoreAsserts.assertNotEmpty(child.getPins().pins);
+        // Check that the child inherited the cleartext value and anchors.
+        assertFalse(child.isCleartextTrafficPermitted());
+        MoreAsserts.assertNotEmpty(child.getTrustAnchors());
+        // Test connections.
+        SSLContext context = TestUtils.getSSLContext(source);
+        TestUtils.assertConnectionSucceeds(context, "android.com", 443);
+        TestUtils.assertConnectionSucceeds(context, "developer.android.com", 443);
+    }
+
+    public void testNestedDomainConfigsOverride() throws Exception {
+        XmlConfigSource source = new XmlConfigSource(getContext(), R.xml.nested_domains_override);
+        ApplicationConfig appConfig = new ApplicationConfig(source);
+        assertTrue(appConfig.hasPerDomainConfigs());
+        NetworkSecurityConfig parent = appConfig.getConfigForHostname("android.com");
+        NetworkSecurityConfig child = appConfig.getConfigForHostname("developer.android.com");
+        MoreAsserts.assertNotEqual(parent, child);
+        assertTrue(parent.isCleartextTrafficPermitted());
+        assertFalse(child.isCleartextTrafficPermitted());
+    }
+
     private void testBadConfig(int configId) throws Exception {
         try {
             XmlConfigSource source = new XmlConfigSource(getContext(), configId);