Home | History | Annotate | Download | only in hotspot2
      1 package com.android.server.wifi.hotspot2;
      2 
      3 import android.util.Base64;
      4 import android.util.Log;
      5 
      6 import com.android.server.wifi.ScanDetail;
      7 import com.android.server.wifi.WifiNative;
      8 import com.android.server.wifi.anqp.ANQPElement;
      9 import com.android.server.wifi.anqp.ANQPFactory;
     10 import com.android.server.wifi.anqp.Constants;
     11 import com.android.server.wifi.anqp.eap.AuthParam;
     12 import com.android.server.wifi.anqp.eap.EAP;
     13 import com.android.server.wifi.anqp.eap.EAPMethod;
     14 import com.android.server.wifi.hotspot2.pps.Credential;
     15 
     16 import java.io.BufferedReader;
     17 import java.io.IOException;
     18 import java.io.StringReader;
     19 import java.net.ProtocolException;
     20 import java.nio.ByteBuffer;
     21 import java.nio.ByteOrder;
     22 import java.nio.CharBuffer;
     23 import java.nio.charset.CharacterCodingException;
     24 import java.nio.charset.StandardCharsets;
     25 import java.util.ArrayList;
     26 import java.util.HashMap;
     27 import java.util.List;
     28 import java.util.Map;
     29 
     30 public class SupplicantBridge {
     31     private final WifiNative mSupplicantHook;
     32     private final SupplicantBridgeCallbacks mCallbacks;
     33     private final Map<Long, ScanDetail> mRequestMap = new HashMap<>();
     34 
     35     private static final int IconChunkSize = 1400;  // 2K*3/4 - overhead
     36     private static final Map<String, Constants.ANQPElementType> sWpsNames = new HashMap<>();
     37 
     38     static {
     39         sWpsNames.put("anqp_venue_name", Constants.ANQPElementType.ANQPVenueName);
     40         sWpsNames.put("anqp_network_auth_type", Constants.ANQPElementType.ANQPNwkAuthType);
     41         sWpsNames.put("anqp_roaming_consortium", Constants.ANQPElementType.ANQPRoamingConsortium);
     42         sWpsNames.put("anqp_ip_addr_type_availability",
     43                 Constants.ANQPElementType.ANQPIPAddrAvailability);
     44         sWpsNames.put("anqp_nai_realm", Constants.ANQPElementType.ANQPNAIRealm);
     45         sWpsNames.put("anqp_3gpp", Constants.ANQPElementType.ANQP3GPPNetwork);
     46         sWpsNames.put("anqp_domain_name", Constants.ANQPElementType.ANQPDomName);
     47         sWpsNames.put("hs20_operator_friendly_name", Constants.ANQPElementType.HSFriendlyName);
     48         sWpsNames.put("hs20_wan_metrics", Constants.ANQPElementType.HSWANMetrics);
     49         sWpsNames.put("hs20_connection_capability", Constants.ANQPElementType.HSConnCapability);
     50         sWpsNames.put("hs20_operating_class", Constants.ANQPElementType.HSOperatingclass);
     51         sWpsNames.put("hs20_osu_providers_list", Constants.ANQPElementType.HSOSUProviders);
     52     }
     53 
     54     /**
     55      * Interface to be implemented by the client to receive callbacks from SupplicantBridge.
     56      */
     57     public interface SupplicantBridgeCallbacks {
     58         /**
     59          * Response from supplicant bridge for the initiated request.
     60          * @param scanDetail
     61          * @param anqpElements
     62          */
     63         void notifyANQPResponse(
     64                 ScanDetail scanDetail,
     65                 Map<Constants.ANQPElementType, ANQPElement> anqpElements);
     66 
     67         /**
     68          * Notify failure.
     69          * @param bssid
     70          */
     71         void notifyIconFailed(long bssid);
     72     }
     73 
     74     public static boolean isAnqpAttribute(String line) {
     75         int split = line.indexOf('=');
     76         return split >= 0 && sWpsNames.containsKey(line.substring(0, split));
     77     }
     78 
     79     public SupplicantBridge(WifiNative supplicantHook, SupplicantBridgeCallbacks callbacks) {
     80         mSupplicantHook = supplicantHook;
     81         mCallbacks = callbacks;
     82     }
     83 
     84     public static Map<Constants.ANQPElementType, ANQPElement> parseANQPLines(List<String> lines) {
     85         if (lines == null) {
     86             return null;
     87         }
     88         Map<Constants.ANQPElementType, ANQPElement> elements = new HashMap<>(lines.size());
     89         for (String line : lines) {
     90             try {
     91                 ANQPElement element = buildElement(line);
     92                 if (element != null) {
     93                     elements.put(element.getID(), element);
     94                 }
     95             }
     96             catch (ProtocolException pe) {
     97                 Log.e(Utils.hs2LogTag(SupplicantBridge.class), "Failed to parse ANQP: " + pe);
     98             }
     99         }
    100         return elements;
    101     }
    102 
    103     public boolean startANQP(ScanDetail scanDetail, List<Constants.ANQPElementType> elements) {
    104         String anqpGet = buildWPSQueryRequest(scanDetail.getNetworkDetail(), elements);
    105         if (anqpGet == null) {
    106             return false;
    107         }
    108         synchronized (mRequestMap) {
    109             mRequestMap.put(scanDetail.getNetworkDetail().getBSSID(), scanDetail);
    110         }
    111         String result = mSupplicantHook.doCustomSupplicantCommand(anqpGet);
    112         if (result != null && result.startsWith("OK")) {
    113             Log.d(Utils.hs2LogTag(getClass()), "ANQP initiated on "
    114                     + scanDetail + " (" + anqpGet + ")");
    115             return true;
    116         }
    117         else {
    118             Log.d(Utils.hs2LogTag(getClass()), "ANQP failed on " +
    119                     scanDetail + ": " + result);
    120             return false;
    121         }
    122     }
    123 
    124     public boolean doIconQuery(long bssid, String fileName) {
    125         String result = mSupplicantHook.doCustomSupplicantCommand("REQ_HS20_ICON " +
    126                 Utils.macToString(bssid) + " " + fileName);
    127         return result != null && result.startsWith("OK");
    128     }
    129 
    130     public byte[] retrieveIcon(IconEvent iconEvent) throws IOException {
    131         byte[] iconData = new byte[iconEvent.getSize()];
    132         try {
    133             int offset = 0;
    134             while (offset < iconEvent.getSize()) {
    135                 int size = Math.min(iconEvent.getSize() - offset, IconChunkSize);
    136 
    137                 String command = String.format("GET_HS20_ICON %s %s %d %d",
    138                         Utils.macToString(iconEvent.getBSSID()), iconEvent.getFileName(),
    139                         offset, size);
    140                 Log.d(Utils.hs2LogTag(getClass()), "Issuing '" + command + "'");
    141                 String response = mSupplicantHook.doCustomSupplicantCommand(command);
    142                 if (response == null) {
    143                     throw new IOException("No icon data returned");
    144                 }
    145 
    146                 try {
    147                     byte[] fragment = Base64.decode(response, Base64.DEFAULT);
    148                     if (fragment.length == 0) {
    149                         throw new IOException("Null data for '" + command + "': " + response);
    150                     }
    151                     if (fragment.length + offset > iconData.length) {
    152                         throw new IOException("Icon chunk exceeds image size");
    153                     }
    154                     System.arraycopy(fragment, 0, iconData, offset, fragment.length);
    155                     offset += fragment.length;
    156                 } catch (IllegalArgumentException iae) {
    157                     throw new IOException("Failed to parse response to '" + command
    158                             + "': " + response);
    159                 }
    160             }
    161             if (offset != iconEvent.getSize()) {
    162                 Log.w(Utils.hs2LogTag(getClass()), "Partial icon data: " + offset +
    163                         ", expected " + iconEvent.getSize());
    164             }
    165         }
    166         finally {
    167             Log.d(Utils.hs2LogTag(getClass()), "Deleting icon for " + iconEvent);
    168             String result = mSupplicantHook.doCustomSupplicantCommand("DEL_HS20_ICON " +
    169                     Utils.macToString(iconEvent.getBSSID()) + " " + iconEvent.getFileName());
    170         }
    171 
    172         return iconData;
    173     }
    174 
    175     public void notifyANQPDone(Long bssid, boolean success) {
    176         ScanDetail scanDetail;
    177         synchronized (mRequestMap) {
    178             scanDetail = mRequestMap.remove(bssid);
    179         }
    180 
    181         if (scanDetail == null) {
    182             if (!success) {
    183                 mCallbacks.notifyIconFailed(bssid);
    184             }
    185             return;
    186         }
    187 
    188         String bssData = mSupplicantHook.scanResult(scanDetail.getBSSIDString());
    189         try {
    190             Map<Constants.ANQPElementType, ANQPElement> elements = parseWPSData(bssData);
    191             Log.d(Utils.hs2LogTag(getClass()), String.format("%s ANQP response for %012x: %s",
    192                     success ? "successful" : "failed", bssid, elements));
    193             mCallbacks.notifyANQPResponse(scanDetail, success ? elements : null);
    194         }
    195         catch (IOException ioe) {
    196             Log.e(Utils.hs2LogTag(getClass()), "Failed to parse ANQP: " +
    197                     ioe.toString() + ": " + bssData);
    198         }
    199         catch (RuntimeException rte) {
    200             Log.e(Utils.hs2LogTag(getClass()), "Failed to parse ANQP: " +
    201                     rte.toString() + ": " + bssData, rte);
    202         }
    203         mCallbacks.notifyANQPResponse(scanDetail, null);
    204     }
    205 
    206     private static String escapeSSID(NetworkDetail networkDetail) {
    207         return escapeString(networkDetail.getSSID(), networkDetail.isSSID_UTF8());
    208     }
    209 
    210     private static String escapeString(String s, boolean utf8) {
    211         boolean asciiOnly = true;
    212         for (int n = 0; n < s.length(); n++) {
    213             char ch = s.charAt(n);
    214             if (ch > 127) {
    215                 asciiOnly = false;
    216                 break;
    217             }
    218         }
    219 
    220         if (asciiOnly) {
    221             return '"' + s + '"';
    222         }
    223         else {
    224             byte[] octets = s.getBytes(utf8 ? StandardCharsets.UTF_8 : StandardCharsets.ISO_8859_1);
    225 
    226             StringBuilder sb = new StringBuilder();
    227             for (byte octet : octets) {
    228                 sb.append(String.format("%02x", octet & Constants.BYTE_MASK));
    229             }
    230             return sb.toString();
    231         }
    232     }
    233 
    234     /**
    235      * Build a wpa_supplicant ANQP query command
    236      * @param networkDetail The network to query.
    237      * @param querySet elements to query
    238      * @return A command string.
    239      */
    240     private static String buildWPSQueryRequest(NetworkDetail networkDetail,
    241                                                List<Constants.ANQPElementType> querySet) {
    242 
    243         boolean baseANQPElements = Constants.hasBaseANQPElements(querySet);
    244         StringBuilder sb = new StringBuilder();
    245         if (baseANQPElements) {
    246             sb.append("ANQP_GET ");
    247         }
    248         else {
    249             sb.append("HS20_ANQP_GET ");     // ANQP_GET does not work for a sole hs20:8 (OSU) query
    250         }
    251         sb.append(networkDetail.getBSSIDString()).append(' ');
    252 
    253         boolean first = true;
    254         for (Constants.ANQPElementType elementType : querySet) {
    255             if (first) {
    256                 first = false;
    257             }
    258             else {
    259                 sb.append(',');
    260             }
    261 
    262             Integer id = Constants.getANQPElementID(elementType);
    263             if (id != null) {
    264                 sb.append(id);
    265             }
    266             else {
    267                 id = Constants.getHS20ElementID(elementType);
    268                 if (baseANQPElements) {
    269                     sb.append("hs20:");
    270                 }
    271                 sb.append(id);
    272             }
    273         }
    274 
    275         return sb.toString();
    276     }
    277 
    278     private static List<String> getWPSNetCommands(String netID, NetworkDetail networkDetail,
    279                                                  Credential credential) {
    280 
    281         List<String> commands = new ArrayList<String>();
    282 
    283         EAPMethod eapMethod = credential.getEAPMethod();
    284         commands.add(String.format("SET_NETWORK %s key_mgmt WPA-EAP", netID));
    285         commands.add(String.format("SET_NETWORK %s ssid %s", netID, escapeSSID(networkDetail)));
    286         commands.add(String.format("SET_NETWORK %s bssid %s",
    287                 netID, networkDetail.getBSSIDString()));
    288         commands.add(String.format("SET_NETWORK %s eap %s",
    289                 netID, mapEAPMethodName(eapMethod.getEAPMethodID())));
    290 
    291         AuthParam authParam = credential.getEAPMethod().getAuthParam();
    292         if (authParam == null) {
    293             return null;            // TLS or SIM/AKA
    294         }
    295         switch (authParam.getAuthInfoID()) {
    296             case NonEAPInnerAuthType:
    297             case InnerAuthEAPMethodType:
    298                 commands.add(String.format("SET_NETWORK %s identity %s",
    299                         netID, escapeString(credential.getUserName(), true)));
    300                 commands.add(String.format("SET_NETWORK %s password %s",
    301                         netID, escapeString(credential.getPassword(), true)));
    302                 commands.add(String.format("SET_NETWORK %s anonymous_identity \"anonymous\"",
    303                         netID));
    304                 break;
    305             default:                // !!! Needs work.
    306                 return null;
    307         }
    308         commands.add(String.format("SET_NETWORK %s priority 0", netID));
    309         commands.add(String.format("ENABLE_NETWORK %s", netID));
    310         commands.add(String.format("SAVE_CONFIG"));
    311         return commands;
    312     }
    313 
    314     private static Map<Constants.ANQPElementType, ANQPElement> parseWPSData(String bssInfo)
    315             throws IOException {
    316         Map<Constants.ANQPElementType, ANQPElement> elements = new HashMap<>();
    317         if (bssInfo == null) {
    318             return elements;
    319         }
    320         BufferedReader lineReader = new BufferedReader(new StringReader(bssInfo));
    321         String line;
    322         while ((line=lineReader.readLine()) != null) {
    323             ANQPElement element = buildElement(line);
    324             if (element != null) {
    325                 elements.put(element.getID(), element);
    326             }
    327         }
    328         return elements;
    329     }
    330 
    331     private static ANQPElement buildElement(String text) throws ProtocolException {
    332         int separator = text.indexOf('=');
    333         if (separator < 0) {
    334             return null;
    335         }
    336 
    337         String elementName = text.substring(0, separator);
    338         Constants.ANQPElementType elementType = sWpsNames.get(elementName);
    339         if (elementType == null) {
    340             return null;
    341         }
    342 
    343         byte[] payload;
    344         try {
    345             payload = Utils.hexToBytes(text.substring(separator + 1));
    346         }
    347         catch (NumberFormatException nfe) {
    348             Log.e(Utils.hs2LogTag(SupplicantBridge.class), "Failed to parse hex string");
    349             return null;
    350         }
    351         return Constants.getANQPElementID(elementType) != null ?
    352                 ANQPFactory.buildElement(ByteBuffer.wrap(payload), elementType, payload.length) :
    353                 ANQPFactory.buildHS20Element(elementType,
    354                         ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN));
    355     }
    356 
    357     private static String mapEAPMethodName(EAP.EAPMethodID eapMethodID) {
    358         switch (eapMethodID) {
    359             case EAP_AKA:
    360                 return "AKA";
    361             case EAP_AKAPrim:
    362                 return "AKA'";  // eap.c:1514
    363             case EAP_SIM:
    364                 return "SIM";
    365             case EAP_TLS:
    366                 return "TLS";
    367             case EAP_TTLS:
    368                 return "TTLS";
    369             default:
    370                 throw new IllegalArgumentException("No mapping for " + eapMethodID);
    371         }
    372     }
    373 
    374     private static final Map<Character,Integer> sMappings = new HashMap<Character, Integer>();
    375 
    376     static {
    377         sMappings.put('\\', (int)'\\');
    378         sMappings.put('"', (int)'"');
    379         sMappings.put('e', 0x1b);
    380         sMappings.put('n', (int)'\n');
    381         sMappings.put('r', (int)'\n');
    382         sMappings.put('t', (int)'\t');
    383     }
    384 
    385     public static String unescapeSSID(String ssid) {
    386 
    387         CharIterator chars = new CharIterator(ssid);
    388         byte[] octets = new byte[ssid.length()];
    389         int bo = 0;
    390 
    391         while (chars.hasNext()) {
    392             char ch = chars.next();
    393             if (ch != '\\' || ! chars.hasNext()) {
    394                 octets[bo++] = (byte)ch;
    395             }
    396             else {
    397                 char suffix = chars.next();
    398                 Integer mapped = sMappings.get(suffix);
    399                 if (mapped != null) {
    400                     octets[bo++] = mapped.byteValue();
    401                 }
    402                 else if (suffix == 'x' && chars.hasDoubleHex()) {
    403                     octets[bo++] = (byte)chars.nextDoubleHex();
    404                 }
    405                 else {
    406                     octets[bo++] = '\\';
    407                     octets[bo++] = (byte)suffix;
    408                 }
    409             }
    410         }
    411 
    412         boolean asciiOnly = true;
    413         for (byte b : octets) {
    414             if ((b&0x80) != 0) {
    415                 asciiOnly = false;
    416                 break;
    417             }
    418         }
    419         if (asciiOnly) {
    420             return new String(octets, 0, bo, StandardCharsets.UTF_8);
    421         } else {
    422             try {
    423                 // If UTF-8 decoding is successful it is almost certainly UTF-8
    424                 CharBuffer cb = StandardCharsets.UTF_8.newDecoder().decode(
    425                         ByteBuffer.wrap(octets, 0, bo));
    426                 return cb.toString();
    427             } catch (CharacterCodingException cce) {
    428                 return new String(octets, 0, bo, StandardCharsets.ISO_8859_1);
    429             }
    430         }
    431     }
    432 
    433     private static class CharIterator {
    434         private final String mString;
    435         private int mPosition;
    436         private int mHex;
    437 
    438         private CharIterator(String s) {
    439             mString = s;
    440         }
    441 
    442         private boolean hasNext() {
    443             return mPosition < mString.length();
    444         }
    445 
    446         private char next() {
    447             return mString.charAt(mPosition++);
    448         }
    449 
    450         private boolean hasDoubleHex() {
    451             if (mString.length() - mPosition < 2) {
    452                 return false;
    453             }
    454             int nh = Utils.fromHex(mString.charAt(mPosition), true);
    455             if (nh < 0) {
    456                 return false;
    457             }
    458             int nl = Utils.fromHex(mString.charAt(mPosition + 1), true);
    459             if (nl < 0) {
    460                 return false;
    461             }
    462             mPosition += 2;
    463             mHex = (nh << 4) | nl;
    464             return true;
    465         }
    466 
    467         private int nextDoubleHex() {
    468             return mHex;
    469         }
    470     }
    471 
    472     private static final String[] TestStrings = {
    473             "test-ssid",
    474             "test\\nss\\tid",
    475             "test\\x2d\\x5f\\nss\\tid",
    476             "test\\x2d\\x5f\\nss\\tid\\\\",
    477             "test\\x2d\\x5f\\nss\\tid\\n",
    478             "test\\x2d\\x5f\\nss\\tid\\x4a",
    479             "another\\",
    480             "an\\other",
    481             "another\\x2"
    482     };
    483 
    484     public static void main(String[] args) {
    485         for (String string : TestStrings) {
    486             System.out.println(unescapeSSID(string));
    487         }
    488     }
    489 }
    490