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