diff --git a/core/java/android/security/net/config/XmlConfigSource.java b/core/java/android/security/net/config/XmlConfigSource.java index 77c9bb60191f1..6abfb66f94509 100644 --- a/core/java/android/security/net/config/XmlConfigSource.java +++ b/core/java/android/security/net/config/XmlConfigSource.java @@ -186,15 +186,21 @@ public class XmlConfigSource implements ConfigSource { return anchors; } - private Pair> parseConfigEntry( - XmlResourceParser parser, Set seenDomains, boolean baseConfig) + private List>> parseConfigEntry( + XmlResourceParser parser, Set seenDomains, + NetworkSecurityConfig.Builder parentBuilder, boolean baseConfig) throws IOException, XmlPullParserException, ParserException { + List>> builders = new ArrayList<>(); NetworkSecurityConfig.Builder builder = new NetworkSecurityConfig.Builder(); + builder.setParent(parentBuilder); Set 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 @@ public class XmlConfigSource implements ConfigSource { // 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 @@ public class XmlConfigSource implements ConfigSource { } 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 @@ public class XmlConfigSource implements ConfigSource { 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 @@ public class XmlConfigSource implements ConfigSource { 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 @@ public class XmlConfigSource implements ConfigSource { for (Pair> entry : builders) { NetworkSecurityConfig.Builder builder = entry.first; Set 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 0000000000000..d45fd77a5f0f8 --- /dev/null +++ b/tests/NetworkSecurityConfigTest/res/xml/nested_domains.xml @@ -0,0 +1,18 @@ + + + + android.com + + + + + + developer.android.com + + 7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y= + + + + + + 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 0000000000000..84e06e324513d --- /dev/null +++ b/tests/NetworkSecurityConfigTest/res/xml/nested_domains_override.xml @@ -0,0 +1,12 @@ + + + + + + + android.com + + developer.android.com + + + diff --git a/tests/NetworkSecurityConfigTest/src/android/security/net/config/XmlConfigTests.java b/tests/NetworkSecurityConfigTest/src/android/security/net/config/XmlConfigTests.java index 4914d06e23114..f52a279958546 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 @@ public class XmlConfigTests extends AndroidTestCase { 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);