Home | History | Annotate | Download | only in hotspot2
      1 package com.android.server.wifi.hotspot2;
      2 
      3 import android.util.Log;
      4 
      5 import com.android.server.wifi.ScanDetail;
      6 import com.android.server.wifi.WifiConfigStore;
      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.BufferUnderflowException;
     21 import java.nio.ByteBuffer;
     22 import java.nio.ByteOrder;
     23 import java.nio.CharBuffer;
     24 import java.nio.charset.CharacterCodingException;
     25 import java.nio.charset.StandardCharsets;
     26 import java.util.ArrayList;
     27 import java.util.HashMap;
     28 import java.util.List;
     29 import java.util.Map;
     30 
     31 public class SupplicantBridge {
     32     private final WifiNative mSupplicantHook;
     33     private final WifiConfigStore mConfigStore;
     34     private final Map<Long, ScanDetail> mRequestMap = new HashMap<>();
     35 
     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     public static boolean isAnqpAttribute(String line) {
     55         int split = line.indexOf('=');
     56         return split >= 0 && sWpsNames.containsKey(line.substring(0, split));
     57     }
     58 
     59     public SupplicantBridge(WifiNative supplicantHook, WifiConfigStore configStore) {
     60         mSupplicantHook = supplicantHook;
     61         mConfigStore = configStore;
     62     }
     63 
     64     public static Map<Constants.ANQPElementType, ANQPElement> parseANQPLines(List<String> lines) {
     65         if (lines == null) {
     66             return null;
     67         }
     68         Map<Constants.ANQPElementType, ANQPElement> elements = new HashMap<>(lines.size());
     69         for (String line : lines) {
     70             try {
     71                 ANQPElement element = buildElement(line);
     72                 if (element != null) {
     73                     elements.put(element.getID(), element);
     74                 }
     75             }
     76             catch (ProtocolException pe) {
     77                 Log.e(Utils.hs2LogTag(SupplicantBridge.class), "Failed to parse ANQP: " + pe);
     78             }
     79         }
     80         return elements;
     81     }
     82 
     83     public void startANQP(ScanDetail scanDetail) {
     84         String anqpGet = buildWPSQueryRequest(scanDetail.getNetworkDetail());
     85         synchronized (mRequestMap) {
     86             mRequestMap.put(scanDetail.getNetworkDetail().getBSSID(), scanDetail);
     87         }
     88         String result = mSupplicantHook.doCustomCommand(anqpGet);
     89         if (result != null && result.startsWith("OK")) {
     90             Log.d(Utils.hs2LogTag(getClass()), "ANQP initiated on " + scanDetail);
     91         }
     92         else {
     93             Log.d(Utils.hs2LogTag(getClass()), "ANQP failed on " +
     94                     scanDetail + ": " + result);
     95         }
     96     }
     97 
     98     public void notifyANQPDone(Long bssid, boolean success) {
     99         ScanDetail scanDetail;
    100         synchronized (mRequestMap) {
    101             scanDetail = mRequestMap.remove(bssid);
    102         }
    103         if (scanDetail == null) {
    104             Log.d(Utils.hs2LogTag(getClass()), String.format("Spurious %s ANQP response for %012x",
    105                             success ? "successful" : "failed", bssid));
    106             return;
    107         }
    108 
    109         String bssData = mSupplicantHook.scanResult(scanDetail.getBSSIDString());
    110         try {
    111             Map<Constants.ANQPElementType, ANQPElement> elements = parseWPSData(bssData);
    112             Log.d(Utils.hs2LogTag(getClass()), String.format("%s ANQP response for %012x: %s",
    113                     success ? "successful" : "failed", bssid, elements));
    114             mConfigStore.notifyANQPResponse(scanDetail, success ? elements : null);
    115         }
    116         catch (IOException ioe) {
    117             Log.e(Utils.hs2LogTag(getClass()), "Failed to parse ANQP: " +
    118                     ioe.toString() + ": " + bssData);
    119         }
    120         catch (RuntimeException rte) {
    121             Log.e(Utils.hs2LogTag(getClass()), "Failed to parse ANQP: " +
    122                     rte.toString() + ": " + bssData, rte);
    123         }
    124         mConfigStore.notifyANQPResponse(scanDetail, null);
    125     }
    126 
    127     /*
    128     public boolean addCredential(HomeSP homeSP, NetworkDetail networkDetail) {
    129         Credential credential = homeSP.getCredential();
    130         if (credential == null)
    131             return false;
    132 
    133         String nwkID = null;
    134         if (mLastSSID != null) {
    135             String nwkList = mSupplicantHook.doCustomCommand("LIST_NETWORKS");
    136 
    137             BufferedReader reader = new BufferedReader(new StringReader(nwkList));
    138             String line;
    139             try {
    140                 while ((line = reader.readLine()) != null) {
    141                     String[] tokens = line.split("\\t");
    142                     if (tokens.length < 2 || ! Utils.isDecimal(tokens[0])) {
    143                         continue;
    144                     }
    145                     if (unescapeSSID(tokens[1]).equals(mLastSSID)) {
    146                         nwkID = tokens[0];
    147                         Log.d("HS2J", "Network " + tokens[0] +
    148                                 " matches last SSID '" + mLastSSID + "'");
    149                         break;
    150                     }
    151                 }
    152             }
    153             catch (IOException ioe) {
    154                 //
    155             }
    156         }
    157 
    158         if (nwkID == null) {
    159             nwkID = mSupplicantHook.doCustomCommand("ADD_NETWORK");
    160             Log.d("HS2J", "add_network: '" + nwkID + "'");
    161             if (! Utils.isDecimal(nwkID)) {
    162                 return false;
    163             }
    164         }
    165 
    166         List<String> credCommand = getWPSNetCommands(nwkID, networkDetail, credential);
    167         for (String command : credCommand) {
    168             String status = mSupplicantHook.doCustomCommand(command);
    169             Log.d("HS2J", "Status of '" + command + "': '" + status + "'");
    170         }
    171 
    172         if (! networkDetail.getSSID().equals(mLastSSID)) {
    173             mLastSSID = networkDetail.getSSID();
    174             PrintWriter out = null;
    175             try {
    176                 out = new PrintWriter(new OutputStreamWriter(
    177                         new FileOutputStream(mLastSSIDFile, false), StandardCharsets.UTF_8));
    178                 out.println(mLastSSID);
    179             } catch (IOException ioe) {
    180             //
    181             } finally {
    182                 if (out != null) {
    183                     out.close();
    184                 }
    185             }
    186         }
    187 
    188         return true;
    189     }
    190     */
    191 
    192     private static String escapeSSID(NetworkDetail networkDetail) {
    193         return escapeString(networkDetail.getSSID(), networkDetail.isSSID_UTF8());
    194     }
    195 
    196     private static String escapeString(String s, boolean utf8) {
    197         boolean asciiOnly = true;
    198         for (int n = 0; n < s.length(); n++) {
    199             char ch = s.charAt(n);
    200             if (ch > 127) {
    201                 asciiOnly = false;
    202                 break;
    203             }
    204         }
    205 
    206         if (asciiOnly) {
    207             return '"' + s + '"';
    208         }
    209         else {
    210             byte[] octets = s.getBytes(utf8 ? StandardCharsets.UTF_8 : StandardCharsets.ISO_8859_1);
    211 
    212             StringBuilder sb = new StringBuilder();
    213             for (byte octet : octets) {
    214                 sb.append(String.format("%02x", octet & Constants.BYTE_MASK));
    215             }
    216             return sb.toString();
    217         }
    218     }
    219 
    220     private static String buildWPSQueryRequest(NetworkDetail networkDetail) {
    221         StringBuilder sb = new StringBuilder();
    222         sb.append("ANQP_GET ").append(networkDetail.getBSSIDString()).append(' ');
    223 
    224         boolean first = true;
    225         for (Constants.ANQPElementType elementType : ANQPFactory.getBaseANQPSet()) {
    226             if (networkDetail.getAnqpOICount() == 0 &&
    227                     elementType == Constants.ANQPElementType.ANQPRoamingConsortium) {
    228                 continue;
    229             }
    230             if (first) {
    231                 first = false;
    232             }
    233             else {
    234                 sb.append(',');
    235             }
    236             sb.append(Constants.getANQPElementID(elementType));
    237         }
    238         if (networkDetail.getHSRelease() != null) {
    239             for (Constants.ANQPElementType elementType : ANQPFactory.getHS20ANQPSet()) {
    240                 sb.append(",hs20:").append(Constants.getHS20ElementID(elementType));
    241             }
    242         }
    243         return sb.toString();
    244     }
    245 
    246     private static List<String> getWPSNetCommands(String netID, NetworkDetail networkDetail,
    247                                                  Credential credential) {
    248 
    249         List<String> commands = new ArrayList<String>();
    250 
    251         EAPMethod eapMethod = credential.getEAPMethod();
    252         commands.add(String.format("SET_NETWORK %s key_mgmt WPA-EAP", netID));
    253         commands.add(String.format("SET_NETWORK %s ssid %s", netID, escapeSSID(networkDetail)));
    254         commands.add(String.format("SET_NETWORK %s bssid %s",
    255                 netID, networkDetail.getBSSIDString()));
    256         commands.add(String.format("SET_NETWORK %s eap %s",
    257                 netID, mapEAPMethodName(eapMethod.getEAPMethodID())));
    258 
    259         AuthParam authParam = credential.getEAPMethod().getAuthParam();
    260         if (authParam == null) {
    261             return null;            // TLS or SIM/AKA
    262         }
    263         switch (authParam.getAuthInfoID()) {
    264             case NonEAPInnerAuthType:
    265             case InnerAuthEAPMethodType:
    266                 commands.add(String.format("SET_NETWORK %s identity %s",
    267                         netID, escapeString(credential.getUserName(), true)));
    268                 commands.add(String.format("SET_NETWORK %s password %s",
    269                         netID, escapeString(credential.getPassword(), true)));
    270                 commands.add(String.format("SET_NETWORK %s anonymous_identity \"anonymous\"",
    271                         netID));
    272                 break;
    273             default:                // !!! Needs work.
    274                 return null;
    275         }
    276         commands.add(String.format("SET_NETWORK %s priority 0", netID));
    277         commands.add(String.format("ENABLE_NETWORK %s", netID));
    278         commands.add(String.format("SAVE_CONFIG"));
    279         return commands;
    280     }
    281 
    282     private static Map<Constants.ANQPElementType, ANQPElement> parseWPSData(String bssInfo)
    283             throws IOException {
    284         Map<Constants.ANQPElementType, ANQPElement> elements = new HashMap<>();
    285         if (bssInfo == null) {
    286             return elements;
    287         }
    288         BufferedReader lineReader = new BufferedReader(new StringReader(bssInfo));
    289         String line;
    290         while ((line=lineReader.readLine()) != null) {
    291             ANQPElement element = buildElement(line);
    292             if (element != null) {
    293                 elements.put(element.getID(), element);
    294             }
    295         }
    296         return elements;
    297     }
    298 
    299     private static ANQPElement buildElement(String text) throws ProtocolException {
    300         int separator = text.indexOf('=');
    301         if (separator < 0) {
    302             return null;
    303         }
    304 
    305         String elementName = text.substring(0, separator);
    306         Constants.ANQPElementType elementType = sWpsNames.get(elementName);
    307         if (elementType == null) {
    308             return null;
    309         }
    310 
    311         byte[] payload;
    312         try {
    313             payload = Utils.hexToBytes(text.substring(separator + 1));
    314         }
    315         catch (NumberFormatException nfe) {
    316             Log.e(Utils.hs2LogTag(SupplicantBridge.class), "Failed to parse hex string");
    317             return null;
    318         }
    319         return Constants.getANQPElementID(elementType) != null ?
    320                 ANQPFactory.buildElement(ByteBuffer.wrap(payload), elementType, payload.length) :
    321                 ANQPFactory.buildHS20Element(elementType,
    322                         ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN));
    323     }
    324 
    325     private static String mapEAPMethodName(EAP.EAPMethodID eapMethodID) {
    326         switch (eapMethodID) {
    327             case EAP_AKA:
    328                 return "AKA";
    329             case EAP_AKAPrim:
    330                 return "AKA'";  // eap.c:1514
    331             case EAP_SIM:
    332                 return "SIM";
    333             case EAP_TLS:
    334                 return "TLS";
    335             case EAP_TTLS:
    336                 return "TTLS";
    337             default:
    338                 throw new IllegalArgumentException("No mapping for " + eapMethodID);
    339         }
    340     }
    341 
    342     private static final Map<Character,Integer> sMappings = new HashMap<Character, Integer>();
    343 
    344     static {
    345         sMappings.put('\\', (int)'\\');
    346         sMappings.put('"', (int)'"');
    347         sMappings.put('e', 0x1b);
    348         sMappings.put('n', (int)'\n');
    349         sMappings.put('r', (int)'\n');
    350         sMappings.put('t', (int)'\t');
    351     }
    352 
    353     public static String unescapeSSID(String ssid) {
    354 
    355         CharIterator chars = new CharIterator(ssid);
    356         byte[] octets = new byte[ssid.length()];
    357         int bo = 0;
    358 
    359         while (chars.hasNext()) {
    360             char ch = chars.next();
    361             if (ch != '\\' || ! chars.hasNext()) {
    362                 octets[bo++] = (byte)ch;
    363             }
    364             else {
    365                 char suffix = chars.next();
    366                 Integer mapped = sMappings.get(suffix);
    367                 if (mapped != null) {
    368                     octets[bo++] = mapped.byteValue();
    369                 }
    370                 else if (suffix == 'x' && chars.hasDoubleHex()) {
    371                     octets[bo++] = (byte)chars.nextDoubleHex();
    372                 }
    373                 else {
    374                     octets[bo++] = '\\';
    375                     octets[bo++] = (byte)suffix;
    376                 }
    377             }
    378         }
    379 
    380         boolean asciiOnly = true;
    381         for (byte b : octets) {
    382             if ((b&0x80) != 0) {
    383                 asciiOnly = false;
    384                 break;
    385             }
    386         }
    387         if (asciiOnly) {
    388             return new String(octets, 0, bo, StandardCharsets.UTF_8);
    389         } else {
    390             try {
    391                 // If UTF-8 decoding is successful it is almost certainly UTF-8
    392                 CharBuffer cb = StandardCharsets.UTF_8.newDecoder().decode(
    393                         ByteBuffer.wrap(octets, 0, bo));
    394                 return cb.toString();
    395             } catch (CharacterCodingException cce) {
    396                 return new String(octets, 0, bo, StandardCharsets.ISO_8859_1);
    397             }
    398         }
    399     }
    400 
    401     private static class CharIterator {
    402         private final String mString;
    403         private int mPosition;
    404         private int mHex;
    405 
    406         private CharIterator(String s) {
    407             mString = s;
    408         }
    409 
    410         private boolean hasNext() {
    411             return mPosition < mString.length();
    412         }
    413 
    414         private char next() {
    415             return mString.charAt(mPosition++);
    416         }
    417 
    418         private boolean hasDoubleHex() {
    419             if (mString.length() - mPosition < 2) {
    420                 return false;
    421             }
    422             int nh = Utils.fromHex(mString.charAt(mPosition), true);
    423             if (nh < 0) {
    424                 return false;
    425             }
    426             int nl = Utils.fromHex(mString.charAt(mPosition + 1), true);
    427             if (nl < 0) {
    428                 return false;
    429             }
    430             mPosition += 2;
    431             mHex = (nh << 4) | nl;
    432             return true;
    433         }
    434 
    435         private int nextDoubleHex() {
    436             return mHex;
    437         }
    438     }
    439 
    440     private static final String[] TestStrings = {
    441             "test-ssid",
    442             "test\\nss\\tid",
    443             "test\\x2d\\x5f\\nss\\tid",
    444             "test\\x2d\\x5f\\nss\\tid\\\\",
    445             "test\\x2d\\x5f\\nss\\tid\\n",
    446             "test\\x2d\\x5f\\nss\\tid\\x4a",
    447             "another\\",
    448             "an\\other",
    449             "another\\x2"
    450     };
    451 
    452     public static void main(String[] args) {
    453         for (String string : TestStrings) {
    454             System.out.println(unescapeSSID(string));
    455         }
    456     }
    457 }
    458