PasspointEventHandler.java revision 09044adabba28c56b48922d105994d30e7ab015e
1/* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.server.wifi.hotspot2; 18 19import android.util.Base64; 20import android.util.Log; 21 22import com.android.server.wifi.WifiNative; 23import com.android.server.wifi.anqp.ANQPElement; 24import com.android.server.wifi.anqp.ANQPFactory; 25import com.android.server.wifi.anqp.Constants; 26 27import java.io.BufferedReader; 28import java.io.IOException; 29import java.io.StringReader; 30import java.net.ProtocolException; 31import java.nio.ByteBuffer; 32import java.nio.ByteOrder; 33import java.util.HashMap; 34import java.util.List; 35import java.util.Map; 36 37/** 38 * This class handles passpoint specific interactions with the AP, such as ANQP 39 * elements requests, passpoint icon requests, and wireless network management 40 * event notifications. 41 */ 42public class PasspointEventHandler { 43 private final WifiNative mSupplicantHook; 44 private final Callbacks mCallbacks; 45 46 private static final int ICON_CHUNK_SIZE = 1400; // 2K*3/4 - overhead 47 private static final Map<String, Constants.ANQPElementType> sWpsNames = new HashMap<>(); 48 49 static { 50 sWpsNames.put("anqp_venue_name", Constants.ANQPElementType.ANQPVenueName); 51 sWpsNames.put("anqp_roaming_consortium", Constants.ANQPElementType.ANQPRoamingConsortium); 52 sWpsNames.put("anqp_ip_addr_type_availability", 53 Constants.ANQPElementType.ANQPIPAddrAvailability); 54 sWpsNames.put("anqp_nai_realm", Constants.ANQPElementType.ANQPNAIRealm); 55 sWpsNames.put("anqp_3gpp", Constants.ANQPElementType.ANQP3GPPNetwork); 56 sWpsNames.put("anqp_domain_name", Constants.ANQPElementType.ANQPDomName); 57 sWpsNames.put("hs20_operator_friendly_name", Constants.ANQPElementType.HSFriendlyName); 58 sWpsNames.put("hs20_wan_metrics", Constants.ANQPElementType.HSWANMetrics); 59 sWpsNames.put("hs20_connection_capability", Constants.ANQPElementType.HSConnCapability); 60 sWpsNames.put("hs20_osu_providers_list", Constants.ANQPElementType.HSOSUProviders); 61 } 62 63 /** 64 * Interface to be implemented by the client to receive callbacks for passpoint 65 * related events. 66 */ 67 public interface Callbacks { 68 /** 69 * Invoked on received of ANQP response. |anqpElements| will be null on failure. 70 * @param bssid BSSID of the AP 71 * @param anqpElements ANQP elements to be queried 72 */ 73 void onANQPResponse(long bssid, 74 Map<Constants.ANQPElementType, ANQPElement> anqpElements); 75 76 /** 77 * Invoked on received of icon response. |filename| and |data| will be null 78 * on failure. 79 * @param bssid BSSID of the AP 80 * @param filename Name of the icon file 81 * @data icon data bytes 82 */ 83 void onIconResponse(long bssid, String filename, byte[] data); 84 85 /** 86 * Invoked on received of Hotspot 2.0 Wireless Network Management frame. 87 * @param data Wireless Network Management frame data 88 */ 89 void onWnmFrameReceived(WnmData data); 90 } 91 92 public PasspointEventHandler(WifiNative supplicantHook, Callbacks callbacks) { 93 mSupplicantHook = supplicantHook; 94 mCallbacks = callbacks; 95 } 96 97 /** 98 * Determine the given |line| string is an ANQP element. 99 * TODO(zqiu): move this to different/new class (e.g. AnqpParser). 100 * @param line input text 101 * @return true if it is an ANQP element, false otherwise 102 */ 103 public static boolean isAnqpAttribute(String line) { 104 int split = line.indexOf('='); 105 return split >= 0 && sWpsNames.containsKey(line.substring(0, split)); 106 } 107 108 /** 109 * Parse ANQP elements. 110 * TODO(zqiu): move this to different/new class (e.g. AnqpParser). 111 * @param lines input text 112 * @return a map of ANQP elements 113 */ 114 public static Map<Constants.ANQPElementType, ANQPElement> parseANQPLines(List<String> lines) { 115 if (lines == null) { 116 return null; 117 } 118 Map<Constants.ANQPElementType, ANQPElement> elements = new HashMap<>(lines.size()); 119 for (String line : lines) { 120 try { 121 ANQPElement element = buildElement(line); 122 if (element != null) { 123 elements.put(element.getID(), element); 124 } 125 } 126 catch (ProtocolException pe) { 127 Log.e(Utils.hs2LogTag(PasspointEventHandler.class), 128 "Failed to parse ANQP: " + pe); 129 } 130 } 131 return elements; 132 } 133 134 /** 135 * Request the specified ANQP elements |elements| from the specified AP |bssid|. 136 * @param bssid BSSID of the AP 137 * @param elements ANQP elements to be queried 138 * @return true if request is sent successfully, false otherwise. 139 */ 140 public boolean requestANQP(long bssid, List<Constants.ANQPElementType> elements) { 141 String anqpGet = buildWPSQueryRequest(bssid, elements); 142 if (anqpGet == null) { 143 return false; 144 } 145 String result = mSupplicantHook.doCustomSupplicantCommand(anqpGet); 146 if (result != null && result.startsWith("OK")) { 147 Log.d(Utils.hs2LogTag(getClass()), "ANQP initiated on " 148 + Utils.macToString(bssid) + " (" + anqpGet + ")"); 149 return true; 150 } 151 else { 152 Log.d(Utils.hs2LogTag(getClass()), "ANQP failed on " + 153 Utils.macToString(bssid) + ": " + result); 154 return false; 155 } 156 } 157 158 /** 159 * Request a passpoint icon file |filename| from the specified AP |bssid|. 160 * @param bssid BSSID of the AP 161 * @param fileName name of the icon file 162 * @return true if request is sent successfully, false otherwise 163 */ 164 public boolean requestIcon(long bssid, String fileName) { 165 String result = mSupplicantHook.doCustomSupplicantCommand("REQ_HS20_ICON " + 166 Utils.macToString(bssid) + " " + fileName); 167 return result != null && result.startsWith("OK"); 168 } 169 170 /** 171 * Invoked when ANQP query is completed. 172 * TODO(zqiu): currently ANQP completion notification is through WifiMonitor, 173 * this shouldn't be needed once we switch over to wificond for ANQP requests. 174 * @param bssid BSSID of the AP 175 * @param success true if query is completed successfully, false otherwise 176 */ 177 public void notifyANQPDone(long bssid, boolean success) { 178 Map<Constants.ANQPElementType, ANQPElement> elements = null; 179 if (success) { 180 String bssData = 181 mSupplicantHook.scanResult(Utils.macToString(bssid)); 182 try { 183 elements = parseWPSData(bssData); 184 Log.d(Utils.hs2LogTag(getClass()), 185 String.format("Successful ANQP response for %012x: %s", 186 bssid, elements)); 187 } 188 catch (IOException ioe) { 189 Log.e(Utils.hs2LogTag(getClass()), "Failed to parse ANQP: " + 190 ioe.toString() + ": " + bssData); 191 } 192 catch (RuntimeException rte) { 193 Log.e(Utils.hs2LogTag(getClass()), "Failed to parse ANQP: " + 194 rte.toString() + ": " + bssData, rte); 195 } 196 } 197 mCallbacks.onANQPResponse(bssid, elements); 198 } 199 200 /** 201 * Invoked when icon query is completed. 202 * TODO(zqiu): currently icon completion notification is through WifiMonitor, 203 * this shouldn't be needed once we switch over to wificond for icon requests. 204 * @param bssid BSSID of the AP 205 * @param iconEvent icon event data 206 */ 207 public void notifyIconDone(long bssid, IconEvent iconEvent) { 208 String filename = null; 209 byte[] data = null; 210 if (iconEvent != null) { 211 try { 212 data = retrieveIcon(iconEvent); 213 filename = iconEvent.getFileName(); 214 } catch (IOException ioe) { 215 Log.e(Utils.hs2LogTag(getClass()), "Failed to retrieve icon: " + 216 ioe.toString() + ": " + iconEvent.getFileName()); 217 } 218 } 219 mCallbacks.onIconResponse(bssid, filename, data); 220 } 221 222 /** 223 * Invoked when a Wireless Network Management (WNM) frame is received. 224 * TODO(zqiu): currently WNM frame notification is through WifiMonitor, 225 * this shouldn't be needed once we switch over to wificond for WNM frame monitoring. 226 * @param data WNM frame data 227 */ 228 public void notifyWnmFrameReceived(WnmData data) { 229 mCallbacks.onWnmFrameReceived(data); 230 } 231 232 /** 233 * Build a wpa_supplicant ANQP query command 234 * @param bssid BSSID of the AP to be queried 235 * @param querySet elements to query 236 * @return A command string. 237 */ 238 private static String buildWPSQueryRequest(long bssid, 239 List<Constants.ANQPElementType> querySet) { 240 241 boolean baseANQPElements = Constants.hasBaseANQPElements(querySet); 242 StringBuilder sb = new StringBuilder(); 243 if (baseANQPElements) { 244 sb.append("ANQP_GET "); 245 } 246 else { 247 // ANQP_GET does not work for a sole hs20:8 (OSU) query 248 sb.append("HS20_ANQP_GET "); 249 } 250 sb.append(Utils.macToString(bssid)).append(' '); 251 252 boolean first = true; 253 for (Constants.ANQPElementType elementType : querySet) { 254 if (first) { 255 first = false; 256 } 257 else { 258 sb.append(','); 259 } 260 261 Integer id = Constants.getANQPElementID(elementType); 262 if (id != null) { 263 sb.append(id); 264 } 265 else { 266 id = Constants.getHS20ElementID(elementType); 267 if (baseANQPElements) { 268 sb.append("hs20:"); 269 } 270 sb.append(id); 271 } 272 } 273 274 return sb.toString(); 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 || separator + 1 == text.length()) { 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(PasspointEventHandler.class), 312 "Failed to parse hex string"); 313 return null; 314 } 315 return Constants.getANQPElementID(elementType) != null ? 316 ANQPFactory.buildElement(ByteBuffer.wrap(payload), elementType, payload.length) : 317 ANQPFactory.buildHS20Element(elementType, 318 ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN)); 319 } 320 321 private byte[] retrieveIcon(IconEvent iconEvent) throws IOException { 322 byte[] iconData = new byte[iconEvent.getSize()]; 323 try { 324 int offset = 0; 325 while (offset < iconEvent.getSize()) { 326 int size = Math.min(iconEvent.getSize() - offset, ICON_CHUNK_SIZE); 327 328 String command = String.format("GET_HS20_ICON %s %s %d %d", 329 Utils.macToString(iconEvent.getBSSID()), iconEvent.getFileName(), 330 offset, size); 331 Log.d(Utils.hs2LogTag(getClass()), "Issuing '" + command + "'"); 332 String response = mSupplicantHook.doCustomSupplicantCommand(command); 333 if (response == null) { 334 throw new IOException("No icon data returned"); 335 } 336 337 try { 338 byte[] fragment = Base64.decode(response, Base64.DEFAULT); 339 if (fragment.length == 0) { 340 throw new IOException("Null data for '" + command + "': " + response); 341 } 342 if (fragment.length + offset > iconData.length) { 343 throw new IOException("Icon chunk exceeds image size"); 344 } 345 System.arraycopy(fragment, 0, iconData, offset, fragment.length); 346 offset += fragment.length; 347 } catch (IllegalArgumentException iae) { 348 throw new IOException("Failed to parse response to '" + command 349 + "': " + response); 350 } 351 } 352 if (offset != iconEvent.getSize()) { 353 Log.w(Utils.hs2LogTag(getClass()), "Partial icon data: " + offset + 354 ", expected " + iconEvent.getSize()); 355 } 356 } 357 finally { 358 // Delete the icon file in supplicant. 359 Log.d(Utils.hs2LogTag(getClass()), "Deleting icon for " + iconEvent); 360 String result = mSupplicantHook.doCustomSupplicantCommand("DEL_HS20_ICON " + 361 Utils.macToString(iconEvent.getBSSID()) + " " + iconEvent.getFileName()); 362 Log.d(Utils.hs2LogTag(getClass()), "Result: " + result); 363 } 364 365 return iconData; 366 } 367} 368