1package com.android.server.wifi.hotspot2;
2
3import android.util.Base64;
4import android.util.Log;
5
6import com.android.server.wifi.ScanDetail;
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 SupplicantBridgeCallbacks mCallbacks;
33    private final Map<Long, ScanDetail> mRequestMap = new HashMap<>();
34
35    private static final int IconChunkSize = 1400;  // 2K*3/4 - overhead
36    private static final Map<String, Constants.ANQPElementType> sWpsNames = new HashMap<>();
37
38    static {
39        sWpsNames.put("anqp_venue_name", Constants.ANQPElementType.ANQPVenueName);
40        sWpsNames.put("anqp_network_auth_type", Constants.ANQPElementType.ANQPNwkAuthType);
41        sWpsNames.put("anqp_roaming_consortium", Constants.ANQPElementType.ANQPRoamingConsortium);
42        sWpsNames.put("anqp_ip_addr_type_availability",
43                Constants.ANQPElementType.ANQPIPAddrAvailability);
44        sWpsNames.put("anqp_nai_realm", Constants.ANQPElementType.ANQPNAIRealm);
45        sWpsNames.put("anqp_3gpp", Constants.ANQPElementType.ANQP3GPPNetwork);
46        sWpsNames.put("anqp_domain_name", Constants.ANQPElementType.ANQPDomName);
47        sWpsNames.put("hs20_operator_friendly_name", Constants.ANQPElementType.HSFriendlyName);
48        sWpsNames.put("hs20_wan_metrics", Constants.ANQPElementType.HSWANMetrics);
49        sWpsNames.put("hs20_connection_capability", Constants.ANQPElementType.HSConnCapability);
50        sWpsNames.put("hs20_operating_class", Constants.ANQPElementType.HSOperatingclass);
51        sWpsNames.put("hs20_osu_providers_list", Constants.ANQPElementType.HSOSUProviders);
52    }
53
54    /**
55     * Interface to be implemented by the client to receive callbacks from SupplicantBridge.
56     */
57    public interface SupplicantBridgeCallbacks {
58        /**
59         * Response from supplicant bridge for the initiated request.
60         * @param scanDetail
61         * @param anqpElements
62         */
63        void notifyANQPResponse(
64                ScanDetail scanDetail,
65                Map<Constants.ANQPElementType, ANQPElement> anqpElements);
66
67        /**
68         * Notify failure.
69         * @param bssid
70         */
71        void notifyIconFailed(long bssid);
72    }
73
74    public static boolean isAnqpAttribute(String line) {
75        int split = line.indexOf('=');
76        return split >= 0 && sWpsNames.containsKey(line.substring(0, split));
77    }
78
79    public SupplicantBridge(WifiNative supplicantHook, SupplicantBridgeCallbacks callbacks) {
80        mSupplicantHook = supplicantHook;
81        mCallbacks = callbacks;
82    }
83
84    public static Map<Constants.ANQPElementType, ANQPElement> parseANQPLines(List<String> lines) {
85        if (lines == null) {
86            return null;
87        }
88        Map<Constants.ANQPElementType, ANQPElement> elements = new HashMap<>(lines.size());
89        for (String line : lines) {
90            try {
91                ANQPElement element = buildElement(line);
92                if (element != null) {
93                    elements.put(element.getID(), element);
94                }
95            }
96            catch (ProtocolException pe) {
97                Log.e(Utils.hs2LogTag(SupplicantBridge.class), "Failed to parse ANQP: " + pe);
98            }
99        }
100        return elements;
101    }
102
103    public boolean startANQP(ScanDetail scanDetail, List<Constants.ANQPElementType> elements) {
104        String anqpGet = buildWPSQueryRequest(scanDetail.getNetworkDetail(), elements);
105        if (anqpGet == null) {
106            return false;
107        }
108        synchronized (mRequestMap) {
109            mRequestMap.put(scanDetail.getNetworkDetail().getBSSID(), scanDetail);
110        }
111        String result = mSupplicantHook.doCustomSupplicantCommand(anqpGet);
112        if (result != null && result.startsWith("OK")) {
113            Log.d(Utils.hs2LogTag(getClass()), "ANQP initiated on "
114                    + scanDetail + " (" + anqpGet + ")");
115            return true;
116        }
117        else {
118            Log.d(Utils.hs2LogTag(getClass()), "ANQP failed on " +
119                    scanDetail + ": " + result);
120            return false;
121        }
122    }
123
124    public boolean doIconQuery(long bssid, String fileName) {
125        String result = mSupplicantHook.doCustomSupplicantCommand("REQ_HS20_ICON " +
126                Utils.macToString(bssid) + " " + fileName);
127        return result != null && result.startsWith("OK");
128    }
129
130    public byte[] retrieveIcon(IconEvent iconEvent) throws IOException {
131        byte[] iconData = new byte[iconEvent.getSize()];
132        try {
133            int offset = 0;
134            while (offset < iconEvent.getSize()) {
135                int size = Math.min(iconEvent.getSize() - offset, IconChunkSize);
136
137                String command = String.format("GET_HS20_ICON %s %s %d %d",
138                        Utils.macToString(iconEvent.getBSSID()), iconEvent.getFileName(),
139                        offset, size);
140                Log.d(Utils.hs2LogTag(getClass()), "Issuing '" + command + "'");
141                String response = mSupplicantHook.doCustomSupplicantCommand(command);
142                if (response == null) {
143                    throw new IOException("No icon data returned");
144                }
145
146                try {
147                    byte[] fragment = Base64.decode(response, Base64.DEFAULT);
148                    if (fragment.length == 0) {
149                        throw new IOException("Null data for '" + command + "': " + response);
150                    }
151                    if (fragment.length + offset > iconData.length) {
152                        throw new IOException("Icon chunk exceeds image size");
153                    }
154                    System.arraycopy(fragment, 0, iconData, offset, fragment.length);
155                    offset += fragment.length;
156                } catch (IllegalArgumentException iae) {
157                    throw new IOException("Failed to parse response to '" + command
158                            + "': " + response);
159                }
160            }
161            if (offset != iconEvent.getSize()) {
162                Log.w(Utils.hs2LogTag(getClass()), "Partial icon data: " + offset +
163                        ", expected " + iconEvent.getSize());
164            }
165        }
166        finally {
167            Log.d(Utils.hs2LogTag(getClass()), "Deleting icon for " + iconEvent);
168            String result = mSupplicantHook.doCustomSupplicantCommand("DEL_HS20_ICON " +
169                    Utils.macToString(iconEvent.getBSSID()) + " " + iconEvent.getFileName());
170        }
171
172        return iconData;
173    }
174
175    public void notifyANQPDone(Long bssid, boolean success) {
176        ScanDetail scanDetail;
177        synchronized (mRequestMap) {
178            scanDetail = mRequestMap.remove(bssid);
179        }
180
181        if (scanDetail == null) {
182            if (!success) {
183                mCallbacks.notifyIconFailed(bssid);
184            }
185            return;
186        }
187
188        String bssData = mSupplicantHook.scanResult(scanDetail.getBSSIDString());
189        try {
190            Map<Constants.ANQPElementType, ANQPElement> elements = parseWPSData(bssData);
191            Log.d(Utils.hs2LogTag(getClass()), String.format("%s ANQP response for %012x: %s",
192                    success ? "successful" : "failed", bssid, elements));
193            mCallbacks.notifyANQPResponse(scanDetail, success ? elements : null);
194        }
195        catch (IOException ioe) {
196            Log.e(Utils.hs2LogTag(getClass()), "Failed to parse ANQP: " +
197                    ioe.toString() + ": " + bssData);
198        }
199        catch (RuntimeException rte) {
200            Log.e(Utils.hs2LogTag(getClass()), "Failed to parse ANQP: " +
201                    rte.toString() + ": " + bssData, rte);
202        }
203        mCallbacks.notifyANQPResponse(scanDetail, null);
204    }
205
206    private static String escapeSSID(NetworkDetail networkDetail) {
207        return escapeString(networkDetail.getSSID(), networkDetail.isSSID_UTF8());
208    }
209
210    private static String escapeString(String s, boolean utf8) {
211        boolean asciiOnly = true;
212        for (int n = 0; n < s.length(); n++) {
213            char ch = s.charAt(n);
214            if (ch > 127) {
215                asciiOnly = false;
216                break;
217            }
218        }
219
220        if (asciiOnly) {
221            return '"' + s + '"';
222        }
223        else {
224            byte[] octets = s.getBytes(utf8 ? StandardCharsets.UTF_8 : StandardCharsets.ISO_8859_1);
225
226            StringBuilder sb = new StringBuilder();
227            for (byte octet : octets) {
228                sb.append(String.format("%02x", octet & Constants.BYTE_MASK));
229            }
230            return sb.toString();
231        }
232    }
233
234    /**
235     * Build a wpa_supplicant ANQP query command
236     * @param networkDetail The network to query.
237     * @param querySet elements to query
238     * @return A command string.
239     */
240    private static String buildWPSQueryRequest(NetworkDetail networkDetail,
241                                               List<Constants.ANQPElementType> querySet) {
242
243        boolean baseANQPElements = Constants.hasBaseANQPElements(querySet);
244        StringBuilder sb = new StringBuilder();
245        if (baseANQPElements) {
246            sb.append("ANQP_GET ");
247        }
248        else {
249            sb.append("HS20_ANQP_GET ");     // ANQP_GET does not work for a sole hs20:8 (OSU) query
250        }
251        sb.append(networkDetail.getBSSIDString()).append(' ');
252
253        boolean first = true;
254        for (Constants.ANQPElementType elementType : querySet) {
255            if (first) {
256                first = false;
257            }
258            else {
259                sb.append(',');
260            }
261
262            Integer id = Constants.getANQPElementID(elementType);
263            if (id != null) {
264                sb.append(id);
265            }
266            else {
267                id = Constants.getHS20ElementID(elementType);
268                if (baseANQPElements) {
269                    sb.append("hs20:");
270                }
271                sb.append(id);
272            }
273        }
274
275        return sb.toString();
276    }
277
278    private static List<String> getWPSNetCommands(String netID, NetworkDetail networkDetail,
279                                                 Credential credential) {
280
281        List<String> commands = new ArrayList<String>();
282
283        EAPMethod eapMethod = credential.getEAPMethod();
284        commands.add(String.format("SET_NETWORK %s key_mgmt WPA-EAP", netID));
285        commands.add(String.format("SET_NETWORK %s ssid %s", netID, escapeSSID(networkDetail)));
286        commands.add(String.format("SET_NETWORK %s bssid %s",
287                netID, networkDetail.getBSSIDString()));
288        commands.add(String.format("SET_NETWORK %s eap %s",
289                netID, mapEAPMethodName(eapMethod.getEAPMethodID())));
290
291        AuthParam authParam = credential.getEAPMethod().getAuthParam();
292        if (authParam == null) {
293            return null;            // TLS or SIM/AKA
294        }
295        switch (authParam.getAuthInfoID()) {
296            case NonEAPInnerAuthType:
297            case InnerAuthEAPMethodType:
298                commands.add(String.format("SET_NETWORK %s identity %s",
299                        netID, escapeString(credential.getUserName(), true)));
300                commands.add(String.format("SET_NETWORK %s password %s",
301                        netID, escapeString(credential.getPassword(), true)));
302                commands.add(String.format("SET_NETWORK %s anonymous_identity \"anonymous\"",
303                        netID));
304                break;
305            default:                // !!! Needs work.
306                return null;
307        }
308        commands.add(String.format("SET_NETWORK %s priority 0", netID));
309        commands.add(String.format("ENABLE_NETWORK %s", netID));
310        commands.add(String.format("SAVE_CONFIG"));
311        return commands;
312    }
313
314    private static Map<Constants.ANQPElementType, ANQPElement> parseWPSData(String bssInfo)
315            throws IOException {
316        Map<Constants.ANQPElementType, ANQPElement> elements = new HashMap<>();
317        if (bssInfo == null) {
318            return elements;
319        }
320        BufferedReader lineReader = new BufferedReader(new StringReader(bssInfo));
321        String line;
322        while ((line=lineReader.readLine()) != null) {
323            ANQPElement element = buildElement(line);
324            if (element != null) {
325                elements.put(element.getID(), element);
326            }
327        }
328        return elements;
329    }
330
331    private static ANQPElement buildElement(String text) throws ProtocolException {
332        int separator = text.indexOf('=');
333        if (separator < 0) {
334            return null;
335        }
336
337        String elementName = text.substring(0, separator);
338        Constants.ANQPElementType elementType = sWpsNames.get(elementName);
339        if (elementType == null) {
340            return null;
341        }
342
343        byte[] payload;
344        try {
345            payload = Utils.hexToBytes(text.substring(separator + 1));
346        }
347        catch (NumberFormatException nfe) {
348            Log.e(Utils.hs2LogTag(SupplicantBridge.class), "Failed to parse hex string");
349            return null;
350        }
351        return Constants.getANQPElementID(elementType) != null ?
352                ANQPFactory.buildElement(ByteBuffer.wrap(payload), elementType, payload.length) :
353                ANQPFactory.buildHS20Element(elementType,
354                        ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN));
355    }
356
357    private static String mapEAPMethodName(EAP.EAPMethodID eapMethodID) {
358        switch (eapMethodID) {
359            case EAP_AKA:
360                return "AKA";
361            case EAP_AKAPrim:
362                return "AKA'";  // eap.c:1514
363            case EAP_SIM:
364                return "SIM";
365            case EAP_TLS:
366                return "TLS";
367            case EAP_TTLS:
368                return "TTLS";
369            default:
370                throw new IllegalArgumentException("No mapping for " + eapMethodID);
371        }
372    }
373
374    private static final Map<Character,Integer> sMappings = new HashMap<Character, Integer>();
375
376    static {
377        sMappings.put('\\', (int)'\\');
378        sMappings.put('"', (int)'"');
379        sMappings.put('e', 0x1b);
380        sMappings.put('n', (int)'\n');
381        sMappings.put('r', (int)'\n');
382        sMappings.put('t', (int)'\t');
383    }
384
385    public static String unescapeSSID(String ssid) {
386
387        CharIterator chars = new CharIterator(ssid);
388        byte[] octets = new byte[ssid.length()];
389        int bo = 0;
390
391        while (chars.hasNext()) {
392            char ch = chars.next();
393            if (ch != '\\' || ! chars.hasNext()) {
394                octets[bo++] = (byte)ch;
395            }
396            else {
397                char suffix = chars.next();
398                Integer mapped = sMappings.get(suffix);
399                if (mapped != null) {
400                    octets[bo++] = mapped.byteValue();
401                }
402                else if (suffix == 'x' && chars.hasDoubleHex()) {
403                    octets[bo++] = (byte)chars.nextDoubleHex();
404                }
405                else {
406                    octets[bo++] = '\\';
407                    octets[bo++] = (byte)suffix;
408                }
409            }
410        }
411
412        boolean asciiOnly = true;
413        for (byte b : octets) {
414            if ((b&0x80) != 0) {
415                asciiOnly = false;
416                break;
417            }
418        }
419        if (asciiOnly) {
420            return new String(octets, 0, bo, StandardCharsets.UTF_8);
421        } else {
422            try {
423                // If UTF-8 decoding is successful it is almost certainly UTF-8
424                CharBuffer cb = StandardCharsets.UTF_8.newDecoder().decode(
425                        ByteBuffer.wrap(octets, 0, bo));
426                return cb.toString();
427            } catch (CharacterCodingException cce) {
428                return new String(octets, 0, bo, StandardCharsets.ISO_8859_1);
429            }
430        }
431    }
432
433    private static class CharIterator {
434        private final String mString;
435        private int mPosition;
436        private int mHex;
437
438        private CharIterator(String s) {
439            mString = s;
440        }
441
442        private boolean hasNext() {
443            return mPosition < mString.length();
444        }
445
446        private char next() {
447            return mString.charAt(mPosition++);
448        }
449
450        private boolean hasDoubleHex() {
451            if (mString.length() - mPosition < 2) {
452                return false;
453            }
454            int nh = Utils.fromHex(mString.charAt(mPosition), true);
455            if (nh < 0) {
456                return false;
457            }
458            int nl = Utils.fromHex(mString.charAt(mPosition + 1), true);
459            if (nl < 0) {
460                return false;
461            }
462            mPosition += 2;
463            mHex = (nh << 4) | nl;
464            return true;
465        }
466
467        private int nextDoubleHex() {
468            return mHex;
469        }
470    }
471
472    private static final String[] TestStrings = {
473            "test-ssid",
474            "test\\nss\\tid",
475            "test\\x2d\\x5f\\nss\\tid",
476            "test\\x2d\\x5f\\nss\\tid\\\\",
477            "test\\x2d\\x5f\\nss\\tid\\n",
478            "test\\x2d\\x5f\\nss\\tid\\x4a",
479            "another\\",
480            "an\\other",
481            "another\\x2"
482    };
483
484    public static void main(String[] args) {
485        for (String string : TestStrings) {
486            System.out.println(unescapeSSID(string));
487        }
488    }
489}
490