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