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