blob: 900cbeb6c9e7576af4a9cced452e3a6df9835031 [file] [log] [blame]
package com.android.server.wifi.hotspot2;
import android.net.wifi.ScanResult;
import android.util.Log;
import com.android.server.wifi.anqp.ANQPElement;
import com.android.server.wifi.anqp.Constants;
import com.android.server.wifi.anqp.VenueNameElement;
import java.net.ProtocolException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import static com.android.server.wifi.anqp.Constants.BYTES_IN_EUI48;
import static com.android.server.wifi.anqp.Constants.BYTE_MASK;
import static com.android.server.wifi.anqp.Constants.getInteger;
public class NetworkDetail {
private static final int EID_SSID = 0;
private static final int EID_BSSLoad = 11;
private static final int EID_HT_OPERATION = 61;
private static final int EID_VHT_OPERATION = 192;
private static final int EID_Interworking = 107;
private static final int EID_RoamingConsortium = 111;
private static final int EID_ExtendedCaps = 127;
private static final int EID_VSA = 221;
private static final int ANQP_DOMID_BIT = 0x04;
private static final int RTT_RESP_ENABLE_BIT = 70;
private static final long SSID_UTF8_BIT = 0x0001000000000000L;
//turn off when SHIP
private static final boolean DBG = true;
private static final boolean VDBG = false;
private static final String TAG = "NetworkDetail:";
public enum Ant {
Private,
PrivateWithGuest,
ChargeablePublic,
FreePublic,
Personal,
EmergencyOnly,
Resvd6,
Resvd7,
Resvd8,
Resvd9,
Resvd10,
Resvd11,
Resvd12,
Resvd13,
TestOrExperimental,
Wildcard
}
public enum HSRelease {
R1,
R2,
Unknown
}
// General identifiers:
private final String mSSID;
private final long mHESSID;
private final long mBSSID;
// BSS Load element:
private final int mStationCount;
private final int mChannelUtilization;
private final int mCapacity;
//channel detailed information
/*
* 0 -- 20 MHz
* 1 -- 40 MHz
* 2 -- 80 MHz
* 3 -- 160 MHz
* 4 -- 80 + 80 MHz
*/
private final int mChannelWidth;
private final int mPrimaryFreq;
private final int mCenterfreq0;
private final int mCenterfreq1;
private final boolean m80211McRTTResponder;
/*
* From Interworking element:
* mAnt non null indicates the presence of Interworking, i.e. 802.11u
* mVenueGroup and mVenueType may be null if not present in the Interworking element.
*/
private final Ant mAnt;
private final boolean mInternet;
private final VenueNameElement.VenueGroup mVenueGroup;
private final VenueNameElement.VenueType mVenueType;
/*
* From HS20 Indication element:
* mHSRelease is null only if the HS20 Indication element was not present.
* mAnqpDomainID is set to -1 if not present in the element.
*/
private final HSRelease mHSRelease;
private final int mAnqpDomainID;
/*
* From beacon:
* mAnqpOICount is how many additional OIs are available through ANQP.
* mRoamingConsortiums is either null, if the element was not present, or is an array of
* 1, 2 or 3 longs in which the roaming consortium values occupy the LSBs.
*/
private final int mAnqpOICount;
private final long[] mRoamingConsortiums;
private final Long mExtendedCapabilities;
private final Map<Constants.ANQPElementType, ANQPElement> mANQPElements;
public NetworkDetail(String bssid, String infoElements, List<String> anqpLines, int freq) {
if (infoElements == null) {
throw new IllegalArgumentException("Null information element string");
}
int separator = infoElements.indexOf('=');
if (separator<0) {
throw new IllegalArgumentException("No element separator");
}
mBSSID = Utils.parseMac(bssid);
ByteBuffer data = ByteBuffer.wrap(Utils.hexToBytes(infoElements.substring(separator + 1)))
.order(ByteOrder.LITTLE_ENDIAN);
String ssid = null;
byte[] ssidOctets = null;
int stationCount = 0;
int channelUtilization = 0;
int capacity = 0;
Ant ant = null;
boolean internet = false;
VenueNameElement.VenueGroup venueGroup = null;
VenueNameElement.VenueType venueType = null;
long hessid = 0L;
int anqpOICount = 0;
long[] roamingConsortiums = null;
HSRelease hsRelease = null;
int anqpDomainID = 0; // No domain ID treated the same as a 0; unique info per AP.
Long extendedCapabilities = null;
int secondChanelOffset = 0;
int channelMode = 0;
int centerFreqIndex1 = 0;
int centerFreqIndex2 = 0;
boolean RTTResponder = false;
RuntimeException exception = null;
try {
while (data.remaining() > 1) {
int eid = data.get() & Constants.BYTE_MASK;
int elementLength = data.get() & Constants.BYTE_MASK;
if (elementLength > data.remaining()) {
throw new IllegalArgumentException("Element length " + elementLength +
" exceeds payload length " + data.remaining() +
" @ " + data.position());
}
if (eid == 0 && elementLength == 0 && ssidOctets != null) {
// Don't overwrite SSID (eid 0) with trailing zero garbage
continue;
}
ByteBuffer element;
switch (eid) {
case EID_SSID:
ssidOctets = new byte[elementLength];
data.get(ssidOctets);
break;
case EID_BSSLoad:
if (elementLength != 5) {
throw new IllegalArgumentException("BSS Load element length is not 5: " +
elementLength);
}
stationCount = data.getShort() & Constants.SHORT_MASK;
channelUtilization = data.get() & Constants.BYTE_MASK;
capacity = data.getShort() & Constants.SHORT_MASK;
break;
case EID_HT_OPERATION:
element = getAndAdvancePayload(data, elementLength);
int primary_channel = element.get();
secondChanelOffset = element.get() & 0x3;
break;
case EID_VHT_OPERATION:
element = getAndAdvancePayload(data, elementLength);
channelMode = element.get() & Constants.BYTE_MASK;
centerFreqIndex1 = element.get() & Constants.BYTE_MASK;
centerFreqIndex2 = element.get() & Constants.BYTE_MASK;
break;
case EID_Interworking:
int anOptions = data.get() & Constants.BYTE_MASK;
ant = Ant.values()[anOptions & 0x0f];
internet = (anOptions & 0x10) != 0;
// Len 1 none, 3 venue-info, 7 HESSID, 9 venue-info & HESSID
if (elementLength == 3 || elementLength == 9) {
try {
ByteBuffer vinfo = data.duplicate();
vinfo.limit(vinfo.position() + 2);
VenueNameElement vne =
new VenueNameElement(Constants.ANQPElementType.ANQPVenueName,
vinfo);
venueGroup = vne.getGroup();
venueType = vne.getType();
data.getShort();
} catch (ProtocolException pe) {
/*Cannot happen*/
}
} else if (elementLength != 1 && elementLength != 7) {
throw new IllegalArgumentException("Bad Interworking element length: " +
elementLength);
}
if (elementLength == 7 || elementLength == 9) {
hessid = getInteger(data, ByteOrder.BIG_ENDIAN, 6);
}
break;
case EID_RoamingConsortium:
anqpOICount = data.get() & Constants.BYTE_MASK;
int oi12Length = data.get() & Constants.BYTE_MASK;
int oi1Length = oi12Length & Constants.NIBBLE_MASK;
int oi2Length = (oi12Length >>> 4) & Constants.NIBBLE_MASK;
int oi3Length = elementLength - 2 - oi1Length - oi2Length;
int oiCount = 0;
if (oi1Length > 0) {
oiCount++;
if (oi2Length > 0) {
oiCount++;
if (oi3Length > 0) {
oiCount++;
}
}
}
roamingConsortiums = new long[oiCount];
if (oi1Length > 0 && roamingConsortiums.length > 0) {
roamingConsortiums[0] =
getInteger(data, ByteOrder.BIG_ENDIAN, oi1Length);
}
if (oi2Length > 0 && roamingConsortiums.length > 1) {
roamingConsortiums[1] =
getInteger(data, ByteOrder.BIG_ENDIAN, oi2Length);
}
if (oi3Length > 0 && roamingConsortiums.length > 2) {
roamingConsortiums[2] =
getInteger(data, ByteOrder.BIG_ENDIAN, oi3Length);
}
break;
case EID_VSA:
element = getAndAdvancePayload(data, elementLength);
if (elementLength >= 5 && element.getInt() == Constants.HS20_FRAME_PREFIX) {
int hsConf = element.get() & Constants.BYTE_MASK;
switch ((hsConf >> 4) & Constants.NIBBLE_MASK) {
case 0:
hsRelease = HSRelease.R1;
break;
case 1:
hsRelease = HSRelease.R2;
break;
default:
hsRelease = HSRelease.Unknown;
break;
}
if ((hsConf & ANQP_DOMID_BIT) != 0) {
if (elementLength < 7) {
throw new IllegalArgumentException(
"HS20 indication element too short: " + elementLength);
}
anqpDomainID = element.getShort() & Constants.SHORT_MASK;
}
}
break;
case EID_ExtendedCaps:
element = data.duplicate();
extendedCapabilities =
Constants.getInteger(data, ByteOrder.LITTLE_ENDIAN, elementLength);
int index = RTT_RESP_ENABLE_BIT / 8;
byte offset = RTT_RESP_ENABLE_BIT % 8;
if (elementLength < index + 1) {
RTTResponder = false;
element.position(element.position() + elementLength);
break;
}
element.position(element.position() + index);
RTTResponder = (element.get() & (0x1 << offset)) != 0;
break;
default:
data.position(data.position() + elementLength);
break;
}
}
}
catch (IllegalArgumentException | BufferUnderflowException | ArrayIndexOutOfBoundsException e) {
Log.d(Utils.hs2LogTag(getClass()), "Caught " + e);
if (ssidOctets == null) {
throw new IllegalArgumentException("Malformed IE string (no SSID)", e);
}
exception = e;
}
if (ssidOctets != null) {
boolean strictUTF8 = extendedCapabilities != null &&
( extendedCapabilities & SSID_UTF8_BIT ) != 0;
/*
* Strict use of the "UTF-8 SSID" bit by APs appears to be spotty at best even if the
* encoding truly is in UTF-8. An unconditional attempt to decode the SSID as UTF-8 is
* therefore always made with a fall back to 8859-1 under normal circumstances.
* If, however, a previous exception was detected and the UTF-8 bit is set, failure to
* decode the SSID will be used as an indication that the whole frame is malformed and
* an exception will be triggered.
*/
CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();
try {
CharBuffer decoded = decoder.decode(ByteBuffer.wrap(ssidOctets));
ssid = decoded.toString();
}
catch (CharacterCodingException cce) {
ssid = null;
}
if (ssid == null) {
if (strictUTF8 && exception != null) {
throw new IllegalArgumentException("Failed to decode SSID in dubious IE string");
}
else {
ssid = new String(ssidOctets, StandardCharsets.ISO_8859_1);
}
}
}
mSSID = ssid;
mHESSID = hessid;
mStationCount = stationCount;
mChannelUtilization = channelUtilization;
mCapacity = capacity;
mAnt = ant;
mInternet = internet;
mVenueGroup = venueGroup;
mVenueType = venueType;
mHSRelease = hsRelease;
mAnqpDomainID = anqpDomainID;
mAnqpOICount = anqpOICount;
mRoamingConsortiums = roamingConsortiums;
mExtendedCapabilities = extendedCapabilities;
mANQPElements = SupplicantBridge.parseANQPLines(anqpLines);
//set up channel info
mPrimaryFreq = freq;
if (channelMode != 0) {
// 80 or 160 MHz
mChannelWidth = channelMode + 1;
mCenterfreq0 = (centerFreqIndex1 - 36) * 5 + 5180;
if(channelMode > 1) { //160MHz
mCenterfreq1 = (centerFreqIndex2 - 36) * 5 + 5180;
} else {
mCenterfreq1 = 0;
}
} else {
//20 or 40 MHz
if (secondChanelOffset != 0) {//40MHz
mChannelWidth = 1;
if (secondChanelOffset == 1) {
mCenterfreq0 = mPrimaryFreq + 20;
} else if (secondChanelOffset == 3) {
mCenterfreq0 = mPrimaryFreq - 20;
} else {
mCenterfreq0 = 0;
Log.e(TAG,"Error on secondChanelOffset");
}
} else {
mCenterfreq0 = 0;
mChannelWidth = 0;
}
mCenterfreq1 = 0;
}
m80211McRTTResponder = RTTResponder;
if (VDBG) {
Log.d(TAG, mSSID + "ChannelWidth is: " + mChannelWidth + " PrimaryFreq: " + mPrimaryFreq +
" mCenterfreq0: " + mCenterfreq0 + " mCenterfreq1: " + mCenterfreq1 +
(m80211McRTTResponder ? "Support RTT reponder" : "Do not support RTT responder"));
}
}
private static ByteBuffer getAndAdvancePayload(ByteBuffer data, int plLength) {
ByteBuffer payload = data.duplicate().order(data.order());
payload.limit(payload.position() + plLength);
data.position(data.position() + plLength);
return payload;
}
private NetworkDetail(NetworkDetail base, Map<Constants.ANQPElementType, ANQPElement> anqpElements) {
mSSID = base.mSSID;
mBSSID = base.mBSSID;
mHESSID = base.mHESSID;
mStationCount = base.mStationCount;
mChannelUtilization = base.mChannelUtilization;
mCapacity = base.mCapacity;
mAnt = base.mAnt;
mInternet = base.mInternet;
mVenueGroup = base.mVenueGroup;
mVenueType = base.mVenueType;
mHSRelease = base.mHSRelease;
mAnqpDomainID = base.mAnqpDomainID;
mAnqpOICount = base.mAnqpOICount;
mRoamingConsortiums = base.mRoamingConsortiums;
mExtendedCapabilities = base.mExtendedCapabilities;
mANQPElements = anqpElements;
mChannelWidth = base.mChannelWidth;
mPrimaryFreq = base.mPrimaryFreq;
mCenterfreq0 = base.mCenterfreq0;
mCenterfreq1 = base.mCenterfreq1;
m80211McRTTResponder = base.m80211McRTTResponder;
}
public NetworkDetail complete(Map<Constants.ANQPElementType, ANQPElement> anqpElements) {
return new NetworkDetail(this, anqpElements);
}
private static long parseMac(String s) {
long mac = 0;
int count = 0;
for (int n = 0; n < s.length(); n++) {
int nibble = Utils.fromHex(s.charAt(n), true);
if (nibble >= 0) {
mac = (mac << 4) | nibble;
count++;
}
}
if (count < 12 || (count&1) == 1) {
throw new IllegalArgumentException("Bad MAC address: '" + s + "'");
}
return mac;
}
public boolean has80211uInfo() {
return mAnt != null || mRoamingConsortiums != null || mHSRelease != null;
}
public boolean hasInterworking() {
return mAnt != null;
}
public String getSSID() {
return mSSID;
}
public String getTrimmedSSID() {
for (int n = 0; n < mSSID.length(); n++) {
if (mSSID.charAt(n) != 0) {
return mSSID;
}
}
return "";
}
public long getHESSID() {
return mHESSID;
}
public long getBSSID() {
return mBSSID;
}
public int getStationCount() {
return mStationCount;
}
public int getChannelUtilization() {
return mChannelUtilization;
}
public int getCapacity() {
return mCapacity;
}
public boolean isInterworking() {
return mAnt != null;
}
public Ant getAnt() {
return mAnt;
}
public boolean isInternet() {
return mInternet;
}
public VenueNameElement.VenueGroup getVenueGroup() {
return mVenueGroup;
}
public VenueNameElement.VenueType getVenueType() {
return mVenueType;
}
public HSRelease getHSRelease() {
return mHSRelease;
}
public int getAnqpDomainID() {
return mAnqpDomainID;
}
public int getAnqpOICount() {
return mAnqpOICount;
}
public long[] getRoamingConsortiums() {
return mRoamingConsortiums;
}
public Long getExtendedCapabilities() {
return mExtendedCapabilities;
}
public Map<Constants.ANQPElementType, ANQPElement> getANQPElements() {
return mANQPElements;
}
public int getChannelWidth() {
return mChannelWidth;
}
public int getCenterfreq0() {
return mCenterfreq0;
}
public int getCenterfreq1() {
return mCenterfreq1;
}
public boolean is80211McResponderSupport() {
return m80211McRTTResponder;
}
public boolean isSSID_UTF8() {
return mExtendedCapabilities != null && (mExtendedCapabilities & SSID_UTF8_BIT) != 0;
}
@Override
public boolean equals(Object thatObject) {
if (this == thatObject) {
return true;
}
if (thatObject == null || getClass() != thatObject.getClass()) {
return false;
}
NetworkDetail that = (NetworkDetail)thatObject;
return getSSID().equals(that.getSSID()) && getBSSID() == that.getBSSID();
}
@Override
public int hashCode() {
return ((mSSID.hashCode() * 31) + (int)(mBSSID >>> 32)) * 31 + (int)mBSSID;
}
@Override
public String toString() {
return String.format("NetworkInfo{mSSID='%s', mHESSID=%x, mBSSID=%x, mStationCount=%d, " +
"mChannelUtilization=%d, mCapacity=%d, mAnt=%s, mInternet=%s, " +
"mVenueGroup=%s, mVenueType=%s, mHSRelease=%s, mAnqpDomainID=%d, " +
"mAnqpOICount=%d, mRoamingConsortiums=%s}",
mSSID, mHESSID, mBSSID, mStationCount,
mChannelUtilization, mCapacity, mAnt, mInternet,
mVenueGroup, mVenueType, mHSRelease, mAnqpDomainID,
mAnqpOICount, Utils.roamingConsortiumsToString(mRoamingConsortiums));
}
public String toKeyString() {
return mHESSID != 0 ?
String.format("'%s':%012x (%012x)", mSSID, mBSSID, mHESSID) :
String.format("'%s':%012x", mSSID, mBSSID);
}
public String getBSSIDString() {
return toMACString(mBSSID);
}
public static String toMACString(long mac) {
StringBuilder sb = new StringBuilder();
boolean first = true;
for (int n = BYTES_IN_EUI48 - 1; n >= 0; n--) {
if (first) {
first = false;
} else {
sb.append(':');
}
sb.append(String.format("%02x", (mac >>> (n * Byte.SIZE)) & BYTE_MASK));
}
return sb.toString();
}
private static final String IE = "ie=" +
"000477696e67" + // SSID wing
"0b052a00cf611e" + // BSS Load 42:207:7777
"6b091e0a01610408621205" + // internet:Experimental:Vehicular:Auto:hessid
"6f0a0e530111112222222229" + // 14:111111:2222222229
"dd07506f9a10143a01"; // r2:314
private static final String IE2 = "ie=000f4578616d706c65204e6574776f726b010882848b960c1218240301012a010432043048606c30140100000fac040100000fac040100000fac0100007f04000000806b091e07010203040506076c027f006f1001531122331020304050010203040506dd05506f9a1000";
public static void main(String[] args) {
ScanResult scanResult = new ScanResult();
scanResult.SSID = "wing";
scanResult.BSSID = "610408";
NetworkDetail nwkDetail = new NetworkDetail(scanResult.BSSID, IE2, null, 0);
System.out.println(nwkDetail);
}
}