1 /* 2 * Copyright (C) 2017 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.wifi.hotspot2.anqp; 18 19 import android.net.Uri; 20 import android.text.TextUtils; 21 22 import com.android.internal.annotations.VisibleForTesting; 23 import com.android.server.wifi.ByteBufferReader; 24 25 import java.net.ProtocolException; 26 import java.nio.BufferUnderflowException; 27 import java.nio.ByteBuffer; 28 import java.nio.ByteOrder; 29 import java.nio.charset.StandardCharsets; 30 import java.util.ArrayList; 31 import java.util.Collections; 32 import java.util.List; 33 import java.util.Locale; 34 import java.util.Objects; 35 36 /** 37 * The OSU Provider subfield in the OSU Providers List ANQP Element, 38 * Wi-Fi Alliance Hotspot 2.0 (Release 2) Technical Specification - Version 5.00, 39 * section 4.8.1 40 * 41 * Format: 42 * 43 * | Length | Friendly Name Length | Friendly Name #1 | ... | Friendly Name #n | 44 * 2 2 variable variable 45 * | Server URI length | Server URI | Method List Length | Method List | 46 * 1 variable 1 variable 47 * | Icon Available Length | Icon Available | NAI Length | NAI | Description Length | 48 * 2 variable 1 variable 2 49 * | Description #1 | ... | Description #n | 50 * variable variable 51 * 52 * | Operator Name Duple #N (optional) | 53 * variable 54 */ 55 public class OsuProviderInfo { 56 /** 57 * The raw payload should minimum include the following fields: 58 * - Friendly Name Length (2) 59 * - Server URI Length (1) 60 * - Method List Length (1) 61 * - Icon Available Length (2) 62 * - NAI Length (1) 63 * - Description Length (2) 64 */ 65 @VisibleForTesting 66 public static final int MINIMUM_LENGTH = 9; 67 68 /** 69 * Maximum octets for a I18N string. 70 */ 71 private static final int MAXIMUM_I18N_STRING_LENGTH = 252; 72 73 private final List<I18Name> mFriendlyNames; 74 private final Uri mServerUri; 75 private final List<Integer> mMethodList; 76 private final List<IconInfo> mIconInfoList; 77 private final String mNetworkAccessIdentifier; 78 private final List<I18Name> mServiceDescriptions; 79 80 @VisibleForTesting OsuProviderInfo(List<I18Name> friendlyNames, Uri serverUri, List<Integer> methodList, List<IconInfo> iconInfoList, String nai, List<I18Name> serviceDescriptions)81 public OsuProviderInfo(List<I18Name> friendlyNames, Uri serverUri, List<Integer> methodList, 82 List<IconInfo> iconInfoList, String nai, List<I18Name> serviceDescriptions) { 83 mFriendlyNames = friendlyNames; 84 mServerUri = serverUri; 85 mMethodList = methodList; 86 mIconInfoList = iconInfoList; 87 mNetworkAccessIdentifier = nai; 88 mServiceDescriptions = serviceDescriptions; 89 } 90 91 /** 92 * Parse a OsuProviderInfo from the given buffer. 93 * 94 * @param payload The buffer to read from 95 * @return {@link OsuProviderInfo} 96 * @throws BufferUnderflowException 97 * @throws ProtocolException 98 */ parse(ByteBuffer payload)99 public static OsuProviderInfo parse(ByteBuffer payload) 100 throws ProtocolException { 101 // Parse length field. 102 int length = (int) ByteBufferReader.readInteger(payload, ByteOrder.LITTLE_ENDIAN, 2) 103 & 0xFFFF; 104 if (length < MINIMUM_LENGTH) { 105 throw new ProtocolException("Invalid length value: " + length); 106 } 107 108 // Parse friendly names. 109 int friendlyNameLength = 110 (int) ByteBufferReader.readInteger(payload, ByteOrder.LITTLE_ENDIAN, 2) & 0xFFFF; 111 ByteBuffer friendlyNameBuffer = getSubBuffer(payload, friendlyNameLength); 112 List<I18Name> friendlyNameList = parseI18Names(friendlyNameBuffer); 113 114 // Parse server URI. 115 Uri serverUri = Uri.parse( 116 ByteBufferReader.readStringWithByteLength(payload, StandardCharsets.UTF_8)); 117 118 // Parse method list. 119 int methodListLength = payload.get() & 0xFF; 120 List<Integer> methodList = new ArrayList<>(); 121 while (methodListLength > 0) { 122 methodList.add(payload.get() & 0xFF); 123 methodListLength--; 124 } 125 126 // Parse list of icon info. 127 int availableIconLength = 128 (int) ByteBufferReader.readInteger(payload, ByteOrder.LITTLE_ENDIAN, 2) & 0xFFFF; 129 ByteBuffer iconBuffer = getSubBuffer(payload, availableIconLength); 130 List<IconInfo> iconInfoList = new ArrayList<>(); 131 while (iconBuffer.hasRemaining()) { 132 iconInfoList.add(IconInfo.parse(iconBuffer)); 133 } 134 135 // Parse Network Access Identifier. 136 String nai = ByteBufferReader.readStringWithByteLength(payload, StandardCharsets.UTF_8); 137 138 // Parse service descriptions. 139 int serviceDescriptionLength = 140 (int) ByteBufferReader.readInteger(payload, ByteOrder.LITTLE_ENDIAN, 2) & 0xFFFF; 141 ByteBuffer descriptionsBuffer = getSubBuffer(payload, serviceDescriptionLength); 142 List<I18Name> serviceDescriptionList = parseI18Names(descriptionsBuffer); 143 144 return new OsuProviderInfo(friendlyNameList, serverUri, methodList, iconInfoList, nai, 145 serviceDescriptionList); 146 } 147 getFriendlyNames()148 public List<I18Name> getFriendlyNames() { 149 return Collections.unmodifiableList(mFriendlyNames); 150 } 151 getServerUri()152 public Uri getServerUri() { 153 return mServerUri; 154 } 155 getMethodList()156 public List<Integer> getMethodList() { 157 return Collections.unmodifiableList(mMethodList); 158 } 159 getIconInfoList()160 public List<IconInfo> getIconInfoList() { 161 return Collections.unmodifiableList(mIconInfoList); 162 } 163 getNetworkAccessIdentifier()164 public String getNetworkAccessIdentifier() { 165 return mNetworkAccessIdentifier; 166 } 167 getServiceDescriptions()168 public List<I18Name> getServiceDescriptions() { 169 return Collections.unmodifiableList(mServiceDescriptions); 170 } 171 172 /** 173 * Return the friendly name string from the friendly name list. The string matching 174 * the default locale will be returned if it is found, otherwise the first name in the list 175 * will be returned. A null will be returned if the list is empty. 176 * 177 * @return friendly name string 178 */ getFriendlyName()179 public String getFriendlyName() { 180 return getI18String(mFriendlyNames); 181 } 182 183 /** 184 * Return the service description string from the service description list. The string 185 * matching the default locale will be returned if it is found, otherwise the first element in 186 * the list will be returned. A null will be returned if the list is empty. 187 * 188 * @return service description string 189 */ getServiceDescription()190 public String getServiceDescription() { 191 return getI18String(mServiceDescriptions); 192 } 193 194 @Override equals(Object thatObject)195 public boolean equals(Object thatObject) { 196 if (this == thatObject) { 197 return true; 198 } 199 if (!(thatObject instanceof OsuProviderInfo)) { 200 return false; 201 } 202 OsuProviderInfo that = (OsuProviderInfo) thatObject; 203 return (mFriendlyNames == null ? that.mFriendlyNames == null 204 : mFriendlyNames.equals(that.mFriendlyNames)) 205 && (mServerUri == null ? that.mServerUri == null 206 : mServerUri.equals(that.mServerUri)) 207 && (mMethodList == null ? that.mMethodList == null 208 : mMethodList.equals(that.mMethodList)) 209 && (mIconInfoList == null ? that.mIconInfoList == null 210 : mIconInfoList.equals(that.mIconInfoList)) 211 && TextUtils.equals(mNetworkAccessIdentifier, that.mNetworkAccessIdentifier) 212 && (mServiceDescriptions == null ? that.mServiceDescriptions == null 213 : mServiceDescriptions.equals(that.mServiceDescriptions)); 214 } 215 216 @Override hashCode()217 public int hashCode() { 218 return Objects.hash(mFriendlyNames, mServerUri, mMethodList, mIconInfoList, 219 mNetworkAccessIdentifier, mServiceDescriptions); 220 } 221 222 @Override toString()223 public String toString() { 224 return "OsuProviderInfo{" 225 + "mFriendlyNames=" + mFriendlyNames 226 + ", mServerUri=" + mServerUri 227 + ", mMethodList=" + mMethodList 228 + ", mIconInfoList=" + mIconInfoList 229 + ", mNetworkAccessIdentifier=" + mNetworkAccessIdentifier 230 + ", mServiceDescriptions=" + mServiceDescriptions 231 + "}"; 232 } 233 234 /** 235 * Parse list of I18N string from the given payload. 236 * 237 * @param payload The payload to parse from 238 * @return List of {@link I18Name} 239 * @throws ProtocolException 240 */ parseI18Names(ByteBuffer payload)241 private static List<I18Name> parseI18Names(ByteBuffer payload) throws ProtocolException { 242 List<I18Name> results = new ArrayList<>(); 243 while (payload.hasRemaining()) { 244 I18Name name = I18Name.parse(payload); 245 // Verify that the number of bytes for the operator name doesn't exceed the max 246 // allowed. 247 int textBytes = name.getText().getBytes(StandardCharsets.UTF_8).length; 248 if (textBytes > MAXIMUM_I18N_STRING_LENGTH) { 249 throw new ProtocolException("I18Name string exceeds the maximum allowed " 250 + textBytes); 251 } 252 results.add(name); 253 } 254 return results; 255 } 256 257 /** 258 * Creates a new byte buffer whose content is a shared subsequence of 259 * the given buffer's content. 260 * 261 * The sub buffer will starts from |payload|'s current position 262 * and ends at |payload|'s current position plus |length|. The |payload|'s current 263 * position will advance pass |length| bytes. 264 * 265 * @param payload The original buffer 266 * @param length The length of the new buffer 267 * @return {@link ByteBuffer} 268 * @throws BufferUnderflowException 269 */ getSubBuffer(ByteBuffer payload, int length)270 private static ByteBuffer getSubBuffer(ByteBuffer payload, int length) { 271 if (payload.remaining() < length) { 272 throw new BufferUnderflowException(); 273 } 274 // Set the subBuffer's starting and ending position. 275 ByteBuffer subBuffer = payload.slice(); 276 subBuffer.limit(length); 277 // Advance the original buffer's current position. 278 payload.position(payload.position() + length); 279 return subBuffer; 280 } 281 282 /** 283 * Return the appropriate I18 string value from the list of I18 string values. 284 * The string matching the default locale will be returned if it is found, otherwise the 285 * first string in the list will be returned. A null will be returned if the list is empty. 286 * 287 * @param i18Strings List of I18 string values 288 * @return String matching the default locale, null otherwise 289 */ getI18String(List<I18Name> i18Strings)290 private static String getI18String(List<I18Name> i18Strings) { 291 for (I18Name name : i18Strings) { 292 if (name.getLanguage().equals(Locale.getDefault().getLanguage())) { 293 return name.getText(); 294 } 295 } 296 if (i18Strings.size() > 0) { 297 return i18Strings.get(0).getText(); 298 } 299 return null; 300 } 301 } 302