1 /*
2  * Copyright (C) 2020 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 
17 package com.android.server.pm.verify.domain;
18 
19 import android.annotation.CheckResult;
20 import android.annotation.NonNull;
21 import android.content.Intent;
22 import android.content.pm.ApplicationInfo;
23 import android.content.pm.PackageManager;
24 import android.content.pm.PackageManager.NameNotFoundException;
25 import android.text.TextUtils;
26 import android.util.Patterns;
27 
28 import com.android.internal.util.CollectionUtils;
29 import com.android.server.compat.PlatformCompat;
30 import com.android.server.pm.PackageManagerService;
31 import com.android.server.pm.pkg.AndroidPackage;
32 
33 import java.util.Set;
34 import java.util.regex.Matcher;
35 
36 public final class DomainVerificationUtils {
37 
38     public static final int MAX_DOMAIN_LENGTH = 254;
39     public static final int MAX_DOMAIN_LABEL_LENGTH = 63;
40 
41     private static final ThreadLocal<Matcher> sCachedMatcher = ThreadLocal.withInitial(
42             () -> Patterns.DOMAIN_NAME.matcher(""));
43 
44     /**
45      * Consolidates package exception messages. A generic unavailable message is included since the
46      * caller doesn't bother to check why the package isn't available.
47      */
48     @CheckResult
throwPackageUnavailable(@onNull String packageName)49     static NameNotFoundException throwPackageUnavailable(@NonNull String packageName)
50             throws NameNotFoundException {
51         throw new NameNotFoundException("Package " + packageName + " unavailable");
52     }
53 
isDomainVerificationIntent(Intent intent, @PackageManager.ResolveInfoFlagsBits long resolveInfoFlags)54     public static boolean isDomainVerificationIntent(Intent intent,
55             @PackageManager.ResolveInfoFlagsBits long resolveInfoFlags) {
56         if (!intent.isWebIntent()) {
57             return false;
58         }
59 
60         String host = intent.getData().getHost();
61         if (TextUtils.isEmpty(host)) {
62             return false;
63         }
64 
65         if (!sCachedMatcher.get().reset(host).matches()) {
66             return false;
67         }
68 
69         Set<String> categories = intent.getCategories();
70         int categoriesSize = CollectionUtils.size(categories);
71         if (categoriesSize > 2) {
72             // Specifying at least one non-app-link category
73             return false;
74         } else if (categoriesSize == 2) {
75             // Check for explicit app link intent with exactly BROWSABLE && DEFAULT
76             return intent.hasCategory(Intent.CATEGORY_DEFAULT)
77                     && intent.hasCategory(Intent.CATEGORY_BROWSABLE);
78         }
79 
80         boolean matchDefaultByFlags = (resolveInfoFlags & PackageManager.MATCH_DEFAULT_ONLY) != 0;
81 
82         // Check if matches (BROWSABLE || none) && DEFAULT
83         if (categoriesSize == 0) {
84             // No categories, only allow matching DEFAULT by flags
85             return matchDefaultByFlags;
86         } else if (intent.hasCategory(Intent.CATEGORY_BROWSABLE)) {
87             // Intent matches BROWSABLE, must match DEFAULT by flags
88             return matchDefaultByFlags;
89         } else {
90             // Otherwise only needs to have DEFAULT
91             return intent.hasCategory(Intent.CATEGORY_DEFAULT);
92         }
93     }
94 
isChangeEnabled(PlatformCompat platformCompat, AndroidPackage pkg, long changeId)95     static boolean isChangeEnabled(PlatformCompat platformCompat, AndroidPackage pkg,
96             long changeId) {
97         return  platformCompat.isChangeEnabledInternalNoLogging(changeId, buildMockAppInfo(pkg));
98     }
99 
100     /**
101      * Passed to {@link PlatformCompat} because this can be invoked mid-install process or when
102      * {@link PackageManagerService#mLock} is being held, and {@link PlatformCompat} will not be
103      * able to query the pending {@link ApplicationInfo} from {@link PackageManager}.
104      * <p>
105      * TODO(b/177613575): Can a different API be used?
106      */
107     @NonNull
buildMockAppInfo(@onNull AndroidPackage pkg)108     private static ApplicationInfo buildMockAppInfo(@NonNull AndroidPackage pkg) {
109         ApplicationInfo appInfo = new ApplicationInfo();
110         appInfo.packageName = pkg.getPackageName();
111         appInfo.targetSdkVersion = pkg.getTargetSdkVersion();
112         return appInfo;
113     }
114 
isValidDomain(String domain)115     static boolean isValidDomain(String domain) {
116         if (domain.length() > MAX_DOMAIN_LENGTH || domain.equals("*")) {
117             return false;
118         }
119         if (domain.charAt(0) == '*') {
120             if (domain.charAt(1) != '.') {
121                 return false;
122             }
123             domain = domain.substring(2);
124         }
125         int labels = 1;
126         int labelStart = -1;
127         for (int i = 0; i < domain.length(); i++) {
128             char c = domain.charAt(i);
129             if (c == '.') {
130                 int labelLength = i - labelStart - 1;
131                 if (labelLength == 0 || labelLength > MAX_DOMAIN_LABEL_LENGTH) {
132                     return false;
133                 }
134                 labelStart = i;
135                 labels += 1;
136             } else if (!isValidDomainChar(c)) {
137                 return false;
138             }
139         }
140         int lastLabelLength = domain.length() - labelStart - 1;
141         if (lastLabelLength == 0 || lastLabelLength > 63) {
142             return false;
143         }
144         return labels > 1;
145     }
146 
isValidDomainChar(char c)147     private static boolean isValidDomainChar(char c) {
148         return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
149                 || (c >= '0' && c <= '9') || c == '-';
150     }
151 }
152