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