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