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