1 /*
2  * Copyright (C) 2019 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 android.net.util;
18 
19 import android.icu.text.Transliterator;
20 import android.text.TextUtils;
21 import android.util.Log;
22 
23 import androidx.annotation.NonNull;
24 import androidx.annotation.Nullable;
25 import androidx.annotation.VisibleForTesting;
26 
27 import java.util.Collections;
28 import java.util.Enumeration;
29 import java.util.HashSet;
30 import java.util.Set;
31 
32 /**
33  * Transliterator to display a human-readable DHCP client hostname.
34  */
35 public class HostnameTransliterator {
36     private static final String TAG = "HostnameTransliterator";
37 
38     // Maximum length of hostname to be encoded in the DHCP message. Following RFC1035#2.3.4
39     // and this transliterator converts the device name to a single label, so the label length
40     // limit applies to the whole hostname.
41     private static final int MAX_DNS_LABEL_LENGTH = 63;
42 
43     @Nullable
44     private final Transliterator mTransliterator;
45 
HostnameTransliterator()46     public HostnameTransliterator() {
47         final Enumeration<String> availableIDs = Transliterator.getAvailableIDs();
48         final Set<String> actualIds = new HashSet<>(Collections.list(availableIDs));
49         final StringBuilder rules = new StringBuilder();
50         if (actualIds.contains("Any-ASCII")) {
51             rules.append(":: Any-ASCII; ");
52         } else if (actualIds.contains("Any-Latin") && actualIds.contains("Latin-ASCII")) {
53             rules.append(":: Any-Latin; :: Latin-ASCII; ");
54         } else {
55             Log.e(TAG, "ICU Transliterator doesn't include supported ID");
56             mTransliterator = null;
57             return;
58         }
59         mTransliterator = Transliterator.createFromRules("", rules.toString(),
60                 Transliterator.FORWARD);
61     }
62 
63     @VisibleForTesting
HostnameTransliterator(Transliterator transliterator)64     public HostnameTransliterator(Transliterator transliterator) {
65         mTransliterator = transliterator;
66     }
67 
68     // RFC952 and RFC1123 stipulates an valid hostname should be:
69     // 1. Only contain the alphabet (A-Z, a-z), digits (0-9), minus sign (-).
70     // 2. No blank or space characters are permitted as part of a name.
71     // 3. The first character must be an alpha character or digit.
72     // 4. The last character must not be a minus sign (-).
maybeRemoveRedundantSymbols(@onNull String string)73     private String maybeRemoveRedundantSymbols(@NonNull String string) {
74         String result = string.replaceAll("[^a-zA-Z0-9-]", "-");
75         result = result.replaceAll("-+", "-");
76         if (result.startsWith("-")) {
77             result = result.replaceFirst("-", "");
78         }
79         if (result.endsWith("-")) {
80             result = result.substring(0, result.length() - 1);
81         }
82         return result;
83     }
84 
85     /**
86      *  Transliterate the device name to valid hostname that could be human-readable string.
87      */
transliterate(@onNull String deviceName)88     public String transliterate(@NonNull String deviceName) {
89         if (deviceName == null) return null;
90         if (mTransliterator == null) {
91             if (!deviceName.matches("\\p{ASCII}*")) return null;
92             deviceName = maybeRemoveRedundantSymbols(deviceName);
93             if (TextUtils.isEmpty(deviceName)) return null;
94             return deviceName.length() > MAX_DNS_LABEL_LENGTH
95                     ? deviceName.substring(0, MAX_DNS_LABEL_LENGTH) : deviceName;
96         }
97 
98         String hostname = maybeRemoveRedundantSymbols(mTransliterator.transliterate(deviceName));
99         if (TextUtils.isEmpty(hostname)) return null;
100         return hostname.length() > MAX_DNS_LABEL_LENGTH
101                 ? hostname.substring(0, MAX_DNS_LABEL_LENGTH) : hostname;
102     }
103 }
104