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