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