SupplicantBridge.java revision 42a1bf5e9b21288524d9a5fe90556abefc035879
1package com.android.server.wifi.hotspot2;
2
3import android.util.Log;
4
5import com.android.server.wifi.ScanDetail;
6import com.android.server.wifi.WifiConfigStore;
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.BufferUnderflowException;
21import java.nio.ByteBuffer;
22import java.nio.ByteOrder;
23import java.nio.CharBuffer;
24import java.nio.charset.CharacterCodingException;
25import java.nio.charset.StandardCharsets;
26import java.util.ArrayList;
27import java.util.HashMap;
28import java.util.List;
29import java.util.Map;
30
31public class SupplicantBridge {
32    private final WifiNative mSupplicantHook;
33    private final WifiConfigStore mConfigStore;
34    private final Map<Long, ScanDetail> mRequestMap = new HashMap<>();
35
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    public static boolean isAnqpAttribute(String line) {
55        int split = line.indexOf('=');
56        return split >= 0 && sWpsNames.containsKey(line.substring(0, split));
57    }
58
59    public SupplicantBridge(WifiNative supplicantHook, WifiConfigStore configStore) {
60        mSupplicantHook = supplicantHook;
61        mConfigStore = configStore;
62    }
63
64    public static Map<Constants.ANQPElementType, ANQPElement> parseANQPLines(List<String> lines) {
65        if (lines == null) {
66            return null;
67        }
68        Map<Constants.ANQPElementType, ANQPElement> elements = new HashMap<>(lines.size());
69        for (String line : lines) {
70            try {
71                ANQPElement element = buildElement(line);
72                if (element != null) {
73                    elements.put(element.getID(), element);
74                }
75            }
76            catch (ProtocolException pe) {
77                Log.e(Utils.hs2LogTag(SupplicantBridge.class), "Failed to parse ANQP: " + pe);
78            }
79        }
80        return elements;
81    }
82
83    public void startANQP(ScanDetail scanDetail) {
84        String anqpGet = buildWPSQueryRequest(scanDetail.getNetworkDetail());
85        synchronized (mRequestMap) {
86            mRequestMap.put(scanDetail.getNetworkDetail().getBSSID(), scanDetail);
87        }
88        String result = mSupplicantHook.doCustomCommand(anqpGet);
89        if (result != null && result.startsWith("OK")) {
90            Log.d(Utils.hs2LogTag(getClass()), "ANQP initiated on " + scanDetail);
91        }
92        else {
93            Log.d(Utils.hs2LogTag(getClass()), "ANQP failed on " +
94                    scanDetail + ": " + result);
95        }
96    }
97
98    public void notifyANQPDone(Long bssid, boolean success) {
99        ScanDetail scanDetail;
100        synchronized (mRequestMap) {
101            scanDetail = mRequestMap.remove(bssid);
102        }
103        if (scanDetail == null) {
104            Log.d(Utils.hs2LogTag(getClass()), String.format("Spurious %s ANQP response for %012x",
105                            success ? "successful" : "failed", bssid));
106            return;
107        }
108
109        String bssData = mSupplicantHook.scanResult(scanDetail.getBSSIDString());
110        try {
111            Map<Constants.ANQPElementType, ANQPElement> elements = parseWPSData(bssData);
112            Log.d(Utils.hs2LogTag(getClass()), String.format("%s ANQP response for %012x: %s",
113                    success ? "successful" : "failed", bssid, elements));
114            mConfigStore.notifyANQPResponse(scanDetail, success ? elements : null);
115        }
116        catch (IOException ioe) {
117            Log.e(Utils.hs2LogTag(getClass()), "Failed to parse ANQP: " +
118                    ioe.toString() + ": " + bssData);
119        }
120        catch (RuntimeException rte) {
121            Log.e(Utils.hs2LogTag(getClass()), "Failed to parse ANQP: " +
122                    rte.toString() + ": " + bssData, rte);
123        }
124        mConfigStore.notifyANQPResponse(scanDetail, null);
125    }
126
127    /*
128    public boolean addCredential(HomeSP homeSP, NetworkDetail networkDetail) {
129        Credential credential = homeSP.getCredential();
130        if (credential == null)
131            return false;
132
133        String nwkID = null;
134        if (mLastSSID != null) {
135            String nwkList = mSupplicantHook.doCustomCommand("LIST_NETWORKS");
136
137            BufferedReader reader = new BufferedReader(new StringReader(nwkList));
138            String line;
139            try {
140                while ((line = reader.readLine()) != null) {
141                    String[] tokens = line.split("\\t");
142                    if (tokens.length < 2 || ! Utils.isDecimal(tokens[0])) {
143                        continue;
144                    }
145                    if (unescapeSSID(tokens[1]).equals(mLastSSID)) {
146                        nwkID = tokens[0];
147                        Log.d("HS2J", "Network " + tokens[0] +
148                                " matches last SSID '" + mLastSSID + "'");
149                        break;
150                    }
151                }
152            }
153            catch (IOException ioe) {
154                //
155            }
156        }
157
158        if (nwkID == null) {
159            nwkID = mSupplicantHook.doCustomCommand("ADD_NETWORK");
160            Log.d("HS2J", "add_network: '" + nwkID + "'");
161            if (! Utils.isDecimal(nwkID)) {
162                return false;
163            }
164        }
165
166        List<String> credCommand = getWPSNetCommands(nwkID, networkDetail, credential);
167        for (String command : credCommand) {
168            String status = mSupplicantHook.doCustomCommand(command);
169            Log.d("HS2J", "Status of '" + command + "': '" + status + "'");
170        }
171
172        if (! networkDetail.getSSID().equals(mLastSSID)) {
173            mLastSSID = networkDetail.getSSID();
174            PrintWriter out = null;
175            try {
176                out = new PrintWriter(new OutputStreamWriter(
177                        new FileOutputStream(mLastSSIDFile, false), StandardCharsets.UTF_8));
178                out.println(mLastSSID);
179            } catch (IOException ioe) {
180            //
181            } finally {
182                if (out != null) {
183                    out.close();
184                }
185            }
186        }
187
188        return true;
189    }
190    */
191
192    private static String escapeSSID(NetworkDetail networkDetail) {
193        return escapeString(networkDetail.getSSID(), networkDetail.isSSID_UTF8());
194    }
195
196    private static String escapeString(String s, boolean utf8) {
197        boolean asciiOnly = true;
198        for (int n = 0; n < s.length(); n++) {
199            char ch = s.charAt(n);
200            if (ch > 127) {
201                asciiOnly = false;
202                break;
203            }
204        }
205
206        if (asciiOnly) {
207            return '"' + s + '"';
208        }
209        else {
210            byte[] octets = s.getBytes(utf8 ? StandardCharsets.UTF_8 : StandardCharsets.ISO_8859_1);
211
212            StringBuilder sb = new StringBuilder();
213            for (byte octet : octets) {
214                sb.append(String.format("%02x", octet & Constants.BYTE_MASK));
215            }
216            return sb.toString();
217        }
218    }
219
220    private static String buildWPSQueryRequest(NetworkDetail networkDetail) {
221        StringBuilder sb = new StringBuilder();
222        sb.append("ANQP_GET ").append(networkDetail.getBSSIDString()).append(' ');
223
224        boolean first = true;
225        for (Constants.ANQPElementType elementType : ANQPFactory.getBaseANQPSet()) {
226            if (networkDetail.getAnqpOICount() == 0 &&
227                    elementType == Constants.ANQPElementType.ANQPRoamingConsortium) {
228                continue;
229            }
230            if (first) {
231                first = false;
232            }
233            else {
234                sb.append(',');
235            }
236            sb.append(Constants.getANQPElementID(elementType));
237        }
238        if (networkDetail.getHSRelease() != null) {
239            for (Constants.ANQPElementType elementType : ANQPFactory.getHS20ANQPSet()) {
240                sb.append(",hs20:").append(Constants.getHS20ElementID(elementType));
241            }
242        }
243        return sb.toString();
244    }
245
246    private static List<String> getWPSNetCommands(String netID, NetworkDetail networkDetail,
247                                                 Credential credential) {
248
249        List<String> commands = new ArrayList<String>();
250
251        EAPMethod eapMethod = credential.getEAPMethod();
252        commands.add(String.format("SET_NETWORK %s key_mgmt WPA-EAP", netID));
253        commands.add(String.format("SET_NETWORK %s ssid %s", netID, escapeSSID(networkDetail)));
254        commands.add(String.format("SET_NETWORK %s bssid %s",
255                netID, networkDetail.getBSSIDString()));
256        commands.add(String.format("SET_NETWORK %s eap %s",
257                netID, mapEAPMethodName(eapMethod.getEAPMethodID())));
258
259        AuthParam authParam = credential.getEAPMethod().getAuthParam();
260        if (authParam == null) {
261            return null;            // TLS or SIM/AKA
262        }
263        switch (authParam.getAuthInfoID()) {
264            case NonEAPInnerAuthType:
265            case InnerAuthEAPMethodType:
266                commands.add(String.format("SET_NETWORK %s identity %s",
267                        netID, escapeString(credential.getUserName(), true)));
268                commands.add(String.format("SET_NETWORK %s password %s",
269                        netID, escapeString(credential.getPassword(), true)));
270                commands.add(String.format("SET_NETWORK %s anonymous_identity \"anonymous\"",
271                        netID));
272                break;
273            default:                // !!! Needs work.
274                return null;
275        }
276        commands.add(String.format("SET_NETWORK %s priority 0", netID));
277        commands.add(String.format("ENABLE_NETWORK %s", netID));
278        commands.add(String.format("SAVE_CONFIG"));
279        return commands;
280    }
281
282    private static Map<Constants.ANQPElementType, ANQPElement> parseWPSData(String bssInfo)
283            throws IOException {
284        Map<Constants.ANQPElementType, ANQPElement> elements = new HashMap<>();
285        if (bssInfo == null) {
286            return elements;
287        }
288        BufferedReader lineReader = new BufferedReader(new StringReader(bssInfo));
289        String line;
290        while ((line=lineReader.readLine()) != null) {
291            ANQPElement element = buildElement(line);
292            if (element != null) {
293                elements.put(element.getID(), element);
294            }
295        }
296        return elements;
297    }
298
299    private static ANQPElement buildElement(String text) throws ProtocolException {
300        int separator = text.indexOf('=');
301        if (separator < 0) {
302            return null;
303        }
304
305        String elementName = text.substring(0, separator);
306        Constants.ANQPElementType elementType = sWpsNames.get(elementName);
307        if (elementType == null) {
308            return null;
309        }
310
311        byte[] payload;
312        try {
313            payload = Utils.hexToBytes(text.substring(separator + 1));
314        }
315        catch (NumberFormatException nfe) {
316            Log.e(Utils.hs2LogTag(SupplicantBridge.class), "Failed to parse hex string");
317            return null;
318        }
319        return Constants.getANQPElementID(elementType) != null ?
320                ANQPFactory.buildElement(ByteBuffer.wrap(payload), elementType, payload.length) :
321                ANQPFactory.buildHS20Element(elementType,
322                        ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN));
323    }
324
325    private static String mapEAPMethodName(EAP.EAPMethodID eapMethodID) {
326        switch (eapMethodID) {
327            case EAP_AKA:
328                return "AKA";
329            case EAP_AKAPrim:
330                return "AKA'";  // eap.c:1514
331            case EAP_SIM:
332                return "SIM";
333            case EAP_TLS:
334                return "TLS";
335            case EAP_TTLS:
336                return "TTLS";
337            default:
338                throw new IllegalArgumentException("No mapping for " + eapMethodID);
339        }
340    }
341
342    private static final Map<Character,Integer> sMappings = new HashMap<Character, Integer>();
343
344    static {
345        sMappings.put('\\', (int)'\\');
346        sMappings.put('"', (int)'"');
347        sMappings.put('e', 0x1b);
348        sMappings.put('n', (int)'\n');
349        sMappings.put('r', (int)'\n');
350        sMappings.put('t', (int)'\t');
351    }
352
353    public static String unescapeSSID(String ssid) {
354
355        CharIterator chars = new CharIterator(ssid);
356        byte[] octets = new byte[ssid.length()];
357        int bo = 0;
358
359        while (chars.hasNext()) {
360            char ch = chars.next();
361            if (ch != '\\' || ! chars.hasNext()) {
362                octets[bo++] = (byte)ch;
363            }
364            else {
365                char suffix = chars.next();
366                Integer mapped = sMappings.get(suffix);
367                if (mapped != null) {
368                    octets[bo++] = mapped.byteValue();
369                }
370                else if (suffix == 'x' && chars.hasDoubleHex()) {
371                    octets[bo++] = (byte)chars.nextDoubleHex();
372                }
373                else {
374                    octets[bo++] = '\\';
375                    octets[bo++] = (byte)suffix;
376                }
377            }
378        }
379
380        boolean asciiOnly = true;
381        for (byte b : octets) {
382            if ((b&0x80) != 0) {
383                asciiOnly = false;
384                break;
385            }
386        }
387        if (asciiOnly) {
388            return new String(octets, 0, bo, StandardCharsets.UTF_8);
389        } else {
390            try {
391                // If UTF-8 decoding is successful it is almost certainly UTF-8
392                CharBuffer cb = StandardCharsets.UTF_8.newDecoder().decode(
393                        ByteBuffer.wrap(octets, 0, bo));
394                return cb.toString();
395            } catch (CharacterCodingException cce) {
396                return new String(octets, 0, bo, StandardCharsets.ISO_8859_1);
397            }
398        }
399    }
400
401    private static class CharIterator {
402        private final String mString;
403        private int mPosition;
404        private int mHex;
405
406        private CharIterator(String s) {
407            mString = s;
408        }
409
410        private boolean hasNext() {
411            return mPosition < mString.length();
412        }
413
414        private char next() {
415            return mString.charAt(mPosition++);
416        }
417
418        private boolean hasDoubleHex() {
419            if (mString.length() - mPosition < 2) {
420                return false;
421            }
422            int nh = Utils.fromHex(mString.charAt(mPosition), true);
423            if (nh < 0) {
424                return false;
425            }
426            int nl = Utils.fromHex(mString.charAt(mPosition + 1), true);
427            if (nl < 0) {
428                return false;
429            }
430            mPosition += 2;
431            mHex = (nh << 4) | nl;
432            return true;
433        }
434
435        private int nextDoubleHex() {
436            return mHex;
437        }
438    }
439
440    private static final String[] TestStrings = {
441            "test-ssid",
442            "test\\nss\\tid",
443            "test\\x2d\\x5f\\nss\\tid",
444            "test\\x2d\\x5f\\nss\\tid\\\\",
445            "test\\x2d\\x5f\\nss\\tid\\n",
446            "test\\x2d\\x5f\\nss\\tid\\x4a",
447            "another\\",
448            "an\\other",
449            "another\\x2"
450    };
451
452    public static void main(String[] args) {
453        for (String string : TestStrings) {
454            System.out.println(unescapeSSID(string));
455        }
456    }
457}
458