1/*
2 * Copyright (C) 2016 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 com.android.server.wifi.hotspot2;
18
19import android.text.TextUtils;
20
21import com.android.server.wifi.hotspot2.Utils;
22
23import java.util.HashMap;
24import java.util.Iterator;
25import java.util.List;
26import java.util.Map;
27
28/**
29 * Utility class for matching domain names.
30 */
31public class DomainMatcher {
32    public static final int MATCH_NONE = 0;
33    public static final int MATCH_PRIMARY = 1;
34    public static final int MATCH_SECONDARY = 2;
35
36    /**
37     * The root of the Label tree.
38     */
39    private final Label mRoot;
40
41    /**
42     * Label tree representation for the domain name.  Labels are delimited by "." in the domain
43     * name.
44     *
45     * For example, the tree representation of "android.google.com" as a primary domain:
46     * [com, None] -> [google, None] -> [android, Primary]
47     *
48     */
49    private static class Label {
50        private final Map<String, Label> mSubDomains;
51        private int mMatch;
52
53        Label(int match) {
54            mMatch = match;
55            mSubDomains = new HashMap<String, Label>();
56        }
57
58        /**
59         * Add sub-domains to this label.
60         *
61         * @param labels The iterator of domain label strings
62         * @param match The match status of the domain
63         */
64        public void addDomain(Iterator<String> labels, int match) {
65            String labelName = labels.next();
66            // Create the Label object if it doesn't exist yet.
67            Label subLabel = mSubDomains.get(labelName);
68            if (subLabel == null) {
69                subLabel = new Label(MATCH_NONE);
70                mSubDomains.put(labelName, subLabel);
71            }
72
73            if (labels.hasNext()) {
74                // Adding sub-domain.
75                subLabel.addDomain(labels, match);
76            } else {
77                // End of the domain, update the match status.
78                subLabel.mMatch = match;
79            }
80        }
81
82        /**
83         * Return the Label for the give label string.
84         * @param labelString The label string to look for
85         * @return {@link Label}
86         */
87        public Label getSubLabel(String labelString) {
88            return mSubDomains.get(labelString);
89        }
90
91        /**
92         * Return the match status
93         *
94         * @return The match status
95         */
96        public int getMatch() {
97            return mMatch;
98        }
99
100        private void toString(StringBuilder sb) {
101            if (mSubDomains != null) {
102                sb.append(".{");
103                for (Map.Entry<String, Label> entry : mSubDomains.entrySet()) {
104                    sb.append(entry.getKey());
105                    entry.getValue().toString(sb);
106                }
107                sb.append('}');
108            } else {
109                sb.append('=').append(mMatch);
110            }
111        }
112
113        @Override
114        public String toString() {
115            StringBuilder sb = new StringBuilder();
116            toString(sb);
117            return sb.toString();
118        }
119    }
120
121    public DomainMatcher(String primaryDomain, List<String> secondaryDomains) {
122        // Create the root label.
123        mRoot = new Label(MATCH_NONE);
124
125        // Add secondary domains.
126        if (secondaryDomains != null) {
127            for (String domain : secondaryDomains) {
128                if (!TextUtils.isEmpty(domain)) {
129                    List<String> secondaryLabel = Utils.splitDomain(domain);
130                    mRoot.addDomain(secondaryLabel.iterator(), MATCH_SECONDARY);
131                }
132            }
133        }
134
135        // Add primary domain, primary overwrites secondary.
136        if (!TextUtils.isEmpty(primaryDomain)) {
137            List<String> primaryLabel = Utils.splitDomain(primaryDomain);
138            mRoot.addDomain(primaryLabel.iterator(), MATCH_PRIMARY);
139        }
140    }
141
142    /**
143     * Check if domain is either the same or a sub-domain of any of the domains in the
144     * domain tree in this matcher, i.e. all or a sub-set of the labels in domain matches
145     * a path in the tree.
146     *
147     * This will have precedence for matching primary domain over secondary domain if both
148     * are found.
149     *
150     * For example, with primary domain set to "test.google.com" and secondary domain set to
151     * "google.com":
152     * "test2.test.google.com" -> Match.Primary
153     * "test1.google.com" -> Match.Secondary
154     *
155     * @param domainName Domain name to be checked.
156     * @return The match status
157     */
158    public int isSubDomain(String domainName) {
159        if (TextUtils.isEmpty(domainName)) {
160            return MATCH_NONE;
161        }
162        List<String> domainLabels = Utils.splitDomain(domainName);
163
164        Label label = mRoot;
165        int match = MATCH_NONE;
166        for (String labelString : domainLabels) {
167            label = label.getSubLabel(labelString);
168            if (label == null) {
169                break;
170            } else if (label.getMatch() != MATCH_NONE) {
171                match = label.getMatch();
172                if (match == MATCH_PRIMARY) {
173                    break;
174                }
175            }
176        }
177        return match;
178    }
179
180    /**
181     * Check if domain2 is a sub-domain of domain1.
182     *
183     * @param domain1 The string of the first domain
184     * @param domain2 The string of the second domain
185     * @return true if the second domain is the sub-domain of the first
186     */
187    public static boolean arg2SubdomainOfArg1(String domain1, String domain2) {
188        if (TextUtils.isEmpty(domain1) || TextUtils.isEmpty(domain2)) {
189            return false;
190        }
191
192        List<String> labels1 = Utils.splitDomain(domain1);
193        List<String> labels2 = Utils.splitDomain(domain2);
194
195        // domain2 must be the same or longer than domain1 in order to be a sub-domain.
196        if (labels2.size() < labels1.size()) {
197            return false;
198        }
199
200        Iterator<String> l1 = labels1.iterator();
201        Iterator<String> l2 = labels2.iterator();
202
203        while(l1.hasNext()) {
204            if (!TextUtils.equals(l1.next(), l2.next())) {
205                return false;
206            }
207        }
208        return true;
209    }
210
211    @Override
212    public String toString() {
213        return "Domain matcher " + mRoot;
214    }
215}
216