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 isAnqpAttribute(String line)54 public static boolean isAnqpAttribute(String line) { 55 int split = line.indexOf('='); 56 return split >= 0 && sWpsNames.containsKey(line.substring(0, split)); 57 } 58 SupplicantBridge(WifiNative supplicantHook, WifiConfigStore configStore)59 public SupplicantBridge(WifiNative supplicantHook, WifiConfigStore configStore) { 60 mSupplicantHook = supplicantHook; 61 mConfigStore = configStore; 62 } 63 parseANQPLines(List<String> lines)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 startANQP(ScanDetail scanDetail)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 notifyANQPDone(Long bssid, boolean success)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 escapeSSID(NetworkDetail networkDetail)192 private static String escapeSSID(NetworkDetail networkDetail) { 193 return escapeString(networkDetail.getSSID(), networkDetail.isSSID_UTF8()); 194 } 195 escapeString(String s, boolean utf8)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 buildWPSQueryRequest(NetworkDetail networkDetail)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 getWPSNetCommands(String netID, NetworkDetail networkDetail, Credential credential)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 parseWPSData(String bssInfo)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 buildElement(String text)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 mapEAPMethodName(EAP.EAPMethodID eapMethodID)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 unescapeSSID(String ssid)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 CharIterator(String s)406 private CharIterator(String s) { 407 mString = s; 408 } 409 hasNext()410 private boolean hasNext() { 411 return mPosition < mString.length(); 412 } 413 next()414 private char next() { 415 return mString.charAt(mPosition++); 416 } 417 hasDoubleHex()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 nextDoubleHex()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 main(String[] args)452 public static void main(String[] args) { 453 for (String string : TestStrings) { 454 System.out.println(unescapeSSID(string)); 455 } 456 } 457 } 458