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