diff options
author | Peter Qiu <zqiu@google.com> | 2016-12-05 16:11:37 -0800 |
---|---|---|
committer | Peter Qiu <zqiu@google.com> | 2016-12-15 15:37:19 -0800 |
commit | 74339de52d7066f22771d914e698da503232c107 (patch) | |
tree | f38b4466b773e40a6ddfb6eec9c2bdc2084542e9 /service | |
parent | fa04a81daf829e6e5c099c9a249b8dd8dd112102 (diff) |
hotspot2: ANQP elements cleanup Part 1
Cleanup and add unit tests for the following ANQP elements (and
the underlying classes used by those elements):
- HSFriendNameElement
- IPAddressTypeAvailabilityElement
- RoamingConsortiumElement
- VenueNameElement
The cleanup included using a static #parse function for parsing
raw bytes into an element object, the new ByteBufferReader APIs
for reading integer and string from ByteBuffer, and documented
possible runtime exceptions.
Additional changes include:
- remove the unnecessary setting of byte order for
the ByteBuffer, since we're not using the ByteBuffer's APIs
for reading integer values (all reads are either byte or byte
array).
- remove the unused functions in ANQPFactory
More ANQP elements cleanup will be done in the upcoming CLs.
Bug: 33000864
Test: frameworks/opt/net/wifi/tests/wifitests/runtests.sh
Change-Id: I6da918c83722d5c0ca7a2374ff5fa5f630cdea6d
Diffstat (limited to 'service')
8 files changed, 428 insertions, 295 deletions
diff --git a/service/java/com/android/server/wifi/hotspot2/PasspointEventHandler.java b/service/java/com/android/server/wifi/hotspot2/PasspointEventHandler.java index 1699625e3..3c6237fb2 100644 --- a/service/java/com/android/server/wifi/hotspot2/PasspointEventHandler.java +++ b/service/java/com/android/server/wifi/hotspot2/PasspointEventHandler.java @@ -19,17 +19,17 @@ package com.android.server.wifi.hotspot2; import android.util.Base64; import android.util.Log; +import com.android.server.wifi.WifiNative; import com.android.server.wifi.hotspot2.anqp.ANQPElement; import com.android.server.wifi.hotspot2.anqp.ANQPFactory; import com.android.server.wifi.hotspot2.anqp.Constants; -import com.android.server.wifi.WifiNative; import java.io.BufferedReader; import java.io.IOException; import java.io.StringReader; import java.net.ProtocolException; +import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; -import java.nio.ByteOrder; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -122,10 +122,9 @@ public class PasspointEventHandler { if (element != null) { elements.put(element.getID(), element); } - } - catch (ProtocolException pe) { + } catch (ProtocolException | BufferUnderflowException e) { Log.e(Utils.hs2LogTag(PasspointEventHandler.class), - "Failed to parse ANQP: " + pe); + "Failed to parse ANQP: " + e); } } return elements; @@ -184,14 +183,9 @@ public class PasspointEventHandler { Log.d(Utils.hs2LogTag(getClass()), String.format("Successful ANQP response for %012x: %s", bssid, elements)); - } - catch (IOException ioe) { + } catch (IOException | BufferUnderflowException e) { Log.e(Utils.hs2LogTag(getClass()), "Failed to parse ANQP: " + - ioe.toString() + ": " + bssData); - } - catch (RuntimeException rte) { - Log.e(Utils.hs2LogTag(getClass()), "Failed to parse ANQP: " + - rte.toString() + ": " + bssData, rte); + e.toString() + ": " + bssData); } } mCallbacks.onANQPResponse(bssid, elements); @@ -312,10 +306,12 @@ public class PasspointEventHandler { "Failed to parse hex string"); return null; } + // Wrap the payload inside a ByteBuffer. + ByteBuffer buffer = ByteBuffer.wrap(payload); + return Constants.getANQPElementID(elementType) != null ? - ANQPFactory.buildElement(ByteBuffer.wrap(payload), elementType, payload.length) : - ANQPFactory.buildHS20Element(elementType, - ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN)); + ANQPFactory.buildElement(elementType, buffer) : + ANQPFactory.buildHS20Element(elementType, buffer); } private byte[] retrieveIcon(IconEvent iconEvent) throws IOException { diff --git a/service/java/com/android/server/wifi/hotspot2/PasspointMatchInfo.java b/service/java/com/android/server/wifi/hotspot2/PasspointMatchInfo.java index a759b0619..9f26675ad 100644 --- a/service/java/com/android/server/wifi/hotspot2/PasspointMatchInfo.java +++ b/service/java/com/android/server/wifi/hotspot2/PasspointMatchInfo.java @@ -6,10 +6,7 @@ import com.android.server.wifi.hotspot2.anqp.Constants.ANQPElementType; import com.android.server.wifi.hotspot2.anqp.HSConnectionCapabilityElement; import com.android.server.wifi.hotspot2.anqp.HSWanMetricsElement; import com.android.server.wifi.hotspot2.anqp.IPAddressTypeAvailabilityElement; -import com.android.server.wifi.hotspot2.anqp.IPAddressTypeAvailabilityElement.IPv4Availability; -import com.android.server.wifi.hotspot2.anqp.IPAddressTypeAvailabilityElement.IPv6Availability; -import java.util.EnumMap; import java.util.HashMap; import java.util.Map; @@ -21,10 +18,8 @@ public class PasspointMatchInfo implements Comparable<PasspointMatchInfo> { private final ScanDetail mScanDetail; private final int mScore; - private static final Map<IPv4Availability, Integer> sIP4Scores = - new EnumMap<>(IPv4Availability.class); - private static final Map<IPv6Availability, Integer> sIP6Scores = - new EnumMap<>(IPv6Availability.class); + private static final Map<Integer, Integer> sIP4Scores = new HashMap<>(); + private static final Map<Integer, Integer> sIP6Scores = new HashMap<>(); private static final Map<Integer, Map<Integer, Integer>> sPortScores = new HashMap<>(); @@ -46,19 +41,18 @@ public class PasspointMatchInfo implements Comparable<PasspointMatchInfo> { sAntScores.put(NetworkDetail.Ant.Wildcard, 1); sAntScores.put(NetworkDetail.Ant.TestOrExperimental, 0); - sIP4Scores.put(IPv4Availability.NotAvailable, 0); - sIP4Scores.put(IPv4Availability.PortRestricted, 1); - sIP4Scores.put(IPv4Availability.PortRestrictedAndSingleNAT, 1); - sIP4Scores.put(IPv4Availability.PortRestrictedAndDoubleNAT, 1); - sIP4Scores.put(IPv4Availability.Unknown, 1); - sIP4Scores.put(IPv4Availability.Public, 2); - sIP4Scores.put(IPv4Availability.SingleNAT, 2); - sIP4Scores.put(IPv4Availability.DoubleNAT, 2); - - sIP6Scores.put(IPv6Availability.NotAvailable, 0); - sIP6Scores.put(IPv6Availability.Reserved, 1); - sIP6Scores.put(IPv6Availability.Unknown, 1); - sIP6Scores.put(IPv6Availability.Available, 2); + sIP4Scores.put(IPAddressTypeAvailabilityElement.IPV4_NOT_AVAILABLE, 0); + sIP4Scores.put(IPAddressTypeAvailabilityElement.IPV4_PORT_RESTRICTED, 1); + sIP4Scores.put(IPAddressTypeAvailabilityElement.IPV4_PORT_RESTRICTED_AND_SINGLE_NAT, 1); + sIP4Scores.put(IPAddressTypeAvailabilityElement.IPV4_PORT_RESTRICTED_AND_DOUBLE_NAT, 1); + sIP4Scores.put(IPAddressTypeAvailabilityElement.IPV4_UNKNOWN, 1); + sIP4Scores.put(IPAddressTypeAvailabilityElement.IPV4_PUBLIC, 2); + sIP4Scores.put(IPAddressTypeAvailabilityElement.IPV4_SINGLE_NAT, 2); + sIP4Scores.put(IPAddressTypeAvailabilityElement.IPV4_DOUBLE_NAT, 2); + + sIP6Scores.put(IPAddressTypeAvailabilityElement.IPV6_NOT_AVAILABLE, 0); + sIP6Scores.put(IPAddressTypeAvailabilityElement.IPV6_UNKNOWN, 1); + sIP6Scores.put(IPAddressTypeAvailabilityElement.IPV6_AVAILABLE, 2); Map<Integer, Integer> tcpMap = new HashMap<>(); tcpMap.put(20, 1); diff --git a/service/java/com/android/server/wifi/hotspot2/anqp/ANQPFactory.java b/service/java/com/android/server/wifi/hotspot2/anqp/ANQPFactory.java index 2f4430f67..6944a6e00 100644 --- a/service/java/com/android/server/wifi/hotspot2/anqp/ANQPFactory.java +++ b/service/java/com/android/server/wifi/hotspot2/anqp/ANQPFactory.java @@ -3,15 +3,11 @@ package com.android.server.wifi.hotspot2.anqp; import com.android.server.wifi.hotspot2.NetworkDetail; import java.net.ProtocolException; +import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; -import java.util.ListIterator; -import java.util.Set; /** * Factory to build a collection of 802.11u ANQP elements from a byte buffer. @@ -73,192 +69,79 @@ public class ANQPFactory { return querySet; } - public static ByteBuffer buildQueryRequest(Set<Constants.ANQPElementType> elements, - ByteBuffer target) { - List<Constants.ANQPElementType> list = new ArrayList<Constants.ANQPElementType>(elements); - Collections.sort(list); - - ListIterator<Constants.ANQPElementType> elementIterator = list.listIterator(); - - target.order(ByteOrder.LITTLE_ENDIAN); - target.putShort((short) Constants.ANQP_QUERY_LIST); - int lenPos = target.position(); - target.putShort((short) 0); - - while (elementIterator.hasNext()) { - Integer id = Constants.getANQPElementID(elementIterator.next()); - if (id != null) { - target.putShort(id.shortValue()); - } else { - elementIterator.previous(); - break; - } - } - target.putShort(lenPos, (short) (target.position() - lenPos - Constants.BYTES_IN_SHORT)); - - // Start a new vendor specific element for HS2.0 elements: - if (elementIterator.hasNext()) { - target.putShort((short) Constants.ANQP_VENDOR_SPEC); - int vsLenPos = target.position(); - target.putShort((short) 0); - - target.putInt(Constants.HS20_PREFIX); - target.put((byte) Constants.HS_QUERY_LIST); - target.put((byte) 0); - - while (elementIterator.hasNext()) { - Constants.ANQPElementType elementType = elementIterator.next(); - Integer id = Constants.getHS20ElementID(elementType); - if (id == null) { - throw new RuntimeException("Unmapped ANQPElementType: " + elementType); + /** + * Build an ANQP element from the pass-in byte buffer. + * + * Note: Each Hotspot 2.0 Release 2 element will be wrapped inside a Vendor Specific element + * in the ANQP response from the AP. However, the lower layer (e.g. wpa_supplicant) should + * already take care of parsing those elements out of Vendor Specific elements. To be safe, + * we will parse the Vendor Specific elements for non-Hotspot 2.0 Release elements or in + * the case they're not parsed by the lower layer. + * + * @param infoID The ANQP element type + * @param payload The buffer to read from + * @return {@link com.android.server.wifi.hotspot2.anqp.ANQPElement} + * @throws BufferUnderflowException + * @throws ProtocolException + */ + public static ANQPElement buildElement(Constants.ANQPElementType infoID, ByteBuffer payload) + throws ProtocolException { + switch (infoID) { + case ANQPVenueName: + return VenueNameElement.parse(payload); + case ANQPRoamingConsortium: + return RoamingConsortiumElement.parse(payload); + case ANQPIPAddrAvailability: + return IPAddressTypeAvailabilityElement.parse(payload); + case ANQPNAIRealm: + return new NAIRealmElement(infoID, payload); + case ANQP3GPPNetwork: + return new ThreeGPPNetworkElement(infoID, payload); + case ANQPDomName: + return new DomainNameElement(infoID, payload); + case ANQPVendorSpec: + if (payload.remaining() > 5) { + int oi = payload.getInt(); + if (oi != Constants.HS20_PREFIX) { + return null; + } + int subType = payload.get() & Constants.BYTE_MASK; + Constants.ANQPElementType hs20ID = Constants.mapHS20Element(subType); + if (hs20ID == null) { + throw new ProtocolException("Bad HS20 info ID: " + subType); + } + payload.get(); // Skip the reserved octet + return buildHS20Element(hs20ID, payload); } else { - target.put(id.byteValue()); + return new GenericBlobElement(infoID, payload); } - } - target.putShort(vsLenPos, - (short) (target.position() - vsLenPos - Constants.BYTES_IN_SHORT)); - } - - target.flip(); - return target; - } - - public static ByteBuffer buildHomeRealmRequest(List<String> realmNames, ByteBuffer target) { - target.order(ByteOrder.LITTLE_ENDIAN); - target.putShort((short) Constants.ANQP_VENDOR_SPEC); - int lenPos = target.position(); - target.putShort((short) 0); - - target.putInt(Constants.HS20_PREFIX); - target.put((byte) Constants.HS_NAI_HOME_REALM_QUERY); - target.put((byte) 0); - - target.put((byte) realmNames.size()); - for (String realmName : realmNames) { - target.put((byte) Constants.UTF8_INDICATOR); - byte[] octets = realmName.getBytes(StandardCharsets.UTF_8); - target.put((byte) octets.length); - target.put(octets); - } - target.putShort(lenPos, (short) (target.position() - lenPos - Constants.BYTES_IN_SHORT)); - - target.flip(); - return target; - } - - public static ByteBuffer buildIconRequest(String fileName, ByteBuffer target) { - target.order(ByteOrder.LITTLE_ENDIAN); - target.putShort((short) Constants.ANQP_VENDOR_SPEC); - int lenPos = target.position(); - target.putShort((short) 0); - - target.putInt(Constants.HS20_PREFIX); - target.put((byte) Constants.HS_ICON_REQUEST); - target.put((byte) 0); - - target.put(fileName.getBytes(StandardCharsets.UTF_8)); - target.putShort(lenPos, (short) (target.position() - lenPos - Constants.BYTES_IN_SHORT)); - - target.flip(); - return target; - } - - public static List<ANQPElement> parsePayload(ByteBuffer payload) throws ProtocolException { - payload.order(ByteOrder.LITTLE_ENDIAN); - List<ANQPElement> elements = new ArrayList<ANQPElement>(); - while (payload.hasRemaining()) { - elements.add(buildElement(payload)); - } - return elements; - } - - private static ANQPElement buildElement(ByteBuffer payload) throws ProtocolException { - if (payload.remaining() < 4) - throw new ProtocolException("Runt payload: " + payload.remaining()); - - int infoIDNumber = payload.getShort() & Constants.SHORT_MASK; - Constants.ANQPElementType infoID = Constants.mapANQPElement(infoIDNumber); - if (infoID == null) { - throw new ProtocolException("Bad info ID: " + infoIDNumber); - } - int length = payload.getShort() & Constants.SHORT_MASK; - - if (payload.remaining() < length) { - throw new ProtocolException("Truncated payload: " + - payload.remaining() + " vs " + length); - } - return buildElement(payload, infoID, length); - } - - public static ANQPElement buildElement(ByteBuffer payload, Constants.ANQPElementType infoID, - int length) throws ProtocolException { - try { - ByteBuffer elementPayload = payload.duplicate().order(ByteOrder.LITTLE_ENDIAN); - payload.position(payload.position() + length); - elementPayload.limit(elementPayload.position() + length); - - switch (infoID) { - case ANQPVenueName: - return new VenueNameElement(infoID, elementPayload); - case ANQPRoamingConsortium: - return new RoamingConsortiumElement(infoID, elementPayload); - case ANQPIPAddrAvailability: - return new IPAddressTypeAvailabilityElement(infoID, elementPayload); - case ANQPNAIRealm: - return new NAIRealmElement(infoID, elementPayload); - case ANQP3GPPNetwork: - return new ThreeGPPNetworkElement(infoID, elementPayload); - case ANQPDomName: - return new DomainNameElement(infoID, elementPayload); - case ANQPVendorSpec: - if (elementPayload.remaining() > 5) { - int oi = elementPayload.getInt(); - if (oi != Constants.HS20_PREFIX) { - return null; - } - int subType = elementPayload.get() & Constants.BYTE_MASK; - Constants.ANQPElementType hs20ID = Constants.mapHS20Element(subType); - if (hs20ID == null) { - throw new ProtocolException("Bad HS20 info ID: " + subType); - } - elementPayload.get(); // Skip the reserved octet - return buildHS20Element(hs20ID, elementPayload); - } else { - return new GenericBlobElement(infoID, elementPayload); - } - default: - throw new ProtocolException("Unknown element ID: " + infoID); - } - } catch (ProtocolException e) { - throw e; - } catch (Exception e) { - // TODO: remove this catch-all for exceptions, once the element parsing code - // has been thoroughly unit tested. b/30562650 - throw new ProtocolException("Unknown parsing error", e); + default: + throw new ProtocolException("Unknown element ID: " + infoID); } } + /** + * Build a Hotspot 2.0 Release 2 ANQP element from the pass-in byte buffer. + * + * @param infoID The ANQP element ID + * @param payload The buffer to read from + * @return {@link com.android.server.wifi.hotspot2.anqp.ANQPElement} + * @throws BufferUnderflowException + * @throws ProtocolException + */ public static ANQPElement buildHS20Element(Constants.ANQPElementType infoID, - ByteBuffer payload) throws ProtocolException { - try { - switch (infoID) { - case HSFriendlyName: - return new HSFriendlyNameElement(infoID, payload); - case HSWANMetrics: - return new HSWanMetricsElement(infoID, payload); - case HSConnCapability: - return new HSConnectionCapabilityElement(infoID, payload); - case HSOSUProviders: - return new RawByteElement(infoID, payload); - default: - return null; - } - } catch (ProtocolException e) { - throw e; - } catch (Exception e) { - // TODO: remove this catch-all for exceptions, once the element parsing code - // has been thoroughly unit tested. b/30562650 - throw new ProtocolException("Unknown parsing error", e); + ByteBuffer payload) throws ProtocolException { + switch (infoID) { + case HSFriendlyName: + return HSFriendlyNameElement.parse(payload); + case HSWANMetrics: + return new HSWanMetricsElement(infoID, payload); + case HSConnCapability: + return new HSConnectionCapabilityElement(infoID, payload); + case HSOSUProviders: + return new RawByteElement(infoID, payload); + default: + throw new ProtocolException("Unknown element ID: " + infoID); } } } diff --git a/service/java/com/android/server/wifi/hotspot2/anqp/HSFriendlyNameElement.java b/service/java/com/android/server/wifi/hotspot2/anqp/HSFriendlyNameElement.java index 3ba170e42..c6794c86e 100644 --- a/service/java/com/android/server/wifi/hotspot2/anqp/HSFriendlyNameElement.java +++ b/service/java/com/android/server/wifi/hotspot2/anqp/HSFriendlyNameElement.java @@ -1,7 +1,11 @@ package com.android.server.wifi.hotspot2.anqp; +import com.android.internal.annotations.VisibleForTesting; + import java.net.ProtocolException; +import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -9,20 +13,55 @@ import java.util.List; /** * The Operator Friendly Name vendor specific ANQP Element, * Wi-Fi Alliance Hotspot 2.0 (Release 2) Technical Specification - Version 5.00, - * section 4.3 + * section 4.3. + * + * Format: + * + * | Operator Name Duple #1 (optional) | ... + * variable + * + * | Operator Name Duple #N (optional) | + * variable */ public class HSFriendlyNameElement extends ANQPElement { - private final List<I18Name> mNames; + /** + * Maximum length for an Operator Name. Refer to Hotspot 2.0 (Release 2) Technical + * Specification section 4.3 for more info. + */ + @VisibleForTesting + public static final int MAXIMUM_OPERATOR_NAME_LENGTH = 252; - public HSFriendlyNameElement(Constants.ANQPElementType infoID, ByteBuffer payload) - throws ProtocolException { - super(infoID); + private final List<I18Name> mNames; - mNames = new ArrayList<I18Name>(); + @VisibleForTesting + public HSFriendlyNameElement(List<I18Name> names) { + super(Constants.ANQPElementType.HSFriendlyName); + mNames = names; + } + /** + * Parse a HSFriendlyNameElement from the given buffer. + * + * @param payload The buffer to read from + * @return {@link HSFriendlyNameElement} + * @throws BufferUnderflowException + * @throws ProtocolException + */ + public static HSFriendlyNameElement parse(ByteBuffer payload) + throws ProtocolException { + List<I18Name> names = new ArrayList<I18Name>(); while (payload.hasRemaining()) { - mNames.add(new I18Name(payload)); + I18Name name = I18Name.parse(payload); + // Verify that the number of bytes for the operator name doesn't exceed the max + // allowed. + int textBytes = name.getText().getBytes(StandardCharsets.UTF_8).length; + if (textBytes > MAXIMUM_OPERATOR_NAME_LENGTH) { + throw new ProtocolException("Operator Name exceeds the maximum allowed " + + textBytes); + } + names.add(name); } + return new HSFriendlyNameElement(names); } public List<I18Name> getNames() { @@ -30,6 +69,23 @@ public class HSFriendlyNameElement extends ANQPElement { } @Override + public boolean equals(Object thatObject) { + if (this == thatObject) { + return true; + } + if (!(thatObject instanceof HSFriendlyNameElement)) { + return false; + } + HSFriendlyNameElement that = (HSFriendlyNameElement) thatObject; + return mNames.equals(that.mNames); + } + + @Override + public int hashCode() { + return mNames.hashCode(); + } + + @Override public String toString() { return "HSFriendlyName{" + "mNames=" + mNames + diff --git a/service/java/com/android/server/wifi/hotspot2/anqp/I18Name.java b/service/java/com/android/server/wifi/hotspot2/anqp/I18Name.java index 81ebdfa2a..3d44b0beb 100644 --- a/service/java/com/android/server/wifi/hotspot2/anqp/I18Name.java +++ b/service/java/com/android/server/wifi/hotspot2/anqp/I18Name.java @@ -16,19 +16,20 @@ package com.android.server.wifi.hotspot2.anqp; -import static com.android.server.wifi.hotspot2.anqp.Constants.BYTE_MASK; +import android.text.TextUtils; import com.android.internal.annotations.VisibleForTesting; import com.android.server.wifi.ByteBufferReader; import java.net.ProtocolException; +import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.Locale; /** * A generic Internationalized name field used in the Operator Friendly Name ANQP element - * (see HS2.0 R2 Spec 4.2), and the Venue Name ANQP element (see 802.11-2012 8.4.4.4). + * (see HS2.0 R2 Spec 4.2) and the Venue Name ANQP element (see 802.11-2012 8.4.4.4). * * Format: * @@ -36,26 +37,16 @@ import java.util.Locale; * 1 3 variable */ public class I18Name { + @VisibleForTesting + public static final int LANGUAGE_CODE_LENGTH = 3; + + @VisibleForTesting + public static final int MINIMUM_LENGTH = LANGUAGE_CODE_LENGTH; + private final String mLanguage; private final Locale mLocale; private final String mText; - public I18Name(ByteBuffer payload) throws ProtocolException { - if (payload.remaining() < Constants.LANG_CODE_LENGTH + 1) { - throw new ProtocolException("Truncated I18Name payload of length " - + payload.remaining()); - } - int length = payload.get() & BYTE_MASK; - if (length < Constants.LANG_CODE_LENGTH || length > payload.remaining()) { - throw new ProtocolException("Invalid I18Name length field value " + length); - } - mLanguage = ByteBufferReader.readString( - payload, Constants.LANG_CODE_LENGTH, StandardCharsets.US_ASCII).trim(); - mLocale = Locale.forLanguageTag(mLanguage); - mText = ByteBufferReader.readString(payload, length - Constants.LANG_CODE_LENGTH, - StandardCharsets.UTF_8); - } - @VisibleForTesting public I18Name(String language, Locale locale, String text) { mLanguage = language; @@ -63,6 +54,34 @@ public class I18Name { mText = text; } + /** + * Parse a I18Name from the given buffer. + * + * @param payload The byte buffer to read from + * @return {@link I18Name} + * @throws BufferUnderflowException + * @throws ProtocolException + */ + public static I18Name parse(ByteBuffer payload) throws ProtocolException { + // Retrieve the length field. + int length = payload.get() & 0xFF; + + // Check for the minimum required length. + if (length < MINIMUM_LENGTH) { + throw new ProtocolException("Invalid length: " + length); + } + + // Read the language string. + String language = ByteBufferReader.readString( + payload, LANGUAGE_CODE_LENGTH, StandardCharsets.US_ASCII).trim(); + Locale locale = Locale.forLanguageTag(language); + + // Read the text string. + String text = ByteBufferReader.readString(payload, length - LANGUAGE_CODE_LENGTH, + StandardCharsets.UTF_8); + return new I18Name(language, locale, text); + } + public String getLanguage() { return mLanguage; } @@ -80,12 +99,13 @@ public class I18Name { if (this == thatObject) { return true; } - if (thatObject == null || getClass() != thatObject.getClass()) { + if (!(thatObject instanceof I18Name)) { return false; } I18Name that = (I18Name) thatObject; - return mLanguage.equals(that.mLanguage) && mText.equals(that.mText); + return TextUtils.equals(mLanguage, that.mLanguage) + && TextUtils.equals(mText, that.mText); } @Override diff --git a/service/java/com/android/server/wifi/hotspot2/anqp/IPAddressTypeAvailabilityElement.java b/service/java/com/android/server/wifi/hotspot2/anqp/IPAddressTypeAvailabilityElement.java index 3f9737ded..ed8f8c19f 100644 --- a/service/java/com/android/server/wifi/hotspot2/anqp/IPAddressTypeAvailabilityElement.java +++ b/service/java/com/android/server/wifi/hotspot2/anqp/IPAddressTypeAvailabilityElement.java @@ -1,48 +1,145 @@ package com.android.server.wifi.hotspot2.anqp; +import com.android.internal.annotations.VisibleForTesting; + import java.net.ProtocolException; import java.nio.ByteBuffer; +import java.util.HashSet; +import java.util.Set; /** * The IP Address Type availability ANQP Element, IEEE802.11-2012 section 8.4.4.9 + * + * Format: + * + * | IP Address | + * 1 + * b0 b7 + * | IPv6 Address | IPv4 Address | + * 2 bits 6 bits + * + * IPv4 Address field values: + * 0 - Address type not available + * 1 - Public IPv4 address available + * 2 - Port-restricted IPv4 address available + * 3 - Single NATed private IPv4 address available + * 4 - Single NATed private IPv4 address available + * 5 - Port-restricted IPv4 address and single NATed IPv4 address available + * 6 - Port-restricted IPv4 address and double NATed IPv4 address available + * 7 - Availability of the address type is not known + * + * IPv6 Address field values: + * 0 - Address type not available + * 1 - Address type not available + * 2 - Availability of the address type not known + * */ public class IPAddressTypeAvailabilityElement extends ANQPElement { - public enum IPv4Availability { - NotAvailable, Public, PortRestricted, SingleNAT, DoubleNAT, - PortRestrictedAndSingleNAT, PortRestrictedAndDoubleNAT, Unknown + @VisibleForTesting + public static final int EXPECTED_BUFFER_LENGTH = 1; + + /** + * Constants for IPv4 availability. + */ + public static final int IPV4_NOT_AVAILABLE = 0; + public static final int IPV4_PUBLIC = 1; + public static final int IPV4_PORT_RESTRICTED = 2; + public static final int IPV4_SINGLE_NAT = 3; + public static final int IPV4_DOUBLE_NAT = 4; + public static final int IPV4_PORT_RESTRICTED_AND_SINGLE_NAT = 5; + public static final int IPV4_PORT_RESTRICTED_AND_DOUBLE_NAT = 6; + public static final int IPV4_UNKNOWN = 7; + private static final Set<Integer> IPV4_AVAILABILITY = new HashSet<Integer>(); + static { + IPV4_AVAILABILITY.add(IPV4_NOT_AVAILABLE); + IPV4_AVAILABILITY.add(IPV4_PUBLIC); + IPV4_AVAILABILITY.add(IPV4_PORT_RESTRICTED); + IPV4_AVAILABILITY.add(IPV4_SINGLE_NAT); + IPV4_AVAILABILITY.add(IPV4_DOUBLE_NAT); + IPV4_AVAILABILITY.add(IPV4_PORT_RESTRICTED_AND_SINGLE_NAT); + IPV4_AVAILABILITY.add(IPV4_PORT_RESTRICTED_AND_DOUBLE_NAT); + } + + /** + * Constants for IPv6 availability. + */ + public static final int IPV6_NOT_AVAILABLE = 0; + public static final int IPV6_AVAILABLE = 1; + public static final int IPV6_UNKNOWN = 2; + private static final Set<Integer> IPV6_AVAILABILITY = new HashSet<Integer>(); + static { + IPV6_AVAILABILITY.add(IPV6_NOT_AVAILABLE); + IPV6_AVAILABILITY.add(IPV6_AVAILABLE); + IPV6_AVAILABILITY.add(IPV6_UNKNOWN); } - public enum IPv6Availability {NotAvailable, Available, Unknown, Reserved} + private static final int IPV4_AVAILABILITY_MASK = 0x3F; + private static final int IPV6_AVAILABILITY_MASK = 0x3; - private final IPv4Availability mV4Availability; - private final IPv6Availability mV6Availability; + private final int mV4Availability; + private final int mV6Availability; - public IPAddressTypeAvailabilityElement(Constants.ANQPElementType infoID, ByteBuffer payload) + @VisibleForTesting + public IPAddressTypeAvailabilityElement(int v4Availability, int v6Availability) { + super(Constants.ANQPElementType.ANQPIPAddrAvailability); + mV4Availability = v4Availability; + mV6Availability = v6Availability; + } + + /** + * Parse an IPAddressTypeAvailabilityElement from the given buffer. + * + * @param payload The byte buffer to read from + * @return {@link IPAddressTypeAvailabilityElement} + * @throws ProtocolException + */ + public static IPAddressTypeAvailabilityElement parse(ByteBuffer payload) throws ProtocolException { - super(infoID); + if (payload.remaining() != EXPECTED_BUFFER_LENGTH) { + throw new ProtocolException("Unexpected buffer length: " + payload.remaining()); + } + + int ipField = payload.get() & 0xFF; - if (payload.remaining() != 1) - throw new ProtocolException("Bad IP Address Type Availability length: " + - payload.remaining()); + int v6Availability = ipField & IPV6_AVAILABILITY_MASK; + if (!IPV6_AVAILABILITY.contains(v6Availability)) { + v6Availability = IPV6_UNKNOWN; + } - int ipField = payload.get(); - mV6Availability = IPv6Availability.values()[ipField & 0x3]; + int v4Availability = (ipField >> 2) & IPV4_AVAILABILITY_MASK; + if (!IPV4_AVAILABILITY.contains(v4Availability)) { + v4Availability = IPV4_UNKNOWN; + } - ipField = (ipField >> 2) & 0x3f; - mV4Availability = ipField < IPv4Availability.values().length ? - IPv4Availability.values()[ipField] : - IPv4Availability.Unknown; + return new IPAddressTypeAvailabilityElement(v4Availability, v6Availability); } - public IPv4Availability getV4Availability() { + public int getV4Availability() { return mV4Availability; } - public IPv6Availability getV6Availability() { + public int getV6Availability() { return mV6Availability; } @Override + public boolean equals(Object thatObject) { + if (this == thatObject) { + return true; + } + if (!(thatObject instanceof IPAddressTypeAvailabilityElement)) { + return false; + } + IPAddressTypeAvailabilityElement that = (IPAddressTypeAvailabilityElement) thatObject; + return mV4Availability == that.mV4Availability && mV6Availability == that.mV6Availability; + } + + @Override + public int hashCode() { + return mV4Availability << 2 + mV6Availability; + } + + @Override public String toString() { return "IPAddressTypeAvailability{" + "mV4Availability=" + mV4Availability + diff --git a/service/java/com/android/server/wifi/hotspot2/anqp/RoamingConsortiumElement.java b/service/java/com/android/server/wifi/hotspot2/anqp/RoamingConsortiumElement.java index 430126070..a40e9d63b 100644 --- a/service/java/com/android/server/wifi/hotspot2/anqp/RoamingConsortiumElement.java +++ b/service/java/com/android/server/wifi/hotspot2/anqp/RoamingConsortiumElement.java @@ -1,46 +1,88 @@ package com.android.server.wifi.hotspot2.anqp; +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.wifi.ByteBufferReader; import com.android.server.wifi.hotspot2.Utils; import java.net.ProtocolException; +import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import static com.android.server.wifi.hotspot2.anqp.Constants.BYTE_MASK; - -import com.android.server.wifi.ByteBufferReader; - /** * The Roaming Consortium ANQP Element, IEEE802.11-2012 section 8.4.4.7 + * + ** Format: + * + * | OI Duple #1 (optional) | ... + * variable + * + * | OI Length | OI | + * 1 variable + * */ public class RoamingConsortiumElement extends ANQPElement { + @VisibleForTesting + public static final int MINIMUM_OI_LENGTH = Byte.BYTES; - private final List<Long> mOis; + @VisibleForTesting + public static final int MAXIMUM_OI_LENGTH = Long.BYTES; - public RoamingConsortiumElement(Constants.ANQPElementType infoID, ByteBuffer payload) - throws ProtocolException { - super(infoID); + private final List<Long> mOIs; - mOis = new ArrayList<Long>(); + @VisibleForTesting + public RoamingConsortiumElement(List<Long> ois) { + super(Constants.ANQPElementType.ANQPRoamingConsortium); + mOIs = ois; + } + /** + * Parse a VenueNameElement from the given payload. + * + * @param payload The byte buffer to read from + * @return {@link RoamingConsortiumElement} + * @throws BufferUnderflowException + * @throws ProtocolException + */ + public static RoamingConsortiumElement parse(ByteBuffer payload) + throws ProtocolException { + List<Long> OIs = new ArrayList<Long>(); while (payload.hasRemaining()) { - int length = payload.get() & BYTE_MASK; - if (length > payload.remaining()) { + int length = payload.get() & 0xFF; + if (length < MINIMUM_OI_LENGTH || length > MAXIMUM_OI_LENGTH) { throw new ProtocolException("Bad OI length: " + length); } - mOis.add(ByteBufferReader.readInteger(payload, ByteOrder.BIG_ENDIAN, length)); + OIs.add(ByteBufferReader.readInteger(payload, ByteOrder.BIG_ENDIAN, length)); } + return new RoamingConsortiumElement(OIs); } public List<Long> getOIs() { - return Collections.unmodifiableList(mOis); + return Collections.unmodifiableList(mOIs); + } + + @Override + public boolean equals(Object thatObject) { + if (this == thatObject) { + return true; + } + if (!(thatObject instanceof RoamingConsortiumElement)) { + return false; + } + RoamingConsortiumElement that = (RoamingConsortiumElement) thatObject; + return mOIs.equals(that.mOIs); + } + + @Override + public int hashCode() { + return mOIs.hashCode(); } @Override public String toString() { - return "RoamingConsortium{mOis=[" + Utils.roamingConsortiumsToString(mOis) + "]}"; + return "RoamingConsortium{mOis=[" + Utils.roamingConsortiumsToString(mOIs) + "]}"; } } diff --git a/service/java/com/android/server/wifi/hotspot2/anqp/VenueNameElement.java b/service/java/com/android/server/wifi/hotspot2/anqp/VenueNameElement.java index 573519080..9a4e64b39 100644 --- a/service/java/com/android/server/wifi/hotspot2/anqp/VenueNameElement.java +++ b/service/java/com/android/server/wifi/hotspot2/anqp/VenueNameElement.java @@ -16,8 +16,12 @@ package com.android.server.wifi.hotspot2.anqp; +import com.android.internal.annotations.VisibleForTesting; + import java.net.ProtocolException; +import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -27,36 +31,59 @@ import java.util.List; * * Format: * - * | Info ID | Length | Venue Info | Venue Name Duple #1 (optional) | ... - * 2 2 2 variable + * | Venue Info | Venue Name Duple #1 (optional) | ... + * 2 variable + * * | Venue Name Duple #N (optional) | * variable * - * Refer to {@link com.android.server.wifi.anqp.I18Name} for the format of the Venue Name Duple + * Refer to {@link I18Name} for the format of the Venue Name Duple * fields. - * - * Note: The payload parsed by this class already has 'Info ID' and 'Length' stripped off. */ public class VenueNameElement extends ANQPElement { - private final List<I18Name> mNames; + @VisibleForTesting + public static final int VENUE_INFO_LENGTH = 2; - public VenueNameElement(Constants.ANQPElementType infoID, ByteBuffer payload) - throws ProtocolException { - super(infoID); + /** + * Maximum length for a Venue Name. Refer to IEEE802.11-2012 section 8.4.4.4 for more info. + */ + @VisibleForTesting + public static final int MAXIMUM_VENUE_NAME_LENGTH = 252; - if (payload.remaining() < 2) { - throw new ProtocolException("Venue Name Element cannot contain less than 2 bytes"); - } + private final List<I18Name> mNames; + @VisibleForTesting + public VenueNameElement(List<I18Name> names) { + super(Constants.ANQPElementType.ANQPVenueName); + mNames = names; + } + + /** + * Parse a VenueNameElement from the given buffer. + * + * @param payload The byte buffer to read from + * @return {@link VenueNameElement} + * @throws BufferUnderflowException + * @throws ProtocolException + */ + public static VenueNameElement parse(ByteBuffer payload) + throws ProtocolException { // Skip the Venue Info field, which we don't use. - for (int i = 0; i < Constants.VENUE_INFO_LENGTH; ++i) { + for (int i = 0; i < VENUE_INFO_LENGTH; ++i) { payload.get(); } - mNames = new ArrayList<I18Name>(); + List<I18Name> names = new ArrayList<I18Name>(); while (payload.hasRemaining()) { - mNames.add(new I18Name(payload)); + I18Name name = I18Name.parse(payload); + // Verify that the number of octets for the venue name doesn't exceed the max allowed. + int textBytes = name.getText().getBytes(StandardCharsets.UTF_8).length; + if (textBytes > MAXIMUM_VENUE_NAME_LENGTH) { + throw new ProtocolException("Venue Name exceeds the maximum allowed " + textBytes); + } + names.add(name); } + return new VenueNameElement(names); } public List<I18Name> getNames() { @@ -64,7 +91,25 @@ public class VenueNameElement extends ANQPElement { } @Override + public boolean equals(Object thatObject) { + if (this == thatObject) { + return true; + } + if (!(thatObject instanceof VenueNameElement)) { + return false; + } + VenueNameElement that = (VenueNameElement) thatObject; + return mNames.equals(that.mNames); + } + + @Override + public int hashCode() { + return mNames.hashCode(); + } + + @Override public String toString() { return "VenueName{ mNames=" + mNames + "}"; } + } |