PasspointEventHandler.java revision 74339de52d7066f22771d914e698da503232c107
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.hotspot2.anqp.ANQPElement; 24import com.android.server.wifi.hotspot2.anqp.ANQPFactory; 25import com.android.server.wifi.hotspot2.anqp.Constants; 26 27import java.io.BufferedReader; 28import java.io.IOException; 29import java.io.StringReader; 30import java.net.ProtocolException; 31import java.nio.BufferUnderflowException; 32import java.nio.ByteBuffer; 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 } catch (ProtocolException | BufferUnderflowException e) { 126 Log.e(Utils.hs2LogTag(PasspointEventHandler.class), 127 "Failed to parse ANQP: " + e); 128 } 129 } 130 return elements; 131 } 132 133 /** 134 * Request the specified ANQP elements |elements| from the specified AP |bssid|. 135 * @param bssid BSSID of the AP 136 * @param elements ANQP elements to be queried 137 * @return true if request is sent successfully, false otherwise. 138 */ 139 public boolean requestANQP(long bssid, List<Constants.ANQPElementType> elements) { 140 String anqpGet = buildWPSQueryRequest(bssid, elements); 141 if (anqpGet == null) { 142 return false; 143 } 144 String result = mSupplicantHook.doCustomSupplicantCommand(anqpGet); 145 if (result != null && result.startsWith("OK")) { 146 Log.d(Utils.hs2LogTag(getClass()), "ANQP initiated on " 147 + Utils.macToString(bssid) + " (" + anqpGet + ")"); 148 return true; 149 } 150 else { 151 Log.d(Utils.hs2LogTag(getClass()), "ANQP failed on " + 152 Utils.macToString(bssid) + ": " + result); 153 return false; 154 } 155 } 156 157 /** 158 * Request a passpoint icon file |filename| from the specified AP |bssid|. 159 * @param bssid BSSID of the AP 160 * @param fileName name of the icon file 161 * @return true if request is sent successfully, false otherwise 162 */ 163 public boolean requestIcon(long bssid, String fileName) { 164 String result = mSupplicantHook.doCustomSupplicantCommand("REQ_HS20_ICON " + 165 Utils.macToString(bssid) + " " + fileName); 166 return result != null && result.startsWith("OK"); 167 } 168 169 /** 170 * Invoked when ANQP query is completed. 171 * TODO(zqiu): currently ANQP completion notification is through WifiMonitor, 172 * this shouldn't be needed once we switch over to wificond for ANQP requests. 173 * @param bssid BSSID of the AP 174 * @param success true if query is completed successfully, false otherwise 175 */ 176 public void notifyANQPDone(long bssid, boolean success) { 177 Map<Constants.ANQPElementType, ANQPElement> elements = null; 178 if (success) { 179 String bssData = 180 mSupplicantHook.scanResult(Utils.macToString(bssid)); 181 try { 182 elements = parseWPSData(bssData); 183 Log.d(Utils.hs2LogTag(getClass()), 184 String.format("Successful ANQP response for %012x: %s", 185 bssid, elements)); 186 } catch (IOException | BufferUnderflowException e) { 187 Log.e(Utils.hs2LogTag(getClass()), "Failed to parse ANQP: " + 188 e.toString() + ": " + bssData); 189 } 190 } 191 mCallbacks.onANQPResponse(bssid, elements); 192 } 193 194 /** 195 * Invoked when icon query is completed. 196 * TODO(zqiu): currently icon completion notification is through WifiMonitor, 197 * this shouldn't be needed once we switch over to wificond for icon requests. 198 * @param bssid BSSID of the AP 199 * @param iconEvent icon event data 200 */ 201 public void notifyIconDone(long bssid, IconEvent iconEvent) { 202 String filename = null; 203 byte[] data = null; 204 if (iconEvent != null) { 205 try { 206 data = retrieveIcon(iconEvent); 207 filename = iconEvent.getFileName(); 208 } catch (IOException ioe) { 209 Log.e(Utils.hs2LogTag(getClass()), "Failed to retrieve icon: " + 210 ioe.toString() + ": " + iconEvent.getFileName()); 211 } 212 } 213 mCallbacks.onIconResponse(bssid, filename, data); 214 } 215 216 /** 217 * Invoked when a Wireless Network Management (WNM) frame is received. 218 * TODO(zqiu): currently WNM frame notification is through WifiMonitor, 219 * this shouldn't be needed once we switch over to wificond for WNM frame monitoring. 220 * @param data WNM frame data 221 */ 222 public void notifyWnmFrameReceived(WnmData data) { 223 mCallbacks.onWnmFrameReceived(data); 224 } 225 226 /** 227 * Build a wpa_supplicant ANQP query command 228 * @param bssid BSSID of the AP to be queried 229 * @param querySet elements to query 230 * @return A command string. 231 */ 232 private static String buildWPSQueryRequest(long bssid, 233 List<Constants.ANQPElementType> querySet) { 234 235 boolean baseANQPElements = Constants.hasBaseANQPElements(querySet); 236 StringBuilder sb = new StringBuilder(); 237 if (baseANQPElements) { 238 sb.append("ANQP_GET "); 239 } 240 else { 241 // ANQP_GET does not work for a sole hs20:8 (OSU) query 242 sb.append("HS20_ANQP_GET "); 243 } 244 sb.append(Utils.macToString(bssid)).append(' '); 245 246 boolean first = true; 247 for (Constants.ANQPElementType elementType : querySet) { 248 if (first) { 249 first = false; 250 } 251 else { 252 sb.append(','); 253 } 254 255 Integer id = Constants.getANQPElementID(elementType); 256 if (id != null) { 257 sb.append(id); 258 } 259 else { 260 id = Constants.getHS20ElementID(elementType); 261 if (baseANQPElements) { 262 sb.append("hs20:"); 263 } 264 sb.append(id); 265 } 266 } 267 268 return sb.toString(); 269 } 270 271 private static Map<Constants.ANQPElementType, ANQPElement> parseWPSData(String bssInfo) 272 throws IOException { 273 Map<Constants.ANQPElementType, ANQPElement> elements = new HashMap<>(); 274 if (bssInfo == null) { 275 return elements; 276 } 277 BufferedReader lineReader = new BufferedReader(new StringReader(bssInfo)); 278 String line; 279 while ((line=lineReader.readLine()) != null) { 280 ANQPElement element = buildElement(line); 281 if (element != null) { 282 elements.put(element.getID(), element); 283 } 284 } 285 return elements; 286 } 287 288 private static ANQPElement buildElement(String text) throws ProtocolException { 289 int separator = text.indexOf('='); 290 if (separator < 0 || separator + 1 == text.length()) { 291 return null; 292 } 293 294 String elementName = text.substring(0, separator); 295 Constants.ANQPElementType elementType = sWpsNames.get(elementName); 296 if (elementType == null) { 297 return null; 298 } 299 300 byte[] payload; 301 try { 302 payload = Utils.hexToBytes(text.substring(separator + 1)); 303 } 304 catch (NumberFormatException nfe) { 305 Log.e(Utils.hs2LogTag(PasspointEventHandler.class), 306 "Failed to parse hex string"); 307 return null; 308 } 309 // Wrap the payload inside a ByteBuffer. 310 ByteBuffer buffer = ByteBuffer.wrap(payload); 311 312 return Constants.getANQPElementID(elementType) != null ? 313 ANQPFactory.buildElement(elementType, buffer) : 314 ANQPFactory.buildHS20Element(elementType, buffer); 315 } 316 317 private byte[] retrieveIcon(IconEvent iconEvent) throws IOException { 318 byte[] iconData = new byte[iconEvent.getSize()]; 319 try { 320 int offset = 0; 321 while (offset < iconEvent.getSize()) { 322 int size = Math.min(iconEvent.getSize() - offset, ICON_CHUNK_SIZE); 323 324 String command = String.format("GET_HS20_ICON %s %s %d %d", 325 Utils.macToString(iconEvent.getBSSID()), iconEvent.getFileName(), 326 offset, size); 327 Log.d(Utils.hs2LogTag(getClass()), "Issuing '" + command + "'"); 328 String response = mSupplicantHook.doCustomSupplicantCommand(command); 329 if (response == null) { 330 throw new IOException("No icon data returned"); 331 } 332 333 try { 334 byte[] fragment = Base64.decode(response, Base64.DEFAULT); 335 if (fragment.length == 0) { 336 throw new IOException("Null data for '" + command + "': " + response); 337 } 338 if (fragment.length + offset > iconData.length) { 339 throw new IOException("Icon chunk exceeds image size"); 340 } 341 System.arraycopy(fragment, 0, iconData, offset, fragment.length); 342 offset += fragment.length; 343 } catch (IllegalArgumentException iae) { 344 throw new IOException("Failed to parse response to '" + command 345 + "': " + response); 346 } 347 } 348 if (offset != iconEvent.getSize()) { 349 Log.w(Utils.hs2LogTag(getClass()), "Partial icon data: " + offset + 350 ", expected " + iconEvent.getSize()); 351 } 352 } 353 finally { 354 // Delete the icon file in supplicant. 355 Log.d(Utils.hs2LogTag(getClass()), "Deleting icon for " + iconEvent); 356 String result = mSupplicantHook.doCustomSupplicantCommand("DEL_HS20_ICON " + 357 Utils.macToString(iconEvent.getBSSID()) + " " + iconEvent.getFileName()); 358 Log.d(Utils.hs2LogTag(getClass()), "Result: " + result); 359 } 360 361 return iconData; 362 } 363} 364